Skip to content

07 ‐ Architecture

gfo974 edited this page Jul 6, 2026 · 8 revisions

Layered Architecture

┌─────────────────────────────────┐
│         Application             │  main.cpp, state machine
├─────────────────────────────────┤
│      Services Framework         │  ServiceManager, 15+ services
├─────────────────────────────────┤
│     DTE Protocol Layer          │  DTEHandler, encoder/decoder
├─────────────────────────────────┤
│    Configuration Store          │  214 params, flash persistence
├─────────────────────────────────┤
│   Hardware Abstraction Layer    │  Sensor, Timer, PMU, GPIO
├─────────────────────────────────┤
│    Board Support Package        │  linkitv4_v1.0, rspbtracker_v1.0
├─────────────────────────────────┤
│     nRF52840 + SoftDevice       │  Nordic SDK 17.0.2
└─────────────────────────────────┘

Directory Organization

  • core/ — Portable code, no hardware dependencies
    • core/services/ — Service implementations
    • core/protocol/ — DTE protocol, encoder, decoder, params
    • core/configuration/ — ConfigStore, calibration
    • core/hardware/ — Abstract interfaces (Sensor, Timer)
    • core/logging/ — Logger, messages
  • ports/nrf52840/ — Hardware-specific implementations
    • ports/nrf52840/bsp/ — Board support packages
    • ports/nrf52840/core/hardware/ — Sensor drivers (BMA400, LPS28DFW, M8, etc.)

Key Design Patterns

  • Dependency Inversion: core/ defines abstract interfaces, ports/ provides implementations
  • RAII: SensorsPowerGuard for VSENSORS power management
  • Static Registry: SensorManager, LoggerManager, CalibratableManager, ServiceManager
  • Variant-based Configuration: BaseType = std::variant<...> for type-safe parameter storage
  • Event-driven Services: ServiceEvent for inter-service communication
  • Bounded recovery > clever optimization (sealed-device mindset): boot-fail counter with factory-reset cascade, exception barriers around every destructive step, virtual RTC fallback, cooldown gates kill latent tasks before reaching the hardware. See Sealed-device hardening below for the full defense layer summary.

Sealed-device hardening

LinkIt v4 deployments target sealed wildlife trackers running months to years without operator access. Every robustness defense is bounded — fail loudly into a known-safe state rather than try to keep going. This section catalogues the defenses; the per-subsystem details live next to their primary owner (RTC in 10 — GPS Guide § E; cooldown in 13 — Underwater § D; rate limiter in 11 — Satellite Communication § E.4; SMD autofallback in 11 § Part D).

Boot-fail counter + factory-reset cascade

Added in: commit ee81573a. Located at core/sm/gentracker.cpp.

A struct s_bootfail_noinit lives in .noinit RAM (survives reset, lost on power-off):

struct BootFailNoinit {
    uint16_t consecutive_failures;   // bumped at each BootState::entry
    uint8_t  factory_reset_attempted; // 1 = already tried
    uint8_t  _pad;
    uint16_t crc;                     // CRC16 over the above
};

Bumped at every BootState::entry(). Cleared on successful entry to OperationalState.

consecutive_failures Action
1–2 Normal boot, ErrorState → soft reset retry
3 Trigger configuration_store->factory_reset(), set factory_reset_attempted=1, then soft reset. Factory reset preserves DECID/HEXID/calibration.
4 Counter resumed; if Operational reached → reset to 0 (added commit dbcca4fe)
5 Fall through to OffState (permanent off, magnet wake required)

In CPPUTEST builds, the cascade is disabled and the counter is reset at BootState::entry() under #ifdef CPPUTEST to prevent contamination between tests.

The cascade addresses the BAD_FILESYSTEM brick identified by the FSM review: previously, multiple firmware paths could put the device in OffState (which goes to System OFF with reed-magnet as the only wake source) without operator intervention, permanently bricking a sealed unit.

5 exception barriers

Added across gentracker.cpp and service.cpp (commit ee81573a, robustness pass 2). Each barrier prevents a single subsystem panic from cascading into a permanent freeze.

# Location Catches Effect
B2 main.cpp top-level std::exception, ... (added to existing ErrorCode) std::bad_alloc, bad_function_call, etc. now post a generic ErrorEvent. Without these, std::terminate → abort() would hang in fputc until 15-min WDT cycle.
B3 OperationalState::exit() every destructive step independently A teardown exception during a reed-magnet gesture would have escaped to main → ErrorState → OffState, bricking the device on a glitchy peripheral. Steps: save_params, LED dispatch, ServiceManager::stopall, BLE stop, battery_monitor unsubscribe.
B4 apply_file_update() (gentracker.cpp:642) OTA-apply exceptions CRC mismatch mid-stream or SMD DFU SPI error previously escaped → ErrorState → OffState. Now stays in Configuration, ready for OTA retry.
GenTracker periodic_config_flush() save_params() exceptions LFS unwritable or RAM transient doesn't terminate the periodic-flush task.
ServiceManager stopall() per-service try/catch + WDT kick between each A single service throwing during teardown doesn't leak past the stopall loop, and the WDT kick prevents the cumulative stopall budget from exhausting the 15 min WDT.

The boot-fail counter is the last-resort recovery if these barriers fail to contain the issue.

POF handler hardening

Power-fail-warning handler (nrf_pmu.cpp, commit 12fdd581) was previously calling configuration_store->save_params() after the cooldown noinit save and the RAM-only RTC update. That triggers an LFS journal write of ~100 ms while the device has only supercap energy left.

POFCON V27 fires at 2.7 V on Vbatt; if the brown-out was caused by deep discharge (supercap drained), the write is truncated and can corrupt the params block. LFS itself is journaling so the filesystem doesn't brick, but a half-written params record can be rejected on next boot and lose all 30 minutes of param updates that the periodic flush already covers cleanly.

Current policy in the POF handler:

  • ServiceManager::save_cooldown_state() — noinit RAM, atomic, ~µs (kept)
  • write_param(LAST_KNOWN_RTC, now) — RAM-only mutation, no flash (kept)
  • save_params()REMOVED

Durable persistence still happens via the 30-min periodic flush (CONFIG_FLUSH_INTERVAL_MS) and the clean-shutdown save_params() in PMU::powerdown(). Trade-off accepted: on POF we may lose up to 30 min of RTC drift / TX counter, but never corrupt all 227 params.

Power-off device while GPS is running (commit 927d0837): the factory-reset / shutdown paths now correctly power off the GPS rail before sending the SoC into System OFF. Previously, a power-off while GPS was actively acquiring left the GPS rail energized, draining the battery for hours.

PreOp reed-stuck escape

PREOP_STUCK_REED_MAX_MS=20 s: a manufacturing magnet residue or hardware short would leave m_confirmation_pending != NONE forever, with no RELEASE event arriving to start the 2-s confirmation timeout. The previous code returned without rescheduling, wedging the device in PreOp forever at full active current (~5–20 mA) — the only HW fault that doesn't self-degrade to near-zero current.

Now the transit task re-arms itself and, after PREOP_STUCK_REED_MAX_MS, forces transit to Operational regardless of the pending confirmation.

Watchdog and long-op kicks

Defense Failure handled Recovery time
start_watchdog (15 min) Genuine CPU hang Hard reset after 15 min
WDT kicks in long ops Long flash op exhausts WDT Refreshed budget
static_assert(sizeof(time_t) >= 8) at file scope (service.cpp) 32-bit time_t silently wrapping January 2038 Build-time guarantee

GenTracker::kick_watchdog() posts to the scheduler at high priority every ~13.5 min (90 % of WDT period). BootState mount/format and ServiceManager::stopall issue per-service WDT kicks for known-long operations.

Defense layers summary (catalog)

Defense Failure handled Owner / location Recovery time
CRC16 on noinit structs Single-bit flip in RAM All _noinit structs (cooldown, rate-limiter, hauled-mode, bootfail) Immediate (struct zeroed on mismatch)
Virtual RTC fallback Cold boot without GNSS 10 § E.3 (main.cpp boot path) Immediate (rtc=1)
M1a periodic RTC save WDT reset losing virtual time 10 § E.4 (every 30 min) At most 30 min lost
LAST_KNOWN_RTC restore validation Flash corruption (POF mid-write) 10 § E.2 (range check) Fallback to virtual RTC
K + L RTC-sync hooks Virtual → real epoch jump 10 § E.7 (m10qasync.cpp::on_fix) Immediate
ANO staleness math guard Virtual-RTC stamp vs real-RTC last save 10 § E.8 Immediate
POF handler doesn't save_params() Brown-out mid-flush corrupting LFS This page (nrf_pmu.cpp, commit 12fdd581) Prevents corruption
Power-off cuts GPS rail Power-off while GPS still running This page (nrf_pmu.cpp, commit 927d0837) Prevents stuck-rail drain
Boot-fail counter Any boot path freezes This page (gentracker.cpp::BootState) 1 boot/reset, factory_reset @ 3, OffState @ 5
5 exception barriers Single subsystem throws This page Caught, logged, FSM continues
PreOp stuck-reed escape Reed-switch stuck closed (manuf. magnet residue) This page (gentracker.cpp::PreOperationalState) After 20 s, force transit to Operational
Cooldown gate Latent tasks posted before cycle complete 13 § D.5 Active until cooldown expires
Rate limiter Burst TX exceeding battery budget 11 § E.4 (rate_limiter.cpp) Configurable rolling window
Spacing-guard Immediate-TX sites firing back-to-back 11 § E.5 (uptime-based, RTC-immune) Bounded by surfacing_burst_init_s
SMD FAST/SAFE autofallback SMD timing too tight for batch / environment 11 § Part D 3 errors → SAFE; trust window 1 h → 24 h
ArgosTxService::service_term hardening (FIX F) Radio rail left indeterminate on FSM teardown argos_tx_service.cpp::service_term Unconditional power_off_immediate() + state reset
ArgosTxService consecutive-device-error reset 3 early errors suspending TX for multi-year deploy argos_tx_service.cpp:537+ Reset on every UW→surface transition
WDT 15-min Genuine CPU hang nrf_pmu.cpp::start_watchdog Hard reset after 15 min
WDT kicks in long ops Long flash op exhausts WDT BootState mount/format, stopall per-service Refreshed budget
static_assert(sizeof(time_t) >= 8) 32-bit time_t silently wrapping 2038 service.cpp file scope Build-time guarantee

Field-debugging cheatsheet

Counters exposed via diagnostics (STATR or logs):

Counter Meaning Threshold for concern
bootfail_noinit.consecutive_failures Boots since last successful Operational > 1 = something blocking init
m_diag.dive_timeout_count (SWS) UW_MAX_DIVE_TIME exceeded > 1/day = sensor drift or misconfig
m_diag.force_surface_count (SWS) Cascade level 3 fired > 0 = sustained sensor issue
m_diag.stuck_recovery_count (SWS) Air baseline collapse recovery > 0 = dry-electrode reading anomaly
m_diag.invalid_adc_count (SWS) SAADC returned invalid value > 60 in a row = ADC hardware issue
RateLimiter::s_noinit.count Current TX in rolling window Compare with RLP03
RateLimiter::passive_count TX blocked by rate limit Per-deployment config
HauledModeService::m_dive_count UW events since last HAULED engagement High = animal active

Known gaps (deferred)

# Gap Where Why deferred
F1 m_safe_trust_window_hours not persisted SMD autofallback Reboots rare on sealed turtles
F2 m_safe_mode_since_ms reset on reboot SMD autofallback Same
F5 No WDT kick in write_credentials_from_config SMD credentials write ~720 ms blocking, fits in 15-min budget
No adaptive water DOWN recalib SWS detection See .claude/sws_adaptive_water_down.md
Stuck SMD MAC_TX_TIMEOUT indistinguishable from "no satellite" SMD autofallback Relies on Argos-side observability to detect dead unit

Service Framework

The Service Framework is the core scheduling and lifecycle engine. All firmware functionality (GNSS, Argos TX, sensors, underwater detection) is implemented as services managed by ServiceManager.

Service Base Class

Location: core/services/service.hpp

Lifecycle:

  1. Construction — Service registers with ServiceManager
  2. start() — Calls service_init(), enables scheduling
  3. Scheduling loop — ServiceManager calls service_initiate() based on service_next_schedule_in_ms()
  4. stop() — Calls service_term(), disables scheduling

Virtual methods to implement:

Method Required Description
service_init() No Initialize resources
service_term() No Release resources
service_is_enabled() Yes Is service enabled in config?
service_next_schedule_in_ms() Yes Interval until next execution
service_initiate() Yes Execute the service task
service_cancel() No Cancel pending operation
service_is_usable_underwater() No Can run underwater? (default: false)
service_is_triggered_on_surfaced() No Trigger when surface detected?
service_is_triggered_on_event() No Trigger on inter-service event?

Utility methods:

  • service_complete() — Mark task complete, optionally reschedule
  • service_log() — Write entry to logger
  • service_read_param<T>(ParamID) — Read configuration parameter
  • service_write_param<T>(ParamID, value) — Write configuration parameter
  • service_current_time() — Get system time
  • service_is_battery_level_low() — Check low battery
  • service_reschedule() — Force reschedule

ServiceManager

Static class managing all services:

  • add(Service&) / remove(Service&)
  • startall() / stopall()
  • notify_underwater_state(bool) — Broadcast underwater state
  • notify_peer_event(ServiceEvent&) — Distribute events
  • inject_event(ServiceEvent&) — Inject custom events

Scheduling

Services return SCHEDULE_DISABLED (0xFFFFFFFF) to disable automatic scheduling. Otherwise return milliseconds until next execution. Services multiply seconds config values by 1000.

SensorService

Location: core/services/sensor_service.hpp

Extends Service for sensor-based services. Handles periodic sensor reading, sample aggregation, log entry creation, and TX data collection.

Method Required Description
sensor_populate_log_entry() Yes Fill log entry from sensor data
sensor_init() No Sensor-specific init (e.g., set full scale)
sensor_is_enabled() Yes Is sensor enabled?
sensor_periodic() Yes Sampling period in ms
sensor_tx_periodic() Yes TX sampling period in ms
sensor_max_samples() Yes Max samples per TX event
sensor_num_channels() Yes Number of channels (1-6)
sensor_enable_tx_mode() Yes TX aggregation mode
sensor_is_usable_underwater() No Usable underwater?

Aggregation modes:

Mode Behavior
OFF Sensor data not included in TX
ONESHOT First sample only
MEAN Average of all samples
MEDIAN Median of all samples

Services Reference

Core Services

Service File Purpose Key Params
ARGOSTxService core/services/argos_tx_service.hpp Argos satellite TX with pass prediction. Modes: OFF, PASS_PREDICTION, LEGACY, DUTY_CYCLE, DOPPLER, SURFACING_BURST ARGOS_MODE, TR_NOM, NTRY_PER_MESSAGE, DUTY_CYCLE, ARGOS_DEPTH_PILE
ARGOSRxService core/services/argos_rx_service.hpp Receives AOP updates from Argos downlink ARGOS_RX_EN, ARGOS_RX_MAX_WINDOW, ARGOS_RX_AOP_UPDATE_PERIOD
GPSService core/services/gps_service.hpp GNSS acquisition with filtering and dynamic models. Cold start retry, AssistNow, HDOP/HACC filtering GNSS_EN, GNSS_ACQ_TIMEOUT, GNSS_FIX_MODE, GNSS_DYN_MODEL
LoRaTxService core/services/lora_tx_service.hpp LoRa RAK3172 TX service. Compact binary packets over LoRaWAN (OTAA/ABP) LRP01-LRP15
MortalityService core/services/mortality_service.hpp RSPB bird mortality detection from activity, temperature, GPS stationarity MTP01-MTP07

Sensor Services

Service File Channels Key Params
PressureSensorService core/services/pressure_sensor_service.hpp 2 (pressure bar, temperature C) + computed altitude PRP01-PRP07
AXLSensorService core/services/axl_sensor_service.hpp 6 (X, Y, Z, activity, temperature, wakeup) AXP01-AXP09
ThermistorSensorService core/services/thermistor_sensor_service.hpp 1 (temperature C) THP01-THP08
ALSSensorService core/services/als_sensor_service.hpp 1 (lumens) LTP01-LTP06
PHSensorService core/services/ph_sensor_service.hpp 1 (pH) PHP01-PHP06
SeaTempSensorService core/services/sea_temp_sensor_service.hpp 1 (temperature C, RTD or TSYS01) STP01-STP06
CDTSensorService core/services/cdt_sensor_service.hpp 3 (conductivity, depth, temperature) CDP01-CDP05
CAMService core/services/cam_service.hpp Camera trigger on surfaced/AXL wakeup CAP01-CAP05

Underwater Detection Services

Service File Source Description
SWSService core/services/sws_service.hpp SWS (0) Digital saltwater switch
SWSAnalogService core/services/sws_analog_service.hpp SWS (0) Analog SWS with auto-calibration, 5-level detection, biofouling adaptation. Test mode via SWSTST DTE command
PressureDetectorService core/services/pressure_detector_service.hpp PRESSURE_SENSOR (1) Pressure threshold detection
GNSSDetectorService core/services/gnss_detector_service.hpp GNSS (2) / SWS_GNSS (3) GNSS signal quality detection

Utility Services

Service File Purpose
DiveModeService core/services/dive_mode_service.hpp Dive state machine with reed switch pause/resume
MemoryMonitorService core/services/memory_monitor_service.hpp Logs heap/stack statistics every 12 hours

Configuration System

The configuration system manages 240 ParamID slots (__PARAM_SIZE = 240 in base_types.hpp) stored in flash memory, of which ~210 are live parameters and the rest are reserved/deprecated slots kept for flash-layout backward compatibility. Parameters are accessed by ParamID enum and exposed to pylinkit / GUI via 5-character DTE keys (ARP01, GNP05, etc.). See 09 — Parameters for the complete table.

ConfigurationStore

Location: core/configuration/config_store.hpp

Abstract base class with pure virtual methods:

  • init() — Load config from flash
  • is_valid() — Check if config is loaded
  • factory_reset() — Reset all params to defaults
  • read_pass_predict() / write_pass_predict() — AOP satellite data
  • serialize_config() — Save to flash

Parameter access:

// Read parameter (type-safe)
bool gnss_en = configuration_store->read_param<bool>(ParamID::GNSS_EN);
unsigned int timeout = configuration_store->read_param<unsigned int>(ParamID::GNSS_ACQ_TIMEOUT);

// Write parameter
configuration_store->write_param(ParamID::GNSS_EN, true);
configuration_store->save_params();  // Persist to flash

BaseType Variant

Parameters are stored as std::variant:

using BaseType = std::variant<
    std::string, unsigned int, int, double, std::time_t, BaseRawData,
    BaseArgosMode, BaseArgosPower, BaseArgosDepthPile, bool,
    BaseGNSSFixMode, BaseGNSSDynModel, BaseLEDMode, BaseZoneType,
    BaseArgosModulation, BaseUnderwaterDetectSource, BaseDebugMode,
    BasePressureSensorLoggingMode, BasePressureSensorFullScale,
    BaseSensorEnableTxMode
>;

Config Version Code

static inline const unsigned int m_config_version_code = 0x1c07e800 | 0x20;

When the version code is bumped, existing devices perform a factory reset on next boot to load new defaults — keeping only ARGOS_DECID/ARGOS_HEXID, everything else returns to factory values. This is necessary when adding new parameters or changing default values. Consequence: BLE re-provisioning is required after flashing across a bump; never OTA-update deployed devices across one (see 09 — Parameters → Firmware upgrade note).

Configuration Modes

The firmware dynamically selects parameter sets based on state:

  • NORMAL — Default parameters
  • LOW_BATTERY — LB_* parameters (when LB_EN=true and battery below LB_THRESHOLD)
  • OUT_OF_ZONE — ZONE_* parameters (when outside geofencing zone)

ConfigStoreFS

Location: core/configuration/config_store_fs.hpp

Flash-backed implementation using LittleFS. Serializes/deserializes BaseType variants to/from binary via operator() overloads (serializer) and switch cases (deserializer). Stores AOP pass prediction data separately.

Calibration System

Location: core/configuration/calibration.hpp

class Calibratable {
    virtual void calibration_write(const double value, const unsigned int offset) {}
    virtual void calibration_read(double& value, const unsigned int offset) {}
    virtual void calibration_save(bool force) {}
};

Devices inherit from Calibratable and register with CalibratableManager. Used for sea level pressure reference, accelerometer axis offsets, and SWS analog baselines.

DTE interface: SCALW (write calibration) and SCALR (read calibration). The Calibration helper class persists values to LittleFS files, with a map<unsigned int, double> keyed by offset.


DTE Protocol

The DTE (Data Terminal Equipment) protocol handles communication between the firmware and external tools (pylinkit, LinkIt GUI). It provides parameter read/write, sensor access, log retrieval, and device management.

Protocol Stack

pylinkit / LinkIt GUI (BLE)
        |
    DTE Framing ($CMD#LEN;PAYLOAD\r)
        |
    DTEDecoder (parse + validate)
        |
    DTEHandler (dispatch to command handler)
        |
    ConfigStore / SensorManager / LoggerManager
        |
    DTEEncoder (format response)
        |
    DTE Framing (response)

Key Components

DTEDecoder (core/protocol/dte_protocol.hpp): Parses incoming DTE messages — extracts command name, payload length, payload. Validates length, decodes arguments based on command prototype (BaseEncoding). For PARMR/PARMW: resolves DTE keys to ParamIDs.

DTEEncoder (core/protocol/dte_protocol.hpp): Formats outgoing DTE responses — OK: $O;CMD#LEN;PAYLOAD\r, Error: $N;CMD#LEN;ERROR_CODE\r. Encodes BaseType values to string using BaseEncoding rules.

DTEHandler (core/protocol/dte_handler.cpp): Central dispatcher. Calls DTEDecoder::decode(), switches on DTECommand enum, calls the appropriate handler, returns DTEAction for post-command processing (RESET, FACTR, CONFIG_UPDATED, etc.).

BaseEncoding Types

Encoding Wire Format C++ Type
UINT Decimal string unsigned int
FLOAT Decimal with decimals double
BOOLEAN "0" or "1" bool
HEXADECIMAL Hex string unsigned int
TEXT Raw string std::string
DATESTRING DD/MM/YYYY HH:MM:SS std::time_t
BASE64 Base64-encoded binary BaseRawData
ARGOSMODE "0"-"4" BaseArgosMode
GNSSFIXMODE "1"-"3" BaseGNSSFixMode
GNSSDYNMODEL "0"-"10" BaseGNSSDynModel
LEDMODE "0","1","3" BaseLEDMode
DEPTHPILE Decimal BaseArgosDepthPile
MODULATION "0"-"2" BaseArgosModulation
UWDETECTSOURCE "0"-"3" BaseUnderwaterDetectSource
DEBUGMODE "0"-"3" BaseDebugMode
PRESSURESENSORLOGGINGMODE "0"-"1" BasePressureSensorLoggingMode
PRESSURESENSORFULLSCALE "0"-"1" BasePressureSensorFullScale
SENSORENABLETXMODE "0"-"3" BaseSensorEnableTxMode
KEY_LIST Comma-separated keys vector<ParamID>
KEY_VALUE_LIST key=value pairs vector<ParamValue>

Argos Encoder/Decoder

For satellite transmission, a separate binary encoder/decoder packs GPS fixes and sensor data into compact Argos packets. DTEEncoder bit-packs GPS fixes into depth-piled payloads, DTEDecoder decodes received satellite data.

Parameter Map

Location: core/protocol/dte_params.cpp

Static array mapping ParamID to DTE metadata:

struct BaseMap {
    BaseName name;           // Human-readable name
    BaseKey key;             // 5-char DTE key (e.g., "GNP01")
    BaseEncoding encoding;   // Type encoding
    BaseConstraint min_value; // Minimum value
    BaseConstraint max_value; // Maximum value
    vector<BaseConstraint> permitted_values; // Enum values
    bool is_implemented;     // Available in this build?
    bool is_writable;        // Can be written via PARMW?
};

The is_implemented flag uses ENABLE_* macros, so disabled sensors have their keys hidden from PARML.


Hardware Abstraction Layer

The firmware uses abstract C++ interfaces in core/hardware/ that are implemented in ports/nrf52840/core/hardware/. This allows the core firmware to be portable across different MCUs.

Sensor Abstraction

Location: core/hardware/sensor.hpp

class Sensor : public Calibratable {
public:
    Sensor(const char *name = "Sensor");
    virtual double read(unsigned int port = 0) = 0;
    virtual void install_event_handler(unsigned int, std::function<void()>) {}
    virtual void remove_event_handler(unsigned int) {}
    virtual void set_full_scale(unsigned int mode) {}
};

SensorManager: Static registry — add(Sensor&, name), find_by_name(name), clear().

Sensor Implementations

Name Driver Bus File
"PRS" LPS28DFW (pressure) I2C ports/nrf52840/core/hardware/lps28dfw/
"AXL" BMA400 (accelerometer) I2C ports/nrf52840/core/hardware/bma400/
"RTD" OEM RTD (temperature) I2C ports/nrf52840/core/hardware/
"TSYS01" TSYS01 (temperature) I2C ports/nrf52840/core/hardware/
"ALS" LTR-303 (ambient light) I2C ports/nrf52840/core/hardware/
"PH" OEM pH I2C ports/nrf52840/core/hardware/
"CDT" AD5933 + ADS1115 I2C ports/nrf52840/core/hardware/
"THERM" NTC Thermistor ADC ports/nrf52840/core/hardware/

Power Management

SensorsPowerGuard (RAII): Acquires VSENSORS power rail on construction, releases on destruction. Sensor I2C registers are volatile and lost when VSENSORS is powered off — drivers must re-apply configuration in every read() call.

void LPS28DFW::read(double& temperature, double& pressure) {
    SensorsPowerGuard power_guard;  // VSENSORS ON
    lps28dfw_init_set(&m_ctx, LPS28DFW_DRV_RDY);
    lps28dfw_mode_set(&m_ctx, &m_mode);
    // ... read sensor ...
}  // VSENSORS OFF (guard destroyed)

PMU (Power Management Unit): Abstract interface — delay_ms(), hardware_version(), device_identifier().

Communication Interfaces

  • I2C: NrfI2C::write/read, two buses (ONBOARD_I2C_BUS, EXTERNAL_I2C_BUS)
  • SPI: Satellite module communication (SMD A+ protocol), SPI_SATELLITE bus
  • UART: UART_GPS for u-blox GPS, debug output via UART or USB CDC
  • GPIO: GPIOPins::set/clear/read, acquire_sensors_pwr/release_sensors_pwr, named pins in BSP

Communication Module Variants

Three mutually exclusive satellite/radio communication modules, selected at build time:

Module Interface Baud Rate Files Build Flag
KIM2 UART async (AT commands) 9600 ports/nrf52840/core/hardware/kim2/ Default (no flag)
SMD SPI (A+ protocol) N/A ports/nrf52840/core/hardware/smd_sat/ -DARGOS_SMD=ON
LoRa RAK3172 UART async (RUI3 AT commands) 115200 ports/nrf52840/core/hardware/lora_rak3172/ -DLORA_RAK3172=ON

All three implement the KineisDevice interface and use the EventEmitter<Listener> pattern. KIM2 and SMD are used by ArgosTxService; LoRa RAK3172 is used by LoRaTxService.

SMD Power & Boot Sequence

The SMD module (STM32WL) is fully powered off between communication sessions (SAT_PWR_EN=LOW). At each session, the boot sequence is:

power_on (150ms) → SPI boot wait (100ms) → ping → state_load_kmac → idle → TX

state_load_kmac handles initialization on every boot:

Step Condition Duration Description
Deferred RCONF Only on error recovery ~180ms Write RCONF + save + KMAC reload
Credentials Only if dirty (DTE write) ~240ms Write ID, ADDR, seckey. Skip master RCONF if adaptive modulation ON. Force KMAC reload.
KMAC load Only on first boot or after credential/RCONF change 500ms load_kmac_profil(1) — skipped on normal power cycles (profile persisted in SMD flash)
TCXO warmup Always ~30ms write_tcxo_warmup() — RAM-only register, lost on power cycle
LPM mode Only if != NONE ~30ms write_lpm() — RAM-only register, lost on power cycle

Typical boot (no credential change, KMAC in flash): ~340ms instead of ~2s.

Adaptive Modulation Pre-switch

In Surfacing Burst mode with adaptive modulation (ARP54=1, ARP01=5), the RCONF is managed to minimize boot time:

  • After every TX complete: pre-switch to VLDA4 while SMD is still powered on (live SPI, not deferred). The SMD always powers off with VLDA4 in flash.
  • Doppler TX (process_doppler_burst): SMD boots with VLDA4 already in flash → no RCONF write needed.
  • GNSS TX (process_gnss_burst): ensure_modulation(LDK) switches live (SMD already on from Doppler phase). After TX complete, pre-switch back to VLDA4.
  • Wakeup pin (SAT_EXTWAKEUP): driven HIGH at power_on, LOW at stopped/idle_enter (for STANDBY/SHUTDOWN LPM modes). Only available on LinkIt V4 board.

LED Abstraction

Single-Color LED (core/hardware/led.hpp): on(), off(), get_state(), flash(period_ms), is_flashing(). Implementations: GPIOLed, NrfLed.

RGB LED (core/hardware/rgb_led.hpp): set(color), off(), flash(color, period_ms), flash_alternate(color1, color2, period_ms). Implementation: NrfRGBLed (active-low, timer-based).

Pin GPIO Function
GPIO_LED_RED P1.07 Red channel (active low)
GPIO_LED_GREEN P1.10 Green channel (active low)
GPIO_LED_BLUE P1.04 Blue channel (active low)

Global instances: status_led (RGB, main), ext_status_led (optional single-color). The LED state machine (core/sm/ledsm.hpp) controls status_led during normal operation.


Logging System

The logging system provides per-service flash-based data logging with CSV-formatted retrieval via the DUMPD command.

LogEntry Structure

Location: core/logging/messages.hpp

struct LogHeader {           // 9 bytes
    uint8_t  day, month;
    uint16_t year;
    uint8_t  hours, minutes, seconds;
    LogType  log_type;
    uint8_t  payload_size;
};

struct LogEntry {            // 128 bytes total
    LogHeader header;
    uint8_t data[MAX_LOG_PAYLOAD];  // 119 bytes payload
};

Service-Specific Log Entries

Each service defines a packed struct overlaying LogEntry:

Service Log Struct Fields
GPS GPSLogEntry event_type, batt_voltage, iTOW, fix data (lat, lon, height, etc.)
Pressure PressureLogEntry pressure (bar), temperature (C), altitude (m)
AXL AXLLogEntry x, y, z (g), activity, temperature
Thermistor ThermistorLogEntry temperature (C)
Camera CAMLogEntry event_type, batt_voltage, counter
Underwater UnderwaterLogEntry DRY or WET event
Battery BatteryLogEntry event, voltage
System SystemStartupLogEntry cause (BROWNOUT, WATCHDOG, etc.)

Logger and LogFormatter

Logger (core/logging/logger.hpp): create(), write(void*), read(void*, int), num_entries(), truncate().

LogFormatter: Converts raw log entries to CSV. Each service implements header() and log_entry(). Example: log_datetime,pressure,temperature,altitude\r\n.

LoggerManager: Static registry — add/remove, create(), truncate(), find_by_name().

Log Types

Type ID Name Description
0 GPS GNSS fixes
1 CAM Camera events
2 AXL Accelerometer data
3 STARTUP System startup events
4 UNDERWATER Wet/dry transitions
5 BATTERY Battery events
6 STATE State changes
7 ZONE Geofence events
8 OTA_UPDATE Firmware update events
9 BLE BLE connection events
10 PRESSURE Pressure sensor data
11 THERMISTOR Thermistor sensor data
12 TSYS01 Sea temperature sensor data
13 SWS Saltwater switch events
14 MORTALITY Mortality detection logs

Logs are retrieved via the DUMPD command, which paginates entries and returns base64-encoded CSV data.


Message & Event System

Services communicate through a typed event system. Events are broadcast by ServiceManager to all registered services.

ServiceEvent

Services emit and receive ServiceEvent objects containing:

  • event_source: ServiceIdentifier (which service sent the event)
  • event_type: Type of event
  • event_data: std::variant with event-specific payload

Event Types

Event Source Description
SERVICE_INIT Any Service initialized
SERVICE_ACTIVE Any Service started processing
SERVICE_INACTIVE Any Service completed processing
SERVICE_LOG_UPDATED Sensors New log entry written
GNSS_ON GPS GNSS acquisition started
GNSS_OFF GPS GNSS acquisition completed

Event Flow

  1. Service calls service_complete() or directly emits event
  2. ServiceManager::notify_peer_event() broadcasts to all services
  3. Each service's notify_peer_event() checks if event is relevant
  4. Service may trigger its own action in response

ServiceIdentifier Enum

Value Service
SERVICE_MANAGER ServiceManager itself
GNSS_SENSOR GPSService
UW_SENSOR Underwater detection
ALS_SENSOR Ambient light
AXL_SENSOR Accelerometer
CDT_SENSOR Conductivity-Depth-Temp
PH_SENSOR pH
PRESSURE_SENSOR Pressure
SEA_TEMP_SENSOR Sea temperature
THERMISTOR_SENSOR Thermistor
CAM_SENSOR Camera
ARGOS_TX_SERVICE Argos TX
ARGOS_RX_SERVICE Argos RX
DIVE_MODE Dive mode
MEMORY_MONITOR Memory monitor

Underwater State Notification

A special broadcast path exists for underwater state changes:

ServiceManager::notify_underwater_state(bool is_underwater);

This notifies all services simultaneously, allowing them to defer scheduling when submerged, trigger GNSS acquisition on surfacing, or start/stop dive mode tracking.


Sealed-Device Recovery (Plan 1 + correctifs, 2026-05)

For trackers deployed sealed for 12+ months without operator access, three independent recovery layers protect against unexpected freezes:

Layer 1 — Boot-fail counter with progressive recovery

A .noinit-RAM struct tracks consecutive boot failures, CRC16-protected:

struct BootFailNoinit {
    uint16_t consecutive_failures;
    uint8_t  factory_reset_attempted;
    uint16_t crc;
};

BootState::entry() increments on every boot, cleared on successful OperationalState::entry(). Cascade:

Failures Action
1–2 Normal — ErrorState → soft reset retry
3 configuration_store->factory_reset(), set factory_reset_attempted=1, soft reset
≥ 5 Fall through to OffState (last resort, magnet required to wake)

Located in core/sm/gentracker.cpp (BootState::entry, ErrorState::entry).

Layer 2 — Exception barriers

5 strategic try/catch blocks ensure a panic in any single subsystem does NOT cascade into a permanent freeze:

Barrier Catches Recovery
GenTracker::periodic_config_flush() save_params() throws Log, continue. Next 30-min flush retries.
OperationalState::exit() Each destructive step (LED, BLE stop, stopall, save_params, battery unsubscribe) Each independently caught, next step still runs.
BootState::entry() factory_reset() itself throws Log, soft reset anyway.
ServiceManager::stopall() Per-service stop() exception + per-service WDT kick One bad service doesn't stop the others.
service.cpp::reschedule() Service-level throws Reset m_is_initiated, reschedule cleanly.

Layer 3 — Watchdog with smart kicks

The 15-min nRF WDT is kicked by GenTracker::kick_watchdog() posted at HIGHEST priority every ~13.5 min (90 % of period). For long blocking operations, additional explicit kicks are added:

  • BootState::entry() before factory_reset(), after, and around filesystem mount/format
  • ServiceManager::stopall() between each service
  • PMU long delays (firmware-update reboot countdown)

This means a single subsystem hang is bounded by the WDT (15 min max), then the boot-fail counter handles the recovery cycle. Worst-case unattended recovery: ~75 min (5 × 15 min) before falling through to OffState.

See Sealed-device hardening above for the defense layer catalog and 10 — GPS Guide § Part E for RTC handling under sealed operation.

Clone this wiki locally