Architecture
<p id="bkmrk-arch-intro">Complete system overviewoverview: showingFreeRTOS FreeRTOS,task model, the sensor polling loop, protobuf encoding, UDP transmission, inbound packet dispatch, and the memory layout. IncludesAll initializationapplication 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 errorPA6/PB5), handlingeach strategy.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
whileid="bkmrk-phase4-desc">The loop runs forever with a 5000 ms period (<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 STEPvalue="2">Toggle 1:the Systemthree HealthLEDs Check(visual heartbeat)</li><li value="3">Build a SensorBoardDiagnostics struct (state OPERATING)</li><li -value="4">Poll CheckpH, 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 (critical:buffer that is freed after sending.<8KB)/p>
<pre -id="bkmrk-udp-send-code"><code Toggleclass="language-c">static statusvoid LEDsudp_send_envelope(uint8_t //dest_ip[4], -PBEnvelope Send raw Ethernet beacon
// STEP 2: Initialize Diagnostics Message
SensorBoardDiagnostics diagnostics_msg;
// STEP 3: Poll All Sensors
// - pH Sensor
// - GPS Sensor
// - IMU Sensor
// - Load Cells (x2)
// - Pressure Sensors (x2)
// STEP 4: Encode & Transmit
// - Encode diagnostics to Protobuf
// - Send UDP broadcast (192.168.0.255:7)
// STEP 5: Wait
osDelay(5000); // 5 seconds
}
Error Handling Strategy
Polling Error States
if (poll_result == RESULT_ERR_UNIMPLEMENTED)*env) {
= SENSOR_IDLE;
sensor.error_code = COMMUNICATION_FAILURE;
} else if (poll_result == RESULT_ERR_COMMS)!sendUDP) { sensor.statereturn; } /* transmit gate, false by default */
uint8_t *encoded = SENSOR_ERROR;NULL;
size_t size = COMMUNICATION_FAILURE;0;
else result_t result = pb_message_encode(env, PBEnvelope_fields, &encoded, &size);
if (poll_resultresult == RESULT_OK) {
ETH_udp_send(dest_ip, PORT, encoded, (validate_sensor_data(value)uint16_t)size, ==1);
{
sensor.state = SENSOR_OPERATING;
sensor.error_code = NO_ERROR; }
{ sensor.statefree(encoded);
}</code></pre>
<p id="bkmrk-sendudp-warning"><strong>Important:</strong> SENSOR_ERROR;the sensor.error_codestatic 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 INVALID_DATA;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 CodesModel</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></td><td><p>Not |
| </p></td><td><p>Normal operation, valid data |
| </p></td><td><p>Communication failure or invalid data</p></td></tr></tbody></table> |
Initialization
<h3 Sequence
Phase 1: Hardware Setup
1. MPU/Cache configuration
2. HAL initialization
3. System clock → 480 MHz
4. RTOS kernel init
5. GPIO initialization
6. Timer initializationid="bkmrk-sensor-status-codes">SensorStatus (Phasestatus)
</h3>
<table Communicationid="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 Setuphardware
1. UART initializationdetected (115200poll baud)returned 2.UNIMPLEMENTED Loggingor systemCOMMS)</p></td></tr><tr><td><p>STATUS_ERROR</p></td><td><p>Unexpected <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 PHYDMA initdescriptors and buffers</p></td></tr><tr><td><p>0x30004900, 16 KB</p></td><td><p>LwIP RAM heap (LAN8742)MEM_SIZE)</p></td></tr><tr><td><p>Static 4. MAC address filtering
5. ARP table setup
Phase 3: Application Setup
1. Packet dispatcher registration
2. Sensor initialization
3.queues</p></td><td><p>Two UDP Phaseat 4:compile Maintime Loop
(xQueueCreateStatic)</p></td></tr></tbody></table>
1.<h2 LEDid="bkmrk-error-handling-strategy">Error initializationHandling 2.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 startspauses 3.for Continuous10 5-seconds cycles
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>