1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-23 00:05:22 +03:00

Feat/vite build (#3292)

feat: vite based web build

* configure vitejs build
* add web serial option
This commit is contained in:
Tomas Chmelevskij 2023-08-31 21:01:06 +03:00 committed by GitHub
parent 172cb0bee9
commit ff4b62d7e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 723 additions and 54 deletions

View file

@ -280,6 +280,11 @@ function processPackage(done, gitRevision, isReleaseBuild) {
// remove gulp-appdmg from the package.json we're going to write // remove gulp-appdmg from the package.json we're going to write
delete pkg.optionalDependencies['gulp-appdmg']; delete pkg.optionalDependencies['gulp-appdmg'];
// keeping this package in `package.json` for some reason
// breaks the nwjs builds. This is not really needed for
// nwjs nor it's imported anywhere at runtime ¯\_(ツ)_/¯
// this probably can go away if we fully move to pwa.
delete pkg.dependencies['@vitejs/plugin-vue2'];
pkg.gitRevision = gitRevision; pkg.gitRevision = gitRevision;
if (!isReleaseBuild) { if (!isReleaseBuild) {

View file

@ -6,6 +6,7 @@
"main": "main.html", "main": "main.html",
"chromium-args": "--disable-features=nw2", "chromium-args": "--disable-features=nw2",
"scripts": { "scripts": {
"dev": "vite",
"start": "run-script-os", "start": "run-script-os",
"start:default": "NODE_ENV=development NW_PRE_ARGS=--load-extension='./node_modules/nw-vue-devtools-prebuilt/extension' gulp debug", "start:default": "NODE_ENV=development NW_PRE_ARGS=--load-extension='./node_modules/nw-vue-devtools-prebuilt/extension' gulp debug",
"start:windows": "set NODE_ENV=development && set NW_PRE_ARGS=--load-extension='./node_modules/nw-vue-devtools-prebuilt/extension' && gulp debug", "start:windows": "set NODE_ENV=development && set NW_PRE_ARGS=--load-extension='./node_modules/nw-vue-devtools-prebuilt/extension' && gulp debug",
@ -54,6 +55,7 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0", "@fortawesome/fontawesome-free": "^5.13.0",
"@panter/vue-i18next": "^0.15.2", "@panter/vue-i18next": "^0.15.2",
"@vitejs/plugin-vue2": "^2.2.0",
"bonjour": "^3.5.0", "bonjour": "^3.5.0",
"crypto-es": "^1.2.7", "crypto-es": "^1.2.7",
"d3": "^7.8.2", "d3": "^7.8.2",
@ -136,7 +138,8 @@
"appdmg": "^0.6.4" "appdmg": "^0.6.4"
}, },
"resolutions": { "resolutions": {
"jquery": "3.6.3" "jquery": "3.6.3",
"libxmljs2": "0.32.0"
}, },
"husky": { "husky": {
"hooks": { "hooks": {

342
src/index.html Normal file
View file

@ -0,0 +1,342 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="author" content="cTn"/>
<script type="module" src="./src/js/utils/common.js"></script>
<script type="module" src="./src/js/browserMain.js"></script>
<title></title>
</head>
<body>
<div id="main-wrapper">
<div id="background"></div>
<div id="side_menu_swipe"></div>
<div class="headerbar">
<div id="menu_btn">
<em class="fas fa-bars"></em>
</div>
<betaflight-logo
:configurator-version="CONFIGURATOR.getDisplayVersion()"
:firmware-version="FC.CONFIG.flightControllerVersion"
:firmware-id="FC.CONFIG.flightControllerIdentifier"
:hardware-id="FC.CONFIG.hardwareName"
></betaflight-logo>
<div id="port-picker">
<div id="port-override-option">
<label for="port-override">
<span i18n="portOverrideText">Port:</span>
<input id="port-override" type="text" value="/dev/rfcomm0"/>
</label>
</div>
<div id="firmware-virtual-option">
<div class="dropdown dropdown-dark">
<select id="firmware-version-dropdown" class="dropdown-select" i18n_title="virtualMSPVersion"></select>
</div>
</div>
<div id="portsinput" style="display: none">
<div class="dropdown dropdown-dark">
<select class="dropdown-select" id="port" i18n_title="firmwareFlasherManualPort">
<option value="loading" i18n="serialPortLoading">Loading</option>
<!-- port list gets generated here -->
</select>
</div>
<div id="auto-connect-and-baud">
<div id="auto-connect-switch">
<label>
<input class="auto_connect togglesmall" type="checkbox"/>
<span class="auto_connect" i18n="autoConnect"></span>
</label>
</div>
<div id="baudselect">
<div class="dropdown dropdown-dark">
<select class="dropdown-select" id="baud" i18n_title="firmwareFlasherBaudRate">
<option value="1000000">1000000</option>
<option value="500000">500000</option>
<option value="250000">250000</option>
<option value="115200" selected="selected">115200</option>
<option value="57600">57600</option>
<option value="38400">38400</option>
<option value="28800">28800</option>
<option value="19200">19200</option>
<option value="14400">14400</option>
<option value="9600">9600</option>
<option value="4800">4800</option>
<option value="2400">2400</option>
<option value="1200">1200</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="header-wrapper">
<div id="quad-status_wrapper">
<battery-icon
:voltage="FC.ANALOG.voltage"
:vbatmincellvoltage="FC.BATTERY_CONFIG.vbatmincellvoltage"
:vbatmaxcellvoltage="FC.BATTERY_CONFIG.vbatmaxcellvoltage"
:vbatwarningcellvoltage="FC.BATTERY_CONFIG.vbatwarningcellvoltage"
:batteryState="FC.BATTERY_STATE?.batteryState"
>
</battery-icon>
<battery-legend
:voltage="FC.ANALOG.voltage"
:vbatmaxcellvoltage="FC.BATTERY_CONFIG.vbatmaxcellvoltage"
></battery-legend>
<div class="bottomStatusIcons">
<div class="armedicon cf_tip" i18n_title="mainHelpArmed"></div>
<div class="failsafeicon cf_tip" i18n_title="mainHelpFailsafe"></div>
<div class="linkicon cf_tip" i18n_title="mainHelpLink"></div>
</div>
</div>
<div id="sensor-status" class="sensor_state mode-connected">
<ul>
<li class="gyro" i18n_title="sensorStatusGyro">
<div class="gyroicon" i18n="sensorStatusGyroShort"></div>
</li>
<li class="accel" i18n_title="sensorStatusAccel">
<div class="accicon" i18n="sensorStatusAccelShort"></div>
</li>
<li class="mag" i18n_title="sensorStatusMag">
<div class="magicon" i18n="sensorStatusMagShort"></div>
</li>
<li class="baro" i18n_title="sensorStatusBaro">
<div class="baroicon" i18n="sensorStatusBaroShort"></div>
</li>
<li class="gps" i18n_title="sensorStatusGPS">
<div class="gpsicon" i18n="sensorStatusGPSShort"></div>
</li>
<li class="sonar" i18n_title="sensorStatusSonar">
<div class="sonaricon" i18n="sensorStatusSonarShort"></div>
</li>
</ul>
</div>
<div id="dataflash_wrapper_global">
<div class="noflash_global" align="center" i18n="sensorDataFlashNotFound"></div>
<ul class="dataflash-contents_global">
<li class="dataflash-free_global">
<div class="legend" i18n="sensorDataFlashFreeSpace"></div>
</li>
</ul>
<div id="expertMode" align="center">
<label>
<input name="expertModeCheckbox" class="togglesmall" type="checkbox"/>
<span i18n="expertMode"></span>
</label>
</div>
</div>
</div>
<div id="header_btns">
<div class="open_firmware_flasher" id="flashbutton">
<div class="firmware_b">
<a class="flash disabled" href="#"></a>
</div>
<div class="flash_state" i18n="flashTab"></div>
</div>
<div class="connect_controls" id="connectbutton">
<div class="connect_b">
<a class="connect disabled" href="#"></a>
</div>
<div class="connect_state" i18n="connect"></div>
</div>
</div>
<div id="reveal_btn">
<em class="fas fa-ellipsis-v"></em>
</div>
</div>
<div id="log">
<div class="logswitch">
<a href="#" id="showlog" i18n="logActionShow"></a>
</div>
<div id="scrollicon"></div>
<div class="wrapper"></div>
</div>
<div id="tab-content-container">
<div class="tab_container">
<betaflight-logo
:configurator-version="CONFIGURATOR.getDisplayVersion()"
:firmware-version="FC.CONFIG.flightControllerVersion"
:firmware-id="FC.CONFIG.flightControllerIdentifier"
:hardware-id="FC.CONFIG.hardwareName"
></betaflight-logo>
<div id="tabs">
<ul class="mode-disconnected">
<li class="tab_landing" id="tab_landing"><a href="#" i18n="tabLanding" class="tabicon ic_welcome" i18n_title="tabLanding"></a>
</li>
<li class="tab_changelog"><a href="#" class="tabicon ic_help" i18n="tabChangelog"></a></li>
<li class="tab_privacy_policy"><a href="#" class="tabicon ic_help" i18n="tabPrivacyPolicy"></a></li>
<li class="tab_help" id="tab_help"><a href="#" i18n="tabHelp" class="tabicon ic_help"
i18n_title="tabHelp"></a></li>
<li class="tab_options" id="tab_options"><a href="#" i18n="tabOptions" class="tabicon ic_config" i18n_title="tabOptions"></a>
</li>
<li class="tab_firmware_flasher" id="tabFirmware"><a href="#" i18n="tabFirmwareFlasher" class="tabicon ic_flasher"
i18n_title="tabFirmwareFlasher"></a></li>
</ul>
<ul class="mode-connected">
<li class="tab_setup"><a href="#" i18n="tabSetup" class="tabicon ic_setup" i18n_title="tabSetup"></a></li>
<li class="tab_setup_osd"><a href="#" i18n="tabSetupOSD" class="tabicon ic_setup" i18n_title="tabSetupOSD"></a></li>
<li class="tab_ports"><a href="#" i18n="tabPorts" class="tabicon ic_ports" i18n_title="tabPorts"></a></li>
<li class="tab_configuration"><a href="#" i18n="tabConfiguration" class="tabicon ic_config"
i18n_title="tabConfiguration"></a></li>
<li class="tab_power"><a href="#" i18n="tabPower" class="tabicon ic_power"
i18n_title="tabPower"></a></li>
<li class="tab_failsafe"><a href="#" i18n="tabFailsafe" class="tabicon ic_failsafe"
i18n_title="tabFailsafe"></a></li>
<li class="tab_presets">
<a href="#" i18n="tabPresets" class="tabicon ic_wizzard" i18n_title="tabPresets"></a>
</li>
<li class="tab_pid_tuning"><a href="#" i18n="tabPidTuning" class="tabicon ic_pid"
i18n_title="tabPidTuning"></a></li>
<li class="tab_receiver"><a href="#" i18n="tabReceiver" class="tabicon ic_rx" i18n_title="tabReceiver"></a></li>
<li class="tab_auxiliary"><a href="#" i18n="tabAuxiliary" class="tabicon ic_modes" i18n_title="tabAuxiliary"></a>
</li>
<li class="tab_adjustments"><a href="#" i18n="tabAdjustments" class="tabicon ic_adjust"
i18n_title="tabAdjustments"></a></li>
<li class="tab_servos"><a href="#" i18n="tabServos" class="tabicon ic_servo" i18n_title="tabServos"></a></li>
<li class="tab_gps"><a href="#" i18n="tabGPS" class="tabicon ic_gps" i18n_title="tabGPS"></a></li>
<li class="tab_motors"><a href="#" i18n="tabMotorTesting" class="tabicon ic_motor" i18n_title="tabMotorTesting"></a>
</li>
<li class="tab_osd"><a href="#" i18n="tabOsd" class="tabicon ic_osd" i18n_title="tabOsd"></a></li>
<li class="tab_vtx"><a href="#" i18n="tabVtx" class="tabicon ic_vtx" i18n_title="tabVtx"></a></li>
<li class="tab_transponder"><a href="#" i18n="tabTransponder" class="tabicon ic_transponder"
i18n_title="tabTransponder"></a></li>
<li class="tab_led_strip"><a href="#" i18n="tabLedStrip" class="tabicon ic_led" i18n_title="tabLedStrip"></a>
</li>
<li class="tab_sensors"><a href="#" i18n="tabRawSensorData" class="tabicon ic_sensors"
i18n_title="tabRawSensorData"></a></li>
<li class="tab_logging"><a href="#" i18n="tabLogging" class="tabicon ic_log"
i18n_title="tabLogging"></a></li>
<li class="tab_onboard_logging"><a href="#" i18n="tabOnboardLogging" class="tabicon ic_data"
i18n_title="tabOnboardLogging"></a></li>
<!-- spare icons
<li class=""><a href="#"class="tabicon ic_mission">Mission (spare icon)</a></li>
<li class=""><a href="#"class="tabicon ic_advanced">Advanced (spare icon)</a></li>
<li class=""><a href="#"class="tabicon ic_wizzard">Wizzard (spare icon)</a></li>
-->
</ul>
<ul class="mode-connected mode-connected-cli">
<li class="tab_cli">
<a href="#" i18n="tabCLI" class="tabicon ic_cli" i18n_title="tabCLI"></a>
</li>
</ul>
</div>
<div class="clear-both"></div>
</div>
<div id="content"></div>
</div>
<status-bar
:port-usage-down="PortUsage.port_usage_down"
:port-usage-up="PortUsage.port_usage_up"
:packet-error="MSP.packet_error"
:i2c-error="FC.CONFIG.i2cError"
:cycle-time="FC.CONFIG.cycleTime"
:cpu-load="FC.CONFIG.cpuload"
:configurator-version="CONFIGURATOR.getDisplayVersion()"
:firmware-version="FC.CONFIG.flightControllerVersion"
:firmware-id="FC.CONFIG.flightControllerIdentifier"
:hardware-id="FC.CONFIG.hardwareName"
></status-bar>
<div id="cache">
<div class="data-loading">
<p>Waiting for data ...</p>
</div>
</div>
</div>
<dialog class="dialogConfiguratorUpdate">
<h3 i18n="noticeTitle"></h3>
<div class="content">
<div class="dialogConfiguratorUpdate-content" style="margin-top: 10px"></div>
</div>
<div class="buttons">
<a href="#" class="dialogConfiguratorUpdate-websitebtn regular-button" i18n="configuratorUpdateWebsite"></a>
<a href="#" class="dialogConfiguratorUpdate-closebtn regular-button" i18n="close"></a>
</div>
</dialog>
<dialog class="dialogConnectWarning">
<h3 i18n="warningTitle"></h3>
<div class="content">
<div class="dialogConnectWarning-content" style="margin-top: 10px"></div>
</div>
<div class="buttons">
<a href="#" class="dialogConnectWarning-closebtn regular-button" i18n="close"></a>
</div>
</dialog>
<dialog id="dialogResetToCustomDefaults">
<h3 i18n="noticeTitle"></h3>
<div class="content">
<div id="dialogResetToCustomDefaults-content" i18n="resetToCustomDefaultsDialog"></div>
</div>
<div>
<span class="buttons">
<a href="#" id="dialogResetToCustomDefaults-acceptbtn" class="regular-button" i18n="resetToCustomDefaultsAccept"></a>
</span>
<span class="buttons">
<a href="#" id="dialogResetToCustomDefaults-cancelbtn" class="regular-button" i18n="cancel"></a>
</span>
</div>
</dialog>
<dialog id="dialogReportProblems">
<h3 i18n="warningTitle"></h3>
<div class="content">
<div id="dialogReportProblems-header" i18n="reportProblemsDialogHeader"></div>
<ul id="dialogReportProblems-list">
<!-- List elements added dynamically -->
</ul>
<div id="dialogReportProblems-footer" i18n="reportProblemsDialogFooter"></div>
</div>
<div>
<span class="buttons">
<a href="#" id="dialogReportProblems-closebtn" class="regular-button" i18n="close"></a>
</span>
</div>
</dialog>
<ul class="hidden"> <!-- Sonar says so -->
<li id="dialogReportProblems-listItemTemplate" class="dialogReportProblems-listItem"></li>
</ul>
<dialog class="dialogError">
<h3 i18n="errorTitle"></h3>
<div class="content">
<div class="dialogError-content" style="margin-top: 10px"></div>
</div>
<div class="buttons">
<a href="#" class="dialogError-closebtn regular-button" i18n="close"></a>
</div>
</dialog>
<dialog class="dialogYesNo">
<h3 class="dialogYesNoTitle"></h3>
<div class="dialogYesNoContent"></div>
<div class="buttons">
<a href="#" class="dialogYesNo-yesButton regular-button"></a>
<a href="#" class="dialogYesNo-noButton regular-button"></a>
</div>
</dialog>
<dialog class="dialogWait">
<div class="data-loading"></div>
<h3 class="dialogWaitTitle"></h3>
<div class="buttons">
<a href="#" class="dialogWait-cancelButton regular-button" i18n="cancel"></a>
</div>
</dialog>
<dialog class="dialogInformation">
<h3 class="dialogInformationTitle"></h3>
<div class="dialogInformationContent"></div>
<div class="buttons">
<a href="#" class="dialogInformation-confirmButton regular-button"></a>
</div>
</dialog>
<!-- CORDOVA_INCLUDE cordova.js -->
</body>
</html>

54
src/js/browserMain.js Normal file
View file

@ -0,0 +1,54 @@
import '../js/jqueryPlugins';
import "jbox/dist/jBox.min.css";
import "../../libraries/jquery.nouislider.min.css";
import "../../libraries/jquery.nouislider.pips.min.css";
import "../../libraries/flightindicators.css";
import "../css/theme.css";
import "../css/main.less";
import "../css/tabs/static_tab.less";
import "../css/tabs/landing.less";
import "../css/tabs/setup.less";
import "../css/tabs/help.less";
import "../css/tabs/ports.less";
import "../css/tabs/configuration.less";
import "../css/tabs/pid_tuning.less";
import "../css/tabs/receiver.less";
import "../css/tabs/servos.less";
import "../css/tabs/gps.less";
import "../css/tabs/motors.less";
import "../css/tabs/led_strip.less";
import "../css/tabs/sensors.less";
import "../css/tabs/cli.less";
import "../tabs/presets/presets.less";
// TODO: for some reason can't resolve these less files
// import "../tabs/presets/TitlePanel/PresetTitlePanel.less";
import "../tabs/presets/DetailedDialog/PresetsDetailedDialog.less";
// TODO: for some reason can't resolve these less files
// import "../tabs/presets/SourcesDialog/SourcesDialog.less";
// import "../tabs/presets/SourcesDialog/SourcePanel.less";
import "../css/tabs/logging.less";
import "../css/tabs/onboard_logging.less";
import "../css/tabs/firmware_flasher.less";
import "../css/tabs/adjustments.less";
import "../css/tabs/auxiliary.less";
import "../css/tabs/failsafe.less";
import "../css/tabs/osd.less";
import "../css/tabs/vtx.less";
import "../css/tabs/power.less";
import "../css/tabs/transponder.less";
import "../css/tabs/privacy_policy.less";
import "../css/tabs/options.less";
import "../css/opensans_webfontkit/fonts.css";
import "../css/dropdown-lists/css/style_lists.css";
import "switchery-latest/dist/switchery.min.css";
import "../css/switchery_custom.less";
import "@fortawesome/fontawesome-free/css/all.css";
import "../components/MotorOutputReordering/Styles.css";
import "../css/select2_custom.less";
import "select2/dist/css/select2.min.css";
import "multiple-select/dist/multiple-select.min.css";
import "../components/EscDshotDirection/Styles.css";
import "../css/dark-theme.less";
import "./main";

View file

@ -50,10 +50,12 @@ function useGlobalNodeFunctions() {
} }
function readConfiguratorVersionMetadata() { function readConfiguratorVersionMetadata() {
if (GUI.isNWJS()) {
const manifest = chrome.runtime.getManifest(); const manifest = chrome.runtime.getManifest();
CONFIGURATOR.productName = manifest.productName; CONFIGURATOR.productName = manifest.productName;
CONFIGURATOR.version = manifest.version; CONFIGURATOR.version = manifest.version;
CONFIGURATOR.gitRevision = manifest.gitRevision; CONFIGURATOR.gitRevision = manifest.gitRevision;
}
} }
function cleanupLocalStorage() { function cleanupLocalStorage() {

View file

@ -49,6 +49,9 @@ MdnsDiscovery.initialize = function() {
reinit(); reinit();
} else { } else {
if(typeof require === 'undefined') {
return 'not implemented';
}
const bonjour = require('bonjour')(); const bonjour = require('bonjour')();
self.mdnsBrowser.browser = bonjour.find({ type: 'http' }, service => { self.mdnsBrowser.browser = bonjour.find({ type: 'http' }, service => {

View file

@ -1,6 +1,9 @@
import GUI from "./gui.js"; import GUI from "./gui.js";
import CONFIGURATOR from "./data_storage.js"; import CONFIGURATOR from "./data_storage.js";
import serial from "./serial.js"; import serialNWJS from "./serial.js";
import serialWeb from "./webSerial.js";
const serial = import.meta.env ? serialWeb : serialNWJS;
const MSP = { const MSP = {
symbols: { symbols: {
@ -66,7 +69,7 @@ const MSP = {
return; return;
} }
const data = new Uint8Array(readInfo.data); const data = new Uint8Array(readInfo.data ?? readInfo);
for (const chunk of data) { for (const chunk of data) {
switch (this.state) { switch (this.state) {
@ -304,14 +307,14 @@ const MSP = {
return bufferOut; return bufferOut;
}, },
send_message(code, data, callback_sent, callback_msp, doCallbackOnError) { send_message(code, data, callback_sent, callback_msp, doCallbackOnError) {
if (code === undefined || !serial.connectionId || CONFIGURATOR.virtualMode) { const connected = import.meta.env ? serial.connected : serial.connectionId;
if (code === undefined || !connected || CONFIGURATOR.virtualMode) {
if (callback_msp) { if (callback_msp) {
callback_msp(); callback_msp();
} }
return false; return false;
} }
// Check if request already exists in the queue
let requestExists = false; let requestExists = false;
for (const instance of this.callbacks) { for (const instance of this.callbacks) {
if (instance.code === code) { if (instance.code === code) {
@ -321,6 +324,12 @@ const MSP = {
} }
} }
if (import.meta.env && (code === undefined || !serial.connectionInfo)) {
console.log('ERROR: code undefined or no connectionId');
return false;
}
const bufferOut = code <= 254 ? this.encode_message_v1(code, data) : this.encode_message_v2(code, data); const bufferOut = code <= 254 ? this.encode_message_v1(code, data) : this.encode_message_v2(code, data);
const obj = { const obj = {
@ -374,7 +383,7 @@ const MSP = {
}, },
callbacks_cleanup() { callbacks_cleanup() {
for (const callback of this.callbacks) { for (const callback of this.callbacks) {
clearInterval(callback.timer); clearTimeout(callback.timer);
} }
this.callbacks = []; this.callbacks = [];

View file

@ -29,6 +29,12 @@ const PortHandler = new function () {
PortHandler.initialize = function () { PortHandler.initialize = function () {
const self = this; const self = this;
// currently web build doesn't need port handler,
// so just bail out.
if (import.meta.env) {
return 'not implemented';
}
const portPickerElementSelector = "div#port-picker #port"; const portPickerElementSelector = "div#port-picker #port";
self.portPickerElement = $(portPickerElementSelector); self.portPickerElement = $(portPickerElementSelector);
self.selectList = document.querySelector(portPickerElementSelector); self.selectList = document.querySelector(portPickerElementSelector);

View file

@ -6,6 +6,7 @@ import { gui_log } from "./gui_log";
import inflection from "inflection"; import inflection from "inflection";
import PortHandler from "./port_handler"; import PortHandler from "./port_handler";
import { checkChromeRuntimeError } from "./utils/common"; import { checkChromeRuntimeError } from "./utils/common";
import { serialDevices } from './serial_devices';
import $ from 'jquery'; import $ from 'jquery';
const serial = { const serial = {
@ -23,14 +24,7 @@ const serial = {
transmitting: false, transmitting: false,
outputBuffer: [], outputBuffer: [],
serialDevices: [ serialDevices,
{'vendorId': 1027, 'productId': 24577}, // FT232R USB UART
{'vendorId': 1155, 'productId': 22336}, // STM Electronics Virtual COM Port
{'vendorId': 4292, 'productId': 60000}, // CP210x
{'vendorId': 4292, 'productId': 60001}, // CP210x
{'vendorId': 4292, 'productId': 60002}, // CP210x
{'vendorId': 0x2e3c, 'productId': 0x5740}, // AT32 VCP
],
connect: function (path, options, callback) { connect: function (path, options, callback) {
const self = this; const self = this;

View file

@ -11,7 +11,6 @@ import MSPCodes from "./msp/MSPCodes";
import PortUsage from "./port_usage"; import PortUsage from "./port_usage";
import PortHandler from "./port_handler"; import PortHandler from "./port_handler";
import CONFIGURATOR, { API_VERSION_1_45, API_VERSION_1_46 } from "./data_storage"; import CONFIGURATOR, { API_VERSION_1_45, API_VERSION_1_46 } from "./data_storage";
import serial from "./serial";
import UI_PHONES from "./phones_ui"; import UI_PHONES from "./phones_ui";
import { bit_check } from './bit.js'; import { bit_check } from './bit.js';
import { sensor_status, have_sensor } from "./sensor_helpers"; import { sensor_status, have_sensor } from "./sensor_helpers";
@ -25,6 +24,11 @@ import CryptoES from "crypto-es";
import $ from 'jquery'; import $ from 'jquery';
import BuildApi from "./BuildApi"; import BuildApi from "./BuildApi";
import serialNWJS from "./serial.js";
import serialWeb from "./webSerial.js";
const serial = import.meta.env ? serialWeb : serialNWJS;
let mspHelper; let mspHelper;
let connectionTimestamp; let connectionTimestamp;
let clicks = false; let clicks = false;
@ -64,10 +68,21 @@ export function initializeSerialBackend() {
GUI.updateManualPortVisibility(); GUI.updateManualPortVisibility();
}); });
$('div.connect_controls a.connect').click(function () {
if (!GUI.connect_lock) { // GUI control overrides the user control
const toggleStatus = function() { $("div.connect_controls a.connect").on('click', function () {
const selectedPort = $('div#port-picker #port option:selected');
let portName;
if (selectedPort.data().isManual) {
portName = $('#port-override').val();
} else {
portName = String($('div#port-picker #port').val());
}
if (!GUI.connect_lock) {
// GUI control overrides the user control
const toggleStatus = function () {
clicks = !clicks; clicks = !clicks;
}; };
@ -76,13 +91,6 @@ export function initializeSerialBackend() {
const selected_baud = parseInt($('div#port-picker #baud').val()); const selected_baud = parseInt($('div#port-picker #baud').val());
const selectedPort = $('div#port-picker #port option:selected'); const selectedPort = $('div#port-picker #port option:selected');
let portName;
if (selectedPort.data().isManual) {
portName = $('#port-override').val();
} else {
portName = String($('div#port-picker #port').val());
}
if (selectedPort.data().isDFU) { if (selectedPort.data().isDFU) {
$('select#baud').hide(); $('select#baud').hide();
} else if (portName !== '0') { } else if (portName !== '0') {
@ -94,16 +102,27 @@ export function initializeSerialBackend() {
$('div#port-picker #port, div#port-picker #baud, div#port-picker #delay').prop('disabled', true); $('div#port-picker #port, div#port-picker #baud, div#port-picker #delay').prop('disabled', true);
$('div.connect_controls div.connect_state').text(i18n.getMessage('connecting')); $('div.connect_controls div.connect_state').text(i18n.getMessage('connecting'));
const baudRate = parseInt($('div#port-picker #baud').val());
if (selectedPort.data().isVirtual) { if (selectedPort.data().isVirtual) {
CONFIGURATOR.virtualMode = true; CONFIGURATOR.virtualMode = true;
CONFIGURATOR.virtualApiVersion = $('#firmware-version-dropdown :selected').val(); CONFIGURATOR.virtualApiVersion = $('#firmware-version-dropdown :selected').val();
serial.connect('virtual', {}, onOpenVirtual); serial.connect('virtual', {}, onOpenVirtual);
} else if (import.meta.env) {
serial.addEventListener('connect', (event) => {
onOpen(event.detail);
toggleStatus();
});
serial.connect({ baudRate });
} else { } else {
serial.connect(portName, {bitrate: selected_baud}, onOpen); serial.connect(
portName,
{ bitrate: selected_baud },
onOpen,
);
toggleStatus();
} }
toggleStatus();
} else { } else {
if ($('div#flashbutton a.flash_state').hasClass('active') && $('div#flashbutton a.flash').hasClass('active')) { if ($('div#flashbutton a.flash_state').hasClass('active') && $('div#flashbutton a.flash').hasClass('active')) {
$('div#flashbutton a.flash_state').removeClass('active'); $('div#flashbutton a.flash_state').removeClass('active');
@ -287,7 +306,11 @@ function onOpen(openInfo) {
result = getConfig('expertMode')?.expertMode ?? false; result = getConfig('expertMode')?.expertMode ?? false;
$('input[name="expertModeCheckbox"]').prop('checked', result).trigger('change'); $('input[name="expertModeCheckbox"]').prop('checked', result).trigger('change');
if(import.meta.env) {
serial.addEventListener('receive', (e) => read_serial(e.detail.buffer));
} else {
serial.onReceive.addListener(read_serial); serial.onReceive.addListener(read_serial);
}
setConnectionTimeout(); setConnectionTimeout();
FC.resetState(); FC.resetState();
mspHelper = new MspHelper(); mspHelper = new MspHelper();

15
src/js/serial_devices.js Normal file
View file

@ -0,0 +1,15 @@
export const serialDevices = [
{ vendorId: 1027, productId: 24577 }, // FT232R USB UART
{ vendorId: 1155, productId: 22336 }, // STM Electronics Virtual COM Port
{ vendorId: 4292, productId: 60000 }, // CP210x
{ vendorId: 4292, productId: 60001 }, // CP210x
{ vendorId: 4292, productId: 60002 }, // CP210x
{ vendorId: 0x2e3c, productId: 0x5740 }, // AT32 VCP
];
export const webSerialDevices = serialDevices.map(
({ vendorId, productId }) => ({
usbVendorId: vendorId,
usbProductId: productId,
}),
);

View file

@ -1,6 +1,10 @@
import windowWatcherUtil from "../utils/window_watchers"; import windowWatcherUtil from "../utils/window_watchers";
import $ from 'jquery'; import $ from 'jquery';
// This is a hack to get the i18n var of the parent, but the i18n.localizePage not works
// It seems than when node opens a new window, the module "context" is different, so the i18n var is not initialized
const i18n = opener.i18n;
const css_dark = [ const css_dark = [
'/css/dark-theme.css', '/css/dark-theme.css',
]; ];
@ -201,10 +205,6 @@ $(window).on("mouseup", function(e) {
windowWatcherUtil.bindWatchers(window, watchers); windowWatcherUtil.bindWatchers(window, watchers);
// This is a hack to get the i18n var of the parent, but the i18n.localizePage not works
// It seems than when node opens a new window, the module "context" is different, so the i18n var is not initialized
const i18n = opener.i18n;
localizePage(); localizePage();
localizeAxisNames(); localizeAxisNames();

156
src/js/webSerial.js Normal file
View file

@ -0,0 +1,156 @@
import { webSerialDevices } from "./serial_devices";
async function* streamAsyncIterable(stream) {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
return;
}
yield value;
}
} finally {
reader.releaseLock();
}
}
class WebSerial extends EventTarget {
constructor() {
super();
this.connected = false;
this.openRequested = false;
this.openCanceled = false;
this.transmitting = false;
this.connectionInfo = null;
this.bitrate = 0;
this.bytesSent = 0;
this.bytesReceived = 0;
this.failed = 0;
this.logHead = "SERIAL: ";
this.port = null;
this.reader = null;
this.writer = null;
this.connect = this.connect.bind(this);
}
async connect(options) {
this.openRequested = true;
this.port = await navigator.serial.requestPort({
filters: webSerialDevices,
});
await this.port.open(options);
const connectionInfo = this.port.getInfo();
this.connectionInfo = connectionInfo;
this.writer = this.port.writable.getWriter();
if (connectionInfo && !this.openCanceled) {
this.connected = true;
this.connectionId = connectionInfo.connectionId;
this.bitrate = options.baudrate;
this.bytesReceived = 0;
this.bytesSent = 0;
this.failed = 0;
this.openRequested = false;
this.addEventListener("receive", (info) => {
this.bytesReceived += info.detail.byteLength;
});
console.log(
`${this.logHead} Connection opened with ID: ${connectionInfo.connectionId}, Baud: ${options.baudRate}`,
);
this.dispatchEvent(
new CustomEvent("connect", { detail: connectionInfo }),
);
// Check if we need the helper function or could polyfill
// the stream async iterable interface:
// https://web.dev/streams/#asynchronous-iteration
for await (let value of streamAsyncIterable(this.port.readable)) {
this.dispatchEvent(
new CustomEvent("receive", { detail: value }),
);
}
} else if (connectionInfo && this.openCanceled) {
this.connectionId = connectionInfo.connectionId;
console.log(
`${this.logHead} Connection opened with ID: ${connectionInfo.connectionId}, but request was canceled, disconnecting`,
);
// some bluetooth dongles/dongle drivers really doesn't like to be closed instantly, adding a small delay
setTimeout(() => {
this.openRequested = false;
this.openCanceled = false;
this.disconnect(() => {
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
});
}, 150);
} else if (this.openCanceled) {
console.log(
`${this.logHead} Connection didn't open and request was canceled`,
);
this.openRequested = false;
this.openCanceled = false;
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
} else {
this.openRequested = false;
console.log(`${this.logHead} Failed to open serial port`);
this.dispatchEvent(new CustomEvent("connect", { detail: false }));
}
}
async disconnect() {
this.connected = false;
if (this.port) {
this.transmitting = false;
if (this.writer) {
await this.writer.close();
this.writer = null;
}
try {
await this.port.close();
this.port = null;
console.log(
`${this.logHead}Connection with ID: ${this.connectionId} closed, Sent: ${this.bytesSent} bytes, Received: ${this.bytesReceived} bytes`,
);
this.connectionId = false;
this.bitrate = 0;
this.dispatchEvent(new CustomEvent("disconnect"));
} catch (error) {
console.error(error);
console.error(
`${this.logHead}Failed to close connection with ID: ${this.connectionId} closed, Sent: ${this.bytesSent} bytes, Received: ${this.bytesReceived} bytes`,
);
}
} else {
this.openCanceled = true;
}
}
async send(data) {
// TODO: previous serial implementation had a buffer of 100, do we still need it with streams?
if (this.writer) {
await this.writer.write(data);
this.bytesSent += data.byteLength;
} else {
console.error(
`${this.logHead}Failed to send data, serial port not open`,
);
}
return {
bytesSent: this.bytesSent,
};
}
}
export default new WebSerial();

View file

@ -1,13 +1,65 @@
/// <reference types="vitest" /> /// <reference types="vitest" />
import { defineConfig } from 'vite'; import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue2";
import path from "node:path";
import { readFileSync } from "node:fs";
/**
* This is plugin to work around the file structure required nwjs.
* In future this can be dropped if we restructure folder structure
* to be more web friendly.
* @returns {import('vite').Plugin}
*/
function serveLocalesPlugin() {
return {
name: "serve-locales",
configureServer(server) {
return () => {
server.middlewares.use((req, res, next) => {
if (req.url.startsWith("/locales/")) {
// Extract the file path from the URL
const filePath = req.url.replace(/^\/locales\//, "");
const absolutePath = path.resolve(
process.cwd(),
"locales",
filePath,
);
try {
const fileContents = readFileSync(
absolutePath,
"utf-8",
);
res.end(fileContents);
} catch (e) {
// If file not found or any other error, pass to the next middleware
next();
}
} else {
next();
}
});
};
},
};
}
export default defineConfig({ export default defineConfig({
test: { test: {
// NOTE: this is a replacement location for karma tests. // NOTE: this is a replacement location for karma tests.
// moving forward we should colocate tests with the // moving forward we should colocate tests with the
// code they test. // code they test.
include: ['test/**/*.test.{js,mjs,cjs}'], include: ["test/**/*.test.{js,mjs,cjs}"],
environment: 'jsdom', environment: "jsdom",
setupFiles: ['test/setup.js'], setupFiles: ["test/setup.js"],
root: '.',
},
plugins: [vue(), serveLocalesPlugin()],
root: "./src",
resolve: {
alias: {
"/src": path.resolve(process.cwd(), "src"),
'vue': path.resolve(__dirname, 'node_modules/vue/dist/vue.esm.js'),
},
}, },
}); });

View file

@ -1653,10 +1653,10 @@
dependencies: dependencies:
cross-spawn "^7.0.1" cross-spawn "^7.0.1"
"@mapbox/node-pre-gyp@^1.0.5": "@mapbox/node-pre-gyp@^1.0.10":
version "1.0.9" version "1.0.11"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa"
integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw== integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==
dependencies: dependencies:
detect-libc "^2.0.0" detect-libc "^2.0.0"
https-proxy-agent "^5.0.0" https-proxy-agent "^5.0.0"
@ -2982,6 +2982,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@vitejs/plugin-vue2@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue2/-/plugin-vue2-2.2.0.tgz#7453207197d6ac2b7023cedc7133b142c604c356"
integrity sha512-1km7zEuZ/9QRPvzXSjikbTYGQPG86Mq1baktpC4sXqsXlb02HQKfi+fl8qVS703JM7cgm24Ga9j+RwKmvFn90A==
"@vue/compiler-core@3.2.33": "@vue/compiler-core@3.2.33":
version "3.2.33" version "3.2.33"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.33.tgz#e915d59cce85898f5c5cfebe4c09e539278c3d59" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.33.tgz#e915d59cce85898f5c5cfebe4c09e539278c3d59"
@ -10385,14 +10390,14 @@ levn@~0.3.0:
prelude-ls "~1.1.2" prelude-ls "~1.1.2"
type-check "~0.3.2" type-check "~0.3.2"
libxmljs2@^0.29.0: libxmljs2@0.32.0, libxmljs2@^0.29.0:
version "0.29.0" version "0.32.0"
resolved "https://registry.yarnpkg.com/libxmljs2/-/libxmljs2-0.29.0.tgz#4d44efc7998d9c0d938e174226cf625a8ebf630d" resolved "https://registry.yarnpkg.com/libxmljs2/-/libxmljs2-0.32.0.tgz#408e35c54a5ad5e0366bc8299ba20ed35224d0d9"
integrity sha512-g9PvujoUGRHBp2R7Z49pKklMdAucayepGSJajfpzl+JlGRO1lHfWWH1KQcwqoylO524G9Mu9pO74ZaQ11fi0/w== integrity sha512-DuvKfSQZeUzw0A4UWZXfcBpr3VqlcJY1b3aw99PxTiX3T5t1rEO4gSpobNrP9S74LIhyDKaAs/lphuErV+n+7w==
dependencies: dependencies:
"@mapbox/node-pre-gyp" "^1.0.5" "@mapbox/node-pre-gyp" "^1.0.10"
bindings "~1.5.0" bindings "~1.5.0"
nan "~2.15.0" nan "~2.17.0"
lie@~3.3.0: lie@~3.3.0:
version "3.3.0" version "3.3.0"
@ -11386,10 +11391,10 @@ nan@^2.12.1, nan@^2.4.0:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
nan@~2.15.0: nan@~2.17.0:
version "2.15.0" version "2.17.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
nanoid@^2.1.0: nanoid@^2.1.0:
version "2.1.4" version "2.1.4"