Update: WebSocket模式添加对Serial和Arduino编译上传的支持
This commit is contained in:
33
.eslintrc.js
Normal file
33
.eslintrc.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
module.exports = {
|
||||||
|
// 继承 Eslint 规则
|
||||||
|
extends: ["eslint:recommended"],
|
||||||
|
env: {
|
||||||
|
es6: true,
|
||||||
|
node: true, // 启用node中全局变量
|
||||||
|
browser: false, // 启用浏览器中全局变量
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 15,
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-dupe-args": 2, // 函数参数不能重复
|
||||||
|
"no-duplicate-case": 2, // switch中的case标签不能重复
|
||||||
|
"no-else-return": 2, // 如果if语句里面有return,后面不能跟else语句
|
||||||
|
"no-empty": 2, // 块语句中的内容不能为空
|
||||||
|
"no-var": 0, // 不能使用 var 定义变量
|
||||||
|
"indent": [2, 4], // 缩进风格
|
||||||
|
"strict": 2,
|
||||||
|
"use-isnan": 2,
|
||||||
|
"no-redeclare": 2, // 禁止重复声明变量
|
||||||
|
"no-trailing-spaces": 1, // 一行结束后面不要有空格
|
||||||
|
"no-this-before-super": 2, // 在调用super()之前不能使用this或super
|
||||||
|
"no-unneeded-ternary": 2, // 禁止不必要的嵌套 var isYes = answer === 1 ? true : false;
|
||||||
|
"no-unreachable": 2, // 不能有无法执行的代码
|
||||||
|
"no-use-before-define": 2, // 未定义前不能使用
|
||||||
|
"new-cap": 2, // 函数名首行大写必须使用new方式调用,首行小写必须用不带new方式调用
|
||||||
|
"new-parens": 2, // new时必须加小括号
|
||||||
|
"eqeqeq": 0, // 必须使用全等
|
||||||
|
"no-import-assign": 0
|
||||||
|
},
|
||||||
|
};
|
||||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,2 +1,9 @@
|
|||||||
|
__pycache__
|
||||||
/node_modules
|
*.pyc
|
||||||
|
*.DS_Store
|
||||||
|
node_modules
|
||||||
|
.idea
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
dist
|
||||||
|
/temp
|
||||||
|
|||||||
22
README.md
Normal file
22
README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
## Mixly3.0 服务端
|
||||||
|
|
||||||
|
1.安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2.脚本执行
|
||||||
|
|
||||||
|
- 调试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
- 针对生产环境打包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:prod
|
||||||
|
```
|
||||||
|
|
||||||
26
certs/server.crt
Normal file
26
certs/server.crt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEXjCCAsagAwIBAgIQC+VBZ3EzgSGCMSBQIbLy+jANBgkqhkiG9w0BAQsFADCB
|
||||||
|
hTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMS0wKwYDVQQLDCRERVNL
|
||||||
|
VE9QLVZQMU4zNzJcVXNlckBERVNLVE9QLVZQMU4zNzIxNDAyBgNVBAMMK21rY2Vy
|
||||||
|
dCBERVNLVE9QLVZQMU4zNzJcVXNlckBERVNLVE9QLVZQMU4zNzIwHhcNMjQxMTI5
|
||||||
|
MTgwODE0WhcNMjcwMzAxMTgwODE0WjBYMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxv
|
||||||
|
cG1lbnQgY2VydGlmaWNhdGUxLTArBgNVBAsMJERFU0tUT1AtVlAxTjM3MlxVc2Vy
|
||||||
|
QERFU0tUT1AtVlAxTjM3MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
||||||
|
AMBMqCJwWtIStsMQMjW0HuLjEZNAQiys1B5XRfd2NE/HJkv2AwKGwGT70gzG28BB
|
||||||
|
2v7EOhfPCN7yXrzIw5BIItu76eCVnwfbd+KhyFNyfRsqHkQp3yFcAXJ+NonyoU8u
|
||||||
|
28dP0CTxTz3n3Pn9yx8p26ePpr2D7fYbB7lyKByxEkoWHnTCCZ8wPeZ58JEba4ob
|
||||||
|
SSuGSYOm3xdqcTBFQVCRi/1AGk9R4YozGNpnycGNqEythsIp4J7chjIVtC2WONFp
|
||||||
|
s0YG/GuPIHIxv/2bHMQJMg3GW5oLJP53E+C7xI1tVv572STv7ZGWFopgsmDomWs5
|
||||||
|
S/XkvdXM3xx5Yr+P9gmRGRMCAwEAAaN2MHQwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud
|
||||||
|
JQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFIb2OJENGB0jIgBAczrFu8nddW/m
|
||||||
|
MCwGA1UdEQQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATAN
|
||||||
|
BgkqhkiG9w0BAQsFAAOCAYEAeYZYXyR3xacF9xZGJwYS8pjm5lBA3Wfuqdl+U7J6
|
||||||
|
u4rRs+vrAGMfDyL1BWRLcwJuDKhog8QyAFsBk/L++vxG1zeDAFRisdu5fcO3yHfl
|
||||||
|
9nlh44sFhvRHs5ZHz/EG7vaduAkV6yu4kBfctPFvyekT3YJ/+1gujcVHrAnw16LA
|
||||||
|
aC59e33ouWDw3+apTO7c9kiNxA3m4RTn6LLpVkSPqthe8OYm+QaoOR9eci8HJzYs
|
||||||
|
DyEGrSBtHdIOivmlbEn7FAqnaVshFg0JnjHvSLp2Nj90JCRPVsTPSHqCwLWBT8RH
|
||||||
|
aT4Tuotz7k8QAMa/LiOFMJdQF/kPIg3rNh37F4W7xz/C7lgfuvMCKqxLdDoBDROO
|
||||||
|
pZPdQuPVGKna2boFp+Hd3eXHPq3UZNjbU1xmYxBg9R0CgTvSZgdCjSrqTjujkLwp
|
||||||
|
qAhcdgxxfZ9JqAwJLmnPJ8Oq+1dJ1aI+JlDWNY4GpIHc7vYPSMT4+4RcfYvK3n2/
|
||||||
|
jXJAGJALls4DN5T7+Zl20Bqh
|
||||||
|
-----END CERTIFICATE-----
|
||||||
28
certs/server.key
Normal file
28
certs/server.key
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDATKgicFrSErbD
|
||||||
|
EDI1tB7i4xGTQEIsrNQeV0X3djRPxyZL9gMChsBk+9IMxtvAQdr+xDoXzwje8l68
|
||||||
|
yMOQSCLbu+nglZ8H23fiochTcn0bKh5EKd8hXAFyfjaJ8qFPLtvHT9Ak8U8959z5
|
||||||
|
/csfKdunj6a9g+32Gwe5cigcsRJKFh50wgmfMD3mefCRG2uKG0krhkmDpt8XanEw
|
||||||
|
RUFQkYv9QBpPUeGKMxjaZ8nBjahMrYbCKeCe3IYyFbQtljjRabNGBvxrjyByMb/9
|
||||||
|
mxzECTINxluaCyT+dxPgu8SNbVb+e9kk7+2RlhaKYLJg6JlrOUv15L3VzN8ceWK/
|
||||||
|
j/YJkRkTAgMBAAECggEAJj6YqdDCn0kNhBnygm/CPMmAM1LyVkRBI4+j4KVnnf4z
|
||||||
|
haP10UjPdsYGbngWOFRgo46PJzELyJtXcCBVlJpkJGNpDjtzl3J0k2QtnTsF2qMz
|
||||||
|
xca2E8Jin0yuuBzDLCZbm3YqJU7AvcUhU66/+QCwKNEPDl3ws6OIk786bwpgYs9P
|
||||||
|
GJYtLxsXt0eoTK9shWBX4J0QP+rBe5gyR9RYQr0SGBX0L4R3bSks1RnSjLTSz0Tr
|
||||||
|
J/mUlTwgtbVVnLJCckbjYFngSMSp4Lqiamh0u5uCIrUkmyx9fJvhSc4dpU1zOijk
|
||||||
|
fMpepXfd03Jmt9QNbFnPC/hhaXkmao6VutKH0bgY4QKBgQDgiBAtFyawSd1HUs3U
|
||||||
|
aHdhy7/Cf053atVqxF7IDQIBcbOBvO8jcQ7QdL+rtabHbrGAdAypIQee4CHhj4In
|
||||||
|
LWNNk9YEqgWygByID/19dFOKAq/KdK0WwJ32bSmozjxB5KSDXrSxd7aOO+3gyzsd
|
||||||
|
ef4mzNN+NV2Z0TZAex8/lyMcawKBgQDbQCTOAt3G6q1nSEk+43q2NrUbzCcKGQuy
|
||||||
|
thdqyXJMkytACnUlsRyLXgXt6nIucm+fcpevy8DAya7HTb/ditu+cuIx9YcFRXff
|
||||||
|
P56A3d2wkDB3m0UjUHrrS2+1T6Yhy/FdwTnK2sbCavZy8ztijuxvpKjPZrfxv0f/
|
||||||
|
yhyE3kyf+QKBgQCCeS6FNTXLLTEDmC95ZbcxwhdNa3LqW89menPlZgGrWyoHkwWX
|
||||||
|
n8QPCDi1DBq9Oyq0TTtqMIJgJTgq/ZyRLYPN+cFc9nvXDTEHM9uGwkklIoiKaiCG
|
||||||
|
3ykroKWbrTRAyh97I1Z25ezUXCjP/uj8KP6yB0ZCybn5fyQ6dhFjf2zsvwKBgQCk
|
||||||
|
lErMZfRqfAD8lZN95K5Zl0lt+1qWxuQ0G2IKo/rrplGB+hej2oZNy59xz1o0qWxG
|
||||||
|
6XMZ4D3pubs8Go3a1IBNPtmynNbkyxfHem8V1vWxsxrevawxbRlIBNFu7cIMfpXJ
|
||||||
|
ReGG96DZkgc7lH/QZO7wg07AmR+dFXQLe4Da8C/eqQKBgG9xSwti9qIaEXvJBOcs
|
||||||
|
mNo3eWeUrLcC4MfYcMwxCNMBUUtgdiSWg2pMJSSaePzidPbJ3GJrz7v8NIHr2n6g
|
||||||
|
4PpbeD6ixS+eynXkbWhuf3F+N1orDwO+B56VXdvLSRmu1p4OyQdV2MY83F2kT6cn
|
||||||
|
INRTi8PwMq1JtN4RglT2Ejow
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
8
nodemon.json
Normal file
8
nodemon.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"watch": [
|
||||||
|
"dist",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"ext": "js,json",
|
||||||
|
"exec": "node dist/bundle.js"
|
||||||
|
}
|
||||||
4115
package-lock.json
generated
4115
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -4,7 +4,11 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"build:dev": "webpack --config=webpack.dev.js",
|
||||||
|
"build:prod": "webpack --config=webpack.prod.js",
|
||||||
|
"start-server": "nodemon --watch dist --exec node dist/bundle.js",
|
||||||
|
"watch": "webpack --watch --config=webpack.dev.js",
|
||||||
|
"start": "npm-run-all --parallel watch start-server"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -17,6 +21,8 @@
|
|||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
|
"fs-plus": "^3.1.1",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"serialport": "^12.0.0",
|
"serialport": "^12.0.0",
|
||||||
@@ -25,5 +31,15 @@
|
|||||||
"simple-git": "^3.27.0",
|
"simple-git": "^3.27.0",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"usb": "^2.14.0"
|
"usb": "^2.14.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.51.0",
|
||||||
|
"eslint-webpack-plugin": "^4.0.1",
|
||||||
|
"nodemon": "^3.1.7",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"webpack": "^5.89.0",
|
||||||
|
"webpack-cli": "^5.1.4",
|
||||||
|
"webpack-merge": "^6.0.1",
|
||||||
|
"webpack-node-externals": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = '';
|
||||||
@@ -2,7 +2,7 @@ import Events from './events';
|
|||||||
|
|
||||||
|
|
||||||
export default class EventsBase {
|
export default class EventsBase {
|
||||||
#events_ = new Events();
|
#events_ = new Events([]);
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
bind(type, func) {
|
bind(type, func) {
|
||||||
@@ -10,11 +10,11 @@ export default class EventsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unbind(id) {
|
unbind(id) {
|
||||||
this.#events_.unbind(id);
|
return this.#events_.unbind(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventsType(eventsType) {
|
addEventsType(eventsType) {
|
||||||
this.#events_.addType(eventsType);
|
return this.#events_.addType(eventsType);
|
||||||
}
|
}
|
||||||
|
|
||||||
runEvent(eventsType, ...args) {
|
runEvent(eventsType, ...args) {
|
||||||
@@ -22,11 +22,11 @@ export default class EventsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
offEvent(eventsType) {
|
offEvent(eventsType) {
|
||||||
this.#events_.off(eventsType);
|
return this.#events_.off(eventsType);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetEvent() {
|
resetEvent() {
|
||||||
this.#events_.reset();
|
return this.#events_.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
disposeEvent() {
|
disposeEvent() {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default class Events {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addType(eventsType) {
|
addType(eventsType) {
|
||||||
this.#eventsType_ = _.uniq(_.concat([this.#eventsType_, eventsType]));
|
this.#eventsType_ = _.uniq(_.concat(this.#eventsType_, eventsType));
|
||||||
}
|
}
|
||||||
|
|
||||||
exist(type) {
|
exist(type) {
|
||||||
@@ -40,7 +40,7 @@ export default class Events {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unbind(id) {
|
unbind(id) {
|
||||||
for (let [_, value] of this.#events_.getAllItems()) {
|
for (let [, value] of this.#events_.getAllItems()) {
|
||||||
let typeEvent = value;
|
let typeEvent = value;
|
||||||
if (!typeEvent.getItem(id)) {
|
if (!typeEvent.getItem(id)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -66,7 +66,7 @@ export default class Events {
|
|||||||
if (!eventsFunc) {
|
if (!eventsFunc) {
|
||||||
return outputs;
|
return outputs;
|
||||||
}
|
}
|
||||||
for (let [_, func] of eventsFunc.getAllItems()) {
|
for (let [, func] of eventsFunc.getAllItems()) {
|
||||||
outputs.push(func(...args));
|
outputs.push(func(...args));
|
||||||
}
|
}
|
||||||
return outputs;
|
return outputs;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import Debug from './debug';
|
||||||
|
|
||||||
|
|
||||||
export default class Registry {
|
export default class Registry {
|
||||||
#registry_ = new Map();
|
#registry_ = new Map();
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import os from 'node:os';
|
import { exec } from 'node:child_process';
|
||||||
import { ChildProcess } from 'node:child_process';
|
|
||||||
import {
|
import {
|
||||||
SerialPort,
|
SerialPort,
|
||||||
ReadlineParser,
|
ReadlineParser,
|
||||||
ByteLengthParser
|
ByteLengthParser
|
||||||
} from 'serialport';
|
} from 'serialport';
|
||||||
|
import _ from 'lodash';
|
||||||
import EventsBase from './events-base';
|
import EventsBase from './events-base';
|
||||||
|
import { CURRENT_PLANTFORM } from './config';
|
||||||
|
|
||||||
|
|
||||||
export default class Serial extends EventsBase {
|
export default class Serial extends EventsBase {
|
||||||
@@ -18,17 +19,17 @@ export default class Serial extends EventsBase {
|
|||||||
|
|
||||||
this.getPorts = async function () {
|
this.getPorts = async function () {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (os.platform() === 'linux') {
|
if (CURRENT_PLANTFORM === 'linux') {
|
||||||
ChildProcess.exec('ls /dev/ttyACM* /dev/tty*USB*', (_, stdout, stderr) => {
|
exec('ls /dev/ttyACM* /dev/tty*USB*', (error, stdout) => {
|
||||||
let portsName = MArray.unique(stdout.split('\n'));
|
let portsName = _.uniq(stdout.split('\n'));
|
||||||
let newPorts = [];
|
let newPorts = [];
|
||||||
for (let i = 0; i < portsName.length; i++) {
|
for (let i = 0; i < portsName.length; i++) {
|
||||||
if (!portsName[i]) {
|
if (!portsName[i]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
newPorts.push({
|
newPorts.push({
|
||||||
vendorId: 'None',
|
vendorId: null,
|
||||||
productId: 'None',
|
productId: null,
|
||||||
name: portsName[i]
|
name: portsName[i]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -56,19 +57,25 @@ export default class Serial extends EventsBase {
|
|||||||
#parserBytes_ = null;
|
#parserBytes_ = null;
|
||||||
#parserLine_ = null;
|
#parserLine_ = null;
|
||||||
#port_ = null;
|
#port_ = null;
|
||||||
|
#isOpened_ = false;
|
||||||
|
|
||||||
constructor(port) {
|
constructor(port) {
|
||||||
|
super();
|
||||||
this.#port_ = port;
|
this.#port_ = port;
|
||||||
this.addEventsType(['buffer', 'String', 'error', 'open', 'close']);
|
this.addEventsType(['buffer', 'string', 'error', 'open', 'close']);
|
||||||
}
|
}
|
||||||
|
|
||||||
#addEventsListener_() {
|
#addEventsListener_() {
|
||||||
this.#parserBytes_.on('data', (buffer) => {
|
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.#parserLine_.on('data', (str) => {
|
||||||
this.runEvent('String', str);
|
this.runEvent('string', str);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#serialport_.on('error', (error) => {
|
this.#serialport_.on('error', (error) => {
|
||||||
@@ -76,31 +83,42 @@ export default class Serial extends EventsBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.#serialport_.on('open', () => {
|
this.#serialport_.on('open', () => {
|
||||||
|
this.#isOpened_ = true;
|
||||||
this.runEvent('open');
|
this.runEvent('open');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#serialport_.on('close', () => {
|
this.#serialport_.on('close', () => {
|
||||||
|
this.#isOpened_ = false;
|
||||||
this.runEvent('close');
|
this.runEvent('close');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isOpened() {
|
||||||
|
return this.#isOpened_;
|
||||||
|
}
|
||||||
|
|
||||||
getPortName() {
|
getPortName() {
|
||||||
return this.#port_;
|
return this.#port_;
|
||||||
}
|
}
|
||||||
|
|
||||||
async open(baud) {
|
async open(baud) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.isOpened()) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.#serialport_ = new SerialPort({
|
this.#serialport_ = new SerialPort({
|
||||||
path: this.getPortName(),
|
path: this.getPortName(),
|
||||||
baudRate: baud, // 波特率
|
baudRate: baud,
|
||||||
dataBits: 8, // 数据位
|
dataBits: 8,
|
||||||
parity: 'none', // 奇偶校验
|
parity: 'none',
|
||||||
stopBits: 1, // 停止位
|
stopBits: 1,
|
||||||
flowControl: false,
|
flowControl: false,
|
||||||
autoOpen: false // 不自动打开
|
autoOpen: false
|
||||||
}, false);
|
}, false);
|
||||||
this.#parserBytes_ = this.#serialport_.pipe(new ByteLengthParser({ length: 1 }));
|
this.#parserBytes_ = this.#serialport_.pipe(new ByteLengthParser({ length: 1 }));
|
||||||
this.#parserLine_ = this.#serialport_.pipe(new ReadlineParser());
|
this.#parserLine_ = this.#serialport_.pipe(new ReadlineParser());
|
||||||
|
this.#addEventsListener_();
|
||||||
this.#serialport_.open((error) => {
|
this.#serialport_.open((error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
@@ -108,12 +126,15 @@ export default class Serial extends EventsBase {
|
|||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.#addEventsListener_();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.isOpened()) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.#serialport_.close((error) => {
|
this.#serialport_.close((error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
@@ -126,18 +147,26 @@ export default class Serial extends EventsBase {
|
|||||||
|
|
||||||
async setBaudRate(baud) {
|
async setBaudRate(baud) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.isOpened()) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.#serialport_.update({ baudRate: baud }, (error) => {
|
this.#serialport_.update({ baudRate: baud }, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
return;
|
} else {
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
resolve();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(data) {
|
async send(data) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.isOpened()) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.#serialport_.write(data, (error) => {
|
this.#serialport_.write(data, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
@@ -150,18 +179,22 @@ export default class Serial extends EventsBase {
|
|||||||
|
|
||||||
async setDTRAndRTS(dtr, rts) {
|
async setDTRAndRTS(dtr, rts) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.isOpened()) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.#serialport_.set({ dtr, rts }, (error) => {
|
this.#serialport_.set({ dtr, rts }, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
super.setDTRAndRTS(dtr, rts);
|
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
async dispose() {
|
||||||
|
await this.close();
|
||||||
this.disposeEvent();
|
this.disposeEvent();
|
||||||
this.#serialport_ = null;
|
this.#serialport_ = null;
|
||||||
this.#parserBytes_ = null;
|
this.#parserBytes_ = null;
|
||||||
|
|||||||
48
src/common/shell-arduino.js
Normal file
48
src/common/shell-arduino.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/common/shell-micropython.js
Normal file
0
src/common/shell-micropython.js
Normal file
116
src/common/shell.js
Normal file
116
src/common/shell.js
Normal 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_;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/index.js
26
src/index.js
@@ -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);
|
||||||
@@ -1,23 +1,174 @@
|
|||||||
import { Server } from 'socket.io';
|
import { Server } from 'socket.io';
|
||||||
import to from 'await-to-js';
|
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 Serial from '../common/serial';
|
||||||
import Debug from '../common/debug';
|
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 {
|
export default class Socket {
|
||||||
#io_ = null;
|
#io_ = null;
|
||||||
|
#serialRegistry_ = new Registry();
|
||||||
|
#shellArduino_ = new ShellArduino();
|
||||||
|
|
||||||
constructor(httpsServer, options) {
|
constructor(httpsServer, options) {
|
||||||
this.#io_ = new Server(httpsServer, options);
|
this.#io_ = new Server(httpsServer, options);
|
||||||
this.#io_.on('connection', (socket) => {
|
this.#io_.on('connection', (socket) => {
|
||||||
this.#addEventsListener_(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) {
|
#addEventsListener_(socket) {
|
||||||
socket.on('serial/get-ports', async () => {
|
socket.on('disconnect', () => {
|
||||||
const [error, result] = await to(Serial.getPorts());
|
let { build } = ARDUINO.path;
|
||||||
error && Debug.error(error);
|
let user = path.resolve(build, socket.id);
|
||||||
return result;
|
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_;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
13
webpack.common.js
Normal file
13
webpack.common.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const nodeExternals = require('webpack-node-externals');
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
target: 'node',
|
||||||
|
entry: './src/index.js',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'bundle.js'
|
||||||
|
},
|
||||||
|
externals: [nodeExternals()],
|
||||||
|
};
|
||||||
13
webpack.dev.js
Normal file
13
webpack.dev.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const common = require('./webpack.common');
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: 'development',
|
||||||
|
plugins: [
|
||||||
|
new ESLintPlugin({
|
||||||
|
context: process.cwd()
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
7
webpack.prod.js
Normal file
7
webpack.prod.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const common = require('./webpack.common');
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: 'production'
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user