# PCB's

# Sensor Board

All about the sensor board

# Overview

The Sensor Board is an embedded system that acquires environmental and process data from multiple sensors and actuates the sampling hardware (water pump). It integrates water quality (pH), motion (IMU), force (load cells), pressure (FSR pads), and water flow measurement, and transmits all data over Ethernet using Protocol Buffer encoding. It also receives control packets (for example pump commands) over the same link.

**Implementation Status Disclaimer:**<span style="white-space: pre-wrap;"> The sensor board code was only partially tested and never fully implemented on hardware. Due to time constraints and other technical issues faced by the 2025-2026 team, several drivers remain compile gated or placeholder (pH ADC path, pressure ADC path, IMU bus access, flow sensor EXTI line). The load cell (HX711), pump (PWM), networking and protobuf pipeline are implemented in firmware; end to end validation on the assembled board was not completed.</span>

## Hardware Platform

<table id="bkmrk-hardware-platform-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Item

</th><th>Value

</th></tr><tr><td>Microcontroller

</td><td>STM32H753ZI (NUCLEO-H753ZI board), ARM Cortex-M7 (480 MHz capable, currently clocked at 64 MHz, see STM32CubeMX Sensor Configuration)

</td></tr><tr><td>Real-Time OS

</td><td>FreeRTOS with CMSIS-RTOS V2

</td></tr><tr><td>Ethernet

</td><td>LAN8742 PHY, RMII, LwIP stack (static IP, no DHCP)

</td></tr><tr><td>Serial logging

</td><td>ST-Link VCP (USART3, 115200 baud)

</td></tr><tr><td>FreeRTOS heap

</td><td>64 KB (configTOTAL\_HEAP\_SIZE = 65536)

</td></tr><tr><td>Message encoding

</td><td>nanopb (Protocol Buffers), definitions in the ERC-Protobufs submodule

</td></tr></tbody></table>

## Key Board Features

- Single main task polls all sensors in one loop (5 second interval)
- Network integration via UDP/Ethernet with protobuf envelopes (PBEnvelope)
- Inbound packet dispatcher for control signals (pump command handler actuates hardware)
- Real-time logging to UART (115200 baud) with uniform per-sensor status lines
- MAC address filtering for selective communication
- Static ARP entry for the destination board, re-added on link up
- LED status indicators (Green, Blue, Red toggled each loop)
- Heap monitoring with critical threshold alert (below 4096 bytes free pauses the loop)

## Integrated Sensors and Actuators

<table id="bkmrk-devicemodel-%2F-interf"><colgroup><col></col><col></col><col></col><col></col></colgroup><tbody><tr><th>Device

</th><th>Model / Interface

</th><th>Count

</th><th>Firmware status

</th></tr><tr><td>pH Sensor

</td><td>DFRobot SEN0161, analog ADC

</td><td>1

</td><td>Driver complete, ADC path compile gated (PH\_SENSOR\_USE\_ADC), poll returns RESULT\_ERR\_UNIMPLEMENTED until an ADC is enabled in CubeMX

</td></tr><tr><td>IMU

</td><td>TBD, I2C1 (PB8/PB9)

</td><td>1

</td><td>Data structure, math and validation helpers implemented; hardware poll is a placeholder

</td></tr><tr><td>Load Cell (Weight)

</td><td>HX711 24-bit ADC, GPIO bit-bang

</td><td>2

</td><td>Implemented, auto-tare on init, scale calibration API

</td></tr><tr><td>Pressure (FSR)

</td><td>Analog force sensing resistor pads, ADC

</td><td>2

</td><td>Driver complete, ADC path compile gated (PRESSURE\_USE\_ADC), poll returns RESULT\_ERR\_UNIMPLEMENTED until an ADC is enabled

</td></tr><tr><td>Flow Sensor

</td><td>FM-PS2216 (5.5 pulses/ml), GPIO EXTI pulse counting on PA4

</td><td>1

</td><td>Implemented; EXTI4 must still be enabled in CubeMX for pulses to be counted

</td></tr><tr><td>Water Pump

</td><td>Grothen 12 V DC mini peristaltic pump, single MOSFET on TIM3 CH3 PWM (PC8)

</td><td>1

</td><td>Implemented, open loop, unidirectional; health cross-checked against the flow sensor

</td></tr></tbody></table>

<span style="white-space: pre-wrap;">Check this out for compiling code and more about project structure- </span>[Project Structure](https://bookstack.roboteamtwente.nl/books/embedded-infastructure/chapter/project-structure "Project Structure")

## Related Pages

- [STM32CubeMX Sensor Configuration](https://bookstack.roboteamtwente.nl/books/pcbs/page/stm32cubemx-sensor-configuration "STM32CubeMX Sensor Configuration")
- [Sensor Board Utility Library](https://bookstack.roboteamtwente.nl/books/pcbs/page/sensor-basics-utility-library "Sensor Board Utility Library")
- [Architecture](https://bookstack.roboteamtwente.nl/books/pcbs/page/architecture "Architecture")
- [Configuration](https://bookstack.roboteamtwente.nl/books/pcbs/page/configuration "Configuration")
- [pH Sensor](https://bookstack.roboteamtwente.nl/books/pcbs/page/ph-sensor "pH Sensor")
- [Load Cell (Weight Sensor)](https://bookstack.roboteamtwente.nl/books/pcbs/page/load-cell "Load Cell (Weight Sensor)")
- [Pressure Sensor](https://bookstack.roboteamtwente.nl/books/pcbs/page/pressure-sensor "Pressure Sensor")
- [Testing](https://bookstack.roboteamtwente.nl/books/pcbs/page/testing "Testing")
- [Reference](https://bookstack.roboteamtwente.nl/books/pcbs/page/reference "Reference")

# STM32CubeMX Sensor Configuration

This page documents the current STM32CubeMX configuration for the Sensor Board firmware and explains how to extend it for the remaining sensor interfaces (ADC, EXTI, I2C device setup) in a way that is safe for code generation.

- **IOC File:**<span style="white-space: pre-wrap;"> components/sensor\_board/firmware/firmware.ioc</span>
- **Generated HAL Init Files:**<span style="white-space: pre-wrap;"> components/sensor\_board/firmware/Core/Src/</span>
- **Application Entry:**<span style="white-space: pre-wrap;"> src/sensor\_board/main.c (MainTask, entry set to "As external" in FreeRTOS tab)</span>

## Current CubeMX Snapshot

<table id="bkmrk-cubemx-snapshot-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Item

</th><th>Value

</th></tr><tr><td>MCU

</td><td>STM32H753ZITx (NUCLEO-H753ZI)

</td></tr><tr><td>CubeMX Version

</td><td>6.15.0

</td></tr><tr><td>STM32Cube FW Package

</td><td>STM32Cube FW\_H7 V1.12.1

</td></tr><tr><td>Build system

</td><td>PlatformIO (env:sensor\_board), stm32cube framework

</td></tr></tbody></table>

### Enabled CubeMX Components

- CORTEX\_M7 (I-Cache and D-Cache enabled, MPU configured)
- DMA (DMA1 Stream 0)
- ETH (RMII mode)
- FREERTOS (CMSIS-RTOS V2, defaultTask plus mainTask)
- I2C1 (fast mode, for the IMU on PB8/PB9)
- LWIP (static IP, DHCP disabled, static ARP entries enabled)
- TIM3 (PWM Generation CH3, pump MOSFET gate)
- TIM7 (base timer)
- USART1 (asynchronous)
- SYS/NVIC/RCC base platform configuration (HAL timebase on TIM6)

## Clock and Core Setup

### Clock Configuration (from IOC)

<table id="bkmrk-clock-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Parameter

</th><th>Value

</th></tr><tr><td>HSE crystal value

</td><td>8 MHz

</td></tr><tr><td>SYSCLK

</td><td>64 MHz

</td></tr><tr><td>APB1

</td><td>64 MHz

</td></tr></tbody></table>

**Note:**<span style="white-space: pre-wrap;"> the STM32H753 is capable of 480 MHz, but the current IOC runs the core at 64 MHz. Raising the clock requires PLL configuration in the Clock Configuration tab and re-validation of the Ethernet and FreeRTOS timing.</span>

### Cortex-M7 / MPU

- Instruction cache: enabled
- Data cache: enabled
- MPU region at 0x30000000, size 32 KB, full access, TEX level 1 (non-cacheable region for the Ethernet DMA descriptors and LwIP heap)
- LwIP RAM heap pointer: 0x30004900, MEM\_SIZE 16 KB

## Pinout and Peripheral Mapping

### Ethernet (RMII)

<table id="bkmrk-eth-pins-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Signal

</th><th>Pin

</th></tr><tr><td>ETH\_REF\_CLK

</td><td>PA1

</td></tr><tr><td>ETH\_MDIO

</td><td>PA2

</td></tr><tr><td>ETH\_CRS\_DV

</td><td>PA7

</td></tr><tr><td>ETH\_MDC

</td><td>PC1

</td></tr><tr><td>ETH\_RXD0

</td><td>PC4

</td></tr><tr><td>ETH\_RXD1

</td><td>PC5

</td></tr><tr><td>ETH\_TX\_EN

</td><td>PG11

</td></tr><tr><td>ETH\_TXD0

</td><td>PB12

</td></tr><tr><td>ETH\_TXD1

</td><td>PB13

</td></tr></tbody></table>

### Sensor and Actuator Pins (labeled in IOC)

<table id="bkmrk-sensor-pins-table"><colgroup><col></col><col></col><col></col><col></col></colgroup><tbody><tr><th>Label

</th><th>Pin

</th><th>Mode

</th><th>Used by

</th></tr><tr><td>FLOW\_SENSOR

</td><td>PA4

</td><td>GPIO Input (EXTI4 not yet enabled)

</td><td>Flow sensor pulse counting

</td></tr><tr><td>WEIGHT\_INPUT\_1

</td><td>PA5

</td><td>GPIO Input

</td><td>HX711 unit 0 DOUT

</td></tr><tr><td>WEIGHT\_INPUT\_2

</td><td>PA6

</td><td>GPIO Input

</td><td>HX711 unit 1 DOUT

</td></tr><tr><td>WEIGHT\_CLOCK\_1

</td><td>PC7

</td><td>GPIO Output

</td><td>HX711 unit 0 SCK

</td></tr><tr><td>WEIGHT\_CLOCK\_2

</td><td>PB5

</td><td>GPIO Output

</td><td>HX711 unit 1 SCK

</td></tr><tr><td>IMU\_I2C\_Clock

</td><td>PB8

</td><td>I2C1\_SCL (pull-up)

</td><td>IMU

</td></tr><tr><td>IMU\_I2C\_Data

</td><td>PB9

</td><td>I2C1\_SDA (pull-up)

</td><td>IMU

</td></tr><tr><td>WATER\_PUMP\_PWM

</td><td>PC8

</td><td>TIM3\_CH3 (PWM)

</td><td>Pump MOSFET gate

</td></tr><tr><td>PH\_ANALOG\_DATA

</td><td>PD14

</td><td>GPIO\_Analog

</td><td>pH sensor (see warning below)

</td></tr><tr><td>FORCE\_ANALOG\_DATA\_1

</td><td>PD15

</td><td>GPIO\_Analog

</td><td>Pressure/FSR sensor 0 (see warning below)

</td></tr><tr><td>FORCE\_ANALOG\_DATA\_2

</td><td>PF3

</td><td>GPIO\_Analog

</td><td>Pressure/FSR sensor 1 (PF3 = ADC3\_INP5)

</td></tr><tr><td>STEPPER\_MOTOR\_1..4

</td><td>PD7, PD6, PD5, PD4

</td><td>GPIO Output

</td><td>Reserved for the sampling stepper

</td></tr></tbody></table>

**Warning:**<span style="white-space: pre-wrap;"> on the STM32H753, PD14 and PD15 have NO ADC function. The pH analog input and FORCE\_ANALOG\_DATA\_1 must be moved to ADC-capable pins (for example PA0, PC0, or PF3-class pins) before the analog drivers can be enabled. PF3 maps to ADC3\_INP5 and can be used as is.</span>

### Serial / COM

<table id="bkmrk-serial-pins-table"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Signal

</th><th>Pin

</th><th>Note

</th></tr><tr><td>USART1\_TX / USART1\_RX

</td><td>PA9 / PA10

</td><td>Spare asynchronous UART

</td></tr><tr><td>USART3\_TX / USART3\_RX

</td><td>PD8 / PD9

</td><td>NUCLEO ST-Link VCP path, used by the logging system (hcom\_uart\[COM1\])

</td></tr></tbody></table>

## RTOS Tasks and Interrupts

### FreeRTOS Tasks (CMSIS V2)

<table id="bkmrk-tasks-table"><colgroup><col></col><col></col><col></col><col></col></colgroup><tbody><tr><th>Task

</th><th>Priority

</th><th>Stack (words)

</th><th>Entry

</th></tr><tr><td>defaultTask

</td><td>24

</td><td>128

</td><td>StartDefaultTask (generated)

</td></tr><tr><td>mainTask

</td><td>39

</td><td>2048

</td><td>MainTask, "As external" (defined in src/sensor\_board/main.c)

</td></tr></tbody></table>

FreeRTOS heap: configTOTAL\_HEAP\_SIZE = 65536 (64 KB).

### Interrupt Priorities (key entries)

<table id="bkmrk-nvic-table"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>IRQ

</th><th>Priority

</th><th>Notes

</th></tr><tr><td>ETH\_IRQn

</td><td>5

</td><td>Ethernet / LwIP path

</td></tr><tr><td>DMA1\_Stream0\_IRQn

</td><td>5

</td><td>DMA

</td></tr><tr><td>USART1\_IRQn

</td><td>5

</td><td>Spare UART

</td></tr><tr><td>EXTI15\_10\_IRQn

</td><td>5

</td><td>External interrupt group (user button)

</td></tr><tr><td>TIM6\_DAC\_IRQn

</td><td>15

</td><td>HAL tick timebase (TIM6)

</td></tr><tr><td>SysTick / PendSV

</td><td>15

</td><td>FreeRTOS kernel

</td></tr></tbody></table>

## Sensor Interface Status

### What Is Already Configured in CubeMX

- Networking stack (LwIP) and RMII pinout
- FreeRTOS scaffolding with the external MainTask
- I2C1 bus for the IMU with pull-ups on PB8/PB9
- TIM3 CH3 PWM output for the pump
- GPIO pins and labels for the HX711 load cells and the flow sensor

### What Is Not Yet Modeled in CubeMX (TO DO once sensors are retrieved and assembled)

- ADC peripherals and channels for the analog sensors (pH, pressure/FSR); PD14 and PD15 must also be moved to ADC-capable pins
- EXTI4 rising-edge interrupt for the flow sensor pin PA4 (until enabled, flow always reads 0)
- IMU device bring-up on I2C1 (the poll function is a placeholder)

**Important:**<span style="white-space: pre-wrap;"> the analog sensor drivers are compile gated (PH\_SENSOR\_USE\_ADC, PRESSURE\_USE\_ADC) so the firmware links without hardware. When bringing up physical sensors, add the corresponding CubeMX peripherals first, then enable the build flags and bind the generated handles.</span>

## Recommended Workflow

1. Open components/sensor\_board/firmware/firmware.ioc in STM32CubeMX.
2. Add the required peripheral for the target sensor (ADC channel, EXTI line, or I2C settings).
3. Assign and lock pins in the Pinout view; avoid overlap with RMII and COM pins.
4. Configure clocks for new peripherals in Clock Configuration.
5. Set NVIC priorities for new ISR sources so Ethernet and RTOS timing remain stable.
6. Generate code with Keep User Code enabled.
7. Rebuild using PlatformIO and validate startup and sensor polling.

### Conflicts To Check Before Saving the .ioc File

- No conflict with ETH RMII pins (PA1, PA2, PA7, PC1, PC4, PC5, PG11, PB12, PB13)
- No conflict with the debug/COM path (PA9/PA10 and PD8/PD9)
- No conflict with oscillator pins (PH0, PH1, PC14, PC15)
- No conflict with the existing sensor labels (PA4, PA5, PA6, PB5, PB8, PB9, PC7, PC8, PD4..PD7, PD14, PD15, PF3)

## Related Pages

- [Configuration](https://bookstack.roboteamtwente.nl/books/pcbs/page/configuration "Configuration")
- [Reference](https://bookstack.roboteamtwente.nl/books/pcbs/page/reference "Reference")
- [Sensor Board Utility Library](https://bookstack.roboteamtwente.nl/books/pcbs/page/sensor-basics-utility-library "Sensor Board Utility Library")

# Sensor Basics Utility Library

The Sensor Basics utility library provides small validation and conversion helpers shared by the sensor drivers and the main application. These are basic features that could be used if required, but were made in "spare time".

## Source Code Location

**Files:**

- `<span class="editor-theme-code">components/sensor_board/sensor_basics/sensor_basics.h</span>`<span style="white-space: pre-wrap;"> (function declarations and documentation)</span>
- `<span class="editor-theme-code">components/sensor_board/sensor_basics/sensor_basics.c</span>`<span style="white-space: pre-wrap;"> (implementation)</span>

**Dependencies:**

- `<span class="editor-theme-code">result.h</span>`<span style="white-space: pre-wrap;"> (standard result/error code definitions and the TRY macro)</span>
- `<span class="editor-theme-code">stdint.h</span>`<span style="white-space: pre-wrap;"> (integer type definitions)</span>

## pH Validation

### validate\_ph\_value()

Validates that a pH value is within the acceptable range (0 to 14).

```c
result_t validate_ph_value(float ph_value) {
    if (ph_value >= 0.0f && ph_value <= 14.0f) {
        return RESULT_OK;
    }
    return RESULT_ERR_INVALID_DATA;
}
```

**Return values:**<span style="white-space: pre-wrap;"> RESULT\_OK when the value is within range, RESULT\_ERR\_INVALID\_DATA otherwise.</span>

**Note:**<span style="white-space: pre-wrap;"> ph\_sensor\_update() already clamps its output to 0..14, so this check only fails for values that bypass the driver (for example raw values received over the network).</span>

## IMU (Accelerometer) Validation

### validate\_accelerometer\_value()

Validates one accelerometer axis. Valid range is -160.0 to +160.0 m/s², which corresponds to a typical ±16 g sensor range.

```c
result_t validate_accelerometer_value(float accel_value) {
    if (accel_value >= -160.0f && accel_value <= 160.0f) {
        return RESULT_OK;
    }
    return RESULT_ERR_INVALID_DATA;
}
```

### validate\_imu\_data()

Validates all three accelerometer axes at once using the TRY macro for early return on the first invalid axis.

```c
result_t validate_imu_data(float accel_x, float accel_y, float accel_z) {
    TRY(validate_accelerometer_value(accel_x));
    TRY(validate_accelerometer_value(accel_y));
    TRY(validate_accelerometer_value(accel_z));
    return RESULT_OK;
}
```

**Note:**<span style="white-space: pre-wrap;"> the IMU driver has its own richer validators (imu\_validate\_accelerometer\_range, imu\_validate\_gyroscope\_range, imu\_validate\_magnetometer\_range) which the main loop uses. See the imu component.</span>

## Conversion Functions (declared, currently inactive)

The header declares four unit conversion helpers. Their implementations exist in sensor\_basics.c but are commented out, so linking against them fails until they are re-enabled.

```c
/* Temperature: F = C * 9/5 + 32,  C = (F - 32) * 5/9 */
result_t celsius_to_fahrenheit(float celsius, float *fahrenheit);
result_t fahrenheit_to_celsius(float fahrenheit, float *celsius);

/* Pressure: psi = bar * 14.5038,  bar = psi / 14.5038 */
result_t bar_to_psi(float bar, float *psi);
result_t psi_to_bar(float psi, float *bar);
```

Each returns RESULT\_OK on success or RESULT\_ERR\_INVALID\_ARG when the output pointer is NULL.

## Implementation Status

**Currently implemented (active):**

- validate\_ph\_value()
- validate\_accelerometer\_value()
- validate\_imu\_data()

**Currently commented out (inactive):**

- celsius\_to\_fahrenheit()
- fahrenheit\_to\_celsius()
- bar\_to\_psi()
- psi\_to\_bar()

**Note:**<span style="white-space: pre-wrap;"> earlier drafts of this page documented GPS validation helpers (latitude, longitude, HDOP, satellite count). These functions do not exist in the current library since there is no GPS driver on the sensor board.</span>

## Error Handling Pattern

All validation functions follow the same pattern used across the firmware:

```c
if (validate_ph_value(ph_value) == RESULT_OK) {
    diagnostics.ph_sensor.state = SensorState_SENSOR_OPERATING;
    diagnostics.ph_sensor.error_code = PHErrorCode_PH_NO_ERROR;
} else {
    diagnostics.ph_sensor.state = SensorState_SENSOR_ERROR;
    diagnostics.ph_sensor.error_code = PHErrorCode_PH_INVALID_DATA;
}
```

## Testing

<span style="white-space: pre-wrap;">Test suite location: </span>`<span class="editor-theme-code">test/sensor_board/test_sensor_basics/</span>`

```bash
# Run only the sensor_basics tests
pio test -e sensor_board -f test_sensor_basics

# Run with verbose output
pio test -e sensor_board -f test_sensor_basics -v
```

Current coverage: accelerometer boundary values (±160.0 accepted, ±160.1 rejected) and multi-axis combination. The temperature and pressure conversion tests exist in the file but are commented out together with their implementations.

# Architecture

Complete system overview: FreeRTOS task model, the sensor polling loop, protobuf encoding, UDP transmission, inbound packet dispatch, and the memory layout. All application logic lives in a single FreeRTOS task (MainTask) defined in src/sensor\_board/main.c.

## Initialization Sequence

### Phase 1: Hardware Setup (init\_board, before the kernel starts)

```c
void init_board() {
  MPU_Config_wrapper();
  SCB_EnableICache();
  SCB_EnableDCache();
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  /* NOTE: no threads here, kernel not initialized yet.
   * osKernelInitialize() is called by cubemx_main.c afterwards. */
}
```

### Phase 2: Driver Initialization (start of MainTask)

1. BSP LEDs (Green, Blue, Red)
2. Logging over the ST-Link VCP UART (LOG\_init(&amp;hcom\_uart\[COM1\]), 115200 baud)
3. IMU (imu\_sensor\_init)
4. pH sensor (ph\_sensor\_init with 3.3 V reference)
5. Two HX711 load cells with their GPIO map (PA5/PC7 and PA6/PB5), each powered up and auto-tared
6. Two pressure/FSR sensors (pressure\_sensor\_init)
7. Flow sensor (flow\_sensor\_init, pulse counting via EXTI callback)
8. Pump (pump\_init on TIM3 CH3 PWM, then commanded to 50 percent and enabled as a startup default)

### Phase 3: Communication Setup

1. ETH\_init with static IP 192.168.0.111, netmask 255.255.255.0, gateway 192.168.0.1 and a link status callback that re-adds the static ARP entry when the link comes up
2. MAC address filtering for three allowed source MACs (ETH\_setup\_MAC\_address\_filtering)
3. Two statically allocated prioritised UDP transmit queues (80 entries each)
4. Packet dispatcher registration for five inbound message types (pH, IMU, load cell, pressure, pump)
5. ETH\_udp\_init(2, send\_queues, DispatchPacket) and a static ARP entry for the destination board 192.168.0.222

### Phase 4: Main Loop

The loop runs forever with a 5000 ms period (MAIN\_TASK\_DELAY\_MS). Each iteration:

1. Read free heap; if below 4096 bytes log CRITICAL and sleep 10 s instead of polling
2. Toggle the three LEDs (visual heartbeat)
3. Build a SensorBoardDiagnostics struct (state OPERATING)
4. Poll pH, IMU, then load cells and pressure sensors (index loop over both units)
5. Poll the flow sensor (rate computed from pulses counted by the EXTI ISR since the last poll)
6. Build the pump status from commanded state, cross-checked against measured flow
7. Wrap each sensor message in a PBEnvelope and send it as a UDP datagram to the sample board
8. osDelay(MAIN\_TASK\_DELAY\_MS)

## Protobuf Encoding and UDP Transmission

Every outbound message is one PBEnvelope with a oneof payload. Encoding uses nanopb via pb\_message\_encode, which allocates a heap buffer that is freed after sending.

```c
static void udp_send_envelope(uint8_t dest_ip[4], PBEnvelope *env) {
  if (!sendUDP) { return; }   /* transmit gate, false by default */
  uint8_t *encoded = NULL;
  size_t size = 0;
  result_t result = pb_message_encode(env, PBEnvelope_fields, &encoded, &size);
  if (result == RESULT_OK) {
    ETH_udp_send(dest_ip, PORT, encoded, (uint16_t)size, 1);
  }
  free(encoded);
}
```

**Important:**<span style="white-space: pre-wrap;"> the static flag </span>`<span class="editor-theme-code">sendUDP</span>`<span style="white-space: pre-wrap;"> in main.c is currently </span>`<span class="editor-theme-code">false</span>`<span style="white-space: pre-wrap;">, so envelope encoding and transmission are skipped entirely. Set it to true to actually transmit. There is a similar development flag </span>`<span class="editor-theme-code">skip_sensor_polling</span>`<span style="white-space: pre-wrap;"> (currently false) that disables all sensor polling when true.</span>

## Inbound Packet Dispatcher

Received UDP packets are decoded by the shared packet dispatcher (components/common/packet\_dispatcher). Handlers are registered with PACKET\_HANDLER\_CONFIG\_STATIC per envelope payload tag:

<table id="bkmrk-dispatcher-table"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Envelope tag

</th><th>Handler

</th><th>Behavior

</th></tr><tr><td>ph\_info

</td><td>handle\_sensor\_ph\_info

</td><td>Log only

</td></tr><tr><td>imu\_info

</td><td>handle\_sensor\_imu\_info

</td><td>Log only

</td></tr><tr><td>load\_cell\_info

</td><td>handle\_sensor\_load\_cell\_info

</td><td>Log only

</td></tr><tr><td>pressure\_info

</td><td>handle\_sensor\_pressure\_info

</td><td>Log only

</td></tr><tr><td>pump\_info

</td><td>handle\_sensor\_pump\_command

</td><td>Actuates the pump: applies enabled, direction, and speed\_percent to the hardware

</td></tr></tbody></table>

## Sensor Status Model

### SensorState (operating state)

<table id="bkmrk-state-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Code

</th><th>Meaning

</th></tr><tr><td>SENSOR\_IDLE

</td><td>Not connected, not implemented, or intentionally off

</td></tr><tr><td>SENSOR\_OPERATING

</td><td>Normal operation, valid data

</td></tr><tr><td>SENSOR\_ERROR

</td><td>Communication failure or invalid data

</td></tr></tbody></table>

### SensorStatus (connection status)

<table id="bkmrk-status-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Code

</th><th>Meaning

</th></tr><tr><td>STATUS\_OK

</td><td>Healthy

</td></tr><tr><td>STATUS\_DISCONNECTED

</td><td>No hardware detected (poll returned UNIMPLEMENTED or COMMS)

</td></tr><tr><td>STATUS\_ERROR

</td><td>Unexpected failure

</td></tr><tr><td>STATUS\_INITIALIZING

</td><td>Warming up (flow sensor first sample window)

</td></tr></tbody></table>

### Poll Result Mapping

handle\_sensor\_poll\_result() maps driver results uniformly:

- RESULT\_ERR\_UNIMPLEMENTED or RESULT\_ERR\_COMMS: state IDLE, status DISCONNECTED (sensor not connected or driver not wired to hardware yet)
- Any other non-OK result: state ERROR, status ERROR
- RESULT\_OK: caller then validates the data and picks OPERATING or ERROR plus a driver-specific error code

<span style="white-space: pre-wrap;">Every sensor produces one uniform log line per loop: </span>`<span class="editor-theme-code">name | STATUS | STATE | detail</span>`.

## Pump Health Cross-Check

The pump is an open loop actuator (no current sense or fault line), so firmware cannot directly detect a connected pump. The only on-board proof that fluid is moving is the inline flow sensor, so the main loop derives pump status from it:

- Not initialised: ERROR / ERROR
- Commanded off (disabled or 0 percent): IDLE / OK (healthy, intentionally off)
- Commanded on and flow detected: OPERATING / OK
- Commanded on and no flow: OPERATING / DISCONNECTED (pump absent, dry, stalled, or the flow sensor is not installed)

## Memory Layout

<table id="bkmrk-memory-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Region

</th><th>Use

</th></tr><tr><td>FreeRTOS heap, 64 KB

</td><td>Task stacks, queues, protobuf encode buffers (malloc/free per message)

</td></tr><tr><td>0x30000000, 32 KB, MPU non-cacheable

</td><td>Ethernet DMA descriptors and buffers

</td></tr><tr><td>0x30004900, 16 KB

</td><td>LwIP RAM heap (MEM\_SIZE)

</td></tr><tr><td>Static queues

</td><td>Two UDP send queues, 80 entries each, allocated at compile time (xQueueCreateStatic)

</td></tr></tbody></table>

## Error Handling Strategy

- Drivers never crash the loop: missing hardware degrades to IDLE/DISCONNECTED and the loop continues
- Each sensor unit reports independently (separate envelope, separate error code enum)
- Heap exhaustion protection: below 4096 bytes free, polling pauses for 10 s per iteration
- Encode failures are logged with result\_to\_short\_str / result\_to\_desc\_str and the buffer is freed in all paths

# Configuration

Compile-time parameters, runtime flags, sensor calibration setup, network addressing, and performance tuning options for the sensor board firmware.

## Main Loop Timing

```c
/* src/sensor_board/main.c */
#define MAIN_TASK_DELAY_MS 5000   /* poll + transmit interval */
```

All sensors are polled and transmitted once per interval. Lowering this value increases network traffic and heap churn (one encode allocation per message).

## Runtime Flags (main.c)

<table id="bkmrk-flags-table"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Flag

</th><th>Default

</th><th>Effect

</th></tr><tr><td>sendUDP

</td><td>false

</td><td>When false, udp\_send\_envelope() returns immediately: nothing is encoded or transmitted. Set to true to enable network output.

</td></tr><tr><td>skip\_sensor\_polling

</td><td>false

</td><td>When true, the pH/IMU/load cell/pressure polling block is skipped (flow and pump still run).

</td></tr></tbody></table>

## Heap Management

```c
uint32_t free_heap = xPortGetFreeHeapSize();
if (free_heap < 4096U) {   /* critical threshold, was 8192U */
    LOGE(TAG, "CRITICAL: Low heap! Free: %lu bytes", free_heap);
    osDelay(10000);
    continue;
}
```

- FreeRTOS heap size: 65536 bytes (configTOTAL\_HEAP\_SIZE, set in CubeMX)
- Critical threshold: 4096 bytes free
- LwIP heap: 16 KB at 0x30004900 (separate from the FreeRTOS heap)

## Network Configuration

<span style="white-space: pre-wrap;">All addresses live in </span>`<span class="editor-theme-code">components/common/networking_constants/ip_mac_constants.h</span>`:

```c
#define NETWORK_IP       {192, 168, 0, 111}                     /* this board */
#define NETWORK_MAC      {0x00, 0x80, 0xe1, 0x00, 0x00, 0x00}

#define SAMPLE_BOARD_IP  {192, 168, 0, 222}                     /* UDP destination */
#define SAMPEL_BOARD_MAC {0x00, 0x43, 0x23, 0xee, 0x21, 0x64}

#define GATEWAY          {192, 168, 0, 1}
#define NETMASK          {255, 255, 255, 0}

#define PORT 1500                                               /* UDP port */
```

### Changing the Destination Address or Port

1. Edit SAMPLE\_BOARD\_IP / SAMPEL\_BOARD\_MAC (used both for sending and for the static ARP entry) or PORT in ip\_mac\_constants.h.
2. Rebuild; no other file references the raw addresses.

### MAC Address Filtering

Three source MACs are allowed through the hardware filter, configured in MainTask:

```c
int mac1[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
int mac2[6] = {0x12, 0x23, 0x34, 0x45, 0x56, 0x67};
int mac3[6] = {0x90, 0x2e, 0x16, 0xbe, 0x1b, 0x33};
ETH_setup_MAC_address_filtering(mac1, mac2, mac3);
```

### UDP Transmit Queues

```c
#define SENSOR_SEND_QUEUE_SIZE 80   /* entries per priority queue, x2 queues */
```

## Sensor Hardware Build Flags

The analog drivers are compile gated so the firmware links without ADC hardware. Add these to build\_flags in platformio.ini once the corresponding CubeMX peripherals exist:

<table id="bkmrk-build-flags-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Flag

</th><th>Effect

</th></tr><tr><td>-D PH\_SENSOR\_USE\_ADC

</td><td>Enables the pH ADC read path (optional: -D PH\_SENSOR\_ADC\_HANDLE=hadc1, -D PH\_SENSOR\_ADC\_MAX=65535, -D PH\_SENSOR\_ADC\_TIMEOUT\_MS=100)

</td></tr><tr><td>-D PRESSURE\_USE\_ADC

</td><td>Enables the pressure/FSR ADC read path; bind each unit with pressure\_sensor\_init\_hw()

</td></tr><tr><td>-D PUMP\_MAX\_RPM\_EST=100

</td><td>Override the pump RPM estimate used when converting duty cycle to speed\_rpm

</td></tr><tr><td>-D CONFIG\_LOG\_LEVEL=LOG\_INFO

</td><td>Log verbosity (already set in platformio.ini)

</td></tr></tbody></table>

## Sensor Calibration Configuration

### pH Sensor

```c
/* components/sensor_board/ph/ph_sensor.h */
#define PH_SAMPLE_COUNT   40     /* averaging buffer size */
#define PH_DEFAULT_SLOPE  3.5f   /* SEN0161 default: pH = 3.5 * V + offset */

/* runtime calibration */
ph_sensor_calibrate(&ph_sensor, offset, slope);
ph_sensor_reset_calibration(&ph_sensor);   /* offset 0, slope 3.5 */
```

### Load Cell (HX711)

```c
/* components/sensor_board/load_cell/load_cell_sensor.h */
#define LOAD_CELL_DEFAULT_N_PER_COUNT 1.0f  /* passthrough until calibrated */
#define LOAD_CELL_GAIN_PULSES         1U    /* 1 = ch A gain 128 */
#define LOAD_CELL_READY_TIMEOUT_MS    200U  /* wait for DOUT low */

/* runtime calibration */
load_cell_tare(&cell, 10);                 /* average 10 reads as zero */
load_cell_set_scale(&cell, n_per_count);   /* marks is_calibrated = true */
```

### Pressure / FSR

```c
/* kPa = voltage * scale_kpa_per_volt + offset_kpa */
pressure_sensor_set_calibration(&sensor, scale_kpa_per_volt, offset_kpa);
```

### Flow Sensor

```c
/* components/sensor_board/sampling/flow_sensor/flow_sensor.h */
#define FLOW_SENSOR_PULSES_PER_ML_X10 55U    /* FM-PS2216: 5.5 pulses/ml */
#define FLOW_SENSOR_SAMPLE_WINDOW_MS  1000U  /* rate calculation window */
#define FLOW_SENSOR_MAX_FLOW_ML_MIN   150U   /* plausibility clamp */
```

## Changing the Sensor Poll Interval

1. Edit MAIN\_TASK\_DELAY\_MS in src/sensor\_board/main.c.
2. Keep it well above the slowest blocking read (HX711 ready wait can take up to 200 ms per cell).
3. The flow rate is computed per poll from the pulse count, so the interval also sets flow rate resolution.

## Enabling / Disabling Sensors

- All sensor polling: skip\_sensor\_polling flag in main.c (flow and pump are outside this block)
- Individual analog sensors: leave their build flag unset; the driver returns RESULT\_ERR\_UNIMPLEMENTED and the sensor reports IDLE / DISCONNECTED without affecting the rest of the loop

# pH Sensor

The pH sensor provides water quality measurement critical for environmental monitoring and anomaly detection.

### Hardware Specifications

<table id="bkmrk-ph-hw-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Parameter

</th><th>Value

</th></tr><tr><td>Model

</td><td>DFRobot SEN0161 (Analog pH meter)

</td></tr><tr><td>Interface

</td><td>Analog ADC (compile gated, see Hardware Status below)

</td></tr><tr><td>Reference Voltage

</td><td>Configurable at init (3.3 V used in main.c; SEN0161 itself prefers a stable 5.0 V supply)

</td></tr><tr><td>Board pin

</td><td>PD14, labeled PH\_ANALOG\_DATA (must be moved, PD14 has no ADC function)

</td></tr><tr><td>Measurement Range

</td><td>0 to 14 pH units (clamped in software)

</td></tr><tr><td>Accuracy

</td><td>±0.1 pH @ 25°C (sensor datasheet)

</td></tr><tr><td>Sample Averaging

</td><td>40 samples, min and max excluded

</td></tr></tbody></table>

## Hardware Status

<span style="white-space: pre-wrap;">No ADC is enabled in CubeMX yet, so the ADC path is compile gated by </span>`<span class="editor-theme-code">PH_SENSOR_USE_ADC</span>`<span style="white-space: pre-wrap;">. Without that flag, </span>`<span class="editor-theme-code">poll_ph_sensor()</span>`<span style="white-space: pre-wrap;"> returns RESULT\_ERR\_UNIMPLEMENTED and the sensor reports IDLE / DISCONNECTED (the firmware still links and runs). To enable:</span>

1. In CubeMX enable an ADC (for example ADC1) and a channel on the pH input pin. PD14 (the current PH\_ANALOG\_DATA label) has NO ADC function on the STM32H753, so move the pH input to an ADC-capable pin (PA0, PC0, ...).
2. The SEN0161 is a 5 V board with output up to about 3 V. The STM32 ADC tops out at 3.3 V, so power or scale the board so its output never exceeds 3.3 V.
3. <span style="white-space: pre-wrap;">Build with </span>`<span class="editor-theme-code">-D PH_SENSOR_USE_ADC</span>`<span style="white-space: pre-wrap;"> (optionally -D PH\_SENSOR\_ADC\_HANDLE=hadc1, -D PH\_SENSOR\_ADC\_MAX=65535).</span>

## Calibration Model

The sensor uses linear voltage-to-pH conversion (DFRobot SEN0161 formula):

```
pH = slope × Voltage + offset
Voltage = averaged_ADC / adc_max × reference_voltage
```

**Default Parameters for SEN0161**<span style="white-space: pre-wrap;"> @ 25°C:</span>

- **Slope**: 3.5 (PH\_DEFAULT\_SLOPE)
- **Offset**: 0.0 by default, set via user calibration

The computed pH is clamped to the 0 to 14 range inside ph\_sensor\_update().

## Data Structure

```c
typedef struct {
    uint16_t raw_value;              /* averaged raw ADC value */
    float voltage;                   /* converted voltage */
    float ph_value;                  /* calculated pH (0-14), init 7.0 */
    float reference_voltage;         /* ADC reference */
    ph_calibration_t calibration;    /* { offset: float, slope: float } */
    uint16_t sample_buffer[40];      /* last 40 samples (PH_SAMPLE_COUNT) */
    uint8_t sample_index;            /* current position in buffer */
    uint8_t samples_collected;       /* samples collected so far (0-40) */
} ph_sensor_t;
```

## Initialization &amp; Usage

### Initialize pH Sensor

```c
ph_sensor_t ph_sensor;
ph_sensor_init(&ph_sensor, 3.3f);  /* 3.3 V reference, as in main.c */
```

### Poll pH Sensor

```c
result_t ph_result = poll_ph_sensor(&ph_sensor);

if (ph_result == RESULT_OK) {
    float ph_value, voltage;
    ph_sensor_get_value(&ph_sensor, &ph_value);
    ph_sensor_get_voltage(&ph_sensor, &voltage);
}
/* RESULT_ERR_UNIMPLEMENTED: ADC path not compiled in
 * RESULT_ERR_COMMS: HAL ADC start/conversion failed */
```

### Manual Sample Addition

```c
/* For manual sampling at regular intervals (e.g. every 20 ms) */
uint16_t adc_reading = 2048;
ph_sensor_add_sample(&ph_sensor, adc_reading);

/* Or feed a reading through the full pipeline (average + convert) */
ph_sensor_update(&ph_sensor, adc_reading, 4095);
```

## Validation

```c
result_t validate_ph_value(float ph_value);
/* Returns RESULT_OK if 0 <= ph_value <= 14
 * Returns RESULT_ERR_INVALID_DATA otherwise */
```

## Sample Averaging Strategy

<table id="bkmrk-ph-averaging-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Parameter

</th><th>Value

</th></tr><tr><td>Sample Buffer Size

</td><td>40 samples (PH\_SAMPLE\_COUNT)

</td></tr><tr><td>Method

</td><td>Circular buffer average, minimum and maximum values excluded (DFRobot sample code algorithm); simple average while fewer than 5 samples collected

</td></tr><tr><td>Purpose

</td><td>Noise filtering and stable readings

</td></tr></tbody></table>

### Averaging Algorithm

```
1. ADC sample added to circular buffer
2. Buffer averaged with min and max excluded
3. Averaged value converted to voltage
4. Voltage converted to pH via calibration (slope, offset)
5. pH clamped to 0-14
```

## Two-Point Calibration Procedure

### Step 1: Neutral Point (pH 7.0)

```
1. Short the BNC input or immerse the electrode in pH 7.0 buffer solution
2. Wait for a stable reading (~2 minutes)
3. Record the reported pH value
4. offset = 7.0 - recorded_value
```

### Step 2: Slope Calibration (pH 4.0 or 10.0)

```
1. Immerse the electrode in a second known pH solution (pH 4.0 buffer)
2. Wait for a stable reading
3. Adjust the board gain potentiometer until the reading is 4.0,
   or compute: slope = (pH_reference - 7.0) / (V_reference - V_neutral)
4. Apply with ph_sensor_calibrate(&sensor, offset, slope)
```

## Protobuf Message Format

```protobuf
message SensorBoardPHInfo {
    float ph_value;
    float voltage;
    SensorState state;
    PHErrorCode error_code;
}

enum PHErrorCode {
    PH_NO_ERROR = 0;
    PH_COMMUNICATION_FAILURE = 1;
    PH_INVALID_DATA = 2;
}
```

## Error Handling (as in main.c)

```c
if (ph_result == RESULT_ERR_UNIMPLEMENTED || ph_result == RESULT_ERR_COMMS) {
    /* Hardware not connected / ADC not enabled */
    diagnostics.ph_sensor.state = SensorState_SENSOR_IDLE;
    diagnostics.ph_sensor.error_code = PHErrorCode_PH_COMMUNICATION_FAILURE;
} else if (ph_result == RESULT_OK) {
    if (validate_ph_value(ph_value) == RESULT_OK) {
        diagnostics.ph_sensor.state = SensorState_SENSOR_OPERATING;
        diagnostics.ph_sensor.error_code = PHErrorCode_PH_NO_ERROR;
    } else {
        diagnostics.ph_sensor.state = SensorState_SENSOR_ERROR;
        diagnostics.ph_sensor.error_code = PHErrorCode_PH_INVALID_DATA;
    }
}
```

## Integration Notes

- Single sensor instance in the main application, initialized with a 3.3 V reference
- Updates transmitted to the network at the main loop interval (5 seconds default), when the sendUDP flag is enabled
- Temperature compensation not implemented (assumes ~25°C)
- Because ph\_sensor\_update() clamps to 0-14, validate\_ph\_value() cannot fail on driver output; it protects against values from other sources
- Electrode response time: ~100-300 ms depending on pH change magnitude
- Unit tested on host: initialization defaults, voltage conversion, clamping, calibration (test/sensor\_board/test\_ph\_sensor)

# IMU

The Inertial Measurement Unit (IMU) provides three-axis acceleration, angular velocity, and magnetic field measurements for attitude determination and motion analysis.

### Hardware

<table id="bkmrk-imu-hw-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Parameter

</th><th>Value

</th></tr><tr><td>Device

</td><td>Xsens Avior (Xbus protocol, MTi-1-series compatible pipe interface)

</td></tr><tr><td>Interface

</td><td>I2C1, pins PB8 (IMU\_I2C\_Clock) / PB9 (IMU\_I2C\_Data), internal pull-ups enabled

</td></tr><tr><td>I2C address

</td><td>0x6B (7-bit, MTi-1 series default, override with -D XSENS\_I2C\_ADDR\_7BIT)

</td></tr><tr><td>Output rate

</td><td>100 Hz requested for accel/gyro/mag (XSENS\_OUTPUT\_RATE\_HZ)

</td></tr><tr><td>I2C timeout

</td><td>100 ms (XSENS\_I2C\_TIMEOUT\_MS)

</td></tr></tbody></table>

**Important:**<span style="white-space: pre-wrap;"> the pipe opcodes, default I2C address, and Xbus data identifiers used by the driver are the documented Xsens MTi-1-series values. The Avior is Xbus-compatible, but confirm these against the Avior datasheet (mtidocs.xsens.com) and override with -D build flags if your unit differs. The driver was never validated against physical hardware.</span>

### Communication Protocol (Xbus over I2C)

Xbus messages move through "pipe" opcodes used as an 8-bit register address:

- 0x03 ControlPipe: write Xbus command messages
- 0x04 PipeStatus: read 4 bytes, notification size (LE16) and measurement size (LE16)
- 0x06 MeasurementPipe: read a pending MTData2 measurement message

```
Xbus frame: [0xFA][0xFF][MID][LEN][DATA...][CHK]
CHK makes (BID + MID + LEN + DATA + CHK) & 0xFF == 0
```

On the first poll the driver configures the device once: GoToConfig, SetOutputConfiguration (acceleration + rate of turn + magnetic field at 100 Hz, float32), GoToMeasure. If configuration fails, poll returns RESULT\_ERR\_COMMS (device not responding on I2C).

## Data Structure

```c
typedef struct {
    float accel[3];            /* [X, Y, Z] acceleration, m/s² */
    float gyro[3];             /* [X, Y, Z] angular velocity, °/s
                                  (converted from rad/s by the driver) */
    float mag[3];              /* [X, Y, Z] magnetic field, Xsens arbitrary
                                  units (~1.0 = local Earth field), NOT µT */
    uint32_t timestamp;        /* Current reading timestamp (HAL_GetTick ms) */
    uint32_t last_timestamp;   /* Previous reading timestamp */
} imu_data_t;
```

**Unit notes:**<span style="white-space: pre-wrap;"> the gyroscope values are converted from rad/s to °/s inside the driver. The Xsens magnetic field output is in arbitrary units where roughly 1.0 equals the local Earth field; it is stored as-is and must be scaled externally if µT are needed.</span>

## Initialization &amp; Usage

### Initialize IMU

```c
imu_data_t imu_data;
imu_sensor_init(&imu_data);   /* zeroes the structure */
```

### Poll IMU Data

```c
result_t imu_result = poll_imu_sensor(&imu_data);

if (imu_result == RESULT_OK) {
    /* Acceleration (m/s²) */
    float accel_x = imu_data.accel[0];
    float accel_y = imu_data.accel[1];
    float accel_z = imu_data.accel[2];

    /* Angular velocity (°/s) */
    float gyro_x = imu_data.gyro[0];
    float gyro_y = imu_data.gyro[1];
    float gyro_z = imu_data.gyro[2];

    /* Magnetic field (Xsens arbitrary units) */
    float mag_x = imu_data.mag[0];
    float mag_y = imu_data.mag[1];
    float mag_z = imu_data.mag[2];
}
/* RESULT_ERR_COMMS: device not responding, or no fresh sample ready yet */
```

## Advanced Functions

### Update with Raw Values

```c
result_t imu_sensor_update(
    imu_data_t *imu,
    float ax, float ay, float az,  /* Accelerometer values */
    float gx, float gy, float gz,  /* Gyroscope values */
    float mx, float my, float mz,  /* Magnetometer values */
    uint32_t timestamp
);
```

### Calculate Acceleration Magnitude

```c
float acceleration_magnitude = imu_get_acceleration_magnitude(&imu_data);
/* |a| = sqrt(ax² + ay² + az²), useful for impact and free-fall detection */
```

### Orientation Helpers (from the gravity vector)

```c
float pitch_deg = imu_get_pitch(&imu_data); /* atan2(ay, sqrt(ax²+az²)) in degrees */
float roll_deg  = imu_get_roll(&imu_data);  /* atan2(ax, sqrt(ay²+az²)) in degrees */
/* Assumes the device is relatively stationary */
```

### Gyroscope Drift Check

```c
/* true when all gyro axes are below the threshold (device at rest) */
bool stable = imu_check_gyroscope_drift(&imu_data, 1.0f);
```

### Copying Data for Another Context

```c
imu_data_t imu_copy;
imu_sensor_read(&imu_data, &imu_copy);
/* Plain struct copy, no locking: safe only if the source is not
 * being updated concurrently */
```

## Sensor Ranges Used by the Driver Validators

<table id="bkmrk-imu-ranges-table"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Measurement

</th><th>Accepted Range

</th><th>Units

</th></tr><tr><td>Acceleration

</td><td>±16 g (±156.9 m/s²)

</td><td>m/s²

</td></tr><tr><td>Angular Velocity

</td><td>±2000

</td><td>°/s

</td></tr><tr><td>Magnetic Field

</td><td>±4900

</td><td>driver limit (see unit note above)

</td></tr></tbody></table>

### Conversion Reference

<table id="bkmrk-imu-conv-table"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>From

</th><th>To

</th><th>Factor

</th></tr><tr><td>g

</td><td>m/s²

</td><td>× 9.80665

</td></tr><tr><td>rad/s

</td><td>°/s

</td><td>× 180/π (applied inside the driver)

</td></tr><tr><td>Gauss

</td><td>µT

</td><td>× 100

</td></tr></tbody></table>

## Validation Functions

```c
/* Driver-level validators (used by the main loop) */
bool imu_validate_accelerometer_range(imu_data_t *imu);  /* ±16 g in m/s² */
bool imu_validate_gyroscope_range(imu_data_t *imu);      /* ±2000 °/s */
bool imu_validate_magnetometer_range(imu_data_t *imu);   /* ±4900 */

/* Utility-library validators (sensor_basics.h) */
result_t validate_accelerometer_value(float accel_value); /* ±160 m/s² */
result_t validate_imu_data(float accel_x, float accel_y, float accel_z);
```

## Protobuf Message Format

```protobuf
message SensorBoardIMUInfo {
    float accel_x;
    float accel_y;
    float accel_z;
    float gyro_x;
    float gyro_y;
    float gyro_z;
    float mag_x;
    float mag_y;
    float mag_z;
    SensorState state;
    IMUErrorCode error_code;
}

enum IMUErrorCode {
    IMU_NO_ERROR = 0;
    IMU_COMMUNICATION_FAILURE = 1;
    IMU_ACCELEROMETER_ERROR = 2;
    IMU_GYROSCOPE_ERROR = 3;
    IMU_MAGNETOMETER_ERROR = 4;
}
```

## Common Applications

### Impact Detection

```c
float mag = imu_get_acceleration_magnitude(&imu_data);
if (mag > IMPACT_THRESHOLD) {
    /* High acceleration detected */
}
```

### Tilt Detection

```c
float pitch = imu_get_pitch(&imu_data);
float roll  = imu_get_roll(&imu_data);
```

### Motion Classification

```c
/* Static vs dynamic based on gyro magnitude */
float gyro_mag = sqrtf(gyro_x*gyro_x + gyro_y*gyro_y + gyro_z*gyro_z);
```

## Integration Notes

- Single IMU instance in the main application (dual IMU planned)
- All nine axes (accel, gyro, mag) transmitted as independent fields at the main loop interval
- First poll performs one-time device configuration; a failing device degrades to IDLE / DISCONNECTED without blocking the loop
- On successful poll the main loop runs the three range validators and sets IMU\_ACCELEROMETER\_ERROR, IMU\_GYROSCOPE\_ERROR, or IMU\_MAGNETOMETER\_ERROR accordingly
- Timestamp tracking (HAL\_GetTick) enables dead reckoning applications
- Filter algorithms can be applied to the raw data for smoothing
- Unit tested on host: init defaults, update/read round-trip, magnitude, pitch/roll, range validators (test/sensor\_board/test\_imu\_sensor)

# Load Cell

Load cells measure force/weight to detect object presence, evaluate structural loading, or monitor mechanical stress. The system supports a dual load cell configuration, each read through its own HX711 24-bit ADC using GPIO bit-banging. This is the most complete sensor driver on the board: it talks to real hardware with no compile gate.

### Hardware Specifications

<table id="bkmrk-load-cell-hw-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Parameter

</th><th>Value

</th></tr><tr><td>Sensor Count

</td><td>2 (independent)

</td></tr><tr><td>ADC

</td><td>HX711 24-bit, one per load cell

</td></tr><tr><td>Interface

</td><td>GPIO bit-bang (DOUT input, SCK output)

</td></tr><tr><td>Gain

</td><td>Channel A, gain 128 (LOAD\_CELL\_GAIN\_PULSES = 1)

</td></tr><tr><td>Measurement

</td><td>Force (Newtons) / Mass (grams) after calibration; raw counts always available

</td></tr><tr><td>Ready timeout

</td><td>200 ms (LOAD\_CELL\_READY\_TIMEOUT\_MS)

</td></tr><tr><td>Supply

</td><td>HX711 VCC 2.7 to 5.5 V, GND common with the STM32

</td></tr></tbody></table>

### Wiring and Pin Map (from firmware.ioc)

<table id="bkmrk-load-cell-pins-table"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Unit

</th><th>HX711 DOUT (data, low = ready)

</th><th>HX711 SCK (clock)

</th></tr><tr><td>0

</td><td>PA5 (WEIGHT\_INPUT\_1)

</td><td>PC7 (WEIGHT\_CLOCK\_1)

</td></tr><tr><td>1

</td><td>PA6 (WEIGHT\_INPUT\_2)

</td><td>PB5 (WEIGHT\_CLOCK\_2)

</td></tr></tbody></table>

## Data Structure

```c
typedef struct {
  int32_t raw_counts;               /* 24-bit two's complement reading */
  float force_newtons;
  float mass_grams;
  float scale_newtons_per_count;    /* default 1.0 (passthrough) */
  int32_t tare_offset_counts;
  bool is_calibrated;               /* true once load_cell_set_scale() called */
  bool read_ok;                     /* true if the last poll succeeded */

  /* HX711 hardware binding (set by load_cell_sensor_init_hw) */
  GPIO_TypeDef *dout_port;
  uint16_t dout_pin;
  GPIO_TypeDef *sck_port;
  uint16_t sck_pin;
  uint8_t gain_pulses;
} load_cell_data_t;
```

## Initialization

### Initialize Load Cells (as in main.c)

```c
load_cell_data_t load_cell_data[2];

/* Unit 0: DOUT = PA5, SCK = PC7. Unit 1: DOUT = PA6, SCK = PB5. */
load_cell_sensor_init_hw(&load_cell_data[0], GPIOA, GPIO_PIN_5, GPIOC, GPIO_PIN_7);
load_cell_sensor_init_hw(&load_cell_data[1], GPIOA, GPIO_PIN_6, GPIOB, GPIO_PIN_5);
/* init_hw powers up the HX711 and auto-tares */

/* Alternative: load_cell_sensor_init(&data) zero-initialises WITHOUT a
 * hardware binding; poll() then returns RESULT_ERR_UNIMPLEMENTED */
```

### Poll Load Cell Sensor

```c
result_t lc_result = poll_load_cell_sensor(&load_cell_data[i]);
```

## Data Access Functions

```c
float force, scale;
float mass;
int32_t counts, tare;
bool valid;

load_cell_get_force_newtons(&cell, &force);
load_cell_get_mass_grams(&cell, &mass);
load_cell_get_raw_counts(&cell, &counts);
load_cell_get_calibration(&cell, &scale, &tare);
load_cell_sensor_is_valid(&cell, &valid);
```

## Calibration Procedure

### Two-Step Calibration

#### Step 1: Tare (Zero Load)

```c
/* With nothing on the cell: average N raw reads and store as zero offset.
 * init_hw already does this automatically at startup. */
load_cell_tare(&cell, 10);
```

#### Step 2: Span (Known Weight)

```c
/* Place a known mass, read raw counts, then:
 *   scale = known_force_newtons / (raw_counts - tare_offset_counts)   */
load_cell_set_scale(&cell, newtons_per_count);   /* sets is_calibrated = true */
```

### Measurement Formulas

```
force_newtons = (raw_counts - tare_offset_counts) × scale_newtons_per_count
mass_grams    = force_newtons / 9.81 × 1000
```

**Note:**<span style="white-space: pre-wrap;"> before calibration the default scale is 1.0 (passthrough): raw\_counts is trustworthy but force\_newtons and mass\_grams are not physical units yet.</span>

## Protobuf Message Format

```protobuf
message SensorBoardLoadCellInfo {
    uint32 sensor_index;            /* 0 or 1 */
    float force_newtons;
    float mass_grams;
    int32 raw_counts;
    float scale_newtons_per_count;
    int32 tare_offset_counts;
    bool is_calibrated;
    SensorState state;
    LoadCellErrorCode error_code;   /* NO_ERROR, COMMUNICATION_FAILURE, INVALID_DATA */
}
```

## Unit Conversions

<table id="bkmrk-load-cell-conv-table"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>From

</th><th>To

</th><th>Factor

</th></tr><tr><td>Newtons

</td><td>kilograms-force (kgf)

</td><td>÷ 9.81

</td></tr><tr><td>Newtons

</td><td>pounds-force (lbf)

</td><td>÷ 4.448

</td></tr><tr><td>grams

</td><td>kilograms

</td><td>÷ 1000

</td></tr></tbody></table>

## Integration Notes

- Two independent units polled every main loop iteration; each is transmitted in its own envelope with its sensor\_index
- Each sensor maintains separate calibration (tare + scale) and independent error reporting
- HX711 gain/channel is selected by extra SCK pulses after the 24 data bits (1 = channel A gain 128, 2 = channel B gain 32, 3 = channel A gain 64)
- The blocking wait for data-ready can take up to 200 ms per cell per poll; keep this in mind when reducing the loop interval
- A failed read maps to state ERROR with LOAD\_CELL\_COMMUNICATION\_FAILURE; a read with implausible data maps to LOAD\_CELL\_INVALID\_DATA

# Pressure Sensor

The pressure sensors are analog force sensing resistor (FSR) pads read via ADC, intended primarily for robotic gripper force feedback: grip force sensing, object presence detection, and load distribution across two gripper pads. The system supports a dual sensor configuration.

### Hardware Specifications

<table id="bkmrk-pressure-hw-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Parameter

</th><th>Value

</th></tr><tr><td>Sensor Count

</td><td>2 (independent)

</td></tr><tr><td>Interface

</td><td>Analog ADC (compile gated, see Hardware Status below)

</td></tr><tr><td>Board pins

</td><td>PD15 (FORCE\_ANALOG\_DATA\_1), PF3 (FORCE\_ANALOG\_DATA\_2)

</td></tr><tr><td>Output unit

</td><td>kPa via linear conversion, plus raw voltage and temperature field

</td></tr></tbody></table>

## Hardware Status

<span style="white-space: pre-wrap;">The ADC path is compile gated by </span>`<span class="editor-theme-code">PRESSURE_USE_ADC</span>`<span style="white-space: pre-wrap;"> because no ADC is enabled in CubeMX yet. Without the flag, </span>`<span class="editor-theme-code">poll_pressure_sensor()</span>`<span style="white-space: pre-wrap;"> returns RESULT\_ERR\_UNIMPLEMENTED and both sensors report IDLE / DISCONNECTED (the firmware still links). To enable:</span>

1. In CubeMX enable an ADC and the channel(s) for the force pins. On the STM32H753, PD15 has NO ADC function and PF3 = ADC3\_INP5, so FORCE\_ANALOG\_DATA\_1 must be moved to an ADC-capable pin.
2. <span style="white-space: pre-wrap;">Build with </span>`<span class="editor-theme-code">-D PRESSURE_USE_ADC</span>`.
3. Bind each unit with pressure\_sensor\_init\_hw().

## Conversion Model

```
voltage      = raw / adc_max × reference_voltage
pressure_kpa = voltage × scale_kpa_per_volt + offset_kpa
```

Defaults: scale\_kpa\_per\_volt = 1.0, offset\_kpa = 0.0 (passthrough until calibrated).

## Data Structure

```c
typedef struct {
  float pressure_kpa;
  float temperature_c;
  float voltage;
  bool is_calibrated;
  bool read_ok;              /* true if the last poll read succeeded */

  /* ADC binding (set by pressure_sensor_init_hw) */
  void *adc_handle;          /* ADC_HandleTypeDef* (void* keeps header HAL-free) */
  uint32_t adc_channel;      /* ADC_CHANNEL_x */
  uint32_t adc_max;          /* full-scale count (e.g. 65535 for 16-bit) */
  float reference_voltage;   /* ADC Vref+ in volts */
  float scale_kpa_per_volt;  /* linear gain (default 1.0) */
  float offset_kpa;          /* linear offset (default 0.0) */
} pressure_sensor_data_t;
```

## Initialization

### Initialize Pressure Sensors (as in main.c)

```c
pressure_sensor_data_t pressure_data[2];
for (size_t i = 0; i < 2; i++) {
    pressure_sensor_init(&pressure_data[i]);
}

/* Once an ADC exists, bind it per unit: */
pressure_sensor_init_hw(&pressure_data[1], &hadc3, ADC_CHANNEL_5,
                        65535U, 3.3f);
```

### Poll Pressure Sensor

```c
result_t pr_result = poll_pressure_sensor(&pressure_data[i]);
```

## Data Access Functions

```c
float kpa, temp_c, voltage;
bool valid;

pressure_sensor_get_pressure_kpa(&sensor, &kpa);
pressure_sensor_get_temperature_c(&sensor, &temp_c);
pressure_sensor_get_voltage(&sensor, &voltage);
pressure_sensor_is_valid(&sensor, &valid);
```

## Calibration

```c
/* kPa = V × scale + offset; marks the sensor calibrated */
pressure_sensor_set_calibration(&sensor, scale_kpa_per_volt, offset_kpa);
```

## Pressure Unit Conversions

<table id="bkmrk-pressure-conv-table"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>From

</th><th>To

</th><th>Multiply By

</th></tr><tr><td>bar

</td><td>kPa

</td><td>100

</td></tr><tr><td>psi

</td><td>kPa

</td><td>6.895

</td></tr><tr><td>atm

</td><td>kPa

</td><td>101.325

</td></tr><tr><td>kPa

</td><td>bar

</td><td>0.01

</td></tr><tr><td>kPa

</td><td>psi

</td><td>0.145

</td></tr><tr><td>kPa

</td><td>atm

</td><td>0.00987

</td></tr></tbody></table>

Function-based conversions (bar\_to\_psi, psi\_to\_bar) are declared in the utility library but currently commented out; see Sensor Board Utility Library.

## Protobuf Message Format

```protobuf
message SensorBoardPressureInfo {
    uint32 sensor_index;            /* 0 or 1 */
    float pressure_kpa;
    float temperature_c;
    float voltage;
    bool is_calibrated;
    SensorState state;
    PressureErrorCode error_code;   /* NO_ERROR, COMMUNICATION_FAILURE, INVALID_DATA */
}
```

## Applications

### Robotic Gripper Control (Primary Use Case)

- Grip force feedback for object handling
- Object presence detection (pressure spike threshold)
- Adaptive compliance for varying object sizes and materials
- Dual sensors support load sharing across gripper pads

### Possible Secondary Uses (not implemented)

- Depth sensing (water), altitude sensing (air), system pressure monitoring, if a suitable transducer replaces the FSR pads

## Integration Notes

- Two independent units polled every main loop iteration, each transmitted in its own envelope with its sensor\_index (logged under the name "Force0"/"Force1")
- Each sensor maintains independent calibration and error reporting
- The temperature\_c field exists for future compensation algorithms; no temperature source is wired up yet
- Until the ADC is enabled the sensors are harmless placeholders: IDLE / DISCONNECTED, zeroed values

# Testing

Test organization, the Unity testing framework usage, coverage per suite, manual hardware testing checklists and a debugging/troubleshooting guide. Reminder: the sensor board code was only partially tested; unit tests cover the pure-logic parts of the drivers, and end to end hardware validation was not completed by the 2025-2026 team.

## Test Organization

<table id="bkmrk-test-suites-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Suite

</th><th>Location

</th></tr><tr><td>test\_sensor\_basics

</td><td>test/sensor\_board/test\_sensor\_basics/test\_sensor\_basics.c

</td></tr><tr><td>test\_ph\_sensor

</td><td>test/sensor\_board/test\_ph\_sensor/test\_ph\_sensor.c

</td></tr><tr><td>test\_imu\_sensor

</td><td>test/sensor\_board/test\_imu\_sensor/test\_imu\_sensor.c

</td></tr></tbody></table>

## Test Framework

- **Framework:**<span style="white-space: pre-wrap;"> Unity (open source C testing framework)</span>
- **Build system:**<span style="white-space: pre-wrap;"> PlatformIO (env:sensor\_board, test\_filter = sensor\_board/\*)</span>
- **Test type:**<span style="white-space: pre-wrap;"> driver logic tests exercising the data structures and math directly; the hardware access paths (ADC, HX711 GPIO, EXTI) are not mocked and not covered</span>

## Building and Running Tests

```bash
# All sensor board tests
pio test -e sensor_board

# One suite
pio test -e sensor_board -f test_ph_sensor

# Verbose output
pio test -e sensor_board -f test_sensor_basics -v
```

## Coverage Per Suite

### test\_sensor\_basics

- Accelerometer boundary values: ±160.0 accepted, ±160.1 rejected
- Multi-axis validation: one bad axis fails validate\_imu\_data()
- Temperature and pressure conversion tests exist but are commented out together with their implementations

### test\_ph\_sensor

- Initialization defaults (raw 0, voltage 0, pH 7.0, stored reference voltage)
- ADC to voltage to pH conversion with a 12-bit ADC scale
- Clamping at the extremes (ADC 0 clamps to pH 14, full scale clamps to pH 0 with the test calibration)
- Calibration changes the measurement (offset and slope applied)

### test\_imu\_sensor

- Initialization zeroes all axes and the timestamp
- Update and read round-trip for accel/gyro/mag and timestamp
- Acceleration magnitude (3-4-12 triangle gives 13)
- Pitch and roll helpers from accelerometer data
- Range validators accept zeros and reject out-of-range values

## Manual Hardware Testing Checklist

1. <span style="white-space: pre-wrap;">Flash with </span>`<span class="editor-theme-code">pio run -e sensor_board -t upload</span>`<span style="white-space: pre-wrap;"> and open the serial monitor at 115200 baud</span>
2. Confirm the boot banner and each "init completed" line (IMU, pH, load cells, pressure, flow, pump, Ethernet)
3. Confirm the three LEDs toggle every 5 seconds (loop heartbeat)
4. Check the per-sensor status lines: connected hardware should read OPERATING | OK, absent hardware IDLE | DISCONNECTED
5. Load cells: press on each cell and watch raw\_counts/force change; verify tare at startup reads near zero
6. Flow and pump: with tubing wet, enabling the pump must produce flow pulses; "commanded on but no flow detected" indicates a dry/absent pump or a not-configured EXTI4 line
7. Network: set sendUDP = true, then capture UDP datagrams on port 1500 at 192.168.0.222 and decode with the PBEnvelope schema
8. Send a SensorBoardPumpInfo command packet and verify the pump speed changes

## Debugging &amp; Troubleshooting

<table id="bkmrk-troubleshooting-table"><colgroup><col></col><col></col><col></col></colgroup><tbody><tr><th>Issue

</th><th>Cause

</th><th>Solution

</th></tr><tr><td>Sensor IDLE / DISCONNECTED

</td><td>Not connected, or driver compile gated

</td><td>Check wiring; for pH/pressure verify the PH\_SENSOR\_USE\_ADC / PRESSURE\_USE\_ADC build flags and the CubeMX ADC config

</td></tr><tr><td>Sensor ERROR

</td><td>Communication failure

</td><td>Verify HX711 wiring and timing, I2C address and pull-ups, ADC channel binding

</td></tr><tr><td>Invalid data

</td><td>Out of range values

</td><td>Check calibration parameters (pH slope/offset, load cell scale/tare, pressure scale/offset)

</td></tr><tr><td>Flow always 0

</td><td>EXTI4 not enabled in CubeMX

</td><td>Configure PA4 as EXTI4 rising edge and enable the EXTI4 NVIC line

</td></tr><tr><td>Pump OPERATING / DISCONNECTED

</td><td>No flow while commanded on

</td><td>Pump absent, dry, or stalled; or the flow sensor is not installed/configured

</td></tr><tr><td>No UDP packets

</td><td>Transmit gate or addressing

</td><td>Set sendUDP = true; check IP/MAC constants, MAC filtering, and that port 1500 is not blocked

</td></tr><tr><td>Low heap warning

</td><td>Memory leak or queue growth

</td><td>Review protobuf encode/free paths and UDP queue sizes

</td></tr><tr><td>Serial monitor silent

</td><td>Wrong port or baud

</td><td>Check the ST-Link COM port and 115200 baud; verify LOG\_init ran

</td></tr></tbody></table>

# Reference

Source code references, build configuration, hardware datasheet pointers, useful commands and the pre-deployment checklist. If you made it till here, you a true G.☮️

## Source Code References

### Main Application

<table id="bkmrk-main-app-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>File

</th><th>Purpose

</th></tr><tr><td>src/sensor\_board/main.c

</td><td>Main entry point, MainTask, sensor loop, packet handlers

</td></tr></tbody></table>

### Sensor and Actuator Drivers

<table id="bkmrk-drivers-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Component

</th><th>Location

</th></tr><tr><td>IMU

</td><td>components/sensor\_board/imu/imu\_sensor.h / .c

</td></tr><tr><td>pH

</td><td>components/sensor\_board/ph/ph\_sensor.h / .c

</td></tr><tr><td>Load Cell (HX711)

</td><td>components/sensor\_board/load\_cell/load\_cell\_sensor.h / .c

</td></tr><tr><td>Pressure (FSR)

</td><td>components/sensor\_board/pressure/pressure\_sensor.h / .c

</td></tr><tr><td>Flow Sensor

</td><td>components/sensor\_board/sampling/flow\_sensor/flow\_sensor.h / .c

</td></tr><tr><td>Pump

</td><td>components/sensor\_board/sampling/pump/pump.h / .c

</td></tr><tr><td>Utilities

</td><td>components/sensor\_board/sensor\_basics/sensor\_basics.h / .c

</td></tr></tbody></table>

### Shared Components

<table id="bkmrk-shared-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Component

</th><th>Location

</th></tr><tr><td>Networking (LwIP glue, UDP)

</td><td>components/common/networking/

</td></tr><tr><td>Network addresses and port

</td><td>components/common/networking\_constants/ip\_mac\_constants.h

</td></tr><tr><td>Packet dispatcher

</td><td>components/common/packet\_dispatcher/

</td></tr><tr><td>Protobuf encode/decode helpers

</td><td>components/common/pb\_message/

</td></tr><tr><td>Result codes and TRY macro

</td><td>components/common/result/

</td></tr><tr><td>Logging

</td><td>components/common/logging/

</td></tr></tbody></table>

### Protobuf Definitions

<span style="white-space: pre-wrap;">Message definitions (PBEnvelope, SensorBoardPHInfo, SensorBoardIMUInfo, SensorBoardLoadCellInfo, SensorBoardPressureInfo, SensorBoardFlowSensorInfo, SensorBoardPumpInfo, SensorBoardDiagnostics) live in the </span>**ERC-Protobufs**<span style="white-space: pre-wrap;"> git submodule and are compiled to C by nanopb during the PlatformIO build. If the build cannot find the .pb.h headers, initialize the submodule:</span>

```bash
git submodule update --init ERC-Protobufs
```

### Build Configuration

<table id="bkmrk-build-config-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>File

</th><th>Purpose

</th></tr><tr><td>platformio.ini

</td><td>Build configuration for all boards (env:sensor\_board for this one)

</td></tr><tr><td>components/sensor\_board/firmware/firmware.ioc

</td><td>CubeMX device configuration

</td></tr><tr><td>components/sensor\_board/STM32H753XX\_FLASH.ld

</td><td>Linker script

</td></tr></tbody></table>

## Development Workflow

### Useful Commands (PlatformIO CLI)

<span style="white-space: pre-wrap;">Check this out for compiling code and more about project structure- </span>[Project Structure](https://bookstack.roboteamtwente.nl/books/embedded-infastructure/chapter/project-structure "Project Structure")

```bash
# Build the sensor board firmware
pio run -e sensor_board

# Flash to the Nucleo board
pio run -e sensor_board -t upload

# Serial monitor (115200 baud)
pio device monitor -b 115200

# Run the unit tests
pio test -e sensor_board
```

### Development Cycle

1. Change peripherals in CubeMX (firmware.ioc), regenerate with Keep User Code
2. Implement or update the driver in components/sensor\_board/
3. Build and run unit tests on the host
4. Flash, watch the serial log, verify the per-sensor status lines
5. Enable sendUDP and verify packets on the network

## Hardware References

<table id="bkmrk-hw-ref-table"><colgroup><col></col><col></col><col></col><col></col></colgroup><tbody><tr><th>Device

</th><th>Model

</th><th>Protocol

</th><th>Note

</th></tr><tr><td>Microcontroller

</td><td>STM32H753ZI (NUCLEO-H753ZI)

</td><td>n/a

</td><td>ARM Cortex-M7, 480 MHz capable (running at 64 MHz), 2 MB Flash; ST STM32H7 reference manual

</td></tr><tr><td>Ethernet PHY

</td><td>LAN8742

</td><td>RMII

</td><td>10/100 Mbps auto-negotiation

</td></tr><tr><td>pH

</td><td>DFRobot SEN0161

</td><td>Analog ADC

</td><td>40-sample averaging, 5 V board (scale output below 3.3 V)

</td></tr><tr><td>IMU

</td><td>Xsens Avior

</td><td>I2C1 (PB8/PB9), Xbus

</td><td>Address 0x6B, 100 Hz; driver untested on hardware (mtidocs.xsens.com)

</td></tr><tr><td>Load Cell ADC

</td><td>HX711 (×2)

</td><td>GPIO bit-bang

</td><td>24-bit, channel A gain 128

</td></tr><tr><td>Pressure

</td><td>FSR pads (×2)

</td><td>Analog ADC

</td><td>Compile gated, needs ADC in CubeMX

</td></tr><tr><td>Flow

</td><td>FM-PS2216

</td><td>GPIO EXTI pulses

</td><td>40 to 150 ml/min, 5.5 pulses/ml

</td></tr><tr><td>Pump

</td><td>Grothen 12 V DC mini peristaltic

</td><td>PWM (TIM3 CH3)

</td><td>Single MOSFET, unidirectional, open loop

</td></tr></tbody></table>

## Network Quick Reference

<table id="bkmrk-network-ref-table"><colgroup><col></col><col></col></colgroup><tbody><tr><th>Item

</th><th>Value

</th></tr><tr><td>Board IP

</td><td>192.168.0.111 (static, no DHCP)

</td></tr><tr><td>Destination (sample board)

</td><td>192.168.0.222

</td></tr><tr><td>UDP port

</td><td>1500

</td></tr><tr><td>Netmask / Gateway

</td><td>255.255.255.0 / 192.168.0.1

</td></tr></tbody></table>

## Quick Reference Checklist

### Before Deployment

- ERC-Protobufs submodule initialized, firmware builds clean
- All connected sensors responding (OPERATING | OK in the log)
- Network IP/MAC configured and sendUDP enabled
- Calibration set for pH (offset/slope) and load cells (tare/scale)
- Serial monitor showing sensor data at 115200 baud
- Heap usage healthy (well above the 4096 byte critical threshold)
- UDP packets reaching 192.168.0.222:1500 and decoding as PBEnvelope

### Monitoring in Production

- Watch per-sensor state/status codes in the log lines
- Monitor the free heap trend printed each loop
- Verify data ranges match expectations (pH 0-14, flow below 150 ml/min)
- Track error rates per sensor and pump/flow cross-check warnings

<span style="white-space: pre-wrap;">Hope you had fun.☮️ End of documentation for the Sensor Board. </span>

May The Force Be With You or Live Long and Prosper, depending on what you like.... but remember the Dark Side always has cooler toys

**The Sensor Board is the coolest board ~ Mybrosky**

# Debugging Board

Lil gameboy doodad

# Overview

<span style="white-space: pre-wrap;">The debugging board is a </span>**dedicated auxiliary system**<span style="white-space: pre-wrap;"> whose only job is to make the rest of the robot less painful to work with.</span>

<p class="callout info">It is not part of the rover’s core functionality.</p>

## Purpose

At a high level, the debugging board serves roles:

### Visibility

Provide real-time insight into system state:

- logs
- status indicators
- network activity
- subsystem health

Instead of digging through serial output on multiple MCUs or adding temporary debug code everywhere, this board aggregates and presents useful information.

### Control / Interaction

<p class="callout danger">Todo :D</p>

### Isolation of debugging concerns

<p class="callout danger">Todo :D</p>

## Physical components

The exact hardware may evolve, but the debugging board generally consists of:

### Ethernet interface

- Connects to the system network (switch / internal bus)
- Receives and sends packets (including protobuf-based messages)
- Acts as a bridge between the debugging interface and the rest of the robot

### Display

- Shows system state, logs, or selected information
- <span style="white-space: pre-wrap;">It is a </span>**ILI9341 SPI Display**

Used for quick, local feedback without needing a laptop.

### Input interface (buttons / panel)

- Physical buttons or switches
- Used to:
    - trigger actions
    - navigate menus
    - send commands

# Display - ILI9341 Hardware Configuratoin

The debugging board incorporates a graphical display based on the ILI9341 controller. This display serves as the primary local interface for presenting system state, diagnostics, and user feedback.

<span style="white-space: pre-wrap;">The ILI9341 is a widely used TFT LCD controller that integrates display driving logic, internal GRAM (Graphics RAM), and a command-based interface over serial or parallel buses. In this system, it is used in </span>**SPI mode**, which aligns with the board’s pin constraints and simplifies integration with the MCU.

## Functional Role in the System

Within the debugging board, the display is responsible for:

- Rendering system status (connectivity, subsystem health, etc.)
- Displaying structured debugging information
- Providing immediate visual feedback to user input (button interactions)
- Supporting simple UI constructs (menus, indicators, overlays)

<p class="callout warning"><span style="white-space: pre-wrap;">The display is not intended for high-throughput graphics or complex rendering. Its role is </span>**informational and interactive**, not graphical-intensive.</p>

## Features of the ILI9341

The ILI9341 controller provides a set of features well suited for embedded applications.

### Resolution and Color Depth

- <span style="white-space: pre-wrap;">Resolution: </span>**240 × 320 pixels**
- <span style="white-space: pre-wrap;">Color depth: </span>**16-bit RGB (RGB565)**

This provides sufficient resolution for:

- text rendering
- simple UI layouts
- basic graphical elements (icons, shapes)

### Internal GRAM (Frame Buffer)

<p class="callout info"><span style="white-space: pre-wrap;">The controller includes internal </span>**Graphics RAM (GRAM)**, which stores pixel data.</p>

- <span style="white-space: pre-wrap;">The MCU does </span>**not**<span style="white-space: pre-wrap;"> need to maintain a full framebuffer</span>
- Pixel data is written directly to the display over SPI
- The display retains the image until overwritten

This significantly reduces RAM requirements on the MCU, which is critical in embedded systems.

### Command-Based Interface

The display is controlled through a command/data protocol:

- Commands configure behavior (e.g., orientation, pixel format)
- Data writes update pixel values in GRAM

Typical operations include:

- setting an address window
- writing pixel data
- issuing initialization sequences

### Display Orientation and Addressing

The controller supports:

- configurable screen rotation (portrait / landscape)
- programmable address windows

This allows:

- flexible UI layout
- efficient partial updates (writing only specific regions)

### Hardware Reset and Initialization

The display requires:

- a hardware reset sequence
- a series of configuration commands during initialization

These typically configure:

- power control
- gamma curves
- pixel format
- memory access control

<p class="callout danger"><span style="white-space: pre-wrap;">ILI9341 is a relatively complex and if you want to do anything with the internal library of it you need more than what can be written here. </span>**Read the** [official documentation](https://cdn-shop.adafruit.com/datasheets/ILI9341.pdf)</p>

## MCU Configuration

The SPI peripheral must be configured with:

- <span style="white-space: pre-wrap;">Mode: </span>**Full-Duplex Master**
- <span style="white-space: pre-wrap;">Data size: </span>**8-bit**
- <span style="white-space: pre-wrap;">First bit: </span>**MSB-first**
- <span style="white-space: pre-wrap;">Clock polarity: </span>**Low**
- <span style="white-space: pre-wrap;">Clock phase: </span>**1st edge**
- <span style="white-space: pre-wrap;">NSS: </span>**Software**
- Baud rate prescaler: selected based on display stability

These settings must match the display’s timing requirements.

<p class="callout info">For the baud rate, you want it to be as high as possible without it being unstable. For debugging and testing, it's good practice to lower it first, get it working there (as it is a lot more stable) and then increase it again.</p>

# Display - ILI9341 Library

## Purpose

<span style="white-space: pre-wrap;">The </span>`<span class="editor-theme-code">ili9341</span>`<span style="white-space: pre-wrap;"> library provides the low-level and mid-level drawing interface for the ILI9341-based display used on the debugging board.</span>

Its role is to hide the raw command sequence and SPI transaction details of the display controller behind a set of functions for:

- initialization
- display configuration
- pixel and region drawing
- primitive graphics
- text rendering
- monochrome bitmap rendering
- rounded rectangle rendering

<p class="callout info">In other words, this library is the software layer that turns the display from a peripheral into a usable rendering surface.</p>

## Scope of the Library

This library sits close to the hardware.

It is responsible for:

- driving the ILI9341 controller over SPI
- controlling the display GPIO lines (`<span class="editor-theme-code">CS</span>`<span style="white-space: pre-wrap;">, </span>`<span class="editor-theme-code">DC</span>`<span style="white-space: pre-wrap;">, </span>`<span class="editor-theme-code">RST</span>`)
- issuing the controller initialization sequence
- writing pixel data to the display GRAM
- exposing simple drawing primitives for higher-level UI code

<span style="white-space: pre-wrap;">It is </span>**not**<span style="white-space: pre-wrap;"> responsible for:</span>

- application UI logic
- layout management
- widget systems
- maintaining a full framebuffer
- asynchronous rendering scheduling

<p class="callout info">This is a direct-draw display driver and utility library, not a graphics framework.</p>

## High-Level Design

The library is structured around four layers of functionality.

### Transport layer

These functions send commands and bytes over SPI:

- `<span class="editor-theme-code">ILI9341_SPI_Send()</span>`
- `<span class="editor-theme-code">ILI9341_Write_Command()</span>`
- `<span class="editor-theme-code">ILI9341_Write_Data()</span>`

### Display control layer

These functions manage display state and configuration:

- `<span class="editor-theme-code">ILI9341_Reset()</span>`
- `<span class="editor-theme-code">ILI9341_Set_Address()</span>`
- `<span class="editor-theme-code">ILI9341_Set_Rotation()</span>`
- `<span class="editor-theme-code">ILI9341_Enable()</span>`
- `<span class="editor-theme-code">ILI9341_Init()</span>`

### Primitive drawing layer

These functions draw directly to the screen:

- single colours
- pixels
- colour bursts
- lines
- rectangles
- bitmaps
- colour arrays

### Utility rendering layer

These functions build on the primitives to provide:

- text rendering
- rounded-corner rendering
- custom monochrome corner bitmap generation

This layered structure is important. Most higher-level code should use the drawing primitives and utility functions, not manually emit ILI9341 commands unless there is a very specific reason.

## Hardware Interface Definitions

The header defines the display connection through compile-time macros.

### SPI instance

```c
#define HSPI_INSTANCE &hspi1
```

This selects the SPI peripheral used to communicate with the display.

### GPIO control lines

```c
#define LCD_CS_PORT TFT_CS_GPIO_Port
#define LCD_CS_PIN TFT_CS_Pin

#define LCD_DC_PORT TFT_DC_GPIO_Port
#define LCD_DC_PIN TFT_DC_Pin

#define LCD_RST_PORT TFT_RESET_GPIO_Port
#define LCD_RST_PIN TFT_RESET_Pin
```

These define:

- chip select
- data/command selection
- hardware reset

The library assumes these symbols are provided by the board support layer.

### Screen dimensions

```c
#define ILI9341_SCREEN_HEIGHT 240
#define ILI9341_SCREEN_WIDTH 320
```

These define the nominal physical display dimensions..

### Burst limit

```c
#define BURST_MAX_SIZE 500
```

This controls the maximum temporary buffer size used during burst-style SPI transfers.

It affects:

- solid colour fills
- colour array streaming
- bitmap rendering

This is a performance and stack/RAM tradeoff parameter.

## Color Definitions

The header provides a set of named RGB565 color constants, for example:

- `<span class="editor-theme-code">BLACK</span>`
- `<span class="editor-theme-code">WHITE</span>`
- `<span class="editor-theme-code">RED</span>`
- `<span class="editor-theme-code">GREEN</span>`
- `<span class="editor-theme-code">BLUE</span>`
- `<span class="editor-theme-code">YELLOW</span>`
- `<span class="editor-theme-code">CYAN</span>`
- `<span class="editor-theme-code">MAGENTA</span>`

These are convenience values for application code and drawing functions.

<span style="white-space: pre-wrap;">All colors are represented in </span>**16-bit RGB565 format**, which matches the configured pixel format of the display controller.

## Initialization Sequence

### `<span class="editor-theme-code">ILI9341_Init()</span>`

```c
void ILI9341_Init(void);
```

This is the main initialization routine.

### What it does

It performs:

1. display enable
2. SPI init hook
3. hardware reset
4. software reset
5. a full controller configuration sequence
6. exit from sleep mode
7. display on
8. initial screen rotation selection

### Initialization sequence contents

The function writes a fixed command sequence configuring:

- power control
- driver timing
- pump ratio
- VCOM control
- memory access control
- pixel format
- frame rate
- gamma correction
- sleep exit
- display enable

This is the board’s current known-good configuration for the display.

### Why this matters

This sequence is not arbitrary boilerplate. It defines the electrical and visual behavior of the panel.

If it is modified, the maintainer must understand whether the change is:

- controller-required
- panel-specific
- timing-related
- cosmetic
- or cargo-culted from another project

## Basic Drawing Primitives

### `<span class="editor-theme-code">ILI9341_Draw_Colour()</span>`

```c
void ILI9341_Draw_Colour(uint16_t Colour);
```

Writes one pixel’s worth of RGB565 data to the display.

This function assumes the correct address window is already set.

It is mainly an internal low-level helper.

### `<span class="editor-theme-code">ILI9341_Draw_Colour_Burst()</span>`

```c
void ILI9341_Draw_Colour_Burst(uint16_t Colour, uint32_t Size);
```

Draws a repeated color value over a number of pixels.

#### Use case

Efficiently fill:

- large solid regions
- lines
- screen clears

#### How it works

It creates a temporary burst buffer containing repeated color bytes and transmits it in chunks.

<p class="callout info">This is much more efficient than sending each pixel individually.</p>

#### Importance

This function is central to the performance of:

- full screen fills
- rectangle fills
- line drawing

### `<span class="editor-theme-code">ILI9341_Draw_Colour_Array()</span>`

```c
void ILI9341_Draw_Colour_Array(const uint16_t *Colour, uint32_t PixelCount);
```

Draws an array of RGB565 pixel values.

#### Use case

Use this when the caller already has pixel data prepared, for example:

- image rendering
- precomputed graphics
- generated color buffers

#### Important implementation detail

<p class="callout warning"><span style="white-space: pre-wrap;">The function converts each </span>`<span class="editor-theme-code">uint16_t</span>`<span style="white-space: pre-wrap;"> color into big-endian byte order before sending.</span></p>

This is correct for SPI transmission to the display controller.

### `<span class="editor-theme-code">ILI9341_Draw_Pixel()</span>`

```c
void ILI9341_Draw_Pixel(uint16_t X, uint16_t Y, uint16_t Colour);
```

Draws one pixel at a specific coordinate.

#### Behavior

It:

- bounds checks the coordinate
- manually sets X address
- manually sets Y address
- issues memory write
- writes one pixel color

#### Performance note

<p class="callout warning"><span style="white-space: pre-wrap;">This is a </span>**very slow** operation compared to region-based drawing because it reissues addressing commands for every pixel.</p>

It is suitable for:

- sparse pixel updates
- debugging
- very small shapes

It is not suitable for rendering larger regions.

### `<span class="editor-theme-code">ILI9341_Fill_Screen()</span>`

```c
void ILI9341_Fill_Screen(uint16_t Colour);
```

Fills the whole display with one color.

#### Behavior

It sets the address window to the whole screen and then sends a repeated-color burst.

## Text Rendering

### `<span class="editor-theme-code">ILI9341_WriteString()</span>`

```c
void ILI9341_WriteString(uint16_t x, uint16_t y, const char *str,
                         ILI9341_FontDef font, uint16_t color,
                         uint16_t bgcolor);
```

Renders a null-terminated string using the specified font and foreground/background colors.

### Behavior

- iterates through each character
- wraps to the next line if the current X position exceeds screen width
- stops if the next line would exceed screen height

### Internal helper

This uses the internal function:

```c
static void ILI9341_WriteChar(...)
```

which renders one character pixel-by-pixel using the font bitmap.

## Bitmap Rendering

### `<span class="editor-theme-code">ILI9341_Draw_Bitmap()</span>`

```c
void ILI9341_Draw_Bitmap(uint16_t x, uint16_t y,
                         uint16_t w, uint16_t h,
                         const uint8_t *bitmap,
                         uint16_t Color, uint16_t BgColor);
```

<span style="white-space: pre-wrap;">Draws a </span>**1-bit-per-pixel bitmap**<span style="white-space: pre-wrap;"> into a rectangular region.</span>

### Expected bitmap format

The input bitmap is interpreted as packed monochrome data:

- 1 bit per pixel
- row-major
- MSB-first within each byte

### Rendering behavior

For each bit:

- <span style="white-space: pre-wrap;">set bit -&gt; draw </span>`<span class="editor-theme-code">Color</span>`
- <span style="white-space: pre-wrap;">clear bit -&gt; draw </span>`<span class="editor-theme-code">BgColor</span>`

### Use case

This is useful for:

- icons
- glyph-like shapes
- masks
- rounded corner patterns

<p class="callout info">It is not for full-color image rendering.</p>

## R³: Rounded Rectangle Rendering

The library includes support for rounded rectangle outlines using generated monochrome corner bitmaps.

<p class="callout info">This is more advanced than the rest of the primitive API and deserves separate explanation.</p>

### Why?

<p class="callout success">The reasons why the R³ system is highly important - if not necessary - are plenty and extensive. That's why I compiled a pastebin[<span style="white-space: pre-wrap;"> that includes all reasons</span>](https://pastebin.com/h6b2e2W0). Feel free to read it even though I believe it is pretty self explanatory</p>

### Concept

A rounded rectangle is rendered by:

1. generating a 1bpp bitmap for one rounded corner
2. rotating that bitmap to obtain all four corners
3. drawing the four corner bitmaps
4. drawing straight rectangle segments between them

This is a practical method for an SPI-driven display because it avoids expensive per-pixel circle calculations at draw time for every corner.

### Internal helpers

The implementation includes internal static helpers:

- `<span class="editor-theme-code">ILI9341_Get_Rounded_Corner_Bitmap()</span>`
- `<span class="editor-theme-code">bitmap_rotate_90_cw_1bpp()</span>`
- `<span class="editor-theme-code">ILI9341_Build_All_Rounded_Corners()</span>`
- `<span class="editor-theme-code">ILI9341_Draw_Rectangle_Custom_Corner()</span>`

These are not part of the public API, but they are important for maintainers to understand.

### `<span class="editor-theme-code">ILI9341_Draw_Rectangle_Rounded_Corner()</span>`

```c
result_t ILI9341_Draw_Rectangle_Rounded_Corner(
    uint16_t X, uint16_t Y, uint16_t Width, uint16_t Height,
    uint8_t thickness, uint8_t radius,
    uint8_t *corner_buffer, size_t corner_buffer_size,
    uint16_t Colour, uint16_t Bg_Colour);
```

This is the main public rounded rectangle API currently implemented with explicit caller-provided corner buffer storage.

#### Why caller-provided memory is used

The function requires the caller to provide a temporary buffer for the generated corner bitmaps.

This avoids hidden dynamic allocation and gives the caller control over memory use.

#### Buffer sizing

<p class="callout warning"><span style="white-space: pre-wrap;">The function expects enough memory for </span>**four**<span style="white-space: pre-wrap;"> 1bpp bitmaps, one for each corner.</span></p>

It computes the required size as:

```c
4 * (((radius + 7) >> 3) * radius)
```

in bytes.

#### Return values

- `<span class="editor-theme-code">RESULT_OK</span>`<span style="white-space: pre-wrap;"> on success</span>
- `<span class="editor-theme-code">RESULT_ERR_NO_MEM</span>`<span style="white-space: pre-wrap;"> if the provided buffer is too small</span>
- `<span class="editor-theme-code">RESULT_ERR_INVALID_ARG</span>`<span style="white-space: pre-wrap;"> for invalid parameters via internal helpers</span>

#### Use case

This function is appropriate when the UI wants rounded bordered rectangles without a full framebuffer.

# Menu Driver - Overview

## Purpose

<p class="callout info"><span style="white-space: pre-wrap;">The menu driver is a </span>**page-based UI framework**<span style="white-space: pre-wrap;"> for an embedded display (ILI9341). </span></p>

It defines:

- how UI is structured into pages
- how state is stored per page
- how navigation works
- how rendering is organized

## Architecture Position

```
[ Application Logic ]        
↓[ Menu Driver ]        
↓[ ILI9341 Driver ]        
↓[ SPI / Hardware ]
```

<span style="white-space: pre-wrap;">It </span>**does not**:

- own the main loop
- schedule tasks
- interpret input fully

<span style="white-space: pre-wrap;">It </span>**does**:

- define UI structure
- manage page lifecycle
- coordinate rendering

---

## 1.3 Design Model

Everything revolves around:

> “A UI is a collection of pages with lifecycle and state.”

Each page has:

- state
- init/update/render/destruct
- parent relationship

```
         Input / System Events
                  │
                  ▼
        ┌─────────────────────┐
        │   menu_manager_t    │
        │─────────────────────│
        │ active_page_id      │
        │ pages[]             │
        │ get_input()         │
        └─────────┬───────────┘
                  │ selects active page
                  ▼
      ┌──────────────────────────────────┐
      │          Active Page             │
      │──────────────────────────────────│
      │ state pointer                    │
      │ init()       ─┐                  │
      │ update()      ├─ custom page     │
      │ render()      ┤  behavior        │
      │ destruct()    ┘                  │
      └────────────────┬─────────────────┘
                       │ reads/writes
                       ▼
              ┌──────────────────┐
              │   Page State     │
              │──────────────────│
              │ selection        │
              │ cached values    │
              │ render flags     │
              │ page-local data  │
              └──────────────────┘
```

# Menu Driver - Configuration Layer

## Visual Configuration

```c
#define MENU_DRIVER_BACKGROUND_COLOR 0x0000
#define MENU_DRIVER_FOREGROUND_COLOR 0xFFFF
```

<span style="white-space: pre-wrap;">Black background, white foreground. </span>

## Layout Constraints

```c
#define MENU_SIDEBAR_WIDTH 38
```

Sidebar (ribbon) width.

## Capacity Limits

### List Pages

```c
#define MAX_LIST_ENTRIES 10
#define MAX_LIST_TITLE_LEN 24
```

### Overview Pages

```c
#define MENU_OVERVIEW_MAX_ENTRIES 10
#define MENU_OVERVIEW_MAX_ENTRY_TITLE_LEN 12
```

### Global

```c
#define MAX_PAGE_NAME_LEN 20
```

These define:

- memory footprint
- UI density
- rendering assumptions

# Menu Driver - Core Data Structures

## Page State Types

A **page state**<span style="white-space: pre-wrap;"> is:</span>

> <span style="white-space: pre-wrap;">The </span>**persistent data container**<span style="white-space: pre-wrap;"> that represents everything a UI page needs to function between frames.</span>

Not just data. It’s:

- memory of what the user did
- memory of what was rendered
- memory of external data (diagnostics, etc.)

### List Page State

```c
typedef struct {
  uint8_t num_entries;
  uint8_t selected_index;
  uint8_t entry_ids[MAX_LIST_ENTRIES];
  const uint8_t (*entry_icons)[MENU_DRIVER_ICON_BYTE_SIZE];
  bool first_render;
} page_list_state;
```

#### Responsibilities:

- track selection
- map entries → page IDs
- hold icons
- manage first render optimization

### Overview Page State

<p class="callout danger">Todo :D</p>

### State Union

```c
typedef union {
  page_list_state list;
  page_overview_state overview;
} menu_page_state;
```

### Page Type

```c
typedef enum {
  MENU_PAGE_TYPE_LIST,
  MENU_PAGE_TYPE_OVERVIEW,
} menu_page_type_t;
```

Used to interpret the union correctly.

## Page Object

```c
typedef struct {
  menu_page_state *state;
  menu_page_type_t type;
  unsigned char id;
  unsigned char parent_id;
  bool needs_render;

  char name[MAX_PAGE_NAME_LEN];

  void (*init)(menu_page_state *);
  void (*update)(menu_manager_t *);
  void (*render)(menu_manager_t *);
  void (*destruct)(menu_page_state *);
} menu_page_t;
```

<p class="callout info"><span style="white-space: pre-wrap;">This is the </span>**core abstraction**.</p>

### Definition and Role

A page object represents one logical screen within the menu system. It encapsulates:

- the data required to represent the page (via its state)
- the functions required to manage its lifecycle
- metadata used for navigation and identification

This abstraction allows the menu system to treat all pages uniformly, regardless of their internal implementation or purpose.

### Important Fields

#### Render Control

```c
bool needs_render;
```

This flag indicates whether the page requires re-rendering.

It allows the system to avoid unnecessary redraw operations, which is critical in environments where display updates are expensive.

<span style="white-space: pre-wrap;">The responsibility for managing this flag lies with the </span>**page implementation**<span style="white-space: pre-wrap;">. </span>

### Lifecycle Function Pointers

Each page defines its own behavior through four function pointers:

#### Initialization

```c
void (*init)(menu_page_state *state);
```

Responsible for preparing the page state when the page becomes active.

Typical responsibilities include:

- resetting selection indices
- initializing flags
- preparing any required data structures

#### Update

```c
void (*update)(struct menu_manager_t *manager);
```

Handles input processing and state updates.

This function is expected to:

- read input through the manager
- modify internal state accordingly
- trigger page transitions if necessary

#### Render

```c
void (*render)(struct menu_manager_t *manager);
```

Responsible for drawing the page to the display.

This function should:

- read from the page state
- issue drawing commands via the display driver
- <span style="white-space: pre-wrap;">respect the </span>`<span class="editor-theme-code">needs_render</span>`<span style="white-space: pre-wrap;"> flag when applicable</span>

#### Destruction

```c
void (*destruct)(menu_page_state *state);
```

Handles cleanup when the page is no longer active.

In embedded systems, this typically involves:

- resetting state fields
- releasing logical ownership of resources

Dynamic memory cleanup is generally not required unless explicitly used.

## Menu Manager

```c
typedef struct {
  unsigned char active_page_id;
  const menu_page_t *pages;
  menu_input (*get_input)(void);
} menu_manager_t;
```

Responsibilities:

- track active page
- provide input access
- hold page table

Does NOT:

- validate anything
- own memory
- manage concurrency

# Menu Driver - Overview Page

## Introduction

<span style="white-space: pre-wrap;">The </span>**List Page**<span style="white-space: pre-wrap;"> is a navigation-oriented page type within the menu driver. It provides a structured interface for selecting between multiple entries, typically representing:</span>

- subpages
- actions
- system modules

<span style="white-space: pre-wrap;">It is the primary mechanism for </span>**user-driven navigation**<span style="white-space: pre-wrap;"> within the menu system.</span>

## Purpose

The list page exists to answer:

> **“Where do you want to go next?”**

It is not responsible for displaying system state in detail. Instead, it:

- presents a bounded set of selectable entries
- tracks the current selection
- provides visual feedback for navigation
- enables transitions to other pages

<span style="white-space: pre-wrap;">In practice, it functions as the </span>**entry point and routing layer**<span style="white-space: pre-wrap;"> of the UI.</span>

## Architectural Role

The list page sits at the intersection of:

- **input handling**<span style="white-space: pre-wrap;"> (user navigation)</span>
- **menu structure**<span style="white-space: pre-wrap;"> (page hierarchy)</span>
- **visual rendering**<span style="white-space: pre-wrap;"> (icons and labels)</span>

```
[ Input ] → [ List Page ] → [ Page Transition ]
```

<span style="white-space: pre-wrap;">It does not consume system data (like overview pages), but rather controls </span>**flow through the interface**.

## Data Model

The list page is backed by the following state structure:

```c
typedef struct {
  uint8_t num_entries;
  uint8_t selected_index;
  uint8_t entry_ids[MAX_LIST_ENTRIES];
  const uint8_t (*entry_icons)[MENU_DRIVER_ICON_BYTE_SIZE];
  bool first_render;
} page_list_state;
```

### Entry Management

`<span class="editor-theme-code">num_entries</span>`

Defines how many entries are currently active.

<span style="white-space: pre-wrap;">This value must not exceed </span>`<span class="editor-theme-code">MAX_LIST_ENTRIES</span>`.

`<span class="editor-theme-code">entry_ids</span>`

Maps each visible entry to a logical identifier.

These IDs are typically used to:

- determine which page to switch to
- associate actions with selections

`<span class="editor-theme-code">entry_icons</span>`

Pointer to icon data associated with each entry.

- Icons are rendered alongside entries
- Each icon is a fixed-size bitmap
- Icons are stored in flash as static data

### Selection State

#### `<span class="editor-theme-code">selected_index</span>`

Indicates which entry is currently selected.

This is the central piece of state for navigation.

All rendering and transitions depend on this value.

### Render Control

`<span class="editor-theme-code">first_render</span>`

Indicates whether the page is being rendered for the first time.

Used to:

- trigger full initial draw
- avoid redundant rendering of static UI elements

## Rendering Model

<span style="white-space: pre-wrap;">The list page uses a </span>**focused rendering strategy**, rather than displaying all entries simultaneously.

### Visible Entries

Only three entries are rendered at any time:

- previous entry
- current (selected) entry
- next entry

This creates a scrolling effect without requiring full list rendering.

### Rendering Optimization

The only expensive draw of the list page is the initial one which draws the selection border, all initial entries (both icons and names).

<span style="white-space: pre-wrap;">After that the only thing that gets redrawn are the icons and the texts. There is also heavier optimization done for minimal font redrawing by keeping track of previously rendered text widths. </span>

<p class="callout info">This is critical for SPI-driven displays, where bandwidth is limited.</p>

## Interaction Model

The list page assumes an abstract input interface:

```
menu_input (*get_input)(void);
```

The page does not interpret physical inputs directly. Instead, it operates on abstract input values, allowing it to remain independent of hardware specifics.

Expected interactions include:

- move selection up
- move selection down
- confirm selection

## Relationship to Menu System

The list page enables hierarchical navigation through:

- `<span class="editor-theme-code">entry_ids</span>`<span style="white-space: pre-wrap;"> → target page identifiers</span>
- `<span class="editor-theme-code">parent_id</span>`<span style="white-space: pre-wrap;"> (in </span>`<span class="editor-theme-code">menu_page_t</span>`) → upward navigation

<p class="callout info"><span style="white-space: pre-wrap;">This allows the menu system to behave as a </span>**tree of pages**, rather than a flat structure.</p>

## Performance Considerations

The list page is designed for constrained environments:

- partial rendering minimizes SPI usage
- static memory avoids allocation overhead
- limited visible entries reduce draw complexity