From 92c4c2ff9804c0f1d9c72408e4ea28befb0f85b5 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 15 Sep 2017 22:22:07 +0200 Subject: [PATCH] Make Term Conn an EventEmitter, add status screen --- js/term.js | 2 +- js/term_conn.js | 125 ++++++++++++++++++++++++---------------------- js/term_screen.js | 79 ++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 63 deletions(-) diff --git a/js/term.js b/js/term.js index e9c5981..bb6824e 100644 --- a/js/term.js +++ b/js/term.js @@ -1,7 +1,7 @@ /** Init the terminal sub-module - called from HTML */ window.termInit = function ({ labels, theme, allFn }) { const screen = new TermScreen() - const conn = Conn(screen) + const conn = new Conn(screen) const input = Input(conn) const termUpload = TermUpl(conn, input, screen) screen.input = input diff --git a/js/term_conn.js b/js/term_conn.js index 37b2800..d5c3ba7 100644 --- a/js/term_conn.js +++ b/js/term_conn.js @@ -1,31 +1,38 @@ /** Handle connections */ -window.Conn = function (screen) { - let ws - let heartbeatTout - let pingIv - let xoff = false - let autoXoffTout - let reconTout - - let pageShown = false +window.Conn = class TermConnection extends EventEmitter { + constructor (screen) { + super() + + this.screen = screen + this.ws = null + this.heartbeatTimeout = null + this.pingInterval = null + this.xoff = false + this.autoXoffTimeout = null + this.reconTimeout = null + + this.pageShown = false + } - function onOpen (evt) { + onWSOpen (evt) { console.log('CONNECTED') - heartbeat() - doSend('i') + this.heartbeat() + this.send('i') + + this.emit('open') } - function onClose (evt) { + onWSClose (evt) { console.warn('SOCKET CLOSED, code ' + evt.code + '. Reconnecting...') - clearTimeout(reconTout) - reconTout = setTimeout(function () { - init() - }, 2000) + clearTimeout(this.reconTimeout) + this.reconTimeout = setTimeout(() => this.init(), 2000) // this happens when the buffer gets fucked up via invalid unicode. // we basically use polling instead of socket then + + this.emit('close', evt.code) } - function onMessage (evt) { + onWSMessage (evt) { try { // . = heartbeat switch (evt.data.charAt(0)) { @@ -35,66 +42,66 @@ window.Conn = function (screen) { case '-': // console.log('xoff'); - xoff = true - autoXoffTout = setTimeout(function () { - xoff = false + this.xoff = true + this.autoXoffTimeout = setTimeout(() => { + this.xoff = false }, 250) break case '+': // console.log('xon'); - xoff = false - clearTimeout(autoXoffTout) + this.xoff = false + clearTimeout(this.autoXoffTimeout) break default: - screen.load(evt.data) - if (!pageShown) { + this.screen.load(evt.data) + if (!this.pageShown) { showPage() - pageShown = true + this.pageShown = true } break } - heartbeat() + this.heartbeat() } catch (e) { console.error(e) } } - function canSend () { - return !xoff + canSend () { + return !this.xoff } - function doSend (message) { - if (_demo) { - if (typeof demoInterface !== 'undefined') { + send (message) { + if (window._demo) { + if (typeof window.demoInterface !== 'undefined') { demoInterface.input(message) } else { console.log(`TX: ${JSON.stringify(message)}`) } return true // Simulate success } - if (xoff) { + if (this.xoff) { // TODO queue console.log("Can't send, flood control.") return false } - if (!ws) return false // for dry testing - if (ws.readyState !== 1) { + if (!this.ws) return false // for dry testing + if (this.ws.readyState !== 1) { console.error('Socket not ready') return false } if (typeof message != 'string') { message = JSON.stringify(message) } - ws.send(message) + this.ws.send(message) return true } - function init () { + init () { if (window._demo) { - if (typeof demoInterface === 'undefined') { + if (typeof window.demoInterface === 'undefined') { alert('Demoing non-demo demo!') // this will catch mistakes when deploying to the website } else { demoInterface.init(screen) @@ -103,42 +110,40 @@ window.Conn = function (screen) { return } - clearTimeout(reconTout) - clearTimeout(heartbeatTout) + clearTimeout(this.reconTimeout) + clearTimeout(this.heartbeatTimeout) - ws = new WebSocket('ws://' + _root + '/term/update.ws') - ws.onopen = onOpen - ws.onclose = onClose - ws.onmessage = onMessage + this.ws = new WebSocket('ws://' + _root + '/term/update.ws') + this.ws.addEventListener('open', (...args) => this.onWSOpen(...args)) + this.ws.addEventListener('close', (...args) => this.onWSClose(...args)) + this.ws.addEventListener('message', (...args) => this.onWSMessage(...args)) console.log('Opening socket.') - heartbeat() + this.heartbeat() + + this.emit('connect') } - function heartbeat () { - clearTimeout(heartbeatTout) - heartbeatTout = setTimeout(heartbeatFail, 2000) + heartbeat () { + clearTimeout(this.heartbeatTimeout) + this.heartbeatTimeout = setTimeout(() => this.onHeartbeatFail(), 2000) } - function heartbeatFail () { + onHeartbeatFail () { console.error('Heartbeat lost, probing server...') - pingIv = setInterval(function () { + clearInterval(this.pingInterval) + this.pingInterval = setInterval(() => { console.log('> ping') - $.get('http://' + _root + '/system/ping', function (resp, status) { + this.emit('ping') + $.get('http://' + _root + '/system/ping', (resp, status) => { if (status === 200) { - clearInterval(pingIv) + clearInterval(this.pingInterval) console.info('Server ready, reloading page...') + this.emit('ping-success') location.reload() - } + } else this.emit('ping-fail', status) }, { timeout: 100 }) }, 1000) } - - return { - ws: null, - init, - send: doSend, - canSend // check flood control - } } diff --git a/js/term_screen.js b/js/term_screen.js index 939fdc8..615e6a9 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -114,7 +114,8 @@ window.TermScreen = class TermScreen extends EventEmitter { fitIntoWidth: 0, fitIntoHeight: 0, debug: false, - graphics: 0 + graphics: 0, + statusScreen: null } // scaling caused by fitIntoWidth/fitIntoHeight @@ -879,9 +880,17 @@ window.TermScreen = class TermScreen extends EventEmitter { height, devicePixelRatio, gridScaleX, - gridScaleY + gridScaleY, + statusScreen } = this.window + if (statusScreen) { + // draw status screen instead + this.drawStatus(statusScreen) + this.startDrawLoop() + return + } else this.stopDrawLoop() + const charSize = this.getCharSize() const { width: cellWidth, height: cellHeight } = this.getCellSize() const screenWidth = width * cellWidth @@ -1085,6 +1094,72 @@ window.TermScreen = class TermScreen extends EventEmitter { if (this.window.debug && this._debug) this._debug.drawEnd() } + drawStatus (statusScreen) { + const ctx = this.ctx + const { + fontFamily, + width, + height + } = this.window + + // reset drawnScreen to force redraw when statusScreen is disabled + this.drawnScreen = [] + + const cellSize = this.getCellSize() + const screenWidth = width * cellSize.width + const screenHeight = height * cellSize.height + + ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) + ctx.clearRect(0, 0, screenWidth, screenHeight) + + ctx.font = `40px ${fontFamily}` + ctx.fillStyle = '#fff' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(statusScreen.title || '', screenWidth / 2, screenHeight / 2 - 20) + + if (statusScreen.loading) { + // show loading spinner + ctx.save() + ctx.translate(screenWidth / 2, screenHeight / 2 + 50) + + ctx.strokeStyle = '#fff' + ctx.lineWidth = 5 + ctx.lineCap = 'round' + + let t = Date.now() / 1000 + + for (let i = 0; i < 12; i++) { + ctx.rotate(Math.PI / 6) + let offset = ((t * 12) - i) % 12 + ctx.globalAlpha = Math.max(0.2, 1 - offset / 3) + ctx.beginPath() + ctx.moveTo(0, 15) + ctx.lineTo(0, 30) + ctx.stroke() + } + + ctx.restore() + } + } + + startDrawLoop () { + if (this._drawTimerThread) return + let threadID = Math.random().toString(36) + this._drawTimerThread = threadID + this.drawTimerLoop(threadID) + } + + stopDrawLoop () { + this._drawTimerThread = null + } + + drawTimerLoop (threadID) { + if (!threadID || threadID !== this._drawTimerThread) return + requestAnimationFrame(() => this.drawTimerLoop(threadID)) + this.draw('draw-loop') + } + /** * Parses the content of an `S` message and schedules a draw * @param {string} str - the message content