mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 06:22:48 +00:00
deploy: e2df34e19c
This commit is contained in:
1 parent
b15b47ea77
commit
2e52ac7989
4 files changed
+409
-1
No files matched your search
@@ -27,6 +27,12 @@
|
||||
"req", "response", "txt_msg", "ack", "advert", "grp_txt", "grp_data", "anon_req",
|
||||
"path", "trace", "multipart", "control", "ota", "13", "14", "raw_custom",
|
||||
]);
|
||||
const RAW_ROUTE_INFO = Object.freeze([
|
||||
Object.freeze({ name: "transport_flood", label: "Transport-scoped flood", simulatorRoute: "scoped_flood", flood: true }),
|
||||
Object.freeze({ name: "flood", label: "Unscoped flood", simulatorRoute: "unscoped_flood", flood: true }),
|
||||
Object.freeze({ name: "direct", label: "Direct route", simulatorRoute: "", flood: false }),
|
||||
Object.freeze({ name: "transport_direct", label: "Transport-scoped direct route", simulatorRoute: "", flood: false }),
|
||||
]);
|
||||
const TYPE_CLASSES = Object.freeze(["class:group", "class:login", "class:other"]);
|
||||
const LOGIN_TYPES = Object.freeze(["req", "response", "txt_msg", "anon_req", "path"]);
|
||||
const GROUP_TYPES = Object.freeze(["grp_txt", "grp_data"]);
|
||||
@@ -746,6 +752,182 @@
|
||||
return normalizePathPrefix(value, 63);
|
||||
}
|
||||
|
||||
function bytesToHex(bytes, start, end) {
|
||||
return bytes.slice(start == null ? 0 : start, end == null ? bytes.length : end)
|
||||
.map((value) => value.toString(16).padStart(2, "0"))
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
function readLittleEndian16(bytes, offset) {
|
||||
return bytes[offset] | (bytes[offset + 1] << 8);
|
||||
}
|
||||
|
||||
function readLittleEndian32(bytes, offset) {
|
||||
return (bytes[offset]
|
||||
| (bytes[offset + 1] << 8)
|
||||
| (bytes[offset + 2] << 16)
|
||||
| (bytes[offset + 3] << 24)) >>> 0;
|
||||
}
|
||||
|
||||
function decodePayloadEnvelope(type, payload) {
|
||||
const fields = [];
|
||||
const warnings = [];
|
||||
const addHex = (label, start, length) => {
|
||||
fields.push({ label, value: bytesToHex(payload, start, start + length) || "empty" });
|
||||
};
|
||||
|
||||
if (["req", "response", "txt_msg", "path"].includes(type)) {
|
||||
if (payload.length < 4) {
|
||||
warnings.push(`${type.toUpperCase()} normally needs a 4-byte destination/source/MAC prefix.`);
|
||||
} else {
|
||||
addHex("Destination hash", 0, 1);
|
||||
addHex("Source hash", 1, 1);
|
||||
addHex("Cipher MAC", 2, 2);
|
||||
addHex("Encrypted body", 4, payload.length - 4);
|
||||
}
|
||||
} else if (["grp_txt", "grp_data"].includes(type)) {
|
||||
if (payload.length < 3) {
|
||||
warnings.push(`${type.toUpperCase()} normally needs a 3-byte channel-hash/MAC prefix.`);
|
||||
} else {
|
||||
addHex("Channel hash", 0, 1);
|
||||
addHex("Cipher MAC", 1, 2);
|
||||
addHex("Encrypted body", 3, payload.length - 3);
|
||||
}
|
||||
} else if (type === "anon_req") {
|
||||
if (payload.length < 35) {
|
||||
warnings.push("ANON_REQ normally needs a 35-byte destination/public-key/MAC prefix.");
|
||||
} else {
|
||||
addHex("Destination hash", 0, 1);
|
||||
addHex("Ephemeral public key", 1, 32);
|
||||
addHex("Cipher MAC", 33, 2);
|
||||
addHex("Encrypted body", 35, payload.length - 35);
|
||||
}
|
||||
} else if (type === "ack") {
|
||||
if (payload.length < 4) warnings.push("ACK normally contains a 4-byte checksum.");
|
||||
else addHex("Message checksum", 0, 4);
|
||||
} else if (type === "advert") {
|
||||
if (payload.length < 100) {
|
||||
warnings.push("ADVERT normally needs at least 100 bytes for its public key, timestamp, and signature.");
|
||||
} else {
|
||||
addHex("Public key", 0, 32);
|
||||
const timestamp = readLittleEndian32(payload, 32);
|
||||
fields.push({ label: "Timestamp", value: `${timestamp} (0x${timestamp.toString(16).padStart(8, "0").toUpperCase()})` });
|
||||
addHex("Signature", 36, 64);
|
||||
addHex("Application data", 100, payload.length - 100);
|
||||
}
|
||||
} else if (type === "control" && payload.length) {
|
||||
fields.push({ label: "Control subtype", value: `0x${(payload[0] >> 4).toString(16).toUpperCase()}` });
|
||||
addHex("Control flags", 0, 1);
|
||||
addHex("Control data", 1, payload.length - 1);
|
||||
}
|
||||
|
||||
return { fields, warnings };
|
||||
}
|
||||
|
||||
function decodeRawPacketHex(value) {
|
||||
const original = clean(value);
|
||||
if (!original) throw new FilterToolError("Raw packet hex is required.");
|
||||
const withoutPrefixes = original.replace(/(^|[\s,:_-])0x(?=[0-9a-f])/gi, "$1");
|
||||
const compact = withoutPrefixes.replace(/[\s,:_-]/g, "");
|
||||
if (/[^0-9a-f]/i.test(compact)) {
|
||||
throw new FilterToolError("Raw packet may contain only hexadecimal bytes and spaces, commas, colons, underscores, or dashes.");
|
||||
}
|
||||
if (compact.length % 2) throw new FilterToolError("Raw packet hex must contain complete two-character bytes.");
|
||||
if (compact.length < 4) throw new FilterToolError("Raw packet must contain at least a header and path-length byte.");
|
||||
if (compact.length > 510) throw new FilterToolError("Raw packet exceeds MeshCore's 255-byte transmission limit.");
|
||||
|
||||
const bytes = [];
|
||||
for (let offset = 0; offset < compact.length; offset += 2) {
|
||||
bytes.push(Number.parseInt(compact.slice(offset, offset + 2), 16));
|
||||
}
|
||||
|
||||
let cursor = 0;
|
||||
const header = bytes[cursor++];
|
||||
const routeCode = header & 0x03;
|
||||
const typeCode = (header >> 2) & 0x0F;
|
||||
const payloadVersionCode = (header >> 6) & 0x03;
|
||||
const route = RAW_ROUTE_INFO[routeCode];
|
||||
if (payloadVersionCode !== 0) {
|
||||
throw new FilterToolError(`Payload version ${payloadVersionCode + 1} is not supported by current MeshCore firmware.`);
|
||||
}
|
||||
|
||||
const transportCodes = [];
|
||||
if (routeCode === 0 || routeCode === 3) {
|
||||
if (cursor + 5 > bytes.length) {
|
||||
throw new FilterToolError("Transport-scoped packet is missing transport codes or its path-length byte.");
|
||||
}
|
||||
transportCodes.push(readLittleEndian16(bytes, cursor), readLittleEndian16(bytes, cursor + 2));
|
||||
cursor += 4;
|
||||
}
|
||||
|
||||
if (cursor >= bytes.length) throw new FilterToolError("Raw packet is missing its path-length byte.");
|
||||
const pathLengthByte = bytes[cursor++];
|
||||
const pathMode = pathLengthByte >> 6;
|
||||
if (pathMode === 3) throw new FilterToolError("Path hash-size mode 3 is reserved and rejected by current firmware.");
|
||||
const hops = pathLengthByte & 0x3F;
|
||||
const pathHashBytes = pathMode + 1;
|
||||
const pathByteLength = hops * pathHashBytes;
|
||||
if (pathByteLength > 64) throw new FilterToolError("Encoded path exceeds MeshCore's 64-byte path limit.");
|
||||
if (cursor + pathByteLength > bytes.length) {
|
||||
throw new FilterToolError(`Packet ends before its ${pathByteLength}-byte encoded path is complete.`);
|
||||
}
|
||||
|
||||
const pathBytes = bytes.slice(cursor, cursor + pathByteLength);
|
||||
cursor += pathByteLength;
|
||||
const payload = bytes.slice(cursor);
|
||||
if (payload.length > 184) throw new FilterToolError("Decoded payload exceeds MeshCore's 184-byte payload limit.");
|
||||
const pathIds = [];
|
||||
for (let offset = 0; offset < pathBytes.length; offset += pathHashBytes) {
|
||||
pathIds.push(bytesToHex(pathBytes, offset, offset + pathHashBytes));
|
||||
}
|
||||
|
||||
const type = TYPE_NAMES[typeCode];
|
||||
const envelope = decodePayloadEnvelope(type, payload);
|
||||
const notes = envelope.warnings.slice();
|
||||
if (!route.flood) {
|
||||
notes.push("Direct packets can be decoded here but remain outside the flood-policy simulator.");
|
||||
}
|
||||
if (transportCodes.length) {
|
||||
notes.push("Transport codes are on the wire, but a local region/scope table is required to resolve their names and allow status.");
|
||||
}
|
||||
if (GROUP_TYPES.includes(type)) {
|
||||
notes.push("The raw channel hash does not authenticate a channel; the matching channel key is required.");
|
||||
}
|
||||
if (["req", "response", "txt_msg", "path", "grp_txt", "grp_data", "anon_req"].includes(type)) {
|
||||
notes.push("Encrypted content, including a displayed sender, cannot be recovered without the appropriate key.");
|
||||
}
|
||||
notes.push("Radio signal measurements, blacklist, path-bucket, loop, and temporary-radio results are receiver-local facts and are not encoded in this packet.");
|
||||
|
||||
return {
|
||||
rawHex: bytesToHex(bytes),
|
||||
rawLength: bytes.length,
|
||||
header,
|
||||
headerHex: bytesToHex([header]),
|
||||
routeCode,
|
||||
routeName: route.name,
|
||||
routeLabel: route.label,
|
||||
simulatorRoute: route.simulatorRoute,
|
||||
filterEligible: route.flood,
|
||||
typeCode,
|
||||
type,
|
||||
payloadVersionCode,
|
||||
payloadVersion: payloadVersionCode + 1,
|
||||
transportCodes,
|
||||
pathLengthByte,
|
||||
pathLengthHex: bytesToHex([pathLengthByte]),
|
||||
pathHashBytes,
|
||||
hops,
|
||||
pathByteLength,
|
||||
pathIds,
|
||||
pathHex: bytesToHex(pathBytes),
|
||||
payloadLength: payload.length,
|
||||
payloadHex: bytesToHex(payload),
|
||||
payloadFields: envelope.fields,
|
||||
notes,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePacket(input) {
|
||||
const scopeStatus = enumValue(input.scopeStatus, ["none", "allowed", "unknown"], "Packet scope status", "none");
|
||||
const type = normalizePacketType(input.type);
|
||||
@@ -1069,6 +1251,9 @@
|
||||
const importInput = root.querySelector("[data-role='import-input']");
|
||||
const importError = root.querySelector("[data-role='import-error']");
|
||||
const explainResults = root.querySelector("[data-role='explain-results']");
|
||||
const rawPacketInput = root.querySelector("[data-role='raw-packet-input']");
|
||||
const packetDecodeError = root.querySelector("[data-role='packet-decode-error']");
|
||||
const packetDecodeResult = root.querySelector("[data-role='packet-decode-result']");
|
||||
const simulationError = root.querySelector("[data-role='simulation-error']");
|
||||
const simulationResult = root.querySelector("[data-role='simulation-result']");
|
||||
const exportDsl = root.querySelector("[data-role='export-dsl']");
|
||||
@@ -1376,6 +1561,24 @@
|
||||
});
|
||||
}
|
||||
|
||||
function loadDecodedPacket(decoded) {
|
||||
if (!decoded.filterEligible) return false;
|
||||
packetField("route").value = decoded.simulatorRoute;
|
||||
packetField("type").value = decoded.type;
|
||||
packetField("hops").value = String(decoded.hops);
|
||||
packetField("path").value = decoded.pathIds.join(",");
|
||||
packetField("channel").value = "";
|
||||
packetField("scope-status").value = decoded.routeCode === 0 ? "unknown" : "none";
|
||||
packetField("scope-name").value = "";
|
||||
packetField("region-name").value = "";
|
||||
packetField("sender").value = "";
|
||||
packetField("blacklist").value = "no";
|
||||
packetField("buckets").value = "";
|
||||
packetField("loop-level").value = "0";
|
||||
packetField("temp-radio").checked = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function decisionValue(label, value) {
|
||||
const item = document.createElement("div");
|
||||
const term = document.createElement("span");
|
||||
@@ -1386,6 +1589,76 @@
|
||||
return item;
|
||||
}
|
||||
|
||||
function renderPacketDecode(decoded, loaded) {
|
||||
packetDecodeResult.replaceChildren();
|
||||
const heading = document.createElement("h3");
|
||||
heading.textContent = "Decoded packet";
|
||||
const summary = document.createElement("div");
|
||||
summary.className = "filter-decision-grid";
|
||||
summary.append(
|
||||
decisionValue("Route", decoded.routeLabel),
|
||||
decisionValue("Payload", `${decoded.type.toUpperCase()} (0x${decoded.typeCode.toString(16).padStart(2, "0").toUpperCase()})`),
|
||||
decisionValue("Version", String(decoded.payloadVersion)),
|
||||
decisionValue("Frame size", `${decoded.rawLength} bytes`),
|
||||
decisionValue("Received hops", String(decoded.hops)),
|
||||
decisionValue("Pbyte width", `${decoded.pathHashBytes} byte${decoded.pathHashBytes === 1 ? "" : "s"}`),
|
||||
decisionValue("Path size", `${decoded.pathByteLength} bytes`),
|
||||
decisionValue("Payload size", `${decoded.payloadLength} bytes`)
|
||||
);
|
||||
const status = document.createElement("p");
|
||||
status.className = "filter-packet-load-status";
|
||||
status.dataset.loaded = loaded ? "true" : "false";
|
||||
status.textContent = loaded
|
||||
? "Loaded the route, exact payload type, hop count, and pbyte path into the simulator. Fields requiring decryption or local receiver state were cleared."
|
||||
: "Decoded successfully, but it was not loaded because direct routes stay outside this flood-policy simulator.";
|
||||
|
||||
const fields = [
|
||||
["Raw frame", decoded.rawHex],
|
||||
["Header", `0x${decoded.headerHex}`],
|
||||
];
|
||||
decoded.transportCodes.forEach((code, index) => {
|
||||
fields.push([`Transport code ${index}`, `0x${code.toString(16).padStart(4, "0").toUpperCase()} (${code})`]);
|
||||
});
|
||||
fields.push(
|
||||
["Path-length byte", `0x${decoded.pathLengthHex}`],
|
||||
["Pbyte path", decoded.pathIds.join(",") || "empty"],
|
||||
["Payload", decoded.payloadHex || "empty"]
|
||||
);
|
||||
decoded.payloadFields.forEach((fieldValue) => fields.push([fieldValue.label, fieldValue.value]));
|
||||
const fieldList = document.createElement("dl");
|
||||
fieldList.className = "filter-packet-fields";
|
||||
fields.forEach(([label, value]) => {
|
||||
const row = document.createElement("div");
|
||||
const term = document.createElement("dt");
|
||||
term.textContent = label;
|
||||
const detail = document.createElement("dd");
|
||||
detail.textContent = value;
|
||||
row.append(term, detail);
|
||||
fieldList.appendChild(row);
|
||||
});
|
||||
const notes = document.createElement("ul");
|
||||
notes.className = "filter-inline-warnings filter-packet-notes";
|
||||
renderWarnings(notes, decoded.notes);
|
||||
packetDecodeResult.append(heading, summary, status, fieldList, notes);
|
||||
packetDecodeResult.hidden = false;
|
||||
}
|
||||
|
||||
function decodePacketInput() {
|
||||
packetDecodeError.hidden = true;
|
||||
packetDecodeError.textContent = "";
|
||||
try {
|
||||
const decoded = decodeRawPacketHex(rawPacketInput.value);
|
||||
const loaded = loadDecodedPacket(decoded);
|
||||
renderPacketDecode(decoded, loaded);
|
||||
simulationResult.hidden = true;
|
||||
simulationError.hidden = true;
|
||||
} catch (error) {
|
||||
packetDecodeResult.hidden = true;
|
||||
packetDecodeError.textContent = error.message;
|
||||
packetDecodeError.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSimulation(result) {
|
||||
simulationResult.replaceChildren();
|
||||
const heading = document.createElement("h3");
|
||||
@@ -1488,7 +1761,9 @@
|
||||
simulationError.hidden = false;
|
||||
}
|
||||
});
|
||||
root.querySelector("[data-role='decode-packet']").addEventListener("click", decodePacketInput);
|
||||
root.querySelector("[data-role='reset-packet']").addEventListener("click", () => {
|
||||
rawPacketInput.value = rawPacketInput.defaultValue;
|
||||
packetField("route").value = "unscoped_flood";
|
||||
packetField("type").value = "grp_data";
|
||||
packetField("hops").value = "4";
|
||||
@@ -1502,6 +1777,8 @@
|
||||
packetField("buckets").value = "";
|
||||
packetField("loop-level").value = "0";
|
||||
packetField("temp-radio").checked = false;
|
||||
packetDecodeResult.hidden = true;
|
||||
packetDecodeError.hidden = true;
|
||||
simulationResult.hidden = true;
|
||||
simulationError.hidden = true;
|
||||
});
|
||||
@@ -1545,6 +1822,7 @@
|
||||
FilterToolError,
|
||||
buildDefinition,
|
||||
decodeBundle,
|
||||
decodeRawPacketHex,
|
||||
encodeBundle,
|
||||
estimateRuleBytes,
|
||||
explainRule,
|
||||
|
||||
@@ -483,6 +483,104 @@
|
||||
margin-top: 0.85rem;
|
||||
}
|
||||
|
||||
.filter-packet-reader {
|
||||
margin-top: 0.85rem;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid var(--filter-border-strong);
|
||||
border-radius: 0.48rem;
|
||||
background:
|
||||
linear-gradient(135deg, var(--filter-accent), transparent 68%),
|
||||
var(--filter-softer);
|
||||
}
|
||||
|
||||
.filter-packet-reader-heading {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.filter-packet-reader-heading h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.filter-packet-reader-heading .filter-field-help {
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.filter-packet-reader-heading > span {
|
||||
flex: 0 0 auto;
|
||||
padding: 0.2rem 0.42rem;
|
||||
border: 1px solid var(--filter-border);
|
||||
border-radius: 999px;
|
||||
color: color-mix(in srgb, var(--md-default-fg-color) 65%, transparent);
|
||||
background: var(--md-default-bg-color);
|
||||
font-size: 0.62rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.filter-packet-reader textarea {
|
||||
min-height: 5.2rem;
|
||||
}
|
||||
|
||||
.filter-packet-decode {
|
||||
margin-top: 0.85rem;
|
||||
padding-top: 0.8rem;
|
||||
border-top: 1px solid var(--filter-border);
|
||||
}
|
||||
|
||||
.filter-packet-decode > h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.filter-packet-load-status {
|
||||
margin: 0.7rem 0;
|
||||
padding: 0.58rem 0.65rem;
|
||||
border-left: 0.22rem solid #2e7d32;
|
||||
border-radius: 0.28rem;
|
||||
background: var(--filter-soft);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.42;
|
||||
}
|
||||
|
||||
.filter-packet-load-status[data-loaded="false"] {
|
||||
border-left-color: var(--filter-warning);
|
||||
}
|
||||
|
||||
.filter-packet-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.55rem;
|
||||
margin: 0.7rem 0 0;
|
||||
}
|
||||
|
||||
.filter-packet-fields > div {
|
||||
min-width: 0;
|
||||
padding: 0.58rem;
|
||||
border: 1px solid var(--filter-border);
|
||||
border-radius: 0.35rem;
|
||||
background: var(--md-code-bg-color);
|
||||
}
|
||||
|
||||
.filter-packet-fields dt {
|
||||
color: color-mix(in srgb, var(--md-default-fg-color) 58%, transparent);
|
||||
font-size: 0.62rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.filter-packet-fields dd {
|
||||
margin: 0.22rem 0 0;
|
||||
font: 0.7rem/1.45 var(--md-code-font-family);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.filter-packet-notes {
|
||||
margin-top: 0.7rem;
|
||||
}
|
||||
|
||||
.filter-tool .filter-simulator-check {
|
||||
display: flex;
|
||||
gap: 0.55rem;
|
||||
@@ -646,11 +744,20 @@
|
||||
}
|
||||
|
||||
.filter-panel-heading,
|
||||
.filter-packet-reader-heading,
|
||||
.filter-rule-card-header {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-packet-reader-heading > span {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.filter-packet-fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.filter-rule-meta {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -1415,6 +1415,29 @@ drafts are not uploaded anywhere.</p>
|
||||
</div>
|
||||
<button type="button" data-role="reset-packet">Reset packet</button>
|
||||
</div>
|
||||
<div class="filter-packet-reader">
|
||||
<div class="filter-packet-reader-heading">
|
||||
<div>
|
||||
<h3>Read a raw MeshCore packet</h3>
|
||||
<p class="filter-field-help">
|
||||
Paste an on-air packet as hexadecimal. The decoder follows the
|
||||
<a href="../packet_format/">MeshCore wire format</a>, displays its
|
||||
header, route, pbyte path, and clear payload envelope, then loads
|
||||
only facts actually present on the wire into the simulator.
|
||||
</p>
|
||||
</div>
|
||||
<span>Runs locally</span>
|
||||
</div>
|
||||
<label>
|
||||
Raw packet hex
|
||||
<textarea data-role="raw-packet-input" rows="3" spellcheck="false">014e912ceebb98918b86772df5dacf1bcba9e4127ffffaa8665596aa3e2903a3b1901fdc53497dfca6b5d7df2d771bea68de</textarea>
|
||||
</label>
|
||||
<div class="filter-builder-actions">
|
||||
<button class="filter-primary-action" type="button" data-role="decode-packet">Decode and load packet</button>
|
||||
</div>
|
||||
<div class="filter-error" data-role="packet-decode-error" role="alert" hidden></div>
|
||||
<div class="filter-packet-decode" data-role="packet-decode-result" aria-live="polite" hidden></div>
|
||||
</div>
|
||||
<div class="filter-form-grid filter-simulator-facts">
|
||||
<label>
|
||||
Received route
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Introduction","text":"<p>Welcome to the MeshCore documentation.</p> <p>Below are a few quick start guides.</p> <ul> <li>Frequently Asked Questions</li> <li>CLI Commands</li> <li>Filter Policy Playground</li> <li>Telemetry History Decoder</li> <li>CLI Availability by Firmware Build</li> <li>Easy LoRa OTA: ESP32 full images and nRF52 deltas</li> <li>MeshTower V2 microSD self-updates</li> <li>GPS Tracking</li> <li>Companion Protocol</li> <li>Packet Format</li> <li>QR Codes</li> </ul> <p>If you find a mistake in any of our documentation, or find something is missing, please feel free to open a pull request for us to review.</p> <ul> <li>Documentation Source</li> </ul>"},{"location":"WiFi/","title":"WiFi and MQTT by Firmware Type","text":"<p>MeshCore itself does not require WiFi or the internet. LoRa packet exchange, repeating, room servers, companions, and sensors can all operate without either. WiFi is added by particular ESP32 firmware targets for one or more of these purposes:</p> <ul> <li>a TCP connection between a WiFi companion and a phone or computer;</li> <li>the WebConfig browser interface;</li> <li>MQTT uplinking from the radio to internet or LAN brokers;</li> <li>WiFi-assisted OTA services on builds that include them;</li> <li>ESP-NOW bridging, which uses the ESP32's 2.4 GHz radio but is not a connection to a WiFi access point.</li> </ul> <p>The firmware role and the build profile are separate choices. For example, a logging repeater is not an MQTT observer, and a WiFi companion does not publish to MQTT unless its target name also contains <code>mqtt</code>.</p>"},{"location":"WiFi/#quick-reference","title":"Quick reference","text":"Firmware target or role Infrastructure WiFi MQTT What WiFi does <code>*_repeater</code> Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active <code>*_repeater_observer_mqtt</code> Yes Yes Uplinks heard and selected transmitted LoRa packets; repeating remains enabled by default <code>*_room_server</code> Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active <code>*_room_server_observer_mqtt</code> Yes Yes Runs the room server and uplinks radio traffic <code>*_companion_radio_wifi</code> Yes No Exposes the MeshCore companion protocol on TCP port 5000 <code>*_companion_radio_wifi_mqtt</code> Yes Yes Runs both the TCP companion interface and the MQTT uplink USB, BLE, or serial companion No No Uses the transport named by the target instead <code>*_repeater_bridge_espnow</code> Build-dependent on ESP32 No Uses ESP-NOW for its bridge; a FULL build also exposes the TCP 5001 LoRa-OTA seeder whenever WiFi is usable RS232 bridge No No Bridges through a serial interface Ethernet repeater/room server No WiFi No Uses wired Ethernet for its role-specific network interface <code>*_sensor</code> Build-dependent on ESP32 No A FULL ESP32 sensor can expose the TCP 5001 LoRa-OTA seeder through its browser-OTA setup AP Terminal-chat or KISS modem No in current targets No Uses LoRa and its role-specific local interface <code>*_lora_ota_no_external_sensors</code> On demand on ESP32 No Lean LoRa-OTA repeater image; ESP32 builds retain the compact <code>start ota</code> browser uploader <p>Direct on-device WiFi and MQTT are currently ESP32 features. nRF52, STM32, and the currently enabled RP2040 targets do not run this MQTT bridge. An nRF52 connected to a Raspberry Pi can still be logged or uplinked by software on the Pi, but that is a separate host-side bridge rather than MQTT running in the radio firmware.</p>"},{"location":"WiFi/#how-the-mqtt-bridge-works","title":"How the MQTT bridge works","text":"<p>The MQTT bridge is an outbound observer. It does not subscribe to MQTT topics and does not inject broker messages into LoRa.</p> <p>For a received packet, the flow is:</p> <pre><code>LoRa radio -> successful packet parse -> MQTT capture queue -> broker slots\n \\----> normal MeshCore filtering/handling\n</code></pre> <p>Packets transmitted by the node can also be queued according to <code>mqtt.tx</code>. Receive capture happens before the packet is passed to the normal MeshCore flood filters and forwarding decision. A packet may therefore be observed on MQTT even when a later scope, path, region, duplicate, or repeat rule prevents the node from forwarding it over LoRa.</p> <p>MQTT publication and LoRa repetition are separate:</p> <ul> <li><code>set repeat on|off</code> controls whether an observer repeats eligible LoRa packets;</li> <li><code>set bridge.enabled on|off</code> starts or stops the MQTT bridge;</li> <li><code>set mqtt.rx on|off</code> controls uplinking of received packets;</li> <li><code>set mqtt.tx Line truncated
|
||||
{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Introduction","text":"<p>Welcome to the MeshCore documentation.</p> <p>Below are a few quick start guides.</p> <ul> <li>Frequently Asked Questions</li> <li>CLI Commands</li> <li>Filter Policy Playground</li> <li>Telemetry History Decoder</li> <li>CLI Availability by Firmware Build</li> <li>Easy LoRa OTA: ESP32 full images and nRF52 deltas</li> <li>MeshTower V2 microSD self-updates</li> <li>GPS Tracking</li> <li>Companion Protocol</li> <li>Packet Format</li> <li>QR Codes</li> </ul> <p>If you find a mistake in any of our documentation, or find something is missing, please feel free to open a pull request for us to review.</p> <ul> <li>Documentation Source</li> </ul>"},{"location":"WiFi/","title":"WiFi and MQTT by Firmware Type","text":"<p>MeshCore itself does not require WiFi or the internet. LoRa packet exchange, repeating, room servers, companions, and sensors can all operate without either. WiFi is added by particular ESP32 firmware targets for one or more of these purposes:</p> <ul> <li>a TCP connection between a WiFi companion and a phone or computer;</li> <li>the WebConfig browser interface;</li> <li>MQTT uplinking from the radio to internet or LAN brokers;</li> <li>WiFi-assisted OTA services on builds that include them;</li> <li>ESP-NOW bridging, which uses the ESP32's 2.4 GHz radio but is not a connection to a WiFi access point.</li> </ul> <p>The firmware role and the build profile are separate choices. For example, a logging repeater is not an MQTT observer, and a WiFi companion does not publish to MQTT unless its target name also contains <code>mqtt</code>.</p>"},{"location":"WiFi/#quick-reference","title":"Quick reference","text":"Firmware target or role Infrastructure WiFi MQTT What WiFi does <code>*_repeater</code> Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active <code>*_repeater_observer_mqtt</code> Yes Yes Uplinks heard and selected transmitted LoRa packets; repeating remains enabled by default <code>*_room_server</code> Build-dependent on ESP32 No FULL builds provide WebConfig, browser OTA, and the TCP 5001 LoRa-OTA seeder while WiFi is active <code>*_room_server_observer_mqtt</code> Yes Yes Runs the room server and uplinks radio traffic <code>*_companion_radio_wifi</code> Yes No Exposes the MeshCore companion protocol on TCP port 5000 <code>*_companion_radio_wifi_mqtt</code> Yes Yes Runs both the TCP companion interface and the MQTT uplink USB, BLE, or serial companion No No Uses the transport named by the target instead <code>*_repeater_bridge_espnow</code> Build-dependent on ESP32 No Uses ESP-NOW for its bridge; a FULL build also exposes the TCP 5001 LoRa-OTA seeder whenever WiFi is usable RS232 bridge No No Bridges through a serial interface Ethernet repeater/room server No WiFi No Uses wired Ethernet for its role-specific network interface <code>*_sensor</code> Build-dependent on ESP32 No A FULL ESP32 sensor can expose the TCP 5001 LoRa-OTA seeder through its browser-OTA setup AP Terminal-chat or KISS modem No in current targets No Uses LoRa and its role-specific local interface <code>*_lora_ota_no_external_sensors</code> On demand on ESP32 No Lean LoRa-OTA repeater image; ESP32 builds retain the compact <code>start ota</code> browser uploader <p>Direct on-device WiFi and MQTT are currently ESP32 features. nRF52, STM32, and the currently enabled RP2040 targets do not run this MQTT bridge. An nRF52 connected to a Raspberry Pi can still be logged or uplinked by software on the Pi, but that is a separate host-side bridge rather than MQTT running in the radio firmware.</p>"},{"location":"WiFi/#how-the-mqtt-bridge-works","title":"How the MQTT bridge works","text":"<p>The MQTT bridge is an outbound observer. It does not subscribe to MQTT topics and does not inject broker messages into LoRa.</p> <p>For a received packet, the flow is:</p> <pre><code>LoRa radio -> successful packet parse -> MQTT capture queue -> broker slots\n \\----> normal MeshCore filtering/handling\n</code></pre> <p>Packets transmitted by the node can also be queued according to <code>mqtt.tx</code>. Receive capture happens before the packet is passed to the normal MeshCore flood filters and forwarding decision. A packet may therefore be observed on MQTT even when a later scope, path, region, duplicate, or repeat rule prevents the node from forwarding it over LoRa.</p> <p>MQTT publication and LoRa repetition are separate:</p> <ul> <li><code>set repeat on|off</code> controls whether an observer repeats eligible LoRa packets;</li> <li><code>set bridge.enabled on|off</code> starts or stops the MQTT bridge;</li> <li><code>set mqtt.rx on|off</code> controls uplinking of received packets;</li> <li><code>set mqtt.tx Line truncated
|
||||
Reference in new issue
Block a user