From 65400a2c648382da5a4c965b738e5c6199496b17 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 7 May 2026 08:12:26 -0500 Subject: [PATCH 01/17] 2.8: Refactor NodeInfoLite structure and add legacy support for migration --- meshtastic/deviceonly.options | 12 ++++ meshtastic/deviceonly.proto | 92 +++++++++++++++++++--------- meshtastic/deviceonly_legacy.options | 7 +++ meshtastic/deviceonly_legacy.proto | 49 +++++++++++++++ 4 files changed, 130 insertions(+), 30 deletions(-) create mode 100644 meshtastic/deviceonly_legacy.options create mode 100644 meshtastic/deviceonly_legacy.proto diff --git a/meshtastic/deviceonly.options b/meshtastic/deviceonly.options index d6aae0c4d..21e812760 100644 --- a/meshtastic/deviceonly.options +++ b/meshtastic/deviceonly.options @@ -12,6 +12,18 @@ *NodeInfoLite.hops_away int_size:8 *NodeInfoLite.next_hop int_size:8 +# Flattened user fields. long_name was 40 on UserLite; trimmed to 25 here so the +# slim header gives back ~15 B/node of RAM. Names that exceeded 24 chars on the +# wire (max 40) get truncated on store, with sanitizeUtf8 cleaning up partial +# multi-byte sequences at the boundary. +*NodeInfoLite.long_name max_size:25 +*NodeInfoLite.short_name max_size:5 +*NodeInfoLite.public_key max_size:32 +# hw_model + role used to be 32-bit enums inside UserLite. We pin them to one +# byte each on the slim header to claw back the alignment overhead. +*NodeInfoLite.hw_model int_size:8 +*NodeInfoLite.role int_size:8 + *UserLite.long_name max_size:40 *UserLite.short_name max_size:5 *UserLite.public_key max_size:32 # public key diff --git a/meshtastic/deviceonly.proto b/meshtastic/deviceonly.proto index b7c377a8f..c21bb7d8d 100644 --- a/meshtastic/deviceonly.proto +++ b/meshtastic/deviceonly.proto @@ -107,16 +107,11 @@ message NodeInfoLite { */ uint32 num = 1; - /* - * The user info for this node - */ - UserLite user = 2; - - /* - * This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true. - * Position.time now indicates the last time we received a POSITION from that node. - */ - PositionLite position = 3; + // Tag 2 was UserLite user; flattened into tags 14..18 (presence via + // NODEINFO_BITFIELD_HAS_USER). + // Tag 3 was PositionLite position; moved to NodeDatabase.positions. + reserved 2, 3; + reserved "user", "position"; /* * Returns the Signal-to-noise ratio (SNR) of the last received message, @@ -128,20 +123,19 @@ message NodeInfoLite { * Set to indicate the last time we received a packet from this node */ fixed32 last_heard = 5; - /* - * The latest device metrics for the node. - */ - DeviceMetrics device_metrics = 6; + + // Tag 6 was DeviceMetrics device_metrics; moved to NodeDatabase.telemetry. + reserved 6; + reserved "device_metrics"; /* * local channel index we heard that node on. Only populated if its not the default channel. */ uint32 channel = 7; - /* - * True if we witnessed the node over MQTT instead of LoRA transport - */ - bool via_mqtt = 8; + // Tags 8/10/11 were via_mqtt/is_favorite/is_ignored bools; packed into bitfield. + reserved 8, 10, 11; + reserved "via_mqtt", "is_favorite", "is_ignored"; /* * Number of hops away from us this node is (0 if direct neighbor) @@ -149,28 +143,42 @@ message NodeInfoLite { optional uint32 hops_away = 9; /* - * True if node is in our favorites list - * Persists between NodeDB internal clean ups + * Last byte of the node number of the node that should be used as the next hop to reach this node. */ - bool is_favorite = 10; + uint32 next_hop = 12; /* - * True if node is in our ignored list - * Persists between NodeDB internal clean ups + * Bitfield for storing booleans. See NODEINFO_BITFIELD_* in src/mesh/NodeDB.h. */ - bool is_ignored = 11; + uint32 bitfield = 13; + + // Flattened user fields (formerly UserLite). macaddr dropped (deprecated 1.2.11). /* - * Last byte of the node number of the node that should be used as the next hop to reach this node. + * A full name for this user, i.e. "Kevin Hester". */ - uint32 next_hop = 12; + string long_name = 14; /* - * Bitfield for storing booleans. - * LSB 0 is_key_manually_verified - * LSB 1 is_muted + * A VERY short name, ideally two characters or an emoji. + * Suitable for a tiny OLED screen. */ - uint32 bitfield = 13; + string short_name = 15; + + /* + * Hardware model the user's device is running. + */ + HardwareModel hw_model = 16; + + /* + * The user's role in the mesh. + */ + Config.DeviceConfig.Role role = 17; + + /* + * The public key of the user's device, for PKI-based encrypted DMs. + */ + bytes public_key = 18; } /* @@ -236,6 +244,24 @@ message DeviceState { repeated NodeRemoteHardwarePin node_remote_hardware_pins = 13; } +// Satellite per-node entries; stored alongside the slim NodeInfoLite so nodes +// that never report don't pay the embedded cost. + +message NodePositionEntry { + uint32 num = 1; + PositionLite position = 2; +} + +message NodeTelemetryEntry { + uint32 num = 1; + DeviceMetrics device_metrics = 2; +} + +message NodeStatusEntry { + uint32 num = 1; + StatusMessage status = 2; +} + message NodeDatabase { /* * A version integer used to invalidate old save files when we make @@ -248,6 +274,12 @@ message NodeDatabase { * New lite version of NodeDB to decrease memory footprint */ repeated NodeInfoLite nodes = 2 [(nanopb).callback_datatype = "std::vector"]; + + // Per-NodeNum satellite arrays. Constrained platforms (e.g. STM32WL) omit + // these via MESHTASTIC_EXCLUDE_*DB build flags. + repeated NodePositionEntry positions = 3 [(nanopb).callback_datatype = "std::vector"]; + repeated NodeTelemetryEntry telemetry = 4 [(nanopb).callback_datatype = "std::vector"]; + repeated NodeStatusEntry status = 5 [(nanopb).callback_datatype = "std::vector"]; } /* diff --git a/meshtastic/deviceonly_legacy.options b/meshtastic/deviceonly_legacy.options new file mode 100644 index 000000000..e23758f55 --- /dev/null +++ b/meshtastic/deviceonly_legacy.options @@ -0,0 +1,7 @@ +# nanopb options for the legacy migration descriptor. +# Mirrors deviceonly.options sizing for the NodeInfoLite_Legacy message so old +# /prefs/nodes.proto saves parse with the same byte layout. + +*NodeInfoLite_Legacy.channel int_size:8 +*NodeInfoLite_Legacy.hops_away int_size:8 +*NodeInfoLite_Legacy.next_hop int_size:8 diff --git a/meshtastic/deviceonly_legacy.proto b/meshtastic/deviceonly_legacy.proto new file mode 100644 index 000000000..944c42f7c --- /dev/null +++ b/meshtastic/deviceonly_legacy.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; + +package meshtastic; + +/* trunk-ignore(buf-lint/COMPILE) */ +import "meshtastic/deviceonly.proto"; +import "meshtastic/telemetry.proto"; +import "nanopb.proto"; + +option csharp_namespace = "Meshtastic.Protobufs"; +option go_package = "github.com/meshtastic/go/generated"; +option java_outer_classname = "DeviceOnlyLegacy"; +option java_package = "org.meshtastic.proto"; +option swift_prefix = ""; +option (nanopb_fileopt).include = ""; + +/* + * Legacy NodeInfoLite descriptor used only to decode pre-split + * /prefs/nodes.proto saves during the v24 -> v25 migration boot. + * Field tags here mirror the original NodeInfoLite tags 3 and 6 so that + * old wire bytes parse cleanly. Steady-state code does not use this + * struct; it is dropped after migration completes. This file should be + * removed once DEVICESTATE_MIN_VER advances past 24. + */ +message NodeInfoLite_Legacy { + uint32 num = 1; + UserLite user = 2; + PositionLite position = 3; + float snr = 4; + fixed32 last_heard = 5; + DeviceMetrics device_metrics = 6; + uint32 channel = 7; + bool via_mqtt = 8; + optional uint32 hops_away = 9; + bool is_favorite = 10; + bool is_ignored = 11; + uint32 next_hop = 12; + uint32 bitfield = 13; +} + +/* + * Legacy NodeDatabase shape: one repeated array of fat NodeInfoLite_Legacy + * with no satellite position/telemetry arrays. + */ +message NodeDatabase_Legacy { + uint32 version = 1; + repeated NodeInfoLite_Legacy nodes = 2 + [(nanopb).callback_datatype = "std::vector"]; +} From 246ee67c8c6c68ccaeb16f47975eacb9f4bf9432 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 7 May 2026 08:23:51 -0500 Subject: [PATCH 02/17] Add NodeEnvironmentEntry message --- meshtastic/deviceonly.proto | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/meshtastic/deviceonly.proto b/meshtastic/deviceonly.proto index c21bb7d8d..fbd56b9e1 100644 --- a/meshtastic/deviceonly.proto +++ b/meshtastic/deviceonly.proto @@ -257,6 +257,11 @@ message NodeTelemetryEntry { DeviceMetrics device_metrics = 2; } +message NodeEnvironmentEntry { + uint32 num = 1; + EnvironmentMetrics environment_metrics = 2; +} + message NodeStatusEntry { uint32 num = 1; StatusMessage status = 2; @@ -280,6 +285,8 @@ message NodeDatabase { repeated NodePositionEntry positions = 3 [(nanopb).callback_datatype = "std::vector"]; repeated NodeTelemetryEntry telemetry = 4 [(nanopb).callback_datatype = "std::vector"]; repeated NodeStatusEntry status = 5 [(nanopb).callback_datatype = "std::vector"]; + repeated NodeEnvironmentEntry environment = 6 + [(nanopb).callback_datatype = "std::vector"]; } /* From 794d61dca8d87bd39c0f02f2526d3c90ef221691 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 13:55:04 +0000 Subject: [PATCH 03/17] Apply buf format to deviceonly.proto and deviceonly_legacy.proto Agent-Logs-Url: https://github.com/meshtastic/protobufs/sessions/df6c83e6-b057-425c-8533-f83df8ed0c75 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- meshtastic/deviceonly.proto | 3 +-- meshtastic/deviceonly_legacy.proto | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/meshtastic/deviceonly.proto b/meshtastic/deviceonly.proto index fbd56b9e1..445078c4f 100644 --- a/meshtastic/deviceonly.proto +++ b/meshtastic/deviceonly.proto @@ -285,8 +285,7 @@ message NodeDatabase { repeated NodePositionEntry positions = 3 [(nanopb).callback_datatype = "std::vector"]; repeated NodeTelemetryEntry telemetry = 4 [(nanopb).callback_datatype = "std::vector"]; repeated NodeStatusEntry status = 5 [(nanopb).callback_datatype = "std::vector"]; - repeated NodeEnvironmentEntry environment = 6 - [(nanopb).callback_datatype = "std::vector"]; + repeated NodeEnvironmentEntry environment = 6 [(nanopb).callback_datatype = "std::vector"]; } /* diff --git a/meshtastic/deviceonly_legacy.proto b/meshtastic/deviceonly_legacy.proto index 944c42f7c..bc0bf0593 100644 --- a/meshtastic/deviceonly_legacy.proto +++ b/meshtastic/deviceonly_legacy.proto @@ -44,6 +44,5 @@ message NodeInfoLite_Legacy { */ message NodeDatabase_Legacy { uint32 version = 1; - repeated NodeInfoLite_Legacy nodes = 2 - [(nanopb).callback_datatype = "std::vector"]; + repeated NodeInfoLite_Legacy nodes = 2 [(nanopb).callback_datatype = "std::vector"]; } From b24d92a9522fb12920a1bc2020bf6677b61baf38 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 7 May 2026 08:56:27 -0500 Subject: [PATCH 04/17] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- meshtastic/deviceonly_legacy.proto | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/meshtastic/deviceonly_legacy.proto b/meshtastic/deviceonly_legacy.proto index bc0bf0593..a4bb02401 100644 --- a/meshtastic/deviceonly_legacy.proto +++ b/meshtastic/deviceonly_legacy.proto @@ -17,9 +17,11 @@ option (nanopb_fileopt).include = ""; /* * Legacy NodeInfoLite descriptor used only to decode pre-split * /prefs/nodes.proto saves during the v24 -> v25 migration boot. - * Field tags here mirror the original NodeInfoLite tags 3 and 6 so that - * old wire bytes parse cleanly. Steady-state code does not use this - * struct; it is dropped after migration completes. This file should be + * This preserves the original NodeInfoLite-compatible field numbers needed + * to parse old wire bytes cleanly, including user (2), position (3), + * device_metrics (6), and the legacy-only compatibility fields via_mqtt (8), + * is_favorite (10), and is_ignored (11). Steady-state code does not use + * this struct; it is dropped after migration completes. This file should be * removed once DEVICESTATE_MIN_VER advances past 24. */ message NodeInfoLite_Legacy { From ff5b392503776bf13073034070543d5c5aa1acf7 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 11 May 2026 20:34:31 -0500 Subject: [PATCH 05/17] Add precision_bits field to PositionLite message for node precision indication --- meshtastic/deviceonly.proto | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/meshtastic/deviceonly.proto b/meshtastic/deviceonly.proto index 445078c4f..64e44ea56 100644 --- a/meshtastic/deviceonly.proto +++ b/meshtastic/deviceonly.proto @@ -50,6 +50,11 @@ message PositionLite { * TODO: REPLACE */ Position.LocSource location_source = 5; + + /* + * Indicates the bits of precision set by the sending node + */ + uint32 precision_bits = 6; } message UserLite { From 108919393a2a3fdf6ab82e50e10965e74394620f Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 13 May 2026 14:30:28 -0500 Subject: [PATCH 06/17] Add initial protobufs for XEdDSA (#753) * Add initial protobufs for XEdDSA * Add nodeinfo bool has_xeddsa_signed * Remove optional tag from xeddsa sig field * Add missed comment marker * Apply buf format to mesh.proto Agent-Logs-Url: https://github.com/meshtastic/protobufs/sessions/f6082d6b-c47b-42a3-bda3-e269d0b63226 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- meshtastic/mesh.options | 1 + meshtastic/mesh.proto | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/meshtastic/mesh.options b/meshtastic/mesh.options index 7d3f8fea3..27fc61f4a 100644 --- a/meshtastic/mesh.options +++ b/meshtastic/mesh.options @@ -19,6 +19,7 @@ # outside of this envelope *Data.payload max_size:233 *Data.bitfield int_size:8 +*Data.xeddsa_signature max_size:64 *NodeInfo.channel int_size:8 *NodeInfo.hops_away int_size:8 diff --git a/meshtastic/mesh.proto b/meshtastic/mesh.proto index 241f09d0a..a6c563249 100644 --- a/meshtastic/mesh.proto +++ b/meshtastic/mesh.proto @@ -899,9 +899,9 @@ enum HardwareModel { HELTEC_MESH_NODE_T1 = 133; /* - * B&Q Consulting Station G3: TBD + * B&Q Consulting Station G3: TBD - */ + */ STATION_G3 = 134; /* * ------------------------------------------------------------------------------------------------------------------------------------------ @@ -1200,6 +1200,11 @@ message Data { * Bitfield for extra flags. First use is to indicate that user approves the packet being uploaded to MQTT. */ optional uint32 bitfield = 9; + + /* + * XEdDSA signature for the payload + */ + bytes xeddsa_signature = 10; } /* @@ -1779,6 +1784,11 @@ message MeshPacket { * Indicates which transport mechanism this packet arrived over */ TransportMechanism transport_mechanism = 21; + + /* + * Indicates whether the packet has a valid signature + */ + bool xeddsa_signed = 22; } /* @@ -1915,6 +1925,13 @@ message NodeInfo { * Persistes between NodeDB internal clean ups */ bool is_muted = 13; + + /* + * True if node is signing its packets via XEdDSA + * Persists between NodeDB internal clean ups + * LSB 1 of the bitfield + */ + bool has_xeddsa_signed = 14; } /* From c0e9d0bde004bf064ef6ecb1f9d3c1e3d47a3b8e Mon Sep 17 00:00:00 2001 From: niccellular <79813408+niccellular@users.noreply.github.com> Date: Fri, 15 May 2026 13:17:53 -0400 Subject: [PATCH 07/17] Add LockdownAuth.max_session_seconds for uptime-bounded sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-boot cap on how long a single auto-unlocked session can hold the device's encrypted storage open, in seconds. 0 = unlimited (current behavior, suitable for unattended infrastructure nodes). When non-zero, the firmware arms an uptime timer at unlock. On expiry the device revokes per-connection auth, re-engages screen redaction, and reboots without deleting the token; next boot auto-unlocks via the boot-count TTL (decrementing boots_remaining) and arms a fresh session. Total exposure ceiling = boots_remaining * max_session_seconds. Uses CPU uptime (millis), not wall-clock time, so the cap is immune to GPS spoofing, RTC backup-battery removal, and Faraday cage isolation — none of those move the uptime counter. The only way to reset the session clock is a reboot, which costs a boot from the HMAC-bound on-flash counter. Companion firmware change: meshtastic/firmware PR #10349. --- meshtastic/admin.proto | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/meshtastic/admin.proto b/meshtastic/admin.proto index e2e1eed41..c23254b39 100644 --- a/meshtastic/admin.proto +++ b/meshtastic/admin.proto @@ -581,6 +581,30 @@ message LockdownAuth { * the locked state. Always honoured regardless of current lock state. */ bool lock_now = 4; + + /* + * Optional per-boot uptime cap on the unlocked session, in seconds. + * 0 = unlimited (token-only enforcement, suitable for unattended + * tower / infrastructure nodes). + * + * When non-zero, the firmware arms an uptime timer at the moment of + * unlock. On expiry the device revokes per-connection admin auth, + * re-engages screen redaction, and reboots — without deleting the + * token, so the next boot auto-unlocks via the boot-count TTL + * (decrementing boots_remaining) and arms a fresh session window. + * Total exposure ceiling = boots_remaining * max_session_seconds. + * + * The cap is persisted in the token, so it survives token-based + * auto-unlock across reboots. Explicit operator Lock Now still + * deletes the token and forces passphrase re-entry. + * + * Uses millis() (CPU uptime), not wall-clock time, so the cap is + * immune to GPS spoofing, RTC backup-battery removal, and Faraday + * cage isolation — none of those move the uptime counter. The only + * way to reset the session clock is a reboot, which costs a boot + * from the on-flash, HMAC-bound counter. + */ + uint32 max_session_seconds = 5; } /* From 0db78090a9ef599dd5106f4035dedbe354d8bbff Mon Sep 17 00:00:00 2001 From: niccellular <79813408+niccellular@users.noreply.github.com> Date: Fri, 15 May 2026 13:46:06 -0400 Subject: [PATCH 08/17] Clarify exposure-ceiling formula re: boots_remaining=0 sentinel Address review feedback: the formula 'boots_remaining * max_session_seconds' is ambiguous because boots_remaining=0 in the request means 'use firmware default' (TOKEN_DEFAULT_BOOTS), not literally zero boots. A client that sends only max_session_seconds and leaves boots_remaining=0 cannot compute the ceiling from the raw request fields. Document that the formula uses the resolved (post-default) boot count, and call out the sentinel explicitly. --- meshtastic/admin.proto | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/meshtastic/admin.proto b/meshtastic/admin.proto index c23254b39..f2feaeaf1 100644 --- a/meshtastic/admin.proto +++ b/meshtastic/admin.proto @@ -591,8 +591,16 @@ message LockdownAuth { * unlock. On expiry the device revokes per-connection admin auth, * re-engages screen redaction, and reboots — without deleting the * token, so the next boot auto-unlocks via the boot-count TTL - * (decrementing boots_remaining) and arms a fresh session window. - * Total exposure ceiling = boots_remaining * max_session_seconds. + * (decrementing the token's boot count) and arms a fresh session window. + * + * Total exposure ceiling = (resolved boot count) * max_session_seconds. + * The resolved boot count is the value the firmware writes into the + * token at unlock time: the client-supplied boots_remaining when + * non-zero, otherwise the firmware default (TOKEN_DEFAULT_BOOTS). + * Note that boots_remaining == 0 in this message means "use firmware + * default", NOT "zero boots" — a client computing the ceiling for + * display should mirror that resolution rather than multiplying the + * raw request value. * * The cap is persisted in the token, so it survives token-based * auto-unlock across reboots. Explicit operator Lock Now still From 7c44755bfdffeb5903a06a4afcbba58a6b4f24da Mon Sep 17 00:00:00 2001 From: niccellular <79813408+niccellular@users.noreply.github.com> Date: Fri, 15 May 2026 14:05:50 -0400 Subject: [PATCH 09/17] Fix off-by-one in exposure-ceiling formula The initial passphrase-unlocked session counts toward total exposure too, since boots_remaining is the count of *subsequent* token-driven auto-unlocks (not total sessions). Ceiling is (resolved_boot_count + 1) * max_session_seconds. --- meshtastic/admin.proto | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/meshtastic/admin.proto b/meshtastic/admin.proto index f2feaeaf1..dce6be7dc 100644 --- a/meshtastic/admin.proto +++ b/meshtastic/admin.proto @@ -593,14 +593,17 @@ message LockdownAuth { * token, so the next boot auto-unlocks via the boot-count TTL * (decrementing the token's boot count) and arms a fresh session window. * - * Total exposure ceiling = (resolved boot count) * max_session_seconds. - * The resolved boot count is the value the firmware writes into the - * token at unlock time: the client-supplied boots_remaining when - * non-zero, otherwise the firmware default (TOKEN_DEFAULT_BOOTS). - * Note that boots_remaining == 0 in this message means "use firmware - * default", NOT "zero boots" — a client computing the ceiling for - * display should mirror that resolution rather than multiplying the - * raw request value. + * Total exposure ceiling = ((resolved boot count) + 1) * max_session_seconds. + * The +1 accounts for the initial passphrase-unlocked session + * itself, since boots_remaining is the number of subsequent + * token-driven auto-unlocks. The resolved boot count is the value + * the firmware writes into the token at unlock time: the + * client-supplied boots_remaining when non-zero, otherwise the + * firmware default (TOKEN_DEFAULT_BOOTS). Note that + * boots_remaining == 0 in this message means "use firmware default", + * NOT "zero boots" — a client computing the ceiling for display + * should mirror that resolution rather than multiplying the raw + * request value. * * The cap is persisted in the token, so it survives token-based * auto-unlock across reboots. Explicit operator Lock Now still From 9ab0b36177e3d466072e1aafc44dd6d14c11fa0b Mon Sep 17 00:00:00 2001 From: niccellular <79813408+niccellular@users.noreply.github.com> Date: Fri, 15 May 2026 14:47:24 -0400 Subject: [PATCH 10/17] Refine session-expiry behavior doc: decrement in place, reboot only on exhaustion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The firmware was updated to decrement the on-flash boot count at each session expiry without rebooting (while budget remains), and only hard-lock + reboot when the boot count reaches zero. Mesh routing continues across session boundaries; per-connection auth is revoked and the screen lock re-engages at each roll so clients must re-auth to see content. The exposure ceiling is unchanged — the boot count still ticks down monotonically once per session boundary. Companion firmware change: meshtastic/firmware PR #10349 (commit d2b47e4ca). --- meshtastic/admin.proto | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/meshtastic/admin.proto b/meshtastic/admin.proto index dce6be7dc..06dd7e082 100644 --- a/meshtastic/admin.proto +++ b/meshtastic/admin.proto @@ -587,23 +587,26 @@ message LockdownAuth { * 0 = unlimited (token-only enforcement, suitable for unattended * tower / infrastructure nodes). * - * When non-zero, the firmware arms an uptime timer at the moment of - * unlock. On expiry the device revokes per-connection admin auth, - * re-engages screen redaction, and reboots — without deleting the - * token, so the next boot auto-unlocks via the boot-count TTL - * (decrementing the token's boot count) and arms a fresh session window. + * When non-zero, the firmware arms an uptime timer at unlock. On + * each expiry, while there is still boot-count budget, the firmware + * decrements the on-flash boot count in place, revokes per- + * connection admin auth (clients must re-authenticate to see + * content), re-engages the screen lock, and re-arms the timer + * without rebooting. Mesh routing keeps running across session + * boundaries; only when the boot-count budget reaches zero does + * the device hard-lock and reboot. * * Total exposure ceiling = ((resolved boot count) + 1) * max_session_seconds. * The +1 accounts for the initial passphrase-unlocked session * itself, since boots_remaining is the number of subsequent - * token-driven auto-unlocks. The resolved boot count is the value - * the firmware writes into the token at unlock time: the - * client-supplied boots_remaining when non-zero, otherwise the - * firmware default (TOKEN_DEFAULT_BOOTS). Note that - * boots_remaining == 0 in this message means "use firmware default", - * NOT "zero boots" — a client computing the ceiling for display - * should mirror that resolution rather than multiplying the raw - * request value. + * session rolls (each consuming one boot from the rollback ledger). + * The resolved boot count is the value the firmware writes into the + * token at unlock time: the client-supplied boots_remaining when + * non-zero, otherwise the firmware default (TOKEN_DEFAULT_BOOTS). + * Note that boots_remaining == 0 in this message means "use firmware + * default", NOT "zero boots" — a client computing the ceiling for + * display should mirror that resolution rather than multiplying the + * raw request value. * * The cap is persisted in the token, so it survives token-based * auto-unlock across reboots. Explicit operator Lock Now still From b61834fe8e0a5a7b31f38647acabb7d00a9fd3a7 Mon Sep 17 00:00:00 2001 From: vidplace7 Date: Fri, 22 May 2026 11:55:16 -0400 Subject: [PATCH 11/17] Add ITU Region 1 and Region 2/3 Amateur Radio 70cm band identifiers to config.proto --- meshtastic/config.proto | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/meshtastic/config.proto b/meshtastic/config.proto index 1e7fa9970..b3e172012 100644 --- a/meshtastic/config.proto +++ b/meshtastic/config.proto @@ -960,6 +960,17 @@ message Config { * EU 868MHz band, with narrow presets */ EU_N_868 = 32; + + /* + * ITU Region 1 Amateur Radio 70cm band (430-440 MHz) + */ + ITU1_70CM = 33; + + /* + * ITU Region 2 / 3 Amateur Radio 70cm band (430-450 MHz) + * Note: Some countries do not allocate 440-450 MHz. Check local law! + */ + ITU23_70CM = 34; } /* From b9fe83c48cf4f6a52736885ac6004a5c6f5a291a Mon Sep 17 00:00:00 2001 From: vidplace7 Date: Sun, 24 May 2026 17:05:42 -0400 Subject: [PATCH 12/17] Update ham regions, split by ITU Stop combining ITU 2 and 3. They will need VERY different default slotOverrides (default freq). In effect, this means each ham band we support will need 3 "regions", ITU1, ITU2, ITU3. This will allow us to better support hams around the world without stirring anger. --- meshtastic/config.proto | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/meshtastic/config.proto b/meshtastic/config.proto index 68c5f5c1f..8f554d0fc 100644 --- a/meshtastic/config.proto +++ b/meshtastic/config.proto @@ -972,10 +972,17 @@ message Config { ITU1_70CM = 34; /* - * ITU Region 2 / 3 Amateur Radio 70cm band (430-450 MHz) + * ITU Region 2 Amateur Radio 70cm band (420-450 MHz) + * Note: Some countries do not allocate 420-430 MHz or 440-450 MHz. + * Check local law! + */ + ITU2_70CM = 35; + + /* + * ITU Region 3 Amateur Radio 70cm band (430-450 MHz) * Note: Some countries do not allocate 440-450 MHz. Check local law! */ - ITU23_70CM = 35; + ITU3_70CM = 36; } /* From ce55ba3cea96f2097d26b0af6dc4de2b814f5d35 Mon Sep 17 00:00:00 2001 From: nomdetom Date: Tue, 2 Jun 2026 21:52:03 +0100 Subject: [PATCH 13/17] feat: add MeshBeacon portnum, wire message, and module config --- meshtastic/mesh_beacon.options | 3 ++ meshtastic/mesh_beacon.proto | 42 ++++++++++++++++++ meshtastic/module_config.options | 6 +++ meshtastic/module_config.proto | 73 ++++++++++++++++++++++++++++++++ meshtastic/portnums.proto | 8 ++++ 5 files changed, 132 insertions(+) create mode 100644 meshtastic/mesh_beacon.options create mode 100644 meshtastic/mesh_beacon.proto diff --git a/meshtastic/mesh_beacon.options b/meshtastic/mesh_beacon.options new file mode 100644 index 000000000..cd4099cde --- /dev/null +++ b/meshtastic/mesh_beacon.options @@ -0,0 +1,3 @@ +*MeshBeacon.message max_size:101 +*MeshBeacon.offer_channel.name max_size:12 +*MeshBeacon.offer_channel.psk max_size:32 diff --git a/meshtastic/mesh_beacon.proto b/meshtastic/mesh_beacon.proto new file mode 100644 index 000000000..3ca59f54e --- /dev/null +++ b/meshtastic/mesh_beacon.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; + +package meshtastic; + +import "meshtastic/channel.proto"; +import "meshtastic/config.proto"; + +option csharp_namespace = "Meshtastic.Protobufs"; +option go_package = "github.com/meshtastic/go/generated"; +option java_outer_classname = "MeshBeaconProtos"; +option java_package = "org.meshtastic.proto"; +option swift_prefix = ""; + +/* + * Payload for MESH_BEACON_APP packets. + * Periodically broadcast by nodes in beacon mode. + * Listeners deliver the text message to the local inbox and cache any offered + * channel/preset for the client app to act on — the firmware never auto-applies them. + */ +message MeshBeacon { + /* + * Human-readable beacon message. Max 100 bytes enforced by firmware on send. + */ + string message = 1; + + /* + * Optional channel (name + PSK) being advertised to listening clients. + * A client app may offer to switch the user to this channel; firmware never applies it automatically. + */ + ChannelSettings offer_channel = 2; + + /* + * Optional region being advertised alongside offer_preset. + */ + Config.LoRaConfig.RegionCode offer_region = 3; + + /* + * Optional modem preset being advertised. + * Combined with offer_region, tells a client "there is a mesh on this preset/region". + */ + Config.LoRaConfig.ModemPreset offer_preset = 4; +} diff --git a/meshtastic/module_config.options b/meshtastic/module_config.options index c6158befa..1a4efaa9a 100644 --- a/meshtastic/module_config.options +++ b/meshtastic/module_config.options @@ -29,3 +29,9 @@ *DetectionSensorConfig.detection_trigger_type max_size:8 *StatusMessageConfig.node_status max_size:80 + +*MeshBeaconConfig.broadcast_message max_size:101 +*MeshBeaconConfig.broadcast_offer_channel.name max_size:12 +*MeshBeaconConfig.broadcast_offer_channel.psk max_size:32 +*MeshBeaconConfig.broadcast_on_channel.name max_size:12 +*MeshBeaconConfig.broadcast_on_channel.psk max_size:32 diff --git a/meshtastic/module_config.proto b/meshtastic/module_config.proto index a134f74c5..987cf42e3 100644 --- a/meshtastic/module_config.proto +++ b/meshtastic/module_config.proto @@ -857,6 +857,74 @@ message ModuleConfig { string node_status = 1; } + /* + * MeshBeacon module config + */ + message MeshBeaconConfig { + /* + * Enable receiving MESH_BEACON_APP packets from other nodes. + * The text portion is delivered to the local message inbox. + * Offered channel/preset are stored for the client app to act on. + */ + bool listen_enabled = 1; + + /* + * Enable periodically broadcasting MESH_BEACON_APP packets from this node. + */ + bool broadcast_enabled = 2; + + /* + * Optional: node ID to send beacon messages AS. + * When set, the `from` field of outgoing beacon packets is set to this node ID, + * making beacons appear to originate from that node. + * When unset (0), beacons are sent as the local node. + * A remote admin can only set this field to their own node ID. + */ + uint32 broadcast_send_as_node = 3; + + /* + * Message to include in each beacon broadcast. Max 100 bytes enforced by firmware. + */ + string broadcast_message = 4; + + /* + * Optional channel (name + PSK) to advertise in the MeshBeacon offer_channel field. + */ + ChannelSettings broadcast_offer_channel = 5; + + /* + * Optional region to advertise in the MeshBeacon offer_region field. + */ + Config.LoRaConfig.RegionCode broadcast_offer_region = 6; + + /* + * Optional modem preset to advertise in the MeshBeacon offer_preset field. + */ + Config.LoRaConfig.ModemPreset broadcast_offer_preset = 7; + + /* + * Channel settings (name + PSK) to use when sending beacons. + * If unset, beacons go out on the primary channel. + */ + ChannelSettings broadcast_on_channel = 8; + + /* + * Region to use when sending beacons on broadcast_on_preset. + */ + Config.LoRaConfig.RegionCode broadcast_on_region = 9; + + /* + * Modem preset to use when sending beacons. + * If different from current config, the radio is temporarily switched for TX. + */ + Config.LoRaConfig.ModemPreset broadcast_on_preset = 10; + + /* + * How often to broadcast, in seconds. Min 3600 (1 h), max 259200 (72 h). Default 3600. + */ + uint32 broadcast_interval_secs = 11; + } + /* * TODO: REPLACE */ @@ -940,6 +1008,11 @@ message ModuleConfig { * TAK team/role configuration for TAK_TRACKER */ TAKConfig tak = 16; + + /* + * MeshBeacon module config + */ + MeshBeaconConfig mesh_beacon = 17; } /* diff --git a/meshtastic/portnums.proto b/meshtastic/portnums.proto index 61412cfe5..5ebe26fb0 100644 --- a/meshtastic/portnums.proto +++ b/meshtastic/portnums.proto @@ -155,6 +155,14 @@ enum PortNum { */ NODE_STATUS_APP = 36; + /* + * Beacon module broadcast packets. + * ENCODING: protobuf (MeshBeacon) + * Periodically broadcast by nodes in beacon mode; received by nodes with listen_enabled. + * Carries a text message plus optional channel/preset offers for client apps. + */ + MESH_BEACON_APP = 37; + /* * Provides a hardware serial interface to send and receive from the Meshtastic network. * Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic From 6546d167d94c715e4d75ad6fbc77d50b2e57854d Mon Sep 17 00:00:00 2001 From: nomdetom Date: Tue, 2 Jun 2026 21:53:57 +0100 Subject: [PATCH 14/17] fix: add missing channel.proto and config.proto imports to module_config --- meshtastic/module_config.proto | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meshtastic/module_config.proto b/meshtastic/module_config.proto index 987cf42e3..001e9c94c 100644 --- a/meshtastic/module_config.proto +++ b/meshtastic/module_config.proto @@ -3,6 +3,8 @@ syntax = "proto3"; package meshtastic; import "meshtastic/atak.proto"; +import "meshtastic/channel.proto"; +import "meshtastic/config.proto"; option csharp_namespace = "Meshtastic.Protobufs"; option go_package = "github.com/meshtastic/go/generated"; From 3d8599d285835996b57f9b931cbc636245624985 Mon Sep 17 00:00:00 2001 From: nomdetom Date: Tue, 2 Jun 2026 22:13:13 +0100 Subject: [PATCH 15/17] feat: add mesh_beacon field to LocalModuleConfig (localonly.proto) --- meshtastic/localonly.proto | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/meshtastic/localonly.proto b/meshtastic/localonly.proto index 2a6c7cacd..4f846e538 100644 --- a/meshtastic/localonly.proto +++ b/meshtastic/localonly.proto @@ -146,6 +146,11 @@ message LocalModuleConfig { */ ModuleConfig.TAKConfig tak = 17; + /* + * MeshBeacon Config + */ + ModuleConfig.MeshBeaconConfig mesh_beacon = 18; + /* * A version integer used to invalidate old save files when we make * incompatible changes This integer is set at build time and is private to From 23b869d373b7a46b33adfc081ba2fe52a871200c Mon Sep 17 00:00:00 2001 From: nomdetom Date: Wed, 3 Jun 2026 06:20:37 +0100 Subject: [PATCH 16/17] mmmmm... beacon --- meshtastic/mesh_beacon.proto | 2 +- meshtastic/module_config.proto | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/meshtastic/mesh_beacon.proto b/meshtastic/mesh_beacon.proto index 3ca59f54e..c6005ab71 100644 --- a/meshtastic/mesh_beacon.proto +++ b/meshtastic/mesh_beacon.proto @@ -38,5 +38,5 @@ message MeshBeacon { * Optional modem preset being advertised. * Combined with offer_region, tells a client "there is a mesh on this preset/region". */ - Config.LoRaConfig.ModemPreset offer_preset = 4; + optional Config.LoRaConfig.ModemPreset offer_preset = 4; } diff --git a/meshtastic/module_config.proto b/meshtastic/module_config.proto index 001e9c94c..5d4878a17 100644 --- a/meshtastic/module_config.proto +++ b/meshtastic/module_config.proto @@ -902,7 +902,7 @@ message ModuleConfig { /* * Optional modem preset to advertise in the MeshBeacon offer_preset field. */ - Config.LoRaConfig.ModemPreset broadcast_offer_preset = 7; + optional Config.LoRaConfig.ModemPreset broadcast_offer_preset = 7; /* * Channel settings (name + PSK) to use when sending beacons. @@ -919,7 +919,7 @@ message ModuleConfig { * Modem preset to use when sending beacons. * If different from current config, the radio is temporarily switched for TX. */ - Config.LoRaConfig.ModemPreset broadcast_on_preset = 10; + optional Config.LoRaConfig.ModemPreset broadcast_on_preset = 10; /* * How often to broadcast, in seconds. Min 3600 (1 h), max 259200 (72 h). Default 3600. From d682190d03892b6281dd59ae45eca33b00185e26 Mon Sep 17 00:00:00 2001 From: nomdetom Date: Wed, 3 Jun 2026 22:10:54 +0100 Subject: [PATCH 17/17] legacy mode activate --- meshtastic/module_config.proto | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/meshtastic/module_config.proto b/meshtastic/module_config.proto index 5d4878a17..d74de6cab 100644 --- a/meshtastic/module_config.proto +++ b/meshtastic/module_config.proto @@ -925,6 +925,16 @@ message ModuleConfig { * How often to broadcast, in seconds. Min 3600 (1 h), max 259200 (72 h). Default 3600. */ uint32 broadcast_interval_secs = 11; + + /* + * When true and both broadcast_message and offer content (preset/channel/region) are present, + * the broadcaster splits them into two separate packets instead of one combined MESH_BEACON_APP: + * - Packet A: MESH_BEACON_APP carrying only the offer fields (no text) on the beacon radio config. + * - Packet B: TEXT_MESSAGE_APP carrying only the broadcast_message on the normal radio config. + * This ensures nodes that only decode TEXT_MESSAGE_APP can still receive the human-readable text. + */ + bool broadcast_legacy_split = 12; + } /*