current.cpp
1 #include "current.h" 2 #include "registry.h" 3 4 #include <config.h> 5 #include <i2c.h> 6 7 #include <Arduino.h> 8 #include <Adafruit_INA228.h> 9 10 namespace { 11 12 Adafruit_INA228 ina228; 13 bool ready = false; 14 15 config::I2CSensorConfig resolved_config = {}; 16 int8_t resolved_mux_channel = config::i2c::DIRECT_CHANNEL; 17 18 bool probe_device(const config::I2CSensorConfig &sensor_config, int8_t mux_channel) { 19 hardware::i2c::DeviceAccessCommand command = { 20 .bus = sensor_config.bus == 0 ? hardware::i2c::Bus::Bus0 : hardware::i2c::Bus::Bus1, 21 .mux_channel = mux_channel, 22 .wire = nullptr, 23 .ok = false, 24 }; 25 if (!hardware::i2c::accessDevice(&command)) return false; 26 27 bool ok = ina228.begin(sensor_config.address, command.wire); 28 hardware::i2c::clearSelection(); 29 return ok; 30 } 31 32 void apply_selection(void) { 33 if (resolved_mux_channel >= 0) { 34 hardware::i2c::DeviceAccessCommand command = { 35 .bus = resolved_config.bus == 0 ? hardware::i2c::Bus::Bus0 : hardware::i2c::Bus::Bus1, 36 .mux_channel = resolved_mux_channel, 37 .wire = nullptr, 38 .ok = false, 39 }; 40 hardware::i2c::accessDevice(&command); 41 } 42 } 43 44 } 45 46 bool sensors::current::initialize() { 47 ready = false; 48 resolved_mux_channel = config::i2c::DIRECT_CHANNEL; 49 50 config::I2CSensorConfig sensor_config = {}; 51 bool found = false; 52 for (size_t index = 0; index < config::i2c_topology::DEVICE_COUNT; index++) { 53 const config::I2CSensorConfig &candidate = config::i2c_topology::DEVICES[index]; 54 if (candidate.kind == config::I2CSensorKind::CurrentINA228) { 55 sensor_config = candidate; 56 found = true; 57 break; 58 } 59 } 60 if (!found) return false; 61 62 hardware::i2c::TopologySnapshot topology = {}; 63 hardware::i2c::accessTopology(&topology); 64 65 if (sensor_config.mux_channel == config::i2c::DIRECT_CHANNEL) { 66 ready = probe_device(sensor_config, config::i2c::DIRECT_CHANNEL); 67 } else if (sensor_config.mux_channel == config::i2c::ANY_MUX_CHANNEL) { 68 if (topology.mux_present && sensor_config.bus == 1) { 69 uint8_t channel_mask = hardware::i2c::mux.find(sensor_config.address); 70 if (channel_mask != 0) { 71 for (uint8_t channel = 0; channel < hardware::i2c::mux.channelCount(); channel++) { 72 if (channel_mask & (1 << channel)) { 73 resolved_mux_channel = (int8_t)channel; 74 ready = probe_device(sensor_config, resolved_mux_channel); 75 if (ready) break; 76 } 77 } 78 } 79 } 80 } else { 81 if (topology.mux_present && sensor_config.bus == 1) { 82 resolved_mux_channel = sensor_config.mux_channel; 83 ready = probe_device(sensor_config, resolved_mux_channel); 84 } 85 } 86 87 if (ready) { 88 resolved_config = sensor_config; 89 ina228.setShunt(config::current::SHUNT_RESISTANCE_OHMS, 90 config::current::MAX_EXPECTED_CURRENT_A); 91 Serial.printf("[current] INA228 at 0x%02X on bus %d\n", 92 sensor_config.address, sensor_config.bus); 93 94 sensors::registry::add({ 95 .kind = SensorKind::Current, 96 .name = "Current", 97 .isAvailable = sensors::current::isAvailable, 98 .instanceCount = []() -> uint8_t { return 1; }, 99 .poll = [](uint8_t, void *out, size_t cap) -> bool { 100 if (cap < sizeof(CurrentSensorData)) return false; 101 return sensors::current::access(static_cast<CurrentSensorData *>(out)); 102 }, 103 .data_size = sizeof(CurrentSensorData), 104 }); 105 } 106 107 hardware::i2c::clearSelection(); 108 return ready; 109 } 110 111 bool sensors::current::isAvailable() { 112 return ready; 113 } 114 115 bool sensors::current::access(CurrentSensorData *sensor_data) { 116 if (!ready) return false; 117 if (!sensor_data) return false; 118 119 apply_selection(); 120 121 sensor_data->current_mA = ina228.readCurrent() * 1000.0f; 122 sensor_data->bus_voltage_V = ina228.readBusVoltage(); 123 sensor_data->shunt_voltage_mV = ina228.readShuntVoltage() * 1000.0f; 124 sensor_data->power_mW = ina228.readPower() * 1000.0f; 125 sensor_data->energy_J = ina228.readEnergy(); 126 sensor_data->charge_C = ina228.readCharge(); 127 sensor_data->die_temperature_C = ina228.readDieTemp(); 128 sensor_data->ok = true; 129 130 hardware::i2c::clearSelection(); 131 return true; 132 } 133 134 #ifdef PIO_UNIT_TESTING 135 136 #include <testing/utils.h> 137 138 139 static void test_current_initializes(void) { 140 WHEN("the INA228 current monitor is initialized"); 141 test_ensure_wire1_with_power(); 142 hardware::i2c::initialize(); 143 144 if (!sensors::current::initialize()) { 145 TEST_IGNORE_MESSAGE("current::initialize() failed — skipping"); 146 return; 147 } 148 149 } 150 151 static void test_current_reads(void) { 152 GIVEN("the INA228 is available"); 153 WHEN("current monitor values are read"); 154 155 if (!sensors::current::isAvailable()) { 156 TEST_IGNORE_MESSAGE("INA228 not available — skipping"); 157 return; 158 } 159 160 CurrentSensorData data = {}; 161 bool ok = sensors::current::access(&data); 162 TEST_ASSERT_TRUE_MESSAGE(ok, "device: current::access() failed"); 163 164 char msg[128]; 165 snprintf(msg, sizeof(msg), "I=%.2fmA V=%.3fV P=%.2fmW T=%.1fC", 166 data.current_mA, data.bus_voltage_V, data.power_mW, data.die_temperature_C); 167 TEST_MESSAGE(msg); 168 } 169 170 static void test_current_rejects_null(void) { 171 WHEN("a null buffer is passed to access"); 172 THEN("it returns false"); 173 TEST_ASSERT_FALSE_MESSAGE(sensors::current::access(nullptr), 174 "device: access should fail with null pointer"); 175 } 176 177 void sensors::current::test() { 178 MODULE("Current"); 179 RUN_TEST(test_current_initializes); 180 RUN_TEST(test_current_reads); 181 RUN_TEST(test_current_rejects_null); 182 } 183 184 #endif