|
| 1 | +const EventEmitter = require('events') |
| 2 | +const SerialPort = require('serialport') |
| 3 | + |
| 4 | +const codeListFiles = ` |
| 5 | +from os import listdir |
| 6 | +print('<BEGINREC>') |
| 7 | +print(listdir()) |
| 8 | +print('<ENDREC>') |
| 9 | +` |
| 10 | +const codeLoadFile = (path) => { |
| 11 | + return ` |
| 12 | +print('<BEGINREC>') |
| 13 | +with open('${path}', 'r') as f: |
| 14 | + line = f.readline() |
| 15 | + while line != '': |
| 16 | + print(line, end='') |
| 17 | + line = f.readline() |
| 18 | +print('<ENDREC>') |
| 19 | +` |
| 20 | +} |
| 21 | + |
| 22 | +const codeRemoveFile = (path) => { |
| 23 | + return ` |
| 24 | +from os import remove |
| 25 | +remove('${path}') |
| 26 | +` |
| 27 | +} |
| 28 | + |
| 29 | +const codeRenameFile = (oldPath, newPath) => { |
| 30 | + return ` |
| 31 | +from os import rename |
| 32 | +rename('${oldPath}', '${newPath}') |
| 33 | +` |
| 34 | +} |
| 35 | + |
| 36 | +const codeCollectGarbage = `import gc |
| 37 | +gc.collect()` |
| 38 | + |
| 39 | +class SerialConnection extends EventEmitter { |
| 40 | + constructor() { |
| 41 | + super() |
| 42 | + this.rawRepl = false |
| 43 | + this.loadingFile = false |
| 44 | + this.loadingFileList = false |
| 45 | + } |
| 46 | + /** |
| 47 | + * List all available serial ports (with vendor id) |
| 48 | + * @return {Promise} Resolves with an array of port objects |
| 49 | + */ |
| 50 | + static listAvailable() { |
| 51 | + return new Promise((resolve, reject) => { |
| 52 | + SerialPort.list().then((ports) => { |
| 53 | + const availablePorts = ports.filter((port) => { |
| 54 | + return !!port.vendorId |
| 55 | + }) |
| 56 | + if (availablePorts) { |
| 57 | + resolve(availablePorts) |
| 58 | + } else { |
| 59 | + reject(new Error('No ports available')) |
| 60 | + } |
| 61 | + }) |
| 62 | + }) |
| 63 | + } |
| 64 | + /** |
| 65 | + * Opens a connection on a given port. |
| 66 | + * @param {String} port Port address to open the connection |
| 67 | + */ |
| 68 | + open(port) { |
| 69 | + this.port = new SerialPort(port, { |
| 70 | + baudRate: 115200, |
| 71 | + autoOpen: false |
| 72 | + }) |
| 73 | + this.port.on('open', () => { |
| 74 | + this.emit('connected') |
| 75 | + this.port.write('\r') |
| 76 | + }) |
| 77 | + this.port.on('data', (data) => this._eventHandler(data)) |
| 78 | + this.port.open() |
| 79 | + } |
| 80 | + /** |
| 81 | + * Closes current connection. |
| 82 | + */ |
| 83 | + close() { |
| 84 | + this.emit('disconnected') |
| 85 | + if (this.port) { |
| 86 | + this.port.close() |
| 87 | + } |
| 88 | + } |
| 89 | + /** |
| 90 | + * Executes code in a string format. This code can contain multiple lines. |
| 91 | + * @param {String} code String of code to be executed. Line breaks must be `\n` |
| 92 | + */ |
| 93 | + execute(code) { |
| 94 | + // TODO: break code in lines and `_execRaw` line by line |
| 95 | + this.stop() |
| 96 | + this._enterRawRepl() |
| 97 | + this._executeRaw(code) |
| 98 | + .then(() => { |
| 99 | + this._exitRawRepl() |
| 100 | + }) |
| 101 | + } |
| 102 | + /** |
| 103 | + * Evaluate a command/expression. |
| 104 | + * @param {String} command Command/expression to be evaluated |
| 105 | + */ |
| 106 | + evaluate(command) { |
| 107 | + this.port.write(Buffer.from(command)) |
| 108 | + } |
| 109 | + /** |
| 110 | + * Send a "stop" command in order to interrupt any running code. For serial |
| 111 | + * REPL this command is "CTRL-C". |
| 112 | + */ |
| 113 | + stop() { |
| 114 | + this.port.write('\r\x03') // CTRL-C |
| 115 | + } |
| 116 | + /** |
| 117 | + * Send a command to "soft reset". |
| 118 | + */ |
| 119 | + softReset() { |
| 120 | + this.stop(); |
| 121 | + this.port.write('\r\x04') // CTRL-D |
| 122 | + } |
| 123 | + /** |
| 124 | + * Prints on console the existing files on file system. |
| 125 | + */ |
| 126 | + listFiles() { |
| 127 | + this.data = '' |
| 128 | + this.loadingFileList = true |
| 129 | + this.execute(codeListFiles) |
| 130 | + } |
| 131 | + /** |
| 132 | + * Prints on console the content of a given file. |
| 133 | + * @param {String} path File's path |
| 134 | + */ |
| 135 | + loadFile(path) { |
| 136 | + this.data = '' |
| 137 | + this.loadingFile = true |
| 138 | + this.execute(codeLoadFile(path)) |
| 139 | + } |
| 140 | + /** |
| 141 | + * Writes a given content to a file in the file system. |
| 142 | + * @param {String} path File's path |
| 143 | + * @param {String} content File's content |
| 144 | + */ |
| 145 | + writeFile(path, content) { |
| 146 | + if (!path || !content) { |
| 147 | + return |
| 148 | + } |
| 149 | + // TODO: Find anoter way to do it without binascii |
| 150 | + let pCode = `f = open('${path}', 'w')\n` |
| 151 | + // pCode += `import gc; gc.collect()\n` |
| 152 | + pCode += codeCollectGarbage + '\n' |
| 153 | + // `content` is what comes from the editor. We want to write it |
| 154 | + // line one by one on a file so we split by `\n` |
| 155 | + var lineCount = 0; |
| 156 | + var lines = content.split('\r\n') |
| 157 | + lines.forEach((line) => { |
| 158 | + if (line) { |
| 159 | + var nlMarker = line.indexOf('\n'); |
| 160 | + var crMarker = line.indexOf('\r'); |
| 161 | + // TODO: Sanitize line replace """ with \""" |
| 162 | + // To avoid the string escaping with weirdly we encode |
| 163 | + // the line plus the `\n` that we just removed to base64 |
| 164 | + pCode += `f.write("""${line}""")` |
| 165 | + if(lineCount != lines.length - 1){ |
| 166 | + pCode += `\nf.write('\\n')\n` |
| 167 | + } |
| 168 | + lineCount++; |
| 169 | + } |
| 170 | + }) |
| 171 | + pCode += `\nf.close()\n` |
| 172 | + this.execute(pCode) |
| 173 | + } |
| 174 | + |
| 175 | + /** |
| 176 | + * Removes file on a given path |
| 177 | + * @param {String} path File's path |
| 178 | + */ |
| 179 | + removeFile(path) { |
| 180 | + this.execute(codeRemoveFile(path)) |
| 181 | + } |
| 182 | + |
| 183 | + renameFile(oldPath, newPath) { |
| 184 | + this.execute(codeRenameFile(oldPath, newPath)) |
| 185 | + } |
| 186 | + /** |
| 187 | + * Handles data comming from connection |
| 188 | + * @param {Buffer} buffer Data comming from connection |
| 189 | + */ |
| 190 | + _eventHandler(buffer) { |
| 191 | + const data = buffer.toString() |
| 192 | + this.emit('output', data) |
| 193 | + |
| 194 | + // Getting data that should be sent to frontend |
| 195 | + // Loading file content, listing files, etc |
| 196 | + // if (data.indexOf('<REC>') !== -1) { |
| 197 | + // this.recordingData = true |
| 198 | + // } |
| 199 | + // if (this.recordingData) { |
| 200 | + // this.data += data |
| 201 | + // } |
| 202 | + // if (data.indexOf('<EOF>') !== -1) { |
| 203 | + // const iofREC = this.data.indexOf('<REC>') |
| 204 | + // const rec = this.data.indexOf('<REC>\r\n')+7 |
| 205 | + // const eof = this.data.indexOf('<EOF>') |
| 206 | + // if (this.loadingFile) { |
| 207 | + // this.emit('file-loaded', this.data.slice(rec, eof)) |
| 208 | + // this.loadingFile = false |
| 209 | + // } |
| 210 | + // if (this.loadingFileList) { |
| 211 | + // this.emit('file-list-loaded', this.data.slice(rec, eof)) |
| 212 | + // this.loadingFileList = false |
| 213 | + // } |
| 214 | + // this.recordingData = false |
| 215 | + // } |
| 216 | + |
| 217 | + if (this.rawRepl && data.indexOf('\n>>> ') != -1) { |
| 218 | + this.emit('execution-finished') |
| 219 | + this.rawRepl = false |
| 220 | + } |
| 221 | + |
| 222 | + if (!this.rawRepl && data.indexOf('raw REPL;') != -1) { |
| 223 | + this.emit('execution-started') |
| 224 | + this.rawRepl = true |
| 225 | + } |
| 226 | + } |
| 227 | + /** |
| 228 | + * Put REPL in raw mode |
| 229 | + */ |
| 230 | + _enterRawRepl() { |
| 231 | + this.port.write('\r\x01') // CTRL-A |
| 232 | + } |
| 233 | + /** |
| 234 | + * Exit REPL raw mode |
| 235 | + */ |
| 236 | + _exitRawRepl() { |
| 237 | + this.port.write('\r\x04\r\x02') // CTRL-D // CTRL-B |
| 238 | + } |
| 239 | + /** |
| 240 | + * Writes a command to connected port |
| 241 | + * @param {String} command Command to be written on connected port |
| 242 | + */ |
| 243 | + _executeRaw(command) { |
| 244 | + const writePromise = (buffer) => { |
| 245 | + return new Promise((resolve, reject) => { |
| 246 | + setTimeout(() => { |
| 247 | + this.port.write(buffer, (err) => { |
| 248 | + if (err) return reject() |
| 249 | + resolve() |
| 250 | + }) |
| 251 | + }, 1) |
| 252 | + }) |
| 253 | + } |
| 254 | + const l = 1024 |
| 255 | + let slices = [] |
| 256 | + for(let i = 0; i < command.length; i+=l) { |
| 257 | + let slice = command.slice(i, i+l) |
| 258 | + slices.push(slice) |
| 259 | + } |
| 260 | + return new Promise((resolve, reject) => { |
| 261 | + slices.reduce((cur, next) => { |
| 262 | + return cur.then(() => { |
| 263 | + return writePromise(next) |
| 264 | + }) |
| 265 | + }, Promise.resolve()) |
| 266 | + .then() |
| 267 | + .then(() => { |
| 268 | + resolve() |
| 269 | + }) |
| 270 | + }) |
| 271 | + } |
| 272 | +} |
| 273 | + |
| 274 | +module.exports = SerialConnection |
0 commit comments