# 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