ESP32 based project

This commit is contained in:
Ollo 2020-09-07 18:18:46 +02:00
parent b6d3f96239
commit c8ebe2a6bc
30 changed files with 41356 additions and 0 deletions

35
board/.gitignore vendored Normal file
View 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)

File diff suppressed because it is too large Load Diff

281
board/PlantCtrlESP32.pro Normal file
View 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

File diff suppressed because it is too large Load Diff

1274
board/PlantCtrlESP32.sch-bak Normal file

File diff suppressed because it is too large Load Diff

11
board/ReadMe.md Normal file
View 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
View File

@ -0,0 +1 @@
0

5
board/gerber/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
*.gbr
*.drl
*.ngc
*.png
*.bakT*

File diff suppressed because it is too large Load Diff

39
board/gerber/ReadMe.md Normal file
View 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
View 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

View 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
View 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
View 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
View File

@ -0,0 +1 @@
config.json

BIN
esp32/homie/ui_bundle.gz Normal file

Binary file not shown.

63
esp32/host/Readme.md Normal file
View 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

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

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