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:
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)
+ }
+ }
+
+}