diff --git a/search/search_index.json b/search/search_index.json index 6f7f6e6a..6e00911e 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -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":"

Welcome to the MeshCore documentation.

Below are a few quick start guides.

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.

"},{"location":"cli_commands/","title":"CLI Commands","text":"

This document provides an overview of CLI commands that can be sent to MeshCore Repeaters, Room Servers and Sensors.

"},{"location":"cli_commands/#navigation","title":"Navigation","text":""},{"location":"cli_commands/#operational","title":"Operational","text":""},{"location":"cli_commands/#reboot-the-node","title":"Reboot the node","text":"

Usage: - reboot

Note: No reply is sent.

"},{"location":"cli_commands/#power-off-the-node","title":"Power-off the node","text":"

Usage: - poweroff, or - shutdown

Note: No reply is sent.

"},{"location":"cli_commands/#reset-the-clock-and-reboot","title":"Reset the clock and reboot","text":"

Usage: - clkreboot

Note: No reply is sent.

"},{"location":"cli_commands/#sync-the-clock-with-the-remote-device","title":"Sync the clock with the remote device","text":"

Usage: - clock sync

"},{"location":"cli_commands/#display-current-time-in-utc","title":"Display current time in UTC","text":"

Usage: - clock

"},{"location":"cli_commands/#set-the-time-to-a-specific-timestamp","title":"Set the time to a specific timestamp","text":"

Usage: - time <epoch_seconds>

Parameters: - epoch_seconds: Unix epoch time

"},{"location":"cli_commands/#send-a-flood-advert","title":"Send a flood advert","text":"

Usage: - advert

"},{"location":"cli_commands/#send-a-zero-hop-advert","title":"Send a zero-hop advert","text":"

Usage: - advert.zerohop

"},{"location":"cli_commands/#start-an-over-the-air-ota-firmware-update","title":"Start an Over-The-Air (OTA) firmware update","text":"

Usage: - start ota

"},{"location":"cli_commands/#erasefactory-reset","title":"Erase/Factory Reset","text":"

Usage: - erase

Serial Only: Yes

Warning: This is destructive!

"},{"location":"cli_commands/#neighbors-repeater-only","title":"Neighbors (Repeater Only)","text":""},{"location":"cli_commands/#list-nearby-neighbors","title":"List nearby neighbors","text":"

Usage: - neighbors

Note: The output of this command is limited to the 8 most recent adverts.

Note: Each line is encoded as {pubkey-prefix}:{timestamp}:{snr*4}

"},{"location":"cli_commands/#remove-a-neighbor","title":"Remove a neighbor","text":"

Usage: - neighbor.remove <pubkey_prefix>

Parameters: - pubkey_prefix: The public key of the node to remove from the neighbors list. This can be a short prefix or the full key. All neighbors matching the provided prefix will be removed.

Note: You can remove all neighbors by sending a space character as the prefix. The space indicates an empty prefix, which matches all existing neighbors.

"},{"location":"cli_commands/#discover-zero-hop-neighbors","title":"Discover zero hop neighbors","text":"

Usage: - discover.neighbors

"},{"location":"cli_commands/#statistics","title":"Statistics","text":""},{"location":"cli_commands/#clear-stats","title":"Clear Stats","text":"

Usage: clear stats

"},{"location":"cli_commands/#system-stats-battery-uptime-queue-length-and-debug-flags","title":"System Stats - Battery, Uptime, Queue Length and Debug Flags","text":"

Usage: - stats-core

Serial Only: Yes

"},{"location":"cli_commands/#radio-stats-noise-floor-last-rssisnr-airtime-receive-errors","title":"Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors","text":"

Usage: stats-radio

Serial Only: Yes

"},{"location":"cli_commands/#packet-stats-packet-counters-received-sent","title":"Packet stats - Packet counters: Received, Sent","text":"

Usage: stats-packets

Serial Only: Yes

"},{"location":"cli_commands/#logging","title":"Logging","text":""},{"location":"cli_commands/#begin-capture-of-rx-log-to-node-storage","title":"Begin capture of rx log to node storage","text":"

Usage: log start

"},{"location":"cli_commands/#end-capture-of-rx-log-to-node-storage","title":"End capture of rx log to node storage","text":"

Usage: log stop

"},{"location":"cli_commands/#erase-captured-log","title":"Erase captured log","text":"

Usage: log erase

"},{"location":"cli_commands/#print-the-captured-log-to-the-serial-terminal","title":"Print the captured log to the serial terminal","text":"

Usage: log

Serial Only: Yes

"},{"location":"cli_commands/#info","title":"Info","text":""},{"location":"cli_commands/#get-the-version","title":"Get the Version","text":"

Usage: ver

"},{"location":"cli_commands/#show-the-hardware-name","title":"Show the hardware name","text":"

Usage: board

"},{"location":"cli_commands/#configuration","title":"Configuration","text":""},{"location":"cli_commands/#radio","title":"Radio","text":""},{"location":"cli_commands/#view-or-change-this-nodes-radio-parameters","title":"View or change this node's radio parameters","text":"

Usage: - get radio - set radio <freq>,<bw>,<sf>,<cr>

Parameters: - freq: Frequency in MHz - bw: Bandwidth in kHz - sf: Spreading factor (5-12) - cr: Coding rate (5-8)

Set by build flag: LORA_FREQ, LORA_BW, LORA_SF, LORA_CR

Default: 869.525,250,11,5

Note: Requires reboot to apply

"},{"location":"cli_commands/#view-or-change-this-nodes-transmit-power","title":"View or change this node's transmit power","text":"

Usage: - get tx - set tx <dbm>

Parameters: - dbm: Power level in dBm (1-22)

Set by build flag: LORA_TX_POWER

Default: Varies by board

Notes: This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Refer to the node's manual for the correct setting to use. Setting a value too high may violate the laws in your country.

"},{"location":"cli_commands/#change-the-radio-parameters-for-a-set-duration","title":"Change the radio parameters for a set duration","text":"

Usage: - tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>

Parameters: - freq: Frequency in MHz (300-2500) - bw: Bandwidth in kHz (7.8-500) - sf: Spreading factor (5-12) - cr: Coding rate (5-8) - timeout_mins: Duration in minutes (must be > 0)

Note: This is not saved to preferences and will clear on reboot

"},{"location":"cli_commands/#view-or-change-this-nodes-frequency","title":"View or change this node's frequency","text":"

Usage: - get freq - set freq <frequency>

Parameters: - frequency: Frequency in MHz

Default: 869.525

Note: Requires reboot to apply Serial Only: set freq <frequency>

"},{"location":"cli_commands/#view-or-change-this-nodes-rx-boosted-gain-mode-sx12xx-and-lr1110-v1141","title":"View or change this node's rx boosted gain mode (SX12xx and LR1110, v1.14.1+)","text":"

Usage: - get radio.rxgain - set radio.rxgain <state>

Parameters: - state: on|off

Default: on

Temporary Note: If you upgraded from an older version to 1.14.1 without erasing flash, this setting is off because of #2118

"},{"location":"cli_commands/#system","title":"System","text":""},{"location":"cli_commands/#view-or-change-this-nodes-name","title":"View or change this node's name","text":"

Usage: - get name - set name <name>

Parameters: - name: Node name

Set by build flag: ADVERT_NAME

Default: Varies by board

Note: Max length varies. If a location is set, the max length is 24 bytes; 32 otherwise. Emoji and unicode characters may take more than one byte.

"},{"location":"cli_commands/#view-or-change-this-nodes-latitude","title":"View or change this node's latitude","text":"

Usage: - get lat - set lat <degrees>

Set by build flag: ADVERT_LAT

Default: 0

Parameters: - degrees: Latitude in degrees

"},{"location":"cli_commands/#view-or-change-this-nodes-longitude","title":"View or change this node's longitude","text":"

Usage: - get lon - set lon <degrees>

Set by build flag: ADVERT_LON

Default: 0

Parameters: - degrees: Longitude in degrees

"},{"location":"cli_commands/#view-or-change-this-nodes-identity-private-key","title":"View or change this node's identity (Private Key)","text":"

Usage: - get prv.key - set prv.key <private_key>

Parameters: - private_key: Private key in hex format (64 hex characters)

Serial Only: - get prv.key: Yes - set prv.key: No

Note: Requires reboot to take effect after setting

"},{"location":"cli_commands/#change-this-nodes-admin-password","title":"Change this node's admin password","text":"

Usage: - password <new_password>

Parameters: - new_password: New admin password

Set by build flag: ADMIN_PASSWORD

Default: password

Note: Command reply echoes the updated password for confirmation.

Note: Any node using this password will be added to the admin ACL list.

"},{"location":"cli_commands/#view-or-change-this-nodes-guest-password","title":"View or change this node's guest password","text":"

Usage: - get guest.password - set guest.password <password>

Parameters: - password: Guest password

Set by build flag: ROOM_PASSWORD (Room Server only)

Default: <blank>

"},{"location":"cli_commands/#view-or-change-this-nodes-owner-info","title":"View or change this node's owner info","text":"

Usage: - get owner.info - set owner.info <text>

Parameters: - text: Owner information text

Default: <blank>

Note: | characters are translated to newlines

Note: Requires firmware 1.12+

"},{"location":"cli_commands/#fine-tune-the-battery-reading","title":"Fine-tune the battery reading","text":"

Usage: - get adc.multiplier - set adc.multiplier <value>

Parameters: - value: ADC multiplier (0.0-10.0)

Default: 0.0 (value defined by board)

Note: Returns \"Error: unsupported by this board\" if hardware doesn't support it

"},{"location":"cli_commands/#view-this-nodes-public-key","title":"View this node's public key","text":"

Usage: get public.key

"},{"location":"cli_commands/#view-this-nodes-firmware-version","title":"View this node's firmware version","text":"

Usage: ver

"},{"location":"cli_commands/#view-this-nodes-configured-role","title":"View this node's configured role","text":"

Usage: get role

"},{"location":"cli_commands/#view-or-change-this-nodes-power-saving-flag-repeater-only","title":"View or change this node's power saving flag (Repeater Only)","text":"

Usage: - powersaving - powersaving on - powersaving off

Parameters: - on: enable power saving - off: disable power saving

Default: off

Note: When enabled, device enters sleep mode between radio transmissions

"},{"location":"cli_commands/#routing","title":"Routing","text":""},{"location":"cli_commands/#view-or-change-this-nodes-repeat-flag","title":"View or change this node's repeat flag","text":"

Usage: - get repeat - set repeat <state>

Parameters: - state: on|off

Default: on

"},{"location":"cli_commands/#view-or-change-this-nodes-advert-path-hash-size","title":"View or change this node's advert path hash size","text":"

Usage: - get path.hash.mode - set path.hash.mode <value>

Parameters: - value: Path hash size (0-2) - 0: 1 Byte hash size (256 unique ids)[64 max flood] - 1: 2 Byte hash size (65,536 unique ids)[32 max flood] - 2: 3 Byte hash size (16,777,216 unique ids)[21 max flood] - 3: DO NOT USE (Reserved)

Default: 0

Note: the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14

Temporary Note: adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propagation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are supported. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes.

"},{"location":"cli_commands/#view-or-change-this-nodes-loop-detection","title":"View or change this node's loop detection","text":"

Usage: - get loop.detect - set loop.detect <state>

Parameters: - state: - off: no loop detection is performed - minimal: packets are dropped if repeater's ID/hash appears 4 or more times (1-byte), 2 or more (2-byte), 1 or more (3-byte) - moderate: packets are dropped if repeater's ID/hash appears 2 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte) - strict: packets are dropped if repeater's ID/hash appears 1 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte)

Default: off

Note: When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (probably some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14

Example: If preference is loop.detect minimal, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet. If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects.

"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-flood-traffic","title":"View or change the retransmit delay factor for flood traffic","text":"

Usage: - get txdelay - set txdelay <value>

Parameters: - value: Transmit delay factor (0-2)

Default: 0.5

Note: When multiple nearby repeaters all hear the same flood packet, each waits a random amount of time before retransmitting to avoid simultaneous collisions. This factor scales the size of that random window. Higher values reduce collision risk at the cost of added latency. 0 disables the window entirely.

"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-direct-traffic","title":"View or change the retransmit delay factor for direct traffic","text":"

Usage: - get direct.txdelay - set direct.txdelay <value>

Parameters: - value: Direct transmit delay factor (0-2)

Default: 0.2

Note: Same collision-avoidance random window as txdelay, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them.

"},{"location":"cli_commands/#experimental-view-or-change-the-processing-delay-for-received-traffic","title":"[Experimental] View or change the processing delay for received traffic","text":"

Usage: - get rxdelay - set rxdelay <value>

Parameters: - value: Receive delay base (0-20)

Default: 0.0

Note: When enabled, repeaters that received a flood packet with a weak signal are held in a delay queue before processing, while those that received it with a strong signal process it immediately. This gives strong-signal paths forwarding priority. By the time weak-signal nodes process their copy, the packet may have already propagated and will be suppressed as a duplicate, reducing redundant retransmissions.

"},{"location":"cli_commands/#view-or-change-the-duty-cycle-limit","title":"View or change the duty cycle limit","text":"

Usage: - get dutycycle - set dutycycle <value>

Parameters: - value: Duty cycle percentage (1-100)

Default: 50% (equivalent to airtime factor 1.0)

Examples: - set dutycycle 100 \u2014 no duty cycle limit - set dutycycle 50 \u2014 50% duty cycle (default) - set dutycycle 10 \u2014 10% duty cycle - set dutycycle 1 \u2014 1% duty cycle (strictest EU requirement)

Note: Added in firmware v1.15.0

"},{"location":"cli_commands/#view-or-change-the-airtime-factor-duty-cycle-limit","title":"View or change the airtime factor (duty cycle limit)","text":"

Deprecated as of firmware v1.15.0. Use get/set dutycycle instead.

Usage: - get af - set af <value>

Parameters: - value: Airtime factor (0-9). After each transmission, the repeater enforces a silent period of approximately the on-air transmission time multiplied by the value. This results in a long-term duty cycle of roughly 1 divided by (1 plus the value). For example: - af = 1 \u2192 ~50% duty - af = 2 \u2192 ~33% duty - af = 3 \u2192 ~25% duty - af = 9 \u2192 ~10% duty You are responsible for choosing a value that is appropriate for your jurisdiction and channel plan (for example EU 868 Mhz 10% duty cycle regulation).

Default: 1.0

"},{"location":"cli_commands/#view-or-change-the-local-interference-threshold","title":"View or change the local interference threshold","text":"

Usage: - get int.thresh - set int.thresh <value>

Parameters: - value: Interference threshold value

Default: 0.0

"},{"location":"cli_commands/#view-or-change-the-agc-reset-interval","title":"View or change the AGC Reset Interval","text":"

Usage: - get agc.reset.interval - set agc.reset.interval <value>

Parameters: - value: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable.

Default: 0.0

"},{"location":"cli_commands/#enable-or-disable-multi-acks-support","title":"Enable or disable Multi-Acks support","text":"

Usage: - get multi.acks - set multi.acks <state>

Parameters: - state: 0 (disable) or 1 (enable)

Default: 0

"},{"location":"cli_commands/#view-or-change-the-flood-advert-interval","title":"View or change the flood advert interval","text":"

Usage: - get flood.advert.interval - set flood.advert.interval <hours>

Parameters: - hours: Interval in hours (3-168)

Default: 12 (Repeater) - 0 (Sensor)

"},{"location":"cli_commands/#view-or-change-the-zero-hop-advert-interval","title":"View or change the zero-hop advert interval","text":"

Usage: - get advert.interval - set advert.interval <minutes>

Parameters: - minutes: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)

Default: 0

"},{"location":"cli_commands/#limit-the-number-of-hops-for-a-flood-message","title":"Limit the number of hops for a flood message","text":"

Usage: - get flood.max - set flood.max <value>

Parameters: - value: Maximum flood hop count (0-64)

Default: 64

"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-unscoped-flood-message","title":"Limit the number of hops for an unscoped flood message","text":"

Usage: - get flood.max.unscoped - set flood.max.unscoped <value>

Parameters: - value: Maximum flood hop count (0-64) for a packet without a scope (no region set)

Default: 64 - (0xFF indicates it hasn't been set, will track flood.max until it is.)

Note: An alternative to region denyf *, setting flood.max.unscoped to a lower value such as 3 would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region.

"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-advert-flood-message","title":"Limit the number of hops for an advert flood message","text":"

Usage: - get flood.max.advert - set flood.max.advert <value>

Parameters: - value: Maximum flood hop count (0-64) for an advert packet

Default: 8

"},{"location":"cli_commands/#acl","title":"ACL","text":""},{"location":"cli_commands/#add-update-or-remove-permissions-for-a-companion","title":"Add, update or remove permissions for a companion","text":"

Usage: - setperm <pubkey> <permissions>

Parameters: - pubkey: Companion public key - permissions: - 0: Guest - 1: Read-only - 2: Read-write - 3: Admin

Note: Removes the entry when permissions is omitted

"},{"location":"cli_commands/#view-the-current-acl","title":"View the current ACL","text":"

Usage: - get acl

Serial Only: Yes

"},{"location":"cli_commands/#view-or-change-this-room-servers-read-only-flag","title":"View or change this room server's 'read-only' flag","text":"

Usage: - get allow.read.only - set allow.read.only <state>

Parameters: - state: on (enable) or off (disable)

Default: off

"},{"location":"cli_commands/#region-management-v110","title":"Region Management (v1.10.+)","text":""},{"location":"cli_commands/#bulk-load-region-lists","title":"Bulk-load region lists","text":"

Usage: - region load - region load <name> [flood_flag]

Parameters: - name: A name of a region. * represents the wildcard region

Note: flood_flag: Optional F to allow flooding

Note: Indentation creates parent-child relationships (max 8 levels)

Note: region load with an empty name will not work remotely (it's interactive)

"},{"location":"cli_commands/#save-any-changes-to-regions-made-since-reboot","title":"Save any changes to regions made since reboot","text":"

Usage: - region save

"},{"location":"cli_commands/#allow-a-region","title":"Allow a region","text":"

Usage: - region allowf <name>

Parameters: - name: Region name (or * for wildcard)

Note: Setting on wildcard * allows packets without region transport codes

"},{"location":"cli_commands/#block-a-region","title":"Block a region","text":"

Usage: - region denyf <name>

Parameters: - name: Region name (or * for wildcard)

Note: Setting on wildcard * drops packets without region transport codes

"},{"location":"cli_commands/#show-information-for-a-region","title":"Show information for a region","text":"

Usage: - region get <name>

Parameters: - name: Region name (or * for wildcard)

"},{"location":"cli_commands/#view-or-change-the-home-region-for-this-node","title":"View or change the home region for this node","text":"

Usage: - region home - region home <name>

Parameters: - name: Region name

"},{"location":"cli_commands/#view-or-change-the-default-scope-region-for-this-node","title":"View or change the default scope region for this node","text":"

Usage: - region default - region default {name|<null>}

Parameters: - name: Region name, or to reset/clear"},{"location":"cli_commands/#create-a-new-region","title":"Create a new region","text":"

Usage: - region put <name> [parent_name]

Parameters: - name: Region name - parent_name: Parent region name (optional, defaults to wildcard)

"},{"location":"cli_commands/#define-region-hierarchy-single-line","title":"Define region hierarchy (single line)","text":"

Usage: - region def <token> [<token> ...]

Parameters (tokens): Space-separated. A logical cursor starts at the wildcard *.

Behavior: Each created region defaults to flood-allowed (same as region put). The reply is the resulting region tree (same format as bare region); review it before running region save to persist. On error, the reply is Err - ... and any regions placed before the failure remain on the node, just like a partial chain of region put.

Existing regions: region def does not clear the existing tree \u2014 if a name already exists, its parent is updated to the current cursor; otherwise a new region is created. To start from scratch, region remove the unwanted regions first.

Limits: Repeater serial accepts one line up to 160 characters. For larger trees, split across multiple region def commands; the cursor resets to * between commands, so lead the next command with child|ancestor to reposition. Each token splits at most once on | \u2014 region def a|b|c|d is not a flat-list shorthand; see the flat-list example below.

Example \u2014 linear chain (each token becomes a child of the previous):

region def a b c d e\nregion save\n

Example \u2014 branched tree (equivalent to region put a, region put b a, region put c b, region put d c, region put e b, region put f e):

region def a b c d|b e f\nregion save\n

Example \u2014 error and partial state:

region def a b c|nope d\n

The reply is Err - unknown jump: nope. a, b, and c were placed before the failure; d was not. Run region to inspect, then re-run with a corrected jump or repair with region remove / region put.

Example \u2014 flat list (each region a child of *). Use |* after each token to pop the cursor back to the root before the next token:

region def a|* b|* c|* d|* e|* f\nregion save\n
"},{"location":"cli_commands/#remove-a-region","title":"Remove a region","text":"

Usage: - region remove <name>

Parameters: - name: Region name

Note: Must remove all child regions before the region can be removed

"},{"location":"cli_commands/#view-all-regions","title":"View all regions","text":"

Usage: - region list <filter>

Serial Only: Yes

Parameters: - filter: allowed|denied

Note: Requires firmware 1.12+

"},{"location":"cli_commands/#dump-all-defined-regions-and-flood-permissions","title":"Dump all defined regions and flood permissions","text":"

Usage: - region

Serial Only: For firmware older than 1.12.0

"},{"location":"cli_commands/#region-examples","title":"Region Examples","text":"

Example 1: Using F Flag with Named Public Region

region load\n#Europe F\n<blank line to end region load>\nregion save\n

Explanation: - Creates a region named #Europe with flooding enabled - Packets from this region will be flooded to other nodes

Example 2: Using Wildcard with F Flag

region load \n* F\n<blank line to end region load>\nregion save\n

Explanation: - Creates a wildcard region * with flooding enabled - Enables flooding for all regions automatically - Applies only to packets without transport codes

Example 3: Using Wildcard Without F Flag

region load \n*\n<blank line to end region load>\nregion save\n

Explanation: - Creates a wildcard region * without flooding - This region exists but doesn't affect packet distribution - Used as a default/empty region

Example 4: Nested Public Region with F Flag

region load \n#Europe F\n  #UK\n    #London\n    #Manchester\n  #France\n    #Paris\n    #Lyon\n<blank line to end region load>\nregion save\n

Explanation: - Creates #Europe region with flooding enabled - Adds nested child regions (#UK, #France) - All nested regions inherit the flooding flag from parent

Example 5: Wildcard with Nested Public Regions

region load \n* F\n  #NorthAmerica\n    #USA\n      #NewYork\n      #California\n    #Canada\n      #Ontario\n      #Quebec\n<blank line to end region load>\nregion save\n

Explanation: - Creates wildcard region * with flooding enabled - Adds nested #NorthAmerica hierarchy - Enables flooding for all child regions automatically - Useful for global networks with specific regional rules

"},{"location":"cli_commands/#gps-when-gps-support-is-compiled-in","title":"GPS (When GPS support is compiled in)","text":""},{"location":"cli_commands/#view-or-change-gps-state","title":"View or change GPS state","text":"

Usage: - gps - gps <state>

Parameters: - state: on|off

Default: off

Note: Output format: - off when the GPS hardware is disabled - on, {active|deactivated}, {fix|no fix}, {sat count} sats when the GPS hardware is enabled

"},{"location":"cli_commands/#sync-this-nodes-clock-with-gps-time","title":"Sync this node's clock with GPS time","text":"

Usage: - gps sync

"},{"location":"cli_commands/#set-this-nodes-location-based-on-the-gps-coordinates","title":"Set this node's location based on the GPS coordinates","text":"

Usage: - gps setloc

"},{"location":"cli_commands/#view-or-change-the-gps-advert-policy","title":"View or change the GPS advert policy","text":"

Usage: - gps advert - gps advert <policy>

Parameters: - policy: none|share|prefs - none: don't include location in adverts - share: share gps location (from SensorManager) - prefs: location stored in node's lat and lon settings

Default: prefs

"},{"location":"cli_commands/#sensors-when-sensor-support-is-compiled-in","title":"Sensors (When sensor support is compiled in)","text":""},{"location":"cli_commands/#view-the-list-of-sensors-on-this-node","title":"View the list of sensors on this node","text":"

Usage: sensor list [start]

Parameters: - start: Optional starting index (defaults to 0)

Note: Output format: <var_name>=<value>\\n

"},{"location":"cli_commands/#view-or-change-the-value-of-a-sensor","title":"View or change the value of a sensor","text":"

Usage: - sensor get <key> - sensor set <key> <value>

Parameters: - key: Sensor setting name - value: The value to set the sensor to

"},{"location":"cli_commands/#bridge-when-bridge-support-is-compiled-in","title":"Bridge (When bridge support is compiled in)","text":""},{"location":"cli_commands/#view-the-compiled-bridge-type","title":"View the compiled bridge type","text":"

Usage: get bridge.type

"},{"location":"cli_commands/#view-or-change-the-bridge-enabled-flag","title":"View or change the bridge enabled flag","text":"

Usage: - get bridge.enabled - set bridge.enabled <state>

Parameters: - state: on|off

Default: off

"},{"location":"cli_commands/#add-a-delay-to-packets-routed-through-this-bridge","title":"Add a delay to packets routed through this bridge","text":"

Usage: - get bridge.delay - set bridge.delay <ms>

Parameters: - ms: Delay in milliseconds (0-10000)

Default: 500

"},{"location":"cli_commands/#view-or-change-the-source-of-packets-bridged-to-the-external-interface","title":"View or change the source of packets bridged to the external interface","text":"

Usage: - get bridge.source - set bridge.source <source>

Parameters: - source: - logRx: bridges received packets - logTx: bridges transmitted packets

Default: logTx

"},{"location":"cli_commands/#view-or-change-the-speed-of-the-bridge-rs-232-only","title":"View or change the speed of the bridge (RS-232 only)","text":"

Usage: - get bridge.baud - set bridge.baud <rate>

Parameters: - rate: Baud rate (9600, 19200, 38400, 57600, or 115200)

Default: 115200

"},{"location":"cli_commands/#view-or-change-the-channel-used-for-bridging-espnow-only","title":"View or change the channel used for bridging (ESPNow only)","text":"

Usage: - get bridge.channel - set bridge.channel <channel>

Parameters: - channel: Channel number (1-14)

"},{"location":"cli_commands/#set-the-esp-now-secret","title":"Set the ESP-Now secret","text":"

Usage: - get bridge.secret - set bridge.secret <secret>

Parameters: - secret: ESP-NOW bridge secret, up to 15 characters

Default: Varies by board

"},{"location":"cli_commands/#view-the-bootloader-version-nrf52-only","title":"View the bootloader version (nRF52 only)","text":"

Usage: get bootloader.ver

"},{"location":"cli_commands/#view-power-management-support","title":"View power management support","text":"

Usage: get pwrmgt.support

"},{"location":"cli_commands/#view-the-current-power-source","title":"View the current power source","text":"

Usage: get pwrmgt.source

Note: Returns an error on boards without power management support.

"},{"location":"cli_commands/#view-the-boot-reset-and-shutdown-reasons","title":"View the boot reset and shutdown reasons","text":"

Usage: get pwrmgt.bootreason

Note: Returns an error on boards without power management support.

"},{"location":"cli_commands/#view-the-boot-voltage","title":"View the boot voltage","text":"

Usage: get pwrmgt.bootmv

Note: Returns an error on boards without power management support.

"},{"location":"companion_protocol/","title":"Companion Protocol","text":"

NOTE: This document is still in development. Some information may be inaccurate.

This document provides a comprehensive guide for communicating with MeshCore devices over Bluetooth Low Energy (BLE).

It is platform-agnostic and can be used for Android, iOS, Python, JavaScript, or any other platform that supports BLE.

"},{"location":"companion_protocol/#official-libraries","title":"Official Libraries","text":"

Please see the following repos for existing MeshCore Companion Protocol libraries.

"},{"location":"companion_protocol/#important-security-note","title":"Important Security Note","text":"

All secrets, hashes, and cryptographic values shown in this guide are example values only.

"},{"location":"companion_protocol/#table-of-contents","title":"Table of Contents","text":"
  1. BLE Connection
  2. Packet Structure
  3. Commands
  4. Channel Management
  5. Message Handling
  6. Response Parsing
  7. Example Implementation Flow
  8. Best Practices
  9. Troubleshooting
"},{"location":"companion_protocol/#ble-connection","title":"BLE Connection","text":""},{"location":"companion_protocol/#service-and-characteristics","title":"Service and Characteristics","text":"

MeshCore Companion devices expose a BLE service with the following UUIDs:

"},{"location":"companion_protocol/#connection-steps","title":"Connection Steps","text":"
  1. Scan for Devices

  2. Connect to GATT

  3. Discover Services and Characteristics

  4. Enable Notifications

  5. Send Initial Commands

Note: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.

"},{"location":"companion_protocol/#ble-write-type","title":"BLE Write Type","text":"

When writing commands to the RX characteristic, specify the write type:

Platform-specific:

Recommendation: Use write with response for reliability.

"},{"location":"companion_protocol/#mtu-maximum-transmission-unit","title":"MTU (Maximum Transmission Unit)","text":"

The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like SET_CHANNEL (50 bytes), you may need to:

  1. Request Larger MTU: Request MTU of 512 bytes if supported
"},{"location":"companion_protocol/#command-sequencing","title":"Command Sequencing","text":"

Critical: Commands must be sent in the correct sequence:

  1. After Connection:

  2. Command-Response Matching:

"},{"location":"companion_protocol/#command-queue-management","title":"Command Queue Management","text":"

For reliable operation, implement a command queue.

Queue Structure:

Error Handling:

"},{"location":"companion_protocol/#packet-structure","title":"Packet Structure","text":"

The MeshCore protocol uses a binary format with the following structure:

Most packets follow this format:

[Packet Type (1 byte)] [Data (variable length)]\n

The first byte indicates the packet type (see Response Parsing).

"},{"location":"companion_protocol/#commands","title":"Commands","text":""},{"location":"companion_protocol/#1-app-start","title":"1. App Start","text":"

Purpose: Initialize communication with the device. Must be sent first after connection.

Command Format:

Byte 0: 0x01\nBytes 1-7: Reserved (currently ignored by firmware)\nBytes 8+: Application name (UTF-8, optional)\n

Example (hex):

01 00 00 00 00 00 00 00 6d 63 63 6c 69\n

Response: PACKET_SELF_INFO (0x05)

"},{"location":"companion_protocol/#2-device-query","title":"2. Device Query","text":"

Purpose: Query device information.

Command Format:

Byte 0: 0x16\nByte 1: 0x03\n

Example (hex):

16 03\n

Response: PACKET_DEVICE_INFO (0x0D) with device information

"},{"location":"companion_protocol/#3-get-channel-info","title":"3. Get Channel Info","text":"

Purpose: Retrieve information about a specific channel.

Command Format:

Byte 0: 0x1F\nByte 1: Channel Index (0-7)\n

Example (get channel 1):

1F 01\n

Response: PACKET_CHANNEL_INFO (0x12) with channel details

"},{"location":"companion_protocol/#4-set-channel","title":"4. Set Channel","text":"

Purpose: Create or update a channel on the device.

Command Format:

Byte 0: 0x20\nByte 1: Channel Index (0-7)\nBytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)\nBytes 34-49: Secret (16 bytes)\n

Total Length: 50 bytes

Channel Index: - Index 0: Reserved for public channels (no secret) - Indices 1-7: Available for private channels

Channel Name: - UTF-8 encoded - Maximum 32 bytes - Padded with null bytes (0x00) if shorter

Secret Field (16 bytes): - For private channels: 16-byte secret - For public channels: All zeros (0x00)

Example (create channel \"YourChannelName\" at index 1 with secret):

20 01 53 4D 53 00 00 ... (name padded to 32 bytes)\n    [16 bytes of secret]\n

Note: The 32-byte secret variant is unsupported and returns PACKET_ERROR.

Response: PACKET_OK (0x00) on success, PACKET_ERROR (0x01) on failure

"},{"location":"companion_protocol/#5-send-channel-message","title":"5. Send Channel Message","text":"

Purpose: Send a text message to a channel.

Command Format:

Byte 0: 0x03\nByte 1: 0x00\nByte 2: Channel Index (0-7)\nBytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds)\nBytes 7+: Message Text (UTF-8, variable length)\n

Timestamp: Unix timestamp in seconds (32-bit unsigned integer, little-endian)

Example (send \"Hello\" to channel 1 at timestamp 1234567890):

03 00 01 D2 02 96 49 48 65 6C 6C 6F\n

Response: PACKET_MSG_SENT (0x06) on success

"},{"location":"companion_protocol/#6-send-channel-data-datagram","title":"6. Send Channel Data Datagram","text":"

Purpose: Send a binary datagram to a channel. Unlike channel text messages, datagrams carry no built-in sender identity and no timestamp \u2014 applications needing either must encode them inside the binary payload.

Command Format:

Byte 0:                         0x3E\nByte 1:                         Channel Index (0-7)\nByte 2:                         Path Length (0xFF = flood, otherwise actual path length)\nBytes 3 .. 2+path_len:          Path (omitted when path_len == 0xFF)\nNext 2 bytes (little-endian):   Data Type (`data_type`, uint16)\nRemaining bytes:                Binary payload (variable length)\n

Example (flood, DATA_TYPE_DEV, payload A1 B2 C3, channel 1):

3E 01 FF FF FF A1 B2 C3\n

Data Type / Transport Mapping: - 0x0000 (DATA_TYPE_RESERVED) is invalid and rejected with PACKET_ERROR. - 0xFFFF (DATA_TYPE_DEV) is the developer namespace for experimenting and developing apps. - Values 0x0001\u20130xFFFE are available for registered application/community namespaces. See the Registered data_type values table below.

Limits: - Maximum payload length is MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163 bytes. - Larger payloads are rejected with PACKET_ERROR (ERR_CODE_ILLEGAL_ARG).

Response: PACKET_OK (0x00) on success, or PACKET_ERROR (0x01) with one of: - ERR_CODE_NOT_FOUND (2) \u2014 unknown channel_idx - ERR_CODE_ILLEGAL_ARG (6) \u2014 invalid path_len, reserved data_type (0x0000), or payload larger than MAX_CHANNEL_DATA_LENGTH - ERR_CODE_TABLE_FULL (3) \u2014 outbound send queue is full; retry later

Inbound datagrams are delivered to the host via RESP_CODE_CHANNEL_DATA_RECV (0x1B); see Receive Channel Data Datagram.

"},{"location":"companion_protocol/#registered-data_type-values","title":"Registered data_type values","text":"

data_type is an application identifier, not a payload-format identifier. Each registered value identifies an application that owns its own internal payload schemas. The firmware does not inspect payload contents \u2014 data_type is transported opaquely.

Value Constant Purpose 0x0000 DATA_TYPE_RESERVED Reserved; invalid on send 0x0001 \u2013 0x00FF \u2014 Reserved for internal use 0x0100 \u2013 0xFEFF \u2014 Registered application namespaces (see number_allocations.md) 0xFF00 \u2013 0xFFFE \u2014 Testing/development; no registration required 0xFFFF DATA_TYPE_DEV Developer/experimental namespace

To register a new application, submit a PR adding a row to the table in docs/number_allocations.md. Internal sub-formats within an allocated application ID are owned by that application and are not tracked in MeshCore firmware or this document.

"},{"location":"companion_protocol/#receive-channel-data-datagram","title":"Receive Channel Data Datagram","text":"

Inbound group datagrams (radio-level PAYLOAD_TYPE_GRP_DATA, 0x06) are forwarded to the host as RESP_CODE_CHANNEL_DATA_RECV notifications.

Frame Format (RESP_CODE_CHANNEL_DATA_RECV, 0x1B):

Byte 0:                 0x1B (packet type)\nByte 1:                 SNR (signed int8, scaled \u00d74 \u2014 divide by 4.0 to recover dB)\nBytes 2-3:              Reserved (clients MUST ignore)\nByte 4:                 Channel Index (0-7)\nByte 5:                 Path Length (actual path length when flooded, otherwise 0xFF for direct)\nBytes 6-7:              Data Type (uint16 little-endian)\nByte 8:                 Data Length\nBytes 9 .. 8+data_len:  Payload\n

Path bytes are not forwarded: Only path_len is reported in the receive frame \u2014 the path itself is not copied to the host. There are no path bytes between byte 5 and the data_type field at bytes 6\u20137, regardless of path_len.

Path Length semantics differ between send and receive:

Direction path_len = 0xFF path_len \u2260 0xFF Send Flood the network Direct route; the encoded path follows (low 6 bits = hash count, top 2 bits + 1 = hash size; on-wire byte count = hash_count \u00d7 hash_size) Receive Packet arrived via direct route Packet was flooded; this is the encoded pkt->path_len field as observed (no path bytes follow)

In other words, the meaning of 0xFF is inverted between the two directions, and on receive the field carries metadata only \u2014 never a routable path. path_len is an encoded byte (see Packet::isValidPathLen / Packet::writePath in src/Packet.cpp), not a raw byte count.

Note: The device may also emit PACKET_MESSAGES_WAITING (0x83) to notify the host that datagrams are queued; poll with CMD_SYNC_NEXT_MESSAGE (0x0A) to retrieve them.

Parsing Pseudocode:

def parse_channel_data_recv(data):\n    if len(data) < 9:\n        return None\n    snr_byte = data[1]\n    snr = (snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0\n    channel_idx = data[4]\n    path_len = data[5]\n    data_type = int.from_bytes(data[6:8], 'little')\n    data_len = data[8]\n    if 9 + data_len > len(data):\n        return None\n    payload = data[9:9 + data_len]\n    return {\n        'snr': snr,\n        'channel_idx': channel_idx,\n        'path_len': path_len,\n        'data_type': data_type,\n        'payload': bytes(payload),\n    }\n
"},{"location":"companion_protocol/#7-get-message","title":"7. Get Message","text":"

Purpose: Request the next queued message from the device.

Command Format:

Byte 0: 0x0A\n

Example (hex):

0A\n

Response: - PACKET_CHANNEL_MSG_RECV (0x08) or PACKET_CHANNEL_MSG_RECV_V3 (0x11) for channel messages - PACKET_CONTACT_MSG_RECV (0x07) or PACKET_CONTACT_MSG_RECV_V3 (0x10) for contact messages - PACKET_CHANNEL_DATA_RECV (0x1B) for channel data datagrams - PACKET_NO_MORE_MSGS (0x0A) if no messages available

Note: Poll this command periodically to retrieve queued messages. The device may also send PACKET_MESSAGES_WAITING (0x83) as a notification when messages are available.

"},{"location":"companion_protocol/#8-get-battery-and-storage","title":"8. Get Battery and Storage","text":"

Purpose: Query device battery voltage and storage usage.

Command Format:

Byte 0: 0x14\n

Example (hex):

14\n

Response: PACKET_BATTERY (0x0C) with battery millivolts and storage information

"},{"location":"companion_protocol/#channel-management","title":"Channel Management","text":""},{"location":"companion_protocol/#channel-types","title":"Channel Types","text":"
  1. Public Channel
  2. Hashtag Channels
  3. Private Channels
"},{"location":"companion_protocol/#channel-lifecycle","title":"Channel Lifecycle","text":"
  1. Set Channel:
  2. Get Channel:
  3. Delete Channel:
"},{"location":"companion_protocol/#message-handling","title":"Message Handling","text":""},{"location":"companion_protocol/#receiving-messages","title":"Receiving Messages","text":"

Messages are received via the TX characteristic (notifications). The device sends:

  1. Channel Messages:
  2. PACKET_CHANNEL_MSG_RECV (0x08) - Standard format
  3. PACKET_CHANNEL_MSG_RECV_V3 (0x11) - Version 3 with SNR

  4. Contact Messages:

  5. PACKET_CONTACT_MSG_RECV (0x07) - Standard format
  6. PACKET_CONTACT_MSG_RECV_V3 (0x10) - Version 3 with SNR

  7. Notifications:

  8. PACKET_MESSAGES_WAITING (0x83) - Indicates messages are queued
"},{"location":"companion_protocol/#contact-message-format","title":"Contact Message Format","text":"

Standard Format (PACKET_CONTACT_MSG_RECV, 0x07):

Byte 0: 0x07 (packet type)\nBytes 1-6: Public Key Prefix (6 bytes, hex)\nByte 7: Path Length\nByte 8: Text Type\nBytes 9-12: Timestamp (32-bit little-endian)\nBytes 13-16: Signature (4 bytes, only if txt_type == 2)\nBytes 17+: Message Text (UTF-8)\n

V3 Format (PACKET_CONTACT_MSG_RECV_V3, 0x10):

Byte 0: 0x10 (packet type)\nByte 1: SNR (signed byte, multiplied by 4)\nBytes 2-3: Reserved\nBytes 4-9: Public Key Prefix (6 bytes, hex)\nByte 10: Path Length\nByte 11: Text Type\nBytes 12-15: Timestamp (32-bit little-endian)\nBytes 16-19: Signature (4 bytes, only if txt_type == 2)\nBytes 20+: Message Text (UTF-8)\n

Parsing Pseudocode:

def parse_contact_message(data):\n    packet_type = data[0]\n    offset = 1\n\n    # Check for V3 format\n    if packet_type == 0x10:  # V3\n        snr_byte = data[offset]\n        snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)\n        offset += 3  # Skip SNR + reserved\n\n    pubkey_prefix = data[offset:offset+6].hex()\n    offset += 6\n\n    path_len = data[offset]\n    txt_type = data[offset + 1]\n    offset += 2\n\n    timestamp = int.from_bytes(data[offset:offset+4], 'little')\n    offset += 4\n\n    # If txt_type == 2, skip 4-byte signature\n    if txt_type == 2:\n        offset += 4\n\n    message = data[offset:].decode('utf-8')\n\n    return {\n        'pubkey_prefix': pubkey_prefix,\n        'path_len': path_len,\n        'txt_type': txt_type,\n        'timestamp': timestamp,\n        'message': message,\n        'snr': snr if packet_type == 0x10 else None\n    }\n
"},{"location":"companion_protocol/#channel-message-format","title":"Channel Message Format","text":"

Standard Format (PACKET_CHANNEL_MSG_RECV, 0x08):

Byte 0: 0x08 (packet type)\nByte 1: Channel Index (0-7)\nByte 2: Path Length\nByte 3: Text Type\nBytes 4-7: Timestamp (32-bit little-endian)\nBytes 8+: Message Text (UTF-8)\n

V3 Format (PACKET_CHANNEL_MSG_RECV_V3, 0x11):

Byte 0: 0x11 (packet type)\nByte 1: SNR (signed byte, multiplied by 4)\nBytes 2-3: Reserved\nByte 4: Channel Index (0-7)\nByte 5: Path Length\nByte 6: Text Type\nBytes 7-10: Timestamp (32-bit little-endian)\nBytes 11+: Message Text (UTF-8)\n

Parsing Pseudocode:

def parse_channel_message(data):\n    packet_type = data[0]\n    offset = 1\n\n    # Check for V3 format\n    if packet_type == 0x11:  # V3\n        snr_byte = data[offset]\n        snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)\n        offset += 3  # Skip SNR + reserved\n\n    channel_idx = data[offset]\n    path_len = data[offset + 1]\n    txt_type = data[offset + 2]\n    timestamp = int.from_bytes(data[offset+3:offset+7], 'little')\n    message = data[offset+7:].decode('utf-8')\n\n    return {\n        'channel_idx': channel_idx,\n        'timestamp': timestamp,\n        'message': message,\n        'snr': snr if packet_type == 0x11 else None\n    }\n
"},{"location":"companion_protocol/#sending-messages","title":"Sending Messages","text":"

Use the SEND_CHANNEL_MESSAGE command (see Commands).

Important: - Messages are limited to 133 characters per MeshCore specification - Long messages should be split into chunks - Include a chunk indicator (e.g., \"[1/3] message text\")

"},{"location":"companion_protocol/#response-parsing","title":"Response Parsing","text":""},{"location":"companion_protocol/#terminology","title":"Terminology","text":"

This document uses a spec-level naming convention (PACKET_*) for bytes the firmware sends back to the host. In the firmware source these same values are split across two #define families by purpose:

Byte values are authoritative; names are aliases. When reading firmware source, RESP_CODE_X / PUSH_CODE_X correspond to this doc's PACKET_X of the same numeric value.

"},{"location":"companion_protocol/#packet-types","title":"Packet Types","text":"Value Name Description 0x00 PACKET_OK Command succeeded 0x01 PACKET_ERROR Command failed 0x02 PACKET_CONTACT_START Start of contact list 0x03 PACKET_CONTACT Contact information 0x04 PACKET_CONTACT_END End of contact list 0x05 PACKET_SELF_INFO Device self-information 0x06 PACKET_MSG_SENT Message sent confirmation 0x07 PACKET_CONTACT_MSG_RECV Contact message (standard) 0x08 PACKET_CHANNEL_MSG_RECV Channel message (standard) 0x09 PACKET_CURRENT_TIME Current time response 0x0A PACKET_NO_MORE_MSGS No more messages available 0x0C PACKET_BATTERY Battery level 0x0D PACKET_DEVICE_INFO Device information 0x10 PACKET_CONTACT_MSG_RECV_V3 Contact message (V3 with SNR) 0x11 PACKET_CHANNEL_MSG_RECV_V3 Channel message (V3 with SNR) 0x12 PACKET_CHANNEL_INFO Channel information 0x1B PACKET_CHANNEL_DATA_RECV Channel data datagram 0x80 PACKET_ADVERTISEMENT Advertisement packet 0x82 PACKET_ACK Acknowledgment 0x83 PACKET_MESSAGES_WAITING Messages waiting notification 0x88 PACKET_LOG_DATA RF log data (can be ignored)"},{"location":"companion_protocol/#parsing-responses","title":"Parsing Responses","text":"

PACKET_OK (0x00):

Byte 0: 0x00\nBytes 1-4: Optional value (32-bit little-endian integer)\n

PACKET_ERROR (0x01):

Byte 0: 0x01\nByte 1: Error code (optional)\n

PACKET_CHANNEL_INFO (0x12):

Byte 0: 0x12\nByte 1: Channel Index\nBytes 2-33: Channel Name (32 bytes, null-terminated)\nBytes 34-49: Secret (16 bytes)\n

Note: The device returns the 16-byte channel secret in this response.

PACKET_DEVICE_INFO (0x0D):

Byte 0: 0x0D\nByte 1: Firmware Version (uint8)\nBytes 2+: Variable length based on firmware version\n\nFor firmware version >= 3:\nByte 2: Max Contacts Raw (uint8, actual = value * 2)\nByte 3: Max Channels (uint8)\nBytes 4-7: BLE PIN (32-bit little-endian)\nBytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded)\nBytes 20-59: Model (40 bytes, UTF-8, null-padded)\nBytes 60-79: Version (20 bytes, UTF-8, null-padded)\nByte 80: Client repeat enabled/preferred (firmware v9+)\nByte 81: Path hash mode (firmware v10+)\n

Parsing Pseudocode:

def parse_device_info(data):\n    if len(data) < 2:\n        return None\n\n    fw_ver = data[1]\n    info = {'fw_ver': fw_ver}\n\n    if fw_ver >= 3 and len(data) >= 80:\n        info['max_contacts'] = data[2] * 2\n        info['max_channels'] = data[3]\n        info['ble_pin'] = int.from_bytes(data[4:8], 'little')\n        info['fw_build'] = data[8:20].decode('utf-8').rstrip('\\x00').strip()\n        info['model'] = data[20:60].decode('utf-8').rstrip('\\x00').strip()\n        info['ver'] = data[60:80].decode('utf-8').rstrip('\\x00').strip()\n\n    return info\n

PACKET_BATTERY (0x0C):

Byte 0: 0x0C\nBytes 1-2: Battery Voltage (16-bit little-endian, millivolts)\nBytes 3-6: Used Storage (32-bit little-endian, KB)\nBytes 7-10: Total Storage (32-bit little-endian, KB)\n

Parsing Pseudocode:

def parse_battery(data):\n    if len(data) < 3:\n        return None\n\n    mv = int.from_bytes(data[1:3], 'little')\n    info = {'battery_mv': mv}\n\n    if len(data) >= 11:\n        info['used_kb'] = int.from_bytes(data[3:7], 'little')\n        info['total_kb'] = int.from_bytes(data[7:11], 'little')\n\n    return info\n

PACKET_SELF_INFO (0x05):

Byte 0: 0x05\nByte 1: Advertisement Type\nByte 2: TX Power\nByte 3: Max TX Power\nBytes 4-35: Public Key (32 bytes, hex)\nBytes 36-39: Advertisement Latitude (32-bit little-endian, divided by 1e6)\nBytes 40-43: Advertisement Longitude (32-bit little-endian, divided by 1e6)\nByte 44: Multi ACKs\nByte 45: Advertisement Location Policy\nByte 46: Telemetry Mode (bitfield)\nByte 47: Manual Add Contacts (bool)\nBytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0)\nBytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0)\nByte 56: Radio Spreading Factor\nByte 57: Radio Coding Rate\nBytes 58+: Device Name (UTF-8, variable length, no null terminator required)\n

Parsing Pseudocode:

def parse_self_info(data):\n    if len(data) < 36:\n        return None\n\n    offset = 1\n    info = {\n        'adv_type': data[offset],\n        'tx_power': data[offset + 1],\n        'max_tx_power': data[offset + 2],\n        'public_key': data[offset + 3:offset + 35].hex()\n    }\n    offset += 35\n\n    lat = int.from_bytes(data[offset:offset+4], 'little') / 1e6\n    lon = int.from_bytes(data[offset+4:offset+8], 'little') / 1e6\n    info['adv_lat'] = lat\n    info['adv_lon'] = lon\n    offset += 8\n\n    info['multi_acks'] = data[offset]\n    info['adv_loc_policy'] = data[offset + 1]\n    telemetry_mode = data[offset + 2]\n    info['telemetry_mode_env'] = (telemetry_mode >> 4) & 0b11\n    info['telemetry_mode_loc'] = (telemetry_mode >> 2) & 0b11\n    info['telemetry_mode_base'] = telemetry_mode & 0b11\n    info['manual_add_contacts'] = data[offset + 3] > 0\n    offset += 4\n\n    freq = int.from_bytes(data[offset:offset+4], 'little') / 1000.0\n    bw = int.from_bytes(data[offset+4:offset+8], 'little') / 1000.0\n    info['radio_freq'] = freq\n    info['radio_bw'] = bw\n    info['radio_sf'] = data[offset + 8]\n    info['radio_cr'] = data[offset + 9]\n    offset += 10\n\n    if offset < len(data):\n        name_bytes = data[offset:]\n        info['name'] = name_bytes.decode('utf-8').rstrip('\\x00').strip()\n\n    return info\n

PACKET_MSG_SENT (0x06):

Byte 0: 0x06\nByte 1: Route Flag (0 = direct, 1 = flood)\nBytes 2-5: Tag / Expected ACK (4 bytes, little-endian)\nBytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds)\n

PACKET_ACK (0x82):

Byte 0: 0x82\nBytes 1-6: ACK Code (6 bytes, hex)\n
"},{"location":"companion_protocol/#error-codes","title":"Error Codes","text":"

PACKET_ERROR (0x01) carries a single-byte error code in byte 1. Values match the ERR_CODE_* constants defined in examples/companion_radio/MyMesh.cpp:

Code Constant (firmware) Description 1 ERR_CODE_UNSUPPORTED_CMD Unknown or unsupported command byte / sub-command 2 ERR_CODE_NOT_FOUND Target not found (channel, contact, message, etc.) 3 ERR_CODE_TABLE_FULL Internal queue or table is full \u2014 retry later 4 ERR_CODE_BAD_STATE Operation not valid in current device state (e.g. iterator already running) 5 ERR_CODE_FILE_IO_ERROR Filesystem or storage I/O failure 6 ERR_CODE_ILLEGAL_ARG Invalid argument (bad length, out-of-range value, reserved field, etc.)

Note: Error codes may vary by firmware version. Always check byte 1 of PACKET_ERROR response, and treat unknown codes as generic errors.

"},{"location":"companion_protocol/#frame-handling","title":"Frame Handling","text":"

BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.

"},{"location":"companion_protocol/#response-handling","title":"Response Handling","text":"
  1. Command-Response Pattern:
  2. Send command via RX characteristic
  3. Wait for response via TX characteristic (notification)
  4. Match response to command using sequence numbers or command type
  5. Handle timeout (typically 5 seconds)
  6. Use command queue to prevent concurrent commands

  7. Asynchronous Messages:

  8. Device may send messages at any time via TX characteristic
  9. Handle PACKET_MESSAGES_WAITING (0x83) by polling GET_MESSAGE command
  10. Parse incoming messages and route to appropriate handlers
  11. Validate frame length before decoding

  12. Response Matching:

  13. Match responses to commands by expected packet type:

  14. Timeout Handling:

  15. Default timeout: 5 seconds per command
  16. On timeout: Log error, clear current command, proceed to next in queue
  17. Some commands may take longer (e.g., SET_CHANNEL may need 1-2 seconds)
  18. Consider longer timeout for channel operations

  19. Error Recovery:

  20. On PACKET_ERROR: Log error code, clear current command
  21. On connection loss: Clear command queue, attempt reconnection
  22. On invalid response: Log warning, clear current command, proceed
"},{"location":"companion_protocol/#example-implementation-flow","title":"Example Implementation Flow","text":""},{"location":"companion_protocol/#initialization","title":"Initialization","text":"
# 1. Scan for MeshCore device\ndevice = scan_for_device(\"MeshCore\")\n\n# 2. Connect to BLE GATT\ngatt = connect_to_device(device)\n\n# 3. Discover services and characteristics\nservice = discover_service(gatt, \"6E400001-B5A3-F393-E0A9-E50E24DCCA9E\")\nrx_char = discover_characteristic(service, \"6E400002-B5A3-F393-E0A9-E50E24DCCA9E\")\ntx_char = discover_characteristic(service, \"6E400003-B5A3-F393-E0A9-E50E24DCCA9E\")\n\n# 4. Enable notifications on TX characteristic\nenable_notifications(tx_char, on_notification_received)\n\n# 5. Send AppStart command\nsend_command(rx_char, build_app_start())\nwait_for_response(PACKET_SELF_INFO)\n
"},{"location":"companion_protocol/#creating-a-private-channel","title":"Creating a Private Channel","text":"
# 1. Generate 16-byte secret\nsecret_16_bytes = generate_secret(16)  # Use CSPRNG\nsecret_hex = secret_16_bytes.hex()\n\n# 2. Build SET_CHANNEL command\nchannel_name = \"YourChannelName\"\nchannel_index = 1  # Use 1-7 for private channels\ncommand = build_set_channel(channel_index, channel_name, secret_16_bytes)\n\n# 3. Send command\nsend_command(rx_char, command)\nresponse = wait_for_response(PACKET_OK)\n\n# 4. Store secret locally\nstore_channel_secret(channel_index, secret_hex)\n
"},{"location":"companion_protocol/#sending-a-message","title":"Sending a Message","text":"
# 1. Build channel message command\nchannel_index = 1\nmessage = \"Hello, MeshCore!\"\ntimestamp = int(time.time())\ncommand = build_channel_message(channel_index, message, timestamp)\n\n# 2. Send command\nsend_command(rx_char, command)\nresponse = wait_for_response(PACKET_MSG_SENT)\n
"},{"location":"companion_protocol/#receiving-messages_1","title":"Receiving Messages","text":"
def on_notification_received(data):\n    packet_type = data[0]\n\n    if packet_type == PACKET_CHANNEL_MSG_RECV or packet_type == PACKET_CHANNEL_MSG_RECV_V3:\n        message = parse_channel_message(data)\n        handle_channel_message(message)\n    elif packet_type == PACKET_MESSAGES_WAITING:\n        # Poll for messages\n        send_command(rx_char, build_get_message())\n
"},{"location":"companion_protocol/#best-practices","title":"Best Practices","text":"
  1. Connection Management:
  2. Implement auto-reconnect with exponential backoff
  3. Handle disconnections gracefully
  4. Store last connected device address for quick reconnection

  5. Secret Management:

  6. Always use cryptographically secure random number generators
  7. Store secrets securely (encrypted storage)
  8. Never log or transmit secrets in plain text

  9. Message Handling:

  10. Send CMD_SYNC_NEXT_MESSAGE when PUSH_CODE_MSG_WAITING is received
  11. Implement message deduplication to avoid displaying the same message twice

  12. Channel Management:

  13. Error Handling:

  14. Implement timeouts for all commands (typically 5 seconds)
  15. Handle RESP_CODE_ERR responses appropriately
"},{"location":"companion_protocol/#troubleshooting","title":"Troubleshooting","text":""},{"location":"companion_protocol/#connection-issues","title":"Connection Issues","text":""},{"location":"companion_protocol/#command-issues","title":"Command Issues","text":""},{"location":"companion_protocol/#message-issues","title":"Message Issues","text":""},{"location":"docs/","title":"Local Documentation","text":"

This document explains how to build and view the MeshCore documentation locally.

"},{"location":"docs/#building-and-viewing-docs","title":"Building and viewing Docs","text":"
pip install mkdocs\npip install mkdocs-material\n
"},{"location":"faq/","title":"Frequently Asked Questions","text":"

A list of frequently-asked questions and answers for MeshCore

"},{"location":"faq/#1-introduction","title":"1. Introduction","text":""},{"location":"faq/#11-q-what-is-meshcore","title":"1.1. Q: What is MeshCore?","text":"

A: MeshCore is a multi-platform system for enabling secure text-based communications utilizing LoRa radio hardware. It can be used for Off-Grid Communication, Emergency Response & Disaster Recovery, Outdoor Activities, Tactical Security including law enforcement and private security and also IoT sensor networks. (source)

MeshCore is free and open source:

Some more advanced, but optional features are available on T-Deck if you register your device for a key to unlock. On the MeshCore smartphone clients for Android and iOS/iPadOS, you can unlock the wait timer for repeater and room server remote management over RF feature.

These features are completely optional and aren't needed for the core messaging experience. They're like super bonus features and to help the developers continue to work on these amazing features, they may charge a small fee for an unlock code to utilize the advanced features.

Anyone is able to build anything they like on top of MeshCore without paying anything.

"},{"location":"faq/#12-q-what-do-you-need-to-start-using-meshcore","title":"1.2. Q: What do you need to start using MeshCore?","text":"

A: Everything you need for MeshCore is available at:

You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server).

"},{"location":"faq/#121-hardware","title":"1.2.1. Hardware","text":"

MeshCore is available on a variety of 433MHz, 868MHz and 915MHz LoRa devices. For example, Lilygo T-Deck, T-Pager, RAK Wireless WisBlock RAK4631 devices (e.g. 19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Nano G2 Ultra, Seeed Studio T1000-E. More devices are being added regularly.

For an up-to-date list of supported devices, please go to https://flasher.meshcore.io

To use MeshCore without using a phone as the client interface, you can run MeshCore on a LilyGo T-Deck, T-Deck Plus, T-Pager, T-Watch, or T-Display Pro. MeshCore Ultra firmware running on these devices is a complete off-grid secure communication solution.

"},{"location":"faq/#122-firmware","title":"1.2.2. Firmware","text":"

MeshCore has four firmware types that are not available on other LoRa systems. MeshCore has the following:

"},{"location":"faq/#123-companion-radio-firmware","title":"1.2.3. Companion Radio Firmware","text":"

Companion radios are for connecting to the Android app or web app as a messenger client. There are two different companion radio firmware versions:

  1. BLE Companion BLE Companion firmware runs on a supported LoRa device and connects to a smart device running the Android or iOS MeshCore client over BLE https://meshcore.io

  2. USB Serial Companion USB Serial Companion firmware runs on a supported LoRa device and connects to a smart device or a computer over USB Serial running the MeshCore web client https://app.meshcore.nz

"},{"location":"faq/#124-repeater","title":"1.2.4. Repeater","text":"

Repeaters are used to extend the range of a MeshCore network. Repeater firmware runs on the same devices that run client firmware. A repeater's job is to forward MeshCore packets to the destination device. It does not forward or retransmit every packet it receives, unlike other LoRa mesh systems.

A repeater can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.

"},{"location":"faq/#125-room-server","title":"1.2.5. Room Server","text":"

A room server is a simple BBS server for sharing posts. T-Deck devices running MeshCore firmware or a BLE Companion client connected to a smartphone running the MeshCore app can connect to a room server.

Room servers store message history on them and push the stored messages to users. Room servers allow roaming users to come back later and retrieve message history. With channels, messages are either received when it's sent, or not received and missed if the channel user is out of range. Room servers are different and more like email servers where you can come back later and get your emails from your mail server.

A room server can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.

When a client logs into a room server, the client will receive the previously 32 unseen messages.

Although room server can also repeat with the command line command set repeat on, it is not recommended nor encouraged. A room server with repeat set to on lacks the full set of repeater and remote administration features that are only available in the repeater firmware.

The recommendation is to run repeater and room server on separate devices for the best experience.

"},{"location":"faq/#2-initial-setup","title":"2. Initial Setup","text":""},{"location":"faq/#21-q-how-many-devices-do-i-need-to-start-using-meshcore","title":"2.1. Q: How many devices do I need to start using MeshCore?","text":"

A: If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android or iOS client via Bluetooth. You can start communicating with other MeshCore users near you.

If you have two supported devices, and there are not many MeshCore users near you, flash both to BLE Companion firmware so you can use your devices to communicate with your nearby friends and family.

If you have two supported devices, and there are other MeshCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach.

After you flashed the latest firmware onto your repeater device, keep the device connected to your computer via USB serial, use the console feature on the web flasher and set the frequency for your region or country, so your client can remote administer the repeater or room server over RF:

set freq {frequency}

The repeater and room server CLI reference is here: https://docs.meshcore.io/cli_commands

If you have more supported devices, you can use your additional devices with the room server firmware.

"},{"location":"faq/#22-q-does-meshcore-cost-any-money","title":"2.2. Q: Does MeshCore cost any money?","text":"

A: All radio firmware versions (e.g. for Heltec V3, RAK, T-1000E, etc.) are free and open source developed by Scott at Ripple Radios.

The native Android and iOS client uses the freemium model and is developed by Liam Cottle, developer of meshtastic map at meshtastic.liamcottle.net on GitHub and reticulum-meshchat on GitHub.

The T-Deck firmware is free to download and most features are available without cost. To support the firmware developer, you can pay for a registration key to unlock your T-Deck for deeper map zoom and remote server administration over RF using the T-Deck. You do not need to pay for the registration to use your T-Deck for direct messaging and connecting to repeaters and room servers.

"},{"location":"faq/#23-q-what-frequencies-are-supported-by-meshcore","title":"2.3. Q: What frequencies are supported by MeshCore?","text":"

A: It supports the 868MHz range in the UK/EU and the 915MHz range in New Zealand, Australia, and the USA. Countries and regions in these two frequency ranges are also supported.

Use the smartphone client or the repeater setup feature on the web flasher to set your radios' RF settings by choosing the preset for your regions.

Recently, as of October 2025, many regions have moved to the \"narrow\" setting, aka using BW62.5 and a lower SF number (instead of the original SF11). For example, USA/Canada (Recommended) preset is 910.525MHz, SF7, BW62.5, CR5.

After extensive testing, many regions have switched or about to switch over to BW62.5 and SF7, 8, or 9. Narrower bandwidth setting and lower SF setting allow MeshCore's radio signals to fit between interference in the ISM band, provide for a lower noise floor, better SNR, and faster transmissions.

If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the #meshcore-app channel on the MeshCore Discord server to let Liam Cottle know.

"},{"location":"faq/#24-q-what-is-an-advert-in-meshcore","title":"2.4. Q: What is an \"advert\" in MeshCore?","text":"

A: Advert means to advertise yourself on the network. In Reticulum terms it would be to announce. In Meshtastic terms it would be the node sending its node info.

MeshCore allows you to manually broadcast your name, position and public encryption key, which is also signed to prevent spoofing. When you click the advert button, it broadcasts that data over LoRa. MeshCore calls that an Advert. There's two ways to advert, \"zero hop\" and \"flood\".

MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 12 hours by default. This interval can be configured using the following command:

set flood.advert.interval {hours}

The separate set advert.interval {minutes} command controls the local zero-hop advert timer.

"},{"location":"faq/#25-q-is-there-a-hop-limit","title":"2.5. Q: Is there a hop limit?","text":"

A: Internally the firmware has maximum limit of 64 hops. In real world settings it will be difficult to get close to the limit due to the environments and timing as packets travel further and further. We want to hear how far your MeshCore conversations go.

"},{"location":"faq/#3-server-administration","title":"3. Server Administration","text":""},{"location":"faq/#31-q-how-do-you-configure-a-repeater-or-a-room-server","title":"3.1. Q: How do you configure a repeater or a room server?","text":"

A: When MeshCore is flashed onto a LoRa device for the first time, it is necessary to set the server device's frequency to make it utilize the frequency that is legal in your country or region.

Repeater or room server can be administered with one of the options below:

https://buymeacoffee.com/ripplebiz/e/249834

"},{"location":"faq/#32-q-do-i-need-to-set-the-location-for-a-repeater","title":"3.2. Q: Do I need to set the location for a repeater?","text":"

A: While not required, with location set for a repeater it will show up on the MeshCore map in the future. Set location with the following command:

set lat <GPS Lat>

set lon <GPS Lon>

You can get the latitude and longitude from Google Maps by right-clicking the location you are at on the map.

"},{"location":"faq/#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server","title":"3.3. Q: What is the password to administer a repeater or a room server?","text":"

A: The default admin password to a repeater and room server is password. Use the following command to change the admin password:

password {new-password}

"},{"location":"faq/#34-q-what-is-the-password-to-join-a-room-server","title":"3.4. Q: What is the password to join a room server?","text":"

A: The default guest password to a room server is hello. Use the following command to change the guest password:

set guest.password {guest-password}

"},{"location":"faq/#35-q-can-i-retrieve-a-repeaters-private-key-or-set-a-repeaters-private-key","title":"3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key?","text":"

A: You can issue these commands to get or set a repeater's private key using a USB serial connection.

get prv.key to print a repeater's private key on the serial console set prv.key <hex> to set a repeater's private key on the serial console

Reboot the repeater after set prv.key <hex> command for the new private key to take effect.

"},{"location":"faq/#36-q-the-first-byte-of-my-repeaters-public-key-collides-with-an-existing-repeater-on-the-mesh-how-do-i-get-a-new-private-key-with-a-matching-public-key-that-has-its-first-byte-of-my-choosing","title":"3.6. Q: The first byte of my repeater's public key collides with an existing repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?","text":"

A: You can generate a new private key and specify the first byte of its public key here: https://gessaman.com/mc-keygen

Having multiple repeaters with the same first byte ID does not negatively affect the mesh or its functionality. Flood and pathed packets will still reach their destinations. First byte ID collision makes traceroute and path analysis harder because these tools don't know exactly which of the two (or more) colliding repeaters is the one in the path.

Best practice is when you set up a new repeater, choose a public key that is not in use. If it is not possible to find a unique first byte for your repeater's public key, choose one that is unique within about 10 miles (16 km) to minimize collision with nearby repeaters.

"},{"location":"faq/#37-q-my-repeater-may-be-suffering-from-deafness-due-to-high-power-interference-near-my-meshs-frequency-it-is-not-hearing-other-in-range-meshcore-radios-what-can-i-do","title":"3.7. Q: My repeater may be suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do?","text":"

A: This may be due to the SX1262 radio's auto gain control feature. You can use this command to periodically reset its AGC.

set agc.reset.interval <number>

The <number> unit is in seconds and is incremented by 4. set agc.reset.interval 4 works well to cure deafness.

This is a very low-cost operation. AGC reset is done by simply setting state = STATE_IDLE; in function RadioLibWrapper::resetAGC() in RadioLibWrappers.cpp

"},{"location":"faq/#38-q-how-do-i-make-my-repeater-an-observer-on-the-mesh","title":"3.8. Q: How do I make my repeater an observer on the mesh?","text":"

A: The observer instruction is available here: https://analyzer.letsmesh.net/observer/onboard

"},{"location":"faq/#39-q-what-is-multibyte-support-what-do-1-byte-2-byte-3-byte-adverts-and-messages-mean","title":"3.9. Q: What is multibyte support? What do 1-byte, 2-byte, 3-byte adverts and messages mean?","text":"

A: The original MeshCore protocol design uses the first byte of a repeater's public key to denote the repeater in a path. And with 1 byte for each repeater in the path, MeshCore packets can travel as many as 64 hops.

However, with 1 byte, there are only 254 unique IDs (exclude 00 and FF which are reserved). Many meshes group have multiple repeaters with the same first byte in their public keys. Packets continue to pass through repeaters and the mesh is not harmed in any way. It does make it harder for tools to analyze paths with duplicated repeater IDs.

Firmware version 1.14 and newer introduces the ability for repeaters to advert with 1-, 2-, or 3-byte adverts. Companions can also send out channel and direct messages with 1-, 2-, or 3-byte path. Adverts and messages sent in 1-byte path is compatible with repeater firmware older or newer than 1.14. They will travel up to 64 hops. 2-byte adverts and messages will travel up to 32 hops. 3-byte adverts and messages will travel up to 21 hops.

"},{"location":"faq/#391-q-what-path-hash-sizes-will-my-repeater-forward","title":"3.9.1. Q: What path hash sizes will my repeater forward?","text":"

Repeaters running firmware 1.14+ repeat packets sent with 1-, 2-, or 3-byte path hash. Repeaters on firmware older than 1.14 only repeat 1-byte path hash packets and silently drop 2- and 3-byte packets.

"},{"location":"faq/#392-q-what-determines-a-packets-path-hash-size","title":"3.9.2. Q: What determines a packet's path hash size?","text":"

The original packet sender determines the path hash size. The most common original sender is a companion app. The other common original sender is a repeater, when it broadcasts its advert.

"},{"location":"faq/#393-q-how-do-i-change-my-companions-path-hash-size","title":"3.9.3. Q: How do I change my companion's path hash size?","text":"

As of firmware version 1.14 and MeshCore app version 1.41.0, in the MeshCore app, you can set your companion's message path hash size in Settings (gear icon), Experimental Settings.

Until your regional mesh has the vast majority of the repeaters updated to 1.14+ firmware, it is recommended to keep your companion at the default 1-byte because pre-1.14 repeaters will silently drop messages with larger path hashes.

"},{"location":"faq/#394-q-what-does-the-cli-command-pathhashmode-do-on-a-repeater","title":"3.9.4. Q: What does the CLI command path.hash.mode do on a repeater?","text":"

This CLI command path.hash.mode only controls the path hash size used in a repeater's own advert broadcasts. It does NOT affect which packets the repeater forwards. A repeater with firmware 1.14+ always forward 1-, 2-, and 3-byte packets regardless of this setting.

Usage: set path.hash.mode {0|1|2}:

\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 path.hash.mode \u2502 Advert path hash size \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 0              \u2502 1 byte (default)      \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 1              \u2502 2 bytes               \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 2              \u2502 3 bytes               \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518  \n

It is safe to set your 1.14+ repeaters to mode 1 or 2.

"},{"location":"faq/#395-q-why-use-2-or-3-byte-path-hash-for-adverts","title":"3.9.5. Q: Why use 2- or 3-byte path hash for adverts?","text":"

A longer path hash helps tools like the LetsMesh.net Analyzer and MeshMapper disambiguate repeaters more reliably. With only 1 byte, the chance of different repeaters having the same first byte in their public key is high, making it harder to tell them apart in mesh network analysis. Since this only affects adverts, there's no downside. 2- and 3-byte adverts don't travel as far as 1-byte adverts, but it is not important for MeshCore nodes to hear a repeater's advert that is 21 or 32 hops away.

"},{"location":"faq/#396-q-when-can-we-move-away-from-1-byte-path-hash-for-channel-and-direct-messages","title":"3.9.6. Q: When can we move away from 1-byte path hash for channel and direct messages?","text":"

You should move to send 2-byte or 3-byte channel and direct messages when the vast majority of the repeaters in your regional mesh are updated to firmware version 1.14 or newer. Setting your repeater's path.hash.mode to 1 (for 2-byte path hash) or 2 (for 3-byte path hash) now helps the community gauge to how many repeaters have updated to 1.14+. Please work with your MeshCore community together to decide when to switch to 2-byte path or 3-byte path for channel and direct messages.

"},{"location":"faq/#4-t-deck-related","title":"4. T-Deck Related","text":""},{"location":"faq/#41-q-is-there-a-user-guide-for-t-deck-t-pager-t-watch-or-t-display-pro","title":"4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?","text":"

A: Yes, it is available on https://buymeacoffee.com/ripplebiz/ultra-v7-7-guide-meshcore-users

"},{"location":"faq/#42-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode","title":"4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?","text":"

A:

  1. Device off
  2. Connect USB cable to device
  3. Hold down trackball (keep holding)
  4. Turn on device
  5. Hear USB connection sound
  6. Release trackball
  7. T-Deck in DFU mode now
  8. At this point you can begin flashing using https://flasher.meshcore.io
"},{"location":"faq/#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock","title":"4.3. Q: Why is my T-Deck Plus not getting any satellite lock?","text":"

A: For T-Deck Plus, the GPS baud rate should be set to 38400. Also, some T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open the device to check the GPS orientation.

GPS on T-Deck is always enabled. You can skip the \"GPS clock sync\" and the T-Deck will continue to try to get a GPS lock. You can go to the GPS Info screen; you should see the Sentences: counter increasing if the baud rate is correct.

Source

"},{"location":"faq/#44-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock","title":"4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?","text":"

A: The OG (non-Plus) T-Deck doesn't come with a GPS. If you added a GPS to your OG T-Deck, please refer to the manual of your GPS to see what baud rate it requires. Alternatively, you can try to set the baud rate from 9600, 19200, etc., and up to 115200 to see which one works.

"},{"location":"faq/#45-q-what-size-of-sd-card-does-the-t-deck-support","title":"4.5. Q: What size of SD card does the T-Deck support?","text":"

A: Users have had no issues using 16GB or 32GB SD cards. Format the SD card to FAT32.

"},{"location":"faq/#46-q-what-is-the-public-key-for-the-default-public-channel","title":"4.6. Q: What is the public key for the default public channel?","text":"

A: T-Deck uses the same key the smartphone apps use but in base64 izOH6cXN6mrJ5e26oRXNcg==

There is no = key on the T-Deck's hardware keyboard. You can use the on-screen software keyboard to enter =. Tap the text box to enable the on-screen software keyboard. The third character is the capital letter O (Oh), not zero 0

The smartphone app key is in hex: 8b3387e9c5cdea6ac9e5edbaa115cd72

Source

"},{"location":"faq/#47-q-how-do-i-get-maps-on-t-deck","title":"4.7. Q: How do I get maps on T-Deck?","text":"

A: You need map tiles. You can get pre-downloaded map tiles here (a good way to support development):

Another way to download map tiles is to use this Python script to get the tiles in the areas you want: https://github.com/fistulareffigy/MTD-Script

There is also a modified script that adds additional error handling and parallel downloads: https://github.com/TheBestJohn/MTD-Script

"},{"location":"faq/#48-q-where-do-the-map-tiles-go","title":"4.8. Q: Where do the map tiles go?","text":"

Once you have the tiles downloaded, copy the \\tiles folder to the root of your T-Deck's SD card.

"},{"location":"faq/#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck","title":"4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?","text":"

A: You can download, install, and use the T-Deck firmware for free, but it has some features (map zoom, server administration) that are enabled if you purchase an unlock code for \\$10 per T-Deck device. Unlock page: https://buymeacoffee.com/ripplebiz/e/249834

"},{"location":"faq/#410-q-how-to-decipher-the-diagnostics-screen-on-t-deck","title":"4.10. Q: How to decipher the diagnostics screen on T-Deck?","text":"

A: Space is tight on T-Deck's screen, so the information is a bit cryptic. The format is : {hops} l:{packet-length}({payload-len}) t:{packet-type} snr:{n} rssi:{n}

See here for packet-type: https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h#L19

#define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)\n#define PAYLOAD_TYPE_RESPONSE 0x01 // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)\n#define PAYLOAD_TYPE_TXT_MSG 0x02 // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)\n#define PAYLOAD_TYPE_ACK 0x03 // a simple ack #define PAYLOAD_TYPE_ADVERT 0x04 // a node advertising its Identity\n#define PAYLOAD_TYPE_GRP_TXT 0x05 // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, \"name: msg\")\n#define PAYLOAD_TYPE_GRP_DATA 0x06 // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: data_type, data_len, blob)\n#define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)\n#define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)\n

Source

"},{"location":"faq/#411-q-the-t-deck-sound-is-too-loud","title":"4.11. Q: The T-Deck sound is too loud?","text":""},{"location":"faq/#412-q-can-you-customize-the-sound","title":"4.12. Q: Can you customize the sound?","text":"

A: You can customize the sounds on the T-Deck, by placing .mp3 files onto the root dir of the SD card. The files are:

"},{"location":"faq/#413-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts","title":"4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?","text":"

A: 'Import from Clipboard' is for importing a contact via a file named 'clipboard.txt' on the SD card. The opposite, is in the Identity screen, the 'Card to Clipboard' menu, which writes to 'clipboard.txt' so you can share yourself (call these 'biz cards', that start with \"meshcore://...\")

"},{"location":"faq/#414-q-how-to-capture-a-screenshot-on-t-deck","title":"4.14. Q: How to capture a screenshot on T-Deck?","text":"

A: To capture a screenshot on a T-Deck, long press the top-left corner of the screen. The screenshot is saved to the microSD card, if one is inserted into the device.

"},{"location":"faq/#5-general","title":"5. General","text":""},{"location":"faq/#51-q-what-are-bw-sf-and-cr","title":"5.1. Q: What are BW, SF, and CR?","text":"

A:

BW is bandwidth - width of frequency spectrum that is used for transmission

SF is spreading factor - how much should the communication spread in time

CR is coding rate - from: https://www.thethingsnetwork.org/docs/lorawan/fec-and-code-rate

TL;DR: default CR to 5 for good stable links. If it is not a solid link and is intermittent, change CR to 7 or 8.

Forward Error Correction is a process of adding redundant bits to the data to be transmitted. During the transmission, data may get corrupted by interference (changes from 0 to 1 / 1 to 0). These error correction bits are used at the receivers for restoring corrupted bits.

The Code Rate of a forward error correction expresses the proportion of bits in a data stream that actually carry useful information.

There are 4 code rates used in LoRaWAN:

4/5 4/6 5/7 4/8

For example, if the code rate is 5/7, for every 5 bits of useful information, the coder generates a total of 7 bits of data, of which 2 bits are redundant.

Making the bandwidth 2x wider (from BW125 to BW250) allows you to send 2x more bytes in the same time. Making the spreading factor 1 step lower (from SF10 to SF9) allows you to send 2x more bytes in the same time.

Lowering the spreading factor makes it more difficult for the gateway to receive a transmission, as it will be more sensitive to noise. You could compare this to two people talking in a noisy place (a bar for example). If you\u2019re far from each other, you have to talk slow (SF10), but if you\u2019re close, you can talk faster (SF7)

So, it's a balancing act between speed of the transmission and resistance to noise. The Things Network is mainly focused on LoRaWAN, but the LoRa low-level stuff still checks out for any LoRa project

"},{"location":"faq/#52-q-do-meshcore-clients-repeat","title":"5.2. Q: Do MeshCore clients repeat?","text":"

A: No, MeshCore clients do not repeat. This is the core of MeshCore's messaging-first design. This is to avoid devices flooding the airwaves and create endless collisions, so messages sent aren't received. In MeshCore, only repeaters and room servers with set repeat on repeat.

"},{"location":"faq/#53-q-what-happens-when-a-node-learns-a-route-via-a-mobile-repeater-and-that-repeater-is-gone","title":"5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone?","text":"

A: If you used to reach a node through a repeater and the repeater is no longer reachable, the client will send the message using the existing (but now broken) known path, the message will fail after 3 retries, and the app will reset the path and send the message as flood on the last retry by default. This can be turned off in settings. If the destination is reachable directly or through another repeater, the new path will be used going forward. Or you can set the path manually if you know a specific repeater to use to reach that destination.

In the case if users are moving around frequently, and the paths are breaking, they just see the phone client retries and revert to flood to attempt to re-establish a path.

"},{"location":"faq/#54-q-how-does-a-node-discover-a-path-to-its-destination-and-then-use-it-to-send-messages-in-the-future-instead-of-flooding-every-message-it-sends-like-meshtastic","title":"5.4. Q: How does a node discover a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?","text":"

Routes are stored in sender's contact list. When you send a message the first time, the message first gets to your destination by flood routing. When your destination node gets the message, it will send back a delivery report to the sender with all repeaters that the original message went through. This delivery report is flood-routed back to you the sender and is a basis for future direct path. When you send the next message, the path will get embedded into the packet and be evaluated by repeaters. If the hop and address of the repeater matches, it will retransmit the message, otherwise it will not retransmit, hence minimizing utilization.

Source

"},{"location":"faq/#55-q-do-public-channels-always-flood-do-private-channels-always-flood","title":"5.5. Q: Do public channels always flood? Do private channels always flood?","text":"

A: Yes, group channels are A to B, so there is no defined path. They have to flood. Repeaters can however deny flood traffic up to some hop limit, with the set flood.max CLI command. Administrators of repeaters get to set the rules of their repeaters.

Source

"},{"location":"faq/#56-q-what-is-the-public-key-for-the-default-public-channel","title":"5.6. Q: What is the public key for the default public channel?","text":"

A: The smartphone app key is in hex:

8b3387e9c5cdea6ac9e5edbaa115cd72

T-Deck uses the same key but in base64:

izOH6cXN6mrJ5e26oRXNcg==

The third character is the capital letter O, not zero 0.

Source

"},{"location":"faq/#57-q-is-meshcore-open-source","title":"5.7. Q: Is MeshCore open source?","text":"

A: Most of the firmware is freely available. Everything is open source except the T-Deck firmware and Liam's native mobile apps.

Firmware repo: https://github.com/meshcore-dev/MeshCore

"},{"location":"faq/#58-q-how-can-i-support-meshcore","title":"5.8. Q: How can I support MeshCore?","text":"

A: Provide your honest feedback on GitHub and on MeshCore Discord server. Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at https://buymeacoffee.com/ripplebiz.

Support Liam Cottle's smartphone client development by unlocking the server administration wait gate with in-app purchase

Support Rastislav Vysoky (recrof)'s flasher website and the map website development through PayPal or Revolut

"},{"location":"faq/#59-q-how-do-i-build-meshcore-firmware-from-source","title":"5.9. Q: How do I build MeshCore firmware from source?","text":"

A: See instructions here: https://discord.com/channels/826570251612323860/1330643963501351004/1341826372120608769

Build instructions for MeshCore:

For Windows, first install WSL and Python+pip via: https://plainenglish.io/blog/setting-up-python-on-windows-subsystem-for-linux-wsl-26510f1b2d80

(Linux, Windows+WSL) In the terminal/shell:

sudo apt update\nsudo apt install libpython3-dev\nsudo apt install python3-venv\n

Mac: python3 should be already installed.

Then it should be the same for all platforms:

python3 -m venv meshcore\ncd meshcore && source bin/activate\npip install -U platformio\ngit clone https://github.com/ripplebiz/MeshCore.git\ncd MeshCore\n

open platformio.ini and in [arduino_base] edit the LORA_FREQ=867.5 save, then run:

pio run -e RAK_4631_Repeater\n

then you'll find firmware.zip in .pio/build/RAK_4631_Repeater

"},{"location":"faq/#510-q-are-there-other-meshcore-related-open-source-projects","title":"5.10. Q: Are there other MeshCore related open source projects?","text":"

A: Liam Cottle's MeshCore web client and MeshCore JavaScript library are open source under MIT license.

Web client: https://github.com/liamcottle/meshcore-web Javascript: https://github.com/liamcottle/meshcore.js

"},{"location":"faq/#511-q-does-meshcore-support-atak","title":"5.11. Q: Does MeshCore support ATAK?","text":"

A: ATAK is not currently on MeshCore's roadmap.

MeshCore would not be best suited to ATAK because MeshCore:

MeshCore clients would need to reset path constantly and flood traffic across the network which could lead to lots of collisions with something as chatty as ATAK.

This could change in the future if MeshCore develops a client firmware that repeats.

Source

"},{"location":"faq/#512-q-how-do-i-add-a-node-to-the-meshcore-map","title":"5.12. Q: How do I add a node to the MeshCore Map?","text":"

A:

To add a BLE Companion radio, connect to the BLE Companion radio from the MeshCore smartphone app. In the app, tap the 3 dot menu icon at the top right corner, then tap Internet Map. Tap the 3 dot menu icon again and choose Add me to the Map

To add a Repeater or Room Server to the map, go to the Contact List, tap the 3 dot next to the Repeater or Room Server you want to add to the Internet Map, tap Share, then tap Upload to Internet Map.

You can use the same companion (same public key) that you used to add your repeaters or room servers to remove them from the Internet Map.

"},{"location":"faq/#513-q-can-i-use-a-raspberry-pi-to-update-a-meshcore-radio","title":"5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?","text":"

A: Yes. Below are the instructions to flash firmware onto a supported LoRa device using a Raspberry Pi over USB serial.

Instructions for nRF devices like RAK, T1000-E, T114 are immediately after the ESP instructions

For ESP-based devices (e.g. Heltec V3) you need:

  1. Download the firmware file from https://flasher.meshcore.io.
  2. Confirm the ttyXXXX device path on your Raspberry Pi.
  3. Install esptool from the shell.
  4. Flash the firmware.

Instructions for nRF devices:

For nRF devices (e.g. RAK, Heltec T114) you need the following:

  1. Download the firmware file from https://flasher.meshcore.io.
  2. Confirm the ttyXXXX device path on your Raspberry Pi.
  3. Install adafruit-nrfutil.
  4. Flash the nRF device.

To manage a repeater or room server connected to a Pi over USB serial using shell commands, you need to install picocom. To install picocom, run the following command:

To start managing your USB serial-connected device using picocom, use the following command:

From here, reference repeater and room server command line commands in the MeshCore docs here:

"},{"location":"faq/#514-q-are-there-projects-built-around-meshcore","title":"5.14. Q: Are there projects built around MeshCore?","text":"

A: Yes, there are many. MeshCore's protocol is open source using the MIT license. The MIT license and the open source protocol makes it very easy for the MeshCore community to build new firmware for radios, applications on mobile devices, map tools, and analysis tools, and integration with other projects like Home Assistant.

As new MeshCore community projects become available on a weekly basis, we have stopped tracking them here in this FAQ. samuk maintains a very exhaustive list of MeshCore community project at https://github.com/samuk/awesome-meshcore/blob/main/README.md. samuk accepts PRs and merges them regularly.

"},{"location":"faq/#515-q-are-there-client-applications-for-windows-or-mac","title":"5.15. Q: Are there client applications for Windows or Mac?","text":"

A: Yes, the same iOS and Android client is also available for Windows and Mac. You can find them together with the Android APK here: https://files.liamcottle.net/MeshCore

Both the Windows and Mac versions of the client app are fully unlocked and are free to use.

"},{"location":"faq/#516-q-are-there-any-resources-that-compare-meshcore-to-other-lora-systems","title":"5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?","text":"

A: Here is a list of MeshCore comparison resources:

"},{"location":"faq/#6-troubleshooting","title":"6. Troubleshooting","text":""},{"location":"faq/#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago","title":"6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.","text":""},{"location":"faq/#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed","title":"6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.","text":"

A:

You can get the epoch time on https://www.epochconverter.com and use it to set your T-Deck clock. For a repeater and room server, the admin can use a T-Deck to remotely set their clock (clock sync), or use the time command in the USB serial console with the server device connected.

"},{"location":"faq/#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth","title":"6.3. Q: How to connect to a repeater via BLE (Bluetooth)?","text":"

A: You can't connect to a device running repeater firmware via Bluetooth. You can connect to devices running the BLE companion firmware via Bluetooth using the Android app.

"},{"location":"faq/#64-q-my-companion-isnt-showing-up-over-bluetooth","title":"6.4. Q: My companion isn't showing up over Bluetooth?","text":"

A: Make sure that you flashed the Bluetooth companion firmware and not the USB-only companion firmware.

"},{"location":"faq/#65-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code","title":"6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?","text":"

A: The default Bluetooth pairing code is 123456

"},{"location":"faq/#66-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone-it-cant-hold-a-solid-bluetooth-connection","title":"6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.","text":"

A: Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification.

"},{"location":"faq/#67-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh","title":"6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?","text":"

A:

  1. Connect USB-C cable to your device, per your device's instruction, get it to flash mode:
  2. A new folder will appear on your computer's desktop
  3. Download the flash_erase*.uf2 file for your device on https://flasher.meshcore.io
  4. drag and drop the uf2 file for your device to the root of the new folder
  5. Wait for the copy to complete. You might get an error dialog, you can ignore it
  6. Go to https://flasher.meshcore.io, click Console and select the serial port for your connected device
  7. In the console, press enter. Your flash should now be erased
  8. You may now flash the latest MeshCore firmware onto your device

Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by holding down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on https://flasher.meshcore.io

"},{"location":"faq/#68-q-webflasher-fails-on-linux-with-failed-to-open","title":"6.8. Q: WebFlasher fails on Linux with failed to open","text":"

A: If the usb port doesn't have the right ownership for this task, the process fails with the following error:

NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.

Allow the browser user on it:

# setfacl -m u:YOUR_USER_HERE:rw /dev/ttyUSB0

"},{"location":"faq/#7-other-questions","title":"7. Other Questions:","text":""},{"location":"faq/#71-q-how-to-update-nrf-rak-t114-seeed-xiao-companion-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app","title":"7.1. Q: How to update nRF (RAK, T114, Seeed XIAO) companion, repeater and room server firmware over the air using the new simpler DFU app?","text":"

A: The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:

  1. Download nRF's DFU app from iOS App Store or Android's Play Store, you can find the app by searching for nrf dfu, the app's full name is nRF Device Firmware Update
  2. On https://flasher.meshcore.io, download the ZIP version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao)
  3. From the MeshCore app, log in remotely to the repeater you want to update with admin privileges
  4. Go to the Command Line tab, type start ota and hit enter.
  5. You should see OK to confirm the repeater device is now in OTA mode
  6. Run the DFU app, then tap Settings in the top-right corner
  7. Enable Packet receipt notifications, and change Number of Packets to 10 for RAK, 8 for T114. 8 also works for RAK.
  8. Select the firmware zip file you downloaded
  9. Select the device you want to update. If the device you want to update is not on the list, try enabling OTA on the device again
  10. If the device is not found, enable Force Scanning in the DFU app
  11. Tap Upload to begin OTA update
  12. If it fails, try toggling Bluetooth on your phone. If that doesn't work, try rebooting your phone. If you keep getting failures at the \"Enabling Bootloader\" step, try forgetting the nRF board in your iOS or Android device's Bluetooth settings and re-pair it through the DFU app.
  13. Wait for the update to complete. It can take a few minutes.
  14. It is strongly recommended that you install and use the OTAFIX bootloader at https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX.
  15. To update a companion node over OTA, it must be running companion firmware v1.15 or greater.
  16. Please see the MeshCore Blog for additional information on OTA firmware flashing:
"},{"location":"faq/#711-q-can-i-update-seeed-studio-wio-tracker-l1-pro-using-ota","title":"7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?","text":"

A: You can flash this safer bootloader to the Wio Tracker L1 Pro https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX

After this bootloader is flashed onto the device, you can trigger an over-the-air update using Bluetooth by holding the button next to the D-Pad and then clicking the reset button. Then follow the same OTA update instructions above. You can skip the start ota instruction and start the update using the DFU app.

"},{"location":"faq/#72-q-how-to-update-esp32-based-devices-over-the-air","title":"7.2. Q: How to update ESP32-based devices over the air?","text":"

A: For ESP32-based devices (e.g. Heltec V3):

  1. On https://flasher.meshcore.io, download the non-merged version of the firmware for your ESP32 device (e.g. Heltec_v3_repeater-v1.6.2-4449fd3.bin, no \"merged\" in the file name).
  2. From the MeshCore app, log in remotely to the repeater you want to update with admin privileges.
  3. Go to the Command Line tab, type start ota and hit enter.
  4. You should see OK to confirm the repeater device is now in OTA mode.
  5. The command start ota on an ESP32-based device starts a Wi-Fi hotspot named MeshCore OTA.
  6. From your phone or computer connect to the 'MeshCore OTA' hotspot.
  7. From a browser, go to http://192.168.4.1/update and upload the non-merged bin from the flasher.
"},{"location":"faq/#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu","title":"7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?","text":"

A: Yes, developer che aporeps has an enhanced OTA DFU bootloader for nRF52 based devices. With this bootloader, if it detects that the application firmware is invalid, it falls back to OTA DFU mode so you can attempt to flash again to recover. This bootloader has other changes to make the OTA DFU process more fault tolerant.

Refer to https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX for the latest information.

Currently, the following boards are supported:

"},{"location":"faq/#74-q-are-the-meshcore-logo-and-font-available","title":"7.4. Q: Are the MeshCore logo and font available?","text":"

A: Yes, it is on the MeshCore GitHub repo here: https://github.com/meshcore-dev/MeshCore/tree/main/logo

"},{"location":"faq/#75-q-what-is-the-format-of-a-contact-or-channel-qr-code","title":"7.5. Q: What is the format of a contact or channel QR code?","text":"

A:

Channel: meshcore://channel/add?name=<name>&secret=<secret>

Contact: meshcore://contact/add?name=<name>&public_key=<secret>&type=<type>

Where &type is:

"},{"location":"faq/#76-q-how-do-i-connect-to-the-companion-via-wi-fi-eg-using-a-heltec-v3","title":"7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?","text":"

A: Wi-Fi firmware requires you to compile it yourself, as you need to set the Wi-Fi SSID and password. Edit WIFI_SSID and WIFI_PWD in ./variants/heltec_v3/platformio.ini and then flash it to your device.

"},{"location":"faq/#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-an-ebyte-e22-900m30s-or-an-ebyte-e22-900m33s-module-what-should-their-transmit-power-be-set-to","title":"7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?","text":"

A: For companion radios, you can set these radios' transmit power in the smartphone app. For repeater and room server radios, you can set their transmit power using the command line command set tx. You can get their current value using command line command get tx

\u26a0\ufe0f WARNING: Set these values at your own risk. Incorrect power settings can permanently damage your radio hardware.

Device / Model Region / Description In-App Setting (dBm) Target Radio Output Notes Station G2 Reference US915 Max Output 19 dBm 36.5 dBm (4.46W) US915 Max at 1dB compression point 16 dBm 35 dBm (3.16W) 1dB compression point EU868 Max at 1dB compression point 15 dBm 34.5 dBm (2.82W) 1dB compression point US915 1W Output 10 dBm 1W Refer to your local government's requirements EU868 1W Output 9 dBm 1W Refer to your local government's requirements Ikoka Stick E22-900M30S 1W Model 19 dBm 1W DO NOT EXCEED (Risk of burn out) data sheet Ikoka Stick E22-900M33S 2W Model 9 dBm 2W DO NOT EXCEED (Risk of burn out) data sheet Refer to your local government's requirements Heltec V4 Standard Output 10 dBm 22 dBm (~0.15W) High Output 22 dBm 28 dBm (~0.5W to 0.6W)"},{"location":"kiss_modem_protocol/","title":"MeshCore KISS Modem Protocol","text":"

Standard KISS TNC firmware for MeshCore LoRa radios. Compatible with any KISS client (Direwolf, APRSdroid, YAAC, etc.) for sending and receiving raw packets. MeshCore-specific extensions (cryptography, radio configuration, telemetry) are available through the standard SetHardware (0x06) command.

"},{"location":"kiss_modem_protocol/#serial-configuration","title":"Serial Configuration","text":"

115200 baud, 8N1, no flow control.

"},{"location":"kiss_modem_protocol/#frame-format","title":"Frame Format","text":"

Standard KISS framing per the KA9Q/K3MC specification.

Byte Name Description 0xC0 FEND Frame delimiter 0xDB FESC Escape character 0xDC TFEND Escaped FEND (FESC + TFEND = 0xC0) 0xDD TFESC Escaped FESC (FESC + TFESC = 0xDB)
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 FEND \u2502 Type Byte \u2502 Data (escaped)\u2502 FEND \u2502\n\u2502 0xC0 \u2502  1 byte   \u2502 0-510 bytes  \u2502 0xC0 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"kiss_modem_protocol/#type-byte","title":"Type Byte","text":"

The type byte is split into two nibbles:

Bits Field Description 7-4 Port Port number (0 for single-port TNC) 3-0 Command Command number

Maximum unescaped frame size: 512 bytes.

"},{"location":"kiss_modem_protocol/#standard-kiss-commands","title":"Standard KISS Commands","text":""},{"location":"kiss_modem_protocol/#host-to-tnc","title":"Host to TNC","text":"Command Value Data Description Data 0x00 Raw packet Queue packet for transmission TXDELAY 0x01 Delay (1 byte) Transmitter keyup delay in 10ms units (default: 50 = 500ms) Persistence 0x02 P (1 byte) CSMA persistence parameter 0-255 (default: 63) SlotTime 0x03 Interval (1 byte) CSMA slot interval in 10ms units (default: 10 = 100ms) TXtail 0x04 Delay (1 byte) Post-TX hold time in 10ms units (default: 0) FullDuplex 0x05 Mode (1 byte) 0 = half duplex, nonzero = full duplex (default: 0) SetHardware 0x06 Sub-command + data MeshCore extensions (see below) Return 0xFF - Exit KISS mode (no-op)"},{"location":"kiss_modem_protocol/#tnc-to-host","title":"TNC to Host","text":"Type Value Data Description Data 0x00 Raw packet Received packet from radio

Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes.

"},{"location":"kiss_modem_protocol/#csma-behavior","title":"CSMA Behavior","text":"

The TNC implements p-persistent CSMA for half-duplex operation:

  1. When a packet is queued, monitor carrier detect
  2. When the channel clears, generate a random value 0-255
  3. If the value is less than or equal to P (Persistence), wait TXDELAY then transmit
  4. Otherwise, wait SlotTime and repeat from step 1

In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.

"},{"location":"kiss_modem_protocol/#sethardware-extensions-0x06","title":"SetHardware Extensions (0x06)","text":"

MeshCore-specific functionality uses the standard KISS SetHardware command. The first byte of SetHardware data is a sub-command. Standard KISS clients ignore these frames.

"},{"location":"kiss_modem_protocol/#frame-format_1","title":"Frame Format","text":"
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 FEND \u2502 0x06 \u2502 Sub-command  \u2502 Data (escaped)\u2502 FEND \u2502\n\u2502 0xC0 \u2502      \u2502   1 byte    \u2502   variable   \u2502 0xC0 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"kiss_modem_protocol/#request-sub-commands-host-to-tnc","title":"Request Sub-commands (Host to TNC)","text":"Sub-command Value Data GetIdentity 0x01 - GetRandom 0x02 Length (1 byte, 1-64) VerifySignature 0x03 PubKey (32) + Signature (64) + Data SignData 0x04 Data to sign EncryptData 0x05 Key (32) + Plaintext DecryptData 0x06 Key (32) + MAC (2) + Ciphertext KeyExchange 0x07 Remote PubKey (32) Hash 0x08 Data to hash SetRadio 0x09 Freq (4) + BW (4) + SF (1) + CR (1) SetTxPower 0x0A Power dBm (1) GetRadio 0x0B - GetTxPower 0x0C - GetCurrentRssi 0x0D - IsChannelBusy 0x0E - GetAirtime 0x0F Packet length (1) GetNoiseFloor 0x10 - GetVersion 0x11 - GetStats 0x12 - GetBattery 0x13 - GetMCUTemp 0x14 - GetSensors 0x15 Permissions (1) GetDeviceName 0x16 - Ping 0x17 - Reboot 0x18 - SetSignalReport 0x19 Enable (1): 0x00=disable, nonzero=enable GetSignalReport 0x1A -"},{"location":"kiss_modem_protocol/#response-sub-commands-tnc-to-host","title":"Response Sub-commands (TNC to Host)","text":"

Response codes use the high-bit convention: response = command | 0x80. Generic and unsolicited responses use the 0xF0+ range.

Sub-command Value Data Identity 0x81 PubKey (32) Random 0x82 Random bytes (1-64) Verify 0x83 Result (1): 0x00=invalid, 0x01=valid Signature 0x84 Signature (64) Encrypted 0x85 MAC (2) + Ciphertext Decrypted 0x86 Plaintext SharedSecret 0x87 Shared secret (32) Hash 0x88 SHA-256 hash (32) Radio 0x8B Freq (4) + BW (4) + SF (1) + CR (1) TxPower 0x8C Power dBm (1) CurrentRssi 0x8D RSSI dBm (1, signed) ChannelBusy 0x8E Result (1): 0x00=clear, 0x01=busy Airtime 0x8F Milliseconds (4) NoiseFloor 0x90 dBm (2, signed) Version 0x91 Version (1) + Reserved (1) Stats 0x92 RX (4) + TX (4) + Errors (4) Battery 0x93 Millivolts (2) MCUTemp 0x94 Temperature (2, signed) Sensors 0x95 CayenneLPP payload DeviceName 0x96 Name (variable, UTF-8) Pong 0x97 - SignalReport 0x9A Status (1): 0x00=disabled, 0x01=enabled OK 0xF0 - Error 0xF1 Error code (1) TxDone 0xF8 Result (1): 0x00=failed, 0x01=success RxMeta 0xF9 SNR (1) + RSSI (1)"},{"location":"kiss_modem_protocol/#error-codes","title":"Error Codes","text":"Code Value Description InvalidLength 0x01 Request data too short InvalidParam 0x02 Invalid parameter value NoCallback 0x03 Feature not available MacFailed 0x04 MAC verification failed UnknownCmd 0x05 Unknown sub-command EncryptFailed 0x06 Encryption failed TxBusy 0x07 Transmit busy"},{"location":"kiss_modem_protocol/#unsolicited-events","title":"Unsolicited Events","text":"

The TNC sends these SetHardware frames without a preceding request:

TxDone (0xF8): Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure.

RxMeta (0xF9): Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Enabled by default; can be toggled with SetSignalReport. Standard KISS clients ignore this frame.

"},{"location":"kiss_modem_protocol/#data-formats","title":"Data Formats","text":""},{"location":"kiss_modem_protocol/#radio-parameters-setradio-radio-response","title":"Radio Parameters (SetRadio / Radio response)","text":"

All values little-endian.

Field Size Description Frequency 4 bytes Hz (e.g., 869618000) Bandwidth 4 bytes Hz (e.g., 62500) SF 1 byte Spreading factor (5-12) CR 1 byte Coding rate (5-8)"},{"location":"kiss_modem_protocol/#version-version-response","title":"Version (Version response)","text":"Field Size Description Version 1 byte Firmware version Reserved 1 byte Always 0"},{"location":"kiss_modem_protocol/#encrypted-encrypted-response","title":"Encrypted (Encrypted response)","text":"Field Size Description MAC 2 bytes HMAC-SHA256 truncated to 2 bytes Ciphertext variable AES-128 block-encrypted data with zero padding"},{"location":"kiss_modem_protocol/#airtime-airtime-response","title":"Airtime (Airtime response)","text":"

All values little-endian.

Field Size Description Airtime 4 bytes uint32_t, estimated air time in milliseconds"},{"location":"kiss_modem_protocol/#noise-floor-noisefloor-response","title":"Noise Floor (NoiseFloor response)","text":"

All values little-endian.

Field Size Description Noise floor 2 bytes int16_t, dBm (signed)

The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.

"},{"location":"kiss_modem_protocol/#stats-stats-response","title":"Stats (Stats response)","text":"

All values little-endian.

Field Size Description RX 4 bytes Packets received TX 4 bytes Packets transmitted Errors 4 bytes Receive errors"},{"location":"kiss_modem_protocol/#battery-battery-response","title":"Battery (Battery response)","text":"

All values little-endian.

Field Size Description Millivolts 2 bytes uint16_t, battery voltage in mV"},{"location":"kiss_modem_protocol/#mcu-temperature-mcutemp-response","title":"MCU Temperature (MCUTemp response)","text":"

All values little-endian.

Field Size Description Temperature 2 bytes int16_t, tenths of \u00b0C (e.g., 253 = 25.3\u00b0C)

Returns NoCallback error if the board does not support temperature readings.

"},{"location":"kiss_modem_protocol/#device-name-devicename-response","title":"Device Name (DeviceName response)","text":"Field Size Description Name variable UTF-8 string, no null terminator"},{"location":"kiss_modem_protocol/#reboot","title":"Reboot","text":"

Sends an OK response, flushes serial, then reboots the device. The host should expect the connection to drop.

"},{"location":"kiss_modem_protocol/#sensor-permissions-getsensors","title":"Sensor Permissions (GetSensors)","text":"Bit Value Description 0 0x01 Base (battery) 1 0x02 Location (GPS) 2 0x04 Environment (temp, humidity, pressure)

Use 0x07 for all permissions.

"},{"location":"kiss_modem_protocol/#sensor-data-sensors-response","title":"Sensor Data (Sensors response)","text":"

Data returned in CayenneLPP format. See CayenneLPP documentation for parsing.

"},{"location":"kiss_modem_protocol/#cryptographic-algorithms","title":"Cryptographic Algorithms","text":"Operation Algorithm Identity / Signing / Verification Ed25519 Key Exchange X25519 (ECDH) Encryption AES-128 block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) Hashing SHA-256"},{"location":"kiss_modem_protocol/#notes","title":"Notes","text":""},{"location":"nrf52_power_management/","title":"nRF52 Power Management","text":""},{"location":"nrf52_power_management/#overview","title":"Overview","text":"

The nRF52 Power Management module provides battery protection features to prevent over-discharge, minimise likelihood of brownout and flash corruption conditions existing, and enable safe voltage-based recovery.

"},{"location":"nrf52_power_management/#features","title":"Features","text":""},{"location":"nrf52_power_management/#boot-voltage-protection","title":"Boot Voltage Protection","text":""},{"location":"nrf52_power_management/#voltage-wake-lpcomp-vbus","title":"Voltage Wake (LPCOMP + VBUS)","text":""},{"location":"nrf52_power_management/#early-boot-register-capture","title":"Early Boot Register Capture","text":""},{"location":"nrf52_power_management/#shutdown-reason-tracking","title":"Shutdown Reason Tracking","text":"

Shutdown reason codes (stored in GPREGRET2):

Code Name Description 0x00 NONE Normal boot / no previous shutdown 0x4C LOW_VOLTAGE Runtime low voltage threshold reached 0x55 USER User requested powerOff() 0x42 BOOT_PROTECT Boot voltage protection triggered"},{"location":"nrf52_power_management/#supported-boards","title":"Supported Boards","text":"Board Implemented LPCOMP wake VBUS wake Seeed Studio XIAO nRF52840 (xiao_nrf52) Yes Yes Yes RAK4631 (rak4631) Yes Yes Yes Heltec T114 (heltec_t114) Yes Yes Yes GAT562 Mesh Watch13 Yes Yes Yes Promicro nRF52840 No No No RAK WisMesh Tag No No No Heltec Mesh Solar No No No LilyGo T-Echo / T-Echo Lite No No No SenseCAP Solar Yes Yes Yes WIO Tracker L1 / L1 E-Ink No No No WIO WM1110 No No No Mesh Pocket No No No Nano G2 Ultra No No No ThinkNode M1/M3/M6 No No No T1000-E No No No Ikoka Nano/Stick/Handheld (nRF) No No No Keepteen LT1 No No No Minewsemi ME25LS01 No No No

Notes: - \"Implemented\" reflects Phase 1 (boot lockout + shutdown reason capture). - User power-off on Heltec T114 does not enable LPCOMP wake. - VBUS detection is used to skip boot lockout on external power, and VBUS wake is configured alongside LPCOMP when supported hardware exposes VBUS to the nRF52.

"},{"location":"nrf52_power_management/#technical-details","title":"Technical Details","text":""},{"location":"nrf52_power_management/#architecture","title":"Architecture","text":"

The power management functionality is integrated into the NRF52Board base class in src/helpers/NRF52Board.cpp. Board variants provide hardware-specific configuration via a PowerMgtConfig struct and override initiateShutdown(uint8_t reason) to perform board-specific power-down work and conditionally enable voltage wake (LPCOMP + VBUS).

"},{"location":"nrf52_power_management/#early-boot-capture","title":"Early Boot Capture","text":"

A static constructor with priority 101 in NRF52Board.cpp captures the RESETREAS and GPREGRET2 registers before: - SystemInit() (priority 102) - which clears RESETREAS - Static C++ constructors (default priority 65535)

This ensures we capture the true reset reason before any initialisation code runs.

"},{"location":"nrf52_power_management/#board-implementation","title":"Board Implementation","text":"

To enable power management on a board variant:

  1. Enable in platformio.ini: ini -D NRF52_POWER_MANAGEMENT

  2. Define configuration in variant.h: c #define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV) #define PWRMGT_LPCOMP_AIN 7 // AIN channel for voltage sensing #define PWRMGT_LPCOMP_REFSEL 2 // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)

  3. Implement in board .cpp file: ```cpp #ifdef NRF52_POWER_MANAGEMENT const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK };

void MyBoard::initiateShutdown(uint8_t reason) { // Board-specific shutdown preparation (e.g., disable peripherals) bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT);

 if (enable_lpcomp) {\n   configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);\n }\n\n enterSystemOff(reason);\n

} #endif

void MyBoard::begin() { NRF52Board::begin(); // or NRF52BoardDCDC::begin() // ... board setup ...

#ifdef NRF52_POWER_MANAGEMENT checkBootVoltage(&power_config); #endif } ```

For user-initiated shutdowns, powerOff() remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).

  1. Declare override in board .h file: cpp #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif
"},{"location":"nrf52_power_management/#voltage-wake-configuration","title":"Voltage Wake Configuration","text":"

The LPCOMP (Low Power Comparator) is configured to: - Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31) - Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16) - Detect UP events (voltage rising above threshold) - Use 50mV hysteresis for noise immunity - Wake the device from SYSTEMOFF when triggered

VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever configureVoltageWake() is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).

LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL):

REFSEL Fraction VBAT @ 1M/1M divider (VDD=3.0-3.3) VBAT @ 1.5M/1M divider (VDD=3.0-3.3) 0 1/8 0.75-0.82 V 0.94-1.03 V 1 2/8 1.50-1.65 V 1.88-2.06 V 2 3/8 2.25-2.47 V 2.81-3.09 V 3 4/8 3.00-3.30 V 3.75-4.12 V 4 5/8 3.75-4.12 V 4.69-5.16 V 5 6/8 4.50-4.95 V 5.62-6.19 V 6 7/8 5.25-5.77 V 6.56-7.22 V 7 ARef - - 8 1/16 0.38-0.41 V 0.47-0.52 V 9 3/16 1.12-1.24 V 1.41-1.55 V 10 5/16 1.88-2.06 V 2.34-2.58 V 11 7/16 2.62-2.89 V 3.28-3.61 V 12 9/16 3.38-3.71 V 4.22-4.64 V 13 11/16 4.12-4.54 V 5.16-5.67 V 14 13/16 4.88-5.36 V 6.09-6.70 V 15 15/16 5.62-6.19 V 7.03-7.73 V

Important: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use: VBAT_threshold \u2248 (VDD * fraction) * divider_scale, where divider_scale = (Rtop + Rbottom) / Rbottom (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).

"},{"location":"nrf52_power_management/#softdevice-compatibility","title":"SoftDevice Compatibility","text":"

The power management code checks whether SoftDevice is enabled and uses the appropriate API: - When SD enabled: sd_power_* functions - When SD disabled: Direct register access (NRF_POWER->*)

This ensures compatibility regardless of BLE stack state.

"},{"location":"nrf52_power_management/#cli-commands","title":"CLI Commands","text":"

Power management status can be queried via the CLI:

Command Description get pwrmgt.support Returns \"supported\" or \"unsupported\" get pwrmgt.source Returns current power source - \"battery\" or \"external\" (5V/USB power) get pwrmgt.bootreason Returns reset and shutdown reason strings get pwrmgt.bootmv Returns boot voltage in millivolts

On boards without power management enabled, all commands except get pwrmgt.support return:

ERROR: Power management not supported\n
"},{"location":"nrf52_power_management/#debug-output","title":"Debug Output","text":"

When MESH_DEBUG=1 is enabled, the power management module outputs:

DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)\nDEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)\nDEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)\n
"},{"location":"nrf52_power_management/#phase-2-planned","title":"Phase 2 (Planned)","text":""},{"location":"nrf52_power_management/#references","title":"References","text":""},{"location":"number_allocations/","title":"Number Allocations","text":"

This document lists unique numbers/identifiers used in various MeshCore protocol payloads.

"},{"location":"number_allocations/#group-data-types","title":"Group Data Types","text":"

The PAYLOAD_TYPE_GRP_DATA payloads have a 16-bit data-type field, which identifies which application the packet is for.

To make sure multiple applications can function without interfering with each other, the table below is for reserving various ranges of data-type values. Just modify this table, adding a row, then submit a PR to have it authorised/merged.

NOTE: the range FF00 - FFFF is for use while you're developing, doing POC, and for these you don't need to request to use/allocate.

Once you have a working app/project, you need to be able to demonstrate it exists/works, and THEN request type IDs. So, just use the testing/dev range while developing, then request IDs before you transition to publishing your project.

Data-Type range App name Contact 0000 - 00FF -reserved for internal use- 0100 MeshCore Open zsylvester@monitormx.com \u2014 https://github.com/zjs81/meshcore-open 0110 - 011F Ripple ripple_biz@protonmail.com \u2014 https://buymeacoffee.com/ripplebiz FF00 - FFFF -reserved for testing/dev-

(add rows, inside the range 0100 - FEFF for custom apps)

"},{"location":"packet_format/","title":"Packet Format","text":"

This document describes the MeshCore packet format.

"},{"location":"packet_format/#version-1-packet-format","title":"Version 1 Packet Format","text":"

This is the protocol level packet structure used in MeshCore firmware v1.12.0

[header][transport_codes(optional)][path_length][path][payload]\n
"},{"location":"packet_format/#packet-format_1","title":"Packet Format","text":"Field Size (bytes) Description header 1 Contains routing type, payload type, and payload version transport_codes 4 (optional) 2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*) path_length 1 Encodes path hash size in bits 6-7 and hop count in bits 0-5 path up to 64 (MAX_PATH_SIZE) Stores hop_count * hash_size bytes of path data if applicable payload up to 184 (MAX_PACKET_PAYLOAD) Data for the provided Payload Type

NOTE: see the Payloads documentation for more information about the content of specific payload types.

"},{"location":"packet_format/#header-format","title":"Header Format","text":"

Bit 0 means the lowest bit (1s place)

Bits Mask Field Description 0-1 0x03 Route Type Flood, Direct, etc 2-5 0x3C Payload Type Request, Response, ACK, etc 6-7 0xC0 Payload Version Versioning of the payload format"},{"location":"packet_format/#route-types","title":"Route Types","text":"Value Name Description 0x00 ROUTE_TYPE_TRANSPORT_FLOOD Flood Routing + Transport Codes 0x01 ROUTE_TYPE_FLOOD Flood Routing 0x02 ROUTE_TYPE_DIRECT Direct Routing 0x03 ROUTE_TYPE_TRANSPORT_DIRECT Direct Routing + Transport Codes"},{"location":"packet_format/#path-length-encoding","title":"Path Length Encoding","text":"

path_length is not a raw byte count. It packs both hash size and hop count:

Bits Field Meaning 0-5 Hop Count Number of path hashes (0-63) 6-7 Hash Size Code Stored as hash_size - 1

Hash size codes:

Bits 6-7 Hash Size Notes 0b00 1 byte Legacy / default mode 0b01 2 bytes Supported in current firmware 0b10 3 bytes Supported in current firmware 0b11 4 bytes Reserved / invalid

Examples:

"},{"location":"packet_format/#payload-types","title":"Payload Types","text":"Value Name Description 0x00 PAYLOAD_TYPE_REQ Request (destination/source hashes + MAC) 0x01 PAYLOAD_TYPE_RESPONSE Response to REQ or ANON_REQ 0x02 PAYLOAD_TYPE_TXT_MSG Plain text message 0x03 PAYLOAD_TYPE_ACK Acknowledgment 0x04 PAYLOAD_TYPE_ADVERT Node advertisement 0x05 PAYLOAD_TYPE_GRP_TXT Group text message (unverified) 0x06 PAYLOAD_TYPE_GRP_DATA Group datagram (unverified) 0x07 PAYLOAD_TYPE_ANON_REQ Anonymous request 0x08 PAYLOAD_TYPE_PATH Returned path 0x09 PAYLOAD_TYPE_TRACE Trace a path, collecting SNR for each hop 0x0A PAYLOAD_TYPE_MULTIPART Packet is part of a sequence of packets 0x0B PAYLOAD_TYPE_CONTROL Control packet data (unencrypted) 0x0C reserved reserved 0x0D reserved reserved 0x0E reserved reserved 0x0F PAYLOAD_TYPE_RAW_CUSTOM Custom packet (raw bytes, custom encryption)"},{"location":"packet_format/#payload-versions","title":"Payload Versions","text":"Value Version Description 0x00 1 1-byte src/dest hashes, 2-byte MAC 0x01 2 Future version (e.g., 2-byte hashes, 4-byte MAC) 0x02 3 Future version 0x03 4 Future version"},{"location":"payloads/","title":"Payload Format","text":"

Inside each MeshCore Packet is a payload, identified by the payload type in the packet header. The types of payloads are:

This document defines the structure of each of these payload types.

NOTE: all 16 and 32-bit integer fields are Little Endian.

"},{"location":"payloads/#important-concepts","title":"Important concepts:","text":""},{"location":"payloads/#node-advertisement","title":"Node advertisement","text":"

This kind of payload notifies receivers that a node exists, and gives information about the node

Field Size (bytes) Description public key 32 Ed25519 public key of the node timestamp 4 unix timestamp of advertisement signature 64 Ed25519 signature of public key, timestamp, and app data appdata rest of payload optional, see below

Appdata

Field Size (bytes) Description flags 1 specifies which of the fields are present, see below latitude 4 (optional) decimal latitude multiplied by 1000000, integer longitude 4 (optional) decimal longitude multiplied by 1000000, integer feature 1 2 (optional) reserved for future use feature 2 2 (optional) reserved for future use name rest of appdata name of the node

Appdata Flags

Value Name Description 0x01 is chat node advert is for a chat node 0x02 is repeater advert is for a repeater 0x03 is room server advert is for a room server 0x04 is sensor advert is for a sensor server 0x10 has location appdata contains lat/long information 0x20 has feature 1 Reserved for future use. 0x40 has feature 2 Reserved for future use. 0x80 has name appdata contains a node name"},{"location":"payloads/#acknowledgement","title":"Acknowledgement","text":"

An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the \"extra\" payload (see Returned Path) instead of as a separate acknowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra.

Field Size (bytes) Description checksum 4 CRC checksum of message timestamp, text, and sender pubkey"},{"location":"payloads/#returned-path-request-response-and-plain-text-message","title":"Returned path, request, response, and plain text message","text":"

Returned path, request, response, and plain text messages are all formatted in the same way. See the subsection for more details about the ciphertext's associated plaintext representation.

Field Size (bytes) Description destination hash 1 first byte of destination node public key source hash 1 first byte of source node public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see subsections below for details"},{"location":"payloads/#returned-path","title":"Returned path","text":"

Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message.

Field Size (bytes) Description path length 1 length of next field path see above a list of node hashes (one byte each) extra type 1 extra, bundled payload type, eg., acknowledgement or response. Same values as in Packet Format extra rest of data extra, bundled payload content, follows same format as main content defined by this document"},{"location":"payloads/#request","title":"Request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) request data rest of payload application-defined request payload body

For the common chat/server helpers in BaseChatMesh, the current request type values are:

Value Name Description 0x01 get stats get stats of repeater or room server 0x02 keepalive keep-alive request used for maintained connections"},{"location":"payloads/#get-stats","title":"Get stats","text":"

Gets information about the node, possibly including the following:

"},{"location":"payloads/#get-telemetry-data","title":"Get telemetry data","text":"

Not defined in BaseChatMesh. Sensor- and application-specific request payloads may be implemented by higher-level firmware.

"},{"location":"payloads/#get-telemetry","title":"Get Telemetry","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#get-minmaxave-sensor-nodes","title":"Get Min/Max/Ave (Sensor nodes)","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#get-access-list","title":"Get Access List","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#get-neighbors","title":"Get Neighbors","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#get-owner-info","title":"Get Owner Info","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#response","title":"Response","text":"Field Size (bytes) Description content rest of payload application-defined response body

Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above.

"},{"location":"payloads/#plain-text-message","title":"Plain text message","text":"Field Size (bytes) Description timestamp 4 send time (unix timestamp) txt_type + attempt 1 upper six bits are txt_type (see below), lower two bits are attempt number (0..3) message rest of payload the message content, see next table

txt_type

Value Description Message content 0x00 plain text message the plain text of the message 0x01 CLI command the command text of the message 0x02 signed plain text message first four bytes is sender pubkey prefix, followed by plain text message"},{"location":"payloads/#anonymous-request","title":"Anonymous request","text":"Field Size (bytes) Description destination hash 1 first byte of destination node public key public key 32 sender's Ed25519 public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details"},{"location":"payloads/#room-server-login","title":"Room server login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) sync timestamp 4 sender's \"sync messages SINCE x\" timestamp password rest of message password for room"},{"location":"payloads/#repeatersensor-login","title":"Repeater/Sensor login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) password rest of message password for repeater/sensor"},{"location":"payloads/#repeater-regions-request","title":"Repeater - Regions request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x01 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-owner-info-request","title":"Repeater - Owner info request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x02 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-clock-and-status-request","title":"Repeater - Clock and status request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x03 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#group-text-message","title":"Group text message","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details

The plaintext contained in the ciphertext matches the format described in plain text message. Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be 0x00 because it is a \"plain text message\". The message will be of the form <sender name>: <message body> (eg., user123: I'm on my way).

"},{"location":"payloads/#group-datagram","title":"Group datagram","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted data, see below for details

The data contained in the ciphertext uses the format below:

Field Size (bytes) Description data type 2 Identifier for type of data. (See number_allocations.md) data len 1 byte length of data data rest of payload (depends on data type)"},{"location":"payloads/#control-data","title":"Control data","text":"Field Size (bytes) Description flags 1 upper 4 bits is sub_type data rest of payload typically unencrypted data"},{"location":"payloads/#discover_req-sub_type","title":"DISCOVER_REQ (sub_type)","text":"Field Size (bytes) Description flags 1 0x8 (upper 4 bits), prefix_only (lowest bit) type_filter 1 bit for each ADV_TYPE_* tag 4 randomly generate by sender since 4 (optional) epoch timestamp (0 by default)"},{"location":"payloads/#discover_resp-sub_type","title":"DISCOVER_RESP (sub_type)","text":"Field Size (bytes) Description flags 1 0x9 (upper 4 bits), node_type (lower 4) snr 1 signed, SNR*4 tag 4 reflected back from DISCOVER_REQ pubkey 8 or 32 node's ID (or prefix)"},{"location":"payloads/#custom-packet","title":"Custom packet","text":"

Custom packets have no defined format.

"},{"location":"qr_codes/","title":"QR Codes","text":"

This document provides an overview of QR Code formats that can be used for sharing MeshCore channels and contacts. The formats described below are supported by the MeshCore mobile app.

"},{"location":"qr_codes/#add-channel","title":"Add Channel","text":"

Example URL:

meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72\n

Parameters:

"},{"location":"qr_codes/#add-contact","title":"Add Contact","text":"

Example URL:

meshcore://contact/add?name=Example+Contact&public_key=9cd8fcf22a47333b591d96a2b848b73f457b1bb1a3ea2453a885f9e5787765b1&type=1\n

Parameters:

"},{"location":"stats_binary_frames/","title":"Stats Binary Frame Structures","text":"

Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.

"},{"location":"stats_binary_frames/#command-codes","title":"Command Codes","text":"Command Code Description CMD_GET_STATS 56 Get statistics (2-byte command: code + sub-type)"},{"location":"stats_binary_frames/#stats-sub-types","title":"Stats Sub-Types","text":"

The CMD_GET_STATS command uses a 2-byte frame structure: - Byte 0: CMD_GET_STATS (56) - Byte 1: Stats sub-type: - STATS_TYPE_CORE (0) - Get core device statistics - STATS_TYPE_RADIO (1) - Get radio statistics - STATS_TYPE_PACKETS (2) - Get packet statistics

"},{"location":"stats_binary_frames/#response-codes","title":"Response Codes","text":"Response Code Description RESP_CODE_STATS 24 Statistics response (2-byte response: code + sub-type)"},{"location":"stats_binary_frames/#stats-response-sub-types","title":"Stats Response Sub-Types","text":"

The RESP_CODE_STATS response uses a 2-byte header structure: - Byte 0: RESP_CODE_STATS (24) - Byte 1: Stats sub-type (matches command sub-type): - STATS_TYPE_CORE (0) - Core device statistics response - STATS_TYPE_RADIO (1) - Radio statistics response - STATS_TYPE_PACKETS (2) - Packet statistics response

"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_core-24-0","title":"RESP_CODE_STATS + STATS_TYPE_CORE (24, 0)","text":"

Total Frame Size: 11 bytes

Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always 0x18 (24) - 1 1 uint8_t stats_type Always 0x00 (STATS_TYPE_CORE) - 2 2 uint16_t battery_mv Battery voltage in millivolts 0 - 65,535 4 4 uint32_t uptime_secs Device uptime in seconds 0 - 4,294,967,295 8 2 uint16_t errors Error flags bitmask - 10 1 uint8_t queue_len Outbound packet queue length 0 - 255"},{"location":"stats_binary_frames/#example-structure-cc","title":"Example Structure (C/C++)","text":"
struct StatsCore {\n    uint8_t  response_code;  // 0x18\n    uint8_t  stats_type;     // 0x00 (STATS_TYPE_CORE)\n    uint16_t battery_mv;\n    uint32_t uptime_secs;\n    uint16_t errors;\n    uint8_t  queue_len;\n} __attribute__((packed));\n
"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_radio-24-1","title":"RESP_CODE_STATS + STATS_TYPE_RADIO (24, 1)","text":"

Total Frame Size: 14 bytes

Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always 0x18 (24) - 1 1 uint8_t stats_type Always 0x01 (STATS_TYPE_RADIO) - 2 2 int16_t noise_floor Radio noise floor in dBm -140 to +10 4 1 int8_t last_rssi Last received signal strength in dBm -128 to +127 5 1 int8_t last_snr SNR scaled by 4 Divide by 4.0 for dB 6 4 uint32_t tx_air_secs Cumulative transmit airtime in seconds 0 - 4,294,967,295 10 4 uint32_t rx_air_secs Cumulative receive airtime in seconds 0 - 4,294,967,295"},{"location":"stats_binary_frames/#example-structure-cc_1","title":"Example Structure (C/C++)","text":"
struct StatsRadio {\n    uint8_t  response_code;  // 0x18\n    uint8_t  stats_type;     // 0x01 (STATS_TYPE_RADIO)\n    int16_t  noise_floor;\n    int8_t   last_rssi;\n    int8_t   last_snr;       // Divide by 4.0 to get actual SNR in dB\n    uint32_t tx_air_secs;\n    uint32_t rx_air_secs;\n} __attribute__((packed));\n
"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_packets-24-2","title":"RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)","text":"

Total Frame Size: 26 bytes (legacy) or 30 bytes (includes recv_errors)

Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always 0x18 (24) - 1 1 uint8_t stats_type Always 0x02 (STATS_TYPE_PACKETS) - 2 4 uint32_t recv Total packets received 0 - 4,294,967,295 6 4 uint32_t sent Total packets sent 0 - 4,294,967,295 10 4 uint32_t flood_tx Packets sent via flood routing 0 - 4,294,967,295 14 4 uint32_t direct_tx Packets sent via direct routing 0 - 4,294,967,295 18 4 uint32_t flood_rx Packets received via flood routing 0 - 4,294,967,295 22 4 uint32_t direct_rx Packets received via direct routing 0 - 4,294,967,295 26 4 uint32_t recv_errors Receive/CRC errors (RadioLib); present only in 30-byte frame 0 - 4,294,967,295"},{"location":"stats_binary_frames/#notes","title":"Notes","text":""},{"location":"stats_binary_frames/#example-structure-cc_2","title":"Example Structure (C/C++)","text":"
struct StatsPackets {\n    uint8_t  response_code;  // 0x18\n    uint8_t  stats_type;     // 0x02 (STATS_TYPE_PACKETS)\n    uint32_t recv;\n    uint32_t sent;\n    uint32_t flood_tx;\n    uint32_t direct_tx;\n    uint32_t flood_rx;\n    uint32_t direct_rx;\n    uint32_t recv_errors;    // present when frame size is 30\n} __attribute__((packed));\n
"},{"location":"stats_binary_frames/#command-usage-example-python","title":"Command Usage Example (Python)","text":"
# Send CMD_GET_STATS command\ndef send_get_stats_core(serial_interface):\n    \"\"\"Send command to get core stats\"\"\"\n    cmd = bytes([56, 0])  # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)\n    serial_interface.write(cmd)\n\ndef send_get_stats_radio(serial_interface):\n    \"\"\"Send command to get radio stats\"\"\"\n    cmd = bytes([56, 1])  # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)\n    serial_interface.write(cmd)\n\ndef send_get_stats_packets(serial_interface):\n    \"\"\"Send command to get packet stats\"\"\"\n    cmd = bytes([56, 2])  # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)\n    serial_interface.write(cmd)\n
"},{"location":"stats_binary_frames/#response-parsing-example-python","title":"Response Parsing Example (Python)","text":"
import struct\n\ndef parse_stats_core(frame):\n    \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_CORE frame (11 bytes)\"\"\"\n    response_code, stats_type, battery_mv, uptime_secs, errors, queue_len = \\\n        struct.unpack('<B B H I H B', frame)\n    assert response_code == 24 and stats_type == 0, \"Invalid response type\"\n    return {\n        'battery_mv': battery_mv,\n        'uptime_secs': uptime_secs,\n        'errors': errors,\n        'queue_len': queue_len\n    }\n\ndef parse_stats_radio(frame):\n    \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_RADIO frame (14 bytes)\"\"\"\n    response_code, stats_type, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs = \\\n        struct.unpack('<B B h b b I I', frame)\n    assert response_code == 24 and stats_type == 1, \"Invalid response type\"\n    return {\n        'noise_floor': noise_floor,\n        'last_rssi': last_rssi,\n        'last_snr': last_snr / 4.0,  # Unscale SNR\n        'tx_air_secs': tx_air_secs,\n        'rx_air_secs': rx_air_secs\n    }\n\ndef parse_stats_packets(frame):\n    \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 bytes)\"\"\"\n    assert len(frame) >= 26, \"STATS_TYPE_PACKETS frame too short\"\n    response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \\\n        struct.unpack('<B B I I I I I I', frame[:26])\n    assert response_code == 24 and stats_type == 2, \"Invalid response type\"\n    result = {\n        'recv': recv,\n        'sent': sent,\n        'flood_tx': flood_tx,\n        'direct_tx': direct_tx,\n        'flood_rx': flood_rx,\n        'direct_rx': direct_rx\n    }\n    if len(frame) >= 30:\n        (recv_errors,) = struct.unpack('<I', frame[26:30])\n        result['recv_errors'] = recv_errors\n    return result\n
"},{"location":"stats_binary_frames/#command-usage-example-javascripttypescript","title":"Command Usage Example (JavaScript/TypeScript)","text":"
// Send CMD_GET_STATS command\nconst CMD_GET_STATS = 56;\nconst STATS_TYPE_CORE = 0;\nconst STATS_TYPE_RADIO = 1;\nconst STATS_TYPE_PACKETS = 2;\n\nfunction sendGetStatsCore(serialInterface: SerialPort): void {\n    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_CORE]);\n    serialInterface.write(cmd);\n}\n\nfunction sendGetStatsRadio(serialInterface: SerialPort): void {\n    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_RADIO]);\n    serialInterface.write(cmd);\n}\n\nfunction sendGetStatsPackets(serialInterface: SerialPort): void {\n    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_PACKETS]);\n    serialInterface.write(cmd);\n}\n
"},{"location":"stats_binary_frames/#response-parsing-example-javascripttypescript","title":"Response Parsing Example (JavaScript/TypeScript)","text":"
interface StatsCore {\n    battery_mv: number;\n    uptime_secs: number;\n    errors: number;\n    queue_len: number;\n}\n\ninterface StatsRadio {\n    noise_floor: number;\n    last_rssi: number;\n    last_snr: number;\n    tx_air_secs: number;\n    rx_air_secs: number;\n}\n\ninterface StatsPackets {\n    recv: number;\n    sent: number;\n    flood_tx: number;\n    direct_tx: number;\n    flood_rx: number;\n    direct_rx: number;\n    recv_errors?: number;  // present when frame is 30 bytes\n}\n\nfunction parseStatsCore(buffer: ArrayBuffer): StatsCore {\n    const view = new DataView(buffer);\n    const response_code = view.getUint8(0);\n    const stats_type = view.getUint8(1);\n    if (response_code !== 24 || stats_type !== 0) {\n        throw new Error('Invalid response type');\n    }\n    return {\n        battery_mv: view.getUint16(2, true),\n        uptime_secs: view.getUint32(4, true),\n        errors: view.getUint16(8, true),\n        queue_len: view.getUint8(10)\n    };\n}\n\nfunction parseStatsRadio(buffer: ArrayBuffer): StatsRadio {\n    const view = new DataView(buffer);\n    const response_code = view.getUint8(0);\n    const stats_type = view.getUint8(1);\n    if (response_code !== 24 || stats_type !== 1) {\n        throw new Error('Invalid response type');\n    }\n    return {\n        noise_floor: view.getInt16(2, true),\n        last_rssi: view.getInt8(4),\n        last_snr: view.getInt8(5) / 4.0,  // Unscale SNR\n        tx_air_secs: view.getUint32(6, true),\n        rx_air_secs: view.getUint32(10, true)\n    };\n}\n\nfunction parseStatsPackets(buffer: ArrayBuffer): StatsPackets {\n    const view = new DataView(buffer);\n    if (buffer.byteLength < 26) {\n        throw new Error('STATS_TYPE_PACKETS frame too short');\n    }\n    const response_code = view.getUint8(0);\n    const stats_type = view.getUint8(1);\n    if (response_code !== 24 || stats_type !== 2) {\n        throw new Error('Invalid response type');\n    }\n    const result: StatsPackets = {\n        recv: view.getUint32(2, true),\n        sent: view.getUint32(6, true),\n        flood_tx: view.getUint32(10, true),\n        direct_tx: view.getUint32(14, true),\n        flood_rx: view.getUint32(18, true),\n        direct_rx: view.getUint32(22, true)\n    };\n    if (buffer.byteLength >= 30) {\n        result.recv_errors = view.getUint32(26, true);\n    }\n    return result;\n}\n
"},{"location":"stats_binary_frames/#field-size-considerations","title":"Field Size Considerations","text":""},{"location":"terminal_chat_cli/","title":"Terminal Chat CLI","text":"

Below are the commands you can enter into the Terminal Chat clients:

set freq {frequency}\n

Set the LoRa frequency. Example: set freq 915.8

set tx {tx-power-dbm}\n

Sets LoRa transmit power in dBm.

set name {name}\n

Sets your advertisement name.

set lat {latitude}\n

Sets your advertisement map latitude. (decimal degrees)

set lon {longitude}\n

Sets your advertisement map longitude. (decimal degrees)

set dutycycle {percent}\n

Sets the transmit duty cycle limit (1-100%). Example: set dutycycle 10 for 10%.

set af {air-time-factor}\n

Sets the transmit air-time-factor. Deprecated \u2014 use set dutycycle instead.

time {epoch-secs}\n

Set the device clock using UNIX epoch seconds. Example: time 1738242833

advert\n

Sends an advertisement packet

clock\n

Displays current time per device's clock.

ver\n

Shows the device version and firmware build date.

card\n

Displays your 'business card', for others to manually import

import {card}\n

Imports the given card to your contacts.

list {n}\n

List all contacts by most recent. (optional {n}, is the last n by advertisement date)

to\n

Shows the name of current recipient contact. (for subsequent 'send' commands)

to {name-prefix}\n

Sets the recipient to the first matching contact (in 'list') by the name prefix. (ie. you don't have to type whole name)

send {text}\n

Sends the text message (as DM) to current recipient.

reset path\n

Resets the path to current recipient, for new path discovery.

public {text}\n

Sends the text message to the built-in 'public' group channel

"},{"location":"design/nearby_redesign/","title":"Nearby Nodes \u2014 analiza i propozycja uporz\u0105dkowania","text":"

Branch: refactor/nearby-nodes (zmergowany do main) Plik \u017ar\u00f3d\u0142owy: NearbyScreen.h Status: zaimplementowane \u2014 dokument zachowany jako zapis analizy/decyzji.

Odchylenia od propozycji (stan faktyczny): - Akcji Filter\u2026 w menu nie ma \u2014 duplikowa\u0142a cykl LEFT/RIGHT po typie, wi\u0119c filtr zosta\u0142 wy\u0142\u0105cznie na li\u015bcie (sekcja 3.1 zak\u0142ada\u0142a Filter\u2026 te\u017c w menu). - Sort nie jest togglem przez Enter, lecz zmienia si\u0119 in-place przez LEFT/RIGHT na pod\u015bwietlonym wierszu w popupie (wzorzec ustawie\u0144 Trail), a wiersz pojawia si\u0119 tylko dla \u017ar\u00f3d\u0142a Zapisane (skan nie ma dystansu). - Filtr i sort utrzymuj\u0105 si\u0119 mi\u0119dzy wej\u015bciami na ekran (nie s\u0105 resetowane).

"},{"location":"design/nearby_redesign/#1-jak-narzedzie-jest-zbudowane-dzisiaj","title":"1. Jak narz\u0119dzie jest zbudowane dzisiaj","text":"

Ekran \u0142\u0105czy w sobie trzy osobne tryby o w\u0142asnych listach, widokach szczeg\u00f3\u0142\u00f3w i popupach:

NearbyScreen\n\u251c\u2500\u2500 LIST (kontakty zapisane w mesh)        \u2190 tryb domy\u015blny\n\u2502   \u251c\u2500\u2500 filtr cyklowany LEFT/RIGHT (7 stan\u00f3w)\n\u2502   \u251c\u2500\u2500 DETAIL  (Enter)  \u2192 Lat/Lon/Dist/Type/Seen\n\u2502   \u2502   \u2514\u2500\u2500 _opts popup (Hold Enter): Navigate / Ping / Save waypoint\n\u2502   \u2502       \u2514\u2500\u2500 _ping_menu popup\n\u2502   \u251c\u2500\u2500 NAV view (pe\u0142noekranowa nawigacja)\n\u2502   \u2514\u2500\u2500 _ctx_menu popup (Hold Enter): Discover / Navigate / Save waypoint\n\u2502\n\u2514\u2500\u2500 DISCOVER (skan na \u017cywo NODE_DISCOVER_REQ) \u2190 osobny pod-ekran\n    \u251c\u2500\u2500 lista wynik\u00f3w (karty 2-liniowe)\n    \u2502   Hold Enter = ponowny skan (brak menu!)\n    \u2514\u2500\u2500 DETAIL (Enter) \u2192 pubkey / RSSI / SNR / status\n        \u2514\u2500\u2500 _ping_menu popup (Hold Enter = od razu ping, bez Options)\n

Stan ekranu trzyma 15+ p\u00f3l (_detail, _nav, _discover_mode, _ddetail, _pinging, _filter, dwa komplety _sel/_scroll, trzy bufory wynik\u00f3w ping\u2026) i trzy instancje PopupMenu (_ctx_menu, _opts, _ping_menu).

"},{"location":"design/nearby_redesign/#2-co-konkretnie-jest-nieuporzadkowane","title":"2. Co konkretnie jest nieuporz\u0105dkowane","text":""},{"location":"design/nearby_redesign/#21-jeden-cykl-filtra-miesza-trzy-rozne-osie","title":"2.1 Jeden cykl filtra miesza trzy r\u00f3\u017cne osie","text":"

LEFT/RIGHT przewija 7 stan\u00f3w:

Fav \u00b7 ALL \u00b7 Comp \u00b7 Rpt \u00b7 Room \u00b7 Snsr \u00b7 TIME\n

Ale to nie jest jedna o\u015b \u2014 to trzy wci\u015bni\u0119te w jeden liniowy cykl:

Stan Czym naprawd\u0119 jest Fav filtr ulubionych (flaga ci.flags & 1) ALL/Comp/Rpt/Room/Snsr filtr po typie w\u0119z\u0142a TIME sortowanie (po lastmod zamiast po dystansie)

Konsekwencje: - u\u017cytkownik \u201escrolluje po kategoriach\u201d na \u015blepo \u2014 nie widzi wszystkich naraz, musi cyklowa\u0107, by trafi\u0107 w to, czego szuka; - kombinacje s\u0105 niemo\u017cliwe: nie da si\u0119 zobaczy\u0107 \u201eulubionych repeater\u00f3w posortowanych po dystansie\" ani \u201erepeater\u00f3w posortowanych po czasie\u201d \u2014 model wymusza dok\u0142adnie jeden stan z siedmiu; - TIME jako \u201efiltr\" jest myl\u0105ce \u2014 zmienia kolejno\u015b\u0107, nie zawarto\u015b\u0107; - etykieta w nag\u0142\u00f3wku (NEARBY[TIME]) nie m\u00f3wi, \u017ce to sort.

"},{"location":"design/nearby_redesign/#22-te-same-akcje-rozne-menu-w-zaleznosci-od-miejsca","title":"2.2 Te same akcje, r\u00f3\u017cne menu w zale\u017cno\u015bci od miejsca","text":"Akcja Lista (_ctx_menu) Detail kontaktu (_opts) Detail discover Navigate \u2713 \u2713 \u2014 Save waypoint \u2713 \u2713 \u2014 Ping \u2014 \u2713 \u2713 Discover \u2713 \u2014 \u2014

Ten sam w\u0119ze\u0142 oferuje inny zestaw akcji w zale\u017cno\u015bci od tego, gdzie na niego patrzysz. Ping jest osi\u0105galny tylko ze szczeg\u00f3\u0142\u00f3w, Discover tylko z listy.

"},{"location":"design/nearby_redesign/#23-hold-enter-znaczy-co-innego-na-kazdym-ekranie","title":"2.3 \u201eHold Enter\u201d znaczy co innego na ka\u017cdym ekranie","text":"Ekran Hold Enter (KEY_CONTEXT_MENU) Lista nearby otwiera menu kontekstowe Detail kontaktu otwiera menu Options Lista discover ponowny skan (\u017cadnego menu) Detail discover od razu Ping (pomija Options)

Brak sp\u00f3jnego modelu \u201eprzytrzymaj = menu akcji\u201d.

"},{"location":"design/nearby_redesign/#24-_ping_menu-to-bespoke-widget","title":"2.4 _ping_menu to bespoke widget","text":"

W przeciwie\u0144stwie do reszty popup\u00f3w, ping-menu: - ma wiersze tylko-do-odczytu (RTT / SNR), wi\u0119c po\u0142yka UP/DOWN; - przebudowuje si\u0119 w trakcie (rebuildPingMenu) gdy przychodz\u0105 wyniki; - zostaje otwarte po SELECTED (reszta popup\u00f3w si\u0119 zamyka); - ma w\u0142asny handlePingMenuInput z trybem allow_enter_to_open.

To dzia\u0142a, ale jest to czwarty, niestandardowy wzorzec interakcji w jednym narz\u0119dziu.

"},{"location":"design/nearby_redesign/#25-duplikacja-listdetailping","title":"2.5 Duplikacja list/detail/ping","text":"

renderDiscover/handleInputDiscover/renderDiscoverDetail to niemal r\u00f3wnoleg\u0142a kopia logiki listy i szczeg\u00f3\u0142\u00f3w nearby (osobne _dsel, _dscroll, _d_visible, w\u0142asne rysowanie kart). Discover i Nearby robi\u0105 to samo \u2014 pokazuj\u0105 list\u0119 w\u0119z\u0142\u00f3w z mo\u017cliwo\u015bci\u0105 wej\u015bcia w szczeg\u00f3\u0142y i pingowania \u2014 ale dwoma osobnymi \u015bcie\u017ckami kodu.

"},{"location":"design/nearby_redesign/#3-propozycja-uporzadkowania","title":"3. Propozycja uporz\u0105dkowania","text":"

Trzy zasady przewodnie: (a) jedno menu akcji wsz\u0119dzie, (b) filtr i sortowanie jako osobne, jawne osie, (c) jeden wzorzec listy.

"},{"location":"design/nearby_redesign/#31-rozdziel-filtr-od-sortowania","title":"3.1 Rozdziel filtr od sortowania","text":"

Zamiast jednego cyklu 7-stanowego \u2014 dwie niezale\u017cne osie wybierane z menu (nie przez \u015blepe cyklowanie):

Filtr (o\u015b \u201eco pokazujemy\") i sort (o\u015b \u201ew jakiej kolejno\u015bci\") s\u0105 od siebie niezale\u017cne i \u0142\u0105cz\u0105 si\u0119 dowolnie:

FILTR (jedna o\u015b \u2014 typ w\u0119z\u0142a, z Ulubionymi)      SORT (prze\u0142\u0105cznik)\n  \u2022 Wszystkie                                      \u2022 Dystans (domy\u015blnie)\n  \u2022 Ulubione                                       \u2022 Ostatnio s\u0142yszane\n  \u2022 Companion\n  \u2022 Repeater\n  \u2022 Room\n  \u2022 Sensor\n

Sterowanie (zatwierdzone): - LEFT/RIGHT = szybki cykl tylko po filtrze-typie (jedna sp\u00f3jna o\u015b, znany gest; bez \u201eTIME\" zanieczyszczaj\u0105cego cykl); - Sort = prze\u0142\u0105cznik w menu akcji (Dystans \u2194 Ostatnio s\u0142yszane), trzymany niezale\u017cnie od filtra; - Filter\u2026 dost\u0119pny te\u017c w menu akcji (odkrywalno\u015b\u0107 \u2014 ca\u0142a lista widoczna naraz, nie tylko cyklowanie).

Dzi\u0119ki temu \u201eulubione repeatery po dystansie\" staje si\u0119 mo\u017cliwe, a w nag\u0142\u00f3wku wida\u0107 oba wymiary, np. NEARBY \u00b7 Rpt \u00b7 \u2193dist.

"},{"location":"design/nearby_redesign/#32-jedno-spojne-menu-akcji-hold-enter-wszedzie","title":"3.2 Jedno sp\u00f3jne menu akcji (Hold Enter wsz\u0119dzie)","text":"

Jeden PopupMenu (zamiast _ctx_menu + _opts), pozycje zale\u017cne od kontekstu, ale kolejno\u015b\u0107 i nazwy sta\u0142e:

Hold Enter \u2192  Options\n  \u2022 Navigate        (gdy w\u0119ze\u0142 ma GPS)\n  \u2022 Ping            (gdy znamy pubkey)\n  \u2022 Save waypoint   (gdy w\u0119ze\u0142 ma GPS)\n  \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  \u2022 Filter\u2026         (podmenu z 3.1)\n  \u2022 Sort\u2026           (toggle z 3.1)\n  \u2022 Discover scan   (uruchamia skan na \u017cywo = prze\u0142\u0105cza \u017ar\u00f3d\u0142o)\n

To samo menu na li\u015bcie i w szczeg\u00f3\u0142ach, w obu \u017ar\u00f3d\u0142ach (Zapisane / Skan). Pozycje niedost\u0119pne s\u0105 pomijane (jak ju\u017c teraz robi has_node), ale nigdy nie zmieniaj\u0105 kolejno\u015bci ani nazw. \u201eHold Enter = menu akcji\" \u2014 bez wyj\u0105tk\u00f3w. Ping zawsze przez Options (znika \u201eod razu Ping\" z detalu Discover); rescan to pozycja menu, nie ukryty Hold Enter.

"},{"location":"design/nearby_redesign/#33-jedna-lista-dwa-zroda-zapisane-skan-na-zywo","title":"3.3 Jedna lista, dwa \u017ar\u00f3d\u0142a (Zapisane / Skan na \u017cywo)","text":"

Zatwierdzone: zamiast osobnego pod-ekranu Discover \u2014 jeden komponent listy/szczeg\u00f3\u0142\u00f3w/menu/ping nap\u0119dzany prze\u0142\u0105cznikiem \u017ar\u00f3d\u0142a:

To nie jest dos\u0142owne zlanie dw\u00f3ch list w jedn\u0105 tablic\u0119 (dane maj\u0105 r\u00f3\u017cny kszta\u0142t \u2014 kontakt ma GPS, wynik skanu ma sygna\u0142), tylko jedna \u015bcie\u017cka interakcji nad dwoma \u017ar\u00f3d\u0142ami. Wyb\u00f3r \u017ar\u00f3d\u0142a = \u201eDiscover scan\" w menu (uruchamia NODE_DISCOVER_REQ i prze\u0142\u0105cza widok na wyniki) oraz powr\u00f3t do Zapisanych przez Cancel.

"},{"location":"design/nearby_redesign/#4-proponowany-zakres-etapami","title":"4. Proponowany zakres (etapami)","text":"Etap Zmiana Ryzyko 1 Rozdziel sort od filtra: TIME znika z cyklu; LEFT/RIGHT cykluj\u0105 tylko typ+Fav; sort jako stan + toggle niskie 2 Scal _ctx_menu i _opts w jedno menu akcji o sta\u0142ej kolejno\u015bci; dodaj Filter\u2026/Sort\u2026 niskie 3 Ujednoli\u0107 Hold Enter w Discover (menu zamiast bezpo\u015bredniego rescan/ping; Ping zawsze przez Options) \u015brednie 4 Scal list\u0119 Discover z list\u0105 Nearby w jeden komponent z prze\u0142\u0105cznikiem \u017ar\u00f3d\u0142a wy\u017csze

Kolejno\u015b\u0107 implementacji: 1 \u2192 2 \u2192 3 \u2192 4. Etapy 1\u20132 daj\u0105 najwi\u0119ksz\u0105 popraw\u0119 \u201euporz\u0105dkowania\" przy najmniejszym ryzyku; 3\u20134 domykaj\u0105 sp\u00f3jno\u015b\u0107 (jedna lista, jedno menu wsz\u0119dzie).

"},{"location":"design/nearby_redesign/#5-decyzje-zatwierdzone-2026-06-14","title":"5. Decyzje (zatwierdzone 2026-06-14)","text":"
  1. Filtr i sort \u2014 pe\u0142ne rozdzielenie osi. LEFT/RIGHT = szybki cykl filtra-typu; Sort jako toggle w menu; Filter\u2026 te\u017c w menu dla odkrywalno\u015bci. (Nie chowamy wszystkiego do menu \u2014 zachowujemy szybki gest.)
  2. Discover \u2014 jedna lista, dwa \u017ar\u00f3d\u0142a. Wsp\u00f3lny komponent interakcji; r\u00f3\u017cni si\u0119 tylko prawa kolumna/pola detalu (dystans vs sygna\u0142).
"},{"location":"design/solo_ui_framework/","title":"Solo UI framework \u2014 a guide for adding features","text":"

Go back

This is a developer guide to the reusable building blocks behind the companion_radio solo firmware UI (the ui-new screens). It is not a user manual \u2014 for what each screen does, see solo_features. The goal here is so that adding a new screen or feature means wiring together existing helpers, not reinventing list scrolling, text wrapping, or persistence.

Everything below lives under examples/companion_radio/ unless a path says otherwise. The screen fragments (ui-new/*.h) are all #included, in order, into one translation unit (ui-new/UITask.cpp) \u2014 so a static inline helper in an earlier header is visible to later ones. Header-include order in UITask.cpp therefore matters; new screens go near the others.

Single-TU only. These fragments compile only as part of UITask.cpp. Some define external-linkage symbols at file scope (e.g. NearbyScreen::FILTER_LABELS), so including a fragment from a second .cpp is a duplicate-symbol link error. Anything genuinely shared across TUs must live in a real header (icons.h, GeoUtils.h, DisplayDriver.h), not a screen fragment.

"},{"location":"design/solo_ui_framework/#1-the-screen-model","title":"1. The screen model","text":"

Every screen implements UIScreen (src/helpers/ui/UIScreen.h):

class UIScreen {\npublic:\n  virtual int  render(DisplayDriver& display) = 0;  // returns ms until the next render\n  virtual bool handleInput(char c) { return false; }\n  virtual void poll() { }\n  virtual void onShow() { }                         // reset per-visit state\n};\n
"},{"location":"design/solo_ui_framework/#wiring-a-screen-into-uitask","title":"Wiring a screen into UITask","text":"
  1. Add a UIScreen* my_screen; member in UITask.h (near the others).
  2. Construct it in UITask::begin() (UITask.cpp): my_screen = new MyScreen(this, \u2026);
  3. Add a navigator \u2014 usually just the one line (the cast-free onShow() runs inside setCurrScreen): cpp void UITask::gotoMyScreen() { setCurrScreen(my_screen); } Only screens needing a parameter at entry add a typed call after it (e.g. gotoRingtoneEditor \u2192 selectSlot(slot), gotoMapScreen \u2192 showMapView()).
  4. Reach it from somewhere \u2014 usually a row in ToolsScreen.h (add an Action enum value, a row in the right section table, and a dispatch() case).

That's the whole contract. Steps 1, 3 and 4 are compiler-checked (a mismatch won't link); only a forgotten step 2 can slip through \u2014 every screen pointer is nullptr-initialised in UITask.h and setCurrScreen() bails on null, so a missed new is an inert no-op rather than a null deref.

The constructor takes UITask* task plus whatever it needs (NodePrefs*, KeyboardWidget*, \u2026); the task back-pointer is how a screen calls shared services (_task->showAlert(...), _task->waypoints(), \u2026).

"},{"location":"design/solo_ui_framework/#2-layout-metrics-text-displaydriver","title":"2. Layout metrics & text (DisplayDriver)","text":"

DisplayDriver (src/helpers/ui/DisplayDriver.h) abstracts OLED vs e-ink and, crucially, font scale: landscape e-ink renders text at 2\u00d7, so never hard-code pixel sizes \u2014 derive everything from these:

Call Meaning getLineHeight() pixel rows per text line (8 at 1\u00d7, 16 at 2\u00d7) lineStep() row pitch = line height + gap; use for row y stepping getCharWidth() / getTextWidth(s) advance width; getTextWidth is font-accurate headerH() / listStart() title-bar height / first content row y listVisible(itemH) how many rows fit below the header valCol() conventional x for a right-hand value column width() / height() panel size in px isEink() true only on landscape e-ink; branch on this, not on pixel counts

Drawing helpers (all clip/measure for you):

"},{"location":"design/solo_ui_framework/#3-lists-drawlist","title":"3. Lists \u2014 drawList","text":"

drawList (ui-new/icons.h) is the workhorse for any scrolling list. It computes the visible window from font metrics, keeps sel in view, reserves the scrollbar column, draws each visible row through your callback, and draws the indicator:

drawList(display, count, _sel, _scroll, [&](int idx, int y, bool sel, int reserve) {\n  drawRowSelection(display, y, sel, reserve);   // canonical highlight bar\n  display.drawTextEllipsized(2, y, display.width() - reserve - 4, items[idx].name);\n});\n

reserve is the width the scrollbar took (0 when the list fits) \u2014 subtract it from any right-aligned content so nothing slides under the indicator. The row callback owns its own selection bar: drawRowSelection(d, y, sel, reserve) (ui-new/icons.h) draws the standard one (full row minus reserve, one pixel short); call display.drawSelectionRow() directly only when a row needs a non-standard geometry (full-width, custom height). For the fold-in-place pattern (sections that expand/collapse) use AccordionList instead (ui-new/AccordionList.h) \u2014 same idea, two callbacks (header + item).

Standalone scroll indicators (drawScrollIndicator, \u2026Px) and the reserve calculator (scrollIndicatorReserve) are exposed for hand-laid lists.

"},{"location":"design/solo_ui_framework/#4-reusable-components","title":"4. Reusable components","text":"Component Header Use for PopupMenu PopupMenu.h a modal action menu over any screen AccordionList AccordionList.h collapsible sectioned lists (Tools, Settings) KeyboardWidget KeyboardWidget.h on-screen text entry DigitEditor DigitEditor.h scroll-edit one number, digit by digit FullscreenMsgView FullscreenMsgView.h scrollable full-message reader + word wrap NavView NavView.h bearing/distance/ETA \"navigate to a point\" view

All follow the same shape: a begin(...) to open, an active flag, a handleInput(c) returning a small Result enum, and a render()/draw(). Typical embedding:

if (_menu.active) {                       // popup eats input while open\n  auto r = _menu.handleInput(c);\n  if (r == PopupMenu::SELECTED) runAction(_menu.selectedIndex());\n  return true;\n}\n...\n_menu.begin(\"Options\", 6);                // open it\n_menu.addItem(\"Navigate\"); _menu.addItem(\"Ping\");\n_menu.active = true;\n

KeyboardWidget additionally supports placeholders ({loc}, {time}, sensor tokens) via addPlaceholder() / clearPlaceholders(); the shared kbAddSensorPlaceholders() (ui-new/SensorPlaceholders.h) adds only the tokens the board's sensors actually provide. Expand them with expandMsg() at send time.

FullscreenMsgView::wrapLines() is a standalone pixel-accurate word-wrapper (O(n), variable-width-font aware) reusable by any multi-line layout; it writes into the shared s_wrap_trans / s_wrap_lines scratch (single-threaded render, never held across a yield \u2014 see \u00a79).

"},{"location":"design/solo_ui_framework/#5-domain-helpers","title":"5. Domain helpers","text":"

Geo (GeoUtils.h, namespace geo, all pure/header-inline):

Coordinates are int32 degrees \u00d7 1e6 everywhere (GPS, contacts, trail, prefs). The message tags are LOCATION_MSG_TAG ([LOC], the sender's own live position) and WAYPOINT_MSG_TAG ([WAY], a saved point to share); both stay human-readable on clients that don't know them.

State stores: TrailStore (Trail.h, GPS breadcrumb ring + GPX export), LiveTrackStore (LiveTrack.h, RAM table of others' [LOC] positions, expiring), WaypointStore (Waypoint.h, persisted saved points). Reach them via the task (_task->trail(), _task->liveTrack(), _task->waypoints()).

Message reply prefix: msgReplyBody(text, nick?, n?) (FullscreenMsgView.h) parses a leading @[nick] reply marker, returning the body and optionally the addressee. Use it instead of re-scanning for @[. The stored/sent prefix is raw UTF-8 (it goes over the air) \u2014 never transliterate it.

"},{"location":"design/solo_ui_framework/#6-mini-icons-the-status-bar","title":"6. Mini-icons & the status bar","text":"

Small glyphs are authored as ASCII art and packed at compile time (ui-new/icons.h):

MINI_ICON(ICON_FOO, 5,\n  packRow(\"..#..\"),\n  packRow(\".###.\"),\n  packRow(\"#####\"));\n

Draw with miniIconDraw(display, x, topY, ICON_FOO) (auto-scaled & centered), miniIconDrawTop (exact placement), or the boxed/slot variants (drawBoxedIcon = lit when active, drawSlotIcon = plain). Bigger page glyphs use BIG_ICON / bigIconDraw. The home status bar composes these right-to-left with a blinkOn() cadence for \"leave it on and forget\" broadcasts (auto-advert, Live Share, trail, repeater) \u2014 follow that pattern when adding an indicator: always shown on e-ink, blinking on OLED.

"},{"location":"design/solo_ui_framework/#7-input","title":"7. Input","text":"

Keys arrive as the KEY_* codes in UIScreen.h: KEY_UP/DOWN/LEFT/RIGHT, KEY_ENTER, KEY_CANCEL, KEY_CONTEXT_MENU (the \"Hold Enter\" menu key).

Use the prev/next convention for value changes so the rotary encoder and the D-pad agree: keyIsPrev(c) (LEFT or encoder-prev) and keyIsNext(c) (RIGHT or encoder-next). KEY_CANCEL and KEY_CONTEXT_MENU stay screen-specific. Joystick rotation is handled upstream (rotateJoystickKey) \u2014 screens see already-rotated keys.

"},{"location":"design/solo_ui_framework/#8-persistence","title":"8. Persistence","text":"

Device settings live in one NodePrefs struct (NodePrefs.h), saved via the_mesh.savePrefs() and loaded by DataStore.cpp. Rules when adding a field:

The _dirty convention: a multi-field editor screen mutates _node_prefs live for instant feedback but only persists once, on exit, gated by a _dirty flag \u2014 so LEFT/RIGHT value-cycling doesn't thrash flash. Set _dirty = true at each edit site, then on the exit path call _task->savePrefsIfDirty(_dirty) (UITask) \u2014 it saves once iff dirty and clears the flag, so every screen's save-on-exit reads the same and the \"did we touch flash?\" decision lives in one place. A one-shot action from a popup (no exit hook) calls the_mesh.savePrefs() immediately. Follow whichever matches your screen.

The shared \"active target\" (Locator/Nav destination) is set through UITask::setTarget() (defines it), setTargetNow() (defines + saves + toast), or clearTarget() \u2014 one definition used by the Locator screen, the map, and the Nearby/Waypoints \"Set as target\" actions. Resolve a person's current position with resolvePersonPos() (live [LOC] share, else last-advertised fix).

"},{"location":"design/solo_ui_framework/#9-conventions-gotchas","title":"9. Conventions & gotchas","text":""},{"location":"design/solo_ui_framework/#10-worked-example-a-new-tools-screen","title":"10. Worked example \u2014 a new Tools screen","text":"
// ui-new/MyToolScreen.h  \u2014 included by UITask.cpp near the other screens\n#pragma once\n#include \"icons.h\"          // drawList + mini-icons\n#include \"../NodePrefs.h\"\n\nclass MyToolScreen : public UIScreen {\n  UITask*    _task;\n  NodePrefs* _prefs;\n  int        _sel = 0, _scroll = 0;\n  bool       _dirty = false;\n  static const int ROWS = 3;\npublic:\n  MyToolScreen(UITask* t, NodePrefs* p) : _task(t), _prefs(p) {}\n  void onShow() override { _sel = 0; _scroll = 0; _dirty = false; }\n\n  int render(DisplayDriver& d) override {\n    d.setTextSize(1);\n    d.drawCenteredHeader(\"MY TOOL\");\n    drawList(d, ROWS, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {\n      drawRowSelection(d, y, sel, reserve);\n      d.setCursor(4, y);\n      d.print(i == 0 ? \"Alpha\" : i == 1 ? \"Bravo\" : \"Charlie\");\n    });\n    return 500;\n  }\n\n  bool handleInput(char c) override {\n    if (c == KEY_CANCEL) {\n      _task->savePrefsIfDirty(_dirty);   // saves once iff dirty, then clears\n      _task->gotoToolsScreen();\n      return true;\n    }\n    if (c == KEY_UP)   { _sel = (_sel + ROWS - 1) % ROWS; return true; }\n    if (c == KEY_DOWN) { _sel = (_sel + 1) % ROWS;        return true; }\n    if (keyIsPrev(c) || keyIsNext(c) || c == KEY_ENTER) {\n      /* mutate _prefs\u2026, set _dirty = true */ return true;\n    }\n    return false;\n  }\n};\n

Then: #include \"MyToolScreen.h\" in UITask.cpp, add the member + constructor + gotoMyToolScreen() (\u00a71), and add a row in ToolsScreen.h. Done \u2014 scrolling, the scrollbar, font scaling, e-ink pacing and persistence batching all come from the framework.

"},{"location":"design/trail_redesign/","title":"Trail (Tools \u203a Trail) \u2014 analiza i propozycja uporz\u0105dkowania","text":"

Branch: refactor/trail-screen (zmergowany do main) Plik \u017ar\u00f3d\u0142owy: TrailScreen.h Status: zaimplementowane \u2014 dokument zachowany jako zapis analizy/decyzji. Aktualny opis funkcji od strony u\u017cytkownika: tools_screen.md \u203a GPS Trail.

"},{"location":"design/trail_redesign/#1-jak-ekran-jest-zbudowany-dzisiaj","title":"1. Jak ekran jest zbudowany dzisiaj","text":"
TrailScreen\n\u251c\u2500\u2500 3 widoki (LEFT/RIGHT):  Summary \u00b7 Map \u00b7 List\n\u251c\u2500\u2500 popup akcji  (Hold Enter)  \u2014 JEDNA p\u0142aska lista, do 12 pozycji\n\u2502   \u251c\u2500\u2500 ustawienia (LEFT/RIGHT cykluje w miejscu): Min dist \u00b7 Readout \u00b7 Grid\n\u2502   \u251c\u2500\u2500 toggle:    Start/Stop tracking\n\u2502   \u251c\u2500\u2500 waypointy: Mark here \u00b7 Waypoints \u00b7 Clear waypoints\n\u2502   \u2514\u2500\u2500 trail:     Save \u00b7 Load \u00b7 Export(live) \u00b7 Export(saved) \u00b7 Reset\n\u251c\u2500\u2500 pod-ekrany waypoint\u00f3w (nak\u0142adane na widoki):\n\u2502   \u251c\u2500\u2500 WP_LIST  (lista + dystanse; Trail-start + \u201e+ Add by coords\")\n\u2502   \u251c\u2500\u2500 WP_NAV   (navview)\n\u2502   \u251c\u2500\u2500 WP_ADD   (formularz lat/lon/label)\n\u2502   \u2514\u2500\u2500 _wp_ctx popup: Rename \u00b7 Delete \u00b7 Send\n\u2514\u2500\u2500 KeyboardWidget (label / lat / lon)  \u2014 nak\u0142adka pe\u0142noekranowa\n

Renderowanie mapy: renderMap() (\u2248140 linii) + renderGrid() (\u2248130 linii) + 7 funkcji rysuj\u0105cych markery.

"},{"location":"design/trail_redesign/#2-co-jest-nieuporzadkowane","title":"2. Co jest nieuporz\u0105dkowane","text":""},{"location":"design/trail_redesign/#21-popup-akcji-to-jedna-paska-lista-12-pozycji-z-mieszanymi-rolami","title":"2.1 Popup akcji to jedna p\u0142aska lista 12 pozycji z mieszanymi rolami","text":"

openActionMenu() (TrailScreen.h:363) buduje jedno menu, kt\u00f3re miesza cztery r\u00f3\u017cne klasy pozycji:

Klasa Pozycje Interakcja Ustawienia Min dist, Readout, Grid LEFT/RIGHT (w miejscu) Stan nagrywania Start/Stop tracking Enter Waypointy Mark here, Waypoints, Clear waypoints Enter Plik trasy Save, Load, Export(live), Export(saved), Reset Enter

Problemy: - D\u0142ugi scroll \u2014 na OLED wida\u0107 ~4 wiersze naraz, wi\u0119c do \u201eReset trail\" trzeba przewin\u0105\u0107 przez ca\u0142\u0105 list\u0119; - Dwa wzorce interakcji w jednym menu \u2014 cz\u0119\u015b\u0107 pozycji reaguje na LEFT/RIGHT (ustawienia), cz\u0119\u015b\u0107 na Enter (akcje). Enter na wierszu ustawie\u0144 nic sensownego nie robi \u2014 tylko zamyka i otwiera menu od nowa (reopenAt, TrailScreen.h:581); - Brak kontekstu widoku \u2014 Grid (dotyczy tylko mapy) i Readout (dotyczy tylko Summary) s\u0105 widoczne zawsze, te\u017c tam, gdzie nie maj\u0105 efektu; - Grid ma dwie \u015bcie\u017cki \u2014 i LEFT/RIGHT (:221) i Enter (:234) robi\u0105 to samo; lekko myli.

"},{"location":"design/trail_redesign/#22-rendergrid-dostaje-11-skalarnych-parametrow-brak-wspolnej-projekcji","title":"2.2 renderGrid dostaje 11 skalarnych parametr\u00f3w \u2014 brak wsp\u00f3lnej projekcji","text":"

renderMap liczy projekcj\u0119 (lokalne lambdy projectLL/project, :816), a renderGrid (:876) dostaje 11 osobnych liczb (area_*, min/max_lat, min_lon, lon_scale_geo, scale, off_*) i powtarza t\u0119 sam\u0105 matematyk\u0119 projekcji r\u0119cznie w p\u0119tli (:988, :992). To samo r\u00f3wnanie \u017cyje w trzech miejscach. Ka\u017cda zmiana modelu mapy wymaga edycji w kilku miejscach naraz.

"},{"location":"design/trail_redesign/#23-wybor-kroku-siatki-wielostopniowa-heurystyka-z-nieaktualnym-komentarzem","title":"2.3 Wyb\u00f3r kroku siatki \u2014 wielostopniowa heurystyka z nieaktualnym komentarzem","text":"

renderGrid wybiera krok siatki w czterech nast\u0119puj\u0105cych po sobie korektach (:912\u2013:952): 1. najwi\u0119kszy krok \u2264 target_m, 2. zwi\u0119kszaj a\u017c odst\u0119p pikseli \u2265 MIN_GRID_PX (22 px), 3. zmniejszaj a\u017c zmieszcz\u0105 si\u0119 \u22652 interwa\u0142y, 4. zwi\u0119kszaj a\u017c liczba linii \u2264 MAX_GRID_LINES (40).

Uwagi: - Komentarz przy kroku 4 (:937) m\u00f3wi o \u201estatic buffers (40\u00d740 = ~1600 intersections)\" \u2014 takich bufor\u00f3w ju\u017c nie ma; p\u0119tla rysuje na bie\u017c\u0105co z continue-guardami (:986\u20131002). Cap 40 ogranicza dzi\u015b tylko liczb\u0119 iteracji p\u0119tli (wydajno\u015b\u0107), nie chroni \u017cadnego bufora. Komentarz wprowadza w b\u0142\u0105d. - Kroki 2 i 3 mog\u0105 sobie przeczy\u0107 na bardzo ma\u0142ych ekranach (MIN_GRID_PX = 22 vs shorter_px/2, gdy shorter_px < 44). Nie powoduje b\u0142\u0119du, ale \u201eostateczny\" krok bywa wtedy przypadkowy.

"},{"location":"design/trail_redesign/#24-drobne","title":"2.4 Drobne","text":""},{"location":"design/trail_redesign/#3-propozycja-uporzadkowania","title":"3. Propozycja uporz\u0105dkowania","text":""},{"location":"design/trail_redesign/#31-popup-dwa-poziomy-zamiast-jednej-paskiej-listy","title":"3.1 Popup: dwa poziomy zamiast jednej p\u0142askiej listy","text":"

G\u00f3rne menu kr\u00f3tkie (akcje), ustawienia i operacje na pliku w podmenu:

Hold Enter \u2192  Trail\n  \u2022 Start / Stop tracking\n  \u2022 Mark here\n  \u2022 Waypoints\u2026          \u2192 istniej\u0105cy WP_LIST\n  \u2022 Trail file\u2026         \u2192 Save / Load / Export (live) / Export (saved) / Reset\n  \u2022 Settings\u2026           \u2192 Min dist \u00b7 Readout \u00b7 Grid   (LEFT/RIGHT w miejscu)\n

Korzy\u015bci: - g\u00f3rne menu to ~5 pozycji, bez scrolla na OLED; - jeden wzorzec na poziom: g\u00f3rny i \u201eTrail file\" = Enter-akcje; \u201eSettings\" = warto\u015bci cyklowane LEFT/RIGHT \u2014 bez mieszania w jednym widoku; - destrukcyjny Reset przeniesiony do \u201eTrail file\u2026\", dalej od przypadkowego Entera; - (opcjonalnie) Grid pokazywa\u0107 tylko gdy aktywny jest widok Map, a Readout tylko przy Summary \u2014 menu zale\u017cne od kontekstu widoku.

Wariant minimalny (mniej kodu): zosta\u0107 przy jednej li\u015bcie, ale pogrupowa\u0107 (ustawienia \u2192 akcje \u2192 plik), Reset na sam d\u00f3\u0142, usun\u0105\u0107 podw\u00f3jn\u0105 \u015bcie\u017ck\u0119 Grid. Mniej porz\u0105dku ni\u017c podmenu, ale ta\u0144sze.

"},{"location":"design/trail_redesign/#32-mapa-wspolny-obiekt-projekcji","title":"3.2 Mapa: wsp\u00f3lny obiekt projekcji","text":"

Wydzieli\u0107 ma\u0142y MapProjection liczony raz w renderMap i przekazywany do renderGrid oraz marker\u00f3w:

struct MapProjection {\n  int32_t min_lat, max_lat, min_lon;\n  float   lon_scale_geo, scale;\n  int     off_x, off_y, area_x, area_y, area_w, area_h;\n  void project(int32_t lat, int32_t lon, int& px, int& py) const;\n};\n

Czysto refaktoryzacyjne \u2014 bez zmiany wygl\u0105du mapy.

"},{"location":"design/trail_redesign/#33-siatka-uproszczenie-i-naprawa-komentarza","title":"3.3 Siatka: uproszczenie i naprawa komentarza","text":"

Wynik wizualnie identyczny, logika kr\u00f3tsza i \u0142atwiejsza do utrzymania.

"},{"location":"design/trail_redesign/#4-proponowany-zakres-etapami","title":"4. Proponowany zakres (etapami)","text":"Etap Zmiana Ryzyko 1 Popup \u2192 dwa poziomy (Trail file\u2026, Settings\u2026); Reset g\u0142\u0119biej; usu\u0144 podw\u00f3jny Grid niskie 2 Wydziel MapProjection; renderGrid i markery przez projekcj\u0119 \u015brednie (czysty refactor) 3 Upro\u015b\u0107 wyb\u00f3r kroku siatki; popraw nieaktualne komentarze; sprz\u0105tnij _act_map niskie

Etap 1 = najwi\u0119ksza poprawa \u201euporz\u0105dkowania popupu\" (g\u0142\u00f3wna pro\u015bba). Etapy 2\u20133 = czyszczenie logiki mapy/siatki bez zmiany wygl\u0105du.

"},{"location":"design/trail_redesign/#5-decyzje-zatwierdzone-2026-06-14","title":"5. Decyzje (zatwierdzone 2026-06-14)","text":"
  1. Popup \u2014 pe\u0142ne podmenu. G\u00f3rne menu kr\u00f3tkie; Trail file\u2026 i Settings\u2026 jako podmenu.
  2. Menu zale\u017cne od widoku \u2014 tak. Grid widoczny w Settings tylko na widoku Map, Readout tylko na Summary.
  3. Mapa \u2014 MapProjection + uproszczenie siatki (etap 2 i 3 razem).
  4. Siatka \u2014 kwadratowe oczka, doci\u0105gni\u0119te do kr\u00f3tszego boku. Krok w przestrzeni pikseli (skala izotropiczna). Kr\u00f3tszy bok dzielony na ca\u0142kowit\u0105 liczb\u0119 r\u00f3wnych kwadrat\u00f3w (siatka dotyka tej pary kraw\u0119dzi \u2014 na poziomym OLED: g\u00f3ra/d\u00f3\u0142), na d\u0142u\u017cszym boku mie\u015bci si\u0119 ca\u0142kowita liczba tych samych kwadrat\u00f3w, wy\u015brodkowana. Dzi\u0119ki temu oczka s\u0105 kwadratowe, siatka wpisana w ramk\u0119 i symetryczna (brak jednostronnego przesuni\u0119cia). Etykieta skali = nominalna okr\u0105g\u0142a warto\u015b\u0107. (Kwadraty + linie na wszystkich 4 kraw\u0119dziach s\u0105 geometrycznie niemo\u017cliwe dla dowolnego prostok\u0105ta \u2014 wybrano kwadraty + 2 kraw\u0119dzie + symetria.)
"},{"location":"solo_features/clock_screen/clock_screen/","title":"Clock screen","text":""},{"location":"solo_features/clock_screen/clock_screen/#clock-screen","title":"Clock Screen","text":"

Go back

"},{"location":"solo_features/clock_screen/clock_screen/#overview","title":"Overview","text":"OLED E-Ink

A full-screen clock page on the home screen. Shows the current time and date, with up to three configurable data fields below.

Time is synchronized from GPS or via the companion app. Timezone offset is applied from Settings \u203a System.

If no time source is available, the screen shows \"! No time sync\" with a hint to enable GPS or connect the app.

"},{"location":"solo_features/clock_screen/clock_screen/#time-display","title":"Time display","text":""},{"location":"solo_features/clock_screen/clock_screen/#data-fields","title":"Data fields","text":"OLED E-Ink

Up to three data fields are shown below the date separator. Each field displays a label and a value on the same line.

Field Label Value None \u2014 \u2014 Batt V Batt Battery voltage (e.g. 3.92V) Batt % Batt Battery percentage using LiPo curve anchored at the low-battery threshold Temperature Temp \u00b0C from onboard sensor Humidity Hum % from onboard sensor Pressure Pres hPa from onboard sensor GPS GPS lat lon decimal degrees, or no fix Altitude Alt metres from onboard sensor (GPS or barometric) Luminosity Lux lux from onboard sensor CO\u2082 CO2 ppm from onboard sensor Contacts Nodes Total contacts in the mesh Messages Msgs Total unread message count

Sensor fields show -- when the sensor is not connected or has no data.

"},{"location":"solo_features/clock_screen/clock_screen/#configuring-fields","title":"Configuring fields","text":"OLED E-Ink

Hold Enter (or press the Context menu key) on the Clock page to open the Dashboard Config screen, where each of the three field slots can be cycled with LEFT/RIGHT.

"},{"location":"solo_features/favourites_dial/favourites_dial/","title":"Favourites dial","text":""},{"location":"solo_features/favourites_dial/favourites_dial/#favourites-dial","title":"Favourites Dial","text":"

Go back

"},{"location":"solo_features/favourites_dial/favourites_dial/#overview","title":"Overview","text":"OLED E-Ink

A dedicated home page showing a grid of up to 6 pinned contacts for quick access. The layout adapts to the display orientation:

"},{"location":"solo_features/favourites_dial/favourites_dial/#navigation","title":"Navigation","text":"

Navigate tiles with UP / DOWN / LEFT / RIGHT. Pressing a directional key at the edge of the grid switches to the adjacent home page instead of wrapping.

Enter on a filled tile \u2014 opens that contact's DM directly.

Enter on an empty tile (+) \u2014 opens a contact picker to fill the slot.

"},{"location":"solo_features/favourites_dial/favourites_dial/#unread-badge","title":"Unread badge","text":"

Filled tiles show an unread message count in the top-right corner when there are unread DMs from that contact. The contact name is ellipsized to make room for the badge.

If a pinned contact is removed from the contacts list \u2014 explicitly, or auto-evicted to make room when the table is full \u2014 its slot is freed automatically and goes back to an empty + tile.

"},{"location":"solo_features/favourites_dial/favourites_dial/#pinning-a-contact","title":"Pinning a contact","text":"

From the Favourites Dial \u2014 press Enter on an empty tile (+). A picker opens showing:

  1. Contacts marked as favourites in the upstream app (starred contacts) \u2014 listed first
  2. Recent DM contacts \u2014 listed after
  3. All remaining chat contacts \u2014 fallback when the first two tiers are empty (e.g. fresh install before any DMs)
OLED E-Ink

Select a contact to pin it to that slot.

From a DM conversation \u2014 Hold Enter \u203a context menu \u203a Pin to dial, then choose a slot from the slot picker (Slot 1\u20136, showing the current occupant name or \"empty\").

If the selected contact is already pinned in another slot, it is moved to the new slot automatically.

"},{"location":"solo_features/favourites_dial/favourites_dial/#unpinning-a-contact","title":"Unpinning a contact","text":"

Open the contact's DM, Hold Enter \u203a context menu \u203a Unpin (slot N).

"},{"location":"solo_features/favourites_dial/favourites_dial/#reordering-the-favourites-page","title":"Reordering the Favourites page","text":"

The position of the Favourites Dial in the home page navigation sequence can be changed in Settings \u203a Home Pages \u2014 press LEFT / RIGHT on the Favourites entry to move it earlier or later.

"},{"location":"solo_features/message_screen/message_screen/","title":"Message screen","text":""},{"location":"solo_features/message_screen/message_screen/#messages-screen","title":"Messages Screen","text":"

Go back

"},{"location":"solo_features/message_screen/message_screen/#overview","title":"Overview","text":"OLED E-Ink

The Messages screen is split into three modes \u2014 DMs, Channels, and Rooms \u2014 selectable with UP/DOWN on the mode-select screen. Each mode shows the corresponding list of conversations with unread counters.

"},{"location":"solo_features/message_screen/message_screen/#sending-messages","title":"Sending messages","text":"OLED E-Ink

Press Enter on a contact or channel to open its history, then press Enter again (or select an empty send row) to compose a message. Choose between:

The keyboard supports placeholders that insert live data at send time:

Placeholder Value Availability {time} current time (HH:MM) always {loc} GPS coordinates always (\"no GPS\" if no fix) {temp} temperature sensor connected {hum} humidity sensor connected {pres} barometric pressure sensor connected {alt} altitude sensor connected {lux} luminosity sensor connected {co2} CO\u2082 concentration sensor connected

Sensor placeholders appear automatically in the placeholder picker when the corresponding sensor is active. {time} and {loc} are always shown.

"},{"location":"solo_features/message_screen/message_screen/#rooms-logging-in","title":"Rooms \u2014 logging in","text":"

Posting to a room server requires a login handshake first, so the device can log in on its own \u2014 no phone app needed. The first time you press Enter on a room, a password prompt opens automatically; type the room's password and press the \u2713 key (leave it empty and submit for open / no-password rooms).

The on-screen keyboard is limited to ASCII (letters, digits and common symbols); accented characters such as \u0105/\u0119 can't be typed on the device. A password containing them can still be set from the phone app \u2014 the device stores and replays it byte-for-byte.

"},{"location":"solo_features/message_screen/message_screen/#message-history","title":"Message history","text":"OLED E-Ink

Each entry in the history list shows the sender name and a compact age indicator (3m, 2h, >1d) in the top-right corner.

Short Enter on a message opens it in fullscreen. Hold Enter \u2014 on a history row or in fullscreen \u2014 opens the same options menu: Reply, plus Navigate / Save waypoint when the message contains a location (see Fullscreen message view). You don't need to open the message first.

"},{"location":"solo_features/message_screen/message_screen/#fullscreen-message-view","title":"Fullscreen message view","text":"OLED E-Ink

Navigate between messages with LEFT (newer) and RIGHT (older). Long messages scroll with UP/DOWN.

If the message is a reply addressed to someone (@[nick]), a To: nick bar is shown below the sender name and the body is displayed without the address prefix.

OLED E-Ink

Hold Enter in fullscreen opens the options menu. It always offers Reply for an incoming message, and when the message contains a location it adds two more:

A location is any lat,lon pair in the text \u2014 exactly what the {loc} placeholder inserts \u2014 so you can navigate to anything a contact shares. A [WAY]lat,lon label share also carries a name, used as the waypoint label. This works on DMs and channel messages, incoming or outgoing.

"},{"location":"solo_features/message_screen/message_screen/#context-menu-contact-list","title":"Context menu \u2014 contact list","text":"

Hold Enter on a contact entry opens a context menu:

OLED E-Ink Item Action Mark as read Clears unread counter for this contact Notif: default / OFF / ON Per-contact notification override \u2014 LEFT/RIGHT to cycle Melody: global / M1 / M2 Per-contact melody override \u2014 LEFT/RIGHT to cycle Pin to dial / Unpin (slot N) Pin this contact to a Favourites Dial slot; if already pinned shows which slot

When Pin to dial is selected, a slot picker opens (Slot 1\u20136 showing current occupant name or \"empty\"). Choosing a slot that already holds another contact moves the new contact there.

In the Rooms list the context menu instead offers a single item:

Item Action Login\u2026 Opens the password prompt to (re-)log in to this room (see Rooms \u2014 logging in)"},{"location":"solo_features/message_screen/message_screen/#context-menu-channel-list","title":"Context menu \u2014 channel list","text":"OLED E-Ink

Hold Enter on a channel entry opens a context menu:

Item Action Mark all read Clears all unread for this channel Notif: default / OFF / ON Per-channel notification override \u2014 LEFT/RIGHT to cycle Melody: global / M1 / M2 Per-channel melody override \u2014 LEFT/RIGHT to cycle Fav: yes / no Add or remove this channel from favourites \u2014 LEFT/RIGHT to toggle"},{"location":"solo_features/message_screen/message_screen/#mark-all-read","title":"Mark all read","text":"

Hold Enter on the DM / Channels / Rooms mode-select screen to clear all unread counters for the highlighted category at once.

"},{"location":"solo_features/screen_lock/screen_lock/","title":"Screen lock","text":""},{"location":"solo_features/screen_lock/screen_lock/#screen-lock","title":"Screen Lock","text":"

Go back

"},{"location":"solo_features/screen_lock/screen_lock/#overview","title":"Overview","text":"OLED E-Ink

Screen lock prevents accidental keypresses. While locked the display turns off and all input is ignored.

"},{"location":"solo_features/screen_lock/screen_lock/#locking-and-unlocking","title":"Locking and unlocking","text":"

Hold Back and press Enter three times within 3 seconds. The sequence works in both directions \u2014 the same combination locks and unlocks.

If the display is off when the sequence begins, it turns on automatically so the hint is visible. Each press in the sequence extends the display-on timer by 5 seconds.

The hint popup at the bottom of the lock screen guides through the sequence:

Step Hint Not started Hold Back + 3\u00d7Enter 1 press done Enter \u00d72 more\u2026 2 presses done Enter \u00d71 more\u2026

If no press is made for 3 seconds, the counter resets.

"},{"location":"solo_features/screen_lock/screen_lock/#lock-screen","title":"Lock screen","text":"OLED E-Ink

A brief press of any button wakes the display and shows the lock screen. It displays:

The display turns off again automatically after 5 seconds of inactivity (or 2 seconds immediately after locking).

"},{"location":"solo_features/screen_lock/screen_lock/#auto-lock","title":"Auto-lock","text":"

Enable Auto-lock in Settings \u203a Display to lock the device automatically whenever the display turns off due to auto-off timeout. With auto-lock on, the device is always locked after the screen goes dark \u2014 no manual lock needed.

"},{"location":"solo_features/settings_screen/settings_screen/","title":"Settings screen","text":""},{"location":"solo_features/settings_screen/settings_screen/#settings-screen","title":"Settings Screen","text":"

Go back

"},{"location":"solo_features/settings_screen/settings_screen/#overview","title":"Overview","text":"OLED E-Ink

All settings are saved to flash and restored on next boot. Settings are organised into collapsible sections. Press Enter on a section header to expand or collapse it \u2014 all sections start collapsed for faster navigation. Press LEFT/RIGHT to change a value, or Enter for toggle items.

Press Cancel/Back to save and return to the home screen.

"},{"location":"solo_features/settings_screen/settings_screen/#display","title":"Display","text":"Setting Options Notes Brightness 1\u20135 LEFT/RIGHT; preview applies immediately Auto-off 5 s / 15 s / 30 s / 60 s / never LEFT/RIGHT Auto-lock on / off Locks device when display turns off Battery icon / % / V Display mode for the top-bar battery indicator Clock seconds show / hide Hiding reduces OLED refresh from 1 s to 60 s Clock format 24 h / 12 h 12 h appends AM/PM Font Default / Lemon Default: 5\u00d77 Adafruit with ASCII transliteration; Lemon: native Unicode with pixel-accurate line wrap Display rotation (e-ink only) 0\u00b0 / 90\u00b0 / 180\u00b0 / 270\u00b0 Applied immediately Joystick rotation (e-ink only) 0\u00b0 / 90\u00b0 / 180\u00b0 / 270\u00b0 Rotates input mapping independently of display rotation; useful for custom enclosures Full refresh interval (e-ink only) off / 5 / 10 / 20 / 30 Partial refreshes between full clears; reduces ghosting on long sessions"},{"location":"solo_features/settings_screen/settings_screen/#sound","title":"Sound","text":"Setting Options Notes Buzzer On / Off / Auto Auto: silences while BLE connected, re-enables on disconnect Volume 1\u20135 LEFT/RIGHT; preview tone plays on each change DM Melody built-in / Melody 1 / Melody 2 / None Notification sound for incoming private messages. None disables the sound for this event. Channel Melody built-in / Melody 1 / Melody 2 / None Notification sound for incoming channel messages. None disables the sound for this event. AD sound built-in / Melody 1 / Melody 2 / None Sound played whenever an advert is received from any node \u2014 pairs with Auto-Advert as an audible \"in range\" heartbeat (see Tools \u203a Auto-Advert). None disables the sound for this event. AD scope All / Zero-hop Filters the AD sound so it plays for every advert or only for local zero-hop adverts.

Melody 1 and Melody 2 are custom sequences editable in Tools \u203a Ringtone Editor.

"},{"location":"solo_features/settings_screen/settings_screen/#home-pages","title":"Home Pages","text":"OLED E-Ink

Lists all available home screen pages. For each entry:

Settings and Messages are always visible and cannot be disabled.

"},{"location":"solo_features/settings_screen/settings_screen/#radio","title":"Radio","text":"Setting Options Notes TX Pwr 2\u201322 dBm LEFT/RIGHT. With Auto pwr on this is the ceiling \u2014 the radio may transmit lower. Preset named presets LEFT/RIGHT cycles community RF presets (region frequency + bandwidth/SF/CR). Enter opens a popup to pick one, save the current settings as a named preset, or delete a saved one. Applies frequency, bandwidth, SF and CR together. Freq chip range Enter opens a digit-by-digit editor: LEFT/RIGHT moves between decimal places, UP/DOWN steps that digit. Bounds come from the radio chip's own validated range, so a value the radio would reject can't be entered. SF 5\u201312 LEFT/RIGHT. Spreading factor. BW 7.8\u2013500 kHz LEFT/RIGHT cycles the standard LoRa bandwidths. CR 5\u20138 LEFT/RIGHT. Coding rate (4/5\u20134/8). Pwr save ON / OFF Battery saver. Hardware duty-cycle receive: the SX126x cycles RX\u2194sleep on its own and wakes on a preamble, cutting average RX current. Trades a little receive latency; leave OFF for lowest-latency reception. Requires an SX126x radio (otherwise stays on continuous RX). Forced off (shown as --) while the repeater is on \u2014 a repeater must listen continuously; your setting is restored when the repeater is switched off. Auto pwr ON / OFF Adaptive Power Control. Lowers actual TX power on strong links to save energy, ramping back up \u2014 to the TX Pwr ceiling \u2014 on weak or lost links. Link quality comes from direct-message ACK SNR and, for channel messages (no ACK), from hearing a repeater rebroadcast your packet. The radio page / name bar shows the live power. Default OFF (fixed TX power). Suppressed (shown as --) while the repeater is on \u2014 a repeater holds full TX power for consistent relay reach; your setting is restored when the repeater is switched off. OLED E-Ink

The repeater mode and its flood filters live on their own screen \u2014 see Tools \u203a Repeater.

"},{"location":"solo_features/settings_screen/settings_screen/#system","title":"System","text":"Setting Options Notes Timezone \u221212 h \u2026 +14 h UTC offset in whole hours Low battery off / 3.0 V / 3.1 V / 3.2 V / 3.3 V / 3.4 V / 3.5 V Auto-shutdown threshold; also sets the 0 % anchor for the battery percentage indicator Units Metric / Imperial Global unit system for every distance/speed shown in Tools (Nearby Nodes, Trail, navigate-to-point). Metric: m / km, km/h, min/km. Imperial: ft / mi, mph, min/mi"},{"location":"solo_features/settings_screen/settings_screen/#contacts","title":"Contacts","text":"Setting Options Notes DMs all / favourites Show all chat contacts or only upstream-starred ones Channels all / favourites Show all channels or only favourited ones Rooms all / favourites Show all room servers or only favourited ones"},{"location":"solo_features/settings_screen/settings_screen/#messages","title":"Messages","text":"Setting Options Notes Resend off / 1\u00d7\u20135\u00d7 Auto-resend an on-device direct message this many times when no delivery ACK is received (default 2\u00d7)

Up to 10 quick reply templates (Q1\u2013Q10). Press Enter on a slot to open the keyboard editor. Supports the same placeholders as the main keyboard ({time}, {loc}, and sensor placeholders when connected).

"},{"location":"solo_features/tools_screen/tools_screen/","title":"Tools screen","text":""},{"location":"solo_features/tools_screen/tools_screen/#tools-screen","title":"Tools Screen","text":"

Go back

"},{"location":"solo_features/tools_screen/tools_screen/#overview","title":"Overview","text":"OLED E-Ink

The Tools screen is a hub for GPS trail recording, nearby node browsing, ringtone editing, auto-reply bot, auto-advert, live location sharing, locator, compass, device diagnostics, and repeater mode. Tools are grouped into collapsible Location / Comms / System sections \u2014 the same fold-in-place model as Settings; Tools always opens folded back to the section list. Navigate with UP/DOWN, press Enter on a section header to expand or collapse it, or on a tool to open it.

"},{"location":"solo_features/tools_screen/tools_screen/#nearby-nodes","title":"Nearby Nodes","text":"OLED E-Ink

Browse nodes that have recently advertised on the mesh. Filter (which nodes) and sort (in what order) are independent axes and combine freely.

Filter by category with LEFT/RIGHT (one coherent axis \u2014 type only):

Filter Shows All All known nodes Fav Upstream-starred contacts only Comp Companion (chat) nodes Rpt Repeaters Room Room servers Snsr Sensors

Select a node to see its coordinates, distance, bearing with cardinal direction, type, and last-heard time. A node that is broadcasting its position via Live Share is marked with a \u2666 diamond beside its name in the list (the same marker the map uses), and its detail shows Sharing pos: with the share age and whether it's DM-verified or channel-only.

Hold Enter opens the same Options menu everywhere (list and detail), in a fixed order \u2014 only the actions that apply appear:

Action Available when Navigate selected node has GPS \u2014 for a node sharing live position, the view follows it as it moves and adds an ETA line Ping a public key is known for the node Save waypoint selected node has GPS Set as target selected node has GPS and a public key \u2014 pins it as the active Locator/Nav target right away (see Locator) Sort: Dist/Recent browsing stored nodes \u2014 LEFT/RIGHT on the row flips distance \u2194 last-heard in place Discover scan / Rescan always (live NODE_DISCOVER_REQ scan)

Filtering stays on the list itself (LEFT/RIGHT cycles the type), so there is no separate Filter action in the menu. Sort is adjusted in place: highlight the Sort row and tap LEFT/RIGHT to flip the list (and its right-hand column) between distance and last-heard without closing the menu \u2014 the same in-popup pattern as Trail's settings. The row appears only while browsing stored nodes (live-scan rows carry signal, not distance). Filter and sort are independent and persist across re-entry to the screen.

Selecting Ping opens the Ping popup:

OLED E-Ink

Use Enter on the popup\u2019s Ping row to send a direct mesh ping to that node. The popup then shows the RTT and SNR values on the next lines, and can be used again immediately for another ping.

[!TIP] Combined with Auto-Advert on the other device, Nearby Nodes becomes a passive location tracker \u2014 as long as the tracked device periodically broadcasts its GPS position, you can see its current distance and bearing without any manual interaction on either end.

"},{"location":"solo_features/tools_screen/tools_screen/#active-discovery-live-scan","title":"Active Discovery (live scan)","text":"OLED E-Ink

Options \u2192 Discover scan sends a NODE_DISCOVER_REQ. Repeaters, sensors and room servers within zero-hop range respond immediately with name, type and signal data. This is not a separate screen \u2014 it is the same list switched to a live-scan source: the right-hand column shows RSSI instead of distance, and node detail shows the public key, signal data and contact status.

Because it is the same list, all the same keys apply \u2014 UP/DOWN to navigate, Enter for detail, Hold Enter for the Options menu (where Rescan repeats the scan and Ping works exactly as on stored nodes).

"},{"location":"solo_features/tools_screen/tools_screen/#gps-trail","title":"GPS Trail","text":"OLED E-Ink

Records your route in a RAM ring buffer (up to 512 points, sampled every 1 s). The track is simplified as it's recorded \u2014 a long straight stretch is kept as just its two endpoints while curves keep their detail (bounded to within the Min dist tolerance of the real path), so the buffer covers a far longer route than a flat point budget would suggest. Tracking runs in the background \u2014 a blinking G appears in the status bar. The trail survives display auto-off but is lost on reboot unless saved to flash first.

[!TIP] The Map view is also reachable directly from the home carousel \u2014 the Map page shows a live mini-preview (your position, trail, and tracked contacts) with a north marker and a bottom-left scale tick. The status line below reads Track:N (tracked-node count) and, when you have a fix and at least one tracked contact, an arrow + distance to the nearest one (e.g. Track:3 \u2192120m). If a Locator/Nav target is set it's drawn as a flag marker (see Locator). Press Enter to open the full Trail Map; Hold Enter shares your position (see Live Share); Back returns home.

A GPS fix indicator also sits in the top status bar, alongside the trail/auto-advert/repeater icons \u2014 boxed (lit) once the receiver has a valid fix, a plain glyph while still searching. It only appears on boards with GPS hardware and while GPS is turned on in Settings; it's hidden the rest of the time rather than sitting there empty.

Cycle views with LEFT / RIGHT:

View Content Summary Distance, elapsed time, avg speed or pace, point count, tracking status Map Auto-fit dot-and-line plot with cos(lat) aspect correction; segment breaks marked; north arrow; square scale grid fitted to the map frame (toggle under Hold Enter \u2192 Settings \u2192 Grid, Map view only). Your current GPS position, all waypoints, and any live-tracked contacts (positions shared via Live Share) are always drawn \u2014 even with no trail recording \u2014 so the map is useful standalone. The active Locator/Nav target, if set, is drawn as a flag on top (folded into the frame so it's never off-screen). Point labels are auto-placed to avoid overlapping (a crowded cluster drops some labels rather than smearing them) List Per-point rows showing local time (HH:MM) and delta distance from the previous point; segment-start rows show start; scroll with UP/DOWN OLED E-Ink

Hold Enter opens the action menu. It is two-level \u2014 a short main menu, plus Trail file\u2026 and Settings\u2026 submenus. Cancel/Back in a submenu returns to the main menu.

Main menu:

Item Action Start / Stop tracking Begin or end a recording session. If GPS is off, choosing Start asks \"GPS is off \u2014 Enable GPS & start\" so a session can't silently run with nothing to record Mark here Drop a waypoint at the current GPS fix (see below) Waypoints\u2026 Open the waypoint list / navigation / add-by-coords Share my pos Send your current position as a one-shot [LOC] message \u2014 pick a contact or channel (see Live Share) Trail file\u2026 Open the file submenu (below) Settings\u2026 Open the settings submenu (below)

Trail file\u2026 (only the operations that apply right now appear):

Item Action Save trail Write RAM ring to flash (/trail) Load trail Restore flash trail into RAM Export (live) Stream live RAM trail as GPX 1.1 over USB Serial Export (saved) Stream saved flash trail as GPX 1.1 over USB Serial Reset trail Clear RAM ring and elapsed time

Settings\u2026 (values cycle with LEFT/RIGHT or Enter; shown only where they apply):

Item Available Action Min dist always Sample gate, 4 levels \u2014 metric: 5/10/25/100 m, imperial: 15/30/75/300 ft Auto-pause always Off / 1 / 2 / 5 min \u2014 auto-freeze the trail after a stop, resume on movement (see below) Readout Summary view Summary shows Speed or Pace (in the global unit system) Grid Map view Toggle scale grid on the map

(Trail file\u2026 appears only when a live or saved trail exists. Mark here needs a GPS fix; Waypoints is always available.)

Auto-pause \u2014 when set, a recording trail automatically pauses after the device has stayed within ~15 m of one spot for the chosen delay: the elapsed timer and point sampling both freeze, and the map line breaks across the idle gap. It resumes on its own as soon as you move again. This keeps a stop (a break, a meal, parking) out of your distance and average-speed stats without you having to remember to stop and restart tracking. A paused trail is still \"on\" (the G marker keeps blinking) \u2014 the Summary Status row shows paused. The stop is detected with its own coarse movement gate, independent of Min dist, so GPS jitter while you're parked doesn't keep it awake.

"},{"location":"solo_features/tools_screen/tools_screen/#waypoints","title":"Waypoints","text":"

A waypoint is a saved spot \u2014 your car, camp, a water source \u2014 that you can navigate back to later. Waypoints are independent of the trail: they live in their own flash file (/waypoints), survive a reboot, and are not cleared by Reset trail. Up to 16 can be stored \u2014 the Waypoints list header shows how many are in use (e.g. WAYPOINTS 3/16).

Dropping a waypoint \u2014 Hold Enter \u2192 Mark here. This captures the current GPS fix and opens the on-screen keyboard for a short label (up to 11 characters \u2014 e.g. CAR, CAMP, H2O). Leaving it blank auto-names it WP1, WP2, \u2026 Marking works whether or not the trail is being recorded; it needs a GPS fix (otherwise it reports No GPS fix).

Adding by coordinates \u2014 open Hold Enter \u2192 Waypoints and select the + Add by coords row (always the last entry in the list). This creates a waypoint without being there \u2014 no GPS fix required (handy for a meeting point or a spot read off a map). It opens a small form with three editable rows plus Save:

OLED E-Ink

On the map \u2014 saved waypoints show on the Trail Map view as a hollow diamond with the label's first two characters beside it (enough to tell nearby waypoints apart). Waypoints and your current GPS position are drawn continuously \u2014 even with no trail recording in progress \u2014 so the Map view doubles as a live \"you + your marks\" view, not just a recorded-track plot. With no trail, the view auto-fits to your waypoints and position. While a trail exists, the view frames the recorded route instead, and any waypoint that falls outside it is clamped to the nearest map edge \u2014 a distant mark can't blow up the scale and squash the trail.

Navigating \u2014 Hold Enter \u2192 Waypoints opens the list (each row shows the label and live distance). The list always begins with a synthetic Trail start row whenever a trail exists, so you can backtrack to where you began without having marked it. Select a row and press Enter to open the navigation view:

   CAMP            \u2190 target label\n   1.4 km          \u2190 distance to target\n   To:  145\u00b0 SE    \u2190 absolute bearing to the target\n   Hdg: 090\u00b0 E     \u2190 your current course over ground (-- when stationary)\n
OLED E-Ink

There is no magnetometer, so the screen shows two absolute bearings and you compare them: target at 145\u00b0, travelling at 90\u00b0 \u2192 bear right. The Hdg line is derived from GPS movement (see Compass) and reads -- until you move.

Managing \u2014 Hold Enter on a waypoint row offers Rename / Delete / Send / Set as target (the Trail start row is navigate-only). Set as target pins the waypoint as the active Locator/Nav target in one step (see Locator). Delete removes one at a time; there is no bulk clear.

Sharing \u2014 Send hands the waypoint to the Messages screen: pick a contact or channel, and the message is pre-filled as [WAY]<lat>,<lon> <label> (e.g. [WAY]37.42123,-122.08456 CAR) for you to confirm or edit before sending. On the receiving device, opening that message and Hold Enter \u2192 Navigate / Save waypoint turns it back into a navigable point (see Messages \u203a Fullscreen message view). The format is plain text, so it stays readable on other firmware and the phone app.

"},{"location":"solo_features/tools_screen/tools_screen/#downloading-gpx","title":"Downloading GPX","text":"

Easiest \u2014 Solo GPX Downloader (browser-based, no install):

  1. Open the link in Chrome or Edge (Web Serial API required).
  2. Click Connect device and select the USB serial port.
  3. On the device: Tools \u203a Trail \u2192 Hold Enter \u2192 Export (live) or Export (saved).
  4. The browser captures the stream automatically \u2014 set a filename and click Download.

Script \u2014 tools/trail_export.py (auto-detects the port, captures from <?xml to </gpx>, writes a timestamped file under tools/gpx/):

uv run tools/trail_export.py\n

Then on the device: Tools \u203a Trail \u2192 Hold Enter \u2192 Export (live) or Export (saved).

Manual fallback \u2014 open a serial terminal at 115200 baud and capture the stream by hand:

Saved waypoints are included in the export as GPX <wpt> elements (with their label as <name>), alongside the track \u2014 so they show as pins in OsmAnd, Garmin BaseCamp, GPX Studio, Google Earth, etc. Either way, the resulting file imports into all of those.

[!NOTE] If the companion app is connected via BLE, the export is safe \u2014 BLE and USB operate independently. If connected via USB, disconnect the app before exporting.

"},{"location":"solo_features/tools_screen/tools_screen/#auto-advert","title":"Auto-Advert","text":"

Periodically broadcasts a 0-hop advert with your GPS position. Configurable interval: off / 30 s / 1 min / 2 min / 5 min / 10 min / 30 min / 1 h. A blinking A appears in the status bar while active.

[!TIP] Audible connection heartbeat \u2014 the device chirps each time it receives an advert from any node (sound chosen in Settings \u203a Sound \u203a AD sound). With Auto-Advert running on both ends (e.g. two people on a hike), each hearing the other's periodic advert becomes a hands-free \"in range\" beep \u2014 no need to look at the screen. It fires for every received advert, so in a busy mesh it can get chatty; choose None in Settings \u203a Sound \u203a AD sound to silence just this event, or set Settings \u203a Sound \u203a Advert scope to Zero-hop to limit it to local adverts only. You can also set Settings \u203a Sound \u203a Buzzer to Off (or Auto, which mutes while a companion app is connected) to silence all buzzer output.

"},{"location":"solo_features/tools_screen/tools_screen/#live-share","title":"Live Share","text":"OLED E-Ink

Share your live position over the mesh as ordinary chat messages, and put other people who do the same on your map. A position is sent as a [LOC]<lat>,<lon> message \u2014 the same coordinate format waypoints use, so it stays readable on other firmware and the phone app (it just looks like a coordinate to anything that doesn't know the tag).

This is independent of Auto-Advert and runs alongside it: Auto-Advert announces your presence as a 0-hop beacon for Nearby Nodes, while Live Share sends your position to a specific channel or contact you choose.

The tool holds both directions of sharing in one flat list. Navigate with UP/DOWN, change a value with LEFT/RIGHT (or Enter); Cancel/Back saves and returns to Tools.

Setting Options Notes Track loc ON / OFF Receive incoming [LOC] shares (DM, monitored channels, and room-server posts) and pin those senders on the map / in Nearby. Off by default. Auto share ON / OFF Periodically broadcast your own position to the target below while you move. To channel or contact Enter opens the Messages recipient chooser to pick the target channel or DM contact. Move 50 / 100 / 250 / 500 m Movement gate \u2014 only send after you've moved at least this far since the last share. Min gap 30 s / 1 / 2 / 5 min Minimum time between sends, so fast movement can't flood the channel. Heartbeat Off / 5 / 15 min Optional keep-alive: re-send even while stationary, so the other end knows you're still there.

How auto-share decides to send. With Auto share on, the device checks a few times a minute: it transmits when you've moved at least Move metres and at least Min gap has passed since the last send \u2014 so a stationary device stays silent unless a Heartbeat is set. It also sends once immediately when you enable sharing (or change the target), so the other end gets a fresh fix right away.

Receiving. With Track loc on, incoming [LOC] messages update a small live table (up to 16 nodes, entries expire ~20 min after the last update). DM shares are keyed by the sender's public key (reliable); channel and room-server shares are keyed by name (best-effort, since channel names are unsigned and a room post only carries a short sender prefix). Tracked nodes appear on the Trail Map as a filled diamond with the first two characters of their name, and in Nearby Nodes with their live distance/bearing.

One-shot share. To send your position once without enabling auto-share, use Tools \u203a Trail \u2192 Hold Enter \u2192 Share my pos \u2014 it builds a [LOC] message and hands it to the Messages screen to pick a recipient. There's also a shortcut from the home Map page: Hold Enter sends an immediate position update to your Live Share target while auto-sharing is on (toast Position shared), or opens the recipient picker if it isn't \u2014 so you never broadcast to a default channel by accident.

"},{"location":"solo_features/tools_screen/tools_screen/#locator","title":"Locator","text":"OLED E-Ink

A single geofence that beeps and shows an alert when you cross into or out of a radius. The target can be a saved waypoint (a fixed place \u2014 \"tell me when I'm back at camp\") or a live contact (a person sharing their position via Live Share \u2014 \"alert me when my friend gets near / falls behind\"). A waypoint target is a snapshot (coordinate + label copied), so it keeps working even if you later edit that waypoint; a contact target follows the person's latest shared position. Deleting the target's waypoint, or the target contact being removed from the contacts list, clears the Locator target back to none instead of leaving it pointed at something that's gone.

Navigate with UP/DOWN, change a value with LEFT/RIGHT (or Enter); Cancel/Back saves and returns to Tools.

Setting Options Notes Alert ON / OFF Master switch. Enabling without a target prompts you to pick one. Target none / person / waypoint Enter opens a picker \u2014 None first (clears the target), then favourites (offered even with no known position yet, so you can arm ahead of time), then any other contact with a currently-resolvable position (live-sharing or just last-advertised, e.g. a repeater), then waypoints; UP/DOWN + Enter to choose. LEFT/RIGHT quick-cycles the same set in place, including back to None. A person is shown with an @ prefix, plus a compact age tag (e.g. @Bob (5m)) when the position is last-advertised rather than a live share. Shows none until set. Radius 50 / 100 / 250 / 500 m / 1 km Geofence size. Mode Arrive / Leave / Both Which crossing fires the alert \u2014 entering the radius, leaving it, or both. Beeper ON / OFF Optional homing tone \u2014 shown only in Arrive / Both modes (see below).

Crossing alert. When armed with a target, the device watches its own GPS fix and fires the alert (a short melody plus an on-screen message) the moment you cross the radius, according to Mode. The wording adapts to the target \u2014 Arrived / Left for a waypoint, Near / Away for a person. The edge has a little hysteresis so a fix hovering right on the boundary doesn't chatter, and the first reading after arming only seeds the in/out state \u2014 it won't fire spuriously just because you armed it while already inside.

Following a person. Pick a favourite (or any contact with a known position) as the target and the geofence tracks the distance between you and them, so it works even while both of you move. The position is resolved with a fixed precedence: an active live [LOC] share wins, and with no current share it falls back to the contact's last-advertised GPS position \u2014 so a rarely-updating but stationary node (a repeater, or someone who shared a fix once) still works as a target. You can arm it ahead of time \u2014 choosing a favourite locks onto their identity (pubkey), and the alert starts working as soon as a position is known. Live following requires a DM share (a channel share carries no stable identity to lock onto); the last-advertised fallback works for any contact regardless.

Proximity beeper. With Beeper on, the device also ticks while you're inside the radius and shortens the gap between ticks the closer you get to the target \u2014 slow near the edge, rapid near the centre \u2014 like a homing beeper guiding you to the exact spot. It's silent outside the radius. Because the beeper is its own opt-in toggle, turning it on overrides the global buzzer mute (Settings \u203a Sound \u203a Buzzer) \u2014 it's an explicit \"I want to hear this\". Since homing only makes sense while you're approaching a target, the Beeper row appears only in Arrive or Both mode \u2014 it's hidden in Leave-only mode, and stays silent there even if it was switched on earlier. Otherwise it's independent of the crossing alert (which does follow the mute), so you can use either or both.

Setting the target from anywhere. Besides this screen's picker, the same active target can be set in one step with Set as target from Nearby Nodes' or Waypoints' own Hold Enter menu \u2014 handy so you don't need a detour through Tools. Picking from this screen's picker saves on exit (so LEFT/RIGHT cycling stays cheap); the per-item shortcuts save immediately and confirm with a Target set toast.

On the map. Whatever the active target is \u2014 person or waypoint \u2014 it's drawn as a flag marker on both the home Map preview and the full Trail Map, on top of any waypoint/contact it overlaps and folded into the frame so it never sits off-screen. This shows even when the Alert master switch is off, so a target you set purely to navigate to still appears.

OLED E-Ink OLED E-Ink

[!TIP] Mark the spot first with Tools \u203a Trail \u2192 Hold Enter \u2192 Mark here (or + Add by coords), then set it as the Locator target.

"},{"location":"solo_features/tools_screen/tools_screen/#compass","title":"Compass","text":"OLED E-Ink

A heads-up GPS compass. The L1 has no magnetometer, so the heading is the course over ground \u2014 derived from how your GPS position moves over the last few seconds. The display is a horizontal heading tape: a fixed travel-direction pointer sits at the centre and the N..E..S..W scale scrolls underneath it as you turn, so whatever is under the pointer is your current course. A large numeric readout below shows that course in degrees and cardinal (e.g. 145\u00b0 SE).

Because the heading comes from movement, it only updates while you are actually moving: standing still shows move to set heading (and navigation's Hdg line reads --). Gross GPS jumps are rejected so a single bad fix can't swing the heading. The heading source runs whenever there's a GPS fix \u2014 recording a trail is not required.

"},{"location":"solo_features/tools_screen/tools_screen/#ringtone-editor","title":"Ringtone Editor","text":"OLED E-Ink

A step sequencer for composing custom notification melodies. Two slots \u2014 Melody 1 and Melody 2 \u2014 switchable from within the editor.

Each melody supports up to 32 notes:

Parameter Options Pitch C / D / E / F / G / A / B / pause Octave 4 \u2013 7 Duration 1/4 / 1/8 / 1/16 / 1/32 BPM 60 / 90 / 120 / 150 / 180

Navigation in the editor:

Options menu:

Item Interaction Action Play / Stop Enter Preview the melody Melody 1 / 2 Enter Switch to the other slot Duration LEFT/RIGHT Cycle duration for selected note BPM LEFT/RIGHT Cycle tempo Insert Enter Insert a new note after the cursor Delete Enter Delete the note at cursor Save & Exit Enter Persist the melody and return to Tools Discard Enter Return to Tools without saving

Melodies can be assigned in Settings \u203a Sound (global default) or overridden per contact or channel from the Messages screen context menu.

"},{"location":"solo_features/tools_screen/tools_screen/#auto-reply-bot","title":"Auto-Reply Bot","text":"OLED E-Ink

Automatically replies to incoming messages that contain a configured trigger word (case-insensitive, contains match).

When the bot is enabled it listens to DMs by default. Channel monitoring is an optional addition \u2014 select a channel separately to activate it alongside DM mode.

Setting Description Enable ON / OFF \u2014 enables DM listening Channel OFF, or which channel to additionally monitor (cycles through your channels) Trigger DM Word or phrase that activates the DM reply (case-insensitive). A lone * means reply to every DM (away mode) and is shown as (any msg). Reply DM Reply text for DMs; supports {time}, {loc} and sensor placeholders Trigger Ch Independent trigger for the monitored channel. * means reply to every channel message \u2014 bounded by the per-channel cooldown, but use sparingly on a busy channel. Reply Ch Reply text for channel messages; supports the same placeholders Commands ON / OFF \u2014 answer ! query commands (see below) Quiet from/to Local-time window during which auto-replies stay silent; set both equal (OFF) to disable

The DM and channel triggers are independent, so you can run e.g. an away-message (*) in DMs while the channel reacts only to a specific keyword (or vice-versa).

The header shows a running count of auto-replies sent since boot.

Throttle. Auto-replies are rate-limited per contact (10 s), so a second sender is never starved while one contact is on cooldown. The channel bot keeps a single 10 s cooldown and won't echo a message identical to its own reply (so two bots running the same reply text on one channel can't ping-pong); the cooldown caps any residual back-and-forth.

Quiet hours suppress the push (trigger) replies between the configured local hours; a window where from is later than to wraps past midnight. Commands are a pull (explicitly requested), so they answer even during quiet hours.

"},{"location":"solo_features/tools_screen/tools_screen/#commands","title":"Commands","text":"

With Commands ON, a DM beginning with ! is answered with live node data, independent of the trigger:

Command Reply !ping pong !batt battery voltage !loc GPS coordinates (or no GPS) !time local time HH:MM !temp temperature (or n/a if no sensor) !hops how many hops the command message took to reach the node (direct if heard directly) !status combined battery / location / time !help list of available commands

Several commands can be combined in one message \u2014 !batt !time !hops is answered with a single 4.10V | 14:30 | 3 hops reply (one transmission). A message with no recognised command falls through to the trigger bot.

Commands also work on the monitored channel (the one selected for channel mode) \u2014 anyone there can query the node. Channel replies are broadcast to everyone, so unlike DM commands they respect quiet hours and use the shared per-channel cooldown. DM commands use the per-contact throttle.

"},{"location":"solo_features/tools_screen/tools_screen/#diagnostics","title":"Diagnostics","text":"OLED E-Ink

A single read-only screen of live device and mesh stats, refreshed once a second. On a small OLED the rows scroll with UP/DOWN; on a larger e-ink display they all fit at once.

Row Shows Uptime Time since boot (d hh:mm:ss) Total rx/tx All received / transmitted packets, summed across the categories below Msg Text and group-text packets, rx/tx Advert Advert packets, rx/tx Ack/Path Ack, path-return and trace packets, rx/tx Other Everything else (requests, responses, control, raw, \u2026), rx/tx Forwarded Packets this node actually re-transmitted as a repeater (reflects overhear suppression, if on) Heap free Free / total heap Stack free Current task's minimum-ever stack headroom Noise floor Live radio noise floor (dBm) RSSI/SNR Signal strength / signal-to-noise of the last received packet Pool free Free entries in the packet pool Queue Packets waiting in the outbound queue Errors Radio error flags since boot/reset \u2014 OK, or tokens F (queue full), C (CAD timeout), R (RX-start timeout)

The packet counters, Forwarded and Errors are cumulative since boot. Hold Enter opens a one-item Reset counters menu (Back dismisses it); the live readings (noise, RSSI/SNR, pool, queue, uptime) are not affected. Cancel/Back returns to the Tools list.

The counters make the repeater behaviour observable: Forwarded confirms the node is actually relaying (not just configured to), and Pool free / Queue show whether forwarding is exhausting the packet pool. See Tools \u203a Repeater for the relaying options.

"},{"location":"solo_features/tools_screen/tools_screen/#repeater","title":"Repeater","text":"OLED E-Ink

Turns the companion into a packet repeater while it keeps working as a normal companion \u2014 no separate firmware. By default, enabling it switches the radio to a dedicated repeater profile rather than relaying on whatever network you're chatting on (see Network below) \u2014 that matches the MeshCore community norm of repeaters sitting on a standard channel, not a private one. Loop-detection and an advert flood-depth cap are always applied. This screen keeps the toggle, the network/profile, and its flood-filter options together; live forwarding stats are on Tools \u203a Diagnostics.

Navigate with UP/DOWN; change a value with LEFT/RIGHT (or Enter for toggles). Cancel/Back saves and returns to Tools.

Setting Options Notes Repeater ON / OFF Master switch. The options below appear only while it is ON. Network Current / Custom Custom (default): enabling the repeater switches the radio to a dedicated profile (below) and disabling restores the companion's settings \u2014 so you can drop onto a separate repeater network and come back. A never-configured device seeds Custom with a frequency in the same band as your own network (433/868/915 MHz region), not a flat one-size-fits-all default \u2014 so it can't land outside what's legal for your region. Switching to Custom afterwards (if it was OFF and unconfigured) seeds it from your current settings instead. Current: relay on the companion's own frequency \u2014 opt-in; not the community norm. Rpt preset named presets (Custom only) Enter picks a community/saved preset for the repeater profile. Rpt freq chip range (Custom only) Enter opens the digit-by-digit editor (chip-validated bounds). Rpt SF / BW / CR 5\u201312 / 7.8\u2013500 kHz / 5\u20138 (Custom only) LEFT/RIGHT to adjust the profile's spreading factor, bandwidth, coding rate. Skip advert ON / OFF Don't re-flood advert packets (the highest-volume flood traffic); messages and acks still relay. Max hops OFF / 1\u20138 Drop a flood packet once it has already travelled this many hops. Yield OFF / x2\u2013x9 Scales the retransmit delay for forwarded floods only (your own sends are unaffected), so a mobile companion defers to better-sited fixed repeaters. Widens the window for Suppress dup. Min SNR OFF / \u221220\u202610 dB Drop a flood copy received below this signal-to-noise threshold, so marginal fringe traffic isn't re-flooded. Suppress dup ON / OFF If the same flood packet is overheard from another node while still queued to retransmit, cancel our copy \u2014 a peer already relayed it. Cuts redundant airtime in dense meshes; pairs with Yield.

The five flood filters are opt-in (default OFF, so a plain repeater is unaffected) and act on flood traffic only \u2014 on a direct route this node is the named next hop, so it never drops those.

Same network vs. separate network. With Network = Current (or a Custom profile set equal to your companion settings) the repeater stays on your own network \u2014 you keep messaging while relaying. With a different Custom profile the device moves entirely onto that network while relaying (a single radio can't be on two at once) and returns to your companion network when the repeater is switched off. The profile also re-applies after a reboot if the repeater was left on.

While the repeater is on, a \u00bb indicator appears in the status bar (same blink convention as the auto-advert and trail markers) so you can tell it's relaying at a glance. Two radio settings are also overridden while relaying and restored afterwards: Settings \u203a Radio \u203a Pwr save is forced off (a repeater must listen continuously) and Auto pwr is forced off (a repeater holds full TX power for consistent relay reach). Both show -- in Settings while the repeater is on.

Live forwarding stats \u2014 Forwarded, Pool free, Queue \u2014 are shown on Tools \u203a Diagnostics (this screen is config-only).

"}]} \ No newline at end of file +{"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":"

Welcome to the MeshCore documentation.

Below are a few quick start guides.

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.

"},{"location":"cli_commands/","title":"CLI Commands","text":"

This document provides an overview of CLI commands that can be sent to MeshCore Repeaters, Room Servers and Sensors.

"},{"location":"cli_commands/#navigation","title":"Navigation","text":""},{"location":"cli_commands/#operational","title":"Operational","text":""},{"location":"cli_commands/#reboot-the-node","title":"Reboot the node","text":"

Usage: - reboot

Note: No reply is sent.

"},{"location":"cli_commands/#power-off-the-node","title":"Power-off the node","text":"

Usage: - poweroff, or - shutdown

Note: No reply is sent.

"},{"location":"cli_commands/#reset-the-clock-and-reboot","title":"Reset the clock and reboot","text":"

Usage: - clkreboot

Note: No reply is sent.

"},{"location":"cli_commands/#sync-the-clock-with-the-remote-device","title":"Sync the clock with the remote device","text":"

Usage: - clock sync

"},{"location":"cli_commands/#display-current-time-in-utc","title":"Display current time in UTC","text":"

Usage: - clock

"},{"location":"cli_commands/#set-the-time-to-a-specific-timestamp","title":"Set the time to a specific timestamp","text":"

Usage: - time <epoch_seconds>

Parameters: - epoch_seconds: Unix epoch time

"},{"location":"cli_commands/#send-a-flood-advert","title":"Send a flood advert","text":"

Usage: - advert

"},{"location":"cli_commands/#send-a-zero-hop-advert","title":"Send a zero-hop advert","text":"

Usage: - advert.zerohop

"},{"location":"cli_commands/#start-an-over-the-air-ota-firmware-update","title":"Start an Over-The-Air (OTA) firmware update","text":"

Usage: - start ota

"},{"location":"cli_commands/#erasefactory-reset","title":"Erase/Factory Reset","text":"

Usage: - erase

Serial Only: Yes

Warning: This is destructive!

"},{"location":"cli_commands/#neighbors-repeater-only","title":"Neighbors (Repeater Only)","text":""},{"location":"cli_commands/#list-nearby-neighbors","title":"List nearby neighbors","text":"

Usage: - neighbors

Note: The output of this command is limited to the 8 most recent adverts.

Note: Each line is encoded as {pubkey-prefix}:{timestamp}:{snr*4}

"},{"location":"cli_commands/#remove-a-neighbor","title":"Remove a neighbor","text":"

Usage: - neighbor.remove <pubkey_prefix>

Parameters: - pubkey_prefix: The public key of the node to remove from the neighbors list. This can be a short prefix or the full key. All neighbors matching the provided prefix will be removed.

Note: You can remove all neighbors by sending a space character as the prefix. The space indicates an empty prefix, which matches all existing neighbors.

"},{"location":"cli_commands/#discover-zero-hop-neighbors","title":"Discover zero hop neighbors","text":"

Usage: - discover.neighbors

"},{"location":"cli_commands/#statistics","title":"Statistics","text":""},{"location":"cli_commands/#clear-stats","title":"Clear Stats","text":"

Usage: clear stats

"},{"location":"cli_commands/#system-stats-battery-uptime-queue-length-and-debug-flags","title":"System Stats - Battery, Uptime, Queue Length and Debug Flags","text":"

Usage: - stats-core

Serial Only: Yes

"},{"location":"cli_commands/#radio-stats-noise-floor-last-rssisnr-airtime-receive-errors","title":"Radio Stats - Noise floor, Last RSSI/SNR, Airtime, Receive errors","text":"

Usage: stats-radio

Serial Only: Yes

"},{"location":"cli_commands/#packet-stats-packet-counters-received-sent","title":"Packet stats - Packet counters: Received, Sent","text":"

Usage: stats-packets

Serial Only: Yes

"},{"location":"cli_commands/#logging","title":"Logging","text":""},{"location":"cli_commands/#begin-capture-of-rx-log-to-node-storage","title":"Begin capture of rx log to node storage","text":"

Usage: log start

"},{"location":"cli_commands/#end-capture-of-rx-log-to-node-storage","title":"End capture of rx log to node storage","text":"

Usage: log stop

"},{"location":"cli_commands/#erase-captured-log","title":"Erase captured log","text":"

Usage: log erase

"},{"location":"cli_commands/#print-the-captured-log-to-the-serial-terminal","title":"Print the captured log to the serial terminal","text":"

Usage: log

Serial Only: Yes

"},{"location":"cli_commands/#info","title":"Info","text":""},{"location":"cli_commands/#get-the-version","title":"Get the Version","text":"

Usage: ver

"},{"location":"cli_commands/#show-the-hardware-name","title":"Show the hardware name","text":"

Usage: board

"},{"location":"cli_commands/#configuration","title":"Configuration","text":""},{"location":"cli_commands/#radio","title":"Radio","text":""},{"location":"cli_commands/#view-or-change-this-nodes-radio-parameters","title":"View or change this node's radio parameters","text":"

Usage: - get radio - set radio <freq>,<bw>,<sf>,<cr>

Parameters: - freq: Frequency in MHz - bw: Bandwidth in kHz - sf: Spreading factor (5-12) - cr: Coding rate (5-8)

Set by build flag: LORA_FREQ, LORA_BW, LORA_SF, LORA_CR

Default: 869.525,250,11,5

Note: Requires reboot to apply

"},{"location":"cli_commands/#view-or-change-this-nodes-transmit-power","title":"View or change this node's transmit power","text":"

Usage: - get tx - set tx <dbm>

Parameters: - dbm: Power level in dBm (1-22)

Set by build flag: LORA_TX_POWER

Default: Varies by board

Notes: This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Refer to the node's manual for the correct setting to use. Setting a value too high may violate the laws in your country.

"},{"location":"cli_commands/#change-the-radio-parameters-for-a-set-duration","title":"Change the radio parameters for a set duration","text":"

Usage: - tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>

Parameters: - freq: Frequency in MHz (300-2500) - bw: Bandwidth in kHz (7.8-500) - sf: Spreading factor (5-12) - cr: Coding rate (5-8) - timeout_mins: Duration in minutes (must be > 0)

Note: This is not saved to preferences and will clear on reboot

"},{"location":"cli_commands/#view-or-change-this-nodes-frequency","title":"View or change this node's frequency","text":"

Usage: - get freq - set freq <frequency>

Parameters: - frequency: Frequency in MHz

Default: 869.525

Note: Requires reboot to apply Serial Only: set freq <frequency>

"},{"location":"cli_commands/#view-or-change-this-nodes-rx-boosted-gain-mode-sx12xx-and-lr1110-v1141","title":"View or change this node's rx boosted gain mode (SX12xx and LR1110, v1.14.1+)","text":"

Usage: - get radio.rxgain - set radio.rxgain <state>

Parameters: - state: on|off

Default: on

Temporary Note: If you upgraded from an older version to 1.14.1 without erasing flash, this setting is off because of #2118

"},{"location":"cli_commands/#system","title":"System","text":""},{"location":"cli_commands/#view-or-change-this-nodes-name","title":"View or change this node's name","text":"

Usage: - get name - set name <name>

Parameters: - name: Node name

Set by build flag: ADVERT_NAME

Default: Varies by board

Note: Max length varies. If a location is set, the max length is 24 bytes; 32 otherwise. Emoji and unicode characters may take more than one byte.

"},{"location":"cli_commands/#view-or-change-this-nodes-latitude","title":"View or change this node's latitude","text":"

Usage: - get lat - set lat <degrees>

Set by build flag: ADVERT_LAT

Default: 0

Parameters: - degrees: Latitude in degrees

"},{"location":"cli_commands/#view-or-change-this-nodes-longitude","title":"View or change this node's longitude","text":"

Usage: - get lon - set lon <degrees>

Set by build flag: ADVERT_LON

Default: 0

Parameters: - degrees: Longitude in degrees

"},{"location":"cli_commands/#view-or-change-this-nodes-identity-private-key","title":"View or change this node's identity (Private Key)","text":"

Usage: - get prv.key - set prv.key <private_key>

Parameters: - private_key: Private key in hex format (64 hex characters)

Serial Only: - get prv.key: Yes - set prv.key: No

Note: Requires reboot to take effect after setting

"},{"location":"cli_commands/#change-this-nodes-admin-password","title":"Change this node's admin password","text":"

Usage: - password <new_password>

Parameters: - new_password: New admin password

Set by build flag: ADMIN_PASSWORD

Default: password

Note: Command reply echoes the updated password for confirmation.

Note: Any node using this password will be added to the admin ACL list.

"},{"location":"cli_commands/#view-or-change-this-nodes-guest-password","title":"View or change this node's guest password","text":"

Usage: - get guest.password - set guest.password <password>

Parameters: - password: Guest password

Set by build flag: ROOM_PASSWORD (Room Server only)

Default: <blank>

"},{"location":"cli_commands/#view-or-change-this-nodes-owner-info","title":"View or change this node's owner info","text":"

Usage: - get owner.info - set owner.info <text>

Parameters: - text: Owner information text

Default: <blank>

Note: | characters are translated to newlines

Note: Requires firmware 1.12+

"},{"location":"cli_commands/#fine-tune-the-battery-reading","title":"Fine-tune the battery reading","text":"

Usage: - get adc.multiplier - set adc.multiplier <value>

Parameters: - value: ADC multiplier (0.0-10.0)

Default: 0.0 (value defined by board)

Note: Returns \"Error: unsupported by this board\" if hardware doesn't support it

"},{"location":"cli_commands/#view-this-nodes-public-key","title":"View this node's public key","text":"

Usage: get public.key

"},{"location":"cli_commands/#view-this-nodes-firmware-version","title":"View this node's firmware version","text":"

Usage: ver

"},{"location":"cli_commands/#view-this-nodes-configured-role","title":"View this node's configured role","text":"

Usage: get role

"},{"location":"cli_commands/#view-or-change-this-nodes-power-saving-flag-repeater-only","title":"View or change this node's power saving flag (Repeater Only)","text":"

Usage: - powersaving - powersaving on - powersaving off

Parameters: - on: enable power saving - off: disable power saving

Default: off

Note: When enabled, device enters sleep mode between radio transmissions

"},{"location":"cli_commands/#routing","title":"Routing","text":""},{"location":"cli_commands/#view-or-change-this-nodes-repeat-flag","title":"View or change this node's repeat flag","text":"

Usage: - get repeat - set repeat <state>

Parameters: - state: on|off

Default: on

"},{"location":"cli_commands/#view-or-change-this-nodes-advert-path-hash-size","title":"View or change this node's advert path hash size","text":"

Usage: - get path.hash.mode - set path.hash.mode <value>

Parameters: - value: Path hash size (0-2) - 0: 1 Byte hash size (256 unique ids)[64 max flood] - 1: 2 Byte hash size (65,536 unique ids)[32 max flood] - 2: 3 Byte hash size (16,777,216 unique ids)[21 max flood] - 3: DO NOT USE (Reserved)

Default: 0

Note: the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14

Temporary Note: adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propagation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are supported. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes.

"},{"location":"cli_commands/#view-or-change-this-nodes-loop-detection","title":"View or change this node's loop detection","text":"

Usage: - get loop.detect - set loop.detect <state>

Parameters: - state: - off: no loop detection is performed - minimal: packets are dropped if repeater's ID/hash appears 4 or more times (1-byte), 2 or more (2-byte), 1 or more (3-byte) - moderate: packets are dropped if repeater's ID/hash appears 2 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte) - strict: packets are dropped if repeater's ID/hash appears 1 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte)

Default: off

Note: When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (probably some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14

Example: If preference is loop.detect minimal, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet. If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects.

"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-flood-traffic","title":"View or change the retransmit delay factor for flood traffic","text":"

Usage: - get txdelay - set txdelay <value>

Parameters: - value: Transmit delay factor (0-2)

Default: 0.5

Note: When multiple nearby repeaters all hear the same flood packet, each waits a random amount of time before retransmitting to avoid simultaneous collisions. This factor scales the size of that random window. Higher values reduce collision risk at the cost of added latency. 0 disables the window entirely.

"},{"location":"cli_commands/#view-or-change-the-retransmit-delay-factor-for-direct-traffic","title":"View or change the retransmit delay factor for direct traffic","text":"

Usage: - get direct.txdelay - set direct.txdelay <value>

Parameters: - value: Direct transmit delay factor (0-2)

Default: 0.2

Note: Same collision-avoidance random window as txdelay, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them.

"},{"location":"cli_commands/#experimental-view-or-change-the-processing-delay-for-received-traffic","title":"[Experimental] View or change the processing delay for received traffic","text":"

Usage: - get rxdelay - set rxdelay <value>

Parameters: - value: Receive delay base (0-20)

Default: 0.0

Note: When enabled, repeaters that received a flood packet with a weak signal are held in a delay queue before processing, while those that received it with a strong signal process it immediately. This gives strong-signal paths forwarding priority. By the time weak-signal nodes process their copy, the packet may have already propagated and will be suppressed as a duplicate, reducing redundant retransmissions.

"},{"location":"cli_commands/#view-or-change-the-duty-cycle-limit","title":"View or change the duty cycle limit","text":"

Usage: - get dutycycle - set dutycycle <value>

Parameters: - value: Duty cycle percentage (1-100)

Default: 50% (equivalent to airtime factor 1.0)

Examples: - set dutycycle 100 \u2014 no duty cycle limit - set dutycycle 50 \u2014 50% duty cycle (default) - set dutycycle 10 \u2014 10% duty cycle - set dutycycle 1 \u2014 1% duty cycle (strictest EU requirement)

Note: Added in firmware v1.15.0

"},{"location":"cli_commands/#view-or-change-the-airtime-factor-duty-cycle-limit","title":"View or change the airtime factor (duty cycle limit)","text":"

Deprecated as of firmware v1.15.0. Use get/set dutycycle instead.

Usage: - get af - set af <value>

Parameters: - value: Airtime factor (0-9). After each transmission, the repeater enforces a silent period of approximately the on-air transmission time multiplied by the value. This results in a long-term duty cycle of roughly 1 divided by (1 plus the value). For example: - af = 1 \u2192 ~50% duty - af = 2 \u2192 ~33% duty - af = 3 \u2192 ~25% duty - af = 9 \u2192 ~10% duty You are responsible for choosing a value that is appropriate for your jurisdiction and channel plan (for example EU 868 Mhz 10% duty cycle regulation).

Default: 1.0

"},{"location":"cli_commands/#view-or-change-the-local-interference-threshold","title":"View or change the local interference threshold","text":"

Usage: - get int.thresh - set int.thresh <value>

Parameters: - value: Interference threshold value

Default: 0.0

"},{"location":"cli_commands/#view-or-change-the-agc-reset-interval","title":"View or change the AGC Reset Interval","text":"

Usage: - get agc.reset.interval - set agc.reset.interval <value>

Parameters: - value: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable.

Default: 0.0

"},{"location":"cli_commands/#enable-or-disable-multi-acks-support","title":"Enable or disable Multi-Acks support","text":"

Usage: - get multi.acks - set multi.acks <state>

Parameters: - state: 0 (disable) or 1 (enable)

Default: 0

"},{"location":"cli_commands/#view-or-change-the-flood-advert-interval","title":"View or change the flood advert interval","text":"

Usage: - get flood.advert.interval - set flood.advert.interval <hours>

Parameters: - hours: Interval in hours (3-168)

Default: 12 (Repeater) - 0 (Sensor)

"},{"location":"cli_commands/#view-or-change-the-zero-hop-advert-interval","title":"View or change the zero-hop advert interval","text":"

Usage: - get advert.interval - set advert.interval <minutes>

Parameters: - minutes: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240)

Default: 0

"},{"location":"cli_commands/#limit-the-number-of-hops-for-a-flood-message","title":"Limit the number of hops for a flood message","text":"

Usage: - get flood.max - set flood.max <value>

Parameters: - value: Maximum flood hop count (0-64)

Default: 64

"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-unscoped-flood-message","title":"Limit the number of hops for an unscoped flood message","text":"

Usage: - get flood.max.unscoped - set flood.max.unscoped <value>

Parameters: - value: Maximum flood hop count (0-64) for a packet without a scope (no region set)

Default: 64 - (0xFF indicates it hasn't been set, will track flood.max until it is.)

Note: An alternative to region denyf *, setting flood.max.unscoped to a lower value such as 3 would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region.

"},{"location":"cli_commands/#limit-the-number-of-hops-for-an-advert-flood-message","title":"Limit the number of hops for an advert flood message","text":"

Usage: - get flood.max.advert - set flood.max.advert <value>

Parameters: - value: Maximum flood hop count (0-64) for an advert packet

Default: 8

"},{"location":"cli_commands/#acl","title":"ACL","text":""},{"location":"cli_commands/#add-update-or-remove-permissions-for-a-companion","title":"Add, update or remove permissions for a companion","text":"

Usage: - setperm <pubkey> <permissions>

Parameters: - pubkey: Companion public key - permissions: - 0: Guest - 1: Read-only - 2: Read-write - 3: Admin

Note: Removes the entry when permissions is omitted

"},{"location":"cli_commands/#view-the-current-acl","title":"View the current ACL","text":"

Usage: - get acl

Serial Only: Yes

"},{"location":"cli_commands/#view-or-change-this-room-servers-read-only-flag","title":"View or change this room server's 'read-only' flag","text":"

Usage: - get allow.read.only - set allow.read.only <state>

Parameters: - state: on (enable) or off (disable)

Default: off

"},{"location":"cli_commands/#region-management-v110","title":"Region Management (v1.10.+)","text":""},{"location":"cli_commands/#bulk-load-region-lists","title":"Bulk-load region lists","text":"

Usage: - region load - region load <name> [flood_flag]

Parameters: - name: A name of a region. * represents the wildcard region

Note: flood_flag: Optional F to allow flooding

Note: Indentation creates parent-child relationships (max 8 levels)

Note: region load with an empty name will not work remotely (it's interactive)

"},{"location":"cli_commands/#save-any-changes-to-regions-made-since-reboot","title":"Save any changes to regions made since reboot","text":"

Usage: - region save

"},{"location":"cli_commands/#allow-a-region","title":"Allow a region","text":"

Usage: - region allowf <name>

Parameters: - name: Region name (or * for wildcard)

Note: Setting on wildcard * allows packets without region transport codes

"},{"location":"cli_commands/#block-a-region","title":"Block a region","text":"

Usage: - region denyf <name>

Parameters: - name: Region name (or * for wildcard)

Note: Setting on wildcard * drops packets without region transport codes

"},{"location":"cli_commands/#show-information-for-a-region","title":"Show information for a region","text":"

Usage: - region get <name>

Parameters: - name: Region name (or * for wildcard)

"},{"location":"cli_commands/#view-or-change-the-home-region-for-this-node","title":"View or change the home region for this node","text":"

Usage: - region home - region home <name>

Parameters: - name: Region name

"},{"location":"cli_commands/#view-or-change-the-default-scope-region-for-this-node","title":"View or change the default scope region for this node","text":"

Usage: - region default - region default {name|<null>}

Parameters: - name: Region name, or to reset/clear"},{"location":"cli_commands/#create-a-new-region","title":"Create a new region","text":"

Usage: - region put <name> [parent_name]

Parameters: - name: Region name - parent_name: Parent region name (optional, defaults to wildcard)

"},{"location":"cli_commands/#define-region-hierarchy-single-line","title":"Define region hierarchy (single line)","text":"

Usage: - region def <token> [<token> ...]

Parameters (tokens): Space-separated. A logical cursor starts at the wildcard *.

Behavior: Each created region defaults to flood-allowed (same as region put). The reply is the resulting region tree (same format as bare region); review it before running region save to persist. On error, the reply is Err - ... and any regions placed before the failure remain on the node, just like a partial chain of region put.

Existing regions: region def does not clear the existing tree \u2014 if a name already exists, its parent is updated to the current cursor; otherwise a new region is created. To start from scratch, region remove the unwanted regions first.

Limits: Repeater serial accepts one line up to 160 characters. For larger trees, split across multiple region def commands; the cursor resets to * between commands, so lead the next command with child|ancestor to reposition. Each token splits at most once on | \u2014 region def a|b|c|d is not a flat-list shorthand; see the flat-list example below.

Example \u2014 linear chain (each token becomes a child of the previous):

region def a b c d e\nregion save\n

Example \u2014 branched tree (equivalent to region put a, region put b a, region put c b, region put d c, region put e b, region put f e):

region def a b c d|b e f\nregion save\n

Example \u2014 error and partial state:

region def a b c|nope d\n

The reply is Err - unknown jump: nope. a, b, and c were placed before the failure; d was not. Run region to inspect, then re-run with a corrected jump or repair with region remove / region put.

Example \u2014 flat list (each region a child of *). Use |* after each token to pop the cursor back to the root before the next token:

region def a|* b|* c|* d|* e|* f\nregion save\n
"},{"location":"cli_commands/#remove-a-region","title":"Remove a region","text":"

Usage: - region remove <name>

Parameters: - name: Region name

Note: Must remove all child regions before the region can be removed

"},{"location":"cli_commands/#view-all-regions","title":"View all regions","text":"

Usage: - region list <filter>

Serial Only: Yes

Parameters: - filter: allowed|denied

Note: Requires firmware 1.12+

"},{"location":"cli_commands/#dump-all-defined-regions-and-flood-permissions","title":"Dump all defined regions and flood permissions","text":"

Usage: - region

Serial Only: For firmware older than 1.12.0

"},{"location":"cli_commands/#region-examples","title":"Region Examples","text":"

Example 1: Using F Flag with Named Public Region

region load\n#Europe F\n<blank line to end region load>\nregion save\n

Explanation: - Creates a region named #Europe with flooding enabled - Packets from this region will be flooded to other nodes

Example 2: Using Wildcard with F Flag

region load \n* F\n<blank line to end region load>\nregion save\n

Explanation: - Creates a wildcard region * with flooding enabled - Enables flooding for all regions automatically - Applies only to packets without transport codes

Example 3: Using Wildcard Without F Flag

region load \n*\n<blank line to end region load>\nregion save\n

Explanation: - Creates a wildcard region * without flooding - This region exists but doesn't affect packet distribution - Used as a default/empty region

Example 4: Nested Public Region with F Flag

region load \n#Europe F\n  #UK\n    #London\n    #Manchester\n  #France\n    #Paris\n    #Lyon\n<blank line to end region load>\nregion save\n

Explanation: - Creates #Europe region with flooding enabled - Adds nested child regions (#UK, #France) - All nested regions inherit the flooding flag from parent

Example 5: Wildcard with Nested Public Regions

region load \n* F\n  #NorthAmerica\n    #USA\n      #NewYork\n      #California\n    #Canada\n      #Ontario\n      #Quebec\n<blank line to end region load>\nregion save\n

Explanation: - Creates wildcard region * with flooding enabled - Adds nested #NorthAmerica hierarchy - Enables flooding for all child regions automatically - Useful for global networks with specific regional rules

"},{"location":"cli_commands/#gps-when-gps-support-is-compiled-in","title":"GPS (When GPS support is compiled in)","text":""},{"location":"cli_commands/#view-or-change-gps-state","title":"View or change GPS state","text":"

Usage: - gps - gps <state>

Parameters: - state: on|off

Default: off

Note: Output format: - off when the GPS hardware is disabled - on, {active|deactivated}, {fix|no fix}, {sat count} sats when the GPS hardware is enabled

"},{"location":"cli_commands/#sync-this-nodes-clock-with-gps-time","title":"Sync this node's clock with GPS time","text":"

Usage: - gps sync

"},{"location":"cli_commands/#set-this-nodes-location-based-on-the-gps-coordinates","title":"Set this node's location based on the GPS coordinates","text":"

Usage: - gps setloc

"},{"location":"cli_commands/#view-or-change-the-gps-advert-policy","title":"View or change the GPS advert policy","text":"

Usage: - gps advert - gps advert <policy>

Parameters: - policy: none|share|prefs - none: don't include location in adverts - share: share gps location (from SensorManager) - prefs: location stored in node's lat and lon settings

Default: prefs

"},{"location":"cli_commands/#sensors-when-sensor-support-is-compiled-in","title":"Sensors (When sensor support is compiled in)","text":""},{"location":"cli_commands/#view-the-list-of-sensors-on-this-node","title":"View the list of sensors on this node","text":"

Usage: sensor list [start]

Parameters: - start: Optional starting index (defaults to 0)

Note: Output format: <var_name>=<value>\\n

"},{"location":"cli_commands/#view-or-change-the-value-of-a-sensor","title":"View or change the value of a sensor","text":"

Usage: - sensor get <key> - sensor set <key> <value>

Parameters: - key: Sensor setting name - value: The value to set the sensor to

"},{"location":"cli_commands/#bridge-when-bridge-support-is-compiled-in","title":"Bridge (When bridge support is compiled in)","text":""},{"location":"cli_commands/#view-the-compiled-bridge-type","title":"View the compiled bridge type","text":"

Usage: get bridge.type

"},{"location":"cli_commands/#view-or-change-the-bridge-enabled-flag","title":"View or change the bridge enabled flag","text":"

Usage: - get bridge.enabled - set bridge.enabled <state>

Parameters: - state: on|off

Default: off

"},{"location":"cli_commands/#add-a-delay-to-packets-routed-through-this-bridge","title":"Add a delay to packets routed through this bridge","text":"

Usage: - get bridge.delay - set bridge.delay <ms>

Parameters: - ms: Delay in milliseconds (0-10000)

Default: 500

"},{"location":"cli_commands/#view-or-change-the-source-of-packets-bridged-to-the-external-interface","title":"View or change the source of packets bridged to the external interface","text":"

Usage: - get bridge.source - set bridge.source <source>

Parameters: - source: - logRx: bridges received packets - logTx: bridges transmitted packets

Default: logTx

"},{"location":"cli_commands/#view-or-change-the-speed-of-the-bridge-rs-232-only","title":"View or change the speed of the bridge (RS-232 only)","text":"

Usage: - get bridge.baud - set bridge.baud <rate>

Parameters: - rate: Baud rate (9600, 19200, 38400, 57600, or 115200)

Default: 115200

"},{"location":"cli_commands/#view-or-change-the-channel-used-for-bridging-espnow-only","title":"View or change the channel used for bridging (ESPNow only)","text":"

Usage: - get bridge.channel - set bridge.channel <channel>

Parameters: - channel: Channel number (1-14)

"},{"location":"cli_commands/#set-the-esp-now-secret","title":"Set the ESP-Now secret","text":"

Usage: - get bridge.secret - set bridge.secret <secret>

Parameters: - secret: ESP-NOW bridge secret, up to 15 characters

Default: Varies by board

"},{"location":"cli_commands/#view-the-bootloader-version-nrf52-only","title":"View the bootloader version (nRF52 only)","text":"

Usage: get bootloader.ver

"},{"location":"cli_commands/#view-power-management-support","title":"View power management support","text":"

Usage: get pwrmgt.support

"},{"location":"cli_commands/#view-the-current-power-source","title":"View the current power source","text":"

Usage: get pwrmgt.source

Note: Returns an error on boards without power management support.

"},{"location":"cli_commands/#view-the-boot-reset-and-shutdown-reasons","title":"View the boot reset and shutdown reasons","text":"

Usage: get pwrmgt.bootreason

Note: Returns an error on boards without power management support.

"},{"location":"cli_commands/#view-the-boot-voltage","title":"View the boot voltage","text":"

Usage: get pwrmgt.bootmv

Note: Returns an error on boards without power management support.

"},{"location":"companion_protocol/","title":"Companion Protocol","text":"

NOTE: This document is still in development. Some information may be inaccurate.

This document provides a comprehensive guide for communicating with MeshCore devices over Bluetooth Low Energy (BLE).

It is platform-agnostic and can be used for Android, iOS, Python, JavaScript, or any other platform that supports BLE.

"},{"location":"companion_protocol/#official-libraries","title":"Official Libraries","text":"

Please see the following repos for existing MeshCore Companion Protocol libraries.

"},{"location":"companion_protocol/#important-security-note","title":"Important Security Note","text":"

All secrets, hashes, and cryptographic values shown in this guide are example values only.

"},{"location":"companion_protocol/#table-of-contents","title":"Table of Contents","text":"
  1. BLE Connection
  2. Packet Structure
  3. Commands
  4. Channel Management
  5. Message Handling
  6. Response Parsing
  7. Example Implementation Flow
  8. Best Practices
  9. Troubleshooting
"},{"location":"companion_protocol/#ble-connection","title":"BLE Connection","text":""},{"location":"companion_protocol/#service-and-characteristics","title":"Service and Characteristics","text":"

MeshCore Companion devices expose a BLE service with the following UUIDs:

"},{"location":"companion_protocol/#connection-steps","title":"Connection Steps","text":"
  1. Scan for Devices

  2. Connect to GATT

  3. Discover Services and Characteristics

  4. Enable Notifications

  5. Send Initial Commands

Note: MeshCore devices may disconnect after periods of inactivity. Implement auto-reconnect logic with exponential backoff.

"},{"location":"companion_protocol/#ble-write-type","title":"BLE Write Type","text":"

When writing commands to the RX characteristic, specify the write type:

Platform-specific:

Recommendation: Use write with response for reliability.

"},{"location":"companion_protocol/#mtu-maximum-transmission-unit","title":"MTU (Maximum Transmission Unit)","text":"

The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like SET_CHANNEL (50 bytes), you may need to:

  1. Request Larger MTU: Request MTU of 512 bytes if supported
"},{"location":"companion_protocol/#command-sequencing","title":"Command Sequencing","text":"

Critical: Commands must be sent in the correct sequence:

  1. After Connection:

  2. Command-Response Matching:

"},{"location":"companion_protocol/#command-queue-management","title":"Command Queue Management","text":"

For reliable operation, implement a command queue.

Queue Structure:

Error Handling:

"},{"location":"companion_protocol/#packet-structure","title":"Packet Structure","text":"

The MeshCore protocol uses a binary format with the following structure:

Most packets follow this format:

[Packet Type (1 byte)] [Data (variable length)]\n

The first byte indicates the packet type (see Response Parsing).

"},{"location":"companion_protocol/#commands","title":"Commands","text":""},{"location":"companion_protocol/#1-app-start","title":"1. App Start","text":"

Purpose: Initialize communication with the device. Must be sent first after connection.

Command Format:

Byte 0: 0x01\nBytes 1-7: Reserved (currently ignored by firmware)\nBytes 8+: Application name (UTF-8, optional)\n

Example (hex):

01 00 00 00 00 00 00 00 6d 63 63 6c 69\n

Response: PACKET_SELF_INFO (0x05)

"},{"location":"companion_protocol/#2-device-query","title":"2. Device Query","text":"

Purpose: Query device information.

Command Format:

Byte 0: 0x16\nByte 1: 0x03\n

Example (hex):

16 03\n

Response: PACKET_DEVICE_INFO (0x0D) with device information

"},{"location":"companion_protocol/#3-get-channel-info","title":"3. Get Channel Info","text":"

Purpose: Retrieve information about a specific channel.

Command Format:

Byte 0: 0x1F\nByte 1: Channel Index (0-7)\n

Example (get channel 1):

1F 01\n

Response: PACKET_CHANNEL_INFO (0x12) with channel details

"},{"location":"companion_protocol/#4-set-channel","title":"4. Set Channel","text":"

Purpose: Create or update a channel on the device.

Command Format:

Byte 0: 0x20\nByte 1: Channel Index (0-7)\nBytes 2-33: Channel Name (32 bytes, UTF-8, null-padded)\nBytes 34-49: Secret (16 bytes)\n

Total Length: 50 bytes

Channel Index: - Index 0: Reserved for public channels (no secret) - Indices 1-7: Available for private channels

Channel Name: - UTF-8 encoded - Maximum 32 bytes - Padded with null bytes (0x00) if shorter

Secret Field (16 bytes): - For private channels: 16-byte secret - For public channels: All zeros (0x00)

Example (create channel \"YourChannelName\" at index 1 with secret):

20 01 53 4D 53 00 00 ... (name padded to 32 bytes)\n    [16 bytes of secret]\n

Note: The 32-byte secret variant is unsupported and returns PACKET_ERROR.

Response: PACKET_OK (0x00) on success, PACKET_ERROR (0x01) on failure

"},{"location":"companion_protocol/#5-send-channel-message","title":"5. Send Channel Message","text":"

Purpose: Send a text message to a channel.

Command Format:

Byte 0: 0x03\nByte 1: 0x00\nByte 2: Channel Index (0-7)\nBytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds)\nBytes 7+: Message Text (UTF-8, variable length)\n

Timestamp: Unix timestamp in seconds (32-bit unsigned integer, little-endian)

Example (send \"Hello\" to channel 1 at timestamp 1234567890):

03 00 01 D2 02 96 49 48 65 6C 6C 6F\n

Response: PACKET_MSG_SENT (0x06) on success

"},{"location":"companion_protocol/#6-send-channel-data-datagram","title":"6. Send Channel Data Datagram","text":"

Purpose: Send a binary datagram to a channel. Unlike channel text messages, datagrams carry no built-in sender identity and no timestamp \u2014 applications needing either must encode them inside the binary payload.

Command Format:

Byte 0:                         0x3E\nByte 1:                         Channel Index (0-7)\nByte 2:                         Path Length (0xFF = flood, otherwise actual path length)\nBytes 3 .. 2+path_len:          Path (omitted when path_len == 0xFF)\nNext 2 bytes (little-endian):   Data Type (`data_type`, uint16)\nRemaining bytes:                Binary payload (variable length)\n

Example (flood, DATA_TYPE_DEV, payload A1 B2 C3, channel 1):

3E 01 FF FF FF A1 B2 C3\n

Data Type / Transport Mapping: - 0x0000 (DATA_TYPE_RESERVED) is invalid and rejected with PACKET_ERROR. - 0xFFFF (DATA_TYPE_DEV) is the developer namespace for experimenting and developing apps. - Values 0x0001\u20130xFFFE are available for registered application/community namespaces. See the Registered data_type values table below.

Limits: - Maximum payload length is MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163 bytes. - Larger payloads are rejected with PACKET_ERROR (ERR_CODE_ILLEGAL_ARG).

Response: PACKET_OK (0x00) on success, or PACKET_ERROR (0x01) with one of: - ERR_CODE_NOT_FOUND (2) \u2014 unknown channel_idx - ERR_CODE_ILLEGAL_ARG (6) \u2014 invalid path_len, reserved data_type (0x0000), or payload larger than MAX_CHANNEL_DATA_LENGTH - ERR_CODE_TABLE_FULL (3) \u2014 outbound send queue is full; retry later

Inbound datagrams are delivered to the host via RESP_CODE_CHANNEL_DATA_RECV (0x1B); see Receive Channel Data Datagram.

"},{"location":"companion_protocol/#registered-data_type-values","title":"Registered data_type values","text":"

data_type is an application identifier, not a payload-format identifier. Each registered value identifies an application that owns its own internal payload schemas. The firmware does not inspect payload contents \u2014 data_type is transported opaquely.

Value Constant Purpose 0x0000 DATA_TYPE_RESERVED Reserved; invalid on send 0x0001 \u2013 0x00FF \u2014 Reserved for internal use 0x0100 \u2013 0xFEFF \u2014 Registered application namespaces (see number_allocations.md) 0xFF00 \u2013 0xFFFE \u2014 Testing/development; no registration required 0xFFFF DATA_TYPE_DEV Developer/experimental namespace

To register a new application, submit a PR adding a row to the table in docs/number_allocations.md. Internal sub-formats within an allocated application ID are owned by that application and are not tracked in MeshCore firmware or this document.

"},{"location":"companion_protocol/#receive-channel-data-datagram","title":"Receive Channel Data Datagram","text":"

Inbound group datagrams (radio-level PAYLOAD_TYPE_GRP_DATA, 0x06) are forwarded to the host as RESP_CODE_CHANNEL_DATA_RECV notifications.

Frame Format (RESP_CODE_CHANNEL_DATA_RECV, 0x1B):

Byte 0:                 0x1B (packet type)\nByte 1:                 SNR (signed int8, scaled \u00d74 \u2014 divide by 4.0 to recover dB)\nBytes 2-3:              Reserved (clients MUST ignore)\nByte 4:                 Channel Index (0-7)\nByte 5:                 Path Length (actual path length when flooded, otherwise 0xFF for direct)\nBytes 6-7:              Data Type (uint16 little-endian)\nByte 8:                 Data Length\nBytes 9 .. 8+data_len:  Payload\n

Path bytes are not forwarded: Only path_len is reported in the receive frame \u2014 the path itself is not copied to the host. There are no path bytes between byte 5 and the data_type field at bytes 6\u20137, regardless of path_len.

Path Length semantics differ between send and receive:

Direction path_len = 0xFF path_len \u2260 0xFF Send Flood the network Direct route; the encoded path follows (low 6 bits = hash count, top 2 bits + 1 = hash size; on-wire byte count = hash_count \u00d7 hash_size) Receive Packet arrived via direct route Packet was flooded; this is the encoded pkt->path_len field as observed (no path bytes follow)

In other words, the meaning of 0xFF is inverted between the two directions, and on receive the field carries metadata only \u2014 never a routable path. path_len is an encoded byte (see Packet::isValidPathLen / Packet::writePath in src/Packet.cpp), not a raw byte count.

Note: The device may also emit PACKET_MESSAGES_WAITING (0x83) to notify the host that datagrams are queued; poll with CMD_SYNC_NEXT_MESSAGE (0x0A) to retrieve them.

Parsing Pseudocode:

def parse_channel_data_recv(data):\n    if len(data) < 9:\n        return None\n    snr_byte = data[1]\n    snr = (snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0\n    channel_idx = data[4]\n    path_len = data[5]\n    data_type = int.from_bytes(data[6:8], 'little')\n    data_len = data[8]\n    if 9 + data_len > len(data):\n        return None\n    payload = data[9:9 + data_len]\n    return {\n        'snr': snr,\n        'channel_idx': channel_idx,\n        'path_len': path_len,\n        'data_type': data_type,\n        'payload': bytes(payload),\n    }\n
"},{"location":"companion_protocol/#7-get-message","title":"7. Get Message","text":"

Purpose: Request the next queued message from the device.

Command Format:

Byte 0: 0x0A\n

Example (hex):

0A\n

Response: - PACKET_CHANNEL_MSG_RECV (0x08) or PACKET_CHANNEL_MSG_RECV_V3 (0x11) for channel messages - PACKET_CONTACT_MSG_RECV (0x07) or PACKET_CONTACT_MSG_RECV_V3 (0x10) for contact messages - PACKET_CHANNEL_DATA_RECV (0x1B) for channel data datagrams - PACKET_NO_MORE_MSGS (0x0A) if no messages available

Note: Poll this command periodically to retrieve queued messages. The device may also send PACKET_MESSAGES_WAITING (0x83) as a notification when messages are available.

"},{"location":"companion_protocol/#8-get-battery-and-storage","title":"8. Get Battery and Storage","text":"

Purpose: Query device battery voltage and storage usage.

Command Format:

Byte 0: 0x14\n

Example (hex):

14\n

Response: PACKET_BATTERY (0x0C) with battery millivolts and storage information

"},{"location":"companion_protocol/#channel-management","title":"Channel Management","text":""},{"location":"companion_protocol/#channel-types","title":"Channel Types","text":"
  1. Public Channel
  2. Hashtag Channels
  3. Private Channels
"},{"location":"companion_protocol/#channel-lifecycle","title":"Channel Lifecycle","text":"
  1. Set Channel:
  2. Get Channel:
  3. Delete Channel:
"},{"location":"companion_protocol/#message-handling","title":"Message Handling","text":""},{"location":"companion_protocol/#receiving-messages","title":"Receiving Messages","text":"

Messages are received via the TX characteristic (notifications). The device sends:

  1. Channel Messages:
  2. PACKET_CHANNEL_MSG_RECV (0x08) - Standard format
  3. PACKET_CHANNEL_MSG_RECV_V3 (0x11) - Version 3 with SNR

  4. Contact Messages:

  5. PACKET_CONTACT_MSG_RECV (0x07) - Standard format
  6. PACKET_CONTACT_MSG_RECV_V3 (0x10) - Version 3 with SNR

  7. Notifications:

  8. PACKET_MESSAGES_WAITING (0x83) - Indicates messages are queued
"},{"location":"companion_protocol/#contact-message-format","title":"Contact Message Format","text":"

Standard Format (PACKET_CONTACT_MSG_RECV, 0x07):

Byte 0: 0x07 (packet type)\nBytes 1-6: Public Key Prefix (6 bytes, hex)\nByte 7: Path Length\nByte 8: Text Type\nBytes 9-12: Timestamp (32-bit little-endian)\nBytes 13-16: Signature (4 bytes, only if txt_type == 2)\nBytes 17+: Message Text (UTF-8)\n

V3 Format (PACKET_CONTACT_MSG_RECV_V3, 0x10):

Byte 0: 0x10 (packet type)\nByte 1: SNR (signed byte, multiplied by 4)\nBytes 2-3: Reserved\nBytes 4-9: Public Key Prefix (6 bytes, hex)\nByte 10: Path Length\nByte 11: Text Type\nBytes 12-15: Timestamp (32-bit little-endian)\nBytes 16-19: Signature (4 bytes, only if txt_type == 2)\nBytes 20+: Message Text (UTF-8)\n

Parsing Pseudocode:

def parse_contact_message(data):\n    packet_type = data[0]\n    offset = 1\n\n    # Check for V3 format\n    if packet_type == 0x10:  # V3\n        snr_byte = data[offset]\n        snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)\n        offset += 3  # Skip SNR + reserved\n\n    pubkey_prefix = data[offset:offset+6].hex()\n    offset += 6\n\n    path_len = data[offset]\n    txt_type = data[offset + 1]\n    offset += 2\n\n    timestamp = int.from_bytes(data[offset:offset+4], 'little')\n    offset += 4\n\n    # If txt_type == 2, skip 4-byte signature\n    if txt_type == 2:\n        offset += 4\n\n    message = data[offset:].decode('utf-8')\n\n    return {\n        'pubkey_prefix': pubkey_prefix,\n        'path_len': path_len,\n        'txt_type': txt_type,\n        'timestamp': timestamp,\n        'message': message,\n        'snr': snr if packet_type == 0x10 else None\n    }\n
"},{"location":"companion_protocol/#channel-message-format","title":"Channel Message Format","text":"

Standard Format (PACKET_CHANNEL_MSG_RECV, 0x08):

Byte 0: 0x08 (packet type)\nByte 1: Channel Index (0-7)\nByte 2: Path Length\nByte 3: Text Type\nBytes 4-7: Timestamp (32-bit little-endian)\nBytes 8+: Message Text (UTF-8)\n

V3 Format (PACKET_CHANNEL_MSG_RECV_V3, 0x11):

Byte 0: 0x11 (packet type)\nByte 1: SNR (signed byte, multiplied by 4)\nBytes 2-3: Reserved\nByte 4: Channel Index (0-7)\nByte 5: Path Length\nByte 6: Text Type\nBytes 7-10: Timestamp (32-bit little-endian)\nBytes 11+: Message Text (UTF-8)\n

Parsing Pseudocode:

def parse_channel_message(data):\n    packet_type = data[0]\n    offset = 1\n\n    # Check for V3 format\n    if packet_type == 0x11:  # V3\n        snr_byte = data[offset]\n        snr = ((snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0)\n        offset += 3  # Skip SNR + reserved\n\n    channel_idx = data[offset]\n    path_len = data[offset + 1]\n    txt_type = data[offset + 2]\n    timestamp = int.from_bytes(data[offset+3:offset+7], 'little')\n    message = data[offset+7:].decode('utf-8')\n\n    return {\n        'channel_idx': channel_idx,\n        'timestamp': timestamp,\n        'message': message,\n        'snr': snr if packet_type == 0x11 else None\n    }\n
"},{"location":"companion_protocol/#sending-messages","title":"Sending Messages","text":"

Use the SEND_CHANNEL_MESSAGE command (see Commands).

Important: - Messages are limited to 133 characters per MeshCore specification - Long messages should be split into chunks - Include a chunk indicator (e.g., \"[1/3] message text\")

"},{"location":"companion_protocol/#response-parsing","title":"Response Parsing","text":""},{"location":"companion_protocol/#terminology","title":"Terminology","text":"

This document uses a spec-level naming convention (PACKET_*) for bytes the firmware sends back to the host. In the firmware source these same values are split across two #define families by purpose:

Byte values are authoritative; names are aliases. When reading firmware source, RESP_CODE_X / PUSH_CODE_X correspond to this doc's PACKET_X of the same numeric value.

"},{"location":"companion_protocol/#packet-types","title":"Packet Types","text":"Value Name Description 0x00 PACKET_OK Command succeeded 0x01 PACKET_ERROR Command failed 0x02 PACKET_CONTACT_START Start of contact list 0x03 PACKET_CONTACT Contact information 0x04 PACKET_CONTACT_END End of contact list 0x05 PACKET_SELF_INFO Device self-information 0x06 PACKET_MSG_SENT Message sent confirmation 0x07 PACKET_CONTACT_MSG_RECV Contact message (standard) 0x08 PACKET_CHANNEL_MSG_RECV Channel message (standard) 0x09 PACKET_CURRENT_TIME Current time response 0x0A PACKET_NO_MORE_MSGS No more messages available 0x0C PACKET_BATTERY Battery level 0x0D PACKET_DEVICE_INFO Device information 0x10 PACKET_CONTACT_MSG_RECV_V3 Contact message (V3 with SNR) 0x11 PACKET_CHANNEL_MSG_RECV_V3 Channel message (V3 with SNR) 0x12 PACKET_CHANNEL_INFO Channel information 0x1B PACKET_CHANNEL_DATA_RECV Channel data datagram 0x80 PACKET_ADVERTISEMENT Advertisement packet 0x82 PACKET_ACK Acknowledgment 0x83 PACKET_MESSAGES_WAITING Messages waiting notification 0x88 PACKET_LOG_DATA RF log data (can be ignored)"},{"location":"companion_protocol/#parsing-responses","title":"Parsing Responses","text":"

PACKET_OK (0x00):

Byte 0: 0x00\nBytes 1-4: Optional value (32-bit little-endian integer)\n

PACKET_ERROR (0x01):

Byte 0: 0x01\nByte 1: Error code (optional)\n

PACKET_CHANNEL_INFO (0x12):

Byte 0: 0x12\nByte 1: Channel Index\nBytes 2-33: Channel Name (32 bytes, null-terminated)\nBytes 34-49: Secret (16 bytes)\n

Note: The device returns the 16-byte channel secret in this response.

PACKET_DEVICE_INFO (0x0D):

Byte 0: 0x0D\nByte 1: Firmware Version (uint8)\nBytes 2+: Variable length based on firmware version\n\nFor firmware version >= 3:\nByte 2: Max Contacts Raw (uint8, actual = value * 2)\nByte 3: Max Channels (uint8)\nBytes 4-7: BLE PIN (32-bit little-endian)\nBytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded)\nBytes 20-59: Model (40 bytes, UTF-8, null-padded)\nBytes 60-79: Version (20 bytes, UTF-8, null-padded)\nByte 80: Client repeat enabled/preferred (firmware v9+)\nByte 81: Path hash mode (firmware v10+)\n

Parsing Pseudocode:

def parse_device_info(data):\n    if len(data) < 2:\n        return None\n\n    fw_ver = data[1]\n    info = {'fw_ver': fw_ver}\n\n    if fw_ver >= 3 and len(data) >= 80:\n        info['max_contacts'] = data[2] * 2\n        info['max_channels'] = data[3]\n        info['ble_pin'] = int.from_bytes(data[4:8], 'little')\n        info['fw_build'] = data[8:20].decode('utf-8').rstrip('\\x00').strip()\n        info['model'] = data[20:60].decode('utf-8').rstrip('\\x00').strip()\n        info['ver'] = data[60:80].decode('utf-8').rstrip('\\x00').strip()\n\n    return info\n

PACKET_BATTERY (0x0C):

Byte 0: 0x0C\nBytes 1-2: Battery Voltage (16-bit little-endian, millivolts)\nBytes 3-6: Used Storage (32-bit little-endian, KB)\nBytes 7-10: Total Storage (32-bit little-endian, KB)\n

Parsing Pseudocode:

def parse_battery(data):\n    if len(data) < 3:\n        return None\n\n    mv = int.from_bytes(data[1:3], 'little')\n    info = {'battery_mv': mv}\n\n    if len(data) >= 11:\n        info['used_kb'] = int.from_bytes(data[3:7], 'little')\n        info['total_kb'] = int.from_bytes(data[7:11], 'little')\n\n    return info\n

PACKET_SELF_INFO (0x05):

Byte 0: 0x05\nByte 1: Advertisement Type\nByte 2: TX Power\nByte 3: Max TX Power\nBytes 4-35: Public Key (32 bytes, hex)\nBytes 36-39: Advertisement Latitude (32-bit little-endian, divided by 1e6)\nBytes 40-43: Advertisement Longitude (32-bit little-endian, divided by 1e6)\nByte 44: Multi ACKs\nByte 45: Advertisement Location Policy\nByte 46: Telemetry Mode (bitfield)\nByte 47: Manual Add Contacts (bool)\nBytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0)\nBytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0)\nByte 56: Radio Spreading Factor\nByte 57: Radio Coding Rate\nBytes 58+: Device Name (UTF-8, variable length, no null terminator required)\n

Parsing Pseudocode:

def parse_self_info(data):\n    if len(data) < 36:\n        return None\n\n    offset = 1\n    info = {\n        'adv_type': data[offset],\n        'tx_power': data[offset + 1],\n        'max_tx_power': data[offset + 2],\n        'public_key': data[offset + 3:offset + 35].hex()\n    }\n    offset += 35\n\n    lat = int.from_bytes(data[offset:offset+4], 'little') / 1e6\n    lon = int.from_bytes(data[offset+4:offset+8], 'little') / 1e6\n    info['adv_lat'] = lat\n    info['adv_lon'] = lon\n    offset += 8\n\n    info['multi_acks'] = data[offset]\n    info['adv_loc_policy'] = data[offset + 1]\n    telemetry_mode = data[offset + 2]\n    info['telemetry_mode_env'] = (telemetry_mode >> 4) & 0b11\n    info['telemetry_mode_loc'] = (telemetry_mode >> 2) & 0b11\n    info['telemetry_mode_base'] = telemetry_mode & 0b11\n    info['manual_add_contacts'] = data[offset + 3] > 0\n    offset += 4\n\n    freq = int.from_bytes(data[offset:offset+4], 'little') / 1000.0\n    bw = int.from_bytes(data[offset+4:offset+8], 'little') / 1000.0\n    info['radio_freq'] = freq\n    info['radio_bw'] = bw\n    info['radio_sf'] = data[offset + 8]\n    info['radio_cr'] = data[offset + 9]\n    offset += 10\n\n    if offset < len(data):\n        name_bytes = data[offset:]\n        info['name'] = name_bytes.decode('utf-8').rstrip('\\x00').strip()\n\n    return info\n

PACKET_MSG_SENT (0x06):

Byte 0: 0x06\nByte 1: Route Flag (0 = direct, 1 = flood)\nBytes 2-5: Tag / Expected ACK (4 bytes, little-endian)\nBytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds)\n

PACKET_ACK (0x82):

Byte 0: 0x82\nBytes 1-6: ACK Code (6 bytes, hex)\n
"},{"location":"companion_protocol/#error-codes","title":"Error Codes","text":"

PACKET_ERROR (0x01) carries a single-byte error code in byte 1. Values match the ERR_CODE_* constants defined in examples/companion_radio/MyMesh.cpp:

Code Constant (firmware) Description 1 ERR_CODE_UNSUPPORTED_CMD Unknown or unsupported command byte / sub-command 2 ERR_CODE_NOT_FOUND Target not found (channel, contact, message, etc.) 3 ERR_CODE_TABLE_FULL Internal queue or table is full \u2014 retry later 4 ERR_CODE_BAD_STATE Operation not valid in current device state (e.g. iterator already running) 5 ERR_CODE_FILE_IO_ERROR Filesystem or storage I/O failure 6 ERR_CODE_ILLEGAL_ARG Invalid argument (bad length, out-of-range value, reserved field, etc.)

Note: Error codes may vary by firmware version. Always check byte 1 of PACKET_ERROR response, and treat unknown codes as generic errors.

"},{"location":"companion_protocol/#frame-handling","title":"Frame Handling","text":"

BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer.

"},{"location":"companion_protocol/#response-handling","title":"Response Handling","text":"
  1. Command-Response Pattern:
  2. Send command via RX characteristic
  3. Wait for response via TX characteristic (notification)
  4. Match response to command using sequence numbers or command type
  5. Handle timeout (typically 5 seconds)
  6. Use command queue to prevent concurrent commands

  7. Asynchronous Messages:

  8. Device may send messages at any time via TX characteristic
  9. Handle PACKET_MESSAGES_WAITING (0x83) by polling GET_MESSAGE command
  10. Parse incoming messages and route to appropriate handlers
  11. Validate frame length before decoding

  12. Response Matching:

  13. Match responses to commands by expected packet type:

  14. Timeout Handling:

  15. Default timeout: 5 seconds per command
  16. On timeout: Log error, clear current command, proceed to next in queue
  17. Some commands may take longer (e.g., SET_CHANNEL may need 1-2 seconds)
  18. Consider longer timeout for channel operations

  19. Error Recovery:

  20. On PACKET_ERROR: Log error code, clear current command
  21. On connection loss: Clear command queue, attempt reconnection
  22. On invalid response: Log warning, clear current command, proceed
"},{"location":"companion_protocol/#example-implementation-flow","title":"Example Implementation Flow","text":""},{"location":"companion_protocol/#initialization","title":"Initialization","text":"
# 1. Scan for MeshCore device\ndevice = scan_for_device(\"MeshCore\")\n\n# 2. Connect to BLE GATT\ngatt = connect_to_device(device)\n\n# 3. Discover services and characteristics\nservice = discover_service(gatt, \"6E400001-B5A3-F393-E0A9-E50E24DCCA9E\")\nrx_char = discover_characteristic(service, \"6E400002-B5A3-F393-E0A9-E50E24DCCA9E\")\ntx_char = discover_characteristic(service, \"6E400003-B5A3-F393-E0A9-E50E24DCCA9E\")\n\n# 4. Enable notifications on TX characteristic\nenable_notifications(tx_char, on_notification_received)\n\n# 5. Send AppStart command\nsend_command(rx_char, build_app_start())\nwait_for_response(PACKET_SELF_INFO)\n
"},{"location":"companion_protocol/#creating-a-private-channel","title":"Creating a Private Channel","text":"
# 1. Generate 16-byte secret\nsecret_16_bytes = generate_secret(16)  # Use CSPRNG\nsecret_hex = secret_16_bytes.hex()\n\n# 2. Build SET_CHANNEL command\nchannel_name = \"YourChannelName\"\nchannel_index = 1  # Use 1-7 for private channels\ncommand = build_set_channel(channel_index, channel_name, secret_16_bytes)\n\n# 3. Send command\nsend_command(rx_char, command)\nresponse = wait_for_response(PACKET_OK)\n\n# 4. Store secret locally\nstore_channel_secret(channel_index, secret_hex)\n
"},{"location":"companion_protocol/#sending-a-message","title":"Sending a Message","text":"
# 1. Build channel message command\nchannel_index = 1\nmessage = \"Hello, MeshCore!\"\ntimestamp = int(time.time())\ncommand = build_channel_message(channel_index, message, timestamp)\n\n# 2. Send command\nsend_command(rx_char, command)\nresponse = wait_for_response(PACKET_MSG_SENT)\n
"},{"location":"companion_protocol/#receiving-messages_1","title":"Receiving Messages","text":"
def on_notification_received(data):\n    packet_type = data[0]\n\n    if packet_type == PACKET_CHANNEL_MSG_RECV or packet_type == PACKET_CHANNEL_MSG_RECV_V3:\n        message = parse_channel_message(data)\n        handle_channel_message(message)\n    elif packet_type == PACKET_MESSAGES_WAITING:\n        # Poll for messages\n        send_command(rx_char, build_get_message())\n
"},{"location":"companion_protocol/#best-practices","title":"Best Practices","text":"
  1. Connection Management:
  2. Implement auto-reconnect with exponential backoff
  3. Handle disconnections gracefully
  4. Store last connected device address for quick reconnection

  5. Secret Management:

  6. Always use cryptographically secure random number generators
  7. Store secrets securely (encrypted storage)
  8. Never log or transmit secrets in plain text

  9. Message Handling:

  10. Send CMD_SYNC_NEXT_MESSAGE when PUSH_CODE_MSG_WAITING is received
  11. Implement message deduplication to avoid displaying the same message twice

  12. Channel Management:

  13. Error Handling:

  14. Implement timeouts for all commands (typically 5 seconds)
  15. Handle RESP_CODE_ERR responses appropriately
"},{"location":"companion_protocol/#troubleshooting","title":"Troubleshooting","text":""},{"location":"companion_protocol/#connection-issues","title":"Connection Issues","text":""},{"location":"companion_protocol/#command-issues","title":"Command Issues","text":""},{"location":"companion_protocol/#message-issues","title":"Message Issues","text":""},{"location":"docs/","title":"Local Documentation","text":"

This document explains how to build and view the MeshCore documentation locally.

"},{"location":"docs/#building-and-viewing-docs","title":"Building and viewing Docs","text":"
pip install mkdocs\npip install mkdocs-material\n
"},{"location":"faq/","title":"Frequently Asked Questions","text":"

A list of frequently-asked questions and answers for MeshCore

"},{"location":"faq/#1-introduction","title":"1. Introduction","text":""},{"location":"faq/#11-q-what-is-meshcore","title":"1.1. Q: What is MeshCore?","text":"

A: MeshCore is a multi-platform system for enabling secure text-based communications utilizing LoRa radio hardware. It can be used for Off-Grid Communication, Emergency Response & Disaster Recovery, Outdoor Activities, Tactical Security including law enforcement and private security and also IoT sensor networks. (source)

MeshCore is free and open source:

Some more advanced, but optional features are available on T-Deck if you register your device for a key to unlock. On the MeshCore smartphone clients for Android and iOS/iPadOS, you can unlock the wait timer for repeater and room server remote management over RF feature.

These features are completely optional and aren't needed for the core messaging experience. They're like super bonus features and to help the developers continue to work on these amazing features, they may charge a small fee for an unlock code to utilize the advanced features.

Anyone is able to build anything they like on top of MeshCore without paying anything.

"},{"location":"faq/#12-q-what-do-you-need-to-start-using-meshcore","title":"1.2. Q: What do you need to start using MeshCore?","text":"

A: Everything you need for MeshCore is available at:

You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server).

"},{"location":"faq/#121-hardware","title":"1.2.1. Hardware","text":"

MeshCore is available on a variety of 433MHz, 868MHz and 915MHz LoRa devices. For example, Lilygo T-Deck, T-Pager, RAK Wireless WisBlock RAK4631 devices (e.g. 19003, 19007, 19026), Heltec V3, Xiao S3 WIO, Xiao C3, Heltec T114, Station G2, Nano G2 Ultra, Seeed Studio T1000-E. More devices are being added regularly.

For an up-to-date list of supported devices, please go to https://flasher.meshcore.io

To use MeshCore without using a phone as the client interface, you can run MeshCore on a LilyGo T-Deck, T-Deck Plus, T-Pager, T-Watch, or T-Display Pro. MeshCore Ultra firmware running on these devices is a complete off-grid secure communication solution.

"},{"location":"faq/#122-firmware","title":"1.2.2. Firmware","text":"

MeshCore has four firmware types that are not available on other LoRa systems. MeshCore has the following:

"},{"location":"faq/#123-companion-radio-firmware","title":"1.2.3. Companion Radio Firmware","text":"

Companion radios are for connecting to the Android app or web app as a messenger client. There are two different companion radio firmware versions:

  1. BLE Companion BLE Companion firmware runs on a supported LoRa device and connects to a smart device running the Android or iOS MeshCore client over BLE https://meshcore.io

  2. USB Serial Companion USB Serial Companion firmware runs on a supported LoRa device and connects to a smart device or a computer over USB Serial running the MeshCore web client https://app.meshcore.nz

"},{"location":"faq/#124-repeater","title":"1.2.4. Repeater","text":"

Repeaters are used to extend the range of a MeshCore network. Repeater firmware runs on the same devices that run client firmware. A repeater's job is to forward MeshCore packets to the destination device. It does not forward or retransmit every packet it receives, unlike other LoRa mesh systems.

A repeater can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.

"},{"location":"faq/#125-room-server","title":"1.2.5. Room Server","text":"

A room server is a simple BBS server for sharing posts. T-Deck devices running MeshCore firmware or a BLE Companion client connected to a smartphone running the MeshCore app can connect to a room server.

Room servers store message history on them and push the stored messages to users. Room servers allow roaming users to come back later and retrieve message history. With channels, messages are either received when it's sent, or not received and missed if the channel user is out of range. Room servers are different and more like email servers where you can come back later and get your emails from your mail server.

A room server can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app.

When a client logs into a room server, the client will receive the previously 32 unseen messages.

Although room server can also repeat with the command line command set repeat on, it is not recommended nor encouraged. A room server with repeat set to on lacks the full set of repeater and remote administration features that are only available in the repeater firmware.

The recommendation is to run repeater and room server on separate devices for the best experience.

"},{"location":"faq/#2-initial-setup","title":"2. Initial Setup","text":""},{"location":"faq/#21-q-how-many-devices-do-i-need-to-start-using-meshcore","title":"2.1. Q: How many devices do I need to start using MeshCore?","text":"

A: If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android or iOS client via Bluetooth. You can start communicating with other MeshCore users near you.

If you have two supported devices, and there are not many MeshCore users near you, flash both to BLE Companion firmware so you can use your devices to communicate with your nearby friends and family.

If you have two supported devices, and there are other MeshCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach.

After you flashed the latest firmware onto your repeater device, keep the device connected to your computer via USB serial, use the console feature on the web flasher and set the frequency for your region or country, so your client can remote administer the repeater or room server over RF:

set freq {frequency}

The repeater and room server CLI reference is here: https://docs.meshcore.io/cli_commands

If you have more supported devices, you can use your additional devices with the room server firmware.

"},{"location":"faq/#22-q-does-meshcore-cost-any-money","title":"2.2. Q: Does MeshCore cost any money?","text":"

A: All radio firmware versions (e.g. for Heltec V3, RAK, T-1000E, etc.) are free and open source developed by Scott at Ripple Radios.

The native Android and iOS client uses the freemium model and is developed by Liam Cottle, developer of meshtastic map at meshtastic.liamcottle.net on GitHub and reticulum-meshchat on GitHub.

The T-Deck firmware is free to download and most features are available without cost. To support the firmware developer, you can pay for a registration key to unlock your T-Deck for deeper map zoom and remote server administration over RF using the T-Deck. You do not need to pay for the registration to use your T-Deck for direct messaging and connecting to repeaters and room servers.

"},{"location":"faq/#23-q-what-frequencies-are-supported-by-meshcore","title":"2.3. Q: What frequencies are supported by MeshCore?","text":"

A: It supports the 868MHz range in the UK/EU and the 915MHz range in New Zealand, Australia, and the USA. Countries and regions in these two frequency ranges are also supported.

Use the smartphone client or the repeater setup feature on the web flasher to set your radios' RF settings by choosing the preset for your regions.

Recently, as of October 2025, many regions have moved to the \"narrow\" setting, aka using BW62.5 and a lower SF number (instead of the original SF11). For example, USA/Canada (Recommended) preset is 910.525MHz, SF7, BW62.5, CR5.

After extensive testing, many regions have switched or about to switch over to BW62.5 and SF7, 8, or 9. Narrower bandwidth setting and lower SF setting allow MeshCore's radio signals to fit between interference in the ISM band, provide for a lower noise floor, better SNR, and faster transmissions.

If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the #meshcore-app channel on the MeshCore Discord server to let Liam Cottle know.

"},{"location":"faq/#24-q-what-is-an-advert-in-meshcore","title":"2.4. Q: What is an \"advert\" in MeshCore?","text":"

A: Advert means to advertise yourself on the network. In Reticulum terms it would be to announce. In Meshtastic terms it would be the node sending its node info.

MeshCore allows you to manually broadcast your name, position and public encryption key, which is also signed to prevent spoofing. When you click the advert button, it broadcasts that data over LoRa. MeshCore calls that an Advert. There's two ways to advert, \"zero hop\" and \"flood\".

MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 12 hours by default. This interval can be configured using the following command:

set flood.advert.interval {hours}

The separate set advert.interval {minutes} command controls the local zero-hop advert timer.

"},{"location":"faq/#25-q-is-there-a-hop-limit","title":"2.5. Q: Is there a hop limit?","text":"

A: Internally the firmware has maximum limit of 64 hops. In real world settings it will be difficult to get close to the limit due to the environments and timing as packets travel further and further. We want to hear how far your MeshCore conversations go.

"},{"location":"faq/#3-server-administration","title":"3. Server Administration","text":""},{"location":"faq/#31-q-how-do-you-configure-a-repeater-or-a-room-server","title":"3.1. Q: How do you configure a repeater or a room server?","text":"

A: When MeshCore is flashed onto a LoRa device for the first time, it is necessary to set the server device's frequency to make it utilize the frequency that is legal in your country or region.

Repeater or room server can be administered with one of the options below:

https://buymeacoffee.com/ripplebiz/e/249834

"},{"location":"faq/#32-q-do-i-need-to-set-the-location-for-a-repeater","title":"3.2. Q: Do I need to set the location for a repeater?","text":"

A: While not required, with location set for a repeater it will show up on the MeshCore map in the future. Set location with the following command:

set lat <GPS Lat>

set lon <GPS Lon>

You can get the latitude and longitude from Google Maps by right-clicking the location you are at on the map.

"},{"location":"faq/#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server","title":"3.3. Q: What is the password to administer a repeater or a room server?","text":"

A: The default admin password to a repeater and room server is password. Use the following command to change the admin password:

password {new-password}

"},{"location":"faq/#34-q-what-is-the-password-to-join-a-room-server","title":"3.4. Q: What is the password to join a room server?","text":"

A: The default guest password to a room server is hello. Use the following command to change the guest password:

set guest.password {guest-password}

"},{"location":"faq/#35-q-can-i-retrieve-a-repeaters-private-key-or-set-a-repeaters-private-key","title":"3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key?","text":"

A: You can issue these commands to get or set a repeater's private key using a USB serial connection.

get prv.key to print a repeater's private key on the serial console set prv.key <hex> to set a repeater's private key on the serial console

Reboot the repeater after set prv.key <hex> command for the new private key to take effect.

"},{"location":"faq/#36-q-the-first-byte-of-my-repeaters-public-key-collides-with-an-existing-repeater-on-the-mesh-how-do-i-get-a-new-private-key-with-a-matching-public-key-that-has-its-first-byte-of-my-choosing","title":"3.6. Q: The first byte of my repeater's public key collides with an existing repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?","text":"

A: You can generate a new private key and specify the first byte of its public key here: https://gessaman.com/mc-keygen

Having multiple repeaters with the same first byte ID does not negatively affect the mesh or its functionality. Flood and pathed packets will still reach their destinations. First byte ID collision makes traceroute and path analysis harder because these tools don't know exactly which of the two (or more) colliding repeaters is the one in the path.

Best practice is when you set up a new repeater, choose a public key that is not in use. If it is not possible to find a unique first byte for your repeater's public key, choose one that is unique within about 10 miles (16 km) to minimize collision with nearby repeaters.

"},{"location":"faq/#37-q-my-repeater-may-be-suffering-from-deafness-due-to-high-power-interference-near-my-meshs-frequency-it-is-not-hearing-other-in-range-meshcore-radios-what-can-i-do","title":"3.7. Q: My repeater may be suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do?","text":"

A: This may be due to the SX1262 radio's auto gain control feature. You can use this command to periodically reset its AGC.

set agc.reset.interval <number>

The <number> unit is in seconds and is incremented by 4. set agc.reset.interval 4 works well to cure deafness.

This is a very low-cost operation. AGC reset is done by simply setting state = STATE_IDLE; in function RadioLibWrapper::resetAGC() in RadioLibWrappers.cpp

"},{"location":"faq/#38-q-how-do-i-make-my-repeater-an-observer-on-the-mesh","title":"3.8. Q: How do I make my repeater an observer on the mesh?","text":"

A: The observer instruction is available here: https://analyzer.letsmesh.net/observer/onboard

"},{"location":"faq/#39-q-what-is-multibyte-support-what-do-1-byte-2-byte-3-byte-adverts-and-messages-mean","title":"3.9. Q: What is multibyte support? What do 1-byte, 2-byte, 3-byte adverts and messages mean?","text":"

A: The original MeshCore protocol design uses the first byte of a repeater's public key to denote the repeater in a path. And with 1 byte for each repeater in the path, MeshCore packets can travel as many as 64 hops.

However, with 1 byte, there are only 254 unique IDs (exclude 00 and FF which are reserved). Many meshes group have multiple repeaters with the same first byte in their public keys. Packets continue to pass through repeaters and the mesh is not harmed in any way. It does make it harder for tools to analyze paths with duplicated repeater IDs.

Firmware version 1.14 and newer introduces the ability for repeaters to advert with 1-, 2-, or 3-byte adverts. Companions can also send out channel and direct messages with 1-, 2-, or 3-byte path. Adverts and messages sent in 1-byte path is compatible with repeater firmware older or newer than 1.14. They will travel up to 64 hops. 2-byte adverts and messages will travel up to 32 hops. 3-byte adverts and messages will travel up to 21 hops.

"},{"location":"faq/#391-q-what-path-hash-sizes-will-my-repeater-forward","title":"3.9.1. Q: What path hash sizes will my repeater forward?","text":"

Repeaters running firmware 1.14+ repeat packets sent with 1-, 2-, or 3-byte path hash. Repeaters on firmware older than 1.14 only repeat 1-byte path hash packets and silently drop 2- and 3-byte packets.

"},{"location":"faq/#392-q-what-determines-a-packets-path-hash-size","title":"3.9.2. Q: What determines a packet's path hash size?","text":"

The original packet sender determines the path hash size. The most common original sender is a companion app. The other common original sender is a repeater, when it broadcasts its advert.

"},{"location":"faq/#393-q-how-do-i-change-my-companions-path-hash-size","title":"3.9.3. Q: How do I change my companion's path hash size?","text":"

As of firmware version 1.14 and MeshCore app version 1.41.0, in the MeshCore app, you can set your companion's message path hash size in Settings (gear icon), Experimental Settings.

Until your regional mesh has the vast majority of the repeaters updated to 1.14+ firmware, it is recommended to keep your companion at the default 1-byte because pre-1.14 repeaters will silently drop messages with larger path hashes.

"},{"location":"faq/#394-q-what-does-the-cli-command-pathhashmode-do-on-a-repeater","title":"3.9.4. Q: What does the CLI command path.hash.mode do on a repeater?","text":"

This CLI command path.hash.mode only controls the path hash size used in a repeater's own advert broadcasts. It does NOT affect which packets the repeater forwards. A repeater with firmware 1.14+ always forward 1-, 2-, and 3-byte packets regardless of this setting.

Usage: set path.hash.mode {0|1|2}:

\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 path.hash.mode \u2502 Advert path hash size \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 0              \u2502 1 byte (default)      \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 1              \u2502 2 bytes               \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 2              \u2502 3 bytes               \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518  \n

It is safe to set your 1.14+ repeaters to mode 1 or 2.

"},{"location":"faq/#395-q-why-use-2-or-3-byte-path-hash-for-adverts","title":"3.9.5. Q: Why use 2- or 3-byte path hash for adverts?","text":"

A longer path hash helps tools like the LetsMesh.net Analyzer and MeshMapper disambiguate repeaters more reliably. With only 1 byte, the chance of different repeaters having the same first byte in their public key is high, making it harder to tell them apart in mesh network analysis. Since this only affects adverts, there's no downside. 2- and 3-byte adverts don't travel as far as 1-byte adverts, but it is not important for MeshCore nodes to hear a repeater's advert that is 21 or 32 hops away.

"},{"location":"faq/#396-q-when-can-we-move-away-from-1-byte-path-hash-for-channel-and-direct-messages","title":"3.9.6. Q: When can we move away from 1-byte path hash for channel and direct messages?","text":"

You should move to send 2-byte or 3-byte channel and direct messages when the vast majority of the repeaters in your regional mesh are updated to firmware version 1.14 or newer. Setting your repeater's path.hash.mode to 1 (for 2-byte path hash) or 2 (for 3-byte path hash) now helps the community gauge to how many repeaters have updated to 1.14+. Please work with your MeshCore community together to decide when to switch to 2-byte path or 3-byte path for channel and direct messages.

"},{"location":"faq/#4-t-deck-related","title":"4. T-Deck Related","text":""},{"location":"faq/#41-q-is-there-a-user-guide-for-t-deck-t-pager-t-watch-or-t-display-pro","title":"4.1. Q: Is there a user guide for T-Deck, T-Pager, T-Watch, or T-Display Pro?","text":"

A: Yes, it is available on https://buymeacoffee.com/ripplebiz/ultra-v7-7-guide-meshcore-users

"},{"location":"faq/#42-q-what-are-the-steps-to-get-a-t-deck-into-dfu-device-firmware-update-mode","title":"4.2. Q: What are the steps to get a T-Deck into DFU (Device Firmware Update) mode?","text":"

A:

  1. Device off
  2. Connect USB cable to device
  3. Hold down trackball (keep holding)
  4. Turn on device
  5. Hear USB connection sound
  6. Release trackball
  7. T-Deck in DFU mode now
  8. At this point you can begin flashing using https://flasher.meshcore.io
"},{"location":"faq/#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock","title":"4.3. Q: Why is my T-Deck Plus not getting any satellite lock?","text":"

A: For T-Deck Plus, the GPS baud rate should be set to 38400. Also, some T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open the device to check the GPS orientation.

GPS on T-Deck is always enabled. You can skip the \"GPS clock sync\" and the T-Deck will continue to try to get a GPS lock. You can go to the GPS Info screen; you should see the Sentences: counter increasing if the baud rate is correct.

Source

"},{"location":"faq/#44-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock","title":"4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?","text":"

A: The OG (non-Plus) T-Deck doesn't come with a GPS. If you added a GPS to your OG T-Deck, please refer to the manual of your GPS to see what baud rate it requires. Alternatively, you can try to set the baud rate from 9600, 19200, etc., and up to 115200 to see which one works.

"},{"location":"faq/#45-q-what-size-of-sd-card-does-the-t-deck-support","title":"4.5. Q: What size of SD card does the T-Deck support?","text":"

A: Users have had no issues using 16GB or 32GB SD cards. Format the SD card to FAT32.

"},{"location":"faq/#46-q-what-is-the-public-key-for-the-default-public-channel","title":"4.6. Q: What is the public key for the default public channel?","text":"

A: T-Deck uses the same key the smartphone apps use but in base64 izOH6cXN6mrJ5e26oRXNcg==

There is no = key on the T-Deck's hardware keyboard. You can use the on-screen software keyboard to enter =. Tap the text box to enable the on-screen software keyboard. The third character is the capital letter O (Oh), not zero 0

The smartphone app key is in hex: 8b3387e9c5cdea6ac9e5edbaa115cd72

Source

"},{"location":"faq/#47-q-how-do-i-get-maps-on-t-deck","title":"4.7. Q: How do I get maps on T-Deck?","text":"

A: You need map tiles. You can get pre-downloaded map tiles here (a good way to support development):

Another way to download map tiles is to use this Python script to get the tiles in the areas you want: https://github.com/fistulareffigy/MTD-Script

There is also a modified script that adds additional error handling and parallel downloads: https://github.com/TheBestJohn/MTD-Script

"},{"location":"faq/#48-q-where-do-the-map-tiles-go","title":"4.8. Q: Where do the map tiles go?","text":"

Once you have the tiles downloaded, copy the \\tiles folder to the root of your T-Deck's SD card.

"},{"location":"faq/#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck","title":"4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?","text":"

A: You can download, install, and use the T-Deck firmware for free, but it has some features (map zoom, server administration) that are enabled if you purchase an unlock code for \\$10 per T-Deck device. Unlock page: https://buymeacoffee.com/ripplebiz/e/249834

"},{"location":"faq/#410-q-how-to-decipher-the-diagnostics-screen-on-t-deck","title":"4.10. Q: How to decipher the diagnostics screen on T-Deck?","text":"

A: Space is tight on T-Deck's screen, so the information is a bit cryptic. The format is : {hops} l:{packet-length}({payload-len}) t:{packet-type} snr:{n} rssi:{n}

See here for packet-type: https://github.com/meshcore-dev/MeshCore/blob/main/src/Packet.h#L19

#define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)\n#define PAYLOAD_TYPE_RESPONSE 0x01 // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)\n#define PAYLOAD_TYPE_TXT_MSG 0x02 // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)\n#define PAYLOAD_TYPE_ACK 0x03 // a simple ack #define PAYLOAD_TYPE_ADVERT 0x04 // a node advertising its Identity\n#define PAYLOAD_TYPE_GRP_TXT 0x05 // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, \"name: msg\")\n#define PAYLOAD_TYPE_GRP_DATA 0x06 // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: data_type, data_len, blob)\n#define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)\n#define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)\n

Source

"},{"location":"faq/#411-q-the-t-deck-sound-is-too-loud","title":"4.11. Q: The T-Deck sound is too loud?","text":""},{"location":"faq/#412-q-can-you-customize-the-sound","title":"4.12. Q: Can you customize the sound?","text":"

A: You can customize the sounds on the T-Deck, by placing .mp3 files onto the root dir of the SD card. The files are:

"},{"location":"faq/#413-q-what-is-the-import-from-clipboard-feature-on-the-t-deck-and-is-there-a-way-to-manually-add-nodes-without-having-to-receive-adverts","title":"4.13. Q: What is the 'Import from Clipboard' feature on the t-deck and is there a way to manually add nodes without having to receive adverts?","text":"

A: 'Import from Clipboard' is for importing a contact via a file named 'clipboard.txt' on the SD card. The opposite, is in the Identity screen, the 'Card to Clipboard' menu, which writes to 'clipboard.txt' so you can share yourself (call these 'biz cards', that start with \"meshcore://...\")

"},{"location":"faq/#414-q-how-to-capture-a-screenshot-on-t-deck","title":"4.14. Q: How to capture a screenshot on T-Deck?","text":"

A: To capture a screenshot on a T-Deck, long press the top-left corner of the screen. The screenshot is saved to the microSD card, if one is inserted into the device.

"},{"location":"faq/#5-general","title":"5. General","text":""},{"location":"faq/#51-q-what-are-bw-sf-and-cr","title":"5.1. Q: What are BW, SF, and CR?","text":"

A:

BW is bandwidth - width of frequency spectrum that is used for transmission

SF is spreading factor - how much should the communication spread in time

CR is coding rate - from: https://www.thethingsnetwork.org/docs/lorawan/fec-and-code-rate

TL;DR: default CR to 5 for good stable links. If it is not a solid link and is intermittent, change CR to 7 or 8.

Forward Error Correction is a process of adding redundant bits to the data to be transmitted. During the transmission, data may get corrupted by interference (changes from 0 to 1 / 1 to 0). These error correction bits are used at the receivers for restoring corrupted bits.

The Code Rate of a forward error correction expresses the proportion of bits in a data stream that actually carry useful information.

There are 4 code rates used in LoRaWAN:

4/5 4/6 5/7 4/8

For example, if the code rate is 5/7, for every 5 bits of useful information, the coder generates a total of 7 bits of data, of which 2 bits are redundant.

Making the bandwidth 2x wider (from BW125 to BW250) allows you to send 2x more bytes in the same time. Making the spreading factor 1 step lower (from SF10 to SF9) allows you to send 2x more bytes in the same time.

Lowering the spreading factor makes it more difficult for the gateway to receive a transmission, as it will be more sensitive to noise. You could compare this to two people talking in a noisy place (a bar for example). If you\u2019re far from each other, you have to talk slow (SF10), but if you\u2019re close, you can talk faster (SF7)

So, it's a balancing act between speed of the transmission and resistance to noise. The Things Network is mainly focused on LoRaWAN, but the LoRa low-level stuff still checks out for any LoRa project

"},{"location":"faq/#52-q-do-meshcore-clients-repeat","title":"5.2. Q: Do MeshCore clients repeat?","text":"

A: No, MeshCore clients do not repeat. This is the core of MeshCore's messaging-first design. This is to avoid devices flooding the airwaves and create endless collisions, so messages sent aren't received. In MeshCore, only repeaters and room servers with set repeat on repeat.

"},{"location":"faq/#53-q-what-happens-when-a-node-learns-a-route-via-a-mobile-repeater-and-that-repeater-is-gone","title":"5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone?","text":"

A: If you used to reach a node through a repeater and the repeater is no longer reachable, the client will send the message using the existing (but now broken) known path, the message will fail after 3 retries, and the app will reset the path and send the message as flood on the last retry by default. This can be turned off in settings. If the destination is reachable directly or through another repeater, the new path will be used going forward. Or you can set the path manually if you know a specific repeater to use to reach that destination.

In the case if users are moving around frequently, and the paths are breaking, they just see the phone client retries and revert to flood to attempt to re-establish a path.

"},{"location":"faq/#54-q-how-does-a-node-discover-a-path-to-its-destination-and-then-use-it-to-send-messages-in-the-future-instead-of-flooding-every-message-it-sends-like-meshtastic","title":"5.4. Q: How does a node discover a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?","text":"

Routes are stored in sender's contact list. When you send a message the first time, the message first gets to your destination by flood routing. When your destination node gets the message, it will send back a delivery report to the sender with all repeaters that the original message went through. This delivery report is flood-routed back to you the sender and is a basis for future direct path. When you send the next message, the path will get embedded into the packet and be evaluated by repeaters. If the hop and address of the repeater matches, it will retransmit the message, otherwise it will not retransmit, hence minimizing utilization.

Source

"},{"location":"faq/#55-q-do-public-channels-always-flood-do-private-channels-always-flood","title":"5.5. Q: Do public channels always flood? Do private channels always flood?","text":"

A: Yes, group channels are A to B, so there is no defined path. They have to flood. Repeaters can however deny flood traffic up to some hop limit, with the set flood.max CLI command. Administrators of repeaters get to set the rules of their repeaters.

Source

"},{"location":"faq/#56-q-what-is-the-public-key-for-the-default-public-channel","title":"5.6. Q: What is the public key for the default public channel?","text":"

A: The smartphone app key is in hex:

8b3387e9c5cdea6ac9e5edbaa115cd72

T-Deck uses the same key but in base64:

izOH6cXN6mrJ5e26oRXNcg==

The third character is the capital letter O, not zero 0.

Source

"},{"location":"faq/#57-q-is-meshcore-open-source","title":"5.7. Q: Is MeshCore open source?","text":"

A: Most of the firmware is freely available. Everything is open source except the T-Deck firmware and Liam's native mobile apps.

Firmware repo: https://github.com/meshcore-dev/MeshCore

"},{"location":"faq/#58-q-how-can-i-support-meshcore","title":"5.8. Q: How can I support MeshCore?","text":"

A: Provide your honest feedback on GitHub and on MeshCore Discord server. Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at https://buymeacoffee.com/ripplebiz.

Support Liam Cottle's smartphone client development by unlocking the server administration wait gate with in-app purchase

Support Rastislav Vysoky (recrof)'s flasher website and the map website development through PayPal or Revolut

"},{"location":"faq/#59-q-how-do-i-build-meshcore-firmware-from-source","title":"5.9. Q: How do I build MeshCore firmware from source?","text":"

A: See instructions here: https://discord.com/channels/826570251612323860/1330643963501351004/1341826372120608769

Build instructions for MeshCore:

For Windows, first install WSL and Python+pip via: https://plainenglish.io/blog/setting-up-python-on-windows-subsystem-for-linux-wsl-26510f1b2d80

(Linux, Windows+WSL) In the terminal/shell:

sudo apt update\nsudo apt install libpython3-dev\nsudo apt install python3-venv\n

Mac: python3 should be already installed.

Then it should be the same for all platforms:

python3 -m venv meshcore\ncd meshcore && source bin/activate\npip install -U platformio\ngit clone https://github.com/ripplebiz/MeshCore.git\ncd MeshCore\n

open platformio.ini and in [arduino_base] edit the LORA_FREQ=867.5 save, then run:

pio run -e RAK_4631_Repeater\n

then you'll find firmware.zip in .pio/build/RAK_4631_Repeater

"},{"location":"faq/#510-q-are-there-other-meshcore-related-open-source-projects","title":"5.10. Q: Are there other MeshCore related open source projects?","text":"

A: Liam Cottle's MeshCore web client and MeshCore JavaScript library are open source under MIT license.

Web client: https://github.com/liamcottle/meshcore-web Javascript: https://github.com/liamcottle/meshcore.js

"},{"location":"faq/#511-q-does-meshcore-support-atak","title":"5.11. Q: Does MeshCore support ATAK?","text":"

A: ATAK is not currently on MeshCore's roadmap.

MeshCore would not be best suited to ATAK because MeshCore:

MeshCore clients would need to reset path constantly and flood traffic across the network which could lead to lots of collisions with something as chatty as ATAK.

This could change in the future if MeshCore develops a client firmware that repeats.

Source

"},{"location":"faq/#512-q-how-do-i-add-a-node-to-the-meshcore-map","title":"5.12. Q: How do I add a node to the MeshCore Map?","text":"

A:

To add a BLE Companion radio, connect to the BLE Companion radio from the MeshCore smartphone app. In the app, tap the 3 dot menu icon at the top right corner, then tap Internet Map. Tap the 3 dot menu icon again and choose Add me to the Map

To add a Repeater or Room Server to the map, go to the Contact List, tap the 3 dot next to the Repeater or Room Server you want to add to the Internet Map, tap Share, then tap Upload to Internet Map.

You can use the same companion (same public key) that you used to add your repeaters or room servers to remove them from the Internet Map.

"},{"location":"faq/#513-q-can-i-use-a-raspberry-pi-to-update-a-meshcore-radio","title":"5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?","text":"

A: Yes. Below are the instructions to flash firmware onto a supported LoRa device using a Raspberry Pi over USB serial.

Instructions for nRF devices like RAK, T1000-E, T114 are immediately after the ESP instructions

For ESP-based devices (e.g. Heltec V3) you need:

  1. Download the firmware file from https://flasher.meshcore.io.
  2. Confirm the ttyXXXX device path on your Raspberry Pi.
  3. Install esptool from the shell.
  4. Flash the firmware.

Instructions for nRF devices:

For nRF devices (e.g. RAK, Heltec T114) you need the following:

  1. Download the firmware file from https://flasher.meshcore.io.
  2. Confirm the ttyXXXX device path on your Raspberry Pi.
  3. Install adafruit-nrfutil.
  4. Flash the nRF device.

To manage a repeater or room server connected to a Pi over USB serial using shell commands, you need to install picocom. To install picocom, run the following command:

To start managing your USB serial-connected device using picocom, use the following command:

From here, reference repeater and room server command line commands in the MeshCore docs here:

"},{"location":"faq/#514-q-are-there-projects-built-around-meshcore","title":"5.14. Q: Are there projects built around MeshCore?","text":"

A: Yes, there are many. MeshCore's protocol is open source using the MIT license. The MIT license and the open source protocol makes it very easy for the MeshCore community to build new firmware for radios, applications on mobile devices, map tools, and analysis tools, and integration with other projects like Home Assistant.

As new MeshCore community projects become available on a weekly basis, we have stopped tracking them here in this FAQ. samuk maintains a very exhaustive list of MeshCore community project at https://github.com/samuk/awesome-meshcore/blob/main/README.md. samuk accepts PRs and merges them regularly.

"},{"location":"faq/#515-q-are-there-client-applications-for-windows-or-mac","title":"5.15. Q: Are there client applications for Windows or Mac?","text":"

A: Yes, the same iOS and Android client is also available for Windows and Mac. You can find them together with the Android APK here: https://files.liamcottle.net/MeshCore

Both the Windows and Mac versions of the client app are fully unlocked and are free to use.

"},{"location":"faq/#516-q-are-there-any-resources-that-compare-meshcore-to-other-lora-systems","title":"5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?","text":"

A: Here is a list of MeshCore comparison resources:

"},{"location":"faq/#6-troubleshooting","title":"6. Troubleshooting","text":""},{"location":"faq/#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago","title":"6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.","text":""},{"location":"faq/#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed","title":"6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.","text":"

A:

You can get the epoch time on https://www.epochconverter.com and use it to set your T-Deck clock. For a repeater and room server, the admin can use a T-Deck to remotely set their clock (clock sync), or use the time command in the USB serial console with the server device connected.

"},{"location":"faq/#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth","title":"6.3. Q: How to connect to a repeater via BLE (Bluetooth)?","text":"

A: You can't connect to a device running repeater firmware via Bluetooth. You can connect to devices running the BLE companion firmware via Bluetooth using the Android app.

"},{"location":"faq/#64-q-my-companion-isnt-showing-up-over-bluetooth","title":"6.4. Q: My companion isn't showing up over Bluetooth?","text":"

A: Make sure that you flashed the Bluetooth companion firmware and not the USB-only companion firmware.

"},{"location":"faq/#65-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code","title":"6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?","text":"

A: The default Bluetooth pairing code is 123456

"},{"location":"faq/#66-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone-it-cant-hold-a-solid-bluetooth-connection","title":"6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.","text":"

A: Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification.

"},{"location":"faq/#67-q-my-rakt1000-exiao_nrf52-device-seems-to-be-corrupted-how-do-i-wipe-it-clean-to-start-fresh","title":"6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh?","text":"

A:

  1. Connect USB-C cable to your device, per your device's instruction, get it to flash mode:
  2. A new folder will appear on your computer's desktop
  3. Download the flash_erase*.uf2 file for your device on https://flasher.meshcore.io
  4. drag and drop the uf2 file for your device to the root of the new folder
  5. Wait for the copy to complete. You might get an error dialog, you can ignore it
  6. Go to https://flasher.meshcore.io, click Console and select the serial port for your connected device
  7. In the console, press enter. Your flash should now be erased
  8. You may now flash the latest MeshCore firmware onto your device

Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by holding down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on https://flasher.meshcore.io

"},{"location":"faq/#68-q-webflasher-fails-on-linux-with-failed-to-open","title":"6.8. Q: WebFlasher fails on Linux with failed to open","text":"

A: If the usb port doesn't have the right ownership for this task, the process fails with the following error:

NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.

Allow the browser user on it:

# setfacl -m u:YOUR_USER_HERE:rw /dev/ttyUSB0

"},{"location":"faq/#7-other-questions","title":"7. Other Questions:","text":""},{"location":"faq/#71-q-how-to-update-nrf-rak-t114-seeed-xiao-companion-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app","title":"7.1. Q: How to update nRF (RAK, T114, Seeed XIAO) companion, repeater and room server firmware over the air using the new simpler DFU app?","text":"

A: The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms:

  1. Download nRF's DFU app from iOS App Store or Android's Play Store, you can find the app by searching for nrf dfu, the app's full name is nRF Device Firmware Update
  2. On https://flasher.meshcore.io, download the ZIP version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao)
  3. From the MeshCore app, log in remotely to the repeater you want to update with admin privileges
  4. Go to the Command Line tab, type start ota and hit enter.
  5. You should see OK to confirm the repeater device is now in OTA mode
  6. Run the DFU app, then tap Settings in the top-right corner
  7. Enable Packet receipt notifications, and change Number of Packets to 10 for RAK, 8 for T114. 8 also works for RAK.
  8. Select the firmware zip file you downloaded
  9. Select the device you want to update. If the device you want to update is not on the list, try enabling OTA on the device again
  10. If the device is not found, enable Force Scanning in the DFU app
  11. Tap Upload to begin OTA update
  12. If it fails, try toggling Bluetooth on your phone. If that doesn't work, try rebooting your phone. If you keep getting failures at the \"Enabling Bootloader\" step, try forgetting the nRF board in your iOS or Android device's Bluetooth settings and re-pair it through the DFU app.
  13. Wait for the update to complete. It can take a few minutes.
  14. It is strongly recommended that you install and use the OTAFIX bootloader at https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX.
  15. To update a companion node over OTA, it must be running companion firmware v1.15 or greater.
  16. Please see the MeshCore Blog for additional information on OTA firmware flashing:
"},{"location":"faq/#711-q-can-i-update-seeed-studio-wio-tracker-l1-pro-using-ota","title":"7.1.1 Q: Can I update Seeed Studio Wio Tracker L1 Pro using OTA?","text":"

A: You can flash this safer bootloader to the Wio Tracker L1 Pro https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX

After this bootloader is flashed onto the device, you can trigger an over-the-air update using Bluetooth by holding the button next to the D-Pad and then clicking the reset button. Then follow the same OTA update instructions above. You can skip the start ota instruction and start the update using the DFU app.

"},{"location":"faq/#72-q-how-to-update-esp32-based-devices-over-the-air","title":"7.2. Q: How to update ESP32-based devices over the air?","text":"

A: For ESP32-based devices (e.g. Heltec V3):

  1. On https://flasher.meshcore.io, download the non-merged version of the firmware for your ESP32 device (e.g. Heltec_v3_repeater-v1.6.2-4449fd3.bin, no \"merged\" in the file name).
  2. From the MeshCore app, log in remotely to the repeater you want to update with admin privileges.
  3. Go to the Command Line tab, type start ota and hit enter.
  4. You should see OK to confirm the repeater device is now in OTA mode.
  5. The command start ota on an ESP32-based device starts a Wi-Fi hotspot named MeshCore OTA.
  6. From your phone or computer connect to the 'MeshCore OTA' hotspot.
  7. From a browser, go to http://192.168.4.1/update and upload the non-merged bin from the flasher.
"},{"location":"faq/#73-q-is-there-a-way-to-lower-the-chance-of-a-failed-ota-device-firmware-update-dfu","title":"7.3. Q: Is there a way to lower the chance of a failed OTA device firmware update (DFU)?","text":"

A: Yes, developer che aporeps has an enhanced OTA DFU bootloader for nRF52 based devices. With this bootloader, if it detects that the application firmware is invalid, it falls back to OTA DFU mode so you can attempt to flash again to recover. This bootloader has other changes to make the OTA DFU process more fault tolerant.

Refer to https://github.com/oltaco/Adafruit_nRF52_Bootloader_OTAFIX for the latest information.

Currently, the following boards are supported:

"},{"location":"faq/#74-q-are-the-meshcore-logo-and-font-available","title":"7.4. Q: Are the MeshCore logo and font available?","text":"

A: Yes, it is on the MeshCore GitHub repo here: https://github.com/meshcore-dev/MeshCore/tree/main/logo

"},{"location":"faq/#75-q-what-is-the-format-of-a-contact-or-channel-qr-code","title":"7.5. Q: What is the format of a contact or channel QR code?","text":"

A:

Channel: meshcore://channel/add?name=<name>&secret=<secret>

Contact: meshcore://contact/add?name=<name>&public_key=<secret>&type=<type>

Where &type is:

"},{"location":"faq/#76-q-how-do-i-connect-to-the-companion-via-wi-fi-eg-using-a-heltec-v3","title":"7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?","text":"

A: Wi-Fi firmware requires you to compile it yourself, as you need to set the Wi-Fi SSID and password. Edit WIFI_SSID and WIFI_PWD in ./variants/heltec_v3/platformio.ini and then flash it to your device.

"},{"location":"faq/#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-an-ebyte-e22-900m30s-or-an-ebyte-e22-900m33s-module-what-should-their-transmit-power-be-set-to","title":"7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?","text":"

A: For companion radios, you can set these radios' transmit power in the smartphone app. For repeater and room server radios, you can set their transmit power using the command line command set tx. You can get their current value using command line command get tx

\u26a0\ufe0f WARNING: Set these values at your own risk. Incorrect power settings can permanently damage your radio hardware.

Device / Model Region / Description In-App Setting (dBm) Target Radio Output Notes Station G2 Reference US915 Max Output 19 dBm 36.5 dBm (4.46W) US915 Max at 1dB compression point 16 dBm 35 dBm (3.16W) 1dB compression point EU868 Max at 1dB compression point 15 dBm 34.5 dBm (2.82W) 1dB compression point US915 1W Output 10 dBm 1W Refer to your local government's requirements EU868 1W Output 9 dBm 1W Refer to your local government's requirements Ikoka Stick E22-900M30S 1W Model 19 dBm 1W DO NOT EXCEED (Risk of burn out) data sheet Ikoka Stick E22-900M33S 2W Model 9 dBm 2W DO NOT EXCEED (Risk of burn out) data sheet Refer to your local government's requirements Heltec V4 Standard Output 10 dBm 22 dBm (~0.15W) High Output 22 dBm 28 dBm (~0.5W to 0.6W)"},{"location":"kiss_modem_protocol/","title":"MeshCore KISS Modem Protocol","text":"

Standard KISS TNC firmware for MeshCore LoRa radios. Compatible with any KISS client (Direwolf, APRSdroid, YAAC, etc.) for sending and receiving raw packets. MeshCore-specific extensions (cryptography, radio configuration, telemetry) are available through the standard SetHardware (0x06) command.

"},{"location":"kiss_modem_protocol/#serial-configuration","title":"Serial Configuration","text":"

115200 baud, 8N1, no flow control.

"},{"location":"kiss_modem_protocol/#frame-format","title":"Frame Format","text":"

Standard KISS framing per the KA9Q/K3MC specification.

Byte Name Description 0xC0 FEND Frame delimiter 0xDB FESC Escape character 0xDC TFEND Escaped FEND (FESC + TFEND = 0xC0) 0xDD TFESC Escaped FESC (FESC + TFESC = 0xDB)
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 FEND \u2502 Type Byte \u2502 Data (escaped)\u2502 FEND \u2502\n\u2502 0xC0 \u2502  1 byte   \u2502 0-510 bytes  \u2502 0xC0 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"kiss_modem_protocol/#type-byte","title":"Type Byte","text":"

The type byte is split into two nibbles:

Bits Field Description 7-4 Port Port number (0 for single-port TNC) 3-0 Command Command number

Maximum unescaped frame size: 512 bytes.

"},{"location":"kiss_modem_protocol/#standard-kiss-commands","title":"Standard KISS Commands","text":""},{"location":"kiss_modem_protocol/#host-to-tnc","title":"Host to TNC","text":"Command Value Data Description Data 0x00 Raw packet Queue packet for transmission TXDELAY 0x01 Delay (1 byte) Transmitter keyup delay in 10ms units (default: 50 = 500ms) Persistence 0x02 P (1 byte) CSMA persistence parameter 0-255 (default: 63) SlotTime 0x03 Interval (1 byte) CSMA slot interval in 10ms units (default: 10 = 100ms) TXtail 0x04 Delay (1 byte) Post-TX hold time in 10ms units (default: 0) FullDuplex 0x05 Mode (1 byte) 0 = half duplex, nonzero = full duplex (default: 0) SetHardware 0x06 Sub-command + data MeshCore extensions (see below) Return 0xFF - Exit KISS mode (no-op)"},{"location":"kiss_modem_protocol/#tnc-to-host","title":"TNC to Host","text":"Type Value Data Description Data 0x00 Raw packet Received packet from radio

Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes.

"},{"location":"kiss_modem_protocol/#csma-behavior","title":"CSMA Behavior","text":"

The TNC implements p-persistent CSMA for half-duplex operation:

  1. When a packet is queued, monitor carrier detect
  2. When the channel clears, generate a random value 0-255
  3. If the value is less than or equal to P (Persistence), wait TXDELAY then transmit
  4. Otherwise, wait SlotTime and repeat from step 1

In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.

"},{"location":"kiss_modem_protocol/#sethardware-extensions-0x06","title":"SetHardware Extensions (0x06)","text":"

MeshCore-specific functionality uses the standard KISS SetHardware command. The first byte of SetHardware data is a sub-command. Standard KISS clients ignore these frames.

"},{"location":"kiss_modem_protocol/#frame-format_1","title":"Frame Format","text":"
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 FEND \u2502 0x06 \u2502 Sub-command  \u2502 Data (escaped)\u2502 FEND \u2502\n\u2502 0xC0 \u2502      \u2502   1 byte    \u2502   variable   \u2502 0xC0 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"kiss_modem_protocol/#request-sub-commands-host-to-tnc","title":"Request Sub-commands (Host to TNC)","text":"Sub-command Value Data GetIdentity 0x01 - GetRandom 0x02 Length (1 byte, 1-64) VerifySignature 0x03 PubKey (32) + Signature (64) + Data SignData 0x04 Data to sign EncryptData 0x05 Key (32) + Plaintext DecryptData 0x06 Key (32) + MAC (2) + Ciphertext KeyExchange 0x07 Remote PubKey (32) Hash 0x08 Data to hash SetRadio 0x09 Freq (4) + BW (4) + SF (1) + CR (1) SetTxPower 0x0A Power dBm (1) GetRadio 0x0B - GetTxPower 0x0C - GetCurrentRssi 0x0D - IsChannelBusy 0x0E - GetAirtime 0x0F Packet length (1) GetNoiseFloor 0x10 - GetVersion 0x11 - GetStats 0x12 - GetBattery 0x13 - GetMCUTemp 0x14 - GetSensors 0x15 Permissions (1) GetDeviceName 0x16 - Ping 0x17 - Reboot 0x18 - SetSignalReport 0x19 Enable (1): 0x00=disable, nonzero=enable GetSignalReport 0x1A -"},{"location":"kiss_modem_protocol/#response-sub-commands-tnc-to-host","title":"Response Sub-commands (TNC to Host)","text":"

Response codes use the high-bit convention: response = command | 0x80. Generic and unsolicited responses use the 0xF0+ range.

Sub-command Value Data Identity 0x81 PubKey (32) Random 0x82 Random bytes (1-64) Verify 0x83 Result (1): 0x00=invalid, 0x01=valid Signature 0x84 Signature (64) Encrypted 0x85 MAC (2) + Ciphertext Decrypted 0x86 Plaintext SharedSecret 0x87 Shared secret (32) Hash 0x88 SHA-256 hash (32) Radio 0x8B Freq (4) + BW (4) + SF (1) + CR (1) TxPower 0x8C Power dBm (1) CurrentRssi 0x8D RSSI dBm (1, signed) ChannelBusy 0x8E Result (1): 0x00=clear, 0x01=busy Airtime 0x8F Milliseconds (4) NoiseFloor 0x90 dBm (2, signed) Version 0x91 Version (1) + Reserved (1) Stats 0x92 RX (4) + TX (4) + Errors (4) Battery 0x93 Millivolts (2) MCUTemp 0x94 Temperature (2, signed) Sensors 0x95 CayenneLPP payload DeviceName 0x96 Name (variable, UTF-8) Pong 0x97 - SignalReport 0x9A Status (1): 0x00=disabled, 0x01=enabled OK 0xF0 - Error 0xF1 Error code (1) TxDone 0xF8 Result (1): 0x00=failed, 0x01=success RxMeta 0xF9 SNR (1) + RSSI (1)"},{"location":"kiss_modem_protocol/#error-codes","title":"Error Codes","text":"Code Value Description InvalidLength 0x01 Request data too short InvalidParam 0x02 Invalid parameter value NoCallback 0x03 Feature not available MacFailed 0x04 MAC verification failed UnknownCmd 0x05 Unknown sub-command EncryptFailed 0x06 Encryption failed TxBusy 0x07 Transmit busy"},{"location":"kiss_modem_protocol/#unsolicited-events","title":"Unsolicited Events","text":"

The TNC sends these SetHardware frames without a preceding request:

TxDone (0xF8): Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure.

RxMeta (0xF9): Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Enabled by default; can be toggled with SetSignalReport. Standard KISS clients ignore this frame.

"},{"location":"kiss_modem_protocol/#data-formats","title":"Data Formats","text":""},{"location":"kiss_modem_protocol/#radio-parameters-setradio-radio-response","title":"Radio Parameters (SetRadio / Radio response)","text":"

All values little-endian.

Field Size Description Frequency 4 bytes Hz (e.g., 869618000) Bandwidth 4 bytes Hz (e.g., 62500) SF 1 byte Spreading factor (5-12) CR 1 byte Coding rate (5-8)"},{"location":"kiss_modem_protocol/#version-version-response","title":"Version (Version response)","text":"Field Size Description Version 1 byte Firmware version Reserved 1 byte Always 0"},{"location":"kiss_modem_protocol/#encrypted-encrypted-response","title":"Encrypted (Encrypted response)","text":"Field Size Description MAC 2 bytes HMAC-SHA256 truncated to 2 bytes Ciphertext variable AES-128 block-encrypted data with zero padding"},{"location":"kiss_modem_protocol/#airtime-airtime-response","title":"Airtime (Airtime response)","text":"

All values little-endian.

Field Size Description Airtime 4 bytes uint32_t, estimated air time in milliseconds"},{"location":"kiss_modem_protocol/#noise-floor-noisefloor-response","title":"Noise Floor (NoiseFloor response)","text":"

All values little-endian.

Field Size Description Noise floor 2 bytes int16_t, dBm (signed)

The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.

"},{"location":"kiss_modem_protocol/#stats-stats-response","title":"Stats (Stats response)","text":"

All values little-endian.

Field Size Description RX 4 bytes Packets received TX 4 bytes Packets transmitted Errors 4 bytes Receive errors"},{"location":"kiss_modem_protocol/#battery-battery-response","title":"Battery (Battery response)","text":"

All values little-endian.

Field Size Description Millivolts 2 bytes uint16_t, battery voltage in mV"},{"location":"kiss_modem_protocol/#mcu-temperature-mcutemp-response","title":"MCU Temperature (MCUTemp response)","text":"

All values little-endian.

Field Size Description Temperature 2 bytes int16_t, tenths of \u00b0C (e.g., 253 = 25.3\u00b0C)

Returns NoCallback error if the board does not support temperature readings.

"},{"location":"kiss_modem_protocol/#device-name-devicename-response","title":"Device Name (DeviceName response)","text":"Field Size Description Name variable UTF-8 string, no null terminator"},{"location":"kiss_modem_protocol/#reboot","title":"Reboot","text":"

Sends an OK response, flushes serial, then reboots the device. The host should expect the connection to drop.

"},{"location":"kiss_modem_protocol/#sensor-permissions-getsensors","title":"Sensor Permissions (GetSensors)","text":"Bit Value Description 0 0x01 Base (battery) 1 0x02 Location (GPS) 2 0x04 Environment (temp, humidity, pressure)

Use 0x07 for all permissions.

"},{"location":"kiss_modem_protocol/#sensor-data-sensors-response","title":"Sensor Data (Sensors response)","text":"

Data returned in CayenneLPP format. See CayenneLPP documentation for parsing.

"},{"location":"kiss_modem_protocol/#cryptographic-algorithms","title":"Cryptographic Algorithms","text":"Operation Algorithm Identity / Signing / Verification Ed25519 Key Exchange X25519 (ECDH) Encryption AES-128 block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) Hashing SHA-256"},{"location":"kiss_modem_protocol/#notes","title":"Notes","text":""},{"location":"nrf52_power_management/","title":"nRF52 Power Management","text":""},{"location":"nrf52_power_management/#overview","title":"Overview","text":"

The nRF52 Power Management module provides battery protection features to prevent over-discharge, minimise likelihood of brownout and flash corruption conditions existing, and enable safe voltage-based recovery.

"},{"location":"nrf52_power_management/#features","title":"Features","text":""},{"location":"nrf52_power_management/#boot-voltage-protection","title":"Boot Voltage Protection","text":""},{"location":"nrf52_power_management/#voltage-wake-lpcomp-vbus","title":"Voltage Wake (LPCOMP + VBUS)","text":""},{"location":"nrf52_power_management/#early-boot-register-capture","title":"Early Boot Register Capture","text":""},{"location":"nrf52_power_management/#shutdown-reason-tracking","title":"Shutdown Reason Tracking","text":"

Shutdown reason codes (stored in GPREGRET2):

Code Name Description 0x00 NONE Normal boot / no previous shutdown 0x4C LOW_VOLTAGE Runtime low voltage threshold reached 0x55 USER User requested powerOff() 0x42 BOOT_PROTECT Boot voltage protection triggered"},{"location":"nrf52_power_management/#supported-boards","title":"Supported Boards","text":"Board Implemented LPCOMP wake VBUS wake Seeed Studio XIAO nRF52840 (xiao_nrf52) Yes Yes Yes RAK4631 (rak4631) Yes Yes Yes Heltec T114 (heltec_t114) Yes Yes Yes GAT562 Mesh Watch13 Yes Yes Yes Promicro nRF52840 No No No RAK WisMesh Tag No No No Heltec Mesh Solar No No No LilyGo T-Echo / T-Echo Lite No No No SenseCAP Solar Yes Yes Yes WIO Tracker L1 / L1 E-Ink No No No WIO WM1110 No No No Mesh Pocket No No No Nano G2 Ultra No No No ThinkNode M1/M3/M6 No No No T1000-E No No No Ikoka Nano/Stick/Handheld (nRF) No No No Keepteen LT1 No No No Minewsemi ME25LS01 No No No

Notes: - \"Implemented\" reflects Phase 1 (boot lockout + shutdown reason capture). - User power-off on Heltec T114 does not enable LPCOMP wake. - VBUS detection is used to skip boot lockout on external power, and VBUS wake is configured alongside LPCOMP when supported hardware exposes VBUS to the nRF52.

"},{"location":"nrf52_power_management/#technical-details","title":"Technical Details","text":""},{"location":"nrf52_power_management/#architecture","title":"Architecture","text":"

The power management functionality is integrated into the NRF52Board base class in src/helpers/NRF52Board.cpp. Board variants provide hardware-specific configuration via a PowerMgtConfig struct and override initiateShutdown(uint8_t reason) to perform board-specific power-down work and conditionally enable voltage wake (LPCOMP + VBUS).

"},{"location":"nrf52_power_management/#early-boot-capture","title":"Early Boot Capture","text":"

A static constructor with priority 101 in NRF52Board.cpp captures the RESETREAS and GPREGRET2 registers before: - SystemInit() (priority 102) - which clears RESETREAS - Static C++ constructors (default priority 65535)

This ensures we capture the true reset reason before any initialisation code runs.

"},{"location":"nrf52_power_management/#board-implementation","title":"Board Implementation","text":"

To enable power management on a board variant:

  1. Enable in platformio.ini: ini -D NRF52_POWER_MANAGEMENT

  2. Define configuration in variant.h: c #define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV) #define PWRMGT_LPCOMP_AIN 7 // AIN channel for voltage sensing #define PWRMGT_LPCOMP_REFSEL 2 // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)

  3. Implement in board .cpp file: ```cpp #ifdef NRF52_POWER_MANAGEMENT const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK };

void MyBoard::initiateShutdown(uint8_t reason) { // Board-specific shutdown preparation (e.g., disable peripherals) bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT);

 if (enable_lpcomp) {\n   configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);\n }\n\n enterSystemOff(reason);\n

} #endif

void MyBoard::begin() { NRF52Board::begin(); // or NRF52BoardDCDC::begin() // ... board setup ...

#ifdef NRF52_POWER_MANAGEMENT checkBootVoltage(&power_config); #endif } ```

For user-initiated shutdowns, powerOff() remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).

  1. Declare override in board .h file: cpp #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif
"},{"location":"nrf52_power_management/#voltage-wake-configuration","title":"Voltage Wake Configuration","text":"

The LPCOMP (Low Power Comparator) is configured to: - Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31) - Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16) - Detect UP events (voltage rising above threshold) - Use 50mV hysteresis for noise immunity - Wake the device from SYSTEMOFF when triggered

VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever configureVoltageWake() is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).

LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL):

REFSEL Fraction VBAT @ 1M/1M divider (VDD=3.0-3.3) VBAT @ 1.5M/1M divider (VDD=3.0-3.3) 0 1/8 0.75-0.82 V 0.94-1.03 V 1 2/8 1.50-1.65 V 1.88-2.06 V 2 3/8 2.25-2.47 V 2.81-3.09 V 3 4/8 3.00-3.30 V 3.75-4.12 V 4 5/8 3.75-4.12 V 4.69-5.16 V 5 6/8 4.50-4.95 V 5.62-6.19 V 6 7/8 5.25-5.77 V 6.56-7.22 V 7 ARef - - 8 1/16 0.38-0.41 V 0.47-0.52 V 9 3/16 1.12-1.24 V 1.41-1.55 V 10 5/16 1.88-2.06 V 2.34-2.58 V 11 7/16 2.62-2.89 V 3.28-3.61 V 12 9/16 3.38-3.71 V 4.22-4.64 V 13 11/16 4.12-4.54 V 5.16-5.67 V 14 13/16 4.88-5.36 V 6.09-6.70 V 15 15/16 5.62-6.19 V 7.03-7.73 V

Important: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use: VBAT_threshold \u2248 (VDD * fraction) * divider_scale, where divider_scale = (Rtop + Rbottom) / Rbottom (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).

"},{"location":"nrf52_power_management/#softdevice-compatibility","title":"SoftDevice Compatibility","text":"

The power management code checks whether SoftDevice is enabled and uses the appropriate API: - When SD enabled: sd_power_* functions - When SD disabled: Direct register access (NRF_POWER->*)

This ensures compatibility regardless of BLE stack state.

"},{"location":"nrf52_power_management/#cli-commands","title":"CLI Commands","text":"

Power management status can be queried via the CLI:

Command Description get pwrmgt.support Returns \"supported\" or \"unsupported\" get pwrmgt.source Returns current power source - \"battery\" or \"external\" (5V/USB power) get pwrmgt.bootreason Returns reset and shutdown reason strings get pwrmgt.bootmv Returns boot voltage in millivolts

On boards without power management enabled, all commands except get pwrmgt.support return:

ERROR: Power management not supported\n
"},{"location":"nrf52_power_management/#debug-output","title":"Debug Output","text":"

When MESH_DEBUG=1 is enabled, the power management module outputs:

DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)\nDEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)\nDEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)\n
"},{"location":"nrf52_power_management/#phase-2-planned","title":"Phase 2 (Planned)","text":""},{"location":"nrf52_power_management/#references","title":"References","text":""},{"location":"number_allocations/","title":"Number Allocations","text":"

This document lists unique numbers/identifiers used in various MeshCore protocol payloads.

"},{"location":"number_allocations/#group-data-types","title":"Group Data Types","text":"

The PAYLOAD_TYPE_GRP_DATA payloads have a 16-bit data-type field, which identifies which application the packet is for.

To make sure multiple applications can function without interfering with each other, the table below is for reserving various ranges of data-type values. Just modify this table, adding a row, then submit a PR to have it authorised/merged.

NOTE: the range FF00 - FFFF is for use while you're developing, doing POC, and for these you don't need to request to use/allocate.

Once you have a working app/project, you need to be able to demonstrate it exists/works, and THEN request type IDs. So, just use the testing/dev range while developing, then request IDs before you transition to publishing your project.

Data-Type range App name Contact 0000 - 00FF -reserved for internal use- 0100 MeshCore Open zsylvester@monitormx.com \u2014 https://github.com/zjs81/meshcore-open 0110 - 011F Ripple ripple_biz@protonmail.com \u2014 https://buymeacoffee.com/ripplebiz FF00 - FFFF -reserved for testing/dev-

(add rows, inside the range 0100 - FEFF for custom apps)

"},{"location":"packet_format/","title":"Packet Format","text":"

This document describes the MeshCore packet format.

"},{"location":"packet_format/#version-1-packet-format","title":"Version 1 Packet Format","text":"

This is the protocol level packet structure used in MeshCore firmware v1.12.0

[header][transport_codes(optional)][path_length][path][payload]\n
"},{"location":"packet_format/#packet-format_1","title":"Packet Format","text":"Field Size (bytes) Description header 1 Contains routing type, payload type, and payload version transport_codes 4 (optional) 2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*) path_length 1 Encodes path hash size in bits 6-7 and hop count in bits 0-5 path up to 64 (MAX_PATH_SIZE) Stores hop_count * hash_size bytes of path data if applicable payload up to 184 (MAX_PACKET_PAYLOAD) Data for the provided Payload Type

NOTE: see the Payloads documentation for more information about the content of specific payload types.

"},{"location":"packet_format/#header-format","title":"Header Format","text":"

Bit 0 means the lowest bit (1s place)

Bits Mask Field Description 0-1 0x03 Route Type Flood, Direct, etc 2-5 0x3C Payload Type Request, Response, ACK, etc 6-7 0xC0 Payload Version Versioning of the payload format"},{"location":"packet_format/#route-types","title":"Route Types","text":"Value Name Description 0x00 ROUTE_TYPE_TRANSPORT_FLOOD Flood Routing + Transport Codes 0x01 ROUTE_TYPE_FLOOD Flood Routing 0x02 ROUTE_TYPE_DIRECT Direct Routing 0x03 ROUTE_TYPE_TRANSPORT_DIRECT Direct Routing + Transport Codes"},{"location":"packet_format/#path-length-encoding","title":"Path Length Encoding","text":"

path_length is not a raw byte count. It packs both hash size and hop count:

Bits Field Meaning 0-5 Hop Count Number of path hashes (0-63) 6-7 Hash Size Code Stored as hash_size - 1

Hash size codes:

Bits 6-7 Hash Size Notes 0b00 1 byte Legacy / default mode 0b01 2 bytes Supported in current firmware 0b10 3 bytes Supported in current firmware 0b11 4 bytes Reserved / invalid

Examples:

"},{"location":"packet_format/#payload-types","title":"Payload Types","text":"Value Name Description 0x00 PAYLOAD_TYPE_REQ Request (destination/source hashes + MAC) 0x01 PAYLOAD_TYPE_RESPONSE Response to REQ or ANON_REQ 0x02 PAYLOAD_TYPE_TXT_MSG Plain text message 0x03 PAYLOAD_TYPE_ACK Acknowledgment 0x04 PAYLOAD_TYPE_ADVERT Node advertisement 0x05 PAYLOAD_TYPE_GRP_TXT Group text message (unverified) 0x06 PAYLOAD_TYPE_GRP_DATA Group datagram (unverified) 0x07 PAYLOAD_TYPE_ANON_REQ Anonymous request 0x08 PAYLOAD_TYPE_PATH Returned path 0x09 PAYLOAD_TYPE_TRACE Trace a path, collecting SNR for each hop 0x0A PAYLOAD_TYPE_MULTIPART Packet is part of a sequence of packets 0x0B PAYLOAD_TYPE_CONTROL Control packet data (unencrypted) 0x0C reserved reserved 0x0D reserved reserved 0x0E reserved reserved 0x0F PAYLOAD_TYPE_RAW_CUSTOM Custom packet (raw bytes, custom encryption)"},{"location":"packet_format/#payload-versions","title":"Payload Versions","text":"Value Version Description 0x00 1 1-byte src/dest hashes, 2-byte MAC 0x01 2 Future version (e.g., 2-byte hashes, 4-byte MAC) 0x02 3 Future version 0x03 4 Future version"},{"location":"payloads/","title":"Payload Format","text":"

Inside each MeshCore Packet is a payload, identified by the payload type in the packet header. The types of payloads are:

This document defines the structure of each of these payload types.

NOTE: all 16 and 32-bit integer fields are Little Endian.

"},{"location":"payloads/#important-concepts","title":"Important concepts:","text":""},{"location":"payloads/#node-advertisement","title":"Node advertisement","text":"

This kind of payload notifies receivers that a node exists, and gives information about the node

Field Size (bytes) Description public key 32 Ed25519 public key of the node timestamp 4 unix timestamp of advertisement signature 64 Ed25519 signature of public key, timestamp, and app data appdata rest of payload optional, see below

Appdata

Field Size (bytes) Description flags 1 specifies which of the fields are present, see below latitude 4 (optional) decimal latitude multiplied by 1000000, integer longitude 4 (optional) decimal longitude multiplied by 1000000, integer feature 1 2 (optional) reserved for future use feature 2 2 (optional) reserved for future use name rest of appdata name of the node

Appdata Flags

Value Name Description 0x01 is chat node advert is for a chat node 0x02 is repeater advert is for a repeater 0x03 is room server advert is for a room server 0x04 is sensor advert is for a sensor server 0x10 has location appdata contains lat/long information 0x20 has feature 1 Reserved for future use. 0x40 has feature 2 Reserved for future use. 0x80 has name appdata contains a node name"},{"location":"payloads/#acknowledgement","title":"Acknowledgement","text":"

An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the \"extra\" payload (see Returned Path) instead of as a separate acknowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra.

Field Size (bytes) Description checksum 4 CRC checksum of message timestamp, text, and sender pubkey"},{"location":"payloads/#returned-path-request-response-and-plain-text-message","title":"Returned path, request, response, and plain text message","text":"

Returned path, request, response, and plain text messages are all formatted in the same way. See the subsection for more details about the ciphertext's associated plaintext representation.

Field Size (bytes) Description destination hash 1 first byte of destination node public key source hash 1 first byte of source node public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see subsections below for details"},{"location":"payloads/#returned-path","title":"Returned path","text":"

Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message.

Field Size (bytes) Description path length 1 length of next field path see above a list of node hashes (one byte each) extra type 1 extra, bundled payload type, eg., acknowledgement or response. Same values as in Packet Format extra rest of data extra, bundled payload content, follows same format as main content defined by this document"},{"location":"payloads/#request","title":"Request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) request data rest of payload application-defined request payload body

For the common chat/server helpers in BaseChatMesh, the current request type values are:

Value Name Description 0x01 get stats get stats of repeater or room server 0x02 keepalive keep-alive request used for maintained connections"},{"location":"payloads/#get-stats","title":"Get stats","text":"

Gets information about the node, possibly including the following:

"},{"location":"payloads/#get-telemetry-data","title":"Get telemetry data","text":"

Not defined in BaseChatMesh. Sensor- and application-specific request payloads may be implemented by higher-level firmware.

"},{"location":"payloads/#get-telemetry","title":"Get Telemetry","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#get-minmaxave-sensor-nodes","title":"Get Min/Max/Ave (Sensor nodes)","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#get-access-list","title":"Get Access List","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#get-neighbors","title":"Get Neighbors","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#get-owner-info","title":"Get Owner Info","text":"

Not defined in BaseChatMesh.

"},{"location":"payloads/#response","title":"Response","text":"Field Size (bytes) Description content rest of payload application-defined response body

Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above.

"},{"location":"payloads/#plain-text-message","title":"Plain text message","text":"Field Size (bytes) Description timestamp 4 send time (unix timestamp) txt_type + attempt 1 upper six bits are txt_type (see below), lower two bits are attempt number (0..3) message rest of payload the message content, see next table

txt_type

Value Description Message content 0x00 plain text message the plain text of the message 0x01 CLI command the command text of the message 0x02 signed plain text message first four bytes is sender pubkey prefix, followed by plain text message"},{"location":"payloads/#anonymous-request","title":"Anonymous request","text":"Field Size (bytes) Description destination hash 1 first byte of destination node public key public key 32 sender's Ed25519 public key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details"},{"location":"payloads/#room-server-login","title":"Room server login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) sync timestamp 4 sender's \"sync messages SINCE x\" timestamp password rest of message password for room"},{"location":"payloads/#repeatersensor-login","title":"Repeater/Sensor login","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) password rest of message password for repeater/sensor"},{"location":"payloads/#repeater-regions-request","title":"Repeater - Regions request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x01 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-owner-info-request","title":"Repeater - Owner info request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x02 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#repeater-clock-and-status-request","title":"Repeater - Clock and status request","text":"Field Size (bytes) Description timestamp 4 sender time (unix timestamp) req type 1 0x03 (request sub type) reply path len 1 path len for reply reply path (variable) reply path"},{"location":"payloads/#group-text-message","title":"Group text message","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted message, see below for details

The plaintext contained in the ciphertext matches the format described in plain text message. Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be 0x00 because it is a \"plain text message\". The message will be of the form <sender name>: <message body> (eg., user123: I'm on my way).

"},{"location":"payloads/#group-datagram","title":"Group datagram","text":"Field Size (bytes) Description channel hash 1 first byte of SHA256 of channel's shared key cipher MAC 2 MAC for encrypted data in next field ciphertext rest of payload encrypted data, see below for details

The data contained in the ciphertext uses the format below:

Field Size (bytes) Description data type 2 Identifier for type of data. (See number_allocations.md) data len 1 byte length of data data rest of payload (depends on data type)"},{"location":"payloads/#control-data","title":"Control data","text":"Field Size (bytes) Description flags 1 upper 4 bits is sub_type data rest of payload typically unencrypted data"},{"location":"payloads/#discover_req-sub_type","title":"DISCOVER_REQ (sub_type)","text":"Field Size (bytes) Description flags 1 0x8 (upper 4 bits), prefix_only (lowest bit) type_filter 1 bit for each ADV_TYPE_* tag 4 randomly generate by sender since 4 (optional) epoch timestamp (0 by default)"},{"location":"payloads/#discover_resp-sub_type","title":"DISCOVER_RESP (sub_type)","text":"Field Size (bytes) Description flags 1 0x9 (upper 4 bits), node_type (lower 4) snr 1 signed, SNR*4 tag 4 reflected back from DISCOVER_REQ pubkey 8 or 32 node's ID (or prefix)"},{"location":"payloads/#custom-packet","title":"Custom packet","text":"

Custom packets have no defined format.

"},{"location":"qr_codes/","title":"QR Codes","text":"

This document provides an overview of QR Code formats that can be used for sharing MeshCore channels and contacts. The formats described below are supported by the MeshCore mobile app.

"},{"location":"qr_codes/#add-channel","title":"Add Channel","text":"

Example URL:

meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72\n

Parameters:

"},{"location":"qr_codes/#add-contact","title":"Add Contact","text":"

Example URL:

meshcore://contact/add?name=Example+Contact&public_key=9cd8fcf22a47333b591d96a2b848b73f457b1bb1a3ea2453a885f9e5787765b1&type=1\n

Parameters:

"},{"location":"stats_binary_frames/","title":"Stats Binary Frame Structures","text":"

Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.

"},{"location":"stats_binary_frames/#command-codes","title":"Command Codes","text":"Command Code Description CMD_GET_STATS 56 Get statistics (2-byte command: code + sub-type)"},{"location":"stats_binary_frames/#stats-sub-types","title":"Stats Sub-Types","text":"

The CMD_GET_STATS command uses a 2-byte frame structure: - Byte 0: CMD_GET_STATS (56) - Byte 1: Stats sub-type: - STATS_TYPE_CORE (0) - Get core device statistics - STATS_TYPE_RADIO (1) - Get radio statistics - STATS_TYPE_PACKETS (2) - Get packet statistics

"},{"location":"stats_binary_frames/#response-codes","title":"Response Codes","text":"Response Code Description RESP_CODE_STATS 24 Statistics response (2-byte response: code + sub-type)"},{"location":"stats_binary_frames/#stats-response-sub-types","title":"Stats Response Sub-Types","text":"

The RESP_CODE_STATS response uses a 2-byte header structure: - Byte 0: RESP_CODE_STATS (24) - Byte 1: Stats sub-type (matches command sub-type): - STATS_TYPE_CORE (0) - Core device statistics response - STATS_TYPE_RADIO (1) - Radio statistics response - STATS_TYPE_PACKETS (2) - Packet statistics response

"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_core-24-0","title":"RESP_CODE_STATS + STATS_TYPE_CORE (24, 0)","text":"

Total Frame Size: 11 bytes

Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always 0x18 (24) - 1 1 uint8_t stats_type Always 0x00 (STATS_TYPE_CORE) - 2 2 uint16_t battery_mv Battery voltage in millivolts 0 - 65,535 4 4 uint32_t uptime_secs Device uptime in seconds 0 - 4,294,967,295 8 2 uint16_t errors Error flags bitmask - 10 1 uint8_t queue_len Outbound packet queue length 0 - 255"},{"location":"stats_binary_frames/#example-structure-cc","title":"Example Structure (C/C++)","text":"
struct StatsCore {\n    uint8_t  response_code;  // 0x18\n    uint8_t  stats_type;     // 0x00 (STATS_TYPE_CORE)\n    uint16_t battery_mv;\n    uint32_t uptime_secs;\n    uint16_t errors;\n    uint8_t  queue_len;\n} __attribute__((packed));\n
"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_radio-24-1","title":"RESP_CODE_STATS + STATS_TYPE_RADIO (24, 1)","text":"

Total Frame Size: 14 bytes

Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always 0x18 (24) - 1 1 uint8_t stats_type Always 0x01 (STATS_TYPE_RADIO) - 2 2 int16_t noise_floor Radio noise floor in dBm -140 to +10 4 1 int8_t last_rssi Last received signal strength in dBm -128 to +127 5 1 int8_t last_snr SNR scaled by 4 Divide by 4.0 for dB 6 4 uint32_t tx_air_secs Cumulative transmit airtime in seconds 0 - 4,294,967,295 10 4 uint32_t rx_air_secs Cumulative receive airtime in seconds 0 - 4,294,967,295"},{"location":"stats_binary_frames/#example-structure-cc_1","title":"Example Structure (C/C++)","text":"
struct StatsRadio {\n    uint8_t  response_code;  // 0x18\n    uint8_t  stats_type;     // 0x01 (STATS_TYPE_RADIO)\n    int16_t  noise_floor;\n    int8_t   last_rssi;\n    int8_t   last_snr;       // Divide by 4.0 to get actual SNR in dB\n    uint32_t tx_air_secs;\n    uint32_t rx_air_secs;\n} __attribute__((packed));\n
"},{"location":"stats_binary_frames/#resp_code_stats-stats_type_packets-24-2","title":"RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)","text":"

Total Frame Size: 26 bytes (legacy) or 30 bytes (includes recv_errors)

Offset Size Type Field Name Description Range/Notes 0 1 uint8_t response_code Always 0x18 (24) - 1 1 uint8_t stats_type Always 0x02 (STATS_TYPE_PACKETS) - 2 4 uint32_t recv Total packets received 0 - 4,294,967,295 6 4 uint32_t sent Total packets sent 0 - 4,294,967,295 10 4 uint32_t flood_tx Packets sent via flood routing 0 - 4,294,967,295 14 4 uint32_t direct_tx Packets sent via direct routing 0 - 4,294,967,295 18 4 uint32_t flood_rx Packets received via flood routing 0 - 4,294,967,295 22 4 uint32_t direct_rx Packets received via direct routing 0 - 4,294,967,295 26 4 uint32_t recv_errors Receive/CRC errors (RadioLib); present only in 30-byte frame 0 - 4,294,967,295"},{"location":"stats_binary_frames/#notes","title":"Notes","text":""},{"location":"stats_binary_frames/#example-structure-cc_2","title":"Example Structure (C/C++)","text":"
struct StatsPackets {\n    uint8_t  response_code;  // 0x18\n    uint8_t  stats_type;     // 0x02 (STATS_TYPE_PACKETS)\n    uint32_t recv;\n    uint32_t sent;\n    uint32_t flood_tx;\n    uint32_t direct_tx;\n    uint32_t flood_rx;\n    uint32_t direct_rx;\n    uint32_t recv_errors;    // present when frame size is 30\n} __attribute__((packed));\n
"},{"location":"stats_binary_frames/#command-usage-example-python","title":"Command Usage Example (Python)","text":"
# Send CMD_GET_STATS command\ndef send_get_stats_core(serial_interface):\n    \"\"\"Send command to get core stats\"\"\"\n    cmd = bytes([56, 0])  # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)\n    serial_interface.write(cmd)\n\ndef send_get_stats_radio(serial_interface):\n    \"\"\"Send command to get radio stats\"\"\"\n    cmd = bytes([56, 1])  # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)\n    serial_interface.write(cmd)\n\ndef send_get_stats_packets(serial_interface):\n    \"\"\"Send command to get packet stats\"\"\"\n    cmd = bytes([56, 2])  # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)\n    serial_interface.write(cmd)\n
"},{"location":"stats_binary_frames/#response-parsing-example-python","title":"Response Parsing Example (Python)","text":"
import struct\n\ndef parse_stats_core(frame):\n    \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_CORE frame (11 bytes)\"\"\"\n    response_code, stats_type, battery_mv, uptime_secs, errors, queue_len = \\\n        struct.unpack('<B B H I H B', frame)\n    assert response_code == 24 and stats_type == 0, \"Invalid response type\"\n    return {\n        'battery_mv': battery_mv,\n        'uptime_secs': uptime_secs,\n        'errors': errors,\n        'queue_len': queue_len\n    }\n\ndef parse_stats_radio(frame):\n    \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_RADIO frame (14 bytes)\"\"\"\n    response_code, stats_type, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs = \\\n        struct.unpack('<B B h b b I I', frame)\n    assert response_code == 24 and stats_type == 1, \"Invalid response type\"\n    return {\n        'noise_floor': noise_floor,\n        'last_rssi': last_rssi,\n        'last_snr': last_snr / 4.0,  # Unscale SNR\n        'tx_air_secs': tx_air_secs,\n        'rx_air_secs': rx_air_secs\n    }\n\ndef parse_stats_packets(frame):\n    \"\"\"Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 bytes)\"\"\"\n    assert len(frame) >= 26, \"STATS_TYPE_PACKETS frame too short\"\n    response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \\\n        struct.unpack('<B B I I I I I I', frame[:26])\n    assert response_code == 24 and stats_type == 2, \"Invalid response type\"\n    result = {\n        'recv': recv,\n        'sent': sent,\n        'flood_tx': flood_tx,\n        'direct_tx': direct_tx,\n        'flood_rx': flood_rx,\n        'direct_rx': direct_rx\n    }\n    if len(frame) >= 30:\n        (recv_errors,) = struct.unpack('<I', frame[26:30])\n        result['recv_errors'] = recv_errors\n    return result\n
"},{"location":"stats_binary_frames/#command-usage-example-javascripttypescript","title":"Command Usage Example (JavaScript/TypeScript)","text":"
// Send CMD_GET_STATS command\nconst CMD_GET_STATS = 56;\nconst STATS_TYPE_CORE = 0;\nconst STATS_TYPE_RADIO = 1;\nconst STATS_TYPE_PACKETS = 2;\n\nfunction sendGetStatsCore(serialInterface: SerialPort): void {\n    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_CORE]);\n    serialInterface.write(cmd);\n}\n\nfunction sendGetStatsRadio(serialInterface: SerialPort): void {\n    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_RADIO]);\n    serialInterface.write(cmd);\n}\n\nfunction sendGetStatsPackets(serialInterface: SerialPort): void {\n    const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_PACKETS]);\n    serialInterface.write(cmd);\n}\n
"},{"location":"stats_binary_frames/#response-parsing-example-javascripttypescript","title":"Response Parsing Example (JavaScript/TypeScript)","text":"
interface StatsCore {\n    battery_mv: number;\n    uptime_secs: number;\n    errors: number;\n    queue_len: number;\n}\n\ninterface StatsRadio {\n    noise_floor: number;\n    last_rssi: number;\n    last_snr: number;\n    tx_air_secs: number;\n    rx_air_secs: number;\n}\n\ninterface StatsPackets {\n    recv: number;\n    sent: number;\n    flood_tx: number;\n    direct_tx: number;\n    flood_rx: number;\n    direct_rx: number;\n    recv_errors?: number;  // present when frame is 30 bytes\n}\n\nfunction parseStatsCore(buffer: ArrayBuffer): StatsCore {\n    const view = new DataView(buffer);\n    const response_code = view.getUint8(0);\n    const stats_type = view.getUint8(1);\n    if (response_code !== 24 || stats_type !== 0) {\n        throw new Error('Invalid response type');\n    }\n    return {\n        battery_mv: view.getUint16(2, true),\n        uptime_secs: view.getUint32(4, true),\n        errors: view.getUint16(8, true),\n        queue_len: view.getUint8(10)\n    };\n}\n\nfunction parseStatsRadio(buffer: ArrayBuffer): StatsRadio {\n    const view = new DataView(buffer);\n    const response_code = view.getUint8(0);\n    const stats_type = view.getUint8(1);\n    if (response_code !== 24 || stats_type !== 1) {\n        throw new Error('Invalid response type');\n    }\n    return {\n        noise_floor: view.getInt16(2, true),\n        last_rssi: view.getInt8(4),\n        last_snr: view.getInt8(5) / 4.0,  // Unscale SNR\n        tx_air_secs: view.getUint32(6, true),\n        rx_air_secs: view.getUint32(10, true)\n    };\n}\n\nfunction parseStatsPackets(buffer: ArrayBuffer): StatsPackets {\n    const view = new DataView(buffer);\n    if (buffer.byteLength < 26) {\n        throw new Error('STATS_TYPE_PACKETS frame too short');\n    }\n    const response_code = view.getUint8(0);\n    const stats_type = view.getUint8(1);\n    if (response_code !== 24 || stats_type !== 2) {\n        throw new Error('Invalid response type');\n    }\n    const result: StatsPackets = {\n        recv: view.getUint32(2, true),\n        sent: view.getUint32(6, true),\n        flood_tx: view.getUint32(10, true),\n        direct_tx: view.getUint32(14, true),\n        flood_rx: view.getUint32(18, true),\n        direct_rx: view.getUint32(22, true)\n    };\n    if (buffer.byteLength >= 30) {\n        result.recv_errors = view.getUint32(26, true);\n    }\n    return result;\n}\n
"},{"location":"stats_binary_frames/#field-size-considerations","title":"Field Size Considerations","text":""},{"location":"terminal_chat_cli/","title":"Terminal Chat CLI","text":"

Below are the commands you can enter into the Terminal Chat clients:

set freq {frequency}\n

Set the LoRa frequency. Example: set freq 915.8

set tx {tx-power-dbm}\n

Sets LoRa transmit power in dBm.

set name {name}\n

Sets your advertisement name.

set lat {latitude}\n

Sets your advertisement map latitude. (decimal degrees)

set lon {longitude}\n

Sets your advertisement map longitude. (decimal degrees)

set dutycycle {percent}\n

Sets the transmit duty cycle limit (1-100%). Example: set dutycycle 10 for 10%.

set af {air-time-factor}\n

Sets the transmit air-time-factor. Deprecated \u2014 use set dutycycle instead.

time {epoch-secs}\n

Set the device clock using UNIX epoch seconds. Example: time 1738242833

advert\n

Sends an advertisement packet

clock\n

Displays current time per device's clock.

ver\n

Shows the device version and firmware build date.

card\n

Displays your 'business card', for others to manually import

import {card}\n

Imports the given card to your contacts.

list {n}\n

List all contacts by most recent. (optional {n}, is the last n by advertisement date)

to\n

Shows the name of current recipient contact. (for subsequent 'send' commands)

to {name-prefix}\n

Sets the recipient to the first matching contact (in 'list') by the name prefix. (ie. you don't have to type whole name)

send {text}\n

Sends the text message (as DM) to current recipient.

reset path\n

Resets the path to current recipient, for new path discovery.

public {text}\n

Sends the text message to the built-in 'public' group channel

"},{"location":"design/nearby_redesign/","title":"Nearby Nodes \u2014 analiza i propozycja uporz\u0105dkowania","text":"

Branch: refactor/nearby-nodes (zmergowany do main) Plik \u017ar\u00f3d\u0142owy: NearbyScreen.h Status: zaimplementowane \u2014 dokument zachowany jako zapis analizy/decyzji.

Odchylenia od propozycji (stan faktyczny): - Akcji Filter\u2026 w menu nie ma \u2014 duplikowa\u0142a cykl LEFT/RIGHT po typie, wi\u0119c filtr zosta\u0142 wy\u0142\u0105cznie na li\u015bcie (sekcja 3.1 zak\u0142ada\u0142a Filter\u2026 te\u017c w menu). - Sort nie jest togglem przez Enter, lecz zmienia si\u0119 in-place przez LEFT/RIGHT na pod\u015bwietlonym wierszu w popupie (wzorzec ustawie\u0144 Trail), a wiersz pojawia si\u0119 tylko dla \u017ar\u00f3d\u0142a Zapisane (skan nie ma dystansu). - Filtr i sort utrzymuj\u0105 si\u0119 mi\u0119dzy wej\u015bciami na ekran (nie s\u0105 resetowane).

"},{"location":"design/nearby_redesign/#1-jak-narzedzie-jest-zbudowane-dzisiaj","title":"1. Jak narz\u0119dzie jest zbudowane dzisiaj","text":"

Ekran \u0142\u0105czy w sobie trzy osobne tryby o w\u0142asnych listach, widokach szczeg\u00f3\u0142\u00f3w i popupach:

NearbyScreen\n\u251c\u2500\u2500 LIST (kontakty zapisane w mesh)        \u2190 tryb domy\u015blny\n\u2502   \u251c\u2500\u2500 filtr cyklowany LEFT/RIGHT (7 stan\u00f3w)\n\u2502   \u251c\u2500\u2500 DETAIL  (Enter)  \u2192 Lat/Lon/Dist/Type/Seen\n\u2502   \u2502   \u2514\u2500\u2500 _opts popup (Hold Enter): Navigate / Ping / Save waypoint\n\u2502   \u2502       \u2514\u2500\u2500 _ping_menu popup\n\u2502   \u251c\u2500\u2500 NAV view (pe\u0142noekranowa nawigacja)\n\u2502   \u2514\u2500\u2500 _ctx_menu popup (Hold Enter): Discover / Navigate / Save waypoint\n\u2502\n\u2514\u2500\u2500 DISCOVER (skan na \u017cywo NODE_DISCOVER_REQ) \u2190 osobny pod-ekran\n    \u251c\u2500\u2500 lista wynik\u00f3w (karty 2-liniowe)\n    \u2502   Hold Enter = ponowny skan (brak menu!)\n    \u2514\u2500\u2500 DETAIL (Enter) \u2192 pubkey / RSSI / SNR / status\n        \u2514\u2500\u2500 _ping_menu popup (Hold Enter = od razu ping, bez Options)\n

Stan ekranu trzyma 15+ p\u00f3l (_detail, _nav, _discover_mode, _ddetail, _pinging, _filter, dwa komplety _sel/_scroll, trzy bufory wynik\u00f3w ping\u2026) i trzy instancje PopupMenu (_ctx_menu, _opts, _ping_menu).

"},{"location":"design/nearby_redesign/#2-co-konkretnie-jest-nieuporzadkowane","title":"2. Co konkretnie jest nieuporz\u0105dkowane","text":""},{"location":"design/nearby_redesign/#21-jeden-cykl-filtra-miesza-trzy-rozne-osie","title":"2.1 Jeden cykl filtra miesza trzy r\u00f3\u017cne osie","text":"

LEFT/RIGHT przewija 7 stan\u00f3w:

Fav \u00b7 ALL \u00b7 Comp \u00b7 Rpt \u00b7 Room \u00b7 Snsr \u00b7 TIME\n

Ale to nie jest jedna o\u015b \u2014 to trzy wci\u015bni\u0119te w jeden liniowy cykl:

Stan Czym naprawd\u0119 jest Fav filtr ulubionych (flaga ci.flags & 1) ALL/Comp/Rpt/Room/Snsr filtr po typie w\u0119z\u0142a TIME sortowanie (po lastmod zamiast po dystansie)

Konsekwencje: - u\u017cytkownik \u201escrolluje po kategoriach\u201d na \u015blepo \u2014 nie widzi wszystkich naraz, musi cyklowa\u0107, by trafi\u0107 w to, czego szuka; - kombinacje s\u0105 niemo\u017cliwe: nie da si\u0119 zobaczy\u0107 \u201eulubionych repeater\u00f3w posortowanych po dystansie\" ani \u201erepeater\u00f3w posortowanych po czasie\u201d \u2014 model wymusza dok\u0142adnie jeden stan z siedmiu; - TIME jako \u201efiltr\" jest myl\u0105ce \u2014 zmienia kolejno\u015b\u0107, nie zawarto\u015b\u0107; - etykieta w nag\u0142\u00f3wku (NEARBY[TIME]) nie m\u00f3wi, \u017ce to sort.

"},{"location":"design/nearby_redesign/#22-te-same-akcje-rozne-menu-w-zaleznosci-od-miejsca","title":"2.2 Te same akcje, r\u00f3\u017cne menu w zale\u017cno\u015bci od miejsca","text":"Akcja Lista (_ctx_menu) Detail kontaktu (_opts) Detail discover Navigate \u2713 \u2713 \u2014 Save waypoint \u2713 \u2713 \u2014 Ping \u2014 \u2713 \u2713 Discover \u2713 \u2014 \u2014

Ten sam w\u0119ze\u0142 oferuje inny zestaw akcji w zale\u017cno\u015bci od tego, gdzie na niego patrzysz. Ping jest osi\u0105galny tylko ze szczeg\u00f3\u0142\u00f3w, Discover tylko z listy.

"},{"location":"design/nearby_redesign/#23-hold-enter-znaczy-co-innego-na-kazdym-ekranie","title":"2.3 \u201eHold Enter\u201d znaczy co innego na ka\u017cdym ekranie","text":"Ekran Hold Enter (KEY_CONTEXT_MENU) Lista nearby otwiera menu kontekstowe Detail kontaktu otwiera menu Options Lista discover ponowny skan (\u017cadnego menu) Detail discover od razu Ping (pomija Options)

Brak sp\u00f3jnego modelu \u201eprzytrzymaj = menu akcji\u201d.

"},{"location":"design/nearby_redesign/#24-_ping_menu-to-bespoke-widget","title":"2.4 _ping_menu to bespoke widget","text":"

W przeciwie\u0144stwie do reszty popup\u00f3w, ping-menu: - ma wiersze tylko-do-odczytu (RTT / SNR), wi\u0119c po\u0142yka UP/DOWN; - przebudowuje si\u0119 w trakcie (rebuildPingMenu) gdy przychodz\u0105 wyniki; - zostaje otwarte po SELECTED (reszta popup\u00f3w si\u0119 zamyka); - ma w\u0142asny handlePingMenuInput z trybem allow_enter_to_open.

To dzia\u0142a, ale jest to czwarty, niestandardowy wzorzec interakcji w jednym narz\u0119dziu.

"},{"location":"design/nearby_redesign/#25-duplikacja-listdetailping","title":"2.5 Duplikacja list/detail/ping","text":"

renderDiscover/handleInputDiscover/renderDiscoverDetail to niemal r\u00f3wnoleg\u0142a kopia logiki listy i szczeg\u00f3\u0142\u00f3w nearby (osobne _dsel, _dscroll, _d_visible, w\u0142asne rysowanie kart). Discover i Nearby robi\u0105 to samo \u2014 pokazuj\u0105 list\u0119 w\u0119z\u0142\u00f3w z mo\u017cliwo\u015bci\u0105 wej\u015bcia w szczeg\u00f3\u0142y i pingowania \u2014 ale dwoma osobnymi \u015bcie\u017ckami kodu.

"},{"location":"design/nearby_redesign/#3-propozycja-uporzadkowania","title":"3. Propozycja uporz\u0105dkowania","text":"

Trzy zasady przewodnie: (a) jedno menu akcji wsz\u0119dzie, (b) filtr i sortowanie jako osobne, jawne osie, (c) jeden wzorzec listy.

"},{"location":"design/nearby_redesign/#31-rozdziel-filtr-od-sortowania","title":"3.1 Rozdziel filtr od sortowania","text":"

Zamiast jednego cyklu 7-stanowego \u2014 dwie niezale\u017cne osie wybierane z menu (nie przez \u015blepe cyklowanie):

Filtr (o\u015b \u201eco pokazujemy\") i sort (o\u015b \u201ew jakiej kolejno\u015bci\") s\u0105 od siebie niezale\u017cne i \u0142\u0105cz\u0105 si\u0119 dowolnie:

FILTR (jedna o\u015b \u2014 typ w\u0119z\u0142a, z Ulubionymi)      SORT (prze\u0142\u0105cznik)\n  \u2022 Wszystkie                                      \u2022 Dystans (domy\u015blnie)\n  \u2022 Ulubione                                       \u2022 Ostatnio s\u0142yszane\n  \u2022 Companion\n  \u2022 Repeater\n  \u2022 Room\n  \u2022 Sensor\n

Sterowanie (zatwierdzone): - LEFT/RIGHT = szybki cykl tylko po filtrze-typie (jedna sp\u00f3jna o\u015b, znany gest; bez \u201eTIME\" zanieczyszczaj\u0105cego cykl); - Sort = prze\u0142\u0105cznik w menu akcji (Dystans \u2194 Ostatnio s\u0142yszane), trzymany niezale\u017cnie od filtra; - Filter\u2026 dost\u0119pny te\u017c w menu akcji (odkrywalno\u015b\u0107 \u2014 ca\u0142a lista widoczna naraz, nie tylko cyklowanie).

Dzi\u0119ki temu \u201eulubione repeatery po dystansie\" staje si\u0119 mo\u017cliwe, a w nag\u0142\u00f3wku wida\u0107 oba wymiary, np. NEARBY \u00b7 Rpt \u00b7 \u2193dist.

"},{"location":"design/nearby_redesign/#32-jedno-spojne-menu-akcji-hold-enter-wszedzie","title":"3.2 Jedno sp\u00f3jne menu akcji (Hold Enter wsz\u0119dzie)","text":"

Jeden PopupMenu (zamiast _ctx_menu + _opts), pozycje zale\u017cne od kontekstu, ale kolejno\u015b\u0107 i nazwy sta\u0142e:

Hold Enter \u2192  Options\n  \u2022 Navigate        (gdy w\u0119ze\u0142 ma GPS)\n  \u2022 Ping            (gdy znamy pubkey)\n  \u2022 Save waypoint   (gdy w\u0119ze\u0142 ma GPS)\n  \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  \u2022 Filter\u2026         (podmenu z 3.1)\n  \u2022 Sort\u2026           (toggle z 3.1)\n  \u2022 Discover scan   (uruchamia skan na \u017cywo = prze\u0142\u0105cza \u017ar\u00f3d\u0142o)\n

To samo menu na li\u015bcie i w szczeg\u00f3\u0142ach, w obu \u017ar\u00f3d\u0142ach (Zapisane / Skan). Pozycje niedost\u0119pne s\u0105 pomijane (jak ju\u017c teraz robi has_node), ale nigdy nie zmieniaj\u0105 kolejno\u015bci ani nazw. \u201eHold Enter = menu akcji\" \u2014 bez wyj\u0105tk\u00f3w. Ping zawsze przez Options (znika \u201eod razu Ping\" z detalu Discover); rescan to pozycja menu, nie ukryty Hold Enter.

"},{"location":"design/nearby_redesign/#33-jedna-lista-dwa-zroda-zapisane-skan-na-zywo","title":"3.3 Jedna lista, dwa \u017ar\u00f3d\u0142a (Zapisane / Skan na \u017cywo)","text":"

Zatwierdzone: zamiast osobnego pod-ekranu Discover \u2014 jeden komponent listy/szczeg\u00f3\u0142\u00f3w/menu/ping nap\u0119dzany prze\u0142\u0105cznikiem \u017ar\u00f3d\u0142a:

To nie jest dos\u0142owne zlanie dw\u00f3ch list w jedn\u0105 tablic\u0119 (dane maj\u0105 r\u00f3\u017cny kszta\u0142t \u2014 kontakt ma GPS, wynik skanu ma sygna\u0142), tylko jedna \u015bcie\u017cka interakcji nad dwoma \u017ar\u00f3d\u0142ami. Wyb\u00f3r \u017ar\u00f3d\u0142a = \u201eDiscover scan\" w menu (uruchamia NODE_DISCOVER_REQ i prze\u0142\u0105cza widok na wyniki) oraz powr\u00f3t do Zapisanych przez Cancel.

"},{"location":"design/nearby_redesign/#4-proponowany-zakres-etapami","title":"4. Proponowany zakres (etapami)","text":"Etap Zmiana Ryzyko 1 Rozdziel sort od filtra: TIME znika z cyklu; LEFT/RIGHT cykluj\u0105 tylko typ+Fav; sort jako stan + toggle niskie 2 Scal _ctx_menu i _opts w jedno menu akcji o sta\u0142ej kolejno\u015bci; dodaj Filter\u2026/Sort\u2026 niskie 3 Ujednoli\u0107 Hold Enter w Discover (menu zamiast bezpo\u015bredniego rescan/ping; Ping zawsze przez Options) \u015brednie 4 Scal list\u0119 Discover z list\u0105 Nearby w jeden komponent z prze\u0142\u0105cznikiem \u017ar\u00f3d\u0142a wy\u017csze

Kolejno\u015b\u0107 implementacji: 1 \u2192 2 \u2192 3 \u2192 4. Etapy 1\u20132 daj\u0105 najwi\u0119ksz\u0105 popraw\u0119 \u201euporz\u0105dkowania\" przy najmniejszym ryzyku; 3\u20134 domykaj\u0105 sp\u00f3jno\u015b\u0107 (jedna lista, jedno menu wsz\u0119dzie).

"},{"location":"design/nearby_redesign/#5-decyzje-zatwierdzone-2026-06-14","title":"5. Decyzje (zatwierdzone 2026-06-14)","text":"
  1. Filtr i sort \u2014 pe\u0142ne rozdzielenie osi. LEFT/RIGHT = szybki cykl filtra-typu; Sort jako toggle w menu; Filter\u2026 te\u017c w menu dla odkrywalno\u015bci. (Nie chowamy wszystkiego do menu \u2014 zachowujemy szybki gest.)
  2. Discover \u2014 jedna lista, dwa \u017ar\u00f3d\u0142a. Wsp\u00f3lny komponent interakcji; r\u00f3\u017cni si\u0119 tylko prawa kolumna/pola detalu (dystans vs sygna\u0142).
"},{"location":"design/solo_ui_framework/","title":"Solo UI framework \u2014 a guide for adding features","text":"

Go back

This is a developer guide to the reusable building blocks behind the companion_radio solo firmware UI (the ui-new screens). It is not a user manual \u2014 for what each screen does, see solo_features. The goal here is so that adding a new screen or feature means wiring together existing helpers, not reinventing list scrolling, text wrapping, or persistence.

Everything below lives under examples/companion_radio/ unless a path says otherwise. The screen fragments (ui-new/*.h) are all #included, in order, into one translation unit (ui-new/UITask.cpp) \u2014 so a static inline helper in an earlier header is visible to later ones. Header-include order in UITask.cpp therefore matters; new screens go near the others.

Single-TU only. These fragments compile only as part of UITask.cpp. Some define external-linkage symbols at file scope (e.g. NearbyScreen::FILTER_LABELS), so including a fragment from a second .cpp is a duplicate-symbol link error. Anything genuinely shared across TUs must live in a real header (icons.h, GeoUtils.h, DisplayDriver.h), not a screen fragment.

"},{"location":"design/solo_ui_framework/#1-the-screen-model","title":"1. The screen model","text":"

Every screen implements UIScreen (src/helpers/ui/UIScreen.h):

class UIScreen {\npublic:\n  virtual int  render(DisplayDriver& display) = 0;  // returns ms until the next render\n  virtual bool handleInput(char c) { return false; }\n  virtual void poll() { }\n  virtual void onShow() { }                         // reset per-visit state\n};\n
"},{"location":"design/solo_ui_framework/#wiring-a-screen-into-uitask","title":"Wiring a screen into UITask","text":"
  1. Add a UIScreen* my_screen; member in UITask.h (near the others).
  2. Construct it in UITask::begin() (UITask.cpp): my_screen = new MyScreen(this, \u2026);
  3. Add a navigator \u2014 usually just the one line (the cast-free onShow() runs inside setCurrScreen): cpp void UITask::gotoMyScreen() { setCurrScreen(my_screen); } Only screens needing a parameter at entry add a typed call after it (e.g. gotoRingtoneEditor \u2192 selectSlot(slot), gotoMapScreen \u2192 showMapView()).
  4. Reach it from somewhere \u2014 usually a row in ToolsScreen.h (add an Action enum value, a row in the right section table, and a dispatch() case).

That's the whole contract. Steps 1, 3 and 4 are compiler-checked (a mismatch won't link); only a forgotten step 2 can slip through \u2014 every screen pointer is nullptr-initialised in UITask.h and setCurrScreen() bails on null, so a missed new is an inert no-op rather than a null deref.

The constructor takes UITask* task plus whatever it needs (NodePrefs*, KeyboardWidget*, \u2026); the task back-pointer is how a screen calls shared services (_task->showAlert(...), _task->waypoints(), \u2026).

"},{"location":"design/solo_ui_framework/#2-layout-metrics-text-displaydriver","title":"2. Layout metrics & text (DisplayDriver)","text":"

DisplayDriver (src/helpers/ui/DisplayDriver.h) abstracts OLED vs e-ink and, crucially, font scale: landscape e-ink renders text at 2\u00d7, so never hard-code pixel sizes \u2014 derive everything from these:

Call Meaning getLineHeight() pixel rows per text line (8 at 1\u00d7, 16 at 2\u00d7) lineStep() row pitch = line height + gap; use for row y stepping getCharWidth() / getTextWidth(s) advance width; getTextWidth is font-accurate headerH() / listStart() title-bar height / first content row y listVisible(itemH) how many rows fit below the header valCol() conventional x for a right-hand value column width() / height() panel size in px isEink() true only on landscape e-ink; branch on this, not on pixel counts

Drawing helpers (all clip/measure for you):

"},{"location":"design/solo_ui_framework/#3-lists-drawlist","title":"3. Lists \u2014 drawList","text":"

drawList (ui-new/icons.h) is the workhorse for any scrolling list. It computes the visible window from font metrics, keeps sel in view, reserves the scrollbar column, draws each visible row through your callback, and draws the indicator:

drawList(display, count, _sel, _scroll, [&](int idx, int y, bool sel, int reserve) {\n  drawRowSelection(display, y, sel, reserve);   // canonical highlight bar\n  display.drawTextEllipsized(2, y, display.width() - reserve - 4, items[idx].name);\n});\n

reserve is the width the scrollbar took (0 when the list fits) \u2014 subtract it from any right-aligned content so nothing slides under the indicator. The row callback owns its own selection bar: drawRowSelection(d, y, sel, reserve) (ui-new/icons.h) draws the standard one (full row minus reserve, one pixel short); call display.drawSelectionRow() directly only when a row needs a non-standard geometry (full-width, custom height). For the fold-in-place pattern (sections that expand/collapse) use AccordionList instead (ui-new/AccordionList.h) \u2014 same idea, two callbacks (header + item).

Standalone scroll indicators (drawScrollIndicator, \u2026Px) and the reserve calculator (scrollIndicatorReserve) are exposed for hand-laid lists.

"},{"location":"design/solo_ui_framework/#4-reusable-components","title":"4. Reusable components","text":"Component Header Use for PopupMenu PopupMenu.h a modal action menu over any screen AccordionList AccordionList.h collapsible sectioned lists (Tools, Settings) KeyboardWidget KeyboardWidget.h on-screen text entry DigitEditor DigitEditor.h scroll-edit one number, digit by digit FullscreenMsgView FullscreenMsgView.h scrollable full-message reader + word wrap NavView NavView.h bearing/distance/ETA \"navigate to a point\" view

All follow the same shape: a begin(...) to open, an active flag, a handleInput(c) returning a small Result enum, and a render()/draw(). Typical embedding:

if (_menu.active) {                       // popup eats input while open\n  auto r = _menu.handleInput(c);\n  if (r == PopupMenu::SELECTED) runAction(_menu.selectedIndex());\n  return true;\n}\n...\n_menu.begin(\"Options\", 6);                // open it\n_menu.addItem(\"Navigate\"); _menu.addItem(\"Ping\");\n_menu.active = true;\n

KeyboardWidget additionally supports placeholders ({loc}, {time}, sensor tokens) via addPlaceholder() / clearPlaceholders(); the shared kbAddSensorPlaceholders() (ui-new/SensorPlaceholders.h) adds only the tokens the board's sensors actually provide. Expand them with expandMsg() at send time.

FullscreenMsgView::wrapLines() is a standalone pixel-accurate word-wrapper (O(n), variable-width-font aware) reusable by any multi-line layout; it writes into the shared s_wrap_trans / s_wrap_lines scratch (single-threaded render, never held across a yield \u2014 see \u00a79).

"},{"location":"design/solo_ui_framework/#5-domain-helpers","title":"5. Domain helpers","text":"

Geo (GeoUtils.h, namespace geo, all pure/header-inline):

Coordinates are int32 degrees \u00d7 1e6 everywhere (GPS, contacts, trail, prefs). The message tags are LOCATION_MSG_TAG ([LOC], the sender's own live position) and WAYPOINT_MSG_TAG ([WAY], a saved point to share); both stay human-readable on clients that don't know them.

State stores: TrailStore (Trail.h, GPS breadcrumb ring + GPX export), LiveTrackStore (LiveTrack.h, RAM table of others' [LOC] positions, expiring), WaypointStore (Waypoint.h, persisted saved points). Reach them via the task (_task->trail(), _task->liveTrack(), _task->waypoints()).

Message reply prefix: msgReplyBody(text, nick?, n?) (FullscreenMsgView.h) parses a leading @[nick] reply marker, returning the body and optionally the addressee. Use it instead of re-scanning for @[. The stored/sent prefix is raw UTF-8 (it goes over the air) \u2014 never transliterate it.

"},{"location":"design/solo_ui_framework/#6-mini-icons-the-status-bar","title":"6. Mini-icons & the status bar","text":"

Small glyphs are authored as ASCII art and packed at compile time (ui-new/icons.h):

MINI_ICON(ICON_FOO, 5,\n  packRow(\"..#..\"),\n  packRow(\".###.\"),\n  packRow(\"#####\"));\n

Draw with miniIconDraw(display, x, topY, ICON_FOO) (auto-scaled & centered), miniIconDrawTop (exact placement), or the boxed/slot variants (drawBoxedIcon = lit when active, drawSlotIcon = plain). Bigger page glyphs use BIG_ICON / bigIconDraw. The home status bar composes these right-to-left with a blinkOn() cadence for \"leave it on and forget\" broadcasts (auto-advert, Live Share, trail, repeater) \u2014 follow that pattern when adding an indicator: always shown on e-ink, blinking on OLED.

"},{"location":"design/solo_ui_framework/#7-input","title":"7. Input","text":"

Keys arrive as the KEY_* codes in UIScreen.h: KEY_UP/DOWN/LEFT/RIGHT, KEY_ENTER, KEY_CANCEL, KEY_CONTEXT_MENU (the \"Hold Enter\" menu key).

Use the prev/next convention for value changes so the rotary encoder and the D-pad agree: keyIsPrev(c) (LEFT or encoder-prev) and keyIsNext(c) (RIGHT or encoder-next). KEY_CANCEL and KEY_CONTEXT_MENU stay screen-specific. Joystick rotation is handled upstream (rotateJoystickKey) \u2014 screens see already-rotated keys.

"},{"location":"design/solo_ui_framework/#8-persistence","title":"8. Persistence","text":"

Device settings live in one NodePrefs struct (NodePrefs.h), saved via the_mesh.savePrefs() and loaded by DataStore.cpp. Rules when adding a field:

The _dirty convention: a multi-field editor screen mutates _node_prefs live for instant feedback but only persists once, on exit, gated by a _dirty flag \u2014 so LEFT/RIGHT value-cycling doesn't thrash flash. Set _dirty = true at each edit site, then on the exit path call _task->savePrefsIfDirty(_dirty) (UITask) \u2014 it saves once iff dirty and clears the flag, so every screen's save-on-exit reads the same and the \"did we touch flash?\" decision lives in one place. A one-shot action from a popup (no exit hook) calls the_mesh.savePrefs() immediately. Follow whichever matches your screen.

The shared \"active target\" (Locator/Nav destination) is set through UITask::setTarget() (defines it), setTargetNow() (defines + saves + toast), or clearTarget() \u2014 one definition used by the Locator screen, the map, and the Nearby/Waypoints \"Set as target\" actions. Resolve a person's current position with resolvePersonPos() (live [LOC] share, else last-advertised fix).

"},{"location":"design/solo_ui_framework/#9-conventions-gotchas","title":"9. Conventions & gotchas","text":""},{"location":"design/solo_ui_framework/#10-worked-example-a-new-tools-screen","title":"10. Worked example \u2014 a new Tools screen","text":"
// ui-new/MyToolScreen.h  \u2014 included by UITask.cpp near the other screens\n#pragma once\n#include \"icons.h\"          // drawList + mini-icons\n#include \"../NodePrefs.h\"\n\nclass MyToolScreen : public UIScreen {\n  UITask*    _task;\n  NodePrefs* _prefs;\n  int        _sel = 0, _scroll = 0;\n  bool       _dirty = false;\n  static const int ROWS = 3;\npublic:\n  MyToolScreen(UITask* t, NodePrefs* p) : _task(t), _prefs(p) {}\n  void onShow() override { _sel = 0; _scroll = 0; _dirty = false; }\n\n  int render(DisplayDriver& d) override {\n    d.setTextSize(1);\n    d.drawCenteredHeader(\"MY TOOL\");\n    drawList(d, ROWS, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {\n      drawRowSelection(d, y, sel, reserve);\n      d.setCursor(4, y);\n      d.print(i == 0 ? \"Alpha\" : i == 1 ? \"Bravo\" : \"Charlie\");\n    });\n    return 500;\n  }\n\n  bool handleInput(char c) override {\n    if (c == KEY_CANCEL) {\n      _task->savePrefsIfDirty(_dirty);   // saves once iff dirty, then clears\n      _task->gotoToolsScreen();\n      return true;\n    }\n    if (c == KEY_UP)   { _sel = (_sel + ROWS - 1) % ROWS; return true; }\n    if (c == KEY_DOWN) { _sel = (_sel + 1) % ROWS;        return true; }\n    if (keyIsPrev(c) || keyIsNext(c) || c == KEY_ENTER) {\n      /* mutate _prefs\u2026, set _dirty = true */ return true;\n    }\n    return false;\n  }\n};\n

Then: #include \"MyToolScreen.h\" in UITask.cpp, add the member + constructor + gotoMyToolScreen() (\u00a71), and add a row in ToolsScreen.h. Done \u2014 scrolling, the scrollbar, font scaling, e-ink pacing and persistence batching all come from the framework.

"},{"location":"design/trail_redesign/","title":"Trail (Tools \u203a Trail) \u2014 analiza i propozycja uporz\u0105dkowania","text":"

Branch: refactor/trail-screen (zmergowany do main) Plik \u017ar\u00f3d\u0142owy: TrailScreen.h Status: zaimplementowane \u2014 dokument zachowany jako zapis analizy/decyzji. Aktualny opis funkcji od strony u\u017cytkownika: tools_screen.md \u203a GPS Trail.

"},{"location":"design/trail_redesign/#1-jak-ekran-jest-zbudowany-dzisiaj","title":"1. Jak ekran jest zbudowany dzisiaj","text":"
TrailScreen\n\u251c\u2500\u2500 3 widoki (LEFT/RIGHT):  Summary \u00b7 Map \u00b7 List\n\u251c\u2500\u2500 popup akcji  (Hold Enter)  \u2014 JEDNA p\u0142aska lista, do 12 pozycji\n\u2502   \u251c\u2500\u2500 ustawienia (LEFT/RIGHT cykluje w miejscu): Min dist \u00b7 Readout \u00b7 Grid\n\u2502   \u251c\u2500\u2500 toggle:    Start/Stop tracking\n\u2502   \u251c\u2500\u2500 waypointy: Mark here \u00b7 Waypoints \u00b7 Clear waypoints\n\u2502   \u2514\u2500\u2500 trail:     Save \u00b7 Load \u00b7 Export(live) \u00b7 Export(saved) \u00b7 Reset\n\u251c\u2500\u2500 pod-ekrany waypoint\u00f3w (nak\u0142adane na widoki):\n\u2502   \u251c\u2500\u2500 WP_LIST  (lista + dystanse; Trail-start + \u201e+ Add by coords\")\n\u2502   \u251c\u2500\u2500 WP_NAV   (navview)\n\u2502   \u251c\u2500\u2500 WP_ADD   (formularz lat/lon/label)\n\u2502   \u2514\u2500\u2500 _wp_ctx popup: Rename \u00b7 Delete \u00b7 Send\n\u2514\u2500\u2500 KeyboardWidget (label / lat / lon)  \u2014 nak\u0142adka pe\u0142noekranowa\n

Renderowanie mapy: renderMap() (\u2248140 linii) + renderGrid() (\u2248130 linii) + 7 funkcji rysuj\u0105cych markery.

"},{"location":"design/trail_redesign/#2-co-jest-nieuporzadkowane","title":"2. Co jest nieuporz\u0105dkowane","text":""},{"location":"design/trail_redesign/#21-popup-akcji-to-jedna-paska-lista-12-pozycji-z-mieszanymi-rolami","title":"2.1 Popup akcji to jedna p\u0142aska lista 12 pozycji z mieszanymi rolami","text":"

openActionMenu() (TrailScreen.h:363) buduje jedno menu, kt\u00f3re miesza cztery r\u00f3\u017cne klasy pozycji:

Klasa Pozycje Interakcja Ustawienia Min dist, Readout, Grid LEFT/RIGHT (w miejscu) Stan nagrywania Start/Stop tracking Enter Waypointy Mark here, Waypoints, Clear waypoints Enter Plik trasy Save, Load, Export(live), Export(saved), Reset Enter

Problemy: - D\u0142ugi scroll \u2014 na OLED wida\u0107 ~4 wiersze naraz, wi\u0119c do \u201eReset trail\" trzeba przewin\u0105\u0107 przez ca\u0142\u0105 list\u0119; - Dwa wzorce interakcji w jednym menu \u2014 cz\u0119\u015b\u0107 pozycji reaguje na LEFT/RIGHT (ustawienia), cz\u0119\u015b\u0107 na Enter (akcje). Enter na wierszu ustawie\u0144 nic sensownego nie robi \u2014 tylko zamyka i otwiera menu od nowa (reopenAt, TrailScreen.h:581); - Brak kontekstu widoku \u2014 Grid (dotyczy tylko mapy) i Readout (dotyczy tylko Summary) s\u0105 widoczne zawsze, te\u017c tam, gdzie nie maj\u0105 efektu; - Grid ma dwie \u015bcie\u017cki \u2014 i LEFT/RIGHT (:221) i Enter (:234) robi\u0105 to samo; lekko myli.

"},{"location":"design/trail_redesign/#22-rendergrid-dostaje-11-skalarnych-parametrow-brak-wspolnej-projekcji","title":"2.2 renderGrid dostaje 11 skalarnych parametr\u00f3w \u2014 brak wsp\u00f3lnej projekcji","text":"

renderMap liczy projekcj\u0119 (lokalne lambdy projectLL/project, :816), a renderGrid (:876) dostaje 11 osobnych liczb (area_*, min/max_lat, min_lon, lon_scale_geo, scale, off_*) i powtarza t\u0119 sam\u0105 matematyk\u0119 projekcji r\u0119cznie w p\u0119tli (:988, :992). To samo r\u00f3wnanie \u017cyje w trzech miejscach. Ka\u017cda zmiana modelu mapy wymaga edycji w kilku miejscach naraz.

"},{"location":"design/trail_redesign/#23-wybor-kroku-siatki-wielostopniowa-heurystyka-z-nieaktualnym-komentarzem","title":"2.3 Wyb\u00f3r kroku siatki \u2014 wielostopniowa heurystyka z nieaktualnym komentarzem","text":"

renderGrid wybiera krok siatki w czterech nast\u0119puj\u0105cych po sobie korektach (:912\u2013:952): 1. najwi\u0119kszy krok \u2264 target_m, 2. zwi\u0119kszaj a\u017c odst\u0119p pikseli \u2265 MIN_GRID_PX (22 px), 3. zmniejszaj a\u017c zmieszcz\u0105 si\u0119 \u22652 interwa\u0142y, 4. zwi\u0119kszaj a\u017c liczba linii \u2264 MAX_GRID_LINES (40).

Uwagi: - Komentarz przy kroku 4 (:937) m\u00f3wi o \u201estatic buffers (40\u00d740 = ~1600 intersections)\" \u2014 takich bufor\u00f3w ju\u017c nie ma; p\u0119tla rysuje na bie\u017c\u0105co z continue-guardami (:986\u20131002). Cap 40 ogranicza dzi\u015b tylko liczb\u0119 iteracji p\u0119tli (wydajno\u015b\u0107), nie chroni \u017cadnego bufora. Komentarz wprowadza w b\u0142\u0105d. - Kroki 2 i 3 mog\u0105 sobie przeczy\u0107 na bardzo ma\u0142ych ekranach (MIN_GRID_PX = 22 vs shorter_px/2, gdy shorter_px < 44). Nie powoduje b\u0142\u0119du, ale \u201eostateczny\" krok bywa wtedy przypadkowy.

"},{"location":"design/trail_redesign/#24-drobne","title":"2.4 Drobne","text":""},{"location":"design/trail_redesign/#3-propozycja-uporzadkowania","title":"3. Propozycja uporz\u0105dkowania","text":""},{"location":"design/trail_redesign/#31-popup-dwa-poziomy-zamiast-jednej-paskiej-listy","title":"3.1 Popup: dwa poziomy zamiast jednej p\u0142askiej listy","text":"

G\u00f3rne menu kr\u00f3tkie (akcje), ustawienia i operacje na pliku w podmenu:

Hold Enter \u2192  Trail\n  \u2022 Start / Stop tracking\n  \u2022 Mark here\n  \u2022 Waypoints\u2026          \u2192 istniej\u0105cy WP_LIST\n  \u2022 Trail file\u2026         \u2192 Save / Load / Export (live) / Export (saved) / Reset\n  \u2022 Settings\u2026           \u2192 Min dist \u00b7 Readout \u00b7 Grid   (LEFT/RIGHT w miejscu)\n

Korzy\u015bci: - g\u00f3rne menu to ~5 pozycji, bez scrolla na OLED; - jeden wzorzec na poziom: g\u00f3rny i \u201eTrail file\" = Enter-akcje; \u201eSettings\" = warto\u015bci cyklowane LEFT/RIGHT \u2014 bez mieszania w jednym widoku; - destrukcyjny Reset przeniesiony do \u201eTrail file\u2026\", dalej od przypadkowego Entera; - (opcjonalnie) Grid pokazywa\u0107 tylko gdy aktywny jest widok Map, a Readout tylko przy Summary \u2014 menu zale\u017cne od kontekstu widoku.

Wariant minimalny (mniej kodu): zosta\u0107 przy jednej li\u015bcie, ale pogrupowa\u0107 (ustawienia \u2192 akcje \u2192 plik), Reset na sam d\u00f3\u0142, usun\u0105\u0107 podw\u00f3jn\u0105 \u015bcie\u017ck\u0119 Grid. Mniej porz\u0105dku ni\u017c podmenu, ale ta\u0144sze.

"},{"location":"design/trail_redesign/#32-mapa-wspolny-obiekt-projekcji","title":"3.2 Mapa: wsp\u00f3lny obiekt projekcji","text":"

Wydzieli\u0107 ma\u0142y MapProjection liczony raz w renderMap i przekazywany do renderGrid oraz marker\u00f3w:

struct MapProjection {\n  int32_t min_lat, max_lat, min_lon;\n  float   lon_scale_geo, scale;\n  int     off_x, off_y, area_x, area_y, area_w, area_h;\n  void project(int32_t lat, int32_t lon, int& px, int& py) const;\n};\n

Czysto refaktoryzacyjne \u2014 bez zmiany wygl\u0105du mapy.

"},{"location":"design/trail_redesign/#33-siatka-uproszczenie-i-naprawa-komentarza","title":"3.3 Siatka: uproszczenie i naprawa komentarza","text":"

Wynik wizualnie identyczny, logika kr\u00f3tsza i \u0142atwiejsza do utrzymania.

"},{"location":"design/trail_redesign/#4-proponowany-zakres-etapami","title":"4. Proponowany zakres (etapami)","text":"Etap Zmiana Ryzyko 1 Popup \u2192 dwa poziomy (Trail file\u2026, Settings\u2026); Reset g\u0142\u0119biej; usu\u0144 podw\u00f3jny Grid niskie 2 Wydziel MapProjection; renderGrid i markery przez projekcj\u0119 \u015brednie (czysty refactor) 3 Upro\u015b\u0107 wyb\u00f3r kroku siatki; popraw nieaktualne komentarze; sprz\u0105tnij _act_map niskie

Etap 1 = najwi\u0119ksza poprawa \u201euporz\u0105dkowania popupu\" (g\u0142\u00f3wna pro\u015bba). Etapy 2\u20133 = czyszczenie logiki mapy/siatki bez zmiany wygl\u0105du.

"},{"location":"design/trail_redesign/#5-decyzje-zatwierdzone-2026-06-14","title":"5. Decyzje (zatwierdzone 2026-06-14)","text":"
  1. Popup \u2014 pe\u0142ne podmenu. G\u00f3rne menu kr\u00f3tkie; Trail file\u2026 i Settings\u2026 jako podmenu.
  2. Menu zale\u017cne od widoku \u2014 tak. Grid widoczny w Settings tylko na widoku Map, Readout tylko na Summary.
  3. Mapa \u2014 MapProjection + uproszczenie siatki (etap 2 i 3 razem).
  4. Siatka \u2014 kwadratowe oczka, doci\u0105gni\u0119te do kr\u00f3tszego boku. Krok w przestrzeni pikseli (skala izotropiczna). Kr\u00f3tszy bok dzielony na ca\u0142kowit\u0105 liczb\u0119 r\u00f3wnych kwadrat\u00f3w (siatka dotyka tej pary kraw\u0119dzi \u2014 na poziomym OLED: g\u00f3ra/d\u00f3\u0142), na d\u0142u\u017cszym boku mie\u015bci si\u0119 ca\u0142kowita liczba tych samych kwadrat\u00f3w, wy\u015brodkowana. Dzi\u0119ki temu oczka s\u0105 kwadratowe, siatka wpisana w ramk\u0119 i symetryczna (brak jednostronnego przesuni\u0119cia). Etykieta skali = nominalna okr\u0105g\u0142a warto\u015b\u0107. (Kwadraty + linie na wszystkich 4 kraw\u0119dziach s\u0105 geometrycznie niemo\u017cliwe dla dowolnego prostok\u0105ta \u2014 wybrano kwadraty + 2 kraw\u0119dzie + symetria.)
"},{"location":"solo_features/clock_screen/clock_screen/","title":"Clock screen","text":""},{"location":"solo_features/clock_screen/clock_screen/#clock-screen","title":"Clock Screen","text":"

Go back

"},{"location":"solo_features/clock_screen/clock_screen/#overview","title":"Overview","text":"OLED E-Ink

A full-screen clock page on the home screen. Shows the current time and date, with up to three configurable data fields below.

Time is synchronized from GPS or via the companion app. Timezone offset is applied from Settings \u203a System.

If no time source is available, the screen shows \"! No time sync\" with a hint to enable GPS or connect the app.

"},{"location":"solo_features/clock_screen/clock_screen/#time-display","title":"Time display","text":""},{"location":"solo_features/clock_screen/clock_screen/#data-fields","title":"Data fields","text":"OLED E-Ink

Up to three data fields are shown below the date separator. Each field displays a label and a value on the same line.

Field Label Value None \u2014 \u2014 Batt V Batt Battery voltage (e.g. 3.92V) Batt % Batt Battery percentage using LiPo curve anchored at the low-battery threshold Temperature Temp \u00b0C from onboard sensor Humidity Hum % from onboard sensor Pressure Pres hPa from onboard sensor GPS GPS lat lon decimal degrees, or no fix Altitude Alt metres from onboard sensor (GPS or barometric) Luminosity Lux lux from onboard sensor CO\u2082 CO2 ppm from onboard sensor Contacts Nodes Total contacts in the mesh Messages Msgs Total unread message count

Sensor fields show -- when the sensor is not connected or has no data.

"},{"location":"solo_features/clock_screen/clock_screen/#configuring-fields","title":"Configuring fields","text":"OLED E-Ink

Hold Enter (or press the Context menu key) on the Clock page to open the Dashboard Config screen, where each of the three field slots can be cycled with LEFT/RIGHT.

"},{"location":"solo_features/favourites_dial/favourites_dial/","title":"Favourites dial","text":""},{"location":"solo_features/favourites_dial/favourites_dial/#favourites-dial","title":"Favourites Dial","text":"

Go back

"},{"location":"solo_features/favourites_dial/favourites_dial/#overview","title":"Overview","text":"OLED E-Ink

A dedicated home page showing a grid of up to 6 pinned contacts for quick access. The layout adapts to the display orientation:

"},{"location":"solo_features/favourites_dial/favourites_dial/#navigation","title":"Navigation","text":"

Navigate tiles with UP / DOWN / LEFT / RIGHT. Pressing a directional key at the edge of the grid switches to the adjacent home page instead of wrapping.

Enter on a filled tile \u2014 opens that contact's DM directly.

Enter on an empty tile (+) \u2014 opens a contact picker to fill the slot.

"},{"location":"solo_features/favourites_dial/favourites_dial/#unread-badge","title":"Unread badge","text":"

Filled tiles show an unread message count in the top-right corner when there are unread DMs from that contact. The contact name is ellipsized to make room for the badge.

If a pinned contact is removed from the contacts list \u2014 explicitly, or auto-evicted to make room when the table is full \u2014 its slot is freed automatically and goes back to an empty + tile.

"},{"location":"solo_features/favourites_dial/favourites_dial/#pinning-a-contact","title":"Pinning a contact","text":"

From the Favourites Dial \u2014 press Enter on an empty tile (+). A picker opens showing:

  1. Contacts marked as favourites in the upstream app (starred contacts) \u2014 listed first
  2. Recent DM contacts \u2014 listed after
  3. All remaining chat contacts \u2014 fallback when the first two tiers are empty (e.g. fresh install before any DMs)
OLED E-Ink

Select a contact to pin it to that slot.

From a DM conversation \u2014 Hold Enter \u203a context menu \u203a Pin to dial, then choose a slot from the slot picker (Slot 1\u20136, showing the current occupant name or \"empty\").

If the selected contact is already pinned in another slot, it is moved to the new slot automatically.

"},{"location":"solo_features/favourites_dial/favourites_dial/#unpinning-a-contact","title":"Unpinning a contact","text":"

Open the contact's DM, Hold Enter \u203a context menu \u203a Unpin (slot N).

"},{"location":"solo_features/favourites_dial/favourites_dial/#reordering-the-favourites-page","title":"Reordering the Favourites page","text":"

The position of the Favourites Dial in the home page navigation sequence can be changed in Settings \u203a Home Pages \u2014 press LEFT / RIGHT on the Favourites entry to move it earlier or later.

"},{"location":"solo_features/message_screen/message_screen/","title":"Message screen","text":""},{"location":"solo_features/message_screen/message_screen/#messages-screen","title":"Messages Screen","text":"

Go back

"},{"location":"solo_features/message_screen/message_screen/#overview","title":"Overview","text":"OLED E-Ink

The Messages screen is split into three modes \u2014 DMs, Channels, and Rooms \u2014 selectable with UP/DOWN on the mode-select screen. Each mode shows the corresponding list of conversations with unread counters.

"},{"location":"solo_features/message_screen/message_screen/#sending-messages","title":"Sending messages","text":"OLED E-Ink

Press Enter on a contact or channel to open its history, then press Enter again (or select an empty send row) to compose a message. Choose between:

The keyboard supports placeholders that insert live data at send time:

Placeholder Value Availability {time} current time (HH:MM) always {loc} GPS coordinates always (\"no GPS\" if no fix) {temp} temperature sensor connected {hum} humidity sensor connected {pres} barometric pressure sensor connected {alt} altitude sensor connected {lux} luminosity sensor connected {co2} CO\u2082 concentration sensor connected

Sensor placeholders appear automatically in the placeholder picker when the corresponding sensor is active. {time} and {loc} are always shown.

"},{"location":"solo_features/message_screen/message_screen/#rooms-logging-in","title":"Rooms \u2014 logging in","text":"

Posting to a room server requires a login handshake first, so the device can log in on its own \u2014 no phone app needed. The first time you press Enter on a room, a password prompt opens automatically; type the room's password and press the \u2713 key (leave it empty and submit for open / no-password rooms).

The on-screen keyboard is limited to ASCII (letters, digits and common symbols); accented characters such as \u0105/\u0119 can't be typed on the device. A password containing them can still be set from the phone app \u2014 the device stores and replays it byte-for-byte.

"},{"location":"solo_features/message_screen/message_screen/#message-history","title":"Message history","text":"OLED E-Ink

Each entry in the history list shows the sender name and a compact age indicator (3m, 2h, >1d) in the top-right corner.

Short Enter on a message opens it in fullscreen. Hold Enter \u2014 on a history row or in fullscreen \u2014 opens the same options menu: Reply, plus Navigate / Save waypoint when the message contains a location (see Fullscreen message view). You don't need to open the message first.

"},{"location":"solo_features/message_screen/message_screen/#fullscreen-message-view","title":"Fullscreen message view","text":"OLED E-Ink

Navigate between messages with LEFT (newer) and RIGHT (older). Long messages scroll with UP/DOWN.

If the message is a reply addressed to someone (@[nick]), a To: nick bar is shown below the sender name and the body is displayed without the address prefix.

OLED E-Ink

Hold Enter in fullscreen opens the options menu. It always offers Reply for an incoming message, and when the message contains a location it adds two more:

A location is any lat,lon pair in the text \u2014 exactly what the {loc} placeholder inserts \u2014 so you can navigate to anything a contact shares. A [WAY]lat,lon label share also carries a name, used as the waypoint label. This works on DMs and channel messages, incoming or outgoing.

"},{"location":"solo_features/message_screen/message_screen/#context-menu-contact-list","title":"Context menu \u2014 contact list","text":"

Hold Enter on a contact entry opens a context menu:

OLED E-Ink Item Action Mark as read Clears unread counter for this contact Notif: default / OFF / ON Per-contact notification override \u2014 LEFT/RIGHT to cycle Melody: global / M1 / M2 Per-contact melody override \u2014 LEFT/RIGHT to cycle Pin to dial / Unpin (slot N) Pin this contact to a Favourites Dial slot; if already pinned shows which slot

When Pin to dial is selected, a slot picker opens (Slot 1\u20136 showing current occupant name or \"empty\"). Choosing a slot that already holds another contact moves the new contact there.

In the Rooms list the context menu instead offers a single item:

Item Action Login\u2026 Opens the password prompt to (re-)log in to this room (see Rooms \u2014 logging in)"},{"location":"solo_features/message_screen/message_screen/#context-menu-channel-list","title":"Context menu \u2014 channel list","text":"OLED E-Ink

Hold Enter on a channel entry opens a context menu:

Item Action Mark all read Clears all unread for this channel Notif: default / OFF / ON Per-channel notification override \u2014 LEFT/RIGHT to cycle Melody: global / M1 / M2 Per-channel melody override \u2014 LEFT/RIGHT to cycle Fav: yes / no Add or remove this channel from favourites \u2014 LEFT/RIGHT to toggle"},{"location":"solo_features/message_screen/message_screen/#mark-all-read","title":"Mark all read","text":"

Hold Enter on the DM / Channels / Rooms mode-select screen to clear all unread counters for the highlighted category at once.

"},{"location":"solo_features/screen_lock/screen_lock/","title":"Screen lock","text":""},{"location":"solo_features/screen_lock/screen_lock/#screen-lock","title":"Screen Lock","text":"

Go back

"},{"location":"solo_features/screen_lock/screen_lock/#overview","title":"Overview","text":"OLED E-Ink

Screen lock prevents accidental keypresses. While locked the display turns off and all input is ignored.

"},{"location":"solo_features/screen_lock/screen_lock/#locking-and-unlocking","title":"Locking and unlocking","text":"

Hold Back and press Enter three times within 3 seconds. The sequence works in both directions \u2014 the same combination locks and unlocks.

If the display is off when the sequence begins, it turns on automatically so the hint is visible. Each press in the sequence extends the display-on timer by 5 seconds.

The hint popup at the bottom of the lock screen guides through the sequence:

Step Hint Not started Hold Back + 3\u00d7Enter 1 press done Enter \u00d72 more\u2026 2 presses done Enter \u00d71 more\u2026

If no press is made for 3 seconds, the counter resets.

"},{"location":"solo_features/screen_lock/screen_lock/#lock-screen","title":"Lock screen","text":"OLED E-Ink

A brief press of any button wakes the display and shows the lock screen. It displays:

The display turns off again automatically after 5 seconds of inactivity (or 2 seconds immediately after locking).

"},{"location":"solo_features/screen_lock/screen_lock/#auto-lock","title":"Auto-lock","text":"

Enable Auto-lock in Settings \u203a Display to lock the device automatically whenever the display turns off due to auto-off timeout. With auto-lock on, the device is always locked after the screen goes dark \u2014 no manual lock needed.

"},{"location":"solo_features/settings_screen/settings_screen/","title":"Settings screen","text":""},{"location":"solo_features/settings_screen/settings_screen/#settings-screen","title":"Settings Screen","text":"

Go back

"},{"location":"solo_features/settings_screen/settings_screen/#overview","title":"Overview","text":"OLED E-Ink

All settings are saved to flash and restored on next boot. Settings are organised into collapsible sections. Press Enter on a section header to expand or collapse it \u2014 all sections start collapsed for faster navigation. Press LEFT/RIGHT to change a value, or Enter for toggle items.

Press Cancel/Back to save and return to the home screen.

"},{"location":"solo_features/settings_screen/settings_screen/#display","title":"Display","text":"Setting Options Notes Brightness 1\u20135 LEFT/RIGHT; preview applies immediately Auto-off 5 s / 15 s / 30 s / 60 s / never LEFT/RIGHT Auto-lock on / off Locks device when display turns off Battery icon / % / V Display mode for the top-bar battery indicator Clock seconds show / hide Hiding reduces OLED refresh from 1 s to 60 s Clock format 24 h / 12 h 12 h appends AM/PM Font Default / Lemon Default: 5\u00d77 Adafruit with ASCII transliteration; Lemon: native Unicode with pixel-accurate line wrap Display rotation (e-ink only) 0\u00b0 / 90\u00b0 / 180\u00b0 / 270\u00b0 Applied immediately Joystick rotation (e-ink only) 0\u00b0 / 90\u00b0 / 180\u00b0 / 270\u00b0 Rotates input mapping independently of display rotation; useful for custom enclosures Full refresh interval (e-ink only) off / 5 / 10 / 20 / 30 Partial refreshes between full clears; reduces ghosting on long sessions"},{"location":"solo_features/settings_screen/settings_screen/#sound","title":"Sound","text":"Setting Options Notes Buzzer On / Off / Auto Auto: silences while BLE connected, re-enables on disconnect Volume 1\u20135 LEFT/RIGHT; preview tone plays on each change DM Melody built-in / Melody 1 / Melody 2 / None Notification sound for incoming private messages. None disables the sound for this event. Channel Melody built-in / Melody 1 / Melody 2 / None Notification sound for incoming channel messages. None disables the sound for this event. AD sound built-in / Melody 1 / Melody 2 / None Sound played whenever an advert is received from any node \u2014 pairs with Auto-Advert as an audible \"in range\" heartbeat (see Tools \u203a Auto-Advert). None disables the sound for this event. AD scope All / Zero-hop Filters the AD sound so it plays for every advert or only for local zero-hop adverts.

Melody 1 and Melody 2 are custom sequences editable in Tools \u203a Ringtone Editor.

"},{"location":"solo_features/settings_screen/settings_screen/#home-pages","title":"Home Pages","text":"OLED E-Ink

Lists all available home screen pages. For each entry:

Settings and Messages are always visible and cannot be disabled.

"},{"location":"solo_features/settings_screen/settings_screen/#radio","title":"Radio","text":"Setting Options Notes TX Pwr 2\u201322 dBm LEFT/RIGHT. With Auto pwr on this is the ceiling \u2014 the radio may transmit lower. Preset named presets LEFT/RIGHT cycles community RF presets (region frequency + bandwidth/SF/CR). Enter opens a popup to pick one, save the current settings as a named preset, or delete a saved one. Applies frequency, bandwidth, SF and CR together. Freq chip range Enter opens a digit-by-digit editor: LEFT/RIGHT moves between decimal places, UP/DOWN steps that digit. Bounds come from the radio chip's own validated range, so a value the radio would reject can't be entered. SF 5\u201312 LEFT/RIGHT. Spreading factor. BW 7.8\u2013500 kHz LEFT/RIGHT cycles the standard LoRa bandwidths. CR 5\u20138 LEFT/RIGHT. Coding rate (4/5\u20134/8). Pwr save ON / OFF Battery saver. Hardware duty-cycle receive: the SX126x cycles RX\u2194sleep on its own and wakes on a preamble, cutting average RX current. Trades a little receive latency; leave OFF for lowest-latency reception. Requires an SX126x radio (otherwise stays on continuous RX). Forced off (shown as --) while the repeater is on \u2014 a repeater must listen continuously; your setting is restored when the repeater is switched off. Auto pwr ON / OFF Adaptive Power Control. Lowers actual TX power on strong links to save energy, ramping back up \u2014 to the TX Pwr ceiling \u2014 on weak or lost links. Link quality comes from direct-message ACK SNR and, for channel messages (no ACK), from hearing a repeater rebroadcast your packet. The radio page / name bar shows the live power. Default OFF (fixed TX power). Suppressed (shown as --) while the repeater is on \u2014 a repeater holds full TX power for consistent relay reach; your setting is restored when the repeater is switched off. OLED E-Ink

The repeater mode and its flood filters live on their own screen \u2014 see Tools \u203a Repeater.

"},{"location":"solo_features/settings_screen/settings_screen/#system","title":"System","text":"Setting Options Notes Timezone \u221212 h \u2026 +14 h UTC offset in whole hours Low battery off / 3.0 V / 3.1 V / 3.2 V / 3.3 V / 3.4 V / 3.5 V Auto-shutdown threshold; also sets the 0 % anchor for the battery percentage indicator Units Metric / Imperial Global unit system for every distance/speed shown in Tools (Nearby Nodes, Trail, navigate-to-point). Metric: m / km, km/h, min/km. Imperial: ft / mi, mph, min/mi"},{"location":"solo_features/settings_screen/settings_screen/#contacts","title":"Contacts","text":"Setting Options Notes DMs all / favourites Show all chat contacts or only upstream-starred ones Channels all / favourites Show all channels or only favourited ones Rooms all / favourites Show all room servers or only favourited ones"},{"location":"solo_features/settings_screen/settings_screen/#messages","title":"Messages","text":"Setting Options Notes Resend off / 1\u00d7\u20135\u00d7 Auto-resend an on-device direct message this many times when no delivery ACK is received (default 2\u00d7)

Up to 10 quick reply templates (Q1\u2013Q10). Press Enter on a slot to open the keyboard editor. Supports the same placeholders as the main keyboard ({time}, {loc}, and sensor placeholders when connected).

"},{"location":"solo_features/tools_screen/tools_screen/","title":"Tools screen","text":""},{"location":"solo_features/tools_screen/tools_screen/#tools-screen","title":"Tools Screen","text":"

Go back

"},{"location":"solo_features/tools_screen/tools_screen/#overview","title":"Overview","text":"OLED E-Ink

The Tools screen is a hub for GPS trail recording, nearby node browsing, ringtone editing, auto-reply bot, auto-advert, live location sharing, locator, compass, device diagnostics, and repeater mode. Tools are grouped into collapsible Location / Comms / System sections \u2014 the same fold-in-place model as Settings; Tools always opens folded back to the section list. Navigate with UP/DOWN, press Enter on a section header to expand or collapse it, or on a tool to open it.

"},{"location":"solo_features/tools_screen/tools_screen/#nearby-nodes","title":"Nearby Nodes","text":"OLED E-Ink

Browse nodes that have recently advertised on the mesh. Filter (which nodes) and sort (in what order) are independent axes and combine freely.

Filter by category with LEFT/RIGHT (one coherent axis \u2014 type only):

Filter Shows All All known nodes Fav Upstream-starred contacts only Comp Companion (chat) nodes Rpt Repeaters Room Room servers Snsr Sensors

Select a node to see its coordinates, distance, bearing with cardinal direction, type, and last-heard time. A node that is broadcasting its position via Live Share is marked with a \u2666 diamond beside its name in the list (the same marker the map uses), and its detail shows Sharing pos: with the share age and whether it's DM-verified or channel-only.

Hold Enter opens the same Options menu everywhere (list and detail), in a fixed order \u2014 only the actions that apply appear:

Action Available when Navigate selected node has GPS \u2014 for a node sharing live position, the view follows it as it moves and adds an ETA line Ping a public key is known for the node Save waypoint selected node has GPS Set as target selected node has GPS and a public key \u2014 pins it as the active Locator/Nav target right away (see Locator) Sort: Dist/Recent browsing stored nodes \u2014 LEFT/RIGHT on the row flips distance \u2194 last-heard in place Discover scan / Rescan always (live NODE_DISCOVER_REQ scan)

Filtering stays on the list itself (LEFT/RIGHT cycles the type), so there is no separate Filter action in the menu. Sort is adjusted in place: highlight the Sort row and tap LEFT/RIGHT to flip the list (and its right-hand column) between distance and last-heard without closing the menu \u2014 the same in-popup pattern as Trail's settings. The row appears only while browsing stored nodes (live-scan rows carry signal, not distance). Filter and sort are independent and persist across re-entry to the screen.

Selecting Ping opens the Ping popup:

OLED E-Ink

Use Enter on the popup\u2019s Ping row to send a direct mesh ping to that node. The popup then shows the RTT and SNR values on the next lines, and can be used again immediately for another ping.

[!TIP] Combined with Auto-Advert on the other device, Nearby Nodes becomes a passive location tracker \u2014 as long as the tracked device periodically broadcasts its GPS position, you can see its current distance and bearing without any manual interaction on either end.

"},{"location":"solo_features/tools_screen/tools_screen/#active-discovery-live-scan","title":"Active Discovery (live scan)","text":"OLED E-Ink

Options \u2192 Discover scan sends a NODE_DISCOVER_REQ. Repeaters, sensors and room servers within zero-hop range respond immediately with name, type and signal data. This is not a separate screen \u2014 it is the same list switched to a live-scan source: the right-hand column shows RSSI instead of distance, and node detail shows the public key, signal data and contact status.

Because it is the same list, all the same keys apply \u2014 UP/DOWN to navigate, Enter for detail, Hold Enter for the Options menu (where Rescan repeats the scan and Ping works exactly as on stored nodes).

"},{"location":"solo_features/tools_screen/tools_screen/#gps-trail","title":"GPS Trail","text":"OLED E-Ink

Records your route in a RAM ring buffer (up to 512 points, sampled every 1 s). The track is simplified as it's recorded \u2014 a long straight stretch is kept as just its two endpoints while curves keep their detail (bounded to within the Min dist tolerance of the real path), so the buffer covers a far longer route than a flat point budget would suggest. Tracking runs in the background \u2014 a blinking G appears in the status bar. The trail survives display auto-off but is lost on reboot unless saved to flash first.

[!TIP] The Map view is also reachable directly from the home carousel \u2014 the Map page shows a live mini-preview (your position, trail, and tracked contacts) with a north marker and a bottom-left scale tick. The status line below reads Track:N (tracked-node count) and, when you have a fix and at least one tracked contact, an arrow + distance to the nearest one (e.g. Track:3 \u2192120m). If a Locator/Nav target is set it's drawn as a flag marker (see Locator). Press Enter to open the full Trail Map; Hold Enter shares your position (see Live Share); Back returns home.

A GPS fix indicator also sits in the top status bar, alongside the trail/auto-advert/repeater icons \u2014 boxed (lit) once the receiver has a valid fix, a plain glyph while still searching. It only appears on boards with GPS hardware and while GPS is turned on in Settings; it's hidden the rest of the time rather than sitting there empty.

Cycle views with LEFT / RIGHT:

View Content Summary Distance, elapsed time, avg speed or pace, point count, tracking status Map Auto-fit dot-and-line plot with cos(lat) aspect correction; segment breaks marked; north arrow; square scale grid fitted to the map frame (toggle under Hold Enter \u2192 Settings \u2192 Grid, Map view only). Your current GPS position, all waypoints, and any live-tracked contacts (positions shared via Live Share) are always drawn \u2014 even with no trail recording \u2014 so the map is useful standalone. The active Locator/Nav target, if set, is drawn as a flag on top (folded into the frame so it's never off-screen). Point labels are auto-placed to avoid overlapping (a crowded cluster drops some labels rather than smearing them) List Per-point rows showing local time (HH:MM) and delta distance from the previous point; segment-start rows show start; scroll with UP/DOWN OLED E-Ink

Hold Enter opens the action menu. It is two-level \u2014 a short main menu, plus Trail file\u2026 and Settings\u2026 submenus. Cancel/Back in a submenu returns to the main menu.

Main menu:

Item Action Start / Stop tracking Begin or end a recording session. If GPS is off, choosing Start asks \"GPS is off \u2014 Enable GPS & start\" so a session can't silently run with nothing to record Mark here Drop a waypoint at the current GPS fix (see below) Waypoints\u2026 Open the waypoint list / navigation / add-by-coords Track back Retrace the recorded route back to its start (needs \u22652 points; see below) Share my pos Send your current position as a one-shot [LOC] message \u2014 pick a contact or channel (see Live Share) Trail file\u2026 Open the file submenu (below) Settings\u2026 Open the settings submenu (below)

Trail file\u2026 (only the operations that apply right now appear):

Item Action Save trail Write RAM ring to flash (/trail) Load trail Restore flash trail into RAM Export (live) Stream live RAM trail as GPX 1.1 over USB Serial Export (saved) Stream saved flash trail as GPX 1.1 over USB Serial Reset trail Clear RAM ring and elapsed time

Settings\u2026 (values cycle with LEFT/RIGHT or Enter; shown only where they apply):

Item Available Action Min dist always Sample gate, 4 levels \u2014 metric: 5/10/25/100 m, imperial: 15/30/75/300 ft Auto-pause always Off / 1 / 2 / 5 min \u2014 auto-freeze the trail after a stop, resume on movement (see below) Mark avg always Off / 5 / 10 / 30 s \u2014 GPS averaging for Mark here (see Waypoints below) Readout Summary view Summary shows Speed or Pace (in the global unit system) Grid Map view Toggle scale grid on the map

(Trail file\u2026 appears only when a live or saved trail exists. Mark here needs a GPS fix; Waypoints is always available.)

Auto-pause \u2014 when set, a recording trail automatically pauses after the device has stayed within ~15 m of one spot for the chosen delay: the elapsed timer and point sampling both freeze, and the map line breaks across the idle gap. It resumes on its own as soon as you move again. This keeps a stop (a break, a meal, parking) out of your distance and average-speed stats without you having to remember to stop and restart tracking. A paused trail is still \"on\" (the G marker keeps blinking) \u2014 the Summary Status row shows paused. The stop is detected with its own coarse movement gate, independent of Min dist, so GPS jitter while you're parked doesn't keep it awake.

"},{"location":"solo_features/tools_screen/tools_screen/#track-back","title":"Track back","text":"

Hold Enter \u2192 Track back retraces the trail you just recorded, back to where you started \u2014 useful for returning the same way in poor visibility or unfamiliar ground. It reuses the navigation view (distance + two absolute bearings; see Waypoints \u203a Navigating), but instead of a single fixed target it walks the recorded breadcrumbs in reverse: it snaps onto the route at the nearest recorded point, guides you to it, then automatically advances to the next earlier point as you reach each one (within ~20 m). The header shows how many points remain (Back: 12 pt), reading Trail start on the final leg; arriving there shows Back at start and exits. Cancel leaves track-back at any time. It needs a trail with at least two points and a GPS fix; it doesn't require tracking to still be running.

"},{"location":"solo_features/tools_screen/tools_screen/#waypoints","title":"Waypoints","text":"

A waypoint is a saved spot \u2014 your car, camp, a water source \u2014 that you can navigate back to later. Waypoints are independent of the trail: they live in their own flash file (/waypoints), survive a reboot, and are not cleared by Reset trail. Up to 16 can be stored \u2014 the Waypoints list header shows how many are in use (e.g. WAYPOINTS 3/16).

Dropping a waypoint \u2014 Hold Enter \u2192 Mark here. This captures the current GPS fix and opens the on-screen keyboard for a short label (up to 11 characters \u2014 e.g. CAR, CAMP, H2O). Leaving it blank auto-names it WP1, WP2, \u2026 Marking works whether or not the trail is being recorded; it needs a GPS fix (otherwise it reports No GPS fix).

GPS averaging \u2014 with Settings \u2192 Mark avg set (5 / 10 / 30 s), Mark here doesn't snapshot a single fix; it samples the GPS once a second for that window and stores the mean position, for a steadier mark than one instantaneous reading (handy for a precise spot \u2014 a cache, a car, a trailhead). A short screen shows the time left and the sample count while it runs; Cancel aborts. When the window closes it opens the label keyboard as usual. With Mark avg = Off (the default) marking is instant.

Adding by coordinates \u2014 open Hold Enter \u2192 Waypoints and select the + Add by coords row (always the last entry in the list). This creates a waypoint without being there \u2014 no GPS fix required (handy for a meeting point or a spot read off a map). It opens a small form with three editable rows plus Save:

OLED E-Ink

On the map \u2014 saved waypoints show on the Trail Map view as a hollow diamond with the label's first two characters beside it (enough to tell nearby waypoints apart). Waypoints and your current GPS position are drawn continuously \u2014 even with no trail recording in progress \u2014 so the Map view doubles as a live \"you + your marks\" view, not just a recorded-track plot. With no trail, the view auto-fits to your waypoints and position. While a trail exists, the view frames the recorded route instead, and any waypoint that falls outside it is clamped to the nearest map edge \u2014 a distant mark can't blow up the scale and squash the trail.

Navigating \u2014 Hold Enter \u2192 Waypoints opens the list (each row shows the label and live distance). The list always begins with a synthetic Trail start row whenever a trail exists, so you can backtrack to where you began without having marked it. Select a row and press Enter to open the navigation view:

   CAMP            \u2190 target label\n   1.4 km          \u2190 distance to target\n   To:  145\u00b0 SE    \u2190 absolute bearing to the target\n   Hdg: 090\u00b0 E     \u2190 your current course over ground (-- when stationary)\n
OLED E-Ink

There is no magnetometer, so the screen shows two absolute bearings and you compare them: target at 145\u00b0, travelling at 90\u00b0 \u2192 bear right. The Hdg line is derived from GPS movement (see Compass) and reads -- until you move.

Managing \u2014 Hold Enter on a waypoint row offers Rename / Delete / Send / Set as target (the Trail start row is navigate-only). Set as target pins the waypoint as the active Locator/Nav target in one step (see Locator). Delete removes one at a time; there is no bulk clear.

Sharing \u2014 Send hands the waypoint to the Messages screen: pick a contact or channel, and the message is pre-filled as [WAY]<lat>,<lon> <label> (e.g. [WAY]37.42123,-122.08456 CAR) for you to confirm or edit before sending. On the receiving device, opening that message and Hold Enter \u2192 Navigate / Save waypoint turns it back into a navigable point (see Messages \u203a Fullscreen message view). The format is plain text, so it stays readable on other firmware and the phone app.

"},{"location":"solo_features/tools_screen/tools_screen/#downloading-gpx","title":"Downloading GPX","text":"

Easiest \u2014 Solo GPX Downloader (browser-based, no install):

  1. Open the link in Chrome or Edge (Web Serial API required).
  2. Click Connect device and select the USB serial port.
  3. On the device: Tools \u203a Trail \u2192 Hold Enter \u2192 Export (live) or Export (saved).
  4. The browser captures the stream automatically \u2014 set a filename and click Download.

Script \u2014 tools/trail_export.py (auto-detects the port, captures from <?xml to </gpx>, writes a timestamped file under tools/gpx/):

uv run tools/trail_export.py\n

Then on the device: Tools \u203a Trail \u2192 Hold Enter \u2192 Export (live) or Export (saved).

Manual fallback \u2014 open a serial terminal at 115200 baud and capture the stream by hand:

Saved waypoints are included in the export as GPX <wpt> elements (with their label as <name>), alongside the track \u2014 so they show as pins in OsmAnd, Garmin BaseCamp, GPX Studio, Google Earth, etc. Either way, the resulting file imports into all of those.

[!NOTE] If the companion app is connected via BLE, the export is safe \u2014 BLE and USB operate independently. If connected via USB, disconnect the app before exporting.

"},{"location":"solo_features/tools_screen/tools_screen/#auto-advert","title":"Auto-Advert","text":"

Periodically broadcasts a 0-hop advert with your GPS position. Configurable interval: off / 30 s / 1 min / 2 min / 5 min / 10 min / 30 min / 1 h. A blinking A appears in the status bar while active.

[!TIP] Audible connection heartbeat \u2014 the device chirps each time it receives an advert from any node (sound chosen in Settings \u203a Sound \u203a AD sound). With Auto-Advert running on both ends (e.g. two people on a hike), each hearing the other's periodic advert becomes a hands-free \"in range\" beep \u2014 no need to look at the screen. It fires for every received advert, so in a busy mesh it can get chatty; choose None in Settings \u203a Sound \u203a AD sound to silence just this event, or set Settings \u203a Sound \u203a Advert scope to Zero-hop to limit it to local adverts only. You can also set Settings \u203a Sound \u203a Buzzer to Off (or Auto, which mutes while a companion app is connected) to silence all buzzer output.

"},{"location":"solo_features/tools_screen/tools_screen/#live-share","title":"Live Share","text":"OLED E-Ink

Share your live position over the mesh as ordinary chat messages, and put other people who do the same on your map. A position is sent as a [LOC]<lat>,<lon> message \u2014 the same coordinate format waypoints use, so it stays readable on other firmware and the phone app (it just looks like a coordinate to anything that doesn't know the tag).

This is independent of Auto-Advert and runs alongside it: Auto-Advert announces your presence as a 0-hop beacon for Nearby Nodes, while Live Share sends your position to a specific channel or contact you choose.

The tool holds both directions of sharing in one flat list. Navigate with UP/DOWN, change a value with LEFT/RIGHT (or Enter); Cancel/Back saves and returns to Tools.

Setting Options Notes Track loc ON / OFF Receive incoming [LOC] shares (DM, monitored channels, and room-server posts) and pin those senders on the map / in Nearby. Off by default. Auto share ON / OFF Periodically broadcast your own position to the target below while you move. To channel or contact Enter opens the Messages recipient chooser to pick the target channel or DM contact. Move 50 / 100 / 250 / 500 m Movement gate \u2014 only send after you've moved at least this far since the last share. Min gap 30 s / 1 / 2 / 5 min Minimum time between sends, so fast movement can't flood the channel. Heartbeat Off / 5 / 15 min Optional keep-alive: re-send even while stationary, so the other end knows you're still there.

How auto-share decides to send. With Auto share on, the device checks a few times a minute: it transmits when you've moved at least Move metres and at least Min gap has passed since the last send \u2014 so a stationary device stays silent unless a Heartbeat is set. It also sends once immediately when you enable sharing (or change the target), so the other end gets a fresh fix right away.

Receiving. With Track loc on, incoming [LOC] messages update a small live table (up to 16 nodes, entries expire ~20 min after the last update). DM shares are keyed by the sender's public key (reliable); channel and room-server shares are keyed by name (best-effort, since channel names are unsigned and a room post only carries a short sender prefix). Tracked nodes appear on the Trail Map as a filled diamond with the first two characters of their name, and in Nearby Nodes with their live distance/bearing.

One-shot share. To send your position once without enabling auto-share, use Tools \u203a Trail \u2192 Hold Enter \u2192 Share my pos \u2014 it builds a [LOC] message and hands it to the Messages screen to pick a recipient. There's also a shortcut from the home Map page: Hold Enter sends an immediate position update to your Live Share target while auto-sharing is on (toast Position shared), or opens the recipient picker if it isn't \u2014 so you never broadcast to a default channel by accident.

"},{"location":"solo_features/tools_screen/tools_screen/#locator","title":"Locator","text":"OLED E-Ink

A single geofence that beeps and shows an alert when you cross into or out of a radius. The target can be a saved waypoint (a fixed place \u2014 \"tell me when I'm back at camp\") or a live contact (a person sharing their position via Live Share \u2014 \"alert me when my friend gets near / falls behind\"). A waypoint target is a snapshot (coordinate + label copied), so it keeps working even if you later edit that waypoint; a contact target follows the person's latest shared position. Deleting the target's waypoint, or the target contact being removed from the contacts list, clears the Locator target back to none instead of leaving it pointed at something that's gone.

Navigate with UP/DOWN, change a value with LEFT/RIGHT (or Enter); Cancel/Back saves and returns to Tools.

Setting Options Notes Alert ON / OFF Master switch. Enabling without a target prompts you to pick one. Target none / person / waypoint Enter opens a picker \u2014 None first (clears the target), then favourites (offered even with no known position yet, so you can arm ahead of time), then any other contact with a currently-resolvable position (live-sharing or just last-advertised, e.g. a repeater), then waypoints; UP/DOWN + Enter to choose. LEFT/RIGHT quick-cycles the same set in place, including back to None. A person is shown with an @ prefix, plus a compact age tag (e.g. @Bob (5m)) when the position is last-advertised rather than a live share. Shows none until set. Radius 50 / 100 / 250 / 500 m / 1 km Geofence size. Mode Arrive / Leave / Both Which crossing fires the alert \u2014 entering the radius, leaving it, or both. Beeper ON / OFF Optional homing tone \u2014 shown only in Arrive / Both modes (see below).

Crossing alert. When armed with a target, the device watches its own GPS fix and fires the alert (a short melody plus an on-screen message) the moment you cross the radius, according to Mode. The wording adapts to the target \u2014 Arrived / Left for a waypoint, Near / Away for a person. The edge has a little hysteresis so a fix hovering right on the boundary doesn't chatter, and the first reading after arming only seeds the in/out state \u2014 it won't fire spuriously just because you armed it while already inside.

Following a person. Pick a favourite (or any contact with a known position) as the target and the geofence tracks the distance between you and them, so it works even while both of you move. The position is resolved with a fixed precedence: an active live [LOC] share wins, and with no current share it falls back to the contact's last-advertised GPS position \u2014 so a rarely-updating but stationary node (a repeater, or someone who shared a fix once) still works as a target. You can arm it ahead of time \u2014 choosing a favourite locks onto their identity (pubkey), and the alert starts working as soon as a position is known. Live following requires a DM share (a channel share carries no stable identity to lock onto); the last-advertised fallback works for any contact regardless.

Proximity beeper. With Beeper on, the device also ticks while you're inside the radius and shortens the gap between ticks the closer you get to the target \u2014 slow near the edge, rapid near the centre \u2014 like a homing beeper guiding you to the exact spot. It's silent outside the radius. Because the beeper is its own opt-in toggle, turning it on overrides the global buzzer mute (Settings \u203a Sound \u203a Buzzer) \u2014 it's an explicit \"I want to hear this\". Since homing only makes sense while you're approaching a target, the Beeper row appears only in Arrive or Both mode \u2014 it's hidden in Leave-only mode, and stays silent there even if it was switched on earlier. Otherwise it's independent of the crossing alert (which does follow the mute), so you can use either or both.

Setting the target from anywhere. Besides this screen's picker, the same active target can be set in one step with Set as target from Nearby Nodes' or Waypoints' own Hold Enter menu \u2014 handy so you don't need a detour through Tools. Picking from this screen's picker saves on exit (so LEFT/RIGHT cycling stays cheap); the per-item shortcuts save immediately and confirm with a Target set toast.

On the map. Whatever the active target is \u2014 person or waypoint \u2014 it's drawn as a flag marker on both the home Map preview and the full Trail Map, on top of any waypoint/contact it overlaps and folded into the frame so it never sits off-screen. This shows even when the Alert master switch is off, so a target you set purely to navigate to still appears.

OLED E-Ink OLED E-Ink

[!TIP] Mark the spot first with Tools \u203a Trail \u2192 Hold Enter \u2192 Mark here (or + Add by coords), then set it as the Locator target.

"},{"location":"solo_features/tools_screen/tools_screen/#compass","title":"Compass","text":"OLED E-Ink

A heads-up GPS compass. The L1 has no magnetometer, so the heading is the course over ground \u2014 derived from how your GPS position moves over the last few seconds. The display is a horizontal heading tape: a fixed travel-direction pointer sits at the centre and the N..E..S..W scale scrolls underneath it as you turn, so whatever is under the pointer is your current course. A large numeric readout below shows that course in degrees and cardinal (e.g. 145\u00b0 SE).

Because the heading comes from movement, it only updates while you are actually moving: standing still shows move to set heading (and navigation's Hdg line reads --). Gross GPS jumps are rejected so a single bad fix can't swing the heading. The heading source runs whenever there's a GPS fix \u2014 recording a trail is not required.

"},{"location":"solo_features/tools_screen/tools_screen/#ringtone-editor","title":"Ringtone Editor","text":"OLED E-Ink

A step sequencer for composing custom notification melodies. Two slots \u2014 Melody 1 and Melody 2 \u2014 switchable from within the editor.

Each melody supports up to 32 notes:

Parameter Options Pitch C / D / E / F / G / A / B / pause Octave 4 \u2013 7 Duration 1/4 / 1/8 / 1/16 / 1/32 BPM 60 / 90 / 120 / 150 / 180

Navigation in the editor:

Options menu:

Item Interaction Action Play / Stop Enter Preview the melody Melody 1 / 2 Enter Switch to the other slot Duration LEFT/RIGHT Cycle duration for selected note BPM LEFT/RIGHT Cycle tempo Insert Enter Insert a new note after the cursor Delete Enter Delete the note at cursor Save & Exit Enter Persist the melody and return to Tools Discard Enter Return to Tools without saving

Melodies can be assigned in Settings \u203a Sound (global default) or overridden per contact or channel from the Messages screen context menu.

"},{"location":"solo_features/tools_screen/tools_screen/#auto-reply-bot","title":"Auto-Reply Bot","text":"OLED E-Ink

Automatically replies to incoming messages that contain a configured trigger word (case-insensitive, contains match).

When the bot is enabled it listens to DMs by default. Channel monitoring is an optional addition \u2014 select a channel separately to activate it alongside DM mode.

Setting Description Enable ON / OFF \u2014 enables DM listening Channel OFF, or which channel to additionally monitor (cycles through your channels) Trigger DM Word or phrase that activates the DM reply (case-insensitive). A lone * means reply to every DM (away mode) and is shown as (any msg). Reply DM Reply text for DMs; supports {time}, {loc} and sensor placeholders Trigger Ch Independent trigger for the monitored channel. * means reply to every channel message \u2014 bounded by the per-channel cooldown, but use sparingly on a busy channel. Reply Ch Reply text for channel messages; supports the same placeholders Commands ON / OFF \u2014 answer ! query commands (see below) Quiet from/to Local-time window during which auto-replies stay silent; set both equal (OFF) to disable

The DM and channel triggers are independent, so you can run e.g. an away-message (*) in DMs while the channel reacts only to a specific keyword (or vice-versa).

The header shows a running count of auto-replies sent since boot.

Throttle. Auto-replies are rate-limited per contact (10 s), so a second sender is never starved while one contact is on cooldown. The channel bot keeps a single 10 s cooldown and won't echo a message identical to its own reply (so two bots running the same reply text on one channel can't ping-pong); the cooldown caps any residual back-and-forth.

Quiet hours suppress the push (trigger) replies between the configured local hours; a window where from is later than to wraps past midnight. Commands are a pull (explicitly requested), so they answer even during quiet hours.

"},{"location":"solo_features/tools_screen/tools_screen/#commands","title":"Commands","text":"

With Commands ON, a DM beginning with ! is answered with live node data, independent of the trigger:

Command Reply !ping pong !batt battery voltage !loc GPS coordinates (or no GPS) !time local time HH:MM !temp temperature (or n/a if no sensor) !hops how many hops the command message took to reach the node (direct if heard directly) !status combined battery / location / time !help list of available commands

Several commands can be combined in one message \u2014 !batt !time !hops is answered with a single 4.10V | 14:30 | 3 hops reply (one transmission). A message with no recognised command falls through to the trigger bot.

Commands also work on the monitored channel (the one selected for channel mode) \u2014 anyone there can query the node. Channel replies are broadcast to everyone, so unlike DM commands they respect quiet hours and use the shared per-channel cooldown. DM commands use the per-contact throttle.

"},{"location":"solo_features/tools_screen/tools_screen/#diagnostics","title":"Diagnostics","text":"OLED E-Ink

A single read-only screen of live device and mesh stats, refreshed once a second. On a small OLED the rows scroll with UP/DOWN; on a larger e-ink display they all fit at once.

Row Shows Uptime Time since boot (d hh:mm:ss) Total rx/tx All received / transmitted packets, summed across the categories below Msg Text and group-text packets, rx/tx Advert Advert packets, rx/tx Ack/Path Ack, path-return and trace packets, rx/tx Other Everything else (requests, responses, control, raw, \u2026), rx/tx Forwarded Packets this node actually re-transmitted as a repeater (reflects overhear suppression, if on) Heap free Free / total heap Stack free Current task's minimum-ever stack headroom Noise floor Live radio noise floor (dBm) RSSI/SNR Signal strength / signal-to-noise of the last received packet Pool free Free entries in the packet pool Queue Packets waiting in the outbound queue Errors Radio error flags since boot/reset \u2014 OK, or tokens F (queue full), C (CAD timeout), R (RX-start timeout)

The packet counters, Forwarded and Errors are cumulative since boot. Hold Enter opens a one-item Reset counters menu (Back dismisses it); the live readings (noise, RSSI/SNR, pool, queue, uptime) are not affected. Cancel/Back returns to the Tools list.

The counters make the repeater behaviour observable: Forwarded confirms the node is actually relaying (not just configured to), and Pool free / Queue show whether forwarding is exhausting the packet pool. See Tools \u203a Repeater for the relaying options.

"},{"location":"solo_features/tools_screen/tools_screen/#repeater","title":"Repeater","text":"OLED E-Ink

Turns the companion into a packet repeater while it keeps working as a normal companion \u2014 no separate firmware. By default, enabling it switches the radio to a dedicated repeater profile rather than relaying on whatever network you're chatting on (see Network below) \u2014 that matches the MeshCore community norm of repeaters sitting on a standard channel, not a private one. Loop-detection and an advert flood-depth cap are always applied. This screen keeps the toggle, the network/profile, and its flood-filter options together; live forwarding stats are on Tools \u203a Diagnostics.

Navigate with UP/DOWN; change a value with LEFT/RIGHT (or Enter for toggles). Cancel/Back saves and returns to Tools.

Setting Options Notes Repeater ON / OFF Master switch. The options below appear only while it is ON. Network Current / Custom Custom (default): enabling the repeater switches the radio to a dedicated profile (below) and disabling restores the companion's settings \u2014 so you can drop onto a separate repeater network and come back. A never-configured device seeds Custom with a frequency in the same band as your own network (433/868/915 MHz region), not a flat one-size-fits-all default \u2014 so it can't land outside what's legal for your region. Switching to Custom afterwards (if it was OFF and unconfigured) seeds it from your current settings instead. Current: relay on the companion's own frequency \u2014 opt-in; not the community norm. Rpt preset named presets (Custom only) Enter picks a community/saved preset for the repeater profile. Rpt freq chip range (Custom only) Enter opens the digit-by-digit editor (chip-validated bounds). Rpt SF / BW / CR 5\u201312 / 7.8\u2013500 kHz / 5\u20138 (Custom only) LEFT/RIGHT to adjust the profile's spreading factor, bandwidth, coding rate. Skip advert ON / OFF Don't re-flood advert packets (the highest-volume flood traffic); messages and acks still relay. Max hops OFF / 1\u20138 Drop a flood packet once it has already travelled this many hops. Yield OFF / x2\u2013x9 Scales the retransmit delay for forwarded floods only (your own sends are unaffected), so a mobile companion defers to better-sited fixed repeaters. Widens the window for Suppress dup. Min SNR OFF / \u221220\u202610 dB Drop a flood copy received below this signal-to-noise threshold, so marginal fringe traffic isn't re-flooded. Suppress dup ON / OFF If the same flood packet is overheard from another node while still queued to retransmit, cancel our copy \u2014 a peer already relayed it. Cuts redundant airtime in dense meshes; pairs with Yield.

The five flood filters are opt-in (default OFF, so a plain repeater is unaffected) and act on flood traffic only \u2014 on a direct route this node is the named next hop, so it never drops those.

Same network vs. separate network. With Network = Current (or a Custom profile set equal to your companion settings) the repeater stays on your own network \u2014 you keep messaging while relaying. With a different Custom profile the device moves entirely onto that network while relaying (a single radio can't be on two at once) and returns to your companion network when the repeater is switched off. The profile also re-applies after a reboot if the repeater was left on.

While the repeater is on, a \u00bb indicator appears in the status bar (same blink convention as the auto-advert and trail markers) so you can tell it's relaying at a glance. Two radio settings are also overridden while relaying and restored afterwards: Settings \u203a Radio \u203a Pwr save is forced off (a repeater must listen continuously) and Auto pwr is forced off (a repeater holds full TX power for consistent relay reach). Both show -- in Settings while the repeater is on.

Live forwarding stats \u2014 Forwarded, Pool free, Queue \u2014 are shown on Tools \u203a Diagnostics (this screen is config-only).

"}]} \ No newline at end of file diff --git a/solo_features/tools_screen/tools_screen/index.html b/solo_features/tools_screen/tools_screen/index.html index 054ccbbf..3dc2fafe 100644 --- a/solo_features/tools_screen/tools_screen/index.html +++ b/solo_features/tools_screen/tools_screen/index.html @@ -1349,6 +1349,17 @@