diff --git a/core/serial.js b/core/serial.js new file mode 100644 index 00000000..6036deb0 --- /dev/null +++ b/core/serial.js @@ -0,0 +1,551 @@ +/* Created by Fry on 7/11/16. +// +//On Mac, to see serial ports, open Terminal app and enter: ls /dev/tty.* or ls /dev/cu.* +// https://learn.sparkfun.com/tutorials/terminal-basics/all geared toward windows, but servicable. +//for devices, mac and linux have both tty and cu for full duplex. windows only has tty. +//Macintosh problems +//sparkfun RedBoard is an arduino Uno, what james loaned me. +//didn't work for uploading programs to the board. +//Arduino Micro and Leonado have something special ie "mouse and kbd support +//that allows them to work on Mac. I got Arduino Leonardo because +//arduino mciro can't take shields. +//Mike says some ardino boards don't work on mac, get a micro-arduino from Microcenter. + // http://www.meetup.com/Cambridge-Hackspace/ tues 6:30PM union sq somerville + //https://www.arduino.cc/en/Reference/Firmata a Kketch for prototcol + //that interfaces to javascript via https://github.com/firmata/firmata.js?utm_source=buffer&utm_medium=twitter&utm_campaign=Buffer&utm_content=buffer52691 +//firmata provides the sketch for arduino and protolcoe. firmata .js is a way to use it +//from js, johnny 5 and cylon use firmata underneath and build on top of it. +//to make a "compiled" file of a arduino sketch, edit the sketch, then use File menu/Sketch/export compiled binary +//gort: CLI that can upload .hex files into an arduino board. for mac, win. linux + The ATmega32U4 on the Micro comes preprogrammed with a bootloader that allows you to upload new code to it without the use of an external hardware programmer. It communicates using the AVR109 protocol. + You can also bypass the bootloader and program the microcontroller through the ICSP (In-Circuit Serial Programming) header using Arduino ISP or similar; see these instructions for details. +temboo IOT js connectivity requires setting up a local js server to use. +// http://www.penvon.com/b/robotics-projects-github-cm596 johnny5 has 3 times the github stars as cylon + // ie 6K vs 2K July, 2016 + + +//strategy: sandbox never has to know connectionIDs, just paths. +//keep map between them on the UI side +*/ + +var SerialPort = require('serialport') //"To use SerialPort, you must call serial_port_init() first." + + +module.exports.SerialPort = SerialPort + +//caled fron ready.js. Prevents error when loading job _engine (and this file) +//on Dexter hardware +function serial_port_init() { + //SerialPort = require('serialport') //now this is always done. + warning("Calling serial_port_init() is no longer necessary to use the serial port capability.
" + + "It now does nothing.") + +} +module.exports.serial_port_init = serial_port_init + + +var serial_path_to_info_map = {} +module.exports.serial_path_to_info_map = serial_path_to_info_map + +function serial_path_to_info(path){ + return serial_path_to_info_map[path] +} + +function serial_devices(){ + if(platform === "dde") { + let dde_ipc = require('electron').ipcRenderer //can't just use global const ipcRenderer here but I don't know why + const reply = dde_ipc.sendSync('serial_devices') + return reply + } + else { + dde_error("The function 'serial_devices' can only be used on dde proper,
" + + "not on the Job Engine.
" + + "You must use serial_devices_async on the Job Engine.") + } +} +module.exports.serial_devices = serial_devices + +function serial_devicies_async_callback_default(err, ports){ + if(err) { + dde_error("serial_devices_async errored with: " + err.message) + } + else { + out("Available serial ports:
" + JSON.stringify(ports)) + } +} + +function serial_devices_async(callback = serial_devicies_async_callback_default){ + SerialPort.list(callback) +} + +module.exports.serial_devices_async = serial_devices_async + + +//only used for testing. +function serial_connect_low_level(path, options, capture_n_items=1, item_delimiter="\n", + trim_whitespace = true, + parse_items=true, capture_extras=false, + callback=onReceiveCallback_low_level, + error_callback=onReceiveErrorCallback_low_level){ + const port = new SerialPort(path, options) + serial_path_to_info_map[path] = + {path: path, //Needed because sometinmes we get the info without having the path thru serial_path_to_connection_idsimulate: simulate, + simulate: false, + port: port, + capture_n_items: capture_n_items, + item_delimiter: item_delimiter, + trim_whitespace: trim_whitespace, + parse_items: parse_items, + capture_extras: capture_extras, + pending_input: ""} + port.on('open', function(err){ + if (err) { + dde_error("new SerialPort to path: " + path + " errored with; " + err) + } + else { + out("Serial connection made to: " + path) + port.on('data', function(data) { callback.call(null, data, path) } ) + port.on('error', function(data) { error_callback.call(null, data, path) } ) + out(stringify_value(serial_path_to_info_map[path])) + } + }) +} +module.exports.serial_connect_low_level = serial_connect_low_level + + +//not used by DDE robots +function serial_send_low_level(path, content){ + let info = serial_path_to_info_map[path] + if (info){ + let arr = + info.port.write(content, //convertStringToArrayBuffer(content), + function(error){ //can't rely on this getting called before onReceived so mostly pretend like its not called, except in error cases + if (error){ + dde_error("In serial_send callback to path: " + path + + " got the error: " + error.message) + } + else { + out("serial write just sent: " + content) + } + }) + } + else { + dde_error("In serial_send, attempt to send to path: " + path + + " which doesn't have info.") + } +} +module.exports.serial_send_low_level = serial_send_low_level + + +//called by serial_connect AND Serial.send +function serial_init_one_info_map_item(path, options, simulate=null, capture_n_items=1, item_delimiter="\n", + trim_whitespace=true, + parse_items=true, capture_extras="error"){ + let sim_actual = Robot.get_simulate_actual(simulate) + serial_path_to_info_map[path] = + {path: path, //Needed because sometimes we get the info without having the path thru serial_path_to_connection_id + simulate: sim_actual, + capture_n_items: capture_n_items, + item_delimiter: item_delimiter, + trim_whitespace: trim_whitespace, + parse_items: parse_items, + capture_extras: capture_extras, + pending_input: ""} + +} + +//called from robot.js +function serial_connect(path, options, simulate=null, capture_n_items=1, item_delimiter="\n", + trim_whitespace=true, + parse_items=true, capture_extras="error", job_instance){ //"ignore", "capture", "error" + const sim_actual = Robot.get_simulate_actual(simulate) + serial_init_one_info_map_item(path, options, simulate, capture_n_items, item_delimiter, + trim_whitespace, parse_items, capture_extras) + if(sim_actual === true){ //if its "both" we let the below handle it. Don't want to call serial_new_socket_callback twice when sim is "both" + serial_new_socket_callback(path, job_instance) //don't need to simulate socket_id for now + } + if ((sim_actual === false) || (sim_actual == "both")){ + const port = new SerialPort(path, options) + port.on('open', function(err){ + if (err) { + dde_error("new SerialPort to path: " + path + " errored with; " + err) + } + else { + out("Serial connection made to: " + path) + serial_path_to_info_map[path].port = port + serial_new_socket_callback(path, job_instance) + let the_path = path //needed for closed over var below + port.on('data', function(data) { serial_onReceiveCallback(data, the_path) } ) + port.on('error', function(data) { onReceiveErrorCallback(data, the_path) } ) + } + }) + } +} + +module.exports.serial_connect = serial_connect + +function serial_new_socket_callback(path, job_instance){ + console.log("serial_new_socket_callback passed: " + "path: " + path) + Serial.set_a_robot_instance_socket_id(path, job_instance) +} + +//_______send data to serial_______ +//https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String?hl=en +//function convertArrayBufferToString(buf) { return String.fromCharCode.apply(null, new Uint16Array(buf));} +//http://www.fabiobiondi.com/blog/2014/02/html5-chrome-packaged-apps-and-arduino-bidirectional-communication-via-serial/ +function convertArrayBufferToString(buf, content_length=null) { + let bufView = new Uint8Array(buf); + let encodedString = String.fromCharCode.apply(null, bufView); + let result = decodeURIComponent(escape(encodedString)); + if (content_length){ result = result.substring(0, content_length) } + return result +}; + +function convertStringToArrayBuffer(str) { + var buf=new ArrayBuffer(str.length); + var bufView=new Uint8Array(buf); + for (var i=0; i") + out("onReceiveCallback_low_level got " + number_of_new_chars_on_end + " bytes of data:
" + new_str_formatted + ".") + } + else { + out("onReceiveCallback_low_level got no data.") + } +} + +function serial_make_default_robot_status_maybe(info){ + if(info.robot_status) { return } + else { + let job_instance = Job.last_job + info.robot_status = [] + info.robot_status[Serial.JOB_ID] = job_instance.program_counter // 0 + info.robot_status[Serial.INSTRUCTION_ID = job_instance.job_id] // 1 + info.robot_status[Serial.START_TIME] = Date.now() // 2 ms since jan 1, 1970? From Dexter's clock + info.robot_status[Serial.STOP_TIME] = Date.now() // 3 ms since jan 1, 1970? From Dexter's clock + info.robot_status[Serial.INSTRUCTION_TYPE] = "I" // 4 "oplet" + info.robot_status[Serial.ERROR_CODE] = 0 // 5 0 means no error + } +} + +function serial_onReceiveCallback(info_from_board, path) { //if there's an error, onReceiveErrorCallback will be called instead + //out("top of serial_onReceiveCallback") + if (info_from_board.length) { //info.connectionId == expectedConnectionId && + //let str = convertArrayBufferToString(info_from_board.buffer, info_from_board.length); //note that if aruino program + let str = info_from_board.toString() + //str should now have in it the NEW chars sent from the serial port. + + //has Serial.println("turned off1"); int foo = 2 + 3; Serial.println("turned off2"); + //then this is ONE call to serial_onReceiveCallback with a string "turned off1\r\nturned off2\t\n" + //so that 1 string should count for 2 items from the board from the dde standpoint. + //maybe the board software batches up the 2 strings and maybe chrome recieve does. + //whichever, I need to handle it. + //Note 2: when simulating, often info_from_board.length will return undefined. + //this is ok as convertArrayBufferToString will just go with the length of the + //info_from_board.buffer, which will usually be correct for the content. + out("serial_onReceiveCallback got " + info_from_board.length + " byte string: '" + str + "'.") + let info = serial_path_to_info_map[path] + if (!info) { + warning("serial_onReceiveCallback got path: " + path + " that's not a known DDE connected path which is possibly OK if you've got other serial devices sending in data.") + } + else { + info.robot_status[Serial.ERROR_CODE] = 0 //since onRecievedErrorCallback wasn't called in place of this, + //we know it didn't error. Can't rely on send callback getting called so do this here. + //first process the returned string + let delim = info.item_delimiter //can be multi_character. + if ((delim === "") || (delim === 0)) { //str holds exactly one item. we never put a substr in pending + if (left_to_capture(info) > 0) { + serial_store_str_in_rs(str, info) + if (left_to_capture(info) == 0){ + serial_on_done_with_sending(info.robot_status, info.path) + } + else { out("waiting for more strings from robot.") } + //beware, they might never come in which case the job is just hanging. + //TODO: Add a timeout option which continues anyway. + //the job has status_code "running" but is really paused waiting for response. + } + else if (info.capture_extras == "capture"){ + serial_send_extra_item_to_job(str, info.path, false, info) //false means everything ok + } + else if (info.capture_extras == "error"){ + serial_send_extra_item_to_job(str, info.path, true, info) //true means job should error + } + else if (info.capture_extras == "ignore") {} + else { dde_error("serial_onReceiveCallback got invalid capture_extras value of: " + info.capture_extras) } + } + else if (typeof(delim) == "string"){ + if (str === "") {}//we do have a real delimiter, but no delimiter in the input str and no chars so this is a no_op + else { + info.pending_input += str + while(true) { + let delim_index = info.pending_input.indexOf(delim); + if(delim_index == -1) { break; } + // no "full" items in the pending input so break now and wait for more data from the serial port + else { //at least one more full item in pending_input. grab it! + let item = info.pending_input.substring(0, delim_index) + info.pending_input = info.pending_input.substring(delim_index + delim.length) + if (info.trim_whitespace) { item = item.trim() } + if (left_to_capture(info) > 0) { + serial_store_str_in_rs(item, info) + if (left_to_capture(info) == 0){ + serial_on_done_with_sending(info.robot_status, info.path) + } + else { out("waiting for more strings from robot.") } + //beware, they might never come in which case the job is just hanging. + //TODO: Add a timeout option which continues anyway. + //the job has status_code "running" but is really paused waiting for response. + } + else if (info.capture_extras == "capture"){ + serial_send_extra_item_to_job(item, info.path, false, info) //false means everything ok + } + else if (info.capture_extras == "error"){ + serial_send_extra_item_to_job(item, info.path, true, info) //true means job should error + } + else if (info.capture_extras == "ignore") {} + else { dde_error("serial_onReceiveCallback got invalid capture_extras value of: " + info.capture_extras) } + } + } + } + } + else if (typeof(delim) == "number"){ + str = info.pending_input + str //pending_input will never contain a delim + info.pending_input = "" //we're using it, so makes this empty, just so there's no using pending twice. + if (str === "") {}//we do have a real delimter, but no delimiter in the input str and no chars so this is a no_op + else { + while(true){ + if (str.length >= delim){ + let item = str.substr(0, delim) //the item this time in the loop + str = str.slice(delim) //get ready for the NEXT iteration + if (left_to_capture(info) > 0) { + serial_store_str_in_rs(item, info) + if (left_to_capture(info) == 0){ + serial_on_done_with_sending(info.robot_status, info.path) + } + else { out("waiting for more strings from robot.") } + //beware, they might never come in which case the job is just hanging. + //TODO: Add a timeout option which continues anyway. + + //the job has status_code "running" but is really paused waiting for response. + } + else if (info.capture_extras == "capture"){ + serial_send_extra_item_to_job(item, info.path, false, info) //false means everything ok + } + else if (info.capture_extras == "error"){ + serial_send_extra_item_to_job(item, info.path, true, info) //true means job should error + } + else if (info.capture_extras == "ignore") {} + else { dde_error("serial_onReceiveCallback got invalid capture_extras value of: " + info.capture_extras) } + } + else { + info.pending = str //might be "" and that's ok + break; + } + } + } + } + else { + dde_error("serial_onReceiveCallback got an item_delimiter of: " + delim + " which is not valid because its not a string or an integer.") + } + } + } +} + +function serial_parse_string_maybe(str, info){ + if (info.parse_items) { + try { + str = JSON.parse(str) + } + catch(e) {} + } + return str +} + +function serial_store_str_in_rs(str, info){ + str = serial_parse_string_maybe(str, info) + info.robot_status.push(str) +} + +function onReceiveErrorCallback_low_level(info_from_board, path) { + //let id = info_from_board.connectionId + let errnum = info_from_board.error + let error_codes = ["disconnected", "timeout", "device_lost", "break", "frame_error", "overrun", + "buffer_overflow", "parity_error", "system_error"] //beware, + //it wouldn't surprise me if "disconnected" was really error_code 1 instead of 0. + //but this is what https://developer.chrome.com/apps/serial#event-onReceive sez. + let info = serial_path_to_info_map[path] + let rs = info.robot_status + out("onReceiveErrorCallback called with path: " + info.path, "red") + rs[Serial.ERROR_CODE] = error_codes[errnum] + serial_on_done_with_sending(rs, info.path) +} + +function onReceiveErrorCallback(info_from_board, path) { + //let id = info_from_board.connectionId + let errnum = info_from_board.error + let error_codes = ["disconnected", "timeout", "device_lost", "break", "frame_error", "overrun", + "buffer_overflow", "parity_error", "system_error"] //beware, + //it wouldn't surprise me if "disconnected" was really error_code 1 instead of 0. + //but this is what https://developer.chrome.com/apps/serial#event-onReceive sez. + let info = serial_path_to_info_map[path] + let rs = info.robot_status + out("onReceiveErrorCallback called with path: " + info.path, "red") + rs[Serial.ERROR_CODE] = error_codes[errnum] + serial_on_done_with_sending(rs, info.path) +} + +//like socket on_receive +function serial_on_done_with_sending(robot_status, path){ + robot_status[Serial.STOP_TIME] = Date.now() + out("serial_on_done_with_sending with rs: " + robot_status) + let job_id = robot_status[Serial.JOB_ID] + let job_instance = Job.job_id_to_job_instance(job_id) + let ins_id = robot_status[Serial.INSTRUCTION_ID] + let ins = ((ins_id >= 0) ? job_instance.do_list[ins_id] : null) + let rob + if (ins && ins.robot) { rob = ins.robot } //used when instruction src code has a subject of a robot isntance + else { rob = job_instance.robot } //get the default robot for the job + rob.robot_done_with_instruction(robot_status) +} + +function serial_send_extra_item_to_job(string_from_robot, path, is_error=false, info){ //info only needed on UI side. + string_from_robot = serial_parse_string_maybe(string_from_robot, info) //after this, might not be a string anymore + let job_instance = Serial.get_job_with_robot_path(path) + if (job_instance){ + job_instance.robot.robot_status.push(string_from_robot) //just add to the current robot_status. Might not be right. + if (is_error){ + job_instance.stop_for_reason("errored", "Serial robot returned unexpected extra string: " + string_from_robot) + } + } +} + +function serial_flush(path){ + let info = serial_path_to_info(path) + if (info && ((info.simulate === false) || (info.simulate === "both"))) { + info.port.flush(function(){ out("Serial path: " + path + " flushed.") }) + } + else { + warning("Attempt to serial_flush path: " + path + " but that path doesn't have info.") + } +} +module.exports.serial_flush = serial_flush + + +//with an arduino connected, this disconnects but then immdiately afterwards, +//a new connection is automatically made with 1 higher port number and same path. +function serial_disconnect(path){ + let info = serial_path_to_info_map[path] + if (info){ + if((info.simulate === false) || (info.simulate === "both")) { + info.port.close(out) + serial_flush(path) //perhaps unnecessary + } + delete serial_path_to_info_map[path] + } +} +module.exports.serial_disconnect = serial_disconnect + + +function serial_disconnect_all(){ + for(let path of Object.keys(serial_path_to_info_map)){ + serial_disconnect(path) + } +} +module.exports.serial_disconnect_all = serial_disconnect_all + + +var {dde_error} = require("./utils.js")