feat: sync mixly root files and common folder

This commit is contained in:
yczpf2019
2026-01-24 16:12:04 +08:00
parent 93e17c00ae
commit c8c5fcf726
2920 changed files with 186461 additions and 0 deletions

View File

@@ -0,0 +1,292 @@
goog.loadJs('web', () => {
goog.require('path');
goog.require('Mixly.FS');
goog.require('Mixly.Debug');
goog.require('Mixly.Web.Serial');
goog.require('Mixly.Web.Ampy');
goog.provide('Mixly.Web.AmpyFS');
const { FS, Debug, Web } = Mixly;
const { Serial, Ampy } = Web;
class AmpyFS extends FS {
#ampy_ = null;
#port_ = '';
#baud_ = 115200;
constructor() {
super();
this.#ampy_ = Ampy;
}
async getAmpy() {
const { mainStatusBarTabs } = Mixly;
const statusBarSerial = mainStatusBarTabs.getStatusBarById(this.#port_);
if (statusBarSerial) {
await statusBarSerial.close();
}
const serial = new Serial(this.#port_);
const ampy = new Ampy(serial);
return ampy;
}
async rename(oldPath, newPath) {
let stdout = '', error = null, ampy = null;
try {
ampy = await this.getAmpy();
await ampy.enter();
stdout = await ampy.rename(oldPath, newPath);
} catch (e) {
error = e;
Debug.error(error);
}
try {
await ampy.exit();
await ampy.dispose();
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async createFile(filePath) {
let stdout = '', error = null, ampy = null;
try {
ampy = await this.getAmpy();
await ampy.enter();
stdout = await ampy.mkfile(filePath);
} catch (e) {
error = e;
Debug.error(error);
}
try {
await ampy.exit();
await ampy.dispose();
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async readFile(filePath, encoding = 'utf8') {
let stdout = '', error = null, ampy = null;
try {
ampy = await this.getAmpy();
await ampy.enter();
stdout = await ampy.get(filePath, encoding);
} catch (e) {
error = e;
Debug.error(error);
}
try {
await ampy.exit();
await ampy.dispose();
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async writeFile(filePath, data) {
let stdout = '', error = null, ampy = null;
try {
ampy = await this.getAmpy();
await ampy.enter();
stdout = await ampy.put(filePath, data);
} catch (e) {
error = e;
Debug.error(error);
}
try {
await ampy.exit();
await ampy.dispose();
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async isFile(filePath) {
let error = null;
if (path.extname(filePath)) {
return [error, true];
} else {
return [error, false];
}
}
async renameFile(oldFilePath, newFilePath) {
return this.rename(oldFilePath, newFilePath);
}
async moveFile(oldFilePath, newFilePath) {
return this.rename(oldFilePath, newFilePath);
}
async copyFile(oldFilePath, newFilePath) {
let stdout = '', error = null, ampy = null;
try {
ampy = await this.getAmpy();
await ampy.enter();
stdout = await ampy.cpfile(oldFilePath, newFilePath);
} catch (e) {
error = e;
Debug.error(error);
}
try {
await ampy.exit();
await ampy.dispose();
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async deleteFile(filePath) {
let stdout = '', error = null, ampy = null;
try {
ampy = await this.getAmpy();
await ampy.enter();
stdout = await ampy.rm(filePath);
} catch (e) {
error = e;
Debug.error(error);
}
try {
await ampy.exit();
await ampy.dispose();
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async createDirectory(folderPath) {
let stdout = '', error = null, ampy = null;
try {
ampy = await this.getAmpy();
await ampy.enter();
stdout = await ampy.mkdir(folderPath);
} catch (e) {
error = e;
Debug.error(error);
}
try {
await ampy.exit();
await ampy.dispose();
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async readDirectory(folderPath) {
let stdout = [], error = null, ampy = null;
try {
ampy = await this.getAmpy();
await ampy.enter();
stdout = await ampy.ls(folderPath, false, false);
} catch (e) {
error = e;
Debug.error(error);
}
try {
await ampy.exit();
await ampy.dispose();
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async isDirectory(folderPath) {
let error = null;
if (path.extname(folderPath)) {
return [error, false];
} else {
return [error, true];
}
}
async isDirectoryEmpty(folderPath) {
return [null, false];
}
async renameDirectory(oldFolderPath, newFolderPath) {
return this.rename(oldFolderPath, newFolderPath);
}
async moveDirectory(oldFolderPath, newFolderPath) {
return this.rename(oldFolderPath, newFolderPath);
}
async copyDirectory(oldFolderPath, newFolderPath) {
let stdout = '', error = null, ampy = null;
try {
ampy = await this.getAmpy();
await ampy.enter();
stdout = await ampy.cpdir(oldFolderPath, newFolderPath);
} catch (e) {
error = e;
Debug.error(error);
}
try {
await ampy.exit();
await ampy.dispose();
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
async deleteDirectory(folderPath) {
let stdout = '', error = null, ampy = null;
try {
ampy = await this.getAmpy();
await ampy.enter();
stdout = await ampy.rmdir(folderPath);
} catch (e) {
error = e;
Debug.error(error);
}
try {
await ampy.exit();
await ampy.dispose();
} catch (e) {
error = e;
Debug.error(error);
}
return [error, stdout];
}
setPortName(port) {
this.#port_ = port;
}
getPortName() {
return this.#port_;
}
setBaudRate(baud) {
this.#baud_ = baud;
}
getBaudRate() {
return this.#baud_;
}
}
Web.AmpyFS = AmpyFS;
});

View File

@@ -0,0 +1,429 @@
goog.loadJs('web', () => {
goog.require('path');
goog.require('Mustache');
goog.require('Mixly.Env');
goog.require('Mixly.Events');
goog.require('Mixly.Msg');
goog.require('Mixly.Ampy');
goog.require('Mixly.Web');
goog.provide('Mixly.Web.Ampy');
const {
Env,
Events,
Msg,
Ampy,
Web
} = Mixly;
class AmpyExt extends Ampy {
static {
this.LS = goog.readFileSync(path.join(Env.templatePath, 'python/ls.py'));
this.LS_RECURSIVE = goog.readFileSync(path.join(Env.templatePath, 'python/ls-recursive.py'));
this.LS_LONG_FORMAT = goog.readFileSync(path.join(Env.templatePath, 'python/ls-long-format.py'));
this.MKDIR = goog.readFileSync(path.join(Env.templatePath, 'python/mkdir.py'));
this.MKFILE = goog.readFileSync(path.join(Env.templatePath, 'python/mkfile.py'));
this.RENAME = goog.readFileSync(path.join(Env.templatePath, 'python/rename.py'));
this.RM = goog.readFileSync(path.join(Env.templatePath, 'python/rm.py'));
this.RMDIR = goog.readFileSync(path.join(Env.templatePath, 'python/rmdir.py'));
this.GET = goog.readFileSync(path.join(Env.templatePath, 'python/get.py'));
this.CWD = goog.readFileSync(path.join(Env.templatePath, 'python/cwd.py'));
this.CPDIR = goog.readFileSync(path.join(Env.templatePath, 'python/cpdir.py'));
this.CPFILE = goog.readFileSync(path.join(Env.templatePath, 'python/cpfile.py'));
}
#device_ = null;
#receiveTemp_ = [];
#writeBuffer_ = true;
#active_ = false;
#dataLength_ = 256;
#events_ = new Events(['message', 'replaceMessage'])
constructor(device, writeBuffer = true, dataLength = 256) {
super();
this.#device_ = device;
this.#writeBuffer_ = writeBuffer;
this.#dataLength_ = dataLength;
this.#addEventsListener_();
}
#addEventsListener_() {
this.#device_.bind('onChar', (char) => {
if (['\r', '\n'].includes(char)) {
this.#receiveTemp_.push('');
} else {
let line = this.#receiveTemp_.pop() ?? '';
this.#receiveTemp_.push(line + char);
}
});
}
bind(...args) {
return this.#events_.bind(...args);
}
message(message) {
this.#events_.run('message', message);
}
replaceMessage(lineNumber, message) {
this.#events_.run('replaceMessage', lineNumber, message);
}
getProgressMessage(name, percent) {
const sended = parseInt(percent * 45);
const left = percent === 0 ? '' : Array(sended).fill('=').join('');
const right = percent === 100 ? '' : Array(45 - sended).fill('-').join('');
return `${name} → |${left}${right}| ${(percent * 100).toFixed(1)}%`;
}
isActive() {
return this.#active_;
}
async readUntil(ending, withEnding = true, timeout = 5000) {
const startTime = Number(new Date());
let nowTime = startTime;
let readStr = '';
while (nowTime - startTime < timeout) {
const nowTime = Number(new Date());
let len = this.#receiveTemp_.length;
for (let i = 0; i < len; i++) {
const data = this.#receiveTemp_.shift();
let index = data.toLowerCase().indexOf(ending);
if (index !== -1) {
if (withEnding) {
index += ending.length;
}
this.#receiveTemp_.unshift(data.substring(index));
readStr += data.substring(0, index);
return readStr;
} else {
readStr += data;
if (i !== len - 1) {
readStr += '\n';
}
}
}
if (nowTime - startTime >= timeout) {
return '';
}
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.dataReadInterrupt']);
}
await this.#device_.sleep(100);
}
}
async interrupt(timeout = 1000) {
for (let i = 0; i < 5; i++) {
// 中断两次
await this.#device_.sendBuffer([0x0D, 0x03]);
await this.#device_.sleep(100);
await this.#device_.sendBuffer([0x03]);
await this.#device_.sleep(100);
if (await this.readUntil('>>>', true, timeout)) {
return true;
}
}
return false;
}
async enterRawREPL(timeout = 1000) {
for (let i = 0; i < 5; i++) {
await this.#device_.sendBuffer([0x01]);
await this.#device_.sleep(100);
if (await this.readUntil('raw repl; ctrl-b to exit', true, timeout)) {
return true;
}
}
return false;
}
async exitRawREPL(timeout = 5000) {
await this.#device_.sendBuffer([0x02]);
await this.#device_.sleep(100);
let succeed = false;
if (await this.readUntil('>>>', true, timeout)) {
succeed = true;
}
return succeed;
}
async exitREPL(timeout = 5000) {
await this.#device_.sendBuffer([0x04]);
await this.#device_.sleep(100);
let succeed = false;
if (await this.readUntil('soft reboot', false, timeout)) {
succeed = true;
}
return succeed;
}
async exec(str, timeout = 5000) {
if (this.#writeBuffer_) {
const buffer = this.#device_.encode(str);
const len = Math.ceil(buffer.length / this.#dataLength_);
for (let i = 0; i < len; i++) {
const start = i * this.#dataLength_;
const end = Math.min((i + 1) * this.#dataLength_, buffer.length);
const writeBuffer = buffer.slice(start, end);
await this.#device_.sendBuffer(writeBuffer);
await this.#device_.sleep(10);
}
} else {
for (let i = 0; i < str.length / this.#dataLength_; i++) {
const start = i * this.#dataLength_;
const end = Math.min((i + 1) * this.#dataLength_, str.length);
let data = str.substring(start, end);
await this.#device_.sendString(data);
await this.#device_.sleep(10);
}
}
await this.#device_.sendBuffer([0x04]);
return await this.follow(timeout);
}
async follow(timeout = 1000) {
let data = await this.readUntil('\x04', true, timeout);
if (data.length < 1) {
throw new Error(Msg.Lang['ampy.waitingFirstEOFTimeout']);
}
let start = data.toLowerCase().lastIndexOf('ok');
if (start === -1) {
start = 0;
} else {
start += 2;
}
data = data.substring(start, data.length - 1);
let dataError = await this.readUntil('\x04', true, timeout);
if (dataError.length < 1) {
throw new Error(Msg.Lang['ampy.secondEOFTimeout']);
}
dataError = dataError.substring(0, dataError.length - 1);
return { data, dataError };
}
async enter() {
if (this.isActive()) {
return;
}
this.#active_ = true;
await this.#device_.open(115200);
await this.#device_.sleep(500);
await this.#device_.sendBuffer([0x02]);
if (!await this.interrupt()) {
throw new Error(Msg.Lang['ampy.interruptFailed']);
}
if (!await this.enterRawREPL()) {
throw new Error(Msg.Lang['ampy.enterRawREPLFailed']);
}
}
async exit() {
if (!this.isActive()) {
return;
}
if (!await this.exitRawREPL()) {
throw new Error(Msg.Lang['ampy.exitRawREPLFailed']);
}
/*if (!await this.exitREPL()) {
throw new Error(Msg.Lang['ampy.exitREPLFailed']);
}*/
await this.#device_.close();
this.#active_ = false;
}
async get(filename, encoding = 'utf8', timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.GET, {
path: filename
});
const { data, dataError } = await this.exec(code, timeout);
if (dataError) {
return '';
}
if (encoding === 'utf8') {
return this.#device_.decode(this.unhexlify(data));
} else {
return this.unhexlify(data);
}
}
async put(filename, data, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
this.message(`Writing ${filename}...\n`);
this.message(this.getProgressMessage('', 0));
await this.exec(`file = open('${filename}', 'wb')`, timeout);
let buffer = null;
if (data.constructor === String) {
buffer = this.#device_.encode(data);
} else if (data.constructor === ArrayBuffer) {
buffer = new Uint8Array(data);
} else {
buffer = data;
}
const len = Math.ceil(buffer.length / 64);
if (!len) {
this.replaceMessage(-1, this.getProgressMessage('', 1));
}
let sendedLength = 0;
for (let i = 0; i < len; i++) {
const writeBuffer = buffer.slice(i * 64, Math.min((i + 1) * 64, buffer.length));
sendedLength += writeBuffer.length;
const percent = sendedLength / buffer.length;
this.replaceMessage(-1, this.getProgressMessage('', percent));
let writeStr = '';
for (let num of writeBuffer) {
let numStr = num.toString(16);
if (numStr.length === 1) {
numStr = '0' + numStr;
}
writeStr += '\\x' + numStr;
}
await this.exec(`file.write(b'${writeStr}')`, timeout);
}
await this.exec('file.close()', timeout);
this.message('\n');
}
async ls(directory = '/', longFormat = true, recursive = false, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
let code = '';
if (longFormat) {
code = Mustache.render(AmpyExt.LS_LONG_FORMAT, {
path: directory
});
} else if (recursive) {
code = Mustache.render(AmpyExt.LS_RECURSIVE, {
path: directory
});
} else {
code = Mustache.render(AmpyExt.LS, {
path: directory
});
}
const { data, dataError } = await this.exec(code, timeout);
if (dataError) {
return [];
}
return JSON.parse(data.replaceAll('\'', '\"'));
}
async mkdir(directory, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.MKDIR, {
path: directory
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async mkfile(file, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.MKFILE, {
path: file
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async rename(oldname, newname, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.RENAME, {
oldPath: oldname,
newPath: newname
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async rm(filename, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.RM, {
path: filename
});
await this.exec(code);
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async rmdir(directory, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.RMDIR, {
path: directory
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async cpdir(oldname, newname, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.CPDIR, {
oldPath: oldname,
newPath: newname
});
const { data, dataError } = await this.exec(code, timeout);
console.log(data, dataError)
return !dataError;
}
async cpfile(oldname, newname, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.CPFILE, {
oldPath: oldname,
newPath: newname
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async cwd(timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.CWD, {});
const { data, dataError } = await this.exec(code, timeout);
if (dataError) {
return '/';
}
return data;
}
getDevice() {
return this.#device_;
}
async dispose() {
this.#active_ = false;
await this.#device_.dispose();
this.#device_ = null;
this.#events_.reset();
this.#events_ = null;
}
}
Web.Ampy = AmpyExt;
});

View File

@@ -0,0 +1,185 @@
goog.loadJs('web', () => {
goog.require('Mixly.Web');
goog.provide('Mixly.Web.Bluetooth');
const { Web } = Mixly;
const { Bluetooth } = Web;
Bluetooth.output = [];
Bluetooth.mtu = 100;
Bluetooth.encoder = new TextEncoder('utf-8');
Bluetooth.decoder = new TextDecoder('utf-8');
Bluetooth.nordicUartServiceUuid = 0xfff0;
Bluetooth.uartRxCharacteristicUuid = 0xfff2;
Bluetooth.uartTxCharacteristicUuid = 0xfff1;
Bluetooth.obj = null;
Bluetooth.server = null;
Bluetooth.service = null;
Bluetooth.uartRxCharacteristic = null;
Bluetooth.uartTxCharacteristic = null;
Bluetooth.name = 'bluetooth';
Bluetooth.connect = (baud = 115200, onDataLine = (message) => {}) => {
return new Promise((resolve, reject) => {
if (Bluetooth.isConnected()) {
resolve();
return;
}
navigator.bluetooth.requestDevice({
// filters: [{name: ['Mixly']}]
optionalServices: [Bluetooth.nordicUartServiceUuid],
acceptAllDevices: true
})
.then((device) => {
Bluetooth.obj = device;
return device.gatt.connect();
})
.then((server) => {
Bluetooth.server = server;
return server.getPrimaryService(Bluetooth.nordicUartServiceUuid);
})
.then((service) => {
Bluetooth.service = service;
return service.getCharacteristic(Bluetooth.uartRxCharacteristicUuid);
})
.then((uartRxCharacteristic) => {
Bluetooth.uartRxCharacteristic = uartRxCharacteristic;
return Bluetooth.service.getCharacteristic(Bluetooth.uartTxCharacteristicUuid);
})
.then((uartTxCharacteristic) => {
Bluetooth.uartTxCharacteristic = uartTxCharacteristic;
return uartTxCharacteristic.startNotifications();
})
.then(() => {
Bluetooth.onDataLine = onDataLine;
Bluetooth.addReadEvent(onDataLine);
resolve();
})
.catch((error) => {
Bluetooth.obj = null;
Bluetooth.server = null;
Bluetooth.service = null;
Bluetooth.uartRxCharacteristic = null;
Bluetooth.uartTxCharacteristic = null;
reject(error);
});
});
}
Bluetooth.close = async () => {
if (Bluetooth.isConnected()) {
await Bluetooth.obj.gatt.disconnect();
Bluetooth.obj = null;
Bluetooth.server = null;
Bluetooth.service = null;
Bluetooth.uartRxCharacteristic = null;
Bluetooth.uartTxCharacteristic = null;
}
}
Bluetooth.isConnected = () => {
return Bluetooth.obj && Bluetooth.obj.gatt.connected;
}
Bluetooth.addReadEvent = (onDataLine = (message) => {}) => {
Bluetooth.uartTxCharacteristic.addEventListener('characteristicvaluechanged', event => {
let data = Bluetooth.decoder.decode(event.target.value);
let dataList = data.split('\n');
if (!dataList.length) {
return;
}
let endStr = '';
if (Bluetooth.output.length) {
endStr = Bluetooth.output.pop();
Bluetooth.output.push(endStr + dataList.shift());
if (dataList.length) {
// console.log(Bluetooth.output[Bluetooth.output.length - 1]);
onDataLine(Bluetooth.output[Bluetooth.output.length - 1]);
}
}
let i = 0;
for (let value of dataList) {
i++;
Bluetooth.output.push(value);
if (i < dataList.length) {
// console.log(value);
onDataLine(value);
}
}
while (Bluetooth.output.length > 500) {
Bluetooth.output.shift();
}
});
}
Bluetooth.AddOnConnectEvent = (onConnect) => {
}
Bluetooth.AddOnDisconnectEvent = (onDisconnect) => {
Bluetooth.obj.addEventListener('gattserverdisconnected', () => {
onDisconnect();
});
}
Bluetooth.writeString = async (str) => {
let buffer = Bluetooth.encoder.encode(str);
await Bluetooth.writeByteArr(buffer);
}
Bluetooth.writeByteArr = async (buffer) => {
buffer = new Uint8Array(buffer);
for (let chunk = 0; chunk < Math.ceil(buffer.length / Bluetooth.mtu); chunk++) {
let start = Bluetooth.mtu * chunk;
let end = Bluetooth.mtu * (chunk + 1);
await Bluetooth.uartRxCharacteristic.writeValueWithResponse(buffer.slice(start, end))
.catch(error => {
if (error == "NetworkError: GATT operation already in progress.") {
Bluetooth.writeByteArr(buffer);
}
else {
return Promise.reject(error);
}
});
}
await Bluetooth.sleep(200);
}
Bluetooth.writeCtrlA = async () => {
await Bluetooth.writeByteArr([1, 13, 10]);
}
Bluetooth.writeCtrlB = async () => {
await Bluetooth.writeByteArr([2, 13, 10]);
}
Bluetooth.writeCtrlC = async () => {
await Bluetooth.writeByteArr([3, 13, 10]);
}
Bluetooth.writeCtrlD = async () => {
await Bluetooth.writeByteArr([3, 4]);
}
Bluetooth.write = async (type, data, dataTail) => {
switch (type) {
case 'string':
return Bluetooth.writeString(data + dataTail);
break;
default:
await Bluetooth.writeByteArr(data);
return Bluetooth.writeString(dataTail);
}
}
Bluetooth.setSignals = async (dtr, rts) => {
}
Bluetooth.setBaudRate = async (baud) => {
}
Bluetooth.sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
}
});

View File

@@ -0,0 +1,738 @@
goog.loadJs('web', () => {
goog.require('path');
goog.require('BoardId');
goog.require('FSWrapper');
goog.require('DAPWrapper');
goog.require('PartialFlashing');
goog.require('esptooljs');
goog.require('CryptoJS');
goog.require('JSZip');
goog.require('Mixly.Env');
goog.require('Mixly.LayerExt');
goog.require('Mixly.Config');
goog.require('Mixly.MFile');
goog.require('Mixly.Boards');
goog.require('Mixly.Msg');
goog.require('Mixly.Workspace');
goog.require('Mixly.Debug');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.MString');
goog.require('Mixly.LayerFirmware');
goog.require('Mixly.LayerProgress');
goog.require('Mixly.Web.Serial');
goog.require('Mixly.Web.Ampy');
goog.require('Mixly.Web.KFlash');
goog.require('Mixly.Web.SerialTransport');
goog.provide('Mixly.Web.BU');
const {
Env,
Web,
LayerExt,
Config,
MFile,
Boards,
Msg,
Workspace,
Debug,
HTMLTemplate,
MString,
LayerFirmware,
LayerProgress
} = Mixly;
const {
Serial,
BU,
Ampy,
KFlash,
SerialTransport
} = Web;
const { BOARD, SELECTED_BOARD } = Config;
const { ESPLoader } = esptooljs;
BU.uploading = false;
BU.burning = false;
BU.firmwareLayer = new LayerFirmware({
width: 400,
title: Msg.Lang['nav.btn.burn'],
cancelValue: false,
cancel: false,
cancelDisplay: false
});
BU.firmwareLayer.bind('burn', (info) => {
const { web } = SELECTED_BOARD;
BU.burnWithEsptool(info, web.burn.erase);
});
BU.progressLayer = new LayerProgress({
width: 200,
cancelValue: false,
cancel: false,
cancelDisplay: false
});
if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
FSWrapper.setupFilesystem(path.join(Env.boardDirPath, 'build'));
}
BU.requestPort = async () => {
await Serial.requestPort();
}
const readBinFile = (path, offset) => {
return new Promise((resolve, reject) => {
fetch(path)
.then((response) => {
return response.blob();
})
.then((blob) => {
const reader = new FileReader();
reader.onload = function (event) {
resolve({
address: parseInt(offset),
data: event.target.result
});
};
reader.onerror = function (error) {
throw(error);
}
reader.readAsBinaryString(blob);
})
.catch((error) => {
reject(error);
});
});
}
const readBinFileAsArrayBuffer = (path, offset) => {
return new Promise((resolve, reject) => {
fetch(path)
.then((response) => {
return response.blob();
})
.then((blob) => {
const reader = new FileReader();
reader.onload = function (event) {
resolve({
address: parseInt(offset),
data: event.target.result
});
};
reader.onerror = function (error) {
throw(error);
}
reader.readAsArrayBuffer(blob);
})
.catch((error) => {
reject(error);
});
});
}
const decodeKfpkgFromArrayBuffer = async(arrayBuf) => {
const zip = await JSZip.loadAsync(arrayBuf);
const manifestEntry = zip.file('flash-list.json');
if (!manifestEntry) {
throw new Error('kfpkg is missing flash-list.json');
}
const manifestText = await manifestEntry.async('string');
const manifest = JSON.parse(manifestText);
const items = [];
for (const f of manifest.files || []) {
const entry = zip.file(f.bin);
if (!entry) {
throw new Error(`Missing files in package: ${f.bin}`);
}
const data = new Uint8Array(await entry.async('uint8array'));
items.push({
address: f.address >>> 0,
filename: f.bin,
sha256Prefix: !!f.sha256Prefix,
data
});
}
return { manifest, items };
}
BU.initBurn = async () => {
if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
await BU.burnWithDAP();
} else if (['MixGo AI'].includes(BOARD.boardType)) {
const { web } = SELECTED_BOARD;
const boardKey = Boards.getSelectedBoardKey();
if (!web?.burn?.binFile) {
return;
}
if (typeof web.burn.binFile !== 'object') {
return;
}
await BU.burnWithKFlash(web.burn.binFile, web.burn.erase);
} else {
const { web } = SELECTED_BOARD;
const boardKey = Boards.getSelectedBoardKey();
if (!web?.burn?.binFile) {
return;
}
if (typeof web.burn.binFile !== 'object') {
return;
}
if (web.burn.special && web.burn.special instanceof Array) {
BU.burnWithSpecialBin();
} else {
await BU.burnWithEsptool(web.burn.binFile, web.burn.erase);
}
}
}
BU.burnWithDAP = async () => {
const { mainStatusBarTabs } = Mixly;
let portName = Serial.getSelectedPortName();
if (!portName) {
try {
await BU.requestPort();
portName = Serial.getSelectedPortName();
if (!portName) {
return;
}
} catch (error) {
Debug.error(error);
return;
}
}
const statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
if (statusBarSerial) {
await statusBarSerial.close();
}
const { web } = SELECTED_BOARD;
const { burn } = web;
const hexStr = goog.readFileSync(path.join(Env.boardDirPath, burn.filePath));
const hex2Blob = new Blob([ hexStr ], { type: 'text/plain' });
const buffer = await hex2Blob.arrayBuffer();
if (!buffer) {
layer.msg(Msg.Lang['shell.bin.readFailed'], { time: 1000 });
return;
}
BU.burning = true;
BU.uploading = false;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue(`${Msg.Lang['shell.burning']}...\n`);
mainStatusBarTabs.show();
mainStatusBarTabs.changeTo('output');
const port = Serial.getPort(portName);
const webUSB = new DAPjs.WebUSB(port);
const dapLink = new DAPjs.DAPLink(webUSB);
try {
await dapLink.connect();
await dapLink.setSerialBaudrate(115200);
} catch (error) {
Debug.error(error);
return;
}
let prevPercent = 0;
dapLink.on(DAPjs.DAPLink.EVENT_PROGRESS, progress => {
const nowPercent = Math.floor(progress * 100);
if (nowPercent > prevPercent) {
prevPercent = nowPercent;
} else {
return;
}
const nowProgressLen = Math.floor(nowPercent / 2);
const leftStr = new Array(nowProgressLen).fill('=').join('');
const rightStr = (new Array(50 - nowProgressLen).fill('-')).join('');
statusBarTerminal.addValue(`[${leftStr}${rightStr}] ${nowPercent}%\n`);
});
BU.progressLayer.title(`${Msg.Lang['shell.burning']}...`);
BU.progressLayer.show();
try {
await dapLink.flash(buffer);
BU.progressLayer.hide();
layer.msg(Msg.Lang['shell.burnSucc'], { time: 1000 });
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnSucc']}==\n`);
} catch (error) {
Debug.error(error);
BU.progressLayer.hide();
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
} finally {
dapLink.removeAllListeners(DAPjs.DAPLink.EVENT_PROGRESS);
await dapLink.disconnect();
await webUSB.close();
await port.close();
}
}
BU.burnWithEsptool = async (binFile, erase) => {
const { mainStatusBarTabs } = Mixly;
let portName = Serial.getSelectedPortName();
if (!portName) {
try {
await BU.requestPort();
portName = Serial.getSelectedPortName();
if (!portName) {
return;
}
} catch (error) {
Debug.error(error);
return;
}
}
const port = Serial.getPort(portName);
if (['HIDDevice'].includes(port.constructor.name)) {
layer.msg(Msg.Lang['burn.notSupport'], { time: 1000 });
return;
}
const statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
if (statusBarSerial) {
await statusBarSerial.close();
}
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue(Msg.Lang['shell.burning'] + '...\n');
mainStatusBarTabs.show();
mainStatusBarTabs.changeTo('output');
BU.progressLayer.title(`${Msg.Lang['shell.burning']}...`);
BU.progressLayer.show();
let esploader = null;
let transport = null;
try {
const baudrate = Boards.getSelectedBoardConfigParam('BurnSpeed') ?? '460800';
const serial = new Serial(portName);
transport = new SerialTransport(serial, false);
esploader = new ESPLoader({
transport,
baudrate,
terminal: {
clean() {
statusBarTerminal.setValue(Msg.Lang['shell.burning'] + '...\n');
},
writeLine(data) {
statusBarTerminal.addValue(data + '\n');
},
write(data) {
statusBarTerminal.addValue(data);
}
}
});
await esploader.main();
} catch (error) {
Debug.error(error);
statusBarTerminal.addValue(`\n${error.toString()}\n`);
try {
await transport.disconnect();
} catch (error) {
Debug.error(error);
}
BU.progressLayer.hide();
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
return;
}
statusBarTerminal.addValue(Msg.Lang['shell.bin.reading'] + "...");
let firmwarePromise = [];
statusBarTerminal.addValue("\n");
for (let i of binFile) {
if (i.path && i.offset) {
let absolutePath = path.join(Env.boardDirPath, i.path);
firmwarePromise.push(readBinFile(absolutePath, i.offset));
}
}
let data = null;
try {
data = await Promise.all(firmwarePromise);
} catch (error) {
statusBarTerminal.addValue("Failed!\n" + Msg.Lang['shell.bin.readFailed'] + "\n");
statusBarTerminal.addValue("\n" + error + "\n", true);
try {
await transport.disconnect();
} catch (error) {
Debug.error(error);
}
BU.progressLayer.hide();
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
return;
}
statusBarTerminal.addValue("Done!\n");
BU.burning = true;
BU.uploading = false;
const flashOptions = {
fileArray: data,
flashSize: 'keep',
eraseAll: erase,
compress: true,
calculateMD5Hash: (image) => CryptoJS.MD5(CryptoJS.enc.Latin1.parse(image))
};
try {
await esploader.writeFlash(flashOptions);
await transport.setDTR(false);
await new Promise((resolve) => setTimeout(resolve, 100));
await transport.setDTR(true);
BU.progressLayer.hide();
layer.msg(Msg.Lang['shell.burnSucc'], { time: 1000 });
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnSucc']}==\n`);
} catch (error) {
Debug.error(error);
BU.progressLayer.hide();
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
} finally {
try {
await transport.disconnect();
} catch (error) {
Debug.error(error);
}
}
}
BU.burnWithKFlash = async (binFile, erase) => {
const { mainStatusBarTabs } = Mixly;
let portName = Serial.getSelectedPortName();
if (!portName) {
try {
await BU.requestPort();
portName = Serial.getSelectedPortName();
if (!portName) {
return;
}
} catch (error) {
Debug.error(error);
return;
}
}
const port = Serial.getPort(portName);
if (['HIDDevice', 'USBDevice'].includes(port.constructor.name)) {
layer.msg(Msg.Lang['burn.notSupport'], { time: 1000 });
return;
}
const statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
if (statusBarSerial) {
await statusBarSerial.close();
}
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue(Msg.Lang['shell.burning'] + '...\n');
mainStatusBarTabs.show();
mainStatusBarTabs.changeTo('output');
BU.progressLayer.title(`${Msg.Lang['shell.burning']}...`);
BU.progressLayer.show();
let data = [];
try {
for (let i of binFile) {
if (i.path && i.offset) {
const extname = path.extname(i.path);
const absolutePath = path.join(Env.boardDirPath, i.path);
const info = await readBinFileAsArrayBuffer(absolutePath, i.offset);
if (extname === '.kfpkg') {
const result = await decodeKfpkgFromArrayBuffer(info.data);
data.push(...result.items);
} else {
data.push(info);
}
}
}
} catch (error) {
statusBarTerminal.addValue(`\n[ERROR] ${error}\n`, true);
BU.progressLayer.hide();
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
return;
}
let serial = null;
try {
serial = new Serial(portName);
const kflash = new KFlash(serial);
kflash.bind('message', (message) => {
statusBarTerminal.addValue(message);
});
kflash.bind('replaceMessage', (lineNumber, message) => {
statusBarTerminal.replaceLine(lineNumber, message);
});
await kflash.enter();
for (let item of data) {
await kflash.write(item.data, item.address, item.sha256Prefix ?? true, item?.filename ?? 'main.bin');
}
BU.progressLayer.hide();
layer.msg(Msg.Lang['shell.burnSucc'], { time: 1000 });
statusBarTerminal.appendLine(`==${Msg.Lang['shell.burnSucc']}==\n`);
} catch (error) {
statusBarTerminal.appendLine(`[ERROR] ${error.message}\n`);
BU.progressLayer.hide();
statusBarTerminal.appendLine(`==${Msg.Lang['shell.burnFailed']}==\n`);
} finally {
try {
serial && await serial.close();
} catch (error) {
Debug.error(error);
}
}
}
BU.getImportModulesName = (code) => {
// 正则表达式: 匹配 import 或 from 导入语句
const importRegex = /(?:import\s+([a-zA-Z0-9_]+)|from\s+([a-zA-Z0-9_]+)\s+import)/g;
let imports = [];
let match;
while ((match = importRegex.exec(code)) !== null) {
if (match[1]) {
imports.push(match[1]); // 'import module'
}
if (match[2]) {
imports.push(match[2]); // 'from module import ...'
}
}
return imports;
}
BU.getImportModules = (code) => {
let importsMap = {};
const libPath = SELECTED_BOARD.upload.libPath;
for (let i = libPath.length - 1; i >= 0; i--) {
const dirname = MString.tpl(libPath[i], { indexPath: Env.boardDirPath });
const map = goog.readJsonSync(path.join(dirname, 'map.json'));
if (!(map && map instanceof Object)) {
continue;
}
for (let key in map) {
importsMap[key] = structuredClone(map[key]);
importsMap[key]['__path__'] = path.join(dirname, map[key]['__name__']);
}
}
let usedMap = {};
let currentImports = BU.getImportModulesName(code);
while (currentImports.length) {
let temp = [];
for (let moduleName of currentImports) {
let moduleInfo = importsMap[moduleName];
if (!moduleInfo) {
continue;
}
usedMap[moduleName] = moduleInfo;
const moduleImports = moduleInfo['__require__'];
if (!moduleImports) {
continue;
}
for (let name of moduleImports) {
if (usedMap[name] || !importsMap[name] || temp.includes(name)) {
continue;
}
temp.push(name);
}
}
currentImports = temp;
}
return usedMap;
}
BU.initUpload = async () => {
let portName = Serial.getSelectedPortName();
if (!portName) {
try {
await BU.requestPort();
portName = Serial.getSelectedPortName();
if (!portName) {
return;
}
} catch (error) {
Debug.error(error);
return;
}
}
if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
await BU.uploadWithDAP(portName);
} else {
await BU.uploadWithAmpy(portName);
}
}
BU.uploadWithDAP = async (portName) => {
const { mainStatusBarTabs } = Mixly;
if (!portName) {
try {
await BU.requestPort();
portName = Serial.getSelectedPortName();
if (!portName) {
return;
}
} catch (error) {
Debug.error(error);
return;
}
}
let statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
if (statusBarSerial) {
await statusBarSerial.close();
}
const port = Serial.getPort(portName);
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const dapWrapper = new DAPWrapper(port, {
event: () => {},
log: () => {}
});
const partialFlashing = new PartialFlashing(dapWrapper, {
event: () => {}
});
let boardId = 0x9901;
const boardKey = Boards.getSelectedBoardKey();
if (boardKey === 'micropython:nrf51822:v2') {
boardId = 0x9903;
}
BU.burning = false;
BU.uploading = true;
statusBarTerminal.setValue(Msg.Lang['shell.uploading'] + '...\n');
mainStatusBarTabs.show();
mainStatusBarTabs.changeTo('output');
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
FSWrapper.writeFile('main.py', code);
const importsMap = BU.getImportModules(code);
for (let key in importsMap) {
const filename = importsMap[key]['__name__'];
const data = goog.readFileSync(importsMap[key]['__path__']);
FSWrapper.writeFile(filename, data);
}
BU.progressLayer.title(`${Msg.Lang['shell.uploading']}...`);
BU.progressLayer.show();
try {
let prevPercent = 0;
await partialFlashing.flashAsync(new BoardId(0x9900), FSWrapper, progress => {
const nowPercent = Math.floor(progress * 100);
if (nowPercent > prevPercent) {
prevPercent = nowPercent;
} else {
return;
}
const nowProgressLen = Math.floor(nowPercent / 2);
const leftStr = new Array(nowProgressLen).fill('=').join('');
const rightStr = (new Array(50 - nowProgressLen).fill('-')).join('');
statusBarTerminal.addValue(`[${leftStr}${rightStr}] ${nowPercent}%\n`);
});
BU.progressLayer.hide();
layer.msg(Msg.Lang['shell.uploadSucc'], { time: 1000 });
statusBarTerminal.addValue(`==${Msg.Lang['shell.uploadSucc']}==\n`);
if (!statusBarSerial) {
mainStatusBarTabs.add('serial', portName);
statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
}
statusBarSerial.setValue('');
mainStatusBarTabs.changeTo(portName);
await statusBarSerial.open();
} catch (error) {
await dapWrapper.disconnectAsync();
Debug.error(error);
BU.progressLayer.hide();
statusBarTerminal.addValue(`${error}\n`);
statusBarTerminal.addValue(`==${Msg.Lang['shell.uploadFailed']}==\n`);
}
}
BU.uploadWithAmpy = async (portName) => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
let statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
BU.burning = false;
BU.uploading = true;
statusBarTerminal.setValue(`${Msg.Lang['shell.uploading']}...\n`);
mainStatusBarTabs.show();
mainStatusBarTabs.changeTo('output');
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const port = Serial.getPort(portName);
BU.progressLayer.title(`${Msg.Lang['shell.uploading']}...`);
BU.progressLayer.show();
const serial = new Serial(portName);
const ampy = new Ampy(serial);
ampy.bind('message', (message) => {
statusBarTerminal.addValue(message);
});
ampy.bind('replaceMessage', (lineNumber, message) => {
statusBarTerminal.replaceLine(lineNumber, message);
});
const code = editor.getCode();
let closePromise = Promise.resolve();
if (statusBarSerial) {
closePromise = statusBarSerial.close();
}
try {
await closePromise;
await ampy.enter();
await ampy.put('main.py', code);
/*const importsMap = BU.getImportModules(code);
let libraries = {};
for (let key in importsMap) {
const filename = importsMap[key]['__name__'];
const data = goog.readFileSync(importsMap[key]['__path__']);
libraries[filename] = {
data,
size: importsMap[key]['__size__']
};
}
let cwd = await ampy.cwd();
const rootInfo = await ampy.ls(cwd);
let rootMap = {};
for (let item of rootInfo) {
rootMap[item[0]] = item[1];
}
if (cwd === '/') {
cwd = '';
}
if (libraries && libraries instanceof Object) {
for (let key in libraries) {
if (rootMap[`${cwd}/${key}`] !== undefined && rootMap[`${cwd}/${key}`] === libraries[key].size) {
statusBarTerminal.addValue(`Writing ${key} (Skipped)\n`);
continue;
}
await ampy.put(key, libraries[key].data);
}
}*/
await ampy.exit();
await ampy.dispose();
BU.progressLayer.hide();
layer.msg(Msg.Lang['shell.uploadSucc'], { time: 1000 });
statusBarTerminal.appendLine(`==${Msg.Lang['shell.uploadSucc']}==\n`);
if (!statusBarSerial) {
mainStatusBarTabs.add('serial', portName);
statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
}
statusBarSerial.setValue('');
mainStatusBarTabs.changeTo(portName);
await statusBarSerial.open();
} catch (error) {
ampy.dispose();
BU.progressLayer.hide();
Debug.error(error);
statusBarTerminal.appendLine(`[ERROR] ${error.message}\n`);
statusBarTerminal.appendLine(`==${Msg.Lang['shell.uploadFailed']}==\n`);
}
}
/**
* @function 特殊固件的烧录
* @return {void}
**/
BU.burnWithSpecialBin = () => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const firmwares = SELECTED_BOARD.web.burn.special;
let menu = [];
let firmwareMap = {};
for (let firmware of firmwares) {
if (!firmware?.name && !firmware?.binFile) continue;
menu.push({
id: firmware.name,
text: firmware.name
});
firmwareMap[firmware.name] = firmware.binFile;
}
BU.firmwareLayer.setMap(firmwareMap);
BU.firmwareLayer.setMenu(menu);
BU.firmwareLayer.show();
}
});

View File

@@ -0,0 +1,47 @@
goog.loadJs('web', () => {
goog.require('path');
goog.require('Mixly.FileTree');
goog.require('Mixly.Web.FS');
goog.provide('Mixly.Web.FileTree');
const { FileTree, Web } = Mixly;
const { FS } = Web;
class FileTreeExt extends FileTree {
constructor() {
super(FS);
}
async readFolder(inPath) {
const fs = this.getFS();
const status = await fs.isDirectory(inPath);
let output = [];
if (!status) {
return output;
}
const children = await fs.readDirectory(inPath);
for (let data of children) {
const dataPath = path.join(inPath, data);
if (await fs.isDirectory(dataPath)) {
const isDirEmpty = await fs.isDirectoryEmpty(dataPath);
output.push({
type: 'folder',
id: dataPath,
children: !isDirEmpty
});
} else {
output.push({
type: 'file',
id: dataPath,
children: false
});
}
}
return output;
}
}
Web.FileTree = FileTreeExt;
});

View File

@@ -0,0 +1,218 @@
goog.loadJs('web', () => {
goog.require('path');
goog.require('Blockly');
goog.require('Mixly.MFile');
goog.require('Mixly.Title');
goog.require('Mixly.LayerNewFile');
goog.require('Mixly.Msg');
goog.require('Mixly.Workspace');
goog.provide('Mixly.Web.File');
const {
MFile,
Web,
LayerNewFile,
Msg,
Title,
Workspace
} = Mixly;
const { MSG } = Blockly.Msg;
const { File } = Web;
const platform = goog.platform();
File.obj = null;
File.newFileLayer = new LayerNewFile();
File.newFileLayer.bind('empty', () => {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const blockEditor = editor.getPage('block').getEditor();
const codeEditor = editor.getPage('code').getEditor();
const generator = Blockly.generator;
blockEditor.clear();
blockEditor.scrollCenter();
Blockly.hideChaff();
codeEditor.setValue(generator.workspaceToCode(blockEditor) || '', -1);
Title.updateTitle(Title.title);
File.obj = null;
});
File.getFileTypes = (filters) => {
let fileTypes = [];
if (platform === 'mobile') {
fileTypes.push({
description: 'Mixly File',
accept: {
'application/octet-stream': filters
}
});
} else {
fileTypes.push({
description: 'Mixly File',
accept: {
'application/xml': filters
}
});
}
return fileTypes;
}
File.open = async () => {
if (window.location.protocol === 'https:') {
let filters = [];
MFile.openFilters.map((data) => {
filters.push(`.${data}`);
});
const fileConfig = {
multiple: false,
types: File.getFileTypes(filters),
excludeAcceptAllOption: true
};
try {
const [ obj ] = await window.showOpenFilePicker(fileConfig);
if (!obj) {
return;
}
File.obj = obj;
const extname = path.extname(obj.name);
const fileInfo = await File.obj.getFile();
if (!fileInfo) {
return;
}
File.parseData(extname, await fileInfo.text());
Title.updateTitle(`${obj.name} - ${Title.title}`);
} catch (error) {
console.log(error);
}
} else {
const filters = '.' + MFile.openFilters.join(',.');
MFile.openFile(filters, 'text', (fileObj) => {
let { data, filename } = fileObj;
const extname = path.extname(filename);
File.parseData(extname, data);
Title.updateTitle(`${filename} - ${Title.title}`);
});
}
}
File.parseData = (extname, text) => {
const index = extname.indexOf(' ');
if (index !== -1) {
extname = extname.substring(0, index);
}
if (['.bin', '.hex'].includes(extname)) {
MFile.loadHex(text);
} else if (['.mix', '.xml', '.ino', '.py'].includes(extname)) {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
editor.setValue(text, extname);
} else {
layer.msg(Msg.Lang['file.type.error'], { time: 1000 });
File.obj = null;
}
}
File.save = async () => {
window.userEvents && window.userEvents.addRecord({
operation: 'save'
});
if (!File.obj) {
File.saveAs();
return;
}
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
let text = '';
let extname = path.extname(File.obj.name);
const index = extname.indexOf(' ');
if (index !== -1) {
extname = extname.substring(0, index);
}
if (['.mix', '.xml'].includes(extname)) {
text = editor.getValue();
} else if (['.ino', '.py'].includes(extname)) {
text = editor.getCode();
} else {
return;
}
try {
let currentLayero = null;
const loadIndex = layer.msg(Msg.Lang['file.saving'], {
icon: 16,
shade: 0,
time: 0,
success: function(layero) {
currentLayero = layero;
}
});
const writer = await File.obj.createWritable({
keepExistingData: true
});
await writer.write(text);
await writer.close();
let $content = currentLayero.children('.layui-layer-content');
$content.html(`<i class="layui-layer-face layui-icon layui-icon-success"></i>${Msg.Lang['file.saveSucc']}`);
currentLayero = null;
$content = null;
setTimeout(() => {
layer.close(loadIndex);
}, 500);
} catch (error) {
console.log(error);
}
}
File.saveAs = async () => {
let filters = [];
MFile.saveFilters.map((data) => {
filters.push(`.${data.extensions[0]}`);
});
const fileConfig = {
types: File.getFileTypes(filters),
suggestedName: 'mixly.mix'
};
try {
const obj = await window.showSaveFilePicker(fileConfig);
if (!obj) {
return;
}
File.obj = obj;
File.save();
Title.updateTitle(`${obj.name} - ${Title.title}`);
} catch (error) {
console.log(error);
}
}
File.new = async () => {
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const blockEditor = editor.getPage('block').getEditor();
const codeEditor = editor.getPage('code').getEditor();
const generator = Blockly.generator;
const blocksList = blockEditor.getAllBlocks();
if (editor.getPageType() === 'code') {
const code = codeEditor.getValue(),
workspaceToCode = generator.workspaceToCode(blockEditor) || '';
if (!blocksList.length && workspaceToCode === code) {
layer.msg(Msg.Lang['editor.codeEditorEmpty'], { time: 1000 });
Title.updateTitle(Title.title);
File.obj = null;
return;
}
} else {
if (!blocksList.length) {
layer.msg(Msg.Lang['editor.blockEditorEmpty'], { time: 1000 });
Title.updateTitle(Title.title);
File.obj = null;
return;
}
}
File.newFileLayer.show();
}
});

View File

@@ -0,0 +1,87 @@
goog.loadJs('web', () => {
goog.require('path');
goog.require('Mixly.Config');
goog.require('Mixly.Env');
goog.require('Mixly.MJson');
goog.require('Mixly.FooterLayerExample');
goog.require('Mixly.Boards');
goog.provide('Mixly.Web.FooterLayerExample');
const {
Config,
Env,
FooterLayerExample,
MJson,
Boards,
Web
} = Mixly;
const { BOARD } = Config;
class FooterLayerExampleExt extends FooterLayerExample {
static DIR_TREE = MJson.get(path.join(Env.boardDirPath, 'examples/map.json')) ?? [];
constructor(element) {
super(element);
}
getRoot() {
const { DIR_TREE } = FooterLayerExampleExt;
let exampleList = [];
if (DIR_TREE instanceof Object) {
exampleList = [{
title: BOARD.boardType,
id: '',
children: []
}];
}
return exampleList;
}
getChildren(inPath) {
const { DIR_TREE } = FooterLayerExampleExt;
let pathList = [];
if (inPath) {
pathList = inPath.split('/');
}
let obj = DIR_TREE;
for (let key of pathList) {
if (!key) {
continue;
}
if (obj[key]) {
obj = obj[key];
} else {
return [];
}
}
if (!(obj instanceof Object)) {
return [];
}
let exampleList = [];
for (let key in obj) {
if (!(obj[key] instanceof Object)) {
continue;
}
const exampleObj = {
title: obj[key]['__name__'],
id: inPath ? (inPath + '/' + key) : key
};
if (!obj[key]['__file__']) {
exampleObj.children = [];
}
exampleList.push(exampleObj);
}
return exampleList;
}
dataToWorkspace(inPath) {
const data = goog.readFileSync(path.join(Env.boardDirPath, 'examples', inPath));
this.updateCode(path.extname(inPath), data);
}
}
Web.FooterLayerExample = FooterLayerExampleExt;
});

View File

@@ -0,0 +1,161 @@
goog.loadJs('web', () => {
goog.require('workerpool');
goog.require('Mixly.Web');
goog.provide('Mixly.Web.FS');
const { FS } = Mixly.Web;
FS.pool = workerpool.pool('../common/modules/mixly-modules/workers/web/file-system-access.js', {
workerOpts: {
name: 'fileSystemAccess'
},
workerType: 'web'
});
FS.showOpenFilePicker = async () => {
return new Promise((resolve, reject) => {
resolve();
});
}
FS.showDirectoryPicker = async () => {
const directoryHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
const permissionStatus = await directoryHandle.requestPermission({ mode: 'readwrite' });
if (permissionStatus !== 'granted') {
throw new Error('readwrite access to directory not granted');
}
await FS.pool.exec('addFileSystemHandler', [directoryHandle]);
return directoryHandle;
}
FS.showSaveFilePicker = async () => {
return new Promise((resolve, reject) => {
});
}
FS.readFile = (path) => {
return new Promise(async (resolve, reject) => {
const [data] = await FS.pool.exec('readFile', [path, 'utf8']);
resolve(data);
});
}
FS.writeFile = (path, data) => {
return new Promise(async (resolve, reject) => {
const [error, entries] = await FS.pool.exec('writeFile', [path, data, 'utf8']);
if (error) {
reject(error);
} else {
resolve(entries);
}
});
}
FS.isFile = (path) => {
return new Promise(async (resolve, reject) => {
const [_, stats] = await FS.pool.exec('stat', [path]);
if (!stats) {
resolve(false);
return;
}
if (stats.mode === 33188) {
resolve(true);
} else {
resolve(false);
}
});
}
FS.renameFile = (oldFilePath, newFilePath) => {
return new Promise(async (resolve, reject) => {
const [error] = await FS.pool.exec('rename', [oldFilePath, newFilePath]);
if (error) {
reject(error);
} else {
resolve();
}
});
}
FS.moveFile = (oldFilePath, newFilePath) => {
return FS.renameFile(oldFilePath, newFilePath);
}
FS.deleteFile = (filePath) => {
return new Promise(async (resolve, reject) => {
const [error] = await FS.pool.exec('unlink', [filePath]);
if (error) {
reject(error);
} else {
resolve();
}
});
}
FS.createDirectory = (folderPath) => {
return new Promise(async (resolve, reject) => {
const [error] = await FS.pool.exec('mkdir', [folderPath, 0o777]);
if (error) {
reject(error);
} else {
resolve();
}
});
}
FS.readDirectory = (path) => {
return new Promise(async (resolve, reject) => {
const [error, entries] = await FS.pool.exec('readdir', [path]);
if (error) {
reject(error);
} else {
resolve(entries);
}
});
}
FS.isDirectory = (path) => {
return new Promise(async (resolve, reject) => {
const [_, stats] = await FS.pool.exec('stat', [path]);
if (!stats) {
resolve(false);
return;
}
if (stats.mode === 33188) {
resolve(false);
} else {
resolve(true);
}
});
}
FS.isDirectoryEmpty = async (path) => {
return !(await FS.readDirectory(path) ?? []).length;
}
FS.moveDirectory = (oldFolderPath, newFolderPath) => {
return new Promise(async (resolve, reject) => {
resolve();
});
}
FS.copyDirectory = (oldFolderPath, newFolderPath) => {
return new Promise(async (resolve, reject) => {
resolve();
});
}
FS.deleteDirectory = (folderPath) => {
return new Promise(async (resolve, reject) => {
const [error] = await FS.pool.exec('rmdir', [folderPath]);
if (!error) {
resolve();
} else {
reject();
}
});
}
});

View File

@@ -0,0 +1,247 @@
goog.loadJs('web', () => {
goog.require('Mixly.Serial');
goog.require('Mixly.Config');
goog.require('Mixly.Web');
goog.provide('Mixly.Web.HID');
const {
Serial,
Config,
Web
} = Mixly;
const { SELECTED_BOARD } = Config;
class WebHID extends Serial {
static {
this.type = 'hid';
this.getConfig = function () {
return Serial.getConfig();
}
this.getSelectedPortName = function () {
return Serial.getSelectedPortName();
}
this.getCurrentPortsName = function () {
return Serial.getCurrentPortsName();
}
this.refreshPorts = function () {
Serial.refreshPorts();;
}
this.requestPort = async function () {
let options = SELECTED_BOARD?.web?.devices?.hid;
if (!options || typeof(options) !== 'object') {
options = {
filters: []
};
}
const devices = await navigator.hid.requestDevice(options);
if (!devices.length) {
return;
}
for (let device of devices) {
this.addPort(device);
}
this.refreshPorts();
}
this.getPort = function (name) {
return Serial.nameToPortRegistry.getItem(name);
}
this.addPort = function (device) {
if (Serial.portToNameRegistry.hasKey(device)) {
return;
}
let name = '';
for (let i = 1; i <= 20; i++) {
name = `hid${i}`;
if (Serial.nameToPortRegistry.hasKey(name)) {
continue;
}
break;
}
Serial.portToNameRegistry.register(device, name);
Serial.nameToPortRegistry.register(name, device);
}
this.removePort = function (device) {
if (!Serial.portToNameRegistry.hasKey(device)) {
return;
}
const name = Serial.portToNameRegistry.getItem(device);
if (!name) {
return;
}
Serial.portToNameRegistry.unregister(device);
Serial.nameToPortRegistry.unregister(name);
}
this.addEventsListener = function () {
navigator?.hid?.addEventListener('connect', (event) => {
this.addPort(event.device);
this.refreshPorts();
});
navigator?.hid?.addEventListener('disconnect', (event) => {
event.device.onclose && event.device.onclose();
this.removePort(event.device);
this.refreshPorts();
});
}
this.init = function () {
navigator?.hid?.getDevices().then((devices) => {
for (let device of devices) {
this.addPort(device);
}
});
this.addEventsListener();
}
}
#device_ = null;
#keepReading_ = null;
#reader_ = null;
#writer_ = null;
#stringTemp_ = '';
#dataLength_ = 31;
constructor(port) {
super(port);
this.#device_ = WebHID.getPort(port);
}
#addEventsListener_() {
this.#device_.oninputreport = (event) => {
const { data } = event;
const length = Math.min(data.getUint8(0) + 1, data.byteLength);
let buffer = [];
for (let i = 1; i < length; i++) {
buffer.push(data.getUint8(i));
}
this.onBuffer(buffer);
};
this.#device_.onclose = () => {
if (!this.isOpened()) {
return;
}
super.close();
this.#stringTemp_ = '';
this.onClose(1);
}
}
async open(baud) {
return new Promise((resolve, reject) => {
const portsName = Serial.getCurrentPortsName();
const currentPortName = this.getPortName();
if (!portsName.includes(currentPortName)) {
reject('no device available');
return;
}
if (this.isOpened()) {
resolve();
return;
}
baud = baud ?? this.getBaudRate();
this.#device_ = WebHID.getPort(currentPortName);
this.#device_.open()
.then(() => {
super.open(baud);
super.setBaudRate(baud);
this.onOpen();
this.#addEventsListener_();
resolve();
})
.catch(reject);
});
}
async close() {
if (!this.isOpened()) {
return;
}
super.close();
await this.#device_.close();
this.#stringTemp_ = '';
this.#device_.oninputreport = null;
this.#device_.onclose = null;
this.onClose(1);
}
async setBaudRate(baud) {
return Promise.resolve();
}
async sendString(str) {
const buffer = this.encode(str);
return this.sendBuffer(buffer);
}
async sendBuffer(buffer) {
if (buffer.constructor.name !== 'Uint8Array') {
buffer = new Uint8Array(buffer);
}
const len = Math.ceil(buffer.length / this.#dataLength_);
for (let i = 0; i < len; i++) {
const start = i * this.#dataLength_;
const end = Math.min((i + 1) * this.#dataLength_, buffer.length);
const writeBuffer = buffer.slice(start, end);
let temp = new Uint8Array(end - start + 1);
temp[0] = writeBuffer.length;
temp.set(writeBuffer, 1);
await this.#device_.sendReport(0, temp);
await this.sleep(10);
}
}
async setDTRAndRTS(dtr, rts) {
return Promise.resolve();
}
async setDTR(dtr) {
return this.setDTRAndRTS(dtr, this.getRTS());
}
async setRTS(rts) {
return this.setDTRAndRTS(this.getDTR(), rts);
}
getVID() {
return this.#device_.vendorId;
}
getPID() {
return this.#device_.productId;
}
onBuffer(buffer) {
super.onBuffer(buffer);
for (let i = 0; i < buffer.length; i++) {
super.onByte(buffer[i]);
}
const string = this.decodeBuffer(buffer);
if (!string) {
return;
}
for (let char of string) {
super.onChar(char);
if (['\r', '\n'].includes(char)) {
super.onString(this.#stringTemp_);
this.#stringTemp_ = '';
} else {
this.#stringTemp_ += char;
}
}
}
}
Web.HID = WebHID;
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,231 @@
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;
_DTR_state = false;
SLIP_END = 0xc0;
SLIP_ESC = 0xdb;
SLIP_ESC_END = 0xdc;
SLIP_ESC_ESC = 0xdd;
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(`Read ${data.length} bytes: ${this.hexConvert(data)}`);
}
});
}
getInfo() {
const VID = this.serial.getVID?.();
const PID = this.serial.getPID?.();
return (VID && PID)
? `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 traceMessage = `TRACE +${delta}ms ${message}`;
this.lastTraceTime = Date.now();
// console.log(traceMessage);
this.traceLog += traceMessage + '\n';
}
async returnTrace() {
await navigator.clipboard.writeText(this.traceLog);
}
hexify(arr) {
return Array.from(arr)
.map(b => b.toString(16).padStart(2, '0'))
.join(' ')
.padEnd(16, ' ');
}
hexConvert(arr, autoSplit = true) {
if (!autoSplit || arr.length <= 16) {
return this.hexify(arr);
}
let out = '';
let s = arr;
while (s.length) {
const line = s.slice(0, 16);
const ascii = String.fromCharCode(...line)
.replace(/[^\x20-\x7E]/g, '.');
s = s.slice(16);
out += `\n ${this.hexify(line.slice(0,8))} ${this.hexify(line.slice(8))} | ${ascii}`;
}
return out;
}
appendArray(a, b) {
const out = new Uint8Array(a.length + b.length);
out.set(a);
out.set(b, a.length);
return out;
}
inWaiting() {
return this.buffer.length;
}
async flushInput() {
this.buffer = new Uint8Array(0);
}
slipWriter(data) {
const out = [this.SLIP_END];
for (const b of data) {
if (b === this.SLIP_END) out.push(this.SLIP_ESC, this.SLIP_ESC_END);
else if (b === this.SLIP_ESC) out.push(this.SLIP_ESC, this.SLIP_ESC_ESC);
else out.push(b);
}
out.push(this.SLIP_END);
return new Uint8Array(out);
}
async write(data) {
const out = this.slipReaderEnabled ? this.slipWriter(data) : data;
if (this.tracing) {
this.trace(`Write ${out.length} bytes: ${this.hexConvert(out)}`);
}
await this.serial.sendBuffer(out);
}
async newRead(numBytes, timeout) {
const start = Date.now();
while (this.buffer.length < numBytes) {
if (!this.serial.isOpened()) return null;
if (Date.now() - start > timeout) break;
await this.sleep(5);
}
const out = this.buffer.slice(0, numBytes);
this.buffer = this.buffer.slice(numBytes);
return out;
}
async *read(timeout = 1000) {
let partial = null;
let escaping = false;
let successfulSlip = false;
while (true) {
const chunk = await this.newRead(1, timeout);
if (!chunk || chunk.length === 0) {
const msg = partial
? 'Packet content transfer stopped'
: successfulSlip
? 'Serial stream stopped'
: 'No serial data received';
throw new Error(msg);
}
const byte = chunk[0];
if (partial === null) {
if (byte === this.SLIP_END) {
partial = new Uint8Array(0);
}
continue;
}
if (escaping) {
escaping = false;
if (byte === this.SLIP_ESC_END)
partial = this.appendArray(partial, new Uint8Array([this.SLIP_END]));
else if (byte === this.SLIP_ESC_ESC)
partial = this.appendArray(partial, new Uint8Array([this.SLIP_ESC]));
else
throw new Error(`Invalid SLIP escape 0xdb 0x${byte.toString(16)}`);
continue;
}
if (byte === this.SLIP_ESC) {
escaping = true;
} else if (byte === this.SLIP_END) {
if (this.tracing) {
this.trace(`Received packet: ${this.hexConvert(partial)}`);
}
successfulSlip = true;
yield partial;
partial = null;
} else {
partial = this.appendArray(partial, new Uint8Array([byte]));
}
}
}
detectPanicHandler(input) {
const text = new TextDecoder().decode(input);
const guru = /G?uru Meditation Error/;
const fatal = /F?atal exception \(\d+\)/;
if (guru.test(text) || fatal.test(text)) {
throw new Error('Guru Meditation Error detected');
}
}
async setRTS(state) {
await this.serial.setRTS(state);
await this.setDTR(this._DTR_state);
}
async setDTR(state) {
this._DTR_state = state;
await this.serial.setDTR(state);
}
async connect(baud = 115200) {
await this.serial.open(baud);
this.baudrate = baud;
if (this.tracing) this.trace(`Serial opened @${baud}`);
}
async disconnect() {
await this.serial.close();
this.buffer = new Uint8Array(0);
if (this.tracing) this.trace('Serial closed');
}
sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
}
Web.SerialTransport = Transport;
});

View File

@@ -0,0 +1,215 @@
goog.loadJs('web', () => {
goog.require('path');
goog.require('Mixly.Config');
goog.require('Mixly.Env');
goog.require('Mixly.Msg');
goog.require('Mixly.Registry');
goog.require('Mixly.Serial');
goog.require('Mixly.LayerExt');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.Web.SerialPort');
goog.require('Mixly.Web.USB');
goog.require('Mixly.Web.USBMini');
goog.require('Mixly.Web.HID');
goog.provide('Mixly.Web.Serial');
const {
Config,
Env,
Msg,
Registry,
Serial,
LayerExt,
HTMLTemplate,
Web
} = Mixly;
const {
SerialPort,
USB,
USBMini,
HID
} = Web;
const { BOARD } = Config;
const platform = goog.platform();
const fullPlatform = goog.fullPlatform();
class WebSerial extends Serial {
static {
this.devicesRegistry = new Registry();
this.type = Serial.type;
this.DEVICES_SELECT_LAYER = new HTMLTemplate(
goog.readFileSync(path.join(Env.templatePath, 'html/devices-select-layer.html'))
);
this.getConfig = function () {
return Serial.getConfig();
}
this.getSelectedPortName = function () {
return Serial.getSelectedPortName();
}
this.getCurrentPortsName = function () {
return Serial.getCurrentPortsName();
}
this.refreshPorts = function () {
Serial.refreshPorts();
}
this.requestPort = function () {
if (this.devicesRegistry.length() < 1) {
throw Error('can not find any device handler');
} else if (this.devicesRegistry.length() === 1) {
const keys = this.devicesRegistry.keys();
return this.devicesRegistry.getItem(keys[0]).requestPort();
}
const msg = {
serialMsg: Msg.Lang['layer.devices.serial'],
serialStatus: this.devicesRegistry.hasKey('serial') ? '' : 'disabled',
hidMsg: Msg.Lang['layer.devices.hid'],
hidStatus: this.devicesRegistry.hasKey('hid') ? '' : 'disabled',
usbMsg: Msg.Lang['layer.devices.usb'],
usbStatus: (
this.devicesRegistry.hasKey('usb') || this.devicesRegistry.hasKey('usbmini')
) ? '' : 'disabled'
};
return new Promise((resolve, reject) => {
let selected = false;
const layerNum = LayerExt.open({
title: [Msg.Lang['layer.devices.select'], '36px'],
area: ['400px', '150px'],
max: false,
min: false,
content: this.DEVICES_SELECT_LAYER.render(msg),
shade: LayerExt.SHADE_ALL,
resize: false,
success: function (layero, index) {
$(layero).on('click', 'button', (event) => {
selected = true;
layer.close(layerNum);
const $btn = $(event.currentTarget);
let mId = $btn.attr('m-id');
if (mId === 'usb' && WebSerial.devicesRegistry.hasKey('usbmini')) {
mId = 'usbmini';
}
const Device = WebSerial.devicesRegistry.getItem(mId);
Device.requestPort().then(resolve).catch(reject);
});
},
end: function () {
if (!selected) {
reject('user not select any device');
}
$(`#layui-layer-shade${layerNum}`).remove();
}
});
});
}
this.getHandler = function (device) {
if (device.constructor.name === 'SerialPort') {
return SerialPort;
} else if (device.constructor.name === 'HIDDevice') {
return HID;
} else if (device.constructor.name === 'USBDevice') {
if (this.devicesRegistry.hasKey('usbmini')) {
return USBMini;
} else {
return USB;
}
}
return null;
}
this.getPort = function (name) {
return Serial.nameToPortRegistry.getItem(name);
}
this.addPort = function (device) {
const handler = this.getHandler(device);
if (!handler) {
return;
}
handler.addPort(device);
}
this.removePort = function (device) {
const handler = this.getHandler(device);
if (!handler) {
return;
}
handler.removePort(device);
}
this.addEventsListener = function () {}
this.init = function () {
if (Env.hasSocketServer) {
return;
}
if (platform === 'win32' && fullPlatform !== 'win10') {
if (BOARD?.web?.devices?.hid) {
this.devicesRegistry.register('hid', HID);
HID.init();
}
if (BOARD?.web?.devices?.serial) {
this.devicesRegistry.register('serial', SerialPort);
SerialPort.init();
}
if (BOARD?.web?.devices?.usb) {
if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
this.devicesRegistry.register('usb', USB);
USB.init();
}
}
} else if (platform === 'mobile') {
if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
this.devicesRegistry.register('usb', USB);
USB.init();
} else {
this.devicesRegistry.register('usbmini', USBMini);
USBMini.init();
}
} else {
if (BOARD?.web?.devices?.serial) {
this.devicesRegistry.register('serial', SerialPort);
SerialPort.init();
} else if (BOARD?.web?.devices?.usb) {
if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
this.devicesRegistry.register('usb', USB);
USB.init();
} else {
this.devicesRegistry.register('usbmini', USBMini);
USBMini.init();
}
} else if (BOARD?.web?.devices?.hid) {
this.devicesRegistry.register('hid', HID);
HID.init();
}
}
}
this.init();
}
constructor(port) {
super(port);
const device = WebSerial.getPort(port);
const handler = WebSerial.getHandler(device);
if (!handler) {
return;
}
return new handler(port);
}
}
Web.Serial = WebSerial;
});

View File

@@ -0,0 +1,279 @@
goog.loadJs('web', () => {
goog.require('Mixly.Serial');
goog.require('Mixly.Debug');
goog.require('Mixly.Config');
goog.require('Mixly.Web');
goog.provide('Mixly.Web.SerialPort');
const {
Serial,
Debug,
Config,
Web
} = Mixly;
const { SELECTED_BOARD } = Config;
class WebSerialPort extends Serial {
static {
this.type = 'serialport';
this.getConfig = function () {
return Serial.getConfig();
}
this.getSelectedPortName = function () {
return Serial.getSelectedPortName();
}
this.getCurrentPortsName = function () {
return Serial.getCurrentPortsName();
}
this.refreshPorts = function () {
Serial.refreshPorts();;
}
this.requestPort = async function () {
let options = SELECTED_BOARD?.web?.devices?.serial;
if (!options || typeof(options) !== 'object') {
options = {
filters: []
};
}
const serialport = await navigator.serial.requestPort(options);
this.addPort(serialport);
this.refreshPorts();
}
this.getPort = function (name) {
return Serial.nameToPortRegistry.getItem(name);
}
this.addPort = function (serialport) {
if (this.portToNameRegistry.hasKey(serialport)) {
return;
}
let name = '';
for (let i = 1; i <= 20; i++) {
name = `serial${i}`;
if (Serial.nameToPortRegistry.hasKey(name)) {
continue;
}
break;
}
Serial.portToNameRegistry.register(serialport, name);
Serial.nameToPortRegistry.register(name, serialport);
}
this.removePort = function (serialport) {
if (!Serial.portToNameRegistry.hasKey(serialport)) {
return;
}
const name = Serial.portToNameRegistry.getItem(serialport);
if (!name) {
return;
}
Serial.portToNameRegistry.unregister(serialport);
Serial.nameToPortRegistry.unregister(name);
}
this.addEventsListener = function () {
navigator?.serial?.addEventListener('connect', (event) => {
this.addPort(event.target);
this.refreshPorts();
});
navigator?.serial?.addEventListener('disconnect', (event) => {
this.removePort(event.target);
this.refreshPorts();
});
}
this.init = function () {
navigator?.serial?.getPorts().then((serialports) => {
for (let serialport of serialports) {
this.addPort(serialport);
}
});
this.addEventsListener();
}
}
#serialport_ = null;
#keepReading_ = null;
#reader_ = null;
#writer_ = null;
#stringTemp_ = '';
constructor(port) {
super(port);
this.#serialport_ = WebSerialPort.getPort(port);
}
#addEventsListener_() {
this.#addReadEventListener_();
}
async #addReadEventListener_() {
const { readable } = this.#serialport_;
while (readable && this.#keepReading_) {
this.#reader_ = readable.getReader();
try {
while (true) {
const { value, done } = await this.#reader_.read();
value !== undefined && this.onBuffer(value);
if (done) {
break;
}
}
} catch (error) {
this.#keepReading_ = false;
Debug.error(error);
} finally {
this.#reader_ && this.#reader_.releaseLock();
await this.close();
}
}
}
async open(baud) {
const portsName = Serial.getCurrentPortsName();
const currentPortName = this.getPortName();
if (!portsName.includes(currentPortName)) {
throw Error('no device available');
return;
}
if (this.isOpened()) {
return;
}
baud = baud ?? this.getBaudRate();
this.#serialport_ = WebSerialPort.getPort(currentPortName);
await this.#serialport_.open({ baudRate: baud });
super.open(baud);
super.setBaudRate(baud);
this.#keepReading_ = true;
this.onOpen();
this.#addEventsListener_();
}
async #waitForUnlock_(timeout) {
while (
(this.#serialport_.readable && this.#serialport_.readable.locked) ||
(this.#serialport_.writable && this.#serialport_.writable.locked)
) {
await this.sleep(timeout);
}
}
async close() {
if (!this.isOpened()) {
return;
}
super.close();
if (this.#serialport_.readable?.locked) {
this.#keepReading_ = false;
await this.#reader_?.cancel();
}
await this.#waitForUnlock_(400);
this.#reader_ = undefined;
await this.#serialport_.close();
this.#stringTemp_ = '';
this.onClose(1);
}
async setBaudRate(baud) {
if (!this.isOpened()
|| this.getRawBaudRate() === baud
|| !this.baudRateIsLegal(baud)) {
return;
}
await this.close();
await this.open(baud);
}
async sendString(str) {
const buffer = this.encode(str);
return this.sendBuffer(buffer);
}
async sendBuffer(buffer) {
const { writable } = this.#serialport_;
const writer = writable.getWriter();
if (buffer.constructor.name !== 'Uint8Array') {
buffer = new Uint8Array(buffer);
}
try {
await writer.write(buffer);
writer.releaseLock();
} catch (error) {
writer.releaseLock();
throw Error(error);
}
}
async setDTRAndRTS(dtr, rts) {
if (!this.isOpened()) {
return;
}
await this.#serialport_.setSignals({
dataTerminalReady: dtr,
requestToSend: rts
});
super.setDTRAndRTS(dtr, rts);
}
async setDTR(dtr) {
if (!this.isOpened()) {
return;
}
await this.#serialport_.setSignals({
dataTerminalReady: dtr
});
super.setDTR(dtr);
}
async setRTS(rts) {
if (!this.isOpened()) {
return;
}
await this.#serialport_.setSignals({
requestToSend: rts
});
super.setRTS(rts);
}
getVID() {
const info = this.#serialport_.getInfo();
return info.usbVendorId;
}
getPID() {
const info = this.#serialport_.getInfo();
return info.usbProductId;
}
onBuffer(buffer) {
super.onBuffer(buffer);
for (let i = 0; i < buffer.length; i++) {
super.onByte(buffer[i]);
}
const string = this.decodeBuffer(buffer);
if (!string) {
return;
}
for (let char of string) {
super.onChar(char);
if (['\r', '\n'].includes(char)) {
super.onString(this.#stringTemp_);
this.#stringTemp_ = '';
} else {
this.#stringTemp_ += char;
}
}
}
}
Web.SerialPort = WebSerialPort;
});

View File

@@ -0,0 +1,350 @@
goog.loadJs('web', () => {
goog.require('Mixly.Serial');
goog.require('Mixly.Registry');
goog.require('Mixly.Config');
goog.require('Mixly.Web');
goog.provide('Mixly.Web.USBMini');
const {
Serial,
Registry,
Config,
Web
} = Mixly;
const { SELECTED_BOARD } = Config;
class USBMini extends Serial {
static {
this.type = 'usb';
this.serialNumberToNameRegistry = new Registry();
this.getConfig = function () {
return Serial.getConfig();
}
this.getSelectedPortName = function () {
return Serial.getSelectedPortName();
}
this.getCurrentPortsName = function () {
return Serial.getCurrentPortsName();
}
this.refreshPorts = function () {
Serial.refreshPorts();;
}
this.requestPort = async function () {
let options = SELECTED_BOARD?.web?.devices?.usb;
if (!options || typeof(options) !== 'object') {
options = {
filters: []
};
}
const device = await navigator.usb.requestDevice(options);
this.addPort(device);
this.refreshPorts();
}
this.getPort = function (name) {
return Serial.nameToPortRegistry.getItem(name);
}
this.addPort = function (device) {
if (Serial.portToNameRegistry.hasKey(device)) {
return;
}
const { serialNumber } = device;
let name = this.serialNumberToNameRegistry.getItem(serialNumber);
if (!name) {
for (let i = 1; i <= 20; i++) {
name = `usb${i}`;
if (Serial.nameToPortRegistry.hasKey(name)) {
continue;
}
break;
}
this.serialNumberToNameRegistry.register(serialNumber, name);
}
Serial.portToNameRegistry.register(device, name);
Serial.nameToPortRegistry.register(name, device);
}
this.removePort = function (device) {
if (!Serial.portToNameRegistry.hasKey(device)) {
return;
}
const name = Serial.portToNameRegistry.getItem(device);
if (!name) {
return;
}
Serial.portToNameRegistry.unregister(device);
Serial.nameToPortRegistry.unregister(name);
}
this.addEventsListener = function () {
navigator?.usb?.addEventListener('connect', (event) => {
this.addPort(event.device);
this.refreshPorts();
});
navigator?.usb?.addEventListener('disconnect', (event) => {
event.device.onclose && event.device.onclose();
this.removePort(event.device);
this.refreshPorts();
});
}
this.init = function () {
navigator?.usb?.getDevices().then((devices) => {
for (let device of devices) {
this.addPort(device);
}
});
this.addEventsListener();
}
}
#device_ = null;
#keepReading_ = null;
#reader_ = null;
#serialPolling_ = false;
#stringTemp_ = '';
#defaultClass_ = 0xFF;
#defaultConfiguration_ = 1;
#endpointIn_ = null;
#endpointOut_ = null;
#ctrlInterfaceNumber_ = -1;
#dataInterfaceNumber_ = -1;
#dataLength_ = 64;
constructor(port) {
super(port);
this.#device_ = USBMini.getPort(port);
}
#addEventsListener_() {
this.#addReadEventListener_();
}
#addReadEventListener_() {
this.#reader_ = this.#startSerialRead_();
this.#device_.onclose = () => {
if (!this.isOpened()) {
return;
}
super.close();
this.#stringTemp_ = '';
this.onClose(1);
}
}
async #read_() {
let result;
if (this.#endpointIn_) {
result = await this.#device_.transferIn(this.#endpointIn_, 64);
} else {
result = await this.#device_.controlTransferIn({
requestType: 'class',
recipient: 'interface',
request: 0x01,
value: 0x100,
index: this.#dataInterfaceNumber_
}, 64);
}
return result?.data;
}
async #write_(data) {
if (this.#endpointOut_) {
await this.#device_.transferOut(this.#endpointOut_, data);
} else {
await this.#device_.controlTransferOut({
requestType: 'class',
recipient: 'interface',
request: 0x09,
value: 0x200,
index: this.#dataInterfaceNumber_
}, data);
}
}
async #startSerialRead_(serialDelay = 1) {
this.#serialPolling_ = true;
try {
while (this.#serialPolling_ ) {
const data = await this.#read_();
if (data !== undefined) {
const numberArray = Array.prototype.slice.call(new Uint8Array(data.buffer));
this.onBuffer(numberArray);
}
await new Promise(resolve => setTimeout(resolve, serialDelay));
}
} catch (_) {}
}
async open(baud) {
const portsName = Serial.getCurrentPortsName();
const currentPortName = this.getPortName();
if (!portsName.includes(currentPortName)) {
throw new Error('no device available');
}
if (this.isOpened()) {
return;
}
baud = baud ?? this.getBaudRate();
this.#device_ = USBMini.getPort(currentPortName);
await this.#device_.open();
await this.#device_.selectConfiguration(this.#defaultConfiguration_);
const interfaces = this.#device_.configuration.interfaces.filter(iface => {
return iface.alternates[0].interfaceClass === this.#defaultClass_;
});
let selectedInterface = interfaces.find(iface => iface.alternates[0].endpoints.length > 0);
if (!selectedInterface) {
selectedInterface = interfaces[0];
}
this.#dataInterfaceNumber_ = selectedInterface.interfaceNumber;
const { endpoints } = selectedInterface.alternates[0];
for (const endpoint of endpoints) {
if (endpoint.direction === 'in') {
this.#endpointIn_ = endpoint.endpointNumber;
} else if (endpoint.direction === 'out') {
this.#endpointOut_ = endpoint.endpointNumber;
}
}
try {
await this.#device_.claimInterface(0);
this.#ctrlInterfaceNumber_ = 0;
} catch (_) {
this.#ctrlInterfaceNumber_ = -1;
}
await this.#device_.claimInterface(this.#dataInterfaceNumber_);
super.open(baud);
await this.setBaudRate(baud);
this.onOpen();
this.#addEventsListener_();
}
async close() {
if (!this.isOpened()) {
return;
}
this.#serialPolling_ = false;
super.close();
await this.#device_.close();
if (this.#reader_) {
await this.#reader_;
}
this.#device_ = null;
this.onClose(1);
}
async setBaudRate(baud) {
if (!this.isOpened() || this.getRawBaudRate() === baud) {
return;
}
if (this.#ctrlInterfaceNumber_ !== -1) {
const dwDTERate = new Uint8Array([
baud & 0xFF,
(baud >> 8) & 0xFF,
(baud >> 16) & 0xFF,
(baud >> 24) & 0xFF
]);
const bCharFormat = 0x00;
const bParityType = 0x00;
const bDataBits = 0x08;
const lineCoding = new Uint8Array([
...dwDTERate,
bCharFormat,
bParityType,
bDataBits
]);
await this.#device_.controlTransferOut({
requestType: 'class',
recipient: 'interface',
request: 0x20,
value: 0x0000,
index: this.#ctrlInterfaceNumber_
}, lineCoding);
}
await super.setBaudRate(baud);
}
async sendString(str) {
const buffer = this.encode(str);
return this.sendBuffer(buffer);
}
async sendBuffer(buffer) {
if (buffer.constructor.name !== 'Uint8Array') {
buffer = new Uint8Array(buffer);
}
const len = Math.ceil(buffer.length / this.#dataLength_);
for (let i = 0; i < len; i++) {
const start = i * this.#dataLength_;
const end = Math.min((i + 1) * this.#dataLength_, buffer.length);
const writeBuffer = buffer.slice(start, end);
await this.#write_(writeBuffer)
await this.sleep(5);
}
}
async setDTRAndRTS(dtr, rts) {
if (!this.isOpened()
|| (this.getDTR() === dtr && this.getRTS() === rts)) {
return;
}
if (this.#ctrlInterfaceNumber_ !== -1) {
await this.#device_.controlTransferOut({
requestType: 'class',
recipient: 'interface',
request: 0x22,
value: dtr | (rts << 1),
index: this.#ctrlInterfaceNumber_
});
}
await super.setDTRAndRTS(dtr, rts);
}
async setDTR(dtr) {
return this.setDTRAndRTS(dtr, this.getRTS());
}
async setRTS(rts) {
return this.setDTRAndRTS(this.getDTR(), rts);
}
getVID() {
return this.#device_.vendorId;
}
getPID() {
return this.#device_.productId;
}
onBuffer(buffer) {
super.onBuffer(buffer);
for (let i = 0; i < buffer.length; i++) {
super.onByte(buffer[i]);
}
const string = this.decodeBuffer(buffer);
if (!string) {
return;
}
for (let char of string) {
super.onChar(char);
if (['\r', '\n'].includes(char)) {
super.onString(this.#stringTemp_);
this.#stringTemp_ = '';
} else {
this.#stringTemp_ += char;
}
}
}
}
Web.USBMini = USBMini;
});

View File

@@ -0,0 +1,258 @@
goog.loadJs('web', () => {
goog.require('DAPjs');
goog.require('Mixly.Serial');
goog.require('Mixly.Registry');
goog.require('Mixly.Config');
goog.require('Mixly.Web');
goog.provide('Mixly.Web.USB');
const {
Serial,
Registry,
Config,
Web
} = Mixly;
const { SELECTED_BOARD } = Config;
class USB extends Serial {
static {
this.type = 'usb';
this.serialNumberToNameRegistry = new Registry();
this.getConfig = function () {
return Serial.getConfig();
}
this.getSelectedPortName = function () {
return Serial.getSelectedPortName();
}
this.getCurrentPortsName = function () {
return Serial.getCurrentPortsName();
}
this.refreshPorts = function () {
Serial.refreshPorts();;
}
this.requestPort = async function () {
let options = SELECTED_BOARD?.web?.devices?.usb;
if (!options || typeof(options) !== 'object') {
options = {
filters: []
};
}
const device = await navigator.usb.requestDevice(options);
this.addPort(device);
this.refreshPorts();
}
this.getPort = function (name) {
return Serial.nameToPortRegistry.getItem(name);
}
this.addPort = function (device) {
if (Serial.portToNameRegistry.hasKey(device)) {
return;
}
const { serialNumber } = device;
let name = this.serialNumberToNameRegistry.getItem(serialNumber);
if (!name) {
for (let i = 1; i <= 20; i++) {
name = `usb${i}`;
if (Serial.nameToPortRegistry.hasKey(name)) {
continue;
}
break;
}
this.serialNumberToNameRegistry.register(serialNumber, name);
}
Serial.portToNameRegistry.register(device, name);
Serial.nameToPortRegistry.register(name, device);
}
this.removePort = function (device) {
if (!Serial.portToNameRegistry.hasKey(device)) {
return;
}
const name = Serial.portToNameRegistry.getItem(device);
if (!name) {
return;
}
Serial.portToNameRegistry.unregister(device);
Serial.nameToPortRegistry.unregister(name);
}
this.addEventsListener = function () {
navigator?.usb?.addEventListener('connect', (event) => {
this.addPort(event.device);
this.refreshPorts();
});
navigator?.usb?.addEventListener('disconnect', (event) => {
event.device.onclose && event.device.onclose();
this.removePort(event.device);
this.refreshPorts();
});
}
this.init = function () {
navigator?.usb?.getDevices().then((devices) => {
for (let device of devices) {
this.addPort(device);
}
});
this.addEventsListener();
}
}
#device_ = null;
#webUSB_ = null;
#dapLink_ = null;
#keepReading_ = null;
#reader_ = null;
#writer_ = null;
#stringTemp_ = '';
constructor(port) {
super(port);
this.#device_ = USB.getPort(port);
}
#addEventsListener_() {
this.#addReadEventListener_();
}
#addReadEventListener_() {
this.#reader_ = this.#startSerialRead_();
this.#device_.onclose = () => {
if (!this.isOpened()) {
return;
}
super.close();
this.#stringTemp_ = '';
this.onClose(1);
}
}
async #startSerialRead_(serialDelay = 1, autoConnect = false) {
this.#dapLink_.serialPolling = true;
while (this.#dapLink_.serialPolling) {
const data = await this.#dapLink_.serialRead();
if (data !== undefined) {
const numberArray = Array.prototype.slice.call(new Uint8Array(data));
this.onBuffer(numberArray);
}
await new Promise(resolve => setTimeout(resolve, serialDelay));
}
}
async open(baud) {
const portsName = Serial.getCurrentPortsName();
const currentPortName = this.getPortName();
if (!portsName.includes(currentPortName)) {
throw new Error('no device available');
}
if (this.isOpened()) {
return;
}
baud = baud ?? this.getBaudRate();
this.#device_ = USB.getPort(currentPortName);
this.#webUSB_ = new DAPjs.WebUSB(this.#device_);
this.#dapLink_ = new DAPjs.DAPLink(this.#webUSB_);
await this.#dapLink_.connect();
super.open(baud);
await this.setBaudRate(baud);
this.onOpen();
this.#addEventsListener_();
}
async close() {
if (!this.isOpened()) {
return;
}
super.close();
this.#dapLink_.stopSerialRead();
if (this.#reader_) {
await this.#reader_;
}
await this.#dapLink_.disconnect();
this.#dapLink_ = null;
await this.#webUSB_.close();
this.#webUSB_ = null;
await this.#device_.close();
this.#device_ = null;
this.onClose(1);
}
async setBaudRate(baud) {
if (!this.isOpened() || this.getRawBaudRate() === baud) {
return;
}
await this.#dapLink_.setSerialBaudrate(baud);
await super.setBaudRate(baud);
}
async sendString(str) {
return this.#dapLink_.serialWrite(str);
}
async sendBuffer(buffer) {
if (buffer.constructor.name !== 'Uint8Array') {
buffer.unshift(buffer.length);
buffer = new Uint8Array(buffer);
}
await this.#dapLink_.send(132, buffer);
}
async setDTRAndRTS(dtr, rts) {
if (!this.isOpened()
|| (this.getDTR() === dtr && this.getRTS() === rts)) {
return;
}
await super.setDTRAndRTS(dtr, rts);
}
async setDTR(dtr) {
return this.setDTRAndRTS(dtr, this.getRTS());
}
async setRTS(rts) {
return this.setDTRAndRTS(this.getDTR(), rts);
}
getVID() {
return this.#device_.vendorId;
}
getPID() {
return this.#device_.productId;
}
onBuffer(buffer) {
super.onBuffer(buffer);
for (let i = 0; i < buffer.length; i++) {
super.onByte(buffer[i]);
}
const string = this.decodeBuffer(buffer);
if (!string) {
return;
}
for (let char of string) {
super.onChar(char);
if (['\r', '\n'].includes(char)) {
super.onString(this.#stringTemp_);
this.#stringTemp_ = '';
} else {
this.#stringTemp_ += char;
}
}
}
}
Web.USB = USB;
});

View File

@@ -0,0 +1,6 @@
goog.loadJs('web', () => {
goog.require('Mixly');
goog.provide('Mixly.Web');
});