This commit is contained in:
2025-10-06 18:27:50 +02:00
commit 3e191a4f60
213 changed files with 22261 additions and 0 deletions
+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;