Core Features
Unread Message Tracking: Added persistent unread counts for contacts and channels with visual badges
Message Deletion: Users can now long-press to delete individual messages in chats and channels
SMAZ Compression: Added per-contact compression settings (previously only channels)
UTF-8 Length Limiting: Text inputs now enforce protocol byte limits correctly
Channel Message Paths: New screen to visualize packet routing through repeater network with map view
Protocol Updates
Added maxContactMessageBytes() and maxChannelMessageBytes() helpers for message length validation
Changed channel PSK format from Base64 to Hexadecimal (breaking change)
Added app version field to connection handshake frame
UI Improvements
Unread badges on all contact and channel list items
Enhanced message bubbles with path visualization for channel messages
Character count displays in message input fields
Improved repeater CLI screen functionality
New Files
lib/storage/unread_store.dart - Unread tracking persistence
lib/storage/contact_settings_store.dart - Per-contact SMAZ settings
lib/widgets/unread_badge.dart - Unread count indicator
lib/helpers/utf8_length_limiter.dart - Byte-aware text input formatter
lib/screens/channel_message_path_screen.dart - Packet path visualization
"Reboots the device. (note, you'll prob get 'Timeout' which is normal)",
),
const_CommandHelpEntry(
command:'clock',
description:"Displays current time per device's clock.",
),
const_CommandHelpEntry(
command:'password {new-password}',
description:'Sets a new admin password for the device.',
),
const_CommandHelpEntry(
command:'ver',
description:'Shows the device version and firmware build date.',
),
const_CommandHelpEntry(
command:'clear stats',
description:'Resets various stats counters to zero.',
),
];
finalsettingsCommands=[
const_CommandHelpEntry(
command:'set af {air-time-factor}',
description:'Sets the air-time-factor.',
),
const_CommandHelpEntry(
command:'set tx {tx-power-dbm}',
description:'Sets LoRa transmit power in dBm. (reboot to apply)',
),
const_CommandHelpEntry(
command:'set repeat {on|off}',
description:'Enables or disables the repeater role for this node.',
),
const_CommandHelpEntry(
command:'set allow.read.only {on|off}',
description:
"(Room server) If 'on', then login in blank password will be allowed, but cannot Post to room. (just read only)",
),
const_CommandHelpEntry(
command:'set flood.max {max-hops}',
description:
'Sets the maximum number of hops of inbound flood packet (if >= max, packet is not forwarded)',
),
const_CommandHelpEntry(
command:'set int.thresh {db}',
description:
'Sets the Interference Threshold (in DB). Default is 14. Set to 0 to disable channel interference detection.',
),
const_CommandHelpEntry(
command:'set agc.reset.interval {seconds}',
description:
'Sets the interval to reset the Auto Gain Controller. Set to 0 to disable.',
),
const_CommandHelpEntry(
command:'set multi.acks {0|1}',
description:"Enables or disables the 'double ACKs' feature.",
),
const_CommandHelpEntry(
command:'set advert.interval {minutes}',
description:
'Sets the timer interval in minutes to send a local (zero-hop) advertisement packet. Set to 0 to disable.',
),
const_CommandHelpEntry(
command:'set flood.advert.interval {hours}',
description:
'Sets the timer interval in hours to send a flood advertisement packet. Set to 0 to disable.',
),
const_CommandHelpEntry(
command:'set guest.password {guess-password}',
description:
'Sets/updates the guest password. (for repeaters, guest logins can send the "Get Stats" request)',
),
const_CommandHelpEntry(
command:'set name {name}',
description:'Sets the advertisement name.',
),
const_CommandHelpEntry(
command:'set lat {latitude}',
description:'Sets the advertisement map latitude. (decimal degrees)',
),
const_CommandHelpEntry(
command:'set lon {longitude}',
description:'Sets the advertisement map longitude. (decimal degrees)',
),
const_CommandHelpEntry(
command:'set radio {freq},{bw},{sf},{cr}',
description:
'Sets completely new radio params, and saves to preferences. Requires a "reboot" command to apply.',
),
const_CommandHelpEntry(
command:'set rxdelay {base}',
description:
'Sets (experimental) base (must be > 1 for effect) for applying slight delay to received packets, based on signal strength/score. Set to 0 to disable.',
),
const_CommandHelpEntry(
command:'set txdelay {factor}',
description:
'Sets a factor multiplied with time-on-air for a flood-mode packet and with a randomized slot system, to delay its forwarding. (to decrease likelihood of collisions)',
),
const_CommandHelpEntry(
command:'set direct.txdelay {factor}',
description:
'Same as txdelay, but for applying a random delay to the forwarding of direct-mode packets.',
),
const_CommandHelpEntry(
command:'set bridge.enabled {on|off}',
description:'Enable/Disable bridge.',
),
const_CommandHelpEntry(
command:'set bridge.delay {0-10000}',
description:'Set delay before retransmitting packets.',
),
const_CommandHelpEntry(
command:'set bridge.source {rx|tx}',
description:
'Choose wether the bridge will retransmit received packets or transmitted packets.',
),
const_CommandHelpEntry(
command:'set bridge.baud {speed}',
description:'Set serial link baudrate for rs232 bridges.',
),
const_CommandHelpEntry(
command:'set bridge.secret {shared-secret}',
description:'Set bridge secret for espnow bridges.',
),
const_CommandHelpEntry(
command:'set adc.multiplier {factor}',
description:
'Sets custom factor to adjust reported battery voltage (only supported on select boards).',
'Sets temporary radio params for the given number of {minutes}, reverting to original radio params afterward. (does NOT save to preferences).',
),
const_CommandHelpEntry(
command:'setperm {pubkey-hex} {permissions}',
description:
'Modifies the ACL. Removes matching entry (by pubkey prefix) if "permissions" is zero. Adds new entry if pubkey-hex is full length and is not currently in ACL. Updates entry by matching pubkey prefix. Permission bits vary per firmware role, but low 2 bits are: 0 (Guest), 1 (Read only), 2 (Read write), 3 (Admin)',
),
];
finalbridgeCommands=[
const_CommandHelpEntry(
command:'get bridge.type',
description:'Gets bridge type none, rs232, espnow',
),
];
finalloggingCommands=[
const_CommandHelpEntry(
command:'log start',
description:'Starts packet logging to file system.',
),
const_CommandHelpEntry(
command:'log stop',
description:'Stops packet logging to file system.',
),
const_CommandHelpEntry(
command:'log erase',
description:'Erases the packet logs from file system.',
),
];
finalneighborCommands=[
const_CommandHelpEntry(
command:'neighbors',
description:
'Shows a list of other repeater nodes heard via zero-hop adverts. Each line is {id-prefix-hex}:{timestamp}:{snr-times-4}',
),
const_CommandHelpEntry(
command:'neighbor.remove {pubkey-prefix}',
description:
'Removes first matching entry (by pubkey prefix (hex)), from neighbors list.',
),
];
finalregionCommands=[
const_CommandHelpEntry(
command:'region',
description:
'(serial only) Lists all defined regions and current flood permissions.',
),
const_CommandHelpEntry(
command:'region load',
description:
'NOTE: this is a special multi-command invocation. Each subsequent command is a region name (indented with spaces to indicate parent hierarchy, with one space at minimum). Terminated by sending a blank line/command.',
),
const_CommandHelpEntry(
command:'region get {* | name-prefix}',
description:
'Searches for region with given name prefix (or "*" for the global scope). Replies with "-> {region-name} ({parent-name}) {\'F\'}"',
),
const_CommandHelpEntry(
command:'region put {name} {* | parent-name-prefix}',
description:'Adds or updates a region definition with given name.',
),
const_CommandHelpEntry(
command:'region remove {name}',
description:
'Removes a region definition with given name. (must match exactly, and have no child regions)',
),
const_CommandHelpEntry(
command:'region allowf {* | name-prefix}',
description:
"Sets the 'F'lood permission for the given region. ('*' for the global/legacy scope)",
),
const_CommandHelpEntry(
command:'region denyf {* | name-prefix}',
description:
"Removes the 'F'lood permission for the given region. (NOTE: at this stage NOT advised to use this on the global/legacy scope!!)",
),
const_CommandHelpEntry(
command:'region home',
description:
"Replies with the current 'home' region. (Note applied anywhere yet, reserved for future)",
),
const_CommandHelpEntry(
command:'region home {* | name-prefix}',
description:"Sets the 'home' region.",
),
const_CommandHelpEntry(
command:'region save',
description:'Persists the region list/map to storage.',
),
];
finalgpsCommands=[
const_CommandHelpEntry(
command:'gps',
description:
'Gives status of gps. When gps is off, it replies only off, if on it replies with on, {status}, {fix}, {sat count}',
),
const_CommandHelpEntry(
command:'gps {on|off}',
description:'Toggles gps power state.',
),
const_CommandHelpEntry(
command:'gps sync',
description:'Syncs node time with gps clock.',
),
const_CommandHelpEntry(
command:'gps setloc',
description:"Sets node's position to gps coordinates and save preferences.",
),
const_CommandHelpEntry(
command:'gps advert',
description:
"Gives location advert configuration of the node:\n- none: don't include location in adverts\n- share: share gps location (from SensorManager)\n- prefs: advert the location stored in preferences",
'set radio {freq},{bw},{sf},{cr} - Sets completely new radio params, and saves to preferences. Requires a "reboot" command to apply.',
'set rxdelay {base} - Sets (experimental) base (must be > 1 for effect) for applying slight delay to received packets, based on signal strength/score. Set to 0 to disable.',
'set txdelay {factor} - Sets a factor multiplied with time-on-air for a flood-mode packet and with a randomized slot system, to delay its forwarding. (to decrease likelihood of collisions)',
'set direct.txdelay {factor} - Same as txdelay, but for applying a random delay to the forwarding of direct-mode packets.',
'set bridge.delay {0-10000} - Set delay before retransmitting packets.',
'set bridge.source {rx|tx} - Choose wether the bridge will retransmit received packets or transmitted packets.',
'set bridge.baud {speed} - Set serial link baudrate for rs232 bridges.',
'set bridge.secret {shared-secret} - Set bridge secret for espnow bridges.',
'set adc.multiplier {factor} - Sets custom factor to adjust reported battery voltage (only supported on select boards).',
'tempradio {freq},{bw},{sf},{cr},{minutes} - Sets temporary radio params for the given number of {minutes}, reverting to original radio params afterward. (does NOT save to preferences).',
'setperm {pubkey-hex} {permissions} - Modifies the ACL. Removes matching entry (by pubkey prefix) if "permissions" is zero. Adds new entry if pubkey-hex is full length and is not currently in ACL. Updates entry by matching pubkey prefix. Permission bits vary per firmware role, but low 2 bits are: 0 (Guest), 1 (Read only), 2 (Read write), 3 (Admin)',
'region commands have been introduced to manage region definitions and permissions.',
'region - (serial only) Lists all defined regions and current flood permissions.',
'region load - NOTE: this is a special multi-command invocation. Each subsequent command is a region name (indented with spaces to indicate parent hierarchy, with one space at minimum). Terminated by sending a blank line/command.',
"region get {* | name-prefix} - Searches for region with given name prefix (or '*' for the global scope). Replies with \"-> {region-name} ({parent-name}) {'F'}\"",
'region put {name} {* | parent-name-prefix} - Adds or updates a region definition with given name.',
'region remove {name} - Removes a region definition with given name. (must match exactly, and have no child regions)',
"region allowf {* | name-prefix} - Sets the 'F'lood permission for the given region. ('*' for the global/legacy scope)",
"region denyf {* | name-prefix} - Removes the 'F'lood permission for the given region. (NOTE: at this stage NOT advised to use this on the global/legacy scope!!)",
"region home - Replies with the current 'home' region. (Note applied anywhere yet, reserved for future)",
"region home {* | name-prefix} - Sets the 'home' region.",
'region save - Persists the region list/map to storage.',
]),
_buildHelpSection(
context,
'Region Management (Repeater only)',
regionCommands,
note:
'Region commands have been introduced to manage region definitions and permissions.',
),
constSizedBox(height:16),
_buildHelpSection('GPS Management',[
'gps command has been introduced to manage location related topics.',
'gps - Gives status of gps. When gps is off, it replies only off, if on it replies with on, {status}, {fix}, {sat count}',
'gps {on|off} - Toggles gps power state.',
'gps sync - Syncs node time with gps clock.',
"gps setloc - Sets node's position to gps coordinates and save preferences.",
'gps advert - Gives location advert configuration of the node:',
"none: don't include location in adverts",
'share: share gps location (from SensorManager)',
'prefs: advert the location stored in preferences',