diff --git a/common/modules/mixly-modules/common/app.js b/common/modules/mixly-modules/common/app.js index e0d3690d..bf5a0347 100644 --- a/common/modules/mixly-modules/common/app.js +++ b/common/modules/mixly-modules/common/app.js @@ -239,6 +239,9 @@ class App extends Component { id: 'command-burn-btn', displayText: Msg.Lang['nav.btn.burn'], preconditionFn: () => { + if (!goog.isElectron && !goog.hasSocketServer && Serial.type !== 'serialport') { + return false; + } return SELECTED_BOARD?.nav?.burn; }, callback: () => BU.initBurn(), diff --git a/common/modules/mixly-modules/deps.json b/common/modules/mixly-modules/deps.json index 75d16e54..2676d792 100644 --- a/common/modules/mixly-modules/deps.json +++ b/common/modules/mixly-modules/deps.json @@ -1639,6 +1639,7 @@ "Mixly.Env", "Mixly.Web.SerialPort", "Mixly.Web.USB", + "Mixly.Web.USBMini", "Mixly.Web.HID" ], "provide": [ @@ -1656,6 +1657,17 @@ "Mixly.Web.SerialPort" ] }, + { + "path": "/web/usb-mini.js", + "require": [ + "Mixly.Serial", + "Mixly.Registry", + "Mixly.Web" + ], + "provide": [ + "Mixly.Web.USBMini" + ] + }, { "path": "/web/usb.js", "require": [ diff --git a/common/modules/mixly-modules/web/burn-upload.js b/common/modules/mixly-modules/web/burn-upload.js index af2a316e..fe801ce4 100644 --- a/common/modules/mixly-modules/web/burn-upload.js +++ b/common/modules/mixly-modules/web/burn-upload.js @@ -603,6 +603,7 @@ BU.uploadByUSB = async (portName) => { resize: false, closeBtn: 0, success: async function (layero, index) { + $('#mixly-loader-btn').hide(); try { let prevPercent = 0; await partialFlashing.flashAsync(new BoardId(0x9900), FSWrapper, progress => { @@ -636,6 +637,10 @@ BU.uploadByUSB = async (portName) => { } BU.burning = false; BU.uploading = false; + }, + end: function () { + $('#mixly-loader-btn').css('display', 'inline-block'); + $('#mixly-loader-div').css('display', 'none'); } }); } @@ -653,7 +658,7 @@ BU.uploadWithAmpy = (portName) => { const editor = mainWorkspace.getEditorsManager().getActive(); let useBuffer = true, dataLength = 256; if (BOARD.web.com === 'usb') { - useBuffer = false; + useBuffer = true; dataLength = 64; } else if (BOARD.web.com === 'hid') { useBuffer = true; @@ -667,6 +672,7 @@ BU.uploadWithAmpy = (portName) => { resize: false, closeBtn: 0, success: async function (layero, index) { + $('#mixly-loader-btn').hide(); const serial = new Serial(portName); const ampy = new Ampy(serial, useBuffer, dataLength); const code = editor.getCode(); @@ -727,6 +733,10 @@ BU.uploadWithAmpy = (portName) => { } BU.burning = false; BU.uploading = false; + }, + end: function () { + $('#mixly-loader-btn').css('display', 'inline-block'); + $('#mixly-loader-div').css('display', 'none'); } }); } diff --git a/common/modules/mixly-modules/web/hid.js b/common/modules/mixly-modules/web/hid.js index 0fc6ce8b..63daa974 100644 --- a/common/modules/mixly-modules/web/hid.js +++ b/common/modules/mixly-modules/web/hid.js @@ -14,6 +14,7 @@ const { class WebHID extends Serial { static { + this.type = 'hid'; this.portToNameRegistry = new Registry(); this.nameToPortRegistry = new Registry(); diff --git a/common/modules/mixly-modules/web/serial.js b/common/modules/mixly-modules/web/serial.js index 29e72ce0..6a9d7c19 100644 --- a/common/modules/mixly-modules/web/serial.js +++ b/common/modules/mixly-modules/web/serial.js @@ -4,6 +4,7 @@ goog.require('Mixly.Config'); goog.require('Mixly.Env'); 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'); @@ -12,6 +13,7 @@ const { Config, Env, Web } = Mixly; const { SerialPort, USB, + USBMini, HID } = Web; @@ -25,7 +27,11 @@ if (goog.platform() === 'win32' && goog.fullPlatform() !== 'win10') { } else if (BOARD?.web?.devices?.serial) { Device = SerialPort; } else if (BOARD?.web?.devices?.usb) { - Device = USB; + if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) { + Device = USB; + } else { + Device = USBMini; + } } } else if (goog.platform() === 'android') { Device = USB; @@ -33,7 +39,11 @@ if (goog.platform() === 'win32' && goog.fullPlatform() !== 'win10') { if (BOARD?.web?.devices?.serial) { Device = SerialPort; } else if (BOARD?.web?.devices?.usb) { - Device = USB; + if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) { + Device = USB; + } else { + Device = USBMini; + } } else if (BOARD?.web?.devices?.hid) { Device = HID; } diff --git a/common/modules/mixly-modules/web/serialport.js b/common/modules/mixly-modules/web/serialport.js index 0b571ebf..cd7f4b84 100644 --- a/common/modules/mixly-modules/web/serialport.js +++ b/common/modules/mixly-modules/web/serialport.js @@ -14,6 +14,7 @@ const { class WebSerialPort extends Serial { static { + this.type = 'serialport'; this.portToNameRegistry = new Registry(); this.nameToPortRegistry = new Registry(); diff --git a/common/modules/mixly-modules/web/usb-mini.js b/common/modules/mixly-modules/web/usb-mini.js new file mode 100644 index 00000000..65635bca --- /dev/null +++ b/common/modules/mixly-modules/web/usb-mini.js @@ -0,0 +1,292 @@ +goog.loadJs('web', () => { + +goog.require('Mixly.Serial'); +goog.require('Mixly.Registry'); +goog.require('Mixly.Web'); +goog.provide('Mixly.Web.USBMini'); + +const { + Serial, + Registry, + Web +} = Mixly; + +class USBMini extends Serial { + static { + this.type = 'usb'; + this.portToNameRegistry = new Registry(); + this.serialNumberToNameRegistry = new Registry(); + this.nameToPortRegistry = new Registry(); + + this.getConfig = function () { + return Serial.getConfig(); + } + + this.getSelectedPortName = function () { + return Serial.getSelectedPortName(); + } + + this.getCurrentPortsName = function () { + return Serial.getCurrentPortsName(); + } + + this.refreshPorts = function () { + let portsName = []; + for (let name of this.nameToPortRegistry.keys()) { + portsName.push({ name }); + } + Serial.renderSelectBox(portsName); + } + + this.requestPort = async function () { + const device = await navigator.usb.requestDevice({ + filters: [] + }); + this.addPort(device); + this.refreshPorts(); + } + + this.getPort = function (name) { + return this.nameToPortRegistry.getItem(name); + } + + this.addPort = function (device) { + if (this.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 (this.nameToPortRegistry.hasKey(name)) { + continue; + } + break; + } + this.serialNumberToNameRegistry.register(serialNumber, name); + } + this.portToNameRegistry.register(device, name); + this.nameToPortRegistry.register(name, device); + } + + this.removePort = function (device) { + if (!this.portToNameRegistry.hasKey(device)) { + return; + } + const name = this.portToNameRegistry.getItem(device); + if (!name) { + return; + } + this.portToNameRegistry.unregister(device); + this.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; + #interfaceNumber_ = 0; + constructor(port) { + super(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.#interfaceNumber_ + }, 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.#interfaceNumber_ + }, 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('无可用串口'); + } + 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.#interfaceNumber_ = 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; + } + } + await this.#device_.claimInterface(this.#interfaceNumber_); + await this.setBaudRate(baud); + super.open(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.getBaudRate() === baud) { + return; + } + await super.setBaudRate(baud); + } + + async sendString(str) { + const buffer = this.encode(str); + return this.sendBuffer(buffer); + } + + async sendBuffer(buffer) { + if (typeof buffer.unshift === 'function') { + // buffer.unshift(buffer.length); + buffer = new Uint8Array(buffer).buffer; + } + return this.#write_(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); + } + + 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; + +}); \ No newline at end of file diff --git a/common/modules/mixly-modules/web/usb.js b/common/modules/mixly-modules/web/usb.js index 04cc89d2..53f73f51 100644 --- a/common/modules/mixly-modules/web/usb.js +++ b/common/modules/mixly-modules/web/usb.js @@ -14,6 +14,7 @@ const { class USB extends Serial { static { + this.type = 'usb'; this.portToNameRegistry = new Registry(); this.serialNumberToNameRegistry = new Registry(); this.nameToPortRegistry = new Registry();