soil.cpp
1 #include "soil.h" 2 #include "registry.h" 3 4 #include <config.h> 5 #include "../hardware/rs485.h" 6 #include "../networking/modbus.h" 7 8 #include <Arduino.h> 9 #include <string.h> 10 11 namespace { 12 13 struct SoilProbeSlot { 14 uint8_t slave_id; 15 hardware::rs485::Channel channel; 16 bool responsive; 17 }; 18 19 SoilProbeSlot slots[8] = {}; 20 uint8_t probe_count = 0; 21 bool available = false; 22 23 void discover_probes() { 24 probe_count = 0; 25 memset(slots, 0, sizeof(slots)); 26 27 for (size_t index = 0; index < config::modbus::DEVICE_COUNT; index++) { 28 const config::ModbusSensorConfig &sensor_config = config::modbus::DEVICES[index]; 29 if (sensor_config.kind != config::ModbusSensorKind::SoilProbe) continue; 30 if (probe_count >= sizeof(slots) / sizeof(slots[0])) break; 31 32 hardware::rs485::Channel channel = sensor_config.channel == 0 33 ? hardware::rs485::Channel::Bus0 34 : hardware::rs485::Channel::Bus1; 35 36 uint16_t output_words[5] = {}; 37 ReadHoldingRegistersCommand command = { 38 .channel = channel, 39 .slave_id = sensor_config.slave_id, 40 .start_register = sensor_config.register_address, 41 .register_count = 5, 42 .output_words = output_words, 43 .error = ModbusError::NotInitialized, 44 }; 45 46 bool ok = networking::modbus::readHoldingRegisters(&command); 47 slots[probe_count] = { 48 .slave_id = sensor_config.slave_id, 49 .channel = channel, 50 .responsive = ok, 51 }; 52 53 if (ok) { 54 Serial.printf("[soil] probe slave %d responsive\n", sensor_config.slave_id); 55 } 56 probe_count++; 57 } 58 } 59 60 } 61 62 bool sensors::soil::access(uint8_t index, SoilSensorData *sensor_data) { 63 if (!sensor_data) return false; 64 memset(sensor_data, 0, sizeof(*sensor_data)); 65 sensor_data->ok = false; 66 if (index >= probe_count) return false; 67 68 const SoilProbeSlot &slot = slots[index]; 69 70 const config::ModbusSensorConfig *sensor_config = nullptr; 71 for (size_t i = 0; i < config::modbus::DEVICE_COUNT; i++) { 72 if (config::modbus::DEVICES[i].kind == config::ModbusSensorKind::SoilProbe && 73 config::modbus::DEVICES[i].slave_id == slot.slave_id) { 74 sensor_config = &config::modbus::DEVICES[i]; 75 break; 76 } 77 } 78 if (!sensor_config) return false; 79 80 uint16_t output_words[5] = {}; 81 ReadHoldingRegistersCommand command = { 82 .channel = slot.channel, 83 .slave_id = slot.slave_id, 84 .start_register = sensor_config->register_address, 85 .register_count = 5, 86 .output_words = output_words, 87 .error = ModbusError::NotInitialized, 88 }; 89 90 if (!networking::modbus::readHoldingRegisters(&command)) return false; 91 92 sensor_data->moisture_percent = output_words[0] / 10.0f; 93 sensor_data->temperature_celsius = static_cast<int16_t>(output_words[1]) / 10.0f; 94 sensor_data->conductivity = output_words[2]; 95 sensor_data->salinity = output_words[3]; 96 sensor_data->tds = output_words[4]; 97 sensor_data->slave_id = slot.slave_id; 98 sensor_data->ok = true; 99 return true; 100 } 101 102 bool sensors::soil::initialize() { 103 discover_probes(); 104 available = false; 105 for (uint8_t i = 0; i < probe_count; i++) { 106 if (slots[i].responsive) { 107 available = true; 108 break; 109 } 110 } 111 if (available) { 112 sensors::registry::add({ 113 .kind = SensorKind::Soil, 114 .name = "Soil", 115 .isAvailable = sensors::soil::isAvailable, 116 .instanceCount = sensors::soil::probeCount, 117 .poll = [](uint8_t index, void *out, size_t cap) -> bool { 118 if (cap < sizeof(SoilSensorData)) return false; 119 return sensors::soil::access( 120 index, static_cast<SoilSensorData *>(out)); 121 }, 122 .data_size = sizeof(SoilSensorData), 123 }); 124 } 125 return available; 126 } 127 128 bool sensors::soil::isAvailable() { 129 return available; 130 } 131 132 uint8_t sensors::soil::probeCount() { 133 return probe_count; 134 } 135 136 #ifdef PIO_UNIT_TESTING 137 138 #include <testing/utils.h> 139 140 static void test_soil_config_lookup(void) { 141 WHEN("the modbus topology is checked for soil probes"); 142 143 uint8_t count = 0; 144 for (size_t i = 0; i < config::modbus::DEVICE_COUNT; i++) { 145 if (config::modbus::DEVICES[i].kind == config::ModbusSensorKind::SoilProbe) 146 count++; 147 } 148 149 if (count == 0) { 150 TEST_IGNORE_MESSAGE("no soil probes configured — skipping"); 151 return; 152 } 153 154 TEST_PRINTF("%d soil probe(s) in topology", count); 155 } 156 157 static void test_soil_rejects_null(void) { 158 WHEN("a null buffer is passed to access"); 159 THEN("it returns false"); 160 TEST_ASSERT_FALSE_MESSAGE(sensors::soil::access(0, nullptr), 161 "device: access should fail with null pointer"); 162 } 163 164 static void test_soil_rejects_out_of_range(void) { 165 WHEN("an out-of-range probe index is requested"); 166 THEN("it returns false"); 167 SoilSensorData data = {}; 168 TEST_ASSERT_FALSE_MESSAGE(sensors::soil::access(255, &data), 169 "device: access should fail for invalid index"); 170 } 171 172 void sensors::soil::test() { 173 MODULE("Soil"); 174 RUN_TEST(test_soil_config_lookup); 175 RUN_TEST(test_soil_rejects_null); 176 RUN_TEST(test_soil_rejects_out_of_range); 177 } 178 179 #endif