Compare commits

..

132 commits

Author SHA1 Message Date
84c4840993 ESPAsyncwebserver template processor bug - size related?
I have a bug on the /system/ page, I also noticed it on /wifi/ page sometimes, not reproducable everytime. So i am glad, the bug also appears here 100% reproducable

I inserted `<li><a class='' href='/system/sensor/'>&#x1F321;&#xFE0F; Sensor configuration</a></li>` to the /system/ html template. With this included, the template processor will corrupt the final output when processing %FOOTER%, which got inserted from AddHeaderFooter() function.

the rendered output, damaged, looks like this

```
<input type='submit' value='&#x1F4BE; Save settings'>
</form>
<div cla%</span></div>
</div></body></html>
```

notice that the div on the second last line is broken at `class` - got `<div cla%</span></div>`.

It should look like this: 

```
<input type='submit' value='&#x1F4BE; Save settings'>
</form>
<div class='footer'><span>Build: 92724fa-esp8266-20241102171544</span></div>
</div></body></html>
```
2024-11-02 17:31:51 +01:00
92724fa1f4 fix css and javascript bug for hiding/show stuff 2024-10-29 01:43:40 +01:00
cf824c1c61 output edit now works as intended 2024-10-29 01:32:56 +01:00
b28c71c9a4 basic work done for editing output
i had to use a global variable to put the outputId to edit in I receive
from the GET param edit. I havent found another solution. I wish I could
just pass the GET param to the template processor, but it seems this is
not possible at the moment or I havent seen the correct solution yet.
2024-10-29 00:15:28 +01:00
790b9bb9c9 put stuff into seperate functions 2024-10-28 22:30:20 +01:00
735cff463e chunked response makes no sense here and does not solve my problem, because it is (afaik) not related 2024-10-27 20:49:23 +01:00
9af343bd3e delete output implemented 2024-10-27 01:33:10 +02:00
1419d625a4 add css to File_cangrow_CSS for linkForm class 2024-10-27 00:44:13 +02:00
fba2210bbd playground - i think using form and submit button for actions edit and delte looks good 2024-10-27 00:42:35 +02:00
aaf9688d1a playing around with html css js 2024-10-27 00:36:08 +02:00
8955824884 playing around with html css js 2024-10-27 00:36:00 +02:00
40d0175564 main stuff for adding new output is done. for i2c i have not idea atm what i need. 2024-10-26 22:10:39 +02:00
b7d21ca868 use toggleDisplay JS func for hiding static ip config when dhcp is used 2024-10-26 20:22:53 +02:00
c466f1396f correct wrong mimetype for cangrow.js 2024-10-26 18:26:04 +02:00
54b6d48e1e call Check_GPIOindex_Used only once 2024-10-26 18:14:44 +02:00
a983129085 put GPIO_Index.note description into char const 2024-10-26 18:05:10 +02:00
c8247268cc implement checks if gpio is free when adding new output 2024-10-26 16:54:39 +02:00
22c316edbb only show configured values 2024-10-26 06:43:37 +02:00
bc15bbab46 first values saving foir the output ports. 2024-10-26 06:26:42 +02:00
b0f3d05576 output:add put next free output id into hidden input. if none available, return 255 2024-10-26 06:04:10 +02:00
f7f5fe073b put the chunked response stuff into own function, fix html javascript include 2024-10-26 05:01:58 +02:00
55997e82eb cleanup - i leave chunked in system/output/add for now, document will get bit bigger i geuss 2024-10-26 04:52:54 +02:00
bba1687022 cleanup - i leave chunked in system/output/add for now, document will get bit bigger i geuss 2024-10-26 04:52:37 +02:00
f4624f860e this was my problem i debbuged the last three hours!? >:(( At least i learned how to use chunked responses, yay 2024-10-26 04:42:26 +02:00
10a0906a93 chunked response works now - but does not solve my problem >:( 2024-10-26 04:33:27 +02:00
f7c4739f0d add output add dialog, try to use chunked response, but not working atm 2024-10-26 03:39:36 +02:00
9c51386be9 cosmetics 2024-10-25 23:43:51 +02:00
dadaf09232 playground output add dialog, add cangrow.js 2024-10-25 23:40:20 +02:00
98ef3de395 add output table , playing around with html in playground 2024-10-25 22:03:16 +02:00
945c208ba4 add gziped favicon as byte array, playground output dialog stuff 2024-10-25 00:30:57 +02:00
3517f6abf4 put cangrow.css and favicon to sepereate webserver path and not incldue them in the html body 2024-10-24 23:22:56 +02:00
d679d896b2 cangrow.sh update arduino-cli to 1.0.4 2024-10-24 21:55:32 +02:00
ca4eb8cfd6 cangrow.sh - add arduino-cli lib update-index before installing libs 2024-10-24 21:52:21 +02:00
8e5268df22 Arduino/CanGrow/cangrow.sh aktualisiert 2024-10-24 21:46:19 +02:00
0d0efbb2c1 add some diff to force new line when inline-block 2024-10-24 19:56:34 +02:00
4808af281c remove headings from system and wifi pages. 2024-10-24 17:20:50 +02:00
7da5bc38d7 implemented highlighting active system subnav entry 2024-10-24 17:16:31 +02:00
1765748422 implemented highlighting active nav entry 2024-10-24 16:53:21 +02:00
bafb623392 i am a webdeveloper! 2024-10-24 15:59:24 +02:00
e3c739e745 i am a webdeveloper! 2024-10-24 15:49:59 +02:00
2ad9c4c03d playing around with css 2024-10-24 15:14:25 +02:00
00a4acfd13 add subnav to every subpage of system 2024-10-24 14:49:31 +02:00
dc8ba42909 cangrow.sh put build target dir into variable 2024-10-24 14:28:11 +02:00
5c019f8df6 add config.system.output.device , add POST stuff for /system/ 2024-10-24 04:14:19 +02:00
458077442a cleanup 2024-10-24 02:54:18 +02:00
99d598e05d it does work with a structure and arrays in it. this is fine 2024-10-24 02:24:23 +02:00
bf1e25d04c playing around, output_ip load/save was broken. fixed. 2024-10-24 01:54:15 +02:00
d68d0fbf79 putting output config into json
i could not apply the structure of configSystemOutputs into json, so i had to
switch to use arrays instead. it works, but looks not that nice as a struct.
2024-10-24 01:45:06 +02:00
4c9c280d45 try to add config.system.outputs to json, but esp8266 actually crashes at loading json 2024-10-24 00:00:35 +02:00
94e79b8fa2 replacing all old configSystem style variables with config.system.something 2024-10-23 21:21:26 +02:00
4f42b64c7e wip - doing config structure things 2024-10-23 21:14:43 +02:00
13087c0cc1 structure PinIndex 2024-10-23 19:03:50 +02:00
0227427b6f add pinIndex structure for ESP8266 and ESP32 2024-10-23 17:51:54 +02:00
8861393e80 wip write down free pins and think about it 2024-10-23 02:27:49 +02:00
2dfa2b0c13 missing </div> 2024-10-22 22:20:12 +02:00
f91b2de002 fix submenu display bug on chrome 2024-10-22 22:03:46 +02:00
c3b9f0f8a6 update system update page HTML 2024-10-22 18:29:53 +02:00
f1a1cb3aa9 Arduino/CanGrow/README.md aktualisiert 2024-10-22 03:16:49 +02:00
947af0dd4d Update README.md
add some instructions for cangrow.sh
2024-10-22 03:11:38 +02:00
4a14629e48 cosmetic changes 2024-10-22 01:58:15 +02:00
44ca2c6a8e Update() fixes for esp32
https://github.com/me-no-dev/ESPAsyncWebServer/issues/455#issuecomment-451728099
2024-10-22 01:16:09 +02:00
5af5bcd94d implement webui littlefs wipe 2024-10-22 01:09:12 +02:00
22fdfc57c2 fine tune web update 2024-10-22 00:40:07 +02:00
e7bb42f72b web firmware update implemented 2024-10-21 23:57:13 +02:00
f093ac843b restart from webui implemented 2024-10-21 23:18:42 +02:00
de26abaf05 added system settings page, added web restart dialoge 2024-10-21 22:16:20 +02:00
827c8cd184 moved wifi stuff into its own file, cosmetic things wifi page 2024-10-21 19:54:20 +02:00
825f2e8e19 configuring and saving wifi settings works now, yay - milestone! 2024-10-21 03:11:42 +02:00
ae7f6cd3f7 saving ssid, password and ip works so far 2024-10-21 01:54:27 +02:00
c2d6d508a2 renamed some files, first steps with wifi settings post 2024-10-21 01:07:29 +02:00
e843df1a28 added wifi network scan 2024-10-20 23:09:20 +02:00
979b214d43 renamed header.h and footer.h to upper case 2024-10-20 21:41:42 +02:00
6e4127398b add wifiSettings page , wip 2024-10-20 15:52:25 +02:00
b5991a576b remove a line 2024-10-20 06:09:03 +02:00
5b54ae3658 add 404 handler, include css in header completely 2024-10-20 06:05:01 +02:00
57fa57fc94 first try of v0.1 webui 2024-10-20 05:09:06 +02:00
187ade247f organizing webserver template stuff, playing with templates 2024-10-20 04:48:05 +02:00
2252fe0142 webserver templating seems to work fine 2024-10-20 02:35:03 +02:00
0ef3656bf6 stuff 2024-10-20 01:44:55 +02:00
2c2adc9678 add configurable webserver logging to serial 2024-10-20 01:32:18 +02:00
7d0880343b add configurable webserver logging to serial 2024-10-20 01:29:57 +02:00
a0735829ae play around with file structure, first webserving tests 2024-10-20 00:41:23 +02:00
45ea8eabaa add include/CanGrow_Webserver.h 2024-10-19 22:52:39 +02:00
8e9a07a65b minor small changes 2024-10-19 22:52:19 +02:00
6ac7b31602 config load and wifi connect works, create ap if no ssid is saved 2024-10-19 18:28:14 +02:00
a044c65503 first WiFi steps 2024-10-19 04:36:39 +02:00
d4e1108759 save/load ip addresses as array 2024-10-19 04:25:22 +02:00
52cd7d469f remove old stuff 2024-10-19 02:52:17 +02:00
219586de93 cangrow.sh remove old version header stuff 2024-10-19 02:33:55 +02:00
603118ad0c add comments to CanGrow.h 2024-10-19 02:19:56 +02:00
f4482ee37b add comments to CanGrow.h 2024-10-19 02:16:30 +02:00
f918529e57 add defines for CANGROW_VER and CANGROW_BUILD if the werent set 2024-10-19 02:11:32 +02:00
42d5939dbc Fix defines CANGROW_VER and CANGROW_BUILD
esp32 and esp8266 need different places where to put them.
2024-10-19 02:10:43 +02:00
16074669c3 get rid of CanGrow_Version.h 2024-10-19 01:29:45 +02:00
f7224a1588 playing around 2024-10-19 00:44:36 +02:00
9d2faf4e4c LittleFS Json config save and load somewhat working 2024-10-19 00:27:33 +02:00
b07696a1c4 LittleFS json config save/load wip 2024-10-18 22:51:42 +02:00
ac10feccf7 create config structs 2024-10-18 21:04:07 +02:00
a3fef36ecb cangrow.sh python3-serial for esp32 compiler stuff was missing 2024-10-18 17:41:49 +02:00
f3f1629001 add esp32 board url to arduino-cli.yml 2024-10-18 17:30:24 +02:00
1d44b6736d Arduino/CanGrow/CanGrow.geany aktualisiert 2024-10-18 14:16:27 +02:00
1d55f36387 basic json config save and load from and to LittleFS works 2024-10-18 02:45:34 +02:00
f347a375ce update geany config 2024-10-18 01:12:02 +02:00
764cf45b80 move cangrow.sh and arduino-cli.yml to Arduino/Cangrow/ 2024-10-18 00:58:28 +02:00
717b201889 update geany configuration 2024-10-18 00:57:53 +02:00
06d4ba73c0 mixing data types in a printf call seems not to be ok 2024-10-18 00:14:39 +02:00
36b2da7802 begin to seperate stuff for esp32 and esp8266 in their own header files 2024-10-17 22:59:51 +02:00
f130700cf0 add deleteFile() to CanGrow_LittleFS.h 2024-10-17 22:43:56 +02:00
985b2cac5d LittleFS wip 2024-10-17 22:20:25 +02:00
4595bea4e5 disabled verbose compile output 2024-10-17 21:48:03 +02:00
c4dea65157 LittleFS wip - putting things together in include/CanGrow_LittleFS.h 2024-10-17 21:32:04 +02:00
d97a220f42 add esp32 d1 mini fqbn as comment 2024-10-17 21:17:52 +02:00
26f0939cc6 LittleFS.format on ESP32 needs LittleFS.begin() before 2024-10-17 20:38:46 +02:00
7dcfc375bb adding include dependencies for specific boards 2024-10-17 15:56:39 +02:00
989fefd1e2 add more library dependencies to setup 2024-10-17 15:56:13 +02:00
a330268960 debugging - LittleFS causes ESP32 D1 Mini to crash - no idea why
when calling LittleFS.format() esp32 d1 mini crashes

```
assert failed: esp_littlefs_format esp_littlefs.c:474 (partition_label)

Backtrace: 0x40083571:0x3ffb2040 0x40088365:0x3ffb2060 0x4008d2bd:0x3ffb2080 0x400e1c83:0x3ffb21b0 0x400d2ba9:0x3ffb21f0 0x400d1a0a:0x3ffb2210 0x400d4d3a:0x3ffb2290
```
2024-10-17 15:28:16 +02:00
04e8de0fde ignore CanGrow_Version.h 2024-10-17 15:27:00 +02:00
73f017cd2f cangrow.sh tweaks 2024-10-17 15:26:41 +02:00
e82046d297 use vanilla espasyncwebserver instead of esphome fork. now it compiles for esp32 as well :) 2024-10-17 14:23:30 +02:00
7b43171231 wip 2024-10-17 02:40:32 +02:00
c4155e1f9b basics of factory reset implemented 2024-10-17 02:35:04 +02:00
4c57546657 basics of factory reset implemented 2024-10-17 02:15:15 +02:00
3f8b060c66 add CanGrow_Version.h 2024-10-17 01:43:19 +02:00
3a29ebd316 first steps with LittleFS 2024-10-17 01:42:34 +02:00
11b7217f57 first serial output 2024-10-17 01:10:01 +02:00
529bd7e556 tune cangrow.sh 2024-10-17 01:01:37 +02:00
cf58ef292b add ESPAsyncWebServer to cangrow.sh setup, wip 2024-10-16 22:51:24 +02:00
ccca8b10ab first little steps 2024-10-16 21:48:49 +02:00
8fb27a1720 wip 2024-10-16 20:07:19 +02:00
8940a9a136 wip 2024-10-16 20:06:54 +02:00
fec057b8db cleanup old v0.1 stuff, lets begin from scratch 2024-10-16 19:47:32 +02:00
a6f5a6539b cleanup old v0.1 stuff, lets begin from scratch 2024-10-16 19:45:16 +02:00
55 changed files with 4174 additions and 12753 deletions

2
.gitignore vendored
View file

@ -3,4 +3,4 @@ KiCad/CanGrow/CanGrow.kicad_sch-bak
KiCad/CanGrow/fp-info-cache
KiCad/CanGrow/gerber/*.zip
Arduino/CanGrow/CanGrow.geany
build/
Arduino/CanGrow/build/

24
.gitmodules vendored
View file

@ -1,24 +0,0 @@
[submodule "Arduino/CanGrow/Adafruit-GFX-Library"]
path = Arduino/CanGrow/Adafruit-GFX-Library
url = https://github.com/adafruit/Adafruit-GFX-Library
[submodule "Arduino/CanGrow/lib/Adafruit-GFX-Library"]
path = Arduino/CanGrow/lib/Adafruit-GFX-Library
url = https://github.com/adafruit/Adafruit-GFX-Library
[submodule "Arduino/CanGrow/lib/Adafruit_SSD1306"]
path = Arduino/CanGrow/lib/Adafruit_SSD1306
url = https://github.com/adafruit/Adafruit_SSD1306
[submodule "Arduino/CanGrow/lib/Adafruit_BME280_Library"]
path = Arduino/CanGrow/lib/Adafruit_BME280_Library
url = https://github.com/adafruit/Adafruit_BME280_Library/
[submodule "Arduino/CanGrow/lib/NTPClient"]
path = Arduino/CanGrow/lib/NTPClient
url = https://github.com/arduino-libraries/NTPClient
[submodule "Arduino/CanGrow/lib/Time"]
path = Arduino/CanGrow/lib/Time
url = https://github.com/PaulStoffregen/Time
[submodule "Arduino/CanGrow/lib/Adafruit_SHT31"]
path = Arduino/CanGrow/lib/Adafruit_SHT31
url = https://github.com/adafruit/Adafruit_SHT31/
[submodule "Arduino/CanGrow/lib/Adafruit_Sensor"]
path = Arduino/CanGrow/lib/Adafruit_Sensor
url = https://github.com/adafruit/Adafruit_Sensor

View file

@ -28,30 +28,34 @@ long_line_behaviour=1
long_line_column=72
[files]
current_page=1
FILE_NAME_0=2265;Arduino;0;EUTF-8;0;1;0;%2Fhome%2Fmarcus%2Fdistrobox%2Fdebian-cangrow%2FCanGrow%2FArduino%2FCanGrow%2FCanGrow.ino;0;2
FILE_NAME_1=14609;C++;0;EUTF-8;0;1;0;%2Fhome%2Fmarcus%2Fdistrobox%2Fdebian-cangrow%2FCanGrow%2FArduino%2FCanGrow%2FCanGrow_HTML.h;0;2
FILE_NAME_2=2884;C++;0;EUTF-8;0;1;0;%2Fhome%2Fmarcus%2Fdistrobox%2Fdebian-cangrow%2FCanGrow%2FArduino%2FCanGrow%2FCanGrow_Init.h;0;2
FILE_NAME_3=0;C++;0;EUTF-8;0;1;0;.%2FCanGrow_Logo.h;0;2
FILE_NAME_4=0;C++;0;EUTF-8;0;1;0;.%2FCanGrow_PinAssignments.h;0;2
FILE_NAME_5=4277;C++;0;EUTF-8;0;1;0;%2Fhome%2Fmarcus%2Fdistrobox%2Fdebian-cangrow%2FCanGrow%2FArduino%2FCanGrow%2FCanGrow_Sensors.h;0;2
FILE_NAME_6=4737;C++;0;EUTF-8;0;1;0;%2Fhome%2Fmarcus%2Fdistrobox%2Fdebian-cangrow%2FCanGrow%2FArduino%2FCanGrow%2FCanGrow_SysFunctions.h;0;2
FILE_NAME_7=27858;C++;0;EUTF-8;0;1;0;%2Fhome%2Fmarcus%2Fdistrobox%2Fdebian-cangrow%2FCanGrow%2FArduino%2FCanGrow%2FCanGrow_WebFunctions.h;0;2
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=~/.local/bin/arduino-cli --no-color compile -b esp8266:esp8266:d1_mini_clone "%d/CanGrow.ino"
C++FT_00_CM=cd .. ; ./cangrow.sh build
C++FT_00_WD=
filetypes=C++;Arduino;
filetypes=C++;Arduino;Sh;
ArduinoFT_00_LB=_Build
ArduinoFT_00_CM=~/.local/bin/arduino-cli --no-color compile -b esp8266:esp8266:d1_mini_clone "%d/CanGrow.ino"
ArduinoFT_00_CM=./cangrow.sh build
ArduinoFT_00_WD=
ArduinoFT_01_LB=Build & Upload
ArduinoFT_01_CM=~/.local/bin/arduino-cli --no-color compile -v -b esp8266:esp8266:d1_mini_clone -u -p /dev/ttyUSB0 "%d/CanGrow.ino"
ArduinoFT_01_CM=./cangrow.sh upload
ArduinoFT_01_WD=
C++FT_01_LB=_Build & Upload
C++FT_01_CM=~/.local/bin/arduino-cli --no-color compile -v -b esp8266:esp8266:d1_mini_clone -u -p /dev/ttyUSB0 "%d/CanGrow.ino"
C++FT_01_CM=cd .. ; ./cangrow.sh upload
C++FT_01_WD=
[VTE]
last_dir=/home/marcus/distrobox/debian-cangrow
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=

View file

@ -1,474 +1,169 @@
/*
* CanGrow - simply DIY automatic plant grow system (for cannabis).
*
* 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.
*
*/
/*
* Includes
*
* Libraries include
*/
// Libraries internal (Arduino Core / ESP)
#include "Arduino.h"
// * ESP8266 *
#ifdef ESP8266
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
// * ESP32 *
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#include <Update.h>
#endif
// https://github.com/mathieucarbou/ESPAsyncWebServer
#include <ESPAsyncWebServer.h>
// LittleFS filesystem
#include "FS.h"
// arduino-core for esp8266 and esp32
#include "LittleFS.h"
// https://github.com/arduino/ArduinoCore-avr/tree/master/libraries/SPI
#include <SPI.h>
// https://github.com/arduino/ArduinoCore-avr/tree/master/libraries/Wire
#include <Wire.h>
// https://github.com/arduino/ArduinoCore-avr/tree/master/libraries/EEPROM
#include <EEPROM.h>
// https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WiFi
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
// https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WebServer
#include <ESP8266WebServer.h>
// OTA update
#include <ESP8266HTTPUpdateServer.h>
// Libraries external
// https://github.com/adafruit/Adafruit-GFX-Library
//include "lib/Adafruit-GFX-Library/Adafruit_GFX.h"
#include <Adafruit_GFX.h>
// https://github.com/adafruit/Adafruit_SSD1306
//include "lib/Adafruit_SSD1306/Adafruit_SSD1306.h"
#include <Adafruit_SSD1306.h>
// https://github.com/adafruit/Adafruit_Sensor
// include "lib/Adafruit_Sensor/Adafruit_Sensor.h"
#include <Adafruit_Sensor.h>
// https://github.com/adafruit/Adafruit_BME280_Library
//include "lib/Adafruit_BME280_Library/Adafruit_BME280.h"
#include <Adafruit_BME280.h>
// https://github.com/adafruit/Adafruit_SHT31/
//#include "lib/Adafruit_SHT31/Adafruit_SHT31.h"
#include <Adafruit_SHT31.h>
// https://github.com/bblanchon/ArduinoJson
#include "lib/ArduinoJson/ArduinoJson-v7.2.1.h"
// https://github.com/arduino-libraries/NTPClient
//#include "lib/NTPClient/NTPClient.h"
#include <NTPClient.h>
#include <ArduinoJson.h>
// https://github.com/PaulStoffregen/Time
//#include "lib/Time/TimeLib.h"
#include <TimeLib.h>
// DHT support dropped
// https://github.com/adafruit/DHT-sensor-library
// #include "DHT.h"
/*
* CanGrow header files
*/
#include "CanGrow_Version.h"
#include "CanGrow_PinAssignments.h"
#include "CanGrow_Init.h"
#include "CanGrow_Logo.h"
#include "CanGrow_Sensors.h"
#include "CanGrow_HTML.h"
#include "CanGrow_SysFunctions.h"
#include "CanGrow_WebFunctions.h"
/*
* Setup
*
* CanGrow includes
*/
/* main header file, where all variables, consts and structs get defined */
#include "include/CanGrow.h"
/* CanGrow platform specific includes */
#include "include/CanGrow_ESP8266.h"
#include "include/CanGrow_ESP32.h"
/* CanGrow header with all functions */
#include "include/CanGrow_Core.h"
#include "include/CanGrow_Wifi.h"
#include "include/CanGrow_LittleFS.h"
#include "include/CanGrow_Webserver.h"
void setup() {
// setup pins
//pinMode(PINdht, INPUT);
pinMode(PINwaterlevel, OUTPUT);
pinMode(PINsoilmoisture, OUTPUT);
pinMode(PinFAN2, OUTPUT);
// define output for onboard LED/WIPE pin
pinMode(PinWIPE, OUTPUT);
// set all OUTPUT to low
digitalWrite(PINwaterlevel, LOW);
// its better to leave them untuched until we know from EEPROM if they have to be inverted or not
//~ digitalWrite(PinFAN, HIGH);
//~ digitalWrite(PinLED, HIGH);
//~ digitalWrite(PinPUMP, HIGH);
// except PINsoilmoisture
// PINsoilmoisture is always HIGH and gets LOW in moment of waterlevel measurement
digitalWrite(PINsoilmoisture, HIGH);
// set FAN2 to off with digitalWrite LOW
analogWrite(PinFAN2, PinFAN2PWM);
// Start EEPROM
EEPROM.begin(512);
// 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");
Serial.print(".:: CanGrow firmware v");
Serial.print(CANGROW_VER);
Serial.print(" build ");
Serial.print(CANGROW_BUILD);
Serial.println(" starting ::.");
Serial.println(":: initialise I2C ::");
// initialise Wire for I2C
Wire.begin();
// just for testing
//Wire.setClockStretchLimit(2500);
Serial.println(":: initialise display ::");
// initialise I2C display
display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // Address 0x3C for 128x64
display.clearDisplay();
display.display();
Serial.printf(".:: CanGrow firmware v%s build %s starting ::.\n", CANGROW_VER, CANGROW_BUILD);
Serial.print("II To format / factory reset LittleFS, pull GPIO ");
Serial.print(PinWIPE);
Serial.print(" (PinWIPE) to ");
// we need to invert the default to tell that user the state for an action
Serial.print(1 - PinWIPE_default);
Serial.println(" - NOW! (2 seconds left) II");
// set display settings
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
// display Logo
display.drawBitmap(0, 0, bmpCanGrow_Logo, 128, 32, WHITE);
display.display();
Serial.println(":: initialise chirp sensor if present ::");
// reset chirp
writeI2CRegister8bit(0x20, 6); //TODO: Do only, when configured
// initialise DHT11
// dht support dropped
// dht.begin(); //TODO: Do only, when configured
// initialise BME280
// dirty way of supporting multiple addresses, DRY? :p
Serial.println(":: initialise BME280 sensor, address 0x76 ::");
// ToDo: let the user configure somewhere the ID of the BME280 sensor
if(!bme_0x76.begin(0x76)) {
Serial.println("!! Cannot find BME280 on I2C bus at address 0x76. Please check connection or ID");
}
Serial.println(":: initialise BME280 sensor, address 0x77 ::");
// ToDo: let the user configure somewhere the ID of the BME280 sensor
if(!bme_0x77.begin(0x77)) {
Serial.println("!! Cannot find BME280 on I2C bus at address 0x77. Please check connection or ID");
}
// initialise SHT31
Serial.println(":: initialise SHT31 sensor, address 0x44 ::");
if (! sht31_0x44.begin(0x44)) { // Set to 0x45 for alternate i2c addr
Serial.println("!! Cannot find SHT31 on I2C bus at address 0x45. Please check connection or ID");
}
Serial.println(":: initialise SHT31 sensor, address 0x45 ::");
if (! sht31_0x45.begin(0x45)) { // Set to 0x45 for alternate i2c addr
Serial.println("!! Cannot find SHT31 on I2C bus at address 0x45. Please check connection or ID");
}
Serial.print(":: SHT31 (0x44) heater enable state ::");
if (sht31_0x44.isHeaterEnabled())
Serial.println("ENABLED");
else
Serial.println("DISABLED");
Serial.print(":: SHT31 (0x45) heater enable state ::");
if (sht31_0x45.isHeaterEnabled())
Serial.println("ENABLED");
else
Serial.println("DISABLED");
Serial.println("To wipe the EEPROM saved data, set D4 (PinWIPE) to LOW - NOW! (2 seconds left)");
// wait a few seconds to let the user pull D4 down to wipe EEPROM
// and we can enjoy the boot screen meanwhile :p
// meanwhile blink with the led onboad :)
// 333 * 6 =~ 2 seconds
display.fillRect(0,36,128,64-36, 0);
display.setCursor(0,36);
display.println("To wipe EEPROM pull");
display.println("D4 (PinWIPE) to GND");
display.display();
// blink with the onboard LED on D4 (PinWIPE)
// blink with the onboard LED on D4/GPIO2 (PinWIPE)
for(byte i = 0; i <= 6 ; i++) {
if(i % 2) {
digitalWrite(PinWIPE, LOW);
digitalWrite(PinWIPE, 1 - PinWIPE_default);
} else {
digitalWrite(PinWIPE, HIGH);
digitalWrite(PinWIPE, PinWIPE_default);
}
delay(333);
}
// set back to HIGH because thats the default
digitalWrite(PinWIPE, HIGH);
//delay(2000);
// set PinWIPE back to its default
digitalWrite(PinWIPE, PinWIPE_default);
// read status from PinWIPE to WIPE
// when PinWIPE is set to LOW, wipe EEPROM
if(digitalRead(PinWIPE) == LOW) {
// wipe EEPROM
wipeEEPROM();
// when PinWIPE is set to LOW, format LittleFS
if(digitalRead(PinWIPE) != PinWIPE_default) {
LFS_Format();
Restart();
}
LFS_Init();
LoadConfig();
Wifi_Init();
Webserver_Init();
Serial.printf(":: [SETUP] Usable Pins: %d\n", GPIOindex_length);
for(byte i = 0; i < GPIOindex_length; i++) {
Serial.printf(":: [SETUP] Pin Index: %d, GPIO: %d, Notes: ", i, GPIOindex[i].gpio);
Serial.println(GPIO_Index_note_descr[GPIOindex[i].note]);
}
/*
* load EEPROM and Setup WiFi
*
* call loadEEPROM() which returns a bool
* When true, CanGrow is already configured and EEPROM values are applied
* When false, CanGrow is unconfigured and we need to run the setup assistant
*/
// load stored values from EEPROM and check what var configured is returned
if(loadEEPROM()) {
// connect to wifi
wifiConnect();
// configured is 0, setup Access Point
} else {
// start an wifi accesspoint
wifiAp();
}
// set web handler
WebHandler();
// start webserver
webserver.begin();
Serial.println(".:: CanGrow Ready ::.");
delay(1000);
if(strlen(GrowName) > 0 ) {
display.clearDisplay();
display.display();
}
// at the end of setup, set the outputs, when configured true
// we do this here because otherwise on inverted
// boards like CanGrow PCB v0.6 it would be turned on
if(configured == true) {
initOutputs();
}
}
bool alrdySaved = false;
/*
*
*
* Loop
*
*
*/
void loop() {
// var definition
unsigned long currentRuntime = millis();
unsigned long currentMillis = millis();
// first we call webserver handle client
webserver.handleClient();
if(currentMillis - schedulerPrevMillis >= config.system.schedulerInterval) {
}
// do every second when everything is configured and grow is started
if( (configured == true) && (strlen(GrowName) > 0) && (currentRuntime - outputPrevTime >= 1000) ){
// refresh all sensor values
refreshSensors();
// calculate VPD - https://www.grower.ch/forum/threads/diy-grow-controller-cangrow-projektvorstellung.163654/page-4#post-4294197
valVPD = (((100 - valHumidity) / 100) * (610.7 * (pow(10, (7.5 * valTemperature / (237.3 + valTemperature))))))/1000;
// calculate acutal DayOfGrow
DayOfGrow = int(ceil(float((timeClient.getEpochTime() - GrowStart) / 60 / 60 / 24)));
// decide if we are in Veg or Bloom phase of grow
// when DayOfGrow is larger then DaysVeg we must be in Bloom
// set the actual state of the Grow LED
// when being in Maintenance Mode and UseRelaisLED not true,
// dimm the light
if(MaintenanceMode == true) {
if((currentRuntime - MaintenanceStarted <= MaintenanceDuration * 1000 ) && (UseLEDrelais == false)) {
// in case of being in Maintenance Mode , dimm the grow light when not a relais is used
setOutput(1, 15);
} else {
MaintenanceMode = false;
}
} else {
controlLED();
}
controlPUMP();
controlFAN();
displayScreens();
// current time gets previous time for new interval
outputPrevTime = currentRuntime;
if((digitalRead(PinWIPE) != PinWIPE_default) && (alrdySaved == false)) {
Serial.println(":: [LOOP] PinWIPE is triggered");
// save config to littlefs as json
SaveConfig();
// only print json to serial
SaveConfig(true);
alrdySaved = true;
} else if( (digitalRead(PinWIPE) != PinWIPE_default) && (alrdySaved == true) ) {
alrdySaved = true;
} else {
alrdySaved = false;
}
// if global var doRestart is true, perform a restart
if(doRestart == true) {
Restart();
}
}
/*
*
* TODO LIST / NOTES
*
*
* - when PWM for fan is set, set fan speed to regulate humidity and
* temperature, depending on which phase of grow the plant is
* (https://www.royalqueenseeds.de/blog-cannabisanbau-im-grow-room-relative-luftfeuchtigkeit-und-temperaturen-n243)
* - re-organize EEPROM saved values.
* - prevent GrowStart to be in the future
* - maybe let the user configure some screens to display.
* - put EEPROM adresses into DEFINEs
*/
/*
* Fan control
*
* Vars:
* - FanVent (byte) Fan1 or Fan2
* - FanExhaust (byte) Fan1 or Fan2
* -
*/
/*
*
*
* PLAYGROUND / TRASH
*
*
*/
/*
unsigned long currentTime = millis();
int valSoilmoisture0 = getSoilmoisture(0);
int valSoilmoisture1 = getSoilmoisture(1);
float valTemperature0 = getTemperature(0);
float valTemperature1 = getTemperature(1);
float valHumidity = getHumidity();
int valWaterlevel = getWaterlevel();
switch(valWaterlevel) {
case 0:
digitalWrite(PinLED, HIGH);
digitalWrite(PinPUMP, LOW);
digitalWrite(PinFAN, LOW);
break;
case 1:
digitalWrite(PinLED, LOW);
digitalWrite(PinPUMP, HIGH);
digitalWrite(PinFAN, LOW);
break;
case 2:
digitalWrite(PinLED, LOW);
digitalWrite(PinPUMP, LOW);
digitalWrite(PinFAN, HIGH);
break;
}
// OUTPUT
if(currentTime - outputPrevTime >= 1000) {
// set display cursor to top left
display.setCursor(0,0);
// display text
display.print("I2C: ");
display.print(valSoilmoisture1);
display.print(", ");
display.println(valTemperature1);
Serial.print("I2C: ");
Serial.print(valSoilmoisture1);
Serial.print(", ");
Serial.println(valTemperature1);
display.print("DHT11: ");
display.print(valTemperature0);
display.print(", ");
display.println(valHumidity);
Serial.print("DHT11: ");
Serial.print(valTemperature0);
Serial.print(", ");
Serial.println(valHumidity);
display.print("Water Status: ");
display.println(valWaterlevel);
Serial.print("Water Status: ");
Serial.println(valWaterlevel);
display.print("ASM: ");
display.print(valSoilmoisture0);
display.println(", ");
Serial.print("ASM: ");
Serial.println(valSoilmoisture0);
// print everything on the display
display.display();
Serial.println("Test");
outputPrevTime = currentTime;
*/
/* if(D6status == true) {
digitalWrite(PinLED, LOW);
digitalWrite(PinPUMP, LOW);
digitalWrite(PinFAN, LOW);
D6status = false;
Serial.println("D6 is off now");
} else {
digitalWrite(PinLED, HIGH);
digitalWrite(PinPUMP, HIGH);
digitalWrite(PinFAN, HIGH);
D6status = true;
Serial.println("D6 is ON now");
}
*/
/*
for(int dutyCycle = 0; dutyCycle < 255; dutyCycle++){
// changing the LED brightness with PWM
analogWrite(PinLED, dutyCycle);
delay(1);
}
// decrease the LED brightness
for(int dutyCycle = 255; dutyCycle > 0; dutyCycle--){
// changing the LED brightness with PWM
analogWrite(PinLED, dutyCycle);
delay(1);
}
*/

View file

@ -1,600 +0,0 @@
/*
* HTML constants for header, footer, css, ...
* Note: I know of the existence of SPIFFS and ESPHtmlTemplateProcessor,
* but to keep things simple for compiling and upload for others, I decided
* to not use those.
*/
// Template: const char HTMLexamplepage[] PROGMEM = R"EOF()EOF";
// first part of HTML header stuff
const char HTMLheaderP1[] PROGMEM = R"EOF(
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
)EOF";
// here comes the page title in returnHTMLheader()
// second part of HTML header stuff
// Having the whole CSS here ensures it's all the time present
const char HTMLheaderP2[] PROGMEM = R"EOF(
<link rel='icon' href='data:;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsTAAALEwEAmpwYAAABcElEQVQ4y42TzU/bQBDFf7Nx1qGuAYVgQSuo2khBggPhyIH//9AiJAQ9tEeLqCKiUD6sxF52OMSEBCdW57aa9968fTsr3V5XWVLPO6sANNL7ZRAMNeU6Ea4T1UEI6pr55kcAwhpMrYOpk2/r/yEQmKWkIonf+TZVgex4Fw0bIEtIAALF3gbZ8U5VwKa3PJ18JT9IpiLvyflBwuhLG5veVUM0/0aoCONPa2hQjWZ8uEVeupJnXSBwO8YOH8iTeAKc2Q4Xt2C1VZL93F7MjbK/bxDnp5Zn7b+So+9pdQ+K/Q5qJlrRj5Ts6DM+rK7Ih7Mr3HaM7jYQVZqXQ6Tb6yqBYdTfomhHiFfUyMI3f+01/z7RHNzTGDyWGThP63SA2d8EEfIkrgQpzmOvH0AV+3M4zegNpUwagAYG8Yp4BS0nl4Kz5Mpf0JXJMby6w/66Aa+M+9uE53/Iexsggq4ESOYWC0jmsBfX8xdXhcJjL4cLc3kBl8uJGQ/CrpAAAAAASUVORK5CYII='>
<style>
body {
color: #cae0d0;
background-color: #1d211e;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.center {
width: 100%;
margin: auto;
}
.centered {
display: block;
margin-left: auto;
margin-right: auto;
}
h1, h2, h3, h4, h5 {
text-align: center;
}
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;
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;
}
.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;
}
.subnav {
text-align: center;
display: table;
margin: auto;
margin-bottom: 10px;
padding: 0;
position: relative;
border-radius: 3px;
}
.nav li {
display: inline-block;
list-style: none;
border-radius: 3px;
}
.subnav li {
background: #026b45;
list-style: none;
border-radius: 3px;
margin-bottom: 3px;
}
.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] {
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);
}
.nav li a:hover, .subnav li a:hover, .activeMenu, .button:link:hover, .button:visited:hover, input[type=button]:hover, input[type=submit]:hover, input[type=reset]:hover {
background: #04AA6D;
color: #fff;
border-radius: 3px;
}
.nav li a:active, .subnav li a:active {
background: #026b45;
color: #cae0d0;
}
.activeMenu {
background: #444;
}
.MenuTime {
background: #292929;
}
.button, .button:link, .button:visited, input[type=button], input[type=submit], input[type=reset] {
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 {
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;
}
@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) {
.subnav li {
display: inline-block;
margin-bottom: 3px;
}
}
/* VPD colors */
.vpd_danger1 {
color: #1a6c9c;
}
.vpd_earlyveg {
color: #22ab9c;
}
.vpd_lateveg {
color: #9cc55b;
}
.vpd_latebloom {
color: #9cc55b;
}
.vpd_danger2 {
color: #1a6c9c;
}
</style>
</head>
<body>
<ul class='nav'>)EOF";
// here comes the menu as unordered List in returnHTMLheader()
const char HTMLfooter[] PROGMEM = R"EOF(
</div>
</body>
</html>
)EOF";
const char HTMLsuccess[] PROGMEM = R"EOF(
<div class='infomsg'>&#x2705; Successfully saved!</div>
)EOF";
const char HTMLneedRestart[] PROGMEM = R"EOF(
<div class='warnmsg'>&#10071; Restart is required to apply new WiFi settings!
<form action='/system/restart'>
<input type='submit' value='Restart now' />
</form>
</div>
)EOF";
const char HTMLhelp[] PROGMEM = R"EOF(
<h2>&#x2753; Help</h2>
Here you will get some helpful help.
<h3>API</h3>
<a href='/api/sensors' target='_blank'>Sensor data</a>: <code>GET /api/sensors</code><br>
<a href='/api/debug' target='_blank'>Debug all data:</a> <code>GET /api/debug</code>
)EOF";
const char JSconvertDateToEpoch[] PROGMEM = R"EOF(
<script>
function convertDateToEpoch(src, dst) {
var valGrowStart = document.getElementById(src).value ;
document.getElementById(dst).value = new Date(valGrowStart).getTime() / 1000;
}
</script>
)EOF";
// The gauge meter are based on sathomas' gaugemeter
// https://github.com/sathomas/material-gauge
const char CSSgauge[] PROGMEM = R"EOF(
.gauge {
position: relative;
}
.gaugeWrapper {
overflow: hidden;
display: flex;
justify-content: center;
}
.gauge__container {
margin: 0;
padding: 0;
position: absolute;
left: 50%;
overflow: hidden;
text-align: center;
-webkit-transform: translateX(-50%);
-moz-transform: translateX(-50%);
-ms-transform: translateX(-50%);
-o-transform: translateX(-50%);
transform: translateX(-50%);
}
.gauge__background {
z-index: 0;
position: absolute;
background-color: #cae0d0;
top: 0;
border-radius: 300px 300px 0 0;
}
.gauge__data {
z-index: 1;
position: absolute;
background-color: #04AA6D;
margin-left: auto;
margin-right: auto;
border-radius: 300px 300px 0 0;
-webkit-transform-origin: center bottom;
-moz-transform-origin: center bottom;
-ms-transform-origin: center bottom;
-o-transform-origin: center bottom;
transform-origin: center bottom;
}
.gauge__center {
z-index: 2;
position: absolute;
background-color: #1d211e;
margin-right: auto;
border-radius: 300px 300px 0 0;
}
.gauge__marker {
z-index: 3;
background-color: #fff;
position: absolute;
width: 1px;
}
.gauge__needle {
z-index: 4;
background-color: #E91E63;
height: 3px;
position: absolute;
-webkit-transform-origin: left center;
-moz-transform-origin: left center;
-ms-transform-origin: left center;
-o-transform-origin: left center;
transform-origin: left center;
}
.gauge__labels {
display: table;
margin: 0 auto;
position: relative;
font-weight: bold;
}
.gauge__label--low {
display: table-cell;
text-align: center;
}
.gauge__label--spacer {
display: table-cell;
}
.gauge__label--high {
display: table-cell;
text-align: center;
}
.gauge { height: calc(60px + 3em); }
.gauge__container { width: 120px; height: 60px; }
.gauge__marker { height: 60px; left: 59.5px; }
.gauge__background { width: 120px; height: 60px; }
.gauge__center { width: 72px; height: 36px; top: 24px; margin-left: 24px; }
.gauge__data { width: 120px; height: 60px; }
.gauge__needle { left: 60px; top: 58px; width: 60px; }
.gauge__labels { top: 60px; width: 120px; }
.gauge__label--low { width: 24px; }
.gauge__label--spacer { width: 72px; text-align: center;}
.gauge__label--high { width: 24px; }
.gaugeLabel { text-align: center; }
@media only screen and (min-width: 720px) {
.gauge { height: calc(120px + 4.2em); }
.gauge__container { width: 240px; height: 120px; }
.gauge__marker { height: 120px; left: 119.5px; }
.gauge__background { width: 240px; height: 120px; }
.gauge__center { width: 144px; height: 72px; top: 48px; margin-left: 48px; }
.gauge__data { width: 240px; height: 120px; }
.gauge__needle { left: 120px; top: 117px; width: 120px; }
.gauge__labels { top: 120px; width: 240px; }
.gauge__label--low { width: 48px; }
.gauge__label--spacer { width: 144px; text-align: center;}
.gauge__label--high { width: 48px; }
.gaugeLabel { font-size: 1.3em; }
.gauge__labels { font-size: 2em; }
}
.gauge--liveupdate .gauge__data,
.gauge--liveupdate .gauge__needle {
-webkit-transition: all 1s ease-in-out;
-moz-transition: all 1s ease-in-out;
-ms-transition: all 1s ease-in-out;
-o-transition: all 1s ease-in-out;
transition: all 1s ease-in-out;
}
.gauge__data {
-webkit-transform: rotate(-.50turn);
-moz-transform: rotate(-.50turn);
-ms-transform: rotate(-.50turn);
-o-transform: rotate(-.50turn);
transform: rotate(-.50turn);
}
.gauge__needle {
-webkit-transform: rotate(-.50turn);
-moz-transform: rotate(-.50turn);
-ms-transform: rotate(-.50turn);
-o-transform: rotate(-.50turn);
transform: rotate(-.50turn);
}
)EOF";
const char JSgauge[] PROGMEM = R"EOF(
function Gauge(el) {
var element, // Containing element for the info component
data, // `.gauge__data` element
needle, // `.gauge__needle` element
value = 0.0, // Current gauge value from 0 to 1
prop, // Style for transform
valueLabel; // `.gauge__label--spacer` element
var setElement = function(el) {
// Keep a reference to the various elements and sub-elements
element = el;
data = element.querySelector('.gauge__data');
needle = element.querySelector('.gauge__needle');
valueLabel = element.querySelector('.gauge__label--spacer');
};
var setValue = function(x, max, unit) {
percentage = x * 100 / max;
value = percentage / 100;
var turns = -0.5 + (value * 0.5);
data.style[prop] = 'rotate(' + turns + 'turn)';
needle.style[prop] = 'rotate(' + turns + 'turn)';
valueLabel.textContent = x + unit;
};
function exports() { };
exports.element = function(el) {
if (!arguments.length) { return element; }
setElement(el);
return this;
};
exports.value = function(x, max=100, unit='%') {
if (!arguments.length) { return value; }
setValue(x, max, unit);
return this;
};
var body = document.getElementsByTagName('body')[0];
['webkitTransform', 'mozTransform', 'msTransform', 'oTransform', 'transform'].
forEach(function(p) {
if (typeof body.style[p] !== 'undefined') { prop = p; }
}
);
if (arguments.length) {
setElement(el);
}
return exports;
};
)EOF";
const char HTMLgauge[] PROGMEM = R"EOF(
<div class='gaugeWrapper'>
<div class='gauge gauge--liveupdate spacer' id='gaugeTemperature' style='float:left; margin-right: 10px;'>
<div class='gaugeLabel'>&#x1F321;&#xFE0F; Temperature</div>
<div class='gauge__container'>
<div class='gauge__background'></div>
<div class='gauge__center'></div>
<div class='gauge__data'></div>
<div class='gauge__needle'></div>
</div>
<div class='gauge__labels mdl-typography__headline'>
<span class='gauge__label--low'></span>
<span class='gauge__label--spacer'></span></span>
<span class='gauge__label--high'></span>
</div>
</div>
<div class='gauge gauge--liveupdate spacer' id='gaugeHumidity' style='float:left; margin-right: 10px;'>
<div class='gaugeLabel'>&#x2601;&#xFE0F; Humidity</div>
<div class='gauge__container'>
<div class='gauge__background'></div>
<div class='gauge__center'></div>
<div class='gauge__data'></div>
<div class='gauge__needle'></div>
</div>
<div class='gauge__labels mdl-typography__headline'>
<span class='gauge__label--low'></span>
<span class='gauge__label--spacer'></span>
<span class='gauge__label--high'></span>
</div>
</div>
<div class='gauge gauge--liveupdate' id='gaugeSoilmoisture' style='float:left;'>
<div class='gaugeLabel'>&#x1FAB4; Soilmoisture</div>
<div class='gauge__container'>
<div class='gauge__background'></div>
<div class='gauge__center'></div>
<div class='gauge__data'></div>
<div class='gauge__needle'></div>
</div>
<div class='gauge__labels mdl-typography__headline'>
<span class='gauge__label--low'></span>
<span class='gauge__label--spacer'></span>
<span class='gauge__label--high'></span>
</div>
</div>
</div>
<script src='gauge.js'></script>
<script>
var gaugeTemperature = new Gauge(document.getElementById('gaugeTemperature'));
var gaugeHumidity = new Gauge(document.getElementById('gaugeHumidity'));
var gaugeSoilmoisture = new Gauge(document.getElementById('gaugeSoilmoisture'));
</script>
)EOF";
const char HTMLupdate[] PROGMEM = R"EOF(
<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/applyUpdate' 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>
)EOF";
const char HTMLsystemSubNav[] PROGMEM = R"EOF(
<ul class='subnav'>
<li><a href='/system/update'>&#x1F504; Firmware update</a></li>
<li><a href='/system/restart' >&#x1F501; CanGrow restart</a></li>
<li><a href='/system/wipe' >&#x1F4A3; Factory reset</a></li>
</ul>
)EOF";
const char JSsoilmoisture[] PROGMEM = R"EOF(
<script>
function MoistureSensorType() {
let selVal = document.getElementById('SelMoistureSensor_Type').value;
let wet = document.getElementById('iSoilmoistureWet');
let dry = document.getElementById('iSoilmoistureDry');
switch(selVal) {
case '1':
wet.value = 160;
dry.value = 360;
console.log(selVal);
break;
case '2':
wet.value = 485;
dry.value = 250;
console.log(selVal);
break;
default:
wet.value = 0;
dry.value = 0;
console.log(selVal);
break;
}
}
function loadJSON(callback) {
var xobj = new XMLHttpRequest();
xobj.overrideMimeType("application/json");
xobj.open('GET', '/api/sensors', true);
xobj.onreadystatechange = function() {
if (xobj.readyState == 4 && xobj.status == "200") {
callback(xobj.responseText);
}
}
xobj.send(null);
}
function SoilmoistureRefresh() {
loadJSON(function(response) {
json = JSON.parse(response);
document.getElementById('iSoilmoistureRaw').textContent = json.soilmoistureRaw;
console.log(json.soilmoistureRaw);
});
}
</script>
)EOF";
const char HTMLsoilmoistureCalibrateText[] PROGMEM = R"EOF(<p class='helpbox'> <b>Calibration</b><br>
Put your soilmoisture sensor into dry soil and hit Refresh.<br>
Adjust the value of '<i>Soilmoisture dry</i>' if needed according to the reading.<br>
Repeat this with wet soil for '<i>Soilmoisture wet</i>'.</p>)EOF";

View file

@ -1,194 +0,0 @@
/*
*
* Constants
*
*/
const char* APssid = "CanGrow-unconfigured";
/*
* TODO - does not work atm. idk why.
* const char* APpass = "CanGrow";
const int APchannel = 6;
const bool APhidden = false;
*
*/
/*
*
* Variables
*
*/
// valSoilmoisture - contains the value of getSoilmoisture()
unsigned short valSoilmoisture;
// helper variable for pump control with soilmoisture
// average of last readings
unsigned short valSoilmoistureAvg = 0;
unsigned short valSoilmoistureAvg_tmp = 0;
byte valSoilmoistureAvg_count = 0;
short valSoilmoistureRaw;
// valTemperature - contains the value of getTemperature()
float valTemperature;
// valTemperature - contains the value of getHumidity()
float valHumidity;
// valWaterlevel - contains the value of getWaterlevel()
byte valWaterlevel;
// do we need a restart? (e.g. after wifi settings change)
bool NeedRestart;
bool FirstRun;
// which screen should be actually displayed
byte ScreenToDisplay = 0;
// how many seconds actual screen got displayed
byte ScreenIterationPassed = 0;
// VPD value - https://www.grower.ch/forum/threads/diy-grow-controller-cangrow-projektvorstellung.163654/page-4#post-4294197
float valVPD;
// DayNight -
// true Day , false night
bool DayNight;
bool MaintenanceMode = false;
unsigned long MaintenanceStarted = 0;
// helper variable to remember how many seconds the pump was
// already on within the actual watering cycle
byte PumpOnTimePassed = 0;
bool PumpOnManual = false;
/*
* millis timer
*
*/
unsigned long outputPrevTime = 0;
/*
*
* EEPROM saved variables
*
*/
//
// WiFi
//
// if empty, CanGrow start in AccessPoint mode
char WIFIssid[32];
char WIFIpassword[64];
// WIFIuseDHCP - if true, get IP by DHCP
bool WIFIuseDHCP;
IPAddress WIFIip(192,168,4,20);
IPAddress WIFInetmask(255,255,255,0);
IPAddress WIFIgateway(192,168,4,254);
IPAddress WIFIdns(0,0,0,0);
//char WebUiUsername[16] = "cangrow";
//char WebUiPassword[32] = "cangrow";
//
// System
//
// configured - if false, let the user configure system settings first
bool configured = false;
// NTP Offset
short NtpOffset;
// MoistureSensor_Type - contains which moisture sensor to use
// 1: analog capacitive sensor
// 2: I2C chirp sensor from catnip electronics
byte MoistureSensor_Type;
// SoilmoistureLow - contains the value , when soil moisture is assumed to be low,
byte SoilmoistureLow = 80;
// UsePump - is the pump used? bool
// PumpMode (short) 1: Pump on every n days, 2: Pump on when Soilmoisture <= SoilmoistureLow, 3: Both
byte UsePump;
// UseFan - is the fan used? bool
// PumpOnTime in seconds
byte PumpOnTime = 3;
byte UseFan;
// In case the user uses no 12V LED on the LED output and an relais instead
// we have to disable PWM. So we ask here for what kind of light user is going
bool UseLEDrelais;
bool UseFANrelais;
// Which temperature sensor to use?
byte TemperatureSensor_Type;
// which humidity sensor to use
byte HumiditySensor_Type;
unsigned short MaintenanceDuration = 300;
char Esp32CamIP[16];
// PumpLastOn (long) timestamp
unsigned long PumpLastOn;
bool OutputInvert;
unsigned short SoilmoistureWet;
unsigned short SoilmoistureDry;
unsigned short PWMFrequency = 13370;
byte DisplayScreenDuration = 3;
//
// Grow Stuff
//
// GrowName - contains the name of the grow/plant. Up to 32 byte
// if empty, let the user setup grow settings first
char GrowName[32];
// GrowStart - contains unix timestamp from date where grow starts (00:00)
// unsigned long is 8 byte
unsigned long GrowStart;
// DayOfGrow contains on which day the grow is
byte DayOfGrow;
// DaysVeg - contains how many days to be in vegetation phase
byte DaysVeg = 35;
// DaysBloom - contains how many days to be in bloom phase
byte DaysBloom = 49;
// LighthoursVeg - contains how many hours the Growlight is on in Veg
byte LighthoursVeg = 16;
// LighthoursBloom - contains how many hours the Growlight is on in Bloom
byte LighthoursBloom = 12;
// SunriseHour - contains to which hour of day the growlight turns on
byte SunriseHour = 7;
// SunriseHour - contains to which minute of SunriseHour the growlight turns on
byte SunriseMinute = 0;
// PinLEDPWM - contains the PWM value for dimming the grow light
// default is 255 to ensure it is just on for the case UseLEDrelais is true
byte PinLEDPWM = 255;
byte PinFANPWM = 255;
byte PinFAN2PWM = 255;
// fade in and out sunrise and sunset?
bool SunFade;
byte SunFadeDuration = 30;
// PumpIntervalVeg (int) in days
byte PumpIntervalVeg = 5;
byte PumpIntervalBloom = 3;
/*
*
* NTP
*
*/
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);
/*
*
* Webserver
*
*/
ESP8266WebServer webserver(80);
ESP8266HTTPUpdateServer webUpdater;
/* I2C Stuff
*
*/
#define WIRE Wire
/*
* Display Stuff
*/
Adafruit_SSD1306 display = Adafruit_SSD1306(128, 64, &WIRE);

View file

@ -1,31 +0,0 @@
/*
*
* Pin assignments
*
* D0 - MOSFET Pump
* D1, D2 - I2C
* D3 - Fan2 PWM
* D4 - PinWIPE
* D5 - MOSFET Fan1, PWM
* D6 - MOSFET Grow LED, PWM
* D7 - waterlevel (set HIGH to read value)
* D8 - analog soil moisture (set HIGH to read value)
* A0 - analog input for soil moisture and waterlevel readings
*
* D4 and D7 cannot be HIGH at the same time!
*/
// D0 is HIGH at boot, no PWM
const uint8_t PinPUMP = D0;
// If D3 is pulled to LOW, boot fails
//const uint8_t PINdht = D3;
// D4 is HIGH at boot, boot fail if pulled to LOW
// During Start Screen you can pull D4 to LOW to wipe saved data in EEPROM
// DO NOT PULL D4 DOWN AT WHEN POWERING ON !!! BOOT WILL FAIL
const uint8_t PinWIPE = D4;
const uint8_t PinFAN = D5;
const uint8_t PinFAN2 = D3;
const uint8_t PinLED = D6; //
const uint8_t PINwaterlevel = D7;
const uint8_t PINsoilmoisture = D8;
const uint8_t PINanalog = A0;

View file

@ -1,254 +0,0 @@
/*
* DHT Stuff
*
*/
// DHT support dropped to get a free pin for fan PWM
//#define DHTTYPE DHT11
//DHT dht(PINdht, DHTTYPE);
/*
* BME280 Stuff
*
*/
#define SEALEVELPRESSURE_HPA (1013.25)
// dirty way of having multiple addresses configurable
Adafruit_BME280 bme_0x76;
Adafruit_BME280 bme_0x77;
/*
* SHT30/31 Stuff
*
*/
Adafruit_SHT31 sht31_0x44 = Adafruit_SHT31();
Adafruit_SHT31 sht31_0x45 = Adafruit_SHT31();
/*
* Chirp functions
*/
void writeI2CRegister8bit(int addr, int value) {
Wire.beginTransmission(addr);
Wire.write(value);
Wire.endTransmission();
}
unsigned int readI2CRegister16bit(int addr, int reg) {
Wire.beginTransmission(addr);
Wire.write(reg);
Wire.endTransmission();
delay(20);
Wire.requestFrom(addr, 2);
unsigned int t = Wire.read() << 8;
t = t | Wire.read();
return t;
}
/*
*
* Sensor functions
*
*/
int getWaterlevel(bool returnRAW = false) {
/*
* waterlevelRAW
* ===========
* 0 - 199 : CRITICAL
* 200 - 399 : WARNING
* >400 : OK
*
* waterlevel
* ==========
* 2 : CRITICAL
* 1 : WARNING
* 0 : OK
*/
short waterlevelWARN = 200;
short waterlevelOK = 400;
short waterlevelRAW = 0;
byte waterlevel = 0;
// disable first PINsoilmoisture
digitalWrite(PINsoilmoisture, LOW);
// enable Vcc for water level sensor
digitalWrite(PINwaterlevel, HIGH);
// wait a bit to let the circuit stabilize
// TODO: replace delay() with millis()
delay(100);
// get the value
for(byte i = 0; i < 10 ; i++) {
waterlevelRAW = waterlevelRAW + analogRead(PINanalog);
}
waterlevelRAW = waterlevelRAW / 10;
// disable Vcc for the sensor to prevent electrolysis effect and release analog pin
digitalWrite(PINwaterlevel, LOW);
// and turn soilmoisture back on
digitalWrite(PINsoilmoisture, HIGH);
if( waterlevelRAW >= waterlevelOK) {
waterlevel = 0;
} else if( waterlevelRAW >= waterlevelWARN) {
waterlevel = 1;
} else {
waterlevel = 2;
}
return waterlevel;
}
float getTemperature(byte tempSensor) {
/*
* tempSensor
* ==========
* 1 : BME280 0x76 temp sensor
* 2 : BME280 0x77 temp sensor
* 3 : SHT31 0x44 temp sensor
* 4 : SHT31 0x45 temp sensor
* 5 : Chirp I2C 0x20 temp sensor
*/
float temperature = 0;
switch(tempSensor) {
case 1:
// read temperature from BME280
temperature = bme_0x76.readTemperature();
break;
case 2:
// read temperature from BME280
temperature = bme_0x77.readTemperature();
break;
case 3:
// read temp from SHT31
temperature = sht31_0x44.readTemperature();
break;
case 4:
// read temp from SHT31
temperature = sht31_0x45.readTemperature();
break;
case 5:
// read temperature from chrip I2C
temperature = readI2CRegister16bit(0x20, 5) * 0.10 ;
break;
default:
// if sensor type is not recognized, return 99
temperature = 99.99;
}
return temperature;
}
float getHumidity(byte HumSensor) {
/*
* sensors:
* 1 : BME280 0x76 humidity sensor
* 2 : BME280 0x77 humidity sensor
* 3 : SHT31 0x44 humidity sensor
* 4 : SHT31 0x45 humidity sensor
*
*/
float humidity;
switch(HumSensor) {
case 1:
humidity = bme_0x76.readHumidity();
break;
case 2:
humidity = bme_0x77.readHumidity();
break;
case 3:
humidity = sht31_0x44.readHumidity();
break;
case 4:
humidity = sht31_0x45.readHumidity();
break;
default:
humidity = 0.0;
break;
}
// return dht.readHumidity();
return humidity;
}
int getSoilmoisture(byte moistureSensor, bool returnRAW = false) {
/*
* moistureSensor
* ==============
* 1 : analog capacitive moisture sensor
* 2 : chirp I2C moisture sensor
*/
// value to return
int soilmoisture = 0;
// value for wet
//~ int wet;
// value for dry
//~ int dry;
switch(moistureSensor) {
case 1:
// read analog value from analog moisture sensor
//~ wet = 180;
// this value was measured in air, without contact to anything
//dry= 590;
// was measured in dry soil, not bone dry but really dry (6 days no watering)
//~ dry = 360;
digitalWrite(PINsoilmoisture, HIGH);
// wait a bit to let the circuit stabilize
//delay(50);
// get analog input value
// get values 10 times and get the middle for more precise data
for(byte i = 0; i < 10 ; i++) {
soilmoisture = soilmoisture + analogRead(PINanalog);
}
soilmoisture = soilmoisture / 10;
// disable Vcc for the sensor to release analog pin
digitalWrite(PINsoilmoisture, LOW);
break;
case 2:
// read soil moisture from chrip I2C
// this value was measured in water
// wet = 560;
// measured in fresh watered soil
//~ wet = 485;
//~ dry= 250;
// get raw value from I2C chirp sensor
soilmoisture = readI2CRegister16bit(0x20, 0);
break;
default:
//~ wet = 0;
//~ dry = 1;
soilmoisture = -1;
}
if(returnRAW == true) {
return soilmoisture;
} else {
short soilmoistureP = map(soilmoisture, SoilmoistureWet, SoilmoistureDry, 100, 0);
// dont return negative percentage values
if(soilmoistureP < 0) {
return 0;
} else {
return soilmoistureP;
}
}
}
int getLightchirp() {
// get the "light value" from I2C chirp module
writeI2CRegister8bit(0x20, 3); //request light measurement
int lightchirp = readI2CRegister16bit(0x20, 4);
return lightchirp;
}

View file

@ -1,843 +0,0 @@
/*
*
*
* System Functions
*
*
*/
void wipeEEPROM() {
Serial.println(":: wipe EEPROM ::");
// wipeMsg is helper variable to know if the Serial.print Message was
// already sent
byte wipeMsg = 0;
while(digitalRead(PinWIPE) == LOW ) {
// only show the Serial message once
if(wipeMsg == 0) {
Serial.println("Please release PinWIPE to erase all data saved in EEPROM");
Serial.println("LAST CHANCE TO KEEP THE DATA BY RESETTING NOW!!");
display.clearDisplay();
display.setCursor(0,0);
display.println("!!!!!!!!!!!!!!!!!!!!!");
display.println("");
display.println("RELEASE PinWIPE");
display.println("TO WIPE EEPROM");
display.display();
// increase i to show the serial message only once
wipeMsg = 1;
}
delay(500);
}
// write a 0 to all 512 bytes of the EEPROM
Serial.print("wiping EEPROM... ");
display.println("Wiping EEPROM...");
display.println("Will restart in 3s");
display.display();
for (int i = 0; i < 512; i++) { EEPROM.write(i, 0); }
// commit everything to EEPROM and end here
EEPROM.end();
Serial.println("DONE");
// set D4 PinWIPE internal LED to Output to give feedback WIPE
// was done
pinMode(PinWIPE, OUTPUT);
Serial.println("!! Device will restart in 3 seconds !!");
// let the internal led blink fast to signalize wipe is done
for(byte i = 0; i <= 24 ; i++) {
if(i % 2) {
digitalWrite(PinWIPE, LOW);
} else {
digitalWrite(PinWIPE, HIGH);
}
delay(125);
}
ESP.restart();
}
bool loadEEPROM() {
/*
* EEPROM Save table
*
* 0 WIFIssid
* 32 WIFIpassword
* 96 WIFIip
* 112 WIFInetmask
* 128 WIFIgateway
* 144 WIFIdns
* 160 WIFIuseDHCP
*
* 161 configured
* 162 UseFan
* 163 UsePump
* 164 PumpOnTime
* 165 MoistureSensor_Type
* 166 SoilmoistureLow
* 167 NtpOffset
* 169 UseLEDrelais
*
* 170 GrowName
* 202 GrowStart
* 206 DaysVeg
* 207 DaysBloom
* 208 LighthoursVet
* 209 LighthoursBloom
* 210 SunriseHour
* 211 SunriseMinute
* 212 DayOfGrow
*
* -- afterwards added, need to sort --
*
* 213 PinLEDPWM
* 214 TemperatureSensor_Type
* 215 UseFANrelais
* 216 PinFANPWM
* 217 SunFade
* 218 SunFadeDuration
* 219 MaintenanceDuration (2 byte)
* 221 Esp32CamIP (16 byte)
* 237 PumpLastOn (4 byte)
* 241 PumpIntervalVeg (1 byte)
* 242 PumpIntervalBloom (1 byte)
* 243 OutputInvert (1 byte)
* 244 SoilmoistureWet (2 byte)
* 246 SoilmoistureDry (2 byte)
* 248 PinFAN2PWM (1 byte)
* 249 HumiditySensor_Type (1 byte)
* 250 PWMFrequency (2 byte)
* 252 DisplayScreenDuration (1 byte)
* 253 ...
*
*/
Serial.println(":: loading EEPROM ::");
display.setCursor(0,36);
display.fillRect(0,36,128,64-36, 0);
display.println("loading EEPROM");
display.display();
// read var WIFIssid from address 0, 32 byte long
// read this first, because we decide on the ssid length (>0?) if
// we run in unconfigured AP mode, nor not
EEPROM.get(0, WIFIssid);
// when length is > 0 then read furter EEPROM config data
if(strlen(WIFIssid)) {
/*
* WIFI settings
*/
// read var WIFIpassword from address 32, 64 byte long
EEPROM.get(32, WIFIpassword);
// read var WIFIip from address 96, 16 byte long
EEPROM.get(96, WIFIip);
// read var WIFInetmask from address 112, 16 byte long
EEPROM.get(112, WIFInetmask);
// read var WIFIgateway from address 128, 16 byte long
EEPROM.get(128, WIFIgateway);
// read var WIFIgateway from address 128, 16 byte long
EEPROM.get(144, WIFIdns);
// read var WIFIuseDHCP from Address 160, 1 byte long
EEPROM.get(160, WIFIuseDHCP);
/*
* System settings
*/
// size is 1 byte
EEPROM.get(161, configured);
if(configured == true) {
// size is 1 byte
EEPROM.get(162, UseFan);
// size is 1 byte
EEPROM.get(163, UsePump);
// size is 1 byte
EEPROM.get(164, PumpOnTime);
// size is 1 byte
EEPROM.get(165, MoistureSensor_Type);
// size is 1 byte
EEPROM.get(166, SoilmoistureLow);
// size is 2 byte
EEPROM.get(167, NtpOffset);
// size is 1 byte
EEPROM.get(169, UseLEDrelais);
// size is 1 byte
EEPROM.get(214, TemperatureSensor_Type);
// size is 1 byte
EEPROM.get(215, UseFANrelais);
// size is 2 byte
EEPROM.get(219, MaintenanceDuration);
// size is 16 byte
EEPROM.get(221, Esp32CamIP);
// size is 4 byte
EEPROM.get(237, PumpLastOn);
// size is 1 byte
EEPROM.get(243, OutputInvert);
// size is 1 byte
EEPROM.get(244, SoilmoistureWet);
// size is 1 byte
EEPROM.get(246, SoilmoistureDry);
// size is 1 byte
EEPROM.get(249, HumiditySensor_Type);
// size is 2 byte
EEPROM.get(250, PWMFrequency);
// size is 1 byte
EEPROM.get(252, DisplayScreenDuration);
}
// TODO auth does not work atm
// EEPROM.get(160, WebUiUsername);
// EEPROM.get(176, WebUiPassword);
/*
* Grow settings
*/
// size is 32 byte
EEPROM.get(170, GrowName);
if(strlen(GrowName) > 0) {
// size is 4 byte
EEPROM.get(202, GrowStart);
// size is 1 byte
EEPROM.get(206, DaysVeg);
// size is 1 byte
EEPROM.get(207, DaysBloom);
// size is 1 byte
EEPROM.get(208, LighthoursVeg);
// size is 1 byte
EEPROM.get(209, LighthoursBloom);
// size is 1 byte
EEPROM.get(210, SunriseHour);
// size is 1 byte
EEPROM.get(211, SunriseMinute);
// size is 1 byte
EEPROM.get(212, DayOfGrow);
// size is 1 byte
EEPROM.get(213, PinLEDPWM);
// size is 1 byte
EEPROM.get(216, PinFANPWM);
EEPROM.get(217, SunFade);
EEPROM.get(218, SunFadeDuration);
// size is 1 byte
EEPROM.get(241, PumpIntervalVeg);
// size is 1 byte
EEPROM.get(242, PumpIntervalBloom);
// size is 1 byte
EEPROM.get(248, PinFAN2PWM);
}
// print values to Serial output
Serial.println("---- WiFi values ----");
Serial.print("WIFIssid: ");
Serial.println(WIFIssid);
Serial.print("WIFIpassword: ");
Serial.println(WIFIpassword);
Serial.print("Use DHCP: ");
Serial.println(WIFIuseDHCP);
Serial.println("---- System values ----");
Serial.print("configured: ");
Serial.println(configured);
Serial.print("UseFan: ");
Serial.println(UseFan);
Serial.print("UsePump: ");
Serial.println(UsePump);
Serial.print("PumpOnTime: ");
Serial.println(PumpOnTime);
Serial.print("MoistureSensor_Type: ");
Serial.println(MoistureSensor_Type);
Serial.print("TemperatureSensor_Type: ");
Serial.println(TemperatureSensor_Type);
Serial.print("SoilmoistureLow: ");
Serial.println(SoilmoistureLow);
Serial.print("NtpOffset: ");
Serial.println(NtpOffset);
Serial.print("UseLEDrelais: ");
Serial.println(UseLEDrelais);
Serial.print("UseFANrelais: ");
Serial.println(UseFANrelais);
Serial.print("MaintenanceDuration: ");
Serial.println(MaintenanceDuration);
Serial.print("PumpLastOn: ");
Serial.println(PumpLastOn);
Serial.print("OutputInvert: ");
Serial.println(OutputInvert);
Serial.print("SoilmoistureWet: ");
Serial.println(SoilmoistureWet);
Serial.print("SoilmoistureDry: ");
Serial.println(SoilmoistureDry);
Serial.print("HumiditySensor_Type: ");
Serial.println(HumiditySensor_Type);
Serial.print("PWMFrequency: ");
Serial.println(PWMFrequency);
Serial.println("---- Grow values ----");
Serial.print("GrowName: ");
Serial.println(GrowName);
Serial.print("GrowStart: ");
Serial.println(GrowStart);
Serial.print("DaysVeg: ");
Serial.println(DaysVeg);
Serial.print("DaysBloom: ");
Serial.println(DaysBloom);
Serial.print("LighthoursVeg: ");
Serial.println(LighthoursVeg);
Serial.print("LighthoursBloom: ");
Serial.println(LighthoursBloom);
Serial.print("SunriseHour: ");
Serial.println(SunriseHour);
Serial.print("SunriseMinute: ");
Serial.println(SunriseMinute);
Serial.print("DayOfGrow: ");
Serial.println(DayOfGrow);
Serial.print("PinLEDPWM: ");
Serial.println(PinLEDPWM);
Serial.print("PinFANPWM: ");
Serial.println(PinFANPWM);
Serial.print("PinFAN2PWM: ");
Serial.println(PinFAN2PWM);
Serial.print("SunFade: ");
Serial.println(SunFade);
Serial.print("SunFadeDuration: ");
Serial.println(SunFadeDuration);
} else {
Serial.println("EEPROM value WIFIssid is empty");
}
Serial.println(":: EEPROM loaded ::");
display.setCursor(0,42);
display.println("EEPROM loaded");
display.display();
return(strlen(WIFIssid));
}
void wifiConnect() {
Serial.println(":: Connecting to WiFi ::");
FirstRun = false;
Serial.print("SSID: ");
Serial.println(WIFIssid);
display.fillRect(0,36,128,64-36, 0);
display.setCursor(0,36);
display.println("Connecting to WiFi");
display.println(WIFIssid);
display.display();
// Start WiFi connection
WiFi.begin(WIFIssid, WIFIpassword);
if(WIFIuseDHCP == false) {
WiFi.config(WIFIip, WIFIdns, WIFIgateway, WIFInetmask);
}
// wait until WiFi connection is established
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" CONNECTED!");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
Serial.println(":: Getting time from NTP ::");
display.fillRect(0,36,128,64-36, 0);
display.setCursor(0,36);
display.println("Getting NTP time");
display.display();
timeClient.begin();
timeClient.setTimeOffset(NtpOffset * 60 * 60);
timeClient.update();
while ( ! timeClient.isTimeSet()) {
timeClient.update();
delay(500);
Serial.print(".");
}
Serial.println(timeClient.getFormattedTime());
Serial.println(timeClient.getEpochTime());
display.println(timeClient.getFormattedTime());
display.display();
display.print("IP: ");
display.print(WiFi.localIP());
display.display();
}
void wifiAp() {
Serial.println(":: Creating Accesspoint ::");
display.fillRect(0,36,128,64-36, 0);
display.setCursor(0,36);
display.println("Creating AccessPoint");
display.println(APssid);
display.display();
FirstRun = true;
// configure WiFi Access Point
WiFi.softAPConfig(WIFIip, WIFIgateway, WIFInetmask);
// start Access Point
// TODO make AP with password - does not work atm. idk why.
WiFi.softAP(APssid);
Serial.print("SSID: ");
Serial.println(APssid);
Serial.print("CanGrow IP address: ");
Serial.println(WiFi.softAPIP());
display.print("IP: ");
display.println(WiFi.softAPIP());
display.display();
// TODO does not work atm, idk why
//Serial.println("The login credentials for the WebUI are 'cangrow' for username and password");
}
unsigned short growState() {
/*
* growState()
*
* returns growState as short
*
* 1 - vegetation
* 2 - bloom
* 3 - harvest
*
*/
unsigned short state;
if(DayOfGrow > (DaysVeg + DaysBloom ) ) {
state = 3;
} else if(DayOfGrow > DaysVeg ) {
state = 2;
} else {
state = 1;
}
return state;
}
void setOutput(byte Output, byte OutputState) {
/*
* Output assignments
*
* 1 - LED
* 2 - FAN
* 3 - PUMP
* 4 - FAN2
*
*/
bool UseRelais = true;
byte OutputPin;
byte OutputState_tmp;
switch(Output) {
case 1:
OutputPin = PinLED;
if(UseLEDrelais == true) {
UseRelais = true;
} else {
UseRelais = false;
}
break;
case 2:
OutputPin = PinFAN;
if(UseFANrelais == true) {
UseRelais = true;
} else {
UseRelais = false;
}
break;
// PUMP Pin (D0) does not support PWM, so we do not need to care about
case 3:
OutputPin = PinPUMP;
break;
case 4:
OutputPin = PinFAN2;
UseRelais = false;
break;
}
//~ Serial.print("Output: ");
//~ Serial.println(Output);
//~ Serial.print("OutputPin: ");
//~ Serial.println(OutputPin);
//~ Serial.print("OutputState: ");
//~ Serial.println(OutputState);
//~ Serial.print("UseRelais: ");
//~ Serial.println(UseRelais);
// TODO read config for inverted outputs
if( (UseRelais == true) || (OutputPin == PinPUMP) ) {
// convert OutputState to bool when using relais, so we can invert it easy
bool OutputState_bool = OutputState;
if(OutputInvert == true) {
OutputState_tmp = 1 - OutputState_bool;
} else {
OutputState_tmp = OutputState_bool;
}
digitalWrite(OutputPin, OutputState_tmp);
} else {
// when OutputInvert is set true AND output is not Fan2, invert
// for the 4-pin Fan PWM we dont need to invert, this could
if( (OutputInvert == true) && (OutputPin != PinFAN2) ) {
OutputState_tmp = 255 - OutputState;
} else {
OutputState_tmp = OutputState;
}
analogWrite(OutputPin, OutputState_tmp);
}
}
void controlLED() {
byte lightHours;
byte PinLEDPWM_tmp;
unsigned int secondsSunrise = (SunriseHour * 60 * 60) + (SunriseMinute * 60);
unsigned int secondsToday = (timeClient.getHours() * 60 * 60) + (timeClient.getMinutes() * 60) + timeClient.getSeconds();
switch(growState()) {
case 1:
lightHours = LighthoursVeg;
break;
case 2:
lightHours = LighthoursBloom;
break;
default:
lightHours = 0;
break;
}
// check if secondsToday is larger then secondsSunrise time AND if
// secondsToday is smaller then the sum of secondsSunrise + seconds of lightHours
if( ((secondsToday >= secondsSunrise) && (secondsToday <= ( secondsSunrise + (lightHours * 60 * 60))) ) && (growState() < 3) ){
//Serial.println("light on time");
// when SunFade is true, fade LED light. Otherwise just turn on or off
if( (SunFade == true) && (UseLEDrelais == false) && (secondsSunrise + SunFadeDuration * 60 >= secondsToday) ) {
// in the first n minutes of lighting (SunFadeDuration), we want
// to raise the light slowly to prevent stress from the plant
// convert progress sunrise to PWM value
PinLEDPWM_tmp = (SunFadeDuration * 60 - ((secondsSunrise + SunFadeDuration * 60) - secondsToday)) * PinLEDPWM / (SunFadeDuration * 60);
setOutput(1, PinLEDPWM_tmp);
//Serial.print("sunrise PWM; ");
//Serial.println(PinLEDPWM_tmp);
} else if( (SunFade == true) && (UseLEDrelais == false) && (secondsToday >= ((secondsSunrise + lightHours * 60 * 60) - SunFadeDuration * 60) ) ) {
// calculate progress sunset to PWM value
PinLEDPWM_tmp = (secondsSunrise + (lightHours * 60 * 60) - secondsToday) * PinLEDPWM / (SunFadeDuration * 60);
setOutput(1, PinLEDPWM_tmp);
//Serial.print("sunset PWM: ");
//Serial.println(PinLEDPWM_tmp);
} else {
//Serial.println("just turn on the light");
// no sunrise or sunset, just keep the LED turned on
setOutput(1, PinLEDPWM);
}
// its daytime
DayNight = true;
} else {
//Serial.println("good night time");
// turn off
setOutput(1, 0);
// nighttime
DayNight = false;
}
}
void refreshSensors() {
byte soilmoistureAvgSampleCount = 5;
valSoilmoisture = getSoilmoisture(MoistureSensor_Type);
valSoilmoistureRaw = getSoilmoisture(MoistureSensor_Type, true);
valHumidity = getHumidity(HumiditySensor_Type);
valTemperature = getTemperature(TemperatureSensor_Type);
valWaterlevel = getWaterlevel();
// get average of 5 readings for valSoilmoisture
valSoilmoistureAvg_tmp = valSoilmoistureAvg_tmp + valSoilmoisture;
if(valSoilmoistureAvg_count < soilmoistureAvgSampleCount - 1) {
valSoilmoistureAvg_count++;
} else {
// build average
valSoilmoistureAvg = valSoilmoistureAvg_tmp / soilmoistureAvgSampleCount;
// reset everything
valSoilmoistureAvg_tmp = 0;
valSoilmoistureAvg_count = 0;
}
}
void displayScreens() {
/*
* which screen to display
* interate through different screens
*
*/
if(ScreenIterationPassed > DisplayScreenDuration){
ScreenIterationPassed = 0;
// helper variable, maybe i find a better way in future
byte LastScreen = 2;
// when the next screen gets displayed, clear display
display.clearDisplay();
display.display();
// when ScreenToDisplay has reach last number of screens, reset to first (0)
if(ScreenToDisplay >= LastScreen) {
ScreenToDisplay = 0;
} else {
ScreenToDisplay++;
}
}
display.setCursor(0,0);
if(MaintenanceMode == true) {
display.drawBitmap(0, 0, bmpCanGrow_Logo, 128, 32, WHITE);
display.display();
display.setCursor(0,32);
display.println("Maintenance mode active");
display.print("Time left: ");
display.print(MaintenanceDuration - ((millis() - MaintenanceStarted) / 1000));
display.println("s");
} else {
// in this switch case the single screens gets defined
// when DisplayScreenDuration is 0, always show 0
if(DisplayScreenDuration == 0) {
ScreenToDisplay = 0;
}
switch(ScreenToDisplay) {
// switch(0) { // just for testing, show screen 0 only
case 0:
display.print("Humidity: ");
display.print(valHumidity);
display.println(" %");
display.println("");
display.print("Temperature: ");
display.print(valTemperature);
display.println(" C");
display.println("");
display.print("Moisture: ");
display.print(valSoilmoisture);
display.print(" % ");
display.println(valSoilmoistureAvg);
display.println("");
if(UsePump > 0) {
display.print("Pump Waterlvl: ");
switch(valWaterlevel) {
case 0:
display.println("OK");
break;
case 1:
display.println("Warn");
break;
case 2:
display.println("Crit");
break;
}
}
break;
case 1:
display.print("LED: ");
display.print(PinLEDPWM * 100 / 255);
display.println(" %");
display.print("State: ");
display.println(digitalRead(PinLED));
display.println("");
display.print("FAN: ");
display.print(PinFANPWM * 100 / 255);
display.println(" %");
display.print("State: ");
display.println(digitalRead(PinFAN));
display.println("");
display.print("Pump state: ");
display.println(digitalRead(PinPUMP));
break;
case 2:
// display Logo
display.drawBitmap(0, 0, bmpCanGrow_Logo, 128, 32, WHITE);
display.display();
display.setCursor(0,32);
display.println(GrowName);
display.print("DoG: ");
display.print(DayOfGrow);
display.print(", ");
display.println(timeClient.getFormattedTime());
display.print("IP: ");
display.println(WiFi.localIP());
break;
}
}
ScreenIterationPassed++;
display.display();
}
/*
* Pump control
*
* Vars:
* - UsePump (byte)
* - PumpOnTime (byte) in sec
* - SoilmoistureLow (byte) in %
* - PumpLastOn (long) timestamp
* - PumpMode (byte) 1: Pump on every n days, 2: Pump on when Soilmoisture <= SoilmoistureLow, 3: Both
*
* - PumpIntervalVeg (byte) in days
* - PumpIntervalBloom (byte) in days
*
*/
void controlPUMP() {
byte PumpInterval;
// UsePump true and not in harvest state?
// dont water within the first 15 sec after startup
if ( (UsePump > 0) && (growState() < 3) && (millis() > 15000) ) {
switch(growState()) {
case 1:
PumpInterval = PumpIntervalVeg;
break;
case 2:
PumpInterval = PumpIntervalBloom;
break;
default:
PumpInterval = 0;
break;
}
// when PumpOnManuel is true, turn pump on for PumpOnTime seconds
if(PumpOnManual == true) {
if(PumpOnTimePassed < PumpOnTime) {
setOutput(3, 1);
//digitalWrite(PinPUMP, HIGH);
PumpOnTimePassed++;
} else {
PumpOnManual = false;
setOutput(3, 0);
//digitalWrite(PinPUMP, LOW);
PumpLastOn = timeClient.getEpochTime();
EEPROM.put(237, PumpLastOn);
EEPROM.commit(); //write to EEPROM
PumpOnTimePassed = 0;
}
// otherwise check which PumpMode to use
} else {
switch(UsePump) {
case 1:
// when diff of time now and time pumpLastOn is greater then PumpInterval, do some watering (Or manual watering)
if( (timeClient.getEpochTime() - PumpLastOn) >= (PumpInterval * 24 * 60 *60) ) { // PumpInterval to, Days * 24 * 60 * 60
// only water as long PumpOnTime
if(PumpOnTimePassed < PumpOnTime) {
setOutput(3, 1);
//digitalWrite(PinPUMP, HIGH);
PumpOnTimePassed++;
} else {
setOutput(3, 0);
//digitalWrite(PinPUMP, LOW);
PumpLastOn = timeClient.getEpochTime();
// write the value to EEPROM for the case ESP gets restarted
EEPROM.put(237, PumpLastOn);
EEPROM.commit(); //write to EEPROM
PumpOnTimePassed = 0;
}
} else {
setOutput(3, 0);
//digitalWrite(PinPUMP, LOW);
}
break;
case 2:
// when valSoilmoistureAvg is lower then SoilMoisture low do some watering
if( (valSoilmoistureAvg < SoilmoistureLow) || ( (valSoilmoistureAvg >= SoilmoistureLow) && ( (PumpOnTimePassed > 0) && (PumpOnTimePassed <= PumpOnTime) ) ) ) {
// check if we alerady exceeded max PumpOnTime
if(PumpOnTimePassed < PumpOnTime) {
setOutput(3, 1);
//digitalWrite(PinPUMP, HIGH);
PumpOnTimePassed++;
} else {
setOutput(3, 0);
//digitalWrite(PinPUMP, LOW);
PumpLastOn = timeClient.getEpochTime();
PumpOnTimePassed = 0;
}
// when valSoilmoistureAvg is greater then the Low value,
} else {
setOutput(3, 0);
//digitalWrite(PinPUMP, LOW);
}
break;
//
case 3:
if( ( (timeClient.getEpochTime() - PumpLastOn) >= (PumpInterval * 24 * 60 *60) ) && // PumpInterval to, Days * 24 * 60 * 60
( (valSoilmoistureAvg < SoilmoistureLow) ||
( (valSoilmoistureAvg >= SoilmoistureLow) && ( (PumpOnTimePassed > 0) && (PumpOnTimePassed <= PumpOnTime) ) )
) ) {
// check if we alerady exceeded max PumpOnTime
if(PumpOnTimePassed < PumpOnTime) {
setOutput(3, 1);
//digitalWrite(PinPUMP, HIGH);
PumpOnTimePassed++;
} else {
setOutput(3, 0);
//digitalWrite(PinPUMP, LOW);
PumpLastOn = timeClient.getEpochTime();
EEPROM.put(237, PumpLastOn);
EEPROM.commit(); //write to EEPROM
PumpOnTimePassed = 0;
}
// when valSoilmoistureAvg is greater then the Low value,
} else {
setOutput(3, 0);
//digitalWrite(PinPUMP, LOW);
}
break;
}
}
} else {
// ensure pump is off when it should be off
setOutput(3, 0);
//digitalWrite(PinPUMP, LOW);
}
}
void controlFAN() {
// FAN1
setOutput(2, PinFANPWM);
// FAN2
setOutput(4, PinFAN2PWM);
}
void initOutputs() {
pinMode(PinLED, OUTPUT);
pinMode(PinPUMP, OUTPUT);
pinMode(PinFAN, OUTPUT);
// set PWM frequency
analogWriteFreq(PWMFrequency);
for(byte i = 1; i <= 4; i++) {
setOutput(i, 0);
}
}

View file

@ -1,13 +0,0 @@
/*
*
*
* Version
*
*
*/
#define CANGROW_VER "0.1.4"
// CANGROW_BUILD default dummy value if not set as Compiler Flag
#ifndef CANGROW_BUILD
#define CANGROW_BUILD "1a2b3c4-0000000000000"
#endif

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,50 @@
# CanGrow - Arduino Code
# CanGrow - An OpenSource grow controller firmware
Here the Code for CanGrow is contained.
A ESP8266 with Arduino Firmware is used (should most times be the standard already).
## Build environment
The helper script `cangrow.sh` is written for a Debian 12 system.
To install all dependencies you need for building the firmware, run the cangrow.sh setup:
```sh
$ ./cangrow.sh help
./cangrow.sh [setup|build|upload|webupload|monitor]
setup: setup build environment, download arduino-cli, install all dependencies for arduino ide
build: build firmware binary. will be saved into build/
upload: upload firmware by serial connection /dev/ttyUSB0
webupload: upload firmware with webupload to 192.168.4.20
monitor: serial monitor /dev/ttyUSB0
# Install all dependencies for build environment
$ ./cangrow.sh setup
```
The script installs [arduino-cli](https://github.com/arduino/arduino-cli) to `~/.local/bin/arduino-cli`.
## Compile
```sh
# compile and output to build/CanGrow_v0.2...bin
# Default Target is ESP8266 D1 Mini
$ ./cangrow.sh build
# Compile for ESP32 D1 Mini
$ export BOARD="esp32:esp32:d1_mini32"
$ ./cangrow.sh build
# Build and webupload to IP
$ export IP="192.168.4.69"
$ ./cangrow.sh build # need to make .bin first
$ ./cangrow.sh webupload # upload
# listen to serial monitor on /dev/ttyUSB2
$ export TTY="/dev/ttyUSB2"
./cangrow.sh monitor
```
I wrote this project using [Geany IDE. ](https://www.geany.org/). The Geany Projectfile is also included, just run
```sh
$ geany CanGrow.geany
```
I wrote this project using [Geany IDE. ](https://www.geany.org/). The Geany Projectfile is also included.
Compiling is done with [arduino-cli](https://github.com/arduino/arduino-cli). It is supposed to be in
`~/.local/bin/arduino-cli`.
**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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

After

Width:  |  Height:  |  Size: 291 KiB

View file

@ -1,3 +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

145
Arduino/CanGrow/cangrow.sh Executable file
View file

@ -0,0 +1,145 @@
#!/bin/bash
#
test -z $TTY && TTY="/dev/ttyUSB0"
test -z $IP && IP="192.168.4.20"
test -z $VER && 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)-$(date '+%Y%m%d%H%M%S')"
ACLI="$HOME/.local/bin/arduino-cli"
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)"
declare -a CORES=(
"esp8266:esp8266@3.1.2"
"esp32:esp32@3.0.5"
)
declare -a LIBS=(
"Adafruit SSD1306@2.5.12"
"Adafruit BME280 Library@2.2.4"
"ArduinoJson@7.2.0"
"NTPClient@3.2.1"
"Time@1.6.1"
"ESP Async WebServer@3.3.17"
"Async TCP@3.2.10"
"ESPAsyncTCP@1.2.4"
)
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 1.0.0 into ${ACLI_DIR}/"
wget -O - "https://github.com/arduino/arduino-cli/releases/download/v1.0.4/arduino-cli_1.0.4_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 ":: 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,282 @@
/*
*
* include/CanGrow.h - main header file
*
*
* 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.
*
*/
//#include "CanGrow_Version.h"
/* 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.x-dev"
#endif
#ifndef CANGROW_BUILD
#define CANGROW_BUILD "0000"
#endif
#define CANGROW_SSID "CanGrow-unconfigured"
/* 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
*/
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;
char BOOTFAILS_LOW_descr[] = "BF_LOW";
char BOOTFAILS_HIGH_descr[] = "BF_HIGH";
char FLASMODE_LOW_descr[] = "FM_LOW";
char INPUT_ONLY_descr[] = "IN_ONLY";
char NO_PWM_descr[] = "NO_PWM";
char HIGH_BOOT_descr[] = "B_HIGH";
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
};
/*
* Output Type
*/
// 0 is unconfigured
const byte Output_Type_total = 4;
const byte OUTPUT_TYPE_GPIO = 1;
const byte OUTPUT_TYPE_I2C = 2;
const byte OUTPUT_TYPE_WEB = 3;
char OUTPUT_TYPE_GPIO_descr[] = "GPIO";
char OUTPUT_TYPE_I2C_descr[] = "I2C";
char OUTPUT_TYPE_WEB_descr[] = "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 Device
*/
// 0 is unconfigured
const byte Output_Device_total = 7;
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;
char OUTPUT_DEVICE_LIGHT_descr[] = "Light";
char OUTPUT_DEVICE_FAN_descr[] = "Fan";
char OUTPUT_DEVICE_PUMP_descr[] = "Pump";
char OUTPUT_DEVICE_HUMIDIFIER_descr[] = "Humidifier";
char OUTPUT_DEVICE_DEHUMIDIFIER_descr[] = "Dehumidifier";
char OUTPUT_DEVICE_HEATING_descr[] = "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,
};
struct GPIO_Index {
const byte gpio;
const byte note;
};
const byte Max_Outputs = 16;
/*
*
* Config
*
*/
/*
* 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
*
* - 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
* - gpio_invert: invert gpio 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 gpio_invert[Max_Outputs];
char i2c[Max_Outputs][8];
char webcall_host[Max_Outputs][32];
char webcall_path_on[Max_Outputs][32];
char webcall_path_off[Max_Outputs][32];
};
/* main System struct */
struct Config_System {
byte ntpOffset;
unsigned short maintenanceDuration;
char esp32camIp[16];
char httpUser[32];
char httpPass[32];
bool httpLogSerial;
unsigned short schedulerInterval = 1000;
Config_System_Output output;
};
/*
* Config Grow
*/
struct Config_Grow {
char growName[64] = "CanGrow";
unsigned short dayOfGrow;
byte daysSeed;
byte daysVeg;
byte daysBloom;
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;

View file

@ -0,0 +1,114 @@
/*
*
* include/CanGrow_Core.h - core stuff header file
*
*
* 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.
*
*/
// blink fast with the built in LED in an infinite loop
void Restart() {
Serial.println(":: [Core:Restart] got triggered, restarting in 2 seconds");
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() {
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;
}
}
#ifndef DEBUG
Serial.printf("DB [Core:Give_Free_OutputId] next free output id: %d\n", outputId_free);
#endif
return outputId_free;
}
//bool Check_OutputId_Used(byte outputId) {
//}
// checks if GPIO is already in use by output or sensor
bool Check_GPIOindex_Used(byte gpio) {
bool used;
#ifndef DEBUG
Serial.printf("DB [Core:Check_GPIOindex_Used] check GPIO: %d\n", gpio);
#endif
// go through each outputid
for(byte i=0; i < Max_Outputs; i++) {
#ifndef DEBUG
//Serial.printf("DB [Core:Check_GPIOindex_Used] OutputId: %d , type: %d\n", i, config.system.output.type[i]);
#endif
// check if output type is gpio
if(config.system.output.type[i] == OUTPUT_TYPE_GPIO) {
#ifndef DEBUG
Serial.printf("DB [Core:Check_GPIOindex_Used] OutputId: %d is GPIO (type %d)\n", i, config.system.output.type[i]);
#endif
// check if gpio id is already in use
if(config.system.output.gpio[i] == gpio) {
#ifndef DEBUG
Serial.printf("DB [Core:Check_GPIOindex_Used] output.gpio[%d](%d) == GPIO %d\n", i, config.system.output.gpio[i], gpio);
#endif
used = true;
break;
} else {
used = false;
}
}
}
#ifndef DEBUG
Serial.printf("DB [Core:Check_GPIOindex_Used] GPIO: %d, used: %d\n", gpio, used);
#endif
// check sensors
return used;
}

View file

@ -0,0 +1,87 @@
/*
*
* include/CanGrow_ESP32.h - ESP32 specific header file
*
*
* 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.
*
*/
#ifdef ESP32
#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[] = { { 0, FLASHMODE_LOW },
{ 4 },
{ 5 },
{ 12, BOOTFAILS_HIGH },
{ 13 },
{ 14 },
{ 15 },
{ 16 },
{ 17 },
{ 18 },
{ 19 },
{ 23 },
{ 25 },
{ 26 },
{ 27 },
{ 32 },
{ 33 },
{ 34, INPUT_ONLY },
{ 35, INPUT_ONLY },
{ 36, INPUT_ONLY },
{ 39, INPUT_ONLY } };
#endif

View file

@ -0,0 +1,57 @@
/*
*
* include/CanGrow_ESP8266.h - ESP8266 specific header file
*
*
* 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.
*
*/
#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[] = { { 0, BOOTFAILS_LOW },
{ 12 },
{ 13 },
{ 14 },
{ 15, BOOTFAILS_HIGH },
{ 16, NO_PWM } };
#endif

View file

@ -0,0 +1,583 @@
/*
*
* include/CanGrow_LittleFS.h - LittleFS handling header file
*
*
* 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.
*
*/
#define CANGROW_CFG "/config.json"
// LittleFS auto format
#define FORMAT_LITTLEFS_IF_FAILED true
void LFS_Init() {
Serial.println(":: [LittleFS] initializing");
// 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
Serial.println("!! [LittleFS] FAILED initializing. You have to format LittleFS manually. Will now restart.");
Restart();
}
}
void LFS_Format() {
Serial.println(":: [LittleFS] formatting...");
// 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()) {
Serial.println(":: [LittleFS] done formatting");
} else {
Serial.println("!! [LittleFS] FAILED formatting");
}
}
bool existFile(const char *path) {
#ifdef ESP8266
File file = LittleFS.open(path, "r");
#endif
#ifdef ESP32
fs::FS &fs = LittleFS;
File file = fs.open(path);
#endif
if (!file) {
Serial.printf(":: [LittleFS] file does not exist: %s\n", path);
file.close();
return false;
} else {
Serial.printf(":: [LittleFS] file does exist: %s\n", path);
file.close();
return true;
}
}
void readFile(const char *path) {
#ifdef ESP8266
File file = LittleFS.open(path, "r");
#endif
#ifdef ESP32
fs::FS &fs = LittleFS;
File file = fs.open(path);
#endif
if (!file) {
Serial.printf(":: [LittleFS] FAILED to open file for reading: %s\n");
return;
}
Serial.printf(":: [LittleFS] file content: %s\n", path);
Serial.println("----");
while (file.available()) { Serial.write(file.read()); }
Serial.println("\n----");
file.close();
}
void writeFile(const char *path, const char *message) {
#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) {
Serial.printf(":: [LittleFS] FAILED to open file for writing: %s\n", path);
return;
}
if (file.print(message)) {
Serial.printf(":: [LittleFS] file written: %s\n", path);
} else {
Serial.printf(":: [LittleFS] writing file FAILED: %s\n", path);
}
//delay(2000); // Make sure the CREATE and LASTWRITE times are different
file.close();
}
void deleteFile(const char *path) {
#ifdef ESP32
fs::FS &fs = LittleFS;
File file = fs.open(path, FILE_WRITE);
#endif
Serial.printf(":: [LittleFS] deleting file: %s\n", path);
#ifdef ESP8266
if (LittleFS.remove(path)) {
#endif
#ifdef ESP32
if (fs.remove(path)) {
#endif
Serial.printf(":: [LittleFS] deleted file: %s\n", path);
} else {
Serial.printf(":: [LittleFS] deleting file FAILED: %s\n", path);
}
}
// https://arduinojson.org/v7/example/config/
// https://arduinojson.org/v7/assistant/
bool LoadConfig() {
#ifdef ESP8266
File file = LittleFS.open(CANGROW_CFG, "r");
#endif
#ifdef ESP32
fs::FS &fs = LittleFS;
File file = fs.open(CANGROW_CFG);
#endif
Serial.printf(":: [LittleFS:LoadConfig] loading config from: %s\n", CANGROW_CFG);
JsonDocument doc;
// Deserialize the JSON document
DeserializationError error = deserializeJson(doc, file);
if(error) {
Serial.printf(":: [LittleFS:LoadConfig] FAILED to load config: %s\n", 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];
strlcpy(config.wifi.ssid, objWifi["ssid"], sizeof(config.wifi.ssid));
strlcpy(config.wifi.password, objWifi["password"], sizeof(config.wifi.password));
// Copy bool / int directly into struct
config.wifi.dhcp = objWifi["dhcp"];
// load the ip addresses as array
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];
config.system.ntpOffset = objSystem["ntpOffset"];
config.system.maintenanceDuration = objSystem["maintenanceDuration"];
strlcpy(config.system.esp32camIp, objSystem["esp32camIp"], sizeof(config.system.esp32camIp));
strlcpy(config.system.httpUser, objSystem["httpUser"], sizeof(config.system.httpUser));
strlcpy(config.system.httpPass, objSystem["httpPass"], sizeof(config.system.httpPass));
config.system.httpLogSerial = objSystem["httpLogSerial"];
config.system.schedulerInterval = objSystem["schedulerInterval"];
JsonObject objSystemOutput = objSystem["output"][0];
/* System Outputs */
for(byte i=0; i < Max_Outputs; i++) {
if(objSystemOutput["type"][i] > 0) {
config.system.output.type[i] = objSystemOutput["type"][i];
config.system.output.device[i] = objSystemOutput["device"][i];
strlcpy(config.system.output.name[i], objSystemOutput["name"][i], sizeof(config.system.output.name[i]));
config.system.output.enabled[i] = objSystemOutput["enabled"][i];
// gpio
config.system.output.gpio[i] = objSystemOutput["gpio"][i];
config.system.output.gpio_invert[i] = objSystemOutput["gpio_invert"][i];
config.system.output.gpio_pwm[i] = objSystemOutput["gpio_pwm"][i];
// i2c
strlcpy(config.system.output.i2c[i], objSystemOutput["i2c"][i], sizeof(config.system.output.i2c[i]));
// web
strlcpy(config.system.output.webcall_host[i], objSystemOutput["webcall_host"][i], sizeof(config.system.output.webcall_host[i]));
strlcpy(config.system.output.webcall_path_on[i], objSystemOutput["webcall_path_on"][i], sizeof(config.system.output.webcall_path_on[i]));
strlcpy(config.system.output.webcall_path_off[i], objSystemOutput["webcall_path_off"][i], sizeof(config.system.output.webcall_path_off[i]));
}
}
/* Grow */
JsonObject objGrow = doc["grow"][0];
strlcpy(config.grow.growName, objGrow["growName"], sizeof(config.grow.growName));
config.grow.dayOfGrow = objGrow["dayOfGrow"];
config.grow.daysSeed = objGrow["daysSeed"];
config.grow.daysVeg = objGrow["daysVeg"];
config.grow.daysBloom = objGrow["daysBloom"];
config.grow.lightHoursVeg = objGrow["lightHoursVeg"];
config.grow.lightHoursBloom = objGrow["lightHoursBloom"];
config.grow.sunriseHour = objGrow["sunriseHour"];
config.grow.sunriseMinute = objGrow["sunriseMinute"];
config.grow.sunFade = objGrow["sunFade"];
config.grow.sunFadeDuration = objGrow["sunFadeDuration"];
// Close the file (Curiously, File's destructor doesn't close the file)
file.close();
Serial.println(":: [LittleFS:LoadConfig] config successfully loaded");
Serial.println(":: [LittleFS:LoadConfig] --- runtime config ---");
serializeJsonPretty(doc, Serial);
Serial.println("");
Serial.println(":: [LittleFS:LoadConfig] ----------------------");
return true;
}
bool SaveConfig(bool writeToSerial = false) {
/*
* 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["esp32camIp"] = config.system.esp32camIp;
objSystem["httpUser"] = config.system.httpUser;
objSystem["httpPass"] = config.system.httpPass;
objSystem["httpLogSerial"] = config.system.httpLogSerial;
objSystem["schedulerInterval"] = config.system.schedulerInterval;
/* 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["gpio_invert"][i] = config.system.output.gpio_invert[i];
objSystemOutput["gpio_pwm"][i] = config.system.output.gpio_pwm[i];
// i2c
objSystemOutput["i2c"][i] = config.system.output.i2c[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];
}
}
/* Grow */
JsonObject objGrow = doc["grow"].add<JsonObject>();
objGrow["growName"] = config.grow.growName;
objGrow["dayOfGrow"] = config.grow.dayOfGrow;
objGrow["daysSeed"] = config.grow.daysSeed;
objGrow["daysVeg"] = config.grow.daysVeg;
objGrow["daysBloom"] = config.grow.daysBloom;
objGrow["lightHoursVeg"] = config.grow.lightHoursVeg;
objGrow["lightHoursBloom"] = config.grow.lightHoursBloom;
objGrow["sunriseHour"] = config.grow.sunriseHour;
objGrow["sunriseMinute"] = config.grow.sunriseMinute;
objGrow["sunFade"] = config.grow.sunFade;
objGrow["sunFadeDuration"] = config.grow.sunFadeDuration;
/*
* 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) {
Serial.printf("!! [LittleFS:SaveConfig] FAILED to open configfile for writing: %s\n", CANGROW_CFG);
return false;
} else {
Serial.printf(":: [LittleFS:SaveConfig] opened for writing %s\n", CANGROW_CFG);
}
// Serialize JSON to file
if (serializeJson(doc, file) == 0) {
Serial.printf("!! [LittleFS:SaveConfig] FAILED to write configfile: %s\n", CANGROW_CFG);
} else {
Serial.printf(":: [LittleFS:SaveConfig] successfully written %s\n", CANGROW_CFG);
}
file.close();
} else {
Serial.printf(":: [LittleFS:SaveConfig] --- %s ---\n", CANGROW_CFG);
serializeJsonPretty(doc, Serial);
Serial.println("");
Serial.printf(":: [LittleFS:SaveConfig] ----------------------\n", CANGROW_CFG);
}
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

View file

@ -0,0 +1,138 @@
/*
*
* include/CanGrow_Webserver.h - webserver header file
*
*
* 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.
*
*/
/*
* 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/Common.h"
#include "Webserver/Page_root.h"
#include "Webserver/Page_wifi.h"
#include "Webserver/Page_system.h"
AsyncWebServer webserver(80);
// load requestLogger middleware
LoggingMiddleware requestLogger;
/*
* 404 error page begins
*/
// 404 page is a good page template btw
const char* Page_404_HTML PROGMEM = R"(%HEADER%
<div class='warnmsg'><h1>&#10071; &#65039; 404 - not found</h1></div>
%FOOTER%)";
/* processor */
String Proc_WebPage_404(const String& var) {
return AddHeaderFooter(var);
}
// https://github.com/mathieucarbou/ESPAsyncWebServer/blob/main/examples/SimpleServer/SimpleServer.ino
void WebserverNotFound(AsyncWebServerRequest* request) {
request->send(404, "text/html", Page_404_HTML, Proc_WebPage_404);
}
/*
* 404 error page ends
*/
/*
* setup all the webhandlers
*/
void Webserver_Init() {
Serial.println(":: [Webserver] initializing");
/* 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);
requestLogger.setOutput(Serial);
// this activates the middleware
if(config.system.httpLogSerial == true) {
Serial.println(":: [Webserver] serial logging: enabled");
webserver.addMiddleware(&requestLogger);
} else {
Serial.println(":: [Webserver] serial logging: disabled");
}
webserver.onNotFound(WebserverNotFound);
// 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
Serial.println(":: [Webserver] call [wifi:ScanNetworks] to workaround empty scan results bug");
WebPage_wifi_ScanNetworks();
webserver.begin();
Serial.println(":: [Webserver] ready to serve");
}

View file

@ -0,0 +1,81 @@
/*
*
* include/CanGrow_Wifi.h - Wifi stuff header file
*
*
* 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.
*
*/
void Wifi_Connect() {
Serial.printf(":: [WiFi] connecting to SSID: %s\n", config.wifi.ssid);
WiFi.begin(config.wifi.ssid, config.wifi.password);
if(config.wifi.dhcp == false) {
Serial.println(":: [WiFi] using static ip configuration:");
Serial.printf(":: [WiFi] IP : %s\n", IP2Char(config.wifi.ip));
Serial.printf(":: [WiFi] Netmask: %s\n", IP2Char(config.wifi.netmask));
Serial.printf(":: [WiFi] Gateway: %s\n", IP2Char(config.wifi.gateway));
Serial.printf(":: [WiFi] DNS : %s\n", IP2Char(config.wifi.dns));
WiFi.config(config.wifi.ip, config.wifi.dns, config.wifi.gateway, config.wifi.netmask);
} else {
Serial.println(":: [WiFi] using DHCP for ip configuration");
}
Serial.print(":: [WiFi] ");
// wait until WiFi connection is established
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("CONNECTED!");
if(config.wifi.dhcp == true) {
Serial.println(":: [WiFi] DHCP offered ip configuration:");
Serial.printf(":: [WiFi] IP : %s\n", IP2Char(WiFi.localIP()));
Serial.printf(":: [WiFi] Netmask: %s\n", IP2Char(WiFi.subnetMask()));
Serial.printf(":: [WiFi] Gateway: %s\n", IP2Char(WiFi.gatewayIP()));
Serial.printf(":: [WiFi] DNS : %s\n", IP2Char(WiFi.dnsIP()));
}
}
void Wifi_AP() {
Serial.printf(":: [WiFi] create access point: %s\n", CANGROW_SSID);
WiFi.softAPConfig(config.wifi.ip, config.wifi.gateway, config.wifi.netmask);
WiFi.softAP(CANGROW_SSID);
Serial.println(":: [WiFi] access point started:");
Serial.printf(":: [WiFi] IP : %s\n", IP2Char(config.wifi.ip));
Serial.printf(":: [WiFi] Netmask: %s\n", IP2Char(config.wifi.netmask));
}
void Wifi_Init() {
Serial.println(":: [WiFi] initializing");
if(strlen(config.wifi.ssid) == 0) {
Serial.println(":: [WiFi] config.wifi.ssid is unset");
Wifi_AP();
} else {
Wifi_Connect();
}
}

View file

@ -0,0 +1,169 @@
/*
*
* include/Webserver/Common.h - header file with common webserver functions
* HTML header or footer to a String()
*
*
* 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.
*
*/
#include "Common_HTML.h"
/*
* TestHeaderFooter - checks if the given var from the webserver processor
* is actual a template variable from header or footer.
*/
bool TestHeaderFooter(const String& var) {
#ifdef DEBUG
Serial.print(":: [Webserver:Page:root:proc:hf] var: ");
Serial.println(var);
#endif
if(
(var == "HEADER") ||
(var == "FOOTER") ||
(var == "CGVER") ||
(var == "CGBUILD") ||
(var == "GROWNAME") ||
(var == "CANGROW_CSS") ||
(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 = "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.growName);
} else if(var == "CANGROW_CSS") {
return String(File_cangrow_CSS);
} 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) {
String gpioIndex_html;
// iterate through through all available GPIOs in index
for(byte i = 0; i < GPIOindex_length; i++) {
bool gpioUsed = Check_GPIOindex_Used(i);
gpioIndex_html += "<option value='";
gpioIndex_html += i;
gpioIndex_html += "'";
// set disabled option for gpio which are already in use or incompatible
if( (((gpioUsed == true) && (i != selectId)) || (GPIOindex[i].note == INPUT_ONLY)) ) {
gpioIndex_html += " disabled";
}
if(i == selectId) {
gpioIndex_html += " selected";
}
gpioIndex_html += ">GPIO ";
gpioIndex_html += GPIOindex[i].gpio;
//add gpio note if there is some
//if(GPIOindex[i].note > 0) {
gpioIndex_html += " ";
gpioIndex_html += GPIO_Index_note_descr[GPIOindex[i].note];
// disable output incompatible gpio
if(GPIOindex[i].note == INPUT_ONLY) {
gpioIndex_html += " (N/A)";
// add USED if gpio is already in use
} else if((gpioUsed == true) && (i != selectId)) {
gpioIndex_html += " (used)";
}
gpioIndex_html += "</option>";
}
return gpioIndex_html;
}
String Html_SelectOpt_bool(byte selectVal = 255, String trueStr = "Yes", String falseStr = "No") {
String html;
html += "<option value='1'";
html += (selectVal == true) ? " selected" : "";
html += ">";
html += trueStr;
html += "</option>";
html += "<option value='0'";
html += (selectVal == false) ? " selected" : "";
html += ">";
html += falseStr;
html += "</option>";
return html;
}

View file

@ -0,0 +1,47 @@
/*
*
* include/Webserver/Common_HTML.h - header file with common HTML snippets
* HTML header or footer to a String()
*
*
* 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.
*
*/
// 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";

View file

@ -0,0 +1,247 @@
/*
*
* include/Webserver/File_cangrow_CSS.h - /cangrow.css header file
*
*
* 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.
*
*/
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;
}
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;
}
.helpbox {
font-size: 0.8em;
}
.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;
}
.visible {
display: inline;
/*justify-content: center!important;*/
}
@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) {
request->send_P(200, "text/css", File_cangrow_CSS);
}

View file

@ -0,0 +1,69 @@
/*
*
* include/Webserver/File_cangrow_JS.h - /cangrow.js header file
*
*
* 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.
*
*/
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 + '?');
})";
void WebFile_cangrow_JS(AsyncWebServerRequest *request) {
request->send_P(200, "text/javascript", File_cangrow_JS);
}

View file

@ -0,0 +1,37 @@
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, "image/x-icon", File_favicon_ico_gz, File_favicon_ico_gz_len);
response->addHeader("Content-Encoding", "gzip");
request->send(response);
}

View file

@ -0,0 +1,31 @@
/*
*
* include/Webserver/footer_HTML.h - footer page HTML header file
*
*
* 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.
*
*/
const char* Footer_HTML PROGMEM = R"(<div class='footer'><span>Build: %CGBUILD%</span></div>
</div></body></html>)";

View file

@ -0,0 +1,49 @@
/*
*
* include/Webserver/header_HTML.h - header page HTML header file
*
*
* 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.
*
*/
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'>04:20:23</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,30 @@
/*
*
* include/Webserver/Page_grow.h - grow page header file
*
*
* 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.
*
*/

View file

@ -0,0 +1,28 @@
/*
*
* include/Webserver/Page_grow_HTML.h - grow page HTML header file
*
*
* 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.
*
*/

View file

@ -0,0 +1,49 @@
/*
*
* include/Webserver/Page_root.h - root page header file
*
*
* 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.
*
*/
#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,33 @@
/*
*
* include/Webserver/Page_root_HTML.h - root page HTML header file
*
*
* 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.
*
*/
const char* Page_root_HTML PROGMEM = R"(%HEADER%
<h2>&#x1F331; Hello world!</h2>
%FOOTER%)";

View file

@ -0,0 +1,736 @@
/*
*
* include/Webserver/Page_system.h - system settings page header file
*
*
* 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.
*
*/
#include "Page_system_HTML.h"
/* global runtime variables */
/* VERY VERY DIRTY WORKAROUND
* I have the problem, that I cannot pass a parameter I receive from a http
* request to it's template processor. In my case i want to edit an output,
* the user should click an edit button on the system/output overview page.
* I am lazy so i want to reuse the output_add page, because it is quite
* kinda exactly the same. so i want to call GET /system/output/add?edit=ID
* I have searched and came to the conclusion, that at this point i see no
* other way then giving the parameter I need, the outputId, to an global
* variable, so the template processor can read it.
*/
byte tmpParam_editOutputId = 255;
/* subnav processor */
bool Test_WebPage_system_SUBNAV(const String& var) {
if(
(var == "SUBNAV") ||
(var == "ACTIVE_SUBNAV_OUTPUT") ||
(var == "ACTIVE_SUBNAV_UPDATE") ||
(var == "ACTIVE_SUBNAV_RESTART") ||
(var == "ACTIVE_SUBNAV_WIPE")) {
return true;
} else {
return false;
}
}
/*
* Proc_WebPage_system_SUBNAV - subnav processor for system
* this function works as same as AddHeaderFooter from Common.h
* byte activeSubnav:
* 1 - Output
* 2 - Update
* 3 - Restart
* 4 - Wipe
*/
String Proc_WebPage_system_SUBNAV(const String& var, byte activeSubnav = 0) {
String activeSubnav_ClassName = "activeNav";
if(var == "SUBNAV") {
return String(Page_system_HTML_SUBNAV);
} else if((var == "ACTIVE_SUBNAV_OUTPUT") && (activeSubnav == 1)) {
return activeSubnav_ClassName;
} else if((var == "ACTIVE_SUBNAV_UPDATE") && (activeSubnav == 2)) {
return activeSubnav_ClassName;
} else if((var == "ACTIVE_SUBNAV_RESTART") && (activeSubnav == 3)) {
return activeSubnav_ClassName;
} else if((var == "ACTIVE_SUBNAV_WIPE") && (activeSubnav == 4)) {
return activeSubnav_ClassName;
} else {
return String();
}
}
/*
* Main system page
*/
// https://techtutorialsx.com/2018/07/23/esp32-arduino-http-server-template-processing-with-multiple-placeholders/
String Proc_WebPage_system(const String& var) {
if(TestHeaderFooter(var)) {
return AddHeaderFooter(var, 2);
} else if(Test_WebPage_system_SUBNAV(var)) {
return Proc_WebPage_system_SUBNAV(var);
} else {
return String();
}
}
String Proc_WebPage_system_POST(const String& var) {
if(var == "SAVE_MSG") {
return String(Common_HTML_SAVE_MSG);
} else {
return Proc_WebPage_system(var);
}
}
String Proc_WebPage_system_POST_ERR(const String& var) {
if(var == "SAVE_MSG") {
return String(Common_HTML_SAVE_MSG_ERR);
} else {
return Proc_WebPage_system(var);
}
}
void WebPage_system(AsyncWebServerRequest *request) {
if(request->method() == HTTP_POST) {
request->send_P(200, "text/html", Page_system_HTML, Proc_WebPage_system_POST);
Serial.println(":: [Webserver:system:output] [POST] hello");
if(request->hasParam("config.system.ntpOffset", true)) {
const AsyncWebParameter* p_ntpOffset = request->getParam("config.system.ntpOffset", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_ntpOffset->name().c_str(), p_ntpOffset->value().c_str());
config.system.ntpOffset = p_ntpOffset->value().toInt();
}
if(request->hasParam("config.system.httpLogSerial", true)) {
const AsyncWebParameter* p_httpLogSerial = request->getParam("config.system.httpLogSerial", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_httpLogSerial->name().c_str(), p_httpLogSerial->value().c_str());
config.system.httpLogSerial = p_httpLogSerial->value().toInt();
}
if(SaveConfig()) {
// we need a restart to apply the new settings
needRestart = true;
Serial.println(":: [Webserver:system] config saved");
request->send_P(200, "text/html", Page_system_HTML, Proc_WebPage_system_POST);
} else {
Serial.println("!! [Webserver:system] ERROR saving config ");
request->send_P(200, "text/html", Page_system_HTML, Proc_WebPage_system_POST_ERR);
}
} else {
request->send_P(200, "text/html", Page_system_HTML, Proc_WebPage_system);
}
}
/*
* Subpage restart
*/
String Proc_WebPage_system_restart(const String& var) {
if(TestHeaderFooter(var)) {
return AddHeaderFooter(var, 2);
} else if(Test_WebPage_system_SUBNAV(var)) {
return Proc_WebPage_system_SUBNAV(var, 3);
} else if(var == "RESTART_MSG") {
return String(Page_system_restart_HTML_RESTART_MSG);
} else {
return String();
}
}
String Proc_WebPage_system_restart_POST(const String& var) {
if(var == "RESTART_MSG") {
return String(Page_system_restart_HTML_RESTART_MSG_POST);
} else {
return Proc_WebPage_system_restart(var);
}
}
void WebPage_system_restart(AsyncWebServerRequest *request) {
if(request->method() == HTTP_POST) {
if(request->hasParam("confirmed", true)) {
doRestart = false;
}
request->send_P(200, "text/html", Page_system_restart_HTML, Proc_WebPage_system_restart_POST);
if(request->hasParam("confirmed", true)) {
Serial.println(":: [Webserver:system:restart] POST[confirmed]: is set, triggering restart");
doRestart = true;
}
} else {
request->send_P(200, "text/html", Page_system_restart_HTML, Proc_WebPage_system_restart);
}
}
/*
* Subpage update
*/
// https://github.com/mathieucarbou/ESPAsyncWebServer/blob/main/docs/index.md#setting-up-the-server
void WebPage_system_update_ApplyUpdate(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){
if(!index){
Serial.printf(":: [Webserver:system:update:ApplyUpdate] Update Start: %s\n", filename.c_str());
// https://github.com/me-no-dev/ESPAsyncWebServer/issues/455#issuecomment-451728099
// workaround for bug with ESP32
#ifdef ESP8266
Update.runAsync(true);
#endif
if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)){
Update.printError(Serial);
}
}
if(!Update.hasError()){
if(Update.write(data, len) != len){
Update.printError(Serial);
}
}
if(final){
if(Update.end(true)){
Serial.printf(":: [Webserver:system:update:ApplyUpdate] Update Success: %uB\n", index+len);
} else {
Serial.println(":: [Webserver:system:update:ApplyUpdate] FAILED Update:");
Update.printError(Serial);
}
}
}
String Proc_WebPage_system_update(const String& var) {
if(TestHeaderFooter(var)) {
return AddHeaderFooter(var, 2);
} else if(Test_WebPage_system_SUBNAV(var)) {
return Proc_WebPage_system_SUBNAV(var, 2);
} else {
return String();
}
}
/* After an update.bin file was uploaded*/
String Proc_WebPage_system_update_POST(const String& var) {
if(var == "CONFIGWIFI_IP") {
if(config.wifi.dhcp == true) {
return WiFi.localIP().toString();
} else {
return String(IP2Char(config.wifi.ip));
}
} else {
return String();
}
}
void WebPage_system_update(AsyncWebServerRequest *request) {
if(request->method() == HTTP_POST) {
doRestart = !Update.hasError();
// when doRestart is true, deliver Page_system_update_HTML_POST
// otherwise Page_system_update_HTML_POST_FAILED
AsyncWebServerResponse *response = request->beginResponse(200, "text/html", doRestart?Page_system_update_HTML_POST:Page_system_update_HTML_POST_FAILED, Proc_WebPage_system_update_POST);
response->addHeader("Connection", "close");
request->send(response);
} else {
request->send_P(200, "text/html", Page_system_update_HTML, Proc_WebPage_system_update);
}
}
/*
* Subpage wipe
*/
String Proc_WebPage_system_wipe(const String& var) {
if(TestHeaderFooter(var)) {
return AddHeaderFooter(var, 2);
} else if(Test_WebPage_system_SUBNAV(var)) {
return Proc_WebPage_system_SUBNAV(var, 4);
} else if(var == "WIPE_MSG") {
return String(Page_system_wipe_HTML_WIPE_MSG);
} else {
return String();
}
}
String Proc_WebPage_system_wipe_POST(const String& var) {
if(var == "WIPE_MSG") {
return String(Page_system_wipe_HTML_WIPE_MSG_POST);
} else {
return Proc_WebPage_system_wipe(var);
}
}
void WebPage_system_wipe(AsyncWebServerRequest *request) {
if(request->method() == HTTP_POST) {
request->send_P(200, "text/html", Page_system_wipe_HTML, Proc_WebPage_system_wipe_POST);
if(request->hasParam("confirmed", true)) {
Serial.println(":: [Webserver:system:wipe] POST[confirmed]: is set, triggering wipe / factory reset");
LFS_Format();
Serial.println(":: [Webserver:system:wipe] triggering restart");
doRestart = true;
}
} else {
request->send_P(200, "text/html", Page_system_wipe_HTML, Proc_WebPage_system_wipe);
}
}
/*
* Subpage output
*/
String Proc_WebPage_system_output(const String& var) {
#ifndef DEBUG
Serial.print("DB [Webserver:system:output(Proc)] var: ");
Serial.println(var);
#endif
if(TestHeaderFooter(var)) {
return AddHeaderFooter(var, 2);
} else if(Test_WebPage_system_SUBNAV(var)) {
return Proc_WebPage_system_SUBNAV(var, 1);
} else if(var == "OUTPUT_TR_TD") {
// build table body
// i dont know a better way at the moment. if you do, please tell me!
String output_tr_td;
for(byte i=0; i < Max_Outputs; i++) {
if(config.system.output.type[i] > 0) {
#ifndef DEBUG
Serial.printf("DB [Webserver:system:output(Proc)] OutputID %d Type %d\n", i, config.system.output.type[i]);
#endif
output_tr_td += "<tr><td>";
output_tr_td += i;
output_tr_td += "</td><td>";
output_tr_td += config.system.output.name[i];
output_tr_td += "</td><td>";
output_tr_td += Output_Type_descr[config.system.output.type[i]];
output_tr_td += "</td><td>";
output_tr_td += Output_Device_descr[config.system.output.device[i]];
output_tr_td += "</td><td>";
output_tr_td += config.system.output.enabled[i];
output_tr_td += "</td><td>";
// edit button
output_tr_td += "<form class='linkForm' action='/system/output/add' method='get'>";
output_tr_td += "<input type='hidden' name='edit' value='";
output_tr_td += i;
output_tr_td += "'>";
output_tr_td += "<input type='submit' value='&#x270F;&#xFE0F;'></form> ";
// delete button
output_tr_td += "<form class='linkForm' action='/system/output/' method='post'>";
output_tr_td += "<input type='hidden' name='delete_output' value='";
output_tr_td += i;
output_tr_td += "'>";
output_tr_td += "<input type='submit' value='&#x274C;' onclick=\"return confirmDelete('";
output_tr_td += config.system.output.name[i];;
output_tr_td += "')\"></form>";
output_tr_td += "</td></tr>";
}
}
return output_tr_td;
} else{
return String();
}
}
String Proc_WebPage_system_output_POST(const String& var) {
if(var == "SAVE_MSG") {
return String(Common_HTML_SAVE_MSG);
} else {
return Proc_WebPage_system_output(var);
}
}
void WebPage_system_output(AsyncWebServerRequest *request) {
if(request->method() == HTTP_POST) {
if(request->hasParam("delete_output", true)) {
byte outputId;
const AsyncWebParameter* p_delete_output = request->getParam("delete_output", true);
Serial.printf(":: [Webserver:system:output] POST[%s]: %s\n", p_delete_output->name().c_str(), p_delete_output->value().c_str());
outputId = p_delete_output->value().toInt();
Serial.printf(":: [Webserver:system:output] Deleting output: %d\n", outputId);
// we ensure that every field is empty
config.system.output.type[outputId] = 0;
config.system.output.device[outputId] = 0;
// set every field of char array to 0x00 with memset
memset(config.system.output.name[outputId], '\0', sizeof config.system.output.name[outputId]);
config.system.output.enabled[outputId] = 0;
config.system.output.gpio[outputId] = 0;
config.system.output.gpio_pwm[outputId] = 0;
config.system.output.gpio_invert[outputId] = 0;
memset(config.system.output.i2c[outputId], '\0', sizeof config.system.output.i2c[outputId]);
memset(config.system.output.webcall_host[outputId], '\0', sizeof config.system.output.webcall_host[outputId]);
memset(config.system.output.webcall_path_on[outputId], '\0', sizeof config.system.output.webcall_path_on[outputId]);
memset(config.system.output.webcall_path_off[outputId], '\0', sizeof config.system.output.webcall_path_off[outputId]);
SaveConfig();
}
request->send_P(200, "text/html", Page_system_output_HTML, Proc_WebPage_system_output_POST);
Serial.println(":: [Webserver:system:output] [POST] hello");
} else {
if(request->hasParam("success")) {
// when GET param success is present, we use the _POST processor for the save message
request->send_P(200, "text/html", Page_system_output_HTML, Proc_WebPage_system_output_POST);
} else {
request->send_P(200, "text/html", Page_system_output_HTML, Proc_WebPage_system_output);
}
}
}
/*
* Subpage output add
*/
/* returns select <option> list of available output types */
String Html_SelOpt_type_WebPage_system_output_add(byte selectId = 255) {
String outputType_html;
// go through all available Output Devices, skip 0 because it means unconfigured
for(byte i = 1; i < Output_Type_total; i++) {
outputType_html += "<option value='";
outputType_html += i;
outputType_html += "'";
if(i == selectId) {
outputType_html += " selected";
}
outputType_html += ">";
outputType_html += Output_Type_descr[i];
outputType_html += "</option>";
}
return outputType_html;
}
String Html_SelOpt_device_WebPage_system_output_add(byte selectId = 255) {
String outputDevice_html;
// go through all available Output Devices, skip 0 because it means unconfigured
for(byte i = 1; i < Output_Device_total; i++) {
outputDevice_html += "<option value='";
outputDevice_html += i;
outputDevice_html += "'";
if(i == selectId) {
outputDevice_html += " selected";
}
outputDevice_html += ">";
outputDevice_html += Output_Device_descr[i];
outputDevice_html += "</option>";
}
return outputDevice_html;
}
String Proc_WebPage_system_output_addCommon(const String& var) {
#ifndef DEBUG
Serial.print("DB [Webserver:system:output:add(Proc)] var: ");
Serial.println(var);
#endif
if(TestHeaderFooter(var)) {
return AddHeaderFooter(var, 2);
} else if(Test_WebPage_system_SUBNAV(var)) {
return Proc_WebPage_system_SUBNAV(var, 1);
} else if(var == "OUTPUT_ID") {
// we check which id is free. A free ID as type == 0
return String(Give_Free_OutputId());
/* OUTPUT_TYPE */
} else if(var == "OUTPUT_TYPE") {
return Html_SelOpt_type_WebPage_system_output_add();
/* OUTPUT_DEVICE */
} else if(var == "OUTPUT_DEVICE") {
return Html_SelOpt_device_WebPage_system_output_add();
/* GPIO_INDEX */
} else if(var == "GPIO_INDEX") {
return Html_SelectOpt_GPIOindex();
} else {
return String();
}
}
String Proc_WebPage_system_output_add(const String& var) {
#ifndef DEBUG
Serial.print("DB [Webserver:system:output:add(Proc)] var: ");
Serial.println(var);
#endif
if(TestHeaderFooter(var)) {
return AddHeaderFooter(var, 2);
} else if(Test_WebPage_system_SUBNAV(var)) {
return Proc_WebPage_system_SUBNAV(var, 1);
} else if(var == "ACTION") {
return String("&#10133; Add");
} else if(var == "OUTPUT_ID") {
// we check which id is free. A free ID as type == 0
return String(Give_Free_OutputId());
} else if(var == "OUTPUT_TYPE") {
return Html_SelOpt_type_WebPage_system_output_add();
} else if(var == "OUTPUT_DEVICE") {
return Html_SelOpt_device_WebPage_system_output_add();
} else if(var == "OUTPUT_ENABLED") {
return Html_SelectOpt_bool();
} else if(var == "GPIO_INDEX") {
return Html_SelectOpt_GPIOindex();
} else if(var == "GPIO_PWM") {
return Html_SelectOpt_bool();
} else if(var == "GPIO_INVERT") {
return Html_SelectOpt_bool();
//} else if(
//(var == "CLASS_TYPE_1") ||
//(var == "CLASS_TYPE_2") ||
//(var == "CLASS_TYPE_3")) {
/* all type div container are hidden when adding */
//return String("hidden");
//}
} else {
return String();
}
}
String Proc_WebPage_system_output_addEdit(const String& var) {
#ifndef DEBUG
Serial.print("DB [Webserver:system:output:addEdit(Proc)] var: ");
Serial.println(var);
#endif
if(TestHeaderFooter(var)) {
return AddHeaderFooter(var, 2);
} else if(Test_WebPage_system_SUBNAV(var)) {
return Proc_WebPage_system_SUBNAV(var, 1);
} else if(var == "ACTION") {
return String("&#x270F;&#xFE0F; Edit");
} else if(var == "OUTPUT_ID") {
// return the outputId we got from GET .../add?edit=ID
// dirty workaround to put this in a global variable
return String(tmpParam_editOutputId);
} else if(var == "OUTPUT_TYPE") {
return Html_SelOpt_type_WebPage_system_output_add(config.system.output.type[tmpParam_editOutputId]);
} else if(var == "OUTPUT_DEVICE") {
return Html_SelOpt_device_WebPage_system_output_add(config.system.output.device[tmpParam_editOutputId]);
} else if(var == "OUTPUT_NAME") {
// "escape" % character, because it would break the template processor.
// tasmote webcall for example has percentage char in its path
String outputName = config.system.output.name[tmpParam_editOutputId];;
outputName.replace("%", "&#37;");
return outputName;
} else if(var == "OUTPUT_ENABLED") {
return Html_SelectOpt_bool(config.system.output.enabled[tmpParam_editOutputId]);
} else if(var == "GPIO_INDEX") {
return Html_SelectOpt_GPIOindex(config.system.output.gpio[tmpParam_editOutputId]);
} else if(var == "GPIO_PWM") {
return Html_SelectOpt_bool(config.system.output.gpio_pwm[tmpParam_editOutputId]);
} else if(var == "GPIO_INVERT") {
return Html_SelectOpt_bool(config.system.output.gpio_invert[tmpParam_editOutputId]);
} else if(var == "I2C") {
return String(config.system.output.i2c[tmpParam_editOutputId]);
} else if(var == "WEBCALL_HOST") {
return String(config.system.output.webcall_host[tmpParam_editOutputId]);
} else if(var == "WEBCALL_PATH_ON") {
String webcallPathOn = config.system.output.webcall_path_on[tmpParam_editOutputId];
webcallPathOn.replace("%", "&#37;");
return webcallPathOn;
} else if(var == "WEBCALL_PATH_OFF") {
String webcallPathOff = config.system.output.webcall_path_off[tmpParam_editOutputId];
webcallPathOff.replace("%", "&#37;");
return webcallPathOff;
} else if(
((var == "CLASS_TYPE_1") && (config.system.output.type[tmpParam_editOutputId] == 1)) ||
((var == "CLASS_TYPE_2") && (config.system.output.type[tmpParam_editOutputId] == 2)) ||
((var == "CLASS_TYPE_3") && (config.system.output.type[tmpParam_editOutputId] == 3))) {
// add class 'visible' which overwrites display with flex!important and justify center
return String("visible");
} else {
return String();
}
}
String Proc_WebPage_system_output_add_POST(const String& var) {
if(var == "SAVE_MSG") {
return String(Common_HTML_SAVE_MSG);
} else {
return Proc_WebPage_system_output_add(var);
}
}
void WebPage_system_output_add(AsyncWebServerRequest *request) {
if(request->method() == HTTP_POST) {
Serial.println(":: [Webserver:system:output:add] [POST] hello");
byte outputId;
byte outputType;
if(request->hasParam("outputId", true)) {
const AsyncWebParameter* p_outputId = request->getParam("outputId", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_outputId->name().c_str(), p_outputId->value().c_str());
outputId = p_outputId->value().toInt();
}
if(request->hasParam("type", true)) {
const AsyncWebParameter* p_type = request->getParam("type", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_type->name().c_str(), p_type->value().c_str());
// put info config struct
config.system.output.type[outputId] = p_type->value().toInt();
// remember the value in own var to work later with here
outputType = p_type->value().toInt();
}
if(request->hasParam("device", true)) {
const AsyncWebParameter* p_device = request->getParam("device", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_device->name().c_str(), p_device->value().c_str());
config.system.output.device[outputId] = p_device->value().toInt();
}
if(request->hasParam("name", true)) {
const AsyncWebParameter* p_name = request->getParam("name", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_name->name().c_str(), p_name->value().c_str());
strlcpy(config.system.output.name[outputId], p_name->value().c_str(), sizeof(config.system.output.name[outputId]));
}
if(request->hasParam("enabled", true)) {
const AsyncWebParameter* p_enabled = request->getParam("enabled", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_enabled->name().c_str(), p_enabled->value().c_str());
config.system.output.enabled[outputId] = p_enabled->value().toInt();
}
// only fill the type related config vars
switch(outputType) {
// GPIO
case 1:
if(request->hasParam("gpio", true)) {
const AsyncWebParameter* p_gpio = request->getParam("gpio", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_gpio->name().c_str(), p_gpio->value().c_str());
config.system.output.gpio[outputId] = p_gpio->value().toInt();
}
if(request->hasParam("gpio_pwm", true)) {
const AsyncWebParameter* p_gpio_pwm = request->getParam("gpio_pwm", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_gpio_pwm->name().c_str(), p_gpio_pwm->value().c_str());
config.system.output.gpio_pwm[outputId] = p_gpio_pwm->value().toInt();
}
if(request->hasParam("gpio_invert", true)) {
const AsyncWebParameter* p_gpio_invert = request->getParam("gpio_invert", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_gpio_invert->name().c_str(), p_gpio_invert->value().c_str());
config.system.output.gpio_invert[outputId] = p_gpio_invert->value().toInt();
}
break;
// I2C
case 2:
if(request->hasParam("i2c", true)) {
const AsyncWebParameter* p_i2c = request->getParam("i2c", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_i2c->name().c_str(), p_i2c->value().c_str());
strlcpy(config.system.output.i2c[outputId], p_i2c->value().c_str(), sizeof(config.system.output.i2c[outputId]));
}
break;
// Webcall
case 3:
if(request->hasParam("webcall_host", true)) {
const AsyncWebParameter* p_webcall_host = request->getParam("webcall_host", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_webcall_host->name().c_str(), p_webcall_host->value().c_str());
strlcpy(config.system.output.webcall_host[outputId], p_webcall_host->value().c_str(), sizeof(config.system.output.webcall_host[outputId]));
}
if(request->hasParam("webcall_path_on", true)) {
const AsyncWebParameter* p_webcall_path_on = request->getParam("webcall_path_on", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_webcall_path_on->name().c_str(), p_webcall_path_on->value().c_str());
strlcpy(config.system.output.webcall_path_on[outputId], p_webcall_path_on->value().c_str(), sizeof(config.system.output.webcall_path_on[outputId]));
}
if(request->hasParam("webcall_path_off", true)) {
const AsyncWebParameter* p_webcall_path_off = request->getParam("webcall_path_off", true);
Serial.printf(":: [Webserver:system] POST[%s]: %s\n", p_webcall_path_off->name().c_str(), p_webcall_path_off->value().c_str());
strlcpy(config.system.output.webcall_path_off[outputId], p_webcall_path_off->value().c_str(), sizeof(config.system.output.webcall_path_off[outputId]));
}
break;
default: break;
}
SaveConfig();
// request->send_P(200, "text/html", Page_system_output_add_HTML, Proc_WebPage_system_output_add_POST);
// I like it more when user gets redirected to the output overview after saving
request->redirect("/system/output/?success");
} else {
if(request->hasParam("edit")) {
const AsyncWebParameter* p_edit = request->getParam("edit");
Serial.println("DB [Webserver:system:output:add?edit] ");
Serial.printf(":: [Webserver:system] POST[%s]: %d\n", p_edit->name().c_str(), p_edit->value().toInt());
tmpParam_editOutputId = p_edit->value().toInt();
request->send_P(200, "text/html", Page_system_output_add_HTML, Proc_WebPage_system_output_addEdit);
} else {
request->send_P(200, "text/html", Page_system_output_add_HTML, Proc_WebPage_system_output_add);
}
}
}

View file

@ -0,0 +1,230 @@
/*
*
* include/Webserver/Page_system_HTML.h - system settings page HTML header file
*
*
* 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.
*
*/
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='%CONFIG_SYSTEM_NTPOFFSET%' required> Hours<br>
<u>Maintenance duration</u>:<br> <input class='inputShort' type='number' name='maintenanceDuration' min='0' max='900' value='%CONFIG_SYSTEM_MAINTDUR%' required> Seconds<br>
<u>ESP32-Cam IP (optional)</u>:<br>
<input type='text' name='esp32camIp' maxlength='16' value='%CONFIG_SYSTEM_ESP32CAMIP%' ><br>
<u>HTTP log to serial</u>:<br>
<select name='config.system.httpLogSerial' required>
<option disabled value='' selected hidden>---</option>
<option value='1'>On</option>
<option value='0'>Off</option>
</select><br>
<input type='submit' value='&#x1F4BE; Save settings'>
</form>
%FOOTER%)";
const char* Page_system_HTML_SUBNAV PROGMEM = R"(<ul class='subnav'>
<li><a class='' href='/system/sensor/'>&#x1F321;&#xFE0F; Sensor configuration</a></li>
<li><a class='%ACTIVE_SUBNAV_OUTPUT%' href='/system/output/'>&#9889; Output configuration</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; CanGrow restart</a></li>
<li><a class='%ACTIVE_SUBNAV_WIPE%' href='/system/wipe' >&#x1F4A3; Factory reset</a></li>
</ul>)";
/*
* 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='15;url=/' /></head>
<body><h1>Successfully updated!
<br>Restarting...</h1><br>
Redirecting to "/" in 15 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...)";
/*
* 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' href='/system/output/add'>&#10133; Add output</a>
<table class='centered'>
<tr>
<th>ID</th>
<th>Name</th>
<th>Type</th>
<th>Device</th>
<th>Enabled</th>
<th>Action</th>
</tr>
%OUTPUT_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'>
<input type='hidden' name='outputId' value='%OUTPUT_ID%' />
<u>Type</u>:<br>
<select id='type_sel' name='type' onchange="showSelect('type_sel', 'type_', 'hidden');" 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='16' value='%OUTPUT_NAME%' required><br>
<u>Enable</u>:<br>
<select name='enabled' required>
<option disabled value='' selected hidden>---</option>
%OUTPUT_ENABLED%
</select><br>
<div class='hidden %CLASS_TYPE_1%' id='type_1'>
<u>GPIO</u>:<br>
<select name='gpio'>
<option disabled value='' selected hidden>---</option>
%GPIO_INDEX%
</select><br>
<u>GPIO PWM</u>:<br>
<select name='gpio_pwm' >
<option disabled value='' selected hidden>---</option>
%GPIO_PWM%
</select><br>
<u>GPIO invert</u>:<br>
<select name='gpio_invert' >
<option disabled value='' selected hidden>---</option>
%GPIO_INVERT%
</select><br>
</div>
<div class='hidden %CLASS_TYPE_2%' id='type_2'>
<u>I2C</u>:<br>
<input type='text' name='i2c' maxlength='16' value='%I2C%' ><br>
</div>
<div class='hidden %CLASS_TYPE_3%' id='type_3'>
<u>Webcall host</u>:<br>
<input type='text' name='webcall_host' maxlength='32' value='%WEBCALL_HOST%' ><br>
<u>Webcall path 'on'</u>:<br>
<input type='text' name='webcall_path_on' maxlength='32' value='%WEBCALL_PATH_ON%' ><br>
<u>Webcall path 'off'</u>:<br>
<input type='text' name='webcall_path_off' maxlength='32' value='%WEBCALL_PATH_OFF%' ><br>
</div>
<br>
<input type='submit' value='&#x1F4BE; Save settings'>
</form>
%FOOTER%)";

View file

@ -0,0 +1,212 @@
/*
*
* include/Webserver/Page_wifi.h - wifi page header file
*
*
* 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.
*
*/
#include "Page_wifi_HTML.h"
String WebPage_wifi_ScanNetworks() {
String html;
Serial.println(":: [Webserver:wifi:ScanNetworks] scanning for available networks:");
// 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 += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + "</option>";
Serial.print(":: [Webserver:wifi:ScanNetworks] - ");
Serial.println(WiFi.SSID(i));
}
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) {
if(request->method() == HTTP_POST) {
if(request->hasParam("config.wifi.ssid", true)) {
const AsyncWebParameter* p_ssid = request->getParam("config.wifi.ssid", true);
Serial.printf(":: [Webserver:wifi] POST[%s]: %s\n", p_ssid->name().c_str(), p_ssid->value().c_str());
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);
Serial.printf(":: [Webserver:wifi] POST[%s]: %s\n", p_password->name().c_str(), p_password->value().c_str());
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);
Serial.printf(":: [Webserver:wifi] POST[config.wifi.ip0-3]: %s . %s . %s . %s\n", p_ip0->value().c_str(), p_ip1->value().c_str(), p_ip2->value().c_str(), p_ip3->value().c_str());
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);
Serial.printf(":: [Webserver:wifi] POST[config.wifi.netmask0-3]: %s . %s . %s . %s\n", p_netmask0->value().c_str(), p_netmask1->value().c_str(), p_netmask2->value().c_str(), p_netmask3->value().c_str());
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);
Serial.printf(":: [Webserver:wifi] POST[config.wifi.gateway0-3]: %s . %s . %s . %s\n", p_gateway0->value().c_str(), p_gateway1->value().c_str(), p_gateway2->value().c_str(), p_gateway3->value().c_str());
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);
Serial.printf(":: [Webserver:wifi] POST[config.wifi.dns0-3]: %s . %s . %s . %s\n", p_dns0->value().c_str(), p_dns1->value().c_str(), p_dns2->value().c_str(), p_dns3->value().c_str());
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);
Serial.printf(":: [Webserver:wifi] POST[%s]: %s\n", p_dhcp->name().c_str(), p_dhcp->value().c_str());
config.wifi.dhcp = p_dhcp->value().toInt();
}
if(SaveConfig()) {
// we need a restart to apply the new settings
needRestart = true;
Serial.println(":: [Webserver:wifi] config saved");
request->send_P(200, "text/html", Page_wifi_HTML, Proc_WebPage_wifi_POST);
} else {
Serial.println("!! [Webserver:wifi] ERROR saving config ");
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,93 @@
/*
*
* include/Webserver/Page_wifi_HTML.h - wifi page HTML header file
*
*
* 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.
*
*/
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>)";

@ -1 +0,0 @@
Subproject commit d4720987895bc49bdb38beb9d3e288de8bd59078

@ -1 +0,0 @@
Subproject commit 9412f3d1a2e334a3415d79df706dad02925488aa

@ -1 +0,0 @@
Subproject commit 5e34a20df62371150f7cb10330919e9393b884b1

@ -1 +0,0 @@
Subproject commit 78d6a130b2045a8b7b76616da81603796757ed47

@ -1 +0,0 @@
Subproject commit 7b2473b6b24ae340f41685b5f5b2b90ad896db04

File diff suppressed because it is too large Load diff

@ -1 +0,0 @@
Subproject commit 61684d4516839b579b81105be3447499c1417908

@ -1 +0,0 @@
Subproject commit a18e50dcea4ee17285d732d39e7bc559482d1d3d

View file

@ -9,82 +9,13 @@ An easy to use DIY grow controller firmware (for cannabis).
![Screenshot_WebUI_root.png](Arduino/CanGrow/Screenshot_WebUI_root.png)
![CanGrow_PCB_Front.png](KiCad/CanGrow/CanGrow_PCB_Front_small.png)
## WORK IN PROGRESS
# WORK IN PROGRESS
## 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.
### Update 09.12.2024 - Release of CanGrow firmware v0.1.0
After some feedback of users from the forum [grower.ch](https://grower.ch) (thank you guys!) I somewhat finalized firmware version v0.1 so this would be in a usable state for others to use.
Work on v0.2 is progressing slowly, so it is better when the code, which already exists, exists in usable way :)
**Release notes:**
First non-dev release of CanGrow Firmware v0.1 branch
I would call this the first "official" release, which is usable. It does not (yet) contain all functions and features I planned to implement, but those will come with v0.2 of the firmware, which is already in progress for about a month now.
CanGrow Firmware v0.1.0 is made for the CanGrow PCB, but you can of course run it with your own hardware. Firmware v0.2 will be fully platform independent.
# Setup
## Perform a factory reset, after you have flashed the firmware to your ESP8266 (for the first time or having problem like crash loops)!
You perform a factory reset (wipe) by connecting Pin D4 with GND (WIPE on the CanGrow PCB) when the builtin LED of the ESP8266 blinks three times during start. Hold for three seconds, then release. The builtin LED should now flash faster and the ESP should restart.
Now you will see an open unprotected Wifi called `CanGrow-unconfigured`, connect to it and open the address [http://192.168.4.20](http://192.168.4.20) and you can now start setup up your CanGrow device :)
# Features
- Gather Values from sensors
- Temperature
- Humidity
- Soilmoisture
- Time based (ntp) Light control
- Smooth Fade in and fade out configurable
- Control of two Fans
- FAN1 is controlled by PWM supply Voltage
- FAN2 is controlled by PWM 4 Pin Fan header
- Simple watering control
- 3 modes for water plan
- By soilmoisture value
- by time (every n days, different interval for vegetation and bloom phase)
- both combined
- Define the different phases of your grow
- Set amount of days for vegetation and bloom phase.
- After bloom phase CanGrow enters harvest mode by its own, which means the light and watering system gets disabled
- Maintenance Mode
- When using LED PWM, you can activate Maintenance Mode, which dimms the light. (at night it will turn on when enabling maintenance mode)
- ESP32Cam integration on the Dashboard
- Support for SSD1306 128x64 I2C OLED display
- rotates through three different screens
- Get measured values as json
- `GET /api/sensors`
# Supported Sensors
- Analog capacitive soilmoisture
- I2C Chirp (https://wemakethings.net/chirp/)
- Soilmoisture
- Temperature
- I2C BME280
- Temperature
- Humidity
- I2C SHT31
- Temperature
- Humidity
- Analog water level indicator
- Builtin on CanGrow PCB
- simple voltage divider, check out https://deltalima.org/blog/index.ph...e-step-water-level-indicator-for-arduino-esp/
# Todo
This version branch v0.1.x will receive only bugfixes and small changes. Work on the next v0.2 has already began, which is a complete rewrite. If you are curious , check out the branch "firmware_v0.2-dev", any feedback is welcome!
***
### Update 14.09.2024 - Code Rewrite v0.2
I took some "summer break" from the project, and had the opportunity to talk to different people about it.
@ -127,7 +58,7 @@ and let the user decide at which pin which output, sensor or whatever will be co
- MQTT support
- API
***
## Old v0.1 Features / ToDo List

View file

@ -1,100 +0,0 @@
#!/bin/bash
#
test -z $TTY && TTY="/dev/ttyUSB0"
test -z $IP && IP="192.168.4.20"
VER="$(grep "define CANGROW_VER" Arduino/CanGrow/CanGrow_Version.h | cut -d \" -f2 |sed -e 's/\"//g')"
BUILD="$(git rev-parse --short HEAD)-$(date '+%Y%m%d%H%M%S')"
ACLI="$HOME/.local/bin/arduino-cli"
ACLI_CMD="$ACLI --config-file arduino-cli.yml"
BOARD="esp8266:esp8266:d1_mini_clone"
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 build/"
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)"
declare -a LIBS=( "Adafruit SSD1306" "Adafruit BME280 Library" "ArduinoJson" "NTPClient" "Time" "Adafruit SHT31 Library" )
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
sudo apt install arduino python3 wget curl xxd
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 1.0.0 into ${ACLI_DIR}/"
wget -O - "https://github.com/arduino/arduino-cli/releases/download/v1.0.0/arduino-cli_1.0.0_Linux_64bit.tar.gz" | tar -C ${ACLI_DIR} -zxvf - arduino-cli
chmod +x ${ACLI}
echo ""
echo ":: Installing ESP8266 core for Arduino"
${ACLI_CMD} core install esp8266:esp8266
echo ":: Installing Arduino libraries"
for lib in ${!LIBS[@]}
do
echo " - ${LIBS[$lib]}"
done
for lib in ${!LIBS[@]}
do
${ACLI_CMD} lib install "${LIBS[$lib]}"
done
echo ""
echo ":: Setup build environment done! You can now build the firmware"
echo " with: $0 build"
;;
b|build)
check_acli
echo ":: Building firmware $VER $BUILD, target dir: $(pwd)/build/"
test -d build || mkdir build
${ACLI_CMD} --no-color compile -b ${BOARD} "Arduino/CanGrow/CanGrow.ino" --build-property "build.extra_flags=-DCANGROW_BUILD=\"${BUILD}\"" --output-dir build/ || exit 1
#${ACLI_CMD} --no-color compile -b ${BOARD} "Arduino/CanGrow/CanGrow.ino" --build-property "build.extra_flags=-DCANGROW_VER=\"${VER}\" -DCANGROW_BUILD=\"${BUILD}\"" --output-dir build/ || exit 1
cp build/CanGrow.ino.bin build/CanGrow_v${VER}_${BUILD}.bin
;;
u|upload)
check_acli
echo ":: Uploading to $TTY"
${ACLI_CMD} --no-color compile -v -b ${BOARD} -u -p $TTY "Arduino/CanGrow/CanGrow.ino"
;;
w|webupload)
echo ":: Uploading to $IP"
curl -v http://$IP/system/applyUpdate -X POST -H 'Content-Type: multipart/form-data' -F "firmware=@$(pwd)/build/CanGrow.ino.bin"
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,203 @@
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;
}
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;
}
.helpbox {
font-size: 0.8em;
}
.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;
}
.hidden {
display: none;
}
.visible {
display: flex;
justify-content: center;
}
@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) {
}*/

View file

@ -0,0 +1,35 @@
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 = '';
}
}
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 + '?');
}

Binary file not shown.

View file

@ -1,147 +0,0 @@
.gauge {
position: relative;
}
.gaugeWrapper {
overflow: hidden;
display: flex;
justify-content: center;
}
.gauge__container {
margin: 0;
padding: 0;
position: absolute;
left: 50%;
overflow: hidden;
text-align: center;
-webkit-transform: translateX(-50%);
-moz-transform: translateX(-50%);
-ms-transform: translateX(-50%);
-o-transform: translateX(-50%);
transform: translateX(-50%);
}
.gauge__background {
z-index: 0;
position: absolute;
background-color: #cae0d0;
top: 0;
border-radius: 300px 300px 0 0;
}
.gauge__data {
z-index: 1;
position: absolute;
background-color: #04AA6D;
margin-left: auto;
margin-right: auto;
border-radius: 300px 300px 0 0;
-webkit-transform-origin: center bottom;
-moz-transform-origin: center bottom;
-ms-transform-origin: center bottom;
-o-transform-origin: center bottom;
transform-origin: center bottom;
}
.gauge__center {
z-index: 2;
position: absolute;
background-color: #1d211e;
margin-right: auto;
border-radius: 300px 300px 0 0;
}
.gauge__marker {
z-index: 3;
background-color: #fff;
position: absolute;
width: 1px;
}
.gauge__needle {
z-index: 4;
background-color: #E91E63;
height: 3px;
position: absolute;
-webkit-transform-origin: left center;
-moz-transform-origin: left center;
-ms-transform-origin: left center;
-o-transform-origin: left center;
transform-origin: left center;
}
.gauge__labels {
display: table;
margin: 0 auto;
position: relative;
font-weight: bold;
}
.gauge__label--low {
display: table-cell;
text-align: center;
}
.gauge__label--spacer {
display: table-cell;
}
.gauge__label--high {
display: table-cell;
text-align: center;
}
.gauge { height: calc(60px + 3em); }
.gauge__container { width: 120px; height: 60px; }
.gauge__marker { height: 60px; left: 59.5px; }
.gauge__background { width: 120px; height: 60px; }
.gauge__center { width: 72px; height: 36px; top: 24px; margin-left: 24px; }
.gauge__data { width: 120px; height: 60px; }
.gauge__needle { left: 60px; top: 58px; width: 60px; }
.gauge__labels { top: 60px; width: 120px; }
.gauge__label--low { width: 24px; }
.gauge__label--spacer { width: 72px; text-align: center;}
.gauge__label--high { width: 24px; }
@media only screen and (min-width: 720px) {
.gauge { height: calc(120px + 4.2em); }
.gauge__container { width: 240px; height: 120px; }
.gauge__marker { height: 120px; left: 119.5px; }
.gauge__background { width: 240px; height: 120px; }
.gauge__center { width: 144px; height: 72px; top: 48px; margin-left: 48px; }
.gauge__data { width: 240px; height: 120px; }
.gauge__needle { left: 120px; top: 117px; width: 120px; }
.gauge__labels { top: 120px; width: 240px; }
.gauge__label--low { width: 48px; }
.gauge__label--spacer { width: 144px; text-align: center;}
.gauge__label--high { width: 48px; }
.gaugeLabel { font-size: 1.3em; }
.gauge__labels { font-size: 2em; }
}
.gauge--liveupdate .gauge__data,
.gauge--liveupdate .gauge__needle {
-webkit-transition: all 1s ease-in-out;
-moz-transition: all 1s ease-in-out;
-ms-transition: all 1s ease-in-out;
-o-transition: all 1s ease-in-out;
transition: all 1s ease-in-out;
}
.gauge__data {
-webkit-transform: rotate(-.50turn);
-moz-transform: rotate(-.50turn);
-ms-transform: rotate(-.50turn);
-o-transform: rotate(-.50turn);
transform: rotate(-.50turn);
}
.gauge__needle {
-webkit-transform: rotate(-.50turn);
-moz-transform: rotate(-.50turn);
-ms-transform: rotate(-.50turn);
-o-transform: rotate(-.50turn);
transform: rotate(-.50turn);
}

View file

@ -1,61 +0,0 @@
function Gauge(el) {
var element, // Containing element for the info component
data, // `.gauge__data` element
needle, // `.gauge__needle` element
value = 0.0, // Current gauge value from 0 to 1
prop, // Style for transform
valueLabel; // `.gauge__label--spacer` element
var setElement = function(el) {
// Keep a reference to the various elements and sub-elements
element = el;
data = element.querySelector('.gauge__data');
//needle = element.querySelector('.gauge__needle');
valueLabel = element.querySelector('.gauge__label--spacer');
};
var setValue = function(x, max, unit) {
percentage = x * 100 / max;
value = percentage / 100;
var turns = -0.5 + (value * 0.5);
data.style[prop] = 'rotate(' + turns + 'turn)';
//needle.style[prop] = 'rotate(' + turns + 'turn)';
valueLabel.textContent = x + unit;
};
function exports() { };
exports.element = function(el) {
if (!arguments.length) { return element; }
setElement(el);
return this;
};
exports.value = function(x, max=100, unit='%') {
if (!arguments.length) { return value; }
setValue(x, max, unit);
return this;
};
var body = document.getElementsByTagName('body')[0];
['webkitTransform', 'mozTransform', 'msTransform', 'oTransform', 'transform'].
forEach(function(p) {
if (typeof body.style[p] !== 'undefined') { prop = p; }
}
);
if (arguments.length) {
setElement(el);
}
return exports;
};

View file

@ -1,161 +1,149 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>CanGrow - Amnesia Haze</title>
<link rel='icon' href='data:;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsTAAALEwEAmpwYAAABcElEQVQ4y42TzU/bQBDFf7Nx1qGuAYVgQSuo2khBggPhyIH//9AiJAQ9tEeLqCKiUD6sxF52OMSEBCdW57aa9968fTsr3V5XWVLPO6sANNL7ZRAMNeU6Ea4T1UEI6pr55kcAwhpMrYOpk2/r/yEQmKWkIonf+TZVgex4Fw0bIEtIAALF3gbZ8U5VwKa3PJ18JT9IpiLvyflBwuhLG5veVUM0/0aoCONPa2hQjWZ8uEVeupJnXSBwO8YOH8iTeAKc2Q4Xt2C1VZL93F7MjbK/bxDnp5Zn7b+So+9pdQ+K/Q5qJlrRj5Ts6DM+rK7Ih7Mr3HaM7jYQVZqXQ6Tb6yqBYdTfomhHiFfUyMI3f+01/z7RHNzTGDyWGThP63SA2d8EEfIkrgQpzmOvH0AV+3M4zegNpUwagAYG8Yp4BS0nl4Kz5Mpf0JXJMby6w/66Aa+M+9uE53/Iexsggq4ESOYWC0jmsBfX8xdXhcJjL4cLc3kBl8uJGQ/CrpAAAAAASUVORK5CYII='>
<title>CanGrow - CanGrow v0.2-dev</title>
<link rel='stylesheet' href='cangrow.css'>
<script type='text/javascript' src='cangrow.js'></script>
<style>
body {
color: #cae0d0;
background-color: #1d211e;
font-family: helvetica;
}
.center {
width: 100%;
margin: auto;
}
.centered {
display: block;
margin-left: auto;
margin-right: auto;
}
h1, h2, h3, h4, h5 {
text-align: center;
}
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;
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;
}
.nav {
background: #333;
width: 100%;
margin: auto;
margin-bottom: 10px;
padding: 0;
position: relative;
border-radius: 3px;
}
.nav li {
display: inline-block;
list-style: none;
}
.nav li:first-of-type {
background: #026b45;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
.nav li a , .nav span, .button, .button:link, input[type=button], input[type=submit], input[type=reset] {
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);
}
.nav li a:hover , .activeMenu, .button:link:hover, .button:visited:hover, input[type=button]:hover, input[type=submit]:hover, input[type=reset]:hover {
background: #04AA6D;
color: #fff;
border-radius: 3px;
}
.nav li a:active {
background: #026b45;
color: #cae0d0;
}
.activeMenu {
background: #444;
}
.MenuTime {
background: #292929;
}
.button, .button:link, .button:visited, input[type=button], input[type=submit], input[type=reset] {
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 {
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;
}
@media only screen and (min-width: 1280px) {
.center, .nav {
width: 60%; min-width: 420px;
.linkForm {
display: inline-block;
}
.linkForm input[type=submit] {
background: #262B27;
padding: 5px;
}
}
</style>
</head>
<body>
<ul class='nav'><li><a href='/'>&#x1F331; Amnesia Haze</a></li>
<li><a href='/growSettings' >&#128262; Grow settings</a></li>
<li><a href='/systemSettings' >&#9881; System settings</a></li>
<li><a href='/wifiSettings' >&#128225; WiFi settings</a></li>
<li><a href='/help' >&#x2753; Help</a></li>
<li><span class='MenuTime'>09:48:27</span></li>
<li><a href='https://git.la10cy.net/DeltaLima/CanGrow' target='_blank'>CanGrow v0.1</a></li>
</ul><div class='center'><h2>&#x1F504; Firmware update</h2>Version: 0.1<br>Build: 4ad16c9
<p>You find the latest CanGrow Firmware Version on the projects <a href='https://git.la10cy.net/DeltaLima/CanGrow/releases' target='_blank'>release page</a></p>
<form method='GET' action='' enctype='multipart/form-data'>
Firmware .bin file:<br>
<input type='file' accept='.bin,.bin.gz' name='firmware'>
</form>
<button onclick="document.getElementById('divUploading').style.display = ''; window.alert('click');">asd</button>
<div id='divUploading' style='display: none;' class='warnmsg'>Uploading, please wait...<div>
</div>
</body>
</html>
<ul class='nav'><li><a href='/'>&#x1F331; CanGrow</a></li>
<li><a class='' href='/grow/' >&#128262; Grow settings</a></li>
<li><a class='activeNav' href='/system/' >&#9881; System settings</a></li>
<li><a class='' href='/wifi/' >&#128225; WiFi settings</a></li>
<li><a class='' href='/help' >&#x2753; Help</a></li>
<li><span class='navTime'>04:20:23</span></li>
<li><a href='https://git.la10cy.net/DeltaLima/CanGrow' target='_blank'>CanGrow v0.2-dev</a></li>
</ul>
<div class='center'>
<ul class='subnav'>
<li><a class='activeNav' href='/system/output/'>&#9889; Output configuration</a></li>
<li><a class='' href='/system/update'>&#x1F504; Firmware update</a></li>
<li><a class='' href='/system/restart' >&#x1F501; CanGrow restart</a></li>
<li><a class='' href='/system/wipe' >&#x1F4A3; Factory reset</a></li>
</ul>
<a class='button' href='/system/output/add'>&#10133; Add output</a>
<table class='centered hidden visible'>
<tr>
<th>ID</th>
<th>Name</th>
<th>Type</th>
<th>Device</th>
<th>Enabled</th>
<th>Action</th>
</tr>
<tr><td>0</td>
<td>Tasmota LED1</td>
<td>Webcall</td>
<td>Light</td>
<td>1</td>
<td>
<form class='linkForm' action='add?edit=0'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x270F;&#xFE0F;'>
</form>
<form class='linkForm' method='post' action='index.html'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x274C;' onclick="return confirmDelete('Tasmota LED1')">
</form>
</td></tr><tr><td>1</td>
<td>LED2 (red)</td>
<td>GPIO</td>
<td>Light</td>
<td>1</td>
<td>
<form class='linkForm' action='add?edit=0'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x270F;&#xFE0F;'>
</form>
<form class='linkForm' method='post' action='index.html'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x274C;' onclick="return confirmDelete('Tasmota LED1')">
</form>
</td></tr><tr><td>2</td>
<td>LED1 Dimmer</td>
<td>I2C</td>
<td>Light</td>
<td>1</td>
<td>
<form class='linkForm' action='add?edit=0'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x270F;&#xFE0F;'>
</form>
<form class='linkForm' method='post' action='index.html'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x274C;' onclick="return confirmDelete('Tasmota LED1')">
</form>
</td></tr><tr><td>3</td>
<td>Fan exhaust</td>
<td>GPIO</td>
<td>Fan</td>
<td>1</td>
<td>
<form class='linkForm' action='add?edit=0'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x270F;&#xFE0F;'>
</form>
<form class='linkForm' method='post' action='index.html'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x274C;' onclick="return confirmDelete('Tasmota LED1')">
</form>
</td></tr><tr><td>4</td>
<td>Fan inside</td>
<td>GPIO</td>
<td>Fan</td>
<td>1</td>
<td>
<form class='linkForm' action='add?edit=0'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x270F;&#xFE0F;'>
</form>
<form class='linkForm' method='post' action='index.html'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x274C;' onclick="return confirmDelete('Tasmota LED1')">
</form>
</td></tr><tr><td>5</td>
<td>Tasmota Dehum</td>
<td>Webcall</td>
<td>Dehumidifier</td>
<td>1</td>
<td>
<form class='linkForm' action='add?edit=0'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x270F;&#xFE0F;'>
</form>
<form class='linkForm' method='post' action='index.html'>
<input type='hidden' name='del' value='0'>
<input type='submit' value='&#x274C;' onclick="return confirmDelete('Tasmota LED1')">
</form>
</td></tr><tr><td>6</td>
<td>Test</td>
<td>GPIO</td>
<td>Humidifier</td>
<td>0</td>
<td>
<form class='linkForm' action='add?edit=0'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x270F;&#xFE0F;'>
</form>
<form class='linkForm' method='post' action='index.html'>
<input type='hidden' name='del' value='0'>
<input class='linkForm' type='submit' value='&#x274C;' onclick="return confirmDelete('Tasmota LED1')">
</form>
</td></tr>
</table>
<div class='footer'><span>Build: 40d0175-esp8266-20241026222209</span></div>
</div></body></html>

View file

@ -4,27 +4,21 @@
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>CanGrow - Amnesia Haze</title>
<title>CanGrow - Ruderalis Indica</title>
<link rel='icon' href='data:;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsTAAALEwEAmpwYAAABcElEQVQ4y42TzU/bQBDFf7Nx1qGuAYVgQSuo2khBggPhyIH//9AiJAQ9tEeLqCKiUD6sxF52OMSEBCdW57aa9968fTsr3V5XWVLPO6sANNL7ZRAMNeU6Ea4T1UEI6pr55kcAwhpMrYOpk2/r/yEQmKWkIonf+TZVgex4Fw0bIEtIAALF3gbZ8U5VwKa3PJ18JT9IpiLvyflBwuhLG5veVUM0/0aoCONPa2hQjWZ8uEVeupJnXSBwO8YOH8iTeAKc2Q4Xt2C1VZL93F7MjbK/bxDnp5Zn7b+So+9pdQ+K/Q5qJlrRj5Ts6DM+rK7Ih7Mr3HaM7jYQVZqXQ6Tb6yqBYdTfomhHiFfUyMI3f+01/z7RHNzTGDyWGThP63SA2d8EEfIkrgQpzmOvH0AV+3M4zegNpUwagAYG8Yp4BS0nl4Kz5Mpf0JXJMby6w/66Aa+M+9uE53/Iexsggq4ESOYWC0jmsBfX8xdXhcJjL4cLc3kBl8uJGQ/CrpAAAAAASUVORK5CYII='>
<style>
body {
color: #cae0d0;
background-color: #1d211e;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-family: helvetica;
}
.center {
width: 100%;
margin: auto;
width: 100%;
margin: auto;
}
.centered {
display: block;
margin-left: auto;
margin-right: auto;
}
h1, h2, h3, h4, h5 {
text-align: center;
}
@ -59,12 +53,6 @@ a:active {
width: 42px;
}
.helpbox {
font-size: 0.8em;
margin-left: 15px;
margin-top: 5px;
margin-bottom: 5px;
}
.nav {
background: #333;
width: 100%;
@ -75,27 +63,9 @@ a:active {
border-radius: 3px;
}
.subnav {
text-align: center;
display: table;
margin: auto;
margin-bottom: 10px;
padding: 0;
position: relative;
border-radius: 3px;
}
.nav li {
display: inline-block;
list-style: none;
border-radius: 3px;
}
.subnav li {
background: #026b45;
list-style: none;
border-radius: 3px;
margin-bottom: 3px;
}
.nav li:first-of-type {
@ -103,7 +73,7 @@ a:active {
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] {
.nav li a , .nav span, .button, .button:link, input[type=button], input[type=submit], input[type=reset] {
color: #ddd;
display: block;
font-family: 'Lucida Sans Unicode', 'Lucida Grande', sans-serif;
@ -113,13 +83,13 @@ a:active {
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.5);
}
.nav li a:hover, .subnav li a:hover, .activeMenu, .button:link:hover, .button:visited:hover, input[type=button]:hover, input[type=submit]:hover, input[type=reset]:hover {
.nav li a:hover , .activeMenu, .button:link:hover, .button:visited:hover, input[type=button]:hover, input[type=submit]:hover, input[type=reset]:hover {
background: #04AA6D;
color: #fff;
border-radius: 3px;
}
.nav li a:active, .subnav li a:active {
.nav li a:active {
background: #026b45;
color: #cae0d0;
}
@ -155,151 +125,73 @@ input[type=text], input[type=date], input[type=number], input[type=password], se
border-radius: 3px;
}
@media only screen and (min-width: 1820px) {
@media only screen and (min-width: 1280px) {
.center, .nav {
width: 60%; min-width: 420px;
}
.subnav li {
display: '';
margin-bottom: 3px;
}
}
@media only screen and (min-width: 640px) {
.subnav li {
display: inline-block;
margin-bottom: 3px;
}
}
/* VPD colors */
.vpd_danger1 {
color: #1a6c9c;
}
.vpd_earlyveg {
color: #22ab9c;
}
.vpd_lateveg {
color: #9cc55b;
}
.vpd_latebloom {
color: #9cc55b;
}
.vpd_danger2 {
color: #1a6c9c;
}
</style>
</head>
<body>
<ul class='nav'><li><a href='/'>&#x1F331; Amnesiaaaa Haze</a></li>
<ul class='nav'><li><a href='/'>&#x1F331; Ruderalis Indica</a></li>
<li><a href='/growSettings' >&#128262; Grow settings</a></li>
<li><a href='/systemSettings' class='activeMenu'>&#9881; System settings</a></li>
<li><a href='/wifiSettings' >&#128225; WiFi settings</a></li>
<li><a href='/help' >&#x2753; Help</a></li>
<li><span class='MenuTime'>00:23:10</span></li>
<li><a href='https://git.la10cy.net/DeltaLima/CanGrow' target='_blank'>CanGrow v0.1.3-dev</a></li>
</ul><div class='center'><h2>&#9881; System settings</h2>
<ul class='subnav'>
<li><a href='/system/update'>&#x1F504; Firmware update</a></li>
<li><a href='/system/restart' >&#x1F501; CanGrow restart</a></li>
<li><a href='/system/wipe' >&#x1F4A3; Factory reset</a></li>
</ul>
<p>here you can set which features and sensors you use<br></p><form method='post' action='/systemSettings/save'>
<b>Output configuration</b><br>Invert Outputs: <select id='OutputInvert' name='OutputInvert' required>
<option value='0'>No</option>
<option value='1' selected >Yes</option>
</select><br>
<p class='helpbox'>When using CanGrow PCB v0.6, set to <b>Yes</b></p>
Use relais for LED (disable PWM): <select id='UseLEDrelais' name='UseLEDrelais' required>
<option value='0' selected >No</option>
<option value='1'>Yes</option>
</select><br>
Use relais for FAN1 (disable PWM): <select id='UseFANrelais' name='UseFANrelais' required>
<option value='0'>No</option>
<option value='1' selected >Yes</option>
</select><br><br>
<b>Sensor configuration</b><br>Soilmoisture sensor: <select id='SelMoistureSensor_Type' name='MoistureSensor_Type' onchange='MoistureSensorType();' required>
<option value='1' selected >Analog capacitive</option>
<option value='2'>I2C Chirp (0x20)</option>
</select><br>
Soilmoisture dry: <input type='number' id='iSoilmoistureDry' name='SoilmoistureDry' min='0' value='360' required><br>
Soilmoisture wet: <input type='number' id='iSoilmoistureWet' name='SoilmoistureWet' min='0' value='160' required><br>
Soilmoisture raw reading: <i><span id='iSoilmoistureRaw'>123</span></i> <input type='button' class='button' value='&#x1F503; Refresh' style='padding: 3px;' onclick='SoilmoistureRefresh();'>
<p class='helpbox'>
<b>Calibration</b><br>
Put your soilmoisture sensor into dry soil and hit Refresh.<br>
Adjust the value of 'Soilmoisture dry' if needed according to the reading.<br>
Repeat this with wet soil for 'Soilmoisture wet'.
</p>
<li><span class='MenuTime'>00:03:39</span></li>
<li><a href='https://git.la10cy.net/DeltaLima/CanGrow' target='_blank'>CanGrow v0.1</a></li>
</ul><div class='center'><h2>&#9881; System settings</h2><p>here you can set which features and sensors you use<br></p><form method='post' action='/systemSettings/save'>
<table>
<tr>
<td>Use FAN: </td>
<td>
<select id='UseFan' name='UseFan' required>
<option value='1' selected >Yes</option>
<option value='0'>No</option>
</select>
</td>
</tr>
<tr>
<td>Use PUMP: </td>
<td><select id='UsePump' name='UsePump' required>
<option value='1' selected >Yes</option>
<option value='0'>No</option>
</select>
</td>
</tr>
<tr>
<td>Use relais for LED:
</td>
<td>
<select id='UseLEDrelais' name='UseLEDrelais' required>
<option value='1'>Yes</option>
<option value='0' selected >No</option>
</select>
</td>
</tr>
<tr>
<td></td>Use relais for FAN: </td>
<select id='UseFANrelais' name='UseFANrelais' required>
<option value='1'>Yes</option>
<option value='0' selected >No</option>
</select>
</tr>
PUMP ON time: <input class='inputShort' type='number' name='PumpOnTime' min='0' max='255' value='3' required> Seconds<br>
Soilmoisture sensor: <select id='MoistureSensor_Type' name='MoistureSensor_Type' required>
<option value='1' selected >Analog capacitive</option>
<option value='2'>I2C chirp</option>
</select><br>
Soilmoisture low: <input class='inputShort' type='number' name='SoilmoistureLow' min='0' value='20' required> %<br>
Temperature sensor: <select id='TemperatureSensor_Type' name='TemperatureSensor_Type' required>
<option value='1' selected >DHT11/22</option>
<option value='2'>I2C chirp</option>
</select><br>
NTP offset: <input class='inputShort' type='number' name='NtpOffset' min='-12' max='14' value='2' required> Hours<br>
Maintenance Duration: <input class='inputShort' type='number' name='MaintenanceDuration' min='0' max='900' value='300' required> Seconds<br>
<input type='submit' value='Save'>
<script>
function loadJSON(callback) {
var xobj = new XMLHttpRequest();
xobj.overrideMimeType("application/json");
xobj.open('GET', '/api/sensors', true);
xobj.onreadystatechange = function() {
if (xobj.readyState == 4 && xobj.status == "200") {
callback(xobj.responseText);
}
}
xobj.send(null);
}
function SoilmoistureRefresh() {
loadJSON(function(response) {
json = JSON.parse(response);
document.getElementById('iSoilmoistureRaw').textContent = json.soilmoistureRaw;
console.log(json.soilmoistureRaw);
});
}
function MoistureSensorType() {
let selVal = document.getElementById('SelMoistureSensor_Type').value;
let wet = document.getElementById('iSoilmoistureWet');
let dry = document.getElementById('iSoilmoistureDry');
switch(selVal) {
case '1':
wet.value = 160;
dry.value = 360;
console.log(selVal);
break;
case '2':
wet.value = 485;
dry.value = 250;
console.log(selVal);
break;
default:
wet.value = 0;
dry.value = 0;
console.log(selVal);
break;
}
}
</script>
Temperature sensor: <select id='TemperatureSensor_Type' name='TemperatureSensor_Type' required>
<option value='1' selected >I2C BME280 (0x76)</option>
<option value='2'>I2C BME280 (0x77)</option>
<option value='3'>I2C SHT31 (0x44)</option>
<option value='4'>I2C SHT31 (0x45)</option>
<option value='5'>I2C Chirp (0x20)</option>
</select><br>
Humidity sensor: <select id='HumiditySensor_Type' name='HumiditySensor_Type' required>
<option value='1' selected >I2C BME280 (0x76)</option>
<option value='2'>I2C BME280 (0x77)</option>
<option value='3'>I2C SHT31 (0x44)</option>
<option value='4'>I2C SHT31 (0x45)</option>
</select><br><br>
<b>General configuration</b><br>NTP offset/UTC timezone: <input class='inputShort' type='number' name='NtpOffset' min='-12' max='14' value='1' required> Hours<br>
Maintenance Duration: <input class='inputShort' type='number' name='MaintenanceDuration' min='0' max='900' value='300' required> Seconds<br>
PWM Frequency: <input type='number' name='PWMFrequency' min='0' max='20000' value='13370' required> Hz<br>
Display rotation interval: <input class='inputShort' type='number' name='DisplayScreenDuration' min='0' max='255' value='3' required> Seconds<br>
<p class='helpbox'><b>0</b> will always show sensor value screen</p>ESP32-Cam IP (optional): <input type='text' name='Esp32CamIP' maxlength='16' value='' ><br><br>
<input type='submit' value='&#x1F4BE; Save settings'>
</table>
</form>
</div>