feat(core): 支持通过web usb烧录固件
This commit is contained in:
@@ -249,10 +249,10 @@ class App extends Component {
|
|||||||
if (goog.isElectron || Env.hasSocketServer) {
|
if (goog.isElectron || Env.hasSocketServer) {
|
||||||
return SELECTED_BOARD?.nav?.burn;
|
return SELECTED_BOARD?.nav?.burn;
|
||||||
}
|
}
|
||||||
if (Serial.devicesRegistry.hasKey('serial')) {
|
if (Serial.devicesRegistry.hasKey('hid')) {
|
||||||
return SELECTED_BOARD?.nav?.burn;
|
|
||||||
} else {
|
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
return SELECTED_BOARD?.nav?.burn;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
callback: () => BU.initBurn(),
|
callback: () => BU.initBurn(),
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ class Serial {
|
|||||||
#baud_ = 0;
|
#baud_ = 0;
|
||||||
#dtr_ = false;
|
#dtr_ = false;
|
||||||
#rts_ = false;
|
#rts_ = false;
|
||||||
|
#vid_ = 0x0000;
|
||||||
|
#pid_ = 0x0000;
|
||||||
#isOpened_ = false;
|
#isOpened_ = false;
|
||||||
#port_ = '';
|
#port_ = '';
|
||||||
#special_ = [];
|
#special_ = [];
|
||||||
@@ -250,6 +252,19 @@ class Serial {
|
|||||||
this.#rts_ = rts;
|
this.#rts_ = rts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setVID(vid) {
|
||||||
|
this.#vid_ = vid;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPID(pid) {
|
||||||
|
this.#pid_ = pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setVIDAndPID(vid, pid) {
|
||||||
|
this.#vid_ = vid;
|
||||||
|
this.#pid_ = pid;
|
||||||
|
}
|
||||||
|
|
||||||
getPortName() {
|
getPortName() {
|
||||||
return this.#port_;
|
return this.#port_;
|
||||||
}
|
}
|
||||||
@@ -270,6 +285,14 @@ class Serial {
|
|||||||
return this.#rts_;
|
return this.#rts_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVID() {
|
||||||
|
return this.#vid_;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPID() {
|
||||||
|
return this.#pid_;
|
||||||
|
}
|
||||||
|
|
||||||
async sendString(str) {}
|
async sendString(str) {}
|
||||||
|
|
||||||
async sendBuffer(buffer) {}
|
async sendBuffer(buffer) {}
|
||||||
|
|||||||
@@ -1600,7 +1600,8 @@
|
|||||||
"Mixly.LayerFirmware",
|
"Mixly.LayerFirmware",
|
||||||
"Mixly.LayerProgress",
|
"Mixly.LayerProgress",
|
||||||
"Mixly.Web.Serial",
|
"Mixly.Web.Serial",
|
||||||
"Mixly.Web.Ampy"
|
"Mixly.Web.Ampy",
|
||||||
|
"Mixly.Web.SerialTransport"
|
||||||
],
|
],
|
||||||
"provide": [
|
"provide": [
|
||||||
"Mixly.Web.BU"
|
"Mixly.Web.BU"
|
||||||
@@ -1656,15 +1657,6 @@
|
|||||||
"Mixly.Web.FS"
|
"Mixly.Web.FS"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "/web/hid-transport.js",
|
|
||||||
"require": [
|
|
||||||
"Mixly.Web"
|
|
||||||
],
|
|
||||||
"provide": [
|
|
||||||
"Mixly.Web.HIDTransport"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "/web/hid.js",
|
"path": "/web/hid.js",
|
||||||
"require": [
|
"require": [
|
||||||
@@ -1676,6 +1668,15 @@
|
|||||||
"Mixly.Web.HID"
|
"Mixly.Web.HID"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "/web/serial-transport.js",
|
||||||
|
"require": [
|
||||||
|
"Mixly.Web"
|
||||||
|
],
|
||||||
|
"provide": [
|
||||||
|
"Mixly.Web.SerialTransport"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "/web/serial.js",
|
"path": "/web/serial.js",
|
||||||
"require": [
|
"require": [
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ goog.require('Mixly.LayerFirmware');
|
|||||||
goog.require('Mixly.LayerProgress');
|
goog.require('Mixly.LayerProgress');
|
||||||
goog.require('Mixly.Web.Serial');
|
goog.require('Mixly.Web.Serial');
|
||||||
goog.require('Mixly.Web.Ampy');
|
goog.require('Mixly.Web.Ampy');
|
||||||
|
goog.require('Mixly.Web.SerialTransport');
|
||||||
goog.provide('Mixly.Web.BU');
|
goog.provide('Mixly.Web.BU');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -42,11 +43,13 @@ const {
|
|||||||
const {
|
const {
|
||||||
Serial,
|
Serial,
|
||||||
BU,
|
BU,
|
||||||
Ampy
|
Ampy,
|
||||||
|
SerialTransport
|
||||||
} = Web;
|
} = Web;
|
||||||
|
|
||||||
const { BOARD, SELECTED_BOARD } = Config;
|
const { BOARD, SELECTED_BOARD } = Config;
|
||||||
const { ESPLoader, Transport } = esptooljs;
|
const { ESPLoader } = esptooljs;
|
||||||
|
|
||||||
|
|
||||||
BU.uploading = false;
|
BU.uploading = false;
|
||||||
BU.burning = false;
|
BU.burning = false;
|
||||||
@@ -246,7 +249,7 @@ BU.burnWithEsptool = async (binFile, erase) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const port = Serial.getPort(portName);
|
const port = Serial.getPort(portName);
|
||||||
if (['HIDDevice', 'USBDevice'].includes(port.constructor.name)) {
|
if (['HIDDevice'].includes(port.constructor.name)) {
|
||||||
layer.msg(Msg.Lang['burn.notSupport'], { time: 1000 });
|
layer.msg(Msg.Lang['burn.notSupport'], { time: 1000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -264,13 +267,14 @@ BU.burnWithEsptool = async (binFile, erase) => {
|
|||||||
let transport = null;
|
let transport = null;
|
||||||
try {
|
try {
|
||||||
const baudrate = Boards.getSelectedBoardConfigParam('BurnSpeed') ?? '460800';
|
const baudrate = Boards.getSelectedBoardConfigParam('BurnSpeed') ?? '460800';
|
||||||
transport = new Transport(port, false);
|
const serial = new Serial(portName);
|
||||||
|
transport = new SerialTransport(serial, false);
|
||||||
esploader = new ESPLoader({
|
esploader = new ESPLoader({
|
||||||
transport,
|
transport,
|
||||||
baudrate,
|
baudrate,
|
||||||
terminal: {
|
terminal: {
|
||||||
clean() {
|
clean() {
|
||||||
statusBarTerminal.setValue('');
|
statusBarTerminal.setValue(Msg.Lang['shell.burning'] + '...\n');
|
||||||
},
|
},
|
||||||
writeLine(data) {
|
writeLine(data) {
|
||||||
statusBarTerminal.addValue(data + '\n');
|
statusBarTerminal.addValue(data + '\n');
|
||||||
@@ -290,6 +294,7 @@ BU.burnWithEsptool = async (binFile, erase) => {
|
|||||||
Debug.error(error);
|
Debug.error(error);
|
||||||
}
|
}
|
||||||
BU.progressLayer.hide();
|
BU.progressLayer.hide();
|
||||||
|
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,8 +304,6 @@ BU.burnWithEsptool = async (binFile, erase) => {
|
|||||||
for (let i of binFile) {
|
for (let i of binFile) {
|
||||||
if (i.path && i.offset) {
|
if (i.path && i.offset) {
|
||||||
let absolutePath = path.join(Env.boardDirPath, i.path);
|
let absolutePath = path.join(Env.boardDirPath, i.path);
|
||||||
// statusBarTerminal.addValue(`${Msg.Lang['读取固件'] + ' '
|
|
||||||
// + Msg.Lang['路径']}:${absolutePath}, ${Msg.Lang['偏移']}:${i.offset}\n`);
|
|
||||||
firmwarePromise.push(readBinFile(absolutePath, i.offset));
|
firmwarePromise.push(readBinFile(absolutePath, i.offset));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,6 +319,7 @@ BU.burnWithEsptool = async (binFile, erase) => {
|
|||||||
Debug.error(error);
|
Debug.error(error);
|
||||||
}
|
}
|
||||||
BU.progressLayer.hide();
|
BU.progressLayer.hide();
|
||||||
|
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
statusBarTerminal.addValue("Done!\n");
|
statusBarTerminal.addValue("Done!\n");
|
||||||
|
|||||||
@@ -1,418 +0,0 @@
|
|||||||
goog.loadJs('web', () => {
|
|
||||||
|
|
||||||
goog.require('Mixly.Web');
|
|
||||||
goog.provide('Mixly.Web.HIDTransport');
|
|
||||||
|
|
||||||
const { Web } = Mixly;
|
|
||||||
|
|
||||||
class Transport {
|
|
||||||
slipReaderEnabled = false;
|
|
||||||
baudrate = 0;
|
|
||||||
traceLog = "";
|
|
||||||
lastTraceTime = Date.now();
|
|
||||||
finished = false;
|
|
||||||
buffer = new Uint8Array(0);
|
|
||||||
device = null;
|
|
||||||
tracing = false;
|
|
||||||
received = new Uint8Array(0);
|
|
||||||
|
|
||||||
constructor(device, tracing, enableSlipReader) {
|
|
||||||
this.slipReaderEnabled = enableSlipReader;
|
|
||||||
this.device = device;
|
|
||||||
this.tracing = tracing;
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendBuffer(buffer) {
|
|
||||||
for (let i = 0; i < buffer.length / 31; i++) {
|
|
||||||
const len = Math.min((i + 1) * 31, buffer.length) - i * 31;
|
|
||||||
let temp = new Uint8Array(len + 1);
|
|
||||||
temp[0] = len;
|
|
||||||
temp.set(buffer.subarray(i * 31, Math.min((i + 1) * 31, buffer.length)), 1);
|
|
||||||
await this.device.sendReport(0, temp);
|
|
||||||
}
|
|
||||||
// if (buffer instanceof Uint8Array) {
|
|
||||||
// let temp = new Uint8Array(buffer.length + 1);
|
|
||||||
// temp[0] = buffer.length;
|
|
||||||
// temp.set(buffer, 1);
|
|
||||||
// buffer = temp;
|
|
||||||
// } else {
|
|
||||||
// buffer.unshift(buffer.length);
|
|
||||||
// buffer = new Uint8Array(buffer);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
#addEventsListener_() {
|
|
||||||
this.device.oninputreport = (event) => {
|
|
||||||
const { data } = event;
|
|
||||||
const length = Math.min(data.getUint8(0), data.byteLength);
|
|
||||||
let buffer = [];
|
|
||||||
for (let i = 1; i <= length; i++) {
|
|
||||||
buffer.push(data.getUint8(i));
|
|
||||||
}
|
|
||||||
this.received = this.appendArray(this.received, new Uint8Array(buffer));
|
|
||||||
};
|
|
||||||
|
|
||||||
// this.device.onclose = () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
async readOrigin(timeout) {
|
|
||||||
let time = Date.now();
|
|
||||||
let output = {
|
|
||||||
done: false,
|
|
||||||
value: null
|
|
||||||
};
|
|
||||||
while (Date.now() - time < timeout && !this.finished) {
|
|
||||||
if (this.received.byteLength) {
|
|
||||||
output.value = this.received;
|
|
||||||
this.received = new Uint8Array(0);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
await this.sleep(5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!output.value) {
|
|
||||||
output.value = new Uint8Array(0);
|
|
||||||
}
|
|
||||||
output.done = this.finished;
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request the serial device vendor ID and Product ID as string.
|
|
||||||
* @returns {string} Return the device VendorID and ProductID from SerialPortInfo as formatted string.
|
|
||||||
*/
|
|
||||||
getInfo() {
|
|
||||||
const { vendorId, productId } = this.device;
|
|
||||||
return vendorId && productId
|
|
||||||
? `WebHID VendorID 0x${vendorId.toString(16)} ProductID 0x${productId.toString(16)}`
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request the serial device product id from SerialPortInfo.
|
|
||||||
* @returns {number | undefined} Return the product ID.
|
|
||||||
*/
|
|
||||||
getPid() {
|
|
||||||
return this.device.productId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format received or sent data for tracing output.
|
|
||||||
* @param {string} message Message to format as trace line.
|
|
||||||
*/
|
|
||||||
trace(message) {
|
|
||||||
if (!this.tracing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const delta = Date.now() - this.lastTraceTime;
|
|
||||||
const prefix = `TRACE ${delta.toFixed(3)}`;
|
|
||||||
const traceMessage = `${prefix} ${message}`;
|
|
||||||
console.log(traceMessage);
|
|
||||||
this.traceLog += traceMessage + "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
async returnTrace() {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(this.traceLog);
|
|
||||||
console.log("Text copied to clipboard!");
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to copy text:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hexify(s) {
|
|
||||||
return Array.from(s)
|
|
||||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
||||||
.join("")
|
|
||||||
.padEnd(16, " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
hexConvert(uint8Array, autoSplit) {
|
|
||||||
if (autoSplit && uint8Array.length > 16) {
|
|
||||||
let result = "";
|
|
||||||
let s = uint8Array;
|
|
||||||
|
|
||||||
while (s.length > 0) {
|
|
||||||
const line = s.slice(0, 16);
|
|
||||||
const asciiLine = String.fromCharCode(...line)
|
|
||||||
.split("")
|
|
||||||
.map((c) => (c === " " || (c >= " " && c <= "~" && c !== " ") ? c : "."))
|
|
||||||
.join("");
|
|
||||||
s = s.slice(16);
|
|
||||||
result += `\n ${this.hexify(line.slice(0, 8))} ${this.hexify(line.slice(8))} | ${asciiLine}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
return this.hexify(uint8Array);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format data packet using the Serial Line Internet Protocol (SLIP).
|
|
||||||
* @param {Uint8Array} data Binary unsigned 8 bit array data to format.
|
|
||||||
* @returns {Uint8Array} Formatted unsigned 8 bit data array.
|
|
||||||
*/
|
|
||||||
slipWriter(data) {
|
|
||||||
const outData = [];
|
|
||||||
outData.push(0xc0);
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
if (data[i] === 0xdb) {
|
|
||||||
outData.push(0xdb, 0xdd);
|
|
||||||
} else if (data[i] === 0xc0) {
|
|
||||||
outData.push(0xdb, 0xdc);
|
|
||||||
} else {
|
|
||||||
outData.push(data[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outData.push(0xc0);
|
|
||||||
return new Uint8Array(outData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write binary data to device using the WebSerial device writable stream.
|
|
||||||
* @param {Uint8Array} data 8 bit unsigned data array to write to device.
|
|
||||||
*/
|
|
||||||
async write(data) {
|
|
||||||
const outData = this.slipWriter(data);
|
|
||||||
if (this.tracing) {
|
|
||||||
console.log("Write bytes");
|
|
||||||
this.trace(`Write ${outData.length} bytes: ${this.hexConvert(outData)}`);
|
|
||||||
}
|
|
||||||
await this.sendBuffer(outData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append a buffer array after another buffer array
|
|
||||||
* @param {Uint8Array} arr1 - First array buffer.
|
|
||||||
* @param {Uint8Array} arr2 - magic hex number to select ROM.
|
|
||||||
* @returns {Uint8Array} Return a 8 bit unsigned array.
|
|
||||||
*/
|
|
||||||
appendArray(arr1, arr2) {
|
|
||||||
const combined = new Uint8Array(arr1.length + arr2.length);
|
|
||||||
combined.set(arr1);
|
|
||||||
combined.set(arr2, arr1.length);
|
|
||||||
return combined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asynchronous generator to yield incoming data chunks
|
|
||||||
async *readLoop(timeout) {
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
// Await the race between the timeout and the reader read
|
|
||||||
const result = await this.readOrigin(timeout);
|
|
||||||
|
|
||||||
// If a timeout occurs, result will be null; otherwise, it will have { value, done }
|
|
||||||
if (result === null) break;
|
|
||||||
|
|
||||||
const { value, done } = result;
|
|
||||||
|
|
||||||
if (done || !value) break;
|
|
||||||
|
|
||||||
yield value; // Yield each data chunk
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading from serial port:", error);
|
|
||||||
} finally {
|
|
||||||
this.buffer = new Uint8Array(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read a specific number of bytes
|
|
||||||
async newRead(numBytes, timeout) {
|
|
||||||
if (this.buffer.length >= numBytes) {
|
|
||||||
const output = this.buffer.slice(0, numBytes);
|
|
||||||
this.buffer = this.buffer.slice(numBytes); // Remove the returned data from buffer
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
while (this.buffer.length < numBytes) {
|
|
||||||
const readLoop = this.readLoop(timeout);
|
|
||||||
const { value, done } = await readLoop.next();
|
|
||||||
|
|
||||||
if (done || !value) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the newly read data to the buffer
|
|
||||||
this.buffer = this.appendArray(this.buffer, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return as much data as possible
|
|
||||||
const output = this.buffer.slice(0, numBytes);
|
|
||||||
this.buffer = this.buffer.slice(numBytes);
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
async flushInput() {}
|
|
||||||
|
|
||||||
async flushOutput() {
|
|
||||||
this.buffer = new Uint8Array(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// `inWaiting` returns the count of bytes in the buffer
|
|
||||||
inWaiting() {
|
|
||||||
return this.buffer.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect if the data read from device is a Fatal or Guru meditation error.
|
|
||||||
* @param {Uint8Array} input Data read from device
|
|
||||||
*/
|
|
||||||
detectPanicHandler(input) {
|
|
||||||
const guruMeditationRegex = /G?uru Meditation Error: (?:Core \d panic'ed \(([a-zA-Z ]*)\))?/;
|
|
||||||
const fatalExceptionRegex = /F?atal exception \(\d+\): (?:([a-zA-Z ]*)?.*epc)?/;
|
|
||||||
|
|
||||||
const inputString = new TextDecoder("utf-8").decode(input);
|
|
||||||
const match = inputString.match(guruMeditationRegex) || inputString.match(fatalExceptionRegex);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const cause = match[1] || match[2];
|
|
||||||
const msg = `Guru Meditation Error detected${cause ? ` (${cause})` : ""}`;
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SLIP_END = 0xc0;
|
|
||||||
SLIP_ESC = 0xdb;
|
|
||||||
SLIP_ESC_END = 0xdc;
|
|
||||||
SLIP_ESC_ESC = 0xdd;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take a data array and return the first well formed packet after
|
|
||||||
* replacing the escape sequence. Reads at least 8 bytes.
|
|
||||||
* @param {number} timeout Timeout read data.
|
|
||||||
* @yields {Uint8Array} Formatted packet using SLIP escape sequences.
|
|
||||||
*/
|
|
||||||
async *read(timeout) {
|
|
||||||
let partialPacket = null;
|
|
||||||
let isEscaping = false;
|
|
||||||
let successfulSlip = false;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const waitingBytes = this.inWaiting();
|
|
||||||
const readBytes = await this.newRead(waitingBytes > 0 ? waitingBytes : 1, timeout);
|
|
||||||
|
|
||||||
if (!readBytes || readBytes.length === 0) {
|
|
||||||
const msg =
|
|
||||||
partialPacket === null
|
|
||||||
? successfulSlip
|
|
||||||
? "Serial data stream stopped: Possible serial noise or corruption."
|
|
||||||
: "No serial data received."
|
|
||||||
: `Packet content transfer stopped`;
|
|
||||||
this.trace(msg);
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.trace(`Read ${readBytes.length} bytes: ${this.hexConvert(readBytes)}`);
|
|
||||||
|
|
||||||
let i = 0; // Track position in readBytes
|
|
||||||
while (i < readBytes.length) {
|
|
||||||
const byte = readBytes[i++];
|
|
||||||
if (partialPacket === null) {
|
|
||||||
if (byte === this.SLIP_END) {
|
|
||||||
partialPacket = new Uint8Array(0); // Start of a new packet
|
|
||||||
} else {
|
|
||||||
this.trace(`Read invalid data: ${this.hexConvert(readBytes)}`);
|
|
||||||
const remainingData = await this.newRead(this.inWaiting(), timeout);
|
|
||||||
this.trace(`Remaining data in serial buffer: ${this.hexConvert(remainingData)}`);
|
|
||||||
this.detectPanicHandler(new Uint8Array([...readBytes, ...(remainingData || [])]));
|
|
||||||
throw new Error(`Invalid head of packet (0x${byte.toString(16)}): Possible serial noise or corruption.`);
|
|
||||||
}
|
|
||||||
} else if (isEscaping) {
|
|
||||||
isEscaping = false;
|
|
||||||
if (byte === this.SLIP_ESC_END) {
|
|
||||||
partialPacket = this.appendArray(partialPacket, new Uint8Array([this.SLIP_END]));
|
|
||||||
} else if (byte === this.SLIP_ESC_ESC) {
|
|
||||||
partialPacket = this.appendArray(partialPacket, new Uint8Array([this.SLIP_ESC]));
|
|
||||||
} else {
|
|
||||||
this.trace(`Read invalid data: ${this.hexConvert(readBytes)}`);
|
|
||||||
const remainingData = await this.newRead(this.inWaiting(), timeout);
|
|
||||||
this.trace(`Remaining data in serial buffer: ${this.hexConvert(remainingData)}`);
|
|
||||||
this.detectPanicHandler(new Uint8Array([...readBytes, ...(remainingData || [])]));
|
|
||||||
throw new Error(`Invalid SLIP escape (0xdb, 0x${byte.toString(16)})`);
|
|
||||||
}
|
|
||||||
} else if (byte === this.SLIP_ESC) {
|
|
||||||
isEscaping = true;
|
|
||||||
} else if (byte === this.SLIP_END) {
|
|
||||||
this.trace(`Received full packet: ${this.hexConvert(partialPacket)}`);
|
|
||||||
this.buffer = this.appendArray(this.buffer, readBytes.slice(i));
|
|
||||||
yield partialPacket;
|
|
||||||
partialPacket = null;
|
|
||||||
successfulSlip = true;
|
|
||||||
} else {
|
|
||||||
partialPacket = this.appendArray(partialPacket, new Uint8Array([byte]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_DTR_state = false;
|
|
||||||
/**
|
|
||||||
* Send the RequestToSend (RTS) signal to given state
|
|
||||||
* # True for EN=LOW, chip in reset and False EN=HIGH, chip out of reset
|
|
||||||
* @param {boolean} state Boolean state to set the signal
|
|
||||||
*/
|
|
||||||
async setRTS(state) {
|
|
||||||
// # Work-around for adapters on Windows using the usbser.sys driver:
|
|
||||||
// # generate a dummy change to DTR so that the set-control-line-state
|
|
||||||
// # request is sent with the updated RTS state and the same DTR state
|
|
||||||
// Referenced to esptool.py
|
|
||||||
await this.setDTR(this._DTR_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the dataTerminalReady (DTS) signal to given state
|
|
||||||
* # True for IO0=LOW, chip in reset and False IO0=HIGH
|
|
||||||
* @param {boolean} state Boolean state to set the signal
|
|
||||||
*/
|
|
||||||
async setDTR(state) {
|
|
||||||
this._DTR_state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to serial device using the Webserial open method.
|
|
||||||
* @param {number} baud Number baud rate for serial connection. Default is 115200.
|
|
||||||
* @param {typeof import("w3c-web-serial").SerialOptions} serialOptions Serial Options for WebUSB SerialPort class.
|
|
||||||
*/
|
|
||||||
async connect(baud = 115200, serialOptions = {}) {
|
|
||||||
// await this.device.open({
|
|
||||||
// baudRate: baud,
|
|
||||||
// dataBits: serialOptions?.dataBits,
|
|
||||||
// stopBits: serialOptions?.stopBits,
|
|
||||||
// bufferSize: serialOptions?.bufferSize,
|
|
||||||
// parity: serialOptions?.parity,
|
|
||||||
// flowControl: serialOptions?.flowControl,
|
|
||||||
// });
|
|
||||||
await this.device.open();
|
|
||||||
this.finished = false;
|
|
||||||
this.#addEventsListener_();
|
|
||||||
this.baudrate = baud;
|
|
||||||
}
|
|
||||||
|
|
||||||
async sleep(ms) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for a given timeout ms for serial device unlock.
|
|
||||||
* @param {number} timeout Timeout time in milliseconds (ms) to sleep
|
|
||||||
*/
|
|
||||||
async waitForUnlock(timeout) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnect from serial device by running SerialPort.close() after streams unlock.
|
|
||||||
*/
|
|
||||||
async disconnect() {
|
|
||||||
await this.waitForUnlock(400);
|
|
||||||
this.finished = true;
|
|
||||||
this.device.oninputreport = null;
|
|
||||||
this.device.onclose = null;
|
|
||||||
await this.device.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Web.HIDTransport = Transport;
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -113,6 +113,7 @@ class WebHID extends Serial {
|
|||||||
#dataLength_ = 31;
|
#dataLength_ = 31;
|
||||||
constructor(port) {
|
constructor(port) {
|
||||||
super(port);
|
super(port);
|
||||||
|
this.#device_ = WebHID.getPort(port);
|
||||||
}
|
}
|
||||||
|
|
||||||
#addEventsListener_() {
|
#addEventsListener_() {
|
||||||
@@ -212,6 +213,14 @@ class WebHID extends Serial {
|
|||||||
return this.setDTRAndRTS(this.getDTR(), rts);
|
return this.setDTRAndRTS(this.getDTR(), rts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVID() {
|
||||||
|
return this.#device_.vendorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPID() {
|
||||||
|
return this.#device_.productId;
|
||||||
|
}
|
||||||
|
|
||||||
onBuffer(buffer) {
|
onBuffer(buffer) {
|
||||||
super.onBuffer(buffer);
|
super.onBuffer(buffer);
|
||||||
for (let i = 0; i < buffer.length; i++) {
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
|||||||
221
common/modules/mixly-modules/web/serial-transport.js
Normal file
221
common/modules/mixly-modules/web/serial-transport.js
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
goog.loadJs('web', () => {
|
||||||
|
|
||||||
|
goog.require('Mixly.Web');
|
||||||
|
goog.provide('Mixly.Web.SerialTransport');
|
||||||
|
|
||||||
|
const { Web } = Mixly;
|
||||||
|
|
||||||
|
|
||||||
|
class Transport {
|
||||||
|
slipReaderEnabled = false;
|
||||||
|
baudrate = 0;
|
||||||
|
traceLog = "";
|
||||||
|
lastTraceTime = Date.now();
|
||||||
|
buffer = new Uint8Array(0);
|
||||||
|
serial = null;
|
||||||
|
tracing = false;
|
||||||
|
|
||||||
|
constructor(serial, tracing = false, enableSlipReader = true) {
|
||||||
|
this.serial = serial;
|
||||||
|
this.tracing = tracing;
|
||||||
|
this.slipReaderEnabled = enableSlipReader;
|
||||||
|
|
||||||
|
this.serial.bind('onBuffer', (data) => {
|
||||||
|
if (!(data instanceof Uint8Array)) data = new Uint8Array(data);
|
||||||
|
this.buffer = this.appendArray(this.buffer, data);
|
||||||
|
if (this.tracing)
|
||||||
|
this.trace(`Received ${data.length} bytes: ${this.hexConvert(data)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfo() {
|
||||||
|
const VID = this.serial.getVID();
|
||||||
|
const PID = this.serial.getPID();
|
||||||
|
return `WebDevice VendorID 0x${VID.toString(16)} ProductID 0x${PID.toString(16)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPid() {
|
||||||
|
return this.serial.getPID();
|
||||||
|
}
|
||||||
|
|
||||||
|
trace(message) {
|
||||||
|
const delta = Date.now() - this.lastTraceTime;
|
||||||
|
const prefix = `TRACE +${delta}ms`;
|
||||||
|
const traceMessage = `${prefix} ${message}`;
|
||||||
|
this.lastTraceTime = Date.now();
|
||||||
|
console.log(traceMessage);
|
||||||
|
this.traceLog += traceMessage + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
async returnTrace() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(this.traceLog);
|
||||||
|
console.log("Trace log copied to clipboard!");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy trace log:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hexify(s) {
|
||||||
|
return Array.from(s)
|
||||||
|
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||||
|
.join(" ")
|
||||||
|
.padEnd(16, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
hexConvert(uint8Array, autoSplit = true) {
|
||||||
|
if (autoSplit && uint8Array.length > 16) {
|
||||||
|
let result = "";
|
||||||
|
let s = uint8Array;
|
||||||
|
while (s.length > 0) {
|
||||||
|
const line = s.slice(0, 16);
|
||||||
|
const asciiLine = String.fromCharCode(...line)
|
||||||
|
.split("")
|
||||||
|
.map((c) =>
|
||||||
|
c === " " || (c >= " " && c <= "~" && c !== " ") ? c : "."
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
s = s.slice(16);
|
||||||
|
result += `\n ${this.hexify(line.slice(0, 8))} ${this.hexify(
|
||||||
|
line.slice(8)
|
||||||
|
)} | ${asciiLine}`;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return this.hexify(uint8Array);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendArray(arr1, arr2) {
|
||||||
|
const combined = new Uint8Array(arr1.length + arr2.length);
|
||||||
|
combined.set(arr1);
|
||||||
|
combined.set(arr2, arr1.length);
|
||||||
|
return combined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(baud = 115200) {
|
||||||
|
await this.serial.open(baud);
|
||||||
|
this.baudrate = baud;
|
||||||
|
if (this.tracing) this.trace(`Serial opened at ${baud} baud`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
await this.serial.close();
|
||||||
|
if (this.tracing) this.trace("Serial connection closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async setRTS(state) {
|
||||||
|
await this.serial.setRTS(state);
|
||||||
|
if (this.tracing) this.trace(`Set RTS = ${state}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDTR(state) {
|
||||||
|
await this.serial.setDTR(state);
|
||||||
|
if (this.tracing) this.trace(`Set DTR = ${state}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
inWaiting() {
|
||||||
|
return this.buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async flushInput() {
|
||||||
|
this.buffer = new Uint8Array(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
slipWriter(data) {
|
||||||
|
const outData = [];
|
||||||
|
outData.push(0xc0);
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
if (data[i] === 0xdb) outData.push(0xdb, 0xdd);
|
||||||
|
else if (data[i] === 0xc0) outData.push(0xdb, 0xdc);
|
||||||
|
else outData.push(data[i]);
|
||||||
|
}
|
||||||
|
outData.push(0xc0);
|
||||||
|
return new Uint8Array(outData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async write(data) {
|
||||||
|
const outData = this.slipWriter(data);
|
||||||
|
if (this.tracing) {
|
||||||
|
this.trace(`Write ${outData.length} bytes: ${this.hexConvert(outData)}`);
|
||||||
|
}
|
||||||
|
await this.serial.sendBuffer(outData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async newRead(numBytes, timeout) {
|
||||||
|
const start = Date.now();
|
||||||
|
while (this.buffer.length < numBytes && Date.now() - start < timeout) {
|
||||||
|
if (!this.serial.isOpened()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await this.sleep(10);
|
||||||
|
}
|
||||||
|
const out = this.buffer.slice(0, numBytes);
|
||||||
|
this.buffer = this.buffer.slice(numBytes);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async *read(timeout = 1000) {
|
||||||
|
let partialPacket = null;
|
||||||
|
let isEscaping = false;
|
||||||
|
const SLIP_END = 0xc0;
|
||||||
|
const SLIP_ESC = 0xdb;
|
||||||
|
const SLIP_ESC_END = 0xdc;
|
||||||
|
const SLIP_ESC_ESC = 0xdd;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const data = await this.newRead(1, timeout);
|
||||||
|
if (!data || data.length === 0) break;
|
||||||
|
|
||||||
|
const byte = data[0];
|
||||||
|
if (partialPacket === null) {
|
||||||
|
if (byte === SLIP_END) partialPacket = new Uint8Array(0);
|
||||||
|
} else if (isEscaping) {
|
||||||
|
isEscaping = false;
|
||||||
|
if (byte === SLIP_ESC_END)
|
||||||
|
partialPacket = this.appendArray(partialPacket, new Uint8Array([SLIP_END]));
|
||||||
|
else if (byte === SLIP_ESC_ESC)
|
||||||
|
partialPacket = this.appendArray(partialPacket, new Uint8Array([SLIP_ESC]));
|
||||||
|
else throw new Error(`Invalid SLIP escape: 0x${byte.toString(16)}`);
|
||||||
|
} else if (byte === SLIP_ESC) {
|
||||||
|
isEscaping = true;
|
||||||
|
} else if (byte === SLIP_END) {
|
||||||
|
if (partialPacket.length > 0) {
|
||||||
|
if (this.tracing)
|
||||||
|
this.trace(`Yield packet: ${this.hexConvert(partialPacket)}`);
|
||||||
|
yield partialPacket;
|
||||||
|
partialPacket = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
partialPacket = this.appendArray(partialPacket, new Uint8Array([byte]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detectPanicHandler(input) {
|
||||||
|
const guruMeditationRegex =
|
||||||
|
/G?uru Meditation Error: (?:Core \d panic'ed \(([a-zA-Z ]*)\))?/;
|
||||||
|
const fatalExceptionRegex =
|
||||||
|
/F?atal exception \(\d+\): (?:([a-zA-Z ]*)?.*epc)?/;
|
||||||
|
|
||||||
|
const inputString = new TextDecoder("utf-8").decode(input);
|
||||||
|
const match =
|
||||||
|
inputString.match(guruMeditationRegex) ||
|
||||||
|
inputString.match(fatalExceptionRegex);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const cause = match[1] || match[2];
|
||||||
|
const msg = `Guru Meditation Error detected${cause ? ` (${cause})` : ""}`;
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Web.SerialTransport = Transport;
|
||||||
|
|
||||||
|
});
|
||||||
@@ -108,6 +108,7 @@ class WebSerialPort extends Serial {
|
|||||||
#stringTemp_ = '';
|
#stringTemp_ = '';
|
||||||
constructor(port) {
|
constructor(port) {
|
||||||
super(port);
|
super(port);
|
||||||
|
this.#serialport_ = WebSerialPort.getPort(port);
|
||||||
}
|
}
|
||||||
|
|
||||||
#addEventsListener_() {
|
#addEventsListener_() {
|
||||||
@@ -230,6 +231,16 @@ class WebSerialPort extends Serial {
|
|||||||
return this.setDTRAndRTS(this.getDTR(), rts);
|
return this.setDTRAndRTS(this.getDTR(), rts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVID() {
|
||||||
|
const info = this.#serialport_.getInfo();
|
||||||
|
return info.usbVendorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPID() {
|
||||||
|
const info = this.#serialport_.getInfo();
|
||||||
|
return info.usbProductId;
|
||||||
|
}
|
||||||
|
|
||||||
onBuffer(buffer) {
|
onBuffer(buffer) {
|
||||||
super.onBuffer(buffer);
|
super.onBuffer(buffer);
|
||||||
for (let i = 0; i < buffer.length; i++) {
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ class USBMini extends Serial {
|
|||||||
#dataLength_ = 64;
|
#dataLength_ = 64;
|
||||||
constructor(port) {
|
constructor(port) {
|
||||||
super(port);
|
super(port);
|
||||||
|
this.#device_ = USBMini.getPort(port);
|
||||||
}
|
}
|
||||||
|
|
||||||
#addEventsListener_() {
|
#addEventsListener_() {
|
||||||
@@ -315,6 +316,14 @@ class USBMini extends Serial {
|
|||||||
return this.setDTRAndRTS(this.getDTR(), rts);
|
return this.setDTRAndRTS(this.getDTR(), rts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVID() {
|
||||||
|
return this.#device_.vendorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPID() {
|
||||||
|
return this.#device_.productId;
|
||||||
|
}
|
||||||
|
|
||||||
onBuffer(buffer) {
|
onBuffer(buffer) {
|
||||||
super.onBuffer(buffer);
|
super.onBuffer(buffer);
|
||||||
for (let i = 0; i < buffer.length; i++) {
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ class USB extends Serial {
|
|||||||
#stringTemp_ = '';
|
#stringTemp_ = '';
|
||||||
constructor(port) {
|
constructor(port) {
|
||||||
super(port);
|
super(port);
|
||||||
|
this.#device_ = USB.getPort(port);
|
||||||
}
|
}
|
||||||
|
|
||||||
#addEventsListener_() {
|
#addEventsListener_() {
|
||||||
@@ -223,6 +224,14 @@ class USB extends Serial {
|
|||||||
return this.setDTRAndRTS(this.getDTR(), rts);
|
return this.setDTRAndRTS(this.getDTR(), rts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVID() {
|
||||||
|
return this.#device_.vendorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPID() {
|
||||||
|
return this.#device_.productId;
|
||||||
|
}
|
||||||
|
|
||||||
onBuffer(buffer) {
|
onBuffer(buffer) {
|
||||||
super.onBuffer(buffer);
|
super.onBuffer(buffer);
|
||||||
for (let i = 0; i < buffer.length; i++) {
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
|||||||
Reference in New Issue
Block a user