/ firmware / src / sensors / voltage.cpp
voltage.cpp
  1  #include "voltage.h"
  2  #include "registry.h"
  3  #include <config.h>
  4  #include <i2c.h>
  5  
  6  #include <Adafruit_ADS1X15.h>
  7  #include <Arduino.h>
  8  
  9  static bool ready = false;
 10  static config::I2CSensorConfig resolved_config = {
 11    config::I2CSensorKind::VoltageADS1115,
 12    0,
 13    0,
 14    config::i2c::DIRECT_CHANNEL,
 15  };
 16  static int8_t resolved_mux_channel = config::i2c::DIRECT_CHANNEL;
 17  
 18  static Adafruit_ADS1115 adc;
 19  
 20  namespace {
 21  
 22  bool access_voltage_descriptor(config::I2CSensorConfig *sensor_config) {
 23    if (!sensor_config) return false;
 24    for (size_t index = 0; index < config::i2c_topology::DEVICE_COUNT; index++) {
 25      const config::I2CSensorConfig &candidate = config::i2c_topology::DEVICES[index];
 26      if (candidate.kind == config::I2CSensorKind::VoltageADS1115) {
 27        *sensor_config = candidate;
 28        return true;
 29      }
 30    }
 31    return false;
 32  }
 33  
 34  bool probe_device(const config::I2CSensorConfig &sensor_config, int8_t mux_channel) {
 35    hardware::i2c::DeviceAccessCommand command = {
 36      .bus = sensor_config.bus == 0 ? hardware::i2c::Bus::Bus0 : hardware::i2c::Bus::Bus1,
 37      .mux_channel = mux_channel,
 38      .wire = nullptr,
 39      .ok = false,
 40    };
 41    if (!hardware::i2c::accessDevice(&command)) return false;
 42  
 43    bool ok = adc.begin(sensor_config.address, command.wire);
 44    hardware::i2c::clearSelection();
 45    return ok;
 46  }
 47  
 48  void apply_selection(void) {
 49    if (resolved_mux_channel >= 0) {
 50      hardware::i2c::DeviceAccessCommand command = {
 51        .bus = resolved_config.bus == 0 ? hardware::i2c::Bus::Bus0 : hardware::i2c::Bus::Bus1,
 52        .mux_channel = resolved_mux_channel,
 53        .wire = nullptr,
 54        .ok = false,
 55      };
 56      hardware::i2c::accessDevice(&command);
 57    }
 58  }
 59  
 60  }
 61  
 62  bool sensors::voltage::initialize() {
 63    ready = false;
 64    resolved_config = {
 65      config::I2CSensorKind::VoltageADS1115,
 66      0,
 67      0,
 68      config::i2c::DIRECT_CHANNEL,
 69    };
 70    resolved_mux_channel = config::i2c::DIRECT_CHANNEL;
 71  
 72    config::I2CSensorConfig sensor_config = {};
 73    if (!access_voltage_descriptor(&sensor_config)) {
 74      return false;
 75    }
 76  
 77    hardware::i2c::TopologySnapshot topology = {};
 78    hardware::i2c::accessTopology(&topology);
 79  
 80    if (sensor_config.mux_channel == config::i2c::DIRECT_CHANNEL) {
 81      ready = probe_device(sensor_config, config::i2c::DIRECT_CHANNEL);
 82    } else if (sensor_config.mux_channel == config::i2c::ANY_MUX_CHANNEL) {
 83      if (topology.mux_present && sensor_config.bus == 1) {
 84        uint8_t channel_mask = hardware::i2c::mux.find(sensor_config.address);
 85        if (channel_mask != 0) {
 86          for (uint8_t channel = 0; channel < hardware::i2c::mux.channelCount(); channel++) {
 87            if (channel_mask & (1 << channel)) {
 88              resolved_mux_channel = (int8_t)channel;
 89              ready = probe_device(sensor_config, resolved_mux_channel);
 90              if (ready) {
 91                Serial.printf("[voltage] found at 0x%02X on mux channel %d\n",
 92                              sensor_config.address, channel);
 93                break;
 94              }
 95            }
 96          }
 97        }
 98      }
 99    } else {
100      if (topology.mux_present && sensor_config.bus == 1) {
101        resolved_mux_channel = sensor_config.mux_channel;
102        ready = probe_device(sensor_config, resolved_mux_channel);
103      }
104    }
105  
106    if (ready) {
107      resolved_config.kind = sensor_config.kind;
108      resolved_config.bus = sensor_config.bus;
109      resolved_config.address = sensor_config.address;
110      resolved_config.mux_channel = sensor_config.mux_channel;
111      adc.setGain(GAIN_TWO);
112  
113      sensors::registry::add({
114          .kind = SensorKind::Voltage,
115          .name = "Voltage",
116          .isAvailable = sensors::voltage::isAvailable,
117          .instanceCount = []() -> uint8_t { return 1; },
118          .poll = [](uint8_t, void *out, size_t cap) -> bool {
119              if (cap < sizeof(VoltageSensorData)) return false;
120              return sensors::voltage::access(static_cast<VoltageSensorData *>(out));
121          },
122          .data_size = sizeof(VoltageSensorData),
123      });
124    }
125  
126    hardware::i2c::clearSelection();
127    return ready;
128  }
129  
130  bool sensors::voltage::isAvailable() {
131    return ready;
132  }
133  
134  const char *sensors::voltage::accessGainLabel() {
135    if (!ready) return "NOT_READY";
136  
137    switch (adc.getGain()) {
138      case GAIN_TWOTHIRDS: return "GAIN_TWOTHIRDS";
139      case GAIN_ONE:       return "GAIN_ONE";
140      case GAIN_TWO:       return "GAIN_TWO";
141      case GAIN_FOUR:      return "GAIN_FOUR";
142      case GAIN_EIGHT:     return "GAIN_EIGHT";
143      case GAIN_SIXTEEN:   return "GAIN_SIXTEEN";
144      default:             return "GAIN_UNKNOWN";
145    }
146  }
147  
148  bool sensors::voltage::access(VoltageSensorData *sensor_data) {
149    if (!ready) return false;
150    if (!sensor_data) return false;
151  
152    apply_selection();
153  
154    for (size_t channel = 0; channel < config::voltage::CHANNEL_COUNT;
155         channel++) {
156      int16_t raw_counts = adc.readADC_SingleEnded(channel);
157      float voltage = adc.computeVolts(raw_counts);
158  
159      bool is_unipolar = (channel == 0) ||
160                         (channel == config::voltage::CHANNEL_COUNT - 1);
161      if (is_unipolar && voltage < 0.0f) {
162        voltage = 0.0f;
163      }
164  
165      sensor_data->channel_volts[channel] = voltage;
166    }
167  
168    hardware::i2c::clearSelection();
169    return true;
170  }
171  
172  #ifdef PIO_UNIT_TESTING
173  
174  #include <testing/utils.h>
175  
176  static void test_voltage_initializes(void) {
177    GIVEN("Wire1 with power enabled");
178    WHEN("the ADS1115 is initialized");
179    test_ensure_wire1_with_power();
180    hardware::i2c::initialize();
181  
182    if (!sensors::voltage::initialize()) {
183      TEST_IGNORE_MESSAGE("voltage::initialize() failed — skipping");
184      return;
185    }
186  }
187  
188  static void test_voltage_reads_channels(void) {
189    GIVEN("an initialized ADS1115");
190    WHEN("all 4 channels are read");
191  
192    if (!sensors::voltage::isAvailable()) {
193      TEST_IGNORE_MESSAGE("ADS1115 not available — skipping");
194      return;
195    }
196  
197    VoltageSensorData sensor_data = {};
198    bool success = sensors::voltage::access(&sensor_data);
199    TEST_ASSERT_TRUE_MESSAGE(success, "device: voltage::access() failed");
200  
201    for (size_t channel = 0; channel < config::voltage::CHANNEL_COUNT;
202         channel++) {
203      char message[64];
204      snprintf(message, sizeof(message), "channel %zu: %.4f V", channel,
205               sensor_data.channel_volts[channel]);
206      TEST_MESSAGE(message);
207  
208      TEST_ASSERT_FLOAT_IS_DETERMINATE_MESSAGE(sensor_data.channel_volts[channel],
209        "device: channel voltage must be a finite number (not NaN or infinity)");
210    }
211  }
212  
213  static void test_voltage_gain_label(void) {
214    GIVEN("an initialized ADS1115");
215    WHEN("the gain label is queried");
216  
217    if (!sensors::voltage::isAvailable()) {
218      TEST_IGNORE_MESSAGE("ADS1115 not available — skipping");
219      return;
220    }
221  
222    const char *label = sensors::voltage::accessGainLabel();
223    TEST_MESSAGE(label);
224  
225    TEST_ASSERT_NOT_EMPTY_MESSAGE(label,
226      "device: gain label should not be empty");
227    TEST_ASSERT_EQUAL_STRING_MESSAGE("GAIN_TWO", label,
228      "device: default gain should be GAIN_TWO");
229  }
230  
231  static void test_voltage_rejects_null_buffer(void) {
232    WHEN("a null buffer is passed to access");
233  
234    bool success = sensors::voltage::access(nullptr);
235    TEST_ASSERT_FALSE_MESSAGE(success,
236      "device: read should fail when sensor_data is null");
237  }
238  
239  void sensors::voltage::test() {
240    MODULE("Voltage");
241    RUN_TEST(test_voltage_initializes);
242    RUN_TEST(test_voltage_reads_channels);
243    RUN_TEST(test_voltage_gain_label);
244    RUN_TEST(test_voltage_rejects_null_buffer);
245  }
246  
247  #endif