224 lines
7.3 KiB
JavaScript
224 lines
7.3 KiB
JavaScript
/**
|
|
* 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, 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<<bitOffset))
|
|
xval = yval = null
|
|
}
|
|
vm.setdev(port, prev) //preserve the input value
|
|
}
|
|
vm.setWriteHook(2, pixelHook) //X
|
|
vm.setWriteHook(3, pixelHook) //Y
|
|
//setup screen renderer
|
|
function renderScreen() {
|
|
var vram = vm.getram(vramOffset, vramSize),
|
|
vdata = ctx.createImageData(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 ? 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 ? 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)
|
|
}
|
|
|
|
//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
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|