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