Architecture
<p id="bkmrk-arch-intro">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.</p>
<h2 id="bkmrk-initialization-sequence">Initialization Sequence</h2>
<h3 id="bkmrk-phase-1-hardware">Phase 1: Hardware Setup (init_board, before the kernel starts)</h3>
<pre id="bkmrk-phase1-code"><code class="language-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. */
}</code></pre>
<h3 id="bkmrk-phase-2-drivers">Phase 2: Driver Initialization (start of MainTask)</h3>
<ol id="bkmrk-phase2-list"><li value="1">BSP LEDs (Green, Blue, Red)</li><li value="2">Logging over the ST-Link VCP UART (LOG_init(&hcom_uart[COM1]), 115200 baud)</li><li value="3">IMU (imu_sensor_init)</li><li value="4">pH sensor (ph_sensor_init with 3.3 V reference)</li><li value="5">Two HX711 load cells with their GPIO map (PA5/PC7 and PA6/PB5), each powered up and auto-tared</li><li value="6">Two pressure/FSR sensors (pressure_sensor_init)</li><li value="7">Flow sensor (flow_sensor_init, pulse counting via EXTI callback)</li><li value="8">Pump (pump_init on TIM3 CH3 PWM, then commanded to 50 percent and enabled as a startup default)</li></ol>
<h3 id="bkmrk-phase-3-network">Phase 3: Communication Setup</h3>
<ol id="bkmrk-phase3-list"><li value="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</li><li value="2">MAC address filtering for three allowed source MACs (ETH_setup_MAC_address_filtering)</li><li value="3">Two statically allocated prioritised UDP transmit queues (80 entries each)</li><li value="4">Packet dispatcher registration for five inbound message types (pH, IMU, load cell, pressure, pump)</li><li value="5">ETH_udp_init(2, send_queues, DispatchPacket) and a static ARP entry for the destination board 192.168.0.222</li></ol>
<h3 id="bkmrk-phase-4-loop">Phase 4: Main Loop</h3>
<p id="bkmrk-phase4-desc">The loop runs forever with a 5000 ms period (MAIN_TASK_DELAY_MS). Each iteration:</p>
<ol id="bkmrk-loop-steps"><li value="1">Read free heap; if below 4096 bytes log CRITICAL and sleep 10 s instead of polling</li><li value="2">Toggle the three LEDs (visual heartbeat)</li><li value="3">Build a SensorBoardDiagnostics struct (state OPERATING)</li><li value="4">Poll pH, IMU, then load cells and pressure sensors (index loop over both units)</li><li value="5">Poll the flow sensor (rate computed from pulses counted by the EXTI ISR since the last poll)</li><li value="6">Build the pump status from commanded state, cross-checked against measured flow</li><li value="7">Wrap each sensor message in a PBEnvelope and send it as a UDP datagram to the sample board</li><li value="8">osDelay(MAIN_TASK_DELAY_MS)</li></ol>
<h2 id="bkmrk-udp-transmission">Protobuf Encoding and UDP Transmission</h2>
<p id="bkmrk-udp-desc">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.</p>
<pre id="bkmrk-udp-send-code"><code class="language-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);
}</code></pre>
<p id="bkmrk-sendudp-warning"><strong>Important:</strong> the static flag <code>sendUDP</code> in main.c is currently <code>false</code>, so envelope encoding and transmission are skipped entirely. Set it to true to actually transmit. There is a similar development flag <code>skip_sensor_polling</code> (currently false) that disables all sensor polling when true.</p>
<h2 id="bkmrk-packet-dispatcher">Inbound Packet Dispatcher</h2>
<p id="bkmrk-dispatcher-desc">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:</p>
<table id="bkmrk-dispatcher-table"><colgroup><col><col><col></colgroup><tbody><tr><th><p>Envelope tag</p></th><th><p>Handler</p></th><th><p>Behavior</p></th></tr><tr><td><p>ph_info</p></td><td><p>handle_sensor_ph_info</p></td><td><p>Log only</p></td></tr><tr><td><p>imu_info</p></td><td><p>handle_sensor_imu_info</p></td><td><p>Log only</p></td></tr><tr><td><p>load_cell_info</p></td><td><p>handle_sensor_load_cell_info</p></td><td><p>Log only</p></td></tr><tr><td><p>pressure_info</p></td><td><p>handle_sensor_pressure_info</p></td><td><p>Log only</p></td></tr><tr><td><p>pump_info</p></td><td><p>handle_sensor_pump_command</p></td><td><p>Actuates the pump: applies enabled, direction, and speed_percent to the hardware</p></td></tr></tbody></table>
<h2 id="bkmrk-status-model">Sensor Status Model</h2>
<h3 id="bkmrk-sensor-state-codes">SensorState (operating state)</h3>
<table id="bkmrk-state-table"><colgroup><col><col></colgroup><tbody><tr><th><p>Code</p></th><th><p>Meaning</p></th></tr><tr><td><p>SENSOR_IDLE</p></td><td><p>Not connected, not implemented, or intentionally off</p></td></tr><tr><td><p>SENSOR_OPERATING</p></td><td><p>Normal operation, valid data</p></td></tr><tr><td><p>SENSOR_ERROR</p></td><td><p>Communication failure or invalid data</p></td></tr></tbody></table>
<h3 id="bkmrk-sensor-status-codes">SensorStatus (connection status)</h3>
<table id="bkmrk-status-table"><colgroup><col><col></colgroup><tbody><tr><th><p>Code</p></th><th><p>Meaning</p></th></tr><tr><td><p>STATUS_OK</p></td><td><p>Healthy</p></td></tr><tr><td><p>STATUS_DISCONNECTED</p></td><td><p>No hardware detected (poll returned UNIMPLEMENTED or COMMS)</p></td></tr><tr><td><p>STATUS_ERROR</p></td><td><p>Unexpected failure</p></td></tr><tr><td><p>STATUS_INITIALIZING</p></td><td><p>Warming up (flow sensor first sample window)</p></td></tr></tbody></table>
<h3 id="bkmrk-poll-result-mapping">Poll Result Mapping</h3>
<p id="bkmrk-poll-mapping-desc">handle_sensor_poll_result() maps driver results uniformly:</p>
<ul id="bkmrk-poll-mapping-list"><li value="1">RESULT_ERR_UNIMPLEMENTED or RESULT_ERR_COMMS: state IDLE, status DISCONNECTED (sensor not connected or driver not wired to hardware yet)</li><li value="2">Any other non-OK result: state ERROR, status ERROR</li><li value="3">RESULT_OK: caller then validates the data and picks OPERATING or ERROR plus a driver-specific error code</li></ul>
<p id="bkmrk-log-format-desc">Every sensor produces one uniform log line per loop: <code>name | STATUS | STATE | detail</code>.</p>
<h2 id="bkmrk-pump-crosscheck">Pump Health Cross-Check</h2>
<p id="bkmrk-pump-crosscheck-desc">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:</p>
<ul id="bkmrk-pump-crosscheck-list"><li value="1">Not initialised: ERROR / ERROR</li><li value="2">Commanded off (disabled or 0 percent): IDLE / OK (healthy, intentionally off)</li><li value="3">Commanded on and flow detected: OPERATING / OK</li><li value="4">Commanded on and no flow: OPERATING / DISCONNECTED (pump absent, dry, stalled, or the flow sensor is not installed)</li></ul>
<h2 id="bkmrk-memory-layout">Memory Layout</h2>
<table id="bkmrk-memory-table"><colgroup><col><col></colgroup><tbody><tr><th><p>Region</p></th><th><p>Use</p></th></tr><tr><td><p>FreeRTOS heap, 64 KB</p></td><td><p>Task stacks, queues, protobuf encode buffers (malloc/free per message)</p></td></tr><tr><td><p>0x30000000, 32 KB, MPU non-cacheable</p></td><td><p>Ethernet DMA descriptors and buffers</p></td></tr><tr><td><p>0x30004900, 16 KB</p></td><td><p>LwIP RAM heap (MEM_SIZE)</p></td></tr><tr><td><p>Static queues</p></td><td><p>Two UDP send queues, 80 entries each, allocated at compile time (xQueueCreateStatic)</p></td></tr></tbody></table>
<h2 id="bkmrk-error-handling-strategy">Error Handling Strategy</h2>
<ul id="bkmrk-error-strategy-list"><li value="1">Drivers never crash the loop: missing hardware degrades to IDLE/DISCONNECTED and the loop continues</li><li value="2">Each sensor unit reports independently (separate envelope, separate error code enum)</li><li value="3">Heap exhaustion protection: below 4096 bytes free, polling pauses for 10 s per iteration</li><li value="4">Encode failures are logged with result_to_short_str / result_to_desc_str and the buffer is freed in all paths</li></ul>