sync production -> git

This commit is contained in:
recrof 2025-03-09 16:29:43 +01:00
parent c548c898f3
commit 56892c5ef7
6 changed files with 685 additions and 390 deletions

View file

@ -1,13 +1,16 @@
import "./lib/beer.min.js";
import { createApp, reactive, ref, nextTick } from "./lib/vue.min.js";
import { createApp, reactive, ref, nextTick, watch, computed } from "./lib/vue.min.js";
import { Dfu } from "./lib/dfu.js";
import { ESPLoader, Transport, HardReset } from "./lib/esp32.js";
import { SerialConsole } from './lib/console.js';
const res = await fetch('./config.json');
const config = await res.json();
const configRes = await fetch('./config.json');
const config = await configRes.json();
const githubRes = await fetch('/releases');
const github = await githubRes.json();
const commandReference = {
'set freq ': 'Set frequency {Mhz}',
'time ': 'Set time {epoch-secs}',
'erase': 'Erase filesystem',
'advert': 'Send Advertisment packet',
@ -19,6 +22,7 @@ const commandReference = {
'log stop': 'Stop packet logging to file system',
'log erase': 'Erase the packet logs from file system',
'ver': 'Show device version',
'set freq ': 'Set frequency {Mhz}',
'set af ': 'Set Air-time factor',
'set tx ': 'Set Tx power {dBm}',
'set repeat ': 'Set repeater mode {on|off}',
@ -27,8 +31,53 @@ const commandReference = {
'set name ': 'Set advertisement name',
'set lat': 'Set the advertisement map latitude',
'set lon': 'Set the advertisement map longitude',
'get freq ': 'Get frequency (Mhz)',
'get af': 'Get Air-time factor',
'get tx': 'Get Tx power (dBm)',
'get repeat': 'Get repeater mode',
'get advert.interval': 'Get advert rebroadcast interval (minutes)',
'get name': 'Get advertisement name',
'get lat': 'Get the advertisement map latitude',
'get lon': 'Get the advertisement map longitude',
};
function getGithubReleases(roleType, files) {
const versions = {};
for(const [fileType, [startsWith, endsWith]] of Object.entries(files)) {
for(const versionType of github) {
if(versionType.type !== roleType) { continue }
const version = versions[versionType.version] ??= {
notes: versionType.notes,
files: []
};
for(const file of versionType.files) {
if(!(file.name.startsWith(startsWith) && file.name.endsWith(endsWith))) { continue }
version.files.push({
type: fileType,
name: file.url,
title: file.name,
})
}
}
}
return versions;
}
function addGithubFiles() {
for(const device of config.device) {
for(const firmware of device.firmware) {
const gDef = firmware.github;
if(!gDef?.files) { continue }
firmware.version = getGithubReleases(gDef.type, gDef.files);
}
}
return config;
}
console.log(addGithubFiles());
function setup() {
const consoleEditBox = ref();
const consoleWindow = ref();
@ -36,11 +85,23 @@ function setup() {
const selected = reactive({
device: null,
firmware: null,
version: null,
wipe: false,
port: null
port: null,
});
const getRoleFwValue = (firmware, key) => {
return firmware[key] || config.role[firmware.role][key] || '';
}
const getSelFwValue = (key) => {
const fwVersion = selected.firmware.version[selected.version];
return fwVersion ? fwVersion[key] || '' : '';
}
const flashing = reactive({
supported: 'Serial' in window,
instance: null,
active: false,
percentage: 0,
@ -68,14 +129,47 @@ function setup() {
location.reload();
}
const getFirmwarePath = (file) => {
return file.name.startsWith('/') ? file.name : `${config.staticPath}/${file.name}`;
}
const firmwareHasData = (firmware) => {
const firstVersion = Object.keys(firmware.version)[0];
if(!firstVersion) return false;
return firmware.version[firstVersion].files.length > 0;
}
const stepBack = () => {
if(selected.device && selected.firmware) {
if(selected.firmware.version[selected.version].customFile) {
selected.firmware = null;
selected.device = null;
return
}
selected.firmware = null;
return;
}
if(selected.device) {
selected.device = null;
}
}
watch(() => selected.firmware, (firmware) => {
if(firmware == null) return;
selected.version = Object.keys(firmware.version)[0];
});
const flasherCleanup = async () => {
const port = selected.port;
flashing.active = false;
flashing.log = '';
flashing.error = '';
flashing.dfuComplete = false;
flashing.percentage = 0;
selected.firmware = null;
selected.version = null;
selected.wipe = false;
selected.device = null;
if(flashing.instance instanceof ESPLoader) {
@ -118,17 +212,49 @@ function setup() {
flashing.dfuComplete = true;
}
const customFirmwareLoad = async(ev) => {
const firmwareFile = ev.target.files[0];
const type = firmwareFile.name.endsWith('.bin') ? 'esp32' : 'nrf52';
selected.device = {
name: 'Custom device',
type,
};
selected.firmware = {
icon: 'unknown_document',
title: firmwareFile.name,
version: {},
}
selected.version = firmwareFile.name;
selected.firmware.version[selected.version] = {
customFile: true,
files: [{ type: 'flash', file: firmwareFile }]
}
}
const flashDevice = async() => {
const device = selected.device;
const firmware = selected.firmware;
const flashFile = firmware.files.find(f => f.type === 'flash');
const firmware = selected.firmware.version[selected.version];
let flashFile;
flashFile = firmware.files.find(f => f.type === 'flash');
if(!flashFile) {
alert('Cannot find configuration for flash file! please report this to Discord.')
flasherCleanup();
return;
}
const url = `${config.basePath}/${flashFile.name}`;
const resp = await fetch(url);
console.log({flashFile, instanceFile: flashFile instanceof File});
if(flashFile.file) {
flashFile = flashFile.file;
} else {
const url = getFirmwarePath(flashFile);
console.log('downloading: ' + url);
const resp = await fetch(url);
flashFile = await resp.blob();
}
const port = selected.port = await navigator.serial.requestPort({});
if(device.type === 'esp32') {
@ -138,9 +264,15 @@ function setup() {
try {
const reader = new FileReader();
fileData = await new Promise(async (resolve) => {
fileData = await new Promise((resolve, reject) => {
reader.addEventListener('error', () => {
reader.abort();
reject(new DOMException('Problem parsing input file.'));
});
reader.addEventListener('load', () => resolve(reader.result));
reader.readAsBinaryString(await resp.blob());
reader.readAsBinaryString(flashFile);
});
}
catch(e) {
@ -163,13 +295,8 @@ function setup() {
data: fileData,
address: 0
}],
reportProgress: async (fileIndex, written, total) => {
reportProgress: async (_, written, total) => {
flashing.percentage = (written / total) * 100;
// we're done with this file
if (written === total) {
return;
}
},
};
@ -204,11 +331,10 @@ function setup() {
else if(device.type === 'nrf52') {
const dfu = flashing.instance = new Dfu(port, selected.wipe);
const zipFile = await resp.blob();
flashing.active = true;
try {
await dfu.dfuUpdate(zipFile, async (progress) => {
await dfu.dfuUpdate(flashFile, async (progress) => {
flashing.percentage = progress;
});
}
@ -225,173 +351,11 @@ function setup() {
config, selected, flashing,
flashDevice, flasherCleanup, dfuMode,
serialCon, openSerialCon, sendCommand, closeSerialCon,
refresh, commandReference
refresh, commandReference,
stepBack,
customFirmwareLoad, getFirmwarePath, getSelFwValue, getRoleFwValue,
firmwareHasData
}
}
const template = `
<div class="flash-container">
<div v-if="flashing.active">
<header>
<nav>
<i>developer_board</i>
<span class="small">{{ selected.device.name }}</span>
<i>chevron_right</i>
<i>{{ selected.firmware.icon }}</i>
<span class="small">{{ selected.firmware.title }}</span>
</nav>
</header>
<article v-if="flashing.error">
<div class="row">
<div class="max">
<h6>Flashing failed!</h6>
<p><span>{{ flashing.error }}</span></p>
<p><button @click="refresh()">Retry</button></p>
</div>
</div>
</article>
<article v-else>
<div class="row">
<div class="max" v-if="flashing.percentage < 100">
<h6><progress class="circle small"></progress> Flashing...</h6>
<p>Please do not disconnect the device</p>
</div>
<div class="max" v-else=>
<h6>Flashing complete!</h6>
<p>
<button @click="flasherCleanup()">Close</button>
</p>
</div>
</div>
<div class="autoscroller">
<pre class="term" v-if="flashing.terminal">{{ flashing.terminal }}</pre>
</div>
<nav>
<progress :value="flashing.percentage" max="100"></progress>
</nav>
</article>
</div>
<div v-else-if="selected.firmware">
<header>
<nav>
<button class="circle transparent" @click="selected.firmware = null"><i>arrow_back</i></button>
<i>developer_board</i>
<a class="small" href="javascript:;" @click="selected.firmware = null">{{ selected.device.name }}</a>
<i>chevron_right</i>
<i>{{ selected.firmware.icon }}</i>
<span class="small">{{ selected.firmware.desc }}</span>
</nav>
<nav class="no-margin">
<h6 class="small max">Install options</h6>
</nav>
</header>
<ul class="list border" v-if="selected.device.type === 'esp32'">
<li>
<label class="checkbox">
<input type="checkbox" v-model="selected.wipe">
<span>Erase device</span>
<div class="tooltip right max">
DO NOT carry out a full erase if you are simply updating your MeshCore device, otherwise it will erase your MeshCore identity for that device.
</div>
</label>
</li>
</ul>
<button @click="dfuMode" :disabled="flashing.dfuComplete" v-if="selected.device.type === 'nrf52'">
<i>{{ flashing.dfuComplete ? 'check' : 'code' }}</i>
<span>{{ flashing.dfuComplete ? 'DFU mode active' : 'Enter DFU mode' }}</span>
<div class="tooltip right max">
Enter DFU mode - this mode enables you to flash your firmware.
If you did not trigger the DFU mode manually, please click this button.
</div>
</button>
<div class="medium-space"></div>
<nav class="small-margin">
<button @click="flashDevice">
<i>bolt</i>
<span>Flash!</span>
<div class="tooltip right max">
Upload the firmware into your device. Existing firwmare will get overwritten.
<span v-if="selected.device.type === 'nrf52'">If you did not trigger DFU mode manually, use the <b>Enter DFU mode</b> before flashing</span>
</div>
</button>
<div class="max"></div>
<button data-ui="#down" class="active">
<i>download</i>
<span>Download</span><i>arrow_drop_down</i>
<menu class="no-wrap" id="down" data-ui="#down">
<li v-for="file in selected.firmware.files">
<a data-ui="menu-selector" :href="config.basePath + '/' + file.name" download>{{ file.title }}</a>
</li>
</menu>
<div class="tooltip left max">Download a copy of the firmware files for use with other flashers</div>
</button>
</nav>
</div>
<div v-else-if="selected.device">
<header>
<nav>
<button class="circle transparent" @click="selected.device = null"><i>arrow_back</i></button>
<i>developer_board</i>
<span>{{ selected.device.name }}</span>
</nav>
<nav class="no-margin">
<h6 class="small max">Choose role</h6>
</nav>
</header>
<ul class="list border">
<li v-for="firmware in selected.device.firmware" :class="firmware.class || config.role[firmware.role].class || ''">
<button class="transparent" @click="selected.firmware = firmware">
<i>{{ firmware.icon || config.role[firmware.role].icon }}</i>
<span>{{ firmware.title || config.role[firmware.role].title }}</span>
<div class="tooltip right max" v-if="firmware.tooltip || config.role[firmware.role].tooltip" v-html="firmware.tooltip || config.role[firmware.role].tooltip"></div>
</button>
</li>
</ul>
</div>
<div v-else>
<header>
<nav>
<i>bolt</i>
<h5 class="small max">MeshCore flasher</h5>
<button class="transparent" @click="openSerialCon()">
<i>terminal</i>
<span>Console</span>
<div class="tooltip left max">Open serial console to manage Routers and Room servers</div>
</button>
</nav>
<nav class="no-margin">
<h6 class="small max">Choose device</h6>
</nav>
</header>
<ul class="list border">
<li v-for="device in config.device">
<button class="transparent" @click="selected.device = device">
<i>developer_board</i>
<span>{{ device.name }}</span>
<div class="tooltip right max" v-if="device.tooltip" v-html="device.tooltip"></div>
</button>
</li>
</ul>
</div>
</div>
<div v-if="serialCon.opened" class="overlay active">
<datalist id="command-db">
<option v-for="(desc, command) in commandReference" :value="command">{{ desc }}</option>
</datalist>
<header>
<nav>
<button class="circle transparent" @click="closeSerialCon()"><i>arrow_back</i></button>
<h6 class="small max">Serial Console</h6>
</nav>
</header>
<pre class="console" @click="consoleEditBox.focus()" ref="consoleWindow">
<code>{{ serialCon.content }}</code>
<div class="holder">
<span>&gt;</span>
<input ref="consoleEditBox" class="console-input" type="text" v-model="serialCon.edit" @keydown.enter.prevent="sendCommand(serialCon.edit)" list="command-db">
</div>
</pre>
</div>
`;
createApp({ setup, template }).mount('#flasher');
createApp({ setup }).mount('#app');