esop-ext.js (8232B)
1 /** 2 * ESOP: Essential Stack-Operated Phone 3 * JS implementation 4 * @version 0.0.1 5 * @author Luxferre 6 * @license Unlicense <https://unlicense.org> 7 */ 8 9 //esop-ext 10 //Extension setup according to the main spec 11 12 function ESOPExtensions() { 13 14 //system 15 function setupSys(vm, configObj) { 16 vm.config = configObj 17 vm.wst=0xfc00 //no effect for now 18 vm.rst=0xfd00 //no effect for now 19 vm.devMemOffset = 0xfff8 20 vm.videoMemOffset = 0xfe00 21 vm.videoMemSize = 504 22 //setup RNG 23 var getRandomByte = ('crypto' in window) ? function() { 24 return window.crypto.getRandomValues(new Uint8Array(1))[0] 25 } : function() {return Math.random()*256|0} 26 vm.tracePort = 0x04 //setup the trace/random port 27 vm.setReadHook(0x04, function() { //update the random byte port before the program consumes it 28 vm.setdev(0x04, getRandomByte()) 29 }) 30 //setup status port 31 vm.setReadHook(0x05, function() { 32 vm.setdev(0x05, 0xff) //emulate full brightness, backlit keypad, full charge and charger plugged in 33 }) 34 } 35 36 //graphics 37 function setupScreen(vm, cnv) { 38 var width = 84, 39 height = 48, 40 vramOffset = vm.videoMemOffset, 41 vramSize = vm.videoMemSize, 42 ctx = cnv.getContext('2d', {alpha: false}), 43 xval = null, yval = null 44 ctx.globalAlpha = 1 45 //setup pixel drawing ports 46 function pixelHook(port, coord, prev) { 47 if(port === 2) xval = coord 48 else if(port === 3) yval = coord 49 if(xval !== null && yval !== null) { //both coordinates set, update the pixel 50 var pxlOffset = yval * width + xval, 51 byteOffset = pxlOffset >>> 3, 52 bitOffset = 7 - pxlOffset + (byteOffset << 3), 53 vramvalue = vm.getram(vramOffset + byteOffset, 1)[0] 54 vm.setram(vramOffset + byteOffset, vramvalue | (1<<bitOffset)) 55 xval = yval = null 56 } 57 vm.setdev(port, prev) //preserve the input value 58 } 59 vm.setWriteHook(2, pixelHook) //X 60 vm.setWriteHook(3, pixelHook) //Y 61 //setup screen renderer 62 function renderScreen() { 63 var vram = vm.getram(vramOffset, vramSize), 64 vdata = ctx.createImageData(width,height), 65 ppos, pval, vbval, i, j 66 for(i=0;i<vramSize;i++) { 67 vbval = vram[i] 68 for(j=0;j<8;j++) { //iterate from highest to lowest 69 ppos = ((i<<3) + j) << 2 70 pval = (vbval>>>(7-j))&1 ? 0 : 0xee 71 vdata.data[ppos] = pval 72 vdata.data[ppos+1] = pval 73 vdata.data[ppos+2] = pval 74 vdata.data[ppos+3] = 255 75 } 76 } 77 ctx.putImageData(vdata, 0,0) 78 } 79 // also, we need to set up the screen vector 80 var rAF = window.requestAnimationFrame || window.webkitRequestAnimationFrame 81 rAF(function vecframe() { 82 //the order is: clear - vector - render 83 vm.setramblk(vramOffset, new Uint8Array(vramSize)) 84 vm.triggerVector(0) 85 renderScreen() 86 if(vm.active) 87 rAF(vecframe) 88 }) 89 } 90 91 //keypad input 92 93 var livemap = { //map keys to bit values (to be updated in one of 4 ports) 94 '0': 0x0, '1': 0x1, '2': 0x2, '3': 0x3, 95 '4': 0x4, '5': 0x5, '6': 0x6, '7': 0x7, 96 '8': 0x8, '9': 0x9, '*': 0xe, '#': 0xf, 97 'ArrowUp': 0xa, //A - Advance 98 'ArrowDown': 0xb, //B - Back 99 'Backspace': 0xc, //C - Cancel 100 'Enter': 0xd //D - Do 101 } 102 103 //set PC and KaiOS aliases if necessary 104 livemap['a'] = livemap['ArrowUp'] 105 livemap['b'] = livemap['ArrowDown'] 106 livemap['c'] = livemap['EndCall'] = livemap['End'] = livemap['Backspace'] 107 livemap['d'] = livemap['Enter'] 108 109 function setupInput(vm) { 110 function keypress(e) { 111 var k = e.key 112 if(k in livemap) { 113 var keyval = livemap[k], 114 portnum = keyval>7 ? 2 : 3, 115 bitmask = 1<<(keyval&7) 116 vm.setdev(portnum, vm.getdev(portnum) | bitmask) 117 } 118 } 119 function keyrelease(e) { 120 var k = e.key 121 if(k in livemap) { 122 var keyval = livemap[k], 123 portnum = keyval>7 ? 2 : 3, 124 bitmask = (~(1<<(keyval&7)))&255 125 vm.setdev(portnum, vm.getdev(portnum) & bitmask) 126 } 127 } 128 window.removeEventListener('keydown', keypress) 129 window.removeEventListener('keyup', keyrelease) 130 window.addEventListener('keydown', keypress) 131 window.addEventListener('keyup', keyrelease) 132 vm.setdev(2, 0) 133 vm.setdev(3, 0) 134 } 135 136 //sound 137 138 //keep sound frequency matrix for all 87 non-zero sound port values with A4 aligned to 0x30 position 139 var soundFreqs = [ 140 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, 141 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, 142 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, 143 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, 144 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, 145 2959.96,3135.96,3322.44,3520,3729.31,3951.07,4186.01 146 ], sfl = soundFreqs.length 147 148 function setupAudio(vm, actx) { 149 var osc = null //keep the existing oscillator reference here 150 vm.setdev(6,0) 151 vm.setWriteHook(6, function(port, portval, prev) { 152 if(portval !== prev) { 153 if(osc && !portval) { //stop the note 154 osc.stop() 155 osc.disconnect() 156 osc = null 157 } 158 if(portval && portval < sfl) { //start the note 159 if(osc) 160 osc.frequency.value = soundFreqs[portval] 161 else { 162 osc = actx.createOscillator() 163 osc.type = 'square' 164 osc.connect(actx.destination) 165 osc.frequency.value = soundFreqs[portval] 166 osc.start(0) 167 } 168 } 169 } 170 }) 171 } 172 173 //syscalls 174 175 //thanks to Toastrackenigma for the idea 176 function isDST(d, y) { 177 var jan = new Date(y, 0, 1).getTimezoneOffset(), 178 jul = new Date(y, 6, 1).getTimezoneOffset() 179 return Math.max(jan, jul) !== d.getTimezoneOffset() 180 } 181 //thanks to user2501097 for the idea 182 function daysIntoYear(y, m, d) { 183 return (Date.UTC(y, m, d) - Date.UTC(y, 0, 0)) / 86400000 184 } 185 186 function fillDTbuf(vm, addr) { 187 var now = new Date(), 188 year = now.getFullYear(), 189 month = now.getMonth() + 1, 190 day = now.getDate(), 191 hour = now.getHours(), 192 minute = now.getMinutes(), 193 second = now.getSeconds(), 194 dotw = now.getDay(), 195 doty = daysIntoYear(year, month - 1, day), 196 dstflag = 0|isDST(now, year) 197 vm.setram(addr, year >> 8) 198 vm.setram(addr+1, year&255) 199 vm.setram(addr+2, month) 200 vm.setram(addr+3, day) 201 vm.setram(addr+4, hour) 202 vm.setram(addr+5, minute) 203 vm.setram(addr+6, second) 204 vm.setram(addr+7, dotw) 205 vm.setram(addr+8, doty >> 8) 206 vm.setram(addr+9, doty&255) 207 vm.setram(addr+10, dstflag) 208 209 } 210 211 function runSyscall(vm, buf) { 212 var call = buf[0] 213 if(call === 0x00) //simulate writing a byte to the debug port 214 console.log('[DBG] ' + ('00' + buf[1].toString(16)).slice(-2)) 215 else if(call === 0x06) { //read datetime info into 10 bytes starting from addr 216 fillDTbuf(vm, (buf[1]<<8)|buf[2]) 217 } 218 else if(call === 0x1f) //halt call, required for all implementations 219 vm.active = false 220 } 221 222 function setupSyscalls(vm) { 223 var syscallBuf = new Uint8Array(9), expectedParams = 0, paramId = 0 224 vm.setdev(7, 0) 225 vm.setWriteHook(7, function(port, portval) { 226 if(expectedParams) { //already expecting a parameter, push there 227 syscallBuf[paramId++] = portval 228 expectedParams-- 229 } else if(portval) { //start constructing a new syscall 230 syscallBuf[0] = portval&31 231 expectedParams = portval >> 5 232 paramId = 1 233 } 234 if(!expectedParams && paramId) { //all parameters used up but the call didn't complete 235 runSyscall(vm, syscallBuf) 236 paramId = 0 237 expectedParams = 0 238 syscallBuf.fill(0) 239 } 240 }) 241 } 242 243 return { 244 setup: function(vm, cfg) { 245 setupSys(vm, cfg) 246 setupScreen(vm, cfg.canvas) 247 setupInput(vm) 248 setupSyscalls(vm) 249 setupAudio(vm, cfg.audio) 250 } 251 } 252 }