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)
![CanGrow_PCB_Front.png](KiCad/CanGrow/CanGrow_PCB_Front_small.png)
```sh
$ ./cangrow.sh help
./cangrow.sh [setup|build|upload|webupload|monitor]
setup: setup build environment, download arduino-cli, install all dependencies for arduino ide
build: build firmware binary. will be saved into build/
upload: upload firmware by serial connection /dev/ttyUSB0
webupload: upload firmware with webupload to 192.168.4.20
monitor: serial monitor /dev/ttyUSB0
# WORK IN PROGRESS
# Install all dependencies for build environment
$ ./cangrow.sh setup
```
## Motivation
I havn't found an already existing grow controller project within the ESP / Arduino Core eco system which
met my personal requirements.
Those are an easy DIY, using low cost parts, Arduino Core sourcecode to hack own things together, having a WebUI, grab some Metrics for monitoring, standalone and my very special need that the Hardware should run completely with 12V.
The script installs [arduino-cli](https://github.com/arduino/arduino-cli) to `~/.local/bin/arduino-cli`.
### Update 14.09.2024 - Code Rewrite v0.2
## Compile
I took some "summer break" from the project, and had the opportunity to talk to different people about it.
My conclusion at this point is, that the focus of this project is not the Hardware, it came out that it should be the software.
So I decided to completely rewrite the code from 0 - with recycling some parts of it.
Goal of the Rewrite is that the Firmware becomes more independent of the hardware used. It has to support both ESP8266 and ESP32
and let the user decide at which pin which output, sensor or whatever will be connected to. Like done in the [Tasmota](https://github.com/arendst/Tasmota) Firmware, I also want to support "Hardware Templates" which come with presets for PCBs like the one I created.
```sh
# compile and output to build/CanGrow_v0.2...bin
# Default Target is ESP8266 D1 Mini
$ ./cangrow.sh build
**Checklist for v0.2 Firmware**
- Support ESP8266 and ESP32
- AsyncWebserver instead ESP8266Webserver
- LittleFS instead of EEPROM()
- deliver static HTML, dynamic Stuff with Javascript
- (or is there a better way? please tell me!)
- Free configurable outputs
- Main outputs for Light, Air, Water
- Support for Tasmota Wifi Plugs (and others?)
- No Limitation for Amount of outputs
- Light
- support for I2C 0-10V Dimm control
- PWM dimm control
- Air
- support for I2C 0-10V Dimm control
- PWM dimm control
- Support for humidifier, heater (, CO2?)
- Read Fan RPM
- Water
- Usual watering
- Pump for fertilizer
- Free configurable Inputs
- Support for various I2C devices
- All kind of sensors for Temp, Humidity, Moisture, and so on
- Support for ADCs to connect multiple analoge sensors
- Support for Analog inputs
- onboard ones or I2C (ADC)
- Analog Multiplexer support (like CD4051)
- Calibrate sensors
- define 0% and 100% values
- Offsets
- MQTT support
- API
# Compile for ESP32 D1 Mini
$ export BOARD="esp32:esp32:d1_mini32"
$ ./cangrow.sh build
## Old v0.1 Features / ToDo List
# Build and webupload to IP
$ export IP="192.168.4.69"
$ ./cangrow.sh build # need to make .bin first
$ ./cangrow.sh webupload # upload
- Measure values :white_check_mark:
- Humidity :white_check_mark:
- soil moisture :white_check_mark:
- temperature :white_check_mark:
- water level for water tank :white_check_mark:
- LED grow light control (on/off, dimming, max. 12V 50W load ) :white_check_mark:
- You can of course use a relais as well, if you want to drive 220V lights :white_check_mark:
- fan control (on/off, (PWM?) max 1A) :white_check_mark:
- pump control for automatic watering (max 1A) :large_blue_circle:
- Web UI and REST API for data and controlling :large_blue_circle:
- simple web ui :white_check_mark:
- REST API :large_blue_circle:
- Send notifications with web call (e.g. for mastodon) :red_circle:
- predefined grow profiles :large_blue_circle:
- persistent data :white_check_mark:
- Start of Grow :white_check_mark:
- day of grow :large_blue_circle:
- grow profile
- watering amount per week :large_blue_circle:
- light cycle :white_check_mark:
- wifi settings :white_check_mark:
- settings in general :white_check_mark:
- Easy to build and use for beginners (i hope so!) :white_check_mark:
- PCB layout to order from manufacture (jlcpcb or pcbway) :white_check_mark:
- easy to build up on a perfboard :white_check_mark:
- easy to etch pcb :white_check_mark:
- easy to access and modify :white_check_mark:
- low cost as possible! :white_check_mark:
# listen to serial monitor on /dev/ttyUSB2
$ export TTY="/dev/ttyUSB2"
./cangrow.sh monitor
```
:white_check_mark: Done - :large_blue_circle: In Progress - :red_circle: ToDo
I wrote this project using [Geany IDE. ](https://www.geany.org/). The Geany Projectfile is also included, just run
```sh
$ geany CanGrow.geany
```
**F8 compiles** the project, **F9 uploads** firmware to /dev/ttyUSB0. You can change these settings for .ino and .h files
in Project -> Settings -> Create/Make.

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