refactor: trim CLAUDE.md per best practices
Some checks failed
ClearGrow Probe CI / Build Development Firmware (push) Has been cancelled
ClearGrow Probe CI / Build Production Firmware (push) Has been cancelled
ClearGrow Probe CI / CI Status Summary (push) Has been cancelled

Following Anthropic's Claude Code best practices:
- CLAUDE.md: 504 → 121 lines

Keep concise: commands, code style, critical rules.
Move detailed specs to /opt/repos/docs/reference/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
CI System
2025-12-10 15:54:36 -07:00
parent 94925bb9d3
commit 4f8a43a582

536
CLAUDE.md
View File

@@ -1,504 +1,120 @@
# ClearGrow Probe Firmware # ClearGrow Probe Firmware
nRF52840-based wireless sensor node with Thread networking, code-based auto-pairing, and comprehensive environmental monitoring. nRF52840 wireless sensor node with Thread networking and code-based pairing.
## Quick Reference ## Commands
```bash ```bash
# Build environment setup # Setup
source ~/ncs/.venv/bin/activate source ~/ncs/.venv/bin/activate
source ~/ncs/zephyr/zephyr-env.sh source ~/ncs/zephyr/zephyr-env.sh
cd /root/cleargrow/probe cd /opt/repos/probe
# Build # Build and flash
west build -b nrf52840dk_nrf52840 west build -b nrf52840dk_nrf52840
# Flash
west flash west flash
# Clean rebuild # Clean rebuild
rm -rf build && west build -b nrf52840dk_nrf52840 rm -rf build && west build -b nrf52840dk_nrf52840
# Monitor logs (RTT or serial) # Memory reports
minicom -D /dev/ttyACM0 -b 115200
# Check memory usage
west build -t rom_report west build -t rom_report
west build -t ram_report west build -t ram_report
# Monitor (RTT recommended)
JLinkRTTClient
# Or serial
minicom -D /dev/ttyACM0 -b 115200
``` ```
## Architecture Overview ## Code Style
### Source Structure - **Zephyr style**: 4-space indent, `snake_case`
- **Logging**: `LOG_MODULE_REGISTER(name, LOG_LEVEL_INF)`, then `LOG_INF()`, `LOG_WRN()`, `LOG_ERR()`
- **Timing**: `k_sleep(K_MSEC(100))`, never raw ticks
## Critical Rules
**DO NOT use Zephyr PM framework** - nRF52840 lacks `HAS_PM` support. Use Nordic HAL directly:
```c
#include <hal/nrf_power.h>
nrf_power_system_off(NRF_POWER); // <1µA, does NOT return
```
**Thread role is MTD-SED** (Sleepy End Device) - poll period 1 second
**CoAP operations are blocking** - never call from time-critical contexts
## Architecture
``` ```
src/ src/
├── main.c # Entry point, threads, state machine ├── main.c # Entry point, state machine
├── sensor_manager.c # I2C sensor drivers, auto-detection ├── sensor_manager.c # I2C sensor drivers
├── thread_node.c # OpenThread MTD-SED, CoAP client ├── thread_node.c # Thread + CoAP client
├── power_manager.c # Battery monitoring, sleep modes ├── power_manager.c # Battery, sleep modes
└── pairing_code.c # PSKd generation and storage └── pairing_code.c # PSKd generation
include/
├── probe_config.h # Hardware config, data structures
└── pairing_code.h # Pairing API declarations
``` ```
### Task Architecture
| Thread | Stack | Priority | Purpose |
|--------|-------|----------|---------|
| Main | 2048 | Default | Watchdog, power state management |
| Sensor | 2048 | 7 | Periodic sensor reading |
| Transmit | 3072 | 8 | CoAP data transmission |
**Data Flow**:
1. Sensor thread reads I2C sensors every 5s (configurable)
2. Signals transmit thread via semaphore
3. Transmit thread encodes TLV and sends CoAP POST
4. Main thread feeds watchdog, manages power states
### Power Management
Target: 1+ year on 2x AA batteries
| State | Current | When Used | Implementation |
|-------|---------|-----------|----------------|
| Active | 10-15mA | Not commissioned | Normal operation |
| Idle | 6-8mA | Normal operation, Thread child | Thread SED mode |
| Sleep | 3-5mA | Low battery | Adaptive polling |
| Deep Sleep | <3µA | Critical battery | Nordic `nrf_power_system_off()` |
| SHIPPING | <1µA | Storage/transit | System OFF, all peripherals disabled |
**Strategy**:
- Thread SED mode with 1s poll period
- Adaptive sensor polling based on battery level
- Soil sensor power controlled via GPIO
- **System OFF via Nordic HAL** (not Zephyr PM - see note below)
**Important**: nRF52840 does NOT support Zephyr's generic `CONFIG_PM` framework (requires `HAS_PM` which is only available on newer chips like nRF54). Deep sleep and SHIPPING mode use Nordic HAL `nrf_power_system_off(NRF_POWER)` directly.
## Sensor Configuration
### Supported Sensors (I2C)
| Sensor | Address | Measurement | Driver Status |
|--------|---------|-------------|---------------|
| SHT4x/SHTC3 | 0x44/0x45 | Temp, humidity, VPD, dew point | Production |
| MLX90614 | 0x5A | Leaf temp (IR), leaf VPD | Production |
| ADS1115 | 0x48 | Soil moisture, EC, pH, temp | Production |
| SCD4x | 0x62 | CO2, temp, humidity | Production |
| VEML7700 | 0x10 | PAR, lux, DLI | Production |
| Module EEPROM | 0x50 | Module identification | Optional |
### Auto-Detection
On startup, `sensor_manager` scans I2C bus for:
1. Module EEPROM (0x50) - reads type, serial, calibration
2. Direct sensor detection - checks if device responds
3. Builds `modules_present` bitmap
**Retry Logic**: 3 attempts with exponential backoff (10ms base)
## Thread Network
### Role: MTD-SED (Minimal Thread Device - Sleepy End Device)
**Configuration** (prj.conf):
```ini
CONFIG_OPENTHREAD_MTD=y
CONFIG_OPENTHREAD_MTD_SED=y
CONFIG_OPENTHREAD_POLL_PERIOD=1000 # Poll parent every 1 second
CONFIG_OPENTHREAD_JOINER=y
CONFIG_OPENTHREAD_SRP_CLIENT=y
```
**Network Behavior**:
1. On boot, checks if commissioned (`otDatasetIsCommissioned`)
2. If not commissioned, auto-starts joiner with PSKd
3. Joiner retry: 5s initial, exponential backoff to 5min max
4. **Battery protection timeout**: 10 minutes total, then enters low-power wait
5. When joined, registers SRP service: `_cleargrow._udp`
### CoAP Message Format
**POST /sensors** (TLV encoding):
```
Header (12 bytes):
[0-7] EUI-64 (probe ID)
[8] Protocol version (1)
[9] Battery percent
[10-11] Sequence number (uint16, little-endian)
Measurements (TLV):
[Type][ValueInfo][Value...]
Types:
0x01 = Temperature (float32)
0x02 = Humidity (float32)
0x03 = CO2 (uint16)
0x04 = PPFD/PAR (float32)
0x05 = VPD (float32)
0x06 = Leaf temp (float32)
0x07 = Soil moisture (float32)
0x08 = Soil temp (float32)
0x09 = EC (float32)
0x0A = pH (float32)
0x0B = DLI (float32)
0x0C = Dew point (float32)
```
**ValueInfo nibbles**:
- Upper nibble: type (0=float32, 3=int16, 4=uint16)
- Lower nibble: length in bytes
**Controller Address**: Set via `thread_node_set_controller_addr()` with IPv6 address. DNS-SD resolution not yet implemented.
## Code-Based Pairing
Uses a printed code on device label for pairing.
### PSKd (Pre-Shared Key for Device)
**Format**:
- 6 characters (configurable via `CONFIG_PSKD_LENGTH`)
- Character set: 0-9, A-Z excluding I, O, Q, Z (Thread spec compliant)
- Generated using cryptographic RNG (`sys_csrand_get`)
- Stored in NVS flash (`settings` subsystem)
**Example**: `A3F2K7`
**Generation** (`pairing_code.c`):
1. On first boot, generates random PSKd
2. Saves to flash: `pairing/pskd`
3. Persists across reboots
4. Can regenerate via `pairing_code_regenerate()`
### Auto-Joiner Behavior
**State Machine** (`thread_node.c:auto_start_joiner()`):
```c
1. Check if commissioned YES: enable Thread, done
2. Check battery timeout (10min) YES: stop attempts, sleep
3. Get PSKd from pairing_code module
4. Start joiner: otJoinerStart(pskd, "ClearGrow", "Probe", "1.0")
5. On failure: schedule retry with exponential backoff
```
**Retry Parameters**:
- Initial delay: 5 seconds
- Max delay: 5 minutes
- Total timeout: 10 minutes (battery protection)
**User Flow**:
1. User powers on probe
2. Probe auto-starts joiner (LED blinks pattern)
3. User enters PSKd on controller UI
4. Controller enables commissioner with wildcard EUI-64
5. Probe completes DTLS handshake, joins network
6. First CoAP message confirms pairing (3 quick blinks)
## Key APIs ## Key APIs
### Sensor Manager (`sensor_manager.c`)
```c ```c
// Initialize (detects modules, starts SCD4x periodic mode) // Sensors
int sensor_manager_init(void); sensor_manager_init();
sensor_manager_get_data(&data);
// Main loop - call periodically // Thread
void sensor_manager_loop(void); thread_node_init();
thread_node_send_sensors(&data);
// Thread-safe data access // Power
int sensor_manager_get_data(probe_sensor_data_t *data); power_manager_get_battery_percent();
power_manager_set_state(POWER_STATE_SLEEP);
// Check if specific module present // Pairing
bool sensor_manager_has_module(probe_module_type_t type); pairing_code_get_pskd(); // Returns 6-char code
// Reset DLI accumulator (daily)
void sensor_manager_reset_dli(void);
``` ```
**VPD Calculation** (Magnus formula): ## Sensors (I2C)
```c
float svp = 0.6108 * exp(17.27 * temp_c / (temp_c + 237.3));
float vpd = svp * (1.0 - (rh / 100.0));
```
**DLI Accumulation** (umol/m2/s → mol/m2/day): | Sensor | Addr | Measures |
```c |--------|------|----------|
dli += (par_umol * dt_sec) / 1000000.0; // Resets at 24h | SHT4x | 0x44 | Temp, humidity |
``` | MLX90614 | 0x5A | Leaf temp (IR) |
| ADS1115 | 0x48 | Soil moisture, EC, pH |
| SCD4x | 0x62 | CO2 |
| VEML7700 | 0x10 | PAR/PPFD, DLI |
### Thread Node (`thread_node.c`) ## Power States
```c | State | Current | Use |
// Initialize Thread stack |-------|---------|-----|
int thread_node_init(void); | Active | 10-15mA | Not commissioned |
| Idle | 6-8mA | Normal operation (SED) |
| Sleep | 3-5mA | Low battery |
| System OFF | <1µA | Critical/shipping |
// Send sensor data (blocking CoAP POST) ## Pairing
int thread_node_send_sensors(const probe_sensor_data_t *data);
// Set controller IPv6 address - **PSKd**: 6-character code printed on device label
int thread_node_set_controller_addr(const struct in6_addr *addr, uint16_t port); - **Format**: `A3F2K7` (excludes I, O, Q, Z)
- **Auto-joiner**: Retries with backoff, 10-min timeout for battery protection
// Get EUI-64 identifier ## Debugging
int thread_node_get_eui64(uint8_t eui64[8]);
// Factory reset Thread credentials
int thread_node_factory_reset(void);
// State callback
void thread_node_set_state_callback(void (*cb)(thread_state_t, void *), void *ctx);
```
**Important**: CoAP operations are blocking. Do NOT call from UI contexts.
### Power Manager (`power_manager.c`)
```c
// Initialize (configures ADC, starts battery sampling)
int power_manager_init(void);
// Set power state
int power_manager_set_state(power_state_t state);
// Get battery info
uint16_t power_manager_get_battery_mv(void);
uint8_t power_manager_get_battery_percent(void);
bool power_manager_is_battery_low(void);
// Adaptive polling interval
uint32_t power_manager_get_recommended_poll_interval(void);
// Soil sensor power control
int power_manager_set_soil_power(bool enabled);
```
**Battery Sampling**:
- ADC channel 0 (AIN0) with voltage divider
- Sampled every 5 minutes (configurable)
- Low battery callback available
### Pairing Code (`pairing_code.c`)
```c
// Initialize (loads or generates PSKd)
int pairing_code_init(void);
// Get current PSKd
const char *pairing_code_get_pskd(void);
// Regenerate PSKd (security)
int pairing_code_regenerate(void);
// Validate PSKd format
bool pairing_code_validate(const char *pskd);
```
## Configuration
### Key Build Options (prj.conf)
```ini
# Thread networking
CONFIG_OPENTHREAD_MTD_SED=y
CONFIG_OPENTHREAD_POLL_PERIOD=1000
CONFIG_OPENTHREAD_JOINER=y
CONFIG_OPENTHREAD_SRP_CLIENT=y
# Code-based pairing
CONFIG_CODE_PAIRING=y
CONFIG_PSKD_LENGTH=6
# Power management (nRF52840-specific)
# NOTE: CONFIG_PM/CONFIG_PM_DEVICE are NOT supported on nRF52840
# (Zephyr PM framework requires HAS_PM, which nRF52840 lacks)
# Instead, use Nordic HAL: nrf_power_system_off() for System OFF state
# See PROBE-SL-002 remediation (2025-12-09) for details
# Persistent storage
CONFIG_NVS=y
CONFIG_SETTINGS=y
CONFIG_SETTINGS_NVS=y
# Watchdog
CONFIG_WATCHDOG=y
CONFIG_WDT_DISABLE_AT_BOOT=n
# MCUboot for OTA
CONFIG_BOOTLOADER_MCUBOOT=y
CONFIG_STREAM_FLASH=y
CONFIG_IMG_MANAGER=y
# Memory
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_HEAP_MEM_POOL_SIZE=16384
```
### Runtime Configuration
**Adaptive Polling** (battery-based):
- Battery >50%: 5 seconds (default)
- Battery 20-50%: 10 seconds
- Battery low: 30 seconds
- Battery critical: 60 seconds
**Thread Poll Period**: 1000ms (1 second)
- Probe wakes to poll parent, check for downlink data
- CPU sleeps between polls
## Debugging Tips
### Common Issues
1. **Probe won't join network**
- Check PSKd matches device label
- Verify controller is in pairing mode (commissioner active)
- Check Thread network is active (border router running)
- Battery level >30% (joiner requires radio power)
- Look for `Joiner failed` logs with error code
2. **High power consumption**
- Verify SED mode: `grep "MTD-SED" build/zephyr/zephyr.dts`
- Check poll period: should be 1000ms
- Ensure soil sensor power disabled between reads
- Look for I2C bus lockups (retry loops)
3. **Sensor read failures**
- Check I2C pull-ups (4.7k to 3.3V)
- Verify I2C address in `probe_config.h`
- Try `sensor_manager_rescan()` to re-detect
- Check module EEPROM if present
4. **Auto-joiner timeout**
- 10-minute timeout is safety feature
- Power cycle to retry
- Check battery level (critical = no join attempts)
- Verify Thread network credentials correct
5. **CoAP transmission failures**
- Controller address must be set (IPv6)
- Check `s_controller_resolved` flag
- Verify controller CoAP server listening on port 5683
- Thread parent must be reachable (RSSI check)
### Log Analysis
**Look for these messages**:
```
✓ "Pairing code initialized (PSKd: XXXXXX)"
✓ "Thread state: Child"
✓ "Sent TLV sensor data (seq X, Y bytes)"
✗ "Joiner failed: X"
✗ "Failed to transmit sensor data: X"
✗ "Battery: XmV (critical)"
```
**Memory usage**:
```bash ```bash
west build -t rom_report # Flash usage by section # Check Thread SED mode
west build -t ram_report # RAM usage by symbol grep "MTD-SED" build/zephyr/zephyr.dts
# Log levels
LOG_MODULE_REGISTER(thread_node, LOG_LEVEL_DBG);
``` ```
**Expected**: **Common issues**:
- Flash: 400-600KB (< 70% of 1MB) - Won't join: Check PSKd, verify controller in pairing mode
- RAM: 64-100KB (< 50% of 256KB) - High power: Verify SED mode, check poll period
- Sensor fails: Check I2C pull-ups (4.7k), verify address
### Serial Logging ## Documentation
**RTT (recommended)**: Full docs: `/opt/repos/docs/reference/firmware/probe/`
```bash
JLinkRTTClient
# Fast, no UART overhead
```
**UART**:
```bash
minicom -D /dev/ttyACM0 -b 115200
# Standard serial console
```
**Log Levels** (per module):
```c
LOG_MODULE_REGISTER(module_name, LOG_LEVEL_INF);
// Change to LOG_LEVEL_DBG for verbose output
```
## OTA Updates
**MCUboot dual-bank scheme**:
- Slot 0: Active firmware
- Slot 1: Download area
- On reboot, MCUboot validates and swaps if needed
**Build artifacts**:
```
build/zephyr/zephyr.hex # Main firmware (for direct flash)
build/zephyr/merged.hex # Firmware + MCUboot bootloader
build/zephyr/zephyr.signed.bin # Signed image for OTA
```
**OTA not yet implemented** - placeholder for future work via CoAP block transfer.
## Memory Map
| Region | Address | Size | Usage |
|--------|---------|------|-------|
| Flash | 0x00000000 | 48KB | MCUboot bootloader |
| Flash | 0x0000C000 | 476KB | Slot 0 (active firmware) |
| Flash | 0x00083000 | 476KB | Slot 1 (OTA download) |
| Flash | 0x000FA000 | 24KB | NVS settings storage |
| RAM | 0x20000000 | 256KB | Application + stack |
## Hardware Variants
**Current target**: nRF52840 DK (`nrf52840dk_nrf52840`)
**Custom board**: Create devicetree overlay in board-specific directory.
**Example** (`boards/nrf52840_probe.overlay`):
```dts
&i2c0 {
status = "okay";
sda-pin = <26>;
scl-pin = <27>;
};
&adc {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
channel@0 {
reg = <0>;
zephyr,gain = "ADC_GAIN_1_6";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <10>;
zephyr,input-positive = <NRF_SAADC_AIN0>;
};
};
```
## Appendix: Constants
**Battery thresholds** (3.0V LiPo):
```c
BATTERY_FULL_MV 4200
BATTERY_NOMINAL_MV 3700
BATTERY_LOW_MV 3400
BATTERY_CRITICAL_MV 3200
BATTERY_CUTOFF_MV 3000
```
**PSKd character set**:
```
0123456789ABCDEFGHJKLMNPRSTUVWXY
(32 chars: excludes I, O, Q, Z)
```
**Module EEPROM magic**: `0x434C4752` ("CLGR")