diff --git a/platformio.ini b/platformio.ini index aa0ba9aa..e60b820c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -49,6 +49,8 @@ lib_deps = https://github.com/tzapu/WiFiManager.git#v2.0.16-rc.2 arduino-libraries/NTPClient@^3.2.1 thijse/ArduinoLog@^1.1.1 + # SHT3x support + robtillaart/SHT31@~0.5.0 [debug] build_type = debug diff --git a/src/devices/Device.hpp b/src/devices/Device.hpp index da7fc9de..8aab4919 100644 --- a/src/devices/Device.hpp +++ b/src/devices/Device.hpp @@ -216,8 +216,6 @@ class Device : ConsoleProvider { deviceDefinition.registerPeripheralFactories(peripheralManager); - // deviceTelemetryCollector.registerProvider("peripherals", peripheralManager); - mqttDeviceRoot->registerCommand(echoCommand); mqttDeviceRoot->registerCommand(pingCommand); // TODO Add reset-wifi command @@ -241,7 +239,19 @@ class Device : ConsoleProvider { // We want RTC to be in sync before we start setting up peripherals kernel.getRtcInSyncState().awaitSet(); - peripheralManager.createPeripherals(); + auto builtInPeripheralsCofig = deviceDefinition.getBuiltInPeripherals(); + Log.traceln("Loading configuration for %d built-in peripherals", + builtInPeripheralsCofig.size()); + for (auto& perpheralConfig : builtInPeripheralsCofig) { + peripheralManager.createPeripheral(perpheralConfig); + } + + auto& peripheralsConfig = deviceConfig.peripherals.get(); + Log.infoln("Loading configuration for %d peripherals", + peripheralsConfig.size()); + for (auto& perpheralConfig : peripheralsConfig) { + peripheralManager.createPeripheral(perpheralConfig.get()); + } kernel.getKernelReadyState().awaitSet(); @@ -277,7 +287,7 @@ class Device : ConsoleProvider { TDeviceConfiguration& deviceConfig = deviceDefinition.config; Kernel kernel { deviceConfig, deviceDefinition.statusLed }; shared_ptr mqttDeviceRoot = kernel.mqtt.forRoot("devices/ugly-duckling/" + deviceConfig.instance.get()); - PeripheralManager peripheralManager { mqttDeviceRoot, deviceConfig.peripherals }; + PeripheralManager peripheralManager { mqttDeviceRoot }; TelemetryCollector deviceTelemetryCollector; MqttTelemetryPublisher deviceTelemetryPublisher { mqttDeviceRoot, deviceTelemetryCollector }; diff --git a/src/devices/DeviceDefinition.hpp b/src/devices/DeviceDefinition.hpp index bfcdc64a..6ca808cf 100644 --- a/src/devices/DeviceDefinition.hpp +++ b/src/devices/DeviceDefinition.hpp @@ -1,15 +1,20 @@ #pragma once +#include + #include #include #include #include #include +#include + #include using namespace farmhub::kernel; using namespace farmhub::kernel::drivers; +using namespace farmhub::peripherals::environment; namespace farmhub::devices { @@ -48,6 +53,18 @@ class DeviceDefinition { } virtual void registerPeripheralFactories(PeripheralManager& peripheralManager) { + peripheralManager.registerFactory(sht3xFactory); + registerDeviceSpecificPeripheralFactories(peripheralManager); + } + + virtual void registerDeviceSpecificPeripheralFactories(PeripheralManager& peripheralManager) { + } + + /** + * @brief Returns zero or more JSON configurations for any built-in peripheral of the device. + */ + virtual std::list getBuiltInPeripherals() { + return {}; } public: @@ -59,6 +76,9 @@ class DeviceDefinition { public: TDeviceConfiguration& config = configFile.config; + +private: + EnvironmentSht3xFactory sht3xFactory; }; template diff --git a/src/devices/Peripheral.hpp b/src/devices/Peripheral.hpp index 9b03fbfb..d26bb658 100644 --- a/src/devices/Peripheral.hpp +++ b/src/devices/Peripheral.hpp @@ -15,6 +15,7 @@ using std::shared_ptr; using std::unique_ptr; using namespace farmhub::kernel; +using namespace farmhub::kernel::drivers; namespace farmhub::devices { @@ -79,35 +80,40 @@ class PeripheralCreationException : public std::exception { public: PeripheralCreationException(const String& name, const String& reason) - : name(name) - , reason(reason) { + : message(String("PeripheralCreationException: Failed to create peripheral '" + name + "' because " + reason)) { } const char* what() const noexcept override { - return String("Failed to create peripheral '" + name + "' because " + reason).c_str(); + return message.c_str(); } - const String name; - const String reason; + const String message; }; class PeripheralFactoryBase { public: - PeripheralFactoryBase(const String& type) - : type(type) { + PeripheralFactoryBase(const String& factoryType, const String& peripheralType) + : factoryType(factoryType) + , peripheralType(peripheralType) { } virtual unique_ptr createPeripheral(const String& name, const String& jsonConfig, shared_ptr mqttRoot) = 0; - const String type; + const String factoryType; + const String peripheralType; }; template class PeripheralFactory : public PeripheralFactoryBase { public: + // By default use the factory type as the peripheral type // TODO Use TDeviceConfigArgs&& instead PeripheralFactory(const String& type, TDeviceConfigArgs... deviceConfigArgs) - : PeripheralFactoryBase(type) + : PeripheralFactory(type, type, std::forward(deviceConfigArgs)...) { + } + + PeripheralFactory(const String& factoryType, const String& peripheralType, TDeviceConfigArgs... deviceConfigArgs) + : PeripheralFactoryBase(factoryType, peripheralType) , deviceConfigArgs(std::forward(deviceConfigArgs)...) { } @@ -145,34 +151,39 @@ class PeripheralManager : public TelemetryPublisher { public: PeripheralManager( - const shared_ptr mqttDeviceRoot, - ArrayProperty& peripheralsConfig) - : mqttDeviceRoot(mqttDeviceRoot) - , peripheralsConfig(peripheralsConfig) { + const shared_ptr mqttDeviceRoot) + : mqttDeviceRoot(mqttDeviceRoot) { } void registerFactory(PeripheralFactoryBase& factory) { Log.traceln("Registering peripheral factory: %s", - factory.type.c_str()); - factories.insert(std::make_pair(factory.type, std::reference_wrapper(factory))); - } - - void createPeripherals() { - Log.infoln("Loading configuration for %d peripherals", - peripheralsConfig.get().size()); - - for (auto& perpheralConfigJsonAsString : peripheralsConfig.get()) { - PeripheralDeviceConfiguration deviceConfig; - deviceConfig.loadFromString(perpheralConfigJsonAsString.get()); - const String& name = deviceConfig.name.get(); - const String& type = deviceConfig.type.get(); - try { - unique_ptr peripheral = createPeripheral(name, type, deviceConfig.params.get().get()); - peripherals.push_back(move(peripheral)); - } catch (const PeripheralCreationException& e) { - Log.errorln("Failed to create peripheral: %s of type %s because %s", - name.c_str(), type.c_str(), e.reason.c_str()); - } + factory.factoryType.c_str()); + factories.insert(std::make_pair(factory.factoryType, std::reference_wrapper(factory))); + } + + void createPeripheral(const String& peripheralConfig) { + Log.info("Creating peripheral with config: %s", + peripheralConfig.c_str()); + PeripheralDeviceConfiguration deviceConfig; + try { + deviceConfig.loadFromString(peripheralConfig); + } catch (const std::exception& e) { + Log.errorln("Failed to parse peripheral config because %s:\n%s", + e.what(), peripheralConfig.c_str()); + return; + } + + const String& name = deviceConfig.name.get(); + const String& factory = deviceConfig.type.get(); + try { + unique_ptr peripheral = createPeripheral(name, factory, deviceConfig.params.get().get()); + peripherals.push_back(move(peripheral)); + } catch (const std::exception& e) { + Log.errorln("Failed to create peripheral '%s' with factory '%s' because %s", + name.c_str(), factory.c_str(), e.what()); + } catch (...) { + Log.errorln("Failed to create peripheral '%s' with factory '%s' because of an unknown exception", + name.c_str(), factory.c_str()); } } @@ -190,20 +201,20 @@ class PeripheralManager Property params { this, "params" }; }; - unique_ptr createPeripheral(const String& name, const String& type, const String& configJson) { - Log.traceln("Creating peripheral: %s of type %s", - name.c_str(), type.c_str()); - auto it = factories.find(type); + unique_ptr createPeripheral(const String& name, const String& factoryType, const String& configJson) { + Log.traceln("Creating peripheral '%s' with factory '%s'", + name.c_str(), factoryType.c_str()); + auto it = factories.find(factoryType); if (it == factories.end()) { - throw PeripheralCreationException(name, "No factory found for peripheral type '" + type + "'"); + throw PeripheralCreationException(name, "Factory not found: '" + factoryType + "'"); } - shared_ptr mqttRoot = mqttDeviceRoot->forSuffix("peripherals/" + type + "/" + name); + const String& peripheralType = it->second.get().peripheralType; + shared_ptr mqttRoot = mqttDeviceRoot->forSuffix("peripherals/" + peripheralType + "/" + name); PeripheralFactoryBase& factory = it->second.get(); return factory.createPeripheral(name, configJson, mqttRoot); } const shared_ptr mqttDeviceRoot; - ArrayProperty& peripheralsConfig; // TODO Use an unordered_map? std::map> factories; diff --git a/src/devices/UglyDucklingMk4.hpp b/src/devices/UglyDucklingMk4.hpp index 6ab5f3d5..62ee12fb 100644 --- a/src/devices/UglyDucklingMk4.hpp +++ b/src/devices/UglyDucklingMk4.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -36,12 +38,27 @@ class UglyDucklingMk4 : public DeviceDefinition { GPIO_NUM_26) { } - void registerPeripheralFactories(PeripheralManager& peripheralManager) override { + void registerDeviceSpecificPeripheralFactories(PeripheralManager& peripheralManager) override { peripheralManager.registerFactory(valveFactory); peripheralManager.registerFactory(flowMeterFactory); peripheralManager.registerFactory(flowControlFactory); } + std::list getBuiltInPeripherals() override { + // Device address is 0x44 = 68 + return { + R"({ + "type": "environment:sht3x", + "name": "environment", + "params": { + "address": "0x44", + "sda": 8, + "scl": 9 + } + })" + }; + } + Drv8801Driver motorDriver { pwm, GPIO_NUM_10, // Enable diff --git a/src/devices/UglyDucklingMk5.hpp b/src/devices/UglyDucklingMk5.hpp index 675a5628..693a6700 100644 --- a/src/devices/UglyDucklingMk5.hpp +++ b/src/devices/UglyDucklingMk5.hpp @@ -40,7 +40,7 @@ class UglyDucklingMk5 : public BatteryPoweredDeviceDefinition { GPIO_NUM_1, 2.4848) { } - void registerPeripheralFactories(PeripheralManager& peripheralManager) override { + void registerDeviceSpecificPeripheralFactories(PeripheralManager& peripheralManager) override { peripheralManager.registerFactory(valveFactory); peripheralManager.registerFactory(flowMeterFactory); peripheralManager.registerFactory(flowControlFactory); diff --git a/src/devices/UglyDucklingMk6.hpp b/src/devices/UglyDucklingMk6.hpp index 6ce0d31f..bffb1f0c 100644 --- a/src/devices/UglyDucklingMk6.hpp +++ b/src/devices/UglyDucklingMk6.hpp @@ -39,7 +39,7 @@ class UglyDucklingMk6 : public BatteryPoweredDeviceDefinition { GPIO_NUM_1, 1.2424) { } - void registerPeripheralFactories(PeripheralManager& peripheralManager) override { + void registerDeviceSpecificPeripheralFactories(PeripheralManager& peripheralManager) override { peripheralManager.registerFactory(valveFactory); peripheralManager.registerFactory(flowMeterFactory); peripheralManager.registerFactory(flowControlFactory); diff --git a/src/kernel/Configuration.hpp b/src/kernel/Configuration.hpp index 7c9555e2..3ded8088 100644 --- a/src/kernel/Configuration.hpp +++ b/src/kernel/Configuration.hpp @@ -13,6 +13,20 @@ using std::reference_wrapper; namespace farmhub::kernel { +class ConfigurationException + : public std::exception { +public: + ConfigurationException(const String& message) + : message("ConfigurationException: " + message) { + } + + const char* what() const noexcept override { + return message.c_str(); + } + + const String message; +}; + class JsonAsString { public: JsonAsString() { @@ -63,8 +77,11 @@ class ConfigurationEntry { void loadFromString(const String& json) { DynamicJsonDocument jsonDocument(docSizeFor(json)); DeserializationError error = deserializeJson(jsonDocument, json); + if (error == DeserializationError::EmptyInput) { + return; + } if (error) { - throw "Cannot parse JSON configuration: " + String(error.c_str()); + throw ConfigurationException("Cannot parse JSON configuration: " + String(error.c_str()) + json); } load(jsonDocument.as()); } @@ -273,21 +290,21 @@ class ConfigurationFile { } else { File file = fs.open(path, FILE_READ); if (!file) { - throw "Cannot open config file " + path; + throw ConfigurationException("Cannot open config file " + path); } DynamicJsonDocument json(docSizeFor(file)); DeserializationError error = deserializeJson(json, file); file.close(); if (error) { - throw "Cannot open config file " + path + " (" + String(error.c_str()) + ")"; + throw ConfigurationException("Cannot open config file " + path + " (" + String(error.c_str()) + ")"); } update(json.as()); } onUpdate([&fs, path](const JsonObject& json) { File file = fs.open(path, FILE_WRITE); if (!file) { - throw "Cannot open config file " + path; + throw ConfigurationException("Cannot open config file " + path); } serializeJson(json, file); diff --git a/src/peripherals/I2CConfig.hpp b/src/peripherals/I2CConfig.hpp new file mode 100644 index 00000000..991043de --- /dev/null +++ b/src/peripherals/I2CConfig.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include + +#include + +using namespace std::chrono; +using namespace farmhub::kernel; + +namespace farmhub::peripherals::environment { + +struct I2CConfig { +public: + uint8_t address; + gpio_num_t sda; + gpio_num_t scl; + + String toString() { + return String("I2C address: 0x") + String(address, HEX) + ", SDA: " + String(sda) + ", SCL: " + String(scl); + } +}; + +class I2CDeviceConfig + : public ConfigurationSection { +public: + // I2C address is typically a hexadecimal number, + // but JSON doesn't support 0x notation, so we + // take it as a string instead + Property address { this, "address" }; + Property sda { this, "sda", GPIO_NUM_NC }; + Property scl { this, "scl", GPIO_NUM_NC }; + + I2CConfig parse() const { + return parse(-1, GPIO_NUM_NC, GPIO_NUM_NC); + } + + I2CConfig parse(uint8_t defaultAddress, gpio_num_t defaultSda, gpio_num_t defaultScl) const { + return { + address.get().isEmpty() + ? defaultAddress + : (uint8_t) strtol(address.get().c_str(), nullptr, 0), + sda.get() == GPIO_NUM_NC + ? defaultSda + : sda.get(), + scl.get() == GPIO_NUM_NC + ? defaultScl + : scl.get() + }; + } +}; + +} // namespace farmhub::peripherals::environment diff --git a/src/peripherals/environment/EnvironmentSht3x.hpp b/src/peripherals/environment/EnvironmentSht3x.hpp new file mode 100644 index 00000000..ac42937d --- /dev/null +++ b/src/peripherals/environment/EnvironmentSht3x.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "EnvironmentSht3xComponent.hpp" + +using namespace farmhub::devices; +using namespace farmhub::kernel; +using namespace farmhub::kernel::drivers; +using std::make_unique; +using std::unique_ptr; +namespace farmhub::peripherals::environment { + +class EnvironmentSht3x + : public Peripheral { +public: + EnvironmentSht3x(const String& name, shared_ptr mqttRoot, I2CConfig config) + : Peripheral(name, mqttRoot) + , sht3x(name, mqttRoot, config) { + } + + void populateTelemetry(JsonObject& telemetryJson) override { + sht3x.populateTelemetry(telemetryJson); + } + +private: + EnvironmentSht3xComponent sht3x; +}; + +class EnvironmentSht3xFactory + : public PeripheralFactory { +public: + EnvironmentSht3xFactory() + : PeripheralFactory("environment:sht3x", "environment") { + } + + unique_ptr> createPeripheral(const String& name, const I2CDeviceConfig& deviceConfig, shared_ptr mqttRoot) override { + auto i2cConfig = deviceConfig.parse(SHT_DEFAULT_ADDRESS, GPIO_NUM_NC, GPIO_NUM_NC); + Log.infoln("Creating SHT3x environment sensor %s with %s", name.c_str(), i2cConfig.toString().c_str()); + return make_unique(name, mqttRoot, i2cConfig); + } +}; + +} // namespace farmhub::peripherals::environment diff --git a/src/peripherals/environment/EnvironmentSht3xComponent.hpp b/src/peripherals/environment/EnvironmentSht3xComponent.hpp new file mode 100644 index 00000000..96aea71c --- /dev/null +++ b/src/peripherals/environment/EnvironmentSht3xComponent.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include +#include + +#include +#include + +#include + +using namespace farmhub::kernel; + +namespace farmhub::peripherals::environment { + +class EnvironmentSht3xComponent + : public Component, + public TelemetryProvider { +public: + EnvironmentSht3xComponent( + const String& name, + shared_ptr mqttRoot, + I2CConfig config) + : Component(name, mqttRoot) + // TODO Add I2C manager to hand out wires + , wire(1) + , sht3x(config.address, &wire) { + + // TODO Add commands to soft/hard reset the sensor + // TODO Add configuration for fast / slow measurement + // TODO Add a separate task to do measurements to unblock telemetry collection? + + Log.infoln("Initializing SHT3s environment sensor with %s", config.toString().c_str()); + if (!wire.begin(config.sda, config.scl, 100000L)) { + Log.errorln("Failed to initialize I2C bus for SHT3x environment sensor"); + return; + } + if (!sht3x.begin()) { + Log.errorln("Failed to initialize SHT3x environment sensor: %d", + sht3x.getError()); + return; + } + if (!sht3x.isConnected()) { + Log.errorln("SHT3x environment sensor is not connected: %d", + sht3x.getError()); + return; + } + initialized = true; + } + + void populateTelemetry(JsonObject& json) override { + if (!initialized) { + return; + } + if (!sht3x.read()) { + Log.errorln("Failed to read SHT3x environment sensor: %d", + sht3x.getError()); + return; + } + json["temperature"] = sht3x.getTemperature(); + json["humidity"] = sht3x.getHumidity(); + } + +private: + TwoWire wire; + SHT31 sht3x; + bool initialized = false; +}; + +} // namespace farmhub::peripherals::environment diff --git a/src/peripherals/flow_control/FlowControl.hpp b/src/peripherals/flow_control/FlowControl.hpp index 0410986a..581bd5c5 100644 --- a/src/peripherals/flow_control/FlowControl.hpp +++ b/src/peripherals/flow_control/FlowControl.hpp @@ -79,7 +79,7 @@ class FlowControlFactory valveConfig.switchDuration.get(), valveConfig.duty.get() / 100.0); } catch (const std::exception& e) { - throw PeripheralCreationException(name, "Failed to create strategy: " + String(e.what())); + throw PeripheralCreationException(name, "failed to create strategy: " + String(e.what())); } return make_unique( name, @@ -99,7 +99,7 @@ class FlowControlFactory return motor.get(); } } - throw PeripheralCreationException(name, "Failed to find motor: " + motorName); + throw PeripheralCreationException(name, "failed to find motor: " + motorName); } private: diff --git a/src/peripherals/valve/Valve.hpp b/src/peripherals/valve/Valve.hpp index cb9e4443..5d8ffc24 100644 --- a/src/peripherals/valve/Valve.hpp +++ b/src/peripherals/valve/Valve.hpp @@ -66,7 +66,7 @@ class ValveFactory deviceConfig.switchDuration.get(), deviceConfig.duty.get() / 100.0); } catch (const std::exception& e) { - throw PeripheralCreationException(name, "Failed to create strategy: " + String(e.what())); + throw PeripheralCreationException(name, "failed to create strategy: " + String(e.what())); } return make_unique(name, targetMotor, *strategy, mqttRoot); } @@ -77,7 +77,7 @@ class ValveFactory return motor.get(); } } - throw PeripheralCreationException(name, "Failed to find motor: " + motorName); + throw PeripheralCreationException(name, "failed to find motor: " + motorName); } private: diff --git a/src/peripherals/valve/ValveConfig.hpp b/src/peripherals/valve/ValveConfig.hpp index 363e6e77..b0c3e01a 100644 --- a/src/peripherals/valve/ValveConfig.hpp +++ b/src/peripherals/valve/ValveConfig.hpp @@ -4,6 +4,7 @@ #include #include +#include #include using namespace farmhub::kernel;