/ firmware / src / sensors / current.cpp
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