// 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 #include //#include #include #include #include #include #include #include // 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 // 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,
I am Bib O'Tron!
Master of buttons and lights.

"; message += "
"; 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 = ""; message += ""; message += ""; message += ""; message += ""; // message += ""; message += ""; message += "
"; if (isOpen) { message += "
"; } else { message += "
"; } message += "
"; if (isOpen) { message += "Le Bib est ouvert"; } else { message += "Le Bib est Fermé"; } message += "
"; 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!
" + msg); } else { JsonObject postObj = doc.as(); 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!
" + msg); } else { JsonObject postObj = doc.as(); 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; i0) { 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 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(leds_0, NUM_LEDS_0); FastLED.addLeds(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 .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(); }