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
43 changed files with 4108 additions and 3764 deletions

2
.gitignore vendored
View file

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

View file

@ -28,30 +28,34 @@ long_line_behaviour=1
long_line_column=72 long_line_column=72
[files] [files]
current_page=1 current_page=0
FILE_NAME_0=0;Arduino;0;EUTF-8;0;1;0;.%2FCanGrow.ino;0;2 FILE_NAME_0=493;Sh;0;EUTF-8;0;1;0;.%2Fcangrow.sh;0;2
FILE_NAME_1=0;C++;0;EUTF-8;0;1;0;.%2FCanGrow_HTML.h;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;.%2FCanGrow_Init.h;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;.%2FCanGrow_Logo.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;.%2FCanGrow_PinAssignments.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;.%2FCanGrow_Sensors.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;.%2FCanGrow_SysFunctions.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;.%2FCanGrow_WebFunctions.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] [build-menu]
C++FT_00_LB=_Compile 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= C++FT_00_WD=
filetypes=C++;Arduino; filetypes=C++;Arduino;Sh;
ArduinoFT_00_LB=_Build 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_00_WD=
ArduinoFT_01_LB=Build & Upload 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= ArduinoFT_01_WD=
C++FT_01_LB=_Build & Upload 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= C++FT_01_WD=
ShFT_00_LB=Build
[VTE] ShFT_00_CM=./cangrow.sh build
last_dir=~ ShFT_00_WD=
ShFT_01_LB=Build & Upload
ShFT_01_CM=./cangrow.sh upload
ShFT_01_WD=

View file

@ -1,419 +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 #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 // https://github.com/arduino/ArduinoCore-avr/tree/master/libraries/SPI
#include <SPI.h> #include <SPI.h>
// https://github.com/arduino/ArduinoCore-avr/tree/master/libraries/Wire // https://github.com/arduino/ArduinoCore-avr/tree/master/libraries/Wire
#include <Wire.h> #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>
// https://github.com/adafruit/Adafruit-GFX-Library
#include <Adafruit_GFX.h>
// https://github.com/adafruit/Adafruit_SSD1306
#include <Adafruit_SSD1306.h>
// https://github.com/adafruit/Adafruit_BME280_Library/
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
// https://github.com/bblanchon/ArduinoJson // https://github.com/bblanchon/ArduinoJson
#include <ArduinoJson.h> #include <ArduinoJson.h>
// https://github.com/arduino-libraries/NTPClient
#include <NTPClient.h>
// https://github.com/PaulStoffregen/Time // https://github.com/PaulStoffregen/Time
#include <TimeLib.h> #include <TimeLib.h>
// DHT support dropped
// https://github.com/adafruit/DHT-sensor-library
// #include "DHT.h"
/*
* CanGrow header files
*/
#include "CanGrow_PinAssignments.h"
#include "CanGrow_Init.h"
#include "CanGrow_Logo.h"
#include "CanGrow_Sensors.h"
#include "CanGrow_Version.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() { void setup() {
// define output for onboard LED/WIPE pin
// setup pins
pinMode(PinFAN, OUTPUT);
//pinMode(PINdht, INPUT);
pinMode(PINwaterlevel, OUTPUT);
pinMode(PINsoilmoisture, OUTPUT);
pinMode(PinLED, OUTPUT);
pinMode(PinPUMP, OUTPUT);
pinMode(PinWIPE, OUTPUT); pinMode(PinWIPE, OUTPUT);
// set all OUTPUT to low
digitalWrite(PinFAN, HIGH);
digitalWrite(PINwaterlevel, LOW);
digitalWrite(PinLED, HIGH);
digitalWrite(PinPUMP, HIGH);
// except PINsoilmoisture
// PINsoilmoisture is always HIGH and gets LOW in moment of waterlevel measurement
digitalWrite(PINsoilmoisture, LOW);
// set PWM frequency to 13.37KHz
analogWriteFreq(13370);
// Start EEPROM
EEPROM.begin(512);
// Start Serial // Start Serial
Serial.begin(115200); Serial.begin(115200);
// Write a line before doing serious output, because before there is some garbage in serial // Write a line before doing serious output, because before there is some garbage in serial
// whats get the cursor somewhere over the place // whats get the cursor somewhere over the place
Serial.println("420"); Serial.println("420");
Serial.print(".:: CanGrow firmware v"); Serial.printf(".:: CanGrow firmware v%s build %s starting ::.\n", CANGROW_VER, CANGROW_BUILD);
Serial.print(CanGrowVer);
Serial.print(" build ");
Serial.print(CanGrowBuild);
Serial.println(" starting ::.");
Serial.println(":: initialise I2C ::"); Serial.print("II To format / factory reset LittleFS, pull GPIO ");
// initialise Wire for I2C Serial.print(PinWIPE);
Wire.begin(); 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");
Serial.println(":: initialise display ::");
// initialise I2C display
display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // Address 0x3C for 128x64
display.clearDisplay();
display.display();
// set display settings // blink with the onboard LED on D4/GPIO2 (PinWIPE)
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
Serial.println(":: initialise BME280 sensor ::");
// ToDo: let the user configure somewhere the ID of the BME280 sensor
if(!bme.begin(0x76)) {
Serial.println("!! Cannot find BME280 on I2C bus. Please check connection or ID");
}
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)
for(byte i = 0; i <= 6 ; i++) { for(byte i = 0; i <= 6 ; i++) {
if(i % 2) { if(i % 2) {
digitalWrite(PinWIPE, LOW); digitalWrite(PinWIPE, 1 - PinWIPE_default);
} else { } else {
digitalWrite(PinWIPE, HIGH); digitalWrite(PinWIPE, PinWIPE_default);
} }
delay(333); delay(333);
} }
// set back to HIGH because thats the default
digitalWrite(PinWIPE, HIGH); // set PinWIPE back to its default
//delay(2000); digitalWrite(PinWIPE, PinWIPE_default);
// read status from PinWIPE to WIPE // read status from PinWIPE to WIPE
// when PinWIPE is set to LOW, wipe EEPROM // when PinWIPE is set to LOW, format LittleFS
if(digitalRead(PinWIPE) == LOW) { if(digitalRead(PinWIPE) != PinWIPE_default) {
// wipe EEPROM LFS_Format();
wipeEEPROM(); Restart();
} }
/*
* 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();
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]);
} }
// set web handler
WebHandler();
// start webserver
webserver.begin();
Serial.println(".:: CanGrow Ready ::.");
delay(1000);
if(strlen(GrowName) > 0 ) {
display.clearDisplay();
display.display();
}
} }
bool alrdySaved = false;
/*
*
*
* Loop
*
*
*/
void loop() { void loop() {
// var definition unsigned long currentMillis = millis();
unsigned long currentRuntime = millis();
if(currentMillis - schedulerPrevMillis >= config.system.schedulerInterval) {
}
// first we call webserver handle client if((digitalRead(PinWIPE) != PinWIPE_default) && (alrdySaved == false)) {
webserver.handleClient(); 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
// do every second when everything is configured and grow is started if(doRestart == true) {
if( (configured == true) && (strlen(GrowName) > 0) && (currentRuntime - outputPrevTime >= 1000) ){ Restart();
// refresh all sensor values
refreshSensors();
// 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();
displayScreens();
// current time gets previous time for new interval
outputPrevTime = currentRuntime;
} }
} }
/*
*
* 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,525 +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;
}
.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;
}
}
</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'>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'>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'>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";

View file

@ -1,178 +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;
// 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;
byte DisplayScreenDuration = 3;
// how many seconds actual screen got displayed
byte ScreenIterationPassed = 0;
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;
// helper variable for pump control with soilmoisture
// average of last readings
unsigned short valSoilmoistureAvg = 0;
unsigned short valSoilmoistureAvg_tmp = 0;
byte valSoilmoistureAvg_count = 0;
//unsigned short
/*
* 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;
unsigned short MaintenanceDuration = 300;
char Esp32CamIP[16];
// PumpLastOn (long) timestamp
unsigned long PumpLastOn;
//
// 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;
// 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,30 +0,0 @@
/*
*
* Pin assignments
*
* D0 - MOSFET Pump
* D1, D2 - I2C
* D3 - DHT11
* D4 - PinWIPE
* D5 - MOSFET Fan
* 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 PinLED = D6; //
const uint8_t PINwaterlevel = D7;
const uint8_t PINsoilmoisture = D8;
const uint8_t PINanalog = A0;

View file

@ -1,205 +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)
Adafruit_BME280 bme;
/*
* 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 : DHT11 temp sensor
* 2 : chirp I2C temp sensor
*/
float temperature = 0;
switch(tempSensor) {
case 1:
// read temperature from BME280
temperature = bme.readTemperature();
// read temperature from DHT11
// dht support dropped
// temperature = dht.readTemperature();
break;
case 2:
// 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() {
// dht support dropped
// return dht.readHumidity();
return bme.readHumidity();
}
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, wet, dry, 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,752 +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 ...
*
*/
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);
}
// 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);
}
// 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.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("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) {
/*
* Pin assignments
*
* 1 - LED
* 2 - FAN
* 3 - PUMP
*
*/
bool UseRelais = true;
byte OutputPin;
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;
}
//~ Serial.print("Output: ");
//~ Serial.println(Output);
//~ Serial.print("OutputPin: ");
//~ Serial.println(OutputPin);
//~ Serial.print("OutputState: ");
//~ Serial.println(OutputState);
//~ Serial.print("UseRelais: ");
//~ Serial.println(UseRelais);
if( (UseRelais == true) || (OutputPin == PinPUMP) ) {
digitalWrite(OutputPin, 1 - OutputState);
} else {
analogWrite(OutputPin, 255 - OutputState);
}
}
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);
}
} else {
//Serial.println("good night time");
// turn off
setOutput(1, 0);
}
}
void refreshSensors() {
byte soilmoistureAvgSampleCount = 5;
valSoilmoisture = getSoilmoisture(MoistureSensor_Type);
valHumidity = getHumidity();
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
//switch(ScreenToDisplay) {
switch(0) {
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);
EEPROM.put(237, PumpLastOn);
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) ) { // TODO: * 24 * 60 * 60 PumpInterval
// 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);
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) ) && //TODO calculate PumpInterval into days as well here
( (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);
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);
}
}

View file

@ -1,5 +0,0 @@
/* CanGrow_Version.h gets generated from cangrow.sh */
const char* CanGrowVer = "0.1-dev";
const char* CanGrowBuild = "d2e264d-20240919022041";

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. ## Build environment
A ESP8266 with Arduino Firmware is used (should most times be the standard already). 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 **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. in Project -> Settings -> Create/Make.

View file

@ -1,3 +1,4 @@
board_manager: board_manager:
additional_urls: additional_urls:
- http://arduino.esp8266.com/stable/package_esp8266com_index.json - 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>)";

View file

@ -1,103 +0,0 @@
#!/bin/bash
#
test -z $TTY && TTY="/dev/ttyUSB0"
test -z $IP && IP="192.168.30.212"
test -z $VER && VER="0.1-dev"
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" )
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
echo "/* CanGrow_Version.h gets generated from cangrow.sh */
const char* CanGrowVer = \"${VER}\";
const char* CanGrowBuild = \"${BUILD}\";
" > Arduino/CanGrow/CanGrow_Version.h
${ACLI_CMD} --no-color compile -b ${BOARD} "Arduino/CanGrow/CanGrow.ino" --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> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset='UTF-8'> <meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'> <meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>CanGrow - Amnesia Haze</title> <title>CanGrow - CanGrow v0.2-dev</title>
<link rel='stylesheet' href='cangrow.css'>
<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='> <script type='text/javascript' src='cangrow.js'></script>
<style> <style>
body { .linkForm {
color: #cae0d0; display: inline-block;
background-color: #1d211e; }
font-family: helvetica;
} .linkForm input[type=submit] {
background: #262B27;
.center { padding: 5px;
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;
} }
}
</style> </style>
</head> </head>
<body> <body>
<ul class='nav'><li><a href='/'>&#x1F331; Amnesia Haze</a></li> <ul class='nav'><li><a href='/'>&#x1F331; CanGrow</a></li>
<li><a href='/growSettings' >&#128262; Grow settings</a></li> <li><a class='' href='/grow/' >&#128262; Grow settings</a></li>
<li><a href='/systemSettings' >&#9881; System settings</a></li> <li><a class='activeNav' href='/system/' >&#9881; System settings</a></li>
<li><a href='/wifiSettings' >&#128225; WiFi settings</a></li> <li><a class='' href='/wifi/' >&#128225; WiFi settings</a></li>
<li><a href='/help' >&#x2753; Help</a></li> <li><a class='' href='/help' >&#x2753; Help</a></li>
<li><span class='MenuTime'>09:48:27</span></li> <li><span class='navTime'>04:20:23</span></li>
<li><a href='https://git.la10cy.net/DeltaLima/CanGrow' target='_blank'>CanGrow v0.1</a></li> <li><a href='https://git.la10cy.net/DeltaLima/CanGrow' target='_blank'>CanGrow v0.2-dev</a></li>
</ul><div class='center'><h2>&#x1F504; Firmware update</h2>Version: 0.1<br>Build: 4ad16c9 </ul>
<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> <div class='center'>
<form method='GET' action='' enctype='multipart/form-data'>
Firmware .bin file:<br>
<input type='file' accept='.bin,.bin.gz' name='firmware'>
</form> <ul class='subnav'>
<button onclick="document.getElementById('divUploading').style.display = ''; window.alert('click');">asd</button> <li><a class='activeNav' href='/system/output/'>&#9889; Output configuration</a></li>
<div id='divUploading' style='display: none;' class='warnmsg'>Uploading, please wait...<div> <li><a class='' href='/system/update'>&#x1F504; Firmware update</a></li>
<li><a class='' href='/system/restart' >&#x1F501; CanGrow restart</a></li>
</div> <li><a class='' href='/system/wipe' >&#x1F4A3; Factory reset</a></li>
</body> </ul>
</html> <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>