refactor 2/2 | temp profiles

This commit is contained in:
lovebird 2025-05-23 23:14:09 +02:00
parent afc9886763
commit daf23e872a
15 changed files with 2038 additions and 16 deletions

243
src/components/RS485.cpp Normal file
View File

@ -0,0 +1,243 @@
#include "RS485.h"
#include <modbus/ModbusRTU.h>
#include "Logger.h"
#include <Component.h>
#include <RTUutils.h>
#include <enums.h>
#include <macros.h>
#include <modbus/ModbusTypes.h>
#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<int>(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<RS485 *>(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);
}

41
src/components/RS485.h Normal file
View File

@ -0,0 +1,41 @@
#ifndef RS485_H
#define RS485_H
#include <Component.h>
#include <ArduinoLog.h>
#include <ModbusClientRTU.h>
#include <modbus/ModbusRTU.h>
#include <modbus/ModbusTypes.h>
#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

View File

@ -7,8 +7,10 @@
#include <enums.h>
#include "config.h"
#include <modbus/Modbus.h>
#include "config-modbus.h"
#include <modbus/ModbusTCP.h>
#include "config-modbus.h" // application-specific modbus configuration
class Bridge;
class Relay : public Component
{

View File

@ -3,15 +3,15 @@
#ifdef ENABLE_RS485
#include <Logger.h>
#include "error_codes.h"
#include "components/SAKO_VFD.h"
#include <modbus/ModbusTypes.h>
#include <modbus/Modbus.h>
#include "RS485.h"
#include "SakoTypes.h"
#include "Sako-Registers.h"
#include <components/RS485.h>
#include <enums.h>
#include <Bridge.h>
#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

View File

@ -1,6 +1,3 @@
#ifndef ERROR_CODES_H
#define ERROR_CODES_H
#endif

View File

@ -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<ushort>(tcpBaseAddr + static_cast<ushort>(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<ushort>(tcpBaseAddr + static_cast<ushort>(offset_enum)), \
1, \
@ -67,4 +67,4 @@
/** @} */
#indif
#endif

View File

@ -1,7 +1,9 @@
#include <ArduinoLog.h>
#include "ModbusTypes.h"
#include "ModbusRTU.h"
#include "RS485.h"
#include <modbus/ModbusTypes.h>
#include <modbus/ModbusRTU.h>
#include <components/RS485.h>
bool RTU_Base::triggerRTUWrite()
{

View File

@ -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<ushort>(tcpBaseAddr + static_cast<ushort>(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<ushort>(tcpBaseAddr + static_cast<ushort>(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<ushort>(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 <typename T>
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.

198
src/profiles/PlotBase.cpp Normal file
View File

@ -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<uint32_t>();
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;
}
}

170
src/profiles/PlotBase.h Normal file
View File

@ -0,0 +1,170 @@
#ifndef PLOT_BASE_H
#define PLOT_BASE_H
#include <stdint.h>
#include <ArduinoJson.h>
#include <Arduino.h> // For millis(), min()
#include <limits.h> // For ULONG_MAX
#include <Component.h>
#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<typename T>
inline T* getUserData() const {
return static_cast<T*>(_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

131
src/profiles/SignalPlot.cpp Normal file
View File

@ -0,0 +1,131 @@
#include "SignalPlot.h"
#include <math.h> // 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<JsonArray>();
_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<JsonObject>();
// Assume keys exist and types are correct
_controlPoints[i].id = pointObj["id"].as<uint8_t>();
_controlPoints[i].time = pointObj["time"].as<uint32_t>();
// Cast the integer state from JSON to the SignalState enum
_controlPoints[i].state = static_cast<E_SignalState>(pointObj["state"].as<int16_t>());
_controlPoints[i].user = pointObj["user"].as<int16_t>();
}
// 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<int16_t>(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
}
}

95
src/profiles/SignalPlot.h Normal file
View File

@ -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": <uint8>, "time": <uint32>, "state": <int16>, "user": <int16> },
* ...
* ]
*/
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

View File

@ -0,0 +1,643 @@
#include "TemperatureProfile.h"
#include <math.h>
#include <Bridge.h>
#include <modbus/ModbusTCP.h>
#include <Logger.h>
#include "enums.h"
#include <modbus/ModbusTypes.h>
#include "config-modbus.h"
#include <modbus/Modbus.h>
#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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<String>();
}
// Load duration (in seconds) if present and convert to milliseconds
if (config.containsKey("duration")) {
uint32_t duration_s = config["duration"].as<uint32_t>();
_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<JsonArray>();
_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<JsonObject>();
int16_t x_scaled = pointObj["x"].as<int16_t>();
int16_t y_scaled = pointObj["y"].as<int16_t>();
int typeInt = pointObj["type"].as<int>();
_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<JsonArray>();
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<uint16_t>()) {
_targetRegisters.push_back(targetArray[i].as<uint16_t>());
}
}
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<ushort>();
}
if (config.containsKey("count")) {
count = config["count"].as<ushort>();
}
return true;
}
int16_t TemperatureProfile::getTemperature(uint32_t elapsedMs) const
{
if (!_running || _numControlPoints < 2)
{
return 0;
}
uint32_t lastPointTimeMs = static_cast<uint32_t>(((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<int16_t>(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<uint32_t>(((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<uint32_t>(((uint64_t)p0.x * (uint64_t)_durationMs) / PROFILE_SCALE);
uint32_t segmentEndTime = static_cast<uint32_t>(((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<int16_t>(scaledTemp);
}
// Calculate progress within the segment (0-PROFILE_SCALE)
uint32_t timeInSegment = elapsedMs - segmentStartTime;
uint16_t t_norm = static_cast<uint16_t>(((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<int16_t>(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<int16_t>(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<int16_t>(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<int16_t>((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 *>(&_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<TemperatureProfile *>(this)->modbusTCP = manager;
Log.infoln("%s: Registering Modbus blocks...", name.c_str());
ModbusBlockView *blocksView = mb_tcp_blocks();
Component *thiz = const_cast<TemperatureProfile *>(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<TemperatureProfileRegisterOffset>(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<TemperatureProfileRegisterOffset>(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<uint32_t>(((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<uint32_t>(((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<uint32_t>(((uint64_t)p0.x * (uint64_t)_durationMs) / PROFILE_SCALE);
uint32_t segmentEndTime = static_cast<uint32_t>(((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<int16_t>(scaledTemp);
}
// Calculate progress within the segment (0-PROFILE_SCALE)
uint32_t timeInSegment = elapsedMs - segmentStartTime;
uint16_t t_norm = static_cast<uint16_t>(((uint64_t)timeInSegment * (uint64_t)PROFILE_SCALE) / segmentDuration);
return lerp(p0.y, p1.y, t_norm);
}
#endif // ENABLE_PROFILE_TEMPERATURE

View File

@ -0,0 +1,173 @@
#ifndef TEMPERATURE_PROFILE_H
#define TEMPERATURE_PROFILE_H
#include "PlotBase.h"
#include <modbus/ModbusTCP.h> // Include for ModbusManager type
#include "enums.h" // Include for error codes (like E_OK)
#include <macros.h> // Include for LOW_WORD/HIGH_WORD (if defined there)
#include <ArduinoJson.h>
#include <Component.h>
#include <modbus/ModbusTypes.h>
#include <vector>
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<uint16_t>(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<uint16_t>& 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<uint16_t> _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

View File

@ -0,0 +1,245 @@
#ifndef WIFI_NETWORK_SETTINGS_H
#define WIFI_NETWORK_SETTINGS_H
#include <Arduino.h> // Should be first for core Arduino types
#include <IPAddress.h> // For IPAddress
#include <WString.h> // Explicitly include for String, though Arduino.h often covers it
#include <ArduinoJson.h> // For JSON functionality
#include <ArduinoLog.h> // 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<const char*>()) {
sta_ssid = sta_ssid_val.as<String>();
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<const char*>()) {
sta_password = sta_password_val.as<String>();
// 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<const char*>()) {
if (tempIp.fromString(sta_local_ip_val.as<const char*>())) {
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<const char*>());
}
} 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<const char*>()) {
if (tempIp.fromString(sta_gateway_val.as<const char*>())) {
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<const char*>());
}
} 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<const char*>()) {
if (tempIp.fromString(sta_subnet_val.as<const char*>())) {
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<const char*>());
}
} 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<const char*>()) {
if (tempIp.fromString(sta_primary_dns_val.as<const char*>())) {
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<const char*>());
}
} 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<const char*>()) {
if (tempIp.fromString(sta_secondary_dns_val.as<const char*>())) {
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<const char*>());
}
} 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<const char*>()) {
ap_ssid = ap_ssid_val.as<String>();
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<const char*>()) {
ap_password = ap_password_val.as<String>();
// 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<const char*>()) {
if (tempIp.fromString(ap_config_ip_val.as<const char*>())) {
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<const char*>());
}
} 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<const char*>()) {
if (tempIp.fromString(ap_config_gateway_val.as<const char*>())) {
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<const char*>());
}
} 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<const char*>()) {
if (tempIp.fromString(ap_config_subnet_val.as<const char*>())) {
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<const char*>());
}
} 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