From 147a55ff10b4c2f5a5f2cf875da38c46b70e58c2 Mon Sep 17 00:00:00 2001 From: babayaga Date: Fri, 6 Jun 2025 14:02:29 +0200 Subject: [PATCH] plot silbing --- src/Component.h | 516 +++++++++++++++++++++++++++- src/ValueWrapper.h | 4 +- src/components/Relay.h | 57 ++- src/components/RestServer.cpp | 15 +- src/components/RestServer.h | 4 +- src/modbus/ModbusRTU.cpp | 52 +-- src/profiles/PlotBase.cpp | 59 ++++ src/profiles/PlotBase.h | 35 +- src/profiles/SignalPlot.cpp | 67 ++-- src/profiles/SignalPlot.h | 11 +- src/profiles/TemperatureProfile.cpp | 63 +--- src/profiles/TemperatureProfile.h | 16 +- 12 files changed, 740 insertions(+), 159 deletions(-) diff --git a/src/Component.h b/src/Component.h index c038adb1..2674c49b 100644 --- a/src/Component.h +++ b/src/Component.h @@ -1 +1,515 @@ -LyoKICogQ29tcG9uZW50LmgKICogUmV2aXNpb246IGluaXRpYWwgZG9jdW1lbnRhdGlvbgogKi8KCiNpZm5kZWYgQ09NUE9ORU5UX0gKI2RlZmluZSBDT01QT05FTlRfSAoKI2luY2x1ZGUgPFdTdHJpbmcuaD4KI2luY2x1ZGUgPEFyZHVpbm9Mb2cuaD4KI2luY2x1ZGUgPFZlY3Rvci5oPgojaW5jbHVkZSAiLi9lbnVtcy5oIgojaW5jbHVkZSAiY29uc3RhbnRzLmgiCiNpbmNsdWRlICJlcnJvcl9jb2Rlcy5oIgojaW5jbHVkZSAibWFjcm9zLmgiCiNpbmNsdWRlICJ4dHlwZXMuaCIKCgpjbGFzcyBCcmlkZ2U7CmNsYXNzIE1vZGJ1c1RDUDsKY2xhc3MgTW9kYnVzQmxvY2tWaWV3OwpjbGFzcyBNQl9SZWdpc3RlcnM7CmNsYXNzIFJTNDg1Owo7KioKICogQGJyaWVmIFRoZSBDb21wb25lbnQgY2xhc3MgcmVwcmVzZW50cyBhIGdlbmVyaWMgY29tcG9uZW50LgogKi8KY2xhc3MgQ29tcG9uZW50CnsKcHVibGljOgogICAgLyoqCiAgICAgKiBAYnJpZWYgVGhlIGRlZmF1bHQgcnVuIGZsYWdzIGZvciBhIGNvbXBvbmVudC4KICAgICAqLwogICAgc3RhdGljIGNvbnN0IGludCBDT01QT05FTlRfREVGQVVMVCA9IDEgPDwgT0JKRUNUX1JVTl9GTEFHU19EX09GX0xPT1AgfCAxIDw8IE9CSkVDVF9SVU5fRkxBR1M6OkVfT0ZfU0VUVVA7CgogICAgLyoqCiAgICAgKiBAYnJpZWYgVGhlIGRlZmF1bHQgSUQgZm9yIGEgY29tcG9uZW50LgogICAgICovCiAgICBzdGF0aWMgY29uc3QgdXNob3J0IENPTVBPTkVOVF9OT19JRCA9IDA7CgogICAgLyoqCiAgICAgKiBAYnJpZWYgVGhlIHR5cGUgb2YgdGhlIGNvbXBvbmVudC4KICAgICAqLwogICAgdXNob3J0IHR5cGUgPSBDT01QT05FTlRfVFlQRTo6Q09NUE9ORU5UX1RZUEVfVU5LT1dOOwoKICAgIC8qKgogICAgICogQGJyaWVmIERlZmF1bHQgY29uc3RydWN0b3IgZm9yIHRoZSBDb21wb25lbnQgY2xhc3MuCiAgICAgKi8KICAgIENvbXBvbmVudCgpIDogbmFtZSgiTk9fTkFNRSIpLCBpZCgwKSwKICAgICAgICAgICAgICAgICAgZmxhZ3MoT0JKRUNUX1JVTl9GTEFHU1dGX05PTkUpLAogICAgICAgICAgICAgICAgICBuRmxhZ3MoT0JKRUNUX05FVF9DQVBTOjpFX05DQVBTX05PTkUpLCAKICAgICAgICAgICAgICAgICAgb3duZXIobnVsbHB0ciksQAAAAnMJbGF2ZUlkKDApIHt9CgogICAgLyoqCiAgICAgKiBAYnJpZWYgQ29uc3RydWN0b3IgZm9yIHRoZSBDb21wb25lbnQgY2xhc3Mgd2l0aCBhIHNwZWNpZmllZCBuYW1lLgogICAgICogQHBhcmFtIF9uYW1lIFRoZSBuYW1lIG9mIHRoZSBjb21wb25lbnQuCiAgICAgKi8KICAgIENvbXBvbmVudChTdHJpbmcgX25hbWUpIDogbmFtZShfbmFtZSksIGlkKENPTVBPTkVOVF9OT19JRCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBmbGFncyhPQkpFQ1RfUlVOX0ZMQUdTOjpFX09GX05PTkUpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgbkZsYWdzKE9CSkVDVF9ORV11zcRQ1Mpo5FX05DQVBTX05PTkUpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIG93bmVyKG51bGxwdHIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgc2xhdmVJZCgwKSB7fQoKICAgIC8qKgogICAgICogQGJyaWVmIENvbnN0cnVjdG9yIGZvciB0aGUgQ29tcG9uZW50IGNsYXNzIHdpdGggYSBzcGVjaWZpZWQgbmFtZSBhbmQgSUQuCiAgICAgKiBAcGFyYW0gX25hbWUgVGhlIG5hbWUgb2YgdGhlIGNvbXBvbmVudC4KICAgICAqIEBwYXJhbSBfaWQgVGhlIElEIG9mIHRoZSBjb21wb25lbnQuCiAgICAgKi8KICAgIENvbXBvbmVudChTdHJpbmcgX25hbWUsIHVzaG9ydCBfaWQpIDogbmFtZShfbmFtZSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaWQoX2lkKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmbGFncyhPQkpFQ1RfUlVOX0ZMQUdTOjpFX09GX05PTkUpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5GbGFncyNVSf2RFQ1RfTkVUX0NBUFPLkVfTkNBUFNfTk9ORSksIAokICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb3duZXIobnVsbHB0ciksCoUAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzbGF2ZUlkKDApIHt9CgogICAgLyoqCiAgICAgKiBAYnJpZWYgQ29uc3RydWN0b3IgZm9yIHRoZSBDb21wb25lbnQgY2xhc3Mgd2l0aCBhIHNwZWNpZmllZCBuYW1lLCBJRCwgYW5kIGZsYWdzLgogICAgICogQHBhcmFtIF9uYW1lIFRoZSBuYW1lIG9mIHRoZSBjb21wb25lbnQuCiAgICAgKiBAcGFyYW0gX2lkIFRoZSBJRCBvZiB0aGUgY29tcG9uZW50LgogICAgICogQHBhcmFtIF9mbGFncyBUaGUgcnVuIGZsYWdzIGZvciB0aGUgY29tcG9uZW50LgogICAgICovCiAgICBDb21wb25lbnQoU3RyaW5nIF9uYW1lLCBzaG9ydCBfaWQsIGludCBfZmxhZ3MpIDogbmFtZShfbmFtZSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaWQoX2lkKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmbGFncyhfZmxhZ3MpLAtyCAgICAgICAgICAgICAgICAgICAgICAgICAgICngwOTAweCAgICAgICAgICAga3ygZSAgICAgICNGbGFncyhPQkpFQ1RfTkVUX0NBUFPl5VX05DQVBTX05PTkUpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBvd25lcilybnVsbHB0ciksOvgKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzbGF2ZUlkKDApCiAgICB7CiAgICB9OvgOvgKICAgIC8qKgogICAgICogQGJyaWVmIJnPDb25zdHJ1Y3RvciBmb3IgdGhlIENvbXBvbmVudCBjbGFzcyB3aXRoIGEgc3BlY2lmaWVkIG5hbWUsIElELCBmbGFncywgybW5kIG9ybmXyi7gkLvgogICAgICogQHBhcmFtIF9uYW1lIFRoZSBuYW1lIG5mIHRoZSBjb21wb25lbnTCKoMgICAgICogQHBhcmFtIF9pZCBUaGUgSUQgb2YgdGhlieNvbXBvbmVudC4KKgAAIAgUCAgICAgKiBAcGFyYW0gX2ZsYWdzIFRoZSBydW4gZmxhZ3MgZm9yIHRoZSBjb21wb25lbnQuCiAgICAgKiBAcGFyYW0gX293bmVyIFRoZSBvd25lciBvZiB0aGUgY29tcG9uZW50LgogICAgICovCiAgICBDb21wb25lbnQoU3RyaW5nIF9uYW1lLCB1c2hvcnQgX2lkLCB1aW50MTZfdCEf ZmxhZ3nSQ*b21wb25lbnQgKl9vd25lcikgOiAKICAgIG5GbGFncyhPQkpFQ1OLTa5FVF9DQVBTX19FX05DQVBTf5NTX05PTkUpLAogICAgbmFtZShfbmFtZSksCiAgICBpZCa3pBid4fkAwogICAgZmxhZ3MoX2ZsYWdz2FBvQkVVEAjb3duZXIoX293bmVyAEm4cHRyeCwKICAgIF9g7+abGx2ZUWlkKLApoqGZ7fQuKQ9hWOogICAgmgdCdKJ4ygb2YKCiYCSK4ggogggYiBVUSKBgC2ABgOOIAEq3VyCAgITUEABQJAgACAEgAEJp \ No newline at end of file +#ifndef COMPONENT_H +#define COMPONENT_H + +#include +#include +#include +#include "./enums.h" +#include "constants.h" +#include "error_codes.h" +#include "macros.h" +#include "xtypes.h" + + +class Bridge; +class ModbusTCP; +class ModbusBlockView; +class MB_Registers; +class RS485; +/** + * @brief The Component class represents a generic component. + */ +class Component +{ +public: + /** + * @brief The default run flags for a component. + */ + static const int COMPONENT_DEFAULT = 1 << OBJECT_RUN_FLAGS::E_OF_LOOP | 1 << OBJECT_RUN_FLAGS::E_OF_SETUP; + + /** + * @brief The default ID for a component. + */ + static const ushort COMPONENT_NO_ID = 0; + + /** + * @brief The type of the component. + */ + ushort type = COMPONENT_TYPE::COMPONENT_TYPE_UNKOWN; + + /** + * @brief Default constructor for the Component class. + */ + Component() : name("NO_NAME"), id(0), + flags(OBJECT_RUN_FLAGS::E_OF_NONE), + nFlags(OBJECT_NET_CAPS::E_NCAPS_NONE), + owner(nullptr), + slaveId(0) {} + + /** + * @brief Constructor for the Component class with a specified name. + * @param _name The name of the component. + */ + Component(String _name) : name(_name), id(COMPONENT_NO_ID), + flags(OBJECT_RUN_FLAGS::E_OF_NONE), + nFlags(OBJECT_NET_CAPS::E_NCAPS_NONE), + owner(nullptr), + slaveId(0) {} + + /** + * @brief Constructor for the Component class with a specified name and ID. + * @param _name The name of the component. + * @param _id The ID of the component. + */ + Component(String _name, ushort _id) : name(_name), + id(_id), + flags(OBJECT_RUN_FLAGS::E_OF_NONE), + nFlags(OBJECT_NET_CAPS::E_NCAPS_NONE), + owner(nullptr), + slaveId(0) {} + + /** + * @brief Constructor for the Component class with a specified name, ID, and flags. + * @param _name The name of the component. + * @param _id The ID of the component. + * @param _flags The run flags for the component. + */ + Component(String _name, short _id, int _flags) : name(_name), + id(_id), + flags(_flags), + nFlags(OBJECT_NET_CAPS::E_NCAPS_NONE), + owner(nullptr), + slaveId(0) + { + } + + /** + * @brief Constructor for the Component class with a specified name, ID, flags, and owner. + * @param _name The name of the component. + * @param _id The ID of the component. + * @param _flags The run flags for the component. + * @param _owner The owner of the component. + */ + Component(String _name, ushort _id, uint16_t _flags, Component *_owner) : + nFlags(OBJECT_NET_CAPS::E_NCAPS_NONE), + name(_name), + id(_id), + flags(_flags), + owner(_owner), + slaveId(0) + { + } + + /** + * @brief Destructor for the Component class. + */ + virtual ~Component() = default; + + /** + * @brief Virtual function to destroy the component. + * @return The error code indicating the success or failure of the operation. + */ + virtual short destroy() { return E_OK; }; + + /** + * @brief Virtual function to debug the component. + * @param stream The stream to output the debug information to. + * @return The error code indicating the success or failure of the operation. + */ + virtual short debug() { return E_OK; }; + + /** + * @brief Virtual function to debug the component. + * @param stream The stream to output the debug information to. + * @return The error code indicating the success or failure of the operation. + */ + virtual short debug(short val0, short val1) { return E_OK; }; + + /** + * @brief Virtual function to display information about the component. + * @param stream The stream to output the information to. + * @return The error code indicating the success or failure of the operation. + */ + virtual short info() { return E_OK; }; + + /** + * @brief Virtual function to display information about the component. + * @param stream The stream to output the information to. + * @return The error code indicating the success or failure of the operation. + */ + virtual short info(short val0, short val1) { return E_OK; }; + + /** + * @brief Virtual function to set up the component. + * @return The error code indicating the success or failure of the operation. + */ + virtual short setup() { return E_OK; }; + + /** + * @brief Virtual function being called after all components have been setup. + * @return The error code indicating the success or failure of the operation. + */ + virtual short onRun() { return E_OK; }; + + /** + * @brief Virtual function to run the component in a loop. + * @return The error code indicating the success or failure of the operation. + */ + virtual short loop() { + _loop_start_time_us = micros(); + // Derived classes will call this base implementation or implement their own logic + // Execution of derived class loop happens here + // Then, the duration is calculated in the derived class if it overrides this, or after App::loop() in PHApp + return E_OK; + }; + + /** + * @brief Checks if the component has a specific flag. + * @param flag The flag to check. + * @return True if the component has the flag, false otherwise. + */ + bool hasFlag(byte flag) + { + return TEST(flags, flag); + } + + /** + * @brief Sets a specific flag for the component. + * @param flag The flag to set. + */ + void setFlag(byte flag) + { + SBI(flags, flag); + } + + /** + * @brief Sets a specific flag for the component. + * @param flag The flag to set. + */ + short toggleFlag(short flag, short value) + { + if (value) + { + SBI(flags, flag); + } + else + { + CBI(flags, flag); + } + return flags; + } + + /** + * @brief Clears a specific flag for the component. + * @param flag The flag to clear. + */ + void clearFlag(byte flag) + { + CBI(flags, flag); + } + + /** + * @brief Enables the component. + */ + void enable() + { + clearFlag(OBJECT_RUN_FLAGS::E_OF_DISABLED); + } + + /** + * @brief Disables the component. + */ + void disable() + { + setFlag(OBJECT_RUN_FLAGS::E_OF_DISABLED); + } + + /** + * @brief Checks if the component is enabled. + * @return True if the component is enabled, false otherwise. + */ + bool enabled() + { + return !hasFlag(OBJECT_RUN_FLAGS::E_OF_DISABLED); + } + + /** + * @brief The name of the component. + */ + String name; + + /** + * @brief The ID of the component. + */ + const ushort id; + + /** + * @brief The run flags for the component. + */ + uint16_t flags; + + /** + * @brief The network capabilities of the component. + */ + uint16_t nFlags; + + /** + * @brief The owner of the component. + */ + Component *owner; + + /** + * @brief The current time in milliseconds. + */ + millis_t now; + + /** + * @brief The last tick time in milliseconds. + */ + millis_t last; + + /** + * @brief Start time of the last loop execution in microseconds. + */ + uint64_t _loop_start_time_us; + + /** + * @brief Duration of the last loop execution in microseconds. + */ + uint64_t _loop_duration_us; + + ////////////////////////////////////////// + // + // Component Hierarchy / Lookup + // + + /** + * @brief Virtual method to retrieve a component managed by this component (or its children) by ID. + * The base implementation returns nullptr. + * Owners like PHApp should override this to provide actual lookup. + * @param id The ID of the component to find. + * @return Pointer to the component if found, nullptr otherwise. + */ + virtual Component* getComponent(short id) { return nullptr; } + + ////////////////////////////////////////// + // + // Messaging + // @todo: extract to a separate class + + /** + * @brief Handles incoming messages. + * + * This function is called when a message is received by the component. + * It processes the message and returns a short value indicating the status of the operation. + * + * @param id The ID of the message. + * @param verb The type of operation to be performed. + * @param flags The flags associated with the message. + * @param user A pointer to user-defined data. + * @param src The source component that sent the message. + * @return A short value indicating the status of the operation. + */ + virtual short onMessage(int id, E_CALLS verb, E_MessageFlags flags, String user, Component *src) + { + return E_OK; + }; + + /** + * @brief Handles incoming messages with a generic void* payload. + * + * @param id The ID of the message. + * @param verb The type of operation to be performed. + * @param flags The flags associated with the message. + * @param user A pointer to user-defined data (nullptr if not provided). + * @param src The source component that sent the message (nullptr if not provided). + * @return A short value indicating the status of the operation. + */ + virtual short onMessage(int id, E_CALLS verb, E_MessageFlags flags, void* user = nullptr, Component *src = nullptr) + { + return E_OK; + }; + /** + * @brief Handles errors. + * @param id The ID of the error. + * @param error The error code. + * @return The error code indicating the success or failure of the operation. + */ + virtual short onError(short id, short error) { return E_OK; }; + + /** + * @brief Handles responses. + * @param id The ID of the response. + * @param response The response code. + * @return The response code indicating the success or failure of the operation. + */ + virtual short onResponse(short id, short response) { return E_OK; }; + + ////////////////////////////////////////// + // + // Binding + + /** + * Registers methods for the component with the specified bridge. + * This method should be overridden by derived classes to provide custom method registration logic. + * + * @param bridge The bridge to register methods with. + * @return The status code indicating the success or failure of the method registration. + */ + virtual short serial_register(Bridge *bridge) { return E_OK; } + + /** + * @brief Sets a specific network capability flag for the component. + * @param flag The network capability flag to set (from OBJECT_NET_CAPS). + */ + void setNetCapability(OBJECT_NET_CAPS flag) + { + SBI(nFlags, flag); + } + + /** + * @brief Checks if the component has a specific network capability flag. + * @param flag The network capability flag to check (from OBJECT_NET_CAPS). + * @return True if the component has the capability, false otherwise. + */ + bool hasNetCapability(OBJECT_NET_CAPS flag) const + { + return TEST(nFlags, flag); + } + + /** + * @brief Clears a specific network capability flag for the component. + * @param flag The network capability flag to clear (from OBJECT_NET_CAPS). + */ + void clearNetCapability(OBJECT_NET_CAPS flag) + { + CBI(nFlags, flag); + } + + ////////////////////////////////////////// + // + // Network Interface (Modbus, Serial, CAN, etc.) + // + + /** + * @brief Called by a network manager (e.g., ModbusTCP) to write a value to this component. + * Derived classes should implement this to handle incoming network writes specific to their function. + * @param address The specific Modbus address being written to within the component's range. + * @param value The value received from the network. + * @return E_OK on success, or an appropriate error code. + */ + virtual short mb_tcp_write(short address, short value) { + return 0; + }; + + /** + * @brief Variant of mb_tcp_write accepting MB_Registers context. + * @param reg The MB_Registers block associated with this write request. + * @param value The value received from the network. + * @return E_OK on success, or an appropriate error code. + */ + virtual short mb_tcp_write(MB_Registers * reg, short value) { + return 0; + }; + + /** + * @brief Called by a network manager (e.g., ModbusTCP) to read a value from this component. + * Derived classes should implement this to provide their current state to the network. + * @param address The specific Modbus address being read within the component's range. + * @return The current value for the given address, or potentially an error indicator. + */ + virtual short mb_tcp_read(short address) { + return 0; + } + + /** + * @brief Variant of mb_tcp_read accepting MB_Registers context. + * @param reg The MB_Registers block associated with this read request. + * @return The current value for the register block, or potentially an error indicator. + */ + virtual short mb_tcp_read(MB_Registers * reg) { + return 0; + } + + /** + * @brief Get the last error code + */ + virtual ushort mb_tcp_error(MB_Registers * reg) { return 0; } + + /** + * @brief Called during setup to allow the component to register its Modbus blocks. + * + * Derived classes should override this. It's recommended to call mb_tcp_blocks() + * inside this function, iterate through the returned view, add the runtime + * component ID to each MB_Registers struct, and then register it with the manager. + * + * @param manager Pointer to the ModbusTCP instance. + */ + virtual void mb_tcp_register(ModbusTCP* manager) const { + // Base implementation does nothing. + } + + /** + * @brief Gets a view of the static Modbus block definitions for this component type. + * + * @note The componentId field within the returned MB_Registers structs may not be + * populated, as the definitions are typically static/constexpr. + * Use mb_tcp_register to handle registration with the correct runtime ID. + * + * @return A ModbusBlockView describing the blocks handled by this component type. + * Default implementation returns an empty view {nullptr, 0}. + */ + virtual ModbusBlockView* mb_tcp_blocks() const { return nullptr; } + + /** + * @brief Gets the base Modbus TCP address allocated for this RTU device instance. + * @return The base TCP address for this device instance. + */ + virtual uint16_t mb_tcp_base_address() const { return 0; } + + /** + * @brief The Modbus slave ID for this component (satisfies the Modbus interfaces) + */ + ushort slaveId; + + /** + * @brief The RS485 interface for this component. + * @todo: move to feature + */ + RS485* rs485; + +protected: + /** + * @brief Called by derived classes when their internal state changes in a way that should be reflected on the network. + * The base class (or a network manager observing this) should handle queuing the update. + */ + virtual void notifyStateChange() { + // Base implementation could potentially interact with a NetworkManager singleton/instance + // Log.verboseln("Component::notifyStateChange - ID %d", id); + } + +public: + ////////////////////////////////////////// + // + // Component Hierarchy / Lookup + virtual Component *byId(ushort id) { return nullptr; } + + /** + * @brief Gets the duration of the last loop execution in microseconds. + * @return The loop duration in microseconds. + */ + uint64_t getLoopDurationUs() const { return _loop_duration_us; } +}; + +/** + * @brief Function pointer type for component member functions. + */ +typedef short (Component::*ComponentFnPtr)(short, short); +/** + * @brief Function pointer type for component member functions with variable arguments. + */ +typedef short (Component::*ComponentVarArgsFnPtr)(...); + +typedef short (Component::*ComponentRxFn)(short size, uint8_t rxBuffer[]); + +#endif diff --git a/src/ValueWrapper.h b/src/ValueWrapper.h index 578a3bba..cf2600b6 100644 --- a/src/ValueWrapper.h +++ b/src/ValueWrapper.h @@ -124,7 +124,7 @@ public: update_msg.functionCode = m_notificationFunctionCode; update_msg.componentId = m_sourceComponentId; - Log.infoln("ValueWrapper (CompID: %u, Mode: %d): Threshold met for reg_offset %u. Old: %d, New: %d. Notifying owner.", + Log.traceln("ValueWrapper (CompID: %u, Mode: %d): Threshold met for reg_offset %u. Old: %d, New: %d. Notifying owner.", m_sourceComponentId, static_cast(m_thresholdMode), m_modbusRegisterOffset, static_cast(oldValue), static_cast(newValue)); @@ -135,7 +135,7 @@ public: m_onNotifiedPostCallback(newValue, oldValue); } } else { - Log.warningln("ValueWrapper (CompID: %u): Threshold met for reg_offset %u, but owner or base_addr_lambda is null. Cannot notify.", + Log.traceln("ValueWrapper (CompID: %u): Threshold met for reg_offset %u, but owner or base_addr_lambda is null. Cannot notify.", m_sourceComponentId, m_modbusRegisterOffset); if (m_onNotifiedPostCallback) { m_onNotifiedPostCallback(newValue, oldValue); diff --git a/src/components/Relay.h b/src/components/Relay.h index fc5e0f5c..79f32795 100644 --- a/src/components/Relay.h +++ b/src/components/Relay.h @@ -16,10 +16,14 @@ class Relay : public Component { private: // Keep address private, provide via getModbusInfo const short modbusAddress; // Store Modbus address internally - MB_Registers m_modbus_block; + MB_Registers m_modbus_block[2]; // m_modbus_view needs to be mutable to be returned as ModbusBlockView* from a const method. mutable ModbusBlockView m_modbus_view; + // for blinking + unsigned long onOffFrequency; // in seconds + unsigned long lastToggleTime; + public: Relay( Component *owner, @@ -29,14 +33,16 @@ public: : Component("Relay", _id, Component::COMPONENT_DEFAULT, owner), pin(_pin), modbusAddress(_modbusAddress), - value(false) + value(false), + onOffFrequency(0), + lastToggleTime(0) { setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); // Initialize instance-specific Modbus block. // The modbusAddress is the actual start address for the Relay's single coil. // So, the offset passed to the macro is 0. - m_modbus_block = INIT_MODBUS_BLOCK_TCP( + m_modbus_block[0] = INIT_MODBUS_BLOCK_TCP( this->modbusAddress, // Base address for this component's block 0, // Offset for this specific register E_FN_CODE::FN_WRITE_COIL, // Function code @@ -45,15 +51,24 @@ public: nullptr // Group (nullptr if not applicable) ); + m_modbus_block[1] = INIT_MODBUS_BLOCK_TCP( + this->modbusAddress, // Base address for this component's block + 1, // Offset for the frequency register + E_FN_CODE::FN_WRITE_HOLD_REGISTER, // Function code + MB_ACCESS_READ_WRITE, // Access type + "Relay On/Off Freq (secs)", // Name + nullptr // Group (nullptr if not applicable) + ); + // Initialize the view to point to this instance-specific block - m_modbus_view.data = &m_modbus_block; // Point to the single block - m_modbus_view.count = 1; + m_modbus_view.data = m_modbus_block; // Point to the array of blocks + m_modbus_view.count = 2; } short info(short flags = 0, short val = 0) override { - Log.verboseln("Relay::info - ID: %d, Pin: %d, Modbus Addr: %d, Value: %d, NetCaps: %d", - id, pin, modbusAddress, value, nFlags); + Log.verboseln("Relay::info - ID: %d, Pin: %d, Modbus Addr: %d, Value: %d, Freq: %lu, NetCaps: %d", + id, pin, modbusAddress, value, onOffFrequency, nFlags); return E_OK; } @@ -84,6 +99,7 @@ public: short setValueCmd(short arg1, short arg2) { + onOffFrequency = 0; // manual override return setValue(arg1 > 0); } @@ -112,11 +128,15 @@ public: { if (address == modbusAddress) // Use internal member { - bool newValue = (networkValue > 0); - if (value != newValue) + onOffFrequency = 0; // Manual control stops blinking + return setValue(networkValue > 0); + } + else if (address == modbusAddress + 1) + { + onOffFrequency = networkValue; + if (onOffFrequency > 0) { - value = newValue; - digitalWrite(pin, value); // Re-enabled GPIO write + lastToggleTime = millis(); } return E_OK; } @@ -134,13 +154,17 @@ public: { return value ? 1 : 0; } + else if (address == modbusAddress + 1) + { + return onOffFrequency; + } return 0; // Default for mismatched addresses } short mb_tcp_read(MB_Registers *reg) override { // Log.traceln(F("Relay::mb_tcp_read (Reg Context) - TCP Addr: %d, Type: %d"), reg->startAddress, reg->type); - return value ? 1 : 0; + return mb_tcp_read(reg->startAddress); } void mb_tcp_register(ModbusTCP *manager) const override @@ -171,6 +195,15 @@ public: short loop() override { Component::loop(); + if (onOffFrequency > 0) + { + unsigned long currentMillis = millis(); + if (currentMillis - lastToggleTime >= onOffFrequency * 1000) + { + lastToggleTime = currentMillis; + setValue(!value); + } + } return E_OK; } diff --git a/src/components/RestServer.cpp b/src/components/RestServer.cpp index 8854d994..3df9d540 100644 --- a/src/components/RestServer.cpp +++ b/src/components/RestServer.cpp @@ -1203,9 +1203,9 @@ void sendJsonResponse(AsyncWebSocketClient *client, const JsonDocument &doc, con Log.warningln("WS #%u: JSON response too large (%u bytes), sending error instead.", client->id(), responseStr.length()); JsonDocument errorDoc; errorDoc["type"] = "error"; - JsonObject dataObj = errorDoc.createNestedObject("data"); - dataObj["message"] = "Response too large to send"; - dataObj["original_type"] = type; + JsonObject errorDataObj = errorDoc["data"].to(); + errorDataObj["message"] = "Response too large to send"; + errorDataObj["original_type"] = type; String errorStr; serializeJson(errorDoc, errorStr); client->text(errorStr); @@ -1470,7 +1470,7 @@ void RESTServer::handleWebSocketMessage(AsyncWebSocketClient *client, void *arg, JsonDocument errorDoc; errorDoc["type"] = "error"; // Maintain the {type: "error", data: {message:"..."}} structure for errors for consistency with sendJsonResponse - JsonObject errorDataObj = errorDoc.createNestedObject("data"); + JsonObject errorDataObj = errorDoc["data"].to(); errorDataObj["message"] = "Response too large to send"; errorDataObj["original_type"] = "registers"; String errorStr; @@ -1663,7 +1663,6 @@ void RESTServer::handleWebSocketMessage(AsyncWebSocketClient *client, void *arg, } } } - void RESTServer::broadcast(BroadcastMessageType type, const JsonDocument &data) { JsonDocument doc; @@ -1682,6 +1681,12 @@ void RESTServer::broadcast(BroadcastMessageType type, const JsonDocument &data) case BROADCAST_SYSTEM_STATUS: typeStr = "system_status"; break; + case BROADCAST_USER_DEFINED: + typeStr = "user_defined"; + break; + case BROADCAST_USER_MESSAGE: + typeStr = "user_message"; + break; default: break; } diff --git a/src/components/RestServer.h b/src/components/RestServer.h index 3a65e234..e7e26b37 100644 --- a/src/components/RestServer.h +++ b/src/components/RestServer.h @@ -26,7 +26,9 @@ typedef enum : uint8_t { BROADCAST_COIL_UPDATE, BROADCAST_REGISTER_UPDATE, BROADCAST_LOG_ENTRY, - BROADCAST_SYSTEM_STATUS // Example + BROADCAST_SYSTEM_STATUS, + BROADCAST_USER_DEFINED, + BROADCAST_USER_MESSAGE } BroadcastMessageType; class ModbusTCP; diff --git a/src/modbus/ModbusRTU.cpp b/src/modbus/ModbusRTU.cpp index 19c6ccc2..4be18e90 100644 --- a/src/modbus/ModbusRTU.cpp +++ b/src/modbus/ModbusRTU.cpp @@ -651,10 +651,9 @@ MB_Error ModbusRTU::executeOperation(ModbusOperation &op) } if (err != Modbus::SUCCESS) { - Log.error("Error adding request: %d for token %u", (int)err, op.token); + // Log.error("Error adding request: %d for token %u", (int)err, op.token); // Clear IN_PROGRESS flag using CBI and OP_IN_PROGRESS_BIT CBI(op.flags, OP_IN_PROGRESS_BIT); - // Only retry on specific communication errors, not queue full if (err == Modbus::TIMEOUT || err == Modbus::CRC_ERROR) { @@ -699,7 +698,9 @@ MB_Error ModbusRTU::executeOperation(ModbusOperation &op) op.slaveId, deviceErrorState.currentBackoffInterval[slaveIdx]); } - lastOperationTime = now + deviceErrorState.currentBackoffInterval[slaveIdx]; + // Add random jitter to the backoff time + long jitter = (random(-10, 11) / 100.0) * deviceErrorState.currentBackoffInterval[slaveIdx]; // +/- 10% + lastOperationTime = now + deviceErrorState.currentBackoffInterval[slaveIdx] + jitter; return MB_Error::OpClientQueueFull; } else @@ -724,6 +725,7 @@ MB_Error ModbusRTU::executeOperation(ModbusOperation &op) // Need to return appropriate error code from the if/else if branches if (err == Modbus::TIMEOUT || err == Modbus::CRC_ERROR) { + Log.traceln("Communication error for token %u, will retry later (retry %d) (error %d)", op.token, op.retries, (int)err); return (op.retries < MAX_RETRIES) ? MB_Error::OpRetrying : MB_Error::OpMaxRetriesExceeded; } else if (err == static_cast(MB_Error::OpClientQueueFull)) @@ -1151,36 +1153,6 @@ void ModbusRTU::setOnErrorCallback(OnErrorCallback callback) ModbusOperation *ModbusRTU::findOperationByToken(uint32_t token) { - unsigned long now = millis(); - if (token > 0 && now - token > OPERATION_TIMEOUT) - { - // Log.verboseln("Token %u is older than timeout (%lu ms old)", token, now - token); - } - - // Detailed debug trace to help understand what's in the queue - if (Log.getLevel() <= LOG_LEVEL_TRACE) - { - Log.traceln("Searching for token %u in queue of %u operations", token, operationCount); - int found = 0; - for (int i = 0; i < MAX_PENDING_OPERATIONS; i++) - { - if (TEST(operationQueue[i].flags, OP_USED_BIT)) - { - Log.traceln(" Queue slot %d: token=%u, address=%u, slaveId=%u, type=%d, age=%lu ms", - i, operationQueue[i].token, operationQueue[i].address, - operationQueue[i].slaveId, operationQueue[i].type, - now - operationQueue[i].timestamp); - found++; - if (found >= 5) - { // Limit to first 5 operations to avoid flooding logs - Log.traceln(" ... and %u more operations", operationCount - 5); - break; - } - } - } - } - - // Search in pending operations - Use TEST for (int i = 0; i < MAX_PENDING_OPERATIONS; i++) { if (TEST(operationQueue[i].flags, OP_USED_BIT) && operationQueue[i].token == token) @@ -1188,8 +1160,6 @@ ModbusOperation *ModbusRTU::findOperationByToken(uint32_t token) return &operationQueue[i]; } } - - Log.traceln("Token %u not found in operation queue", token); return nullptr; } @@ -1204,7 +1174,7 @@ void ModbusRTU::onErrorReceived(Error error, uint32_t token) if (operation == nullptr) { MB_Error mbError = static_cast(error); - Log.errorln("Callback Error: Operation not found for token %u (error %u: %s)", token, (int)error, modbusErrorToString(mbError)); + // Log.errorln("Callback Error: Operation not found for token %u (error %u: %s)", token, (int)error, modbusErrorToString(mbError)); return; } @@ -1233,9 +1203,8 @@ void ModbusRTU::onErrorReceived(Error error, uint32_t token) // Handle device-specific backoff for timeouts if (error == Modbus::TIMEOUT) { - uint8_t slaveIdx = operation->slaveId - 1; + uint8_t slaveIdx = operation->slaveId - 1; uint32_t now = millis(); - if (now - deviceErrorState.lastBackoffIncreaseTime[slaveIdx] > deviceErrorState.currentBackoffInterval[slaveIdx]) { deviceErrorState.lastBackoffIncreaseTime[slaveIdx] = now; @@ -1245,11 +1214,9 @@ void ModbusRTU::onErrorReceived(Error error, uint32_t token) deviceErrorState.currentBackoffInterval[slaveIdx] = MB_OFFLINE_BACKOFF_MAX; } } - Log.traceln("Device %u timeout. Setting backoff to %lu ms", - operation->slaveId, deviceErrorState.currentBackoffInterval[slaveIdx]); } - // This will pause the main ModbusRTU scheduler for the current backoff duration of this slave - lastOperationTime = now + deviceErrorState.currentBackoffInterval[slaveIdx]; + long jitter = (random(-10, 11) / 100.0) * deviceErrorState.currentBackoffInterval[slaveIdx]; // +/- 10% + lastOperationTime = now + deviceErrorState.currentBackoffInterval[slaveIdx] + jitter; } MB_Error mbError = static_cast(error); @@ -1626,6 +1593,5 @@ void ModbusRTU::resetDeviceOfflineState(uint8_t slaveId) { uint8_t idx = slaveId - 1; deviceErrorState.lastBackoffIncreaseTime[idx] = 0; deviceErrorState.currentBackoffInterval[idx] = MB_OFFLINE_BACKOFF_INITIAL; - Log.noticeln("Reset backoff for device %u to %lu ms", slaveId, deviceErrorState.currentBackoffInterval[idx]); } } diff --git a/src/profiles/PlotBase.cpp b/src/profiles/PlotBase.cpp index 2c44422a..1e0063bb 100644 --- a/src/profiles/PlotBase.cpp +++ b/src/profiles/PlotBase.cpp @@ -34,6 +34,7 @@ void PlotBase::start() { _running = true; _paused = false; _explicitlyStopped = false; // Ensure explicitlyStopped is false when starting + onStart(); } else { _running = false; _paused = false; @@ -47,6 +48,7 @@ void PlotBase::stop() { _explicitlyStopped = true; // Mark as explicitly stopped _elapsedMsAtPause = 0; _startTimeMs = 0; // Reset startTime to ensure IDLE/STOPPED state if duration not met + onStop(); } void PlotBase::pause() { @@ -61,6 +63,7 @@ void PlotBase::pause() { _elapsedMsAtPause = min(currentElapsed, _durationMs); _paused = true; _explicitlyStopped = false; // Pausing implies it's not in a fully stopped state + onPause(); } } @@ -74,6 +77,7 @@ void PlotBase::resume() { } _paused = false; _explicitlyStopped = false; // Resuming implies it's not stopped + onResume(); } } @@ -198,4 +202,59 @@ PlotStatus PlotBase::getCurrentStatus() const { } return PlotStatus::IDLE; // Default if no other state matches +} + +bool PlotBase::addPlot(PlotBase* plot) { + if (!plot) return false; + for (int i = 0; i < MAX_PLOTS; ++i) { + if (!_plots[i]) { + _plots[i] = plot; + plot->setParent(this); + plot->setDuration(_durationMs); + return true; + } + } + return false; // No available slot +} + +PlotBase* PlotBase::getPlot(uint8_t index) const { + if (index < MAX_PLOTS) { + return _plots[index]; + } + return nullptr; +} + +void PlotBase::onStart() { + for (int i = 0; i < MAX_PLOTS; ++i) { + if (_plots[i]) { + _plots[i]->start(); + } + } +} + +void PlotBase::onStop() { + for (int i = 0; i < MAX_PLOTS; ++i) { + if (_plots[i]) { + _plots[i]->stop(); + } + } +} + +void PlotBase::onPause() { + for (int i = 0; i < MAX_PLOTS; ++i) { + if (_plots[i]) { + _plots[i]->pause(); + } + } + if(getParent()) { + getParent()->pause(); + } +} + +void PlotBase::onResume() { + for (int i = 0; i < MAX_PLOTS; ++i) { + if (_plots[i]) { + _plots[i]->resume(); + } + } } \ No newline at end of file diff --git a/src/profiles/PlotBase.h b/src/profiles/PlotBase.h index 8db210c0..55e1a7b6 100644 --- a/src/profiles/PlotBase.h +++ b/src/profiles/PlotBase.h @@ -9,6 +9,8 @@ #include "config.h" #define PROFILE_SCALE 10000 +#define PROFILE_TIME_SCALE 1000 +#define MAX_PLOTS 1 //------------------------------------------------------------------------------ // Status Enum @@ -34,7 +36,13 @@ class PlotBase : public Component { // Ensure inheritance is active public: PlotBase(Component* owner, ushort componentId) : Component("PlotBase", componentId, Component::COMPONENT_DEFAULT, owner), - _durationMs(0), _startTimeMs(0), _elapsedMsAtPause(0), _running(false), _paused(false), _explicitlyStopped(false), _userData(nullptr) {} + _durationMs(0), _startTimeMs(0), _elapsedMsAtPause(0), _running(false), _paused(false), _explicitlyStopped(false), _userData(nullptr), + _parent(nullptr) { + for (int i = 0; i < MAX_PLOTS; ++i) { + _plots[i] = nullptr; + } + } + virtual ~PlotBase() = default; /** * @brief Loads configuration from a JSON object. @@ -82,6 +90,10 @@ public: */ virtual void seek(uint32_t targetMs); + // --- Sub-Plot Management --- + bool addPlot(PlotBase* plot); + PlotBase* getPlot(uint8_t index) const; + /** * @brief Checks if the plot is currently running (actively progressing). * @return true if running, false otherwise. @@ -100,6 +112,12 @@ public: */ uint32_t getDuration() const { return _durationMs; } + /** + * @brief Sets the total duration of the plot. + * @param durationMs The new duration in milliseconds. + */ + virtual void setDuration(uint32_t durationMs) { _durationMs = 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. @@ -160,10 +178,16 @@ public: */ uint32_t getElapsedMs() const; + PlotBase* getParent() const { return _parent; } + void setParent(PlotBase* parent) { _parent = parent; } + + protected: - - - + virtual void onStart(); + virtual void onStop(); + virtual void onPause(); + virtual void onResume(); + uint32_t _durationMs; uint32_t _startTimeMs; uint32_t _elapsedMsAtPause; // Stores elapsed time when pause() is called @@ -171,6 +195,9 @@ protected: bool _paused; // True if pause() called while running bool _explicitlyStopped; // True if stop() was called and not superseded by start() void* _userData; // Pointer for arbitrary user data + + PlotBase* _plots[MAX_PLOTS]; + PlotBase* _parent; }; #endif // PLOT_BASE_H \ No newline at end of file diff --git a/src/profiles/SignalPlot.cpp b/src/profiles/SignalPlot.cpp index 89056154..32558580 100644 --- a/src/profiles/SignalPlot.cpp +++ b/src/profiles/SignalPlot.cpp @@ -8,6 +8,7 @@ #include "Arduino.h" // For pinMode, digitalWrite, analogWrite #include "CommandMessage.h" // For CommandMessage #include "Bridge.h" // For Bridge component +#include "PHApp.h" // For PHApp component SignalPlot::SignalPlot(Component* owner, ushort slot, ushort componentId) : PlotBase(owner, componentId), @@ -54,19 +55,12 @@ short SignalPlot::loop() { if (!isRunning() || !enabled() || modbusTCP == nullptr || modbusTCP->modbusServer == nullptr) { return E_OK; } - uint32_t currentElapsedMs = getElapsedMs(); - // Duration is now in _durationMs from PlotBase, loaded in milliseconds - + uint32_t now = millis(); for (uint8_t i = 0; i < _numControlPoints; ++i) { S_SignalControlPoint& cp = _controlPoints[i]; - // Calculate the absolute trigger time in milliseconds for this control point - // cp.time is scaled 0-PROFILE_SCALE (e.g., 0-1000) - uint32_t absoluteTriggerMs = static_cast(((uint64_t)cp.time * (uint64_t)getDuration()) / PROFILE_SCALE); - // Check if the control point is due and hasn't been processed yet + uint32_t absoluteTriggerMs = static_cast(((uint64_t)cp.time * (uint64_t)getDuration()) / 1000); if (cp.state == E_SIGNAL_STATE::STATE_NONE && currentElapsedMs >= absoluteTriggerMs) { - Log.verboseln("%s: Executing CP id %d (scaled time: %lu, abs time: %lu ms) at plot elapsed: %lu ms", - name.c_str(), cp.id, cp.time, absoluteTriggerMs, currentElapsedMs); executeControlPointAction(i); } } @@ -76,7 +70,7 @@ short SignalPlot::loop() { bool SignalPlot::load(const JsonObject& config) { // Load name if present - if (config.containsKey("name") && config["name"].is()) { + if (config["name"].is()) { this->name = config["name"].as(); Log.verboseln("%s: Loaded name from config: %s", this->name.c_str(), this->name.c_str()); } else { @@ -85,23 +79,18 @@ bool SignalPlot::load(const JsonObject& config) { } // Load duration if present (expected in milliseconds from JSON) - if (config.containsKey("duration") && config["duration"].is()) { + if (config["duration"].is()) { this->_durationMs = config["duration"].as(); // Assign directly as ms - Log.verboseln("%s: Loaded duration from config: %lu ms", this->name.c_str(), this->_durationMs); - } else { - Log.warningln("%s: 'duration' not found in config or not uint32. Current duration: %lu ms", this->name.c_str(), this->_durationMs); - // Keep existing _durationMs (could be 0 from PlotBase constructor, or previously set) } - + // Slot is set by constructor and typically not overridden by JSON load(). - _numControlPoints = 0; // Reset count before loading new points for (int i = 0; i < MAX_SIGNAL_POINTS; ++i) { _controlPoints[i] = {}; // Clear existing points } - if (!config.containsKey("controlPoints") || !config["controlPoints"].is()) { + if (!config["controlPoints"].is()) { Log.warningln("%s: 'controlPoints' array not found or not an array in config. No points loaded.", name.c_str()); _numControlPoints = 0; return false; // No points to load is considered a failure for a plot that needs points. @@ -129,7 +118,7 @@ bool SignalPlot::load(const JsonObject& config) { _controlPoints[i].arg_0 = pointObj["arg_0"].as(); // Always load arg_1 directly from JSON. // JSON must provide the correct value (e.g., 0 or 1 for MB_WRITE_COIL). - if (pointObj.containsKey("arg_1")) { + if (pointObj["arg_1"].is()) { _controlPoints[i].arg_1 = pointObj["arg_1"].as(); } else { _controlPoints[i].arg_1 = 0; // Default if not present, or log warning @@ -137,20 +126,20 @@ bool SignalPlot::load(const JsonObject& config) { name.c_str(), _controlPoints[i].id, static_cast(_controlPoints[i].type)); } - if (pointObj.containsKey("arg_2")) { + if (pointObj["arg_2"].is()) { _controlPoints[i].arg_2 = pointObj["arg_2"].as(); } else { _controlPoints[i].arg_2 = 0; // Default if not present } // user field logic remains as is (currently skipped for assignment) - if (pointObj.containsKey("user")) { + if (!pointObj["user"].isNull()) { // ... user field handling ... } // Load the actual execution state for this point, if provided in JSON. // This is distinct from the coil value that might have been in jsonState before. - if (pointObj.containsKey("state")) { + if (pointObj["state"].is()) { _controlPoints[i].state = static_cast(pointObj["state"].as()); } else { _controlPoints[i].state = E_SIGNAL_STATE::STATE_NONE; // Default initial state for execution tracking @@ -167,7 +156,8 @@ const S_SignalControlPoint* SignalPlot::findActivePoint(uint32_t elapsedMs) cons // 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) { + uint32_t pointTimeMs = static_cast(((uint64_t)_controlPoints[i].time * (uint64_t)getDuration()) / 1000); + if (pointTimeMs <= elapsedMs) { // Found the latest point at or before the elapsed time lastApplicablePoint = &_controlPoints[i]; break; // Since sorted, this is the correct one @@ -218,7 +208,7 @@ bool SignalPlot::getCurrentControlPointInfo(uint8_t& outId, uint32_t& outTimeMs, if (activePoint != nullptr) { outId = activePoint->id; - outTimeMs = activePoint->time; + outTimeMs = static_cast(((uint64_t)activePoint->time * (uint64_t)getDuration()) / 1000); outValue = static_cast(activePoint->state); // Cast enum state to int16_t outUser = *((int16_t*)activePoint->user); return true; @@ -338,7 +328,6 @@ short SignalPlot::mb_tcp_write(MB_Registers *reg, short value) { void SignalPlot::start() { PlotBase::start(); - // Reset execution state for all control points when the plot starts for (uint8_t i = 0; i < _numControlPoints; ++i) { _controlPoints[i].state = E_SIGNAL_STATE::STATE_NONE; } @@ -350,9 +339,7 @@ void SignalPlot::executeControlPointAction(uint8_t cpIndex) { Log.errorln("%s: Invalid control point index %d in executeControlPointAction.", name.c_str(), cpIndex); return; } - S_SignalControlPoint& cp = _controlPoints[cpIndex]; - switch (cp.type) { case E_SIGNAL_TYPE::MB_WRITE_COIL: { if (modbusTCP == nullptr || modbusTCP->modbusServer == nullptr) { @@ -419,7 +406,6 @@ void SignalPlot::executeControlPointAction(uint8_t cpIndex) { String cmdStr = String((char*)cp.user); Log.infoln("%s: CP id %d: CALL_METHOD executing: %s", name.c_str(), cp.id, cmdStr.c_str()); - Bridge* bridge = static_cast(owner->byId(COMPONENT_KEY_MB_BRIDGE)); if (!bridge) { Log.errorln("%s: CP id %d: Bridge component not found.", name.c_str(), cp.id); @@ -444,6 +430,31 @@ void SignalPlot::executeControlPointAction(uint8_t cpIndex) { } break; } + case E_SIGNAL_TYPE::DISPLAY_MESSAGE: { + String message = cp.description; + if (message.isEmpty()) { + message = cp.name; + } + PHApp* phApp = static_cast(owner); + if (phApp) { + JsonDocument doc; + doc["message"] = message; + doc["id"] = cp.id; + doc["type"] = "display_message"; + phApp->broadcast(BROADCAST_USER_MESSAGE, doc); + }else{ + Log.errorln("%s: CP id %d: DISPLAY_MESSAGE: PHApp component not found.", name.c_str(), cp.id); + } + Log.infoln("%s: CP id %d: DISPLAY_MESSAGE: %s", name.c_str(), cp.id, message.c_str()); + cp.state = E_SIGNAL_STATE::STATE_ON; + break; + } + case E_SIGNAL_TYPE::PAUSE: { + Log.infoln("%s: CP id %d: PAUSE", name.c_str(), cp.id); + pause(); + cp.state = E_SIGNAL_STATE::STATE_ON; + break; + } case E_SIGNAL_TYPE::MB_WRITE_HOLDING_REGISTER: Log.warningln("%s: CP id %d: MB_WRITE_HOLDING_REGISTER not yet fully implemented in loop.", name.c_str(), cp.id); cp.state = E_SIGNAL_STATE::STATE_ERROR; diff --git a/src/profiles/SignalPlot.h b/src/profiles/SignalPlot.h index 34fc6c21..fbc13ca4 100644 --- a/src/profiles/SignalPlot.h +++ b/src/profiles/SignalPlot.h @@ -40,7 +40,9 @@ enum class E_SIGNAL_TYPE : int16_t CALL_FUNCTION = 4, CALL_REST = 5, GPIO_WRITE = 6, - USER_DEFINED = 7 + DISPLAY_MESSAGE = 7, + USER_DEFINED = 8, + PAUSE = 9 }; // New enum for GPIO Write Modes @@ -77,6 +79,8 @@ struct S_SignalControlPoint * - CALL_FUNCTION: Function ID (ushort) // TODO: not implemented yet * - CALL_REST: Not used directly, path/params likely in name/description or separate storage. // TODO: not implemented yet * - USER_DEFINED: User-specific. // TODO: not implemented yet + * - DISPLAY_MESSAGE: Not currently used. + * - PAUSE: Not used. */ int16_t arg_0; @@ -88,6 +92,8 @@ struct S_SignalControlPoint * - CALL_METHOD: Method index/ID. // TODO: not implemented yet * - CALL_FUNCTION: Not used typically, or first param. // TODO: not implemented yet * - USER_DEFINED: User-specific. // TODO: not implemented yet + * - DISPLAY_MESSAGE: Not currently used. + * - PAUSE: Not used. */ int16_t arg_1; @@ -97,6 +103,8 @@ struct S_SignalControlPoint * - CALL_METHOD: First method parameter (if any). // TODO: not implemented yet * - CALL_FUNCTION: Second param / etc. // TODO: not implemented yet * - USER_DEFINED: User-specific. // TODO: not implemented yet + * - DISPLAY_MESSAGE: Not currently used. + * - PAUSE: Not used. * - Others: Not used (typically). */ int16_t arg_2; @@ -200,6 +208,7 @@ private: ModbusTCP *modbusTCP; MB_Registers _modbusBlocks[SIGNAL_PLOT_REGISTER_COUNT]; ModbusBlockView _modbusBlockView; + uint32_t _lastLogMs = 0; }; #endif // SIGNAL_PLOT_H \ No newline at end of file diff --git a/src/profiles/TemperatureProfile.cpp b/src/profiles/TemperatureProfile.cpp index 5f5d540c..e7675b94 100644 --- a/src/profiles/TemperatureProfile.cpp +++ b/src/profiles/TemperatureProfile.cpp @@ -277,6 +277,11 @@ bool TemperatureProfile::load(const JsonObject &config) { uint32_t duration_s = config["duration"].as(); _durationMs = duration_s; + for (int i = 0; i < MAX_PLOTS; ++i) { + if (_plots[i]) { + _plots[i]->setDuration(_durationMs); + } + } } else { @@ -354,7 +359,7 @@ bool TemperatureProfile::load(const JsonObject &config) Log.infoln("%s: Loaded %d target registers.", name.c_str(), _targetRegisters.size()); // Load associated signal plot slot ID if present - if (config.containsKey("signalPlot") && config["signalPlot"].is()) + if (config["signalPlot"].is()) { _signalPlotSlotId = config["signalPlot"].as(); Log.verboseln("%s: Loaded signalPlot: %d", name.c_str(), _signalPlotSlotId); @@ -761,63 +766,23 @@ uint16_t TemperatureProfile::getCount() const return count; } -void TemperatureProfile::start() -{ - PlotBase::start(); - if (_signalPlotSlotId >= 0 && owner != nullptr) - { - PHApp* app = static_cast(owner); - app->startSignalPlot(_signalPlotSlotId); - } -} - -void TemperatureProfile::stop() -{ - PlotBase::stop(); - if (_signalPlotSlotId >= 0 && owner != nullptr) - { - PHApp* app = static_cast(owner); - app->stopSignalPlot(_signalPlotSlotId); - } -} - void TemperatureProfile::enable() { PlotBase::enable(); - if (_signalPlotSlotId >= 0 && owner != nullptr) - { - PHApp* app = static_cast(owner); - app->enableSignalPlot(_signalPlotSlotId, true); + for (int i = 0; i < MAX_PLOTS; ++i) { + if (_plots[i]) { + _plots[i]->enable(); + } } } void TemperatureProfile::disable() { PlotBase::disable(); - if (_signalPlotSlotId >= 0 && owner != nullptr) - { - PHApp* app = static_cast(owner); - app->enableSignalPlot(_signalPlotSlotId, false); - } -} - -void TemperatureProfile::pause() -{ - PlotBase::pause(); - if (_signalPlotSlotId >= 0 && owner != nullptr) - { - PHApp* app = static_cast(owner); - app->pauseSignalPlot(_signalPlotSlotId); - } -} - -void TemperatureProfile::resume() -{ - PlotBase::resume(); - if (_signalPlotSlotId >= 0 && owner != nullptr) - { - PHApp* app = static_cast(owner); - app->resumeSignalPlot(_signalPlotSlotId); + for (int i = 0; i < MAX_PLOTS; ++i) { + if (_plots[i]) { + _plots[i]->disable(); + } } } diff --git a/src/profiles/TemperatureProfile.h b/src/profiles/TemperatureProfile.h index 5b801e6d..9667710f 100644 --- a/src/profiles/TemperatureProfile.h +++ b/src/profiles/TemperatureProfile.h @@ -1,9 +1,4 @@ -/** - * TemperatureProfile.h - * Revision: initial documentation - */ - -#nfndef TEMPERATURE_PROFILE_H +#ifndef TEMPERATURE_PROFILE_H #define TEMPERATURE_PROFILE_H #include "PlotBase.h" @@ -13,7 +8,7 @@ #include #include #include -#include "../ValueWrapper.h" // Include the updated ValueWrapper +#include #include class ModbusTCP; @@ -71,13 +66,8 @@ public: short setup() override; short loop() override; - // Overriding PlotBase methods to integrate SignalPlot control - void start() override; - void stop() override; void enable(); void disable(); - void pause() override; - void resume() override; // --- Profile Specific Methods --- /** @@ -222,4 +212,4 @@ private: void _initializeControlPoints(); }; -#endif // TEMPERATURE_PROFILE_H \ No newline at end of file +#endif // TEMPERATURE_PROFILE_H \ No newline at end of file