init
This commit is contained in:
+46
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "hardware",
|
||||
"dependencies": {
|
||||
"nanopb": "*"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 = ⌖
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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(×tamp);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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__); }
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "ota",
|
||||
"dependencies": {
|
||||
"hardware": "*"
|
||||
}
|
||||
}
|
||||
+445
@@ -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*>(×tamp), 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*>(×tamp), 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
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "tft.hpp"
|
||||
|
||||
TFT tft;
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user