455 lines
14 KiB
C++
455 lines
14 KiB
C++
// SPDX-License-Identifier: CC0-1.0
|
|
// Copyright © Fabrice Bellamy 2026
|
|
|
|
/*
|
|
* The Bib O'Tron project
|
|
* A proof of concept for replacing the raspberry pi that is managing the bib button by an ESP32 microcontroller.
|
|
*
|
|
* license : Creative Commons Zero v1.0 Universal
|
|
* original source code and documentation : https://git.interhacker.space/12b/bibotron
|
|
*/
|
|
|
|
// Include some libraries
|
|
#include <FastLED.h>
|
|
#include <Arduino.h>
|
|
//#include <WiFi.h>
|
|
#include <SPI.h>
|
|
#include <EthernetESP32.h>
|
|
#include <WebServer.h>
|
|
#include <ESPmDNS.h>
|
|
#include <ESPping.h>
|
|
#include <ArduinoJson.h>
|
|
|
|
// You may need to change the below line in libraries/ElegantOTA/src/ElegantOTA.h for the ElegantOTA library to be built with the correct web server type. If you know a better way of doing this feel free to send a pull request ;-)
|
|
// #define ELEGANTOTA_USE_ASYNC_WEBSERVER O
|
|
#include <ElegantOTA.h>
|
|
|
|
// Include a non-versioned file that containes the web OTA update credentials (you have to create this file by copying the example one if you do not have it)
|
|
#include "secrets.h"
|
|
|
|
#if !( defined(ESP32) )
|
|
#error This code is designed for ESP32_S3 + W5500! Please check your Tools->Board setting. (see the file README.md for the settings to use)
|
|
#endif
|
|
|
|
// LED GPIOs
|
|
#define LEDS_PIN_0 21 // The integrated board LED
|
|
#define LEDS_PIN_1 2 // External RGB LED(s strip)
|
|
// Traffic lights GPIOs
|
|
#define LIGHT_RED 39
|
|
#define LIGHT_GREEN 40
|
|
#define LIGHT_ORANGE 38
|
|
#define SPARE_1 37
|
|
#define SPARE_2 42
|
|
#define SPARE_3 41
|
|
|
|
// Button GPIO
|
|
#define BIB_BUTTON 3 // Digital input for the bib button
|
|
// W5500 SPI/Ethernet chip GPIOs
|
|
#define W5500_CS 14 // Chip Select pin
|
|
#define W5500_RST 9 // Reset pin
|
|
#define W5500_INT 10 // Interrupt pin
|
|
#define W5500_MISO 12 // MISO pin
|
|
#define W5500_MOSI 11 // MOSI pin
|
|
#define W5500_SCK 13 // Clock pin
|
|
|
|
// Number of LEDs for FastLED arrays
|
|
#define NUM_LEDS_0 1 // The integrated board LED is only one LED
|
|
#define NUM_LEDS_1 64 // LED(s) connected to GPIO LEDS_PIN_1.
|
|
|
|
// Other settings
|
|
#define LEDS_BRIGHTNESS 50 // Min 0, Max 255
|
|
#define HEARTBEAT_PERIOD 20 // Cycle time for aniating the heartbeat LED (in ms)
|
|
// #define PING_PERIOD 60000 // Cycle time for checking network connectivity by ping of our reverse proxy
|
|
#define PING_PERIOD 1000 // Cycle time for checking network connectivity by ping of our reverse proxy
|
|
|
|
// SPI ethernet chip driver
|
|
W5500Driver driver(W5500_CS);
|
|
|
|
// HTTP Web server
|
|
WebServer server(8000);
|
|
|
|
// LEDs values arrays for FastLED
|
|
CRGB leds_0[NUM_LEDS_0];
|
|
CRGB leds_1[NUM_LEDS_1];
|
|
|
|
// Heartbeat related variables
|
|
int heartbeat_index = 0;
|
|
unsigned long last_heartbeat_time = 0;
|
|
unsigned long last_ping_time = 0;
|
|
bool is_network_ok = false;
|
|
|
|
int forced_state = -1;
|
|
int button_state = -1;
|
|
unsigned long long request_count = 0;
|
|
unsigned long long request_chen = 0;
|
|
bool clear_msb = false;
|
|
|
|
|
|
|
|
// Home web page handler
|
|
void handleRoot() {
|
|
Serial.println("serving page /");
|
|
String message = "Hello,<br>I am <b>Bib O'Tron</b>!<br>Master of buttons and lights.</p>";
|
|
message += "<hr><ul>";
|
|
message += "<li><a href=\"/bibutton\">Bib button status</a></li>";
|
|
message += "<li><a href=\"https://git.distrilab.fr/Charavane/arduino/src/branch/main/bibotron/bibotron\">Source code and documentation</a></li>";
|
|
message += "<li><a href=\"/update\">Update firmware via Web OTA</a></li>";
|
|
message += "</ul>";
|
|
server.send(200, "text/html", message);
|
|
}
|
|
|
|
// `/bibutton` page handler
|
|
void handleGetBibbutton() {
|
|
Serial.println("serving page /bibutton");
|
|
Serial.print(" FreeHeapSize : ");
|
|
Serial.println(xPortGetFreeHeapSize());
|
|
|
|
request_count++;
|
|
request_chen|=1;
|
|
|
|
bool isOpen = false;
|
|
|
|
if ((forced_state < 0)) {
|
|
isOpen = (button_state == HIGH);
|
|
} else {
|
|
isOpen = (forced_state == HIGH);
|
|
}
|
|
|
|
String message = "<html><head>";
|
|
message += "<meta charset=\"UTF-8\">";
|
|
message += "<meta http-equiv=\"refresh\" content=\"60\">";
|
|
message += "<link rel=\"stylesheet\" href=\"/css/bibstate.css\">";
|
|
message += "<link rel=\"stylesheet\" href=\"/css/style.css\">";
|
|
// message += "<link rel=\"icon\" type=\"image/png\" href=\"https://lebib.org/sites/default/files/favicon_02.png\"/>";
|
|
message += "</head><body>";
|
|
message += "<div id=\"bibstate\">";
|
|
|
|
if (isOpen) {
|
|
message += "<div id=\"bibopen\"></div>";
|
|
} else {
|
|
message += "<div id=\"bibclose\"></div>";
|
|
}
|
|
message += "<div class=\"bibstatus-status\">";
|
|
if (isOpen) {
|
|
message += "Le Bib est ouvert";
|
|
} else {
|
|
message += "Le Bib est Fermé";
|
|
}
|
|
message += "</div></div></body></html>";
|
|
server.send(200, "text/html", message);
|
|
|
|
Serial.print(" FreeHeapSize : ");
|
|
Serial.println(xPortGetFreeHeapSize());
|
|
}
|
|
|
|
// `/bibleds` POST request handler
|
|
void handleSetButton() {
|
|
if (!server.authenticate(button_user, button_password)) {
|
|
return server.requestAuthentication();
|
|
}
|
|
|
|
if (server.method() == HTTP_POST) {
|
|
Serial.println("received post request on /bibutton");
|
|
String postBody = server.arg("plain");
|
|
/* Disable printing the request body for prod
|
|
Serial.println(postBody);
|
|
*/
|
|
DynamicJsonDocument doc(512);
|
|
DeserializationError error = deserializeJson(doc, postBody);
|
|
if (error) {
|
|
Serial.print(F("Error parsing JSON "));
|
|
Serial.println(error.c_str());
|
|
String msg = error.c_str();
|
|
server.send(400, F("text/html"),
|
|
"Error while parsing json data! <br>" + msg);
|
|
} else {
|
|
JsonObject postObj = doc.as<JsonObject>();
|
|
forced_state = postObj["forceState"] | forced_state;
|
|
if ((forced_state != -1) && (forced_state != LOW) && (forced_state != HIGH)) {
|
|
forced_state = -1;
|
|
}
|
|
DynamicJsonDocument doc(512);
|
|
doc["forced_state"] = forced_state;
|
|
doc["button_state"] = button_state;
|
|
String buf;
|
|
serializeJson(doc, buf);
|
|
server.send(200, F("application/json"), buf);
|
|
Serial.print(F("done."));
|
|
}
|
|
}
|
|
}
|
|
|
|
// `/bibutton/leds` POST request handler
|
|
void handleSetLeds() {
|
|
if (!server.authenticate(leds_user, leds_password)) {
|
|
return server.requestAuthentication();
|
|
}
|
|
|
|
if (server.method() == HTTP_POST) {
|
|
Serial.println("received post request on /bibutton/leds");
|
|
String postBody = server.arg("plain");
|
|
/* Disable printing the request body for prod
|
|
Serial.println(postBody);
|
|
*/
|
|
DynamicJsonDocument doc(512);
|
|
DeserializationError error = deserializeJson(doc, postBody);
|
|
if (error) {
|
|
Serial.print(F("Error parsing JSON "));
|
|
Serial.println(error.c_str());
|
|
String msg = error.c_str();
|
|
server.send(400, F("text/html"),
|
|
"Error while parsing json data! <br>" + msg);
|
|
} else {
|
|
JsonObject postObj = doc.as<JsonObject>();
|
|
if (postObj.containsKey("index") && postObj.containsKey("count") && postObj.containsKey("values")) {
|
|
int index = postObj["index"] | -1;
|
|
int count = postObj["count"] | -1;
|
|
JsonArray values = postObj["values"];
|
|
if (index >= 0 && index < NUM_LEDS_1 && count > 0 && (index+count) <= NUM_LEDS_1) {
|
|
for (int i=0; i<count; i++) {
|
|
JsonArray val = values[i];
|
|
byte red = val[0];
|
|
byte green = val[1];
|
|
byte blue = val[2];
|
|
/* Disable printing these details for prod
|
|
Serial.print(F("Setting led #"));
|
|
Serial.print(index+i);
|
|
Serial.print(F(" to (r,g,b)=("));
|
|
Serial.print(red);
|
|
Serial.print(F(","));
|
|
Serial.print(green);
|
|
Serial.print(F(","));
|
|
Serial.print(blue);
|
|
Serial.println(F(")"));
|
|
*/
|
|
leds_1[i] = CRGB(red,green,blue);
|
|
}
|
|
}else {
|
|
DynamicJsonDocument doc(512);
|
|
doc["status"] = "Failed";
|
|
doc["message"] = F("Invalid index or count value!");
|
|
String buf;
|
|
serializeJson(doc, buf);
|
|
server.send(400, F("application/json"), buf);
|
|
Serial.print(F("done."));
|
|
}
|
|
|
|
DynamicJsonDocument doc(512);
|
|
doc["status"] = "OK";
|
|
String buf;
|
|
serializeJson(doc, buf);
|
|
server.send(201, F("application/json"), buf);
|
|
}else {
|
|
DynamicJsonDocument doc(512);
|
|
doc["status"] = "Failed";
|
|
doc["message"] = F("No data found, or incorrect!");
|
|
String buf;
|
|
serializeJson(doc, buf);
|
|
server.send(400, F("application/json"), buf);
|
|
Serial.print(F("done."));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Error 404 handler
|
|
void handleNotFound() {
|
|
Serial.println("serving page 404");
|
|
String message = "Error 404\n\n";
|
|
message += "URI: ";
|
|
message += server.uri();
|
|
message += "\nMethod: ";
|
|
message += (server.method() == HTTP_GET) ? "GET" : "POST";
|
|
message += "\nArguments: ";
|
|
message += server.args();
|
|
message += "\n";
|
|
for (uint8_t i = 0; i < server.args(); i++) {
|
|
message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
|
|
}
|
|
server.send(404, "text/plain", message);
|
|
}
|
|
|
|
|
|
void animateLedStrip(void) {
|
|
unsigned long long val = request_count ^ request_chen;
|
|
|
|
int i = 0;
|
|
while (i<NUM_LEDS_1 && val>0) {
|
|
unsigned bit = val & 1;
|
|
if (bit == 1) {
|
|
leds_1[NUM_LEDS_1-1-i] = CHSV((i+heartbeat_index)%255,255,255);
|
|
} else {
|
|
leds_1[NUM_LEDS_1-1-i] = CRGB(0,0,0);
|
|
}
|
|
val >>= 1;
|
|
i++;
|
|
}
|
|
if (i<NUM_LEDS_1) {
|
|
leds_1[NUM_LEDS_1-1-i] = CRGB(0,0,0);
|
|
}
|
|
if (clear_msb) {
|
|
clear_msb = false;
|
|
leds_1[0] = CRGB(0,0,0);
|
|
}
|
|
if (request_chen > 0) {
|
|
request_chen <<= 1;
|
|
clear_msb = (request_chen == 0);
|
|
}
|
|
}
|
|
|
|
|
|
// Setup the web OTA update library
|
|
void otaSetup(void) {
|
|
ElegantOTA.onStart([]() {
|
|
Serial.println("OTA update process started.");
|
|
});
|
|
// This is spitting out too many line to the serial line. Enable it only for debugging purpose.
|
|
// ElegantOTA.onProgress([](size_t current, size_t final) {
|
|
// Serial.printf("Progress: %u%%\n", (current * 100) / final);
|
|
// });
|
|
ElegantOTA.onEnd([](bool success) {
|
|
if (success) {
|
|
Serial.println("OTA update completed successfully.");
|
|
} else {
|
|
Serial.println("OTA update failed.");
|
|
}
|
|
});
|
|
ElegantOTA.begin(&server);
|
|
ElegantOTA.setAuth(ota_user, ota_password);
|
|
}
|
|
|
|
|
|
void webServerSetup() {
|
|
server.on("/", handleRoot);
|
|
server.on("/bibutton", HTTP_GET, handleGetBibbutton);
|
|
server.on("/bibutton", HTTP_POST, handleSetButton);
|
|
server.on("/bibleds", HTTP_POST, handleSetLeds);
|
|
server.onNotFound(handleNotFound);
|
|
server.begin();
|
|
Serial.println("HTTP server started");
|
|
}
|
|
|
|
|
|
// Main setup function
|
|
void setup() {
|
|
// Init serial port
|
|
Serial.begin(500000);
|
|
Serial.setDebugOutput(true);
|
|
Serial.println("Serial port initialized.");
|
|
|
|
// Safety delay
|
|
delay(1000);
|
|
|
|
// Setup Addressable LEDs
|
|
Serial.println("Setting-up the LEDs...");
|
|
FastLED.addLeds<WS2812, LEDS_PIN_0, GRB>(leds_0, NUM_LEDS_0);
|
|
FastLED.addLeds<WS2812, LEDS_PIN_1, GRB>(leds_1, NUM_LEDS_1);
|
|
FastLED.setBrightness(LEDS_BRIGHTNESS);
|
|
FastLED.show();
|
|
|
|
// Setup the GPIO of the bib button as an input with an integrated weak pull-up resistor
|
|
Serial.println("Setting-up the button GPIO...");
|
|
pinMode(BIB_BUTTON, INPUT_PULLDOWN);
|
|
// Setup the GPIOs of the traffic lights as output
|
|
pinMode(LIGHT_RED, OUTPUT);
|
|
pinMode(LIGHT_GREEN, OUTPUT);
|
|
pinMode(LIGHT_ORANGE, OUTPUT);
|
|
|
|
Serial.println("Setting-up the ethernet interface...");
|
|
// Initialize SPI with specified pin configuration
|
|
SPI.begin(W5500_SCK, W5500_MISO, W5500_MOSI);
|
|
// Initialize Ethernet using DHCP to obtain an IP address
|
|
Ethernet.init(driver);
|
|
if (Ethernet.begin(mac[SELECTED_MAC]) == 0) {
|
|
Serial.println("DHCP failed, falling back to static IP...");
|
|
// Initialize with static IP
|
|
Ethernet.begin(mac[SELECTED_MAC], myIP, myDNS, myGW, mySN);
|
|
} else {
|
|
is_network_ok = true;
|
|
}
|
|
// Print the assigned IP address
|
|
Serial.print("IP Address: ");
|
|
Serial.println(Ethernet.localIP());
|
|
|
|
//Setup mdns so that other hosts can resolve our <hostname>.local DNS name
|
|
Serial.println("Setting-up the MDNS responder...");
|
|
if (!MDNS.begin(hostname)) {
|
|
Serial.println("Error setting up MDNS responder!");
|
|
}
|
|
|
|
Serial.println("Setting-up the web server...");
|
|
webServerSetup();
|
|
|
|
Serial.println("Setting-up the web OTA library...");
|
|
otaSetup();
|
|
|
|
Serial.println("Setup completed :-)");
|
|
}
|
|
|
|
// Main loop function
|
|
void loop() {
|
|
// Get current time
|
|
unsigned long current_time = micros();
|
|
|
|
// Check if it is time to update the heartbeat LED
|
|
if ((current_time - last_heartbeat_time) >= 1000*HEARTBEAT_PERIOD) {
|
|
// Update last heartbeat time
|
|
last_heartbeat_time = current_time;
|
|
|
|
// Increment the hue we are displaying on the board LED, and cycle back to 0 when reaching 256
|
|
heartbeat_index = (heartbeat_index + 1) % 256;
|
|
// Update the LED color in the FastLED array
|
|
leds_0[0] = CHSV(heartbeat_index,255,255);
|
|
|
|
// Read the bibutton status and turn on/off the red and green lights accordingly
|
|
button_state = digitalRead(BIB_BUTTON);
|
|
if ((forced_state < 0) || forced_state == button_state) {
|
|
forced_state = -1;
|
|
if (button_state == HIGH) {
|
|
digitalWrite(LIGHT_GREEN, HIGH);
|
|
digitalWrite(LIGHT_RED, LOW);
|
|
} else {
|
|
digitalWrite(LIGHT_GREEN, LOW);
|
|
digitalWrite(LIGHT_RED, HIGH);
|
|
}
|
|
} else {
|
|
digitalWrite(LIGHT_GREEN, HIGH);
|
|
digitalWrite(LIGHT_RED, HIGH);
|
|
}
|
|
|
|
// Turn on/off the orange light according to network connectivity status
|
|
if (!is_network_ok) {
|
|
digitalWrite(LIGHT_ORANGE, HIGH);
|
|
} else {
|
|
digitalWrite(LIGHT_ORANGE, LOW);
|
|
}
|
|
|
|
// Do something on the LED strip values
|
|
animateLedStrip();
|
|
|
|
// Update the LEDs with the values we set in the arrays
|
|
FastLED.show();
|
|
}
|
|
|
|
// Check if it is time to check the network connectivity
|
|
if ((current_time - last_ping_time) >= 1000*PING_PERIOD) {
|
|
// ToDo : move that to an asynchronous task as the ping can take a while if the internet is not reachable. But that is not that important, because if the net is down, our main purpose is borked anyway.
|
|
is_network_ok = Ping.ping(myProxy, 1);
|
|
if (is_network_ok) {
|
|
Serial.print("ping time: ");
|
|
Serial.print(Ping.averageTime());
|
|
Serial.println(" ms");
|
|
} else {
|
|
Serial.println("Failed to ping remote host!");
|
|
}
|
|
last_ping_time = micros();
|
|
}
|
|
|
|
// Do nothing for 1 ms
|
|
delay(1);
|
|
|
|
// Let the web server and the web OTA update library do their things
|
|
server.handleClient();
|
|
ElegantOTA.loop();
|
|
}
|