From 6e8b8eac8b5fd9d3e67ba65bfc2b0433393ab548 Mon Sep 17 00:00:00 2001 From: Sergey Polyakov Date: Thu, 11 Apr 2019 22:55:25 +0200 Subject: [PATCH 1/9] `particle log` command --- src/cli/index.js | 2 + src/cli/log.js | 39 ++++++ src/cmd/log.js | 351 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 392 insertions(+) create mode 100644 src/cli/log.js create mode 100644 src/cmd/log.js diff --git a/src/cli/index.js b/src/cli/index.js index 0795c4e56..e373bb842 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -24,6 +24,7 @@ const webhook = require('./webhook'); const whoami = require('./whoami'); const mesh = require('./mesh'); const usb = require('./usb'); +const log = require('./log'); /** * The default function export from this module registers all the available commands. @@ -66,5 +67,6 @@ module.exports = function registerAllCommands(context) { whoami(context); mesh(context); usb(context); + log(context); alias(context); }; diff --git a/src/cli/log.js b/src/cli/log.js new file mode 100644 index 000000000..56572def5 --- /dev/null +++ b/src/cli/log.js @@ -0,0 +1,39 @@ +const settings = require('../../settings'); + +function logCommand() { + if (!logCommand._instance) { + const LogCommand = require('../cmd/log'); + logCommand._instance = new LogCommand(settings); + } + return logCommand._instance; +} + +module.exports = ({ commandProcessor, root }) => { + commandProcessor.createCommand(root, 'log', 'Display log messages from a device', { + params: ' [stream] [serial_port]', + options: { + 'level': { + description: 'Default logging level', + string: true, + alias: 'l' + }, + 'filter': { + description: 'Category filter', + string: true, + alias: 'f' + }, + 'baud': { + description: 'Baud rate', + number: true, + alias: 'b' + }, + 'raw': { + description: 'Display raw logging output', + boolean: true + } + }, + handler: (args) => { + return logCommand().run(args); + } + }); +}; diff --git a/src/cmd/log.js b/src/cmd/log.js new file mode 100644 index 000000000..72bc42910 --- /dev/null +++ b/src/cmd/log.js @@ -0,0 +1,351 @@ +const ParticleApi = require('./api').default; +const { openUsbDevice, openUsbDeviceById } = require('./usb-util'); +const { prompt, spin } = require('../app/ui'); +const deviceSpecs = require('../lib/deviceSpecs'); + +const SerialPort = require('serialport'); +const chalk = require('chalk'); +const when = require('when'); + +const EventEmitter = require('events'); + +// USB vedor/product IDs of the Particle devices in the CDC mode +const PARTICLE_USB_IDS = Object.values(deviceSpecs).reduce((set, spec) => { + return set.add(`${spec.serial.vid}:${spec.serial.pid}`.toLowerCase()); // vid:pid +}, new Set()); + +const STREAM_TYPES = [ + { + name: 'Serial', + isUsbSerial: true + }, + { + name: 'USBSerial1', + isUsbSerial: true + }, + { + name: 'Serial1' + } +]; + +const LOG_LEVELS = [ 'all', 'trace', 'info', 'warn', 'error', 'none' ]; + +const DEFAULT_STREAM = 'Serial'; +const DEFAULT_BAUD_RATE = 115200; +const DEFAULT_LOG_LEVEL = 'trace'; + +const CATEGORY_FIELD_WIDTH = 14; + +class PrettyFormatter extends EventEmitter { + constructor() { + super(); + this._buf = ''; + } + + update(data) { + this._buf += data.toString('ascii'); + for (;;) { + const r = this._findJsonObject(this._buf); + if (!r.valid) { + this._buf = ''; + break; // The buffer doesn't contain JSON objects + } + if (!r.complete) { + break; // Wait for more input + } + let msg = this._buf.slice(r.startIndex, r.endIndex); + this._buf = this._buf.slice(r.endIndex); + try { + msg = JSON.parse(msg); + } catch (e) { + continue; // Not a valid JSON document + } + if (!msg.l || !msg.t) { + continue; // Not a log message + } + msg = this._formatMessage(msg); + this.emit('data', msg); + } + } + + _formatMessage(msg) { + let m = msg.m; + switch (msg.l) { + case 't': // Trace + m = chalk.dim(m); + break; + case 'w': // Warning + m = chalk.yellow.bold(m); + break; + case 'e': // Error + m = chalk.red.bold(m); + break; + } + const t = this._formatTimestamp(msg.t); + const c = this._formatCategory(msg.c); + return `${chalk.dim(t)} ${chalk.dim(':')} ${chalk.dim(c)} ${chalk.dim(':')} ${m}\n`; + } + + _formatTimestamp(t) { + const totalSec = Math.floor(t / 1000); + const totalMin = Math.floor(totalSec / 60); + const msec = (t % 1000).toString().padStart(3, '0'); + const sec = (totalSec % 60).toString().padStart(2, '0'); + const min = (totalMin % 60).toString().padStart(2, '0'); + const hr = Math.floor(totalMin / 60).toString().padStart(2, '0'); + return `${hr}:${min}:${sec}.${msec}`; + } + + _formatCategory(c) { + if (!c) { + c = ''; + } + if (c.length > CATEGORY_FIELD_WIDTH) { + c = c.slice(0, CATEGORY_FIELD_WIDTH - 1); + if (c.charAt(c.length - 1) === '.') { + c = c.slice(0, c.length - 1); + } + c += '~'; + } + c = c.padEnd(CATEGORY_FIELD_WIDTH, ' '); + return c; + } + + // Finds a substring that looks like a valid JSON object + _findJsonObject(srcStr) { + const br = []; + let start = null; + let esc = false; + let str = false; + let i = 0; + for (; i < srcStr.length; ++i) { + const c = srcStr.charAt(i); + if (!str) { + if (c === '{') { + br.push(c); + if (start === null) { + start = i; + } + } if (start !== null) { + if (c === '[') { + br.push(c); + } else if (c === '}' || c === ']') { + const c1 = br.pop(); + if ((c === '}' && c1 !== '{') || (c === ']' && c1 !== '[')) { + return { valid: false }; + } + if (br.length === 0) { + break; // Done + } + } else if (c === '"') { + str = true; + } + } + } else if (esc) { + esc = false; + } else if (c === '"') { + str = false; + } else if (c === '\\') { + esc = true; + } + } + if (i === srcStr.length) { + if (start === null) { + return { valid: false }; + } + return { + valid: true, + complete: false, + startIndex: start + }; + } + return { + valid: true, + complete: true, + startIndex: start, + endIndex: i + 1 + }; + } +} + +function findStreamType(streamName) { + const name = streamName.toLowerCase(); + const stream = STREAM_TYPES.find(s => s.name.toLowerCase() === name); + if (!stream) { + throw new Error(`Unknown stream type: ${streamName}`); + } + return stream; +} + +function parseLogLevel(levelStr) { + const str = levelStr.toLowerCase(); + const level = LOG_LEVELS.find(l => l.startsWith(str)); + if (!level) { + throw new Error(`Invalid logging level: ${levelStr}`); + } + return level; +} + +function handlerIdForStreamType(streamType) { + return `__cli_${streamType.name}`; +} + +function isParticleSerialPort(port) { + if (!port.vendorId || !port.productId) { + return false; + } + const s = `${port.vendorId}:${port.productId}`.toLowerCase(); + return PARTICLE_USB_IDS.has(s); +} + +module.exports = class LogCommand { + constructor(settings) { + this._auth = settings.access_token; + this._api = new ParticleApi(settings.apiUrl, { accessToken: this._auth }).api; + } + + run(args) { + let streamType = null; + let defaultLevel = null; + let filters = null; + let baudRate = null; + let rawFormat = null; + let usbDevice = null; + let deviceId = null; + let handlerId = null; + return when.resolve().then(() => { + // Parse arguments + streamType = findStreamType(args.params.stream || DEFAULT_STREAM); + defaultLevel = args.level ? parseLogLevel(args.level) : DEFAULT_LOG_LEVEL; + filters = this._parseFilters(args); + baudRate = args.baud || DEFAULT_BAUD_RATE; + rawFormat = args.raw; + // Open the device + return openUsbDeviceById({ id: args.params.device, api: this._api, auth: this._auth }); + }) + .then(dev => { + // Configure logging + usbDevice = dev; + deviceId = usbDevice.id; + handlerId = handlerIdForStreamType(streamType); + const p = usbDevice.addLogHandler({ + id: handlerId, + format: rawFormat ? 'default' : 'json', + stream: streamType.name, + level: defaultLevel, + filters: filters, + baudRate: baudRate + }) + .then(() => { + return usbDevice.close(); + }); + return spin(p, 'Configuring the device...'); + }) + .then(() => { + // Get the serial port assigned to the device + if (args.params.serial_port) { + return args.params.serial_port; + } + return this._findSerialPort(streamType, deviceId); + }) + .then(portName => { + // Open serial port + return when.promise((resolve, reject) => { + console.log(`Opening serial port: ${portName}`); + const port = new SerialPort(portName, { baudRate }, err => { + if (err) { + return reject(err); + } + resolve(port); + }); + }); + }) + .then(port => { + // Unregister the log handler on exit + const onExit = () => { + let error = null; + openUsbDevice(usbDevice).then(() => { + return usbDevice.removeLogHandler({ id: handlerId }); + }) + .then(() => { + return usbDevice.close(); + }) + .catch(e => { + error = e; + }) + .finally(() => { + process.stdout.write('\n'); + process.exit(error ? 1 : 0); + }); + }; + process.on('SIGINT', onExit); + process.on('SIGTERM', onExit); + // Start reading the logging output + console.log('Press Ctrl-C to exit.'); + let log = port; + if (!rawFormat) { + log = new PrettyFormatter(); + port.on('data', d => log.update(d)); + } + log.on('data', d => process.stdout.write(d)); + }) + .finally(() => { + if (usbDevice) { + return usbDevice.close(); + } + }); + } + + _findSerialPort(streamType, deviceId) { + return when.resolve().then(() => { + return SerialPort.list(); + }) + .then(ports => { + if (streamType.isUsbSerial) { + // Get all ports with a matching serial number + ports = ports.filter(p => p.serialNumber && p.serialNumber.toLowerCase() === deviceId); + } else { + // Filter out all Particle and system TTY devices + ports = ports.filter(p => p.vendorId && p.productId && !isParticleSerialPort(p)); + } + if (ports.length == 0) { + throw new Error('Serial port is not found'); + } + if (ports.length == 1) { + return ports[0].comName; + } + return prompt({ + name: 'port', + type: 'list', + message: `Please specify the serial port assigned to ${streamType.name}`, + choices: ports.map(p => p.comName) + }) + .then(r => r.port); + }); + } + + _parseFilters(args) { + args = args.filter; + if (!args) { + return null; + } + if (typeof(args) === 'string') { + args = [ args ]; + } + return args.map(arg => { + const s = arg.split(':'); // category:level + if (s.length > 2 || !s[0] || (s.length == 2 && !s[1])) { + throw new Error('Invalid filter string'); + } + const f = { + category: s[0] + }; + if (s.length == 2) { + f.level = parseLogLevel(s[1]); + } else { + f.level = 'all'; + } + return f; + }); + } +}; From 2277f22b741f173e99221cd34c1c2dda4ed53812 Mon Sep 17 00:00:00 2001 From: Sergey Polyakov Date: Sat, 13 Apr 2019 00:04:29 +0200 Subject: [PATCH 2/9] Improve error handling; add comments --- src/cmd/log.js | 106 +++++++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 44 deletions(-) diff --git a/src/cmd/log.js b/src/cmd/log.js index 72bc42910..7e49dbdc3 100644 --- a/src/cmd/log.js +++ b/src/cmd/log.js @@ -32,7 +32,7 @@ const LOG_LEVELS = [ 'all', 'trace', 'info', 'warn', 'error', 'none' ]; const DEFAULT_STREAM = 'Serial'; const DEFAULT_BAUD_RATE = 115200; -const DEFAULT_LOG_LEVEL = 'trace'; +const DEFAULT_LOG_LEVEL = 'all'; const CATEGORY_FIELD_WIDTH = 14; @@ -69,7 +69,7 @@ class PrettyFormatter extends EventEmitter { } _formatMessage(msg) { - let m = msg.m; + let m = this._formatText(msg.m); switch (msg.l) { case 't': // Trace m = chalk.dim(m); @@ -86,6 +86,11 @@ class PrettyFormatter extends EventEmitter { return `${chalk.dim(t)} ${chalk.dim(':')} ${chalk.dim(c)} ${chalk.dim(':')} ${m}\n`; } + _formatText(m) { + // TODO: Split long messages into multiple lines depending on the terminal width + return m; + } + _formatTimestamp(t) { const totalSec = Math.floor(t / 1000); const totalMin = Math.floor(totalSec / 60); @@ -124,38 +129,38 @@ class PrettyFormatter extends EventEmitter { if (c === '{') { br.push(c); if (start === null) { - start = i; + start = i; // Beginning of a JSON object } - } if (start !== null) { + } if (start !== null) { // Keep skipping characters until the first '{' is found if (c === '[') { br.push(c); } else if (c === '}' || c === ']') { const c1 = br.pop(); if ((c === '}' && c1 !== '{') || (c === ']' && c1 !== '[')) { - return { valid: false }; + return { valid: false }; // Invalid closing bracket } if (br.length === 0) { - break; // Done + break; // End of the JSON object } } else if (c === '"') { - str = true; + str = true; // Beginning of a string } } } else if (esc) { - esc = false; - } else if (c === '"') { - str = false; + esc = false; // Ignore the escaped character } else if (c === '\\') { - esc = true; + esc = true; // Beginning of an escaped character + } else if (c === '"') { + str = false; // End of the string } } if (i === srcStr.length) { if (start === null) { - return { valid: false }; + return { valid: false }; // The string doesn't contain a '{' character } return { valid: true, - complete: false, + complete: false, // Found an incomplete JSON object startIndex: start }; } @@ -181,7 +186,7 @@ function parseLogLevel(levelStr) { const str = levelStr.toLowerCase(); const level = LOG_LEVELS.find(l => l.startsWith(str)); if (!level) { - throw new Error(`Invalid logging level: ${levelStr}`); + throw new Error(`Invalid log level: ${levelStr}`); } return level; } @@ -209,34 +214,35 @@ module.exports = class LogCommand { let defaultLevel = null; let filters = null; let baudRate = null; - let rawFormat = null; let usbDevice = null; + let serialPort = null; let deviceId = null; let handlerId = null; + let handlerEnabled = false; return when.resolve().then(() => { // Parse arguments streamType = findStreamType(args.params.stream || DEFAULT_STREAM); defaultLevel = args.level ? parseLogLevel(args.level) : DEFAULT_LOG_LEVEL; filters = this._parseFilters(args); baudRate = args.baud || DEFAULT_BAUD_RATE; - rawFormat = args.raw; // Open the device return openUsbDeviceById({ id: args.params.device, api: this._api, auth: this._auth }); }) .then(dev => { - // Configure logging + // Enable logging usbDevice = dev; deviceId = usbDevice.id; handlerId = handlerIdForStreamType(streamType); const p = usbDevice.addLogHandler({ id: handlerId, - format: rawFormat ? 'default' : 'json', + format: args.raw ? 'default' : 'json', stream: streamType.name, level: defaultLevel, filters: filters, baudRate: baudRate }) .then(() => { + handlerEnabled = true; return usbDevice.close(); }); return spin(p, 'Configuring the device...'); @@ -261,37 +267,46 @@ module.exports = class LogCommand { }); }) .then(port => { - // Unregister the log handler on exit - const onExit = () => { - let error = null; - openUsbDevice(usbDevice).then(() => { - return usbDevice.removeLogHandler({ id: handlerId }); - }) - .then(() => { - return usbDevice.close(); - }) - .catch(e => { - error = e; - }) - .finally(() => { - process.stdout.write('\n'); - process.exit(error ? 1 : 0); - }); - }; - process.on('SIGINT', onExit); - process.on('SIGTERM', onExit); + serialPort = port; + // TODO: Try to reopen the serial port and re-register the log handler on USB/serial port errors + const p = when.promise((resolve, reject) => { + serialPort.on('error', err => reject(err)); + serialPort.on('close', err => err ? reject(err) : resolve()); + }); + // Close the serial port on exit + process.on('SIGINT', () => serialPort.close()); + process.on('SIGTERM', () => serialPort.close()); // Start reading the logging output console.log('Press Ctrl-C to exit.'); - let log = port; - if (!rawFormat) { + let log = serialPort; + if (!args.raw) { log = new PrettyFormatter(); - port.on('data', d => log.update(d)); + serialPort.on('data', d => log.update(d)); } log.on('data', d => process.stdout.write(d)); + return p; + }) + .finally(() => { + if (serialPort) { + // Close the serial port + return when.promise((resolve, reject) => { + serialPort.close(() => resolve()); // Ignore errors + }); + } + }) + .finally(() => { + if (usbDevice && handlerEnabled) { + // Unregister the log handler + return openUsbDevice(usbDevice) + .then(() => usbDevice.removeLogHandler({ id: handlerId })) + .catch(e => {}); // Ignore errors + } }) .finally(() => { if (usbDevice) { - return usbDevice.close(); + // Close the USB device + return usbDevice.close() + .catch(e => {}); // Ignore errors } }); } @@ -302,10 +317,12 @@ module.exports = class LogCommand { }) .then(ports => { if (streamType.isUsbSerial) { - // Get all ports with a matching serial number + // Get all USB serial ports with a matching serial number + // TODO: Check the interface index to identify ports assigned to Serial and USBSerial1 ports = ports.filter(p => p.serialNumber && p.serialNumber.toLowerCase() === deviceId); } else { - // Filter out all Particle and system TTY devices + // Filter out all Particle and non-USB serial ports + // FIXME: This will likely filter out built-in UARTs on Raspberry Pi ports = ports.filter(p => p.vendorId && p.productId && !isParticleSerialPort(p)); } if (ports.length == 0) { @@ -314,6 +331,7 @@ module.exports = class LogCommand { if (ports.length == 1) { return ports[0].comName; } + // Let the user identify the serial port return prompt({ name: 'port', type: 'list', @@ -343,7 +361,7 @@ module.exports = class LogCommand { if (s.length == 2) { f.level = parseLogLevel(s[1]); } else { - f.level = 'all'; + f.level = DEFAULT_LOG_LEVEL; } return f; }); From bae93bc0e2bd5261278e7e4011f9c96b3ed650ab Mon Sep 17 00:00:00 2001 From: Sergey Polyakov Date: Sat, 13 Apr 2019 00:10:08 +0200 Subject: [PATCH 3/9] Fix linting errors --- src/cmd/log.js | 622 ++++++++++++++++++++++++------------------------- 1 file changed, 311 insertions(+), 311 deletions(-) diff --git a/src/cmd/log.js b/src/cmd/log.js index 7e49dbdc3..9bc9c2137 100644 --- a/src/cmd/log.js +++ b/src/cmd/log.js @@ -11,24 +11,24 @@ const EventEmitter = require('events'); // USB vedor/product IDs of the Particle devices in the CDC mode const PARTICLE_USB_IDS = Object.values(deviceSpecs).reduce((set, spec) => { - return set.add(`${spec.serial.vid}:${spec.serial.pid}`.toLowerCase()); // vid:pid + return set.add(`${spec.serial.vid}:${spec.serial.pid}`.toLowerCase()); // vid:pid }, new Set()); const STREAM_TYPES = [ - { - name: 'Serial', - isUsbSerial: true - }, - { - name: 'USBSerial1', - isUsbSerial: true - }, - { - name: 'Serial1' - } + { + name: 'Serial', + isUsbSerial: true + }, + { + name: 'USBSerial1', + isUsbSerial: true + }, + { + name: 'Serial1' + } ]; -const LOG_LEVELS = [ 'all', 'trace', 'info', 'warn', 'error', 'none' ]; +const LOG_LEVELS = ['all', 'trace', 'info', 'warn', 'error', 'none']; const DEFAULT_STREAM = 'Serial'; const DEFAULT_BAUD_RATE = 115200; @@ -37,170 +37,170 @@ const DEFAULT_LOG_LEVEL = 'all'; const CATEGORY_FIELD_WIDTH = 14; class PrettyFormatter extends EventEmitter { - constructor() { - super(); - this._buf = ''; - } + constructor() { + super(); + this._buf = ''; + } - update(data) { - this._buf += data.toString('ascii'); - for (;;) { - const r = this._findJsonObject(this._buf); - if (!r.valid) { - this._buf = ''; - break; // The buffer doesn't contain JSON objects - } - if (!r.complete) { - break; // Wait for more input - } - let msg = this._buf.slice(r.startIndex, r.endIndex); - this._buf = this._buf.slice(r.endIndex); - try { - msg = JSON.parse(msg); - } catch (e) { - continue; // Not a valid JSON document - } - if (!msg.l || !msg.t) { - continue; // Not a log message - } - msg = this._formatMessage(msg); - this.emit('data', msg); - } - } + update(data) { + this._buf += data.toString('ascii'); + for (;;) { + const r = this._findJsonObject(this._buf); + if (!r.valid) { + this._buf = ''; + break; // The buffer doesn't contain JSON objects + } + if (!r.complete) { + break; // Wait for more input + } + let msg = this._buf.slice(r.startIndex, r.endIndex); + this._buf = this._buf.slice(r.endIndex); + try { + msg = JSON.parse(msg); + } catch (e) { + continue; // Not a valid JSON document + } + if (!msg.l || !msg.t) { + continue; // Not a log message + } + msg = this._formatMessage(msg); + this.emit('data', msg); + } + } - _formatMessage(msg) { - let m = this._formatText(msg.m); - switch (msg.l) { - case 't': // Trace - m = chalk.dim(m); - break; - case 'w': // Warning - m = chalk.yellow.bold(m); - break; - case 'e': // Error - m = chalk.red.bold(m); - break; - } - const t = this._formatTimestamp(msg.t); - const c = this._formatCategory(msg.c); - return `${chalk.dim(t)} ${chalk.dim(':')} ${chalk.dim(c)} ${chalk.dim(':')} ${m}\n`; - } + _formatMessage(msg) { + let m = this._formatText(msg.m); + switch (msg.l) { + case 't': // Trace + m = chalk.dim(m); + break; + case 'w': // Warning + m = chalk.yellow.bold(m); + break; + case 'e': // Error + m = chalk.red.bold(m); + break; + } + const t = this._formatTimestamp(msg.t); + const c = this._formatCategory(msg.c); + return `${chalk.dim(t)} ${chalk.dim(':')} ${chalk.dim(c)} ${chalk.dim(':')} ${m}\n`; + } - _formatText(m) { - // TODO: Split long messages into multiple lines depending on the terminal width - return m; - } + _formatText(m) { + // TODO: Split long messages into multiple lines depending on the terminal width + return m; + } - _formatTimestamp(t) { - const totalSec = Math.floor(t / 1000); - const totalMin = Math.floor(totalSec / 60); - const msec = (t % 1000).toString().padStart(3, '0'); - const sec = (totalSec % 60).toString().padStart(2, '0'); - const min = (totalMin % 60).toString().padStart(2, '0'); - const hr = Math.floor(totalMin / 60).toString().padStart(2, '0'); - return `${hr}:${min}:${sec}.${msec}`; - } + _formatTimestamp(t) { + const totalSec = Math.floor(t / 1000); + const totalMin = Math.floor(totalSec / 60); + const msec = (t % 1000).toString().padStart(3, '0'); + const sec = (totalSec % 60).toString().padStart(2, '0'); + const min = (totalMin % 60).toString().padStart(2, '0'); + const hr = Math.floor(totalMin / 60).toString().padStart(2, '0'); + return `${hr}:${min}:${sec}.${msec}`; + } - _formatCategory(c) { - if (!c) { - c = ''; - } - if (c.length > CATEGORY_FIELD_WIDTH) { - c = c.slice(0, CATEGORY_FIELD_WIDTH - 1); - if (c.charAt(c.length - 1) === '.') { - c = c.slice(0, c.length - 1); - } - c += '~'; - } - c = c.padEnd(CATEGORY_FIELD_WIDTH, ' '); - return c; - } + _formatCategory(c) { + if (!c) { + c = ''; + } + if (c.length > CATEGORY_FIELD_WIDTH) { + c = c.slice(0, CATEGORY_FIELD_WIDTH - 1); + if (c.charAt(c.length - 1) === '.') { + c = c.slice(0, c.length - 1); + } + c += '~'; + } + c = c.padEnd(CATEGORY_FIELD_WIDTH, ' '); + return c; + } - // Finds a substring that looks like a valid JSON object - _findJsonObject(srcStr) { - const br = []; - let start = null; - let esc = false; - let str = false; - let i = 0; - for (; i < srcStr.length; ++i) { - const c = srcStr.charAt(i); - if (!str) { - if (c === '{') { - br.push(c); - if (start === null) { - start = i; // Beginning of a JSON object - } - } if (start !== null) { // Keep skipping characters until the first '{' is found - if (c === '[') { - br.push(c); - } else if (c === '}' || c === ']') { - const c1 = br.pop(); - if ((c === '}' && c1 !== '{') || (c === ']' && c1 !== '[')) { - return { valid: false }; // Invalid closing bracket - } - if (br.length === 0) { - break; // End of the JSON object - } - } else if (c === '"') { - str = true; // Beginning of a string - } - } - } else if (esc) { - esc = false; // Ignore the escaped character - } else if (c === '\\') { - esc = true; // Beginning of an escaped character - } else if (c === '"') { - str = false; // End of the string - } - } - if (i === srcStr.length) { - if (start === null) { - return { valid: false }; // The string doesn't contain a '{' character - } - return { - valid: true, - complete: false, // Found an incomplete JSON object - startIndex: start - }; - } - return { - valid: true, - complete: true, - startIndex: start, - endIndex: i + 1 - }; - } + // Finds a substring that looks like a valid JSON object + _findJsonObject(srcStr) { + const br = []; + let start = null; + let esc = false; + let str = false; + let i = 0; + for (; i < srcStr.length; ++i) { + const c = srcStr.charAt(i); + if (!str) { + if (c === '{') { + br.push(c); + if (start === null) { + start = i; // Beginning of a JSON object + } + } if (start !== null) { // Keep skipping characters until the first '{' is found + if (c === '[') { + br.push(c); + } else if (c === '}' || c === ']') { + const c1 = br.pop(); + if ((c === '}' && c1 !== '{') || (c === ']' && c1 !== '[')) { + return { valid: false }; // Invalid closing bracket + } + if (br.length === 0) { + break; // End of the JSON object + } + } else if (c === '"') { + str = true; // Beginning of a string + } + } + } else if (esc) { + esc = false; // Ignore the escaped character + } else if (c === '\\') { + esc = true; // Beginning of an escaped character + } else if (c === '"') { + str = false; // End of the string + } + } + if (i === srcStr.length) { + if (start === null) { + return { valid: false }; // The string doesn't contain a '{' character + } + return { + valid: true, + complete: false, // Found an incomplete JSON object + startIndex: start + }; + } + return { + valid: true, + complete: true, + startIndex: start, + endIndex: i + 1 + }; + } } function findStreamType(streamName) { - const name = streamName.toLowerCase(); - const stream = STREAM_TYPES.find(s => s.name.toLowerCase() === name); - if (!stream) { - throw new Error(`Unknown stream type: ${streamName}`); - } - return stream; + const name = streamName.toLowerCase(); + const stream = STREAM_TYPES.find(s => s.name.toLowerCase() === name); + if (!stream) { + throw new Error(`Unknown stream type: ${streamName}`); + } + return stream; } function parseLogLevel(levelStr) { - const str = levelStr.toLowerCase(); - const level = LOG_LEVELS.find(l => l.startsWith(str)); - if (!level) { - throw new Error(`Invalid log level: ${levelStr}`); - } - return level; + const str = levelStr.toLowerCase(); + const level = LOG_LEVELS.find(l => l.startsWith(str)); + if (!level) { + throw new Error(`Invalid log level: ${levelStr}`); + } + return level; } function handlerIdForStreamType(streamType) { - return `__cli_${streamType.name}`; + return `__cli_${streamType.name}`; } function isParticleSerialPort(port) { - if (!port.vendorId || !port.productId) { - return false; - } - const s = `${port.vendorId}:${port.productId}`.toLowerCase(); - return PARTICLE_USB_IDS.has(s); + if (!port.vendorId || !port.productId) { + return false; + } + const s = `${port.vendorId}:${port.productId}`.toLowerCase(); + return PARTICLE_USB_IDS.has(s); } module.exports = class LogCommand { @@ -210,160 +210,160 @@ module.exports = class LogCommand { } run(args) { - let streamType = null; - let defaultLevel = null; - let filters = null; - let baudRate = null; - let usbDevice = null; - let serialPort = null; - let deviceId = null; - let handlerId = null; - let handlerEnabled = false; + let streamType = null; + let defaultLevel = null; + let filters = null; + let baudRate = null; + let usbDevice = null; + let serialPort = null; + let deviceId = null; + let handlerId = null; + let handlerEnabled = false; return when.resolve().then(() => { - // Parse arguments - streamType = findStreamType(args.params.stream || DEFAULT_STREAM); - defaultLevel = args.level ? parseLogLevel(args.level) : DEFAULT_LOG_LEVEL; - filters = this._parseFilters(args); - baudRate = args.baud || DEFAULT_BAUD_RATE; - // Open the device - return openUsbDeviceById({ id: args.params.device, api: this._api, auth: this._auth }); - }) - .then(dev => { - // Enable logging - usbDevice = dev; - deviceId = usbDevice.id; - handlerId = handlerIdForStreamType(streamType); - const p = usbDevice.addLogHandler({ - id: handlerId, - format: args.raw ? 'default' : 'json', - stream: streamType.name, - level: defaultLevel, - filters: filters, - baudRate: baudRate - }) - .then(() => { - handlerEnabled = true; - return usbDevice.close(); - }); - return spin(p, 'Configuring the device...'); - }) - .then(() => { - // Get the serial port assigned to the device - if (args.params.serial_port) { - return args.params.serial_port; - } - return this._findSerialPort(streamType, deviceId); - }) - .then(portName => { - // Open serial port - return when.promise((resolve, reject) => { - console.log(`Opening serial port: ${portName}`); - const port = new SerialPort(portName, { baudRate }, err => { - if (err) { - return reject(err); - } - resolve(port); - }); - }); - }) - .then(port => { - serialPort = port; - // TODO: Try to reopen the serial port and re-register the log handler on USB/serial port errors - const p = when.promise((resolve, reject) => { - serialPort.on('error', err => reject(err)); - serialPort.on('close', err => err ? reject(err) : resolve()); - }); - // Close the serial port on exit - process.on('SIGINT', () => serialPort.close()); - process.on('SIGTERM', () => serialPort.close()); - // Start reading the logging output - console.log('Press Ctrl-C to exit.'); - let log = serialPort; - if (!args.raw) { - log = new PrettyFormatter(); - serialPort.on('data', d => log.update(d)); - } - log.on('data', d => process.stdout.write(d)); - return p; - }) - .finally(() => { - if (serialPort) { - // Close the serial port - return when.promise((resolve, reject) => { - serialPort.close(() => resolve()); // Ignore errors - }); - } - }) - .finally(() => { - if (usbDevice && handlerEnabled) { - // Unregister the log handler - return openUsbDevice(usbDevice) - .then(() => usbDevice.removeLogHandler({ id: handlerId })) - .catch(e => {}); // Ignore errors - } - }) - .finally(() => { - if (usbDevice) { - // Close the USB device - return usbDevice.close() - .catch(e => {}); // Ignore errors - } - }); + // Parse arguments + streamType = findStreamType(args.params.stream || DEFAULT_STREAM); + defaultLevel = args.level ? parseLogLevel(args.level) : DEFAULT_LOG_LEVEL; + filters = this._parseFilters(args); + baudRate = args.baud || DEFAULT_BAUD_RATE; + // Open the device + return openUsbDeviceById({ id: args.params.device, api: this._api, auth: this._auth }); + }) + .then(dev => { + // Enable logging + usbDevice = dev; + deviceId = usbDevice.id; + handlerId = handlerIdForStreamType(streamType); + const p = usbDevice.addLogHandler({ + id: handlerId, + format: args.raw ? 'default' : 'json', + stream: streamType.name, + level: defaultLevel, + filters: filters, + baudRate: baudRate + }) + .then(() => { + handlerEnabled = true; + return usbDevice.close(); + }); + return spin(p, 'Configuring the device...'); + }) + .then(() => { + // Get the serial port assigned to the device + if (args.params.serial_port) { + return args.params.serial_port; + } + return this._findSerialPort(streamType, deviceId); + }) + .then(portName => { + // Open serial port + return when.promise((resolve, reject) => { + console.log(`Opening serial port: ${portName}`); + const port = new SerialPort(portName, { baudRate }, err => { + if (err) { + return reject(err); + } + resolve(port); + }); + }); + }) + .then(port => { + serialPort = port; + // TODO: Try to reopen the serial port and re-register the log handler on USB/serial port errors + const p = when.promise((resolve, reject) => { + serialPort.on('error', err => reject(err)); + serialPort.on('close', err => err ? reject(err) : resolve()); + }); + // Close the serial port on exit + process.on('SIGINT', () => serialPort.close()); + process.on('SIGTERM', () => serialPort.close()); + // Start reading the logging output + console.log('Press Ctrl-C to exit.'); + let log = serialPort; + if (!args.raw) { + log = new PrettyFormatter(); + serialPort.on('data', d => log.update(d)); + } + log.on('data', d => process.stdout.write(d)); + return p; + }) + .finally(() => { + if (serialPort) { + // Close the serial port + return when.promise((resolve, reject) => { + serialPort.close(() => resolve()); // Ignore errors + }); + } + }) + .finally(() => { + if (usbDevice && handlerEnabled) { + // Unregister the log handler + return openUsbDevice(usbDevice) + .then(() => usbDevice.removeLogHandler({ id: handlerId })) + .catch(e => {}); // Ignore errors + } + }) + .finally(() => { + if (usbDevice) { + // Close the USB device + return usbDevice.close() + .catch(e => {}); // Ignore errors + } + }); } - _findSerialPort(streamType, deviceId) { - return when.resolve().then(() => { - return SerialPort.list(); - }) - .then(ports => { - if (streamType.isUsbSerial) { - // Get all USB serial ports with a matching serial number - // TODO: Check the interface index to identify ports assigned to Serial and USBSerial1 - ports = ports.filter(p => p.serialNumber && p.serialNumber.toLowerCase() === deviceId); - } else { - // Filter out all Particle and non-USB serial ports - // FIXME: This will likely filter out built-in UARTs on Raspberry Pi - ports = ports.filter(p => p.vendorId && p.productId && !isParticleSerialPort(p)); - } - if (ports.length == 0) { - throw new Error('Serial port is not found'); - } - if (ports.length == 1) { - return ports[0].comName; - } - // Let the user identify the serial port - return prompt({ - name: 'port', - type: 'list', - message: `Please specify the serial port assigned to ${streamType.name}`, - choices: ports.map(p => p.comName) - }) - .then(r => r.port); - }); - } + _findSerialPort(streamType, deviceId) { + return when.resolve().then(() => { + return SerialPort.list(); + }) + .then(ports => { + if (streamType.isUsbSerial) { + // Get all USB serial ports with a matching serial number + // TODO: Check the interface index to identify ports assigned to Serial and USBSerial1 + ports = ports.filter(p => p.serialNumber && p.serialNumber.toLowerCase() === deviceId); + } else { + // Filter out all Particle and non-USB serial ports + // FIXME: This will likely filter out built-in UARTs on Raspberry Pi + ports = ports.filter(p => p.vendorId && p.productId && !isParticleSerialPort(p)); + } + if (ports.length === 0) { + throw new Error('Serial port is not found'); + } + if (ports.length === 1) { + return ports[0].comName; + } + // Let the user identify the serial port + return prompt({ + name: 'port', + type: 'list', + message: `Please specify the serial port assigned to ${streamType.name}`, + choices: ports.map(p => p.comName) + }) + .then(r => r.port); + }); + } - _parseFilters(args) { - args = args.filter; - if (!args) { - return null; - } - if (typeof(args) === 'string') { - args = [ args ]; - } - return args.map(arg => { - const s = arg.split(':'); // category:level - if (s.length > 2 || !s[0] || (s.length == 2 && !s[1])) { - throw new Error('Invalid filter string'); - } - const f = { - category: s[0] - }; - if (s.length == 2) { - f.level = parseLogLevel(s[1]); - } else { - f.level = DEFAULT_LOG_LEVEL; - } - return f; - }); - } + _parseFilters(args) { + args = args.filter; + if (!args) { + return null; + } + if (typeof args === 'string') { + args = [args]; + } + return args.map(arg => { + const s = arg.split(':'); // category:level + if (s.length > 2 || !s[0] || (s.length === 2 && !s[1])) { + throw new Error('Invalid filter string'); + } + const f = { + category: s[0] + }; + if (s.length === 2) { + f.level = parseLogLevel(s[1]); + } else { + f.level = DEFAULT_LOG_LEVEL; + } + return f; + }); + } }; From 8118874f80fd92ef822b3d3abb2aec67d490c166 Mon Sep 17 00:00:00 2001 From: Sergey Polyakov Date: Sat, 13 Apr 2019 00:26:01 +0200 Subject: [PATCH 4/9] Add examples for the `particle log` command --- src/cli/log.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/cli/log.js b/src/cli/log.js index 56572def5..c400076b4 100644 --- a/src/cli/log.js +++ b/src/cli/log.js @@ -32,6 +32,12 @@ module.exports = ({ commandProcessor, root }) => { boolean: true } }, + examples: { + '$0 $command my_device': 'Start reading and displaying log messages from the device "my_device"', + '$0 $command my_device -l none -f app:all': 'Show only application messages', + '$0 $command my_device -l warn -f app:all': 'Show all application messages as well as system warnings and errors', + '$0 $command my_device Serial1': 'Use the hardware serial interface for logging' + }, handler: (args) => { return logCommand().run(args); } From 5bfee95d41134cca525e85342fac1334de5c743f Mon Sep 17 00:00:00 2001 From: Sergey Polyakov Date: Sat, 13 Apr 2019 00:42:15 +0200 Subject: [PATCH 5/9] Minor fixes --- src/cmd/log.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/cmd/log.js b/src/cmd/log.js index 9bc9c2137..c79e8498f 100644 --- a/src/cmd/log.js +++ b/src/cmd/log.js @@ -257,7 +257,7 @@ module.exports = class LogCommand { .then(portName => { // Open serial port return when.promise((resolve, reject) => { - console.log(`Opening serial port: ${portName}`); + console.error(`Opening serial port: ${portName}`); const port = new SerialPort(portName, { baudRate }, err => { if (err) { return reject(err); @@ -273,11 +273,23 @@ module.exports = class LogCommand { serialPort.on('error', err => reject(err)); serialPort.on('close', err => err ? reject(err) : resolve()); }); - // Close the serial port on exit - process.on('SIGINT', () => serialPort.close()); - process.on('SIGTERM', () => serialPort.close()); + // Close the serial port on Ctrl-C + let closing = false; + const signalHandler = () => { + if (!closing) { + closing = true; + console.error('\nClosing the device...'); + serialPort.close(); + } else { + // Terminate the process on second Ctrl-C + console.error('\nAborted.'); + process.exit(1); + } + }; + process.on('SIGINT', signalHandler); + process.on('SIGTERM', signalHandler); // Start reading the logging output - console.log('Press Ctrl-C to exit.'); + console.error('Press Ctrl-C to exit.'); let log = serialPort; if (!args.raw) { log = new PrettyFormatter(); @@ -308,6 +320,9 @@ module.exports = class LogCommand { return usbDevice.close() .catch(e => {}); // Ignore errors } + }) + .then(() => { + console.error('Done.'); }); } From 475070e6b3c07cb23470ef91ec8c0727fa3da8f3 Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 25 Apr 2019 08:54:06 -0700 Subject: [PATCH 6/9] lintings --- src/cmd/log.js | 214 +++++++++++++++++++++++++------------------------ 1 file changed, 108 insertions(+), 106 deletions(-) diff --git a/src/cmd/log.js b/src/cmd/log.js index c79e8498f..283d3685c 100644 --- a/src/cmd/log.js +++ b/src/cmd/log.js @@ -123,6 +123,7 @@ class PrettyFormatter extends EventEmitter { let esc = false; let str = false; let i = 0; + /* eslint-disable max-depth */ for (; i < srcStr.length; ++i) { const c = srcStr.charAt(i); if (!str) { @@ -154,6 +155,7 @@ class PrettyFormatter extends EventEmitter { str = false; // End of the string } } + /* eslint-enable max-depth */ if (i === srcStr.length) { if (start === null) { return { valid: false }; // The string doesn't contain a '{' character @@ -228,133 +230,133 @@ module.exports = class LogCommand { // Open the device return openUsbDeviceById({ id: args.params.device, api: this._api, auth: this._auth }); }) - .then(dev => { + .then(dev => { // Enable logging - usbDevice = dev; - deviceId = usbDevice.id; - handlerId = handlerIdForStreamType(streamType); - const p = usbDevice.addLogHandler({ - id: handlerId, - format: args.raw ? 'default' : 'json', - stream: streamType.name, - level: defaultLevel, - filters: filters, - baudRate: baudRate + usbDevice = dev; + deviceId = usbDevice.id; + handlerId = handlerIdForStreamType(streamType); + const p = usbDevice.addLogHandler({ + id: handlerId, + format: args.raw ? 'default' : 'json', + stream: streamType.name, + level: defaultLevel, + filters: filters, + baudRate: baudRate + }) + .then(() => { + handlerEnabled = true; + return usbDevice.close(); + }); + return spin(p, 'Configuring the device...'); }) .then(() => { - handlerEnabled = true; - return usbDevice.close(); - }); - return spin(p, 'Configuring the device...'); - }) - .then(() => { // Get the serial port assigned to the device - if (args.params.serial_port) { - return args.params.serial_port; - } - return this._findSerialPort(streamType, deviceId); - }) - .then(portName => { + if (args.params.serial_port) { + return args.params.serial_port; + } + return this._findSerialPort(streamType, deviceId); + }) + .then(portName => { // Open serial port - return when.promise((resolve, reject) => { - console.error(`Opening serial port: ${portName}`); - const port = new SerialPort(portName, { baudRate }, err => { - if (err) { - return reject(err); - } - resolve(port); + return when.promise((resolve, reject) => { + console.error(`Opening serial port: ${portName}`); + const port = new SerialPort(portName, { baudRate }, err => { + if (err) { + return reject(err); + } + resolve(port); + }); }); - }); - }) - .then(port => { - serialPort = port; - // TODO: Try to reopen the serial port and re-register the log handler on USB/serial port errors - const p = when.promise((resolve, reject) => { - serialPort.on('error', err => reject(err)); - serialPort.on('close', err => err ? reject(err) : resolve()); - }); - // Close the serial port on Ctrl-C - let closing = false; - const signalHandler = () => { - if (!closing) { - closing = true; - console.error('\nClosing the device...'); - serialPort.close(); - } else { + }) + .then(port => { + serialPort = port; + // TODO: Try to reopen the serial port and re-register the log handler on USB/serial port errors + const p = when.promise((resolve, reject) => { + serialPort.on('error', err => reject(err)); + serialPort.on('close', err => err ? reject(err) : resolve()); + }); + // Close the serial port on Ctrl-C + let closing = false; + const signalHandler = () => { + if (!closing) { + closing = true; + console.error('\nClosing the device...'); + serialPort.close(); + } else { // Terminate the process on second Ctrl-C - console.error('\nAborted.'); - process.exit(1); + console.error('\nAborted.'); + process.exit(1); + } + }; + process.on('SIGINT', signalHandler); + process.on('SIGTERM', signalHandler); + // Start reading the logging output + console.error('Press Ctrl-C to exit.'); + let log = serialPort; + if (!args.raw) { + log = new PrettyFormatter(); + serialPort.on('data', d => log.update(d)); } - }; - process.on('SIGINT', signalHandler); - process.on('SIGTERM', signalHandler); - // Start reading the logging output - console.error('Press Ctrl-C to exit.'); - let log = serialPort; - if (!args.raw) { - log = new PrettyFormatter(); - serialPort.on('data', d => log.update(d)); - } - log.on('data', d => process.stdout.write(d)); - return p; - }) - .finally(() => { - if (serialPort) { + log.on('data', d => process.stdout.write(d)); + return p; + }) + .finally(() => { + if (serialPort) { // Close the serial port - return when.promise((resolve, reject) => { - serialPort.close(() => resolve()); // Ignore errors - }); - } - }) - .finally(() => { - if (usbDevice && handlerEnabled) { + return when.promise((resolve) => { + serialPort.close(() => resolve()); // Ignore errors + }); + } + }) + .finally(() => { + if (usbDevice && handlerEnabled) { // Unregister the log handler - return openUsbDevice(usbDevice) - .then(() => usbDevice.removeLogHandler({ id: handlerId })) - .catch(e => {}); // Ignore errors - } - }) - .finally(() => { - if (usbDevice) { + return openUsbDevice(usbDevice) + .then(() => usbDevice.removeLogHandler({ id: handlerId })) + .catch(() => {}); // Ignore errors + } + }) + .finally(() => { + if (usbDevice) { // Close the USB device - return usbDevice.close() - .catch(e => {}); // Ignore errors - } - }) - .then(() => { - console.error('Done.'); - }); + return usbDevice.close() + .catch(() => {}); // Ignore errors + } + }) + .then(() => { + console.error('Done.'); + }); } _findSerialPort(streamType, deviceId) { return when.resolve().then(() => { return SerialPort.list(); }) - .then(ports => { - if (streamType.isUsbSerial) { + .then(ports => { + if (streamType.isUsbSerial) { // Get all USB serial ports with a matching serial number // TODO: Check the interface index to identify ports assigned to Serial and USBSerial1 - ports = ports.filter(p => p.serialNumber && p.serialNumber.toLowerCase() === deviceId); - } else { + ports = ports.filter(p => p.serialNumber && p.serialNumber.toLowerCase() === deviceId); + } else { // Filter out all Particle and non-USB serial ports // FIXME: This will likely filter out built-in UARTs on Raspberry Pi - ports = ports.filter(p => p.vendorId && p.productId && !isParticleSerialPort(p)); - } - if (ports.length === 0) { - throw new Error('Serial port is not found'); - } - if (ports.length === 1) { - return ports[0].comName; - } - // Let the user identify the serial port - return prompt({ - name: 'port', - type: 'list', - message: `Please specify the serial port assigned to ${streamType.name}`, - choices: ports.map(p => p.comName) - }) - .then(r => r.port); - }); + ports = ports.filter(p => p.vendorId && p.productId && !isParticleSerialPort(p)); + } + if (ports.length === 0) { + throw new Error('Serial port is not found'); + } + if (ports.length === 1) { + return ports[0].comName; + } + // Let the user identify the serial port + return prompt({ + name: 'port', + type: 'list', + message: `Please specify the serial port assigned to ${streamType.name}`, + choices: ports.map(p => p.comName) + }) + .then(r => r.port); + }); } _parseFilters(args) { From 1ca02f7548ba4a55d6aedb53de7fe6fb7b32b67e Mon Sep 17 00:00:00 2001 From: Sergey Polyakov Date: Mon, 20 May 2019 23:58:38 +0200 Subject: [PATCH 7/9] Minor fixes --- src/cmd/log.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/cmd/log.js b/src/cmd/log.js index 283d3685c..472297d3c 100644 --- a/src/cmd/log.js +++ b/src/cmd/log.js @@ -1,4 +1,4 @@ -const ParticleApi = require('./api').default; +const ParticleApi = require('./api'); const { openUsbDevice, openUsbDeviceById } = require('./usb-util'); const { prompt, spin } = require('../app/ui'); const deviceSpecs = require('../lib/deviceSpecs'); @@ -231,7 +231,7 @@ module.exports = class LogCommand { return openUsbDeviceById({ id: args.params.device, api: this._api, auth: this._auth }); }) .then(dev => { - // Enable logging + // Enable logging usbDevice = dev; deviceId = usbDevice.id; handlerId = handlerIdForStreamType(streamType); @@ -250,14 +250,14 @@ module.exports = class LogCommand { return spin(p, 'Configuring the device...'); }) .then(() => { - // Get the serial port assigned to the device + // Get the serial port assigned to the device if (args.params.serial_port) { return args.params.serial_port; } return this._findSerialPort(streamType, deviceId); }) .then(portName => { - // Open serial port + // Open serial port return when.promise((resolve, reject) => { console.error(`Opening serial port: ${portName}`); const port = new SerialPort(portName, { baudRate }, err => { @@ -283,7 +283,7 @@ module.exports = class LogCommand { console.error('\nClosing the device...'); serialPort.close(); } else { - // Terminate the process on second Ctrl-C + // Terminate the process on second Ctrl-C console.error('\nAborted.'); process.exit(1); } @@ -302,7 +302,7 @@ module.exports = class LogCommand { }) .finally(() => { if (serialPort) { - // Close the serial port + // Close the serial port return when.promise((resolve) => { serialPort.close(() => resolve()); // Ignore errors }); @@ -310,7 +310,7 @@ module.exports = class LogCommand { }) .finally(() => { if (usbDevice && handlerEnabled) { - // Unregister the log handler + // Unregister the log handler return openUsbDevice(usbDevice) .then(() => usbDevice.removeLogHandler({ id: handlerId })) .catch(() => {}); // Ignore errors @@ -318,7 +318,7 @@ module.exports = class LogCommand { }) .finally(() => { if (usbDevice) { - // Close the USB device + // Close the USB device return usbDevice.close() .catch(() => {}); // Ignore errors } @@ -334,12 +334,12 @@ module.exports = class LogCommand { }) .then(ports => { if (streamType.isUsbSerial) { - // Get all USB serial ports with a matching serial number - // TODO: Check the interface index to identify ports assigned to Serial and USBSerial1 + // Get all USB serial ports with a matching serial number + // TODO: Check the interface index to identify ports assigned to Serial and USBSerial1 ports = ports.filter(p => p.serialNumber && p.serialNumber.toLowerCase() === deviceId); } else { - // Filter out all Particle and non-USB serial ports - // FIXME: This will likely filter out built-in UARTs on Raspberry Pi + // Filter out all Particle and non-USB serial ports + // FIXME: This will likely filter out built-in UARTs on Raspberry Pi ports = ports.filter(p => p.vendorId && p.productId && !isParticleSerialPort(p)); } if (ports.length === 0) { From 05ae39535ac43adbc93116cd58db630a3c7f5379 Mon Sep 17 00:00:00 2001 From: Sergey Polyakov Date: Tue, 21 May 2019 01:24:17 +0200 Subject: [PATCH 8/9] Show nicer messages on some typical errors --- src/cli/log.js | 3 ++- src/cmd/log.js | 23 ++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/cli/log.js b/src/cli/log.js index c400076b4..fff582bf6 100644 --- a/src/cli/log.js +++ b/src/cli/log.js @@ -36,7 +36,8 @@ module.exports = ({ commandProcessor, root }) => { '$0 $command my_device': 'Start reading and displaying log messages from the device "my_device"', '$0 $command my_device -l none -f app:all': 'Show only application messages', '$0 $command my_device -l warn -f app:all': 'Show all application messages as well as system warnings and errors', - '$0 $command my_device Serial1': 'Use the hardware serial interface for logging' + '$0 $command my_device Serial1': 'Use the hardware serial interface for logging', + '$0 $command my_device Serial1 /dev/ttyUSB0': 'Use the specified serial port instead of auto-detecting' }, handler: (args) => { return logCommand().run(args); diff --git a/src/cmd/log.js b/src/cmd/log.js index 472297d3c..9c6b12907 100644 --- a/src/cmd/log.js +++ b/src/cmd/log.js @@ -36,6 +36,9 @@ const DEFAULT_LOG_LEVEL = 'all'; const CATEGORY_FIELD_WIDTH = 14; +// TODO: Define this result code in particle-usb +const REQUEST_RESULT_DISABLED = -131; + class PrettyFormatter extends EventEmitter { constructor() { super(); @@ -205,6 +208,13 @@ function isParticleSerialPort(port) { return PARTICLE_USB_IDS.has(s); } +function serialPortError(error) { + if (error.disconnected) { + return new Error('Serial connection closed'); + } + return error; +} + module.exports = class LogCommand { constructor(settings) { this._auth = settings.access_token; @@ -247,7 +257,14 @@ module.exports = class LogCommand { handlerEnabled = true; return usbDevice.close(); }); - return spin(p, 'Configuring the device...'); + return spin(p, 'Configuring the device...') + .catch(e => { + if (e.result === REQUEST_RESULT_DISABLED) { + throw new Error(`Unable to configure logging. Make sure the ${chalk.bold('FEATURE_LOG_CONFIG')} feature is \ +enabled in the application code`); + } + throw e; + }); }) .then(() => { // Get the serial port assigned to the device @@ -272,8 +289,8 @@ module.exports = class LogCommand { serialPort = port; // TODO: Try to reopen the serial port and re-register the log handler on USB/serial port errors const p = when.promise((resolve, reject) => { - serialPort.on('error', err => reject(err)); - serialPort.on('close', err => err ? reject(err) : resolve()); + serialPort.on('error', err => reject(serialPortError(err))); + serialPort.on('close', err => err ? reject(serialPortError(err)) : resolve()); }); // Close the serial port on Ctrl-C let closing = false; From 15164da8394bdda02086e01a7a4eec20f05dab78 Mon Sep 17 00:00:00 2001 From: Sergey Polyakov Date: Tue, 21 May 2019 03:11:46 +0200 Subject: [PATCH 9/9] Disable USBSerial1 interface for now --- src/cmd/log.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/cmd/log.js b/src/cmd/log.js index 9c6b12907..38e3fde63 100644 --- a/src/cmd/log.js +++ b/src/cmd/log.js @@ -19,10 +19,12 @@ const STREAM_TYPES = [ name: 'Serial', isUsbSerial: true }, - { - name: 'USBSerial1', - isUsbSerial: true - }, + // TODO: It looks like calling USBSerial1.begin() in the Device OS causes the device to be detached + // from the system. Disabling this serial interface for now + // { + // name: 'USBSerial1', + // isUsbSerial: true + // }, { name: 'Serial1' }