diff --git a/http.js b/http.js index dbe9a4f..3d7dd40 100644 --- a/http.js +++ b/http.js @@ -80,10 +80,16 @@ PSocket.prototype.write = function(data) { var that = this; return new Promise(function(resolve, reject) { chrome.sockets.tcp.send(that.socketId, data, function(info) { - if (info && info.resultCode >= 0) - resolve(info.bytesSent); - else - reject(new Error('chrome sockets.tcp error ' + (info && info.resultCode))); + if (!chrome.runtime.lastError) { + if (info.resultCode === 0) + resolve(info.bytesSent); + else + reject(new Error('Socket TCP error ' + (info.resultCode))); + } else { + let emsg = 'Socket ID ' + that.socketId + ' error: ' + chrome.runtime.lastError.message; + console.log(emsg); + reject(new Error(emsg)); + } }); }); }; diff --git a/index.js b/index.js index 7ef9c0e..91cb965 100644 --- a/index.js +++ b/index.js @@ -92,16 +92,14 @@ var platform = pfUnk; Unknown ChromeOS Linux macOS Windows */ portPattern = ["", "/dev/ttyUSB", "dev/tty", "/dev/cu.usbserial", "COM"]; -// A list of connected websockets. -var sockets = []; - // Http and ws servers var server = new http.Server(); var wsServer = new http.WebSocketServer(server); var isServer = false; -// Keep track of the interval that sends the port list so it can be turned off -var portListener = null; +// Timer(s) to scan and send the port list +var wScannerInterval = null; +var portLister = []; // Is verbose loggin turned on? var verboseLogging = false; @@ -226,10 +224,14 @@ document.addEventListener('DOMContentLoaded', function() { function connect() { connect_ws($('bpc-port').value, $('bpc-url').value); + scanWPorts(); + wScannerInterval = setInterval(scanWPorts, 6010); // 6010: Scan at different intervals than send processes } function disconnect() { closeSockets(); + clearInterval(wScannerInterval); + wScannerInterval = null; } function updateStatus(connected) { @@ -254,36 +256,17 @@ function closeServer() { isServer = false; } -function findSocketIdx(socket) { -/* Return index of socket in sockets list - Returns -1 if not found*/ - return sockets.findIndex(function(s) {return s.socket === socket}); -} - function closeSockets() { -// Close all sockets and remove them from the list - while (sockets.length) { - sockets[0].socket.close(); - deleteSocket(0); - } -} - -function deleteSocket(socketOrIdx) { -/* Delete socket from lists (sockets and ports) - socketOrIdx is socket object or index of socket record to delete*/ - let idx = (typeof socketOrIdx === "number") ? socketOrIdx : findSocketIdx(socketOrIdx); -// log("Deleting socket at index " + idx, mDbug); - if (idx > -1 && idx < sockets.length) { - // Clear port's knowledge of socket connection record - if (sockets[idx].portIdx > -1) { -// log(" Clearing port index " + sockets[idx].portIdx + " reference to this socket", mDbug); - ports[sockets[idx].portIdx].bSocket = null; - ports[sockets[idx].portIdx].bSocketIdx = -1; +// Close all sockets and remove them from the ports and portLister lists + ports.forEach(function(p) { + if (p.bSocket) { + p.bSocket.close(); + p.bSocket = null; + }}); + while (portLister.length) { + clearInterval(portLister[0].scanner); + portLister.splice(0, 1); } - // Delete socket connection record and adjust ports' later references down, if any - sockets.splice(idx, 1); - ports.forEach(function(v) {if (v.bSocketIdx > idx) {v.bSocketIdx--}}); - } } function connect_ws(ws_port, url_path) { @@ -308,14 +291,7 @@ function connect_ws(ws_port, url_path) { wsServer.addEventListener('request', function(req) { var socket = req.accept(); -// log("Adding socket at index " + sockets.length, mDbug); - sockets.push({socket:socket, portIdx:-1}); - - //Listen for ports - if(portListener === null) { - portListener = setInterval(function() {sendPortList();}, 5000); - } - + socket.addEventListener('message', function(e) { if (isJson(e.data)) { var ws_msg = JSON.parse(e.data); @@ -329,7 +305,11 @@ function connect_ws(ws_port, url_path) { serialTerminal(socket, ws_msg.action, ws_msg.portPath, ws_msg.baudrate, ws_msg.msg); // action is "open", "close" or "msg" // send an updated port list } else if (ws_msg.type === "port-list-request") { - sendPortList(); + // Send port list now and set up scanner to send port list on regular interval +// log("Browser requested port-list for socket " + socket.pSocket_.socketId, mDbug); + sendPortList(socket); + let s = setInterval(function() {sendPortList(socket)}, 5000); + portLister.push({socket: socket, scanner: s}); // Handle unknown messages } else if (ws_msg.type === "hello-browser") { helloClient(socket, ws_msg.baudrate || 115200); @@ -347,14 +327,18 @@ function connect_ws(ws_port, url_path) { }); - // When a socket is closed, remove it from the list of connected sockets. + // Browser socket closed; terminate its port scans and remove it from list of ports. socket.addEventListener('close', function() { - deleteSocket(socket); - if (sockets.length === 0) { - updateStatus(false); - clearInterval(portListener); - portListener = null; - chrome.app.window.current().drawAttention(); + log("Browser socket closing: " + socket.pSocket_.socketId, mDbug); + let Idx = portLister.findIndex(function(s) {return s.socket === socket}); + if (Idx > -1) { + clearInterval(portLister[Idx].scanner); + portLister.splice(Idx, 1); + } + ports.forEach(function(p) {if (p.bSocket === socket) {p.bSocket = null}}); + if (!portLister.length) { + updateStatus(false); + chrome.app.window.current().drawAttention(); } }); @@ -405,49 +389,57 @@ function connect_ws(ws_port, url_path) { } function enableWX() { - wx_scanner_interval = setInterval(function() { - discoverWirelessPorts(); - ageWirelessPorts(); - displayWirelessPorts(); - }, 3500); + scanWXPorts(); + wScannerInterval = setInterval(scanWXPorts, 3500); } function disableWX() { - if(wx_scanner_interval) { - clearInterval(wx_scanner_interval); + if(wScannerInterval) { + clearInterval(wScannerInterval); $('wx-list').innerHTML = ''; } } -function sendPortList() { -// find and send list of communication ports (filtered according to platform and type) - chrome.serial.getDevices( - function(portlist) { - let wn = []; - let wln = []; - // update wired ports - portlist.forEach(function(port) { - if ((port.path.indexOf(portPattern[platform]) === 0) && (port.displayName.indexOf(' bt ') === -1 && port.displayName.indexOf('bluetooth') === -1)) { - addPort({path: port.path}); - } - }); - ageWiredPorts(); //Note, wired ports age here (just scanned) and wireless ports age elsewhere (where they are scanned) - - // gather separated and sorted port lists (wired names and wireless names) - ports.forEach(function(p) {if (p.isWired) {wn.push(p.path)} else {wln.push(p.path)}}); - wn.sort(); - wln.sort(); - - // report back to editor - var msg_to_send = {type:'port-list',ports:wn.concat(wln)}; - for (var i = 0; i < sockets.length; i++) { - sockets[i].socket.send(JSON.stringify(msg_to_send)); - if (chrome.runtime.lastError) { - console.log(chrome.runtime.lastError); +function scanWPorts() { +// Generate list of current wired ports (filtered according to platform and type) + chrome.serial.getDevices( + function(portlist) { + let wn = []; + let wln = []; + // update wired ports + portlist.forEach(function(port) { + if ((port.path.indexOf(portPattern[platform]) === 0) && (port.displayName.indexOf(' bt ') === -1 && port.displayName.indexOf('bluetooth') === -1)) { + addPort({path: port.path}); + } + }); + ageWiredPorts(); //Note, wired ports age here (just scanned) and wireless ports age elsewhere (where they are scanned) } + ); +} + +function scanWXPorts() { +// Generate list of current wireless ports + discoverWirelessPorts(); + ageWirelessPorts(); + displayWirelessPorts(); +} + +function sendPortList(socket) { +// Find and send list of communication ports (filtered according to platform and type) to browser via socket + let wn = []; + let wln = []; +// log("sendPortList() for socket " + socket.pSocket_.socketId, mDbug); + // gather separated and sorted port lists (wired names and wireless names) + ports.forEach(function(p) {if (p.isWired) {wn.push(p.path)} else {wln.push(p.path)}}); + wn.sort(); + wln.sort(); + + // report back to editor + var msg_to_send = {type:'port-list',ports:wn.concat(wln)}; + socket.send(JSON.stringify(msg_to_send)); + if (chrome.runtime.lastError) { + console.log(chrome.runtime.lastError); } - } - ); } @@ -462,39 +454,34 @@ function serialTerminal(sock, action, portPath, baudrate, msg) { // Find port from portPath let port = findPort(byPath, portPath); if (port) { - if(port.isWired) { - if (action === "open") { - // Open port for terminal use - openPort(sock, portPath, baudrate, 'debug') - .then(function() {log('Connected terminal to ' + portPath + ' at ' + baudrate + ' baud.');}) - .catch(function() { - log('Unable to connect terminal to ' + portPath); - var msg_to_send = {type:'serial-terminal', msg:'Failed to connect.\rPlease close this terminal and select a connected serial port.'}; - sock.send(JSON.stringify(msg_to_send)); - }); - } else if (action === "close") { - /* Terminal closed. Keep port open because chrome.serial always toggles DTR upon closing (resetting the Propeller) which causes - lots of unnecessary confusion (especially if an older version of the user's app is in the Propeller's EEPROM). - Instead, update the connection mode so that serial debug data halts.*/ - port.mode = 'none'; - } else if (action === "msg") { - // Message to send to the Propeller - if (port.connId) { - send(port, msg, false); - } - } + // Convert msg from string or buffer to an ArrayBuffer + if (typeof msg === 'string') { + msg = str2ab(msg); } else { - // TODO add WX module debug passthrough functions - if (action === 'open') { - - } else if (action === 'close') { - - } else if (action === 'msg') { - + if (msg instanceof ArrayBuffer === false) {msg = buf2ab(msg);} + } + if (action === "open") { + // Open port for terminal use + openPort(sock, portPath, baudrate, 'debug') + .then(function() {log('Connected terminal to ' + portPath + ' at ' + baudrate + ' baud.');}) + .catch(function() { + log('Unable to connect terminal to ' + portPath); + var msg_to_send = {type:'serial-terminal', msg:'Failed to connect.\rPlease close this terminal and select a connected port.'}; + sock.send(JSON.stringify(msg_to_send)); + }); + } else if (action === "close") { + /* Terminal closed. Keep wired port open because chrome.serial always toggles DTR upon closing (resetting the Propeller) which causes + lots of unnecessary confusion (especially if an older version of the user's app is in the Propeller's EEPROM). + Instead, update the connection mode so that serial debug data halts.*/ + port.mode = 'none'; + } else if (action === "msg") { + // Message to send to the Propeller + if ((port.isWired && port.connId) || (port.isWireless && port.ptSocket)) { //Send only if port is open + send(port, msg, false); } } } else { - var msg_to_send = {type:'serial-terminal', msg:'Failed to connect.\rPlease close this terminal and select a valid serial port.'}; + var msg_to_send = {type:'serial-terminal', msg:'Port ' + portPath + ' not found.\rPlease close this terminal and select an existing port.'}; sock.send(JSON.stringify(msg_to_send)); } } diff --git a/loader.js b/loader.js index ce6183f..9303219 100644 --- a/loader.js +++ b/loader.js @@ -23,19 +23,26 @@ let txData; //Data to transmit to the Pr const defaultClockSpeed = 80000000; const defaultClockMode = 0x6F; const maxDataSize = 1392; //Max data packet size (for packets sent to running Micro Boot Loader) +const mblRespSize = 8; //Size of Micro Boot Loader Response (and Expected) array buffers // propComm stage values const sgIdle = -1; const sgHandshake = 0; const sgVersion = 1; const sgRAMChecksum = 2; -const sgMBLResponse = 3; +const sgMBLResponse = 3; //NOTE: hearFromProp() requires all further to be wireless stages const sgWXResponse = 4; +// hearFromProp data source valuse +const dsDontCare = -1; +const dsWired = 0; +const dsHTTP = 1; +const dsTelnet = 2; + // Propeller Communication (propComm) status; categorizes Propeller responses let propComm = {}; //Holds current status -let mblRespAB = new ArrayBuffer(8); //Buffer for Micro Boot Loader actual responses -let mblExpdAB = new ArrayBuffer(8); //Buffer for Micro Boot Loader expected responses +let mblRespAB = new ArrayBuffer(mblRespSize); //Buffer for Micro Boot Loader actual responses +let mblExpdAB = new ArrayBuffer(mblRespSize); //Buffer for Micro Boot Loader expected responses const propCommStart = { //propCommStart is used to initialize propComm stage : sgIdle, //Propeller Protocol Stage @@ -60,6 +67,18 @@ const ltProgramEEPROM = 1; //Generate Program EEPROM e const ltReadyToLaunch = 2; //Generate Ready To Launch executable packet const ltLaunchNow = 3; //Generate Launch Now executable packet +//Receiver Handshake pattern +const rxHandshake = [ + 0xEE,0xCE,0xCE,0xCF,0xEF,0xCF,0xEE,0xEF,0xCF,0xCF,0xEF,0xEF,0xCF,0xCE,0xEF,0xCF, //The rxHandshake array consists of 125 bytes encoded to represent + 0xEE,0xEE,0xCE,0xEE,0xEF,0xCF,0xCE,0xEE,0xCE,0xCF,0xEE,0xEE,0xEF,0xCF,0xEE,0xCE, //the expected 250-bit (125-byte @ 2 bits/byte) response of + 0xEE,0xCE,0xEE,0xCF,0xEF,0xEE,0xEF,0xCE,0xEE,0xEE,0xCF,0xEE,0xCF,0xEE,0xEE,0xCF, //continuing-LFSR stream bits from the Propeller, prompted by the + 0xEF,0xCE,0xCF,0xEE,0xEF,0xEE,0xEE,0xEE,0xEE,0xEF,0xEE,0xCF,0xCF,0xEF,0xEE,0xCE, //timing templates following the txHandshake stream. + 0xEF,0xEF,0xEF,0xEF,0xCE,0xEF,0xEE,0xEF,0xCF,0xEF,0xCF,0xCF,0xCE,0xCE,0xCE,0xCF, + 0xCF,0xEF,0xCE,0xEE,0xCF,0xEE,0xEF,0xCE,0xCE,0xCE,0xEF,0xEF,0xCF,0xCF,0xEE,0xEE, + 0xEE,0xCE,0xCF,0xCE,0xCE,0xCF,0xCE,0xEE,0xEF,0xEE,0xEF,0xEF,0xCF,0xEF,0xCE,0xCE, + 0xEF,0xCE,0xEE,0xCE,0xEF,0xCE,0xCE,0xEE,0xCF,0xCF,0xCE,0xCF,0xCF +]; + /*********************************************************** * Support Functions * ***********************************************************/ @@ -123,7 +142,7 @@ function loadPropeller(sock, portPath, action, payload, debug) { if (port.connId) { // Connection exists, prep to reuse it originalBaudrate = port.baud; - port.mode = "programming"; + updatePort(port, {mode: "programming", bSocket: sock}); connect = function() {return changeBaudrate(port, initialBaudrate)} } else { // No connection yet, prep to create one @@ -135,6 +154,7 @@ function loadPropeller(sock, portPath, action, payload, debug) { postResetDelay = 1; //TODO Retrieve actual current baudrate originalBaudrate = initialBaudrate; + updatePort(port, {mode: "programming", bSocket: sock}); connect = function() {return Promise.resolve()}; } // Use connection to download application to the Propeller @@ -151,6 +171,7 @@ function loadPropeller(sock, portPath, action, payload, debug) { sock.send(JSON.stringify({type:"ui-command", action:(debug === "term") ? "open-terminal" : "open-graph"})); sock.send(JSON.stringify({type:"ui-command", action:"close-compile"})); } else { //Else + updatePort(port, {mode: "none"}); // Clear port mode if (port.isWireless) closePort(port, false).catch(function(e) {log(e.message, mAll, sock);}) // Close Telnet port (if wireless) } }) //Error? Disable listener and display error @@ -158,6 +179,7 @@ function loadPropeller(sock, portPath, action, payload, debug) { listen(port, false); log(e.message, mAll, sock); log(notice(neDownloadFailed), mAll, sock); + updatePort(port, {mode: "none"}); if ((port.isWired && port.connId) || port.isWireless) {changeBaudrate(port, originalBaudrate)} if (port.isWireless) {closePort(port, false)} }); @@ -258,7 +280,7 @@ function talkToProp(sock, port, binImage, toEEPROM) { setTimeout(function() { //Prep for expected packetID:transmissionId response (Micro-Boot-Loader's "Ready" signal) propComm.mblEPacketId[0] = packetId; - propComm.mblETransId[0] = transmissionId; + propComm.mblETransId[0] = 0; //MBL transmission's Id is always 0 //Send Micro Boot Loader package and get response; if wired port, unpause (may be auto-paused by incoming data error); wireless ports, carry on immediately log("Transmitting Micro Boot Loader package", mDeep); send(port, txData, true) @@ -320,11 +342,10 @@ function talkToProp(sock, port, binImage, toEEPROM) { Math.min(Math.trunc(maxDataSize / 4) - 2, Math.trunc(binImage.byteLength / 4) - pIdx); txData = new ArrayBuffer(txPacketLength * 4); //Set packet length (in longs)} txView = new Uint8Array(txData); - transmissionId = Math.floor(Math.random()*4294967296); //Create next random Transmission ID - propComm.mblEPacketId[0] = packetId-1; - propComm.mblETransId[0] = transmissionId; + propComm.mblEPacketId[0] = packetId-1; //Set next expected packetId + propComm.mblETransId[0] = Math.floor(Math.random()*4294967296); //Set next random Transmission ID (new DataView(txData, 0, 4)).setUint32(0, packetId, true); //Store Packet ID - (new DataView(txData, 4, 4)).setUint32(0, transmissionId, true); //Store random Transmission ID + (new DataView(txData, 4, 4)).setUint32(0, propComm.mblETransId[0], true); //Store random Transmission ID txView.set((new Uint8Array(binImage)).slice(pIdx * 4, pIdx * 4 + (txPacketLength - 2) * 4), 8); //Store section of binary image send(port, txData, false) //Transmit packet .then(function() {pIdx += txPacketLength - 2; packetId--; resolve();}); //Increment image index, decrement Packet ID (to next packet), resolve @@ -359,16 +380,16 @@ function talkToProp(sock, port, binImage, toEEPROM) { log(next.value.sendLog, mAll, sock); generateLoaderPacket(next.value.type, packetId); //Generate next executable packet - transmissionId = Math.floor(Math.random()*4294967296); //Create next random Transmission ID - (new DataView(txData, 4, 4)).setUint32(0, transmissionId, true); //Store random Transmission ID if (next.value.type !== ltLaunchNow) { //Response expected from MBL? prepForMBLResponse(next.value.recvTime, notice(neCommunicationLost)); // Prepare to receive next MBL response packetId = next.value.nextId; // Ready next Packet ID propComm.mblEPacketId[0] = packetId; // Note expected response - propComm.mblETransId[0] = transmissionId; + propComm.mblETransId[0] = Math.floor(Math.random()*4294967296); } + (new DataView(txData, 4, 4)).setUint32(0, propComm.mblETransId[0], true); //Store random Transmission ID (or 0) + send(port, txData, false) //Transmit packet .then(function() { if (next.value.type !== ltLaunchNow) { // If not last instruction packet... @@ -396,7 +417,6 @@ function talkToProp(sock, port, binImage, toEEPROM) { //Determine number of required packets for target application image; value becomes first Packet ID var totalPackets = Math.ceil(binImage.byteLength / (maxDataSize-4*2)); //binary image size (in bytes) / (max packet size - packet header) var packetId = totalPackets; - var transmissionId = 0; //Initial Transmission ID var pIdx = 0; //Packet index (points to next data in binary image to send //Calculate target application's full checksum (used for RAM Checksum confirmation)} binView = new Uint8Array(binImage); //Create view of the Propeller Application Image @@ -435,27 +455,27 @@ function talkToProp(sock, port, binImage, toEEPROM) { function hearFromProp(info) { /* Receive Propeller's responses during programming. Parse responses for expected stages. - This function is called asynchronously whenever data arrives*/ - - log("Received " + info.data.byteLength + " bytes", mDeep); - // Parse HTTP-command responses into proper object, or treat wired and Telnet-wireless streams as an unformatted array - let stream = (propComm.port.phSocket) ? parseHTTP(info.data) : new Uint8Array(info.data) - // Exit if we're idling or if socket-based data is not in response to our Propeller communication socket - if ((propComm.stage === sgIdle) || (info.hasOwnProperty("socketId") && info.socketId !== propComm.port.phSocket && info.socketId !== propComm.port.ptSocket)) { - log("...ignoring", mDeep); + This function is called asynchronously whenever data arrives and may receive data not related to programming (such as a debug stream or unrelated IP traffic) + so it filters and ignores what it doesn't need.*/ + + let dataSource = (info.hasOwnProperty("socketId")) ? + /*Is Expected Wireless HTTP?*/ (info.socketId === propComm.port.phSocket) ? dsHTTP : + /*Is Expected Wireless Telnet?*/ (!propComm.port.phSocket && propComm.stage === sgMBLResponse && info.socketId === propComm.port.ptSocket) ? dsTelnet : + /*Unexpected WL Protocol for State*/ dsDontCare : + /*Is Expected Wired stream?*/ (propComm.stage !== sgIdle) ? dsWired : dsDontCare; + + // Exit if this isn't the data we're looking for + if (dataSource === dsDontCare) { + log("Ignoring " + info.data.byteLength + " unexpected bytes", mDeep); +// console.log(info.data); return; } - const rxHandshake = [ - 0xEE,0xCE,0xCE,0xCF,0xEF,0xCF,0xEE,0xEF,0xCF,0xCF,0xEF,0xEF,0xCF,0xCE,0xEF,0xCF, //The rxHandshake array consists of 125 bytes encoded to represent - 0xEE,0xEE,0xCE,0xEE,0xEF,0xCF,0xCE,0xEE,0xCE,0xCF,0xEE,0xEE,0xEF,0xCF,0xEE,0xCE, //the expected 250-bit (125-byte @ 2 bits/byte) response of - 0xEE,0xCE,0xEE,0xCF,0xEF,0xEE,0xEF,0xCE,0xEE,0xEE,0xCF,0xEE,0xCF,0xEE,0xEE,0xCF, //continuing-LFSR stream bits from the Propeller, prompted by the - 0xEF,0xCE,0xCF,0xEE,0xEF,0xEE,0xEE,0xEE,0xEE,0xEF,0xEE,0xCF,0xCF,0xEF,0xEE,0xCE, //timing templates following the txHandshake stream. - 0xEF,0xEF,0xEF,0xEF,0xCE,0xEF,0xEE,0xEF,0xCF,0xEF,0xCF,0xCF,0xCE,0xCE,0xCE,0xCF, - 0xCF,0xEF,0xCE,0xEE,0xCF,0xEE,0xEF,0xCE,0xCE,0xCE,0xEF,0xEF,0xCF,0xCF,0xEE,0xEE, - 0xEE,0xCE,0xCF,0xCE,0xCE,0xCF,0xCE,0xEE,0xEF,0xEE,0xEF,0xEF,0xCF,0xEF,0xCE,0xCE, - 0xEF,0xCE,0xEE,0xCE,0xEF,0xCE,0xCE,0xEE,0xCF,0xCF,0xCE,0xCF,0xCF - ]; + // Parse HTTP-command responses into proper object, or treat wired and Telnet-wireless streams as an unformatted array + let stream = (dataSource === dsHTTP) ? parseHTTP(info.data) : new Uint8Array(info.data) + log("Received " + info.data.byteLength + " bytes", mDeep); +// console.log(stream); + var sIdx = 0; /* Validate rxHandshake @@ -543,39 +563,40 @@ function hearFromProp(info) { propComm.rxCount = 0; } - // Receive Micro Boot Loader's response. The first is its "Ready" signal; the rest are packet responses. + // Receive Micro Boot Loader's response. The first serves as its "Ready" signal, the rest are packet responses; all are formatted as 4-byte expected Packet ID followed by 4-byte Transmission ID. + // NOTE: For wireless, the first is contained in the body of an HTTP response and the rest are delivered as Telnet responses. if (propComm.stage === sgMBLResponse) { - if (propComm.port.isWireless) { + if (dataSource !== dsWired) { // Wireless response - console.log(stream); - if (propComm.port.phSocket) { //HTTP-command response + if (dataSource === dsHTTP) { //HTTP-command response; we'll assume right size, errors will be caught by timeout if (stream.ResponseCode === 200) { - propComm.mblRespBuf.set(new Uint8Array(stream.Body.slice(0, propComm.mblRespBuf.byteLength))); - propComm.rxCount = propComm.mblRespBuf.byteLength; + propComm.mblRespBuf.set(new Uint8Array(stream.Body.slice(-mblRespSize))); + propComm.rxCount = mblRespSize; } if (stream.hasOwnProperty("Connection") && stream.Connection === "close") {updatePort(propComm.port, {phSocket: null})} // Forget socket id (closed by host)} - } else { //Telnet response - propComm.mblRespBuf.set(new Uint8Array(stream.slice(0, propComm.mblRespBuf.byteLength))); - propComm.rxCount = propComm.mblRespBuf.byteLength; + } else { //Telnet response; we'll assume right size (perhaps tacked onto end of previous debug stream), errors will be caught by timeout + propComm.mblRespBuf.set(new Uint8Array(stream.slice(-mblRespSize))); + propComm.rxCount = mblRespSize; } } else { // Wired response - while (sIdx < stream.length && propComm.rxCount < propComm.mblRespBuf.byteLength) { + //TODO consider set function here + while (sIdx < stream.length && propComm.rxCount < mblRespSize) { propComm.mblRespBuf[propComm.rxCount++] = stream[sIdx++]; } } //Finish stage when expected response size received - if (propComm.rxCount === propComm.mblRespBuf.byteLength) { - clearPropCommTimer(); - propComm.stage = sgIdle; + if (propComm.rxCount === mblRespSize) { // log("Response PacketId: "+ propComm.mblRPacketId+ " TransId: "+ propComm.mblRTransId, mDeep); // log("Expected PacketId: "+ propComm.mblEPacketId+ " TransId: "+ propComm.mblETransId, mDeep); if ((propComm.mblRPacketId[0] === propComm.mblEPacketId[0]) && (propComm.mblRTransId[0] === propComm.mblETransId[0])) { //MBL Response is perfect; Note resolved + clearPropCommTimer(); + propComm.stage = sgIdle; propComm.response.resolve(); } else { - //MBL Response invalid; Note rejected; Ignore the rest - propComm.response.reject(Error(notice(neLoaderFailed))); + //MBL Response invalid; may be leftover debug data; ignore + propComm.rxCount = 0; } return; } @@ -585,10 +606,9 @@ function hearFromProp(info) { if (propComm.stage === sgWXResponse) { clearPropCommTimer(); propComm.stage = sgIdle; - console.log(stream); if (stream.ResponseCode === 200) { -// propComm.mblRespBuf.set(new Uint8Array(stream.Body.slice(0, propComm.mblRespBuf.byteLength))); -// propComm.rxCount = propComm.mblRespBuf.byteLength; +// propComm.mblRespBuf.set(new Uint8Array(stream.Body.slice(0, mblRespSize))); +// propComm.rxCount = mblRespSize; propComm.response.resolve(); } else { //TODO Designate a proper error here (probably best to pass on error from response) @@ -858,7 +878,7 @@ function generateLoaderPacket(loaderType, packetId, clockSpeed, clockMode) { txView.set(timingPulses, txLength + encodedLoader.byteLength); } else /*loaderType === ltCore*/ { //[ltUnEncCore] Prepare unencoded loader packet (for wireless downloads) - let postStr = str2ab("POST /propeller/load?baud-rate="+initialBaudrate+"&reset-pin=12&response-size=8&response-timeout=1000 HTTP/1.1\r\nContent-Length: "+patchedLoader.byteLength+"\r\n\r\n"); + let postStr = str2ab("POST /propeller/load?baud-rate="+initialBaudrate+"&final-baud-rate="+finalBaudrate+"&reset-pin=12&response-size=8&response-timeout=1000 HTTP/1.1\r\nContent-Length: "+patchedLoader.byteLength+"\r\n\r\n"); txData = new ArrayBuffer(postStr.byteLength+patchedLoader.byteLength); txView = new Uint8Array(txData); diff --git a/manifest.json b/manifest.json index 78eb318..00e2678 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "BlocklyProp Launcher", "description": "A Chrome application that connects your Propeller-Powered Hardware to the BlocklyProp website.", - "version": "0.9.0", + "version": "0.9.1", "manifest_version": 2, "minimum_chrome_version": "45", diff --git a/port.js b/port.js index 15f2b4d..b2d8704 100644 --- a/port.js +++ b/port.js @@ -16,9 +16,11 @@ // Find Port identifier types -const byID = "connId"; -const byMAC = "mac"; -const byPath = "path"; +const byCID = "connId"; //Represents numeric Connection ID (cid) type +const byPHID = "phSocket"; //Represents numeric Propeller HTTP Socket ID type +const byPTID = "ptSocket"; //Represents numeric Propeller Telnet Socket ID type +const byMAC = "mac"; //Represents alphanumeric MAC address type +const byPath = "path"; //Represents alphanumeric path (wired/wireless port identifier) type // Port's max lifetime const wLife = 2; @@ -28,11 +30,11 @@ const wlLife = 3; var ports = []; // Serial packet handling (for transmissions to browser's terminal) -const serPacketFillTime = 10; // Max wait time to fill packet (ms) -const serPacketMax = Math.trunc(serPacketFillTime/1000/(1/finalBaudrate*10)); // Size of buffer to hold max bytes receivable in FillTime at max baudrate +const serPacketMaxTxTime = 100; // Max wait time before transmitting packet (ms) +const serPacketMax = 1492; // Size of buffer to transmit serial data to browser const serPacket = { id : 0, - bufView : new Uint8Array(new ArrayBuffer(serPacketMax)), + bufView : null, /*set later to new Uint8Array(new ArrayBuffer(serPacketMax)) since Object.assign() only shallow-copies*/ len : 0, timer : null, }; @@ -75,22 +77,22 @@ function addPort(alist) { ip : get("ip", alist, ""), /*[""+] Wireless port's IP address; */ life : (!get("ip", alist, "")) ? wLife : wlLife, /*[>=0] Initial life value; wired and wireless*/ bSocket : null, /*[null+] Socket to browser (persistent)*/ - bSocketIdx : -1, /*[>=-1] Index of browser socket in sockets list*/ phSocket : null, /*[null+] Socket to Propeller's HTTP service (not persistent)*/ ptSocket : null, /*[null+] Socket to Propeller's Telnet service (persistent)*/ - mode : "", /*[""+] Intention of the connection; "", "debug", or "programming"*/ + mode : "none", /*["none"+] Intention of the connection; "none", "debug", or "programming"*/ baud : 0, /*[>=0] Wired port's data rate*/ packet : {}, /*[...] Packet buffer for socket*/ isWired : !Boolean(get("ip", alist, "")), /*[true/false] indicates if port is wired or not*/ isWireless : Boolean(get("ip", alist, "")) /*[true/false] indicates if port is wireless or not*/ }); - // Give it its own packet buffer + // Give it its own packet object and buffer Object.assign(ports[ports.length-1].packet, serPacket); + ports[ports.length-1].packet.bufView = new Uint8Array(new ArrayBuffer(serPacketMax)); } } function updatePort(port, alist) { -/* Update port attributes if necessary. Automatically handles special cases like baudrate changes and sockets<->ports links. +/* Update port attributes if necessary. Automatically handles special case of baudrate changes. port: [required] port object to update alist: [required] one or more attributes of port to update. Unchanging attributes can be omitted. Possible attributes are: path: the string path to the wired serial port, or custom name of wireless port. Can be empty ("") and a wireless name will be fabricated from the MAC address @@ -99,7 +101,7 @@ function updatePort(port, alist) { bSocket: active socket to browser to associate with port; may be null phSocket: active socket to Propeller's HTTP service; may be null ptSocket: active socket to Propeller's Telnet service; may be null - mode: the current point of the connection; "", "debug", "programming" + mode: the current point of the connection; "none", "debug", "programming" baud: wired serial speed*/ return new Promise(function(resolve, reject) { @@ -118,18 +120,7 @@ function updatePort(port, alist) { set("connId"); set("ip"); port.life = (port.isWired) ? wLife : wlLife; - // Update sockets<->ports links as necessary - if (exists("bSocket", alist)) { - let sIdx = findSocketIdx(alist.bSocket); - if (port.bSocketIdx !== sIdx) { - // new browser socket is different; adjust existing browser socket's record (if any), then apply new browser socket details to port -// log(" Linking to browser socket index " + sIdx, mDbug); - if (port.bSocketIdx !== -1) {sockets[port.bSocketIdx].portIdx = -1} - port.bSocket = alist.bSocket; - port.bSocketIdx = sIdx; - if (sIdx > -1) {sockets[sIdx].portIdx = findPortIdx(byPath, port.path)} - } - } + set("bSocket"); set("phSocket"); set("ptSocket"); set("mode"); @@ -149,7 +140,9 @@ function exists(attr, src) { function findPortIdx(type, clue) { /* Return index of wired or wireless port associated with clue type / clue pairs must be: - byID / numeric Connection ID (cid) + byCID / numeric Connection ID (cid) + byPHID / numeric Propeller HTTP Socket ID + byPTID / numeric Propeller Telnet Socket ID byMAC / alphanumeric MAC address byPath / alphanumeric path (wired/wireless port identifier) Returns -1 if not found*/ @@ -159,7 +152,9 @@ function findPortIdx(type, clue) { function findPort(type, clue) { /* Return port record associated with clue. This allows caller to later directly retrieve any member of the record (provided caller safely checks for null) type / clue pairs must be: - byID / numeric Connection ID (cid) + byCID / numeric Connection ID (cid) + byPHID / numeric Propeller HTTP Socket ID + byPTID / numeric Propeller Telnet Socket ID byMAC / alphanumeric MAC address byPath / alphanumeric path (wired/wireless port identifier) Returns null if not found*/ @@ -176,18 +171,14 @@ function findPort(type, clue) { function deletePort(type, clue) { /* Delete wired or wireless port associated with clue type / clue pairs must be: - byID / numeric Connection ID (cid) + byCID / numeric Connection ID (cid) + byPHID / numeric Propeller HTTP Socket ID + byPTID / numeric Propeller Telnet Socket ID byMAC / alphanumeric MAC address byPath / alphanumeric path (wired/wireless port identifier)*/ let idx = findPortIdx(type, clue); if (idx > -1) { log("Deleting port: " + ports[idx].path, mDbug); - if (ports[idx].bSocketIdx > -1) { - // Clear socket's knowledge of wired or wireless port record - sockets[ports[idx].bSocketIdx].portIdx = -1; - } - // Delete port record and adjust socket's later references down, if any - ports.splice(idx, 1) - sockets.forEach(function(v) {if (v.portIdx > idx) {v.portIdx--}}); + ports.splice(idx, 1); } } \ No newline at end of file diff --git a/serial.js b/serial.js index a809546..ae75fd4 100644 --- a/serial.js +++ b/serial.js @@ -24,12 +24,12 @@ * Serial Support Functions * ***********************************************************/ -//TODO Consider returning error object //TODO Consider enhancing error to indicate if the port is already open (this would only be for developer mistakes though) function openPort(sock, portPath, baudrate, connMode) { -/* Return a promise to open serial port at portPath with baudrate and connect to sock. - sock can be null to open serial port without an associated socket - portPath is the string path to the wired serial port +/* Return a promise to open wired or wireless port at portPath with baudrate and connect to browser sock. If wireless, the port is opened + as a Telnet-based debug service. + sock can be null to open port without an associated browser socket + portPath is the string path to the wired or wireless port baudrate is optional; defaults to initialBaudrate connMode is the current point of the connection; 'debug', 'programming' Resolves (with nothing); rejects with Error*/ @@ -37,32 +37,33 @@ function openPort(sock, portPath, baudrate, connMode) { baudrate = baudrate ? parseInt(baudrate) : initialBaudrate; var port = findPort(byPath, portPath); if (port) { - if (port.connId) { - //Already open; ensure correct baudrate, socket, and connMode, then resolve. - updatePort(port, {bSocket: sock, mode: connMode, baud: baudrate}) - .then(function() {resolve()}) - .catch(function (e) {reject(e)}); - } else { - //Not already open; attempt to open it - chrome.serial.connect(portPath, { - 'bitrate': baudrate, - 'dataBits': 'eight', - 'parityBit': 'no', - 'stopBits': 'one', - 'ctsFlowControl': false - }, - function (openInfo) { - if (!chrome.runtime.lastError) { - // No error; update serial port object - updatePort(port, {connId: openInfo.connectionId, bSocket: sock, mode: connMode, baud: baudrate}); - log("Port " + portPath + " open with ID " + openInfo.connectionId, mStat); - resolve(); - } else { - // Error - reject(Error(notice(neCanNotOpenPort, [portPath]))); + if (port.isWired) { /*Wired port*/ + if (port.connId) { + //Already open; ensure correct baudrate, socket, and connMode, then resolve. + updatePort(port, {bSocket: sock, mode: connMode, baud: baudrate}) + .then(function() {resolve()}) + .catch(function(e) {reject(e)}); + } else { + //Not already open; attempt to open it + chrome.serial.connect(portPath, {bitrate: baudrate, dataBits: 'eight', parityBit: 'no', stopBits: 'one', ctsFlowControl: false}, + function (openInfo) { + if (!chrome.runtime.lastError) { + // No error; update serial port object + updatePort(port, {connId: openInfo.connectionId, bSocket: sock, mode: connMode, baud: baudrate}); + log("Port " + portPath + " open with ID " + openInfo.connectionId, mStat); + resolve(); + } else { + // Error + reject(Error(notice(neCanNotOpenPort, [portPath]))); + } } - } - ); + ); + } + } else { /*Wireless port*/ + openSocket(port, false) + .then(updatePort(port, {bSocket: sock, mode: connMode, baud: baudrate}) + .then(function() {resolve()}) + .catch(function (e) {reject(e)})); } } else { // Error; port record not found @@ -71,6 +72,30 @@ function openPort(sock, portPath, baudrate, connMode) { }); } +function openSocket(port, command) { +/* Open Propeller command (HTTP) or debug (Telnet) socket on port + port is the port's object + command is true to open HTTP-based command service and false to open Telnet-based Debug service + Resolves with object describing socket type*/ + return new Promise(function(resolve, reject) { + let p = (command) ? {socket: "phSocket", portNum: 80} : {socket: "ptSocket", portNum: 23}; + if (port[p.socket]) { // Already open; resolve + resolve(p); + } else { // No ph or pt socket yet; create one and connect to it + chrome.sockets.tcp.create(function (info) { + updatePort(port, {[p.socket]: info.socketId}); + chrome.sockets.tcp.connect(port[p.socket], port.ip, p.portNum, function () { + //TODO Handle connect result + chrome.sockets.tcp.setNoDelay(info.socketId, true, function(result) { + if (result < 0) {log("Warning: unable to disable Nagle timer", mDbug)} + resolve(p); + }); + }); + }); + } + }); +} + //TODO !!! This is no longer a pure-wired-serial function; decide what to do long-term function closePort(port, command) { /* Close the port. @@ -81,7 +106,7 @@ function closePort(port, command) { return new Promise(function(resolve, reject) { function socketClose(socket) { - // Nullify port's socket reference + // Nullify port's HTTP or Telnet socket reference let sID = port[socket]; updatePort(port, {[socket]: null}); if (sID) { @@ -154,21 +179,16 @@ function changeBaudrate(port, baudrate) { } else { //TODO Need to check for errors. resetPropComm(port, 1500, sgWXResponse, notice(neCanNotSetBaudrate, [port.path, baudrate]), true); - chrome.sockets.tcp.create(function (info) { - //Update port record with socket to Propeller's HTTP service - updatePort(port, {phSocket: info.socketId}); - if (!port.phSocket) { - log("NULL SOCKET!!!", mDbug); //!!!! - } - let postStr = "POST /wx/setting?name=baud-rate&value=" + baudrate + " HTTP/1.1\r\n\r\n"; - chrome.sockets.tcp.connect(port.phSocket, port.ip, 80, function() { + openSocket(port, true) + .then(function(p) { + let postStr = "POST /wx/setting?name=baud-rate&value=" + baudrate + " HTTP/1.1\r\n\r\n"; chrome.sockets.tcp.send(port.phSocket, str2ab(postStr), function () { propComm.response .then(function() {port.baud = baudrate; return resolve();}) //Update baud; does not use updatePort() because of circular reference //!!! .catch(function(e) {return reject(e);}) }); - }); - }); + }) + .catch(function(e) {return reject(e)}); } } else { // Port is already set to baudrate @@ -226,63 +246,35 @@ function ageWiredPorts() { //TODO Check send callback //TODO Reject with error objects as needed function send(port, data, command) { -/* Return a promise that transmits data on port +/* Return a promise that transmits data on port. Port must already be open if wired, may be open or not if wireless. port is the port's object - data is an ArrayBuffer (preferrably), string, or array - command [ignored unless wireless] must be true to send to Wi-Fi Module's HTTP-based command service and false to send to Propeller via Telnet service*/ - + data is an ArrayBuffer + command [ignored unless wireless] is true to send to Wi-Fi Module's HTTP-based command service and false to send to Propeller via Telnet service*/ return new Promise(function(resolve, reject) { - - log("in send()", mDbug); //!!!! - - function socketSend(p) { - log("in socketSend()", mDbug); //!!!! - if (!port[p.socket]) { // No ph or pt socket yet; create one and connect to it - chrome.sockets.tcp.create(function (info) { - log("in sockets.tcp.create()", mDbug); //!!!! - updatePort(port, {[p.socket]: info.socketId}); - chrome.sockets.tcp.connect(port[p.socket], port.ip, p.portNum, function () { - //TODO Handle connect result - log("in sockets.tcp.connect()", mDbug); //!!!! - chrome.sockets.tcp.send(port[p.socket], data, function () { - //TODO handle send result - log("in sockets.tcp.connect > send()", mDbug); //!!!! - resolve(); - }); - }); - }); - } else { // Socket exists; use it - log("socket exists", mDbug); //!!!! - chrome.sockets.tcp.send(port[p.socket], data, function () { - //TODO handle send result - log("in sockets.tcp.send()", mDbug); //!!!! - resolve(); - }); - } - } - - // Convert data from string or buffer to an ArrayBuffer - if (typeof data === 'string') { - data = str2ab(data); - } else { - if (data instanceof ArrayBuffer === false) {data = buf2ab(data);} - } - if (port.isWired) { // Wired port chrome.serial.send(port.connId, data, function (sendResult) { resolve(); }); } else { // Wireless port - socketSend((command) ? {socket: "phSocket", portNum: 80} : {socket: "ptSocket", portNum: 23}); + openSocket(port, command) + .then(function (p) { + chrome.sockets.tcp.send(port[p.socket], data, function () { + //TODO handle send result + resolve(); + }); + }) + .catch(function (e) {reject(e)}) } }); } -chrome.serial.onReceive.addListener(function(info) { -// Permanent serial receive listener- routes debug data from Propeller to connected browser when necessary - let port = findPort(byID, info.connectionId); +//TODO !!! This is no longer a pure-wired-serial function; decide what to do long-term +function debugReceiver(info) { +// Wired and wireless receive listener- routes debug data from Propeller to connected browser when necessary + let wired = (info.hasOwnProperty("connectionId")); + let port = wired ? findPort(byCID, info.connectionId) : findPort(byPTID, info.socketId); if (port) { - if (port.mode === 'debug' && port.bSocket !== null) { + if (port.mode === 'debug' && port.bSocket) { // send to terminal in browser tab let offset = 0; do { @@ -293,7 +285,7 @@ chrome.serial.onReceive.addListener(function(info) { if (port.packet.len === serPacketMax) { sendDebugPacket(port); } else if (port.packet.timer === null) { - port.packet.timer = setTimeout(sendDebugPacket, serPacketFillTime, port) + port.packet.timer = setTimeout(sendDebugPacket, serPacketMaxTxTime, port) } } while (offset < info.data.byteLength); } @@ -304,17 +296,36 @@ chrome.serial.onReceive.addListener(function(info) { clearTimeout(port.packet.timer); port.packet.timer = null; } - port.bSocket.send(JSON.stringify({type: 'serial-terminal', packetID: port.packet.id++, msg: btoa(ab2str(port.packet.bufView.slice(0, port.packet.len)))})); + if (port.mode === 'debug' && port.bSocket) { + port.bSocket.send(JSON.stringify({type: 'serial-terminal', packetID: port.packet.id++, msg: btoa(ab2str(port.packet.bufView.slice(0, port.packet.len)))})); + } port.packet.len = 0; } -}); +}; -chrome.serial.onReceiveError.addListener(function(info) { -// Permanent serial receive error listener. - switch (info.error) { - case "disconnected": - case "device_lost" : - case "system_error": deletePort(byID, info.connectionId); +//TODO !!! This is no longer a pure-wired-serial function; decide what to do long-term +function debugErrorReceiver(info) { +// Wired and wireless receive error listener. + if (info.hasOwnProperty("connectionId")) { + switch (info.error) { + case "disconnected": + case "device_lost" : + case "system_error": deletePort(byCID, info.connectionId); + } +// log("Error: PortID "+info.connectionId+" "+info.error, mDeep); + } else { + switch (info.resultCode) { + case -100: //port closed + let port = findPort(byPTID, info.socketId); + if (!port) {port = findPort(byPHID, info.socketId)} + if (port) { + log("SocketID "+info.socketId+" connection closed" + ((port) ? " for port " + port.path + "." : "."), mDeep); + } + break; + default: log("Error: SocketID "+info.socketId+" Code "+info.resultCode, mDeep); + } } -// log("Error: PortID "+info.connectionId+" "+info.error, mDeep); -}); \ No newline at end of file +}; + +chrome.serial.onReceive.addListener(debugReceiver); +chrome.serial.onReceiveError.addListener(debugErrorReceiver); \ No newline at end of file diff --git a/wx.js b/wx.js index 2734e22..ba94c3e 100644 --- a/wx.js +++ b/wx.js @@ -29,7 +29,7 @@ var udp_sock; var disc_packet = '\0\0\0\0'; // Holder for the interval for discovering modules -var wx_scanner_interval = null; +var wxScannerInterval = null; function calcBroadcastAddr(mip) { // Calculate a broadcast IP from a given address and subnet mask @@ -180,6 +180,9 @@ function isValidWiFiVersion(response) { return valid; } +chrome.sockets.tcp.onReceive.addListener(debugReceiver); +chrome.sockets.tcp.onReceiveError.addListener(debugErrorReceiver); + /* function loadPropellerWX(portPath, action, payload, debug) {