bibotron/bibotron.ino
2026-01-28 17:57:15 +01:00

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();
}