#include "echem.h"
#include "ad5940.h"
#include "ble.h"
#include <math.h>
#include <string.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

/* LP RTIA register mapping */
const uint32_t lp_rtia_map[] = {
    [LP_RTIA_200]  = LPTIARTIA_200R,
    [LP_RTIA_1K]   = LPTIARTIA_1K,
    [LP_RTIA_2K]   = LPTIARTIA_2K,
    [LP_RTIA_3K]   = LPTIARTIA_3K,
    [LP_RTIA_4K]   = LPTIARTIA_4K,
    [LP_RTIA_6K]   = LPTIARTIA_6K,
    [LP_RTIA_8K]   = LPTIARTIA_8K,
    [LP_RTIA_10K]  = LPTIARTIA_10K,
    [LP_RTIA_12K]  = LPTIARTIA_12K,
    [LP_RTIA_16K]  = LPTIARTIA_16K,
    [LP_RTIA_20K]  = LPTIARTIA_20K,
    [LP_RTIA_24K]  = LPTIARTIA_24K,
    [LP_RTIA_30K]  = LPTIARTIA_30K,
    [LP_RTIA_32K]  = LPTIARTIA_32K,
    [LP_RTIA_40K]  = LPTIARTIA_40K,
    [LP_RTIA_48K]  = LPTIARTIA_48K,
    [LP_RTIA_64K]  = LPTIARTIA_64K,
    [LP_RTIA_85K]  = LPTIARTIA_85K,
    [LP_RTIA_96K]  = LPTIARTIA_96K,
    [LP_RTIA_100K] = LPTIARTIA_100K,
    [LP_RTIA_120K] = LPTIARTIA_120K,
    [LP_RTIA_128K] = LPTIARTIA_128K,
    [LP_RTIA_160K] = LPTIARTIA_160K,
    [LP_RTIA_196K] = LPTIARTIA_196K,
    [LP_RTIA_256K] = LPTIARTIA_256K,
    [LP_RTIA_512K] = LPTIARTIA_512K,
};

/* LP RTIA ohms for current conversion */
const float lp_rtia_ohms[] = {
    [LP_RTIA_200]  = 200.0f,
    [LP_RTIA_1K]   = 1000.0f,
    [LP_RTIA_2K]   = 2000.0f,
    [LP_RTIA_3K]   = 3000.0f,
    [LP_RTIA_4K]   = 4000.0f,
    [LP_RTIA_6K]   = 6000.0f,
    [LP_RTIA_8K]   = 8000.0f,
    [LP_RTIA_10K]  = 10000.0f,
    [LP_RTIA_12K]  = 12000.0f,
    [LP_RTIA_16K]  = 16000.0f,
    [LP_RTIA_20K]  = 20000.0f,
    [LP_RTIA_24K]  = 24000.0f,
    [LP_RTIA_30K]  = 30000.0f,
    [LP_RTIA_32K]  = 32000.0f,
    [LP_RTIA_40K]  = 40000.0f,
    [LP_RTIA_48K]  = 48000.0f,
    [LP_RTIA_64K]  = 64000.0f,
    [LP_RTIA_85K]  = 85000.0f,
    [LP_RTIA_96K]  = 96000.0f,
    [LP_RTIA_100K] = 100000.0f,
    [LP_RTIA_120K] = 120000.0f,
    [LP_RTIA_128K] = 128000.0f,
    [LP_RTIA_160K] = 160000.0f,
    [LP_RTIA_196K] = 196000.0f,
    [LP_RTIA_256K] = 256000.0f,
    [LP_RTIA_512K] = 512000.0f,
};

/*
 * LPDAC math (2.5V reference):
 *   6-bit DAC  → VZERO: 200mV + code * 34.375mV  (code 0-63)
 *   12-bit DAC → VBIAS: 200mV + code * 0.537mV    (code 0-4095)
 *   Cell potential: V_cell = VZERO - VBIAS
 *
 * VZERO fixed at ~1100mV (code 26).
 * VBIAS swept to set cell potential.
 */
#define VZERO_CODE   26
#define VZERO_MV     (200.0f + VZERO_CODE * 34.375f)   /* ~1093.75 mV */
#define VBIAS_OFFSET 200.0f
#define VBIAS_LSB    0.537f

static uint16_t mv_to_vbias_code(float v_cell_mv)
{
    /* V_cell = VZERO - VBIAS → VBIAS = VZERO - V_cell */
    float vbias_mv = VZERO_MV - v_cell_mv;
    float code = (vbias_mv - VBIAS_OFFSET) / VBIAS_LSB;
    if (code < 0) code = 0;
    if (code > 4095) code = 4095;
    return (uint16_t)(code + 0.5f);
}

static void echem_init_lp(uint32_t rtia_reg)
{
    CLKCfg_Type clk;
    memset(&clk, 0, sizeof(clk));
    clk.HFOSCEn        = bTRUE;
    clk.HfOSC32MHzMode = bFALSE;
    clk.SysClkSrc       = SYSCLKSRC_HFOSC;
    clk.ADCCLkSrc       = ADCCLKSRC_HFOSC;
    clk.SysClkDiv       = SYSCLKDIV_1;
    clk.ADCClkDiv       = ADCCLKDIV_1;
    clk.LFOSCEn         = bTRUE;
    clk.HFXTALEn        = bFALSE;
    AD5940_CLKCfg(&clk);

    AFERefCfg_Type ref;
    AD5940_StructInit(&ref, sizeof(ref));
    ref.HpBandgapEn = bTRUE;
    ref.Hp1V1BuffEn = bTRUE;
    ref.Hp1V8BuffEn = bTRUE;
    ref.LpBandgapEn = bTRUE;
    ref.LpRefBufEn  = bTRUE;
    ref.LpRefBoostEn = bTRUE;
    AD5940_REFCfgS(&ref);

    AD5940_AFEPwrBW(AFEPWR_LP, AFEBW_250KHZ);

    LPLoopCfg_Type lp;
    AD5940_StructInit(&lp, sizeof(lp));

    lp.LpDacCfg.LpdacSel     = LPDAC0;
    lp.LpDacCfg.LpDacSrc     = LPDACSRC_MMR;
    lp.LpDacCfg.LpDacVzeroMux = LPDACVZERO_6BIT;
    lp.LpDacCfg.LpDacVbiasMux = LPDACVBIAS_12BIT;
    lp.LpDacCfg.LpDacSW      = LPDACSW_VZERO2LPTIA | LPDACSW_VBIAS2LPPA;
    lp.LpDacCfg.LpDacRef     = LPDACREF_2P5;
    lp.LpDacCfg.DataRst      = bFALSE;
    lp.LpDacCfg.PowerEn      = bTRUE;
    lp.LpDacCfg.DacData6Bit  = VZERO_CODE;
    lp.LpDacCfg.DacData12Bit = mv_to_vbias_code(0);

    lp.LpAmpCfg.LpAmpSel    = LPAMP0;
    lp.LpAmpCfg.LpAmpPwrMod = LPAMPPWR_BOOST3;
    lp.LpAmpCfg.LpPaPwrEn   = bTRUE;
    lp.LpAmpCfg.LpTiaPwrEn  = bTRUE;
    lp.LpAmpCfg.LpTiaRf     = LPTIARF_SHORT;
    lp.LpAmpCfg.LpTiaRload  = LPTIARLOAD_SHORT;
    lp.LpAmpCfg.LpTiaRtia   = rtia_reg;
    /* CE0=drive, RE0=ref, SE0=working: SW2,4,5,7,12 */
    lp.LpAmpCfg.LpTiaSW     = LPTIASW(2) | LPTIASW(4) | LPTIASW(5) |
                                LPTIASW(7) | LPTIASW(12);
    AD5940_LPLoopCfgS(&lp);

    /* ADC: SINC2+Notch on LPTIA output */
    ADCBaseCfg_Type adc;
    adc.ADCMuxP = ADCMUXP_LPTIA0_P;
    adc.ADCMuxN = ADCMUXN_LPTIA0_N;
    adc.ADCPga  = ADCPGA_1P5;
    AD5940_ADCBaseCfgS(&adc);

    ADCFilterCfg_Type filt;
    AD5940_StructInit(&filt, sizeof(filt));
    filt.ADCSinc3Osr         = ADCSINC3OSR_4;
    filt.ADCSinc2Osr         = ADCSINC2OSR_667;
    filt.ADCAvgNum           = ADCAVGNUM_16;
    filt.ADCRate             = ADCRATE_800KHZ;
    filt.BpNotch             = bFALSE;
    filt.BpSinc3             = bFALSE;
    filt.Sinc2NotchEnable    = bTRUE;
    filt.Sinc3ClkEnable      = bTRUE;
    filt.Sinc2NotchClkEnable = bTRUE;
    filt.DFTClkEnable        = bFALSE;
    filt.WGClkEnable         = bFALSE;
    AD5940_ADCFilterCfgS(&filt);

    AD5940_INTCCfg(AFEINTC_0, AFEINTSRC_SINC2RDY, bTRUE);
    AD5940_INTCCfg(AFEINTC_1, AFEINTSRC_SINC2RDY, bTRUE);
    AD5940_INTCClrFlag(AFEINTSRC_ALLINT);

    AGPIOCfg_Type gpio;
    AD5940_StructInit(&gpio, sizeof(gpio));
    gpio.FuncSet     = GP0_INT;
    gpio.OutputEnSet = AGPIO_Pin0;
    AD5940_AGPIOCfg(&gpio);

    AD5940_WriteReg(REG_AFE_FIFOCON, 0);
}

static float read_current_ua(float rtia_ohms)
{
    AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY);
    AD5940_AFECtrlS(AFECTRL_ADCPWR, bTRUE);
    AD5940_Delay10us(25);
    AD5940_AFECtrlS(AFECTRL_ADCCNV, bTRUE);

    AD5940_ClrMCUIntFlag();
    while (!AD5940_GetMCUIntFlag())
        vTaskDelay(1);

    AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_ADCPWR, bFALSE);
    AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY);

    uint32_t raw = AD5940_ReadAfeResult(AFERESULT_SINC2);
    int32_t code = (raw & (1UL << 15)) ? (int32_t)(raw | 0xFFFF0000UL) : (int32_t)raw;

    /* clamp near ADC saturation to prevent sign-flip wrap artifact */
    if (code > 32700) code = 32700;
    if (code < -32700) code = -32700;

    /* I = V_tia / RTIA, V_tia = code * Vref / (PGA * 32768) */
    float v_tia = (float)code * 1.82f / (1.5f * 32768.0f);
    float i_a = v_tia / rtia_ohms;
    return i_a * 1e6f; /* convert to uA */
}

static void echem_init_adc(void)
{
    CLKCfg_Type clk;
    memset(&clk, 0, sizeof(clk));
    clk.HFOSCEn        = bTRUE;
    clk.HfOSC32MHzMode = bFALSE;
    clk.SysClkSrc       = SYSCLKSRC_HFOSC;
    clk.ADCCLkSrc       = ADCCLKSRC_HFOSC;
    clk.SysClkDiv       = SYSCLKDIV_1;
    clk.ADCClkDiv       = ADCCLKDIV_1;
    clk.LFOSCEn         = bTRUE;
    clk.HFXTALEn        = bFALSE;
    AD5940_CLKCfg(&clk);

    AFERefCfg_Type ref;
    AD5940_StructInit(&ref, sizeof(ref));
    ref.HpBandgapEn = bTRUE;
    ref.Hp1V1BuffEn = bTRUE;
    ref.Hp1V8BuffEn = bTRUE;
    ref.LpBandgapEn = bTRUE;
    ref.LpRefBufEn  = bTRUE;
    ref.LpRefBoostEn = bTRUE;
    AD5940_REFCfgS(&ref);

    AD5940_AFEPwrBW(AFEPWR_LP, AFEBW_250KHZ);

    ADCFilterCfg_Type filt;
    AD5940_StructInit(&filt, sizeof(filt));
    filt.ADCSinc3Osr         = ADCSINC3OSR_4;
    filt.ADCSinc2Osr         = ADCSINC2OSR_667;
    filt.ADCAvgNum           = ADCAVGNUM_16;
    filt.ADCRate             = ADCRATE_800KHZ;
    filt.BpNotch             = bFALSE;
    filt.BpSinc3             = bFALSE;
    filt.Sinc2NotchEnable    = bTRUE;
    filt.Sinc3ClkEnable      = bTRUE;
    filt.Sinc2NotchClkEnable = bTRUE;
    filt.DFTClkEnable        = bFALSE;
    filt.WGClkEnable         = bFALSE;
    AD5940_ADCFilterCfgS(&filt);

    AD5940_INTCCfg(AFEINTC_0, AFEINTSRC_SINC2RDY, bTRUE);
    AD5940_INTCCfg(AFEINTC_1, AFEINTSRC_SINC2RDY, bTRUE);
    AD5940_INTCClrFlag(AFEINTSRC_ALLINT);

    AGPIOCfg_Type gpio;
    AD5940_StructInit(&gpio, sizeof(gpio));
    gpio.FuncSet     = GP0_INT;
    gpio.OutputEnSet = AGPIO_Pin0;
    AD5940_AGPIOCfg(&gpio);

    AD5940_WriteReg(REG_AFE_FIFOCON, 0);
}

static float read_voltage_mv(uint32_t muxp)
{
    AD5940_ADCMuxCfgS(muxp, ADCMUXN_VSET1P1);
    AD5940_Delay10us(50);

    AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY);
    AD5940_AFECtrlS(AFECTRL_ADCPWR, bTRUE);
    AD5940_Delay10us(25);
    AD5940_AFECtrlS(AFECTRL_ADCCNV, bTRUE);

    AD5940_ClrMCUIntFlag();
    while (!AD5940_GetMCUIntFlag())
        vTaskDelay(1);

    AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_ADCPWR, bFALSE);
    AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY);

    uint32_t raw = AD5940_ReadAfeResult(AFERESULT_SINC2);
    int32_t code = (raw & (1UL << 15)) ? (int32_t)(raw | 0xFFFF0000UL) : (int32_t)raw;

    /* V_diff = code * Vref / (PGA * 32768), PGA=1.5, Vref=1.82V */
    return (float)code * 1820.0f / (1.5f * 32768.0f);
}

static void echem_shutdown_lp(void)
{
    LPLoopCfg_Type lp;
    AD5940_StructInit(&lp, sizeof(lp));
    lp.LpDacCfg.LpdacSel     = LPDAC0;
    lp.LpDacCfg.LpDacSrc     = LPDACSRC_MMR;
    lp.LpDacCfg.LpDacVzeroMux = LPDACVZERO_6BIT;
    lp.LpDacCfg.LpDacVbiasMux = LPDACVBIAS_12BIT;
    lp.LpDacCfg.LpDacSW      = 0;
    lp.LpDacCfg.LpDacRef     = LPDACREF_2P5;
    lp.LpDacCfg.PowerEn      = bFALSE;
    lp.LpDacCfg.DataRst      = bFALSE;
    lp.LpDacCfg.DacData6Bit  = 0;
    lp.LpDacCfg.DacData12Bit = 0;
    lp.LpAmpCfg.LpAmpSel    = LPAMP0;
    lp.LpAmpCfg.LpAmpPwrMod = LPAMPPWR_NORM;
    lp.LpAmpCfg.LpPaPwrEn   = bFALSE;
    lp.LpAmpCfg.LpTiaPwrEn  = bFALSE;
    lp.LpAmpCfg.LpTiaRf     = LPTIARF_OPEN;
    lp.LpAmpCfg.LpTiaRload  = LPTIARLOAD_SHORT;
    lp.LpAmpCfg.LpTiaRtia   = LPTIARTIA_OPEN;
    lp.LpAmpCfg.LpTiaSW     = 0;
    AD5940_LPLoopCfgS(&lp);

    SWMatrixCfg_Type sw = { SWD_OPEN, SWP_OPEN, SWN_OPEN, SWT_OPEN };
    AD5940_SWMatrixCfgS(&sw);
}

/* ---- public ---- */

int echem_clean(float v_mv, float duration_s)
{
    echem_init_lp(LPTIARTIA_200R);

    uint16_t code = mv_to_vbias_code(v_mv);
    AD5940_LPDAC0WriteS(code, VZERO_CODE);

    printf("Clean: %.0f mV for %.0f s\n", v_mv, duration_s);
    vTaskDelay(pdMS_TO_TICKS((uint32_t)(duration_s * 1000.0f)));

    echem_shutdown_lp();
    AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
    printf("Clean: done\n");
    return 0;
}

void echem_default_lsv(LSVConfig *cfg)
{
    memset(cfg, 0, sizeof(*cfg));
    cfg->v_start   = 0.0f;
    cfg->v_stop    = 500.0f;
    cfg->scan_rate = 50.0f;
    cfg->lp_rtia   = LP_RTIA_10K;
}

void echem_default_amp(AmpConfig *cfg)
{
    memset(cfg, 0, sizeof(*cfg));
    cfg->v_hold      = 200.0f;
    cfg->interval_ms = 100.0f;
    cfg->duration_s  = 60.0f;
    cfg->lp_rtia     = LP_RTIA_10K;
}

static void lsv_calc_step(const LSVConfig *cfg, uint32_t max_points,
                          uint32_t *n_out, float *step_out)
{
    float v_range = cfg->v_stop - cfg->v_start;
    uint32_t n_lsb = (uint32_t)(fabsf(v_range / VBIAS_LSB) + 0.5f);
    uint32_t n_steps = n_lsb;
    uint32_t step_mult = 1;
    if (n_steps > max_points) {
        step_mult = (n_lsb + max_points - 1) / max_points;
        n_steps = (n_lsb + step_mult - 1) / step_mult;
    }
    if (n_steps < 2) n_steps = 2;
    *n_out = n_steps;
    *step_out = (v_range > 0) ? VBIAS_LSB * step_mult : -VBIAS_LSB * step_mult;
}

uint32_t echem_lsv_calc_steps(const LSVConfig *cfg, uint32_t max_points)
{
    float v_range = cfg->v_stop - cfg->v_start;
    if (fabsf(v_range) < 0.001f) return 0;
    uint32_t n;
    float step;
    lsv_calc_step(cfg, max_points, &n, &step);
    return n;
}

int echem_lsv(const LSVConfig *cfg, LSVPoint *out, uint32_t max_points, lsv_point_cb_t cb)
{
    if (cfg->lp_rtia >= LP_RTIA_COUNT) return 0;
    float rtia = lp_rtia_ohms[cfg->lp_rtia];

    echem_init_lp(lp_rtia_map[cfg->lp_rtia]);

    /* set starting voltage and flush SINC2 filter */
    AD5940_LPDAC0WriteS(mv_to_vbias_code(cfg->v_start), VZERO_CODE);
    vTaskDelay(pdMS_TO_TICKS(50));
    for (int i = 0; i < 4; i++)
        read_current_ua(rtia);

    float v_range = cfg->v_stop - cfg->v_start;
    if (fabsf(v_range) < 0.001f) return 0;

    uint32_t n_steps;
    float step;
    lsv_calc_step(cfg, max_points, &n_steps, &step);

    float delay_ms = fabsf(step / cfg->scan_rate) * 1000.0f;
    if (delay_ms < 1.0f) delay_ms = 1.0f;
    TickType_t ticks = pdMS_TO_TICKS((uint32_t)delay_ms);
    if (ticks < 1) ticks = 1;

    printf("\n%10s  %10s\n", "V(mV)", "I(uA)");
    printf("------------------------\n");

    for (uint32_t i = 0; i < n_steps; i++) {
        float v_mv = cfg->v_start + i * step;
        uint16_t code = mv_to_vbias_code(v_mv);
        AD5940_LPDAC0WriteS(code, VZERO_CODE);
        vTaskDelay(ticks);

        float i_ua = read_current_ua(rtia);
        out[i].v_mv = v_mv;
        out[i].i_ua = i_ua;

        printf("%10.1f  %10.3f\n", v_mv, i_ua);
        if (cb) cb((uint16_t)i, v_mv, i_ua);
    }

    echem_shutdown_lp();
    AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
    return (int)n_steps;
}

int echem_amp(const AmpConfig *cfg, AmpPoint *out, uint32_t max_points, amp_point_cb_t cb)
{
    if (cfg->lp_rtia >= LP_RTIA_COUNT) return 0;
    float rtia = lp_rtia_ohms[cfg->lp_rtia];

    echem_init_lp(lp_rtia_map[cfg->lp_rtia]);

    uint16_t code = mv_to_vbias_code(cfg->v_hold);
    AD5940_LPDAC0WriteS(code, VZERO_CODE);
    vTaskDelay(pdMS_TO_TICKS(50));
    for (int i = 0; i < 4; i++)
        read_current_ua(rtia);

    TickType_t interval = pdMS_TO_TICKS((uint32_t)cfg->interval_ms);
    if (interval < 1) interval = 1;

    uint32_t max_samples = max_points;
    if (cfg->duration_s > 0) {
        uint32_t duration_n = (uint32_t)(cfg->duration_s * 1000.0f / cfg->interval_ms + 0.5f);
        if (duration_n < max_samples) max_samples = duration_n;
    }

    printf("\n%10s  %10s\n", "t(ms)", "I(uA)");
    printf("------------------------\n");

    TickType_t t0 = xTaskGetTickCount();
    uint32_t count = 0;

    for (uint32_t i = 0; i < max_samples; i++) {
        BleCommand cmd;
        if (ble_recv_command(&cmd, 0) == 0 && cmd.type == CMD_STOP_AMP)
            break;

        float i_ua = read_current_ua(rtia);
        float t_ms = (float)(xTaskGetTickCount() - t0) * portTICK_PERIOD_MS;

        out[i].t_ms = t_ms;
        out[i].i_ua = i_ua;
        count++;

        printf("%10.1f  %10.3f\n", t_ms, i_ua);
        if (cb) cb((uint16_t)i, t_ms, i_ua);

        vTaskDelay(interval);
    }

    echem_shutdown_lp();
    AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
    return (int)count;
}

void echem_default_cl(ClConfig *cfg)
{
    memset(cfg, 0, sizeof(*cfg));
    cfg->v_cond    = 800.0f;    /* +800 mV conditioning pulse */
    cfg->t_cond_ms = 2000.0f;
    cfg->v_free    = 100.0f;    /* +100 mV for HOCl reduction */
    cfg->v_total   = -200.0f;   /* -200 mV for total chlorine */
    cfg->t_dep_ms  = 5000.0f;   /* 5s settling */
    cfg->t_meas_ms = 5000.0f;   /* 5s sampling */
    cfg->lp_rtia   = LP_RTIA_10K;
}

static uint32_t sample_phase(float v_mv, float t_dep_ms, float t_meas_ms,
                              uint8_t phase, float rtia_ohms,
                              ClPoint *out, uint32_t idx, uint32_t max_points,
                              TickType_t t0, float *avg_out, cl_point_cb_t cb)
{
    AD5940_LPDAC0WriteS(mv_to_vbias_code(v_mv), VZERO_CODE);

    /* settling — no samples recorded */
    vTaskDelay(pdMS_TO_TICKS((uint32_t)t_dep_ms));

    /* measurement — sample at ~50ms intervals */
    uint32_t n_samples = (uint32_t)(t_meas_ms / 50.0f + 0.5f);
    if (n_samples < 2) n_samples = 2;
    TickType_t interval = pdMS_TO_TICKS(50);

    float sum = 0;
    uint32_t count = 0;

    for (uint32_t i = 0; i < n_samples && idx < max_points; i++) {
        float i_ua = read_current_ua(rtia_ohms);
        float t_ms = (float)(xTaskGetTickCount() - t0) * portTICK_PERIOD_MS;

        out[idx].t_ms  = t_ms;
        out[idx].i_ua  = i_ua;
        out[idx].phase = phase;

        if (cb) cb((uint16_t)idx, t_ms, i_ua, phase);
        idx++;

        sum += i_ua;
        count++;

        vTaskDelay(interval);
    }

    *avg_out = (count > 0) ? sum / (float)count : 0.0f;
    return idx;
}

int echem_chlorine(const ClConfig *cfg, ClPoint *out, uint32_t max_points,
                   ClResult *result, cl_point_cb_t cb)
{
    if (cfg->lp_rtia >= LP_RTIA_COUNT) return 0;
    float rtia = lp_rtia_ohms[cfg->lp_rtia];

    echem_init_lp(lp_rtia_map[cfg->lp_rtia]);

    TickType_t t0 = xTaskGetTickCount();
    uint32_t idx = 0;

    printf("Cl: conditioning at %.0f mV for %.0f ms\n", cfg->v_cond, cfg->t_cond_ms);
    AD5940_LPDAC0WriteS(mv_to_vbias_code(cfg->v_cond), VZERO_CODE);
    vTaskDelay(pdMS_TO_TICKS((uint32_t)cfg->t_cond_ms));

    printf("Cl: free chlorine at %.0f mV\n", cfg->v_free);
    idx = sample_phase(cfg->v_free, cfg->t_dep_ms, cfg->t_meas_ms,
                        CL_PHASE_FREE, rtia, out, idx, max_points, t0,
                        &result->i_free_ua, cb);

    printf("Cl: total chlorine at %.0f mV\n", cfg->v_total);
    idx = sample_phase(cfg->v_total, cfg->t_dep_ms, cfg->t_meas_ms,
                        CL_PHASE_TOTAL, rtia, out, idx, max_points, t0,
                        &result->i_total_ua, cb);

    printf("Cl: free=%.3f uA, total=%.3f uA\n", result->i_free_ua, result->i_total_ua);

    echem_shutdown_lp();
    AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
    return (int)idx;
}

void echem_default_ph(PhConfig *cfg)
{
    memset(cfg, 0, sizeof(*cfg));
    cfg->stabilize_s = 30.0f;
    cfg->temp_c      = 25.0f;
}

int echem_ph_ocp(const PhConfig *cfg, PhResult *result)
{
    echem_init_adc();

    /* ADC mux: read SE0 and RE0 pin voltages directly */
    ADCBaseCfg_Type adc;
    adc.ADCMuxP = ADCMUXP_VSE0;
    adc.ADCMuxN = ADCMUXN_VSET1P1;
    adc.ADCPga  = ADCPGA_1P5;
    AD5940_ADCBaseCfgS(&adc);

    printf("pH: stabilizing %0.f s\n", cfg->stabilize_s);
    vTaskDelay(pdMS_TO_TICKS((uint32_t)(cfg->stabilize_s * 1000.0f)));

    /* average N readings of V(SE0) and V(RE0) */
    #define PH_AVG_N 10
    float sum_se0 = 0, sum_re0 = 0;
    for (int i = 0; i < PH_AVG_N; i++) {
        sum_se0 += read_voltage_mv(ADCMUXP_VSE0);
        sum_re0 += read_voltage_mv(ADCMUXP_VRE0);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
    float v_se0 = sum_se0 / PH_AVG_N;
    float v_re0 = sum_re0 / PH_AVG_N;
    float ocp = v_se0 - v_re0;

    float t_k = cfg->temp_c + 273.15f;
    float slope = 0.1984f * t_k;  /* mV/pH at temperature */

    result->v_ocp_mv = ocp;
    result->ph       = 7.0f - ocp / slope;
    result->temp_c   = cfg->temp_c;

    printf("pH: SE0=%.1f mV, RE0=%.1f mV, OCP=%.1f mV, pH=%.2f\n",
           v_se0, v_re0, ocp, result->ph);

    echem_shutdown_lp();
    AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
    return 0;
}
