removed KiCad files, moved Arduino source code files

PCB lives now in its own git repo https://git.la10cy.net/DeltaLima/CanGrow-12V-PCB
This commit is contained in:
DeltaLima 2025-03-21 00:13:15 +01:00
parent 6ca2cb5545
commit 28df687bf9
54 changed files with 10239 additions and 81 deletions

61
CanGrow.geany Normal file
View file

@ -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=

216
CanGrow.ino Normal file
View file

@ -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;
}
}

121
README.md
View file

@ -1,94 +1,53 @@
# CanGrow # CanGrow - An OpenSource grow controller firmware for ESP8266 / ESP32
![Screenshot_montage.png](Screenshot_montage.png)
![CanGrowLogo](Logo/CanGrow_Logo.png)
## 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:
![Screenshot_WebUI_root.png](Arduino/CanGrow/Screenshot_WebUI_root.png) ```sh
![CanGrow_PCB_Front.png](KiCad/CanGrow/CanGrow_PCB_Front_small.png) $ ./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 The script installs [arduino-cli](https://github.com/arduino/arduino-cli) to `~/.local/bin/arduino-cli`.
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.
### 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. ```sh
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. # compile and output to build/CanGrow_v0.2...bin
So I decided to completely rewrite the code from 0 - with recycling some parts of it. # Default Target is ESP8266 D1 Mini
Goal of the Rewrite is that the Firmware becomes more independent of the hardware used. It has to support both ESP8266 and ESP32 $ ./cangrow.sh build
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.
**Checklist for v0.2 Firmware** # Compile for ESP32 D1 Mini
- Support ESP8266 and ESP32 $ export BOARD="esp32:esp32:d1_mini32"
- AsyncWebserver instead ESP8266Webserver $ ./cangrow.sh build
- 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
# Build and webupload to IP
$ export IP="192.168.4.69"
## Old v0.1 Features / ToDo List $ ./cangrow.sh build # need to make .bin first
$ ./cangrow.sh webupload # upload
- Measure values :white_check_mark: # listen to serial monitor on /dev/ttyUSB2
- Humidity :white_check_mark: $ export TTY="/dev/ttyUSB2"
- soil moisture :white_check_mark: ./cangrow.sh monitor
- 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:
: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.

BIN
Screenshot_montage.png Normal file

Binary file not shown.

After

(image error) Size: 940 KiB

10
allbuild.sh Executable file
View file

@ -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

4
arduino-cli.yml Normal file
View file

@ -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

167
cangrow.sh Executable file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
*
*/

411
include/CanGrow.h Normal file
View file

@ -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];

View file

@ -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;
}

407
include/CanGrow_Control.h Normal file
View file

@ -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 */
}
}

642
include/CanGrow_Core.h Normal file
View file

@ -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;
}
}

855
include/CanGrow_LittleFS.h Normal file
View file

@ -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

41
include/CanGrow_Logo.h Normal file
View file

@ -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
};

540
include/CanGrow_Output.h Normal file
View file

@ -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
}

758
include/CanGrow_Sensor.h Normal file
View file

@ -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
}

61
include/CanGrow_Timer.h Normal file
View file

@ -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);
}

131
include/CanGrow_Webserver.h Normal file
View file

@ -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);
}

136
include/CanGrow_Wifi.h Normal file
View file

@ -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();
}
}

View file

@ -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 = {"&#x1F4A1; Light"};
const char OUTPUT_DEVICE_FAN_descr[] PROGMEM = {"&#x1F300; Fan"};
const char OUTPUT_DEVICE_PUMP_descr[] PROGMEM = {"&#x1F4A7; Pump"};
const char OUTPUT_DEVICE_HUMIDIFIER_descr[] PROGMEM = {"&#x1F300; Humidifier"};
const char OUTPUT_DEVICE_DEHUMIDIFIER_descr[] PROGMEM = {"&#x1F300; Dehumidifier"};
const char OUTPUT_DEVICE_HEATING_descr[] PROGMEM = {"&#x1F300; 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,
};

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

43
include/Sensor/04_SHT3x.h Normal file
View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

88
include/Sensor/09_Chirp.h Normal file
View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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>)";

View file

@ -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='/'>&#x1F331; %GROWNAME%</a></li>
<li><a class='%ACTIVE_NAV_GROW%' href='/grow/' >&#128262; Grow settings</a></li>
<li><a class='%ACTIVE_NAV_SYSTEM%' href='/system/' >&#9881; System settings</a></li>
<li><a class='%ACTIVE_NAV_WIFI%' href='/wifi/' >&#128225; WiFi settings</a></li>
<li><a class='%ACTIVE_NAV_HELP%' href='/help' >&#x2753; 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%)";

View file

@ -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>&#10071; &#65039; 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
*/

File diff suppressed because it is too large Load diff

View file

@ -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/'>&#x1F6E0;&#xFE0F; General</a></li>
<li><a class='%ACTIVE_SUBNAV_LIGHT%' href='/grow/light/'>&#x1F4A1; Light</a></li>
<li><a class='%ACTIVE_SUBNAV_AIR%' href='/grow/air/'>&#x1F300; Air</a></li>
<li><a class='%ACTIVE_SUBNAV_WATER%' href='/grow/water/'>&#x1F4A7; Water</a></li>
<li><a class='%ACTIVE_SUBNAV_DASHBOARD%' href='/grow/dashboard/' >&#x1F5A5;&#xFE0F; 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='&#x1F4BE; 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% )";

View file

@ -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);
}

View file

@ -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>&#x1F331; Hello world!</h2>
<a href='/api/sensor/'>Sensor data -> /api/sensor/</a>
%FOOTER% )EOF";

File diff suppressed because it is too large Load diff

View file

@ -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/'>&#x1F6E0;&#xFE0F; General</a></li>
<li><a class='%ACTIVE_SUBNAV_SENSOR%' href='/system/sensor/'>&#x1F321;&#xFE0F; Sensor</a></li>
<li><a class='%ACTIVE_SUBNAV_OUTPUT%' href='/system/output/'>&#9889; Output</a></li>
<li><a class='%ACTIVE_SUBNAV_UPDATE%' href='/system/update'>&#x1F504; Firmware update</a></li>
<li><a class='%ACTIVE_SUBNAV_RESTART%' href='/system/restart' >&#x1F501; System restart</a></li>
<li><a class='%ACTIVE_SUBNAV_WIPE%' href='/system/wipe' >&#x1F4A3; 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='&#x1F4BE; 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'>&#x1F6DC; 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'>&#10133; Add output</a>
<table class='centered'>
<tr>
<th>&nbsp;</th>
<th>ID</th>
<th>Name</th>
<th>Type</th>
<th>Device</th>
<th>&nbsp;</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='&#x1F4BE; 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'>&#10133; Add sensor</a>
<table class='centered'>
<tr>
<th>&nbsp;</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='&#x1F4BE; 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>&#x1F39B;&#xFE0F; Calibrate sensor ID %SENSOR_ID% (%SENSOR_NAME%)</h3>
%SAVE_MSG%
<p>Calibrate CanGrow sensor.</p>
%SENSOR_READING%
<script>
</script>
%FOOTER% )";

View file

@ -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);
}
}

View file

@ -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='&#x1F4BE; 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>)";

View file

@ -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;
}

View file

@ -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'>&#x2705; 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'>&#10071; 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";