From 294744d9004f40704e829ed893a6e70de2aff603 Mon Sep 17 00:00:00 2001 From: Gene Date: Sat, 30 Dec 2023 18:32:48 +0100 Subject: [PATCH] =?UTF-8?q?HomeGenie=20Mini=201.2=20=F0=9F=8E=89=20?= =?UTF-8?q?=F0=9F=A5=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + .idea/clion.iml | 2 - .idea/homegenie-mini.iml | 2 +- .idea/misc.xml | 28 +- .idea/modules.xml | 9 - .idea/platformio.iml | 2 - .idea/vcs.xml | 2 +- .travis.yml | 90 +--- CMakeLists.txt | 57 ++- CMakeListsPrivate.txt | 127 ++--- README.md | 373 +++++++++++--- data/images/phone/hg_panel_dashboard.png | Bin 0 -> 55804 bytes data/images/phone/hg_panel_discovery.png | Bin 0 -> 35917 bytes .../phone/hg_panel_discovery_select.png | Bin 0 -> 61860 bytes data/images/phone/hg_server_dashboard.png | Bin 0 -> 61716 bytes data/images/phone/hg_server_mqtt_config.png | Bin 0 -> 28612 bytes examples/playground/README.md | 0 examples/playground/configuration.h | 0 examples/playground/playground.cpp | 85 ++++ examples/rf-transceiver/README.md | 0 .../rf-transceiver/api/RCSwitchHandler.cpp | 110 +++++ examples/rf-transceiver/api/RCSwitchHandler.h | 61 +++ examples/rf-transceiver/configuration.h | 10 + .../rf-transceiver/io/RFReceiverConfig.cpp | 48 ++ examples/rf-transceiver/io/RFReceiverConfig.h | 55 +++ examples/rf-transceiver/io/RFTransmitter.cpp | 106 ++++ examples/rf-transceiver/io/RFTransmitter.h | 56 +++ .../rf-transceiver/io/RFTransmitterConfig.cpp | 22 +- .../rf-transceiver/io/RFTransmitterConfig.h | 52 ++ examples/rf-transceiver/rf-transceiver.cpp | 64 +++ examples/smart-sensor/README.md | 0 examples/smart-sensor/configuration.h | 16 + .../smart-sensor/io}/DS18B20.cpp | 14 +- .../smart-sensor/io}/DS18B20.h | 27 +- .../smart-sensor/io}/LightSensor.cpp | 13 +- .../smart-sensor/io}/LightSensor.h | 24 +- examples/smart-sensor/smart-sensor.cpp | 67 +++ examples/x10-transceiver/README.md | 0 examples/x10-transceiver/api/X10Handler.cpp | 273 +++++++++++ .../x10-transceiver}/api/X10Handler.h | 35 +- examples/x10-transceiver/configuration.h | 10 + .../x10-transceiver/io}/RFReceiver.cpp | 19 +- .../x10-transceiver/io}/RFReceiver.h | 14 +- .../x10-transceiver/io}/RFReceiverConfig.cpp | 4 +- .../x10-transceiver/io}/RFReceiverConfig.h | 6 +- .../x10-transceiver/io}/RFTransmitter.cpp | 2 +- .../x10-transceiver/io}/RFTransmitter.h | 7 +- .../io}/RFTransmitterConfig.cpp | 4 +- .../x10-transceiver/io}/RFTransmitterConfig.h | 6 +- .../x10-transceiver/io}/X10Message.cpp | 2 +- .../x10-transceiver/io}/X10Message.h | 8 +- examples/x10-transceiver/x10-transceiver.cpp | 73 +++ lib/NTPClient-master/NTPClient.cpp | 43 +- lib/NTPClient-master/NTPClient.h | 44 +- lib/README | 46 ++ library.json | 21 + library.properties | 11 + platformio.ini | 108 +++- src/Config.h | 9 +- src/HomeGenie.cpp | 316 ++++++++++++ src/HomeGenie.h | 173 +++++++ src/Task.cpp | 10 +- src/Task.h | 16 +- src/TaskManager.cpp | 18 +- src/TaskManager.h | 7 +- src/Utility.cpp | 4 +- src/Utility.h | 5 +- src/defs.h | 62 +++ src/io/IOEvent.h | 25 +- src/io/IOEventDomains.h | 3 +- src/io/IOEventPaths.h | 5 +- src/io/IOManager.cpp | 37 +- src/io/IOManager.h | 52 +- src/io/IOModule.cpp | 9 - src/io/IOModule.h | 29 -- src/io/Logger.cpp | 4 +- src/io/Logger.h | 3 +- src/io/gpio/GPIOPort.cpp | 74 +++ src/io/gpio/GPIOPort.h | 103 ++++ src/io/gpio/P1Port.cpp | 52 -- src/io/sys/Diagnostics.cpp | 16 +- src/io/sys/Diagnostics.h | 17 +- src/main.cpp | 96 +--- src/net/BLEManager.cpp | 130 +++++ src/{io/gpio/P1Port.h => net/BLEManager.h} | 43 +- src/net/HTTPServer.cpp | 95 ++-- src/net/HTTPServer.h | 23 +- src/net/MQTTServer.cpp | 2 +- src/net/MQTTServer.h | 14 +- src/net/NetManager.cpp | 140 +++++- src/net/NetManager.h | 35 +- src/net/SSDPDevice.cpp | 463 ++++++++++++++++++ src/net/SSDPDevice.h | 150 ++++++ src/net/WiFiManager.cpp | 79 ++- src/net/WiFiManager.h | 18 +- src/net/mqtt/MQTTBrokerMini.cpp | 7 +- src/net/mqtt/MQTTBrokerMini.h | 7 +- src/scripting/ProgramEngine.cpp | 2 +- src/scripting/ProgramEngine.h | 4 +- src/service/EventRouter.cpp | 19 +- src/service/EventRouter.h | 10 +- src/service/HomeGenie.cpp | 220 --------- src/service/HomeGenie.h | 86 ---- src/service/Module.cpp | 9 + src/service/Module.h | 82 ++++ src/service/api/APIHandler.h | 35 +- src/service/api/APIRequest.cpp | 2 +- src/service/api/APIRequest.h | 6 +- src/service/api/HomeGenieHandler.cpp | 236 +++++---- src/service/api/HomeGenieHandler.h | 39 +- src/service/api/X10Handler.cpp | 274 ----------- test/README | 6 +- 112 files changed, 4059 insertions(+), 1583 deletions(-) delete mode 100644 .idea/clion.iml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/platformio.iml create mode 100644 data/images/phone/hg_panel_dashboard.png create mode 100644 data/images/phone/hg_panel_discovery.png create mode 100644 data/images/phone/hg_panel_discovery_select.png create mode 100644 data/images/phone/hg_server_dashboard.png create mode 100644 data/images/phone/hg_server_mqtt_config.png create mode 100644 examples/playground/README.md create mode 100644 examples/playground/configuration.h create mode 100644 examples/playground/playground.cpp create mode 100644 examples/rf-transceiver/README.md create mode 100644 examples/rf-transceiver/api/RCSwitchHandler.cpp create mode 100644 examples/rf-transceiver/api/RCSwitchHandler.h create mode 100644 examples/rf-transceiver/configuration.h create mode 100644 examples/rf-transceiver/io/RFReceiverConfig.cpp create mode 100644 examples/rf-transceiver/io/RFReceiverConfig.h create mode 100644 examples/rf-transceiver/io/RFTransmitter.cpp create mode 100644 examples/rf-transceiver/io/RFTransmitter.h rename src/service/defs.h => examples/rf-transceiver/io/RFTransmitterConfig.cpp (65%) create mode 100644 examples/rf-transceiver/io/RFTransmitterConfig.h create mode 100644 examples/rf-transceiver/rf-transceiver.cpp create mode 100644 examples/smart-sensor/README.md create mode 100644 examples/smart-sensor/configuration.h rename {src/io/env => examples/smart-sensor/io}/DS18B20.cpp (86%) rename {src/io/env => examples/smart-sensor/io}/DS18B20.h (75%) rename {src/io/env => examples/smart-sensor/io}/LightSensor.cpp (76%) rename {src/io/env => examples/smart-sensor/io}/LightSensor.h (74%) create mode 100644 examples/smart-sensor/smart-sensor.cpp create mode 100644 examples/x10-transceiver/README.md create mode 100644 examples/x10-transceiver/api/X10Handler.cpp rename {src/service => examples/x10-transceiver}/api/X10Handler.h (53%) create mode 100644 examples/x10-transceiver/configuration.h rename {src/io/rf/x10 => examples/x10-transceiver/io}/RFReceiver.cpp (81%) rename {src/io/rf/x10 => examples/x10-transceiver/io}/RFReceiver.h (87%) rename {src/io/rf/x10 => examples/x10-transceiver/io}/RFReceiverConfig.cpp (95%) rename {src/io/rf/x10 => examples/x10-transceiver/io}/RFReceiverConfig.h (95%) rename {src/io/rf/x10 => examples/x10-transceiver/io}/RFTransmitter.cpp (98%) rename {src/io/rf/x10 => examples/x10-transceiver/io}/RFTransmitter.h (91%) rename {src/io/rf/x10 => examples/x10-transceiver/io}/RFTransmitterConfig.cpp (95%) rename {src/io/rf/x10 => examples/x10-transceiver/io}/RFTransmitterConfig.h (94%) rename {src/io/rf/x10 => examples/x10-transceiver/io}/X10Message.cpp (99%) rename {src/io/rf/x10 => examples/x10-transceiver/io}/X10Message.h (98%) create mode 100644 examples/x10-transceiver/x10-transceiver.cpp create mode 100644 lib/README create mode 100644 library.json create mode 100644 library.properties create mode 100644 src/HomeGenie.cpp create mode 100644 src/HomeGenie.h create mode 100644 src/defs.h delete mode 100644 src/io/IOModule.cpp delete mode 100644 src/io/IOModule.h create mode 100644 src/io/gpio/GPIOPort.cpp create mode 100644 src/io/gpio/GPIOPort.h delete mode 100644 src/io/gpio/P1Port.cpp create mode 100644 src/net/BLEManager.cpp rename src/{io/gpio/P1Port.h => net/BLEManager.h} (53%) create mode 100644 src/net/SSDPDevice.cpp create mode 100644 src/net/SSDPDevice.h delete mode 100644 src/service/HomeGenie.cpp delete mode 100644 src/service/HomeGenie.h create mode 100644 src/service/Module.cpp create mode 100644 src/service/Module.h delete mode 100644 src/service/api/X10Handler.cpp diff --git a/.gitignore b/.gitignore index 5afc065..7c80c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ .piolibdeps .clang_complete .gcc-flags.json +cmake-build-debug +CMakeFiles +.idea # Clion diff --git a/.idea/clion.iml b/.idea/clion.iml deleted file mode 100644 index f08604b..0000000 --- a/.idea/clion.iml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/.idea/homegenie-mini.iml b/.idea/homegenie-mini.iml index f08604b..2810554 100644 --- a/.idea/homegenie-mini.iml +++ b/.idea/homegenie-mini.iml @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 9b483ee..d858eb1 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,19 +1,17 @@ - - - - - - - - - - - - - - - + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 9ce81f0..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/platformio.iml b/.idea/platformio.iml deleted file mode 100644 index f08604b..0000000 --- a/.idea/platformio.iml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0dc767e..fa09d0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,84 +1,10 @@ -# Continuous Integration (CI) is the practice, in software -# engineering, of merging all developer working copies with a shared mainline -# several times a day < https://docs.platformio.org/page/ci/index.html > -# -# Documentation: -# -# * Travis CI Embedded Builds with PlatformIO -# < https://docs.travis-ci.com/user/integration/platformio/ > -# -# * PlatformIO integration with Travis CI -# < https://docs.platformio.org/page/ci/travis.html > -# -# * User Guide for `platformio ci` command -# < https://docs.platformio.org/page/userguide/cmd_ci.html > -# -# -# Please choose one of the following templates (proposed below) and uncomment -# it (remove "# " before each line) or use own configuration according to the -# Travis CI documentation (see above). -# - - -# -# Template #1: General project. Test it using existing `platformio.ini`. -# - -# language: python -# python: -# - "2.7" -# -# sudo: false -# cache: -# directories: -# - "~/.platformio" -# -# install: -# - pip install -U platformio -# - platformio update -# -# script: -# - platformio run - - -# -# Template #2: The project is intended to be used as a library with examples. -# - -# language: python -# python: -# - "2.7" -# -# sudo: false -# cache: -# directories: -# - "~/.platformio" -# -# env: -# - PLATFORMIO_CI_SRC=path/to/test/file.c -# - PLATFORMIO_CI_SRC=examples/file.ino -# - PLATFORMIO_CI_SRC=path/to/test/directory -# -# install: -# - pip install -U platformio -# - platformio update -# -# script: -# - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N - -language: python -python: - - "2.7" - +language: c sudo: false -cache: - directories: - - "~/.platformio" - -install: - - pip install -U platformio - - platformio update - - platformio lib install - +before_install: + - source <(curl -SLs https://raw.githubusercontent.com/adafruit/travis-ci-arduino/master/install.sh) script: - - platformio run + - build_platform esp8266 +notifications: + email: + on_success: change + on_failure: change diff --git a/CMakeLists.txt b/CMakeLists.txt index 33c4a95..cf19e4b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,41 +1,70 @@ +# !!! WARNING !!! AUTO-GENERATED FILE, PLEASE DO NOT MODIFY IT AND USE +# https://docs.platformio.org/page/projectconf/section_env_build.html#build-flags +# +# If you need to override existing CMake configuration or add extra, +# please create `CMakeListsUser.txt` in the root of project. +# The `CMakeListsUser.txt` will not be overwritten by PlatformIO. + cmake_minimum_required(VERSION 3.2) -project(homegenie-mini) +project("homegenie-mini") include(CMakeListsPrivate.txt) +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/CMakeListsUser.txt) +include(CMakeListsUser.txt) +endif() + add_custom_target( PLATFORMIO_BUILD ALL - COMMAND ${PLATFORMIO_CMD} -f -c clion run + COMMAND ${PLATFORMIO_CMD} -f -c clion run "$<$>:-e${CMAKE_BUILD_TYPE}>" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_BUILD_VERBOSE ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion run --verbose "$<$>:-e${CMAKE_BUILD_TYPE}>" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) add_custom_target( PLATFORMIO_UPLOAD ALL - COMMAND ${PLATFORMIO_CMD} -f -c clion run --target upload + COMMAND ${PLATFORMIO_CMD} -f -c clion run --target upload "$<$>:-e${CMAKE_BUILD_TYPE}>" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) add_custom_target( PLATFORMIO_CLEAN ALL - COMMAND ${PLATFORMIO_CMD} -f -c clion run --target clean + COMMAND ${PLATFORMIO_CMD} -f -c clion run --target clean "$<$>:-e${CMAKE_BUILD_TYPE}>" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_MONITOR ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion device monitor "$<$>:-e${CMAKE_BUILD_TYPE}>" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) add_custom_target( PLATFORMIO_TEST ALL - COMMAND ${PLATFORMIO_CMD} -f -c clion test + COMMAND ${PLATFORMIO_CMD} -f -c clion test "$<$>:-e${CMAKE_BUILD_TYPE}>" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) add_custom_target( PLATFORMIO_PROGRAM ALL - COMMAND ${PLATFORMIO_CMD} -f -c clion run --target program + COMMAND ${PLATFORMIO_CMD} -f -c clion run --target program "$<$>:-e${CMAKE_BUILD_TYPE}>" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) add_custom_target( PLATFORMIO_UPLOADFS ALL - COMMAND ${PLATFORMIO_CMD} -f -c clion run --target uploadfs + COMMAND ${PLATFORMIO_CMD} -f -c clion run --target uploadfs "$<$>:-e${CMAKE_BUILD_TYPE}>" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target( + PLATFORMIO_BUILD_DEBUG ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion run --target debug "$<$>:-e${CMAKE_BUILD_TYPE}>" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) @@ -51,12 +80,10 @@ add_custom_target( WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) -add_executable(${PROJECT_NAME} ${SRC_LIST} src/io/IOManager.cpp src/io/IOManager.h src/net/HTTPServer.cpp src/net/HTTPServer.h src/net/NetManager.cpp src/net/NetManager.h src/io/Logger.cpp src/io/Logger.h src/scripting/ProgramEngine.cpp src/scripting/ProgramEngine.h src/io/env/LightSensor.cpp src/io/env/LightSensor.h src/io/env/DS18B20.cpp src/io/env/DS18B20.h src/service/api/APIRequest.cpp src/service/api/APIRequest.h src/TaskManager.cpp src/TaskManager.h src/Task.cpp src/Task.h src/net/MQTTServer.cpp src/net/MQTTServer.h src/net/mqtt/MQTTBrokerConfig.cpp src/net/mqtt/MQTTBrokerConfig.h src/io/rf/x10/X10Message.cpp src/io/rf/x10/X10Message.h src/io/sys/Diagnostics.cpp src/io/sys/Diagnostics.h src/io/IOEvent.h src/io/IOEventDomains.h src/io/IOEventPaths.h src/service/EventRouter.cpp src/service/EventRouter.h src/service/api/APIHandler.h src/service/api/X10Handler.cpp src/service/api/X10Handler.h src/Utility.cpp src/Utility.h src/service/api/HomeGenieHandler.cpp src/service/api/HomeGenieHandler.h src/service/defs.h src/io/gpio/P1Port.cpp src/io/gpio/P1Port.h src/io/IOModule.cpp src/io/IOModule.h src/net/WiFiManager.cpp src/net/WiFiManager.h src/Config.h) +add_custom_target( + PLATFORMIO_DEVICE_LIST ALL + COMMAND ${PLATFORMIO_CMD} -f -c clion device list + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) -include_directories(./.piolibdeps/ArduinoJson_ID64) -include_directories(./.piolibdeps/ArduinoLog_ID1532) -include_directories(./.piolibdeps/ESP8266UPnP_ID2048) -include_directories(./.piolibdeps/LinkedList_ID443) -include_directories(./.piolibdeps/WebSockets_ID549/src) -include_directories(./.piolibdeps/OneWire_ID1) -include_directories(lib/NTPClient-master) +#add_executable(Z_DUMMY_TARGET ${SRC_LIST}) diff --git a/CMakeListsPrivate.txt b/CMakeListsPrivate.txt index 5cf622a..e069f21 100644 --- a/CMakeListsPrivate.txt +++ b/CMakeListsPrivate.txt @@ -1,63 +1,64 @@ -set(ENV{PATH} "/home/gene/.nvm/versions/node/v8.1.0/bin:/home/gene/Scrivania/Work/mcu/esp/esp-open-sdk/xtensa-lx106-elf/bin:/home/gene/bin:/home/gene/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/gene/.platformio/penv/bin") -set(PLATFORMIO_CMD "/home/gene/.local/bin/platformio") - -SET(CMAKE_C_COMPILER "/home/gene/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-gcc") -SET(CMAKE_CXX_COMPILER "/home/gene/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-g++") -SET(CMAKE_CXX_FLAGS_DISTRIBUTION "-fno-rtti -fno-exceptions -std=c++11 -Os -mlongcalls -mtext-section-literals -falign-functions=4 -U__STRICT_ANSI__ -ffunction-sections -fdata-sections -Wall") -SET(CMAKE_C_FLAGS_DISTRIBUTION "-std=gnu99 -Wpointer-arith -Wno-implicit-function-declaration -Wl,-EL -fno-inline-functions -nostdlib -Os -mlongcalls -mtext-section-literals -falign-functions=4 -U__STRICT_ANSI__ -ffunction-sections -fdata-sections -Wall") -set(CMAKE_CXX_STANDARD 11) - -add_definitions(-D'PLATFORMIO=30603') -add_definitions(-D'ESP8266') -add_definitions(-D'ARDUINO_ARCH_ESP8266') -add_definitions(-D'ESP8266_WEMOS_D1MINI') -add_definitions(-D'F_CPU=80000000L') -add_definitions(-D'__ets__') -add_definitions(-D'ICACHE_FLASH') -add_definitions(-D'ARDUINO=10805') -add_definitions(-D'ARDUINO_BOARD=\"PLATFORMIO_D1_MINI\"') -add_definitions(-D'LWIP_OPEN_SRC') -add_definitions(-D'TCP_MSS=536') -add_definitions(-D'VTABLES_IN_FLASH') - -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/tools/sdk/include") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/tools/sdk/libc/xtensa-lx106-elf/include") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/cores/esp8266") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/tools/sdk/lwip2/include") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/variants/d1_mini") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ArduinoOTA") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/DNSServer/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/EEPROM") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266AVRISP/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266HTTPClient/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266HTTPUpdateServer/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266LLMNR") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266NetBIOS") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266SSDP") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266WebServer/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266WiFi/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266WiFiMesh/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266httpUpdate/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266mDNS") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Ethernet/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/GDBStub/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Hash/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SD/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SPI") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SPISlave/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Servo/src") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SoftwareSerial") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/TFT_Touch_Shield_V2") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Ticker") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Wire") -include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/esp8266/src") -include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/xtensa-lx106-elf/include") -include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/xtensa-lx106-elf/include/c++/4.8.2") -include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/xtensa-lx106-elf/include/c++/4.8.2/xtensa-lx106-elf") -include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/lib/gcc/xtensa-lx106-elf/4.8.2/include") -include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/lib/gcc/xtensa-lx106-elf/4.8.2/include-fixed") -include_directories("$ENV{HOME}/.platformio/packages/tool-unity") -include_directories("$ENV{HOME}/Documents/PlatformIO/Projects/homegenie-mini/include") -include_directories("$ENV{HOME}/Documents/PlatformIO/Projects/homegenie-mini/src") - -FILE(GLOB_RECURSE SRC_LIST "./src/*.*" "./lib/*.*" "./.piolibdeps/*.*") +set(ENV{PATH} "$ENV{HOME}/.nvm/versions/node/v8.1.0/bin:$ENV{HOME}/.platformio/penv/bin") +set(PLATFORMIO_CMD "$ENV{HOME}/.platformio/penv/bin/platformio") + +SET(CMAKE_C_COMPILER "$ENV{HOME}/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-gcc") +SET(CMAKE_CXX_COMPILER "$ENV{HOME}/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-g++") +SET(CMAKE_CXX_FLAGS_DISTRIBUTION "-fno-rtti -fno-exceptions -std=c++11 -Os -mlongcalls -mtext-section-literals -falign-functions=4 -U__STRICT_ANSI__ -ffunction-sections -fdata-sections -Wall") +SET(CMAKE_C_FLAGS_DISTRIBUTION "-std=gnu99 -Wpointer-arith -Wno-implicit-function-declaration -Wl,-EL -fno-inline-functions -nostdlib -Os -mlongcalls -mtext-section-literals -falign-functions=4 -U__STRICT_ANSI__ -ffunction-sections -fdata-sections -Wall") +set(CMAKE_CXX_STANDARD 11) + +add_definitions(-D'PLATFORMIO=30603') +add_definitions(-D'ESP8266') +add_definitions(-D'ARDUINO_ARCH_ESP8266') +add_definitions(-D'ESP8266_WEMOS_D1MINI') +add_definitions(-D'F_CPU=80000000L') +add_definitions(-D'__ets__') +add_definitions(-D'ICACHE_FLASH') +add_definitions(-D'ARDUINO=10805') +add_definitions(-D'ARDUINO_BOARD=\"PLATFORMIO_D1_MINI\"') +add_definitions(-D'LWIP_OPEN_SRC') +add_definitions(-D'TCP_MSS=536') +add_definitions(-D'VTABLES_IN_FLASH') + +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/tools/sdk/include") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/tools/sdk/libc/xtensa-lx106-elf/include") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/cores/esp8266") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/tools/sdk/lwip2/include") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/variants/d1_mini") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ArduinoOTA") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/DNSServer/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/EEPROM") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266AVRISP/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266HTTPClient/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266HTTPUpdateServer/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266LLMNR") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266NetBIOS") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266SSDP") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266WebServer/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266WiFi/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266WiFiMesh/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266httpUpdate/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266mDNS") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Ethernet/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/GDBStub/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Hash/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SD/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SPI") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SPISlave/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Servo/src") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SoftwareSerial") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/TFT_Touch_Shield_V2") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Ticker") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Wire") +include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/esp8266/src") +include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/xtensa-lx106-elf/include") +include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/xtensa-lx106-elf/include/c++/4.8.2") +include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/xtensa-lx106-elf/include/c++/4.8.2/xtensa-lx106-elf") +include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/lib/gcc/xtensa-lx106-elf/4.8.2/include") +include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/lib/gcc/xtensa-lx106-elf/4.8.2/include-fixed") +include_directories("$ENV{HOME}/.platformio/packages/tool-unity") + +include_directories("./include") +include_directories("./src") + +FILE(GLOB_RECURSE SRC_LIST "./src/*.*" "./lib/*.*" "./.piolibdeps/*.*") diff --git a/README.md b/README.md index 97863b6..898f8d1 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,251 @@ [![Travis Build Status](https://travis-ci.org/genielabs/homegenie-mini.svg?branch=master)](https://travis-ci.org/genielabs/homegenie-mini) -# HomeGenie Mini +# HomeGenie Mini v1.2 `ESP32 / ESP8266` -HomeGenie mini *(code name **Sbirulino**)* is an open hardware + firmware solution for building smart devices -based on the popular *ESP8266 chip*, a WiFi capable micro controller. +HomeGenie mini *(code name **Sbirulino**)* is an **open source library** for building custom firmwares for smart devices +based on *ESP32* or *ESP8266* chip. -[![HomeGenie Mini Quick Setup Video](https://raw.githubusercontent.com/genielabs/homegenie-mini/master/pcb/images/video_cover.jpg)](https://youtu.be/CovB1jl3980) -(click the picture above to watch the video) +## Features -**Hardware features** +- Easy Wi-Fi configuration using Bluetooth (ESP32) or WPA (ESP8266) +- Does not require an Internet connection to be configured or to work properly +- Time synchronization using internal RTC (ESP32), mobile app time or NTP +- Device discovery through SNMP/UPnP advertising with customizable name +- Multi-channel I/O: HTTP, WebSocket, SSE, MQTT +- Status LED +- Configuration/Pairing Button +- Builtin GPIO control API +- Switch level restore on power-on / restart +- Event router +- Simple task manager +- Extensible API +- Can connect directly to *HomeGenie Panel* either via Wi-Fi access point or hotspot/tethering +- Can be easily connected to *HomeGenie Server* via MQTT -- WPS button for quick WiFi protected setup -- Temperature and light sensors -- RF transceiver (315/330/433Mhz) -- Expansion connector (P1) with 4 GPIO configurable as SPI/DIO/PWM +## Building and flashing the firmware -*Example applications of the P1 connector:* +The firmware can be easily installed using [Platform.IO core](https://docs.platformio.org/en/latest/installation.html) CLI. +After installing *Platform.IO core*, download [HomeGenie Mini](https://github.com/genielabs/homegenie-mini/archive/refs/heads/master.zip) source code, +unzip it and open a terminal with the current directory set to `homegenie-mini` folder. +Then enter the following commands to install libraries required to build the firmware: -- control up to 4 relays to actuate lights and appliances (DIO) -- hosting additional sensors, connecting a display or other hardware (SPI) -- control the brightness of a led or drive a motor at different speed (PWM) -- breadboard playground +```bash +pio update +pio lib install +``` -**Firmware features** +To actually build and install *HomeGenie Mini* firmware, connect your ESP device to your +PC's USB port and issue the command: -- Automatic discovery (SSDP) for instant client setup -- GPIO mapping to virtual modules: switch for digital output, dimmer for analog output or sensor for inputs (work in progress) -- X10 home automation RF protocol encoding and decoding with mapping to virtual modules -- Modules state persistence -- HTTP API (subset of standard HomeGenie API) -- Real time event stream over WebSocket or SSE connection -- MQTT broker over websocket -- NTP client for time sync -- Scripting engine (work in progress) -- Serial CLI with same API as HTTP +```bash +pio run -e default -t upload +``` -## Assembling a HomeGenie Mini device +**Congratulations!! =)** You've just got a new shiny HomeGenie Mini device up and running. -![HomeGenie Mini PCB front and rear](https://raw.githubusercontent.com/genielabs/homegenie-mini/master/pcb/images/hg_mini_board_front_rear.png) -*HomeGenie Mini board front and rear view* +## Configuration environments -### STEP 1: Start by soldering the 3 resistors, 2 LEDS, light sensor, temperature sensor and the momentary switch button +The option `-e default` shown in the above command is used to specify the configuration environment. +The **default** environment is for building the base firmware for a generic *ESP32* board. -![HomeGenie Mini assembling step 1](https://raw.githubusercontent.com/genielabs/homegenie-mini/master/pcb/images/hg_mini_assembling_step_1.png) +The following configurations are available: -### STEP 2: Solder the header pins to the D1 Mini +- `default` +Generic *ESP32* board. +- `d1-mini` +D1-Mini board with *ESP8266*. +- `d1-mini-esp32` +D1-Mini board with *ESP32*. +- `sonoff` +Configuration for *Sonoff 2-Gang Wi-Fi Smart Switch (DUALR3)*. -![HomeGenie Mini assembling step 2](https://raw.githubusercontent.com/genielabs/homegenie-mini/master/pcb/images/hg_mini_assembling_step_2.png) +For example, to install *HomeGenie Mini* on a **Sonoff DUALR3 Smart Switch** device, the following command is used: -### STEP 3: Solder the D1 Mini to HomeGenie Mini board +```bash +pio run -e sonoff -t upload +``` -![HomeGenie Mini assembling step 3](https://raw.githubusercontent.com/genielabs/homegenie-mini/master/pcb/images/hg_mini_assembling_step_3.png) +It's possible to add custom configuration environment to build your own version of the firmware to support +different hardware and functionality as explained later in this file. -### STEP 4: Optionally solder the RF receiver and transmitter: the firmware currently only support X10 home automation protocol. More protocols might be added in the future (any request?). -The picture below shows a basic HomeGenie Mini device **without** the RF transceiver but adding the RF transceiver is easy as solder [two more components](https://www.google.it/search?q=rf+MX-FS-03V+MX-05V+315+433+transmitter-receiver+module). -You can also take advantage of the expansion port (P1) to connect a [relay module](https://www.google.it/search?q=buy+4+or+2+channels+relay+module+arduino) to control lights and appliances or -any other additional sensors/components required for your projects. +## Connecting the device -![HomeGenie Mini assembling step 4](https://raw.githubusercontent.com/genielabs/homegenie-mini/master/pcb/images/hg_mini_assembling_step_4.png) +Once the firmware is installed you can configure and control the device using +the [HomeGenie Panel](https://play.google.com/store/apps/details?id=com.glabs.homegenieplus) app available from Google PlayStore. -## Building and flashing the firmware +The device status LED will blink continuously indicating that the device is not +connected to Wi-Fi, and is in pairing mode accepting connections via Bluetooth. + +Enable Bluetooth on your phone, open *HomeGenie Panel* and select the *"Discovery"* option. + +![HomeGenie Panel - Discovery](data/images/phone/hg_panel_discovery.png) + +The new HG-Mini device will be detected via Bluetooth and the app will display a dialog to +configure the device name and data to connect it to Wi-Fi. +After confirming the settings, the HG-Mini will exit pairing mode, reboot and connect +to Wi-Fi. +At this point the device will blink slowly (every 2 seconds) indicating that is connected +correctly, and it will appear in the list of detected devices in the *HomeGenie Panel* app. + +Select it from the list and click the *"Done"* button. + +![HomeGenie Panel - Discovery: select device](data/images/phone/hg_panel_discovery_select.png) + +Depending on the installed firmware version you will be able to select different kind of modules +to show in the panel dashboard. The following picture refers to the `smart-sensor-d1-mini-esp32` +firmware that implements temperature and light sensor and 4 GPIO switches. + +![HomeGenie Panel - Dashboard](data/images/phone/hg_panel_dashboard.png) -HomeGenie Mini firmware is based on **Platform.IO**. You can choose to build firmware -without installing the code editor but just the [Platform.IO core](https://docs.platformio.org/en/latest/installation.html). -After installing *Platform.IO core* you can build the firmware by entering this command: +### Connecting to HomeGenie Automation Server + +HG-Mini devices can also be connected to [HomeGenie Automation Server](https://github.com/genielabs/HomeGenie) +configuring the *MQTT client* as shown in the following picture. + +![HomeGenie Server - MQTT configuration](data/images/phone/hg_server_mqtt_config.png) + +Is then possible to use HG-mini device for automation tasks, logging, statistics and use of all other +features of *HomeGenie*. + +![HomeGenie Server - Dashboard](data/images/phone/hg_server_dashboard.png) + + + + +## Monitoring via serial port + +When the device is connected to your PC's USB port, you can monitor its activity logs +by entering the following command:: + +```bash +pio device monitor -b 115200 ``` -platformio update -platformio lib install -platformio run + +**Example output** +``` +[1970-01-01T00:00:00.055Z] HomeGenie Mini 1.2.0 +[1970-01-01T00:00:00.056Z] Booting... +[1970-01-01T00:00:00.057Z] + Starting HomeGenie service +[1970-01-01T00:00:00.068Z] + Starting NetManager +[1970-01-01T00:00:00.068Z] | - Connecting to WI-FI . +[1970-01-01T00:00:00.187Z] | - WI-FI SSID: HG-NET +[1970-01-01T00:00:00.188Z] | - WI-FI Password: * +[1970-01-01T00:00:00.214Z] | x WiFi disconnected +[1970-01-01T00:00:00.774Z] | ✔ HTTP service +[1970-01-01T00:00:00.784Z] | ✔ WebSocket server +[1970-01-01T00:00:00.786Z] | ✔ MQTT service +[1970-01-01T00:00:00.791Z] @IO::GPIO::GPIOPort [Status.Level 0] +[1970-01-01T00:00:00.792Z] :Service::HomeGenie [IOManager::IOEvent] >> [domain 'HomeAutomation.HomeGenie' address '14' event 'Status.Level'] +[1970-01-01T00:00:00.807Z] @IO::GPIO::GPIOPort [Status.Level 0] +[1970-01-01T00:00:00.809Z] :Service::HomeGenie [IOManager::IOEvent] >> [domain 'HomeAutomation.HomeGenie' address '27' event 'Status.Level'] +[1970-01-01T00:00:00.822Z] READY. +[1970-01-01T00:00:00.823Z] @IO::Sys::Diagnostics [System.BytesFree 147128] +[1970-01-01T00:00:00.835Z] :Service::HomeGenie [IOManager::IOEvent] >> [domain 'HomeAutomation.HomeGenie' address 'mini' event 'System.BytesFree'] +[1970-01-01T00:00:00.848Z] :Service::EventRouter dequeued event >> [domain 'HomeAutomation.HomeGenie' address 'mini' event 'System.BytesFree'] +[2023-12-29T17:01:11.050Z] | - RTC updated via TimeClient (NTP) +[2023-12-29T17:01:11.053Z] | ✔ UPnP friendly name: Bagno +[2023-12-29T17:01:11.054Z] | ✔ SSDP service +[2023-12-29T17:01:11.200Z] | - Connected to 'HG-NET' +[2023-12-29T17:01:11.201Z] | - IP: 192.168.x.y +[2023-12-29T17:01:15.048Z] @IO::Sys::Diagnostics [System.BytesFree 143268] +[2023-12-29T17:01:15.049Z] :Service::HomeGenie [IOManager::IOEvent] >> [domain 'HomeAutomation.HomeGenie' address 'mini' event 'System.BytesFree'] +[2023-12-29T17:01:15.063Z] :Service::EventRouter dequeued event >> [domain 'HomeAutomation.HomeGenie' address 'mini' event 'System.BytesFree'] ``` -If you prefer installing the whole IDE follow instructions for [Platform.IO IDE](https://platformio.org/platformio-ide) installation instead. -To install the firmware connect HomeGenie Mini to the USB port of your PC and issue the command: +## Firmwares examples with source code + +In the examples folder you can find some smart device projects using *HomeGenie Mini* library. + +### Smart sensor example + +This example implements a smart sensor with temperature and luminance sensing. It can also control +4 GPIO switches. + +The data pins number can be modified from the `configuration.h` file. + +**Generic ESP32** +```bash +pio run -e smart-sensor -t upload +``` +**D1 Mini - ESP8266** +```bash +pio run -e smart-sensor-d1-mini -t upload ``` -platformio run -t upload +**D1 Mini - ESP32** +```bash +pio run -e smart-sensor-d1-mini-esp32 -t upload ``` -**Congratulations!! =)** You've just got a new shiny HomeGenie Mini device up and running. + +### X10 transceiver example + +Smart Wi-Fi connected X10 transceiver. + +The data pins number can be modified from the `configuration.h` file. + +**Generic ESP32** +```bash +pio run -e x10-transceiver -t upload +``` + +### RF transceiver example + +Smart Wi-Fi connected RF transceiver with RF commands capturing and playback. + +The data pins number can be modified from the `configuration.h` file. + +**Generic ESP32** +```bash +pio run -e rf-transceiver -t upload +``` + +### Playground project + +Just a generic playground project to mess with the library =) + +**Generic ESP32** +```bash +pio run -e playground -t upload +``` + + + + + + + + + -### HomeGenie API + + + + + + +## HomeGenie API HomeGenie Mini API is a subset of HomeGenie Server API that makes HomeGenie Mini a real -fully working light version of HomeGenie Server specifically designed for micro controllers. +fully working light version of HomeGenie Server specifically designed for microcontrollers. -#### [HomeAutomation.HomeGenie](https://genielabs.github.io/HomeGenie/api/mig/core_api_config.html) +### [HomeAutomation.HomeGenie](https://genielabs.github.io/HomeGenie/api/mig/core_api_config.html) Implemented subset: @@ -129,23 +284,14 @@ GET /api/HomeAutomation.HomeGenie/Config/Modules.Get/HomeAutomation.HomeGenie/mi } ``` -#### [HomeAutomation.X10](https://genielabs.github.io/HomeGenie/api/mig/mig_api_x10.html) - -Implemented subset: - -- [`/api/HomeAutomation.X10//Control.On`](https://genielabs.github.io/HomeGenie/api/mig/mig_api_x10.html#1) -- [`/api/HomeAutomation.X10//Control.Off`](https://genielabs.github.io/HomeGenie/api/mig/mig_api_x10.html#2) -- [`/api/HomeAutomation.X10//Control.Level`](https://genielabs.github.io/HomeGenie/api/mig/mig_api_x10.html#5) -- [`/api/HomeAutomation.X10//Control.Toggle`](https://genielabs.github.io/HomeGenie/api/mig/mig_api_x10.html#6) - - -#### HomeGenie Mini specific API +### HomeGenie Mini builtin API It's possible to control the 4 GPIOs on the `P1` expansion port using the following API: -- `/api/HomeAutomation.HomeGenie//Control.On` -- `/api/HomeAutomation.HomeGenie//Control.Off` -- `/api/HomeAutomation.HomeGenie//Control.Level/` +- `/api/HomeAutomation.HomeGenie//Control.On` +- `/api/HomeAutomation.HomeGenie//Control.Off` +- `/api/HomeAutomation.HomeGenie//Control.Level/` +- `/api/HomeAutomation.HomeGenie//Control.Toggle` Where `` can be `D5`, `D6`, `D7` or `D8` and `` a integer between `0` and `100`. @@ -166,6 +312,90 @@ Where `` can be `D5`, `D6`, `D7` or `D8` and `` a integer betwe /api/HomeAutomation.HomeGenie/D8/Control.Off ``` +### [HomeAutomation.X10](https://genielabs.github.io/HomeGenie/api/mig/mig_api_x10.html) API (x10-transceiver firmware) + +Implemented subset: + +- [`/api/HomeAutomation.X10//Control.On`](https://genielabs.github.io/HomeGenie/api/mig/mig_api_x10.html#1) +- [`/api/HomeAutomation.X10//Control.Off`](https://genielabs.github.io/HomeGenie/api/mig/mig_api_x10.html#2) +- [`/api/HomeAutomation.X10//Control.Level`](https://genielabs.github.io/HomeGenie/api/mig/mig_api_x10.html#5) +- [`/api/HomeAutomation.X10//Control.Toggle`](https://genielabs.github.io/HomeGenie/api/mig/mig_api_x10.html#6) + + + +--- + +## Disclaimer + + +THIS PROJECT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS PROJECT, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +--- + + + +# Previous release (2019) + +## HomeGenie Mini v1.1 (playground board) + +[![HomeGenie Mini Quick Setup Video](https://raw.githubusercontent.com/genielabs/homegenie-mini/master/pcb/images/video_cover.jpg)](https://youtu.be/CovB1jl3980) + +(click the picture above to watch the video) + +**Hardware features** + +- WPS button for quick Wi-Fi protected setup +- Temperature and light sensors +- RF transceiver (315/330/433Mhz) +- Expansion connector (P1) with 4 GPIO configurable as SPI/DIO/PWM + +*Example applications of the P1 connector:* + +- control up to 4 relays to actuate lights and appliances (DIO) +- hosting additional sensors, connecting a display or other hardware (SPI) +- control the brightness of a LED or drive a motor at different speed (PWM) +- breadboard playground + +**Firmware features** + +- Automatic discovery (SSDP) for instant client setup +- GPIO mapping to virtual modules: switch for digital output, dimmer for analog output or sensor for inputs (work in progress) +- X10 home automation RF protocol encoding and decoding with mapping to virtual modules +- Modules state persistence +- HTTP API (subset of standard HomeGenie API) +- Real time event stream over WebSocket or SSE connection +- MQTT broker over websocket +- NTP client for time sync +- Scripting engine (work in progress) +- Serial CLI with same API as HTTP + +## Assembling a HomeGenie Mini device + +![HomeGenie Mini PCB front and rear](https://raw.githubusercontent.com/genielabs/homegenie-mini/master/pcb/images/hg_mini_board_front_rear.png) + +*HomeGenie Mini board front and rear view* + +### STEP 1: Start by soldering the 3 resistors, 2 LEDs, light sensor, temperature sensor and the momentary switch button + +![HomeGenie Mini assembling step 1](https://raw.githubusercontent.com/genielabs/homegenie-mini/master/pcb/images/hg_mini_assembling_step_1.png) + +### STEP 2: Solder the header pins to the D1 Mini + +![HomeGenie Mini assembling step 2](https://raw.githubusercontent.com/genielabs/homegenie-mini/master/pcb/images/hg_mini_assembling_step_2.png) + +### STEP 3: Solder the D1 Mini to HomeGenie Mini board + +![HomeGenie Mini assembling step 3](https://raw.githubusercontent.com/genielabs/homegenie-mini/master/pcb/images/hg_mini_assembling_step_3.png) + +### STEP 4: Optionally solder the RF receiver and transmitter: the firmware currently only support X10 home automation protocol. More protocols might be added in the future (any request?). + +The picture below shows a basic HomeGenie Mini device **without** the RF transceiver but adding the RF transceiver is easy as solder [two more components](https://www.google.it/search?q=rf+MX-FS-03V+MX-05V+315+433+transmitter-receiver+module). +You can also take advantage of the expansion port (P1) to connect a [relay module](https://www.google.it/search?q=buy+4+or+2+channels+relay+module+arduino) to control lights and appliances or +any other additional sensors/components required for your projects. + +![HomeGenie Mini assembling step 4](https://raw.githubusercontent.com/genielabs/homegenie-mini/master/pcb/images/hg_mini_assembling_step_4.png) + ## Components listing @@ -184,7 +414,7 @@ The PCB size is 44mm*50mm. **Components listing** -- 1 ESP8266 WeMoo D1 mini WiFi module (or equivalent) +- 1 ESP8266 WeMoo D1 mini Wi-Fi module (or equivalent) - 1 DS18B20 (temperature sensor) - 1 LDR (light sensor) - 1 FS1000A (RF transmitter) @@ -203,8 +433,3 @@ The PCB size is 44mm*50mm. **Release** - https://github.com/genielabs/homegenie-mini/releases/ - - -## Disclaimer - -THIS PROJECT IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS PROJECT, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/data/images/phone/hg_panel_dashboard.png b/data/images/phone/hg_panel_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..a9dcea0be092f5b5eb1866fe7e652fc0fefa2ef7 GIT binary patch literal 55804 zcmV)qK$^daP)`R*_r+;JS~$iZM6!(p6rk|kM^Wh>{j(nhT|r%7G^devdNXL@#L^9c3R zZ?qFSSAF%p_v+Pq45KVdm#+wx78O=P<+Rl{)!jN5`TT- zbHL;A_&FFj7kh9XW{h8d~+?mdyZK3T?`+~O9jstlObPnxFXy=98 zk#pk$0PURmwd>m%*Mj)9>eoV7BlpvGme-}wJ<$E@C(`d-HFng_t6#G!4xnEG-Lt%I z`MmV&HLlb0(hj4whW42ryF%yCglSi<-HU#++K=*HnD$4%6zv2|zXR<~w3}i2OaH=n zVBFYC+^l&K+Vd7yp(xPXMjlIRd;Pe2dvk8Sr>Cb1i76gWOm8#UUfNx$`nFsZzo`mj zpHX9Cd$CDOoG2EJM51D=a4Z(<5qortV|DPD+TPRG)4hGin|=B-8#7^}am-6Expaol z>sccn?!_LDcd6IsofQZK^RlzEy?(!6o>hDgy#vXY?TRdq!1|lhw$wf7JH?|wc4P@e zy+bi(Hf2nWf$3dDbaw40@m`I^5R1lS&tv*QYmp)F!KRet~ywi%d6K$&M`*}Sc`KZnRE(UQY%yYbVDMLBhp}D}izC$j89W&S z%)W&=%$uFi+Tr5wxlYKOBcYRu0l^pm7&p1a92A)SY_>5)A3zLI7>joXW_jRo)-vyd z!x$v2_r?-BFEg&(KA2e%mauKoMI!z?QL=Gi%nB~^6HDkMi6fE0ryTZ!*hXPXTR3+> z#{tZCS`O<&q-oU!1AQQ&N`$pYiHy3al`m4v_nZ2q`3($7buP)90{Yh zrw3h~U1)7>!I?8nXliKaZf$Kn)Z5+tS}dkMEl6zRUmtt4E#m;Z?$WhIf@$v*H+D~c zLE+-5)25+%)@)2DD}&eTLyx$z_O@2EoINXEEokp(Lr+&XdU|^i4u|DPEk{z-($PpO zRajEn*_k9hifWzE+HbhoxW?#de$OqVy_Jc~lNgN`2d5nu%yu0H+iF@mO9G438W3l1 z^EYN4>8-yTV}xR31FObiDu|}0z--sT%xh1))&x+pcu$pp;Fe=pG>OFh?OGA!psZ;) z@j=KKxU_E+TYJLzU4J%)*5VPz(X=1q+=}IT4DfQPHk;a*^;)10xVC|S#Rn6{bc;v( z%Irb<^Oowk?Rii?RP^n+***kfkJpP}Ac*YjEac|pAuq20`33pnRUp43X=yo&<42F; z(18PLV`Ke6!PI{h`~Kn4#~<4{?f_P@z}Bu@e~Z`a`KN-yqNOwE%*BlAYWM_bTf`_- zbNmF3A3KIqr)%Vh)X~v_UIE%@Gy+##8Ms^WM1D^A%;Kz@OxJcjgE1rz!_1~gNDJGh zzUTuQxM^OPnv=dGnQR1RFM&BL3;Fr^m@uIXlP6bU#*At~WHT{o@??Vd9u9{d5rf`;KmOzsZ;T6oFWazThA=+9?hgjw0jyG8gTURVH`Pf z2z7O}=uQe4mORNk zvZ+mW6^ct+H>SpUn>(3|rfW&tPqiwOrZ#K3S}d9_b5s!%)pjdE+q#s!Hzl`@#jazH zTC=5dn_7XBS(1zsZaqCu_B|vx9h`0t4h}6xdz@P##QHmzp}i!tB@)dSs|mo8H3sX$ zXC})s(|nVCPhmDKO;-dp|B7Xxh$~!Ns?VWNNNSm#ogL_CZxdv7Mv@hQx=et1rqD*0 zELn<$ix&%1vJ4IN_1OH{tEf3~T&UCRXX2ROdFtt>8paI3mtS_-jY2*BZgy^7H32su z07&X_&B>EEa##SjzE<8?o=_`jB&wV=S&T{*Qnd&Mv*d_H>Mf~U!tjSsE2Iq(jYJU< z?5xIAOJnl{Th0Qm6s8}y>f4-K8j>5BwB57`yo8P?zfZLBOqGu(V1*_2Jln9sY)%no zamdabZ~uSV#W~3lG%<~?Mv2wSF#Rj zRsb{W?@agiJDBl`$#T|vwLcSMV78hlrlXHJ40F2rZM3)5L1!F8-aS)ny~;Ej5}bkr zdX0U3@cDf5FIfrHr(0Uj;>3yLLUrDc`uaNL=Hy}4tXWvHY&n*%T!o5?i8y#*FJ61) zWpsCS?Ggw3>~qgO``oAkcydLRGJ9_Iy~0-gQciwhR!Qju3ApCQMjShK1T{4$g&nJ+ zx_Tz&&0mO#m6L?(5R?xp5{aOzOBndVCOCVx8EtJX!r<=|BUiT^!RU7ayQwdJzO-1HBk&z91 zUGs7?=6sxlJr#Fe#V510fsZ@f0oMl^vV%SF3-A&#WebgxG|ZCX5=GI{|#>R`!J%jqX)1<0?^`-5#-$D10 zkq7Y7rAvbadHMH=3;%Q=D_a=t1%gFmsI5IMMv{F(W$47*xpT2%)oPRqwILdbp|39_ zzHdYA=~Fl@Ud?A3rK&*=tA0msHt;1&hl1TVk+K83G92N92mY{+mF}R%Qw+MP?CEsB zAHMi}eiRiIW9qaSm@{`CiVBOcecKki{`w|4Fc1S@uw)sQtyqb&vNF8+##S6Tct8MJ z{a#OR=nHS{+|fH?0A8|aQI;54zAqs2L0=$I;LMIz)hZ@ec@D$8DQuo8ov%w|Po)I?* z#^_(f!c|q%u=s){(wVzu^G4mo%*Xus3ow7-BFWfC4jn+P&|(EE|GK-U_Y?aM?C(zJ zjGq40=84|2it_&ys>?^nW+87H8Rz@=?G@nNk41|wz*W~=FVu%Th?7e7_>JwaW82nE zXlSUH_5fpwIVCYLc%9VDbb!Z(&`@j;0|grd6Ab3OqcOY3&H-ShGkB)kCTyIa4NvNe z)&M2Js%RB9@u5QpVYtFbI}L|J63|3=G@q<^yn^hyh4va*;`95;OD2>()zsJ+8y0}) z&7c28ae4PfqA>~HoZKAj*}V(Lj~~M&m#)LA3)g5~xZYkI5TJhTwU>oHUn?)#0B(8> zmg;ww1_06l^l%!MrSX|%0JvBh%s*TWg^%#PvQhqhEd3_V*JA*gSh>BuRp2FT0e4iA z9s!u}MGjq>YV~=&%YuHta=Nzmg&_lY`LdJ$ao}D(vwKq6Rvoo+Ef?~8r)}db0VASQY?qnn7*z50J$6E`@ zJRd?*7_bg*3Seq!X^{X91pIP3Dk=ywCo5~s)TvXC)toxDd(Z%0x@75GugCYh-oCz~ zuwdBI2@}xN)Pw{3_hG~3SE9Ol4!U}}Wu(c*mtR0beO&@@(_t?*${^%JRIRO@a3nhZ zSlw4T=Eraty}##?DFaoeN@~r>VCS~BRumQ#%IPVZvGI63N_JNE>SFHAMpBq9En6$(Sd0Gmcqt%ys-_-S6qmB3ok%tSGS=4vv}>57i5@ZeC}+x&Zyzo zR4fg3I-QO-oX$9y{rV5fNUt-b8f=E+bmK!j&RBwXAV*<3&vm3d?5U2;`n8wiOdOK| zAd@FgmeGp6y&+=cJbxfCdB*hWKOaAF+|eFRLSE48zg1P$8@mO=dVO9L6&GRG&Yh^5 zLY}nc=;`i7M|(RqZ+r=z9ql@Jd5VWPM$Ku)HxEbU?GGd)*@+Vym!~i{hI+o?+W%~f z0LjzbW-xs#)iX@@yWw7II&I*5EW%M=UoTFaI3di{2^2Sob^+*EEOvu1l5b6QG#h{~ zxL`?<7!5uzRA+@o0djAjK7CpSTCBNb9m3)-on-sgO)_lE9%DL?`ZZK=vjH2JJmY~t zP`d0HLq8#n!|9kj#kCK2uo^4CjFrtip4Z35>3IysRAVXo6oHsL(?pE4daqYtt+T63 zV3hyU6_;I6JRpF59?zZ8Xmnv;s1I2=*@A(6a=v%vs?`Vtvn7DZS=HE3m!LL}y{bhZ zC3}G;%qLEqh{{Uwnp7$0rO6?{M=Y{~jqtVGm%Uk|8HfL3mMJ^ycd$)ur1wfbe!Qc> zau0kI2Ihg7b1>{S++IY|RnjRbT`FC|W8Fy`9zgT3VGmo?0dgv2+e& z0lFC*EKLj5wn$%F+9xxfXj+tINj5VR6Cl3C6V}`aTwlDPT_-IKkDKilb9+89CE%eHjwjgG#Gj)s_bCn+SBpu z4|I-!wKy6~b8~ZrHd!gx7Le&$CQMi|4++3I+1Yc%b0=H)he;~*%o(%j`QH`_g&47K zfdE6JV6cLM0vXRq!5R(qwL(8{bO_EE3)_k(y{8+akj`jSL2h9sX3v`oo>hpR5TlzF z01u>LzmC1FMpxnY`)rHjKTnmnVDL~I>@O;dYJ@u-PoP! zD}rc=olHlBc!B_J8J%KTZl~yH(^YJR&Z!0XnCY)ZY;M=dP8^73*&v-=Lv?7Sa}GA7 z0gI+(D+GY2PoE)uO|5NhGUSpHUr_Xx0Qq~@uUi{T0&q@l_B7GR>xq2{6xjj*^>uYv zuwap72#O9lcI1$)gN$sn)8LO2m6<$L$y}k{cI?}K2!1cco_cllPaPrqW`w=2qPLT3 zABjXwG%?F4EH~kCJgesK+*~%NEgj^DuaD$$Ta_ABv9F!6rO$)0=2-){1ioO>`1umA zTFY)C7q5HuSx5ak={VF5!*e$b#jBidKVc5wXF~wkP=a^{*YCB+tQhKQPh;Vt3s5LX zw7%}N#3HppU$~E!i!O^r)s;yA7W=LfjZ{JhAoV#afJ{+UGiJ_0uRwlF^BL(KOYLVCYHOh>WR*r+nQBOwDwKeLz#ff7r9B#pX@iyqN>#p(=^(VdRBo2|VVgG-R>k4%+|*Hz69w9sJ-c!Y;2SWg^dv4j^whRkmYiMYYT0`pVWeFW*tRsn-ygo8bw6v%W zN&BqHxDa0mV$Dw(&mX45v%WAZc)4Ysp4qZ^8YyIWQqDX3K&cw)cY+E|{Ut00QEG=| z``5|ynkt}yr3vD}+NR^_yz0;PPTkMwIa0r%tR6J8k`)M|y{#QNIXP%=?~q!XKcK8z zx$;8)wr#J6C4i?+o0cnfTtPolqf$I(ZEY>)&0io}j8;uF3-%q}4X|N6+*CKKNCu;f z2RXv{C#M#vbCf86?1%JLKK1vY*$56qaO}iVg*8=uJ~F1%Xm`z|L4~DgZ{AM_9!uW| z8wbF$G%%35{}C}wojMJLMMW~pWkKOI$;vd1>-YKQdlgnCHasc;9Ps(43Y%#v*}s}M z)FV~6swq?CUs6Zg+uO$YfuwR~BnN}jb+?I|w04fv%$tsPkPXoBI7T++IQ4NDcW}2@ zSa6{C{ejSFoBsB-Gc`{GVClrc_ZQ*_41V=mI^N-d`_02@#F*p$SQ>-u(5bpja*%{2$GFWI?esp?E7dmr?K|3?c5M34SlYrvpHAx%q)EP! zyxe?grcnMkB3AN;aFtVXp*f}#%j5A*r`}ICjpEVRm-5^Ch5k*Bi;j+V(>pri9@$ZA zl#x`yMqAw*Xq6B8u`S2z*{9>ANykw+)EMQVIEm6p5-~ha@<=^vB20?AkRE7(Jz6JB zq)HC0m}YX#umIRorRe2D3;>f0o9T`syEivg;|$gV%|;Oze3+eTqz0yRz4d>q$-L49 z`DhM0>4I<|2IV7oef=}Du`(&t7n0s*324E_UayusUz=8Bl=ctI@B8-?YPyESnV^|>)~m5ruZ0K&p;`G#@ro-vB*y~v_XEn zv`NE^;gU#c87~2v)M%NGnWhe9_EwbS7ka%qfIWrQV0WTvDiuLO^NHkz9y}1vh1k^5 z8X&BlhatT%J2nXIfREyR^AUQsBN-ErYNjCFwit=u2HJik;^gDzl1v&OjhZE7$oni4 z(sMIDy+H9X4Zt47n@2PDGLMzqh$BBQU!TlZ9U_|2u@?ptGnxtjLuDv7%nmvdoo_Vf zf7l?dC7pH|jn!6cP#t#U2KC{XNa+kDV<)kw8qb|2z-+{|tC~M5+voB5bp}>)w0Hp3 ze4x^JR#n{tsitu=qs^);Lyk8XPcj=shv0Bt;6EC`87%;eW!n!XvM$|wVi-DWkL2^# zJ`3tJt7}k%q!|J$OI0Hv;OvtCW=zRam?e0gehmbYGVnXI$= zJx6;m;21a|`FIRe!-@G#XFEQ~--gQmd=U3Eo<4y726}6qWYt{HDI+EUugbWgLMs8e zKGdj>Bh+YGqGv1e)o(+O)Km)_2yE zpyQ_#1kXq4xeTX659b>A5C)~8XbOYPn>EtJk**(SaQDR>3$vR=_P9R~kQ;b)5Zhwt zwY8sW!u_R_>l71CwWDs^1q-H5*u{B+d) zbf4EqngBx?tj6M54~75+V}hk70)3H=YW(C?;;A9*Iyp}#v)*9Tqp?U+I$Td8OZ!2d zAHp%X9M$PQpV3G-I2P*i*gb1&!ocMJDOf8e8Q3fE%Z$Vhas$SZeo0w373xTXd@xD) zMz*BtSUMKZw+aqy(9fD#3DqD|9jF4_JQD8XI)Kd_XO<$)+)`zJpb@#h1)G%VvHt}kY=i!D`GfnMx;i)f1_4yPHz+6rv!IJcd+jn3C zH(wxYI&unD!Jjon@Oc(RgSh&!E3Rk=hN^}T={t?C_N@r@98NKeFh|{wTi#eqCa&tM z8oXMDU+z4r4n+IRgZ$!k$St^7vMv>5(sV=BZ8%q4(E&-M7R;H8K=w>@3lMjleO|yM zoF>=99)%@;;c6%x*5-j_oEBq_%05eiwA~2GFW!K>q79O9IgjYTbv;$CxMIhQs^LRO zZsEl!D7{L@3?ESScT7PhcI@lzVLF zwno(q1IkVsvBIG$W=Y97c{`Za@lRg=1TlI9T=sRvl_3gaKKKNVoWPsPBDVXKbro7G zNvo{~Hj9@HP~~3n1Tns4J6&<*Tt_fX;smXVvXd38F|T8ND5e8g0QjIldY<%qSzw0 ztqv_0$F7W?P%u&4fy!)kjn6fq)qQmRj18xZ>OedluN0~1-+Ilj<5i}0*R=vR%t&3% z`?saz20a_pGCFGbNF*lr@%z2923XN#-(ejWe3}}w-&qi)Jq_z177MR(U==bI#73|0rGQy zw*l|CVUbk#etrK|{QI|`8<9acrfO567c8vAQqec)gm3OVjoo|eW#6EFIa1)IT1EG~ ze>Lv8dkwblsKGyd=`nP5h0+4?U~+~e#Y*a!@-i(vgvHI?kx54q3Ptgb8yDe|A6?ro z3wy+kKJf6H_{q;V4cK@Du1hYOf!l6gf@`mukJmOI$159;;j3S~M(#`Roz|{8txLA8 zrPV*v@mDUNg8%sU8}ZhzTKwhFJ^0!euf@N8>sf5scG6^Gr}2RXAR0t|^^;v;$9C7#9oHDlN^G&n`D7ke0SKuy&>*81UhXpMM+fxMdk~aP$cUuXmxj zxeK5B!e5b{VVUSgHHh7NHZA$d+3#C-a-5Hq`C z=M;MGKl{}d{O*D6=;{nfb7;7f7>#oL^SvAJp}SYxe##a7{~L6aqD$TJirr|6b_|%#J*$`)SPPVH?sk$G^BD2 z^eiJ070Z`Z;fw$mjo1XPp1v@yylf8g^8s|D z{$aapQ|~YaRT`i*KZ)JXL+|N2sE-bGKg`Co2Becy5HJg5&A;4mVll-Q2+g_Wz{5~! zh_T*&{UZF!y;sVcNp&c{P&Iz~i%t0ck6!Ip?I3mZm5oQyD8NDhq+?#ZdIqXRgVBTB zcc1~^`qvw=a@iEg(4YM1rN|N>|G;}MlqwB@`_^|Z!H<8o2{&B3P^whK{J;3g9k~CY zH!!K90IOF`l{WUlLrrotU9q$Zcig%h|Mi2H@$(^9bnNO_4-S1h6Z+_=F+;Gi8JoxYqy)f1JjbK?YFA_y0 ztaWm)=;Q&xyu$^r24Ifq(~4105KOsEK|z)T`H^GI@Op<1-mIVx)2Ei8rEN~TMgRaH z07*naRJBKHWm20|v~b7Cl?77cqQ2UnEzBJu8_#ZIkIP6n4pR*kT6aP-JO6Sg2iYKm z)S;)JJ0RI;pkb)DH!`65L=9Y5-;R@~TJX^it-+zgXK>FaAH;v&cM}TogD5J@7Pj$H zeEAzsV(q+*Ihkdc9`$}_luG}4;^k2D%)ncU2R<(-YvjcC>pfoY&Rwp=i(K? z$OKH;(L`ss`>s`}np`AUel}*#n1DY#v;(uMC*bI@vnVOf!I%H_DO8l@Va@7klD!ws zufWdTwX#9!e6MdiA+|4*&mkmKb-(ZYR-?(elrNa}J$GD;$&(5(Z*Dnu?Wr3ITEv8{yBN{cu|NPkTd3g?Jo)T?d9L%_1~v+F{lll%ivjrJ zxM>s}IgdR(V;ZEXFU_njb=>CI@v~CH8cutV4#46kpnS_MxsLyZZdRLJm>Hl1ki+ESzsai}7-cG==d`1nUIF>0EoU{;1d z{%NOt@B?}7Xil$MJRkz`o4Zb9VtGF1&nw5%&+nJEEy-2FHeJ1P3V#0U*YODfP#O{Q z1Vht^zjJq;uv_!wc`jWuLkhsgj9*{Qg%{N_u_VzxZ8H6Q6T)1+Y+~=rJ zyDr}_8y~;tVlthtf#OpiUn{hLEL2wJB&EgkdWrnEVL;yTmJhyH%mW>RY=qO=69Z#+YD#AiUx0TdeXGlx4v;B zzVVgogtm1Ociy%fOGKal)_0#r&FR+kopKJA6*JPv>dG@ucwRgXV0#TSuE%OI$4p)6 zd!^|CJ^lOvJpb~c0Tmyzh0=SV3_-Eolh5qOrp+g$^Xe%9I$~C0;a~l3E0$g`Nlr_> z^4d{ccg=ik-Bu&*5ZY(!_LG8b`|y-_PlLcW?t4apl^kLJ@x2$Mh)L zMZ=lSb2`dsq#Ra#ePL|gavX~nR?6>4^V#v%X&GcOQbO(a$YXotfc}XOUxGyoCdzJ0 zbNN5~-;H?og+n7U*Gz+PLt`gC{HX_}L-SkTxB>S+xC8J1=pWF~&^|biI_uv<&3Uq_ zZD#Da>WVA7JG;8FYwPQT%3CDVz8>6i%WVinxUf0v@!V67;mnyv8JT6+5R_4%aKhWo zG$H+sx+5KlB`44Iur+#75QqT|>G>0#Au8CP0!WPF{siN_y7 zNogs{%gWHw(jv5qEEE+K?(6F6TD5!M-fo?N;~dV+j2wIfMqP|}$ECv&fQ-Xh-&3X# zx))>gvrgN@x6?pOpQ+<;xt>xH4a-n38u{V>dlMi2z(q2|?_a+1xSS`}0y4BobUL;* zSj6kqj%P47oj;(TpK7_D?~bcP+p()FjPHK`B{S2+u;+gJVPYDfX&Tco)kYe;X1@}D zKl_D8q?0u(L2XaV;&oD?S^tS)V@WnJEDMIYp@U~8YgosGaobByDc*J}umKFs2eq!mh|`j96$Bt_<;_-sZ)fUu%Mm6){cCUD7-5Mo+ntuK0$iX|WVuXs3mR@e&ZsWRdtZ zQj%AYJ2(jWc>rRe7P-vn${2~gx`V}Y%wUPVVhJ#fFT+%{w)&sj>b;XsjhqMEn3Ac)($3@$>@tS@uOKxaxg}rMByW+|?SjEiF zz~;~PvPYT?Y@0E*Sj5}~tt+mKCR}-%a5ju(Ba?^$SM3wgp>3gnPF`tOTybS= zP|WPsFiUV*+ZZ?rr&f@=_;tmVaX~Wc2Y{vx%q>cG+y=IM>xLD&;>zd>Pj3@i_NpdS zQ1SK$aSpdek}Ix^ixJF|*D2|R%WOL;W*i4|^<`IF$rvh5#G@IFrNBA?<+ZV_#fcS< z#&SXJiYtzAQ@KoKOG{5_ewMh9O0CfQbjr6Fgz9Xi+B4a4OS)N$y$dhnh^Ga#ENkE{ zbX{>Jr9>kta`OV1x3CQ5lXKt;C{UvuZOtJZ+}n(X`VN`isXrG|>a?=Ss&X8>+^}yA z26o69&0H6>E3TvxsuD79{sb&vSAk>Ahp@f&7<&4;;Pd%0v2+sF-n0OXhbLg;i^sIO z3PY_p+VAg1ePG-ABzroKp)6={DlR6+h=jWsN*T_$m)tGi>B{&q@BD@3ShBtlf8F#a zw6>mxN6``0Ft?f?(bsm~|(X9jMIIw+;Ju zXdE2hixS5LQY3(M9{z+X2$g7&srnUs&^ z#Gn@R0eKO4sot^LgQqt>hCoFZ<}E0fpzl|O=KZ0_`ag@~Qjg^DrbZ}t$Vb~hoGFp{ zyKh@Ala~GBH(TZQH(t9C#l_k9-(PM)R?sVRPEpR@kAL_QoI2fxM<3rK0ZtExvOiK{ zFiHtZpP9n6td8O$a%Ipw4ZMk5f5H|1L zg!N0W#lZt-`lH%ZLD4!`?a6i$5^~7eNRFi&h7uo5H&{KRROZE{43DHboNetvp8(YQ zORHrfbgBZl_L6FxIV0Ym?Z(=Rt5IH_j|rtYvJ6RWZ9CSlorT#mOVN0y3){BW$eg|e z&Sgs{o*%MG! zmM5zdQsQ+=7fq!wDD9{VVpq~N--rOpCS~EJLx!%dTg0yK+8+I1XaNs9x`eREHJIr^u2c zX9O6j2-&VZ^|<1)*@y^G?>o?l)vKmSMy4_tRQ}|T|M!-x0sQC_d+-JE_a6mdPuI4Y zOj}W&hg<Z&N5izvAQQ>+6#9v8rDyY!J;4AXJ_UZ)XU> zx({f+hnmbhyL1m! z0fF}a+-El6e}4Qbs;f)U(9nUeeEliBAsG0YE9YUkU}-8<`tARG0ZW%up|~hVmIb9+ zNmR%8r@z=N>m5-IBbB>ec2@>x0~06|j~>9NYCaq#6ob+efu~2WRKkq}bc*Qv9!=Bh z$6I9Yh7PFHv%E7CxA93{#qN1#mcrm%uNOSHPEH`Bk|Y-_o`gAb$|S>4iN%A5&qx)5 z0QTypqheI@$)wv`?+bzrEm|7?@yQr8g+pIEYX*f<^Ndp|v(D`?jy2QlpFw3~g9QRcZsq zRvXr?I}we=rk<{=MP7bBdV6|t^DVcEj?M(|YVq9DkKt@{Q`*JNNL?V-Oe`@$Q$~h2 zn3zy2N(QYMv(BzjF~UtK&BLcYcB#DKX;X^vclZ7U&1btL*hnoW#x_FM$Wdj~E~3A* zPfV)jx(T3PhcW#m3sLzLvmn~}UNO*>!FsYZHTt@D&O=>nKeoK}BK#gL;T-pH4OKD0 z)Ug;UD`w+uORmMEzdwY|u2526b1rViOpL8ub&(*uDm?!9Ur<(7E?yIG_Uu{YWMv~i zH+NriOUtSw$BuS;xn-`GAs&Ur?SM~PwE8f~Ba5_QE%Uy{dvjZ8bHnW&y?Ee{Z%Xg- zf9-lj*5Ea^wbm(86z#oHc#QsP$BldJnAiSbj8Skjs^PhEKIqZf>$xWm3lJ|v?nQan zwr4XsyITZmRoUVF{v0fxy$H*ut;9=@)<|u0p#Bz*hfbJlVh&8qV9V6ubuM5gSd80O zJq*@f7^)M0?gXZ(SKJAtt3bg-0IThZPr2#to}(MOUm9J)s_< z5&BUOEJ9Z!@Yo*?qph_UUY{}$sSo(8bx~nr?pUv_gp$=r9>?Vjq$Yda*&SCh7{oM# zv1#K8y!D13B@=SwR1r;$wwyhQ_Vzv~9yTC&lL?18;Fnuwgt!?_tXdZITVg%94fE=7 z(#mFfN?MnuT^VNgA(M`rtF@=w%&0~3Hm3{RRIaKxeiD-Em~&2XCiccRuuLcEP9WC* zDNxt|AM*bEDtyQ4!Cer$GM>WI;Yseei8mF;#w(Hz0)Rs2=K0xZ z|G?FVRTTo!41Q>S4amjcdeH4au4E*tnUlq5&uIs*sk=Lxu+HFBV;rH7P?LEqiG9^v z1+hR7pamLgChI;xUv?0EWfaBZ^<&4DH?aGSexd7Z zbQ3nsP)ca|W)mlk%sMQV{R~2$HM$#ys(d+%G9)nav0fcS zi={$Qw{N;KAg_${;uogGWZ3V(gM@D0SrT7a+14B9+vG!80X3S~A`QSJE;2hS=*B0z zl5s+|YHuhk`>8U9z7l)3XYQz}nqFw@y2PZ*wnyfIaEdx|Tg0*ZKw{R6C=@DjZf>3g^I%LjEfK_M+Q7+#$sPPxrV6ciJTV@NIjhZ1RP2f?nP#I3 z5O#NWqpr3Vot+)B#7S0G777ZBh;e-bvmS%3u+sQ2q%q1_AFAR}^t2|bc~c&JAwnz`OR3h-HuPI!16jZKk`ql; z`)x#@7`+Wu%$3aO)YKLyPn^J+GfgrjRK>(fvt4b03`~{~lnk6D%(lWp3I`71Ow$>h zX>7#Q=`&DJP%thacC>*LvclTCTzuM>>I27^v4vG-HdX5JaXfT&bqQ6d-S%x;YnvQt zso}i39dhFM3HdTW=w!DvH8siOB~HdSH#ehQs9N;>@#Dwk=T!61G(a5Ke*ir_y>3R! zi~uj?NQ+?K-o0q=Xh-FwNdlBbT8$!A%e_WfjYFdU_Hl+V*FxS6CZ@lEKtKR~BJ#z4 zM-CrCZSCn%@-7doLOY|tj&N>S7;G%)SGY<5W)@s)A(XcCvSTd4pdUZ~>Cf@`&)sX< zHuS*%;WPh$-~Rfy$O;BQB7O^)P7Y#RdQY#GmKJ>W?>~$E`}WE2 zT^UF4ina-d`viFRz~}d)yu3mHSJ6~t(OuiQf?5PnTI7p51#ScnOeC zJ@FJyoH!|eqnrHH-+c<#Tzd_Ax_i*n*o5BRUIEtqXg+gB9GPro>J^oY=|e4wz5r_o3c8sGVPa+? z0MQSELh;NMATC50tscJwdqpgSLZ+$I)L+PkuB)p>ZeAYp^76*fvBjOUQYK(sNy0wo z=33zKOl#%$`6Xk%@6Nk%*Sp_~yYISNjJgM<-9pSvIiD*kD$w2AhescM9J}7yiGTnu zG3QS4e*3oVsHv&ZMr_dt*IsumwrqJFt;EKgH_L%w>9VDWMl?31sRv?o3Mi?sug48H zUXKSJ`h!dm|Lil*N(UO*$TUE>GJfEtwr_9mKuKw-mh@i-?->E!GQqYvY816hu&|;H z;`uy^A~lRW@t4%=dhvZD^PyM(w@5H_o!F05Zo_#?%v)4kf|JLOYtu{P=I>y(WLWlk z+fr@N8Q8Qd^nA*UnL$_M>8y#yqNtiug-?F^lhUz7s_>V-_$8^n68N-YXHkSBVLbob z3)r@Gn+$pT(NBJWZ{PQAw6?V3x*M*;^#X|DP#E*(&lB4{kC$J33C}# z2x~AtJ%s%He9T|CK(KDHWNrdBJrDx3>%ba6p*F8QeOdsl*d(1!p$4B3pqwaJmnK6` zu>eX1SPR6fS1@odEu0tIP|R=vk70sf=w@LDpAaCQ!b3u}4x?VE(*f~5s0U2s=H{Za zylqF?~NJjI;Yl1uJz5lI)D@RF__n?N{@pOu!==PtzExX0+>|jLSgR^15T{0 zL|%R#UKfnIYUOhL*Z01IU;N_N`0)>aEWL_9{rOJ?lg6YfeOfd)H2{UdT`O$dABkD_6#!eMw8AsH*JjY(2aYI3vJVC_qUeg*EVFG7rPU z=zTzvQMLfBPpHme!LoG%=svOCBmq{xU@+RgTD&KqpAk@)jEFf`4;M5kXl>yV71XNOo32X!)0u+5F+p|!B_nr^mgYSRu`|`Vq6D#n? zhyEymO`tUx+L#ZN`-$USb?w#o`q#dJb?et**321lN+=Z4vWn1gjX7S5<}v1a>3g~s z<9j#kbNrlF}dElx`hQ8B?qHWn@~O?{9kNO#DQ36w>`)}`sA0s*{U@!2C(<6}LgA5=jt^3O&0~bZ zwt>Rw*w!hcRNuQYwg9I5YZQIO^pcjc5Fk~$q`JMNX)%pK3z!B(vWX`tQ7Y<-!zXJ{ zSyqZE`Go?wUb$}$0agG%N7}~>d7`6t>Hs#_jB;1kckxXcnf zJsL^8TybTj`ZtF)MHG`jrjLF-LO5Ie^~-o+p+N}-HZ)QCoF%^Z>!D3PeLu;`EQKmN zCK^V6+^kbBPu!4M7?{~=6ecCK1KdT0u8gl3%l(XgNo*Nl8c2yvDM%w*|Lhn2Ia^Ru zm|K?NaYNPSlBt=#AGxNo~g4k-{>Tz>Vs_rDY}r)Eq{cPGR%!keZz(fKAgyS&GhH zoYZb@)lx8_laty`%MZsT(W1k;e8UWummSeUu?$z0bIb+@+>!;ZWCDO`N{4_Hv&;Zw zLt|b7CONan8I_}O-NB`OZ?3dsTF`@0MTbD&XUzNRT|J9g;4VGiy;{)7D4UZ`hiBIR zl7DACq3&#BP)bmeEpF!#&t&I{D??PFynUqHxdd-FLX|B-Z)3!B`)#hwBRPIprBxh-yeLxlB6m#yEDs$uq&COMpJw; z^-E%51H|OaBK9<9>-~CnRn>&(20dn&zGu<_rg^2wiA4@Cdnr#QliD269a#iOigA!h zH4?L`GVM2y&eZfG*7hWTmsICSG$J#7@G)jzrrW&?%mK>Wd=|#~WyNqyCnW)MaqCKk z2{8{X@AUh8Xcr7=O?~6h*_Y;WL*W?uLaJb>u$%|hLd|W;4*&ol07*naRAE(wLV~@+ zs?3$8=-WH_0JA_$zn3XNURBi;Gw<5i4pec>a^2@_ODk(hQZE5)j5_u(E*HJ7WUkDp zo{3JOLQwnq_aB4L<3siID)E|*Lr0F`uTMOIV@Ho7)Z6Xo-=P7a zsi8rp=_%C{28^G^k-)qT(3H_)c9P~vId|av(G^$5O_ioa1~X^RMzdf_0nL4%7v=gH|CSA|hUwx&X^NpFE z-(X!@)Y8<{D1e?T*?p{MPvZ;v?Th!h9uUnEbYmlDj%47Z)UiDIK(4rwks!{?$HGO6 z-Db~}$NnFC@fqcV5iJejS7vPrhJwjetvSC&>GCls@+U^1$3(2`P04nWI83kveF zV8H^J8+yDFHP~wdLl3@UHdaOoO7&`M4QX^zEJxx=di1y^=yS!Der#fTQI^W$l45kU zcgSg`_SROJ7||#%YS@u7(}v$K6Fn3cm&nvPTu<0Ae)r&%6KjCaCVJ5FjZ5UL+coiQtVoT2#qFX7$iZu&9}k9*On$U3^~2SmtZRq1Qxag8H18 zfn%&Qa=7BkaDYrgVD;%q{*jb^x1xD~5!KN}i;vyC8PuTvD&k zjq4oPJmq>Q8BbQB<2$p25g@U^(s(=%nzHj*rtK(DJv>Im5h&~~=4O#~Wjt8flGM&D zvJg*^m7qfFv6u0jvzZZ#MRDZ&PAb^EUon@0l@@SZyE2A=PXjXnOwaEnZ<@3tJ@1*8 zn@^wmlgrDr%qenE;QGG$?1EOQf@#~+_w;*f7ejuImNSVbB=<9tG|;w2ynEEmHtWhz z!J3kFlPfb=mtH8=JVOpz^SA<;u0QDKD9Pt|;&l%?dLZA^4IiyzIMc=?cv?Cc>RT92 zG&1xFc0PQXp*PGVpqe|hZQHuEZRviF)G{=;GYRBYv2SfNLua2jP6xxmQ<{_DL<7U; zf9gd|6GMBq#`=`dvaJi~4FOH!+;wM}96V?ndU+JLGrBT5HZn_?7v^yWX13(%upym~ zYcL0~+l+}EbE;x^@huNFEK#v~o{FD5>cOE}1t9^d_g@vm{m*+O7#mv|iu1Invb-$d z9T&&&?+?`rr*QoIDG%PUMnzc>#|v*Na%IbPD^*;vEQYX~HahbdE`zhI zI@!QRcqc3wMPy-bt`|Bp0#XtazVigi9`Bnb5ch>O^_SkC70mh9Endv5qpx!L&ydTrF61 z^F9r%w4bV;yuUrDl=|n_1k~cv^KkG1KZXQTK!RpMlz49(9G(7w!}g+*w-YEx8w$mGN_uGT2aJJ)tY`Y z5}6qfBq&trNkYB8@uC~L^82hvno`}zVus(?f^3~80JbQ*0!rG=1B`Y=skJ5 zC5ywIGtNjR7JZ_skmC&1DC6wh8iA|}h&Z-L*1`M|OLpA0#8Onp_^G-!(|wvXkOW z%^ZAuO9YP$=vBB=I_J3>P2yphrA?+X6Z=EBsS(?X7aBDqtnDy)EmZ*w&gr&6RTiybNZJXjRvyq8Ys;*1me*ItMH%mBkk;XwzdX zp42Ycjm0vWeF;!qU0pc1{~#J0o6Ow31TreYLEgEZ?%t$WZaEqYaQ1Zf;Lw3XIMZ~- zEEp}1>+{NGXmreoUPPSa_^}h{>g;ksd=7!PCk(uL#Di=fN6^brlFhNVj^Wsu%=1J$ ztp(-|)NIX2N&S>_mFcUIxP1c{72H_HVVD57f8Rme|LglvTwE-`)`$(4ug5jlUyat* zR&3t51+Tum5lfdZ!ENup)mEUJz*}Eek01Zf|02I2A1!BFaP1A(;L>#$%e&pPYcKxz z;KO1N2;zeuz8jU3CgQngUKHQ&K_D2wop;?K|GM>vGh0YyKGdY(_*n(-yD*A^AaKn* z74>ZjUOA%RJ;iF~77bz+o=xG0G61Zq9_FeVW^w%G;lE(brEBo^ zo36uKZ|=fRe)u!2zkD5zA3cHQ=Cdd%DHaUfopiwj;3pq@3e&5n0GMr;OzWyto$fXPvUMBQYPDgr zvXGraX+t?KSh5%&y61hEK682!$VeTga*_4*4Vb@Ro}6EpE`UiH8Av_e{`z)Im{2NH zvON3r^HR;GW4!I^E9GEt>Do&q<7e7pa95lGQJ%-q(FYuERAeVV+s$xDysya9f^B$) z4LHs|C<`LFJrjo(WX0pl32l3`bQ0{0*Ud7_n4tkQ)EC0T5B^DlnLtc-Yg7O^mSSC> zR6WFRUSTtm!GbVET+{IC1HmrtMt}9QU!$O~09RderDWH12Y@6ESbXm4?#5+TZos?mycO*oUHF&Je+e}wYfwF_ z+Ga<)a&9m%`5WJHK@`=c3Qo2%R0~yl*<_C5T%K|B_KvHO%&e3NDAY4^7_%i_qc^7- zkTP+gB5(1-5BwRueSP@U-+f##s!xI}m2FC1YL949s!T^r-O=7IMq?(^ijqp4otuNa zygYMz%Fgfi`y}Hs*W5j4K}@rRE2gPfIZYq<)yxdK8Q31zqp$o_Xq7l$A}urcJM-YDyIf3ktC2lGXUxkAERm=uE*j2Z z5T6TKQk?x(mPLfj_^f^B^LW}?dwU=B`MeK$Bg8TZ^P9G1{TA~$6P&Nam^1>B=a_6( z0vL@<6Us_aI-yjk*HO%!HyeF|UCG{MipI1(J-rgtm#(`+uyq8_Klh>ldyc@4o8}Y#IX!?8Ab#KGl zuDL>1V5je5Qm<06T(I-96-(td>NqF+cIA9Rumc-&W>o2hB>{&Kp%u=bP~baT(;D&` z={a~}3JzmJI0JJ-59F{)1fKOXfj(lUwX|(i{f2K@oHtw1d%Cf z@_>+>f7UIl;)*My&A{Br0yaZ>jb()bvteH_N3xG#U}k23wt8`$JlL+d;>sB4(K?oy zwc3K^t-xuxv7td@V3U{~Mghl&4p&@pWrTI;MBF9*tR-fPuiNLmwdIWbq&O{w;7NJ# zTye#fG38^^nas_wNhcxqvl<@&nCZUGu!J10xbim))>O8tJ*JY!b?Z2QnUN_dUg=v; zqLmp4mZeaJxPFsv&dn0($~mM*_lpGXP(DSm+rP|i`;K!QAzckkw6Xmk$7JOKgUuTg&!}o^d33-Zum0d7RX`b1}oeE9bnLZH8*u{Zn4@XtAUUXRu{|A5YtWn76wx4mPTuu=gaxEo)== z`IBC(UmU}e+q9(WR4!#&xr*H<6x@7?mdBX@_}X3toA)WQklC_XDz3XQhF?A7MNK2a z(wQnQUZ~=?&v_7uGP%s~(>GDFc7}&v^<6@u6#CI4QP$WU~k5g&gm_GA2{bs;-aV zoDiQ9&yRHuA=W{`=1DoD6|tqePlX%tN5P?|d!HtqAsk|@pX zKzldC+C?h(qU7yV4&}j@JviRLWTl21*ThiM zMCD8we(|(d9&>sHuyMD7A3o}pU_W%4;fbvtnLOT=%y$k^lgx5h3Xa*6 zIRbvI?(e}nFnT`0@KWr;9aY1V0BGN71~URfo9Y8H`g`%A%)wP&=3AV(J^?T*B>mt4c3#w#umn4|h{RLHxzO{?iU zf2vj6i!I!sNPQc5EDb_TYeRw{Cd?fRore?#$xj$iA}NNj}GC-VxR8 zU}&sJwTj4|{rqiFq2h9UJ>5bl1SYpb~Iu@=`*RlRDof^R8_Jr?YC@}rbbtl#urBt%Tj@GV&Zj+ zRP&g`yi{tGjQxR_IWe#hN zL;#}i$WEn(GRmh!EUGgBJEq1}YI<40NA-R~`Y~db5-dhdU^)i&DG5bm ztn&W8(AmfE=4nNO)S8?zAOL$>sM1H98M?y4R<2c0-^nsG zKqny@lEJaef+Cj_GJDDnyZxKV4d?vunHw^7-r2n^-M}B-@Zh!M3c^C$>Wez}4eH+A zApztz;CI_S62#-j!gl3133b*AgWNfHme;;va(zRdp-`sW&Q?{HvThJ?DTy6rs5ELG zxoL6E58Ia#xqo&8IWxIr-uxg(XUIPHOWlRk>B4M|nZj27{1s8j%Hv&jRP#MK#q}D+ z&A5$~ZC+>?p_JBz!?4n|1GvYTbt$FJZ+`JxG&P+e8vt3^S-9)&JCU20i$6a2CmcC= z1X)=@-1M%SFlB0$3*z%Y2((l_g@kO|OJ~IJfUtL^4XisniJ32`R55D;mq0GcN-o+y z9yV~I^C}LvgkppGC+qbp@lCnS1064`!hj62gDLQWN}cT9y;o+KeA_iw;?nh(NbtV$ z(yMrD$4}zMm=1T?BEqsRNQhwOfoYS`OfuoTrxd|+m^=A zA#CLC5F2OkCaTa*D`&Wkff-A-g1t;HF~Sj#^e~O*46O1^*GjdF^7HaAXWndCNwB!M zP-d3g_0}$2vUUxs1$Ze(G*v025~7nPO+qy0mPI)qSa@fhf`{Mq;Df89SU)R%wepHsL3)c_mxlwk(#RLy?_v;{=q9L$48J$;^L=+A>E*l{phP)7RUF)2B}3 zNB{j3SyGfL72b0DyX5!T+1ZHEprjY^rhL$@6aD-UVqXf-xORRF6AQSULiBk#UO()? z;z=rsvo&(Ma&AmAaAwT~C%dRv5+f&L*vomMrpyvD!e^MnA{xV_s!6!_Up^wZ(@p_S*+0UJ{J)ZzvAynqd{CK1sfq2^~MFM(Su@>AzHn0rr$>3C+S#H|+fFpzLbZ{o9?}oK_alTt zVVtTtCG6ZLR8F3RY160StvBA1Wk>h#+b^%5O0Kvyea~OV7EL2jcx7HN6BnQ)MNlOf zu1R0Q9(d)o6m8J6cQC|fpE8fRsFv^QH5cO0!Na)kU%xHqe5q*JyqU8xxoUj^+>(fX~ z-^1zxiYw7Pqd-h5F}HnR%fT$B4{xN7FUOVm!t7Zse6K=MY?38tQb4xPOHy3U|zArM52on+I=U2(;gvC^XLYlPzztEw?0frTO%m}Zd_OJXga zkY8(b!D~!mxkH32=Z3yJIx2LcgP5f}w?5@7b`WgLD3C)r&PH{Bp*6fcJw0;q zfh(?@3#x875*_))F|5BIxC4gGIVQ!+6lTM{EvY5LsM*0XKaod~>vug#D!ePMWFmkW zyrY)k#o{bx4s_4s2<7p@gUqYQKn^E^TI2M%g3Ctjp|~k;Tsc<&JSu(F30_I(Nt`9p zDOf8!5wBjrd7Qb&n-!gwG`ix--`v39phr8+3dMazrmI?^`hMica-{1-LwiicBWz?( zOh%q~R0TZdD%r05EelTp<O zz@2Y@HKTl!0l zMcal#A@egeA}vRxzqICf>+9PP3IEkDAAzvNch$9i{c5 zxjT=?E6+j4vknHXj6Hbea?@B$YYXw7+J)Ma#CSjY?c<#9ne}JFT9*7Wrf{!G2iBMk z9v`9D7^={8zK&=YHg4O9P){F1EFv^%1?_F^*tla8=3hGpSuAK?GG(3I_{wWIar}hL zNI9Xr44v&AGP&KmZht3sZr_PFcf2JR_h0|^Yw`M~t!Qj)L{E1wDkfIoY;&_r25{p| zH(>L|EjV!Cp#1*Qbr<8YN1l+NBS2nt?Ui`;spn)OfvTxhc;&@cWYU1@nbk5kHA#DW zyWEDdU@p2~Ar@b-2!DR)|HOb2me>1^cfB14_8r8oUAvK&n}=&}xLPJ4a6vrw;H78& zkN@~N965X#zx>s&@$erX!h;Vyfbaj$4{)aW48HQEFX2<4{tRxv<93sl2CfAy;!oMdMtu&nj033)*5aXfbN8sdC=9}g($Z#(5G!q zo~S`pRTb7>whnuC?iN6~37s9C*!|WXY}xcW-gWEEC>Owf>G@YsbFu~t7cIb&Wf$P^ zp+mU!j(4G^<`j1C+JlX+zJ?1gT8aI8_X(gKz^PNGaP@Up$>-5j--M;hmSE}fC34<+ zzxe&4HLDQ}2DE(BG(RXBdUDkyOrJ3wn_k^4^L9lA_}_KgJ7sQay3TD|-w>VRRumNE zi$UO3v;2!IV+&x?o?hR)8Jjn4LTk%e(f+%zWy==S*4EM}AN)@Ff1#w~O``YcRCNv=N!QzuVBlVHk8!E(I@a0D}^Oh-@= zDo)gX5k8?Fmz0*sd|2gzJqwEpWGbCD!P>!K5M>ieFlE{lY<_KvP;CQp+uq(@xh?HO zEJ(4X#GLhY4VX87p4h(tg@uJEDJem|02__i1UA~gqM{s^UwN4XeL$$n^whNY)K;Mh zoJ3`1r38I@Ya1q3PDGK|f5L=PbhLLOCp!xz#f7rm%Z@kR%E+8!f9pbRN&Vt?_x~R4 z9UYiCYc{_8m9OHi_r4FymM@p!o;QC!=8M?yeqjyf8XCJ27+iOg#1EGsqTdH=Td| z73&3a@56J?y@W#t4~fpQ%ynjsBZz0tnkD->B}FVNFBg4al35gQ?b@}nUyPM`WBQ+2 zJgm|-eX4(sE63b)!criIYD*f;nIn&n*yVv`8w%&Ca_Eutf}f*z4L^cO^r6AdWQU4Dpc#suiRiVIekyZB5)hWUvued zc^m@Yd@&$c8=kz_s|1J*JFC6D19i1^=n?=XAQu!ANLBl?%e8a33HQ!TG5W&D+U2Qu zna4fS+{{{M_Av=yio`O16l}*7=IFf|Y4#;Mu`d)sUtb7$xj6!)YVw0hBY49)avGp8 zzN2FhJDS@@V@dlN`y_6gcq}<;hacVDew;oq<%?$!PL+R-SiIbxMAbo+wOXM?<((m z_l-sxjij|nR$8r@3-U^u(P%XD&b#H^d#(z0gK)3^3XR#;j!x;KMMJ?UU=40$#=|hr zYE!N#%;pW5U4jy`SP!$r|0c9{d*np5{IW(KR;&O3AOJ~3K~&SNB1ujDAoJS*?P&mB z$_o(I+tG?DX*ovAbdJaZ%iv3T4jN+=#Nyt7N$Z8Lx-f_zI$A+23Q$#4*TO6=&Y;3v zC8~QGral|R8vT7T2HJ(@J#efXHT8{AUS86>vIs<;SxAXKw{R$o9BTDZ(2j_H5R1O| zn9#yC4UI@~I%J8VvASYN+)TdYItdG^skI$l!o`XPc-8rxhi``DQvggyv?1T;7O$`e z6u-A;2R)h*Xwt-;UESyq{;+{vL*fy{J_d{^TWav9FKex~NWIFW2~DL)j7opP+1xQK z=QHYp4f!&g=&XklB(A5rQnR0l42vjrSct!-sXb)DYTgh-42gn@*I2oMgpJ?^gFKAV zGl7AHYpkO9!aOD>v>}EV_&xb{u4%WRip3-xV7*;qzup&@Y#vZ$D# z+Twe?NgYb5}IaB)uSO^UtXu(W{q(Lh#`iY!ua%h(dA3xi3X5Q z&N!zrrCD%PN&&Fe&t-uRX;}iIrG4q*q$Vd}P+=ZYlbkXlZHOW9BpxwF8=6~j=y(NM z+dF%4jPg^mt5wt4#6m;L7FiPvJ$)<>T``Ulc+*mnF>%BYBna*6HB4wj;!ub`r64C$ zDwxl9@9$+0hbl>?%VrL&2JsNH-ojO*fWe>Ro8I?)F+h{Wn4X#htHsi*1r4FCRdn6GHPF>Fb5Jg{ zNv71m*<-x8e2yEFhIr*DcYFHo zH}nYJpk>}*JX&NXr72Y7G8P{_5ooqkX+X7{%v$qBo5QW=_>FXo$QLt|$61|mjk3V_ z64I)4Yu?>r#@}8Rz&*w=te_j$E@;QZL7m9YaKmBu!eUa)XqTIzvE76NCu~@=I}z`1 zap3h&ZMblz2e+N?MnN`5tZ1;bK=sq;_|0e@ydo6jyR%>)*z3!r)I0lqt$PVL#Mw)9 zt;8~!mYyc5bEs&Z9#nkKea?;3;gpm&>iYyVqOl>N_gh<9B{eM_gNp05h5M?|cM+=w zu}-R!fXQbrRaaNbzuDQ@vY6jUIO%&KU$)!D@c+GP!9#DGQIzSzufEcZStHw#oG3Id zht#_2t)ifDf{i23?nY6T8*|6B>{BzIa?YG{>0}nm`htq*KUw;FC|Jy&%*4`#3fNy{I+c}aU-%*+~^isw##J-%lN4%HwJ}OiK9?}{yXQ_!1Bo3LnW4pWo5D2qj$E_Y`(m?M@xl8a;@*3Hhr51#7tTC$7M^|PIsEts zKgNWK6VTSyCN=Z#@B2McQj_u8tFPgQ-}@m(j2wa1)>g^?Ox+kh-}X6v@}r;P=38&Z zvU8T;)mL7_kG}sSJpJ6$a$lG1nqbBD?c4FIU;IjHU!t5Pa4P5U0iJ#KS^WEn$HaGb z;Fg9zL`v5kTU*Nz1T-lP!F!*x;KkJ@+$ey1)7g!_?i?~R3Cw0vk{(RHzswY9 zRN%8!O-xA`ruE#V)7;2Quf|W+mPzym@(2TCW6?Q&?gOM^YHj3Ptlj$!9s0k z7JjCPh4!T{U-8NcOq?_kU%C1#lBDk2-}?@J`~Q9mn{0&vX$nu6GyxOFkHd{Z`;uw9 zfA2oo)`Y-o7B<$auf8fjd&@U(#-Kri@y*-6DOn}o5L(${QA#N3b0sAu5~R0$^A^d1 zsa_-6=5!`u;GjVmJa`Zsi4MuDWpqsJI}!6Lp8MF0r#>{}>bWhrZeBggkL^d<(ZgtO zX+&pdE4n*c#eEVJtcR#!-QsR}B~a;})5J^#oVXixd^Ygme`J40`O{uB}2bL;5iG;~a`w*Z>L-RmQr8LDI3HG2M z6?I}?uwXu3_|FTt?BdHLaL+vJOw5=$J**FHNDTa53vi%Z_`R=KF?C2MZV;N6VzA9k zjcBN^MM_#0a`TFioRWqLF@Cy)3Qb8#k?xp$n3UdB19EeaC=>wqFv$u>W1vg;(*p&F zzkg9Ze)woM{<*>;ed6dA@bDlLI>_2(TDO*mjZH}aE2T(WgHjgg4;%#O*nwE}feFZT z2J6-}U_Oh-zu&W>{IXXJU@b(hhrwjFUm1t{iLP6m`y!X)>G&Q$j`t<3@%F2>KO&V8g)~vx$J z7idL-%T)(uWr9FFeOL!BnBIn$*CgTMGu@atm8OW-6GdZ*0Q8ef#nCYrb~UEEhfHrpA7z4mD z7OR+Zk5n8B{Z0Jv`#+M=Wiq2FK$Ru{OXf70vE=WnEk5Zpi9$2e zb25`CKy}e27fAsB;CnyB;>8Q`)|>Ch&zyI``Qr1gu-~h)^3!B6dhBTY=ehsj$)}z{ zmzYQ@tE!~-^?Hp6bj%~q0>GbqZbC~tuw+tOkX9l;*GoPxGlK~qnI`NVJ9fZiwxhkh zO=?2BK2@01>#>Guq%VWBYo_Jixw(< zg$2Nup0gA!EiHI`#p_aI|KeA_z?`{r3?H};1~R2PuGwWqah40i^1I>I^ojPkU5dM9 z@+Clsdr7g9W95}_9q$fm{!XSe^{es~P zJbNJ)EnXyVDD6vutv?FjQ&$D6ELrr1fLHzO)M-;Oea3XZMyH7M*RT0Hz9OsvX~qdt zT_%tiYrwGuumDt((7gN0O&C+u0cV0&YDy)2pW4*8B#4>G>kHn9$vZy>2M-^Upj{}m zEwv8awQCoWlAIVlnwA?|l*aB<;a~~?TuunWtR9l*LZX#p*HIJBp4>;pHoOOb`%P%h z8BRgd#xjWI*TfnY1Ok+{4N4>s$-snWn+jI76J1ZD|19@)ZdY$?Up2k@(wooX^4B*r zG`0f8!n~E4t)9RbmjD(Tm2m~YZt;9zX#qan__eXIw3vDEgl{mt1-#MkLVbpky0rUw3Mr+SE{W>HD@D<-5F!1@YXl$I}PkV z6QOmPMRC;`0hdx4HD+_9K-j6g*V~^pBzh)hYbTS@U@A}unRq?q1G^OsEbbKnx-d5j zrN#NEsH#I{WtEIHJDm>9oIXYD>k`N7@TJR|VHak#&0+JKopiVDLi;8qdeG2nl7Nnd zD;cg2Quynk0kFbHrF=rp1ez*f&e3-X(R6WbHgwn4qGj7QbXQful97RwapPdm&j%Np zl^Y}gUmD>GHW6CM$E80@i5pCS(aQzEy>6r=C*W&eITweH)#Le>UdQa26EJhy1PmTB z7!CC`GD}O%clUaeU`*p!LIhe$YU?xwjdY5O zkQOytZ7^99#B(b++1%c4R9Dnu)!I!+c3Oqz9fO>#6k$5oq26voVq%iNp*@ZBWM6Ht zI>ft$kKNgALQ;Yc8&T21Jgl3_5TU?<2X`zZPIn9Ef!@Hu6-@k8T;u3yUaJ*H|NeJW zJn;ku-T6~wFI@(6f*oF$8x1R0;=r$eg)X6yhurn6keNj-9aQ~UO33hdykW}>4e3AH zH!YdNA;5H^!Qv}wXVO%D=IN(Ve*B1RKGxLKg2u)snJ3>MCVQ&@^58*(aOTVlkdl^+ z4wur#r}9J@vU75UhBeFKV^!-7)iK1lFtN?je9p_@atVv_fqESaEP@H`tJ)6-VtuKr zOyp1+2}(B?9-A}FTIHL*>eOyXJGG7euA>)%aye}p*@?oeugp2A4KkDm!b0c{~&A8 zBKbLsxavob9K+*}J}H-+a&oe9-i7DlH^00GH{S9MnGgNJdmrLk-@Q#{;243aex!lL ztr7+pCJ*!A=?`ouud|>e$0aB1JAZa3*1Y{NYHKQC7WbWU16tcUk(`>2*7jCZR#u{= zu?CI8?`>#mgS)$3j1L>y+gf0!>@D$McUK~~Kf0^Lgf<^YfZYm=E%o}!52NH8AP{iY zHV6$?s=tF+xPEB`m9;#EU!TIE3d$0x5MOc$Vc6KV)op= z9cf~FS6MmS!Y5XviSNGsK8BANhM)ZWhd6ThDE92$i>}U2y!z4#)YsL^0tgtDME%x` zo;%))E;qxfT@J~z5dgRsZdbP~@uYOgPBDgRYpRf7<(NKcIBKdZP>`2_@neRdzP1uw z9c?lT%hlZ_EVE9y#Ap4%D-)Nd^T#`!m@t^5G?zz_YDZh0z)zx}RWcp!2Psp^A{A(c zQ%HT*Ezmz1@*aLR)6LRcGH+yNWg@|5gHxEw&CSgyEG|Uppn)=t(n{-nhV)Az5RD(? z#njL#-^b!8!n#~t*riVY17_3(5z!HQNq^ZXmaIy(f1)s5P!a&)vei?QvM z$shEfWhHe~5i#CJ+nqQjOz2C_bc?YRJNGe!*2@1kI4sRHV_rslxR%zHg{|k0*)Je% z>liu&jce8-p|Ais=bsO+m{q+U9q_idA#=e3B##|~#+2rUYTqUbo1Nss;<5Ta_wIXhj=QV3Nv5 zy|OCFE#J3T?5JtzMooPu1`i%6wh0%jyc%UEs$jP;fA*Hw2YLdJ)xxED{=`Qqm@(*P*v|%HL-hDS5gNMi< zmSe<76y0zGI*%Tc{_o(Q{ah|FabNMmyoGb+F?Md>g|A=xRTP&NVeb4nvSR6$+iybU ziOP^{D?|E^P$I{iab8?}rU%ciN)rBQH_jZ(uE0a!TmRN%O#}fvU0E|{_Eqer0PR zmW<3;a_(aPYbrZD_xxoN++)U$mSuq9@}LdrKfx@L?_TW2uA?^m{>5~3FFJ-3Cn`{0 zQB5AKpE3trtJwnY({p91(d1`;X>4gn>F8R#`gsxF-|E1Ru5jawQQnwnU-fq}775nj zz!xCvvnUwlnjn_8QA;1_V4{8%AS=1!4p@ceJbd3DaP)6~fi*K5?#4#Ag=w67#pNiv z`4(TuE;x7DrMv1XV2%3S)5jMckk0Y@8(g^k?^fLNVj&g}xdKz?oB-t{xjkb1_?B2m z6La4nri_1+h8N2h0D{db#tcirKi(b>&0DvjySf_Ij0_}?8z+3@LL-&(ODts8jx6!w@3*_~ z(?_%MZrLSBE$zaUbDNQy?v~X=9&co;(InfiDADCDhZ6Cxw^Fh5D8p^%xpBvZZn@pl zheL})XZ<2lV-?ke*1XNN379&bo|w9hq=`E_1!yyswO(IhDw)w?KKLQ z^JDK?@Z`s-c>0qR49xAu&^(vW$R0TDUfIm7vCWKQ)fVilu%WWif}Awq#-$!yHP4O0 zERHUBtj9U;!K4m0pk+F?*Q;4N_LQdRx*jrZGy{Q9ElOysks?{xa>>ls8H5V?EwY$j<~;jaG;kgf#)cRY??6^5U)D8! zIHd2O*vaW|Zrrl%vrEPEYKuyG&HcI0EX?8AX?Y(cDSW(Vb5WltizQ_G>^J95tfEn# zXr!X?Q7N6YQzs<|#TlW4J{LL!y^Q&K4=S&P@7p!Ba7fj@Vi5F#w!TYQqz!>oa}##=z;2LYh@W+yi2M$_vDn6s2$(rW0pMCNkRoVO@5xZ zo&dGZZ*Wov;l176MIHLQ7QdwvhR2efrmin#w6wQ(pqohBLwQQ$%!KwzQgVmWDF-TP zj!yBK_`OV~csKt!+B@3ix$8SZ_6m5FNbG;Dv1x!y5XYrv(m1fXq8)OTP1Hw5PGCOr zSmsmHTATZHbzh_z<@NNF8H$B;qM-IL{bh3HULF-IhcxfWz)KCid(zCOa*SANT*ks{ zV7*%_*(jM3zA^YvnGHavK z8coc(+2jW>*L_A)R>;FS#D*Bs=Kv08f)MAKO+2g?3)Z~96IU*}2M_)6Em(y4Zu4ym zC-I?$c?4d%fG?{qBPm)~tq1l))D!~Z;25?6;?orj{f%PCqz5lkK=|5o>+$)ePDI%R;Go!+5BS``>`B3U;=~EO^y15CZ)?Z=g$po!`ZT=! z;!D`QXAkT)yKJIG&7?PP-Xi|mk(ZZ;i!Q!Uf<7L=OOf~&Uw9e2ckPnrdg-N?;K0EH zSi5E&uKwy(62vb&{{ogSTZ-i5ptFv z+pqe&vg8zLUAiyI?RaD*>33dri$hGegDHq_$8dR1Y4FqF{=a|x3q?gmSiEF0o_OqW z96NRtTep6W!Gj0ml1nefj2YAM*_N%y%^e_L&zyBe+yn(l^S<-eJNSItHe4mZdic;` ztXT0H8X6k#(DH}z(MKQ4iDuQx)siSZ-h-Jmp|1X5Csx0|L+D)gY$6gcr~8<=lhbbsb2+XnVw3Aab~vY0j%+Ljn*SQ4i;+HL8S3ZmM+Hh z8PiZ)T7n&)?~od|QGlRAXlJ^NWaczCHKU@O2-i5w7IR!oK%`AKZ}|)h&Yq94W5-~z z_#K~b`yAcfE=-#?6(4{2G0KjYA<^N8`+-hNMf-M^;X7abCn_rHVD>GnkQP3&uTnft z$k&r*4uQ>N#oa%73NJme8rC?t7pLMLYuPzC*A+P^0QM@GSekkz&Jj5xW{3nM^Ra15 zUr~TsCIpD6p7@JT?0A4{&0pOc1_qa;ihVZfa;px5tI?69-}3)WLC;h1KIF zju1aKSyTXX9eFI{e%eh#LWP1p0|w;4X0u`ay7f4C@Srq7hYufyuFftjJbMAIzwsLw zIdX)|pgQxcGjYRB-@wF4;fu>yQ^Tj!5WoP40_y1X%_T;ChrlyE<9w$hmuvvjDS`cp{13f!-nFAKmLK#s#IJ}xye^u zeWl#4lF46u$%R3$<7pD2b0&~dkcxbFGDXLX87&|59!*9+x%0a02 zE!qu?>t;IthYj#X(qui);=Et~hIn0xAOM)! zeO|ARxY6SdwI;lM_B+48_v#W&JigdOz=UQjs0+BiU6O%aL*j7KA<*elDrJjN-*bn< zhVT6HVt6=p9d!42tiGG5SdYMF!S(z>{mV=qA1^e*I>|hYq0%SWqJ|g}Q46yk3i&BI z-R*Us(s6j)X<%Pgqz>pz15%S$aobD*yP5j~S84$SHvk)_VwgoTxBPzqpIrC$c>2(M z4&&?UxANeiR=88O1eZhvbTx<8vF4|p_O)27vZ5p2X=y0g!e+PY&fOw;vf{nhF`2kl zn=NXrCn^YO+3Qs6dbvjH_PLL*F_C-H&;FLE8avpQb7DBklCq0)RTC;yPGI7iiuaz!bgl{FT%%oYDEp==DH?m)K1kG{xE43HPh*EcAELVh8Pl$CUm6kgnqO#t@|WD#=G2I zst>F;pM4awJT|)6C|xw9zr`MUUEk`W&>^Ion)-asl@2K`_?MBm07pT%zUEW#=RR{5 zeSRIvmT7NmlWTZ}o;RdF0NCYr$0pLvncfuEc*UH%T|Bs$LAyvCtGo@GGiW)-(7=ZD z7XX8qdXbqXe1q?RX6DehNMXKWes5=o&V&xK%VQINmP#S ztBD*e^u)Lt`RYSd$$){`_ftFuOQs>G<#d4Xrmj!VtICu`{;Wlt8p^rqGqV6QM<4j7 z+{{tcRf8ktM`beqzyYPmFsFfe`_5M;S{AxKUa!{}cZS4Hk?Ec&OsIkFp~=8p7nP1V zQh>en2k_k6FW|(93ONG}95@hHF1j4WjskxYcK}#se@diMBdSb3RH>tGBkirUJ~1mb zq)@n#)z&E5eW`V)+vS#2Gk)vD%Oy{;)nY|XZjL`UnfS0&rb)FeiS~UuhD!KPg$J^q zt?zu?v5ICI5>}|fi&j5~%Z%A7*@vtLI)0o7d)FZ8l7tG7l?6195*L-0H#cx)Gr&0 zoeqI0dOY>y6DSvx^6lUH7S^m@jSoI}AK(1uZD?t2#bb{?ipws$3=_mXbGf1kVt#Uf zYhqH2sYNYb{|8M0#SBf8CEwz5NYmkH&qoYx-R-EVu9VuYqN55mUG=b1%rnS!iVpEgd>NK;`LYGKtVwsKKkGz91@>>`}Mbxlaq}d+jj_XA4Ps%F7gU; z=P5qMpz=WyEG)a0H)l7-~Rg7`1#L&h6DTeWBCIQ;P?05i*@VP;f>c{!~OUD z0Z$5mYn#=@+%*oNfDX^ZX;G0ItfiF*7LTzBEjgF12}nyzLrqOBauTv+pBuLTTxLcF ztR`XB>Lx_$2|H@cDCFi15Wv`vi4(@*^Q}81C~d-ooj!dsQc{z#dgWT#WNhsCv667T zsi{S1#c@(=9zJwLnCq3oOfHw^6K&7U&&8yv6D4@b#LgCAb~qi#6F?p|e5laEdoXw`uhbQVb_Cl<_@kJ`Fgq0yS;c@4-PfVq=r!_I9XRm!9R31IXf zb_TbwF36WjO-+^j#)kxu$#f>)l|Ur-6~2u}w{KQf7A8%efVS2)q^75d&vr>)nEC~A z9|dcJ(6H6jHAqcMmEe8l#T6JSZafj9Z{N0Ee5M2Ag*i@UefEHC@!n39l~tg+vPzop zTer00=&>@Po%f0fCpWHIt$djPOpnX1xI)HDsGG(t0o*xrX3MdVot=$e|K>L_@_|qJ zr1e$=w^|ap%iZFu2WCubNQaAo#m>a>Fia>Ii?qvAvFGSsVV+trx^x5zo%sT-2mvq6 zpqW{jvRlHKv7_aC!$%H9NoldryeYyY{zL+7?!4LfZ1ZPGN=}kZ$3~1CF83WRwD%xk zO4CnW`NA6e%QEVAa7XMKT723|BEIBpQH%JS>B%Ti%G*I@~ zrP+P>h++QkFalU#dXHRZLNh;&PDtmUry- z(7`MK%RDEK)oB}ssn7L)RnUgr*VnU1HJ9}Js(-cTpx;-0?*FXVOg)mOpD_)M^-VIc zMoUWSEN`qO_ul|mSC`A*m7{kxFxNSgJ&k(C+q(UX92!llIjO)gd6X`OFk^&&Sy|f< zu>)RzPO+p3iMkk$7&TlLY#5;J|GJVM6*YA-))FIk*I&G#Z1ayH-rdovYpvqb!odfK zE_~?Z4waoig58F^tPFn=#_1C=Q7*cPF{8P1KSwW>z8*bMh2vG#y;wb`HKEnD-T)`8 zpMZmfLwU2$gbwaa7<6pLSX2X9bbOl4w(mcJs=5Ybq$bPMNka^YCvgd%sIIYDK0+Ij z^=3ee1vSJADd9{K16G~Cd-cA~~YharYUIaCs+g)i5c z&@5=5K%~mVIoDgxnok?KLWUR;BN*!d9ohO7sMMLz@I|^93sA}SRXL#{hJ1WC&a z5AIGF6scmSKoBc3YYvYYeuEk|8_O+*#G#F=j89V(hbIf8jCok!So^}_x0boKmY?;k z!+ROL7PA?7IT^@IN$I!!iVQh@#qIW>rlAp)bqxw>M6aQkN5-;l2}qaVp);XX)0stR zWf(>9Qpw-AVS|vBp6VyKGsKX1GjwyaGmu?dj~xe&$n@IY0kJ;L9KNPdSp(MA=5rnd zV8(ga2_f2GqT>%K%15>UZ#2`>3^AlPBtJV7t?eB+aICynbD>YRU14rv@H5?M#H5ci z`!zRbKor}Zk@iin*^r%)7WYlI4e4hZlUlEuOfdO)&a@AGujMZyls_>bGhOK`-J6wU zVR|D4N2LRcN(wfE5;@egEhT8>-@>Dl3EpS)3WX^qBMmlnG2Ed+> z0B>KqaN*ovEit=}Tb6UL2EZ(s4L$__aiAP~gT3*8IflU=8NWhiacgTUjvhH8v~4Gn zQ&N#LAlFYw7j)m$2HE00($Xz0&8VrWM)rUl6ciLn(8spyGaTxGaLpD1K$K%q7sP_v zje&(kzxs06N?v8*rBl&8&7|~KC4aapv z$542jSrFX{)$-EyqW9_M#tl!BA#xm4l$WETq8w>y>B!8=@`E-k5H+hDbYHWwvgQ4( ztE)w0Lj#5k9V$#~M+`yC!yD3i7D`=nGc~bNXF`Xna0c{QHWq9t{ISPTQ&WTD zl45-0`fo^@)(sms;n}C2m28!Cvb62;D=$ZJQ4v0P|3hrvycxIMahvpWh*k2DhabTa z@psVRfw=zq>rqfx5X6B*1A$&|-n3bAK3{U#C2%+#k`Cvcx8A{vFT9B4q$J#U%S{+Q zVz|L&6(`_5eCQw=h1r~!Umz!HJ@A5Cj_9$>{pL5B(Zoc#Yv(Qu8#YX6@svIwU9(7~ZA)<@vozi&Toxb6m_NxQIY*%BNOuW$Uujc9FY#gW5D@XE`t zproV(BS($Ix;5)?)Acu@xw#p8cJGnf=mry4^NrWvC_%h%;e71dyAL--5L zbmNZ*^XrCdZ@_K0+=fRVc~ss!TAyCOc0KO=>7AG|WwIo*`_8w%gZlb{d7u?QkZDM`8k;jzsxLqKDIgWHC`E zift(Zw3$L9QxkzU>RBC=>Nw`ts(QRJ|HPxAyJDfJ4DAoh!>RY!`!&xz{fz9eaNnQq zheKT8IrHb?-n)N?YN3s3I}ya)eCy3fO-{l&=bnQ_3l?F^<}FARHzpw=K~4fMz4($0 zX#DLTe?w+^8qPWITr69>46nVq0@vSogP&NA%&A*%yBUWL9mZpi{9D={W?{O&^Y%L! zIdUX!yZu(wH8f$#*-NnD(+%SLmi1~B(vU9(-kR!a)YsJ`zo0;&`>ie)W5No1g2TUb zME9TGqZ(ebMG0id);1K-VoH|-xuitIguk_4)uWpJy-fRSfl#0q%Fsai%4Yb4PvHEuqWW2d+(9$O*U=ZtlYTz2AMy- zW5*83ESZ*`F5W+=7CptO2lzu|g4N zN`bW4tO|I|76D!bw8_aSFbgeBW5fg{&skA)UP(sSDen2Pqen1k$WZVYOw_DL_m~Cy zzbx1%u=qn*YDZC~X*wJeP7Z|iPYA>`i;^Mp)RRx)h38(7zURI7-77mJkY7yZF#(GR z*RQ|ndQ6xw4(ryg3mn7cmK_tc1Aw%xi^$qFK&8pRqvqz4yQsC=(~qruSZO@Z8(1HfN-PgQB*P*Yu0T>OKXShZKlIwKT_^mWCbuaQ(XVvQwhl zZDfx1h4$^}XqWdtPgqP6R7^>3@QVBD^}3}-ZfdASYilz;`uJlp$u|hM=#aFqXuQzs z!WwZeGc(gsTv{SCtEgqIlGqe5WTloC6Lm#J1%?eD5q%Sy>*G}V^OGs)fNcHdd3FDC=fTgy?n5)0mF2XU`tYK5G_c&73KL z>-93pIH?`;bnK+$WK^CwA<6mGrNr0-we(9%OhZfQFK7tX!2X72x;5a?0Un#1Mg558 z&YO!L|MVyL=Rf|5hn7DCm$*TfUVbU2O`C$1D_6^|3j|t9mGhgv;{2RWr);&-?V@%k zlkw}{{#qX6;fEfEMfgF#x#u?$i0V4Ac8;ULK|PXGAD3<%O#sXPwOn>}`2G*SF9(mY z794w_drX;DNwmbngF`%C>UAXn?3D{Kq>)|X{?|9QpuD_1D0hJFAEj7A_JMYZ_q~b+ z*6A_Yk0yiW<|bt442X#@9Nqxdhpl*!j# zxN+l9TU#qlVv1pm8Z}({%+%ygiAskS?$E1*#6+2TN$bSXcY#@09B8qpc|Sa@EDQPp zO5${1#aq>MH|jY=pi@Cjf9-a=4EgJqa%6i_@jhu`bzeFL-8AxnR8WRD+Cz^^pQq2t z0YqlAkxkim(PuF^t5rtYfh_}$;@)^YbbmZP4eY|el6-90woBe~8V_U&la!PcqolYH z%}tGR#8V$DDr(@|8tjMqmKWRGTB8@l;@WlKNL|gsV4Le{Wf5o&98}2^2g@atWKL`9 zbS4yc@`mwMd-7vd^XpV@LZr_c()Z$$=B_zxUOJh~Zl4dVYUJX=0m#fqLrrbH%*<2k z&8JMB04jiMZD~?U4#j6^rNU%S_4mbA$AaQ6MY9Q=K8O{t1VYoGwHAUl3LSv~FCO?P zDYJ%tTy2{N?cKshwJ>BQndJN39zz{|nMCHJhaHoO#`TB?cQ2QIx}KFTVa z@X4wT7(6f^f?&5?usN7jf(}%q)(43@ zfg1flj7oDJEKC$SwDhR&{9gj^`+K_Zs}=Q_QksBYo|B546bl;K+<0S0C%*fidVFVL zDlVVwK)0KRzu7!MM-nuPt8sXvJg{F0MUb_{_iA<6*3Y)0tfm{SZNgveaG_E7w$(ND zIC0_xDl03+=ghd^+=a->&Ot*{r_j``=yti0k(sT4mr=wy{CFUDii-Cs-IDi#E5&r2 zSt^@INHtP0qk9-iycmi_%y>uZ+_>k>MqEAJiCgBR$cs#CbtY!P%)tp5Uueaj-fzP2 z95cohDw>$YLg46d*EOH!uIXk zV7F4)D1#%>h6KA6MTG@8Yx+WXg;riTe-8E?Jc7hFsbZsI{tP0XJ+si)KlZzlAGjxw8o7e8x9L7EvioS6)_hjLXl zR)ILpOD8+A=3p0|-q41z1vX{65x}J?k{6zP36&?RaK^0Zm^^h7mjCl1xs7`DQbc>( z)@^V)oR~Fx7N&^p#2)w+RjF5%)S9 z?VTQcvUVG0O&g07<;T&`*ow2xm;#H%j@H&Dv2UUR)XoI0x9R_0id^UA<;Boi)AtDt zt_$R0c9h;Qh0$41`4o>hQ$*L#fecPHGE!xF@-$~55B%vR#>g5inSQ{_fmw*qQf z_kH_~cO;0f`r4IvOFZARdoRk5m*brCmSE|!MKX;tCnpD&UwH|J4jW=r6!jw^%Zio) z+ge+Llud4KE^>3y;Y_5GLM1P{v!fFY_0{mYJ1}Hmz5sVUGSicURvv)Hh8lR>-Eh0Q zg@)}wS7(PTS>?Kc=J63k&@y7*x?8@Mql{}7i!{y6Otku~uNjVElW)C{tI;iRm6~XZ zIAPHHDTyYzuSW?d%B8X6$I38z%qR>SItZDW8K|zTmW%bTzVsS4ZrCi>cz1lh124V! zsw@OIIwJNTA=8-{H#!C2^y#ijl80LoJWxg!-BwE6u$j70n(xGlH$OyYTN4Hrq@k{+ z0yb$yuAp&Fl=_EKujjpUvOYxw)ks zC&XlKvrgQfO&z#)y%lVglZtgWPH=WzZV6mXX8P>5S>^ zZH~U+)FCry;m8EMySo#|8ay&0treRYXlV1|m96cVEzImhn_^;+xwG_~C9*L4AAkK9 zW}P(?!$%C0bUQgY*+@!GLUwi*ii(RQu&p+$fr@^pg$oOd(AnPZ&&*OA#4GvC9_1m- z=)Bx4B&FpbRXom~I}HO%3Wb|!l}sHDVQ$m9G1U^&5|aco3uxc^x;kWLWWwo8ii1QR zrYmod^~;*(W~K;cG7lw~j)hP}dFf~e-rC-YAH7hIdoE1Jz$}YDyinQX!EfJal=ID% zQyow%`{+W`?EK9;Zjm5P@}*PWa>vawir69iqGjhTRe}>8odz2EsTQ``62zojB59Bc zi;B_K)`~h|ZMC$tQY2H7AgV-%CRQ)aHD$TT_r7$SeB|zOA9`&TCUYyY9LZAilK)#I zzC&ZFM&0rZ&cq56aAN9`DAUo(YKS=ePXg^3beSp94`k7)k=DIi0QXm~HQ<`ZYB8`*5+;El`}D#PF9Z(y?lP7gmag^m*+Jcg#!1 zf)NRD2-Df+iOe<_?zNG;ar%I`xVS{#&$^mgTz>WSlKw~m9M@~4a8@Z)NYnq658d3< zfcpA6;dYHcqQlWgrZYph1ghZd3>K8&AvG}5_`2M`8_<{O4rCgW>3jCbMEUS)dX5ut zT}J%rOYaYtmJXEGR@w0~q@<-HIXP96g~v`3IAXpsMHi~7tE8niYRnkPz1DYrFYjr# z(Nb3WmrpF5hoj(#(5fH1FO`=amrFkmrxO+;=|^xJ zN6Dv#Op7FPyyli>xVpN;Zcm*-EJElo3ghoR;7j*T+%X8St6|$TP+r3WMm>cCr8%( zs`cZ26PybV;-Cy?_$&+EDO#W|D{&sGs$o?77!v=&eIKd>QmrSdQ zHBlmNEVUS7NFVsX;okJG1Th_ewYD@fnS;6^1x4*5s(M2VF(f)Bbcm8gB-$$lz-C_} zhwmdy59Xjm2;*WKVn|FhFhhtB97vG#Grd#o0Mq452I=D<)c55wk|Bolti_lw-}xlI zaa_8Ip@}C!OGYf9VD(K`lw4@F52+sZh8WThfNS!xOP-3DeBkhC__gBUH|XKqGu#6j z*rX(<_^KQ<_P4*)iUDXt;uySY%g`_UIqD-ECxi}>dU9(QgBW9Bon=`M9K_bBU4Js7 zR))kgc+KYjn(|dP3Lx;u#TXBcQn4WHspd5gtD!BN2eLMTC|Gs=@2RNvco&**t+Ex4 z@=H8kVrVe@`u-@SJ=rOM{A6ab*+&`Mr{;8zlSrUjgWto~oT^2;aKsRBmUWH5`}u55 zz~yLY@!<1A3}tl;iB@1(p$Ef@nB@L-8`VwyO8cIQ=9P8tCr7zL8ygp6U0;(Bc#j2l z91IF#dA+`ZBtM9mQdX#6JMNDaK{5r6dGY3_9z5`70uEJL;BqU!Eh)i^*<-qK$A#S} z8DNFWW4H|c!;&&Z1h&;;mI12v)-L?^hfm>aH!r~GiKR-V5%G1Ypz(*g1 z<~=KF&b=BRn5$0|6|Jh{aHYllP9YQBEc4){m2Uj>Q7876S>5u)*BNn)Kd1X11lS}47et*&j(hhMs$+^k94{EhgeJ;`^!4<_Z103yP70$wZD;W zn!^rkKWfMF*G-b#i~=KShX>=)dyn4gFBHjqCe8cIgCAk}{jbB_?UsrEHZe=9-8epc zb1Uu^8rj|L34bnq7|e>K7@7Y3d}g%PU$<~g(-(yd%&Uon_21ByLRRR(2U{2_n`}tv zfrOHPy>dqacI>T0X?~)@HEl?*>dfR6r(&u`!HhGRiAk~r|9RwNeCMu9kUbz3O^t1S zO+@K!(o03ZNKL_t)+)r)?IjcfN}=Dg8xx!iGioq{6Leh?~6*aCSPnoKM( zeJ|4Bgv$OK#}X!0w@aAKC(q6y)493RjQjrdGV&72;Z@rv8`3L6h4i!LOvj~PIaiYL z_jbgWBFV=NRtxRw#ndy0`NwltmrM54A}K7%!tgOg*!t-~%n|^OX9>3~tTJta1)wbf zY-VQeHRdc1M>4B!-+UPJmQH}xZo#*Hb|GBEwjpk3 zZFMvD?JC0+*U#;D0XwF{wKiI1@f*#Ig>~q|QET;!`HN;*MQ1R!$1eyYdX&|H~IJd~^}=i!+trmf4K% zE;sJ~`3uO+%fO7;BNR}d!f|5*&z?_(`|zkPgULc9la^LiO-oXVwN!#TKQSfmp9q#e z+=6jKlW@_LV|ZfC5ZM<;yNW=+k-%G+-ij+{)gn2iI8K8bhG_3uxCYN|0m>V{w+u%P zR^sc+{(#G_osA(Q^HE>hf>)kei^jS({N>48k>Ie2d+do1{GQ}Xxki`j4B-GLmcK%f ztYt#B$7ez_1T#eU!%{+;@g2@ITsgM|7Slew_E`~XS{zV4T$|a82}Skz>RDx&G%6P+ zv(3=H{UNqN-RgA+Ci6VeX~XXyyb&v&UW+$h+JM)dUk^u;9n;SoiLc$V06BSS=ngYM zOA8^6GO>b|b9o>`(ocJ(CwAw)P7GWFdCqzNk77Z|vdPUa#MNh4Ve*hY`228!1hLKR z!N8n0j33NVQj{-d=2Qc`{bgdNpoQ5S)|9qbOt}2IIk@<1XQHFM8&<0Y35iyCg!$PW z!Mfsc$)CkES&_c%Go~fkQ333OIH6nBz>)ZO`l}YEHMGp^d`wTv#Kci;vZ1rdWP{C~ zA|6iJTEhTuKhnSy-%yf$L$?V{-0gKMJ+hoO#Z5YiTjY)7d(XJ0oG^^xEj6%7ZTh57 zd_JvwCk~6%4y!EzPR)B9M$e@FC{o*o(ZbsMeJZy#o(lVe{d}(uhk;lW)1cYHl{Uw^ zv@M*LHFn%!-8`-CB@wd!FKL!kN=nJYYwI9REEce`wVXT% zVwE-Mu?#UJnm#Z`IAZs3i5yx`n@lEV;#w1&hd0X&4Qz-Zu~o_e<;j2`=Qdaan}Wd4 zPBSJuLkx*~^Izw;>+0}%58BjUCG4*^)DmkMX0suN!~()SYDy}QWGuW^wFuCf)EZ!Y z)Y-(M@BE%B!=NFC^u4bdt+C2NUWL!R;<~QEN+uTLK@KqYHT%}O($O* z5JOH5;BJ>IiqY;!Yev4#DJt29SF?=7X%$JGUHnx_JSOfhosY-u2G`U27-C4A0@&k; zK7n|o`9YaDq@oQ5u>S0X+RB;Wdydj3fyF|x)_UArAoB13J=y-rgn8akUKfYWWM zuzIawf1hlpVYT=O2{_ztcZ_CDLk#I{4IEzS=vRld+boPJZQlGLa(`Q39+rD37L!qp zyAE%xdmDT9?~xPBgfSDac+vu-vJ{<#z|h&*i8oe!DF5aR$ibOs&GMf!by=Y2ufDhf zbLP)MT3V{CdnST4szi}3DlKNo$Uya_^j&7NNj{g`#l8u4J3jenHIkB@7(Zd0Kj)T$ z8FabKW{dx|_Ppr31N#MD%Vc_SY_&BeZ8s*E*o)O_mG`@=t4rPsi^VF59_XIfY&N+% z68mZ;O*chdr%(>=BX{=%VZh{jD1(g5RURYfH-dDVU3lx$cd=^iDl|4UqOP_M?|t+h zR_<5>uh|=9PMb|;)Ku4?>{uBlO`eD$Lx%*miJMYYRgJy7_u=S~WAbLuP1(797n&NI z#0|6fU)R*s$=A)z&Dg$eCz_j@@W%4`d*p(+L#Xh^G#lTF4i62X$d$MQG z9;{uvM&8pCCr;qQ58jvet*xyM@4fdf8X6kDaO3dabMaUhhf+2*3$cVPf$DyRl|$=7 zYn|s<(CLu5+}zcIy$AQgA~ai7R~@VZr0M2V>^!g=^T*9W5_1M>CB^x+x3!~QTB$FkeHZ=RiCWJ zq9qHlY2y|+9S*Eny%twqeK}ru_9e+@eDJ_Q3>!WaD?eI^askSAag(Rbn2O`a%jE<^ z6T$SE(=c-62uc0<(FY&P!B8%?pL5ebl0cP}<<*oZ&<`A>N4(MR#}tFMa5_&C0G$L;v( zop<8yd+rhYcE;2X)>_M4w=T>jc|BJZuTKLrT`x3UMWNrlj?vhnHW^l7&d^{^Fxy}= z*(4~)10&8&_U+87>w3q@<*vxTHv$Po&j%3((O7aisv)q$!io(cX^LtJca1 zg6c`>SWc%yXiyhMj2wpC+yRQoC9V@4$LVmQy}d(%|LEZ(VnVnAbLY*L7jomLn{ejr zS-ASER|spMQEKB@&GuhSIP0vl zFm~K{=}N`soO+0BX`iyt9W9a)D_jQ#u2=KX2QdLP#hQ#M9r84+b{nRR zoq|M`5TvCD(3KUHu-dE`IJgw=y!oyKC5hc;7XWHUr|@w)Iy&UUkR-J4z(J(~(1mi% z`_O^IvYkpBY4N&xY~HXLmt1xcc5L4%eOs5S8!lnu5)hMrGMUg8tJOeO z3;|4&h@mC_$T_HR?##+Wodmqn># z;Jyq>6V%w1KR24iHVSR_m@#k6Su%LEeeVwGuS^;>2~$fafQ9LE6+ej`Q= zM_PKSyzo<|PZlQdi+K9+XJn9S@X$fR2QA0ym22S;nse#0#V8qAf`32!ILyK)ows1N z1n7J3ekegzSX6+DiV8fu{NHG3Y{00|BQSsATx?jsNtoRUn0eM2s1?35`NT9R^73=# zfS?;spZ(;cRnm_dF?u*=%$$K&URohRoR*e`vllPGD=)5)6U8twzINSLg=XxOfTbIB?gh)xP~U(= zN1_Dll4XnJYo|}UUj6l}q_(F0X=Xn6{BvZ$i?YS2Sr&n_SoqX5no9o07m;H3o~4MlT*`D<@u>I zw!%Zr{GX%e=^1G_dh{4dh50^p#uQ;ncZu_|iF39anztW`I%YjyPcLh>zp|3$ECg+)fal}x7SlohI)SR0OJ>iRGbP0R=^BdN@IJh(3@ zi((LqdM_V1UV&s`q9!>LrLTHA4r#jh1l({Fr6K)8^AdO)np<$ByfO-IB@5#dvrrR1 z5R=k0Hxmx9vXT1}3c|DRL}5k~C)3*2fi1iDqa=R-(o>RTwAc_s;z@`@nk>a56;*QO zp!e(7{bhpw9%U@d9+b%8YX%&oflX=~3KrI6j&2m1Op*4^PV7E(RB4rAXeUGB&Vus7 z?e@x{X^H_ON60%qjjCGETGx{L12eS>N!7$LH&J{S#Vr#d48PEjc(b8d)O_A>J}}eP zBlxr@i{Qpu;OmvdOuofq9=R$a7QTj|ferZ*>v|24wE$;&e^yH?8`u<}c!#h0A<`EZ zVu&HV2UxASt^sibCC7tkWjRbrhjKOZifbW#Qd`{+Lk#(!Ep(0M4K5?p0$XLT5l;e9 zXzFdII_myOfwP!vTd$FgWJnx~DT?&RYQm51%?K~0Z5;4{J+g4!6hL|8V?+QOCgiW!vEBJE16MODWNBZtWsCfAe}vgrE2 zbi_eL`52g=E5U0R>4wCgr^+RW4;-sNSyfFGG%-8rR z?xbdH?3b4eG2~=gm&R>*SsALUs$de@F*zv-DJiK6t+3WL^et5q(1~v8!2W%(CnTV# zs7UVb>9bT`eVJf@-dU$nLu&Oc3b!MZ>%fu=8PKdVMbQU7B@Jzo<_=AGjSZNZ^~2j%OmoE&+twS)}e;wviA1F^QWiqG%cyAN4H8y6K9_Ypg&Hf55B31{($ z9;~mRUvZMDY>M}&Xk+)o2!u3CzWvtQs3@<%;30!C>&#j5;uF8}*3Y&oeu&tY*aV4{ zl0bN1{{fVjm*b3?GbJF&7kdA__ry&;EP12p7&I|xO(vqsIe6$GR)4Zu%*b}kn?DaZ zVldFbiB)p-%GHvFXa4;8$jZ(N+x@|i*qh9CwY4~O=nyh8GLR%pVQ&vB8Cp#Gla>Tn zlai9*a5zz0Q-j^RcVXD@5wa>SmLLwY$bG!*bc%joS64z9#EGSLCsatIy+QgH;bwst zXr^p|8IXyW>OT+uM-p$pvf^cw6c=IL`VIL0cfOAU2M@{Fcff!F_{abK z1DP4=Shx05eEn-*#}!|>LULo#b^_*WD_+C<@4t`#yzm@Gj~e3iqzFPfV9d{&R!gZ1myWBQEg zl9b)$@rviu@Y-7|kmO8~=Cjh&%qo|@GP5&fSBK8_j-Vb-t5&YU#EBE7_NA@}q+v&o z8I9GeR^z<$&-MAc-MIC(TP2x132A)tk215eCD}Kb>HCEly=K)K9rW19bLn!Awn3)m?n>=DsJD(|e(A?h%jp+>ZSjL~} zCbhP;^?RFc}V-1UoJVdI94 zxa6`+P&#m+p?&*M`%<)-ZfH_ck~EtMf^J$qs;xq6OOpg~ZhkR7+rAU6VnQE0XfUk8 z8l&cAWGYt(a1I?hR1yu!&+B`EiKkiFIdZ>@j7&K$qOXZr&n~NP>@pvcgJ@-y%0V?a ztv!$_Wan6!6J+{OtCRYMdinhn=BEJEU;q49%$+}1uH(_o%+1S{!KJCwr(*ts`EvQd zzm_c~p4607R8>{UpQCg)2O1k2<*~H08EvBp`)79CIin7BJ$nU+s5_R=W z(gGtzKV;nj&bLY@5FZD&yF8A7F)J##jc$@aozRT`ZX{O49eK~>5qRZZ{F|k zyH~Ca(t`i;|67j6rbhhYmp>CT`5?Kyrm7}(6)A=UjftA-YS~4D04Af&zLD?sxP)8d zl729~RZ(#qtAv}>(bNXtLD@E)iHSioBwc*!|4YYMykxQTcW=Jo77QCURIc}(f58PpTQBhU zy`h`?#A8p$2=lD7X5l;E`mStbcELp#VBCapQY+I^5_M>x?%@k{-ZHDNuY*%q zS^m~2>OB`@!6QJ-ydLqM6|U|sxtic|b7^tWSRt#90-PEsJ^w<^Ja0K}2LN+izKb8i-T8G!`cj#4O4jv}V9Sa|r+J64=FMpK5tfNPcBJ12N zOc$D%Oj3}M zd-5?K_&st$7(8SMCQKNQr=NRTE<@2;AvF)DHOIbQ2Uge-IsO+zz$=3`tlw}5lAlX% zOG;{5@ZgpmGh711Vp3&l=9L#&nQ7Ru>wwg*nVFf=MXIT(kz=N`v{+hQbnht&?c=El zsTLqJy1BUt>EfQp2DuSZr+8%iSv1zSX8nnNj0xc>bz*bJ2}A@yN~%zES?Pk4<|Ln{ zq@+YfmbEvNJ~MF8Km z(|!2xVgB#$OYqY8vY3Rg=klVmk)ghYl5f8Fam?b@rY8Z}=^PHbNgTrjZyW)jAU|2I9n{?3dp`KeYT@UsnSnIp$QZ1Q2h^5_6-60MNgy_N%5Lp;-M+z(uz_cJ!j?r_Q zNd0F{bz3aOV&Z7+@Zh~ICcL@PjsxWuw04rU^56KAfFl7Ioa@G-$!;u};znAs0_1oF zFj-fTdMGPa6PeInIdI7zE-lWL{hM1`+wk$nAN%j&xUr*=J(X|JH=2Yu6yO_xeUF){Eo4y4H;!UEYO>Lp=h- z=D55D1X8sUC|DzM310GNO(rTv@XFUYnW>mPb0W$s8*un&88Xt6k)M}?d9$aZy{$zy zDKq&JI>=p1usB2QO*Oz3jNlLW_lKByXtxwgFf7CMbl`n7Xo0qFj#sv|W97as zGdBYPhHO-FN7B`;X+>V#FwBxrIq+?8h72WRBx4e;VOiiO3zkS+)qXN8%w&;A1 z-3nAUnZ)-b;$L^Pqad5Cp7=7SDG|fVLz~|BROgtvxX1LMV+>R5K)!4WH zAol+M?Oh3wTt|8S`yF%bF?-COTAh|xmSvD_u&{+3zy|~yWWa@uD+WUWsf46LNJ0z_ zxd{PNDFOs4V2;8uq+-s5!{7@WEE_PEEZNAmj+L}J_THJD`+S}L`@Mei-kX`dcV{H^ zU-fE^cX#*K|Iz<(=n!1?vO&0D;{c@6X-FiJj6+js4Y%9iM| zWo}zuP^xh6pjC`mE{02M3C$~S#KH+NnK$6047@FVfY00#f!ULZh;>|jy3bzMHl0KibMe1eYG{ut{;Z@3_y0nN=Z|Zg z^R6WM*+@7#qQZTT=2-Yo;H+*H@9j_Lx_rFkeSy7#Ui zfXnS%qXa0ajbNUU!|)tOp|TF?>WR(-zRfu7RLX$Q-r5O~W)(7p@&mW{34H9vHh9lB zCgEF8C*d!y3v-=LG{T-=J_*AohIyn?Pj5H;=qEphOeV`jsxU;lBXIc8VN3)Dm%Mx% zT)5@Jh6FJVScP+61Ah86rhi$T`}!D&AKItGA6%1TT-e}TxDh}syrWPw#^ofjMyu<7 z9+dLb&%eu*7T+y1Lf@B|1FutNqXvP~dkXXCOM0ge!Nv63(1F0*QE( zO$tFi&VvCzc+K#;6 zob!4`DFzAb6V@ zI8h81p?=u!*I$1fTzmE9ux)SvHVkw@XL}Qu;&Pb;q!LjGFwr8Jm}c*0FjWD=RHyj$ z^4SdJvMIjh^O*{!g_dXJ@aXZk(pKnlJ6q*N9g;4!CKHiJk9s(;QVU4+(us>qKbgrG zu(m_1xi*RIxsL767Jvg>aEX1-JPSK_T?uc!c{c-S5QdHn@o=-TQS{0wLWupBN~U09 zVv5bafrb^G&5ktSu23Z~`43SDC0<@(ix}w@W0_4e4ak^-Sm&fJ=?IRET~7 zg+iWNq9-QCpouZo9osj--e>kfTeFYp!kb`vVg%BO8GcN$kmqa2xa{NAXtvjpf>u=^GSUKXlinSpiW>qS*kg3@&D;miE}2-ovi0ZJt>t) z@g?j$>r{*@#@V{Bua{dTaVEteiQc{*Q&(IheWR`{4Rin4myTWw6Zv=?41=DXA^aFdYpTk{o<)-a1?(4 z02yXUL_t*N;qvWCHdcy!LM#;0?8qjo<}cx{nBI1tiN#>u`t_@xV#iJj>otcvEzwR9 z+TKS=-D{UoO9y`CdOtkE#OO2=kGRCs@3^`IAAVaA zZhu_~y2F$Y+694yn#Q6=R51qDu7y}MY9^qwW7bQyfy*crx%eOH=!BvY2C9bP_pjRp zy*>R9OB5i(#I{gSXWX~QkHrnwjm6l<2_OBRkr|-=yXsCHHdcwNQYf{Sn7nprjjtlv z@rIQP#j7qtv-Z0A@(}#rfi!&XkvM$lx>gAK2nVe~O!4{aPbT2GQ#rWjot^gUpr_5? zMHj-KeDIH9VtfKF*u04cb${T`{>aR)mCeDMZn}ZH&f)jQ)qkm#T+nb9p_?1LOyb?L zhQg=r$ivpPW%27>uO)ETf9bGqNP&5kowD5ccz!8rI5I`l-mlaTG z*b*)-A757=%x(0-WuOngtpMBBQ7!@tx}cZBM{X{_d;h}^nVh>Y)@;`^$4cW(a=aW5 z6KzumUx4Y!NiI$wJ)MTB@vNh=2=ydLw~BlTFe^?_1B9mySifx&f}vIz8ySIh8#X|w zsi`h26vVDaMr>wEY-bWu0eG!Kd~1MCLQr10(Fb3BR|M|;RRZq$Ws-9k^asAU-v?j5 zy_0|DsyK0Y{iL1gZ;Z3V|59W}nJtq(a^VbfWk#<40!s80={`M zbxVA2(6DD5(1TmHz>6;&ctOUpEIPF77LmYE@zFtF1QTk`hu#C(z_0R_WM|ln_U%(IHa2xjt5CfJvbVH{;kD@p(6=A@? z-|vHqwqDG0t~Hut4*_s2DUpgK8=70^@}{w@Z)JwERTN}JmpK4D9tZgHLmGVS?FIf@ z#HXX`@s)>|csND4r`5S7d6;9ir>~C(-D4)nsfh_bWO4NYEy?8eT3X5l`^S9>rABwCeLxqh`MF&PXS(g@jHT&g?9WH9J*; zQP@VP2f(D_U{dWWk{X#r5{ILs1V*F82Jq1l(;4Jk&%;NpWQQnLx3#r%t}2$~gfwEh zB$_r9vCiFS%ZcS+{%S7Gr1~i5mZGypVILdaX zv>;EZL~bL@#tRl7SvMX&V`iD;VV3H$j~PiT*w57KzvEDXiOrrp%ScW|-sP+%d$Nx5 zFhIDew6I7b(Wy(|xY*@*-?I}4a}U_Y^ntIL2+M(AYF1Swq)JL1VU43sL0sauo>XEI z(8itug!~3%3(Bfkk#PtFbPAjMFyFR%)(il}>DxRH0CVef16iP*Ipxa+H-L(SR;1D* zrC@ncS_S-e`3i~Rx)8>(uInno)`6@mzIP=F-NrZd}_l_v|G`ixI69gC7 zeMJmff&>~p>_e2!5g~z#Kfh5YDr$+%&hKJX$dyuuR>G4wLa?gVE0*mkwaVE*ywYi77c-?&Cw~i+E%5nV)n=LmXSWk=;;TsQY@Z>>1#FFOP=(?^Fyk%z|cJIW_R_k7_ zMdV*1RtXVP=TRZ(*Wmrvoq_d{B>ZS^4~)h_ye9IZCs-=79RhTXQ zFupdxZmx5SF0pC`2{Eth6oh%27Ku=KY@V_U9f&*Hyk@W3X$G0 z1VcW%xVyK_8^$Va+a+jq-bu=KXRXY#fmbWH3j+f^i(xDDBm$Gs41D6hgD^CzL&)#$ zaLi7DefjM@L0H>01Aq8tOpd12@vX~ZD}qO1`k98sx|l9(7&?E}u5cc%d3hXu@8T$+ zTkm3@R8vh0E>V1Zf>3oI=uG6)*bdYSo!~XMr5nkXbAqdsvNU+?f&FmzUH8L1Kll)| zbvA>K&DN?)_~^#}HxBl~hd+J`YsX?m!!;6s$sCR6D)3D$mn%Zt2NF2@%~n{Ninq{Ki+mvAqW z!(zNFrlrA@G8mGF8~#=i$XGj9DM#=u{$9i`ce7FxbvBcSFZ}EO!VPcV0c+NFLOh|X&1D2<@IvDnG3Gl{V?pk{ zN7OmpLNOK1SWJh{e&GRF*BS!@>KFMglF6B&a3!7@5_LMP^MrA)s2gD&Aj})Rq#4!{ zIg!h&idiSO{2d)fRG$2ez|y!cwe-1HU3EEJvGcMO$xDV3!{NiHVd6|2Zn|x!RmFgE zkP5DP#jY)|fpOrcetr-xx@2v`j$7OMRMiA!0+oP62PVYp$00ma&i+WGLZJxp6q(Oh z=AKUn4jh9KWr$7bzWR40?<+I8y6H7!*ag}!?8sPZ%qPJ$L^IW@DBZ!mnQVuF<@5f+ zgmZYwE3Ue1MMOKiWP)*A&96dpYk;>6zR#u$`ykZhH`Bc{b)mhZ1!7aF#%4TDso5^* zZGJED7-qv2!LJGN*-Rni^$4N0*xQng8^o#W5wh&!I$j&~xkzh%&dfmguIMar(@j^x z)^)FDfT?r%4t2WxSqaMxlOvb^qf1KrDx@u;>neit^4|{Q6W5&;DzVgy51L>)~sk{R#Zj=kEZb5~jx!p75O-Mfm=G zdmuHFW}@_F$gt<|2K~>+HyZDqXKI-k`%3|^#?Cj4()=3F>nTY+99X0vw*8->()+_X zbNuRw$Klw~!&TSR7BWOp-)ag4S5zFyW^*t+;bS0GYR>!!lyq|*Zg~&~Zi8ih^Q2dS zWlp$Gyz$=+9>qec+iCd(6rD=K@Ud}tbl*MT^J`^y2KMF13unL|(BbA=AGh>yQf11V zk7`39na)~eZhIW8V|6mA=KkoKxT)odMVym}4N<$cU(GyrG8##eq}2hhivt_3yjT{D zZ7HA(V9yeZ+vSqVoLG`1)kAY~7%5GA2zAsREwfq0o6f@tVhLVJlIm^$D9gwKjSk*i z_E;beRbCb^SzhYs=zv>qy_FXiZ`8_&OCfZm7#kad`|rOWQmK^e@TCdXZfu)*x_6bOT}?R3JG0$ki3+2nxMLC}Et*c9It6#$d8cd$N%Iz>i9?koN=2lUQ-pRe zBXQqb?(Jhm#z+nZ;~XPSE4hRdCAHiRU$*BRnr*2Ekc*GE>Li1iLWmQmf)3iKFiT1 zQOlSTN2LZXE!%663Z-IUl2{oZg!l|Rh+G<>B}rO+;xXW^V&r9M)ESMWiI_=yElI=$ zV<0)t0Aa}1lq6}@8F2L)qu$Ipnn1HN|1-rTYMg}^rDfIDtR$h9+yHi% zt*Mpj#;^_4QV*3Rsh$8vi!=qR1Ps@Kjl8V`Gb%cqw)kg}ab7ApuOvxzAY0FMIB=Lc zF0sm}vVXHSfZ6_>DO(fU+k@vFwlgEjCY2W1!Ra!9 zDU9&a*IeE<7mM?`9547KPL?99BuT3`=0pXEazMXQHfjMB7+&qXxtN<{gqN>l~k{B$I}#*iu@C+MIv~4bw>(GHY&$#0JDQnvd@gLIHS}&GczN0 zg5U3#Eh?gTbIB zN=kiDlBCrjzdyi5=R_jTC1C2TRtfOCq2tHO(MZ49`%H=L*r#$u+29-)P1tcJMk5Cf zg_@WQu1kjwNs?BHLZJ{Bmt(OQXP{jFEs0oQ`djC*ip8FvWMDOfTeQa5_xRWt_;ih5 z052#>lBAU<DNq1K5dv*F3_0_J~26N zv-8$)n`~K0l2(|Ifd&EruJ;ZP4|C?J>6kQ!F#Q*6ag)dOKxnuEz|TCpcZ>je#Pt3s zaSn`2Nl`Wn_yc}u@9dPVD@oESP;+xDOflTW<8f$j@8HY0Xqze(i=Ytr?u!SXqZI(= zM9BYOWKuqt=U!OIg^wRQ%H6lZj05}G%pe^bBuQF1Ld!FX&?9HgaJ3aHc_97@g*=MS zhnSlCV1>vnzG%AtF-=i_jn-#$)(SGfPE1ZfEIJKMp(g0;?2;`kNzw|6&wjrTVzDR( zY=LGQHy}!+Ay7mu@3#SppxF+1m zfID>b2&9q;Sif;2(~bR-3rmu;0vs5PRh^xkjBy6=AIPNBP%IXXYCio7HT7oiA58BZ z?(XVp&1AE?bidygn@^q`W@2`m-~%?MsxRK_T!@v#x;>Fei-wo<9247rme zX&J<4hBs(xYU0M@WFmpatNlLRf5(wyN8$?xFw*4IAVvpk zbhB(}Z3UIxEN2G0U?LumF~<1r zv5B$A=f%C}bE7r=YyQPB=w0m{9X!QOKA(rl$q9%?qx=d)nzSTIS`c{A7#xXoaj_Y} zi()f^modcmM`O|N%v*P!k9>aVCENdw0R1#_T?8)%9Cvl~Kwn=U=&CLaT9PEq6S#;c zoH0}23*z`jSz4n697U_t* zJ736uDi{iOx3zchtR017fk!XR%*2=+pRsjf31&&>GiZ^_6`T;XwzlwfUd_sJmC2+b z##GQ!srW-(*Z({kiw!O2K8r$Fq@&{%*<9|^ilV+Q+!p3D3sR8*8PoA3lS%%GMbez& zwFI=Ja}}u0F^Mf%zyhr;^D@cJQa$TEm|k86V9q_Wp}TR9{a| zQ#>AjU&$~&==1wGhQsYv{w&~roG4PGwK|*4nGV^QQNS>yzAUL>X(0^8OuKAC2*HUP zf)-Uvv=Lz%DosG$#gEWZ^_TR)C)8>H4QLu-obEr zyFU=P2(4;3>*ykr00twMN}M|*M@AsyZ`{g&rgq}4g*2ehxxJqY?;y;>Tdfpwxu|mO z>rq!T&h`w>Z2Qjd)qk7MGuySy`vKUsW2;5FR5;C&9~^_=I-7hK+ox3!*c z?rRd)b;R-Jr*mKBapg_n+*WLgRn5d4^F^K#C(9(!bUF>Wd=67LKg^E#eE%EX@$m4E z?^_m5wrnyj19E33o4bxu<7QRWuJ-$Vy~vSq({&$zLD3NI!o|+caPD^=BOa4h?M%AJ zN^IhO)%x0b?aKYM0$g*KQub!{Bo)`JJZA1^cE7v3o#xl|wC2FB_D+=mC5}Guf(sV# zJmq((HW@pgg;KmH_54pfQ#JKFn=aV=o0ndAVOd4c6;VnVSl6fK&EzEIq&6c!!ole{}#mnO#t)nn6N6x{-0u>r`Qz^vuo^QBJ|U$>U%Dm zO(#}N>|bfULIuzb0v+s~*Rh`is6Mks?PWJ^XRqHv6b0iG{p@={A&Q?hhRzyAXUAx) zfv|caE_GoA3I1t4GhvOTINrhe9^SQW!_nK6m{YeQegiMaNX2jbH1HU<8Dbwbd(jZ* zv0;FPl=*RHs$NPfjxo>2A&!Xm-a|ZxeU9UNYCeNv;CvzW8|G+nU0*zhb!~AC9JE%O z@Nf93n1^6JXi6MyMtFN!2Vq4I$8@AvZA`3l6z6#+4v6bN$F;$6yyzpv0+mwb_=-)u z12G$r%yJGm`jWS|5YQYO&@wlebu;_ab*!`Ba%_K^?M<+IMA`2t_IsLb6Kor2jA@Jw h+jusc%CDB*{{MaA**>T#H^Kk_002ovPDHLkV1nIZRXhLy literal 0 HcmV?d00001 diff --git a/data/images/phone/hg_panel_discovery.png b/data/images/phone/hg_panel_discovery.png new file mode 100644 index 0000000000000000000000000000000000000000..1f10551bc093d25b5d3720bfdf315921fda498c6 GIT binary patch literal 35917 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 literal 0 HcmV?d00001 diff --git a/data/images/phone/hg_panel_discovery_select.png b/data/images/phone/hg_panel_discovery_select.png new file mode 100644 index 0000000000000000000000000000000000000000..73860a6dbd130d31b5ab9a94c37976626ba54dee GIT binary patch literal 61860 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 z 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 F{|lBz5hVZs literal 0 HcmV?d00001 diff --git a/data/images/phone/hg_server_mqtt_config.png b/data/images/phone/hg_server_mqtt_config.png new file mode 100644 index 0000000000000000000000000000000000000000..e53052a2b2186e0501655613688608590d9320c8 GIT binary patch literal 28612 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. + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2019-01-10 v1.0: initial release. + * - 2023-12-08 v1,1: added BLE support; targeting ESP32 by default. + * + */ + +#include + +#include +Servo myservo; +int pos = 0; + +#include "configuration.h" + +using namespace IO; +using namespace Service; + +HomeGenie* homeGenie; + +const int frequency = 50; // Hz // Standard is 50(hz) servo +const int servoPin = 15; +int minUs = 500; +int maxUs = 2500; + +int minWrite = 500; // 0 +int maxWrite = 2500; // 180 +int inc = 3; +unsigned long lastStepTs = millis(); + +void setup() { + + homeGenie = HomeGenie::getInstance(); + + auto miniModule = homeGenie->getDefaultModule(); + + homeGenie->begin(); + + myservo.setPeriodHertz(frequency); + myservo.attach(servoPin, minUs, maxUs); + + //myservo.attach(servoPin); + + //myservo.detach(); +} + +void loop() +{ + homeGenie->loop(); + + if (millis() - lastStepTs > 15) { + lastStepTs = millis(); + if (pos <= maxWrite) { + pos += inc; + Serial.println(pos); + myservo.write(pos); + } else { pos = minWrite; + delay(5000); + } + } + +} diff --git a/examples/rf-transceiver/README.md b/examples/rf-transceiver/README.md new file mode 100644 index 0000000..e69de29 diff --git a/examples/rf-transceiver/api/RCSwitchHandler.cpp b/examples/rf-transceiver/api/RCSwitchHandler.cpp new file mode 100644 index 0000000..f5a58e4 --- /dev/null +++ b/examples/rf-transceiver/api/RCSwitchHandler.cpp @@ -0,0 +1,110 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2020-01-18 Initial release + * + */ + +#include "RCSwitchHandler.h" + +namespace Service { namespace API { + + RCSwitchHandler::RCSwitchHandler(RFTransmitter* transmitter) { + this->transmitter = transmitter; + + auto domain = IO::IOEventDomains::HomeAutomation_RCS; + // HomeGenie Mini module + auto rfModule = new Module(); + rfModule->domain = domain; + rfModule->address = CONFIG_RCSwitchRF_MODULE_ADDRESS; + rfModule->type = "Sensor"; + rfModule->name = "RF"; //TODO: CONFIG_RCSwitchRF_MODULE_NAME; + // add properties + auto propRawData = new ModuleParameter(IOEventPaths::Receiver_RawData); + rfModule->properties.add(propRawData); + + moduleList.add(rfModule); + + } + + + void RCSwitchHandler::init() { + } + + + + + + bool RCSwitchHandler::handleRequest(APIRequest *command, WebServer &server) { + if (command->Domain == (IOEventDomains::HomeAutomation_RCS) + && command->Address == CONFIG_RCSwitchRF_MODULE_ADDRESS + && command->Command == "Control.SendRaw") { + + // parse long data from options string + long data = atol(command->OptionsString.c_str()); + // Disable RFTransmitter callbacks during transmission to prevent echo + noInterrupts(); + transmitter->sendCommand(data, 24, 1, 0); + interrupts(); + command->Response = R"({ "ResponseText": "OK" })"; + + return true; + } + return false; + } + + bool RCSwitchHandler::canHandleDomain(String* domain) { + return domain->equals(IO::IOEventDomains::HomeAutomation_RCS); + } + + bool RCSwitchHandler::handleEvent(IIOEventSender *sender, + const char* domain, const char* address, + const unsigned char *eventPath, void *eventData, IOEventDataType dataType) { + + String event = String((char*)eventPath); + /* + * RCS RF Receiver "Sensor.RawData" event + */ + if (String(address) == CONFIG_RCSwitchRF_MODULE_ADDRESS && event == (IOEventPaths::Receiver_RawData) /*&& ioManager.getRCSReceiver().isEnabled()*/) { + // TODO: ... + return true; + } + + return false; + } + + Module* RCSwitchHandler::getModule(const char* domain, const char* address) { + for (int i = 0; i < moduleList.size(); i++) { + Module* module = moduleList.get(i); + if (module->domain.equals(domain) && module->address.equals(address)) + return module; + } + return nullptr; + } + LinkedList* RCSwitchHandler::getModuleList() { + return &moduleList; + } + +}} diff --git a/examples/rf-transceiver/api/RCSwitchHandler.h b/examples/rf-transceiver/api/RCSwitchHandler.h new file mode 100644 index 0000000..8973ee7 --- /dev/null +++ b/examples/rf-transceiver/api/RCSwitchHandler.h @@ -0,0 +1,61 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2020-01-18 Initial release + * + */ + +#ifndef HOMEGENIE_MINI_RCSWITCHHANDLER_H +#define HOMEGENIE_MINI_RCSWITCHHANDLER_H + +#include + +#include "../configuration.h" +#include "../io/RFTransmitter.h" + +namespace Service { namespace API { + + using namespace IO::RCS; + + class RCSwitchHandler : public APIHandler { + private: + LinkedList moduleList; + IO::RCS::RFTransmitter* transmitter; + public: + RCSwitchHandler(IO::RCS::RFTransmitter* transmitter); + void init() override; + bool canHandleDomain(String* domain) override; + bool handleRequest(APIRequest *request, WebServer &server) override; + bool handleEvent(IIOEventSender *sender, + const char* domain, const char* address, + const unsigned char *eventPath, void *eventData, IOEventDataType dataType) override; + + Module* getModule(const char* domain, const char* address) override; + LinkedList* getModuleList() override; + }; + +}} + +#endif //HOMEGENIE_MINI_RCSWITCHHANDLER_H diff --git a/examples/rf-transceiver/configuration.h b/examples/rf-transceiver/configuration.h new file mode 100644 index 0000000..c8e4319 --- /dev/null +++ b/examples/rf-transceiver/configuration.h @@ -0,0 +1,10 @@ +#define CONFIG_RCSwitchReceiverPin 5 +#define CONFIG_RCSwitchTransmitterPin 4 +#define CONFIG_RCSwitchRF_MODULE_ADDRESS "RF" + +#ifdef MINI_ESP32 + +#define CONFIG_RCSwitchReceiverPin 22 +#define CONFIG_RCSwitchTransmitterPin 21 + +#endif diff --git a/examples/rf-transceiver/io/RFReceiverConfig.cpp b/examples/rf-transceiver/io/RFReceiverConfig.cpp new file mode 100644 index 0000000..f67f0cc --- /dev/null +++ b/examples/rf-transceiver/io/RFReceiverConfig.cpp @@ -0,0 +1,48 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2019-01-10 Initial release + * + */ + +#include "RFReceiverConfig.h" + +namespace IO { namespace RCS { + + RFReceiverConfig::RFReceiverConfig() + { + pin = interrupt = CONFIG_RCSwitchReceiverPin; // 5 + } + RFReceiverConfig::RFReceiverConfig(uint8_t pin) : RFReceiverConfig() + { + this->pin = pin; + } + RFReceiverConfig::RFReceiverConfig( + uint8_t interrupt, uint8_t pin + ) : RFReceiverConfig(pin) + { + this->interrupt = interrupt; + } +}} // ns diff --git a/examples/rf-transceiver/io/RFReceiverConfig.h b/examples/rf-transceiver/io/RFReceiverConfig.h new file mode 100644 index 0000000..15c736a --- /dev/null +++ b/examples/rf-transceiver/io/RFReceiverConfig.h @@ -0,0 +1,55 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2019-01-10 Initial release + * + */ + +#ifndef HOMEGENIE_MINI_RCS_RF_RECEIVER_CONFIG_H_ +#define HOMEGENIE_MINI_RCS_RF_RECEIVER_CONFIG_H_ + +#include + +#include "../configuration.h" + +namespace IO { namespace RCS { + /** + * Decodes RCS RF messages + */ + class RFReceiverConfig + { + public: + RFReceiverConfig(); + RFReceiverConfig(uint8_t pin); + RFReceiverConfig(uint8_t interrupt, uint8_t pin); + uint8_t getPin(); + uint8_t getInterrupt(); + private: + uint8_t interrupt; + uint8_t pin; + }; +}} // ns + +#endif // HOMEGENIE_MINI_RCS_RF_RECEIVER_CONFIG_H_ diff --git a/examples/rf-transceiver/io/RFTransmitter.cpp b/examples/rf-transceiver/io/RFTransmitter.cpp new file mode 100644 index 0000000..28afe54 --- /dev/null +++ b/examples/rf-transceiver/io/RFTransmitter.cpp @@ -0,0 +1,106 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2020-01-18 Initial release + * + */ + +/* + Simple example for receiving + + https://github.com/sui77/rc-switch/ +*/ + +#include "RFTransmitter.h" + +namespace IO { namespace RCS { + + RCSwitch RF = RCSwitch(); + + /* + // Projector Screen commands + const long lowerMhz = 10045956; + const long raiseMhz = 10045953; + const long stopMhz = 10045954; + */ + + const byte protocol = 1; // <--- See RCSwitch documentation + + RFTransmitter::RFTransmitter() { + } + + RFTransmitter::RFTransmitter(RFTransmitterConfig *configuration) : RFTransmitter() { + this->configuration = configuration; + } + + void RFTransmitter::begin() { + Logger::info("| - IO::RCS::RFTransmitter (PIN=%d)", configuration->getPin()); + RF.enableTransmit(configuration->getPin()); // Set RF transmit pin + RF.setProtocol(protocol); // Set RF protocal + Logger::info("| ✔ IO::RCS::RFTransmitter"); + } + + /* + // TODO: code for RFReceiver.cpp + + RCSwitch RF = RCSwitch(); + + // .... + + void RFReceiver::begin() { + if (!Config::RCSwitchRFTransmitterEnabled) return; + Logger::info("| - IO::RCSwitch::RFReceiver (PIN=%d, INTERRUPT=%d)", configuration->getPin(), configuration->getInterrupt()); + RF.enableReceive(D1); // Receiver on pin D1 + Logger::info("| ✔ IO::RCSwitch::RFReceiver"); + } + + void RFReceiver::receive() { + // TODO: ... + if (RF.available()) { + + Serial.print("Received "); + Serial.print(RF.getReceivedValue()); + Serial.print(" / "); + Serial.print(RF.getReceivedBitlength()); + Serial.print("bit "); + Serial.print("Protocol: "); + Serial.println(RF.getReceivedProtocol()); + + RF.resetAvailable(); + } + } + + // .... + + */ + + void RFTransmitter::sendCommand(long command, unsigned short bitLength, unsigned short repeat, unsigned short repeat_delay) { + for (int i = 0; i < repeat; i++) { + RF.send(command, bitLength); + delay(repeat_delay); + } + } + +}} diff --git a/examples/rf-transceiver/io/RFTransmitter.h b/examples/rf-transceiver/io/RFTransmitter.h new file mode 100644 index 0000000..2e5d2ca --- /dev/null +++ b/examples/rf-transceiver/io/RFTransmitter.h @@ -0,0 +1,56 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2020-01-18 Initial release + * + */ + +#ifndef HOMEGENIE_MINI_RCS_RFTRANSMITTER_H +#define HOMEGENIE_MINI_RCS_RFTRANSMITTER_H + +#include +#include + +#include "../configuration.h" +#include "RFTransmitterConfig.h" + +namespace IO { namespace RCS { + + class RFTransmitter { + public: + RFTransmitter(); + RFTransmitter(RFTransmitterConfig *); + + void begin(); + void sendCommand(long command, unsigned short bitLength, unsigned short repeat, unsigned short delay); + + private: + RFTransmitterConfig *configuration; + // TODO: declare other private members + }; + +}} + +#endif //HOMEGENIE_MINI_RCS_RFTRANSMITTER_H diff --git a/src/service/defs.h b/examples/rf-transceiver/io/RFTransmitterConfig.cpp similarity index 65% rename from src/service/defs.h rename to examples/rf-transceiver/io/RFTransmitterConfig.cpp index ea76a98..aa41595 100644 --- a/src/service/defs.h +++ b/examples/rf-transceiver/io/RFTransmitterConfig.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -23,14 +23,20 @@ * * * Releases: - * - 2019-02-19 Initial release + * - 2019-01-10 Initial release * */ -#ifndef HOMEGENIE_MINI_DEFS_H -#define HOMEGENIE_MINI_DEFS_H +#include "RFTransmitterConfig.h" -#define HOMEGENIE_BUILTIN_MODULE_ADDRESS "mini" -#define HOMEGENIE_X10RF_MODULE_ADDRESS "RF" - -#endif //HOMEGENIE_MINI_DEFS_H +namespace IO { namespace RCS { + RFTransmitterConfig::RFTransmitterConfig() + { + pin = CONFIG_RCSwitchTransmitterPin; + } + RFTransmitterConfig::RFTransmitterConfig(uint8_t pin) : RFTransmitterConfig() + { + this->pin = pin; + } + uint8_t RFTransmitterConfig::getPin() { return pin; }; +}} // ns diff --git a/examples/rf-transceiver/io/RFTransmitterConfig.h b/examples/rf-transceiver/io/RFTransmitterConfig.h new file mode 100644 index 0000000..c54ba3b --- /dev/null +++ b/examples/rf-transceiver/io/RFTransmitterConfig.h @@ -0,0 +1,52 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2020-01-18 Initial release + * + */ + +#ifndef HOMEGENIE_MINI_RCS_RFTRANSMITTERCONFIG_H +#define HOMEGENIE_MINI_RCS_RFTRANSMITTERCONFIG_H + +#include + +#include "../configuration.h" + +namespace IO { namespace RCS { + /** + * Encodes RCS RF messages + */ + class RFTransmitterConfig + { + public: + RFTransmitterConfig(); + RFTransmitterConfig(uint8_t pin); + uint8_t getPin(); + private: + uint8_t pin; + }; +}} // ns + +#endif //HOMEGENIE_MINI_RCS_RFTRANSMITTERCONFIG_H diff --git a/examples/rf-transceiver/rf-transceiver.cpp b/examples/rf-transceiver/rf-transceiver.cpp new file mode 100644 index 0000000..ac22015 --- /dev/null +++ b/examples/rf-transceiver/rf-transceiver.cpp @@ -0,0 +1,64 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2019-01-10 v1.0: initial release. + * - 2023-12-08 v1,1: added BLE support; targeting ESP32 by default. + * + */ + +#include + +#include "configuration.h" +#include "io/RFTransmitter.h" +#include "api/RCSwitchHandler.h" + +using namespace Service; + +HomeGenie* homeGenie; + +void setup() { + + homeGenie = HomeGenie::getInstance(); + auto miniModule = homeGenie->getDefaultModule(); + + + // RCSwitch RF Transmitter + auto rcsTransmitterConfig = new RCS::RFTransmitterConfig(CONFIG_RCSwitchTransmitterPin); + auto rcsTransmitter = new RCS::RFTransmitter(rcsTransmitterConfig); + homeGenie->addAPIHandler(new RCSwitchHandler(rcsTransmitter)); + + // TODO: homeGenie->addIOHandler(new RCS::RFReceiver()); + // TODO: auto propRawData = new ModuleParameter(IOEventPaths::Receiver_RawData); + // TODO: miniModule->properties.add(propRawData); + + + homeGenie->begin(); + +} + +void loop() +{ + homeGenie->loop(); +} diff --git a/examples/smart-sensor/README.md b/examples/smart-sensor/README.md new file mode 100644 index 0000000..e69de29 diff --git a/examples/smart-sensor/configuration.h b/examples/smart-sensor/configuration.h new file mode 100644 index 0000000..b9c4d6d --- /dev/null +++ b/examples/smart-sensor/configuration.h @@ -0,0 +1,16 @@ +#ifdef ESP8266 + + #define CONFIG_DS18B20_DataPin 0 + #define CONFIG_LightSensorPin 17 + +#elif MINI_ESP32 + + #define CONFIG_DS18B20_DataPin 17 + #define CONFIG_LightSensorPin 36 + +#else + + #define CONFIG_DS18B20_DataPin 0 + #define CONFIG_LightSensorPin 34 + +#endif diff --git a/src/io/env/DS18B20.cpp b/examples/smart-sensor/io/DS18B20.cpp similarity index 86% rename from src/io/env/DS18B20.cpp rename to examples/smart-sensor/io/DS18B20.cpp index ebf318b..4f80bfd 100644 --- a/src/io/env/DS18B20.cpp +++ b/examples/smart-sensor/io/DS18B20.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -32,8 +32,8 @@ namespace IO { namespace Env { void DS18B20::begin() { - ds = new OneWire(pinNumber); - Logger::info("| ✔ %s", DS18B20_NS_PREFIX); + ds = new OneWire(inputPin); + Logger::info("| ✔ %s (PIN=%d)", DS18B20_NS_PREFIX, inputPin); } void DS18B20::loop() { @@ -44,7 +44,7 @@ namespace IO { namespace Env { if (currentTemperature != temperature) { currentTemperature = temperature; Logger::info("@%s [%s %0.2f]", DS18B20_NS_PREFIX, (IOEventPaths::Sensor_Temperature), currentTemperature); - sendEvent((const uint8_t*)(IOEventPaths::Sensor_Temperature), (float_t *)¤tTemperature, SensorTemperature); + sendEvent(domain.c_str(), address.c_str(), (const uint8_t*)(IOEventPaths::Sensor_Temperature), (float_t *)¤tTemperature, SensorTemperature); } Logger::verbose(" > %s::loop() << END", DS18B20_NS_PREFIX); @@ -65,12 +65,12 @@ namespace IO { namespace Env { } if (OneWire::crc8(addr, 7) != addr[7]) { - Serial.println("CRC is not valid!"); + // Serial.println("CRC is not valid!"); return DS18B20_READ_ERROR; } if (addr[0] != 0x10 && addr[0] != 0x28) { - Serial.print("Device is not recognized"); + // Serial.print("Device is not recognized"); return DS18B20_READ_ERROR; } @@ -101,7 +101,7 @@ namespace IO { namespace Env { } void DS18B20::setInputPin(const uint8_t pinNumber) { - this->pinNumber = pinNumber; + this->inputPin = pinNumber; } void DS18B20::setSamplingRate(const uint32_t samplingRate) { diff --git a/src/io/env/DS18B20.h b/examples/smart-sensor/io/DS18B20.h similarity index 75% rename from src/io/env/DS18B20.h rename to examples/smart-sensor/io/DS18B20.h index 8e3da12..736bc01 100644 --- a/src/io/env/DS18B20.h +++ b/examples/smart-sensor/io/DS18B20.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,44 +30,41 @@ #ifndef HOMEGENIE_MINI_DS18B20_H #define HOMEGENIE_MINI_DS18B20_H -#include +#include #include -#include -#include -#include -#include -#include -#include +#include "../configuration.h" #define DS18B20_NS_PREFIX "IO::Env::DS18B10" #define DS18B20_SAMPLING_RATE 60000L #define DS18B20_READ_ERROR -1000 // TODO: maybe put this as a configurable parameter through API -#define DS18B20_MEASURE_OFFSET (float_t)-7.0 +#define DS18B20_MEASURE_OFFSET (float_t)-2.00 namespace IO { namespace Env { + using namespace Service; + class DS18B20 : Task, public IIOEventSender { public: DS18B20() { setLoopInterval(DS18B20_SAMPLING_RATE); - // IEventSender members - domain = (const uint8_t*)IOEventDomains::HomeAutomation_HomeGenie; - address = (const uint8_t*)HOMEGENIE_BUILTIN_MODULE_ADDRESS; } - void begin(); - void loop(); + void begin() override; + void loop() override; void setInputPin(uint8_t); void setSamplingRate(uint32_t); float_t getTemperature(); private: + String domain = IOEventDomains::HomeAutomation_HomeGenie; + String address = CONFIG_BUILTIN_MODULE_ADDRESS; // Default I/O pin number is D3 (0) - uint8_t pinNumber = D3; + uint8_t inputPin = CONFIG_DS18B20_DataPin; // Temperature chip I/O OneWire *ds; // Current temperature float_t currentTemperature; + }; }} diff --git a/src/io/env/LightSensor.cpp b/examples/smart-sensor/io/LightSensor.cpp similarity index 76% rename from src/io/env/LightSensor.cpp rename to examples/smart-sensor/io/LightSensor.cpp index f518eed..052c949 100644 --- a/src/io/env/LightSensor.cpp +++ b/examples/smart-sensor/io/LightSensor.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -32,7 +32,7 @@ namespace IO { namespace Env { void LightSensor::begin() { - Logger::info("| ✔ %s", LIGHTSENSOR_NS_PREFIX); + Logger::info("| ✔ %s (PIN=%d)", LIGHTSENSOR_NS_PREFIX, inputPin); } void LightSensor::loop() { @@ -41,7 +41,7 @@ namespace IO { namespace Env { if (lightLevel != currentLevel) { currentLevel = lightLevel; Logger::info("@%s [%s %d]", LIGHTSENSOR_NS_PREFIX, (IOEventPaths::Sensor_Luminance), currentLevel); - sendEvent((const uint8_t*)(IOEventPaths::Sensor_Luminance), (uint16_t *)¤tLevel, SensorLight); + sendEvent(domain.c_str(), address.c_str(), (const uint8_t*)(IOEventPaths::Sensor_Luminance), (uint16_t *)¤tLevel, SensorLight); } } @@ -50,7 +50,12 @@ namespace IO { namespace Env { } uint16_t LightSensor::getLightLevel() { - // It returns values between 0-1024 +#ifdef ESP8266 + // It returns values between 0-1023 return (uint16_t)analogRead(inputPin); +#else // ESP32 + // It returns values between 0-4095 + return (uint16_t)analogRead(inputPin) / 4; +#endif } }} diff --git a/src/io/env/LightSensor.h b/examples/smart-sensor/io/LightSensor.h similarity index 74% rename from src/io/env/LightSensor.h rename to examples/smart-sensor/io/LightSensor.h index 69b9724..c9de39d 100644 --- a/src/io/env/LightSensor.h +++ b/examples/smart-sensor/io/LightSensor.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,32 +30,30 @@ #ifndef HOMEGENIE_MINI_LIGHTSENSOR_H #define HOMEGENIE_MINI_LIGHTSENSOR_H -#include -#include -#include -#include -#include -#include +#include + +#include "../configuration.h" #define LIGHTSENSOR_NS_PREFIX "IO::Env::LightSensor" #define LIGHTSENSOR_SAMPLING_RATE 5000L namespace IO { namespace Env { + using namespace Service; + class LightSensor : Task, public IIOEventSender { public: LightSensor() { setLoopInterval(LIGHTSENSOR_SAMPLING_RATE); - // IEventSender members - domain = (const uint8_t*)IOEventDomains::HomeAutomation_HomeGenie; - address = (const uint8_t*)HOMEGENIE_BUILTIN_MODULE_ADDRESS; } - void begin(); - void loop(); + void begin() override; + void loop() override; void setInputPin(uint8_t number); uint16_t getLightLevel(); private: - uint8_t inputPin = A0; // Analogic input pin A0 (0) + String domain = IOEventDomains::HomeAutomation_HomeGenie; + String address = CONFIG_BUILTIN_MODULE_ADDRESS; + uint8_t inputPin = CONFIG_LightSensorPin; // Analogic input pin A0 (0) uint16_t currentLevel = 0; }; diff --git a/examples/smart-sensor/smart-sensor.cpp b/examples/smart-sensor/smart-sensor.cpp new file mode 100644 index 0000000..142a0de --- /dev/null +++ b/examples/smart-sensor/smart-sensor.cpp @@ -0,0 +1,67 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2019-01-10 v1.0: initial release. + * - 2023-12-08 v1,1: added BLE support; targeting ESP32 by default. + * + */ + + +#include + +#include "io/DS18B20.h" +#include "io/LightSensor.h" + +using namespace IO::Env; +using namespace Service; + +HomeGenie* homeGenie; + +void setup() { + + homeGenie = HomeGenie::getInstance(); + + auto miniModule = homeGenie->getDefaultModule(); + + // Temperature sensor + homeGenie->addIOHandler(new DS18B20()); + auto temperature = new ModuleParameter(IOEventPaths::Sensor_Temperature); + miniModule->properties.add(temperature); + + // Light sensor + homeGenie->addIOHandler(new LightSensor()); + auto luminance = new ModuleParameter(IOEventPaths::Sensor_Luminance); + miniModule->properties.add(luminance); + + homeGenie->begin(); + +} + +void loop() +{ + + homeGenie->loop(); + +} diff --git a/examples/x10-transceiver/README.md b/examples/x10-transceiver/README.md new file mode 100644 index 0000000..e69de29 diff --git a/examples/x10-transceiver/api/X10Handler.cpp b/examples/x10-transceiver/api/X10Handler.cpp new file mode 100644 index 0000000..47a8c93 --- /dev/null +++ b/examples/x10-transceiver/api/X10Handler.cpp @@ -0,0 +1,273 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2019-01-28 Initial release + * + */ +#include "config.h" + +#include "X10Handler.h" + +namespace Service { namespace API { + + enum ModuleType { + Switch = 0, + Light, + Dimmer, + MotionDetector, + DoorWindow + }; + + String DeviceTypes[] = { + "Dimmer", + "Light", + "Switch", + "Sensor", // Generic sensor + "DoorWindow" + }; + + X10Handler::X10Handler(RFTransmitter* transmitter) { + this->transmitter = transmitter; + + // X10 Home Automation modules + auto domain = IO::IOEventDomains::HomeAutomation_X10; + // RF module + rfModule = new Module(); + rfModule->domain = domain; + rfModule->address = CONFIG_X10RF_MODULE_ADDRESS; + rfModule->type = "Sensor"; + rfModule->name = "RF"; //TODO: CONFIG_X10RF_MODULE_NAME; + // add properties + receiverRawData = new ModuleParameter(IOEventPaths::Receiver_RawData); + rfModule->properties.add(receiverRawData); + receiverCommand = new ModuleParameter(IOEventPaths::Receiver_Command); + rfModule->properties.add(receiverCommand); + + moduleList.add(rfModule); +/* + for (int h = HOUSE_MIN; h <= HOUSE_MAX; h++) { + for (int m = 0; m < UNIT_MAX; m++) { + auto address = String((char)h)+String(m+UNIT_MIN); + address.toUpperCase(); + auto module = new Module(); + module->domain = IOEventDomains::HomeAutomation_X10; + module->address = address; + moduleList.add(module); + } + }*/ + + } + + void X10Handler::init() { + } + + + bool X10Handler::handleRequest(APIRequest *command, WebServer &server) { + + if (command->Domain == (IOEventDomains::HomeAutomation_X10) + && command->Address == CONFIG_X10RF_MODULE_ADDRESS + && command->Command == "Control.SendRaw") { + + uint8_t data[command->OptionsString.length() / 2]; + Utility::getBytes(command->OptionsString, data); + // Disable RFTransmitter callbacks during transmission to prevent echo + noInterrupts(); + transmitter->sendCommand(data, sizeof(data)); + interrupts(); + command->Response = R"({ "ResponseText": "OK" })"; + + return true; + } else if (command->Domain == (IOEventDomains::HomeAutomation_X10)) { + uint8_t data[5]; + auto hu = command->Address; hu.toLowerCase(); + int h = (int)hu.charAt(0) - (int)HOUSE_MIN; // house code 0..15 + int u = hu.substring(1).toInt() - UNIT_MIN; // unit code 0..15 + auto moduleStatus = &moduleList[h][u]; + + auto x10Message = X10Message(); + x10Message.houseCode = HouseCodeLut[h]; + x10Message.unitCode = UnitCodeLut[u]; + + uint8_t sendRepeat = 0; // fallback to default repeat (3) + bool ignoreCommand = false; + + //auto currentTime = NetManager::getTimeClient().getFormattedDate(); + + Module* module; + for (int m = 0; m < moduleList.size(); m++) { + module = moduleList[m]; + if (module->domain.equals(command->Domain) && module->address.equals(command->Address)) { + break; + } + module = nullptr; + } + + + if (module) { + QueuedMessage m = QueuedMessage(command->Domain, command->Address, (IOEventPaths::Status_Level), ""); + auto levelProperty = module->getProperty(IOEventPaths::Status_Level); + + if (command->Command == "Control.On") { + x10Message.command = X10::Command::CMD_ON; + levelProperty->setValue("1"); + } else if (command->Command == "Control.Off") { + x10Message.command = X10::Command::CMD_OFF; + levelProperty->setValue("0"); + } else if (command->Command == "Control.Level") { + float level = command->getOption(0).toFloat() / 100.0f; + float prevLevel = levelProperty->value.toFloat(); + sendRepeat = abs((level - prevLevel) / X10_DIM_BRIGHT_STEP); + if (level > prevLevel) { + x10Message.command = X10::Command::CMD_BRIGHT; + levelProperty->value = String(prevLevel + (sendRepeat * X10_DIM_BRIGHT_STEP)); + if (levelProperty->value.toFloat() > 1) levelProperty->value = "1"; + } else if (level < prevLevel) { + x10Message.command = X10::Command::CMD_DIM; + levelProperty->value = String(prevLevel - (sendRepeat * X10_DIM_BRIGHT_STEP)); + if (levelProperty->value.toFloat() < 0) levelProperty->value = "0"; + } + if (sendRepeat == 0) { + ignoreCommand = true; + } else { + sendRepeat += 2; // improve initial burst detection + } + } else if (command->Command == "Control.Toggle") { + if (levelProperty->value.toFloat() > 0) { + x10Message.command = X10::Command::CMD_OFF; + levelProperty->setValue("0"); + } else { + x10Message.command = X10::Command::CMD_ON; + levelProperty->setValue("1"); + } + } else return false; + + m.value = levelProperty->value; + HomeGenie::getInstance()->getEventRouter().signalEvent(m); + } + + if (!ignoreCommand) { + X10::X10Message::encodeCommand(&x10Message, data); + noInterrupts(); + transmitter->sendCommand(&data[1], sizeof(data)-1, sendRepeat); + interrupts(); + } + command->Response = R"({ "ResponseText": "OK" })"; + + return true; + } + + return false; + } + + bool X10Handler::canHandleDomain(String* domain) { + return domain->equals(IO::IOEventDomains::HomeAutomation_X10); + } + + bool X10Handler::handleEvent(IIOEventSender *sender, + const char* domain, const char* address, + const unsigned char *eventPath, void *eventData, IOEventDataType dataType) { + auto module = getModule(domain, address); + if (module) { + String event = String((char*)eventPath); + /* + * X10 RF Receiver "Sensor.RawData" event + */ + if (String(address) == CONFIG_X10RF_MODULE_ADDRESS && event == (IOEventPaths::Receiver_RawData) /*&& ioManager.getX10Receiver().isEnabled()*/) { + // decode event data (X10 RF packet) + auto data = ((uint8_t *) eventData); + /// \param type Type of message (eg. 0x20 = standard, 0x29 = security, ...) + /// \param b0 Byte 1 + /// \param b1 Byte 2 + /// \param b2 Byte 3 + /// \param b3 Byte 4 + uint8_t type = data[0]; + uint8_t b0 = data[1]; + uint8_t b1 = data[2]; + uint8_t b2 = data[3]; + uint8_t b3 = data[4]; + + String rawDataString = Utility::byteToHex(type) + "-" + Utility::byteToHex((b0)) + "-" + Utility::byteToHex((b1)) + "-" + Utility::byteToHex(b2) + "-" + Utility::byteToHex(b3) + ((type == 0x29) ? "-00-00" : ""); + rawDataString.toUpperCase(); + Logger::info(":%s [X10::RFReceiver] >> [%s]", HOMEGENIEMINI_NS_PREFIX, rawDataString.c_str()); + + // Decode RF message data to X10Message class + auto *decodedMessage = new X10Message(); + uint8_t encodedMessage[5]{type, b0, b1, b2, b3}; + X10Message::decodeCommand(encodedMessage, decodedMessage); + + + // Convert enums to string + String houseCode(house_code_to_char(decodedMessage->houseCode)); + String unitCode(unit_code_to_int(decodedMessage->unitCode)); + const char* command = cmd_code_to_str(decodedMessage->command); + String commandString = (houseCode + unitCode + " " + command); + + Logger::trace(":%s %s", HOMEGENIEMINI_NS_PREFIX, commandString.c_str()); + + receiverRawData->setValue(rawDataString.c_str()); + HomeGenie::getInstance()->getEventRouter().signalEvent(QueuedMessage(domain, CONFIG_X10RF_MODULE_ADDRESS, IOEventPaths::Receiver_RawData, rawDataString)); + + receiverCommand->setValue(commandString.c_str()); + HomeGenie::getInstance()->getEventRouter().signalEvent(QueuedMessage(domain, CONFIG_X10RF_MODULE_ADDRESS, IOEventPaths::Receiver_Command, commandString)); +/* + QueuedMessage m = QueuedMessage(domain, houseCode + unitCode, (IOEventPaths::Status_Level), ""); + switch (decodedMessage->command) { + case Command::CMD_ON: + m.value = "1"; + homeGenie->getEventRouter().signalEvent(m); + break; + case Command::CMD_OFF: + m.value = "0"; + homeGenie->getEventRouter().signalEvent(m); + break; +// TODO: Implement all X10 events + Camera and Security + } +*/ + delete decodedMessage; + + // TODO: blink led ? (visible feedback) + //digitalWrite(LED_BUILTIN, LOW); // turn the LED on (HIGH is the voltage level) + //delay(10); // wait for a blink + //digitalWrite(LED_BUILTIN, HIGH); + + return true; + } + } + return false; + } + + Module* X10Handler::getModule(const char* domain, const char* address) { + for (int i = 0; i < moduleList.size(); i++) { + Module* module = moduleList.get(i); + if (module->domain.equals(domain) && module->address.equals(address)) + return module; + } + return nullptr; + } + LinkedList* X10Handler::getModuleList() { + return &moduleList; + } + +}} diff --git a/src/service/api/X10Handler.h b/examples/x10-transceiver/api/X10Handler.h similarity index 53% rename from src/service/api/X10Handler.h rename to examples/x10-transceiver/api/X10Handler.h index 75f04b0..8282d99 100644 --- a/src/service/api/X10Handler.h +++ b/examples/x10-transceiver/api/X10Handler.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,25 +30,36 @@ #ifndef HOMEGENIE_MINI_X10APIHANDLER_H #define HOMEGENIE_MINI_X10APIHANDLER_H -#include "APIHandler.h" +#include "HomeGenie.h" -#include -#include +#include "../configuration.h" +#include "../io/X10Message.h" +#include "../io/RFTransmitter.h" #define X10_DIM_BRIGHT_STEP (1.0f/21.0f) namespace Service { namespace API { + using namespace IO::X10; + class X10Handler : public APIHandler { + private: + LinkedList moduleList; + Module* rfModule; + ModuleParameter* receiverRawData; + ModuleParameter* receiverCommand; + IO::X10::RFTransmitter* transmitter; public: - bool canHandleDomain(String &domain); - bool handleRequest(HomeGenie &homeGenie, APIRequest *request, ESP8266WebServer &server); - bool handleEvent(HomeGenie &homeGenie, IIOEventSender *sender, const unsigned char *eventPath, void *eventData, IOEventDataType dataType); - - void getModuleJSON(OutputStreamCallback *outputCallback, String &domain, String &address); - void getModuleListJSON(OutputStreamCallback *outputCallback); - // TODO: deprecate `getGroupListJSON` (read note in the function body from .cpp file) - void getGroupListJSON(OutputStreamCallback *outputCallback); + X10Handler(IO::X10::RFTransmitter* transmitter); + void init() override; + bool canHandleDomain(String* domain) override; + bool handleRequest(APIRequest *request, WebServer &server) override; + bool handleEvent(IIOEventSender *sender, + const char* domain, const char* address, + const unsigned char *eventPath, void *eventData, IOEventDataType dataType) override; + + Module* getModule(const char* domain, const char* address) override; + LinkedList* getModuleList() override; }; }} diff --git a/examples/x10-transceiver/configuration.h b/examples/x10-transceiver/configuration.h new file mode 100644 index 0000000..77583e8 --- /dev/null +++ b/examples/x10-transceiver/configuration.h @@ -0,0 +1,10 @@ +#define CONFIG_X10RFReceiverPin 5 +#define CONFIG_X10RFTransmitterPin 4 +#define CONFIG_X10RF_MODULE_ADDRESS "RF" + +#ifdef MINI_ESP32 + +#define CONFIG_X10RFReceiverPin 22 +#define CONFIG_X10RFTransmitterPin 21 + +#endif diff --git a/src/io/rf/x10/RFReceiver.cpp b/examples/x10-transceiver/io/RFReceiver.cpp similarity index 81% rename from src/io/rf/x10/RFReceiver.cpp rename to examples/x10-transceiver/io/RFReceiver.cpp index e532617..f983945 100644 --- a/src/io/rf/x10/RFReceiver.cpp +++ b/examples/x10-transceiver/io/RFReceiver.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -39,9 +39,6 @@ namespace IO { namespace X10 { RFReceiver::RFReceiver() { receiverInstance = this; - // IEventSender members - domain = (const uint8_t *)(IOEventDomains::HomeAutomation_X10); - address = (const uint8_t *)HOMEGENIE_X10RF_MODULE_ADDRESS; } RFReceiver::RFReceiver(RFReceiverConfig *configuration) : RFReceiver() { @@ -53,13 +50,11 @@ namespace IO { namespace X10 { ////////////////////////////// void RFReceiver::begin() { - if (Config::X10RFReceiverEnabled) { - Logger::info("| - %s (PIN=%d INT=%d)", X10_RFRECEIVER_NS_PREFIX, configuration->getPin(), - configuration->getInterrupt()); - pinMode(configuration->getPin(), INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(configuration->getInterrupt()), receiverInstance_wrapper, RISING); - Logger::info("| ✔ %s", X10_RFRECEIVER_NS_PREFIX); - } + Logger::info("| - %s (PIN=%d INT=%d)", X10_RFRECEIVER_NS_PREFIX, configuration->getPin(), + configuration->getInterrupt()); + pinMode(configuration->getPin(), INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(configuration->getInterrupt()), receiverInstance_wrapper, RISING); + Logger::info("| ✔ %s", X10_RFRECEIVER_NS_PREFIX); } void RFReceiver::receive() { @@ -100,7 +95,7 @@ namespace IO { namespace X10 { if (isStandardCode || isSecurityCode) { messageType = isStandardCode ? (uint8_t) 0x20 : (uint8_t) 0x29; uint8_t data[] = { messageType, byteBuffer[0], byteBuffer[1], (byteBuffer[2]), (byteBuffer[3]) }; - sendEvent((const uint8_t*)(IOEventPaths::Sensor_RawData), data, IOEventDataType::Undefined); + sendEvent(domain.c_str(), address.c_str(), (const uint8_t*)(IOEventPaths::Receiver_RawData), data, IOEventDataType::Undefined); } receivedCount = -1; diff --git a/src/io/rf/x10/RFReceiver.h b/examples/x10-transceiver/io/RFReceiver.h similarity index 87% rename from src/io/rf/x10/RFReceiver.h rename to examples/x10-transceiver/io/RFReceiver.h index 6f5b168..f1254f1 100644 --- a/src/io/rf/x10/RFReceiver.h +++ b/examples/x10-transceiver/io/RFReceiver.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,15 +30,7 @@ #ifndef HOMEGENIE_MINI_X10_RFRECEIVER_H_ #define HOMEGENIE_MINI_X10_RFRECEIVER_H_ -#include "Arduino.h" - -#include -#include -#include -#include -#include -#include -#include +#include #include "RFReceiverConfig.h" @@ -55,6 +47,8 @@ namespace IO { namespace X10 { void receive(); private: + String domain = IOEventDomains::HomeAutomation_X10; + String address = CONFIG_X10RF_MODULE_ADDRESS; RFReceiverConfig *configuration; // 32-bit RF message decoding volatile uint8_t messageType = 0x00; diff --git a/src/io/rf/x10/RFReceiverConfig.cpp b/examples/x10-transceiver/io/RFReceiverConfig.cpp similarity index 95% rename from src/io/rf/x10/RFReceiverConfig.cpp rename to examples/x10-transceiver/io/RFReceiverConfig.cpp index 501576e..f73be3d 100644 --- a/src/io/rf/x10/RFReceiverConfig.cpp +++ b/examples/x10-transceiver/io/RFReceiverConfig.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -33,7 +33,7 @@ namespace IO { namespace X10 { RFReceiverConfig::RFReceiverConfig() { - pin = interrupt = D1; // 5 + pin = interrupt = CONFIG_X10RFReceiverPin; // 5 startBustMin = 8000; startBustMax = 16000; diff --git a/src/io/rf/x10/RFReceiverConfig.h b/examples/x10-transceiver/io/RFReceiverConfig.h similarity index 95% rename from src/io/rf/x10/RFReceiverConfig.h rename to examples/x10-transceiver/io/RFReceiverConfig.h index a0c0124..af53149 100644 --- a/src/io/rf/x10/RFReceiverConfig.h +++ b/examples/x10-transceiver/io/RFReceiverConfig.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,7 +30,9 @@ #ifndef HOMEGENIE_MINI_X10_RF_RECEIVER_CONFIG_H_ #define HOMEGENIE_MINI_X10_RF_RECEIVER_CONFIG_H_ -#include "Arduino.h" +#include + +#include "../configuration.h" namespace IO { namespace X10 { /** diff --git a/src/io/rf/x10/RFTransmitter.cpp b/examples/x10-transceiver/io/RFTransmitter.cpp similarity index 98% rename from src/io/rf/x10/RFTransmitter.cpp rename to examples/x10-transceiver/io/RFTransmitter.cpp index edd41d0..ec19c66 100644 --- a/src/io/rf/x10/RFTransmitter.cpp +++ b/examples/x10-transceiver/io/RFTransmitter.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). diff --git a/src/io/rf/x10/RFTransmitter.h b/examples/x10-transceiver/io/RFTransmitter.h similarity index 91% rename from src/io/rf/x10/RFTransmitter.h rename to examples/x10-transceiver/io/RFTransmitter.h index 07c25ed..14a37ba 100644 --- a/src/io/rf/x10/RFTransmitter.h +++ b/examples/x10-transceiver/io/RFTransmitter.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,10 +30,9 @@ #ifndef HOMEGENIE_MINI_X10_RF_TRANSMITTER_H_ #define HOMEGENIE_MINI_X10_RF_TRANSMITTER_H_ -#include "Arduino.h" +#include -#include -#include +#include "RFTransmitterConfig.h" namespace IO { namespace X10 { diff --git a/src/io/rf/x10/RFTransmitterConfig.cpp b/examples/x10-transceiver/io/RFTransmitterConfig.cpp similarity index 95% rename from src/io/rf/x10/RFTransmitterConfig.cpp rename to examples/x10-transceiver/io/RFTransmitterConfig.cpp index 9119870..addcdcf 100644 --- a/src/io/rf/x10/RFTransmitterConfig.cpp +++ b/examples/x10-transceiver/io/RFTransmitterConfig.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -33,7 +33,7 @@ namespace IO { namespace X10 { RFTransmitterConfig::RFTransmitterConfig() { - pin = D2; // 4 + pin = CONFIG_X10RFTransmitterPin; // 4 sendRepeat = 3; startBustLong = 9000; diff --git a/src/io/rf/x10/RFTransmitterConfig.h b/examples/x10-transceiver/io/RFTransmitterConfig.h similarity index 94% rename from src/io/rf/x10/RFTransmitterConfig.h rename to examples/x10-transceiver/io/RFTransmitterConfig.h index 0b12257..e6ed8e9 100644 --- a/src/io/rf/x10/RFTransmitterConfig.h +++ b/examples/x10-transceiver/io/RFTransmitterConfig.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,7 +30,9 @@ #ifndef HOMEGENIE_MINI_X10_RF_TRANSMITTER_CONFIG_H_ #define HOMEGENIE_MINI_X10_RF_TRANSMITTER_CONFIG_H_ -#include "Arduino.h" +#include + +#include "../configuration.h" namespace IO { namespace X10 { diff --git a/src/io/rf/x10/X10Message.cpp b/examples/x10-transceiver/io/X10Message.cpp similarity index 99% rename from src/io/rf/x10/X10Message.cpp rename to examples/x10-transceiver/io/X10Message.cpp index 0ae3928..308e285 100644 --- a/src/io/rf/x10/X10Message.cpp +++ b/examples/x10-transceiver/io/X10Message.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). diff --git a/src/io/rf/x10/X10Message.h b/examples/x10-transceiver/io/X10Message.h similarity index 98% rename from src/io/rf/x10/X10Message.h rename to examples/x10-transceiver/io/X10Message.h index 29bb83f..ce2a40a 100644 --- a/src/io/rf/x10/X10Message.h +++ b/examples/x10-transceiver/io/X10Message.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -31,8 +31,10 @@ * */ +#ifndef HOMEGENIE_MINI_X10_X10_MESSAGE_H_ +#define HOMEGENIE_MINI_X10_X10_MESSAGE_H_ -#include +#include /* Maximum debugging level to actually display */ #define DBG_MAX DBG_INFO @@ -308,3 +310,5 @@ namespace IO { } } + +#endif // HOMEGENIE_MINI_IOMANAGER_H \ No newline at end of file diff --git a/examples/x10-transceiver/x10-transceiver.cpp b/examples/x10-transceiver/x10-transceiver.cpp new file mode 100644 index 0000000..8e4730d --- /dev/null +++ b/examples/x10-transceiver/x10-transceiver.cpp @@ -0,0 +1,73 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2019-01-10 v1.0: initial release. + * - 2023-12-08 v1,1: added BLE support; targeting ESP32 by default. + * + */ + +#include + +#include "api/X10Handler.h" +#include "io/RFReceiver.h" +#include "io/RFTransmitter.h" + +using namespace IO; +using namespace Service; + +HomeGenie* homeGenie; + +void setup() { + + homeGenie = HomeGenie::getInstance(); + auto miniModule = homeGenie->getDefaultModule(); + + // X10 RF RFTransmitter + auto x10TransmitterConfig = new X10::RFTransmitterConfig(CONFIG_X10RFTransmitterPin); + auto x10Transmitter = new X10::RFTransmitter(x10TransmitterConfig); + + auto apiHandler = new X10Handler(x10Transmitter); + homeGenie->addAPIHandler(apiHandler); + + // X10 RF RFReceiver + auto x10ReceiverConfig = new X10::RFReceiverConfig(CONFIG_X10RFReceiverPin); + auto x10Receiver = new X10::RFReceiver(x10ReceiverConfig); + homeGenie->addIOHandler(x10Receiver); + + auto propRawData = new ModuleParameter(IOEventPaths::Receiver_RawData); + auto propCommand = new ModuleParameter(IOEventPaths::Receiver_Command); + miniModule->properties.add(propRawData); + miniModule->properties.add(propCommand); + + homeGenie->begin(); + +} + +void loop() +{ + + homeGenie->loop(); + +} diff --git a/lib/NTPClient-master/NTPClient.cpp b/lib/NTPClient-master/NTPClient.cpp index 1fcb56c..e998157 100644 --- a/lib/NTPClient-master/NTPClient.cpp +++ b/lib/NTPClient-master/NTPClient.cpp @@ -25,7 +25,7 @@ NTPClient::NTPClient(UDP& udp) { this->_udp = &udp; } -NTPClient::NTPClient(UDP& udp, int timeOffset) { +NTPClient::NTPClient(UDP& udp, long timeOffset) { this->_udp = &udp; this->_timeOffset = timeOffset; } @@ -35,13 +35,13 @@ NTPClient::NTPClient(UDP& udp, const char* poolServerName) { this->_poolServerName = poolServerName; } -NTPClient::NTPClient(UDP& udp, const char* poolServerName, int timeOffset) { +NTPClient::NTPClient(UDP& udp, const char* poolServerName, long timeOffset) { this->_udp = &udp; this->_timeOffset = timeOffset; this->_poolServerName = poolServerName; } -NTPClient::NTPClient(UDP& udp, const char* poolServerName, int timeOffset, unsigned long updateInterval) { +NTPClient::NTPClient(UDP& udp, const char* poolServerName, long timeOffset, unsigned long updateInterval) { this->_udp = &udp; this->_timeOffset = timeOffset; this->_poolServerName = poolServerName; @@ -52,7 +52,7 @@ void NTPClient::begin() { this->begin(NTP_DEFAULT_LOCAL_PORT); } -void NTPClient::begin(int port) { +void NTPClient::begin(unsigned int port) { this->_port = port; this->_udp->begin(this->_port); @@ -60,7 +60,7 @@ void NTPClient::begin(int port) { this->_udpSetup = true; } -bool NTPClient::isValid(byte * ntpPacket) +bool NTPClient::isValid(const byte * ntpPacket) { //Perform a few validity checks on the packet if((ntpPacket[0] & 0b11000000) == 0b11000000) //Check for LI=UNSYNC @@ -117,40 +117,44 @@ bool NTPClient::forceUpdate() { // this is NTP time (seconds since Jan 1 1900): unsigned long secsSince1900 = highWord << 16 | lowWord; - this->_currentEpoc = secsSince1900 - SEVENZYYEARS; + this->_currentEpoc = secsSince1900 - SEVENTY_YEARS; return true; } +bool NTPClient::isUpdated() const { + return (millis() - this->_lastUpdate < this->_updateInterval) + && this->_lastUpdate != 0; // if there was no update yet. +} + bool NTPClient::update() { - if ((millis() - this->_lastUpdate >= this->_updateInterval) // Update after _updateInterval - || this->_lastUpdate == 0) { // Update if there was no update yet. - if (!this->_udpSetup) this->begin(); // setup the UDP client if needed + if (!isUpdated()) { + if (!this->_udpSetup) this->begin(); // setup the UDP client if needed return this->forceUpdate(); } return true; } -unsigned long NTPClient::getEpochTime() { +unsigned long NTPClient::getEpochTime() const { return this->_timeOffset + // User offset - this->_currentEpoc + // Epoc returned by the NTP server + this->_currentEpoc + // Epoch returned by the NTP server ((millis() - this->_lastUpdate) / 1000); // Time since last update } -int NTPClient::getDay() { +int NTPClient::getDay() const { return (((this->getEpochTime() / 86400L) + 4 ) % 7); //0 is Sunday } -int NTPClient::getHours() { +int NTPClient::getHours() const { return ((this->getEpochTime() % 86400L) / 3600); } -int NTPClient::getMinutes() { +int NTPClient::getMinutes() const { return ((this->getEpochTime() % 3600) / 60); } -int NTPClient::getSeconds() { +int NTPClient::getSeconds() const { return (this->getEpochTime() % 60); } -String NTPClient::getFormattedTime(unsigned long secs) { +String NTPClient::getFormattedTime(unsigned long secs) const { unsigned long rawTime = secs ? secs : this->getEpochTime(); unsigned long hours = (rawTime % 86400L) / 3600; String hoursStr = hours < 10 ? "0" + String(hours) : String(hours); @@ -191,14 +195,14 @@ String NTPClient::getFormattedDate(unsigned long secs) { return String(year) + "-" + monthStr + "-" + dayStr + "T" + this->getFormattedTime(secs ? secs : 0) + getFormattedMilliseconds() + "Z"; } -int NTPClient::getMilliseconds() { +int NTPClient::getMilliseconds() const { return (int)((millis() - this->_lastUpdate) % 1000); } -String NTPClient::getFormattedMilliseconds() { +String NTPClient::getFormattedMilliseconds() const { char ms[5]; snprintf(ms, 5, ".%03d", getMilliseconds()); - return String(ms); + return {ms}; } void NTPClient::end() { @@ -239,4 +243,5 @@ void NTPClient::sendNTPPacket() { void NTPClient::setEpochTime(unsigned long secs) { this->_currentEpoc = secs; + this->_lastUpdate = millis(); } diff --git a/lib/NTPClient-master/NTPClient.h b/lib/NTPClient-master/NTPClient.h index 8026520..1f3585d 100644 --- a/lib/NTPClient-master/NTPClient.h +++ b/lib/NTPClient-master/NTPClient.h @@ -1,13 +1,15 @@ -#pragma once +#ifndef HOMEGENIE_MINI_NTP_CLIENT_H +#define HOMEGENIE_MINI_NTP_CLIENT_H #include "Arduino.h" - #include -#define SEVENZYYEARS 2208988800UL + +#define SEVENTY_YEARS 2208988800UL #define NTP_PACKET_SIZE 48 #define NTP_DEFAULT_LOCAL_PORT 1337 #define LEAP_YEAR(Y) ( (Y>0) && !(Y%4) && ( (Y%100) || !(Y%400) ) ) +#define NTP_UPDATE_INTERVAL_MS 1800000 /* half an hour in ms */ class NTPClient { @@ -16,10 +18,10 @@ class NTPClient { bool _udpSetup = false; const char* _poolServerName = "pool.ntp.org"; // Default time server - int _port = NTP_DEFAULT_LOCAL_PORT; - int _timeOffset = 0; + unsigned int _port = NTP_DEFAULT_LOCAL_PORT; + long _timeOffset = 0; - unsigned long _updateInterval = 60000; // In ms + unsigned long _updateInterval = NTP_UPDATE_INTERVAL_MS; // In ms unsigned long _currentEpoc = 0; // In s unsigned long _lastUpdate = 0; // In ms @@ -27,14 +29,14 @@ class NTPClient { byte _packetBuffer[NTP_PACKET_SIZE]; void sendNTPPacket(); - bool isValid(byte * ntpPacket); + static bool isValid(const byte * ntpPacket); public: NTPClient(UDP& udp); - NTPClient(UDP& udp, int timeOffset); + NTPClient(UDP& udp, long timeOffset); NTPClient(UDP& udp, const char* poolServerName); - NTPClient(UDP& udp, const char* poolServerName, int timeOffset); - NTPClient(UDP& udp, const char* poolServerName, int timeOffset, unsigned long updateInterval); + NTPClient(UDP& udp, const char* poolServerName, long timeOffset); + NTPClient(UDP& udp, const char* poolServerName, long timeOffset, unsigned long updateInterval); /** * Starts the underlying UDP client with the default local port @@ -44,7 +46,9 @@ class NTPClient { /** * Starts the underlying UDP client with the specified local port */ - void begin(int port); + void begin(unsigned int port); + + bool isUpdated() const; /** * This should be called in the main loop of your application. By default an update from the NTP Server is only @@ -61,12 +65,12 @@ class NTPClient { */ bool forceUpdate(); - int getDay(); - int getHours(); - int getMinutes(); - int getSeconds(); - int getMilliseconds(); - String getFormattedMilliseconds(); + int getDay() const; + int getHours() const; + int getMinutes() const; + int getSeconds() const; + int getMilliseconds() const; + String getFormattedMilliseconds() const; /** * Changes the time offset. Useful for changing timezones dynamically @@ -82,12 +86,12 @@ class NTPClient { /** * @return secs argument (or 0 for current time) formatted like `hh:mm:ss` */ - String getFormattedTime(unsigned long secs = 0); + String getFormattedTime(unsigned long secs = 0) const; /** * @return time in seconds since Jan. 1, 1970 */ - unsigned long getEpochTime(); + unsigned long getEpochTime() const; /** * @return secs argument (or 0 for current date) formatted to ISO 8601 @@ -105,3 +109,5 @@ class NTPClient { */ void setEpochTime(unsigned long secs); }; + +#endif // HOMEGENIE_MINI_NTP_CLIENT_H diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/library.json b/library.json new file mode 100644 index 0000000..656a0ef --- /dev/null +++ b/library.json @@ -0,0 +1,21 @@ +{ + "name": "HomeGenie", + "description": "Library for creating easily smart devices based on ESP32 or ESP8266 chip.", + "keywords": "iot,esp32,esp8266,mqtt,sse,http,websocket,homegenie,smarthome,", + "authors": { + "name": "genielabs", + "url": "https://github.com/genielabs", + "maintainer": true + }, + "repository": { + "type": "git", + "url": "https://github.com/genielabs/homegenie-mini.git" + }, + "version": "1.2.0", + "frameworks": ["arduino", "espidf", "*"], + "platforms": ["espressif32", "espressif8266"], + "headers": "HomeGenie.h", + "build": { + "libArchive": false + } +} \ No newline at end of file diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..94290b2 --- /dev/null +++ b/library.properties @@ -0,0 +1,11 @@ +name=HomeGenie +version=1.2.0 +author=Generoso Martello +maintainer=Generoso Martello, https://glabs.it +sentence=Library for creating easily smart devices based on ESP32 or ESP8266 chip. +paragraph=Automatic configuration of the device via Bluetooth or WPA. Implements HTTP, SSE, WebSocket, MQTT protocols for routing device activity and signals to connected clients and gateways. Free Android client available on Google PlayStore (HomeGenie Panel) to autodetect and configure the device. +category=Signal Input/Output +url=https://github.com/genielabs/homegenie-mini +architectures=ESP32,ESP8266 +includes=HomeGenie.h +license=GPLv3 diff --git a/platformio.ini b/platformio.ini index 1e1eb6e..7b7e589 100644 --- a/platformio.ini +++ b/platformio.ini @@ -7,17 +7,103 @@ ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html +[platformio] +src_dir = . +include_dir = ./src -[env:d1_mini] -platform = espressif8266@1.7.3 ; -board = d1_mini +[env] +build_src_filter = -<*> + framework = arduino +board = esp32dev lib_deps = - ArduinoJson - ArduinoLog - WebSockets - OneWire - LinkedList - TINYXML -;build_flags = -D PIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH -;build_flags = -L./lib -llibjerry-core -llibjerry-libm + ArduinoJson@6.21.4 + thijse/ArduinoLog@1.1.1 + WebSockets@2.4.1 + OneWire@2.3.8 + vortigont/LinkedList@1.5.0 + adafruit/TINYXML@1.0.3 + ESP32Time@2.0.4 + mbed-seeed/BluetoothSerial@0.0.0+sha.f56002898ee8 +# h2zero/NimBLE-Arduino + + +[env:default] +platform = espressif32 +board_build.flash_size = 4MB +board_build.partitions = min_spiffs.csv + + +[env:d1-mini] +platform = espressif8266@1.7.3 +board = d1_mini +lib_ignore = + ESP32Time + BluetoothSerial + +[env:d1-mini-esp32] +platform = espressif32 +build_flags = -DMINI_ESP32 -DCONFIG_ServiceButtonPin=16 -DCONFIG_StatusLedPin=26 -DCONFIG_GPIO_OUT={18,19,23,5} -DCONFIG_GPIO_IN={} +board_build.flash_size = 4MB +board_build.partitions = min_spiffs.csv + + +[env:sonoff] +platform = espressif32 +build_flags = -DCONFIG_ServiceButtonPin=0 -DCONFIG_StatusLedPin=13 -DCONFIG_GPIO_OUT={14,27} -DCONFIG_GPIO_IN={32,33} +board_build.flash_size = 4MB +board_build.partitions = min_spiffs.csv + + +#------------------[ Examples ]------------------ + + +[env:smart-sensor] +platform = espressif32 +build_flags = -I examples -I src +build_src_filter = + - + +board_build.flash_size = 4MB +board_build.partitions = min_spiffs.csv + +[env:smart-sensor-d1-mini-esp32] +platform = espressif32 +build_flags = -I examples -I src -DMINI_ESP32 -DCONFIG_ServiceButtonPin=16 -DCONFIG_StatusLedPin=26 -DCONFIG_GPIO_OUT={18,19,23,5} -DCONFIG_GPIO_IN={} +build_src_filter = + - + +board_build.flash_size = 4MB +board_build.partitions = min_spiffs.csv + +[env:smart-sensor-d1-mini] +platform = espressif8266@1.7.3 +board = d1_mini +build_flags = -I examples -I src +build_src_filter = + - + +lib_ignore = + ESP32Time + BluetoothSerial + +[env:x10-transceiver] +platform = espressif32 +build_flags = -I examples -I src +build_src_filter = + - + +board_build.flash_size = 4MB +board_build.partitions = min_spiffs.csv + +[env:rf-transceiver] +platform = espressif32 +build_flags = -I examples -I src +build_src_filter = + - + +board_build.flash_size = 4MB +board_build.partitions = min_spiffs.csv +lib_deps = ${env.lib_deps} + rc-switch@2.6.4 + + +[env:playground] +platform = espressif32 +build_flags = -I examples -I src +build_src_filter = + - + +board_build.flash_size = 4MB +board_build.partitions = min_spiffs.csv +lib_deps = ${env.lib_deps} +# roboticsbrno/ServoESP32@1.1.0 +# ESP32Servo@1.1.1 + madhephaestus/ESP32Servo diff --git a/src/Config.h b/src/Config.h index 2a3c359..17bf052 100644 --- a/src/Config.h +++ b/src/Config.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -32,12 +32,13 @@ #include +#include "defs.h" + class Config { public: - const static uint8_t ServiceButtonPin = D4; - const static uint8_t StatusLedPin = D0; + const static uint8_t ServiceButtonPin = CONFIG_ServiceButtonPin; + const static uint8_t StatusLedPin = CONFIG_StatusLedPin; const static uint16_t WpsModePushInterval = 2500; - const static bool X10RFReceiverEnabled = false; }; #endif //HOMEGENIE_MINI_CONFIG_H diff --git a/src/HomeGenie.cpp b/src/HomeGenie.cpp new file mode 100644 index 0000000..ed8ada9 --- /dev/null +++ b/src/HomeGenie.cpp @@ -0,0 +1,316 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2019-01-10 Initial release + * + */ + +#include "HomeGenie.h" + +namespace Service { + HomeGenie* HomeGenie::serviceInstance = nullptr; + + HomeGenie::HomeGenie() { + eventRouter.withNetManager(netManager); + + // Setup status led + pinMode(Config::StatusLedPin, OUTPUT); + // Setup button + pinMode(Config::ServiceButtonPin, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(Config::ServiceButtonPin), buttonChange, CHANGE); + + // Logger initialization + Logger::begin(LOG_LEVEL_TRACE); + + // Welcome message + Logger::info("%s %s", CONFIG_DEVICE_MODEL_NAME, CONFIG_DEVICE_MODEL_NUMBER); + Logger::info("Booting..."); + + // Default service handler + auto gpioPort = new GPIOPort(); + addIOHandler(gpioPort); + auto homeGenieHandler = new HomeGenieHandler(gpioPort); + addAPIHandler(homeGenieHandler); + + Logger::info("+ Starting HomeGenie service"); + } + + void HomeGenie::begin() { + netManager.begin(); + netManager.getHttpServer().addHandler(this); +#ifndef DISABLE_BLE + netManager.getBLEManager().addHandler(this); +#endif + ioManager.begin(); + ioManager.setOnEventCallback(this); + + // Initialize custom API handlers + for (int i = 0; i < handlers.size(); i++) { + auto handler = handlers.get(i); + handler->init(); + } + + Logger::info("READY."); + } + + void HomeGenie::loop() { + Logger::verbose(":%s loop() >> BEGIN", HOMEGENIEMINI_NS_PREFIX); + + statusLedLoop(); + checkServiceButton(); + + // HomeGenie-Mini Terminal CLI + if (Serial.available() > 0) { + String cmd = Serial.readStringUntil('\n'); + auto apiCommand = APIRequest::parse(cmd); + // TODO: implement API commands from console input as well + // - see `HomeGenie::api(...)` method + } + + // TODO: sort of system load index could be obtained by measuring time elapsed for `TaskManager::loop()` method + TaskManager::loop(); + + Logger::verbose(":%s loop() << END", HOMEGENIEMINI_NS_PREFIX); + } + + bool HomeGenie::addAPIHandler(APIHandler* handler) { + handlers.add(handler); + return true; + } + bool HomeGenie::addIOHandler(IO::IIOEventSender *handler) { + ioManager.addEventSender(handler); + return true; + } + + NetManager &HomeGenie::getNetManager() { + return netManager; + } + + IOManager &HomeGenie::getIOManager() { + return ioManager; + } + + EventRouter &HomeGenie::getEventRouter() { + return eventRouter; + } + + + // BEGIN IIOEventSender interface methods + void HomeGenie::onIOEvent(IIOEventSender *sender, const char* domain, const char* address, const unsigned char *eventPath, void *eventData, + IOEventDataType dataType) { + String event = String((char *) eventPath); + Logger::trace(":%s [IOManager::IOEvent] >> [domain '%s' address '%s' event '%s']", HOMEGENIEMINI_NS_PREFIX, + domain, address, event.c_str()); + String d = domain; + for (int i = 0; i < handlers.size(); i++) { + auto handler = handlers.get(i); + if (handler->canHandleDomain(&d)) { + handler->handleEvent(sender, domain, address, eventPath, eventData, dataType); + break; + } + } + } + // END IIOEventSender + + // BEGIN RequestHandler interface methods + bool HomeGenie::canHandle(HTTPMethod method, String uri) { + return uri != nullptr && uri.startsWith("/api/"); + } + + bool HomeGenie::handle(WebServer &server, HTTPMethod requestMethod, String requestUri) { + auto command = APIRequest::parse(requestUri); + if (api(&command, server)) { + if (command.Response.length() > 0) { + server.send(200, "application/json", command.Response); + } + } else { + server.send(400, "application/json", command.Response); + } + return true; + } + // END RequestHandler interface methods + + bool HomeGenie::api(APIRequest *request, WebServer &server) { + for (int i = 0; i < handlers.size(); i++) { + auto handler = handlers.get(i); + if (handler->canHandleDomain(&request->Domain)) { + return handler->handleRequest(request, server); + } + } + return false; + } + + Module* HomeGenie::getDefaultModule() { + auto domain = String(IOEventDomains::HomeAutomation_HomeGenie); + auto address = String(CONFIG_BUILTIN_MODULE_ADDRESS); + return getModule(&domain, &address); + } + + Module* HomeGenie::getModule(String* domain, String* address) { + for (int i = 0; i < handlers.size(); i++) { + auto handler = handlers.get(i); + if (handler->canHandleDomain(domain)) { + return handler->getModule(domain->c_str(), address->c_str()); + } + } + return nullptr; + } + + const char* HomeGenie::getModuleJSON(Module* module) { + String parameters = ""; + for(int p = 0; p < module->properties.size(); p++) { + auto param = module->properties.get(p); + parameters += HomeGenie::createModuleParameter(param->name.c_str(), param->value.c_str(), param->updateTime.c_str()); + if (p < module->properties.size() - 1) { + parameters += ","; + } + } + String out = HomeGenie::createModule(module->domain.c_str(), module->address.c_str(), + module->name.c_str(), module->description.c_str(), module->type.c_str(), + parameters.c_str()); + return out.c_str(); + } + + unsigned int HomeGenie::writeModuleJSON(WebServer *server, String* domain, String* address) { + auto outputCallback = APIHandlerOutputCallback(server); + auto module = getModule(domain, address); + if (module != nullptr) { + String out = getModuleJSON(module); + outputCallback.write(out); + } + return outputCallback.outputLength; + } + + unsigned int HomeGenie::writeModuleListJSON(WebServer *server) { + bool firstModule = true; + auto outputCallback = APIHandlerOutputCallback(server); + String out = "["; + outputCallback.write(out); + for (int i = 0; i < handlers.size(); i++) { + auto handler = handlers.get(i); + auto moduleList = handler->getModuleList(); + + for(int m = 0; m < moduleList->size(); m++) { + auto module = moduleList->get(m); + if (!firstModule) { + out = ","; + } else { + firstModule = false; + out = ""; + } + out += getModuleJSON(module); + outputCallback.write(out); + } + } + out = "]\n"; + outputCallback.write(out); + return outputCallback.outputLength; + } + + unsigned int HomeGenie::writeGroupListJSON(WebServer *server) { + bool firstModule = true; + auto outputCallback = APIHandlerOutputCallback(server); + String defaultGroupName = "Dashboard"; +#ifndef CONFIGURE_WITH_WPA + Preferences preferences; + preferences.begin(CONFIG_SYSTEM_NAME, true); + String deviceName = preferences.getString("device:name", ""); + preferences.end(); + if (deviceName.length() > 0) { + defaultGroupName = deviceName; + } +#endif + String out = R"([{"Name": ")" + defaultGroupName + R"(", "Modules": [)"; + outputCallback.write(out); + for (int i = 0; i < handlers.size(); i++) { + auto handler = handlers.get(i); + auto moduleList = handler->getModuleList(); + + for(int m = 0; m < moduleList->size(); m++) { + if (!firstModule) { + out = ","; + } else { + firstModule = false; + out = ""; + } + auto module = moduleList->get(m); + out += R"({"Address": ")" + module->address + R"(", "Domain": ")" + module->domain + R"("})"; + outputCallback.write(out); + } + + } + out = "]}]\n"; + outputCallback.write(out); + return outputCallback.outputLength; + } + + + String HomeGenie::createModuleParameter(const char *name, const char *value, const char *timestamp) { + static const char *parameterTemplate = R"({ + "Name": "%s", + "Value": "%s", + "Description": "%s", + "FieldType": "%s", + "UpdateTime": "%s" + })"; + ssize_t size = snprintf(nullptr, 0, parameterTemplate, + name, value, "", "", timestamp + ) + 1; + char *parameterJson = (char *) malloc(size); + snprintf(parameterJson, size, parameterTemplate, + name, value, "", "", timestamp + ); + auto p = String(parameterJson); + free(parameterJson); + return p; + } + + String HomeGenie::createModule(const char *domain, const char *address, const char *name, const char *description, + const char *deviceType, const char *parameters) { + static const char *moduleTemplate = R"({ + "Name": "%s", + "Description": "%s", + "DeviceType": "%s", + "Domain": "%s", + "Address": "%s", + "Properties": [%s] +})"; + ssize_t size = snprintf(nullptr, 0, moduleTemplate, + name, description, deviceType, + domain, address, + parameters + ) + 1; + char *moduleJson = (char *) malloc(size); + snprintf(moduleJson, size, moduleTemplate, + name, description, deviceType, + domain, address, + parameters + ); + auto m = String(moduleJson); + free(moduleJson); + return m; + } + +} diff --git a/src/HomeGenie.h b/src/HomeGenie.h new file mode 100644 index 0000000..9f00901 --- /dev/null +++ b/src/HomeGenie.h @@ -0,0 +1,173 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2019-01-10 Initial release + * + */ + +#ifndef HOMEGENIE_MINI_HOMEGENIE_H +#define HOMEGENIE_MINI_HOMEGENIE_H + + +#include "Task.h" +#include "TaskManager.h" + +#include "io/gpio/GPIOPort.h" +#include "io/IOEventPaths.h" +#include "io/IOManager.h" +#include "net/NetManager.h" +#include "service/api/APIRequest.h" +#include "service/api/APIHandler.h" +#include "service/api/HomeGenieHandler.h" +#include "service/EventRouter.h" +#include "service/Module.h" + + +#define HOMEGENIEMINI_NS_PREFIX "Service::HomeGenie" + +namespace Service { + + using namespace IO; + using namespace Net; + using namespace Service::API; + + class HomeGenie: RequestHandler, IIOEventReceiver { + public: + static HomeGenie* getInstance() { + if (serviceInstance == nullptr) { + serviceInstance = new HomeGenie(); + } + return serviceInstance; + }; + + HomeGenie(); + + void begin(); + + // Task overrides + void loop(); + + // IIOEventReceiver overrides + void onIOEvent(IIOEventSender *sender, const char* domain, const char* address, const unsigned char *eventPath, void *eventData, IOEventDataType dataType) override; + + // RequestHandler overrides + bool canHandle(HTTPMethod method, String uri) override; + bool handle(WebServer& server, HTTPMethod requestMethod, String requestUri) override; + bool api(APIRequest *request, WebServer &server); + + /** + * + * @param handler + * @return + */ + bool addAPIHandler(APIHandler* handler); + /** + * + * @param handler + * @return + */ + bool addIOHandler(IIOEventSender* handler); + + NetManager& getNetManager(); + IOManager& getIOManager(); + EventRouter& getEventRouter(); + + /** + * + * @return + */ + Module* getDefaultModule(); + Module* getModule(String* domain, String* address); + + const char* getModuleJSON(Module* module); + unsigned int writeModuleListJSON(WebServer *server); + unsigned int writeModuleJSON(WebServer *server, String* domain, String* address); + unsigned int writeGroupListJSON(WebServer *server); + + static String createModule(const char *domain, const char *address, const char *name, const char* description, const char *deviceType, const char *parameters); + static String createModuleParameter(const char *name, const char* value, const char *timestamp); + + private: + static HomeGenie* serviceInstance; + NetManager netManager; + IOManager ioManager; + EventRouter eventRouter; + LinkedList handlers; + + // Service Button handling + + volatile int64_t buttonPressStart = 0; + volatile bool buttonPressed = false; + static void buttonChange() { + getInstance()->buttonPressed = (digitalRead(Config::ServiceButtonPin) == LOW); + if (getInstance()->buttonPressed) { + getInstance()->buttonPressStart = millis(); + } + } + static void checkServiceButton() { + int64_t elapsed = 0; + if (getInstance()->buttonPressed) { + // released + elapsed = millis() - getInstance()->buttonPressStart; + if (elapsed > Config::WpsModePushInterval) { + noInterrupts(); + getInstance()->getNetManager().getWiFiManager().configure(); + interrupts(); + } + } + } + bool statusLedOn = false; + uint64_t statusLedTs = 0; + void statusLedLoop() { + if (WiFi.isConnected()) { + // when connected the LED will blink quickly every 2 seconds + if (millis() - statusLedTs > 1950 && !statusLedOn) { + statusLedOn = true; + digitalWrite(Config::StatusLedPin, HIGH); + statusLedTs = millis(); + } else if (statusLedOn && millis() - statusLedTs > 50) { + statusLedOn = false; + digitalWrite(Config::StatusLedPin, LOW); + statusLedTs = millis(); + } + } else { + // if not connected the LED will blink quickly every 200ms + if (millis() - statusLedTs > 100 && !statusLedOn) { + statusLedOn = true; + digitalWrite(Config::StatusLedPin, HIGH); + statusLedTs = millis(); + } else if (statusLedOn && millis() - statusLedTs > 100) { + statusLedOn = false; + digitalWrite(Config::StatusLedPin, LOW); + statusLedTs = millis(); + } + } + } + + }; + +} + +#endif //HOMEGENIE_MINI_HOMEGENIE_H diff --git a/src/Task.cpp b/src/Task.cpp index 7e34080..897e69b 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -27,15 +27,15 @@ * */ -#include -#include +#include "Task.h" +#include "TaskManager.h" Task::Task() { creationTs = millis(); TaskManager::addTask(this); } -bool Task::willLoop() { +bool Task::willLoop() const { unsigned long now = millis(); return now - lastLoopTs >= loopInterval; @@ -46,6 +46,6 @@ void Task::loopExit() { lastLoopTs = millis(); } -uint64_t Task::taskIdleTime() { +uint64_t Task::taskIdleTime() const { return millis()-lastLoopTs; }; diff --git a/src/Task.h b/src/Task.h index d79ecd2..13bb180 100644 --- a/src/Task.h +++ b/src/Task.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,9 +30,7 @@ #ifndef HOMEGENIE_MINI_TASK_H #define HOMEGENIE_MINI_TASK_H -#include - -#include +#include "Config.h" // Task interface @@ -47,7 +45,7 @@ class Task { * If the task loop() is scheduled. * @return `true` if scheduled, `false` otherwise. */ - bool willLoop(); + bool willLoop() const; /** * Set task loop() schedule interval. * @param interval_ms schedule interval in milliseconds. @@ -55,12 +53,12 @@ class Task { void setLoopInterval(uint64_t interval_ms) { loopInterval = interval_ms; }; /// Pointer to the next task - Task* nextTask = NULL; + Task* nextTask = nullptr; /// Pointer to the previous task - Task* previousTask = NULL; + Task* previousTask = nullptr; void loopExit(); - uint64_t taskIdleTime(); + uint64_t taskIdleTime() const; uint64_t uptime() { return millis() - creationTs; } @@ -68,7 +66,7 @@ class Task { private: uint64_t creationTs; uint64_t lastLoopTs; - uint64_t loopInterval; + uint64_t loopInterval = 0; }; diff --git a/src/TaskManager.cpp b/src/TaskManager.cpp index 88af749..cc1a852 100644 --- a/src/TaskManager.cpp +++ b/src/TaskManager.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -29,11 +29,11 @@ #include "TaskManager.h" -static Task *taskList = NULL, *currentTask = NULL; +static Task *taskList = nullptr, *currentTask = nullptr; TaskManager::TaskManager() { - taskList = NULL; - currentTask = NULL; + taskList = nullptr; + currentTask = nullptr; } void TaskManager::loop() { @@ -42,7 +42,7 @@ void TaskManager::loop() { IO::Logger::verbose("%s loop() >> BEGIN", TASKMANAGER_LOG_PREFIX); Task *t = taskList; uint c = 0; - while (t != NULL) { + while (t != nullptr) { IO::Logger::verbose("%s - running task %d", TASKMANAGER_LOG_PREFIX, c++); if (t->willLoop()) { t->loop(); @@ -54,14 +54,14 @@ void TaskManager::loop() { } void TaskManager::addTask(Task *task) { - if (taskList == NULL) { + if (taskList == nullptr) { taskList = currentTask = task; - taskList->nextTask = NULL; - taskList->previousTask = NULL; + taskList->nextTask = nullptr; + taskList->previousTask = nullptr; } else { currentTask->nextTask = task; task->previousTask = currentTask; - task->nextTask = NULL; + task->nextTask = nullptr; currentTask = task; } } diff --git a/src/TaskManager.h b/src/TaskManager.h index 55107c9..9226b8e 100644 --- a/src/TaskManager.h +++ b/src/TaskManager.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,9 +30,8 @@ #ifndef HOMEGENIE_MINI_TASKMANAGER_H #define HOMEGENIE_MINI_TASKMANAGER_H - -#include -#include +#include "Task.h" +#include "io/Logger.h" #define TASKMANAGER_LOG_PREFIX "@ROOT::TaskManager" diff --git a/src/Utility.cpp b/src/Utility.cpp index d07f6db..a497035 100644 --- a/src/Utility.cpp +++ b/src/Utility.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -40,7 +40,7 @@ void Utility::getBytes(const String &rawBytes, uint8_t *data) { { tmp[0] = msg[i * 2]; tmp[1] = msg[(i * 2) + 1]; - data[i] = strtol(tmp, NULL, 16); + data[i] = strtol(tmp, nullptr, 16); } } String Utility::byteToHex(byte b) { diff --git a/src/Utility.h b/src/Utility.h index 6fad165..2e0ccf4 100644 --- a/src/Utility.h +++ b/src/Utility.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,7 +30,7 @@ #ifndef HOMEGENIE_MINI_UTILITY_H #define HOMEGENIE_MINI_UTILITY_H -#include +#include "Config.h" class Utility { public: @@ -40,5 +40,4 @@ class Utility { static uint8_t reverseByte(uint8_t n); }; - #endif //HOMEGENIE_MINI_UTILITY_H diff --git a/src/defs.h b/src/defs.h new file mode 100644 index 0000000..6a758ec --- /dev/null +++ b/src/defs.h @@ -0,0 +1,62 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + */ + +#ifndef HOMEGENIE_MINI_DEFS_H +#define HOMEGENIE_MINI_DEFS_H + +#define CONFIG_SYSTEM_NAME "hg-mini" + +#define CONFIG_DEVICE_MODEL_NAME "HomeGenie Mini" +#define CONFIG_DEVICE_MODEL_NUMBER "1.2.0" +#define CONFIG_DEVICE_SERIAL_NUMBER "ABC0123456789" + +#define CONFIG_BUILTIN_MODULE_NAME "HG-Mini" +#define CONFIG_BUILTIN_MODULE_ADDRESS "mini" + +#define DISABLE_BLE + +#ifdef ESP8266 + #define CONFIGURE_WITH_WPA + #define WebServer ESP8266WebServer + #ifndef CONFIG_GPIO_OUT + #define CONFIG_GPIO_OUT {14,12,13,15} + #endif +#endif + +#ifndef CONFIG_ServiceButtonPin + #define CONFIG_ServiceButtonPin 2 +#endif +#ifndef CONFIG_StatusLedPin + #define CONFIG_StatusLedPin 16 +#endif +#ifndef CONFIG_GPIO_OUT + #define CONFIG_GPIO_OUT {14,12,13,17} +#endif +#ifndef CONFIG_GPIO_IN + #define CONFIG_GPIO_IN { /* not implemented */ } +#endif + +#endif // HOMEGENIE_MINI_DEFS_H diff --git a/src/io/IOEvent.h b/src/io/IOEvent.h index c84b04b..4e4ba76 100644 --- a/src/io/IOEvent.h +++ b/src/io/IOEvent.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,8 +30,6 @@ #ifndef HOMEGENIE_MINI_IIOEVENT_H #define HOMEGENIE_MINI_IIOEVENT_H -#include - namespace IO { class IIOEventSender; @@ -45,30 +43,29 @@ namespace IO { SensorTemperature }; -// IIOEventReceiver interface + // IIOEventReceiver interface class IIOEventReceiver { public: - virtual void onIOEvent(IIOEventSender *, const uint8_t *, void *, IOEventDataType dataType = Undefined) = 0; // pure virtual + virtual void onIOEvent(IIOEventSender *, const char *, const char *, const uint8_t *, void *, IOEventDataType dataType = Undefined) = 0; // pure virtual }; -// IIOEventSender interface + // IIOEventSender interface class IIOEventSender { public: - const uint8_t* getDomain() { return domain; } - const uint8_t* getAddress() { return address; } +// const uint8_t* getDomain() { return domain; } +// const uint8_t* getAddress() { return address; } + virtual void begin() = 0; void setEventReceiver(IIOEventReceiver *receiver) { eventReceiver = receiver; } - virtual void sendEvent(const uint8_t *eventPath, void *eventData, IOEventDataType dataType) { - if (eventReceiver != NULL) { - eventReceiver->onIOEvent(this, eventPath, eventData, dataType); + virtual void sendEvent(const char *domain, const char *address, const uint8_t *eventPath, void *eventData, IOEventDataType dataType) { + if (eventReceiver != nullptr) { + eventReceiver->onIOEvent(this, domain, address, eventPath, eventData, dataType); } }; protected: - const uint8_t *domain; - const uint8_t *address; - IIOEventReceiver *eventReceiver; + IIOEventReceiver *eventReceiver = nullptr; }; } diff --git a/src/io/IOEventDomains.h b/src/io/IOEventDomains.h index d468871..8593266 100644 --- a/src/io/IOEventDomains.h +++ b/src/io/IOEventDomains.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -34,6 +34,7 @@ namespace IO { namespace IOEventDomains { const char HomeAutomation_HomeGenie[] = "HomeAutomation.HomeGenie"; const char HomeAutomation_X10[] = "HomeAutomation.X10"; + const char HomeAutomation_RCS[] = "HomeAutomation.RCS"; }; } diff --git a/src/io/IOEventPaths.h b/src/io/IOEventPaths.h index e0f60e0..b79a0e8 100644 --- a/src/io/IOEventPaths.h +++ b/src/io/IOEventPaths.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -32,7 +32,8 @@ namespace IO { namespace IOEventPaths { - const char Sensor_RawData[] = "Sensor.RawData"; + const char Receiver_RawData[] = "Receiver.RawData"; + const char Receiver_Command[] = "Receiver.Command"; const char Status_Level[] = "Status.Level"; const char Sensor_Luminance[] = "Sensor.Luminance"; const char Sensor_Temperature[] = "Sensor.Temperature"; diff --git a/src/io/IOManager.cpp b/src/io/IOManager.cpp index 88abf31..578990e 100644 --- a/src/io/IOManager.cpp +++ b/src/io/IOManager.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -27,45 +27,34 @@ * */ -#include +#include "IOManager.h" namespace IO { IOManager::IOManager() { systemDiagnostics = new System::Diagnostics(); systemDiagnostics->setEventReceiver(this); - // Instantiate the X10 RFReceiver Class - RFReceiverConfig x10ReceiverConfig = RFReceiverConfig(CONFIG_RF_RX_PIN); - x10Receiver = new RFReceiver(&x10ReceiverConfig); - x10Receiver->setEventReceiver(this); - // X10 RF RFReceiver and RFTransmitter objects - RFTransmitterConfig x10TransmitterConfig = RFTransmitterConfig(CONFIG_RF_TX_PIN); - x10Transmitter = new RFTransmitter(&x10TransmitterConfig); - // DS18B20 Temperature Sensor - temperatureSensor = new DS18B20(); - temperatureSensor->setEventReceiver(this); - // Light Sensor - lightSensor = new LightSensor(); - lightSensor->setEventReceiver(this); - // P1 expansion port - p1Port = new P1Port(); - p1Port->setEventReceiver(this); } void IOManager::begin() { - x10Receiver->begin(); - x10Transmitter->begin(); - temperatureSensor->begin(); - lightSensor->begin(); + for(int i = 0; i < eventSenders.size(); i++) { + eventSenders[i]->begin(); + } + } + + bool IOManager::addEventSender(IIOEventSender* sender) { + eventSenders.add(sender); + sender->setEventReceiver(this); + return true; } void IOManager::setOnEventCallback(IIOEventReceiver *callback) { ioEventCallback = callback; } - void IOManager::onIOEvent(IIOEventSender *sender, const unsigned char *eventPath, void *eventData, IOEventDataType dataType) { + void IOManager::onIOEvent(IIOEventSender *sender, const char* domain, const char* address, const unsigned char *eventPath, void *eventData, IOEventDataType dataType) { // route event to HomeGenie - ioEventCallback->onIOEvent(sender, eventPath, eventData, dataType); + ioEventCallback->onIOEvent(sender, domain, address, eventPath, eventData, dataType); } } \ No newline at end of file diff --git a/src/io/IOManager.h b/src/io/IOManager.h index 8724537..3dab986 100644 --- a/src/io/IOManager.h +++ b/src/io/IOManager.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,31 +30,17 @@ #ifndef HOMEGENIE_MINI_IOMANAGER_H #define HOMEGENIE_MINI_IOMANAGER_H -#include +#include -#include "Logger.h" +#include "io/IOEvent.h" +#include "io/IOEventDomains.h" +#include "io/IOEventPaths.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define CONFIG_RF_RX_PIN D1 // 5 -#define CONFIG_RF_TX_PIN D2 // 4 +#include "io/sys/Diagnostics.h" +#include "service/api/APIRequest.h" namespace IO { - using namespace Env; - using namespace GPIO; - using namespace X10; - class IOManager : IIOEventReceiver { public: @@ -62,34 +48,18 @@ namespace IO { void begin(); + bool addEventSender(IIOEventSender* sender); + // IIOEventReceiver interface - void onIOEvent(IIOEventSender *, const uint8_t *, void *, IOEventDataType); + void onIOEvent(IIOEventSender *, const char*, const char*, const uint8_t *, void *, IOEventDataType); void setOnEventCallback(IIOEventReceiver *); - RFReceiver getX10Receiver(){ return *x10Receiver; } - RFTransmitter getX10Transmitter(){ return *x10Transmitter; } - DS18B20 getTemperatureSensor(){ return *temperatureSensor; } - LightSensor getLightSensor(){ return *lightSensor; } - P1Port getExpansionPort(){ return *p1Port; } - private: // Diagnostics System::Diagnostics *systemDiagnostics; - //class X10ApiHandler; IIOEventReceiver *ioEventCallback; - // Instantiate the X10 RFReceiver Class - RFReceiverConfig *x10ReceiverConfig; - RFReceiver *x10Receiver; - // X10 RF RFReceiver and RFTransmitter objects - RFTransmitterConfig *x10TransmitterConfig; - RFTransmitter *x10Transmitter; - // DS18B20 Temperature sensor - DS18B20 *temperatureSensor; - // Light Sensor / PhotoResistor - LightSensor *lightSensor; - // P1Port GPIOs - P1Port *p1Port; + LinkedList eventSenders; }; } diff --git a/src/io/IOModule.cpp b/src/io/IOModule.cpp deleted file mode 100644 index 25bd7e3..0000000 --- a/src/io/IOModule.cpp +++ /dev/null @@ -1,9 +0,0 @@ -// -// Created by gene on 03/02/19. -// - -#include "IOModule.h" - -namespace IO { - -} \ No newline at end of file diff --git a/src/io/IOModule.h b/src/io/IOModule.h deleted file mode 100644 index 085d0b9..0000000 --- a/src/io/IOModule.h +++ /dev/null @@ -1,29 +0,0 @@ -// -// Created by gene on 03/02/19. -// - -#ifndef HOMEGENIE_MINI_IOMODULE_H -#define HOMEGENIE_MINI_IOMODULE_H - -#include - -#include - -namespace IO { - - using namespace Net; - - class IOModule { - public: - uint8_t Type = 0; - float Level = 0; - String UpdateTime; - IOModule() { - UpdateTime = NetManager::getTimeClient().getFormattedDate(); - } - }; - -} - - -#endif //HOMEGENIE_MINI_IOMODULE_H diff --git a/src/io/Logger.cpp b/src/io/Logger.cpp index 3b67497..c47d1b1 100644 --- a/src/io/Logger.cpp +++ b/src/io/Logger.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -29,7 +29,7 @@ #include "Logger.h" -#include +#include "net/NetManager.h" namespace IO { diff --git a/src/io/Logger.h b/src/io/Logger.h index 4809cb9..cb552e3 100644 --- a/src/io/Logger.h +++ b/src/io/Logger.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,7 +30,6 @@ #ifndef HOMEGENIE_MINI_LOGGER_H #define HOMEGENIE_MINI_LOGGER_H -#include #include #define FORMAT_STRING_VARGS char formatted[1024]; \ diff --git a/src/io/gpio/GPIOPort.cpp b/src/io/gpio/GPIOPort.cpp new file mode 100644 index 0000000..030abfd --- /dev/null +++ b/src/io/gpio/GPIOPort.cpp @@ -0,0 +1,74 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2019-02-03 Initial release + * + */ + +#include "GPIOPort.h" + +namespace IO { namespace GPIO { + + void GPIOPort::begin() {} + + void GPIOPort::setInput(uint8_t pinNumber, void(*callback)()) { + pinMode(pinNumber, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(pinNumber), callback, CHANGE); + } + void GPIOPort::setOutput(uint8_t pinNumber, uint8_t value) { + float level = (value/(GPIO_PORT_LEVEL_MAX - GPIO_PORT_LEVEL_MIN)); + // address is a private member inherited from `IOEventSender` superclass + String address = (String(pinNumber)).c_str(); + pinMode(pinNumber, OUTPUT); + if (value == GPIO_PORT_LEVEL_MAX) { + digitalWrite(pinNumber, HIGH); + } else if (value == GPIO_PORT_LEVEL_MIN) { + digitalWrite(pinNumber, LOW); + } else if (value > GPIO_PORT_LEVEL_MIN && value < GPIO_PORT_LEVEL_MAX) { + analogWrite(pinNumber, value); + } + Logger::info("@%s [%s %.f]", GPIO_PORT_NS_PREFIX, (IOEventPaths::Status_Level), level); + sendEvent(IO::IOEventDomains::HomeAutomation_HomeGenie, address.c_str(), (const uint8_t*)(IOEventPaths::Status_Level), &level, Float); + } + + + float GPIOPort::on(uint8_t pinNumber) { + float savedLevel = loadLastOnLevel(pinNumber); + savedLevel = savedLevel > 0 ? savedLevel : 1; + float gpioLevel = GPIO_PORT_LEVEL_MIN + (savedLevel * (GPIO_PORT_LEVEL_MAX - GPIO_PORT_LEVEL_MIN)); + setOutput(pinNumber, gpioLevel); + return savedLevel; + } + float GPIOPort::off(uint8_t pinNumber) { + setOutput(pinNumber, GPIO_PORT_LEVEL_MIN); + return 0; + } + float GPIOPort::level(uint8_t pinNumber, float level) { + float gpioLevel = GPIO_PORT_LEVEL_MIN + (level * (GPIO_PORT_LEVEL_MAX - GPIO_PORT_LEVEL_MIN) / 100); + setOutput(pinNumber, gpioLevel); + level = (gpioLevel - GPIO_PORT_LEVEL_MIN) / GPIO_PORT_LEVEL_MAX; + return level; + } +}} diff --git a/src/io/gpio/GPIOPort.h b/src/io/gpio/GPIOPort.h new file mode 100644 index 0000000..6ce3150 --- /dev/null +++ b/src/io/gpio/GPIOPort.h @@ -0,0 +1,103 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2019-02-03 Initial release + * + */ + +#ifndef HOMEGENIE_MINI_GPIOPORT_H +#define HOMEGENIE_MINI_GPIOPORT_H + +#ifndef ESP8266 +#include +#endif + +#include "Config.h" + +#include "io/IOEvent.h" +#include "io/IOEventDomains.h" +#include "io/IOEventPaths.h" +#include "io/Logger.h" + +#define GPIO_PORT_NS_PREFIX "IO::GPIO::GPIOPort" + +#define GPIO_PORT_LEVEL_MIN 0.0F +#define GPIO_PORT_LEVEL_MAX 254.0F + +namespace IO { namespace GPIO { + + class GPIOPort: public IIOEventSender { + public: + void begin() override; + void setInput(uint8_t pinNumber, void(*callback)()); + void setOutput(uint8_t pinNumber, uint8_t value); + float on(uint8_t pinNumber); + float off(uint8_t pinNumber); + float toggle(uint8_t pinNumber); + float level(uint8_t pinNumber, float level); + static float loadLevel(int pinNumber) { +#ifndef ESP8266 + Preferences preferences; + preferences.begin(CONFIG_SYSTEM_NAME, true); + float buttonState = preferences.getFloat((String("gpio:") + pinNumber + ":level").c_str(), 0); + preferences.end(); + return buttonState; +#else + return 0; +#endif + } + static float loadLastOnLevel(int pinNumber) { +#ifndef ESP8266 + Preferences preferences; + preferences.begin(CONFIG_SYSTEM_NAME, true); + float buttonState = preferences.getFloat((String("gpio:") + pinNumber + ":last").c_str(), 0); + preferences.end(); + return buttonState; +#else + return 0; +#endif + } + static void saveLevel(int pinNumber, float level) { +#ifndef ESP8266 + Preferences preferences; + preferences.begin(CONFIG_SYSTEM_NAME, false); + preferences.putFloat((String("gpio:") + pinNumber + ":level").c_str(), level); + preferences.end(); +#endif + } + static void saveLastOnLevel(int pinNumber, float level) { +#ifndef ESP8266 + Preferences preferences; + preferences.begin(CONFIG_SYSTEM_NAME, false); + preferences.putFloat((String("gpio:") + pinNumber + ":last").c_str(), level); + preferences.end(); +#endif + } + }; + +}} + + +#endif //HOMEGENIE_MINI_GPIOPORT_H diff --git a/src/io/gpio/P1Port.cpp b/src/io/gpio/P1Port.cpp deleted file mode 100644 index 67f6169..0000000 --- a/src/io/gpio/P1Port.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * HomeGenie-Mini (c) 2018-2019 G-Labs - * - * - * This file is part of HomeGenie-Mini (HGM). - * - * HomeGenie-Mini is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * HomeGenie-Mini is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with HomeGenie-Mini. If not, see . - * - * - * Authors: - * - Generoso Martello - * - * - * Releases: - * - 2019-02-03 Initial release - * - */ - -#include "P1Port.h" - -namespace IO { namespace GPIO { - - static uint8_t IOPin[] = { D5, D6, D7, D8 }; - - void P1Port::setOutput(uint8_t pinNumber, uint8_t value) { - auto pin = IOPin[pinNumber-1]; - float level = (value/P1PORT_GPIO_LEVEL_MAX); - // address is a private member inherited from `IOEventSender` superclass - address = (const uint8_t*)(String("D")+pinNumber).c_str(); - pinMode(pin, OUTPUT); - if (value == P1PORT_GPIO_LEVEL_MAX) { - digitalWrite(pin, HIGH); - } else if (value == P1PORT_GPIO_LEVEL_MIN) { - digitalWrite(pin, LOW); - } else if (value > P1PORT_GPIO_LEVEL_MIN && value < P1PORT_GPIO_LEVEL_MAX) { - analogWrite(pin, lround((PWMRANGE/P1PORT_GPIO_LEVEL_MAX)*value)); - } - Logger::info("@%s [%s %d]", P1PORT_NS_PREFIX, (IOEventPaths::Status_Level), String(level).c_str()); - sendEvent((const uint8_t*)(IOEventPaths::Sensor_Luminance), &level, Float); - } -}} diff --git a/src/io/sys/Diagnostics.cpp b/src/io/sys/Diagnostics.cpp index 5bf1cf5..730fb2b 100644 --- a/src/io/sys/Diagnostics.cpp +++ b/src/io/sys/Diagnostics.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -27,24 +27,28 @@ * */ -#include #include "Diagnostics.h" namespace IO { namespace System { Diagnostics::Diagnostics() { - // IEventSender members - domain = (const uint8_t *)(IOEventDomains::HomeAutomation_HomeGenie); - address = (const uint8_t*)HOMEGENIE_BUILTIN_MODULE_ADDRESS; // update interval setLoopInterval(DIAGNOSTICS_SAMPLING_RATE); } + void Diagnostics::begin() { + + } + void Diagnostics::loop() { +#ifdef ESP8266 uint32_t freeMem = system_get_free_heap_size(); +#else + uint32_t freeMem = esp_get_free_heap_size(); +#endif if (currentFreeMemory != freeMem) { Logger::trace("@%s [%s %lu]", DIAGNOSTICS_NS_PREFIX, (IOEventPaths::System_BytesFree), freeMem, UnsignedNumber); - sendEvent((const uint8_t*)(IOEventPaths::System_BytesFree), &freeMem, UnsignedNumber); + sendEvent(domain.c_str(), address.c_str(), (const uint8_t*)(IOEventPaths::System_BytesFree), &freeMem, UnsignedNumber); currentFreeMemory = freeMem; } } diff --git a/src/io/sys/Diagnostics.h b/src/io/sys/Diagnostics.h index 3e5bf0e..adb88b8 100644 --- a/src/io/sys/Diagnostics.h +++ b/src/io/sys/Diagnostics.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,15 +30,17 @@ #ifndef HOMEGENIE_MINI_DIAGNOSTICS_H #define HOMEGENIE_MINI_DIAGNOSTICS_H +#ifdef ESP8266 extern "C" { #include "user_interface.h" } +#endif -#include -#include -#include -#include -#include +#include "Task.h" +#include "io/Logger.h" +#include "io/IOEvent.h" +#include "io/IOEventDomains.h" +#include "io/IOEventPaths.h" #define DIAGNOSTICS_NS_PREFIX "IO::Sys::Diagnostics" #define DIAGNOSTICS_SAMPLING_RATE 5000L @@ -48,9 +50,12 @@ namespace IO { namespace System { class Diagnostics : Task, public IIOEventSender { public: Diagnostics(); + void begin(); void loop(); private: + String domain = IOEventDomains::HomeAutomation_HomeGenie; + String address = CONFIG_BUILTIN_MODULE_ADDRESS; uint32_t currentFreeMemory; }; diff --git a/src/main.cpp b/src/main.cpp index 54c1554..befe04f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -23,106 +23,30 @@ * * * Releases: - * - 2019-01-10 Initial release + * - 2019-01-10 v1.0: initial release. + * - 2023-12-08 v1,1: added BLE support; targeting ESP32 by default. * */ -#include +#include "HomeGenie.h" -#include -#include -#include -#include -#include -#include -#include - -#define HOMEGENIE_MINI_VERSION "1.0" - -using namespace IO; using namespace Net; using namespace Service; -HomeGenie homeGenie; +HomeGenie* homeGenie; -volatile int64_t buttonPressStart = -1; -volatile bool buttonPressed = false; -void buttonChange() { - buttonPressed = (digitalRead(Config::ServiceButtonPin) == LOW); - if (buttonPressed) { - buttonPressStart = millis(); - } -} -void checkServiceButton() { - int64_t elapsed = 0; - if (buttonPressed) { - // released - elapsed = millis() - buttonPressStart; - if (elapsed > Config::WpsModePushInterval) { - noInterrupts(); - homeGenie.getNetManager().getWiFiManager().startWPS(); - interrupts(); - } - } -} -bool statusLedOn = false; -uint64_t statusLedTs = 0; -void statusLedLoop() { - if (WiFi.isConnected()) { - // when connected the led will blink quickly every 2 seconds - if (millis() - statusLedTs > 1950 && !statusLedOn) { - statusLedOn = true; - digitalWrite(Config::StatusLedPin, HIGH); - statusLedTs = millis(); - } else if (statusLedOn && millis() - statusLedTs > 50) { - statusLedOn = false; - digitalWrite(Config::StatusLedPin, LOW); - statusLedTs = millis(); - } - } else { - // if not connected the led will blink quickly every 200ms - if (millis() - statusLedTs > 100 && !statusLedOn) { - statusLedOn = true; - digitalWrite(Config::StatusLedPin, HIGH); - statusLedTs = millis(); - } else if (statusLedOn && millis() - statusLedTs > 100) { - statusLedOn = false; - digitalWrite(Config::StatusLedPin, LOW); - statusLedTs = millis(); - } - } -} - -/// This gets called just before the main application loop() void setup() { - // Setup status led - pinMode(Config::StatusLedPin, OUTPUT); - // Setup button - pinMode(Config::ServiceButtonPin, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(Config::ServiceButtonPin), buttonChange, CHANGE); - - // Logger initialization - Logger::begin(LOG_LEVEL_TRACE); + homeGenie = HomeGenie::getInstance(); - // Welcome message - Logger::info("HomeGenie-Mini %s", HOMEGENIE_MINI_VERSION); - Logger::info("Booting..."); + // TODO: configure your I/O and API handlers here + // See examples folder. - Logger::info("+ Starting HomeGenie service"); - homeGenie.begin(); - - Logger::info("READY."); + homeGenie->begin(); } -/// Main application loop void loop() { - statusLedLoop(); - checkServiceButton(); - // TODO: sort of system load index could be obtained by measuring time elapsed for `TaskManager::loop()` method - TaskManager::loop(); + homeGenie->loop(); } - -////////////////////////////////////////// diff --git a/src/net/BLEManager.cpp b/src/net/BLEManager.cpp new file mode 100644 index 0000000..8819760 --- /dev/null +++ b/src/net/BLEManager.cpp @@ -0,0 +1,130 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + * + * Releases: + * - 2023-12-08 Initial release + * + */ + +#include "BLEManager.h" + +#ifndef DISABLE_BLE + +#define BLEMANAGER_NS_PREFIX "Net::BLEManager" + +#define SERVICE_UUID "033214d2-0ff0-4cba-814e-c5074c1ad00c" +#define CHARACTERISTIC_UUID "ac6744a7-77f3-43e9-b3c8-9955ac6bb0d4" + +namespace Net { + + uint8_t devicesConnected = 0; + uint8_t currentClients = 0; + + class CharacteristicCallbacks : public BLECharacteristicCallbacks { + void onWrite(NimBLECharacteristic *characteristic) { + Serial.println("onWrite"); + std::string value = characteristic->getValue(); + + Serial.println(value.c_str()); + } + + void onRead(NimBLECharacteristic *characteristic) { + Serial.println("onRead"); + characteristic->setValue("Hello"); + } + }; + + class ServerCallbacks : public BLEServerCallbacks { + void onConnect(NimBLEServer *server) { + Serial.println("Client connected"); + devicesConnected++; + advertising->start(); + } + + void onDisconnect(NimBLEServer *server) { + Serial.println("Client disconnected"); + devicesConnected--; + } + }; + + BLEManager::BLEManager() + { + setLoopInterval(1000); + } + + void BLEManager::begin() { + + NimBLEDevice::init(CONFIG_BUILTIN_MODULE_NAME); + NimBLEServer *server = NimBLEDevice::createServer(); + + server->setCallbacks(new ServerCallbacks()); + + NimBLEService *service = server->createService(SERVICE_UUID); + + characteristic = service->createCharacteristic(CHARACTERISTIC_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::INDICATE); +/* characteristic->createDescriptor("ABCD", + NIMBLE_PROPERTY::READ | + NIMBLE_PROPERTY::WRITE | + NIMBLE_PROPERTY::WRITE_ENC, + 25);*/ + characteristic->setCallbacks(new CharacteristicCallbacks()); + + service->start(); + + advertising = NimBLEDevice::getAdvertising(); + advertising->addServiceUUID(SERVICE_UUID); + advertising->setScanResponse(false); + advertising->setMinPreferred(0x0); + advertising->start(); + + IO::Logger::info("| ✔ BLE enabled"); + + } + + void BLEManager::loop() { + + if (devicesConnected != currentClients) { + currentClients = devicesConnected; + IO::Logger::info("@%s [%s %d]", BLEMANAGER_NS_PREFIX, "Clients:", currentClients); +// Serial.println("Notifying devices"); +// characteristic->setValue("Hello connected devices!"); +// characteristic->notify(); + } + + } + + void BLEManager::addHandler(IO::IIOEventReceiver* handler) { + // TODO: .... + //handler->onIOEvent(this, 0, 0); + + // TODO: .... + // TODO: .... + // TODO: .... + + + } + +} // Net + +#endif \ No newline at end of file diff --git a/src/io/gpio/P1Port.h b/src/net/BLEManager.h similarity index 53% rename from src/io/gpio/P1Port.h rename to src/net/BLEManager.h index a381b70..3ba11bd 100644 --- a/src/io/gpio/P1Port.h +++ b/src/net/BLEManager.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -23,38 +23,35 @@ * * * Releases: - * - 2019-02-03 Initial release + * - 2023-12-08 Initial release * */ -#ifndef HOMEGENIE_MINI_P1PORT_H -#define HOMEGENIE_MINI_P1PORT_H +#include -#include -#include -#include -#include -#include -#include +#ifndef DISABLE_BLE + +#ifndef HOMEGENIE_MINI_BLEMANAGER_H +#define HOMEGENIE_MINI_BLEMANAGER_H -#define P1PORT_NS_PREFIX "IO::GPIO::P1Port" +#include +#include -#define P1PORT_GPIO_COUNT 4 -#define P1PORT_GPIO_LEVEL_MIN 0.0F -#define P1PORT_GPIO_LEVEL_MAX 100.0F +static NimBLECharacteristic *characteristic; +static NimBLEAdvertising *advertising; -namespace IO { namespace GPIO { +namespace Net { - class P1Port: public IIOEventSender { + class BLEManager : Task, IO::IIOEventSender { public: - P1Port() { - // IEventSender members - domain = (const uint8_t*)IOEventDomains::HomeAutomation_HomeGenie; - } - void setOutput(uint8_t pinNumber, uint8_t value); + BLEManager(); + void begin() override; + void loop() override; + void addHandler(IO::IIOEventReceiver* handler); }; -}} +} // Net +#endif //HOMEGENIE_MINI_BLEMANAGER_H -#endif //HOMEGENIE_MINI_P1PORT_H +#endif diff --git a/src/net/HTTPServer.cpp b/src/net/HTTPServer.cpp index 5ab7182..0b32a12 100644 --- a/src/net/HTTPServer.cpp +++ b/src/net/HTTPServer.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -29,71 +29,89 @@ #include "HTTPServer.h" -#include -#include +#include "net/NetManager.h" +#include "service/EventRouter.h" namespace Net { using namespace IO; using namespace Service; - const char SSDP_Description[] PROGMEM = "description.xml"; - const char SSDP_Name[] PROGMEM = "HomeGenie:mini V1.0"; - const char SSDP_SerialNumber[] PROGMEM = "ABC0123456789"; - const char SSDP_ModelName[] PROGMEM = "HomeGenie:mini 2019"; - const char SSDP_ModelNumber[] PROGMEM = "2134567890"; - const char SSDP_ModelURL[] PROGMEM = "http://homegenie.it"; + const char SSDP_SchemaURL[] PROGMEM = "description.xml"; + const char SSDP_Name[] PROGMEM = CONFIG_DEVICE_MODEL_NAME; + const char SSDP_SerialNumber[] PROGMEM = CONFIG_DEVICE_SERIAL_NUMBER; + const char SSDP_ModelName[] PROGMEM = CONFIG_DEVICE_MODEL_NAME; + const char SSDP_ModelNumber[] PROGMEM = CONFIG_DEVICE_MODEL_NUMBER; + const char SSDP_ModelDescription[] PROGMEM = "HomeGenie Mini Device"; + const char SSDP_ModelURL[] PROGMEM = "https://homegenie.it"; const char SSDP_Manufacturer[] PROGMEM = "G-Labs"; const char SSDP_ManufacturerURL[] PROGMEM = "https://glabs.it"; - static ESP8266WebServer httpServer(HTTP_SERVER_PORT); + static WebServer httpServer(HTTP_SERVER_PORT); LinkedList wifiClients; LinkedList events; - HTTPServer::HTTPServer() { - - } + String ipAddress; void HTTPServer::begin() { httpServer.on("/start.html", HTTP_GET, []() { httpServer.send(200, "text/plain", "Hello World!"); }); httpServer.on("/description.xml", HTTP_GET, []() { - SSDP.schema(httpServer.client()); + SSDPDevice.schema(httpServer.client()); }); static HTTPServer* i = this; httpServer.on("/api/HomeAutomation.HomeGenie/Logging/RealTime.EventStream/", HTTP_GET, []() { - WiFiClient sseClient = httpServer.client(); - wifiClients.add(sseClient); - // TODO: check out this "keepAlive" - sseClient.keepAlive(65535); - i->serverSentEventHeader(sseClient); - //sseClient.flush(); - // connection: CLOSE + i->sseClientAccept(); + }); + // alias of "../Logging/RealTime.EventStream" + httpServer.on("/events", HTTP_GET, []() { + i->sseClientAccept(); }); httpServer.addHandler(this); httpServer.begin(); Logger::info("| ✔ HTTP service"); - SSDP.setSchemaURL(FPSTR(SSDP_Description)); - SSDP.setHTTPPort(80); - SSDP.setName(FPSTR(SSDP_Name)); - SSDP.setSerialNumber(FPSTR(SSDP_SerialNumber)); - SSDP.setURL(WiFi.localIP().toString()); - SSDP.setModelName(FPSTR(SSDP_ModelName)); - SSDP.setModelNumber(FPSTR(SSDP_ModelNumber)); - SSDP.setModelURL(FPSTR(SSDP_ModelURL)); - SSDP.setManufacturer(FPSTR(SSDP_Manufacturer)); - SSDP.setManufacturerURL(FPSTR(SSDP_ManufacturerURL)); - SSDP.begin(); - Logger::info("| ✔ SSDP service"); +// ipAddress = WiFi.localIP().toString(); } void HTTPServer::loop() { Logger::verbose("%s loop() >> BEGIN", HTTPSERVER_LOG_PREFIX); + + String localIP = WiFi.localIP().toString(); + if (!ipAddress.equals(localIP) && !localIP.equals("0.0.0.0")) { + ipAddress = localIP; + //Logger::info("| ✔ New IP address %s", ipAddress.c_str()); + + SSDPDevice.setSchemaURL(FPSTR(SSDP_SchemaURL)); + SSDPDevice.setHTTPPort(80); + SSDPDevice.setName(FPSTR(SSDP_Name)); + SSDPDevice.setSerialNumber(FPSTR(SSDP_SerialNumber)); + SSDPDevice.setURL(ipAddress); + SSDPDevice.setModelName(FPSTR(SSDP_ModelName)); + SSDPDevice.setModelNumber(FPSTR(SSDP_ModelNumber)); + SSDPDevice.setModelDescription(FPSTR(SSDP_ModelDescription)); + SSDPDevice.setModelURL(FPSTR(SSDP_ModelURL)); + SSDPDevice.setManufacturer(FPSTR(SSDP_Manufacturer)); + SSDPDevice.setManufacturerURL(FPSTR(SSDP_ManufacturerURL)); + +#ifndef CONFIGURE_WITH_WPA + // Read friendly name from prefs + Preferences preferences; + preferences.begin(CONFIG_SYSTEM_NAME, true); + String friendlyName = preferences.getString("device:name", CONFIG_BUILTIN_MODULE_NAME); + preferences.end(); + SSDPDevice.setFriendlyName(friendlyName); + Logger::info("| ✔ UPnP friendly name: %s", friendlyName.c_str()); +#endif + + Logger::info("| ✔ SSDP service"); + } + httpServer.handleClient(); + SSDPDevice.handleClient(); // TODO: "if (millis() % 50 == 0) ..." lower priority routine if (events.size() > 0) { @@ -118,7 +136,8 @@ namespace Net { bool HTTPServer::canHandle(HTTPMethod method, String uri) { return false; } - bool HTTPServer::handle(ESP8266WebServer& server, HTTPMethod requestMethod, String requestUri) { + + bool HTTPServer::handle(WebServer& server, HTTPMethod requestMethod, String requestUri) { return false; } // END RequestHandler interface methods @@ -155,4 +174,12 @@ namespace Net { client.println(); //client.flush(); } + + void HTTPServer::sseClientAccept() { + WiFiClient sseClient = httpServer.client(); + wifiClients.add(sseClient); + this->serverSentEventHeader(sseClient); + //sseClient.flush(); + // connection: CLOSE + } } diff --git a/src/net/HTTPServer.h b/src/net/HTTPServer.h index eba2b2e..4724b2c 100644 --- a/src/net/HTTPServer.h +++ b/src/net/HTTPServer.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,14 +30,19 @@ #ifndef HOMEGENIE_MINI_HTTPSERVER_H #define HOMEGENIE_MINI_HTTPSERVER_H +#ifdef ESP8266 +#define WebServer ESP8266WebServer #include -#include -#include +#else +#include +#endif +#include #include -#include -#include +#include "Task.h" + +#include "SSDPDevice.h" #define HTTPSERVER_LOG_PREFIX "@Net::HTTPServer" #define HTTP_SERVER_PORT 80 @@ -47,15 +52,15 @@ namespace Net { /// Implements HTTP and SSDP services class HTTPServer : Task, RequestHandler { public: - HTTPServer(); void begin(); - void loop(); + void loop() override; void addHandler(RequestHandler* handler); // RequestHandler interface methods - bool canHandle(HTTPMethod method, String uri); - bool handle(ESP8266WebServer& server, HTTPMethod requestMethod, String requestUri); + bool canHandle(HTTPMethod method, String uri) override; + bool handle(WebServer& server, HTTPMethod requestMethod, String requestUri) override; void sendSSEvent(String domain, String address, String event, String value); private: + void sseClientAccept(); void serverSentEventHeader(WiFiClient &client); void serverSentEvent(WiFiClient &client, String &domain, String &address, String &event, String &value); }; diff --git a/src/net/MQTTServer.cpp b/src/net/MQTTServer.cpp index d4c8024..b9959b2 100644 --- a/src/net/MQTTServer.cpp +++ b/src/net/MQTTServer.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). diff --git a/src/net/MQTTServer.h b/src/net/MQTTServer.h index 3f17829..75ce296 100644 --- a/src/net/MQTTServer.h +++ b/src/net/MQTTServer.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,13 +30,11 @@ #ifndef HOMEGENIE_MINI_MQTTSERVER_H #define HOMEGENIE_MINI_MQTTSERVER_H -#include - #include #include -#include -#include +#include "Task.h" +#include "net/mqtt/MQTTBrokerMini.h" namespace Net { using namespace MQTT; @@ -45,15 +43,15 @@ namespace Net { class MQTTServer : Task { public: void begin(); - void loop(); + void loop() override; void broadcast(String *topic, String *payload); static void webSocketEventStatic(uint8_t num, WStype_t type, uint8_t * payload, size_t length); static void mqttCallbackStatic(uint8_t num, Events_t event, String topic_name, uint8_t * payload, uint16_t length_payload); private: - WebSocketsServer *webSocket; - MQTTBrokerMini *mqttBroker; + WebSocketsServer *webSocket = nullptr; + MQTTBrokerMini *mqttBroker = nullptr; }; } diff --git a/src/net/NetManager.cpp b/src/net/NetManager.cpp index 7fd15c6..e6576ff 100644 --- a/src/net/NetManager.cpp +++ b/src/net/NetManager.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -36,15 +36,39 @@ namespace Net { // Time sync WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP); +#ifdef ESP32 + ESP32Time rtc(0); +#endif // Variables to save date and time String formattedDate; String dayStamp; String timeStamp; +#ifdef ESP32 + bool rtcTimeSet = (esp_reset_reason() != ESP_RST_POWERON && esp_reset_reason() != ESP_RST_UNKNOWN); +#else + bool rtcTimeSet = false; +#endif + long lastTimeCheck = -100000; +#ifndef CONFIGURE_WITH_WPA + BluetoothSerial SerialBT; +#endif + + NetManager::NetManager() { + // TODO: ... + } + NetManager::~NetManager() { + // TODO: !!!! IMPLEMENT DESTRUCTOR AS WELL FOR HttpServer and MQTTServer classes + delete httpServer; + delete mqttServer; + delete webSocket; + } bool NetManager::begin() { Logger::info("+ Starting NetManager"); - +#ifndef DISABLE_BLE + bleManager = new BLEManager(); +#endif wiFiManager = new WiFiManager(); bool wpsSuccess = wiFiManager->checkWiFiStatus(); @@ -83,28 +107,112 @@ namespace Net { // GMT 0 = 0 timeClient.setTimeOffset(0); + +#ifndef CONFIGURE_WITH_WPA + Preferences preferences; + preferences.begin(CONFIG_SYSTEM_NAME, true); + String ssid = preferences.getString("wifi:ssid"); + String pass = preferences.getString("wifi:password"); + // start SerialBT only if Wi-Fi is not configured + if (ssid.isEmpty() || pass.isEmpty()) { + SerialBT.begin(CONFIG_BUILTIN_MODULE_NAME); + } + preferences.end(); +#endif + + + return wpsSuccess; } void NetManager::loop() { Logger::verbose("%s loop() >> BEGIN", NETMANAGER_LOG_PREFIX); +#ifndef CONFIGURE_WITH_WPA + + // CONFIGURE WITH BLUETOOTH + if (SerialBT.available()) { + String message = SerialBT.readStringUntil('\n'); + if (message != nullptr) { + + Serial.println(message); + + // ECHO? --> SerialBT.println(message); + + Preferences preferences; + preferences.begin(CONFIG_SYSTEM_NAME, false); + + if (message.startsWith("#CONFIG:device-name ")) { + preferences.putString("device:name", message.substring(20)); + } + if (message.startsWith("#CONFIG:wifi-ssid ")) { + preferences.putString("wifi:ssid", message.substring(18)); + } + if (message.startsWith("#CONFIG:wifi-password ")) { + preferences.putString("wifi:password", message.substring(22)); + } + if (message.startsWith("#CONFIG:system-time ")) { + String time = message.substring(20); + long seconds = time.substring(0, time.length() - 3).toInt(); + long ms = time.substring(time.length() - 3).toInt(); + rtc.setTime(seconds, ms); + } + + preferences.end(); + + if (message.equals("#RESET")) { + SerialBT.disconnect(); + SerialBT.end(); + IO::Logger::info("RESET!"); + delay(2000); + esp_restart(); + } + + } + } + +#endif + webSocket->loop(); - if (WiFi.isConnected() && !timeClient.update()) { - digitalWrite(Config::StatusLedPin, HIGH); - timeClient.forceUpdate(); - // The formattedDate comes with the following format: - // 2018-05-28T16:00:13Z - // We need to extract date and time - formattedDate = timeClient.getFormattedDate(); - Logger::info("NTP Time: %s", formattedDate.c_str()); - digitalWrite(Config::StatusLedPin, LOW); + if (rtcTimeSet) { +#ifdef ESP32 + if (millis() - lastTimeCheck > 60000) { + lastTimeCheck = millis(); + if (!timeClient.isUpdated()) { + // sync TimeClient with RTC + timeClient.setEpochTime(rtc.getLocalEpoch()); + Logger::info("| - TimeClient: synced with RTC"); + } + } +#endif + } else if (WiFi.isConnected() && WiFi.status() == WL_CONNECTED && millis() - lastTimeCheck > 60000) { + lastTimeCheck = millis(); + if (!timeClient.isUpdated()) { + if (timeClient.update()) { + // TimeClient synced with NTP +#ifdef ESP32 + rtc.setTime(timeClient.getEpochTime(), 0); + rtcTimeSet = true; + Logger::info("| - RTC updated via TimeClient (NTP)"); +#endif + } else { + // NTP Update failed + digitalWrite(Config::StatusLedPin, HIGH); + Logger::warn("| x TimeClient: NTP update failed!"); + } + } } Logger::verbose("%s loop() << END", NETMANAGER_LOG_PREFIX); } +#ifndef DISABLE_BLE + BLEManager& NetManager::getBLEManager() { + return *bleManager; + } +#endif + WiFiManager& NetManager::getWiFiManager() { return *wiFiManager; } @@ -121,16 +229,6 @@ namespace Net { return *webSocket; } - NetManager::NetManager() { - // TODO: ... - } - NetManager::~NetManager() { - // TODO: !!!! IMPLEMENT DESTRUCTOR AS WELL FOR HttpServer and MQTTServer classes - delete httpServer; - delete mqttServer; - delete webSocket; - } - NTPClient& NetManager::getTimeClient() { return timeClient; } diff --git a/src/net/NetManager.h b/src/net/NetManager.h index 1ca653f..cf69d6e 100644 --- a/src/net/NetManager.h +++ b/src/net/NetManager.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,7 +30,14 @@ #ifndef HOMEGENIE_MINI_NETMANAGER_H #define HOMEGENIE_MINI_NETMANAGER_H +#ifdef ESP8266 #include +#else +#include +#ifdef ESP32 +#include +#endif +#endif #include #include #include @@ -38,12 +45,18 @@ #include #include -#include -#include -#include -#include -#include -#include +#include "Config.h" +#include "net/HTTPServer.h" +#include "net/MQTTServer.h" +#include "net/WiFiManager.h" + +#ifndef DISABLE_BLE +#include +#endif +#ifndef CONFIGURE_WITH_WPA +#include +#include +#endif #define NETMANAGER_LOG_PREFIX "@Net::NetManager" @@ -55,7 +68,10 @@ namespace Net { NetManager(); ~NetManager(); bool begin(); - void loop(); + void loop() override; +#ifndef DISABLE_BLE + BLEManager& getBLEManager(); +#endif WiFiManager& getWiFiManager(); HTTPServer& getHttpServer(); MQTTServer& getMQTTServer(); @@ -63,6 +79,9 @@ namespace Net { static NTPClient& getTimeClient(); private: +#ifndef DISABLE_BLE + BLEManager *bleManager; +#endif WiFiManager *wiFiManager; HTTPServer *httpServer; MQTTServer *mqttServer; diff --git a/src/net/SSDPDevice.cpp b/src/net/SSDPDevice.cpp new file mode 100644 index 0000000..58c7cfa --- /dev/null +++ b/src/net/SSDPDevice.cpp @@ -0,0 +1,463 @@ +// based on https://github.com/esp8266/Arduino/issues/2283 +// https://github.com/esp8266/Arduino/files/980894/SSDPDevice.zip +// by Pawel Dino + + +#include "SSDPDevice.h" + +static const char* PROGMEM SSDP_RESPONSE_TEMPLATE = + "HTTP/1.1 200 OK\r\n" + "EXT:\r\n"; + +static const char* PROGMEM SSDP_NOTIFY_ALIVE_TEMPLATE = + "NOTIFY * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "NTS: ssdp:alive\r\n"; + +static const char* PROGMEM SSDP_NOTIFY_UPDATE_TEMPLATE = + "NOTIFY * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "NTS: ssdp:update\r\n"; + +static const char* PROGMEM SSDP_PACKET_TEMPLATE = + "%s" // _ssdp_response_template / _ssdp_notify_template + "CACHE-CONTROL: max-age=%u\r\n" // SSDP_INTERVAL + "SERVER: UPNP/1.1 %s/%s\r\n" // m_modelName, m_modelNumber + "USN: %s%s%s\r\n" // m_uuid + "%s: %s\r\n" // "NT" or "ST", m_deviceType + "LOCATION: http://%u.%u.%u.%u:%u/%s\r\n" // WiFi.localIP(), m_port, m_schemaURL + "\r\n"; + +static const char* PROGMEM SSDP_SCHEMA_TEMPLATE = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/xml\r\n" + "Connection: close\r\n" + "Access-Control-Allow-Origin: *\r\n" + "\r\n" + "" + "" + "" + "1" + "0" + "" + "http://%u.%u.%u.%u:%u/%s" // WiFi.localIP(), _port + "" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "%s" + "uuid:%s" + "" + // "" + // "" + // "image/png" + // "48" + // "48" + // "24" + // "icon48.png" + // "" + // "" + // "image/png" + // "120" + // "120" + // "24" + // "icon120.png" + // "" + // "" + "\r\n" + "\r\n"; + +SSDPDeviceClass::SSDPDeviceClass() : + m_server(0), + m_port(80), + m_ttl(SSDP_MULTICAST_TTL) +{ + m_uuid[0] = '\0'; + m_modelNumber[0] = '\0'; + sprintf(m_deviceType, "urn:schemas-upnp-org:device:Basic:1"); + m_friendlyName[0] = '\0'; + m_presentationURL[0] = '\0'; + m_serialNumber[0] = '\0'; + m_modelName[0] = '\0'; + m_modelURL[0] = '\0'; + m_manufacturer[0] = '\0'; + m_manufacturerURL[0] = '\0'; + sprintf(m_schemaURL, "ssdp/schema.xml"); + +#ifdef ESP8266 + uint32_t chipId = ESP.getChipId(); +#else + uint32_t chipId = ESP.getEfuseMac(); +#endif + + sprintf(m_uuid, "38323636-4558-4dda-9188-cda0e6%02x%02x%02x", + (uint16_t)((chipId >> 16) & 0xff), + (uint16_t)((chipId >> 8) & 0xff), + (uint16_t)chipId & 0xff); + + for (int i = 0; i < SSDP_QUEUE_SIZE; i++) { + m_queue[i].time = 0; + } +} + +void SSDPDeviceClass::update() { + postNotifyUpdate(); +} + +bool SSDPDeviceClass::readLine(String &value) { + char buffer[65]; + int bufferPos = 0; + + while (1) { + int c = m_server->read(); + + if (c < 0) { + buffer[bufferPos] = '\0'; + + break; + } + if (c == '\r' && m_server->peek() == '\n') { + m_server->read(); + + buffer[bufferPos] = '\0'; + + break; + } + if (bufferPos < 64) { + buffer[bufferPos++] = c; + } + } + + value = String(buffer); + + return bufferPos > 0; +} + +bool SSDPDeviceClass::readKeyValue(String &key, String &value) { + char buffer[65]; + int bufferPos = 0; + + while (1) { + int c = m_server->read(); + + if (c < 0) { + if (bufferPos == 0) return false; + + buffer[bufferPos] = '\0'; + + break; + } + if (c == ':') { + buffer[bufferPos] = '\0'; + + while (m_server->peek() == ' ') m_server->read(); + + break; + } + else if (c == '\r' && m_server->peek() == '\n') { + m_server->read(); + + if (bufferPos == 0) return false; + + buffer[bufferPos] = '\0'; + + key = String(); + value = String(buffer); + + return true; + } + if (bufferPos < 64) { + buffer[bufferPos++] = c; + } + } + + key = String(buffer); + + readLine(value); + + return true; +} + +void SSDPDeviceClass::postNotifyALive() { + unsigned long time = millis(); + + post(NOTIFY_ALIVE_INIT, ROOT_FOR_ALL, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 10); + post(NOTIFY_ALIVE_INIT, ROOT_BY_UUID, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 55); + post(NOTIFY_ALIVE_INIT, ROOT_BY_TYPE, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 80); + + post(NOTIFY_ALIVE_INIT, ROOT_FOR_ALL, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 210); + post(NOTIFY_ALIVE_INIT, ROOT_BY_UUID, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 255); + post(NOTIFY_ALIVE_INIT, ROOT_BY_TYPE, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 280); + + post(NOTIFY_ALIVE, ROOT_FOR_ALL, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 610); + post(NOTIFY_ALIVE, ROOT_BY_UUID, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 655); + post(NOTIFY_ALIVE, ROOT_BY_TYPE, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 680); +} + +void SSDPDeviceClass::postNotifyUpdate() { + unsigned long time = millis(); + + post(NOTIFY_UPDATE, ROOT_FOR_ALL, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 10); + post(NOTIFY_UPDATE, ROOT_BY_UUID, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 55); + post(NOTIFY_UPDATE, ROOT_BY_TYPE, SSDP_MULTICAST_ADDR, SSDP_PORT, time + 80); +} + +void SSDPDeviceClass::postResponse(long mx) { + unsigned long time = millis(); + unsigned long delay = random(0, mx) * 900L; // 1000 ms - 100 ms + + IPAddress address = m_server->remoteIP(); + uint16_t port = m_server->remotePort(); + + post(RESPONSE, ROOT_FOR_ALL, address, port, time + delay / 3); + post(RESPONSE, ROOT_BY_UUID, address, port, time + delay / 3 * 2); + post(RESPONSE, ROOT_BY_TYPE, address, port, time + delay); +} + +void SSDPDeviceClass::postResponse(ssdp_udn_t udn, long mx) { + post(RESPONSE, udn, m_server->remoteIP(), m_server->remotePort(), millis() + random(0, mx) * 900L); // 1000 ms - 100 ms +} + +void SSDPDeviceClass::post(ssdp_message_t type, ssdp_udn_t udn, IPAddress address, uint16_t port, unsigned long time) { + for (int i = 0; i < SSDP_QUEUE_SIZE; i++) { + if (m_queue[i].time == 0) { + m_queue[i].type = type; + m_queue[i].udn = udn; + m_queue[i].address = address; + m_queue[i].port = port; + m_queue[i].time = time; + + break; + } + } +} + +void SSDPDeviceClass::send(ssdp_send_parameters_t *parameters) { + uint8_t buffer[1460]; + unsigned int ip = WiFi.localIP(); + + const char *typeTemplate; + const char *uri, *usn1, *usn2, *usn3; + + switch (parameters->type) { + case NOTIFY_ALIVE_INIT: + case NOTIFY_ALIVE: + typeTemplate = SSDP_NOTIFY_ALIVE_TEMPLATE; + break; + case NOTIFY_UPDATE: + typeTemplate = SSDP_NOTIFY_UPDATE_TEMPLATE; + break; + default: // RESPONSE + typeTemplate = SSDP_RESPONSE_TEMPLATE; + break; + } + + String uuid = "uuid:" + String(m_uuid); + + switch (parameters->udn) { + case ROOT_FOR_ALL: + uri = "upnp:rootdevice"; + usn1 = uuid.c_str(); + usn2 = "::"; + usn3 = "upnp:rootdevice"; + break; + case ROOT_BY_UUID: + uri = uuid.c_str(); + usn1 = uuid.c_str(); + usn2 = ""; + usn3 = ""; + break; + case ROOT_BY_TYPE: + uri = m_deviceType; + usn1 = uuid.c_str(); + usn2 = "::"; + usn3 = m_deviceType; + break; + } + + int len = snprintf_P((char *)buffer, sizeof(buffer), + SSDP_PACKET_TEMPLATE, typeTemplate, + SSDP_INTERVAL, m_modelName, m_modelNumber, usn1, usn2, usn3, parameters->type == RESPONSE ? "ST" : "NT", uri, + LIP2STR(&ip), m_port, m_schemaURL + ); + + if (parameters->address == (uint32_t) SSDP_MULTICAST_ADDR) { +#ifdef ESP8266 + m_server->beginPacketMulticast(parameters->address, parameters->port, m_ttl); +#else + m_server->beginMulticast(parameters->address, parameters->port); + m_server->beginMulticastPacket(); +#endif + } else { + m_server->beginPacket(parameters->address, parameters->port); + } + + m_server->write(buffer, len); + m_server->endPacket(); + + parameters->time = parameters->type == NOTIFY_ALIVE ? parameters->time + SSDP_INTERVAL * 900L : 0; // 1000 ms - 100 ms +} + +void SSDPDeviceClass::schema(WiFiClient client) { + uint32_t ip = WiFi.localIP(); + client.printf(SSDP_SCHEMA_TEMPLATE, + LIP2STR(&ip), m_port, m_schemaURL, + m_deviceType, + m_friendlyName, + m_presentationURL, + m_serialNumber, + m_modelName, + m_modelNumber, + m_modelDescription, + m_modelURL, + m_manufacturer, + m_manufacturerURL, + m_uuid + ); +} + +void SSDPDeviceClass::handleClient() { + IPAddress current = WiFi.localIP(); + + if (m_last != current) { + m_last = current; + + for (int i = 0; i < SSDP_QUEUE_SIZE; i++) { + m_queue[i].time = 0; + } + + if (current != INADDR_NONE) { + if (!m_server) m_server = new WiFiUDP(); + +#ifdef ESP8266 + m_server->beginMulticast(current, SSDP_MULTICAST_ADDR, SSDP_PORT); +#else + m_server->beginMulticast(SSDP_MULTICAST_ADDR, SSDP_PORT); + m_server->beginMulticastPacket(); +#endif + postNotifyALive(); + } + else if (m_server) { + m_server->stop(); + } + } + + if (m_server && m_server->parsePacket()) { + String value; + + if (readLine(value) && value.equalsIgnoreCase("M-SEARCH * HTTP/1.1")) { + String key, st; + bool host = false, man = false; + long mx = 0; + + while (readKeyValue(key, value)) { + if (key.equalsIgnoreCase("HOST") && value.equals("239.255.255.250:1900")) { + host = true; + } + else if (key.equalsIgnoreCase("MAN") && value.equals("\"ssdp:discover\"")) { + man = true; + } + else if (key.equalsIgnoreCase("ST")) { + st = value; + } + else if (key.equalsIgnoreCase("MX")) { + mx = value.toInt(); + } + } + + if (host && man && mx > 0) { + if (st.equals("ssdp:all")) { + postResponse(mx); + } + else if (st.equals("upnp:rootdevice")) { + postResponse(ROOT_FOR_ALL, mx); + } + else if (st.equals("uuid:" + String(m_uuid))) { + postResponse(ROOT_BY_UUID, mx); + } + else if (st.equals(m_deviceType)) { + postResponse(ROOT_BY_TYPE, mx); + } + } + } + + m_server->flush(); + } + else { + unsigned long time = millis(); + + for (int i = 0; i < SSDP_QUEUE_SIZE; i++) { + if (m_queue[i].time > 0 && m_queue[i].time < time) { + send(&m_queue[i]); + } + } + } +} + +void SSDPDeviceClass::setSchemaURL(const char *url) { + strlcpy(m_schemaURL, url, sizeof(m_schemaURL)); +} + +void SSDPDeviceClass::setHTTPPort(uint16_t port) { + m_port = port; +} + +void SSDPDeviceClass::setDeviceType(const char *deviceType) { + strlcpy(m_deviceType, deviceType, sizeof(m_deviceType)); +} + +void SSDPDeviceClass::setName(const char *name) { + strlcpy(m_friendlyName, name, sizeof(m_friendlyName)); +} + +void SSDPDeviceClass::setURL(const char *url) { + strlcpy(m_presentationURL, url, sizeof(m_presentationURL)); +} + +void SSDPDeviceClass::setSerialNumber(const char *serialNumber) { + strlcpy(m_serialNumber, serialNumber, sizeof(m_serialNumber)); +} + +void SSDPDeviceClass::setSerialNumber(const uint32_t serialNumber) { + snprintf(m_serialNumber, sizeof(uint32_t) * 2 + 1, "%08X", serialNumber); +} + +void SSDPDeviceClass::setModelName(const char *name) { + strlcpy(m_modelName, name, sizeof(m_modelName)); +} + +void SSDPDeviceClass::setModelDescription(const char *desc) { + strlcpy(m_modelDescription, desc, sizeof(m_modelDescription)); +} + +void SSDPDeviceClass::setModelNumber(const char *num) { + strlcpy(m_modelNumber, num, sizeof(m_modelNumber)); +} + +void SSDPDeviceClass::setModelURL(const char *url) { + strlcpy(m_modelURL, url, sizeof(m_modelURL)); +} + +void SSDPDeviceClass::setManufacturer(const char *name) { + strlcpy(m_manufacturer, name, sizeof(m_manufacturer)); +} + +void SSDPDeviceClass::setManufacturerURL(const char *url) { + strlcpy(m_manufacturerURL, url, sizeof(m_manufacturerURL)); +} + +void SSDPDeviceClass::setFriendlyName(const char *url) { + strlcpy(m_friendlyName, url, sizeof(m_manufacturerURL)); +} + +void SSDPDeviceClass::setTTL(const uint8_t ttl) { + m_ttl = ttl; +} + +SSDPDeviceClass SSDPDevice; diff --git a/src/net/SSDPDevice.h b/src/net/SSDPDevice.h new file mode 100644 index 0000000..900da6a --- /dev/null +++ b/src/net/SSDPDevice.h @@ -0,0 +1,150 @@ +// SSDPDevice.h +// based on https://github.com/esp8266/Arduino/issues/2283 +// https://github.com/esp8266/Arduino/files/980894/SSDPDevice.zip +// by Pawel Dino + +#ifndef _SSDPDEVICE_h +#define _SSDPDEVICE_h + +#include "lwip/igmp.h" + +#ifdef ESP8266 +#include +#else +#include +#endif +#include + +#define pip41(ipaddr) ((u16_t)(((u8_t*)(ipaddr))[0])) +#define pip42(ipaddr) ((u16_t)(((u8_t*)(ipaddr))[1])) +#define pip43(ipaddr) ((u16_t)(((u8_t*)(ipaddr))[2])) +#define pip44(ipaddr) ((u16_t)(((u8_t*)(ipaddr))[3])) + + +#define LIP2STR(ipaddr) pip41(ipaddr), \ + pip42(ipaddr), \ + pip43(ipaddr), \ + pip44(ipaddr) + + +#define SSDP_INTERVAL 1200 +#define SSDP_PORT 1900 +//#define SSDP_METHOD_SIZE 10 +//#define SSDP_URI_SIZE 2 +//#define SSDP_BUFFER_SIZE 64 +#define SSDP_MULTICAST_TTL 2 + +#define SSDP_QUEUE_SIZE 21 + +static const IPAddress SSDP_MULTICAST_ADDR(239, 255, 255, 250); + +#define SSDP_UUID_SIZE 37 +#define SSDP_SCHEMA_URL_SIZE 64 +#define SSDP_DEVICE_TYPE_SIZE 64 +#define SSDP_FRIENDLY_NAME_SIZE 64 +#define SSDP_SERIAL_NUMBER_SIZE 32 +#define SSDP_PRESENTATION_URL_SIZE 128 +#define SSDP_MODEL_NAME_SIZE 64 +#define SSDP_MODEL_DESCRIPTION_SIZE 64 +#define SSDP_MODEL_URL_SIZE 128 +#define SSDP_MODEL_VERSION_SIZE 32 +#define SSDP_MANUFACTURER_SIZE 64 +#define SSDP_MANUFACTURER_URL_SIZE 128 + +typedef enum { + NOTIFY_ALIVE_INIT, + NOTIFY_ALIVE, + NOTIFY_UPDATE, + RESPONSE +} ssdp_message_t; + +typedef enum { + ROOT_FOR_ALL, + ROOT_BY_UUID, + ROOT_BY_TYPE +} ssdp_udn_t; + +typedef struct { + unsigned long time; + + ssdp_message_t type; + ssdp_udn_t udn; + uint32_t address; + uint16_t port; +} ssdp_send_parameters_t; + +class SSDPDeviceClass { +private: + WiFiUDP *m_server; + + IPAddress m_last; + + char m_schemaURL[SSDP_SCHEMA_URL_SIZE]; + char m_uuid[SSDP_UUID_SIZE]; + char m_deviceType[SSDP_DEVICE_TYPE_SIZE]; + char m_friendlyName[SSDP_FRIENDLY_NAME_SIZE]; + char m_serialNumber[SSDP_SERIAL_NUMBER_SIZE]; + char m_presentationURL[SSDP_PRESENTATION_URL_SIZE]; + char m_manufacturer[SSDP_MANUFACTURER_SIZE]; + char m_manufacturerURL[SSDP_MANUFACTURER_URL_SIZE]; + char m_modelName[SSDP_MODEL_NAME_SIZE]; + char m_modelDescription[SSDP_MODEL_DESCRIPTION_SIZE]; + char m_modelURL[SSDP_MODEL_URL_SIZE]; + char m_modelNumber[SSDP_MODEL_VERSION_SIZE]; + + uint16_t m_port; + uint8_t m_ttl; + + ssdp_send_parameters_t m_queue[SSDP_QUEUE_SIZE]; +protected: + bool readLine(String &value); + bool readKeyValue(String &key, String &value); + + void postNotifyALive(); + void postNotifyUpdate(); + void postResponse(long mx); + void postResponse(ssdp_udn_t udn, long mx); + void post(ssdp_message_t type, ssdp_udn_t udn, IPAddress address, uint16_t port, unsigned long time); + + void send(ssdp_send_parameters_t *parameters); +public: + SSDPDeviceClass(); + + void update(); + + void schema(WiFiClient client); + + void handleClient(); + + void setDeviceType(const String& deviceType) { setDeviceType(deviceType.c_str()); } + void setDeviceType(const char *deviceType); + void setName(const String& name) { setName(name.c_str()); } + void setName(const char *name); + void setURL(const String& url) { setURL(url.c_str()); } + void setURL(const char *url); + void setSchemaURL(const String& url) { setSchemaURL(url.c_str()); } + void setSchemaURL(const char *url); + void setSerialNumber(const String& serialNumber) { setSerialNumber(serialNumber.c_str()); } + void setSerialNumber(const char *serialNumber); + void setSerialNumber(const uint32_t serialNumber); + void setModelName(const String& name) { setModelName(name.c_str()); } + void setModelName(const char *name); + void setModelDescription(const String& name) { setModelDescription(name.c_str()); } + void setModelDescription(const char *name); + void setModelNumber(const String& num) { setModelNumber(num.c_str()); } + void setModelNumber(const char *num); + void setModelURL(const String& url) { setModelURL(url.c_str()); } + void setModelURL(const char *url); + void setManufacturer(const String& name) { setManufacturer(name.c_str()); } + void setManufacturer(const char *name); + void setManufacturerURL(const String& url) { setManufacturerURL(url.c_str()); } + void setManufacturerURL(const char *url); + void setFriendlyName(const String& url) { setFriendlyName(url.c_str()); } + void setFriendlyName(const char *url); + void setHTTPPort(uint16_t port); + void setTTL(uint8_t ttl); +}; + +extern SSDPDeviceClass SSDPDevice; + +#endif diff --git a/src/net/WiFiManager.cpp b/src/net/WiFiManager.cpp index e9a550a..767bd80 100644 --- a/src/net/WiFiManager.cpp +++ b/src/net/WiFiManager.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -34,13 +34,15 @@ namespace Net { WiFiManager::WiFiManager() { setLoopInterval(1000); wiFiStatus = WL_DISCONNECTED; +#ifdef ESP8266 // WI-FI will not boot without this delay!!! delay(2000); +#endif initWiFi(); } void WiFiManager::loop() { - wl_status_t status = WiFi.status(); + auto status = WiFi.status(); if (status != wiFiStatus) { wiFiStatus = status; checkWiFiStatus(); @@ -51,30 +53,67 @@ namespace Net { IO::Logger::info("| - Connecting to WI-FI ."); // WPS works in STA (Station mode) only -> not working in WIFI_AP_STA !!! WiFi.mode(WIFI_STA); +#ifdef CONFIGURE_WITH_WPA delay(1000); // TODO: is this delay necessary? WiFi.begin(WiFi.SSID().c_str(), WiFi.psk().c_str()); +#else + Preferences preferences; + preferences.begin(CONFIG_SYSTEM_NAME, true); + String ssid = preferences.getString("wifi:ssid"); + String pass = preferences.getString("wifi:password"); + + if (!ssid.isEmpty() && !pass.isEmpty()) { + IO::Logger::info("| - WI-FI SSID: %s", ssid.c_str()); + IO::Logger::info("| - WI-FI Password: *"); // pass.c_str() + WiFi.begin(ssid.c_str(), pass.c_str()); + } + preferences.end(); +#endif } bool WiFiManager::checkWiFiStatus() { bool wpsSuccess = false; - wl_status_t status = WiFi.status(); + auto status = WiFi.status(); if (status == WL_CONNECTED) { digitalWrite(Config::StatusLedPin, LOW); - IO::Logger::info("| ✔ Connected to '%s'", WiFi.SSID().c_str()); - IO::Logger::info("| ✔ IP: %s", WiFi.localIP().toString().c_str()); + IO::Logger::info("| - Connected to '%s'", WiFi.SSID().c_str()); + IO::Logger::info("| - IP: %s", WiFi.localIP().toString().c_str()); wpsSuccess = true; - } else if (status == WL_CONNECTION_LOST || status == WL_NO_SSID_AVAIL || status == WL_CONNECT_FAILED) { - digitalWrite(Config::StatusLedPin, HIGH); - IO::Logger::error("| x Lost connection to WiFi"); - initWiFi(); } else { digitalWrite(Config::StatusLedPin, HIGH); - IO::Logger::error("| x Not connected to WiFi (state='%d')", status); + switch (status) { + case WL_NO_SSID_AVAIL: + IO::Logger::error("| x WiFi SSID not available"); + initWiFi(); + break; + case WL_CONNECT_FAILED: + IO::Logger::error("| x WiFi connection failed"); + initWiFi(); + break; + case WL_CONNECTION_LOST: + IO::Logger::error("| x WiFi connection lost"); + initWiFi(); + break; + case WL_DISCONNECTED: + IO::Logger::error("| x WiFi disconnected"); + break; + case WL_NO_SHIELD: + IO::Logger::error("| x WiFi initialization failed"); + break; + case WL_SCAN_COMPLETED: + IO::Logger::error("| x Not connected to WiFi (state='%d')", status); + break; + case WL_IDLE_STATUS: + break; + } } return wpsSuccess; } - bool WiFiManager::startWPS() { + bool WiFiManager::configure() { +#ifdef CONFIGURE_WITH_WPA +#ifdef ESP8266 + // WPA currently only works for ESP8266 digitalWrite(Config::StatusLedPin, LOW); WiFi.disconnect(); delay(100); @@ -84,6 +123,24 @@ namespace Net { IO::Logger::info ("| >> Press WPS button on your router <<"); bool wpsSuccess = WiFi.beginWPSConfig(); return wpsSuccess; +#else + // WPA only works with ESP8266 + return true; +#endif +#else + Preferences preferences; + preferences.begin(CONFIG_SYSTEM_NAME, false); + preferences.putString("wifi:ssid", ""); + preferences.putString("wifi:password", ""); + preferences.end(); + preferences.clear(); + IO::Logger::info("| - WI-FI credentials reset!"); + delay(2000); + IO::Logger::info("REBOOT!"); + // Reboot + esp_restart(); + return true; +#endif } } \ No newline at end of file diff --git a/src/net/WiFiManager.h b/src/net/WiFiManager.h index 4ffb788..6364bfe 100644 --- a/src/net/WiFiManager.h +++ b/src/net/WiFiManager.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,21 +30,27 @@ #ifndef HOMEGENIE_MINI_WIFIMANAGER_H #define HOMEGENIE_MINI_WIFIMANAGER_H +#ifdef ESP8266 #include #include +#else +#include +#include +#endif +#include -#include -#include -#include +#include "Config.h" +#include "Task.h" +#include "io/Logger.h" namespace Net { class WiFiManager : Task { public: WiFiManager(); - void loop(); + void loop() override; void initWiFi(); - bool startWPS(); + bool configure(); bool checkWiFiStatus(); private: wl_status_t wiFiStatus; diff --git a/src/net/mqtt/MQTTBrokerMini.cpp b/src/net/mqtt/MQTTBrokerMini.cpp index ec53308..7db4da8 100644 --- a/src/net/mqtt/MQTTBrokerMini.cpp +++ b/src/net/mqtt/MQTTBrokerMini.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -43,7 +43,7 @@ namespace Net { namespace MQTT { } void MQTTBrokerMini::unsetCallback(void) { - callback = NULL; + callback = nullptr; } void MQTTBrokerMini::runCallback(uint8_t num, Events_t event, uint8_t *topic_name, uint16_t length_topic_name, @@ -59,6 +59,7 @@ namespace Net { namespace MQTT { for (auto &MQTTclient : MQTTclients) { MQTTclient.status = false; } + Logger::info("| ✔ MQTT service"); } void MQTTBrokerMini::parsing(uint8_t num, uint8_t *payload, uint16_t length) { @@ -116,7 +117,7 @@ namespace Net { namespace MQTT { //Length_topic_name = MSB_LSB(&payload[len]); Length_topic_name = (payload[len + 1] * 256) + payload[len + 2]; - uint8_t *Packet_identifier = NULL; + uint8_t *Packet_identifier = nullptr; uint8_t Packet_identifier_length = 0; if (QoS > 0) { diff --git a/src/net/mqtt/MQTTBrokerMini.h b/src/net/mqtt/MQTTBrokerMini.h index 691101c..20cbc78 100644 --- a/src/net/mqtt/MQTTBrokerMini.h +++ b/src/net/mqtt/MQTTBrokerMini.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -33,10 +33,9 @@ #ifndef MQTTBROKER_H_ #define MQTTBROKER_H_ -#include #include -#include +#include "io/Logger.h" #define MQTT_VERSION_3_1_1 4 @@ -105,6 +104,8 @@ namespace Net { namespace MQTT { typedef void(*callback_t)(uint8_t num, Events_t event, String topic_name, uint8_t *payload, uint16_t length_payload); + using namespace IO; + class MQTTBrokerMini { public: MQTTBrokerMini(WebSocketsServer *webSocket); diff --git a/src/scripting/ProgramEngine.cpp b/src/scripting/ProgramEngine.cpp index b0600c8..668ac7a 100644 --- a/src/scripting/ProgramEngine.cpp +++ b/src/scripting/ProgramEngine.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). diff --git a/src/scripting/ProgramEngine.h b/src/scripting/ProgramEngine.h index 0fd433c..fe36743 100644 --- a/src/scripting/ProgramEngine.h +++ b/src/scripting/ProgramEngine.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -34,7 +34,7 @@ #include "tinyjs/TinyJS_Functions.h" #include "tinyjs/TinyJS_MathFunctions.h" */ -#include +#include "io/Logger.h" namespace Scripting { diff --git a/src/service/EventRouter.cpp b/src/service/EventRouter.cpp index 0bc42fe..9c8dec2 100644 --- a/src/service/EventRouter.cpp +++ b/src/service/EventRouter.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -29,7 +29,7 @@ #include "EventRouter.h" -#include +#include "HomeGenie.h" namespace Service { @@ -38,24 +38,23 @@ namespace Service { for (int i = 0; i < eventsQueue.size(); i++) { // route event through MQTT auto m = eventsQueue.pop(); - Logger::trace(":%s de-queued event >> [domain '%s' address '%s' event '%s']", EVENTROUTER_NS_PREFIX, m.domain.c_str(), m.sender.c_str(), m.event.c_str()); - - auto currentTime = NetManager::getTimeClient().getFormattedDate().c_str(); + Logger::verbose(":%s dequeued event >> [domain '%s' address '%s' event '%s']", EVENTROUTER_NS_PREFIX, m.domain.c_str(), m.sender.c_str(), m.event.c_str()); + auto date = NetManager::getTimeClient().getFormattedDate(); // MQTT - auto topic = String("hg-mini/"+m.domain+"/" + m.sender + "/event"); - auto details = Service::HomeGenie::createModuleParameter(m.event.c_str(), m.value.c_str(), currentTime); + auto topic = String(String(CONFIG_SYSTEM_NAME) + "/" + m.domain + "/" + m.sender + "/event"); + auto details = Service::HomeGenie::createModuleParameter(m.event.c_str(), m.value.c_str(), date.c_str()); netManager->getMQTTServer().broadcast(&topic, &details); // SSE +// TODO: "sendSSSEvent" is BUGGED - memory leak netManager->getHttpServer().sendSSEvent(m.domain, m.sender, m.event, m.value); // WS if (netManager->getWebSocketServer().connectedClients() > 0) { - String date = Net::NetManager::getTimeClient().getFormattedDate(); unsigned long epoch = Net::NetManager::getTimeClient().getEpochTime(); int ms = Net::NetManager::getTimeClient().getMilliseconds(); - int sz = 1+snprintf(NULL, 0, R"(data: {"Timestamp":"%s","UnixTimestamp":%lu%03d,"Description":"","Domain":"%s","Source":"%s","Property":"%s","Value":"%s"})", + int sz = 1+snprintf(nullptr, 0, R"(data: {"Timestamp":"%s","UnixTimestamp":%lu%03d,"Description":"","Domain":"%s","Source":"%s","Property":"%s","Value":"%s"})", date.c_str(), epoch, ms, m.domain.c_str(), m.sender.c_str(), m.event.c_str(), m.value.c_str()); char msg[sz]; snprintf(msg, sz, R"({"Timestamp":"%s","UnixTimestamp":%lu%03d,"Description":"","Domain":"%s","Source":"%s","Property":"%s","Value":"%s"})", @@ -68,7 +67,9 @@ namespace Service { } void EventRouter::signalEvent(QueuedMessage m) { +// if (WiFi.isConnected()) { eventsQueue.add(m); +// } } void EventRouter::withNetManager(NetManager &manager) { diff --git a/src/service/EventRouter.h b/src/service/EventRouter.h index bb1e671..cfd272e 100644 --- a/src/service/EventRouter.h +++ b/src/service/EventRouter.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,12 +30,10 @@ #ifndef HOMEGENIE_MINI_EVENTROUTER_H #define HOMEGENIE_MINI_EVENTROUTER_H -#include #include -#include -#include -#include +#include "Task.h" +#include "net/NetManager.h" #define EVENTROUTER_NS_PREFIX "Service::EventRouter" @@ -74,4 +72,4 @@ namespace Service { } -#endif //HOMEGENIE_MINI_EVENTROUTER_H +#endif //HOMEGENIE_MINI_EVENTROUTER_H \ No newline at end of file diff --git a/src/service/HomeGenie.cpp b/src/service/HomeGenie.cpp deleted file mode 100644 index 94cf8c8..0000000 --- a/src/service/HomeGenie.cpp +++ /dev/null @@ -1,220 +0,0 @@ -/* - * HomeGenie-Mini (c) 2018-2019 G-Labs - * - * - * This file is part of HomeGenie-Mini (HGM). - * - * HomeGenie-Mini is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * HomeGenie-Mini is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with HomeGenie-Mini. If not, see . - * - * - * Authors: - * - Generoso Martello - * - * - * Releases: - * - 2019-01-10 Initial release - * - */ - -#include "HomeGenie.h" - -#include -#include - -namespace Service { - - API::HomeGenieHandler homeGenieHandler; - API::X10Handler x10Handler; - - HomeGenie::HomeGenie() { - setLoopInterval(20L); - eventRouter.withNetManager(netManager); - } - - void HomeGenie::begin() { - netManager.begin(); - netManager.getHttpServer().addHandler(this); - ioManager.begin(); - ioManager.setOnEventCallback(this); - } - - void HomeGenie::loop() { - Logger::verbose(":%s loop() >> BEGIN", HOMEGENIEMINI_NS_PREFIX); - - // HomeGenie-Mini Terminal CLI - if(Serial.available() > 0) { - String cmd = Serial.readStringUntil('\n'); - auto apiCommand = APIRequest::parse(cmd); - // TODO: implement API commands from console input as well - // - see `HomeGenie::api(...)` method - } - - Logger::verbose(":%s loop() << END", HOMEGENIEMINI_NS_PREFIX); - } - - NetManager& HomeGenie::getNetManager() { - return netManager; - } - IOManager& HomeGenie::getIOManager() { - return ioManager; - } - EventRouter &HomeGenie::getEventRouter() { - return eventRouter; - } - - - // BEGIN IIOEventSender interface methods - void HomeGenie::onIOEvent(IIOEventSender *sender, const unsigned char *eventPath, void *eventData, IOEventDataType dataType) { - String domain = String((char*)sender->getDomain()); - String address = String((char*)sender->getAddress()); - String event = String((char*)eventPath); - Logger::trace(":%s [IOManager::IOEvent] >> [domain '%s' address '%s' event '%s']", HOMEGENIEMINI_NS_PREFIX, domain.c_str(), address.c_str(), event.c_str()); - if (domain == (IOEventDomains::HomeAutomation_HomeGenie)) { - - homeGenieHandler.handleEvent(*this, sender, eventPath, eventData, dataType); - - } else if (domain == (IOEventDomains::HomeAutomation_X10)) { - - x10Handler.handleEvent(*this, sender, eventPath, eventData, dataType); - - } - } - // END IIOEventSender - - // BEGIN RequestHandler interface methods - bool HomeGenie::canHandle(HTTPMethod method, String uri) { - return uri != NULL && uri.startsWith("/api/"); - } - bool HomeGenie::handle(ESP8266WebServer& server, HTTPMethod requestMethod, String requestUri) { - auto command = APIRequest::parse(requestUri); - if (api(&command, server)) { - server.send(200, "application/json", command.Response); - } else { - server.send(400, "application/json", command.Response); - } - return true; - } - // END RequestHandler interface methods - - - String HomeGenie::createModuleParameter(const char* name, const char *value, const char *timestamp) { - static const char *parameterTemplate = R"({ - "Name": "%s", - "Value": "%s", - "Description": "%s", - "FieldType": "%s", - "UpdateTime": "%s" - })"; - ssize_t size = snprintf(NULL, 0, parameterTemplate, - name, value, "", "", timestamp - )+1; - char* parameterJson = (char*)malloc(size); - snprintf(parameterJson, size, parameterTemplate, - name, value, "", "", timestamp - ); - auto p = String(parameterJson); - free(parameterJson); - return p; - } - - String HomeGenie::createModule(const char* domain, const char *address, const char *name, const char* description, const char *deviceType, const char *parameters) { - static const char* moduleTemplate = R"({ - "Name": "%s", - "Description": "%s", - "DeviceType": "%s", - "Domain": "%s", - "Address": "%s", - "Properties": [%s], - "RoutingNode": "" -})"; - // TODO: WARNING removing "RoutingNode" property will break HomeGenie plus client compatibility - - ssize_t size = snprintf(NULL, 0, moduleTemplate, - name, description, deviceType, - domain, address, - parameters - )+1; - char* moduleJson = (char*)malloc(size); - snprintf(moduleJson, size, moduleTemplate, - name, description, deviceType, - domain, address, - parameters - ); - auto m = String(moduleJson); - free(moduleJson); - return m; - } - - String HomeGenie::getBuiltinModuleJSON() { - auto currentTime = NetManager::getTimeClient().getFormattedDate(); - auto lightSensor = getIOManager().getLightSensor(); - auto temperatureSensor = getIOManager().getTemperatureSensor(); - auto paramLuminance = HomeGenie::createModuleParameter("Sensor.Luminance", String(lightSensor.getLightLevel()).c_str(), currentTime.c_str()); - auto paramTemperature = HomeGenie::createModuleParameter("Sensor.Temperature", String(temperatureSensor.getTemperature()).c_str(), currentTime.c_str()); - return String(HomeGenie::createModule(IOEventDomains::HomeAutomation_HomeGenie, HOMEGENIE_BUILTIN_MODULE_ADDRESS, - "HG-Mini", "HomeGenie Mini node", "Sensor", - (paramLuminance+","+paramTemperature).c_str())); - } - - bool HomeGenie::api(APIRequest *request, ESP8266WebServer &server) { - if (request->Domain == (IOEventDomains::HomeAutomation_X10)) { - - return x10Handler.handleRequest(*this, request, server); - - } else if (request->Domain == (IOEventDomains::HomeAutomation_HomeGenie)) { - - return homeGenieHandler.handleRequest(*this, request, server); - - } else return false; - } - - int HomeGenie::writeModuleJSON(ESP8266WebServer *server, String &domain, String &address) { - auto outputCallback = APIHandlerOutputCallback(server); - if (domain == (IOEventDomains::HomeAutomation_HomeGenie) && address == HOMEGENIE_BUILTIN_MODULE_ADDRESS) { - auto module = getBuiltinModuleJSON(); - outputCallback.write(module); - // TODO: check out if `module` gets actually disposed - } else if (domain == (IOEventDomains::HomeAutomation_X10)) { - x10Handler.getModuleJSON(&outputCallback, domain, address); - } - return outputCallback.outputLength; - } - - int HomeGenie::writeModuleListJSON(ESP8266WebServer *server) { - auto outputCallback = APIHandlerOutputCallback(server); - String line = "["; - outputCallback.write(line); - // HomeAutomation.HomeGenie - line = getBuiltinModuleJSON(); - outputCallback.write(line); - line = ",\n"; - outputCallback.write(line); - // HomeGenie Mini P1Port modules (GPIO D5, D6, D6, D8) - homeGenieHandler.getModuleListJSON(&outputCallback); - line = ",\n"; - outputCallback.write(line); - // HomeAutomation.X10 - x10Handler.getModuleListJSON(&outputCallback); - line = "]\n"; - outputCallback.write(line); - return outputCallback.outputLength; - } - - int HomeGenie::writeGroupListJSON(ESP8266WebServer *server) { - auto outputCallback = APIHandlerOutputCallback(server); - x10Handler.getGroupListJSON(&outputCallback); - return outputCallback.outputLength; - } - -} diff --git a/src/service/HomeGenie.h b/src/service/HomeGenie.h deleted file mode 100644 index 1f96235..0000000 --- a/src/service/HomeGenie.h +++ /dev/null @@ -1,86 +0,0 @@ -/* - * HomeGenie-Mini (c) 2018-2019 G-Labs - * - * - * This file is part of HomeGenie-Mini (HGM). - * - * HomeGenie-Mini is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * HomeGenie-Mini is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with HomeGenie-Mini. If not, see . - * - * - * Authors: - * - Generoso Martello - * - * - * Releases: - * - 2019-01-10 Initial release - * - */ - -#ifndef HOMEGENIE_MINI_HOMEGENIE_H -#define HOMEGENIE_MINI_HOMEGENIE_H - -#include - -#include -#include -#include -#include -#include -#include - -#define HOMEGENIEMINI_NS_PREFIX "Service::HomeGenie" - -namespace Service { - - using namespace IO; - using namespace Net; - using namespace Service::API; - - class HomeGenie: Task, RequestHandler, IIOEventReceiver { - public: - HomeGenie(); - void begin(); - - // Task overrides - void loop(); - - // IIOEventSender - void onIOEvent(IIOEventSender *sender, const unsigned char *eventPath, void *eventData, IOEventDataType dataType); - - // RequestHandler overrides - bool canHandle(HTTPMethod method, String uri); - bool handle(ESP8266WebServer& server, HTTPMethod requestMethod, String requestUri); - - bool api(APIRequest *request, ESP8266WebServer &server); - - NetManager& getNetManager(); - IOManager& getIOManager(); - EventRouter& getEventRouter(); - - String getBuiltinModuleJSON(); - int writeModuleJSON(ESP8266WebServer *server,String &domain, String &address); - int writeModuleListJSON(ESP8266WebServer *server); - int writeGroupListJSON(ESP8266WebServer *server); - - static String createModule(const char *domain, const char *address, const char *name, const char* description, const char *deviceType, const char *parameters); - static String createModuleParameter(const char *name, const char* value, const char *timestamp); - private: - NetManager netManager; - IOManager ioManager; - EventRouter eventRouter; - }; - -} - -#endif //HOMEGENIE_MINI_HOMEGENIE_H diff --git a/src/service/Module.cpp b/src/service/Module.cpp new file mode 100644 index 0000000..21f2c5a --- /dev/null +++ b/src/service/Module.cpp @@ -0,0 +1,9 @@ +// +// Created by gene on 12/12/23. +// + +#include "Module.h" + +namespace Service { + +} \ No newline at end of file diff --git a/src/service/Module.h b/src/service/Module.h new file mode 100644 index 0000000..e0a0be5 --- /dev/null +++ b/src/service/Module.h @@ -0,0 +1,82 @@ +// +// Created by gene on 12/12/23. +// + +#ifndef HOMEGENIE_MINI_MODULE_H +#define HOMEGENIE_MINI_MODULE_H + +#include "net/NetManager.h" + +namespace Service { + + using namespace Net; + + enum ModuleTypes { + Generic = 0, + Switch, + Dimmer, + Color, + Sensor + }; + + class ModuleParameter { + public: + String name; + String value; + String updateTime; + + ModuleParameter() { + updateTime = NetManager::getTimeClient().getFormattedDate(); + } + ModuleParameter(String name): ModuleParameter() { + this->name = name; + }; + ModuleParameter(String name, String value): ModuleParameter(name) { + this->value = value; + }; + + bool is(const char* n) const { + return name.equals(n); + } + void setValue(const char* v) { + value = v; + updateTime = NetManager::getTimeClient().getFormattedDate(); + } + }; + + class Module { + public: + String domain; + String address; + String type; + String name; + String description; + LinkedList properties; + bool setProperty(String pn, String pv) { + for(int p = 0; p < properties.size(); p++) { + auto param = properties.get(p); + if (param->is(pn.c_str())) { + param->setValue(pv.c_str()); + return true; + } + } + // add new parameter + properties.add(new ModuleParameter(pn, pv)); + return false; + } + ModuleParameter* getProperty(String pn) { + for(int p = 0; p < properties.size(); p++) { + auto param = properties.get(p); + if (param->is(pn.c_str())) { + return param; + } + } + return nullptr; + } + }; + +} + + + +#endif //HOMEGENIE_MINI_MODULE_H diff --git a/src/service/api/APIHandler.h b/src/service/api/APIHandler.h index 60e3a4f..08034aa 100644 --- a/src/service/api/APIHandler.h +++ b/src/service/api/APIHandler.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,10 +30,10 @@ #ifndef HOMEGENIE_MINI_APIHANDLER_H #define HOMEGENIE_MINI_APIHANDLER_H -#include +#include "io/IOEvent.h" +#include "service/Module.h" -#include -#include +#include "APIRequest.h" namespace Service { namespace API { @@ -46,11 +46,28 @@ namespace Service { namespace API { }; class APIHandler { - virtual bool canHandleDomain(String &domain) = 0; - virtual bool handleRequest(HomeGenie &homeGenie, APIRequest *request, ESP8266WebServer &server) = 0; - virtual bool handleEvent(HomeGenie &homeGenie, IIOEventSender *sender, const unsigned char *eventPath, void *eventData, IOEventDataType dataType) = 0; - virtual void getModuleJSON(OutputStreamCallback *outputCallback, String &domain, String &address) = 0; - virtual void getModuleListJSON(OutputStreamCallback *outputCallback) = 0; + public: + virtual void init() = 0; + virtual bool canHandleDomain(String* domain) = 0; + virtual bool handleRequest(APIRequest *request, WebServer &server) = 0; + virtual bool handleEvent(IIOEventSender *sender, const char* domain, const char* address, const unsigned char *eventPath, void *eventData, IOEventDataType dataType) = 0; + virtual Module* getModule(const char* domain, const char* address) = 0; + virtual LinkedList* getModuleList() = 0; + }; + + class APIHandlerOutputCallback : public OutputStreamCallback { + WebServer *server; + public: + unsigned int outputLength = 0; + APIHandlerOutputCallback(WebServer *server) { + this->server = server; + } + void write(String &s) { + outputLength += s.length(); + if (server != nullptr) { + server->sendContent(s); + } + } }; }} diff --git a/src/service/api/APIRequest.cpp b/src/service/api/APIRequest.cpp index 91e31af..262fafe 100644 --- a/src/service/api/APIRequest.cpp +++ b/src/service/api/APIRequest.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). diff --git a/src/service/api/APIRequest.h b/src/service/api/APIRequest.h index 291eec6..c60988f 100644 --- a/src/service/api/APIRequest.h +++ b/src/service/api/APIRequest.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,8 +30,8 @@ #ifndef HOMEGENIE_MINI_APIREQUEST_H #define HOMEGENIE_MINI_APIREQUEST_H -#include -#include +#include "Config.h" +#include "io/Logger.h" #define APIREQUEST_LOG_PREFIX "@Service::ApiRequest" diff --git a/src/service/api/HomeGenieHandler.cpp b/src/service/api/HomeGenieHandler.cpp index 4693203..0262775 100644 --- a/src/service/api/HomeGenieHandler.cpp +++ b/src/service/api/HomeGenieHandler.cpp @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -31,144 +31,186 @@ namespace Service { namespace API { - static IOModule moduleList[P1PORT_GPIO_COUNT]; + HomeGenieHandler::HomeGenieHandler(GPIOPort* gpioPort) { + this->gpioPort = gpioPort; + // HomeGenie Mini module (default module) + auto miniModule = new Module(); + miniModule->domain = IO::IOEventDomains::HomeAutomation_HomeGenie; + miniModule->address = CONFIG_BUILTIN_MODULE_ADDRESS; + miniModule->type = "Sensor"; + miniModule->name = CONFIG_BUILTIN_MODULE_NAME; + miniModule->description = "HomeGenie Mini node"; + moduleList.add(miniModule); + } + + void HomeGenieHandler::init() { + // Add GPIO modules + // Output GPIO pins + uint8_t gpio[] = CONFIG_GPIO_OUT; + uint8_t gpio_count = *(&gpio + 1) - gpio; + for (int m = 0; m < gpio_count; m++) { + auto address = String(gpio[m]); + auto module = new Module(); + module->domain = IO::IOEventDomains::HomeAutomation_HomeGenie; + module->address = address; + module->type = "Dimmer"; + module->name = "GPIO " + address; + module->description = ""; + + // init pins with current level + float level = GPIOPort::loadLevel(gpio[m]); + level > 0 ? gpioPort->on(gpio[m]) : gpioPort->off(gpio[m]); + auto propLevel = new ModuleParameter(IOEventPaths::Status_Level, String(level)); + module->properties.add(propLevel); + + moduleList.add(module); + } + + // TODO: implement Input GPIO pins as well (CONFIG_GPIO_IN) - bool HomeGenieHandler::canHandleDomain(String &domain) { - return domain == (IO::IOEventDomains::HomeAutomation_HomeGenie); } - bool HomeGenieHandler::handleRequest(Service::HomeGenie &homeGenie, Service::APIRequest *request, ESP8266WebServer &server) { - if (request->Address.length() == 2 && request->Address.startsWith("D")) { - uint8_t pinNumber = request->Address.substring(1).toInt()-4; - if (pinNumber >= 1 && pinNumber <= P1PORT_GPIO_COUNT) { - QueuedMessage m = QueuedMessage(request->Domain, request->Address, (IOEventPaths::Status_Level), ""); - auto module = &moduleList[pinNumber]; - auto gpioPort = homeGenie.getIOManager().getExpansionPort(); - if (request->Command == "Control.On") { - gpioPort.setOutput(pinNumber, P1PORT_GPIO_LEVEL_MAX); - module->Level = 1; - } else if (request->Command == "Control.Off") { - gpioPort.setOutput(pinNumber, P1PORT_GPIO_LEVEL_MIN); - module->Level = 0; - } else if (request->Command == "Control.Level") { - uint8_t level = (uint8_t)request->getOption(0).toInt(); - gpioPort.setOutput(pinNumber, level); - module->Level = (level/P1PORT_GPIO_LEVEL_MAX); - } else if (request->Command == "Control.Toggle") { - if (module->Level == 0) { - gpioPort.setOutput(pinNumber, P1PORT_GPIO_LEVEL_MAX); - module->Level = 1; - } else { - gpioPort.setOutput(pinNumber, P1PORT_GPIO_LEVEL_MIN); - module->Level = 0; - } - } - m.value = String(module->Level); - homeGenie.getEventRouter().signalEvent(m); - request->Response = R"({ "ResponseText": "OK" })"; - return true; - } else return false; - } else if (request->Address == "Config") { + bool HomeGenieHandler::canHandleDomain(String* domain) { + return domain->equals(IO::IOEventDomains::HomeAutomation_HomeGenie); + } + + bool HomeGenieHandler::handleRequest(Service::APIRequest *request, WebServer &server) { + auto homeGenie = HomeGenie::getInstance(); + if (request->Address == "Config") { if (request->Command == "Modules.List") { - // HG Mini multi-sensor module - auto contentLength = (size_t)homeGenie.writeModuleListJSON(NULL); + auto contentLength = (size_t)homeGenie->writeModuleListJSON(nullptr); server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "0"); server.setContentLength(contentLength); server.send(200, "application/json; charset=utf-8", ""); - homeGenie.writeModuleListJSON(&server); + homeGenie->writeModuleListJSON(&server); //server.client().flush(); return true; } else if (request->Command == "Modules.Get") { String domain = request->getOption(0); String address = request->getOption(1); - auto contentLength = (size_t)homeGenie.writeModuleJSON(NULL, domain, address); + auto contentLength = (size_t)homeGenie->writeModuleJSON(nullptr, &domain, &address); if (contentLength == 0) return false; server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "0"); server.setContentLength(contentLength); server.send(200, "application/json; charset=utf-8", ""); - homeGenie.writeModuleJSON(&server, domain, address); + homeGenie->writeModuleJSON(&server, &domain, &address); return true; } else if (request->Command == "Groups.List") { - auto contentLength = (size_t ) homeGenie.writeGroupListJSON(NULL); + auto contentLength = (size_t ) homeGenie->writeGroupListJSON(nullptr); server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "0"); server.setContentLength(contentLength); server.send(200, "application/json; charset=utf-8", ""); - homeGenie.writeGroupListJSON(&server); + homeGenie->writeGroupListJSON(&server); //server.client().flush(); return true; + } else if (request->Command == "WebSocket.GetToken") { + + // TODO: implement random token with expiration (like in HG server) for websocket client verification + + request->Response = R"({ "ResponseValue": "e046f885-1d51-4dd2-b952-38e7134a9c0f" })"; + return true; + } + } else { + uint8_t gpio[] = CONFIG_GPIO_OUT; + uint8_t pinNumber = request->Address.toInt(); + bool validPin = std::find(std::begin(gpio), std::end(gpio), pinNumber) != std::end(gpio); + if (validPin) { + + // TODO: move to utility class -- lookup module by domain/address + + Module* module; + for (int m = 0; m < moduleList.size(); m++) { + module = moduleList[m]; + if (module->domain.equals(request->Domain) && module->address.equals(request->Address)) { + break; + } + module = nullptr; + } + + if (module) { + + auto levelProperty = module->getProperty(IOEventPaths::Status_Level); + if (levelProperty->value.toFloat() > 0) { + GPIOPort::saveLastOnLevel(pinNumber, levelProperty->value.toFloat()); + } + + float level = 0; + if (request->Command == "Control.On") { + level = gpioPort->on(pinNumber); + } else if (request->Command == "Control.Off") { + level = gpioPort->off(pinNumber); + } else if (request->Command == "Control.Level") { + level = gpioPort->level(pinNumber, (uint8_t)request->getOption(0).toFloat()); + } else if (request->Command == "Control.Toggle") { + if (levelProperty->value.toFloat() == 0) { + level = gpioPort->on(pinNumber); + } else { + level = gpioPort->off(pinNumber); + } + } + + levelProperty->setValue(String(level).c_str()); + GPIOPort::saveLevel(pinNumber, levelProperty->value.toFloat()); + + request->Response = R"({ "ResponseText": "OK" })"; + return true; + } } } return false; } - bool HomeGenieHandler::handleEvent(Service::HomeGenie &homeGenie, IO::IIOEventSender *sender, + bool HomeGenieHandler::handleEvent(IO::IIOEventSender *sender, + const char* domain, const char* address, const unsigned char *eventPath, void *eventData, IO::IOEventDataType dataType) { - - auto domain = String((char*)sender->getDomain()); - auto address = String((char*)sender->getAddress()); - auto event = String((char*)eventPath); - - // Event Stream Message Enqueue (for MQTT/SSE/WebSocket propagation) - QueuedMessage m = QueuedMessage(domain, address, event, ""); - // Data type handling - switch (dataType) { - case SensorLight: - m.value = String(*(uint16_t *)eventData); - break; - case SensorTemperature: - m.value = String(*(float_t *)eventData); - break; - case UnsignedNumber: - m.value = String(*(uint32_t *)eventData); - break; - case Number: - m.value = String(*(int32_t *)eventData); - break; - case Float: - m.value = String(*(float *)eventData); - break; - default: - m.value = String(*(int32_t *)eventData); + auto module = getModule(domain, address); + if (module) { + auto event = String((char *) eventPath); + // Event Stream Message Enqueue (for MQTT/SSE/WebSocket propagation) + auto m = QueuedMessage(domain, address, event.c_str(), ""); + // Data type handling + switch (dataType) { + case SensorLight: + m.value = String(*(uint16_t *) eventData); + break; + case SensorTemperature: + m.value = String(*(float_t *) eventData); + break; + case UnsignedNumber: + m.value = String(*(uint32_t *) eventData); + break; + case Number: + m.value = String(*(int32_t *) eventData); + break; + case Float: + m.value = String(*(float *) eventData); + break; + default: + m.value = String(*(int32_t *) eventData); + } + module->setProperty(event, m.value); + HomeGenie::getInstance()->getEventRouter().signalEvent(m); } - homeGenie.getEventRouter().signalEvent(m); - return false; } - void HomeGenieHandler::getModuleJSON(OutputStreamCallback *outputCallback, String &domain, String &address) { - if (address.length() != 2) return; - int pinNumber = address.substring(1).toInt()-P1PORT_GPIO_COUNT; - if (pinNumber >= 1 && pinNumber <= P1PORT_GPIO_COUNT) { - auto module = &moduleList[pinNumber]; - auto paramLevel = String(module->Level); - auto deviceType = String("Dimmer"); // TODO: DeviceTypes[module->Type]; - if (module->UpdateTime.startsWith("1970-")) { - module->UpdateTime = NetManager::getTimeClient().getFormattedDate(); - } - paramLevel = HomeGenie::createModuleParameter(IOEventPaths::Status_Level, paramLevel.c_str(), module->UpdateTime.c_str()); - auto moduleJSON = HomeGenie::createModule(domain.c_str(), address.c_str(), - "", "P1 Module", deviceType.c_str(), - paramLevel.c_str()); - outputCallback->write(moduleJSON); + Module* HomeGenieHandler::getModule(const char* domain, const char* address) { + for (int i = 0; i < moduleList.size(); i++) { + Module* module = moduleList.get(i); + if (module->domain.equals(domain) && module->address.equals(address)) + return module; } + return nullptr; } - - void HomeGenieHandler::getModuleListJSON(OutputStreamCallback *outputCallback) { - auto domain = String((IOEventDomains::HomeAutomation_HomeGenie)); - auto separator = String(",\n"); - // P1 Expansion Port modules - for (int m = 0; m < P1PORT_GPIO_COUNT; m++) { - auto address = "D"+String(m+1+P1PORT_GPIO_COUNT); - if (m != 0) outputCallback->write(separator); - getModuleJSON(outputCallback, domain, address); - } + LinkedList* HomeGenieHandler::getModuleList() { + return &moduleList; } }} diff --git a/src/service/api/HomeGenieHandler.h b/src/service/api/HomeGenieHandler.h index bc7757f..feecb19 100644 --- a/src/service/api/HomeGenieHandler.h +++ b/src/service/api/HomeGenieHandler.h @@ -1,5 +1,5 @@ /* - * HomeGenie-Mini (c) 2018-2019 G-Labs + * HomeGenie-Mini (c) 2018-2024 G-Labs * * * This file is part of HomeGenie-Mini (HGM). @@ -30,36 +30,25 @@ #ifndef HOMEGENIE_MINI_HOMEGENIEHANDLER_H #define HOMEGENIE_MINI_HOMEGENIEHANDLER_H -#include -#include -#include -#include +#include "HomeGenie.h" namespace Service { namespace API { - class APIHandlerOutputCallback : public OutputStreamCallback { - ESP8266WebServer *server; - public: - int outputLength = 0; - APIHandlerOutputCallback(ESP8266WebServer *server) { - this->server = server; - } - void write(String &s) { - outputLength += s.length(); - if (server != NULL) { - server->sendContent(s); - } - } - }; + using namespace IO::GPIO; class HomeGenieHandler : public APIHandler { + private: + GPIOPort* gpioPort; + LinkedList moduleList; public: - bool canHandleDomain(String &domain); - bool handleRequest(HomeGenie &homeGenie, APIRequest *request, ESP8266WebServer &server); - bool handleEvent(HomeGenie &homeGenie, IIOEventSender *sender, const unsigned char *eventPath, void *eventData, - IOEventDataType dataType); - void getModuleJSON(OutputStreamCallback *outputCallback, String &domain, String &address); - void getModuleListJSON(OutputStreamCallback *outputCallback); + HomeGenieHandler(GPIOPort* gpioPort); + void init() override; + bool canHandleDomain(String* domain) override; + bool handleRequest(APIRequest *request, WebServer &server) override; + bool handleEvent(IIOEventSender *sender, const char* domain, const char* address, const unsigned char *eventPath, void *eventData, + IOEventDataType dataType) override; + Module* getModule(const char* domain, const char* address) override; + LinkedList* getModuleList() override; }; }} diff --git a/src/service/api/X10Handler.cpp b/src/service/api/X10Handler.cpp deleted file mode 100644 index 5656a3c..0000000 --- a/src/service/api/X10Handler.cpp +++ /dev/null @@ -1,274 +0,0 @@ -/* - * HomeGenie-Mini (c) 2018-2019 G-Labs - * - * - * This file is part of HomeGenie-Mini (HGM). - * - * HomeGenie-Mini is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * HomeGenie-Mini is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with HomeGenie-Mini. If not, see . - * - * - * Authors: - * - Generoso Martello - * - * - * Releases: - * - 2019-01-28 Initial release - * - */ - -#include "X10Handler.h" - -namespace Service { namespace API { - - enum ModuleType { - Switch = 0, - Light, - Dimmer, - MotionDetector, - DoorWindow - }; - - String DeviceTypes[] = { - "Dimmer", - "Light", - "Switch", - "Sensor", // Generic sensor - "DoorWindow" - }; - - IOModule moduleList[/*house codes*/ (HOUSE_MAX-HOUSE_MIN+1)][/*units*/ UNIT_MAX]; - - void X10Handler::getModuleJSON(OutputStreamCallback *outputCallback, String &domain, String &address) { - address.toLowerCase(); - int h = (int)address.charAt(0)-(int)'a'; // house code 0..15 - int m = address.substring(1).toInt()-UNIT_MIN; // unit code 0..15 - auto module = &moduleList[h][m]; - auto paramLevel = String(module->Level); - auto deviceType = DeviceTypes[module->Type]; - if (module->UpdateTime.startsWith("1970-")) { - module->UpdateTime = NetManager::getTimeClient().getFormattedDate(); - } - paramLevel = HomeGenie::createModuleParameter(IOEventPaths::Status_Level, paramLevel.c_str(), module->UpdateTime.c_str()); - auto moduleJSON = HomeGenie::createModule(IOEventDomains::HomeAutomation_X10, (String((char)('A'+h))+String(m+1)).c_str(), - "", "X10 Module", deviceType.c_str(), - paramLevel.c_str()); - outputCallback->write(moduleJSON); - } - - void X10Handler::getModuleListJSON(OutputStreamCallback *outputCallback) { - auto domain = String((IOEventDomains::HomeAutomation_X10)); - auto separator = String(",\n"); - // X10 Home Automation modules - for (int h = HOUSE_MIN; h <= HOUSE_MAX; h++) { - for (int m = 0; m < UNIT_MAX; m++) { - auto address = String((char)h)+String(m+UNIT_MIN); - if (h != HOUSE_MIN || m != 0) outputCallback->write(separator); - getModuleJSON(outputCallback, domain, address); - } - } - } - - void X10Handler::getGroupListJSON(OutputStreamCallback *outputCallback) { - // TODO: Groups have to be managed from Service::HomeGenie class, read below: - // TODO: implement X10Handler::getGetModules() and move this code to HomeGenie::writeGroupListJSON(&server) - String separator = ","; - String line = R"([{"Name":"Dashboard","Modules":[{"Address":")"; - line += HOMEGENIE_BUILTIN_MODULE_ADDRESS; - line += R"(","Domain":"HomeAutomation.HomeGenie"}]},)"; - outputCallback->write(line); - line = R"({"Name":"GPIO P1 Port", "Modules":[)"; - outputCallback->write(line); - for (int m = 0; m < P1PORT_GPIO_COUNT; m++) { - line = R"({"Address":")" + String("D")+String(m+1+P1PORT_GPIO_COUNT) + R"(","Domain":"HomeAutomation.HomeGenie"})"; - outputCallback->write(line); - if (m != P1PORT_GPIO_COUNT-1) outputCallback->write(separator); - } - line = "]},"; - outputCallback->write(line); - line = R"({"Name":"X10 Modules", "Modules":[)"; - outputCallback->write(line); - for (int h = 0; h <= (HOUSE_MAX-HOUSE_MIN); h++) { - for (int m = 0; m < UNIT_MAX; m++) { - line = R"({"Address":")" + String((char)('A'+h))+String(m + 1) + R"(","Domain":"HomeAutomation.X10"})"; - outputCallback->write(line); - if (!(m == UNIT_MAX-1 && h == (HOUSE_MAX-HOUSE_MIN))) outputCallback->write(separator); - } - } - line = "]}]"; - outputCallback->write(line); - } - - bool X10Handler::handleRequest(HomeGenie &homeGenie, APIRequest *command, ESP8266WebServer &server) { - - if (command->Domain == (IOEventDomains::HomeAutomation_X10) - && command->Address == HOMEGENIE_X10RF_MODULE_ADDRESS - && command->Command == "Control.SendRaw") { - - uint8_t data[command->OptionsString.length() / 2]; Utility::getBytes(command->OptionsString, data); - // Disable RFReceiver callbacks during transmission to prevent echo - noInterrupts(); - homeGenie.getIOManager().getX10Transmitter().sendCommand(data, sizeof(data)); - interrupts(); - command->Response = R"({ "ResponseText": "OK" })"; - - return true; - } else if (command->Domain == (IOEventDomains::HomeAutomation_X10)) { - uint8_t data[5]; - auto hu = command->Address; hu.toLowerCase(); - int h = (int)hu.charAt(0) - (int)HOUSE_MIN; // house code 0..15 - int u = hu.substring(1).toInt() - UNIT_MIN; // unit code 0..15 - auto moduleStatus = &moduleList[h][u]; - - auto x10Message = X10Message(); - x10Message.houseCode = HouseCodeLut[h]; - x10Message.unitCode = UnitCodeLut[u]; - - uint8_t sendRepeat = 0; // fallback to default repeat (3) - bool ignoreCommand = false; - - auto currentTime = NetManager::getTimeClient().getFormattedDate(); - QueuedMessage m = QueuedMessage(command->Domain, command->Address, (IOEventPaths::Status_Level), ""); - if (command->Command == "Control.On") { - x10Message.command = X10::Command::CMD_ON; - moduleStatus->Level = 1; - } else if (command->Command == "Control.Off") { - x10Message.command = X10::Command::CMD_OFF; - moduleStatus->Level = 0; - } else if (command->Command == "Control.Level") { - float level = command->getOption(0).toFloat()/100.0f; - sendRepeat = abs((level-moduleStatus->Level) / X10_DIM_BRIGHT_STEP); - if (level > moduleStatus->Level) { - x10Message.command = X10::Command::CMD_BRIGHT; - moduleStatus->Level += (sendRepeat * X10_DIM_BRIGHT_STEP); - if (moduleStatus->Level > 1) moduleStatus->Level = 1; - } else if (level < moduleStatus->Level) { - x10Message.command = X10::Command::CMD_DIM; - moduleStatus->Level -= (sendRepeat * X10_DIM_BRIGHT_STEP); - if (moduleStatus->Level < 0) moduleStatus->Level = 0; - } - if (sendRepeat == 0) { - ignoreCommand = true; - } else { - sendRepeat += 2; // improve initial burst detection - } - } else if (command->Command == "Control.Toggle") { - if (moduleStatus->Level > 0) { - x10Message.command = X10::Command::CMD_OFF; - moduleStatus->Level = 0; - } else { - x10Message.command = X10::Command::CMD_ON; - moduleStatus->Level = 1; - } - } else return false; - - moduleStatus->UpdateTime = currentTime; - - m.value = String(moduleStatus->Level); - homeGenie.getEventRouter().signalEvent(m); - - if (!ignoreCommand) { - X10::X10Message::encodeCommand(&x10Message, data); - noInterrupts(); - homeGenie.getIOManager().getX10Transmitter().sendCommand(&data[1], sizeof(data)-1, sendRepeat); - interrupts(); - } - command->Response = R"({ "ResponseText": "OK" })"; - - return true; - } - - return false; - } - - bool X10Handler::canHandleDomain(String &domain) { - return domain == (IO::IOEventDomains::HomeAutomation_X10); - } - - bool X10Handler::handleEvent(HomeGenie &homeGenie, IIOEventSender *sender, const unsigned char *eventPath, void *eventData, IOEventDataType dataType) { - - String domain = String((char*)sender->getDomain()); - String address = String((char*)sender->getAddress()); - String event = String((char*)eventPath); - /* - * X10 RF Receiver "Sensor.RawData" event - */ - if (address == HOMEGENIE_X10RF_MODULE_ADDRESS && event == (IOEventPaths::Sensor_RawData) /*&& ioManager.getX10Receiver().isEnabled()*/) { - // decode event data (X10 RF packet) - auto data = ((uint8_t *) eventData); - /// \param type Type of message (eg. 0x20 = standard, 0x29 = security, ...) - /// \param b0 Byte 1 - /// \param b1 Byte 2 - /// \param b2 Byte 3 - /// \param b3 Byte 4 - uint8_t type = data[0]; - uint8_t b0 = data[1]; - uint8_t b1 = data[2]; - uint8_t b2 = data[3]; - uint8_t b3 = data[4]; - Logger::info(":%s [X10::RFReceiver] >> [%s%s%s%s%s%s]", HOMEGENIEMINI_NS_PREFIX, - Utility::byteToHex(type).c_str(), - Utility::byteToHex((b0)).c_str(), - Utility::byteToHex((b1)).c_str(), - Utility::byteToHex(b2).c_str(), - Utility::byteToHex(b3).c_str(), - (type == 0x29) ? "0000" : "" - ); - - // Decode RF message data to X10Message class - auto *decodedMessage = new X10Message(); - uint8_t encodedMessage[5]{type, b0, b1, b2, b3}; - X10Message::decodeCommand(encodedMessage, decodedMessage); - - // Convert enums to string - String houseCode(house_code_to_char(decodedMessage->houseCode)); - String unitCode(unit_code_to_int(decodedMessage->unitCode)); - Logger::trace(":%s %s%s %s", HOMEGENIEMINI_NS_PREFIX, houseCode.c_str(), unitCode.c_str(), - cmd_code_to_str(decodedMessage->command)); - - // NOTE: Calling `getMQTTServer().broadcast(..)` out of the loop() would cause crashing, - // NOTE: so an `eventsQueue` is used to store messages that are then sent in the `loop()` - // NOTE: method. Currently the queue will only hold one element but it can be used as a real - // NOTE: queue by processing queued elements at every n-th loop() cycle (currently the queue - // NOTE: is processed at every cycle). (not sure if it would be of any use though) - - // MQTT Message Queue (enqueue) - QueuedMessage m = QueuedMessage(domain, houseCode + unitCode, (IOEventPaths::Status_Level), ""); - switch (decodedMessage->command) { - case Command::CMD_ON: - // TODO: update moduleList as well! - m.value = "1"; - homeGenie.getEventRouter().signalEvent(m); - break; - case Command::CMD_OFF: - // TODO: update moduleList as well! - m.value = "0"; - homeGenie.getEventRouter().signalEvent(m); - break; -// TODO: Implement all X10 events + Camera and Security - } - - delete decodedMessage; - - // TODO: blink led ? (visible feedback) - //digitalWrite(LED_BUILTIN, LOW); // turn the LED on (HIGH is the voltage level) - //delay(10); // wait for a blink - //digitalWrite(LED_BUILTIN, HIGH); - - return true; - } - - return false; - } - -}} diff --git a/test/README b/test/README index 3be2da0..9b1e87b 100644 --- a/test/README +++ b/test/README @@ -1,5 +1,5 @@ -This directory is intended for PIO Unit Testing and project tests. +This directory is intended for PlatformIO Test Runner and project tests. Unit Testing is a software testing method by which individual units of source code, sets of one or more MCU program modules together with associated @@ -7,5 +7,5 @@ control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use. Unit testing finds problems early in the development cycle. -More information about PIO Unit Testing: -- https://docs.platformio.org/page/plus/unitCode-testing.html +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html