This firmware is specifically optimized for battery-powered sensor nodes that sleep between measurements to maximize battery life. It uses the RAK4631 WisBlock Core module with a DS3231 RTC for timed wake-up, achieving ultra-low sleep current (~0.4µA) for months of operation on a single battery charge.
This firmware has been carefully optimized by removing features incompatible with sleep/wake cycles and implementing sleep-aware alternatives. Every design decision prioritizes power efficiency and reliability for nodes that spend >99% of their time asleep.
Key Optimizations:
- Removed alert system - retries and acknowledgments incompatible with sleeping nodes
- Removed time-based advertisements - replaced with wakeup counter-based periodic ads
- Removed temporary radio parameters - not suitable for nodes that reset on wake
- Immediate ACL writes - no lazy writes that could be lost during sleep
- Added private channel support - encrypted telemetry broadcasts for secure sensor data
- Added zone-based broadcasting - transport codes for selective routing and reduced congestion
The firmware uses a state machine architecture that keeps the device awake during sensor processing while remaining responsive to commands and mesh operations. This provides a flexible foundation for complex sensor scenarios while maintaining ultra-low power consumption.
┌─────────────────────────────────────────────────────────────┐
│ WAKE UP (RTC Alarm) │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌────────────────┐
│ SAMPLING │ ◄─── Take samples at intervals
│ (10 samples) │ (configurable timing & count)
└────────┬───────┘
│
▼
┌────────────────┐
│ PROCESSING │ ◄─── Average samples
│ (averaging) │ Broadcast telemetry (always)
└────────┬───────┘ Check wakeup counter
│
┌────────────┴────────────┐
│ │
counter >= threshold? counter < threshold?
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ ADVERTISING │ │ READY_TO_SLEEP│
│ (send advert)│ │ (save & sleep)│
└──────┬───────┘ └──────┬────────┘
│ │
└────────────┬────────────┘
│
▼
┌────────────────┐
│ READY_TO_SLEEP │
│ (enter sleep) │
└────────┬───────┘
│
▼
┌────────────────┐
│ INTERACTIVE │ ◄─── Command received?
│ MODE │ Stay awake, no sleep
└────────────────┘ 5min inactivity timeout
- Active loop architecture: Loop runs continuously during wake cycles
- Responsive design: mesh.loop(), sensors.loop(), serial commands all active
- State machine: Clean separation of concerns (SAMPLING → PROCESSING → ADVERTISING → SLEEP)
- Multi-sample averaging: Configurable sample count and timing
- Separate lifecycles: Telemetry sent every wake, advertisements sent periodically
- Private channel support: AES-encrypted telemetry broadcasts for secure sensor data
- Zone-based broadcasting: Transport codes for selective routing and reduced network congestion
- Interactive mode: Automatic entry when commands received, stays awake for configuration
- Configurable parameters: Sample interval, sample count, sleep duration all configurable
- Safety timeouts: 5-minute max awake time (except in interactive mode)
- Immediate persistence: All configuration changes saved immediately to prevent data loss
To maximize reliability and power efficiency for sleeping nodes, the following features have been intentionally removed:
Alert System (with retries and acknowledgments)
- Why removed: Alerts require persistent connections and retry logic that are incompatible with nodes that sleep between wake cycles. Acknowledgments cannot be reliably received when the sender is asleep.
- Alternative: Implement application-level notifications at the receiver/gateway side based on received telemetry data.
Time-Based Advertisements
- Why removed: Time-based scheduling requires continuous operation and is unreliable when nodes wake at irregular intervals.
- Alternative: Wakeup counter-based advertisements - advertise every N wakeups (e.g., advertise on every 12th wakeup).
Temporary Radio Parameters
- Why removed: Temporary radio configurations are lost when the node enters system-off sleep mode and the CPU resets on wake.
- Alternative: Use permanent configuration via preferences that persist across sleep cycles.
ACL Lazy Writes
- Why removed: Delayed writes to flash increase the risk of data loss if the node enters sleep before the write completes.
- Alternative: Immediate writes to flash ensure configuration changes are persisted before sleep.
Neighbor Tracking and Time Series Logging
- Why removed: These features require continuous operation and significant RAM/flash resources that are wasted on nodes that only wake briefly.
- Alternative: Simple multi-sample averaging during wake cycles, with raw telemetry sent to gateway for storage and analysis.
- RAK4631 - WisBlock Core module (nRF52840 + SX1262)
- DS3231 RTC Module - Connected via I2C (address 0x68)
- RAK19007 or RAK5005-O - WisBlock Base Board
- Optional: RAK1906 or other environment sensors
- Insert RAK4631 into the WisBlock Base Board CPU slot
- Connect DS3231 RTC module to I2C pins:
- SDA: Pin 13 (WB_I2C1_SDA)
- SCL: Pin 14 (WB_I2C1_SCL)
- INT/SQW: GPIO 17 (WB_IO1) for wake-up interrupt
The following constants can be adjusted in main.cpp:204-206:
static const uint32_t SAMPLE_INTERVAL_MS = 1000; // 1 second between samples
static const uint8_t NUM_SAMPLES = 10; // Number of samples to collect
static const uint32_t MAX_AWAKE_TIME_MS = 5 * 60 * 1000; // 5 minutes max awake time
static const uint32_t INTERACTIVE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes interactive timeoutFuture: These will be moved to node preferences for runtime configuration via serial commands.
Wake Cycle:
- RTC alarm triggers → Wake from sleep
- SAMPLING state: Collect samples at configured intervals
- Takes
NUM_SAMPLESsamples (default: 10) - Interval:
SAMPLE_INTERVAL_MS(default: 1 second) - Loop remains active, mesh/sensors responsive
- Takes
- PROCESSING state: Process collected samples
- Average samples
- Always broadcast telemetry data (every wake cycle)
- Check wakeup counter against threshold
- ADVERTISING state (conditional):
- Only if
wakeup_count >= wakeups_per_advert - Send self-advertisement
- Reset counter to 0
- Only if
- READY_TO_SLEEP state:
- Save wakeup counter to flash
- Log awake duration
- Enter system-off sleep mode (60 seconds)
Entry: Automatically enters when serial command received
Behavior:
- Node stays awake indefinitely
- All loops remain active (mesh, sensors, serial)
- Can send multiple commands
- Each command resets 5-minute inactivity timer
- After 5 minutes of no activity → transitions to READY_TO_SLEEP
Use cases:
- Configuration/debugging
- Real-time sensor monitoring
- Firmware updates via serial
- Manual mesh operations
Exit: Automatic after 5 minutes of inactivity, or manual via exit command
This firmware implements separate lifecycles for telemetry and advertisements:
- Frequency: Every wake cycle (e.g., every 60 seconds)
- Content: Sensor readings (battery voltage, temperature, etc.)
- Format: CayenneLPP via group messages
- Encryption: Can be public or AES-encrypted via private channels
- Routing: Can use zones (transport codes) for selective forwarding
- Purpose: Continuous data reporting
- Frequency: Based on
wakeups_per_advertcounter - Content: Node identity, timestamp, name
- Encryption: Always unencrypted (public)
- Purpose: Mesh network presence, discovery, routing
- Example: If
wakeups_per_advert = 12and wake every 60s → advertise every 12 minutes
Rationale: Sensor data needs to be transmitted frequently and can be encrypted/routed selectively, but advertisements (which help establish mesh routing) only need to happen occasionally and must remain public for network discovery.
Build with PlatformIO:
pio run -e RAK_4631_sleeping_sensor
pio run -e RAK_4631_sleeping_sensor -t uploadsleep set <seconds> - Set sleep interval (default: 60)
sleep status - Show current sleep configuration
advert set <count> - Set wakeups per advertisement (1-255)
advert status - Show advertisement frequency
Example:
sleep set 60 # Wake every 60 seconds
advert set 12 # Advertise every 12 wakeups (12 minutes)Private channels enable AES-encrypted telemetry broadcasts to secure sensor data in untrusted environments. This is ideal for sensitive measurements or multi-tenant deployments.
channel set <psk_base64> - Enable private channel with base64-encoded PSK (16 or 32 bytes)
channel clear - Disable private channel (revert to public broadcast)
channel status - Show current channel status
PSK Requirements:
- Must be base64-encoded
- Supports 128-bit (16 bytes) or 256-bit (32 bytes) keys
- Same PSK must be configured on all nodes and gateways that need to communicate
Generating a PSK:
# Generate 128-bit (16-byte) random key and encode as base64
openssl rand -base64 16
# Generate 256-bit (32-byte) random key and encode as base64
openssl rand -base64 32Example Configuration:
# Enable private channel with 256-bit encryption
channel set m3JKxnP0dF8kL2mN5qR9sT1vU7wX3yZ6aC4eG8hJ0lM=
# Check status
channel status
# Output: Private channel: enabled (hash: A5B2C3D4...)
# Disable encryption
channel clear
# Output: Private channel disabled (public broadcast)Security Notes:
- Private channel PSK is persisted to flash and survives reboots
- All nodes sharing the same PSK can decrypt each other's telemetry
- Gateway/receiver nodes must be configured with the same PSK to decrypt data
- PSK is stored in plaintext in flash - physical security is required
Zones use transport codes to enable selective packet forwarding, reducing network congestion by limiting which repeaters forward your packets.
zone set <name> - Set transport code zone for filtering
zone clear - Clear zone (standard flood)
zone status - Show current zone configuration
How Zones Work:
- Sensor node sets a zone name (e.g., "building-a", "sensor", "floor-2")
- Packets are tagged with a transport code derived from the zone name
- Only repeaters configured with matching zones will forward the packets
- Reduces unnecessary retransmissions and interference
Example:
# Configure sensor to use "building-a" zone
zone set building-a
# Check status
zone status
# Output: Zone: building-a (transport code: 0x1A2B)
# Revert to standard flooding (all repeaters forward)
zone clear
# Output: Zone cleared (standard flood)Use Cases:
- Multi-building deployments: Separate "building-a", "building-b" zones
- Floor-based routing: "floor-1", "floor-2", "floor-3" zones
- Device type filtering: "sensor", "actuator", "gateway" zones
- Tenant isolation: "tenant-1", "tenant-2" in multi-tenant setups
Configuration:
- Zone name is persisted to flash and survives reboots
- Default zone:
sensor(can be changed in firmware or via commands) - Repeaters must be configured with matching zones to forward packets
Typical power consumption with 60-second sleep interval and default settings:
| State | Current | Duration | Notes |
|---|---|---|---|
| Deep Sleep | 0.4µA | ~47 sec | nRF52 system-off + RTC |
| Sampling | 20mA | ~10 sec | Taking 10 samples @ 1/sec |
| Processing/TX | 130mA | ~3 sec | Telemetry broadcast |
| Average | ~8µA | 60 sec | Per wake cycle |
Battery Life Estimate (3000mAh):
- With hourly advertisement: ~150 days (~5 months)
- With 2-hour advertisement: ~180 days (~6 months)
src/
├── main.cpp # State machine implementation
├── SensorMesh.h/cpp # Sensor mesh base class
├── TimeSeriesData.h/cpp # Time series data handling
variants/rak4631/
├── RAK4631Board.h # Board definitions
└── RAK4631Board.cpp # RTC wake-up, sleep implementation
enum SensorNodeState {
SAMPLING, // Collecting sensor samples
PROCESSING, // Averaging samples, broadcasting telemetry
ADVERTISING, // Sending self-advertisement (periodic)
READY_TO_SLEEP, // Saving state and entering sleep
INTERACTIVE_MODE // Awake for configuration (command-triggered)
};-
Max awake timeout: 5-minute safety timeout forces sleep if state machine hangs
- Does NOT apply in interactive mode
- Prevents battery drain from bugs
-
Inactivity timeout: Interactive mode exits after 5 minutes of no commands
- Automatic return to normal operation
- Prevents accidentally leaving node awake
-
State tracking: All timing tracked with
awake_start_time,state_start_time- Easy debugging via serial output
- Accurate power profiling
-- todo
Adjust sampling in the SAMPLING state case (main.cpp:107-122):
case SAMPLING: {
if (now - last_sample_time >= SAMPLE_INTERVAL_MS) {
// Custom sampling logic here
sensor_samples[sample_count] = readYourSensor();
sample_count++;
last_sample_time = now;
if (sample_count >= NUM_SAMPLES) {
current_state = PROCESSING;
}
}
break;
}Easy to extend the state machine:
enum SensorNodeState {
WARMING_UP, // New: Wait for sensor warmup
SAMPLING,
PROCESSING,
CALIBRATING, // New: Sensor calibration
ADVERTISING,
READY_TO_SLEEP,
INTERACTIVE_MODE
};
// In loop():
case WARMING_UP: {
if (now - state_start_time >= 5000) { // 5 second warmup
current_state = SAMPLING;
MESH_DEBUG_PRINTLN("Sensor ready, starting sampling");
}
break;
}- I2C Address: 0x68
- Alarm: Alarm 1 used for wake-up
- Interrupt: Active-LOW on INT/SQW pin
- Accuracy: ±2ppm (better than RV-3028-C7)
- DS3231 alarm triggers, pulls INT pin LOW
- nRF52840 GPIO sense detects LOW on WB_IO1
- System exits system-off mode, CPU resets
board.begin()checks RTC alarm flagsetup()initializes state machine to SAMPLINGloop()begins executing state machine
- Transition to
READY_TO_SLEEPstate - Save wakeup counter to flash
- Call
board.enterLowPowerSleep(60) - Configure DS3231 alarm for 60 seconds
- Configure GPIO sense on INT pin
- Enter nRF52 system-off mode
- CPU halts (~0.4µA) until GPIO sense triggers
- Check I2C connections (SDA=13, SCL=14)
- Verify DS3231 module address (0x68)
- Enable debug:
-D MESH_DEBUG=1in platformio.ini - Check I2C pull-ups (4.7kΩ typically required)
- Verify INT/SQW connected to WB_IO1 (GPIO 17)
- Check DS3231 alarm configuration
- Ensure SQW/INT pin configured for alarm output
- Monitor serial output for RTC status messages
- Check serial output for state transitions
- 5-minute safety timeout should force sleep
- Send serial command to enter interactive mode for debugging
- Verify sleep actually happening (LED should be off)
- Check for infinite loops in state machine
- Monitor awake duration in serial output
- Ensure mesh.loop() not blocking
- Runtime configuration: Move sampling constants (interval, count, timeouts) to node preferences for configuration via serial commands
- Adaptive sampling: Adjust sample rate based on sensor variance (e.g., sample more frequently when temperature changing rapidly)
- Low battery mode: Reduce wake frequency and disable non-critical features when battery below threshold
- Sensor warmup state: Built-in support for sensors requiring warmup time (e.g., gas sensors, CO2 sensors)
- Data buffering with retry: Store multiple telemetry readings in flash when mesh unavailable, transmit on next successful wake
- Conditional telemetry: Only transmit when sensor values change beyond a threshold to reduce airtime
- Multi-zone broadcasting: Broadcast to multiple zones simultaneously for redundant routing
- Configurable encryption: Runtime PSK configuration without hardcoding, with key rotation support
- PSK is stored in plaintext in flash - physical device security is critical
- Use 256-bit keys (32 bytes) for maximum security
- Rotate keys periodically, especially if devices are lost or compromised
- Keep PSK secret and use secure channels for distribution to nodes/gateways
- Zones provide routing isolation, not security/encryption
- Zone names are public and can be observed by network monitoring
- Use descriptive but non-sensitive zone names (avoid "vault", "sensitive-lab", etc.)
- Combine zones with private channels for both routing control and data security
- Nodes contain cryptographic material (identity keys, PSKs)
- If device is physically compromised, assume all keys are compromised
- Consider tamper-evident enclosures for high-security deployments
- Use secure boot/flash encryption on platforms that support it
- Defense in depth: Use both private channels AND zones for maximum security
- Key management: Maintain a secure registry of PSKs and which nodes use them
- Network segmentation: Use different PSKs for different security zones
- Monitoring: Log and monitor telemetry at gateways for anomalies
- Battery monitoring: Low battery can indicate theft or tampering
Same as parent MeshCore project.