diff --git a/CanGrow.geany b/CanGrow.geany new file mode 100644 index 0000000..bdea1c0 --- /dev/null +++ b/CanGrow.geany @@ -0,0 +1,61 @@ +[editor] +line_wrapping=false +line_break_column=72 +auto_continue_multiline=true + +[file_prefs] +final_new_line=true +ensure_convert_new_lines=false +strip_trailing_spaces=false +replace_tabs=false + +[indentation] +indent_width=2 +indent_type=0 +indent_hard_tab_width=8 +detect_indent=false +detect_indent_width=false +indent_mode=2 + +[project] +name=CanGrow +base_path=./ +description= +file_patterns=.ino,;.h; + +[long line marker] +long_line_behaviour=1 +long_line_column=72 + +[files] +current_page=0 +FILE_NAME_0=493;Sh;0;EUTF-8;0;1;0;.%2Fcangrow.sh;0;2 +FILE_NAME_1=0;Arduino;0;EUTF-8;0;1;0;.%2FCanGrow.ino;0;2 +FILE_NAME_2=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow.h;0;2 +FILE_NAME_3=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow_Core.h;0;2 +FILE_NAME_4=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow_ESP32.h;0;2 +FILE_NAME_5=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow_ESP8266.h;0;2 +FILE_NAME_6=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow_LittleFS.h;0;2 +FILE_NAME_7=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow_Logo.h;0;2 +FILE_NAME_8=0;C++;0;EUTF-8;0;1;0;.%2Finclude%2FCanGrow_Version.h;0;2 + +[build-menu] +C++FT_00_LB=_Compile +C++FT_00_CM=cd .. ; ./cangrow.sh build +C++FT_00_WD= +filetypes=C++;Arduino;Sh; +ArduinoFT_00_LB=_Build +ArduinoFT_00_CM=./cangrow.sh build +ArduinoFT_00_WD= +ArduinoFT_01_LB=Build & Upload +ArduinoFT_01_CM=./cangrow.sh upload +ArduinoFT_01_WD= +C++FT_01_LB=_Build & Upload +C++FT_01_CM=cd .. ; ./cangrow.sh upload +C++FT_01_WD= +ShFT_00_LB=Build +ShFT_00_CM=./cangrow.sh build +ShFT_00_WD= +ShFT_01_LB=Build & Upload +ShFT_01_CM=./cangrow.sh upload +ShFT_01_WD= diff --git a/CanGrow.ino b/CanGrow.ino new file mode 100644 index 0000000..23d1540 --- /dev/null +++ b/CanGrow.ino @@ -0,0 +1,216 @@ +/* + * + * CanGrow - an OpenSource growcontroller firmware (for cannabis) + * + * + * MIT License + * + * Copyright (c) 2024 DeltaLima + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +/* + * Libraries include + */ + +#include "Arduino.h" + +// * ESP8266 * +#ifdef ESP8266 + #include <ESP8266WiFi.h> + #include <ESPAsyncTCP.h> + #include <ESP8266HTTPClient.h> + #include <WiFiClient.h> +#endif + +// * ESP32 * +#ifdef ESP32 + #include <WiFi.h> + #include <AsyncTCP.h> + #include <Update.h> + #include <HTTPClient.h> +#endif + +#include <WiFiUdp.h> + +// https://github.com/thijse/Arduino-Log/ +#include <ArduinoLog.h> + +// https://github.com/mathieucarbou/ESPAsyncWebServer +#include <ESPAsyncWebServer.h> + +// LittleFS filesystem +#include "FS.h" +// arduino-core for esp8266 and esp32 +#include "LittleFS.h" + +//#include <SPI.h> +#include <Wire.h> + +// https://github.com/bblanchon/ArduinoJson +#include <ArduinoJson.h> +#include "AsyncJson.h" + +// https://github.com/PaulStoffregen/Time +#include <TimeLib.h> +// https://github.com/arduino-libraries/NTPClient/ +#include <NTPClient.h> + +// https://github.com/nusabot-iot/NusabotSimpleTimer/ +#include <NusabotSimpleTimer.h> + +// https://github.com/adafruit/RTClib/ +#include "RTClib.h" + +/* + * CanGrow includes + */ + +/* main header file, where all variables, consts and structs get defined */ +#include "include/CanGrow.h" +/* CanGrow platform specific includes */ +#include "include/Architecture/ESP8266.h" +#include "include/Architecture/ESP32.h" +#include "include/Architecture/ESP32_LOLIN_S2_MINI.h" +#include "include/Architecture/ESP32_MAKERGO_C3_SUPERMINI.h" + +/* CanGrow header with all functions + * order is important - I need to learn how to do it right, so order is not important */ +#include "include/CanGrow_ConfigHelper.h" +#include "include/CanGrow_Sensor.h" +#include "include/CanGrow_Output.h" +#include "include/CanGrow_Core.h" +#include "include/CanGrow_Wifi.h" +#include "include/CanGrow_LittleFS.h" + +#include "include/CanGrow_Control.h" +#include "include/CanGrow_Timer.h" +#include "include/CanGrow_Webserver.h" + + + +void setup() { + /* Measure start up time */ + unsigned long millisFinish; + // define output for onboard LED/WIPE pin + pinMode(PinWIPE, OUTPUT); + + + // Start Serial + Serial.begin(115200); + + // Write a line before doing serious output, because before there is some garbage in serial + // whats get the cursor somewhere over the place + Serial.println("420"); + + // initiate ArduinoLog + + Log.setPrefix(LogPrefix); + Log.begin(LOG_LEVEL_VERBOSE, &Serial); + // disable show loglevel, we do it in Prefix + Log.setShowLevel(false); + // set Log Location, to tell user at which part of the code we are + const char LogLoc[] = "[SETUP]"; + + //Serial.printf(".:: CanGrow firmware v%s build %s starting ::.\n", CANGROW_VER, CANGROW_BUILD); + Log.notice(F("CanGrow firmware v%s build %s starting ::" CR), CANGROW_VER, CANGROW_BUILD); + + Log.warning(F("%s To format / factory reset LittleFS, pull GPIO %d (PinWIPE) to %d - NOW! (2 seconds left)" CR), LogLoc, PinWIPE, 1 - PinWIPE_default ); + + // blink with the onboard LED on D4/GPIO2 (PinWIPE) + for(byte i = 0; i <= 6 ; i++) { + if(i % 2) { + digitalWrite(PinWIPE, 1 - PinWIPE_default); + } else { + digitalWrite(PinWIPE, PinWIPE_default); + } + delay(333); + } + + // set PinWIPE back to its default + digitalWrite(PinWIPE, PinWIPE_default); + + // read status from PinWIPE to WIPE + // when PinWIPE is set to LOW, format LittleFS + if(digitalRead(PinWIPE) != PinWIPE_default) { + LFS_Format(); + Restart(); + } + /* for ESP32-C3 supermini board compatibility, we initiate I2C here and not at the beginning + * ESP32-C3 supermini board shares GPIO 8 Internal LED with I2C SDA */ + /* I2C init*/ + Wire.begin(Pin_I2C_SDA, Pin_I2C_SCL); + + LFS_Init(); + LoadConfig(); + Wifi_Init(); + Webserver_Init(); + + Log.notice(F("%s Usable Pins: %d" CR), LogLoc, GPIOindex_length); + // List all available pins + for(byte i = 1; i <= GPIOindex_length; i++) { + Log.notice(F("%s Pin Index: %d, GPIO: %d, Notes: %s" CR), LogLoc, i , GPIOindex[i].gpio, GPIO_Index_note_descr[GPIOindex[i].note]); + } + + // time init + Time_Init(); + TimeR_Init(); + + + #ifdef ESP8266 + /* set pwm frequency global for ESP8266. + * ESP32 pwm frequency setting is done withing CanGrow_Output / Init */ + analogWriteFreq(config.system.pwmFreq); + #endif + + Output_Init(); + + Sensor_Init(); + + Log.notice(F("%s Done. Startup took : %u ms" CR), LogLoc, millis()); +} + +bool alrdySaved = false; + +void loop() { + const char LogLoc[] = "[LOOP]"; + + /* Execute main timer, runs Timer_1s, Timer_3s, Timer_5s by default */ + timer.run(); + + // if global var doRestart is true, perform a restart + if(doRestart == true) { + /* wait 100ms after Restart got triggered. This should workaround some crash problems with AsyncWebserver stuff + * for example when updating the firmware by web upload */ + Log.verbose(F("%s Restart got triggered. Waiting 100ms before doing it" CR), LogLoc); + timer.setTimeout(100, Restart); + //Restart(); + } + + // does ntp offset need an update? + if(updateNtpOffset) { + /* doing ntp offset update here, because when doing it in the webserver:system function + * where the new value gets entered, it sometimes crashed */ + NTP_OffsetUpdate(); + updateNtpOffset = false; + } +} diff --git a/README.md b/README.md index 4f25752..36449d7 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,53 @@ -# CanGrow +# CanGrow - An OpenSource grow controller firmware for ESP8266 / ESP32 + - +## Build environment +The helper script `cangrow.sh` is written for a Debian 12 system. -An easy to use DIY grow controller firmware (for cannabis). +To install all dependencies you need for building the firmware, run the cangrow.sh setup: - - +```sh +$ ./cangrow.sh help +./cangrow.sh [setup|build|upload|webupload|monitor] +setup: setup build environment, download arduino-cli, install all dependencies for arduino ide +build: build firmware binary. will be saved into build/ +upload: upload firmware by serial connection /dev/ttyUSB0 +webupload: upload firmware with webupload to 192.168.4.20 +monitor: serial monitor /dev/ttyUSB0 -# WORK IN PROGRESS +# Install all dependencies for build environment +$ ./cangrow.sh setup +``` -## Motivation -I havn't found an already existing grow controller project within the ESP / Arduino Core eco system which -met my personal requirements. -Those are an easy DIY, using low cost parts, Arduino Core sourcecode to hack own things together, having a WebUI, grab some Metrics for monitoring, standalone and my very special need that the Hardware should run completely with 12V. +The script installs [arduino-cli](https://github.com/arduino/arduino-cli) to `~/.local/bin/arduino-cli`. -### Update 14.09.2024 - Code Rewrite v0.2 +## Compile -I took some "summer break" from the project, and had the opportunity to talk to different people about it. -My conclusion at this point is, that the focus of this project is not the Hardware, it came out that it should be the software. -So I decided to completely rewrite the code from 0 - with recycling some parts of it. -Goal of the Rewrite is that the Firmware becomes more independent of the hardware used. It has to support both ESP8266 and ESP32 -and let the user decide at which pin which output, sensor or whatever will be connected to. Like done in the [Tasmota](https://github.com/arendst/Tasmota) Firmware, I also want to support "Hardware Templates" which come with presets for PCBs like the one I created. +```sh +# compile and output to build/CanGrow_v0.2...bin +# Default Target is ESP8266 D1 Mini +$ ./cangrow.sh build -**Checklist for v0.2 Firmware** -- Support ESP8266 and ESP32 -- AsyncWebserver instead ESP8266Webserver -- LittleFS instead of EEPROM() -- deliver static HTML, dynamic Stuff with Javascript - - (or is there a better way? please tell me!) -- Free configurable outputs - - Main outputs for Light, Air, Water - - Support for Tasmota Wifi Plugs (and others?) - - No Limitation for Amount of outputs - - Light - - support for I2C 0-10V Dimm control - - PWM dimm control - - Air - - support for I2C 0-10V Dimm control - - PWM dimm control - - Support for humidifier, heater (, CO2?) - - Read Fan RPM - - Water - - Usual watering - - Pump for fertilizer -- Free configurable Inputs - - Support for various I2C devices - - All kind of sensors for Temp, Humidity, Moisture, and so on - - Support for ADCs to connect multiple analoge sensors - - Support for Analog inputs - - onboard ones or I2C (ADC) - - Analog Multiplexer support (like CD4051) - - Calibrate sensors - - define 0% and 100% values - - Offsets -- MQTT support -- API +# Compile for ESP32 D1 Mini +$ export BOARD="esp32:esp32:d1_mini32" +$ ./cangrow.sh build - - -## Old v0.1 Features / ToDo List +# Build and webupload to IP +$ export IP="192.168.4.69" +$ ./cangrow.sh build # need to make .bin first +$ ./cangrow.sh webupload # upload -- Measure values :white_check_mark: - - Humidity :white_check_mark: - - soil moisture :white_check_mark: - - temperature :white_check_mark: - - water level for water tank :white_check_mark: -- LED grow light control (on/off, dimming, max. 12V 50W load ) :white_check_mark: - - You can of course use a relais as well, if you want to drive 220V lights :white_check_mark: -- fan control (on/off, (PWM?) max 1A) :white_check_mark: -- pump control for automatic watering (max 1A) :large_blue_circle: -- Web UI and REST API for data and controlling :large_blue_circle: - - simple web ui :white_check_mark: - - REST API :large_blue_circle: - - Send notifications with web call (e.g. for mastodon) :red_circle: - - predefined grow profiles :large_blue_circle: - - persistent data :white_check_mark: - - Start of Grow :white_check_mark: - - day of grow :large_blue_circle: - - grow profile - - watering amount per week :large_blue_circle: - - light cycle :white_check_mark: - - wifi settings :white_check_mark: - - settings in general :white_check_mark: -- Easy to build and use for beginners (i hope so!) :white_check_mark: - - PCB layout to order from manufacture (jlcpcb or pcbway) :white_check_mark: - - easy to build up on a perfboard :white_check_mark: - - easy to etch pcb :white_check_mark: - - easy to access and modify :white_check_mark: - - low cost as possible! :white_check_mark: +# listen to serial monitor on /dev/ttyUSB2 +$ export TTY="/dev/ttyUSB2" +./cangrow.sh monitor +``` -:white_check_mark: Done - :large_blue_circle: In Progress - :red_circle: ToDo +I wrote this project using [Geany IDE. ](https://www.geany.org/). The Geany Projectfile is also included, just run +```sh +$ geany CanGrow.geany +``` + +**F8 compiles** the project, **F9 uploads** firmware to /dev/ttyUSB0. You can change these settings for .ino and .h files +in Project -> Settings -> Create/Make. diff --git a/Screenshot_montage.png b/Screenshot_montage.png new file mode 100644 index 0000000..7fbb2d6 Binary files /dev/null and b/Screenshot_montage.png differ diff --git a/allbuild.sh b/allbuild.sh new file mode 100755 index 0000000..1ecf7a4 --- /dev/null +++ b/allbuild.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# + +rm -Rf build/* +for board in esp8266:esp8266:d1_mini_clone esp32:esp32:d1_mini32 esp32:esp32:makergo_c3_supermini esp32:esp32:lolin_s2_mini +do + echo "Build firmware binary for $board" + echo "===================================================================" + BOARD="$board" ./cangrow.sh build +done diff --git a/arduino-cli.yml b/arduino-cli.yml new file mode 100644 index 0000000..29db8f5 --- /dev/null +++ b/arduino-cli.yml @@ -0,0 +1,4 @@ +board_manager: + additional_urls: + - http://arduino.esp8266.com/stable/package_esp8266com_index.json + - https://espressif.github.io/arduino-esp32/package_esp32_index.json diff --git a/cangrow.sh b/cangrow.sh new file mode 100755 index 0000000..eda39dc --- /dev/null +++ b/cangrow.sh @@ -0,0 +1,167 @@ +#!/bin/bash +# + +test -z $TTY && TTY="/dev/ttyUSB0" +test -z $IP && IP="192.168.4.20" +test -z $VER && VER="$(grep "define CANGROW_VER" include/CanGrow.h | cut -d \" -f2 |sed -e 's/\"//g')" #VER="0.2-dev" +test -z $BOARD && BOARD="esp8266:esp8266:d1_mini_clone" +#test -z $BOARD && BOARD="esp32:esp32:d1_mini32" + +BUILD="$(git rev-parse --short HEAD)-$(echo $BOARD | cut -d : -f1)_$(echo $BOARD | cut -d : -f3)-$(date '+%Y%m%d%H%M%S')" + +# arduino-cli path and version +ACLI="$HOME/.local/bin/arduino-cli" +ACLI_VER="1.2.0" +ACLI_CMD="$ACLI --config-file arduino-cli.yml" +test -z $BUILDDIR && BUILDDIR="build" + + +function help() { + echo "$0 [setup|build|upload|webupload|monitor]" + echo "setup: setup build environment, download arduino-cli, install all dependencies for arduino ide" + echo "build: build firmware binary. will be saved into ${BUILDDIR}/" + echo "upload: upload firmware by serial connection $TTY" + echo "webupload: upload firmware with webupload to $IP" + echo "monitor: serial monitor $TTY" + exit 1 +} + +function check_acli() { + if [ ! -x $ACLI ] + then + echo "$ACLI does not exist nor is executable. Please run '$0 setup' first" + exit 1 + fi +} + +test -z $1 && help + +case $1 in + s|setup) + ACLI_DIR="$(dirname $ACLI)" + ALIB_DIR="${HOME}/Arduino/libraries/" + declare -a CORES=( + "esp8266:esp8266@3.1.2" + "esp32:esp32@3.0.7" + ) + declare -a LIBS=( + "Adafruit SSD1306@2.5.12" + "Adafruit BME280 Library@2.2.4" + "ArduinoJson@7.3.0" + "NTPClient@3.2.1" + "Time@1.6.1" + "ESP Async WebServer@3.6.0" + "Async TCP@3.3.2" + "Nusabot Simple Timer@1.0.0" + "ArduinoLog@1.1.1" + "RTClib@2.1.4" + "Adafruit BME680 Library@2.0.5" + "Adafruit ADS1X15@2.5.0" + "Adafruit SHT31 Library@2.2.2" + "Adafruit MCP4725@2.0.2" + "Adafruit TCS34725@1.4.4" + "Adafruit MLX90614 Library@2.1.5" + "I2CSoilMoistureSensor@1.1.4" + "DFRobot_GP8XXX@1.0.1" + "Adafruit CCS811 Library@1.1.3" + ) + + echo ":: Setting up build environment for CanGrow Firmware." + echo " This will download the binary for arduino-cli and install" + echo " the packages for the arduino ide from the debian repository." + echo " !! This script is meant to be executed on a Debian stable (bookworm) system !!" + echo "" + echo ":: Press Enter to continue" + read + echo "" + echo ":: Installing Arduino IDE packages with apt, please enter sudo password:" + sudo apt update || exit 1 + sudo apt install arduino python3 python3-serial wget curl xxd || exit 1 + echo ":: Ensure directory ${ACLI_DIR} is present" + test -d ${ACLI_DIR} || mkdir -p ${ACLI_DIR} + echo ":: Please ensure ${ACLI_DIR} is in your \$PATH, I wont do it." + echo "" + echo ":: Downloading arduino-cli ${ACLI_VER} into ${ACLI_DIR}/" + wget -O - "https://github.com/arduino/arduino-cli/releases/download/v${ACLI_VER}/arduino-cli_${ACLI_VER}_Linux_64bit.tar.gz" | tar -C ${ACLI_DIR} -zxvf - arduino-cli + chmod +x ${ACLI} + echo "" + echo ":: Installing ESP8266 and ESP32 cores for Arduino" + for core in ${!CORES[@]} + do + ${ACLI_CMD} core install ${CORES[$core]} + done + echo ":: Installing Arduino libraries" + ${ACLI_CMD} lib update-index || exit 1 + for lib in ${!LIBS[@]} + do + echo " - ${LIBS[$lib]}" + done + + for lib in ${!LIBS[@]} + do + ${ACLI_CMD} lib install "${LIBS[$lib]}" || exit 1 + done + echo "" + + echo ":: fetching ESPAsyncTCP-esphome from GIT" + wget -q https://github.com/mathieucarbou/esphome-ESPAsyncTCP/archive/refs/tags/v2.0.0.tar.gz -O - | tar -xzf - -C $ALIB_DIR + mv $ALIB_DIR/esphome-ESPAsyncTCP-2.0.0 $ALIB_DIR/ESPAsyncTCP-esphome + echo ":: Patching ArduinoLog (https://github.com/thijse/Arduino-Log/pull/28/commits/57d350a25428376935b793a2138210320cf3801c)" + sed -i -e 's/register//g' $ALIB_DIR/ArduinoLog/ArduinoLog.cpp + + echo ":: Setup build environment done! You can now build the firmware" + echo " with: $0 build" + + ;; + b|build) + check_acli + ACLI_CMD="${ACLI_CMD} --output-dir ${BUILDDIR}" + echo ":: Building firmware $VER $BUILD, target dir: ${BUILDDIR}/" + + test -d ${BUILDDIR} || mkdir ${BUILDDIR} + + + # esp8266 and esp32 compiler have to use different compile flags for VER and BUILD + if [ "$(echo $BOARD | cut -d : -f1)" == "esp8266" ] + then + ${ACLI_CMD} --no-color compile -b ${BOARD} --build-property "build.extra_flags=-DCANGROW_VER=\"${VER}\" -DCANGROW_BUILD=\"${BUILD}\"" "CanGrow.ino" || exit 1 + elif [ "$(echo $BOARD | cut -d : -f1)" == "esp32" ] + then + ${ACLI_CMD} --no-color compile -b ${BOARD} --build-property "build.defines=-DCANGROW_VER=\"${VER}\" -DCANGROW_BUILD=\"${BUILD}\"" "CanGrow.ino" || exit 1 + fi + + cp ${BUILDDIR}/CanGrow.ino.bin ${BUILDDIR}/CanGrow_v${VER}_${BUILD}.bin + ;; + u|upload) + check_acli + echo ":: Build and upload firmware $VER $BUILD to $TTY" + + test -d build || mkdir build + + # esp8266 and esp32 compiler have to use different compile flags for VER and BUILD + if [ "$(echo $BOARD | cut -d : -f1)" == "esp8266" ] + then + ${ACLI_CMD} --no-color compile -b ${BOARD} --build-property "build.extra_flags=-DCANGROW_VER=\"${VER}\" -DCANGROW_BUILD=\"${BUILD}\"" ${ACLI_BUILD_OPTS} -u -p $TTY "CanGrow.ino" + elif [ "$(echo $BOARD | cut -d : -f1)" == "esp32" ] + then + ${ACLI_CMD} --no-color compile -b ${BOARD} --build-property "build.defines=-DCANGROW_VER=\"${VER}\" -DCANGROW_BUILD=\"${BUILD}\"" ${ACLI_BUILD_OPTS} -u -p $TTY "CanGrow.ino" + fi + + ;; + w|webupload) + test -z "$2" && UPLOAD_FILE="${BUILDDIR}/CanGrow.ino.bin" + test -n "$2" && UPLOAD_FILE="$2" + + echo ":: Uploading $UPLOAD_FILE to $IP" + curl -v http://$IP/system/update -X POST -H 'Content-Type: multipart/form-data' -F "firmware=@${UPLOAD_FILE}" + echo + ;; + m|mon|monitor) + check_acli + echo ":: Open serial monitor $TTY" + ${ACLI_CMD} monitor -c baudrate=115200 -b ${BOARD} -p $TTY + ;; + *) + help + ;; +esac diff --git a/include/Architecture/ESP32.h b/include/Architecture/ESP32.h new file mode 100644 index 0000000..af3331c --- /dev/null +++ b/include/Architecture/ESP32.h @@ -0,0 +1,69 @@ +/* + * + * include/CanGrow_ESP32.h - ESP32 specific header file for generic ESP32_DEV board + * + * + * + * + */ +#if defined(ARDUINO_ESP32_DEV) || defined(ARDUINO_D1_MINI32) + +#define PinWIPE 2 +#define PinWIPE_default LOW +#define Pin_I2C_SCL 22 +#define Pin_I2C_SDA 21 + +/* https://randomnerdtutorials.com/esp32-pinout-reference-gpios/ + * + * free usable pins + * - GPIO 0 PU OK outputs PWM signal at boot, must be LOW to enter flashing mode + * - GPIO 4 OK OK + * - GPIO 5 OK OK outputs PWM signal at boot, strapping pin + * - GPIO 12 OK OK boot fails if pulled high, strapping pin + * - GPIO 13 OK OK + * - GPIO 14 OK OK outputs PWM signal at boot + * - GPIO 15 OK OK outputs PWM signal at boot, strapping pin + * - GPIO 16 OK OK + * - GPIO 17 OK OK + * - GPIO 18 OK OK + * - GPIO 19 OK OK + * - GPIO 23 OK OK + * - GPIO 25 OK OK + * - GPIO 26 OK OK + * - GPIO 27 OK OK + * - GPIO 32 OK OK + * - GPIO 33 OK OK + * - GPIO 34 OK input only + * - GPIO 35 OK input only + * - GPIO 36 OK input only + * - GPIO 39 OK input only + */ + + +// +const byte GPIOindex_length = 21; +// initialize pinIndex with all usable GPIOs +GPIO_Index GPIOindex[] = {{ 255, 255 }, + { 0, FLASHMODE_LOW }, + { 4 }, + { 5 }, + { 12, BOOTFAILS_HIGH }, + { 13 }, + { 14 }, + { 15 }, + { 16 }, + { 17 }, + { 18 }, + { 19 }, + { 23 }, + { 25, INT_DAC }, + { 26, INT_DAC }, + { 27 }, + { 32, INT_ADC }, + { 33, INT_ADC }, + { 34, INPUT_ONLY }, + { 35, INPUT_ONLY }, + { 36, INPUT_ONLY }, + { 39, INPUT_ONLY } + }; +#endif diff --git a/include/Architecture/ESP32_LOLIN_S2_MINI.h b/include/Architecture/ESP32_LOLIN_S2_MINI.h new file mode 100644 index 0000000..fedcbaf --- /dev/null +++ b/include/Architecture/ESP32_LOLIN_S2_MINI.h @@ -0,0 +1,74 @@ +/* + * + * include/CanGrow_ESP32.h - ESP32 specific header file for Lolin S2 Mini + * + * + * + */ +#ifdef ARDUINO_LOLIN_S2_MINI + +#define PinWIPE 15 +#define PinWIPE_default LOW +#define Pin_I2C_SCL 33 +#define Pin_I2C_SDA 35 + + +/* https://done.land/components/microcontroller/families/esp/esp32/developmentboards/esp32-s2/s2mini/ + * + * free usable pins + Pin Remark Description + EN Reset button + 3V3 direct power supply to CPU + VBUS connected to ME6211C33 voltage regulator + 0 not exposed Boot button pulls it low + 1-6 general purpose: analog input (ADC1) and digital in/output + 7 SPI SCK general purpose: analog input (ADC1) and digital in/output + 8 general purpose: analog input (ADC1) and digital in/output + 9 SPI MISO general purpose: analog input (ADC1) and digital in/output + 10 general purpose: analog input (ADC1) and digital in/output + 11 SPI MOSI general purpose: analog input (ADC2) and digital in/output + 12 SPI SS general purpose: analog input (ADC2) and digital in/output + 13-14 general purpose: analog input (ADC2) and digital in/output + 15 internal LED general purpose: analog input (ADC2) and digital in/output + 16 general purpose: analog input (ADC2) and digital in/output + 17 DAC1 general purpose: analog input (ADC2) and digital in/output + 18 DAC2 general purpose: analog input (ADC2) and digital in/output + 19, 20 not exposed USB D1/D2, connected to the USB C connector + 21 general purpose digital in/output + 33 I2C SDA general purpose digital in/output + 34 general purpose digital in/output + 35 I2C SCL general purpose digital in/output + 36-40 general purpose digital in/output + */ + + +// +const byte GPIOindex_length = 24; +// initialize pinIndex with all usable GPIOs +GPIO_Index GPIOindex[] = {{ 255, 255 }, + { 1, INT_ADC }, + { 2, INT_ADC }, + { 3, INT_ADC }, + { 4, INT_ADC }, + { 5, INT_ADC }, + { 6, INT_ADC }, + { 7, INT_ADC }, + { 8, INT_ADC }, + { 9, INT_ADC }, + { 10, INT_ADC }, + { 11 }, + { 12 }, + { 13 }, + { 14 }, + { 16 }, + { 17, INT_DAC }, + { 18, INT_DAC }, + { 21 }, + { 34 }, + { 36 }, + { 37 }, + { 38 }, + { 39 }, + { 40 } + }; +#endif diff --git a/include/Architecture/ESP32_MAKERGO_C3_SUPERMINI.h b/include/Architecture/ESP32_MAKERGO_C3_SUPERMINI.h new file mode 100644 index 0000000..2201151 --- /dev/null +++ b/include/Architecture/ESP32_MAKERGO_C3_SUPERMINI.h @@ -0,0 +1,48 @@ +/* + * + * include/Platform/ESP32_MAKERGO_C3_SUPERMINI.h - ESP32 specific header file + * + * + * + */ +#ifdef ARDUINO_MAKERGO_C3_SUPERMINI + +#define PinWIPE 8 +#define PinWIPE_default HIGH +#define Pin_I2C_SCL 9 +#define Pin_I2C_SDA 8 + +/* https://www.sudo.is/docs/esphome/boards/esp32c3supermini/ + * + * free usable pins + 0 GPIO0 ADC1 +1 GPIO1 ADC1 +2 GPIO2 ADC1, boot mode / strapping pin +3 GPIO3 ADC1 +4 GPIO4 ADC1, JTAG +5 GPIO5 JTAG +6 GPIO6 JTAG +7 GPIO7 JTAG +8 GPIO8 Blue status_led (inverted), boot mode / strapping pin +9 GPIO9 Boot mode / strapping pin, boot button +10 GPIO10 +20 GPIO20 RX +21 GPIO21 TX + */ + + +// +const byte GPIOindex_length = 9; +// initialize pinIndex with all usable GPIOs +GPIO_Index GPIOindex[] = {{ 255, 255 }, + { 0, INT_ADC }, + { 1, INT_ADC }, + { 2, INT_ADC }, + { 3, INT_ADC }, + { 4, INT_ADC }, + { 5 }, + { 6 }, + { 7 }, + { 10 } + }; +#endif diff --git a/include/Architecture/ESP8266.h b/include/Architecture/ESP8266.h new file mode 100644 index 0000000..a061bbc --- /dev/null +++ b/include/Architecture/ESP8266.h @@ -0,0 +1,51 @@ +/* + * + * include/CanGrow_ESP8266.h - ESP8266 specific header file + * + * + * + */ +#ifdef ESP8266 + +// GPIO 2 Boot fails if pulled to LOW +#define PinWIPE 2 +#define PinWIPE_default HIGH +#define Pin_I2C_SCL 5 +#define Pin_I2C_SDA 4 + +/* https://randomnerdtutorials.com/esp8266-pinout-reference-gpios/ + * + * free usable pins + * - GPIO 0 / D3 boot fails if pulled LOW + * - GPIO 12 / D6 + * - GPIO 13 / D7 + * - GPIO 14 / D5 + * - GPIO 15 / D8 Boot fails if pulled HIGH + * - GPIO 16 / D0 + */ + +const byte GPIOindex_length = 6; +// initialize pinIndex with all usable GPIOs +GPIO_Index GPIOindex[] = {{ 255, 255 }, + { 0, BOOTFAILS_LOW }, + { 12 }, + { 13 }, + { 14 }, + { 15, BOOTFAILS_HIGH }, + { 16, NO_PWM } }; + +#endif + + +/* CanGrow 12V PCB v0.6 Pin assignment + * + * + * LED - D6 (GPIO 12) + * FAN1 - D5 (GPIO 14) + * FAN2 - D3 (GPIO 0) + * PUMP - D0 (GPIO 16) + * + * WaterlevelVCC - D7 (GPIO 13) + * SoilmoistureVCC - D8 (GPIO 15) + * + */ diff --git a/include/CanGrow.h b/include/CanGrow.h new file mode 100644 index 0000000..7d405ab --- /dev/null +++ b/include/CanGrow.h @@ -0,0 +1,411 @@ +/* + * + * include/CanGrow.h - main header file + * + * + * + */ + +/* If you need detailed debug output, uncomment the following lines. + * DEBUG is less noisy messages + * DEBUG2 are noisy messages + * DEBUG3 are super noisy messages */ +//#define DEBUG +//#define DEBUG2 +//#define DEBUG3 + +/* ensure the code will also compile when CANGROW_VER and CANGROW_BUILD + * are not defined by the compiler arguments + * like -DCANGROW_VER="0.x-dev" or -DCANGROW_BUILD="commitid-core-timestamp" +*/ + + +/* + * + * + * Constants + * + * + */ + +#ifndef CANGROW_VER +#define CANGROW_VER "0.2-dev2" +#endif +#ifndef CANGROW_BUILD +#define CANGROW_BUILD "0420" +#endif +#ifndef CANGROW_BUILDTIME +#define CANGROW_BUILDTIME "1711922400" // 1.4.2024 +#endif + +#define CANGROW_DEFAULT_WIFI_SSID "CanGrow-unconfigured" +#define CANGROW_DEFAULT_WIFI_PASSWORD "letitgrow!" + +#define CANGROW_CFG "/config.json" +#define TIME2FS "/time" + +/* define Max limits for outputs and sensors */ +const byte Max_Outputs = 16; +const byte Max_Sensors = 16; +/* How much values can a sensor contain at max */ +const byte Max_Sensors_Read = 6; +/* how much GPIOs a Sensor can use */ +const byte Max_Sensors_GPIO = 2; + + +/* actual structure initialization for GPIO_Index is done within the header files + * for ESP32 and ESP8266 + * + * GPIO_Index.note explenation: + * 1 - BOOTFAILS_LOW: BootFails when LOW + * 2 - BOOTFAILS_HIGH: BootFails when HIGH + * 3 - FLASHMODE_LOW: FlashMode needs LOW to enter + * 4 - INPUT_ONLY: Input Only + * 5 - NO_PWM: No PWM output + * 6 - PWM_BOOT: PWM at boot time + * 7 - INT_ADC Pin for internal ADC (only ESP32, ESP8266 only has one Pin, A0) + */ +const byte BOOTFAILS_LOW = 1; +const byte BOOTFAILS_HIGH = 2; +const byte FLASHMODE_LOW = 3; +const byte INPUT_ONLY = 4; +const byte NO_PWM = 5; +const byte HIGH_BOOT = 6; +const byte INT_ADC = 7; +const byte INT_DAC = 8; + + +//const char signMessage[] PROGMEM = {"I AM PREDATOR, UNSEEN COMBATANT. CREATED BY THE UNITED STATES DEPART"}; + + +const char BOOTFAILS_LOW_descr[] PROGMEM = {"BF_LOW"}; +const char BOOTFAILS_HIGH_descr[] PROGMEM = {"BF_HIGH"}; +const char FLASMODE_LOW_descr[] PROGMEM = {"FM_LOW"}; +const char INPUT_ONLY_descr[] PROGMEM = {"IN_ONLY"}; +const char NO_PWM_descr[] PROGMEM = {"NO_PWM"}; +const char HIGH_BOOT_descr[] PROGMEM = {"B_HIGH"}; +const char INT_ADC_descr[] PROGMEM = {"INT_ADC"}; +const char INT_DAC_descr[] PROGMEM = {"INT_DAC"}; + +const char * GPIO_Index_note_descr[] = { + NULL, // 0 - no note + BOOTFAILS_LOW_descr, // 1 + BOOTFAILS_HIGH_descr, // 2 + FLASMODE_LOW_descr, // 3 + INPUT_ONLY_descr, // 4 + NO_PWM_descr, // 5 + HIGH_BOOT_descr, // 6 + INT_ADC_descr, // 7 + INT_DAC_descr, // 8 +}; + + +/* + * RTCs available + */ + +// 0 is unconfigured +const byte RTCs_total = 4; + +const byte RTCs_DS1307 = 1; +const byte RTCs_DS3231 = 2; +const byte RTCs_PCF8523 = 3; +const byte RTCs_PCF8563 = 4; + +const char RTCs_DS1307_descr[] PROGMEM = {"DS1307"}; +const char RTCs_DS3231_descr[] PROGMEM = {"DS3231"}; +const char RTCs_PCF8523_descr[] PROGMEM = {"PCF8523"}; +const char RTCs_PCF8563_descr[] PROGMEM = {"PCF8563"}; + +const char * RTCs_descr[] = { + NULL, // unconfigured + RTCs_DS1307_descr, + RTCs_DS3231_descr, + RTCs_PCF8523_descr, + RTCs_PCF8563_descr, +}; + + +/* + * Time scales + */ + +// 0 is unconfigured +const byte TIMESCALE_total = 7; + +const byte TIMESCALE_SECOND = 0; +const byte TIMESCALE_MINUTE = 1; +const byte TIMESCALE_HOUR = 2; +const byte TIMESCALE_DAY = 3; +const byte TIMESCALE_WEEK = 4; +const byte TIMESCALE_MONTH = 5; +const byte TIMESCALE_YEAR = 6; + + +const char TIMESCALE_SECOND_descr[] PROGMEM = {"Second"}; +const char TIMESCALE_MINUTE_descr[] PROGMEM = {"Minute"}; +const char TIMESCALE_HOUR_descr[] PROGMEM = {"Hour"}; +const char TIMESCALE_DAY_descr[] PROGMEM = {"Day"}; +const char TIMESCALE_WEEK_descr[] PROGMEM = {"Week"}; +const char TIMESCALE_MONTH_descr[] PROGMEM = {"Month"}; +const char TIMESCALE_YEAR_descr[] PROGMEM = {"Year"}; + +const char * Timescale_descr[] = { + TIMESCALE_SECOND_descr, + TIMESCALE_MINUTE_descr, + TIMESCALE_HOUR_descr, + TIMESCALE_DAY_descr, + TIMESCALE_WEEK_descr, + TIMESCALE_MONTH_descr, + TIMESCALE_YEAR_descr, +}; + + + +/* GPIO Index struct + * filled with CanGrow_ESP8266.h and CanGrow_ESP32.h + */ + +struct GPIO_Index { + const byte gpio; + const byte note; +}; + + +/* + * + * Config + * + * Note: when adding/removing/changing a saved Config variable + * you have to touch the config struct, LoadConfig() and SaveConfig() at least too! + */ + +/* + * Config WiFi + */ +struct Config_WiFi { + char ssid[32]; + char password[64]; + bool dhcp; + byte ip[4] = {192,168,4,20}; + byte netmask[4] = {255,255,255,0}; + byte gateway[4] = {0,0,0,0}; + byte dns[4] = {0,0,0,0}; +}; + + +/* + * Config System + */ + +struct Config_System_Output { + + /* + * Config System Output + * + * - type: output type like GPIO, I2C, URL + * 1 - GPIO + * 2 - I2C + * 3 - Web + * - device: what this output is connected to + * 1 - Light + * 2 - Fan + * 3 - Pump + * 4 - Humudifier + * 5 - Dehumidifier + * 6 - Heating + * - name: name of output + * - enabled: enable output + * - gpio: which gpio is used + * - invert: invert output + * - gpio_pwm: enable pwm for output + * - i2c: + * - webcall_host: ip to smart plug (tasmota e.g.) + * - webcall_path_on: GET request path to turn ON + * - webcall_path_off: GET request path to turn OFF + + * + */ + byte type[Max_Outputs]; + byte device[Max_Outputs]; + char name[Max_Outputs][32]; + bool enabled[Max_Outputs]; + byte gpio[Max_Outputs]; + bool gpio_pwm[Max_Outputs]; + bool invert[Max_Outputs]; + byte i2c_type[Max_Outputs]; + byte i2c_addr[Max_Outputs]; + byte i2c_port[Max_Outputs]; + char webcall_host[Max_Outputs][32]; + char webcall_path_on[Max_Outputs][32]; + char webcall_path_off[Max_Outputs][32]; + char webcall_user[Max_Outputs][32]; + char webcall_password[Max_Outputs][32]; +}; + +struct Config_System_Sensor { + /* + * Config System Sensor + * - type: Index ID of SensorIndex, which Sensor to use (ADC, BME280, Chirp, ...) + * - name: nice name + * - gpio[]: gpio to use for RPM reading, builtin ADC, OneWire, TwoWire + */ + + byte type[Max_Sensors]; + char name[Max_Sensors][32]; + byte i2c_addr[Max_Sensors]; + byte gpio[Max_Sensors][Max_Sensors_GPIO]; + float offset[Max_Sensors][Max_Sensors_Read]; + unsigned int low[Max_Sensors][Max_Sensors_Read]; + unsigned int high[Max_Sensors][Max_Sensors_Read]; + byte rawConvert[Max_Sensors][Max_Sensors_Read]; +}; + +/* main System struct */ +struct Config_System { + bool ntp = true; + byte rtc; + bool time2fs; + short ntpOffset; + unsigned short maintenanceDuration; + char esp32cam[16]; + char httpUser[32]; + char httpPass[32]; + bool httpLogSerial; + unsigned short schedulerInterval = 1000; + unsigned short pwmFreq = 13370; + Config_System_Output output; + Config_System_Sensor sensor; +}; + + + +/* + * Config Grow + */ + +struct Config_Grow_Light { + bool configured[Max_Outputs]; + byte output[Max_Outputs]; + byte sunriseHourVeg[Max_Outputs]; + byte sunriseMinuteVeg[Max_Outputs]; + byte sunsetHourVeg[Max_Outputs]; + byte sunsetMinuteVeg[Max_Outputs]; + + byte sunriseHourBloom[Max_Outputs]; + byte sunriseMinuteBloom[Max_Outputs]; + byte sunsetHourBloom[Max_Outputs]; + byte sunsetMinuteBloom[Max_Outputs]; + + byte power[Max_Outputs]; + bool fade[Max_Outputs]; + byte fadeDuration[Max_Outputs]; +}; + +struct Config_Grow_Air { + bool configured[Max_Outputs]; + byte output[Max_Outputs]; + byte power[Max_Sensors]; + byte controlSensor[Max_Outputs]; + byte controlRead[Max_Outputs]; + byte controlMode[Max_Outputs]; + float min[Max_Outputs]; + float max[Max_Outputs]; +}; + +struct Config_Grow_Water { + bool configured[Max_Outputs]; + byte output[Max_Outputs]; + byte controlSensor[Max_Outputs]; + byte controlRead[Max_Outputs]; + byte controlMode[Max_Outputs]; + byte onTime[Max_Sensors]; + byte min[Max_Sensors]; + byte max[Max_Sensors]; + byte interval[Max_Sensors]; + byte intervalUnit[Max_Sensors]; +}; + +struct Config_Grow_Dashboard { + bool configured[Max_Sensors][Max_Sensors_Read]; + byte sensor[Max_Sensors][Max_Sensors_Read]; +}; + +struct Config_Grow { + char name[64] = "CanGrow"; + unsigned long start; + byte daysVeg = 42; + byte daysBloom = 69; + Config_Grow_Light light; + Config_Grow_Air air; + Config_Grow_Water water; + Config_Grow_Dashboard dashboard; + //unsigned short dayOfGrow; + //byte daysSeed; + + //byte lightHoursVeg; + //byte lightHoursBloom; + //byte sunriseHour; + //byte sunriseMinute; + //bool sunFade; + //byte sunFadeDuration; +}; + + +/* + * main Config struct + */ +struct Config { + char test[16] = "123"; + Config_WiFi wifi; + Config_System system; + Config_Grow grow; + + +}; + +Config config; + + + + + +/* + * + * + * Global Runtime variables + * + * + */ + + +// do we need a restart? (e.g. after wifi settings change) +bool needRestart = false; +// this triggers Restart() from the main loop +bool doRestart = false; +// previous value of millis within the scheduler loop +unsigned long schedulerPrevMillis = 0; +/* in which time status is the system + * 0 - OK + * 1 - RTC fallback is used + * 2 - Time2FS fallback is used + */ +byte timeSrcStatus; + +/* rtcError - false no Error, true had error while init */ +bool rtcError = false; +// did ntp offset got changed? +bool updateNtpOffset = false; +/* sensorStatus[] to keep track if sensor init succeeded or not, true is OK */ +bool sensorStatus[Max_Sensors]; +/* outputStatus[] to keep track if output init succeeded or not, true is OK */ +bool outputStatus[Max_Outputs]; +/* outputState[] gets read by Output_Update() */ +byte outputState[Max_Outputs]; +/* keep track how often a http call failed */ +byte outputWebcallFailed[Max_Outputs]; + +/* remember timestamp when pump was turned on to turn it off after config.grow.water.onTime */ +unsigned long controlWaterLastStarted[Max_Outputs]; +/* remember timestamp when last water cycle was done.*/ +unsigned long controlWaterLast[Max_Outputs]; diff --git a/include/CanGrow_ConfigHelper.h b/include/CanGrow_ConfigHelper.h new file mode 100644 index 0000000..3e49a19 --- /dev/null +++ b/include/CanGrow_ConfigHelper.h @@ -0,0 +1,22 @@ +/* + * + * include/CanGrow_Core.h - core stuff header file + * + * + * + */ + +/* Give free grow.light id */ +byte Give_Free_Grow_LightId() { + byte freeId; + for(byte i = 0; i < Max_Outputs; i++) { + if(config.grow.light.configured[i] == true) { + // here i define that 255 stands for "no more free outputs" + freeId = 255; + } else { + freeId = i; + break; + } + } + return freeId; +} diff --git a/include/CanGrow_Control.h b/include/CanGrow_Control.h new file mode 100644 index 0000000..b05bd70 --- /dev/null +++ b/include/CanGrow_Control.h @@ -0,0 +1,407 @@ +/* + * + * include/CanGrow_Control.h - control stuff for light,air,water header file + * + * + * + */ + + +/* + * + * Light stuff + * + */ + +/* Light fade */ +byte Light_Power(byte id, unsigned int sunriseSec, unsigned int sunsetSec, unsigned int nowSec, bool shifted) { + const static char LogLoc[] PROGMEM = "[Control:Light_Power]"; + if(config.grow.light.fade[id] == true) { + unsigned int fadeDurationSec = config.grow.light.fadeDuration[id] * 60; + byte power_tmp; + //byte power_tmp; // = (durationSec - ((sunriseSec + durationSec) - nowSec) * config.grow.light.power[id] / durationSec); + + /* rising sun */ + if(nowSec <= sunriseSec + fadeDurationSec) { + /* calculate fade power value */ + //power_tmp = ( ( (nowSec - sunriseSec) / (fadeDurationSec / 255) ) * config.grow.light.power[id] ) / 255; + power_tmp = (fadeDurationSec - ((sunriseSec + fadeDurationSec) - nowSec)) * config.grow.light.power[id] / fadeDurationSec; + /* setting sun */ + } else if((nowSec >= sunsetSec - fadeDurationSec) && (nowSec <= sunsetSec)) { + /* calculate fade power value */ + //power_tmp = ( ( (sunsetSec - nowSec) / (fadeDurationSec / 255) ) * config.grow.light.power[id] ) / 255; + power_tmp = (sunsetSec - nowSec) * config.grow.light.power[id] / fadeDurationSec; + } else { + /* otherwise just turn the light on with configured value */ + power_tmp = config.grow.light.power[id]; + } + + //if(shifted == false) { + + //} else { + + //} + + #ifdef DEBUG + Log.verbose(F("%s Light %d - power_tmp %d" CR), LogLoc, id, power_tmp); + #endif + return power_tmp; + } else { + return config.grow.light.power[id]; + } + + + //return 0; +} +/* Function to set light based on time */ +void Control_Light() { + const static char LogLoc[] PROGMEM = "[Control:Light]"; + //Log.verbose(F("%s start %s %s" CR), LogLoc, Str_DateNow(), Str_TimeNow()); + /* iterate through all configured lights */ + for(byte i = 0; i < Max_Outputs; i++) { + if(config.grow.light.configured[i] == true) { + unsigned int nowSec = (hour() * 60 * 60) + (minute() * 60) + second(); + unsigned int sunriseSec; + unsigned int sunsetSec; + + /* check if veg or bloom */ + if((config.grow.start < 1) || (now() - config.grow.start <= config.grow.daysVeg * 24 * 60 * 60)) { + sunriseSec = (config.grow.light.sunriseHourVeg[i] * 60 * 60) + (config.grow.light.sunriseMinuteVeg[i] * 60); + sunsetSec = (config.grow.light.sunsetHourVeg[i] * 60 * 60) + (config.grow.light.sunsetMinuteVeg[i] * 60); + #ifdef DEBUG + Log.verbose(F("%s Veg" CR), LogLoc); + #endif + /* now > than veg = bloom */ + } else if(now() - config.grow.start > config.grow.daysVeg * 24 * 60 * 60) { + sunriseSec = (config.grow.light.sunriseHourBloom[i] * 60 * 60) + (config.grow.light.sunriseMinuteBloom[i] * 60); + sunsetSec = (config.grow.light.sunsetHourBloom[i] * 60 * 60) + (config.grow.light.sunsetMinuteBloom[i] * 60); + #ifdef DEBUG + Log.verbose(F("%s Bloom" CR), LogLoc); + #endif + /* now > than veg+bloom = harvest*/ + } //else if(now() - config.grow.start > (config.grow.daysVeg + config.grow.daysBloom) * 24 * 60 * 60)) { + //} + + + + + /* + * Sunrise / Day + */ + + /* when now is greater than sunrise AND sunsetTime is greater than sunrise */ + if((nowSec >= sunriseSec) && (nowSec < sunsetSec) && (sunsetSec > sunriseSec)) { + //outputState[i] = config.grow.light.power[i]; + outputState[i] = Light_Power(i, sunriseSec, sunsetSec, nowSec, false); + //Log.verbose(F("%s Light %d - nowSec %d - sunriseSec %d - sunsetSec %d - %s %s Day" CR), LogLoc, i, nowSec, sunriseSec, sunsetSec, Str_DateNow(), Str_TimeNow()); + + + /* when now is greater than sunrise OR */ + } else if(((nowSec >= sunriseSec) && (sunsetSec < sunriseSec)) || + /* when now is smaller than sunset AND sunset is + * smaller than sunrise - this is a shifted daytime */ + ((nowSec <= sunsetSec) && (sunsetSec < sunriseSec))) { + + //outputState[i] = config.grow.light.power[i]; + outputState[i] = Light_Power(i, sunriseSec, sunsetSec, nowSec, true); + //Log.verbose(F("%s Light %d - nowSec %d - sunriseSec %d - sunsetSec %d - %s %s Day (shifted)" CR), LogLoc, i, nowSec, sunriseSec, sunsetSec, Str_DateNow(), Str_TimeNow()); + + + } else { + /* otherwise its night, turn off the light */ + outputState[i] = 0; + //Log.verbose(F("%s Light %d - nowSec %d - sunriseSec %d - sunsetSec %d - %s %s Night" CR), LogLoc, i, nowSec, sunriseSec, sunsetSec, Str_DateNow(), Str_TimeNow()); + } + } + } +} + + +/* + * + * Air stuff + * + */ + + +/* + * Output Device + */ + +/* Air Mode definitions */ +// 0 is unconfigured +const byte CONTROL_AIR_MODE__TOTAL = 3; + +const byte CONTROL_AIR_MODE_ONOFF = 1; +const byte CONTROL_AIR_MODE_LINEAR = 2; +const byte CONTROL_AIR_MODE_STEPS = 3; + +const char CONTROL_AIR_MODE_ONOFF_descr[] PROGMEM = {"On/Off"}; +const char CONTROL_AIR_MODE_LINEAR_descr[] PROGMEM = {"Linear"}; +const char CONTROL_AIR_MODE_STEPS_descr[] PROGMEM = {"Steps"}; + +const char * Control_Air_Mode_descr[] = { + NULL, // 0 - no description because 0 means unconfigured + CONTROL_AIR_MODE_ONOFF_descr, + CONTROL_AIR_MODE_LINEAR_descr, + CONTROL_AIR_MODE_STEPS_descr, +}; + + +/* Air control modes themselfs */ + +byte Control_Air_Mode_OnOff(byte id) { + /* turns the output on or off, depending if the is within min and max */ + + /* if only min is set (max = 0), turn on when above it */ + if((config.grow.air.min[id] > 0) && (config.grow.air.max[id] == 0)) { + /* check if Sensor reading is above min value, then turn on */ + if(Sensor_getCalibratedValue(config.grow.air.controlSensor[id], config.grow.air.controlRead[id]) >= config.grow.air.min[id]) { + return config.grow.air.power[id]; + } else { + return 0; + } + + /* if only max is set (min = 0), turn off when above */ + } else if((config.grow.air.min[id] == 0) && (config.grow.air.max[id] > 0)) { + /* check if Sensor reading is under max value, then turn on */ + if(Sensor_getCalibratedValue(config.grow.air.controlSensor[id], config.grow.air.controlRead[id]) <= config.grow.air.max[id]) { + return config.grow.air.power[id]; + } else { + return 0; + } + /* when min and max are set (> 0) turn output on when within the given values */ + } else if((config.grow.air.min[id] > 0) && (config.grow.air.max[id] > 0)) { + if((Sensor_getCalibratedValue(config.grow.air.controlSensor[id], config.grow.air.controlRead[id]) >= config.grow.air.min[id]) && (Sensor_getCalibratedValue(config.grow.air.controlSensor[id], config.grow.air.controlRead[id]) <= config.grow.air.max[id])) { + return config.grow.air.power[id]; + } else { + return 0; + } + } + + return 0; +} + +byte Control_Air_Mode_Linear(byte id) { + /* if min and max are set */ + if((config.grow.air.min[id] > 0) && (config.grow.air.max[id] > 0)) { + /* return power value calculated with map() and contrain() + * multiply by 100 to "convert" the float to int. With constrain we prevent returning negative or out of range values */ + return map(constrain(Sensor_getCalibratedValue(config.grow.air.controlSensor[id], config.grow.air.controlRead[id]) * 100, config.grow.air.min[id] * 100, config.grow.air.max[id] * 100), + config.grow.air.min[id] * 100, + config.grow.air.max[id] * 100, + 0, + config.grow.air.power[id]); + } else { + return Control_Air_Mode_OnOff(id); + } + + return 0; +} + +byte Control_Air_Mode_Steps(byte id) { +/* if min and max are set */ + if((config.grow.air.min[id] > 0) && (config.grow.air.max[id] > 0)) { + /* return power value calculated with map() and contrain() + * multiply by 100 to "convert" the float to int. With constrain we prevent returning negative or out of range values */ + byte power_tmp = map(constrain(Sensor_getCalibratedValue(config.grow.air.controlSensor[id], config.grow.air.controlRead[id]) * 100, config.grow.air.min[id] * 100, config.grow.air.max[id] * 100), + config.grow.air.min[id] * 100, + config.grow.air.max[id] * 100, + 0, + config.grow.air.power[id]); + if(power_tmp == 0) { + return 0; + } else if(power_tmp < 64) { + return 64; + } else if(power_tmp < 128) { + return 128; + } else if(power_tmp < 192) { + return 192; + } else if(power_tmp < 255) { + return 192; + } else { + return 255; + } + } else { + return Control_Air_Mode_OnOff(id); + } + + return 0; +} + +/* Function to set air devices */ +void Control_Air() { + const static char LogLoc[] PROGMEM = "[Control:Air]"; + + /* iterate through all configured air devices */ + for(byte i = 0; i < Max_Outputs; i++) { + if(config.grow.air.configured[i] == true) { + /* check if a control Sensor reading is set. As SensorIndex starts by 0, 255 is "unset" */ + if((config.grow.air.controlSensor[i] < 255) && (config.grow.air.controlRead[i] < 255)) { + /* switch for control modes */ + switch(config.grow.air.controlMode[i]) { + + case CONTROL_AIR_MODE_ONOFF: + outputState[i] = Control_Air_Mode_OnOff(i); + break; + + case CONTROL_AIR_MODE_LINEAR: + outputState[i] = Control_Air_Mode_Linear(i); + break; + + case CONTROL_AIR_MODE_STEPS: + outputState[i] = Control_Air_Mode_Steps(i); + break; + + } + + /* if there is no control sensor reading selected, just set power */ + } else { + outputState[i] = config.grow.air.power[i]; + } + } + } +} + + + + + +/* + * + * Water stuff + * + */ + + +/* + * Output Device + */ + +/* Water Mode definitions */ +// 0 is unconfigured +const byte CONTROL_WATER_MODE__TOTAL = 3; + +const byte CONTROL_WATER_MODE_TIMEINTERVAL = 1; +const byte CONTROL_WATER_MODE_SENSOR_MIN_THRESHOLD = 2; +const byte CONTROL_WATER_MODE_SENSMIN_TIMEINT_COMBINED = 3; + +const char CONTROL_WATER_MODE_TIMEINTERVAL_descr[] PROGMEM = {"Timeinterval"}; +const char CONTROL_WATER_MODE_SENSOR_MIN_THRESHOLD_descr[] PROGMEM = {"Sensor min threshold"}; +const char CONTROL_WATER_MODE_SENSMIN_TIMEINT_COMBINED_descr[] PROGMEM = {"Sensor min + Timeinterval"}; + +const char * Control_Water_Mode_descr[] = { + NULL, // 0 - no description because 0 means unconfigured + CONTROL_WATER_MODE_TIMEINTERVAL_descr, + CONTROL_WATER_MODE_SENSOR_MIN_THRESHOLD_descr, + CONTROL_WATER_MODE_SENSMIN_TIMEINT_COMBINED_descr, +}; + + + + + +/* Function to set water devices */ +void Control_Water() { + const static char LogLoc[] PROGMEM = "[Control:Water]"; + + /* iterate through all configured water devices which have a control mode set */ + for(byte i = 0; i < Max_Outputs; i++) { + if((config.grow.water.configured[i] == true) && (config.grow.water.controlMode[i] > 0)) { + + /* which mode was set in config.grow.water.controlMode */ + switch(config.grow.water.controlMode[i]) { + + case CONTROL_WATER_MODE_TIMEINTERVAL: + // when diff of time now and time pumpLastOn is greater then water.interval, do some watering (Or manual watering) + if( (now() - controlWaterLast[i]) >= (config.grow.water.interval[i] * Timescale(config.grow.water.intervalUnit[i])) ) { + /* check if output is already on. If not so, we begin a watering cycle and remember the timestamp of it. */ + if(outputState[i] == 0) { + controlWaterLastStarted[i] = now(); + } + + /* when diff of now and controlWaterLastStarted is smaller than onTime, turn output on */ + if((now() - controlWaterLastStarted[i]) < config.grow.water.onTime[i]) { + /* at the moment i think PWM is not necessary here, so we set the output to 255 */ + outputState[i] = 255; + /* when onTime is exceeded, turn output off */ + } else { + outputState[i] = 0; + + /* remember when we finished watering */ + controlWaterLast[i] = now(); + + /* Todo, write controlWaterLast to LittleFS. */ + } + } else { + /* turn output off when interval is not exceeded */ + outputState[i] = 0; + } + break; + + case CONTROL_WATER_MODE_SENSOR_MIN_THRESHOLD: + /* when sensor reading config.grow.water.controlSensor is lower then config.grow.water.min , do some watering */ + if( (Sensor_getCalibratedValue(config.grow.water.controlSensor[i], config.grow.water.controlRead[i]) < config.grow.water.min[i]) || + /* or when the sensor value is larger than min but onTime has not exceeded yet */ + ((Sensor_getCalibratedValue(config.grow.water.controlSensor[i], config.grow.water.controlRead[i]) >= config.grow.water.min[i]) && ( (now() - controlWaterLastStarted[i]) < config.grow.water.onTime[i])) + ) { + /* check if output is already on. If not so, we begin a watering cycle and remember the timestamp of it. */ + if(outputState[i] == 0) { + controlWaterLastStarted[i] = now(); + } + + /* when diff of now and controlWaterLastStarted is smaller than onTime, turn output on */ + if((now() - controlWaterLastStarted[i]) < config.grow.water.onTime[i]) { + /* at the moment i think PWM is not necessary here, so we set the output to 255 */ + outputState[i] = 255; + /* when onTime is exceeded, turn output off */ + } else { + outputState[i] = 0; + } + /* turn output off when water conditions are not met */ + } else { + outputState[i] = 0; + } + break; + + case CONTROL_WATER_MODE_SENSMIN_TIMEINT_COMBINED: + // when diff of time now and time pumpLastOn is greater then water.interval AND sensor read value is below min + if( ( (now() - controlWaterLast[i]) >= (config.grow.water.interval[i] * Timescale(config.grow.water.intervalUnit[i])) ) && + ( (Sensor_getCalibratedValue(config.grow.water.controlSensor[i], config.grow.water.controlRead[i]) < config.grow.water.min[i]) || + /* or when the sensor value is larger than min but onTime has not exceeded yet */ + ((Sensor_getCalibratedValue(config.grow.water.controlSensor[i], config.grow.water.controlRead[i]) >= config.grow.water.min[i]) && ( (now() - controlWaterLastStarted[i]) < config.grow.water.onTime[i])) ) + ) { + /* check if output is already on. If not so, we begin a watering cycle and remember the timestamp of it. */ + if(outputState[i] == 0) { + controlWaterLastStarted[i] = now(); + } + + /* when diff of now and controlWaterLastStarted is smaller than onTime, turn output on */ + if((now() - controlWaterLastStarted[i]) < config.grow.water.onTime[i]) { + /* at the moment i think PWM is not necessary here, so we set the output to 255 */ + outputState[i] = 255; + /* when onTime is exceeded, turn output off */ + } else { + outputState[i] = 0; + + /* remember when we finished watering */ + controlWaterLast[i] = now(); + + /* Todo, write controlWaterLast to LittleFS. */ + } + } else { + /* turn output off when interval is not exceeded */ + outputState[i] = 0; + } + break; + + default: + /* when no mode is selected, turn output off */ + outputState[i] = 0; + break; + + } + } + /* if neither configured or mode set, force output being off */ + } +} diff --git a/include/CanGrow_Core.h b/include/CanGrow_Core.h new file mode 100644 index 0000000..9ed08ec --- /dev/null +++ b/include/CanGrow_Core.h @@ -0,0 +1,642 @@ +/* + * + * include/CanGrow_Core.h - core stuff header file + * + * + * + */ + +/* + * NTP Stuff + */ + +WiFiUDP ntpUDP; +NTPClient timeClient(ntpUDP); + +/* + * RTC Stuff + */ + +/* I would more like not to define four individual globals for each RTC type + * but Adafruit lib seems to work only this way - and i am too lazyscared to use + * some other lib or do it myself - so i hope this will not eat up my ram */ +RTC_DS1307 rtc_ds1307; +RTC_DS3231 rtc_ds3231; +RTC_PCF8523 rtc_pcf8523; +RTC_PCF8563 rtc_pcf8563; + + +/* + * Timer stuff + */ +NusabotSimpleTimer timer; + + +/* + * Logging stuff + * + * Example Log call + * const static char LogLoc[] PROGMEM= "[Some:Stuff:Happening]" + * Log.notice(F("%s This is %d" CR), LogLoc, i); + * + * LogLoc stands for "LogLocation" + */ + +/* Logging prefix */ +void LogPrefix(Print* _logOutput, int logLevel) { + //_logOutput->print(":: TEST"); + switch (logLevel) + { + default: + // silent + case 0:_logOutput->print("--" ); break; + // fatal + case 1:_logOutput->print("!!!! " ); break; + // error + case 2:_logOutput->print("!! " ); break; + // warning + case 3:_logOutput->print("!: "); break; + // info / notice + case 4:_logOutput->print(":: " ); break; + // trace + case 5:_logOutput->print("T: " ); break; + // verbose / debug + case 6:_logOutput->print("DB "); break; + } +} + +/* System core stuff , like restart , give free Id of xy, .. */ +void Restart() { + const static char LogLoc[] PROGMEM = "[Core:Restart]"; + Log.notice(F("%s got triggered, restarting in 2 seconds" CR), LogLoc); + + // blink fast with the built in LED in an infinite loop + byte i = 0; + while(i <= 16) { + if(i % 2) { + digitalWrite(PinWIPE, 1 - PinWIPE_default); + + } else { + digitalWrite(PinWIPE, PinWIPE_default); + + } + i++; + delay(125); + } + ESP.restart(); + +} + + +// IP2Char helper function to convert ip arrarys to char arrays +char* IP2Char(IPAddress ipaddr){ + // https://forum.arduino.cc/t/trouble-returning-char-array-string/473246/6 + static char buffer[18]; + sprintf(buffer, "%d.%d.%d.%d", ipaddr[0], ipaddr[1], ipaddr[2], ipaddr[3] ); + return buffer; +} + +byte Give_Free_OutputId() { + const static char LogLoc[] PROGMEM = "[Core:Give_Free_OutputId]"; + byte outputId_free; + for(byte i=0; i < Max_Outputs; i++) { + if(config.system.output.type[i] > 0) { + // here i define that 255 stands for "no more free outputs" + outputId_free = 255; + } else { + outputId_free = i; + break; + } + } + #ifdef DEBUG + Log.verbose(F("%s next free output id: %d" CR), LogLoc, outputId_free); + #endif + return outputId_free; +} + +byte Give_Free_SensorId() { + const static char LogLoc[] PROGMEM = "[Core:Give_Free_SensorId]"; + + byte sensorId_free; + for(byte i=0; i < Max_Sensors; i++) { + if(config.system.sensor.type[i] > 0) { + // here i define that 255 stands for "no more free outputs" + sensorId_free = 255; + } else { + sensorId_free = i; + break; + } + } + #ifdef DEBUG + Log.verbose(F("%s next free sensor id: %d" CR), LogLoc, sensorId_free); + #endif + return sensorId_free; +} + + + +// checks if GPIO is already in use by output or sensor +bool Check_GPIOindex_Used(byte gpio) { + const static char LogLoc[] PROGMEM = "[Core:Check_GPIOindex_Used]"; + + bool used; + + //Log.verbose(F("%s check GPIO: %d" CR), LogLoc, gpio); + + // go through each outputid + for(byte i=0; i < Max_Outputs; i++) { + + // check if output type is gpio + if(config.system.output.type[i] == OUTPUT_TYPE_GPIO) { + #ifdef DEBUG + Log.verbose(F("%s OutputId: %d is GPIO (type %d)" CR), LogLoc, i, config.system.output.type[i]); + #endif + // check if gpio id is already in use + if(config.system.output.gpio[i] == gpio) { + #ifdef DEBUG + Log.verbose(F("%s output.gpio[%d](%d) == GPIO %d" CR), LogLoc, i, config.system.output.gpio[i], gpio); + #endif + used = true; + break; + } else { + used = false; + } + } + } + + if(used == false) { + for(byte i=0; i < Max_Sensors; i++) { + + // check if sensor type uses gpio + if((SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_INTADC) || (SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_ONEWIRE) || (SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_TWOWIRE)) { + #ifdef DEBUG + Log.verbose(F("%s SensorId: %d is using GPIO (type %d)" CR), LogLoc, i, config.system.sensor.type[i]); + #endif + // check if gpio id is already in use + for(byte j = 0; j < Max_Sensors_GPIO; j++) { + if(config.system.sensor.gpio[i][j] == gpio) { + #ifdef DEBUG + Log.verbose(F("%s sensor.gpio[%d][%d](%d) == GPIO %d" CR), LogLoc, i, j, config.system.sensor.gpio[i][j], gpio); + #endif + used = true; + break; + } else { + used = false; + } + } + } + } + } + #ifdef DEBUG + Log.verbose(F("%s GPIO: %d, used: %d" CR), LogLoc, gpio, used); + #endif + return used; +} + + +/* + * + * Time related stuff + * + */ + +/* + * NTP stuff + */ + +void NTP_OffsetUpdate() { + const static char LogLoc[] PROGMEM = "[Core:NTP_OffsetUpdate]"; + #ifdef DEBUG + Log.verbose(F("%s updating time with offset %dh" CR), LogLoc, config.system.ntpOffset); + #endif + timeClient.setTimeOffset(config.system.ntpOffset * 60 * 60); + if( (config.system.ntp == true) && (timeSrcStatus < 1) ) { + timeClient.update(); + setTime(timeClient.getEpochTime()); + } + #ifdef DEBUG + else { + Log.verbose(F("%s update requirements not met, timeSrcStatus %d > 0" CR), LogLoc, timeSrcStatus); + } + #endif +} + +bool NTP_Init() { + const static char LogLoc[] PROGMEM = "[Core:NTP_Init]"; + bool result; + timeClient.begin(); + NTP_OffsetUpdate(); + // when NTP update failes (e.g. no connection to internet) + Log.notice(F("%s updating " ), LogLoc); + + byte i = 0; + while( (! timeClient.isTimeSet()) && ( i < 5 )) { + timeClient.update(); + delay(100); + Serial.print("."); + i++; + } + Serial.println(); + + if( ! timeClient.isTimeSet()) { + Log.error(F("%s FAILED" CR), LogLoc); + //Serial.println("!! [Core:NTP_Init] update failed"); + result = false; + } else { + + Log.notice(F("%s Success! Time: %s (%u), Offset: %d h" CR), LogLoc, timeClient.getFormattedTime(), timeClient.getEpochTime(), config.system.ntpOffset); + + result = true; + } + return result; +} + + +time_t NTP_getEpochTime() { + /* convert epoch from ntp (UL) to time_t */ + const static char LogLoc[] PROGMEM = "[Core:NTP_getEpochTime]"; + unsigned long epochTime = timeClient.getEpochTime(); + Log.verbose(F("%s epochTime: %u" CR), LogLoc, epochTime); + return epochTime; +} + +/* + * RTC stuff + */ + +void RTC_Init() { + const static char LogLoc[] PROGMEM = "[Core:RTC_Init]"; + + switch(config.system.rtc) { + case RTCs_DS1307: + if (! rtc_ds1307.begin()) { + Log.warning(F("%s Couldn't find RTC DS1307" CR), LogLoc); + rtcError = true; + } else { + Log.notice(F("%s RTC DS1307 found" CR), LogLoc); + if (rtc_ds1307.isrunning()) { + Log.warning(F("%s RTC DS1307 is not running, let's set the time!" CR), LogLoc); + rtcError = true; + } + } + + break; + + case RTCs_DS3231: + if (! rtc_ds3231.begin()) { + Log.warning(F("%s Couldn't find RTC DS3231" CR), LogLoc); + rtcError = true; + } else { + Log.notice(F("%s RTC DS3231 found" CR), LogLoc); + if (rtc_ds3231.lostPower()) { + Log.warning(F("%s RTC DS3231 lost power, let's set the time!" CR), LogLoc); + rtcError = true; + } + } + + break; + + case RTCs_PCF8563: + if (! rtc_pcf8563.begin()) { + Log.warning(F("%s Couldn't find RTC PCF8563" CR), LogLoc); + rtcError = true; + + } else { + Log.notice(F("%s RTC PCF8563 found" CR), LogLoc); + if (rtc_pcf8563.lostPower()) { + Log.warning(F("%s RTC PCF8563 lost power, let's set the time!" CR), LogLoc); + rtcError = true; + } + } + rtc_pcf8563.start(); + break; + + case RTCs_PCF8523: + if (! rtc_pcf8523.begin()) { + Log.warning(F("%s Couldn't find RTC PCF8523" CR), LogLoc); + rtcError = true; + + } else { + Log.notice(F("%s RTC PCF8523 found" CR), LogLoc); + if ( ! rtc_pcf8523.initialized() || rtc_pcf8523.lostPower()) { + Log.warning(F("%s RTC PCF8523 lost power, let's set the time!" CR), LogLoc); + rtcError = true; + } + } + rtc_pcf8523.start(); + break; + + default: + break; + } + +} + + +time_t RTC_getEpochTime() { + /* convert epoch from RTC (UL) to time_t */ + const static char LogLoc[] PROGMEM = "[Core:RTC_getEpochTime]"; + unsigned long epochTime; // = timeClient.getEpochTime(); + DateTime TimeNow; + switch(config.system.rtc) { + case RTCs_DS1307: + TimeNow = rtc_ds1307.now(); + break; + + case RTCs_DS3231: + TimeNow = rtc_ds3231.now(); + break; + + case RTCs_PCF8523: + TimeNow = rtc_pcf8523.now(); + break; + + case RTCs_PCF8563: + TimeNow = rtc_pcf8563.now(); + break; + + default: + break; + } + epochTime = TimeNow.unixtime(); + Log.verbose(F("%s epochTime: %u" CR), LogLoc, epochTime); + return epochTime; +} + +void RTC_SaveTime() { + const static char LogLoc[] PROGMEM = "[Core:RTC_SaveTime]"; + unsigned int TimeNow = now(); + bool saved = true; + + switch(config.system.rtc) { + case RTCs_DS1307: + rtc_ds1307.adjust(DateTime(TimeNow)); + break; + + case RTCs_DS3231: + rtc_ds3231.adjust(DateTime(TimeNow)); + break; + + case RTCs_PCF8523: + rtc_pcf8523.adjust(DateTime(TimeNow)); + break; + + case RTCs_PCF8563: + rtc_pcf8563.adjust(DateTime(TimeNow)); + break; + + default: + /* only when not in case, we consider not saved */ + saved = false; + break; + } + #ifdef DEBUG + if(saved == true) + Log.verbose(F("%s Time (%u) saved to %S" CR), LogLoc, TimeNow, RTCs_descr[config.system.rtc]); + #endif +} + +/* + * Main Time stuff + * + */ + +String Str_TimeNow() { + /* simple helper function to return a String with HH:MM:SS */ + String str_time; + if(hour() < 10) + str_time += F("0"); + str_time += hour(); + str_time += F(":"); + if(minute() < 10) + str_time += F("0"); + str_time += minute(); + str_time += F(":"); + if(second() < 10) + str_time += F("0"); + str_time += second(); + return str_time; +} + +String Str_DateNow() { + /* simple helper function to return a String with HH:MM:SS */ + String str_date; + if(day() < 10) + str_date += F("0"); + str_date += day(); + str_date += F("."); + if(month() < 10) + str_date += F("0"); + str_date += month(); + str_date += F("."); + str_date += year(); + return str_date; +} + +String Str_Epoch2Date(unsigned long epochTime) { + String dateStr; + byte Day = day(epochTime); + byte Month = month(epochTime); + unsigned int Year = year(epochTime); + + dateStr = Year; + dateStr += "-"; + + if(Month < 10) { + dateStr += "0"; + dateStr += Month; + } else { + dateStr += Month; + } + + dateStr += "-"; + + if(Day < 10) { + dateStr += "0"; + dateStr += Day; + } else { + dateStr += Day; + } + + return dateStr; +} + + +/* Those two functions should be in LittleFS file, but because dependency and lazyness */ +void Time2FS_Save() { + const static char LogLoc[] PROGMEM = "[Core:Time2FS_Save]"; + unsigned long TimeNow; + #ifdef ESP8266 + File file = LittleFS.open(TIME2FS, "w"); + #endif + + #ifdef ESP32 + fs::FS &fs = LittleFS; + File file = fs.open(TIME2FS, FILE_WRITE); + #endif + + if (!file) { + Log.error(F("%s FAILED to open file for writing: %s" CR), LogLoc, TIME2FS); + return; + } + TimeNow = now(); + if (!file.print(TimeNow)) { + Log.error(F("%s writing time FAILED" CR), LogLoc); + } + #ifdef DEBUG + else { + Log.verbose(F("%s time (%u) written: %s" CR), LogLoc, TimeNow, TIME2FS); + } + #endif + //delay(2000); // Make sure the CREATE and LASTWRITE times are different + file.close(); + +} + +void Time2FS_Read() { + const static char LogLoc[] PROGMEM = "[Core:Time2FS_Read]"; + String TimeRead; + #ifdef ESP8266 + File file = LittleFS.open(TIME2FS, "r"); + #endif + + #ifdef ESP32 + fs::FS &fs = LittleFS; + File file = fs.open(TIME2FS); + #endif + + if (!file) { + Log.error(F("%s FAILED to open time file: %s" CR), LogLoc, TIME2FS); + return; + } + + //Log.notice(F("%s file content: %s" CR), LogLoc, TIME2FS); + //Log.notice(F("%s ----------" CR), LogLoc); + //while (file.available()) { Serial.write(file.read()); } + //Log.notice(F("%s ----------" CR), LogLoc); + + while (file.available()) { TimeRead = file.readString(); } + #ifdef DEBUG + Log.verbose(F("%s applying time (%u) to system" CR), LogLoc, TimeRead.toInt()); + #endif + setTime(TimeRead.toInt()); + file.close(); +} + + +/* Time_Init - Main function for time initialization */ +void Time_Init() { + const static char LogLoc[] PROGMEM = "[Core:Time_Init]"; + + /* first check if RTC is configured and init if */ + if(config.system.rtc > 0) + RTC_Init(); + + /* check if ntp is enabled */ + if(config.system.ntp == true) { + Log.notice(F("%s Using NTP" CR), LogLoc); + /* initialize NTP and check */ + if(NTP_Init()) { + #ifdef DEBUG + Log.verbose(F("%s set NTP as TimeLib SyncProvider" CR), LogLoc); + #endif + setSyncProvider(NTP_getEpochTime); + + /* when having a RTC, update it now with new not time */ + if(config.system.rtc > 0) { + RTC_SaveTime(); + } + + if(config.system.time2fs == true) { + true; + //writeFile(TIME2FS, now()); + Time2FS_Save(); + } + + } else if((config.system.rtc > 0) && (rtcError == false)) { + #ifdef DEBUG + Log.verbose(F("%s set RTC as TimeLib SyncProvider" CR), LogLoc); + #endif + setSyncProvider(RTC_getEpochTime); + //setTime(RTC_getEpochTime()); + timeSrcStatus = 1; + } else { + Log.warning(F("%s no TimeLib SyncProvider available. Reading last Timestamp from flash memory" CR), LogLoc); + Time2FS_Read(); + timeSrcStatus = 2; + } + } + /* how often TimeLib should sync with source + * 10 minutes is ok i guess + */ + setSyncInterval(600); + + Log.notice(F("%s Time initialization done. Fallback status %d, %s %s (%u)" CR), LogLoc, timeSrcStatus, Str_DateNow(), Str_TimeNow(), now()); + + +} + + +/* + * semi random string generator + * https://arduino.stackexchange.com/a/86659 + */ +const byte RANDOMSTRING_MAX = 16; +const char * RandomString(){ + /* Change to allowable characters */ + const char possible[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?_-=()%&.,;:"; + static char str[RANDOMSTRING_MAX + 1]; + for(byte p = 0, i = 0; i < RANDOMSTRING_MAX; i++){ + byte r = random(0, strlen(possible)); + str[p++] = possible[r]; + } + str[RANDOMSTRING_MAX] = '\0'; + return str; +} + + +/* + * Timescale() + * returns timescale unit (seconds, minutes, hours,...) in seconds + */ + +unsigned long Timescale(byte unit) { + switch(unit) { + case TIMESCALE_SECOND: + return 1; + break; + + case TIMESCALE_MINUTE: + return 60; + break; + + case TIMESCALE_HOUR: + //return 60*60; + return 3600; + break; + + case TIMESCALE_DAY: + //return 60*60*24; + return 86400; + break; + + case TIMESCALE_WEEK: + //return 60*60*24*7; + return 604800; + break; + + case TIMESCALE_MONTH: + //return 60*60*24*7*4; + return 2419200; + break; + + case TIMESCALE_YEAR: + //return 60*60*24*7*4*52; + return 125798400; + break; + + default: + return 0; + break; + } +} diff --git a/include/CanGrow_LittleFS.h b/include/CanGrow_LittleFS.h new file mode 100644 index 0000000..01210b1 --- /dev/null +++ b/include/CanGrow_LittleFS.h @@ -0,0 +1,855 @@ +/* + * + * include/CanGrow_LittleFS.h - LittleFS handling header file + * + * + * + */ + + +// LittleFS auto format +#define FORMAT_LITTLEFS_IF_FAILED true + +void LFS_Init() { + const static char LogLoc[] PROGMEM = "[LittleFS:Init]"; + Log.notice(F("%s" CR), LogLoc); + // ESP8266 crashes with first argument set + #ifdef ESP8266 + if(!LittleFS.begin()) { + #endif + // ESP32 works, do autoformat if mount fails + #ifdef ESP32 + if(!LittleFS.begin(FORMAT_LITTLEFS_IF_FAILED)) { + #endif + + Log.notice(F("%s FAILED initializing. You have to format LittleFS manually. Will now restart." CR), LogLoc); + Restart(); + } +} + +void LFS_Format() { + const static char LogLoc[] PROGMEM = "[LittleFS:Format]"; + Log.notice(F("%s formatting ..." CR), LogLoc); + // ESP32 LittleFS needs begin() first, otherwise it would crash + // ESP8266 does not need it, so we leave it + #ifdef ESP32 + LittleFS.begin(); + #endif + if(LittleFS.format()) { + Log.notice(F("%s done!" CR), LogLoc); + } else { + Log.error(F("%s FAILED" CR), LogLoc); + } +} + +bool existFile(const char *path) { + const static char LogLoc[] PROGMEM = "[LittleFS]"; + #ifdef ESP8266 + File file = LittleFS.open(path, "r"); + #endif + + #ifdef ESP32 + fs::FS &fs = LittleFS; + File file = fs.open(path); + #endif + + if (!file) { + Log.notice(F("%s file exists: %s" CR), LogLoc, path); + file.close(); + return false; + } else { + Log.warning(F("%s file does not exist: %s" CR), LogLoc, path); + file.close(); + return true; + } +} + +String readFile(const char *path) { + const static char LogLoc[] PROGMEM = "[LittleFS]"; + String fileContent; + + #ifdef ESP8266 + File file = LittleFS.open(path, "r"); + #endif + + #ifdef ESP32 + fs::FS &fs = LittleFS; + File file = fs.open(path); + #endif + + if (!file) { + Log.error(F("%s FAILED to open file for reading: %s" CR), LogLoc, path); + return String(F("ERROR CANNOT OPEN")); + } + + Log.notice(F("%s file content: %s" CR), LogLoc, path); + Log.notice(F("%s ----------" CR), LogLoc); + while (file.available()) { Serial.write(file.read()); } + Log.notice(F("%s ----------" CR), LogLoc); + fileContent = file.readString(); + file.close(); + return fileContent; +} + +void writeFile(const char *path, const char *message) { + const static char LogLoc[] PROGMEM = "[LittleFS]"; + + #ifdef ESP8266 + File file = LittleFS.open(path, "w"); + #endif + + #ifdef ESP32 + fs::FS &fs = LittleFS; + File file = fs.open(path, FILE_WRITE); + #endif + + if (!file) { + Log.error(F("%s FAILED to open file for reading: %s" CR), LogLoc, path); + return; + } + if (file.print(message)) { + Log.notice(F("%s file written: %s" CR), LogLoc, path); + } else { + Log.error(F("%s writing file FAILED: %s" CR), LogLoc, path); + } + //delay(2000); // Make sure the CREATE and LASTWRITE times are different + file.close(); +} + +void deleteFile(const char *path) { + const static char LogLoc[] PROGMEM = "[LittleFS]"; + + #ifdef ESP32 + fs::FS &fs = LittleFS; + File file = fs.open(path, FILE_WRITE); + #endif + + Log.notice(F("%s deleting file: %s" CR), LogLoc, path); + #ifdef ESP8266 + if (LittleFS.remove(path)) { + #endif + #ifdef ESP32 + if (fs.remove(path)) { + #endif + Log.notice(F("%s deleted file: %s" CR), LogLoc, path); + } else { + Log.error(F("%s deleting file FAILED: %s" CR), LogLoc, path); + } +} + +// https://arduinojson.org/v7/example/config/ +// https://arduinojson.org/v7/assistant/ +bool LoadConfig() { + const static char LogLoc[] PROGMEM = "[LittleFS:LoadConfig]"; + #ifdef ESP8266 + File file = LittleFS.open(CANGROW_CFG, "r"); + #endif + + #ifdef ESP32 + fs::FS &fs = LittleFS; + File file = fs.open(CANGROW_CFG); + #endif + + Log.notice(F("%s loading config from: %s" CR), LogLoc, CANGROW_CFG); + + JsonDocument doc; + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, file); + if(error) { + Log.error(F("%s FAILED to load config: %s" CR), LogLoc, CANGROW_CFG); + if (existFile(CANGROW_CFG)) { + readFile(CANGROW_CFG); + } + return false; + } + + /* + * put json values into config structs + */ + + // Copy strings from the JsonDocument to the Config struct as char + + strlcpy(config.test, doc["test"], sizeof(config.test)); + + /* WiFi */ + JsonObject objWifi = doc["wifi"][0]; + if(objWifi.containsKey("ssid")) + strlcpy(config.wifi.ssid, objWifi["ssid"], sizeof(config.wifi.ssid)); + if(objWifi.containsKey("password")) + strlcpy(config.wifi.password, objWifi["password"], sizeof(config.wifi.password)); + // Copy bool / int directly into struct + if(objWifi.containsKey("dhcp")) + config.wifi.dhcp = objWifi["dhcp"]; + // load the ip addresses as array + if(objWifi.containsKey("ip")) { + for(byte i=0; i < 4 ; i++) { + config.wifi.ip[i] = objWifi["ip"][i]; + config.wifi.netmask[i] = objWifi["netmask"][i]; + config.wifi.gateway[i] = objWifi["gateway"][i]; + config.wifi.dns[i] = objWifi["dns"][i]; + } + } + + + /* System */ + JsonObject objSystem = doc["system"][0]; + if(objSystem.containsKey("ntpOffset")) + config.system.ntpOffset = objSystem["ntpOffset"]; + if(objSystem.containsKey("maintenanceDuration")) + config.system.maintenanceDuration = objSystem["maintenanceDuration"]; + if(objSystem.containsKey("esp32cam")) + strlcpy(config.system.esp32cam, objSystem["esp32cam"], sizeof(config.system.esp32cam)); + if(objSystem.containsKey("httpUser")) + strlcpy(config.system.httpUser, objSystem["httpUser"], sizeof(config.system.httpUser)); + if(objSystem.containsKey("httpPass")) + strlcpy(config.system.httpPass, objSystem["httpPass"], sizeof(config.system.httpPass)); + if(objSystem.containsKey("httpLogSerial")) + config.system.httpLogSerial = objSystem["httpLogSerial"]; + + if(objSystem.containsKey("schedulerInterval")) + config.system.schedulerInterval = objSystem["schedulerInterval"]; + + if(objSystem.containsKey("ntp")) + config.system.ntp = objSystem["ntp"]; + if(objSystem.containsKey("rtc")) + config.system.rtc = objSystem["rtc"]; + if(objSystem.containsKey("time2fs")) + config.system.time2fs = objSystem["time2fs"]; + if(objSystem.containsKey("pwmFreq")) + config.system.pwmFreq = objSystem["pwmFreq"]; + + /* System Outputs */ + JsonObject objSystemOutput = objSystem["output"][0]; + for(byte i=0; i < Max_Outputs; i++) { + if(objSystemOutput["type"][i] > 0) { + + if(objSystemOutput.containsKey("type")) + config.system.output.type[i] = objSystemOutput["type"][i]; + if(objSystemOutput.containsKey("device")) + config.system.output.device[i] = objSystemOutput["device"][i]; + if(objSystemOutput.containsKey("name")) + strlcpy(config.system.output.name[i], objSystemOutput["name"][i], sizeof(config.system.output.name[i])); + if(objSystemOutput.containsKey("enabled")) + config.system.output.enabled[i] = objSystemOutput["enabled"][i]; + // gpio + if(objSystemOutput.containsKey("gpio")) + config.system.output.gpio[i] = objSystemOutput["gpio"][i]; + if(objSystemOutput.containsKey("invert")) + config.system.output.invert[i] = objSystemOutput["invert"][i]; + if(objSystemOutput.containsKey("gpio_pwm")) + config.system.output.gpio_pwm[i] = objSystemOutput["gpio_pwm"][i]; + // i2c type + if(objSystemOutput.containsKey("i2c_type")) + config.system.output.i2c_type[i] = objSystemOutput["i2c_type"][i]; + // i2c addr + if(objSystemOutput.containsKey("i2c_addr")) + config.system.output.i2c_addr[i] = objSystemOutput["i2c_addr"][i]; + // i2c port + if(objSystemOutput.containsKey("i2c_port")) + config.system.output.i2c_port[i] = objSystemOutput["i2c_port"][i]; + // web + if(objSystemOutput.containsKey("webcall_host")) + strlcpy(config.system.output.webcall_host[i], objSystemOutput["webcall_host"][i], sizeof(config.system.output.webcall_host[i])); + if(objSystemOutput.containsKey("webcall_path_on")) + strlcpy(config.system.output.webcall_path_on[i], objSystemOutput["webcall_path_on"][i], sizeof(config.system.output.webcall_path_on[i])); + if(objSystemOutput.containsKey("webcall_path_off")) + strlcpy(config.system.output.webcall_path_off[i], objSystemOutput["webcall_path_off"][i], sizeof(config.system.output.webcall_path_off[i])); + } + } + + + /* System Sensors */ + JsonObject objSystemSensor = objSystem["sensor"][0]; + for(byte i=0; i < Max_Sensors; i++) { + if(objSystemSensor["type"][i] > 0) { + if(objSystemSensor.containsKey("type")) + config.system.sensor.type[i] = objSystemSensor["type"][i]; + if(objSystemSensor.containsKey("name")) + strlcpy(config.system.sensor.name[i], objSystemSensor["name"][i], sizeof(config.system.sensor.name[i])); + if(objSystemSensor.containsKey("i2c_addr")) + //strlcpy(config.system.sensor.i2c_addr[i], objSystemSensor["i2c_addr"][i], sizeof(config.system.sensor.i2c_addr[i])); + config.system.sensor.i2c_addr[i] = objSystemSensor["i2c_addr"][i]; + // gpio + if(objSystemSensor.containsKey("gpio")) { + for(byte j = 0; j < Max_Sensors_GPIO; j++) { + config.system.sensor.gpio[i][j] = objSystemSensor["gpio"][i][j]; + } + } + + // offset + if(objSystemSensor.containsKey("offset")) { + for(byte j = 0; j < Max_Sensors_Read; j++) { + config.system.sensor.offset[i][j] = objSystemSensor["offset"][i][j]; + } + } + + // low + if(objSystemSensor.containsKey("low")) { + for(byte j = 0; j < Max_Sensors_Read; j++) { + config.system.sensor.low[i][j] = objSystemSensor["low"][i][j]; + } + } + + // high + if(objSystemSensor.containsKey("high")) { + for(byte j = 0; j < Max_Sensors_Read; j++) { + config.system.sensor.high[i][j] = objSystemSensor["high"][i][j]; + } + } + + // rawConvert + if(objSystemSensor.containsKey("rawConvert")) { + for(byte j = 0; j < Max_Sensors_Read; j++) { + config.system.sensor.rawConvert[i][j] = objSystemSensor["rawConvert"][i][j]; + } + } + + } + } + + + /* Grow */ + JsonObject objGrow = doc["grow"][0]; + if(objGrow.containsKey("name")) + strlcpy(config.grow.name, objGrow["name"], sizeof(config.grow.name)); + if(objGrow.containsKey("start")) + config.grow.start = objGrow["start"]; + if(objGrow.containsKey("daysVeg")) + config.grow.daysVeg = objGrow["daysVeg"]; + if(objGrow.containsKey("daysBloom")) + config.grow.daysBloom = objGrow["daysBloom"]; + + /* Grow Light */ + JsonObject objLight = objGrow["light"][0]; + for(byte i = 0; i < Max_Outputs; i++) { + /* get light.configured */ + if(objLight.containsKey("configured")) + config.grow.light.configured[i] = objLight["configured"][i]; + /* check if light is configured */ + if(config.grow.light.configured[i] == true) { + /* get the rest of the config */ + if(objLight.containsKey("output")) + config.grow.light.output[i] = objLight["output"][i]; + if(objLight.containsKey("sunriseHourVeg")) + config.grow.light.sunriseHourVeg[i] = objLight["sunriseHourVeg"][i]; + if(objLight.containsKey("sunriseMinuteVeg")) + config.grow.light.sunriseMinuteVeg[i] = objLight["sunriseMinuteVeg"][i]; + if(objLight.containsKey("sunsetHourVeg")) + config.grow.light.sunsetHourVeg[i] = objLight["sunsetHourVeg"][i]; + if(objLight.containsKey("sunsetMinuteVeg")) + config.grow.light.sunsetMinuteVeg[i] = objLight["sunsetMinuteVeg"][i]; + if(objLight.containsKey("sunriseHourBloom")) + config.grow.light.sunriseHourBloom[i] = objLight["sunriseHourBloom"][i]; + if(objLight.containsKey("sunriseMinuteBloom")) + config.grow.light.sunriseMinuteBloom[i] = objLight["sunriseMinuteBloom"][i]; + if(objLight.containsKey("sunsetHourBloom")) + config.grow.light.sunsetHourBloom[i] = objLight["sunsetHourBloom"][i]; + if(objLight.containsKey("sunsetMinuteBloom")) + config.grow.light.sunsetMinuteBloom[i] = objLight["sunsetMinuteBloom"][i]; + + if(objLight.containsKey("power")) + config.grow.light.power[i] = objLight["power"][i]; + if(objLight.containsKey("fade")) + config.grow.light.fade[i] = objLight["fade"][i]; + if(objLight.containsKey("fadeDuration")) + config.grow.light.fadeDuration[i] = objLight["fadeDuration"][i]; + } + } + + /* Grow Air */ + JsonObject objAir = objGrow["air"][0]; + for(byte i = 0; i < Max_Outputs; i++) { + /* get air.configured */ + if(objAir.containsKey("configured")) + config.grow.air.configured[i] = objAir["configured"][i]; + /* check if air is configured */ + if(config.grow.air.configured[i] == true) { + /* get the rest of the config */ + if(objAir.containsKey("output")) + config.grow.air.output[i] = objAir["output"][i]; + if(objAir.containsKey("power")) + config.grow.air.power[i] = objAir["power"][i]; + if(objAir.containsKey("controlSensor")) + config.grow.air.controlSensor[i] = objAir["controlSensor"][i]; + if(objAir.containsKey("controlRead")) + config.grow.air.controlRead[i] = objAir["controlRead"][i]; + if(objAir.containsKey("controlMode")) + config.grow.air.controlMode[i] = objAir["controlMode"][i]; + if(objAir.containsKey("min")) + config.grow.air.min[i] = objAir["min"][i]; + if(objAir.containsKey("max")) + config.grow.air.max[i] = objAir["max"][i]; + } + } + + /* Grow Water */ + JsonObject objWater = objGrow["water"][0]; + for(byte i = 0; i < Max_Outputs; i++) { + /* get air.configured */ + if(objWater.containsKey("configured")) + config.grow.water.configured[i] = objWater["configured"][i]; + /* check if air is configured */ + if(config.grow.water.configured[i] == true) { + /* get the rest of the config */ + if(objWater.containsKey("output")) + config.grow.water.output[i] = objWater["output"][i]; + if(objWater.containsKey("onTime")) + config.grow.water.onTime[i] = objWater["onTime"][i]; + if(objWater.containsKey("controlSensor")) + config.grow.water.controlSensor[i] = objWater["controlSensor"][i]; + if(objWater.containsKey("controlRead")) + config.grow.water.controlRead[i] = objWater["controlRead"][i]; + if(objWater.containsKey("controlMode")) + config.grow.water.controlMode[i] = objWater["controlMode"][i]; + if(objWater.containsKey("min")) + config.grow.water.min[i] = objWater["min"][i]; + if(objWater.containsKey("max")) + config.grow.water.max[i] = objWater["max"][i]; + if(objWater.containsKey("interval")) + config.grow.water.interval[i] = objWater["interval"][i]; + if(objWater.containsKey("intervalUnit")) + config.grow.water.intervalUnit[i] = objWater["intervalUnit"][i]; + } + } + + // Close the file (Curiously, File's destructor doesn't close the file) + file.close(); + Log.notice(F("%s config successfully loaded" CR), LogLoc); + #ifdef DEBUG + Log.verbose(F("%s --- runtime config ---" CR), LogLoc); + serializeJsonPretty(doc, Serial); + // Json output does not end with NewLine + Serial.println(""); + Log.verbose(F("%s ----------------------" CR), LogLoc); + #endif + return true; +} + +bool SaveConfig(bool writeToSerial = false) { + const static char LogLoc[] PROGMEM = "[LittleFS:SaveConfig]"; + /* + * Building config.json here + */ + JsonDocument doc; + + /* Root */ + doc["test"] = config.test; + + /* WiFi */ + JsonObject objWifi = doc["wifi"].add<JsonObject>(); + objWifi["ssid"] = config.wifi.ssid; + objWifi["password"] = config.wifi.password; + // save the ip addressess as array + int i; + for(i=0; i <4 ; i++) { + objWifi["ip"][i] = config.wifi.ip[i]; + objWifi["netmask"][i] = config.wifi.netmask[i]; + objWifi["gateway"][i] = config.wifi.gateway[i]; + objWifi["dns"][i] = config.wifi.dns[i]; + } + objWifi["dhcp"] = config.wifi.dhcp; + + /* System */ + JsonObject objSystem = doc["system"].add<JsonObject>(); + objSystem["ntpOffset"] = config.system.ntpOffset; + objSystem["maintenanceDuration"] = config.system.maintenanceDuration; + objSystem["esp32cam"] = config.system.esp32cam; + objSystem["httpUser"] = config.system.httpUser; + objSystem["httpPass"] = config.system.httpPass; + objSystem["httpLogSerial"] = config.system.httpLogSerial; + objSystem["schedulerInterval"] = config.system.schedulerInterval; + objSystem["ntp"] = config.system.ntp; + objSystem["rtc"] = config.system.rtc; + objSystem["time2fs"] = config.system.time2fs; + objSystem["pwmFreq"] = config.system.pwmFreq; + + /* System Outputs */ + JsonObject objSystemOutput = objSystem["output"].add<JsonObject>(); + for(byte i=0; i < Max_Outputs; i++) { + if(config.system.output.type[i] > 0) { + objSystemOutput["type"][i] = config.system.output.type[i]; + objSystemOutput["device"][i] = config.system.output.device[i]; + objSystemOutput["name"][i] = config.system.output.name[i]; + objSystemOutput["enabled"][i] = config.system.output.enabled[i]; + // gpio + objSystemOutput["gpio"][i] = config.system.output.gpio[i]; + objSystemOutput["invert"][i] = config.system.output.invert[i]; + objSystemOutput["gpio_pwm"][i] = config.system.output.gpio_pwm[i]; + // i2c type + objSystemOutput["i2c_type"][i] = config.system.output.i2c_type[i]; + objSystemOutput["i2c_addr"][i] = config.system.output.i2c_addr[i]; + objSystemOutput["i2c_port"][i] = config.system.output.i2c_port[i]; + // web + objSystemOutput["webcall_host"][i] = config.system.output.webcall_host[i]; + objSystemOutput["webcall_path_on"][i] = config.system.output.webcall_path_on[i]; + objSystemOutput["webcall_path_off"][i] = config.system.output.webcall_path_off[i]; + + } + } + + /* System Sensors */ + JsonObject objSystemSensor = objSystem["sensor"].add<JsonObject>(); + for(byte i=0; i < Max_Sensors; i++) { + if(config.system.sensor.type[i] > 0) { + objSystemSensor["type"][i] = config.system.sensor.type[i]; + objSystemSensor["name"][i] = config.system.sensor.name[i]; + objSystemSensor["i2c_addr"][i] = config.system.sensor.i2c_addr[i]; + for(byte j = 0; j < Max_Sensors_GPIO; j++) { + objSystemSensor["gpio"][i][j] = config.system.sensor.gpio[i][j]; + } + + /* offset reading */ + for(byte j = 0; j < Max_Sensors_Read; j++) { + objSystemSensor["offset"][i][j] = config.system.sensor.offset[i][j]; + } + + /* low reading */ + for(byte j = 0; j < Max_Sensors_Read; j++) { + objSystemSensor["low"][i][j] = config.system.sensor.low[i][j]; + } + + /* high reading */ + for(byte j = 0; j < Max_Sensors_Read; j++) { + objSystemSensor["high"][i][j] = config.system.sensor.high[i][j]; + } + + /* rawConvert reading */ + for(byte j = 0; j < Max_Sensors_Read; j++) { + objSystemSensor["rawConvert"][i][j] = config.system.sensor.rawConvert[i][j]; + } + } + } + + + /* Grow */ + JsonObject objGrow = doc["grow"].add<JsonObject>(); + objGrow["name"] = config.grow.name; + objGrow["start"] = config.grow.start; + objGrow["daysVeg"] = config.grow.daysVeg; + objGrow["daysBloom"] = config.grow.daysBloom; + + /* Grow Light */ + JsonObject objLight = objGrow["light"].add<JsonObject>(); + for(byte i = 0; i < Max_Outputs; i++) { + #ifdef DEBUG + Log.verbose(F("%s LightId %d, Max_Outputs %d, light.configured %T" CR), LogLoc, i, Max_Outputs, config.grow.light.configured[i]); + #endif + if(config.grow.light.configured[i] == true) { + objLight["configured"][i] = config.grow.light.configured[i]; + objLight["output"][i] = config.grow.light.output[i]; + objLight["sunriseHourVeg"][i] = config.grow.light.sunriseHourVeg[i]; + objLight["sunriseMinuteVeg"][i] = config.grow.light.sunriseMinuteVeg[i]; + objLight["sunsetHourVeg"][i] = config.grow.light.sunsetHourVeg[i]; + objLight["sunsetMinuteVeg"][i] = config.grow.light.sunsetMinuteVeg[i]; + + objLight["sunriseHourBloom"][i] = config.grow.light.sunriseHourBloom[i]; + objLight["sunriseMinuteBloom"][i] = config.grow.light.sunriseMinuteBloom[i]; + objLight["sunsetHourBloom"][i] = config.grow.light.sunsetHourBloom[i]; + objLight["sunsetMinuteBloom"][i] = config.grow.light.sunsetMinuteBloom[i]; + + objLight["power"][i] = config.grow.light.power[i]; + objLight["fade"][i] = config.grow.light.fade[i]; + objLight["fadeDuration"][i] = config.grow.light.fadeDuration[i]; + } + } + + /* Grow Air */ + JsonObject objAir = objGrow["air"].add<JsonObject>(); + for(byte i = 0; i < Max_Outputs; i++) { + //Log.verbose(F("%s LightId %d, Max_Outputs %d, light.configured %T" CR), LogLoc, i, Max_Outputs, config.grow.light.configured[i]); + if(config.grow.air.configured[i] == true) { + objAir["configured"][i] = config.grow.air.configured[i]; + objAir["output"][i] = config.grow.air.output[i]; + objAir["power"][i] = config.grow.air.power[i]; + objAir["controlSensor"][i] = config.grow.air.controlSensor[i]; + objAir["controlRead"][i] = config.grow.air.controlRead[i]; + objAir["controlMode"][i] = config.grow.air.controlMode[i]; + objAir["min"][i] = config.grow.air.min[i]; + objAir["max"][i] = config.grow.air.max[i]; + + } + } + + /* Grow Water */ + JsonObject objWater = objGrow["water"].add<JsonObject>(); + for(byte i = 0; i < Max_Outputs; i++) { + //Log.verbose(F("%s LightId %d, Max_Outputs %d, light.configured %T" CR), LogLoc, i, Max_Outputs, config.grow.light.configured[i]); + if(config.grow.water.configured[i] == true) { + objWater["configured"][i] = config.grow.water.configured[i]; + objWater["output"][i] = config.grow.water.output[i]; + objWater["onTime"][i] = config.grow.water.onTime[i]; + objWater["controlSensor"][i] = config.grow.water.controlSensor[i]; + objWater["controlRead"][i] = config.grow.water.controlRead[i]; + objWater["controlMode"][i] = config.grow.water.controlMode[i]; + objWater["min"][i] = config.grow.water.min[i]; + objWater["max"][i] = config.grow.water.max[i]; + objWater["interval"][i] = config.grow.water.interval[i]; + objWater["intervalUnit"][i] = config.grow.water.intervalUnit[i]; + + } + } + + + /* + * END Building config.json here + */ + + // if writeToSerial is true, output json to serial, but do not write to LittleFS + if(writeToSerial == false) { + #ifdef ESP8266 + File file = LittleFS.open(CANGROW_CFG, "w"); + #endif + + #ifdef ESP32 + fs::FS &fs = LittleFS; + File file = fs.open(CANGROW_CFG, FILE_WRITE); + #endif + + if (!file) { + //Log.notice(F("%s loading config from: %s" CR), LogLoc, CANGROW_CFG); + Log.error(F("%s FAILED to open configfile for writing: %s" CR), LogLoc, CANGROW_CFG); + return false; + } else { + Log.notice(F("%s opened for writing %s" CR), LogLoc, CANGROW_CFG); + } + // Serialize JSON to file + if (serializeJson(doc, file) == 0) { + Log.error(F("%s FAILED to write configfile: %s" CR), LogLoc, CANGROW_CFG); + } else { + Log.notice(F("%s successfully written %s" CR), LogLoc, CANGROW_CFG); + } + file.close(); + } else { + Log.notice(F("%s --- %s ---" CR), LogLoc, CANGROW_CFG); + serializeJsonPretty(doc, Serial); + Serial.println(""); + Log.notice(F("%s ----------------------" CR), LogLoc, CANGROW_CFG); + } + + /* every time config get saved, we save the actual time too + * so when ntp is not available, we hopefully do not lack behind too much + * (better then nothing) */ + Time2FS_Save(); + return true; + +} + +///* + //* ESP8266 functions + //*/ + +///*functions from https://github.com/esp8266/Arduino/blob/master/libraries/LittleFS/examples/LittleFS_Timestamp/LittleFS_Timestamp.ino*/ +//#ifdef ESP8266 +//void listDir(const char *dirname) { + //Serial.printf("Listing directory: %s\n", dirname); + + //Dir root = LittleFS.openDir(dirname); + + //while (root.next()) { + //File file = root.openFile("r"); + //Serial.print(" FILE: "); + //Serial.print(root.fileName()); + //Serial.print(" SIZE: "); + //Serial.print(file.size()); + //time_t cr = file.getCreationTime(); + //time_t lw = file.getLastWrite(); + //file.close(); + //struct tm *tmstruct = localtime(&cr); + //Serial.printf(" CREATION: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour, tmstruct->tm_min, tmstruct->tm_sec); + //tmstruct = localtime(&lw); + //Serial.printf(" LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour, tmstruct->tm_min, tmstruct->tm_sec); + //} +//} + + +//void readFile(const char *path) { + //Serial.printf("Reading file: %s\n", path); + + //File file = LittleFS.open(path, "r"); + //if (!file) { + //Serial.println("Failed to open file for reading"); + //return; + //} + + //Serial.print("Read from file: "); + //while (file.available()) { Serial.write(file.read()); } + //file.close(); +//} + +//void writeFile(const char *path, const char *message) { + //Serial.printf("Writing file: %s\n", path); + + //File file = LittleFS.open(path, "w"); + //if (!file) { + //Serial.println("Failed to open file for writing"); + //return; + //} + //if (file.print(message)) { + //Serial.println("File written"); + //} else { + //Serial.println("Write failed"); + //} + //delay(2000); // Make sure the CREATE and LASTWRITE times are different + //file.close(); +//} + +//void appendFile(const char *path, const char *message) { + //Serial.printf("Appending to file: %s\n", path); + + //File file = LittleFS.open(path, "a"); + //if (!file) { + //Serial.println("Failed to open file for appending"); + //return; + //} + //if (file.print(message)) { + //Serial.println("Message appended"); + //} else { + //Serial.println("Append failed"); + //} + //file.close(); +//} + +//void renameFile(const char *path1, const char *path2) { + //Serial.printf("Renaming file %s to %s\n", path1, path2); + //if (LittleFS.rename(path1, path2)) { + //Serial.println("File renamed"); + //} else { + //Serial.println("Rename failed"); + //} +//} + +//void deleteFile(const char *path) { + //Serial.printf("Deleting file: %s\n", path); + //if (LittleFS.remove(path)) { + //Serial.println("File deleted"); + //} else { + //Serial.println("Delete failed"); + //} +//} +//#endif + + +///* + //* ESP32 functions + //*/ + +///*functions from https://github.com/espressif/arduino-esp32/blob/master/libraries/LittleFS/examples/LITTLEFS_time/LITTLEFS_time.ino*/ +//#ifdef ESP32 +//void listDir(fs::FS &fs, const char *dirname, uint8_t levels) { + //Serial.printf("Listing directory: %s\n", dirname); + + //File root = fs.open(dirname); + //if (!root) { + //Serial.println("Failed to open directory"); + //return; + //} + //if (!root.isDirectory()) { + //Serial.println("Not a directory"); + //return; + //} + + //File file = root.openNextFile(); + //while (file) { + //if (file.isDirectory()) { + //Serial.print(" DIR : "); + //Serial.print(file.name()); + //time_t t = file.getLastWrite(); + //struct tm *tmstruct = localtime(&t); + //Serial.printf( + //" LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour, + //tmstruct->tm_min, tmstruct->tm_sec + //); + //if (levels) { + //listDir(fs, file.path(), levels - 1); + //} + //} else { + //Serial.print(" FILE: "); + //Serial.print(file.name()); + //Serial.print(" SIZE: "); + //Serial.print(file.size()); + //time_t t = file.getLastWrite(); + //struct tm *tmstruct = localtime(&t); + //Serial.printf( + //" LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour, + //tmstruct->tm_min, tmstruct->tm_sec + //); + //} + //file = root.openNextFile(); + //} +//} + +//void removeDir(fs::FS &fs, const char *path) { + //Serial.printf("Removing Dir: %s\n", path); + //if (fs.rmdir(path)) { + //Serial.println("Dir removed"); + //} else { + //Serial.println("rmdir failed"); + //} +//} + +//void readFile(fs::FS &fs, const char *path) { + //Serial.printf("Reading file: %s\n", path); + + //File file = fs.open(path); + //if (!file) { + //Serial.println("Failed to open file for reading"); + //return; + //} + + //Serial.print("Read from file: "); + //while (file.available()) { + //Serial.write(file.read()); + //} + //file.close(); +//} + +//void writeFile(fs::FS &fs, const char *path, const char *message) { + //Serial.printf("Writing file: %s\n", path); + + //File file = fs.open(path, FILE_WRITE); + //if (!file) { + //Serial.println("Failed to open file for writing"); + //return; + //} + //if (file.print(message)) { + //Serial.println("File written"); + //} else { + //Serial.println("Write failed"); + //} + //file.close(); +//} + +//void appendFile(fs::FS &fs, const char *path, const char *message) { + //Serial.printf("Appending to file: %s\n", path); + + //File file = fs.open(path, FILE_APPEND); + //if (!file) { + //Serial.println("Failed to open file for appending"); + //return; + //} + //if (file.print(message)) { + //Serial.println("Message appended"); + //} else { + //Serial.println("Append failed"); + //} + //file.close(); +//} + +//void renameFile(fs::FS &fs, const char *path1, const char *path2) { + //Serial.printf("Renaming file %s to %s\n", path1, path2); + //if (fs.rename(path1, path2)) { + //Serial.println("File renamed"); + //} else { + //Serial.println("Rename failed"); + //} +//} + +//void deleteFile(fs::FS &fs, const char *path) { + //Serial.printf("Deleting file: %s\n", path); + //if (fs.remove(path)) { + //Serial.println("File deleted"); + //} else { + //Serial.println("Delete failed"); + //} +//} +//#endif diff --git a/include/CanGrow_Logo.h b/include/CanGrow_Logo.h new file mode 100644 index 0000000..b64522e --- /dev/null +++ b/include/CanGrow_Logo.h @@ -0,0 +1,41 @@ +// 'CanGrow_Logo', 128x32px +const unsigned char bmpCanGrow_Logo [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x1f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0e, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x38, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x70, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x03, 0x00, 0x00, 0x00, 0x04, 0x07, 0xe0, 0x20, 0x60, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x03, 0x00, 0x00, 0x00, 0x06, 0x07, 0xe0, 0xe0, 0x60, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x00, 0x03, 0x87, 0xe1, 0xc0, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x3f, 0xc3, 0xff, 0x03, 0xc7, 0xe3, 0xc0, 0xcf, 0xf9, 0xff, 0xe3, 0xfc, 0xc1, 0x83, + 0x30, 0x00, 0x7f, 0xe3, 0xff, 0x83, 0xe7, 0xe7, 0xc0, 0xcf, 0xfb, 0xff, 0xe7, 0xfe, 0xc3, 0x87, + 0x30, 0x00, 0xe0, 0x73, 0x80, 0xc1, 0xf7, 0xef, 0xc0, 0xc0, 0x1b, 0x80, 0x0e, 0x03, 0xc3, 0x86, + 0x30, 0x00, 0xc0, 0x33, 0x00, 0xc1, 0xff, 0xff, 0x80, 0xc0, 0x1b, 0x00, 0x0c, 0x03, 0xc7, 0x8e, + 0x30, 0x01, 0xc0, 0x37, 0x00, 0xc0, 0xff, 0xff, 0x80, 0xc0, 0x3b, 0x00, 0x1c, 0x03, 0xc7, 0x8c, + 0x60, 0x01, 0xc0, 0x37, 0x00, 0xc0, 0xff, 0xff, 0x01, 0x80, 0x3f, 0x00, 0x18, 0x03, 0xcf, 0x9c, + 0x60, 0x00, 0x00, 0x37, 0x00, 0xc0, 0x7f, 0xfe, 0x01, 0x80, 0x37, 0x00, 0x18, 0x03, 0xcf, 0x9c, + 0x60, 0x00, 0x00, 0x76, 0x01, 0xc0, 0x1f, 0xfc, 0x01, 0x80, 0x36, 0x00, 0x18, 0x06, 0xdf, 0xb8, + 0x60, 0x00, 0x7f, 0xe6, 0x01, 0x9f, 0x9f, 0xfc, 0xf9, 0x80, 0x36, 0x00, 0x18, 0x06, 0xdd, 0xb8, + 0x60, 0x00, 0xff, 0xe6, 0x01, 0x87, 0xff, 0xff, 0xf1, 0x80, 0x76, 0x00, 0x18, 0x06, 0xdd, 0xb0, + 0xc0, 0x01, 0xc0, 0xee, 0x01, 0x83, 0xff, 0xff, 0xc3, 0x00, 0x7e, 0x00, 0x30, 0x06, 0xf9, 0xf0, + 0xc0, 0x0b, 0x80, 0x6e, 0x01, 0x81, 0xff, 0xff, 0x83, 0x00, 0x6e, 0x00, 0x30, 0x06, 0xf9, 0xe0, + 0xc0, 0x1b, 0x00, 0xec, 0x01, 0x80, 0x1f, 0xf8, 0x03, 0x00, 0x6c, 0x00, 0x30, 0x0e, 0xf1, 0xe0, + 0xc0, 0x3b, 0x00, 0xcc, 0x03, 0x80, 0x3f, 0xfc, 0x03, 0x00, 0xec, 0x00, 0x30, 0x0c, 0xf1, 0xc0, + 0xc0, 0x7b, 0x01, 0xcc, 0x03, 0x00, 0x7f, 0xfe, 0x03, 0x01, 0xec, 0x00, 0x30, 0x1c, 0xe1, 0xc0, + 0x7f, 0xf1, 0xff, 0xdc, 0x03, 0x00, 0xf0, 0x8f, 0x01, 0xff, 0xfc, 0x00, 0x1f, 0xf8, 0xe1, 0xc0, + 0x3f, 0xe0, 0xff, 0xcc, 0x03, 0x00, 0x00, 0x80, 0x00, 0xff, 0xcc, 0x00, 0x0f, 0xf0, 0xc1, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +// Array of all bitmaps for convenience. (Total bytes used to store images in PROGMEM = 528) +const int bmpallArray_LEN = 1; +const unsigned char* bmpallArray[1] = { + bmpCanGrow_Logo +}; diff --git a/include/CanGrow_Output.h b/include/CanGrow_Output.h new file mode 100644 index 0000000..53b1112 --- /dev/null +++ b/include/CanGrow_Output.h @@ -0,0 +1,540 @@ +/* + * + * include/CanGrow_Output.h - Output header file + * + * + * + */ + +#include "Output/Output_Common.h" +#include "Output/Output_I2C_01_MCP4725.h" +#include "Output/Output_I2C_02_GP8403.h" + +/* OutputI2C index struct */ +struct OutputI2C_Index { + const char name[16]; + const byte port[OUTPUT_TYPE_I2C_MAX_PORTS]; + const byte max; +}; + +OutputI2C_Index OutputI2Cindex[] { + /* OutputI2C 00 is unset */ + { "unset", { + {}, + }}, + /* OutputI2C 01 */ + { + OUTPUT_I2C_01_NAME, + { + OUTPUT_TYPE_I2C_PORT_BYTE + }, + sizeof(Output_I2C_01_MCP4725_Addr), + }, + + /* 02 - DFRobot Gravity (GP8403) */ + { + OUTPUT_I2C_02_NAME, + { + OUTPUT_TYPE_I2C_PORT_BYTE, + OUTPUT_TYPE_I2C_PORT_BYTE + }, + sizeof(Output_I2C_02_GP8403_Addr), + } + +}; + +/* Dont forget to increase the index counter after you added a new output module */ +const byte OutputI2Cindex_length = 2; + + +byte Output_I2C_Addr_Init_Update(const byte OutputI2CindexId, const byte AddrId, const byte PortId, const byte Mode, const bool Invert = false, const byte Value = 0) { + const static char LogLoc[] PROGMEM = "[Output:I2C:Addr_Init_Update]"; + /* Multi purpose function. + * + * Modes: + * 0 - return the i2c address as byte + * 1 - init the output i2c module, returns true (1) if succeeded + * 2 - update i2 module data + */ + //byte Dummy_Addr[] = { 0x42, 0x69 }; + + /* invert Value if set, save it to Value_tmp */ + byte Value_tmp = Value; + if(Invert == true) + /* Value comes from outputState[] which is only type byte (max 255) */ + Value_tmp = 255 - Value; + + switch(OutputI2CindexId) { + + /* I2C Output module 01 */ + case 1: + switch(Mode) { + case OUPUT_I2C_AIU_MODE_ADDR: + return Output_I2C_01_MCP4725_Addr[AddrId]; + break; + case OUPUT_I2C_AIU_MODE_INIT: + return Output_I2C_01_MCP4725_Init(AddrId, PortId); + break; + case OUPUT_I2C_AIU_MODE_UPDATE: + Output_I2C_01_MCP4725_Update(AddrId, PortId, Value_tmp); + break; + } + break; + + /* I2C Output module 02 */ + case 2: + switch(Mode) { + case OUPUT_I2C_AIU_MODE_ADDR: + return Output_I2C_02_GP8403_Addr[AddrId]; + break; + case OUPUT_I2C_AIU_MODE_INIT: + return Output_I2C_02_GP8403_Init(AddrId, PortId); + break; + case OUPUT_I2C_AIU_MODE_UPDATE: + Output_I2C_02_GP8403_Update(AddrId, PortId, Value_tmp); + break; + } + break; + + + + /* 02 - dummy*/ + //case 2: + + //switch(Mode) { + //case 0: + //return Dummy_Addr[AddrId]; + //break; + //case 1: + //return true; + //break; + //case 2: + //true; + //break; + //} + //break; + + /* unknown i2c output module id */ + default: + Log.error(F("%s OutputI2Cindex ID %d not found" CR), LogLoc, OutputI2CindexId); + break; + } + + return 0; +} + + +byte Output_GPIO_Init_Update(const byte GPIOindexId, const bool PWM, const bool Invert, const byte Mode, const byte Value = 0) { + const static char LogLoc[] PROGMEM = "[Output:GPIO:Init_Update]"; + bool Value_bool = Value; + switch(Mode) { + + case OUTPUT_GPIO_IU_MODE_INIT: + pinMode(GPIOindex[GPIOindexId].gpio, OUTPUT); + + if(Invert == true) { + digitalWrite(GPIOindex[GPIOindexId].gpio, 1 - Value_bool); + } else { + digitalWrite(GPIOindex[GPIOindexId].gpio, Value_bool); + } + return true; + break; + + case OUTPUT_GPIO_IU_MODE_UPDATE: + if((PWM == false) || (GPIOindex[GPIOindexId].note == NO_PWM)) { + if(Invert == true) { + digitalWrite(GPIOindex[GPIOindexId].gpio, 1 - Value_bool); + } else { + digitalWrite(GPIOindex[GPIOindexId].gpio, Value); + } + } else { + /* output inverted? */ + if(Invert == true) { + /* when output is set to 0 (off), use digitalWrite LOW to prevent spikes */ + if(GPIOindex[GPIOindexId].gpio < 255) { + analogWrite(GPIOindex[GPIOindexId].gpio, 255 - Value); + } else { + digitalWrite(GPIOindex[GPIOindexId].gpio, HIGH); + #ifdef DEBUG + Log.verbose(F("%s digitalWrite HIGH" CR), LogLoc); + #endif + } + /* not inverted */ + } else { + /* when output is set to 0 (off), use digitalWrite LOW to prevent spikes */ + if(GPIOindex[GPIOindexId].gpio < 255) { + analogWrite(GPIOindex[GPIOindexId].gpio, Value); + } else { + digitalWrite(GPIOindex[GPIOindexId].gpio, LOW); + #ifdef DEBUG + Log.verbose(F("%s digitalWrite LOW" CR), LogLoc); + #endif + } + } + } + break; + } + + return 0; +} + +bool Output_Webcall_Init_Update(const byte OutputId, const bool Value = false) { + const static char LogLoc[] PROGMEM = "[Output:Webcall:Init_Update]"; + + /* here we invert the value if set. First we hand it to a tmp var, Value_tmp */ + bool Value_tmp = Value; + /* check if output has inverted flagged */ + if(config.system.output.invert[OutputId] == true) + Value_tmp = 1 - Value; + + + String url; + url += F("http://"); + url += config.system.output.webcall_host[OutputId]; + url += F("/"); + + WiFiClient client; + HTTPClient http; + + ///* set timeout to three seconds */ + //#ifdef ESP8266 + ///* on ESP8266 http.setTimeout accepts miliseconds */ + //http.setTimeout(3000); + //#endif + + //#ifdef ESP32 + ///* on ESP32 http.setTimeout() accepts only seconds - https://github.com/espressif/arduino-esp32/issues/3732 */ + //http.setTimeout(3); + //#endif + + switch(Value_tmp) { + /* turn on */ + case true: + url += config.system.output.webcall_path_on[OutputId]; + break; + + /* turn off */ + case false: + url += config.system.output.webcall_path_off[OutputId]; + break; + } + + /* build request */ + http.begin(client, url); + + /* fire request and check result, */ + int httpResponseCode = http.GET(); + /* if 200 , OK */ + if(httpResponseCode > 0) { + #ifdef DEBUG2 + Log.verbose(F("%s GET %s (%d)" CR), LogLoc, url.c_str(), httpResponseCode); + #endif + return true; + } else { + #ifdef DEBUG2 + Log.verbose(F("%s FAILED GET %s (%d)" CR), LogLoc, url.c_str(), httpResponseCode); + #endif + return false; + } + + return 0; +} + +bool Output_Check_PWM(const byte OutputId) { + /* when we verify output is GPIO PWM OR */ + if(((config.system.output.type[OutputId] == OUTPUT_TYPE_GPIO) && (config.system.output.gpio_pwm[OutputId] == true) && (GPIOindex[config.system.output.type[OutputId]].note != NO_PWM)) || + /* Output is type I2C */ + (config.system.output.type[OutputId] == OUTPUT_TYPE_I2C) + ) { + /* return true */ + return true; + } else { + return false; + } +} + +/* + * Output_Device + * + * add, remove, (modify) config.grow.light[] objects + */ +void Output_Device_Grow_AddRemove(const byte OutputId, const byte mode) { + /* switch on device type of the output + * + * Modes: + * 0 - add - receive OutputId + * 1 - remove - receive LightId + * 2 - modify - receive LightId + * */ + switch(config.system.output.device[OutputId]) { + case OUTPUT_DEVICE_LIGHT: + //byte LightId; + switch(mode) { + /* add */ + case 0: + config.grow.light.configured[OutputId] = true; + config.grow.light.output[OutputId] = OutputId; + config.grow.light.sunriseHourVeg[OutputId] = 6; + config.grow.light.sunriseMinuteVeg[OutputId] = 0; + config.grow.light.sunsetHourVeg[OutputId] = 23; + config.grow.light.sunsetMinuteVeg[OutputId] = 59; + + config.grow.light.sunriseHourBloom[OutputId] = 6; + config.grow.light.sunriseMinuteBloom[OutputId] = 0; + config.grow.light.sunsetHourBloom[OutputId] = 18; + config.grow.light.sunsetMinuteBloom[OutputId] = 0; + + config.grow.light.power[OutputId] = 0; + if(Output_Check_PWM(OutputId)) { + config.grow.light.fade[OutputId] = true; + } else { + config.grow.light.fade[OutputId] = false; + } + + config.grow.light.fadeDuration[OutputId] = 30; + //SaveConfig() + break; + + /* remove */ + case 1: + /* get LightId for this Output */ + //for(byte i = 0; i < Max_Outputs; i++) { + //if((config.grow.light.configured[i] == true) && (config.grow.light.output[i] == OutputId)) { + //LightId = i; + //break; + //} + //} + config.grow.light.configured[OutputId] = 0; + config.grow.light.output[OutputId] = 0; + config.grow.light.sunriseHourVeg[OutputId] = 0; + config.grow.light.sunriseMinuteVeg[OutputId] = 0; + config.grow.light.sunsetHourVeg[OutputId] = 0; + config.grow.light.sunsetMinuteVeg[OutputId] = 0; + + config.grow.light.sunriseHourBloom[OutputId] = 0; + config.grow.light.sunriseMinuteBloom[OutputId] = 0; + config.grow.light.sunsetHourBloom[OutputId] = 0; + config.grow.light.sunsetMinuteBloom[OutputId] = 0; + + config.grow.light.power[OutputId] = 0; + config.grow.light.fade[OutputId] = 0; + + config.grow.light.fadeDuration[OutputId] = 0; + //SaveConfig(); + break; + + //case 2: + //true; + //break; + } + break; + + case OUTPUT_DEVICE_FAN: + case OUTPUT_DEVICE_HUMIDIFIER: + case OUTPUT_DEVICE_DEHUMIDIFIER: + case OUTPUT_DEVICE_HEATING: + switch(mode) { + case 0: + config.grow.air.configured[OutputId] = true; + config.grow.air.output[OutputId] = OutputId; + config.grow.air.power[OutputId] = 0; + config.grow.air.controlSensor[OutputId] = 255; // 255 means unconfigured, because SensorId begins at 0 + config.grow.air.controlRead[OutputId] = 255; // same here + config.grow.air.controlMode[OutputId] = 0; + config.grow.air.min[OutputId] = 0; + config.grow.air.max[OutputId] = 0; + break; + + case 1: + config.grow.air.configured[OutputId] = false; + config.grow.air.output[OutputId] = 0; + config.grow.air.power[OutputId] = 0; + config.grow.air.controlSensor[OutputId] = 0; + config.grow.air.controlRead[OutputId] = 0; + config.grow.air.controlMode[OutputId] = 0; + config.grow.air.min[OutputId] = 0; + config.grow.air.max[OutputId] = 0; + break; + } + break; + + case OUTPUT_DEVICE_PUMP: + switch(mode) { + case 0: + config.grow.water.configured[OutputId] = true; + config.grow.water.output[OutputId] = OutputId; + config.grow.water.onTime[OutputId] = 0; + config.grow.water.controlSensor[OutputId] = 255; // 255 means unconfigured, because SensorId begins at 0 + config.grow.water.controlRead[OutputId] = 255; // same here + config.grow.water.controlMode[OutputId] = 0; + config.grow.water.min[OutputId] = 0; + config.grow.water.max[OutputId] = 0; + config.grow.water.interval[OutputId] = 0; + config.grow.water.intervalUnit[OutputId] = 0; + break; + + case 1: + config.grow.water.configured[OutputId] = false; + config.grow.water.output[OutputId] = 0; + config.grow.water.onTime[OutputId] = 0; + config.grow.water.controlSensor[OutputId] = 0; + config.grow.water.controlRead[OutputId] = 0; + config.grow.water.controlMode[OutputId] = 0; + config.grow.water.min[OutputId] = 0; + config.grow.water.max[OutputId] = 0; + config.grow.water.interval[OutputId] = 0; + config.grow.water.intervalUnit[OutputId] = 0; + break; + } + break; + + //case OUTPUT_DEVICE_HUMIDIFIER: + //break; + + //case OUTPUT_DEVICE_DEHUMIDIFIER: + //break; + + //case OUTPUT_DEVICE_HEATING: + //break; + } +} + + +/* + * Output main functions + * + */ +void Output_Init() { + /* initialize all configured outputs */ + const static char LogLoc[] PROGMEM = "[Output:Init]"; + + #ifdef DEBUG + Log.verbose(F("%s == configured outputs ==" CR), LogLoc); + #endif + + /* interate through all available Output IDs */ + for(byte i = 0; i < Max_Outputs; i++) { + /* if configured */ + if(config.system.output.type[i] > 0) { + /* get the configured output type */ + switch(config.system.output.type[i]) { + case OUTPUT_TYPE_GPIO: + #ifdef DEBUG + Log.verbose(F("%s Output ID %d: %s - Device %s (%d), GPIO %d (%d), Enabled %T, PWM %T, Invert %T" CR), + LogLoc, + i, config.system.output.name[i], + Output_Device_descr[config.system.output.device[i]], config.system.output.device[i], + GPIOindex[config.system.output.gpio[i]].gpio, config.system.output.gpio[i], + config.system.output.enabled[i], config.system.output.gpio_pwm[i], config.system.output.invert[i]); + #endif + /* TODO implement gpio init */ + outputStatus[i] = Output_GPIO_Init_Update(config.system.output.gpio[i], config.system.output.gpio_pwm[i], config.system.output.invert[i], OUTPUT_GPIO_IU_MODE_INIT); + + break; + + case OUTPUT_TYPE_I2C: + #ifdef DEBUG + Log.verbose(F("%s Output ID %d: %s - Device %s (%d), I2C type %s (%d), I2C addr 0x%x (%d), Module port %d, Enabled %T, PWM %T, Invert %T" CR), + LogLoc, + i, config.system.output.name[i], + Output_Device_descr[config.system.output.device[i]], config.system.output.device[i], + OutputI2Cindex[config.system.output.i2c_type[i]].name, config.system.output.i2c_type[i], + config.system.output.enabled[i], config.system.output.gpio_pwm[i], config.system.output.invert[i]); + #endif + outputStatus[i] = Output_I2C_Addr_Init_Update(config.system.output.i2c_type[i], config.system.output.i2c_addr[i], config.system.output.i2c_port[i], OUPUT_I2C_AIU_MODE_INIT, config.system.output.invert[i]); + break; + + case OUTPUT_TYPE_WEB: + #ifdef DEBUG + Log.verbose(F("%s Output ID %d: %s - Device %s (%d), Webcall host %s, Webcall Path ON '%s', Webcall Path OFF '%s', Enabled %T, PWM %T, Invert %T" CR), + LogLoc, + i, config.system.output.name[i], + Output_Device_descr[config.system.output.device[i]], config.system.output.device[i], + config.system.output.webcall_host[i], config.system.output.webcall_path_on[i], config.system.output.webcall_path_off[i], + config.system.output.enabled[i], config.system.output.gpio_pwm[i], config.system.output.invert[i]); + #endif + /* TODO implement webcall init */ + outputStatus[i] = Output_Webcall_Init_Update(i); + + break; + + default: + Log.error(F("%s (%d) Output type %d not found" CR), LogLoc, i, config.system.output.type[i]); + break; + } + } + } + +} + +void Output_Update() { + const static char LogLoc[] PROGMEM = "[Output:Update]"; + #ifdef DEBUG2 + unsigned long mStart = millis(); + unsigned long mStop; + Log.verbose(F("%s Start %u" CR), LogLoc, mStart); + #endif + /* interate through all available Output IDs */ + for(byte i = 0; i < Max_Outputs; i++) { + /* if configured and enabled */ + if((config.system.output.type[i] > 0) && (config.system.output.enabled[i] == true)) { + /* get the configured output type */ + switch(config.system.output.type[i]) { + + /******* + * GPIO + * *****/ + case OUTPUT_TYPE_GPIO: + /* update GPIO output */ + Output_GPIO_Init_Update(config.system.output.gpio[i], config.system.output.gpio_pwm[i], config.system.output.invert[i], OUTPUT_GPIO_IU_MODE_UPDATE, outputState[i]); + + break; + + /******* + * I2C + * *****/ + case OUTPUT_TYPE_I2C: + /* perform I2C output update only, when outputStatus is OK (true) */ + if(outputStatus[i] == true) + Output_I2C_Addr_Init_Update(config.system.output.i2c_type[i], config.system.output.i2c_addr[i], config.system.output.i2c_port[i], OUPUT_I2C_AIU_MODE_UPDATE, config.system.output.invert[i], outputState[i]); + break; + + /******* + * WEB + * ****/ + case OUTPUT_TYPE_WEB: + /* check how often webcall output failed. if limit exceeded, do not update anymore */ + if((outputStatus[i] == false) && (outputWebcallFailed[i] > 5)) { + /* if webcall fail counter has reached limit of 255, reset to 0 + * so we retry a call. */ + if(outputWebcallFailed[i] >= 255) { + outputWebcallFailed[i] = 0; + } else { + /* increment webcall failed counter. */ + outputWebcallFailed[i]++; + } + } else { + /* update webcall output */ + outputStatus[i] = Output_Webcall_Init_Update(i, outputState[i]); + if(outputStatus[i] == false) { + /* increment webcall failed counter */ + outputWebcallFailed[i]++; + } else { + /* otherwise set to 0 */ + outputWebcallFailed[i] = 0; + } + } + true; + break; + + default: + Log.error(F("%s (%d) Output type %d not found" CR), LogLoc, i, config.system.output.type[i]); + break; + } + } + } + #ifdef DEBUG2 + mStop = millis(); + Log.verbose(F("%s Stop %u (%u)" CR), LogLoc, mStop, mStop - mStart); + #endif +} diff --git a/include/CanGrow_Sensor.h b/include/CanGrow_Sensor.h new file mode 100644 index 0000000..55dc7eb --- /dev/null +++ b/include/CanGrow_Sensor.h @@ -0,0 +1,758 @@ +/* + * + * include/CanGrow_Sensor.h - sensor header file + * + * + * + * ADD A NEW SENSOR + * **************** + * If you want to add a new sensor, you have to to following things: + * + * Check what it's the last used SensorIndex ID. If it's Sensor 08, you have to + * take 09 as next. + * + * Copy Sensor/00_Example.h to Sensor/09_YourSensor.h and rename everything in it from + * "00_Example" to "09_YourSensor" and edit all the needed functions and variables to your needs. + * + * Add a new include line to CanGrow_Sensor.h (this file) + * #include "Sensor/09_YourSensor.h" + * + * Add a new Entry to the SensorIndex Array, like: + * ***** SensorIndex[] ***** + * ,{ + * // 9 - YourSensor + * // Sensor Name + * SENSOR_09_NAME, + * { + * // Sensor Readings + * SENSOR_READ_TYPE_TEMP, + * SENSOR_READ_TYPE_HUMIDITY, + * SENSOR_READ_TYPE_RAW + * }, + * // Maximal Sensor Units (most time the Sum of available Addresses) + * sizeof(Sensor_09_YourSensor_Addr), + * } + * ************************ + * + * If you are done with that, you have to add a new Switch case for the new Sensor ID + * to Sensor_Addr_Init_Update() and Sensor_getValue() like: + * + * ***** Sensor_Addr_Init_Update() ***** + * // Sensor 09 + * case 9: + * switch(mode) { + * case 0: + * return Sensor_09_YourSensor_Addr[AddrId]; + * break; + * + * case 1: + * return Sensor_09_YourSensor_Init(AddrId); + * break; + * + * case 2: + * Sensor_09_YourSensor_Update(AddrId); + * break; + * } + * break; + * ************************************* + * + * ***** Sensor_getValue() ***** + * // Sensor 09 + * case 9: + * return Sensor_09_YourSensor[AddrId][ReadValId]; + * break; + * ***************************** + */ + +/* should come as dependency with all adafruit sensor libs. If not, see here: + * https://github.com/adafruit/Adafruit_Sensor */ +#include <Adafruit_Sensor.h> + +#include "Sensor/Sensor_Common.h" +#include "Sensor/01_ADC_builtin.h" +#include "Sensor/02_BME280.h" +#include "Sensor/03_BME680.h" +#include "Sensor/04_SHT3x.h" +#include "Sensor/05_MLX90614.h" +#include "Sensor/06_TCS34725.h" +#include "Sensor/07_ADS1115.h" +#include "Sensor/08_ADS1015.h" +#include "Sensor/09_Chirp.h" +#include "Sensor/10_CCS811.h" + +/* + * Sensor Todo list: + * + * - CCS811 CO2 sensor, will have type SENSOR_TYPE_I2C_WITH_GPIO, it needs signal on pin WAK + * cheap - ~ 8€ on Aliexpress + * - HX711 for weight sensor, this sensor needs two GPIOs for communication + * - SCD30/40 CO2 sensor, expensive, >70€ + */ + + +struct Sensor_Index { + /* + * Sensor Index + * - name + * - readings (array, up to 8 entries) + * - 0 unset + * - 1 Raw + * - 2 Temp + * - 3 Humidity + * - 4 Moisture + * - 5 Pressure + * - 6 Gas restistance + * - max units + * + */ + const char name[16]; + const byte type; + const byte read[Max_Sensors_Read]; + const byte max; + const byte gpioMax; +}; + +Sensor_Index SensorIndex[] { + /* + * Example: + * + * // 0 - Example + * { SENSOR_00_NAME, + * { + * SENSOR_READ_TYPE_TEMP, + * SENSOR_READ_TYPE_HUMIDITY, + * SENSOR_READ_TYPE_RAW, + * SENSOR_READ_TYPE_RAW + * }, + * // max nr of sensor units by nr of available addresses + * sizeof(Sensor_00_Example_Addr), + * }, + * + */ + + /* 0 is for unset in config */ + { "unset", 255, { + {}, + }}, + +// 1 - internal ADC + { SENSOR_01_NAME, + SENSOR_TYPE_INTADC, + { + SENSOR_READ_TYPE_RAW + }, + SENSOR_01_MAX, + }, + + // 2 - BME280 + { SENSOR_02_NAME, + SENSOR_TYPE_I2C, + { + SENSOR_READ_TYPE_TEMP, + SENSOR_READ_TYPE_HUMIDITY, + SENSOR_READ_TYPE_PRESSURE, + SENSOR_READ_TYPE_ALTITUDE + }, + // max nr of sensor units by nr of available addresses + sizeof(Sensor_02_BME280_Addr), + }, + + // 3 - BME680 + { SENSOR_03_NAME, + SENSOR_TYPE_I2C, + { + SENSOR_READ_TYPE_TEMP, + SENSOR_READ_TYPE_HUMIDITY, + SENSOR_READ_TYPE_PRESSURE, + SENSOR_READ_TYPE_ALTITUDE, + SENSOR_READ_TYPE_GAS_RESISTANCE + }, + sizeof(Sensor_03_BME680_Addr), + }, + + // 4 - SHT3x + { SENSOR_04_NAME, + SENSOR_TYPE_I2C, + { + SENSOR_READ_TYPE_TEMP, + SENSOR_READ_TYPE_HUMIDITY + }, + sizeof(Sensor_04_SHT3X_Addr), + }, + + // 5 - MLX90614 + { SENSOR_05_NAME, + SENSOR_TYPE_I2C, + { + /* Ambient temp */ + SENSOR_READ_TYPE_TEMP, + /* Object temp */ + SENSOR_READ_TYPE_TEMP + }, + sizeof(Sensor_05_MLX90614_Addr), + }, + + // 6 - TCS34725 + { SENSOR_06_NAME, + SENSOR_TYPE_I2C, + { + SENSOR_READ_TYPE_COLOR_TEMP, + SENSOR_READ_TYPE_LUX, + SENSOR_READ_TYPE_COLOR_RED, + SENSOR_READ_TYPE_COLOR_GREEN, + SENSOR_READ_TYPE_COLOR_BLUE + }, + sizeof(Sensor_06_TCS34725_Addr), + }, + + { + // 7 - ADS1115 + SENSOR_07_NAME, + SENSOR_TYPE_I2C, + { + /* A0 */ + SENSOR_READ_TYPE_RAW, + /* A1 */ + SENSOR_READ_TYPE_RAW, + /* A2 */ + SENSOR_READ_TYPE_RAW, + /* A3 */ + SENSOR_READ_TYPE_RAW + + }, + sizeof(Sensor_07_ADS1115_Addr), + }, + + { + // 8 - ADS1015 + SENSOR_08_NAME, + SENSOR_TYPE_I2C, + { + SENSOR_READ_TYPE_RAW, + SENSOR_READ_TYPE_RAW, + SENSOR_READ_TYPE_RAW, + SENSOR_READ_TYPE_RAW + + }, + sizeof(Sensor_08_ADS1015_Addr), + }, + + { + // 9 - I2C Chirp soilmoisture/temperature sensor + SENSOR_09_NAME, + SENSOR_TYPE_I2C, + { + // raw soilmoisture value + SENSOR_READ_TYPE_RAW, + // temperature + SENSOR_READ_TYPE_TEMP, + /* raw light value takes 3s to use, so we dont use it. if you need it, + * uncomment it here and in Sensor/09_Chirp Sensor_09_Chirp_Update() */ + // SENSOR_READ_TYPE_RAW + }, + sizeof(Sensor_09_Chirp_Addr), + }, + + { + /* 10 - CCS811 CO2 I2C sensor */ + SENSOR_10_NAME, + SENSOR_TYPE_I2C, + { + /* CO2 as parts per million */ + SENSOR_READ_TYPE_PARTS_PER_MILLION, + /* TVOC value (Total Volatile Organic Compouds)*/ + SENSOR_READ_TYPE_TVOC, + }, + sizeof(Sensor_10_CCS811_Addr), + } +}; + +/* sum up of number of sensors. Dont forget to increment if you add one :) */ +const byte SensorIndex_length = 10; + +byte Sensor_Addr_Init_Update(const byte SensorIndexId, const byte AddrId, const byte mode, const byte Gpio2 = 0) { + const static char LogLoc[] PROGMEM = "[Sensor:Addr_Init_Update]"; + /* Multi purpose function. + * + * Modes: + * 0 - get the address as byte (i2c_addr index, gpio index) + * 1 - init the sensor, returns true (1) if succeeded + * 2 - update sensors data + * + * When using a sensor which is using bare GPIOs like int ADC, or some 1- or 2-Wire sensors + * AddrId is used for the first GPIO and gpio2 for the second. + * Maybe i come up later with a better idea, but for now... + */ + #ifdef DEBUG3 + if(mode > 0) + Log.verbose(F("%s Mode: %d, SensorIndexId: %d, AddrId: %d" CR), LogLoc, mode, SensorIndexId, AddrId); + #endif + switch(SensorIndexId) { + /* + * Example: + * + * case 0: + * if(!onlyReturn) + * Sensor_00_Example_Init(AddrId); + * return Sensor_00_Example_Addr[AddrId]; + * break; + */ + + /* Sensor 01 */ + /* Internal ADC is an exception. Its clearly not an I2C device, but as I let the ADC + * have for both, 8266 and 32, configurable GPIOs, it kinda has. + * AddrId with int ADC is GPIOindex[].type */ + case 1: + switch(mode) { + case 0: + /* internal ADC does not has a address here */ + return 0; + break; + + case 1: + return Sensor_01_ADC_Init(AddrId); + break; + + case 2: + Sensor_01_ADC_Update(AddrId); + break; + } + break; + + /* Sensor 02 */ + case 2: + switch(mode) { + case 0: + return Sensor_02_BME280_Addr[AddrId]; + break; + + case 1: + return Sensor_02_BME280_Init(AddrId); + break; + + case 2: + Sensor_02_BME280_Update(AddrId); + break; + } + break; + + /* Sensor 03 */ + case 3: + switch(mode) { + case 0: + return Sensor_03_BME680_Addr[AddrId]; + break; + + case 1: + return Sensor_03_BME680_Init(AddrId); + break; + + case 2: + Sensor_03_BME680_Update(AddrId); + break; + } + break; + + /* Sensor 04 */ + case 4: + switch(mode) { + case 0: + return Sensor_04_SHT3X_Addr[AddrId]; + break; + + case 1: + return Sensor_04_SHT3X_Init(AddrId); + break; + + case 2: + Sensor_04_SHT3X_Update(AddrId); + break; + } + break; + + /* Sensor 05 */ + case 5: + switch(mode) { + case 0: + return Sensor_05_MLX90614_Addr[AddrId]; + break; + + case 1: + return Sensor_05_MLX90614_Init(AddrId); + break; + + case 2: + Sensor_05_MLX90614_Update(AddrId); + break; + } + break; + + /* Sensor 06 */ + case 6: + switch(mode) { + case 0: + return Sensor_06_TCS34725_Addr[AddrId]; + break; + + case 1: + return Sensor_06_TCS34725_Init(AddrId); + break; + + case 2: + Sensor_06_TCS34725_Update(AddrId); + break; + } + break; + + /* Sensor 07 */ + case 7: + switch(mode) { + case 0: + return Sensor_07_ADS1115_Addr[AddrId]; + break; + + case 1: + return Sensor_07_ADS1115_Init(AddrId); + break; + + case 2: + Sensor_07_ADS1115_Update(AddrId); + break; + } + break; + + /* Sensor 08 */ + case 8: + switch(mode) { + case 0: + return Sensor_08_ADS1015_Addr[AddrId]; + break; + + case 1: + return Sensor_08_ADS1015_Init(AddrId); + break; + + case 2: + Sensor_08_ADS1015_Update(AddrId); + break; + } + break; + + /* Sensor 09 */ + case 9: + switch(mode) { + case 0: + return Sensor_09_Chirp_Addr[AddrId]; + break; + + case 1: + return Sensor_09_Chirp_Init(AddrId); + break; + + case 2: + Sensor_09_Chirp_Update(AddrId); + break; + } + break; + + /* Sensor 10 */ + case 10: + switch(mode) { + case 0: + return Sensor_10_CCS811_Addr[AddrId]; + break; + + case 1: + return Sensor_10_CCS811_Init(AddrId); + break; + + case 2: + Sensor_10_CCS811_Update(AddrId); + break; + } + break; + + /* unknown sensor id */ + default: + Log.error(F("%s SensorIndex ID %d not found" CR), LogLoc, config.system.sensor.type[AddrId]); + break; + } + + return 0; + +} + +float Sensor_getValue(const byte SensorIndexId, const byte AddrId, const byte ReadValId = 0) { + const static char LogLoc[] PROGMEM = "[Sensor:getValue]"; + /* not the best solution, but solution for the moment + * i hope i can come up in future with a way, where i do not have to + * maintain three different places with huge switch cases + * and return everything as float, even when it could be easy an int + * + * here we read the value ReadVal from the given SensorIndexId and its AddrId + * In case of RAW readings, like from ADCs , There is an Index ReadValId as well + */ + switch(SensorIndexId) { + + /* Sensor 01 */ + case 1: + #ifdef ESP8266 + return Sensor_01_ADC[AddrId]; + #endif + + #ifdef ESP32 + /* */ + return Sensor_01_ADC[Sensor_01_ADC_ArrId(AddrId)]; + #endif + break; + + /* Sensor 02 */ + case 2: + return Sensor_02_BME280[AddrId][ReadValId]; + break; + + /* Sensor 03 */ + case 3: + return Sensor_03_BME680[AddrId][ReadValId]; + break; + + /* Sensor 04 */ + case 4: + return Sensor_04_SHT3X[AddrId][ReadValId]; + break; + + /* Sensor 05 */ + case 5: + return Sensor_05_MLX90614[AddrId][ReadValId]; + break; + + /* Sensor 06 */ + case 6: + return Sensor_06_TCS34725[AddrId][ReadValId]; + break; + + /* Sensor 07 */ + case 7: + return Sensor_07_ADS1115[AddrId][ReadValId]; + break; + + /* Sensor 08 */ + case 8: + return Sensor_08_ADS1015[AddrId][ReadValId]; + break; + + /* Sensor 09 */ + case 9: + return Sensor_09_Chirp[AddrId][ReadValId]; + break; + + /* Sensor 10 */ + case 10: + return Sensor_10_CCS811[AddrId][ReadValId]; + break; + + /* unknown sensor id */ + default: + Log.error(F("%s SensorIndex ID %d not found" CR), LogLoc, SensorIndexId); + break; + } + + return 0; +} + + + + +/* + * ********************************************************************************************* + * From here on you do not need to touch any code (hopefully) if you want to add a new sensor! + * ********************************************************************************************* + */ + +float Sensor_getCalibratedValue(const byte SensorId, const byte ReadId) { + float valueRaw; + float value; + + + + /* if SensorId is configured and there is a reading on ReadingId */ + if((config.system.sensor.type[SensorId] > 0) && (SensorIndex[config.system.sensor.type[SensorId]].read[ReadId] > 0)) { + /* first, get the raw / original value */ + if(SensorIndex[config.system.sensor.type[SensorId]].type == SENSOR_TYPE_INTADC) { + valueRaw = Sensor_getValue(config.system.sensor.type[SensorId], config.system.sensor.gpio[SensorId][0], ReadId); + } else if(SensorIndex[config.system.sensor.type[SensorId]].type == SENSOR_TYPE_I2C) { + valueRaw = Sensor_getValue(config.system.sensor.type[SensorId], config.system.sensor.i2c_addr[SensorId], ReadId); + } + + /* if reading is RAW, check what to do with it */ + if(SensorIndex[config.system.sensor.type[SensorId]].read[ReadId] == SENSOR_READ_TYPE_RAW) { + /* config.system.sensor.rawConvert + * 0 - unconfigured, return raw value + * 1 - soilmoisture, return percentage + * 2 - other TBD */ + switch(config.system.sensor.rawConvert[SensorId][ReadId]) { + /* soilmoisture as percentage */ + case SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE: + /* dont use map when both , low and high, are 0 - this causes a crash */ + if((config.system.sensor.low[SensorId][ReadId] > 0) || (config.system.sensor.high[SensorId][ReadId] > 0)) { + /* use map to calculate percentage value */ + value = map(valueRaw, config.system.sensor.low[SensorId][ReadId], config.system.sensor.high[SensorId][ReadId], 0, 100); + } else { + value = 0; + } + return value; + break; + + default: + return valueRaw; + break; + } + } else { + /* if not a RAW value, return the value with the offset */ + return valueRaw + config.system.sensor.offset[SensorId][ReadId]; + } + } + return 0; +} + + +void Sensor_Log_Readings(const byte SensorIndexId , const byte AddrId) { + const static char LogLoc[] PROGMEM = "[Sensor:Log_Readings]"; + Log.verbose(F("%s Sensor %s (%d)" CR), LogLoc, SensorIndex[SensorIndexId].name, AddrId); + /* iterate through the SensorIndex readings */ + for(byte j = 0; j < Max_Sensors_Read; j++) { + /* if SensorIndex[].read[] > 0 (means there is a value to read) */ + if(SensorIndex[SensorIndexId].read[j] > 0 ) { + if(SensorIndex[SensorIndexId].type == SENSOR_TYPE_INTADC) { + Log.verbose(F("%s - %s: %F %s" CR), LogLoc, Sensor_Read_descr[SensorIndex[SensorIndexId].read[j]], + Sensor_getValue(SensorIndexId, AddrId), Sensor_Read_unit[SensorIndex[SensorIndexId].read[j]]); + } else { + Log.verbose(F("%s - %s: %F %s" CR), LogLoc, Sensor_Read_descr[SensorIndex[SensorIndexId].read[j]], + Sensor_getValue(SensorIndexId, AddrId, j), Sensor_Read_unit[SensorIndex[SensorIndexId].read[j]]); + } + } + } +} + +void Sensor_Init() { + /* main function that does initialize all configured sensors at once. called from setup()*/ + const static char LogLoc[] PROGMEM = "[Sensor:Init]"; + /* Go through all configured sensors and initialize them */ + #ifdef DEBUG + Log.verbose(F("%s == Sensor drivers ==" CR), LogLoc); + + for(byte i = 1; i <= SensorIndex_length; i++) { + Log.verbose(F("%s Sensor_Index %d, Name %s, Readings" CR), LogLoc, i, SensorIndex[i].name ); + + for(byte j = 0; j < Max_Sensors_Read; j++) { + if(SensorIndex[i].read[j] > 0 ) { + Log.verbose(F("%s %d: %S %S (%d)" CR), LogLoc, j, Sensor_Read_descr[SensorIndex[i].read[j]], Sensor_Read_unit[SensorIndex[i].read[j]], SensorIndex[i].read[j]); + } + } + } + + Log.verbose(F("%s == configured Sensors ==" CR), LogLoc); + #endif + + + /* iterate through configured sensors */ + for(byte i = 0; i < Max_Sensors; i++) { + if(config.system.sensor.type[i] > 0) { + /* if sensor type is internal ADC */ + if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_INTADC) { + + #ifdef DEBUG + Log.verbose(F("%s Sensor ID %d: %s - %s (GPIO ID %d, GPIO %d), offering" CR), LogLoc, i,config.system.sensor.name[i], + SensorIndex[config.system.sensor.type[i]].name, config.system.sensor.gpio[i][0], GPIOindex[config.system.sensor.gpio[i][0]].gpio); + #endif + + /* initialize */ + sensorStatus[i] = Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.gpio[i][0], SENSOR_AIU_MODE_INIT); + + #ifdef DEBUG + /* when init was successful, list the sensor values */ + if(sensorStatus[i] == true) { + Sensor_Log_Readings(config.system.sensor.type[i], config.system.sensor.gpio[i][0]); + + } + #endif + + } else if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_I2C) { + /* when SensorIndex[].type is == I2C sensor*/ + /* get only the I2C Address */ + byte Addr = Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.i2c_addr[i], SENSOR_AIU_MODE_ADDR); + #ifdef DEBUG + Log.verbose(F("%s Sensor ID %d: %s - %s (I2C %d, 0x%x), offering" CR), LogLoc, i,config.system.sensor.name[i], + SensorIndex[config.system.sensor.type[i]].name, config.system.sensor.i2c_addr[i], Addr); + #endif + /* initialize */ + sensorStatus[i] = Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.i2c_addr[i], SENSOR_AIU_MODE_INIT); + #ifdef DEBUG + /* when init was successful, list the sensor values */ + if(sensorStatus[i] == true) { + Sensor_Log_Readings(config.system.sensor.type[i], config.system.sensor.i2c_addr[i]); + } + #endif + } + } + } +} + + +void Sensor_Update() { + /* Update all configured sensors Values */ + const static char LogLoc[] PROGMEM = "[Sensor:Update]"; + + #ifdef DEBUG2 + unsigned long mStart = millis(); + unsigned long mStop; + Log.verbose(F("%s Start %u" CR), LogLoc, mStart); + #endif + + /* go through all possible existing Sensor configurations */ + for(byte i = 0; i < Max_Sensors; i++) { + /* every configured one */ + if(config.system.sensor.type[i] > 0) { + /* if internal ADC */ + if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_INTADC) { + if(sensorStatus[i] == true) { + #ifdef DEBUG2 + Log.verbose(F("%s (%d) %s: %s (%d)" CR), LogLoc, i, config.system.sensor.name[i], SensorIndex[config.system.sensor.type[i]].name, config.system.sensor.gpio[i][0]); + #endif + /* perform update of sensor values */ + Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.gpio[i][0], SENSOR_AIU_MODE_UPDATE); + #ifdef DEBUG2 + Sensor_Log_Readings(config.system.sensor.type[i], config.system.sensor.gpio[i][0]); + #endif + } + #ifdef DEBUG2 + else { + Log.verbose(F("%s Sensor %d (%s, %d) not initialized." CR), LogLoc, i, SensorIndex[config.system.sensor.type[i]].name, Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.gpio[i][0], SENSOR_AIU_MODE_ADDR)); + } + #endif + /* Everything above 1 is an I2C sensor */ + } else if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_I2C) { + if(sensorStatus[i] == true) { + #ifdef DEBUG2 + Log.verbose(F("%s (%d) %s: %s (%d)" CR), LogLoc, i, config.system.sensor.name[i], SensorIndex[config.system.sensor.type[i]].name, config.system.sensor.i2c_addr[i]); + #endif + /* perform update of sensor values */ + Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.i2c_addr[i], SENSOR_AIU_MODE_UPDATE); + #ifdef DEBUG2 + Sensor_Log_Readings(config.system.sensor.type[i], config.system.sensor.i2c_addr[i]); + #endif + } + #ifdef DEBUG2 + else { + Log.verbose(F("%s Sensor %d (%s, 0x%x) not initialized." CR), LogLoc, i, SensorIndex[config.system.sensor.type[i]].name, Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.i2c_addr[i], SENSOR_AIU_MODE_ADDR)); + } + #endif + } + } + } + + #ifdef DEBUG2 + mStop = millis(); + Log.verbose(F("%s Stop %u (%u)" CR), LogLoc, mStop, mStop - mStart); + #endif +} diff --git a/include/CanGrow_Timer.h b/include/CanGrow_Timer.h new file mode 100644 index 0000000..1103657 --- /dev/null +++ b/include/CanGrow_Timer.h @@ -0,0 +1,61 @@ +/* + * + * include/CanGrow_Timer.h - timer header file + * + * + * + */ + + +/* + * Timer stuff + */ + +void Timer_Sensor() { + Sensor_Update(); +} + +void Timer_Output() { + /* Update the outputs (switching GPIOs, sending webcall, etc) */ + Output_Update(); +} + + + +void Timer_Control() { + const static char LogLoc[] PROGMEM = "[Core:Timer_1s]"; + #ifdef DEBUG2 + Log.verbose(F("%s - trigger [Sensor:Update]" CR), LogLoc); + #endif + + /* Updating Light output states in memory */ + Control_Light(); + + /* Updating Air output states in memory */ + Control_Air(); + + /* Updating Water output sates in memory */ + Control_Water(); +} + +void Timer_3s() { + const static char LogLoc[] PROGMEM = "[Core:Timer_3s]"; + #ifdef DEBUG2 + Log.verbose(F("%s" CR), LogLoc); + #endif + +} + +void Timer_5s() { + const static char LogLoc[] PROGMEM = "[Core:Timer_5s]"; + #ifdef DEBUG2 + Log.verbose(F("%s" CR), LogLoc); + #endif +} + +void TimeR_Init() { + timer.setInterval(1000, Timer_Output); + timer.setInterval(1000, Timer_Sensor); + timer.setInterval(100, Timer_Control); + +} diff --git a/include/CanGrow_Webserver.h b/include/CanGrow_Webserver.h new file mode 100644 index 0000000..e69de79 --- /dev/null +++ b/include/CanGrow_Webserver.h @@ -0,0 +1,131 @@ +/* + * + * include/CanGrow_Webserver.h - webserver header file + * + * + * + */ + +/* + * include static files files + */ +#include "Webserver/File_cangrow_CSS.h" +#include "Webserver/File_cangrow_JS.h" +#include "Webserver/File_favicon_ico.h" + +/* + * include webpages header files + */ +#include "Webserver/Header.h" +#include "Webserver/Footer.h" +#include "Webserver/Webserver_Common.h" +#include "Webserver/Page_404.h" +#include "Webserver/Page_root.h" +#include "Webserver/Page_wifi.h" +#include "Webserver/Page_system.h" +#include "Webserver/Page_grow.h" + +/* + * include Api header files + */ +#include "Webserver/Api_sensor.h" + +AsyncWebServer webserver(80); +// load requestLogger middleware +AsyncLoggingMiddleware requestLogger; + +/* + * setup all the webhandlers + */ +void Webserver_Init() { + const static char LogLoc[] PROGMEM = "[Webserver]"; + Log.notice(F("%s initializing" CR), LogLoc); + + + /* url handler definition */ + webserver.on("/", HTTP_GET, WebPage_root); + webserver.on("/cangrow.css", HTTP_GET, WebFile_cangrow_CSS); + webserver.on("/cangrow.js", HTTP_GET, WebFile_cangrow_JS); + webserver.on("/favicon.ico", HTTP_GET, WebFile_favicon_ico); + + webserver.on("/wifi/", HTTP_GET, WebPage_wifi); + webserver.on("/wifi/", HTTP_POST, WebPage_wifi); + webserver.on("/system/", HTTP_GET, WebPage_system); + webserver.on("/system/", HTTP_POST, WebPage_system); + + webserver.on("/system/update", HTTP_GET, WebPage_system_update); + webserver.on("/system/update", HTTP_POST, WebPage_system_update, WebPage_system_update_ApplyUpdate); + + webserver.on("/system/restart", HTTP_GET, WebPage_system_restart); + webserver.on("/system/restart", HTTP_POST, WebPage_system_restart); + + webserver.on("/system/wipe", HTTP_GET, WebPage_system_wipe); + webserver.on("/system/wipe", HTTP_POST, WebPage_system_wipe); + + webserver.on("/system/output/", HTTP_GET, WebPage_system_output); + webserver.on("/system/output/", HTTP_POST, WebPage_system_output); + + webserver.on("/system/output/add", HTTP_GET, WebPage_system_output_add); + webserver.on("/system/output/add", HTTP_POST, WebPage_system_output_add); + + webserver.on("/system/sensor/", HTTP_GET, WebPage_system_sensor); + webserver.on("/system/sensor/", HTTP_POST, WebPage_system_sensor); + + webserver.on("/system/sensor/add", HTTP_GET, WebPage_system_sensor_add); + webserver.on("/system/sensor/add", HTTP_POST, WebPage_system_sensor_add); + + webserver.on("/system/sensor/calibrate", HTTP_GET, WebPage_system_sensor_calibrate); + webserver.on("/system/sensor/calibrate", HTTP_POST, WebPage_system_sensor_calibrate); + + + /* grow */ + webserver.on("/grow/", HTTP_GET, WebPage_grow); + webserver.on("/grow/", HTTP_POST, WebPage_grow); + webserver.on("/grow/light/", HTTP_GET, WebPage_grow_light); + webserver.on("/grow/light/", HTTP_POST, WebPage_grow_light); + webserver.on("/grow/air/", HTTP_GET, WebPage_grow_air); + webserver.on("/grow/air/", HTTP_POST, WebPage_grow_air); + webserver.on("/grow/water/", HTTP_GET, WebPage_grow_water); + webserver.on("/grow/water/", HTTP_POST, WebPage_grow_water); + webserver.on("/grow/dashboard/", HTTP_GET, WebPage_grow_dashboard); + webserver.on("/grow/dashboard/", HTTP_POST, WebPage_grow_dashboard); + /* api */ + //webserver.on("/api/sensor", HTTP_GET, Api_sensor_data); + webserver.on("/api/sensor/", HTTP_GET, Api_sensor_data); + webserver.on("/api/sensor/raw", HTTP_GET, Api_sensor_data_raw); + webserver.on("/api/sensor/driver", HTTP_GET, Api_sensor_driver); + + + /* DEBUG only - offer config for direct download */ + #ifndef DEBUG + webserver.serveStatic(CANGROW_CFG, LittleFS, CANGROW_CFG); + #endif + + /* 404 Error page */ + webserver.onNotFound(WebserverNotFound); + + + // this activates the middleware + if(config.system.httpLogSerial == true) { + requestLogger.setOutput(Serial); + Log.notice(F("%s serial logging: enabled" CR), LogLoc); + webserver.addMiddleware(&requestLogger); + } else { + Log.notice(F("%s serial logging: disabled" CR), LogLoc); + } + + + // Workaround, see comment at + // https://github.com/mathieucarbou/ESPAsyncWebServer/blob/main/docs/index.md#scanning-for-available-wifi-networks + // call the network scan once, so there are some values at the first call + // of the wifi settings page. otherwise the first call of the wifi scan would return + // an empty list of networks + Log.notice(F("%s call [wifi:ScanNetworks] to workaround empty scan results bug" CR), LogLoc); + + WebPage_wifi_ScanNetworks(); + + webserver.begin(); + Log.notice(F("%s Ready to serve" CR), LogLoc); + +} + diff --git a/include/CanGrow_Wifi.h b/include/CanGrow_Wifi.h new file mode 100644 index 0000000..7aa9ec4 --- /dev/null +++ b/include/CanGrow_Wifi.h @@ -0,0 +1,136 @@ +/* + * + * include/CanGrow_Wifi.h - Wifi stuff header file + * + * + * + */ + + +void Wifi_AP() { + const static char LogLoc[] PROGMEM = "[WiFi:AP]"; + char randNr[5]; + itoa(random(9999), randNr, 10); + //WiFi.softAPConfig(config.wifi.ip, config.wifi.gateway, config.wifi.netmask); + IPAddress ip(192,168,4,20); + IPAddress gateway(0,0,0,0); + IPAddress netmask(255,255,255,0); + WiFi.softAPConfig(ip, gateway, netmask); + + /* when no ssid is configured, we assume here cangrow is in a fresh factory reset mode + * when a ssid is already configured, we seem not to be able to connect to it. so we protect + * our already configured cangrow controller with setting a temporary wifi ap password + * and log it to serial. */ + if(strlen(config.wifi.ssid) > 0) { + const char * password = RandomString(); + /* growName[64] + 8 */ + char ssid[20+5]; + strcpy(ssid, "CanGrow-FAILED-WIFI-"); + /* random maximum 4 digit number for ssid + * https://arduino.stackexchange.com/a/42987*/ + + strcat(ssid, randNr); + + Log.error(F("%s create access point" CR), LogLoc); + + Log.error(F("%s SSID : %s" CR), LogLoc, ssid); + Log.error(F("%s Password: %s" CR), LogLoc, password); + WiFi.softAP(ssid, password); + } else { + char ssid[21+4]; + strcpy(ssid, CANGROW_DEFAULT_WIFI_SSID); + //strcat(ssid, "-"); + /* random maximum 4 digit number for ssid + * https://arduino.stackexchange.com/a/42987*/ + //strcat(ssid, randNr); + /* start access point default password when being unconfigured */ + Log.notice(F("%s create access point" CR), LogLoc); + Log.notice(F("%s SSID : %S" CR), LogLoc, ssid); + Log.notice(F("%s Password: %S" CR), LogLoc, CANGROW_DEFAULT_WIFI_PASSWORD); + WiFi.softAP(ssid, CANGROW_DEFAULT_WIFI_PASSWORD); + //WiFi.softAP(CANGROW_DEFAULT_WIFI_SSID); + } + + + Log.notice(F("%s access point started." CR), LogLoc); + Log.notice(F("%s IP : %s" CR), LogLoc, IP2Char(ip)); + Log.notice(F("%s Netmask : %s" CR), LogLoc, IP2Char(netmask)); +} + + +void Wifi_Connect() { + const static char LogLoc[] PROGMEM = "[WiFi:Connect]"; + Log.notice(F("%s connecting to SSID: %s" CR), LogLoc, config.wifi.ssid); + + WiFi.begin(config.wifi.ssid, config.wifi.password); + if(config.wifi.dhcp == false) { + Log.notice(F("%s using static ip configuration:" CR), LogLoc); + + Log.notice(F("%s IP : %s" CR), LogLoc, IP2Char(config.wifi.ip)); + Log.notice(F("%s Netmask: %s" CR), LogLoc, IP2Char(config.wifi.netmask)); + Log.notice(F("%s Gateway: %s" CR), LogLoc, IP2Char(config.wifi.gateway)); + Log.notice(F("%s DNS : %s" CR), LogLoc, IP2Char(config.wifi.dns)); + + WiFi.config(config.wifi.ip, config.wifi.dns, config.wifi.gateway, config.wifi.netmask); + } else { + Log.notice(F("%s using DHCP for ip configuration" CR), LogLoc); + } + + Log.notice("%s ", LogLoc); + const byte max = 30; + byte count = 0; + // wait until WiFi connection is established + while (count < max) { + /* check connection stations */ + if(WiFi.status() != WL_CONNECTED) { + /* if not connected, print dot and increment count */ + delay(500); + Serial.print("."); + count++; + } else { + /* if connected, set count to 10 to exit loop*/ + count = max+1; + } + } + + /* check connection status. */ + if(WiFi.status() != WL_CONNECTED) { + /* if connection failed, create AP */ + Log.error(F("FAILED! Fallback to AP mode" CR), LogLoc); + WiFi.disconnect(); + /* + * TODO / BUG + * + * On ESP32 there are no scan results shown in wifi tab, when connect to + * a saved network failed and the esp created then its own network. + * + * without trying to connect it works fine, like when doing a factory reset. + * + * switch mode to softAP + /* WiFi.mode(WIFI_AP_STA); + */ + + Wifi_AP(); + } else { + Serial.println("CONNECTED!"); + if(config.wifi.dhcp == true) { + Log.notice(F("%s DHCP offered ip configuration:" CR), LogLoc); + Log.notice(F("%s IP : %s" CR), LogLoc, IP2Char(WiFi.localIP())); + Log.notice(F("%s Netmask: %s" CR), LogLoc, IP2Char(WiFi.subnetMask())); + Log.notice(F("%s Gateway: %s" CR), LogLoc, IP2Char(WiFi.gatewayIP())); + Log.notice(F("%s DNS : %s" CR), LogLoc, IP2Char(WiFi.dnsIP())); + } + } +} + +void Wifi_Init() { + const static char LogLoc[] PROGMEM = "[WiFi:Init]"; + Log.notice(F("%s" CR), LogLoc); + + if(strlen(config.wifi.ssid) == 0) { + Log.notice(F("%s config.wifi.ssid is unset" CR), LogLoc); + Wifi_AP(); + } else { + Wifi_Connect(); + } +} diff --git a/include/Output/Output_Common.h b/include/Output/Output_Common.h new file mode 100644 index 0000000..b00b2e9 --- /dev/null +++ b/include/Output/Output_Common.h @@ -0,0 +1,84 @@ +/* + * + * include/Output/Output_Common.h - Output common header file + * + * + * + */ + +/* + * Output Type + */ + +// How many output types exist +const byte OUTPUT_TYPE__TOTAL = 3; + +const byte OUTPUT_TYPE_GPIO = 1; +const byte OUTPUT_TYPE_I2C = 2; +const byte OUTPUT_TYPE_WEB = 3; + +const char OUTPUT_TYPE_GPIO_descr[] PROGMEM = {"GPIO"}; +const char OUTPUT_TYPE_I2C_descr[] PROGMEM = {"I2C"}; +const char OUTPUT_TYPE_WEB_descr[] PROGMEM = {"Webcall"}; + +const char * Output_Type_descr[] = { + NULL, // 0 - no description because 0 means unconfigured + OUTPUT_TYPE_GPIO_descr, + OUTPUT_TYPE_I2C_descr, + OUTPUT_TYPE_WEB_descr, +}; + +/* Output_GPIO_Addr_Init_Update() modes */ +const byte OUTPUT_GPIO_IU_MODE_INIT = 0; +const byte OUTPUT_GPIO_IU_MODE_UPDATE = 1; + +/* Output_Webcall_Addr_Init_Update() modes */ +const byte OUTPUT_WEB_IU_MODE_INIT = 0; +const byte OUTPUT_WEB_IU_MODE_UPDATE = 1; + + +/* + * OutputI2C types / modules + */ +const byte OUTPUT_TYPE_I2C_MAX_PORTS = 2; +/* Total number of I2C PORT Types */ +const byte OUTPUT_TYPE_I2C_PORT__TOTAL = 1; +/* port type for percentage. Those ports receive an int from 0 up to 100 to set their output value */ +const byte OUTPUT_TYPE_I2C_PORT_BYTE = 1; + +/* Output_I2C_Addr_Init_Update() modes */ +const byte OUPUT_I2C_AIU_MODE_ADDR = 0; +const byte OUPUT_I2C_AIU_MODE_INIT = 1; +const byte OUPUT_I2C_AIU_MODE_UPDATE = 2; + + +/* + * Output Device + */ +// 0 is unconfigured +const byte OUTPUT_DEVICE__TOTAL = 6; + +const byte OUTPUT_DEVICE_LIGHT = 1; +const byte OUTPUT_DEVICE_FAN = 2; +const byte OUTPUT_DEVICE_PUMP = 3; +const byte OUTPUT_DEVICE_HUMIDIFIER = 4; +const byte OUTPUT_DEVICE_DEHUMIDIFIER = 5; +const byte OUTPUT_DEVICE_HEATING = 6; + +const char OUTPUT_DEVICE_LIGHT_descr[] PROGMEM = {"💡 Light"}; +const char OUTPUT_DEVICE_FAN_descr[] PROGMEM = {"🌀 Fan"}; +const char OUTPUT_DEVICE_PUMP_descr[] PROGMEM = {"💧 Pump"}; +const char OUTPUT_DEVICE_HUMIDIFIER_descr[] PROGMEM = {"🌀 Humidifier"}; +const char OUTPUT_DEVICE_DEHUMIDIFIER_descr[] PROGMEM = {"🌀 Dehumidifier"}; +const char OUTPUT_DEVICE_HEATING_descr[] PROGMEM = {"🌀 Heating"}; + + +const char * Output_Device_descr[] = { + NULL, // 0 - no description because 0 means unconfigured + OUTPUT_DEVICE_LIGHT_descr, + OUTPUT_DEVICE_FAN_descr, + OUTPUT_DEVICE_PUMP_descr, + OUTPUT_DEVICE_HUMIDIFIER_descr, + OUTPUT_DEVICE_DEHUMIDIFIER_descr, + OUTPUT_DEVICE_HEATING_descr, +}; diff --git a/include/Output/Output_I2C_01_MCP4725.h b/include/Output/Output_I2C_01_MCP4725.h new file mode 100644 index 0000000..6820f38 --- /dev/null +++ b/include/Output/Output_I2C_01_MCP4725.h @@ -0,0 +1,45 @@ +/* + * + * include/Output/OutputI2C_01_MCP4725.h - sensor header for I2C Output MCP4725 sensor + * + * + * + */ + +#include <Adafruit_MCP4725.h> + +#define OUTPUT_I2C_01_NAME "MCP4725" + +const byte Output_I2C_01_MCP4725_Addr[] = { 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67 }; + +Adafruit_MCP4725 MCP4725[sizeof(Output_I2C_01_MCP4725_Addr)]; + +const byte Output_I2C_01_MCP4725_Ports = 1; + +void Output_I2C_01_MCP4725_Update(const byte AddrId, const byte PortId, const byte Value) { + /* Update Output Port of I2C Module */ + const static char LogLoc[] PROGMEM = "[Output:I2C:01_MCP4725:Update]"; + + /* 'Value' , which comes from outputState[], is byte, so 0-255. So we need to map this to the MCPs 0-4095 */ + MCP4725[AddrId].setVoltage(map(Value, 0, 255, 0, 4095), false); + + #ifdef DEBUG + Log.verbose(F("%s 0x%x Port %d, Value %d, MCP4725_Value %d" CR), LogLoc, Output_I2C_01_MCP4725_Addr[AddrId], PortId, Value, map(Value, 0, 255, 0, 4095)); + #endif + +} + +bool Output_I2C_01_MCP4725_Init(const byte AddrId, const byte PortId) { + /* Initialize I2C Module, return true when successful */ + const static char LogLoc[] PROGMEM = "[Output:I2C:01_MCP4725:Init]"; + bool returnCode; + if(MCP4725[AddrId].begin(Output_I2C_01_MCP4725_Addr[AddrId])) { + Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Output_I2C_01_MCP4725_Addr[AddrId]); + Output_I2C_01_MCP4725_Update(AddrId, PortId, 50); + returnCode = true; + } else { + Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Output_I2C_01_MCP4725_Addr[AddrId]); + returnCode = false; + } + return returnCode; +} diff --git a/include/Output/Output_I2C_02_GP8403.h b/include/Output/Output_I2C_02_GP8403.h new file mode 100644 index 0000000..ad16776 --- /dev/null +++ b/include/Output/Output_I2C_02_GP8403.h @@ -0,0 +1,52 @@ +/* + * + * include/Output/OutputI2C_Output_I2C_02_GP8403.h - sensor header for I2C Output GP8403 (DFR Gravity) sensor + * + * + * + */ + +#include <DFRobot_GP8XXX.h> + +#define OUTPUT_I2C_02_NAME "GP8403" + +const byte Output_I2C_02_GP8403_Addr[] = { 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F }; + +DFRobot_GP8403 GP8403[sizeof(Output_I2C_02_GP8403_Addr)]; + +const byte Output_I2C_02_GP8403_Ports = 2; + + +void Output_I2C_02_GP8403_Update(const byte AddrId, const byte PortId, const byte Value) { + /* Update Output Port of I2C Module */ + const static char LogLoc[] PROGMEM = "[Output:I2C:02_GP8403:Update]"; + + /* 'Value' , which comes from outputState[], is byte, so 0-255. So we need to map this to the GP8403 0-4095 */ + GP8403[AddrId].setDACOutVoltage(map(Value, 0, 255, 0, 4095), PortId); + + #ifdef DEBUG + Log.verbose(F("%s 0x%x Port %d, Value %d, GP8403_Value %d" CR), LogLoc, Output_I2C_02_GP8403_Addr[AddrId], PortId, Value, map(Value, 0, 255, 0, 4095)); + #endif + +} + +bool Output_I2C_02_GP8403_Init(const byte AddrId, const byte PortId) { + /* Initialize I2C Module, return true when successful */ + const static char LogLoc[] PROGMEM = "[Output:I2C:02_GP8403:Init]"; + bool returnCode; + + /* Overwrite the default address of the library 0x58 with configured one */ + GP8403[AddrId] = DFRobot_GP8403(Output_I2C_02_GP8403_Addr[AddrId]); + + if(GP8403[AddrId].begin() == 0) { + /* Set output to 0-10V - this is what most grow devices are using as control standard */ + GP8403[AddrId].setDACOutRange(GP8403[AddrId].eOutputRange10V); + Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Output_I2C_02_GP8403_Addr[AddrId]); + Output_I2C_02_GP8403_Update(AddrId, PortId, 0); + returnCode = true; + } else { + Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Output_I2C_02_GP8403_Addr[AddrId]); + returnCode = false; + } + return returnCode; +} diff --git a/include/Sensor/00_Example.h b/include/Sensor/00_Example.h new file mode 100644 index 0000000..4c18eee --- /dev/null +++ b/include/Sensor/00_Example.h @@ -0,0 +1,46 @@ +/* + * + * include/Sensor/00_Example.h - example sensor header I2C device + * + * + * + */ + +#include <Adafruit_WhateverLib.h> + +#define SENSOR_00_NAME "Example sensor" + +const byte Sensor_00_Example_Addr[] = { 0x00, 0x01 }; + +Adafruit_WhateverLib Whatever[sizeof(Sensor_00_Example_Addr)]; + +/* Create main data array specifying max amount of readings */ +float Sensor_00_Example[sizeof(Sensor_00_Example_Addr)][4]; + +void Sensor_00_Example_Update(const byte AddrId) { + /* keep the same order as in SensorIndex[].read[] !! */ + Sensor_00_Example[AddrId][0] = Whatever[AddrId].temperature(); + Sensor_00_Example[AddrId][1] = Whatever[AddrId].humidity(); + Sensor_00_Example[AddrId][2] = Whatever[AddrId].raw1(); + Sensor_00_Example[AddrId][3] = Whatever[AddrId].raw2(); +} + +bool Sensor_00_Example_Init(const byte AddrId) { + /* Sensor Init function + * + * returns true (1) when Init was successful + * returns false (0) if not. + */ + const static char LogLoc[] PROGMEM = "[Sensor:00_Example:Init]"; + bool returnCode; + + if(Whatever[AddrId].begin(Sensor_00_Example_Addr[AddrId])) { + Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_00_Example_Addr[AddrId]); + Sensor_00_Example_Update(AddrId); + returnCode = true; + } else { + Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_00_Example_Addr[AddrId]); + returnCode = false; + } + return returnCode; +} diff --git a/include/Sensor/01_ADC_builtin.h b/include/Sensor/01_ADC_builtin.h new file mode 100644 index 0000000..68e1b43 --- /dev/null +++ b/include/Sensor/01_ADC_builtin.h @@ -0,0 +1,112 @@ +/* + * + * include/Sensor/01_ADC_builtin.h - sensor header for builtin ADC + * + * + * "Driver" for the internal ADC of the ESP8266 and ESP32 + * + * Bit dirty hacky workaround to support both boards ADC + * + * ESP8266 only has one ADC onboard. For "Multiplexing" I have added + * the to "add a GPIO" to it. It simply turns on the given GPIO for + * 100ms, and then turns it off. + * This is kinda a poor (wo)mans multiplexer. Control the supply voltage + * of your analog sensor with the GPIO, put a diode to the AOUT and enjoy. + * + * You can theoretically use all available pins as "multiplexer", thats why + * the Sensor_01_ADC[] array is as large as GPIOindex_length + * ************ + * + * ESP32 has a bunch of ADCs onboard. So it is not needed to go this hacky way, + * we can just use all the nice ADCs available. + * + * in GPIOindex.note we get the info if the GPIO is an ADC or not + * INT_ADC and INPUT_ONLY tells us this. + * To save memory, I build an own "index" for the available + * ADCs. Thats wahat Sensor_01_ADC_ArrId() is for. + * + * It returns which Index / slot in the array the given GPIO ID + * from GPIOindex has. + */ + +#define SENSOR_01_NAME "ADC builtin" + +/* Create main data array specifying max amount of readings */ +#ifdef ESP8266 +const byte SENSOR_01_MAX = GPIOindex_length + 1; +#endif + +#ifdef ESP32 +/* indexing function for our ADC GPIO pins we could theoretically all use */ +byte Sensor_01_ADC_ArrId(const byte GPIOid) { + const static char LogLoc[] PROGMEM = "[Sensor:01_ADC:Slot]"; + byte count = 0; + //Log.verbose(F("%s GPIO %d (%d) START" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid); + for(byte i = 1; i <= GPIOindex_length; i++) { + //Log.verbose(F("%s GPIO %d (%d) NOTE %d - %S" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid, GPIOindex[i].note, GPIO_Index_note_descr[GPIOindex[i].note]); + if((GPIOindex[i].note == INPUT_ONLY) || (GPIOindex[i].note == INT_ADC)) { + if(i == GPIOid) { + //Log.verbose(F("%s ??? GPIO %d (%d) i %d" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid, i); + return count; + } else { + count++; + //Log.verbose(F("%s GPIO %d (%d) i %d count" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid, i, count); + } + } + } + return 0; +} + +/* this dumb, but yeah - i counted the avail ADC manually (INT_ADC or INPUT_ONLY) */ +const byte SENSOR_01_MAX = 6; +#endif + +int Sensor_01_ADC[SENSOR_01_MAX]; + +void Sensor_01_ADC_Update(const byte GPIOid) { + const static char LogLoc[] PROGMEM = "[Sensor:01_ADC:Update]"; + #ifdef ESP8266 + if(GPIOid > 0) { + //digitalWrite(GPIOindex[GPIOid].gpio, HIGH); + //Log.notice(F("%s GPIO %d (%d) delay ON" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid); + digitalWrite(GPIOindex[GPIOid].gpio, HIGH); + delay(50); + Sensor_01_ADC[GPIOid] = analogRead(A0); + digitalWrite(GPIOindex[GPIOid].gpio, LOW); + //Log.notice(F("%s GPIO %d (%d) delay OFF" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid); + } else { + //Log.notice(F("%s GPIO %d (%d) READ" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid); + Sensor_01_ADC[GPIOid] = analogRead(A0); + } + #endif + + #ifdef ESP32 + byte slot = Sensor_01_ADC_ArrId(GPIOid); + //Log.notice(F("%s GPIO %d (%d) READ - slot %d" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid, slot); + Sensor_01_ADC[slot] = analogRead(GPIOindex[GPIOid].gpio); + //Log.notice(F("%s GPIO %d (%d) READ - slot %d Val %d" CR), LogLoc, GPIOindex[GPIOid].gpio, GPIOid, slot, Sensor_01_ADC[slot]); + #endif + +} + +bool Sensor_01_ADC_Init(const byte GPIOid) { + /* Sensor Init function + * + * returns true (1) when Init was successful + * returns false (0) if not. + */ + const static char LogLoc[] PROGMEM = "[Sensor:01_ADC:Init]"; + + #ifdef ESP8266 + //Log.notice(F("%s setting GPIO ID %d (%d) as OUTPUT for internal ADC" CR), LogLoc, GPIOid, GPIOindex[GPIOid].gpio); + pinMode(GPIOindex[GPIOid].gpio, OUTPUT); + #endif + + #ifdef ESP32 + //Log.notice(F("%s GPIO ID %d (%d) as INPUT for internal ADC slot %d" CR), LogLoc, GPIOid, GPIOindex[GPIOid].gpio, Sensor_01_ADC_ArrId(GPIOid)); + //pinMode(GPIOindex[GPIOid].gpio, INPUT); + #endif + + Sensor_01_ADC_Update(GPIOid); + return true; +} diff --git a/include/Sensor/02_BME280.h b/include/Sensor/02_BME280.h new file mode 100644 index 0000000..0b72163 --- /dev/null +++ b/include/Sensor/02_BME280.h @@ -0,0 +1,54 @@ +/* + * + * include/Sensor/00_ADC_builtin.h - sensor header for BME280 I2C sensor + * + * + * + */ + + +#include <Adafruit_BME280.h> + + +#define SENSOR_02_NAME "BME280" +//#define SENSOR_02_MAXUNITS 2 + +/* available addresses in byte array, default is at 0 */ +const byte Sensor_02_BME280_Addr[] = { 0x76, 0x77 }; + +Adafruit_BME280 BME280[sizeof(Sensor_02_BME280_Addr)]; + +/* creation of BME280 Value Struct, as many as addresses */ +//Sensor_02_BME280 Sensor_02_BME280_Data[sizeof(Sensor_02_BME280_Addr)]; + +/* main data array */ +float Sensor_02_BME280[sizeof(Sensor_02_BME280_Addr)][4]; + +void Sensor_02_BME280_Update(const byte AddrId) { + /* Temp */ + Sensor_02_BME280[AddrId][0] = BME280[AddrId].readTemperature(); + /* Humidity */ + Sensor_02_BME280[AddrId][1] = BME280[AddrId].readHumidity(); + /* Pressure */ + Sensor_02_BME280[AddrId][2] = BME280[AddrId].readPressure() / 1000.00; + /* Altitude */ + Sensor_02_BME280[AddrId][3] = BME280[AddrId].readAltitude(SEALEVELPRESSURE_HPA); +} + + +bool Sensor_02_BME280_Init(const byte AddrId) { + const static char LogLoc[] PROGMEM = "[Sensor:02_BME280:Init]"; + bool returnCode; + //Log.notice(F("%s Init at addr 0x%x (%d)" CR), LogLoc, Sensor_02_BME280_Addr[AddrId], AddrId); + if(BME280[AddrId].begin(Sensor_02_BME280_Addr[AddrId])) { + Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_02_BME280_Addr[AddrId]); + //Log.notice(F("%s Temp: %F°C Humidity: %F % Pressure: %FhPa, Appr. Altitude %Fm" CR), LogLoc, BME280[AddrId].readTemperature(), BME280[AddrId].readHumidity(), BME280[AddrId].readPressure() / 1000.00, BME280[AddrId].readAltitude(SEALEVELPRESSURE_HPA)); + Sensor_02_BME280_Update(AddrId); + returnCode = true; + } else { + Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_02_BME280_Addr[AddrId]); + returnCode = false; + } + + return returnCode; +} diff --git a/include/Sensor/03_BME680.h b/include/Sensor/03_BME680.h new file mode 100644 index 0000000..eedf416 --- /dev/null +++ b/include/Sensor/03_BME680.h @@ -0,0 +1,98 @@ +/* + * + * include/Sensor/00_ADC_builtin.h - sensor header for BME680 I2C sensor + * + * + * + */ + + +#include <Adafruit_BME680.h> + +#define SENSOR_03_NAME "BME680" + +/* available addresses in byte array, default is at 0 */ +const byte Sensor_03_BME680_Addr[] = { 0x77, 0x76 }; + +Adafruit_BME680 BME680[sizeof(Sensor_03_BME680_Addr)]; + +unsigned long BME680_endtime[sizeof(Sensor_03_BME680_Addr)]; + +/*struct Sensor_03_BME680 { + float humidity; + float temperature; + float pressure; + float altitude; + float gas_resistance; +}; +*/ +/* creation of BME680 Value Struct, as many as addresses */ +/*Sensor_03_BME680 Sensor_03_BME680_Data[sizeof(Sensor_03_BME680_Addr)];*/ + +float Sensor_03_BME680[sizeof(Sensor_03_BME680_Addr)][5]; + +/* for async read of BME680 we need to trigger a new reading cycle (as adafruit doc says) */ +void Sensor_03_BME680_BeginReading(const byte AddrId) { + const static char LogLoc[] PROGMEM = "[Sensor:03_BME680:BeginReading]"; + + #ifdef DEBUG3 + Log.warning(F("%s Start reading %u , finishing %u (0x%x)" CR), LogLoc, millis(), BME680_endtime[AddrId], Sensor_03_BME680_Addr[AddrId]); + #endif + + // Tell BME680 to begin measurement. + BME680_endtime[AddrId] = BME680[AddrId].beginReading(); + if(BME680_endtime[AddrId] == 0) { + Log.warning(F("%s Failed to begin reading (0x%x)" CR), LogLoc, Sensor_03_BME680_Addr[AddrId]); + } +} + +void Sensor_03_BME680_Update(const byte AddrId) { + const static char LogLoc[] PROGMEM = "[Sensor:03_BME680:Update]"; + + #ifdef DEBUG3 + Log.warning(F("%s Start reading %u , finishing %u (0x%x)" CR), LogLoc, millis(), BME680_endtime[AddrId], Sensor_03_BME680_Addr[AddrId]); + #endif + + if(!BME680[AddrId].endReading()) { + Log.warning(F("%s Failed to complete reading (0x%x)" CR), LogLoc, Sensor_03_BME680_Addr[AddrId]); + return; + } + + Sensor_03_BME680[AddrId][0] = BME680[AddrId].readTemperature(); + Sensor_03_BME680[AddrId][1] = BME680[AddrId].readHumidity(); + Sensor_03_BME680[AddrId][2] = BME680[AddrId].readPressure() / 1000; + Sensor_03_BME680[AddrId][3] = BME680[AddrId].readAltitude(SEALEVELPRESSURE_HPA); + Sensor_03_BME680[AddrId][4] = BME680[AddrId].gas_resistance / 1000.0; + + /* begin new reading cycle */ + Sensor_03_BME680_BeginReading(AddrId); + +} + +bool Sensor_03_BME680_Init(const byte AddrId) { + const static char LogLoc[] PROGMEM = "[Sensor:03_BME680:Init]"; + bool returnCode; + //Log.notice(F("%s Init at addr 0x%x (%d)" CR), LogLoc, Sensor_03_BME680_Addr[AddrId], AddrId); + if(BME680[AddrId].begin(Sensor_03_BME680_Addr[AddrId])) { + Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_03_BME680_Addr[AddrId]); + + // Set up oversampling and filter initialization + BME680[AddrId].setTemperatureOversampling(BME680_OS_8X); + BME680[AddrId].setHumidityOversampling(BME680_OS_2X); + BME680[AddrId].setPressureOversampling(BME680_OS_4X); + BME680[AddrId].setIIRFilterSize(BME680_FILTER_SIZE_3); + BME680[AddrId].setGasHeater(320, 150); // 320*C for 150 ms + + /* start to do readings here, like shown in async example + * https://github.com/adafruit/Adafruit_BME680/blob/master/examples/bme680async/bme680async.ino */ + Sensor_03_BME680_BeginReading(AddrId); + + Sensor_03_BME680_Update(AddrId); + returnCode = true; + } else { + Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_03_BME680_Addr[AddrId]); + returnCode = false; + } + return returnCode; +} + diff --git a/include/Sensor/04_SHT3x.h b/include/Sensor/04_SHT3x.h new file mode 100644 index 0000000..c9dea61 --- /dev/null +++ b/include/Sensor/04_SHT3x.h @@ -0,0 +1,43 @@ +/* + * + * include/Sensor/04_SHT3X.h - SHT3X I2C temp/humidity sensor + * + * + * + */ + +#include <Adafruit_SHT31.h> + +#define SENSOR_04_NAME "SHT3x" + +const byte Sensor_04_SHT3X_Addr[] = { 0x44, 0x45 }; + +Adafruit_SHT31 SHT3X[sizeof(Sensor_04_SHT3X_Addr)]; + +/* Create main data array specifying max amount of readings */ +float Sensor_04_SHT3X[sizeof(Sensor_04_SHT3X_Addr)][2]; + +void Sensor_04_SHT3X_Update(const byte AddrId) { + Sensor_04_SHT3X[AddrId][0] = SHT3X[AddrId].readTemperature(); + Sensor_04_SHT3X[AddrId][1] = SHT3X[AddrId].readHumidity(); + +} + +bool Sensor_04_SHT3X_Init(const byte AddrId) { + /* Sensor Init function + * + * returns true (1) when Init was successful + * returns false (0) if not. + */ + const static char LogLoc[] PROGMEM = "[Sensor:04_SHT3X:Init]"; + bool returnCode; + if(SHT3X[AddrId].begin(Sensor_04_SHT3X_Addr[AddrId])) { + Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_04_SHT3X_Addr[AddrId]); + Sensor_04_SHT3X_Update(AddrId); + returnCode = true; + } else { + Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_04_SHT3X_Addr[AddrId]); + returnCode = false; + } + return returnCode; +} diff --git a/include/Sensor/05_MLX90614.h b/include/Sensor/05_MLX90614.h new file mode 100644 index 0000000..dac5709 --- /dev/null +++ b/include/Sensor/05_MLX90614.h @@ -0,0 +1,44 @@ +/* + * + * include/Sensor/05_MLX90614.h - MLX90614 I2C IR temp sensor + * + * + * + */ + +#include <Adafruit_MLX90614.h> + +#define SENSOR_05_NAME "MLX90614" + +const byte Sensor_05_MLX90614_Addr[] = { 0x5A, 0x5B, 0x5C, 0x5D }; + +Adafruit_MLX90614 MLX90614[sizeof(Sensor_05_MLX90614_Addr)]; + +/* Create main data array specifying max amount of readings */ +float Sensor_05_MLX90614[sizeof(Sensor_05_MLX90614_Addr)][2]; + +void Sensor_05_MLX90614_Update(const byte AddrId) { + /* keep the same order as in SensorIndex[].read[] !! */ + Sensor_05_MLX90614[AddrId][0] = MLX90614[AddrId].readAmbientTempC(); + Sensor_05_MLX90614[AddrId][1] = MLX90614[AddrId].readObjectTempC(); +} + +bool Sensor_05_MLX90614_Init(const byte AddrId) { + /* Sensor Init function + * + * returns true (1) when Init was successful + * returns false (0) if not. + */ + const static char LogLoc[] PROGMEM = "[Sensor:05_MLX90614:Init]"; + bool returnCode; + + if(MLX90614[AddrId].begin(Sensor_05_MLX90614_Addr[AddrId])) { + Log.notice(F("%s found at addr 0x%x - emissivity set to %F" CR), LogLoc, Sensor_05_MLX90614_Addr[AddrId], MLX90614[AddrId].readEmissivity()); + Sensor_05_MLX90614_Update(AddrId); + returnCode = true; + } else { + Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_05_MLX90614_Addr[AddrId]); + returnCode = false; + } + return returnCode; +} diff --git a/include/Sensor/06_TCS34725.h b/include/Sensor/06_TCS34725.h new file mode 100644 index 0000000..10dc0c5 --- /dev/null +++ b/include/Sensor/06_TCS34725.h @@ -0,0 +1,72 @@ +/* + * + * include/Sensor/06_TCS34725.h - header for I2C color sensor TCS34725 + * + * + * + */ + +//#include "TCS34725.h" + +#include "Adafruit_TCS34725.h" + +#define SENSOR_06_NAME "TCS34725" + +const byte Sensor_06_TCS34725_Addr[] = { 0x29 }; + +Adafruit_TCS34725 TCS34725[sizeof(Sensor_06_TCS34725_Addr)]; +/* This library causes a 240ms (or greater if chosen) delay when reading the values from the sensor + * this is not optimal, and there are libs workarounding this behaviour. + * But unfortunatelly the other libs wont connect successful by i2c to the sensor, + * which only the adafruit lib does reliably 240MS integration time and 4x gain + * seems to be the sweet spot between delay and value resolutin */ + + +/* Create main data array specifying max amount of readings */ +float Sensor_06_TCS34725[sizeof(Sensor_06_TCS34725_Addr)][5]; + +void Sensor_06_TCS34725_Update(const byte AddrId) { + uint16_t r, g, b, c, colorTemp, lux; + + TCS34725[AddrId].getRawData(&r, &g, &b, &c); + colorTemp = TCS34725[AddrId].calculateColorTemperature_dn40(r, g, b, c); + lux = TCS34725[AddrId].calculateLux(r, g, b); + Sensor_06_TCS34725[AddrId][0] = colorTemp; + Sensor_06_TCS34725[AddrId][1] = lux; + Sensor_06_TCS34725[AddrId][2] = r; + Sensor_06_TCS34725[AddrId][3] = g; + Sensor_06_TCS34725[AddrId][4] = b; + +} + +bool Sensor_06_TCS34725_Init(const byte AddrId) { + /* Sensor Init function + * + * returns true (1) when Init was successful + * returns false (0) if not. + */ + const static char LogLoc[] PROGMEM = "[Sensor:06_TCS34725:Init]"; + bool returnCode; + + if(TCS34725[AddrId].begin()) { + /* Here I hardcoded here the values for Integration time and Gain. + * For calibration I used my desk lamp and a lux smartphone app. + * I fooled around until the smartphone app reading was kinda the + * same as the TCS34725 ones. Yay! + * + * Comes out TCS34725_INTEGRATIONTIME_240MS and TCS34725_GAIN_16X + * seem to be good values. Smartphone reading of my desk lamp is + * 3507lx and on the exakt same spot, height, angle and so on the + * TCS34725 measures 3487lx. I guess this is fine. */ + + Log.notice(F("%s found at addr 0x%x - Integration time: 240ms Gain: 16x" CR), LogLoc, Sensor_06_TCS34725_Addr[AddrId]); + TCS34725[AddrId].setIntegrationTime(TCS34725_INTEGRATIONTIME_240MS); + TCS34725[AddrId].setGain(TCS34725_GAIN_16X); + Sensor_06_TCS34725_Update(AddrId); + returnCode = true; + } else { + Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_06_TCS34725_Addr[AddrId]); + returnCode = false; + } + return returnCode; +} diff --git a/include/Sensor/07_ADS1115.h b/include/Sensor/07_ADS1115.h new file mode 100644 index 0000000..f63f353 --- /dev/null +++ b/include/Sensor/07_ADS1115.h @@ -0,0 +1,38 @@ +/* + * + * include/Sensor/07_ADS1115.h - ADS1115 16 bit ADC I2C driver + * + * + * + */ + +#include <Adafruit_ADS1X15.h> + +#define SENSOR_07_NAME "ADS1115" + + +const byte Sensor_07_ADS1115_Addr[] = { 0x48, 0x49, 0x4A, 0x4B }; + +Adafruit_ADS1115 ADS1115[sizeof(Sensor_07_ADS1115_Addr)]; + +int Sensor_07_ADS1115[sizeof(Sensor_07_ADS1115_Addr)][4]; + +void Sensor_07_ADS1115_Update(const byte AddrId) { + for(byte i = 0; i < 4; i++) { + Sensor_07_ADS1115[AddrId][i] = ADS1115[AddrId].readADC_SingleEnded(i); + } +} + +bool Sensor_07_ADS1115_Init(const byte AddrId) { + const static char LogLoc[] PROGMEM = "[Sensor:07_ADS1115:Init]"; + bool returnCode; + if(ADS1115[AddrId].begin(Sensor_07_ADS1115_Addr[AddrId])) { + Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_07_ADS1115_Addr[AddrId]); + Sensor_07_ADS1115_Update(AddrId); + returnCode = true; + } else { + Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_07_ADS1115_Addr[AddrId]); + returnCode = false; + } + return returnCode; +} diff --git a/include/Sensor/08_ADS1015.h b/include/Sensor/08_ADS1015.h new file mode 100644 index 0000000..1ed6f03 --- /dev/null +++ b/include/Sensor/08_ADS1015.h @@ -0,0 +1,41 @@ +/* + * + * include/Sensor/08_ADS1015.h - ADS1115 16 bit ADC I2C driver + * + * + * + */ + +/* + * #include <Adafruit_ADS1X15.h> + * This already got included in Sensor_07_ADS1115.h + */ + +#define SENSOR_08_NAME "ADS1015" + + +const byte Sensor_08_ADS1015_Addr[] = { 0x48, 0x49, 0x4A, 0x4B }; + +Adafruit_ADS1015 ADS1015[sizeof(Sensor_08_ADS1015_Addr)]; + +int Sensor_08_ADS1015[sizeof(Sensor_08_ADS1015_Addr)][4]; + +void Sensor_08_ADS1015_Update(const byte AddrId) { + for(byte i = 0; i < 4; i++) { + Sensor_08_ADS1015[AddrId][i] = ADS1015[AddrId].readADC_SingleEnded(i); + } +} + +bool Sensor_08_ADS1015_Init(const byte AddrId) { + const static char LogLoc[] PROGMEM = "[Sensor:08_ADS1015:Init]"; + bool returnCode; + if(ADS1015[AddrId].begin(Sensor_08_ADS1015_Addr[AddrId])) { + Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_08_ADS1015_Addr[AddrId]); + Sensor_08_ADS1015_Update(AddrId); + returnCode = true; + } else { + Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_08_ADS1015_Addr[AddrId]); + returnCode = false; + } + return returnCode; +} diff --git a/include/Sensor/09_Chirp.h b/include/Sensor/09_Chirp.h new file mode 100644 index 0000000..7499d68 --- /dev/null +++ b/include/Sensor/09_Chirp.h @@ -0,0 +1,88 @@ +/* + * + * include/Sensor/09_Chirp.h - example sensor header I2C device + * + * + * + */ + +#include <I2CSoilMoistureSensor.h> + +#define SENSOR_09_NAME "I2C-Chirp" + +const byte Sensor_09_Chirp_Addr[] = { 0x20, 0x21, 0x22, 0x23 }; + +I2CSoilMoistureSensor Chirp[sizeof(Sensor_09_Chirp_Addr)]; + +/* Create main data array specifying max amount of readings */ +float Sensor_09_Chirp[sizeof(Sensor_09_Chirp_Addr)][3]; + +void Sensor_09_Chirp_Update(const byte AddrId) { + const static char LogLoc[] PROGMEM = "[Sensor:09_Chirp:Update]"; + + #ifdef DEBUG + unsigned long mStart = millis(); + unsigned long mStop; + Log.verbose(F("%s Start %u" CR), LogLoc, mStart); + #endif + + /* keep the same order as in SensorIndex[].read[] !! */ + + #ifdef DEBUG + Log.verbose(F("%s capacitance (%u)" CR), LogLoc, millis()); + #endif + Sensor_09_Chirp[AddrId][0] = Chirp[AddrId].getCapacitance(); + + #ifdef DEBUG + Log.verbose(F("%s temperature (%u)" CR), LogLoc, millis()); + #endif + Sensor_09_Chirp[AddrId][1] = Chirp[AddrId].getTemperature()/(float)10; + + /* light sensor is disabled, because it takes 3s to read, which is just too much */ + //#ifndef DEBUG + //Log.verbose(F("%s light (%u)" CR), LogLoc, millis()); + //#endif + //Sensor_09_Chirp[AddrId][2] = Chirp[AddrId].getLight(true); + + Chirp[AddrId].sleep(); + + + #ifdef DEBUG + mStop = millis(); + Log.verbose(F("%s Stop %u (%u)" CR), LogLoc, mStop, mStop - mStart); + #endif +} + +bool Sensor_09_Chirp_Init(const byte AddrId) { + /* Sensor Init function + * + * returns true (1) when Init was successful + * returns false (0) if not. + */ + const static char LogLoc[] PROGMEM = "[Sensor:09_Chirp:Init]"; + bool returnCode; + + /* manually check if I2C address answers on bus, i2c chirp lib does not return a value */ + Wire.beginTransmission(Sensor_09_Chirp_Addr[AddrId]); + short i2cError = Wire.endTransmission(); + + /* when i2c sensor answered to our previous init request */ + if(i2cError == 0) { + Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_09_Chirp_Addr[AddrId]); + + #ifdef ESP8266 + /* maybe its not the best idea to place it here, but for the moment.. */ + Wire.setClockStretchLimit(2500); + #endif + + /* change chirp library I2C address, it will also trigger .begin() afterwards */ + Chirp[AddrId].changeSensor(Sensor_09_Chirp_Addr[AddrId], false); + + Sensor_09_Chirp_Update(AddrId); + returnCode = true; + } else { + Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_09_Chirp_Addr[AddrId]); + returnCode = false; + } + return returnCode; +} diff --git a/include/Sensor/10_CCS811.h b/include/Sensor/10_CCS811.h new file mode 100644 index 0000000..11df5d9 --- /dev/null +++ b/include/Sensor/10_CCS811.h @@ -0,0 +1,58 @@ +/* + * + * include/Sensor/10_CCS811_.h - CCS811 CO2 I2C sensor + * + * + * + */ + +#include "Adafruit_CCS811.h" + +#define SENSOR_10_NAME "CCS811" + +const byte Sensor_10_CCS811_Addr[] = { 0x5a, 0x5b }; + +Adafruit_CCS811 CCS811[sizeof(Sensor_10_CCS811_Addr)]; + +/* Create main data array specifying max amount of readings */ +float Sensor_10_CCS811[sizeof(Sensor_10_CCS811_Addr)][4]; + +void Sensor_10_CCS811_Update(const byte AddrId) { + const static char LogLoc[] PROGMEM = "[Sensor:10_CCS811:Update]"; + if(CCS811[AddrId].available()){ + if(!CCS811[AddrId].readData()){ + /* keep the same order as in SensorIndex[].read[] !! */ + /* CO2 in ppm */ + Sensor_10_CCS811[AddrId][0] = CCS811[AddrId].geteCO2(); + /* TVOC (Total Volatile Organic Compouds) */ + Sensor_10_CCS811[AddrId][1] = CCS811[AddrId].getTVOC(); + } + #ifndef DEBUG + else { + Log.error(F("%s 0x%x ERROR getting new data" CR), LogLoc, Sensor_10_CCS811_Addr[AddrId]); + } + #endif + } + + +} + +bool Sensor_10_CCS811_Init(const byte AddrId) { + /* Sensor Init function + * + * returns true (1) when Init was successful + * returns false (0) if not. + */ + const static char LogLoc[] PROGMEM = "[Sensor:10_CCS811:Init]"; + bool returnCode; + + if(CCS811[AddrId].begin(Sensor_10_CCS811_Addr[AddrId])) { + Log.notice(F("%s found at addr 0x%x" CR), LogLoc, Sensor_10_CCS811_Addr[AddrId]); + Sensor_10_CCS811_Update(AddrId); + returnCode = true; + } else { + Log.error(F("%s FAILED! Not found at addr 0x%x" CR), LogLoc, Sensor_10_CCS811_Addr[AddrId]); + returnCode = false; + } + return returnCode; +} diff --git a/include/Sensor/Sensor_Common.h b/include/Sensor/Sensor_Common.h new file mode 100644 index 0000000..7998335 --- /dev/null +++ b/include/Sensor/Sensor_Common.h @@ -0,0 +1,152 @@ +/* + * + * include/Sensor/Common.h - common sensor header file + * + * + * + */ + + + +/* + * Common used consts and variables, used within the Sensor header for example + */ + +// for bme280 and bme680 +#define SEALEVELPRESSURE_HPA (1013.25) + +/* sensor types, int ADC, i2c, one wire , two wire, ...*/ +const byte SENSOR_TYPE__TOTAL = 5; + +const byte SENSOR_TYPE_INTADC = 0; +const byte SENSOR_TYPE_I2C = 1; +const byte SENSOR_TYPE_ONEWIRE = 2; +const byte SENSOR_TYPE_TWOWIRE = 3; +const byte SENSOR_TYPE_I2C_WITH_GPIO = 4; + +/* How many different read types exists */ +const byte SENSOR_READ_TYPE__TOTAL = 14; + + +const byte SENSOR_READ_TYPE_RAW = 1; +const char SENSOR_READ_TYPE_RAW_descr[] PROGMEM = {"Raw value"}; +const char SENSOR_READ_TYPE_RAW_unit[] PROGMEM = {""}; + +const byte SENSOR_READ_TYPE_TEMP = 2; +const char SENSOR_READ_TYPE_TEMP_descr[] PROGMEM = {"Temperature"}; +const char SENSOR_READ_TYPE_TEMP_unit[] PROGMEM = {"°C"}; + +const byte SENSOR_READ_TYPE_HUMIDITY = 3; +const char SENSOR_READ_TYPE_HUMIDITY_descr[] PROGMEM = {"Humidity"}; +const char SENSOR_READ_TYPE_HUMIDITY_unit[] PROGMEM = {"%"}; + +const byte SENSOR_READ_TYPE_SOILMOISTURE = 4; +const char SENSOR_READ_TYPE_SOILMOISTURE_descr[] PROGMEM = {"Moisture"}; +const char SENSOR_READ_TYPE_SOILMOISTURE_unit[] PROGMEM = {"%"}; + +const byte SENSOR_READ_TYPE_PRESSURE = 5; +const char SENSOR_READ_TYPE_PRESSURE_descr[] PROGMEM = {"Pressure"}; +const char SENSOR_READ_TYPE_PRESSURE_unit[] PROGMEM = {"Pa"}; + +const byte SENSOR_READ_TYPE_ALTITUDE = 6; +const char SENSOR_READ_TYPE_ALTITUDE_descr[] PROGMEM = {"Altitude"}; +const char SENSOR_READ_TYPE_ALTITUDE_unit[] PROGMEM = {"m"}; + +const byte SENSOR_READ_TYPE_GAS_RESISTANCE = 7; +const char SENSOR_READ_TYPE_GAS_RESISTANCE_descr[] PROGMEM = {"Gas resistance"}; +const char SENSOR_READ_TYPE_GAS_RESISTANCE_unit[] PROGMEM = {"KOhm"}; + +const byte SENSOR_READ_TYPE_COLOR_TEMP = 8; +const char SENSOR_READ_TYPE_COLOR_TEMP_descr[] PROGMEM = {"Color temperature"}; +const char SENSOR_READ_TYPE_COLOR_TEMP_unit[] PROGMEM = {"K"}; + +const byte SENSOR_READ_TYPE_LUX = 9; +const char SENSOR_READ_TYPE_LUX_descr[] PROGMEM = {"Lux"}; +const char SENSOR_READ_TYPE_LUX_unit[] PROGMEM = {"lx"}; + +const byte SENSOR_READ_TYPE_COLOR_RED = 10; +const char SENSOR_READ_TYPE_COLOR_RED_descr[] PROGMEM = {"Color red"}; +const char SENSOR_READ_TYPE_COLOR_RED_unit[] PROGMEM = {""}; + +const byte SENSOR_READ_TYPE_COLOR_GREEN = 11; +const char SENSOR_READ_TYPE_COLOR_GREEN_descr[] PROGMEM = {"Color green"}; +const char SENSOR_READ_TYPE_COLOR_GREEN_unit[] PROGMEM = {""}; + +const byte SENSOR_READ_TYPE_COLOR_BLUE = 12; +const char SENSOR_READ_TYPE_COLOR_BLUE_descr[] PROGMEM = {"Color blue"}; +const char SENSOR_READ_TYPE_COLOR_BLUE_unit[] PROGMEM = {""}; + +const byte SENSOR_READ_TYPE_PARTS_PER_MILLION = 13; +const char SENSOR_READ_TYPE_PARTS_PER_MILLION_descr[] PROGMEM = {"Part per million"}; +const char SENSOR_READ_TYPE_PARTS_PER_MILLION_unit[] PROGMEM = {"ppm"}; + +const byte SENSOR_READ_TYPE_TVOC = 14; +const char SENSOR_READ_TYPE_TVOC_descr[] PROGMEM = {"TVOC"}; +const char SENSOR_READ_TYPE_TVOC_unit[] PROGMEM = {""}; + + +const char * Sensor_Read_descr[] = { + NULL, // 0 is unset + SENSOR_READ_TYPE_RAW_descr, + SENSOR_READ_TYPE_TEMP_descr, + SENSOR_READ_TYPE_HUMIDITY_descr, + SENSOR_READ_TYPE_SOILMOISTURE_descr, + SENSOR_READ_TYPE_PRESSURE_descr, + SENSOR_READ_TYPE_ALTITUDE_descr, + SENSOR_READ_TYPE_GAS_RESISTANCE_descr, + SENSOR_READ_TYPE_COLOR_TEMP_descr, + SENSOR_READ_TYPE_LUX_descr, + SENSOR_READ_TYPE_COLOR_RED_descr, + SENSOR_READ_TYPE_COLOR_GREEN_descr, + SENSOR_READ_TYPE_COLOR_BLUE_descr, + SENSOR_READ_TYPE_PARTS_PER_MILLION_descr, + SENSOR_READ_TYPE_TVOC_descr +}; + +const char * Sensor_Read_unit[] = { + NULL, // 0 is unset + SENSOR_READ_TYPE_RAW_unit, + SENSOR_READ_TYPE_TEMP_unit, + SENSOR_READ_TYPE_HUMIDITY_unit, + SENSOR_READ_TYPE_SOILMOISTURE_unit, + SENSOR_READ_TYPE_PRESSURE_unit, + SENSOR_READ_TYPE_ALTITUDE_unit, + SENSOR_READ_TYPE_GAS_RESISTANCE_unit, + SENSOR_READ_TYPE_COLOR_TEMP_unit, + SENSOR_READ_TYPE_LUX_unit, + SENSOR_READ_TYPE_COLOR_RED_unit, + SENSOR_READ_TYPE_COLOR_GREEN_unit, + SENSOR_READ_TYPE_COLOR_BLUE_unit, + SENSOR_READ_TYPE_PARTS_PER_MILLION_unit, + SENSOR_READ_TYPE_TVOC_unit +}; + + +/* How many different read convert types exists */ +const byte SENSOR_CONVERT_RAW_TYPE__TOTAL = 1; + +const byte SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE = 1; +const char SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE_descr[] PROGMEM = {"Soilmoisture"}; +const char SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE_unit[] PROGMEM = {"%"}; + +//const byte SENSOR_CONVERT_RAW_TYPE_OTHER = 2; +//const char SENSOR_CONVERT_RAW_TYPE_OTHER_descr[] PROGMEM = {"Other"}; +//const char SENSOR_CONVERT_RAW_TYPE_OTHER_unit[] PROGMEM = {"n/a"}; + + +const char * Sensor_Convert_Raw_descr[] = { + NULL, // 0 is unset + SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE_descr, + //SENSOR_CONVERT_RAW_TYPE_OTHER_descr +}; + +const char * Sensor_Convert_Raw_unit[] = { + NULL, // 0 is unset + SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE_unit, + //SENSOR_CONVERT_RAW_TYPE_OTHER_unit +}; + +// Addr_Init_Update modes +const byte SENSOR_AIU_MODE_ADDR = 0; +const byte SENSOR_AIU_MODE_INIT = 1; +const byte SENSOR_AIU_MODE_UPDATE = 2; diff --git a/include/Webserver/Api_sensor.h b/include/Webserver/Api_sensor.h new file mode 100644 index 0000000..e7a153b --- /dev/null +++ b/include/Webserver/Api_sensor.h @@ -0,0 +1,124 @@ +/* + * + * include/Webserver/Api_Sensor.h - Sensor API header file + * + * + * + */ + +void Api_sensor_data(AsyncWebServerRequest* request) { + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot().to<JsonObject>(); + //root["hello"] = "world"; + for(byte i = 0 ; i < Max_Sensors ; i++) { + if(config.system.sensor.type[i] > 0) { + + JsonObject objSensor = root["sensor"].add<JsonObject>(); + objSensor["id"] = i; + objSensor["name"] = config.system.sensor.name[i]; + objSensor["type"] = SensorIndex[config.system.sensor.type[i]].name; + objSensor["status"] = sensorStatus[i]; + if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_I2C) + objSensor["i2c_addr"] = "0x" + String(Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.i2c_addr[i], SENSOR_AIU_MODE_ADDR), HEX); + if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_INTADC) + objSensor["gpio"] = GPIOindex[config.system.sensor.gpio[i][0]].gpio; + + for(byte j = 0; j < Max_Sensors_Read; j++) { + if(SensorIndex[config.system.sensor.type[i]].read[j] > 0) { + + JsonObject objReading = objSensor["reading"].add<JsonObject>(); + + /* when for a RAW reading rawConvert is set, return the converted description and unit */ + if((SensorIndex[config.system.sensor.type[i]].read[j] == SENSOR_READ_TYPE_RAW) && (config.system.sensor.rawConvert[i][j] > 0)) { + objReading["descr"] = FPSTR(Sensor_Convert_Raw_descr[config.system.sensor.rawConvert[i][j]]); + objReading["unit"] = FPSTR(Sensor_Convert_Raw_unit[config.system.sensor.rawConvert[i][j]]); + } else { + objReading["descr"] = FPSTR(Sensor_Read_descr[SensorIndex[config.system.sensor.type[i]].read[j]]); + objReading["unit"] = FPSTR(Sensor_Read_unit[SensorIndex[config.system.sensor.type[i]].read[j]]); + } + + /* read RAW values + when internal ADC */ + if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_INTADC) { + objReading["raw"] = Sensor_getValue( config.system.sensor.type[i], config.system.sensor.gpio[i][0]); + } else if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_I2C) { + objReading["raw"] = Sensor_getValue( config.system.sensor.type[i], config.system.sensor.i2c_addr[i], j); + } + + objReading["value"] = Sensor_getCalibratedValue(i, j); + } + } + } + } + response->setLength(); + request->send(response); +} + + +void Api_sensor_data_raw(AsyncWebServerRequest* request) { + /* Api_sensor_data_raw returns the raw reading value of a specific reading of a sensor + * you can call it with GET http://<IP>/api/sensor/raw?sensor=1&reading=2*/ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot().to<JsonObject>(); + //root["hello"] = "world"; + + if((request->hasParam("sensor")) && (request->hasParam("reading"))) { + const AsyncWebParameter* paramSensor = request->getParam("sensor"); + byte sensorId = paramSensor->value().toInt(); + + const AsyncWebParameter* paramReading = request->getParam("reading"); + byte readingId = paramReading->value().toInt(); + + root["sensorId"] = sensorId; + root["readingId"] = readingId; + + /* when reading is RAW */ + if(SensorIndex[config.system.sensor.type[sensorId]].read[readingId] == SENSOR_READ_TYPE_RAW) { + /* when internal ADC */ + if(SensorIndex[config.system.sensor.type[sensorId]].type == SENSOR_TYPE_INTADC) { + root["value"] = Sensor_getValue( config.system.sensor.type[sensorId], config.system.sensor.gpio[sensorId][0]); + } else if(SensorIndex[config.system.sensor.type[sensorId]].type == SENSOR_TYPE_I2C) { + root["value"] = Sensor_getValue( config.system.sensor.type[sensorId], config.system.sensor.i2c_addr[sensorId], readingId); + } + } else { + root["msg"] = String(F("not a RAW reading")); + } + } else { + root["msg"] = String(F("sensor or reading not given")); + } + + response->setLength(); + request->send(response); +} + +void Api_sensor_driver(AsyncWebServerRequest* request) { + /* Api_sensor_data_raw returns the raw reading value of a specific reading of a sensor + * you can call it with GET http://<IP>/api/sensor/raw?sensor=1&reading=2*/ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot().to<JsonObject>(); + //root["hello"] = "world"; + + root["drivers"] = SensorIndex_length; + root["maxReadings"] = Max_Sensors_Read; + + /* empty driver because 0 is unconfigured */ + JsonObject objSensor = root["sensor"].add<JsonObject>(); + for(byte i = 1; i <= SensorIndex_length; i++) { + //Log.verbose(F("%s Sensor_Index %d, Name %s, Readings" CR), LogLoc, i, SensorIndex[i].name ); + JsonObject objSensor = root["sensor"].add<JsonObject>(); + objSensor["index"] = i; + objSensor["name"] = FPSTR(SensorIndex[i].name); + for(byte j = 0; j < Max_Sensors_Read; j++) { + if(SensorIndex[i].read[j] > 0 ) { + //Log.verbose(F("%s %d: %s (%d %d)" CR), LogLoc, j, Sensor_Read_descr[SensorIndex[i].read[j]], SensorIndex[i].read[j], Sensor_Read_unit[SensorIndex[i].read[j]], SensorIndex[i].read[j]); + JsonObject objReading = objSensor["reading"].add<JsonObject>(); + objReading["index"] = j; + objReading["descr"] = FPSTR(Sensor_Read_descr[SensorIndex[i].read[j]]); + objReading["unit"] = FPSTR(Sensor_Read_unit[SensorIndex[i].read[j]]); + } + } + } + + response->setLength(); + request->send(response); +} diff --git a/include/Webserver/File_cangrow_CSS.h b/include/Webserver/File_cangrow_CSS.h new file mode 100644 index 0000000..34a9a7c --- /dev/null +++ b/include/Webserver/File_cangrow_CSS.h @@ -0,0 +1,257 @@ +/* + * + * include/Webserver/File_cangrow_CSS.h - /cangrow.css header file + * + * + * + */ + + +const char File_cangrow_CSS[] PROGMEM = R"(body { + color: #cae0d0; + background-color: #1d211e; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + text-align: center; +} + +.footer { + color: #343B35; +} + +.center { + /*width: 100; */ + margin: auto; +} + +.centered { + margin-left: auto; + margin-right: auto; +} + +h1 { + margin: 15px; +} + +h2 { + margin: 10px; +} + +h3 { + margin: 5px; +} + +td { + text-align: left; + vertical-align: middle; + border-bottom: 1px solid #262B27; +} + +hr { + height: 1px; + border-width: 0; + color: #262B27; + background-color: #262B27; + margin-top: 0.5em; + margin-bottom: 0.5em; + margin-left: auto; + margin-right: auto; + border-style: inset; + width: 320px; +} + +a:link, a:visited { + color: #04AA6D; +} +a:hover { + color: #64AA6D; +} +a:active { + color: #04AA6D; +} +.infomsg , .warnmsg { + color: #fff; + border-radius: 3px; + padding: 4px; + /*width: fit-content; min-width: 200px; max-width: 420px;*/ + display: inline-block; + margin: auto; + margin-bottom: 5px; + font-weight: bold; + /*text-align: center;*/ + text-decoration: none; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.5); +} +.infomsg { + background: #04AA6D; +} +.warnmsg { + background: #aa4204; +} +.inputShort { + width: 42px; +} + +.sensorReading { + font-style: italic; + color: #64AA6D; +} + +.helpbox { + font-size: 0.8em; + margin-left: 15px; + margin-top: 5px; + margin-bottom: 5px; +} +.nav { + background: #333; + /*width: 100; */ + margin: auto; + margin-bottom: 10px; + padding: 0; + position: relative; + border-radius: 3px; + display: inline-block; + text-align: left; +} + +.subnav { + /*text-align: center;*/ + margin: auto; + margin-bottom: 10px; + padding: 0; + position: relative; + border-radius: 3px; +} + +.subnavTitle { + font-size: 1em; + /*font-weight: bold;*/ + margin-top: -10px; + margin-bottom: 10px; + /*text-align: center;*/ +} +.nav li { + display: inline-block; + list-style: none; + border-radius: 3px; +} + +.subnav li { + background: #262B27; + list-style: none; + border-radius: 3px; + margin-bottom: 3px; + display: inline-block; +} + +.nav li:first-of-type { + background: #026b45; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} +.nav li a, .nav span, .subnav li a, .subnav span, .button, .button:link, input[type=button], input[type=submit], +input[type=reset], .linkForm input[type=submit] { + color: #ddd; + display: block; + font-family: 'Lucida Sans Unicode', 'Lucida Grande', sans-serif; + font-size:0.8em; + padding: 10px 20px; + text-decoration: none; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.5); +} + +.subnav li a, .subnav span { + padding: 5px 10px; +} + +.nav li a:hover, .subnav li a:hover, .activeNav, .button:link:hover, .button:visited:hover, input[type=button]:hover, +input[type=submit]:hover, input[type=reset]:hover, .linkForm input[type=submit]:hover { + background: #04AA6D; + color: #fff; + border-radius: 3px; +} + +.nav li a:active, .subnav li a:active { + background: #026b45; + color: #cae0d0; +} + +.activeNav { + background: #444; +} + +.navTime { + background: #292929; +} + +.button, .button:link, .button:visited, input[type=button], input[type=submit],input[type=reset], +.linkForm input[type=submit] { + background: #026b45; + color: #fff; + border-radius: 3px; + padding: 6px 12px; + /*text-align: center;*/ + text-decoration: none; + display: inline-block; + border: none; +} + +.button:link:active, .button:visited:active, input[type=button]:active, input[type=submit]:active, +input[type=reset]:active, .linkForm input[type=submit]:active { + background: #026b45; + color: #cae0d0; +} + +input[type=text], input[type=date], input[type=number], input[type=password], select { + background: #cae0d0; + color: #1d211e; + border: 1px solid #026b45; + border-radius: 3px; +} + +.linkForm { + display: inline-block; +} + +.linkForm input[type=submit] { + background: #262B27; + padding: 5px; + +} + +.hidden { + display: none; +} + +.force_hide { + display: none !important; +} + +.visible { + display: inline; + /*justify-content: center!important;*/ +} +/* a disabled class */ +a.disabled { + pointer-events: none; +} + @media only screen and (min-width: 1820px) { + /*.center, .nav { + width: 60; min-width: 420px; + }*/ + .subnav li { + display: ''; + margin-bottom: 3px; + } +} + +/*@media only screen and (min-width: 640px) { + +}*/)"; + +void WebFile_cangrow_CSS(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), File_cangrow_CSS); + response->addHeader(F("Cache-control"), F("max-age=600")); + request->send(response); + //request->send_P(200, "text/css", File_cangrow_CSS); +} diff --git a/include/Webserver/File_cangrow_JS.h b/include/Webserver/File_cangrow_JS.h new file mode 100644 index 0000000..36375f7 --- /dev/null +++ b/include/Webserver/File_cangrow_JS.h @@ -0,0 +1,241 @@ +/* + * + * include/Webserver/File_cangrow_JS.h - /cangrow.js header file + * + * + * + */ + + +const char File_cangrow_JS[] PROGMEM = R"(function toggleDisplay(id) { + let el = document.getElementById(id); + let el_cs = getComputedStyle(el); + + if (el_cs.getPropertyValue('display') === 'none') { + el.style.display = 'inline'; + } else { + el.style.display = 'none'; + } +} + +function hideAllClass(classname) { + + const el = document.getElementsByClassName(classname); + + for(let i = 0; i < el.length ; i++) { + el[i].style.display = 'none'; + } +} + +function showSelect(selectId, prefix, hideClass = '') { + if(hideClass != '') { + hideAllClass(hideClass); + } + + let selVal = document.getElementById(selectId).value; + toggleId = prefix + selVal; + if(document.getElementById(toggleId) !== null ) { + toggleDisplay(toggleId); + } +} + +function confirmDelete(name) { + return confirm('Delete ' + name + '?'); +} + +function SystemOutputAddselectRequired(selectId) { + let selVal = document.getElementById(selectId).value; + //hideAllClass('hidden'); + console.log('selectReq Status: ' + selVal); + switch(selVal) { + case '1': + document.getElementById('gpio').required = true; + document.getElementById('gpio_pwm').required = true; + + document.getElementById('i2c_type').required = false; + document.getElementById('i2c_addr').required = false; + document.getElementById('i2c_port').required = false; + document.getElementById('webcall_host').required = false; + document.getElementById('webcall_path_on').required = false; + document.getElementById('webcall_path_off').required = false; + break; + + case '2': + document.getElementById('gpio').required = false; + document.getElementById('gpio_pwm').required = false; + + document.getElementById('i2c_type').required = true; + document.getElementById('i2c_addr').required = true; + document.getElementById('i2c_port').required = true; + document.getElementById('webcall_host').required = false; + document.getElementById('webcall_path_on').required = false; + document.getElementById('webcall_path_off').required = false; + break; + + case '3': + document.getElementById('gpio').required = false; + document.getElementById('gpio_pwm').required = false; + + document.getElementById('i2c_type').required = false; + document.getElementById('i2c_addr').required = false; + document.getElementById('i2c_port').required = false; + document.getElementById('webcall_host').required = true; + document.getElementById('webcall_path_on').required = true; + document.getElementById('webcall_path_off').required = true; + break; + + default: + break; + } +} + +// https://stackoverflow.com/a/67412019 +function SystemOutputAdd_replaceI2cAddr(selectId, replaceId) { + let sel = document.querySelector('#' + replaceId); + let selVal = document.getElementById(selectId).value; + // Remove existing options + Array.from(sel).forEach((option) => { + sel.removeChild(option) + }); + // get or set your new options here. + if(selVal) { + addr[selVal].map((optionData) => { + let opt = document.createElement('option'); + let PortsUsed = 0; + let label = optionData[0]; + opt.value = optionData[1]; + // iterate through i2c modules available ports + for(i = 0; i < optionData[2].length; i++) { + if(optionData[2][i] > 0) { + PortsUsed++; + } + } + if(PortsUsed >= optionData[2].length) { + opt.disabled = true; + label = label + ' (used)'; + } + opt.appendChild(document.createTextNode(label)); + sel.appendChild(opt); + }); + SystemOutputAdd_replaceI2cPort('i2c_type', 'i2c_addr', 'i2c_port'); + } + +} +////////////////////////////////////// +function SystemOutputAdd_replaceI2cPort(selectTypeId, selectAddrId, replaceId) { + let repl = document.querySelector('#' + replaceId); + let selValType = document.getElementById(selectTypeId).value; + let selValAddr = document.getElementById(selectAddrId).value; + // Remove existing options + Array.from(repl).forEach((option) => { + repl.removeChild(option) + }); + if(selValAddr) { + console.log('true'); + // iterate through i2c modules available ports + for(i = 0; i < addr[selValType][selValAddr][2].length; i++) { + let opt = document.createElement('option'); + let label = 'Port ' + i; + opt.value = i; + if(addr[selValType][selValAddr][2][i] > 0) { + label = label + ' (used)'; + opt.disabled = true; + } + opt.appendChild(document.createTextNode(label)); + repl.appendChild(opt); + console.log('PortID ' + i + ' Port sum ' + addr[selValType][selValAddr][2].length); + } + } else { + let opt = document.createElement('option'); + opt.appendChild(document.createTextNode('n/a')); + opt.disabled = true; + repl.appendChild(opt); + } +} +//javascript is my passion + + +function SystemSensorAddGpioI2cSel(selectId) { + let selVal = document.getElementById(selectId).value; + hideAllClass('hidden'); + if(selVal == 1) { + document.getElementById('type_1').style.display = 'inline'; + document.getElementById('i2c_addr').required = false; + if(ESP == '32') { + document.getElementById('gpio').required = true; + } + } else if(selVal > 1) { + document.getElementById('type_2').style.display = 'inline'; + document.getElementById('i2c_addr').required = true; + if(ESP == '32') { + document.getElementById('gpio').required = false; + } + } +} + +function convertDateToEpoch(src, dst) { + var val = document.getElementById(src).value ; + document.getElementById(dst).value = new Date(val).getTime() / 1000; +} + + +function GrowSelectControlSensorRead(selectId, inputSensor, inputRead) { + let selVal = document.getElementById(selectId).value; + let sensor = selVal.split(':')[0]; + let read = selVal.split(':')[1]; + document.getElementById(inputSensor).value = sensor; + document.getElementById(inputRead).value = read; +} + + + + +function GetSensorJson(callback) { + let path = '/api/sensor/'; + //let path = '/api/sensor/raw_' + sensor + '_' + reading; + var xobj = new XMLHttpRequest(); + xobj.overrideMimeType('application/json'); + xobj.open('GET', path, true); + xobj.onreadystatechange = function() { + if (xobj.readyState == 4 && xobj.status == "200") { + callback(xobj.responseText); + } + } + xobj.send(null); +} + + +/* propably not the best place, but this as global as it can get i guess */ +var SensorJson; +function SensorJsonRefresh() { + GetSensorJson(function(response) { + /* needs to be a global */ + SensorJson = JSON.parse(response); + }); + //console.log('Refresh SensorJson'); +} + + +function rawRefresh(sensor, reading, id) { + let element = id + sensor + '-' + reading; + document.getElementById(element).textContent = SensorJson.sensor[sensor].reading[reading].raw; + //console.log(SensorJson.sensor[sensor].reading[reading].raw); + //console.log('sensor:' + sensor + ';reading:' + reading + ';id:' + id + ';element:' + element); +} + + +function sensorRefresh(sensor, reading, id) { + let element = id + sensor + '-' + reading; + document.getElementById(element).textContent = SensorJson.sensor[sensor].reading[reading].value + ' ' + SensorJson.sensor[sensor].reading[reading].unit; + //console.log(SensorJson.sensor[sensor].reading[reading].value + SensorJson.sensor[sensor].reading[reading].unit); + //console.log('sensor:' + sensor + ';reading:' + reading + ';id:' + id + ';element:' + element); +} + +)"; + +void WebFile_cangrow_JS(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/javascript"), File_cangrow_JS); + response->addHeader(F("Cache-control"), F("max-age=600")); + request->send(response); + //request->send_P(200, "text/javascript", File_cangrow_JS); +} diff --git a/include/Webserver/File_favicon_ico.h b/include/Webserver/File_favicon_ico.h new file mode 100644 index 0000000..2b241cb --- /dev/null +++ b/include/Webserver/File_favicon_ico.h @@ -0,0 +1,38 @@ +unsigned char File_favicon_ico_gz[] = { + 0x1f, 0x8b, 0x08, 0x08, 0x11, 0x71, 0x19, 0x67, 0x00, 0x03, 0x43, 0x61, + 0x6e, 0x47, 0x72, 0x6f, 0x77, 0x5f, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6f, + 0x2e, 0x69, 0x63, 0x6f, 0x00, 0xed, 0x94, 0x49, 0x4b, 0xc3, 0x40, 0x18, + 0x86, 0xdf, 0xd8, 0xc5, 0xaa, 0xe9, 0x12, 0xa7, 0xcd, 0xd2, 0x26, 0x99, + 0x7c, 0x89, 0x76, 0x45, 0xb4, 0x2a, 0xb6, 0xa2, 0x42, 0xb1, 0x52, 0x73, + 0x11, 0xd4, 0x83, 0xdb, 0xc1, 0x8b, 0x08, 0x75, 0xf9, 0xff, 0x67, 0xbf, + 0x49, 0x3c, 0x58, 0xa4, 0x17, 0xc1, 0x5b, 0x9e, 0xe4, 0x1d, 0xe6, 0xf9, + 0x86, 0x61, 0x32, 0x03, 0x19, 0x40, 0xe3, 0xa7, 0x56, 0x03, 0xb7, 0x25, + 0xcc, 0x0b, 0x80, 0x09, 0xa0, 0xcb, 0xe1, 0x12, 0x02, 0xa4, 0xf5, 0x65, + 0x44, 0xed, 0x08, 0x51, 0x27, 0xc2, 0x56, 0x2f, 0xc2, 0x76, 0x9f, 0x33, + 0x88, 0xd0, 0xde, 0x09, 0xd1, 0xd9, 0x0b, 0xd1, 0x1d, 0x86, 0xe8, 0x1d, + 0x86, 0xe8, 0x8f, 0x08, 0x83, 0x31, 0x61, 0x77, 0x12, 0x60, 0x38, 0x0d, + 0xb0, 0x3f, 0x93, 0x38, 0x88, 0x25, 0x8e, 0xae, 0x7c, 0x8c, 0xae, 0x7d, + 0x8c, 0x6f, 0x3c, 0x8e, 0x8f, 0xe3, 0x5b, 0x0f, 0x27, 0x77, 0x2e, 0x4e, + 0xef, 0x39, 0x0f, 0x2e, 0xce, 0x1e, 0x39, 0x4f, 0x2d, 0x4c, 0x9e, 0x9b, + 0x38, 0x7f, 0x71, 0x30, 0xe5, 0x5c, 0xbc, 0xda, 0x98, 0xcd, 0x2d, 0xcc, + 0xde, 0x2c, 0x5c, 0xbe, 0x5b, 0x88, 0x3f, 0x4c, 0xc4, 0x9f, 0xe6, 0xd2, + 0xef, 0xcb, 0xc8, 0xc8, 0xf8, 0x7f, 0x7e, 0xfc, 0x81, 0x45, 0x61, 0xe4, + 0x57, 0xac, 0xbc, 0x21, 0x8a, 0xa9, 0x6b, 0x82, 0xa4, 0x0c, 0x24, 0x09, + 0x2d, 0x55, 0x43, 0x78, 0xc4, 0x78, 0xc2, 0x50, 0x85, 0x9c, 0x4d, 0x92, + 0x12, 0x24, 0xd9, 0x1b, 0x7c, 0xe9, 0x08, 0x51, 0x72, 0x13, 0x5f, 0xaf, + 0x8a, 0x4d, 0x35, 0x61, 0xd5, 0x4b, 0xc7, 0xfd, 0xef, 0x5b, 0xa8, 0x4e, + 0x14, 0x24, 0xaf, 0x9f, 0xe8, 0x5a, 0x40, 0x75, 0x87, 0xc8, 0xe1, 0xb2, + 0xae, 0xbc, 0x2c, 0x85, 0xa6, 0x9c, 0x97, 0x2d, 0x2b, 0x6f, 0x15, 0x34, + 0x28, 0x87, 0x56, 0x6d, 0x2a, 0xcf, 0x71, 0x2a, 0xca, 0xd3, 0x6e, 0x0a, + 0x51, 0x65, 0x61, 0x3b, 0x44, 0x8b, 0xdb, 0x6b, 0x34, 0x16, 0x5d, 0xd7, + 0xff, 0x74, 0x4a, 0xbf, 0xf9, 0x02, 0x31, 0x98, 0x4b, 0x6b, 0x7e, 0x05, + 0x00, 0x00 +}; +unsigned int File_favicon_ico_gz_len = 326; + +void WebFile_favicon_ico(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = request->beginResponse_P(200, F("image/x-icon"), File_favicon_ico_gz, File_favicon_ico_gz_len); + response->addHeader(F("Content-Encoding"), F("gzip")); + response->addHeader(F("Cache-control"), F("max-age=600")); + request->send(response); +} diff --git a/include/Webserver/Footer.h b/include/Webserver/Footer.h new file mode 100644 index 0000000..61ad019 --- /dev/null +++ b/include/Webserver/Footer.h @@ -0,0 +1,9 @@ +/* + * + * include/Webserver/footer_HTML.h - footer page HTML header file + * + * + * + */ + +const char Footer_HTML[] PROGMEM = R"(<div class='footer'><span>Build: %CGBUILD%</span></div></div></body></html>)"; diff --git a/include/Webserver/Header.h b/include/Webserver/Header.h new file mode 100644 index 0000000..083dd81 --- /dev/null +++ b/include/Webserver/Header.h @@ -0,0 +1,28 @@ +/* + * + * include/Webserver/header_HTML.h - header page HTML header file + * + * + * + */ + +const char Header_HTML[] PROGMEM = R"(<!DOCTYPE html> +<html> + <head> + <meta charset='UTF-8'> + <meta name='viewport' content='width=device-width, initial-scale=1.0'> + <title>%GROWNAME% - CanGrow v%CGVER%</title> + <link rel='stylesheet' href='/cangrow.css'> + <script type='text/javascript' src='/cangrow.js'></script> +</head> +<body> + <ul class='nav'><li><a href='/'>🌱 %GROWNAME%</a></li> + <li><a class='%ACTIVE_NAV_GROW%' href='/grow/' >🔆 Grow settings</a></li> + <li><a class='%ACTIVE_NAV_SYSTEM%' href='/system/' >⚙ System settings</a></li> + <li><a class='%ACTIVE_NAV_WIFI%' href='/wifi/' >📡 WiFi settings</a></li> + <li><a class='%ACTIVE_NAV_HELP%' href='/help' >❓ Help</a></li> + <li><span class='navTime'>%TIME%</span></li> + <li><a href='https://git.la10cy.net/DeltaLima/CanGrow' target='_blank'>CanGrow v%CGVER%</a></li> + </ul> + <div class='center'> + %NEED_RESTART%)"; diff --git a/include/Webserver/Page_404.h b/include/Webserver/Page_404.h new file mode 100644 index 0000000..c04c856 --- /dev/null +++ b/include/Webserver/Page_404.h @@ -0,0 +1,26 @@ +/* + * 404 error page begins + */ + +// 404 page is a good page template btw +const char Page_404_HTML[] PROGMEM = R"EOF(%HEADER% +<div class='warnmsg'><h1>❗ ️ 404 - not found</h1></div> +%FOOTER% )EOF"; + +/* processor */ +String Proc_WebPage_404(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var); + } else { + return String(); + } +} + +// https://github.com/mathieucarbou/ESPAsyncWebServer/blob/main/examples/SimpleServer/SimpleServer.ino +void WebserverNotFound(AsyncWebServerRequest* request) { + request->send_P(404, TEXT_HTML, Page_404_HTML, Proc_WebPage_404); +} + +/* + * 404 error page ends + */ diff --git a/include/Webserver/Page_grow.h b/include/Webserver/Page_grow.h new file mode 100644 index 0000000..a329c9a --- /dev/null +++ b/include/Webserver/Page_grow.h @@ -0,0 +1,1000 @@ +/* + * + * include/Webserver/Page_grow.h - grow page header file + * + * + * + */ + +#include "Page_grow_HTML.h" + + +/* subnav processor */ +const byte WEB_GROW_SUBNAV_GENERAL = 1; +const byte WEB_GROW_SUBNAV_LIGHT = 2; +const byte WEB_GROW_SUBNAV_AIR = 3; +const byte WEB_GROW_SUBNAV_WATER = 4; +const byte WEB_GROW_SUBNAV_DASHBOARD = 5; + +bool Test_WebPage_grow_SUBNAV(const String& var) { + if( + (var == "SUBNAV") || + (var == "ACTIVE_SUBNAV_GENERAL") || + (var == "ACTIVE_SUBNAV_LIGHT") || + (var == "ACTIVE_SUBNAV_AIR") || + (var == "ACTIVE_SUBNAV_WATER") || + (var == "ACTIVE_SUBNAV_DASHBOARD")) { + return true; + } else { + return false; + } +} + +/* + * Proc_WebPage_grow_SUBNAV - subnav processor for grow + * this function works as same as AddHeaderFooter from Common.h + * byte activeSubnav: + * 1 - Output + * 2 - Update + * 3 - Restart + * 4 - Wipe + */ +String Proc_WebPage_grow_SUBNAV(const String& var, byte activeSubnav = 0) { + String activeSubnav_ClassName = "activeNav"; + if(var == "SUBNAV") { + return String(Page_grow_HTML_SUBNAV); + } else if((var == "ACTIVE_SUBNAV_GENERAL") && (activeSubnav == WEB_GROW_SUBNAV_GENERAL)) { + return activeSubnav_ClassName; + } else if((var == "ACTIVE_SUBNAV_LIGHT") && (activeSubnav == WEB_GROW_SUBNAV_LIGHT)) { + return activeSubnav_ClassName; + } else if((var == "ACTIVE_SUBNAV_AIR") && (activeSubnav == WEB_GROW_SUBNAV_AIR)) { + return activeSubnav_ClassName; + } else if((var == "ACTIVE_SUBNAV_WATER") && (activeSubnav == WEB_GROW_SUBNAV_WATER)) { + return activeSubnav_ClassName; + } else if((var == "ACTIVE_SUBNAV_DASHBOARD") && (activeSubnav == WEB_GROW_SUBNAV_DASHBOARD)) { + return activeSubnav_ClassName; + } else { + return String(); + } +} + +/******************************************************************************* + * Main grow page + */ +// https://techtutorialsx.com/2018/07/23/esp32-arduino-http-server-template-processing-with-multiple-placeholders/ +String Proc_WebPage_grow(const String& var) { + const static char LogLoc[] PROGMEM = "[Webserver:grow(Proc)]"; + /* This is a processor function, which returns a string. + * We check if var contains one of our placeholders from the template. + * If we hit a placeholder, we just return the String we want. + * + * TestHeaderFooter() Is kinda a processor too, but only checks for + * header specific placeholders. + */ + + //Log.verbose(F("%s var: %s" CR), LogLoc, var); + + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 1); + } else if(Test_WebPage_grow_SUBNAV(var)) { + return Proc_WebPage_grow_SUBNAV(var, WEB_GROW_SUBNAV_GENERAL); + } else if(var == "GROWNAME") { + return String(config.grow.name); + } else if(var == "GROWSTART_EPOCH") { + /* check if there is an reasonable grow start time */ + if(config.grow.start > 1711922400) { + return String(config.grow.start); + } else { + return String(); //String(now()); + } + } else if(var == "GROWSTART") { + if(config.grow.start > 1711922400) { + return Str_Epoch2Date(config.grow.start); + } else { + return String(); //Str_Epoch2Date(now()); + } + } else if(var == "DAYS_VEG") { + return String(config.grow.daysVeg); + } else if(var == "DAYS_BLOOM") { + return String(config.grow.daysBloom); + } else { + return String(); + } +} + +String Proc_WebPage_grow_POST(const String& var) { + /* This is the processor for POST + * Its exactly the same, just looking for SAVE_MSG string. + * If nothing matches, it calles the main Proc_WebPage_grow() + * processor function, so all the other stuff like header and so + * on get replaced + */ + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG); + } else { + return Proc_WebPage_grow(var); + } +} + +String Proc_WebPage_grow_POST_ERR(const String& var) { + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG_ERR); + } else { + return Proc_WebPage_grow(var); + } +} + +/* WebPage function */ +void WebPage_grow(AsyncWebServerRequest *request) { + const static char LogLoc[] PROGMEM = "[Webserver:grow]"; + + + /* Which kind of Request */ + if(request->method() == HTTP_POST) { + + if(request->hasParam("name", true)) { + const AsyncWebParameter* param = request->getParam("name", true); + strlcpy(config.grow.name, param->value().c_str(), sizeof(config.grow.name)); + } + + if(request->hasParam("start", true)) { + const AsyncWebParameter* param = request->getParam("start", true); + config.grow.start = param->value().toInt(); + } + + if(request->hasParam("daysVeg", true)) { + const AsyncWebParameter* param = request->getParam("daysVeg", true); + config.grow.daysVeg = param->value().toInt(); + } + + if(request->hasParam("daysBloom", true)) { + const AsyncWebParameter* param = request->getParam("daysBloom", true); + config.grow.daysBloom = param->value().toInt(); + } + + + if(SaveConfig()) { + // we need a restart to apply the new settings + + Log.notice(F("%s config saved" CR), LogLoc); + + request->send_P(200, "text/html", Page_grow_HTML, Proc_WebPage_grow_POST); + } else { + Log.error(F("%s ERROR while saving config" CR), LogLoc); + request->send_P(200, TEXT_HTML, Page_grow_HTML, Proc_WebPage_grow_POST_ERR); + } + } else { + request->send_P(200, TEXT_HTML, Page_grow_HTML, Proc_WebPage_grow); + } +} + + + + + + + + +/******************************************************************************* + * grow light page + */ +String Proc_WebPage_grow_light(const String& var) { + const static char LogLoc[] PROGMEM = "[Webserver:grow:light(Proc)]"; + + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 1); + } else if(Test_WebPage_grow_SUBNAV(var)) { + return Proc_WebPage_grow_SUBNAV(var, WEB_GROW_SUBNAV_LIGHT); + } else if(var == "LIGHT") { + String html; + for(byte i = 0; i < Max_Outputs; i++) { + if(config.grow.light.configured[i] == true) { + /* form */ + html += F("<form method='post' action='/grow/light/'>"); + /* OutputId */ + html += F("<input type='hidden' name='output' value='"); + html += i; + html += F("' required>"); + + + html += F("<h2>"); + html += config.system.output.name[i]; + html += F("</h2>"); + + /* Device */ + html += F("<u>Device:</u><br><b>"); + html += FPSTR(Output_Device_descr[config.system.output.device[i]]); + html += F(" ("); + html += FPSTR(Output_Type_descr[config.system.output.type[i]]); + html += F(")</b><br>"); + + /* Sunrise */ + html += F("<u>Sunrise</u><br><b>Vegetation </b><input class='inputShort' type='number' name='sunriseHourVeg' min='0' max='23' value='"); + html += config.grow.light.sunriseHourVeg[i]; + html += F("' required><b>:</b> <input class='inputShort' type='number' name='sunriseMinuteVeg' min='0' max='59' value='"); + html += config.grow.light.sunriseMinuteVeg[i]; + html += F("' required><br>"); + + /* when bloon days is set to 0, disable it */ + if(config.grow.daysBloom > 0) { + html += F("<b>Bloom </b><input class='inputShort' type='number' name='sunriseHourBloom' min='0' max='23' value='"); + html += config.grow.light.sunriseHourBloom[i]; + html += F("' required><b>:</b> <input class='inputShort' type='number' name='sunriseMinuteBloom' min='0' max='59' value='"); + html += config.grow.light.sunriseMinuteBloom[i]; + html += F("' required><br>"); + } + + /* Sunset */ + html += F("<u>Sunset</u><br><b>Vegetation </b><input class='inputShort' type='number' name='sunsetHourVeg' min='0' max='23' value='"); + html += config.grow.light.sunsetHourVeg[i]; + html += F("' required><b>:</b> <input class='inputShort' type='number' name='sunsetMinuteVeg' min='0' max='59' value='"); + html += config.grow.light.sunsetMinuteVeg[i]; + html += F("' required><br>"); + /* when bloon days is set to 0, disable it */ + if(config.grow.daysBloom > 0) { + html += F("<b>Bloom </b><input class='inputShort' type='number' name='sunsetHourBloom' min='0' max='23' value='"); + html += config.grow.light.sunsetHourBloom[i]; + html += F("' required><b>:</b> <input class='inputShort' type='number' name='sunsetMinuteBloom' min='0' max='59' value='"); + html += config.grow.light.sunsetMinuteBloom[i]; + html += F("' required><br>"); + } + + /* power */ + /* if no pwm, show simple bool select */ + if(Output_Check_PWM(i) == true) { + html += F("<u>Power</u><br><input type='range' name='power' min='0' max='255' value='"); + html += config.grow.light.power[i]; + html += F("'/> %<br>"); + + /* fade */ + html += F("<u>Fade sunset/sunrise</u><br><select name='fade' required>"); + html += Html_SelectOpt_bool(config.grow.light.fade[i]); + html += F("'/></select><br>"); + + /* fade duration */ + html += F("<u>Fade duration</u><br><input class='inputShort' type='number' name='fadeDuration' min='1' max='255' value='"); + html += config.grow.light.fadeDuration[i]; + html += F("' required> Minutes<br>"); + } else { + html += F("<u>Power</u><br><select name='power' required>"); + html += Html_SelectOpt_bool(config.grow.light.power[i], "On", "Off"); + html += F("'/></select><br>"); + } + + /* submit button */ + html += F("<input type='submit' value='💾 Save settings' style='margin-top: 8px;'></form>"); + + /* HR HORIZONTAL LINE TO SIGNAL END */ + html += F("<hr>"); + + + } + } + + return html; + } else { + return String(); + } +} + +String Proc_WebPage_grow_light_POST(const String& var) { + /* This is the processor for POST + * Its exactly the same, just looking for SAVE_MSG string. + * If nothing matches, it calles the main Proc_WebPage_grow() + * processor function, so all the other stuff like header and so + * on get replaced + */ + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG); + } else { + return Proc_WebPage_grow_light(var); + } +} + +/* WebPage function */ +void WebPage_grow_light(AsyncWebServerRequest *request) { + const static char LogLoc[] PROGMEM = "[Webserver:grow:light]"; + + + /* Which kind of Request */ + if(request->method() == HTTP_POST) { + byte OutputId; + if(request->hasParam("output", true)) { + const AsyncWebParameter* param = request->getParam("output", true); + OutputId = param->value().toInt(); + + config.grow.light.output[OutputId] = param->value().toInt(); + } + + if(request->hasParam("sunriseHourVeg", true)) { + const AsyncWebParameter* param = request->getParam("sunriseHourVeg", true); + config.grow.light.sunriseHourVeg[OutputId] = param->value().toInt(); + } + + if(request->hasParam("sunriseMinuteVeg", true)) { + const AsyncWebParameter* param = request->getParam("sunriseMinuteVeg", true); + config.grow.light.sunriseMinuteVeg[OutputId] = param->value().toInt(); + } + + if(request->hasParam("sunriseHourBloom", true)) { + const AsyncWebParameter* param = request->getParam("sunriseHourBloom", true); + config.grow.light.sunriseHourBloom[OutputId] = param->value().toInt(); + } + + if(request->hasParam("sunriseMinuteBloom", true)) { + const AsyncWebParameter* param = request->getParam("sunriseMinuteBloom", true); + config.grow.light.sunriseMinuteBloom[OutputId] = param->value().toInt(); + } + + + if(request->hasParam("sunsetHourVeg", true)) { + const AsyncWebParameter* param = request->getParam("sunsetHourVeg", true); + config.grow.light.sunsetHourVeg[OutputId] = param->value().toInt(); + } + + if(request->hasParam("sunsetMinuteVeg", true)) { + const AsyncWebParameter* param = request->getParam("sunsetMinuteVeg", true); + config.grow.light.sunsetMinuteVeg[OutputId] = param->value().toInt(); + } + + if(request->hasParam("sunsetHourBloom", true)) { + const AsyncWebParameter* param = request->getParam("sunsetHourBloom", true); + config.grow.light.sunsetHourBloom[OutputId] = param->value().toInt(); + } + + if(request->hasParam("sunsetMinuteBloom", true)) { + const AsyncWebParameter* param = request->getParam("sunsetMinuteBloom", true); + config.grow.light.sunsetMinuteBloom[OutputId] = param->value().toInt(); + } + + + + + + if(request->hasParam("power", true)) { + const AsyncWebParameter* param = request->getParam("power", true); + config.grow.light.power[OutputId] = param->value().toInt(); + } + + if(request->hasParam("fade", true)) { + const AsyncWebParameter* param = request->getParam("fade", true); + config.grow.light.fade[OutputId] = param->value().toInt(); + } + + if(request->hasParam("fadeDuration", true)) { + const AsyncWebParameter* param = request->getParam("fadeDuration", true); + config.grow.light.fadeDuration[OutputId] = param->value().toInt(); + } + + SaveConfig(); + + Log.notice(F("%s config saved" CR), LogLoc); + + request->send_P(200, TEXT_HTML, Page_grow_light_HTML, Proc_WebPage_grow_light_POST); + + } else { + request->send_P(200, TEXT_HTML, Page_grow_light_HTML, Proc_WebPage_grow_light); + } +} + + + + + + + + + + + + + + + + + + +/******************************************************************************* + * grow air page + */ +String Proc_WebPage_grow_air(const String& var) { + const static char LogLoc[] PROGMEM = "[Webserver:grow:air(Proc)]"; + + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 1); + } else if(Test_WebPage_grow_SUBNAV(var)) { + return Proc_WebPage_grow_SUBNAV(var, WEB_GROW_SUBNAV_AIR); + } else if(var == "AIR") { + String html; + + for(byte i = 0; i < Max_Outputs; i++) { + if(config.grow.air.configured[i] == true) { + /* form */ + html += F("<form method='post' action='/grow/air/'>"); + /* OutputId */ + html += F("<input type='hidden' name='output' value='"); + html += i; + html += F("' required>"); + + + html += F("<h2>"); + html += config.system.output.name[i]; + html += F("</h2>"); + + /* Device */ + html += F("<u>Device:</u><br><b>"); + html += FPSTR(Output_Device_descr[config.system.output.device[i]]); + html += F(" ("); + html += FPSTR(Output_Type_descr[config.system.output.type[i]]); + html += F(")</b><br>"); + + + /* speed / power */ + /* if no pwm, show simple bool select */ + if(Output_Check_PWM(i) == true) { + /* <input type='range' id='PinLEDPWM' name='PinLEDPWM' min='0' max='255' value='255'/> %<br> */ + if(config.system.output.device[i] == OUTPUT_DEVICE_FAN) { + html += F("<u>Speed</u>"); + } else { + html += F("<u>Power</u>"); + } + html += F("<br><input type='range' name='power' min='0' max='255' value='"); + html += config.grow.air.power[i]; + html += F("'/> %<br>"); + } else { + html += F("<u>Power</u><br><select name='power' required>"); + //<input type='range' name='power' min='0' max='100' value='"); + html += Html_SelectOpt_bool(config.grow.air.power[i], "On", "Off"); + html += F("'/></select><br>"); + } + + + /* hidden inputs for SensorId and ReadId*/ + html += F("<input type='hidden' name='controlSensor' value='"); + html += config.grow.air.controlSensor[i]; + html += F("' id='controlSensor"); + html += i; + html += F("'>"); + + html += F("<input type='hidden' name='controlRead' value='"); + html += config.grow.air.controlRead[i]; + html += F("' id='controlRead"); + html += i; + html += F("'>"); + /* controledBy */ + html += F("<u>Controled by</u><br><select name='controlBy' id='ctrl"); + html += i; + html += F("'onChange=\"GrowSelectControlSensorRead('ctrl"); + html += i; + html += F("', 'controlSensor"); + html += i; + html += F("', 'controlRead"); + html += i; + html += F("');\"><option value='255:255' selected >---</option>"); + + /* iterate through available sensors and offer useful values */ + byte count = 0; + for(byte j = 0; j < Max_Sensors; j++) { + /* if sensor is configured */ + if(config.system.sensor.type[j] > 0) { + /* we want to offer humidity, temperature, gas resistance */ + for(byte k = 0; k < Max_Sensors_Read; k++) { + if(SensorIndex[config.system.sensor.type[j]].read[k] > 0) { + if((SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_TEMP) || + (SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_HUMIDITY) || + (SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_GAS_RESISTANCE)) { + + html += F("<option value='"); + + /* put SensorId and ReadId into one colon sperated string. This we seperate later within javascript */ + html += j; // SensorId + html += F(":"); + html += k; // ReadId + + html += F("'"); + if((config.grow.air.controlSensor[i] == j) && (config.grow.air.controlRead[i] == k)) + html += F(" selected"); + html += F(">"); + html += config.system.sensor.name[j]; + html += F(" - "); + html += FPSTR(Sensor_Read_descr[SensorIndex[config.system.sensor.type[j]].read[k]]); + html += F(" ("); + html += Sensor_getCalibratedValue(j, k); + html += F(" "); + /* put unit into string */ + String unit = FPSTR(Sensor_Read_unit[SensorIndex[config.system.sensor.type[j]].read[k]]); + /* to be able to replace % sign, which is already used by ESPAsyncWebserver's template engine + * with html code for it */ + html += F(" "); + unit.replace(F("%"), F("%")); + html += unit; + html += F(")</option>"); + count++; + } + } + } + } + } + html += F("</select><br>"); + + /* controlMode */ + html += F("<u>Control mode</u><br><select name='controlMode' ><option value='' selected>---</option>"); + //<input type='range' name='power' min='0' max='100' value='"); + html += Html_SelectOpt_array(CONTROL_AIR_MODE__TOTAL, Control_Air_Mode_descr, config.grow.air.controlMode[i]); + html += F("'/> %</select><br>"); + + + /* min */ + html += F("<u>Min</u><br><input type='number' name='min' step='0.1' value='"); + html += config.grow.air.min[i]; + html += F("' required><br>"); + + /* max */ + html += F("<u>Max</u><br><input type='number' name='max' step='0.1' value='"); + html += config.grow.air.max[i]; + html += F("' required><br>"); + + + /* submit button */ + html += F("<input type='submit' value='💾 Save settings' style='margin-top: 8px;'></form>"); + + /* HR HORIZONTAL LINE TO SIGNAL END */ + html += F("<hr>"); + + + } + } + + return html; + } else { + return String(); + } +} + +String Proc_WebPage_grow_air_POST(const String& var) { + /* This is the processor for POST + * Its exactly the same, just looking for SAVE_MSG string. + * If nothing matches, it calles the main Proc_WebPage_grow() + * processor function, so all the other stuff like header and so + * on get replaced + */ + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG); + } else { + return Proc_WebPage_grow_air(var); + } +} + +/* WebPage function */ +void WebPage_grow_air(AsyncWebServerRequest *request) { + const static char LogLoc[] PROGMEM = "[Webserver:grow:air]"; + + + /* Which kind of Request */ + if(request->method() == HTTP_POST) { + + byte OutputId; + if(request->hasParam("output", true)) { + const AsyncWebParameter* param = request->getParam("output", true); + OutputId = param->value().toInt(); + config.grow.air.output[OutputId] = param->value().toInt(); + } + + if(request->hasParam("power", true)) { + const AsyncWebParameter* param = request->getParam("power", true); + config.grow.air.power[OutputId] = param->value().toInt(); + } + + + if(request->hasParam("controlSensor", true)) { + const AsyncWebParameter* param = request->getParam("controlSensor", true); + config.grow.air.controlSensor[OutputId] = param->value().toInt(); + } + + if(request->hasParam("controlRead", true)) { + const AsyncWebParameter* param = request->getParam("controlRead", true); + config.grow.air.controlRead[OutputId] = param->value().toInt(); + } + + //byte controlBy; + //if(request->hasParam("controlBy", true)) { + //const AsyncWebParameter* param = request->getParam("controlBy", true); + //controlBy = param->value().toInt(); + //} + + if(request->hasParam("controlMode", true)) { + const AsyncWebParameter* param = request->getParam("controlMode", true); + config.grow.air.controlMode[OutputId] = param->value().toInt(); + } + + if(request->hasParam("min", true)) { + const AsyncWebParameter* param = request->getParam("min", true); + config.grow.air.min[OutputId] = param->value().toFloat(); + } + + if(request->hasParam("max", true)) { + const AsyncWebParameter* param = request->getParam("max", true); + config.grow.air.max[OutputId] = param->value().toFloat(); + } + + + + SaveConfig(); + + Log.notice(F("%s config saved" CR), LogLoc); + + request->send_P(200, TEXT_HTML, Page_grow_air_HTML, Proc_WebPage_grow_air_POST); + + } else { + request->send_P(200, TEXT_HTML, Page_grow_air_HTML, Proc_WebPage_grow_air); + } +} + + + + + + + + + + + + + + + + + +/******************************************************************************* + * grow water page + */ +String Proc_WebPage_grow_water(const String& var) { + const static char LogLoc[] PROGMEM = "[Webserver:grow:water(Proc)]"; + + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 1); + } else if(Test_WebPage_grow_SUBNAV(var)) { + return Proc_WebPage_grow_SUBNAV(var, WEB_GROW_SUBNAV_WATER); + } else if(var == "WATER") { + String html; + + for(byte i = 0; i < Max_Outputs; i++) { + if(config.grow.water.configured[i] == true) { + /* form */ + html += F("<form method='post' action='/grow/water/'>"); + /* OutputId */ + html += F("<input type='hidden' name='output' value='"); + html += i; + html += F("' required>"); + + + html += F("<h2>"); + html += config.system.output.name[i]; + html += F("</h2>"); + + /* Device */ + html += F("<u>Device:</u><br><b>"); + html += FPSTR(Output_Device_descr[config.system.output.device[i]]); + html += F(" ("); + html += FPSTR(Output_Type_descr[config.system.output.type[i]]); + html += F(")</b><br>"); + + + /* + * struct Config_Grow_Water { + bool configured[Max_Outputs]; + byte output[Max_Outputs]; + byte controlSensor[Max_Outputs]; + byte controlRead[Max_Outputs]; + byte controlMode[Max_Outputs]; + byte pumpOn[Max_Sensors]; + byte min[Max_Sensors]; + byte max[Max_Sensors]; + byte interval[Max_Sensors]; + byte intervalUnit[Max_Sensors]; + }; + */ + + /* min */ + html += F("<u>Pump On</u><br><input class='inputShort' type='number' name='onTime' min='0' max='255' value='"); + html += config.grow.water.onTime[i]; + html += F("' required> Seconds<br>"); + + /* hidden inputs for SensorId and ReadId*/ + html += F("<input type='hidden' name='controlSensor' value='"); + html += config.grow.water.controlSensor[i]; + html += F("' id='controlSensor"); + html += i; + html += F("'>"); + + html += F("<input type='hidden' name='controlRead' value='"); + html += config.grow.water.controlRead[i]; + html += F("' id='controlRead"); + html += i; + html += F("'>"); + + + /* controlMode */ + html += F("<u>Control mode</u><br><select name='controlMode'><option value='' selected>---</option>"); + //<input type='range' name='power' min='0' max='100' value='"); + html += Html_SelectOpt_array(CONTROL_WATER_MODE__TOTAL, Control_Water_Mode_descr, config.grow.water.controlMode[i]); + html += F("</select><br>"); + + /* interval */ + html += F("<u>Interval</u><br><input class='inputShort' type='number' name='interval' min='0' max='255' value='"); + html += config.grow.water.interval[i]; + html += F("' required> "); + + /* intervalUnit */ + html += F("<select name='intervalUnit'>"); + //<input type='range' name='power' min='0' max='100' value='"); + //html += Html_SelectOpt_bool(config.grow.water.intervalUnit[i]); + + /* iterate through time scale units */ + for(byte j = 0; j <= TIMESCALE_WEEK; j++) { + html += F("<option value='"); + html += j; + html += F("'"); + if(config.grow.water.intervalUnit[i] == j) + html += F(" selected"); + html += F(">"); + html += FPSTR(Timescale_descr[j]); + html += F("</option>"); + } + + html += F("</select><br>"); + + + /* controledBy */ + html += F("<u>Controled by</u><br><select name='controlBy' id='ctrl"); + html += i; + html += F("'onChange=\"GrowSelectControlSensorRead('ctrl"); + html += i; + html += F("', 'controlSensor"); + html += i; + html += F("', 'controlRead"); + html += i; + html += F("');\"><option value='255:255' selected >---</option>"); + + /* iterate through available sensors and offer useful values */ + byte count = 0; + for(byte j = 0; j < Max_Sensors; j++) { + /* if sensor is configured */ + if(config.system.sensor.type[j] > 0) { + /* we want to offer humidity, temperature, gas resistance */ + for(byte k = 0; k < Max_Sensors_Read; k++) { + if(SensorIndex[config.system.sensor.type[j]].read[k] > 0) { + if((SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_SOILMOISTURE) || ((SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_RAW) && (config.system.sensor.rawConvert[j][k] == SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE))) { + + html += F("<option value='"); + + /* put SensorId and ReadId into one colon sperated string. This we seperate later within javascript */ + html += j; // SensorId + html += F(":"); + html += k; // ReadId + + html += F("'"); + if((config.grow.water.controlSensor[i] == j) && (config.grow.water.controlRead[i] == k)) + html += F(" selected"); + html += F(">"); + html += config.system.sensor.name[j]; + html += F(" - ("); + html += k; + html += F(") "); + + if((SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_RAW) && (config.system.sensor.rawConvert[j][k] == SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE)) { + html += FPSTR(Sensor_Convert_Raw_descr[config.system.sensor.rawConvert[j][k]]); + } else { + html += FPSTR(Sensor_Read_descr[SensorIndex[config.system.sensor.type[j]].read[k]]); + } + + html += F(" ("); + html += Sensor_getCalibratedValue(j, k); + html += F(" "); + /* put unit into string */ + String unit; + if((SensorIndex[config.system.sensor.type[j]].read[k] == SENSOR_READ_TYPE_RAW) && (config.system.sensor.rawConvert[j][k] == SENSOR_CONVERT_RAW_TYPE_SOILMOISTURE)) { + unit = FPSTR(Sensor_Convert_Raw_unit[config.system.sensor.rawConvert[j][k]]); + } else { + unit = FPSTR(Sensor_Read_unit[SensorIndex[config.system.sensor.type[j]].read[k]]); + } + /* to be able to replace % sign, which is already used by ESPAsyncWebserver's template engine + * with html code for it */ + html += F(" "); + unit.replace(F("%"), F("%")); + html += unit; + html += F(")</option>"); + count++; + } + } + } + } + } + html += F("</select><br>"); + + + /* min */ + html += F("<u>Min</u><br><input class='inputShort' type='number' name='min' min='0' max='255' value='"); + html += config.grow.water.min[i]; + html += F("' required><br>"); + + /* max */ + html += F("<u>Max</u><br><input class='inputShort' type='number' name='max' min='0' max='255' value='"); + html += config.grow.water.max[i]; + html += F("' required><br>"); + + + + /* submit button */ + html += F("<input type='submit' value='💾 Save settings' style='margin-top: 8px;'></form>"); + + /* HR HORIZONTAL LINE TO SIGNAL END */ + html += F("<hr>"); + + + } + } + + return html; + } else { + return String(); + } +} + +String Proc_WebPage_grow_water_POST(const String& var) { + /* This is the processor for POST + * Its exactly the same, just looking for SAVE_MSG string. + * If nothing matches, it calles the main Proc_WebPage_grow() + * processor function, so all the other stuff like header and so + * on get replaced + */ + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG); + } else { + return Proc_WebPage_grow_water(var); + } +} + +/* WebPage function */ +void WebPage_grow_water(AsyncWebServerRequest *request) { + const static char LogLoc[] PROGMEM = "[Webserver:grow:water]"; + + + /* Which kind of Request */ + if(request->method() == HTTP_POST) { + + byte OutputId; + if(request->hasParam("output", true)) { + const AsyncWebParameter* param = request->getParam("output", true); + OutputId = param->value().toInt(); + config.grow.water.output[OutputId] = param->value().toInt(); + } + + if(request->hasParam("onTime", true)) { + const AsyncWebParameter* param = request->getParam("onTime", true); + config.grow.water.onTime[OutputId] = param->value().toInt(); + } + + + if(request->hasParam("controlSensor", true)) { + const AsyncWebParameter* param = request->getParam("controlSensor", true); + config.grow.water.controlSensor[OutputId] = param->value().toInt(); + } + + if(request->hasParam("controlRead", true)) { + const AsyncWebParameter* param = request->getParam("controlRead", true); + config.grow.water.controlRead[OutputId] = param->value().toInt(); + } + + + if(request->hasParam("controlMode", true)) { + const AsyncWebParameter* param = request->getParam("controlMode", true); + config.grow.water.controlMode[OutputId] = param->value().toInt(); + } + + if(request->hasParam("min", true)) { + const AsyncWebParameter* param = request->getParam("min", true); + config.grow.water.min[OutputId] = param->value().toInt(); + } + + if(request->hasParam("max", true)) { + const AsyncWebParameter* param = request->getParam("max", true); + config.grow.water.max[OutputId] = param->value().toInt(); + } + + if(request->hasParam("interval", true)) { + const AsyncWebParameter* param = request->getParam("interval", true); + config.grow.water.interval[OutputId] = param->value().toInt(); + } + + if(request->hasParam("intervalUnit", true)) { + const AsyncWebParameter* param = request->getParam("intervalUnit", true); + config.grow.water.intervalUnit[OutputId] = param->value().toInt(); + } + + SaveConfig(); + + Log.notice(F("%s config saved" CR), LogLoc); + + request->send_P(200, TEXT_HTML, Page_grow_water_HTML, Proc_WebPage_grow_water_POST); + + } else { + request->send_P(200, TEXT_HTML, Page_grow_water_HTML, Proc_WebPage_grow_water); + } +} + + + + + + + + + + + + + + + + + + + + + + + + +/******************************************************************************* + * grow dashboards page + */ +String Proc_WebPage_grow_dashboard(const String& var) { + const static char LogLoc[] PROGMEM = "[Webserver:grow:dashboard(Proc)]"; + + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 1); + } else if(Test_WebPage_grow_SUBNAV(var)) { + return Proc_WebPage_grow_SUBNAV(var, WEB_GROW_SUBNAV_DASHBOARD); + } else if(var == "DASHBOARD") { + String html; + return html; + } else { + return String(); + } +} + +String Proc_WebPage_grow_dashboard_POST(const String& var) { + /* This is the processor for POST + * Its exactly the same, just looking for SAVE_MSG string. + * If nothing matches, it calles the main Proc_WebPage_grow() + * processor function, so all the other stuff like header and so + * on get replaced + */ + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG); + } else { + return Proc_WebPage_grow_dashboard(var); + } +} + +/* WebPage function */ +void WebPage_grow_dashboard(AsyncWebServerRequest *request) { + const static char LogLoc[] PROGMEM = "[Webserver:grow:dashboard]"; + + + /* Which kind of Request */ + if(request->method() == HTTP_POST) { + + + SaveConfig(); + + Log.notice(F("%s config saved" CR), LogLoc); + + request->send_P(200, TEXT_HTML, Page_grow_dashboard_HTML, Proc_WebPage_grow_dashboard_POST); + + } else { + request->send_P(200, TEXT_HTML, Page_grow_dashboard_HTML, Proc_WebPage_grow_dashboard); + } +} diff --git a/include/Webserver/Page_grow_HTML.h b/include/Webserver/Page_grow_HTML.h new file mode 100644 index 0000000..e0c5bf7 --- /dev/null +++ b/include/Webserver/Page_grow_HTML.h @@ -0,0 +1,102 @@ +/* + * + * include/Webserver/Page_grow_HTML.h - grow page HTML header file + * + * + * + */ + +/* submenu SUBNAV */ +const char Page_grow_HTML_SUBNAV[] PROGMEM = R"(<ul class='subnav'> + <li><a class='%ACTIVE_SUBNAV_GENERAL%' href='/grow/'>🛠️ General</a></li> + <li><a class='%ACTIVE_SUBNAV_LIGHT%' href='/grow/light/'>💡 Light</a></li> + <li><a class='%ACTIVE_SUBNAV_AIR%' href='/grow/air/'>🌀 Air</a></li> + <li><a class='%ACTIVE_SUBNAV_WATER%' href='/grow/water/'>💧 Water</a></li> + <li><a class='%ACTIVE_SUBNAV_DASHBOARD%' href='/grow/dashboard/' >🖥️ Dashboard</a></li> +</ul>)"; + +/* /grow/ main page */ +const char Page_grow_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +%SAVE_MSG% +<p>here you can set grow stuff<br></p><form method='post' action='/grow/'> + +<u>Grow name:</u><br> +<input type='text' name='name' maxlength='31' value='%GROWNAME%' required><br> +<input type='hidden' id='start' name='start' value='%GROWSTART_EPOCH%' required> +<u>Grow start date:</u><br> +<input type='date' id='GrowStart_sel' onChange='convertDateToEpoch("GrowStart_sel", "start");' value='%GROWSTART%' ><br> +<u>Vegetation duration:</u><br> +<input class='inputShort' type='number' name='daysVeg' min='0' max='255' value='%DAYS_VEG%' required> Days<br> +<u>Bloom duration:</u><br> +<input class='inputShort' type='number' name='daysBloom' min='0' max='255' value='%DAYS_BLOOM%' required> Days<br> + +<br> +<input type='submit' value='💾 Save settings'> +</form> +%FOOTER% )"; + +/* /grow/light/ page */ +const char Page_grow_light_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +%SAVE_MSG% +<p>here you can set light stuff<br></p> + + +%LIGHT% + + + + + + +%FOOTER% )"; + + +/* /grow/air/ page */ +const char Page_grow_air_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +%SAVE_MSG% +<p>here you can set air stuff<br></p> + + +%AIR% + + + + + + +%FOOTER% )"; + +/* /grow/water/ page */ +const char Page_grow_water_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +%SAVE_MSG% +<p>here you can set water stuff<br></p> + + +%WATER% + + + + + + +%FOOTER% )"; + +/* /grow/dashboard/ page */ +const char Page_grow_dashboard_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +%SAVE_MSG% +<p>here you can set dashboard stuff<br></p> + + +%DASHBOARD% + + + + + + +%FOOTER% )"; diff --git a/include/Webserver/Page_root.h b/include/Webserver/Page_root.h new file mode 100644 index 0000000..dbdd5dc --- /dev/null +++ b/include/Webserver/Page_root.h @@ -0,0 +1,28 @@ +/* + * + * include/Webserver/Page_root.h - root page header file + * + * + * + */ + +#include "Page_root_HTML.h" + + + +// https://techtutorialsx.com/2018/07/23/esp32-arduino-http-server-template-processing-with-multiple-placeholders/ +String Proc_WebPage_root(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var); + } else if(var == "LOL") { + return String("Nice"); + } else if(var == "LOL") { + return String("Jojoojo :)"); + } else { + return String(); + } +} + +void WebPage_root(AsyncWebServerRequest *request) { + request->send_P(200, TEXT_HTML, Page_root_HTML, Proc_WebPage_root); +} diff --git a/include/Webserver/Page_root_HTML.h b/include/Webserver/Page_root_HTML.h new file mode 100644 index 0000000..8edcaaa --- /dev/null +++ b/include/Webserver/Page_root_HTML.h @@ -0,0 +1,13 @@ +/* + * + * include/Webserver/Page_root_HTML.h - root page HTML header file + * + * + * + */ + + +const char Page_root_HTML[] PROGMEM = R"EOF(%HEADER% +<h2>🌱 Hello world!</h2> +<a href='/api/sensor/'>Sensor data -> /api/sensor/</a> +%FOOTER% )EOF"; diff --git a/include/Webserver/Page_system.h b/include/Webserver/Page_system.h new file mode 100644 index 0000000..578824c --- /dev/null +++ b/include/Webserver/Page_system.h @@ -0,0 +1,1721 @@ +/* + * + * include/Webserver/Page_system.h - system settings page header file + * + * + * + */ + + +#include "Page_system_HTML.h" + +/* global runtime variables */ + +/* VERY VERY DIRTY WORKAROUND + * I have the problem, that I cannot pass a parameter I receive from a http + * request to it's template processor. In my case i want to edit an output, + * the user should click an edit button on the system/output overview page. + * I am lazy so i want to reuse the output_add page, because it is quite + * kinda exactly the same. so i want to call GET /system/output/add?edit=ID + * I have searched and came to the conclusion, that at this point i see no + * other way then giving the parameter I need, the outputId, to an global + * variable, so the template processor can read it. + */ +byte tmpParam_editOutputId = 255; +byte tmpParam_editSensorId = 255; +byte tmpParam_calibrateSensorId = 255; + + +/* subnav processor */ +const byte WEB_SYSTEM_SUBNAV_GENERAL = 1; +const byte WEB_SYSTEM_SUBNAV_SENSOR = 2; +const byte WEB_SYSTEM_SUBNAV_OUTPUT = 3; +const byte WEB_SYSTEM_SUBNAV_UPDATE = 4; +const byte WEB_SYSTEM_SUBNAV_RESTART = 5; +const byte WEB_SYSTEM_SUBNAV_WIPE = 6; + +bool Test_WebPage_system_SUBNAV(const String& var) { + if( + (var == "SUBNAV") || + (var == "ACTIVE_SUBNAV_GENERAL") || + (var == "ACTIVE_SUBNAV_SENSOR") || + (var == "ACTIVE_SUBNAV_OUTPUT") || + (var == "ACTIVE_SUBNAV_UPDATE") || + (var == "ACTIVE_SUBNAV_RESTART") || + (var == "ACTIVE_SUBNAV_WIPE")) { + return true; + } else { + return false; + } +} + +/* + * Proc_WebPage_system_SUBNAV - subnav processor for system + * this function works as same as AddHeaderFooter from Common.h + * byte activeSubnav: + * 1 - Output + * 2 - Update + * 3 - Restart + * 4 - Wipe + */ +String Proc_WebPage_system_SUBNAV(const String& var, byte activeSubnav = 0) { + String activeSubnav_ClassName = "activeNav"; + if(var == "SUBNAV") { + return String(Page_system_HTML_SUBNAV); + } else if((var == "ACTIVE_SUBNAV_GENERAL") && (activeSubnav == WEB_SYSTEM_SUBNAV_GENERAL)) { + return activeSubnav_ClassName; + } else if((var == "ACTIVE_SUBNAV_SENSOR") && (activeSubnav == WEB_SYSTEM_SUBNAV_SENSOR)) { + return activeSubnav_ClassName; + } else if((var == "ACTIVE_SUBNAV_OUTPUT") && (activeSubnav == WEB_SYSTEM_SUBNAV_OUTPUT)) { + return activeSubnav_ClassName; + } else if((var == "ACTIVE_SUBNAV_UPDATE") && (activeSubnav == WEB_SYSTEM_SUBNAV_UPDATE)) { + return activeSubnav_ClassName; + } else if((var == "ACTIVE_SUBNAV_RESTART") && (activeSubnav == WEB_SYSTEM_SUBNAV_RESTART)) { + return activeSubnav_ClassName; + } else if((var == "ACTIVE_SUBNAV_WIPE") && (activeSubnav == WEB_SYSTEM_SUBNAV_WIPE)) { + return activeSubnav_ClassName; + } else { + return String(); + } +} + +/******************************************************************************* + * Main system page + */ +// https://techtutorialsx.com/2018/07/23/esp32-arduino-http-server-template-processing-with-multiple-placeholders/ +String Proc_WebPage_system(const String& var) { + const static char LogLoc[] PROGMEM = "[Webserver:system(Proc)]"; + /* This is a processor function, which returns a string. + * We check if var contains one of our placeholders from the template. + * If we hit a placeholder, we just return the String we want. + * + * TestHeaderFooter() Is kinda a processor too, but only checks for + * header specific placeholders. + */ + + //Log.verbose(F("%s var: %s" CR), LogLoc, var); + + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 2); + } else if(Test_WebPage_system_SUBNAV(var)) { + return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_GENERAL); + } else if(var == "NTPOFFSET") { + return String(config.system.ntpOffset); + + } else if(var == "MAINTDUR") { + return String(config.system.maintenanceDuration); + } else if(var == "ESP32CAM") { + return String(config.system.esp32cam); + } else if(var == "HTTPLOGSERIAL") { + return Html_SelectOpt_bool(config.system.httpLogSerial); + } else if(var == "RTC_STATUS") { + /* show warn sign if rtcError is true (there was an error), otherwise green checkmark */ + if(config.system.rtc > 0) { + if(rtcError == true) { + return F(" ⚠️ "); + } else { + return F(" ✅ "); + } + } else { + return String(); + } + } else if(var == "RTC_AVAILABLE") { + return Html_SelectOpt_array(RTCs_total, RTCs_descr, config.system.rtc); + } else if(var == "TIME2FS") { + return Html_SelectOpt_bool(config.system.time2fs); + } else if(var == "PWMFREQ") { + return String(config.system.pwmFreq); + } else { + return String(); + } +} + +String Proc_WebPage_system_POST(const String& var) { + /* This is the processor for POST + * Its exactly the same, just looking for SAVE_MSG string. + * If nothing matches, it calles the main Proc_WebPage_system() + * processor function, so all the other stuff like header and so + * on get replaced + */ + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG); + } else { + return Proc_WebPage_system(var); + } +} + +String Proc_WebPage_system_POST_ERR(const String& var) { + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG_ERR); + } else { + return Proc_WebPage_system(var); + } +} + +/* WebPage function */ +void WebPage_system(AsyncWebServerRequest *request) { + const static char LogLoc[] PROGMEM = "[Webserver:system]"; + + /* when changing httpLogSerial it requires a restart to take effect + * for this we keep the old val to compare it if it got changed + * to notice user for a restart */ + bool old_httpLogSerial = config.system.httpLogSerial; + byte old_rtc = config.system.rtc; + short old_ntpOffset; + + /* Which kind of Request */ + if(request->method() == HTTP_POST) { + + if(request->hasParam("ntp", true)) { + const AsyncWebParameter* param = request->getParam("ntp", true); + config.system.ntp = param->value().toInt(); + } + + if(request->hasParam("ntpOffset", true)) { + const AsyncWebParameter* param = request->getParam("ntpOffset", true); + //Log.verbose(F("%s POST[%s]: %s" CR), LogLoc, param->value().c_str()); + old_ntpOffset = config.system.ntpOffset; + config.system.ntpOffset = param->value().toInt(); + if((config.system.ntp == true) && (old_ntpOffset != config.system.ntpOffset)) { + // trigger ntp offset update + updateNtpOffset = true; + } + + } + + if(request->hasParam("maintenanceDuration", true)) { + const AsyncWebParameter* param = request->getParam("maintenanceDuration", true); + config.system.maintenanceDuration = param->value().toInt(); + } + + if(request->hasParam("esp32cam", true)) { + const AsyncWebParameter* param = request->getParam("esp32cam", true); + //config.system.esp32cam = param->value().toInt(); + strlcpy(config.system.esp32cam, param->value().c_str(), sizeof(config.system.esp32cam)); + } + + if(request->hasParam("httpLogSerial", true)) { + const AsyncWebParameter* param = request->getParam("httpLogSerial", true); + config.system.httpLogSerial = param->value().toInt(); + if( old_httpLogSerial != config.system.httpLogSerial) { + needRestart = true; + } + } + + + if(request->hasParam("rtc", true)) { + const AsyncWebParameter* param = request->getParam("rtc", true); + config.system.rtc = param->value().toInt(); + if( old_rtc != config.system.rtc) { + needRestart = true; + if(config.system.rtc > 0) + rtcError = true; + } + } + + if(request->hasParam("time2fs", true)) { + const AsyncWebParameter* param = request->getParam("time2fs", true); + config.system.time2fs = param->value().toInt(); + } + + if(request->hasParam("pwmFreq", true)) { + const AsyncWebParameter* param = request->getParam("pwmFreq", true); + config.system.pwmFreq = param->value().toInt(); + #ifdef ESP8266 + /* set pwm frequency global for ESP8266. + * ESP32 pwm frequency setting is done withing CanGrow_Output / Init */ + analogWriteFreq(config.system.pwmFreq); + #endif + } + + if(SaveConfig()) { + // we need a restart to apply the new settings + + Log.notice(F("%s config saved" CR), LogLoc); + + request->send_P(200, "text/html", Page_system_HTML, Proc_WebPage_system_POST); + } else { + Log.error(F("%s ERROR while saving config" CR), LogLoc); + request->send_P(200, TEXT_HTML, Page_system_HTML, Proc_WebPage_system_POST_ERR); + } + } else { + request->send_P(200, TEXT_HTML, Page_system_HTML, Proc_WebPage_system); + } +} + + +/******************************************************************************* + * Subpage restart + */ +String Proc_WebPage_system_restart(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 2); + } else if(Test_WebPage_system_SUBNAV(var)) { + return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_RESTART); + } else if(var == "RESTART_MSG") { + return String(Page_system_restart_HTML_RESTART_MSG); + } else { + return String(); + } +} + +String Proc_WebPage_system_restart_POST(const String& var) { + if(var == "RESTART_MSG") { + return String(Page_system_restart_HTML_RESTART_MSG_POST); + } else { + return Proc_WebPage_system_restart(var); + } +} + +void WebPage_system_restart(AsyncWebServerRequest *request) { + const static char LogLoc[] PROGMEM = "[Webserver:system:restart]"; + if(request->method() == HTTP_POST) { + if(request->hasParam("confirmed", true)) { + doRestart = false; + } + //request->send_P(200, TEXT_HTML, Page_system_restart_HTML, Proc_WebPage_system_restart_POST); + + /* Add custom header for redirect after timeout + * https://github.com/mathieucarbou/ESPAsyncWebServer?tab=readme-ov-file#send-large-webpage-from-progmem-containing-templates-and-extra-headers */ + AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", Page_system_restart_HTML, Proc_WebPage_system_restart_POST); + /* return Refresh header to redirect to root page after restart */ + if(config.wifi.dhcp == true) { + response->addHeader("Refresh","20; url=http://" + WiFi.localIP().toString()); + } else { + response->addHeader("Refresh","20; url=http://" + String(IP2Char(config.wifi.ip))); + } + + request->send(response); + + if(request->hasParam("confirmed", true)) { + Log.notice(F("%s POST[confirmed]: is set, triggering restart" CR), LogLoc); + + // set global var doRestart to true causes a restart + doRestart = true; + } + + } else { + request->send_P(200, TEXT_HTML, Page_system_restart_HTML, Proc_WebPage_system_restart); + } + +} + + + +/******************************************************************************* + * Subpage update + */ + +// https://github.com/mathieucarbou/ESPAsyncWebServer/blob/main/docs/index.md#setting-up-the-server +void WebPage_system_update_ApplyUpdate(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){ + const static char LogLoc[] PROGMEM = "[Webserver:system:update:ApplyUpdate]"; + if(!index){ + Log.notice(F("%s Update Start: %s" CR), LogLoc, filename.c_str()); + + // https://github.com/me-no-dev/ESPAsyncWebServer/issues/455#issuecomment-451728099 + // workaround for bug with ESP32 + #ifdef ESP8266 + Update.runAsync(true); + #endif + if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)){ + Update.printError(Serial); + } + } + if(!Update.hasError()){ + if(Update.write(data, len) != len){ + Update.printError(Serial); + } + } + if(final){ + if(Update.end(true)){ + Log.notice(F("%s Update Success: %uB" CR), LogLoc, index+len); + } else { + Log.error(F("%s FAILED Update:" CR), LogLoc); + Update.printError(Serial); + } + } + } + +String Proc_WebPage_system_update(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 2); + } else if(Test_WebPage_system_SUBNAV(var)) { + return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_UPDATE); + } else { + return String(); + } +} + +/* After an update.bin file was uploaded*/ +String Proc_WebPage_system_update_POST(const String& var) { + if(var == "CONFIGWIFI_IP") { + if(config.wifi.dhcp == true) { + return WiFi.localIP().toString(); + } else { + return String(IP2Char(config.wifi.ip)); + } + } else { + return String(); + } +} + +void WebPage_system_update(AsyncWebServerRequest *request) { + if(request->method() == HTTP_POST) { + doRestart = !Update.hasError(); + // when doRestart is true, deliver Page_system_update_HTML_POST + // otherwise Page_system_update_HTML_POST_FAILED + AsyncWebServerResponse *response = request->beginResponse_P(200, TEXT_HTML, doRestart?Page_system_update_HTML_POST:Page_system_update_HTML_POST_FAILED, Proc_WebPage_system_update_POST); + response->addHeader(F("Connection"), F("close")); + request->send(response); + } else { + request->send_P(200, TEXT_HTML, Page_system_update_HTML, Proc_WebPage_system_update); + } +} + + +/******************************************************************************* + * Subpage wipe + */ +String Proc_WebPage_system_wipe(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 2); + } else if(Test_WebPage_system_SUBNAV(var)) { + return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_WIPE); + } else if(var == "WIPE_MSG") { + return String(Page_system_wipe_HTML_WIPE_MSG); + } else { + return String(); + } +} + +String Proc_WebPage_system_wipe_POST(const String& var) { + if(var == "WIPE_MSG") { + return String(Page_system_wipe_HTML_WIPE_MSG_POST); + } else { + return Proc_WebPage_system_wipe(var); + } +} + +void WebPage_system_wipe(AsyncWebServerRequest *request) { + const static char LogLoc[] PROGMEM = "[Webserver:system:wipe]"; + if(request->method() == HTTP_POST) { + request->send_P(200, TEXT_HTML, Page_system_wipe_HTML, Proc_WebPage_system_wipe_POST); + + if(request->hasParam("confirmed", true)) { + Log.notice(F("%s POST[confirmed]: is set, triggering wipe / factory reset" CR), LogLoc); + LFS_Format(); + Log.notice(F("%s triggering restart" CR), LogLoc); + doRestart = true; + } + + } else { + request->send_P(200, TEXT_HTML, Page_system_wipe_HTML, Proc_WebPage_system_wipe); + } + +} + + +/******************************************************************************* + * Subpage output + */ +String Proc_WebPage_system_output(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 2); + } else if(Test_WebPage_system_SUBNAV(var)) { + return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_OUTPUT); + } else if(var == "ADD_DISABLED") { + /* check if there is a free Output Id. Give_Free_OutputId returns 255 if no id available, otherwise it + * gives us the next free id. Here we check if the given ID is greater then Max_Outputs. This will also + * reflect a valid result, if there is a free id left or not. */ + if(Give_Free_OutputId() > Max_Outputs ) { + return F("disabled force_hide"); + } else { + return String(); + } + + } else if(var == "TR_TD") { + // build table body + // i dont know a better way at the moment. if you do, please tell me! + String html; + for(byte i=0; i < Max_Outputs; i++) { + if(config.system.output.type[i] > 0) { + + html += F("<tr><td>"); + /* show warn sign if outputStatus is false (uninitialized), otherwise green checkmark */ + if(outputStatus[i] == false) { + html += F(" ⚠️"); + } else { + html += F(" ✅"); + } + /* bit spacing after the status icon */ + html += F(" "); + html += F("</td><td>"); + html += i; + + html += F("</td><td>"); + html += config.system.output.name[i]; + html += F("</td><td>"); + html += FPSTR(Output_Type_descr[config.system.output.type[i]]); + + if((config.system.output.type[i] == OUTPUT_TYPE_GPIO) || ( (config.system.output.type[i] == OUTPUT_TYPE_I2C) && (config.system.output.i2c_type[i] > 0) )) { + html += F(" ("); + + switch(config.system.output.type[i]) { + case OUTPUT_TYPE_GPIO: + html += GPIOindex[config.system.output.gpio[i]].gpio; + break; + + case OUTPUT_TYPE_I2C: + html += OutputI2Cindex[config.system.output.i2c_type[i]].name; + break; + + default: + break; + } + + html += F(")"); + } + + html += F("</td><td>"); + html += FPSTR(Output_Device_descr[config.system.output.device[i]]); + html += F("</td><td>"); + + if(config.system.output.enabled[i] > 0) { + html += F(" 🟢 "); + } else { + html += F(" 🔴 "); + } + + html += F("</td><td>"); + + // edit button + html += F("<form class='linkForm' action='/system/output/add' method='get'>"); + html += F("<input type='hidden' name='edit' value='"); + html += i; + html += F("'>"); + html += F("<input type='submit' value='✏️' title='Edit'></form> "); + + + // delete button + html += F("<form class='linkForm' action='/system/output/' method='post'>"); + html += F("<input type='hidden' name='delete_output' value='"); + html += i; + html += F("'>"); + html += F("<input type='submit' value='❌' onclick=\"return confirmDelete('"); + html += config.system.output.name[i];; + html += F("')\" title='Delete'></form>"); + + html += F("</td></tr>"); + } + } + + return html; + } else{ + return String(); + } +} + +String Proc_WebPage_system_output_POST(const String& var) { + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG); + } else { + return Proc_WebPage_system_output(var); + } +} + +void WebPage_system_output(AsyncWebServerRequest *request) { + if(request->method() == HTTP_POST) { + if(request->hasParam("delete_output", true)) { + byte outputId; + + const AsyncWebParameter* param = request->getParam("delete_output", true); + + outputId = param->value().toInt(); + + /* remove grow objects */ + Output_Device_Grow_AddRemove(outputId, 1); + + // we ensure that every field is empty + config.system.output.type[outputId] = 0; + config.system.output.device[outputId] = 0; + // set every field of char array to 0x00 with memset + memset(config.system.output.name[outputId], '\0', sizeof config.system.output.name[outputId]); + config.system.output.enabled[outputId] = 0; + config.system.output.gpio[outputId] = 0; + config.system.output.gpio_pwm[outputId] = 0; + config.system.output.invert[outputId] = 0; + config.system.output.i2c_type[outputId] = 0; + memset(config.system.output.webcall_host[outputId], '\0', sizeof config.system.output.webcall_host[outputId]); + memset(config.system.output.webcall_path_on[outputId], '\0', sizeof config.system.output.webcall_path_on[outputId]); + memset(config.system.output.webcall_path_off[outputId], '\0', sizeof config.system.output.webcall_path_off[outputId]); + + #ifdef DEBUG + SaveConfig(true); + #endif + SaveConfig(); + } + + request->send_P(200, TEXT_HTML, Page_system_output_HTML, Proc_WebPage_system_output_POST); + + + } else { + if(request->hasParam("success")) { + // when GET param success is present, we use the _POST processor for the save message + request->send_P(200, TEXT_HTML, Page_system_output_HTML, Proc_WebPage_system_output_POST); + } else { + request->send_P(200, TEXT_HTML, Page_system_output_HTML, Proc_WebPage_system_output); + } + } + +} + +/******************************************************************************* + * Subpage output add + */ + + +/* returns select <option> list of available output types */ +String Html_SelOpt_type_WebPage_system_output_i2c_add(byte selectId = 255) { + String html; + // go through all available Output I2C modules, skip 0 because it means unconfigured + for(byte i = 1; i <= OutputI2Cindex_length; i++) { + html += F("<option value='"); + html += i; + html += F("'"); + if(i == selectId) { + html += F(" selected"); + } + html += F(">"); + html += OutputI2Cindex[i].name; + html += F("</option>"); + } + return html; +} + +String Js_I2cAddr_Array_WebPage_system_output_i2c_add() { + const static char LogLoc[] PROGMEM = "[Webserver:system:output:add:i2c:Js_I2cAddr_Array]"; + /* bit hacky, bit dirty, but may work + * here we return a 2-dimensional javascript array. the returned stuff + * gets directly injected in the template into a js function + */ + String js; + + /* iterate through all OutputI2Cindex in index*/ + for(byte i = 1; i <= OutputI2Cindex_length; i++) { + // name + js += F("["); + /* iterate through all available addresses */ + for(byte j = 0; j < OutputI2Cindex[i].max; j++) { + js += F("['0x"); + js += String(Output_I2C_Addr_Init_Update(i, j, 0, OUPUT_I2C_AIU_MODE_ADDR), HEX); + // value + js += F("', '"); + js += j; + // used + + js += F("', ["); + /* check I2C module ports available */ + for(byte k = 0; k < OUTPUT_TYPE_I2C_MAX_PORTS; k++) { + + /* When this Port of the module offers a value */ + if(OutputI2Cindex[i].port[k] > 0) { + js += F("'"); + /* check if I2C module and port are used in config */ + for(byte l = 0; l < Max_Outputs; l++) { + if((config.system.output.type[l] == OUTPUT_TYPE_I2C) && (config.system.output.i2c_type[l] == i) && + (config.system.output.i2c_addr[l] == j) && (config.system.output.i2c_port[l] == k)) { + js += 1; + /* exit loop here */ + l = Max_Outputs + 1; + } + } + js += F("',"); + } + + } + + js += F("]],"); + } + js += F("],\n"); + } + + return js; +} + + + + +String Proc_WebPage_system_output_add(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 2); + } else if(Test_WebPage_system_SUBNAV(var)) { + return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_OUTPUT); + } else if(var == "ACTION") { + return F("➕ Add"); + + } else if(var == "OUTPUT_ID") { + // we check which id is free. A free ID as type == 0 + return String(Give_Free_OutputId()); + + + } else if(var == "OUTPUT_TYPE") { + return Html_SelectOpt_array(OUTPUT_TYPE__TOTAL, Output_Type_descr); + + } else if(var == "OUTPUT_DEVICE") { + return Html_SelectOpt_array(OUTPUT_DEVICE__TOTAL, Output_Device_descr); + + } else if(var == "OUTPUT_ENABLED") { + return Html_SelectOpt_bool(); + + } else if(var == "INVERT") { + return Html_SelectOpt_bool(); + } else if(var == "GPIO_INDEX") { + return Html_SelectOpt_GPIOindex(); + + } else if(var == "GPIO_PWM") { + return Html_SelectOpt_bool(); + + } else if(var == "I2C_TYPE") { + return Html_SelOpt_type_WebPage_system_output_i2c_add(); + } else if(var == "REPLACE_I2CADDR_JS") { + + return Js_I2cAddr_Array_WebPage_system_output_i2c_add(); + + } else { + return String(); + } +} + +String Proc_WebPage_system_output_addEdit(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 2); + } else if(Test_WebPage_system_SUBNAV(var)) { + return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_OUTPUT); + } else if(var == "ACTION") { + return F("✏️ Edit"); + + } else if(var == "EDIT_MODE") { + return F("editmode"); + } else if(var == "OUTPUT_ID") { + // return the outputId we got from GET .../add?edit=ID + // dirty workaround to put this in a global variable + return String(tmpParam_editOutputId); + + } else if(var == "OUTPUT_TYPE") { + return Html_SelectOpt_array(OUTPUT_TYPE__TOTAL, Output_Type_descr, config.system.output.type[tmpParam_editOutputId]); + + } else if(var == "OUTPUT_DEVICE") { + return Html_SelectOpt_array(OUTPUT_DEVICE__TOTAL, Output_Device_descr, config.system.output.device[tmpParam_editOutputId]); + + } else if(var == "OUTPUT_NAME") { + // "escape" % character, because it would break the template processor. + // tasmote webcall for example has percentage char in its path + String outputName = config.system.output.name[tmpParam_editOutputId];; + outputName.replace(F("%"), F("%")); + return outputName; + + } else if(var == "OUTPUT_ENABLED") { + return Html_SelectOpt_bool(config.system.output.enabled[tmpParam_editOutputId]); + + } else if(var == "INVERT") { + return Html_SelectOpt_bool(config.system.output.invert[tmpParam_editOutputId]); + + } else if(var == "GPIO_INDEX") { + return Html_SelectOpt_GPIOindex(config.system.output.gpio[tmpParam_editOutputId]); + + } else if(var == "GPIO_PWM") { + return Html_SelectOpt_bool(config.system.output.gpio_pwm[tmpParam_editOutputId]); + + } else if(var == "I2C_TYPE") { + //return String(config.system.output.i2c_type[tmpParam_editOutputId]); + //Html_SelectOpt_array(OutputI2Cindex_length, OutputI2Cindex[].name, config.system.output.i2c_type[tmpParam_editOutputId]); + return Html_SelOpt_type_WebPage_system_output_i2c_add(config.system.output.i2c_type[tmpParam_editOutputId]); + } else if(var == "REPLACE_I2CADDR_JS") { + + return Js_I2cAddr_Array_WebPage_system_output_i2c_add(); + + } else if(var == "I2C_SAVED") { + + /* Add bit javascript to ensure the saved value is selected */ + String js; + js += F("showSelect('type_sel', 'type_', 'hidden'); SystemOutputAddselectRequired('type_sel');"); + + //js += F("document.getElementById('i2c_type').value='"); + //js += config.system.output.i2c_type[tmpParam_editOutputId]; + //js += F("';\n"); + + js += F("SystemOutputAdd_replaceI2cAddr('i2c_type', 'i2c_addr');"); + + js += F("document.getElementById('i2c_addr').value='"); + js += config.system.output.i2c_addr[tmpParam_editOutputId]; + js += F("';\n"); + + js += F("SystemOutputAdd_replaceI2cPort('i2c_type', 'i2c_addr', 'i2c_port');\n"); + + js += F("document.getElementById('i2c_port').value='"); + js += config.system.output.i2c_port[tmpParam_editOutputId]; + js += F("';\n"); + + + //js += "SystemOutputAdd_replaceI2cPort('i2c_type', 'i2c_addr', 'i2c_port');"; + return js; + + + } else if(var == "WEBCALL_HOST") { + return String(config.system.output.webcall_host[tmpParam_editOutputId]); + + } else if(var == "WEBCALL_PATH_ON") { + String webcallPathOn = config.system.output.webcall_path_on[tmpParam_editOutputId]; + webcallPathOn.replace(F("%"), F("%")); + return webcallPathOn; + + } else if(var == "WEBCALL_PATH_OFF") { + String webcallPathOff = config.system.output.webcall_path_off[tmpParam_editOutputId]; + webcallPathOff.replace(F("%"), F("%")); + return webcallPathOff; + + } else if( + ((var == "CLASS_TYPE_1") && (config.system.output.type[tmpParam_editOutputId] == 1)) || + ((var == "CLASS_TYPE_2") && (config.system.output.type[tmpParam_editOutputId] == 2)) || + ((var == "CLASS_TYPE_3") && (config.system.output.type[tmpParam_editOutputId] == 3))) { + // add class 'visible' which overwrites display with flex!important and justify center + return F("visible"); + + } else { + return String(); + } +} + +String Proc_WebPage_system_output_add_POST(const String& var) { + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG); + } else { + return Proc_WebPage_system_output_add(var); + } +} + + +void WebPage_system_output_add(AsyncWebServerRequest *request) { + const static char LogLoc[] PROGMEM = "[Webserver:system:output:add]"; + if(request->method() == HTTP_POST) { + + byte outputId; + byte outputType; + //byte outputType_old; + + byte outputDevice_old; + + + if(request->hasParam("outputId", true)) { + const AsyncWebParameter* param = request->getParam("outputId", true); + outputId = param->value().toInt(); + } + + + + + + + if(request->hasParam("type", true)) { + const AsyncWebParameter* param = request->getParam("type", true); + // put info config struct + config.system.output.type[outputId] = param->value().toInt(); + // remember the value in own var to work later with here + //outputType = param->value().toInt(); + } + + /* save outputDevice_old */ + outputDevice_old = config.system.output.device[outputId]; + + if(request->hasParam("device", true)) { + const AsyncWebParameter* param = request->getParam("device", true); + + byte outputDevice = param->value().toInt(); + + /* check if output type has changed. if so, delete old Output + * Grow Devices and recreate them with new type later */ + //if((outputType != outputType_old) || (outputDevice != outputDevice_old)) + if(outputDevice != outputDevice_old) { + #ifdef DEBUG + Log.verbose(F("%s - device changed, delete old Grow object" CR), LogLoc); + #endif + Output_Device_Grow_AddRemove(outputId, 1); + //config.grow.light.configured[outputId] = false; + } + /* finally write the new device type into the config */ + config.system.output.device[outputId] = param->value().toInt(); + } + + + + + + + + if(request->hasParam("name", true)) { + const AsyncWebParameter* param = request->getParam("name", true); + strlcpy(config.system.output.name[outputId], param->value().c_str(), sizeof(config.system.output.name[outputId])); + } + + if(request->hasParam("enabled", true)) { + const AsyncWebParameter* param = request->getParam("enabled", true); + config.system.output.enabled[outputId] = param->value().toInt(); + } + + + if(request->hasParam("invert", true)) { + const AsyncWebParameter* param = request->getParam("invert", true); + config.system.output.invert[outputId] = param->value().toInt(); + } + + // only fill the type related config vars + switch(config.system.output.type[outputId]) { + // GPIO + case OUTPUT_TYPE_GPIO: + if(request->hasParam("gpio", true)) { + byte old_gpio = config.system.output.gpio[outputId]; + const AsyncWebParameter* param = request->getParam("gpio", true); + config.system.output.gpio[outputId] = param->value().toInt(); + if(old_gpio != config.system.output.gpio[outputId]) + needRestart = true; + + } + + if(request->hasParam("gpio_pwm", true)) { + const AsyncWebParameter* param = request->getParam("gpio_pwm", true); + config.system.output.gpio_pwm[outputId] = param->value().toInt(); + } + break; + + // I2C + case OUTPUT_TYPE_I2C: + if(request->hasParam("i2c_type", true)) { + byte old_i2c_type = config.system.output.i2c_type[outputId]; + const AsyncWebParameter* param = request->getParam("i2c_type", true); + config.system.output.i2c_type[outputId] = param->value().toInt(); + if(old_i2c_type != config.system.output.i2c_type[outputId]) + needRestart = true; + outputStatus[outputId] = false; + } + + if(request->hasParam("i2c_addr", true)) { + byte old_i2c_addr = config.system.output.i2c_addr[outputId]; + const AsyncWebParameter* param = request->getParam("i2c_addr", true); + config.system.output.i2c_addr[outputId] = param->value().toInt(); + if(old_i2c_addr != config.system.output.i2c_addr[outputId]) + needRestart = true; + outputStatus[outputId] = false; + } + + if(request->hasParam("i2c_port", true)) { + const AsyncWebParameter* param = request->getParam("i2c_port", true); + config.system.output.i2c_port[outputId] = param->value().toInt(); + } + break; + // Webcall + case OUTPUT_TYPE_WEB: + if(request->hasParam("webcall_host", true)) { + const AsyncWebParameter* param = request->getParam("webcall_host", true); + strlcpy(config.system.output.webcall_host[outputId], param->value().c_str(), sizeof(config.system.output.webcall_host[outputId])); + } + + if(request->hasParam("webcall_path_on", true)) { + const AsyncWebParameter* param = request->getParam("webcall_path_on", true); + strlcpy(config.system.output.webcall_path_on[outputId], param->value().c_str(), sizeof(config.system.output.webcall_path_on[outputId])); + } + + if(request->hasParam("webcall_path_off", true)) { + const AsyncWebParameter* param = request->getParam("webcall_path_off", true); + strlcpy(config.system.output.webcall_path_off[outputId], param->value().c_str(), sizeof(config.system.output.webcall_path_off[outputId])); + } + + /* reset in any case the webcall fail counter to trigger update retry */ + outputWebcallFailed[outputId] = 0; + break; + default: break; + } + + + /* create grow objects */ + if(request->hasParam("editmode")) { + //Log.verbose(F("%s - has edit" CR), LogLoc); + if(config.system.output.device[outputId] != outputDevice_old) { + #ifdef DEBUG + Log.verbose(F("%s - device changed, Recreate Grow object" CR), LogLoc); + #endif + // remove - already done few lines before + //Output_Device_Grow_AddRemove(outputId, 1); + // add empty + Output_Device_Grow_AddRemove(outputId, 0); + } + } else { + #ifdef DEBUG + Log.verbose(F("%s - no edit" CR), LogLoc); + #endif + Output_Device_Grow_AddRemove(outputId, 0); + } + #ifdef DEBUG + SaveConfig(true); + #endif + SaveConfig(); + // request->send_P(200, "text/html", Page_system_output_add_HTML, Proc_WebPage_system_output_add_POST); + // I like it more when user gets redirected to the output overview after saving + request->redirect(F("/system/output/?success")); + } else { + + /* When in edit mode */ + if(request->hasParam("edit")) { + const AsyncWebParameter* param = request->getParam("edit"); + tmpParam_editOutputId = param->value().toInt(); + request->send_P(200, TEXT_HTML, Page_system_output_add_HTML, Proc_WebPage_system_output_addEdit); + /* when just adding a new output, check if there are free IDs */ + } else if(Give_Free_OutputId() > Max_Outputs) { + /* if not, send error */ + request->send_P(200, TEXT_HTML, Page_system_output_add_HTML_NO_ID_AVAILABLE, Proc_WebPage_system_output_add); + } else { + /* otherwise let the user create new output */ + request->send_P(200, TEXT_HTML, Page_system_output_add_HTML, Proc_WebPage_system_output_add); + } + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +/******************************************************************************* + * Subpage sensor + */ + +String Proc_WebPage_system_sensor(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 2); + } else if(Test_WebPage_system_SUBNAV(var)) { + return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_SENSOR); + } else if(var == "ADD_DISABLED") { + if(Give_Free_SensorId() > Max_Outputs ) { + return F("disabled force_hide"); + } else { + return String(); + } + } else if(var == "TR_TD") { + // build table body + // i dont know a better way at the moment. if you do, please tell me! + String html; + for(byte i=0; i < Max_Sensors; i++) { + if(config.system.sensor.type[i] > 0) { + + html += F("<tr><td>"); + /* show warn sign if sensorStatus is false (uninitialized), otherwise green checkmark */ + if(sensorStatus[i] == false) { + html += F(" ⚠️"); + } else { + html += F(" ✅"); + } + /* bit spacing after the status icon */ + html += F(" "); + html += F("</td><td>"); + /* sens*/ + html += i; + + html += F("</td><td>"); + html += config.system.sensor.name[i]; + html += F("</td><td>"); + html += SensorIndex[config.system.sensor.type[i]].name; + + /* when GPIO pin or I2C sensor is configured (1 is int adc), shot the Pin / addr in overview */ + if(( + (SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_INTADC) && config.system.sensor.gpio[i][0] > 0) || + (SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_ONEWIRE) || + (SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_TWOWIRE) || + (SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_I2C)) { + html += F(" ("); + + if(config.system.sensor.gpio[i][0] > 0) { + html += GPIOindex[config.system.sensor.gpio[i][0]].gpio; + + if(config.system.sensor.gpio[i][1] > 0) { + html += F("/"); + html += GPIOindex[config.system.sensor.gpio[i][1]].gpio; + } + } else if(SensorIndex[config.system.sensor.type[i]].type == SENSOR_TYPE_I2C) { + html += F("0x"); + html += String(Sensor_Addr_Init_Update(config.system.sensor.type[i], config.system.sensor.i2c_addr[i], SENSOR_AIU_MODE_ADDR), HEX); + } + html += F(")"); + } + + html += F("</td><td>"); + + // calibrate button + html += F("<form class='linkForm' action='/system/sensor/calibrate' method='get'>"); + html += F("<input type='hidden' name='calibrate' value='"); + html += i; + html += F("'>"); + html += F("<input type='submit' value='🎛️' title='Calibrate'></form> "); + + // edit button + html += F("<form class='linkForm' action='/system/sensor/add' method='get'>"); + html += F("<input type='hidden' name='edit' value='"); + html += i; + html += F("'>"); + html += F("<input type='submit' value='✏️' title='Edit'></form> "); + + // delete button + html += F("<form class='linkForm' action='/system/sensor/' method='post'>"); + html += F("<input type='hidden' name='delete_sensor' value='"); + html += i; + html += F("'>"); + html += F("<input type='submit' value='❌' onclick=\"return confirmDelete('"); + html += config.system.sensor.name[i];; + html += F("')\" title='Delete'></form>"); + + html += F("</td></tr>"); + } + } + + return html; + } else{ + return String(); + } +} + +String Proc_WebPage_system_sensor_POST(const String& var) { + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG); + } else { + return Proc_WebPage_system_sensor(var); + } +} + +void WebPage_system_sensor(AsyncWebServerRequest *request) { + if(request->method() == HTTP_POST) { + if(request->hasParam("delete_sensor", true)) { + byte sensorId; + + const AsyncWebParameter* param = request->getParam("delete_sensor", true); + sensorId = param->value().toInt(); + + // we ensure that every field is empty + config.system.sensor.type[sensorId] = 0; + memset(config.system.sensor.name[sensorId], '\0', sizeof config.system.sensor.name[sensorId]); + + /* go through all GPIOs */ + for(byte i = 0; i < Max_Sensors_GPIO; i++) { + config.system.sensor.gpio[sensorId][i] = 0; + } + + config.system.sensor.i2c_addr[sensorId] = 0; + sensorStatus[sensorId]; + + SaveConfig(); + } + request->send_P(200, TEXT_HTML, Page_system_sensor_HTML, Proc_WebPage_system_sensor_POST); + + } else { + if(request->hasParam("success")) { + // when GET param success is present, we use the _POST processor for the save message + request->send_P(200, TEXT_HTML, Page_system_sensor_HTML, Proc_WebPage_system_sensor_POST); + } else { + request->send_P(200, TEXT_HTML, Page_system_sensor_HTML, Proc_WebPage_system_sensor); + } + } + +} + + +/******************************************************************************* + * Subpage sensor add + */ + +/* returns select <option> list of available output types */ +String Html_SelOpt_type_WebPage_system_sensor_add(byte selectId = 255) { + String html; + // go through all available Output Devices, skip 0 because it means unconfigured + for(byte i = 1; i <= SensorIndex_length; i++) { + html += F("<option value='"); + html += i; + html += F("'"); + if(i == selectId) { + html += F(" selected"); + } + html += F(">"); + html += SensorIndex[i].name; + html += F("</option>"); + } + return html; +} + +String Js_I2cAddr_Array_WebPage_system_sensor_add() { + const static char LogLoc[] PROGMEM = "[Webserver:system:sensor:add:Js_I2cAddr_Array]"; + /* bit hacky, bit dirty, but may work + * here we return a 2-dimensional javascript array. the returned stuff + * gets directly injected in the template into a js function + */ + String js; + + /* iterate through all SensorIds in index*/ + for(byte i = 1; i <= SensorIndex_length; i++) { + // name + js += F("["); + /* iterate through all available addresses */ + for(byte j = 0; j < SensorIndex[i].max; j++) { + js += F("['0x"); + js += String(Sensor_Addr_Init_Update(i, j, SENSOR_AIU_MODE_ADDR), HEX); + // value + js += F("', '"); + js += j; + // used + bool used; + js += F("', '"); + /* check if addr is used */ + for(byte k = 0; k < Max_Sensors; k++) { + if(config.system.sensor.type[k] == i ) { + if(config.system.sensor.i2c_addr[k] == j) { + js += F("1"); + // exit loop here + k = Max_Sensors + 1; + } //else { + //js += "0"; + //} + + } + } + js += F("'],"); + } + js += F("],\n"); + } + + return js; +} + +String Proc_WebPage_system_sensor_add(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 2); + } else if(Test_WebPage_system_SUBNAV(var)) { + return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_SENSOR); + } else if(var == "ACTION") { + return F("➕ Add"); + + } else if(var == "SENSOR_ID") { + // we check which id is free. A free ID as type == 0 + return String(Give_Free_SensorId()); + + + } else if(var == "SENSOR_TYPE") { + return Html_SelOpt_type_WebPage_system_sensor_add(); + + } else if(var == "GPIO_INDEX") { + return Html_SelectOpt_GPIOindex(255, true); + + } else if(var == "ESP_PLATFORM") { + #ifdef ESP8266 + return F("8266"); + #endif + + #ifdef ESP32 + return F("32"); + #endif + } else if(var == "REPLACE_I2CADDR_JS") { + + return Js_I2cAddr_Array_WebPage_system_sensor_add(); + + } else { + return String(); + } +} + +String Proc_WebPage_system_sensor_addEdit(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 2); + } else if(Test_WebPage_system_SUBNAV(var)) { + return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_SENSOR); + } else if(var == "ACTION") { + return F("✏️ Edit"); + + } else if(var == "SENSOR_ID") { + // return the sensorId we got from GET .../add?edit=ID + // dirty workaround to put this in a global variable + return String(tmpParam_editSensorId); + + } else if(var == "SENSOR_TYPE") { + return Html_SelOpt_type_WebPage_system_sensor_add(config.system.sensor.type[tmpParam_editSensorId]); + + } else if(var == "SENSOR_NAME") { + // "escape" % character, because it would break the template processor. + // tasmote webcall for example has percentage char in its path + String sensorName = config.system.sensor.name[tmpParam_editSensorId];; + sensorName.replace(F("%"), F("%")); + return sensorName; + + } else if(var == "GPIO_INDEX") { + return Html_SelectOpt_GPIOindex(config.system.sensor.gpio[tmpParam_editSensorId][0], true); + + } else if(var == "ESP_PLATFORM") { + #ifdef ESP8266 + return F("8266"); + #endif + + #ifdef ESP32 + return F("32"); + #endif + } else if(var == "REPLACE_I2CADDR_JS") { + + return Js_I2cAddr_Array_WebPage_system_sensor_add(); + + } else if(var == "I2C_SAVED") { + + /* Add bit javascript to ensure the saved value is selected */ + String js; + js += F("SystemSensorAddGpioI2cSel('type_sel');SystemSensor_replaceAddr('type_sel', 'i2c_addr');"); + js += F("document.getElementById('i2c_addr').value='"); + js += config.system.sensor.i2c_addr[tmpParam_editSensorId]; + js += F("';"); + return js; + + + } else { + return String(); + } +} + +String Proc_WebPage_system_sensor_add_POST(const String& var) { + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG); + } else { + return Proc_WebPage_system_sensor_add(var); + } +} + + +void WebPage_system_sensor_add(AsyncWebServerRequest *request) { + if(request->method() == HTTP_POST) { + byte sensorId; + //byte sensorType; + if(request->hasParam("sensorId", true)) { + const AsyncWebParameter* param = request->getParam("sensorId", true); + sensorId = param->value().toInt(); + } + + if(request->hasParam("type", true)) { + const AsyncWebParameter* param = request->getParam("type", true); + byte old_type = config.system.sensor.type[sensorId]; + // put info config struct + config.system.sensor.type[sensorId] = param->value().toInt(); + /* when config changed to a different sensor which is not internal ADC, then need restart */ + if((config.system.sensor.type[sensorId] != old_type) && (config.system.sensor.type[sensorId] != 1 )) + needRestart = true; + + } + + + if(request->hasParam("name", true)) { + const AsyncWebParameter* param = request->getParam("name", true); + strlcpy(config.system.sensor.name[sensorId], param->value().c_str(), sizeof(config.system.sensor.name[sensorId])); + } + + if(request->hasParam("i2c_addr", true)) { + const AsyncWebParameter* param = request->getParam("i2c_addr", true); + //strlcpy(config.system.sensor.i2c_addr[sensorId], param->value().c_str(), sizeof(config.system.sensor.i2c_addr[sensorId])); + byte old_i2c_addr = config.system.sensor.i2c_addr[sensorId]; + config.system.sensor.i2c_addr[sensorId] = param->value().toInt(); + /* when i2c address changes, we need a restart + * TODO or re-initialise the sensor(s) (possible?) */ + // check if sensor is I2C one and i2c_addr differs from before, otherwise we dont care whats inside here + + if((SensorIndex[config.system.sensor.type[sensorId]].type == SENSOR_TYPE_I2C) && (config.system.sensor.i2c_addr[sensorId] != old_i2c_addr)) { + needRestart = true; + /* disable sensor, otherwise ESP will crash */ + sensorStatus[sensorId] = false; + } + } + + if(request->hasParam("gpio", true)) { + const AsyncWebParameter* param = request->getParam("gpio", true); + byte old_gpio = config.system.sensor.gpio[sensorId][0]; + config.system.sensor.gpio[sensorId][0] = param->value().toInt(); + /* when internal ADC we can initialize the sensors here */ + if((SensorIndex[config.system.sensor.type[sensorId]].type == SENSOR_TYPE_INTADC) && (config.system.sensor.gpio[sensorId][0] != old_gpio)) { + + /* TODO Crashes on ESP8266 sometimes, idk why, is OK on ESP32 */ + #ifdef ESP32 + Sensor_Addr_Init_Update(config.system.sensor.type[sensorId], config.system.sensor.gpio[sensorId][0], SENSOR_AIU_MODE_INIT); + sensorStatus[sensorId] = true; + #endif + + #ifdef ESP8266 + /* because on ESP8266 it crashes sometimes, we force a restart until this get fixed */ + needRestart = true; + sensorStatus[sensorId] = false; + #endif + } + } + + + SaveConfig(); + + // request->send_P(200, "text/html", Page_system_output_add_HTML, Proc_WebPage_system_output_add_POST); + // I like it more when user gets redirected to the output overview after saving + request->redirect("/system/sensor/?success"); + } else { + + /* when in edit mode */ + if(request->hasParam("edit")) { + const AsyncWebParameter* param = request->getParam("edit"); + tmpParam_editSensorId = param->value().toInt(); + request->send_P(200, TEXT_HTML, Page_system_sensor_add_HTML, Proc_WebPage_system_sensor_addEdit); + /* if we want to add new sensor, check if a sensor id is available. if not send error */ + } else if(Give_Free_SensorId() > Max_Sensors) { + request->send_P(200, TEXT_HTML, Page_system_sensor_add_HTML_NO_ID_AVAILABLE, Proc_WebPage_system_sensor_add); + /* Otherwise let the user create new sensor */ + } else { + request->send_P(200, TEXT_HTML, Page_system_sensor_add_HTML, Proc_WebPage_system_sensor_add); + } + } +} + + + + + + +/******************************************************************************* + * Subpage sensor calibrate + */ + +/******************************************************************* +String Js_I2cAddr_Array_WebPage_system_sensor_calibrate() { + const static char LogLoc[] PROGMEM = "[Webserver:system:sensor:add:Js_I2cAddr_Array]"; + + String js; + + + for(byte i = 1; i <= SensorIndex_length; i++) { + // name + js += F("["); + + for(byte j = 0; j < SensorIndex[i].max; j++) { + js += F("['0x"); + js += String(Sensor_Addr_Init_Update(i, j, SENSOR_AIU_MODE_ADDR), HEX); + // value + js += F("', '"); + js += j; + // used + bool used; + js += F("', '"); + + for(byte k = 0; k < Max_Sensors; k++) { + if(config.system.sensor.type[k] == i ) { + if(config.system.sensor.i2c_addr[k] == j) { + js += F("1"); + // exit loop here + k = Max_Sensors + 1; + } //else { + //js += "0"; + //} + + } + } + js += F("'],"); + } + js += F("],\n"); + } + + return js; +} + + +String Proc_WebPage_system_sensor_calibrate3(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 2); + } else if(Test_WebPage_system_SUBNAV(var)) { + return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_SENSOR); + } else if(var == "ACTION") { + return F("➕ Add"); + + } else if(var == "SENSOR_ID") { + // we check which id is free. A free ID as type == 0 + return String(Give_Free_SensorId()); + + + } else if(var == "SENSOR_TYPE") { + return Html_SelOpt_type_WebPage_system_sensor_add(); + + } else if(var == "GPIO_INDEX") { + return Html_SelectOpt_GPIOindex(255, true); + + } else if(var == "ESP_PLATFORM") { + #ifdef ESP8266 + return F("8266"); + #endif + + #ifdef ESP32 + return F("32"); + #endif + } else if(var == "REPLACE_I2CADDR_JS") { + + return Js_I2cAddr_Array_WebPage_system_sensor_add(); + + } else { + return String(); + } +} +*******************************************************************/ + +String Proc_WebPage_system_sensor_calibrate(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 2); + } else if(Test_WebPage_system_SUBNAV(var)) { + return Proc_WebPage_system_SUBNAV(var, WEB_SYSTEM_SUBNAV_SENSOR); + } else if(var == "SENSOR_ID") { + // return the sensorId we got from GET .../add?edit=ID + // dirty workaround to put this in a global variable + return String(tmpParam_calibrateSensorId); + + } else if(var == "SENSOR_NAME") { + // "escape" % character, because it would break the template processor. + // tasmote webcall for example has percentage char in its path + String sensorName = config.system.sensor.name[tmpParam_calibrateSensorId];; + sensorName.replace(F("%"), F("%")); + return sensorName; + + } else if(var == "SENSOR_READING") { + String html; + /* TODO the way this page is built is ulgy IMHO + * maybe i will replace everything dynamic at this place with javascript? or i can make + * more / better use of the ESPAsync template engine */ + html += F("<script>SensorJsonRefresh(); var refreshJson = setInterval(SensorJsonRefresh, 1000);</script>"); + for(byte i = 0; i < Max_Sensors_Read; i++) { + if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i] > 0) { + html += F("<form method='post' action='/system/sensor/calibrate'>"); + html += F("<input type='hidden' name='sensorId' value='"); + html += tmpParam_calibrateSensorId; + html += F("'/>"); + html += F("<input type='hidden' name='readId' value='"); + html += i; + html += F("'/>"); + + html += F("<u>Reading "); + html += i; + html += F(":</u><br><b>"); + html += FPSTR(Sensor_Read_descr[SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i]]); + html += F("</b> ("); + html += F("<span class='sensorReading' id='raw-"); + html += tmpParam_calibrateSensorId; + html += "-"; + html += i; + html += F("'>"); + + /* is sensor internal ADC ? */ + if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].type == SENSOR_TYPE_INTADC) { + /* reading type RAW ? */ + if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i] == SENSOR_READ_TYPE_RAW) { + /* get RAW value */ + html += (int)Sensor_getValue( config.system.sensor.type[tmpParam_calibrateSensorId], config.system.sensor.gpio[tmpParam_calibrateSensorId][0]); + } else { + /* print calibrated value if not RAW */ + html += Sensor_getCalibratedValue(tmpParam_calibrateSensorId, i); + //html += Sensor_getValue( config.system.sensor.type[tmpParam_calibrateSensorId], config.system.sensor.gpio[tmpParam_calibrateSensorId][0]); + } + } else if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].type == SENSOR_TYPE_I2C) { + /* same stuff for i2c sensor */ + if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i] == SENSOR_READ_TYPE_RAW) { + html += (int)Sensor_getValue( config.system.sensor.type[tmpParam_calibrateSensorId], config.system.sensor.i2c_addr[tmpParam_calibrateSensorId], i); + } else { + html += Sensor_getCalibratedValue(tmpParam_calibrateSensorId, i); + html += " "; + String unit; + unit += FPSTR(Sensor_Read_unit[SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i]]); + unit.replace(F("%"), F("%")); + html += unit; + //html += Sensor_getValue( config.system.sensor.type[tmpParam_calibrateSensorId], config.system.sensor.i2c_addr[tmpParam_calibrateSensorId], i); + } + } + + html += F("</span>)"); + + if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i] == SENSOR_READ_TYPE_RAW) { + html += F("<script>var raw_"); + html += tmpParam_calibrateSensorId; + html += F("_"); + html += i; + html += F(" = setInterval(rawRefresh, 1000, "); + html += tmpParam_calibrateSensorId; + html += F(", "); + html += i; + html += F(", 'raw-');</script>"); + } + + html += F("<br>"); + + if(SensorIndex[config.system.sensor.type[tmpParam_calibrateSensorId]].read[i] == SENSOR_READ_TYPE_RAW) { + html += F("<u>Convert RAW value to:</u><br>"); + html += F("<select name='rawConvert'><option value='0' >---</option>"); + for(byte j = 1; j <= SENSOR_CONVERT_RAW_TYPE__TOTAL; j++) { + html += F("<option value='"); + html += j; + + html += F("'"); + if(config.system.sensor.rawConvert[tmpParam_calibrateSensorId][i] == j) + html += F(" selected"); + html += F(">"); + html += FPSTR(Sensor_Convert_Raw_descr[j]); + html += F("</option>"); + } + html += F("</select><br>"); + + /* when raw convert is set, display converted reading */ + if(config.system.sensor.rawConvert[tmpParam_calibrateSensorId][i] > 0) { + //html += F("<span class='sensorReading'>"); + html += F("<span class='sensorReading' id='reading-"); + html += tmpParam_calibrateSensorId; + html += "-"; + html += i; + html += F("'>"); + + html += Sensor_getCalibratedValue(tmpParam_calibrateSensorId, i); + html += " "; + String unit; + unit += FPSTR(Sensor_Convert_Raw_unit[config.system.sensor.rawConvert[tmpParam_calibrateSensorId][i]]); + unit.replace(F("%"), F("%")); + html += unit; + html += F("</span>"); + + html += F("<script>var reading_"); + html += tmpParam_calibrateSensorId; + html += F("_"); + html += i; + html += F(" = setInterval(sensorRefresh, 1000, "); + html += tmpParam_calibrateSensorId; + html += F(", "); + html += i; + html += F(", 'reading-');</script>"); + + html += F("<br>"); + } + + html += F("<u>Low:</u><br>"); + html += F("<input type='number' name='low' value='"); + html += config.system.sensor.low[tmpParam_calibrateSensorId][i]; + html += F("'/><br>"); + + html += F("<u>High:</u><br>"); + html += F("<input type='number' name='high' value='"); + html += config.system.sensor.high[tmpParam_calibrateSensorId][i]; + html += F("'/><br>"); + + } else { + + html += F("<u>Offset:</u><br>"); + html += F("<input type='number' step='0.01' name='offset' value='"); + html += config.system.sensor.offset[tmpParam_calibrateSensorId][i]; + html += F("'/><br>"); + + } + html += F("<br><input type='submit' value='💾 Save settings'></form>"); + html += F("<hr>\n\n"); + } + } + + return html; + + } else { + return String(); + } +} + +String Proc_WebPage_system_sensor_calibrate_POST(const String& var) { + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG); + } else { + return Proc_WebPage_system_sensor_calibrate(var); + } +} + + +void WebPage_system_sensor_calibrate(AsyncWebServerRequest *request) { + if(request->method() == HTTP_POST) { + byte sensorId; + byte readId; + //byte sensorType; + if(request->hasParam("sensorId", true)) { + const AsyncWebParameter* param = request->getParam("sensorId", true); + sensorId = param->value().toInt(); + tmpParam_calibrateSensorId = sensorId; + } + + if(request->hasParam("readId", true)) { + const AsyncWebParameter* param = request->getParam("readId", true); + readId = param->value().toInt(); + } + + if(request->hasParam("offset", true)) { + const AsyncWebParameter* param = request->getParam("offset", true); + config.system.sensor.offset[sensorId][readId] = param->value().toFloat(); + } + + if(request->hasParam("low", true)) { + const AsyncWebParameter* param = request->getParam("low", true); + config.system.sensor.low[sensorId][readId] = param->value().toInt(); + } + + if(request->hasParam("high", true)) { + const AsyncWebParameter* param = request->getParam("high", true); + config.system.sensor.high[sensorId][readId] = param->value().toInt(); + } + + if(request->hasParam("rawConvert", true)) { + const AsyncWebParameter* param = request->getParam("rawConvert", true); + config.system.sensor.rawConvert[sensorId][readId] = param->value().toInt(); + } + + SaveConfig(); + + request->send_P(200, "text/html", Page_system_sensor_calibrate_HTML, Proc_WebPage_system_sensor_calibrate_POST); + // I like it more when user gets redirected to the output overview after saving + //request->redirect("/system/sensor/?success"); + } else { + + /* when in edit mode */ + if(request->hasParam("calibrate")) { + const AsyncWebParameter* param = request->getParam("calibrate"); + tmpParam_calibrateSensorId = param->value().toInt(); + request->send_P(200, TEXT_HTML, Page_system_sensor_calibrate_HTML, Proc_WebPage_system_sensor_calibrate); + /* if we want to add new sensor, check if a sensor id is available. if not send error */ + } else { + request->send_P(200, TEXT_HTML, Page_system_sensor_calibrate_HTML, Proc_WebPage_system_sensor_calibrate); + } + } +} + diff --git a/include/Webserver/Page_system_HTML.h b/include/Webserver/Page_system_HTML.h new file mode 100644 index 0000000..792856c --- /dev/null +++ b/include/Webserver/Page_system_HTML.h @@ -0,0 +1,396 @@ +/* + * + * include/Webserver/Page_system_HTML.h - system settings page HTML header file + * + * + * + */ + +/* submenu SUBNAV */ +const char Page_system_HTML_SUBNAV[] PROGMEM = R"(<ul class='subnav'> + <li><a class='%ACTIVE_SUBNAV_GENERAL%' href='/system/'>🛠️ General</a></li> + <li><a class='%ACTIVE_SUBNAV_SENSOR%' href='/system/sensor/'>🌡️ Sensor</a></li> + <li><a class='%ACTIVE_SUBNAV_OUTPUT%' href='/system/output/'>⚡ Output</a></li> + <li><a class='%ACTIVE_SUBNAV_UPDATE%' href='/system/update'>🔄 Firmware update</a></li> + <li><a class='%ACTIVE_SUBNAV_RESTART%' href='/system/restart' >🔁 System restart</a></li> + <li><a class='%ACTIVE_SUBNAV_WIPE%' href='/system/wipe' >💣 Factory reset</a></li> +</ul>)"; + +/* /system main page */ +const char Page_system_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +%SAVE_MSG% +<p>here you can set which features and sensors you use<br></p><form method='post' action='/system/'> + +<u>NTP offset</u>:<br> +<input class='inputShort' type='number' name='ntpOffset' min='-12' max='14' value='%NTPOFFSET%' required> Hours<br> + +<u>Maintenance duration</u>:<br> <input class='inputShort' type='number' name='maintenanceDuration' min='0' max='900' value='%MAINTDUR%' required> Seconds<br> + +<u>ESP32-Cam IP (optional)</u>:<br> +<input type='text' name='esp32cam' maxlength='16' value='%ESP32CAM%' ><br> + +<u>HTTP log to serial</u>:<br> +<select name='httpLogSerial' required> +<option disabled value='' selected hidden>---</option> +%HTTPLOGSERIAL% +</select><br> + +<u>I2C RTC</u>:<br> +%RTC_STATUS%<select name='rtc' required> +<option value='0' selected >---</option> +%RTC_AVAILABLE% +</select><br> + +<u>Save time to LittleFS</u>:<br> +<select name='time2fs' required> +<option disabled value='' selected hidden>---</option> +%TIME2FS% +</select><br> + +<u>PWM Frequency</u>:<br> +<input type='number' name='pwmFreq' min='0' max='65535' value='%PWMFREQ%'> +</select><br> + + +<br> + +<input type='submit' value='💾 Save settings'> +</form> +%FOOTER% )"; + + + +/******************************************************************************* + * Subpage update + */ +const char Page_system_update_HTML[] PROGMEM = R"(%HEADER% + +%SUBNAV% +Version: %CGVER% <br> +Build : %CGBUILD% <br> + +<p>You find the latest CanGrow firmware version on the <a href='https://git.la10cy.net/DeltaLima/CanGrow/releases' target='_blank'>release page</a> of the git repository.</p> +<form method='POST' action='/system/update' enctype='multipart/form-data' onsubmit="document.getElementById('divUploading').style.display = '';"> + <b>Select .bin file:</b><br> + <input type='file' accept='.bin,.bin.gz' name='firmware' required> + <input type='submit' value='Update Firmware'> +</form> +<div id='divUploading' style='display: none;' class='warnmsg'>🛜 Uploading, please wait...</div> + +%FOOTER% )"; + +const char Page_system_update_HTML_POST[] PROGMEM = R"(<html> +<head><meta http-equiv='refresh' content='20;url=/' /></head> +<body><h1>Successfully updated! +<br>Restarting...</h1><br> +Redirecting to "/" in 20 seconds +</body></html>)"; + +const char Page_system_update_HTML_POST_FAILED[] PROGMEM = R"(<html> +<head></head><body><h1>UPDATE FAILED</h1><br> +Please see messages on serial monitor for more information and go back to <a href='/system/update'>\"System settings > Firmware update\"</a> and try another file. +</body></html>)"; + +/******************************************************************************* + * Subpage restart + */ +const char Page_system_restart_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +<div class='warnmsg'> +%RESTART_MSG% +</div> +%FOOTER% )"; + +const char Page_system_restart_HTML_RESTART_MSG[] PROGMEM = R"(Do you want to restart CanGrow?<br>Please confirm. +<form action='/system/restart' method='post'><input type='hidden' name='confirmed' value='true' /> +<input type='submit' value='Confirm restart' /> +</form>)"; + +const char Page_system_restart_HTML_RESTART_MSG_POST[] PROGMEM = R"(Restarting...<br><span style='helpbox'>Redirecting to root page in 20 seconds.</span>)"; + + + + +/******************************************************************************* + * Subpage wipe + */ +const char Page_system_wipe_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +<div class='warnmsg'> +%WIPE_MSG% +</div> + +%FOOTER% )"; + +const char Page_system_wipe_HTML_WIPE_MSG[] PROGMEM = R"(All settings will be removed!!<br><br> +Please confirm wiping LittleFS +<form action='/system/wipe' method='post'><br> +Please confirm: <input type='checkbox' id='confirmed' name='confirmed' required /><br> +<input type='submit' value='Confirm wiping' /> +</form>)"; + + +const char Page_system_wipe_HTML_WIPE_MSG_POST[] PROGMEM = R"(Restarting...)"; + +/******************************************************************************* + * Subpage output + */ +const char Page_system_output_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +%SAVE_MSG% +<a class='button %ADD_DISABLED%' href='/system/output/add'>➕ Add output</a> +<table class='centered'> + <tr> + <th> </th> + <th>ID</th> + <th>Name</th> + <th>Type</th> + <th>Device</th> + <th> </th> + <th>Action</th> + </tr> + %TR_TD% +</table> +%FOOTER% )"; + +/******************************************************************************* + * Subpage output add + */ +const char Page_system_output_add_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +<h3>%ACTION% output ID %OUTPUT_ID%</h3> +%SAVE_MSG% + +<p>Add/Edit CanGrow output.</p> +<form method='post' action='/system/output/add?%EDIT_MODE%'> +<input type='hidden' name='outputId' value='%OUTPUT_ID%' /> + +<u>Type</u>:<br> +<select id='type_sel' name='type' onchange="showSelect('type_sel', 'type_', 'hidden'); SystemOutputAddselectRequired('type_sel');" required> + <option disabled value='' selected hidden>---</option> + %OUTPUT_TYPE% +</select><br> + +<u>Device</u>:<br> +<select name='device' required> + <option disabled value='' selected hidden>---</option> + %OUTPUT_DEVICE% +</select><br> + +<u>Name</u>:<br> +<input type='text' name='name' maxlength='32' value='%OUTPUT_NAME%' required><br> + +<u>Enable</u>:<br> +<select name='enabled' required> + <option disabled value='' selected hidden>---</option> + %OUTPUT_ENABLED% +</select><br> + +<u>Invert</u>:<br> +<select name='invert' required> + <option disabled value='' selected hidden>---</option> + %INVERT% +</select><br> + + +<div class='hidden %CLASS_TYPE_1%' id='type_1'> + <u>GPIO</u>:<br> + <select id='gpio' name='gpio'> + <option disabled value='' selected hidden>---</option> + %GPIO_INDEX% + </select><br> + + <u>GPIO PWM</u>:<br> + <select id='gpio_pwm' name='gpio_pwm' > + <option disabled value='' selected hidden>---</option> + %GPIO_PWM% + </select><br> + +</div> + + +<div class='hidden ' id='type_2'> + <u>I2C type</u>:<br> + <select id='i2c_type' name='i2c_type' onchange="SystemOutputAdd_replaceI2cAddr('i2c_type', 'i2c_addr');"> + <option disabled value='' selected hidden>---</option> +%I2C_TYPE% + </select><br> + <u>I2C address</u>:<br> + <select id='i2c_addr' name='i2c_addr' onchange="SystemOutputAdd_replaceI2cPort('i2c_type', 'i2c_addr', 'i2c_port');"> + <option value='' selected >---</option> + </select><br> + <u>I2C module port</u>:<br> + <select id='i2c_port' name='i2c_port'> + <option value='' selected >---</option> + </select><br> +</div> + +<div class='hidden %CLASS_TYPE_3%' id='type_3'> + <u>Webcall host</u>:<br> + <input id='webcall_host' type='text' name='webcall_host' maxlength='32' value='%WEBCALL_HOST%' ><br> + + <u>Webcall path 'on'</u>:<br> + <input id='webcall_path_on' type='text' name='webcall_path_on' maxlength='32' value='%WEBCALL_PATH_ON%' ><br> + + <u>Webcall path 'off'</u>:<br> + <input id='webcall_path_off' type='text' name='webcall_path_off' maxlength='32' value='%WEBCALL_PATH_OFF%' ><br> +</div> + +<br> + +<input type='submit' value='💾 Save settings'> +</form> + +<script> + var addr = [ [] , +%REPLACE_I2CADDR_JS% ]; + +%I2C_SAVED% +</script> + + +%FOOTER% )"; + +const char Page_system_output_add_HTML_NO_ID_AVAILABLE[] PROGMEM = R"(%HEADER% +%SUBNAV% +<h3>You cannot create more outputs, limit reached.</h3> +%FOOTER% )"; + + + + + + + + + +/******************************************************************************* + * Subpage sensor + */ +const char Page_system_sensor_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +%SAVE_MSG% +<a class='button %ADD_DISABLED%' href='/system/sensor/add'>➕ Add sensor</a> +<table class='centered'> + <tr> + <th> </th> + <th>ID</th> + <th>Name</th> + <th>Type</th> + </tr> + %TR_TD% +</table> +%FOOTER% )"; + +/******************************************************************************* + * Subpage sensor add + */ +const char Page_system_sensor_add_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +<h3>%ACTION% sensor ID %SENSOR_ID%</h3> +%SAVE_MSG% + +<p>Add/Edit CanGrow sensor.</p> +<form method='post' action='/system/sensor/add'> +<input type='hidden' name='sensorId' value='%SENSOR_ID%' /> +<u>Type</u>:<br> +<select id='type_sel' name='type' onchange="SystemSensorAddGpioI2cSel('type_sel'); SystemSensor_replaceAddr('type_sel', 'i2c_addr');" required> + <option disabled value='' selected hidden>---</option> + %SENSOR_TYPE% +</select><br> + + + +<u>Name</u>:<br> +<input type='text' name='name' maxlength='32' value='%SENSOR_NAME%' required><br> + + +<div class='hidden %CLASS_TYPE_1%' id='type_1'> + <u>GPIO</u>:<br> + <select id='gpio' name='gpio'> + <option value='' selected >---</option> + %GPIO_INDEX% + </select><br> +</div> + + +<div class='hidden %CLASS_TYPE_2%' id='type_2'> + <u>I2C address</u>:<br> + <select id='i2c_addr' name='i2c_addr' required> + <option value='' selected >---</option> + </select><br> +</div> + +<div class='hidden %CLASS_TYPE_3% %CLASS_TYPE_1%' id='type_3'> + <span>Special 3</span> +</div> + +<!-- Sensor reading calibration --> + + +<br> + +<input type='submit' value='💾 Save settings'> +</form> + +<script> + var ESP = '%ESP_PLATFORM%'; + // https://stackoverflow.com/a/67412019 + function SystemSensor_replaceAddr(selectId, replaceId) { + var sel = document.querySelector('#' + replaceId); + let selVal = document.getElementById(selectId).value; + // Remove existing options + Array.from(sel).forEach((option) => { + sel.removeChild(option) + }) + + var addr = [ [] , +%REPLACE_I2CADDR_JS% + ] + + addr[selVal].map((optionData) => { + let opt = document.createElement('option') + opt.appendChild(document.createTextNode(optionData[0])); + opt.value = optionData[1] + if(optionData[2] > 0) { + opt.disabled = true + } + sel.appendChild(opt); + }) + } + +%I2C_SAVED% +</script> + +%FOOTER% )"; + +const char Page_system_sensor_add_HTML_NO_ID_AVAILABLE[] PROGMEM = R"(%HEADER% +%SUBNAV% +<h3>You cannot create more sensors, limit reached.</h3> +%FOOTER% )"; + + + + + +/******************************************************************************* + * Subpage sensor calibrate + */ +const char Page_system_sensor_calibrate_HTML[] PROGMEM = R"(%HEADER% +%SUBNAV% +<h3>🎛️ Calibrate sensor ID %SENSOR_ID% (%SENSOR_NAME%)</h3> +%SAVE_MSG% + +<p>Calibrate CanGrow sensor.</p> + + +%SENSOR_READING% + + + +<script> + +</script> + +%FOOTER% )"; diff --git a/include/Webserver/Page_wifi.h b/include/Webserver/Page_wifi.h new file mode 100644 index 0000000..088b246 --- /dev/null +++ b/include/Webserver/Page_wifi.h @@ -0,0 +1,197 @@ +/* + * + * include/Webserver/Page_wifi.h - wifi page header file + * + * + * + */ + + +#include "Page_wifi_HTML.h" + +String WebPage_wifi_ScanNetworks() { + const static char LogLoc[] PROGMEM= "[Webserver:wifi:ScanNetworks]"; + String html; + #ifdef DEBUG + Log.verbose(F("%s scanning for available networks:" CR), LogLoc); + #endif + // https://github.com/mathieucarbou/ESPAsyncWebServer/blob/main/docs/index.md#scanning-for-available-wifi-networks + int n = WiFi.scanComplete(); + if(n == -2){ + WiFi.scanNetworks(true); + } else if(n){ + for (int i = 0; i < n; ++i){ + html += F("<option value='"); + html += WiFi.SSID(i); + html += F("'>"); + html += WiFi.SSID(i); + html += F("</option>"); + /* dirty hack, arduino-log somehow destroys wifi names in output + /* so i have to print them oldschool with Serial.println + */ + #ifdef DEBUG + Log.verbose(F("%s - "), LogLoc); + Serial.println(WiFi.SSID(i)); + #endif + } + WiFi.scanDelete(); + if(WiFi.scanComplete() == -2){ + WiFi.scanNetworks(true); + } + } + return html; +} + +// https://techtutorialsx.com/2018/07/23/esp32-arduino-http-server-template-processing-with-multiple-placeholders/ +String Proc_WebPage_wifi(const String& var) { + if(TestHeaderFooter(var)) { + return AddHeaderFooter(var, 3); + //CURRENT_SETTINGS + } else if(var == "CURRENT_SETTINGS") { + if(strlen(config.wifi.ssid) > 0) { + return String(Page_wifi_HTML_CURRENT_SETTINGS); + } else { + return String(); + } + } else if(var == "CONFIGWIFI_SSID") { + return String(config.wifi.ssid); + } else if(var == "CONFIGWIFI_DHCP") { + return String(config.wifi.dhcp); + } else if(var == "CONFIGWIFI_IP") { + return String(WiFi.localIP().toString()); + } else if(var == "CONFIGWIFI_NETMASK") { + return String(WiFi.subnetMask().toString()); + } else if(var == "CONFIGWIFI_GATEWAY") { + return String(WiFi.gatewayIP().toString()); + } else if(var == "CONFIGWIFI_DNS") { + return String(WiFi.dnsIP().toString()); + } else if(var == "WIFI_LIST") { + return String(WebPage_wifi_ScanNetworks()); + } else { + return String(); + } +} + + +String Proc_WebPage_wifi_POST(const String& var) { + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG); + } else { + return Proc_WebPage_wifi(var); + } +} + +String Proc_WebPage_wifi_POST_ERR(const String& var) { + if(var == "SAVE_MSG") { + return String(Common_HTML_SAVE_MSG_ERR); + } else { + return Proc_WebPage_wifi(var); + } +} + +void WebPage_wifi(AsyncWebServerRequest *request) { + const static char LogLoc[] PROGMEM = "[Webserver:wifi]"; + + if(request->method() == HTTP_POST) { + if(request->hasParam("config.wifi.ssid", true)) { + const AsyncWebParameter* p_ssid = request->getParam("config.wifi.ssid", true); + strlcpy(config.wifi.ssid, p_ssid->value().c_str(), sizeof(config.wifi.ssid)); + + } + + if(request->hasParam("config.wifi.password", true)) { + const AsyncWebParameter* p_password = request->getParam("config.wifi.password", true); + strlcpy(config.wifi.password, p_password->value().c_str(), sizeof(config.wifi.password)); + } + + + if( + (request->hasParam("config.wifi.ip0", true)) && + (request->hasParam("config.wifi.ip1", true)) && + (request->hasParam("config.wifi.ip2", true)) && + (request->hasParam("config.wifi.ip3", true))) { + + const AsyncWebParameter* p_ip0 = request->getParam("config.wifi.ip0", true); + const AsyncWebParameter* p_ip1 = request->getParam("config.wifi.ip1", true); + const AsyncWebParameter* p_ip2 = request->getParam("config.wifi.ip2", true); + const AsyncWebParameter* p_ip3 = request->getParam("config.wifi.ip3", true); + + config.wifi.ip[0] = p_ip0->value().toInt(); + config.wifi.ip[1] = p_ip1->value().toInt(); + config.wifi.ip[2] = p_ip2->value().toInt(); + config.wifi.ip[3] = p_ip3->value().toInt(); + } + + + if( + (request->hasParam("config.wifi.netmask0", true)) && + (request->hasParam("config.wifi.netmask1", true)) && + (request->hasParam("config.wifi.netmask2", true)) && + (request->hasParam("config.wifi.netmask3", true))) { + + const AsyncWebParameter* p_netmask0 = request->getParam("config.wifi.netmask0", true); + const AsyncWebParameter* p_netmask1 = request->getParam("config.wifi.netmask1", true); + const AsyncWebParameter* p_netmask2 = request->getParam("config.wifi.netmask2", true); + const AsyncWebParameter* p_netmask3 = request->getParam("config.wifi.netmask3", true); + + config.wifi.netmask[0] = p_netmask0->value().toInt(); + config.wifi.netmask[1] = p_netmask1->value().toInt(); + config.wifi.netmask[2] = p_netmask2->value().toInt(); + config.wifi.netmask[3] = p_netmask3->value().toInt(); + } + + if( + (request->hasParam("config.wifi.gateway0", true)) && + (request->hasParam("config.wifi.gateway1", true)) && + (request->hasParam("config.wifi.gateway2", true)) && + (request->hasParam("config.wifi.gateway3", true))) { + + const AsyncWebParameter* p_gateway0 = request->getParam("config.wifi.gateway0", true); + const AsyncWebParameter* p_gateway1 = request->getParam("config.wifi.gateway1", true); + const AsyncWebParameter* p_gateway2 = request->getParam("config.wifi.gateway2", true); + const AsyncWebParameter* p_gateway3 = request->getParam("config.wifi.gateway3", true); + + config.wifi.gateway[0] = p_gateway0->value().toInt(); + config.wifi.gateway[1] = p_gateway1->value().toInt(); + config.wifi.gateway[2] = p_gateway2->value().toInt(); + config.wifi.gateway[3] = p_gateway3->value().toInt(); + } + + if( + (request->hasParam("config.wifi.dns0", true)) && + (request->hasParam("config.wifi.dns1", true)) && + (request->hasParam("config.wifi.dns2", true)) && + (request->hasParam("config.wifi.dns3", true))) { + + const AsyncWebParameter* p_dns0 = request->getParam("config.wifi.dns0", true); + const AsyncWebParameter* p_dns1 = request->getParam("config.wifi.dns1", true); + const AsyncWebParameter* p_dns2 = request->getParam("config.wifi.dns2", true); + const AsyncWebParameter* p_dns3 = request->getParam("config.wifi.dns3", true); + + config.wifi.dns[0] = p_dns0->value().toInt(); + config.wifi.dns[1] = p_dns1->value().toInt(); + config.wifi.dns[2] = p_dns2->value().toInt(); + config.wifi.dns[3] = p_dns3->value().toInt(); + } + + if(request->hasParam("config.wifi.dhcp", true)) { + const AsyncWebParameter* p_dhcp = request->getParam("config.wifi.dhcp", true); + + config.wifi.dhcp = p_dhcp->value().toInt(); + } + + if(SaveConfig()) { + // we need a restart to apply the new settings + needRestart = true; + Log.notice(F("%s config saved" CR), LogLoc); + request->send_P(200, TEXT_HTML, Page_wifi_HTML, Proc_WebPage_wifi_POST); + } else { + Log.error(F("%s ERROR while saving config" CR), LogLoc); + request->send_P(200, TEXT_HTML, Page_wifi_HTML, Proc_WebPage_wifi_POST_ERR); + } + + } else { + request->send_P(200, TEXT_HTML, Page_wifi_HTML, Proc_WebPage_wifi); + } + +} diff --git a/include/Webserver/Page_wifi_HTML.h b/include/Webserver/Page_wifi_HTML.h new file mode 100644 index 0000000..63bb34a --- /dev/null +++ b/include/Webserver/Page_wifi_HTML.h @@ -0,0 +1,72 @@ +/* + * + * include/Webserver/Page_wifi_HTML.h - wifi page HTML header file + * + * + * + */ + +const char Page_wifi_HTML[] PROGMEM = R"(%HEADER% +%SAVE_MSG% +%CURRENT_SETTINGS% + +<p>Select your wifi network from the SSID list. +<br>Reload the page, if your network is not listed.</p> +<form method='post' action='/wifi/'> + +<u>SSID</u>:<br> +<select id='config.wifi.ssid' name='config.wifi.ssid' required> +<option disabled value='' selected hidden>-Select your network-</option> + +%WIFI_LIST% + +</select><br> + +<u>Password</u>:<br> +<input type='password' name='config.wifi.password'><br> + +<u>DHCP</u>:<br> +<select id='dhcp_sel' name='config.wifi.dhcp' onchange="showSelect('dhcp_sel', 'dhcp_', 'hidden');" required> +<option disabled value='' selected hidden>---</option> +<option value='1'>On</option> +<option value='0'>Off</option> +</select><br> + +<div class='hidden' id='dhcp_0'> + <u>IP</u>:<br> + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.ip0'> . + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.ip1'> . + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.ip2'> . + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.ip3'><br> + + <u>Netmask</u>:<br> + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.netmask0'> . + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.netmask1'> . + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.netmask2'> . + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.netmask3'><br> + + <u>Gateway</u>:<br> + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.gateway0'> . + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.gateway1'> . + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.gateway2'> . + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.gateway3'><br> + + <u>DNS</u>:<br> + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.dns0'> . + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.dns1'> . + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.dns2'> . + <input class='inputShort' type='number' min='0' max='255' name='config.wifi.dns3'><br> +</div> +<br> +<input type='submit' value='💾 Save settings'> +</form> +%FOOTER% )"; + +const char Page_wifi_HTML_CURRENT_SETTINGS[] PROGMEM = R"(<b><u>Current Settings:</u></b><br>WiFi SSID: <b>%CONFIGWIFI_SSID%</b><br> +Use DHCP: <b>%CONFIGWIFI_DHCP%</b><br> +IP address: <b>%CONFIGWIFI_IP%</b><br> +Subnet mask: <b>%CONFIGWIFI_NETMASK%</b><br> +Gateway: <b>%CONFIGWIFI_GATEWAY%</b><br> +DNS: <b>%CONFIGWIFI_DNS%</b><br><br>)"; + + diff --git a/include/Webserver/Webserver_Common.h b/include/Webserver/Webserver_Common.h new file mode 100644 index 0000000..78fe702 --- /dev/null +++ b/include/Webserver/Webserver_Common.h @@ -0,0 +1,190 @@ +/* + * + * include/Webserver/Common.h - header file with common webserver functions + * HTML header or footer to a String() + * + * + * + */ + +#include "Webserver_Common_HTML.h" + +/* + * global char constants for various HTML tags and stuff + * + */ + +/* return type */ +const char TEXT_HTML[] PROGMEM = "text/html"; + + +/* + * TestHeaderFooter - checks if the given var from the webserver processor + * is actual a template variable from header or footer. + */ +bool TestHeaderFooter(const String& var) { + const static char LogLoc[] PROGMEM = "[Webserver:Common:TestHeaderFooter]"; + #ifdef DEBUG3 + Log.verbose(F("%s var: %s" CR), LogLoc, var); + #endif + if( + (var == "HEADER") || + (var == "FOOTER") || + (var == "CGVER") || + (var == "CGBUILD") || + (var == "GROWNAME") || + (var == "CANGROW_CSS") || + (var == "TIME") || + (var == "NEED_RESTART") || + (var == "ACTIVE_NAV_GROW") || + (var == "ACTIVE_NAV_SYSTEM") || + (var == "ACTIVE_NAV_WIFI") || + (var == "ACTIVE_NAV_HELP") || + (var == "PLACEHOLDER")) { + return true; + } else { + return false; + } +} + + +/* + * AddHeaderFooter - processor for header and footer template variables + * + * String& var: + * the string we receive from the processor is the actual + * variable name we replace here. + * byte activeNav: + * contains the number representing which page is active + * 1 - grow settings + * 2 - system settings + * 3 - wifi settings + * 4 - help page + */ +String AddHeaderFooter(const String& var, byte activeNav = 0) { + String activeNav_ClassName = F("activeNav"); + if(var == "HEADER") { + return String(Header_HTML); + } else if(var == "FOOTER") { + return String(Footer_HTML); + } else if(var == "CGVER") { + return String(CANGROW_VER); + } else if(var == "CGBUILD") { + return String(CANGROW_BUILD); + } else if(var == "GROWNAME") { + return String(config.grow.name); + } else if(var == "CANGROW_CSS") { + return String(File_cangrow_CSS); + } else if(var == "TIME") { + return Str_TimeNow(); + } else if((var == "ACTIVE_NAV_GROW") && (activeNav == 1)) { + return activeNav_ClassName; + } else if((var == "ACTIVE_NAV_SYSTEM") && (activeNav == 2)) { + return activeNav_ClassName; + } else if((var == "ACTIVE_NAV_WIFI") && (activeNav == 3)) { + return activeNav_ClassName; + } else if((var == "ACTIVE_NAV_HELP") && (activeNav == 4)) { + return activeNav_ClassName; + } else if(var == "NEED_RESTART") { + if(needRestart == true) { + return String(Common_HTML_NEED_RESTART); + } else { + return String(); + } + + } else { + return String(); + } +} + + +/* + * Html_SelectOpt_GPIOindex + * + * returns <option> list as string with available gpios + */ + +String Html_SelectOpt_GPIOindex(byte selectId = 255, bool input = false) { + + String gpioIndex_html; + // iterate through through all available GPIOs in index + for(byte i = 1; i <= GPIOindex_length; i++) { + bool gpioUsed = Check_GPIOindex_Used(i); + + gpioIndex_html += F("<option value='"); + gpioIndex_html += i; + gpioIndex_html += F("'"); + // set disabled option for gpio which are already in use or incompatible // or only inputs when configuring sensor ADC + // || ( (input == true) && ((GPIOindex[i].note != INPUT_ONLY) || (GPIOindex[i].note != INT_ADC)) ) + /* when GPIO is already in use AND not selected OR + * input is false AND GPIO is Input only OR + * input is true AND GPIO is not INT_ADC AND not INPUT_only*/ + if( ((gpioUsed == true) && (i != selectId)) || ((input == false) && (GPIOindex[i].note == INPUT_ONLY)) + #ifdef ESP32 + /* If we are on ESP32, we check our input GPIOs - we dont need this on ESP8266, as it only has 1 ADC */ + || ((input == true) && ((GPIOindex[i].note != INT_ADC) && GPIOindex[i].note != INPUT_ONLY)) + #endif + ) { + //|| ((input == true) && ((GPIOindex[i].note != INPUT_ONLY) || (GPIOindex[i].note != INT_ADC) )) + gpioIndex_html += F(" disabled"); + } + + if(i == selectId) { + gpioIndex_html += F(" selected"); + } + gpioIndex_html += F(">GPIO "); + gpioIndex_html += GPIOindex[i].gpio; + //add gpio note if there is some + //if(GPIOindex[i].note > 0) { + gpioIndex_html += F(" "); + gpioIndex_html += FPSTR(GPIO_Index_note_descr[GPIOindex[i].note]); + + // disable output incompatible gpio + if((GPIOindex[i].note == INPUT_ONLY) && (input == false)) { + gpioIndex_html += F(" (N/A)"); + // add USED if gpio is already in use + } else if((gpioUsed == true) && (i != selectId)) { + gpioIndex_html += F(" (used)"); + } + gpioIndex_html += F("</option>"); + } + return gpioIndex_html; +} + + +String Html_SelectOpt_bool(byte selectVal = 255, String trueStr = "Yes", String falseStr = "No") { + String html; + html += F("<option value='1'"); + html += ((selectVal > 0) && (selectVal < 255)) ? F(" selected") : F(""); + html += F(">"); + html += trueStr; + html += F("</option>"); + + html += F("<option value='0'"); + html += (selectVal == 0) ? F(" selected") : F(""); + html += F(">"); + html += falseStr; + html += F("</option>"); + return html; +} + + +String Html_SelectOpt_array(byte total, const char * descr[] , byte selectVal = 255) { + const static char LogLoc[] PROGMEM= "[Webserver:Common:Html_Select_Opt_array]"; + String html; + // go through all available array entries, skip 0 because it means unconfigured + for(byte i = 1; i <= total; i++) { + //Log.notice(F("%s i: %d selectVal: %d descr: %S" CR), LogLoc, i, selectVal, descr[i]); + html += F("<option value='"); + html += i; + html += F("'"); + if(i == selectVal) { + html += F(" selected"); + } + html += F(">"); + // use FPSTR because our descr is stored in PROGMEM + html += FPSTR(descr[i]); + html += F("</option>"); + } + return html; +} diff --git a/include/Webserver/Webserver_Common_HTML.h b/include/Webserver/Webserver_Common_HTML.h new file mode 100644 index 0000000..1d263b8 --- /dev/null +++ b/include/Webserver/Webserver_Common_HTML.h @@ -0,0 +1,26 @@ +/* + * + * include/Webserver/Common_HTML.h - header file with common HTML snippets + * HTML header or footer to a String() + * + * + * + */ + + +// double div to force a linebreak. infomsg , warnmsg are inline-block +const char Common_HTML_SAVE_MSG[] PROGMEM = R"EOF( +<div><div class='infomsg'>✅ Successfully saved!</div></div> +)EOF"; + +const char Common_HTML_SAVE_MSG_ERR[] PROGMEM = R"EOF( +<div><div class='infomsg'>!! ERROR saving!</div></div> +)EOF"; + +const char Common_HTML_NEED_RESTART[] PROGMEM = R"EOF( +<div><div class='warnmsg'>❗ Restart is required to apply new settings! +<form action='/system/restart' method='post'><input type='hidden' name='confirmed' value='true' /> + <input type='submit' value='Restart now' /> +</form> +</div></div> +)EOF";