# pH Sensor

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

### Hardware Specifications

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

</th><th>Value

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## Hardware Status

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

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

## Calibration Model

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

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

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

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

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

## Data Structure

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

## Initialization &amp; Usage

### Initialize pH Sensor

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

### Poll pH Sensor

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

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

### Manual Sample Addition

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

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

## Validation

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

## Sample Averaging Strategy

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

</th><th>Value

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

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

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

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

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

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

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

### Averaging Algorithm

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

## Two-Point Calibration Procedure

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

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

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

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

## Protobuf Message Format

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

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

## Error Handling (as in main.c)

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

## Integration Notes

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