diff --git a/src/components/RS485.cpp b/src/components/RS485.cpp new file mode 100644 index 00000000..d3602ed1 --- /dev/null +++ b/src/components/RS485.cpp @@ -0,0 +1,243 @@ +#include "RS485.h" +#include +#include "Logger.h" +#include +#include +#include +#include +#include + + +#include "RS485Devices.h" // registerApplicationDevices + +RS485* RS485::instance = nullptr; + +RS485::RS485(Component *owner) + : Component("RS485", COMPONENT_KEY_RS485, COMPONENT_DEFAULT, owner), + modbus(REDEPIN_MODBUS, 32) +{ + this->owner = owner; + RS485::instance = this; + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); +} +RS485::~RS485() +{ + Log.infoln("RS485 component destroyed."); +} +short RS485::setup() +{ + Log.noticeln(F("--- Setting up RS485 Interface ---")); + pinMode(REDEPIN_MODBUS, OUTPUT); // Use define from ModbusTypes.h or similar + digitalWrite(REDEPIN_MODBUS, LOW); // Use define from ModbusTypes.h or similar + RTUutils::prepareHardwareSerial(RS485_SERIAL_PORT); + RS485_SERIAL_PORT.begin(MB_RTU_BAUDRATE, SERIAL_8N1, RXD1_PIN, TXD1_PIN); + if (!RS485_SERIAL_PORT) + { // Basic check if Serial port failed + Log.errorln(F("RS485: Failed to begin Serial port!")); + return E_SERIAL_INIT_FAILED; + } + // 3. Initialize Modbus RTU Master/Client + MB_Error initResult = modbus.begin(RS485_SERIAL_PORT, MB_RTU_BAUDRATE); + if (initResult == MB_Error::Success) + { + Log.noticeln(F("RS485: ModbusRTU initialized successfully.")); + modbus.setOnRegisterChangeCallback(RS485::staticRtuRegisterChangeCallback); + modbus.setResponseCallback(Manager::responseCallback); + modbus.setOnErrorCallback(Manager::staticOnError); + deviceManager.setAsGlobalInstance(); + +#ifdef ENABLE_RS485_DEVICES + RS485Devices::registerApplicationDevices(this); + Log.infoln(F("RS485: Application device registration triggered via RS485Devices.")); +#endif + } + else + { + Log.errorln(F("RS485: ModbusRTU initialization failed! Error: %d"), static_cast(initResult)); + return E_MODBUS_INIT_FAILED; // Use defined error code + } + deviceManager.initializeDevices(modbus); + deviceManager.printDeviceStatuses(modbus); + return E_OK; +} +short RS485::loop() +{ + unsigned long now = millis(); + if (now - lastLoopTime >= RS485_LOOP_INTERVAL_MS) + { + lastLoopTime = now; + modbus.process(); + deviceManager.processDevices(modbus); + } + return E_OK; +} +short RS485::mb_tcp_read(short address) +{ + // TODO: Implement TCP read delegation + // 1. Find the RTU_Base* device responsible for this TCP address (requires mapping info) + // 2. Call a method on that device (e.g., readTcpMappedValue(address)) to get the value. + // Log.warningln(F("RS485::mb_tcp_read STUB for address %d"), address); + return E_NOT_IMPLEMENTED; +} +short RS485::mb_tcp_write(short address, short value) +{ + // TODO: Implement TCP write delegation + // 1. Find the RTU_Base* device responsible for this TCP address (requires mapping info). + // 2. Call a method on that device (e.g., writeTcpMappedValue(address, value)). + // Log.warningln(F("RS485::mb_tcp_write STUB for address %d, value %d"), address, value); + return E_NOT_IMPLEMENTED; +} +short RS485::mb_tcp_read(MB_Registers *reg) +{ + if (!reg) + return E_INVALID_PARAMETER; + + RTU_Base *targetDevice = deviceManager.getDeviceById(reg->slaveId); + if (targetDevice) + { + return targetDevice->mb_tcp_read(reg); + } + else + { + Log.errorln(F("RS485::mb_tcp_read - Device not found for Slave ID %d (from MB_Registers for TCP %d)"), reg->slaveId, reg->startAddress); + return (short)MB_Error::ServerDeviceFailure; + } +} +short RS485::mb_tcp_write(MB_Registers *reg, short value) +{ + if (!reg || !reg->slaveId) + return E_INVALID_PARAMETER; + if (reg->slaveId == 0 || reg->slaveId > MAX_MODBUS_DEVICES) + { + Log.warningln(F("RS485::mb_tcp_write - Invalid Slave ID %d in MB_Registers for TCP Addr %d."), reg->slaveId, reg->startAddress); + return (short)MB_Error::ServerDeviceFailure; // Indicate internal error + } + + RTU_Base *targetDevice = deviceManager.getDeviceById(reg->slaveId); + if (targetDevice) + { + return targetDevice->mb_tcp_write(reg, value); + } + else + { + Log.errorln(F("RS485::mb_tcp_write - Device not found for Slave ID %d (from MB_Registers for TCP %d)"), reg->slaveId, reg->startAddress); + return (short)MB_Error::ServerDeviceFailure; // Device specified by mapping doesn't exist + } +} +void RS485::mb_tcp_register(ModbusTCP *manager) const +{ + if (!manager) + { + Log.errorln(F("RS485::mb_tcp_register: ModbusTCP manager is null!")); + return; + } + + // Check if this component itself should be registered (it acts as a gateway) + if (!hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS)) + { + Log.warningln(F("RS485::mb_tcp_register: Gateway component lacks E_NCAPS_MODBUS flag. Skipping registration.")); + // Consider setting the flag in the constructor or setup if this registration should always happen. + return; + } + + Log.infoln(F("RS485::mb_tcp_register: Registering TCP blocks for managed RTU devices (Gateway ID: %d)..."), this->id); + + // Get the array of device pointers and the size + RTU_Base *const *devices = deviceManager.getDevices(); + int maxDevices = deviceManager.getMaxDevices(); + int totalRegisteredBlocks = 0; + + // Cast self to non-const for registerModbus call + // Ensure this is safe and appropriate based on registerModbus's contract + Component *thiz = const_cast(this); + + // Iterate through the device array + for (int i = 0; i < maxDevices; ++i) + { + const RTU_Base *device = devices[i]; + if (device) + { // Check if the slot is not null + + // Get the block definitions from the device + // ASSUMPTION: RTU_Base (or derived class) has mb_tcp_blocks() + ModbusBlockView *deviceBlocks = device->mb_tcp_blocks(); // User confirmed this method name + + if (deviceBlocks && deviceBlocks->data && deviceBlocks->count > 0) + { // Use ->data based on PHApp example +// Log device name if available, otherwise just ID +#ifdef RTU_BASE_HAS_DEVICE_NAME // Check if RTU_Base eventually gets deviceName + Log.verboseln(F("RS485: Device ID %d (%s) provided %d TCP block(s). Registering with Gateway ID %d."), + device->slaveId, device->deviceName.c_str(), deviceBlocks->count, this->id); +#else + Log.verboseln(F("RS485: Device ID %d provided %d TCP block(s). Registering with Gateway ID %d."), + device->slaveId, deviceBlocks->count, this->id); +#endif + + // Iterate through the blocks provided by this specific device + for (int blockIdx = 0; blockIdx < deviceBlocks->count; ++blockIdx) + { + MB_Registers info = deviceBlocks->data[blockIdx]; // Use ->data + + // CRITICAL: Associate the block with the GATEWAY component ID + info.componentId = this->id; + + // Register this block with the TCP manager using the gateway component pointer + manager->registerModbus(thiz, info); + totalRegisteredBlocks++; + } + // Memory Management Note: If mb_tcp_blocks allocates, it needs freeing. Assume static/managed for now. + } + else + { + Log.verboseln(F("RS485: Device ID %d provided no TCP blocks."), device->slaveId); + } + } + } + + Log.infoln(F("RS485::mb_tcp_register: Finished. Registered %d total blocks from managed devices."), totalRegisteredBlocks); +} +ushort RS485::mb_tcp_error(MB_Registers *reg) +{ + if (!reg) + return E_INVALID_PARAMETER; + + RTU_Base *device = deviceManager.getDeviceById(reg->slaveId); + if (device) + { + return device->mb_tcp_error(reg); + } + else + { + Log.errorln(F("RS485::mb_tcp_error - Device not found for Slave ID %d (from MB_Registers for TCP %d)"), reg->slaveId, reg->startAddress); + return E_INVALID_PARAMETER; + } +} +void RS485::staticRtuRegisterChangeCallback(const ModbusOperation &op, uint16_t oldValue, uint16_t newValue) +{ + if (RS485::instance) { + RS485::instance->handleRtuRegisterChange(op, oldValue, newValue); + } +} +void RS485::handleRtuRegisterChange(const ModbusOperation &op, uint16_t oldValue, uint16_t newValue) +{ + deviceManager.handleRegisterChange(op, oldValue, newValue); + RTU_Base *rtuDevice = deviceManager.getDeviceById(op.slaveId); + if (!rtuDevice) + { + Log.warningln(F("[RS485::handleRtuRegisterChange] Device not found for Slave ID %d. Cannot translate for broadcast."), op.slaveId); + return; + } + uint16_t tcpBaseAddress = rtuDevice->mb_tcp_base_address(); + uint16_t tcpOffset = rtuDevice->mb_tcp_offset_for_rtu_address(op.address); + if (tcpBaseAddress == 0 || tcpOffset == 0) + { + return; + } + uint16_t tcpAddress = tcpBaseAddress + tcpOffset; + RtuUpdateData update; + update.slaveId = op.slaveId; + update.address = tcpAddress; + update.value = newValue; + update.functionCode = op.type; + owner->onMessage(this->id, E_CALLS::EC_USER, E_MessageFlags::E_MF_NONE, &update, this); +} diff --git a/src/components/RS485.h b/src/components/RS485.h new file mode 100644 index 00000000..2cd36f8d --- /dev/null +++ b/src/components/RS485.h @@ -0,0 +1,41 @@ +#ifndef RS485_H +#define RS485_H + +#include +#include +#include +#include +#include + +#include "config-modbus.h" // application modbus config + +class ModbusTCP; +class ModbusBlockView; + +class RS485 : public Component { +public: + RS485(Component *owner); + virtual ~RS485(); + + short setup() override; + short loop() override; + + short mb_tcp_read(short address) override; + short mb_tcp_write(short address, short value) override; + + short mb_tcp_read(MB_Registers * reg); + short mb_tcp_write(MB_Registers * reg, short value); + ushort mb_tcp_error(MB_Registers * reg); + void mb_tcp_register(ModbusTCP* manager) const override; + + ModbusRTU modbus; // RTU Master instance + Manager deviceManager; // Manages RTU slave devices + ModbusTCP* manager; + +private: + static RS485* instance; + static void staticRtuRegisterChangeCallback(const ModbusOperation &op, uint16_t oldValue, uint16_t newValue); + void handleRtuRegisterChange(const ModbusOperation &op, uint16_t oldValue, uint16_t newValue); + unsigned long lastLoopTime = 0; +}; +#endif // RS485_H \ No newline at end of file diff --git a/src/components/Relay.h b/src/components/Relay.h index 01c73642..fc5e0f5c 100644 --- a/src/components/Relay.h +++ b/src/components/Relay.h @@ -7,8 +7,10 @@ #include #include "config.h" #include -#include "config-modbus.h" #include + +#include "config-modbus.h" // application-specific modbus configuration + class Bridge; class Relay : public Component { diff --git a/src/components/SAKO_VFD.cpp b/src/components/SAKO_VFD.cpp index 86f24fee..4344ea2a 100644 --- a/src/components/SAKO_VFD.cpp +++ b/src/components/SAKO_VFD.cpp @@ -3,15 +3,15 @@ #ifdef ENABLE_RS485 #include -#include "error_codes.h" -#include "components/SAKO_VFD.h" - #include #include -#include "RS485.h" -#include "SakoTypes.h" -#include "Sako-Registers.h" +#include #include +#include + +#include "./SAKO_VFD.h" +#include "./SakoTypes.h" +#include "./Sako-Registers.h" #define SAKO_MB_MONITOR_REGS 10 #define SAKO_MB_TCP_OFFSET COMPONENT_KEY_SAKO_VFD * 10 diff --git a/src/error_codes.h b/src/error_codes.h index a3f3c22b..5b5dc55c 100644 --- a/src/error_codes.h +++ b/src/error_codes.h @@ -1,6 +1,3 @@ #ifndef ERROR_CODES_H #define ERROR_CODES_H - - - #endif diff --git a/src/modbus/Modbus.h b/src/modbus/Modbus.h index c69bd422..2d7c7d7e 100644 --- a/src/modbus/Modbus.h +++ b/src/modbus/Modbus.h @@ -31,7 +31,7 @@ * @param group Group identifier for organization * @ingroup ModbusMacros */ -#define INIT_MODBLUSE_BLOCK_TCP(tcpBaseAddr, offset_enum, fn_code, access, desc, group) \ +#define INIT_MODBUS_BLOCK_TCP(tcpBaseAddr, offset_enum, fn_code, access, desc, group) \ { \ static_cast(tcpBaseAddr + static_cast(offset_enum)), \ 1, \ @@ -53,7 +53,7 @@ * @param group Group identifier for organization * @ingroup ModbusMacros */ -#define INIT_MODBUSE_BLOCK(offset_enum, fn_code, access, desc, group) \ +#define INIT_MODBUS_BLOCK(offset_enum, fn_code, access, desc, group) \ { \ static_cast(tcpBaseAddr + static_cast(offset_enum)), \ 1, \ @@ -67,4 +67,4 @@ /** @} */ -#indif +#endif diff --git a/src/modbus/ModbusTypes.cpp b/src/modbus/ModbusTypes.cpp index 2a6e4ea3..9267bb50 100644 --- a/src/modbus/ModbusTypes.cpp +++ b/src/modbus/ModbusTypes.cpp @@ -1,7 +1,9 @@ #include -#include "ModbusTypes.h" -#include "ModbusRTU.h" -#include "RS485.h" + +#include +#include + +#include bool RTU_Base::triggerRTUWrite() { diff --git a/src/modbus/Modbus_audit.md b/src/modbus/Modbus_audit.md new file mode 100644 index 00000000..6446817c --- /dev/null +++ b/src/modbus/Modbus_audit.md @@ -0,0 +1,82 @@ +# Security and Performance Audit for Modbus.h + +## File Information + +- **Path**: src/modbus/Modbus.h +- **Purpose**: Header file for Modbus communication in ESP-32 industrial applications +- **Author**: Polymech Development Team + +## Findings + +### Security Considerations + +#### 1. Memory Safety + +**Issue**: The macros do fixed size initializations without boundary checks + +```c +#define INIT_MODBLUSE_BLOCK_TCP(tcpBaseAddr, offset_enum, fn_code, access, desc, group) \ +{ \ +static_cast(tcpBaseAddr + static_cast(offset_enum)), \ +1, \ +fn_code, \ +access, \ +this->id, \ +this->slaveId, \ +desc, \ +group \ +} +``` + +**Risk**: If `desc` or `group` are string literals passed to these macros, there is no length validation in the macro itself. This could potentially lead to buffer overflows if the strings exceed expected lengths in the struct that's being initialized. + +**Recommendation**: Add explicit string length checks if these are being copied to fixed-size buffers in the target structure. + +#### 2. Address Calculation Safety + +**Issue**: The address calculation uses direct addition which could potentially overflow if large values are provided + +```c +static_cast(tcpBaseAddr + static_cast(offset_enum)) +``` + +**Risk**: Integer overflow could occur if `tcpBaseAddr` + `offset_enum` exceeds `USHRT_MAX` (65535). This might lead to addressing wrong memory locations. + +**Recommendation**: Add guards to check for potential overflow or use safer arithmetic methods: + +```c +if ((USHRT_MAX - static_cast(offset_enum)) < tcpBaseAddr) { +// Handle error - would overflow +} +``` + +### Performance Considerations + +#### 1. Macro Usage + +**Issue**: Extensive use of macros with multiple parameters + +**Risk**: While not a performance issue per se, complex macros can make debugging difficult as they expand inline, potentially increasing code size if used frequently. + +**Recommendation**: Consider using inline functions instead of macros for better type safety and debugging: + +```cpp +template +inline auto init_modbus_block(ushort tcpBaseAddr, T offset_enum, uint8_t fn_code, /* other params */) { +return /* struct initialization */; +} +``` + +#### 2. Code Structure + +**Issue**: There is a typo in one macro name (`INIT_MODBLUSE_BLOCK_TCP` instead of `INIT_MODBUS_BLOCK_TCP`) + +**Risk**: This could lead to consistency issues and confusion for developers. + +**Recommendation**: Fix the typo to maintain naming consistency across the codebase. + +## Summary + +The Modbus.h header file provides macro utilities for initializing Modbus communication structures. While functionally sound, there are potential security issues related to memory safety and address calculations that should be addressed. Additionally, replacing macros with inline functions would improve type safety and debuggability. + +Overall, the code structure is clean and meets industrial application requirements, but the suggested improvements would enhance robustness and maintainability. diff --git a/src/profiles/PlotBase.cpp b/src/profiles/PlotBase.cpp new file mode 100644 index 00000000..09a00146 --- /dev/null +++ b/src/profiles/PlotBase.cpp @@ -0,0 +1,198 @@ +#include "PlotBase.h" + +//------------------------------------------------------------------------------ +// Base Class: PlotBase Implementation +//------------------------------------------------------------------------------ + +// Default implementations for Component methods if needed +// void PlotBase::setup() { /* Base setup if any */ } +// void PlotBase::loop() { /* Base loop if any */ } + +bool PlotBase::loadFromJsonObject(const JsonObject& config) { + // Reset state + _running = false; + _durationMs = 0; + _startTimeMs = 0; + + // Assume 'duration' exists and is valid uint32_t > 0 + // Optional: Add check config.containsKey("duration") if data isn't guaranteed + _durationMs = config["duration"].as(); + if (_durationMs == 0) { + // A plot with zero duration is generally invalid + // Serial.println(F("[PlotBase] Error: Duration cannot be zero.")); // Optional logging + return false; + } + + // Call derived class implementation for specific fields + return load(config); +} + +void PlotBase::start() { + // Can only start if duration is set + if (_durationMs > 0) { + _startTimeMs = millis(); + _elapsedMsAtPause = 0; + _running = true; + _paused = false; + } else { + _running = false; + _paused = false; + } +} + +void PlotBase::stop() { + _running = false; + _paused = false; + _elapsedMsAtPause = 0; +} + +void PlotBase::pause() { + if (_running && !_paused) { + // Calculate elapsed time accurately at the point of pause + uint32_t now = millis(); + uint32_t currentElapsed = 0; + if (now >= _startTimeMs) { + currentElapsed = now - _startTimeMs; + } else { // Rollover handled + currentElapsed = (ULONG_MAX - _startTimeMs) + now + 1; + } + // Store the clamped elapsed time + _elapsedMsAtPause = min(currentElapsed, _durationMs); + _paused = true; + // _running remains true + } +} + +void PlotBase::resume() { + if (_running && _paused) { + // Calculate the new startTime to continue from the paused time + uint32_t now = millis(); + // Adjust start time backwards by the time already elapsed when paused + if (now >= _elapsedMsAtPause) { + _startTimeMs = now - _elapsedMsAtPause; + } else { // Handle rollover + _startTimeMs = ULONG_MAX - (_elapsedMsAtPause - now - 1); + } + _paused = false; + } +} + +void PlotBase::seek(uint32_t targetMs) { + // Clamp target time to valid range + if (targetMs > _durationMs) { + targetMs = _durationMs; + } + + if (!_running) { + // Don't allow seeking if the plot hasn't even been started. + // Or maybe set _elapsedMsAtPause and allow start() to pick it up? + // For now, do nothing if IDLE. + return; + } + + if (_paused) { + // If paused, just update the stored pause time. + // resume() will use this value later. + _elapsedMsAtPause = targetMs; + } else { + // If running, adjust the start time to reflect the seek. + uint32_t now = millis(); + if (now >= targetMs) { + _startTimeMs = now - targetMs; + } else { + _startTimeMs = ULONG_MAX - (targetMs - now - 1); + } + } +} + +uint32_t PlotBase::getElapsedMs() const { + if (_paused) { + // If paused, return the time elapsed when pause was called + return _elapsedMsAtPause; + } + + if (!_running) { + return 0; // Not running, not paused -> 0 elapsed + } + + // Running and not paused: calculate current elapsed time + uint32_t currentTime = millis(); + uint32_t elapsedMs = 0; + + // Handle millis() rollover + if (currentTime >= _startTimeMs) { + elapsedMs = currentTime - _startTimeMs; + } else { + // Rollover occurred + elapsedMs = (ULONG_MAX - _startTimeMs) + currentTime + 1; + } + + // Clamp to duration + return min(elapsedMs, _durationMs); +} + +uint32_t PlotBase::getRemainingTime() const { + if (!_running) { + return 0; // Not running, no time remaining + } + uint32_t elapsed = getElapsedMs(); // Already clamped to duration + if (elapsed >= _durationMs) { + return 0; // Already finished + } + return _durationMs - elapsed; +} + +PlotStatus PlotBase::getCurrentStatus() const { + if (_durationMs == 0) { // Should not happen if loaded correctly + return PlotStatus::IDLE; + } + + // Calculate current potential elapsed time *without clamping* + uint32_t currentTime = millis(); + uint32_t elapsedMsUnclamped = 0; + bool everStarted = (_startTimeMs != 0 || _elapsedMsAtPause > 0 || _running); // Heuristic: has time potentially progressed? + + if (everStarted) { + // Use _elapsedMsAtPause if paused, otherwise calculate from _startTimeMs + if (_paused) { + elapsedMsUnclamped = _elapsedMsAtPause; // Elapsed time is frozen at pause time + } else if (_running) { + if (currentTime >= _startTimeMs) { + elapsedMsUnclamped = currentTime - _startTimeMs; + } else { + // Rollover occurred since start/resume + elapsedMsUnclamped = (ULONG_MAX - _startTimeMs) + currentTime + 1; + } + } else { + // It was stopped. What was the elapsed time *then*? We don't store it. + // We only know the _startTimeMs it *would* have had if it were still running. + // Let's use _elapsedMsAtPause if it was paused then stopped? + // If it was running then stopped, we can't know the exact finish time. + // Simplification: If duration is reached, it's FINISHED. Otherwise IDLE. + if (_startTimeMs != 0) { // Check if it ever started to avoid calculating based on 0 + if (currentTime >= _startTimeMs) { + elapsedMsUnclamped = currentTime - _startTimeMs; + } else { + elapsedMsUnclamped = (ULONG_MAX - _startTimeMs) + currentTime + 1; + } + } else { + elapsedMsUnclamped = _elapsedMsAtPause; // Best guess if stopped after pause + } + } + } + + // Determine status based on state flags and calculated time + if (elapsedMsUnclamped >= _durationMs) { + // If time is up, it's Finished, regardless of run/pause state? Yes. + return PlotStatus::FINISHED; + } else if (_paused) { + // If not finished and paused flag is set + return PlotStatus::PAUSED; + } else if (_running) { + // If not finished, not paused, and running flag is set + return PlotStatus::RUNNING; + } else { + // Otherwise (not running, not paused, not finished) + return PlotStatus::IDLE; + } +} \ No newline at end of file diff --git a/src/profiles/PlotBase.h b/src/profiles/PlotBase.h new file mode 100644 index 00000000..6ab0dc5a --- /dev/null +++ b/src/profiles/PlotBase.h @@ -0,0 +1,170 @@ +#ifndef PLOT_BASE_H +#define PLOT_BASE_H + +#include +#include +#include // For millis(), min() +#include // For ULONG_MAX +#include +#include "config.h" +//------------------------------------------------------------------------------ +// Status Enum +//------------------------------------------------------------------------------ +enum class PlotStatus { + IDLE, // Not started or stopped before completion + RUNNING, // Actively running + PAUSED, // Started, but currently paused + FINISHED // Reached or exceeded duration +}; + +//------------------------------------------------------------------------------ +// Base Class: PlotBase +//------------------------------------------------------------------------------ + +/** + * @brief Base class for representing time-based signal plots. + * Inherits from Component and handles common timeline aspects like duration, + * running state, and loading the duration from JSON. + */ +class PlotBase : public Component { // Ensure inheritance is active +public: + PlotBase(Component* owner) : + Component("PlotBase", COMPONENT_KEY_PROFILE_START, Component::COMPONENT_DEFAULT, owner), + _durationMs(0), _startTimeMs(0), _elapsedMsAtPause(0), _running(false), _paused(false), _userData(nullptr) {} + virtual ~PlotBase() = default; + /** + * @brief Loads configuration from a JSON object. + * Parses common field 'duration' and calls the pure virtual load. + * Assumes the caller provides the correct JSON object for the specific derived type. + * Assumes data is sanitized/valid as per user request. + * + * @param config The JsonObject containing the configuration for this plot. + * @return true if duration parsing was okay and specific loading succeeded, false otherwise. + */ + virtual bool loadFromJsonObject(const JsonObject& config); + + // --- Plot Control --- + + /** + * @brief Starts the plot execution timer. + */ + virtual void start(); + + /** + * @brief Stops the plot execution timer and resets pause state. + */ + virtual void stop(); + + /** + * @brief Pauses the plot execution timer if running. + * Stores the elapsed time at the moment of pausing. + */ + virtual void pause(); + + /** + * @brief Resumes the plot execution timer if paused. + * Calculates a new start time based on the time elapsed before pausing. + */ + virtual void resume(); + + /** + * @brief Sets the current position within the plot to a specific time. + * If the plot is running, it adjusts the start time. + * If the plot is paused, it adjusts the elapsed time stored at pause. + * Does nothing if the plot is IDLE. + * + * @param targetMs The target elapsed time in milliseconds to seek to. + * Value will be clamped between 0 and the plot duration. + */ + virtual void seek(uint32_t targetMs); + + /** + * @brief Checks if the plot is currently running (actively progressing). + * @return true if running, false otherwise. + */ + bool isRunning() const { return _running; } + + /** + * @brief Checks if the plot is currently paused. + * @return true if paused, false otherwise. + */ + bool isPaused() const { return _paused; } + + /** + * @brief Gets the total duration of the plot. + * @return Plot duration in milliseconds. + */ + uint32_t getDuration() const { return _durationMs; } + + /** + * @brief Gets the remaining time in the plot based on the current elapsed time. + * @return Remaining time in milliseconds. Returns 0 if the plot is not running or has finished. + */ + uint32_t getRemainingTime() const; + + /** + * @brief Gets the current operational status of the plot. + * @return PlotStatus enum value (IDLE, RUNNING, PAUSED, FINISHED). + */ + PlotStatus getCurrentStatus() const; + + /** + * @brief Gets information about the control point defining the current state/value. + * Derived classes implement this to return details about the active point/segment. + * + * @param[out] outId ID of the active point (or relevant signal). Set appropriately by derived class. + * @param[out] outTimeMs Start time (timeMs or absolute time) of the active point/segment. + * @param[out] outValue Current calculated/active value or state. + * @param[out] outUser Custom user value associated with the active point (if applicable). + * @return true if currently running and a point/segment is active, false otherwise. + */ + virtual bool getCurrentControlPointInfo(uint8_t& outId, uint32_t& outTimeMs, int16_t& outValue, int16_t& outUser) const = 0; + + /** + * @brief Sets the user data pointer associated with this plot. + * The PlotBase class does not manage the lifetime of this data. + * @param data Pointer to user data. + */ + inline void setUserData(void* data) { + _userData = data; + } + + /** + * @brief Gets the user data pointer, casting it to the specified type. + * It's the user's responsibility to ensure the requested type T matches + * the type originally stored. + * @tparam T The type to cast the user data pointer to. + * @return Pointer to the user data as type T*, or nullptr if no user data was set. + */ + template + inline T* getUserData() const { + return static_cast(_userData); + } + + /** + * @brief Loads type-specific configuration from the JSON object. + * To be implemented by derived classes (e.g., parse 'controlPoints'). + * + * @param config The JsonObject containing the configuration. + * @return true if specific loading was successful, false otherwise. + */ + virtual bool load(const JsonObject& config) = 0; + +protected: + + /** + * @brief Calculates the elapsed time since start(), handling rollover. + * @return Elapsed time in milliseconds, clamped to duration. Returns 0 if not running. + */ + uint32_t getElapsedMs() const; + + + uint32_t _durationMs; + uint32_t _startTimeMs; + uint32_t _elapsedMsAtPause; // Stores elapsed time when pause() is called + bool _running; // True if started and not stopped + bool _paused; // True if pause() called while running + void* _userData; // Pointer for arbitrary user data +}; + +#endif // PLOT_BASE_H \ No newline at end of file diff --git a/src/profiles/SignalPlot.cpp b/src/profiles/SignalPlot.cpp new file mode 100644 index 00000000..2759b521 --- /dev/null +++ b/src/profiles/SignalPlot.cpp @@ -0,0 +1,131 @@ +#include "SignalPlot.h" +#include // For round() - No longer needed unless used elsewhere + +SignalPlot::SignalPlot(Component* owner) : PlotBase(owner), _numControlPoints(0) { + // Initialize control points array + for (int i = 0; i < MAX_SIGNAL_POINTS; ++i) { + _controlPoints[i] = {}; + } +} + +bool SignalPlot::load(const JsonObject& config) { + _numControlPoints = 0; // Reset count + for (int i = 0; i < MAX_SIGNAL_POINTS; ++i) { // Clear old data + _controlPoints[i] = {}; + } + + // Assume 'controlPoints' exists and is a valid JsonArray + JsonArray pointsArray = config["controlPoints"].as(); + + _numControlPoints = min((uint8_t)pointsArray.size(), (uint8_t)MAX_SIGNAL_POINTS); + if (_numControlPoints < 1) { + _numControlPoints = 0; // Need at least 1 point to define any signal + return false; // Consider it a failure if no points + } + + // Assume points are valid and chronologically sorted by 'time'. + for (uint8_t i = 0; i < _numControlPoints; ++i) { + JsonObject pointObj = pointsArray[i].as(); + + // Assume keys exist and types are correct + _controlPoints[i].id = pointObj["id"].as(); + _controlPoints[i].time = pointObj["time"].as(); + // Cast the integer state from JSON to the SignalState enum + _controlPoints[i].state = static_cast(pointObj["state"].as()); + _controlPoints[i].user = pointObj["user"].as(); + } + + // Data is assumed sorted by time, no sorting needed here. + + return true; +} + +const S_SignalControlPoint* SignalPlot::findActivePoint(uint32_t elapsedMs) const { + const S_SignalControlPoint* lastApplicablePoint = nullptr; + // Iterate backwards assuming points are sorted by time ascending + // This is more efficient as we likely hit the correct segment sooner. + for (int i = _numControlPoints - 1; i >= 0; --i) { + if (_controlPoints[i].time <= elapsedMs) { + // Found the latest point at or before the elapsed time + lastApplicablePoint = &_controlPoints[i]; + break; // Since sorted, this is the correct one + } + } + return lastApplicablePoint; +} + +E_SignalState SignalPlot::getState(E_SignalState defaultState) const { + if (!_running || _numControlPoints == 0) { + return defaultState; + } + + uint32_t elapsedMs = getElapsedMs(); + const S_SignalControlPoint* activePoint = findActivePoint(elapsedMs); + + if (activePoint != nullptr) { + return activePoint->state; + } else { + // No point defined at or before the current time + return defaultState; + } +} + +int16_t SignalPlot::getUserValue(int16_t defaultValue) const { + if (!_running || _numControlPoints == 0) { + return defaultValue; + } + + uint32_t elapsedMs = getElapsedMs(); + const S_SignalControlPoint* activePoint = findActivePoint(elapsedMs); + + if (activePoint != nullptr) { + return activePoint->user; + } else { + // No point defined at or before the current time + return defaultValue; + } +} + +// --- PlotBase Overrides --- + +bool SignalPlot::getCurrentControlPointInfo(uint8_t& outId, uint32_t& outTimeMs, int16_t& outValue, int16_t& outUser) const { + if (!_running || _numControlPoints == 0) { + return false; + } + + uint32_t elapsedMs = getElapsedMs(); + const S_SignalControlPoint* activePoint = findActivePoint(elapsedMs); + + if (activePoint != nullptr) { + outId = activePoint->id; + outTimeMs = activePoint->time; + outValue = static_cast(activePoint->state); // Cast enum state to int16_t + outUser = activePoint->user; + return true; + } else { + // No point defined at or before the current time + return false; + } +} + +bool SignalPlot::seekToControlPoint(uint8_t pointId) { + // Find the control point with the specified ID + const S_SignalControlPoint* targetPoint = nullptr; + for (uint8_t i = 0; i < _numControlPoints; ++i) { + if (_controlPoints[i].id == pointId) { + targetPoint = &_controlPoints[i]; + break; // Assuming IDs are unique, we can stop once found + } + } + + if (targetPoint != nullptr) { + // Call the base class seek with the point's absolute time + seek(targetPoint->time); + return true; // Indicate success + } else { + // Point ID not found + // Serial.print(F("[SignalPlot] Error: Cannot seek to non-existent point ID: ")); // Optional logging + // Serial.println(pointId); + return false; // Indicate failure + } +} \ No newline at end of file diff --git a/src/profiles/SignalPlot.h b/src/profiles/SignalPlot.h new file mode 100644 index 00000000..7490fcb2 --- /dev/null +++ b/src/profiles/SignalPlot.h @@ -0,0 +1,95 @@ +#ifndef SIGNAL_PLOT_H +#define SIGNAL_PLOT_H + +#include "PlotBase.h" + +#define PROFILE_SCALE 100 + +#define MAX_SIGNAL_POINTS 20 + +enum class E_SignalState : int16_t { + STATE_OFF = 0, + STATE_ON = 1, + STATE_ERROR = -1, + STATE_CUSTOM_1 = 100 +}; + +struct S_SignalControlPoint { + uint8_t id; // Identifier for this specific control point instance (0-255) + uint32_t time; // Absolute time in milliseconds when this state becomes active + E_SignalState state; // Target state active from this time forward + int16_t user; // Custom user-defined integer associated with this point (e.g., target register value) +}; + +/** + * @brief Represents a single signal with discrete state changes plotted over time. + * Inherits from PlotBase. + * Assumes control points in the source JSON are sorted chronologically by 'time'. + */ +class SignalPlot : public PlotBase { +public: + SignalPlot(Component* owner); + virtual ~SignalPlot() = default; + + // --- Component Overrides (Optional) --- + // void setup() override; + // void loop() override; + + // --- Profile Specific Methods --- + + /** + * @brief Gets the active state for the signal at the current elapsed time. + * + * Finds the latest control point that occurred at or before + * the current time and returns its state. + * + * @param defaultState The state to return if the plot isn't running or no point has occurred yet. + * @return The determined state (SignalState). + */ + E_SignalState getState(E_SignalState defaultState = E_SignalState::STATE_OFF) const; + + /** + * @brief Gets the user-defined integer associated with the active state at the current time. + * + * Finds the latest control point that occurred at or before + * the current time and returns its associated user value. + * + * @param defaultValue The value to return if the plot isn't running or no point has occurred yet. + * @return The determined user value (int16_t). + */ + int16_t getUserValue(int16_t defaultValue = 0) const; + + /** + * @brief Seeks the plot time to the start time of a specific control point. + * + * @param pointId The ID of the SignalControlPoint to seek to. + * @return true if the point was found and seek was initiated, false otherwise. + */ + bool seekToControlPoint(uint8_t pointId); + + // --- PlotBase Overrides --- + bool getCurrentControlPointInfo(uint8_t& outId, uint32_t& outTimeMs, int16_t& outValue, int16_t& outUser) const override; + + /** + * @brief Loads the controlPoints array (discrete state changes) from the JSON config. + * Called by PlotBase::from. + * Assumes points are sorted chronologically by 'time' in the JSON. + * Expected JSON format within the config object: + * "controlPoints": [ + * { "id": , "time": , "state": , "user": }, + * ... + * ] + */ + bool load(const JsonObject& config) override; +protected: + +private: + S_SignalControlPoint _controlPoints[MAX_SIGNAL_POINTS]; + uint8_t _numControlPoints; + + // Helper to find the applicable control point + // Returns nullptr if no point is applicable yet + const S_SignalControlPoint* findActivePoint(uint32_t elapsedMs) const; +}; + +#endif // SIGNAL_PLOT_H \ No newline at end of file diff --git a/src/profiles/TemperatureProfile.cpp b/src/profiles/TemperatureProfile.cpp new file mode 100644 index 00000000..0f7afa18 --- /dev/null +++ b/src/profiles/TemperatureProfile.cpp @@ -0,0 +1,643 @@ +#include "TemperatureProfile.h" +#include +#include +#include +#include +#include "enums.h" +#include +#include "config-modbus.h" +#include + +#ifdef ENABLE_PROFILE_TEMPERATURE + +// start : <<900;2;64;start:0:0>> +// stop : <<900;2;64;stop:0:0>> +// pause : <<900;2;64;pause:0:0>> +// resume : <<900;2;64;resume:0:0>> +// seek : <<900;2;64;seek:0:0>> + +// The TemperatureProfileRegisterOffset enum and TEMP_PROFILE_REGISTER_COUNT +// are now expected to be included from TemperatureProfile.h + +// Base address for the first temperature profile instance's registers +#define MB_HREG_TEMP_PROFILE_BASE 200 + +// Updated constructor signature and initializer list +TemperatureProfile::TemperatureProfile(Component *owner, + short slot) : PlotBase(owner), + _numControlPoints(0), + slot(slot), + slaveId(1), + _seekTargetMs(0), + modbusTCP(nullptr), + _lastLoopExecutionMs(0), + _lastLogMs(0) +{ + name = "TemperatureProfile - Slot: " + String(slot); + for (int i = 0; i < MAX_TEMP_CONTROL_POINTS; ++i) + { + _controlPoints[i] = {}; + } + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + + // Calculate base address for this specific profile instance based on its slot + const uint16_t tcpBaseAddr = MB_HREG_TEMP_PROFILE_BASE + (slot * TEMP_PROFILE_REGISTER_COUNT); + + // Use the macro to initialize the Modbus blocks + _modbusBlocks[static_cast(TemperatureProfileRegisterOffset::STATUS)] = + INIT_MODBUS_BLOCK(TemperatureProfileRegisterOffset::STATUS, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "TProf Status", name.c_str()); + _modbusBlocks[static_cast(TemperatureProfileRegisterOffset::CURRENT_TEMP)] = + INIT_MODBUS_BLOCK(TemperatureProfileRegisterOffset::CURRENT_TEMP, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "TProf Curr Temp", name.c_str()); + _modbusBlocks[static_cast(TemperatureProfileRegisterOffset::DURATION_LW)] = + INIT_MODBUS_BLOCK(TemperatureProfileRegisterOffset::DURATION_LW, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "TProf Duration LW", name.c_str()); + _modbusBlocks[static_cast(TemperatureProfileRegisterOffset::DURATION_HW)] = + INIT_MODBUS_BLOCK(TemperatureProfileRegisterOffset::DURATION_HW, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "TProf Duration HW", name.c_str()); + _modbusBlocks[static_cast(TemperatureProfileRegisterOffset::ELAPSED_LW)] = + INIT_MODBUS_BLOCK(TemperatureProfileRegisterOffset::ELAPSED_LW, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "TProf Elapsed LW", name.c_str()); + _modbusBlocks[static_cast(TemperatureProfileRegisterOffset::ELAPSED_HW)] = + INIT_MODBUS_BLOCK(TemperatureProfileRegisterOffset::ELAPSED_HW, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "TProf Elapsed HW", name.c_str()); + _modbusBlocks[static_cast(TemperatureProfileRegisterOffset::REMAIN_LW)] = + INIT_MODBUS_BLOCK(TemperatureProfileRegisterOffset::REMAIN_LW, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "TProf Remain LW", name.c_str()); + _modbusBlocks[static_cast(TemperatureProfileRegisterOffset::REMAIN_HW)] = + INIT_MODBUS_BLOCK(TemperatureProfileRegisterOffset::REMAIN_HW, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "TProf Remain HW", name.c_str()); + _modbusBlocks[static_cast(TemperatureProfileRegisterOffset::COMMAND)] = + INIT_MODBUS_BLOCK(TemperatureProfileRegisterOffset::COMMAND, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_WRITE_ONLY, "TProf Command", name.c_str()); + _modbusBlocks[static_cast(TemperatureProfileRegisterOffset::SEEK_LW)] = + INIT_MODBUS_BLOCK(TemperatureProfileRegisterOffset::SEEK_LW, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_WRITE_ONLY, "TProf Seek LW", name.c_str()); + _modbusBlocks[static_cast(TemperatureProfileRegisterOffset::SEEK_HW)] = + INIT_MODBUS_BLOCK(TemperatureProfileRegisterOffset::SEEK_HW, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_WRITE_ONLY, "TProf Seek HW", name.c_str()); + + // Initialize the view to point to the member array, using the defined count + _modbusBlockView = {_modbusBlocks, TEMP_PROFILE_REGISTER_COUNT}; +} + +short TemperatureProfile::setup() +{ + sample(); + return E_OK; +} +short TemperatureProfile::loop() +{ + PlotBase::loop(); + uint32_t now = millis(); + if (now - _lastLoopExecutionMs < TEMPERATURE_PROFILE_SYNC_INTERVAL_MS) + { + return E_OK; + } + _lastLoopExecutionMs = now; + + // Check if PROFILE is currently in the RUNNING state, and proceed only if so + if (getCurrentStatus() == PlotStatus::RUNNING && modbusTCP != nullptr && !_targetRegisters.empty()) + { + uint32_t elapsed = getElapsedMs(); + int16_t currentTemp = getTemperature(elapsed); + if (currentTemp >= 0) + { + uint16_t tempValue = static_cast(currentTemp); + for (uint16_t currentTargetRegister : _targetRegisters) + { + ModbusMessage resp; + ModbusMessage req; + Error err = SUCCESS; + req.setMessage(1, FN_WRITE_HOLD_REGISTER, currentTargetRegister, tempValue); + resp = modbusTCP->modbusServer->localRequest(req); + if ((err = resp.getError()) != SUCCESS) + { + ModbusError me(err); + Log.errorln("%s: Error writing temp %u to slave %d, register %u: %02d - %s", + name.c_str(), tempValue, 1, currentTargetRegister, err, (const char *)me); + } + } + } + } + + return E_OK; +} +void TemperatureProfile::sample() +{ + _numControlPoints = 0; + for (int i = 0; i < MAX_TEMP_CONTROL_POINTS; ++i) + { + _controlPoints[i] = {}; + } + stop(); + + _durationMs = 1000 * 10 * 20; + // Point 0: Start at 20% (scaled) temp at time 0 + _controlPoints[0].x = 0; // 0% time + _controlPoints[0].y = 200; // 20% of PROFILE_SCALE + + // Point 1: Ramp up linearly to 80% temp by 30% time (18 seconds) + _controlPoints[1].x = 500; // 30% time (scaled) + _controlPoints[1].y = 800; // 80% temp (scaled) + + // Point 2: Hold at 80% temp until 70% time (42 seconds) + _controlPoints[2].x = 700; // 70% time (scaled) + _controlPoints[2].y = 400; // 80% temp (scaled) - same as previous + + // Point 3: Ramp down linearly to 30% temp by 90% time (54 seconds) + _controlPoints[3].x = 900; // 90% time (scaled) + _controlPoints[3].y = 300; // 30% temp (scaled) + + // Point 4: End at 25% temp at 100% time (60 seconds) + _controlPoints[4].x = 1000; // 100% time (Use macro) + _controlPoints[4].y = 200; // 25% temp (scaled) + _numControlPoints = 5; + + Log.traceln("%s: Sample profile generated with %d points, duration %lu ms.", name.c_str(), _numControlPoints, _durationMs); +} +bool TemperatureProfile::load(const JsonObject &config) +{ + if (config.containsKey("name")) { + name = config["name"].as(); + } + + // Load duration (in seconds) if present and convert to milliseconds + if (config.containsKey("duration")) { + uint32_t duration_s = config["duration"].as(); + _durationMs = duration_s * 1000; + Log.traceln("%s: Loaded duration %u s (%lu ms)", name.c_str(), duration_s, _durationMs); + } + else { + Log.warningln("%s: Duration not found in config, using default %lu ms", name.c_str(), _durationMs); + // Keep the default _durationMs if not specified + } + + _numControlPoints = 0; + for (int i = 0; i < MAX_TEMP_CONTROL_POINTS; ++i) + { + _controlPoints[i] = {}; + } + JsonArray pointsArray = config["controlPoints"].as(); + _numControlPoints = min((uint8_t)pointsArray.size(), (uint8_t)MAX_TEMP_CONTROL_POINTS); + if (_numControlPoints < 2) + { + _numControlPoints = 0; // Need at least 2 points for interpolation + return false; // Consider it a failure if not enough points + } + for (uint8_t i = 0; i < _numControlPoints; ++i) + { + JsonObject pointObj = pointsArray[i].as(); + int16_t x_scaled = pointObj["x"].as(); + int16_t y_scaled = pointObj["y"].as(); + int typeInt = pointObj["type"].as(); + _controlPoints[i].x = x_scaled; + _controlPoints[i].y = y_scaled; + } + + // --- Load Target Registers (Affinity) --- + _targetRegisters.clear(); // Clear any previous targets + if (config.containsKey("targetRegisters")) { + JsonArray targetArray = config["targetRegisters"].as(); + uint8_t numTargets = min((uint8_t)targetArray.size(), (uint8_t)MAX_PROFILE_TARGETS); + + _targetRegisters.reserve(numTargets); // Re-add reserve for std::vector + + for(uint8_t i = 0; i < numTargets; ++i) { + if (targetArray[i].is()) { + _targetRegisters.push_back(targetArray[i].as()); + } + } + Log.traceln("%s: Loaded %d target registers.", name.c_str(), _targetRegisters.size()); + + if (targetArray.size() > MAX_PROFILE_TARGETS) { + Log.warningln("%s: Config specified %d targets, but limited to %d.", name.c_str(), targetArray.size(), MAX_PROFILE_TARGETS); + } + } else { + Log.warningln("%s: 'targetRegisters' array not found in config.", name.c_str()); + } + // --- End Load Target Registers --- + + if (config.containsKey("max")) { + max = config["max"].as(); + } + if (config.containsKey("count")) { + count = config["count"].as(); + } + + return true; +} + +int16_t TemperatureProfile::getTemperature(uint32_t elapsedMs) const +{ + if (!_running || _numControlPoints < 2) + { + return 0; + } + + uint32_t lastPointTimeMs = static_cast(((uint64_t)_controlPoints[_numControlPoints - 1].x * (uint64_t)_durationMs) / PROFILE_SCALE); + if (elapsedMs >= lastPointTimeMs) + { + // Use last point's normalized value, but scale it before returning + int16_t normalizedTemp = _controlPoints[_numControlPoints - 1].y; + int32_t scaledTemp = ((int32_t)normalizedTemp * (int32_t)max) / PROFILE_SCALE; + if (scaledTemp > INT16_MAX) scaledTemp = INT16_MAX; + if (scaledTemp < INT16_MIN) scaledTemp = INT16_MIN; + return static_cast(scaledTemp); + } + + // Now, the segment is between controlPoints[segmentIndex - 1] and controlPoints[segmentIndex]. + uint8_t segmentIndex = 1; // Index of the *end* point of the segment + while (segmentIndex < _numControlPoints) + { + // Calculate the time for the current segment end point + uint32_t pointTimeMs = static_cast(((uint64_t)_controlPoints[segmentIndex].x * (uint64_t)_durationMs) / PROFILE_SCALE); + if (elapsedMs < pointTimeMs) + { + // Found the segment: elapsedMs is before this point's time + break; + } + segmentIndex++; + } + + // Now, the segment is between controlPoints[segmentIndex - 1] and controlPoints[segmentIndex]. + const TempControlPoint &p0 = _controlPoints[segmentIndex - 1]; + const TempControlPoint &p1 = _controlPoints[segmentIndex]; + + // Calculate segment times based on x values + uint32_t segmentStartTime = static_cast(((uint64_t)p0.x * (uint64_t)_durationMs) / PROFILE_SCALE); + uint32_t segmentEndTime = static_cast(((uint64_t)p1.x * (uint64_t)_durationMs) / PROFILE_SCALE); + uint32_t segmentDuration = segmentEndTime - segmentStartTime; + + // Handle coincident points (based on calculated time) + if (segmentDuration == 0) + { + // Use value of the point at the start, but scale it before returning + int16_t normalizedTemp = p0.y; + int32_t scaledTemp = ((int32_t)normalizedTemp * (int32_t)max) / PROFILE_SCALE; + if (scaledTemp > INT16_MAX) scaledTemp = INT16_MAX; + if (scaledTemp < INT16_MIN) scaledTemp = INT16_MIN; + return static_cast(scaledTemp); + } + // Calculate progress within the segment (0-PROFILE_SCALE) + uint32_t timeInSegment = elapsedMs - segmentStartTime; + uint16_t t_norm = static_cast(((uint64_t)timeInSegment * (uint64_t)PROFILE_SCALE) / segmentDuration); + int16_t normalizedTemp = lerp(p0.y, p1.y, t_norm); + + // Scale the normalized temperature (0-PROFILE_SCALE) to the actual range (0-max) + // Use int32_t for intermediate calculation to avoid potential overflow + int32_t scaledTemp = ((int32_t)normalizedTemp * (int32_t)max) / PROFILE_SCALE; + + // Clamp to ensure it fits within int16_t bounds + if (scaledTemp > INT16_MAX) scaledTemp = INT16_MAX; + if (scaledTemp < INT16_MIN) scaledTemp = INT16_MIN; + + return static_cast(scaledTemp); +} +int16_t TemperatureProfile::lerp(int16_t y0, int16_t y1, uint16_t t) const +{ + // t is scaled 0-PROFILE_SCALE + int32_t deltaY = (int32_t)y1 - (int32_t)y0; + int32_t interpolated = (int32_t)y0 + ((int64_t)deltaY * t) / PROFILE_SCALE; + return static_cast(interpolated); +} +int16_t TemperatureProfile::cubicBezier(int16_t y0, int16_t y1, int16_t y2, int16_t y3, uint16_t t_norm) const +{ + // t_norm is scaled 0-PROFILE_SCALE + float t = (float)t_norm / (float)PROFILE_SCALE; + if (t < 0.0f) + t = 0.0f; // Ensure t is in [0, 1] + if (t > 1.0f) + t = 1.0f; + float u = 1.0f - t; + + // Using float calculations for Bezier curve + float u3 = u * u * u; + float u2t = 3.0f * u * u * t; + float ut2 = 3.0f * u * t * t; + float t3 = t * t * t; + + float result = (u3 * y0) + (u2t * y1) + (ut2 * y2) + (t3 * y3); + + // Clamp result to the PROFILE_SCALE range before rounding + if (result < 0.0f) + result = 0.0f; + if (result > (float)PROFILE_SCALE) + result = (float)PROFILE_SCALE; + + return static_cast(round(result)); +} +/** + * @brief Integer-only Cubic Bezier interpolation using int64_t for intermediate calculations. + * + * @param y0 Start value (P0y). + * @param y1 Control point 1 value (P1y). + * @param y2 Control point 2 value (P2y). + * @param y3 End value (P3y). + * @param t_norm Interpolation factor (scaled 0-PROFILE_SCALE). + * @return Interpolated value (int16_t). + * + * @note Potential Issues: + * - Requires int64_t support. + * - Performance relative to float version depends heavily on platform/compiler optimization. + * - Division by PROFILE_SCALE can introduce small truncation errors at each step, potentially accumulating. + * - The intermediate scale factor can become very large (PROFILE_SCALE^3), requiring careful handling. + */ +int16_t TemperatureProfile::cubicBezierInt(int16_t y0, int16_t y1, int16_t y2, int16_t y3, uint16_t t_norm) const +{ + // Ensure t_norm is within bounds (though uint16_t >= 0) + if (t_norm > PROFILE_SCALE) + t_norm = PROFILE_SCALE; + + int64_t t = t_norm; + int64_t u = PROFILE_SCALE - t; + + // Calculate terms using int64_t to avoid overflow. + // We apply scaling progressively to try and manage the magnitude. + // Result needs to be eventually divided by PROFILE_SCALE^3. + + // Term 0: u^3 * y0 + int64_t term0 = u; // u + term0 = (term0 * u) / PROFILE_SCALE; // u^2 / S + term0 = (term0 * u) / PROFILE_SCALE; // u^3 / S^2 + term0 = (term0 * y0); + + // Term 1: 3 * u^2 * t * y1 + int64_t term1 = 3 * u; // 3u + term1 = (term1 * u) / PROFILE_SCALE; // 3u^2 / S + term1 = (term1 * t) / PROFILE_SCALE; // 3u^2*t / S^2 + term1 = (term1 * y1); + + // Term 2: 3 * u * t^2 * y2 + int64_t term2 = 3 * u; // 3u + term2 = (term2 * t) / PROFILE_SCALE; // 3ut / S + term2 = (term2 * t) / PROFILE_SCALE; // 3ut^2 / S^2 + term2 = (term2 * y2); + + // Term 3: t^3 * y3 + int64_t term3 = t; // t + term3 = (term3 * t) / PROFILE_SCALE; // t^2 / S + term3 = (term3 * t) / PROFILE_SCALE; // t^3 / S^2 + term3 = (term3 * y3); + + // Combine terms (already scaled by S^2 implicitly through divisions) + int64_t result_scaled_by_S2 = term0 + term1 + term2 + term3; + + // Final division to get the result back to original scale + // Add PROFILE_SCALE / 2 for rounding before integer division + int16_t final_result = static_cast((result_scaled_by_S2 + (PROFILE_SCALE / 2)) / PROFILE_SCALE); + + // Clamp final result (although intermediate math should prevent exceeding bounds if inputs are valid) + if (final_result < 0) + final_result = 0; + if (final_result > PROFILE_SCALE) + final_result = PROFILE_SCALE; + + return final_result; +} + +// --- New Getters --- + +/** + * @brief Gets a pointer to the internal array of control points. + */ +const TempControlPoint* TemperatureProfile::getTempControlPoints() const +{ + return _controlPoints; +} + +/** + * @brief Gets the number of currently defined control points. + */ +uint8_t TemperatureProfile::getNumTempControlPoints() const +{ + return _numControlPoints; +} + +// --- End New Getters --- + +short TemperatureProfile::status() +{ + uint32_t duration = getDuration(); + uint32_t remaining = getRemainingTime(); + PlotStatus status = getCurrentStatus(); + int16_t temp = getTemperature(getElapsedMs()); + + Log.noticeln(" Status: %d (%s)", (int)status, + status == PlotStatus::IDLE ? "IDLE" : status == PlotStatus::RUNNING ? "RUNNING" + : status == PlotStatus::PAUSED ? "PAUSED" + : "FINISHED"); + Log.noticeln(" Duration: %lu ms", duration); + Log.noticeln(" Elapsed: %lu ms", duration - remaining); + Log.noticeln(" Remaining: %lu ms", remaining); + Log.noticeln(" Current Temp (scaled): %d", temp); + return E_OK; +} +short TemperatureProfile::serial_register(Bridge *bridge) +{ + bridge->registerMemberFunction(id, this, C_STR("status"), (ComponentFnPtr)&TemperatureProfile::status); + bridge->registerMemberFunction(id, this, C_STR("start"), (ComponentFnPtr)&TemperatureProfile::start); + bridge->registerMemberFunction(id, this, C_STR("pause"), (ComponentFnPtr)&TemperatureProfile::pause); + bridge->registerMemberFunction(id, this, C_STR("stop"), (ComponentFnPtr)&TemperatureProfile::stop); + bridge->registerMemberFunction(id, this, C_STR("resume"), (ComponentFnPtr)&TemperatureProfile::resume); + return E_OK; +} +ModbusBlockView *TemperatureProfile::mb_tcp_blocks() const +{ + // Return a pointer to the member variable initialized in the constructor + // Need to cast away const because the return type is non-const pointer, + // although the underlying data might be treated as const by the caller. + // Alternatively, change return type to const ModbusBlockView* + return const_cast(&_modbusBlockView); +} +void TemperatureProfile::mb_tcp_register(ModbusTCP *manager) const +{ + if (!hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS)) + return; + + // Store the manager pointer for later use (e.g., in loop) + // Need to cast away const-ness of 'this' to modify a member + const_cast(this)->modbusTCP = manager; + Log.infoln("%s: Registering Modbus blocks...", name.c_str()); + ModbusBlockView *blocksView = mb_tcp_blocks(); + Component *thiz = const_cast(this); + for (int i = 0; i < blocksView->count; ++i) + { + MB_Registers info = blocksView->data[i]; + info.componentId = this->id; + manager->registerModbus(thiz, info); + } +} +short TemperatureProfile::mb_tcp_read(MB_Registers *reg) +{ + uint32_t val32 = 0; + short requestedAddress = reg->startAddress; + // Calculate the base address for THIS specific instance based on its slot + const uint16_t instanceBaseAddr = MB_HREG_TEMP_PROFILE_BASE + (slot * TEMP_PROFILE_REGISTER_COUNT); + const uint16_t instanceEndAddr = instanceBaseAddr + TEMP_PROFILE_REGISTER_COUNT; + + // Calculate the relative offset within this instance's block + short offset = requestedAddress - instanceBaseAddr; + + // Check if the requested address even falls within this instance's range + if (requestedAddress < instanceBaseAddr || requestedAddress >= instanceEndAddr) + { + // This shouldn't happen if registration is correct, but good to check. + Log.warningln("%s: Received read request for address %d which is outside my block [%d - %d).", + name.c_str(), requestedAddress, instanceBaseAddr, instanceEndAddr); + return 0xFFFF; // Indicate error (or potentially MODBUS_ERROR_ILLEGAL_DATA_ADDRESS if we change return type) + } + + // Log.verboseln("%s: Received read request for address %d (offset %d). | StartAddr: %d, EndAddr: %d | Mapping: %d", name.c_str(), requestedAddress, offset, instanceBaseAddr, instanceEndAddr, reg->startAddress); + // Now switch based on the relative offset within this instance's block + TemperatureProfileRegisterOffset regOffset = static_cast(offset); + switch (regOffset) // Switch on the enum representation of the offset + { + case TemperatureProfileRegisterOffset::STATUS: + return (short)getCurrentStatus(); + case TemperatureProfileRegisterOffset::CURRENT_TEMP: + return getTemperature(getElapsedMs()); + case TemperatureProfileRegisterOffset::DURATION_LW: + val32 = getDuration(); + return (uint16_t)LOW_WORD(val32); + case TemperatureProfileRegisterOffset::DURATION_HW: + val32 = getDuration(); + return (uint16_t)HIGH_WORD(val32); + case TemperatureProfileRegisterOffset::ELAPSED_LW: + val32 = getElapsedMs(); + return (uint16_t)LOW_WORD(val32); + case TemperatureProfileRegisterOffset::ELAPSED_HW: + val32 = getElapsedMs(); + return (uint16_t)HIGH_WORD(val32); + case TemperatureProfileRegisterOffset::REMAIN_LW: + val32 = getRemainingTime(); + return (uint16_t)LOW_WORD(val32); + case TemperatureProfileRegisterOffset::REMAIN_HW: + //Log.warningln("%s: Write attempt on read-only register at address %d (offset %d).", name.c_str(), requestedAddress, offset); + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; // Access violation + case TemperatureProfileRegisterOffset::COMMAND: + case TemperatureProfileRegisterOffset::SEEK_LW: + case TemperatureProfileRegisterOffset::SEEK_HW: + // Log.warningln("%s: Read attempt on write-only register at address %d (offset %d).", name.c_str(), requestedAddress, offset); + return 0xFFFF; // Indicate error + default: + // This case should technically not be reached if the initial range check is done, + // because 'offset' must be between 0 and TEMP_PROFILE_REGISTER_COUNT - 1. + // But as a safeguard: + Log.errorln("%s: Reached default case in mb_tcp_read with offset %d for address %d. This indicates a logic error.", name.c_str(), offset, requestedAddress); + return 0xFFFF; // Indicate internal error + } +} +short TemperatureProfile::mb_tcp_write(MB_Registers *reg, short value) +{ + short requestedAddress = reg->startAddress; + + // Calculate the base address and offset for this instance + const uint16_t instanceBaseAddr = MB_HREG_TEMP_PROFILE_BASE + (slot * TEMP_PROFILE_REGISTER_COUNT); + const uint16_t instanceEndAddr = instanceBaseAddr + TEMP_PROFILE_REGISTER_COUNT; + short offset = requestedAddress - instanceBaseAddr; + + // Check if the requested address falls within this instance's block + if (requestedAddress < instanceBaseAddr || requestedAddress >= instanceEndAddr) + { + Log.warningln("%s: Received write request for address %d which is outside my block [%d - %d).", + name.c_str(), requestedAddress, instanceBaseAddr, instanceEndAddr); + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; // Address out of range for this instance + } + + // Convert offset to enum for clarity in switch + TemperatureProfileRegisterOffset regOffset = static_cast(offset); + + // Log the attempt (optional, can be verbose) + // Log.verboseln("%s: Write attempt addr=%d, offset=%d, value=%d", name.c_str(), requestedAddress, offset, value); + + switch (regOffset) + { + case TemperatureProfileRegisterOffset::COMMAND: // Offset 8 + Log.infoln("%s: Received command via Modbus (Offset %d): %d", name.c_str(), offset, value); + switch (value) + { + case 1: + start(); + break; + case 2: + stop(); + break; + case 3: + pause(); + break; + case 4: + resume(); + break; + default: + Log.warningln("%s: Invalid command value %d received.", name.c_str(), value); + return MODBUS_ERROR_ILLEGAL_DATA_VALUE; + } + return E_OK; + + case TemperatureProfileRegisterOffset::SEEK_LW: // Offset 9 + // Store the low word in the instance member variable + _seekTargetMs = (_seekTargetMs & 0xFFFF0000) | (value & 0xFFFF); // Mask value just in case + Log.verboseln("%s: Received seek LW (Offset %d): %u. Current target: %lu", name.c_str(), offset, (value & 0xFFFF), _seekTargetMs); + return E_OK; + + case TemperatureProfileRegisterOffset::SEEK_HW: // Offset 10 + // Store the high word and perform the seek + _seekTargetMs = (_seekTargetMs & 0x0000FFFF) | ((uint32_t)value << 16); + Log.infoln("%s: Received seek HW (Offset %d): %u. Seeking to: %lu ms", name.c_str(), offset, value, _seekTargetMs); + seek(_seekTargetMs); // Call base class seek method + _seekTargetMs = 0; // Reset after use for safety + return E_OK; + default: + // This case should not be reached if the initial range check works + // and the enum covers all valid offsets. + Log.errorln("%s: Reached default case in mb_tcp_write with offset %d for address %d. Logic error.", name.c_str(), offset, requestedAddress); + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; + } +} +bool TemperatureProfile::getCurrentControlPointInfo(uint8_t &outId, uint32_t &outTimeMs, int16_t &outValue, int16_t &outUser) const +{ + if (!_running || _numControlPoints < 2) + { // Need >= 2 points for a segment + return false; + } + + uint32_t elapsedMs = getElapsedMs(); + + // Find the segment + uint8_t segmentIndex = 1; // Index of the *end* point of the segment + while (segmentIndex < _numControlPoints) + { + // Calculate the time for the current segment end point + uint32_t pointTimeMs = static_cast(((uint64_t)_controlPoints[segmentIndex].x * (uint64_t)_durationMs) / PROFILE_SCALE); + if (elapsedMs < pointTimeMs) + { + // Found the segment: elapsedMs is before this point's time + break; + } + segmentIndex++; + } + + // If elapsedMs is beyond or exactly at the last point's time + // Check against the calculated time of the last point + uint32_t lastPointTimeMs = static_cast(((uint64_t)_controlPoints[_numControlPoints - 1].x * (uint64_t)_durationMs) / PROFILE_SCALE); + if (segmentIndex >= _numControlPoints || elapsedMs >= lastPointTimeMs) + { + return _controlPoints[_numControlPoints - 1].y; // Return last point's value + } + + // Now, the segment is between controlPoints[segmentIndex - 1] and controlPoints[segmentIndex]. + const TempControlPoint &p0 = _controlPoints[segmentIndex - 1]; + const TempControlPoint &p1 = _controlPoints[segmentIndex]; + + // Calculate segment times based on x values + uint32_t segmentStartTime = static_cast(((uint64_t)p0.x * (uint64_t)_durationMs) / PROFILE_SCALE); + uint32_t segmentEndTime = static_cast(((uint64_t)p1.x * (uint64_t)_durationMs) / PROFILE_SCALE); + uint32_t segmentDuration = segmentEndTime - segmentStartTime; + + // Handle coincident points (based on calculated time) + if (segmentDuration == 0) + { + // Use value of the point at the start, but scale it before returning + int16_t normalizedTemp = p0.y; + int32_t scaledTemp = ((int32_t)normalizedTemp * (int32_t)max) / PROFILE_SCALE; + if (scaledTemp > INT16_MAX) scaledTemp = INT16_MAX; + if (scaledTemp < INT16_MIN) scaledTemp = INT16_MIN; + return static_cast(scaledTemp); + } + // Calculate progress within the segment (0-PROFILE_SCALE) + uint32_t timeInSegment = elapsedMs - segmentStartTime; + uint16_t t_norm = static_cast(((uint64_t)timeInSegment * (uint64_t)PROFILE_SCALE) / segmentDuration); + return lerp(p0.y, p1.y, t_norm); +} +#endif // ENABLE_PROFILE_TEMPERATURE diff --git a/src/profiles/TemperatureProfile.h b/src/profiles/TemperatureProfile.h new file mode 100644 index 00000000..20f16f84 --- /dev/null +++ b/src/profiles/TemperatureProfile.h @@ -0,0 +1,173 @@ +#ifndef TEMPERATURE_PROFILE_H +#define TEMPERATURE_PROFILE_H + +#include "PlotBase.h" +#include // Include for ModbusManager type +#include "enums.h" // Include for error codes (like E_OK) +#include // Include for LOW_WORD/HIGH_WORD (if defined there) +#include +#include +#include +#include + +class ModbusTCP; + +// Define the maximum number of individual Modbus registers a profile can target +#define MAX_PROFILE_TARGETS 10 + +enum class TemperatureProfileRegisterOffset : uint16_t { + STATUS = 0, + CURRENT_TEMP = 1, + DURATION_LW = 2, + DURATION_HW = 3, + ELAPSED_LW = 4, + ELAPSED_HW = 5, + REMAIN_LW = 6, + REMAIN_HW = 7, + COMMAND = 8, + SEEK_LW = 9, + SEEK_HW = 10, + SLAVE_ID = 11, + _COUNT +}; + +// Calculate the number of registers per profile instance based on the enum +const uint16_t TEMP_PROFILE_REGISTER_COUNT = static_cast(TemperatureProfileRegisterOffset::_COUNT); + +// Define the scale used for internal representation of temperature/time values +#define PROFILE_SCALE 10000 + +// Define max size for control point data array +#define MAX_TEMP_CONTROL_POINTS 10 // Adjust as needed + +// Alternatively, define LOW_WORD/HIGH_WORD if not in macros.h +#ifndef LOW_WORD +#define LOW_WORD(lw) ((uint16_t)(((uint32_t)(lw)) & 0xFFFF)) +#endif +#ifndef HIGH_WORD +#define HIGH_WORD(hw) ((uint16_t)((((uint32_t)(hw)) >> 16) & 0xFFFF)) +#endif + +enum class TempProfileControlType : uint8_t { + LINEAR = 0, + CUBIC = 1 +}; + +struct TempControlPoint { + int16_t x; // Time proportion (scaled 0-PROFILE_SCALE) + int16_t y; // Temperature value (scaled 0-PROFILE_SCALE) +}; + +/** + * @brief Represents a temperature profile using interpolated segments. + * Inherits from PlotBase. + */ +class TemperatureProfile : public PlotBase { +public: + TemperatureProfile(Component* owner, short slot); + virtual ~TemperatureProfile() = default; + + short setup() override; + short loop() override; + + // --- Profile Specific Methods --- + /** + * @brief Gets the interpolated temperature value for the current time. + * @param elapsedMs The elapsed time in milliseconds. + * @return The interpolated temperature (scaled 0-PROFILE_SCALE) or 0 if not running/invalid. + */ + int16_t getTemperature(uint32_t elapsedMs) const; + + /** + * @brief Gets a pointer to the internal array of control points. + * @return Const pointer to the TempControlPoint array. + */ + const TempControlPoint* getTempControlPoints() const; + + /** + * @brief Gets the number of currently defined control points. + * @return Number of control points. + */ + uint8_t getNumTempControlPoints() const; + + // --- TemperatureProfile Max Temperature --- + ushort max; + // --- Number of associated PIDs + ushort count; + ushort slaveId; + /** + * @brief Populates the profile with sample data for testing/defaults. + * Overwrites any existing control points. + */ + void sample(); + + /** + * @brief Sets the control points for the temperature profile. + * + * @param points An array of TempControlPoint structures. + * @param numPoints The number of points in the array. + * @param durationMs The total duration of the profile in milliseconds. + * @return True if the points were set successfully, false otherwise (e.g., invalid number of points). + */ + bool setControlPoints(const TempControlPoint points[], uint8_t numPoints, uint32_t durationMs); + + // --- PlotBase / Component Overrides --- + bool getCurrentControlPointInfo(uint8_t& outId, uint32_t& outTimeMs, int16_t& outValue, int16_t& outUser) const override; + void mb_tcp_register(ModbusTCP* manager) const override; + ModbusBlockView* mb_tcp_blocks() const override; + short mb_tcp_read(MB_Registers *reg) override; + short mb_tcp_write(MB_Registers *reg, short value) override; + short serial_register(Bridge *bridge) override; + + /** + * @brief Loads temperature profile specific data (controlPoints) from JSON. + * Called by PlotBase::loadFromJsonObject. + */ + bool load(const JsonObject& config) override; + + // --- Target Registers --- + const std::vector& getTargetRegisters() const { return _targetRegisters; } + uint8_t getTargetRegisterCount() const { return _targetRegisters.size(); } + uint16_t getTargetRegister(uint8_t index) const { return _targetRegisters[index]; } + +protected: + + short status(); + + // --- TemperatureProfile Slot --- + ushort slot; + + + +private: + // Storage for the target register vector - <<< Remove storage array + // uint16_t _targetRegistersStorage[MAX_PROFILE_TARGETS]; + // Vector to hold the specific target Modbus register addresses + std::vector _targetRegisters; // <<< Use std::vector + + + // Modbus block definitions (instance-specific) + MB_Registers _modbusBlocks[TEMP_PROFILE_REGISTER_COUNT]; + ModbusBlockView _modbusBlockView; + + TempControlPoint _controlPoints[MAX_TEMP_CONTROL_POINTS]; + uint8_t _numControlPoints; + + // Temporary storage for multi-register writes (e.g., seek) + uint32_t _seekTargetMs; + + // Pointer to the Modbus manager (set during registration) + ModbusTCP* modbusTCP; + + // Timestamp of the last loop execution + uint32_t _lastLoopExecutionMs; + uint32_t _lastLogMs; // Timestamp for logging + + // Helper methods for interpolation + int16_t lerp(int16_t y0, int16_t y1, uint16_t t) const; + int16_t cubicBezier(int16_t y0, int16_t y1, int16_t y2, int16_t y3, uint16_t t_norm) const; + int16_t cubicBezierInt(int16_t y0, int16_t y1, int16_t y2, int16_t y3, uint16_t t_norm) const; + void _initializeControlPoints(); +}; + +#endif // TEMPERATURE_PROFILE_H \ No newline at end of file diff --git a/src/profiles/WiFiNetworkSettings.h b/src/profiles/WiFiNetworkSettings.h new file mode 100644 index 00000000..c4e80102 --- /dev/null +++ b/src/profiles/WiFiNetworkSettings.h @@ -0,0 +1,245 @@ +#ifndef WIFI_NETWORK_SETTINGS_H +#define WIFI_NETWORK_SETTINGS_H + +#include // Should be first for core Arduino types +#include // For IPAddress +#include // Explicitly include for String, though Arduino.h often covers it +#include // For JSON functionality +#include // For logging macros + +#include "config.h" // For WIFI_SSID, WIFI_PASSWORD, AP_SSID, AP_PASSWORD, ENABLE_AP_STA +#include "enums.h" // For E_OK +#include "Logger.h" // For Log object (if Log is a custom object wrapper) + +// Struct to hold all WiFi Network Settings +struct WiFiNetworkSettings { + // STA Configuration + String sta_ssid; + String sta_password; + IPAddress sta_local_IP; + IPAddress sta_gateway; + IPAddress sta_subnet; + IPAddress sta_primary_dns; + IPAddress sta_secondary_dns; + + // AP Configuration (for AP_STA mode) + String ap_ssid; + String ap_password; + IPAddress ap_config_ip; + IPAddress ap_config_gateway; + IPAddress ap_config_subnet; + + WiFiNetworkSettings() { + // Initialize STA settings with defaults from config.h values + sta_ssid = WIFI_SSID; + sta_password = WIFI_PASSWORD; + sta_local_IP = IPAddress(192, 168, 1, 250); // Direct initialization + sta_gateway = IPAddress(192, 168, 1, 1); // Direct initialization + sta_subnet = IPAddress(255, 255, 0, 0); // Direct initialization + sta_primary_dns = IPAddress(8, 8, 8, 8); // Direct initialization + sta_secondary_dns = IPAddress(8, 8, 4, 4); // Direct initialization + +#ifdef ENABLE_AP_STA + ap_ssid = AP_SSID; + ap_password = AP_PASSWORD; + ap_config_ip = IPAddress(192, 168, 4, 1); // Direct initialization + ap_config_gateway = IPAddress(192, 168, 4, 1); // Direct initialization + ap_config_subnet = IPAddress(255, 255, 255, 240); // Direct initialization (using the last attempted /28 value) +#else + ap_ssid = ""; + ap_password = ""; + // IPAddress members will be default constructed (0.0.0.0) +#endif + } + + void print() const { + Log.infoln("--- WiFiNetworkSettings Dump ---"); + Log.infoln("STA SSID: %s", sta_ssid.c_str()); + // Note: STA password is not logged for security. + Log.infoln("STA IP: %s", sta_local_IP.toString().c_str()); + Log.infoln("STA Gateway: %s", sta_gateway.toString().c_str()); + Log.infoln("STA Subnet: %s", sta_subnet.toString().c_str()); + Log.infoln("STA DNS1: %s", sta_primary_dns.toString().c_str()); + Log.infoln("STA DNS2: %s", sta_secondary_dns.toString().c_str()); + +#if defined(ENABLE_AP_STA) + Log.infoln("AP SSID: %s", ap_ssid.c_str()); + // Note: AP password is not logged for security. + Log.infoln("AP IP: %s", ap_config_ip.toString().c_str()); + Log.infoln("AP Gateway: %s", ap_config_gateway.toString().c_str()); + Log.infoln("AP Subnet: %s", ap_config_subnet.toString().c_str()); +#else + Log.infoln("AP_STA mode not enabled, AP settings not actively used by setupNetwork."); +#endif + Log.infoln("--- End WiFiNetworkSettings Dump ---"); + } + + short loadSettings(JsonObject& doc) { + Log.infoln("WiFiNetworkSettings::load - Loading WiFi settings from JSON..."); + IPAddress tempIp; + + // STA Settings + JsonVariant sta_ssid_val = doc["sta_ssid"]; + if (sta_ssid_val.is()) { + sta_ssid = sta_ssid_val.as(); + Log.infoln("Loaded sta_ssid: %s", sta_ssid.c_str()); + } else if (!sta_ssid_val.isNull()) { + Log.warningln("Value for 'sta_ssid' is not a string."); + } + + JsonVariant sta_password_val = doc["sta_password"]; + if (sta_password_val.is()) { + sta_password = sta_password_val.as(); + // Avoid logging password: Log.infoln("Loaded sta_password"); + } else if (!sta_password_val.isNull()) { + Log.warningln("Value for 'sta_password' is not a string."); + } + + JsonVariant sta_local_ip_val = doc["sta_local_ip"]; + if (sta_local_ip_val.is()) { + if (tempIp.fromString(sta_local_ip_val.as())) { + sta_local_IP = tempIp; + Log.infoln("Loaded sta_local_IP: %s", sta_local_IP.toString().c_str()); + } else { + Log.warningln("Failed to parse IP from string for 'sta_local_ip': %s", sta_local_ip_val.as()); + } + } else if (!sta_local_ip_val.isNull()) { + Log.warningln("Value for 'sta_local_ip' is not a string, cannot parse as IP."); + } + + JsonVariant sta_gateway_val = doc["sta_gateway"]; + if (sta_gateway_val.is()) { + if (tempIp.fromString(sta_gateway_val.as())) { + sta_gateway = tempIp; + Log.infoln("Loaded sta_gateway: %s", sta_gateway.toString().c_str()); + } else { + Log.warningln("Failed to parse IP from string for 'sta_gateway': %s", sta_gateway_val.as()); + } + } else if (!sta_gateway_val.isNull()) { + Log.warningln("Value for 'sta_gateway' is not a string, cannot parse as IP."); + } + + JsonVariant sta_subnet_val = doc["sta_subnet"]; + if (sta_subnet_val.is()) { + if (tempIp.fromString(sta_subnet_val.as())) { + sta_subnet = tempIp; + Log.infoln("Loaded sta_subnet: %s", sta_subnet.toString().c_str()); + } else { + Log.warningln("Failed to parse IP from string for 'sta_subnet': %s", sta_subnet_val.as()); + } + } else if (!sta_subnet_val.isNull()) { + Log.warningln("Value for 'sta_subnet' is not a string, cannot parse as IP."); + } + + JsonVariant sta_primary_dns_val = doc["sta_primary_dns"]; + if (sta_primary_dns_val.is()) { + if (tempIp.fromString(sta_primary_dns_val.as())) { + sta_primary_dns = tempIp; + Log.infoln("Loaded sta_primary_dns: %s", sta_primary_dns.toString().c_str()); + } else { + Log.warningln("Failed to parse IP from string for 'sta_primary_dns': %s", sta_primary_dns_val.as()); + } + } else if (!sta_primary_dns_val.isNull()) { + Log.warningln("Value for 'sta_primary_dns' is not a string, cannot parse as IP."); + } + + JsonVariant sta_secondary_dns_val = doc["sta_secondary_dns"]; + if (sta_secondary_dns_val.is()) { + if (tempIp.fromString(sta_secondary_dns_val.as())) { + sta_secondary_dns = tempIp; + Log.infoln("Loaded sta_secondary_dns: %s", sta_secondary_dns.toString().c_str()); + } else { + Log.warningln("Failed to parse IP from string for 'sta_secondary_dns': %s", sta_secondary_dns_val.as()); + } + } else if (!sta_secondary_dns_val.isNull()) { + Log.warningln("Value for 'sta_secondary_dns' is not a string, cannot parse as IP."); + } + +#ifdef ENABLE_AP_STA + // AP Settings + JsonVariant ap_ssid_val = doc["ap_ssid"]; + if (ap_ssid_val.is()) { + ap_ssid = ap_ssid_val.as(); + Log.infoln("Loaded ap_ssid: %s", ap_ssid.c_str()); + } else if (!ap_ssid_val.isNull()) { + Log.warningln("Value for 'ap_ssid' is not a string."); + } + + JsonVariant ap_password_val = doc["ap_password"]; + if (ap_password_val.is()) { + ap_password = ap_password_val.as(); + // Avoid logging password: Log.infoln("Loaded ap_password"); + } else if (!ap_password_val.isNull()) { + Log.warningln("Value for 'ap_password' is not a string."); + } + + JsonVariant ap_config_ip_val = doc["ap_config_ip"]; + if (ap_config_ip_val.is()) { + if (tempIp.fromString(ap_config_ip_val.as())) { + ap_config_ip = tempIp; + Log.infoln("Loaded ap_config_ip: %s", ap_config_ip.toString().c_str()); + } else { + Log.warningln("Failed to parse IP from string for 'ap_config_ip': %s", ap_config_ip_val.as()); + } + } else if (!ap_config_ip_val.isNull()) { + Log.warningln("Value for 'ap_config_ip' is not a string, cannot parse as IP."); + } + + JsonVariant ap_config_gateway_val = doc["ap_config_gateway"]; + if (ap_config_gateway_val.is()) { + if (tempIp.fromString(ap_config_gateway_val.as())) { + ap_config_gateway = tempIp; + Log.infoln("Loaded ap_config_gateway: %s", ap_config_gateway.toString().c_str()); + } else { + Log.warningln("Failed to parse IP from string for 'ap_config_gateway': %s", ap_config_gateway_val.as()); + } + } else if (!ap_config_gateway_val.isNull()) { + Log.warningln("Value for 'ap_config_gateway' is not a string, cannot parse as IP."); + } + + JsonVariant ap_config_subnet_val = doc["ap_config_subnet"]; + if (ap_config_subnet_val.is()) { + if (tempIp.fromString(ap_config_subnet_val.as())) { + ap_config_subnet = tempIp; + Log.infoln("Loaded ap_config_subnet: %s", ap_config_subnet.toString().c_str()); + } else { + Log.warningln("Failed to parse IP from string for 'ap_config_subnet': %s", ap_config_subnet_val.as()); + } + } else if (!ap_config_subnet_val.isNull()) { + Log.warningln("Value for 'ap_config_subnet' is not a string, cannot parse as IP."); + } +#endif + Log.infoln("WiFiNetworkSettings::load - Finished loading WiFi settings."); + return E_OK; + } + + JsonDocument toJSON() const { + JsonDocument doc; // Using dynamic allocation for ArduinoJson v6+ + + doc["sta_ssid"] = sta_ssid; + // doc["sta_password"] = sta_password; // Omit password for security + doc["sta_local_ip"] = sta_local_IP.toString(); + doc["sta_gateway"] = sta_gateway.toString(); + doc["sta_subnet"] = sta_subnet.toString(); + doc["sta_primary_dns"] = sta_primary_dns.toString(); + doc["sta_secondary_dns"] = sta_secondary_dns.toString(); + +#ifdef ENABLE_AP_STA + doc["ap_ssid"] = ap_ssid; + // doc["ap_password"] = ap_password; // Omit password for security + doc["ap_config_ip"] = ap_config_ip.toString(); + doc["ap_config_gateway"] = ap_config_gateway.toString(); + doc["ap_config_subnet"] = ap_config_subnet.toString(); +#else + // Consistently represent AP settings even if not enabled + doc["ap_ssid"] = ""; // Or consider omitting if not defined by ENABLE_AP_STA + // doc["ap_password"] = ""; + doc["ap_config_ip"] = "0.0.0.0"; + doc["ap_config_gateway"] = "0.0.0.0"; + doc["ap_config_subnet"] = "0.0.0.0"; +#endif + return doc; + } +}; + +#endif // WIFI_NETWORK_SETTINGS_H \ No newline at end of file