Update: WebSocket模式添加对Serial和Arduino编译上传的支持

This commit is contained in:
王立帮
2024-12-01 14:42:25 +08:00
parent d8ceafadbf
commit d245eaf458
21 changed files with 4717 additions and 41 deletions

View File

@@ -1 +1,20 @@
export const DEBUG = false;
import os from 'node:os';
export const DEBUG = true;
export const ARDUINO = {
path: {
folder: 'D:/gitee/arduino-cli-win32/arduino-cli',
cli: 'D:/gitee/arduino-cli-win32/arduino-cli/arduino-cli.exe',
libraries: ['D:/gitee/arduino-cli-win32/arduino-cli/libraries'],
cache: 'D:/gitee/arduino-cli-win32/arduino-cli/cache',
config: 'D:/gitee/arduino-cli-win32/arduino-cli/arduino-cli.json',
build: 'D:/gitee/mixly3-server/temp/web-socket',
code: ''
},
key: '',
port: '',
code: ''
};
export const CURRENT_PLANTFORM = os.platform();
export const WEB_SOCKT_TEMP_PATH = '';
export const WEB_COMPILER_TEMP_PATH = '';

View File

@@ -2,7 +2,7 @@ import Events from './events';
export default class EventsBase {
#events_ = new Events();
#events_ = new Events([]);
constructor() {}
bind(type, func) {
@@ -10,11 +10,11 @@ export default class EventsBase {
}
unbind(id) {
this.#events_.unbind(id);
return this.#events_.unbind(id);
}
addEventsType(eventsType) {
this.#events_.addType(eventsType);
return this.#events_.addType(eventsType);
}
runEvent(eventsType, ...args) {
@@ -22,11 +22,11 @@ export default class EventsBase {
}
offEvent(eventsType) {
this.#events_.off(eventsType);
return this.#events_.off(eventsType);
}
resetEvent() {
this.#events_.reset();
return this.#events_.reset();
}
disposeEvent() {

View File

@@ -14,7 +14,7 @@ export default class Events {
}
addType(eventsType) {
this.#eventsType_ = _.uniq(_.concat([this.#eventsType_, eventsType]));
this.#eventsType_ = _.uniq(_.concat(this.#eventsType_, eventsType));
}
exist(type) {
@@ -40,7 +40,7 @@ export default class Events {
}
unbind(id) {
for (let [_, value] of this.#events_.getAllItems()) {
for (let [, value] of this.#events_.getAllItems()) {
let typeEvent = value;
if (!typeEvent.getItem(id)) {
continue;
@@ -66,7 +66,7 @@ export default class Events {
if (!eventsFunc) {
return outputs;
}
for (let [_, func] of eventsFunc.getAllItems()) {
for (let [, func] of eventsFunc.getAllItems()) {
outputs.push(func(...args));
}
return outputs;

View File

@@ -1,6 +1,9 @@
import Debug from './debug';
export default class Registry {
#registry_ = new Map();
constructor() {
this.reset();
}

View File

@@ -1,11 +1,12 @@
import os from 'node:os';
import { ChildProcess } from 'node:child_process';
import { exec } from 'node:child_process';
import {
SerialPort,
ReadlineParser,
ByteLengthParser
} from 'serialport';
import _ from 'lodash';
import EventsBase from './events-base';
import { CURRENT_PLANTFORM } from './config';
export default class Serial extends EventsBase {
@@ -18,17 +19,17 @@ export default class Serial extends EventsBase {
this.getPorts = async function () {
return new Promise((resolve, reject) => {
if (os.platform() === 'linux') {
ChildProcess.exec('ls /dev/ttyACM* /dev/tty*USB*', (_, stdout, stderr) => {
let portsName = MArray.unique(stdout.split('\n'));
if (CURRENT_PLANTFORM === 'linux') {
exec('ls /dev/ttyACM* /dev/tty*USB*', (error, stdout) => {
let portsName = _.uniq(stdout.split('\n'));
let newPorts = [];
for (let i = 0; i < portsName.length; i++) {
if (!portsName[i]) {
continue;
}
newPorts.push({
vendorId: 'None',
productId: 'None',
vendorId: null,
productId: null,
name: portsName[i]
});
}
@@ -56,19 +57,25 @@ export default class Serial extends EventsBase {
#parserBytes_ = null;
#parserLine_ = null;
#port_ = null;
#isOpened_ = false;
constructor(port) {
super();
this.#port_ = port;
this.addEventsType(['buffer', 'String', 'error', 'open', 'close']);
this.addEventsType(['buffer', 'string', 'error', 'open', 'close']);
}
#addEventsListener_() {
this.#parserBytes_.on('data', (buffer) => {
this.runEvent('buffer', buffer);
const arr = [];
for (let i = 0; i < buffer.length; i++) {
arr.push(buffer[i]);
}
this.runEvent('buffer', arr);
});
this.#parserLine_.on('data', (str) => {
this.runEvent('String', str);
this.runEvent('string', str);
});
this.#serialport_.on('error', (error) => {
@@ -76,31 +83,42 @@ export default class Serial extends EventsBase {
});
this.#serialport_.on('open', () => {
this.#isOpened_ = true;
this.runEvent('open');
});
this.#serialport_.on('close', () => {
this.#isOpened_ = false;
this.runEvent('close');
});
}
isOpened() {
return this.#isOpened_;
}
getPortName() {
return this.#port_;
}
async open(baud) {
return new Promise((resolve, reject) => {
if (this.isOpened()) {
resolve();
return;
}
this.#serialport_ = new SerialPort({
path: this.getPortName(),
baudRate: baud, // 波特率
dataBits: 8, // 数据位
parity: 'none', // 奇偶校验
stopBits: 1, // 停止位
baudRate: baud,
dataBits: 8,
parity: 'none',
stopBits: 1,
flowControl: false,
autoOpen: false // 不自动打开
autoOpen: false
}, false);
this.#parserBytes_ = this.#serialport_.pipe(new ByteLengthParser({ length: 1 }));
this.#parserLine_ = this.#serialport_.pipe(new ReadlineParser());
this.#addEventsListener_();
this.#serialport_.open((error) => {
if (error) {
reject(error);
@@ -108,12 +126,15 @@ export default class Serial extends EventsBase {
resolve();
}
});
this.#addEventsListener_();
});
}
async close() {
return new Promise((resolve, reject) => {
if (!this.isOpened()) {
resolve();
return;
}
this.#serialport_.close((error) => {
if (error) {
reject(error);
@@ -126,18 +147,26 @@ export default class Serial extends EventsBase {
async setBaudRate(baud) {
return new Promise((resolve, reject) => {
if (!this.isOpened()) {
resolve();
return;
}
this.#serialport_.update({ baudRate: baud }, (error) => {
if (error) {
reject(error);
return;
} else {
resolve();
}
resolve();
});
});
}
async send(data) {
return new Promise((resolve, reject) => {
if (!this.isOpened()) {
resolve();
return;
}
this.#serialport_.write(data, (error) => {
if (error) {
reject(error);
@@ -150,18 +179,22 @@ export default class Serial extends EventsBase {
async setDTRAndRTS(dtr, rts) {
return new Promise((resolve, reject) => {
if (!this.isOpened()) {
resolve();
return;
}
this.#serialport_.set({ dtr, rts }, (error) => {
if (error) {
reject(error);
} else {
super.setDTRAndRTS(dtr, rts);
resolve();
}
});
});
}
dispose() {
async dispose() {
await this.close();
this.disposeEvent();
this.#serialport_ = null;
this.#parserBytes_ = null;

View File

@@ -0,0 +1,48 @@
import _ from 'lodash';
import Shell from './shell';
import { ARDUINO } from './config';
export default class ShellArduino extends Shell {
constructor() {
super();
}
async compile(config) {
let arduino = _.merge({}, ARDUINO);
arduino = _.merge(arduino, config);
const command = [
`"${arduino.path.cli}"`,
'compile',
'-b', arduino.key,
'--config-file', `"${arduino.path.config}"`,
'--build-cache-path', `"${arduino.path.cache}"`,
'--verbose',
'--libraries', `"${arduino.path.libraries.join('","')}"`,
'--build-path', `"${arduino.path.build}"`,
`"${arduino.path.code}"`,
'--no-color'
].join(' ');
return this.exec(command);
}
async upload(config) {
let arduino = _.merge({}, ARDUINO);
arduino = _.merge(arduino, config);
const command = [
`"${arduino.path.cli}"`,
'compile',
'--upload',
'-p', arduino.port,
'-b', arduino.key,
'--config-file', `"${arduino.path.config}"`,
'--build-cache-path', `"${arduino.path.cache}"`,
'--verbose',
'--libraries', `"${arduino.path.libraries.join('","')}"`,
'--build-path', `"${arduino.path.build}"`,
`"${arduino.path.code}"`,
'--no-color'
].join(' ');
return this.exec(command);
}
}

View File

116
src/common/shell.js Normal file
View File

@@ -0,0 +1,116 @@
import { execFile, exec } from 'node:child_process';
import * as iconv_lite from 'iconv-lite';
// import duration from 'dayjs/plugin/duration';
import Debug from './debug';
import EventsBase from './events-base';
import { CURRENT_PLANTFORM } from './config';
export default class Shell extends EventsBase {
static {
this.ENCODING = CURRENT_PLANTFORM == 'win32' ? 'cp936' : 'utf-8';
}
#shell_ = null;
#killed_ = false;
#defaultOptions_ = {
maxBuffer: 4096 * 1000000,
encoding: 'binary',
};
constructor() {
super();
this.addEventsType(['data', 'error', 'close']);
}
#decode_(str) {
try {
str = decodeURIComponent(str.replace(/(_E[0-9A-F]{1}_[0-9A-F]{2}_[0-9A-F]{2})+/gm, '%$1'));
str = decodeURIComponent(str.replace(/\\(u[0-9a-fA-F]{4})/gm, '%$1'));
} catch (error) {
Debug.error(error);
}
return str;
}
#addEventsListener_() {
const { stdout, stderr } = this.#shell_;
stdout.on('data', (data) => {
if (data.length > 1000) {
return;
}
data = iconv_lite.decode(Buffer.from(data, 'binary'), 'utf-8');
this.runEvent('data', data);
});
stderr.on('data', (data) => {
let lines = data.split('\n');
for (let i in lines) {
let encoding = 'utf-8';
if (lines[i].indexOf('can\'t open device') !== -1) {
encoding = Shell.ENCODING;
}
lines[i] = iconv_lite.decode(Buffer.from(lines[i], 'binary'), encoding);
}
data = lines.join('\n');
data = this.#decode_(data);
this.runEvent('error', data);
});
}
async #waitUntilClosed_() {
return new Promise((resolve) => {
const startTime = Number(new Date());
this.#shell_.on('close', (code) => {
this.#killed_ = true;
if (code === null) {
code = 1;
}
const endTime = Number(new Date());
const time = endTime - startTime;
const info = { code, time };
this.runEvent('close', code, time);
resolve(info);
});
});
}
async exec(command, options = {}) {
this.#shell_ = exec(command, { ...this.#defaultOptions_, ...options });
this.#addEventsListener_();
const result = await this.#waitUntilClosed_();
return result;
}
async execFile(file, args, options = {}) {
this.#shell_ = execFile(file, args, { ...this.#defaultOptions_, ...options });
this.#addEventsListener_();
const result = await this.#waitUntilClosed_();
return result;
}
async kill() {
new Promise((resolve, reject) => {
if (this.#killed_) {
return;
}
this.#shell_.stdin.end();
this.#shell_.stdout.end();
if (CURRENT_PLANTFORM === 'win32') {
exec(`taskkill /pid ${this.#shell_.pid} /f /t`, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
} else {
this.#shell_.kill('SIGTERM')
resolve();
}
})
}
getShell() {
return this.#shell_;
}
}

View File

@@ -0,0 +1,26 @@
import express from 'express';
import path from 'node:path';
import * as url from 'node:url';
import { readFileSync } from 'node:fs';
import { createServer } from 'https';
import Socket from './web-socket/socket';
const __dirname = path.dirname(url.fileURLToPath(new URL(import.meta.url)));
const app = express();
const httpsServer = createServer({
key: readFileSync(path.resolve(__dirname, '../certs/server.key')),
cert: readFileSync(path.resolve(__dirname, '../certs/server.crt'))
}, app);
new Socket(httpsServer, {
path: '/mixly-socket/',
cors: {
origin: '*',
methods: ['GET', 'POST'],
transports: ['websocket', 'polling', 'flashsocket'],
credentials: true
}
});
httpsServer.listen(4000);

View File

@@ -1,23 +1,174 @@
import { Server } from 'socket.io';
import to from 'await-to-js';
import { usb } from 'usb';
import path from 'node:path';
import fsExtra from 'fs-extra';
import fsPlus from 'fs-plus';
import Serial from '../common/serial';
import Debug from '../common/debug';
import Registry from '../common/registry';
import ShellArduino from '../common/shell-arduino';
import { ARDUINO } from '../common/config';
export default class Socket {
#io_ = null;
#serialRegistry_ = new Registry();
#shellArduino_ = new ShellArduino();
constructor(httpsServer, options) {
this.#io_ = new Server(httpsServer, options);
this.#io_.on('connection', (socket) => {
this.#addEventsListener_(socket);
});
usb.on('attach', (device) => {
this.#io_.emit('serial.attachEvent', device);
});
usb.on('detach', (device) => {
this.#io_.emit('serial.detachEvent', device);
});
}
#addEventsListenerForArduino_(socket) {
this.#shellArduino_.bind('data', (data) => {
socket.emit('arduino.dataEvent', data);
});
this.#shellArduino_.bind('error', (data) => {
socket.emit('arduino.errorEvent', data);
});
this.#shellArduino_.bind('close', (code, time) => {
socket.emit('arduino.closeEvent', code, time);
});
socket.on('arduino.compile', async (config, callback) => {
let { build } = ARDUINO.path;
let user = path.resolve(build, socket.id);
config.path = config?.path ?? {};
config.path.build = path.resolve(user, 'build');
config.path.code = path.resolve(user, 'testArduino/testArduino.ino');
let [error1,] = await to(fsExtra.ensureDir(config.path.build));
error1 && Debug.error(error1);
let [error2,] = await to(fsExtra.outputFile(config.path.code, config.code));
error2 && Debug.error(error2);
const [error, result] = await to(this.#shellArduino_.compile(config));
error && Debug.error(error);
callback([error, result]);
});
socket.on('arduino.upload', async (config, callback) => {
let { build } = ARDUINO.path;
let user = path.resolve(build, socket.id);
config.path = config?.path ?? {};
config.path.build = path.resolve(user, 'build');
config.path.code = path.resolve(user, 'testArduino/testArduino.ino');
let [error1,] = await to(fsExtra.ensureDir(config.path.build));
error1 && Debug.error(error1);
let [error2,] = await to(fsExtra.outputFile(config.path.code, config.code));
error2 && Debug.error(error2);
const [error, result] = await to(this.#shellArduino_.upload(config));
error && Debug.error(error);
callback([error, result]);
});
socket.on('arduino.kill', async (callback) => {
const [error, result] = await to(this.#shellArduino_.kill());
error && Debug.error(error);
callback([error, result]);
});
}
#addEventsListenerForSerial_(socket) {
socket.on('serial.getPorts', async (callback) => {
const [error, result] = await to(Serial.getPorts());
error && Debug.error(error);
callback([error, result]);
});
socket.on('serial.create', (port) => {
const serial = new Serial(port);
this.#serialRegistry_.register(port, serial);
serial.bind('buffer', (buffer) => {
socket.emit('serial.bufferEvent', port, buffer);
});
serial.bind('string', (str) => {
socket.emit('serial.stringEvent', port, str);
});
serial.bind('error', (error) => {
socket.emit('serial.errorEvent', port, error);
});
serial.bind('open', () => {
socket.emit('serial.openEvent', port);
});
serial.bind('close', (code) => {
socket.emit('serial.closeEvent', port, code);
});
});
socket.on('serial.dispose', async (port, callback) => {
const serial = this.#serialRegistry_.getItem(port);
const [error, result] = await to(serial.dispose());
error && Debug.error(error);
this.#serialRegistry_.unregister(port);
callback([error, result]);
});
socket.on('serial.open', async (port, baud, callback) => {
const serial = this.#serialRegistry_.getItem(port);
const [error, result] = await to(serial.open(baud));
error && Debug.error(error);
callback([error, result]);
});
socket.on('serial.close', async (port, callback) => {
const serial = this.#serialRegistry_.getItem(port);
const [error, result] = await to(serial.close());
error && Debug.error(error);
callback([error, result]);
});
socket.on('serial.setBaudRate', async (port, baud, callback) => {
const serial = this.#serialRegistry_.getItem(port);
const [error, result] = await to(serial.setBaudRate(baud));
error && Debug.error(error);
callback([error, result]);
});
socket.on('serial.send', async (port, data, callback) => {
const serial = this.#serialRegistry_.getItem(port);
const [error, result] = await to(serial.send(data));
error && Debug.error(error);
callback([error, result]);
});
socket.on('serial.setDTRAndRTS', async (port, dtr, rts, callback) => {
const serial = this.#serialRegistry_.getItem(port);
const [error, result] = await to(serial.setDTRAndRTS(dtr, rts));
error && Debug.error(error);
callback([error, result]);
});
}
#addEventsListener_(socket) {
socket.on('serial/get-ports', async () => {
const [error, result] = await to(Serial.getPorts());
error && Debug.error(error);
return result;
socket.on('disconnect', () => {
let { build } = ARDUINO.path;
let user = path.resolve(build, socket.id);
if (fsPlus.isDirectorySync(user)) {
fsExtra.remove(user);
}
for (let key of this.#serialRegistry_.keys()) {
const serial = this.#serialRegistry_.getItem(key);
serial.dispose().catch(Debug.error);
this.#serialRegistry_.unregister(key);
}
});
this.#addEventsListenerForArduino_(socket);
this.#addEventsListenerForSerial_(socket);
}
getIO() {
return this.#io_;
}
}