Repository files navigation

PoliTOcean Float 2025 - Technical Documentation

CI

Version: 11.2.0 Team: PoliTOcean @ Politecnico di Torino
Maintainers: Colabella Davide, Benevenga Filippo
Competition: MATE ROV 2025/26


TABLE OF CONTENTS


PROJECT OVERVIEW

Introduction and Requirements

By MATE 2026 requirements documentation (Task 4.1 - MATE Floats Under the Ice), the FLOAT must complete operational vertical profiling missions under simulated ice conditions.

Pre-Deployment Requirements:

  • The FLOAT must communicate with the Mission Station (CS) prior to descending, transmitting a defined data packet containing:
    • Company number (provided by MATE)
    • Time data (UTC/local/float time)
    • Pressure data (pa or kpa) and/or depth data (m or cm)
    • Additional data as required
  • Example packet: EX01 1:51:42 UTC 9.8 kpa 1.00 meters

Vertical Profile Requirements:

The FLOAT must complete two vertical profiles using a buoyancy engine (fluid displacement system, not thrusters). Each profile consists of:

  1. Descent Phase: Descend from surface to 2.5 meters depth (± 33 cm)
  2. Deep Hold: Maintain depth at 2.5 meters for 30 seconds (bottom of float as reference)
  3. Ascent Phase: Rise to 40 cm depth (± 33 cm) without breaking surface or contacting ice
  4. Shallow Hold: Maintain depth at 40 cm for 30 seconds (top of float as reference)

Data Collection & Transmission:

  • Collect depth/pressure measurements during both profiles and transmit judge packets every 5 seconds (minimum 20 data packets)
  • Store data in ESP32 internal flash as a LittleFS CSV containing: company number, profile id, time, pressure, judge/reference depth, phase, and raw sensor depth
  • After recovery, transmit all collected data wirelessly to the Mission Station
  • Data packets must show 7 sequential measurements (spanning 30 seconds at 5-second intervals: 0, 5, 10, 15, 20, 25, 30) confirming proper depth maintenance at both 2.5m and 0.4m

Post-Mission Requirements:

  • Upon surface recovery, autonomously transmit all profile data to the CS
  • CS GUI plots depth over time using received data (minimum 20 data packets required)
  • Graph must display time (X-axis) vs depth (Y-axis) for both completed profiles

Current firmware storage note: the active implementation uses the internal flash CSV log (FLASH_LOG_PATH) as the primary mission data source. EEPROM compact records remain only as an internal legacy buffer. The legacy serial command name is still CLEAR_SD, but it now resets the flash CSV log and the legacy EEPROM buffer.

Auto Mode (AM):

An autonomous operating mode that triggers profile execution in case of connection loss with the CS, ensuring mission completion if communication is temporarily unavailable. AM will autonomously commit up to two profiles when connection is lost, preventing incomplete missions due to transient WiFi failures.

Penalties:

  • Breaking surface or contacting ice sheet during profile: -5 points per profile
  • FLOAT must remain submerged between 40 cm and 2.5 m throughout the ascent/descent phases

System Behavior

The general idea is that the FLOAT provides some micro-services that the CS can activate by sending commands to it. Every command can be requested at any moment, with the only limit that a command can be accepted by the FLOAT only when the previous one has been completed (more info on command cycle later).

The FLOAT has two main logical states: the command execution one, and the idle one in which it waits for the new command. In idle state, the FLOAT can have buffered flash data from the last completed profile that can be sent to the CS.

Syringe / Motor Convention

The FLOAT changes its buoyancy by pulling and pushing water through a pair of syringes driven by a stepper motor through a lead screw. The mechanical convention is:

  • Home (motor_pos = 0): piston fully inserted, syringes empty of water → the FLOAT floats. At this position the TOF reads ≈ TOF_HOMING_THRESHOLD (75 mm) because the piston is far from the sensor.
  • Full extension (motor_pos = uToMotorPos(1.0f)): piston extracted, syringes full of water → the FLOAT sinks. TOF reads ≈ TOF_SAFE_RANGE_MIN_MM (40 mm).
  • PID logical convention: u ∈ [0, 1] with u = 0 → float (empty) and u = 1 → sink (full). The helper uToMotorPos(u) in include/config.h maps u to the actual motor target while respecting MOTOR_INVERT_LOGICAL, so callers never hard-code signs.

TOF safety limits used during motion:

ConstantDefaultMeaning
TOF_HOMING_THRESHOLD75 mmPhase 2 of homing stops when the TOF reads above this
TOF_HOMING_APPROACH_MM50 mmPhase 1 of homing stops when the TOF reads below this
TOF_SAFE_RANGE_MIN_MM40 mmLower bound: syringe fully extended (mechanical limit)
TOF_SAFE_RANGE_MAX_MM85 mmUpper bound: 10 mm above the homing threshold; any higher and the piston would risk hitting the back mechanical stop

Surface Target Offset

When the FLOAT is "floating", we usually want its top a few centimetres below the water surface — not exactly at the waterline — so that the float remains visible without being completely above water. This is controlled by SURFACE_TARGET_OFFSET_M (default 0.10 m: top of the float 10 cm below the surface).

Two ways to change it:

  • At compile time: edit SURFACE_TARGET_OFFSET_M in include/config.h.
  • At runtime: send command SURFACE_OFFSET <m> via the CS, or SURFACE_OFFSET <m> over USB serial on ESPA. The change persists until the next reboot.

The offset is geometry-agnostic: FLOAT_TOP_TO_SENSOR_M (geometric distance between the top of the float and the barometer) and SURFACE_TARGET_OFFSET_M (operational target) are kept as separate constants in include/config.h.


HARDWARE CONFIGURATION

Hardware Used

HardwareRoleLinkKey parameters / notes
ESP32 Dev Module x2ESPA float controller and ESPB communication bridgeEspressif ESP32Arduino framework, ESP-NOW link, USB serial bridge on ESPB
DRV8825 stepper driverStepper motor driver for syringe motionPololu DRV8825 carrierSTEP/DIR control, active-low enable, SLEEP and RESET held HIGH during operation
Stepper motor with planetary gearboxSyringe actuator motorStepperOnline 17HS15-1684S-PG27NEMA 17, 200 steps/rev, 1.8 deg/step, configured gear ratio 26.85124:1, microstep setting 1
Lead screw / threaded rodConverts motor rotation to linear travelSIENOC 500 mm trapezoidal lead screwPitch 2.0 mm, 4 starts, lead 8.0 mm/rev, configured travel 35 mm
VL53L7CX Time-of-Flight sensorNon-contact homing distance sensor (multi-zone)ST VL53L7CXI2C 0x29, LPn (XSHUT) GPIO16, GPIO1 GPIO15, 4×4 zone mode with central-zone mask 0x0660, 6 mm raw offset, 75 mm homing threshold
Bar02 pressure sensorPressure/depth measurementBlue Robotics Bar02MS5837_02BA model, I2C 0x76, used for depth and pressure
INA219 battery monitorBattery bus-voltage monitorAdafruit INA219 breakoutI2C 0x40, initialized at 100 kHz, configured with 5 A max and 0.1 ohm shunt

Pin Mapping

ESPA (Float Controller) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Motor Control
DIRGPIO32DRV8825 DirectionStepper direction control
STEPGPIO33DRV8825 StepStep pulse generation
ENGPIO27DRV8825 EnableActive-LOW, disables outputs when HIGH
SLEEPGPIO25DRV8825 SleepActive-LOW, must be HIGH for operation
RSTGPIO26DRV8825 ResetActive-LOW, must be HIGH for operation
TOF Sensor
SDAGPIO21VL53L7CX I2C DataI2C bus (shared with sensors)
SCLGPIO22VL53L7CX I2C ClockI2C bus @ 1MHz
XSHUTGPIO16VL53L7CX LPn (shutdown)Sensor enable / shutdown control
GPIO1GPIO15VL53L7CX InterruptOptional interrupt pin, unused in polling mode
Sensors
SDAGPIO21Bar02, INA219I2C bus (shared)
SCLGPIO22Bar02, INA219I2C bus (shared)
Status LED
LED_RGPIO19Red ChannelPWM control
LED_GGPIO18Green ChannelPWM control
LED_BGPIO5Blue ChannelPWM control

ESPB (Communication Bridge) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Communication
TXGPIO1USB Serial115200 baud
RXGPIO3USB Serial115200 baud
Status LED
Built-in LEDGPIO2Onboard LEDStatus indication

I2C Device Addresses:

DeviceAddressBus Speed
VL53L7CX TOF0x291 MHz
Bar02 Pressure0x76Shared I2C bus
INA219 Battery0x40Initialized at 100 kHz

The firmware initializes the INA219 at 100 kHz, then the VL53L7CX driver raises the shared Wire clock to 1 MHz for TOF ranging.


SYSTEM ARCHITECTURE

Deployment Diagram

FLOAT code has to be deployed on two ESP32, one mounted on the FLOAT board (ESPA) together with the sensors and the power supply, and the other (ESPB) communicating with the Control Station via USB. The two ESP32 communicates via WiFi using ESP-NOW protocol. The software on the two ESP32 is designed to work regardless of the design of the GUI on the CS.

The idea is to bring all the complexity on the ESPA and GUI, leaving no trace of logic on the ESPB.

graph TB
subgraph "Control Station"
GUI[GUI Application]
USB[USB Serial]
end
subgraph "ESPB - Communication Bridge"
ESPB_FW[ESPB Firmware]
ESPB_WiFi[WiFi ESP-NOW]
ESPB_LED[Built-in LED]
end
subgraph "ESPA - Float Controller"
ESPA_FW[ESPA Firmware]
ESPA_WiFi[WiFi ESP-NOW]
subgraph "Motor System"
DRV8825[DRV8825 Driver]
STEPPER[Stepper Motor]
TOF[VL53L7CX TOF Sensor]
end
subgraph "Sensors"
BAR02[Bar02 Pressure]
INA219[INA219 Battery]
end
RGB[RGB LED]
I2C[I2C Bus]
end
GUI -->|Commands| USB
USB <-->|Serial 115200| ESPB_FW
ESPB_FW <-->|ESP-NOW 2.4GHz| ESPB_WiFi
ESPB_WiFi <-.->|WiFi| ESPA_WiFi
ESPA_WiFi <-->|ESP-NOW| ESPA_FW
ESPA_FW -->|Control Signals| DRV8825
DRV8825 -->|STEP/DIR| STEPPER
TOF -->|Distance Data| ESPA_FW
BAR02 -->|I2C| I2C
INA219 -->|I2C| I2C
I2C -->|Sensor Data| ESPA_FW
ESPA_FW -->|Status| RGB
ESPB_FW -->|Status| ESPB_LED
style ESPA_FW fill:#4CAF50
style ESPB_FW fill:#2196F3
style GUI fill:#FF9800
style TOF fill:#9C27B0
Loading

Software Structure

The project follows a modular architecture with separate compilation units:

  • Central Config (include/config.h) - pin mapping, motor constants, PID defaults, mission timing, network parameters
  • Shared Protocol (include/float_common.h) - ESP-NOW packet structs, ACK strings, EEPROM size, shared LED enum
  • Motor Control (lib/motor) - DRV8825/FastAccelStepper setup, position tracking, bounded movement primitives
  • TOF Sensor (lib/tof) - VL53L7CX initialization (4×4 multi-zone), aggregated minimum-distance reading with raw offset compensation
  • Motion Control (lib/motion_control) - TOF homing, safe max-extension move, balance routine, emergency stop handling
  • Communication (lib/comms) - ESP-NOW wireless protocol and ElegantOTA session management
  • Sensors (lib/sensors) - Bar02 pressure/depth and INA219 battery monitoring
  • PID Controller (lib/pid) - depth control algorithm with runtime gain updates
  • Profile Manager (lib/profile) - mission profile execution and flash-backed mission logging
  • Flash Storage (lib/flash_storage) - LittleFS CSV mission log and replay helpers
  • LED Controller (lib/led) - RGB status indication system

ESPA State Machine

The ESPA firmware operates as a state machine coordinating motor control, sensors, and communications:

stateDiagram-v2
[*] --> INIT: Power On
INIT --> HOMING: Sensors OK
INIT --> ERROR: Init Failed
HOMING --> IDLE: Homing Success
HOMING --> ERROR: Homing Failed/Timeout
IDLE --> EXECUTING: Command Received
IDLE --> IDLE: No Command
EXECUTING --> PROFILE: GO Command
EXECUTING --> BALANCE: BALANCE Command
EXECUTING --> SEND_DATA: LISTENING Command
EXECUTING --> CLEAR_DATA: CLEAR_SD Command
EXECUTING --> UPDATE_PID: PARAMS Command
EXECUTING --> UPDATE_PID_EXT: PARAMS_EXT Command
EXECUTING --> TEST_SPEED: TEST_FREQ Command
EXECUTING --> TEST_STEPS: TEST_STEPS Command
EXECUTING --> DEBUG_MODE: DEBUG Command
EXECUTING --> HOMING: HOME_MOTOR Command
EXECUTING --> OTA: TRY_UPLOAD Command
EXECUTING --> SYRINGE_SET: SYRINGE_SET Command
EXECUTING --> PID_HOLD: PID_HOLD Command
EXECUTING --> PID_STEP: PID_STEP Command
EXECUTING --> SET_SURFACE_OFFSET: SURFACE_OFFSET Command
PROFILE --> PID_CONTROL: Descending
PID_CONTROL --> PID_CONTROL: Depth Control Active
PID_CONTROL --> ASCENT: Target Reached/Timeout
ASCENT --> IDLE_W_DATA: At Surface
BALANCE --> IDLE: Balance Complete
SEND_DATA --> IDLE: Data Sent
CLEAR_DATA --> IDLE: Flash Log Cleared
UPDATE_PID --> IDLE: Gains Updated
UPDATE_PID_EXT --> IDLE: Period/alpha Updated
TEST_SPEED --> IDLE: Speed Stored
TEST_STEPS --> IDLE: Test Move Complete
DEBUG_MODE --> IDLE: Debug Toggle Complete
OTA --> IDLE: Upload Complete
SYRINGE_SET --> IDLE: Bench Test Complete
PID_HOLD --> IDLE: Hold Complete / Timeout
PID_STEP --> IDLE: Step Complete / Timeout
SET_SURFACE_OFFSET --> IDLE: Offset Stored
IDLE_W_DATA --> SENDING: LISTENING Command
SENDING --> IDLE: Data Transmitted
ERROR --> [*]: Manual Reset Required
note right of IDLE
RGB: Green Solid
Waiting for command
end note
note right of HOMING
RGB: Purple Blink
TOF-based homing
end note
note right of PID_CONTROL
RGB: Cyan Blink
Active depth control
end note
note right of ERROR
RGB: Red Blink
Fatal error state
end note
Loading

COMMUNICATION PROTOCOL

Command Lifecycle

A command life-cycle does not overlaps/interfere with the previous nor the next one: when the CS sends a command (and it arrives to the FLOAT), a feedback from the FLOAT should inform about the acceptance of the command and if this acknowledgement arrives within a given period (specified later), next command requests will be ignored until end of execution of the current one, signaled by an idle acknowledgement.

If the acknowledgement doesn't arrive within that time span, the command commit can be considered failed: this could happen for WiFi connection failures or FLOAT electronics issues.

When waiting for the command commit acknowledgement, other command requests will be ignored as well.

As already mentioned, after command completion the FLOAT will try to send an idle acknowledgement to signal that it is listening for a new command: together with the idle state, this acknowledgement can also inform about the presence of new flash-backed profile data that has to be sent to the CS. After an idle acknowledgement is received, a new command can be accepted.

To maintain consistency with the status stored on the ESPB, and hence with the GUI visuals, the FLOAT grants to send the acknowledgement signalling a command commit only when the commit can be given for sure. In the same way, if the acknowledgement fails to be sent due to connection issues, the command is not committed.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant WiFi as ESP-NOW
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Fresh State
CS->>ESPB: Send Command (e.g., "GO")
ESPB->>WiFi: Forward Command
WiFi->>ESPA: Deliver Command
ESPA->>ESPA: Validate & Accept
ESPA->>WiFi: ACK (e.g., "GO_RECVD")
WiFi->>ESPB: Deliver ACK
ESPB->>ESPB: Update State (status=2)
ESPB->>CS: Forward ACK
Note over ESPA: Executing Command...
ESPA->>ESPA: Complete Task
ESPA->>WiFi: Completion ACK (e.g., "FLOAT_IDLE")
WiFi->>ESPB: Deliver Completion
ESPB->>ESPB: Update State (status=0)
ESPB->>CS: Forward Completion
Note over CS: Ready for Next Command
Loading

ESPB Bridge Role

The ESPB role is only to make the CS task of continuously checking on the WiFi channel less resource consuming.

In particular, ESPB receives commands from CS via USB only to forward them to the FLOAT via WiFi. At the same time, it can receive feedback and data from the FLOAT. In the latter case the ESPB will forward the packages on the USB channel, while using them to update an internal state accordingly. The only code that should trigger the update of the state stored on ESPB is the firmware on the FLOAT, via the data and the feedback sent to CS (more later). This is because no assumptions have to be done by other software on the commands completion and acceptance.

The ESPB state should mirror the FLOAT state at each moment (more details later) and can be used by the GUI to give visual feedback to the user or to drive its internal logic. It can be requested to the ESPB by the CS at any moment with a specific command. The state info sent to the CS with this command also contains WiFi connection state info and AM activation state info.

In general, the ESPB feedback could be stale when requested (for example because the CS could poll it with a low frequency), so to get fresh, real-time data the CS should listen on the USB channel for the FLOAT packages, after a command request or when waiting for command completion and retrieve the FLOAT current state directly by those packages.

Periodic polling remains a legit choice in case the CS cannot exploit interrupts triggered by Serial connection, but notice that this solution leads to delayed GUI visual feedback with respect to the changes on the FLOAT state.

In some cases, connection losses can undermine consistency between the feedback of the ESPB (either they are polled or real time) and the real current FLOAT state (consistency threats for each FLOAT state later).

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Stale State Polling
CS->>ESPB: Send Command
ESPB->>ESPA: Forward Command
ESPA->>ESPA: Accept & Execute
ESPA->>ESPB: ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward ACK
Note over ESPA: Command Executing...
loop Periodic Polling
CS->>ESPB: Request STATUS
ESPB->>CS: Return Cached State
Note over CS: State may be stale<br/>if ESPA completed recently
end
ESPA->>ESPA: Complete Command
ESPA->>ESPB: Completion ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward Completion
CS->>ESPB: Request STATUS
ESPB->>CS: Return Fresh State
Loading

FLOAT Commands

Table of FLOAT commands with relative effects and acknowledgements:

Cmd stringCmd ESPA numberCmd effectsESPA ack stringESPA ack effects on ESPB state
GO1Performs the two MATE vertical profiles, sends the pre-descent data packet before the first descent, and logs pressure/depth records to flash CSVGO_RECVDstatus to 2 (command execution)
LISTENING2Streams flash CSV records as JSON data packets at 5-second cadence, followed by STOP_DATAAck is data itselfstatus to 2 after first package arrival
BALANCE3Cycles full extension and retraction with holdMs holds until Bar02 pressure rises above the startup baseline by BALANCE_STOP_PRESSURE_DELTA_KPA. Requires the motor to be homed first — otherwise the command fails with Balance: homing requiredCMD3_RECVDstatus to 2
CLEAR_SD4Clears and recreates the flash CSV log, and clears the legacy EEPROM buffer. The command string is kept as CLEAR_SD for compatibilityCMD4_RECVDstatus to 2
SWITCH_AUTO_MODE5Toggles FLOAT Auto ModeSWITCH_AM_RECVDstatus to 2, AM activation state toggled
SEND_PACKAGE6Sends a single live JSON snapshot containing company number, time, pressure, judge/reference depth, phase, and raw sensor depthAck is the package itselfstatus to 2
TRY_UPLOAD7Starts the ElegantOTA access point on ESPA for a 5-minute upload window, then restores ESP-NOWTRY_UPLOAD_RECVDstatus to 2
PARAMS kp ki kd8Updates PID gains at runtimeCHNG_PARMS_RECVDstatus to 2
TEST_FREQ freq9Sets manual test movement speed, clamped to 10-1200 steps/sTEST_FREQ_RECVDstatus to 2
TEST_STEPS n10Moves the motor by n relative steps at the current test speedTEST_STEPS_RECVDstatus to 2
DEBUG11Toggles remote debug forwarding through DebugSerialDEBUG_MODE_RECVDstatus to 2
HOME_MOTOR12Runs TOF-based homing remotelyHOME_RECVDstatus to 2
STOP13Triggers a remote emergency stop, stops the motor, disables outputs, and returns to idleSTOP_RECVDstatus to 2
PARAMS_EXT period alpha14Updates PID tick period (ms) and derivative LPF coefficient alphaD at runtimeCHNG_PID_EXT_RECVDstatus to 2
SYRINGE_SET u dur_s15Bench test: drives the syringe to normalized position u ∈ [0,1] for dur_s seconds, logging depth — bypasses the PID (DC gain / time-constant characterization)SYRINGE_SET_RECVDstatus to 2
PID_HOLD depth dur_s16Bench test: holds depth at depth_m for dur_s seconds with the PID active, logging at 5 HzPID_HOLD_RECVDstatus to 2
PID_STEP depth17Bench test: step response — drives the PID to depth_m for up to 60 s, logging at 10 HzPID_STEP_RECVDstatus to 2
SURFACE_OFFSET m18Sets the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime: the FLOAT will hold its top m metres below the waterline when "floating" (default 0.10)SURFACE_OFF_RECVDstatus to 2
STATUS-Requests stale ESPB status plus AM state, WiFi connection state, battery millivolts, and last RSSI--

Once a command is completed, ESPA acknowledgement can be:

ESPA ack stringESPA ack effects on ESPB stateESPA state
FLOAT_IDLEstatus to 0 (idle)Idle with no data to be sent
FLOAT_IDLE_W_DATAstatus to 1 (idle with data to be sent)Idle with data from last profile to be sent

STATUS COMMAND: ESPB RESPONSE

ESPB response to STATUS command is composed by five parts of information: ESPA state (stale), activation of the AM on the FLOAT, WiFi connection state, last received battery millivolts, and last received RSSI. The WiFi connection state is detected by sending a dummy command code 0, while the other states are kept consistent with the ones on the FLOAT by updating them after acknowledgements reception.

ESPA state:

ESPB state stringESPB state numberState description
UNKNOWN-1ESPB has not received any state message from ESPA since boot
CONNECTED0The FLOAT is listening for new command. Previous command succeeded
CONNECTED_W_DATA1The FLOAT is listening for new command and has some new data from last profile to be sent. Previous command succeeded
EXECUTING_CMD2FLOAT is executing a command
STATUS_ERROR-Internal error in reading the state number

WARNING:
If committing a profile automatically, the relative acknowledgement will likely fail due to connection loss. The profile is committed anyway as it is generated from connection loss in the first place, but the GUI may not have mean to detect it. So it will likely read an inconsistent idle status (CONNECTED or CONNECTED_W_DATA) until FLOAT is at water level with a stable WiFi connection. In the meantime the command commits will fail, for connection loss or because the FLOAT is underwater. Anyway WiFi connection state can be detected by the STATUS command, hence giving feedback on status consistency.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over ESPA: Auto Mode Active
Note over ESPA,ESPB: Connection Lost!
ESPA->>ESPA: Detect Connection Loss
ESPA->>ESPA: Auto-commit Profile
Note over ESPA: Descending...<br/>WiFi Unavailable
ESPA-xESPB: ACK Fails (No Connection)
Note over ESPB: State Becomes Inconsistent<br/>Still shows "IDLE"
CS->>ESPB: Request STATUS
ESPB->>CS: CONNECTED | CONN_LOST
Note over CS: GUI shows inconsistent state<br/>but WiFi loss detected
Note over ESPA: At Surface...<br/>WiFi Restored
ESPA->>ESPB: FLOAT_IDLE_W_DATA
ESPB->>ESPB: Update to Consistent State
ESPB->>CS: Forward State
Note over CS,ESPA: Consistency Restored
Loading

AM state:

ESPB state stringState description
AUTO_MODE_YESAM on FLOAT is activated
AUTO_MODE_NOAM on FLOAT is not activated: connection losses will not trigger profiles

WiFi connection state:

ESPB state stringState description
CONN_OKWiFi connection is ok
CONN_LOSTWiFi connection is currently down. ESPB state could be wrong

Battery and RSSI fields:

ESPB fieldState description
BATTERY: <mV>Last battery voltage received from ESPA acknowledgements
RSSI: <dBm>Last ESP-NOW packet RSSI captured by ESPB promiscuous callback

Example of ESPB state response:CONNECTED_W_DATA | AUTO_MODE_NO | CONN_OK | BATTERY: 12450 | RSSI: -63

At ESPB boot, before any ESPA packet is received, a status request may return: UNKNOWN | AUTO_MODE_NO | CONN_LOST | BATTERY: 0 | RSSI: 0.

GUI / ESPB / ESPA Protocol Contract

The GUI sends command strings to ESPB over USB serial. ESPB parses the string, sends the command number to ESPA over ESP-NOW, and forwards ESPA acknowledgements/data back to the GUI.

GUI commandESPA command numberESPA acknowledgement / response
GO1GO_RECVD
LISTENING2Stored data packets, then STOP_DATA
BALANCE3CMD3_RECVD
CLEAR_SD4CMD4_RECVD
SWITCH_AUTO_MODE5SWITCH_AM_RECVD
SEND_PACKAGE6Live JSON packet
TRY_UPLOAD7TRY_UPLOAD_RECVD
PARAMS kp ki kd8CHNG_PARMS_RECVD
TEST_FREQ freq9TEST_FREQ_RECVD
TEST_STEPS n10TEST_STEPS_RECVD
DEBUG11DEBUG_MODE_RECVD
HOME_MOTOR12HOME_RECVD
STOP13STOP_RECVD
PARAMS_EXT period_ms alpha_d14CHNG_PID_EXT_RECVD
SYRINGE_SET u dur_s15SYRINGE_SET_RECVD
PID_HOLD depth_m dur_s16PID_HOLD_RECVD
PID_STEP depth_m17PID_STEP_RECVD
SURFACE_OFFSET m18SURFACE_OFF_RECVD
STATUS-ESPB local status line with five `

The peer MAC addresses are configured centrally in include/config.h: MAC_ESPA is used by ESPB, and MAC_ESPB is used by ESPA.


LED STATUS INDICATORS

The FLOAT is equipped with RGB LEDs on both ESP32 boards that provide visual feedback about the system status:

ESPA (Float Board) LED States:

Driven by LEDState (scoped enum in lib/led/include/led.h):

LED Color/PatternStateDescription
Green Solid / Boot BlinksLEDState::INITSystem initializing
Green SolidLEDState::IDLEReady and idle, waiting for commands
Green BlinkLEDState::IDLE_WITH_DATAIdle with data ready to send
Red SolidLEDState::LOW_BATTERYBattery voltage below BATT_THRESH (12.0 V)
Red BlinkLEDState::ERRORError state or motor emergency stop
Blue SolidLEDState::PROFILERunning non-PID profile phase
Yellow BlinkLEDState::AUTO_MODEAuto mode active
Purple BlinkLEDState::HOMINGMotor homing in progress
Purple SolidLEDState::MOTOR_MOVINGMotor moving
Cyan BlinkLEDState::PID_CONTROLPID depth control active
White SolidLEDState::COMMUNICATIONCommand received / communicating with ESPB
Orange BlinkLEDState::OTA_MODEOTA update mode active
OffLEDState::OFFSystem off or disabled

ESPB uses a separate FloatLEDState enum (LED_* prefix) defined in include/float_common.h; the two enums are deliberately independent because the two boards have different LED states to signal.

ESPB (Communication Bridge) LED States:

LED PatternStateDescription
Solid OnLED_IDLEConnected and ready
Very Fast BlinkLED_ERRORCommunication error
OffLED_OFFSystem off or disabled

Note: ESPB uses the built-in LED (pin 2) with different blink patterns to indicate status, as it does not have external RGB connections.


DEVELOPMENT AND TESTING

PlatformIO Environments

EnvironmentPurposeMain Source
espAFloat controller firmware with sensors, TOF homing, motion control, PID, ESP-NOW, and OTAsrc/espA/main.cpp
espA_poolESPA firmware compiled with conservative 70 cm pool-test targets (POOL_TEST_PROFILE)src/espA/main.cpp
espBUSB-to-ESP-NOW bridge for the Control Stationsrc/espB/main.cpp
espA_manual_keyboardBench firmware for serial keyboard continuous motor movement without homingsrc/espA_manual_keyboard/main.cpp

Common commands:

pio run -e espA
pio run -e espA_pool
pio run -e espB
pio run -e espA_manual_keyboard
pio test -e espA

CLI Startup

Run all commands from the project root:

cd Float_2025

To build and upload the main firmware targets:

pio run -e espA -t upload
pio run -e espB -t upload

For a conservative shallow-pool test at about 70 cm, upload ESPA with:

pio run -e espA_pool -t upload

To open the serial monitor at 115200 baud:

pio device monitor -e espA
pio device monitor -e espB

Direct USB Tuning Commands (ESPA)

All commands in the FLOAT Commands table can be sent over the ESPB USB serial bridge using the same string syntax. The commands below — useful for bench tuning — can also be sent directly over ESPA's USB serial port (e.g. when ESPA is wired to a laptop for tuning runs), bypassing ESPB and ESP-NOW entirely.

CommandEffect
PARAMS <kp> <ki> <kd>Update PID gains at runtime (same effect as command 8)
PARAMS_EXT <period_ms> <alpha_d>Update PID tick period and derivative LPF coefficient (command 14)
SYRINGE_SET <u> <dur_s>Drive the syringe to position u ∈ [0,1] for dur_s seconds and log depth — bypasses the PID, useful for DC-gain and time-constant estimation (command 15)
PID_HOLD <depth_m> <dur_s>Hold PID at depth_m for dur_s seconds, log at 5 Hz (command 16)
PID_STEP <depth_m>Step response: PID at depth_m for up to 60 s, log at 10 Hz (command 17)
SURFACE_OFFSET <m>Set the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime (command 18)

CLI Tests

To run all available tests for the espA environment:

pio test -e espA

To run a single test:

pio test -e espA -f unit_hw/motor/test_max_steps
pio test -e espA -f unit_hw/motor/test_speed
pio test -e espA -f integration/test_screw_lead_20mm
pio test -e espA -f integration/test_homing_only
pio test -e espA -f integration/test_tof_reading
pio test -e espA -f integration/test_homing_move_to_max
pio test -e espA -f integration/test_motor_direction
pio test -e espA -f integration/test_tof_motor_accuracy

To test ESPB without ESPA powered on:

pio test -e espB -f unit_hw/espb_bridge/test_parser
pio test -e espB -f unit_hw/espb_bridge/test_status_format
pio test -e espB -f unit_hw/espb_bridge/test_protocol_contract

To test the real ESPB-to-ESPA bridge, first upload the real espA firmware, wait until ESPA is idle, then run:

pio test -e espB -f integration/test_espnow_bridge

This test only uses the dummy command 0 and SWITCH_AUTO_MODE; it does not start profiles or move the motor.

Available tests:

TestCommandWhat it checks
test_max_stepspio test -e espA -f unit_hw/motor/test_max_stepsMoves only the motor to the safe maximum extension starting from logical position 0
test_speedpio test -e espA -f unit_hw/motor/test_speedMoves only the motor through 6 alternating 40 mm moves, increasing speed and acceleration up to 2300
test_screw_lead_20mmpio test -e espA -f integration/test_screw_lead_20mmRuns TOF homing, moves the motor by 20 mm, and compares the internal TOF delta
test_motor_directionpio test -e espA -f integration/test_motor_directionMoves only the motor forward/backward and verifies the logical direction; by default it does not use TOF
test_tof_readingpio test -e espA -f integration/test_tof_readingInitializes only the TOF sensor and checks valid readings for about 30 s
test_homing_onlypio test -e espA -f integration/test_homing_onlyRuns only TOF-based homing
test_homing_move_to_maxpio test -e espA -f integration/test_homing_move_to_maxRuns TOF homing and then moves to the safe maximum extension
test_tof_motor_accuracypio test -e espA -f integration/test_tof_motor_accuracyCompares TOF distance and motor position after homing
test_parserpio test -e espB -f unit_hw/espb_bridge/test_parserVerifies GUI/Serial command parsing into ESPA packets without ESPA powered on
test_status_formatpio test -e espB -f unit_hw/espb_bridge/test_status_formatVerifies ESPB cached state and the five-field STATUS format
test_protocol_contractpio test -e espB -f unit_hw/espb_bridge/test_protocol_contractLocks the command/ACK consistency contract between GUI, ESPB, and ESPA
test_espnow_bridgepio test -e espB -f integration/test_espnow_bridgeVerifies real ESP-NOW with the real ESPA firmware powered on, without movement

The test_max_steps, test_speed, and test_motor_direction tests are useful when you need to move only the motor without TOF homing. Before running them, make sure the piston is away from the mechanical end stops and can move in both directions.

Manual Motor-Only Control

To upload the bench firmware that lets you move the motor from the serial keyboard:

pio run -e espA_manual_keyboard -t upload
pio device monitor -e espA_manual_keyboard

Commands in the serial monitor:

KeyAction
Up arrow or wHold to move toward home/up
Down arrow or sHold to move toward extension/down
Space or xStop immediately and disable motor outputs
pPrint the current position
tPrint one TOF reading
h or ?Print help

This firmware does not run homing: at startup it assigns a centered logical position and moves while it receives repeated keypresses; when the key is released it stops automatically after a short timeout. During movement it periodically prints motor position and TOF distance. Use it only when the mechanism is in a physically safe position.

Test Layout

Hardware-oriented tests are stored under test/:

  • test/unit_hw/motor/test_max_steps checks safe maximum extension from a known zero
  • test/unit_hw/motor/test_speed checks alternating 40 mm moves while speed and acceleration increase up to 2300
  • test/integration/test_screw_lead_20mm checks the configured screw pitch, starts, and lead with one 20 mm move measured internally by TOF
  • test/integration/test_tof_reading checks that the TOF sensor initializes and returns valid distance samples for about 30 seconds
  • test/integration/test_homing_only checks TOF-based homing
  • test/integration/test_homing_move_to_max checks homing followed by safe full extension
  • test/integration/test_motor_direction checks logical/physical motion direction, optionally using TOF
  • test/integration/test_tof_motor_accuracy checks TOF and motor movement consistency

Continuous Integration

GitHub Actions builds all three PlatformIO environments (espA, espB, espA_pool) on every push to any branch and on every pull request to master. Workflow file: .github/workflows/ci.yml.

CI does not run the unit_hw/ or integration/ PlatformIO tests because they need a real ESP32 with the float wired up. Run those locally on the bench.

Pushing a v* tag triggers .github/workflows/release.yml, which builds all three environments and attaches the resulting firmware.bin / firmware.elf to a GitHub Release auto-named after the tag.

See CONTRIBUTING.md for the full git workflow (trunk-based with PR review on master), commit conventions, and one-time branch protection setup.


UTILITIES AND RESOURCES

Arduino Library Repositories:

Development Tools:

  • PlatformIO IDE: Modern embedded development platform
  • ESP32 Arduino Core: Framework for ESP32 development
  • FastAccelStepper Library: Timer/task-driven stepper motor control
  • VL53L7CX Library: Multi-zone Time-of-Flight sensor driver

GLOSSARY

  • AM (Auto Mode): Autonomous operation mode that triggers profiles on connection loss
  • CS (Control Station): Ground-based computer running the GUI application
  • ESPA: ESP32 mounted on the Float board (primary controller)
  • ESPB: ESP32 communication bridge between Float and CS
  • Flash Profile Log: Current onboard LittleFS CSV storage used before JSON transmission
  • FastAccelStepper: Timer/task-driven stepper library used by MotorController
  • MotionController: Firmware layer that combines motor, TOF, LEDs, debug, timeouts, and emergency stops for safe movement routines
  • Commit a command: To accept a sent command. After commit, command execution and success is ideally granted
  • Complete a command: To execute all the requirements requested by a command
  • Profile: A complete mission cycle (descent → depth control → ascent → data transmission)
  • TOF (Time-of-Flight): Non-contact distance measurement technology using light pulses
  • Homing: Process of establishing the motor's zero reference position
  • PID Control: Proportional-Integral-Derivative controller for precise depth maintenance
  • ESP-NOW: Low-latency peer-to-peer WiFi communication protocol by Espressif

Documentation Version: 11.2.0 Last Updated: May 2026

Recent changes:

  • PID output normalized to u ∈ [0, 1] (fraction of syringe travel). Default gains Kp = 0.17, Kd = 0.13, expressed per metre of depth error so they stay valid if MOTOR_MAX_STEPS changes.
  • Motor geometry: home = piston fully inserted (empty syringes, floats); full extension = piston extracted (full syringes, sinks). The mapping uToMotorPos() in include/config.h encapsulates MOTOR_INVERT_LOGICAL so motion code never hard-codes signs.
  • TOF safety range widened to [40, 85] mm to give 10 mm of margin above the homing threshold without risking the mechanical end stop.
  • balance now refuses to start without a prior homing (was forcing pos = 0 as a fallback, mechanically risky).
  • New SURFACE_TARGET_OFFSET_M constant and SURFACE_OFFSET <m> command (number 18) for tuning the surface idle position at runtime. Team Contact: PoliTOcean @ Politecnico di Torino Maintainers: Colabella Davide, Benevenga Filippo

About

Firmware for the float project

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

PoliTOcean Float 2025 - Technical Documentation

CI

Version: 11.2.0 Team: PoliTOcean @ Politecnico di Torino
Maintainers: Colabella Davide, Benevenga Filippo
Competition: MATE ROV 2025/26


TABLE OF CONTENTS


PROJECT OVERVIEW

Introduction and Requirements

By MATE 2026 requirements documentation (Task 4.1 - MATE Floats Under the Ice), the FLOAT must complete operational vertical profiling missions under simulated ice conditions.

Pre-Deployment Requirements:

  • The FLOAT must communicate with the Mission Station (CS) prior to descending, transmitting a defined data packet containing:
    • Company number (provided by MATE)
    • Time data (UTC/local/float time)
    • Pressure data (pa or kpa) and/or depth data (m or cm)
    • Additional data as required
  • Example packet: EX01 1:51:42 UTC 9.8 kpa 1.00 meters

Vertical Profile Requirements:

The FLOAT must complete two vertical profiles using a buoyancy engine (fluid displacement system, not thrusters). Each profile consists of:

  1. Descent Phase: Descend from surface to 2.5 meters depth (± 33 cm)
  2. Deep Hold: Maintain depth at 2.5 meters for 30 seconds (bottom of float as reference)
  3. Ascent Phase: Rise to 40 cm depth (± 33 cm) without breaking surface or contacting ice
  4. Shallow Hold: Maintain depth at 40 cm for 30 seconds (top of float as reference)

Data Collection & Transmission:

  • Collect depth/pressure measurements during both profiles and transmit judge packets every 5 seconds (minimum 20 data packets)
  • Store data in ESP32 internal flash as a LittleFS CSV containing: company number, profile id, time, pressure, judge/reference depth, phase, and raw sensor depth
  • After recovery, transmit all collected data wirelessly to the Mission Station
  • Data packets must show 7 sequential measurements (spanning 30 seconds at 5-second intervals: 0, 5, 10, 15, 20, 25, 30) confirming proper depth maintenance at both 2.5m and 0.4m

Post-Mission Requirements:

  • Upon surface recovery, autonomously transmit all profile data to the CS
  • CS GUI plots depth over time using received data (minimum 20 data packets required)
  • Graph must display time (X-axis) vs depth (Y-axis) for both completed profiles

Current firmware storage note: the active implementation uses the internal flash CSV log (FLASH_LOG_PATH) as the primary mission data source. EEPROM compact records remain only as an internal legacy buffer. The legacy serial command name is still CLEAR_SD, but it now resets the flash CSV log and the legacy EEPROM buffer.

Auto Mode (AM):

An autonomous operating mode that triggers profile execution in case of connection loss with the CS, ensuring mission completion if communication is temporarily unavailable. AM will autonomously commit up to two profiles when connection is lost, preventing incomplete missions due to transient WiFi failures.

Penalties:

  • Breaking surface or contacting ice sheet during profile: -5 points per profile
  • FLOAT must remain submerged between 40 cm and 2.5 m throughout the ascent/descent phases

System Behavior

The general idea is that the FLOAT provides some micro-services that the CS can activate by sending commands to it. Every command can be requested at any moment, with the only limit that a command can be accepted by the FLOAT only when the previous one has been completed (more info on command cycle later).

The FLOAT has two main logical states: the command execution one, and the idle one in which it waits for the new command. In idle state, the FLOAT can have buffered flash data from the last completed profile that can be sent to the CS.

Syringe / Motor Convention

The FLOAT changes its buoyancy by pulling and pushing water through a pair of syringes driven by a stepper motor through a lead screw. The mechanical convention is:

  • Home (motor_pos = 0): piston fully inserted, syringes empty of water → the FLOAT floats. At this position the TOF reads ≈ TOF_HOMING_THRESHOLD (75 mm) because the piston is far from the sensor.
  • Full extension (motor_pos = uToMotorPos(1.0f)): piston extracted, syringes full of water → the FLOAT sinks. TOF reads ≈ TOF_SAFE_RANGE_MIN_MM (40 mm).
  • PID logical convention: u ∈ [0, 1] with u = 0 → float (empty) and u = 1 → sink (full). The helper uToMotorPos(u) in include/config.h maps u to the actual motor target while respecting MOTOR_INVERT_LOGICAL, so callers never hard-code signs.

TOF safety limits used during motion:

ConstantDefaultMeaning
TOF_HOMING_THRESHOLD75 mmPhase 2 of homing stops when the TOF reads above this
TOF_HOMING_APPROACH_MM50 mmPhase 1 of homing stops when the TOF reads below this
TOF_SAFE_RANGE_MIN_MM40 mmLower bound: syringe fully extended (mechanical limit)
TOF_SAFE_RANGE_MAX_MM85 mmUpper bound: 10 mm above the homing threshold; any higher and the piston would risk hitting the back mechanical stop

Surface Target Offset

When the FLOAT is "floating", we usually want its top a few centimetres below the water surface — not exactly at the waterline — so that the float remains visible without being completely above water. This is controlled by SURFACE_TARGET_OFFSET_M (default 0.10 m: top of the float 10 cm below the surface).

Two ways to change it:

  • At compile time: edit SURFACE_TARGET_OFFSET_M in include/config.h.
  • At runtime: send command SURFACE_OFFSET <m> via the CS, or SURFACE_OFFSET <m> over USB serial on ESPA. The change persists until the next reboot.

The offset is geometry-agnostic: FLOAT_TOP_TO_SENSOR_M (geometric distance between the top of the float and the barometer) and SURFACE_TARGET_OFFSET_M (operational target) are kept as separate constants in include/config.h.


HARDWARE CONFIGURATION

Hardware Used

HardwareRoleLinkKey parameters / notes
ESP32 Dev Module x2ESPA float controller and ESPB communication bridgeEspressif ESP32Arduino framework, ESP-NOW link, USB serial bridge on ESPB
DRV8825 stepper driverStepper motor driver for syringe motionPololu DRV8825 carrierSTEP/DIR control, active-low enable, SLEEP and RESET held HIGH during operation
Stepper motor with planetary gearboxSyringe actuator motorStepperOnline 17HS15-1684S-PG27NEMA 17, 200 steps/rev, 1.8 deg/step, configured gear ratio 26.85124:1, microstep setting 1
Lead screw / threaded rodConverts motor rotation to linear travelSIENOC 500 mm trapezoidal lead screwPitch 2.0 mm, 4 starts, lead 8.0 mm/rev, configured travel 35 mm
VL53L7CX Time-of-Flight sensorNon-contact homing distance sensor (multi-zone)ST VL53L7CXI2C 0x29, LPn (XSHUT) GPIO16, GPIO1 GPIO15, 4×4 zone mode with central-zone mask 0x0660, 6 mm raw offset, 75 mm homing threshold
Bar02 pressure sensorPressure/depth measurementBlue Robotics Bar02MS5837_02BA model, I2C 0x76, used for depth and pressure
INA219 battery monitorBattery bus-voltage monitorAdafruit INA219 breakoutI2C 0x40, initialized at 100 kHz, configured with 5 A max and 0.1 ohm shunt

Pin Mapping

ESPA (Float Controller) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Motor Control
DIRGPIO32DRV8825 DirectionStepper direction control
STEPGPIO33DRV8825 StepStep pulse generation
ENGPIO27DRV8825 EnableActive-LOW, disables outputs when HIGH
SLEEPGPIO25DRV8825 SleepActive-LOW, must be HIGH for operation
RSTGPIO26DRV8825 ResetActive-LOW, must be HIGH for operation
TOF Sensor
SDAGPIO21VL53L7CX I2C DataI2C bus (shared with sensors)
SCLGPIO22VL53L7CX I2C ClockI2C bus @ 1MHz
XSHUTGPIO16VL53L7CX LPn (shutdown)Sensor enable / shutdown control
GPIO1GPIO15VL53L7CX InterruptOptional interrupt pin, unused in polling mode
Sensors
SDAGPIO21Bar02, INA219I2C bus (shared)
SCLGPIO22Bar02, INA219I2C bus (shared)
Status LED
LED_RGPIO19Red ChannelPWM control
LED_GGPIO18Green ChannelPWM control
LED_BGPIO5Blue ChannelPWM control

ESPB (Communication Bridge) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Communication
TXGPIO1USB Serial115200 baud
RXGPIO3USB Serial115200 baud
Status LED
Built-in LEDGPIO2Onboard LEDStatus indication

I2C Device Addresses:

DeviceAddressBus Speed
VL53L7CX TOF0x291 MHz
Bar02 Pressure0x76Shared I2C bus
INA219 Battery0x40Initialized at 100 kHz

The firmware initializes the INA219 at 100 kHz, then the VL53L7CX driver raises the shared Wire clock to 1 MHz for TOF ranging.


SYSTEM ARCHITECTURE

Deployment Diagram

FLOAT code has to be deployed on two ESP32, one mounted on the FLOAT board (ESPA) together with the sensors and the power supply, and the other (ESPB) communicating with the Control Station via USB. The two ESP32 communicates via WiFi using ESP-NOW protocol. The software on the two ESP32 is designed to work regardless of the design of the GUI on the CS.

The idea is to bring all the complexity on the ESPA and GUI, leaving no trace of logic on the ESPB.

graph TB
subgraph "Control Station"
GUI[GUI Application]
USB[USB Serial]
end
subgraph "ESPB - Communication Bridge"
ESPB_FW[ESPB Firmware]
ESPB_WiFi[WiFi ESP-NOW]
ESPB_LED[Built-in LED]
end
subgraph "ESPA - Float Controller"
ESPA_FW[ESPA Firmware]
ESPA_WiFi[WiFi ESP-NOW]
subgraph "Motor System"
DRV8825[DRV8825 Driver]
STEPPER[Stepper Motor]
TOF[VL53L7CX TOF Sensor]
end
subgraph "Sensors"
BAR02[Bar02 Pressure]
INA219[INA219 Battery]
end
RGB[RGB LED]
I2C[I2C Bus]
end
GUI -->|Commands| USB
USB <-->|Serial 115200| ESPB_FW
ESPB_FW <-->|ESP-NOW 2.4GHz| ESPB_WiFi
ESPB_WiFi <-.->|WiFi| ESPA_WiFi
ESPA_WiFi <-->|ESP-NOW| ESPA_FW
ESPA_FW -->|Control Signals| DRV8825
DRV8825 -->|STEP/DIR| STEPPER
TOF -->|Distance Data| ESPA_FW
BAR02 -->|I2C| I2C
INA219 -->|I2C| I2C
I2C -->|Sensor Data| ESPA_FW
ESPA_FW -->|Status| RGB
ESPB_FW -->|Status| ESPB_LED
style ESPA_FW fill:#4CAF50
style ESPB_FW fill:#2196F3
style GUI fill:#FF9800
style TOF fill:#9C27B0
Loading

Software Structure

The project follows a modular architecture with separate compilation units:

  • Central Config (include/config.h) - pin mapping, motor constants, PID defaults, mission timing, network parameters
  • Shared Protocol (include/float_common.h) - ESP-NOW packet structs, ACK strings, EEPROM size, shared LED enum
  • Motor Control (lib/motor) - DRV8825/FastAccelStepper setup, position tracking, bounded movement primitives
  • TOF Sensor (lib/tof) - VL53L7CX initialization (4×4 multi-zone), aggregated minimum-distance reading with raw offset compensation
  • Motion Control (lib/motion_control) - TOF homing, safe max-extension move, balance routine, emergency stop handling
  • Communication (lib/comms) - ESP-NOW wireless protocol and ElegantOTA session management
  • Sensors (lib/sensors) - Bar02 pressure/depth and INA219 battery monitoring
  • PID Controller (lib/pid) - depth control algorithm with runtime gain updates
  • Profile Manager (lib/profile) - mission profile execution and flash-backed mission logging
  • Flash Storage (lib/flash_storage) - LittleFS CSV mission log and replay helpers
  • LED Controller (lib/led) - RGB status indication system

ESPA State Machine

The ESPA firmware operates as a state machine coordinating motor control, sensors, and communications:

stateDiagram-v2
[*] --> INIT: Power On
INIT --> HOMING: Sensors OK
INIT --> ERROR: Init Failed
HOMING --> IDLE: Homing Success
HOMING --> ERROR: Homing Failed/Timeout
IDLE --> EXECUTING: Command Received
IDLE --> IDLE: No Command
EXECUTING --> PROFILE: GO Command
EXECUTING --> BALANCE: BALANCE Command
EXECUTING --> SEND_DATA: LISTENING Command
EXECUTING --> CLEAR_DATA: CLEAR_SD Command
EXECUTING --> UPDATE_PID: PARAMS Command
EXECUTING --> UPDATE_PID_EXT: PARAMS_EXT Command
EXECUTING --> TEST_SPEED: TEST_FREQ Command
EXECUTING --> TEST_STEPS: TEST_STEPS Command
EXECUTING --> DEBUG_MODE: DEBUG Command
EXECUTING --> HOMING: HOME_MOTOR Command
EXECUTING --> OTA: TRY_UPLOAD Command
EXECUTING --> SYRINGE_SET: SYRINGE_SET Command
EXECUTING --> PID_HOLD: PID_HOLD Command
EXECUTING --> PID_STEP: PID_STEP Command
EXECUTING --> SET_SURFACE_OFFSET: SURFACE_OFFSET Command
PROFILE --> PID_CONTROL: Descending
PID_CONTROL --> PID_CONTROL: Depth Control Active
PID_CONTROL --> ASCENT: Target Reached/Timeout
ASCENT --> IDLE_W_DATA: At Surface
BALANCE --> IDLE: Balance Complete
SEND_DATA --> IDLE: Data Sent
CLEAR_DATA --> IDLE: Flash Log Cleared
UPDATE_PID --> IDLE: Gains Updated
UPDATE_PID_EXT --> IDLE: Period/alpha Updated
TEST_SPEED --> IDLE: Speed Stored
TEST_STEPS --> IDLE: Test Move Complete
DEBUG_MODE --> IDLE: Debug Toggle Complete
OTA --> IDLE: Upload Complete
SYRINGE_SET --> IDLE: Bench Test Complete
PID_HOLD --> IDLE: Hold Complete / Timeout
PID_STEP --> IDLE: Step Complete / Timeout
SET_SURFACE_OFFSET --> IDLE: Offset Stored
IDLE_W_DATA --> SENDING: LISTENING Command
SENDING --> IDLE: Data Transmitted
ERROR --> [*]: Manual Reset Required
note right of IDLE
RGB: Green Solid
Waiting for command
end note
note right of HOMING
RGB: Purple Blink
TOF-based homing
end note
note right of PID_CONTROL
RGB: Cyan Blink
Active depth control
end note
note right of ERROR
RGB: Red Blink
Fatal error state
end note
Loading

COMMUNICATION PROTOCOL

Command Lifecycle

A command life-cycle does not overlaps/interfere with the previous nor the next one: when the CS sends a command (and it arrives to the FLOAT), a feedback from the FLOAT should inform about the acceptance of the command and if this acknowledgement arrives within a given period (specified later), next command requests will be ignored until end of execution of the current one, signaled by an idle acknowledgement.

If the acknowledgement doesn't arrive within that time span, the command commit can be considered failed: this could happen for WiFi connection failures or FLOAT electronics issues.

When waiting for the command commit acknowledgement, other command requests will be ignored as well.

As already mentioned, after command completion the FLOAT will try to send an idle acknowledgement to signal that it is listening for a new command: together with the idle state, this acknowledgement can also inform about the presence of new flash-backed profile data that has to be sent to the CS. After an idle acknowledgement is received, a new command can be accepted.

To maintain consistency with the status stored on the ESPB, and hence with the GUI visuals, the FLOAT grants to send the acknowledgement signalling a command commit only when the commit can be given for sure. In the same way, if the acknowledgement fails to be sent due to connection issues, the command is not committed.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant WiFi as ESP-NOW
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Fresh State
CS->>ESPB: Send Command (e.g., "GO")
ESPB->>WiFi: Forward Command
WiFi->>ESPA: Deliver Command
ESPA->>ESPA: Validate & Accept
ESPA->>WiFi: ACK (e.g., "GO_RECVD")
WiFi->>ESPB: Deliver ACK
ESPB->>ESPB: Update State (status=2)
ESPB->>CS: Forward ACK
Note over ESPA: Executing Command...
ESPA->>ESPA: Complete Task
ESPA->>WiFi: Completion ACK (e.g., "FLOAT_IDLE")
WiFi->>ESPB: Deliver Completion
ESPB->>ESPB: Update State (status=0)
ESPB->>CS: Forward Completion
Note over CS: Ready for Next Command
Loading

ESPB Bridge Role

The ESPB role is only to make the CS task of continuously checking on the WiFi channel less resource consuming.

In particular, ESPB receives commands from CS via USB only to forward them to the FLOAT via WiFi. At the same time, it can receive feedback and data from the FLOAT. In the latter case the ESPB will forward the packages on the USB channel, while using them to update an internal state accordingly. The only code that should trigger the update of the state stored on ESPB is the firmware on the FLOAT, via the data and the feedback sent to CS (more later). This is because no assumptions have to be done by other software on the commands completion and acceptance.

The ESPB state should mirror the FLOAT state at each moment (more details later) and can be used by the GUI to give visual feedback to the user or to drive its internal logic. It can be requested to the ESPB by the CS at any moment with a specific command. The state info sent to the CS with this command also contains WiFi connection state info and AM activation state info.

In general, the ESPB feedback could be stale when requested (for example because the CS could poll it with a low frequency), so to get fresh, real-time data the CS should listen on the USB channel for the FLOAT packages, after a command request or when waiting for command completion and retrieve the FLOAT current state directly by those packages.

Periodic polling remains a legit choice in case the CS cannot exploit interrupts triggered by Serial connection, but notice that this solution leads to delayed GUI visual feedback with respect to the changes on the FLOAT state.

In some cases, connection losses can undermine consistency between the feedback of the ESPB (either they are polled or real time) and the real current FLOAT state (consistency threats for each FLOAT state later).

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Stale State Polling
CS->>ESPB: Send Command
ESPB->>ESPA: Forward Command
ESPA->>ESPA: Accept & Execute
ESPA->>ESPB: ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward ACK
Note over ESPA: Command Executing...
loop Periodic Polling
CS->>ESPB: Request STATUS
ESPB->>CS: Return Cached State
Note over CS: State may be stale<br/>if ESPA completed recently
end
ESPA->>ESPA: Complete Command
ESPA->>ESPB: Completion ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward Completion
CS->>ESPB: Request STATUS
ESPB->>CS: Return Fresh State
Loading

FLOAT Commands

Table of FLOAT commands with relative effects and acknowledgements:

Cmd stringCmd ESPA numberCmd effectsESPA ack stringESPA ack effects on ESPB state
GO1Performs the two MATE vertical profiles, sends the pre-descent data packet before the first descent, and logs pressure/depth records to flash CSVGO_RECVDstatus to 2 (command execution)
LISTENING2Streams flash CSV records as JSON data packets at 5-second cadence, followed by STOP_DATAAck is data itselfstatus to 2 after first package arrival
BALANCE3Cycles full extension and retraction with holdMs holds until Bar02 pressure rises above the startup baseline by BALANCE_STOP_PRESSURE_DELTA_KPA. Requires the motor to be homed first — otherwise the command fails with Balance: homing requiredCMD3_RECVDstatus to 2
CLEAR_SD4Clears and recreates the flash CSV log, and clears the legacy EEPROM buffer. The command string is kept as CLEAR_SD for compatibilityCMD4_RECVDstatus to 2
SWITCH_AUTO_MODE5Toggles FLOAT Auto ModeSWITCH_AM_RECVDstatus to 2, AM activation state toggled
SEND_PACKAGE6Sends a single live JSON snapshot containing company number, time, pressure, judge/reference depth, phase, and raw sensor depthAck is the package itselfstatus to 2
TRY_UPLOAD7Starts the ElegantOTA access point on ESPA for a 5-minute upload window, then restores ESP-NOWTRY_UPLOAD_RECVDstatus to 2
PARAMS kp ki kd8Updates PID gains at runtimeCHNG_PARMS_RECVDstatus to 2
TEST_FREQ freq9Sets manual test movement speed, clamped to 10-1200 steps/sTEST_FREQ_RECVDstatus to 2
TEST_STEPS n10Moves the motor by n relative steps at the current test speedTEST_STEPS_RECVDstatus to 2
DEBUG11Toggles remote debug forwarding through DebugSerialDEBUG_MODE_RECVDstatus to 2
HOME_MOTOR12Runs TOF-based homing remotelyHOME_RECVDstatus to 2
STOP13Triggers a remote emergency stop, stops the motor, disables outputs, and returns to idleSTOP_RECVDstatus to 2
PARAMS_EXT period alpha14Updates PID tick period (ms) and derivative LPF coefficient alphaD at runtimeCHNG_PID_EXT_RECVDstatus to 2
SYRINGE_SET u dur_s15Bench test: drives the syringe to normalized position u ∈ [0,1] for dur_s seconds, logging depth — bypasses the PID (DC gain / time-constant characterization)SYRINGE_SET_RECVDstatus to 2
PID_HOLD depth dur_s16Bench test: holds depth at depth_m for dur_s seconds with the PID active, logging at 5 HzPID_HOLD_RECVDstatus to 2
PID_STEP depth17Bench test: step response — drives the PID to depth_m for up to 60 s, logging at 10 HzPID_STEP_RECVDstatus to 2
SURFACE_OFFSET m18Sets the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime: the FLOAT will hold its top m metres below the waterline when "floating" (default 0.10)SURFACE_OFF_RECVDstatus to 2
STATUS-Requests stale ESPB status plus AM state, WiFi connection state, battery millivolts, and last RSSI--

Once a command is completed, ESPA acknowledgement can be:

ESPA ack stringESPA ack effects on ESPB stateESPA state
FLOAT_IDLEstatus to 0 (idle)Idle with no data to be sent
FLOAT_IDLE_W_DATAstatus to 1 (idle with data to be sent)Idle with data from last profile to be sent

STATUS COMMAND: ESPB RESPONSE

ESPB response to STATUS command is composed by five parts of information: ESPA state (stale), activation of the AM on the FLOAT, WiFi connection state, last received battery millivolts, and last received RSSI. The WiFi connection state is detected by sending a dummy command code 0, while the other states are kept consistent with the ones on the FLOAT by updating them after acknowledgements reception.

ESPA state:

ESPB state stringESPB state numberState description
UNKNOWN-1ESPB has not received any state message from ESPA since boot
CONNECTED0The FLOAT is listening for new command. Previous command succeeded
CONNECTED_W_DATA1The FLOAT is listening for new command and has some new data from last profile to be sent. Previous command succeeded
EXECUTING_CMD2FLOAT is executing a command
STATUS_ERROR-Internal error in reading the state number

WARNING:
If committing a profile automatically, the relative acknowledgement will likely fail due to connection loss. The profile is committed anyway as it is generated from connection loss in the first place, but the GUI may not have mean to detect it. So it will likely read an inconsistent idle status (CONNECTED or CONNECTED_W_DATA) until FLOAT is at water level with a stable WiFi connection. In the meantime the command commits will fail, for connection loss or because the FLOAT is underwater. Anyway WiFi connection state can be detected by the STATUS command, hence giving feedback on status consistency.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over ESPA: Auto Mode Active
Note over ESPA,ESPB: Connection Lost!
ESPA->>ESPA: Detect Connection Loss
ESPA->>ESPA: Auto-commit Profile
Note over ESPA: Descending...<br/>WiFi Unavailable
ESPA-xESPB: ACK Fails (No Connection)
Note over ESPB: State Becomes Inconsistent<br/>Still shows "IDLE"
CS->>ESPB: Request STATUS
ESPB->>CS: CONNECTED | CONN_LOST
Note over CS: GUI shows inconsistent state<br/>but WiFi loss detected
Note over ESPA: At Surface...<br/>WiFi Restored
ESPA->>ESPB: FLOAT_IDLE_W_DATA
ESPB->>ESPB: Update to Consistent State
ESPB->>CS: Forward State
Note over CS,ESPA: Consistency Restored
Loading

AM state:

ESPB state stringState description
AUTO_MODE_YESAM on FLOAT is activated
AUTO_MODE_NOAM on FLOAT is not activated: connection losses will not trigger profiles

WiFi connection state:

ESPB state stringState description
CONN_OKWiFi connection is ok
CONN_LOSTWiFi connection is currently down. ESPB state could be wrong

Battery and RSSI fields:

ESPB fieldState description
BATTERY: <mV>Last battery voltage received from ESPA acknowledgements
RSSI: <dBm>Last ESP-NOW packet RSSI captured by ESPB promiscuous callback

Example of ESPB state response:CONNECTED_W_DATA | AUTO_MODE_NO | CONN_OK | BATTERY: 12450 | RSSI: -63

At ESPB boot, before any ESPA packet is received, a status request may return: UNKNOWN | AUTO_MODE_NO | CONN_LOST | BATTERY: 0 | RSSI: 0.

GUI / ESPB / ESPA Protocol Contract

The GUI sends command strings to ESPB over USB serial. ESPB parses the string, sends the command number to ESPA over ESP-NOW, and forwards ESPA acknowledgements/data back to the GUI.

GUI commandESPA command numberESPA acknowledgement / response
GO1GO_RECVD
LISTENING2Stored data packets, then STOP_DATA
BALANCE3CMD3_RECVD
CLEAR_SD4CMD4_RECVD
SWITCH_AUTO_MODE5SWITCH_AM_RECVD
SEND_PACKAGE6Live JSON packet
TRY_UPLOAD7TRY_UPLOAD_RECVD
PARAMS kp ki kd8CHNG_PARMS_RECVD
TEST_FREQ freq9TEST_FREQ_RECVD
TEST_STEPS n10TEST_STEPS_RECVD
DEBUG11DEBUG_MODE_RECVD
HOME_MOTOR12HOME_RECVD
STOP13STOP_RECVD
PARAMS_EXT period_ms alpha_d14CHNG_PID_EXT_RECVD
SYRINGE_SET u dur_s15SYRINGE_SET_RECVD
PID_HOLD depth_m dur_s16PID_HOLD_RECVD
PID_STEP depth_m17PID_STEP_RECVD
SURFACE_OFFSET m18SURFACE_OFF_RECVD
STATUS-ESPB local status line with five `

The peer MAC addresses are configured centrally in include/config.h: MAC_ESPA is used by ESPB, and MAC_ESPB is used by ESPA.


LED STATUS INDICATORS

The FLOAT is equipped with RGB LEDs on both ESP32 boards that provide visual feedback about the system status:

ESPA (Float Board) LED States:

Driven by LEDState (scoped enum in lib/led/include/led.h):

LED Color/PatternStateDescription
Green Solid / Boot BlinksLEDState::INITSystem initializing
Green SolidLEDState::IDLEReady and idle, waiting for commands
Green BlinkLEDState::IDLE_WITH_DATAIdle with data ready to send
Red SolidLEDState::LOW_BATTERYBattery voltage below BATT_THRESH (12.0 V)
Red BlinkLEDState::ERRORError state or motor emergency stop
Blue SolidLEDState::PROFILERunning non-PID profile phase
Yellow BlinkLEDState::AUTO_MODEAuto mode active
Purple BlinkLEDState::HOMINGMotor homing in progress
Purple SolidLEDState::MOTOR_MOVINGMotor moving
Cyan BlinkLEDState::PID_CONTROLPID depth control active
White SolidLEDState::COMMUNICATIONCommand received / communicating with ESPB
Orange BlinkLEDState::OTA_MODEOTA update mode active
OffLEDState::OFFSystem off or disabled

ESPB uses a separate FloatLEDState enum (LED_* prefix) defined in include/float_common.h; the two enums are deliberately independent because the two boards have different LED states to signal.

ESPB (Communication Bridge) LED States:

LED PatternStateDescription
Solid OnLED_IDLEConnected and ready
Very Fast BlinkLED_ERRORCommunication error
OffLED_OFFSystem off or disabled

Note: ESPB uses the built-in LED (pin 2) with different blink patterns to indicate status, as it does not have external RGB connections.


DEVELOPMENT AND TESTING

PlatformIO Environments

EnvironmentPurposeMain Source
espAFloat controller firmware with sensors, TOF homing, motion control, PID, ESP-NOW, and OTAsrc/espA/main.cpp
espA_poolESPA firmware compiled with conservative 70 cm pool-test targets (POOL_TEST_PROFILE)src/espA/main.cpp
espBUSB-to-ESP-NOW bridge for the Control Stationsrc/espB/main.cpp
espA_manual_keyboardBench firmware for serial keyboard continuous motor movement without homingsrc/espA_manual_keyboard/main.cpp

Common commands:

pio run -e espA
pio run -e espA_pool
pio run -e espB
pio run -e espA_manual_keyboard
pio test -e espA

CLI Startup

Run all commands from the project root:

cd Float_2025

To build and upload the main firmware targets:

pio run -e espA -t upload
pio run -e espB -t upload

For a conservative shallow-pool test at about 70 cm, upload ESPA with:

pio run -e espA_pool -t upload

To open the serial monitor at 115200 baud:

pio device monitor -e espA
pio device monitor -e espB

Direct USB Tuning Commands (ESPA)

All commands in the FLOAT Commands table can be sent over the ESPB USB serial bridge using the same string syntax. The commands below — useful for bench tuning — can also be sent directly over ESPA's USB serial port (e.g. when ESPA is wired to a laptop for tuning runs), bypassing ESPB and ESP-NOW entirely.

CommandEffect
PARAMS <kp> <ki> <kd>Update PID gains at runtime (same effect as command 8)
PARAMS_EXT <period_ms> <alpha_d>Update PID tick period and derivative LPF coefficient (command 14)
SYRINGE_SET <u> <dur_s>Drive the syringe to position u ∈ [0,1] for dur_s seconds and log depth — bypasses the PID, useful for DC-gain and time-constant estimation (command 15)
PID_HOLD <depth_m> <dur_s>Hold PID at depth_m for dur_s seconds, log at 5 Hz (command 16)
PID_STEP <depth_m>Step response: PID at depth_m for up to 60 s, log at 10 Hz (command 17)
SURFACE_OFFSET <m>Set the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime (command 18)

CLI Tests

To run all available tests for the espA environment:

pio test -e espA

To run a single test:

pio test -e espA -f unit_hw/motor/test_max_steps
pio test -e espA -f unit_hw/motor/test_speed
pio test -e espA -f integration/test_screw_lead_20mm
pio test -e espA -f integration/test_homing_only
pio test -e espA -f integration/test_tof_reading
pio test -e espA -f integration/test_homing_move_to_max
pio test -e espA -f integration/test_motor_direction
pio test -e espA -f integration/test_tof_motor_accuracy

To test ESPB without ESPA powered on:

pio test -e espB -f unit_hw/espb_bridge/test_parser
pio test -e espB -f unit_hw/espb_bridge/test_status_format
pio test -e espB -f unit_hw/espb_bridge/test_protocol_contract

To test the real ESPB-to-ESPA bridge, first upload the real espA firmware, wait until ESPA is idle, then run:

pio test -e espB -f integration/test_espnow_bridge

This test only uses the dummy command 0 and SWITCH_AUTO_MODE; it does not start profiles or move the motor.

Available tests:

TestCommandWhat it checks
test_max_stepspio test -e espA -f unit_hw/motor/test_max_stepsMoves only the motor to the safe maximum extension starting from logical position 0
test_speedpio test -e espA -f unit_hw/motor/test_speedMoves only the motor through 6 alternating 40 mm moves, increasing speed and acceleration up to 2300
test_screw_lead_20mmpio test -e espA -f integration/test_screw_lead_20mmRuns TOF homing, moves the motor by 20 mm, and compares the internal TOF delta
test_motor_directionpio test -e espA -f integration/test_motor_directionMoves only the motor forward/backward and verifies the logical direction; by default it does not use TOF
test_tof_readingpio test -e espA -f integration/test_tof_readingInitializes only the TOF sensor and checks valid readings for about 30 s
test_homing_onlypio test -e espA -f integration/test_homing_onlyRuns only TOF-based homing
test_homing_move_to_maxpio test -e espA -f integration/test_homing_move_to_maxRuns TOF homing and then moves to the safe maximum extension
test_tof_motor_accuracypio test -e espA -f integration/test_tof_motor_accuracyCompares TOF distance and motor position after homing
test_parserpio test -e espB -f unit_hw/espb_bridge/test_parserVerifies GUI/Serial command parsing into ESPA packets without ESPA powered on
test_status_formatpio test -e espB -f unit_hw/espb_bridge/test_status_formatVerifies ESPB cached state and the five-field STATUS format
test_protocol_contractpio test -e espB -f unit_hw/espb_bridge/test_protocol_contractLocks the command/ACK consistency contract between GUI, ESPB, and ESPA
test_espnow_bridgepio test -e espB -f integration/test_espnow_bridgeVerifies real ESP-NOW with the real ESPA firmware powered on, without movement

The test_max_steps, test_speed, and test_motor_direction tests are useful when you need to move only the motor without TOF homing. Before running them, make sure the piston is away from the mechanical end stops and can move in both directions.

Manual Motor-Only Control

To upload the bench firmware that lets you move the motor from the serial keyboard:

pio run -e espA_manual_keyboard -t upload
pio device monitor -e espA_manual_keyboard

Commands in the serial monitor:

KeyAction
Up arrow or wHold to move toward home/up
Down arrow or sHold to move toward extension/down
Space or xStop immediately and disable motor outputs
pPrint the current position
tPrint one TOF reading
h or ?Print help

This firmware does not run homing: at startup it assigns a centered logical position and moves while it receives repeated keypresses; when the key is released it stops automatically after a short timeout. During movement it periodically prints motor position and TOF distance. Use it only when the mechanism is in a physically safe position.

Test Layout

Hardware-oriented tests are stored under test/:

  • test/unit_hw/motor/test_max_steps checks safe maximum extension from a known zero
  • test/unit_hw/motor/test_speed checks alternating 40 mm moves while speed and acceleration increase up to 2300
  • test/integration/test_screw_lead_20mm checks the configured screw pitch, starts, and lead with one 20 mm move measured internally by TOF
  • test/integration/test_tof_reading checks that the TOF sensor initializes and returns valid distance samples for about 30 seconds
  • test/integration/test_homing_only checks TOF-based homing
  • test/integration/test_homing_move_to_max checks homing followed by safe full extension
  • test/integration/test_motor_direction checks logical/physical motion direction, optionally using TOF
  • test/integration/test_tof_motor_accuracy checks TOF and motor movement consistency

Continuous Integration

GitHub Actions builds all three PlatformIO environments (espA, espB, espA_pool) on every push to any branch and on every pull request to master. Workflow file: .github/workflows/ci.yml.

CI does not run the unit_hw/ or integration/ PlatformIO tests because they need a real ESP32 with the float wired up. Run those locally on the bench.

Pushing a v* tag triggers .github/workflows/release.yml, which builds all three environments and attaches the resulting firmware.bin / firmware.elf to a GitHub Release auto-named after the tag.

See CONTRIBUTING.md for the full git workflow (trunk-based with PR review on master), commit conventions, and one-time branch protection setup.


UTILITIES AND RESOURCES

Arduino Library Repositories:

Development Tools:

  • PlatformIO IDE: Modern embedded development platform
  • ESP32 Arduino Core: Framework for ESP32 development
  • FastAccelStepper Library: Timer/task-driven stepper motor control
  • VL53L7CX Library: Multi-zone Time-of-Flight sensor driver

GLOSSARY

  • AM (Auto Mode): Autonomous operation mode that triggers profiles on connection loss
  • CS (Control Station): Ground-based computer running the GUI application
  • ESPA: ESP32 mounted on the Float board (primary controller)
  • ESPB: ESP32 communication bridge between Float and CS
  • Flash Profile Log: Current onboard LittleFS CSV storage used before JSON transmission
  • FastAccelStepper: Timer/task-driven stepper library used by MotorController
  • MotionController: Firmware layer that combines motor, TOF, LEDs, debug, timeouts, and emergency stops for safe movement routines
  • Commit a command: To accept a sent command. After commit, command execution and success is ideally granted
  • Complete a command: To execute all the requirements requested by a command
  • Profile: A complete mission cycle (descent → depth control → ascent → data transmission)
  • TOF (Time-of-Flight): Non-contact distance measurement technology using light pulses
  • Homing: Process of establishing the motor's zero reference position
  • PID Control: Proportional-Integral-Derivative controller for precise depth maintenance
  • ESP-NOW: Low-latency peer-to-peer WiFi communication protocol by Espressif

Documentation Version: 11.2.0 Last Updated: May 2026

Recent changes:

  • PID output normalized to u ∈ [0, 1] (fraction of syringe travel). Default gains Kp = 0.17, Kd = 0.13, expressed per metre of depth error so they stay valid if MOTOR_MAX_STEPS changes.
  • Motor geometry: home = piston fully inserted (empty syringes, floats); full extension = piston extracted (full syringes, sinks). The mapping uToMotorPos() in include/config.h encapsulates MOTOR_INVERT_LOGICAL so motion code never hard-codes signs.
  • TOF safety range widened to [40, 85] mm to give 10 mm of margin above the homing threshold without risking the mechanical end stop.
  • balance now refuses to start without a prior homing (was forcing pos = 0 as a fallback, mechanically risky).
  • New SURFACE_TARGET_OFFSET_M constant and SURFACE_OFFSET <m> command (number 18) for tuning the surface idle position at runtime. Team Contact: PoliTOcean @ Politecnico di Torino Maintainers: Colabella Davide, Benevenga Filippo

About

Firmware for the float project

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PoliTOcean Float 2025 - Technical Documentation

CI

Version: 11.2.0 Team: PoliTOcean @ Politecnico di Torino
Maintainers: Colabella Davide, Benevenga Filippo
Competition: MATE ROV 2025/26


TABLE OF CONTENTS


PROJECT OVERVIEW

Introduction and Requirements

By MATE 2026 requirements documentation (Task 4.1 - MATE Floats Under the Ice), the FLOAT must complete operational vertical profiling missions under simulated ice conditions.

Pre-Deployment Requirements:

  • The FLOAT must communicate with the Mission Station (CS) prior to descending, transmitting a defined data packet containing:
    • Company number (provided by MATE)
    • Time data (UTC/local/float time)
    • Pressure data (pa or kpa) and/or depth data (m or cm)
    • Additional data as required
  • Example packet: EX01 1:51:42 UTC 9.8 kpa 1.00 meters

Vertical Profile Requirements:

The FLOAT must complete two vertical profiles using a buoyancy engine (fluid displacement system, not thrusters). Each profile consists of:

  1. Descent Phase: Descend from surface to 2.5 meters depth (± 33 cm)
  2. Deep Hold: Maintain depth at 2.5 meters for 30 seconds (bottom of float as reference)
  3. Ascent Phase: Rise to 40 cm depth (± 33 cm) without breaking surface or contacting ice
  4. Shallow Hold: Maintain depth at 40 cm for 30 seconds (top of float as reference)

Data Collection & Transmission:

  • Collect depth/pressure measurements during both profiles and transmit judge packets every 5 seconds (minimum 20 data packets)
  • Store data in ESP32 internal flash as a LittleFS CSV containing: company number, profile id, time, pressure, judge/reference depth, phase, and raw sensor depth
  • After recovery, transmit all collected data wirelessly to the Mission Station
  • Data packets must show 7 sequential measurements (spanning 30 seconds at 5-second intervals: 0, 5, 10, 15, 20, 25, 30) confirming proper depth maintenance at both 2.5m and 0.4m

Post-Mission Requirements:

  • Upon surface recovery, autonomously transmit all profile data to the CS
  • CS GUI plots depth over time using received data (minimum 20 data packets required)
  • Graph must display time (X-axis) vs depth (Y-axis) for both completed profiles

Current firmware storage note: the active implementation uses the internal flash CSV log (FLASH_LOG_PATH) as the primary mission data source. EEPROM compact records remain only as an internal legacy buffer. The legacy serial command name is still CLEAR_SD, but it now resets the flash CSV log and the legacy EEPROM buffer.

Auto Mode (AM):

An autonomous operating mode that triggers profile execution in case of connection loss with the CS, ensuring mission completion if communication is temporarily unavailable. AM will autonomously commit up to two profiles when connection is lost, preventing incomplete missions due to transient WiFi failures.

Penalties:

  • Breaking surface or contacting ice sheet during profile: -5 points per profile
  • FLOAT must remain submerged between 40 cm and 2.5 m throughout the ascent/descent phases

System Behavior

The general idea is that the FLOAT provides some micro-services that the CS can activate by sending commands to it. Every command can be requested at any moment, with the only limit that a command can be accepted by the FLOAT only when the previous one has been completed (more info on command cycle later).

The FLOAT has two main logical states: the command execution one, and the idle one in which it waits for the new command. In idle state, the FLOAT can have buffered flash data from the last completed profile that can be sent to the CS.

Syringe / Motor Convention

The FLOAT changes its buoyancy by pulling and pushing water through a pair of syringes driven by a stepper motor through a lead screw. The mechanical convention is:

  • Home (motor_pos = 0): piston fully inserted, syringes empty of water → the FLOAT floats. At this position the TOF reads ≈ TOF_HOMING_THRESHOLD (75 mm) because the piston is far from the sensor.
  • Full extension (motor_pos = uToMotorPos(1.0f)): piston extracted, syringes full of water → the FLOAT sinks. TOF reads ≈ TOF_SAFE_RANGE_MIN_MM (40 mm).
  • PID logical convention: u ∈ [0, 1] with u = 0 → float (empty) and u = 1 → sink (full). The helper uToMotorPos(u) in include/config.h maps u to the actual motor target while respecting MOTOR_INVERT_LOGICAL, so callers never hard-code signs.

TOF safety limits used during motion:

ConstantDefaultMeaning
TOF_HOMING_THRESHOLD75 mmPhase 2 of homing stops when the TOF reads above this
TOF_HOMING_APPROACH_MM50 mmPhase 1 of homing stops when the TOF reads below this
TOF_SAFE_RANGE_MIN_MM40 mmLower bound: syringe fully extended (mechanical limit)
TOF_SAFE_RANGE_MAX_MM85 mmUpper bound: 10 mm above the homing threshold; any higher and the piston would risk hitting the back mechanical stop

Surface Target Offset

When the FLOAT is "floating", we usually want its top a few centimetres below the water surface — not exactly at the waterline — so that the float remains visible without being completely above water. This is controlled by SURFACE_TARGET_OFFSET_M (default 0.10 m: top of the float 10 cm below the surface).

Two ways to change it:

  • At compile time: edit SURFACE_TARGET_OFFSET_M in include/config.h.
  • At runtime: send command SURFACE_OFFSET <m> via the CS, or SURFACE_OFFSET <m> over USB serial on ESPA. The change persists until the next reboot.

The offset is geometry-agnostic: FLOAT_TOP_TO_SENSOR_M (geometric distance between the top of the float and the barometer) and SURFACE_TARGET_OFFSET_M (operational target) are kept as separate constants in include/config.h.


HARDWARE CONFIGURATION

Hardware Used

HardwareRoleLinkKey parameters / notes
ESP32 Dev Module x2ESPA float controller and ESPB communication bridgeEspressif ESP32Arduino framework, ESP-NOW link, USB serial bridge on ESPB
DRV8825 stepper driverStepper motor driver for syringe motionPololu DRV8825 carrierSTEP/DIR control, active-low enable, SLEEP and RESET held HIGH during operation
Stepper motor with planetary gearboxSyringe actuator motorStepperOnline 17HS15-1684S-PG27NEMA 17, 200 steps/rev, 1.8 deg/step, configured gear ratio 26.85124:1, microstep setting 1
Lead screw / threaded rodConverts motor rotation to linear travelSIENOC 500 mm trapezoidal lead screwPitch 2.0 mm, 4 starts, lead 8.0 mm/rev, configured travel 35 mm
VL53L7CX Time-of-Flight sensorNon-contact homing distance sensor (multi-zone)ST VL53L7CXI2C 0x29, LPn (XSHUT) GPIO16, GPIO1 GPIO15, 4×4 zone mode with central-zone mask 0x0660, 6 mm raw offset, 75 mm homing threshold
Bar02 pressure sensorPressure/depth measurementBlue Robotics Bar02MS5837_02BA model, I2C 0x76, used for depth and pressure
INA219 battery monitorBattery bus-voltage monitorAdafruit INA219 breakoutI2C 0x40, initialized at 100 kHz, configured with 5 A max and 0.1 ohm shunt

Pin Mapping

ESPA (Float Controller) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Motor Control
DIRGPIO32DRV8825 DirectionStepper direction control
STEPGPIO33DRV8825 StepStep pulse generation
ENGPIO27DRV8825 EnableActive-LOW, disables outputs when HIGH
SLEEPGPIO25DRV8825 SleepActive-LOW, must be HIGH for operation
RSTGPIO26DRV8825 ResetActive-LOW, must be HIGH for operation
TOF Sensor
SDAGPIO21VL53L7CX I2C DataI2C bus (shared with sensors)
SCLGPIO22VL53L7CX I2C ClockI2C bus @ 1MHz
XSHUTGPIO16VL53L7CX LPn (shutdown)Sensor enable / shutdown control
GPIO1GPIO15VL53L7CX InterruptOptional interrupt pin, unused in polling mode
Sensors
SDAGPIO21Bar02, INA219I2C bus (shared)
SCLGPIO22Bar02, INA219I2C bus (shared)
Status LED
LED_RGPIO19Red ChannelPWM control
LED_GGPIO18Green ChannelPWM control
LED_BGPIO5Blue ChannelPWM control

ESPB (Communication Bridge) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Communication
TXGPIO1USB Serial115200 baud
RXGPIO3USB Serial115200 baud
Status LED
Built-in LEDGPIO2Onboard LEDStatus indication

I2C Device Addresses:

DeviceAddressBus Speed
VL53L7CX TOF0x291 MHz
Bar02 Pressure0x76Shared I2C bus
INA219 Battery0x40Initialized at 100 kHz

The firmware initializes the INA219 at 100 kHz, then the VL53L7CX driver raises the shared Wire clock to 1 MHz for TOF ranging.


SYSTEM ARCHITECTURE

Deployment Diagram

FLOAT code has to be deployed on two ESP32, one mounted on the FLOAT board (ESPA) together with the sensors and the power supply, and the other (ESPB) communicating with the Control Station via USB. The two ESP32 communicates via WiFi using ESP-NOW protocol. The software on the two ESP32 is designed to work regardless of the design of the GUI on the CS.

The idea is to bring all the complexity on the ESPA and GUI, leaving no trace of logic on the ESPB.

graph TB
subgraph "Control Station"
GUI[GUI Application]
USB[USB Serial]
end
subgraph "ESPB - Communication Bridge"
ESPB_FW[ESPB Firmware]
ESPB_WiFi[WiFi ESP-NOW]
ESPB_LED[Built-in LED]
end
subgraph "ESPA - Float Controller"
ESPA_FW[ESPA Firmware]
ESPA_WiFi[WiFi ESP-NOW]
subgraph "Motor System"
DRV8825[DRV8825 Driver]
STEPPER[Stepper Motor]
TOF[VL53L7CX TOF Sensor]
end
subgraph "Sensors"
BAR02[Bar02 Pressure]
INA219[INA219 Battery]
end
RGB[RGB LED]
I2C[I2C Bus]
end
GUI -->|Commands| USB
USB <-->|Serial 115200| ESPB_FW
ESPB_FW <-->|ESP-NOW 2.4GHz| ESPB_WiFi
ESPB_WiFi <-.->|WiFi| ESPA_WiFi
ESPA_WiFi <-->|ESP-NOW| ESPA_FW
ESPA_FW -->|Control Signals| DRV8825
DRV8825 -->|STEP/DIR| STEPPER
TOF -->|Distance Data| ESPA_FW
BAR02 -->|I2C| I2C
INA219 -->|I2C| I2C
I2C -->|Sensor Data| ESPA_FW
ESPA_FW -->|Status| RGB
ESPB_FW -->|Status| ESPB_LED
style ESPA_FW fill:#4CAF50
style ESPB_FW fill:#2196F3
style GUI fill:#FF9800
style TOF fill:#9C27B0
Loading

Software Structure

The project follows a modular architecture with separate compilation units:

  • Central Config (include/config.h) - pin mapping, motor constants, PID defaults, mission timing, network parameters
  • Shared Protocol (include/float_common.h) - ESP-NOW packet structs, ACK strings, EEPROM size, shared LED enum
  • Motor Control (lib/motor) - DRV8825/FastAccelStepper setup, position tracking, bounded movement primitives
  • TOF Sensor (lib/tof) - VL53L7CX initialization (4×4 multi-zone), aggregated minimum-distance reading with raw offset compensation
  • Motion Control (lib/motion_control) - TOF homing, safe max-extension move, balance routine, emergency stop handling
  • Communication (lib/comms) - ESP-NOW wireless protocol and ElegantOTA session management
  • Sensors (lib/sensors) - Bar02 pressure/depth and INA219 battery monitoring
  • PID Controller (lib/pid) - depth control algorithm with runtime gain updates
  • Profile Manager (lib/profile) - mission profile execution and flash-backed mission logging
  • Flash Storage (lib/flash_storage) - LittleFS CSV mission log and replay helpers
  • LED Controller (lib/led) - RGB status indication system

ESPA State Machine

The ESPA firmware operates as a state machine coordinating motor control, sensors, and communications:

stateDiagram-v2
[*] --> INIT: Power On
INIT --> HOMING: Sensors OK
INIT --> ERROR: Init Failed
HOMING --> IDLE: Homing Success
HOMING --> ERROR: Homing Failed/Timeout
IDLE --> EXECUTING: Command Received
IDLE --> IDLE: No Command
EXECUTING --> PROFILE: GO Command
EXECUTING --> BALANCE: BALANCE Command
EXECUTING --> SEND_DATA: LISTENING Command
EXECUTING --> CLEAR_DATA: CLEAR_SD Command
EXECUTING --> UPDATE_PID: PARAMS Command
EXECUTING --> UPDATE_PID_EXT: PARAMS_EXT Command
EXECUTING --> TEST_SPEED: TEST_FREQ Command
EXECUTING --> TEST_STEPS: TEST_STEPS Command
EXECUTING --> DEBUG_MODE: DEBUG Command
EXECUTING --> HOMING: HOME_MOTOR Command
EXECUTING --> OTA: TRY_UPLOAD Command
EXECUTING --> SYRINGE_SET: SYRINGE_SET Command
EXECUTING --> PID_HOLD: PID_HOLD Command
EXECUTING --> PID_STEP: PID_STEP Command
EXECUTING --> SET_SURFACE_OFFSET: SURFACE_OFFSET Command
PROFILE --> PID_CONTROL: Descending
PID_CONTROL --> PID_CONTROL: Depth Control Active
PID_CONTROL --> ASCENT: Target Reached/Timeout
ASCENT --> IDLE_W_DATA: At Surface
BALANCE --> IDLE: Balance Complete
SEND_DATA --> IDLE: Data Sent
CLEAR_DATA --> IDLE: Flash Log Cleared
UPDATE_PID --> IDLE: Gains Updated
UPDATE_PID_EXT --> IDLE: Period/alpha Updated
TEST_SPEED --> IDLE: Speed Stored
TEST_STEPS --> IDLE: Test Move Complete
DEBUG_MODE --> IDLE: Debug Toggle Complete
OTA --> IDLE: Upload Complete
SYRINGE_SET --> IDLE: Bench Test Complete
PID_HOLD --> IDLE: Hold Complete / Timeout
PID_STEP --> IDLE: Step Complete / Timeout
SET_SURFACE_OFFSET --> IDLE: Offset Stored
IDLE_W_DATA --> SENDING: LISTENING Command
SENDING --> IDLE: Data Transmitted
ERROR --> [*]: Manual Reset Required
note right of IDLE
RGB: Green Solid
Waiting for command
end note
note right of HOMING
RGB: Purple Blink
TOF-based homing
end note
note right of PID_CONTROL
RGB: Cyan Blink
Active depth control
end note
note right of ERROR
RGB: Red Blink
Fatal error state
end note
Loading

COMMUNICATION PROTOCOL

Command Lifecycle

A command life-cycle does not overlaps/interfere with the previous nor the next one: when the CS sends a command (and it arrives to the FLOAT), a feedback from the FLOAT should inform about the acceptance of the command and if this acknowledgement arrives within a given period (specified later), next command requests will be ignored until end of execution of the current one, signaled by an idle acknowledgement.

If the acknowledgement doesn't arrive within that time span, the command commit can be considered failed: this could happen for WiFi connection failures or FLOAT electronics issues.

When waiting for the command commit acknowledgement, other command requests will be ignored as well.

As already mentioned, after command completion the FLOAT will try to send an idle acknowledgement to signal that it is listening for a new command: together with the idle state, this acknowledgement can also inform about the presence of new flash-backed profile data that has to be sent to the CS. After an idle acknowledgement is received, a new command can be accepted.

To maintain consistency with the status stored on the ESPB, and hence with the GUI visuals, the FLOAT grants to send the acknowledgement signalling a command commit only when the commit can be given for sure. In the same way, if the acknowledgement fails to be sent due to connection issues, the command is not committed.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant WiFi as ESP-NOW
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Fresh State
CS->>ESPB: Send Command (e.g., "GO")
ESPB->>WiFi: Forward Command
WiFi->>ESPA: Deliver Command
ESPA->>ESPA: Validate & Accept
ESPA->>WiFi: ACK (e.g., "GO_RECVD")
WiFi->>ESPB: Deliver ACK
ESPB->>ESPB: Update State (status=2)
ESPB->>CS: Forward ACK
Note over ESPA: Executing Command...
ESPA->>ESPA: Complete Task
ESPA->>WiFi: Completion ACK (e.g., "FLOAT_IDLE")
WiFi->>ESPB: Deliver Completion
ESPB->>ESPB: Update State (status=0)
ESPB->>CS: Forward Completion
Note over CS: Ready for Next Command
Loading

ESPB Bridge Role

The ESPB role is only to make the CS task of continuously checking on the WiFi channel less resource consuming.

In particular, ESPB receives commands from CS via USB only to forward them to the FLOAT via WiFi. At the same time, it can receive feedback and data from the FLOAT. In the latter case the ESPB will forward the packages on the USB channel, while using them to update an internal state accordingly. The only code that should trigger the update of the state stored on ESPB is the firmware on the FLOAT, via the data and the feedback sent to CS (more later). This is because no assumptions have to be done by other software on the commands completion and acceptance.

The ESPB state should mirror the FLOAT state at each moment (more details later) and can be used by the GUI to give visual feedback to the user or to drive its internal logic. It can be requested to the ESPB by the CS at any moment with a specific command. The state info sent to the CS with this command also contains WiFi connection state info and AM activation state info.

In general, the ESPB feedback could be stale when requested (for example because the CS could poll it with a low frequency), so to get fresh, real-time data the CS should listen on the USB channel for the FLOAT packages, after a command request or when waiting for command completion and retrieve the FLOAT current state directly by those packages.

Periodic polling remains a legit choice in case the CS cannot exploit interrupts triggered by Serial connection, but notice that this solution leads to delayed GUI visual feedback with respect to the changes on the FLOAT state.

In some cases, connection losses can undermine consistency between the feedback of the ESPB (either they are polled or real time) and the real current FLOAT state (consistency threats for each FLOAT state later).

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Stale State Polling
CS->>ESPB: Send Command
ESPB->>ESPA: Forward Command
ESPA->>ESPA: Accept & Execute
ESPA->>ESPB: ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward ACK
Note over ESPA: Command Executing...
loop Periodic Polling
CS->>ESPB: Request STATUS
ESPB->>CS: Return Cached State
Note over CS: State may be stale<br/>if ESPA completed recently
end
ESPA->>ESPA: Complete Command
ESPA->>ESPB: Completion ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward Completion
CS->>ESPB: Request STATUS
ESPB->>CS: Return Fresh State
Loading

FLOAT Commands

Table of FLOAT commands with relative effects and acknowledgements:

Cmd stringCmd ESPA numberCmd effectsESPA ack stringESPA ack effects on ESPB state
GO1Performs the two MATE vertical profiles, sends the pre-descent data packet before the first descent, and logs pressure/depth records to flash CSVGO_RECVDstatus to 2 (command execution)
LISTENING2Streams flash CSV records as JSON data packets at 5-second cadence, followed by STOP_DATAAck is data itselfstatus to 2 after first package arrival
BALANCE3Cycles full extension and retraction with holdMs holds until Bar02 pressure rises above the startup baseline by BALANCE_STOP_PRESSURE_DELTA_KPA. Requires the motor to be homed first — otherwise the command fails with Balance: homing requiredCMD3_RECVDstatus to 2
CLEAR_SD4Clears and recreates the flash CSV log, and clears the legacy EEPROM buffer. The command string is kept as CLEAR_SD for compatibilityCMD4_RECVDstatus to 2
SWITCH_AUTO_MODE5Toggles FLOAT Auto ModeSWITCH_AM_RECVDstatus to 2, AM activation state toggled
SEND_PACKAGE6Sends a single live JSON snapshot containing company number, time, pressure, judge/reference depth, phase, and raw sensor depthAck is the package itselfstatus to 2
TRY_UPLOAD7Starts the ElegantOTA access point on ESPA for a 5-minute upload window, then restores ESP-NOWTRY_UPLOAD_RECVDstatus to 2
PARAMS kp ki kd8Updates PID gains at runtimeCHNG_PARMS_RECVDstatus to 2
TEST_FREQ freq9Sets manual test movement speed, clamped to 10-1200 steps/sTEST_FREQ_RECVDstatus to 2
TEST_STEPS n10Moves the motor by n relative steps at the current test speedTEST_STEPS_RECVDstatus to 2
DEBUG11Toggles remote debug forwarding through DebugSerialDEBUG_MODE_RECVDstatus to 2
HOME_MOTOR12Runs TOF-based homing remotelyHOME_RECVDstatus to 2
STOP13Triggers a remote emergency stop, stops the motor, disables outputs, and returns to idleSTOP_RECVDstatus to 2
PARAMS_EXT period alpha14Updates PID tick period (ms) and derivative LPF coefficient alphaD at runtimeCHNG_PID_EXT_RECVDstatus to 2
SYRINGE_SET u dur_s15Bench test: drives the syringe to normalized position u ∈ [0,1] for dur_s seconds, logging depth — bypasses the PID (DC gain / time-constant characterization)SYRINGE_SET_RECVDstatus to 2
PID_HOLD depth dur_s16Bench test: holds depth at depth_m for dur_s seconds with the PID active, logging at 5 HzPID_HOLD_RECVDstatus to 2
PID_STEP depth17Bench test: step response — drives the PID to depth_m for up to 60 s, logging at 10 HzPID_STEP_RECVDstatus to 2
SURFACE_OFFSET m18Sets the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime: the FLOAT will hold its top m metres below the waterline when "floating" (default 0.10)SURFACE_OFF_RECVDstatus to 2
STATUS-Requests stale ESPB status plus AM state, WiFi connection state, battery millivolts, and last RSSI--

Once a command is completed, ESPA acknowledgement can be:

ESPA ack stringESPA ack effects on ESPB stateESPA state
FLOAT_IDLEstatus to 0 (idle)Idle with no data to be sent
FLOAT_IDLE_W_DATAstatus to 1 (idle with data to be sent)Idle with data from last profile to be sent

STATUS COMMAND: ESPB RESPONSE

ESPB response to STATUS command is composed by five parts of information: ESPA state (stale), activation of the AM on the FLOAT, WiFi connection state, last received battery millivolts, and last received RSSI. The WiFi connection state is detected by sending a dummy command code 0, while the other states are kept consistent with the ones on the FLOAT by updating them after acknowledgements reception.

ESPA state:

ESPB state stringESPB state numberState description
UNKNOWN-1ESPB has not received any state message from ESPA since boot
CONNECTED0The FLOAT is listening for new command. Previous command succeeded
CONNECTED_W_DATA1The FLOAT is listening for new command and has some new data from last profile to be sent. Previous command succeeded
EXECUTING_CMD2FLOAT is executing a command
STATUS_ERROR-Internal error in reading the state number

WARNING:
If committing a profile automatically, the relative acknowledgement will likely fail due to connection loss. The profile is committed anyway as it is generated from connection loss in the first place, but the GUI may not have mean to detect it. So it will likely read an inconsistent idle status (CONNECTED or CONNECTED_W_DATA) until FLOAT is at water level with a stable WiFi connection. In the meantime the command commits will fail, for connection loss or because the FLOAT is underwater. Anyway WiFi connection state can be detected by the STATUS command, hence giving feedback on status consistency.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over ESPA: Auto Mode Active
Note over ESPA,ESPB: Connection Lost!
ESPA->>ESPA: Detect Connection Loss
ESPA->>ESPA: Auto-commit Profile
Note over ESPA: Descending...<br/>WiFi Unavailable
ESPA-xESPB: ACK Fails (No Connection)
Note over ESPB: State Becomes Inconsistent<br/>Still shows "IDLE"
CS->>ESPB: Request STATUS
ESPB->>CS: CONNECTED | CONN_LOST
Note over CS: GUI shows inconsistent state<br/>but WiFi loss detected
Note over ESPA: At Surface...<br/>WiFi Restored
ESPA->>ESPB: FLOAT_IDLE_W_DATA
ESPB->>ESPB: Update to Consistent State
ESPB->>CS: Forward State
Note over CS,ESPA: Consistency Restored
Loading

AM state:

ESPB state stringState description
AUTO_MODE_YESAM on FLOAT is activated
AUTO_MODE_NOAM on FLOAT is not activated: connection losses will not trigger profiles

WiFi connection state:

ESPB state stringState description
CONN_OKWiFi connection is ok
CONN_LOSTWiFi connection is currently down. ESPB state could be wrong

Battery and RSSI fields:

ESPB fieldState description
BATTERY: <mV>Last battery voltage received from ESPA acknowledgements
RSSI: <dBm>Last ESP-NOW packet RSSI captured by ESPB promiscuous callback

Example of ESPB state response:CONNECTED_W_DATA | AUTO_MODE_NO | CONN_OK | BATTERY: 12450 | RSSI: -63

At ESPB boot, before any ESPA packet is received, a status request may return: UNKNOWN | AUTO_MODE_NO | CONN_LOST | BATTERY: 0 | RSSI: 0.

GUI / ESPB / ESPA Protocol Contract

The GUI sends command strings to ESPB over USB serial. ESPB parses the string, sends the command number to ESPA over ESP-NOW, and forwards ESPA acknowledgements/data back to the GUI.

GUI commandESPA command numberESPA acknowledgement / response
GO1GO_RECVD
LISTENING2Stored data packets, then STOP_DATA
BALANCE3CMD3_RECVD
CLEAR_SD4CMD4_RECVD
SWITCH_AUTO_MODE5SWITCH_AM_RECVD
SEND_PACKAGE6Live JSON packet
TRY_UPLOAD7TRY_UPLOAD_RECVD
PARAMS kp ki kd8CHNG_PARMS_RECVD
TEST_FREQ freq9TEST_FREQ_RECVD
TEST_STEPS n10TEST_STEPS_RECVD
DEBUG11DEBUG_MODE_RECVD
HOME_MOTOR12HOME_RECVD
STOP13STOP_RECVD
PARAMS_EXT period_ms alpha_d14CHNG_PID_EXT_RECVD
SYRINGE_SET u dur_s15SYRINGE_SET_RECVD
PID_HOLD depth_m dur_s16PID_HOLD_RECVD
PID_STEP depth_m17PID_STEP_RECVD
SURFACE_OFFSET m18SURFACE_OFF_RECVD
STATUS-ESPB local status line with five `

The peer MAC addresses are configured centrally in include/config.h: MAC_ESPA is used by ESPB, and MAC_ESPB is used by ESPA.


LED STATUS INDICATORS

The FLOAT is equipped with RGB LEDs on both ESP32 boards that provide visual feedback about the system status:

ESPA (Float Board) LED States:

Driven by LEDState (scoped enum in lib/led/include/led.h):

LED Color/PatternStateDescription
Green Solid / Boot BlinksLEDState::INITSystem initializing
Green SolidLEDState::IDLEReady and idle, waiting for commands
Green BlinkLEDState::IDLE_WITH_DATAIdle with data ready to send
Red SolidLEDState::LOW_BATTERYBattery voltage below BATT_THRESH (12.0 V)
Red BlinkLEDState::ERRORError state or motor emergency stop
Blue SolidLEDState::PROFILERunning non-PID profile phase
Yellow BlinkLEDState::AUTO_MODEAuto mode active
Purple BlinkLEDState::HOMINGMotor homing in progress
Purple SolidLEDState::MOTOR_MOVINGMotor moving
Cyan BlinkLEDState::PID_CONTROLPID depth control active
White SolidLEDState::COMMUNICATIONCommand received / communicating with ESPB
Orange BlinkLEDState::OTA_MODEOTA update mode active
OffLEDState::OFFSystem off or disabled

ESPB uses a separate FloatLEDState enum (LED_* prefix) defined in include/float_common.h; the two enums are deliberately independent because the two boards have different LED states to signal.

ESPB (Communication Bridge) LED States:

LED PatternStateDescription
Solid OnLED_IDLEConnected and ready
Very Fast BlinkLED_ERRORCommunication error
OffLED_OFFSystem off or disabled

Note: ESPB uses the built-in LED (pin 2) with different blink patterns to indicate status, as it does not have external RGB connections.


DEVELOPMENT AND TESTING

PlatformIO Environments

EnvironmentPurposeMain Source
espAFloat controller firmware with sensors, TOF homing, motion control, PID, ESP-NOW, and OTAsrc/espA/main.cpp
espA_poolESPA firmware compiled with conservative 70 cm pool-test targets (POOL_TEST_PROFILE)src/espA/main.cpp
espBUSB-to-ESP-NOW bridge for the Control Stationsrc/espB/main.cpp
espA_manual_keyboardBench firmware for serial keyboard continuous motor movement without homingsrc/espA_manual_keyboard/main.cpp

Common commands:

pio run -e espA
pio run -e espA_pool
pio run -e espB
pio run -e espA_manual_keyboard
pio test -e espA

CLI Startup

Run all commands from the project root:

cd Float_2025

To build and upload the main firmware targets:

pio run -e espA -t upload
pio run -e espB -t upload

For a conservative shallow-pool test at about 70 cm, upload ESPA with:

pio run -e espA_pool -t upload

To open the serial monitor at 115200 baud:

pio device monitor -e espA
pio device monitor -e espB

Direct USB Tuning Commands (ESPA)

All commands in the FLOAT Commands table can be sent over the ESPB USB serial bridge using the same string syntax. The commands below — useful for bench tuning — can also be sent directly over ESPA's USB serial port (e.g. when ESPA is wired to a laptop for tuning runs), bypassing ESPB and ESP-NOW entirely.

CommandEffect
PARAMS <kp> <ki> <kd>Update PID gains at runtime (same effect as command 8)
PARAMS_EXT <period_ms> <alpha_d>Update PID tick period and derivative LPF coefficient (command 14)
SYRINGE_SET <u> <dur_s>Drive the syringe to position u ∈ [0,1] for dur_s seconds and log depth — bypasses the PID, useful for DC-gain and time-constant estimation (command 15)
PID_HOLD <depth_m> <dur_s>Hold PID at depth_m for dur_s seconds, log at 5 Hz (command 16)
PID_STEP <depth_m>Step response: PID at depth_m for up to 60 s, log at 10 Hz (command 17)
SURFACE_OFFSET <m>Set the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime (command 18)

CLI Tests

To run all available tests for the espA environment:

pio test -e espA

To run a single test:

pio test -e espA -f unit_hw/motor/test_max_steps
pio test -e espA -f unit_hw/motor/test_speed
pio test -e espA -f integration/test_screw_lead_20mm
pio test -e espA -f integration/test_homing_only
pio test -e espA -f integration/test_tof_reading
pio test -e espA -f integration/test_homing_move_to_max
pio test -e espA -f integration/test_motor_direction
pio test -e espA -f integration/test_tof_motor_accuracy

To test ESPB without ESPA powered on:

pio test -e espB -f unit_hw/espb_bridge/test_parser
pio test -e espB -f unit_hw/espb_bridge/test_status_format
pio test -e espB -f unit_hw/espb_bridge/test_protocol_contract

To test the real ESPB-to-ESPA bridge, first upload the real espA firmware, wait until ESPA is idle, then run:

pio test -e espB -f integration/test_espnow_bridge

This test only uses the dummy command 0 and SWITCH_AUTO_MODE; it does not start profiles or move the motor.

Available tests:

TestCommandWhat it checks
test_max_stepspio test -e espA -f unit_hw/motor/test_max_stepsMoves only the motor to the safe maximum extension starting from logical position 0
test_speedpio test -e espA -f unit_hw/motor/test_speedMoves only the motor through 6 alternating 40 mm moves, increasing speed and acceleration up to 2300
test_screw_lead_20mmpio test -e espA -f integration/test_screw_lead_20mmRuns TOF homing, moves the motor by 20 mm, and compares the internal TOF delta
test_motor_directionpio test -e espA -f integration/test_motor_directionMoves only the motor forward/backward and verifies the logical direction; by default it does not use TOF
test_tof_readingpio test -e espA -f integration/test_tof_readingInitializes only the TOF sensor and checks valid readings for about 30 s
test_homing_onlypio test -e espA -f integration/test_homing_onlyRuns only TOF-based homing
test_homing_move_to_maxpio test -e espA -f integration/test_homing_move_to_maxRuns TOF homing and then moves to the safe maximum extension
test_tof_motor_accuracypio test -e espA -f integration/test_tof_motor_accuracyCompares TOF distance and motor position after homing
test_parserpio test -e espB -f unit_hw/espb_bridge/test_parserVerifies GUI/Serial command parsing into ESPA packets without ESPA powered on
test_status_formatpio test -e espB -f unit_hw/espb_bridge/test_status_formatVerifies ESPB cached state and the five-field STATUS format
test_protocol_contractpio test -e espB -f unit_hw/espb_bridge/test_protocol_contractLocks the command/ACK consistency contract between GUI, ESPB, and ESPA
test_espnow_bridgepio test -e espB -f integration/test_espnow_bridgeVerifies real ESP-NOW with the real ESPA firmware powered on, without movement

The test_max_steps, test_speed, and test_motor_direction tests are useful when you need to move only the motor without TOF homing. Before running them, make sure the piston is away from the mechanical end stops and can move in both directions.

Manual Motor-Only Control

To upload the bench firmware that lets you move the motor from the serial keyboard:

pio run -e espA_manual_keyboard -t upload
pio device monitor -e espA_manual_keyboard

Commands in the serial monitor:

KeyAction
Up arrow or wHold to move toward home/up
Down arrow or sHold to move toward extension/down
Space or xStop immediately and disable motor outputs
pPrint the current position
tPrint one TOF reading
h or ?Print help

This firmware does not run homing: at startup it assigns a centered logical position and moves while it receives repeated keypresses; when the key is released it stops automatically after a short timeout. During movement it periodically prints motor position and TOF distance. Use it only when the mechanism is in a physically safe position.

Test Layout

Hardware-oriented tests are stored under test/:

  • test/unit_hw/motor/test_max_steps checks safe maximum extension from a known zero
  • test/unit_hw/motor/test_speed checks alternating 40 mm moves while speed and acceleration increase up to 2300
  • test/integration/test_screw_lead_20mm checks the configured screw pitch, starts, and lead with one 20 mm move measured internally by TOF
  • test/integration/test_tof_reading checks that the TOF sensor initializes and returns valid distance samples for about 30 seconds
  • test/integration/test_homing_only checks TOF-based homing
  • test/integration/test_homing_move_to_max checks homing followed by safe full extension
  • test/integration/test_motor_direction checks logical/physical motion direction, optionally using TOF
  • test/integration/test_tof_motor_accuracy checks TOF and motor movement consistency

Continuous Integration

GitHub Actions builds all three PlatformIO environments (espA, espB, espA_pool) on every push to any branch and on every pull request to master. Workflow file: .github/workflows/ci.yml.

CI does not run the unit_hw/ or integration/ PlatformIO tests because they need a real ESP32 with the float wired up. Run those locally on the bench.

Pushing a v* tag triggers .github/workflows/release.yml, which builds all three environments and attaches the resulting firmware.bin / firmware.elf to a GitHub Release auto-named after the tag.

See CONTRIBUTING.md for the full git workflow (trunk-based with PR review on master), commit conventions, and one-time branch protection setup.


UTILITIES AND RESOURCES

Arduino Library Repositories:

Development Tools:

  • PlatformIO IDE: Modern embedded development platform
  • ESP32 Arduino Core: Framework for ESP32 development
  • FastAccelStepper Library: Timer/task-driven stepper motor control
  • VL53L7CX Library: Multi-zone Time-of-Flight sensor driver

GLOSSARY

  • AM (Auto Mode): Autonomous operation mode that triggers profiles on connection loss
  • CS (Control Station): Ground-based computer running the GUI application
  • ESPA: ESP32 mounted on the Float board (primary controller)
  • ESPB: ESP32 communication bridge between Float and CS
  • Flash Profile Log: Current onboard LittleFS CSV storage used before JSON transmission
  • FastAccelStepper: Timer/task-driven stepper library used by MotorController
  • MotionController: Firmware layer that combines motor, TOF, LEDs, debug, timeouts, and emergency stops for safe movement routines
  • Commit a command: To accept a sent command. After commit, command execution and success is ideally granted
  • Complete a command: To execute all the requirements requested by a command
  • Profile: A complete mission cycle (descent → depth control → ascent → data transmission)
  • TOF (Time-of-Flight): Non-contact distance measurement technology using light pulses
  • Homing: Process of establishing the motor's zero reference position
  • PID Control: Proportional-Integral-Derivative controller for precise depth maintenance
  • ESP-NOW: Low-latency peer-to-peer WiFi communication protocol by Espressif

Documentation Version: 11.2.0 Last Updated: May 2026

Recent changes:

  • PID output normalized to u ∈ [0, 1] (fraction of syringe travel). Default gains Kp = 0.17, Kd = 0.13, expressed per metre of depth error so they stay valid if MOTOR_MAX_STEPS changes.
  • Motor geometry: home = piston fully inserted (empty syringes, floats); full extension = piston extracted (full syringes, sinks). The mapping uToMotorPos() in include/config.h encapsulates MOTOR_INVERT_LOGICAL so motion code never hard-codes signs.
  • TOF safety range widened to [40, 85] mm to give 10 mm of margin above the homing threshold without risking the mechanical end stop.
  • balance now refuses to start without a prior homing (was forcing pos = 0 as a fallback, mechanically risky).
  • New SURFACE_TARGET_OFFSET_M constant and SURFACE_OFFSET <m> command (number 18) for tuning the surface idle position at runtime. Team Contact: PoliTOcean @ Politecnico di Torino Maintainers: Colabella Davide, Benevenga Filippo

About

Firmware for the float project

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PoliTOcean Float 2025 - Technical Documentation

CI

Version: 11.2.0 Team: PoliTOcean @ Politecnico di Torino
Maintainers: Colabella Davide, Benevenga Filippo
Competition: MATE ROV 2025/26


TABLE OF CONTENTS


PROJECT OVERVIEW

Introduction and Requirements

By MATE 2026 requirements documentation (Task 4.1 - MATE Floats Under the Ice), the FLOAT must complete operational vertical profiling missions under simulated ice conditions.

Pre-Deployment Requirements:

  • The FLOAT must communicate with the Mission Station (CS) prior to descending, transmitting a defined data packet containing:
    • Company number (provided by MATE)
    • Time data (UTC/local/float time)
    • Pressure data (pa or kpa) and/or depth data (m or cm)
    • Additional data as required
  • Example packet: EX01 1:51:42 UTC 9.8 kpa 1.00 meters

Vertical Profile Requirements:

The FLOAT must complete two vertical profiles using a buoyancy engine (fluid displacement system, not thrusters). Each profile consists of:

  1. Descent Phase: Descend from surface to 2.5 meters depth (± 33 cm)
  2. Deep Hold: Maintain depth at 2.5 meters for 30 seconds (bottom of float as reference)
  3. Ascent Phase: Rise to 40 cm depth (± 33 cm) without breaking surface or contacting ice
  4. Shallow Hold: Maintain depth at 40 cm for 30 seconds (top of float as reference)

Data Collection & Transmission:

  • Collect depth/pressure measurements during both profiles and transmit judge packets every 5 seconds (minimum 20 data packets)
  • Store data in ESP32 internal flash as a LittleFS CSV containing: company number, profile id, time, pressure, judge/reference depth, phase, and raw sensor depth
  • After recovery, transmit all collected data wirelessly to the Mission Station
  • Data packets must show 7 sequential measurements (spanning 30 seconds at 5-second intervals: 0, 5, 10, 15, 20, 25, 30) confirming proper depth maintenance at both 2.5m and 0.4m

Post-Mission Requirements:

  • Upon surface recovery, autonomously transmit all profile data to the CS
  • CS GUI plots depth over time using received data (minimum 20 data packets required)
  • Graph must display time (X-axis) vs depth (Y-axis) for both completed profiles

Current firmware storage note: the active implementation uses the internal flash CSV log (FLASH_LOG_PATH) as the primary mission data source. EEPROM compact records remain only as an internal legacy buffer. The legacy serial command name is still CLEAR_SD, but it now resets the flash CSV log and the legacy EEPROM buffer.

Auto Mode (AM):

An autonomous operating mode that triggers profile execution in case of connection loss with the CS, ensuring mission completion if communication is temporarily unavailable. AM will autonomously commit up to two profiles when connection is lost, preventing incomplete missions due to transient WiFi failures.

Penalties:

  • Breaking surface or contacting ice sheet during profile: -5 points per profile
  • FLOAT must remain submerged between 40 cm and 2.5 m throughout the ascent/descent phases

System Behavior

The general idea is that the FLOAT provides some micro-services that the CS can activate by sending commands to it. Every command can be requested at any moment, with the only limit that a command can be accepted by the FLOAT only when the previous one has been completed (more info on command cycle later).

The FLOAT has two main logical states: the command execution one, and the idle one in which it waits for the new command. In idle state, the FLOAT can have buffered flash data from the last completed profile that can be sent to the CS.

Syringe / Motor Convention

The FLOAT changes its buoyancy by pulling and pushing water through a pair of syringes driven by a stepper motor through a lead screw. The mechanical convention is:

  • Home (motor_pos = 0): piston fully inserted, syringes empty of water → the FLOAT floats. At this position the TOF reads ≈ TOF_HOMING_THRESHOLD (75 mm) because the piston is far from the sensor.
  • Full extension (motor_pos = uToMotorPos(1.0f)): piston extracted, syringes full of water → the FLOAT sinks. TOF reads ≈ TOF_SAFE_RANGE_MIN_MM (40 mm).
  • PID logical convention: u ∈ [0, 1] with u = 0 → float (empty) and u = 1 → sink (full). The helper uToMotorPos(u) in include/config.h maps u to the actual motor target while respecting MOTOR_INVERT_LOGICAL, so callers never hard-code signs.

TOF safety limits used during motion:

ConstantDefaultMeaning
TOF_HOMING_THRESHOLD75 mmPhase 2 of homing stops when the TOF reads above this
TOF_HOMING_APPROACH_MM50 mmPhase 1 of homing stops when the TOF reads below this
TOF_SAFE_RANGE_MIN_MM40 mmLower bound: syringe fully extended (mechanical limit)
TOF_SAFE_RANGE_MAX_MM85 mmUpper bound: 10 mm above the homing threshold; any higher and the piston would risk hitting the back mechanical stop

Surface Target Offset

When the FLOAT is "floating", we usually want its top a few centimetres below the water surface — not exactly at the waterline — so that the float remains visible without being completely above water. This is controlled by SURFACE_TARGET_OFFSET_M (default 0.10 m: top of the float 10 cm below the surface).

Two ways to change it:

  • At compile time: edit SURFACE_TARGET_OFFSET_M in include/config.h.
  • At runtime: send command SURFACE_OFFSET <m> via the CS, or SURFACE_OFFSET <m> over USB serial on ESPA. The change persists until the next reboot.

The offset is geometry-agnostic: FLOAT_TOP_TO_SENSOR_M (geometric distance between the top of the float and the barometer) and SURFACE_TARGET_OFFSET_M (operational target) are kept as separate constants in include/config.h.


HARDWARE CONFIGURATION

Hardware Used

HardwareRoleLinkKey parameters / notes
ESP32 Dev Module x2ESPA float controller and ESPB communication bridgeEspressif ESP32Arduino framework, ESP-NOW link, USB serial bridge on ESPB
DRV8825 stepper driverStepper motor driver for syringe motionPololu DRV8825 carrierSTEP/DIR control, active-low enable, SLEEP and RESET held HIGH during operation
Stepper motor with planetary gearboxSyringe actuator motorStepperOnline 17HS15-1684S-PG27NEMA 17, 200 steps/rev, 1.8 deg/step, configured gear ratio 26.85124:1, microstep setting 1
Lead screw / threaded rodConverts motor rotation to linear travelSIENOC 500 mm trapezoidal lead screwPitch 2.0 mm, 4 starts, lead 8.0 mm/rev, configured travel 35 mm
VL53L7CX Time-of-Flight sensorNon-contact homing distance sensor (multi-zone)ST VL53L7CXI2C 0x29, LPn (XSHUT) GPIO16, GPIO1 GPIO15, 4×4 zone mode with central-zone mask 0x0660, 6 mm raw offset, 75 mm homing threshold
Bar02 pressure sensorPressure/depth measurementBlue Robotics Bar02MS5837_02BA model, I2C 0x76, used for depth and pressure
INA219 battery monitorBattery bus-voltage monitorAdafruit INA219 breakoutI2C 0x40, initialized at 100 kHz, configured with 5 A max and 0.1 ohm shunt

Pin Mapping

ESPA (Float Controller) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Motor Control
DIRGPIO32DRV8825 DirectionStepper direction control
STEPGPIO33DRV8825 StepStep pulse generation
ENGPIO27DRV8825 EnableActive-LOW, disables outputs when HIGH
SLEEPGPIO25DRV8825 SleepActive-LOW, must be HIGH for operation
RSTGPIO26DRV8825 ResetActive-LOW, must be HIGH for operation
TOF Sensor
SDAGPIO21VL53L7CX I2C DataI2C bus (shared with sensors)
SCLGPIO22VL53L7CX I2C ClockI2C bus @ 1MHz
XSHUTGPIO16VL53L7CX LPn (shutdown)Sensor enable / shutdown control
GPIO1GPIO15VL53L7CX InterruptOptional interrupt pin, unused in polling mode
Sensors
SDAGPIO21Bar02, INA219I2C bus (shared)
SCLGPIO22Bar02, INA219I2C bus (shared)
Status LED
LED_RGPIO19Red ChannelPWM control
LED_GGPIO18Green ChannelPWM control
LED_BGPIO5Blue ChannelPWM control

ESPB (Communication Bridge) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Communication
TXGPIO1USB Serial115200 baud
RXGPIO3USB Serial115200 baud
Status LED
Built-in LEDGPIO2Onboard LEDStatus indication

I2C Device Addresses:

DeviceAddressBus Speed
VL53L7CX TOF0x291 MHz
Bar02 Pressure0x76Shared I2C bus
INA219 Battery0x40Initialized at 100 kHz

The firmware initializes the INA219 at 100 kHz, then the VL53L7CX driver raises the shared Wire clock to 1 MHz for TOF ranging.


SYSTEM ARCHITECTURE

Deployment Diagram

FLOAT code has to be deployed on two ESP32, one mounted on the FLOAT board (ESPA) together with the sensors and the power supply, and the other (ESPB) communicating with the Control Station via USB. The two ESP32 communicates via WiFi using ESP-NOW protocol. The software on the two ESP32 is designed to work regardless of the design of the GUI on the CS.

The idea is to bring all the complexity on the ESPA and GUI, leaving no trace of logic on the ESPB.

graph TB
subgraph "Control Station"
GUI[GUI Application]
USB[USB Serial]
end
subgraph "ESPB - Communication Bridge"
ESPB_FW[ESPB Firmware]
ESPB_WiFi[WiFi ESP-NOW]
ESPB_LED[Built-in LED]
end
subgraph "ESPA - Float Controller"
ESPA_FW[ESPA Firmware]
ESPA_WiFi[WiFi ESP-NOW]
subgraph "Motor System"
DRV8825[DRV8825 Driver]
STEPPER[Stepper Motor]
TOF[VL53L7CX TOF Sensor]
end
subgraph "Sensors"
BAR02[Bar02 Pressure]
INA219[INA219 Battery]
end
RGB[RGB LED]
I2C[I2C Bus]
end
GUI -->|Commands| USB
USB <-->|Serial 115200| ESPB_FW
ESPB_FW <-->|ESP-NOW 2.4GHz| ESPB_WiFi
ESPB_WiFi <-.->|WiFi| ESPA_WiFi
ESPA_WiFi <-->|ESP-NOW| ESPA_FW
ESPA_FW -->|Control Signals| DRV8825
DRV8825 -->|STEP/DIR| STEPPER
TOF -->|Distance Data| ESPA_FW
BAR02 -->|I2C| I2C
INA219 -->|I2C| I2C
I2C -->|Sensor Data| ESPA_FW
ESPA_FW -->|Status| RGB
ESPB_FW -->|Status| ESPB_LED
style ESPA_FW fill:#4CAF50
style ESPB_FW fill:#2196F3
style GUI fill:#FF9800
style TOF fill:#9C27B0
Loading

Software Structure

The project follows a modular architecture with separate compilation units:

  • Central Config (include/config.h) - pin mapping, motor constants, PID defaults, mission timing, network parameters
  • Shared Protocol (include/float_common.h) - ESP-NOW packet structs, ACK strings, EEPROM size, shared LED enum
  • Motor Control (lib/motor) - DRV8825/FastAccelStepper setup, position tracking, bounded movement primitives
  • TOF Sensor (lib/tof) - VL53L7CX initialization (4×4 multi-zone), aggregated minimum-distance reading with raw offset compensation
  • Motion Control (lib/motion_control) - TOF homing, safe max-extension move, balance routine, emergency stop handling
  • Communication (lib/comms) - ESP-NOW wireless protocol and ElegantOTA session management
  • Sensors (lib/sensors) - Bar02 pressure/depth and INA219 battery monitoring
  • PID Controller (lib/pid) - depth control algorithm with runtime gain updates
  • Profile Manager (lib/profile) - mission profile execution and flash-backed mission logging
  • Flash Storage (lib/flash_storage) - LittleFS CSV mission log and replay helpers
  • LED Controller (lib/led) - RGB status indication system

ESPA State Machine

The ESPA firmware operates as a state machine coordinating motor control, sensors, and communications:

stateDiagram-v2
[*] --> INIT: Power On
INIT --> HOMING: Sensors OK
INIT --> ERROR: Init Failed
HOMING --> IDLE: Homing Success
HOMING --> ERROR: Homing Failed/Timeout
IDLE --> EXECUTING: Command Received
IDLE --> IDLE: No Command
EXECUTING --> PROFILE: GO Command
EXECUTING --> BALANCE: BALANCE Command
EXECUTING --> SEND_DATA: LISTENING Command
EXECUTING --> CLEAR_DATA: CLEAR_SD Command
EXECUTING --> UPDATE_PID: PARAMS Command
EXECUTING --> UPDATE_PID_EXT: PARAMS_EXT Command
EXECUTING --> TEST_SPEED: TEST_FREQ Command
EXECUTING --> TEST_STEPS: TEST_STEPS Command
EXECUTING --> DEBUG_MODE: DEBUG Command
EXECUTING --> HOMING: HOME_MOTOR Command
EXECUTING --> OTA: TRY_UPLOAD Command
EXECUTING --> SYRINGE_SET: SYRINGE_SET Command
EXECUTING --> PID_HOLD: PID_HOLD Command
EXECUTING --> PID_STEP: PID_STEP Command
EXECUTING --> SET_SURFACE_OFFSET: SURFACE_OFFSET Command
PROFILE --> PID_CONTROL: Descending
PID_CONTROL --> PID_CONTROL: Depth Control Active
PID_CONTROL --> ASCENT: Target Reached/Timeout
ASCENT --> IDLE_W_DATA: At Surface
BALANCE --> IDLE: Balance Complete
SEND_DATA --> IDLE: Data Sent
CLEAR_DATA --> IDLE: Flash Log Cleared
UPDATE_PID --> IDLE: Gains Updated
UPDATE_PID_EXT --> IDLE: Period/alpha Updated
TEST_SPEED --> IDLE: Speed Stored
TEST_STEPS --> IDLE: Test Move Complete
DEBUG_MODE --> IDLE: Debug Toggle Complete
OTA --> IDLE: Upload Complete
SYRINGE_SET --> IDLE: Bench Test Complete
PID_HOLD --> IDLE: Hold Complete / Timeout
PID_STEP --> IDLE: Step Complete / Timeout
SET_SURFACE_OFFSET --> IDLE: Offset Stored
IDLE_W_DATA --> SENDING: LISTENING Command
SENDING --> IDLE: Data Transmitted
ERROR --> [*]: Manual Reset Required
note right of IDLE
RGB: Green Solid
Waiting for command
end note
note right of HOMING
RGB: Purple Blink
TOF-based homing
end note
note right of PID_CONTROL
RGB: Cyan Blink
Active depth control
end note
note right of ERROR
RGB: Red Blink
Fatal error state
end note
Loading

COMMUNICATION PROTOCOL

Command Lifecycle

A command life-cycle does not overlaps/interfere with the previous nor the next one: when the CS sends a command (and it arrives to the FLOAT), a feedback from the FLOAT should inform about the acceptance of the command and if this acknowledgement arrives within a given period (specified later), next command requests will be ignored until end of execution of the current one, signaled by an idle acknowledgement.

If the acknowledgement doesn't arrive within that time span, the command commit can be considered failed: this could happen for WiFi connection failures or FLOAT electronics issues.

When waiting for the command commit acknowledgement, other command requests will be ignored as well.

As already mentioned, after command completion the FLOAT will try to send an idle acknowledgement to signal that it is listening for a new command: together with the idle state, this acknowledgement can also inform about the presence of new flash-backed profile data that has to be sent to the CS. After an idle acknowledgement is received, a new command can be accepted.

To maintain consistency with the status stored on the ESPB, and hence with the GUI visuals, the FLOAT grants to send the acknowledgement signalling a command commit only when the commit can be given for sure. In the same way, if the acknowledgement fails to be sent due to connection issues, the command is not committed.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant WiFi as ESP-NOW
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Fresh State
CS->>ESPB: Send Command (e.g., "GO")
ESPB->>WiFi: Forward Command
WiFi->>ESPA: Deliver Command
ESPA->>ESPA: Validate & Accept
ESPA->>WiFi: ACK (e.g., "GO_RECVD")
WiFi->>ESPB: Deliver ACK
ESPB->>ESPB: Update State (status=2)
ESPB->>CS: Forward ACK
Note over ESPA: Executing Command...
ESPA->>ESPA: Complete Task
ESPA->>WiFi: Completion ACK (e.g., "FLOAT_IDLE")
WiFi->>ESPB: Deliver Completion
ESPB->>ESPB: Update State (status=0)
ESPB->>CS: Forward Completion
Note over CS: Ready for Next Command
Loading

ESPB Bridge Role

The ESPB role is only to make the CS task of continuously checking on the WiFi channel less resource consuming.

In particular, ESPB receives commands from CS via USB only to forward them to the FLOAT via WiFi. At the same time, it can receive feedback and data from the FLOAT. In the latter case the ESPB will forward the packages on the USB channel, while using them to update an internal state accordingly. The only code that should trigger the update of the state stored on ESPB is the firmware on the FLOAT, via the data and the feedback sent to CS (more later). This is because no assumptions have to be done by other software on the commands completion and acceptance.

The ESPB state should mirror the FLOAT state at each moment (more details later) and can be used by the GUI to give visual feedback to the user or to drive its internal logic. It can be requested to the ESPB by the CS at any moment with a specific command. The state info sent to the CS with this command also contains WiFi connection state info and AM activation state info.

In general, the ESPB feedback could be stale when requested (for example because the CS could poll it with a low frequency), so to get fresh, real-time data the CS should listen on the USB channel for the FLOAT packages, after a command request or when waiting for command completion and retrieve the FLOAT current state directly by those packages.

Periodic polling remains a legit choice in case the CS cannot exploit interrupts triggered by Serial connection, but notice that this solution leads to delayed GUI visual feedback with respect to the changes on the FLOAT state.

In some cases, connection losses can undermine consistency between the feedback of the ESPB (either they are polled or real time) and the real current FLOAT state (consistency threats for each FLOAT state later).

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Stale State Polling
CS->>ESPB: Send Command
ESPB->>ESPA: Forward Command
ESPA->>ESPA: Accept & Execute
ESPA->>ESPB: ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward ACK
Note over ESPA: Command Executing...
loop Periodic Polling
CS->>ESPB: Request STATUS
ESPB->>CS: Return Cached State
Note over CS: State may be stale<br/>if ESPA completed recently
end
ESPA->>ESPA: Complete Command
ESPA->>ESPB: Completion ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward Completion
CS->>ESPB: Request STATUS
ESPB->>CS: Return Fresh State
Loading

FLOAT Commands

Table of FLOAT commands with relative effects and acknowledgements:

Cmd stringCmd ESPA numberCmd effectsESPA ack stringESPA ack effects on ESPB state
GO1Performs the two MATE vertical profiles, sends the pre-descent data packet before the first descent, and logs pressure/depth records to flash CSVGO_RECVDstatus to 2 (command execution)
LISTENING2Streams flash CSV records as JSON data packets at 5-second cadence, followed by STOP_DATAAck is data itselfstatus to 2 after first package arrival
BALANCE3Cycles full extension and retraction with holdMs holds until Bar02 pressure rises above the startup baseline by BALANCE_STOP_PRESSURE_DELTA_KPA. Requires the motor to be homed first — otherwise the command fails with Balance: homing requiredCMD3_RECVDstatus to 2
CLEAR_SD4Clears and recreates the flash CSV log, and clears the legacy EEPROM buffer. The command string is kept as CLEAR_SD for compatibilityCMD4_RECVDstatus to 2
SWITCH_AUTO_MODE5Toggles FLOAT Auto ModeSWITCH_AM_RECVDstatus to 2, AM activation state toggled
SEND_PACKAGE6Sends a single live JSON snapshot containing company number, time, pressure, judge/reference depth, phase, and raw sensor depthAck is the package itselfstatus to 2
TRY_UPLOAD7Starts the ElegantOTA access point on ESPA for a 5-minute upload window, then restores ESP-NOWTRY_UPLOAD_RECVDstatus to 2
PARAMS kp ki kd8Updates PID gains at runtimeCHNG_PARMS_RECVDstatus to 2
TEST_FREQ freq9Sets manual test movement speed, clamped to 10-1200 steps/sTEST_FREQ_RECVDstatus to 2
TEST_STEPS n10Moves the motor by n relative steps at the current test speedTEST_STEPS_RECVDstatus to 2
DEBUG11Toggles remote debug forwarding through DebugSerialDEBUG_MODE_RECVDstatus to 2
HOME_MOTOR12Runs TOF-based homing remotelyHOME_RECVDstatus to 2
STOP13Triggers a remote emergency stop, stops the motor, disables outputs, and returns to idleSTOP_RECVDstatus to 2
PARAMS_EXT period alpha14Updates PID tick period (ms) and derivative LPF coefficient alphaD at runtimeCHNG_PID_EXT_RECVDstatus to 2
SYRINGE_SET u dur_s15Bench test: drives the syringe to normalized position u ∈ [0,1] for dur_s seconds, logging depth — bypasses the PID (DC gain / time-constant characterization)SYRINGE_SET_RECVDstatus to 2
PID_HOLD depth dur_s16Bench test: holds depth at depth_m for dur_s seconds with the PID active, logging at 5 HzPID_HOLD_RECVDstatus to 2
PID_STEP depth17Bench test: step response — drives the PID to depth_m for up to 60 s, logging at 10 HzPID_STEP_RECVDstatus to 2
SURFACE_OFFSET m18Sets the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime: the FLOAT will hold its top m metres below the waterline when "floating" (default 0.10)SURFACE_OFF_RECVDstatus to 2
STATUS-Requests stale ESPB status plus AM state, WiFi connection state, battery millivolts, and last RSSI--

Once a command is completed, ESPA acknowledgement can be:

ESPA ack stringESPA ack effects on ESPB stateESPA state
FLOAT_IDLEstatus to 0 (idle)Idle with no data to be sent
FLOAT_IDLE_W_DATAstatus to 1 (idle with data to be sent)Idle with data from last profile to be sent

STATUS COMMAND: ESPB RESPONSE

ESPB response to STATUS command is composed by five parts of information: ESPA state (stale), activation of the AM on the FLOAT, WiFi connection state, last received battery millivolts, and last received RSSI. The WiFi connection state is detected by sending a dummy command code 0, while the other states are kept consistent with the ones on the FLOAT by updating them after acknowledgements reception.

ESPA state:

ESPB state stringESPB state numberState description
UNKNOWN-1ESPB has not received any state message from ESPA since boot
CONNECTED0The FLOAT is listening for new command. Previous command succeeded
CONNECTED_W_DATA1The FLOAT is listening for new command and has some new data from last profile to be sent. Previous command succeeded
EXECUTING_CMD2FLOAT is executing a command
STATUS_ERROR-Internal error in reading the state number

WARNING:
If committing a profile automatically, the relative acknowledgement will likely fail due to connection loss. The profile is committed anyway as it is generated from connection loss in the first place, but the GUI may not have mean to detect it. So it will likely read an inconsistent idle status (CONNECTED or CONNECTED_W_DATA) until FLOAT is at water level with a stable WiFi connection. In the meantime the command commits will fail, for connection loss or because the FLOAT is underwater. Anyway WiFi connection state can be detected by the STATUS command, hence giving feedback on status consistency.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over ESPA: Auto Mode Active
Note over ESPA,ESPB: Connection Lost!
ESPA->>ESPA: Detect Connection Loss
ESPA->>ESPA: Auto-commit Profile
Note over ESPA: Descending...<br/>WiFi Unavailable
ESPA-xESPB: ACK Fails (No Connection)
Note over ESPB: State Becomes Inconsistent<br/>Still shows "IDLE"
CS->>ESPB: Request STATUS
ESPB->>CS: CONNECTED | CONN_LOST
Note over CS: GUI shows inconsistent state<br/>but WiFi loss detected
Note over ESPA: At Surface...<br/>WiFi Restored
ESPA->>ESPB: FLOAT_IDLE_W_DATA
ESPB->>ESPB: Update to Consistent State
ESPB->>CS: Forward State
Note over CS,ESPA: Consistency Restored
Loading

AM state:

ESPB state stringState description
AUTO_MODE_YESAM on FLOAT is activated
AUTO_MODE_NOAM on FLOAT is not activated: connection losses will not trigger profiles

WiFi connection state:

ESPB state stringState description
CONN_OKWiFi connection is ok
CONN_LOSTWiFi connection is currently down. ESPB state could be wrong

Battery and RSSI fields:

ESPB fieldState description
BATTERY: <mV>Last battery voltage received from ESPA acknowledgements
RSSI: <dBm>Last ESP-NOW packet RSSI captured by ESPB promiscuous callback

Example of ESPB state response:CONNECTED_W_DATA | AUTO_MODE_NO | CONN_OK | BATTERY: 12450 | RSSI: -63

At ESPB boot, before any ESPA packet is received, a status request may return: UNKNOWN | AUTO_MODE_NO | CONN_LOST | BATTERY: 0 | RSSI: 0.

GUI / ESPB / ESPA Protocol Contract

The GUI sends command strings to ESPB over USB serial. ESPB parses the string, sends the command number to ESPA over ESP-NOW, and forwards ESPA acknowledgements/data back to the GUI.

GUI commandESPA command numberESPA acknowledgement / response
GO1GO_RECVD
LISTENING2Stored data packets, then STOP_DATA
BALANCE3CMD3_RECVD
CLEAR_SD4CMD4_RECVD
SWITCH_AUTO_MODE5SWITCH_AM_RECVD
SEND_PACKAGE6Live JSON packet
TRY_UPLOAD7TRY_UPLOAD_RECVD
PARAMS kp ki kd8CHNG_PARMS_RECVD
TEST_FREQ freq9TEST_FREQ_RECVD
TEST_STEPS n10TEST_STEPS_RECVD
DEBUG11DEBUG_MODE_RECVD
HOME_MOTOR12HOME_RECVD
STOP13STOP_RECVD
PARAMS_EXT period_ms alpha_d14CHNG_PID_EXT_RECVD
SYRINGE_SET u dur_s15SYRINGE_SET_RECVD
PID_HOLD depth_m dur_s16PID_HOLD_RECVD
PID_STEP depth_m17PID_STEP_RECVD
SURFACE_OFFSET m18SURFACE_OFF_RECVD
STATUS-ESPB local status line with five `

The peer MAC addresses are configured centrally in include/config.h: MAC_ESPA is used by ESPB, and MAC_ESPB is used by ESPA.


LED STATUS INDICATORS

The FLOAT is equipped with RGB LEDs on both ESP32 boards that provide visual feedback about the system status:

ESPA (Float Board) LED States:

Driven by LEDState (scoped enum in lib/led/include/led.h):

LED Color/PatternStateDescription
Green Solid / Boot BlinksLEDState::INITSystem initializing
Green SolidLEDState::IDLEReady and idle, waiting for commands
Green BlinkLEDState::IDLE_WITH_DATAIdle with data ready to send
Red SolidLEDState::LOW_BATTERYBattery voltage below BATT_THRESH (12.0 V)
Red BlinkLEDState::ERRORError state or motor emergency stop
Blue SolidLEDState::PROFILERunning non-PID profile phase
Yellow BlinkLEDState::AUTO_MODEAuto mode active
Purple BlinkLEDState::HOMINGMotor homing in progress
Purple SolidLEDState::MOTOR_MOVINGMotor moving
Cyan BlinkLEDState::PID_CONTROLPID depth control active
White SolidLEDState::COMMUNICATIONCommand received / communicating with ESPB
Orange BlinkLEDState::OTA_MODEOTA update mode active
OffLEDState::OFFSystem off or disabled

ESPB uses a separate FloatLEDState enum (LED_* prefix) defined in include/float_common.h; the two enums are deliberately independent because the two boards have different LED states to signal.

ESPB (Communication Bridge) LED States:

LED PatternStateDescription
Solid OnLED_IDLEConnected and ready
Very Fast BlinkLED_ERRORCommunication error
OffLED_OFFSystem off or disabled

Note: ESPB uses the built-in LED (pin 2) with different blink patterns to indicate status, as it does not have external RGB connections.


DEVELOPMENT AND TESTING

PlatformIO Environments

EnvironmentPurposeMain Source
espAFloat controller firmware with sensors, TOF homing, motion control, PID, ESP-NOW, and OTAsrc/espA/main.cpp
espA_poolESPA firmware compiled with conservative 70 cm pool-test targets (POOL_TEST_PROFILE)src/espA/main.cpp
espBUSB-to-ESP-NOW bridge for the Control Stationsrc/espB/main.cpp
espA_manual_keyboardBench firmware for serial keyboard continuous motor movement without homingsrc/espA_manual_keyboard/main.cpp

Common commands:

pio run -e espA
pio run -e espA_pool
pio run -e espB
pio run -e espA_manual_keyboard
pio test -e espA

CLI Startup

Run all commands from the project root:

cd Float_2025

To build and upload the main firmware targets:

pio run -e espA -t upload
pio run -e espB -t upload

For a conservative shallow-pool test at about 70 cm, upload ESPA with:

pio run -e espA_pool -t upload

To open the serial monitor at 115200 baud:

pio device monitor -e espA
pio device monitor -e espB

Direct USB Tuning Commands (ESPA)

All commands in the FLOAT Commands table can be sent over the ESPB USB serial bridge using the same string syntax. The commands below — useful for bench tuning — can also be sent directly over ESPA's USB serial port (e.g. when ESPA is wired to a laptop for tuning runs), bypassing ESPB and ESP-NOW entirely.

CommandEffect
PARAMS <kp> <ki> <kd>Update PID gains at runtime (same effect as command 8)
PARAMS_EXT <period_ms> <alpha_d>Update PID tick period and derivative LPF coefficient (command 14)
SYRINGE_SET <u> <dur_s>Drive the syringe to position u ∈ [0,1] for dur_s seconds and log depth — bypasses the PID, useful for DC-gain and time-constant estimation (command 15)
PID_HOLD <depth_m> <dur_s>Hold PID at depth_m for dur_s seconds, log at 5 Hz (command 16)
PID_STEP <depth_m>Step response: PID at depth_m for up to 60 s, log at 10 Hz (command 17)
SURFACE_OFFSET <m>Set the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime (command 18)

CLI Tests

To run all available tests for the espA environment:

pio test -e espA

To run a single test:

pio test -e espA -f unit_hw/motor/test_max_steps
pio test -e espA -f unit_hw/motor/test_speed
pio test -e espA -f integration/test_screw_lead_20mm
pio test -e espA -f integration/test_homing_only
pio test -e espA -f integration/test_tof_reading
pio test -e espA -f integration/test_homing_move_to_max
pio test -e espA -f integration/test_motor_direction
pio test -e espA -f integration/test_tof_motor_accuracy

To test ESPB without ESPA powered on:

pio test -e espB -f unit_hw/espb_bridge/test_parser
pio test -e espB -f unit_hw/espb_bridge/test_status_format
pio test -e espB -f unit_hw/espb_bridge/test_protocol_contract

To test the real ESPB-to-ESPA bridge, first upload the real espA firmware, wait until ESPA is idle, then run:

pio test -e espB -f integration/test_espnow_bridge

This test only uses the dummy command 0 and SWITCH_AUTO_MODE; it does not start profiles or move the motor.

Available tests:

TestCommandWhat it checks
test_max_stepspio test -e espA -f unit_hw/motor/test_max_stepsMoves only the motor to the safe maximum extension starting from logical position 0
test_speedpio test -e espA -f unit_hw/motor/test_speedMoves only the motor through 6 alternating 40 mm moves, increasing speed and acceleration up to 2300
test_screw_lead_20mmpio test -e espA -f integration/test_screw_lead_20mmRuns TOF homing, moves the motor by 20 mm, and compares the internal TOF delta
test_motor_directionpio test -e espA -f integration/test_motor_directionMoves only the motor forward/backward and verifies the logical direction; by default it does not use TOF
test_tof_readingpio test -e espA -f integration/test_tof_readingInitializes only the TOF sensor and checks valid readings for about 30 s
test_homing_onlypio test -e espA -f integration/test_homing_onlyRuns only TOF-based homing
test_homing_move_to_maxpio test -e espA -f integration/test_homing_move_to_maxRuns TOF homing and then moves to the safe maximum extension
test_tof_motor_accuracypio test -e espA -f integration/test_tof_motor_accuracyCompares TOF distance and motor position after homing
test_parserpio test -e espB -f unit_hw/espb_bridge/test_parserVerifies GUI/Serial command parsing into ESPA packets without ESPA powered on
test_status_formatpio test -e espB -f unit_hw/espb_bridge/test_status_formatVerifies ESPB cached state and the five-field STATUS format
test_protocol_contractpio test -e espB -f unit_hw/espb_bridge/test_protocol_contractLocks the command/ACK consistency contract between GUI, ESPB, and ESPA
test_espnow_bridgepio test -e espB -f integration/test_espnow_bridgeVerifies real ESP-NOW with the real ESPA firmware powered on, without movement

The test_max_steps, test_speed, and test_motor_direction tests are useful when you need to move only the motor without TOF homing. Before running them, make sure the piston is away from the mechanical end stops and can move in both directions.

Manual Motor-Only Control

To upload the bench firmware that lets you move the motor from the serial keyboard:

pio run -e espA_manual_keyboard -t upload
pio device monitor -e espA_manual_keyboard

Commands in the serial monitor:

KeyAction
Up arrow or wHold to move toward home/up
Down arrow or sHold to move toward extension/down
Space or xStop immediately and disable motor outputs
pPrint the current position
tPrint one TOF reading
h or ?Print help

This firmware does not run homing: at startup it assigns a centered logical position and moves while it receives repeated keypresses; when the key is released it stops automatically after a short timeout. During movement it periodically prints motor position and TOF distance. Use it only when the mechanism is in a physically safe position.

Test Layout

Hardware-oriented tests are stored under test/:

  • test/unit_hw/motor/test_max_steps checks safe maximum extension from a known zero
  • test/unit_hw/motor/test_speed checks alternating 40 mm moves while speed and acceleration increase up to 2300
  • test/integration/test_screw_lead_20mm checks the configured screw pitch, starts, and lead with one 20 mm move measured internally by TOF
  • test/integration/test_tof_reading checks that the TOF sensor initializes and returns valid distance samples for about 30 seconds
  • test/integration/test_homing_only checks TOF-based homing
  • test/integration/test_homing_move_to_max checks homing followed by safe full extension
  • test/integration/test_motor_direction checks logical/physical motion direction, optionally using TOF
  • test/integration/test_tof_motor_accuracy checks TOF and motor movement consistency

Continuous Integration

GitHub Actions builds all three PlatformIO environments (espA, espB, espA_pool) on every push to any branch and on every pull request to master. Workflow file: .github/workflows/ci.yml.

CI does not run the unit_hw/ or integration/ PlatformIO tests because they need a real ESP32 with the float wired up. Run those locally on the bench.

Pushing a v* tag triggers .github/workflows/release.yml, which builds all three environments and attaches the resulting firmware.bin / firmware.elf to a GitHub Release auto-named after the tag.

See CONTRIBUTING.md for the full git workflow (trunk-based with PR review on master), commit conventions, and one-time branch protection setup.


UTILITIES AND RESOURCES

Arduino Library Repositories:

Development Tools:

  • PlatformIO IDE: Modern embedded development platform
  • ESP32 Arduino Core: Framework for ESP32 development
  • FastAccelStepper Library: Timer/task-driven stepper motor control
  • VL53L7CX Library: Multi-zone Time-of-Flight sensor driver

GLOSSARY

  • AM (Auto Mode): Autonomous operation mode that triggers profiles on connection loss
  • CS (Control Station): Ground-based computer running the GUI application
  • ESPA: ESP32 mounted on the Float board (primary controller)
  • ESPB: ESP32 communication bridge between Float and CS
  • Flash Profile Log: Current onboard LittleFS CSV storage used before JSON transmission
  • FastAccelStepper: Timer/task-driven stepper library used by MotorController
  • MotionController: Firmware layer that combines motor, TOF, LEDs, debug, timeouts, and emergency stops for safe movement routines
  • Commit a command: To accept a sent command. After commit, command execution and success is ideally granted
  • Complete a command: To execute all the requirements requested by a command
  • Profile: A complete mission cycle (descent → depth control → ascent → data transmission)
  • TOF (Time-of-Flight): Non-contact distance measurement technology using light pulses
  • Homing: Process of establishing the motor's zero reference position
  • PID Control: Proportional-Integral-Derivative controller for precise depth maintenance
  • ESP-NOW: Low-latency peer-to-peer WiFi communication protocol by Espressif

Documentation Version: 11.2.0 Last Updated: May 2026

Recent changes:

  • PID output normalized to u ∈ [0, 1] (fraction of syringe travel). Default gains Kp = 0.17, Kd = 0.13, expressed per metre of depth error so they stay valid if MOTOR_MAX_STEPS changes.
  • Motor geometry: home = piston fully inserted (empty syringes, floats); full extension = piston extracted (full syringes, sinks). The mapping uToMotorPos() in include/config.h encapsulates MOTOR_INVERT_LOGICAL so motion code never hard-codes signs.
  • TOF safety range widened to [40, 85] mm to give 10 mm of margin above the homing threshold without risking the mechanical end stop.
  • balance now refuses to start without a prior homing (was forcing pos = 0 as a fallback, mechanically risky).
  • New SURFACE_TARGET_OFFSET_M constant and SURFACE_OFFSET <m> command (number 18) for tuning the surface idle position at runtime. Team Contact: PoliTOcean @ Politecnico di Torino Maintainers: Colabella Davide, Benevenga Filippo

About

Firmware for the float project

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

PoliTOcean Float 2025 - Technical Documentation

CI

Version: 11.2.0 Team: PoliTOcean @ Politecnico di Torino
Maintainers: Colabella Davide, Benevenga Filippo
Competition: MATE ROV 2025/26


TABLE OF CONTENTS


PROJECT OVERVIEW

Introduction and Requirements

By MATE 2026 requirements documentation (Task 4.1 - MATE Floats Under the Ice), the FLOAT must complete operational vertical profiling missions under simulated ice conditions.

Pre-Deployment Requirements:

  • The FLOAT must communicate with the Mission Station (CS) prior to descending, transmitting a defined data packet containing:
    • Company number (provided by MATE)
    • Time data (UTC/local/float time)
    • Pressure data (pa or kpa) and/or depth data (m or cm)
    • Additional data as required
  • Example packet: EX01 1:51:42 UTC 9.8 kpa 1.00 meters

Vertical Profile Requirements:

The FLOAT must complete two vertical profiles using a buoyancy engine (fluid displacement system, not thrusters). Each profile consists of:

  1. Descent Phase: Descend from surface to 2.5 meters depth (± 33 cm)
  2. Deep Hold: Maintain depth at 2.5 meters for 30 seconds (bottom of float as reference)
  3. Ascent Phase: Rise to 40 cm depth (± 33 cm) without breaking surface or contacting ice
  4. Shallow Hold: Maintain depth at 40 cm for 30 seconds (top of float as reference)

Data Collection & Transmission:

  • Collect depth/pressure measurements during both profiles and transmit judge packets every 5 seconds (minimum 20 data packets)
  • Store data in ESP32 internal flash as a LittleFS CSV containing: company number, profile id, time, pressure, judge/reference depth, phase, and raw sensor depth
  • After recovery, transmit all collected data wirelessly to the Mission Station
  • Data packets must show 7 sequential measurements (spanning 30 seconds at 5-second intervals: 0, 5, 10, 15, 20, 25, 30) confirming proper depth maintenance at both 2.5m and 0.4m

Post-Mission Requirements:

  • Upon surface recovery, autonomously transmit all profile data to the CS
  • CS GUI plots depth over time using received data (minimum 20 data packets required)
  • Graph must display time (X-axis) vs depth (Y-axis) for both completed profiles

Current firmware storage note: the active implementation uses the internal flash CSV log (FLASH_LOG_PATH) as the primary mission data source. EEPROM compact records remain only as an internal legacy buffer. The legacy serial command name is still CLEAR_SD, but it now resets the flash CSV log and the legacy EEPROM buffer.

Auto Mode (AM):

An autonomous operating mode that triggers profile execution in case of connection loss with the CS, ensuring mission completion if communication is temporarily unavailable. AM will autonomously commit up to two profiles when connection is lost, preventing incomplete missions due to transient WiFi failures.

Penalties:

  • Breaking surface or contacting ice sheet during profile: -5 points per profile
  • FLOAT must remain submerged between 40 cm and 2.5 m throughout the ascent/descent phases

System Behavior

The general idea is that the FLOAT provides some micro-services that the CS can activate by sending commands to it. Every command can be requested at any moment, with the only limit that a command can be accepted by the FLOAT only when the previous one has been completed (more info on command cycle later).

The FLOAT has two main logical states: the command execution one, and the idle one in which it waits for the new command. In idle state, the FLOAT can have buffered flash data from the last completed profile that can be sent to the CS.

Syringe / Motor Convention

The FLOAT changes its buoyancy by pulling and pushing water through a pair of syringes driven by a stepper motor through a lead screw. The mechanical convention is:

  • Home (motor_pos = 0): piston fully inserted, syringes empty of water → the FLOAT floats. At this position the TOF reads ≈ TOF_HOMING_THRESHOLD (75 mm) because the piston is far from the sensor.
  • Full extension (motor_pos = uToMotorPos(1.0f)): piston extracted, syringes full of water → the FLOAT sinks. TOF reads ≈ TOF_SAFE_RANGE_MIN_MM (40 mm).
  • PID logical convention: u ∈ [0, 1] with u = 0 → float (empty) and u = 1 → sink (full). The helper uToMotorPos(u) in include/config.h maps u to the actual motor target while respecting MOTOR_INVERT_LOGICAL, so callers never hard-code signs.

TOF safety limits used during motion:

ConstantDefaultMeaning
TOF_HOMING_THRESHOLD75 mmPhase 2 of homing stops when the TOF reads above this
TOF_HOMING_APPROACH_MM50 mmPhase 1 of homing stops when the TOF reads below this
TOF_SAFE_RANGE_MIN_MM40 mmLower bound: syringe fully extended (mechanical limit)
TOF_SAFE_RANGE_MAX_MM85 mmUpper bound: 10 mm above the homing threshold; any higher and the piston would risk hitting the back mechanical stop

Surface Target Offset

When the FLOAT is "floating", we usually want its top a few centimetres below the water surface — not exactly at the waterline — so that the float remains visible without being completely above water. This is controlled by SURFACE_TARGET_OFFSET_M (default 0.10 m: top of the float 10 cm below the surface).

Two ways to change it:

  • At compile time: edit SURFACE_TARGET_OFFSET_M in include/config.h.
  • At runtime: send command SURFACE_OFFSET <m> via the CS, or SURFACE_OFFSET <m> over USB serial on ESPA. The change persists until the next reboot.

The offset is geometry-agnostic: FLOAT_TOP_TO_SENSOR_M (geometric distance between the top of the float and the barometer) and SURFACE_TARGET_OFFSET_M (operational target) are kept as separate constants in include/config.h.


HARDWARE CONFIGURATION

Hardware Used

HardwareRoleLinkKey parameters / notes
ESP32 Dev Module x2ESPA float controller and ESPB communication bridgeEspressif ESP32Arduino framework, ESP-NOW link, USB serial bridge on ESPB
DRV8825 stepper driverStepper motor driver for syringe motionPololu DRV8825 carrierSTEP/DIR control, active-low enable, SLEEP and RESET held HIGH during operation
Stepper motor with planetary gearboxSyringe actuator motorStepperOnline 17HS15-1684S-PG27NEMA 17, 200 steps/rev, 1.8 deg/step, configured gear ratio 26.85124:1, microstep setting 1
Lead screw / threaded rodConverts motor rotation to linear travelSIENOC 500 mm trapezoidal lead screwPitch 2.0 mm, 4 starts, lead 8.0 mm/rev, configured travel 35 mm
VL53L7CX Time-of-Flight sensorNon-contact homing distance sensor (multi-zone)ST VL53L7CXI2C 0x29, LPn (XSHUT) GPIO16, GPIO1 GPIO15, 4×4 zone mode with central-zone mask 0x0660, 6 mm raw offset, 75 mm homing threshold
Bar02 pressure sensorPressure/depth measurementBlue Robotics Bar02MS5837_02BA model, I2C 0x76, used for depth and pressure
INA219 battery monitorBattery bus-voltage monitorAdafruit INA219 breakoutI2C 0x40, initialized at 100 kHz, configured with 5 A max and 0.1 ohm shunt

Pin Mapping

ESPA (Float Controller) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Motor Control
DIRGPIO32DRV8825 DirectionStepper direction control
STEPGPIO33DRV8825 StepStep pulse generation
ENGPIO27DRV8825 EnableActive-LOW, disables outputs when HIGH
SLEEPGPIO25DRV8825 SleepActive-LOW, must be HIGH for operation
RSTGPIO26DRV8825 ResetActive-LOW, must be HIGH for operation
TOF Sensor
SDAGPIO21VL53L7CX I2C DataI2C bus (shared with sensors)
SCLGPIO22VL53L7CX I2C ClockI2C bus @ 1MHz
XSHUTGPIO16VL53L7CX LPn (shutdown)Sensor enable / shutdown control
GPIO1GPIO15VL53L7CX InterruptOptional interrupt pin, unused in polling mode
Sensors
SDAGPIO21Bar02, INA219I2C bus (shared)
SCLGPIO22Bar02, INA219I2C bus (shared)
Status LED
LED_RGPIO19Red ChannelPWM control
LED_GGPIO18Green ChannelPWM control
LED_BGPIO5Blue ChannelPWM control

ESPB (Communication Bridge) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Communication
TXGPIO1USB Serial115200 baud
RXGPIO3USB Serial115200 baud
Status LED
Built-in LEDGPIO2Onboard LEDStatus indication

I2C Device Addresses:

DeviceAddressBus Speed
VL53L7CX TOF0x291 MHz
Bar02 Pressure0x76Shared I2C bus
INA219 Battery0x40Initialized at 100 kHz

The firmware initializes the INA219 at 100 kHz, then the VL53L7CX driver raises the shared Wire clock to 1 MHz for TOF ranging.


SYSTEM ARCHITECTURE

Deployment Diagram

FLOAT code has to be deployed on two ESP32, one mounted on the FLOAT board (ESPA) together with the sensors and the power supply, and the other (ESPB) communicating with the Control Station via USB. The two ESP32 communicates via WiFi using ESP-NOW protocol. The software on the two ESP32 is designed to work regardless of the design of the GUI on the CS.

The idea is to bring all the complexity on the ESPA and GUI, leaving no trace of logic on the ESPB.

graph TB
subgraph "Control Station"
GUI[GUI Application]
USB[USB Serial]
end
subgraph "ESPB - Communication Bridge"
ESPB_FW[ESPB Firmware]
ESPB_WiFi[WiFi ESP-NOW]
ESPB_LED[Built-in LED]
end
subgraph "ESPA - Float Controller"
ESPA_FW[ESPA Firmware]
ESPA_WiFi[WiFi ESP-NOW]
subgraph "Motor System"
DRV8825[DRV8825 Driver]
STEPPER[Stepper Motor]
TOF[VL53L7CX TOF Sensor]
end
subgraph "Sensors"
BAR02[Bar02 Pressure]
INA219[INA219 Battery]
end
RGB[RGB LED]
I2C[I2C Bus]
end
GUI -->|Commands| USB
USB <-->|Serial 115200| ESPB_FW
ESPB_FW <-->|ESP-NOW 2.4GHz| ESPB_WiFi
ESPB_WiFi <-.->|WiFi| ESPA_WiFi
ESPA_WiFi <-->|ESP-NOW| ESPA_FW
ESPA_FW -->|Control Signals| DRV8825
DRV8825 -->|STEP/DIR| STEPPER
TOF -->|Distance Data| ESPA_FW
BAR02 -->|I2C| I2C
INA219 -->|I2C| I2C
I2C -->|Sensor Data| ESPA_FW
ESPA_FW -->|Status| RGB
ESPB_FW -->|Status| ESPB_LED
style ESPA_FW fill:#4CAF50
style ESPB_FW fill:#2196F3
style GUI fill:#FF9800
style TOF fill:#9C27B0
Loading

Software Structure

The project follows a modular architecture with separate compilation units:

  • Central Config (include/config.h) - pin mapping, motor constants, PID defaults, mission timing, network parameters
  • Shared Protocol (include/float_common.h) - ESP-NOW packet structs, ACK strings, EEPROM size, shared LED enum
  • Motor Control (lib/motor) - DRV8825/FastAccelStepper setup, position tracking, bounded movement primitives
  • TOF Sensor (lib/tof) - VL53L7CX initialization (4×4 multi-zone), aggregated minimum-distance reading with raw offset compensation
  • Motion Control (lib/motion_control) - TOF homing, safe max-extension move, balance routine, emergency stop handling
  • Communication (lib/comms) - ESP-NOW wireless protocol and ElegantOTA session management
  • Sensors (lib/sensors) - Bar02 pressure/depth and INA219 battery monitoring
  • PID Controller (lib/pid) - depth control algorithm with runtime gain updates
  • Profile Manager (lib/profile) - mission profile execution and flash-backed mission logging
  • Flash Storage (lib/flash_storage) - LittleFS CSV mission log and replay helpers
  • LED Controller (lib/led) - RGB status indication system

ESPA State Machine

The ESPA firmware operates as a state machine coordinating motor control, sensors, and communications:

stateDiagram-v2
[*] --> INIT: Power On
INIT --> HOMING: Sensors OK
INIT --> ERROR: Init Failed
HOMING --> IDLE: Homing Success
HOMING --> ERROR: Homing Failed/Timeout
IDLE --> EXECUTING: Command Received
IDLE --> IDLE: No Command
EXECUTING --> PROFILE: GO Command
EXECUTING --> BALANCE: BALANCE Command
EXECUTING --> SEND_DATA: LISTENING Command
EXECUTING --> CLEAR_DATA: CLEAR_SD Command
EXECUTING --> UPDATE_PID: PARAMS Command
EXECUTING --> UPDATE_PID_EXT: PARAMS_EXT Command
EXECUTING --> TEST_SPEED: TEST_FREQ Command
EXECUTING --> TEST_STEPS: TEST_STEPS Command
EXECUTING --> DEBUG_MODE: DEBUG Command
EXECUTING --> HOMING: HOME_MOTOR Command
EXECUTING --> OTA: TRY_UPLOAD Command
EXECUTING --> SYRINGE_SET: SYRINGE_SET Command
EXECUTING --> PID_HOLD: PID_HOLD Command
EXECUTING --> PID_STEP: PID_STEP Command
EXECUTING --> SET_SURFACE_OFFSET: SURFACE_OFFSET Command
PROFILE --> PID_CONTROL: Descending
PID_CONTROL --> PID_CONTROL: Depth Control Active
PID_CONTROL --> ASCENT: Target Reached/Timeout
ASCENT --> IDLE_W_DATA: At Surface
BALANCE --> IDLE: Balance Complete
SEND_DATA --> IDLE: Data Sent
CLEAR_DATA --> IDLE: Flash Log Cleared
UPDATE_PID --> IDLE: Gains Updated
UPDATE_PID_EXT --> IDLE: Period/alpha Updated
TEST_SPEED --> IDLE: Speed Stored
TEST_STEPS --> IDLE: Test Move Complete
DEBUG_MODE --> IDLE: Debug Toggle Complete
OTA --> IDLE: Upload Complete
SYRINGE_SET --> IDLE: Bench Test Complete
PID_HOLD --> IDLE: Hold Complete / Timeout
PID_STEP --> IDLE: Step Complete / Timeout
SET_SURFACE_OFFSET --> IDLE: Offset Stored
IDLE_W_DATA --> SENDING: LISTENING Command
SENDING --> IDLE: Data Transmitted
ERROR --> [*]: Manual Reset Required
note right of IDLE
RGB: Green Solid
Waiting for command
end note
note right of HOMING
RGB: Purple Blink
TOF-based homing
end note
note right of PID_CONTROL
RGB: Cyan Blink
Active depth control
end note
note right of ERROR
RGB: Red Blink
Fatal error state
end note
Loading

COMMUNICATION PROTOCOL

Command Lifecycle

A command life-cycle does not overlaps/interfere with the previous nor the next one: when the CS sends a command (and it arrives to the FLOAT), a feedback from the FLOAT should inform about the acceptance of the command and if this acknowledgement arrives within a given period (specified later), next command requests will be ignored until end of execution of the current one, signaled by an idle acknowledgement.

If the acknowledgement doesn't arrive within that time span, the command commit can be considered failed: this could happen for WiFi connection failures or FLOAT electronics issues.

When waiting for the command commit acknowledgement, other command requests will be ignored as well.

As already mentioned, after command completion the FLOAT will try to send an idle acknowledgement to signal that it is listening for a new command: together with the idle state, this acknowledgement can also inform about the presence of new flash-backed profile data that has to be sent to the CS. After an idle acknowledgement is received, a new command can be accepted.

To maintain consistency with the status stored on the ESPB, and hence with the GUI visuals, the FLOAT grants to send the acknowledgement signalling a command commit only when the commit can be given for sure. In the same way, if the acknowledgement fails to be sent due to connection issues, the command is not committed.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant WiFi as ESP-NOW
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Fresh State
CS->>ESPB: Send Command (e.g., "GO")
ESPB->>WiFi: Forward Command
WiFi->>ESPA: Deliver Command
ESPA->>ESPA: Validate & Accept
ESPA->>WiFi: ACK (e.g., "GO_RECVD")
WiFi->>ESPB: Deliver ACK
ESPB->>ESPB: Update State (status=2)
ESPB->>CS: Forward ACK
Note over ESPA: Executing Command...
ESPA->>ESPA: Complete Task
ESPA->>WiFi: Completion ACK (e.g., "FLOAT_IDLE")
WiFi->>ESPB: Deliver Completion
ESPB->>ESPB: Update State (status=0)
ESPB->>CS: Forward Completion
Note over CS: Ready for Next Command
Loading

ESPB Bridge Role

The ESPB role is only to make the CS task of continuously checking on the WiFi channel less resource consuming.

In particular, ESPB receives commands from CS via USB only to forward them to the FLOAT via WiFi. At the same time, it can receive feedback and data from the FLOAT. In the latter case the ESPB will forward the packages on the USB channel, while using them to update an internal state accordingly. The only code that should trigger the update of the state stored on ESPB is the firmware on the FLOAT, via the data and the feedback sent to CS (more later). This is because no assumptions have to be done by other software on the commands completion and acceptance.

The ESPB state should mirror the FLOAT state at each moment (more details later) and can be used by the GUI to give visual feedback to the user or to drive its internal logic. It can be requested to the ESPB by the CS at any moment with a specific command. The state info sent to the CS with this command also contains WiFi connection state info and AM activation state info.

In general, the ESPB feedback could be stale when requested (for example because the CS could poll it with a low frequency), so to get fresh, real-time data the CS should listen on the USB channel for the FLOAT packages, after a command request or when waiting for command completion and retrieve the FLOAT current state directly by those packages.

Periodic polling remains a legit choice in case the CS cannot exploit interrupts triggered by Serial connection, but notice that this solution leads to delayed GUI visual feedback with respect to the changes on the FLOAT state.

In some cases, connection losses can undermine consistency between the feedback of the ESPB (either they are polled or real time) and the real current FLOAT state (consistency threats for each FLOAT state later).

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Stale State Polling
CS->>ESPB: Send Command
ESPB->>ESPA: Forward Command
ESPA->>ESPA: Accept & Execute
ESPA->>ESPB: ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward ACK
Note over ESPA: Command Executing...
loop Periodic Polling
CS->>ESPB: Request STATUS
ESPB->>CS: Return Cached State
Note over CS: State may be stale<br/>if ESPA completed recently
end
ESPA->>ESPA: Complete Command
ESPA->>ESPB: Completion ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward Completion
CS->>ESPB: Request STATUS
ESPB->>CS: Return Fresh State
Loading

FLOAT Commands

Table of FLOAT commands with relative effects and acknowledgements:

Cmd stringCmd ESPA numberCmd effectsESPA ack stringESPA ack effects on ESPB state
GO1Performs the two MATE vertical profiles, sends the pre-descent data packet before the first descent, and logs pressure/depth records to flash CSVGO_RECVDstatus to 2 (command execution)
LISTENING2Streams flash CSV records as JSON data packets at 5-second cadence, followed by STOP_DATAAck is data itselfstatus to 2 after first package arrival
BALANCE3Cycles full extension and retraction with holdMs holds until Bar02 pressure rises above the startup baseline by BALANCE_STOP_PRESSURE_DELTA_KPA. Requires the motor to be homed first — otherwise the command fails with Balance: homing requiredCMD3_RECVDstatus to 2
CLEAR_SD4Clears and recreates the flash CSV log, and clears the legacy EEPROM buffer. The command string is kept as CLEAR_SD for compatibilityCMD4_RECVDstatus to 2
SWITCH_AUTO_MODE5Toggles FLOAT Auto ModeSWITCH_AM_RECVDstatus to 2, AM activation state toggled
SEND_PACKAGE6Sends a single live JSON snapshot containing company number, time, pressure, judge/reference depth, phase, and raw sensor depthAck is the package itselfstatus to 2
TRY_UPLOAD7Starts the ElegantOTA access point on ESPA for a 5-minute upload window, then restores ESP-NOWTRY_UPLOAD_RECVDstatus to 2
PARAMS kp ki kd8Updates PID gains at runtimeCHNG_PARMS_RECVDstatus to 2
TEST_FREQ freq9Sets manual test movement speed, clamped to 10-1200 steps/sTEST_FREQ_RECVDstatus to 2
TEST_STEPS n10Moves the motor by n relative steps at the current test speedTEST_STEPS_RECVDstatus to 2
DEBUG11Toggles remote debug forwarding through DebugSerialDEBUG_MODE_RECVDstatus to 2
HOME_MOTOR12Runs TOF-based homing remotelyHOME_RECVDstatus to 2
STOP13Triggers a remote emergency stop, stops the motor, disables outputs, and returns to idleSTOP_RECVDstatus to 2
PARAMS_EXT period alpha14Updates PID tick period (ms) and derivative LPF coefficient alphaD at runtimeCHNG_PID_EXT_RECVDstatus to 2
SYRINGE_SET u dur_s15Bench test: drives the syringe to normalized position u ∈ [0,1] for dur_s seconds, logging depth — bypasses the PID (DC gain / time-constant characterization)SYRINGE_SET_RECVDstatus to 2
PID_HOLD depth dur_s16Bench test: holds depth at depth_m for dur_s seconds with the PID active, logging at 5 HzPID_HOLD_RECVDstatus to 2
PID_STEP depth17Bench test: step response — drives the PID to depth_m for up to 60 s, logging at 10 HzPID_STEP_RECVDstatus to 2
SURFACE_OFFSET m18Sets the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime: the FLOAT will hold its top m metres below the waterline when "floating" (default 0.10)SURFACE_OFF_RECVDstatus to 2
STATUS-Requests stale ESPB status plus AM state, WiFi connection state, battery millivolts, and last RSSI--

Once a command is completed, ESPA acknowledgement can be:

ESPA ack stringESPA ack effects on ESPB stateESPA state
FLOAT_IDLEstatus to 0 (idle)Idle with no data to be sent
FLOAT_IDLE_W_DATAstatus to 1 (idle with data to be sent)Idle with data from last profile to be sent

STATUS COMMAND: ESPB RESPONSE

ESPB response to STATUS command is composed by five parts of information: ESPA state (stale), activation of the AM on the FLOAT, WiFi connection state, last received battery millivolts, and last received RSSI. The WiFi connection state is detected by sending a dummy command code 0, while the other states are kept consistent with the ones on the FLOAT by updating them after acknowledgements reception.

ESPA state:

ESPB state stringESPB state numberState description
UNKNOWN-1ESPB has not received any state message from ESPA since boot
CONNECTED0The FLOAT is listening for new command. Previous command succeeded
CONNECTED_W_DATA1The FLOAT is listening for new command and has some new data from last profile to be sent. Previous command succeeded
EXECUTING_CMD2FLOAT is executing a command
STATUS_ERROR-Internal error in reading the state number

WARNING:
If committing a profile automatically, the relative acknowledgement will likely fail due to connection loss. The profile is committed anyway as it is generated from connection loss in the first place, but the GUI may not have mean to detect it. So it will likely read an inconsistent idle status (CONNECTED or CONNECTED_W_DATA) until FLOAT is at water level with a stable WiFi connection. In the meantime the command commits will fail, for connection loss or because the FLOAT is underwater. Anyway WiFi connection state can be detected by the STATUS command, hence giving feedback on status consistency.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over ESPA: Auto Mode Active
Note over ESPA,ESPB: Connection Lost!
ESPA->>ESPA: Detect Connection Loss
ESPA->>ESPA: Auto-commit Profile
Note over ESPA: Descending...<br/>WiFi Unavailable
ESPA-xESPB: ACK Fails (No Connection)
Note over ESPB: State Becomes Inconsistent<br/>Still shows "IDLE"
CS->>ESPB: Request STATUS
ESPB->>CS: CONNECTED | CONN_LOST
Note over CS: GUI shows inconsistent state<br/>but WiFi loss detected
Note over ESPA: At Surface...<br/>WiFi Restored
ESPA->>ESPB: FLOAT_IDLE_W_DATA
ESPB->>ESPB: Update to Consistent State
ESPB->>CS: Forward State
Note over CS,ESPA: Consistency Restored
Loading

AM state:

ESPB state stringState description
AUTO_MODE_YESAM on FLOAT is activated
AUTO_MODE_NOAM on FLOAT is not activated: connection losses will not trigger profiles

WiFi connection state:

ESPB state stringState description
CONN_OKWiFi connection is ok
CONN_LOSTWiFi connection is currently down. ESPB state could be wrong

Battery and RSSI fields:

ESPB fieldState description
BATTERY: <mV>Last battery voltage received from ESPA acknowledgements
RSSI: <dBm>Last ESP-NOW packet RSSI captured by ESPB promiscuous callback

Example of ESPB state response:CONNECTED_W_DATA | AUTO_MODE_NO | CONN_OK | BATTERY: 12450 | RSSI: -63

At ESPB boot, before any ESPA packet is received, a status request may return: UNKNOWN | AUTO_MODE_NO | CONN_LOST | BATTERY: 0 | RSSI: 0.

GUI / ESPB / ESPA Protocol Contract

The GUI sends command strings to ESPB over USB serial. ESPB parses the string, sends the command number to ESPA over ESP-NOW, and forwards ESPA acknowledgements/data back to the GUI.

GUI commandESPA command numberESPA acknowledgement / response
GO1GO_RECVD
LISTENING2Stored data packets, then STOP_DATA
BALANCE3CMD3_RECVD
CLEAR_SD4CMD4_RECVD
SWITCH_AUTO_MODE5SWITCH_AM_RECVD
SEND_PACKAGE6Live JSON packet
TRY_UPLOAD7TRY_UPLOAD_RECVD
PARAMS kp ki kd8CHNG_PARMS_RECVD
TEST_FREQ freq9TEST_FREQ_RECVD
TEST_STEPS n10TEST_STEPS_RECVD
DEBUG11DEBUG_MODE_RECVD
HOME_MOTOR12HOME_RECVD
STOP13STOP_RECVD
PARAMS_EXT period_ms alpha_d14CHNG_PID_EXT_RECVD
SYRINGE_SET u dur_s15SYRINGE_SET_RECVD
PID_HOLD depth_m dur_s16PID_HOLD_RECVD
PID_STEP depth_m17PID_STEP_RECVD
SURFACE_OFFSET m18SURFACE_OFF_RECVD
STATUS-ESPB local status line with five `

The peer MAC addresses are configured centrally in include/config.h: MAC_ESPA is used by ESPB, and MAC_ESPB is used by ESPA.


LED STATUS INDICATORS

The FLOAT is equipped with RGB LEDs on both ESP32 boards that provide visual feedback about the system status:

ESPA (Float Board) LED States:

Driven by LEDState (scoped enum in lib/led/include/led.h):

LED Color/PatternStateDescription
Green Solid / Boot BlinksLEDState::INITSystem initializing
Green SolidLEDState::IDLEReady and idle, waiting for commands
Green BlinkLEDState::IDLE_WITH_DATAIdle with data ready to send
Red SolidLEDState::LOW_BATTERYBattery voltage below BATT_THRESH (12.0 V)
Red BlinkLEDState::ERRORError state or motor emergency stop
Blue SolidLEDState::PROFILERunning non-PID profile phase
Yellow BlinkLEDState::AUTO_MODEAuto mode active
Purple BlinkLEDState::HOMINGMotor homing in progress
Purple SolidLEDState::MOTOR_MOVINGMotor moving
Cyan BlinkLEDState::PID_CONTROLPID depth control active
White SolidLEDState::COMMUNICATIONCommand received / communicating with ESPB
Orange BlinkLEDState::OTA_MODEOTA update mode active
OffLEDState::OFFSystem off or disabled

ESPB uses a separate FloatLEDState enum (LED_* prefix) defined in include/float_common.h; the two enums are deliberately independent because the two boards have different LED states to signal.

ESPB (Communication Bridge) LED States:

LED PatternStateDescription
Solid OnLED_IDLEConnected and ready
Very Fast BlinkLED_ERRORCommunication error
OffLED_OFFSystem off or disabled

Note: ESPB uses the built-in LED (pin 2) with different blink patterns to indicate status, as it does not have external RGB connections.


DEVELOPMENT AND TESTING

PlatformIO Environments

EnvironmentPurposeMain Source
espAFloat controller firmware with sensors, TOF homing, motion control, PID, ESP-NOW, and OTAsrc/espA/main.cpp
espA_poolESPA firmware compiled with conservative 70 cm pool-test targets (POOL_TEST_PROFILE)src/espA/main.cpp
espBUSB-to-ESP-NOW bridge for the Control Stationsrc/espB/main.cpp
espA_manual_keyboardBench firmware for serial keyboard continuous motor movement without homingsrc/espA_manual_keyboard/main.cpp

Common commands:

pio run -e espA
pio run -e espA_pool
pio run -e espB
pio run -e espA_manual_keyboard
pio test -e espA

CLI Startup

Run all commands from the project root:

cd Float_2025

To build and upload the main firmware targets:

pio run -e espA -t upload
pio run -e espB -t upload

For a conservative shallow-pool test at about 70 cm, upload ESPA with:

pio run -e espA_pool -t upload

To open the serial monitor at 115200 baud:

pio device monitor -e espA
pio device monitor -e espB

Direct USB Tuning Commands (ESPA)

All commands in the FLOAT Commands table can be sent over the ESPB USB serial bridge using the same string syntax. The commands below — useful for bench tuning — can also be sent directly over ESPA's USB serial port (e.g. when ESPA is wired to a laptop for tuning runs), bypassing ESPB and ESP-NOW entirely.

CommandEffect
PARAMS <kp> <ki> <kd>Update PID gains at runtime (same effect as command 8)
PARAMS_EXT <period_ms> <alpha_d>Update PID tick period and derivative LPF coefficient (command 14)
SYRINGE_SET <u> <dur_s>Drive the syringe to position u ∈ [0,1] for dur_s seconds and log depth — bypasses the PID, useful for DC-gain and time-constant estimation (command 15)
PID_HOLD <depth_m> <dur_s>Hold PID at depth_m for dur_s seconds, log at 5 Hz (command 16)
PID_STEP <depth_m>Step response: PID at depth_m for up to 60 s, log at 10 Hz (command 17)
SURFACE_OFFSET <m>Set the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime (command 18)

CLI Tests

To run all available tests for the espA environment:

pio test -e espA

To run a single test:

pio test -e espA -f unit_hw/motor/test_max_steps
pio test -e espA -f unit_hw/motor/test_speed
pio test -e espA -f integration/test_screw_lead_20mm
pio test -e espA -f integration/test_homing_only
pio test -e espA -f integration/test_tof_reading
pio test -e espA -f integration/test_homing_move_to_max
pio test -e espA -f integration/test_motor_direction
pio test -e espA -f integration/test_tof_motor_accuracy

To test ESPB without ESPA powered on:

pio test -e espB -f unit_hw/espb_bridge/test_parser
pio test -e espB -f unit_hw/espb_bridge/test_status_format
pio test -e espB -f unit_hw/espb_bridge/test_protocol_contract

To test the real ESPB-to-ESPA bridge, first upload the real espA firmware, wait until ESPA is idle, then run:

pio test -e espB -f integration/test_espnow_bridge

This test only uses the dummy command 0 and SWITCH_AUTO_MODE; it does not start profiles or move the motor.

Available tests:

TestCommandWhat it checks
test_max_stepspio test -e espA -f unit_hw/motor/test_max_stepsMoves only the motor to the safe maximum extension starting from logical position 0
test_speedpio test -e espA -f unit_hw/motor/test_speedMoves only the motor through 6 alternating 40 mm moves, increasing speed and acceleration up to 2300
test_screw_lead_20mmpio test -e espA -f integration/test_screw_lead_20mmRuns TOF homing, moves the motor by 20 mm, and compares the internal TOF delta
test_motor_directionpio test -e espA -f integration/test_motor_directionMoves only the motor forward/backward and verifies the logical direction; by default it does not use TOF
test_tof_readingpio test -e espA -f integration/test_tof_readingInitializes only the TOF sensor and checks valid readings for about 30 s
test_homing_onlypio test -e espA -f integration/test_homing_onlyRuns only TOF-based homing
test_homing_move_to_maxpio test -e espA -f integration/test_homing_move_to_maxRuns TOF homing and then moves to the safe maximum extension
test_tof_motor_accuracypio test -e espA -f integration/test_tof_motor_accuracyCompares TOF distance and motor position after homing
test_parserpio test -e espB -f unit_hw/espb_bridge/test_parserVerifies GUI/Serial command parsing into ESPA packets without ESPA powered on
test_status_formatpio test -e espB -f unit_hw/espb_bridge/test_status_formatVerifies ESPB cached state and the five-field STATUS format
test_protocol_contractpio test -e espB -f unit_hw/espb_bridge/test_protocol_contractLocks the command/ACK consistency contract between GUI, ESPB, and ESPA
test_espnow_bridgepio test -e espB -f integration/test_espnow_bridgeVerifies real ESP-NOW with the real ESPA firmware powered on, without movement

The test_max_steps, test_speed, and test_motor_direction tests are useful when you need to move only the motor without TOF homing. Before running them, make sure the piston is away from the mechanical end stops and can move in both directions.

Manual Motor-Only Control

To upload the bench firmware that lets you move the motor from the serial keyboard:

pio run -e espA_manual_keyboard -t upload
pio device monitor -e espA_manual_keyboard

Commands in the serial monitor:

KeyAction
Up arrow or wHold to move toward home/up
Down arrow or sHold to move toward extension/down
Space or xStop immediately and disable motor outputs
pPrint the current position
tPrint one TOF reading
h or ?Print help

This firmware does not run homing: at startup it assigns a centered logical position and moves while it receives repeated keypresses; when the key is released it stops automatically after a short timeout. During movement it periodically prints motor position and TOF distance. Use it only when the mechanism is in a physically safe position.

Test Layout

Hardware-oriented tests are stored under test/:

  • test/unit_hw/motor/test_max_steps checks safe maximum extension from a known zero
  • test/unit_hw/motor/test_speed checks alternating 40 mm moves while speed and acceleration increase up to 2300
  • test/integration/test_screw_lead_20mm checks the configured screw pitch, starts, and lead with one 20 mm move measured internally by TOF
  • test/integration/test_tof_reading checks that the TOF sensor initializes and returns valid distance samples for about 30 seconds
  • test/integration/test_homing_only checks TOF-based homing
  • test/integration/test_homing_move_to_max checks homing followed by safe full extension
  • test/integration/test_motor_direction checks logical/physical motion direction, optionally using TOF
  • test/integration/test_tof_motor_accuracy checks TOF and motor movement consistency

Continuous Integration

GitHub Actions builds all three PlatformIO environments (espA, espB, espA_pool) on every push to any branch and on every pull request to master. Workflow file: .github/workflows/ci.yml.

CI does not run the unit_hw/ or integration/ PlatformIO tests because they need a real ESP32 with the float wired up. Run those locally on the bench.

Pushing a v* tag triggers .github/workflows/release.yml, which builds all three environments and attaches the resulting firmware.bin / firmware.elf to a GitHub Release auto-named after the tag.

See CONTRIBUTING.md for the full git workflow (trunk-based with PR review on master), commit conventions, and one-time branch protection setup.


UTILITIES AND RESOURCES

Arduino Library Repositories:

Development Tools:

  • PlatformIO IDE: Modern embedded development platform
  • ESP32 Arduino Core: Framework for ESP32 development
  • FastAccelStepper Library: Timer/task-driven stepper motor control
  • VL53L7CX Library: Multi-zone Time-of-Flight sensor driver

GLOSSARY

  • AM (Auto Mode): Autonomous operation mode that triggers profiles on connection loss
  • CS (Control Station): Ground-based computer running the GUI application
  • ESPA: ESP32 mounted on the Float board (primary controller)
  • ESPB: ESP32 communication bridge between Float and CS
  • Flash Profile Log: Current onboard LittleFS CSV storage used before JSON transmission
  • FastAccelStepper: Timer/task-driven stepper library used by MotorController
  • MotionController: Firmware layer that combines motor, TOF, LEDs, debug, timeouts, and emergency stops for safe movement routines
  • Commit a command: To accept a sent command. After commit, command execution and success is ideally granted
  • Complete a command: To execute all the requirements requested by a command
  • Profile: A complete mission cycle (descent → depth control → ascent → data transmission)
  • TOF (Time-of-Flight): Non-contact distance measurement technology using light pulses
  • Homing: Process of establishing the motor's zero reference position
  • PID Control: Proportional-Integral-Derivative controller for precise depth maintenance
  • ESP-NOW: Low-latency peer-to-peer WiFi communication protocol by Espressif

Documentation Version: 11.2.0 Last Updated: May 2026

Recent changes:

  • PID output normalized to u ∈ [0, 1] (fraction of syringe travel). Default gains Kp = 0.17, Kd = 0.13, expressed per metre of depth error so they stay valid if MOTOR_MAX_STEPS changes.
  • Motor geometry: home = piston fully inserted (empty syringes, floats); full extension = piston extracted (full syringes, sinks). The mapping uToMotorPos() in include/config.h encapsulates MOTOR_INVERT_LOGICAL so motion code never hard-codes signs.
  • TOF safety range widened to [40, 85] mm to give 10 mm of margin above the homing threshold without risking the mechanical end stop.
  • balance now refuses to start without a prior homing (was forcing pos = 0 as a fallback, mechanically risky).
  • New SURFACE_TARGET_OFFSET_M constant and SURFACE_OFFSET <m> command (number 18) for tuning the surface idle position at runtime. Team Contact: PoliTOcean @ Politecnico di Torino Maintainers: Colabella Davide, Benevenga Filippo

About

Firmware for the float project

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PoliTOcean Float 2025 - Technical Documentation

CI

Version: 11.2.0 Team: PoliTOcean @ Politecnico di Torino
Maintainers: Colabella Davide, Benevenga Filippo
Competition: MATE ROV 2025/26


TABLE OF CONTENTS


PROJECT OVERVIEW

Introduction and Requirements

By MATE 2026 requirements documentation (Task 4.1 - MATE Floats Under the Ice), the FLOAT must complete operational vertical profiling missions under simulated ice conditions.

Pre-Deployment Requirements:

  • The FLOAT must communicate with the Mission Station (CS) prior to descending, transmitting a defined data packet containing:
    • Company number (provided by MATE)
    • Time data (UTC/local/float time)
    • Pressure data (pa or kpa) and/or depth data (m or cm)
    • Additional data as required
  • Example packet: EX01 1:51:42 UTC 9.8 kpa 1.00 meters

Vertical Profile Requirements:

The FLOAT must complete two vertical profiles using a buoyancy engine (fluid displacement system, not thrusters). Each profile consists of:

  1. Descent Phase: Descend from surface to 2.5 meters depth (± 33 cm)
  2. Deep Hold: Maintain depth at 2.5 meters for 30 seconds (bottom of float as reference)
  3. Ascent Phase: Rise to 40 cm depth (± 33 cm) without breaking surface or contacting ice
  4. Shallow Hold: Maintain depth at 40 cm for 30 seconds (top of float as reference)

Data Collection & Transmission:

  • Collect depth/pressure measurements during both profiles and transmit judge packets every 5 seconds (minimum 20 data packets)
  • Store data in ESP32 internal flash as a LittleFS CSV containing: company number, profile id, time, pressure, judge/reference depth, phase, and raw sensor depth
  • After recovery, transmit all collected data wirelessly to the Mission Station
  • Data packets must show 7 sequential measurements (spanning 30 seconds at 5-second intervals: 0, 5, 10, 15, 20, 25, 30) confirming proper depth maintenance at both 2.5m and 0.4m

Post-Mission Requirements:

  • Upon surface recovery, autonomously transmit all profile data to the CS
  • CS GUI plots depth over time using received data (minimum 20 data packets required)
  • Graph must display time (X-axis) vs depth (Y-axis) for both completed profiles

Current firmware storage note: the active implementation uses the internal flash CSV log (FLASH_LOG_PATH) as the primary mission data source. EEPROM compact records remain only as an internal legacy buffer. The legacy serial command name is still CLEAR_SD, but it now resets the flash CSV log and the legacy EEPROM buffer.

Auto Mode (AM):

An autonomous operating mode that triggers profile execution in case of connection loss with the CS, ensuring mission completion if communication is temporarily unavailable. AM will autonomously commit up to two profiles when connection is lost, preventing incomplete missions due to transient WiFi failures.

Penalties:

  • Breaking surface or contacting ice sheet during profile: -5 points per profile
  • FLOAT must remain submerged between 40 cm and 2.5 m throughout the ascent/descent phases

System Behavior

The general idea is that the FLOAT provides some micro-services that the CS can activate by sending commands to it. Every command can be requested at any moment, with the only limit that a command can be accepted by the FLOAT only when the previous one has been completed (more info on command cycle later).

The FLOAT has two main logical states: the command execution one, and the idle one in which it waits for the new command. In idle state, the FLOAT can have buffered flash data from the last completed profile that can be sent to the CS.

Syringe / Motor Convention

The FLOAT changes its buoyancy by pulling and pushing water through a pair of syringes driven by a stepper motor through a lead screw. The mechanical convention is:

  • Home (motor_pos = 0): piston fully inserted, syringes empty of water → the FLOAT floats. At this position the TOF reads ≈ TOF_HOMING_THRESHOLD (75 mm) because the piston is far from the sensor.
  • Full extension (motor_pos = uToMotorPos(1.0f)): piston extracted, syringes full of water → the FLOAT sinks. TOF reads ≈ TOF_SAFE_RANGE_MIN_MM (40 mm).
  • PID logical convention: u ∈ [0, 1] with u = 0 → float (empty) and u = 1 → sink (full). The helper uToMotorPos(u) in include/config.h maps u to the actual motor target while respecting MOTOR_INVERT_LOGICAL, so callers never hard-code signs.

TOF safety limits used during motion:

ConstantDefaultMeaning
TOF_HOMING_THRESHOLD75 mmPhase 2 of homing stops when the TOF reads above this
TOF_HOMING_APPROACH_MM50 mmPhase 1 of homing stops when the TOF reads below this
TOF_SAFE_RANGE_MIN_MM40 mmLower bound: syringe fully extended (mechanical limit)
TOF_SAFE_RANGE_MAX_MM85 mmUpper bound: 10 mm above the homing threshold; any higher and the piston would risk hitting the back mechanical stop

Surface Target Offset

When the FLOAT is "floating", we usually want its top a few centimetres below the water surface — not exactly at the waterline — so that the float remains visible without being completely above water. This is controlled by SURFACE_TARGET_OFFSET_M (default 0.10 m: top of the float 10 cm below the surface).

Two ways to change it:

  • At compile time: edit SURFACE_TARGET_OFFSET_M in include/config.h.
  • At runtime: send command SURFACE_OFFSET <m> via the CS, or SURFACE_OFFSET <m> over USB serial on ESPA. The change persists until the next reboot.

The offset is geometry-agnostic: FLOAT_TOP_TO_SENSOR_M (geometric distance between the top of the float and the barometer) and SURFACE_TARGET_OFFSET_M (operational target) are kept as separate constants in include/config.h.


HARDWARE CONFIGURATION

Hardware Used

HardwareRoleLinkKey parameters / notes
ESP32 Dev Module x2ESPA float controller and ESPB communication bridgeEspressif ESP32Arduino framework, ESP-NOW link, USB serial bridge on ESPB
DRV8825 stepper driverStepper motor driver for syringe motionPololu DRV8825 carrierSTEP/DIR control, active-low enable, SLEEP and RESET held HIGH during operation
Stepper motor with planetary gearboxSyringe actuator motorStepperOnline 17HS15-1684S-PG27NEMA 17, 200 steps/rev, 1.8 deg/step, configured gear ratio 26.85124:1, microstep setting 1
Lead screw / threaded rodConverts motor rotation to linear travelSIENOC 500 mm trapezoidal lead screwPitch 2.0 mm, 4 starts, lead 8.0 mm/rev, configured travel 35 mm
VL53L7CX Time-of-Flight sensorNon-contact homing distance sensor (multi-zone)ST VL53L7CXI2C 0x29, LPn (XSHUT) GPIO16, GPIO1 GPIO15, 4×4 zone mode with central-zone mask 0x0660, 6 mm raw offset, 75 mm homing threshold
Bar02 pressure sensorPressure/depth measurementBlue Robotics Bar02MS5837_02BA model, I2C 0x76, used for depth and pressure
INA219 battery monitorBattery bus-voltage monitorAdafruit INA219 breakoutI2C 0x40, initialized at 100 kHz, configured with 5 A max and 0.1 ohm shunt

Pin Mapping

ESPA (Float Controller) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Motor Control
DIRGPIO32DRV8825 DirectionStepper direction control
STEPGPIO33DRV8825 StepStep pulse generation
ENGPIO27DRV8825 EnableActive-LOW, disables outputs when HIGH
SLEEPGPIO25DRV8825 SleepActive-LOW, must be HIGH for operation
RSTGPIO26DRV8825 ResetActive-LOW, must be HIGH for operation
TOF Sensor
SDAGPIO21VL53L7CX I2C DataI2C bus (shared with sensors)
SCLGPIO22VL53L7CX I2C ClockI2C bus @ 1MHz
XSHUTGPIO16VL53L7CX LPn (shutdown)Sensor enable / shutdown control
GPIO1GPIO15VL53L7CX InterruptOptional interrupt pin, unused in polling mode
Sensors
SDAGPIO21Bar02, INA219I2C bus (shared)
SCLGPIO22Bar02, INA219I2C bus (shared)
Status LED
LED_RGPIO19Red ChannelPWM control
LED_GGPIO18Green ChannelPWM control
LED_BGPIO5Blue ChannelPWM control

ESPB (Communication Bridge) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Communication
TXGPIO1USB Serial115200 baud
RXGPIO3USB Serial115200 baud
Status LED
Built-in LEDGPIO2Onboard LEDStatus indication

I2C Device Addresses:

DeviceAddressBus Speed
VL53L7CX TOF0x291 MHz
Bar02 Pressure0x76Shared I2C bus
INA219 Battery0x40Initialized at 100 kHz

The firmware initializes the INA219 at 100 kHz, then the VL53L7CX driver raises the shared Wire clock to 1 MHz for TOF ranging.


SYSTEM ARCHITECTURE

Deployment Diagram

FLOAT code has to be deployed on two ESP32, one mounted on the FLOAT board (ESPA) together with the sensors and the power supply, and the other (ESPB) communicating with the Control Station via USB. The two ESP32 communicates via WiFi using ESP-NOW protocol. The software on the two ESP32 is designed to work regardless of the design of the GUI on the CS.

The idea is to bring all the complexity on the ESPA and GUI, leaving no trace of logic on the ESPB.

graph TB
subgraph "Control Station"
GUI[GUI Application]
USB[USB Serial]
end
subgraph "ESPB - Communication Bridge"
ESPB_FW[ESPB Firmware]
ESPB_WiFi[WiFi ESP-NOW]
ESPB_LED[Built-in LED]
end
subgraph "ESPA - Float Controller"
ESPA_FW[ESPA Firmware]
ESPA_WiFi[WiFi ESP-NOW]
subgraph "Motor System"
DRV8825[DRV8825 Driver]
STEPPER[Stepper Motor]
TOF[VL53L7CX TOF Sensor]
end
subgraph "Sensors"
BAR02[Bar02 Pressure]
INA219[INA219 Battery]
end
RGB[RGB LED]
I2C[I2C Bus]
end
GUI -->|Commands| USB
USB <-->|Serial 115200| ESPB_FW
ESPB_FW <-->|ESP-NOW 2.4GHz| ESPB_WiFi
ESPB_WiFi <-.->|WiFi| ESPA_WiFi
ESPA_WiFi <-->|ESP-NOW| ESPA_FW
ESPA_FW -->|Control Signals| DRV8825
DRV8825 -->|STEP/DIR| STEPPER
TOF -->|Distance Data| ESPA_FW
BAR02 -->|I2C| I2C
INA219 -->|I2C| I2C
I2C -->|Sensor Data| ESPA_FW
ESPA_FW -->|Status| RGB
ESPB_FW -->|Status| ESPB_LED
style ESPA_FW fill:#4CAF50
style ESPB_FW fill:#2196F3
style GUI fill:#FF9800
style TOF fill:#9C27B0
Loading

Software Structure

The project follows a modular architecture with separate compilation units:

  • Central Config (include/config.h) - pin mapping, motor constants, PID defaults, mission timing, network parameters
  • Shared Protocol (include/float_common.h) - ESP-NOW packet structs, ACK strings, EEPROM size, shared LED enum
  • Motor Control (lib/motor) - DRV8825/FastAccelStepper setup, position tracking, bounded movement primitives
  • TOF Sensor (lib/tof) - VL53L7CX initialization (4×4 multi-zone), aggregated minimum-distance reading with raw offset compensation
  • Motion Control (lib/motion_control) - TOF homing, safe max-extension move, balance routine, emergency stop handling
  • Communication (lib/comms) - ESP-NOW wireless protocol and ElegantOTA session management
  • Sensors (lib/sensors) - Bar02 pressure/depth and INA219 battery monitoring
  • PID Controller (lib/pid) - depth control algorithm with runtime gain updates
  • Profile Manager (lib/profile) - mission profile execution and flash-backed mission logging
  • Flash Storage (lib/flash_storage) - LittleFS CSV mission log and replay helpers
  • LED Controller (lib/led) - RGB status indication system

ESPA State Machine

The ESPA firmware operates as a state machine coordinating motor control, sensors, and communications:

stateDiagram-v2
[*] --> INIT: Power On
INIT --> HOMING: Sensors OK
INIT --> ERROR: Init Failed
HOMING --> IDLE: Homing Success
HOMING --> ERROR: Homing Failed/Timeout
IDLE --> EXECUTING: Command Received
IDLE --> IDLE: No Command
EXECUTING --> PROFILE: GO Command
EXECUTING --> BALANCE: BALANCE Command
EXECUTING --> SEND_DATA: LISTENING Command
EXECUTING --> CLEAR_DATA: CLEAR_SD Command
EXECUTING --> UPDATE_PID: PARAMS Command
EXECUTING --> UPDATE_PID_EXT: PARAMS_EXT Command
EXECUTING --> TEST_SPEED: TEST_FREQ Command
EXECUTING --> TEST_STEPS: TEST_STEPS Command
EXECUTING --> DEBUG_MODE: DEBUG Command
EXECUTING --> HOMING: HOME_MOTOR Command
EXECUTING --> OTA: TRY_UPLOAD Command
EXECUTING --> SYRINGE_SET: SYRINGE_SET Command
EXECUTING --> PID_HOLD: PID_HOLD Command
EXECUTING --> PID_STEP: PID_STEP Command
EXECUTING --> SET_SURFACE_OFFSET: SURFACE_OFFSET Command
PROFILE --> PID_CONTROL: Descending
PID_CONTROL --> PID_CONTROL: Depth Control Active
PID_CONTROL --> ASCENT: Target Reached/Timeout
ASCENT --> IDLE_W_DATA: At Surface
BALANCE --> IDLE: Balance Complete
SEND_DATA --> IDLE: Data Sent
CLEAR_DATA --> IDLE: Flash Log Cleared
UPDATE_PID --> IDLE: Gains Updated
UPDATE_PID_EXT --> IDLE: Period/alpha Updated
TEST_SPEED --> IDLE: Speed Stored
TEST_STEPS --> IDLE: Test Move Complete
DEBUG_MODE --> IDLE: Debug Toggle Complete
OTA --> IDLE: Upload Complete
SYRINGE_SET --> IDLE: Bench Test Complete
PID_HOLD --> IDLE: Hold Complete / Timeout
PID_STEP --> IDLE: Step Complete / Timeout
SET_SURFACE_OFFSET --> IDLE: Offset Stored
IDLE_W_DATA --> SENDING: LISTENING Command
SENDING --> IDLE: Data Transmitted
ERROR --> [*]: Manual Reset Required
note right of IDLE
RGB: Green Solid
Waiting for command
end note
note right of HOMING
RGB: Purple Blink
TOF-based homing
end note
note right of PID_CONTROL
RGB: Cyan Blink
Active depth control
end note
note right of ERROR
RGB: Red Blink
Fatal error state
end note
Loading

COMMUNICATION PROTOCOL

Command Lifecycle

A command life-cycle does not overlaps/interfere with the previous nor the next one: when the CS sends a command (and it arrives to the FLOAT), a feedback from the FLOAT should inform about the acceptance of the command and if this acknowledgement arrives within a given period (specified later), next command requests will be ignored until end of execution of the current one, signaled by an idle acknowledgement.

If the acknowledgement doesn't arrive within that time span, the command commit can be considered failed: this could happen for WiFi connection failures or FLOAT electronics issues.

When waiting for the command commit acknowledgement, other command requests will be ignored as well.

As already mentioned, after command completion the FLOAT will try to send an idle acknowledgement to signal that it is listening for a new command: together with the idle state, this acknowledgement can also inform about the presence of new flash-backed profile data that has to be sent to the CS. After an idle acknowledgement is received, a new command can be accepted.

To maintain consistency with the status stored on the ESPB, and hence with the GUI visuals, the FLOAT grants to send the acknowledgement signalling a command commit only when the commit can be given for sure. In the same way, if the acknowledgement fails to be sent due to connection issues, the command is not committed.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant WiFi as ESP-NOW
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Fresh State
CS->>ESPB: Send Command (e.g., "GO")
ESPB->>WiFi: Forward Command
WiFi->>ESPA: Deliver Command
ESPA->>ESPA: Validate & Accept
ESPA->>WiFi: ACK (e.g., "GO_RECVD")
WiFi->>ESPB: Deliver ACK
ESPB->>ESPB: Update State (status=2)
ESPB->>CS: Forward ACK
Note over ESPA: Executing Command...
ESPA->>ESPA: Complete Task
ESPA->>WiFi: Completion ACK (e.g., "FLOAT_IDLE")
WiFi->>ESPB: Deliver Completion
ESPB->>ESPB: Update State (status=0)
ESPB->>CS: Forward Completion
Note over CS: Ready for Next Command
Loading

ESPB Bridge Role

The ESPB role is only to make the CS task of continuously checking on the WiFi channel less resource consuming.

In particular, ESPB receives commands from CS via USB only to forward them to the FLOAT via WiFi. At the same time, it can receive feedback and data from the FLOAT. In the latter case the ESPB will forward the packages on the USB channel, while using them to update an internal state accordingly. The only code that should trigger the update of the state stored on ESPB is the firmware on the FLOAT, via the data and the feedback sent to CS (more later). This is because no assumptions have to be done by other software on the commands completion and acceptance.

The ESPB state should mirror the FLOAT state at each moment (more details later) and can be used by the GUI to give visual feedback to the user or to drive its internal logic. It can be requested to the ESPB by the CS at any moment with a specific command. The state info sent to the CS with this command also contains WiFi connection state info and AM activation state info.

In general, the ESPB feedback could be stale when requested (for example because the CS could poll it with a low frequency), so to get fresh, real-time data the CS should listen on the USB channel for the FLOAT packages, after a command request or when waiting for command completion and retrieve the FLOAT current state directly by those packages.

Periodic polling remains a legit choice in case the CS cannot exploit interrupts triggered by Serial connection, but notice that this solution leads to delayed GUI visual feedback with respect to the changes on the FLOAT state.

In some cases, connection losses can undermine consistency between the feedback of the ESPB (either they are polled or real time) and the real current FLOAT state (consistency threats for each FLOAT state later).

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Stale State Polling
CS->>ESPB: Send Command
ESPB->>ESPA: Forward Command
ESPA->>ESPA: Accept & Execute
ESPA->>ESPB: ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward ACK
Note over ESPA: Command Executing...
loop Periodic Polling
CS->>ESPB: Request STATUS
ESPB->>CS: Return Cached State
Note over CS: State may be stale<br/>if ESPA completed recently
end
ESPA->>ESPA: Complete Command
ESPA->>ESPB: Completion ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward Completion
CS->>ESPB: Request STATUS
ESPB->>CS: Return Fresh State
Loading

FLOAT Commands

Table of FLOAT commands with relative effects and acknowledgements:

Cmd stringCmd ESPA numberCmd effectsESPA ack stringESPA ack effects on ESPB state
GO1Performs the two MATE vertical profiles, sends the pre-descent data packet before the first descent, and logs pressure/depth records to flash CSVGO_RECVDstatus to 2 (command execution)
LISTENING2Streams flash CSV records as JSON data packets at 5-second cadence, followed by STOP_DATAAck is data itselfstatus to 2 after first package arrival
BALANCE3Cycles full extension and retraction with holdMs holds until Bar02 pressure rises above the startup baseline by BALANCE_STOP_PRESSURE_DELTA_KPA. Requires the motor to be homed first — otherwise the command fails with Balance: homing requiredCMD3_RECVDstatus to 2
CLEAR_SD4Clears and recreates the flash CSV log, and clears the legacy EEPROM buffer. The command string is kept as CLEAR_SD for compatibilityCMD4_RECVDstatus to 2
SWITCH_AUTO_MODE5Toggles FLOAT Auto ModeSWITCH_AM_RECVDstatus to 2, AM activation state toggled
SEND_PACKAGE6Sends a single live JSON snapshot containing company number, time, pressure, judge/reference depth, phase, and raw sensor depthAck is the package itselfstatus to 2
TRY_UPLOAD7Starts the ElegantOTA access point on ESPA for a 5-minute upload window, then restores ESP-NOWTRY_UPLOAD_RECVDstatus to 2
PARAMS kp ki kd8Updates PID gains at runtimeCHNG_PARMS_RECVDstatus to 2
TEST_FREQ freq9Sets manual test movement speed, clamped to 10-1200 steps/sTEST_FREQ_RECVDstatus to 2
TEST_STEPS n10Moves the motor by n relative steps at the current test speedTEST_STEPS_RECVDstatus to 2
DEBUG11Toggles remote debug forwarding through DebugSerialDEBUG_MODE_RECVDstatus to 2
HOME_MOTOR12Runs TOF-based homing remotelyHOME_RECVDstatus to 2
STOP13Triggers a remote emergency stop, stops the motor, disables outputs, and returns to idleSTOP_RECVDstatus to 2
PARAMS_EXT period alpha14Updates PID tick period (ms) and derivative LPF coefficient alphaD at runtimeCHNG_PID_EXT_RECVDstatus to 2
SYRINGE_SET u dur_s15Bench test: drives the syringe to normalized position u ∈ [0,1] for dur_s seconds, logging depth — bypasses the PID (DC gain / time-constant characterization)SYRINGE_SET_RECVDstatus to 2
PID_HOLD depth dur_s16Bench test: holds depth at depth_m for dur_s seconds with the PID active, logging at 5 HzPID_HOLD_RECVDstatus to 2
PID_STEP depth17Bench test: step response — drives the PID to depth_m for up to 60 s, logging at 10 HzPID_STEP_RECVDstatus to 2
SURFACE_OFFSET m18Sets the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime: the FLOAT will hold its top m metres below the waterline when "floating" (default 0.10)SURFACE_OFF_RECVDstatus to 2
STATUS-Requests stale ESPB status plus AM state, WiFi connection state, battery millivolts, and last RSSI--

Once a command is completed, ESPA acknowledgement can be:

ESPA ack stringESPA ack effects on ESPB stateESPA state
FLOAT_IDLEstatus to 0 (idle)Idle with no data to be sent
FLOAT_IDLE_W_DATAstatus to 1 (idle with data to be sent)Idle with data from last profile to be sent

STATUS COMMAND: ESPB RESPONSE

ESPB response to STATUS command is composed by five parts of information: ESPA state (stale), activation of the AM on the FLOAT, WiFi connection state, last received battery millivolts, and last received RSSI. The WiFi connection state is detected by sending a dummy command code 0, while the other states are kept consistent with the ones on the FLOAT by updating them after acknowledgements reception.

ESPA state:

ESPB state stringESPB state numberState description
UNKNOWN-1ESPB has not received any state message from ESPA since boot
CONNECTED0The FLOAT is listening for new command. Previous command succeeded
CONNECTED_W_DATA1The FLOAT is listening for new command and has some new data from last profile to be sent. Previous command succeeded
EXECUTING_CMD2FLOAT is executing a command
STATUS_ERROR-Internal error in reading the state number

WARNING:
If committing a profile automatically, the relative acknowledgement will likely fail due to connection loss. The profile is committed anyway as it is generated from connection loss in the first place, but the GUI may not have mean to detect it. So it will likely read an inconsistent idle status (CONNECTED or CONNECTED_W_DATA) until FLOAT is at water level with a stable WiFi connection. In the meantime the command commits will fail, for connection loss or because the FLOAT is underwater. Anyway WiFi connection state can be detected by the STATUS command, hence giving feedback on status consistency.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over ESPA: Auto Mode Active
Note over ESPA,ESPB: Connection Lost!
ESPA->>ESPA: Detect Connection Loss
ESPA->>ESPA: Auto-commit Profile
Note over ESPA: Descending...<br/>WiFi Unavailable
ESPA-xESPB: ACK Fails (No Connection)
Note over ESPB: State Becomes Inconsistent<br/>Still shows "IDLE"
CS->>ESPB: Request STATUS
ESPB->>CS: CONNECTED | CONN_LOST
Note over CS: GUI shows inconsistent state<br/>but WiFi loss detected
Note over ESPA: At Surface...<br/>WiFi Restored
ESPA->>ESPB: FLOAT_IDLE_W_DATA
ESPB->>ESPB: Update to Consistent State
ESPB->>CS: Forward State
Note over CS,ESPA: Consistency Restored
Loading

AM state:

ESPB state stringState description
AUTO_MODE_YESAM on FLOAT is activated
AUTO_MODE_NOAM on FLOAT is not activated: connection losses will not trigger profiles

WiFi connection state:

ESPB state stringState description
CONN_OKWiFi connection is ok
CONN_LOSTWiFi connection is currently down. ESPB state could be wrong

Battery and RSSI fields:

ESPB fieldState description
BATTERY: <mV>Last battery voltage received from ESPA acknowledgements
RSSI: <dBm>Last ESP-NOW packet RSSI captured by ESPB promiscuous callback

Example of ESPB state response:CONNECTED_W_DATA | AUTO_MODE_NO | CONN_OK | BATTERY: 12450 | RSSI: -63

At ESPB boot, before any ESPA packet is received, a status request may return: UNKNOWN | AUTO_MODE_NO | CONN_LOST | BATTERY: 0 | RSSI: 0.

GUI / ESPB / ESPA Protocol Contract

The GUI sends command strings to ESPB over USB serial. ESPB parses the string, sends the command number to ESPA over ESP-NOW, and forwards ESPA acknowledgements/data back to the GUI.

GUI commandESPA command numberESPA acknowledgement / response
GO1GO_RECVD
LISTENING2Stored data packets, then STOP_DATA
BALANCE3CMD3_RECVD
CLEAR_SD4CMD4_RECVD
SWITCH_AUTO_MODE5SWITCH_AM_RECVD
SEND_PACKAGE6Live JSON packet
TRY_UPLOAD7TRY_UPLOAD_RECVD
PARAMS kp ki kd8CHNG_PARMS_RECVD
TEST_FREQ freq9TEST_FREQ_RECVD
TEST_STEPS n10TEST_STEPS_RECVD
DEBUG11DEBUG_MODE_RECVD
HOME_MOTOR12HOME_RECVD
STOP13STOP_RECVD
PARAMS_EXT period_ms alpha_d14CHNG_PID_EXT_RECVD
SYRINGE_SET u dur_s15SYRINGE_SET_RECVD
PID_HOLD depth_m dur_s16PID_HOLD_RECVD
PID_STEP depth_m17PID_STEP_RECVD
SURFACE_OFFSET m18SURFACE_OFF_RECVD
STATUS-ESPB local status line with five `

The peer MAC addresses are configured centrally in include/config.h: MAC_ESPA is used by ESPB, and MAC_ESPB is used by ESPA.


LED STATUS INDICATORS

The FLOAT is equipped with RGB LEDs on both ESP32 boards that provide visual feedback about the system status:

ESPA (Float Board) LED States:

Driven by LEDState (scoped enum in lib/led/include/led.h):

LED Color/PatternStateDescription
Green Solid / Boot BlinksLEDState::INITSystem initializing
Green SolidLEDState::IDLEReady and idle, waiting for commands
Green BlinkLEDState::IDLE_WITH_DATAIdle with data ready to send
Red SolidLEDState::LOW_BATTERYBattery voltage below BATT_THRESH (12.0 V)
Red BlinkLEDState::ERRORError state or motor emergency stop
Blue SolidLEDState::PROFILERunning non-PID profile phase
Yellow BlinkLEDState::AUTO_MODEAuto mode active
Purple BlinkLEDState::HOMINGMotor homing in progress
Purple SolidLEDState::MOTOR_MOVINGMotor moving
Cyan BlinkLEDState::PID_CONTROLPID depth control active
White SolidLEDState::COMMUNICATIONCommand received / communicating with ESPB
Orange BlinkLEDState::OTA_MODEOTA update mode active
OffLEDState::OFFSystem off or disabled

ESPB uses a separate FloatLEDState enum (LED_* prefix) defined in include/float_common.h; the two enums are deliberately independent because the two boards have different LED states to signal.

ESPB (Communication Bridge) LED States:

LED PatternStateDescription
Solid OnLED_IDLEConnected and ready
Very Fast BlinkLED_ERRORCommunication error
OffLED_OFFSystem off or disabled

Note: ESPB uses the built-in LED (pin 2) with different blink patterns to indicate status, as it does not have external RGB connections.


DEVELOPMENT AND TESTING

PlatformIO Environments

EnvironmentPurposeMain Source
espAFloat controller firmware with sensors, TOF homing, motion control, PID, ESP-NOW, and OTAsrc/espA/main.cpp
espA_poolESPA firmware compiled with conservative 70 cm pool-test targets (POOL_TEST_PROFILE)src/espA/main.cpp
espBUSB-to-ESP-NOW bridge for the Control Stationsrc/espB/main.cpp
espA_manual_keyboardBench firmware for serial keyboard continuous motor movement without homingsrc/espA_manual_keyboard/main.cpp

Common commands:

pio run -e espA
pio run -e espA_pool
pio run -e espB
pio run -e espA_manual_keyboard
pio test -e espA

CLI Startup

Run all commands from the project root:

cd Float_2025

To build and upload the main firmware targets:

pio run -e espA -t upload
pio run -e espB -t upload

For a conservative shallow-pool test at about 70 cm, upload ESPA with:

pio run -e espA_pool -t upload

To open the serial monitor at 115200 baud:

pio device monitor -e espA
pio device monitor -e espB

Direct USB Tuning Commands (ESPA)

All commands in the FLOAT Commands table can be sent over the ESPB USB serial bridge using the same string syntax. The commands below — useful for bench tuning — can also be sent directly over ESPA's USB serial port (e.g. when ESPA is wired to a laptop for tuning runs), bypassing ESPB and ESP-NOW entirely.

CommandEffect
PARAMS <kp> <ki> <kd>Update PID gains at runtime (same effect as command 8)
PARAMS_EXT <period_ms> <alpha_d>Update PID tick period and derivative LPF coefficient (command 14)
SYRINGE_SET <u> <dur_s>Drive the syringe to position u ∈ [0,1] for dur_s seconds and log depth — bypasses the PID, useful for DC-gain and time-constant estimation (command 15)
PID_HOLD <depth_m> <dur_s>Hold PID at depth_m for dur_s seconds, log at 5 Hz (command 16)
PID_STEP <depth_m>Step response: PID at depth_m for up to 60 s, log at 10 Hz (command 17)
SURFACE_OFFSET <m>Set the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime (command 18)

CLI Tests

To run all available tests for the espA environment:

pio test -e espA

To run a single test:

pio test -e espA -f unit_hw/motor/test_max_steps
pio test -e espA -f unit_hw/motor/test_speed
pio test -e espA -f integration/test_screw_lead_20mm
pio test -e espA -f integration/test_homing_only
pio test -e espA -f integration/test_tof_reading
pio test -e espA -f integration/test_homing_move_to_max
pio test -e espA -f integration/test_motor_direction
pio test -e espA -f integration/test_tof_motor_accuracy

To test ESPB without ESPA powered on:

pio test -e espB -f unit_hw/espb_bridge/test_parser
pio test -e espB -f unit_hw/espb_bridge/test_status_format
pio test -e espB -f unit_hw/espb_bridge/test_protocol_contract

To test the real ESPB-to-ESPA bridge, first upload the real espA firmware, wait until ESPA is idle, then run:

pio test -e espB -f integration/test_espnow_bridge

This test only uses the dummy command 0 and SWITCH_AUTO_MODE; it does not start profiles or move the motor.

Available tests:

TestCommandWhat it checks
test_max_stepspio test -e espA -f unit_hw/motor/test_max_stepsMoves only the motor to the safe maximum extension starting from logical position 0
test_speedpio test -e espA -f unit_hw/motor/test_speedMoves only the motor through 6 alternating 40 mm moves, increasing speed and acceleration up to 2300
test_screw_lead_20mmpio test -e espA -f integration/test_screw_lead_20mmRuns TOF homing, moves the motor by 20 mm, and compares the internal TOF delta
test_motor_directionpio test -e espA -f integration/test_motor_directionMoves only the motor forward/backward and verifies the logical direction; by default it does not use TOF
test_tof_readingpio test -e espA -f integration/test_tof_readingInitializes only the TOF sensor and checks valid readings for about 30 s
test_homing_onlypio test -e espA -f integration/test_homing_onlyRuns only TOF-based homing
test_homing_move_to_maxpio test -e espA -f integration/test_homing_move_to_maxRuns TOF homing and then moves to the safe maximum extension
test_tof_motor_accuracypio test -e espA -f integration/test_tof_motor_accuracyCompares TOF distance and motor position after homing
test_parserpio test -e espB -f unit_hw/espb_bridge/test_parserVerifies GUI/Serial command parsing into ESPA packets without ESPA powered on
test_status_formatpio test -e espB -f unit_hw/espb_bridge/test_status_formatVerifies ESPB cached state and the five-field STATUS format
test_protocol_contractpio test -e espB -f unit_hw/espb_bridge/test_protocol_contractLocks the command/ACK consistency contract between GUI, ESPB, and ESPA
test_espnow_bridgepio test -e espB -f integration/test_espnow_bridgeVerifies real ESP-NOW with the real ESPA firmware powered on, without movement

The test_max_steps, test_speed, and test_motor_direction tests are useful when you need to move only the motor without TOF homing. Before running them, make sure the piston is away from the mechanical end stops and can move in both directions.

Manual Motor-Only Control

To upload the bench firmware that lets you move the motor from the serial keyboard:

pio run -e espA_manual_keyboard -t upload
pio device monitor -e espA_manual_keyboard

Commands in the serial monitor:

KeyAction
Up arrow or wHold to move toward home/up
Down arrow or sHold to move toward extension/down
Space or xStop immediately and disable motor outputs
pPrint the current position
tPrint one TOF reading
h or ?Print help

This firmware does not run homing: at startup it assigns a centered logical position and moves while it receives repeated keypresses; when the key is released it stops automatically after a short timeout. During movement it periodically prints motor position and TOF distance. Use it only when the mechanism is in a physically safe position.

Test Layout

Hardware-oriented tests are stored under test/:

  • test/unit_hw/motor/test_max_steps checks safe maximum extension from a known zero
  • test/unit_hw/motor/test_speed checks alternating 40 mm moves while speed and acceleration increase up to 2300
  • test/integration/test_screw_lead_20mm checks the configured screw pitch, starts, and lead with one 20 mm move measured internally by TOF
  • test/integration/test_tof_reading checks that the TOF sensor initializes and returns valid distance samples for about 30 seconds
  • test/integration/test_homing_only checks TOF-based homing
  • test/integration/test_homing_move_to_max checks homing followed by safe full extension
  • test/integration/test_motor_direction checks logical/physical motion direction, optionally using TOF
  • test/integration/test_tof_motor_accuracy checks TOF and motor movement consistency

Continuous Integration

GitHub Actions builds all three PlatformIO environments (espA, espB, espA_pool) on every push to any branch and on every pull request to master. Workflow file: .github/workflows/ci.yml.

CI does not run the unit_hw/ or integration/ PlatformIO tests because they need a real ESP32 with the float wired up. Run those locally on the bench.

Pushing a v* tag triggers .github/workflows/release.yml, which builds all three environments and attaches the resulting firmware.bin / firmware.elf to a GitHub Release auto-named after the tag.

See CONTRIBUTING.md for the full git workflow (trunk-based with PR review on master), commit conventions, and one-time branch protection setup.


UTILITIES AND RESOURCES

Arduino Library Repositories:

Development Tools:

  • PlatformIO IDE: Modern embedded development platform
  • ESP32 Arduino Core: Framework for ESP32 development
  • FastAccelStepper Library: Timer/task-driven stepper motor control
  • VL53L7CX Library: Multi-zone Time-of-Flight sensor driver

GLOSSARY

  • AM (Auto Mode): Autonomous operation mode that triggers profiles on connection loss
  • CS (Control Station): Ground-based computer running the GUI application
  • ESPA: ESP32 mounted on the Float board (primary controller)
  • ESPB: ESP32 communication bridge between Float and CS
  • Flash Profile Log: Current onboard LittleFS CSV storage used before JSON transmission
  • FastAccelStepper: Timer/task-driven stepper library used by MotorController
  • MotionController: Firmware layer that combines motor, TOF, LEDs, debug, timeouts, and emergency stops for safe movement routines
  • Commit a command: To accept a sent command. After commit, command execution and success is ideally granted
  • Complete a command: To execute all the requirements requested by a command
  • Profile: A complete mission cycle (descent → depth control → ascent → data transmission)
  • TOF (Time-of-Flight): Non-contact distance measurement technology using light pulses
  • Homing: Process of establishing the motor's zero reference position
  • PID Control: Proportional-Integral-Derivative controller for precise depth maintenance
  • ESP-NOW: Low-latency peer-to-peer WiFi communication protocol by Espressif

Documentation Version: 11.2.0 Last Updated: May 2026

Recent changes:

  • PID output normalized to u ∈ [0, 1] (fraction of syringe travel). Default gains Kp = 0.17, Kd = 0.13, expressed per metre of depth error so they stay valid if MOTOR_MAX_STEPS changes.
  • Motor geometry: home = piston fully inserted (empty syringes, floats); full extension = piston extracted (full syringes, sinks). The mapping uToMotorPos() in include/config.h encapsulates MOTOR_INVERT_LOGICAL so motion code never hard-codes signs.
  • TOF safety range widened to [40, 85] mm to give 10 mm of margin above the homing threshold without risking the mechanical end stop.
  • balance now refuses to start without a prior homing (was forcing pos = 0 as a fallback, mechanically risky).
  • New SURFACE_TARGET_OFFSET_M constant and SURFACE_OFFSET <m> command (number 18) for tuning the surface idle position at runtime. Team Contact: PoliTOcean @ Politecnico di Torino Maintainers: Colabella Davide, Benevenga Filippo

About

Firmware for the float project

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PoliTOcean Float 2025 - Technical Documentation

CI

Version: 11.2.0 Team: PoliTOcean @ Politecnico di Torino
Maintainers: Colabella Davide, Benevenga Filippo
Competition: MATE ROV 2025/26


TABLE OF CONTENTS


PROJECT OVERVIEW

Introduction and Requirements

By MATE 2026 requirements documentation (Task 4.1 - MATE Floats Under the Ice), the FLOAT must complete operational vertical profiling missions under simulated ice conditions.

Pre-Deployment Requirements:

  • The FLOAT must communicate with the Mission Station (CS) prior to descending, transmitting a defined data packet containing:
    • Company number (provided by MATE)
    • Time data (UTC/local/float time)
    • Pressure data (pa or kpa) and/or depth data (m or cm)
    • Additional data as required
  • Example packet: EX01 1:51:42 UTC 9.8 kpa 1.00 meters

Vertical Profile Requirements:

The FLOAT must complete two vertical profiles using a buoyancy engine (fluid displacement system, not thrusters). Each profile consists of:

  1. Descent Phase: Descend from surface to 2.5 meters depth (± 33 cm)
  2. Deep Hold: Maintain depth at 2.5 meters for 30 seconds (bottom of float as reference)
  3. Ascent Phase: Rise to 40 cm depth (± 33 cm) without breaking surface or contacting ice
  4. Shallow Hold: Maintain depth at 40 cm for 30 seconds (top of float as reference)

Data Collection & Transmission:

  • Collect depth/pressure measurements during both profiles and transmit judge packets every 5 seconds (minimum 20 data packets)
  • Store data in ESP32 internal flash as a LittleFS CSV containing: company number, profile id, time, pressure, judge/reference depth, phase, and raw sensor depth
  • After recovery, transmit all collected data wirelessly to the Mission Station
  • Data packets must show 7 sequential measurements (spanning 30 seconds at 5-second intervals: 0, 5, 10, 15, 20, 25, 30) confirming proper depth maintenance at both 2.5m and 0.4m

Post-Mission Requirements:

  • Upon surface recovery, autonomously transmit all profile data to the CS
  • CS GUI plots depth over time using received data (minimum 20 data packets required)
  • Graph must display time (X-axis) vs depth (Y-axis) for both completed profiles

Current firmware storage note: the active implementation uses the internal flash CSV log (FLASH_LOG_PATH) as the primary mission data source. EEPROM compact records remain only as an internal legacy buffer. The legacy serial command name is still CLEAR_SD, but it now resets the flash CSV log and the legacy EEPROM buffer.

Auto Mode (AM):

An autonomous operating mode that triggers profile execution in case of connection loss with the CS, ensuring mission completion if communication is temporarily unavailable. AM will autonomously commit up to two profiles when connection is lost, preventing incomplete missions due to transient WiFi failures.

Penalties:

  • Breaking surface or contacting ice sheet during profile: -5 points per profile
  • FLOAT must remain submerged between 40 cm and 2.5 m throughout the ascent/descent phases

System Behavior

The general idea is that the FLOAT provides some micro-services that the CS can activate by sending commands to it. Every command can be requested at any moment, with the only limit that a command can be accepted by the FLOAT only when the previous one has been completed (more info on command cycle later).

The FLOAT has two main logical states: the command execution one, and the idle one in which it waits for the new command. In idle state, the FLOAT can have buffered flash data from the last completed profile that can be sent to the CS.

Syringe / Motor Convention

The FLOAT changes its buoyancy by pulling and pushing water through a pair of syringes driven by a stepper motor through a lead screw. The mechanical convention is:

  • Home (motor_pos = 0): piston fully inserted, syringes empty of water → the FLOAT floats. At this position the TOF reads ≈ TOF_HOMING_THRESHOLD (75 mm) because the piston is far from the sensor.
  • Full extension (motor_pos = uToMotorPos(1.0f)): piston extracted, syringes full of water → the FLOAT sinks. TOF reads ≈ TOF_SAFE_RANGE_MIN_MM (40 mm).
  • PID logical convention: u ∈ [0, 1] with u = 0 → float (empty) and u = 1 → sink (full). The helper uToMotorPos(u) in include/config.h maps u to the actual motor target while respecting MOTOR_INVERT_LOGICAL, so callers never hard-code signs.

TOF safety limits used during motion:

ConstantDefaultMeaning
TOF_HOMING_THRESHOLD75 mmPhase 2 of homing stops when the TOF reads above this
TOF_HOMING_APPROACH_MM50 mmPhase 1 of homing stops when the TOF reads below this
TOF_SAFE_RANGE_MIN_MM40 mmLower bound: syringe fully extended (mechanical limit)
TOF_SAFE_RANGE_MAX_MM85 mmUpper bound: 10 mm above the homing threshold; any higher and the piston would risk hitting the back mechanical stop

Surface Target Offset

When the FLOAT is "floating", we usually want its top a few centimetres below the water surface — not exactly at the waterline — so that the float remains visible without being completely above water. This is controlled by SURFACE_TARGET_OFFSET_M (default 0.10 m: top of the float 10 cm below the surface).

Two ways to change it:

  • At compile time: edit SURFACE_TARGET_OFFSET_M in include/config.h.
  • At runtime: send command SURFACE_OFFSET <m> via the CS, or SURFACE_OFFSET <m> over USB serial on ESPA. The change persists until the next reboot.

The offset is geometry-agnostic: FLOAT_TOP_TO_SENSOR_M (geometric distance between the top of the float and the barometer) and SURFACE_TARGET_OFFSET_M (operational target) are kept as separate constants in include/config.h.


HARDWARE CONFIGURATION

Hardware Used

HardwareRoleLinkKey parameters / notes
ESP32 Dev Module x2ESPA float controller and ESPB communication bridgeEspressif ESP32Arduino framework, ESP-NOW link, USB serial bridge on ESPB
DRV8825 stepper driverStepper motor driver for syringe motionPololu DRV8825 carrierSTEP/DIR control, active-low enable, SLEEP and RESET held HIGH during operation
Stepper motor with planetary gearboxSyringe actuator motorStepperOnline 17HS15-1684S-PG27NEMA 17, 200 steps/rev, 1.8 deg/step, configured gear ratio 26.85124:1, microstep setting 1
Lead screw / threaded rodConverts motor rotation to linear travelSIENOC 500 mm trapezoidal lead screwPitch 2.0 mm, 4 starts, lead 8.0 mm/rev, configured travel 35 mm
VL53L7CX Time-of-Flight sensorNon-contact homing distance sensor (multi-zone)ST VL53L7CXI2C 0x29, LPn (XSHUT) GPIO16, GPIO1 GPIO15, 4×4 zone mode with central-zone mask 0x0660, 6 mm raw offset, 75 mm homing threshold
Bar02 pressure sensorPressure/depth measurementBlue Robotics Bar02MS5837_02BA model, I2C 0x76, used for depth and pressure
INA219 battery monitorBattery bus-voltage monitorAdafruit INA219 breakoutI2C 0x40, initialized at 100 kHz, configured with 5 A max and 0.1 ohm shunt

Pin Mapping

ESPA (Float Controller) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Motor Control
DIRGPIO32DRV8825 DirectionStepper direction control
STEPGPIO33DRV8825 StepStep pulse generation
ENGPIO27DRV8825 EnableActive-LOW, disables outputs when HIGH
SLEEPGPIO25DRV8825 SleepActive-LOW, must be HIGH for operation
RSTGPIO26DRV8825 ResetActive-LOW, must be HIGH for operation
TOF Sensor
SDAGPIO21VL53L7CX I2C DataI2C bus (shared with sensors)
SCLGPIO22VL53L7CX I2C ClockI2C bus @ 1MHz
XSHUTGPIO16VL53L7CX LPn (shutdown)Sensor enable / shutdown control
GPIO1GPIO15VL53L7CX InterruptOptional interrupt pin, unused in polling mode
Sensors
SDAGPIO21Bar02, INA219I2C bus (shared)
SCLGPIO22Bar02, INA219I2C bus (shared)
Status LED
LED_RGPIO19Red ChannelPWM control
LED_GGPIO18Green ChannelPWM control
LED_BGPIO5Blue ChannelPWM control

ESPB (Communication Bridge) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Communication
TXGPIO1USB Serial115200 baud
RXGPIO3USB Serial115200 baud
Status LED
Built-in LEDGPIO2Onboard LEDStatus indication

I2C Device Addresses:

DeviceAddressBus Speed
VL53L7CX TOF0x291 MHz
Bar02 Pressure0x76Shared I2C bus
INA219 Battery0x40Initialized at 100 kHz

The firmware initializes the INA219 at 100 kHz, then the VL53L7CX driver raises the shared Wire clock to 1 MHz for TOF ranging.


SYSTEM ARCHITECTURE

Deployment Diagram

FLOAT code has to be deployed on two ESP32, one mounted on the FLOAT board (ESPA) together with the sensors and the power supply, and the other (ESPB) communicating with the Control Station via USB. The two ESP32 communicates via WiFi using ESP-NOW protocol. The software on the two ESP32 is designed to work regardless of the design of the GUI on the CS.

The idea is to bring all the complexity on the ESPA and GUI, leaving no trace of logic on the ESPB.

graph TB
subgraph "Control Station"
GUI[GUI Application]
USB[USB Serial]
end
subgraph "ESPB - Communication Bridge"
ESPB_FW[ESPB Firmware]
ESPB_WiFi[WiFi ESP-NOW]
ESPB_LED[Built-in LED]
end
subgraph "ESPA - Float Controller"
ESPA_FW[ESPA Firmware]
ESPA_WiFi[WiFi ESP-NOW]
subgraph "Motor System"
DRV8825[DRV8825 Driver]
STEPPER[Stepper Motor]
TOF[VL53L7CX TOF Sensor]
end
subgraph "Sensors"
BAR02[Bar02 Pressure]
INA219[INA219 Battery]
end
RGB[RGB LED]
I2C[I2C Bus]
end
GUI -->|Commands| USB
USB <-->|Serial 115200| ESPB_FW
ESPB_FW <-->|ESP-NOW 2.4GHz| ESPB_WiFi
ESPB_WiFi <-.->|WiFi| ESPA_WiFi
ESPA_WiFi <-->|ESP-NOW| ESPA_FW
ESPA_FW -->|Control Signals| DRV8825
DRV8825 -->|STEP/DIR| STEPPER
TOF -->|Distance Data| ESPA_FW
BAR02 -->|I2C| I2C
INA219 -->|I2C| I2C
I2C -->|Sensor Data| ESPA_FW
ESPA_FW -->|Status| RGB
ESPB_FW -->|Status| ESPB_LED
style ESPA_FW fill:#4CAF50
style ESPB_FW fill:#2196F3
style GUI fill:#FF9800
style TOF fill:#9C27B0
Loading

Software Structure

The project follows a modular architecture with separate compilation units:

  • Central Config (include/config.h) - pin mapping, motor constants, PID defaults, mission timing, network parameters
  • Shared Protocol (include/float_common.h) - ESP-NOW packet structs, ACK strings, EEPROM size, shared LED enum
  • Motor Control (lib/motor) - DRV8825/FastAccelStepper setup, position tracking, bounded movement primitives
  • TOF Sensor (lib/tof) - VL53L7CX initialization (4×4 multi-zone), aggregated minimum-distance reading with raw offset compensation
  • Motion Control (lib/motion_control) - TOF homing, safe max-extension move, balance routine, emergency stop handling
  • Communication (lib/comms) - ESP-NOW wireless protocol and ElegantOTA session management
  • Sensors (lib/sensors) - Bar02 pressure/depth and INA219 battery monitoring
  • PID Controller (lib/pid) - depth control algorithm with runtime gain updates
  • Profile Manager (lib/profile) - mission profile execution and flash-backed mission logging
  • Flash Storage (lib/flash_storage) - LittleFS CSV mission log and replay helpers
  • LED Controller (lib/led) - RGB status indication system

ESPA State Machine

The ESPA firmware operates as a state machine coordinating motor control, sensors, and communications:

stateDiagram-v2
[*] --> INIT: Power On
INIT --> HOMING: Sensors OK
INIT --> ERROR: Init Failed
HOMING --> IDLE: Homing Success
HOMING --> ERROR: Homing Failed/Timeout
IDLE --> EXECUTING: Command Received
IDLE --> IDLE: No Command
EXECUTING --> PROFILE: GO Command
EXECUTING --> BALANCE: BALANCE Command
EXECUTING --> SEND_DATA: LISTENING Command
EXECUTING --> CLEAR_DATA: CLEAR_SD Command
EXECUTING --> UPDATE_PID: PARAMS Command
EXECUTING --> UPDATE_PID_EXT: PARAMS_EXT Command
EXECUTING --> TEST_SPEED: TEST_FREQ Command
EXECUTING --> TEST_STEPS: TEST_STEPS Command
EXECUTING --> DEBUG_MODE: DEBUG Command
EXECUTING --> HOMING: HOME_MOTOR Command
EXECUTING --> OTA: TRY_UPLOAD Command
EXECUTING --> SYRINGE_SET: SYRINGE_SET Command
EXECUTING --> PID_HOLD: PID_HOLD Command
EXECUTING --> PID_STEP: PID_STEP Command
EXECUTING --> SET_SURFACE_OFFSET: SURFACE_OFFSET Command
PROFILE --> PID_CONTROL: Descending
PID_CONTROL --> PID_CONTROL: Depth Control Active
PID_CONTROL --> ASCENT: Target Reached/Timeout
ASCENT --> IDLE_W_DATA: At Surface
BALANCE --> IDLE: Balance Complete
SEND_DATA --> IDLE: Data Sent
CLEAR_DATA --> IDLE: Flash Log Cleared
UPDATE_PID --> IDLE: Gains Updated
UPDATE_PID_EXT --> IDLE: Period/alpha Updated
TEST_SPEED --> IDLE: Speed Stored
TEST_STEPS --> IDLE: Test Move Complete
DEBUG_MODE --> IDLE: Debug Toggle Complete
OTA --> IDLE: Upload Complete
SYRINGE_SET --> IDLE: Bench Test Complete
PID_HOLD --> IDLE: Hold Complete / Timeout
PID_STEP --> IDLE: Step Complete / Timeout
SET_SURFACE_OFFSET --> IDLE: Offset Stored
IDLE_W_DATA --> SENDING: LISTENING Command
SENDING --> IDLE: Data Transmitted
ERROR --> [*]: Manual Reset Required
note right of IDLE
RGB: Green Solid
Waiting for command
end note
note right of HOMING
RGB: Purple Blink
TOF-based homing
end note
note right of PID_CONTROL
RGB: Cyan Blink
Active depth control
end note
note right of ERROR
RGB: Red Blink
Fatal error state
end note
Loading

COMMUNICATION PROTOCOL

Command Lifecycle

A command life-cycle does not overlaps/interfere with the previous nor the next one: when the CS sends a command (and it arrives to the FLOAT), a feedback from the FLOAT should inform about the acceptance of the command and if this acknowledgement arrives within a given period (specified later), next command requests will be ignored until end of execution of the current one, signaled by an idle acknowledgement.

If the acknowledgement doesn't arrive within that time span, the command commit can be considered failed: this could happen for WiFi connection failures or FLOAT electronics issues.

When waiting for the command commit acknowledgement, other command requests will be ignored as well.

As already mentioned, after command completion the FLOAT will try to send an idle acknowledgement to signal that it is listening for a new command: together with the idle state, this acknowledgement can also inform about the presence of new flash-backed profile data that has to be sent to the CS. After an idle acknowledgement is received, a new command can be accepted.

To maintain consistency with the status stored on the ESPB, and hence with the GUI visuals, the FLOAT grants to send the acknowledgement signalling a command commit only when the commit can be given for sure. In the same way, if the acknowledgement fails to be sent due to connection issues, the command is not committed.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant WiFi as ESP-NOW
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Fresh State
CS->>ESPB: Send Command (e.g., "GO")
ESPB->>WiFi: Forward Command
WiFi->>ESPA: Deliver Command
ESPA->>ESPA: Validate & Accept
ESPA->>WiFi: ACK (e.g., "GO_RECVD")
WiFi->>ESPB: Deliver ACK
ESPB->>ESPB: Update State (status=2)
ESPB->>CS: Forward ACK
Note over ESPA: Executing Command...
ESPA->>ESPA: Complete Task
ESPA->>WiFi: Completion ACK (e.g., "FLOAT_IDLE")
WiFi->>ESPB: Deliver Completion
ESPB->>ESPB: Update State (status=0)
ESPB->>CS: Forward Completion
Note over CS: Ready for Next Command
Loading

ESPB Bridge Role

The ESPB role is only to make the CS task of continuously checking on the WiFi channel less resource consuming.

In particular, ESPB receives commands from CS via USB only to forward them to the FLOAT via WiFi. At the same time, it can receive feedback and data from the FLOAT. In the latter case the ESPB will forward the packages on the USB channel, while using them to update an internal state accordingly. The only code that should trigger the update of the state stored on ESPB is the firmware on the FLOAT, via the data and the feedback sent to CS (more later). This is because no assumptions have to be done by other software on the commands completion and acceptance.

The ESPB state should mirror the FLOAT state at each moment (more details later) and can be used by the GUI to give visual feedback to the user or to drive its internal logic. It can be requested to the ESPB by the CS at any moment with a specific command. The state info sent to the CS with this command also contains WiFi connection state info and AM activation state info.

In general, the ESPB feedback could be stale when requested (for example because the CS could poll it with a low frequency), so to get fresh, real-time data the CS should listen on the USB channel for the FLOAT packages, after a command request or when waiting for command completion and retrieve the FLOAT current state directly by those packages.

Periodic polling remains a legit choice in case the CS cannot exploit interrupts triggered by Serial connection, but notice that this solution leads to delayed GUI visual feedback with respect to the changes on the FLOAT state.

In some cases, connection losses can undermine consistency between the feedback of the ESPB (either they are polled or real time) and the real current FLOAT state (consistency threats for each FLOAT state later).

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Stale State Polling
CS->>ESPB: Send Command
ESPB->>ESPA: Forward Command
ESPA->>ESPA: Accept & Execute
ESPA->>ESPB: ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward ACK
Note over ESPA: Command Executing...
loop Periodic Polling
CS->>ESPB: Request STATUS
ESPB->>CS: Return Cached State
Note over CS: State may be stale<br/>if ESPA completed recently
end
ESPA->>ESPA: Complete Command
ESPA->>ESPB: Completion ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward Completion
CS->>ESPB: Request STATUS
ESPB->>CS: Return Fresh State
Loading

FLOAT Commands

Table of FLOAT commands with relative effects and acknowledgements:

Cmd stringCmd ESPA numberCmd effectsESPA ack stringESPA ack effects on ESPB state
GO1Performs the two MATE vertical profiles, sends the pre-descent data packet before the first descent, and logs pressure/depth records to flash CSVGO_RECVDstatus to 2 (command execution)
LISTENING2Streams flash CSV records as JSON data packets at 5-second cadence, followed by STOP_DATAAck is data itselfstatus to 2 after first package arrival
BALANCE3Cycles full extension and retraction with holdMs holds until Bar02 pressure rises above the startup baseline by BALANCE_STOP_PRESSURE_DELTA_KPA. Requires the motor to be homed first — otherwise the command fails with Balance: homing requiredCMD3_RECVDstatus to 2
CLEAR_SD4Clears and recreates the flash CSV log, and clears the legacy EEPROM buffer. The command string is kept as CLEAR_SD for compatibilityCMD4_RECVDstatus to 2
SWITCH_AUTO_MODE5Toggles FLOAT Auto ModeSWITCH_AM_RECVDstatus to 2, AM activation state toggled
SEND_PACKAGE6Sends a single live JSON snapshot containing company number, time, pressure, judge/reference depth, phase, and raw sensor depthAck is the package itselfstatus to 2
TRY_UPLOAD7Starts the ElegantOTA access point on ESPA for a 5-minute upload window, then restores ESP-NOWTRY_UPLOAD_RECVDstatus to 2
PARAMS kp ki kd8Updates PID gains at runtimeCHNG_PARMS_RECVDstatus to 2
TEST_FREQ freq9Sets manual test movement speed, clamped to 10-1200 steps/sTEST_FREQ_RECVDstatus to 2
TEST_STEPS n10Moves the motor by n relative steps at the current test speedTEST_STEPS_RECVDstatus to 2
DEBUG11Toggles remote debug forwarding through DebugSerialDEBUG_MODE_RECVDstatus to 2
HOME_MOTOR12Runs TOF-based homing remotelyHOME_RECVDstatus to 2
STOP13Triggers a remote emergency stop, stops the motor, disables outputs, and returns to idleSTOP_RECVDstatus to 2
PARAMS_EXT period alpha14Updates PID tick period (ms) and derivative LPF coefficient alphaD at runtimeCHNG_PID_EXT_RECVDstatus to 2
SYRINGE_SET u dur_s15Bench test: drives the syringe to normalized position u ∈ [0,1] for dur_s seconds, logging depth — bypasses the PID (DC gain / time-constant characterization)SYRINGE_SET_RECVDstatus to 2
PID_HOLD depth dur_s16Bench test: holds depth at depth_m for dur_s seconds with the PID active, logging at 5 HzPID_HOLD_RECVDstatus to 2
PID_STEP depth17Bench test: step response — drives the PID to depth_m for up to 60 s, logging at 10 HzPID_STEP_RECVDstatus to 2
SURFACE_OFFSET m18Sets the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime: the FLOAT will hold its top m metres below the waterline when "floating" (default 0.10)SURFACE_OFF_RECVDstatus to 2
STATUS-Requests stale ESPB status plus AM state, WiFi connection state, battery millivolts, and last RSSI--

Once a command is completed, ESPA acknowledgement can be:

ESPA ack stringESPA ack effects on ESPB stateESPA state
FLOAT_IDLEstatus to 0 (idle)Idle with no data to be sent
FLOAT_IDLE_W_DATAstatus to 1 (idle with data to be sent)Idle with data from last profile to be sent

STATUS COMMAND: ESPB RESPONSE

ESPB response to STATUS command is composed by five parts of information: ESPA state (stale), activation of the AM on the FLOAT, WiFi connection state, last received battery millivolts, and last received RSSI. The WiFi connection state is detected by sending a dummy command code 0, while the other states are kept consistent with the ones on the FLOAT by updating them after acknowledgements reception.

ESPA state:

ESPB state stringESPB state numberState description
UNKNOWN-1ESPB has not received any state message from ESPA since boot
CONNECTED0The FLOAT is listening for new command. Previous command succeeded
CONNECTED_W_DATA1The FLOAT is listening for new command and has some new data from last profile to be sent. Previous command succeeded
EXECUTING_CMD2FLOAT is executing a command
STATUS_ERROR-Internal error in reading the state number

WARNING:
If committing a profile automatically, the relative acknowledgement will likely fail due to connection loss. The profile is committed anyway as it is generated from connection loss in the first place, but the GUI may not have mean to detect it. So it will likely read an inconsistent idle status (CONNECTED or CONNECTED_W_DATA) until FLOAT is at water level with a stable WiFi connection. In the meantime the command commits will fail, for connection loss or because the FLOAT is underwater. Anyway WiFi connection state can be detected by the STATUS command, hence giving feedback on status consistency.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over ESPA: Auto Mode Active
Note over ESPA,ESPB: Connection Lost!
ESPA->>ESPA: Detect Connection Loss
ESPA->>ESPA: Auto-commit Profile
Note over ESPA: Descending...<br/>WiFi Unavailable
ESPA-xESPB: ACK Fails (No Connection)
Note over ESPB: State Becomes Inconsistent<br/>Still shows "IDLE"
CS->>ESPB: Request STATUS
ESPB->>CS: CONNECTED | CONN_LOST
Note over CS: GUI shows inconsistent state<br/>but WiFi loss detected
Note over ESPA: At Surface...<br/>WiFi Restored
ESPA->>ESPB: FLOAT_IDLE_W_DATA
ESPB->>ESPB: Update to Consistent State
ESPB->>CS: Forward State
Note over CS,ESPA: Consistency Restored
Loading

AM state:

ESPB state stringState description
AUTO_MODE_YESAM on FLOAT is activated
AUTO_MODE_NOAM on FLOAT is not activated: connection losses will not trigger profiles

WiFi connection state:

ESPB state stringState description
CONN_OKWiFi connection is ok
CONN_LOSTWiFi connection is currently down. ESPB state could be wrong

Battery and RSSI fields:

ESPB fieldState description
BATTERY: <mV>Last battery voltage received from ESPA acknowledgements
RSSI: <dBm>Last ESP-NOW packet RSSI captured by ESPB promiscuous callback

Example of ESPB state response:CONNECTED_W_DATA | AUTO_MODE_NO | CONN_OK | BATTERY: 12450 | RSSI: -63

At ESPB boot, before any ESPA packet is received, a status request may return: UNKNOWN | AUTO_MODE_NO | CONN_LOST | BATTERY: 0 | RSSI: 0.

GUI / ESPB / ESPA Protocol Contract

The GUI sends command strings to ESPB over USB serial. ESPB parses the string, sends the command number to ESPA over ESP-NOW, and forwards ESPA acknowledgements/data back to the GUI.

GUI commandESPA command numberESPA acknowledgement / response
GO1GO_RECVD
LISTENING2Stored data packets, then STOP_DATA
BALANCE3CMD3_RECVD
CLEAR_SD4CMD4_RECVD
SWITCH_AUTO_MODE5SWITCH_AM_RECVD
SEND_PACKAGE6Live JSON packet
TRY_UPLOAD7TRY_UPLOAD_RECVD
PARAMS kp ki kd8CHNG_PARMS_RECVD
TEST_FREQ freq9TEST_FREQ_RECVD
TEST_STEPS n10TEST_STEPS_RECVD
DEBUG11DEBUG_MODE_RECVD
HOME_MOTOR12HOME_RECVD
STOP13STOP_RECVD
PARAMS_EXT period_ms alpha_d14CHNG_PID_EXT_RECVD
SYRINGE_SET u dur_s15SYRINGE_SET_RECVD
PID_HOLD depth_m dur_s16PID_HOLD_RECVD
PID_STEP depth_m17PID_STEP_RECVD
SURFACE_OFFSET m18SURFACE_OFF_RECVD
STATUS-ESPB local status line with five `

The peer MAC addresses are configured centrally in include/config.h: MAC_ESPA is used by ESPB, and MAC_ESPB is used by ESPA.


LED STATUS INDICATORS

The FLOAT is equipped with RGB LEDs on both ESP32 boards that provide visual feedback about the system status:

ESPA (Float Board) LED States:

Driven by LEDState (scoped enum in lib/led/include/led.h):

LED Color/PatternStateDescription
Green Solid / Boot BlinksLEDState::INITSystem initializing
Green SolidLEDState::IDLEReady and idle, waiting for commands
Green BlinkLEDState::IDLE_WITH_DATAIdle with data ready to send
Red SolidLEDState::LOW_BATTERYBattery voltage below BATT_THRESH (12.0 V)
Red BlinkLEDState::ERRORError state or motor emergency stop
Blue SolidLEDState::PROFILERunning non-PID profile phase
Yellow BlinkLEDState::AUTO_MODEAuto mode active
Purple BlinkLEDState::HOMINGMotor homing in progress
Purple SolidLEDState::MOTOR_MOVINGMotor moving
Cyan BlinkLEDState::PID_CONTROLPID depth control active
White SolidLEDState::COMMUNICATIONCommand received / communicating with ESPB
Orange BlinkLEDState::OTA_MODEOTA update mode active
OffLEDState::OFFSystem off or disabled

ESPB uses a separate FloatLEDState enum (LED_* prefix) defined in include/float_common.h; the two enums are deliberately independent because the two boards have different LED states to signal.

ESPB (Communication Bridge) LED States:

LED PatternStateDescription
Solid OnLED_IDLEConnected and ready
Very Fast BlinkLED_ERRORCommunication error
OffLED_OFFSystem off or disabled

Note: ESPB uses the built-in LED (pin 2) with different blink patterns to indicate status, as it does not have external RGB connections.


DEVELOPMENT AND TESTING

PlatformIO Environments

EnvironmentPurposeMain Source
espAFloat controller firmware with sensors, TOF homing, motion control, PID, ESP-NOW, and OTAsrc/espA/main.cpp
espA_poolESPA firmware compiled with conservative 70 cm pool-test targets (POOL_TEST_PROFILE)src/espA/main.cpp
espBUSB-to-ESP-NOW bridge for the Control Stationsrc/espB/main.cpp
espA_manual_keyboardBench firmware for serial keyboard continuous motor movement without homingsrc/espA_manual_keyboard/main.cpp

Common commands:

pio run -e espA
pio run -e espA_pool
pio run -e espB
pio run -e espA_manual_keyboard
pio test -e espA

CLI Startup

Run all commands from the project root:

cd Float_2025

To build and upload the main firmware targets:

pio run -e espA -t upload
pio run -e espB -t upload

For a conservative shallow-pool test at about 70 cm, upload ESPA with:

pio run -e espA_pool -t upload

To open the serial monitor at 115200 baud:

pio device monitor -e espA
pio device monitor -e espB

Direct USB Tuning Commands (ESPA)

All commands in the FLOAT Commands table can be sent over the ESPB USB serial bridge using the same string syntax. The commands below — useful for bench tuning — can also be sent directly over ESPA's USB serial port (e.g. when ESPA is wired to a laptop for tuning runs), bypassing ESPB and ESP-NOW entirely.

CommandEffect
PARAMS <kp> <ki> <kd>Update PID gains at runtime (same effect as command 8)
PARAMS_EXT <period_ms> <alpha_d>Update PID tick period and derivative LPF coefficient (command 14)
SYRINGE_SET <u> <dur_s>Drive the syringe to position u ∈ [0,1] for dur_s seconds and log depth — bypasses the PID, useful for DC-gain and time-constant estimation (command 15)
PID_HOLD <depth_m> <dur_s>Hold PID at depth_m for dur_s seconds, log at 5 Hz (command 16)
PID_STEP <depth_m>Step response: PID at depth_m for up to 60 s, log at 10 Hz (command 17)
SURFACE_OFFSET <m>Set the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime (command 18)

CLI Tests

To run all available tests for the espA environment:

pio test -e espA

To run a single test:

pio test -e espA -f unit_hw/motor/test_max_steps
pio test -e espA -f unit_hw/motor/test_speed
pio test -e espA -f integration/test_screw_lead_20mm
pio test -e espA -f integration/test_homing_only
pio test -e espA -f integration/test_tof_reading
pio test -e espA -f integration/test_homing_move_to_max
pio test -e espA -f integration/test_motor_direction
pio test -e espA -f integration/test_tof_motor_accuracy

To test ESPB without ESPA powered on:

pio test -e espB -f unit_hw/espb_bridge/test_parser
pio test -e espB -f unit_hw/espb_bridge/test_status_format
pio test -e espB -f unit_hw/espb_bridge/test_protocol_contract

To test the real ESPB-to-ESPA bridge, first upload the real espA firmware, wait until ESPA is idle, then run:

pio test -e espB -f integration/test_espnow_bridge

This test only uses the dummy command 0 and SWITCH_AUTO_MODE; it does not start profiles or move the motor.

Available tests:

TestCommandWhat it checks
test_max_stepspio test -e espA -f unit_hw/motor/test_max_stepsMoves only the motor to the safe maximum extension starting from logical position 0
test_speedpio test -e espA -f unit_hw/motor/test_speedMoves only the motor through 6 alternating 40 mm moves, increasing speed and acceleration up to 2300
test_screw_lead_20mmpio test -e espA -f integration/test_screw_lead_20mmRuns TOF homing, moves the motor by 20 mm, and compares the internal TOF delta
test_motor_directionpio test -e espA -f integration/test_motor_directionMoves only the motor forward/backward and verifies the logical direction; by default it does not use TOF
test_tof_readingpio test -e espA -f integration/test_tof_readingInitializes only the TOF sensor and checks valid readings for about 30 s
test_homing_onlypio test -e espA -f integration/test_homing_onlyRuns only TOF-based homing
test_homing_move_to_maxpio test -e espA -f integration/test_homing_move_to_maxRuns TOF homing and then moves to the safe maximum extension
test_tof_motor_accuracypio test -e espA -f integration/test_tof_motor_accuracyCompares TOF distance and motor position after homing
test_parserpio test -e espB -f unit_hw/espb_bridge/test_parserVerifies GUI/Serial command parsing into ESPA packets without ESPA powered on
test_status_formatpio test -e espB -f unit_hw/espb_bridge/test_status_formatVerifies ESPB cached state and the five-field STATUS format
test_protocol_contractpio test -e espB -f unit_hw/espb_bridge/test_protocol_contractLocks the command/ACK consistency contract between GUI, ESPB, and ESPA
test_espnow_bridgepio test -e espB -f integration/test_espnow_bridgeVerifies real ESP-NOW with the real ESPA firmware powered on, without movement

The test_max_steps, test_speed, and test_motor_direction tests are useful when you need to move only the motor without TOF homing. Before running them, make sure the piston is away from the mechanical end stops and can move in both directions.

Manual Motor-Only Control

To upload the bench firmware that lets you move the motor from the serial keyboard:

pio run -e espA_manual_keyboard -t upload
pio device monitor -e espA_manual_keyboard

Commands in the serial monitor:

KeyAction
Up arrow or wHold to move toward home/up
Down arrow or sHold to move toward extension/down
Space or xStop immediately and disable motor outputs
pPrint the current position
tPrint one TOF reading
h or ?Print help

This firmware does not run homing: at startup it assigns a centered logical position and moves while it receives repeated keypresses; when the key is released it stops automatically after a short timeout. During movement it periodically prints motor position and TOF distance. Use it only when the mechanism is in a physically safe position.

Test Layout

Hardware-oriented tests are stored under test/:

  • test/unit_hw/motor/test_max_steps checks safe maximum extension from a known zero
  • test/unit_hw/motor/test_speed checks alternating 40 mm moves while speed and acceleration increase up to 2300
  • test/integration/test_screw_lead_20mm checks the configured screw pitch, starts, and lead with one 20 mm move measured internally by TOF
  • test/integration/test_tof_reading checks that the TOF sensor initializes and returns valid distance samples for about 30 seconds
  • test/integration/test_homing_only checks TOF-based homing
  • test/integration/test_homing_move_to_max checks homing followed by safe full extension
  • test/integration/test_motor_direction checks logical/physical motion direction, optionally using TOF
  • test/integration/test_tof_motor_accuracy checks TOF and motor movement consistency

Continuous Integration

GitHub Actions builds all three PlatformIO environments (espA, espB, espA_pool) on every push to any branch and on every pull request to master. Workflow file: .github/workflows/ci.yml.

CI does not run the unit_hw/ or integration/ PlatformIO tests because they need a real ESP32 with the float wired up. Run those locally on the bench.

Pushing a v* tag triggers .github/workflows/release.yml, which builds all three environments and attaches the resulting firmware.bin / firmware.elf to a GitHub Release auto-named after the tag.

See CONTRIBUTING.md for the full git workflow (trunk-based with PR review on master), commit conventions, and one-time branch protection setup.


UTILITIES AND RESOURCES

Arduino Library Repositories:

Development Tools:

  • PlatformIO IDE: Modern embedded development platform
  • ESP32 Arduino Core: Framework for ESP32 development
  • FastAccelStepper Library: Timer/task-driven stepper motor control
  • VL53L7CX Library: Multi-zone Time-of-Flight sensor driver

GLOSSARY

  • AM (Auto Mode): Autonomous operation mode that triggers profiles on connection loss
  • CS (Control Station): Ground-based computer running the GUI application
  • ESPA: ESP32 mounted on the Float board (primary controller)
  • ESPB: ESP32 communication bridge between Float and CS
  • Flash Profile Log: Current onboard LittleFS CSV storage used before JSON transmission
  • FastAccelStepper: Timer/task-driven stepper library used by MotorController
  • MotionController: Firmware layer that combines motor, TOF, LEDs, debug, timeouts, and emergency stops for safe movement routines
  • Commit a command: To accept a sent command. After commit, command execution and success is ideally granted
  • Complete a command: To execute all the requirements requested by a command
  • Profile: A complete mission cycle (descent → depth control → ascent → data transmission)
  • TOF (Time-of-Flight): Non-contact distance measurement technology using light pulses
  • Homing: Process of establishing the motor's zero reference position
  • PID Control: Proportional-Integral-Derivative controller for precise depth maintenance
  • ESP-NOW: Low-latency peer-to-peer WiFi communication protocol by Espressif

Documentation Version: 11.2.0 Last Updated: May 2026

Recent changes:

  • PID output normalized to u ∈ [0, 1] (fraction of syringe travel). Default gains Kp = 0.17, Kd = 0.13, expressed per metre of depth error so they stay valid if MOTOR_MAX_STEPS changes.
  • Motor geometry: home = piston fully inserted (empty syringes, floats); full extension = piston extracted (full syringes, sinks). The mapping uToMotorPos() in include/config.h encapsulates MOTOR_INVERT_LOGICAL so motion code never hard-codes signs.
  • TOF safety range widened to [40, 85] mm to give 10 mm of margin above the homing threshold without risking the mechanical end stop.
  • balance now refuses to start without a prior homing (was forcing pos = 0 as a fallback, mechanically risky).
  • New SURFACE_TARGET_OFFSET_M constant and SURFACE_OFFSET <m> command (number 18) for tuning the surface idle position at runtime. Team Contact: PoliTOcean @ Politecnico di Torino Maintainers: Colabella Davide, Benevenga Filippo

About

Firmware for the float project

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

PoliTOcean Float 2025 - Technical Documentation

CI

Version: 11.2.0 Team: PoliTOcean @ Politecnico di Torino
Maintainers: Colabella Davide, Benevenga Filippo
Competition: MATE ROV 2025/26


TABLE OF CONTENTS


PROJECT OVERVIEW

Introduction and Requirements

By MATE 2026 requirements documentation (Task 4.1 - MATE Floats Under the Ice), the FLOAT must complete operational vertical profiling missions under simulated ice conditions.

Pre-Deployment Requirements:

  • The FLOAT must communicate with the Mission Station (CS) prior to descending, transmitting a defined data packet containing:
    • Company number (provided by MATE)
    • Time data (UTC/local/float time)
    • Pressure data (pa or kpa) and/or depth data (m or cm)
    • Additional data as required
  • Example packet: EX01 1:51:42 UTC 9.8 kpa 1.00 meters

Vertical Profile Requirements:

The FLOAT must complete two vertical profiles using a buoyancy engine (fluid displacement system, not thrusters). Each profile consists of:

  1. Descent Phase: Descend from surface to 2.5 meters depth (± 33 cm)
  2. Deep Hold: Maintain depth at 2.5 meters for 30 seconds (bottom of float as reference)
  3. Ascent Phase: Rise to 40 cm depth (± 33 cm) without breaking surface or contacting ice
  4. Shallow Hold: Maintain depth at 40 cm for 30 seconds (top of float as reference)

Data Collection & Transmission:

  • Collect depth/pressure measurements during both profiles and transmit judge packets every 5 seconds (minimum 20 data packets)
  • Store data in ESP32 internal flash as a LittleFS CSV containing: company number, profile id, time, pressure, judge/reference depth, phase, and raw sensor depth
  • After recovery, transmit all collected data wirelessly to the Mission Station
  • Data packets must show 7 sequential measurements (spanning 30 seconds at 5-second intervals: 0, 5, 10, 15, 20, 25, 30) confirming proper depth maintenance at both 2.5m and 0.4m

Post-Mission Requirements:

  • Upon surface recovery, autonomously transmit all profile data to the CS
  • CS GUI plots depth over time using received data (minimum 20 data packets required)
  • Graph must display time (X-axis) vs depth (Y-axis) for both completed profiles

Current firmware storage note: the active implementation uses the internal flash CSV log (FLASH_LOG_PATH) as the primary mission data source. EEPROM compact records remain only as an internal legacy buffer. The legacy serial command name is still CLEAR_SD, but it now resets the flash CSV log and the legacy EEPROM buffer.

Auto Mode (AM):

An autonomous operating mode that triggers profile execution in case of connection loss with the CS, ensuring mission completion if communication is temporarily unavailable. AM will autonomously commit up to two profiles when connection is lost, preventing incomplete missions due to transient WiFi failures.

Penalties:

  • Breaking surface or contacting ice sheet during profile: -5 points per profile
  • FLOAT must remain submerged between 40 cm and 2.5 m throughout the ascent/descent phases

System Behavior

The general idea is that the FLOAT provides some micro-services that the CS can activate by sending commands to it. Every command can be requested at any moment, with the only limit that a command can be accepted by the FLOAT only when the previous one has been completed (more info on command cycle later).

The FLOAT has two main logical states: the command execution one, and the idle one in which it waits for the new command. In idle state, the FLOAT can have buffered flash data from the last completed profile that can be sent to the CS.

Syringe / Motor Convention

The FLOAT changes its buoyancy by pulling and pushing water through a pair of syringes driven by a stepper motor through a lead screw. The mechanical convention is:

  • Home (motor_pos = 0): piston fully inserted, syringes empty of water → the FLOAT floats. At this position the TOF reads ≈ TOF_HOMING_THRESHOLD (75 mm) because the piston is far from the sensor.
  • Full extension (motor_pos = uToMotorPos(1.0f)): piston extracted, syringes full of water → the FLOAT sinks. TOF reads ≈ TOF_SAFE_RANGE_MIN_MM (40 mm).
  • PID logical convention: u ∈ [0, 1] with u = 0 → float (empty) and u = 1 → sink (full). The helper uToMotorPos(u) in include/config.h maps u to the actual motor target while respecting MOTOR_INVERT_LOGICAL, so callers never hard-code signs.

TOF safety limits used during motion:

ConstantDefaultMeaning
TOF_HOMING_THRESHOLD75 mmPhase 2 of homing stops when the TOF reads above this
TOF_HOMING_APPROACH_MM50 mmPhase 1 of homing stops when the TOF reads below this
TOF_SAFE_RANGE_MIN_MM40 mmLower bound: syringe fully extended (mechanical limit)
TOF_SAFE_RANGE_MAX_MM85 mmUpper bound: 10 mm above the homing threshold; any higher and the piston would risk hitting the back mechanical stop

Surface Target Offset

When the FLOAT is "floating", we usually want its top a few centimetres below the water surface — not exactly at the waterline — so that the float remains visible without being completely above water. This is controlled by SURFACE_TARGET_OFFSET_M (default 0.10 m: top of the float 10 cm below the surface).

Two ways to change it:

  • At compile time: edit SURFACE_TARGET_OFFSET_M in include/config.h.
  • At runtime: send command SURFACE_OFFSET <m> via the CS, or SURFACE_OFFSET <m> over USB serial on ESPA. The change persists until the next reboot.

The offset is geometry-agnostic: FLOAT_TOP_TO_SENSOR_M (geometric distance between the top of the float and the barometer) and SURFACE_TARGET_OFFSET_M (operational target) are kept as separate constants in include/config.h.


HARDWARE CONFIGURATION

Hardware Used

HardwareRoleLinkKey parameters / notes
ESP32 Dev Module x2ESPA float controller and ESPB communication bridgeEspressif ESP32Arduino framework, ESP-NOW link, USB serial bridge on ESPB
DRV8825 stepper driverStepper motor driver for syringe motionPololu DRV8825 carrierSTEP/DIR control, active-low enable, SLEEP and RESET held HIGH during operation
Stepper motor with planetary gearboxSyringe actuator motorStepperOnline 17HS15-1684S-PG27NEMA 17, 200 steps/rev, 1.8 deg/step, configured gear ratio 26.85124:1, microstep setting 1
Lead screw / threaded rodConverts motor rotation to linear travelSIENOC 500 mm trapezoidal lead screwPitch 2.0 mm, 4 starts, lead 8.0 mm/rev, configured travel 35 mm
VL53L7CX Time-of-Flight sensorNon-contact homing distance sensor (multi-zone)ST VL53L7CXI2C 0x29, LPn (XSHUT) GPIO16, GPIO1 GPIO15, 4×4 zone mode with central-zone mask 0x0660, 6 mm raw offset, 75 mm homing threshold
Bar02 pressure sensorPressure/depth measurementBlue Robotics Bar02MS5837_02BA model, I2C 0x76, used for depth and pressure
INA219 battery monitorBattery bus-voltage monitorAdafruit INA219 breakoutI2C 0x40, initialized at 100 kHz, configured with 5 A max and 0.1 ohm shunt

Pin Mapping

ESPA (Float Controller) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Motor Control
DIRGPIO32DRV8825 DirectionStepper direction control
STEPGPIO33DRV8825 StepStep pulse generation
ENGPIO27DRV8825 EnableActive-LOW, disables outputs when HIGH
SLEEPGPIO25DRV8825 SleepActive-LOW, must be HIGH for operation
RSTGPIO26DRV8825 ResetActive-LOW, must be HIGH for operation
TOF Sensor
SDAGPIO21VL53L7CX I2C DataI2C bus (shared with sensors)
SCLGPIO22VL53L7CX I2C ClockI2C bus @ 1MHz
XSHUTGPIO16VL53L7CX LPn (shutdown)Sensor enable / shutdown control
GPIO1GPIO15VL53L7CX InterruptOptional interrupt pin, unused in polling mode
Sensors
SDAGPIO21Bar02, INA219I2C bus (shared)
SCLGPIO22Bar02, INA219I2C bus (shared)
Status LED
LED_RGPIO19Red ChannelPWM control
LED_GGPIO18Green ChannelPWM control
LED_BGPIO5Blue ChannelPWM control

ESPB (Communication Bridge) Pin Mapping:

FunctionGPIO PinConnected ToNotes
Communication
TXGPIO1USB Serial115200 baud
RXGPIO3USB Serial115200 baud
Status LED
Built-in LEDGPIO2Onboard LEDStatus indication

I2C Device Addresses:

DeviceAddressBus Speed
VL53L7CX TOF0x291 MHz
Bar02 Pressure0x76Shared I2C bus
INA219 Battery0x40Initialized at 100 kHz

The firmware initializes the INA219 at 100 kHz, then the VL53L7CX driver raises the shared Wire clock to 1 MHz for TOF ranging.


SYSTEM ARCHITECTURE

Deployment Diagram

FLOAT code has to be deployed on two ESP32, one mounted on the FLOAT board (ESPA) together with the sensors and the power supply, and the other (ESPB) communicating with the Control Station via USB. The two ESP32 communicates via WiFi using ESP-NOW protocol. The software on the two ESP32 is designed to work regardless of the design of the GUI on the CS.

The idea is to bring all the complexity on the ESPA and GUI, leaving no trace of logic on the ESPB.

graph TB
subgraph "Control Station"
GUI[GUI Application]
USB[USB Serial]
end
subgraph "ESPB - Communication Bridge"
ESPB_FW[ESPB Firmware]
ESPB_WiFi[WiFi ESP-NOW]
ESPB_LED[Built-in LED]
end
subgraph "ESPA - Float Controller"
ESPA_FW[ESPA Firmware]
ESPA_WiFi[WiFi ESP-NOW]
subgraph "Motor System"
DRV8825[DRV8825 Driver]
STEPPER[Stepper Motor]
TOF[VL53L7CX TOF Sensor]
end
subgraph "Sensors"
BAR02[Bar02 Pressure]
INA219[INA219 Battery]
end
RGB[RGB LED]
I2C[I2C Bus]
end
GUI -->|Commands| USB
USB <-->|Serial 115200| ESPB_FW
ESPB_FW <-->|ESP-NOW 2.4GHz| ESPB_WiFi
ESPB_WiFi <-.->|WiFi| ESPA_WiFi
ESPA_WiFi <-->|ESP-NOW| ESPA_FW
ESPA_FW -->|Control Signals| DRV8825
DRV8825 -->|STEP/DIR| STEPPER
TOF -->|Distance Data| ESPA_FW
BAR02 -->|I2C| I2C
INA219 -->|I2C| I2C
I2C -->|Sensor Data| ESPA_FW
ESPA_FW -->|Status| RGB
ESPB_FW -->|Status| ESPB_LED
style ESPA_FW fill:#4CAF50
style ESPB_FW fill:#2196F3
style GUI fill:#FF9800
style TOF fill:#9C27B0
Loading

Software Structure

The project follows a modular architecture with separate compilation units:

  • Central Config (include/config.h) - pin mapping, motor constants, PID defaults, mission timing, network parameters
  • Shared Protocol (include/float_common.h) - ESP-NOW packet structs, ACK strings, EEPROM size, shared LED enum
  • Motor Control (lib/motor) - DRV8825/FastAccelStepper setup, position tracking, bounded movement primitives
  • TOF Sensor (lib/tof) - VL53L7CX initialization (4×4 multi-zone), aggregated minimum-distance reading with raw offset compensation
  • Motion Control (lib/motion_control) - TOF homing, safe max-extension move, balance routine, emergency stop handling
  • Communication (lib/comms) - ESP-NOW wireless protocol and ElegantOTA session management
  • Sensors (lib/sensors) - Bar02 pressure/depth and INA219 battery monitoring
  • PID Controller (lib/pid) - depth control algorithm with runtime gain updates
  • Profile Manager (lib/profile) - mission profile execution and flash-backed mission logging
  • Flash Storage (lib/flash_storage) - LittleFS CSV mission log and replay helpers
  • LED Controller (lib/led) - RGB status indication system

ESPA State Machine

The ESPA firmware operates as a state machine coordinating motor control, sensors, and communications:

stateDiagram-v2
[*] --> INIT: Power On
INIT --> HOMING: Sensors OK
INIT --> ERROR: Init Failed
HOMING --> IDLE: Homing Success
HOMING --> ERROR: Homing Failed/Timeout
IDLE --> EXECUTING: Command Received
IDLE --> IDLE: No Command
EXECUTING --> PROFILE: GO Command
EXECUTING --> BALANCE: BALANCE Command
EXECUTING --> SEND_DATA: LISTENING Command
EXECUTING --> CLEAR_DATA: CLEAR_SD Command
EXECUTING --> UPDATE_PID: PARAMS Command
EXECUTING --> UPDATE_PID_EXT: PARAMS_EXT Command
EXECUTING --> TEST_SPEED: TEST_FREQ Command
EXECUTING --> TEST_STEPS: TEST_STEPS Command
EXECUTING --> DEBUG_MODE: DEBUG Command
EXECUTING --> HOMING: HOME_MOTOR Command
EXECUTING --> OTA: TRY_UPLOAD Command
EXECUTING --> SYRINGE_SET: SYRINGE_SET Command
EXECUTING --> PID_HOLD: PID_HOLD Command
EXECUTING --> PID_STEP: PID_STEP Command
EXECUTING --> SET_SURFACE_OFFSET: SURFACE_OFFSET Command
PROFILE --> PID_CONTROL: Descending
PID_CONTROL --> PID_CONTROL: Depth Control Active
PID_CONTROL --> ASCENT: Target Reached/Timeout
ASCENT --> IDLE_W_DATA: At Surface
BALANCE --> IDLE: Balance Complete
SEND_DATA --> IDLE: Data Sent
CLEAR_DATA --> IDLE: Flash Log Cleared
UPDATE_PID --> IDLE: Gains Updated
UPDATE_PID_EXT --> IDLE: Period/alpha Updated
TEST_SPEED --> IDLE: Speed Stored
TEST_STEPS --> IDLE: Test Move Complete
DEBUG_MODE --> IDLE: Debug Toggle Complete
OTA --> IDLE: Upload Complete
SYRINGE_SET --> IDLE: Bench Test Complete
PID_HOLD --> IDLE: Hold Complete / Timeout
PID_STEP --> IDLE: Step Complete / Timeout
SET_SURFACE_OFFSET --> IDLE: Offset Stored
IDLE_W_DATA --> SENDING: LISTENING Command
SENDING --> IDLE: Data Transmitted
ERROR --> [*]: Manual Reset Required
note right of IDLE
RGB: Green Solid
Waiting for command
end note
note right of HOMING
RGB: Purple Blink
TOF-based homing
end note
note right of PID_CONTROL
RGB: Cyan Blink
Active depth control
end note
note right of ERROR
RGB: Red Blink
Fatal error state
end note
Loading

COMMUNICATION PROTOCOL

Command Lifecycle

A command life-cycle does not overlaps/interfere with the previous nor the next one: when the CS sends a command (and it arrives to the FLOAT), a feedback from the FLOAT should inform about the acceptance of the command and if this acknowledgement arrives within a given period (specified later), next command requests will be ignored until end of execution of the current one, signaled by an idle acknowledgement.

If the acknowledgement doesn't arrive within that time span, the command commit can be considered failed: this could happen for WiFi connection failures or FLOAT electronics issues.

When waiting for the command commit acknowledgement, other command requests will be ignored as well.

As already mentioned, after command completion the FLOAT will try to send an idle acknowledgement to signal that it is listening for a new command: together with the idle state, this acknowledgement can also inform about the presence of new flash-backed profile data that has to be sent to the CS. After an idle acknowledgement is received, a new command can be accepted.

To maintain consistency with the status stored on the ESPB, and hence with the GUI visuals, the FLOAT grants to send the acknowledgement signalling a command commit only when the commit can be given for sure. In the same way, if the acknowledgement fails to be sent due to connection issues, the command is not committed.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant WiFi as ESP-NOW
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Fresh State
CS->>ESPB: Send Command (e.g., "GO")
ESPB->>WiFi: Forward Command
WiFi->>ESPA: Deliver Command
ESPA->>ESPA: Validate & Accept
ESPA->>WiFi: ACK (e.g., "GO_RECVD")
WiFi->>ESPB: Deliver ACK
ESPB->>ESPB: Update State (status=2)
ESPB->>CS: Forward ACK
Note over ESPA: Executing Command...
ESPA->>ESPA: Complete Task
ESPA->>WiFi: Completion ACK (e.g., "FLOAT_IDLE")
WiFi->>ESPB: Deliver Completion
ESPB->>ESPB: Update State (status=0)
ESPB->>CS: Forward Completion
Note over CS: Ready for Next Command
Loading

ESPB Bridge Role

The ESPB role is only to make the CS task of continuously checking on the WiFi channel less resource consuming.

In particular, ESPB receives commands from CS via USB only to forward them to the FLOAT via WiFi. At the same time, it can receive feedback and data from the FLOAT. In the latter case the ESPB will forward the packages on the USB channel, while using them to update an internal state accordingly. The only code that should trigger the update of the state stored on ESPB is the firmware on the FLOAT, via the data and the feedback sent to CS (more later). This is because no assumptions have to be done by other software on the commands completion and acceptance.

The ESPB state should mirror the FLOAT state at each moment (more details later) and can be used by the GUI to give visual feedback to the user or to drive its internal logic. It can be requested to the ESPB by the CS at any moment with a specific command. The state info sent to the CS with this command also contains WiFi connection state info and AM activation state info.

In general, the ESPB feedback could be stale when requested (for example because the CS could poll it with a low frequency), so to get fresh, real-time data the CS should listen on the USB channel for the FLOAT packages, after a command request or when waiting for command completion and retrieve the FLOAT current state directly by those packages.

Periodic polling remains a legit choice in case the CS cannot exploit interrupts triggered by Serial connection, but notice that this solution leads to delayed GUI visual feedback with respect to the changes on the FLOAT state.

In some cases, connection losses can undermine consistency between the feedback of the ESPB (either they are polled or real time) and the real current FLOAT state (consistency threats for each FLOAT state later).

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over CS,ESPA: Command Execution with Stale State Polling
CS->>ESPB: Send Command
ESPB->>ESPA: Forward Command
ESPA->>ESPA: Accept & Execute
ESPA->>ESPB: ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward ACK
Note over ESPA: Command Executing...
loop Periodic Polling
CS->>ESPB: Request STATUS
ESPB->>CS: Return Cached State
Note over CS: State may be stale<br/>if ESPA completed recently
end
ESPA->>ESPA: Complete Command
ESPA->>ESPB: Completion ACK
ESPB->>ESPB: Update State
ESPB->>CS: Forward Completion
CS->>ESPB: Request STATUS
ESPB->>CS: Return Fresh State
Loading

FLOAT Commands

Table of FLOAT commands with relative effects and acknowledgements:

Cmd stringCmd ESPA numberCmd effectsESPA ack stringESPA ack effects on ESPB state
GO1Performs the two MATE vertical profiles, sends the pre-descent data packet before the first descent, and logs pressure/depth records to flash CSVGO_RECVDstatus to 2 (command execution)
LISTENING2Streams flash CSV records as JSON data packets at 5-second cadence, followed by STOP_DATAAck is data itselfstatus to 2 after first package arrival
BALANCE3Cycles full extension and retraction with holdMs holds until Bar02 pressure rises above the startup baseline by BALANCE_STOP_PRESSURE_DELTA_KPA. Requires the motor to be homed first — otherwise the command fails with Balance: homing requiredCMD3_RECVDstatus to 2
CLEAR_SD4Clears and recreates the flash CSV log, and clears the legacy EEPROM buffer. The command string is kept as CLEAR_SD for compatibilityCMD4_RECVDstatus to 2
SWITCH_AUTO_MODE5Toggles FLOAT Auto ModeSWITCH_AM_RECVDstatus to 2, AM activation state toggled
SEND_PACKAGE6Sends a single live JSON snapshot containing company number, time, pressure, judge/reference depth, phase, and raw sensor depthAck is the package itselfstatus to 2
TRY_UPLOAD7Starts the ElegantOTA access point on ESPA for a 5-minute upload window, then restores ESP-NOWTRY_UPLOAD_RECVDstatus to 2
PARAMS kp ki kd8Updates PID gains at runtimeCHNG_PARMS_RECVDstatus to 2
TEST_FREQ freq9Sets manual test movement speed, clamped to 10-1200 steps/sTEST_FREQ_RECVDstatus to 2
TEST_STEPS n10Moves the motor by n relative steps at the current test speedTEST_STEPS_RECVDstatus to 2
DEBUG11Toggles remote debug forwarding through DebugSerialDEBUG_MODE_RECVDstatus to 2
HOME_MOTOR12Runs TOF-based homing remotelyHOME_RECVDstatus to 2
STOP13Triggers a remote emergency stop, stops the motor, disables outputs, and returns to idleSTOP_RECVDstatus to 2
PARAMS_EXT period alpha14Updates PID tick period (ms) and derivative LPF coefficient alphaD at runtimeCHNG_PID_EXT_RECVDstatus to 2
SYRINGE_SET u dur_s15Bench test: drives the syringe to normalized position u ∈ [0,1] for dur_s seconds, logging depth — bypasses the PID (DC gain / time-constant characterization)SYRINGE_SET_RECVDstatus to 2
PID_HOLD depth dur_s16Bench test: holds depth at depth_m for dur_s seconds with the PID active, logging at 5 HzPID_HOLD_RECVDstatus to 2
PID_STEP depth17Bench test: step response — drives the PID to depth_m for up to 60 s, logging at 10 HzPID_STEP_RECVDstatus to 2
SURFACE_OFFSET m18Sets the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime: the FLOAT will hold its top m metres below the waterline when "floating" (default 0.10)SURFACE_OFF_RECVDstatus to 2
STATUS-Requests stale ESPB status plus AM state, WiFi connection state, battery millivolts, and last RSSI--

Once a command is completed, ESPA acknowledgement can be:

ESPA ack stringESPA ack effects on ESPB stateESPA state
FLOAT_IDLEstatus to 0 (idle)Idle with no data to be sent
FLOAT_IDLE_W_DATAstatus to 1 (idle with data to be sent)Idle with data from last profile to be sent

STATUS COMMAND: ESPB RESPONSE

ESPB response to STATUS command is composed by five parts of information: ESPA state (stale), activation of the AM on the FLOAT, WiFi connection state, last received battery millivolts, and last received RSSI. The WiFi connection state is detected by sending a dummy command code 0, while the other states are kept consistent with the ones on the FLOAT by updating them after acknowledgements reception.

ESPA state:

ESPB state stringESPB state numberState description
UNKNOWN-1ESPB has not received any state message from ESPA since boot
CONNECTED0The FLOAT is listening for new command. Previous command succeeded
CONNECTED_W_DATA1The FLOAT is listening for new command and has some new data from last profile to be sent. Previous command succeeded
EXECUTING_CMD2FLOAT is executing a command
STATUS_ERROR-Internal error in reading the state number

WARNING:
If committing a profile automatically, the relative acknowledgement will likely fail due to connection loss. The profile is committed anyway as it is generated from connection loss in the first place, but the GUI may not have mean to detect it. So it will likely read an inconsistent idle status (CONNECTED or CONNECTED_W_DATA) until FLOAT is at water level with a stable WiFi connection. In the meantime the command commits will fail, for connection loss or because the FLOAT is underwater. Anyway WiFi connection state can be detected by the STATUS command, hence giving feedback on status consistency.

sequenceDiagram
participant CS as Control Station
participant ESPB as ESPB Bridge
participant ESPA as ESPA Float
Note over ESPA: Auto Mode Active
Note over ESPA,ESPB: Connection Lost!
ESPA->>ESPA: Detect Connection Loss
ESPA->>ESPA: Auto-commit Profile
Note over ESPA: Descending...<br/>WiFi Unavailable
ESPA-xESPB: ACK Fails (No Connection)
Note over ESPB: State Becomes Inconsistent<br/>Still shows "IDLE"
CS->>ESPB: Request STATUS
ESPB->>CS: CONNECTED | CONN_LOST
Note over CS: GUI shows inconsistent state<br/>but WiFi loss detected
Note over ESPA: At Surface...<br/>WiFi Restored
ESPA->>ESPB: FLOAT_IDLE_W_DATA
ESPB->>ESPB: Update to Consistent State
ESPB->>CS: Forward State
Note over CS,ESPA: Consistency Restored
Loading

AM state:

ESPB state stringState description
AUTO_MODE_YESAM on FLOAT is activated
AUTO_MODE_NOAM on FLOAT is not activated: connection losses will not trigger profiles

WiFi connection state:

ESPB state stringState description
CONN_OKWiFi connection is ok
CONN_LOSTWiFi connection is currently down. ESPB state could be wrong

Battery and RSSI fields:

ESPB fieldState description
BATTERY: <mV>Last battery voltage received from ESPA acknowledgements
RSSI: <dBm>Last ESP-NOW packet RSSI captured by ESPB promiscuous callback

Example of ESPB state response:CONNECTED_W_DATA | AUTO_MODE_NO | CONN_OK | BATTERY: 12450 | RSSI: -63

At ESPB boot, before any ESPA packet is received, a status request may return: UNKNOWN | AUTO_MODE_NO | CONN_LOST | BATTERY: 0 | RSSI: 0.

GUI / ESPB / ESPA Protocol Contract

The GUI sends command strings to ESPB over USB serial. ESPB parses the string, sends the command number to ESPA over ESP-NOW, and forwards ESPA acknowledgements/data back to the GUI.

GUI commandESPA command numberESPA acknowledgement / response
GO1GO_RECVD
LISTENING2Stored data packets, then STOP_DATA
BALANCE3CMD3_RECVD
CLEAR_SD4CMD4_RECVD
SWITCH_AUTO_MODE5SWITCH_AM_RECVD
SEND_PACKAGE6Live JSON packet
TRY_UPLOAD7TRY_UPLOAD_RECVD
PARAMS kp ki kd8CHNG_PARMS_RECVD
TEST_FREQ freq9TEST_FREQ_RECVD
TEST_STEPS n10TEST_STEPS_RECVD
DEBUG11DEBUG_MODE_RECVD
HOME_MOTOR12HOME_RECVD
STOP13STOP_RECVD
PARAMS_EXT period_ms alpha_d14CHNG_PID_EXT_RECVD
SYRINGE_SET u dur_s15SYRINGE_SET_RECVD
PID_HOLD depth_m dur_s16PID_HOLD_RECVD
PID_STEP depth_m17PID_STEP_RECVD
SURFACE_OFFSET m18SURFACE_OFF_RECVD
STATUS-ESPB local status line with five `

The peer MAC addresses are configured centrally in include/config.h: MAC_ESPA is used by ESPB, and MAC_ESPB is used by ESPA.


LED STATUS INDICATORS

The FLOAT is equipped with RGB LEDs on both ESP32 boards that provide visual feedback about the system status:

ESPA (Float Board) LED States:

Driven by LEDState (scoped enum in lib/led/include/led.h):

LED Color/PatternStateDescription
Green Solid / Boot BlinksLEDState::INITSystem initializing
Green SolidLEDState::IDLEReady and idle, waiting for commands
Green BlinkLEDState::IDLE_WITH_DATAIdle with data ready to send
Red SolidLEDState::LOW_BATTERYBattery voltage below BATT_THRESH (12.0 V)
Red BlinkLEDState::ERRORError state or motor emergency stop
Blue SolidLEDState::PROFILERunning non-PID profile phase
Yellow BlinkLEDState::AUTO_MODEAuto mode active
Purple BlinkLEDState::HOMINGMotor homing in progress
Purple SolidLEDState::MOTOR_MOVINGMotor moving
Cyan BlinkLEDState::PID_CONTROLPID depth control active
White SolidLEDState::COMMUNICATIONCommand received / communicating with ESPB
Orange BlinkLEDState::OTA_MODEOTA update mode active
OffLEDState::OFFSystem off or disabled

ESPB uses a separate FloatLEDState enum (LED_* prefix) defined in include/float_common.h; the two enums are deliberately independent because the two boards have different LED states to signal.

ESPB (Communication Bridge) LED States:

LED PatternStateDescription
Solid OnLED_IDLEConnected and ready
Very Fast BlinkLED_ERRORCommunication error
OffLED_OFFSystem off or disabled

Note: ESPB uses the built-in LED (pin 2) with different blink patterns to indicate status, as it does not have external RGB connections.


DEVELOPMENT AND TESTING

PlatformIO Environments

EnvironmentPurposeMain Source
espAFloat controller firmware with sensors, TOF homing, motion control, PID, ESP-NOW, and OTAsrc/espA/main.cpp
espA_poolESPA firmware compiled with conservative 70 cm pool-test targets (POOL_TEST_PROFILE)src/espA/main.cpp
espBUSB-to-ESP-NOW bridge for the Control Stationsrc/espB/main.cpp
espA_manual_keyboardBench firmware for serial keyboard continuous motor movement without homingsrc/espA_manual_keyboard/main.cpp

Common commands:

pio run -e espA
pio run -e espA_pool
pio run -e espB
pio run -e espA_manual_keyboard
pio test -e espA

CLI Startup

Run all commands from the project root:

cd Float_2025

To build and upload the main firmware targets:

pio run -e espA -t upload
pio run -e espB -t upload

For a conservative shallow-pool test at about 70 cm, upload ESPA with:

pio run -e espA_pool -t upload

To open the serial monitor at 115200 baud:

pio device monitor -e espA
pio device monitor -e espB

Direct USB Tuning Commands (ESPA)

All commands in the FLOAT Commands table can be sent over the ESPB USB serial bridge using the same string syntax. The commands below — useful for bench tuning — can also be sent directly over ESPA's USB serial port (e.g. when ESPA is wired to a laptop for tuning runs), bypassing ESPB and ESP-NOW entirely.

CommandEffect
PARAMS <kp> <ki> <kd>Update PID gains at runtime (same effect as command 8)
PARAMS_EXT <period_ms> <alpha_d>Update PID tick period and derivative LPF coefficient (command 14)
SYRINGE_SET <u> <dur_s>Drive the syringe to position u ∈ [0,1] for dur_s seconds and log depth — bypasses the PID, useful for DC-gain and time-constant estimation (command 15)
PID_HOLD <depth_m> <dur_s>Hold PID at depth_m for dur_s seconds, log at 5 Hz (command 16)
PID_STEP <depth_m>Step response: PID at depth_m for up to 60 s, log at 10 Hz (command 17)
SURFACE_OFFSET <m>Set the surface target offset (SURFACE_TARGET_OFFSET_M) at runtime (command 18)

CLI Tests

To run all available tests for the espA environment:

pio test -e espA

To run a single test:

pio test -e espA -f unit_hw/motor/test_max_steps
pio test -e espA -f unit_hw/motor/test_speed
pio test -e espA -f integration/test_screw_lead_20mm
pio test -e espA -f integration/test_homing_only
pio test -e espA -f integration/test_tof_reading
pio test -e espA -f integration/test_homing_move_to_max
pio test -e espA -f integration/test_motor_direction
pio test -e espA -f integration/test_tof_motor_accuracy

To test ESPB without ESPA powered on:

pio test -e espB -f unit_hw/espb_bridge/test_parser
pio test -e espB -f unit_hw/espb_bridge/test_status_format
pio test -e espB -f unit_hw/espb_bridge/test_protocol_contract

To test the real ESPB-to-ESPA bridge, first upload the real espA firmware, wait until ESPA is idle, then run:

pio test -e espB -f integration/test_espnow_bridge

This test only uses the dummy command 0 and SWITCH_AUTO_MODE; it does not start profiles or move the motor.

Available tests:

TestCommandWhat it checks
test_max_stepspio test -e espA -f unit_hw/motor/test_max_stepsMoves only the motor to the safe maximum extension starting from logical position 0
test_speedpio test -e espA -f unit_hw/motor/test_speedMoves only the motor through 6 alternating 40 mm moves, increasing speed and acceleration up to 2300
test_screw_lead_20mmpio test -e espA -f integration/test_screw_lead_20mmRuns TOF homing, moves the motor by 20 mm, and compares the internal TOF delta
test_motor_directionpio test -e espA -f integration/test_motor_directionMoves only the motor forward/backward and verifies the logical direction; by default it does not use TOF
test_tof_readingpio test -e espA -f integration/test_tof_readingInitializes only the TOF sensor and checks valid readings for about 30 s
test_homing_onlypio test -e espA -f integration/test_homing_onlyRuns only TOF-based homing
test_homing_move_to_maxpio test -e espA -f integration/test_homing_move_to_maxRuns TOF homing and then moves to the safe maximum extension
test_tof_motor_accuracypio test -e espA -f integration/test_tof_motor_accuracyCompares TOF distance and motor position after homing
test_parserpio test -e espB -f unit_hw/espb_bridge/test_parserVerifies GUI/Serial command parsing into ESPA packets without ESPA powered on
test_status_formatpio test -e espB -f unit_hw/espb_bridge/test_status_formatVerifies ESPB cached state and the five-field STATUS format
test_protocol_contractpio test -e espB -f unit_hw/espb_bridge/test_protocol_contractLocks the command/ACK consistency contract between GUI, ESPB, and ESPA
test_espnow_bridgepio test -e espB -f integration/test_espnow_bridgeVerifies real ESP-NOW with the real ESPA firmware powered on, without movement

The test_max_steps, test_speed, and test_motor_direction tests are useful when you need to move only the motor without TOF homing. Before running them, make sure the piston is away from the mechanical end stops and can move in both directions.

Manual Motor-Only Control

To upload the bench firmware that lets you move the motor from the serial keyboard:

pio run -e espA_manual_keyboard -t upload
pio device monitor -e espA_manual_keyboard

Commands in the serial monitor:

KeyAction
Up arrow or wHold to move toward home/up
Down arrow or sHold to move toward extension/down
Space or xStop immediately and disable motor outputs
pPrint the current position
tPrint one TOF reading
h or ?Print help

This firmware does not run homing: at startup it assigns a centered logical position and moves while it receives repeated keypresses; when the key is released it stops automatically after a short timeout. During movement it periodically prints motor position and TOF distance. Use it only when the mechanism is in a physically safe position.

Test Layout

Hardware-oriented tests are stored under test/:

  • test/unit_hw/motor/test_max_steps checks safe maximum extension from a known zero
  • test/unit_hw/motor/test_speed checks alternating 40 mm moves while speed and acceleration increase up to 2300
  • test/integration/test_screw_lead_20mm checks the configured screw pitch, starts, and lead with one 20 mm move measured internally by TOF
  • test/integration/test_tof_reading checks that the TOF sensor initializes and returns valid distance samples for about 30 seconds
  • test/integration/test_homing_only checks TOF-based homing
  • test/integration/test_homing_move_to_max checks homing followed by safe full extension
  • test/integration/test_motor_direction checks logical/physical motion direction, optionally using TOF
  • test/integration/test_tof_motor_accuracy checks TOF and motor movement consistency

Continuous Integration

GitHub Actions builds all three PlatformIO environments (espA, espB, espA_pool) on every push to any branch and on every pull request to master. Workflow file: .github/workflows/ci.yml.

CI does not run the unit_hw/ or integration/ PlatformIO tests because they need a real ESP32 with the float wired up. Run those locally on the bench.

Pushing a v* tag triggers .github/workflows/release.yml, which builds all three environments and attaches the resulting firmware.bin / firmware.elf to a GitHub Release auto-named after the tag.

See CONTRIBUTING.md for the full git workflow (trunk-based with PR review on master), commit conventions, and one-time branch protection setup.


UTILITIES AND RESOURCES

Arduino Library Repositories:

Development Tools:

  • PlatformIO IDE: Modern embedded development platform
  • ESP32 Arduino Core: Framework for ESP32 development
  • FastAccelStepper Library: Timer/task-driven stepper motor control
  • VL53L7CX Library: Multi-zone Time-of-Flight sensor driver

GLOSSARY

  • AM (Auto Mode): Autonomous operation mode that triggers profiles on connection loss
  • CS (Control Station): Ground-based computer running the GUI application
  • ESPA: ESP32 mounted on the Float board (primary controller)
  • ESPB: ESP32 communication bridge between Float and CS
  • Flash Profile Log: Current onboard LittleFS CSV storage used before JSON transmission
  • FastAccelStepper: Timer/task-driven stepper library used by MotorController
  • MotionController: Firmware layer that combines motor, TOF, LEDs, debug, timeouts, and emergency stops for safe movement routines
  • Commit a command: To accept a sent command. After commit, command execution and success is ideally granted
  • Complete a command: To execute all the requirements requested by a command
  • Profile: A complete mission cycle (descent → depth control → ascent → data transmission)
  • TOF (Time-of-Flight): Non-contact distance measurement technology using light pulses
  • Homing: Process of establishing the motor's zero reference position
  • PID Control: Proportional-Integral-Derivative controller for precise depth maintenance
  • ESP-NOW: Low-latency peer-to-peer WiFi communication protocol by Espressif

Documentation Version: 11.2.0 Last Updated: May 2026

Recent changes:

  • PID output normalized to u ∈ [0, 1] (fraction of syringe travel). Default gains Kp = 0.17, Kd = 0.13, expressed per metre of depth error so they stay valid if MOTOR_MAX_STEPS changes.
  • Motor geometry: home = piston fully inserted (empty syringes, floats); full extension = piston extracted (full syringes, sinks). The mapping uToMotorPos() in include/config.h encapsulates MOTOR_INVERT_LOGICAL so motion code never hard-codes signs.
  • TOF safety range widened to [40, 85] mm to give 10 mm of margin above the homing threshold without risking the mechanical end stop.
  • balance now refuses to start without a prior homing (was forcing pos = 0 as a fallback, mechanically risky).
  • New SURFACE_TARGET_OFFSET_M constant and SURFACE_OFFSET <m> command (number 18) for tuning the surface idle position at runtime. Team Contact: PoliTOcean @ Politecnico di Torino Maintainers: Colabella Davide, Benevenga Filippo

About

Firmware for the float project

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages