/** * ESOP: Essential Stack-Operated Phone * JS implementation * @version 0.0.1 * @author Luxferre * @license Unlicense */ //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, backlit keypad, 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}), xval = null, yval = null ctx.globalAlpha = 1 //setup pixel drawing ports function pixelHook(port, coord, prev) { if(port === 2) xval = coord else if(port === 3) yval = coord if(xval !== null && yval !== null) { //both coordinates set, update the pixel var pxlOffset = yval * width + xval, byteOffset = pxlOffset >>> 3, bitOffset = 7 - pxlOffset + (byteOffset << 3), vramvalue = vm.getram(vramOffset + byteOffset, 1)[0] vm.setram(vramOffset + byteOffset, vramvalue | (1<>>(7-j))&1 ? 0 : 0xee 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() { //the order is: clear - vector - render vm.setramblk(vramOffset, new Uint8Array(vramSize)) 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 PC and KaiOS aliases if necessary livemap['a'] = livemap['ArrowUp'] livemap['b'] = livemap['ArrowDown'] livemap['c'] = livemap['EndCall'] = livemap['End'] = 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 ? 2 : 3, 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 ? 2 : 3, 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) } //sound //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, prev) { if(portval !== prev) { if(osc && !portval) { //stop the note osc.stop() osc.disconnect() osc = null } if(portval && portval < sfl) { //start the note if(osc) osc.frequency.value = soundFreqs[portval] else { osc = actx.createOscillator() osc.type = 'square' osc.connect(actx.destination) osc.frequency.value = soundFreqs[portval] osc.start(0) } } } }) } //syscalls //thanks to Toastrackenigma for the idea function isDST(d, y) { var jan = new Date(y, 0, 1).getTimezoneOffset(), jul = new Date(y, 6, 1).getTimezoneOffset() return Math.max(jan, jul) !== d.getTimezoneOffset() } //thanks to user2501097 for the idea function daysIntoYear(y, m, d) { return (Date.UTC(y, m, d) - Date.UTC(y, 0, 0)) / 86400000 } function fillDTbuf(vm, addr) { var now = new Date(), year = now.getFullYear(), month = now.getMonth() + 1, day = now.getDate(), hour = now.getHours(), minute = now.getMinutes(), second = now.getSeconds(), dotw = now.getDay(), doty = daysIntoYear(year, month - 1, day), dstflag = 0|isDST(now, year) vm.setram(addr, year >> 8) vm.setram(addr+1, year&255) vm.setram(addr+2, month) vm.setram(addr+3, day) vm.setram(addr+4, hour) vm.setram(addr+5, minute) vm.setram(addr+6, second) vm.setram(addr+7, dotw) vm.setram(addr+8, doty >> 8) vm.setram(addr+9, doty&255) vm.setram(addr+10, dstflag) } 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 10 bytes starting from addr fillDTbuf(vm, (buf[1]<<8)|buf[2]) } 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) } } }