feat: sync mixly root files and common folder
This commit is contained in:
292
mixly/common/modules/mixly-modules/web/ampy-fs.js
Normal file
292
mixly/common/modules/mixly-modules/web/ampy-fs.js
Normal 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;
|
||||
|
||||
});
|
||||
429
mixly/common/modules/mixly-modules/web/ampy.js
Normal file
429
mixly/common/modules/mixly-modules/web/ampy.js
Normal 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;
|
||||
|
||||
});
|
||||
185
mixly/common/modules/mixly-modules/web/bluetooth.js
Normal file
185
mixly/common/modules/mixly-modules/web/bluetooth.js
Normal 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));
|
||||
}
|
||||
|
||||
});
|
||||
738
mixly/common/modules/mixly-modules/web/burn-upload.js
Normal file
738
mixly/common/modules/mixly-modules/web/burn-upload.js
Normal 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();
|
||||
}
|
||||
|
||||
});
|
||||
47
mixly/common/modules/mixly-modules/web/file-tree.js
Normal file
47
mixly/common/modules/mixly-modules/web/file-tree.js
Normal 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;
|
||||
|
||||
});
|
||||
218
mixly/common/modules/mixly-modules/web/file.js
Normal file
218
mixly/common/modules/mixly-modules/web/file.js
Normal 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();
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
161
mixly/common/modules/mixly-modules/web/fs.js
Normal file
161
mixly/common/modules/mixly-modules/web/fs.js
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
247
mixly/common/modules/mixly-modules/web/hid.js
Normal file
247
mixly/common/modules/mixly-modules/web/hid.js
Normal 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;
|
||||
|
||||
});
|
||||
531
mixly/common/modules/mixly-modules/web/kflash.js
Normal file
531
mixly/common/modules/mixly-modules/web/kflash.js
Normal file
File diff suppressed because one or more lines are too long
231
mixly/common/modules/mixly-modules/web/serial-transport.js
Normal file
231
mixly/common/modules/mixly-modules/web/serial-transport.js
Normal 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;
|
||||
|
||||
});
|
||||
215
mixly/common/modules/mixly-modules/web/serial.js
Normal file
215
mixly/common/modules/mixly-modules/web/serial.js
Normal 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;
|
||||
|
||||
});
|
||||
279
mixly/common/modules/mixly-modules/web/serialport.js
Normal file
279
mixly/common/modules/mixly-modules/web/serialport.js
Normal 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;
|
||||
|
||||
});
|
||||
350
mixly/common/modules/mixly-modules/web/usb-mini.js
Normal file
350
mixly/common/modules/mixly-modules/web/usb-mini.js
Normal 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;
|
||||
|
||||
});
|
||||
258
mixly/common/modules/mixly-modules/web/usb.js
Normal file
258
mixly/common/modules/mixly-modules/web/usb.js
Normal 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;
|
||||
|
||||
});
|
||||
6
mixly/common/modules/mixly-modules/web/web.js
Normal file
6
mixly/common/modules/mixly-modules/web/web.js
Normal file
@@ -0,0 +1,6 @@
|
||||
goog.loadJs('web', () => {
|
||||
|
||||
goog.require('Mixly');
|
||||
goog.provide('Mixly.Web');
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user