temperature_and_humidity.cpp
1 #include "temperature_and_humidity.h" 2 #include "registry.h" 3 #include <config.h> 4 #include <i2c.h> 5 6 #include <Arduino.h> 7 #include <CHT832X.h> 8 #include <SHT31.h> 9 #include <math.h> 10 11 namespace { 12 13 enum class TemperatureHumidityBackend : uint8_t { 14 CHT832X, 15 SHT31, 16 }; 17 18 struct TemperatureHumiditySlot { 19 TemperatureHumidityBackend backend; 20 config::I2CSensorConfig config; 21 int8_t resolved_mux_channel; 22 bool available; 23 }; 24 25 TemperatureHumiditySlot slots[config::temperature_humidity::MAX_SENSORS] = {}; 26 uint8_t sensor_count = 0; 27 28 hardware::i2c::Bus to_bus(uint8_t bus) { 29 return bus == 0 ? hardware::i2c::Bus::Bus0 : hardware::i2c::Bus::Bus1; 30 } 31 32 bool access_i2c_device(const config::I2CSensorConfig &config, 33 int8_t resolved_mux_channel, 34 hardware::i2c::DeviceAccessCommand *command) { 35 if (!command) return false; 36 command->bus = to_bus(config.bus); 37 command->mux_channel = resolved_mux_channel; 38 command->wire = nullptr; 39 command->ok = false; 40 return hardware::i2c::accessDevice(command); 41 } 42 43 bool probe_cht832x(const config::I2CSensorConfig &config, 44 int8_t resolved_mux_channel) { 45 hardware::i2c::DeviceAccessCommand command = {}; 46 if (!access_i2c_device(config, resolved_mux_channel, &command)) return false; 47 48 CHT832X sensor(config.address, command.wire); 49 int result = sensor.begin(); 50 if (result != CHT832X_OK) { 51 hardware::i2c::clearSelection(); 52 return false; 53 } 54 55 uint16_t manufacturer = sensor.getManufacturer(); 56 hardware::i2c::clearSelection(); 57 return manufacturer == 0x5959; 58 } 59 60 bool probe_sht31(const config::I2CSensorConfig &config, 61 int8_t resolved_mux_channel) { 62 hardware::i2c::DeviceAccessCommand command = {}; 63 if (!access_i2c_device(config, resolved_mux_channel, &command)) return false; 64 65 SHT31 sensor(config.address, command.wire); 66 bool ok = sensor.begin() && sensor.isConnected(); 67 hardware::i2c::clearSelection(); 68 return ok; 69 } 70 71 bool probe_descriptor(const config::I2CSensorConfig &config, 72 int8_t resolved_mux_channel) { 73 switch (config.kind) { 74 case config::I2CSensorKind::TemperatureHumidityCHT832X: 75 return probe_cht832x(config, resolved_mux_channel); 76 case config::I2CSensorKind::TemperatureHumiditySHT3X: 77 return probe_sht31(config, resolved_mux_channel); 78 default: 79 return false; 80 } 81 } 82 83 bool read_cht832x(const TemperatureHumiditySlot &slot, 84 TemperatureHumiditySensorData *sensor_data) { 85 hardware::i2c::DeviceAccessCommand command = {}; 86 if (!access_i2c_device(slot.config, slot.resolved_mux_channel, &command)) return false; 87 88 CHT832X sensor(slot.config.address, command.wire); 89 int begin_result = sensor.begin(); 90 if (begin_result != CHT832X_OK) { 91 hardware::i2c::clearSelection(); 92 return false; 93 } 94 95 sensor.setReadDelay(config::temperature_humidity::READ_DELAY_MS); 96 int result = sensor.read(); 97 bool success = (result == CHT832X_OK || result == CHT832X_ERROR_CRC); 98 if (success) { 99 sensor_data->temperature_celsius = sensor.getTemperature(); 100 sensor_data->relative_humidity_percent = sensor.getHumidity(); 101 sensor_data->model = "CHT832X"; 102 sensor_data->ok = true; 103 } else { 104 sensor_data->model = "CHT832X"; 105 sensor_data->ok = false; 106 Serial.printf("[temperature_and_humidity] CHT832X read failed: %d\n", result); 107 } 108 109 hardware::i2c::clearSelection(); 110 return success; 111 } 112 113 bool read_sht31(const TemperatureHumiditySlot &slot, 114 TemperatureHumiditySensorData *sensor_data) { 115 hardware::i2c::DeviceAccessCommand command = {}; 116 if (!access_i2c_device(slot.config, slot.resolved_mux_channel, &command)) return false; 117 118 SHT31 sensor(slot.config.address, command.wire); 119 bool success = sensor.begin() && sensor.read(true); 120 if (success) { 121 sensor_data->temperature_celsius = sensor.getTemperature(); 122 sensor_data->relative_humidity_percent = sensor.getHumidity(); 123 sensor_data->model = "SHT31"; 124 sensor_data->ok = true; 125 } else { 126 sensor_data->model = "SHT31"; 127 sensor_data->ok = false; 128 } 129 130 hardware::i2c::clearSelection(); 131 return success; 132 } 133 134 void append_slot(TemperatureHumidityBackend backend, 135 const config::I2CSensorConfig &config, 136 int8_t resolved_mux_channel) { 137 if (sensor_count >= config::temperature_humidity::MAX_SENSORS) return; 138 slots[sensor_count++] = { 139 .backend = backend, 140 .config = config, 141 .resolved_mux_channel = resolved_mux_channel, 142 .available = true, 143 }; 144 } 145 146 } 147 148 bool sensors::temperature_and_humidity::initialize() { 149 sensor_count = 0; 150 151 hardware::i2c::TopologySnapshot topology = {}; 152 hardware::i2c::accessTopology(&topology); 153 154 for (size_t index = 0; index < config::i2c_topology::DEVICE_COUNT; index++) { 155 const config::I2CSensorConfig &device = config::i2c_topology::DEVICES[index]; 156 TemperatureHumidityBackend backend; 157 158 switch (device.kind) { 159 case config::I2CSensorKind::TemperatureHumidityCHT832X: 160 backend = TemperatureHumidityBackend::CHT832X; 161 break; 162 case config::I2CSensorKind::TemperatureHumiditySHT3X: 163 backend = TemperatureHumidityBackend::SHT31; 164 break; 165 default: 166 continue; 167 } 168 169 if (device.mux_channel == config::i2c::DIRECT_CHANNEL) { 170 if (probe_descriptor(device, config::i2c::DIRECT_CHANNEL)) { 171 append_slot(backend, device, config::i2c::DIRECT_CHANNEL); 172 Serial.printf("[temperature_and_humidity] found %s on bus %u addr 0x%02X\n", 173 backend == TemperatureHumidityBackend::CHT832X ? "CHT832X" : "SHT31", 174 device.bus, device.address); 175 } 176 continue; 177 } 178 179 if (device.mux_channel == config::i2c::ANY_MUX_CHANNEL) { 180 if (!topology.mux_present || device.bus != 1) continue; 181 for (uint8_t channel = 0; channel < hardware::i2c::mux.channelCount(); channel++) { 182 if (probe_descriptor(device, channel)) { 183 append_slot(backend, device, channel); 184 Serial.printf("[temperature_and_humidity] found %s on mux channel %u addr 0x%02X\n", 185 backend == TemperatureHumidityBackend::CHT832X ? "CHT832X" : "SHT31", 186 channel, device.address); 187 } 188 } 189 continue; 190 } 191 192 if (!topology.mux_present || device.bus != 1) continue; 193 if (probe_descriptor(device, device.mux_channel)) { 194 append_slot(backend, device, device.mux_channel); 195 Serial.printf("[temperature_and_humidity] found %s on mux channel %d addr 0x%02X\n", 196 backend == TemperatureHumidityBackend::CHT832X ? "CHT832X" : "SHT31", 197 device.mux_channel, device.address); 198 } 199 } 200 201 hardware::i2c::clearSelection(); 202 Serial.printf("[temperature_and_humidity] discovered %d sensor(s)\n", 203 sensor_count); 204 205 if (sensor_count > 0) { 206 sensors::registry::add({ 207 .kind = SensorKind::TemperatureHumidity, 208 .name = "Temperature & Humidity", 209 .isAvailable = []() -> bool { 210 return sensors::temperature_and_humidity::sensorCount() > 0; 211 }, 212 .instanceCount = sensors::temperature_and_humidity::sensorCount, 213 .poll = [](uint8_t index, void *out, size_t cap) -> bool { 214 if (cap < sizeof(TemperatureHumiditySensorData)) return false; 215 return sensors::temperature_and_humidity::access( 216 index, static_cast<TemperatureHumiditySensorData *>(out)); 217 }, 218 .data_size = sizeof(TemperatureHumiditySensorData), 219 }); 220 } 221 return sensor_count > 0; 222 } 223 224 uint8_t sensors::temperature_and_humidity::sensorCount() { 225 return sensor_count; 226 } 227 228 bool sensors::temperature_and_humidity::access(uint8_t index, 229 TemperatureHumiditySensorData *sensor_data) { 230 if (index >= sensor_count) return false; 231 if (!sensor_data) return false; 232 233 switch (slots[index].backend) { 234 case TemperatureHumidityBackend::CHT832X: 235 return read_cht832x(slots[index], sensor_data); 236 case TemperatureHumidityBackend::SHT31: 237 return read_sht31(slots[index], sensor_data); 238 default: 239 return false; 240 } 241 } 242 243 uint8_t sensors::temperature_and_humidity::accessAll(TemperatureHumiditySensorData *sensor_data, 244 bool *read_ok, 245 uint8_t max_count) { 246 uint8_t count = (sensor_count < max_count) ? sensor_count : max_count; 247 uint8_t successful_reads = 0; 248 249 for (uint8_t index = 0; index < count; index++) { 250 read_ok[index] = sensors::temperature_and_humidity::access(index, &sensor_data[index]); 251 if (read_ok[index]) { 252 successful_reads++; 253 } else { 254 sensor_data[index].temperature_celsius = NAN; 255 sensor_data[index].relative_humidity_percent = NAN; 256 } 257 } 258 259 hardware::i2c::clearSelection(); 260 return successful_reads; 261 } 262 263 #ifdef PIO_UNIT_TESTING 264 265 #include <testing/utils.h> 266 267 268 static void test_temp_humidity_discovers_sensors(void) { 269 GIVEN("Wire1 with power enabled"); 270 WHEN("sensors are discovered from the I2C topology"); 271 test_ensure_wire1_with_power(); 272 hardware::i2c::initialize(); 273 274 uint8_t count = sensors::temperature_and_humidity::initialize(); 275 char message[64]; 276 snprintf(message, sizeof(message), "discovered %d sensor(s)", count); 277 TEST_MESSAGE(message); 278 if (count == 0) { 279 TEST_IGNORE_MESSAGE("no temperature/humidity sensors connected"); 280 return; 281 } 282 } 283 284 static void test_temp_humidity_reads_plausible_values(void) { 285 GIVEN("at least one discovered sensor"); 286 WHEN("sensor 0 is read"); 287 288 if (sensors::temperature_and_humidity::sensorCount() == 0) { 289 TEST_IGNORE_MESSAGE("no sensors discovered, skipping"); 290 return; 291 } 292 293 TemperatureHumiditySensorData sensor_data = {}; 294 bool success = sensors::temperature_and_humidity::access(0, &sensor_data); 295 296 if (!success) { 297 TEST_IGNORE_MESSAGE("temperature/humidity read failed — sensor may be absent or busy"); 298 return; 299 } 300 301 char message[128]; 302 snprintf(message, sizeof(message), 303 "sensor 0 (%s): %.2f C, %.2f %% RH", sensor_data.model ? sensor_data.model : "unknown", 304 sensor_data.temperature_celsius, 305 sensor_data.relative_humidity_percent); 306 TEST_MESSAGE(message); 307 308 TEST_ASSERT_FLOAT_IS_DETERMINATE_MESSAGE(sensor_data.temperature_celsius, 309 "device: temperature reading is NaN or Inf"); 310 TEST_ASSERT_FLOAT_IS_DETERMINATE_MESSAGE(sensor_data.relative_humidity_percent, 311 "device: humidity reading is NaN or Inf"); 312 TEST_ASSERT_GREATER_OR_EQUAL_FLOAT_MESSAGE(-40.0f, sensor_data.temperature_celsius, 313 "device: temperature below SHT3x minimum (-40 C)"); 314 TEST_ASSERT_LESS_OR_EQUAL_FLOAT_MESSAGE(85.0f, sensor_data.temperature_celsius, 315 "device: temperature above SHT3x maximum (85 C)"); 316 TEST_ASSERT_GREATER_OR_EQUAL_FLOAT_MESSAGE(0.0f, sensor_data.relative_humidity_percent, 317 "device: humidity below 0 %"); 318 TEST_ASSERT_LESS_OR_EQUAL_FLOAT_MESSAGE(100.0f, sensor_data.relative_humidity_percent, 319 "device: humidity above 100 %"); 320 } 321 322 static void test_temp_humidity_rejects_out_of_range_index(void) { 323 WHEN("an out-of-range sensor index is read"); 324 325 TemperatureHumiditySensorData sensor_data = {}; 326 bool success = sensors::temperature_and_humidity::access( 327 sensors::temperature_and_humidity::sensorCount(), &sensor_data); 328 329 TEST_ASSERT_FALSE_MESSAGE(success, 330 "device: read should fail for index >= sensor_count"); 331 } 332 333 static void test_temp_humidity_reads_all_sensors(void) { 334 GIVEN("discovered sensors"); 335 WHEN("all sensors are read"); 336 337 if (sensors::temperature_and_humidity::sensorCount() == 0) { 338 TEST_IGNORE_MESSAGE("no temperature/humidity sensors discovered"); 339 return; 340 } 341 342 delay(1100); 343 344 uint8_t count = sensors::temperature_and_humidity::sensorCount(); 345 TemperatureHumiditySensorData sensor_data[config::temperature_humidity::MAX_SENSORS]; 346 bool read_ok[config::temperature_humidity::MAX_SENSORS]; 347 348 uint8_t successful = sensors::temperature_and_humidity::accessAll( 349 sensor_data, read_ok, count); 350 351 char message[128]; 352 snprintf(message, sizeof(message), 353 "%d of %d sensors read successfully", successful, count); 354 TEST_MESSAGE(message); 355 356 TEST_ASSERT_GREATER_THAN_MESSAGE(0, successful, 357 "device: at least one sensor should read successfully"); 358 359 for (uint8_t index = 0; index < count; index++) { 360 if (read_ok[index]) { 361 snprintf(message, sizeof(message), 362 "sensor %d (%s): %.2f C, %.2f %% RH", 363 index, sensor_data[index].model ? sensor_data[index].model : "unknown", 364 sensor_data[index].temperature_celsius, 365 sensor_data[index].relative_humidity_percent); 366 TEST_MESSAGE(message); 367 } 368 } 369 } 370 371 static void test_temp_humidity_cht832x_manufacturer_id(void) { 372 GIVEN("a discovered CHT832X sensor"); 373 WHEN("the manufacturer ID is read"); 374 375 if (sensors::temperature_and_humidity::sensorCount() == 0) { 376 test_ensure_wire1_with_power(); 377 hardware::i2c::initialize(); 378 sensors::temperature_and_humidity::initialize(); 379 } 380 381 size_t cht_index = SIZE_MAX; 382 for (uint8_t index = 0; index < sensors::temperature_and_humidity::sensorCount(); index++) { 383 if (slots[index].backend == TemperatureHumidityBackend::CHT832X) { 384 cht_index = index; 385 break; 386 } 387 } 388 389 if (cht_index == SIZE_MAX) { 390 TEST_IGNORE_MESSAGE("no CHT832X sensors discovered"); 391 return; 392 } 393 394 hardware::i2c::DeviceAccessCommand command = {}; 395 TEST_ASSERT_TRUE_MESSAGE(access_i2c_device(slots[cht_index].config, 396 slots[cht_index].resolved_mux_channel, 397 &command), 398 "device: failed to access resolved CHT832X device"); 399 CHT832X sensor(slots[cht_index].config.address, command.wire); 400 TEST_ASSERT_EQUAL_INT_MESSAGE(CHT832X_OK, sensor.begin(), 401 "device: CHT832X begin failed during manufacturer check"); 402 uint16_t manufacturer = sensor.getManufacturer(); 403 hardware::i2c::clearSelection(); 404 405 char message[64]; 406 snprintf(message, sizeof(message), "manufacturer ID: 0x%04X", manufacturer); 407 TEST_MESSAGE(message); 408 409 TEST_ASSERT_EQUAL_HEX16_MESSAGE(0x5959, manufacturer, 410 "device: unexpected manufacturer ID (expected 0x5959)"); 411 } 412 413 void sensors::temperature_and_humidity::test() { 414 MODULE("Temperature & Humidity"); 415 RUN_TEST(test_temp_humidity_discovers_sensors); 416 RUN_TEST(test_temp_humidity_reads_plausible_values); 417 RUN_TEST(test_temp_humidity_rejects_out_of_range_index); 418 RUN_TEST(test_temp_humidity_reads_all_sensors); 419 RUN_TEST(test_temp_humidity_cht832x_manufacturer_id); 420 } 421 422 #endif