This commit is contained in:
Michal Courson
2025-08-28 21:39:35 -04:00
commit b1e155d08b
28 changed files with 7246 additions and 0 deletions

4
main/CMakeLists.txt Normal file
View File

@ -0,0 +1,4 @@
idf_component_register(
SRCS "Ethernet.cpp" "Server.cpp" "Config.cpp" "main.cpp" "kartReciever.cpp" "Switch.cpp" "Ethernet.cpp"
INCLUDE_DIRS ""
)

61
main/Config.cpp Normal file
View File

@ -0,0 +1,61 @@
#include "Config.hpp"
#include "FS.h"
#include "SPIFFS.h"
Config config;
void Config::Init() {
SPIFFS.begin(true);
File f = SPIFFS.open("/config.json", FILE_READ);
if (!f) {
SetDefaults();
return;
}
DeserializationError err = deserializeJson(conf_json, f);
if (err == DeserializationError::Ok) {
Load();
} else {
SetDefaults();
}
f.close();
}
void Config::SetDefaults() {
Serial.println("Setting default config");
// Set default MAC address
mac_address[0] = 0x34;
mac_address[1] = 0x85;
mac_address[2] = 0x18;
mac_address[3] = 0xa9;
mac_address[4] = 0x3d;
mac_address[5] = 0xfc;
// Set default beacon ID
beacon_id = esp_random();
// Save the defaults to SPIFFS
Save();
}
void Config::Save() {
conf_json.to<JsonObject>();
JsonArray arr = conf_json["mac"].to<JsonArray>();
for (int i = 0; i < 6; i++) {
arr.add<int>(mac_address[i]);
}
conf_json["beacon_id"] = beacon_id;
String out;
serializeJson(conf_json, out);
File f = SPIFFS.open("/config.json", FILE_WRITE, true);
f.print(out);
f.close();
Serial.printf("Config saved: %s\n", out.c_str());
}
void Config::Load() {
JsonArray arr = conf_json["mac"];
for (int i = 0; i < 6; i++) {
mac_address[i] = arr[i];
}
beacon_id = conf_json["beacon_id"];
}

22
main/Config.hpp Normal file
View File

@ -0,0 +1,22 @@
#ifndef CONFIG_HPP
#define CONFIG_HPP
#include <Arduino.h>
#include <ArduinoJson.h>
class Config {
public:
void Init();
void Save();
uint8_t mac_address[6] = {0x34, 0x85, 0x18, 0xa9, 0x3d, 0xfc};
uint32_t beacon_id = -1;
private:
void Load();
void SetDefaults();
JsonDocument conf_json;
};
extern Config config;
#endif

115
main/Ethernet.cpp Normal file
View File

@ -0,0 +1,115 @@
#include "Ethernet.hpp"
#include "ETH.h"
#include "SPI.h"
#define ETH_PHY_TYPE ETH_PHY_W5500
#define ETH_PHY_ADDR 1
#define ETH_PHY_CS 8
#define ETH_PHY_IRQ 3
#define ETH_PHY_RST 5
#define ETH_SPI_SCK 11
#define ETH_SPI_MISO 10
#define ETH_SPI_MOSI 9
static bool eth_connected = false;
EthernetHandler eth;
void onEvent(arduino_event_id_t event, arduino_event_info_t info) {
switch (event) {
case ARDUINO_EVENT_ETH_START:
Serial.println("ETH Started");
// set eth hostname here
ETH.setHostname("esp32-eth0");
break;
case ARDUINO_EVENT_ETH_CONNECTED:
Serial.println("ETH Connected");
break;
case ARDUINO_EVENT_ETH_GOT_IP:
Serial.printf("ETH Got IP: '%s'\n", esp_netif_get_desc(info.got_ip.esp_netif));
Serial.println(ETH);
#if USE_TWO_ETH_PORTS
Serial.println(ETH1);
#endif
eth_connected = true;
break;
case ARDUINO_EVENT_ETH_LOST_IP:
Serial.println("ETH Lost IP");
eth_connected = false;
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
Serial.println("ETH Disconnected");
eth_connected = false;
break;
case ARDUINO_EVENT_ETH_STOP:
Serial.println("ETH Stopped");
eth_connected = false;
break;
default:
break;
}
}
void EthernetHandler::init() {
// Initialization code here
Network.onEvent(onEvent);
SPI.begin(ETH_SPI_SCK, ETH_SPI_MISO, ETH_SPI_MOSI);
ETH.begin(ETH_PHY_TYPE, ETH_PHY_ADDR, ETH_PHY_CS, ETH_PHY_IRQ, ETH_PHY_RST, SPI);
}
void EthernetHandler::begin() {
// Code to start Ethernet communication
}
void EthernetHandler::end() {
// Code to end Ethernet communication
}
String EthernetHandler::post(const String& url, const String& payload) {
if (!eth_connected) {
Serial.println("Ethernet not connected. Cannot perform POST request.");
return "";
}
HTTPClient http;
http.begin(url);
http.addHeader("Content-Type", "application/json");
int httpResponseCode = http.POST(payload);
String response;
if (httpResponseCode > 0) {
response = http.getString();
Serial.printf("POST Response code: %d\n", httpResponseCode);
Serial.println("Response: " + response);
} else {
Serial.printf("Error on sending POST: %s\n", http.errorToString(httpResponseCode).c_str());
}
http.end();
return response;
}
String EthernetHandler::get(const String& url) {
if (!eth_connected) {
Serial.println("Ethernet not connected. Cannot perform GET request.");
return "";
}
HTTPClient http;
http.begin(url);
int httpResponseCode = http.GET();
String response;
if (httpResponseCode > 0) {
response = http.getString();
Serial.printf("GET Response code: %d\n", httpResponseCode);
Serial.println("Response: " + response);
} else {
Serial.printf("Error on sending GET: %s\n", http.errorToString(httpResponseCode).c_str());
}
http.end();
return response;
}

17
main/Ethernet.hpp Normal file
View File

@ -0,0 +1,17 @@
#ifndef ETHERNET_HANDLER_HPP
#define ETHERNET_HANDLER_HPP
#include "Arduino.h"
#include "HTTPClient.h"
class EthernetHandler {
public:
void init();
void begin();
void end();
String post(const String& url, const String& payload);
String get(const String& url);
};
extern EthernetHandler eth;
#endif // ETHERNET_HANDLER_HPP

83
main/Server.cpp Normal file
View File

@ -0,0 +1,83 @@
#include "Server.hpp"
#include <Arduino.h>
#include "Config.hpp"
#include "WiFi.h"
#include "index.hpp"
WebServer web_server;
void WebServer::Init() {
running = false;
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/html", index_html);
});
// Endpoint to get config
server.on("/config", HTTP_GET, [](AsyncWebServerRequest *request) {
JsonDocument doc;
JsonArray arr = doc["mac_address"].to<JsonArray>();
for (int i = 0; i < 6; i++) {
arr.add(config.mac_address[i]);
}
doc["beacon_id"] = config.beacon_id;
String out;
serializeJson(doc, out);
Serial.printf("Config requested: %s\n", out.c_str());
request->send(200, "application/json", out);
});
// Endpoint to set config
server.on("/config", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("mac_address", true)) {
String macStr = request->getParam("mac_address", true)->value();
int idx = 0;
int last = 0;
for (int i = 0; i < macStr.length() && idx < 6; i++) {
if (macStr[i] == ',') {
config.mac_address[idx++] = macStr.substring(last, i).toInt();
last = i + 1;
}
}
if (idx < 6) config.mac_address[idx] = macStr.substring(last).toInt();
}
if (request->hasParam("beacon_id", true)) {
config.beacon_id = request->getParam("beacon_id", true)->value().toInt();
}
config.Save();
request->send(200, "text/plain", "Config updated");
});
// Endpoint to restart system
server.on("/restart", HTTP_POST, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "Restarting...");
for (int i = 0; i < 10; i++) {
digitalWrite(LED_BUILTIN, i % 2);
delay(50);
};
ESP.restart();
});
}
void WebServer::Start() {
if (running) return;
WiFi.disconnect();
IPAddress local_ip(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
WiFi.softAPConfig(local_ip, gateway, subnet);
WiFi.mode(WIFI_AP);
WiFi.softAP(String("beacon_") + String(config.beacon_id, HEX), "password");
running = true;
server.begin();
// Start server logic here
}
void WebServer::Stop() {
if (!running) return;
running = false;
server.end();
// Stop server logic here
}

21
main/Server.hpp Normal file
View File

@ -0,0 +1,21 @@
#ifndef SERVER_HPP
#define SERVER_HPP
#include "ESPAsyncWebServer.h"
class WebServer {
public:
WebServer() : server(80) {}
void Init();
void Start();
void Stop();
bool isRunning() const { return running; }
private:
bool running = false;
AsyncWebServer server;
};
extern WebServer web_server;
#endif

42
main/Switch.cpp Normal file
View File

@ -0,0 +1,42 @@
#include "Switch.hpp"
void Switch::Init(int pin,
float update_rate,
Type t,
Polarity pol,
uint8_t mode) {
last_update_ = millis();
updated_ = false;
state_ = 0x00;
t_ = t;
// Flip may seem opposite to logical direction,
// but here 1 is pressed, 0 is not.
flip_ = pol == POLARITY_INVERTED ? true : false;
pinMode(pin, mode);
pin_ = pin;
}
void Switch::Init(int pin, float update_rate) {
Init(pin,
update_rate,
TYPE_MOMENTARY,
POLARITY_INVERTED,
INPUT_PULLUP);
}
void Switch::Debounce() {
// update no faster than 1kHz
uint32_t now = millis();
updated_ = false;
if (now - last_update_ >= 1) {
last_update_ = now;
updated_ = true;
// shift over, and introduce new state.
const bool new_val = digitalRead(pin_);
state_ = (state_ << 1) | (flip_ ? !new_val : new_val);
// Set time at which button was pressed
if (state_ == 0x7f)
rising_edge_time_ = millis();
}
}

87
main/Switch.hpp Normal file
View File

@ -0,0 +1,87 @@
#ifndef SWITCH_HPP
#define SWITCH_HPP
#include <Arduino.h>
class Switch {
public:
/** Specifies the expected behavior of the switch */
enum Type {
TYPE_TOGGLE, /**< & */
TYPE_MOMENTARY, /**< & */
};
/** Specifies whether the pressed is HIGH or LOW. */
enum Polarity {
POLARITY_NORMAL, /**< & */
POLARITY_INVERTED, /**< & */
};
Switch() {}
~Switch() {}
/**
Initializes the switch object with a given port/pin combo.
\param pin port/pin object to tell the switch which hardware pin to use.
\param update_rate Does nothing. Backwards compatibility until next breaking update.
\param t switch type -- Default: TYPE_MOMENTARY
\param pol switch polarity -- Default: POLARITY_INVERTED
\param pu switch pull up/down -- Default: PULL_UP
*/
void Init(int pin,
float update_rate,
Type t,
Polarity pol,
uint8_t mode = INPUT_PULLUP);
/**
Simplified Init.
\param pin port/pin object to tell the switch which hardware pin to use.
\param update_rate Left for backwards compatibility until next breaking change.
*/
void Init(int pin, float update_rate = 0.f);
/**
Called at update_rate to debounce and handle timing for the switch.
In order for events not to be missed, its important that the Edge/Pressed checks
be made at the same rate as the debounce function is being called.
*/
void Debounce();
/** \return true if a button was just pressed. */
inline bool RisingEdge() const { return updated_ ? state_ == 0x7f : false; }
/** \return true if the button was just released */
inline bool FallingEdge() const {
return updated_ ? state_ == 0x80 : false;
}
/** \return true if the button is held down (or if the toggle is on) */
inline bool Pressed() const { return state_ == 0xff; }
/** \return true if the button is held down, without debouncing */
inline bool RawState() {
const bool raw = digitalRead(pin_);
return flip_ ? !raw : raw;
}
/** \return the time in milliseconds that the button has been held (or toggle has been on) */
inline float TimeHeldMs() const {
return Pressed() ? millis() - rising_edge_time_ : 0;
}
/** Left for backwards compatability until next breaking change
* \param update_rate Doesn't do anything
*/
inline void SetUpdateRate(float update_rate) {}
private:
uint32_t last_update_;
bool updated_;
Type t_;
uint8_t state_;
bool flip_;
float rising_edge_time_;
int pin_;
};
#endif

37
main/TimeSync.hpp Normal file
View File

@ -0,0 +1,37 @@
#ifndef TIMESYNC_HPP
#define TIMESYNC_HPP
#include <sys/time.h>
#include "Arduino.h"
#include "WiFi.h"
#include "time.h"
class TimeSync {
public:
static void Sync() {
const char* ssid = "ConnectonRefused";
const char* password = "retryconnection";
const char* ntpServer = "pool.ntp.org";
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected.");
configTime(0, 0, ntpServer);
delay(2000);
WiFi.disconnect();
}
static uint64_t GetTime() {
struct timeval tv;
gettimeofday(&tv, NULL); // gets time in seconds and microseconds
uint64_t epochMillis = (uint64_t)tv.tv_sec * 1000 + (tv.tv_usec / 1000);
return epochMillis;
}
};
#endif

19
main/idf_component.yml Normal file
View File

@ -0,0 +1,19 @@
## IDF Component Manager Manifest File
dependencies:
## Required IDF version
idf:
version: '>=4.1.0'
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
# # For 3rd party components:
# username/component: ">=1.0.0,<2.0.0"
# username2/component2:
# version: "~1.0.0"
# # For transient dependencies `public` flag can be set.
# # `public` flag doesn't have an effect dependencies of the `main` component.
# # All dependencies of `main` are public by default.
# public: true
espressif/arduino-esp32: ^3.3.0
bblanchon/arduinojson: ^7.4.2
esp32async/espasyncwebserver: ^3.7.10

84
main/index.hpp Normal file
View File

@ -0,0 +1,84 @@
#ifndef INDEX_HPP
#define INDEX_HPP
#include <Arduino.h>
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<style>
html {font-family: Arial; display: inline-block; text-align: center;}
h2 {font-size: 3.0rem;}
p {font-size: 3.0rem;}
body {max-width: 600px; margin:0px auto; padding-bottom: 25px;}
.switch {position: relative; display: inline-block; width: 120px; height: 68px}
.switch input {display: none}
.slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 6px}
.slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 3px}
input:checked+.slider {background-color: #b30000}
input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
.input-group {margin: 20px 0;}
label {font-size: 1.2rem;}
input[type='text'] {font-size: 1.2rem; padding: 5px;}
button {font-size: 1.2rem; padding: 10px 20px; margin: 10px;}
</style>
</head>
<body>
<h2>Beacon Config</h2>
<div id="config">
<h3>Device Config</h3>
<div class="input-group">
<label for="mac_address">MAC Address (hex, colon separated):</label>
<input type="text" id="mac_address" value="" placeholder="34:85:18:A9:3D:FC">
</div>
<div class="input-group">
<label for="beacon_id">Beacon ID (hex):</label>
<input type="text" id="beacon_id" value="" placeholder="AABBCCDD">
</div>
<button onclick="saveConfig()">Save Config</button>
<button onclick="restartSystem()">Restart System</button>
<p id="status"></p>
</div>
<script>
function toHex(num, len=2) {
return num.toString(16).toUpperCase().padStart(len, '0');
}
function fetchConfig() {
fetch('/config').then(r => r.json()).then(cfg => {
document.getElementById('mac_address').value = cfg.mac_address.map(x => toHex(x)).join(':');
document.getElementById('beacon_id').value = toHex(cfg.beacon_id, 8);
});
}
function saveConfig() {
const mac = document.getElementById('mac_address').value;
const beacon = document.getElementById('beacon_id').value;
// Convert MAC from hex string to decimal array for backend
const macArr = mac.split(':').map(x => parseInt(x, 16));
const macDec = macArr.join(',');
const beaconDec = parseInt(beacon, 16);
const params = new URLSearchParams();
params.append('mac_address', macDec);
params.append('beacon_id', beaconDec);
fetch('/config', {
method: 'POST',
body: params
}).then(r => r.text()).then(txt => {
document.getElementById('status').innerText = txt;
fetchConfig();
});
}
function restartSystem() {
fetch('/restart', {method: 'POST'}).then(r => r.text()).then(txt => {
document.getElementById('status').innerText = txt;
});
}
window.onload = fetchConfig;
</script>
</body>
</html>
)rawliteral";
#endif

10
main/kartMessage.hpp Normal file
View File

@ -0,0 +1,10 @@
#ifndef KART_MESSAGE_HPP
#define KART_MESSAGE_HPP
#include "Arduino.h"
typedef struct {
uint32_t kart_id;
uint32_t transmision;
} kart_msg;
#endif

43
main/kartReciever.cpp Normal file
View File

@ -0,0 +1,43 @@
#include "kartReciever.hpp"
#include <esp_now.h>
#include <esp_wifi.h>
#include "WiFi.h"
kartReceiver kart_receiver;
static kart_msg rcv_msg;
static void (*onReceiveCallback)(kart_msg *message) = nullptr;
static void OnDataRecv(const esp_now_recv_info_t *esp_now_info, const uint8_t *data, int data_len) {
// todo rssi filtering
memcpy(&rcv_msg, data, sizeof(kart_msg));
// Serial.print("Bytes received: ");
// Serial.println(data_len);
if (onReceiveCallback) {
onReceiveCallback(&rcv_msg);
}
}
void kartReceiver::init() {
}
bool kartReceiver::begin() {
WiFi.disconnect();
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return false;
}
esp_now_register_recv_cb(OnDataRecv);
run_status = true;
return true;
}
void kartReceiver::end() {
esp_now_deinit();
run_status = false;
}
bool kartReceiver::isBegin() {
return run_status;
}
void kartReceiver::registerCallback(void (*onReceiveCallback_)(kart_msg *message)) {
onReceiveCallback = onReceiveCallback_;
}

20
main/kartReciever.hpp Normal file
View File

@ -0,0 +1,20 @@
#ifndef KART_REC_HPP
#define KART_REC_HPP
#include "kartMessage.hpp"
class kartReceiver {
public:
void init();
bool begin();
void end();
bool isBegin();
void registerCallback(void (*onReceiveCallback)(kart_msg* message));
private:
bool run_status;
};
extern kartReceiver kart_receiver;
#endif

115
main/main.cpp Normal file
View File

@ -0,0 +1,115 @@
#include <esp_now.h>
#include <esp_wifi.h>
#include "Arduino.h"
#include "Config.hpp"
#include "Ethernet.hpp"
#include "SPIFFS.h"
#include "Server.hpp"
#include "Switch.hpp"
#include "TimeSync.hpp"
#include "WiFi.h"
#include "esp32-hal-ledc.h"
#include "kartReciever.hpp"
Switch web_entry_switch;
// 34:85:18:A9:3D:FC
uint8_t trackside_mac[] = {0x34, 0x85, 0x18, 0xa9, 0x3d, 0xfc};
typedef struct struct_message {
char a[32];
int b;
float c;
bool d;
} struct_message;
// Create a struct_message called myData
struct_message myData;
bool tp_state = false;
// callback function that will be executed when data is received
void OnKartMsg(kart_msg* msg) {
tp_state = !tp_state;
digitalWrite(21, tp_state);
Serial.printf("Message\n id: %lu\nTransmission: %lu\n\n", msg->kart_id, msg->transmision);
digitalWrite(RGB_BUILTIN, 1);
delay(100);
digitalWrite(RGB_BUILTIN, 0);
}
void setup() {
// Initialize Serial Monitor
Serial.begin(115200);
randomSeed(analogRead(5));
config.Init();
pinMode(21, OUTPUT);
digitalWrite(21, tp_state);
ledcAttach(1, 256000, 2);
ledcWrite(1, 2); // Sets
pinMode(LED_BUILTIN, OUTPUT);
// TimeSync::Sync()
eth.init();
eth.begin();
kart_receiver.begin();
kart_receiver.registerCallback(OnKartMsg);
web_server.Init();
web_entry_switch.Init(0);
// // Init ESP-NOW
// if (esp_now_init() != ESP_OK) {
// Serial.println("Error initializing ESP-NOW");
// return;
// }
// // Once ESPNow is successfully Init, we will register for recv CB to
// // get recv packer info
// esp_now_register_recv_cb(OnDataRecv);
}
uint8_t led_state = 0;
uint32_t last_led_change = 0;
String speed_in = "";
void loop() {
// Serial.println("loop");
while (Serial.available()) {
char c = (char)Serial.read();
speed_in += c;
if (c == '\n') {
// Process the complete line
Serial.println("Received speed input: " + speed_in);
if (speed_in.toInt() >= 0) {
ledcChangeFrequency(1, speed_in.toInt(), 2);
}
speed_in = "";
}
}
web_entry_switch.Debounce();
if (web_entry_switch.RisingEdge()) {
String response = eth.get("http://example.com");
Serial.println("GET Response: " + response);
}
if (web_server.isRunning()) {
if (millis() - last_led_change > 500) {
led_state ^= 1;
digitalWrite(RGB_BUILTIN, led_state);
last_led_change = millis();
}
if (web_entry_switch.RisingEdge()) {
Serial.println("Web server toggled");
web_server.Stop();
kart_receiver.begin();
digitalWrite(RGB_BUILTIN, 0);
}
} else {
if (web_entry_switch.Pressed() && web_entry_switch.TimeHeldMs() > 3000) {
Serial.println("Web server started");
kart_receiver.end();
web_server.Start();
}
// digitalWrite(LED_BUILTIN, 0);
}
delay(1);
}