Skip to content

Commit

Permalink
WIP OTA and AP-based configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
GerardKrupa-Cap authored and GJKrupa committed Jul 9, 2022
1 parent 32248c0 commit a8a0c99
Show file tree
Hide file tree
Showing 43 changed files with 1,297 additions and 225 deletions.
45 changes: 45 additions & 0 deletions .drone.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
kind: pipeline
type: docker
name: branch-build

steps:
- name: build-pio
image: uxian/pio-esp32
commands:
- pio run
when:
event:
- pull_request

- name: docker-build
image: docker:dind
volumes:
- name: dockersock
path: /var/run
commands:
- sleep 5 # give docker enough time to start
- docker buildx ls
- docker buildx create --use --name my-builder
- docker buildx build --platform linux/amd64,linux/arm64 -t test:latest .
when:
event:
- pull_request

services:
- name: dind
image: docker:dind
privileged: true
volumes:
- name: dockersock
path: /var/run

volumes:
- name: dockersock
temp: {}

trigger:
ref:
include:
- refs/pull/**
- refs/tags/**
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM python:3-slim AS stage

COPY server/* /app/
WORKDIR /app

RUN python3 -m venv /opt/venv
RUN /opt/venv/bin/pip install -r requirements.txt
RUN /opt/venv/bin/pip list -v

FROM python:3-slim

ARG VERSION_NUMBER

COPY --from=stage /app /app
COPY --from=stage /opt/venv /opt/venv
COPY .pio/build/lolin_d32/firmware.bin /assets/firmware-${VERSION_NUMBER}.bin
RUN echo "${VERSION_NUMBER}" > /.version

ENV PYTHONPATH /piplib

WORKDIR /app

ENTRYPOINT ["/opt/venv/bin/python"]
CMD ["main.py"]
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Environment Monitor with Logging to MQTT

This device measures environmental conditions such as temperature, humidity, relative pressure and rainfall and reports that data to InfluxDB via the Telegraf endpoint when deployed as part of the TICK stack (https://www.influxdata.com/time-series-platform/). It also includes a battery voltage monitor to enable alerting when the device needs to be recharged. The current timestamp is retrieved via NTP each time the board wakes from deep sleep mode.
This device measures environmental conditions such as temperature, humidity, relative pressure and rainfall and reports that data to MQTT topics based on the location. The data pushed only includes the reading (no timestamp) but should be suitable for consumption
by systems such as Home Assistant.

## MQTT deployment

Expand Down Expand Up @@ -35,4 +36,19 @@ I'll leave that as an exercise for the user since I'm not 100% happy with the as

![Assembled Outdoor Sensor](docs/outdoor-version.jpg)

Since the rain sensor needs to be outside the box I had to drill a few holes for the standoffs and cabling which I sealed (hopefully) using Sugru.
Since the rain sensor needs to be outside the box I had to drill a few holes for the standoffs and cabling which I sealed (hopefully) using Sugru.

## Setup

On first launch the device will create a WiFi hotspot called sensor-XXXXXX (where XXXXXX is the MAC address of the device). Connect
a laptop or phone to this hotspot to use the captive portal to configure your WiFi, MQTT sever and (optionally) syslog server and/or firmware server.

If the system detects a new firmware has been installed that requires additional configuration it will automatically switch back into configuration mode with an AP named sensor-LOCATION instead.

To force the device back into configuration mode, press the reboot button and the press the BOOT button twice in quick succession.

## Firmware Server

This software uses the esp32FOTA library to detect firmware updates. This library expects a JSON descriptor that identifies the current firmware version and provides a link to the firmware's binary image. I have provided a Dockerfile and Python script that will provide such a server. To run it needs an environment BASE_URL to indicate where it is being server from. This is because the esp32FOTA library does not support relative URLs.

I've configure esp32FOTA to support insecure HTTPS without validating certificates since I'll be serving this from a closed environment on a LAN. You may want to add your own code for certificate validation if you want to improve security.
7 changes: 7 additions & 0 deletions assets/bootstrap.min.css

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions assets/bootstrap.min.js

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions assets/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />

<link rel="stylesheet" href="bootstrap.min.css" />
<title>Configure Environment Sensor</title>
</head>
<body>
<div class="container">
<div class="alert alert-%STATUS%" role="alert">
%MESSAGE%
</div>

<h2>Configure Environment Sensor</h2>

<hr />

<h3>General Configuration</h3>

<form action="/configure" method="POST">
<div class="form-group row">
<label for="name" class="col-sm-2">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="name" name="name" aria-describedby="nameHelp" value="%NAME%" />
<small id="nameHelp" class="form-text text-muted">This should be a single word with no spaces or punctuation. It will be used to define the MQTT topic as environment/<i>name</i>/<i>gauge</i></small>
</div>
</div>

<hr />

<h3>Network</h3>

<div class="form-group row">
<label for="ssid" class="col-sm-2">SSID</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="ssid" name="ssid" aria-describedby="ssidHelp" value="%SSID%" />
<small id="ssidHelp" class="form-text text-muted">SSID of your WiFi (must support 2.4GHz)</small>
</div>
</div>

<div class="form-group row">
<label for="passphrase" class="col-sm-2">Passphrase</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="passphrase" name="passphrase" aria-describedby="passphraseHelp" />
<small id="passphraseHelp" class="form-text text-muted">Passphrase of your WiFi access point</small>
</div>
</div>

<div class="form-group row">
<label for="mqttHost" class="col-sm-2">MQTT Hostname</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="mqttHost" name="mqttHost" value="%MQTT_HOST%" />
</div>
<label for="mqttPort" class="col-sm-1">Port</label>
<div class="col-sm-2">
<input type="number" class="form-control" id="mqttPort" name="mqttPort" value="%MQTT_PORT%" />
</div>
</div>

<div class="form-group row">
<label for="updateUrl" class="col-sm-2">Firmware Version URL</label>
<div class="col-sm-10">
<input type="url" class="form-control" id="updateUrl" name="updateUrl" aria-describedby="updateUrlHelp" value="%UPDATE_URL%" />
<small id="updateUrlHelp" class="form-text text-muted">This should point to version.json</small>
</div>
</div>

<div class="form-group row">
<label for="syslogHost" class="col-sm-2">syslog Hostname</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="syslogHost" name="syslogHost" value="%SYSLOG_HOST%" />
</div>
<label for="syslogPort" class="col-sm-1">Port</label>
<div class="col-sm-2">
<input type="number" class="form-control" id="syslogPort" name="syslogPort" value="%SYSLOG_PORT%" />
</div>
</div>

<hr />

<h3>Features</h3>

<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="rainSensor" name="rainSensor" %RAIN_SENSOR%>
<label class="form-check-label" for="rainSensor">
Rain Sensor
</label>
</div>

<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="batterySensor" name="batterySensor" %BATTERY_SENSOR%>
<label class="form-check-label" for="batterySensor">
Battery Sensor
</label>
</div>

<hr />

<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>

<script src="bootstrap.min.js"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions assets/success.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Success</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<h3>Success</h3>
<p>The configuration process is complete. The device will now reboot and you may close this window.</p>
</body>
</html>
13 changes: 13 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
REPO=${1:-}
TAG=${2:-}

export PLATFORMIO_BUILD_FLAGS="'-DVERSION_NUMBER=\"${TAG}\"'"
pio run

docker build \
--progress=plain \
--build-arg VERSION_NUMBER="${TAG}" \
-t "${REPO}:${TAG}" \
.
8 changes: 8 additions & 0 deletions partitions.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x009000, 0x4000,
otadata, data, ota, 0x00d000, 0x2000,
phy_init, data, phy, 0x00f000, 0x1000,
ota_0, app, ota_0, 0x010000, 0x180000,
ota_1, app, ota_1, 0x190000, 0x180000,
spiffs, data, spiffs, 0x310000, 0xf0000,
30 changes: 22 additions & 8 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,33 @@
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[platformio]
extra_configs = secret_config.ini

[env:lolin_d32]
platform = espressif32
board = lolin32
framework = arduino
board_build.partitions = partitions.csv

lib_deps =
adafruit/Adafruit BME280 Library @ ^2.2.2
ottowinter/AsyncMqttClient-esphome @ ^0.8.6
rpolitex/ArduinoNvs @ ^2.5
ottowinter/ESPAsyncWebServer-esphome @ ^2.1.0
arcao/Syslog @ ^2.0.0
chrisjoyce911/esp32FOTA @ ^0.1.6

board_build.embed_txtfiles =
assets/bootstrap.min.css
assets/bootstrap.min.js
assets/index.html
assets/success.html

build_flags =
-std=gnu++17

build_unflags =
-std=gnu++11

upload_port = /dev/cu.usbserial-0001
upload_speed = 921600
monitor_port = /dev/cu.usbserial-0001
monitor_speed = 115200
lib_deps =
adafruit/Adafruit BME280 Library @ ^2.2.2
arduino-libraries/NTPClient @ ^3.2.1
ottowinter/AsyncMqttClient-esphome @ ^0.8.6
arcao/Syslog @ ^2.0.0
13 changes: 0 additions & 13 deletions secret_config.ini.template

This file was deleted.

35 changes: 35 additions & 0 deletions server/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from flask import Flask
from flask import send_from_directory
from flask import request
import os

url_override = os.environ.get('BASE_URL')
if (url_override is not None and not url_override.endswith('/')):
url_override = url_override + '/'

app = Flask(__name__)
global version
with open("/.version", "r") as f:
version = f.readline().rstrip()

@app.route("/version.json")
def version_json():
global version
base_url = ""

if url_override is None:
base_url = request.host_url
else:
base_url = url_override

return {
"type": "home-sensor",
"version": version,
"url": base_url + "fw/firmware-" + version + ".bin"
}

@app.route('/fw/<path:path>')
def firmware(path):
return send_from_directory('/assets', path)

app.run(host='0.0.0.0', port=80)
1 change: 1 addition & 0 deletions server/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
flask >= 2.1.2
Loading

0 comments on commit a8a0c99

Please sign in to comment.