copy code from my personal git repo
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
secrets.h
|
||||
tests/test-env.sh
|
||||
build/**
|
||||
test/**
|
||||
100
README.md
|
|
@ -1,2 +1,100 @@
|
|||
# bibotron
|
||||
# The Bib O'Tron project
|
||||
A proof of concept for replacing the raspberry pi that is managing the bib button by an ESP32 microcontroller.
|
||||
|
||||
## Features
|
||||
- Read the status of the bib button using a digital input
|
||||
- Display the state of the bib button using a trafic lights
|
||||
- Implement a HTTP server to serve :
|
||||
- A basic home page with some links
|
||||
- A bib button status page, that is meant to be included in the future static version of the [bib web site](https://lebib.org/)
|
||||
- CSS/images are not hosted locally (links to the [bib web site](https://www.lebib.org/))
|
||||
- A web OTA page to update the firmware through the network
|
||||
- A REST API to force the button state remotely
|
||||
- Periodically pings a remote host and displays an error code on the RGB LED in case of failure (blinking orange color)
|
||||
- Connects to the network through an RJ45 ethernet interface
|
||||
- Implements Multicast DNS so that its IP address can be found through the bibotron.local name
|
||||
- Can be powered by USB-C, using a 5V power supply connected to screw terminals or though the ethernet cable if the optional POE module is installed.
|
||||
|
||||
## Hardware
|
||||
The microcontroller board used is a [Waveshare ESP32-S3-ETH](https://www.waveshare.com/wiki/ESP32-S3-ETH).
|
||||
The RGB LED is a WS2812
|
||||
|
||||
## /!\ Warning /!\
|
||||
This board, doesn't have any protection between the USB 5V line and the POE 5V line. you MUST remove the POE module and/or disconnect the ethernet cable before plugging-in a USB cable, or else the proverbial magic smoke will be released!!!
|
||||
(potentially on your computer side)
|
||||
|
||||
## Pictures
|
||||
check [the pictures](doc/pictures.md) in the doc directory
|
||||
|
||||
## License
|
||||
[Creative Commons Zero v1.0 Universal](https://creativecommons.org/public-domain/cc0/)
|
||||
|
||||
|
||||
## User guide
|
||||
### Changing the bibutton state physically from Le Bib
|
||||
Juste operate the switch that is next to the traffic lighs.
|
||||
**ToDo :** add a picture of the switch
|
||||
|
||||
### Changing the bibutton state remotely
|
||||
In case the person who last closed Le Bib has forgotten to switch the bibutton state to the closed position, or if the physical switch is broken, it is possible to call a remote API to force the button state.
|
||||
This can be done from the command line with curl as below :
|
||||
- To force the bibutton to the closed state :
|
||||
```
|
||||
curl -i -u "${bibutton_user}:${bibutton_password}" --request POST --data "{forceState:0}" --header "Content-Type: application/json" ${TARGET}
|
||||
```
|
||||
- To force the bibutton to the opened state :
|
||||
```
|
||||
curl -i -u "${bibutton_user}:${bibutton_password}" --request POST --data "{forceState:0}" --header "Content-Type: application/json" ${TARGET}
|
||||
```
|
||||
|
||||
**ToDo :** The user and password to use need to be stored in the password manager used by Le Bib
|
||||
|
||||
While the bibutton state is being remotely forced to a diffent state than the physical switch state, the red and green traffic lights will be both illuminated at the same time.
|
||||
As soon as the the physical switch is changed to the same state as the remotely forced state, or the state is remotly forced back to the same state as the physical switch, the "remotly forced" mode will be canceled, and only the red or green light will be on, according the physical switch state.
|
||||
|
||||
### Traffic lights
|
||||
Depending on the bibutton state, the traffic lights will be illuminated as follow :
|
||||
- Red color (upper light) when the bibutton is closed,
|
||||
- Green color (bottom light) when button is opened, Solid )
|
||||
- Orange color (middle light) when the network is down (the last ping to the reverse proxy failed)
|
||||
- Both Green and Red color at the same time when the bibutton state has been remotely forced to a diffent state than the physical switch
|
||||
|
||||
### LED light strip
|
||||
This is work in progress.
|
||||
|
||||
The light strip currently displays the count of requests received to read the bibutton state since the last boot of the microntroller, as a 64 bits binary number.
|
||||
And there is an effect with sliding LED from the right side to the left side, each time a new request is processed.
|
||||
It is also possible to change the LEDs through a REST API, that is only accessible from the local network. Check the scripts in the test directory to learn more.
|
||||
But note that the request count is frequently displayed to the LED strip, which will overwrite the least significant bits of whatever has been set through the API. Also the slifing effect will overwrite everything each time a request is processed.
|
||||
|
||||
The LED strip behavior is not functionally usefull for the Bibutton feature. You can hack it any way you want.
|
||||
|
||||
## Developer guide
|
||||
The project is developed using the Arduino IDE and the following libraries :
|
||||
- FastLED, to drive the addressable RGB LEDs
|
||||
- EthernetESP32, to drive the SPI ethernet chip (W5500) of the ESP32-S3-ETH
|
||||
- ESPping, to periodically check network connectivity
|
||||
- ElegantOTA light, to updade the firmware through the network
|
||||
- ArduinoJson, to parse json in the rest api handlers
|
||||
|
||||
Additionally to these libraries, the esp32 boards package must be installed in Arduino IDE.
|
||||
Then in the `Tools` menu, select :
|
||||
- `Board` > `esp32` > `ESP32-S3-Box`
|
||||
- `Partition scheme` > `16 MB Flash`
|
||||
- `USB mode" > `Hardware CDC and JTAG`
|
||||
- `Programmer` > `esptools`
|
||||
- `Port` > the tty port of the board (if programming through USB)
|
||||
|
||||
Before building the souce code, you need to add in your working copy the secret.h file that contains the sensitive information that we not want to publish in the this git repository
|
||||
**ToDo :** The secret.h needs to be stored in the password manager used by Le Bib
|
||||
If your are building the source code for a different site than Le Bib, you can create your own secret.h based on secret.h.example
|
||||
|
||||
To update the firmware through Web OTA, first create the binary using the menu `Sketch` > `Export compiled Binary`.
|
||||
Then :
|
||||
- In your web browser, go to http://bibotron.local/update (or use the local IP address if the MDNS resolution of bibotron.local is not working : 10.13.12.40)
|
||||
- **ToDo :** The user and password to use need to be stored in the password manager used by Le Bib
|
||||
- In `OTA Mode` select firmware
|
||||
- Click on `Select file` and select the file `bibotron.ino.bin` that should have been generated somewhere under the project build folder (do not used any of the other .bin files that have been generated).
|
||||
- Wait for the upload to complete.
|
||||
- The ESP32 should automatically restart after the upgrade. (The LEDs and traffic lights will turn off for a while during the reset)
|
||||
- Check on www.lebib.org that the bibutton state is correctly displayed
|
||||
|
|
|
|||
455
bibotron.ino
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
// 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();
|
||||
}
|
||||
BIN
doc/ESP32-S3-ETH-Schematic.pdf
Normal file
BIN
doc/ESP32-S3-ETH-pinout.jpg
Normal file
|
After Width: | Height: | Size: 287 KiB |
BIN
doc/bibotron-button-closed.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
doc/bibotron-button-opened.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
doc/bibotron-home-page.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
doc/bibotron-prototype-bottom.jpg
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
doc/bibotron-prototype-top.jpg
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
doc/bibotron_20251106.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
8
doc/pictures.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Pictures of the Bib O'Tron project
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
44
secrets.h.example
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const char* hostname = "bibotron";
|
||||
const char* ota_user = "adminuser";
|
||||
const char* ota_password = "b1b0tr0n";
|
||||
const char* button_user = "poweruser";
|
||||
const char* button_password = "AllComputersAreBroken";
|
||||
const char* leds_user = "user";
|
||||
const char* leds_password = "yolo";
|
||||
|
||||
// MAC address and IP address
|
||||
// You may want to set this to different values if you have several boards connected to your LAN using similar code.
|
||||
// In the code we are only using the first MAC of the array. But the array is there if you want to change the code to randomly pick one each time the system boots.
|
||||
#define NUMBER_OF_MAC 20
|
||||
#define SELECTED_MAC 0
|
||||
byte mac[][NUMBER_OF_MAC] =
|
||||
{
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x00 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x02 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x03 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x04 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x05 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x06 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x07 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x08 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x09 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x0A },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x0B },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x0C },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x0D },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x0E },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x0F },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x10 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x11 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x12 },
|
||||
{ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x13 },
|
||||
};
|
||||
// Fallback static IPv4 configuration in case DHCP is not working
|
||||
IPAddress myIP(10, 13, 12, 40);
|
||||
IPAddress myGW(10, 13, 12, 254);
|
||||
IPAddress mySN(255, 255, 255, 0);
|
||||
IPAddress myDNS(10, 13, 12, 254);
|
||||
|
||||
// Server to ping to check the network connectivity
|
||||
IPAddress myProxy(10, 13, 12, 254);
|
||||
1
tests/button_empty.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
1
tests/button_force_off.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{forceState:0}
|
||||
1
tests/button_force_on.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{forceState:1}
|
||||
1
tests/leds_3_values.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{index:12,count:3,values:[[255,0,0],[0,255,0],[0,0,255]]}
|
||||
70
tests/leds_64_values.json
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
index:0,
|
||||
count:64,
|
||||
values:[
|
||||
[255,0,0],
|
||||
[0,255,0],
|
||||
[0,0,255],
|
||||
[128,128,0],
|
||||
[128,0,128],
|
||||
[0,128,128],
|
||||
[84,84,84],
|
||||
[0,0,0],
|
||||
[255,0,0],
|
||||
[0,255,0],
|
||||
[0,0,255],
|
||||
[128,128,0],
|
||||
[128,0,128],
|
||||
[0,128,128],
|
||||
[84,84,84],
|
||||
[0,0,0],
|
||||
[255,0,0],
|
||||
[0,255,0],
|
||||
[0,0,255],
|
||||
[128,128,0],
|
||||
[128,0,128],
|
||||
[0,128,128],
|
||||
[84,84,84],
|
||||
[0,0,0],
|
||||
[255,0,0],
|
||||
[0,255,0],
|
||||
[0,0,255],
|
||||
[128,128,0],
|
||||
[128,0,128],
|
||||
[0,128,128],
|
||||
[84,84,84],
|
||||
[0,0,0],
|
||||
[255,0,0],
|
||||
[0,255,0],
|
||||
[0,0,255],
|
||||
[128,128,0],
|
||||
[128,0,128],
|
||||
[0,128,128],
|
||||
[84,84,84],
|
||||
[0,0,0],
|
||||
[255,0,0],
|
||||
[0,255,0],
|
||||
[0,0,255],
|
||||
[128,128,0],
|
||||
[128,0,128],
|
||||
[0,128,128],
|
||||
[84,84,84],
|
||||
[0,0,0],
|
||||
[255,0,0],
|
||||
[0,255,0],
|
||||
[0,0,255],
|
||||
[128,128,0],
|
||||
[128,0,128],
|
||||
[0,128,128],
|
||||
[84,84,84],
|
||||
[0,0,0],
|
||||
[255,0,0],
|
||||
[0,255,0],
|
||||
[0,0,255],
|
||||
[128,128,0],
|
||||
[128,0,128],
|
||||
[0,128,128],
|
||||
[84,84,84],
|
||||
[0,0,0]
|
||||
]
|
||||
}
|
||||
70
tests/leds_all-off.json
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
index:0,
|
||||
count:64,
|
||||
values:[
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[0,0,0]
|
||||
]
|
||||
}
|
||||
1
tests/leds_invalid_index_1.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{index:-12,count:3,values:[[255,0,0],[0,255,0],[0,0,255]]}
|
||||
1
tests/leds_invalid_index_2.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{index:1234567,count:3,values:[[255,0,0],[0,255,0],[0,0,255]]}
|
||||
1
tests/leds_invalid_index_3.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{index:12345678901234567890,count:3,values:[[255,0,0],[0,255,0],[0,0,255]]}
|
||||
1
tests/leds_invalid_index_4.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{index:"All Computers Are Broken",count:3,values:[[255,0,0],[0,255,0],[0,0,255]]}
|
||||
1
tests/leds_missing_values.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{index:12,count:32,values:[[255,0,0],[0,255,0],[0,0,255]]}
|
||||
1
tests/leds_out_of_range_values.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{index:12,count:3,values:[[-1,0,0],[0,600,0],[0,0,70000]]}
|
||||
10
tests/send_one_request.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
source "test-env.sh"
|
||||
REQUEST=${1:-"./leds_64_values.json"}
|
||||
TARGET=${test_base_url}${2:-/bibleds}
|
||||
echo "##########################################"
|
||||
echo "# sending post request to ${TARGET} with :"
|
||||
echo "# ${REQUEST}"
|
||||
echo "##########################################"
|
||||
curl -i -u "${leds_user}:${leds_password}" --request POST --data "@${REQUEST}" --header "Content-Type: application/json" ${TARGET}
|
||||
echo
|
||||
12
tests/send_test_requests.sh
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env bash
|
||||
source "test-env.sh"
|
||||
TARGET=${test_base_url}/bibleds
|
||||
for f in leds*.json; do
|
||||
echo "##########################################"
|
||||
echo "# sending post request to ${TARGET} with :"
|
||||
echo "# ${f}"
|
||||
echo "##########################################"
|
||||
time curl -u "${leds_user}:${leds_password}" --request POST --data "@${f}" --header "Content-Type: application/json" ${TARGET}
|
||||
echo
|
||||
echo
|
||||
done
|
||||
5
tests/test-env.sh.example
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export test_button_user="poweruser"
|
||||
export test_button_password="AllComputersAreBroken"
|
||||
export test_leds_user="user"
|
||||
export test_leds_password="yolo"
|
||||
export test_base_url="http://10.13.12.40:8000"
|
||||