diff --git a/Peripherals.md b/Peripherals.md new file mode 100644 index 00000000..db03c54e --- /dev/null +++ b/Peripherals.md @@ -0,0 +1,25 @@ +# Peripherals + +* A **device** is a physical object that you can move around that has an MCU and can connect to the hub. + + An example of a _device_ is an actual Ugly Duckling PCB, placed in the field, connected to whatever externals. + + The device publishes telemetry, and receives commands under `/devices/ugly-duckling/$INSTANCE`. + +* A **peripheral** is an independently meaningful functionality made available by the device. This can mean to measure and provide telemetry, and/or to be operated via direct commands and/or via published configuration. + + An example of a _peripheral_ is a flow-control device that consists of a valve, a flow sensor, and optionally a thermometer. + + A peripheral publishes telemetry and receives commands and configuration under `/peripherals/$TYPE/$NAME`. + +* A **component** is a functional aspect of a peripheral connected to services. Components are created as part of the creation of a peripheral, and are not addressable. + + An example of a _component_ is a valve that depends on a motor driver for operation. + + A component can register its own commands under its owner peripheral’s topic. + + A component will contribute to the telemetry published by its owner peripheral. + + A component will receive the configuration of its owning peripheral; it needs to pick out the data it needs from it. + +* A **service** is an internally addressable feature of the device, i.e. that it has a LED connected to a pin, or a motor driver on a certain set of pins, or even its raw pins themselves. diff --git a/pio-docker b/pio-docker old mode 100644 new mode 100755 diff --git a/platformio.ini b/platformio.ini index 95072786..aa0ba9aa 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,16 +13,30 @@ default_envs = mk6 boards_dir = boards build_cache_dir = .pio/build-cache +[base] +build_unflags = + -std=gnu++11 +build_flags = + -std=gnu++17 +lib_deps = + bblanchon/ArduinoJson@^6.21.3 + +[env:native] +extends = base +platform = native@~1.2.1 +test_framework = googletest + [esp32base] +extends = base platform = espressif32@~6.4.0 framework = espidf, arduino extra_scripts = pre:git-version.py build_type = release build_unflags = - -std=gnu++11 + ${base.build_unflags} build_flags = - -std=gnu++17 + ${base.build_flags} -D WM_NODEBUG=1 -D FARMHUB_REPORT_MEMORY @@ -30,8 +44,8 @@ monitor_filters = esp32_exception_decoder monitor_speed = 115200 lib_deps = + ${base.lib_deps} 256dpi/MQTT@^2.5.1 - bblanchon/ArduinoJson@^6.21.3 https://github.com/tzapu/WiFiManager.git#v2.0.16-rc.2 arduino-libraries/NTPClient@^3.2.1 thijse/ArduinoLog@^1.1.1 diff --git a/src/devices/Device.hpp b/src/devices/Device.hpp index 531e57e4..91b76d8d 100644 --- a/src/devices/Device.hpp +++ b/src/devices/Device.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #ifndef FARMHUB_LOG_LEVEL #ifdef FARMHUB_DEBUG @@ -19,6 +20,7 @@ #include using namespace std::chrono; +using std::shared_ptr; using namespace farmhub::kernel; #if defined(MK4) @@ -42,14 +44,12 @@ typedef farmhub::devices::Mk6Config TDeviceConfiguration; #error "No device defined" #endif -namespace farmhub { namespace devices { +namespace farmhub::devices { #ifdef FARMHUB_DEBUG class ConsolePrinter : public Print { public: ConsolePrinter() { - Serial.begin(115200); - static const String spinner = "|/-\\"; static const int spinnerLength = spinner.length(); Task::loop("console", 8192, 1, [this](Task& task) { @@ -159,13 +159,20 @@ void printLogLine(Print* printer, int level) { class ConsoleProvider { public: ConsoleProvider() { + Serial.begin(115200); #ifdef FARMHUB_DEBUG Log.begin(FARMHUB_LOG_LEVEL, &consolePrinter); Log.setSuffix(printLogLine); #else Log.begin(FARMHUB_LOG_LEVEL, &Serial); #endif - Log.infoln("Starting up..."); + Log.infoln(F(" ______ _ _ _")); + Log.infoln(F(" | ____| | | | | | |")); + Log.infoln(F(" | |__ __ _ _ __ _ __ ___ | |__| |_ _| |__")); + Log.infoln(F(" | __/ _` | '__| '_ ` _ \\| __ | | | | '_ \\")); + Log.infoln(F(" | | | (_| | | | | | | | | | | | |_| | |_) |")); + Log.infoln(F(" |_| \\__,_|_| |_| |_| |_|_| |_|\\__,_|_.__/ %s"), VERSION); + Log.infoln(""); } }; @@ -178,22 +185,20 @@ class MemoryTelemetryProvider : public TelemetryProvider { class MqttTelemetryPublisher : public TelemetryPublisher { public: - MqttTelemetryPublisher(MqttDriver::MqttRoot& mqtt, TelemetryCollector& telemetryCollector) - : mqttRoot(mqtt) + MqttTelemetryPublisher(shared_ptr mqttRoot, TelemetryCollector& telemetryCollector) + : mqttRoot(mqttRoot) , telemetryCollector(telemetryCollector) { } void publishTelemetry() { - mqttRoot.publish("telemetry", [&](JsonObject& json) { telemetryCollector.collect(json); }); + mqttRoot->publish("telemetry", [&](JsonObject& json) { telemetryCollector.collect(json); }); } private: - MqttDriver::MqttRoot& mqttRoot; + shared_ptr mqttRoot; TelemetryCollector& telemetryCollector; }; -typedef std::function CommandHandler; - class Device : ConsoleProvider { public: Device() { @@ -213,19 +218,17 @@ class Device : ConsoleProvider { // deviceTelemetryCollector.registerProvider("peripherals", peripheralManager); - registerCommand(echoCommand); - registerCommand(pingCommand); + mqttDeviceRoot->registerCommand(echoCommand); + mqttDeviceRoot->registerCommand(pingCommand); // TODO Add reset-wifi command - // registerCommand(resetWifiCommand); - registerCommand(restartCommand); - registerCommand(sleepCommand); - registerCommand(fileListCommand); - registerCommand(fileReadCommand); - registerCommand(fileWriteCommand); - registerCommand(fileRemoveCommand); - registerCommand(httpUpdateCommand); - - peripheralManager.begin(); + // mqttDeviceRoot->registerCommand(resetWifiCommand); + mqttDeviceRoot->registerCommand(restartCommand); + mqttDeviceRoot->registerCommand(sleepCommand); + mqttDeviceRoot->registerCommand(fileListCommand); + mqttDeviceRoot->registerCommand(fileReadCommand); + mqttDeviceRoot->registerCommand(fileWriteCommand); + mqttDeviceRoot->registerCommand(fileRemoveCommand); + mqttDeviceRoot->registerCommand(httpUpdateCommand); #if defined(MK4) deviceDefinition.motorDriver.wakeUp(); @@ -235,9 +238,14 @@ class Device : ConsoleProvider { deviceDefinition.motorDriver.wakeUp(); #endif - kernel.begin(); + // We want RTC to be in sync before we start setting up peripherals + kernel.getRtcInSyncState().awaitSet(); + + peripheralManager.begin(); + + kernel.getKernelReadyState().awaitSet(); - mqttDeviceRoot.publish( + mqttDeviceRoot->publish( "init", [&](JsonObject& json) { // TODO Remove redundanty mentions of "ugly-duckling" @@ -265,33 +273,13 @@ class Device : ConsoleProvider { task.delayUntil(milliseconds(60000)); } - void registerCommand(const String& name, CommandHandler handler) { - String suffix = "commands/" + name; - mqttDeviceRoot.subscribe(suffix, MqttDriver::QoS::ExactlyOnce, [this, name, suffix, handler](const String&, const JsonObject& request) { - // Clear topic - mqttDeviceRoot.clear(suffix, MqttDriver::Retention::Retain, MqttDriver::QoS::ExactlyOnce); - DynamicJsonDocument responseDoc(2048); - auto response = responseDoc.to(); - handler(request, response); - if (response.size() > 0) { - mqttDeviceRoot.publish("responses/" + name, responseDoc, MqttDriver::Retention::NoRetain, MqttDriver::QoS::ExactlyOnce); - } - }); - } - - void registerCommand(Command& command) { - registerCommand(command.name, [&](const JsonObject& request, JsonObject& response) { - command.handle(request, response); - }); - } - TDeviceDefinition deviceDefinition; TDeviceConfiguration& deviceConfig = deviceDefinition.config; Kernel kernel { deviceConfig, deviceDefinition.statusLed }; PeripheralManager peripheralManager { kernel.mqtt, deviceConfig.peripherals }; TelemetryCollector deviceTelemetryCollector; - MqttDriver::MqttRoot mqttDeviceRoot = kernel.mqtt.forRoot("devices/ugly-duckling/" + deviceConfig.instance.get()); + shared_ptr mqttDeviceRoot = kernel.mqtt.forRoot("devices/ugly-duckling/" + deviceConfig.instance.get()); MqttTelemetryPublisher deviceTelemetryPublisher { mqttDeviceRoot, deviceTelemetryCollector }; PingCommand pingCommand { deviceTelemetryPublisher }; @@ -310,4 +298,4 @@ class Device : ConsoleProvider { HttpUpdateCommand httpUpdateCommand { kernel.version }; }; -}} // namespace farmhub::devices +} // namespace farmhub::devices diff --git a/src/devices/DeviceDefinition.hpp b/src/devices/DeviceDefinition.hpp index bf2483f3..bfcdc64a 100644 --- a/src/devices/DeviceDefinition.hpp +++ b/src/devices/DeviceDefinition.hpp @@ -11,7 +11,7 @@ using namespace farmhub::kernel; using namespace farmhub::kernel::drivers; -namespace farmhub { namespace devices { +namespace farmhub::devices { class DeviceConfiguration : public ConfigurationSection { public: @@ -23,8 +23,8 @@ class DeviceConfiguration : public ConfigurationSection { Property model; Property instance; - MqttDriver::Config mqtt { this, "mqtt" }; - RtcDriver::Config ntp { this, "ntp" }; + NamedConfigurationEntry mqtt { this, "mqtt" }; + NamedConfigurationEntry ntp { this, "ntp" }; ArrayProperty peripherals { this, "peripherals" }; @@ -73,4 +73,4 @@ class BatteryPoweredDeviceDefinition : public DeviceDefinition #include +#include #include #include using std::move; +using std::shared_ptr; using std::unique_ptr; using namespace farmhub::kernel; -namespace farmhub { namespace devices { +namespace farmhub::devices { // Peripherals class PeripheralBase - : public TelemetryProvider { + : public TelemetryProvider, + public Named { public: - PeripheralBase(const String& name, MqttDriver::MqttRoot& mqttRoot, size_t telemetrySize = 2048) - : name(name) + PeripheralBase(const String& name, shared_ptr mqttRoot, size_t telemetrySize = 2048) + : Named(name) , mqttRoot(mqttRoot) , telemetrySize(telemetrySize) { + mqttRoot->registerCommand("ping", [this](const JsonObject& request, JsonObject& response) { + Serial.println("Received ping request"); + publishTelemetry(); + response["pong"] = millis(); + }); } virtual ~PeripheralBase() = default; @@ -35,19 +43,18 @@ class PeripheralBase populateTelemetry(telemetryJson); if (telemetryJson.begin() == telemetryJson.end()) { // No telemetry added + Log.verboseln("No telemetry to publish for peripheral: %s", name.c_str()); return; } // TODO Add device ID - mqttRoot.publish("telemetry", telemetryDoc); + mqttRoot->publish("telemetry", telemetryDoc); } - virtual void populateTelemetry(JsonObject& telemetryJson) { + virtual void populateTelemetry(JsonObject& telemetryJson) override { } - const String name; - protected: - MqttDriver::MqttRoot mqttRoot; + shared_ptr mqttRoot; private: const size_t telemetrySize; @@ -57,7 +64,7 @@ template class Peripheral : public PeripheralBase { public: - Peripheral(const String& name, MqttDriver::MqttRoot& mqttRoot) + Peripheral(const String& name, shared_ptr mqttRoot) : PeripheralBase(name, mqttRoot) { } @@ -68,41 +75,64 @@ class Peripheral // Peripheral factories +class PeripheralCreationException + : public std::exception { +public: + PeripheralCreationException(const String& name, const String& reason) + : name(name) + , reason(reason) { + } + + const char* what() const noexcept override { + return String("Failed to create peripheral '" + name + "' because " + reason).c_str(); + } + + const String name; + const String reason; +}; + class PeripheralFactoryBase { public: PeripheralFactoryBase(const String& type) : type(type) { } - virtual PeripheralBase* createPeripheral(const String& name, const String& jsonConfig, MqttDriver::MqttRoot mqttRoot) = 0; + virtual unique_ptr createPeripheral(const String& name, const String& jsonConfig, shared_ptr mqttRoot) = 0; const String type; }; -template +template class PeripheralFactory : public PeripheralFactoryBase { public: - PeripheralFactory(const String& type) - : PeripheralFactoryBase(type) { + // TODO Use TDeviceConfigArgs&& instead + PeripheralFactory(const String& type, TDeviceConfigArgs... deviceConfigArgs) + : PeripheralFactoryBase(type) + , deviceConfigArgs(std::forward(deviceConfigArgs)...) { } - virtual TDeviceConfig* createDeviceConfig() = 0; - - PeripheralBase* createPeripheral(const String& name, const String& jsonConfig, MqttDriver::MqttRoot mqttRoot) override { - ConfigurationFile* configFile = new ConfigurationFile(FileSystem::get(), "peripherals/" + name + ".json"); - mqttRoot.subscribe("config", [name, configFile](const String&, const JsonObject& configJson) { + unique_ptr createPeripheral(const String& name, const String& jsonConfig, shared_ptr mqttRoot) override { + // Use short prefix because SPIFFS has a 32 character limit + ConfigurationFile* configFile = new ConfigurationFile(FileSystem::get(), "/p/" + name); + mqttRoot->subscribe("config", [name, configFile](const String&, const JsonObject& configJson) { Log.traceln("Received configuration update for peripheral: %s", name.c_str()); configFile->update(configJson); }); - TDeviceConfig* deviceConfig = createDeviceConfig(); - deviceConfig->loadFromString(jsonConfig); - Peripheral* peripheral = createPeripheral(name, *deviceConfig, mqttRoot); + TDeviceConfig deviceConfig = std::apply([](TDeviceConfigArgs... args) { + return TDeviceConfig(std::forward(args)...); + }, + deviceConfigArgs); + deviceConfig.loadFromString(jsonConfig); + unique_ptr> peripheral = createPeripheral(name, deviceConfig, mqttRoot); peripheral->configure(configFile->config); return peripheral; } - virtual Peripheral* createPeripheral(const String& name, const TDeviceConfig& deviceConfig, MqttDriver::MqttRoot mqttRoot) = 0; + virtual unique_ptr> createPeripheral(const String& name, const TDeviceConfig& deviceConfig, shared_ptr mqttRoot) = 0; + +private: + std::tuple deviceConfigArgs; }; // Peripheral manager @@ -130,13 +160,13 @@ class PeripheralManager deviceConfig.loadFromString(perpheralConfigJsonAsString.get()); const String& name = deviceConfig.name.get(); const String& type = deviceConfig.type.get(); - PeripheralBase* peripheral = createPeripheral(name, type, deviceConfig.params.get().get()); - if (peripheral == nullptr) { - Log.errorln("Failed to create peripheral: %s of type %s", - name.c_str(), type.c_str()); - return; + 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()); } - peripherals.push_back(unique_ptr(peripheral)); } } @@ -154,20 +184,16 @@ class PeripheralManager Property params { this, "params" }; }; - PeripheralBase* createPeripheral(const String& name, const String& type, const String& configJson) { + 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); if (it == factories.end()) { - // TODO Handle the case where no factory is found for the given type - Log.errorln("No factory found for peripheral type: %s among %d factories", - type.c_str(), factories.size()); - return nullptr; + throw PeripheralCreationException(name, "No factory found for peripheral type '" + type + "'"); } - MqttDriver::MqttRoot mqttRoot(mqtt, "peripherals/" + type + "/" + name); + shared_ptr mqttRoot = mqtt.forRoot("peripherals/" + type + "/" + name); PeripheralFactoryBase& factory = it->second.get(); - PeripheralBase* peripheral = factory.createPeripheral(name, configJson, mqttRoot); - return peripheral; + return factory.createPeripheral(name, configJson, mqttRoot); } MqttDriver& mqtt; @@ -178,4 +204,4 @@ class PeripheralManager std::list> peripherals; }; -}} // namespace farmhub::devices +} // namespace farmhub::devices diff --git a/src/devices/UglyDucklingMk4.hpp b/src/devices/UglyDucklingMk4.hpp index d1109192..6ab5f3d5 100644 --- a/src/devices/UglyDucklingMk4.hpp +++ b/src/devices/UglyDucklingMk4.hpp @@ -9,12 +9,16 @@ #include +#include +#include #include using namespace farmhub::kernel; +using namespace farmhub::peripherals::flow_control; +using namespace farmhub::peripherals::flow_meter; using namespace farmhub::peripherals::valve; -namespace farmhub { namespace devices { +namespace farmhub::devices { class Mk4Config : public DeviceConfiguration { @@ -34,6 +38,8 @@ class UglyDucklingMk4 : public DeviceDefinition { void registerPeripheralFactories(PeripheralManager& peripheralManager) override { peripheralManager.registerFactory(valveFactory); + peripheralManager.registerFactory(flowMeterFactory); + peripheralManager.registerFactory(flowControlFactory); } Drv8801Driver motorDriver { @@ -50,6 +56,8 @@ class UglyDucklingMk4 : public DeviceDefinition { const ServiceRef motor { "motor", motorDriver }; ValveFactory valveFactory { { motor }, ValveControlStrategyType::Latching }; + FlowMeterFactory flowMeterFactory; + FlowControlFactory flowControlFactory { { motor }, ValveControlStrategyType::Latching }; }; -}} // namespace farmhub::devices +} // namespace farmhub::devices diff --git a/src/devices/UglyDucklingMk5.hpp b/src/devices/UglyDucklingMk5.hpp index 20a3d2b8..675a5628 100644 --- a/src/devices/UglyDucklingMk5.hpp +++ b/src/devices/UglyDucklingMk5.hpp @@ -9,9 +9,13 @@ #include +#include +#include #include using namespace farmhub::kernel; +using namespace farmhub::peripherals::flow_control; +using namespace farmhub::peripherals::flow_meter; using namespace farmhub::peripherals::valve; namespace farmhub { @@ -38,6 +42,8 @@ class UglyDucklingMk5 : public BatteryPoweredDeviceDefinition { void registerPeripheralFactories(PeripheralManager& peripheralManager) override { peripheralManager.registerFactory(valveFactory); + peripheralManager.registerFactory(flowMeterFactory); + peripheralManager.registerFactory(flowControlFactory); } Drv8874Driver motorADriver { @@ -62,6 +68,8 @@ class UglyDucklingMk5 : public BatteryPoweredDeviceDefinition { const ServiceRef motorB { "b", motorBDriver }; ValveFactory valveFactory { { motorA, motorB }, ValveControlStrategyType::Latching }; + FlowMeterFactory flowMeterFactory; + FlowControlFactory flowControlFactory { { motorA, motorB }, ValveControlStrategyType::Latching }; }; }} // namespace farmhub::devices diff --git a/src/devices/UglyDucklingMk6.hpp b/src/devices/UglyDucklingMk6.hpp index 3c7e83b8..6ce0d31f 100644 --- a/src/devices/UglyDucklingMk6.hpp +++ b/src/devices/UglyDucklingMk6.hpp @@ -10,12 +10,16 @@ #include #include +#include +#include #include using namespace farmhub::kernel; +using namespace farmhub::peripherals::flow_control; +using namespace farmhub::peripherals::flow_meter; using namespace farmhub::peripherals::valve; -namespace farmhub { namespace devices { +namespace farmhub::devices { class Mk6Config : public DeviceConfiguration { @@ -37,6 +41,8 @@ class UglyDucklingMk6 : public BatteryPoweredDeviceDefinition { void registerPeripheralFactories(PeripheralManager& peripheralManager) override { peripheralManager.registerFactory(valveFactory); + peripheralManager.registerFactory(flowMeterFactory); + peripheralManager.registerFactory(flowControlFactory); } LedDriver secondaryStatusLed { "status-2", GPIO_NUM_4 }; @@ -55,6 +61,8 @@ class UglyDucklingMk6 : public BatteryPoweredDeviceDefinition { const ServiceRef motorB { "b", motorDriver.getMotorB() }; ValveFactory valveFactory { { motorA, motorB }, ValveControlStrategyType::Latching }; + FlowMeterFactory flowMeterFactory; + FlowControlFactory flowControlFactory { { motorA, motorB }, ValveControlStrategyType::Latching }; }; -}} // namespace farmhub::devices +} // namespace farmhub::devices diff --git a/src/kernel/BootClock.hpp b/src/kernel/BootClock.hpp new file mode 100644 index 00000000..566121a2 --- /dev/null +++ b/src/kernel/BootClock.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +using namespace std; + +namespace farmhub::kernel { + +/** + * @brief Monotonic clock based on ESP's esp_timer_get_time() + * + * Time returned has the property of only increasing at a uniform rate. + */ +struct boot_clock { + typedef chrono::microseconds duration; + typedef duration::rep rep; + typedef duration::period period; + typedef chrono::time_point time_point; + + static constexpr bool is_steady = true; + + static time_point now() noexcept { + return time_point(duration(esp_timer_get_time())); + } +}; + +} // namespace farmhub::kernel diff --git a/src/kernel/Command.hpp b/src/kernel/Command.hpp index c2f4866c..9bc82cb8 100644 --- a/src/kernel/Command.hpp +++ b/src/kernel/Command.hpp @@ -10,21 +10,24 @@ #include #include +#include #include using namespace std::chrono; -namespace farmhub { namespace kernel { +namespace farmhub::kernel { -class Command { +class Command : public Named { public: virtual void handle(const JsonObject& request, JsonObject& response) = 0; - const String name; + virtual size_t getResponseSize() { + return 1024; + } protected: Command(const String& name) - : name(name) { + : Named(name) { } }; @@ -233,4 +236,4 @@ class HttpUpdateCommand : public Command { const String currentVersion; }; -}} // namespace farmhub::kernel +} // namespace farmhub::kernel diff --git a/src/kernel/Component.hpp b/src/kernel/Component.hpp new file mode 100644 index 00000000..d7d2e238 --- /dev/null +++ b/src/kernel/Component.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +using namespace farmhub::kernel::drivers; + +namespace farmhub::kernel { + +class Component : public Named { +protected: + Component(const String& name, shared_ptr mqttRoot) + : Named(name) + , mqttRoot(mqttRoot) { + } + + shared_ptr mqttRoot; +}; + +} // namespace farmhub::kernel diff --git a/src/kernel/Concurrent.hpp b/src/kernel/Concurrent.hpp index 142e4ade..f93f4482 100644 --- a/src/kernel/Concurrent.hpp +++ b/src/kernel/Concurrent.hpp @@ -10,7 +10,7 @@ using namespace std::chrono; -namespace farmhub { namespace kernel { +namespace farmhub::kernel { template class Queue { @@ -161,4 +161,4 @@ class Mutex { const SemaphoreHandle_t mutex; }; -}} // namespace farmhub::kernel +} // namespace farmhub::kernel diff --git a/src/kernel/Configuration.hpp b/src/kernel/Configuration.hpp index e5326fef..7c9555e2 100644 --- a/src/kernel/Configuration.hpp +++ b/src/kernel/Configuration.hpp @@ -11,7 +11,7 @@ using std::list; using std::ref; using std::reference_wrapper; -namespace farmhub { namespace kernel { +namespace farmhub::kernel { class JsonAsString { public: @@ -113,17 +113,22 @@ class ConfigurationSection : public ConfigurationEntry { list> entries; }; -class NamedConfigurationSection : public ConfigurationSection { +class EmptyConfiguration : public ConfigurationSection { }; + +template +class NamedConfigurationEntry : public ConfigurationEntry { public: - NamedConfigurationSection(ConfigurationSection* parent, const String& name) - : name(name) { + template + NamedConfigurationEntry(ConfigurationSection* parent, const String& name, Args&&... args) + : name(name) + , delegate(std::forward(args)...) { parent->add(*this); } void load(const JsonObject& json) override { if (json.containsKey(name)) { namePresentAtLoad = true; - ConfigurationSection::load(json[name]); + delegate.load(json[name]); } else { reset(); } @@ -132,21 +137,26 @@ class NamedConfigurationSection : public ConfigurationSection { void store(JsonObject& json, bool inlineDefaults) const override { if (inlineDefaults || hasValue()) { auto section = json.createNestedObject(name); - ConfigurationSection::store(section, inlineDefaults); + delegate.store(section, inlineDefaults); } } bool hasValue() const override { - return namePresentAtLoad || ConfigurationSection::hasValue(); + return namePresentAtLoad || delegate.hasValue(); } void reset() override { namePresentAtLoad = false; - ConfigurationSection::reset(); + delegate.reset(); + } + + const TDelegate& get() const { + return delegate; } private: const String name; + TDelegate delegate; bool namePresentAtLoad = false; }; @@ -324,9 +334,9 @@ class ConfigurationFile { std::list> callbacks; }; -}} // namespace farmhub::kernel +} // namespace farmhub::kernel -namespace std { namespace chrono { +namespace std::chrono { using namespace std::chrono; @@ -340,4 +350,4 @@ void convertFromJson(JsonVariantConst src, Duration& dst) { dst = Duration { src.as() }; } -}} // namespace std::chrono +} // namespace std::chrono diff --git a/src/kernel/FileSystem.hpp b/src/kernel/FileSystem.hpp index d9d2c56a..fe71de31 100644 --- a/src/kernel/FileSystem.hpp +++ b/src/kernel/FileSystem.hpp @@ -4,7 +4,7 @@ #include -namespace farmhub { namespace kernel { +namespace farmhub::kernel { class FileSystem; static FileSystem* initializeFileSystem(); @@ -72,10 +72,10 @@ static FileSystem* initializeFileSystem() { break; } Log.infoln(" - %s (%d bytes)", - file.name(), file.size()); + file.path(), file.size()); file.close(); } return new SpiffsFileSystem(); } -}} // namespace farmhub::kernel +} // namespace farmhub::kernel diff --git a/src/kernel/Json.hpp b/src/kernel/Json.hpp index 9059cb04..01b5246d 100644 --- a/src/kernel/Json.hpp +++ b/src/kernel/Json.hpp @@ -3,7 +3,7 @@ #include #include -namespace farmhub { namespace kernel { +namespace farmhub::kernel { size_t constexpr _docSizeFor(size_t size) { return size * 2 + 1024; @@ -21,4 +21,4 @@ size_t inline docSizeFor(const File file) { return _docSizeFor(file.size()); } -}} +} // namespace farmhub::kernel diff --git a/src/kernel/Kernel.hpp b/src/kernel/Kernel.hpp index e874dc5e..a0908753 100644 --- a/src/kernel/Kernel.hpp +++ b/src/kernel/Kernel.hpp @@ -16,7 +16,7 @@ #include -namespace farmhub { namespace kernel { +namespace farmhub::kernel { using namespace farmhub::kernel::drivers; @@ -63,12 +63,12 @@ class Kernel { Task::loop("status-update", 4096, [this](Task&) { updateState(); }); } - void begin() { - kernelReadyState.awaitSet(); - + const State& getRtcInSyncState() const { + return rtcInSyncState; + } - Log.infoln("Kernel ready in %d ms", - millis()); + const State& getKernelReadyState() const { + return kernelReadyState; } const String version; @@ -169,10 +169,10 @@ class Kernel { OtaDriver ota { networkReadyState, deviceConfig.getHostname() }; #endif MdnsDriver mdns { networkReadyState, deviceConfig.getHostname(), "ugly-duckling", version, mdnsReadyState }; - RtcDriver rtc { networkReadyState, mdns, deviceConfig.ntp, rtcInSyncState }; + RtcDriver rtc { networkReadyState, mdns, deviceConfig.ntp.get(), rtcInSyncState }; public: - MqttDriver mqtt { networkReadyState, mdns, deviceConfig.mqtt, deviceConfig.instance.get(), mqttReadyState }; + MqttDriver mqtt { networkReadyState, mdns, deviceConfig.mqtt.get(), deviceConfig.instance.get(), mqttReadyState }; }; -}} // namespace farmhub::kernel +} // namespace farmhub::kernel diff --git a/src/kernel/Named.hpp b/src/kernel/Named.hpp new file mode 100644 index 00000000..37e85cb5 --- /dev/null +++ b/src/kernel/Named.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace farmhub::kernel { + +class Named { +protected: + Named(const String& name) + : name(name) { + } + +public: + const String name; +}; + +} // namespace farmhub::kernel diff --git a/src/kernel/NvmStore.hpp b/src/kernel/NvmStore.hpp index 4d1869cb..5d97a6de 100644 --- a/src/kernel/NvmStore.hpp +++ b/src/kernel/NvmStore.hpp @@ -9,7 +9,7 @@ #include -namespace farmhub { namespace kernel { +namespace farmhub::kernel { /** * @brief Thread safe NVM store for JSON serializable objects. @@ -91,4 +91,4 @@ class NvmStore { static const size_t DEFAULT_BUFFER_SIZE = 2048; }; -}} // namespace farmhub::kernel +} // namespace farmhub::kernel diff --git a/src/kernel/PwmManager.hpp b/src/kernel/PwmManager.hpp index 3fe62a44..0ee4aacd 100644 --- a/src/kernel/PwmManager.hpp +++ b/src/kernel/PwmManager.hpp @@ -4,7 +4,7 @@ #include -namespace farmhub { namespace kernel { +namespace farmhub::kernel { // TODO Figure out what to do with low/high speed modes // See https://docs.espressif.com/projects/esp-idf/en/release-v4.2/esp32/api-reference/peripherals/ledc.html#ledc-high-and-low-speed-mode @@ -52,4 +52,4 @@ class PwmManager { uint8_t nextChannel = 0; }; -}} // namespace farmhub::kernel +} // namespace farmhub::kernel diff --git a/src/kernel/Service.hpp b/src/kernel/Service.hpp index 09ee4e49..70440021 100644 --- a/src/kernel/Service.hpp +++ b/src/kernel/Service.hpp @@ -2,13 +2,15 @@ #include -namespace farmhub { namespace kernel { +#include + +namespace farmhub::kernel { template -class ServiceRef { +class ServiceRef : public Named { public: ServiceRef(const String& name, T& instance) - : name(name) + : Named(name) , reference(instance) { } @@ -25,8 +27,7 @@ class ServiceRef { } private: - const String name; const std::reference_wrapper reference; }; -}} // namespace farmhub::kernel +} // namespace farmhub::kernel diff --git a/src/kernel/State.hpp b/src/kernel/State.hpp index e3a9f20b..11d6eeeb 100644 --- a/src/kernel/State.hpp +++ b/src/kernel/State.hpp @@ -14,7 +14,7 @@ using namespace std::chrono; -namespace farmhub { namespace kernel { +namespace farmhub::kernel { // 0th bit reserved to indicate that a state has changed static const int STATE_CHANGE_BIT_MASK = (1 << 0); @@ -174,4 +174,4 @@ class StateManager { int nextEventBit = 1; }; -}} // namespace farmhub::kernel +} // namespace farmhub::kernel diff --git a/src/kernel/Task.hpp b/src/kernel/Task.hpp index 7ccafa1b..2d285239 100644 --- a/src/kernel/Task.hpp +++ b/src/kernel/Task.hpp @@ -10,11 +10,11 @@ #include -using namespace std::chrono; +#include -namespace farmhub { namespace kernel { +using namespace std::chrono; -using ticks = std::chrono::duration>; +namespace farmhub::kernel { static const uint32_t DEFAULT_STACK_SIZE = 2048; static const unsigned int DEFAULT_PRIORITY = 1; @@ -104,8 +104,12 @@ class Task { vTaskSuspend(nullptr); } + void yield() { + taskYIELD(); + } + #ifdef FARMHUB_DEBUG - static const uint32_t CONSOLE_BUFFER_INDEX = 1; + static const BaseType_t CONSOLE_BUFFER_INDEX = 1; static String* consoleBuffer() { String* buffer = static_cast(pvTaskGetThreadLocalStoragePointer(nullptr, CONSOLE_BUFFER_INDEX)); @@ -118,7 +122,14 @@ class Task { #endif private: + Task() { + Log.traceln("Starting task %s\n", + pcTaskGetName(nullptr)); + } + ~Task() { + Log.traceln("Finished task %s\n", + pcTaskGetName(nullptr)); #ifdef FARMHUB_DEBUG String* buffer = static_cast(pvTaskGetThreadLocalStoragePointer(nullptr, CONSOLE_BUFFER_INDEX)); if (buffer != nullptr) { @@ -133,15 +144,11 @@ class Task { TaskFunction taskFunction(*taskFunctionParam); delete taskFunctionParam; - Log.traceln("Starting task %s\n", - pcTaskGetName(nullptr)); Task task; taskFunction(task); - Log.traceln("Finished task %s\n", - pcTaskGetName(nullptr)); } TickType_t lastWakeTime { xTaskGetTickCount() }; }; -}} // namespace farmhub::kernel +} // namespace farmhub::kernel diff --git a/src/kernel/Telemetry.hpp b/src/kernel/Telemetry.hpp index fe0b6127..26f5f2a5 100644 --- a/src/kernel/Telemetry.hpp +++ b/src/kernel/Telemetry.hpp @@ -6,7 +6,7 @@ #include -namespace farmhub { namespace kernel { +namespace farmhub::kernel { class TelemetryProvider { public: @@ -39,4 +39,4 @@ class TelemetryPublisher { virtual void publishTelemetry() = 0; }; -}} // namespace farmhub::kernel +} // namespace farmhub::kernel diff --git a/src/kernel/Time.hpp b/src/kernel/Time.hpp new file mode 100644 index 00000000..3c068782 --- /dev/null +++ b/src/kernel/Time.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace farmhub::kernel { + +using ticks = std::chrono::duration>; + +} diff --git a/src/kernel/drivers/BatteryDriver.hpp b/src/kernel/drivers/BatteryDriver.hpp index abe480b9..6e7893d5 100644 --- a/src/kernel/drivers/BatteryDriver.hpp +++ b/src/kernel/drivers/BatteryDriver.hpp @@ -6,7 +6,7 @@ #include -namespace farmhub { namespace kernel { namespace drivers { +namespace farmhub::kernel::drivers { class BatteryDriver : public TelemetryProvider { @@ -35,4 +35,4 @@ class BatteryDriver const float voltageDividerRatio; }; -}}} // namespace farmhub::kernel::drivers +} // namespace farmhub::kernel::drivers diff --git a/src/kernel/drivers/Drv8801Driver.hpp b/src/kernel/drivers/Drv8801Driver.hpp index 353bb6a2..230085cc 100644 --- a/src/kernel/drivers/Drv8801Driver.hpp +++ b/src/kernel/drivers/Drv8801Driver.hpp @@ -10,7 +10,7 @@ using namespace std::chrono; -namespace farmhub { namespace kernel { namespace drivers { +namespace farmhub::kernel::drivers { /** * @brief Texas Instruments DRV8801 motor driver. @@ -99,4 +99,4 @@ class Drv8801Driver std::atomic sleeping { false }; }; -}}} // namespace farmhub::kernel::drivers +} // namespace farmhub::kernel::drivers diff --git a/src/kernel/drivers/Drv8833Driver.hpp b/src/kernel/drivers/Drv8833Driver.hpp index a08995bc..0eec7793 100644 --- a/src/kernel/drivers/Drv8833Driver.hpp +++ b/src/kernel/drivers/Drv8833Driver.hpp @@ -10,7 +10,7 @@ using namespace std::chrono; -namespace farmhub { namespace kernel { namespace drivers { +namespace farmhub::kernel::drivers { /** * @brief Texas Instruments DRV883 dual motor driver. @@ -113,4 +113,4 @@ class Drv8833Driver { std::atomic sleeping { false }; }; -}}} // namespace farmhub::kernel::drivers +} // namespace farmhub::kernel::drivers diff --git a/src/kernel/drivers/Drv8874Driver.hpp b/src/kernel/drivers/Drv8874Driver.hpp index 29deab97..1c9dec82 100644 --- a/src/kernel/drivers/Drv8874Driver.hpp +++ b/src/kernel/drivers/Drv8874Driver.hpp @@ -10,7 +10,7 @@ using namespace std::chrono; -namespace farmhub { namespace kernel { namespace drivers { +namespace farmhub::kernel::drivers { /** * @brief Texas Instruments DRV8874 motor driver. @@ -91,4 +91,4 @@ class Drv8874Driver std::atomic sleeping { false }; }; -}}} // namespace farmhub::kernel::drivers +} // namespace farmhub::kernel::drivers diff --git a/src/kernel/drivers/LedDriver.hpp b/src/kernel/drivers/LedDriver.hpp index 76ca6f67..247d265f 100644 --- a/src/kernel/drivers/LedDriver.hpp +++ b/src/kernel/drivers/LedDriver.hpp @@ -11,7 +11,7 @@ using namespace std::chrono; -namespace farmhub { namespace kernel { namespace drivers { +namespace farmhub::kernel::drivers { class LedDriver { public: @@ -88,4 +88,4 @@ class LedDriver { BlinkPattern currentPattern; }; -}}} // namespace farmhub::kernel::drivers +} // namespace farmhub::kernel::drivers diff --git a/src/kernel/drivers/MdnsDriver.hpp b/src/kernel/drivers/MdnsDriver.hpp index 3bad2b6c..010209cd 100644 --- a/src/kernel/drivers/MdnsDriver.hpp +++ b/src/kernel/drivers/MdnsDriver.hpp @@ -10,7 +10,7 @@ #include #include -namespace farmhub { namespace kernel { namespace drivers { +namespace farmhub::kernel::drivers { struct MdnsRecord { String hostname; @@ -116,4 +116,4 @@ void convertFromJson(JsonVariantConst src, MdnsRecord& dst) { dst.port = jsonRecord["port"].as(); } -}}} // namespace farmhub::kernel::drivers +} // namespace farmhub::kernel::drivers diff --git a/src/kernel/drivers/MotorDriver.hpp b/src/kernel/drivers/MotorDriver.hpp index 06cdeb9f..dfd40d41 100644 --- a/src/kernel/drivers/MotorDriver.hpp +++ b/src/kernel/drivers/MotorDriver.hpp @@ -1,6 +1,6 @@ #pragma once -namespace farmhub { namespace kernel { namespace drivers { +namespace farmhub::kernel::drivers { enum class MotorPhase { FORWARD = 1, @@ -22,4 +22,4 @@ class PwmMotorDriver { virtual void drive(MotorPhase phase, double duty = 1) = 0; }; -}}} // namespace farmhub::kernel::drivers +} // namespace farmhub::kernel::drivers diff --git a/src/kernel/drivers/MqttDriver.hpp b/src/kernel/drivers/MqttDriver.hpp index d384180f..e6462e8a 100644 --- a/src/kernel/drivers/MqttDriver.hpp +++ b/src/kernel/drivers/MqttDriver.hpp @@ -1,10 +1,12 @@ #pragma once #include +#include #include #include +#include #include #include #include @@ -13,8 +15,10 @@ #include using namespace farmhub::kernel; +using std::make_shared; +using std::shared_ptr; -namespace farmhub { namespace kernel { namespace drivers { +namespace farmhub::kernel::drivers { class MqttDriver { public: @@ -29,6 +33,8 @@ class MqttDriver { ExactlyOnce = 2 }; + typedef std::function CommandHandler; + typedef std::function SubscriptionHandler; class MqttRoot { @@ -38,11 +44,6 @@ class MqttDriver { , rootTopic(rootTopic) { } - MqttRoot(const MqttRoot& other) - : mqtt(other.mqtt) - , rootTopic(other.rootTopic) { - } - bool publish(const String& suffix, const JsonDocument& json, Retention retain = Retention::NoRetain, QoS qos = QoS::AtMostOnce) { return mqtt.publish(fullTopic(suffix), json, retain, qos); } @@ -62,6 +63,30 @@ class MqttDriver { return subscribe(suffix, QoS::ExactlyOnce, handler); } + bool registerCommand(const String& name, CommandHandler handler) { + return registerCommand(name, 1024, handler); + } + + bool registerCommand(const String& name, size_t responseSize, CommandHandler handler) { + String suffix = "commands/" + name; + return subscribe(suffix, QoS::ExactlyOnce, [this, name, suffix, responseSize, handler](const String&, const JsonObject& request) { + // Clear topic + clear(suffix, Retention::Retain, QoS::ExactlyOnce); + DynamicJsonDocument responseDoc(responseSize); + auto response = responseDoc.to(); + handler(request, response); + if (response.size() > 0) { + publish("responses/" + name, responseDoc, Retention::NoRetain, QoS::ExactlyOnce); + } + }); + } + + void registerCommand(Command& command) { + registerCommand(command.name, command.getResponseSize(), [&](const JsonObject& request, JsonObject& response) { + command.handle(request, response); + }); + } + /** * @brief Subscribes to the given topic under the topic prefix. * @@ -77,7 +102,7 @@ class MqttDriver { } MqttDriver& mqtt; - String rootTopic; + const String rootTopic; }; private: @@ -135,12 +160,8 @@ class MqttDriver { }; public: - class Config : public NamedConfigurationSection { + class Config : public ConfigurationSection { public: - Config(ConfigurationSection* parent, const String& name) - : NamedConfigurationSection(parent, name) { - } - Property host { this, "host", "" }; Property port { this, "port", 1883 }; Property clientId { this, "clientId", "" }; @@ -151,7 +172,7 @@ class MqttDriver { MqttDriver( State& networkReady, MdnsDriver& mdns, - Config& config, + const Config& config, const String& instanceName, StateSource& mqttReady) : networkReady(networkReady) @@ -169,8 +190,8 @@ class MqttDriver { }); } - MqttRoot forRoot(const String& topic) { - return MqttRoot(*this, topic); + shared_ptr forRoot(const String& topic) { + return make_shared(*this, topic); } private: @@ -332,7 +353,7 @@ class MqttDriver { State& networkReady; WiFiClient wifiClient; MdnsDriver& mdns; - Config& config; + const Config& config; const String instanceName; const String clientId; @@ -353,4 +374,5 @@ class MqttDriver { static constexpr milliseconds MQTT_QUEUE_TIMEOUT = seconds(1); static const int MQTT_BUFFER_SIZE = 2048; }; -}}} // namespace farmhub::kernel::drivers + +} // namespace farmhub::kernel::drivers diff --git a/src/kernel/drivers/OtaDriver.hpp b/src/kernel/drivers/OtaDriver.hpp index 17f30749..8dc3ba00 100644 --- a/src/kernel/drivers/OtaDriver.hpp +++ b/src/kernel/drivers/OtaDriver.hpp @@ -9,7 +9,7 @@ #include -namespace farmhub { namespace kernel { namespace drivers { +namespace farmhub::kernel::drivers { class OtaDriver { @@ -66,4 +66,4 @@ class OtaDriver { } }; -}}} // namespace farmhub::kernel::drivers +} // namespace farmhub::kernel::drivers diff --git a/src/kernel/drivers/RtcDriver.hpp b/src/kernel/drivers/RtcDriver.hpp index a93a7a35..7b3dde7d 100644 --- a/src/kernel/drivers/RtcDriver.hpp +++ b/src/kernel/drivers/RtcDriver.hpp @@ -14,7 +14,7 @@ using namespace std::chrono; -namespace farmhub { namespace kernel { namespace drivers { +namespace farmhub::kernel::drivers { /** * @brief Ensures the real-time clock is properly set up and holds a real time. @@ -29,16 +29,12 @@ namespace farmhub { namespace kernel { namespace drivers { */ class RtcDriver { public: - class Config : public NamedConfigurationSection { + class Config : public ConfigurationSection { public: - Config(ConfigurationSection* parent, const String& name) - : NamedConfigurationSection(parent, name) { - } - Property host { this, "host", "" }; }; - RtcDriver(State& networkReady, MdnsDriver& mdns, Config& ntpConfig, StateSource& rtcInSync) { + RtcDriver(State& networkReady, MdnsDriver& mdns, const Config& ntpConfig, StateSource& rtcInSync) { Task::run("rtc-check", [&rtcInSync](Task& task) { while (true) { time_t now; @@ -125,4 +121,4 @@ class RtcDriver { } }; -}}} // namespace farmhub::kernel::drivers +} // namespace farmhub::kernel::drivers diff --git a/src/kernel/drivers/WiFiDriver.hpp b/src/kernel/drivers/WiFiDriver.hpp index b29d5059..726087e6 100644 --- a/src/kernel/drivers/WiFiDriver.hpp +++ b/src/kernel/drivers/WiFiDriver.hpp @@ -13,7 +13,7 @@ using namespace farmhub::kernel; -namespace farmhub { namespace kernel { namespace drivers { +namespace farmhub::kernel::drivers { class WiFiDriver { public: @@ -84,4 +84,4 @@ class WiFiDriver { Queue reconnectQueue { "wifi-reconnect", 1 }; }; -}}} // namespace farmhub::kernel::drivers +} // namespace farmhub::kernel::drivers diff --git a/src/main.cpp b/src/main.cpp index d8fc6eb0..6dc086c5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,7 +12,8 @@ extern "C" void app_main() { new farmhub::devices::Device(); - Log.infoln("Application initialized, entering idle loop"); + Log.infoln("Device ready in %d ms, entering idle loop", + millis()); while (true) { vTaskDelay(portMAX_DELAY); diff --git a/src/peripherals/flow_control/FlowControl.hpp b/src/peripherals/flow_control/FlowControl.hpp new file mode 100644 index 00000000..0410986a --- /dev/null +++ b/src/peripherals/flow_control/FlowControl.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace farmhub::devices; +using namespace farmhub::kernel::drivers; +using namespace farmhub::peripherals::flow_meter; +using namespace farmhub::peripherals::valve; +using std::make_unique; +using std::unique_ptr; + +namespace farmhub::peripherals::flow_control { + +class FlowControlConfig + : public ValveConfig { +}; + +class FlowControl : public Peripheral { +public: + FlowControl(const String& name, shared_ptr mqttRoot, + PwmMotorDriver& controller, ValveControlStrategy& strategy, + gpio_num_t pin, double qFactor, milliseconds measurementFrequency) + : Peripheral(name, mqttRoot) + , valve(name, controller, strategy, mqttRoot) + , flowMeter(name, mqttRoot, pin, qFactor, measurementFrequency) { + } + + void configure(const FlowControlConfig& config) override { + valve.setSchedules(config.schedule.get()); + } + + void populateTelemetry(JsonObject& telemetryJson) override { + valve.populateTelemetry(telemetryJson); + flowMeter.populateTelemetry(telemetryJson); + } + +private: + ValveComponent valve; + FlowMeterComponent flowMeter; +}; + +class FlowControlDeviceConfig + : public ConfigurationSection { +public: + FlowControlDeviceConfig(ValveControlStrategyType defaultStrategy) + : valve(this, "valve", defaultStrategy) + , flowMeter(this, "flow-meter") { + } + + NamedConfigurationEntry valve; + NamedConfigurationEntry flowMeter; +}; + +class FlowControlFactory + : public PeripheralFactory { +public: + FlowControlFactory(const std::list>& motors, ValveControlStrategyType defaultStrategy) + : PeripheralFactory("flow-control", defaultStrategy) + , motors(motors) { + } + + unique_ptr> createPeripheral(const String& name, const FlowControlDeviceConfig& deviceConfig, shared_ptr mqttRoot) override { + const ValveDeviceConfig& valveConfig = deviceConfig.valve.get(); + const FlowMeterDeviceConfig& flowMeterConfig = deviceConfig.flowMeter.get(); + + PwmMotorDriver& targetMotor = findMotor(name, valveConfig.motor.get()); + ValveControlStrategy* strategy; + try { + strategy = createValveControlStrategy( + valveConfig.strategy.get(), + valveConfig.switchDuration.get(), + valveConfig.duty.get() / 100.0); + } catch (const std::exception& e) { + throw PeripheralCreationException(name, "Failed to create strategy: " + String(e.what())); + } + return make_unique( + name, + mqttRoot, + + targetMotor, + *strategy, + + flowMeterConfig.pin.get(), + flowMeterConfig.qFactor.get(), + flowMeterConfig.measurementFrequency.get()); + } + + PwmMotorDriver& findMotor(const String& name, const String& motorName) { + for (auto& motor : motors) { + if (motor.getName() == motorName) { + return motor.get(); + } + } + throw PeripheralCreationException(name, "Failed to find motor: " + motorName); + } + +private: + const std::list> motors; +}; + +} // namespace farmhub::peripherals::flow_control diff --git a/src/peripherals/flow_meter/FlowMeter.hpp b/src/peripherals/flow_meter/FlowMeter.hpp new file mode 100644 index 00000000..6b88d8fc --- /dev/null +++ b/src/peripherals/flow_meter/FlowMeter.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +using namespace farmhub::devices; +using namespace farmhub::kernel; +using namespace farmhub::kernel::drivers; +using std::make_unique; +using std::unique_ptr; +namespace farmhub::peripherals::flow_meter { + +class FlowMeter + : public Peripheral { +public: + FlowMeter(const String& name, shared_ptr mqttRoot, gpio_num_t pin, double qFactor, milliseconds measurementFrequency) + : Peripheral(name, mqttRoot) + , flowMeter(name, mqttRoot, pin, qFactor, measurementFrequency) { + } + + void populateTelemetry(JsonObject& telemetryJson) override { + flowMeter.populateTelemetry(telemetryJson); + } + +private: + FlowMeterComponent flowMeter; +}; + +class FlowMeterFactory + : public PeripheralFactory { +public: + FlowMeterFactory() + : PeripheralFactory("flow-meter") { + } + + unique_ptr> createPeripheral(const String& name, const FlowMeterDeviceConfig& deviceConfig, shared_ptr mqttRoot) override { + return make_unique(name, mqttRoot, deviceConfig.pin.get(), deviceConfig.qFactor.get(), deviceConfig.measurementFrequency.get()); + } +}; + +} // namespace farmhub::peripherals::flow_meter diff --git a/src/peripherals/flow_meter/FlowMeterComponent.hpp b/src/peripherals/flow_meter/FlowMeterComponent.hpp new file mode 100644 index 00000000..9f6429bf --- /dev/null +++ b/src/peripherals/flow_meter/FlowMeterComponent.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace farmhub::kernel::drivers; + +namespace farmhub::peripherals::flow_meter { + +class FlowMeterComponent + : public Component, + public TelemetryProvider { +public: + FlowMeterComponent( + const String& name, + shared_ptr mqttRoot, + gpio_num_t pin, + double qFactor, + milliseconds measurementFrequency) + : Component(name, mqttRoot) + , pin(pin) + , qFactor(qFactor) { + + Log.infoln("Initializing flow meter on pin %d with Q = %F", pin, qFactor); + + pinMode(pin, INPUT); + + // TODO Manage PCNT globally + pcnt_config_t pcntConfig = {}; + pcntConfig.pulse_gpio_num = pin; + pcntConfig.ctrl_gpio_num = PCNT_PIN_NOT_USED; + pcntConfig.lctrl_mode = PCNT_MODE_KEEP; + pcntConfig.hctrl_mode = PCNT_MODE_KEEP; + pcntConfig.pos_mode = PCNT_COUNT_INC; + pcntConfig.neg_mode = PCNT_COUNT_DIS; + pcntConfig.unit = PCNT_UNIT_0; + pcntConfig.channel = PCNT_CHANNEL_0; + + pcnt_unit_config(&pcntConfig); + pcnt_intr_disable(PCNT_UNIT_0); + pcnt_set_filter_value(PCNT_UNIT_0, 1023); + pcnt_filter_enable(PCNT_UNIT_0); + pcnt_counter_clear(PCNT_UNIT_0); + + auto now = boot_clock::now(); + lastMeasurement = now; + lastSeenFlow = now; + lastPublished = now; + + Task::loop(name, 2048, [this, measurementFrequency](Task& task) { + auto now = boot_clock::now(); + milliseconds elapsed = duration_cast(now - lastMeasurement); + if (elapsed.count() > 0) { + lastMeasurement = now; + + int16_t pulses; + pcnt_get_counter_value(PCNT_UNIT_0, &pulses); + + if (pulses > 0) { + pcnt_counter_clear(PCNT_UNIT_0); + updateMutex.lock(); + double currentVolume = pulses / this->qFactor / 60.0f; + Log.verboseln("Counted %d pulses, %F l/min, %F l", + pulses, currentVolume / (elapsed.count() / 1000.0f / 60.0f), currentVolume); + volume += currentVolume; + lastSeenFlow = now; + updateMutex.unlock(); + } + } + task.delayUntil(measurementFrequency); + }); + } + + virtual ~FlowMeterComponent() = default; + + void populateTelemetry(JsonObject& json) override { + updateMutex.lock(); + pupulateTelemetryUnderLock(json); + updateMutex.unlock(); + } + +private: + void inline pupulateTelemetryUnderLock(JsonObject& json) { + auto currentVolume = volume; + volume = 0; + // Volume is measured in liters + json["volume"] = currentVolume; + auto duration = duration_cast(lastMeasurement - lastPublished); + if (duration > microseconds::zero()) { + // Flow rate is measured in in liters / min + json["flowRate"] = currentVolume / duration.count() * 1000 * 1000 * 60; + } + lastPublished = lastMeasurement; + } + + const gpio_num_t pin; + const double qFactor; + + time_point lastMeasurement; + time_point lastSeenFlow; + time_point lastPublished; + double volume = 0.0; + + Mutex updateMutex; +}; + +} // namespace farmhub::peripherals::flow_meter diff --git a/src/peripherals/flow_meter/FlowMeterConfig.hpp b/src/peripherals/flow_meter/FlowMeterConfig.hpp new file mode 100644 index 00000000..09c4a833 --- /dev/null +++ b/src/peripherals/flow_meter/FlowMeterConfig.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include + +using namespace std::chrono; +using namespace farmhub::kernel; + +namespace farmhub::peripherals::flow_meter { + +class FlowMeterDeviceConfig + : public ConfigurationSection { +public: + Property pin { this, "pin", GPIO_NUM_NC }; + Property qFactor { this, "qFactor", 5.0 }; + Property measurementFrequency { this, "measurementFrequency", milliseconds(1000) }; +}; + +} diff --git a/src/peripherals/valve/Valve.hpp b/src/peripherals/valve/Valve.hpp index 534c44af..cb9e4443 100644 --- a/src/peripherals/valve/Valve.hpp +++ b/src/peripherals/valve/Valve.hpp @@ -15,6 +15,8 @@ #include #include +#include +#include #include using namespace std::chrono; @@ -25,357 +27,61 @@ using std::unique_ptr; using namespace farmhub::devices; using namespace farmhub::kernel::drivers; -namespace farmhub { namespace peripherals { namespace valve { - -enum class ValveControlStrategyType { - NormallyOpen, - NormallyClosed, - Latching -}; - -class ValveControlStrategy { -public: - virtual void open(PwmMotorDriver& controller) = 0; - virtual void close(PwmMotorDriver& controller) = 0; - virtual ValveState getDefaultState() = 0; - - virtual String describe() = 0; -}; - -class HoldingValveControlStrategy - : public ValveControlStrategy { - -public: - HoldingValveControlStrategy(milliseconds switchDuration, double holdDuty) - : switchDuration(switchDuration) - , holdDuty(holdDuty) { - } - -protected: - void driveAndHold(PwmMotorDriver& controller, ValveState targetState) { - switch (targetState) { - case ValveState::OPEN: - controller.drive(MotorPhase::FORWARD, holdDuty); - break; - case ValveState::CLOSED: - controller.drive(MotorPhase::REVERSE, holdDuty); - break; - default: - // Ignore - break; - } - delay(switchDuration.count()); - controller.stop(); - } - - const milliseconds switchDuration; - const double holdDuty; -}; - -class NormallyClosedValveControlStrategy - : public HoldingValveControlStrategy { -public: - NormallyClosedValveControlStrategy(milliseconds switchDuration, double holdDuty) - : HoldingValveControlStrategy(switchDuration, holdDuty) { - } - - void open(PwmMotorDriver& controller) override { - driveAndHold(controller, ValveState::OPEN); - } - - void close(PwmMotorDriver& controller) override { - controller.stop(); - } - - ValveState getDefaultState() override { - return ValveState::CLOSED; - } - - String describe() override { - return "normally closed with switch duration " + String((int) switchDuration.count()) + "ms and hold duty " + String(holdDuty * 100) + "%"; - } -}; - -class NormallyOpenValveControlStrategy - : public HoldingValveControlStrategy { -public: - NormallyOpenValveControlStrategy(milliseconds switchDuration, double holdDuty) - : HoldingValveControlStrategy(switchDuration, holdDuty) { - } - - void open(PwmMotorDriver& controller) override { - controller.stop(); - } - - void close(PwmMotorDriver& controller) override { - driveAndHold(controller, ValveState::CLOSED); - } - - ValveState getDefaultState() override { - return ValveState::OPEN; - } - - String describe() override { - return "normally open with switch duration " + String((int) switchDuration.count()) + "ms and hold duty " + String(holdDuty * 100) + "%"; - } -}; - -class LatchingValveControlStrategy - : public ValveControlStrategy { -public: - LatchingValveControlStrategy(milliseconds switchDuration, double switchDuty = 1.0) - : switchDuration(switchDuration) - , switchDuty(switchDuty) { - } - - void open(PwmMotorDriver& controller) override { - controller.drive(MotorPhase::FORWARD, switchDuty); - delay(switchDuration.count()); - controller.stop(); - } - - void close(PwmMotorDriver& controller) override { - controller.drive(MotorPhase::REVERSE, switchDuty); - delay(switchDuration.count()); - controller.stop(); - } - - ValveState getDefaultState() override { - return ValveState::NONE; - } - - String describe() override { - return "latching with switch duration " + String((int) switchDuration.count()) + "ms with switch duty " + String(switchDuty * 100) + "%"; - } - -private: - const milliseconds switchDuration; - const double switchDuty; -}; - -class ValveConfig - : public ConfigurationSection { -public: - Property frequency { this, "frequency", seconds(15000) }; - ArrayProperty schedule { this, "schedule" }; -}; +namespace farmhub::peripherals::valve { class Valve : public Peripheral { public: - Valve(const String& name, PwmMotorDriver& controller, ValveControlStrategy& strategy, MqttDriver::MqttRoot mqttRoot) + Valve(const String& name, PwmMotorDriver& controller, ValveControlStrategy& strategy, shared_ptr mqttRoot) : Peripheral(name, mqttRoot) - , controller(controller) - , strategy(strategy) { - Log.infoln("Creating valve '%s' with strategy %s", - name.c_str(), strategy.describe().c_str()); - - controller.stop(); - - // TODO Restore stored state? - - Task::loop(name, 3072, [this](Task& task) { - auto now = system_clock::now(); - auto update = ValveScheduler::getStateUpdate(schedules, now, this->strategy.getDefaultState()); - Log.traceln("Valve '%s' state is %d, will change after %d ms", - this->name.c_str(), static_cast(update.state), update.transitionAfter.count()); - setState(update.state); - task.delayUntil(update.transitionAfter); - }); + , valve(name, controller, strategy, mqttRoot) { } void configure(const ValveConfig& config) override { - Log.infoln("Configuring valve '%s' with frequency %d", - name.c_str(), config.frequency.get().count()); - // TODO Do this thread safe? - schedules = std::list(config.schedule.get()); - // TODO Notify the task to reevaluate the schedule - } - - void open() { - Log.traceln("Opening valve"); - strategy.open(controller); - this->state = ValveState::OPEN; + valve.setSchedules(config.schedule.get()); } - void close() { - Log.traceln("Closing valve"); - strategy.close(controller); - this->state = ValveState::CLOSED; - } - - void reset() { - Log.traceln("Resetting valve"); - controller.stop(); - } - - void setState(ValveState state) { - switch (state) { - case ValveState::OPEN: - open(); - break; - case ValveState::CLOSED: - close(); - break; - default: - // Ignore - break; - } - // TODO Publish event - } - - void populateTelemetry(JsonObject& telemetry) { - telemetry["state"] = this->state; + void populateTelemetry(JsonObject& telemetry) override { + valve.populateTelemetry(telemetry); } private: - PwmMotorDriver& controller; - ValveControlStrategy& strategy; - - ValveState state = ValveState::NONE; - TaskHandle* task = nullptr; - std::list schedules; -}; - -class ValveDeviceConfig - : public ConfigurationSection { -public: - ValveDeviceConfig(ValveControlStrategyType defaultStrategy) - : strategy(this, "strategy", defaultStrategy) { - } - - Property motor { this, "motor" }; - Property strategy; - Property duty { this, "duty", 100 }; - Property switchDuration { this, "switchDuration", milliseconds(500) }; + ValveComponent valve; }; class ValveFactory - : public PeripheralFactory { + : public PeripheralFactory { public: ValveFactory(const std::list>& motors, ValveControlStrategyType defaultStrategy) - : PeripheralFactory("valve") - , motors(motors) - , defaultStrategy(defaultStrategy) { - } - - ValveDeviceConfig* createDeviceConfig() override { - return new ValveDeviceConfig(defaultStrategy); + : PeripheralFactory("valve", defaultStrategy) + , motors(motors) { + } + + unique_ptr> createPeripheral(const String& name, const ValveDeviceConfig& deviceConfig, shared_ptr mqttRoot) override { + PwmMotorDriver& targetMotor = findMotor(name, deviceConfig.motor.get()); + ValveControlStrategy* strategy; + try { + strategy = createValveControlStrategy( + deviceConfig.strategy.get(), + deviceConfig.switchDuration.get(), + deviceConfig.duty.get() / 100.0); + } catch (const std::exception& e) { + throw PeripheralCreationException(name, "Failed to create strategy: " + String(e.what())); + } + return make_unique(name, targetMotor, *strategy, mqttRoot); } - Valve* createPeripheral(const String& name, const ValveDeviceConfig& deviceConfig, MqttDriver::MqttRoot mqttRoot) override { - PwmMotorDriver* targetMotor = nullptr; + PwmMotorDriver& findMotor(const String& name, const String& motorName) { for (auto& motor : motors) { - if (motor.getName() == deviceConfig.motor.get()) { - targetMotor = &(motor.get()); - break; + if (motor.getName() == motorName) { + return motor.get(); } } - if (targetMotor == nullptr) { - // TODO Add proper error handling - Log.errorln("Failed to find motor: %s", - deviceConfig.motor.get().c_str()); - return nullptr; - } - ValveControlStrategy* strategy = createStrategy(deviceConfig); - if (strategy == nullptr) { - // TODO Add proper error handling - Log.errorln("Failed to create strategy"); - return nullptr; - } - return new Valve(name, *targetMotor, *strategy, mqttRoot); + throw PeripheralCreationException(name, "Failed to find motor: " + motorName); } private: - ValveControlStrategy* createStrategy(const ValveDeviceConfig& config) { - auto switchDuration = config.switchDuration.get(); - auto duty = config.duty.get() / 100.0; - switch (config.strategy.get()) { - case ValveControlStrategyType::NormallyOpen: - return new NormallyOpenValveControlStrategy(switchDuration, duty); - case ValveControlStrategyType::NormallyClosed: - return new NormallyClosedValveControlStrategy(switchDuration, duty); - case ValveControlStrategyType::Latching: - return new LatchingValveControlStrategy(switchDuration, duty); - default: - // TODO Add proper error handling - return nullptr; - } - } - const std::list> motors; - const ValveControlStrategyType defaultStrategy; -}; - -// JSON: ValveState - -bool convertToJson(const ValveState& src, JsonVariant dst) { - return dst.set(static_cast(src)); -} -void convertFromJson(JsonVariantConst src, ValveState& dst) { - dst = static_cast(src.as()); -} - -// JSON: ValveControlStrategyType - -bool convertToJson(const ValveControlStrategyType& src, JsonVariant dst) { - switch (src) { - case ValveControlStrategyType::NormallyOpen: - return dst.set("NO"); - case ValveControlStrategyType::NormallyClosed: - return dst.set("NC"); - case ValveControlStrategyType::Latching: - return dst.set("latching"); - default: - Log.errorln("Unknown strategy: %d", - static_cast(src)); - return dst.set("NC"); - } -} -void convertFromJson(JsonVariantConst src, ValveControlStrategyType& dst) { - String strategy = src.as(); - if (strategy == "NO") { - dst = ValveControlStrategyType::NormallyOpen; - } else if (strategy == "NC") { - dst = ValveControlStrategyType::NormallyClosed; - } else if (strategy == "latching") { - dst = ValveControlStrategyType::Latching; - } else { - Log.errorln("Unknown strategy: %s", - strategy.c_str()); - dst = ValveControlStrategyType::NormallyClosed; - } -} - -}}} // namespace farmhub::peripherals::valve - -namespace ArduinoJson { - -using farmhub::peripherals::valve::ValveSchedule; -template <> -struct Converter { - static void toJson(const ValveSchedule& src, JsonVariant dst) { - JsonObject obj = dst.to(); - char buf[64]; - strftime(buf, sizeof(buf), "%FT%TZ", &src.getStart()); - obj["start"] = buf; - obj["period"] = src.getPeriod().count(); - obj["duration"] = src.getDuration().count(); - } - - static ValveSchedule fromJson(JsonVariantConst src) { - tm start; - strptime(src["start"].as(), "%FT%TZ", &start); - seconds period = seconds(src["period"].as()); - seconds duration = seconds(src["duration"].as()); - return ValveSchedule(start, period, duration); - } - - static bool checkJson(JsonVariantConst src) { - return src["start"].is() - && src["period"].is() - && src["duration"].is(); - } }; -} // namespace ArduinoJson +} // namespace farmhub::peripherals::valve diff --git a/src/peripherals/valve/ValveComponent.hpp b/src/peripherals/valve/ValveComponent.hpp new file mode 100644 index 00000000..5d457223 --- /dev/null +++ b/src/peripherals/valve/ValveComponent.hpp @@ -0,0 +1,283 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace std::chrono; +using std::make_unique; +using std::move; +using std::unique_ptr; + +using namespace farmhub::devices; +using namespace farmhub::kernel::drivers; + +namespace farmhub::peripherals::valve { + +class ValveControlStrategy { +public: + virtual void open(PwmMotorDriver& controller) = 0; + virtual void close(PwmMotorDriver& controller) = 0; + virtual ValveState getDefaultState() const = 0; + + virtual String describe() const = 0; +}; + +class HoldingValveControlStrategy + : public ValveControlStrategy { + +public: + HoldingValveControlStrategy(milliseconds switchDuration, double holdDuty) + : switchDuration(switchDuration) + , holdDuty(holdDuty) { + } + +protected: + void driveAndHold(PwmMotorDriver& controller, ValveState targetState) { + switch (targetState) { + case ValveState::OPEN: + controller.drive(MotorPhase::FORWARD, holdDuty); + break; + case ValveState::CLOSED: + controller.drive(MotorPhase::REVERSE, holdDuty); + break; + default: + // Ignore + break; + } + delay(switchDuration.count()); + controller.stop(); + } + + const milliseconds switchDuration; + const double holdDuty; +}; + +class NormallyClosedValveControlStrategy + : public HoldingValveControlStrategy { +public: + NormallyClosedValveControlStrategy(milliseconds switchDuration, double holdDuty) + : HoldingValveControlStrategy(switchDuration, holdDuty) { + } + + void open(PwmMotorDriver& controller) override { + driveAndHold(controller, ValveState::OPEN); + } + + void close(PwmMotorDriver& controller) override { + controller.stop(); + } + + ValveState getDefaultState() const override { + return ValveState::CLOSED; + } + + String describe() const override { + return "normally closed with switch duration " + String((int) switchDuration.count()) + "ms and hold duty " + String(holdDuty * 100) + "%"; + } +}; + +class NormallyOpenValveControlStrategy + : public HoldingValveControlStrategy { +public: + NormallyOpenValveControlStrategy(milliseconds switchDuration, double holdDuty) + : HoldingValveControlStrategy(switchDuration, holdDuty) { + } + + void open(PwmMotorDriver& controller) override { + controller.stop(); + } + + void close(PwmMotorDriver& controller) override { + driveAndHold(controller, ValveState::CLOSED); + } + + ValveState getDefaultState() const override { + return ValveState::OPEN; + } + + String describe() const override { + return "normally open with switch duration " + String((int) switchDuration.count()) + "ms and hold duty " + String(holdDuty * 100) + "%"; + } +}; + +class LatchingValveControlStrategy + : public ValveControlStrategy { +public: + LatchingValveControlStrategy(milliseconds switchDuration, double switchDuty = 1.0) + : switchDuration(switchDuration) + , switchDuty(switchDuty) { + } + + void open(PwmMotorDriver& controller) override { + controller.drive(MotorPhase::FORWARD, switchDuty); + delay(switchDuration.count()); + controller.stop(); + } + + void close(PwmMotorDriver& controller) override { + controller.drive(MotorPhase::REVERSE, switchDuty); + delay(switchDuration.count()); + controller.stop(); + } + + ValveState getDefaultState() const override { + return ValveState::NONE; + } + + String describe() const override { + return "latching with switch duration " + String((int) switchDuration.count()) + "ms with switch duty " + String(switchDuty * 100) + "%"; + } + +private: + const milliseconds switchDuration; + const double switchDuty; +}; + +class ValveComponent : public Component { +public: + ValveComponent(const String& name, PwmMotorDriver& controller, ValveControlStrategy& strategy, shared_ptr mqttRoot) + : Component(name, mqttRoot) + , controller(controller) + , strategy(strategy) { + Log.infoln("Creating valve '%s' with strategy %s", + name.c_str(), strategy.describe().c_str()); + + controller.stop(); + + // TODO Restore stored state? + + mqttRoot->registerCommand("override", [this](const JsonObject& request, JsonObject& response) { + ValveState targetState = request["state"].as(); + if (targetState == ValveState::NONE) { + override(ValveState::NONE, time_point()); + } else { + seconds duration = request.containsKey("duration") + ? request["duration"].as() + : hours { 1 }; + override(targetState, system_clock::now() + duration); + response["duration"] = duration; + } + response["state"] = state; + }); + + Task::loop(name, 3072, [this, name](Task& task) { + auto now = system_clock::now(); + if (overrideState != ValveState::NONE && now > overrideUntil) { + Log.traceln("Valve '%s' override expired", name.c_str()); + overrideUntil = time_point(); + overrideState = ValveState::NONE; + } + ValveStateUpdate update; + if (overrideState != ValveState::NONE) { + update = { overrideState, duration_cast(overrideUntil - now) }; + Log.traceln("Valve '%s' override state is %d, will change after %F sec", + name.c_str(), static_cast(update.state), update.transitionAfter.count() / 1000.0); + } else { + update = ValveScheduler::getStateUpdate(schedules, now, this->strategy.getDefaultState()); + Log.traceln("Valve '%s' state is %d, will change after %F s", + name.c_str(), static_cast(update.state), update.transitionAfter.count() / 1000.0); + } + setState(update.state); + // TODO Account for time spent in setState() + updateQueue.pollIn(update.transitionAfter, [this](const std::variant& change) { + std::visit([this](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + overrideState = arg.state; + overrideUntil = arg.until; + } else if constexpr (std::is_same_v) { + schedules = std::list(arg.schedules); + } + }, + change); + }); + }); + } + + void setSchedules(const std::list& schedules) { + Log.traceln("Setting %d schedules for valve %s", + schedules.size(), name.c_str()); + updateQueue.put(ScheduleSpec { schedules }); + } + + void populateTelemetry(JsonObject& telemetry) { + telemetry["state"] = this->state; + } + +private: + void override(ValveState state, time_point until) { + updateQueue.put(OverrideSpec { state, until }); + } + + void open() { + Log.traceln("Opening valve %s", name.c_str()); + strategy.open(controller); + this->state = ValveState::OPEN; + } + + void close() { + Log.traceln("Closing valve"); + strategy.close(controller); + this->state = ValveState::CLOSED; + } + + void reset() { + Log.traceln("Resetting valve"); + controller.stop(); + } + + void setState(ValveState state) { + switch (state) { + case ValveState::OPEN: + open(); + break; + case ValveState::CLOSED: + close(); + break; + default: + // Ignore + break; + } + // TODO Publish event + } + + PwmMotorDriver& controller; + ValveControlStrategy& strategy; + + ValveState state = ValveState::NONE; + + struct OverrideSpec { + public: + ValveState state; + time_point until; + }; + + struct ScheduleSpec { + public: + std::list schedules; + }; + + std::list schedules; + ValveState overrideState = ValveState::NONE; + time_point overrideUntil; + Queue> updateQueue { "eventQueue", 1 }; +}; + +} // namespace farmhub::peripherals::valve diff --git a/src/peripherals/valve/ValveConfig.hpp b/src/peripherals/valve/ValveConfig.hpp new file mode 100644 index 00000000..363e6e77 --- /dev/null +++ b/src/peripherals/valve/ValveConfig.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include +#include + +#include +#include + +using namespace farmhub::kernel; + +namespace farmhub::peripherals::valve { + +enum class ValveControlStrategyType { + NormallyOpen, + NormallyClosed, + Latching +}; + +static ValveControlStrategy* createValveControlStrategy(ValveControlStrategyType strategy, milliseconds switchDuration, double duty) { + switch (strategy) { + case ValveControlStrategyType::NormallyOpen: + return new NormallyOpenValveControlStrategy(switchDuration, duty); + case ValveControlStrategyType::NormallyClosed: + return new NormallyClosedValveControlStrategy(switchDuration, duty); + case ValveControlStrategyType::Latching: + return new LatchingValveControlStrategy(switchDuration, duty); + default: + throw std::runtime_error("Unknown strategy"); + } +} + +class ValveConfig + : public ConfigurationSection { +public: + ArrayProperty schedule { this, "schedule" }; +}; + +class ValveDeviceConfig + : public ConfigurationSection { +public: + ValveDeviceConfig(ValveControlStrategyType defaultStrategy) + : strategy(this, "strategy", defaultStrategy) { + } + + Property motor { this, "motor" }; + Property strategy; + Property duty { this, "duty", 100 }; + Property switchDuration { this, "switchDuration", milliseconds(500) }; +}; + +// JSON: ValveState + +bool convertToJson(const ValveState& src, JsonVariant dst) { + return dst.set(static_cast(src)); +} +void convertFromJson(JsonVariantConst src, ValveState& dst) { + dst = static_cast(src.as()); +} + +// JSON: ValveControlStrategyType + +bool convertToJson(const ValveControlStrategyType& src, JsonVariant dst) { + switch (src) { + case ValveControlStrategyType::NormallyOpen: + return dst.set("NO"); + case ValveControlStrategyType::NormallyClosed: + return dst.set("NC"); + case ValveControlStrategyType::Latching: + return dst.set("latching"); + default: + Log.errorln("Unknown strategy: %d", + static_cast(src)); + return dst.set("NC"); + } +} +void convertFromJson(JsonVariantConst src, ValveControlStrategyType& dst) { + String strategy = src.as(); + if (strategy == "NO") { + dst = ValveControlStrategyType::NormallyOpen; + } else if (strategy == "NC") { + dst = ValveControlStrategyType::NormallyClosed; + } else if (strategy == "latching") { + dst = ValveControlStrategyType::Latching; + } else { + Log.errorln("Unknown strategy: %s", + strategy.c_str()); + dst = ValveControlStrategyType::NormallyClosed; + } +} + +} // namespace farmhub::peripherals::valve + +namespace ArduinoJson { + +using farmhub::peripherals::valve::ValveSchedule; +template <> +struct Converter { + static void toJson(const ValveSchedule& src, JsonVariant dst) { + JsonObject obj = dst.to(); + auto startLocalTime = src.getStart(); + auto startTime = system_clock::to_time_t(startLocalTime); + tm startTm; + localtime_r(&startTime, &startTm); + char buf[64]; + strftime(buf, sizeof(buf), "%FT%TZ", &startTm); + obj["start"] = buf; + obj["period"] = src.getPeriod().count(); + obj["duration"] = src.getDuration().count(); + } + + static ValveSchedule fromJson(JsonVariantConst src) { + tm startTm; + strptime(src["start"].as(), "%FT%TZ", &startTm); + auto startTime = mktime(&startTm); + auto startLocalTime = system_clock::from_time_t(startTime); + seconds period = seconds(src["period"].as()); + seconds duration = seconds(src["duration"].as()); + return ValveSchedule(startLocalTime, period, duration); + } + + static bool checkJson(JsonVariantConst src) { + return src["start"].is() + && src["period"].is() + && src["duration"].is(); + } +}; + +} // namespace ArduinoJson diff --git a/src/peripherals/valve/ValveScheduler.hpp b/src/peripherals/valve/ValveScheduler.hpp index 613ec24e..ef875d1c 100644 --- a/src/peripherals/valve/ValveScheduler.hpp +++ b/src/peripherals/valve/ValveScheduler.hpp @@ -1,10 +1,14 @@ #pragma once #include +#include + +#include using namespace std::chrono; +using namespace farmhub::kernel; -namespace farmhub { namespace peripherals { namespace valve { +namespace farmhub::peripherals::valve { enum class ValveState { CLOSED = -1, @@ -15,7 +19,7 @@ enum class ValveState { class ValveSchedule { public: ValveSchedule( - const tm& start, + time_point start, seconds period, seconds duration) : start(start) @@ -23,18 +27,19 @@ class ValveSchedule { , duration(duration) { } - const tm& getStart() const { + time_point getStart() const { return start; } + seconds getPeriod() const { return period; } + seconds getDuration() const { return duration; } -private: - const tm start; + const time_point start; const seconds period; const seconds duration; }; @@ -49,18 +54,15 @@ class ValveScheduler { static ValveStateUpdate getStateUpdate(std::list schedules, time_point now, ValveState defaultState) { ValveStateUpdate next = { defaultState, ticks::max() }; for (auto& schedule : schedules) { - tm scheduleTm = schedule.getStart(); - auto scheduleStart = mktime(&scheduleTm); - auto scheduleStartLocalTime = system_clock::from_time_t(scheduleStart); - + auto start = schedule.getStart(); auto period = schedule.getPeriod(); auto duration = schedule.getDuration(); - if (scheduleStartLocalTime > now) { + if (start > now) { continue; } - auto diff = duration_cast(now - scheduleStartLocalTime); + auto diff = duration_cast(now - start); auto periodPosition = diff % period; if (periodPosition < duration) { @@ -83,4 +85,4 @@ class ValveScheduler { } }; -}}} // namespace farmhub::peripherals::valve +} // namespace farmhub::peripherals::valve diff --git a/test/ValveSchedulerTest.cpp b/test/ValveSchedulerTest.cpp new file mode 100644 index 00000000..13851943 --- /dev/null +++ b/test/ValveSchedulerTest.cpp @@ -0,0 +1,103 @@ +#include + +// TODO Move this someplace else? +#define configTICK_RATE_HZ 1000 + +#include +#include +#include +#include +#include + +#include + +#include + +using namespace std::chrono; +using namespace farmhub::peripherals::valve; + +static time_point parseTime(const char* str) { + tm time; + std::istringstream ss(str); + ss >> std::get_time(&time, "%Y-%m-%d %H:%M:%S"); + return system_clock::from_time_t(mktime(&time)); +} + +class ValveSchedulerTest : public testing::Test { +public: + time_point base = parseTime("2024-01-01 00:00:00"); + ValveScheduler scheduler; +}; + +TEST_F(ValveSchedulerTest, can_create_schedule) { + ValveSchedule schedule(base, hours { 1 }, minutes { 1 }); + EXPECT_EQ(schedule.start, base); + EXPECT_EQ(schedule.period, hours { 1 }); + EXPECT_EQ(schedule.duration, minutes { 1 }); +} + +// TEST_F(ValveSchedulerTest, can_create_schedule_from_json) { +// DynamicJsonDocument doc(2048); +// deserializeJson(doc, R"({ +// "start": "2020-01-01T00:00:00Z", +// "period": 60, +// "duration": 15 +// })"); +// ValveSchedule schedule(doc.as()); +// EXPECT_EQ(schedule.start, time_point { system_clock::from_time_t(1577836800) }); +// EXPECT_EQ(schedule.period, minutes { 1 }); +// EXPECT_EQ(schedule.duration, seconds { 15 }); +// } + +// TEST_F(ValveSchedulerTest, not_scheduled_when_empty) { +// EXPECT_FALSE(scheduler.isScheduled({}, base)); +// } + +// TEST_F(ValveSchedulerTest, matches_single_schedule) { +// std::list schedules { +// ValveSchedule(base, minutes { 1 }, seconds { 15 }), +// }; +// EXPECT_TRUE(scheduler.isScheduled(schedules, base)); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + seconds { 1 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + seconds { 14 })); +// EXPECT_FALSE(scheduler.isScheduled(schedules, base + seconds { 15 })); +// EXPECT_FALSE(scheduler.isScheduled(schedules, base + seconds { 30 })); +// EXPECT_FALSE(scheduler.isScheduled(schedules, base + seconds { 59 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + seconds { 60 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + seconds { 74 })); +// EXPECT_FALSE(scheduler.isScheduled(schedules, base + seconds { 75 })); +// } + +// TEST_F(ValveSchedulerTest, does_not_match_schedule_not_yet_started) { +// std::list schedules { +// ValveSchedule(base, minutes { 1 }, minutes { 1 }), +// }; +// EXPECT_FALSE(scheduler.isScheduled(schedules, base - seconds { 1 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base)); +// } + +// TEST_F(ValveSchedulerTest, matches_multiple_schedules) { +// std::list schedules { +// ValveSchedule(base, minutes { 1 }, seconds { 15 }), +// ValveSchedule(base, minutes { 5 }, seconds { 60 }), +// }; +// EXPECT_TRUE(scheduler.isScheduled(schedules, base)); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + seconds { 1 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + seconds { 14 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + seconds { 15 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + seconds { 30 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + seconds { 59 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + seconds { 60 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + seconds { 74 })); +// EXPECT_FALSE(scheduler.isScheduled(schedules, base + seconds { 75 })); + +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + minutes { 2 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + minutes { 2 } + seconds { 1 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + minutes { 2 } + seconds { 14 })); +// EXPECT_FALSE(scheduler.isScheduled(schedules, base + minutes { 2 } + seconds { 15 })); +// EXPECT_FALSE(scheduler.isScheduled(schedules, base + minutes { 2 } + seconds { 30 })); +// EXPECT_FALSE(scheduler.isScheduled(schedules, base + minutes { 2 } + seconds { 59 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + minutes { 2 } + seconds { 60 })); +// EXPECT_TRUE(scheduler.isScheduled(schedules, base + minutes { 2 } + seconds { 74 })); +// EXPECT_FALSE(scheduler.isScheduled(schedules, base + minutes { 2 } + seconds { 75 })); +// } diff --git a/test/main.cpp b/test/main.cpp new file mode 100644 index 00000000..d19e96c2 --- /dev/null +++ b/test/main.cpp @@ -0,0 +1,35 @@ +#include + +#if defined(ARDUINO) +#include + +void setup() +{ + // should be the same value as for the `test_speed` option in "platformio.ini" + // default value is test_speed=115200 + Serial.begin(115200); + + // give the 1-2 seconds to the test runner to connect to the board + delay(1000); + + ::testing::InitGoogleTest(); + // if you plan to use GMock, replace the line above with + // ::testing::InitGoogleMock(&argc, argv); + if (RUN_ALL_TESTS()); +} + +void loop() +{ + // nothing to be done here. + delay(100); +} + +#else +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + // if you plan to use GMock, replace the line above with + // ::testing::InitGoogleMock(&argc, argv); + return RUN_ALL_TESTS(); +} +#endif