This commit is contained in:
2025-10-06 18:27:50 +02:00
commit 3e191a4f60
213 changed files with 22261 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html
+131
View File
@@ -0,0 +1,131 @@
#include "hardware_config.hpp"
#include <pb_encode.h>
#include <pb_decode.h>
#include <Arduino.h>
#include <logger.hpp>
HardwareConfigRepo::HardwareConfigRepo() {
setDefaults();
}
HardwareConfigRepo::~HardwareConfigRepo() {
// Cleanup if needed
}
void HardwareConfigRepo::setChangeCallback(std::function<void(const hardware_HardwareConfig&)> callback) {
changeCallback_ = callback;
}
void HardwareConfigRepo::setDefaults() {
data_.hold_duration_ms = DEFAULT_HOLD_DURATION_MS;
data_.override = DEFAULT_OVERRIDE;
data_.relay_pin = DEFAULT_RELAY_PIN;
data_.sensor_rx_pin = DEFAULT_SENSOR_RX_PIN;
data_.sensor_tx_pin = DEFAULT_SENSOR_TX_PIN;
// Initialize LedConfig to defaults (empty)
// Nanopb initializes to zero, so ok
}
bool HardwareConfigRepo::fileExists() {
return LittleFS.exists(filename_);
}
bool HardwareConfigRepo::load() {
if (!LittleFS.begin()) {
LOG_ERROR("HardwareConfigRepo: LittleFS mount failed");
setDefaults();
return false;
}
if (!fileExists()) {
// File doesn't exist, use defaults
setDefaults();
runCallback();
return true;
}
File file = LittleFS.open(filename_, "r");
if (!file) {
LOG_ERROR("Failed to open hardware config file for reading");
setDefaults();
return false;
}
// Read file into buffer
size_t fileSize = file.size();
uint8_t buffer[fileSize];
file.read(buffer, fileSize);
file.close();
// Decode protobuf
pb_istream_t stream = pb_istream_from_buffer(buffer, fileSize);
bool status = pb_decode(&stream, hardware_HardwareConfig_fields, &data_);
if (!status) {
LOG_ERROR("Failed to decode hardware config protobuf");
setDefaults();
return false;
}
runCallback();
return true;
}
bool HardwareConfigRepo::save() {
if (!LittleFS.begin()) {
LOG_ERROR("HardwareConfigRepo: LittleFS mount failed");
return false;
}
// Encode to buffer first to get size
size_t bufferSize = 0;
pb_get_encoded_size(&bufferSize, hardware_HardwareConfig_fields, &data_);
uint8_t buffer[bufferSize];
pb_ostream_t stream = pb_ostream_from_buffer(buffer, bufferSize);
bool status = pb_encode(&stream, hardware_HardwareConfig_fields, &data_);
if (!status) {
LOG_ERROR("Failed to encode hardware config protobuf");
return false;
}
// Write to file
File file = LittleFS.open(filename_, "w");
if (!file) {
LOG_ERROR("Failed to open hardware config file for writing");
return false;
}
size_t bytesWritten = file.write(buffer, bufferSize);
file.close();
if (bytesWritten != bufferSize) {
LOG_ERROR("Failed to write complete hardware config file");
return false;
}
runCallback();
return true;
}
void HardwareConfigRepo::setData(const hardware_HardwareConfig& newData) {
data_ = newData;
save(); // Persist immediately
}
void HardwareConfigRepo::resetToDefaults() {
setDefaults();
save();
}
void HardwareConfigRepo::runCallback() {
if (changeCallback_) {
changeCallback_(data_);
}
}
HardwareConfigRepo hardwareConfigRepo;
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <LittleFS.h>
#include <hardware.pb.h>
class HardwareConfigRepo {
public:
HardwareConfigRepo();
~HardwareConfigRepo();
// Set callback that gets called when config changes (load/save)
void setChangeCallback(std::function<void(const hardware_HardwareConfig&)> callback);
// Load config from LittleFS, use defaults if file doesn't exist
bool load();
// Get reference to config data
hardware_HardwareConfig& getData() { return data_; }
// Set config data
void setData(const hardware_HardwareConfig& newData);
// Reset to default values
void resetToDefaults();
private:
// Save current config to LittleFS
bool save();
hardware_HardwareConfig data_;
const char* filename_ = "/hardware.pb";
std::function<void(const hardware_HardwareConfig&)> changeCallback_ = nullptr;
// Default values
static constexpr uint32_t DEFAULT_HOLD_DURATION_MS = 5000;
static constexpr bool DEFAULT_OVERRIDE = false;
static constexpr uint32_t DEFAULT_RELAY_PIN = 2;
static constexpr uint32_t DEFAULT_SENSOR_RX_PIN = 16;
static constexpr uint32_t DEFAULT_SENSOR_TX_PIN = 17;
// Helper methods
void setDefaults();
bool fileExists();
void runCallback();
};
extern HardwareConfigRepo hardwareConfigRepo;
+6
View File
@@ -0,0 +1,6 @@
{
"name": "hardware",
"dependencies": {
"nanopb": "*"
}
}
+282
View File
@@ -0,0 +1,282 @@
#include "rfid.hpp"
#include <logger.hpp>
RfidDB rfidDB;
RfidDB::RfidDB(const String& filename)
: filename_(filename), tmpFilename_(filename + ".tmp"), initialized_(false) {}
RfidDB::~RfidDB() {}
void printDatabaseContents() {
LOG_DEBUG("--- RFID Database Contents (Stored Values) ---");
rfidDB.iterate([](uint32_t stored_id) {
// 'stored_id' ist der Wert, wie er in der Datei steht (Byte-geswappt)
Serial.printf("Stored HEX: 0x%08X\n", stored_id);
});
LOG_DEBUG("-------------------------------------------");
}
bool RfidDB::begin() {
if (initialized_) {
return true;
}
if (!LittleFS.begin()) {
LOG_ERROR("RfidDB: LittleFS mount failed");
return false;
}
// Ensure the file exists, creating it if necessary.
if (!LittleFS.exists(filename_)) {
File f = LittleFS.open(filename_, "w");
if (!f) {
return false; // Could not create the file
}
f.close();
}
initialized_ = true;
printDatabaseContents();
return true;
}
uint32_t RfidDB::count() {
if (!initialized_) return 0;
File f = LittleFS.open(filename_, "r");
if (!f) return 0;
uint32_t n = fileEntryCount(f);
f.close();
return n;
}
bool RfidDB::contains(uint32_t raw_id) {
if (!initialized_) {
LOG_DEBUG("RfidDB: contains not initialized");
return false;
}
File f = LittleFS.open(filename_, "r");
if (!f) {
LOG_DEBUG("RfidDB: contains failed to open file");
return false;
}
uint32_t id = byteSwap(raw_id);
uint32_t idx;
bool found = false;
bool ok = binarySearch(f, idx, id, found);
f.close();
LOG_DEBUG("RfidDB: contains id=%08X, ok=%d, found=%d", raw_id, ok, found);
return ok && found;
}
void RfidDB::iterate(std::function<void(uint32_t)> callback) {
if (!initialized_ || !callback) return;
File f = LittleFS.open(filename_, "r");
if (!f) return;
uint32_t n = fileEntryCount(f);
for (uint32_t i = 0; i < n; ++i) {
uint32_t v;
if (readEntryAt(f, i, v)) {
callback(v);
}
}
f.close();
}
uint32_t RfidDB::fileEntryCount(File &f) {
return f.size() / ENTRY_SIZE;
}
// 🚀 More efficient read/write methods
bool RfidDB::readEntryAt(File &f, uint32_t index, uint32_t &out) {
if (!f.seek(index * ENTRY_SIZE, SeekSet)) return false;
return f.read(reinterpret_cast<uint8_t*>(&out), ENTRY_SIZE) == ENTRY_SIZE;
}
bool RfidDB::writeEntryAt(File &f, uint32_t index, uint32_t value) {
if (!f.seek(index * ENTRY_SIZE, SeekSet)) return false;
return f.write(reinterpret_cast<const uint8_t*>(&value), ENTRY_SIZE) == ENTRY_SIZE;
}
bool RfidDB::binarySearch(File &f, uint32_t &outIndex, uint32_t key, bool &found) {
// Get the number of entries in the file (each entry is 4 bytes)
uint32_t n = fileEntryCount(f);
// If the file is empty, return with outIndex = 0 and found = false
if (n == 0) {
outIndex = 0;
found = false;
return true;
}
// Initialize search boundaries for binary search
uint32_t left = 0;
uint32_t right = n - 1;
// Perform binary search on the sorted database
// Note: The database must contain values in ascending order of their *swapped* (Little-Endian) representation
// to ensure correct search results. The 'key' parameter is the byte-swapped (Little-Endian) value of the
// raw RFID ID (e.g., raw ID 0x635C426D is swapped to 0x6D425C63 for comparison).
while (left <= right) {
// Calculate the middle index
uint32_t mid = left + (right - left) / 2;
uint32_t v;
// Read the entry at index 'mid' from the file
// The file stores IDs as Big-Endian (e.g., bytes 63 5C 42 6D for original ID 0x635C426D).
// On this Little-Endian platform (e.g., ESP32), reading 4 bytes into 'v' interprets them as
// Little-Endian, so bytes 63 5C 42 6D become v = 0x6D425C63 (swapped).
if (!readEntryAt(f, mid, v)) {
return false; // Failed to read entry
}
// Compare the read value 'v' (Little-Endian, swapped) with the search key (also Little-Endian, swapped)
if (v == key) {
outIndex = mid;
found = true;
return true; // Found the ID at index 'mid'
}
// Since the database is sorted by the swapped (Little-Endian) values,
// adjust the search boundaries based on the comparison
if (v < key) {
left = mid + 1; // Search in the right half
} else {
if (mid == 0) break; // Prevent underflow when right = mid - 1
right = mid - 1; // Search in the left half
}
}
// If not found, set outIndex to the insertion point where the key would be added
// to maintain the sorted order of swapped (Little-Endian) values
outIndex = left;
found = false;
return true;
}
uint32_t RfidDB::byteSwap(uint32_t x) const
{
return ((x & 0xFF000000) >> 24) |
((x & 0x00FF0000) >> 8) |
((x & 0x0000FF00) << 8) |
((x & 0x000000FF) << 24);
}
bool RfidDB::add(uint32_t raw_id) {
if (!initialized_) return false;
File src = LittleFS.open(filename_, "r");
if (!src) return false;
uint32_t id = byteSwap(raw_id);
uint32_t idx;
bool found;
if (!binarySearch(src, idx, id, found)) {
src.close();
return false;
}
if (found) {
src.close();
return true; // Already present, we consider this a success
}
File dst = LittleFS.open(tmpFilename_, "w");
if (!dst) {
src.close();
return false;
}
bool success = true;
uint32_t n = fileEntryCount(src);
uint32_t v;
// Copy entries before the insertion point
for (uint32_t i = 0; i < idx; ++i) {
if (!readEntryAt(src, i, v) || !writeEntryAt(dst, i, v)) {
success = false;
break;
}
}
// Insert the new entry
if (success && !writeEntryAt(dst, idx, id)) {
success = false;
}
// Copy the remaining entries
if (success) {
for (uint32_t i = idx; i < n; ++i) {
if (!readEntryAt(src, i, v) || !writeEntryAt(dst, i + 1, v)) {
success = false;
break;
}
}
}
src.close();
dst.close();
if (!success) {
LittleFS.remove(tmpFilename_);
return false;
}
// Atomic replace
if (!LittleFS.rename(tmpFilename_, filename_)) {
// Fallback in case rename fails
LittleFS.remove(tmpFilename_);
return false;
}
return true;
}
bool RfidDB::remove(uint32_t raw_id) {
if (!initialized_) return false;
File src = LittleFS.open(filename_, "r");
if (!src) return false;
uint32_t id = byteSwap(raw_id);
uint32_t idx;
bool found;
if (!binarySearch(src, idx, id, found) || !found) {
src.close();
return false; // Not found, so nothing to remove
}
File dst = LittleFS.open(tmpFilename_, "w");
if (!dst) {
src.close();
return false;
}
bool success = true;
uint32_t n = fileEntryCount(src);
uint32_t written = 0;
uint32_t v;
for (uint32_t i = 0; i < n; ++i) {
if (i == idx) continue; // Skip the entry to be deleted
if (!readEntryAt(src, i, v) || !writeEntryAt(dst, written, v)) {
success = false;
break;
}
written++;
}
src.close();
dst.close();
if (!success) {
LittleFS.remove(tmpFilename_);
return false;
}
if (!LittleFS.rename(tmpFilename_, filename_)) {
LittleFS.remove(tmpFilename_);
return false;
}
return true;
}
+68
View File
@@ -0,0 +1,68 @@
#pragma once
#include <Arduino.h>
#include <LittleFS.h>
#include <functional> // For std::function
class RfidDB {
public:
// The default constructor uses "/rfid.db"
RfidDB(const String& filename = "/rfid.db");
~RfidDB();
/**
* @brief Initializes the filesystem and the database file.
* Must be called once in setup().
* @return true on success, otherwise false.
*/
bool begin();
/**
* @brief Returns the number of stored IDs.
*/
uint32_t count();
/**
* @brief Checks if an ID exists in the database.
* @param id The ID to check.
* @return true if the ID exists, otherwise false.
*/
bool contains(uint32_t id);
/**
* @brief Adds a new ID. The ID is only added if it does not already exist.
* @param id The ID to add.
* @return true if the ID was successfully added, otherwise false.
*/
bool add(uint32_t id);
/**
* @brief Removes an ID from the database.
* @param id The ID to remove.
* @return true if the ID was found and removed, otherwise false.
*/
bool remove(uint32_t id);
/**
* @brief Iterates over all stored IDs and calls a callback function for each.
* @param callback The function to be called for each ID (e.g., a lambda: `[](uint32_t id){ Serial.println(id); }`).
*/
void iterate(std::function<void(uint32_t)> callback);
private:
String filename_;
String tmpFilename_;
bool initialized_ = false;
static constexpr size_t ENTRY_SIZE = sizeof(uint32_t);
// Helper methods
bool readEntryAt(File &f, uint32_t index, uint32_t &out);
bool writeEntryAt(File &f, uint32_t index, uint32_t value);
bool binarySearch(File &f, uint32_t &outIndex, uint32_t key, bool &found);
uint32_t byteSwap(uint32_t x) const;
uint32_t fileEntryCount(File &f);
};
// Global instance, kept as requested
extern RfidDB rfidDB;
+108
View File
@@ -0,0 +1,108 @@
#include "session.hpp"
#include <pgmspace.h> // Required for PROGMEM
// Definition of the global instance
SessionManager sessionManager;
// The character set is now stored in flash memory to save RAM.
const char charset[] PROGMEM = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
SessionManager::SessionManager() {
session_mutex_ = xSemaphoreCreateMutex();
for (size_t i = 0; i < MAX_SESSIONS; ++i) {
sessions_[i].username[0] = '\0';
}
}
SessionManager::~SessionManager() {
vSemaphoreDelete(session_mutex_);
}
const char* SessionManager::createSession(const char* username) {
const char* result_token = nullptr;
// Lock the mutex to ensure exclusive access to the sessions array.
if (xSemaphoreTake(session_mutex_, portMAX_DELAY) == pdTRUE) {
for (size_t i = 0; i < MAX_SESSIONS; ++i) {
if (sessions_[i].username[0] == '\0') {
strncpy(sessions_[i].username, username, sizeof(sessions_[i].username) - 1);
sessions_[i].username[sizeof(sessions_[i].username) - 1] = '\0';
generateToken(sessions_[i].token, sizeof(sessions_[i].token));
sessions_[i].expiry_time = millis() + (15 * 60 * 1000);
result_token = sessions_[i].token;
break; // Exit loop once a slot is found
}
}
xSemaphoreGive(session_mutex_); // Release the mutex
}
return result_token;
}
const char* SessionManager::validateSession(const char* token) {
if (token == nullptr) {
return nullptr;
}
// We must copy the username to a static buffer, because the session array
// could be modified by another task after we release the mutex.
static char valid_username[33];
valid_username[0] = '\0';
if (xSemaphoreTake(session_mutex_, portMAX_DELAY) == pdTRUE) {
for (size_t i = 0; i < MAX_SESSIONS; ++i) {
if (sessions_[i].username[0] != '\0' && strcmp(sessions_[i].token, token) == 0) {
if (millis() < sessions_[i].expiry_time) {
strcpy(valid_username, sessions_[i].username);
} else {
sessions_[i].username[0] = '\0'; // Expired, clear it
}
break;
}
}
xSemaphoreGive(session_mutex_);
}
return (valid_username[0] != '\0') ? valid_username : nullptr;
}
bool SessionManager::endSession(const char* token) {
if (token == nullptr) return false;
bool found = false;
if (xSemaphoreTake(session_mutex_, portMAX_DELAY) == pdTRUE) {
for (size_t i = 0; i < MAX_SESSIONS; ++i) {
if (sessions_[i].username[0] != '\0' && strcmp(sessions_[i].token, token) == 0) {
sessions_[i].username[0] = '\0';
found = true;
break;
}
}
xSemaphoreGive(session_mutex_);
}
return found;
}
void SessionManager::cleanupExpiredSessions() {
unsigned long current_time = millis();
if (xSemaphoreTake(session_mutex_, portMAX_DELAY) == pdTRUE) {
for (size_t i = 0; i < MAX_SESSIONS; ++i) {
if (sessions_[i].username[0] != '\0') {
if (current_time >= sessions_[i].expiry_time) {
sessions_[i].username[0] = '\0';
}
}
}
xSemaphoreGive(session_mutex_);
}
}
void SessionManager::generateToken(char* buffer, size_t buffer_size) {
const size_t charset_size = sizeof(charset) - 1;
for (size_t i = 0; i < buffer_size - 1; ++i) {
// Read the character directly from flash memory (PROGMEM).
buffer[i] = pgm_read_byte(&charset[esp_random() % charset_size]);
}
buffer[buffer_size - 1] = '\0';
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> // Required for mutex
// Represents an active session.
// An empty username (username[0] == '\0') indicates the slot is free.
struct Session {
char username[33];
char token[17]; // 16 chars + null terminator
unsigned long expiry_time; // Expiration timestamp from millis()
};
class SessionManager {
public:
SessionManager();
~SessionManager(); // Destructor to clean up the mutex
const char* createSession(const char* username);
const char* validateSession(const char* token);
bool endSession(const char* token);
void cleanupExpiredSessions();
private:
static constexpr size_t MAX_SESSIONS = 3;
Session sessions_[MAX_SESSIONS];
SemaphoreHandle_t session_mutex_; // Mutex to protect access to sessions_
void generateToken(char* buffer, size_t buffer_size);
};
// Central, global instance.
extern SessionManager sessionManager;
+161
View File
@@ -0,0 +1,161 @@
#include "settings.hpp"
#include <pb_encode.h>
#include <pb_decode.h>
#include <Arduino.h>
#include <logger.hpp>
Settings::Settings() {
setDefaults();
}
Settings::~Settings() {
// Cleanup if needed
}
void Settings::setDefaults() {
strcpy(data_.sync_server_url, "https://ssvc.ldev/api/");
strcpy(data_.device_api_key, "c62a6b5c-914e-49d4-9871-1896a627e0df");
strcpy(data_.sync_server_url, DEFAULT_SYNC_SERVER_URL);
strcpy(data_.device_api_key, DEFAULT_DEVICE_API_KEY);
data_.sync_interval_seconds = DEFAULT_SYNC_INTERVAL;
data_.auto_sync = DEFAULT_AUTO_SYNC;
data_.wifi_mode = DEFAULT_WIFI_MODE;
strcpy(data_.station_ssid, DEFAULT_STATION_SSID);
strcpy(data_.station_password, DEFAULT_STATION_PASSWORD);
strcpy(data_.ap_ssid, DEFAULT_AP_SSID);
strcpy(data_.ap_password, DEFAULT_AP_PASSWORD);
data_.ap_channel = DEFAULT_AP_CHANNEL;
data_.enable_fallback_ap = DEFAULT_ENABLE_FALLBACK_AP;
data_.log_level = DEFAULT_LOG_LEVEL;
data_.updated_at = millis(); // Use current uptime as timestamp
data_.version = DEFAULT_VERSION;
}
bool Settings::fileExists() {
return LittleFS.exists(filename_);
}
bool Settings::load() {
if (!fileExists()) {
// File doesn't exist, use defaults
setDefaults();
}else{
File file = LittleFS.open(filename_, "r");
if (!file) {
LOG_ERROR("Failed to open settings file for reading");
setDefaults();
return false;
}
// Read file into buffer
size_t fileSize = file.size();
uint8_t buffer[fileSize];
file.read(buffer, fileSize);
file.close();
// Decode protobuf
pb_istream_t stream = pb_istream_from_buffer(buffer, fileSize);
bool status = pb_decode(&stream, settings_SettingsData_fields, &data_);
if (!status) {
LOG_ERROR("Failed to decode settings protobuf");
setDefaults();
return false;
}
}
// callback if set
runCallback();
return true;
}
bool Settings::save() {
// Update timestamp and version
data_.updated_at = millis();
data_.version++;
// Encode to buffer first to get size
size_t bufferSize = 0;
pb_get_encoded_size(&bufferSize, settings_SettingsData_fields, &data_);
uint8_t buffer[bufferSize];
pb_ostream_t stream = pb_ostream_from_buffer(buffer, bufferSize);
bool status = pb_encode(&stream, settings_SettingsData_fields, &data_);
if (!status) {
LOG_ERROR("Failed to encode settings protobuf");
return false;
}
// Write to file
File file = LittleFS.open(filename_, "w");
if (!file) {
LOG_ERROR("Failed to open settings file for writing");
return false;
}
size_t bytesWritten = file.write(buffer, bufferSize);
file.close();
if (bytesWritten != bufferSize) {
LOG_ERROR("Failed to write complete settings file");
return false;
}
return true;
}
void Settings::setData(const settings_SettingsData& newData) {
data_ = newData;
data_.updated_at = millis();
data_.version++;
// Notify callback if set
runCallback();
}
void Settings::resetToDefaults() {
setDefaults();
}
struct SettingsCallbackParams {
settings_SettingsData data;
std::function<void(const settings_SettingsData&)> callback;
};
static void settingsChangeTask(void* pvParameters) {
SettingsCallbackParams* params = static_cast<SettingsCallbackParams*>(pvParameters);
if (params->callback) {
vTaskDelay(100 / portTICK_PERIOD_MS); // Small delay to ensure system stability
LOG_DEBUG("Settings (Task): Executing change callback...");
params->callback(params->data);
LOG_DEBUG("Settings (Task): Callback finished.");
}
delete params;
vTaskDelete(NULL);
}
void Settings::runCallback()
{
if (changeCallback_) {
SettingsCallbackParams* params = new SettingsCallbackParams();
params->data = data_; // Create a safe copy for the task
params->callback = changeCallback_;
xTaskCreate(
settingsChangeTask,
"SettingsChangeTask",
4096, // Stack size
params,
1, // Priority
NULL
);
}
}
void Settings::setChangeCallback(std::function<void(const settings_SettingsData&)> callback) {
changeCallback_ = callback;
}
Settings settings;
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <LittleFS.h>
#include <settings.pb.h>
class Settings {
public:
Settings();
~Settings();
// Set callback that gets called when settings change
void setChangeCallback(std::function<void(const settings_SettingsData&)> callback);
// Load settings from LittleFS, use defaults if file doesn't exist
bool load();
// Save current settings to LittleFS
bool save();
// Get reference to settings data
settings_SettingsData& getData() { return data_; }
// Set settings data and update timestamp/version
void setData(const settings_SettingsData& newData);
// Reset to default values
void resetToDefaults();
private:
void runCallback();
settings_SettingsData data_;
const char* filename_ = "/settings.pb";
std::function<void(const settings_SettingsData&)> changeCallback_ = nullptr;
SemaphoreHandle_t dataMutex_ = nullptr;
// Default values
static constexpr const char* DEFAULT_SYNC_SERVER_URL = "https://sync.example.com/api";
static constexpr const char* DEFAULT_DEVICE_API_KEY = "";
static constexpr uint32_t DEFAULT_SYNC_INTERVAL = 1800;
static constexpr bool DEFAULT_AUTO_SYNC = true;
static constexpr settings_WifiMode DEFAULT_WIFI_MODE = settings_WifiMode_WIFI_MODE_AP_STATION;
static constexpr const char* DEFAULT_STATION_SSID = "HOME";
static constexpr const char* DEFAULT_STATION_PASSWORD = "redacted";
static constexpr const char* DEFAULT_AP_SSID = "RFID-Master-AP";
static constexpr const char* DEFAULT_AP_PASSWORD = "rfid12345";
static constexpr uint32_t DEFAULT_AP_CHANNEL = 6;
static constexpr bool DEFAULT_ENABLE_FALLBACK_AP = true;
static constexpr settings_LogLevel DEFAULT_LOG_LEVEL = settings_LogLevel_LOG_LEVEL_INFO;
static constexpr uint32_t DEFAULT_VERSION = 1;
// Helper methods
void setDefaults();
bool fileExists();
};
extern Settings settings;
+392
View File
@@ -0,0 +1,392 @@
#include "users.hpp"
#include <pb_encode.h>
#include <pb_decode.h>
#include <Arduino.h>
#include <cstring>
#include <functional>
#include <SHA256.h>
#include <logger.hpp>
// Definition of the global instance.
Users users;
// =================================================================
// == 1. NANOPB STREAM HELPERS for Arduino File Objects
// =================================================================
// A state structure to pass the Arduino File object to Nanopb's stream callbacks.
struct FileStreamState {
File* file;
};
// Nanopb callback for writing data to a File.
static bool writeFileCallback(pb_ostream_t *stream, const uint8_t *buf, size_t count) {
FileStreamState* state = (FileStreamState*)stream->state;
return state->file->write(buf, count) == count;
}
// Nanopb callback for reading from a File.
static bool readFileCallback(pb_istream_t *stream, uint8_t *buf, size_t count) {
FileStreamState* state = (FileStreamState*)stream->state;
if (count == 0) return true;
size_t len = state->file->read(buf, count);
if (len < count) {
stream->bytes_left = 0; // Signal End-Of-File
}
return len > 0 || count == 0;
}
// Helper to create a Nanopb output stream from an Arduino File object.
static pb_ostream_t pb_ostream_from_file(File &file) {
FileStreamState* state = new FileStreamState{&file};
pb_ostream_t stream = {&writeFileCallback, state, SIZE_MAX, 0};
return stream;
}
// Helper to create a Nanopb input stream from an Arduino File object.
static pb_istream_t pb_istream_from_file(File &file) {
FileStreamState* state = new FileStreamState{&file};
pb_istream_t stream = {&readFileCallback, state, file.size()};
return stream;
}
// This structure is used to pass data to the nanopb decode callbacks
// when decoding into a single, stack-allocated UserData object.
struct DecodeTarget {
UserData* user_buffer;
};
// Nanopb callback to decode a username string into our temporary buffer.
static bool decodeUsernameCallback(pb_istream_t *stream, const pb_field_t *field, void **arg) {
DecodeTarget* target = (DecodeTarget*)*arg;
// 1. Store the length of the incoming string BEFORE reading from the stream.
size_t len = stream->bytes_left;
if (len >= sizeof(target->user_buffer->username)) {
return false; // Input string is too long for our buffer.
}
// 2. Read 'len' bytes. This will modify stream->bytes_left.
if (!pb_read(stream, (uint8_t*)target->user_buffer->username, len)) {
return false;
}
// 3. Use the stored length 'len' to place the null terminator correctly.
target->user_buffer->username[len] = '\0';
return true;
}
// Nanopb callback to decode a password hash (bytes) into our temporary buffer.
static bool decodePasswordCallback(pb_istream_t *stream, const pb_field_t *field, void **arg) {
DecodeTarget* target = (DecodeTarget*)*arg;
if (stream->bytes_left != sizeof(target->user_buffer->password_hash)) {
return false; // A SHA-256 hash MUST be 32 bytes.
}
return pb_read(stream, target->user_buffer->password_hash, stream->bytes_left);
}
// =================================================================
// == 2. USERS CLASS IMPLEMENTATION
// =================================================================
Users::Users() {}
Users::~Users() {}
void Users::hashPassword(const char* password, uint8_t* hash_out) const {
SHA256 sha256;
sha256.update(password, strlen(password));
sha256.finalize(hash_out, 32);
}
// --- PRIVATE STATIC HELPERS (Moved inside the class scope) ---
// Generic helper function to iterate over all users in the file
// and execute a callback for each one. The callback can stop the iteration by returning false.
bool Users::_iterateUsers(const char* filename, const std::function<bool(UserData&)>& user_callback) {
// check if file exists
if(!LittleFS.exists(filename)){
return true; // No file is not an error, just means no users to iterate.
}
File file = LittleFS.open(filename, "r");
if (!file || file.size() == 0) {
return true; // No file is not an error, just means no users to iterate.
}
pb_istream_t istream = pb_istream_from_file(file);
UserData user_buffer; // Single user buffer on the stack
DecodeTarget target = { &user_buffer };
webui_WebUiUser user_msg = webui_WebUiUser_init_zero;
user_msg.password_hash.funcs.decode = &decodePasswordCallback;
user_msg.password_hash.arg = &target;
bool continue_iterating = true;
while (istream.bytes_left > 0) {
memset(&user_buffer, 0, sizeof(user_buffer));
if (pb_decode_delimited(&istream, webui_WebUiUser_fields, &user_msg)) {
strcpy(user_buffer.username, user_msg.username);
if (!user_callback(user_buffer)) {
continue_iterating = false;
break;
}
} else {
// Decoding failed, stop iterating.
break;
}
}
delete (FileStreamState*)istream.state;
file.close();
return continue_iterating;
}
// Generic helper for streaming transformations (delete, update).
// The callback modifies a user buffer and returns true to keep it, false to delete it.
bool Users::_transformUsers(const char* src_filename, const std::function<bool(UserData&)>& transform_callback) {
const char* tmp_filename = "/users.pb.tmp";
File src_file = LittleFS.open(src_filename, "r");
if (!src_file) return true;
File dst_file = LittleFS.open(tmp_filename, "w");
if (!dst_file) {
src_file.close();
return false;
}
pb_istream_t istream = pb_istream_from_file(src_file);
pb_ostream_t ostream = pb_ostream_from_file(dst_file);
bool success = true;
UserData user_buffer; // Our 1-user buffer
// --- CORRECTION START ---
// 1. A structure ONLY for decoding (reading)
DecodeTarget decode_target = { &user_buffer };
webui_WebUiUser decode_msg = webui_WebUiUser_init_zero;
decode_msg.password_hash.funcs.decode = &decodePasswordCallback;
decode_msg.password_hash.arg = &decode_target;
// 2. A structure ONLY for encoding (writing)
webui_WebUiUser encode_msg = webui_WebUiUser_init_zero;
encode_msg.password_hash.funcs.encode = [](pb_ostream_t *stream, const pb_field_t *field, void * const *arg) {
UserData* u = (UserData*)*arg;
return pb_encode_tag_for_field(stream, field) && pb_encode_string(stream, u->password_hash, sizeof(u->password_hash));
};
// 3. Main loop: Decode with decode_msg, transform user_buffer, encode with encode_msg
while (istream.bytes_left > 0 && pb_decode_delimited(&istream, webui_WebUiUser_fields, &decode_msg)) {
strcpy(user_buffer.username, decode_msg.username);
if (transform_callback(user_buffer)) {
// Keep: Encode the content from the buffer with the encode_msg structure
strcpy(encode_msg.username, user_buffer.username);
encode_msg.password_hash.arg = &user_buffer;
if (!pb_encode_delimited(&ostream, webui_WebUiUser_fields, &encode_msg)) {
success = false;
break;
}
}
}
// --- Rest of the function remains the same ---
delete (FileStreamState*)istream.state;
delete (FileStreamState*)ostream.state;
src_file.close();
dst_file.close();
if (!success) {
LittleFS.remove(tmp_filename);
return false;
}
if (LittleFS.exists(src_filename)) {
LittleFS.remove(src_filename);
}
return LittleFS.rename(tmp_filename, src_filename);
}
// --- PUBLIC READ-ONLY METHODS ---
size_t Users::getUserCount() {
if(initialized_) {
return userCount_;
}
size_t count = 0;
_iterateUsers(filename_, [&](UserData& user) {
count++;
return true; // Continue iterating
});
userCount_ = count;
initialized_ = true;
return count;
}
bool Users::userExists(const char* username) const {
bool found = false;
_iterateUsers(filename_, [&](UserData& user) {
if (strcmp(user.username, username) == 0) {
found = true;
return false; // Stop iterating
}
return true; // Continue iterating
});
return found;
}
bool Users::checkPassword(const char* username, const char* password) const {
bool success = false;
_iterateUsers(filename_, [&](UserData& user) {
if (strcmp(user.username, username) == 0) {
uint8_t provided_hash[32];
hashPassword(password, provided_hash);
if (memcmp(user.password_hash, provided_hash, 32) == 0) {
success = true;
}
return false; // Stop iterating, user found.
}
return true; // Continue iterating
});
return success;
}
bool Users::loadUsernamesCallback(const std::function<bool(const char *)> &callback) const
{
if (!callback) return false;
return _iterateUsers(filename_, [&](UserData &user) {
auto res = callback(user.username);
if (!res) return false; // Stop iterating
return true; // Continue iterating
});
}
bool Users::addUser(const char* username, const char* password) {
// --- Phase 1: Checking preconditions ---
bool exists = userExists(username);
size_t count = getUserCount();
if (exists || count >= MAX_USERS) {
if (exists) {
LOG_ERROR("addUser: User already exists. Aborting.");
} else {
LOG_ERROR("addUser: User limit reached. Aborting.");
}
return false;
}
// --- Phase 2: Preparing the files ---
const char* tmp_filename = "/users.pb.tmp";
File dst_file = LittleFS.open(tmp_filename, "w");
if (!dst_file) {
LOG_ERROR("addUser: Failed to open temporary file. Check filesystem space and permissions.");
return false;
}
File src_file = LittleFS.open(filename_, "r");
if (src_file && src_file.size() > 0) {
uint8_t buffer[128];
while (src_file.available()) {
size_t len = src_file.read(buffer, sizeof(buffer));
dst_file.write(buffer, len);
}
src_file.close();
} else {
if (src_file) src_file.close();
}
// --- Phase 3: Appending the new user ---
pb_ostream_t ostream = pb_ostream_from_file(dst_file);
webui_WebUiUser new_user_msg = webui_WebUiUser_init_zero;
// Prepare username for the encoder
strcpy(new_user_msg.username, username);
// Prepare password hash for the encoder
uint8_t password_hash[32];
hashPassword(password, password_hash); // Important: Hash is created here, not in the lambda
new_user_msg.password_hash.arg = password_hash;
new_user_msg.password_hash.funcs.encode = [](pb_ostream_t *stream, const pb_field_t *field, void * const *arg) {
return pb_encode_tag_for_field(stream, field) && pb_encode_string(stream, (uint8_t*)*arg, 32);
};
bool status = pb_encode_delimited(&ostream, webui_WebUiUser_fields, &new_user_msg);
// Important: Always release resources (stream state and file)
delete (FileStreamState*)ostream.state;
dst_file.close();
if (!status) {
LOG_ERROR("addUser: Protobuf encoding (pb_encode_delimited) failed!");
LOG_DEBUG("addUser: Removing failed temporary file: %s\n", tmp_filename);
LittleFS.remove(tmp_filename);
return false;
}
// --- Phase 4: Atomic replacement of the old file ---
if (LittleFS.exists(filename_)) {
if (!LittleFS.remove(filename_)) {
LOG_ERROR("addUser: Could not remove old file '%s'. Aborting.\n", filename_);
LittleFS.remove(tmp_filename); // Cleanup temp file
return false;
}
}
bool rename_ok = LittleFS.rename(tmp_filename, filename_);
if (rename_ok) {
} else {
LOG_ERROR("addUser: Final rename failed! Filesystem might be in an inconsistent state.");
LittleFS.remove(tmp_filename); // Attempt to clean up
}
if (rename_ok) {
userCount_++;
}
return rename_ok;
}
bool Users::deleteUser(const char* username) {
bool user_was_found = false;
bool success = _transformUsers(filename_, [&](UserData& user) {
if (strcmp(user.username, username) == 0) {
user_was_found = true;
return false; // Delete this user
}
return true; // Keep this user
});
bool user_deleted = success && user_was_found;
if (user_deleted) {
userCount_--;
}
return user_deleted;
}
bool Users::updatePassword(const char* username, const char* newPassword) {
bool user_was_found = false;
bool success = _transformUsers(filename_, [&](UserData& user) {
if (strcmp(user.username, username) == 0) {
user_was_found = true;
hashPassword(newPassword, user.password_hash);
}
return true; // Always keep the user, just maybe modified.
});
return success && user_was_found;
}
void Users::reset() {
if (LittleFS.exists(filename_)) {
LittleFS.remove(filename_);
}
userCount_ = 0;
initialized_ = false;
}
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <LittleFS.h>
#include <webui.pb.h> // Ensure this is the version with 'bytes password_hash'
#include <functional> // Required for std::function
// This struct definition is correct and necessary.
struct UserData {
char username[33]; // 32 chars + null terminator
uint8_t password_hash[32]; // 32-byte raw SHA-256 hash
};
class Users {
public:
Users();
~Users();
bool addUser(const char* username, const char* password);
bool deleteUser(const char* username);
bool updatePassword(const char* username, const char* newPassword);
bool userExists(const char* username) const;
bool checkPassword(const char* username, const char* password) const;
bool loadUsernamesCallback(const std::function<bool(const char*)>& callback) const;
size_t getUserCount();
void reset();
private:
const char* filename_ = "/users.pb";
static constexpr size_t MAX_USERS = 5;
mutable size_t userCount_ = 0;
mutable bool initialized_ = false;
void hashPassword(const char* password, uint8_t* hash_out) const; static bool _iterateUsers(const char* filename, const std::function<bool(UserData&)>& user_callback);
static bool _transformUsers(const char* src_filename, const std::function<bool(UserData&)>& transform_callback);
};
extern Users users;
+168
View File
@@ -0,0 +1,168 @@
#include "hardware_rtc.hpp"
#include <logger.hpp>
HardwareRTC::HardwareRTC(TwoWire& wire): wire(wire), initialized(false) {}
HardwareRTC::~HardwareRTC() {}
void HardwareRTC::begin() {
if(wire.getClock() == 0) {
wire.begin();
wire.setClock(100000); // Set to 100kHz
}
initialized = true;
LOG_DEBUG("RTC initialized");
setSystemTime(0);
}
bool HardwareRTC::setTime(time_t unixTimestamp) {
if (!initialized) {
LOG_WARN("RTC not initialized");
return false;
}
// Use time_t, which on ESP32 is 64-bit and safe for this conversion
time_t t = unixTimestamp;
struct tm timeStruct;
gmtime_r(&t, &timeStruct);
// Check if the year is within the DS1307's valid range (2000-2099)
if (timeStruct.tm_year < 100 || timeStruct.tm_year > 199) {
// Year is out of the 2000-2099 range that the DS1307 can store.
LOG_ERROR("RTC setTime: Year %d is out of range (2000-2099)", timeStruct.tm_year + 1900);
return false;
}
uint8_t buffer[7];
buffer[0] = decToBcd(timeStruct.tm_sec);
buffer[1] = decToBcd(timeStruct.tm_min);
buffer[2] = decToBcd(timeStruct.tm_hour);
buffer[3] = decToBcd(timeStruct.tm_wday + 1);
buffer[4] = decToBcd(timeStruct.tm_mday);
buffer[5] = decToBcd(timeStruct.tm_mon + 1);
// The DS1307 only stores years 00-99. We handle this by taking the year since 2000.
buffer[6] = decToBcd(timeStruct.tm_year - 100);
writeRegisters(buffer, 7);
// set system time to match RTC
setSystemTime(0);
LOG_DEBUG("RTC setTime: %s", toDateString(unixTimestamp).c_str());
return true;
}
time_t HardwareRTC::getTime() {
if (!initialized) return 0;
uint8_t buffer[7];
readRegisters(buffer, 7);
struct tm timeStruct = {0}; // Important: Initialize the struct to zero
timeStruct.tm_sec = bcdToDec(buffer[0] & 0x7F);
timeStruct.tm_min = bcdToDec(buffer[1]);
timeStruct.tm_hour = bcdToDec(buffer[2] & 0x3F);
timeStruct.tm_wday = bcdToDec(buffer[3]) - 1;
timeStruct.tm_mday = bcdToDec(buffer[4]);
timeStruct.tm_mon = bcdToDec(buffer[5]) - 1;
// Assume all 2-digit years from the RTC are in the 21st century (2000-2099)
timeStruct.tm_year = bcdToDec(buffer[6]) + 100; // Years since 1900
// mktime converts a local time struct to a time_t.
// Since the RTC stores time without timezone info, we treat it as UTC.
// timegm is the correct function for this, but mktime is often used
// on embedded systems with the timezone set to UTC.
time_t t = mktime(&timeStruct);
LOG_DEBUG("RTC getTime: %s", toDateString(t).c_str());
// Cast the 64-bit time_t to uint64_t for the return type
return static_cast<time_t>(t);
}
void HardwareRTC::setSystemTime(int timezoneOffsetHours) {
if (!initialized) {
LOG_WARN("RTC not initialized");
return;
}
uint64_t unixTime = getTime();
if (unixTime == 0) return;
unixTime += timezoneOffsetHours * 3600;
LOG_DEBUG("RTC setSystemTime: %s", toDateString(unixTime).c_str());
struct timeval tv;
// The tv_sec field is of type time_t, which is 64-bit on ESP32
tv.tv_sec = unixTime;
tv.tv_usec = 0;
settimeofday(&tv, NULL);
}
bool HardwareRTC::isRunning() {
if (!initialized) return false;
uint8_t buffer[1];
readRegisters(buffer, 1);
return !(buffer[0] & 0x80); // CH bit is 0 when running
}
void HardwareRTC::setTimezone(const char* timezone) {
setenv("TZ", timezone, 1);
tzset();
}
void HardwareRTC::update()
{
static uint32_t nextSync = 0;
time_t now = millis();
if (now >= nextSync) {
if(!isRunning()) {
LOG_WARN("RTC is not running, skipping update");
return;
}
setSystemTime(0);
nextSync = now + (15UL * 60UL * 1000UL); // every 15 minutes
LOG_DEBUG("RTC update: System time synchronized with RTC");
}
}
String HardwareRTC::toDateString(time_t timestamp) {
struct tm* timeinfo = gmtime(&timestamp);
char buffer[40];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", timeinfo);
return String(buffer);
}
uint8_t HardwareRTC::bcdToDec(uint8_t bcd) {
return ((bcd >> 4) * 10) + (bcd & 0x0F);
}
uint8_t HardwareRTC::decToBcd(uint8_t dec) {
return ((dec / 10) << 4) | (dec % 10);
}
void HardwareRTC::readRegisters(uint8_t* buffer, uint8_t length) {
Wire.beginTransmission(DS1307_ADDRESS);
Wire.write(0x00); // start at register 0
Wire.endTransmission();
Wire.requestFrom(DS1307_ADDRESS, length);
for (uint8_t i = 0; i < length; i++) {
buffer[i] = Wire.read();
}
}
void HardwareRTC::writeRegisters(uint8_t* buffer, uint8_t length) {
Wire.beginTransmission(DS1307_ADDRESS);
Wire.write(0x00); // start at register 0
for (uint8_t i = 0; i < length; i++) {
Wire.write(buffer[i]);
}
Wire.endTransmission();
}
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <Wire.h>
#include <time.h>
#include <sys/time.h>
#include <logger.hpp>
#define DS1307_ADDRESS 0x68
#define EEPROM_ADDRESS 0x50
class HardwareRTC {
public:
HardwareRTC(TwoWire& wire = Wire);
~HardwareRTC();
void begin();
// Set RTC time from a 64-bit Unix timestamp
bool setTime(time_t unixTimestamp);
// Get current time as a 64-bit Unix timestamp
time_t getTime();
static time_t getSystemTime() {
struct timeval tv;
if (gettimeofday(&tv, nullptr) != 0) {
LOG_ERROR("Failed to get system time for log entry");
return 0; // Fallback-Wert
} else {
return tv.tv_sec; // Unix-Timestamp in Sekunden
}
gettimeofday(&tv, NULL);
return tv.tv_sec;
}
// Helper method to convert time_t to date string
static String toDateString(time_t timestamp);
void setSystemTime(int timezoneOffsetHours = 0);
bool isRunning();
void setTimezone(const char* timezone);
void update(); // Placeholder for future use
private:
bool initialized;
TwoWire& wire;
uint8_t bcdToDec(uint8_t bcd);
uint8_t decToBcd(uint8_t dec);
void readRegisters(uint8_t* buffer, uint8_t length);
void writeRegisters(uint8_t* buffer, uint8_t length);
};
+222
View File
@@ -0,0 +1,222 @@
#include "hardware_serial.hpp"
#include <Arduino.h>
#include <logger.hpp>
//#define DEBUG_PROTO
#ifdef DEBUG_PROTO
#define LOG(msg) LOG_DEBUG(msg)
#else
#define LOG(msg)
#endif
ProtoSerial::ProtoSerial()
: serial(nullptr), initialized(false), lastError{0}, callback(nullptr),
currentState(WAITING_FOR_SYNC_START), payloadLength(0), messageAvailable(false),
lastUpdate(0), lastByteTime(0) {
receivedMessage = INCOMING_MESSAGE_INIT;
}
ProtoSerial::~ProtoSerial() {
// No delete needed; serial is caller-managed
}
void ProtoSerial::begin(SerialType& serialPort) {
serial = &serialPort;
initialized = true;
currentState = WAITING_FOR_SYNC_START;
payloadLength = 0;
messageAvailable = false;
lastError[0] = '\0';
lastUpdate = 0;
lastByteTime = 0;
snprintf(lastError, sizeof(lastError), "Initialized ProtoSerial");
LOG(lastError);
}
bool ProtoSerial::sendMessage(const OutgoingMessage& message) {
if (!initialized || !serial || !serial->availableForWrite()) {
snprintf(lastError, sizeof(lastError), "Serial not initialized or not ready");
LOG(lastError);
return false;
}
uint8_t buffer[OUTGOING_MESSAGE_SIZE];
pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
// Encode the message
if (!pb_encode(&stream, OUTGOING_MESSAGE_FIELDS, &message)) {
snprintf(lastError, sizeof(lastError), "Failed to encode message");
LOG(lastError);
return false;
}
uint16_t messageLength = stream.bytes_written;
if (messageLength > sizeof(buffer)) {
snprintf(lastError, sizeof(lastError), "Encoded message too large: %u", messageLength);
LOG(lastError);
return false;
}
// Send framing: sync start, length, payload, CRC, sync end
serial->write(SYNC_START);
serial->write((uint8_t*)&messageLength, sizeof(messageLength));
serial->write(buffer, messageLength);
uint8_t crc = calculate_crc8(buffer, messageLength);
serial->write(crc);
serial->write(SYNC_END);
serial->flush();
#ifdef DEBUG_PROTO
dumpHex(buffer, messageLength, "Sent");
snprintf(lastError, sizeof(lastError), "Sent message, length: %u, CRC: 0x%02X", messageLength, crc);
LOG(lastError);
#endif
return true;
}
void ProtoSerial::update() {
if (!initialized || !serial) {
snprintf(lastError, sizeof(lastError), "Serial not initialized");
LOG(lastError);
return;
}
// Throttle updates to ~100Hz
unsigned long now = millis();
if (now - lastUpdate < 10) return;
lastUpdate = now;
// Timeout if no data received for 1 second
if (currentState != WAITING_FOR_SYNC_START && now - lastByteTime > 1000) {
snprintf(lastError, sizeof(lastError), "Receive timeout");
LOG(lastError);
currentState = WAITING_FOR_SYNC_START;
payloadLength = 0;
}
while (serial->available()) {
lastByteTime = now;
switch (currentState) {
case WAITING_FOR_SYNC_START: {
uint8_t byte = serial->read();
if (byte == SYNC_START) {
currentState = WAITING_FOR_LENGTH;
LOG("Received SYNC_START");
}
break;
}
case WAITING_FOR_LENGTH: {
if (serial->available() >= sizeof(payloadLength)) {
serial->readBytes((uint8_t*)&payloadLength, sizeof(payloadLength));
if (payloadLength > sizeof(payloadBuffer) || payloadLength == 0) {
snprintf(lastError, sizeof(lastError), "Invalid payload length: %u", payloadLength);
LOG(lastError);
currentState = WAITING_FOR_SYNC_START;
payloadLength = 0;
} else {
currentState = READING_PAYLOAD;
snprintf(lastError, sizeof(lastError), "Received length: %u", payloadLength);
LOG(lastError);
}
}
break;
}
case READING_PAYLOAD: {
if (serial->available() >= payloadLength) {
serial->readBytes(payloadBuffer, payloadLength);
currentState = READING_CRC;
}
break;
}
case READING_CRC: {
if (serial->available() >= 1) {
uint8_t received_crc = serial->read();
uint8_t expected_crc = calculate_crc8(payloadBuffer, payloadLength);
if (received_crc != expected_crc) {
snprintf(lastError, sizeof(lastError), "CRC mismatch: received 0x%02X, expected 0x%02X", received_crc, expected_crc);
LOG(lastError);
} else if (serial->available() >= 1 && serial->read() != SYNC_END) {
snprintf(lastError, sizeof(lastError), "Missing SYNC_END");
LOG(lastError);
} else {
dumpHex(payloadBuffer, payloadLength, "Received");
processReceivedMessage(payloadBuffer, payloadLength);
snprintf(lastError, sizeof(lastError), "Received valid message, length: %u", payloadLength);
LOG(lastError);
}
currentState = WAITING_FOR_SYNC_START;
payloadLength = 0;
}
break;
}
}
}
}
bool ProtoSerial::hasMessage() const {
return messageAvailable;
}
const IncomingMessage& ProtoSerial::getMessage() const {
return receivedMessage;
}
void ProtoSerial::clearMessage() {
messageAvailable = false;
receivedMessage = INCOMING_MESSAGE_INIT;
snprintf(lastError, sizeof(lastError), "Message cleared");
LOG(lastError);
}
void ProtoSerial::setCallback(Callback cb) {
callback = cb;
snprintf(lastError, sizeof(lastError), "Callback set");
LOG(lastError);
}
void ProtoSerial::processReceivedMessage(uint8_t* buffer, uint16_t length) {
receivedMessage = INCOMING_MESSAGE_INIT;
pb_istream_t stream = pb_istream_from_buffer(buffer, length);
if (pb_decode(&stream, INCOMING_MESSAGE_FIELDS, &receivedMessage)) {
if (receivedMessage.which_payload == 0) {
snprintf(lastError, sizeof(lastError), "Invalid message: which_payload not set");
LOG(lastError);
return;
}
messageAvailable = true;
if (callback) {
callback(receivedMessage);
}
snprintf(lastError, sizeof(lastError), "Message decoded successfully");
LOG(lastError);
} else {
snprintf(lastError, sizeof(lastError), "Failed to decode message");
LOG(lastError);
}
}
uint8_t ProtoSerial::calculate_crc8(const uint8_t* data, uint16_t len) {
uint8_t crc = 0x00;
for (uint16_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x80) crc = (crc << 1) ^ 0x31; // CRC-8 polynomial
else crc <<= 1;
}
}
return crc;
}
void ProtoSerial::dumpHex(const uint8_t* data, uint16_t len, const char* label) {
#ifdef DEBUG_PROTO
Serial.print(label);
Serial.print(": ");
for (uint16_t i = 0; i < len; i++) {
if (i % 16 == 0) Serial.println();
Serial.printf("%02X ", data[i]);
}
Serial.println();
#endif
}
ProtoSerial pserial;
+171
View File
@@ -0,0 +1,171 @@
#pragma once
#ifdef ESP32
#include <HardwareSerial.h>
typedef HardwareSerial SerialType;
#define SERIAL_TYPE HardwareSerial
#elif defined(ESP8266)
#include <HardwareSerial.h>
typedef HardwareSerial SerialType;
#define SERIAL_TYPE HardwareSerial
#else
#error "Unsupported platform. Only ESP32 and ESP8266 are supported."
#endif
#include <pb_encode.h>
#include <pb_decode.h>
#include "hardware.pb.h"
// Define the role of this device
// Uncomment one of the following defines to set the role
#define HARDWARE_SERIAL_ROLE_CONTROL
// #define HARDWARE_SERIAL_ROLE_SENSOR
// Based on role, define the message types
#if defined(HARDWARE_SERIAL_ROLE_CONTROL)
typedef hardware_SensorToControlMessage IncomingMessage;
typedef hardware_ControlToSensorMessage OutgoingMessage;
#define INCOMING_MESSAGE_SIZE hardware_SensorToControlMessage_size
#define OUTGOING_MESSAGE_SIZE hardware_ControlToSensorMessage_size
#define INCOMING_MESSAGE_FIELDS hardware_SensorToControlMessage_fields
#define OUTGOING_MESSAGE_FIELDS hardware_ControlToSensorMessage_fields
#define INCOMING_MESSAGE_INIT hardware_SensorToControlMessage_init_zero
#define OUTGOING_MESSAGE_INIT hardware_ControlToSensorMessage_init_zero
#elif defined(HARDWARE_SERIAL_ROLE_SENSOR)
typedef hardware_ControlToSensorMessage IncomingMessage;
typedef hardware_SensorToControlMessage OutgoingMessage;
#define INCOMING_MESSAGE_SIZE hardware_ControlToSensorMessage_size
#define OUTGOING_MESSAGE_SIZE hardware_SensorToControlMessage_size
#define INCOMING_MESSAGE_FIELDS hardware_ControlToSensorMessage_fields
#define OUTGOING_MESSAGE_FIELDS hardware_SensorToControlMessage_fields
#define INCOMING_MESSAGE_INIT hardware_ControlToSensorMessage_init_zero
#define OUTGOING_MESSAGE_INIT hardware_ControlToSensorMessage_init_zero
#else
#error "Must define either HARDWARE_SERIAL_ROLE_CONTROL or HARDWARE_SERIAL_ROLE_SENSOR"
#endif
/**
* @class ProtoSerial
* @brief Manages serial communication with Protocol Buffers for ESP32/ESP8266.
* Supports length-prefixed messages with CRC-8 checksum and sync bytes.
* Configured for SENSOR or CONTROL role via HARDWARE_SERIAL_ROLE_* macros.
* On ESP8266, uses UART0 (Serial, RX=GPIO3, TX=GPIO1). On ESP32, uses caller-provided UART.
*/
class ProtoSerial {
public:
using Callback = void (*)(const IncomingMessage&); ///< Callback for received messages.
/**
* @brief Constructor.
*/
ProtoSerial();
/**
* @brief Destructor. No-op (serial is managed by caller).
*/
~ProtoSerial();
/**
* @brief Initializes serial communication with a caller-provided serial port.
* @param serialPort Reference to a configured SerialType (e.g., Serial for ESP8266, HardwareSerial for ESP32).
*/
void begin(SerialType& serialPort);
/**
* @brief Sends a Protocol Buffers message with sync bytes and CRC-8.
* @param message The message to send (OutgoingMessage type).
* @return True if sent successfully, false otherwise.
*/
bool sendMessage(const OutgoingMessage& message);
/**
* @brief Checks for incoming messages (non-blocking).
*/
void update();
/**
* @brief Checks if a message is available.
* @return True if a message is ready to be read.
*/
bool hasMessage() const;
/**
* @brief Gets the received message.
* @return Reference to the received message (valid only if hasMessage() is true).
*/
const IncomingMessage& getMessage() const;
/**
* @brief Clears the received message.
*/
void clearMessage();
/**
* @brief Sets the callback for received messages.
* @param cb Callback function to handle incoming messages.
*/
void setCallback(Callback cb);
/**
* @brief Checks if the serial interface is initialized.
* @return True if initialized.
*/
bool isInitialized() const { return initialized; }
/**
* @brief Gets the last error message (for debugging).
* @return Last error string or empty if none.
*/
const char* getLastError() const { return lastError; }
private:
SerialType* serial; ///< Serial interface (pointer to caller-provided SerialType).
bool initialized; ///< Tracks initialization state.
char lastError[64]; ///< Last error message (fixed-size to avoid heap issues).
Callback callback; ///< Callback for received messages.
// Framing constants
static const uint8_t SYNC_START = 0xAA; ///< Start byte for message framing.
static const uint8_t SYNC_END = 0xBB; ///< End byte for message framing.
// Receive state machine
enum RxState {
WAITING_FOR_SYNC_START, ///< Waiting for start byte (0xAA).
WAITING_FOR_LENGTH, ///< Waiting for 2-byte length.
READING_PAYLOAD, ///< Reading payload bytes.
READING_CRC ///< Reading CRC-8 byte.
};
RxState currentState; ///< Current state of receive state machine.
uint16_t payloadLength; ///< Length of incoming payload.
uint8_t payloadBuffer[INCOMING_MESSAGE_SIZE + 10]; ///< Buffer for payload (plus overhead).
IncomingMessage receivedMessage; ///< Decoded incoming message.
bool messageAvailable; ///< True if a message is ready.
unsigned long lastUpdate; ///< Timestamp of last update (for throttling).
unsigned long lastByteTime; ///< Timestamp of last byte received (for timeout).
// Helper methods
/**
* @brief Processes a received payload and decodes it.
* @param buffer Payload buffer.
* @param length Payload length.
*/
void processReceivedMessage(uint8_t* buffer, uint16_t length);
/**
* @brief Calculates CRC-8 for a buffer.
* @param data Buffer to compute CRC over.
* @param len Length of buffer.
* @return CRC-8 value.
*/
uint8_t calculate_crc8(const uint8_t* data, uint16_t len);
/**
* @brief Logs a hex dump of a buffer (debug only).
* @param data Buffer to dump.
* @param len Length of buffer.
* @param label Label for the dump.
*/
void dumpHex(const uint8_t* data, uint16_t len, const char* label);
};
extern ProtoSerial pserial;
+149
View File
@@ -0,0 +1,149 @@
#include "logger.hpp"
#include <Arduino.h>
#include <string.h>
#include <stdarg.h> // For va_list, vsnprintf
#include <sys/time.h>
Logger::Logger()
: logCount(0), logIndex(0), currentLogLevel(LOG_INFO),
logCallback(nullptr), logQueue(nullptr), logTaskHandle(nullptr) {}
Logger::~Logger() {
// Clean up FreeRTOS objects
if (logTaskHandle) vTaskDelete(logTaskHandle);
if (logQueue) vQueueDelete(logQueue);
}
void Logger::begin(UBaseType_t taskPriority, uint32_t taskStackSize) {
// Create a queue that can hold up to 10 LogEntry items.
logQueue = xQueueCreate(10, sizeof(LogEntry));
if (logQueue == nullptr) {
Serial.println("FATAL: Failed to create logger queue!");
return;
}
// Create the background task. It passes 'this' so the static function
// can access the instance's members.
xTaskCreate(
logProcessingTask,
"LoggerTask",
taskStackSize,
this, // Pass the current object instance to the task
taskPriority,
&logTaskHandle
);
if (logTaskHandle == nullptr) {
Serial.println("FATAL: Failed to create logger task!");
}
}
void Logger::addLog(LogLevel level, const char* format, va_list args) {
if (level < currentLogLevel) {
return;
}
LogEntry entry;
entry.level = level;
// Verwende Systemzeit statt millis()
struct timeval tv;
if (gettimeofday(&tv, nullptr) != 0) {
LOG_ERROR("Failed to get system time for log entry");
entry.timestamp = 0; // Fallback-Wert
} else {
entry.timestamp = tv.tv_sec; // Unix-Timestamp in Sekunden
}
vsnprintf(entry.message, sizeof(entry.message), format, args);
// Add to internal circular buffer for history
logs[logIndex] = entry;
logIndex = (logIndex + 1) % MAX_LOGS;
if (logCount < MAX_LOGS) {
logCount++;
}
if (logQueue != nullptr) {
xQueueSend(logQueue, &entry, pdMS_TO_TICKS(10));
}
}
// This is the function that runs in its own task.
void Logger::logProcessingTask(void* instance) {
// The instance pointer is the 'this' we passed in xTaskCreate.
Logger* loggerInstance = static_cast<Logger*>(instance);
LogEntry entryToProcess;
while (true) {
if (xQueueReceive(loggerInstance->logQueue, &entryToProcess, portMAX_DELAY) == pdPASS) {
if (loggerInstance->logCallback) {
loggerInstance->logCallback(entryToProcess);
}
}
}
}
void Logger::setLogLevel(LogLevel level) {
currentLogLevel = level;
}
LogLevel Logger::getLogLevel() const {
return currentLogLevel;
}
void Logger::setLogCallback(std::function<void(const LogEntry& entry)> callback) {
logCallback = callback;
}
// --- NEW: Public methods now handle the variable arguments ---
void Logger::debug(const char* format, ...) {
va_list args;
va_start(args, format);
addLog(LOG_DEBUG, format, args);
va_end(args);
}
void Logger::info(const char* format, ...) {
va_list args;
va_start(args, format);
addLog(LOG_INFO, format, args);
va_end(args);
}
void Logger::warn(const char* format, ...) {
va_list args;
va_start(args, format);
addLog(LOG_WARN, format, args);
va_end(args);
}
void Logger::error(const char* format, ...) {
va_list args;
va_start(args, format);
addLog(LOG_ERROR, format, args);
va_end(args);
}
const LogEntry* Logger::getLogs(int& count) const {
count = logCount;
return logs;
}
void Logger::clear() {
logCount = 0;
logIndex = 0;
}
const char* Logger::levelToString(LogLevel level) const {
switch (level) {
case LOG_DEBUG: return "D";
case LOG_INFO: return "I";
case LOG_WARN: return "W";
case LOG_ERROR: return "E";
default: return "U";
}
}
Logger logger;
+68
View File
@@ -0,0 +1,68 @@
#pragma once
#include <stdint.h>
#include <time.h>
#include <functional> // For std::function
#include <cstdarg>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
enum LogLevel {
LOG_DEBUG,
LOG_INFO,
LOG_WARN,
LOG_ERROR
};
struct LogEntry {
LogLevel level;
time_t timestamp;
char message[128];
};
class Logger {
public:
Logger();
~Logger();
void begin(UBaseType_t taskPriority = 1, uint32_t taskStackSize = 2048);
void setLogLevel(LogLevel level);
LogLevel getLogLevel() const;
// Use the more flexible std::function for callbacks
void setLogCallback(std::function<void(const LogEntry& entry)> callback);
// --- NEW: Variadic functions for printf-style formatting ---
void debug(const char* format, ...);
void info(const char* format, ...);
void warn(const char* format, ...);
void error(const char* format, ...);
const LogEntry* getLogs(int& count) const;
void clear();
const char* levelToString(LogLevel level) const;
private:
static const int MAX_LOGS = 20;
LogEntry logs[MAX_LOGS];
int logCount;
int logIndex;
LogLevel currentLogLevel;
std::function<void(const LogEntry& entry)> logCallback;
QueueHandle_t logQueue;
TaskHandle_t logTaskHandle;
static void logProcessingTask(void* instance);
void addLog(LogLevel level, const char* format, va_list args);
};
extern Logger logger;
// --- OPTIONAL: Performance-enhancing macros ---
#define LOG_DEBUG(format, ...) if(logger.getLogLevel() <= LOG_DEBUG) { logger.debug(format, ##__VA_ARGS__); }
#define LOG_INFO(format, ...) if(logger.getLogLevel() <= LOG_INFO) { logger.info(format, ##__VA_ARGS__); }
#define LOG_WARN(format, ...) if(logger.getLogLevel() <= LOG_WARN) { logger.warn(format, ##__VA_ARGS__); }
#define LOG_ERROR(format, ...) if(logger.getLogLevel() <= LOG_ERROR) { logger.error(format, ##__VA_ARGS__); }
+94
View File
@@ -0,0 +1,94 @@
#include "ota_manager.hpp"
OTAManager::OTAManager() : enabled(false), status("Disabled") {}
OTAManager::~OTAManager() {}
void OTAManager::begin() {
ArduinoOTA.setHostname("RFID-Master-Client");
ArduinoOTA.onStart([this]() {
this->onStart();
});
ArduinoOTA.onEnd([this]() {
this->onEnd();
});
ArduinoOTA.onProgress([this](unsigned int progress, unsigned int total) {
this->onProgress(progress, total);
});
ArduinoOTA.onError([this](ota_error_t error) {
this->onError(error);
});
LOG_INFO("OTA Manager initialized");
}
void OTAManager::enable() {
if (!enabled) {
ArduinoOTA.begin();
enabled = true;
status = "Enabled";
LOG_INFO("OTA enabled");
}
}
void OTAManager::disable() {
if (enabled) {
ArduinoOTA.end();
enabled = false;
status = "Disabled";
LOG_INFO("OTA disabled");
}
}
bool OTAManager::isEnabled() const {
return enabled;
}
String OTAManager::getStatus() const {
return status;
}
void OTAManager::onStart() {
status = "Starting OTA update...";
LOG_INFO("OTA update started");
}
void OTAManager::onEnd() {
status = "OTA update completed";
LOG_INFO("OTA update completed");
}
void OTAManager::onProgress(unsigned int progress, unsigned int total) {
status = "OTA progress: " + String(progress / (total / 100)) + "%";
LOG_DEBUG("OTA progress: %u/%u", progress, total);
}
void OTAManager::onError(ota_error_t error) {
String errorMsg;
switch (error) {
case OTA_AUTH_ERROR:
errorMsg = "Auth Failed";
break;
case OTA_BEGIN_ERROR:
errorMsg = "Begin Failed";
break;
case OTA_CONNECT_ERROR:
errorMsg = "Connect Failed";
break;
case OTA_RECEIVE_ERROR:
errorMsg = "Receive Failed";
break;
case OTA_END_ERROR:
errorMsg = "End Failed";
break;
default:
errorMsg = "Unknown Error";
break;
}
status = "OTA Error: " + errorMsg;
LOG_ERROR("OTA error: %s", errorMsg.c_str());
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <ArduinoOTA.h>
#include <WiFi.h>
#include <logger.hpp>
class OTAManager {
public:
OTAManager();
~OTAManager();
void begin();
void enable();
void disable();
bool isEnabled() const;
String getStatus() const;
private:
bool enabled;
String status;
void onStart();
void onEnd();
void onProgress(unsigned int progress, unsigned int total);
void onError(ota_error_t error);
};
+6
View File
@@ -0,0 +1,6 @@
{
"name": "ota",
"dependencies": {
"hardware": "*"
}
}
+445
View File
@@ -0,0 +1,445 @@
#include "rpc.hpp"
#include <Arduino.h>
#include <WiFi.h>
#include <LittleFS.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include <vector>
#include <cstring>
#include <HTTPClient.h>
#include <control_communication.pb.h>
#include <Stream.h>
#include <logger.hpp>
#include <hardware_rtc.hpp>
#define LAST_SYNC_TIME_FILE "/last_sync.time"
// ---- Constructor / Destructor ----
RPC::RPC() {
// NOTE: Initialization is done here. Ensure this global object is created
// AFTER core systems like LittleFS are initialized in setup().
apiKey_[0] = '\0';
baseUrl_[0] = '\0';
if (!accessLogQueue) {
accessLogQueue = xQueueCreate(100, sizeof(RpcAccessLogEntry));
if (!accessLogQueue) {
LOG_ERROR("RPC: FATAL: Failed to create access log queue!");
}
}
syncTriggerSemaphore_ = xSemaphoreCreateBinary();
if (!syncTriggerSemaphore_) {
LOG_ERROR("RPC: FATAL: Failed to create sync trigger semaphore!");
}
// Load the last sync time from persistent storage on startup.
this->lastSyncTime = loadLastSyncTime();
LOG_INFO("RPC: Instance created. Loaded last sync timestamp: %s", HardwareRTC::toDateString(this->lastSyncTime).c_str());
}
RPC::~RPC() {
stopAutoSync();
if (accessLogQueue) {
vQueueDelete(accessLogQueue);
accessLogQueue = nullptr;
}
if (syncTriggerSemaphore_) {
vSemaphoreDelete(syncTriggerSemaphore_);
syncTriggerSemaphore_ = nullptr;
}
}
// ---- Public Methods ----
bool RPC::sync(bool forceDownload) {
if (isSyncing_) {
logger.warn("RPC: Sync aborted, another sync is already in progress.");
return false;
}
if (baseUrl_[0] == '\0') {
LOG_ERROR("RPC: Sync aborted (Base URL not set). Call startAutoSync first.");
return false;
}
if (WiFi.status() != WL_CONNECTED) {
LOG_ERROR("RPC: Sync aborted (no WiFi connection).");
return false;
}
isSyncing_ = true;
LOG_INFO("RPC: Starting synchronization process...");
bool pendingChanges = false;
bool syncSuccess = false;
if (!syncLogsAndCheckForUpdates(&pendingChanges)) {
LOG_ERROR("RPC: Log synchronization failed.");
syncSuccess = false;
} else {
// Log sync was successful, now check if a DB download is needed.
if (pendingChanges || forceDownload) {
const char* dbPath = "/rfid.db";
LOG_DEBUG("RPC: Pending changes: %d, forceDownload: %d", pendingChanges, forceDownload);
if (!downloadDatabaseToFile(dbPath)) {
LOG_ERROR("RPC: Failed to download database.");
syncSuccess = false;
} else {
syncSuccess = true;
}
} else {
LOG_DEBUG("RPC: No pending changes reported by server, skipping database download.");
syncSuccess = true;
}
}
isSyncing_ = false;
if(syncSuccess) {
LOG_INFO("RPC: Synchronization completed successfully.");
} else {
LOG_ERROR("RPC: Synchronization failed.");
}
return syncSuccess;
}
void RPC::addAccessLog(uint32_t rfidId) {
if (!accessLogQueue) {
return; // Queue not created, do nothing.
}
struct timeval tv;
RpcAccessLogEntry entry;
entry.timestamp = HardwareRTC::getSystemTime();
entry.rfidId = rfidId;
if (xQueueSend(accessLogQueue, &entry, 0) != pdPASS) {
LOG_WARN("RPC: Access log queue is full, dropping entry.");
return;
}
// --- TRIGGER LOGIC ---
// If the queue has reached the threshold and we can sync, trigger it.
if (uxQueueMessagesWaiting(accessLogQueue) >= LOG_SYNC_THRESHOLD && baseUrl_[0] != '\0') {
LOG_INFO("RPC: Queue size: %d. Triggering immediate sync.", uxQueueMessagesWaiting(accessLogQueue));
xSemaphoreGive(syncTriggerSemaphore_);
}
}
// ---- Auto-Sync Task Management ----
void RPC::startAutoSync(uint32_t intervalMs) {
stopAutoSync(); // Always stop any existing task to ensure a clean restart.
if (baseUrl_[0] == '\0') {
LOG_ERROR("RPC: Cannot start auto-sync. Base URL is not configured.");
return;
}
LOG_INFO("RPC: Starting auto-sync");
syncIntervalMs = intervalMs;
autoSyncRunning = true;
BaseType_t result = xTaskCreate(
autoSyncTask, "AutoSyncTask", 8192, this, 1, &autoSyncTaskHandle
);
if (result != pdPASS) {
LOG_ERROR("RPC: Failed to create auto-sync task.");
autoSyncRunning = false;
autoSyncTaskHandle = nullptr;
} else {
LOG_INFO("RPC: Auto-sync task started successfully.");
}
}
void RPC::stopAutoSync() {
if (!autoSyncTaskHandle) {
return;
}
LOG_INFO("RPC: Attempting to stop auto-sync task...");
autoSyncRunning = false; // Signal the task to exit its loop
// Wake the task if it's waiting on the semaphore
if (syncTriggerSemaphore_) {
xSemaphoreGive(syncTriggerSemaphore_);
}
// Wait for the task to terminate itself (indicated by handle becoming nullptr)
uint32_t startTime = millis();
while (autoSyncTaskHandle != nullptr && millis() - startTime < 1000) {
vTaskDelay(pdMS_TO_TICKS(50));
}
// If the task did not stop gracefully, force its deletion
if (autoSyncTaskHandle != nullptr) {
LOG_ERROR("RPC: Task did not stop gracefully. Forcing deletion.");
vTaskDelete(autoSyncTaskHandle);
autoSyncTaskHandle = nullptr;
} else {
LOG_INFO("RPC: Auto-sync task stopped gracefully.");
}
}
void RPC::setApiKeyAndBaseUrl(const char* apiKey, const char* baseUrl) {
if (apiKey) {
strncpy(apiKey_, apiKey, sizeof(apiKey_) - 1);
apiKey_[sizeof(apiKey_) - 1] = '\0'; // Ensure null-termination
} else {
apiKey_[0] = '\0'; // Set to empty string
}
if (baseUrl) {
strncpy(baseUrl_, baseUrl, sizeof(baseUrl_) - 1);
baseUrl_[sizeof(baseUrl_) - 1] = '\0';
} else {
baseUrl_[0] = '\0';
}
}
// ---- Private Helper Methods ----
HttpResponse RPC::sendRequest(const String& url, const String& method, const uint8_t* payload, size_t payloadLen, const String& contentType) {
HttpResponse response; // Default httpCode is 0
if (WiFi.status() != WL_CONNECTED) {
response.httpCode = -1; // Custom code for no WiFi
return response;
}
HTTPClient http;
http.begin(url);
http.setTimeout(10000);
if (apiKey_[0] != '\0') {
http.addHeader("x-device-key", apiKey_);
}
if (method.equalsIgnoreCase("POST")) {
http.addHeader("Content-Type", contentType);
response.httpCode = http.POST(const_cast<uint8_t*>(payload), payloadLen);
} else {
response.httpCode = http.GET();
}
if (response.httpCode > 0) {
int len = http.getSize();
if (len > 0) {
response.payload.resize(len);
WiFiClient* stream = http.getStreamPtr();
stream->readBytes(response.payload.data(), len);
}
} else {
LOG_ERROR(("RPC: HTTP " + method + " " + url + " failed: " + http.errorToString(response.httpCode)).c_str());
}
http.end(); // Encapsulated resource management
return response;
}
bool RPC::syncLogsAndCheckForUpdates(bool* pendingChanges) {
// NOTE: This implementation assumes the logs should be sent with the sync request.
// The previous code collected logs but did not send them. This version does.
std::vector<RpcAccessLogEntry> logBatch;
RpcAccessLogEntry entry;
// Drain the queue to send all pending logs.
// To be more memory efficient on larger queues, this could be done in batches.
while (xQueueReceive(accessLogQueue, &entry, 0) == pdPASS) {
logBatch.push_back(entry);
}
control_communication_SyncRequest request = control_communication_SyncRequest_init_zero;
request.has_lastSync = (lastSyncTime > 0);
request.lastSync = lastSyncTime;
request.accessLogs.arg = &logBatch;
request.accessLogs.funcs.encode = [](pb_ostream_t* stream, const pb_field_t* field, void* const* arg) -> bool {
auto logs = static_cast<std::vector<RpcAccessLogEntry>*>(*arg);
if (!logs || logs->empty()) {
return true; // No logs to encode, return success
}
for (const auto& log : *logs) {
control_communication_SyncRequest_AccessLogsEntry entry = { log.timestamp, log.rfidId };
if (!pb_encode_tag_for_field(stream, field)) {
return false;
}
if (!pb_encode_submessage(stream, control_communication_SyncRequest_AccessLogsEntry_fields, &entry)) {
return false;
}
}
return true;
};
uint8_t requestBuffer[512];
pb_ostream_t ostream = pb_ostream_from_buffer(requestBuffer, sizeof(requestBuffer));
if (!pb_encode(&ostream, control_communication_SyncRequest_fields, &request)) {
LOG_ERROR("RPC: Failed to encode sync request.");
return false;
}
String url = String(baseUrl_) + "device-communication/sync-binary";
HttpResponse httpResponse = sendRequest(url, "POST", requestBuffer, ostream.bytes_written, "application/octet-stream");
if (httpResponse.httpCode != HTTP_CODE_OK) {
LOG_ERROR("RPC: Sync logs request failed with HTTP code %d", httpResponse.httpCode)
return false;
}
// A successful connection was made.
lastSyncConnection = millis();
control_communication_SyncResponse response = control_communication_SyncResponse_init_zero;
pb_istream_t istream = pb_istream_from_buffer(httpResponse.payload.data(), httpResponse.payload.size());
if (!pb_decode(&istream, control_communication_SyncResponse_fields, &response)) {
LOG_ERROR("RPC: Failed to decode sync response.");
return false;
}
// As per your design, only update lastSyncTime if the server reports pending changes.
if (response.pendingChanges) {
this->lastSyncTime = response.currentTime;
saveLastSyncTime(this->lastSyncTime);
}
// Pass pendingChanges flag back to the caller.
if (pendingChanges) {
*pendingChanges = response.pendingChanges;
}
if(newTimeCallback_){
newTimeCallback_(response.currentTime);
}
return true;
}
bool RPC::downloadDatabaseToFile(const char* filePath) {
String url = String(baseUrl_) + "device-communication/db";
if (WiFi.status() != WL_CONNECTED) {
return false;
}
HTTPClient http;
http.begin(url);
http.setTimeout(10000);
if (apiKey_[0] != '\0') {
http.addHeader("x-device-key", apiKey_);
}
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
LOG_ERROR(("RPC: DB download failed with HTTP code " + String(httpCode)).c_str());
http.end();
return false;
}
lastSyncConnection = millis();
int len = http.getSize();
if (len <= 0) { // Removed the multiple of 4 check, as it might be too restrictive.
LOG_ERROR(("RPC: Invalid database size: " + String(len) + " bytes.").c_str());
http.end();
return false;
}
LOG_INFO(("RPC: Downloading database (" + String(len) + " bytes) to file '" + String(filePath) + "'...").c_str());
File dbFile = LittleFS.open(filePath, "w");
if (!dbFile) {
LOG_ERROR(("RPC: Failed to open " + String(filePath) + " for writing.").c_str());
http.end();
return false;
}
// Write the file content by streaming it from the WiFi client.
WiFiClient* stream = http.getStreamPtr();
const size_t bufferSize = 1024;
uint8_t buffer[bufferSize];
size_t written = 0;
// Read from the stream in chunks until all bytes are received
while (http.connected() && (written < (size_t)len)) {
// Get available data size
size_t available = stream->available();
if (available) {
// Read up to bufferSize bytes
int bytesRead = stream->read(buffer, std::min(bufferSize, available));
// Write the chunk to the file
dbFile.write(buffer, bytesRead);
written += bytesRead;
}
}
dbFile.close();
http.end();
if (written != (size_t)len) {
LOG_ERROR("RPC: File download incomplete. Wrote %d of %d bytes.", (unsigned long)written, len);
LittleFS.remove(filePath); // Delete partial file
return false;
}
LOG_INFO("RPC: Database downloaded successfully.");
return true;
}
// ---- Persistence Methods ----
void RPC::saveLastSyncTime(time_t timestamp) {
File file = LittleFS.open(LAST_SYNC_TIME_FILE, "w");
if (!file) {
LOG_ERROR("RPC: Failed to open last_sync.time for writing.");
return;
}
file.write(reinterpret_cast<const uint8_t*>(&timestamp), sizeof(timestamp));
file.close();
}
time_t RPC::loadLastSyncTime() {
if (!LittleFS.exists(LAST_SYNC_TIME_FILE)) {
return 0;
}
File file = LittleFS.open(LAST_SYNC_TIME_FILE, "r");
if (!file || file.size() != sizeof(time_t)) {
LOG_ERROR("RPC: Corrupt or unreadable timestamp file. Resetting to 0.");
if(file) file.close();
LittleFS.remove(LAST_SYNC_TIME_FILE);
return 0;
}
time_t timestamp = 0;
file.read(reinterpret_cast<uint8_t*>(&timestamp), sizeof(timestamp));
file.close();
return timestamp;
}
// ---- FreeRTOS Task ----
void RPC::autoSyncTask(void* pvParameters) {
RPC* rpc = static_cast<RPC*>(pvParameters);
while (rpc->autoSyncRunning) {
// Wait for either the timer to expire OR the trigger semaphore to be given.
if (xSemaphoreTake(rpc->syncTriggerSemaphore_, pdMS_TO_TICKS(rpc->syncIntervalMs)) == pdTRUE) {
LOG_INFO("RPC: Sync triggered by log queue.");
} else {
LOG_INFO("RPC: Sync triggered by timer.");
}
if (!rpc->autoSyncRunning) {
break; // Exit immediately if stop was called while waiting
}
rpc->sync(); // Call sync without forcing a download
}
// Safer handshake to signal graceful shutdown
rpc->autoSyncTaskHandle = nullptr;
vTaskDelete(NULL); // Task deletes itself
}
RPC rpc;
+155
View File
@@ -0,0 +1,155 @@
#pragma once
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <freertos/semphr.h> // Required for semaphores
#include <vector>
/**
* @struct RpcAccessLogEntry
* @brief Holds a single access log record to be queued for synchronization.
*/
struct RpcAccessLogEntry {
time_t timestamp;
uint32_t rfidId;
};
/**
* @struct HttpResponse
* @brief Encapsulates the result of an HTTP request.
*/
struct HttpResponse {
int httpCode = 0;
std::vector<uint8_t> payload;
};
/**
* @class RPC
* @brief Manages remote procedure calls (RPC) for device synchronization.
* * This class handles queuing access logs, synchronizing them with a server,
* and downloading updated data. It uses a background FreeRTOS task for
* automatic, periodic synchronization.
*/
class RPC {
public:
RPC();
~RPC();
/**
* @brief Performs a full, one-time synchronization cycle.
* @param forceDownload If true, the database will be downloaded even if the
* server reports no pending changes.
* @return true on success, false on failure.
*/
bool sync(bool forceDownload = false);
/**
* @brief Adds an access log entry to the queue for the next sync cycle.
* If the queue reaches a threshold, a sync may be triggered.
* @param rfidId The RFID ID that was accessed.
*/
void addAccessLog(uint32_t rfidId);
/**
* @brief Starts or restarts the automatic background synchronization task.
* @param intervalMs The sync interval in milliseconds.
*/
void startAutoSync(uint32_t intervalMs);
/**
* @brief Stops the automatic synchronization task gracefully.
*/
void stopAutoSync();
/**
* @brief Sets the API key and base URL for server communication.
*/
void setApiKeyAndBaseUrl(const char* apiKey, const char* baseUrl);
/**
* @brief Gets the timestamp (from millis()) of the last successful communication.
* @return The last connection timestamp, or 0 if never connected.
*/
time_t getLastConnectTime() const { return lastSyncConnection; }
/**
* @brief Checks if a sync operation is currently in progress.
* @return true if syncing, false otherwise.
*/
bool isSyncing() const { return isSyncing_; }
/**
* @brief Sets a callback function to get the current time in seconds since epoch.
* This is used to set the lastSync time during sync operations.
* @param callback A function that returns the current time as time_t.
*/
void setNewTimeCallback(std::function<void(time_t)> callback) {
newTimeCallback_ = callback;
}
private:
// --- Member Variables ---
// Configuration
char apiKey_[65]; // 64 chars + 1 null-terminator
char baseUrl_[257]; // 256 chars + 1 null-terminator
// State
time_t lastSyncTime = 0;
time_t lastSyncConnection = 0;
bool isSyncing_ = false;
// FreeRTOS components
QueueHandle_t accessLogQueue = nullptr;
TaskHandle_t autoSyncTaskHandle = nullptr;
SemaphoreHandle_t syncTriggerSemaphore_ = nullptr;
uint32_t syncIntervalMs = 300000; // Default: 5 minutes
bool autoSyncRunning = false;
// Constants
static constexpr uint8_t LOG_SYNC_THRESHOLD = 20;
// --- Private Helper Methods ---
/**
* @brief Sends an HTTP request and returns the full response.
* This method is self-contained and manages the HTTPClient lifecycle.
*/
HttpResponse sendRequest(const String& url, const String& method = "GET",
const uint8_t* payload = nullptr, size_t payloadLen = 0,
const String& contentType = "");
/**
* @brief Streams an HTTP GET response directly to a file on LittleFS.
* This is memory-efficient for large files like a database.
*/
bool downloadDatabaseToFile(const char* filePath);
/**
* @brief Sends queued logs to the server and checks if updates are pending.
* This is the first step in the sync process.
*/
bool syncLogsAndCheckForUpdates(bool* pendingChanges);
/**
* @brief Saves the last successful database download timestamp to flash.
*/
void saveLastSyncTime(time_t timestamp);
/**
* @brief Loads the last successful database download timestamp from flash.
*/
time_t loadLastSyncTime();
/**
* @brief The static function that runs as the FreeRTOS background task.
*/
static void autoSyncTask(void* pvParameters);
std::function<void(time_t)> newTimeCallback_;
};
extern RPC rpc;
+3
View File
@@ -0,0 +1,3 @@
#include "tft.hpp"
TFT tft;
+48
View File
@@ -0,0 +1,48 @@
#include <Adafruit_ST7735.h>
#include <Print.h> // Wichtig, um von der Print-Klasse erben zu können
#define TFT_CS 5
#define TFT_RST 4
#define TFT_DC 19
#define TFT_MOSI 23
#define TFT_SCLK 18
class TFT : public Print {
public:
void setup() {
// Initialize the TFT display
// Setup SPI and display parameters
tft.initR(INITR_144GREENTAB); // Initialize a ST7735S chip, green tab
tft.fillScreen(ST77XX_BLACK);
tft.setRotation(1); // Landscape orientation
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(1);
//tft.setScrollDefinition(0, tft.height(), 0);
tft.setCursor(0, 0);
}
virtual size_t write(uint8_t c) override {
return tft.write(c);
}
Adafruit_ST7735& getGfx() {
return tft;
}
void checkCursor() {
if (tft.getCursorY() >= tft.height()) {
tft.setCursor(0, 0);
tft.fillScreen(ST77XX_BLACK);
}
}
void resetCursor() {
tft.fillScreen(ST77XX_BLACK);
tft.setCursor(0, 0);
}
private:
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);
};
extern TFT tft; // Deine globale Instanz