esop

Essential Stack-Operated Phone (concept)
git clone git://git.luxferre.top/esop.git
Log | Files | Refs | README | LICENSE

commit 08ed67ff95207e10fda4e683dd0be1b3caf56d61
parent 5a98969bce57dc9fab87f3a1139e0d82ca3d5bc7
Author: Luxferre <lux@ferre>
Date:   Thu, 28 Jul 2022 18:14:21 +0300

First web implementation draft of the draft

Diffstat:
MREADME.md | 7++++---
Aweb/esop-ext.js | 213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aweb/esop-web-app.js | 28++++++++++++++++++++++++++++
Aweb/esop-web.html | 34++++++++++++++++++++++++++++++++++
Aweb/uxncore.js | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 470 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md @@ -87,7 +87,7 @@ The lower nibble (bits 0 to 3) of the status can be modified from the applicatio ## Sound output -ESOP supports monophonic sound output with a beeper controllable via `0x06` port. As long as the port value is non-zero, the beeper must emit a sound wave of the specified frequency. The frequency is specified in semitones relative to A4 (440 Hz), with A4 itself being located at `0x80` value. +ESOP supports monophonic sound output with a beeper controllable via `0x06` port. As long as the port value is non-zero, the beeper must emit a sound wave of the specified frequency. The frequency is specified in semitones relative to A4 (440 Hz), with A4 itself being located at `0x30` value. ## System calls @@ -107,6 +107,7 @@ The only system call required to be implemented in any ESOP-compatible runtime i - serial/debug port I/O; - persistent/flash memory I/O (up to 16 MB), operating with 256-byte pages or individual bytes; +- setting and getting datetime information; - initiating and receiving GSM voice calls; - sending DTMF signals during active voice calls; - active voice call manipulation (terminate, hold/unhold, bridge); @@ -130,8 +131,8 @@ ID |Params |Command|Meaning `0x03`|1s 3b [addr addr]|`0xa3` |Read a byte under the 3-byte flash address `0x04`|1b 3b [page addr]|`0x84` |Write a page under the 3-byte flash address `0x05`|1b 3b [page addr]|`0x85` |Read a page under the 3-byte flash address -`0x06`|||(reserved) -`0x07`|||(reserved) +`0x06`|1s [addr] |`0x46` |Read the current system time/date information (5 bytes/40 bits, Unix time) into the address +`0x07`|1s [addr] |`0x47` |Set the current system time/date information (5 bytes/40 bits, Unix time) from the address `0x08`|||(reserved) `0x09`|||(reserved) `0x0a`|||(reserved) diff --git a/web/esop-ext.js b/web/esop-ext.js @@ -0,0 +1,213 @@ +/** + * ESOP: Essential Stack-Operated Phone + * JS implementation + * @version 0.0.1 + * @author Luxferre + * @license Unlicense <https://unlicense.org> + */ + +//esop-ext +//Extension setup according to the main spec + +function ESOPExtensions() { + + //system + function setupSys(vm, configObj) { + vm.config = configObj + vm.wst=0xfc00 //no effect for now + vm.rst=0xfd00 //no effect for now + vm.devMemOffset = 0xfff8 + vm.videoMemOffset = 0xfe00 + vm.videoMemSize = 504 + + //setup RNG + var getRandomByte = ('crypto' in window) ? function() { + return window.crypto.getRandomValues(new Uint8Array(1))[0] + } : function() {return Math.random()*256|0} + + vm.tracePort = 0x04 //setup the trace/random port + vm.setReadHook(0x04, function() { //update the random byte port before the program consumes it + vm.setdev(0x04, getRandomByte()) + }) + + //setup status port + vm.setReadHook(0x05, function() { + vm.setdev(0x05, 0xff) //emulate full brightness, full charge and charger plugged in + }) + + } + + //graphics + function setupScreen(vm, cnv) { + var width = 84, + height = 48, + vramOffset = vm.videoMemOffset, + vramSize = vm.videoMemSize, + ctx = cnv.getContext('2d', {alpha: false}) + ctx.globalAlpha = 1 + + //setup screen renderer + + function renderScreen() { + var vram = vm.getram(vramOffset, vramSize), + vdata = ctx.getImageData(0,0,width,height), + ppos, pval, vbval, i, j + for(i=0;i<vRamSize;i++) { + vbval = vram[i] + for(j=0;j<8;j++) { //iterate from highest to lowest + ppos = ((i<<3) + j) << 2 + pval = (vbval>>>(7-j))&1 ? 255 : 0 + vdata.data[ppos] = pval + vdata.data[ppos+1] = pval + vdata.data[ppos+2] = pval + vdata.data[ppos+3] = 255 + } + } + ctx.putImageData(vdata, 0,0) + } + + // also, we need to set up the screen vector + + var rAF = window.requestAnimationFrame || window.webkitRequestAnimationFrame + rAF(function vecframe() { + vm.triggerVector(0) + renderScreen() + if(vm.active) + rAF(vecframe) + }) + + } + + //keypad input + + var livemap = { //map keys to bit values (to be updated in one of 4 ports) + '0': 0x0, + '1': 0x1, + '2': 0x2, + '3': 0x3, + '4': 0x4, + '5': 0x5, + '6': 0x6, + '7': 0x7, + '8': 0x8, + '9': 0x9, + '*': 0xe, + '#': 0xf, + 'ArrowUp': 0xa, //A - Advance + 'ArrowDown': 0xb, //B - Back + 'Backspace': 0xc, //C - Cancel + 'Enter': 0xd //D - Do + } + + //set KaiOS aliases if necessary + livemap['End'] = livemap['Backspace'] + livemap['EndCall'] = livemap['End'] + + //now remap the keys for PC according to the spec recommendation + + livemap['a'] = livemap['ArrowUp'] + livemap['b'] = livemap['ArrowDown'] + livemap['c'] = livemap['Backspace'] + livemap['d'] = livemap['Enter'] + + function setupInput(vm) { + + function keypress(e) { + var k = e.key + if(k in livemap) { + var keyval = livemap[k], + portnum = keyval>7 ? 3 : 2, + bitmask = 1<<(keyval&7) + vm.setdev(portnum, vm.getdev(portnum) | bitmask) + } + } + function keyrelease(e) { + var k = e.key + if(k in livemap) { + var keyval = livemap[k], + portnum = keyval>7 ? 3 : 2, + bitmask = (~(1<<(keyval&7)))&255 + vm.setdev(portnum, vm.getdev(portnum) & bitmask) + } + } + window.removeEventListener('keydown', keypress) + window.removeEventListener('keyup', keyrelease) + window.addEventListener('keydown', keypress) + window.addEventListener('keyup', keyrelease) + + vm.setdev(2, 0) + vm.setdev(3, 0) + } + + //keep sound frequency matrix for all 87 non-zero sound port values with A4 aligned to 0x30 position + var soundFreqs = [0,29.14,30.87,32.7,34.65,36.71,38.89,41.2,43.65,46.25,49,51.91,55,58.27,61.74,65.41,69.3,73.42,77.78,82.41,87.31,92.5,98,103.83,110,116.54,123.47,130.81,138.59,146.83,155.56,164.81,174.61,185,196,207.65,220,233.08,246.94,261.63,277.18,293.66,311.13,329.63,349.23,369.99,392,415.3,440,466.16,493.88,523.25,554.37,587.33,622.25,659.26,698.46,739.99,783.99,830.61,880,932.33,987.77,1046.5,1108.73,1174.66,1244.51,1318.51,1396.91,1479.98,1567.98,1661.22,1760,1864.66,1975.53,2093,2217.46,2349.32,2489.02,2637.02,2793.83,2959.96,3135.96,3322.44,3520,3729.31,3951.07,4186.01], sfl = soundFreqs.length + + function setupAudio(vm, actx) { + var osc = null //keep the existing oscillator reference here + vm.setdev(6,0) + vm.setWriteHook(6, function(port, portval) { + if(osc && !portval) { //stop the note + osc.stop() + osc.disconnect() + osc = null + } + else if(portval < sfl) { //start the note + osc = actx.createOscillator() + osc.type = 'square' + osc.frequency = soundFreqs[portval] + osc.connect(actx.destination) + osc.start(0) + } + }) + } + + function runSyscall(vm, buf) { + var call = buf[0] + if(call === 0x00) //simulate writing a byte to the debug port + console.log(('[DBG] 00' + buf[1].toString(16)).slice(-2)) + else if(call === 0x06) { //read datetime info into 5 bytes starting from addr + //not very optimal but a future-proof approach for now + var tm = ('00' + (0|(Date.now()/1000)).toString(16)).slice(-10).match(/.{2}/g).map(function(x){return parseInt(x, 16)}), + targetaddr = (buf[1]<<8)|buf[2] + vm.setram(targetaddr, tm[0]) + vm.setram(targetaddr+1, tm[1]) + vm.setram(targetaddr+2, tm[2]) + vm.setram(targetaddr+3, tm[3]) + vm.setram(targetaddr+4, tm[4]) + } + else if(call === 0x1f) //halt call, required for all implementations + vm.active = false + } + + function setupSyscalls(vm) { + var syscallBuf = new Uint8Array(9), expectedParams = 0, paramId = 0 + vm.setdev(7, 0) + vm.setWriteHook(7, function(port, portval) { + if(expectedParams) { //already expecting a parameter, push there + syscallBuf[paramId++] = portval + expectedParams-- + } else if(portval) { //start constructing a new syscall + syscallBuf[0] = portval&31 + expectedParams = portval >> 5 + paramId = 1 + } + if(!expectedParams && paramId) { //all parameters used up but the call didn't complete + runSyscall(vm, syscallBuf) + paramId = 0 + expectedParams = 0 + syscallBuf.fill(0) + } + }) + } + + return { + setup: function(vm, cfg) { + setupSys(vm, cfg) + setupScreen(vm, cfg.canvas) + setupInput(vm) + setupSyscalls(vm) + setupAudio(vm, cfg.audio) + } + } +} + diff --git a/web/esop-web-app.js b/web/esop-web-app.js @@ -0,0 +1,28 @@ +//app part not related to ESOP JS implementation + +function main() { + var devCfg = { + audio: window.audioContext || window.webkitAudioContext, + canvas: document.getElementById('C') //JS-specific implementation canvas DOM reference + } + + document.getElementById('apprun').addEventListener('click', function() { + var fileObj = document.getElementById('appselect').files[0] + if(fileObj.name.endsWith('.eso')) { + var rdr = new FileReader() + rdr.onload = function() { + var vm = UxnCore() + var ext = ESOPExtensions() + vm.boot() + ext.setup(vm, devCfg) + vm.load(new Uint8Array(rdr.result)) + console.log('VM started') + vm.start(0x100) + } + rdr.readAsArrayBuffer(fileObj) + } + else window.alert('Please select an .eso file to run on ESOP') + }) +} + +addEventListener('DOMContentLoaded', main) diff --git a/web/esop-web.html b/web/esop-web.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>ESOP Web ref test</title> + <meta name=viewport content="width=device-width"> +<style> +html, body { + margin: 0; + padding: 0; + text-align: center; +} +#C { + width: 240px; + height: auto; + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-crisp-edges; + image-rendering: pixelated; + image-rendering: crisp-edges; +} +</style> + </head> + <body> + <h1>Essential Stack-Operated Phone</h1> + <canvas id=C width=84 height=48></canvas> + <div> + <input type=file id=appselect><br> + <button type=button id=apprun>Run!</button> + </div> + <script src=uxncore.js></script> + <script src=esop-ext.js></script> + <script src=esop-web-app.js></script> + </body> +</html> diff --git a/web/uxncore.js b/web/uxncore.js @@ -0,0 +1,191 @@ +/** + * Uxn core JS implementation for ESOP VM + * @version 0.0.1 + * @author Luxferre + * @license Unlicense <https://unlicense.org> + */ + +function UxnCore() { + + function initMem() { + return { + ram: new Uint8Array(65536), //main RAM + wst: {ptr: 0, pk: 0, dat: new Uint8Array(256)}, //working stack + rst: {ptr: 0, pk: 0, dat: new Uint8Array(256)}, //return stack + dev: new Uint8Array(256) //device memory + } + } + + var mem = null, + bs = 0, //byte/short flag + km = 0, //keep mode flag + pc = 0, //program counter + devWriteHooks = {}, + devReadHooks = {} + + //helper methods + + function trapout(errcode) { //error reporting with vector execution + var vec = (mem.dev[0] << 8) | mem.dev[1] + if(vec) + uxnstart(vec) + else { + console.error('Error code: '+errcode+' at instruction #'+pc) + console.log('Working stack: ', mem.wst.dat) + console.log('Return stack: ', mem.rst.dat) + } + pc = 0 + } + + function rel(x) {return x > 0x80 ? x - 256 : x} + + function PUSH8(s, x) {if(s.ptr === 255) trapout(2);s.dat[s.ptr++] = x} + function PUSH16(s, x) {PUSH8(s, x>>8);PUSH8(s, x&255)} + function PUSH(s, x) {if(bs) PUSH16(s, x);else PUSH8(s, x)} + function POP8(s) { + var ptr = km ? --s.pk : --s.ptr + if(ptr < 0) trapout(1) + return s.dat[ptr] + } + function POP16(s) {return POP8(s) | (POP8(s)<<8)} + function POP(s) {return bs ? POP16(s) : POP8 (s)} + function JUMP(x, pc) {return bs ? x : (pc + rel(x))} + function POKE(x, y) { + if(bs) { + mem.ram[x] = y>>8 + mem.ram[x+1] = y&255 + } + else mem.ram[x] = y + } + function PEEK16(x) {return (mem.ram[x]<<8) | mem.ram[x+1]} + function PEEK(x) {return bs ? PEEK16(x) : mem.ram[x]} + function DEVR(x) {if(x in devReadHooks) devReadHooks[x]();return bs ? (mem.dev[x] << 8)|mem.dev[x+1] : mem.dev[x]} + function DEVW(x, y) { + if(bs) { + mem.dev[x] = y>>8 + if(x in devWriteHooks) + devWriteHooks[x](x, mem.dev[x]) + mem.dev[x+1] = y&255 + if((x+1) in devWriteHooks) + devWriteHooks[x+1](x+1, mem.dev[x+1]) + } + else { + mem.dev[x] = y + if(x in devWriteHooks) + devWriteHooks[x](x, y) + } + } + + // main eval loop + + function uxnstart(startaddr, vmTrace) { + pc = startaddr & 65535 + var instr, src, dst, a, b, c, tmpTrace = !!vmTrace + if(!pc || mem.dev[0x0f]) { + this.active = false + return 0 + } + this.active = true + while(instr = mem.ram[pc++]) { + bs = (instr & 0x20) ? 1 : 0 //short mode + if(instr & 0x40) { //return mode + src = mem.rst + dst = mem.wst + } else { + src = mem.wst + dst = mem.rst + } + if(instr & 0x80) { //keep mode + km = 1 + src.pk = src.ptr + dst.pk = dst.ptr + } else km = 0 + // main instruction selector + switch(instr & 0x1f) { + // Stack + case 0x00: /* LIT */ PUSH(src, PEEK(pc)); pc += bs + 1; break; + case 0x01: /* INC */ PUSH(src, POP(src) + 1); break; + case 0x02: /* POP */ POP(src); break; + case 0x03: /* NIP */ a = POP(src); POP(src); PUSH(src, a); break; + case 0x04: /* SWP */ a = POP(src); b = POP(src); PUSH(src, a); PUSH(src, b); break; + case 0x05: /* ROT */ a = POP(src); b = POP(src); c = POP(src); PUSH(src, b); PUSH(src, a); PUSH(src, c); break; + case 0x06: /* DUP */ a = POP(src); PUSH(src, a); PUSH(src, a); break; + case 0x07: /* OVR */ a = POP(src); b = POP(src); PUSH(src, b); PUSH(src, a); PUSH(src, b); break; + // Logic + case 0x08: /* EQU */ a = POP(src); b = POP(src); PUSH8(src, 0|(b == a)); break; + case 0x09: /* NEQ */ a = POP(src); b = POP(src); PUSH8(src, 0|(b != a)); break; + case 0x0a: /* GTH */ a = POP(src); b = POP(src); PUSH8(src, 0|(b > a)); break; + case 0x0b: /* LTH */ a = POP(src); b = POP(src); PUSH8(src, 0|(b < a)); break; + case 0x0c: /* JMP */ pc = JUMP(POP(src), pc); break; + case 0x0d: /* JCN */ a = POP(src); if(POP8(src)) pc = JUMP(a, pc); break; + case 0x0e: /* JSR */ PUSH16(dst, pc); pc = JUMP(POP(src), pc); break; + case 0x0f: /* STH */ PUSH(dst, POP(src)); break; + // Memory + case 0x10: /* LDZ */ PUSH(src, PEEK(POP8(src))); break; + case 0x11: /* STZ */ POKE(POP8(src), POP(src)); break; + case 0x12: /* LDR */ PUSH(src, PEEK(pc + rel(POP8(src)))); break; + case 0x13: /* STR */ POKE(pc + rel(POP8(src)), POP(src)); break; + case 0x14: /* LDA */ PUSH(src, PEEK(POP16(src))); break; + case 0x15: /* STA */ POKE(POP16(src), POP(src)); break; + case 0x16: /* DEI */ PUSH(src, DEVR(POP8(src))); break; + case 0x17: /* DEO */ a = POP8(src); b = POP(src); if(a===this.tracePort) tmpTrace=true;else DEVW(a, b); break; + // Arithmetic + case 0x18: /* ADD */ a = POP(src); b = POP(src); PUSH(src, b + a); break; + case 0x19: /* SUB */ a = POP(src); b = POP(src); PUSH(src, b - a); break; + case 0x1a: /* MUL */ a = POP(src); b = POP(src); PUSH(src, b * a); break; + case 0x1b: /* DIV */ a = POP(src); b = POP(src); if(!a) trapout(3); PUSH(src, b / a); break; + case 0x1c: /* AND */ a = POP(src); b = POP(src); PUSH(src, b & a); break; + case 0x1d: /* ORA */ a = POP(src); b = POP(src); PUSH(src, b | a); break; + case 0x1e: /* EOR */ a = POP(src); b = POP(src); PUSH(src, b ^ a); break; + case 0x1f: /* SFT */ a = POP8(src); b = POP(src); PUSH(src, b >> (a & 0x0f) << ((a & 0xf0) >> 4)); break; + } + if(vmTrace || tmpTrace) { + console.log('Step: ', pc, 'Instruction:', instr.toString(16), 'Base instr:', (instr&0x1f).toString(16), 'stackptr:', src.ptr, 'stack:', src.dat) + tmpTrace = vmTrace + } + } + } + + return { + active: false, + boot: function() { + this.active = false + this.tracePort = 0x0e + this.memDevOffset = 0 + mem = initMem() + bs = 0 + pc = 0 + km = 0 + devWriteHooks = {} + devReadHooks = {} + }, + load: function(prog, addr) { + if(!addr) addr = 0x100 + for(var i=0, l=prog.length;i<l;i++) + mem.ram[addr+i] = prog[i] + }, + start: uxnstart, + setdev: function(port, val) { + mem.dev[port] = val + if(this.memDevOffset) + mem.ram[this.memDevOffset+port] = val + }, + getdev: function(port) {return mem.dev[port]}, + getram: function(start, length) {return mem.ram.subarray(start, start+length)}, + setram: function(addr, bVal) {mem.ram[addr] = bVal}, + setWriteHook: function(port, cb) { + if(!cb) cb = function(){} + devWriteHooks[port] = cb + }, + setReadHook: function(devPort, cb) { + if(!cb) cb = function(){} + devReadHooks[devPort] = cb + }, + triggerVector: function(devId) { + var basePos = (devId >> 4) << 4 + var targetAddr = (mem.dev[basePos] << 8) | mem.dev[basePos + 1] + if(targetAddr) uxnstart(targetAddr) + } + } + +}