ESP32 based project
This commit is contained in:
parent
b6d3f96239
commit
c8ebe2a6bc
35
board/.gitignore
vendored
Normal file
35
board/.gitignore
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Software code stuff
|
||||||
|
*.swp
|
||||||
|
*.o
|
||||||
|
*.hex
|
||||||
|
*.lst
|
||||||
|
*.eep
|
||||||
|
*.sym
|
||||||
|
*.map
|
||||||
|
*.lss
|
||||||
|
*.elf
|
||||||
|
.dep/
|
||||||
|
# KiCAD board stuff
|
||||||
|
# export files for BOM
|
||||||
|
*.csv
|
||||||
|
*.tsv
|
||||||
|
*.xml
|
||||||
|
# backup files
|
||||||
|
*.bak
|
||||||
|
# Temporary files
|
||||||
|
*.000
|
||||||
|
*.bak
|
||||||
|
*.bck
|
||||||
|
*.kicad_pcb-bak
|
||||||
|
*~
|
||||||
|
_autosave-*
|
||||||
|
*.tmp
|
||||||
|
*-cache.lib
|
||||||
|
*-rescue.lib
|
||||||
|
*-save.pro
|
||||||
|
*-save.kicad_pcb
|
||||||
|
|
||||||
|
# Netlist files (exported from Eeschema)
|
||||||
|
*.net
|
||||||
|
|
||||||
|
# Autorouter files (exported from Pcbnew)
|
3476
board/PlantCtrlESP32.kicad_pcb
Normal file
3476
board/PlantCtrlESP32.kicad_pcb
Normal file
File diff suppressed because it is too large
Load Diff
281
board/PlantCtrlESP32.pro
Normal file
281
board/PlantCtrlESP32.pro
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
update=Mi 26 Aug 2020 18:08:55 CEST
|
||||||
|
version=1
|
||||||
|
last_client=kicad
|
||||||
|
[general]
|
||||||
|
version=1
|
||||||
|
RootSch=
|
||||||
|
BoardNm=
|
||||||
|
[cvpcb]
|
||||||
|
version=1
|
||||||
|
NetIExt=net
|
||||||
|
[eeschema]
|
||||||
|
version=1
|
||||||
|
LibDir=
|
||||||
|
[eeschema/libraries]
|
||||||
|
[pcbnew]
|
||||||
|
version=1
|
||||||
|
PageLayoutDescrFile=
|
||||||
|
LastNetListRead=PlantCtrlESP32.net
|
||||||
|
CopperLayerCount=2
|
||||||
|
BoardThickness=1.6
|
||||||
|
AllowMicroVias=0
|
||||||
|
AllowBlindVias=0
|
||||||
|
RequireCourtyardDefinitions=0
|
||||||
|
ProhibitOverlappingCourtyards=1
|
||||||
|
MinTrackWidth=0.2
|
||||||
|
MinViaDiameter=0.4
|
||||||
|
MinViaDrill=0.3
|
||||||
|
MinMicroViaDiameter=0.2
|
||||||
|
MinMicroViaDrill=0.09999999999999999
|
||||||
|
MinHoleToHole=0.25
|
||||||
|
TrackWidth1=1.2
|
||||||
|
ViaDiameter1=0.8
|
||||||
|
ViaDrill1=0.4
|
||||||
|
dPairWidth1=0.2
|
||||||
|
dPairGap1=0.25
|
||||||
|
dPairViaGap1=0.25
|
||||||
|
SilkLineWidth=0.12
|
||||||
|
SilkTextSizeV=1
|
||||||
|
SilkTextSizeH=1
|
||||||
|
SilkTextSizeThickness=0.15
|
||||||
|
SilkTextItalic=0
|
||||||
|
SilkTextUpright=1
|
||||||
|
CopperLineWidth=0.2
|
||||||
|
CopperTextSizeV=1.5
|
||||||
|
CopperTextSizeH=1.5
|
||||||
|
CopperTextThickness=0.3
|
||||||
|
CopperTextItalic=0
|
||||||
|
CopperTextUpright=1
|
||||||
|
EdgeCutLineWidth=0.05
|
||||||
|
CourtyardLineWidth=0.05
|
||||||
|
OthersLineWidth=0.15
|
||||||
|
OthersTextSizeV=1
|
||||||
|
OthersTextSizeH=1
|
||||||
|
OthersTextSizeThickness=0.15
|
||||||
|
OthersTextItalic=0
|
||||||
|
OthersTextUpright=1
|
||||||
|
SolderMaskClearance=0.051
|
||||||
|
SolderMaskMinWidth=0.25
|
||||||
|
SolderPasteClearance=0
|
||||||
|
SolderPasteRatio=-0
|
||||||
|
[pcbnew/Layer.F.Cu]
|
||||||
|
Name=F.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.In1.Cu]
|
||||||
|
Name=In1.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In2.Cu]
|
||||||
|
Name=In2.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In3.Cu]
|
||||||
|
Name=In3.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In4.Cu]
|
||||||
|
Name=In4.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In5.Cu]
|
||||||
|
Name=In5.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In6.Cu]
|
||||||
|
Name=In6.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In7.Cu]
|
||||||
|
Name=In7.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In8.Cu]
|
||||||
|
Name=In8.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In9.Cu]
|
||||||
|
Name=In9.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In10.Cu]
|
||||||
|
Name=In10.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In11.Cu]
|
||||||
|
Name=In11.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In12.Cu]
|
||||||
|
Name=In12.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In13.Cu]
|
||||||
|
Name=In13.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In14.Cu]
|
||||||
|
Name=In14.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In15.Cu]
|
||||||
|
Name=In15.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In16.Cu]
|
||||||
|
Name=In16.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In17.Cu]
|
||||||
|
Name=In17.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In18.Cu]
|
||||||
|
Name=In18.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In19.Cu]
|
||||||
|
Name=In19.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In20.Cu]
|
||||||
|
Name=In20.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In21.Cu]
|
||||||
|
Name=In21.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In22.Cu]
|
||||||
|
Name=In22.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In23.Cu]
|
||||||
|
Name=In23.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In24.Cu]
|
||||||
|
Name=In24.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In25.Cu]
|
||||||
|
Name=In25.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In26.Cu]
|
||||||
|
Name=In26.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In27.Cu]
|
||||||
|
Name=In27.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In28.Cu]
|
||||||
|
Name=In28.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In29.Cu]
|
||||||
|
Name=In29.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.In30.Cu]
|
||||||
|
Name=In30.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Layer.B.Cu]
|
||||||
|
Name=B.Cu
|
||||||
|
Type=0
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.B.Adhes]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.F.Adhes]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.B.Paste]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.F.Paste]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.B.SilkS]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.F.SilkS]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.B.Mask]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.F.Mask]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.Dwgs.User]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.Cmts.User]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.Eco1.User]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.Eco2.User]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.Edge.Cuts]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.Margin]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.B.CrtYd]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.F.CrtYd]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.B.Fab]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.F.Fab]
|
||||||
|
Enabled=1
|
||||||
|
[pcbnew/Layer.Rescue]
|
||||||
|
Enabled=0
|
||||||
|
[pcbnew/Netclasses]
|
||||||
|
[pcbnew/Netclasses/Default]
|
||||||
|
Name=Default
|
||||||
|
Clearance=0.2
|
||||||
|
TrackWidth=1.2
|
||||||
|
ViaDiameter=0.8
|
||||||
|
ViaDrill=0.4
|
||||||
|
uViaDiameter=0.3
|
||||||
|
uViaDrill=0.1
|
||||||
|
dPairWidth=0.2
|
||||||
|
dPairGap=0.25
|
||||||
|
dPairViaGap=0.25
|
||||||
|
[pcbnew/Netclasses/1]
|
||||||
|
Name=5V
|
||||||
|
Clearance=0.2
|
||||||
|
TrackWidth=1.4
|
||||||
|
ViaDiameter=0.8
|
||||||
|
ViaDrill=0.4
|
||||||
|
uViaDiameter=0.3
|
||||||
|
uViaDrill=0.1
|
||||||
|
dPairWidth=0.2
|
||||||
|
dPairGap=0.25
|
||||||
|
dPairViaGap=0.25
|
||||||
|
[pcbnew/Netclasses/2]
|
||||||
|
Name=Mini
|
||||||
|
Clearance=0.2
|
||||||
|
TrackWidth=1
|
||||||
|
ViaDiameter=0.8
|
||||||
|
ViaDrill=0.4
|
||||||
|
uViaDiameter=0.3
|
||||||
|
uViaDrill=0.1
|
||||||
|
dPairWidth=0.2
|
||||||
|
dPairGap=0.25
|
||||||
|
dPairViaGap=0.25
|
||||||
|
[pcbnew/Netclasses/3]
|
||||||
|
Name=Power
|
||||||
|
Clearance=0.2
|
||||||
|
TrackWidth=1.7
|
||||||
|
ViaDiameter=0.8
|
||||||
|
ViaDrill=0.4
|
||||||
|
uViaDiameter=0.3
|
||||||
|
uViaDrill=0.1
|
||||||
|
dPairWidth=0.2
|
||||||
|
dPairGap=0.25
|
||||||
|
dPairViaGap=0.25
|
||||||
|
[schematic_editor]
|
||||||
|
version=1
|
||||||
|
PageLayoutDescrFile=
|
||||||
|
PlotDirectoryName=/tmp/
|
||||||
|
SubpartIdSeparator=0
|
||||||
|
SubpartFirstId=65
|
||||||
|
NetFmtName=Pcbnew
|
||||||
|
SpiceAjustPassiveValues=0
|
||||||
|
LabSize=50
|
||||||
|
ERC_TestSimilarLabels=1
|
1198
board/PlantCtrlESP32.sch
Normal file
1198
board/PlantCtrlESP32.sch
Normal file
File diff suppressed because it is too large
Load Diff
1274
board/PlantCtrlESP32.sch-bak
Normal file
1274
board/PlantCtrlESP32.sch-bak
Normal file
File diff suppressed because it is too large
Load Diff
11
board/ReadMe.md
Normal file
11
board/ReadMe.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# ESP32 Plant Control Board
|
||||||
|
|
||||||
|
The board was built with manily through hole components for easy build and production with a mill.
|
||||||
|
|
||||||
|
## GPIO Mapping
|
||||||
|
See in the parent folder at **include/ControllerConfiguration.h**
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
In order to use the the mill, the following parameter were used:
|
||||||
|
* Clearance 0.2mm
|
||||||
|
* Track width of 1.2mm or 1.0mm at minimum (when pins of a part are too close)
|
1
board/fp-info-cache
Normal file
1
board/fp-info-cache
Normal file
@ -0,0 +1 @@
|
|||||||
|
0
|
5
board/gerber/.gitignore
vendored
Normal file
5
board/gerber/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
*.gbr
|
||||||
|
*.drl
|
||||||
|
*.ngc
|
||||||
|
*.png
|
||||||
|
*.bakT*
|
33564
board/gerber/PlantCtrlESP32-drl_map.dxf
Normal file
33564
board/gerber/PlantCtrlESP32-drl_map.dxf
Normal file
File diff suppressed because it is too large
Load Diff
39
board/gerber/ReadMe.md
Normal file
39
board/gerber/ReadMe.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Export Gerber
|
||||||
|
The exported gerber files can be used to convert it into gcode for a mill
|
||||||
|
## Export settings
|
||||||
|
|
||||||
|
Open the board in KiCad and select:
|
||||||
|
File | Plot
|
||||||
|
### General
|
||||||
|
Plot format: Gerber
|
||||||
|
### Include Layer
|
||||||
|
Include the Layer ***B.Cu*** and ***Edge.Cuts***
|
||||||
|
[ ] Plot border and title block
|
||||||
|
[x] Plot footprint values
|
||||||
|
[x] Plot footprint reference
|
||||||
|
[ ] Force plotting of invisible values / refs
|
||||||
|
[x] Exclude PCB edge layer from other layers
|
||||||
|
[x] Exclude pads from silk screen
|
||||||
|
[ ] Do not tent vias
|
||||||
|
[x] Use auxilary axis as origin
|
||||||
|
Drill marks: None
|
||||||
|
Scaling: 1:1
|
||||||
|
Plot mode: Filled
|
||||||
|
Default line width: 0.1mm
|
||||||
|
[ ] Mirrored plot
|
||||||
|
[ ] Negated plot
|
||||||
|
### Gerber Options
|
||||||
|
[ ] Use Protel filename extensions
|
||||||
|
[ ] Generate Geber job file
|
||||||
|
[ ] Substract soldermask from silkscreen
|
||||||
|
Coordinate format: 4.6, unit mm
|
||||||
|
[ ] Use extended X2 format
|
||||||
|
[ ] Include netlist attributes
|
||||||
|
### Doing
|
||||||
|
Click
|
||||||
|
* **Plot**
|
||||||
|
* **Generate Drill Files ...**
|
||||||
|
* [x] PTH and NPTH in a single file
|
||||||
|
* Map File Format: DXF
|
||||||
|
* Drill Units: mm
|
||||||
|
* Drill Origin: Auxilary axis
|
16
board/gerber/generatePCB.sh
Executable file
16
board/gerber/generatePCB.sh
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Needs the tool pcb2gcode
|
||||||
|
# Was documented at: http://marcuswolschon.blogspot.de/2013/02/milling-pcbs-using-gerber2gcode.html
|
||||||
|
|
||||||
|
MILLSPEED=600
|
||||||
|
MILLFEED=200
|
||||||
|
PROJECT=PlantCtrlESP32
|
||||||
|
pcb2gcode --back $PROJECT-B_Cu.gbr --metric --zsafe 5 --zchange 10 --zwork -0.01 --offset 0.02 --mill-feed $MILLFEED --mill-speed $MILLSPEED --drill $PROJECT.drl --zdrill -2.5 --drill-feed $MILLFEED --drill-speed $MILLSPEED --basename $PROJECT
|
||||||
|
|
||||||
|
if [ "$1" == "C3MA" ]; then
|
||||||
|
#update all Tools higher and equal to T4 in generated file
|
||||||
|
for i in 4 5 6 7; do
|
||||||
|
echo "Replace T$i"
|
||||||
|
sed -i.bakT$i "s/T${i}/T3/" ${PROJECT}_drill.ngc
|
||||||
|
done
|
||||||
|
fi
|
17
esp32/PlantControl.code-workspace
Normal file
17
esp32/PlantControl.code-workspace
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"files.associations": {
|
||||||
|
"*.tcc": "cpp",
|
||||||
|
"bitset": "cpp",
|
||||||
|
"algorithm": "cpp",
|
||||||
|
"istream": "cpp",
|
||||||
|
"limits": "cpp",
|
||||||
|
"streambuf": "cpp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
esp32/Readme.md
Normal file
15
esp32/Readme.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# PlantControl
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
Uses ESP32MniniKit
|
||||||
|
|
||||||
|
### Used Pins:
|
||||||
|
* IO27 for DS18B20 temperature sensor
|
||||||
|
|
||||||
|
## Software
|
||||||
|
* Mqtt topics
|
||||||
|
* temperature
|
||||||
|
* switch1
|
||||||
|
* Settings:
|
||||||
|
* ds18b20 - Enables Temperature measurement
|
||||||
|
* deepsleep - Setup intervall how long the controller sleeps
|
11
esp32/generatePCB.sh
Executable file
11
esp32/generatePCB.sh
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Needs the tool pcb2gcode
|
||||||
|
# Was documented at: http://marcuswolschon.blogspot.de/2013/02/milling-pcbs-using-gerber2gcode.html
|
||||||
|
|
||||||
|
MILLSPEED=600
|
||||||
|
MILLFEED=200
|
||||||
|
PROJECT=PlantCtrlESP32
|
||||||
|
pcb2gcode --back $PROJECT-B_Cu.gbr --outline $PROJECT-Edge_Cuts.gbr --metric \
|
||||||
|
--zsafe 5 --zchange 10 --zwork -0.01 --offset 0.02 --mill-feed $MILLFEED --mill-speed $MILLSPEED \
|
||||||
|
--drill $PROJECT.drl --zdrill -2.5 --drill-feed $MILLFEED --drill-speed $MILLSPEED --basename $PROJECT \
|
||||||
|
--zcut 1.0 --cut-infeed 1.0 --cut-feed $MILLFEED --cutter-diameter 1.0 --cut-speed $MILLSPEED
|
1
esp32/homie/.gitignore
vendored
Normal file
1
esp32/homie/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
config.json
|
BIN
esp32/homie/ui_bundle.gz
Normal file
BIN
esp32/homie/ui_bundle.gz
Normal file
Binary file not shown.
63
esp32/host/Readme.md
Normal file
63
esp32/host/Readme.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Configuration
|
||||||
|
## File
|
||||||
|
Generate a file as, described in
|
||||||
|
https://homieiot.github.io/homie-esp8266/docs/3.0.0/configuration/json-configuration-file/
|
||||||
|
For further details have a look at the Readme.md one level above.
|
||||||
|
|
||||||
|
## Upload
|
||||||
|
* Start ESP
|
||||||
|
* Login to Wifi, opened by the ESP
|
||||||
|
* Use the script to upload the configuration file
|
||||||
|
* restart the ESP
|
||||||
|
|
||||||
|
# Remote Upload
|
||||||
|
|
||||||
|
|
||||||
|
This script will allow you to send an OTA update to your device.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Requirements are:
|
||||||
|
* paho-mqtt
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```text
|
||||||
|
usage: ota_updater.py [-h] -l BROKER_HOST -p BROKER_PORT [-u BROKER_USERNAME]
|
||||||
|
[-d BROKER_PASSWORD] [-t BASE_TOPIC] -i DEVICE_ID
|
||||||
|
firmware
|
||||||
|
|
||||||
|
ota firmware update scirpt for ESP8226 implemenation of the Homie mqtt IoT
|
||||||
|
convention.
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
firmware path to the firmware to be sent to the device
|
||||||
|
|
||||||
|
arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-l BROKER_HOST, --broker-host BROKER_HOST
|
||||||
|
host name or ip address of the mqtt broker
|
||||||
|
-p BROKER_PORT, --broker-port BROKER_PORT
|
||||||
|
port of the mqtt broker
|
||||||
|
-u BROKER_USERNAME, --broker-username BROKER_USERNAME
|
||||||
|
username used to authenticate with the mqtt broker
|
||||||
|
-d BROKER_PASSWORD, --broker-password BROKER_PASSWORD
|
||||||
|
password used to authenticate with the mqtt broker
|
||||||
|
-t BASE_TOPIC, --base-topic BASE_TOPIC
|
||||||
|
base topic of the homie devices on the broker
|
||||||
|
-i DEVICE_ID, --device-id DEVICE_ID
|
||||||
|
homie device id
|
||||||
|
```
|
||||||
|
|
||||||
|
* `BROKER_HOST` and `BROKER_PORT` defaults to 127.0.0.1 and 1883 respectively if not set.
|
||||||
|
* `BROKER_USERNAME` and `BROKER_PASSWORD` are optional.
|
||||||
|
* `BASE_TOPIC` has to end with a slash, defaults to `homie/` if not set.
|
||||||
|
|
||||||
|
### Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python ota_updater.py -l localhost -u admin -d secure -t "homie/" -i "device-id" /path/to/firmware.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source
|
||||||
|
https://github.com/homieiot/homie-esp8266/blob/develop/scripts/ota_updater
|
27
esp32/host/config-example.json
Normal file
27
esp32/host/config-example.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "PlantControl",
|
||||||
|
"device_id": "PlantCtrl1",
|
||||||
|
"device_stats_interval": 60,
|
||||||
|
"wifi": {
|
||||||
|
"ssid": "SSID",
|
||||||
|
"bssid" : "BSSID",
|
||||||
|
"password": "mysecretPassword",
|
||||||
|
"channel": 1
|
||||||
|
},
|
||||||
|
"mqtt": {
|
||||||
|
"host": "[0-255].[0-255].[0-255].[0-255]",
|
||||||
|
"port": 1883,
|
||||||
|
"base_topic": "mqtt/topic/",
|
||||||
|
"auth": false
|
||||||
|
},
|
||||||
|
"ota": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"deepsleep": 60000,
|
||||||
|
"plants" : 3,
|
||||||
|
"moist1" : 2000,
|
||||||
|
"moist2" : 2000,
|
||||||
|
"moist3" : 2000
|
||||||
|
}
|
||||||
|
}
|
174
esp32/host/ota_updater.py
Executable file
174
esp32/host/ota_updater.py
Executable file
@ -0,0 +1,174 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from __future__ import division, print_function
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import base64, sys, math
|
||||||
|
from hashlib import md5
|
||||||
|
|
||||||
|
# The callback for when the client receives a CONNACK response from the server.
|
||||||
|
def on_connect(client, userdata, flags, rc):
|
||||||
|
if rc != 0:
|
||||||
|
print("Connection Failed with result code {}".format(rc))
|
||||||
|
client.disconnect()
|
||||||
|
else:
|
||||||
|
print("Connected with result code {}".format(rc))
|
||||||
|
|
||||||
|
client.subscribe("{base_topic}{device_id}/$state".format(**userdata)) # v3 / v4 devices
|
||||||
|
client.subscribe("{base_topic}{device_id}/$online".format(**userdata)) # v2 devices
|
||||||
|
|
||||||
|
|
||||||
|
print("Waiting for device to come online...")
|
||||||
|
|
||||||
|
|
||||||
|
# The callback for when a PUBLISH message is received from the server.
|
||||||
|
def on_message(client, userdata, msg):
|
||||||
|
# decode string for python2/3 compatiblity
|
||||||
|
msg.payload = msg.payload.decode()
|
||||||
|
|
||||||
|
if msg.topic.endswith('$implementation/ota/status'):
|
||||||
|
status = int(msg.payload.split()[0])
|
||||||
|
|
||||||
|
if userdata.get("published"):
|
||||||
|
if status == 206: # in progress
|
||||||
|
# state in progress, print progress bar
|
||||||
|
progress, total = [int(x) for x in msg.payload.split()[1].split('/')]
|
||||||
|
bar_width = 30
|
||||||
|
bar = int(bar_width*(progress/total))
|
||||||
|
print("\r[", '+'*bar, ' '*(bar_width-bar), "] ", msg.payload.split()[1], end='', sep='')
|
||||||
|
if (progress == total):
|
||||||
|
print()
|
||||||
|
sys.stdout.flush()
|
||||||
|
elif status == 304: # not modified
|
||||||
|
print("Device firmware already up to date with md5 checksum: {}".format(userdata.get('md5')))
|
||||||
|
client.disconnect()
|
||||||
|
elif status == 403: # forbidden
|
||||||
|
print("Device ota disabled, aborting...")
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
elif msg.topic.endswith('$fw/checksum'):
|
||||||
|
checksum = msg.payload
|
||||||
|
|
||||||
|
if userdata.get("published"):
|
||||||
|
if checksum == userdata.get('md5'):
|
||||||
|
print("Device back online. Update Successful!")
|
||||||
|
else:
|
||||||
|
print("Expecting checksum {}, got {}, update failed!".format(userdata.get('md5'), checksum))
|
||||||
|
client.disconnect()
|
||||||
|
else:
|
||||||
|
if checksum != userdata.get('md5'): # save old md5 for comparison with new firmware
|
||||||
|
userdata.update({'old_md5': checksum})
|
||||||
|
else:
|
||||||
|
print("Device firmware already up to date with md5 checksum: {}".format(checksum))
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
elif msg.topic.endswith('ota/enabled'):
|
||||||
|
if msg.payload == 'true':
|
||||||
|
userdata.update({'ota_enabled': True})
|
||||||
|
else:
|
||||||
|
print("Device ota disabled, aborting...")
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
elif msg.topic.endswith('$state') or msg.topic.endswith('$online'):
|
||||||
|
if (msg.topic.endswith('$state') and msg.payload != 'ready') or (msg.topic.endswith('$online') and msg.payload == 'false'):
|
||||||
|
return
|
||||||
|
|
||||||
|
# calcluate firmware md5
|
||||||
|
firmware_md5 = md5(userdata['firmware']).hexdigest()
|
||||||
|
userdata.update({'md5': firmware_md5})
|
||||||
|
|
||||||
|
# Subscribing in on_connect() means that if we lose the connection and
|
||||||
|
# reconnect then subscriptions will be renewed.
|
||||||
|
client.subscribe("{base_topic}{device_id}/$implementation/ota/status".format(**userdata))
|
||||||
|
client.subscribe("{base_topic}{device_id}/$implementation/ota/enabled".format(**userdata))
|
||||||
|
client.subscribe("{base_topic}{device_id}/$fw/#".format(**userdata))
|
||||||
|
|
||||||
|
# Wait for device info to come in and invoke the on_message callback where update will continue
|
||||||
|
print("Waiting for device info...")
|
||||||
|
|
||||||
|
if ( not userdata.get("published") ) and ( userdata.get('ota_enabled') ) and \
|
||||||
|
( 'old_md5' in userdata.keys() ) and ( userdata.get('md5') != userdata.get('old_md5') ):
|
||||||
|
# push the firmware binary
|
||||||
|
userdata.update({"published": True})
|
||||||
|
topic = "{base_topic}{device_id}/$implementation/ota/firmware/{md5}".format(**userdata)
|
||||||
|
print("Publishing new firmware with checksum {}".format(userdata.get('md5')))
|
||||||
|
client.publish(topic, userdata['firmware'])
|
||||||
|
|
||||||
|
|
||||||
|
def main(broker_host, broker_port, broker_username, broker_password, broker_ca_cert, base_topic, device_id, firmware):
|
||||||
|
# initialise mqtt client and register callbacks
|
||||||
|
client = mqtt.Client()
|
||||||
|
client.on_connect = on_connect
|
||||||
|
client.on_message = on_message
|
||||||
|
|
||||||
|
# set username and password if given
|
||||||
|
if broker_username and broker_password:
|
||||||
|
client.username_pw_set(broker_username, broker_password)
|
||||||
|
|
||||||
|
if broker_ca_cert is not None:
|
||||||
|
client.tls_set(
|
||||||
|
ca_certs=broker_ca_cert
|
||||||
|
)
|
||||||
|
|
||||||
|
# save data to be used in the callbacks
|
||||||
|
client.user_data_set({
|
||||||
|
"base_topic": base_topic,
|
||||||
|
"device_id": device_id,
|
||||||
|
"firmware": firmware
|
||||||
|
})
|
||||||
|
|
||||||
|
# start connection
|
||||||
|
print("Connecting to mqtt broker {} on port {}".format(broker_host, broker_port))
|
||||||
|
client.connect(broker_host, broker_port, 60)
|
||||||
|
|
||||||
|
# Blocking call that processes network traffic, dispatches callbacks and handles reconnecting.
|
||||||
|
client.loop_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='ota firmware update scirpt for ESP8226 implemenation of the Homie mqtt IoT convention.')
|
||||||
|
|
||||||
|
# ensure base topic always ends with a '/'
|
||||||
|
def base_topic_arg(s):
|
||||||
|
s = str(s)
|
||||||
|
if not s.endswith('/'):
|
||||||
|
s = s + '/'
|
||||||
|
return s
|
||||||
|
|
||||||
|
# specify arguments
|
||||||
|
parser.add_argument('-l', '--broker-host', type=str, required=False,
|
||||||
|
help='host name or ip address of the mqtt broker', default="127.0.0.1")
|
||||||
|
parser.add_argument('-p', '--broker-port', type=int, required=False,
|
||||||
|
help='port of the mqtt broker', default=1883)
|
||||||
|
parser.add_argument('-u', '--broker-username', type=str, required=False,
|
||||||
|
help='username used to authenticate with the mqtt broker')
|
||||||
|
parser.add_argument('-d', '--broker-password', type=str, required=False,
|
||||||
|
help='password used to authenticate with the mqtt broker')
|
||||||
|
parser.add_argument('-t', '--base-topic', type=base_topic_arg, required=False,
|
||||||
|
help='base topic of the homie devices on the broker', default="homie/")
|
||||||
|
parser.add_argument('-i', '--device-id', type=str, required=True,
|
||||||
|
help='homie device id')
|
||||||
|
parser.add_argument('firmware', type=argparse.FileType('rb'),
|
||||||
|
help='path to the firmware to be sent to the device')
|
||||||
|
|
||||||
|
parser.add_argument("--broker-tls-cacert", default=None, required=False,
|
||||||
|
help="CA certificate bundle used to validate TLS connections. If set, TLS will be enabled on the broker conncetion"
|
||||||
|
)
|
||||||
|
|
||||||
|
# workaround for http://bugs.python.org/issue9694
|
||||||
|
parser._optionals.title = "arguments"
|
||||||
|
|
||||||
|
# get and validate arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# read the contents of firmware into buffer
|
||||||
|
fw_buffer = args.firmware.read()
|
||||||
|
args.firmware.close()
|
||||||
|
firmware = bytearray()
|
||||||
|
firmware.extend(fw_buffer)
|
||||||
|
|
||||||
|
# Invoke the business logic
|
||||||
|
main(args.broker_host, args.broker_port, args.broker_username,
|
||||||
|
args.broker_password, args.broker_tls_cacert, args.base_topic, args.device_id, firmware)
|
13
esp32/host/upload.sh
Executable file
13
esp32/host/upload.sh
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Homie device is in AP mode, then the configuration can be uploaded"
|
||||||
|
|
||||||
|
if [ ! -f config.json ]; then
|
||||||
|
echo "Create config file according to :"
|
||||||
|
echo "https://homieiot.github.io/homie-esp8266/docs/3.0.0/configuration/json-configuration-file/"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Check connection to Plug in AP-mode"
|
||||||
|
ping -c 4 192.168.123.1
|
||||||
|
|
||||||
|
curl -X PUT http://192.168.123.1/config --header "Content-Type: application/json" -d @config.json
|
55
esp32/include/ControllerConfiguration.h
Normal file
55
esp32/include/ControllerConfiguration.h
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* @file ControllerConfiguration.h
|
||||||
|
* @author your name (you@domain.com)
|
||||||
|
* @brief
|
||||||
|
* @version 0.1
|
||||||
|
* @date 2020-05-30
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020
|
||||||
|
* Describe the used PINs of the controller
|
||||||
|
*/
|
||||||
|
#ifndef CONTROLLER_CONFIG_H
|
||||||
|
#define CONTROLLER_CONFIG_H
|
||||||
|
|
||||||
|
#define FIRMWARE_VERSION "0.9.5"
|
||||||
|
|
||||||
|
#define ADC_TO_VOLT(adc) ((adc) * 3.3 ) / 4095)
|
||||||
|
#define ADC_TO_VOLT_WITH_MULTI(adc, multi) (((adc) * 3.3 * (multi)) / 4095)
|
||||||
|
|
||||||
|
#define SOLAR_VOLT(adc) ADC_TO_VOLT_WITH_MULTI(adc, 4.0306) /**< 100k and 33k voltage dividor */
|
||||||
|
#define ADC_5V_TO_3V3(adc) ADC_TO_VOLT_WITH_MULTI(adc, 1.7) /**< 33k and 47k8 voltage dividor */
|
||||||
|
#define MS_TO_S 1000
|
||||||
|
|
||||||
|
#define SENSOR_LIPO 34 /**< GPIO 34 (ADC1) */
|
||||||
|
#define SENSOR_SOLAR 35 /**< GPIO 35 (ADC1) */
|
||||||
|
#define SENSOR_PLANT1 32 /**< GPIO 32 (ADC1) */
|
||||||
|
#define SENSOR_PLANT2 33 /**< GPIO 33 (ADC1) */
|
||||||
|
#define SENSOR_PLANT3 25 /**< GPIO 25 (ADC2) */
|
||||||
|
#define SENSOR_PLANT4 26 /**< GPIO 26 (ADC2) */
|
||||||
|
#define SENSOR_PLANT5 27 /**< GPIO 27 (ADC2) */
|
||||||
|
#define SENSOR_PLANT6 14 /**< GPIO 14 (ADC2) */
|
||||||
|
|
||||||
|
#define OUTPUT_PUMP1 5 /**< GPIO 5 */
|
||||||
|
#define OUTPUT_PUMP2 18 /**< GPIO 18 */
|
||||||
|
#define OUTPUT_PUMP3 19 /**< GPIO 19 */
|
||||||
|
#define OUTPUT_PUMP4 21 /**< GPIO 21 */
|
||||||
|
#define OUTPUT_PUMP5 22 /**< GPIO 22 */
|
||||||
|
#define OUTPUT_PUMP6 23 /**< GPIO 23 */
|
||||||
|
|
||||||
|
#define OUTPUT_SENSOR 4 /**< GPIO 4 */
|
||||||
|
#define INPUT_WATER_LOW 2 /**< GPIO 2 */
|
||||||
|
#define INPUT_WATER_EMPTY 15 /**< GPIO 15 */
|
||||||
|
#define INPUT_WATER_OVERFLOW 12 /**< GPIO 12 */
|
||||||
|
|
||||||
|
#define SENSOR_DS18B20 13 /**< GPIO 13 */
|
||||||
|
#define BUTTON 0 /**< GPIO 0 */
|
||||||
|
|
||||||
|
#define MIN_TIME_RUNNING 10UL /**< Amount of seconds the controller must stay awoken */
|
||||||
|
#define MAX_PLANTS 3
|
||||||
|
#define EMPTY_LIPO_MULTIPL 3 /**< Multiplier to increase time for sleeping when lipo is empty */
|
||||||
|
#define MINIMUM_LIPO_VOLT 3.3f /**< Minimum voltage of the Lipo, that must be present */
|
||||||
|
#define MINIMUM_SOLAR_VOLT 4.0f /**< Minimum voltage of the sun, to detect daylight */
|
||||||
|
|
||||||
|
#define HC_SR04 /**< Ultrasonic distance sensor to measure water level */
|
||||||
|
|
||||||
|
#endif
|
52
esp32/include/DS18B20.h
Normal file
52
esp32/include/DS18B20.h
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* @file DS18B20.h
|
||||||
|
* @author your name (you@domain.com)
|
||||||
|
* @brief
|
||||||
|
* @version 0.1
|
||||||
|
* @date 2020-06-09
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020
|
||||||
|
* Based on the LUA code from the ESP8266
|
||||||
|
* --------------------------------------------------------------------------------
|
||||||
|
* -- DS18B20 one wire module for NODEMCU
|
||||||
|
* -- NODEMCU TEAM
|
||||||
|
* -- LICENCE: http://opensource.org/licenses/MIT
|
||||||
|
* -- Vowstar <vowstar@nodemcu.com>
|
||||||
|
* -- 2015/02/14 sza2 <sza2trash@gmail.com> Fix for negative values
|
||||||
|
* --------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DS18B20_H
|
||||||
|
#define DS18B20_H
|
||||||
|
|
||||||
|
#include <OneWire.h>
|
||||||
|
|
||||||
|
class Ds18B20 {
|
||||||
|
private:
|
||||||
|
OneWire* mDs;
|
||||||
|
int foundDevices;
|
||||||
|
public:
|
||||||
|
Ds18B20(int pin) {
|
||||||
|
this->mDs = new OneWire(pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
~Ds18B20() {
|
||||||
|
delete this->mDs;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @brief read amount sensots
|
||||||
|
* check for available of DS18B20 sensors
|
||||||
|
* @return amount of sensors
|
||||||
|
*/
|
||||||
|
int readDevices(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Read all temperatures in celsius
|
||||||
|
*
|
||||||
|
* @param pTemperatures array of float valuies
|
||||||
|
* @param maxTemperatures size of the given array
|
||||||
|
* @return int amount of read temperature values
|
||||||
|
*/
|
||||||
|
int readAllTemperatures(float* pTemperatures, int maxTemperatures);
|
||||||
|
};
|
||||||
|
#endif
|
76
esp32/include/PlantCtrl.h
Normal file
76
esp32/include/PlantCtrl.h
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* @file PlantCtrl.h
|
||||||
|
* @author your name (you@domain.com)
|
||||||
|
* @brief Abstraction to handle the Sensors
|
||||||
|
* @version 0.1
|
||||||
|
* @date 2020-05-27
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef PLANT_CTRL_H
|
||||||
|
#define PLANT_CTRL_H
|
||||||
|
|
||||||
|
class Plant {
|
||||||
|
|
||||||
|
private:
|
||||||
|
int mPinSensor=0; /**< Pin of the moist sensor */
|
||||||
|
int mPinPump=0; /**< Pin of the pump */
|
||||||
|
|
||||||
|
int mValue = 0; /**< Value of the moist sensor */
|
||||||
|
|
||||||
|
int mAnalogValue=0; /**< moist sensor values, used for a calculation */
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Construct a new Plant object
|
||||||
|
*
|
||||||
|
* @param pinSensor Pin of the Sensor to use to measure moist
|
||||||
|
* @param pinPump Pin of the Pump to use
|
||||||
|
*/
|
||||||
|
Plant(int pinSensor, int pinPump);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add a value, to be measured
|
||||||
|
*
|
||||||
|
* @param analogValue
|
||||||
|
*/
|
||||||
|
void addSenseValue(int analogValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Calculate the value based on the information
|
||||||
|
* @see amountMeasurePoints
|
||||||
|
* Internal memory, used by addSenseValue will be resetted
|
||||||
|
* @return int analog value
|
||||||
|
*/
|
||||||
|
void calculateSensorValue(int amountMeasurePoints);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Sensor Pin of the analog measuring
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
int getSensorPin() { return mPinSensor; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the Pump Pin object
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
int getPumpPin() { return mPinPump; }
|
||||||
|
|
||||||
|
int getSensorValue() { return mValue; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if a plant is too dry and needs some water.
|
||||||
|
*
|
||||||
|
* @param boundary
|
||||||
|
* @return true
|
||||||
|
* @return false
|
||||||
|
*/
|
||||||
|
bool isPumpRequired(int boundary) { return (this->mValue < boundary); }
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
39
esp32/include/README
Normal file
39
esp32/include/README
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
This directory is intended for project header files.
|
||||||
|
|
||||||
|
A header file is a file containing C declarations and macro definitions
|
||||||
|
to be shared between several project source files. You request the use of a
|
||||||
|
header file in your project source file (C, C++, etc) located in `src` folder
|
||||||
|
by including it, with the C preprocessing directive `#include'.
|
||||||
|
|
||||||
|
```src/main.c
|
||||||
|
|
||||||
|
#include "header.h"
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Including a header file produces the same results as copying the header file
|
||||||
|
into each source file that needs it. Such copying would be time-consuming
|
||||||
|
and error-prone. With a header file, the related declarations appear
|
||||||
|
in only one place. If they need to be changed, they can be changed in one
|
||||||
|
place, and programs that include the header file will automatically use the
|
||||||
|
new version when next recompiled. The header file eliminates the labor of
|
||||||
|
finding and changing all the copies as well as the risk that a failure to
|
||||||
|
find one copy will result in inconsistencies within a program.
|
||||||
|
|
||||||
|
In C, the usual convention is to give header files names that end with `.h'.
|
||||||
|
It is most portable to use only letters, digits, dashes, and underscores in
|
||||||
|
header file names, and at most one dot.
|
||||||
|
|
||||||
|
Read more about using header files in official GCC documentation:
|
||||||
|
|
||||||
|
* Include Syntax
|
||||||
|
* Include Operation
|
||||||
|
* Once-Only Headers
|
||||||
|
* Computed Includes
|
||||||
|
|
||||||
|
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
46
esp32/lib/README
Normal file
46
esp32/lib/README
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
This directory is intended for project specific (private) libraries.
|
||||||
|
PlatformIO will compile them to static libraries and link into executable file.
|
||||||
|
|
||||||
|
The source code of each library should be placed in a an own separate directory
|
||||||
|
("lib/your_library_name/[here are source files]").
|
||||||
|
|
||||||
|
For example, see a structure of the following two libraries `Foo` and `Bar`:
|
||||||
|
|
||||||
|
|--lib
|
||||||
|
| |
|
||||||
|
| |--Bar
|
||||||
|
| | |--docs
|
||||||
|
| | |--examples
|
||||||
|
| | |--src
|
||||||
|
| | |- Bar.c
|
||||||
|
| | |- Bar.h
|
||||||
|
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||||
|
| |
|
||||||
|
| |--Foo
|
||||||
|
| | |- Foo.c
|
||||||
|
| | |- Foo.h
|
||||||
|
| |
|
||||||
|
| |- README --> THIS FILE
|
||||||
|
|
|
||||||
|
|- platformio.ini
|
||||||
|
|--src
|
||||||
|
|- main.c
|
||||||
|
|
||||||
|
and a contents of `src/main.c`:
|
||||||
|
```
|
||||||
|
#include <Foo.h>
|
||||||
|
#include <Bar.h>
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
PlatformIO Library Dependency Finder will find automatically dependent
|
||||||
|
libraries scanning project source files.
|
||||||
|
|
||||||
|
More information about PlatformIO Library Dependency Finder
|
||||||
|
- https://docs.platformio.org/page/librarymanager/ldf.html
|
21
esp32/platformio.ini
Normal file
21
esp32/platformio.ini
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[env:esp32doit-devkit-v1]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32doit-devkit-v1
|
||||||
|
framework = arduino
|
||||||
|
build_flags = -DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY
|
||||||
|
board_build.partitions = huge_app.csv
|
||||||
|
|
||||||
|
; the latest development brankitchen-lightch (convention V3.0.x)
|
||||||
|
lib_deps = https://github.com/homieiot/homie-esp8266.git#v3.0
|
||||||
|
OneWire
|
||||||
|
upload_port = /dev/ttyUSB0
|
105
esp32/src/DS18B20.cpp
Normal file
105
esp32/src/DS18B20.cpp
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* @file DS18B20.cpp
|
||||||
|
* @author your name (you@domain.com)
|
||||||
|
* @brief
|
||||||
|
* @version 0.1
|
||||||
|
* @date 2020-06-09
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "DS18B20.h"
|
||||||
|
|
||||||
|
#define STARTCONV 0x44
|
||||||
|
#define READSCRATCH 0xBE // Read EEPROM
|
||||||
|
#define TEMP_LSB 0
|
||||||
|
#define TEMP_MSB 1
|
||||||
|
#define SCRATCHPADSIZE 9
|
||||||
|
#define OFFSET_CRC8 8 /**< 9th byte has the CRC of the complete data */
|
||||||
|
|
||||||
|
//Printf debugging
|
||||||
|
//#define DS_DEBUG
|
||||||
|
|
||||||
|
int Ds18B20::readDevices() {
|
||||||
|
byte addr[8];
|
||||||
|
|
||||||
|
int amount = -1;
|
||||||
|
while (this->mDs->search(addr)) {
|
||||||
|
amount++;
|
||||||
|
}
|
||||||
|
this->mDs->reset_search();
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Ds18B20::readAllTemperatures(float* pTemperatures, int maxTemperatures) {
|
||||||
|
byte addr[8];
|
||||||
|
uint8_t scratchPad[SCRATCHPADSIZE];
|
||||||
|
int currentTemp = 0;
|
||||||
|
|
||||||
|
while (this->mDs->search(addr)) {
|
||||||
|
#ifdef DS_DEBUG
|
||||||
|
Serial.print(" ROM =");
|
||||||
|
for (i = 0; i < 8; i++) {
|
||||||
|
Serial.write(' ');
|
||||||
|
Serial.print(addr[i], HEX);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
this->mDs->reset();
|
||||||
|
this->mDs->select(addr);
|
||||||
|
this->mDs->write(STARTCONV);
|
||||||
|
this->mDs->reset();
|
||||||
|
this->mDs->select(addr);
|
||||||
|
this->mDs->write(READSCRATCH);
|
||||||
|
|
||||||
|
// Read all registers in a simple loop
|
||||||
|
// byte 0: temperature LSB
|
||||||
|
// byte 1: temperature MSB
|
||||||
|
// byte 2: high alarm temp
|
||||||
|
// byte 3: low alarm temp
|
||||||
|
// byte 4: DS18S20: store for crc
|
||||||
|
// DS18B20 & DS1822: configuration register
|
||||||
|
// byte 5: internal use & crc
|
||||||
|
// byte 6: DS18S20: COUNT_REMAIN
|
||||||
|
// DS18B20 & DS1822: store for crc
|
||||||
|
// byte 7: DS18S20: COUNT_PER_C
|
||||||
|
// DS18B20 & DS1822: store for crc
|
||||||
|
// byte 8: SCRATCHPAD_CRC
|
||||||
|
#ifdef DS_DEBUG
|
||||||
|
Serial.write("\r\nDATA:");
|
||||||
|
for (uint8_t i = 0; i < 9; i++) {
|
||||||
|
Serial.print(scratchPad[i], HEX);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
delay(50);
|
||||||
|
#endif
|
||||||
|
for (uint8_t i = 0; i < 9; i++) {
|
||||||
|
scratchPad[i] = this->mDs->read();
|
||||||
|
}
|
||||||
|
uint8_t crc8 = this->mDs->crc8(scratchPad, 8);
|
||||||
|
|
||||||
|
/* Only work an valid data */
|
||||||
|
if (crc8 == scratchPad[OFFSET_CRC8]) {
|
||||||
|
int16_t fpTemperature = (((int16_t) scratchPad[TEMP_MSB]) << 11)
|
||||||
|
| (((int16_t) scratchPad[TEMP_LSB]) << 3);
|
||||||
|
float celsius = (float) fpTemperature * 0.0078125;
|
||||||
|
#ifdef DS_DEBUG
|
||||||
|
Serial.printf("\r\nTemp%d %f °C (Raw: %d, %x =? %x)\r\n", (currentTemp+1), celsius, fpTemperature, crc8, scratchPad[8]);
|
||||||
|
#endif
|
||||||
|
/* check, if the buffer as some space for our data */
|
||||||
|
if (currentTemp < maxTemperatures) {
|
||||||
|
pTemperatures[currentTemp] = celsius;
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentTemp++;
|
||||||
|
}
|
||||||
|
this->mDs->reset();
|
||||||
|
#ifdef DS_DEBUG
|
||||||
|
Serial.println(" No more addresses.");
|
||||||
|
Serial.println();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return currentTemp;
|
||||||
|
}
|
27
esp32/src/PlantCtrl.cpp
Normal file
27
esp32/src/PlantCtrl.cpp
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* @file PlantCtrl.cpp
|
||||||
|
* @author your name (you@domain.com)
|
||||||
|
* @brief
|
||||||
|
* @version 0.1
|
||||||
|
* @date 2020-05-27
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020
|
||||||
|
*
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "PlantCtrl.h"
|
||||||
|
|
||||||
|
Plant::Plant(int pinSensor, int pinPump) {
|
||||||
|
this->mPinSensor = pinSensor;
|
||||||
|
this->mPinPump = pinPump;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plant::addSenseValue(int analog) {
|
||||||
|
this->mAnalogValue += analog;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plant::calculateSensorValue(int amountMeasurePoints) {
|
||||||
|
this->mValue = this->mAnalogValue / amountMeasurePoints;
|
||||||
|
this->mAnalogValue = 0;
|
||||||
|
}
|
703
esp32/src/main.cpp
Normal file
703
esp32/src/main.cpp
Normal file
@ -0,0 +1,703 @@
|
|||||||
|
/**
|
||||||
|
* @file main.cpp
|
||||||
|
* @author Ollo
|
||||||
|
* @brief PlantControl
|
||||||
|
* @version 0.1
|
||||||
|
* @date 2020-05-01
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#include "PlantCtrl.h"
|
||||||
|
#include "ControllerConfiguration.h"
|
||||||
|
#include "DS18B20.h"
|
||||||
|
#include <Homie.h>
|
||||||
|
|
||||||
|
const unsigned long TEMPREADCYCLE = 30000; /**< Check temperature all half minutes */
|
||||||
|
|
||||||
|
#define AMOUNT_SENOR_QUERYS 8
|
||||||
|
#define SENSOR_QUERY_SHIFTS 3
|
||||||
|
#define SOLAR4SENSORS 6.0f
|
||||||
|
#define TEMP_INIT_VALUE -999.0f
|
||||||
|
#define TEMP_MAX_VALUE 85.0f
|
||||||
|
|
||||||
|
bool mLoopInited = false;
|
||||||
|
bool mDeepSleep = false;
|
||||||
|
|
||||||
|
bool mPumpIsRunning=false;
|
||||||
|
|
||||||
|
int plantSensor1 = 0;
|
||||||
|
|
||||||
|
int lipoSenor = -1;
|
||||||
|
int lipoSensorValues = 0;
|
||||||
|
int solarSensor = -1;
|
||||||
|
int solarSensorValues = 0;
|
||||||
|
|
||||||
|
int mWaterAtEmptyLevel = 0;
|
||||||
|
#ifndef HC_SR04
|
||||||
|
int mWaterLow = 0;
|
||||||
|
#else
|
||||||
|
int mWaterGone = -1; /**< Amount of centimeter, where no water is seen */
|
||||||
|
#endif
|
||||||
|
int mOverflow = 0;
|
||||||
|
|
||||||
|
int readCounter = 0;
|
||||||
|
int mButtonClicks = 0;
|
||||||
|
|
||||||
|
|
||||||
|
#if (MAX_PLANTS >= 1)
|
||||||
|
HomieNode plant1("plant1", "Plant 1", "Plant");
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 2)
|
||||||
|
HomieNode plant2("plant2", "Plant 2", "Plant");
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 3)
|
||||||
|
HomieNode plant3("plant3", "Plant 3", "Plant");
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 4)
|
||||||
|
HomieNode plant4("plant4", "Plant 4", "Plant");
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 5)
|
||||||
|
HomieNode plant5("plant5", "Plant 5", "Plant");
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 6)
|
||||||
|
HomieNode plant6("plant6", "Plant 6", "Plant");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
HomieNode sensorLipo("lipo", "Battery Status", "Lipo");
|
||||||
|
HomieNode sensorSolar("solar", "Solar Status", "Solarpanel");
|
||||||
|
HomieNode sensorWater("water", "WaterSensor", "Water");
|
||||||
|
HomieNode sensorTemp("temperature", "Temperature", "temperature");
|
||||||
|
|
||||||
|
HomieSetting<long> deepSleepTime("deepsleep", "time in milliseconds to sleep (0 deactivats it)");
|
||||||
|
HomieSetting<long> deepSleepNightTime("nightsleep", "time in milliseconds to sleep (0 usese same setting: deepsleep at night, too)");
|
||||||
|
HomieSetting<long> wateringTime("watering", "time seconds the pump is running (60 is the default)");
|
||||||
|
HomieSetting<long> plantCnt("plants", "amout of plants to control (1 ... 6)");
|
||||||
|
|
||||||
|
#ifdef HC_SR04
|
||||||
|
HomieSetting<long> waterLevel("watermaxlevel", "Water maximum level in centimeter (50 cm default)");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#if (MAX_PLANTS >= 1)
|
||||||
|
HomieSetting<long> plant1SensorTrigger("moist1", "Moist1 sensor value, when pump activates");
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 2)
|
||||||
|
HomieSetting<long> plant2SensorTrigger("moist2", "Moist2 sensor value, when pump activates");
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 3)
|
||||||
|
HomieSetting<long> plant3SensorTrigger("moist3", "Moist3 sensor value, when pump activates");
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 4)
|
||||||
|
HomieSetting<long> plant4SensorTrigger("moist4", "Moist4 sensor value, when pump activates");
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 5)
|
||||||
|
HomieSetting<long> plant5SensorTrigger("moist5", "Moist5 sensor value, when pump activates");
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 6)
|
||||||
|
HomieSetting<long> plant6SensorTrigger("moist6", "Moist6 sensor value, when pump activates");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Ds18B20 dallas(SENSOR_DS18B20);
|
||||||
|
|
||||||
|
Plant mPlants[MAX_PLANTS] = {
|
||||||
|
#if (MAX_PLANTS >= 1)
|
||||||
|
Plant(SENSOR_PLANT1, OUTPUT_PUMP1),
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 2)
|
||||||
|
Plant(SENSOR_PLANT2, OUTPUT_PUMP2),
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 3)
|
||||||
|
Plant(SENSOR_PLANT3, OUTPUT_PUMP3),
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 4)
|
||||||
|
Plant(SENSOR_PLANT4, OUTPUT_PUMP4),
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 5)
|
||||||
|
Plant(SENSOR_PLANT5, OUTPUT_PUMP5),
|
||||||
|
#endif
|
||||||
|
#if (MAX_PLANTS >= 6)
|
||||||
|
Plant(SENSOR_PLANT6, OUTPUT_PUMP6)
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
void readAnalogValues() {
|
||||||
|
if (readCounter < AMOUNT_SENOR_QUERYS) {
|
||||||
|
lipoSensorValues += analogRead(SENSOR_LIPO);
|
||||||
|
solarSensorValues += analogRead(SENSOR_SOLAR);
|
||||||
|
readCounter++;
|
||||||
|
} else {
|
||||||
|
lipoSenor = (lipoSensorValues >> SENSOR_QUERY_SHIFTS);
|
||||||
|
lipoSensorValues = 0;
|
||||||
|
solarSensor = (solarSensorValues >> SENSOR_QUERY_SHIFTS);
|
||||||
|
solarSensorValues = 0;
|
||||||
|
|
||||||
|
readCounter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief cyclic Homie callback
|
||||||
|
* All logic, to be done by the controller cyclically
|
||||||
|
*/
|
||||||
|
void loopHandler() {
|
||||||
|
|
||||||
|
/* Move from Setup to this position, because the Settings are only here available */
|
||||||
|
if (!mLoopInited) {
|
||||||
|
// Configure Deep Sleep:
|
||||||
|
if (deepSleepTime.get()) {
|
||||||
|
Serial << "HOMIE | Setup sleeping for " << deepSleepTime.get() << " ms" << endl;
|
||||||
|
}
|
||||||
|
if (wateringTime.get()) {
|
||||||
|
Serial << "HOMIE | Setup watering for " << abs(wateringTime.get()) << " s" << endl;
|
||||||
|
}
|
||||||
|
/* Publish default values */
|
||||||
|
plant1.setProperty("switch").send(String("OFF"));
|
||||||
|
plant2.setProperty("switch").send(String("OFF"));
|
||||||
|
plant3.setProperty("switch").send(String("OFF"));
|
||||||
|
|
||||||
|
#if (MAX_PLANTS >= 4)
|
||||||
|
plant4.setProperty("switch").send(String("OFF"));
|
||||||
|
plant5.setProperty("switch").send(String("OFF"));
|
||||||
|
plant6.setProperty("switch").send(String("OFF"));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
for(int i=0; i < plantCnt.get(); i++) {
|
||||||
|
mPlants[i].calculateSensorValue(AMOUNT_SENOR_QUERYS);
|
||||||
|
int boundary4MoistSensor=-1;
|
||||||
|
switch (i)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
boundary4MoistSensor = plant1SensorTrigger.get();
|
||||||
|
plant1.setProperty("moist").send(String(100 * mPlants[i].getSensorValue() / 4095 ));
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
boundary4MoistSensor = plant2SensorTrigger.get();
|
||||||
|
plant2.setProperty("moist").send(String(100 * mPlants[i].getSensorValue() / 4095));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
boundary4MoistSensor = plant3SensorTrigger.get();
|
||||||
|
plant3.setProperty("moist").send(String(100 * mPlants[i].getSensorValue() / 4095));
|
||||||
|
break;
|
||||||
|
#if (MAX_PLANTS >= 4)
|
||||||
|
case 3:
|
||||||
|
boundary4MoistSensor = plant4SensorTrigger.get();
|
||||||
|
plant4.setProperty("moist").send(String(100 * mPlants[i].getSensorValue() / 4095));
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
boundary4MoistSensor = plant5SensorTrigger.get();
|
||||||
|
plant5.setProperty("moist").send(String(100 * mPlants[i].getSensorValue() / 4095));
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
boundary4MoistSensor = plant6SensorTrigger.get();
|
||||||
|
plant6.setProperty("moist").send(String(100 * mPlants[i].getSensorValue() / 4095));
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef HC_SR04
|
||||||
|
if (SOLAR_VOLT(solarSensor) > SOLAR4SENSORS) {
|
||||||
|
if (mWaterLow && mWaterAtEmptyLevel) {
|
||||||
|
sensorWater.setProperty("remaining").send("50");
|
||||||
|
} else if (!mWaterLow && mWaterAtEmptyLevel) {
|
||||||
|
sensorWater.setProperty("remaining").send("10");
|
||||||
|
} else if (!mWaterLow && !mWaterAtEmptyLevel) {
|
||||||
|
sensorWater.setProperty("remaining").send("0");
|
||||||
|
} else if (!mWaterLow && !mWaterAtEmptyLevel) {
|
||||||
|
sensorWater.setProperty("remaining").send("-1");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial << "Sun not strong enough for sensors (" << String(SOLAR_VOLT(solarSensor)) << "V )" << endl;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
mWaterAtEmptyLevel = (mWaterGone <= waterLevel.get());
|
||||||
|
int waterLevelPercent = (100 * mWaterGone) / waterLevel.get();
|
||||||
|
sensorWater.setProperty("remaining").send(String(waterLevelPercent));
|
||||||
|
Serial << "Water : " << mWaterGone << " cm (" << waterLevelPercent << "%)" << endl;
|
||||||
|
#endif
|
||||||
|
mPumpIsRunning=false;
|
||||||
|
/* Check if a plant needs water */
|
||||||
|
if (mPlants[i].isPumpRequired(boundary4MoistSensor) &&
|
||||||
|
(mWaterAtEmptyLevel) &&
|
||||||
|
(!mPumpIsRunning)) {
|
||||||
|
if (digitalRead(mPlants[i].getPumpPin()) == LOW) {
|
||||||
|
Serial << "Plant" << (i+1) << " needs water" << endl;
|
||||||
|
switch (i)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
plant1.setProperty("switch").send(String("ON"));
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
plant2.setProperty("switch").send(String("ON"));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
plant3.setProperty("switch").send(String("ON"));
|
||||||
|
break;
|
||||||
|
#if (MAX_PLANTS >= 4)
|
||||||
|
case 3:
|
||||||
|
plant4.setProperty("switch").send(String("ON"));
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
plant5.setProperty("switch").send(String("ON"));
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
plant6.setProperty("switch").send(String("ON"));
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
digitalWrite(mPlants[i].getPumpPin(), HIGH);
|
||||||
|
mPumpIsRunning=true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mLoopInited = true;
|
||||||
|
|
||||||
|
readAnalogValues();
|
||||||
|
|
||||||
|
if ((millis() % 1500) == 0) {
|
||||||
|
sensorLipo.setProperty("percent").send( String(100 * lipoSenor / 4095) );
|
||||||
|
sensorLipo.setProperty("volt").send( String(ADC_5V_TO_3V3(lipoSenor)) );
|
||||||
|
sensorSolar.setProperty("percent").send(String((100 * solarSensor ) / 4095));
|
||||||
|
sensorSolar.setProperty("volt").send( String(SOLAR_VOLT(solarSensor)) );
|
||||||
|
} else if ((millis() % 1000) == 0) {
|
||||||
|
float temp[2] = { TEMP_INIT_VALUE, TEMP_INIT_VALUE };
|
||||||
|
float* pFloat = temp;
|
||||||
|
int devices = dallas.readAllTemperatures(pFloat, 2);
|
||||||
|
if (devices < 2) {
|
||||||
|
if ((pFloat[0] > TEMP_INIT_VALUE) && (pFloat[0] < TEMP_MAX_VALUE) ) {
|
||||||
|
sensorTemp.setProperty("control").send( String(pFloat[0]));
|
||||||
|
}
|
||||||
|
} else if (devices >= 2) {
|
||||||
|
if ((pFloat[0] > TEMP_INIT_VALUE) && (pFloat[0] < TEMP_MAX_VALUE) ) {
|
||||||
|
sensorTemp.setProperty("temp").send( String(pFloat[0]));
|
||||||
|
}
|
||||||
|
if ((pFloat[1] > TEMP_INIT_VALUE) && (pFloat[1] < TEMP_MAX_VALUE) ) {
|
||||||
|
sensorTemp.setProperty("control").send( String(pFloat[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Loop functionality */
|
||||||
|
if ((!mPumpIsRunning) || (!mWaterAtEmptyLevel) ) {
|
||||||
|
/* let the ESP sleep qickly, as nothing must be done */
|
||||||
|
if ((millis() >= (MIN_TIME_RUNNING * MS_TO_S)) && (deepSleepTime.get() > 0)) {
|
||||||
|
mDeepSleep = true;
|
||||||
|
Serial << "No Water or Pump" << endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Always check, that after 5 minutes the device is sleeping */
|
||||||
|
/* Pump is running, go to sleep after defined time */
|
||||||
|
if ((millis() >= (((MIN_TIME_RUNNING + abs(wateringTime.get())) * MS_TO_S) + 5)) &&
|
||||||
|
(deepSleepTime.get() > 0)) {
|
||||||
|
Serial << "No sleeping activated (maximum)" << endl;
|
||||||
|
Serial << "Pump was running:" << mPumpIsRunning << "Water level is empty: " << mWaterAtEmptyLevel << endl;
|
||||||
|
mDeepSleep = true;
|
||||||
|
} else if ((millis() >= (((MIN_TIME_RUNNING + abs(wateringTime.get())) * MS_TO_S) + 0)) &&
|
||||||
|
(deepSleepTime.get() > 0)) {
|
||||||
|
Serial << "Maximum time reached: " << endl;
|
||||||
|
Serial << (mPumpIsRunning ? "Pump was running " : "No Pump ") << (mWaterAtEmptyLevel ? "Water level is empty" : "Water available") << endl;
|
||||||
|
mDeepSleep = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool switchGeneralPumpHandler(const int pump, const HomieRange& range, const String& value) {
|
||||||
|
if (range.isRange) return false; // only one switch is present
|
||||||
|
switch (pump)
|
||||||
|
{
|
||||||
|
#if MAX_PLANTS >= 1
|
||||||
|
case 0:
|
||||||
|
#endif
|
||||||
|
#if MAX_PLANTS >= 2
|
||||||
|
case 1:
|
||||||
|
#endif
|
||||||
|
#if MAX_PLANTS >= 3
|
||||||
|
#endif
|
||||||
|
case 2:
|
||||||
|
#if MAX_PLANTS >= 4
|
||||||
|
case 3:
|
||||||
|
#endif
|
||||||
|
#if MAX_PLANTS >= 5
|
||||||
|
case 4:
|
||||||
|
#endif
|
||||||
|
#if MAX_PLANTS >= 6
|
||||||
|
case 5:
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if ((value.equals("ON")) || (value.equals("On")) || (value.equals("on")) || (value.equals("true"))) {
|
||||||
|
digitalWrite(mPlants[pump].getPumpPin(), HIGH);
|
||||||
|
return true;
|
||||||
|
} else if ((value.equals("OFF")) || (value.equals("Off")) || (value.equals("off")) || (value.equals("false")) ) {
|
||||||
|
digitalWrite(mPlants[pump].getPumpPin(), LOW);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle Mqtt commands for the pumpe, responsible for the first plant
|
||||||
|
*
|
||||||
|
* @param range multiple transmitted values (not used for this function)
|
||||||
|
* @param value single value
|
||||||
|
* @return true when the command was parsed and executed succuessfully
|
||||||
|
* @return false on errors when parsing the request
|
||||||
|
*/
|
||||||
|
bool switch1Handler(const HomieRange& range, const String& value) {
|
||||||
|
return switchGeneralPumpHandler(0, range, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle Mqtt commands for the pumpe, responsible for the second plant
|
||||||
|
*
|
||||||
|
* @param range multiple transmitted values (not used for this function)
|
||||||
|
* @param value single value
|
||||||
|
* @return true when the command was parsed and executed succuessfully
|
||||||
|
* @return false on errors when parsing the request
|
||||||
|
*/
|
||||||
|
bool switch2Handler(const HomieRange& range, const String& value) {
|
||||||
|
return switchGeneralPumpHandler(1, range, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle Mqtt commands for the pumpe, responsible for the third plant
|
||||||
|
*
|
||||||
|
* @param range multiple transmitted values (not used for this function)
|
||||||
|
* @param value single value
|
||||||
|
* @return true when the command was parsed and executed succuessfully
|
||||||
|
* @return false on errors when parsing the request
|
||||||
|
*/
|
||||||
|
bool switch3Handler(const HomieRange& range, const String& value) {
|
||||||
|
return switchGeneralPumpHandler(2, range, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sensors, that are connected to GPIOs, mandatory for WIFI.
|
||||||
|
* These sensors (ADC2) can only be read when no Wifi is used.
|
||||||
|
*/
|
||||||
|
void readSensors() {
|
||||||
|
/* activate all sensors */
|
||||||
|
pinMode(OUTPUT_SENSOR, OUTPUT);
|
||||||
|
digitalWrite(OUTPUT_SENSOR, HIGH);
|
||||||
|
/* Use Pump 4 to activate and deactivate the Sensors */
|
||||||
|
#if (MAX_PLANTS < 4)
|
||||||
|
pinMode(OUTPUT_PUMP4, OUTPUT);
|
||||||
|
digitalWrite(OUTPUT_PUMP4, HIGH);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
delay(100);
|
||||||
|
/* wait before reading something */
|
||||||
|
for (int readCnt=0;readCnt < AMOUNT_SENOR_QUERYS; readCnt++) {
|
||||||
|
for(int i=0; i < MAX_PLANTS; i++) {
|
||||||
|
mPlants[i].addSenseValue(analogRead(mPlants[i].getSensorPin()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef HC_SR04
|
||||||
|
mWaterAtEmptyLevel = digitalRead(INPUT_WATER_EMPTY);
|
||||||
|
mWaterLow = digitalRead(INPUT_WATER_LOW);
|
||||||
|
mOverflow = digitalRead(INPUT_WATER_OVERFLOW);
|
||||||
|
#else
|
||||||
|
/* Use the Ultrasonic sensor to measure waterLevel */
|
||||||
|
|
||||||
|
/* deactivate all sensors and measure the pulse */
|
||||||
|
digitalWrite(INPUT_WATER_EMPTY, LOW);
|
||||||
|
delayMicroseconds(2);
|
||||||
|
digitalWrite(INPUT_WATER_EMPTY, HIGH);
|
||||||
|
delayMicroseconds(10);
|
||||||
|
digitalWrite(INPUT_WATER_EMPTY, LOW);
|
||||||
|
float duration = pulseIn(INPUT_WATER_LOW, HIGH);
|
||||||
|
float distance = (duration*.0343)/2;
|
||||||
|
mWaterGone = (int) distance;
|
||||||
|
Serial << "HC_SR04 | Distance : " << String(distance) << " cm" << endl;
|
||||||
|
#endif
|
||||||
|
/* deactivate the sensors */
|
||||||
|
digitalWrite(OUTPUT_SENSOR, LOW);
|
||||||
|
#if (MAX_PLANTS < 4)
|
||||||
|
digitalWrite(OUTPUT_PUMP4, LOW);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Startup function
|
||||||
|
* Is called once, the controller is started
|
||||||
|
*/
|
||||||
|
void setup() {
|
||||||
|
/* Required to read the temperature once */
|
||||||
|
float temp[2] = {0, 0};
|
||||||
|
float* pFloat = temp;
|
||||||
|
|
||||||
|
/* read button */
|
||||||
|
pinMode(BUTTON, INPUT);
|
||||||
|
/* Prepare Water sensors */
|
||||||
|
pinMode(INPUT_WATER_EMPTY, INPUT);
|
||||||
|
pinMode(INPUT_WATER_LOW, INPUT);
|
||||||
|
pinMode(INPUT_WATER_OVERFLOW, INPUT);
|
||||||
|
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.setTimeout(1000); // Set timeout of 1 second
|
||||||
|
Serial << endl << endl;
|
||||||
|
Serial << "Read analog sensors..." << endl;
|
||||||
|
/* Disable Wifi and bluetooth */
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
/* now ADC2 can be used */
|
||||||
|
readSensors();
|
||||||
|
/* activate Wifi again */
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
|
|
||||||
|
Homie_setFirmware("PlantControl", FIRMWARE_VERSION);
|
||||||
|
Homie.setLoopFunction(loopHandler);
|
||||||
|
|
||||||
|
// Load the settings
|
||||||
|
deepSleepTime.setDefaultValue(0);
|
||||||
|
deepSleepNightTime.setDefaultValue(0);
|
||||||
|
wateringTime.setDefaultValue(60);
|
||||||
|
plantCnt.setDefaultValue(0).setValidator([] (long candidate) {
|
||||||
|
return ((candidate >= 0) && (candidate <= 6) );
|
||||||
|
});
|
||||||
|
plant1SensorTrigger.setDefaultValue(0);
|
||||||
|
plant2SensorTrigger.setDefaultValue(0);
|
||||||
|
plant3SensorTrigger.setDefaultValue(0);
|
||||||
|
#if (MAX_PLANTS >= 4)
|
||||||
|
plant4SensorTrigger.setDefaultValue(0);
|
||||||
|
plant5SensorTrigger.setDefaultValue(0);
|
||||||
|
plant6SensorTrigger.setDefaultValue(0);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HC_SR04
|
||||||
|
waterLevel.setDefaultValue(50);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Advertise topics
|
||||||
|
plant1.advertise("switch").setName("Pump 1")
|
||||||
|
.setDatatype("boolean")
|
||||||
|
.settable(switch1Handler);
|
||||||
|
plant1.advertise("moist").setName("Percent")
|
||||||
|
.setDatatype("number")
|
||||||
|
.setUnit("%");
|
||||||
|
plant2.advertise("switch").setName("Pump 2")
|
||||||
|
.setDatatype("boolean")
|
||||||
|
.settable(switch2Handler);
|
||||||
|
plant2.advertise("moist").setName("Percent")
|
||||||
|
.setDatatype("number")
|
||||||
|
.setUnit("%");
|
||||||
|
plant3.advertise("switch").setName("Pump 3")
|
||||||
|
.setDatatype("boolean")
|
||||||
|
.settable(switch3Handler);
|
||||||
|
plant3.advertise("moist").setName("Percent")
|
||||||
|
.setDatatype("number")
|
||||||
|
.setUnit("%");
|
||||||
|
#if (MAX_PLANTS >= 4)
|
||||||
|
plant4.advertise("moist").setName("Percent")
|
||||||
|
.setDatatype("number")
|
||||||
|
.setUnit("%");
|
||||||
|
plant5.advertise("moist").setName("Percent")
|
||||||
|
.setDatatype("number")
|
||||||
|
.setUnit("%");
|
||||||
|
plant6.advertise("moist").setName("Percent")
|
||||||
|
.setDatatype("number")
|
||||||
|
.setUnit("%");
|
||||||
|
#endif
|
||||||
|
sensorTemp.advertise("control")
|
||||||
|
.setName("Temperature")
|
||||||
|
.setDatatype("number")
|
||||||
|
.setUnit("°C");
|
||||||
|
sensorTemp.advertise("temp")
|
||||||
|
.setName("Temperature")
|
||||||
|
.setDatatype("number")
|
||||||
|
.setUnit("°C");
|
||||||
|
|
||||||
|
sensorLipo.advertise("percent")
|
||||||
|
.setName("Percent")
|
||||||
|
.setDatatype("number")
|
||||||
|
.setUnit("%");
|
||||||
|
sensorLipo.advertise("volt")
|
||||||
|
.setName("Volt")
|
||||||
|
.setDatatype("number")
|
||||||
|
.setUnit("V");
|
||||||
|
|
||||||
|
sensorSolar.advertise("percent")
|
||||||
|
.setName("Percent")
|
||||||
|
.setDatatype("number")
|
||||||
|
.setUnit("%");
|
||||||
|
sensorSolar.advertise("volt")
|
||||||
|
.setName("Volt")
|
||||||
|
.setDatatype("number")
|
||||||
|
.setUnit("V");
|
||||||
|
sensorWater.advertise("remaining").setDatatype("number").setUnit("%");
|
||||||
|
|
||||||
|
Homie.setup();
|
||||||
|
|
||||||
|
/* Intialize inputs and outputs */
|
||||||
|
for(int i=0; i < plantCnt.get(); i++) {
|
||||||
|
pinMode(mPlants[i].getPumpPin(), OUTPUT);
|
||||||
|
pinMode(mPlants[i].getSensorPin(), ANALOG);
|
||||||
|
digitalWrite(mPlants[i].getPumpPin(), LOW);
|
||||||
|
}
|
||||||
|
/* Setup Solar and Lipo measurement */
|
||||||
|
pinMode(SENSOR_LIPO, ANALOG);
|
||||||
|
pinMode(SENSOR_SOLAR, ANALOG);
|
||||||
|
/* Read analog values at the start */
|
||||||
|
do {
|
||||||
|
readAnalogValues();
|
||||||
|
} while (readCounter != 0);
|
||||||
|
|
||||||
|
|
||||||
|
// Configure Deep Sleep:
|
||||||
|
if ((deepSleepNightTime.get() > 0) &&
|
||||||
|
( SOLAR_VOLT(solarSensor) < MINIMUM_SOLAR_VOLT)) {
|
||||||
|
Serial << "HOMIE | Setup sleeping for " << deepSleepNightTime.get() << " ms as sun is at " << SOLAR_VOLT(solarSensor) << "V" << endl;
|
||||||
|
uint64_t usSleepTime = deepSleepNightTime.get() * 1000U;
|
||||||
|
esp_sleep_enable_timer_wakeup(usSleepTime);
|
||||||
|
}else if (deepSleepTime.get()) {
|
||||||
|
Serial << "HOMIE | Setup sleeping for " << deepSleepTime.get() << " ms" << endl;
|
||||||
|
uint64_t usSleepTime = deepSleepTime.get() * 1000U;
|
||||||
|
esp_sleep_enable_timer_wakeup(usSleepTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( (ADC_5V_TO_3V3(lipoSenor) < MINIMUM_LIPO_VOLT) && (deepSleepTime.get()) ) {
|
||||||
|
long sleepEmptyLipo = (deepSleepTime.get() * EMPTY_LIPO_MULTIPL);
|
||||||
|
Serial << "HOMIE | Change sleeping to " << sleepEmptyLipo << " ms as lipo is at " << ADC_5V_TO_3V3(lipoSenor) << "V" << endl;
|
||||||
|
esp_sleep_enable_timer_wakeup(sleepEmptyLipo * 1000U);
|
||||||
|
mDeepSleep = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read the temperature sensors once, as first time 85 degree is returned */
|
||||||
|
Serial << "DS18B20 | sensors: " << String(dallas.readDevices()) << endl;
|
||||||
|
delay(200);
|
||||||
|
if (dallas.readAllTemperatures(pFloat, 2) > 0) {
|
||||||
|
Serial << "DS18B20 | Temperature 1: " << String(temp[0]) << endl;
|
||||||
|
Serial << "DS18B20 | Temperature 2: " << String(temp[1]) << endl;
|
||||||
|
}
|
||||||
|
delay(200);
|
||||||
|
if (dallas.readAllTemperatures(pFloat, 2) > 0) {
|
||||||
|
Serial << "Temperature 1: " << String(temp[0]) << endl;
|
||||||
|
Serial << "Temperature 2: " << String(temp[1]) << endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Cyclic call
|
||||||
|
* Executs the Homie base functionallity or triggers sleeping, if requested.
|
||||||
|
*/
|
||||||
|
void loop() {
|
||||||
|
if (!mDeepSleep) {
|
||||||
|
if (Serial.available() > 0) {
|
||||||
|
// read the incoming byte:
|
||||||
|
int incomingByte = Serial.read();
|
||||||
|
|
||||||
|
switch ((char) incomingByte)
|
||||||
|
{
|
||||||
|
case 'P':
|
||||||
|
Serial << "Activate Sensor OUTPUT " << endl;
|
||||||
|
pinMode(OUTPUT_SENSOR, OUTPUT);
|
||||||
|
digitalWrite(OUTPUT_SENSOR, HIGH);
|
||||||
|
break;
|
||||||
|
case 'p':
|
||||||
|
Serial << "Deactivate Sensor OUTPUT " << endl;
|
||||||
|
pinMode(OUTPUT_SENSOR, OUTPUT);
|
||||||
|
digitalWrite(OUTPUT_SENSOR, LOW);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((digitalRead(BUTTON) == LOW) && (mButtonClicks % 2) == 0) {
|
||||||
|
float temp[2] = {0, 0};
|
||||||
|
float* pFloat = temp;
|
||||||
|
mButtonClicks++;
|
||||||
|
|
||||||
|
Serial << "SELF TEST (clicks: " << String(mButtonClicks) << ")" << endl;
|
||||||
|
|
||||||
|
Serial << "DS18B20 sensors: " << String(dallas.readDevices()) << endl;
|
||||||
|
delay(200);
|
||||||
|
if (dallas.readAllTemperatures(pFloat, 2) > 0) {
|
||||||
|
Serial << "Temperature 1: " << String(temp[0]) << endl;
|
||||||
|
Serial << "Temperature 2: " << String(temp[1]) << endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(mButtonClicks) {
|
||||||
|
case 1:
|
||||||
|
case 3:
|
||||||
|
case 5:
|
||||||
|
if (mButtonClicks > 1) {
|
||||||
|
Serial << "Read analog sensors..." << endl;
|
||||||
|
/* Disable Wifi and bluetooth */
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
delay(50);
|
||||||
|
/* now ADC2 can be used */
|
||||||
|
readSensors();
|
||||||
|
}
|
||||||
|
#ifndef HC_SR04
|
||||||
|
Serial << "Water Low: " << String(mWaterLow) << endl;
|
||||||
|
Serial << "Water Empty: " << String(mWaterAtEmptyLevel) << endl;
|
||||||
|
Serial << "Water Overflow: " << String(mOverflow) << endl;
|
||||||
|
#else
|
||||||
|
Serial << "Water gone: " << String(mWaterGone) << " cm" << endl;
|
||||||
|
#endif
|
||||||
|
for(int i=0; i < MAX_PLANTS; i++) {
|
||||||
|
mPlants[i].calculateSensorValue(AMOUNT_SENOR_QUERYS);
|
||||||
|
|
||||||
|
Serial << "Moist Sensor " << (i+1) << ": " << String(mPlants[i].getSensorValue()) << " Volt: " << String(ADC_5V_TO_3V3(mPlants[i].getSensorValue())) << endl;
|
||||||
|
}
|
||||||
|
/* Read enough values */
|
||||||
|
do {
|
||||||
|
readAnalogValues();
|
||||||
|
Serial << "Read Analog (" << String(readCounter) << ")" << endl;;
|
||||||
|
} while (readCounter != 0);
|
||||||
|
|
||||||
|
Serial << "Lipo Sensor - Raw: " << String(lipoSenor) << " Volt: " << String(ADC_5V_TO_3V3(lipoSenor)) << endl;
|
||||||
|
Serial << "Solar Sensor - Raw: " << String(solarSensor) << " Volt: " << String(SOLAR_VOLT(solarSensor)) << endl;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
Serial << "Activate Sensor OUTPUT " << endl;
|
||||||
|
pinMode(OUTPUT_SENSOR, OUTPUT);
|
||||||
|
digitalWrite(OUTPUT_SENSOR, HIGH);
|
||||||
|
break;
|
||||||
|
case 9:
|
||||||
|
Serial << "Activate Pump1 GPIO" << String(mPlants[0].getPumpPin()) << endl;
|
||||||
|
digitalWrite(mPlants[0].getPumpPin(), HIGH);
|
||||||
|
break;
|
||||||
|
case 11:
|
||||||
|
Serial << "Activate Pump2 GPIO" << String(mPlants[1].getPumpPin()) << endl;
|
||||||
|
digitalWrite(mPlants[1].getPumpPin(), HIGH);
|
||||||
|
break;
|
||||||
|
case 13:
|
||||||
|
Serial << "Activate Pump3 GPIO" << String(mPlants[2].getPumpPin()) << endl;
|
||||||
|
digitalWrite(mPlants[2].getPumpPin(), HIGH);
|
||||||
|
break;
|
||||||
|
case 15:
|
||||||
|
Serial << "Activate Pump4/Sensor GPIO" << String(OUTPUT_PUMP4) << endl;
|
||||||
|
digitalWrite(OUTPUT_PUMP4, HIGH);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Serial << "No further tests! Please reboot" << endl;
|
||||||
|
}
|
||||||
|
Serial.flush();
|
||||||
|
}else if (mButtonClicks > 0 && (digitalRead(BUTTON) == HIGH) && (mButtonClicks % 2) == 1) {
|
||||||
|
Serial << "Self Test Ended" << endl;
|
||||||
|
mButtonClicks++;
|
||||||
|
/* Always reset all outputs */
|
||||||
|
digitalWrite(OUTPUT_SENSOR, LOW);
|
||||||
|
for(int i=0; i < MAX_PLANTS; i++) {
|
||||||
|
digitalWrite(mPlants[i].getPumpPin(), LOW);
|
||||||
|
}
|
||||||
|
digitalWrite(OUTPUT_PUMP4, LOW);
|
||||||
|
} else if (mButtonClicks == 0) {
|
||||||
|
Homie.loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Serial << (millis()/ 1000) << "s running; sleeeping ..." << endl;
|
||||||
|
Serial.flush();
|
||||||
|
esp_deep_sleep_start();
|
||||||
|
}
|
||||||
|
}
|
11
esp32/test/README
Normal file
11
esp32/test/README
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
This directory is intended for PIO Unit Testing and project tests.
|
||||||
|
|
||||||
|
Unit Testing is a software testing method by which individual units of
|
||||||
|
source code, sets of one or more MCU program modules together with associated
|
||||||
|
control data, usage procedures, and operating procedures, are tested to
|
||||||
|
determine whether they are fit for use. Unit testing finds problems early
|
||||||
|
in the development cycle.
|
||||||
|
|
||||||
|
More information about PIO Unit Testing:
|
||||||
|
- https://docs.platformio.org/page/plus/unit-testing.html
|
Loading…
Reference in New Issue
Block a user