Added basic WebSocket Functionality

This commit is contained in:
2025-07-12 18:14:40 +03:00
parent 516eeab751
commit c1fa1d5e57
27 changed files with 138489 additions and 906 deletions

17
.vscode/arduino.json vendored
View File

@@ -1,17 +0,0 @@
{
"port": "",
"configuration": "",
"output": "build",
"board": "",
"programmer": "",
"useProgrammer": false,
"configurationRequired": false,
"monitorPortSettings": {
"port": "",
"baudRate": 115200,
"lineEnding": "\r\n",
"dataBits": 8,
"parity": "none",
"stopBits": "one"
}
}

View File

@@ -1,21 +0,0 @@
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**",
"C:/Users/Admin/Documents/Arduino/libraries/**"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"compilerPath": null,
"cStandard": "c17",
"cppStandard": "c++14",
"intelliSenseMode": "windows-clang-x64"
}
],
"version": 4
}

View File

@@ -1,4 +0,0 @@
{
"C_Cpp.default.compilerPath": "",
"C_Cpp.errorSquiggles": "disabled"
}

View File

@@ -1,83 +0,0 @@
/*KC868-A6 DS1307 CODE*/
// Date and time functions using a DS1307 RTC connected via I2C and Wire lib
#include "RTClib.h"
RTC_DS1307 rtc;
char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
void setup () {
Serial.begin(57600);
#ifndef ESP8266
while (!Serial); // wait for serial port to connect. Needed for native USB
#endif
if (! rtc.begin()) {
Serial.println("Couldn't find RTC");
Serial.flush();
while (1) delay(10);
}
if (! rtc.isrunning()) {
Serial.println("RTC is NOT running, let's set the time!");
// When time needs to be set on a new device, or after a power loss, the
// following line sets the RTC to the date & time this sketch was compiled
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
// This line sets the RTC with an explicit date & time, for example to set
// January 21, 2014 at 3am you would call:
// rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
}
// When time needs to be re-set on a previously configured device, the
// following line sets the RTC to the date & time this sketch was compiled
// rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
// This line sets the RTC with an explicit date & time, for example to set
// January 21, 2014 at 3am you would call:
// rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
}
void loop () {
DateTime now = rtc.now();
Serial.print(now.year(), DEC);
Serial.print('/');
Serial.print(now.month(), DEC);
Serial.print('/');
Serial.print(now.day(), DEC);
Serial.print(" (");
Serial.print(daysOfTheWeek[now.dayOfTheWeek()]);
Serial.print(") ");
Serial.print(now.hour(), DEC);
Serial.print(':');
Serial.print(now.minute(), DEC);
Serial.print(':');
Serial.print(now.second(), DEC);
Serial.println();
Serial.print(" since midnight 1/1/1970 = ");
Serial.print(now.unixtime());
Serial.print("s = ");
Serial.print(now.unixtime() / 86400L);
Serial.println("d");
// calculate a date which is 7 days, 12 hours, 30 minutes, and 6 seconds into the future
DateTime future (now + TimeSpan(7,12,30,6));
Serial.print(" now + 7d + 12h + 30m + 6s: ");
Serial.print(future.year(), DEC);
Serial.print('/');
Serial.print(future.month(), DEC);
Serial.print('/');
Serial.print(future.day(), DEC);
Serial.print(' ');
Serial.print(future.hour(), DEC);
Serial.print(':');
Serial.print(future.minute(), DEC);
Serial.print(':');
Serial.print(future.second(), DEC);
Serial.println();
Serial.println();
delay(3000);
}

View File

@@ -1,48 +0,0 @@
#include "Arduino.h"
#include "PCF8574.h"
// Set i2c address
PCF8574 pcf8574(0x22,4,15);
unsigned long timeElapsed;
void setup()
{
Serial.begin(115200);
delay(1000);
pcf8574.pinMode(P0, INPUT);
pcf8574.pinMode(P1, INPUT);
pcf8574.pinMode(P2, INPUT);
pcf8574.pinMode(P3, INPUT);
pcf8574.pinMode(P4, INPUT);
pcf8574.pinMode(P5, INPUT);
pcf8574.pinMode(P6, INPUT);
pcf8574.pinMode(P7, INPUT);
Serial.print("Init pcf8574...");
if (pcf8574.begin()){
Serial.println("OK");
}else{
Serial.println("KO");
}
}
void loop()
{
uint8_t val1 = pcf8574.digitalRead(P0);
uint8_t val2 = pcf8574.digitalRead(P1);
uint8_t val3 = pcf8574.digitalRead(P2);
uint8_t val4 = pcf8574.digitalRead(P3);
uint8_t val5 = pcf8574.digitalRead(P4);
uint8_t val6 = pcf8574.digitalRead(P5);
uint8_t val7 = pcf8574.digitalRead(P6);
uint8_t val8 = pcf8574.digitalRead(P7);
if (val1==LOW) Serial.println("KEY1 PRESSED");
if (val2==LOW) Serial.println("KEY2 PRESSED");
if (val3==LOW) Serial.println("KEY3 PRESSED");
if (val4==LOW) Serial.println("KEY4 PRESSED");
if (val5==LOW) Serial.println("KEY5 PRESSED");
if (val6==LOW) Serial.println("KEY6 PRESSED");
if (val7==LOW) Serial.println("KEY7 PRESSED");
if (val8==LOW) Serial.println("KEY8 PRESSED");
delay(300);
}

View File

@@ -1,58 +0,0 @@
/*
Blink led on PIN0
by Mischianti Renzo <http://www.mischianti.org>
https://www.mischianti.org/2019/01/02/pcf8574-i2c-digital-i-o-expander-fast-easy-usage/
*/
#include "Arduino.h"
#include "PCF8574.h"
// Set i2c address
PCF8574 pcf8574(0x24,4,15);
void setup()
{
Serial.begin(115200);
// delay(1000);
// Set pinMode to OUTPUT
pcf8574.pinMode(P0, OUTPUT);
pcf8574.pinMode(P1, OUTPUT);
pcf8574.pinMode(P2, OUTPUT);
pcf8574.pinMode(P3, OUTPUT);
pcf8574.pinMode(P4, OUTPUT);
pcf8574.pinMode(P5, OUTPUT);
pcf8574.digitalWrite(P0, HIGH);
pcf8574.digitalWrite(P1, HIGH);
pcf8574.digitalWrite(P2, HIGH);
pcf8574.digitalWrite(P3, HIGH);
pcf8574.digitalWrite(P4, HIGH);
pcf8574.digitalWrite(P5, HIGH);
Serial.print("Init pcf8574...");
if (pcf8574.begin()){
Serial.println("OK");
}else{
Serial.println("KO");
}
}
void loop()
{
delay(300);
pcf8574.digitalWrite(P0, LOW);
delay(300);
pcf8574.digitalWrite(P1, LOW);
delay(300);
pcf8574.digitalWrite(P2, LOW);
delay(300);
pcf8574.digitalWrite(P3, LOW);
delay(300);
pcf8574.digitalWrite(P4, LOW);
delay(300);
pcf8574.digitalWrite(P5, LOW);
delay(300);
}

View File

@@ -1,83 +0,0 @@
/*KC868-A6 DS1307 CODE*/
// Date and time functions using a DS1307 RTC connected via I2C and Wire lib
#include "RTClib.h"
RTC_DS1307 rtc;
char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
void setup () {
Serial.begin(57600);
#ifndef ESP8266
while (!Serial); // wait for serial port to connect. Needed for native USB
#endif
if (! rtc.begin()) {
Serial.println("Couldn't find RTC");
Serial.flush();
while (1) delay(10);
}
if (! rtc.isrunning()) {
Serial.println("RTC is NOT running, let's set the time!");
// When time needs to be set on a new device, or after a power loss, the
// following line sets the RTC to the date & time this sketch was compiled
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
// This line sets the RTC with an explicit date & time, for example to set
// January 21, 2014 at 3am you would call:
// rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
}
// When time needs to be re-set on a previously configured device, the
// following line sets the RTC to the date & time this sketch was compiled
// rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
// This line sets the RTC with an explicit date & time, for example to set
// January 21, 2014 at 3am you would call:
// rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
}
void loop () {
DateTime now = rtc.now();
Serial.print(now.year(), DEC);
Serial.print('/');
Serial.print(now.month(), DEC);
Serial.print('/');
Serial.print(now.day(), DEC);
Serial.print(" (");
Serial.print(daysOfTheWeek[now.dayOfTheWeek()]);
Serial.print(") ");
Serial.print(now.hour(), DEC);
Serial.print(':');
Serial.print(now.minute(), DEC);
Serial.print(':');
Serial.print(now.second(), DEC);
Serial.println();
Serial.print(" since midnight 1/1/1970 = ");
Serial.print(now.unixtime());
Serial.print("s = ");
Serial.print(now.unixtime() / 86400L);
Serial.println("d");
// calculate a date which is 7 days, 12 hours, 30 minutes, and 6 seconds into the future
DateTime future (now + TimeSpan(7,12,30,6));
Serial.print(" now + 7d + 12h + 30m + 6s: ");
Serial.print(future.year(), DEC);
Serial.print('/');
Serial.print(future.month(), DEC);
Serial.print('/');
Serial.print(future.day(), DEC);
Serial.print(' ');
Serial.print(future.hour(), DEC);
Serial.print(':');
Serial.print(future.minute(), DEC);
Serial.print(':');
Serial.print(future.second(), DEC);
Serial.println();
Serial.println();
delay(3000);
}

View File

@@ -1,6 +0,0 @@
void setup() {
}
void loop() {
}

View File

@@ -1,17 +0,0 @@
{
"port": "COM8",
"configuration": "UploadSpeed=921600,CPUFreq=240,FlashFreq=80,FlashMode=qio,FlashSize=4M,PartitionScheme=default,DebugLevel=none,PSRAM=disabled,LoopCore=1,EventsCore=1,EraseFlash=none,JTAGAdapter=default,ZigbeeMode=default",
"output": "build",
"board": "esp32:esp32:esp32",
"programmer": "",
"useProgrammer": false,
"configurationRequired": true,
"monitorPortSettings": {
"port": "COM8",
"baudRate": 115200,
"lineEnding": "\r\n",
"dataBits": 8,
"parity": "none",
"stopBits": "one"
}
}

View File

@@ -1,101 +0,0 @@
{
"configurations": [
{
"name": "Arduino",
"includePath": [
"C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\cores\\esp32/**",
"C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\variants\\esp32/**",
"C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\libraries\\WiFi\\src/**",
"C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\libraries\\Network\\src/**",
"C:\\Users\\espi_\\Documents\\Arduino\\libraries\\AsyncMqttClient\\src/**",
"C:\\Users\\espi_\\Documents\\Arduino\\libraries\\Async_TCP\\src/**",
"C:\\Users\\espi_\\Documents\\Arduino\\libraries\\ArduinoJson\\src/**",
"C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\libraries\\FS\\src/**",
"C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\libraries\\SPIFFS\\src/**",
"C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\libraries\\Wire\\src/**",
"C:\\Users\\espi_\\Documents\\Arduino\\libraries\\Adafruit_PCF8574/**",
"C:\\Users\\espi_\\Documents\\Arduino\\libraries\\Adafruit_BusIO/**",
"C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\libraries\\SPI\\src/**",
"C:\\Users\\espi_\\Documents\\Arduino\\libraries/**",
"C:\\Users\\espi_\\Documents\\Arduino\\4. Bell Systems\\1. Main Projects\\Project - Vesper/**",
"C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\tools\\esp32-arduino-libs\\idf-release_v5.3-cfea4f7c-v1\\esp32\\include/**"
],
"forcedInclude": [
"C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\cores\\esp32\\Arduino.h"
],
"compilerPath": "C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\tools\\esp-x32\\2405/bin/xtensa-esp32-elf-g++",
"compilerArgs": [
"C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\tools\\esp-x32\\2405/bin/xtensa-esp32-elf-g++",
"-MMD",
"-c",
"@C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\tools\\esp32-arduino-libs\\idf-release_v5.3-cfea4f7c-v1\\esp32/flags/cpp_flags",
"-w",
"-Os",
"-Werror=return-type",
"-DF_CPU=240000000L",
"-DARDUINO=10607",
"-DARDUINO_ESP32_DEV",
"-DARDUINO_ARCH_ESP32",
"-DARDUINO_BOARD=\"ESP32_DEV\"",
"-DARDUINO_VARIANT=\"esp32\"",
"-DARDUINO_PARTITION_default",
"-DARDUINO_HOST_OS=\"windows\"",
"-DARDUINO_FQBN=\"esp32:esp32:esp32:UploadSpeed=921600,CPUFreq=240,FlashFreq=80,FlashMode=qio,FlashSize=4M,PartitionScheme=default,DebugLevel=none,PSRAM=disabled,LoopCore=1,EventsCore=1,EraseFlash=none,JTAGAdapter=default,ZigbeeMode=default\"",
"-DESP32=ESP32",
"-DCORE_DEBUG_LEVEL=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1",
"-DARDUINO_USB_CDC_ON_BOOT=0",
"@C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\tools\\esp32-arduino-libs\\idf-release_v5.3-cfea4f7c-v1\\esp32/flags/defines",
"-IC:\\Users\\espi_\\Documents\\Arduino\\4. Bell Systems\\1. Main Projects\\Project - Vesper",
"-iprefix",
"C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\tools\\esp32-arduino-libs\\idf-release_v5.3-cfea4f7c-v1\\esp32/include/",
"@C:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\tools\\esp32-arduino-libs\\idf-release_v5.3-cfea4f7c-v1\\esp32/flags/includes",
"-IC:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\tools\\esp32-arduino-libs\\idf-release_v5.3-cfea4f7c-v1\\esp32/qio_qspi/include",
"-IC:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\cores\\esp32",
"-IC:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\variants\\esp32",
"-IC:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\libraries\\WiFi\\src",
"-IC:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\libraries\\Network\\src",
"-IC:\\Users\\espi_\\Documents\\Arduino\\libraries\\AsyncMqttClient\\src",
"-IC:\\Users\\espi_\\Documents\\Arduino\\libraries\\Async_TCP\\src",
"-IC:\\Users\\espi_\\Documents\\Arduino\\libraries\\ArduinoJson\\src",
"-IC:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\libraries\\FS\\src",
"-IC:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\libraries\\SPIFFS\\src",
"-IC:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\libraries\\Wire\\src",
"-IC:\\Users\\espi_\\Documents\\Arduino\\libraries\\Adafruit_PCF8574",
"-IC:\\Users\\espi_\\Documents\\Arduino\\libraries\\Adafruit_BusIO",
"-IC:\\Users\\espi_\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\3.1.1\\libraries\\SPI\\src",
"@C:\\Users\\espi_\\Documents\\Arduino\\4. Bell Systems\\1. Main Projects\\Project - Vesper\\build/build_opt.h",
"@C:\\Users\\espi_\\Documents\\Arduino\\4. Bell Systems\\1. Main Projects\\Project - Vesper\\build/file_opts",
"C:\\Users\\espi_\\Documents\\Arduino\\4. Bell Systems\\1. Main Projects\\Project - Vesper\\build\\sketch\\Project - Vesper.ino.cpp",
"-o",
"C:\\Users\\espi_\\Documents\\Arduino\\4. Bell Systems\\1. Main Projects\\Project - Vesper\\build\\sketch\\Project - Vesper.ino.cpp.o"
],
"defines": [
"F_CPU=240000000L",
"ARDUINO=10607",
"ARDUINO_ESP32_DEV",
"ARDUINO_ARCH_ESP32",
"ARDUINO_BOARD=\"ESP32_DEV\"",
"ARDUINO_VARIANT=\"esp32\"",
"ARDUINO_PARTITION_default",
"ARDUINO_HOST_OS=\"windows\"",
"ARDUINO_FQBN=\"esp32:esp32:esp32:UploadSpeed=921600,CPUFreq=240,FlashFreq=80,FlashMode=qio,FlashSize=4M,PartitionScheme=default,DebugLevel=none,PSRAM=disabled,LoopCore=1,EventsCore=1,EraseFlash=none,JTAGAdapter=default,ZigbeeMode=default\"",
"ESP32=ESP32",
"CORE_DEBUG_LEVEL=0",
"ARDUINO_RUNNING_CORE=1",
"ARDUINO_EVENT_RUNNING_CORE=1",
"ARDUINO_USB_CDC_ON_BOOT=0",
"ARDUINO_BOARD=\\\"ESP32_DEV\\\"\"",
"ARDUINO_VARIANT=\\\"esp32\\\"\"",
"ARDUINO_HOST_OS=\\\"windows\\\"\"",
"ARDUINO_FQBN=\\\"esp32:esp32:esp32:UploadSpeed=921600,CPUFreq=240,FlashFreq=80,FlashMode=qio,FlashSize=4M,PartitionScheme=default,DebugLevel=none,PSRAM=disabled,LoopCore=1,EventsCore=1,EraseFlash=none,JTAGAdapter=default,ZigbeeMode=default\\\"\"",
"ARDUINO_CORE_BUILD",
"USBCON"
],
"cStandard": "c17",
"cppStandard": "c++17"
}
],
"version": 4
}

View File

@@ -16,30 +16,45 @@ JsonDocument handleJSON(char * payload) {
return doc; return doc;
} }
void replyOnWebSocket(AsyncWebSocketClient *client, String list) {
client->text(list);
}
// Handles the JSON Commands
void handleCommand(JsonDocument json, AsyncWebSocketClient *client = nullptr){
String cmd = json["cmd"];
JsonVariant contents = json["contents"];
if (cmd == "playback") {
player.command(contents);
} else if (cmd == "set_melody") {
player.setMelodyAttributes(contents);
player.loadMelodyInRAM(melody_steps);
} else if (cmd == "list_melodies") {
String list = listFilesAsJson("/melodies");
PublishMqtt(list.c_str());
if (client) {
replyOnWebSocket(client, list); // Only reply via WS if client exists
}
} else if (cmd == "set_relay_timers") {
updateRelayTimings(contents);
} else if (cmd == "add_melody"){
addMelody(contents);
} else {
LOG_WARNING("Unknown Command Received");
}
}
// Subscribes to certain topics on the MQTT Server. // Subscribes to certain topics on the MQTT Server.
void SuscribeMqtt() { void SuscribeMqtt() {
String topicPlayback = String("vesper/") + DEV_ID + "/control/playback"; String command = String("vesper/") + DEV_ID + "/control";
String topicSetMelody = String("vesper/") + DEV_ID + "/control/setMelody"; uint16_t command_id = mqttClient.subscribe(command.c_str(), 2);
String topicAddMelody = String("vesper/") + DEV_ID + "/control/addMelody"; LOG_INFO("Subscribing to Command topic, QoS 2, packetId: %d", command_id);
String topicAddSchedule = String("vesper/") + DEV_ID + "/control/addSchedule";
String topicRelayTimers = String("vesper/") + DEV_ID + "/control/settings/relayTimers";
uint16_t control_id = mqttClient.subscribe(topicPlayback.c_str(), 2);
LOG_INFO("Subscribing to Playback Control topic, QoS 2, packetId: %d", control_id);
uint16_t set_melody_id = mqttClient.subscribe(topicSetMelody.c_str(), 2);
LOG_INFO("Subscribing to Set-Melody topic, QoS 2, packetId: %d", set_melody_id);
// doesn't work yet:
uint16_t add_melody_id = mqttClient.subscribe(topicAddMelody.c_str(), 2);
LOG_INFO("Subscribing to Add-Melody topic, QoS 2, packetId: %d", add_melody_id);
uint16_t add_schedule_id = mqttClient.subscribe(topicAddSchedule.c_str(), 2);
LOG_INFO("Subscribing to Add-Schedule topic, QoS 2, packetId: %d", add_schedule_id);
uint16_t relay_timers_id = mqttClient.subscribe(topicRelayTimers.c_str(), 2);
LOG_INFO("Subscribing to Relay-Timers topic, QoS 2, packetId: %d", relay_timers_id);
} }
@@ -47,41 +62,36 @@ void SuscribeMqtt() {
// Could move logic out of this into a dedicated function. // Could move logic out of this into a dedicated function.
void OnMqttReceived(char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { void OnMqttReceived(char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
String topicPlayback = String("vesper/") + DEV_ID + "/control/playback"; String command = String("vesper/") + DEV_ID + "/control";
String topicSetMelody = String("vesper/") + DEV_ID + "/control/setMelody";
String topicAddMelody = String("vesper/") + DEV_ID + "/control/addMelody";
String topicAddSchedule = String("vesper/") + DEV_ID + "/control/addSchedule";
String topicRelayTimers = String("vesper/") + DEV_ID + "/control/settings/relayTimers";
// Don't know what this is. Check it out later. // Don't know what this is. Check it out later.
//String payloadContent = String(payload).substring(0, len); //String payloadContent = String(payload).substring(0, len);
if (String(topic) == topicPlayback){ if (String(topic) == command){
player.command(payload); JsonDocument json = handleJSON(payload);
handleCommand(json);
} }
}
// Handles incoming WebSocket messages on subscribed topics.
// Could move logic out of this into a dedicated function.
void onWebSocketReceived(AsyncWebSocketClient *client, void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = '\0'; // Null-terminate the received data
Serial.printf("Received message: %s\n", (char*)data);
JsonDocument json = handleJSON((char*)data);
handleCommand(json, client);
else if (String(topic) == topicSetMelody) {
player.setMelodyAttributes(handleJSON(payload));
player.loadMelodyInRAM(melody_steps);
} }
}
else if (String(topic) == topicAddMelody) {
// Handle adding melody
LOG_INFO("Adding melody...");
// You can call a function here to handle adding the melody
}
else if (String(topic) == topicAddSchedule) { void onWebSocketEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
updateSchedule(handleJSON(payload)); if (type == WS_EVT_DATA) {
} onWebSocketReceived(client, arg, data, len);
else if (String(topic) == topicRelayTimers) {
updateRelayTimings(handleJSON(payload));
}
else {
// Handle unknown topics
LOG_WARNING("Unknown topic received.");
} }
} }

View File

@@ -12,21 +12,17 @@ String GetPayloadContent(char * data, size_t len) {
return content; return content;
} }
void ConnectToMqtt() void ConnectToMqtt() {
{
LOG_INFO("Connecting to MQTT..."); LOG_INFO("Connecting to MQTT...");
mqttClient.connect(); mqttClient.connect();
} }
void OnMqttConnect(bool sessionPresent) void OnMqttConnect(bool sessionPresent) {
{
LOG_INFO("Connected to MQTT."); LOG_INFO("Connected to MQTT.");
//LOG_INFO("Session present: %s", sessionPresent ? "Yes":"No");
SuscribeMqtt(); SuscribeMqtt();
} }
void OnMqttDisconnect(AsyncMqttClientDisconnectReason reason) void OnMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
{
LOG_WARNING("Disconnected from MQTT."); LOG_WARNING("Disconnected from MQTT.");
if(WiFi.isConnected()) if(WiFi.isConnected())
@@ -35,51 +31,44 @@ void OnMqttDisconnect(AsyncMqttClientDisconnectReason reason)
} }
} }
void OnMqttSubscribe(uint16_t packetId, uint8_t qos) void OnMqttSubscribe(uint16_t packetId, uint8_t qos) {
{
LOG_INFO("Subscribe acknowledged. PacketID: %d / QoS: %d", packetId, qos); LOG_INFO("Subscribe acknowledged. PacketID: %d / QoS: %d", packetId, qos);
} }
void OnMqttUnsubscribe(uint16_t packetId) void OnMqttUnsubscribe(uint16_t packetId) {
{
LOG_INFO("Unsubscribe Acknowledged. PacketID: %d",packetId); LOG_INFO("Unsubscribe Acknowledged. PacketID: %d",packetId);
} }
void OnMqttPublish(uint16_t packetId) void OnMqttPublish(uint16_t packetId) {
{
LOG_INFO("Publish Acknowledged. PacketID: %d", packetId); LOG_INFO("Publish Acknowledged. PacketID: %d", packetId);
} }
void ConnectWiFi_STA(bool useStaticIP = false) void ConnectWiFi_STA(bool useStaticIP = true) {
{
Serial.println("");
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
if(useStaticIP) { if(useStaticIP) {
WiFi.config(ip, gateway, subnet); WiFi.config(ip, gateway, subnet);
WiFi.setHostname(hostname); WiFi.setHostname(hostname);
} }
WiFi.begin(ssid, password); //WiFi.begin(ssid, password);
WiFi.begin();
while (WiFi.status() != WL_CONNECTED) { while (WiFi.status() != WL_CONNECTED) {
delay(100); delay(10);
Serial.print('.');
} }
if (LOG_LEVEL_ENABLED(LOG_LEVEL_INFO)){ if (LOG_LEVEL_ENABLED(LOG_LEVEL_INFO)){
Serial.println(""); Serial.println("");
Serial.print("Initiating STA:\t"); Serial.print("NIGGA - Initiating STA:\t");
Serial.println(ssid); Serial.println(ssid);
Serial.print("IP address:\t"); Serial.print("IP address:\t");
Serial.println(WiFi.localIP()); Serial.println(WiFi.localIP());
} }
} }
void ConnectWiFi_AP(bool useStaticIP = false) void ConnectWiFi_AP(bool useStaticIP = false) {
{
Serial.println(""); Serial.println("");
WiFi.mode(WIFI_AP); WiFi.mode(WIFI_AP);
while(!WiFi.softAP(ssid, password)) while(!WiFi.softAP(ssid, password)) {
{
Serial.println("."); Serial.println(".");
delay(100); delay(100);
} }
@@ -95,26 +84,53 @@ void ConnectWiFi_AP(bool useStaticIP = false)
} }
} }
void WiFiEvent(WiFiEvent_t event) void NetworkEvent(arduino_event_id_t event, arduino_event_info_t info) {
{ LOG_INFO("(NET) event: %d\n", event);
LOG_INFO("[WiFi-event] event: %d\n", event); IPAddress ip = WiFi.localIP();
switch(event) switch(event)
{ {
case ARDUINO_EVENT_WIFI_STA_GOT_IP: case ARDUINO_EVENT_WIFI_STA_GOT_IP:
Serial.print("[INFO] - WiFi connected. IP Address: "); LOG_DEBUG("WiFi connected. IP Address: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
Serial.println(WiFi.localIP()); //xTimerStop(wifiReconnectTimer, 0);
ConnectToMqtt(); ConnectToMqtt();
break; break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
LOG_WARNING("WiFi Lost Connection! :()"); LOG_WARNING("WiFi Lost Connection! :(");
xTimerStop(mqttReconnectTimer, 0); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi xTimerStop(mqttReconnectTimer, 0);
xTimerStart(wifiReconnectTimer, 0); xTimerStart(wifiReconnectTimer, 0);
break; break;
case ARDUINO_EVENT_ETH_START:
LOG_DEBUG("ETH Started");
ETH.setHostname(hostname);
break;
case ARDUINO_EVENT_ETH_CONNECTED:
LOG_DEBUG("ETH Connected !");
break;
case ARDUINO_EVENT_ETH_GOT_IP:
LOG_INFO("ETH Got IP: '%s'\n", esp_netif_get_desc(info.got_ip.esp_netif));
WiFi.disconnect(true);
ConnectToMqtt();
break;
case ARDUINO_EVENT_ETH_LOST_IP:
LOG_WARNING("ETH Lost IP");
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
LOG_WARNING("ETH Disconnected");
xTimerStop(mqttReconnectTimer, 0);
xTimerStart(wifiReconnectTimer, 0);
break;
case ARDUINO_EVENT_ETH_STOP:
LOG_INFO("ETH Stopped");
xTimerStop(mqttReconnectTimer, 0);
xTimerStart(wifiReconnectTimer, 0);
break;
default:
break;
} }
} }
void InitMqtt() void InitMqtt() {
{
mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(ConnectToMqtt)); mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(ConnectToMqtt));
wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(5000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(ConnectWiFi_STA)); wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(5000), pdFALSE, (void*)0, reinterpret_cast<TimerCallbackFunction_t>(ConnectWiFi_STA));

View File

@@ -1,17 +0,0 @@
#pragma once
// Timed Function. Will Run ONCE per 10 sec
void schedule_timer() {
// Tick the calendar to perform internal functions.
// (Updates current time. Updates Daily Schedule if its Midnight)
timekeeper.tick();
timekeeper.checkAndRunSchedule(player);
// This is here for debugging purposes only.
timekeeper.printTimeNow();
}
// void dailyScheduleCheck(std::vector<ScheduleEntry>){
// }

View File

@@ -54,7 +54,6 @@ void relayControlTask(void *param) {
} }
} }
void loop_playback(std::vector<uint16_t> &melody_steps) { void loop_playback(std::vector<uint16_t> &melody_steps) {
while(player.isPlaying && !player.isPaused){ while(player.isPlaying && !player.isPaused){
@@ -72,7 +71,6 @@ void loop_playback(std::vector<uint16_t> &melody_steps) {
} }
} }
// Function to activate relays for a specific note // Function to activate relays for a specific note
void itsHammerTime(uint16_t note) { void itsHammerTime(uint16_t note) {
uint64_t now = millis(); uint64_t now = millis();

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -13,7 +13,7 @@ public:
uint32_t loop_duration = 0; // Duration of the playback per segment uint32_t loop_duration = 0; // Duration of the playback per segment
uint32_t interval = 0; // Indicates the Duration of the Interval between finished segments, IF "inf" is true uint32_t interval = 0; // Indicates the Duration of the Interval between finished segments, IF "inf" is true
bool infinite_play = false; // Infinite Loop Indicator (If True the melody will loop forever or until stoped, with pauses of "interval" in between loops) bool infinite_play = false; // Infinite Loop Indicator (If True the melody will loop forever or until stoped, with pauses of "interval" in between loops)
bool isPlaying = false;; // Indicates if the Melody is actually Playing right now. bool isPlaying = false; // Indicates if the Melody is actually Playing right now.
bool isPaused = false; // If playing, indicates if the Melody is Paused bool isPaused = false; // If playing, indicates if the Melody is Paused
uint64_t startTime = 0; // The time-point the Melody started Playing uint64_t startTime = 0; // The time-point the Melody started Playing
uint64_t loopStartTime = 0; // The time-point the current segment started Playing uint64_t loopStartTime = 0; // The time-point the current segment started Playing
@@ -51,19 +51,20 @@ public:
} }
// Handles Incoming Commands to PLAY or STOP // Handles Incoming Commands to PLAY or STOP
void command(char * command){ void command(JsonVariant content){
LOG_DEBUG("Incoming Command: %s",command); String action = content["action"];
if (command[0] == '1') { LOG_DEBUG("Incoming Command: %s", action);
if (action == "play") {
play(); play();
PublishMqtt("OK - PLAY"); PublishMqtt("OK - PLAY");
} else if (command[0] == '0') { } else if (action == "stop") {
forceStop(); forceStop();
PublishMqtt("OK - STOP"); PublishMqtt("OK - STOP");
} }
} }
// Sets incoming Attributes for the Melody, into the class' variables. // Sets incoming Attributes for the Melody, into the class' variables.
void setMelodyAttributes(JsonDocument doc){ void setMelodyAttributes(JsonVariant doc){
if (doc.containsKey("name")) { if (doc.containsKey("name")) {
name = doc["name"].as<const char*>(); name = doc["name"].as<const char*>();
@@ -98,229 +99,35 @@ public:
); );
} }
// Loads the Selected melody from a .bin file, into RAM
// Loads the Selected melody from a .bin file on SD into RAM
void loadMelodyInRAM(std::vector<uint16_t> &melody_steps) { void loadMelodyInRAM(std::vector<uint16_t> &melody_steps) {
String filePath = "/melodies/" + String(name.c_str()) + ".bin";
LOG_INFO("Loading Melody."); File bin_file = SD.open(filePath.c_str(), FILE_READ);
std::string filePath = "/" + name + ".bin";
File bin_file = SPIFFS.open(filePath.c_str(), "r");
if (!bin_file) { if (!bin_file) {
LOG_ERROR("Failed to Open File"); LOG_ERROR("Failed to open file: %s", filePath.c_str());
return; return;
} }
size_t fileSize = bin_file.size(); size_t fileSize = bin_file.size();
size_t steps = fileSize / 2; if (fileSize % 2 != 0) {
melody_steps.resize(steps); LOG_ERROR("Invalid file size: %u (not a multiple of 2)", fileSize);
LOG_DEBUG("Opened File, size: %zu - Steps: %zu",fileSize,steps)
for (size_t i=0; i<steps; i++){
melody_steps[i] = bin_file.read() << 8 | bin_file.read();
LOG_DEBUG("Current Step: %03d // HEX Value: 0x%04X", i, melody_steps[i]);
}
LOG_DEBUG("Closing File");
bin_file.close(); bin_file.close();
// closing the file
}
};
class TimeKeeper {
private:
// holds the daily scheduled melodies
struct ScheduleEntry {
std::string mel;
uint8_t hour;
uint8_t minute;
uint16_t speed;
uint16_t duration;
};
struct TimeNow {
uint8_t hour;
uint8_t minute;
uint8_t second;
};
std::vector<ScheduleEntry> dailySchedule;
TimeNow now;
// update to current time
void updateTime(){
now.hour = getHour();
now.minute = getMinute();
now.second = getSecond();
}
// Read the current Month's JSON file and add the daily entries to the dailySchedule
void loadDailySchedule() {
std::string filePath = "/" + getMonth() + ".json";
File file = SPIFFS.open(filePath.c_str(), "r");
if (!file) {
LOG_ERROR("Failed to open file: %s\n", filePath.c_str());
return; return;
} }
LOG_INFO("Opened daily schedule file"); melody_steps.resize(fileSize / 2);
StaticJsonDocument<8192> doc; // Adjust size based on expected JSON complexity
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error) { for (size_t i = 0; i < melody_steps.size(); i++) {
LOG_ERROR("Failed to parse JSON: %s\n", error.c_str()); uint8_t high = bin_file.read();
return; uint8_t low = bin_file.read();
melody_steps[i] = (high << 8) | low;
} }
if (LOG_LEVEL_ENABLED(LOG_LEVEL_DEBUG)){ LOG_INFO("Melody Load Successful");
String jsonString;
serializeJsonPretty(doc, jsonString);
LOG_DEBUG("Serialized JSON: /n%s",jsonString.c_str());
}
// Extract entries for the current day bin_file.close();
std::string currentDay = getDay();
dailySchedule.clear(); // Clear previous day's schedule
LOG_DEBUG("Current Day: %s\n", currentDay.c_str());
if (doc.containsKey(currentDay.c_str())) {
LOG_DEBUG("Doc contains key!");
// Check if the current day contains a single object or an array
JsonVariant dayVariant = doc[currentDay.c_str()];
if (dayVariant.is<JsonArray>()) {
// Handle case where the day contains an array of entries
JsonArray dayEntries = dayVariant.as<JsonArray>();
for (JsonObject entry : dayEntries) {
ScheduleEntry schedule;
schedule.mel = entry["name"].as<std::string>();
schedule.hour = entry["hour"];
schedule.minute = entry["minute"];
schedule.speed = entry["speed"];
schedule.duration = entry["duration"];
dailySchedule.push_back(schedule);
LOG_INFO("Added Entry - Melody: %s, Hour: %d, Minute: %d, Speed: %d, Duration: %d\n",
schedule.mel.c_str(), schedule.hour, schedule.minute, schedule.speed, schedule.duration);
}
} else if (dayVariant.is<JsonObject>()) {
// Handle case where the day contains a single object
JsonObject entry = dayVariant.as<JsonObject>();
ScheduleEntry schedule;
schedule.mel = entry["name"].as<std::string>();
schedule.hour = entry["hour"];
schedule.minute = entry["minute"];
schedule.speed = entry["speed"];
schedule.duration = entry["duration"];
dailySchedule.push_back(schedule);
LOG_INFO("Added Single Entry - Melody: %s, Hour: %d, Minute: %d, Speed: %d, Duration: %d\n",
schedule.mel.c_str(), schedule.hour, schedule.minute, schedule.speed, schedule.duration);
} else {
LOG_WARNING("Invalid data format for the current day.");
}
} else {
LOG_INFO("No schedule found for today.");
}
}
// Calls "loadDailySchedule" and returns true ONCE per new day (00:00)
bool newDayCheck() {
// Static variable to store whether we've already detected the new day
static bool alreadyTriggered = false;
// Check if it's midnight
if (now.hour == 0 && now.minute == 0) {
if (!alreadyTriggered) {
// First time at midnight, trigger the event
alreadyTriggered = true;
LOG_DEBUG("New day detected, returning true.");
loadDailySchedule();
return true;
} else {
// It's still midnight, but we've already triggered
LOG_DEBUG("Already triggered for today, returning false.");
return false;
}
} else {
// Reset the trigger after midnight has passed
alreadyTriggered = false;
return false;
}
}
public:
// get date and time data from the RTC module
uint16_t getYear() { return rtc.now().year(); }
std::string getMonth() { return monthName(rtc.now().month()); }
std::string getDay() { return std::to_string(rtc.now().day()); }
uint8_t getHour() { return rtc.now().hour(); }
uint8_t getMinute() { return rtc.now().minute(); }
uint8_t getSecond() { return rtc.now().second(); }
// turn months from decimal to char*
std::string monthName(uint8_t monthNumber) {
const char* monthNames[] = {
"jan", "feb", "mar", "apr", "may", "jun",
"jul", "aug", "sep", "oct", "nov", "dec"
};
return monthNumber >= 1 && monthNumber <= 12 ? monthNames[monthNumber - 1] : "unknown";
}
// Loads and updates the daily schedule
void refreshDailySchedule() { loadDailySchedule(); }
// Returns the daily schedule in "ScheduleEntry" format
const std::vector<ScheduleEntry>& getDailySchedule() const { return dailySchedule; }
// Prints the time, NOW.
void printTimeNow(){
LOG_INFO("Current Time: %s %s, %u - %02u:%02u:%02u\n",
getMonth().c_str(), // Month as a string
getDay().c_str(), // Day as a string
getYear(), // Year as an integer
getHour(), // Hour (24-hour format)
getMinute(), // Minute
getSecond()); // Second
}
void tick(){
updateTime();
newDayCheck();
}
void checkAndRunSchedule(Player & player) {
LOG_DEBUG("Running daily schedule check");
for (auto it = dailySchedule.begin(); it != dailySchedule.end(); ) {
if (now.hour == it->hour && now.minute == it->minute) {
LOG_DEBUG("Entry Exists, Calling program.");
StaticJsonDocument<200> jsonDoc;
jsonDoc["name"] = it->mel.c_str();
jsonDoc["speed"] = it->speed;
jsonDoc["duration"] = it->duration;
jsonDoc["loop_dur"] = 0;
jsonDoc["internal"] = 0;
//LOG_INFO("Entry Found. Name: %s Speed: %d Duration: %d",it->mel.c_str(), it->speed, it->duration);
if (!player.isPlaying){
player.setMelodyAttributes(jsonDoc);
player.loadMelodyInRAM(melody_steps);
player.play();
it = dailySchedule.erase(it);
}
}
else {
LOG_DEBUG("Entry's time doesn't match. Skipping.");
++it;
}
}
} }
}; };

62
vesper/commands.json Normal file
View File

@@ -0,0 +1,62 @@
ADD MELODY:
{
"cmd": "add_melody",
"contents": {
"url": "URL",
"filename": "NAME.EXT"
}
}
SET RELAY TIMERS:
{
"cmd": "set_relay_timers",
"contents": {"b1":100, "b2":200}
}
START PLAYBACK:
{
"cmd": "playback",
"contents": {
"action": "play"
}
}
STOP PLAYBACK:
{
"cmd": "playback",
"contents": {
"action": "stop"
}
}
SET MELODY:
{
"cmd": "set_melody",
"contents": {
"name": "esperinos",
"id": "10",
"duration": 5000,
"infinite": true,
"interval": 1000,
"speed": 200,
"loop_dur": 2000
}
}
LIST MELODIES
{
"cmd": "list_melodies",
}

View File

@@ -1,17 +1,48 @@
const char* ssid = "SmartNet"; #pragma once
const char* password = "smartpass";
#define DEV_ID "PC201504180001"
// Network Config
const char* ssid = "SmartNet"; //Not used with WiFi Manager
const char* password = "smartpass"; //Not used with WiFi Manager
const char* hostname = "ESP32_mqtt_test"; const char* hostname = "ESP32_mqtt_test";
const IPAddress MQTT_HOST(10,98,20,10);
const int MQTT_PORT = 1883;
#define MQTT_USER "esp32_vesper"
#define MQTT_PASS "vesper"
IPAddress ip(10, 98, 30, 150); IPAddress ip(10, 98, 30, 150);
IPAddress gateway(10, 98, 30, 1); IPAddress gateway(10, 98, 30, 1);
IPAddress subnet(255, 255, 255, 0); IPAddress subnet(255, 255, 255, 0);
String ap_ssid = String("BellSystems - ") + DEV_ID;
String ap_pass = "password";
#define DEV_ID "id-96638646" // Version Controll Settings
const char* versionUrl = "http://10.98.20.10:85/version.txt";
const char* firmwareUrl = "http://10.98.20.10:85/firmware.bin";
const float currentVersion = 1.1;
// NTP Config
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 7200;
const int daylightOffset_sec = 3600;
// MQTT Config
const IPAddress MQTT_HOST(10,98,20,10);
const int MQTT_PORT = 1883;
#define MQTT_USER "esp32_vesper"
#define MQTT_PASS "vesper"
// Hardware Configuration
#define PCF8574_ADDR 0x24 #define PCF8574_ADDR 0x24
// SPI W5500 ETHERNET SETUP
#define USE_TWO_ETH_PORTS 0
#ifndef ETH_PHY_CS
#define ETH_PHY_TYPE ETH_PHY_W5500
#define ETH_PHY_ADDR 1
#define ETH_PHY_CS 5
#define ETH_PHY_IRQ -1
#define ETH_PHY_RST -1
#endif
// SPI pins
#define ETH_SPI_SCK 18
#define ETH_SPI_MISO 19
#define ETH_SPI_MOSI 23

View File

@@ -7,10 +7,8 @@ void saveRelayTimings();
// - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Sets Incoming Relay Durations to RAM and then call funtion to save them to file. // Sets Incoming Relay Durations to RAM and then call funtion to save them to file.
void updateRelayTimings(JsonDocument doc) { void updateRelayTimings(JsonVariant doc) {
// Iterate through the relays in the JSON payload // Iterate through the relays in the JSON payload
for (uint8_t i = 0; i < 16; i++) { for (uint8_t i = 0; i < 16; i++) {
String key = String("b") + (i + 1); // Generate "b1", "b2", ... String key = String("b") + (i + 1); // Generate "b1", "b2", ...
@@ -25,16 +23,33 @@ void updateRelayTimings(JsonDocument doc) {
LOG_INFO("Updated Relay Timings.") LOG_INFO("Updated Relay Timings.")
} }
// Save file "fileName" with data: "data" // Save file "filename" with data: "data" to the "dirPath" directory of the SD card
void savefile(const char* fileName, const char* data) { void saveFileToSD(const char* dirPath, const char* filename, const char* data) {
File file = SPIFFS.open(fileName, "w"); // Initialize SD (if not already done)
if (!file) { if (!SD.begin(SD_CS)) {
LOG_ERROR("Failed to open file!"); LOG_ERROR("SD Card not initialized!");
return; return;
} }
// Make sure directory exists
if (!SD.exists(dirPath)) {
SD.mkdir(dirPath);
}
// Build full path
String fullPath = String(dirPath);
if (!fullPath.endsWith("/")) fullPath += "/";
fullPath += filename;
File file = SD.open(fullPath.c_str(), FILE_WRITE);
if (!file) {
LOG_ERROR("Failed to open file: %s", fullPath.c_str());
return;
}
file.print(data); file.print(data);
file.close(); file.close();
LOG_INFO("File %s saved successfully.\n", fileName); LOG_INFO("File %s saved successfully.\n", fullPath.c_str());
} }
// Saves Relay Durations from RAM, into a file // Saves Relay Durations from RAM, into a file
@@ -54,41 +69,30 @@ void saveRelayTimings() {
return; return;
} }
const char * fileName = "/settings/relayTimings.json"; const char * path = "/settings";
savefile(fileName, buffer); const char * filename = "relayTimings.json";
saveFileToSD(path, filename, buffer);
// // Open the file for writing
// File file = SPIFFS.open("/settings/relayTimings.json", "w");
// if (!file) {
// Serial.println("Failed to open file for writing");
// return;
// }
//
// // Serialize JSON to the file
// if (serializeJson(doc, file) == 0) {
// Serial.println("Failed to write JSON to file");
// } else {
// Serial.println("Relay timings saved successfully");
// }
//
// file.close();
} }
// Loads Relay Durations from file into RAM (called during boot) // Loads Relay Durations from file into RAM (called during boot)
void loadRelayTimings() { void loadRelayTimings() {
// Open the file for reading
File file = SPIFFS.open("/settings/relayTimings.json", "r"); if (!SD.begin(SD_CS)) {
if (!file) { LOG_ERROR("SD Card not initialized. Using default relay timings.");
LOG_ERROR("Settings file not found. Using default relay timings.");
return; return;
} }
File file = SD.open("/settings/relayTimings.json", FILE_READ);
if (!file) {
LOG_ERROR("Settings file not found on SD. Using default relay timings.");
return;
}
// Parse the JSON file // Parse the JSON file
StaticJsonDocument<512> doc; // Adjust size if needed StaticJsonDocument<512> doc; // Adjust size if needed
DeserializationError error = deserializeJson(doc, file); DeserializationError error = deserializeJson(doc, file);
if (error) {
LOG_ERROR("Failed to parse settings file. Using default relay timings.");
file.close(); file.close();
if (error) {
LOG_ERROR("Failed to parse settings from SD. Using default relay timings.");
return; return;
} }
@@ -101,28 +105,343 @@ void loadRelayTimings() {
} }
} }
file.close();
} }
// // Function to sync time with NTP server and update RTC
void updateSchedule(JsonDocument doc) { void syncTimeWithNTP() {
const char* fileName = doc["file"]; // Connect to Wi-Fi if not already connected
if (WiFi.status() != WL_CONNECTED) {
LOG_DEBUG("Connecting to Wi-Fi...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
LOG_DEBUG(".");
}
LOG_DEBUG("\nWi-Fi connected!");
}
// Ensure fileName exists and is valid // Configure NTP
if (!fileName || strlen(fileName) == 0) { configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
LOG_ERROR("Invalid JSON payload: Missing or invalid 'file' field");
// Sync time from NTP server
LOG_DEBUG("Syncing time with NTP server...");
struct tm timeInfo;
if (!getLocalTime(&timeInfo)) {
LOG_DEBUG("Failed to obtain time from NTP server!");
return; return;
} }
// Serialize the "data" object into a string // Update RTC with synchronized time
String dataString; rtc.adjust(DateTime(timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
serializeJson(doc["data"], dataString); // Converts the "data" object to a JSON string timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec));
if (dataString.length() > 0) { // Log synchronized time
savefile(fileName, dataString.c_str()); LOG_INFO("Time synced with NTP server.");
LOG_INFO("File '%s' updated successfully.\n", fileName); LOG_DEBUG("Synced time: %02d:%02d:%02d, %02d/%02d/%04d",
} else { timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec,
LOG_DEBUG("Invalid JSON payload: Unable to serialize 'data'"); timeInfo.tm_mday, timeInfo.tm_mon + 1, timeInfo.tm_year + 1900);
}
timekeeper.refreshDailySchedule();
} }
// Call this function with the Firebase URL and desired local filename
bool downloadFileToSD(const String& url, const String& directory, const String& filename) {
LOG_INFO("HTTP Starting download...");
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
LOG_ERROR("(HTTP) GET failed, error: %s\n", http.errorToString(httpCode).c_str());
http.end();
return false;
}
if (!SD.begin(SD_CS)) {
LOG_ERROR("SD Card init failed!");
http.end();
return false;
}
// Ensure the directory ends with '/'
String dirPath = directory;
if (!dirPath.endsWith("/")) dirPath += "/";
// Create directory if it doesn't exist
SD.mkdir(dirPath.c_str());
String fullPath = dirPath + filename;
File file = SD.open(fullPath.c_str(), FILE_WRITE);
if (!file) {
LOG_ERROR("SD Failed to open file for writing: %s", fullPath.c_str());
http.end();
return false;
}
WiFiClient* stream = http.getStreamPtr();
uint8_t buffer[1024];
int bytesRead;
while (http.connected() && (bytesRead = stream->readBytes(buffer, sizeof(buffer))) > 0) {
file.write(buffer, bytesRead);
}
file.close();
http.end();
LOG_INFO("HTTP Download complete, file saved to: %s", fullPath.c_str());
return true;
}
// Returns the list of melodies (the filenames) currently inside the SD Card.
String listFilesAsJson(const char* dirPath) {
if (!SD.begin(SD_CS)) {
LOG_ERROR("SD init failed");
return "{}";
}
File dir = SD.open(dirPath);
if (!dir || !dir.isDirectory()) {
LOG_ERROR("Directory not found");
return "{}";
}
DynamicJsonDocument doc(1024); // Adjust size if needed
JsonArray fileList = doc.createNestedArray("files");
File file = dir.openNextFile();
while (file) {
if (!file.isDirectory()) {
fileList.add(file.name());
}
file = dir.openNextFile();
}
String json;
serializeJson(doc, json);
return json;
}
// Prints the Steps of a Melody from a file using its filename
void printMelodyFile(const String& filename) {
if (!SD.begin(SD_CS)) {
LOG_ERROR("SD Card not initialized.");
return;
}
File file = SD.open("/melodies/" + filename, FILE_READ);
if (!file) {
Serial.println("Failed to open Melody file for reading");
return;
}
Serial.printf("---- Contents of %s ----\n", filename.c_str());
uint16_t step;
int index = 0;
while (file.available() >= 2) {
uint8_t low = file.read();
uint8_t high = file.read();
step = (high << 8) | low;
Serial.printf("Step %5d: 0x%04X (%d)\n", index++, step, step);
}
file.close();
Serial.println("---- End of File ----");
}
// Prints the contents of a Text file using its filename
void printFileAsText(const String& path, const String& filename) {
if (!SD.begin(SD_CS)) {
LOG_ERROR("SD Card not initialized.");
return;
}
String fullPath = path;
if (!fullPath.endsWith("/")) fullPath += "/";
fullPath += filename;
File file = SD.open(fullPath, FILE_READ);
if (!file) {
Serial.println("Failed to open file for reading");
return;
}
Serial.printf("---- Contents of %s ----\n", filename.c_str());
while (file.available()) {
String line = file.readStringUntil('\n');
Serial.println(line);
}
file.close();
Serial.println("---- End of File ----");
}
// Downloads a new melody from HTTP
void addMelody(JsonVariant doc) {
LOG_INFO("Trying Saving...");
const char* url = doc["url"];
const char* filename = doc["filename"];
downloadFileToSD(url, "/melodies", filename);
}
// Checks the onboard SD Card for new firmware
void checkFirmwareUpdate() {
if (!SD.begin(SD_CS)) {
Serial.println("SD init failed");
return;
}
File updateBin = SD.open("/firmware/update.bin");
if (!updateBin) {
Serial.println("No update.bin found");
return;
}
size_t updateSize = updateBin.size();
if (updateSize == 0) {
Serial.println("Empty update file");
updateBin.close();
return;
}
Serial.println("Starting firmware update...");
if (Update.begin(updateSize)) {
size_t written = Update.writeStream(updateBin);
if (written == updateSize) {
Serial.println("Update written successfully");
} else {
Serial.printf("Written only %d/%d bytes\n", written, updateSize);
}
if (Update.end()) {
Serial.println("Update finished!");
if (Update.isFinished()) {
Serial.println("Update complete. Rebooting...");
updateBin.close();
SD.remove("/firmware/update.bin"); // optional cleanup
ESP.restart();
} else {
Serial.println("Update not complete");
}
} else {
Serial.printf("Update error: %s\n", Update.errorString());
}
} else {
Serial.println("Not enough space to begin update");
}
updateBin.close();
}
// UNSUSED FUNCTIONS.
// void startConfigPortal() {
// WiFi.mode(WIFI_AP);
// WiFi.softAP("Device_Config", "12345678");
// Serial.println("AP mode started. Connect to 'Device_Config'.");
// // Serve the configuration page
// server.on("/", HTTP_GET, []() {
// server.send(200, "text/html", generateConfigPageHTML());
// });
// // Handle form submission
// server.on("/save", HTTP_POST, []() {
// ssid = server.arg("ssid");
// password = server.arg("password");
// mqttHost.fromString(server.arg("mqttHost"));
// mqttUser = server.arg("mqttUser");
// mqttPassword = server.arg("mqttPassword");
// saveSettings(); // Save new settings to SPIFFS
// server.send(200, "text/plain", "Settings saved! Rebooting...");
// delay(1000);
// ESP.restart();
// });
// server.begin();
// }
// // Save settings to SPIFFS
// void saveSettings() {
// StaticJsonDocument<512> doc;
// doc["ssid"] = ssid;
// doc["password"] = password;
// doc["mqttHost"] = mqttHost.toString();
// doc["mqttUser"] = mqttUser;
// doc["mqttPassword"] = mqttPassword;
// File configFile = SPIFFS.open(CONFIG_FILE, "w");
// if (!configFile) {
// Serial.println("Failed to open config file for writing.");
// return;
// }
// serializeJson(doc, configFile);
// configFile.close();
// Serial.println("Settings saved to SPIFFS.");
// }
// // Load settings from SPIFFS
// void loadSettings() {
// if (!SPIFFS.exists(CONFIG_FILE)) {
// Serial.println("Config file not found. Using defaults.");
// return;
// }
// File configFile = SPIFFS.open(CONFIG_FILE, "r");
// if (!configFile) {
// Serial.println("Failed to open config file.");
// return;
// }
// StaticJsonDocument<512> doc;
// DeserializationError error = deserializeJson(doc, configFile);
// if (error) {
// Serial.println("Failed to parse config file.");
// return;
// }
// ssid = doc["ssid"].as<String>();
// password = doc["password"].as<String>();
// mqttHost.fromString(doc["mqttHost"].as<String>());
// mqttUser = doc["mqttUser"].as<String>();
// mqttPassword = doc["mqttPassword"].as<String>();
// configFile.close();
// Serial.println("Settings loaded from SPIFFS.");
// }
// // Generate HTML page for configuration
// String generateConfigPageHTML() {
// String page = R"rawliteral(
// <!DOCTYPE html>
// <html>
// <body>
// <h2>Device Configuration</h2>
// <form action="/save" method="POST">
// WiFi SSID: <input type="text" name="ssid" value=")rawliteral" +
// ssid + R"rawliteral("><br>
// WiFi Password: <input type="password" name="password" value=")rawliteral" +
// password + R"rawliteral("><br>
// MQTT Host: <input type="text" name="mqttHost" value=")rawliteral" +
// mqttHost.toString() + R"rawliteral("><br>
// MQTT Username: <input type="text" name="mqttUser" value=")rawliteral" +
// mqttUser + R"rawliteral("><br>
// MQTT Password: <input type="password" name="mqttPassword" value=")rawliteral" +
// mqttPassword + R"rawliteral("><br>
// <input type="submit" value="Save">
// </form>
// </body>
// </html>
// )rawliteral";
// return page;
// }

65
vesper/ota.hpp Normal file
View File

@@ -0,0 +1,65 @@
void checkForUpdates();
void performOTA();
void checkForUpdates() {
Serial.println("Checking for firmware updates...");
// Step 1: Check the current version on the server
HTTPClient http;
http.begin(versionUrl);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
String newVersionStr = http.getString();
float newVersion = newVersionStr.toFloat();
Serial.printf("Current version: %.1f, Available version: %.1f\n", currentVersion, newVersion);
// Step 2: Compare the version
if (newVersion > currentVersion) {
Serial.println("New version available. Starting update...");
performOTA(); // Perform the OTA update if a new version is found
} else {
Serial.println("No new version available.");
}
} else {
Serial.printf("Failed to retrieve version. HTTP error code: %d\n", httpCode);
}
http.end();
}
void performOTA() {
HTTPClient http;
http.begin(firmwareUrl);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
int contentLength = http.getSize();
if (contentLength > 0) {
bool canBegin = Update.begin(contentLength);
if (canBegin) {
Serial.println("Starting OTA update...");
WiFiClient *client = http.getStreamPtr();
size_t written = Update.writeStream(*client);
if (written == contentLength) {
Serial.println("Update complete");
if (Update.end()) {
Serial.println("Update successfully finished. Rebooting...");
ESP.restart(); // Reboot to apply the update
} else {
Serial.printf("Update failed: %s\n", Update.errorString());
}
} else {
Serial.println("Update failed: Written size mismatch.");
}
} else {
Serial.println("Not enough space for update.");
}
} else {
Serial.println("Firmware file is empty.");
}
} else {
Serial.printf("Firmware HTTP error code: %d\n", httpCode);
}
http.end();
}

1
vesper/temp.hpp Normal file
View File

@@ -0,0 +1 @@

View File

@@ -1,52 +1,87 @@
/* /*
TODO List: Done Features:
- Add OTA Updates - Initiate General Structure
- Add reset to Factory Defaults - Add WiFi Support
- Add MQTT Support both for Subscribing and Publishing
- Add JSON support to handle MQTT messaging
- Add File Handling
- Add Melody Class with functions to Play/Pause/Stop etc.
- Add Main BellEngine
- Add custom Relay Timings (saved on-board)
- Add RTC support
- Add Timekeeper class, with functions to track time and call schedules
- Add OTA Update Functionality
- Add global logger with Mode Selection (None, Error, Warning, Info, Debug)
- Add Captive Portal / WiFi HotSpot - Add Captive Portal / WiFi HotSpot
- Add manual Sync-Time
- Add NTP Time Sync - Add NTP Time Sync
ToDo Features:
- Add reset to Factory Defaults button
- Add manual Sync-Time (for No-Connectivity Setups)
- Add the ability to report the list of melodies - Add the ability to report the list of melodies
- Add the ability to report a month's ScheduleEntry - Add the ability to report a month's ScheduleEntry
- Add the ability to report the free space in SPIFFS. - Add the ability to report the free space in SPIFFS.
- Add global logger - Add Bluetooth support
- Add - Add WiFi Direct AP Support
- Add PCB Temperature Sensor
- Counters and Statistics:
- Counter for each bell (counts total times the bell ringed)
- Counter per bell, beats/minute for reliability and thermal protection. Warranty Void scenario.
- Counter per playback, to figure out which melody is the most played.
- Counter of items per Scheduler
*/ */
#include "logging.hpp" #include "logging.hpp"
#include <SD.h>
#include <FS.h>
#include <ETH.h>
#include <SPI.h>
#include <Arduino.h> #include <Arduino.h>
#include <WiFi.h> #include <WiFi.h>
#include <HTTPClient.h>
#include <Update.h>
#include <AsyncMqttClient.h> #include <AsyncMqttClient.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <FS.h>
#include <SPIFFS.h>
#include <string> #include <string>
#include <Wire.h> #include <Wire.h>
#include <Adafruit_PCF8574.h> #include <Adafruit_PCF8574.h>
#include "RTClib.h" #include "RTClib.h"
#include <WebServer.h>
#include <ESPAsyncWebServer.h>
#include <WiFiManager.h>
// Hardware Constructors: // Hardware Constructors:
Adafruit_PCF8574 relays; Adafruit_PCF8574 relays;
RTC_DS1307 rtc; RTC_DS1307 rtc;
// SD Card Chip Select:
#define SD_CS 5
// Include Classes // Include Classes
#include "classes.hpp" #include "classes.hpp"
// Class Constructors // Class Constructors
AsyncMqttClient mqttClient; AsyncMqttClient mqttClient;
Player player; Player player;
TimeKeeper timekeeper;
std::vector<uint16_t> melody_steps; // holds the steps of the melody. Should move into bell Engine. std::vector<uint16_t> melody_steps; // holds the steps of the melody. Should move into bell Engine.
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
#include "config.h" #include "config.h"
#include "ota.hpp"
#include "functions.hpp" #include "functions.hpp"
#include "MQTT_Message_Handling.hpp" #include "MQTT_Message_Handling.hpp"
#include "MQTT_WiFi_Utilities.hpp" #include "MQTT_WiFi_Utilities.hpp"
#include "PlaybackControls.hpp" #include "PlaybackControls.hpp"
#include "bellEngine.hpp" #include "bellEngine.hpp"
#include "Scheduler.hpp"
TaskHandle_t bellEngineHandle = NULL; TaskHandle_t bellEngineHandle = NULL;
TimerHandle_t schedulerTimer; TimerHandle_t schedulerTimer;
@@ -57,64 +92,69 @@ void setup()
{ {
// Initialize Serial Communications & I2C Bus (for debugging) // Initialize Serial Communications & I2C Bus (for debugging)
Serial.begin(115200); Serial.begin(115200);
Wire.begin(4,15);
SPI.begin(ETH_SPI_SCK, ETH_SPI_MISO, ETH_SPI_MOSI);
delay(50); delay(50);
// Initialize PCF8574 // Initialize PCF8574 and Relays
Wire.begin(4,15);
relays.begin(PCF8574_ADDR, &Wire); relays.begin(PCF8574_ADDR, &Wire);
// Initialize Relays
for (uint8_t p=0; p<6; p++){ for (uint8_t p=0; p<6; p++){
relays.pinMode(p, OUTPUT); relays.pinMode(p, OUTPUT);
relays.digitalWrite(p, HIGH); relays.digitalWrite(p, HIGH);
} }
// Initialize SPIFFS
if (!SPIFFS.begin(true)) { // 'true' means format SPIFFS if initialization fails
LOG_ERROR("Failed to mount SPIFFS"); // Initialize SD Card
while(true) delay(10); if (!SD.begin(SD_CS)) {
Serial.println("SD card not found. Using defaults.");
} else {
// do nothing
} }
LOG_INFO("SPIFFS mounted successfully");
delay(50);
// Initialize RTC // Initialize RTC
if (!rtc.begin()) { if (!rtc.begin()) {
Serial.println("Couldn't find RTC"); LOG_ERROR("Couldn't find RTC");
while(true) delay(10); while(true) delay(10);
} }
if (! rtc.isrunning()) { // Initialize Networking and MQTT
LOG_INFO("RTC is NOT running, let's set the time!"); Network.onEvent(NetworkEvent);
// When time needs to be set on a new device, or after a power loss, the ETH.begin(ETH_PHY_TYPE, ETH_PHY_ADDR, ETH_PHY_CS, ETH_PHY_IRQ, ETH_PHY_RST, SPI);
// following line sets the RTC to the date & time this sketch was compiled InitMqtt();
rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); WiFiManager wm;
// This line sets the RTC with an explicit date & time, for example to set //wm.resetSettings(); // Only for Debugging.
// January 21, 2014 at 3am you would call: bool res;
// rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0)); res = wm.autoConnect(ap_ssid.c_str(),ap_pass.c_str());
if(!res) {
LOG_ERROR("Failed to connect to WiFi");
}
else {
LOG_INFO("Connected to WiFi");
} }
delay(100);
// Initialize WiFi and MQTT checkForUpdates(); // checks for updates online
WiFi.onEvent(WiFiEvent); syncTimeWithNTP(); // syncs time from NTP Server
InitMqtt();
ConnectWiFi_STA();
delay(1000);
delay(100);
// WebSocket setup
ws.onEvent(onWebSocketEvent);
server.addHandler(&ws);
// Start the server
server.begin();
// Tasks and Timers
xTaskCreatePinnedToCore(bellEngine,"bellEngine", 8192, NULL, 1, &bellEngineHandle, 1); xTaskCreatePinnedToCore(bellEngine,"bellEngine", 8192, NULL, 1, &bellEngineHandle, 1);
xTaskCreatePinnedToCore(durationTimer, "durationTimer", 8192, NULL, 2, NULL, 1); xTaskCreatePinnedToCore(durationTimer, "durationTimer", 8192, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(relayControlTask, "Relay Control", 2048, NULL, 2, NULL, 1); xTaskCreatePinnedToCore(relayControlTask, "Relay Control", 2048, NULL, 2, NULL, 1);
schedulerTimer = xTimerCreate("Timer",pdMS_TO_TICKS(10000),pdTRUE,(void*)0,reinterpret_cast<TimerCallbackFunction_t>(schedule_timer));
if (schedulerTimer != NULL) {
xTimerStart(schedulerTimer, 0);
} else {
LOG_ERROR("Failed to create timer!");
}
timekeeper.refreshDailySchedule();
loadRelayTimings(); loadRelayTimings();
} }
void loop() void loop()
{ {
} }