diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 78c2612a..5f9f1fde 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -44,27 +44,54 @@ static inline float defaultRepeaterFreqForBand(float companion_freq) { #define ADVERT_SOUND_SCOPE_ZERO_HOP 1 struct NodePrefs { // persisted to file - float airtime_factor; + // Fields below are grouped thematically for readability. This grouping is + // purely a source-level convenience: on-disk order is defined solely by the + // explicit rd()/wr() call sequence in DataStore::loadPrefsInt()/savePrefs(), + // not by this struct's declaration order, so reordering fields here never + // touches the file format. That file format is still strictly append-only + // (see the serialization tripwire below) — a handful of fields below note + // that their on-disk position is at the struct's tail even though they're + // declared here beside their logical siblings. + + // ── Identity & scope ────────────────────────────────────────────────── char node_name[32]; + char default_scope_name[31]; + uint8_t default_scope_key[16]; + + // ── Radio (companion's own link) ────────────────────────────────────── + float airtime_factor; float freq; uint8_t sf; uint8_t cr; - uint8_t multi_acks; - uint8_t manual_add_contacts; float bw; int8_t tx_power_dbm; - uint8_t telemetry_mode_base; - uint8_t telemetry_mode_loc; - uint8_t telemetry_mode_env; float rx_delay_base; - uint32_t ble_pin; - uint8_t advert_loc_policy; - uint8_t buzzer_quiet; - uint8_t buzzer_volume; // 0=min..4=max, default 4 - uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled) - uint32_t gps_interval; // GPS duty-cycle sleep window in seconds (0 = disabled, GPS stays continuous) - uint8_t autoadd_config; // bitmask for auto-add contacts config + uint8_t multi_acks; + uint8_t path_hash_mode; // which path mode to use when sending + // Hardware duty-cycle receive (battery saver): 0=continuous RX (default), 1=on. + // The SX126x cycles RX↔sleep on its own and wakes on a preamble — cuts average + // RX current at the cost of a little receive latency. See RadioLibWrapper + // power-save (startReceiveDutyCycleAuto). + uint8_t rx_powersave; uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted) + // Adaptive Power Control: 0=off (fixed tx_power_dbm, default), 1=on. When on, + // tx_power_dbm is treated as a ceiling and the radio's actual power is lowered + // at runtime on strong links (good ACK SNR), saving TX energy. Never persisted + // below the ceiling, so disabling restores the user's configured power. + uint8_t tx_apc; + // RSSI-based interference detection (relative to the radio's own noise + // floor) and hardware Channel Activity Detection before TX. See + // MyMesh::getInterferenceThreshold()/getCADEnabled() — CAD is also + // auto-enabled whenever rx_powersave is active, regardless of this flag, + // since duty-cycle sleep leaves the noise floor stale. Both default 0/off, + // and neither has a Settings/CLI toggle yet on companion_radio — persisted + // and wired for a future manual override. + uint8_t interference_threshold; + uint8_t cad_enabled; + // Auto-resend for on-device DMs: number of extra send attempts (0..5) made when + // no end-to-end ACK arrives before the deadline, before the delivery marker + // shows ✗. 0 = no auto-resend (single attempt). Default 2. + uint8_t dm_resend_count; // External LoRa FEM gain (LNA/PA), from upstream companion-v1.17.1 — // board-level only right now (BaseCustomBoard::setLoRaFemLnaEnabled/ // PaGainEnabled, both default no-op false), no companion CLI/UI to set @@ -73,122 +100,6 @@ struct NodePrefs { // persisted to file // of the DataStore save/load tripwire below. uint8_t radio_fem_rxgain; uint8_t radio_fem_txgain; - uint8_t client_repeat; - uint8_t path_hash_mode; // which path mode to use when sending - uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) - char default_scope_name[31]; - uint8_t default_scope_key[16]; - uint8_t display_brightness; // 0=min..4=max, default 2 (medium) - uint16_t auto_off_secs; // display auto-off: 0=never, else seconds (default 15) - uint8_t auto_lock; // 0=disabled, 1=lock screen when display turns off - int8_t tz_offset_hours; // timezone offset from UTC, -12..+14 (default 0) - uint16_t low_batt_mv; // auto-shutdown threshold: 0=disabled, 3000-3500 mV - uint8_t batt_display_mode; // 0=icon, 1=percent, 2=voltage - char custom_msgs[10][140]; // user-defined quick messages (supports {loc}, {time}) - uint64_t ch_notif_override; // bitmask: bit i = channel i has explicit notification setting [del→onChannelRemoved] - uint64_t ch_notif_muted; // bitmask: bit i = channel i muted (only if override bit set) [del→onChannelRemoved] - uint8_t dm_show_all; // 0=favourites only, 1=all chat contacts (default) - uint8_t room_fav_only; // 0=all room servers (default), 1=favourites only - uint8_t ringtone_bpm_idx; // index into {60,90,120,150,180} - uint8_t ringtone_len; // number of notes in custom ringtone (0 = use default) - uint8_t ringtone_notes[32]; // packed: bits0-2=pitch, bits3-4=octave-4, bits5-6=dur_idx - uint16_t home_pages_mask; // bitmask of visible home pages (bit0=Clock..bit8=Shutdown); 0=all visible - uint8_t bot_enabled; // 0=disabled, 1=DM trigger-reply active — DM only; channel/room have their own bot_channel_enabled/bot_room_enabled and don't depend on this - uint8_t bot_channel_enabled; // 0=disabled, 1=channel bot active for bot_channel_idx - uint8_t bot_channel_idx; // channel index for channel bot [del→onChannelRemoved] - char bot_trigger[64]; // DM trigger phrase (case-insensitive contains; "*" = any DM) - char bot_reply_dm[140]; // auto-reply text for DM - char bot_reply_ch[140]; // auto-reply text for channel - char bot_trigger_ch[64]; // channel trigger phrase (independent of DM; "*" = any channel msg) - uint8_t bot_commands_enabled; // 0=off, 1=answer !ping/!batt/!loc/!time/!help DM commands — DM only, see bot_commands_ch/bot_commands_room below for the other two targets - uint8_t bot_quiet_start; // quiet-hours start hour, local 0-23 (start==end → disabled) - uint8_t bot_quiet_end; // quiet-hours end hour, local 0-23 - uint8_t clock_hide_seconds; // 0=show HH:MM:SS/refresh 1s (default), 1=hide/refresh 60s - uint8_t clock_12h; // 0=24h (default), 1=12h with AM/PM - uint8_t buzzer_auto; // 0=manual (default), 1=auto-mute when BT connected - struct DmNotifEntry { uint8_t prefix[4]; uint8_t state; }; // state: 0=default,1=muted,2=force-on - static const int DM_NOTIF_TABLE_MAX = 16; - DmNotifEntry dm_notif[DM_NOTIF_TABLE_MAX]; // 16*5 = 80 bytes [del→onContactRemoved] - uint8_t dashboard_fields[3]; // 0=None,1=Batt V,2=Temp,3=Hum,4=Pres,5=GPS,6=Alt,7=Lux,8=CO2,9=Nodes,10=Msgs,11=Batt % - uint32_t advert_auto_interval_sec; // periodic 0-hop advert with GPS: 0=off, else seconds - // Second melody slot (same packing as ringtone_*) - uint8_t ringtone2_bpm_idx; - uint8_t ringtone2_len; - uint8_t ringtone2_notes[32]; - // Global melodies for notifications: 0=built-in, 1=melody1, 2=melody2, 3=none - uint8_t notif_melody_dm; - uint8_t notif_melody_ch; - uint8_t notif_melody_ad; - // Advert sound filter: 0=all adverts, 1=zero-hop only - uint8_t advert_sound_scope; - // Per-channel melody override (2 bitmasks, 1 bit per channel) - uint64_t ch_notif_melody_set; // bit i = channel i has explicit melody [del→onChannelRemoved] - uint64_t ch_notif_melody_2; // bit i = use melody 2 (else melody 1, when set bit is set) - // Per-DM melody table - struct DmMelodyEntry { uint8_t prefix[4]; uint8_t slot; }; // slot: 0=global,1=melody1,2=melody2 - static const int DM_MELODY_TABLE_MAX = 16; - DmMelodyEntry dm_melody[DM_MELODY_TABLE_MAX]; // [del→onContactRemoved] - uint8_t use_lemon_font; // 0=default Adafruit font, 1=Lemon font (Unicode, pixel-accurate wrap) - uint8_t display_rotation; // 0-3; only used on e-ink displays - // Home screen page order: each byte = HomePageBit + 1. 0 terminates the list. - // Validity gated by page_order_set magic (see below) — not by entry value range, - // so a junk byte in 1..HPB_COUNT cannot trigger custom-order mode. - // Declared as a literal (not HPB_COUNT) so the field offset stays stable across - // builds that add HomePageBit entries. The first PAGE_ORDER_LEN_V1 bytes persist - // at this offset (backward-compatible); the remaining slots live at the file - // tail (see DataStore) so pre-0x0019 saves still load without shifting. - uint8_t page_order[13]; - uint8_t joystick_rotation; // 0-3 steps CW; independent of display_rotation - uint8_t eink_full_refresh_every; // index into {0,5,10,20,30}: full refresh every N partials (0=off) - uint8_t page_order_set; // 0xA5 = page_order is user-configured; anything else = use default - static const uint8_t PAGE_ORDER_MAGIC = 0xA5; - - // Favourites dial: 6 pinned targets. Layout transposes between landscape - // (3×2) and portrait (2×3). What a slot holds depends on favourite_kinds[] - // (stored at the struct tail, see there): - // FAV_KIND_CONTACT — first 6 bytes of pub_key; covers chat contacts and - // room servers alike. All-zero = empty slot (a real pub_key starts with - // 6 zero bytes with probability 2^-48). - // FAV_KIND_CHANNEL — channel index in byte 0, rest zero. Never empty: - // channel 0 is all-zero, so emptiness is decided by the kind first. - static const uint8_t FAVOURITES_COUNT = 6; - static const uint8_t FAVOURITE_PREFIX_LEN = 6; - static const uint8_t FAV_KIND_CONTACT = 0; - static const uint8_t FAV_KIND_CHANNEL = 1; - static const uint8_t FAV_KIND_MAX = 1; - uint8_t favourite_contacts[FAVOURITES_COUNT][FAVOURITE_PREFIX_LEN]; // [del→onContactRemoved] - - // GPS trail cadence. Logging on/off is a runtime state (Tools › Trail), - // not a persisted preference. - uint8_t trail_interval_idx; // reserved — sampling cadence is now fixed at TrailStore::SAMPLING_SECS - uint8_t trail_min_delta_idx; // min-distance gate level (0=finest..3); metres or feet per units_imperial - uint8_t trail_units_idx; // legacy: old combined speed/pace+unit index (km/h, mph, min/km, min/mi) - - uint64_t ch_fav_bitmask; // bit i = channel i is marked as favourite [del→onChannelRemoved] - uint8_t ch_fav_only; // 0=show all channels (default), 1=show favourites only - - // Global measurement system for every distance/speed shown in the UI - // (Nearby, Trail, navigate-to-point). 0=metric (default), 1=imperial. - uint8_t units_imperial; - // Trail Summary readout: 0=speed (km/h or mph), 1=pace (min/km or min/mi). - // The km-vs-mi choice now comes from units_imperial, so this is just the mode. - uint8_t trail_show_pace; - // Hardware duty-cycle receive (battery saver): 0=continuous RX (default), 1=on. - // The SX126x cycles RX↔sleep on its own and wakes on a preamble — cuts average - // RX current at the cost of a little receive latency. See RadioLibWrapper - // power-save (startReceiveDutyCycleAuto). - uint8_t rx_powersave; - // Adaptive Power Control: 0=off (fixed tx_power_dbm, default), 1=on. When on, - // tx_power_dbm is treated as a ceiling and the radio's actual power is lowered - // at runtime on strong links (good ACK SNR), saving TX energy. Never persisted - // below the ceiling, so disabling restores the user's configured power. - uint8_t tx_apc; - - // Auto-resend for on-device DMs: number of extra send attempts (0..5) made when - // no end-to-end ACK arrives before the deadline, before the delivery marker - // shows ✗. 0 = no auto-resend (single attempt). Default 2. - uint8_t dm_resend_count; - // User-saved radio presets, written by the "Save current..." entry in the // shared preset picker (Settings > Radio and Tools > Repeater both populate // these same slots). name[0] == '\0' marks an empty slot. @@ -202,6 +113,8 @@ struct NodePrefs { // persisted to file static const uint8_t USER_RADIO_PRESET_MAX = 4; UserRadioPreset user_radio_presets[USER_RADIO_PRESET_MAX]; + // ── Repeater ─────────────────────────────────────────────────────────── + uint8_t client_repeat; // Repeater forwarding filters — only consulted when client_repeat is on, via // MyMesh::allowPacketForward(). Both default to off (0) so behaviour is // unchanged until the user opts in (Tools > Repeater). @@ -216,16 +129,27 @@ struct NodePrefs { // persisted to file // fringe traffic isn't re-flooded. REPEAT_SNR_DISABLED (-128) = off. // repeat_suppress_dup: 1 = cancel a queued retransmit when the same flood is // overheard from another node first (less redundant airtime in dense mesh). - // (repeat_scope_only / repeat_extra_scopes belong to this group too, but live - // at the struct tail — the file format is strictly append-only, so a new - // field can never be slotted in next to its logical siblings.) uint8_t repeat_skip_adverts; uint8_t repeat_max_hops; uint8_t repeat_delay_boost; int8_t repeat_min_snr; static const int8_t REPEAT_SNR_DISABLED = -128; uint8_t repeat_suppress_dup; - + // On-disk position is at the struct's append-only tail (see the + // serialization tripwire below), even though grouped here with the rest of + // repeat_* for readability. + // repeat_scope_only: 1 = only forward flood packets matching this device's + // own scope (Settings > Radio > Scope, default_scope_key) or one of the + // repeat_extra_scopes below — drops unscoped floods and floods tagged for + // a different community. A no-op (forwards everything, unchanged) while + // no scope is configured at all, so enabling this on an unconfigured + // device can't silently blackhole all flood traffic. + uint8_t repeat_scope_only; + // Extra region names this repeater also relays for, beyond its own + // Settings > Radio > Scope (comma-separated, e.g. "eu,de") — see + // MyMesh::rebuildRepeatScopes(). Relay-only: never affects what scope the + // companion's own messages send under, only what repeat_scope_only accepts. + char repeat_extra_scopes[24]; // Optional dedicated radio profile for repeater mode. When repeater_use_profile // is 1, enabling the repeater switches the radio to repeater_freq/bw/sf/cr and // disabling restores the companion's freq/bw/sf/cr (the fields above). 0 = the @@ -240,106 +164,48 @@ struct NodePrefs { // persisted to file uint8_t repeater_sf; uint8_t repeater_cr; - // Track positions shared by other nodes via [LOC] messages (LiveTrackStore). - // 0 = ignore shared positions (default), 1 = parse incoming DM/channel [LOC] - // shares and update the live-track table (Nearby "Live" view / map). - uint8_t track_shared_loc; + // ── Telemetry ────────────────────────────────────────────────────────── + uint8_t telemetry_mode_base; + uint8_t telemetry_mode_loc; + uint8_t telemetry_mode_env; - // Live location sharing — a message-based "beacon". When enabled, the device - // periodically sends a [LOC] message to the chosen target while it moves. - // Configured from the Map (Trail screen) "Live share" menu. - uint8_t loc_share_enabled; // 0=off (default), 1=auto-sharing on - uint8_t loc_share_target_type; // 0=channel, 1=DM contact - uint8_t loc_share_channel_idx; // target channel index (when target_type==0) [del→onChannelRemoved] - uint8_t loc_share_dm_prefix[6]; // target contact pubkey prefix (when target_type==1) [del→onContactRemoved] - uint8_t loc_share_move_idx; // movement gate level (index into locShareMoveMeters) - uint8_t loc_share_interval_idx; // min send interval (index into locShareIntervalSecs) - uint8_t loc_share_heartbeat_idx; // stationary heartbeat (index into locShareHeartbeatSecs) + // ── Contacts (auto-add) ─────────────────────────────────────────────── + uint8_t manual_add_contacts; + uint8_t autoadd_config; // bitmask for auto-add contacts config + uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) - // Locator — a single geofence around a saved point. When enabled the device - // watches its own GPS fix and beeps + shows an alert when it crosses into - // (arrive) or out of (leave) the radius. The target coordinate/label is a - // snapshot of a chosen waypoint, so the alert survives the waypoint being - // edited or deleted. Configured from Tools › Locator. - uint8_t locator_enabled; // 0=off (default), 1=armed - uint8_t locator_has_target; // 0=no target chosen yet, 1=target set - uint8_t locator_radius_idx; // index into locatorRadiusMeters - uint8_t locator_mode; // 0=arrive, 1=leave, 2=both - int32_t locator_lat_1e6; // target latitude (1e6-scaled; last-known for a contact) - int32_t locator_lon_1e6; // target longitude (1e6-scaled; last-known for a contact) - char locator_label[12]; // target name for the alert text (WAYPOINT_LABEL_LEN) - // Target can be a static waypoint or a live contact: for a contact the engine - // re-reads the latest [LOC] position each evaluation (keyed by pubkey prefix), - // so the geofence follows a moving person ("alert when my friend is near"). - uint8_t locator_target_kind; // 0=waypoint (static), 1=live contact - uint8_t locator_key[6]; // contact pubkey prefix when target_kind==1 [del→onContactRemoved] + // ── Connectivity (BLE) ──────────────────────────────────────────────── + uint32_t ble_pin; - // Trail auto-pause — when tracking, automatically freeze the trail (timer + - // sampling) after the device has sat still for this long, and resume on the - // next real movement. 0 = off. Index into trailAutoPauseSecs. - uint8_t trail_autopause_idx; - - // Locator proximity beeper — when on (and the alert is armed with a target), - // the device ticks while inside the radius and shortens the gap between ticks - // the closer it gets to the target, like a homing beeper. Independent of the - // discrete arrive/leave alert (locator_mode). - uint8_t locator_beeper; // 0=off (default), 1=on - - // GPS averaging for waypoint marking — when set, "Mark here" samples the GPS - // fix for this many seconds and stores the mean position, for a more accurate - // mark than a single instantaneous fix. 0 = off (instant mark, the default). - // Index into gpsAvgSecs(). [Tools › Trail › Settings › Mark avg] - uint8_t gps_avg_idx; - - // Alarm clock — a wake alarm, configured from the Clock page (Enter › Alarm). - // Stored as a local time-of-day; the actual fire instant is (re)computed as an - // absolute time in tickBackground(), which is what makes it robust to RTC - // re-syncs: the mesh (every inbound packet), the companion app, GPS and the - // CLI can all jump getCurrentTime() at any moment, but an absolute target - // instant stays correct across small corrections and still fires (late) if - // the clock jumps over it. With alarm_repeat_mask == 0 it's one-shot: fires - // once, then alarm_on clears. A non-zero mask instead re-arms it for the next - // matching weekday (see UITask::computeAlarmNextFire/evaluateAlarm) and - // alarm_on stays set. The minutnik (countdown) and stoper (stopwatch) are - // runtime-only and not persisted. - uint8_t alarm_on; // 0=off (default), 1=armed - uint8_t alarm_hour; // 0-23, local time - uint8_t alarm_min; // 0-59 - // Repeat days as a bitmask, bit i = 1< // Keyboard). 0=ABC grid, alphabetical order (default), 1=T9 multi-tap // (phone-keypad groups, cycled with repeated Enter presses — see KeyboardWidget.h). uint8_t keyboard_type; - // Additional (non-Latin) keyboard alphabet, orthogonal to keyboard_type above // — either layout style (ABC grid or T9) can show any alphabet's characters. // 0 = Latin only (default): the keyboard's existing #@/abc page-cycle key @@ -366,28 +232,123 @@ struct NodePrefs { // persisted to file static const char* L[KB_ALPHABET_COUNT] = { "Latin", "Cyrillic", "Greek" }; return L[idx < KB_ALPHABET_COUNT ? idx : 0]; } - - // Auto-save the live GPS trail to /trail on shutdown (covers the low-battery - // auto-shutdown, which otherwise loses the whole route). Only writes when the - // trail has points and the toggle is on. 0 = off (default), 1 = on. - // [Tools › Trail › Settings › Auto-save] - uint8_t trail_autosave_lowbatt; - - // Appended at the tail (see the alarm doc comment above for the field itself - // and the serialization tripwire below for why new fields land here, not - // inline with alarm_on/hour/min). - uint8_t alarm_repeat_mask; - - // Appended at the tail (see the keyboard_alt_alphabet doc comment above). + // On-disk position is at the struct's append-only tail (see the + // serialization tripwire below); declared here with keyboard_type for + // readability. uint8_t keyboard_alt_alphabet; + // Which script (KB_ALPHABET_LATIN_ONLY/CYRILLIC/GREEK) occupies the on-screen + // keyboard's page 0 -- its default/opening page -- vs. keyboard_alt_alphabet + // above, which occupies page 1. Settings > Keyboard's Main/Additional rows. + // Equal to keyboard_alt_alphabet means no second page (see KeyboardWidget's + // hasAltAlphabet()). On-disk position is at the tail (see the serialization + // tripwire below); default 0 (Latin) matches the keyboard's original always- + // Latin-main behaviour for upgraders. + uint8_t keyboard_main_alphabet; + // Settings > Keyboard's "Ext. KB" row (boards with a second I2C bus for an + // optional CardKB, see ENV_PIN_SDA/ENV_PIN_SCL, only). When on, the + // on-screen keyboard skips drawing its full letter grid + special-row icons + // -- an external-keyboard typist never looks at them -- and shows a compact + // one-line status (current script/page, caps) instead; the accent and + // placeholder popups still render on top exactly as before (see + // KeyboardWidget::render()). Manual toggle rather than auto-detected, so it + // stays put even if the module is briefly unplugged. Default 0 (full grid, + // unchanged behaviour) on upgrade. + uint8_t keyboard_cardkb_compact; + // ── Clock & alarm ───────────────────────────────────────────────────── + uint8_t clock_hide_seconds; // 0=show HH:MM:SS/refresh 1s (default), 1=hide/refresh 60s + uint8_t clock_12h; // 0=24h (default), 1=12h with AM/PM + // Alarm clock — a wake alarm, configured from the Clock page (Enter › Alarm). + // Stored as a local time-of-day; the actual fire instant is (re)computed as an + // absolute time in tickBackground(), which is what makes it robust to RTC + // re-syncs: the mesh (every inbound packet), the companion app, GPS and the + // CLI can all jump getCurrentTime() at any moment, but an absolute target + // instant stays correct across small corrections and still fires (late) if + // the clock jumps over it. With alarm_repeat_mask == 0 it's one-shot: fires + // once, then alarm_on clears. A non-zero mask instead re-arms it for the next + // matching weekday (see UITask::computeAlarmNextFire/evaluateAlarm) and + // alarm_on stays set. The minutnik (countdown) and stoper (stopwatch) are + // runtime-only and not persisted. + uint8_t alarm_on; // 0=off (default), 1=armed + uint8_t alarm_hour; // 0-23, local time + uint8_t alarm_min; // 0-59 + // Repeat-days bitmask — see the alarm doc comment above for the full + // explanation (bit i = 1< Keyboard's Main/Additional rows. - // Equal to keyboard_alt_alphabet means no second page (see KeyboardWidget's - // hasAltAlphabet()). Appended at the tail (see the serialization tripwire - // below); default 0 (Latin) matches the keyboard's original always-Latin- - // main behaviour for upgraders. - uint8_t keyboard_main_alphabet; - // Per-target toggle for bot *action* commands (!buzz/!gps/!advert) -- // separate from bot_commands_ch/bot_commands_room above, which only gate // the read-only query commands (!ping/!batt/...). Nested under that @@ -425,10 +374,112 @@ struct NodePrefs { // persisted to file // that target); Actions=on additionally lets the state-changing commands // through. Default 0 (off) -- these change device behaviour remotely, so // upgraders don't get them silently enabled. - uint8_t bot_actions_dm; - uint8_t bot_actions_ch; uint8_t bot_actions_room; + uint8_t bot_quiet_start; // quiet-hours start hour, local 0-23 (start==end → disabled) + uint8_t bot_quiet_end; // quiet-hours end hour, local 0-23 + // ── GPS, trail & location sharing ───────────────────────────────────── + uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled) + uint32_t gps_interval; // GPS duty-cycle sleep window in seconds (0 = disabled, GPS stays continuous) + // Global measurement system for every distance/speed shown in the UI + // (Nearby, Trail, navigate-to-point). 0=metric (default), 1=imperial. + uint8_t units_imperial; + // GPS trail cadence. Logging on/off is a runtime state (Tools › Trail), + // not a persisted preference. + uint8_t trail_interval_idx; // reserved — sampling cadence is now fixed at TrailStore::SAMPLING_SECS + uint8_t trail_min_delta_idx; // min-distance gate level (0=finest..3); metres or feet per units_imperial + uint8_t trail_units_idx; // legacy: old combined speed/pace+unit index (km/h, mph, min/km, min/mi) + // Trail Summary readout: 0=speed (km/h or mph), 1=pace (min/km or min/mi). + // The km-vs-mi choice now comes from units_imperial, so this is just the mode. + uint8_t trail_show_pace; + // Trail auto-pause — when tracking, automatically freeze the trail (timer + + // sampling) after the device has sat still for this long, and resume on the + // next real movement. 0 = off. Index into trailAutoPauseSecs. + uint8_t trail_autopause_idx; + // Auto-save the live GPS trail to /trail on shutdown (covers the low-battery + // auto-shutdown, which otherwise loses the whole route). Only writes when the + // trail has points and the toggle is on. 0 = off (default), 1 = on. + // [Tools › Trail › Settings › Auto-save] + uint8_t trail_autosave_lowbatt; + // GPS averaging for waypoint marking — when set, "Mark here" samples the GPS + // fix for this many seconds and stores the mean position, for a more accurate + // mark than a single instantaneous fix. 0 = off (instant mark, the default). + // Index into gpsAvgSecs(). [Tools › Trail › Settings › Mark avg] + uint8_t gps_avg_idx; + // Track positions shared by other nodes via [LOC] messages (LiveTrackStore). + // 0 = ignore shared positions (default), 1 = parse incoming DM/channel [LOC] + // shares and update the live-track table (Nearby "Live" view / map). + uint8_t track_shared_loc; + // Live location sharing — a message-based "beacon". When enabled, the device + // periodically sends a [LOC] message to the chosen target while it moves. + // Configured from the Map (Trail screen) "Live share" menu. + uint8_t loc_share_enabled; // 0=off (default), 1=auto-sharing on + uint8_t loc_share_target_type; // 0=channel, 1=DM contact + uint8_t loc_share_channel_idx; // target channel index (when target_type==0) [del→onChannelRemoved] + uint8_t loc_share_dm_prefix[6]; // target contact pubkey prefix (when target_type==1) [del→onContactRemoved] + uint8_t loc_share_move_idx; // movement gate level (index into locShareMoveMeters) + uint8_t loc_share_interval_idx; // min send interval (index into locShareIntervalSecs) + uint8_t loc_share_heartbeat_idx; // stationary heartbeat (index into locShareHeartbeatSecs) + // Locator — a single geofence around a saved point. When enabled the device + // watches its own GPS fix and beeps + shows an alert when it crosses into + // (arrive) or out of (leave) the radius. The target coordinate/label is a + // snapshot of a chosen waypoint, so the alert survives the waypoint being + // edited or deleted. Configured from Tools › Locator. + uint8_t locator_enabled; // 0=off (default), 1=armed + uint8_t locator_has_target; // 0=no target chosen yet, 1=target set + uint8_t locator_radius_idx; // index into locatorRadiusMeters + uint8_t locator_mode; // 0=arrive, 1=leave, 2=both + int32_t locator_lat_1e6; // target latitude (1e6-scaled; last-known for a contact) + int32_t locator_lon_1e6; // target longitude (1e6-scaled; last-known for a contact) + char locator_label[12]; // target name for the alert text (WAYPOINT_LABEL_LEN) + // Target can be a static waypoint or a live contact: for a contact the engine + // re-reads the latest [LOC] position each evaluation (keyed by pubkey prefix), + // so the geofence follows a moving person ("alert when my friend is near"). + uint8_t locator_target_kind; // 0=waypoint (static), 1=live contact + uint8_t locator_key[6]; // contact pubkey prefix when target_kind==1 [del→onContactRemoved] + // Locator proximity beeper — when on (and the alert is armed with a target), + // the device ticks while inside the radius and shortens the gap between ticks + // the closer it gets to the target, like a homing beeper. Independent of the + // discrete arrive/leave alert (locator_mode). + uint8_t locator_beeper; // 0=off (default), 1=on + + // ── Contacts, channels & favourites ─────────────────────────────────── + uint8_t dm_show_all; // 0=favourites only, 1=all chat contacts (default) + uint8_t room_fav_only; // 0=all room servers (default), 1=favourites only + uint64_t ch_fav_bitmask; // bit i = channel i is marked as favourite [del→onChannelRemoved] + uint8_t ch_fav_only; // 0=show all channels (default), 1=show favourites only + // Favourites dial: 6 pinned targets. Layout transposes between landscape + // (3×2) and portrait (2×3). What a slot holds depends on favourite_kinds[] + // below: + // FAV_KIND_CONTACT — first 6 bytes of pub_key; covers chat contacts and + // room servers alike. All-zero = empty slot (a real pub_key starts with + // 6 zero bytes with probability 2^-48). + // FAV_KIND_CHANNEL — channel index in byte 0, rest zero. Never empty: + // channel 0 is all-zero, so emptiness is decided by the kind first. + static const uint8_t FAVOURITES_COUNT = 6; + static const uint8_t FAVOURITE_PREFIX_LEN = 6; + static const uint8_t FAV_KIND_CONTACT = 0; + static const uint8_t FAV_KIND_CHANNEL = 1; + static const uint8_t FAV_KIND_MAX = 1; + uint8_t favourite_contacts[FAVOURITES_COUNT][FAVOURITE_PREFIX_LEN]; // [del→onContactRemoved/onChannelRemoved] + // What each favourite_contacts[] slot holds (see FAV_KIND_* above). + // On-disk position is at the struct's append-only tail (see the + // serialization tripwire below), even though declared here beside + // favourite_contacts for readability. Zero-init = every slot is a contact, + // which is what pre-0x28 saves are. + uint8_t favourite_kinds[FAVOURITES_COUNT]; // [del→onContactRemoved/onChannelRemoved] -- clearFavouriteSlot() clears both fields together, called from either handler depending on the slot's kind + // Settings > Contacts > "Favs top". Stored inverted so that both a fresh + // memset and an older prefs file (no bytes here at all) mean "on", which is + // the default -- a positive flag would read back as off for every upgrader. + uint8_t fav_sort_off; // 0 = favourites first in every list (default), 1 = natural order + + // ── Advert ───────────────────────────────────────────────────────────── + uint8_t advert_loc_policy; + uint32_t advert_auto_interval_sec; // periodic 0-hop advert with GPS: 0=off, else seconds + // Advert sound filter: 0=all adverts, 1=zero-hop only + uint8_t advert_sound_scope; + + // ── GPIO ─────────────────────────────────────────────────────────────── // User-assignable GPIO pins (board-specific, see PIN_GPIO1..4 in the // variant header -- currently Wio Tracker L1 only). One byte per pin packs // direction + output level (+ analog, gpio1/2 only) into one field: @@ -441,52 +492,8 @@ struct NodePrefs { // persisted to file uint8_t gpio3_mode; uint8_t gpio4_mode; - // Settings > Keyboard's "Ext. KB" row (boards with a second I2C bus for an - // optional CardKB, see ENV_PIN_SDA/ENV_PIN_SCL, only). When on, the - // on-screen keyboard skips drawing its full letter grid + special-row icons - // -- an external-keyboard typist never looks at them -- and shows a compact - // one-line status (current script/page, caps) instead; the accent and - // placeholder popups still render on top exactly as before (see - // KeyboardWidget::render()). Manual toggle rather than auto-detected, so it - // stays put even if the module is briefly unplugged. Default 0 (full grid, - // unchanged behaviour) on upgrade. - uint8_t keyboard_cardkb_compact; - - // RSSI-based interference detection (relative to the radio's own noise - // floor) and hardware Channel Activity Detection before TX. See - // MyMesh::getInterferenceThreshold()/getCADEnabled() — CAD is also - // auto-enabled whenever rx_powersave is active, regardless of this flag, - // since duty-cycle sleep leaves the noise floor stale. Both default 0/off, - // and neither has a Settings/CLI toggle yet on companion_radio — persisted - // and wired for a future manual override. - uint8_t interference_threshold; - uint8_t cad_enabled; - - // Repeater scope filter — logically part of the repeat_* group far above, but - // the on-disk format is append-only (see the tripwire note at the bottom of - // this file), so it has to live here at the tail. - // repeat_scope_only: 1 = only forward flood packets matching this device's - // own scope (Settings > Radio > Scope, default_scope_key) or one of the - // repeat_extra_scopes below — drops unscoped floods and floods tagged for - // a different community. A no-op (forwards everything, unchanged) while - // no scope is configured at all, so enabling this on an unconfigured - // device can't silently blackhole all flood traffic. - uint8_t repeat_scope_only; - // Extra region names this repeater also relays for, beyond its own - // Settings > Radio > Scope (comma-separated, e.g. "eu,de") — see - // MyMesh::rebuildRepeatScopes(). Relay-only: never affects what scope the - // companion's own messages send under, only what repeat_scope_only accepts. - char repeat_extra_scopes[24]; - - // What each favourite_contacts[] slot holds (see FAV_KIND_* by that field). - // Appended here, not next to it, because the on-disk format is append-only. - // Zero-init = every slot is a contact, which is what pre-0x28 saves are. - uint8_t favourite_kinds[FAVOURITES_COUNT]; // [del→onChannelRemoved] - - // Settings > Contacts > "Favs top". Stored inverted so that both a fresh - // memset and an older prefs file (no bytes here at all) mean "on", which is - // the default -- a positive flag would read back as off for every upgrader. - uint8_t fav_sort_off; // 0 = favourites first in every list (default), 1 = natural order + // ── Custom messages ──────────────────────────────────────────────────── + char custom_msgs[10][140]; // user-defined quick messages (supports {loc}, {time}) // Single source of truth for the live-share option tables (shared by the Map // UI labels and the auto-send engine in UITask). @@ -685,6 +692,20 @@ struct NodePrefs { // persisted to file // the next alignment boundary. Confirmed via a real Heltec_v3 build. // fav_sort_off (0xC0DE0029) landed in the 2 bytes of padding the 0xC0DE0028 // bump left over -- confirmed via a real Heltec_v3 build, sizeof unchanged. +// +// 2026-09: fields reordered into thematic groups for source readability (see +// the comment at the top of the struct) -- NOT a schema change. No field was +// added, removed, resized, or re-sequenced in DataStore's rd()/wr() calls, so +// the on-disk format and SCHEMA_SENTINEL are untouched. The reorder happened +// to let the compiler pack same-size fields together with less padding, +// dropping sizeof from 2760 to 2752 -- confirmed via a real +// Heltec_v3_companion_radio_ble build. Purely a compile-time in-memory +// layout change; this static_assert exists precisely to catch that kind of +// accidental drift, so the trip was expected here and this comment is that +// re-check. dashboard_fields[3] was then moved from the favourites group to +// Display (a better thematic fit), which shifted padding again and put +// sizeof back at 2760 -- also confirmed via a real +// Heltec_v3_companion_radio_ble build. Still no schema change. static_assert(sizeof(NodePrefs) == 2760, "NodePrefs layout changed — sync DataStore save/load + clamp, bump " "SCHEMA_SENTINEL, then update this size (see steps above)."); diff --git a/test/test_companion_node_prefs/test_companion_node_prefs.cpp b/test/test_companion_node_prefs/test_companion_node_prefs.cpp index e252272f..102e4fd0 100644 --- a/test/test_companion_node_prefs/test_companion_node_prefs.cpp +++ b/test/test_companion_node_prefs/test_companion_node_prefs.cpp @@ -1,83 +1,165 @@ #include -#include #include -#include - -#include #include "../../examples/companion_radio/NodePrefs.h" -class ReplayStream : public Stream { - const char* _text; - int _pos = 0; - int _len; +// These target the pure, hardware-independent helpers declared alongside +// NodePrefs (free functions and NodePrefs:: static lookup tables). They build +// and run on the `native` host env with no board/filesystem mocking, unlike +// DataStore::savePrefs()/loadPrefsInt() (the actual on-disk read/write path), +// which pulls in FILESYSTEM/File (Adafruit_LittleFS or fs::FS, depending on +// platform) plus IdentityStore/ContactInfo/ChannelDetails/target.h — real +// coverage of that path needs a filesystem mock this test file doesn't have. -public: - explicit ReplayStream(const char* text) : _text(text), _len(strlen(text)) { } +TEST(DefaultRepeaterFreqForBand, PicksTheBandBelowTheCompanionFrequency) { + EXPECT_FLOAT_EQ(433.000f, defaultRepeaterFreqForBand(490.0f)); // < 500 + EXPECT_FLOAT_EQ(869.495f, defaultRepeaterFreqForBand(500.0f)); // 500 boundary + EXPECT_FLOAT_EQ(869.495f, defaultRepeaterFreqForBand(868.0f)); // 500..890 + EXPECT_FLOAT_EQ(918.000f, defaultRepeaterFreqForBand(890.0f)); // 890 boundary + EXPECT_FLOAT_EQ(918.000f, defaultRepeaterFreqForBand(915.0f)); // >= 890 +} - int available() override { return _len - _pos; } - int read() override { return _pos < _len ? _text[_pos++] : -1; } - int peek() override { return _pos < _len ? _text[_pos] : -1; } -}; +TEST(IsValidRepeaterProfile, AcceptsAProfileWithinAllBounds) { + EXPECT_TRUE(isValidRepeaterProfile(868.0f, 250.0f, 10, 5, 850.0f, 930.0f)); +} -class CaptureStream : public Stream { - std::string _text; +TEST(IsValidRepeaterProfile, RejectsFrequencyOutsideTheChipRange) { + EXPECT_FALSE(isValidRepeaterProfile(800.0f, 250.0f, 10, 5, 850.0f, 930.0f)); + EXPECT_FALSE(isValidRepeaterProfile(950.0f, 250.0f, 10, 5, 850.0f, 930.0f)); +} - size_t emit(long long value) { - char text[24]; - int length = snprintf(text, sizeof(text), "%lld", value); - return write(reinterpret_cast(text), length); +TEST(IsValidRepeaterProfile, RejectsSpreadingFactorOutsideFiveToTwelve) { + EXPECT_FALSE(isValidRepeaterProfile(868.0f, 250.0f, 4, 5, 850.0f, 930.0f)); + EXPECT_FALSE(isValidRepeaterProfile(868.0f, 250.0f, 13, 5, 850.0f, 930.0f)); +} + +TEST(IsValidRepeaterProfile, RejectsCodingRateOutsideFiveToEight) { + EXPECT_FALSE(isValidRepeaterProfile(868.0f, 250.0f, 10, 4, 850.0f, 930.0f)); + EXPECT_FALSE(isValidRepeaterProfile(868.0f, 250.0f, 10, 9, 850.0f, 930.0f)); +} + +TEST(IsValidRepeaterProfile, RejectsBandwidthOutsideSevenTo510) { + EXPECT_FALSE(isValidRepeaterProfile(868.0f, 6.9f, 10, 5, 850.0f, 930.0f)); + EXPECT_FALSE(isValidRepeaterProfile(868.0f, 510.1f, 10, 5, 850.0f, 930.0f)); +} + +TEST(SeedDefaultRepeaterProfile, SeedsFromTheCompanionFrequencyBand) { + NodePrefs prefs{}; + prefs.freq = 915.0f; + seedDefaultRepeaterProfile(prefs); + EXPECT_EQ(1, prefs.repeater_use_profile); + EXPECT_FLOAT_EQ(918.000f, prefs.repeater_freq); + EXPECT_FLOAT_EQ((float)LORA_BW, prefs.repeater_bw); + EXPECT_EQ((uint8_t)LORA_SF, prefs.repeater_sf); + EXPECT_EQ((uint8_t)LORA_CR, prefs.repeater_cr); +} + +TEST(AlarmRepeatRoundTrip, EveryPresetIndexRoundTripsThroughItsMask) { + for (uint8_t idx = 0; idx < NodePrefs::ALARM_REPEAT_COUNT; idx++) { + uint8_t mask = NodePrefs::alarmRepeatMaskForIdx(idx); + EXPECT_EQ(idx, NodePrefs::alarmRepeatIdxForMask(mask)) << "idx=" << (int)idx; } +} + +TEST(AlarmRepeatRoundTrip, OutOfRangeIndexClampsToNone) { + // Cast to a prvalue: EXPECT_EQ binds its arguments by const T&, which would + // otherwise ODR-use this in-class-initialized static const and need an + // out-of-line definition that doesn't exist. + EXPECT_EQ((uint8_t)NodePrefs::ALARM_REPEAT_NONE, NodePrefs::alarmRepeatMaskForIdx(99)); +} + +TEST(AlarmRepeatRoundTrip, AnArbitraryNonPresetMaskReadsAsOff) { + // Documents the reverse-lookup's documented behaviour: a mask that doesn't + // match a preset (e.g. a future custom-day picker's value) reads as index 0 + // ("OFF") even though it isn't actually the all-zero NONE mask. + EXPECT_EQ(0, NodePrefs::alarmRepeatIdxForMask(0x15)); +} + +TEST(AlarmRepeatLabel, MatchesEachPresetIndex) { + EXPECT_STREQ("OFF", NodePrefs::alarmRepeatLabel(0)); + EXPECT_STREQ("Daily", NodePrefs::alarmRepeatLabel(1)); + EXPECT_STREQ("Weekdays", NodePrefs::alarmRepeatLabel(2)); + EXPECT_STREQ("Weekends", NodePrefs::alarmRepeatLabel(3)); +} + +TEST(AlarmRepeatLabel, OutOfRangeIndexClampsToOff) { + EXPECT_STREQ("OFF", NodePrefs::alarmRepeatLabel(99)); +} + +TEST(KeyboardAlphabetLabel, MatchesEachAlphabetIndex) { + EXPECT_STREQ("Latin", NodePrefs::keyboardAlphabetLabel(NodePrefs::KB_ALPHABET_LATIN_ONLY)); + EXPECT_STREQ("Cyrillic", NodePrefs::keyboardAlphabetLabel(NodePrefs::KB_ALPHABET_CYRILLIC)); + EXPECT_STREQ("Greek", NodePrefs::keyboardAlphabetLabel(NodePrefs::KB_ALPHABET_GREEK)); +} -public: - size_t write(uint8_t value) override { - _text.push_back(static_cast(value)); - return 1; - } +TEST(KeyboardAlphabetLabel, OutOfRangeIndexClampsToLatin) { + EXPECT_STREQ("Latin", NodePrefs::keyboardAlphabetLabel(99)); +} + +TEST(HomePageLabel, MatchesEveryBitIndexInOrder) { + EXPECT_STREQ("Clock", NodePrefs::homePageLabel(NodePrefs::HPB_CLOCK)); + EXPECT_STREQ("Tools", NodePrefs::homePageLabel(NodePrefs::HPB_TOOLS)); + EXPECT_STREQ("Messages", NodePrefs::homePageLabel(NodePrefs::HPB_QUICK_MSG)); + EXPECT_STREQ("Favourites", NodePrefs::homePageLabel(NodePrefs::HPB_FAVOURITES)); + EXPECT_STREQ("Map", NodePrefs::homePageLabel(NodePrefs::HPB_MAP)); +} + +TEST(HomePageLabel, OutOfRangeBitReturnsEmptyString) { + EXPECT_STREQ("", NodePrefs::homePageLabel(NodePrefs::HPB_COUNT)); +} + +TEST(OptionTables, InRangeIndicesReturnTheDocumentedValues) { + EXPECT_EQ(250, NodePrefs::locShareMoveMeters(2)); + EXPECT_EQ(120, NodePrefs::locShareIntervalSecs(2)); + EXPECT_EQ(900, NodePrefs::locShareHeartbeatSecs(2)); + EXPECT_EQ(500, NodePrefs::locatorRadiusMeters(3)); + EXPECT_STREQ("Both", NodePrefs::locatorModeLabel(2)); + EXPECT_EQ(10, NodePrefs::gpsAvgSecs(2)); + EXPECT_STREQ("10s", NodePrefs::gpsAvgLabel(2)); + EXPECT_EQ(120, NodePrefs::trailAutoPauseSecs(2)); + EXPECT_STREQ("2m", NodePrefs::trailAutoPauseLabel(2)); +} + +// Each option table clamps an out-of-range index independently, and they +// don't all agree on which in-range index to fall back to (1 for the +// loc-share/locator tables, 0 for the GPS-averaging/trail-autopause ones) — +// this pins down that existing, slightly inconsistent behaviour so a future +// change to any one table doesn't silently change another's fallback. +TEST(OptionTables, OutOfRangeIndexFallsBackPerTable) { + EXPECT_EQ(NodePrefs::locShareMoveMeters(1), NodePrefs::locShareMoveMeters(99)); + EXPECT_EQ(NodePrefs::locShareIntervalSecs(1), NodePrefs::locShareIntervalSecs(99)); + EXPECT_EQ(NodePrefs::locShareHeartbeatSecs(0), NodePrefs::locShareHeartbeatSecs(99)); + EXPECT_EQ(NodePrefs::locatorRadiusMeters(1), NodePrefs::locatorRadiusMeters(99)); + EXPECT_STREQ("Arrive", NodePrefs::locatorModeLabel(99)); + EXPECT_EQ(NodePrefs::gpsAvgSecs(0), NodePrefs::gpsAvgSecs(99)); + EXPECT_EQ(NodePrefs::trailAutoPauseSecs(0), NodePrefs::trailAutoPauseSecs(99)); +} + +TEST(BuildRTTTLString, EmptyWhenLengthIsZero) { + char buf[64] = "unchanged"; + NodePrefs::buildRTTTLString(nullptr, 0, 0, buf, sizeof(buf)); + EXPECT_STREQ("", buf); +} + +TEST(BuildRTTTLString, EncodesANoteAndARestWithTheChosenTempo) { + // pitch=2('d'), octave offset=1 (-> octave 5), dur_idx=1 (-> 8th note) + const uint8_t note = 2 | (1 << 3) | (1 << 5); + // pitch=0 (rest), dur_idx=0 (-> 4th note) + const uint8_t rest = 0; + const uint8_t notes[2] = { note, rest }; - size_t write(const uint8_t* buffer, size_t size) override { - _text.append(reinterpret_cast(buffer), size); - return size; - } + char buf[64]; + NodePrefs::buildRTTTLString(notes, 2, /*bpm_idx=*/1, buf, sizeof(buf)); + EXPECT_STREQ("Ring:d=8,o=5,b=90:8d5,4p", buf); +} - size_t print(unsigned char value, int = DEC) override { return emit(value); } - size_t print(int value, int = DEC) override { return emit(value); } - size_t print(unsigned int value, int = DEC) override { return emit(value); } - size_t print(long value, int = DEC) override { return emit(value); } - size_t print(unsigned long value, int = DEC) override { return emit(value); } - size_t print(long long value, int = DEC) override { return emit(value); } - size_t print(unsigned long long value, int = DEC) override { return emit(value); } - - const std::string& text() const { return _text; } -}; - -#if 0 -// Re-enable test once we can SET fem_ values in companion -TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) { - NodePrefs saved; - saved.rx_boosted_gain = 0; - saved.radio_fem_rxgain = 1; - saved.radio_fem_txgain = 0; - - CaptureStream output; - ASSERT_TRUE(saved.saveSerial(output)); - EXPECT_NE(std::string::npos, output.text().find("rxgain:0")); - EXPECT_NE(std::string::npos, output.text().find("fem_rxgain:1")); - EXPECT_NE(std::string::npos, output.text().find("fem_txgain:0")); - - ReplayStream input("{radio:{rxgain:1,fem_rxgain:0,fem_txgain:1}}"); - NodePrefs loaded; - loaded.rx_boosted_gain = 0; - loaded.radio_fem_rxgain = 1; - loaded.radio_fem_txgain = 0; - - ASSERT_TRUE(loaded.loadSerial(input)); - EXPECT_EQ(1, loaded.rx_boosted_gain); - EXPECT_EQ(0, loaded.radio_fem_rxgain); - EXPECT_EQ(1, loaded.radio_fem_txgain); +TEST(BuildRTTTLString, OutOfRangeBpmIndexClampsToTheMiddlePreset) { + const uint8_t note = 0; // a single rest is enough to isolate the tempo + char buf[64]; + NodePrefs::buildRTTTLString(¬e, 1, /*bpm_idx=*/9, buf, sizeof(buf)); + EXPECT_STREQ("Ring:d=8,o=5,b=120:4p", buf); } -#endif int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv);