esop

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

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 }