diff --git a/.gitmodules b/.gitmodules index 7aa51f79..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,24 +0,0 @@ -[submodule "components/SHT31"] - path = components/SHT31 - url = https://github.com/RobTillaart/SHT31 -[submodule "components/SHT2x"] - path = components/SHT2x - url = https://github.com/RobTillaart/SHT2x -[submodule "components/OneWire"] - path = components/OneWire - url = https://github.com/PaulStoffregen/OneWire -[submodule "components/DallasTemperature"] - path = components/DallasTemperature - url = https://github.com/milesburton/Arduino-Temperature-Control-Library -[submodule "components/BH1750"] - path = components/BH1750 - url = https://github.com/claws/BH1750 -[submodule "components/Adafruit_TSL2591_Library"] - path = components/Adafruit_TSL2591_Library - url = https://github.com/adafruit/Adafruit_TSL2591_Library -[submodule "components/Adafruit_Sensor"] - path = components/Adafruit_Sensor - url = https://github.com/adafruit/Adafruit_Sensor -[submodule "components/Adafruit_BusIO"] - path = components/Adafruit_BusIO - url = https://github.com/adafruit/Adafruit_BusIO diff --git a/CMakeLists.txt b/CMakeLists.txt index 6109f478..2254072e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,17 @@ cmake_minimum_required(VERSION 3.16.0) + +# Add esp-idf-lib components +include(FetchContent) +FetchContent_Declare( + espidflib + GIT_REPOSITORY https://github.com/UncleRus/esp-idf-lib.git + GIT_TAG "0.9.4" +) +FetchContent_MakeAvailable(espidflib) +set(EXTRA_COMPONENT_DIRS ${espidflib_SOURCE_DIR}/components) + + if(NOT DEFINED UD_GEN) set(UD_GEN "$ENV{UD_GEN}") endif() @@ -40,33 +52,19 @@ set_property(DIRECTORY PROPERTY UD_DEBUG_TRACKER "${UD_DEBUG}") if (UD_DEBUG) message("Building with debug options") set(SDKCONFIG_DEFAULTS "sdkconfig.defaults;sdkconfig.${UD_GEN_LOWER}.defaults;sdkconfig.debug.defaults") + set(CMAKE_BUILD_TYPE Debug) else() message("Building with release options") set(SDKCONFIG_DEFAULTS "sdkconfig.defaults;sdkconfig.${UD_GEN_LOWER}.defaults") - add_compile_options("-O2") + set(CMAKE_BUILD_TYPE Release) endif() +add_link_options("-Wl,--gc-sections") include($ENV{IDF_PATH}/tools/cmake/project.cmake) -idf_build_set_property(DEPENDENCIES_LOCK "dependencies.lock") - -# list(APPEND EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/components) -list(APPEND EXTRA_COMPONENT_DIRS components) - -# Adds necessary definitions for compiling it using Serial symbol attached to the HW USB CDC port -add_compile_definitions(ARDUINO_USB_CDC_ON_BOOT=1) -add_compile_definitions(ARDUINO_USB_MODE=1) - -if(UD_GEN STREQUAL MK4) - add_compile_definitions(HWCDCSerial=Serial0) -endif() - # Keep struct initializers simple add_compile_options("-Wno-missing-field-initializers") -# TODO Make this depend on some flag -set(CMAKE_BUILD_TYPE Debug) - project(ugly-duckling) # Use `idf.py -DFSUPLOAD=1 flash` to upload the data partition diff --git a/README.md b/README.md index 31321cbe..17113820 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Ugly Duckling is a firmware for IoT devices participating in the FarmHub ecosystem. -The devices are built around the Espressif ESP32 micro-controller using FreeRTOS and the Arduino framework. +The devices are built around the Espressif ESP32 micro-controller using the ESP-IDF framework and FreeRTOS. The devices can report telemetry to, and receive configuration and commands from the FarmHub server via MQTT over WiFi. They can also receive firmware updates via HTTP(S). @@ -150,8 +150,6 @@ See `FileCommands` for more information. - ESP-IDF v5.3.2 (see [installation instructions](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/index.html)) -We are using this version because it is the latest version that is compatible with Arduino-ESP32 3.1.0-rc-1. - ### Building There are two ways to build the firmware: diff --git a/components/Adafruit_BusIO b/components/Adafruit_BusIO deleted file mode 160000 index 15fbda59..00000000 --- a/components/Adafruit_BusIO +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 15fbda592d1b237b0a92cfb91841adb01a34efd9 diff --git a/components/Adafruit_Sensor b/components/Adafruit_Sensor deleted file mode 160000 index 7b2473b6..00000000 --- a/components/Adafruit_Sensor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7b2473b6b24ae340f41685b5f5b2b90ad896db04 diff --git a/components/Adafruit_TSL2591_Library b/components/Adafruit_TSL2591_Library deleted file mode 160000 index a857bf93..00000000 --- a/components/Adafruit_TSL2591_Library +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a857bf93c10b5d60d02928c7a6c364e67ff5e0f3 diff --git a/components/BH1750 b/components/BH1750 deleted file mode 160000 index b6986b55..00000000 --- a/components/BH1750 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b6986b553faed246ee267801ca23ccaea038d7cc diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt deleted file mode 100644 index f43754ba..00000000 --- a/components/CMakeLists.txt +++ /dev/null @@ -1,31 +0,0 @@ -FILE(GLOB_RECURSE adafruit_busio_sources Adafruit_BusIO/*.cpp) -FILE(GLOB_RECURSE mqtt_sources MQTTPubSubClient/src/MQTTPubSubClient/*.*) -idf_component_register( - SRCS ${adafruit_busio_sources} - INCLUDE_DIRS "Adafruit_BusIO" - - SRCS "Adafruit_Sensor/Adafruit_Sensor.cpp" - INCLUDE_DIRS "Adafruit_Sensor" - - SRCS "Adafruit_TSL2591_Library/Adafruit_TSL2591.cpp" - INCLUDE_DIRS "Adafruit_TSL2591_Library" - - SRCS "BH1750/src/BH1750.cpp" - INCLUDE_DIRS "BH1750/src" - - SRCS "DallasTemperature/DallasTemperature.cpp" - INCLUDE_DIRS "DallasTemperature" - - SRCS "OneWire/OneWire.cpp" - INCLUDE_DIRS "OneWire" - - SRCS "SHT31/SHT31.cpp" - INCLUDE_DIRS "SHT31" - - SRCS "SHT2x/SHT2x.cpp" - INCLUDE_DIRS "SHT2x" - - REQUIRES "arduino-esp32" -) - -add_compile_definitions(WM_NODEBUG=1) diff --git a/components/DallasTemperature b/components/DallasTemperature deleted file mode 160000 index 4a0ccc11..00000000 --- a/components/DallasTemperature +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4a0ccc11c5ba32a4ebf466d7706178c57a8ef61c diff --git a/components/OneWire b/components/OneWire deleted file mode 160000 index 72249e22..00000000 --- a/components/OneWire +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 72249e22ef9092b0750e303d266199965a89e500 diff --git a/components/SHT2x b/components/SHT2x deleted file mode 160000 index b25998da..00000000 --- a/components/SHT2x +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b25998da5d182a0fe77910d25380fd18e1283495 diff --git a/components/SHT31 b/components/SHT31 deleted file mode 160000 index e5361f60..00000000 --- a/components/SHT31 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e5361f60e3f4ea31a7357055f92dee277f3b689f diff --git a/data-templates/multiplexer-mk6.json b/data-templates/multiplexer-mk6.json new file mode 100644 index 00000000..cf50bcab --- /dev/null +++ b/data-templates/multiplexer-mk6.json @@ -0,0 +1,23 @@ +{ + "instance": "DEVICE_INSTANCE", + "id": "DEVICE_ID", + "peripherals": [ + { + "name": "relay", + "type": "multiplexer:xl9535", + "params": { + "sda": "C1", + "scl": "C4", + "address": "0x20" + } + }, + { + "name": "valve-1", + "type": "valve", + "params": { + "pin": "relay:0" + } + } + ], + "sleepWhenIdle": true +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 327f0764..7f74d9de 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,5 +1,11 @@ FILE(GLOB_RECURSE app_sources main.cpp **/*.cpp) +idf_component_register( + SRCS ${app_sources} + INCLUDE_DIRS "." + EMBED_TXTFILES "${CMAKE_SOURCE_DIR}/partitions.csv" +) + if(NOT DEFINED WOKWI) set(WOKWI "$ENV{WOKWI}") endif() @@ -11,12 +17,6 @@ if(WOKWI) component_compile_definitions(WOKWI) endif() -idf_component_register( - SRCS ${app_sources} - INCLUDE_DIRS "." - EMBED_TXTFILES "${CMAKE_SOURCE_DIR}/partitions.csv" -) - component_compile_definitions("${UD_GEN}") component_compile_definitions(FARMHUB_REPORT_MEMORY) diff --git a/main/devices/Device.hpp b/main/devices/Device.hpp index 957552e8..9138ebc7 100644 --- a/main/devices/Device.hpp +++ b/main/devices/Device.hpp @@ -3,15 +3,11 @@ #include #include -#include - #include "esp_netif.h" #include "esp_wifi.h" #include #include -#include - #include #include #include @@ -114,42 +110,18 @@ class ConsolePrinter { static const int spinnerLength = strlen(spinner); auto uptime = duration_cast(boot_clock::now().time_since_epoch()); - status.clear(); counter = (counter + 1) % spinnerLength; - status.concat("["); - status.concat(spinner[counter]); - status.concat("] "); - - status.concat("\033[33m"); - status.concat(farmhubVersion); - status.concat("\033[0m"); - - status.concat(", uptime: \033[33m"); - status.concat(String(uptime.count() / 1000.0, 1)); - status.concat("\033[0m s"); - - status.concat(", WIFI: "); - status.concat(wifiStatus()); - status.concat(" (up \033[33m"); - status.concat(String(double(wifi.getUptime().count()) / 1000.0, 1)); - status.concat("\033[0m s)"); - - status.concat(", RTC: \033[33m"); - status.concat(RtcDriver::isTimeSet() ? "OK" : "UNSYNCED"); - status.concat("\033[0m"); - - status.concat(", heap: \033[33m"); - status.concat(String(double(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)) / 1024.0, 2)); - status.concat("\033[0m kB"); - - status.concat(", CPU: \033[33m"); - status.concat(esp_clk_cpu_freq() / 1000000); - status.concat("\033[0m MHz"); + status.clear(); + status += std::format("[{}] ", spinner[counter]); + status += std::format("\033[33m{}\033[0m", farmhubVersion); + status += std::format(", uptime: \033[33m{:.2f}\033[0m s", uptime.count() / 1000.0); + status += std::format(", WIFI: {} (up \033[33m{:.1f}\033[0m s)", wifiStatus(), wifi.getUptime().count() / 1000.0); + status += std::format(", RTC \033[33m{}\033[0m", RtcDriver::isTimeSet() ? "OK" : "UNSYNCED"); + status += std::format(", heap \033[33m{:.2f}\033[0m kB", heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024.0); + status += std::format(", CPU: \033[33m{}\033[0m MHz", esp_clk_cpu_freq() / 1000000); if (battery != nullptr) { - status.concat(", battery: \033[33m"); - status.concat(String(battery->getVoltage(), 2)); - status.concat("\033[0m V"); + status += std::format(", battery: \033[33m{:.2f}\033[0m V", battery->getVoltage()); } printf("\033[1G\033[0K%s", status.c_str()); @@ -205,7 +177,7 @@ class ConsolePrinter { } int counter; - String status; + std::string status; const std::shared_ptr battery; WiFiDriver& wifi; }; @@ -335,14 +307,14 @@ class ConfiguredKernel { } printf("Shutdown process finished\n"); }); - task.delay(LOW_BATTERY_SHUTDOWN_TIMEOUT); + Task::delay(LOW_BATTERY_SHUTDOWN_TIMEOUT); enterLowPowerDeepSleep(); } } [[noreturn]] inline void enterLowPowerDeepSleep() { printf("Entering low power deep sleep\n"); - ESP.deepSleep(duration_cast(LOW_POWER_SLEEP_CHECK_INTERVAL).count()); + esp_deep_sleep(duration_cast(LOW_POWER_SLEEP_CHECK_INTERVAL).count()); // Signal to the compiler that we are not returning for real abort(); } @@ -435,10 +407,10 @@ class Device { // Remove the level prefix auto messageStart = 2; // Remove trailing newline - auto messageEnd = record.message.charAt(length - 1) == '\n' + auto messageEnd = record.message[length - 1] == '\n' ? length - 1 : length; - String message = record.message.substring(messageStart, messageEnd); + std::string message = record.message.substr(messageStart, messageEnd - messageStart); mqttDeviceRoot->publish( "log", [&](JsonObject& json) { @@ -513,7 +485,7 @@ class Device { kernel.getKernelReadyState().set(); LOGI("Device ready in %.2f s (kernel version %s on %s instance '%s' with hostname '%s' and IP '%s', SSID '%s', current time is %lld)", - millis() / 1000.0, + duration_cast(boot_clock::now().time_since_epoch()).count() / 1000.0, kernel.version.c_str(), deviceConfig.model.get().c_str(), deviceConfig.instance.get().c_str(), @@ -534,7 +506,7 @@ class Device { peripheralManager.publishTelemetry(); } - String locationPrefix() { + std::string locationPrefix() { if (deviceConfig.location.hasValue()) { return deviceConfig.location.get() + "/"; } else { @@ -565,7 +537,7 @@ class Device { FileReadCommand fileReadCommand { fs }; FileWriteCommand fileWriteCommand { fs }; FileRemoveCommand fileRemoveCommand { fs }; - HttpUpdateCommand httpUpdateCommand { [this](const String& url) { + HttpUpdateCommand httpUpdateCommand { [this](const std::string& url) { kernel.prepareUpdate(url); } }; diff --git a/main/devices/DeviceDefinition.hpp b/main/devices/DeviceDefinition.hpp index bcd9d312..30286705 100644 --- a/main/devices/DeviceDefinition.hpp +++ b/main/devices/DeviceDefinition.hpp @@ -3,11 +3,6 @@ #include #include -#include - -#include -#include - #include #include @@ -21,7 +16,7 @@ #include #include #include -#include +#include #include #include #include @@ -36,15 +31,15 @@ namespace farmhub::devices { class DeviceConfiguration : public ConfigurationSection { public: - DeviceConfiguration(const String& defaultModel) + DeviceConfiguration(const std::string& defaultModel) : model(this, "model", defaultModel) , instance(this, "instance", getMacAddress()) { } - Property model; - Property id { this, "id", "UNIDENTIFIED" }; - Property instance; - Property location { this, "location" }; + Property model; + Property id { this, "id", "UNIDENTIFIED" }; + Property instance; + Property location { this, "location" }; NamedConfigurationEntry ntp { this, "ntp" }; @@ -54,10 +49,10 @@ class DeviceConfiguration : public ConfigurationSection { Property publishLogs { this, "publishLogs", Level::Info }; - virtual const String getHostname() { - String hostname = instance.get(); - hostname.replace(":", "-"); - hostname.replace("?", ""); + virtual const std::string getHostname() { + std::string hostname = instance.get(); + std::replace(hostname.begin(), hostname.end(), ':', '-'); + std::erase(hostname, '?'); return hostname; } }; @@ -89,7 +84,7 @@ class DeviceDefinition { /** * @brief Returns zero or more JSON configurations for any built-in peripheral of the device. */ - virtual std::list getBuiltInPeripherals() { + virtual std::list getBuiltInPeripherals() { return {}; } @@ -112,9 +107,10 @@ class DeviceDefinition { MqttDriver::Config& mqttConfig = mqttConfigFile.config; private: - I2CEnvironmentFactory sht3xFactory { "sht3x", 0x44 /* Also supports 0x45 */ }; - I2CEnvironmentFactory> sht2xFactory { "sht2x", 0x40 /* Not configurable */ }; - I2CEnvironmentFactory> htu2xFactory { "htu2x", 0x40 /* Not configurable */ }; + I2CEnvironmentFactory sht3xFactory { "sht3x", 0x44 /* Also supports 0x45 */ }; + // TODO Unify these two factories + I2CEnvironmentFactory sht2xFactory { "sht2x", 0x40 /* Not configurable */ }; + I2CEnvironmentFactory htu2xFactory { "htu2x", 0x40 /* Not configurable */ }; SoilMoistureSensorFactory soilMoistureSensorFactory; Ds18B20SoilSensorFactory ds18b20SoilSensorFactory; diff --git a/main/devices/UglyDucklingMk4.hpp b/main/devices/UglyDucklingMk4.hpp index dd3487d6..001029db 100644 --- a/main/devices/UglyDucklingMk4.hpp +++ b/main/devices/UglyDucklingMk4.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include #include #include @@ -70,7 +68,7 @@ class UglyDucklingMk4 : public DeviceDefinition { peripheralManager.registerFactory(chickenDoorFactory); } - std::list getBuiltInPeripherals() override { + std::list getBuiltInPeripherals() override { // Device address is 0x44 = 68 return { R"({ diff --git a/main/devices/UglyDucklingMk6.hpp b/main/devices/UglyDucklingMk6.hpp index 16e7fa37..5d5cf048 100644 --- a/main/devices/UglyDucklingMk6.hpp +++ b/main/devices/UglyDucklingMk6.hpp @@ -92,8 +92,8 @@ class UglyDucklingMk6 : public DeviceDefinition { pins::BOOT) { // Switch off strapping pin // TODO: Add a LED driver instead - pins::LEDA_RED->pinMode(OUTPUT); - pins::LEDA_RED->digitalWrite(HIGH); + pins::LEDA_RED->pinMode(Pin::Mode::Output); + pins::LEDA_RED->digitalWrite(1); } virtual std::shared_ptr createBatteryDriver(I2CManager& i2c) override { diff --git a/main/idf_component.yml b/main/idf_component.yml index 60c78db3..024b1463 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -1,7 +1,6 @@ ## IDF Component Manager Manifest File dependencies: - espressif/arduino-esp32: "3.1.0-rc1" - espressif/mdns: "1.4.2" - bblanchon/arduinojson: "7.2.1" + espressif/mdns: 1.4.2 + bblanchon/arduinojson: 7.2.1 idf: - version: "5.3.2" + version: 5.3.2 diff --git a/main/kernel/Command.hpp b/main/kernel/Command.hpp index 1ba21467..76a5f1b0 100644 --- a/main/kernel/Command.hpp +++ b/main/kernel/Command.hpp @@ -4,10 +4,11 @@ #include #include -#include #include +#include +#include #include #include #include @@ -21,7 +22,7 @@ class Command : public Named { virtual void handle(const JsonObject& request, JsonObject& response) = 0; protected: - Command(const String& name) + Command(const std::string& name) : Named(name) { } }; @@ -51,7 +52,7 @@ class PingCommand : public Command { } catch (...) { LOGE("Failed to send ping response"); } - response["pong"] = millis(); + response["pong"] = duration_cast(boot_clock::now().time_since_epoch()).count(); } private: @@ -65,8 +66,9 @@ class RestartCommand : public Command { } void handle(const JsonObject& request, JsonObject& response) override { printf("Restarting...\n"); - Serial.flush(); - ESP.restart(); + fflush(stdout); + fsync(fileno(stdout)); + esp_restart(); } }; @@ -86,7 +88,7 @@ class SleepCommand : public Command { class FileCommand : public Command { public: - FileCommand(const String& name, FileSystem& fs) + FileCommand(const std::string& name, FileSystem& fs) : Command(name) , fs(fs) { } @@ -102,21 +104,12 @@ class FileListCommand : public FileCommand { } void handle(const JsonObject& request, JsonObject& response) override { - File root = fs.open("/", FILE_READ); JsonArray files = response["files"].to(); - while (true) { - File entry = root.openNextFile(); - if (!entry) { - break; - } + fs.readDir("/", [&](const std::string& name, off_t size) { JsonObject file = files.add(); - file["name"] = String(entry.name()); - file["size"] = entry.size(); - file["type"] = entry.isDirectory() - ? "dir" - : "file"; - entry.close(); - } + file["name"] = name; + file["size"] = size; + }); } }; @@ -127,17 +120,21 @@ class FileReadCommand : public FileCommand { } void handle(const JsonObject& request, JsonObject& response) override { - String path = request["path"]; - if (!path.startsWith("/")) { + std::string path = request["path"]; + if (!path.starts_with("/")) { path = "/" + path; } LOGI("Reading %s", path.c_str()); response["path"] = path; - File file = fs.open(path, FILE_READ); - if (file) { - response["size"] = file.size(); - response["contents"] = file.readString(); + if (fs.exists(path)) { + size_t size = fs.size(path); + response["size"] = size; + char* buffer = (char*) malloc(size + 1); + auto read = fs.read(path, buffer, size); + buffer[read] = '\0'; + response["contents"] = buffer; + free(buffer); } else { response["error"] = "File not found"; } @@ -151,23 +148,16 @@ class FileWriteCommand : public FileCommand { } void handle(const JsonObject& request, JsonObject& response) override { - String path = request["path"]; - if (!path.startsWith("/")) { + std::string path = request["path"]; + if (!path.starts_with("/")) { path = "/" + path; } LOGI("Writing %s", path.c_str()); - String contents = request["contents"]; + std::string contents = request["contents"]; response["path"] = path; - File file = fs.open(path, FILE_WRITE); - if (file) { - auto written = file.print(contents); - file.flush(); - response["written"] = written; - file.close(); - } else { - response["error"] = "File not found"; - } + size_t written = fs.write(path, contents.c_str(), contents.length()); + response["written"] = written; } }; @@ -178,14 +168,14 @@ class FileRemoveCommand : public FileCommand { } void handle(const JsonObject& request, JsonObject& response) override { - String path = request["path"]; - if (!path.startsWith("/")) { + std::string path = request["path"]; + if (!path.starts_with("/")) { path = "/" + path; } LOGI("Removing %s", path.c_str()); response["path"] = path; - if (fs.remove(path)) { + if (unlink(path.c_str()) == 0) { response["removed"] = true; } else { response["error"] = "File not found"; @@ -195,17 +185,17 @@ class FileRemoveCommand : public FileCommand { class HttpUpdateCommand : public Command { public: - HttpUpdateCommand(const std::function prepareUpdate) + HttpUpdateCommand(const std::function prepareUpdate) : Command("update") , prepareUpdate(prepareUpdate) { } void handle(const JsonObject& request, JsonObject& response) override { - if (!request["url"].is()) { + if (!request["url"].is()) { response["failure"] = "Command contains no URL"; return; } - String url = request["url"]; + std::string url = request["url"]; if (url.length() == 0) { response["failure"] = "Command contains empty url"; return; @@ -214,14 +204,14 @@ class HttpUpdateCommand : public Command { response["success"] = true; Task::run("update", 3072, [](Task& task) { LOGI("Restarting in 5 seconds to apply update"); - delay(5000); - ESP.restart(); + Task::delay(5s); + esp_restart(); }); } private: - const std::function prepareUpdate; - const String currentVersion; + const std::function prepareUpdate; + const std::string currentVersion; }; } // namespace farmhub::kernel diff --git a/main/kernel/Component.hpp b/main/kernel/Component.hpp index 54837437..cb30c4fe 100644 --- a/main/kernel/Component.hpp +++ b/main/kernel/Component.hpp @@ -9,7 +9,7 @@ namespace farmhub::kernel { class Component : public Named { protected: - Component(const String& name, shared_ptr mqttRoot) + Component(const std::string& name, shared_ptr mqttRoot) : Named(name) , mqttRoot(mqttRoot) { } diff --git a/main/kernel/Concurrent.hpp b/main/kernel/Concurrent.hpp index de8f8ad5..63dffe3b 100644 --- a/main/kernel/Concurrent.hpp +++ b/main/kernel/Concurrent.hpp @@ -17,7 +17,7 @@ namespace farmhub::kernel { class BaseQueue { protected: - BaseQueue(const String& name, size_t messageSize, size_t capacity) + BaseQueue(const std::string& name, size_t messageSize, size_t capacity) : name(name) , queue(xQueueCreate(capacity, messageSize)) { } @@ -35,14 +35,14 @@ class BaseQueue { protected: - const String name; + const std::string name; const QueueHandle_t queue; }; template class Queue : public BaseQueue { public: - Queue(const String& name, size_t capacity = 16) + Queue(const std::string& name, size_t capacity = 16) : BaseQueue(name, sizeof(TMessage*), capacity) { } @@ -151,7 +151,7 @@ class Queue : public BaseQueue { template class CopyQueue : public BaseQueue { public: - CopyQueue(const String& name, size_t capacity = 16) + CopyQueue(const std::string& name, size_t capacity = 16) : BaseQueue(name, sizeof(TMessage), capacity) { } diff --git a/main/kernel/Configuration.hpp b/main/kernel/Configuration.hpp index fa2ed34f..44ee73c3 100644 --- a/main/kernel/Configuration.hpp +++ b/main/kernel/Configuration.hpp @@ -1,9 +1,11 @@ #pragma once -#include +#include #include #include +#include + #include using std::list; @@ -15,7 +17,7 @@ namespace farmhub::kernel { class ConfigurationException : public std::exception { public: - ConfigurationException(const String& message) + ConfigurationException(const std::string& message) : message("ConfigurationException: " + message) { } @@ -23,7 +25,7 @@ class ConfigurationException return message.c_str(); } - const String message; + const std::string message; }; class JsonAsString { @@ -31,7 +33,7 @@ class JsonAsString { JsonAsString() { } - JsonAsString(const String& value) + JsonAsString(const std::string& value) : value(value) { } @@ -41,20 +43,20 @@ class JsonAsString { JsonAsString& operator=(const JsonAsString& other) = default; - const String& get() const { + const std::string& get() const { return value; } - void set(const String& value) { + void set(const std::string& value) { this->value = value; } private: - String value; + std::string value; }; bool convertToJson(const JsonAsString& src, JsonVariant dst) { - const String& stringValue = src.get(); + const std::string& stringValue = src.get(); JsonDocument doc; DeserializationError error = deserializeJson(doc, stringValue); @@ -67,7 +69,7 @@ bool convertToJson(const JsonAsString& src, JsonVariant dst) { return true; } bool convertFromJson(JsonVariantConst src, JsonAsString& dst) { - String value; + std::string value; serializeJson(src, value); dst.set(value); return true; @@ -75,16 +77,17 @@ bool convertFromJson(JsonVariantConst src, JsonAsString& dst) { class ConfigurationEntry { public: - void loadFromString(const String& json) { + std::expected loadFromString(const std::string& json) { JsonDocument jsonDocument; DeserializationError error = deserializeJson(jsonDocument, json); if (error == DeserializationError::EmptyInput) { - return; + return {}; } if (error) { - throw ConfigurationException("Cannot parse JSON configuration: " + String(error.c_str()) + json); + return std::unexpected(error.c_str()); } load(jsonDocument.as()); + return {}; } virtual void load(const JsonObject& json) = 0; @@ -137,7 +140,7 @@ template class NamedConfigurationEntry : public ConfigurationEntry { public: template - NamedConfigurationEntry(ConfigurationSection* parent, const String& name, Args&&... args) + NamedConfigurationEntry(ConfigurationSection* parent, const std::string& name, Args&&... args) : name(name) , delegate(std::forward(args)...) { parent->add(*this); @@ -173,7 +176,7 @@ class NamedConfigurationEntry : public ConfigurationEntry { } private: - const String name; + const std::string name; TDelegate delegate; bool namePresentAtLoad = false; }; @@ -181,7 +184,7 @@ class NamedConfigurationEntry : public ConfigurationEntry { template class Property : public ConfigurationEntry { public: - Property(ConfigurationSection* parent, const String& name, const T& defaultValue = T(), const bool secret = false) + Property(ConfigurationSection* parent, const std::string& name, const T& defaultValue = T(), const bool secret = false) : name(name) , secret(secret) , value(defaultValue) @@ -223,7 +226,7 @@ class Property : public ConfigurationEntry { } private: - const String name; + const std::string name; const bool secret; bool configured = false; T value; @@ -233,7 +236,7 @@ class Property : public ConfigurationEntry { template class ArrayProperty : public ConfigurationEntry { public: - ArrayProperty(ConfigurationSection* parent, const String& name) + ArrayProperty(ConfigurationSection* parent, const std::string& name) : name(name) { parent->add(*this); } @@ -269,21 +272,21 @@ class ArrayProperty : public ConfigurationEntry { } private: - const String name; + const std::string name; std::list entries; }; template class ConfigurationFile { public: - ConfigurationFile(const FileSystem& fs, const String& path) + ConfigurationFile(const FileSystem& fs, const std::string& path) : path(path) { if (!fs.exists(path)) { LOGD("The configuration file '%s' was not found, falling back to defaults", path.c_str()); } else { - File file = fs.open(path, FILE_READ); - if (!file) { + std::ifstream file = fs.openRead(path); + if (file.fail()) { throw ConfigurationException("Cannot open config file " + path); } @@ -298,19 +301,18 @@ class ConfigurationFile { path.c_str()); break; default: - throw ConfigurationException("Cannot open config file " + path + " (" + String(error.c_str()) + ")"); + throw ConfigurationException("Cannot open config file " + path + " (" + std::string(error.c_str()) + ")"); } update(json.as()); LOGI("Effective configuration for '%s': %s", path.c_str(), toString().c_str()); } onUpdate([&fs, path](const JsonObject& json) { - File file = fs.open(path, FILE_WRITE); - if (!file) { - throw ConfigurationException("Cannot open config file " + path); - } - + std::ofstream file = fs.openWrite(path); serializeJson(json, file); + if (file.fail()) { + throw ConfigurationException("Cannot write config file " + path); + } file.close(); }); } @@ -335,11 +337,11 @@ class ConfigurationFile { config.store(json, inlineDefaults); } - String toString(bool includeDefaults = true) { + std::string toString(bool includeDefaults = true) { JsonDocument json; auto root = json.to(); store(root, includeDefaults); - String jsonString; + std::string jsonString; serializeJson(json, jsonString); return jsonString; } @@ -347,7 +349,7 @@ class ConfigurationFile { TConfiguration config; private: - const String path; + const std::string path; std::list> callbacks; }; diff --git a/main/kernel/Console.hpp b/main/kernel/Console.hpp index c68c2c42..10c4d204 100644 --- a/main/kernel/Console.hpp +++ b/main/kernel/Console.hpp @@ -1,14 +1,12 @@ #pragma once -#include - #include namespace farmhub::kernel { struct LogRecord { Level level; - String message; + std::string message; }; #define FARMHUB_LOG_COLOR_BLACK "30" @@ -37,34 +35,34 @@ class ConsoleProvider { private: static int processLogFunc(const char* format, va_list args) { - String message = consoleProvider->renderMessage(format, args); + std::string message = consoleProvider->renderMessage(format, args); return consoleProvider->processLog(message); } - int processLog(const String& message) { - if (message.isEmpty()) { + int processLog(const std::string& message) { + if (message.empty()) { return 0; } - String assembledMessage; + std::string assembledMessage; { std::lock_guard lock(partialMessageMutex); - if (message.charAt(message.length() - 1) != '\n') { + if (message[message.length() - 1] != '\n') { partialMessage += message; return 0; - } else if (!partialMessage.isEmpty()) { + } else if (!partialMessage.empty()) { assembledMessage = partialMessage + message; partialMessage.clear(); } } - if (assembledMessage.isEmpty()) { + if (assembledMessage.empty()) { return processLogLine(message); } else { return processLogLine(assembledMessage); } } - int processLogLine(const String& message) { + int processLogLine(const std::string& message) { Level level = getLevel(message); if (level <= recordedLevel) { logRecords.offer(level, message); @@ -105,7 +103,7 @@ class ConsoleProvider { return count; } - String renderMessage(const char* format, va_list args) { + std::string renderMessage(const char* format, va_list args) { int length; { std::lock_guard lock(bufferMutex); @@ -113,7 +111,7 @@ class ConsoleProvider { if (length < 0) { return ""; } else if (length < BUFFER_SIZE) { - return String(buffer, length); + return std::string(buffer, length); } } @@ -121,17 +119,17 @@ class ConsoleProvider { length = std::min(length, 2048); char* heapBuffer = new char[length + 1]; vsnprintf(heapBuffer, length + 1, format, args); - String result(heapBuffer, length); + std::string result(heapBuffer, length); delete[] heapBuffer; return result; } - static Level getLevel(const String& message) { + static Level getLevel(const std::string& message) { // Anything that doesn't look like 'X ...' is a debug message - if (message.length() < 2 || message.charAt(1) != ' ') { + if (message.length() < 2 || message[1] != ' ') { return Level::Debug; } - switch (message.charAt(0)) { + switch (message[0]) { case 'E': return Level::Error; case 'W': @@ -156,7 +154,7 @@ class ConsoleProvider { char buffer[BUFFER_SIZE]; std::mutex partialMessageMutex; - String partialMessage; + std::string partialMessage; }; } // namespace farmhub::kernel diff --git a/main/kernel/FileSystem.hpp b/main/kernel/FileSystem.hpp index c4fe49ff..883d30ec 100644 --- a/main/kernel/FileSystem.hpp +++ b/main/kernel/FileSystem.hpp @@ -1,89 +1,145 @@ #pragma once -#include +#include +#include +#include +#include +#include + +#include namespace farmhub::kernel { -class FileSystem; -static FileSystem* initializeFileSystem(); +static constexpr const char* PARTITION = "data"; class FileSystem { public: - virtual bool exists(const String& path) const = 0; - - virtual File open(const String& path, const char* mode = FILE_READ) const = 0; - - virtual bool remove(const String& path) const = 0; - - virtual bool reset() const = 0; - - static FileSystem& get() { - static FileSystem* instance = initializeFileSystem(); - return *instance; + bool exists(const std::string& path) const { + struct stat fileStat; + return stat(resolve(path).c_str(), &fileStat) == 0; } -}; -class SpiffsFileSystem : public FileSystem { -public: - SpiffsFileSystem() { + FILE* open(const std::string& path, const char* mode) const { + return fopen(resolve(path).c_str(), mode); } - bool exists(const String& path) const override { - return SPIFFS.exists(path); + std::ifstream openRead(const std::string& path) const { + return std::ifstream(resolve(path).c_str()); } - File open(const String& path, const char* mode = FILE_READ) const override { - return SPIFFS.open(path, mode); + std::ofstream openWrite(const std::string& path) const { + return std::ofstream(resolve(path).c_str()); } - bool remove(const String& path) const override { - return SPIFFS.remove(path); + size_t size(const std::string& path) const { + struct stat fileStat; + if (stat(resolve(path).c_str(), &fileStat) != 0) { + return 0; + } + return fileStat.st_size; } - bool reset() const override { - return SPIFFS.format(); + size_t read(const std::string& path, char* buffer, size_t size) const { + FILE* file = open(resolve(path), "r"); + if (file == nullptr) { + return 0; + } + + size_t bytesRead = fread(buffer, 1, size, file); + fclose(file); + return bytesRead; } -}; -class UninitializedFileSystem : public FileSystem { -public: - UninitializedFileSystem() { + size_t write(const std::string& path, const char* buffer, size_t size) const { + FILE* file = open(resolve(path), "w"); + if (file == nullptr) { + return 0; + } + + size_t bytesWritten = fwrite(buffer, 1, size, file); + fclose(file); + return bytesWritten; } - bool exists(const String& path) const override { - return false; + bool readDir(const std::string& path, std::function callback) const { + std::string resolvedPath = resolve(path); + DIR* dir = opendir(resolvedPath.c_str()); + if (dir == nullptr) { + LOGTE("fs", "Failed to open directory: %s", path.c_str()); + return false; + } + + struct dirent* entry; + while ((entry = readdir(dir)) != nullptr) { + std::string fullPath = std::string(resolvedPath) + "/" + entry->d_name; + callback(entry->d_name, size(fullPath)); + } + + closedir(dir); + return true; } - File open(const String& path, const char* mode = FILE_READ) const override { - return File(); + std::string resolve(const std::string& path) const { + return mountPoint + path; } - bool remove(const String& path) const override { - return false; + static bool format() { + esp_err_t ret = esp_spiffs_format("data"); + if (ret == ESP_OK) { + LOGTV("fs", "SPIFFS partition '%s' formatted successfully", PARTITION); + return true; + } else { + LOGTE("fs", "Error formatting SPIFFS partition '%s': %s\n", PARTITION, esp_err_to_name(ret)); + return false; + } } - bool reset() const override { - return false; + static FileSystem& get() { + static FileSystem* instance = initializeFileSystem(); + return *instance; } -}; -static FileSystem* initializeFileSystem() { - if (!SPIFFS.begin()) { - LOGI("File system not initialized"); - return new UninitializedFileSystem(); +private: + FileSystem(const std::string& mountPoint) + : mountPoint(mountPoint) { } - LOGI("File system contents:"); - File root = SPIFFS.open("/", FILE_READ); - while (true) { - File file = root.openNextFile(); - if (!file) { - break; + + static FileSystem* initializeFileSystem() { + const std::string mountPoint = "/" + std::string(PARTITION); + FileSystem* fs = nullptr; + + esp_vfs_spiffs_conf_t conf = { + .base_path = mountPoint.c_str(), + .partition_label = PARTITION, + .max_files = 5, + .format_if_mount_failed = false + }; + esp_err_t ret = esp_vfs_spiffs_register(&conf); + + switch (ret) { + case ESP_OK: { + LOGTI("fs", "SPIFFS partition '%s' mounted successfully", PARTITION); + fs = new FileSystem(mountPoint); + fs->readDir(mountPoint, [&](const std::string& name, size_t size) { + LOGTI("fs", " - %s (%u bytes)", name.c_str(), size); + }); + break; + } + case ESP_FAIL: + LOGTE("fs", "Failed to mount partition '%s'", PARTITION); + break; + case ESP_ERR_NOT_FOUND: + LOGTE("fs", "Failed to find SPIFFS partition '%s'", PARTITION); + break; + default: + LOGTE("fs", "Failed to initialize SPIFFS partition '%s' (%s)", PARTITION, esp_err_to_name(ret)); + break; } - LOGI(" - %s (%d bytes)", - file.path(), file.size()); - file.close(); + + return fs; } - return new SpiffsFileSystem(); -} + + const std::string mountPoint; +}; } // namespace farmhub::kernel diff --git a/main/kernel/I2CManager.hpp b/main/kernel/I2CManager.hpp index a44e2fda..6beaa021 100644 --- a/main/kernel/I2CManager.hpp +++ b/main/kernel/I2CManager.hpp @@ -1,12 +1,9 @@ #pragma once #include -#include #include -#include -#include -#include +#include #include @@ -24,166 +21,144 @@ struct I2CConfig { InternalPinPtr sda; InternalPinPtr scl; - String toString() const { - return String("I2C address: 0x") + String(address, HEX) + ", SDA: " + sda->getName() + ", SCL: " + scl->getName(); + std::string toString() const { + return std::format("I2C address: 0x{:#x}, SDA: {}, SCL: {}", address, sda->getName(), scl->getName()); } }; class I2CBus { public: - I2CBus(TwoWire& wire) - : wire(wire) { - } - - TwoWire& wire; - Mutex mutex; + const i2c_port_t port; + const InternalPinPtr sda; + const InternalPinPtr scl; }; -class I2CTransmission; - class I2CDevice { public: - I2CDevice(const String& name, I2CBus& bus, uint8_t address) + I2CDevice(const std::string& name, shared_ptr bus, uint8_t address) : name(name) , bus(bus) - , address(address) { - } - -private: - const String name; - I2CBus& bus; - const uint8_t address; - - friend class I2CTransmission; -}; - -class I2CTransmission { -public: - I2CTransmission(shared_ptr device) - : device(device) - , lock(device->bus.mutex) { - wire().beginTransmission(device->address); - } - - ~I2CTransmission() { - auto result = wire().endTransmission(); - if (result != 0) { - LOGE("Communication unsuccessful with I2C device %s at address 0x%02x, result: %d", - device->name.c_str(), device->address, result); - } - } - - size_t requestFrom(size_t len, bool stopBit = true) { - LOGV("Requesting %d bytes from I2C device %s at address 0x%02x", - len, device->name.c_str(), device->address); - auto count = wire().requestFrom(device->address, len, stopBit); - LOGV("Received %d bytes from I2C device %s at address 0x%02x", - count, device->name.c_str(), device->address); - return count; - } - - size_t write(uint8_t data) { - LOGV("Writing 0x%02x to I2C device %s at address 0x%02x", - data, device->name.c_str(), device->address); - auto count = wire().write(data); - LOGV("Wrote %d bytes to I2C device %s at address 0x%02x", - count, device->name.c_str(), device->address); - return count; + , address(address) + , device({ + .port = bus->port, + .cfg = { + .mode = I2C_MODE_MASTER, + .sda_io_num = bus->sda->getGpio(), + .scl_io_num = bus->scl->getGpio(), + // TODO Allow this to be configred + .sda_pullup_en = false, + .scl_pullup_en = false, + .master { + // TODO Allow clock speed to be configured + .clk_speed = 400000, + }, + }, + .addr = address, + }) { + // TODO Do we need a mutex here? + i2c_dev_create_mutex(&device); + } + + ~I2CDevice() { + i2c_dev_delete_mutex(&device); + } + + esp_err_t probeRead() { + return i2c_dev_probe(&device, I2C_DEV_READ); + } + + uint8_t readRegByte(uint8_t reg) { + uint8_t value; + ESP_ERROR_CHECK(i2c_dev_read(&device, ®, 1, &value, 1)); + return value; } - size_t write(const uint8_t* data, size_t quantity) { - LOGV("Writing %d bytes to I2C device %s at address 0x%02x", - quantity, device->name.c_str(), device->address); - auto count = wire().write(data, quantity); - LOGV("Wrote %d bytes to I2C device %s at address 0x%02x", - count, device->name.c_str(), device->address); - return count; + uint16_t readRegWord(uint8_t reg) { + uint16_t value; + ESP_ERROR_CHECK(i2c_dev_read(&device, ®, 1, &value, 2)); + return value; } - int available() { - return wire().available(); + void readReg(uint8_t reg, uint8_t* buffer, size_t length) { + ESP_ERROR_CHECK(i2c_dev_read(&device, ®, 1, buffer, length)); } - int read() { - auto value = wire().read(); - LOGV("Read 0x%02x from I2C device %s at address 0x%02x", - value, device->name.c_str(), device->address); - return value; + void writeRegByte(uint8_t reg, uint8_t value) { + ESP_ERROR_CHECK(i2c_dev_write(&device, ®, 1, &value, 1)); } - int peek() { - auto value = wire().peek(); - LOGV("Peeked 0x%02x from I2C device %s at address 0x%02x", - value, device->name.c_str(), device->address); - return value; + void writeRegWord(uint8_t reg, uint16_t value) { + ESP_ERROR_CHECK(i2c_dev_write(&device, ®, 1, &value, 2)); } - void flush() { - LOGV("Flushing I2C device %s at address 0x%02x", - device->name.c_str(), device->address); - wire().flush(); + void writeReg(uint8_t reg, uint8_t* buffer, size_t length) { + ESP_ERROR_CHECK(i2c_dev_write(&device, ®, 1, buffer, length)); } private: - inline TwoWire& wire() const { - return device->bus.wire; - } - - shared_ptr device; - Lock lock; + const std::string name; + const shared_ptr bus; + const uint8_t address; + i2c_dev_t device; }; class I2CManager { public: - TwoWire& getWireFor(const I2CConfig& config) { - return getWireFor(config.sda, config.scl); + I2CManager() { + ESP_ERROR_CHECK(i2cdev_init()); + buses.reserve(BUS_COUNT); } - TwoWire& getWireFor(InternalPinPtr sda, InternalPinPtr scl) { - return getBusFor(sda, scl).wire; + ~I2CManager() { + ESP_ERROR_CHECK(i2cdev_done()); } - shared_ptr createDevice(const String& name, const I2CConfig& config) { + shared_ptr createDevice(const std::string& name, const I2CConfig& config) { return createDevice(name, config.sda, config.scl, config.address); } - shared_ptr createDevice(const String& name, InternalPinPtr sda, InternalPinPtr scl, uint8_t address) { + shared_ptr createDevice(const std::string& name, InternalPinPtr sda, InternalPinPtr scl, uint8_t address) { auto device = std::make_shared(name, getBusFor(sda, scl), address); LOGI("Created I2C device %s at address 0x%02x", name.c_str(), address); // Test if communication is possible - I2CTransmission tx(device); + // esp_err_t err = device->probeRead(); + // if (err != ESP_OK) { + // throw std::runtime_error( + // std::format("Failed to communicate with I2C device {} at address 0x{:#x}: {}", name, address, esp_err_to_name(err)).c_str()); + // } return device; } -private: - I2CBus& getBusFor(InternalPinPtr sda, InternalPinPtr scl) { - GpioPair key = std::make_pair(sda, scl); - auto it = busMap.find(key); - if (it != busMap.end()) { - LOGV("Reusing already registered I2C bus for SDA: %s, SCL: %s", - sda->getName().c_str(), scl->getName().c_str()); - return *(it->second); - } else { + shared_ptr getBusFor(const I2CConfig& config) { + return getBusFor(config.sda, config.scl); + } + + shared_ptr getBusFor(InternalPinPtr sda, InternalPinPtr scl) { + Lock lock(mutex); + for (auto bus : buses) { + if (bus->sda == sda && bus->scl == scl) { + LOGV("Reusing already registered I2C bus for SDA: %s, SCL: %s", + sda->getName().c_str(), scl->getName().c_str()); + return bus; + } + } + if (buses.size() < BUS_COUNT) { LOGD("Creating new I2C bus for SDA: %s, SCL: %s", sda->getName().c_str(), scl->getName().c_str()); - if (nextBus >= 2) { - throw std::runtime_error("Maximum number of I2C buses reached"); - } - TwoWire* wire = new TwoWire(nextBus++); - if (!wire->begin(sda->getGpio(), scl->getGpio())) { - throw std::runtime_error( - String("Failed to initialize I2C bus for SDA: " + sda->getName() + ", SCL: " + scl->getName()).c_str()); - } - I2CBus* bus = new I2CBus(*wire); - busMap[key] = bus; - return *bus; + auto bus = std::make_shared(I2CBus { .port = static_cast(buses.size()), .sda = sda, .scl = scl }); + buses.push_back(bus); + return bus; } + + throw std::runtime_error("Maximum number of I2C buses reached"); } - uint8_t nextBus = 0; +private: + static constexpr size_t BUS_COUNT = 2; - std::map busMap; + Mutex mutex; + std::vector> buses; }; } // namespace farmhub::kernel diff --git a/main/kernel/Kernel.hpp b/main/kernel/Kernel.hpp index 13de1f6b..d6fdfa60 100644 --- a/main/kernel/Kernel.hpp +++ b/main/kernel/Kernel.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -36,11 +37,11 @@ class Kernel; static RTC_DATA_ATTR int bootCount = 0; -static const String UPDATE_FILE = "/update.json"; +static constexpr const char* UPDATE_FILE = "/update.json"; // TODO Move this to a separate file -static const String& getMacAddress() { - static String macAddress; +static const std::string& getMacAddress() { + static std::string macAddress; if (macAddress.length() == 0) { uint8_t rawMac[6]; for (int i = 0; i < 6; i++) { @@ -79,7 +80,7 @@ class Kernel { Task::loop("status-update", 3072, [this](Task&) { updateState(); }); httpUpdateResult = handleHttpUpdate(); - if (!httpUpdateResult.isEmpty()) { + if (!httpUpdateResult.empty()) { LOGE("HTTP update failed because: %s", httpUpdateResult.c_str()); } @@ -97,12 +98,12 @@ class Kernel { return kernelReadyState; } - const String& getHttpUpdateResult() const { + const std::string& getHttpUpdateResult() const { return httpUpdateResult; } - void prepareUpdate(const String& url) { - auto fUpdate = fs.open(UPDATE_FILE, FILE_WRITE); + void prepareUpdate(const std::string& url) { + std::ofstream fUpdate = fs.openWrite(UPDATE_FILE); JsonDocument doc; doc["url"] = url; serializeJson(doc, fUpdate); @@ -113,29 +114,29 @@ class Kernel { LOGI("Performing factory reset"); statusLed.turnOn(); - delay(1000); + Task::delay(1s); statusLed.turnOff(); - delay(1000); + Task::delay(1s); statusLed.turnOn(); if (completeReset) { - delay(1000); + Task::delay(1s); statusLed.turnOff(); - delay(1000); + Task::delay(1s); statusLed.turnOn(); LOGI(" - Deleting the file system..."); - fs.reset(); + FileSystem::format(); } LOGI(" - Clearing NVS..."); nvs_flash_erase(); LOGI(" - Restarting..."); - ESP.restart(); + esp_restart(); } - const String version; + const std::string version; FileSystem& fs { FileSystem::get() }; @@ -207,21 +208,21 @@ class Kernel { stateManager.awaitStateChange(); } - String handleHttpUpdate() { + std::string handleHttpUpdate() { if (!fs.exists(UPDATE_FILE)) { return ""; } - auto fUpdate = fs.open(UPDATE_FILE, FILE_READ); + std::ifstream fUpdate = fs.openRead(UPDATE_FILE); JsonDocument doc; auto error = deserializeJson(doc, fUpdate); fUpdate.close(); - fs.remove(UPDATE_FILE); + unlink(UPDATE_FILE); if (error) { - return "Failed to parse update.json: " + String(error.c_str()); + return "Failed to parse update.json: " + std::string(error.c_str()); } - String url = doc["url"]; + std::string url = doc["url"]; if (url.length() == 0) { return "Command contains empty url"; } @@ -263,9 +264,9 @@ class Kernel { Task::delay(5s); esp_restart(); } else { - LOGE("Update failed (err = %d), continuing with regular boot", - ret); - return "Firmware upgrade failed: " + String(ret); + LOGE("Update failed (%s), continuing with regular boot", + esp_err_to_name(ret)); + return std::format("Firmware upgrade failed: {}", esp_err_to_name(ret)); } } // namespace farmhub::kernel @@ -327,7 +328,7 @@ class Kernel { MdnsDriver mdns { wifi, deviceConfig.getHostname(), "ugly-duckling", version, mdnsReadyState }; RtcDriver rtc { wifi, mdns, deviceConfig.ntp.get(), rtcInSyncState }; - String httpUpdateResult; + std::string httpUpdateResult; public: MqttDriver mqtt { wifi, mdns, mqttConfig, deviceConfig.instance.get(), deviceConfig.sleepWhenIdle.get(), mqttReadyState }; diff --git a/main/kernel/Log.hpp b/main/kernel/Log.hpp index dff195d7..63b3a4f1 100644 --- a/main/kernel/Log.hpp +++ b/main/kernel/Log.hpp @@ -48,6 +48,8 @@ void convertFromJson(JsonVariantConst src, Level& dst) { static void initLogging() { const char* logTags[] = { "farmhub", + "farmhub:fs", + "farmhub:ledc", "farmhub:mdns", "farmhub:mqtt", "farmhub:pm", diff --git a/main/kernel/Named.hpp b/main/kernel/Named.hpp index fd864d53..831f98b9 100644 --- a/main/kernel/Named.hpp +++ b/main/kernel/Named.hpp @@ -1,17 +1,15 @@ #pragma once -#include - namespace farmhub::kernel { class Named { protected: - Named(const String& name) + Named(const std::string& name) : name(name) { } public: - const String name; + const std::string name; }; } // namespace farmhub::kernel diff --git a/main/kernel/NvsStore.hpp b/main/kernel/NvsStore.hpp index bb146472..9607dd3d 100644 --- a/main/kernel/NvsStore.hpp +++ b/main/kernel/NvsStore.hpp @@ -2,115 +2,152 @@ #include -#include +#include +#include #include - #include namespace farmhub::kernel { /** - * @brief Thread safe NVS store for JSON serializable objects. + * @brief Thread-safe NVS store for JSON serializable objects. */ class NvsStore { public: - NvsStore(const String& name) + NvsStore(const std::string& name) : name(name) { + // Initialize NVS + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + // NVS partition was truncated and needs to be erased + // Retry nvs_flash_init + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + ESP_ERROR_CHECK(err); } - bool contains(const String& key) { + bool contains(const std::string& key) { return contains(key.c_str()); } bool contains(const char* key) { - return withPreferences(true, [&]() { - return preferences.isKey(key); - }); + return withPreferences(true, [&](nvs_handle_t handle) { + size_t length = 0; + esp_err_t err = nvs_get_str(handle, key, nullptr, &length); + switch (err) { + case ESP_OK: + case ESP_ERR_NVS_NOT_FOUND: + break; + default: + LOGTW("nvs", "contains(%s) = failed to read: %s", key, esp_err_to_name(err)); + break; + } + return err; + }) == ESP_OK; } template - bool get(const String& key, T& value) { + bool get(const std::string& key, T& value) { return get(key.c_str(), value); } template bool get(const char* key, T& value) { - return withPreferences(true, [&]() { - if (!preferences.isKey(key)) { - LOGTV("nvs", "get(%s) = not found", - key); - return false; + return withPreferences(true, [&](nvs_handle_t handle) { + size_t length = 0; + esp_err_t err = nvs_get_str(handle, key, nullptr, &length); + if (err != ESP_OK) { + LOGTV("nvs", "get(%s) = failed to read: %s", key, esp_err_to_name(err)); + return err; + } + + char json[length]; + err = nvs_get_str(handle, key, json, &length); + if (err != ESP_OK) { + LOGTE("nvs", "get(%s) = failed to read: %s", key, esp_err_to_name(err)); + return err; } - String jsonString = preferences.getString(key); - LOGTV("nvs", "get(%s) = %s", - key, jsonString.c_str()); + + LOGTV("nvs", "get(%s) = %s", key, json); + JsonDocument jsonDocument; - deserializeJson(jsonDocument, jsonString); - if (jsonDocument.isNull()) { - LOGTE("nvs", "get(%s) = invalid JSON", - key); - return false; + DeserializationError jsonError = deserializeJson(jsonDocument, json); + if (jsonError) { + LOGTE("nvs", "get(%s) = invalid JSON: %s", key, jsonError.c_str()); + return ESP_FAIL; } + value = jsonDocument.as(); - return true; - }); + return ESP_OK; + }) == ESP_OK; } template - bool set(const String& key, const T& value) { + bool set(const std::string& key, const T& value) { return set(key.c_str(), value); } template bool set(const char* key, const T& value) { - return withPreferences(false, [&]() { + return withPreferences(false, [&](nvs_handle_t handle) { JsonDocument jsonDocument; jsonDocument.set(value); - String jsonString; + std::string jsonString; serializeJson(jsonDocument, jsonString); - LOGTV("nvs", "set(%s) = %s", - key, jsonString.c_str()); - return preferences.putString(key, jsonString.c_str()); - }); + + LOGTV("nvs", "set(%s) = %s", key, jsonString.c_str()); + + esp_err_t err = nvs_set_str(handle, key, jsonString.c_str()); + if (err != ESP_OK) { + LOGTE("nvs", "set(%s) = failed to write: %s", key, esp_err_to_name(err)); + return err; + } + + return nvs_commit(handle); + }) == ESP_OK; } - bool remove(const String& key) { + bool remove(const std::string& key) { return remove(key.c_str()); } bool remove(const char* key) { - return withPreferences(false, [&]() { - LOGTV("nvs", "remove(%s)", - key); - if (preferences.isKey(key)) { - return preferences.remove(key); - } else { - return false; + return withPreferences(false, [&](nvs_handle_t handle) { + LOGTV("nvs", "remove(%s)", key); + esp_err_t err = nvs_erase_key(handle, key); + if (err != ESP_OK) { + LOGTE("nvs", "remove(%s) = cannot delete: %s", key, esp_err_to_name(err)); + return err; } - }); + + return nvs_commit(handle); + }) == ESP_OK; } private: - bool withPreferences(bool readOnly, std::function action) { + esp_err_t withPreferences(bool readOnly, std::function action) { Lock lock(preferencesMutex); - LOGTV("nvs", "%s '%s'", - readOnly ? "read" : "write", name.c_str()); - if (!preferences.begin(name.c_str(), readOnly)) { - LOGTE("nvs", "failed to %s '%s'", - readOnly ? "read" : "write", name.c_str()); + LOGTV("nvs", "%s '%s'", readOnly ? "read" : "write", name.c_str()); + + nvs_handle_t handle; + esp_err_t err = nvs_open(name.c_str(), readOnly ? NVS_READONLY : NVS_READWRITE, &handle); + if (err != ESP_OK) { + LOGTE("nvs", "failed to %s '%s'", readOnly ? "read" : "write", name.c_str()); return false; } - bool result = action(); - preferences.end(); + + esp_err_t result = action(handle); + nvs_close(handle); + LOGTV("nvs", "finished %s '%s', result: %s", - readOnly ? "read" : "write", name.c_str(), result ? "true" : "false"); + readOnly ? "read" : "write", name.c_str(), esp_err_to_name(result)); return result; } - Preferences preferences; Mutex preferencesMutex; - const String name; + const std::string name; }; } // namespace farmhub::kernel diff --git a/main/kernel/PcntManager.hpp b/main/kernel/PcntManager.hpp index 6df56774..569f56c5 100644 --- a/main/kernel/PcntManager.hpp +++ b/main/kernel/PcntManager.hpp @@ -4,13 +4,8 @@ #include -#include - 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 - // TODO Limit number of channels available struct PcntUnit { PcntUnit(pcnt_unit_handle_t unit, InternalPinPtr pin) diff --git a/main/kernel/Pin.hpp b/main/kernel/Pin.hpp index 86fb8198..b3cac534 100644 --- a/main/kernel/Pin.hpp +++ b/main/kernel/Pin.hpp @@ -6,7 +6,9 @@ #include #include -#include +#include +#include +#include #include @@ -25,47 +27,54 @@ using InternalPinPtr = std::shared_ptr; */ class Pin { public: - static PinPtr byName(const String& name) { + static PinPtr byName(const std::string& name) { auto it = BY_NAME.find(name); if (it != BY_NAME.end()) { return it->second; } - throw std::runtime_error(String("Unknown pin: " + name).c_str()); + throw std::runtime_error(std::string("Unknown pin: " + name).c_str()); } - virtual void pinMode(uint8_t mode) const = 0; + enum class Mode { + Output, + Input, + InputPullUp, + InputPullDown, + }; + + virtual void pinMode(Mode mode) const = 0; virtual void digitalWrite(uint8_t val) const = 0; virtual int digitalRead() const = 0; - inline const String& getName() const { + inline const std::string& getName() const { return name; } - static void registerPin(const String& name, PinPtr pin) { + static void registerPin(const std::string& name, PinPtr pin) { BY_NAME[name] = pin; } protected: - Pin(const String& name) + Pin(const std::string& name) : name(name) { } protected: - const String name; + const std::string name; - static std::map BY_NAME; + static std::map BY_NAME; }; -std::map Pin::BY_NAME; +std::map Pin::BY_NAME; /** * @brief An internal GPIO pin of the MCU. These pins can do analog reads as well, and can expose the GPIO number. */ class InternalPin : public Pin { public: - static InternalPinPtr registerPin(const String& name, gpio_num_t gpio) { + static InternalPinPtr registerPin(const std::string& name, gpio_num_t gpio) { auto pin = std::make_shared(name, gpio); INTERNAL_BY_GPIO[gpio] = pin; INTERNAL_BY_NAME[name] = pin; @@ -73,43 +82,45 @@ class InternalPin : public Pin { return pin; } - static InternalPinPtr byName(const String& name) { + static InternalPinPtr byName(const std::string& name) { auto it = INTERNAL_BY_NAME.find(name); if (it != INTERNAL_BY_NAME.end()) { return it->second; } - throw std::runtime_error(String("Unknown internal pin: " + name).c_str()); + throw std::runtime_error(std::string("Unknown internal pin: " + name).c_str()); } static InternalPinPtr byGpio(gpio_num_t pin) { auto it = INTERNAL_BY_GPIO.find(pin); if (it == INTERNAL_BY_GPIO.end()) { - String name = "GPIO_NUM_" + String(pin); + std::string name = std::format("GPIO_NUM_{}", static_cast(pin)); return registerPin(name, pin); } else { return it->second; } } - InternalPin(const String& name, gpio_num_t gpio) + InternalPin(const std::string& name, gpio_num_t gpio) : Pin(name) , gpio(gpio) { } - inline void pinMode(uint8_t mode) const override { - ::pinMode(gpio, mode); + void pinMode(Mode mode) const override { + gpio_config_t conf = { + .pin_bit_mask = (1ULL << gpio), + .mode = mode == Mode::Output ? GPIO_MODE_OUTPUT : GPIO_MODE_INPUT, + .pull_up_en = mode == Mode::InputPullUp ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE, + .pull_down_en = mode == Mode::InputPullDown ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE, + }; + ESP_ERROR_CHECK(gpio_config(&conf)); } inline void digitalWrite(uint8_t val) const override { - ::digitalWrite(gpio, val); + gpio_set_level(gpio, val); } inline int digitalRead() const override { - return ::digitalRead(gpio); - } - - inline uint16_t analogRead() const { - return ::analogRead(gpio); + return gpio_get_level(gpio); } inline gpio_num_t getGpio() const { @@ -118,12 +129,64 @@ class InternalPin : public Pin { private: const gpio_num_t gpio; - static std::map INTERNAL_BY_NAME; + static std::map INTERNAL_BY_NAME; static std::map INTERNAL_BY_GPIO; }; -std::map InternalPin::INTERNAL_BY_NAME; +class AnalogPin { +public: + AnalogPin(const InternalPinPtr pin) + : pin(pin) { + adc_unit_t unit; + ESP_ERROR_CHECK(adc_oneshot_io_to_channel(pin->getGpio(), &unit, &channel)); + + handle = getUnitHandle(unit); + + adc_oneshot_chan_cfg_t config = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_DEFAULT, + }; + ESP_ERROR_CHECK(adc_oneshot_config_channel(handle, channel, &config)); + } + + ~AnalogPin() { + ESP_ERROR_CHECK(adc_oneshot_del_unit(handle)); + } + + int analogRead() const { + int value; + ESP_ERROR_CHECK(adc_oneshot_read(handle, channel, &value)); + return value; + } + + const std::string& getName() const { + return pin->getName(); + } + +private: + static adc_oneshot_unit_handle_t getUnitHandle(adc_unit_t unit) { + adc_oneshot_unit_handle_t handle = ANALOG_UNITS[unit]; + if (handle == nullptr) { + adc_oneshot_unit_init_cfg_t config = { + .unit_id = unit, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + ESP_ERROR_CHECK(adc_oneshot_new_unit(&config, &handle)); + ANALOG_UNITS[unit] = handle; + } + return handle; + } + + static std::vector ANALOG_UNITS; + + const InternalPinPtr pin; + adc_oneshot_unit_handle_t handle; + adc_channel_t channel; +}; + +std::map InternalPin::INTERNAL_BY_NAME; std::map InternalPin::INTERNAL_BY_GPIO; +std::vector AnalogPin::ANALOG_UNITS { 2 }; } // namespace farmhub::kernel @@ -148,7 +211,7 @@ struct Converter { if (src.is()) { return Pin::byName(src.as()); } else { - throw std::runtime_error(String("Invalid pin name: " + src.as()).c_str()); + throw std::runtime_error(std::string("Invalid pin name: " + src.as()).c_str()); } } @@ -162,7 +225,7 @@ struct Converter { static void toJson(const InternalPinPtr& src, JsonVariant dst) { if (src == nullptr) { dst.set(nullptr); - } else if (src->getName().startsWith("GPIO_NUM_")) { + } else if (src->getName().starts_with("GPIO_NUM_")) { dst.set(static_cast(src->getGpio())); } else { dst.set(src->getName()); diff --git a/main/kernel/PowerManager.hpp b/main/kernel/PowerManager.hpp index 5ac060ea..fb2245e5 100644 --- a/main/kernel/PowerManager.hpp +++ b/main/kernel/PowerManager.hpp @@ -2,8 +2,6 @@ #include -#include - #include // FIXME Why do we need to define these manually? @@ -60,7 +58,7 @@ class PowerManager { // printf("Task Name\tState\tPrio\tStack\tNum\n"); // printf("%s\n", buffer); // #endif - // task.delay(10s); + // Task::delay(10s); // }); } const bool sleepWhenIdle; @@ -109,7 +107,7 @@ class PowerManager { class PowerManagementLock { public: - PowerManagementLock(const String& name, esp_pm_lock_type_t type) : name(name) { + PowerManagementLock(const std::string& name, esp_pm_lock_type_t type) : name(name) { ESP_ERROR_CHECK(esp_pm_lock_create(type, 0, name.c_str(), &lock)); } @@ -122,7 +120,7 @@ class PowerManagementLock { PowerManagementLock& operator=(const PowerManagementLock&) = delete; private: - const String name; + const std::string name; esp_pm_lock_handle_t lock; friend class PowerManagementLockGuard; diff --git a/main/kernel/PwmManager.hpp b/main/kernel/PwmManager.hpp index d10803b3..5865e25d 100644 --- a/main/kernel/PwmManager.hpp +++ b/main/kernel/PwmManager.hpp @@ -1,45 +1,129 @@ #pragma once -#include +#include 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 -// TODO Limit number of channels available -struct PwmPin { - PwmPin(InternalPinPtr pin, uint32_t freq, uint8_t resolutionBits) - : pin(pin) - , freq(freq) - , resolutionBits(resolutionBits) { +class LedcTimer { +public: + LedcTimer(ledc_mode_t speedMode, ledc_timer_bit_t dutyResolution, ledc_timer_t timerNum, uint32_t freqHz, ledc_clk_cfg_t clkSrc) + : speedMode(speedMode) + , dutyResolution(dutyResolution) + , timerNum(timerNum) + , freqHz(freqHz) + , clkSrc(clkSrc) { + ledc_timer_config_t config = { + .speed_mode = speedMode, + .duty_resolution = dutyResolution, + .timer_num = timerNum, + .freq_hz = freqHz, + .clk_cfg = clkSrc, + }; + ESP_ERROR_CHECK(ledc_timer_config(&config)); + } + + ~LedcTimer() { + ESP_ERROR_CHECK(ledc_timer_rst(speedMode, timerNum)); + } + + inline bool isSameConfig(ledc_mode_t otherSpeedMode, ledc_timer_bit_t otherDutyResolution, uint32_t otherFreqHz, ledc_clk_cfg_t otherClkCfg) { + return speedMode == otherSpeedMode && dutyResolution == otherDutyResolution && freqHz == otherFreqHz && clkSrc == otherClkCfg; } - PwmPin(const PwmPin& other) - : PwmPin(other.pin, other.freq, other.resolutionBits) { + uint32_t constexpr maxValue() const { + return (1 << dutyResolution) - 1; + } + +private: + const ledc_mode_t speedMode; + const ledc_timer_bit_t dutyResolution; + const ledc_timer_t timerNum; + const uint32_t freqHz; + const ledc_clk_cfg_t clkSrc; + + friend class PwmPin; + friend class PwmManager; +}; + +class PwmPin { +public: + PwmPin(InternalPinPtr pin, const LedcTimer& timer, ledc_channel_t channel) + : pin(pin) + , timer(timer) + , channel(channel) { + + ledc_channel_config_t config = { + .gpio_num = pin->getGpio(), + .speed_mode = timer.speedMode, + .channel = channel, + .intr_type = LEDC_INTR_DISABLE, + .timer_sel = timer.timerNum, + .duty = 0, // Set duty to 0% + .hpoint = 0 + }; + ESP_ERROR_CHECK(ledc_channel_config(&config)); } uint32_t constexpr maxValue() const { - return (1 << resolutionBits) - 1; + return timer.maxValue(); } void write(uint32_t value) const { - ledcWrite(pin->getGpio(), value); + ESP_ERROR_CHECK(ledc_set_duty(timer.speedMode, channel, value)); + ESP_ERROR_CHECK(ledc_update_duty(timer.speedMode, channel)); } + const std::string& getName() const { + return pin->getName(); + } + +private: const InternalPinPtr pin; - const uint32_t freq; - const uint8_t resolutionBits; + const LedcTimer& timer; + const ledc_channel_t channel; + + friend class PmwManager; }; class PwmManager { public: - PwmPin registerPin(InternalPinPtr pin, uint32_t freq, uint8_t resolutionBits = 8) { - ledcAttach(pin->getGpio(), freq, resolutionBits); - LOGD("Registered PWM channel on pin %s with freq %ld and resolution %d", - pin->getName().c_str(), freq, resolutionBits); - return PwmPin(pin, freq, resolutionBits); + PwmPin& registerPin(InternalPinPtr pin, uint32_t freq, ledc_timer_bit_t dutyResolution = LEDC_TIMER_8_BIT, ledc_clk_cfg_t clkSrc = LEDC_AUTO_CLK) { + LedcTimer& timer = getOrCreateTimer(LEDC_LOW_SPEED_MODE, dutyResolution, freq, clkSrc); + + ledc_channel_t channel = static_cast(pins.size()); + if (channel >= LEDC_CHANNEL_MAX) { + throw std::runtime_error("No more LEDC channels available"); + } + + pins.emplace_back(pin, timer, channel); + LOGTD("ledc", "Registered PWM channel on pin %s with freq %ld and resolution %d", + pin->getName().c_str(), freq, dutyResolution); + return pins.back(); + } + +private: + LedcTimer& getOrCreateTimer(ledc_mode_t speedMode, ledc_timer_bit_t dutyResolution, uint32_t freqHz, ledc_clk_cfg_t clkSrc) { + for (LedcTimer& timer : timers) { + if (timer.isSameConfig(speedMode, dutyResolution, freqHz, clkSrc)) { + return timer; + } + } + ledc_timer_t timerNum = static_cast(timers.size()); + if (timerNum >= LEDC_TIMER_MAX) { + throw std::runtime_error("No more LEDC timers available"); + } + + timers.emplace_back(speedMode, dutyResolution, timerNum, freqHz, clkSrc); + LOGTD("ledc", "Created LEDC timer %d with freq %ld and resolution %d bits", + timerNum, freqHz, dutyResolution); + return timers.back(); } + + std::list timers; + std::list pins; }; } // namespace farmhub::kernel diff --git a/main/kernel/Service.hpp b/main/kernel/Service.hpp index 768adf29..82611d71 100644 --- a/main/kernel/Service.hpp +++ b/main/kernel/Service.hpp @@ -9,7 +9,7 @@ namespace farmhub::kernel { template class ServiceRef : public Named { public: - ServiceRef(const String& name, T& instance) + ServiceRef(const std::string& name, T& instance) : Named(name) , reference(instance) { } @@ -18,7 +18,7 @@ class ServiceRef : public Named { : ServiceRef(other.name, other.reference) { } - const String& getName() const { + const std::string& getName() const { return name; } diff --git a/main/kernel/State.hpp b/main/kernel/State.hpp index 4c16b9d1..da253510 100644 --- a/main/kernel/State.hpp +++ b/main/kernel/State.hpp @@ -6,8 +6,6 @@ #include #include -#include - #include using namespace std::chrono; @@ -26,7 +24,7 @@ class StateManager; */ class State { public: - State(const String& name, EventGroupHandle_t eventGroup, EventBits_t eventBits) + State(const std::string& name, EventGroupHandle_t eventGroup, EventBits_t eventBits) : name(name) , eventGroup(eventGroup) , eventBits(eventBits) { @@ -66,7 +64,7 @@ class State { return (bits & eventBits) == eventBits; } - const String name; + const std::string name; const EventGroupHandle_t eventGroup; const EventBits_t eventBits; @@ -76,7 +74,7 @@ class State { class StateSource : public State { public: - StateSource(const String& name, EventGroupHandle_t eventGroup, EventBits_t eventBits) + StateSource(const std::string& name, EventGroupHandle_t eventGroup, EventBits_t eventBits) : State(name, eventGroup, eventBits) { } diff --git a/main/kernel/StateManager.hpp b/main/kernel/StateManager.hpp index baa3f860..7e97eb2b 100644 --- a/main/kernel/StateManager.hpp +++ b/main/kernel/StateManager.hpp @@ -17,7 +17,7 @@ class StateManager { : eventGroup(xEventGroupCreate()) { } - StateSource createStateSource(const String& name) { + StateSource createStateSource(const std::string& name) { LOGV("Creating state: %s", name.c_str()); if (nextEventBit > 31) { @@ -27,7 +27,7 @@ class StateManager { return StateSource(name, eventGroup, eventBits); } - State combineStates(const String& name, const std::list& states) const { + State combineStates(const std::string& name, const std::list& states) const { LOGD("Creating combined state: %s", name.c_str()); int eventBits = 0; diff --git a/main/kernel/Task.hpp b/main/kernel/Task.hpp index 10d5a484..e607e5ac 100644 --- a/main/kernel/Task.hpp +++ b/main/kernel/Task.hpp @@ -6,8 +6,6 @@ #include #include -#include - #include using namespace std::chrono; @@ -75,10 +73,10 @@ class TaskHandle { class Task { public: - static TaskHandle inline run(const String& name, uint32_t stackSize, const TaskFunction runFunction) { + static TaskHandle inline run(const std::string& name, uint32_t stackSize, const TaskFunction runFunction) { return Task::run(name, stackSize, DEFAULT_PRIORITY, runFunction); } - static TaskHandle run(const String& name, uint32_t stackSize, UBaseType_t priority, const TaskFunction runFunction) { + static TaskHandle run(const std::string& name, uint32_t stackSize, UBaseType_t priority, const TaskFunction runFunction) { TaskFunction* taskFunction = new TaskFunction(runFunction); LOGD("Creating task %s with priority %d and stack size %ld", name.c_str(), priority, stackSize); @@ -97,10 +95,10 @@ class Task { TIMEOUT, }; - static RunResult inline runIn(const String& name, ticks timeout, uint32_t stackSize, const TaskFunction runFunction) { + static RunResult inline runIn(const std::string& name, ticks timeout, uint32_t stackSize, const TaskFunction runFunction) { return Task::runIn(name, timeout, stackSize, DEFAULT_PRIORITY, runFunction); } - static RunResult runIn(const String& name, ticks timeout, uint32_t stackSize, UBaseType_t priority, const TaskFunction runFunction) { + static RunResult runIn(const std::string& name, ticks timeout, uint32_t stackSize, UBaseType_t priority, const TaskFunction runFunction) { TaskHandle_t caller = xTaskGetCurrentTaskHandle(); TaskHandle callee = run(name, stackSize, priority, [runFunction, caller](Task& task) { runFunction(task); @@ -117,10 +115,10 @@ class Task { } } - static TaskHandle inline loop(const String& name, uint32_t stackSize, TaskFunction loopFunction) { + static TaskHandle inline loop(const std::string& name, uint32_t stackSize, TaskFunction loopFunction) { return Task::loop(name, stackSize, DEFAULT_PRIORITY, loopFunction); } - static TaskHandle loop(const String& name, uint32_t stackSize, UBaseType_t priority, TaskFunction loopFunction) { + static TaskHandle loop(const std::string& name, uint32_t stackSize, UBaseType_t priority, TaskFunction loopFunction) { return Task::run(name, stackSize, priority, [loopFunction](Task& task) { while (true) { loopFunction(task); diff --git a/main/kernel/Telemetry.hpp b/main/kernel/Telemetry.hpp index 812794e7..c047e53b 100644 --- a/main/kernel/Telemetry.hpp +++ b/main/kernel/Telemetry.hpp @@ -5,6 +5,7 @@ #include +#include #include namespace farmhub::kernel { @@ -17,7 +18,7 @@ class TelemetryProvider { class TelemetryCollector { public: void collect(JsonObject& root) { - root["uptime"] = millis(); + root["uptime"] = duration_cast(boot_clock::now().time_since_epoch()).count(); for (auto& entry : providers) { auto& name = entry.first; auto& provider = entry.second; @@ -26,14 +27,14 @@ class TelemetryCollector { } } - void registerProvider(const String& name, std::shared_ptr provider) { + void registerProvider(const std::string& name, std::shared_ptr provider) { LOGV("Registering telemetry provider %s", name.c_str()); // TODO Check for duplicates providers.emplace(name, provider); } private: - std::map> providers; + std::map> providers; }; class TelemetryPublisher { diff --git a/main/kernel/Watchdog.hpp b/main/kernel/Watchdog.hpp index 3a6df8c5..545c6dac 100644 --- a/main/kernel/Watchdog.hpp +++ b/main/kernel/Watchdog.hpp @@ -18,7 +18,7 @@ typedef std::function WatchdogCallback; class Watchdog { public: - Watchdog(const String& name, const ticks timeout, WatchdogCallback callback) + Watchdog(const std::string& name, const ticks timeout, WatchdogCallback callback) : name(name) , timeout(timeout) , callback(callback) { @@ -48,7 +48,7 @@ class Watchdog { } private: - const String name; + const std::string name; const ticks timeout; const WatchdogCallback callback; diff --git a/main/kernel/drivers/BatteryDriver.hpp b/main/kernel/drivers/BatteryDriver.hpp index 7993f23b..0ce61369 100644 --- a/main/kernel/drivers/BatteryDriver.hpp +++ b/main/kernel/drivers/BatteryDriver.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include #include @@ -23,21 +21,19 @@ class AnalogBatteryDriver : public BatteryDriver { public: AnalogBatteryDriver(InternalPinPtr pin, float voltageDividerRatio) - : pin(pin) + : analogPin(pin) , voltageDividerRatio(voltageDividerRatio) { LOGI("Initializing analog battery driver on pin %s", - pin->getName().c_str()); - - pin->pinMode(INPUT); + analogPin.getName().c_str()); } float getVoltage() { - auto batteryLevel = pin->analogRead(); + auto batteryLevel = analogPin.analogRead(); return batteryLevel * 3.3 / 4096 * voltageDividerRatio; } private: - const InternalPinPtr pin; + AnalogPin analogPin; const float voltageDividerRatio; }; diff --git a/main/kernel/drivers/Bq27220Driver.hpp b/main/kernel/drivers/Bq27220Driver.hpp index 59c5156d..37a0129d 100644 --- a/main/kernel/drivers/Bq27220Driver.hpp +++ b/main/kernel/drivers/Bq27220Driver.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include #include @@ -28,8 +26,8 @@ class Bq27220Driver : public BatteryDriver { } float getVoltage() override { - // LOGV("Capacityt: %d/%d", readWord(0x10), readWord(0x12)); - return readWord(0x08) / 1000.0; + // LOGV("Capacityt: %d/%d", readRegWord(0x10), readRegWord(0x12)); + return device->readRegWord(0x08) / 1000.0; } float getCurrent() { @@ -37,69 +35,27 @@ class Bq27220Driver : public BatteryDriver { } float getTemperature() { - return readWord(0x06) * 0.1 - 273.2; + return device->readRegWord(0x06) * 0.1 - 273.2; } protected: void populateTelemetry(JsonObject& json) override { BatteryDriver::populateTelemetry(json); json["current"] = getCurrent(); - auto status = readWord(0x0A); + auto status = device->readRegWord(0x0A); json["status"] = status; json["charging"] = (status & 0x0001) == 0; json["temperature"] = getTemperature(); } private: - bool readFrom(uint8_t reg, uint8_t* buffer, size_t length) { - { - I2CTransmission tx(device); - tx.write(reg); - } - { - I2CTransmission tx(device); - auto rxResult = tx.requestFrom((uint8_t) length); - if (rxResult != length) { - LOGE("Failed to read from 0x%02x: %d", reg, rxResult); - return false; - } - for (size_t i = 0; i < length; i++) { - buffer[i] = tx.read(); - } - } - return true; - } - - void writeTo(uint8_t reg, const uint8_t* buffer, size_t length) { - I2CTransmission tx(device); - tx.write(reg); - tx.write(buffer, length); - } - - uint8_t readByte(uint8_t reg) { - uint8_t buffer; - readFrom(reg, &buffer, 1); - return buffer; - } - - uint16_t readWord(uint8_t reg) { - uint16_t buffer; - readFrom(reg, reinterpret_cast(&buffer), 2); - return buffer; - } - int16_t readSigned(uint8_t reg) { - return static_cast(readWord(reg)); - } - - void writeWord(uint8_t reg, uint16_t value) { - uint16_t buffer = value; - writeTo(reg, reinterpret_cast(&buffer), 2); + return static_cast(device->readRegWord(reg)); } uint16_t readControlWord(uint16_t subcommand) { - writeWord(0x00, subcommand); - return readByte(0x40) | (readByte(0x41) << 8); + device->writeRegWord(0x00, subcommand); + return device->readRegByte(0x40) | (device->readRegByte(0x41) << 8); } shared_ptr device; diff --git a/main/kernel/drivers/Drv8801Driver.hpp b/main/kernel/drivers/Drv8801Driver.hpp index 4ee0bc4b..c2bae6df 100644 --- a/main/kernel/drivers/Drv8801Driver.hpp +++ b/main/kernel/drivers/Drv8801Driver.hpp @@ -3,8 +3,6 @@ #include #include -#include - #include #include @@ -21,8 +19,8 @@ class Drv8801Driver : public PwmMotorDriver { private: - const uint32_t PWM_FREQ = 25000; // 25kHz - const uint8_t PWM_RESOLUTION = 8; // 8 bit + static constexpr uint32_t PWM_FREQ = 25000; + static constexpr ledc_timer_bit_t PWM_RESOLUTION = LEDC_TIMER_8_BIT; public: // Note: on Ugly Duckling MK5, the DRV8874's PMODE is wired to 3.3V, so it's locked in PWM mode @@ -50,16 +48,16 @@ class Drv8801Driver mode2Pin->getName().c_str(), currentPin->getName().c_str()); - enablePin->pinMode(OUTPUT); - mode1Pin->pinMode(OUTPUT); - mode2Pin->pinMode(OUTPUT); - sleepPin->pinMode(OUTPUT); - faultPin->pinMode(INPUT); - currentPin->pinMode(INPUT); + enablePin->pinMode(Pin::Mode::Output); + mode1Pin->pinMode(Pin::Mode::Output); + mode2Pin->pinMode(Pin::Mode::Output); + sleepPin->pinMode(Pin::Mode::Output); + faultPin->pinMode(Pin::Mode::Input); + currentPin->pinMode(Pin::Mode::Input); // TODO Allow using the DRV8801 in other modes - mode1Pin->digitalWrite(HIGH); - mode2Pin->digitalWrite(HIGH); + mode1Pin->digitalWrite(1); + mode2Pin->digitalWrite(1); sleep(); } @@ -68,11 +66,11 @@ class Drv8801Driver if (duty == 0) { LOGD("Stopping"); sleep(); - enablePin->digitalWrite(LOW); + enablePin->digitalWrite(0); return; } wakeUp(); - enablePin->digitalWrite(HIGH); + enablePin->digitalWrite(1); int direction = (phase == MotorPhase::FORWARD ? 1 : -1); int dutyValue = phaseChannel.maxValue() / 2 + direction * (int) (phaseChannel.maxValue() / 2 * duty); @@ -84,12 +82,12 @@ class Drv8801Driver } void sleep() { - sleepPin->digitalWrite(LOW); + sleepPin->digitalWrite(0); sleeping = true; } void wakeUp() { - sleepPin->digitalWrite(HIGH); + sleepPin->digitalWrite(1); sleeping = false; } @@ -99,7 +97,7 @@ class Drv8801Driver private: const PinPtr enablePin; - const PwmPin phaseChannel; + const PwmPin& phaseChannel; const PinPtr currentPin; const PinPtr faultPin; const PinPtr sleepPin; diff --git a/main/kernel/drivers/Drv8833Driver.hpp b/main/kernel/drivers/Drv8833Driver.hpp index 0800c380..b7fc6644 100644 --- a/main/kernel/drivers/Drv8833Driver.hpp +++ b/main/kernel/drivers/Drv8833Driver.hpp @@ -3,8 +3,6 @@ #include #include -#include - #include #include @@ -43,9 +41,9 @@ class Drv8833Driver { sleepPin->getName().c_str()); if (sleepPin != nullptr) { - sleepPin->pinMode(OUTPUT); + sleepPin->pinMode(Pin::Mode::Output); } - faultPin->pinMode(INPUT); + faultPin->pinMode(Pin::Mode::Input); updateSleepState(); } @@ -61,8 +59,8 @@ class Drv8833Driver { private: class Drv8833MotorDriver : public PwmMotorDriver { private: - static constexpr uint32_t PWM_FREQ = 25000; // 25kHz - static constexpr uint8_t PWM_RESOLUTION = 10; // 10 bit + static constexpr uint32_t PWM_FREQ = 25000; + static constexpr ledc_timer_bit_t PWM_RESOLUTION = LEDC_TIMER_10_BIT; public: Drv8833MotorDriver( @@ -81,8 +79,8 @@ class Drv8833Driver { int dutyValue = static_cast((in1Channel.maxValue() + in1Channel.maxValue() * duty) / 2); LOGD("Driving motor %s on pins %s/%s at %d%% (duty = %d)", phase == MotorPhase::FORWARD ? "forward" : "reverse", - in1Channel.pin->getName().c_str(), - in2Channel.pin->getName().c_str(), + in1Channel.getName().c_str(), + in2Channel.getName().c_str(), (int) (duty * 100), dutyValue); @@ -120,8 +118,8 @@ class Drv8833Driver { private: Drv8833Driver* const driver; - const PwmPin in1Channel; - const PwmPin in2Channel; + const PwmPin& in1Channel; + const PwmPin& in2Channel; bool sleeping; }; @@ -132,7 +130,7 @@ class Drv8833Driver { void setSleepState(bool sleep) { if (sleepPin != nullptr) { - sleepPin->digitalWrite(sleep ? LOW : HIGH); + sleepPin->digitalWrite(sleep ? 0 : 1); } } diff --git a/main/kernel/drivers/Drv8874Driver.hpp b/main/kernel/drivers/Drv8874Driver.hpp index 0845fc8b..1f0f29d0 100644 --- a/main/kernel/drivers/Drv8874Driver.hpp +++ b/main/kernel/drivers/Drv8874Driver.hpp @@ -3,8 +3,6 @@ #include #include -#include - #include #include @@ -21,8 +19,8 @@ class Drv8874Driver : public PwmMotorDriver { private: - const uint32_t PWM_FREQ = 25000; // 25kHz - const uint8_t PWM_RESOLUTION = 8; // 8 bit + static constexpr uint32_t PWM_FREQ = 25000; + static constexpr ledc_timer_bit_t PWM_RESOLUTION = LEDC_TIMER_8_BIT; public: // Note: on Ugly Duckling MK5, the DRV8874's PMODE is wired to 3.3V, so it's locked in PWM mode @@ -46,9 +44,9 @@ class Drv8874Driver sleepPin->getName().c_str(), currentPin->getName().c_str()); - sleepPin->pinMode(OUTPUT); - faultPin->pinMode(INPUT); - currentPin->pinMode(INPUT); + sleepPin->pinMode(Pin::Mode::Output); + faultPin->pinMode(Pin::Mode::Input); + currentPin->pinMode(Pin::Mode::Input); sleep(); } @@ -79,12 +77,12 @@ class Drv8874Driver } void sleep() { - sleepPin->digitalWrite(LOW); + sleepPin->digitalWrite(0); sleeping = true; } void wakeUp() { - sleepPin->digitalWrite(HIGH); + sleepPin->digitalWrite(1); sleeping = false; } @@ -93,8 +91,8 @@ class Drv8874Driver } private: - const PwmPin in1Channel; - const PwmPin in2Channel; + const PwmPin& in1Channel; + const PwmPin& in2Channel; const PinPtr currentPin; const PinPtr faultPin; const PinPtr sleepPin; diff --git a/main/kernel/drivers/LedDriver.hpp b/main/kernel/drivers/LedDriver.hpp index 2808fe6c..532b65c2 100644 --- a/main/kernel/drivers/LedDriver.hpp +++ b/main/kernel/drivers/LedDriver.hpp @@ -18,14 +18,14 @@ class LedDriver { public: typedef std::vector BlinkPattern; - LedDriver(const String& name, PinPtr pin) + LedDriver(const std::string& name, PinPtr pin) : pin(pin) , patternQueue(name, 1) , pattern({ -milliseconds::max() }) { LOGI("Initializing LED driver on pin %s", pin->getName().c_str()); - pin->pinMode(OUTPUT); + pin->pinMode(Pin::Mode::Output); Task::loop(name, 2048, [this](Task& task) { handleIteration(); }); @@ -64,9 +64,9 @@ class LedDriver { cursor++; if (blinkTime > milliseconds::zero()) { - setLedState(LOW); + setLedState(0); } else { - setLedState(HIGH); + setLedState(1); } // TOOD Substract processing time from delay diff --git a/main/kernel/drivers/MdnsDriver.hpp b/main/kernel/drivers/MdnsDriver.hpp index 88acdddc..5bc996cc 100644 --- a/main/kernel/drivers/MdnsDriver.hpp +++ b/main/kernel/drivers/MdnsDriver.hpp @@ -13,12 +13,44 @@ namespace farmhub::kernel::drivers { struct MdnsRecord { - String hostname; - IPAddress ip; + std::string hostname; + esp_ip4_addr_t ip; int port; - bool validate() { - return hostname.length() > 0 && ip != IPAddress() && port > 0; + bool hasHostname() const { + return !hostname.empty(); + } + + bool hasIp() const { + return ip.addr != 0; + } + + bool hasPort() const { + return port > 0; + } + + bool validate() const { + return (hasHostname() || hasIp()) && hasPort(); + } + + std::string ipAsString() const { + char ipStr[16]; + esp_ip4addr_ntoa(&ip, ipStr, sizeof(ipStr)); + return ipStr; + } + + std::string ipOrHost() const { + if (hasIp()) { + return ipAsString(); + } else { + return hostname; + } + } + + std::string toString() const { + std::string result = ipOrHost(); + result += ":" + port; + return result; } }; @@ -26,9 +58,9 @@ class MdnsDriver { public: MdnsDriver( WiFiDriver& wifi, - const String& hostname, - const String& instanceName, - const String& version, + const std::string& hostname, + const std::string& instanceName, + const std::string& version, StateSource& mdnsReady) : wifi(wifi) , mdnsReady(mdnsReady) { @@ -54,7 +86,7 @@ class MdnsDriver { }); } - bool lookupService(const String& serviceName, const String& port, MdnsRecord& record, bool loadFromCache = true, milliseconds timeout = 5s) { + bool lookupService(const std::string& serviceName, const std::string& port, MdnsRecord& record, bool loadFromCache = true, milliseconds timeout = 5s) { // Wait indefinitely Lock lock(lookupMutex); auto result = lookupServiceUnderMutex(serviceName, port, record, loadFromCache, timeout); @@ -62,9 +94,9 @@ class MdnsDriver { } private: - bool lookupServiceUnderMutex(const String& serviceName, const String& port, MdnsRecord& record, bool loadFromCache, milliseconds timeout) { + bool lookupServiceUnderMutex(const std::string& serviceName, const std::string& port, MdnsRecord& record, bool loadFromCache, milliseconds timeout) { // TODO Use a callback and retry if cached entry doesn't work - String cacheKey = serviceName + "." + port; + std::string cacheKey = serviceName + "." + port; if (loadFromCache) { if (nvs.get(cacheKey, record)) { if (record.validate()) { @@ -87,7 +119,7 @@ class MdnsDriver { mdnsReady.awaitSet(); mdns_result_t* results = nullptr; - esp_err_t err = mdns_query_ptr(String("_" + serviceName).c_str(), String("_" + port).c_str(), timeout.count(), 1, &results); + esp_err_t err = mdns_query_ptr(std::string("_" + serviceName).c_str(), std::string("_" + port).c_str(), timeout.count(), 1, &results); if (err) { LOGTE("mdns", "query failed for %s.%s: %d", serviceName.c_str(), port.c_str(), err); @@ -104,7 +136,7 @@ class MdnsDriver { record.hostname = result.hostname; } if (result.addr != nullptr) { - record.ip = IPAddress(result.addr->addr.u_addr.ip4.addr); + record.ip = result.addr->addr.u_addr.ip4; } record.port = result.port; mdns_query_results_free(results); @@ -125,16 +157,35 @@ class MdnsDriver { bool convertToJson(const MdnsRecord& src, JsonVariant dst) { auto jsonRecord = dst.to(); - jsonRecord["hostname"] = src.hostname; - jsonRecord["ip"] = src.ip.toString(); - jsonRecord["port"] = src.port; + if (src.hasHostname()) { + jsonRecord["hostname"] = src.hostname; + } + if (src.hasIp()) { + jsonRecord["ip"] = src.ipAsString(); + } + if (src.hasPort()) { + jsonRecord["port"] = src.port; + } return true; } void convertFromJson(JsonVariantConst src, MdnsRecord& dst) { auto jsonRecord = src.as(); - dst.hostname = jsonRecord["hostname"].as(); - dst.ip.fromString(jsonRecord["ip"].as()); - dst.port = jsonRecord["port"].as(); + if (jsonRecord["hostname"].is()) { + dst.hostname = jsonRecord["hostname"].as(); + } else { + dst.hostname = ""; + } + if (jsonRecord["ip"].is()) { + const char* ipStr = jsonRecord["ip"].as().c_str(); + dst.ip.addr = esp_ip4addr_aton(ipStr); + } else { + dst.ip.addr = 0; + } + if (jsonRecord["port"].is()) { + dst.port = jsonRecord["port"].as(); + } else { + dst.port = 0; + } } } // namespace farmhub::kernel::drivers diff --git a/main/kernel/drivers/RtcDriver.hpp b/main/kernel/drivers/RtcDriver.hpp index 2eb897d3..bacbd94c 100644 --- a/main/kernel/drivers/RtcDriver.hpp +++ b/main/kernel/drivers/RtcDriver.hpp @@ -33,7 +33,7 @@ class RtcDriver { public: class Config : public ConfigurationSection { public: - Property host { this, "host", "" }; + Property host { this, "host", "" }; }; RtcDriver(WiFiDriver& wifi, MdnsDriver& mdns, const Config& ntpConfig, StateSource& rtcInSync) @@ -57,13 +57,13 @@ class RtcDriver { // Attempt a retry, but with mDNS cache disabled LOGTE("rtc", "NTP update failed, retrying in 10 seconds with mDNS cache disabled"); trustMdnsCache = false; - task.delay(10s); + Task::delay(10s); continue; } } // We are good for a while now - task.delay(1h); + Task::delay(1h); } }); } @@ -99,12 +99,9 @@ class RtcDriver { } else { MdnsRecord ntpServer; if (mdns.lookupService("ntp", "udp", ntpServer, trustMdnsCache)) { - LOGTD("rtc", "using NTP server %s:%d (%s) from mDNS", - ntpServer.hostname.c_str(), - ntpServer.port, - ntpServer.ip.toString().c_str()); - auto serverIp = convertIp4(ntpServer.ip); - esp_sntp_setserver(0, &serverIp); + LOGTD("rtc", "using NTP server %s from mDNS", + ntpServer.toString().c_str()); + esp_sntp_setserver(0, (const ip_addr_t*) &ntpServer.ip); } else { LOGTD("rtc", "no NTP server configured, using default"); } @@ -148,14 +145,6 @@ class RtcDriver { } } - // TODO Use ESP-IDF's ip4_addr_t - static ip_addr_t convertIp4(const IPAddress& ip) { - ip_addr_t espIP; - IP4_ADDR(&espIP.u_addr.ip4, ip[0], ip[1], ip[2], ip[3]); - espIP.type = IPADDR_TYPE_V4; - return espIP; - } - WiFiDriver& wifi; MdnsDriver& mdns; const Config& ntpConfig; diff --git a/main/kernel/drivers/SwitchManager.hpp b/main/kernel/drivers/SwitchManager.hpp index 69838167..30a591c6 100644 --- a/main/kernel/drivers/SwitchManager.hpp +++ b/main/kernel/drivers/SwitchManager.hpp @@ -6,8 +6,6 @@ #include -#include - #include #include #include @@ -25,7 +23,7 @@ enum class SwitchMode { class Switch { public: - virtual const String& getName() const = 0; + virtual const std::string& getName() const = 0; virtual InternalPinPtr getPin() const = 0; virtual bool isEngaged() const = 0; }; @@ -54,22 +52,22 @@ class SwitchManager { typedef std::function SwitchEngagementHandler; typedef std::function SwitchReleaseHandler; - const Switch& onEngaged(const String& name, InternalPinPtr pin, SwitchMode mode, SwitchEngagementHandler engagementHandler) { + const Switch& onEngaged(const std::string& name, InternalPinPtr pin, SwitchMode mode, SwitchEngagementHandler engagementHandler) { return registerHandler( name, pin, mode, engagementHandler, [](const Switch&, milliseconds) {}); } - const Switch& onReleased(const String& name, InternalPinPtr pin, SwitchMode mode, SwitchReleaseHandler releaseHandler) { + const Switch& onReleased(const std::string& name, InternalPinPtr pin, SwitchMode mode, SwitchReleaseHandler releaseHandler) { return registerHandler( name, pin, mode, [](const Switch&) {}, releaseHandler); } - const Switch& registerHandler(const String& name, InternalPinPtr pin, SwitchMode mode, SwitchEngagementHandler engagementHandler, SwitchReleaseHandler releaseHandler) { + const Switch& registerHandler(const std::string& name, InternalPinPtr pin, SwitchMode mode, SwitchEngagementHandler engagementHandler, SwitchReleaseHandler releaseHandler) { LOGI("Registering switch %s on pin %s, mode %s", name.c_str(), pin->getName().c_str(), mode == SwitchMode::PullUp ? "pull-up" : "pull-down"); // Configure PIN_INPUT as input - pin->pinMode(mode == SwitchMode::PullUp ? INPUT_PULLUP : INPUT_PULLDOWN); + pin->pinMode(mode == SwitchMode::PullUp ? Pin::Mode::InputPullUp : Pin::Mode::InputPullDown); // gpio_set_direction(pin, GPIO_MODE_INPUT); // gpio_set_pull_mode(pin, mode == SwitchMode::PullUp ? GPIO_PULLUP_ONLY : GPIO_PULLDOWN_ONLY); @@ -92,7 +90,7 @@ class SwitchManager { private: struct SwitchState : public Switch { public: - const String& getName() const override { + const std::string& getName() const override { return name; } @@ -101,11 +99,11 @@ class SwitchManager { } bool isEngaged() const override { - return pin->digitalRead() == (mode == SwitchMode::PullUp ? LOW : HIGH); + return pin->digitalRead() == (mode == SwitchMode::PullUp ? 0 : 1); } private: - String name; + std::string name; InternalPinPtr pin; SwitchMode mode; @@ -135,7 +133,7 @@ class SwitchManager { // ISR handler for GPIO interrupt static void IRAM_ATTR handleSwitchInterrupt(void* arg) { SwitchManager::SwitchState* state = static_cast(arg); - bool engaged = state->pin->digitalRead() == (state->mode == SwitchMode::PullUp ? LOW : HIGH); + bool engaged = state->pin->digitalRead() == (state->mode == SwitchMode::PullUp ? 0 : 1); state->manager->queueSwitchStateChange(state, engaged); } diff --git a/main/kernel/drivers/WiFiDriver.hpp b/main/kernel/drivers/WiFiDriver.hpp index 958617c4..9b404928 100644 --- a/main/kernel/drivers/WiFiDriver.hpp +++ b/main/kernel/drivers/WiFiDriver.hpp @@ -21,7 +21,7 @@ namespace farmhub::kernel::drivers { class WiFiDriver { public: - WiFiDriver(StateSource& networkRequested, StateSource& networkConnecting, StateSource& networkReady, StateSource& configPortalRunning, const String& hostname, bool powerSaveMode) + WiFiDriver(StateSource& networkRequested, StateSource& networkConnecting, StateSource& networkReady, StateSource& configPortalRunning, const std::string& hostname, bool powerSaveMode) : networkRequested(networkRequested) , networkConnecting(networkConnecting) , networkReady(networkReady) @@ -48,17 +48,17 @@ class WiFiDriver { }); } - std::optional getSsid() { + std::optional getSsid() { Lock lock(metadataMutex); return ssid; } - std::optional getIp() { + std::optional getIp() { Lock lock(metadataMutex); return ip.transform([](const esp_ip4_addr_t& ip) { char ipString[16]; esp_ip4addr_ntoa(&ip, ipString, sizeof(ipString)); - return String(ipString); + return std::string(ipString); }); } @@ -93,7 +93,7 @@ class WiFiDriver { } case WIFI_EVENT_STA_CONNECTED: { auto event = static_cast(eventData); - String newSsid(event->ssid, event->ssid_len); + std::string newSsid((const char*) event->ssid, event->ssid_len); { Lock lock(metadataMutex); ssid = newSsid; @@ -112,7 +112,7 @@ class WiFiDriver { } eventQueue.offer(WiFiEvent::DISCONNECTED); LOGTD("wifi", "Disconnected from the AP %s, reason: %d", - String(event->ssid, event->ssid_len).c_str(), event->reason); + std::string((const char*) event->ssid, event->ssid_len).c_str(), event->reason); break; } case WIFI_EVENT_AP_STACONNECTED: { @@ -408,7 +408,7 @@ class WiFiDriver { StateSource& networkConnecting; StateSource& networkReady; StateSource& configPortalRunning; - const String hostname; + const std::string hostname; const bool powerSaveMode; StateManager internalStates; @@ -430,7 +430,7 @@ class WiFiDriver { static constexpr milliseconds WIFI_CHECK_INTERVAL = 1min; Mutex metadataMutex; - std::optional ssid; + std::optional ssid; std::optional ip; std::optional> wifiUpSince; diff --git a/main/kernel/mqtt/MqttDriver.hpp b/main/kernel/mqtt/MqttDriver.hpp index 66c0c7a2..b2e7f449 100644 --- a/main/kernel/mqtt/MqttDriver.hpp +++ b/main/kernel/mqtt/MqttDriver.hpp @@ -53,15 +53,15 @@ enum class PublishStatus { typedef std::function CommandHandler; -typedef std::function SubscriptionHandler; +typedef std::function SubscriptionHandler; class MqttRoot; class MqttDriver { private: struct OutgoingMessage { - const String topic; - const String payload; + const std::string topic; + const std::string payload; const Retention retain; const QoS qos; const TaskHandle_t waitingTask; @@ -73,12 +73,12 @@ class MqttDriver { }; struct IncomingMessage { - const String topic; - const String payload; + const std::string topic; + const std::string payload; }; struct Subscription { - const String topic; + const std::string topic; const QoS qos; const SubscriptionHandler handle; }; @@ -93,20 +93,20 @@ class MqttDriver { public: class Config : public ConfigurationSection { public: - Property host { this, "host", "" }; + Property host { this, "host", "" }; Property port { this, "port", 1883 }; - Property clientId { this, "clientId", "" }; + Property clientId { this, "clientId", "" }; Property queueSize { this, "queueSize", 128 }; - ArrayProperty serverCert { this, "serverCert" }; - ArrayProperty clientCert { this, "clientCert" }; - ArrayProperty clientKey { this, "clientKey" }; + ArrayProperty serverCert { this, "serverCert" }; + ArrayProperty clientCert { this, "clientCert" }; + ArrayProperty clientKey { this, "clientKey" }; }; MqttDriver( WiFiDriver& wifi, MdnsDriver& mdns, const Config& config, - const String& instanceName, + const std::string& instanceName, bool powerSaveMode, StateSource& mqttReady) : wifi(wifi) @@ -132,15 +132,15 @@ class MqttDriver { }); } - shared_ptr forRoot(const String& topic) { + shared_ptr forRoot(const std::string& topic) { return make_shared(*this, topic); } private: - PublishStatus publish(const String& topic, const JsonDocument& json, Retention retain, QoS qos, ticks timeout = ticks::zero(), LogPublish log = LogPublish::Log, milliseconds extendAlert = MQTT_ALERT_AFTER_OUTGOING) { + PublishStatus publish(const std::string& topic, const JsonDocument& json, Retention retain, QoS qos, ticks timeout = ticks::zero(), LogPublish log = LogPublish::Log, milliseconds extendAlert = MQTT_ALERT_AFTER_OUTGOING) { if (log == LogPublish::Log) { #ifdef DUMP_MQTT - String serializedJson; + std::string serializedJson; serializeJsonPretty(json, serializedJson); LOGTD("mqtt", "Queuing topic '%s'%s (qos = %d, timeout = %lld ms): %s", topic.c_str(), @@ -156,14 +156,14 @@ class MqttDriver { duration_cast(timeout).count()); #endif } - String payload; + std::string payload; serializeJson(json, payload); return executeAndAwait(timeout, [&](TaskHandle_t waitingTask) { return eventQueue.offerIn(MQTT_QUEUE_TIMEOUT, OutgoingMessage { topic, payload, retain, qos, waitingTask, log, extendAlert }); }); } - PublishStatus clear(const String& topic, Retention retain, QoS qos, ticks timeout = ticks::zero(), milliseconds extendAlert = MQTT_ALERT_AFTER_OUTGOING) { + PublishStatus clear(const std::string& topic, Retention retain, QoS qos, ticks timeout = ticks::zero(), milliseconds extendAlert = MQTT_ALERT_AFTER_OUTGOING) { LOGTD("mqtt", "Clearing topic '%s' (qos = %d, timeout = %lld ms)", topic.c_str(), static_cast(qos), @@ -201,16 +201,16 @@ class MqttDriver { * * Note that subscription does not support wildcards. */ - bool subscribe(const String& topic, QoS qos, SubscriptionHandler handler) { + bool subscribe(const std::string& topic, QoS qos, SubscriptionHandler handler) { // Allow some time for the queue to empty return eventQueue.offerIn(MQTT_QUEUE_TIMEOUT, Subscription { topic, qos, handler }); } - static String joinStrings(std::list strings) { + static std::string joinStrings(std::list strings) { if (strings.empty()) { return ""; } - String result; + std::string result; for (auto& str : strings) { result += str + "\n"; } @@ -315,9 +315,9 @@ class MqttDriver { if (!mqttReady.isSet()) { if (client == nullptr) { LOGTD("mqtt", "Connecting to MQTT server"); - String hostname; + std::string hostname; uint32_t port; - if (configHostname.isEmpty()) { + if (configHostname.empty()) { #ifdef WOKWI hostname = "host.wokwi.internal"; port = 1883; @@ -327,9 +327,7 @@ class MqttDriver { LOGTE("mqtt", "Failed to lookup MQTT server"); return false; } - hostname = mqttServer.ip == IPAddress() - ? mqttServer.hostname - : mqttServer.ip.toString(); + hostname = mqttServer.ipOrHost(); port = mqttServer.port; #endif } else { @@ -357,13 +355,13 @@ class MqttDriver { config.broker.address.port, config.credentials.client_id); - if (!configServerCert.isEmpty()) { + if (!configServerCert.empty()) { config.broker.address.transport = MQTT_TRANSPORT_OVER_SSL; config.broker.verification.certificate = configServerCert.c_str(); LOGTV("mqtt", "Server cert:\n%s", config.broker.verification.certificate); - if (!configClientCert.isEmpty() && !configClientKey.isEmpty()) { + if (!configClientCert.empty() && !configClientKey.empty()) { config.credentials.authentication = { .certificate = configClientCert.c_str(), .key = configClientKey.c_str(), @@ -449,8 +447,8 @@ class MqttDriver { break; } case MQTT_EVENT_DATA: { - String topic(event->topic, event->topic_len); - String payload(event->data, event->data_len); + std::string topic(event->topic, event->topic_len); + std::string payload(event->data, event->data_len); LOGTV("mqtt", "Received message on topic '%s'", topic.c_str()); incomingQueue.offerIn(MQTT_QUEUE_TIMEOUT, IncomingMessage { topic, payload }); @@ -556,10 +554,10 @@ class MqttDriver { } void processIncomingMessage(const IncomingMessage& message) { - const String& topic = message.topic; - const String& payload = message.payload; + const std::string& topic = message.topic; + const std::string& payload = message.payload; - if (payload.isEmpty()) { + if (payload.empty()) { LOGTV("mqtt", "Ignoring empty payload"); return; } @@ -609,7 +607,7 @@ class MqttDriver { } } - static String getClientId(const String& clientId, const String& instanceName) { + static std::string getClientId(const std::string& clientId, const std::string& instanceName) { if (clientId.length() > 0) { return clientId; } @@ -621,12 +619,12 @@ class MqttDriver { MdnsDriver& mdns; bool trustMdnsCache = true; - const String configHostname; + const std::string configHostname; const int configPort; - const String configServerCert; - const String configClientCert; - const String configClientKey; - const String clientId; + const std::string configServerCert; + const std::string configClientCert; + const std::string configClientKey; + const std::string clientId; const bool powerSaveMode; StateSource& mqttReady; diff --git a/main/kernel/mqtt/MqttRoot.hpp b/main/kernel/mqtt/MqttRoot.hpp index e4061ad4..ee65960f 100644 --- a/main/kernel/mqtt/MqttRoot.hpp +++ b/main/kernel/mqtt/MqttRoot.hpp @@ -8,37 +8,37 @@ namespace farmhub::kernel::mqtt { class MqttRoot { public: - MqttRoot(MqttDriver& mqtt, const String& rootTopic) + MqttRoot(MqttDriver& mqtt, const std::string& rootTopic) : mqtt(mqtt) , rootTopic(rootTopic) { } - shared_ptr forSuffix(const String& suffix) { + shared_ptr forSuffix(const std::string& suffix) { return make_shared(mqtt, rootTopic + "/" + suffix); } - PublishStatus publish(const String& suffix, const JsonDocument& json, Retention retain = Retention::NoRetain, QoS qos = QoS::AtMostOnce, ticks timeout = ticks::zero(), LogPublish log = LogPublish::Log) { + PublishStatus publish(const std::string& suffix, const JsonDocument& json, Retention retain = Retention::NoRetain, QoS qos = QoS::AtMostOnce, ticks timeout = ticks::zero(), LogPublish log = LogPublish::Log) { return mqtt.publish(fullTopic(suffix), json, retain, qos, timeout, log); } - PublishStatus publish(const String& suffix, std::function populate, Retention retain = Retention::NoRetain, QoS qos = QoS::AtMostOnce, ticks timeout = ticks::zero(), LogPublish log = LogPublish::Log) { + PublishStatus publish(const std::string& suffix, std::function populate, Retention retain = Retention::NoRetain, QoS qos = QoS::AtMostOnce, ticks timeout = ticks::zero(), LogPublish log = LogPublish::Log) { JsonDocument doc; JsonObject root = doc.to(); populate(root); return publish(suffix, doc, retain, qos, timeout, log); } - PublishStatus clear(const String& suffix, Retention retain = Retention::NoRetain, QoS qos = QoS::AtMostOnce, ticks timeout = ticks::zero()) { + PublishStatus clear(const std::string& suffix, Retention retain = Retention::NoRetain, QoS qos = QoS::AtMostOnce, ticks timeout = ticks::zero()) { return mqtt.clear(fullTopic(suffix), retain, qos, timeout); } - bool subscribe(const String& suffix, SubscriptionHandler handler) { + bool subscribe(const std::string& suffix, SubscriptionHandler handler) { return subscribe(suffix, QoS::ExactlyOnce, handler); } - bool registerCommand(const String& name, CommandHandler handler) { - String suffix = "commands/" + name; - return subscribe(suffix, QoS::ExactlyOnce, [this, name, suffix, handler](const String&, const JsonObject& request) { + bool registerCommand(const std::string& name, CommandHandler handler) { + std::string suffix = "commands/" + name; + return subscribe(suffix, QoS::ExactlyOnce, [this, name, suffix, handler](const std::string&, const JsonObject& request) { // TODO Do exponential backoff when clear cannot be finished // Clear topic and wait for it to be cleared auto clearStatus = mqtt.clear(fullTopic(suffix), Retention::Retain, QoS::ExactlyOnce, std::chrono::seconds { 5 }, MQTT_ALERT_AFTER_INCOMING); @@ -67,17 +67,17 @@ class MqttRoot { * * Note that subscription does not support wildcards. */ - bool subscribe(const String& suffix, QoS qos, SubscriptionHandler handler) { + bool subscribe(const std::string& suffix, QoS qos, SubscriptionHandler handler) { return mqtt.subscribe(fullTopic(suffix), qos, handler); } private: - String fullTopic(const String& suffix) const { + std::string fullTopic(const std::string& suffix) const { return rootTopic + "/" + suffix; } MqttDriver& mqtt; - const String rootTopic; + const std::string rootTopic; static constexpr milliseconds MQTT_ALERT_AFTER_INCOMING = 30s; }; diff --git a/main/main.cpp b/main/main.cpp index f66f968a..6e3567c4 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -79,8 +79,6 @@ static void dumpPerTaskHeapInfo() { #include extern "C" void app_main() { - initArduino(); - initLogging(); #ifdef CONFIG_HEAP_TRACING diff --git a/main/peripherals/I2CConfig.hpp b/main/peripherals/I2CConfig.hpp index 90fc3b0f..2af10d14 100644 --- a/main/peripherals/I2CConfig.hpp +++ b/main/peripherals/I2CConfig.hpp @@ -2,8 +2,6 @@ #include -#include - #include #include @@ -18,13 +16,13 @@ class I2CDeviceConfig // 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 address { this, "address" }; Property sda { this, "sda" }; Property scl { this, "scl" }; I2CConfig parse(uint8_t defaultAddress = 0xFF, InternalPinPtr defaultSda = nullptr, InternalPinPtr defaultScl = nullptr) const { return { - address.get().isEmpty() + address.get().empty() ? defaultAddress : (uint8_t) strtol(address.get().c_str(), nullptr, 0), sda.get() == nullptr diff --git a/main/peripherals/Motorized.hpp b/main/peripherals/Motorized.hpp index 78a6aee7..31ef44ef 100644 --- a/main/peripherals/Motorized.hpp +++ b/main/peripherals/Motorized.hpp @@ -2,8 +2,6 @@ #include -#include - #include #include @@ -20,9 +18,9 @@ class Motorized { : motors(motors) { } - PwmMotorDriver& findMotor(const String& motorName) { + PwmMotorDriver& findMotor(const std::string& motorName) { // If there's only one motor and no name is specified, use it - if (motorName.isEmpty() && motors.size() == 1) { + if (motorName.empty() && motors.size() == 1) { return motors.front().get(); } for (auto& motor : motors) { diff --git a/main/peripherals/Peripheral.hpp b/main/peripherals/Peripheral.hpp index ad5ec846..26f87907 100644 --- a/main/peripherals/Peripheral.hpp +++ b/main/peripherals/Peripheral.hpp @@ -1,8 +1,10 @@ #pragma once +#include #include #include +#include #include #include #include @@ -28,14 +30,14 @@ class PeripheralBase : public TelemetryProvider, public Named { public: - PeripheralBase(const String& name, shared_ptr mqttRoot, size_t telemetrySize = 2048) + PeripheralBase(const std::string& name, shared_ptr mqttRoot, size_t telemetrySize = 2048) : Named(name) , mqttRoot(mqttRoot) , telemetrySize(telemetrySize) { mqttRoot->registerCommand("ping", [this](const JsonObject& request, JsonObject& response) { LOGV("Received ping request"); publishTelemetry(); - response["pong"] = millis(); + response["pong"] = duration_cast(boot_clock::now().time_since_epoch()).count(); }); } @@ -74,7 +76,7 @@ template class Peripheral : public PeripheralBase { public: - Peripheral(const String& name, shared_ptr mqttRoot) + Peripheral(const std::string& name, shared_ptr mqttRoot) : PeripheralBase(name, mqttRoot) { } @@ -88,15 +90,15 @@ class Peripheral class PeripheralCreationException : public std::exception { public: - PeripheralCreationException(const String& reason) - : message(String(reason)) { + PeripheralCreationException(const std::string& reason) + : message(std::string(reason)) { } const char* what() const noexcept override { return message.c_str(); } - const String message; + const std::string message; }; struct PeripheralServices { @@ -108,34 +110,34 @@ struct PeripheralServices { class PeripheralFactoryBase { public: - PeripheralFactoryBase(const String& factoryType, const String& peripheralType) + PeripheralFactoryBase(const std::string& factoryType, const std::string& peripheralType) : factoryType(factoryType) , peripheralType(peripheralType) { } - virtual unique_ptr createPeripheral(const String& name, const String& jsonConfig, shared_ptr mqttRoot, PeripheralServices& services, JsonObject& initConfigJson) = 0; + virtual std::expected, std::string> createPeripheral(const std::string& name, const std::string& jsonConfig, shared_ptr mqttRoot, PeripheralServices& services, JsonObject& initConfigJson) = 0; - const String factoryType; - const String peripheralType; + const std::string factoryType; + const std::string peripheralType; }; template class PeripheralFactory : public PeripheralFactoryBase { public: // By default use the factory type as the peripheral type - PeripheralFactory(const String& type, TDeviceConfigArgs... deviceConfigArgs) + PeripheralFactory(const std::string& type, TDeviceConfigArgs... deviceConfigArgs) : PeripheralFactory(type, type, std::forward(deviceConfigArgs)...) { } - PeripheralFactory(const String& factoryType, const String& peripheralType, TDeviceConfigArgs... deviceConfigArgs) + PeripheralFactory(const std::string& factoryType, const std::string& peripheralType, TDeviceConfigArgs... deviceConfigArgs) : PeripheralFactoryBase(factoryType, peripheralType) , deviceConfigArgs(std::forward(deviceConfigArgs)...) { } - unique_ptr createPeripheral(const String& name, const String& jsonConfig, shared_ptr mqttRoot, PeripheralServices& services, JsonObject& initConfigJson) override { + std::expected, std::string> createPeripheral(const std::string& name, const std::string& jsonConfig, shared_ptr mqttRoot, PeripheralServices& services, JsonObject& initConfigJson) 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) { + mqttRoot->subscribe("config", [name, configFile](const std::string&, const JsonObject& configJson) { LOGD("Received configuration update for peripheral: %s", name.c_str()); configFile->update(configJson); }); @@ -144,7 +146,10 @@ class PeripheralFactory : public PeripheralFactoryBase { return TDeviceConfig(std::forward(args)...); }, deviceConfigArgs); - deviceConfig.loadFromString(jsonConfig); + auto result = deviceConfig.loadFromString(jsonConfig); + if (!result) { + return std::unexpected(result.error()); + } unique_ptr> peripheral = createPeripheral(name, deviceConfig, mqttRoot, services); peripheral->configure(configFile->config); @@ -153,7 +158,7 @@ class PeripheralFactory : public PeripheralFactoryBase { return peripheral; } - virtual unique_ptr> createPeripheral(const String& name, const TDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) = 0; + virtual unique_ptr> createPeripheral(const std::string& name, const TDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) = 0; private: std::tuple deviceConfigArgs; @@ -180,47 +185,39 @@ class PeripheralManager factories.insert(std::make_pair(factory.factoryType, std::reference_wrapper(factory))); } - bool createPeripheral(const String& peripheralConfig, JsonArray peripheralsInitJson) { + bool createPeripheral(const std::string& peripheralConfig, JsonArray peripheralsInitJson) { LOGI("Creating peripheral with config: %s", peripheralConfig.c_str()); PeripheralDeviceConfiguration deviceConfig; - try { - deviceConfig.loadFromString(peripheralConfig); - } catch (const std::exception& e) { + auto result = deviceConfig.loadFromString(peripheralConfig); + if (!result) { LOGE("Failed to parse peripheral config because %s:\n%s", - e.what(), peripheralConfig.c_str()); + result.error().c_str(), peripheralConfig.c_str()); return false; } - String name = deviceConfig.name.get(); - String type = deviceConfig.type.get(); + std::string name = deviceConfig.name.get(); + std::string type = deviceConfig.type.get(); JsonObject initJson = peripheralsInitJson.add(); deviceConfig.store(initJson, true); - try { - Lock lock(stateMutex); - if (state == State::Stopped) { - LOGE("Not creating peripheral '%s' because the peripheral manager is stopped", - name.c_str()); - return false; - } - JsonDocument initConfigDoc; - JsonObject initConfigJson = initConfigDoc.to(); - unique_ptr peripheral = createPeripheral(name, type, deviceConfig.params.get().get(), initConfigJson); - initJson["config"].to().set(initConfigJson); - peripherals.push_back(move(peripheral)); - - return true; - } catch (const std::exception& e) { - LOGE("Failed to create '%s' peripheral '%s' because %s", - type.c_str(), name.c_str(), e.what()); - initJson["error"] = String(e.what()); + Lock lock(stateMutex); + if (state == State::Stopped) { + LOGE("Not creating peripheral '%s' because the peripheral manager is stopped", + name.c_str()); return false; - } catch (...) { - LOGE("Failed to create '%s' peripheral '%s' because of an unknown exception", - type.c_str(), name.c_str()); - initJson["error"] = "unknown exception"; + } + JsonDocument initConfigDoc; + JsonObject initConfigJson = initConfigDoc.to(); + auto peripheral = createPeripheral(name, type, deviceConfig.params.get().get(), initConfigJson); + if (!peripheral) { + LOGE("Failed to create '%s' peripheral '%s' because %s", + type.c_str(), name.c_str(), peripheral.error().c_str()); + initJson["error"] = std::string(peripheral.error()); return false; } + initJson["config"].to().set(initConfigJson); + peripherals.push_back(move(peripheral.value())); + return true; } void publishTelemetry() override { @@ -253,19 +250,19 @@ class PeripheralManager private: class PeripheralDeviceConfiguration : public ConfigurationSection { public: - Property name { this, "name", "default" }; - Property type { this, "type" }; + Property name { this, "name", "default" }; + Property type { this, "type" }; Property params { this, "params" }; }; - unique_ptr createPeripheral(const String& name, const String& factoryType, const String& configJson, JsonObject& initConfigJson) { + std::expected, std::string> createPeripheral(const std::string& name, const std::string& factoryType, const std::string& configJson, JsonObject& initConfigJson) { LOGD("Creating peripheral '%s' with factory '%s'", name.c_str(), factoryType.c_str()); auto it = factories.find(factoryType); if (it == factories.end()) { - throw PeripheralCreationException("Factory not found: '" + factoryType + "'"); + return std::unexpected(std::format("Factory not found: '{}'", factoryType)); } - const String& peripheralType = it->second.get().peripheralType; + const std::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, services, initConfigJson); @@ -282,7 +279,7 @@ class PeripheralManager const shared_ptr mqttDeviceRoot; // TODO Use an unordered_map? - std::map> factories; + std::map> factories; Mutex stateMutex; State state = State::Running; std::list> peripherals; diff --git a/main/peripherals/SinglePinDeviceConfig.hpp b/main/peripherals/SinglePinDeviceConfig.hpp index b84d83f0..a32ec816 100644 --- a/main/peripherals/SinglePinDeviceConfig.hpp +++ b/main/peripherals/SinglePinDeviceConfig.hpp @@ -2,8 +2,6 @@ #include -#include - #include using namespace std::chrono; diff --git a/main/peripherals/chicken_door/ChickenDoor.hpp b/main/peripherals/chicken_door/ChickenDoor.hpp index 12a5ccad..12ffafb7 100644 --- a/main/peripherals/chicken_door/ChickenDoor.hpp +++ b/main/peripherals/chicken_door/ChickenDoor.hpp @@ -5,8 +5,6 @@ #include #include -#include - #include #include #include @@ -59,8 +57,8 @@ void convertFromJson(JsonVariantConst src, OperationState& dst) { class ChickenDoorLightSensorConfig : public I2CDeviceConfig { public: - Property type { this, "type", "bh1750" }; - Property i2c { this, "i2c" }; + Property type { this, "type", "bh1750" }; + Property i2c { this, "i2c" }; Property measurementFrequency { this, "measurementFrequency", 1s }; Property latencyInterval { this, "latencyInterval", 5s }; }; @@ -68,7 +66,7 @@ class ChickenDoorLightSensorConfig class ChickenDoorDeviceConfig : public ConfigurationSection { public: - Property motor { this, "motor" }; + Property motor { this, "motor" }; Property openPin { this, "openPin" }; Property closedPin { this, "closedPin" }; Property movementTimeout { this, "movementTimeout", seconds(60) }; @@ -88,7 +86,7 @@ class ChickenDoorComponent public TelemetryProvider { public: ChickenDoorComponent( - const String& name, + const std::string& name, shared_ptr mqttRoot, SwitchManager& switches, PwmMotorDriver& motor, @@ -155,7 +153,7 @@ class ChickenDoorComponent auto timeinfo = gmtime(&rawtime); char buffer[80]; strftime(buffer, 80, "%FT%TZ", timeinfo); - telemetry["overrideEnd"] = String(buffer); + telemetry["overrideEnd"] = std::string(buffer); telemetry["overrideState"] = overrideState; } } @@ -354,7 +352,7 @@ class ChickenDoor : public Peripheral { public: ChickenDoor( - const String& name, + const std::string& name, shared_ptr mqttRoot, I2CManager& i2c, uint8_t lightSensorAddress, @@ -400,7 +398,7 @@ class ChickenDoor class NoLightSensorComponent : public LightSensorComponent { public: NoLightSensorComponent( - const String& name, + const std::string& name, shared_ptr mqttRoot, I2CManager& i2c, I2CConfig config, @@ -425,7 +423,7 @@ class ChickenDoorFactory , Motorized(motors) { } - unique_ptr> createPeripheral(const String& name, const ChickenDoorDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { + unique_ptr> createPeripheral(const std::string& name, const ChickenDoorDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { PwmMotorDriver& motor = findMotor(deviceConfig.motor.get()); auto lightSensorType = deviceConfig.lightSensor.get().type.get(); try { diff --git a/main/peripherals/environment/Ds18B20SoilSensor.hpp b/main/peripherals/environment/Ds18B20SoilSensor.hpp index 5cf649fd..1a003518 100644 --- a/main/peripherals/environment/Ds18B20SoilSensor.hpp +++ b/main/peripherals/environment/Ds18B20SoilSensor.hpp @@ -2,14 +2,11 @@ #include -#include +#include -#include -#include - -#include -#include #include +#include +#include #include using namespace farmhub::kernel; @@ -29,67 +26,49 @@ class Ds18B20SoilSensorComponent public TelemetryProvider { public: Ds18B20SoilSensorComponent( - const String& name, + const std::string& name, shared_ptr mqttRoot, InternalPinPtr pin) - : Component(name, mqttRoot) { + : Component(name, mqttRoot) + , pin(pin) { LOGI("Initializing DS18B20 soil temperature sensor on pin %s", pin->getName().c_str()); - oneWire.begin(pin->getGpio()); - - // locate devices on the bus - LOGV("Locating devices..."); - sensors.begin(); - LOGD("Found %d devices, parasitic power is %s", - sensors.getDeviceCount(), - sensors.isParasitePowerMode() ? "ON" : "OFF"); - - DeviceAddress thermometer; - if (!sensors.getAddress(thermometer, 0)) { - throw PeripheralCreationException("unable to find address for device"); + gpio_set_pull_mode(pin->getGpio(), GPIO_PULLUP_ONLY); + + LOGV("Locating DS18B20 sensors on bus..."); + size_t sensorCount; + // TODO How many slots do we need here actually? + int maxSensors = 1; + + esp_err_t searchResult = ds18x20_scan_devices(pin->getGpio(), &sensor, maxSensors, &sensorCount); + if (searchResult == ESP_OK) { + if (!sensorCount) { + throw PeripheralCreationException("No DS18B20 sensors found on bus"); + } + LOGD("Found a DS18B20 at address: %016llX", sensor); + } else { + throw PeripheralCreationException("Error searching for DS18B20 devices: " + std::string(esp_err_to_name(searchResult))); } - - // show the addresses we found on the bus - LOGD("Device 0 Address: %s", - toStringAddress(thermometer).c_str()); } void populateTelemetry(JsonObject& json) override { - if (!sensors.requestTemperaturesByIndex(0)) { - LOGE("Failed to get temperature from DS18B20 sensor"); - return; - } - float temperature = sensors.getTempCByIndex(0); - if (temperature == DEVICE_DISCONNECTED_C) { - LOGE("Failed to get temperature from DS18B20 sensor"); - return; - } + // TODO Get temperature in a task to avoid delaying reporting + float temperature; + ESP_ERROR_CHECK(ds18x20_measure_and_read_multi(pin->getGpio(), &sensor, 1, &temperature)); json["temperature"] = temperature; } private: - - OneWire oneWire; - - // Pass our oneWire reference to Dallas Temperature. - DallasTemperature sensors { &oneWire }; - - String toStringAddress(DeviceAddress address) { - char result[17]; - for (int i = 0; i < 8; i++) { - sprintf(&result[i * 2], "%02X", address[i]); - } - result[16] = '\0'; - return result; - } + const InternalPinPtr pin; + onewire_addr_t sensor; }; class Ds18B20SoilSensor : public Peripheral { public: - Ds18B20SoilSensor(const String& name, shared_ptr mqttRoot, InternalPinPtr pin) + Ds18B20SoilSensor(const std::string& name, shared_ptr mqttRoot, InternalPinPtr pin) : Peripheral(name, mqttRoot) , sensor(name, mqttRoot, pin) { } @@ -109,7 +88,7 @@ class Ds18B20SoilSensorFactory : PeripheralFactory("environment:ds18b20", "environment") { } - unique_ptr> createPeripheral(const String& name, const SinglePinDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { + unique_ptr> createPeripheral(const std::string& name, const SinglePinDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { return make_unique(name, mqttRoot, deviceConfig.pin.get()); } }; diff --git a/main/peripherals/environment/Environment.hpp b/main/peripherals/environment/Environment.hpp index 678f7a6d..675d6bee 100644 --- a/main/peripherals/environment/Environment.hpp +++ b/main/peripherals/environment/Environment.hpp @@ -21,8 +21,8 @@ class Environment : public Peripheral { public: Environment( - const String& name, - const String& sensorType, + const std::string& name, + const std::string& sensorType, shared_ptr mqttRoot, I2CManager& i2c, I2CConfig config) @@ -42,13 +42,13 @@ template class I2CEnvironmentFactory : public PeripheralFactory { public: - I2CEnvironmentFactory(const String& sensorType, uint8_t defaultAddress) + I2CEnvironmentFactory(const std::string& sensorType, uint8_t defaultAddress) : PeripheralFactory("environment:" + sensorType, "environment") , sensorType(sensorType) , defaultAddress(defaultAddress) { } - unique_ptr> createPeripheral(const String& name, const I2CDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { + unique_ptr> createPeripheral(const std::string& name, const I2CDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { auto i2cConfig = deviceConfig.parse(defaultAddress); LOGI("Creating %s sensor %s with %s", sensorType.c_str(), name.c_str(), i2cConfig.toString().c_str()); @@ -56,7 +56,7 @@ class I2CEnvironmentFactory } private: - const String sensorType; + const std::string sensorType; const uint8_t defaultAddress; }; diff --git a/main/peripherals/environment/Sht2xComponent.hpp b/main/peripherals/environment/Sht2xComponent.hpp index 3682838f..9cefddc2 100644 --- a/main/peripherals/environment/Sht2xComponent.hpp +++ b/main/peripherals/environment/Sht2xComponent.hpp @@ -1,8 +1,8 @@ #pragma once -#include +#include -#include +#include #include #include @@ -17,21 +17,20 @@ using namespace farmhub::peripherals; namespace farmhub::peripherals::environment { /** - * @tparam TSensor Works with SHT2x or HTU2x + * @brief Works with SHT2x or HTU2x. */ -template class Sht2xComponent : public Component, public TelemetryProvider { public: Sht2xComponent( - const String& name, - const String& sensorType, + const std::string& name, + const std::string& sensorType, shared_ptr mqttRoot, I2CManager& i2c, I2CConfig config) : Component(name, mqttRoot) - , sensor(&i2c.getWireFor(config)) { + , bus(i2c.getBusFor(config)) { // TODO Add commands to soft/hard reset the sensor // TODO Add configuration for fast / slow measurement @@ -40,26 +39,40 @@ class Sht2xComponent LOGI("Initializing %s environment sensor with %s", sensorType.c_str(), config.toString().c_str()); - if (!sensor.begin()) { - throw PeripheralCreationException("failed to initialize environment sensor: 0x" + String(sensor.getError(), HEX)); - } - if (!sensor.isConnected()) { - throw PeripheralCreationException("environment sensor is not connected: 0x" + String(sensor.getError(), HEX)); - } + memset(&sensor, 0, sizeof(i2c_dev_t)); + ESP_ERROR_CHECK(si7021_init_desc(&sensor, bus->port, bus->sda->getGpio(), bus->scl->getGpio())); } void populateTelemetry(JsonObject& json) override { - if (!sensor.read()) { - LOGE("Failed to read environment sensor: %d", - sensor.getError()); - return; - } - json["temperature"] = sensor.getTemperature(); - json["humidity"] = sensor.getHumidity(); + json["temperature"] = getTemperature(); + json["humidity"] = getHumidity(); } private: - TSensor sensor; + float getTemperature() { + float value; + esp_err_t res = si7021_measure_temperature(&sensor, &value); + if (res != ESP_OK) { + LOGD("Could not measure temperature: %s", esp_err_to_name(res)); + return std::numeric_limits::quiet_NaN(); + } else { + return value; + } + } + + float getHumidity() { + float value; + esp_err_t res = si7021_measure_humidity(&sensor, &value); + if (res != ESP_OK) { + LOGD("Could not measure humidity: %s", esp_err_to_name(res)); + return std::numeric_limits::quiet_NaN(); + } else { + return value; + } + } + + shared_ptr bus; + i2c_dev_t sensor; }; } // namespace farmhub::peripherals::environment diff --git a/main/peripherals/environment/Sht31Component.hpp b/main/peripherals/environment/Sht3xComponent.hpp similarity index 53% rename from main/peripherals/environment/Sht31Component.hpp rename to main/peripherals/environment/Sht3xComponent.hpp index 18377343..5c3cacff 100644 --- a/main/peripherals/environment/Sht31Component.hpp +++ b/main/peripherals/environment/Sht3xComponent.hpp @@ -1,8 +1,8 @@ #pragma once -#include +#include -#include +#include #include #include @@ -16,18 +16,18 @@ using namespace farmhub::peripherals; namespace farmhub::peripherals::environment { -class Sht31Component +class Sht3xComponent : public Component, public TelemetryProvider { public: - Sht31Component( - const String& name, - const String& sensorType, + Sht3xComponent( + const std::string& name, + const std::string& sensorType, shared_ptr mqttRoot, I2CManager& i2c, I2CConfig config) : Component(name, mqttRoot) - , sensor(config.address, &i2c.getWireFor(config)) { + , bus(i2c.getBusFor(config)) { // TODO Add commands to soft/hard reset the sensor // TODO Add configuration for fast / slow measurement @@ -36,26 +36,27 @@ class Sht31Component LOGI("Initializing %s environment sensor with %s", sensorType.c_str(), config.toString().c_str()); - if (!sensor.begin()) { - throw PeripheralCreationException("Failed to initialize environment sensor: " + String(sensor.getError())); - } - if (!sensor.isConnected()) { - throw PeripheralCreationException("Environment sensor is not connected: " + String(sensor.getError())); - } + ESP_ERROR_CHECK(sht3x_init_desc(&sensor, config.address, bus->port, bus->sda->getGpio(), bus->scl->getGpio())); + ESP_ERROR_CHECK(sht3x_init(&sensor)); } void populateTelemetry(JsonObject& json) override { - if (!sensor.read()) { - LOGE("Failed to read SHT3x environment sensor: %d", - sensor.getError()); - return; + float temperature; + float humidity; + esp_err_t res = sht3x_measure(&sensor, &temperature, &humidity); + if (res != ESP_OK) { + LOGD("Could not measure temperature: %s", esp_err_to_name(res)); + temperature = std::numeric_limits::quiet_NaN(); + humidity = std::numeric_limits::quiet_NaN(); } - json["temperature"] = sensor.getTemperature(); - json["humidity"] = sensor.getHumidity(); + + json["temperature"] = temperature; + json["humidity"] = humidity; } private: - SHT31 sensor; + shared_ptr bus; + sht3x_t sensor; }; } // namespace farmhub::peripherals::environment diff --git a/main/peripherals/environment/SoilMoistureSensor.hpp b/main/peripherals/environment/SoilMoistureSensor.hpp index 7590e2b3..c4c14fd9 100644 --- a/main/peripherals/environment/SoilMoistureSensor.hpp +++ b/main/peripherals/environment/SoilMoistureSensor.hpp @@ -2,9 +2,6 @@ #include -#include -#include - #include #include @@ -29,7 +26,7 @@ class SoilMoistureSensorComponent public TelemetryProvider { public: SoilMoistureSensorComponent( - const String& name, + const std::string& name, shared_ptr mqttRoot, const SoilMoistureSensorDeviceConfig& config) : Component(name, mqttRoot) @@ -38,13 +35,11 @@ class SoilMoistureSensorComponent , pin(config.pin.get()) { LOGI("Initializing soil moisture sensor on pin %s; air value: %d; water value: %d", - pin->getName().c_str(), airValue, waterValue); - - pin->pinMode(INPUT); + pin.getName().c_str(), airValue, waterValue); } void populateTelemetry(JsonObject& json) override { - uint16_t soilMoistureValue = pin->analogRead(); + uint16_t soilMoistureValue = pin.analogRead(); LOGV("Soil moisture value: %d", soilMoistureValue); @@ -59,13 +54,13 @@ class SoilMoistureSensorComponent private: const int airValue; const int waterValue; - InternalPinPtr pin; + AnalogPin pin; }; class SoilMoistureSensor : public Peripheral { public: - SoilMoistureSensor(const String& name, shared_ptr mqttRoot, const SoilMoistureSensorDeviceConfig& config) + SoilMoistureSensor(const std::string& name, shared_ptr mqttRoot, const SoilMoistureSensorDeviceConfig& config) : Peripheral(name, mqttRoot) , sensor(name, mqttRoot, config) { } @@ -85,7 +80,7 @@ class SoilMoistureSensorFactory : PeripheralFactory("environment:soil-moisture", "environment") { } - unique_ptr> createPeripheral(const String& name, const SoilMoistureSensorDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { + unique_ptr> createPeripheral(const std::string& name, const SoilMoistureSensorDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { return std::make_unique(name, mqttRoot, deviceConfig); } }; diff --git a/main/peripherals/fence/ElectricFenceMonitor.hpp b/main/peripherals/fence/ElectricFenceMonitor.hpp index e6fd0324..c75f238b 100644 --- a/main/peripherals/fence/ElectricFenceMonitor.hpp +++ b/main/peripherals/fence/ElectricFenceMonitor.hpp @@ -3,8 +3,6 @@ #include #include -#include - #include #include #include @@ -45,17 +43,17 @@ class ElectricFenceMonitorComponent public TelemetryProvider { public: ElectricFenceMonitorComponent( - const String& name, + const std::string& name, shared_ptr mqttRoot, PcntManager& pcnt, const ElectricFenceMonitorDeviceConfig& config) : Component(name, mqttRoot) { - String pinsDescription; + std::string pinsDescription; for (auto& pinConfig : config.pins.get()) { if (pinsDescription.length() > 0) pinsDescription += ", "; - pinsDescription += pinConfig.pin->getName() + "=" + String(pinConfig.voltage) + "V"; + pinsDescription += std::format("{}={}V", pinConfig.pin->getName(), pinConfig.voltage); } LOGI("Initializing electric fence with pins %s", pinsDescription.c_str()); @@ -72,7 +70,7 @@ class ElectricFenceMonitorComponent int16_t count = pin.pcntUnit.getAndClearCount(); if (count > 0) { - lastVoltage = max(pin.voltage, lastVoltage); + lastVoltage = std::max(pin.voltage, lastVoltage); LOGV("Counted %d pulses on pin %s (voltage: %dV)", count, pin.pcntUnit.getPin()->getName().c_str(), pin.voltage); } @@ -107,7 +105,7 @@ class ElectricFenceMonitorComponent class ElectricFenceMonitor : public Peripheral { public: - ElectricFenceMonitor(const String& name, shared_ptr mqttRoot, PcntManager& pcnt, const ElectricFenceMonitorDeviceConfig& config) + ElectricFenceMonitor(const std::string& name, shared_ptr mqttRoot, PcntManager& pcnt, const ElectricFenceMonitorDeviceConfig& config) : Peripheral(name, mqttRoot) , monitor(name, mqttRoot, pcnt, config) { } @@ -127,7 +125,7 @@ class ElectricFenceMonitorFactory : PeripheralFactory("electric-fence") { } - unique_ptr> createPeripheral(const String& name, const ElectricFenceMonitorDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { + unique_ptr> createPeripheral(const std::string& name, const ElectricFenceMonitorDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { return std::make_unique(name, mqttRoot, services.pcntManager, deviceConfig); } }; diff --git a/main/peripherals/flow_control/FlowControl.hpp b/main/peripherals/flow_control/FlowControl.hpp index b75ec967..4e675ffc 100644 --- a/main/peripherals/flow_control/FlowControl.hpp +++ b/main/peripherals/flow_control/FlowControl.hpp @@ -29,7 +29,7 @@ class FlowControlConfig class FlowControl : public Peripheral { public: FlowControl( - const String& name, + const std::string& name, shared_ptr mqttRoot, PcntManager& pcnt, ValveControlStrategy& strategy, @@ -84,7 +84,7 @@ class FlowControlFactory , Motorized(motors) { } - unique_ptr> createPeripheral(const String& name, const FlowControlDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { + unique_ptr> createPeripheral(const std::string& name, const FlowControlDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { auto strategy = deviceConfig.valve.get().createValveControlStrategy(this); auto flowMeterConfig = deviceConfig.flowMeter.get(); diff --git a/main/peripherals/flow_meter/FlowMeter.hpp b/main/peripherals/flow_meter/FlowMeter.hpp index 1c9af51e..a328bcd9 100644 --- a/main/peripherals/flow_meter/FlowMeter.hpp +++ b/main/peripherals/flow_meter/FlowMeter.hpp @@ -19,7 +19,7 @@ namespace farmhub::peripherals::flow_meter { class FlowMeter : public Peripheral { public: - FlowMeter(const String& name, shared_ptr mqttRoot, PcntManager& pcnt, InternalPinPtr pin, double qFactor, milliseconds measurementFrequency) + FlowMeter(const std::string& name, shared_ptr mqttRoot, PcntManager& pcnt, InternalPinPtr pin, double qFactor, milliseconds measurementFrequency) : Peripheral(name, mqttRoot) , flowMeter(name, mqttRoot, pcnt, pin, qFactor, measurementFrequency) { } @@ -39,7 +39,7 @@ class FlowMeterFactory : PeripheralFactory("flow-meter") { } - unique_ptr> createPeripheral(const String& name, const FlowMeterDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { + unique_ptr> createPeripheral(const std::string& name, const FlowMeterDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { return make_unique(name, mqttRoot, services.pcntManager, deviceConfig.pin.get(), deviceConfig.qFactor.get(), deviceConfig.measurementFrequency.get()); } }; diff --git a/main/peripherals/flow_meter/FlowMeterComponent.hpp b/main/peripherals/flow_meter/FlowMeterComponent.hpp index 24bf48fe..4e163555 100644 --- a/main/peripherals/flow_meter/FlowMeterComponent.hpp +++ b/main/peripherals/flow_meter/FlowMeterComponent.hpp @@ -2,7 +2,6 @@ #include -#include #include #include @@ -22,7 +21,7 @@ class FlowMeterComponent public TelemetryProvider { public: FlowMeterComponent( - const String& name, + const std::string& name, shared_ptr mqttRoot, PcntManager& pcnt, InternalPinPtr pin, diff --git a/main/peripherals/light_sensor/Bh1750.hpp b/main/peripherals/light_sensor/Bh1750.hpp index cb462fa3..f0419189 100644 --- a/main/peripherals/light_sensor/Bh1750.hpp +++ b/main/peripherals/light_sensor/Bh1750.hpp @@ -4,10 +4,9 @@ #include #include -#include -#include +#include -#include +#include #include #include @@ -37,36 +36,37 @@ class Bh1750Component : public LightSensorComponent { public: Bh1750Component( - const String& name, + const std::string& name, shared_ptr mqttRoot, I2CManager& i2c, I2CConfig config, seconds measurementFrequency, seconds latencyInterval) - : LightSensorComponent(name, mqttRoot, measurementFrequency, latencyInterval) - , sensor(config.address) { + : LightSensorComponent(name, mqttRoot, measurementFrequency, latencyInterval) { LOGI("Initializing BH1750 light sensor with %s", config.toString().c_str()); - // TODO Make mode configurable - // TODO What's the difference between one-time and continuous mode here? - // Can we save some battery by using one-time mode? Are we losing anything by doing so? - TwoWire& wire = i2c.getWireFor(config); - if (!sensor.begin(BH1750::CONTINUOUS_LOW_RES_MODE, config.address, &wire)) { - throw PeripheralCreationException("Failed to initialize BH1750 light sensor"); - } + memset(&sensor, 0, sizeof(i2c_dev_t)); // Zero descriptor + + // TODO Use I2CManager to create device + ESP_ERROR_CHECK(bh1750_init_desc(&sensor, config.address, I2C_NUM_0, config.sda->getGpio(), config.scl->getGpio())); + ESP_ERROR_CHECK(bh1750_setup(&sensor, BH1750_MODE_CONTINUOUS, BH1750_RES_LOW)); runLoop(); } protected: double readLightLevel() override { - return sensor.readLightLevel(); + uint16_t lightLevel; + if (bh1750_read(&sensor, &lightLevel) != ESP_OK) { + LOGE("Could not read light level"); + } + return lightLevel; } private: - BH1750 sensor; + i2c_dev_t sensor; }; class Bh1750 @@ -74,7 +74,7 @@ class Bh1750 public: Bh1750( - const String& name, + const std::string& name, shared_ptr mqttRoot, I2CManager& i2c, const I2CConfig& config, @@ -99,7 +99,7 @@ class Bh1750Factory : PeripheralFactory("light-sensor:bh1750", "light-sensor") { } - unique_ptr> createPeripheral(const String& name, const Bh1750DeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { + unique_ptr> createPeripheral(const std::string& name, const Bh1750DeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { I2CConfig i2cConfig = deviceConfig.parse(0x23); return std::make_unique(name, mqttRoot, services.i2c, i2cConfig, deviceConfig.measurementFrequency.get(), deviceConfig.latencyInterval.get()); } diff --git a/main/peripherals/light_sensor/LightSensor.hpp b/main/peripherals/light_sensor/LightSensor.hpp index e42e6b00..a124d0d5 100644 --- a/main/peripherals/light_sensor/LightSensor.hpp +++ b/main/peripherals/light_sensor/LightSensor.hpp @@ -4,11 +4,6 @@ #include #include -#include -#include - -#include - #include #include #include @@ -31,7 +26,7 @@ class LightSensorComponent public TelemetryProvider { public: LightSensorComponent( - const String& name, + const std::string& name, shared_ptr mqttRoot, seconds measurementFrequency, seconds latencyInterval) diff --git a/main/peripherals/light_sensor/Tsl2591.hpp b/main/peripherals/light_sensor/Tsl2591.hpp index ec77daad..586a6168 100644 --- a/main/peripherals/light_sensor/Tsl2591.hpp +++ b/main/peripherals/light_sensor/Tsl2591.hpp @@ -4,11 +4,7 @@ #include #include -#include -#include - -#include -#include +#include #include #include @@ -27,6 +23,8 @@ using namespace farmhub::peripherals; namespace farmhub::peripherals::light_sensor { +static constexpr uint8_t TSL2591_ADDR = 0x29; + class Tsl2591DeviceConfig : public I2CDeviceConfig { public: @@ -38,40 +36,45 @@ class Tsl2591Component : public LightSensorComponent { public: Tsl2591Component( - const String& name, + const std::string& name, shared_ptr mqttRoot, I2CManager& i2c, I2CConfig config, seconds measurementFrequency, seconds latencyInterval) - : LightSensorComponent(name, mqttRoot, measurementFrequency, latencyInterval) { + : LightSensorComponent(name, mqttRoot, measurementFrequency, latencyInterval) + , bus(i2c.getBusFor(config)) { LOGI("Initializing TSL2591 light sensor with %s", config.toString().c_str()); - if (!sensor.begin(&i2c.getWireFor(config), config.address)) { - throw PeripheralCreationException("Failed to initialize TSL2591 light sensor"); - } + ESP_ERROR_CHECK(tsl2591_init_desc(&sensor, bus->port, bus->sda->getGpio(), bus->scl->getGpio())); + ESP_ERROR_CHECK(tsl2591_init(&sensor)); // TODO Make these configurable - sensor.setGain(TSL2591_GAIN_MED); - sensor.setTiming(TSL2591_INTEGRATIONTIME_300MS); - - sensor_t sensorInfo; - sensor.getSensor(&sensorInfo); - LOGD("Found sensor: %s, driver version: %ld, unique ID: %ld, max value: %.2f lux, min value: %.2f lux, resolution: %.2f mlux", - sensorInfo.name, sensorInfo.version, sensorInfo.sensor_id, sensorInfo.max_value, sensorInfo.min_value, sensorInfo.resolution * 1000); + ESP_ERROR_CHECK(tsl2591_set_power_status(&sensor, TSL2591_POWER_ON)); + ESP_ERROR_CHECK(tsl2591_set_als_status(&sensor, TSL2591_ALS_ON)); + ESP_ERROR_CHECK(tsl2591_set_gain(&sensor, TSL2591_GAIN_MEDIUM)); + ESP_ERROR_CHECK(tsl2591_set_integration_time(&sensor, TSL2591_INTEGRATION_300MS)); runLoop(); } protected: double readLightLevel() override { - return sensor.getLuminosity(TSL2591_VISIBLE); + esp_err_t res; + float lux; + if ((res = tsl2591_get_lux(&sensor, &lux)) != ESP_OK) { + LOGD("Could not read light level: %s", esp_err_to_name(res)); + return std::numeric_limits::quiet_NaN(); + } else { + return lux; + } } private: - Adafruit_TSL2591 sensor; + shared_ptr bus; + tsl2591_t sensor; }; class Tsl2591 @@ -79,7 +82,7 @@ class Tsl2591 public: Tsl2591( - const String& name, + const std::string& name, shared_ptr mqttRoot, I2CManager& i2c, const I2CConfig& config, @@ -104,7 +107,7 @@ class Tsl2591Factory : PeripheralFactory("light-sensor:tsl2591", "light-sensor") { } - unique_ptr> createPeripheral(const String& name, const Tsl2591DeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { + unique_ptr> createPeripheral(const std::string& name, const Tsl2591DeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { I2CConfig i2cConfig = deviceConfig.parse(TSL2591_ADDR); return std::make_unique(name, mqttRoot, services.i2c, i2cConfig, deviceConfig.measurementFrequency.get(), deviceConfig.latencyInterval.get()); } diff --git a/main/peripherals/multiplexer/Xl9535.hpp b/main/peripherals/multiplexer/Xl9535.hpp index d13c0ce6..6a5976fb 100644 --- a/main/peripherals/multiplexer/Xl9535.hpp +++ b/main/peripherals/multiplexer/Xl9535.hpp @@ -1,8 +1,5 @@ #pragma once -#include -#include - #include #include #include @@ -18,7 +15,7 @@ class Xl9535Component public: Xl9535Component( - const String& name, + const std::string& name, shared_ptr mqttRoot, I2CManager& i2c, I2CConfig config) @@ -28,8 +25,9 @@ class Xl9535Component config.toString().c_str()); } - void pinMode(uint8_t pin, uint8_t mode) { - if (mode == OUTPUT) { + void pinMode(uint8_t pin, Pin::Mode mode) { + // TODO Signal if pull-up or pull-down is requested that we cannot support it + if (mode == Pin::Mode::Output) { direction &= ~(1 << pin); } else { direction |= 1 << pin; @@ -42,7 +40,7 @@ class Xl9535Component } void digitalWrite(uint8_t pin, uint8_t val) { - if (val == HIGH) { + if (val == 1) { output |= 1 << pin; } else { output &= ~(1 << pin); @@ -55,36 +53,25 @@ class Xl9535Component } int digitalRead(uint8_t pin) { - I2CTransmission tx(device); - tx.write(pin < 8 ? 0x00 : 0x01); - tx.requestFrom(1); - uint8_t data = tx.read(); + uint8_t data = device->readRegByte(pin < 8 ? 0x00 : 0x01); return (data >> (pin % 8)) & 1; } private: void updateDirection1() { - I2CTransmission tx(device); - tx.write(0x06); - tx.write(direction & 0xFF); + device->writeRegByte(0x06, direction & 0xFF); } void updateDirection2() { - I2CTransmission tx(device); - tx.write(0x07); - tx.write(direction >> 8); + device->writeRegByte(0x07, direction >> 8); } void updateOutput1() { - I2CTransmission tx(device); - tx.write(0x02); - tx.write(output & 0xFF); + device->writeRegByte(0x02, output & 0xFF); } void updateOutput2() { - I2CTransmission tx(device); - tx.write(0x03); - tx.write(output >> 8); + device->writeRegByte(0x03, output >> 8); } shared_ptr device; @@ -96,13 +83,13 @@ class Xl9535Component class Xl9535Pin : public Pin { public: - Xl9535Pin(const String& name, Xl9535Component& mpx, uint8_t pin) + Xl9535Pin(const std::string& name, Xl9535Component& mpx, uint8_t pin) : Pin(name) , mpx(mpx) , pin(pin) { } - void pinMode(uint8_t mode) const override { + void pinMode(Mode mode) const override { mpx.pinMode(pin, mode); } @@ -122,13 +109,13 @@ class Xl9535Pin : public Pin { class Xl9535 : public Peripheral { public: - Xl9535(const String& name, shared_ptr mqttRoot, I2CManager& i2c, I2CConfig config) + Xl9535(const std::string& name, shared_ptr mqttRoot, I2CManager& i2c, I2CConfig config) : Peripheral(name, mqttRoot) , component(name, mqttRoot, i2c, config) { // Create a pin for each bit in the pins mask for (int i = 0; i < 16; i++) { - String pinName = name + ":" + String(i); + std::string pinName = std::format("{}:{}", name, i); LOGV("Registering external pin %s", pinName.c_str()); auto pin = std::make_shared(pinName, component, i); @@ -147,7 +134,7 @@ class Xl9535Factory : PeripheralFactory("multiplexer:xl9535") { } - unique_ptr> createPeripheral(const String& name, const Xl9535DeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { + unique_ptr> createPeripheral(const std::string& name, const Xl9535DeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { return make_unique(name, mqttRoot, services.i2c, deviceConfig.parse()); } }; diff --git a/main/peripherals/valve/Valve.hpp b/main/peripherals/valve/Valve.hpp index 9232d36e..35ba12ee 100644 --- a/main/peripherals/valve/Valve.hpp +++ b/main/peripherals/valve/Valve.hpp @@ -4,8 +4,6 @@ #include #include -#include - #include #include @@ -33,7 +31,7 @@ class Valve : public Peripheral { public: Valve( - const String& name, + const std::string& name, ValveControlStrategy& strategy, shared_ptr mqttRoot) : Peripheral(name, mqttRoot) @@ -69,7 +67,7 @@ class ValveFactory , Motorized(motors) { } - unique_ptr> createPeripheral(const String& name, const ValveDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { + unique_ptr> createPeripheral(const std::string& name, const ValveDeviceConfig& deviceConfig, shared_ptr mqttRoot, PeripheralServices& services) override { auto strategy = deviceConfig.createValveControlStrategy(this); return make_unique(name, *strategy, mqttRoot); } diff --git a/main/peripherals/valve/ValveComponent.hpp b/main/peripherals/valve/ValveComponent.hpp index 0709f1ae..75cee77a 100644 --- a/main/peripherals/valve/ValveComponent.hpp +++ b/main/peripherals/valve/ValveComponent.hpp @@ -6,8 +6,6 @@ #include #include -#include - #include #include @@ -38,7 +36,7 @@ class ValveControlStrategy { virtual void close() = 0; virtual ValveState getDefaultState() const = 0; - virtual String describe() const = 0; + virtual std::string describe() const = 0; }; class MotorValveControlStrategy @@ -83,7 +81,7 @@ class HoldingMotorValveControlStrategy private: void driveAndHold(MotorPhase phase) { controller.drive(phase, 1.0); - delay(switchDuration.count()); + Task::delay(switchDuration); controller.drive(phase, holdDuty); } }; @@ -107,8 +105,8 @@ class NormallyClosedMotorValveControlStrategy return ValveState::CLOSED; } - String describe() const override { - return "normally closed with switch duration " + String((int) switchDuration.count()) + "ms and hold duty " + String(holdDuty * 100) + "%"; + std::string describe() const override { + return std::format("normally closed with switch duration {} ms and hold duty {}%", switchDuration.count(), holdDuty * 100); } }; @@ -131,8 +129,8 @@ class NormallyOpenMotorValveControlStrategy return ValveState::OPEN; } - String describe() const override { - return "normally open with switch duration " + String((int) switchDuration.count()) + "ms and hold duty " + String(holdDuty * 100) + "%"; + std::string describe() const override { + return std::format("normally open with switch duration {} ms and hold duty {}%", switchDuration.count(), holdDuty * 100); } }; @@ -147,13 +145,13 @@ class LatchingMotorValveControlStrategy void open() override { controller.drive(MotorPhase::FORWARD, switchDuty); - delay(switchDuration.count()); + Task::delay(switchDuration); controller.stop(); } void close() override { controller.drive(MotorPhase::REVERSE, switchDuty); - delay(switchDuration.count()); + Task::delay(switchDuration); controller.stop(); } @@ -161,8 +159,8 @@ class LatchingMotorValveControlStrategy return ValveState::NONE; } - String describe() const override { - return "latching with switch duration " + String((int) switchDuration.count()) + "ms with switch duty " + String(switchDuty * 100) + "%"; + std::string describe() const override { + return std::format("latching with switch duration {} ms and switch duty {}%", switchDuration.count(), switchDuty * 100); } private: @@ -175,22 +173,22 @@ class LatchingPinValveControlStrategy public: LatchingPinValveControlStrategy(PinPtr pin) : pin(pin) { - pin->pinMode(OUTPUT); + pin->pinMode(Pin::Mode::Output); } void open() override { - pin->digitalWrite(HIGH); + pin->digitalWrite(1); } void close() override { - pin->digitalWrite(LOW); + pin->digitalWrite(0); } ValveState getDefaultState() const override { return ValveState::NONE; } - String describe() const override { + std::string describe() const override { return "latching with pin " + pin->getName(); } @@ -201,7 +199,7 @@ class LatchingPinValveControlStrategy class ValveComponent : public Component { public: ValveComponent( - const String& name, + const std::string& name, ValveControlStrategy& strategy, shared_ptr mqttRoot, std::function publishTelemetry) @@ -315,7 +313,7 @@ class ValveComponent : public Component { auto timeinfo = gmtime(&rawtime); char buffer[80]; strftime(buffer, 80, "%FT%TZ", timeinfo); - telemetry["overrideEnd"] = String(buffer); + telemetry["overrideEnd"] = std::string(buffer); telemetry["overrideState"] = this->overrideState.load(); } } diff --git a/main/peripherals/valve/ValveConfig.hpp b/main/peripherals/valve/ValveConfig.hpp index b38a42a9..535e538c 100644 --- a/main/peripherals/valve/ValveConfig.hpp +++ b/main/peripherals/valve/ValveConfig.hpp @@ -48,7 +48,7 @@ class ValveDeviceConfig * * @details When the pin is specified, this is ignored. */ - Property motor { this, "motor" }; + Property motor { this, "motor" }; /** * @brief The strategy to use to control the motorized valve. @@ -112,7 +112,7 @@ bool convertToJson(const ValveControlStrategyType& src, JsonVariant dst) { } } void convertFromJson(JsonVariantConst src, ValveControlStrategyType& dst) { - String strategy = src.as(); + std::string strategy = src.as(); if (strategy == "NO") { dst = ValveControlStrategyType::NormallyOpen; } else if (strategy == "NC") { diff --git a/sdkconfig.debug.defaults b/sdkconfig.debug.defaults index 3509855e..e9aa7a47 100644 --- a/sdkconfig.debug.defaults +++ b/sdkconfig.debug.defaults @@ -4,3 +4,6 @@ CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=n CONFIG_ESP_COREDUMP_ENABLE_TO_UART=y CONFIG_ESP_COREDUMP_DECODE=y CONFIG_ESP_COREDUMP_LOGS=y + +CONFIG_COMPILER_OPTIMIZATION_SIZE=n +CONFIG_COMPILER_OPTIMIZATION_DEBUG=y diff --git a/sdkconfig.defaults b/sdkconfig.defaults index e259a7b4..46979316 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -13,6 +13,12 @@ CONFIG_SECURE_FLASH_ENC_ENABLED=n # We want to use exceptions CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_COMPILER_OPTIMIZATION_SIZE=y + +# Reduce binary size +CONFIG_LWIP_IPV6=n +CONFIG_VFS_SUPPORT_TERMIOS=n +CONFIG_VFS_SUPPORT_SELECT=n # Less chatty CONFIG_BOOTLOADER_LOG_LEVEL_ERROR=y @@ -25,12 +31,6 @@ CONFIG_LOG_COLORS=n # Seems like a good idea to have some stack checks CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y -# This is ERROR by default -CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL_WARN=y - -# Why compile more than necessary? -CONFIG_ARDUINO_SELECTIVE_COMPILATION=y - # Give peripheral-related tasks to have some length to their names CONFIG_FREERTOS_MAX_TASK_NAME_LEN=32 diff --git a/test/main.cpp b/test/main.cpp index d19e96c2..95eb634d 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -1,30 +1,5 @@ #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); @@ -32,4 +7,3 @@ int main(int argc, char **argv) // ::testing::InitGoogleMock(&argc, argv); return RUN_ALL_TESTS(); } -#endif