mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 16:52:22 +00:00
Add GitHub Pages firmware picker
This commit is contained in:
1 parent
7d2f69e34f
commit
ae1b928e81
6 files changed
+768
No files matched your search
@@ -0,0 +1,359 @@
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
const PURPOSES = Object.freeze({
|
||||
"usb-logging": Object.freeze({
|
||||
title: "Station G2 FULL USB logging repeater",
|
||||
summary: "Best for a G2 that normally remains connected to a computer collecting serial packet logs.",
|
||||
pattern: /^Station_G2_repeater-full-logging-ota-/,
|
||||
expected: "Station_G2_repeater-full-logging-ota-...",
|
||||
full: true,
|
||||
notes: [
|
||||
"Back up the node configuration before the first FULL install.",
|
||||
"Use the merged image once to install the expanded partition table.",
|
||||
"Reconnect at 115200 baud and leave the USB data connection attached for logging.",
|
||||
],
|
||||
}),
|
||||
field: Object.freeze({
|
||||
title: "Station G2 standard repeater",
|
||||
summary: "The simplest sensor-enabled repeater for local USB installation and ordinary field use.",
|
||||
pattern: /^Station_G2_repeater-v/,
|
||||
expected: "Station_G2_repeater-v...",
|
||||
full: false,
|
||||
notes: [
|
||||
"Choose this when USB logging, MQTT, ESP-NOW, and remote LoRa updates are not required.",
|
||||
"This keeps the standard Station G2 partition profile.",
|
||||
],
|
||||
}),
|
||||
"field-ota": Object.freeze({
|
||||
title: "Station G2 lean OTA repeater",
|
||||
summary: "A standalone repeater with LoRa/WiFi update support and no optional external environmental-sensor drivers.",
|
||||
pattern: /^Station_G2_repeater_lora_ota_no_external_sensors-ota-/,
|
||||
expected: "Station_G2_repeater_lora_ota_no_external_sensors-ota-...",
|
||||
full: false,
|
||||
notes: [
|
||||
"Board-native GPS support remains available; only optional external sensor drivers are removed.",
|
||||
"Use the exact matching target for every later LoRa package.",
|
||||
],
|
||||
}),
|
||||
mqtt: Object.freeze({
|
||||
title: "Station G2 FULL MQTT observer/repeater",
|
||||
summary: "For direct radio-to-MQTT forwarding over WiFi with the complete CLI and expanded partitions.",
|
||||
pattern: /^Station_G2_repeater_observer_mqtt-full-ota-/,
|
||||
expected: "Station_G2_repeater_observer_mqtt-full-ota-...",
|
||||
full: true,
|
||||
notes: [
|
||||
"This is an MQTT observer role, not USB packet logging.",
|
||||
"Use its merged image once before any app-only WiFi or LoRa update.",
|
||||
],
|
||||
}),
|
||||
espnow: Object.freeze({
|
||||
title: "Station G2 FULL ESP-NOW bridge repeater",
|
||||
summary: "For a repeater that bridges packets through ESP-NOW and needs the complete CLI.",
|
||||
pattern: /^Station_G2_repeater_bridge_espnow-full-ota-/,
|
||||
expected: "Station_G2_repeater_bridge_espnow-full-ota-...",
|
||||
full: true,
|
||||
notes: ["ESP-NOW bridge builds use the expanded FULL partition profile."],
|
||||
}),
|
||||
room: Object.freeze({
|
||||
title: "Station G2 room server",
|
||||
summary: "A standard room-server role without MQTT.",
|
||||
pattern: /^Station_G2_room_server-v/,
|
||||
expected: "Station_G2_room_server-v...",
|
||||
full: false,
|
||||
notes: ["Do not install this when the node is intended to remain a repeater."],
|
||||
}),
|
||||
"room-mqtt": Object.freeze({
|
||||
title: "Station G2 FULL MQTT room observer",
|
||||
summary: "A room server with direct MQTT/WiFi forwarding and expanded partitions.",
|
||||
pattern: /^Station_G2_room_server_observer_mqtt-full-ota-/,
|
||||
expected: "Station_G2_room_server_observer_mqtt-full-ota-...",
|
||||
full: true,
|
||||
notes: ["Use its merged image once before later same-profile OTA updates."],
|
||||
}),
|
||||
"companion-usb": Object.freeze({
|
||||
title: "Station G2 USB Companion",
|
||||
summary: "For a Station G2 controlled as a Companion through a USB serial connection.",
|
||||
pattern: /^Station_G2_companion_radio_usb-ota-/,
|
||||
expected: "Station_G2_companion_radio_usb-ota-...",
|
||||
full: false,
|
||||
notes: ["This is a Companion, not a standalone repeater or USB packet logger."],
|
||||
}),
|
||||
"companion-ble": Object.freeze({
|
||||
title: "Station G2 Bluetooth Companion",
|
||||
summary: "For a Station G2 controlled by a phone or computer through Bluetooth.",
|
||||
pattern: /^Station_G2_companion_radio_ble-/,
|
||||
expected: "Station_G2_companion_radio_ble-...",
|
||||
full: false,
|
||||
notes: ["Choose USB Companion instead when the data connection is normally wired."],
|
||||
}),
|
||||
"companion-wifi": Object.freeze({
|
||||
title: "Station G2 WiFi Companion",
|
||||
summary: "For Companion protocol access over WiFi.",
|
||||
pattern: /^Station_G2_companion_radio_wifi-ota-/,
|
||||
expected: "Station_G2_companion_radio_wifi-ota-...",
|
||||
full: false,
|
||||
notes: ["This is not the standalone MQTT observer role."],
|
||||
}),
|
||||
"companion-full": Object.freeze({
|
||||
title: "Station G2 Full Companion",
|
||||
summary: "A host-backed Companion that can serve LoRa OTA content and exposes the complete transport set.",
|
||||
pattern: /^Station_G2_companion_radio_full-v/,
|
||||
expected: "Station_G2_companion_radio_full-...",
|
||||
full: true,
|
||||
notes: [
|
||||
"Full Companion is an OTA source and host-backed controller, not a self-updating repeater.",
|
||||
"Install its merged image over USB.",
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
function isSafeGithubUrl(value) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === "https:" &&
|
||||
(url.hostname === "github.com" || url.hostname === "objects.githubusercontent.com");
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function flattenReleases(releases, channel) {
|
||||
const allowPrerelease = channel === "development";
|
||||
const rows = [];
|
||||
(Array.isArray(releases) ? releases : []).forEach(function (release) {
|
||||
if (!release || release.draft || (!allowPrerelease && release.prerelease)) return;
|
||||
(Array.isArray(release.assets) ? release.assets : []).forEach(function (asset) {
|
||||
if (!asset || typeof asset.name !== "string") return;
|
||||
rows.push({
|
||||
name: asset.name,
|
||||
url: isSafeGithubUrl(asset.browser_download_url) ? asset.browser_download_url : "",
|
||||
size: Number(asset.size) || 0,
|
||||
releaseName: release.name || release.tag_name || "Release",
|
||||
releaseTag: release.tag_name || "",
|
||||
releaseUrl: isSafeGithubUrl(release.html_url) ? release.html_url : "",
|
||||
prerelease: Boolean(release.prerelease),
|
||||
publishedAt: release.published_at || release.created_at || "",
|
||||
});
|
||||
});
|
||||
});
|
||||
return rows.sort(function (a, b) {
|
||||
return String(b.publishedAt).localeCompare(String(a.publishedAt));
|
||||
});
|
||||
}
|
||||
|
||||
function wantedSuffix(install) {
|
||||
return install === "first" ? "-merged.bin" : ".bin";
|
||||
}
|
||||
|
||||
function selectAsset(rows, purposeKey, install) {
|
||||
const purpose = PURPOSES[purposeKey];
|
||||
if (!purpose) return null;
|
||||
const suffix = wantedSuffix(install);
|
||||
const matches = rows.filter(function (row) {
|
||||
if (!purpose.pattern.test(row.name) || !row.name.endsWith(suffix)) return false;
|
||||
if (install !== "first" && row.name.endsWith("-merged.bin")) return false;
|
||||
return true;
|
||||
});
|
||||
matches.sort(function (a, b) {
|
||||
const byDate = String(b.publishedAt).localeCompare(String(a.publishedAt));
|
||||
if (byDate) return byDate;
|
||||
const byLength = a.name.length - b.name.length;
|
||||
return byLength || a.name.localeCompare(b.name);
|
||||
});
|
||||
return matches[0] || null;
|
||||
}
|
||||
|
||||
function formatBytes(value) {
|
||||
const size = Number(value) || 0;
|
||||
if (size >= 1024 * 1024) return (size / (1024 * 1024)).toFixed(2) + " MiB";
|
||||
if (size >= 1024) return (size / 1024).toFixed(1) + " KiB";
|
||||
return size + " bytes";
|
||||
}
|
||||
|
||||
function installExplanation(install) {
|
||||
if (install === "first") {
|
||||
return {
|
||||
label: "USB merged-image install",
|
||||
warning: "This can replace the partition table. Back up configuration first.",
|
||||
};
|
||||
}
|
||||
if (install === "lora") {
|
||||
return {
|
||||
label: "LoRa OTA source image",
|
||||
warning: "Package the non-merged application for the exact target. The installed partition signature must already match.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: "Browser/WiFi app-only update",
|
||||
warning: "Use only when the installed board, role, and partition layout already match.",
|
||||
};
|
||||
}
|
||||
|
||||
function createElement(tag, text) {
|
||||
const element = document.createElement(tag);
|
||||
if (text != null) element.textContent = text;
|
||||
return element;
|
||||
}
|
||||
|
||||
function replaceFacts(container, facts) {
|
||||
container.replaceChildren();
|
||||
facts.forEach(function (fact) {
|
||||
container.appendChild(createElement("dt", fact[0]));
|
||||
const dd = createElement("dd", fact[1]);
|
||||
if (fact[2]) dd.title = fact[2];
|
||||
container.appendChild(dd);
|
||||
});
|
||||
}
|
||||
|
||||
function renderSteps(container, purpose, install) {
|
||||
container.replaceChildren();
|
||||
container.appendChild(createElement("strong", "What to do"));
|
||||
const list = createElement("ol");
|
||||
const common = install === "first"
|
||||
? [
|
||||
"Back up configuration, keys, and radio settings.",
|
||||
"Verify that the filename begins with Station_G2 before flashing.",
|
||||
"Flash the merged image over a data-capable USB cable.",
|
||||
"Wait for the board to reboot, reconnect, and verify its role and radio settings.",
|
||||
]
|
||||
: install === "lora"
|
||||
? [
|
||||
"Confirm the running target and partition signature already match this profile.",
|
||||
"Use the non-merged application as input to the LoRa OTA packaging workflow.",
|
||||
"Verify the target identity and completed update after reboot.",
|
||||
]
|
||||
: [
|
||||
"Confirm the running target and partition signature already match this profile.",
|
||||
"Upload the non-merged application through the firmware's browser/WiFi updater.",
|
||||
"Verify the role and version after reboot.",
|
||||
];
|
||||
common.concat(purpose.notes).forEach(function (text) {
|
||||
list.appendChild(createElement("li", text));
|
||||
});
|
||||
container.appendChild(list);
|
||||
}
|
||||
|
||||
function initPicker(root) {
|
||||
const repo = root.getAttribute("data-release-repo") || "mikecarper/MeshCore";
|
||||
const form = root.querySelector('[data-role="form"]');
|
||||
const channelField = root.querySelector('[data-field="channel"]');
|
||||
const purposeField = root.querySelector('[data-field="purpose"]');
|
||||
const installField = root.querySelector('[data-field="install"]');
|
||||
const status = root.querySelector('[data-role="status"]');
|
||||
const result = root.querySelector('[data-role="result"]');
|
||||
const missing = root.querySelector('[data-role="missing"]');
|
||||
const search = root.querySelector('[data-field="asset-search"]');
|
||||
const assetResults = root.querySelector('[data-role="asset-results"]');
|
||||
let releases = [];
|
||||
|
||||
function currentRows() {
|
||||
return flattenReleases(releases, channelField.value);
|
||||
}
|
||||
|
||||
function render() {
|
||||
const purpose = PURPOSES[purposeField.value];
|
||||
const install = installField.value;
|
||||
const rows = currentRows();
|
||||
const asset = selectAsset(rows, purposeField.value, install);
|
||||
const installInfo = installExplanation(install);
|
||||
result.hidden = true;
|
||||
missing.hidden = true;
|
||||
|
||||
if (!asset) {
|
||||
missing.hidden = false;
|
||||
const suffix = wantedSuffix(install);
|
||||
root.querySelector('[data-role="missing-text"]').textContent =
|
||||
"Expected a recent asset beginning with " + purpose.expected +
|
||||
" and ending in " + suffix + ". Try the other release channel or open the release collection.";
|
||||
return;
|
||||
}
|
||||
|
||||
root.querySelector('[data-role="result-title"]').textContent = purpose.title;
|
||||
root.querySelector('[data-role="result-summary"]').textContent = purpose.summary;
|
||||
replaceFacts(root.querySelector('[data-role="result-facts"]'), [
|
||||
["File", asset.name],
|
||||
["Release", asset.releaseName],
|
||||
["Install path", installInfo.label],
|
||||
["Size", formatBytes(asset.size)],
|
||||
["Important", installInfo.warning],
|
||||
]);
|
||||
const download = root.querySelector('[data-role="download"]');
|
||||
download.href = asset.url || asset.releaseUrl;
|
||||
download.hidden = !(asset.url || asset.releaseUrl);
|
||||
const releaseLink = root.querySelector('[data-role="release"]');
|
||||
releaseLink.href = asset.releaseUrl || "https://github.com/" + repo + "/releases";
|
||||
renderSteps(root.querySelector('[data-role="steps"]'), purpose, install);
|
||||
result.hidden = false;
|
||||
}
|
||||
|
||||
function renderSearch() {
|
||||
assetResults.replaceChildren();
|
||||
const query = search.value.trim().toLowerCase();
|
||||
if (query.length < 2) return;
|
||||
const matches = currentRows().filter(function (row) {
|
||||
return row.name.toLowerCase().includes(query);
|
||||
}).slice(0, 80);
|
||||
if (!matches.length) {
|
||||
assetResults.appendChild(createElement("p", "No recent release filename matched."));
|
||||
return;
|
||||
}
|
||||
const list = createElement("ul");
|
||||
list.className = "firmware-asset-list";
|
||||
matches.forEach(function (row) {
|
||||
const item = createElement("li");
|
||||
const link = createElement("a", row.name);
|
||||
link.href = row.url || row.releaseUrl;
|
||||
item.appendChild(link);
|
||||
const meta = createElement("span", row.releaseName + " - " + formatBytes(row.size));
|
||||
meta.className = "firmware-asset-meta";
|
||||
item.appendChild(meta);
|
||||
list.appendChild(item);
|
||||
});
|
||||
assetResults.appendChild(list);
|
||||
}
|
||||
|
||||
form.addEventListener("change", function () {
|
||||
render();
|
||||
renderSearch();
|
||||
});
|
||||
search.addEventListener("input", renderSearch);
|
||||
|
||||
const endpoint = "https://api.github.com/repos/" + encodeURIComponent(repo.split("/")[0]) +
|
||||
"/" + encodeURIComponent(repo.split("/")[1]) + "/releases?per_page=20";
|
||||
fetch(endpoint, { headers: { Accept: "application/vnd.github+json" } })
|
||||
.then(function (response) {
|
||||
if (!response.ok) throw new Error("GitHub returned HTTP " + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
releases = Array.isArray(data) ? data : [];
|
||||
const count = flattenReleases(releases, "development").length;
|
||||
status.textContent = "Loaded " + count + " firmware assets from the 20 most recent releases.";
|
||||
render();
|
||||
})
|
||||
.catch(function (error) {
|
||||
status.textContent = "Could not load the GitHub release catalog: " + error.message;
|
||||
missing.hidden = false;
|
||||
root.querySelector('[data-role="missing-text"]').textContent =
|
||||
"Open the release collection and look for the expected filename shown by this picker.";
|
||||
});
|
||||
}
|
||||
|
||||
const api = Object.freeze({
|
||||
PURPOSES: PURPOSES,
|
||||
flattenReleases: flattenReleases,
|
||||
selectAsset: selectAsset,
|
||||
wantedSuffix: wantedSuffix,
|
||||
formatBytes: formatBytes,
|
||||
});
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
||||
global.MeshCoreFirmwarePicker = api;
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelectorAll("[data-firmware-picker]").forEach(initPicker);
|
||||
});
|
||||
}
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this);
|
||||
@@ -0,0 +1,173 @@
|
||||
.firmware-picker {
|
||||
--picker-accent: #0f766e;
|
||||
--picker-accent-dark: #115e59;
|
||||
--picker-border: color-mix(in srgb, currentColor 18%, transparent);
|
||||
margin: 1.25rem 0 2rem;
|
||||
}
|
||||
|
||||
.firmware-picker-intro,
|
||||
.firmware-picker-result,
|
||||
.firmware-picker-missing,
|
||||
.firmware-asset-browser {
|
||||
border: 1px solid var(--picker-border);
|
||||
border-radius: 0.7rem;
|
||||
padding: 1rem 1.15rem;
|
||||
}
|
||||
|
||||
.firmware-picker-intro {
|
||||
border-left: 0.35rem solid var(--picker-accent);
|
||||
background: color-mix(in srgb, var(--picker-accent) 8%, transparent);
|
||||
}
|
||||
|
||||
.firmware-picker-intro p,
|
||||
.firmware-picker-result p,
|
||||
.firmware-picker-missing p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.firmware-picker-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
|
||||
gap: 0.9rem;
|
||||
margin: 1.2rem 0;
|
||||
}
|
||||
|
||||
.firmware-picker-form label,
|
||||
.firmware-asset-browser label {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.firmware-picker-form select,
|
||||
.firmware-asset-browser input {
|
||||
width: 100%;
|
||||
min-height: 2.7rem;
|
||||
padding: 0.45rem 0.6rem;
|
||||
border: 1px solid var(--picker-border);
|
||||
border-radius: 0.35rem;
|
||||
color: inherit;
|
||||
background: var(--md-default-bg-color);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.firmware-picker-status {
|
||||
min-height: 1.5rem;
|
||||
margin: 0.75rem 0;
|
||||
color: var(--md-default-fg-color--light);
|
||||
}
|
||||
|
||||
.firmware-picker-result {
|
||||
border-top: 0.3rem solid var(--picker-accent);
|
||||
}
|
||||
|
||||
.firmware-picker-eyebrow {
|
||||
margin: 0 0 0.15rem !important;
|
||||
color: var(--picker-accent);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.firmware-picker-result h2,
|
||||
.firmware-picker-missing h2 {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.firmware-picker-facts {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(7rem, auto) 1fr;
|
||||
gap: 0.35rem 0.9rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.firmware-picker-facts dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.firmware-picker-facts dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.firmware-picker-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.firmware-picker-actions a {
|
||||
display: inline-block;
|
||||
padding: 0.55rem 0.8rem;
|
||||
border: 1px solid var(--picker-accent);
|
||||
border-radius: 0.4rem;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.firmware-picker-actions .firmware-picker-primary {
|
||||
color: white;
|
||||
background: var(--picker-accent);
|
||||
}
|
||||
|
||||
.firmware-picker-actions .firmware-picker-primary:hover {
|
||||
background: var(--picker-accent-dark);
|
||||
}
|
||||
|
||||
.firmware-picker-steps {
|
||||
border-top: 1px solid var(--picker-border);
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.8rem;
|
||||
}
|
||||
|
||||
.firmware-picker-steps ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.firmware-picker-missing {
|
||||
border-left: 0.35rem solid #b45309;
|
||||
}
|
||||
|
||||
.firmware-asset-browser {
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
.firmware-asset-browser summary {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.firmware-asset-list {
|
||||
max-height: 24rem;
|
||||
margin-top: 0.8rem;
|
||||
padding-left: 1.2rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.firmware-asset-list li {
|
||||
margin: 0.35rem 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.firmware-asset-meta {
|
||||
display: block;
|
||||
color: var(--md-default-fg-color--light);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
@media (max-width: 44rem) {
|
||||
.firmware-picker-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.firmware-picker-facts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.firmware-picker-facts dd + dt {
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
# Firmware picker
|
||||
|
||||
Choose what the board will do, then let the picker reduce the release assets
|
||||
to one recommended firmware. The first guided board is Station G2 because its
|
||||
standard, logging, FULL, LoRa-OTA, MQTT, bridge, room-server, and Companion
|
||||
profiles are easy to confuse.
|
||||
|
||||
The picker reads public release metadata from GitHub. It does not upload any
|
||||
device information.
|
||||
|
||||
<div class="firmware-picker" data-firmware-picker data-release-repo="mikecarper/MeshCore">
|
||||
<div class="firmware-picker-intro" role="note">
|
||||
<strong>Station G2 shortcut</strong>
|
||||
<p>
|
||||
If the G2 normally stays connected to a computer and you want USB packet
|
||||
logs, choose <strong>USB logging repeater</strong>. The recommended image
|
||||
is the expanded <strong>FULL logging</strong> profile. The smaller legacy
|
||||
logging image is mainly a compatibility fallback, not the normal choice
|
||||
for a computer-powered logger.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="firmware-picker-form" data-role="form">
|
||||
<label>
|
||||
Release channel
|
||||
<select data-field="channel">
|
||||
<option value="development" selected>Current development and pre-releases</option>
|
||||
<option value="stable">Stable releases</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
What should the Station G2 do?
|
||||
<select data-field="purpose">
|
||||
<option value="usb-logging">USB logging repeater - recommended when connected to a computer</option>
|
||||
<option value="field">Simple standalone repeater</option>
|
||||
<option value="field-ota">Standalone repeater with LoRa/WiFi OTA, no external sensors</option>
|
||||
<option value="mqtt">Full MQTT observer/repeater</option>
|
||||
<option value="espnow">ESP-NOW bridge repeater</option>
|
||||
<option value="room">Room server</option>
|
||||
<option value="room-mqtt">Full MQTT room observer</option>
|
||||
<option value="companion-usb">USB Companion</option>
|
||||
<option value="companion-ble">Bluetooth Companion</option>
|
||||
<option value="companion-wifi">WiFi Companion</option>
|
||||
<option value="companion-full">Full Companion and LoRa-OTA source</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
How will this firmware be installed?
|
||||
<select data-field="install">
|
||||
<option value="first">First install, recovery, or changing partition/profile - USB cable</option>
|
||||
<option value="wifi">Same profile and partition - browser/WiFi OTA</option>
|
||||
<option value="lora">Same profile and partition - LoRa OTA</option>
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<div class="firmware-picker-status" data-role="status" aria-live="polite">
|
||||
Loading release catalog...
|
||||
</div>
|
||||
|
||||
<section class="firmware-picker-result" data-role="result" aria-live="polite" hidden>
|
||||
<p class="firmware-picker-eyebrow">Recommended firmware</p>
|
||||
<h2 data-role="result-title"></h2>
|
||||
<p data-role="result-summary"></p>
|
||||
<dl class="firmware-picker-facts" data-role="result-facts"></dl>
|
||||
<div class="firmware-picker-actions">
|
||||
<a class="firmware-picker-primary" data-role="download" href="#">Download recommended file</a>
|
||||
<a data-role="release" href="#">Open its release</a>
|
||||
</div>
|
||||
<div class="firmware-picker-steps" data-role="steps"></div>
|
||||
</section>
|
||||
|
||||
<section class="firmware-picker-missing" data-role="missing" aria-live="polite" hidden>
|
||||
<h2>The matching asset is not in this release channel yet</h2>
|
||||
<p data-role="missing-text"></p>
|
||||
<a href="https://github.com/mikecarper/MeshCore/releases">Browse all firmware releases</a>
|
||||
</section>
|
||||
|
||||
<details class="firmware-asset-browser">
|
||||
<summary>Advanced: search all recent release filenames</summary>
|
||||
<p>
|
||||
This search is for uncommon hardware variants and expert recovery. A
|
||||
filename match is not a board-identity check.
|
||||
</p>
|
||||
<label>
|
||||
Filename contains
|
||||
<input data-field="asset-search" placeholder="Station_G2, heltec_v4, repeater, ...">
|
||||
</label>
|
||||
<div data-role="asset-results"></div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
## What FULL means for a USB logger
|
||||
|
||||
A Station G2 that is powered from and monitored by a computer normally should
|
||||
use the FULL logging profile:
|
||||
|
||||
1. Save its name, radio settings, keys, and other configuration.
|
||||
2. Download the exact Station G2 `full-logging-ota` merged image recommended
|
||||
above.
|
||||
3. Flash that `-merged.bin` over USB once. It installs the expanded partition
|
||||
table as well as the application.
|
||||
4. Reconnect the serial terminal at 115200 baud and restore any settings that
|
||||
were not retained.
|
||||
5. Leave the data-capable USB connection attached to collect logs. Use
|
||||
`set usb.logging off` temporarily if the runtime output needs to be quiet.
|
||||
|
||||
Choose the standalone or lean OTA repeater instead when the board is normally
|
||||
battery/solar powered, has no computer collecting USB output, or must retain a
|
||||
known legacy partition layout.
|
||||
|
||||
## Can a Heltec V4 partition be expanded by OTA?
|
||||
|
||||
No. WiFi OTA and LoRa OTA write an application into the inactive application
|
||||
partition. They do not replace the partition table at flash offset `0x8000`.
|
||||
Allowing a running application to move its own active/inactive partitions
|
||||
would risk overwriting the running image, staged image, NVS, or filesystem.
|
||||
|
||||
The current Heltec V4 definition already uses its 16 MiB flash efficiently:
|
||||
|
||||
| Region | Size |
|
||||
|---|---:|
|
||||
| Application slot A | `0x640000` (6.25 MiB) |
|
||||
| Application slot B | `0x640000` (6.25 MiB) |
|
||||
| SPIFFS | `0x360000` (3.375 MiB) |
|
||||
|
||||
That is much larger than current MeshCore V4 applications. A different layout
|
||||
is possible only as a custom build, and it requires a one-time exact-board
|
||||
merged-image flash over USB. Expanding the app slots further would shrink or
|
||||
remove SPIFFS, or remove the second slot and therefore remove safe OTA.
|
||||
|
||||
After installing a new partition table over USB, later WiFi or LoRa updates
|
||||
must use a non-merged application built for the same board, role, and partition
|
||||
signature. Never use an app-only FULL image to try to migrate a device that is
|
||||
still running another partition layout.
|
||||
|
||||
## Merged versus non-merged files
|
||||
|
||||
| File | Use |
|
||||
|---|---|
|
||||
| `-merged.bin` | First install, recovery, role/profile migration, or partition-table change over USB |
|
||||
| `.bin` without `-merged` | Browser/WiFi application update when the installed partition layout already matches |
|
||||
| `.mota` | LoRa OTA package for the exact target identity and installed partition layout |
|
||||
|
||||
When uncertain, back up the configuration and use the exact-board merged image
|
||||
over USB. Do not send a merged image through browser OTA or LoRa OTA.
|
||||
@@ -5,6 +5,7 @@ Welcome to the MeshCore documentation.
|
||||
Below are a few quick start guides.
|
||||
|
||||
- [Frequently Asked Questions](./faq.md)
|
||||
- [Firmware Picker](./firmware_picker.md)
|
||||
- [CLI Commands](./cli_commands.md)
|
||||
- [Filter Policy Playground](./filter_tool.md)
|
||||
- [Telemetry Decoder](./telemetry_decoder.md)
|
||||
|
||||
@@ -17,9 +17,11 @@ theme:
|
||||
|
||||
extra_css:
|
||||
- _stylesheets/extra.css
|
||||
- _stylesheets/firmware_picker.css
|
||||
- _stylesheets/telemetry_decoder.css
|
||||
- _stylesheets/filter_tool.css
|
||||
|
||||
extra_javascript:
|
||||
- _javascript/firmware_picker.js
|
||||
- _javascript/telemetry_decoder.js
|
||||
- _javascript/filter_tool.js
|
||||
@@ -0,0 +1,85 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("assert");
|
||||
const picker = require("../docs/_javascript/firmware_picker.js");
|
||||
|
||||
const releases = [
|
||||
{
|
||||
name: "Stable FULL profiles",
|
||||
tag_name: "full-profiles-v1",
|
||||
html_url: "https://github.com/mikecarper/MeshCore/releases/tag/full-profiles-v1",
|
||||
published_at: "2026-08-20T12:00:00Z",
|
||||
prerelease: false,
|
||||
draft: false,
|
||||
assets: [
|
||||
{
|
||||
name: "Station_G2_repeater-full-logging-ota-v1-deadbee-merged.bin",
|
||||
browser_download_url: "https://github.com/mikecarper/MeshCore/releases/download/full-profiles-v1/Station_G2_repeater-full-logging-ota-v1-deadbee-merged.bin",
|
||||
size: 2000000,
|
||||
},
|
||||
{
|
||||
name: "Station_G2_repeater-full-logging-ota-v1-merged.bin",
|
||||
browser_download_url: "https://github.com/mikecarper/MeshCore/releases/download/full-profiles-v1/Station_G2_repeater-full-logging-ota-v1-merged.bin",
|
||||
size: 2000000,
|
||||
},
|
||||
{
|
||||
name: "Station_G2_repeater-full-logging-ota-v1.bin",
|
||||
browser_download_url: "https://github.com/mikecarper/MeshCore/releases/download/full-profiles-v1/Station_G2_repeater-full-logging-ota-v1.bin",
|
||||
size: 1500000,
|
||||
},
|
||||
{
|
||||
name: "Station_G2_companion_radio_full-logging-v1-merged.bin",
|
||||
browser_download_url: "https://github.com/mikecarper/MeshCore/releases/download/full-profiles-v1/Station_G2_companion_radio_full-logging-v1-merged.bin",
|
||||
size: 1800000,
|
||||
},
|
||||
{
|
||||
name: "Station_G2_companion_radio_full-v1-merged.bin",
|
||||
browser_download_url: "https://github.com/mikecarper/MeshCore/releases/download/full-profiles-v1/Station_G2_companion_radio_full-v1-merged.bin",
|
||||
size: 1700000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Development FULL profiles",
|
||||
tag_name: "full-profiles-v2-dev",
|
||||
html_url: "https://github.com/mikecarper/MeshCore/releases/tag/full-profiles-v2-dev",
|
||||
published_at: "2026-08-21T12:00:00Z",
|
||||
prerelease: true,
|
||||
draft: false,
|
||||
assets: [
|
||||
{
|
||||
name: "Station_G2_repeater-full-logging-ota-v2-merged.bin",
|
||||
browser_download_url: "https://github.com/mikecarper/MeshCore/releases/download/full-profiles-v2-dev/Station_G2_repeater-full-logging-ota-v2-merged.bin",
|
||||
size: 2100000,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const stable = picker.flattenReleases(releases, "stable");
|
||||
const development = picker.flattenReleases(releases, "development");
|
||||
|
||||
assert.strictEqual(stable.length, 5);
|
||||
assert.strictEqual(development.length, 6);
|
||||
assert.strictEqual(picker.wantedSuffix("first"), "-merged.bin");
|
||||
assert.strictEqual(picker.wantedSuffix("wifi"), ".bin");
|
||||
assert.strictEqual(
|
||||
picker.selectAsset(stable, "usb-logging", "first").name,
|
||||
"Station_G2_repeater-full-logging-ota-v1-merged.bin"
|
||||
);
|
||||
assert.strictEqual(
|
||||
picker.selectAsset(stable, "usb-logging", "wifi").name,
|
||||
"Station_G2_repeater-full-logging-ota-v1.bin"
|
||||
);
|
||||
assert.strictEqual(
|
||||
picker.selectAsset(development, "usb-logging", "first").name,
|
||||
"Station_G2_repeater-full-logging-ota-v2-merged.bin"
|
||||
);
|
||||
assert.strictEqual(picker.selectAsset(stable, "field", "first"), null);
|
||||
assert.strictEqual(
|
||||
picker.selectAsset(stable, "companion-full", "first").name,
|
||||
"Station_G2_companion_radio_full-v1-merged.bin"
|
||||
);
|
||||
assert.strictEqual(picker.formatBytes(2097152), "2.00 MiB");
|
||||
|
||||
console.log("firmware picker tests passed");
|
||||
Reference in new issue
Block a user