polymech - fw latest | web ui

This commit is contained in:
2026-04-18 10:31:24 +02:00
parent a105c5ee85
commit ab2ff368a6
2972 changed files with 441416 additions and 372 deletions
+417
View File
@@ -0,0 +1,417 @@
// Include config first to get the feature flag
#include "config.h"
#ifdef ENABLE_MB_SCRIPT
#include "ModbusLogicEngine.h"
#include "PHApp.h" // Include PHApp to access other components/methods
#include "ModbusTCP.h" // To potentially interact with Modbus directly if needed
#include "config-modbus.h" // Assuming constants like MODBUS_LOGIC_RULES_START are here
#include "enums.h" // For error codes like E_OK, component states etc.
#include <Arduino.h> // For millis()
#include <cmath> // For isnan, isinf if needed
#include <cstring> // For strncpy
#include <ArduinoLog.h>
// Placeholder for Component ID - Define this properly elsewhere (e.g., enums.h)
#ifndef COMPONENT_ID_MLE
#define COMPONENT_ID_MLE 99
#endif
// --- Constructor ---
ModbusLogicEngine::ModbusLogicEngine(PHApp* ownerApp)
// Using the most complete constructor signature found in error logs
// Assuming 0 for default flags
: Component("LogicEngine", COMPONENT_ID_MLE, 0, ownerApp), app(ownerApp), initialized_(false) { // Initialize internal state flag
// Name, ID, Flags, Owner set in base initializer list
}
// --- Setup ---
short ModbusLogicEngine::setup() {
Log.infoln(F("MLE: Initializing Logic Engine..."));
rules.resize(MAX_LOGIC_RULES); // Allocate space for all rules
// Optional: Load rules from persistent storage if implemented
Log.infoln(F("MLE: Initialized %d rules."), MAX_LOGIC_RULES);
// Set internal initialization flag
initialized_ = true;
return E_OK;
}
// --- Loop (Rule Evaluation) ---
short ModbusLogicEngine::loop() {
if (!initialized_) {
return E_OK; // Not ready to run
}
unsigned long currentTime = millis();
if (currentTime - lastLoopTime < loopInterval) {
return E_OK; // Not time to evaluate yet
}
lastLoopTime = currentTime;
//Log.verboseln(F("MLE: Evaluating rules..."));
for (int i = 0; i < rules.size(); ++i) {
LogicRule& rule = rules[i];
if (!rule.isEnabled()) {
continue; // Skip disabled rules
}
bool conditionMet = false;
bool conditionEvalSuccess = evaluateCondition(rule);
if (!conditionEvalSuccess) {
// Error already logged and status updated in evaluateCondition
if(rule.isDebugEnabled()) Log.verboseln(F("MLE: Rule %d condition eval FAILED."), i);
continue; // Move to the next rule
}
conditionMet = conditionEvalSuccess; // If evaluation didn't fail, the result is stored
if (conditionMet) {
if(rule.isDebugEnabled()) Log.verboseln(F("MLE: Rule %d condition MET."), i);
bool actionSuccess = performAction(rule);
if (actionSuccess) {
// Status updated in performAction
// Timestamp and counter are also updated in performAction
if(rule.isReceiptEnabled()) Log.infoln(F("MLE: Rule %d action successful."), i);
} else {
// Error logged and status updated in performAction
if(rule.isDebugEnabled()) Log.warningln(F("MLE: Rule %d action FAILED."), i);
}
} else {
// Condition not met
if(rule.isDebugEnabled()) Log.verboseln(F("MLE: Rule %d condition NOT met."), i);
// Ensure status is reset to OK/Idle if it wasn't an error
if (rule.lastStatus != RuleStatus::IllegalDataAddress &&
rule.lastStatus != RuleStatus::IllegalDataValue) {
updateRuleStatus(rule, RuleStatus::Success);
}
}
} // End for each rule
return E_OK;
}
// --- Evaluate Condition ---
bool ModbusLogicEngine::evaluateCondition(LogicRule& rule) {
uint16_t currentValue = 0;
uint16_t targetValue = rule.getCondValue();
RegisterState::E_RegType sourceType = rule.getCondSourceType();
uint16_t sourceAddr = rule.getCondSourceAddr();
ConditionOperator op = rule.getCondOperator();
// Read the source value
bool readSuccess = readConditionSourceValue(sourceType, sourceAddr, currentValue);
if (!readSuccess) {
Log.warningln(F("MLE: Failed to read condition source (Type: %d, Addr: %u)"), (int)sourceType, sourceAddr);
updateRuleStatus(rule, RuleStatus::IllegalDataAddress);
return false; // Indicate evaluation failure
}
if (rule.isDebugEnabled()) {
Log.verboseln(F("MLE Eval [%d]: SrcType=%d, SrcAddr=%u, Op=%d, Target=%u, Current=%u"),
&rule - &rules[0], // Crude way to get index for logging
(int)sourceType, sourceAddr, (int)op, targetValue, currentValue);
}
// Perform comparison
bool result = false;
switch (op) {
case ConditionOperator::EQUAL: result = (currentValue == targetValue); break;
case ConditionOperator::NOT_EQUAL: result = (currentValue != targetValue); break;
case ConditionOperator::LESS_THAN: result = (currentValue < targetValue); break;
case ConditionOperator::LESS_EQUAL: result = (currentValue <= targetValue); break;
case ConditionOperator::GREATER_THAN: result = (currentValue > targetValue); break;
case ConditionOperator::GREATER_EQUAL: result = (currentValue >= targetValue); break;
default:
Log.warningln(F("MLE: Invalid condition operator (%d)"), (int)op);
updateRuleStatus(rule, RuleStatus::IllegalDataValue);
return false; // Indicate evaluation failure
}
// If we got here, evaluation itself succeeded, return the comparison result
// Status will be updated later based on whether action is performed
return result;
}
// --- Perform Action (Renamed Command) ---
bool ModbusLogicEngine::performAction(LogicRule& rule) { // Renamed function
CommandType commandType = rule.getCommandType(); // Renamed enum and accessor
uint16_t target = rule.getCommandTarget(); // Renamed accessor
uint16_t param1 = rule.getCommandParam1(); // Renamed accessor
uint16_t param2 = rule.getCommandParam2(); // Renamed accessor
bool success = false;
if (rule.isDebugEnabled()) {
Log.verboseln(F("MLE Action [%d]: CmdType=%d, Target=%u, P1=%u, P2=%u"),
&rule - &rules[0], // Crude index for logging
(int)commandType, target, param1, param2);
}
switch (commandType) {
case CommandType::NONE:
success = true; // No action is considered success
break;
case CommandType::WRITE_HOLDING_REGISTER:
case CommandType::WRITE_COIL:
success = performWriteAction(commandType, target, param1);
if (!success) updateRuleStatus(rule, RuleStatus::ServerDeviceFailure);
break;
case CommandType::CALL_COMPONENT_METHOD:
success = performCallAction(target, param1, 0);
if (!success) updateRuleStatus(rule, RuleStatus::OpExecutionFailed);
break;
default:
Log.warningln(F("MLE: Invalid command type (%d)"), (int)commandType);
updateRuleStatus(rule, RuleStatus::IllegalFunction);
success = false;
break;
}
// Update timestamp and counter only on successful action execution
if (success) {
updateRuleStatus(rule, RuleStatus::Success);
rule.lastTriggerTimestamp = millis() / 1000; // Store seconds since boot
rule.triggerCount++;
}
return success;
}
// --- Read Condition Source Value ---
bool ModbusLogicEngine::readConditionSourceValue(RegisterState::E_RegType type, uint16_t address, uint16_t& value) {
if (!app || !app->modbusManager) {
Log.errorln(F("MLE: ModbusManager not available!"));
return false;
}
// Find the component responsible for this address
Component* targetComponent = app->modbusManager->findComponentForAddress(address);
if (!targetComponent) {
Log.warningln(F("MLE: No component found for read address %u"), address);
// Optionally: Check if it's a global app address?
if (app->modbusManager->findComponentForAddress(address) == static_cast<Component*>(app)){
targetComponent = static_cast<Component*>(app);
} else {
return false;
}
}
short result = -1;
switch (type) {
case RegisterState::E_RegType::REG_HOLDING:
case RegisterState::E_RegType::REG_INPUT:
result = targetComponent->mb_tcp_read(address);
break;
case RegisterState::E_RegType::REG_COIL:
case RegisterState::E_RegType::REG_DISCRETE_INPUT:
result = targetComponent->mb_tcp_read(address);
break;
default:
Log.warningln(F("MLE: Unsupported condition source type: %d"), (int)type);
return false;
}
if (result == E_INVALID_PARAMETER || result < -1) {
Log.warningln(F("MLE: Read failed for address %u (Result: %d)"), address, result);
return false;
}
// Handle potential E_NOT_IMPLEMENTED or other non-value returns if necessary
if (result == E_NOT_IMPLEMENTED) {
Log.warningln(F("MLE: Read not implemented for address %u"), address);
return false;
}
value = (uint16_t)result;
Log.verboseln(F("MLE: Read condition value %u from address %u"), value, address);
return true;
}
// --- Perform Write Action ---
bool ModbusLogicEngine::performWriteAction(CommandType type, uint16_t address, uint16_t value) {
if (!app || !app->modbusManager) {
Log.errorln(F("MLE: ModbusManager not available!"));
return false;
}
// Find the component responsible for this address
Component* targetComponent = app->modbusManager->findComponentForAddress(address);
if (!targetComponent) {
Log.warningln(F("MLE: No component found for write address %u"), address);
// Optionally: Check if it's a global app address?
if (app->modbusManager->findComponentForAddress(address) == static_cast<Component*>(app)){
targetComponent = static_cast<Component*>(app);
} else {
return false;
}
}
Log.verboseln(F("MLE: Writing value %u to address %u (Type: %d)"), value, address, (int)type);
// Assuming mb_tcp_write handles both Registers and Coils
// It should return E_OK on success
short result = targetComponent->mb_tcp_write(address, value);
if (result != E_OK) {
Log.warningln(F("MLE: Write failed for address %u, value %u (Result: %d)"), address, value, result);
return false;
}
return true;
}
// --- Perform Call Action ---
bool ModbusLogicEngine::performCallAction(uint16_t componentId, uint16_t methodId, uint16_t arg1) {
uint32_t combinedId = ((uint32_t)componentId << 16) | methodId;
auto it = callableMethods.find(combinedId);
// Find the rule being processed - This requires passing the rule or index down
int ruleIndex = -1; // Placeholder index
// Logic to determine the correct ruleIndex based on loop context is needed here.
// For now, just check if index is valid before accessing rules vector.
if (ruleIndex < 0 || ruleIndex >= rules.size()) {
Log.errorln(F("MLE: Invalid rule context in performCallAction!"));
// Cannot update status without rule context
return false;
}
LogicRule& rule = rules[ruleIndex]; // Now safe to access
if (it == callableMethods.end()) {
Log.warningln(F("MLE: Method not registered (CompID: %u, MethodID: %u)"), componentId, methodId);
updateRuleStatus(rule, RuleStatus::IllegalDataAddress);
return false;
}
Log.verboseln(F("MLE: Calling method (CompID: %u, MethodID: %u) with arg (%d)"), componentId, methodId, (short)arg1);
// Pass only arg1 (param2 from rule) to the registered function, assuming it now takes only one argument
// OR pass arg1 and a dummy second argument if the registered function still expects two.
// Let's assume the registered CallableMethod now expects only one argument
// TODO: Confirm signature of registered CallableMethod
// short result = it->second((short)arg1, (short)arg2); // Old call
short result = it->second((short)arg1, 0); // Pass arg1 and a dummy 0 for now
if (result != E_OK) { // Check against generic E_OK from enums.h
Log.warningln(F("MLE: Method call failed (CompID: %u, MethodID: %u, Result: %d)"), componentId, methodId, result);
// Status is updated in performAction based on return
return false;
}
return true;
}
// --- Register Method ---
bool ModbusLogicEngine::registerMethod(uint16_t componentId, uint16_t methodId, CallableMethod method) {
uint32_t combinedId = ((uint32_t)componentId << 16) | methodId;
auto result = callableMethods.insert({combinedId, method});
if (result.second) {
Log.infoln(F("MLE: Registered method (CompID: %u, MethodID: %u)"), componentId, methodId);
} else {
Log.warningln(F("MLE: Failed to register duplicate method (CompID: %u, MethodID: %u)"), componentId, methodId);
}
return result.second; // Returns true if insertion took place
}
// --- Modbus Network Interface ---
// Helper to get rule index and offset from a Modbus address
bool ModbusLogicEngine::getRuleInfoFromAddress(short address, int& ruleIndex, short& offset) {
if (address < MODBUS_LOGIC_RULES_START) {
return false;
}
short relativeAddress = address - MODBUS_LOGIC_RULES_START;
ruleIndex = relativeAddress / LOGIC_ENGINE_REGISTERS_PER_RULE;
offset = relativeAddress % LOGIC_ENGINE_REGISTERS_PER_RULE;
if (ruleIndex < 0 || ruleIndex >= MAX_LOGIC_RULES) {
return false; // Address out of range
}
return true;
}
// Read Configuration/Status via Modbus
short ModbusLogicEngine::mb_tcp_read(short address) {
int ruleIndex = -1;
short offset = -1;
if (!getRuleInfoFromAddress(address, ruleIndex, offset)) {
return E_INVALID_PARAMETER;
}
const LogicRule& rule = rules[ruleIndex];
// Read configuration registers (Offset 0 to 8)
if (offset >= ModbusLogicEngineOffsets::ENABLED && offset <= ModbusLogicEngineOffsets::COMMAND_PARAM2) {
return rule.config[offset];
}
// Read status/flag registers (Offset 9 to 12)
else if (offset == ModbusLogicEngineOffsets::FLAGS) {
return rule.getFlags(); // Use getFlags() to read from config array
}
else if (offset == ModbusLogicEngineOffsets::LAST_STATUS) {
// Return MB_Error as short
return static_cast<short>(rule.lastStatus);
}
else if (offset == ModbusLogicEngineOffsets::LAST_TRIGGER_TS) {
// Timestamps might be 32-bit, Modbus registers are 16-bit.
// Return lower 16 bits for simplicity, or implement 32-bit reading.
return (short)(rule.lastTriggerTimestamp & 0xFFFF); // Return lower 16 bits
// TODO: Implement reading upper 16 bits at offset + 1 if needed
}
else if (offset == ModbusLogicEngineOffsets::TRIGGER_COUNT) {
return rule.triggerCount;
}
else {
return E_INVALID_PARAMETER;
}
}
// Write Configuration via Modbus
short ModbusLogicEngine::mb_tcp_write(short address, short value) {
int ruleIndex = -1;
short offset = -1;
if (!getRuleInfoFromAddress(address, ruleIndex, offset)) {
return E_INVALID_PARAMETER;
}
LogicRule& rule = rules[ruleIndex];
// Allow writing to configuration registers (Offset 0 to 9, including FLAGS)
if (offset >= ModbusLogicEngineOffsets::ENABLED && offset <= ModbusLogicEngineOffsets::FLAGS) {
Log.verboseln(F("MLE: Setting Rule %d, Offset %d to %d"), ruleIndex, offset, value);
rule.setConfigValue(offset, (uint16_t)value);
// Optional: Persist rule change here if implementing storage
return E_OK;
}
// Allow resetting trigger count (Offset 12)
else if (offset == ModbusLogicEngineOffsets::TRIGGER_COUNT && value == 0) {
Log.infoln(F("MLE: Resetting trigger count for Rule %d"), ruleIndex);
rule.triggerCount = 0;
return E_OK;
}
// Disallow writing to other status registers directly (Offsets 10, 11)
else if (offset == ModbusLogicEngineOffsets::LAST_STATUS || offset == ModbusLogicEngineOffsets::LAST_TRIGGER_TS) {
Log.warningln(F("MLE: Attempt to write read-only status register (Rule %d, Offset %d)"), ruleIndex, offset);
return E_INVALID_PARAMETER;
}
else {
return E_INVALID_PARAMETER;
}
}
// --- Helper to update status (and log changes) ---
void ModbusLogicEngine::updateRuleStatus(LogicRule& rule, RuleStatus newStatus) {
if (rule.lastStatus != newStatus) {
Log.verboseln(F("MLE: Rule Status changing from %d to %d"), static_cast<uint8_t>(rule.lastStatus), static_cast<uint8_t>(newStatus));
rule.lastStatus = newStatus;
// Optional: Log specific error messages based on the status code
}
}
#endif // ENABLE_MB_SCRIPT
+195
View File
@@ -0,0 +1,195 @@
#ifndef MODBUS_LOGIC_ENGINE_H
#define MODBUS_LOGIC_ENGINE_H
#include "config.h"
#include <Component.h>
#include <ArduinoLog.h>
#include <vector>
#include <cstdint> // For uint16_t etc.
#include <modbus/ModbusTypes.h>
// Forward declarations
class PHApp; // Assuming PHApp provides access to other components/Modbus
// --- Configuration Constants (Define these in config-modbus.h or similar) ---
#ifndef MAX_LOGIC_RULES
#define MAX_LOGIC_RULES 8 // Default number of rules
#endif
#ifndef LOGIC_ENGINE_REGISTERS_PER_RULE
#define LOGIC_ENGINE_REGISTERS_PER_RULE 13 // Now 13 (removed Param3)
#endif
#ifndef MODBUS_LOGIC_RULES_START
#define MODBUS_LOGIC_RULES_START 1000 // Example starting address
#endif
// --- End Configuration Constants ---
// Define constants for rule structure offsets (matching mb-lang.md)
namespace ModbusLogicEngineOffsets {
// --- Configuration (8 registers) ---
const short ENABLED = 0;
const short COND_SRC_TYPE = 1;
const short COND_SRC_ADDR = 2;
const short COND_OPERATOR = 3;
const short COND_VALUE = 4;
const short COMMAND_TYPE = 5;
const short COMMAND_TARGET = 6;
// Parameters below depend on COMMAND_TYPE:
// - WRITE_HOLDING_REGISTER: PARAM1=Value
// - WRITE_COIL: PARAM1=Value (0 or 1)
// - CALL_COMPONENT_METHOD: PARAM1=MethodID, PARAM2=Arg1
const short COMMAND_PARAM1 = 7;
const short COMMAND_PARAM2 = 8;
// Removed PARAM3
// --- Status & Flags (Moved to end - 4 registers) ---
const short FLAGS = 9; // Moved
const short LAST_STATUS = 10; // Moved
const short LAST_TRIGGER_TS = 11; // Moved
const short TRIGGER_COUNT = 12; // Moved
}
// --- Rule Flags (Bitmasks for FLAGS register) ---
#define RULE_FLAG_DEBUG (1 << 0) // Enable verbose debug logging for this rule
#define RULE_FLAG_RECEIPT (1 << 1) // Enable logging upon successful trigger/action
// Define constants for condition operators
enum class ConditionOperator : uint16_t {
EQUAL = 0,
NOT_EQUAL = 1,
LESS_THAN = 2,
LESS_EQUAL = 3,
GREATER_THAN = 4,
GREATER_EQUAL = 5
};
// Define constants for command types
// Partially aligned with E_MB_OpType from ModbusTypes.h
enum class CommandType : uint16_t {
NONE = 0,
// Use standard op types where possible (Values from E_MB_OpType)
WRITE_COIL = 2, // Matches E_MB_OpType::MB_WRITE_COIL
WRITE_HOLDING_REGISTER = 3, // Matches E_MB_OpType::MB_WRITE_REGISTER
// Custom command type (value > standard op types)
CALL_COMPONENT_METHOD = 100
};
// Type alias for rule status using standard Modbus errors
using RuleStatus = MB_Error;
// Structure to hold the configuration and state of a single rule
struct LogicRule {
// --- Configuration (Set via Modbus) ---
// Store config registers directly (Enabled to Param2)
uint16_t config[LOGIC_ENGINE_REGISTERS_PER_RULE - 4]; // Size now 13-4 = 9
// Helper accessors for configuration
bool isEnabled() const { return config[ModbusLogicEngineOffsets::ENABLED] == 1; }
RegisterState::E_RegType getCondSourceType() const { return static_cast<RegisterState::E_RegType>(config[ModbusLogicEngineOffsets::COND_SRC_TYPE]); }
uint16_t getCondSourceAddr() const { return config[ModbusLogicEngineOffsets::COND_SRC_ADDR]; }
ConditionOperator getCondOperator() const { return static_cast<ConditionOperator>(config[ModbusLogicEngineOffsets::COND_OPERATOR]); }
uint16_t getCondValue() const { return config[ModbusLogicEngineOffsets::COND_VALUE]; }
CommandType getCommandType() const { return static_cast<CommandType>(config[ModbusLogicEngineOffsets::COMMAND_TYPE]); }
uint16_t getCommandTarget() const { return config[ModbusLogicEngineOffsets::COMMAND_TARGET]; }
uint16_t getCommandParam1() const { return config[ModbusLogicEngineOffsets::COMMAND_PARAM1]; }
uint16_t getCommandParam2() const { return config[ModbusLogicEngineOffsets::COMMAND_PARAM2]; }
// --- Status & Flags (Read/Write via Modbus, updated internally) ---
// Note: These are conceptually separate but stored contiguously in Modbus address space.
// The internal representation uses separate members for status/timestamp/count
// and reads/writes the FLAGS register (config[9]) via Modbus.
uint16_t getFlags() const { return config[ModbusLogicEngineOffsets::FLAGS]; }
bool isDebugEnabled() const { return (getFlags() & RULE_FLAG_DEBUG) != 0; }
bool isReceiptEnabled() const { return (getFlags() & RULE_FLAG_RECEIPT) != 0; }
void setConfigValue(short offset, uint16_t value) {
// Size check updated for new register count (9 config registers)
if (offset >= 0 && offset < (LOGIC_ENGINE_REGISTERS_PER_RULE - 4)) {
config[offset] = value;
} else {
Log.warningln(F("MLE: Attempt to write invalid config offset %d"), offset);
}
}
// Internal state members (not directly part of config array)
RuleStatus lastStatus = RuleStatus::Success;
uint32_t lastTriggerTimestamp = 0;
uint16_t triggerCount = 0;
LogicRule() {
// Initialize configuration registers (0-8)
for(int i = 0; i < (LOGIC_ENGINE_REGISTERS_PER_RULE - 4); ++i) {
config[i] = 0;
}
}
};
class ModbusLogicEngine : public Component {
public:
// Define the callable method signature: takes two shorts, returns a short (e.g., error code)
// Using std::function allows storing lambdas, member functions, etc.
// NOTE: This internal registration is used for CALL_COMPONENT_METHOD.
// It is separate from the Bridge mechanism used for serial commands.
using CallableMethod = std::function<short(short, short)>;
// Constructor - Requires access to PHApp to interact with other components/Modbus
ModbusLogicEngine(PHApp* ownerApp); // Pass PHApp reference
virtual ~ModbusLogicEngine() = default;
short setup() override;
short loop() override;
// Handle Modbus reads/writes for rule configuration and status
short mb_tcp_read(short address) override;
short mb_tcp_write(short address, short value) override;
/**
* @brief Registers a method that can be called by the logic engine.
*
* @param componentId A unique ID for the component exposing the method.
* @param methodId A unique ID for the method within that component.
* @param method The function object (lambda, bound member function) to call.
* @return True if registration was successful, false otherwise (e.g., duplicate ID).
*/
bool registerMethod(uint16_t componentId, uint16_t methodId, CallableMethod method);
private:
PHApp* app; // Pointer to the main application instance
bool initialized_; // Flag to track initialization state
// Storage for all logic rules
std::vector<LogicRule> rules;
// Registry for callable methods: Map<(ComponentID << 16) | MethodID, Function>
// Combining IDs into a single 32-bit key for the map.
std::map<uint32_t, CallableMethod> callableMethods;
// --- Internal Logic ---
unsigned long lastLoopTime = 0;
const unsigned long loopInterval = 100; // Evaluate rules every 100ms (adjust as needed)
bool evaluateCondition(LogicRule& rule);
bool performAction(LogicRule& rule);
// --- Helper Methods ---
// Reads the value required for a rule's condition
// Updated signature to use RegisterState::E_RegType
bool readConditionSourceValue(RegisterState::E_RegType type, uint16_t address, uint16_t& value);
// Performs the write action (Register or Coil)
// Updated signature to use CommandType
bool performWriteAction(CommandType type, uint16_t address, uint16_t value);
// Performs the method call action (now only 2 params)
bool performCallAction(uint16_t componentId, uint16_t methodId, uint16_t arg1);
// Helper to calculate rule index and offset from Modbus address
bool getRuleInfoFromAddress(short address, int& ruleIndex, short& offset);
// Helper to update status fields
// Updated signature to use RuleStatus (MB_Error)
void updateRuleStatus(LogicRule& rule, RuleStatus status);
};
#endif // MODBUS_LOGIC_ENGINE_H
+107
View File
@@ -0,0 +1,107 @@
#include "PHApp.h"
#ifdef ENABLE_MODBUS_TCP
#include <modbus/ModbusTCP.h> // For ModbusManager class
#include <ArduinoLog.h> // For logging
#include <enums.h> // For error codes like E_INVALID_PARAMETER
#include <ModbusServerTCPasync.h> // Include for ModbusServerTCPasync
#include <enums.h>
#include <modbus/ModbusTypes.h>
#include "config-modbus.h" // Include centralized addresses (defines MODBUS_PORT)
extern ModbusServerTCPasync mb;
void PHApp::mb_tcp_register(ModbusTCP *manager) const
{
if (!hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS))
return;
ModbusBlockView *blocksView = mb_tcp_blocks();
Component *thiz = const_cast<PHApp *>(this);
for (int i = 0; i < blocksView->count; ++i)
{
MB_Registers info = blocksView->data[i];
info.componentId = this->id;
manager->registerModbus(thiz, info);
}
}
ModbusBlockView *PHApp::mb_tcp_blocks() const
{
static const MB_Registers kBlocks[] = {
{MB_ADDR_SYSTEM_ERROR, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, static_cast<ushort>(this->id), 0, "PHApp: System Error","PHApp"},
{MB_ADDR_APP_STATE, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_WRITE, static_cast<ushort>(this->id), 0, "PHApp: App State","PHApp"},
{MB_ADDR_RESET_CONTROLLER, 1, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_READ_WRITE, static_cast<ushort>(this->id), 0, "PHApp: Reset Controller","PHApp"},
{MB_ADDR_ECHO_TEST, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_WRITE, static_cast<ushort>(this->id), 0, "PHApp: Echo Test","PHApp"},
};
static ModbusBlockView blockView = {kBlocks, int(sizeof(kBlocks) / sizeof(kBlocks[0]))};
return &blockView;
}
/**
* @brief Handles Modbus read requests for PHApp's specific registers.
*/
short PHApp::mb_tcp_read(short address)
{
switch (address)
{
case MB_ADDR_SYSTEM_ERROR:
return (short)getLastError();
case MB_ADDR_APP_STATE:
return (short)_state;
case MB_ADDR_ECHO_TEST:
return (short)88;
default:
return 0;
}
}
short PHApp::mb_tcp_write(MB_Registers *reg, short networkValue)
{
return mb_tcp_write(reg->startAddress, networkValue);
}
/**
* @brief Handles Modbus write requests for PHApp's specific registers.
*/
short PHApp::mb_tcp_write(short address, short value)
{
switch (address)
{
case MB_ADDR_RESET_CONTROLLER:
reset(0, 0);
return E_OK;
default:
return E_INVALID_PARAMETER;
}
}
short PHApp::loopModbus()
{
return E_OK;
}
short PHApp::setupModbus()
{
Log.infoln("Setting up Modbus TCP...");
modbusManager = new ModbusTCP(this, &mb);
if (!modbusManager)
{
Log.fatalln("Failed to create ModbusTCP!");
return E_INVALID_PARAMETER;
}
components.push_back(modbusManager); // Add manager to component list for setup/loop calls
setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS);
// --- Register Components with Modbus Manager ---
Log.infoln("PHApp::setupModbus - Registering components with ModbusTCP...");
mb_tcp_register(modbusManager);
Log.infoln("--- End Modbus Mappings DUMP --- ");
if (modbusManager->enabled())
{
mb.start(MODBUS_PORT, 10, 2000);
}
else
{
Log.warningln("PHApp::setupModbus - ModbusTCP not available or disabled. Skipping Modbus server start.");
}
return E_OK;
}
#endif // ENABLE_MODBUS_TCP
+99
View File
@@ -0,0 +1,99 @@
#include "PHApp.h"
#include "config.h"
#include <ArduinoLog.h> // For logging
#include <enums.h> // For error codes like E_INVALID_PARAMETER
#ifdef ENABLE_PROFILE_TEMPERATURE
#include <LittleFS.h> // For file system access
#include <ArduinoJson.h> // For JSON parsing
#include "profiles/TemperatureProfile.h" // For TemperatureProfile
#endif // ENABLE_PROFILE_TEMPERATURE
short PHApp::load(short val0, short val1)
{
Log.infoln(F("PHApp::load() - Loading application data..."));
#ifdef ENABLE_PROFILE_TEMPERATURE
Log.infoln(F("PHApp::load() - Attempting to load temperature profiles..."));
if (!LittleFS.begin(true)) { // Ensure LittleFS is mounted (true formats if necessary)
Log.errorln(F("PHApp::load() - Failed to mount LittleFS. Cannot load profiles."));
return E_INVALID_PARAMETER; // Use invalid parameter as fallback
}
const char* filename = "/profiles/defaults.json"; // Path in LittleFS
File file = LittleFS.open(filename, "r");
if (!file) {
Log.errorln(F("PHApp::load() - Failed to open profile file: %s"), filename);
LittleFS.end(); // Close LittleFS
return E_NOT_FOUND; // Use standard not found
}
// Increased size slightly for safety, adjust if needed
// DynamicJsonDocument doc(JSON_ARRAY_SIZE(PROFILE_TEMPERATURE_COUNT) + PROFILE_TEMPERATURE_COUNT * JSON_OBJECT_SIZE(5 + MAX_TEMP_CONTROL_POINTS));
// Replace DynamicJsonDocument with JsonDocument, letting it handle allocation.
JsonDocument doc;
// Deserialize the JSON document
DeserializationError error = deserializeJson(doc, file);
file.close(); // Close the file ASAP
LittleFS.end(); // Close LittleFS
if (error) {
Log.errorln(F("PHApp::load() - Failed to parse profile JSON: %s"), error.c_str());
return E_INVALID_PARAMETER; // Use invalid parameter
}
// Check if the root is a JSON array
if (!doc.is<JsonArray>()) {
Log.errorln(F("PHApp::load() - Profile JSON root is not an array."));
return E_INVALID_PARAMETER; // Use invalid parameter
}
JsonArray profilesArray = doc.as<JsonArray>();
Log.infoln(F("PHApp::load() - Found %d profiles in JSON file."), profilesArray.size());
uint8_t profileIndex = 0;
for (JsonObject profileJson : profilesArray) {
if (profileIndex >= PROFILE_TEMPERATURE_COUNT) {
Log.warningln(F("PHApp::load() - Too many profiles in JSON (%d), only loading the first %d."), profilesArray.size(), PROFILE_TEMPERATURE_COUNT);
break;
}
if (!tempProfiles[profileIndex]) {
Log.errorln(F("PHApp::load() - TemperatureProfile slot %d is not initialized. Skipping JSON profile."), profileIndex);
// Don't increment profileIndex here, try to load next JSON into same slot if possible?
// Or increment profileIndex to align JSON index with slot index? Let's align.
profileIndex++;
continue;
}
// Assuming TemperatureProfile (or its base PlotBase) has a public method
// like loadFromJson that takes the JsonObject and calls the protected virtual load.
// We also assume it returns bool or short (E_OK for success).
Log.infoln(F("PHApp::load() - Loading JSON data into TemperatureProfile slot %d..."), profileIndex);
// Now call the protected load() directly, as PHApp is a friend
if (tempProfiles[profileIndex]->load(profileJson)) { // returns bool
const char* name = profileJson["name"] | "Unnamed"; // Get name for logging
Log.infoln(F("PHApp::load() - Successfully loaded profile '%s' into slot %d."), name, profileIndex);
} else {
Log.errorln(F("PHApp::load() - Failed to load profile data into slot %d."), profileIndex);
// Decide if we should return an error or just continue loading others
// return E_INVALID_PARAMETER; // Option: Stop loading on first failure
}
profileIndex++; // Move to the next TemperatureProfile slot
}
// Handle case where JSON has fewer profiles than allocated slots
if (profileIndex < profilesArray.size()) {
Log.warningln(F("PHApp::load() - Processed %d JSON profiles but only %d slots were available/initialized."), profilesArray.size(), profileIndex);
} else if (profileIndex < PROFILE_TEMPERATURE_COUNT) {
Log.infoln(F("PHApp::load() - Loaded %d profiles from JSON into %d available slots."), profileIndex, PROFILE_TEMPERATURE_COUNT);
}
Log.infoln(F("PHApp::load() - Finished loading temperature profiles."));
#endif // ENABLE_PROFILE_TEMPERATURE
Log.infoln(F("PHApp::load() - Application data loading complete."));
return E_OK;
}
+668
View File
@@ -0,0 +1,668 @@
#include <Arduino.h>
#include <macros.h>
#include <Component.h>
#include <enums.h>
#include <Logger.h>
#include "./PHApp.h"
#include "./config.h"
#include "./config_adv.h"
#include "./config-modbus.h"
#include "./features.h"
#ifdef ENABLE_PROCESS_PROFILE
#include <profiles/PlotBase.h>
#include <profiles/SignalPlot.h>
#include <profiles/TemperatureProfile.h>
#endif
#ifdef ENABLE_MODBUS_TCP
#include <modbus/ModbusTCP.h>
#include <modbus/ModbusTypes.h>
#endif
#define MB_R_APP_STATE_REG 9
#define MB_R_SYSTEM_CMD_PRINT_RESET 1
#define MB_R_SYSTEM_CMD_PRINT_REGS 2
#define MB_R_SYSTEM_CMD_PRINT_MEMORY 5
#define MB_R_SYSTEM_CMD_PRINT_VFD 6
#ifdef ENABLE_PROFILER
uint32_t PHApp::initialFreeHeap = 0;
uint64_t PHApp::initialCpuTicks = 0;
#endif
#ifndef LOG_LEVEL
#define LOG_LEVEL LOG_LEVEL_NONE
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Network Servers
//
#if defined(ENABLE_WEBSERVER)
WiFiServer server(80);
#endif
#ifdef ENABLE_MODBUS_TCP
ModbusServerTCPasync mb;
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Factory : Instances
//
#define ADD_RELAY(relayNum, relayPin, relayKey, relayAddr) \
relay_##relayNum = new Relay(this, relayPin, relayKey, relayAddr); \
components.push_back(relay_##relayNum);
#define ADD_POT(potNum, potPin, potKey, potAddr) \
pot_##potNum = new POT(this, potPin, potKey, potAddr); \
components.push_back(pot_##potNum);
#define ADD_POS3ANALOG(posNum, switchPin1, switchPin2, switchKey, switchAddr) \
pos3Analog_##posNum = new Pos3Analog(this, switchPin1, switchPin2, switchKey, switchAddr); \
components.push_back(pos3Analog_##posNum);
#ifdef ENABLE_PID
#define ADD_PID(pidNum, nameStr, doPin, csPin, clkPin, outPin, key) \
pidController_##pidNum = new PIDController(key, nameStr, doPin, csPin, clkPin, outPin); \
components.push_back(pidController_##pidNum);
#endif
void PHApp::printRegisters()
{
Log.verboseln(F("--- Entering PHApp::printRegisters ---"));
#if ENABLED(HAS_MODBUS_REGISTER_DESCRIPTIONS)
Log.setShowLevel(false);
Serial.print("| Name | ID | Address | RW | Function Code | Number Addresses |Register Description| \n");
Serial.print("|------|----------|----|----|----|----|-------|\n");
short size = components.size();
Log.verboseln(F("PHApp::printRegisters - Processing %d components..."), size);
for (int i = 0; i < size; i++)
{
Component *component = components[i];
if (!component)
{
Log.errorln(F("PHApp::printRegisters - Found NULL component at index %d"), i);
continue;
}
Log.verboseln(F("PHApp::printRegisters - Component %d: ID=%d, Name=%s"), i, component->id, component->name.c_str());
// if (!(component->nFlags & 1 << OBJECT_NET_CAPS::E_NCAPS_MODBUS)) // <-- Modbus flag check might be different now
// {
// continue;
// }
// Log.verbose("| %s | %d | %d | %s | %d | %d | %s |\n", // <-- Calls to removed ModbusGateway methods
// component->name.c_str(),
// component->id,
// component->getAddress(),
// component->getRegisterMode(),
// component->getFunctionCode(),
// component->getNumberAddresses(),
// component->getRegisterDescription().c_str());
Log.verbose("| %s | %d | - | - | - | - | - |\n", // <-- Simplified output
component->name.c_str(),
component->id);
}
Log.setShowLevel(true);
#endif
Log.verboseln(F("--- Exiting PHApp::printRegisters ---"));
}
short PHApp::reset(short val0, short val1)
{
_state = APP_STATE::RESET;
_error = E_OK;
#if defined(ESP32) || defined(ESP8266) // Use ESP.restart() for ESP32 and ESP8266
ESP.restart();
#else
return E_NOT_IMPLEMENTED;
#endif
return E_OK;
}
short PHApp::list(short val0, short val1)
{
uchar s = components.size();
for (uchar i = 0; i < s; i++)
{
Component *component = components[i];
if (component)
{
Log.verboseln("PHApp::list - %d | %s (ID: %d)", i, component->name.c_str(), component->id);
}
else
{
Log.warningln("PHApp::list - NULL component at index %d", i);
}
}
return E_OK;
}
short PHApp::setup()
{
Log.verbose("--------------------PHApp::setup() Begin.-------------------");
#ifdef ENABLE_PROFILER
if (initialFreeHeap == 0 && initialCpuTicks == 0)
{
initialFreeHeap = ESP.getFreeHeap();
initialCpuTicks = esp_cpu_get_ccount();
}
#endif
#ifndef DISABLE_SERIAL_LOGGING
// Serial Setup
Serial.begin(SERIAL_BAUD_RATE);
while (!Serial && !Serial.available())
{
}
// Log Setup
Log.begin(LOG_LEVEL, &Serial);
Log.setShowLevel(true);
#else
// Log Setup (without Serial)
Log.begin(LOG_LEVEL_WARNING, nullptr); // Or LOG_LEVEL_NONE, or some other target if available
Log.setShowLevel(false);
#endif
// Components
bridge = new Bridge(this);
#ifndef DISABLE_SERIAL_LOGGING
com_serial = new SerialMessage(Serial, bridge);
components.push_back(com_serial);
#endif
components.push_back(bridge);
// Network
short networkSetupResult = setupNetwork();
if (networkSetupResult != E_OK)
{
Log.errorln("Network setup failed with error code: %d", networkSetupResult);
}
// Components
#ifdef ENABLE_RELAYS
#ifdef AUX_RELAY_0
ADD_RELAY(0, AUX_RELAY_0, COMPONENT_KEY_RELAY_0, MB_ADDR_AUX_5);
#endif
#ifdef AUX_RELAY_1
ADD_RELAY(1, AUX_RELAY_1, COMPONENT_KEY_RELAY_1, MB_ADDR_AUX_6);
#endif
#endif
#ifdef MB_ANALOG_0
ADD_POT(0, MB_ANALOG_0, COMPONENT_KEY_ANALOG_0, MB_ADDR_AUX_2);
#endif
#ifdef MB_ANALOG_1
ADD_POT(1, MB_ANALOG_1, COMPONENT_KEY_ANALOG_1, MB_ADDR_AUX_3);
#endif
#if (defined(AUX_ANALOG_3POS_SWITCH_0) && (defined(AUX_ANALOG_3POS_SWITCH_1)))
ADD_POS3ANALOG(0, AUX_ANALOG_3POS_SWITCH_0, AUX_ANALOG_3POS_SWITCH_1, COMPONENT_KEY_MB_ANALOG_3POS_SWITCH_0, MB_ADDR_AUX_3);
#endif
#if (defined(MB_ANALOG_3POS_SWITCH_2) && (defined(MB_ANALOG_3POS_SWITCH_3)))
// ADD_POS3ANALOG(1, MB_ANALOG_3POS_SWITCH_2, MB_ANALOG_3POS_SWITCH_3, COMPONENT_KEY_MB_ANALOG_3POS_SWITCH_1, MB_R_SWITCH_1); // <-- Temporarily disable
#endif
#ifdef MB_GPIO_MB_MAP_7
// --- Define configuration for the MB_GPIO group ---
std::vector<GPIO_PinConfig> gpioConfigs;
gpioConfigs.reserve(2); // Reserve space for 2 elements
gpioConfigs.push_back(
GPIO_PinConfig(
E_GPIO_7, // pinNumber: The physical pin to manage
E_GPIO_TYPE_ANALOG_INPUT, // pinType: Treat as analog input
300, // startAddress: Modbus register address
E_FN_CODE::FN_READ_HOLD_REGISTER, // type: Map to a Holding Register
MB_ACCESS_READ_ONLY, // access: Allow Modbus read only
1000, // opIntervalMs: Update interval in milliseconds
"GPIO_6", // name: Custom name for this pin
"GPIO_Group" // group: Group name for this pin
));
gpioConfigs.push_back(
GPIO_PinConfig(
E_GPIO_15, // pinNumber: The physical pin to manage
E_GPIO_TYPE_ANALOG_INPUT, // pinType: Treat as analog input
301, // startAddress: Modbus register address
E_FN_CODE::FN_READ_HOLD_REGISTER, // type: Map to a Holding Register
MB_ACCESS_READ_ONLY, // access: Allow Modbus read only
1000, // opIntervalMs: Update interval in milliseconds
"GPIO_15", // name: Custom name for this pin
"GPIO_Group" // group: Group name for this pin
));
const short gpioGroupId = COMPONENT_KEY_GPIO_MAP; // Using defined key
gpio_0 = new MB_GPIO(this, gpioGroupId, gpioConfigs);
components.push_back(gpio_0);
#endif
#ifdef PIN_ANALOG_LEVEL_SWITCH_0
analogLevelSwitch_0 = new AnalogLevelSwitch(
this, // owner
PIN_ANALOG_LEVEL_SWITCH_0, // analogPin
ALS_0_NUM_LEVELS, // numLevels
ALS_0_ADC_STEP, // levelStep
ALS_0_ADC_OFFSET, // adcValueOffset
ID_ANALOG_LEVEL_SWITCH_0, // id
ALS_0_MB_ADDR // modbusAddress
);
if (analogLevelSwitch_0)
{
components.push_back(analogLevelSwitch_0);
Log.infoln(F("AnalogLevelSwitch_0 initialized. Pin:%d, ID:%d, Levels:%d, Step:%d, Offset:%d, Smooth:%d(Fixed), Debounce:%d(Fixed), MB:%d"),
PIN_ANALOG_LEVEL_SWITCH_0, ID_ANALOG_LEVEL_SWITCH_0, ALS_0_NUM_LEVELS,
ALS_0_ADC_STEP, ALS_0_ADC_OFFSET,
ALS_SMOOTHING_SIZE,
ALS_DEBOUNCE_COUNT,
ALS_0_MB_ADDR);
}
else
{
Log.errorln(F("AnalogLevelSwitch_0 initialization failed."));
}
#endif
#ifdef ENABLE_STATUS
statusLight_0 = new StatusLight(this,
STATUS_WARNING_PIN,
COMPONENT_KEY_FEEDBACK_0,
MB_MONITORING_STATUS_FEEDBACK_0); // Keep original address for now, add to config-modbus.h later if needed
components.push_back(statusLight_0);
statusLight_1 = new StatusLight(this,
STATUS_ERROR_PIN,
COMPONENT_KEY_FEEDBACK_1,
MB_MONITORING_STATUS_FEEDBACK_1); // Keep original address for now
components.push_back(statusLight_1);
#else
statusLight_0 = NULL;
statusLight_1 = NULL;
#endif
Log.infoln("PHApp::setup - Base App::setup() called.");
#ifdef PIN_LED_FEEDBACK_0
ledFeedback_0 = new LEDFeedback(
this, // owner
PIN_LED_FEEDBACK_0, // pin
LED_PIXEL_COUNT_0, // pixelCount
ID_LED_FEEDBACK_0, // id
LED_FEEDBACK_0_MB_ADDR // modbusAddress
);
if (ledFeedback_0)
{
components.push_back(ledFeedback_0);
Log.infoln(F("LEDFeedback_0 initialized. Pin:%d, Count:%d, ID:%d, MB:%d"),
PIN_LED_FEEDBACK_0, LED_PIXEL_COUNT_0,
ID_LED_FEEDBACK_0, LED_FEEDBACK_0_MB_ADDR);
}
else
{
Log.errorln(F("LEDFeedback_0 initialization failed."));
}
#endif
#ifdef ENABLE_JOYSTICK
joystick_0 = new Joystick(
this, // owner
PIN_JOYSTICK_UP, // UP pin
PIN_JOYSTICK_DOWN, // DOWN pin
PIN_JOYSTICK_LEFT, // LEFT pin
PIN_JOYSTICK_RIGHT, // RIGHT pin
MB_ADDR_AUX_7 // modbusAddress
);
if (joystick_0)
{
components.push_back(joystick_0);
}
else
{
Log.errorln(F("Joystick_0 initialization failed."));
}
#endif
// Motors
#ifdef ENABLE_SAKO_VFD
vfd_0 = new SAKO_VFD(MB_SAKO_VFD_SLAVE_ID, MB_SAKO_VFD_READ_INTERVAL);
components.push_back(vfd_0);
#endif
// Temperature
#ifdef ENABLE_PROFILE_TEMPERATURE
for (ushort i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i)
{
tempProfiles[i] = new TemperatureProfile(
this,
i);
components.push_back(tempProfiles[i]);
}
#endif
#ifdef ENABLE_PID
const int8_t PID2_THERMO_DO = 19; // Example MISO pin
const int8_t PID2_THERMO_CS = 5; // Example Chip Select pin
const int8_t PID2_THERMO_CLK = 18; // Example SCK pin
const int8_t PID2_OUTPUT_PIN = 23; // Example PWM/Output pin
ADD_PID(0, "PID Temp Controller 2", PID2_THERMO_DO, PID2_THERMO_CS, PID2_THERMO_CLK, PID2_OUTPUT_PIN, COMPONENT_KEY_PID_2);
#endif
// RS485
#ifdef ENABLE_RS485
rs485 = new RS485(this);
components.push_back(rs485);
#endif
// Systems : Extruder
#ifdef ENABLE_EXTRUDER
extruder_0 = new Extruder(this, vfd_0, nullptr, nullptr, nullptr);
components.push_back(extruder_0);
#endif
#ifdef ENABLE_PLUNGER
plunger_0 = new Plunger(this, vfd_0, joystick_0, pot_0, pot_1);
components.push_back(plunger_0);
#endif
// Application stuff
registerComponents(bridge);
serial_register(bridge);
_state = APP_STATE::RESET;
_error = E_OK;
App::setup();
#ifdef ENABLE_MODBUS_TCP
for (Component *comp : components)
{
if (comp && comp->hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS))
{
comp->mb_tcp_register(modbusManager);
}
}
#endif
#ifdef ENABLE_PROFILE_TEMPERATURE
load(0, 0);
#endif
#ifdef ENABLE_RELAYS
#ifdef AUX_RELAY_0
relay_0->setValue(1);
#endif
#endif
///////////////////////////////////////////////////////////////////////////////////////////////
//
// Post initialization
//
#ifdef ENABLE_WEBSERVER
registerRoutes(webServer);
#endif
return E_OK;
}
short PHApp::serial_register(Bridge *bridge)
{
bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this, C_STR("list"), (ComponentFnPtr)&PHApp::list);
bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this, C_STR("reset"), (ComponentFnPtr)&PHApp::reset);
bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this, C_STR("printRegisters"), (ComponentFnPtr)&PHApp::printRegisters);
bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this, C_STR("load"), (ComponentFnPtr)&PHApp::load);
return E_OK;
}
short PHApp::onWarning(short code)
{
return E_OK;
}
short PHApp::onRun(short code)
{
return E_OK;
}
short PHApp::onError(short id, short code)
{
if (code == getLastError())
{
return code;
}
Log.error(F("* App:onError - component=%d code=%d" CR), id, code);
setLastError(code);
#ifdef ENABLE_STATUS
if (statusLight_0)
{
switch (id)
{
case COMPONENT_KEY_PLUNGER:
{
switch (code)
{
#if defined(ENABLE_PLUNGER)
case (short)PlungerState::IDLE:
{
statusLight_1->set(0, 0);
break;
}
case (short)PlungerState::JAMMED:
{
statusLight_1->set(1, 1);
break;
}
default:
{
statusLight_1->set(1, 0);
break;
}
}
#endif
}
}
}
#endif
return code;
}
short PHApp::onStop(short val)
{
return E_OK;
}
short PHApp::clearError()
{
setLastError(E_OK);
return E_OK;
}
short PHApp::loop()
{
App::loop();
#ifdef ENABLE_WEBSERVER
loopWeb();
#endif
#ifdef ENABLE_MODBUS_TCP
loopModbus();
#endif
return E_OK;
}
short PHApp::loopWeb()
{
#if defined(ENABLE_WIFI) && defined(ENABLE_WEBSERVER)
if (webServer != nullptr)
{
webServer->loop();
}
#endif
return E_OK;
}
short PHApp::getAppState(short val)
{
return _state;
}
PHApp::PHApp() : App()
{
name = "PHApp";
webServer = nullptr;
pidController_0 = nullptr;
bridge = nullptr;
com_serial = nullptr;
pot_0 = nullptr;
pot_1 = nullptr;
pot_2 = nullptr;
statusLight_0 = nullptr;
statusLight_1 = nullptr;
relay_0 = nullptr;
relay_1 = nullptr;
relay_2 = nullptr;
relay_3 = nullptr;
relay_4 = nullptr;
relay_5 = nullptr;
relay_6 = nullptr;
relay_7 = nullptr;
pos3Analog_0 = nullptr;
pos3Analog_1 = nullptr;
modbusManager = nullptr;
logPrinter = nullptr;
rs485 = nullptr;
joystick_0 = nullptr;
#ifdef ENABLE_PROFILE_TEMPERATURE
// Initialize the array elements to nullptr
for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i)
{
tempProfiles[i] = nullptr;
}
#endif
// WiFi settings are now initialized by WiFiNetworkSettings constructor
}
void PHApp::cleanupComponents()
{
Log.infoln("PHApp::cleanupComponents - Cleaning up %d components...", components.size());
for (Component *comp : components)
{
delete comp;
}
components.clear(); // Clear the vector AFTER deleting objects
// Nullify pointers that were manually managed or outside the vector
bridge = nullptr;
com_serial = nullptr;
pot_0 = nullptr;
pot_1 = nullptr;
pot_2 = nullptr;
statusLight_0 = nullptr;
statusLight_1 = nullptr;
relay_0 = nullptr;
relay_1 = nullptr;
relay_2 = nullptr;
relay_3 = nullptr;
relay_4 = nullptr;
relay_5 = nullptr;
relay_6 = nullptr;
relay_7 = nullptr;
pos3Analog_0 = nullptr;
pos3Analog_1 = nullptr;
pidController_0 = nullptr;
modbusManager = nullptr;
joystick_0 = nullptr;
#ifdef ENABLE_PROFILE_TEMPERATURE
// Clean up temperature profiles (they were also added to components vector, so already deleted there)
// Ensure the pointers in the array are nulled
for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i)
{
tempProfiles[i] = nullptr;
}
#endif
#ifdef ENABLE_WEBSERVER
if (webServer)
{
delete webServer;
webServer = nullptr;
}
#endif
#ifdef ENABLE_RS485
rs485 = nullptr; // RS485 interface was in the vector, already deleted
#endif
Log.infoln("PHApp::cleanupComponents - Cleanup complete.");
}
PHApp::~PHApp()
{
cleanupComponents();
}
short PHApp::setAppState(short newState)
{
if (_state != newState)
{
_state = (APP_STATE)newState;
}
return E_OK;
}
#ifdef ENABLE_PID
short PHApp::getPid2Register(short offset, short unused)
{
if (!pidController_0)
{
Log.errorln("Serial Command Error: PID Controller 2 not initialized.");
return E_INVALID_PARAMETER; // Use defined error code
}
if (offset < 0 || offset >= PID_2_REGISTER_COUNT)
{
Log.errorln("Serial Command Error: Invalid PID2 offset %d.", offset);
return E_INVALID_PARAMETER;
}
short address = MB_HREG_PID_2_BASE_ADDRESS + offset;
short value = pidController_0->mb_tcp_read(address);
Log.noticeln("PID2 Register Offset %d (Addr %d) Value: %d", offset, address, value);
// Optionally send value back over serial if needed by the protocol
return E_OK;
}
short PHApp::setPid2Register(short offset, short value)
{
if (!pidController_0)
{
Log.errorln("Serial Command Error: PID Controller 2 not initialized.");
return E_INVALID_PARAMETER; // Use defined error code
}
if (offset < 0 || offset >= PID_2_REGISTER_COUNT)
{
Log.errorln("Serial Command Error: Invalid PID2 offset %d.", offset);
return E_INVALID_PARAMETER;
}
short address = MB_HREG_PID_2_BASE_ADDRESS + offset;
short result = pidController_0->mb_tcp_write(address, value);
if (result == E_OK)
{
Log.noticeln("PID2 Register Offset %d (Addr %d) set to: %d", offset, address, value);
}
else
{
Log.errorln("PID2 Register Offset %d (Addr %d) failed to set to %d. Error: %d", offset, address, value, result);
}
return result;
}
#endif
short PHApp::onMessage(int id, E_CALLS verb, E_MessageFlags flags, void *user, Component *src)
{
#if ENABLED(ENABLE_RS485, ENABLE_WEBSERVER, ENABLE_WEBSOCKET)
if (verb == E_CALLS::EC_USER && user != nullptr && webServer != nullptr)
{
return webServer->onMessage(id, E_CALLS::EC_USER, E_MessageFlags::E_MF_NONE, user, this);
}
#endif
return App::onMessage(id, verb, flags, user, src);
}
/**
* @brief Retrieves a component by its ID.
*
* @param id The ID of the component to retrieve.
* @return A pointer to the component with the specified ID, or nullptr if not found.
* @note Top-Level PHApp cant be part of components vector, so we need to handle it separately.
*/
Component *PHApp::byId(ushort id)
{
Component *comp = App::byId(id);
if (comp)
{
return comp;
}
else if (id == COMPONENT_KEY_APP)
{
return this;
}
return nullptr;
}
+264
View File
@@ -0,0 +1,264 @@
#ifndef PHAPP_H
#define PHAPP_H
#include "config.h"
#include "config-modbus.h"
#include "features.h"
#include <enums.h>
#include <vector>
#include <xmath.h>
#include <macros.h>
#include <App.h>
#include <Component.h>
#include <Bridge.h>
#include <SerialMessage.h>
#include <ArduinoLog.h>
#include <Logger.h>
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <xstatistics.h>
#include <modbus/ModbusTCP.h>
#include <modbus/ModbusTypes.h>
#include <profiles/SignalPlot.h>
#include <profiles/WiFiNetworkSettings.h>
#include <components/OmronE5.h>
class POT;
class Relay;
class RS485;
class Pos3Analog;
class StatusLight;
class RESTServer;
class PIDController;
class TemperatureProfile;
class SAKO_VFD;
class MB_GPIO;
class AnalogLevelSwitch;
class LEDFeedback;
class Extruder;
class Plunger;
class Joystick;
class PHApp;
class AsyncWebServerRequest;
class PHApp : public App
{
public:
//////////////////////////////////////////////////////////////
// Enums
//////////////////////////////////////////////////////////////
enum CONTROLLER_STATE
{
E_CS_OK = 0,
E_CS_ERROR = 10
};
enum APP_STATE
{
RESET = 0,
EXTRUDING = 1,
STANDBY = 2,
ERROR = 5,
PID_TIMEOUT = 11,
FEED_TIMEOUT = 12,
CONTROL_PANEL_INVALID = 13,
PID_ERROR = 20,
FEED_ERROR = 40,
};
//////////////////////////////////////////////////////////////
// Constructor / Destructor
//////////////////////////////////////////////////////////////
PHApp();
~PHApp() override;
//////////////////////////////////////////////////////////////
// Core Application Logic
//////////////////////////////////////////////////////////////
virtual short setup();
short loop() override;
short load(short val0, short val1);
virtual short serial_register(Bridge *bridge);
virtual Component *byId(ushort id);
// App States & Error Handling
short _state;
short _cstate;
short _error;
short setAppState(short newState);
short getAppState(short val);
short getLastError() { return _error; }
short setLastError(short val = 0) { _error = val; return _error; }
short onError(short id, short code);
short clearError();
short reset(short arg1, short arg2); // Related to resetting state?
//////////////////////////////////////////////////////////////
// Components
//////////////////////////////////////////////////////////////
SerialMessage *com_serial;
POT *pot_0;
POT *pot_1;
POT *pot_2;
StatusLight *statusLight_0;
StatusLight *statusLight_1;
Relay *relay_0;
Relay *relay_1;
Relay *relay_2;
Relay *relay_3;
Relay *relay_4;
Relay *relay_5;
Relay *relay_6;
Relay *relay_7;
Pos3Analog *pos3Analog_0;
Pos3Analog *pos3Analog_1;
PIDController *pidController_0;
SAKO_VFD *vfd_0;
Extruder *extruder_0;
Plunger *plunger_0;
MB_GPIO *gpio_0;
AnalogLevelSwitch *analogLevelSwitch_0;
LEDFeedback *ledFeedback_0;
Joystick *joystick_0;
// Component Callbacks/Control
short onStop(short code = 0);
short onRun(short code = 0);
short onWarning(short code);
//////////////////////////////////////////////////////////////
// Logging
//////////////////////////////////////////////////////////////
std::vector<String> logBuffer;
size_t currentLogIndex = 0;
CircularLogPrinter *logPrinter = nullptr;
std::vector<String> getLogSnapshot()
{
std::vector<String> snapshot;
snapshot.reserve(logBuffer.size());
if (logBuffer.size() < LOG_BUFFER_LINES)
{
for (size_t i = 0; i < logBuffer.size(); ++i)
{
snapshot.push_back(logBuffer[i]);
}
}
else
{
// Buffer is full and circular
size_t startIndex = (currentLogIndex + 1) % LOG_BUFFER_LINES; // <-- Note: LOG_BUFFER_LINES is now defined in Logger.h
for (size_t i = 0; i < LOG_BUFFER_LINES; ++i)
{
snapshot.push_back(logBuffer[(startIndex + i) % LOG_BUFFER_LINES]);
}
}
return snapshot;
}
//////////////////////////////////////////////////////////////
// Network Management
//////////////////////////////////////////////////////////////
short setupNetwork();
short loadNetworkSettings();
short saveNetworkSettings(JsonObject& doc);
WiFiNetworkSettings wifiSettings;
//////////////////////////////////////////////////////////////
// Modbus TCP
//////////////////////////////////////////////////////////////
ModbusTCP *modbusManager;
short loopModbus();
#ifdef ENABLE_MODBUS_TCP
short setupModbus();
short mb_tcp_write(short address, short value) override;
short mb_tcp_write(MB_Registers *reg, short networkValue) override;
short mb_tcp_read(short address) override;
void mb_tcp_register(ModbusTCP *manager) const override;
ModbusBlockView *mb_tcp_blocks() const override;
int client_count;
int client_max;
int client_total;
millis_t client_track_ts;
short updateClientCount(short val0, short val1);
short resetClientStats(short val0, short val1);
short getClientStats(short val0, short val1);
// Modbus PID Specific (Conditional)
#ifdef ENABLE_PID
short getPid2Register(short offset, short unused);
short setPid2Register(short offset, short value);
#endif // ENABLE_PID
#endif // ENABLE_MODBUS_TCP
RESTServer *webServer;
short loopWeb();
#ifdef ENABLE_RS485
friend class RS485Devices;
#endif // ENABLE_RS485
//////////////////////////////////////////////////////////////
// Component Overrides / Message Handling
/////////////////////////////////////////////////////////////
/**
* @brief Handles incoming messages, including RTU updates via void*.
*/
short onMessage(int id, E_CALLS verb, E_MessageFlags flags, void* user, Component *src) override;
//////////////////////////////////////////////////////////////
// Debugging & Utility Methods
//////////////////////////////////////////////////////////////
void printRegisters();
short list(short val0, short val1);
short print(short arg1, short arg2);
//////////////////////////////////////////////////////////////
// Profiling & Feature Specific (Conditional)
//////////////////////////////////////////////////////////////
#ifdef ENABLE_PROFILER
static uint32_t initialFreeHeap;
static uint64_t initialCpuTicks;
#endif // ENABLE_PROFILER
#ifdef ENABLE_PROFILE_TEMPERATURE
TemperatureProfile* tempProfiles[PROFILE_TEMPERATURE_COUNT]; // Array to hold multiple temperature profiles
#endif // ENABLE_PROCESS_PROFILE
//////////////////////////////////////////////////////////////
// Web Server
//////////////////////////////////////////////////////////////
/**
* @brief Register routes with the RESTServer. This will be called upon built-in RESTServer initialization.
*
* @param server The RESTServer instance to register routes with.
* @return short The result of the operation.
*/
short registerRoutes(RESTServer *instance);
// Network settings handlers
#ifdef ENABLE_WEBSERVER_WIFI_SETTINGS
void handleGetNetworkSettings(AsyncWebServerRequest *request);
void handleSetNetworkSettings(AsyncWebServerRequest *request, JsonVariant &json);
#endif
#ifdef ENABLE_PROFILE_TEMPERATURE
void getProfilesHandler(AsyncWebServerRequest *request);
void setProfileHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot); // Adjusted for body handling
#endif
void getSystemLogsHandler(AsyncWebServerRequest *request);
private:
//////////////////////////////////////////////////////////////
// Private Methods
//////////////////////////////////////////////////////////////
void handleSerialCommand(const String &command); // Moved here as it's private impl detail
void cleanupComponents(); // Moved here as it's private impl detail
};
#endif
+284
View File
@@ -0,0 +1,284 @@
#include <Arduino.h>
#include <macros.h>
#include <Component.h>
#include <enums.h>
#include "Logger.h"
#include "./PHApp.h"
#include <ESPmDNS.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "./config.h"
#include "./config_adv.h"
#include "./config-modbus.h"
#include "./features.h"
#ifdef ENABLE_PROCESS_PROFILE
#include "profiles/PlotBase.h"
#include "profiles/SignalPlot.h"
#include "profiles/TemperatureProfile.h"
#endif
#include <modbus/ModbusTCP.h>
#include <modbus/ModbusTypes.h>
short PHApp::loadNetworkSettings() {
Log.infoln(F("PHApp::loadNetworkSettings() - Attempting to load network configuration from LittleFS..."));
if (!LittleFS.begin(true)) { // Ensure LittleFS is mounted (true formats if necessary)
Log.errorln(F("PHApp::loadNetworkSettings() - Failed to mount LittleFS. Cannot load network configuration."));
wifiSettings.print(); // Print defaults before returning
return E_FATAL; // Use E_FATAL for critical FS failure
}
File configFile = LittleFS.open(NETWORK_CONFIG_FILENAME, "r");
if (!configFile) {
Log.warningln(F("PHApp::loadNetworkSettings() - Failed to open network config file: %s. Using default settings."), NETWORK_CONFIG_FILENAME);
LittleFS.end(); // Close LittleFS
wifiSettings.print(); // Print defaults before returning
return E_NOT_FOUND; // Indicates file wasn't found, defaults will be used.
}
Log.infoln(F("PHApp::loadNetworkSettings() - Opened network config file: %s"), NETWORK_CONFIG_FILENAME);
JsonDocument doc; // Using JsonDocument for automatic memory management
DeserializationError error = deserializeJson(doc, configFile);
configFile.close(); // Close the file as soon as possible
if (error) {
Log.errorln(F("PHApp::loadNetworkSettings() - Failed to parse network config JSON: %s. Using default settings."), error.c_str());
LittleFS.end(); // Close LittleFS
wifiSettings.print(); // Print defaults before returning
return E_INVALID_PARAMETER; // Indicates a parsing error, defaults will be used.
}
JsonObject root = doc.as<JsonObject>();
if (root.isNull()) {
Log.errorln(F("PHApp::loadNetworkSettings() - Network config JSON root is not an object. Using default settings."));
LittleFS.end();
wifiSettings.print(); // Print defaults before returning
return E_INVALID_PARAMETER;
}
Log.infoln(F("PHApp::loadNetworkSettings() - Successfully parsed network config file. Applying settings..."));
short loadResult = wifiSettings.loadSettings(root); // Call the existing method in WiFiNetworkSettings
LittleFS.end(); // Ensure LittleFS is closed after operations
if (loadResult == E_OK) {
Log.infoln(F("PHApp::loadNetworkSettings() - Network settings loaded successfully from %s."), NETWORK_CONFIG_FILENAME);
} else {
Log.warningln(F("PHApp::loadNetworkSettings() - Issues applying parsed network settings. Some defaults may still be in use."));
}
wifiSettings.print(); // Print settings after attempting to load them
return loadResult;
}
short PHApp::saveNetworkSettings(JsonObject& doc) {
Log.infoln(F("PHApp::saveNetworkSettings() - Attempting to save network configuration to LittleFS..."));
if (!LittleFS.begin(true)) { // Ensure LittleFS is mounted
Log.errorln(F("PHApp::saveNetworkSettings() - Failed to mount LittleFS. Cannot save network configuration."));
return E_FATAL; // Or a more specific LittleFS error
}
File configFile = LittleFS.open(NETWORK_CONFIG_FILENAME, "w"); // Open for writing, creates if not exists, truncates if exists
if (!configFile) {
Log.errorln(F("PHApp::saveNetworkSettings() - Failed to open network config file '%s' for writing."), NETWORK_CONFIG_FILENAME);
LittleFS.end(); // Close LittleFS
return E_FATAL; // Replaced E_FS_ERROR with E_FATAL
}
Log.infoln(F("PHApp::saveNetworkSettings() - Opened/created network config file: %s for writing."), NETWORK_CONFIG_FILENAME);
size_t bytesWritten = serializeJson(doc, configFile);
configFile.close(); // Close the file as soon as possible
if (bytesWritten > 0) {
Log.infoln(F("PHApp::saveNetworkSettings() - Successfully wrote %d bytes to %s."), bytesWritten, NETWORK_CONFIG_FILENAME);
} else {
Log.errorln(F("PHApp::saveNetworkSettings() - Failed to serialize JSON to file or wrote 0 bytes to %s."), NETWORK_CONFIG_FILENAME);
LittleFS.end(); // Close LittleFS
// Attempt to remove the (potentially empty or corrupted) file if serialization failed.
if (LittleFS.exists(NETWORK_CONFIG_FILENAME)) {
LittleFS.remove(NETWORK_CONFIG_FILENAME);
}
return E_INVALID_PARAMETER; // Or a more specific serialization error
}
LittleFS.end(); // Ensure LittleFS is closed after operations
Log.infoln(F("PHApp::saveNetworkSettings() - Network settings saved successfully to %s."), NETWORK_CONFIG_FILENAME);
// Optionally, after saving, you might want to immediately reload and apply these settings:
// loadNetworkSettings();
// Or, signal that a restart is needed for settings to take full effect if they are only read at boot.
return E_OK;
}
short PHApp::setupNetwork()
{
loadNetworkSettings(); // Load settings from LittleFS first
bool sta_connected = false;
bool ap_started = false;
#if defined(ENABLE_AP_STA)
WiFi.mode(WIFI_AP_STA);
Log.infoln("Setting up AP_STA with SSID: %s", wifiSettings.ap_ssid.c_str());
if (!WiFi.softAPConfig(wifiSettings.ap_config_ip, wifiSettings.ap_config_gateway, wifiSettings.ap_config_subnet))
{
Log.errorln("AP Failed to configure");
}
else
{
if (!WiFi.softAP(wifiSettings.ap_ssid.c_str(), wifiSettings.ap_password.c_str()))
{
Log.errorln("AP Failed to start");
}
else
{
Log.infoln("AP IP address: %s", WiFi.softAPIP().toString().c_str());
ap_started = true;
}
}
// Configure Station (STA) part
Log.infoln("Configuring STA for AP_STA mode...");
if (!WiFi.config(wifiSettings.sta_local_IP, wifiSettings.sta_gateway, wifiSettings.sta_subnet, wifiSettings.sta_primary_dns, wifiSettings.sta_secondary_dns))
{
Log.errorln("STA (for AP_STA) Failed to configure");
}
WiFi.begin(wifiSettings.sta_ssid.c_str(), wifiSettings.sta_password.c_str());
Log.infoln("Attempting to connect to STA WiFi: %s", wifiSettings.sta_ssid.c_str());
int connect_timeout_ms = 30000;
unsigned long start_time = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - start_time < connect_timeout_ms))
{
delay(100);
}
if (WiFi.status() == WL_CONNECTED)
{
Log.infoln("STA IP address (AP_STA mode): %s", WiFi.localIP().toString().c_str());
sta_connected = true;
}
else
{
Log.warningln("STA (for AP_STA) connection failed or timed out. AP is still active.");
}
#elif defined(ENABLE_WIFI) // STA mode only
Log.infoln("Configuring WiFi in STA mode...");
if (!WiFi.config(wifiSettings.sta_local_IP, wifiSettings.sta_gateway, wifiSettings.sta_subnet, wifiSettings.sta_primary_dns, wifiSettings.sta_secondary_dns))
{
Log.errorln("STA Failed to configure");
}
WiFi.begin(wifiSettings.sta_ssid.c_str(), wifiSettings.sta_password.c_str());
int connect_timeout_ms = 30000;
unsigned long start_time = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - start_time < connect_timeout_ms))
{
delay(100);
}
if (WiFi.status() == WL_CONNECTED)
{
Log.infoln("IP address: %s", WiFi.localIP().toString().c_str());
sta_connected = true;
}
else
{
Log.errorln("WiFi connection timed out!");
// return E_WIFI_CONNECTION_FAILED; // Keep network setup going if AP might work or for mDNS on AP
}
#endif
// Initialize mDNS
// It should be started if either STA is connected or AP is successfully started.
if (sta_connected || ap_started) {
const char* mdns_hostname = "polymech-rtu"; // You can make this configurable later
if (MDNS.begin(mdns_hostname)) {
Log.infoln("mDNS responder started. Hostname: %s", mdns_hostname);
MDNS.addService("http", "tcp", 80);
Log.infoln("mDNS service _http._tcp.local on port 80 advertised.");
Log.infoln("Access the web server at: http://%s.local", mdns_hostname);
} else {
Log.errorln("Error starting mDNS responder!");
}
} else {
Log.warningln("Neither STA connected nor AP started. mDNS will not be initialized.");
}
#ifdef ENABLE_MODBUS_TCP
setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS);
setupModbus();
#else
modbusManager = nullptr;
#endif
#ifdef ENABLE_LITTLEFS
Log.infoln("Attempting to mount LittleFS...");
if (!LittleFS.begin(true))
{
Log.errorln("LittleFS mount failed AND format failed! Web files will not be available.");
}
else
{
Log.infoln("LittleFS mounted successfully (or formatted and mounted).");
}
#endif
#if defined(ENABLE_WEBSERVER) && defined(ENABLE_MODBUS_TCP)
if (modbusManager) // Check Modbus dependency first
{
IPAddress webserverIP = IPAddress(0,0,0,0);
bool canStartWebServer = false;
#if defined(ENABLE_AP_STA)
webserverIP = WiFi.softAPIP(); // IP of the AP interface
if (webserverIP && webserverIP != IPAddress(0,0,0,0)) {
Log.infoln("AP_STA mode: Web server will use AP IP: %s", webserverIP.toString().c_str());
canStartWebServer = true;
} else {
Log.errorln("AP_STA mode: Soft AP IP is invalid or not yet available. Cannot determine IP for web server on AP.");
}
// Log STA IP for informational purposes if connected
if (WiFi.status() == WL_CONNECTED) {
Log.infoln("AP_STA mode: STA interface is also connected with IP: %s", WiFi.localIP().toString().c_str());
Log.infoln(" External clients (on STA network) might try http://%s", WiFi.localIP().toString().c_str());
}
#elif defined(ENABLE_WIFI) // STA mode only
if (WiFi.status() == WL_CONNECTED) {
webserverIP = WiFi.localIP();
Log.infoln("STA mode: Web server will use STA IP: %s", webserverIP.toString().c_str());
canStartWebServer = true;
} else {
Log.errorln("STA mode: WiFi not connected. Cannot start web server.");
}
#else
// This case should not be hit if ENABLE_WEBSERVER implies one of the WiFi modes for IP-based server.
Log.warningln("WebServer enabled, but no WiFi mode (AP_STA or STA) is configured to provide an IP address.");
#endif
if (canStartWebServer) {
webServer = new RESTServer(webserverIP, 80, modbusManager, this);
components.push_back(webServer);
Log.infoln("RESTServer initialized.");
Log.infoln("Clients connected to the ESP32 (e.g., via AP) should try accessing the server at: http://%s", webserverIP.toString().c_str());
} else {
Log.errorln("Cannot initialize RESTServer: No suitable IP address available from current WiFi configuration.");
webServer = nullptr;
}
}
else
{
Log.errorln("Cannot initialize RESTServer: ModbusTCP is null! Ensure Modbus is setup first.");
webServer = nullptr;
return E_DEPENDENCY_NOT_MET;
}
#elif defined(ENABLE_WEBSERVER) && !defined(ENABLE_MODBUS_TCP)
Log.warningln("WebServer enabled but Modbus TCP is not. RESTServer initialization might be incomplete.");
webServer = nullptr; // Keep it null if it relies on ModbusTCP
#endif
return E_OK;
}
+5
View File
@@ -0,0 +1,5 @@
#include "PHApp.h"
#include "config.h"
#include <ArduinoLog.h>
#include <ArduinoJson.h>
+265
View File
@@ -0,0 +1,265 @@
#include "PHApp.h"
#include <components/RestServer.h>
#include <ESPAsyncWebServer.h>
short PHApp::registerRoutes(RESTServer *instance)
{
#ifdef ENABLE_PLUNGER
instance->server.on("/api/v1/plunger/settings", HTTP_GET, [instance](AsyncWebServerRequest *request)
{
Component* comp = instance->owner->byId(COMPONENT_KEY_PLUNGER);
if (!comp) {
request->send(404, "application/json", "{\"success\":false,\"error\":\"Plunger component not found\"}");
return;
}
Plunger* plunger = static_cast<Plunger*>(comp);
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
plunger->getSettingsJson(doc);
serializeJson(doc, *response);
request->send(response); });
AsyncCallbackJsonWebHandler *setPlungerSettingsHandler = new AsyncCallbackJsonWebHandler("/api/v1/plunger/settings",
[instance](AsyncWebServerRequest *request, JsonVariant &json)
{
Component *comp = instance->owner->byId(COMPONENT_KEY_PLUNGER);
if (!comp)
{
request->send(404, "application/json", "{\"success\":false,\"error\":\"Plunger component not found\"}");
return;
}
Plunger *plunger = static_cast<Plunger *>(comp);
if (!json.is<JsonObject>())
{
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid JSON payload: Expected an object.\"}");
return;
}
JsonObject jsonObj = json.as<JsonObject>();
if (plunger->updateSettingsFromJson(jsonObj))
{
request->send(200, "application/json", "{\"success\":true,\"message\":\"Plunger settings updated and saved.\"}");
}
else
{
request->send(500, "application/json", "{\"success\":false,\"error\":\"Failed to update or save Plunger settings.\"}");
}
});
setPlungerSettingsHandler->setMethod(HTTP_POST);
instance->server.addHandler(setPlungerSettingsHandler);
instance->server.on("/api/v1/plunger/settings/load-defaults", HTTP_POST, [instance](AsyncWebServerRequest *request)
{
Component* comp = instance->owner->byId(COMPONENT_KEY_PLUNGER);
if (!comp) {
request->send(404, "application/json", "{\"success\":false,\"error\":\"Plunger component not found\"}");
return;
}
Plunger* plunger = static_cast<Plunger*>(comp);
if (plunger->loadDefaultSettings()) {
request->send(200, "application/json", "{\"success\":true,\"message\":\"Plunger default settings loaded and applied to operational settings.\"}");
} else {
request->send(500, "application/json", "{\"success\":false,\"error\":\"Failed to load default settings or save them to operational path.\"}");
} });
#endif
#ifdef ENABLE_PROFILE_TEMPERATURE
// --- Temperature Profile Routes ---
server.on("/api/v1/profiles", HTTP_GET, [this](AsyncWebServerRequest *request)
{ this->getProfilesHandler(request); });
// Use AsyncJsonRequestBodyHandler for POST with JSON body
// Slot number is now expected in the JSON payload
AsyncCallbackJsonWebHandler *postProfileHandler = new AsyncCallbackJsonWebHandler("/api/v1/profiles",
[this](AsyncWebServerRequest *request, JsonVariant &json)
{
// Modern check: Use is<T>() which implicitly handles existence.
// If !json.is<JsonObject>(), the whole condition is true.
// If json is an object, then !json["slot"].is<int>() checks for existence AND integer type.
if (!json.is<JsonObject>() || !json["slot"].is<int>())
{
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid payload: Must be JSON object containing an integer 'slot' field.\"}");
return;
}
int slot = json["slot"].as<int>();
// Basic validation - check if slot is within a reasonable range
if (slot < 0 /*|| slot >= MAX_PROFILES - check MAX_PROFILES definition */)
{ // Remove check against 0 if slot 0 is valid
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid profile slot number in payload\"}");
return;
}
// Call the actual handler, passing the parsed JSON and extracted slot number
this->setProfileHandler(request, json, slot);
});
instance->server.addHandler(postProfileHandler);
#endif
#ifdef ENABLE_WEBSERVER_WIFI_SETTINGS
instance->server.on("/api/network/settings", HTTP_GET, std::bind(&PHApp::handleGetNetworkSettings, this, std::placeholders::_1));
AsyncCallbackJsonWebHandler *setNetworkSettingsHandler = new AsyncCallbackJsonWebHandler("/api/network/settings",
std::bind(&PHApp::handleSetNetworkSettings, this, std::placeholders::_1, std::placeholders::_2));
setNetworkSettingsHandler->setMethod(HTTP_POST);
instance->server.addHandler(setNetworkSettingsHandler);
#endif
instance->server.on("/api/v1/system/logs", HTTP_GET, [this](AsyncWebServerRequest *request)
{ this->getSystemLogsHandler(request); });
return E_OK;
}
#ifdef ENABLE_WEBSERVER_WIFI_SETTINGS
void PHApp::handleGetNetworkSettings(AsyncWebServerRequest *request)
{
JsonDocument doc = wifiSettings.toJSON();
String responseStr;
serializeJson(doc, responseStr);
request->send(200, "application/json", responseStr);
}
void PHApp::handleSetNetworkSettings(AsyncWebServerRequest *request, JsonVariant &json)
{
if (!json.is<JsonObject>())
{
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid JSON payload: Expected an object.\"}");
return;
}
JsonObject jsonObj = json.as<JsonObject>();
// Attempt to save the settings
short saveResult = saveNetworkSettings(jsonObj);
if (saveResult != E_OK)
{
Log.errorln("REST: Failed to save network settings, error: %d", saveResult);
request->send(500, "application/json", "{\"success\":false,\"error\":\"Failed to save network settings to persistent storage.\"}");
return;
}
// Attempt to load and apply the new settings immediately
short loadResult = loadNetworkSettings();
if (loadResult != E_OK && loadResult != E_NOT_FOUND)
{ // E_NOT_FOUND is ok if we just saved it, means it was applied from the save buffer
Log.warningln("REST: Issue loading network settings after save, error: %d. Settings might not be immediately active.", loadResult);
// Decide if this is a critical failure for the response
}
request->send(200, "application/json", "{\"success\":true,\"message\":\"Network settings saved. Device will attempt to apply them. A restart might be required for all changes to take effect.\"}");
}
void PHApp::getSystemLogsHandler(AsyncWebServerRequest *request)
{
String levelStr = "verbose"; // Default to verbose
if (request->hasParam("level"))
{
levelStr = request->getParam("level")->value();
}
// Map string log levels to their integer values
int requestedLevel = LOG_LEVEL_VERBOSE; // Default to verbose
if (levelStr == "none")
requestedLevel = LOG_LEVEL_SILENT;
else if (levelStr == "error")
requestedLevel = LOG_LEVEL_ERROR;
else if (levelStr == "warning")
requestedLevel = LOG_LEVEL_WARNING;
else if (levelStr == "notice")
requestedLevel = LOG_LEVEL_NOTICE;
else if (levelStr == "trace")
requestedLevel = LOG_LEVEL_TRACE;
else if (levelStr == "verbose")
requestedLevel = LOG_LEVEL_VERBOSE;
else
{
request->send(400, "application/json", "{\"error\":\"Invalid log level\"}");
return;
}
String response;
// Get logs using existing logBuffer implementation in PHApp
std::vector<String> logSnapshot = getLogSnapshot();
// Begin JSON array response
response = "[";
bool first = true;
// Function to escape special characters in JSON
auto escapeJSON = [](const String &str) -> String
{
String result;
for (size_t i = 0; i < str.length(); i++)
{
char c = str.charAt(i);
switch (c)
{
case '"':
result += "\\\"";
break;
case '\\':
result += "\\\\";
break;
case '\b':
result += "\\b";
break;
case '\f':
result += "\\f";
break;
case '\n':
result += "\\n";
break;
case '\r':
result += "\\r";
break;
case '\t':
result += "\\t";
break;
default:
if (c < ' ')
{
char hex[7];
snprintf(hex, sizeof(hex), "\\u%04x", c);
result += hex;
}
else
{
result += c;
}
}
}
return result;
};
// Function to determine log level from a log line
auto getLogLevel = [](const String &line) -> int
{
if (line.startsWith("E:"))
return LOG_LEVEL_ERROR;
if (line.startsWith("W:"))
return LOG_LEVEL_WARNING;
if (line.startsWith("N:"))
return LOG_LEVEL_NOTICE;
if (line.startsWith("T:"))
return LOG_LEVEL_TRACE;
if (line.startsWith("V:"))
return LOG_LEVEL_VERBOSE;
if (line.startsWith("I:"))
return LOG_LEVEL_INFO;
return LOG_LEVEL_VERBOSE; // Default to verbose if no prefix found
};
// Add each log entry to the response if it meets the requested level
for (const auto &logLine : logSnapshot)
{
int lineLevel = getLogLevel(logLine);
if (lineLevel <= requestedLevel)
{
if (!first)
response += ",";
response += "\"" + escapeJSON(logLine) + "\"";
first = false;
}
}
response += "]";
request->send(200, "application/json", response);
}
#endif
+50
View File
@@ -0,0 +1,50 @@
#include "features.h"
#ifdef ENABLE_RS485
#include <Logger.h>
#include <components/RS485.h>
#include <components/OmronE5.h>
#include <components/SAKO_VFD.h>
#include "RS485Devices.h"
#include "PHApp.h"
void RS485Devices::registerApplicationDevices(RS485 *rs485Interface)
{
if (!rs485Interface)
{
Log.errorln(F("RS485Devices: Cannot register devices, RS485 interface is null!"));
return;
}
Log.infoln(F("RS485Devices: Registering %d application RS485 slaves..."), NUM_OMRON_DEVICES);
PHApp *phApp = (PHApp *)rs485Interface->owner;
#ifdef ENABLE_OMRON_E5
for (uint8_t i = 0; i < NUM_OMRON_DEVICES; ++i)
{
uint8_t omronSlaveId = OMRON_E5_SLAVE_ID_BASE + i;
OmronE5 *omronDevice = new OmronE5(omronSlaveId);
omronDevice->owner = rs485Interface;
omronDevice->setup();
if (!rs485Interface->deviceManager.addDevice(omronDevice))
{
Log.errorln(F("RS485Devices: Failed to add OmronE5 Slave %d to manager"), omronSlaveId);
delete omronDevice;
}
else
{
Log.infoln(F("RS485Devices: OmronE5 Slave %d added to manager"), omronSlaveId);
}
}
#endif
#ifdef ENABLE_SAKO_VFD
phApp->vfd_0->owner = rs485Interface;
if (!rs485Interface->deviceManager.addDevice(phApp->vfd_0))
{
Log.errorln(F("RS485Devices: Failed to add SAKO_VFD Slave %d to manager"), MB_SAKO_VFD_SLAVE_ID);
}
#endif
Log.infoln(F("RS485Devices: Finished registering application RS485 slaves."));
}
#endif
+24
View File
@@ -0,0 +1,24 @@
#ifndef RS485_DEVICES_H
#define RS485_DEVICES_H
#include "features.h"
#ifdef ENABLE_RS485
class RS485;
/**
* @brief Utility class to register application-specific RS485 devices.
*/
class RS485Devices {
public:
/**
* @brief Registers all required downstream RS485 slave devices with the main RS485 interface.
*
* Call this function once during setup after the RS485 component has been initialized.
*
* @param rs485Interface A pointer to the initialized RS485 component instance.
*/
static void registerApplicationDevices(RS485* rs485Interface);
};
#endif // ENABLE_RS485
#endif // RS485_DEVICES_H
+114
View File
@@ -0,0 +1,114 @@
#include "config.h"
#ifdef ENABLE_PROFILE_TEMPERATURE // Guard the whole file
#include <components/RestServer.h>
#include "PHApp.h" // Needed for appInstance and tempProfiles
#include "profiles/TemperatureProfile.h" // Needed for TemperatureProfile type
#include <ArduinoJson.h>
#include "Logger.h"
/**
* @brief Handles GET requests to /api/v1/profiles
* Returns a list of available temperature profile slots.
*/
void RESTServer::getProfilesHandler(AsyncWebServerRequest *request)
{
Log.verboseln("REST: getProfilesHandler called");
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
// Use modern syntax: doc[key].to<JsonArray>()
JsonArray profilesArray = doc["profiles"].to<JsonArray>();
#ifdef ENABLE_PROFILE_TEMPERATURE
Log.verboseln(" Found TempProfileManager");
for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i) {
TemperatureProfile *profile = appInstance->tempProfiles[i];
if (profile) {
Log.verboseln(" Processing Profile Slot %d: %s", i, profile->name.c_str());
JsonObject profileObj = profilesArray.add<JsonObject>();
profileObj["slot"] = i;
profileObj["duration"] = profile->getDuration(); // Assuming getDuration returns ms
profileObj["status"] = (int)profile->getCurrentStatus();
profileObj["currentTemp"] = profile->getTemperature(-1); // Get current interpolated temp
// Use modern syntax: profileObj[key].to<JsonArray>()
JsonArray pointsArray = profileObj["controlPoints"].to<JsonArray>();
const TempControlPoint* points = profile->getTempControlPoints();
uint8_t numPoints = profile->getNumTempControlPoints();
Log.verboseln(" Adding %d control points to JSON", numPoints);
for (uint8_t j = 0; j < numPoints; ++j) {
JsonObject pointObj = pointsArray.add<JsonObject>();
pointObj["time"] = points[j].x; // Assuming x is time (scaled 0-1000)
pointObj["temperature"] = points[j].y; // Assuming y is temp (scaled)
}
// Use modern syntax: profileObj[key].to<JsonArray>()
JsonArray targetRegistersArray = profileObj["targetRegisters"].to<JsonArray>();
const std::vector<uint16_t>& targets = profile->getTargetRegisters();
Log.verboseln(" Adding %d target registers to JSON", targets.size());
for(uint16_t targetReg : targets) {
targetRegistersArray.add(targetReg);
}
} else {
Log.warningln(" Profile slot %d is null", i);
}
}
#else
doc["error"] = "Temperature profiles feature not enabled.";
#endif
serializeJson(doc, *response);
request->send(response);
}
/**
* @brief Handles POST requests to /api/v1/profiles/{slot}
* Updates the specified temperature profile using the provided JSON data.
*
* @param request The incoming web request.
* @param json The parsed JSON body from the request.
* @param slot The profile slot number extracted from the URL.
*/
void RESTServer::setProfileHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot)
{
if (slot < 0 || slot >= PROFILE_TEMPERATURE_COUNT) {
Log.warningln("REST: setProfileHandler - Invalid slot number %d provided.", slot);
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid profile slot number\"}");
return;
}
// Check if the profile object exists for this slot
TemperatureProfile* targetProfile = appInstance->tempProfiles[slot];
if (!targetProfile) {
Log.warningln("REST: setProfileHandler - No profile found for slot %d.", slot);
request->send(404, "application/json", "{\"success\":false,\"error\":\"Profile slot not found or not initialized\"}");
return;
}
// Check if the JSON is an object
if (!json.is<JsonObject>()) {
Log.warningln("REST: setProfileHandler - Invalid JSON payload (not an object) for slot %d.", slot);
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid JSON payload: must be an object.\"}");
return;
}
JsonObject jsonObj = json.as<JsonObject>();
// Attempt to load the configuration into the profile object
bool success = targetProfile->load(jsonObj);
if (success) {
Log.infoln("REST: Profile slot %d updated successfully.", slot);
request->send(200, "application/json", "{\"success\":true}");
} else {
Log.errorln("REST: Failed to update profile slot %d from JSON.", slot);
// Provide a more specific error if `load` can indicate the reason
request->send(400, "application/json", "{\"success\":false,\"error\":\"Failed to load profile data. Check format and values.\"}");
}
}
#endif // ENABLE_PROFILE_TEMPERATURE
@@ -0,0 +1,354 @@
#include "components/AmperageBudgetManager.h"
#include <vector> // Ensure vector is included
#include <pid_constants.h>
// Define a threshold (e.g., 5 minutes) after which we consider a device budget-limited
#define ERROR_STATE_THRESHOLD_MS (5 * 60 * 1000)
AmperageBudgetManager::AmperageBudgetManager(uint32_t wattBudget, uint32_t minOnTimeMs, uint32_t maxContiguousRunTimeMs)
: Component("AmperageBudgetManager"),
_wattBudget(wattBudget),
_minOnTimeMs(minOnTimeMs),
_maxContiguousRunTimeMs(maxContiguousRunTimeMs),
_numDevices(0),
_nextDeviceIndex(0),
_lastLoopTime(0) {
// Initialize the managed devices array (optional, as default constructors handle it)
for (uint8_t i = 0; i < MAX_MANAGED_DEVICES; ++i) {
_managedDevices[i] = ManagedDevice{}; // Explicitly default construct
}
}
bool AmperageBudgetManager::addManagedDevice(OmronE5* device, uint8_t priority) {
if (_numDevices >= MAX_MANAGED_DEVICES) {
Log.errorln(F("[%s] Cannot add more devices, manager full (%d)."), _name, MAX_MANAGED_DEVICES);
return false;
}
if (device == nullptr) {
Log.errorln(F("[%s] Cannot add null device pointer."), _name);
return false;
}
// Check if device already added
for(uint8_t i = 0; i < _numDevices; ++i) {
if (_managedDevices[i].device == device) {
Log.warningln(F("[%s] Device already added (Index %d). Ignoring."), _name, i);
return true; // Or false depending on desired behavior
}
}
_managedDevices[_numDevices].device = device;
_managedDevices[_numDevices].state = ManagedState::UNKNOWN;
_managedDevices[_numDevices].originalIndex = _numDevices;
_managedDevices[_numDevices].priority = priority; // Store priority
_managedDevices[_numDevices].lastGrantedTime = 0;
_managedDevices[_numDevices].heatRequestStartTime = 0;
_managedDevices[_numDevices].wantsHeat = false;
// Get initial consumption - might be better to do this in setup/loop
_managedDevices[_numDevices].consumption = device->getConsumption();
Log.traceln(F("[%s] Added device %d (Priority: %d, Consumption: %d W)."), _name, _numDevices, priority, _managedDevices[_numDevices].consumption);
_numDevices++;
return true;
}
bool AmperageBudgetManager::setDevicePriority(OmronE5* device, uint8_t priority) {
if (device == nullptr) return false;
for (uint8_t i = 0; i < _numDevices; ++i) {
if (_managedDevices[i].device == device) {
_managedDevices[i].priority = priority;
Log.infoln(F("[%s] Set priority for device %d to %d."), _name, _managedDevices[i].originalIndex, priority);
return true;
}
}
Log.warningln(F("[%s] setDevicePriority: Device not found."), _name);
return false;
}
short AmperageBudgetManager::setup() {
Log.infoln(F("[%s] Setting up with Budget: %d W, MinOnTime: %d ms, MaxContigRun: %d ms."),
_name, _wattBudget, _minOnTimeMs, _maxContiguousRunTimeMs);
if (_numDevices == 0) {
Log.warningln(F("[%s] No devices added to manage."), _name);
}
// Initial state update could happen here, but loop will handle it
_lastLoopTime = millis();
return 0; // Success
}
short AmperageBudgetManager::loop() {
if (_numDevices == 0) {
return 0; // Nothing to do
}
// Optional: Add a delay or check elapsed time to control loop frequency
// millis_t currentTime = millis();
// if (currentTime - _lastLoopTime < BUDGET_LOOP_INTERVAL_MS) {
// return 0;
// }
// _lastLoopTime = currentTime;
_updateDeviceStates();
_allocateBudget();
return 0; // Success
}
void AmperageBudgetManager::_updateDeviceStates() {
millis_t currentTime = millis();
for (uint8_t i = 0; i < _numDevices; ++i) {
ManagedDevice& managed = _managedDevices[i];
if (!managed.device) continue;
uint16_t pv = 0, sp = 0;
bool pvValid = managed.device->getPV(pv);
bool spValid = managed.device->getSP(sp);
managed.consumption = managed.device->getConsumption(); // Update consumption potentially
if (pvValid && spValid) {
bool previousWantsHeat = managed.wantsHeat;
managed.wantsHeat = (pv < sp);
// Track when heat request starts
if (managed.wantsHeat && !previousWantsHeat) {
managed.heatRequestStartTime = currentTime;
} else if (!managed.wantsHeat) {
managed.heatRequestStartTime = 0; // Reset if no longer wants heat
}
// State transitions based *only* on PV/SP (actual run/stop in _allocateBudget)
if (!managed.wantsHeat) {
// If it was heating or requesting, it now wants to stop.
// Let _allocateBudget handle stopping based on min/max times.
// If it wasn't heating/requesting, ensure it's IDLE.
if (managed.state != ManagedState::HEATING && managed.state != ManagedState::REQUESTING_HEAT) {
managed.state = ManagedState::IDLE;
}
} else { // wantsHeat is true
if (managed.state == ManagedState::IDLE || managed.state == ManagedState::UNKNOWN) {
managed.state = ManagedState::REQUESTING_HEAT;
Log.verboseln(F("[%s] Device %d (Pri: %d) requesting heat (PV:%d < SP:%d)."),
_name, managed.originalIndex, managed.priority, pv, sp);
} else if (managed.state == ManagedState::REQUESTING_HEAT) {
// Check if it's been requesting heat for too long without getting budget
if (managed.heatRequestStartTime > 0 && (currentTime - managed.heatRequestStartTime > ERROR_STATE_THRESHOLD_MS)) {
Log.warningln(F("[%s] Device %d (Pri: %d) timed out requesting heat. Moving to ERROR_BUDGET_LIMITED state."),
_name, managed.originalIndex, managed.priority);
managed.state = ManagedState::ERROR_BUDGET_LIMITED;
}
} else if (managed.state == ManagedState::ERROR_BUDGET_LIMITED) {
// Remains in error state as long as it wants heat
// Could add logic here to potentially retry or alert
}
}
} else {
Log.warningln(F("[%s] Failed to read PV/SP for device %d."), _name, managed.originalIndex);
managed.wantsHeat = false; // Assume doesn't want heat if reading fails
managed.heatRequestStartTime = 0;
// Could potentially set state to UNKNOWN or leave as is.
// Setting to IDLE might be safest if reads fail.
if (managed.state == ManagedState::HEATING || managed.state == ManagedState::ERROR_BUDGET_LIMITED) {
Log.warningln(F("[%s] Stopping device %d due to read failure while %s."),
_name, managed.originalIndex, _getStateStr(managed.state));
managed.device->stop();
}
managed.state = ManagedState::IDLE;
}
}
}
void AmperageBudgetManager::_allocateBudget() {
uint32_t currentWattage = 0;
std::vector<uint8_t> potentialRunnerIndices; // Indices of devices that *could* run
std::vector<uint8_t> willRunIndices; // Indices of devices that *will* run this cycle
millis_t currentTime = millis();
// --- Pass 1: Identify who *must* keep running (min on-time) and who *can* be stopped (max contiguous time) ---
for (uint8_t i = 0; i < _numDevices; ++i) {
ManagedDevice& managed = _managedDevices[i];
if (managed.state == ManagedState::HEATING) {
bool minTimeMet = (currentTime - managed.lastGrantedTime >= _minOnTimeMs);
bool maxTimeExceeded = (_maxContiguousRunTimeMs > 0) && (currentTime - managed.lastGrantedTime >= _maxContiguousRunTimeMs);
if (managed.wantsHeat && !minTimeMet) { // Still wants heat, min time NOT met
// MUST RUN (unless budget forces stop)
if (currentWattage + managed.consumption <= _wattBudget) {
currentWattage += managed.consumption;
willRunIndices.push_back(managed.originalIndex);
Log.verboseln(F("[%s] Device %d (Pri: %d) MUST run (MinOnTime). Budget: %d/%d W."),
_name, managed.originalIndex, managed.priority, currentWattage, _wattBudget);
} else {
Log.warningln(F("[%s] Budget %d W exceeded by forced run of Dev %d (Pri: %d, %d W). Stopping!."),
_name, _wattBudget, managed.originalIndex, managed.priority, managed.consumption);
managed.device->stop();
managed.state = ManagedState::REQUESTING_HEAT; // Still wants heat but lost budget
}
} else if (managed.wantsHeat && maxTimeExceeded) { // Still wants heat, min time MET, max time EXCEEDED
// CAN BE STOPPED for fairness, add to potential runners
Log.verboseln(F("[%s] Device %d (Pri: %d) preempted (MaxContigTime). Adding to potential."),
_name, managed.originalIndex, managed.priority);
potentialRunnerIndices.push_back(managed.originalIndex);
// Don't stop it yet, let Pass 3 handle it if it doesn't get budget again
} else if (managed.wantsHeat) { // Still wants heat, min time MET, max time NOT exceeded (or disabled)
// CAN BE STOPPED, but prefers to run. Add to potential runners.
potentialRunnerIndices.push_back(managed.originalIndex);
} else { // Does NOT want heat anymore (PV >= SP)
// Min time met doesn't matter if it doesn't want heat
Log.verboseln(F("[%s] Device %d (Pri: %d) stopping (PV >= SP)."), _name, managed.originalIndex, managed.priority);
managed.device->stop();
managed.state = ManagedState::IDLE;
}
} else if (managed.state == ManagedState::REQUESTING_HEAT && managed.wantsHeat) {
potentialRunnerIndices.push_back(managed.originalIndex); // Wants heat, wasn't running.
} else if (!managed.wantsHeat && (managed.state == ManagedState::REQUESTING_HEAT || managed.state == ManagedState::HEATING) ) {
// This covers cases where it was requesting but PV rose, or it was heating but PV rose (handled above)
if (managed.state == ManagedState::HEATING) {
// Should have been stopped in the HEATING block above if PV rose
Log.warningln(F("[%s] Device %d logic error? Was HEATING but !wantsHeat here."),_name, managed.originalIndex);
managed.device->stop(); // Ensure stopped
}
managed.state = ManagedState::IDLE;
}
}
// --- Pass 2: Allocate remaining budget using Priority + Round-Robin ---
uint8_t numPotential = potentialRunnerIndices.size();
if (numPotential > 0) {
// Find the highest priority (lowest number) among potential runners
uint8_t highestPriority = std::numeric_limits<uint8_t>::max();
for(uint8_t index : potentialRunnerIndices) {
if (_managedDevices[index].priority < highestPriority) {
highestPriority = _managedDevices[index].priority;
}
}
// Create list of highest priority candidates for this cycle
std::vector<uint8_t> highPriorityCandidates;
for(uint8_t index : potentialRunnerIndices) {
if (_managedDevices[index].priority == highestPriority) {
highPriorityCandidates.push_back(index);
}
}
// Sort the highest priority candidates using round-robin
if (!highPriorityCandidates.empty()) {
std::sort(highPriorityCandidates.begin(), highPriorityCandidates.end(),
[&](uint8_t a, uint8_t b) {
uint8_t effectiveA = (a >= _nextDeviceIndex) ? (a - _nextDeviceIndex) : (a + _numDevices - _nextDeviceIndex);
uint8_t effectiveB = (b >= _nextDeviceIndex) ? (b - _nextDeviceIndex) : (b + _numDevices - _nextDeviceIndex);
return effectiveA < effectiveB;
});
Log.verboseln(F("[%s] High Priority (%d) Candidates (RR start %d): "), _name, highestPriority, _nextDeviceIndex);
// for(uint8_t idx : highPriorityCandidates) { Log.verboseln(F(" %d"), idx); } Log.verboseln("");
// Try allocating budget to these high-priority, round-robin sorted candidates
for (uint8_t originalIdx : highPriorityCandidates) {
ManagedDevice& potential = _managedDevices[originalIdx];
if (currentWattage + potential.consumption <= _wattBudget) {
// Check if not already running (from Pass 1 - min on time)
bool alreadyRunning = false;
for(uint8_t runningIdx : willRunIndices) { if (runningIdx == originalIdx) { alreadyRunning = true; break; } }
if (!alreadyRunning) {
currentWattage += potential.consumption;
willRunIndices.push_back(originalIdx);
Log.verboseln(F("[%s] Device %d (Pri: %d) WILL run. Budget: %d/%d W."),
_name, originalIdx, potential.priority, currentWattage, _wattBudget);
}
} else {
Log.verboseln(F("[%s] Device %d (Pri: %d) cannot run (Budget %d/%d W, needs %d W)."),
_name, originalIdx, potential.priority, currentWattage, _wattBudget, potential.consumption);
}
}
}
// Note: This implementation only allocates to the *highest* priority group requesting heat each cycle.
// If budget remains after satisfying the highest priority group, lower priority groups
// will only get a chance in subsequent cycles when they become the highest priority *requesting* group.
}
// --- Pass 3: Apply changes - Start/Stop devices based on willRun list ---
for (uint8_t i = 0; i < _numDevices; ++i) {
ManagedDevice& managed = _managedDevices[i];
bool shouldBeRunning = false;
for (uint8_t runIdx : willRunIndices) {
if (managed.originalIndex == runIdx) {
shouldBeRunning = true;
break;
}
}
if (shouldBeRunning) {
if (managed.state != ManagedState::HEATING) {
Log.infoln(F("[%s] Starting device %d (Pri: %d)."), _name, managed.originalIndex, managed.priority);
if (managed.device->run()) { // Check if run command succeeded (optional)
managed.state = ManagedState::HEATING;
managed.lastGrantedTime = currentTime;
} else {
Log.errorln(F("[%s] Failed to execute run() command for device %d!"), _name, managed.originalIndex);
// Keep state as REQUESTING_HEAT or move to UNKNOWN/ERROR?
}
}
// If it was already HEATING, just let it continue (lastGrantedTime might update implicitly if needed later)
} else { // Should NOT be running
if (managed.state == ManagedState::HEATING) {
Log.infoln(F("[%s] Stopping device %d (Pri: %d, Budget revoked/expired/preempted)."), _name, managed.originalIndex, managed.priority);
if (managed.device->stop()) { // Check if stop command succeeded (optional)
managed.state = managed.wantsHeat ? ManagedState::REQUESTING_HEAT : ManagedState::IDLE;
} else {
Log.errorln(F("[%s] Failed to execute stop() command for device %d!"), _name, managed.originalIndex);
// State remains HEATING, potentially problematic. Maybe add an error state?
}
} else if (managed.state == ManagedState::REQUESTING_HEAT && !managed.wantsHeat) {
managed.state = ManagedState::IDLE;
}
}
}
// Update round-robin index for next cycle
if (_numDevices > 0) {
_nextDeviceIndex = (_nextDeviceIndex + 1) % _numDevices;
}
}
short AmperageBudgetManager::info() {
Log.notice(F("[%s] Budget:%d W, MinOn:%dms, MaxRun:%dms, Devs:%d/%d, RR_Idx:%d\n"),
_name, _wattBudget, _minOnTimeMs, _maxContiguousRunTimeMs, _numDevices, MAX_MANAGED_DEVICES, _nextDeviceIndex);
uint32_t currentWattage = 0;
uint32_t potentialWattage = 0;
for (uint8_t i = 0; i < _numDevices; ++i) {
const ManagedDevice& managed = _managedDevices[i];
if (managed.state == ManagedState::HEATING) {
currentWattage += managed.consumption;
}
if (managed.wantsHeat) {
potentialWattage += managed.consumption;
}
millis_t timeSinceGrant = (managed.lastGrantedTime == 0) ? 0 : (millis() - managed.lastGrantedTime);
millis_t timeSinceRequest = (managed.heatRequestStartTime == 0) ? 0 : (millis() - managed.heatRequestStartTime);
Log.notice(F(" Dev %d: Pri=%d, State=%s, Wants=%T, Cons=%d W, LastRun=%lums ago, ReqFor=%lums\n"),
managed.originalIndex,
managed.priority,
_getStateStr(managed.state),
managed.wantsHeat,
managed.consumption,
timeSinceGrant,
managed.wantsHeat ? timeSinceRequest : 0 // Only show request time if currently wants heat
);
}
Log.notice(F(" Current Wattage: %d W | Potential Demand: %d W\n"), currentWattage, potentialWattage);
return 0;
}
const char* AmperageBudgetManager::_getStateStr(ManagedState state) {
switch (state) {
case ManagedState::UNKNOWN: return "UNKNOWN";
case ManagedState::IDLE: return "IDLE";
case ManagedState::REQUESTING_HEAT: return "REQUESTING_HEAT";
case ManagedState::HEATING: return "HEATING";
case ManagedState::ERROR_BUDGET_LIMITED: return "ERR_BUDGET_LTD"; // Added new state string
default: return "INVALID";
}
}
@@ -0,0 +1,119 @@
#ifndef AMPERAGE_BUDGET_MANAGER_H
#define AMPERAGE_BUDGET_MANAGER_H
#include "config.h"
#include <Component.h>
#include <ArduinoLog.h>
#include <vector>
#include <numeric>
#include <algorithm>
#include <limits>
#include "components/OmronE5.h"
// Define the maximum number of devices this manager can handle.
// Adjust based on expected system size and memory constraints.
#ifndef MAX_MANAGED_DEVICES
#define MAX_MANAGED_DEVICES 8
#endif
// Ensure these are defined in your config.h or secrets.h
#ifndef DEFAULT_POWER_BUDGET_WATTS
#define DEFAULT_POWER_BUDGET_WATTS 10000
#endif
#ifndef DEFAULT_MIN_ON_TIME_MS
#define DEFAULT_MIN_ON_TIME_MS 5000
#endif
#ifndef DEFAULT_MAX_CONTIGUOUS_RUN_TIME_MS
#define DEFAULT_MAX_CONTIGUOUS_RUN_TIME_MS 0 // 0 means disabled by default
#endif
// Define default priority - lower number is higher priority
#define DEFAULT_DEVICE_PRIORITY 10
class AmperageBudgetManager : public Component {
public:
// Enum to track the state of managed devices from the budget manager's perspective.
enum class ManagedState {
UNKNOWN, // Initial state before first check
IDLE, // PV >= SP, not requesting heat
REQUESTING_HEAT, // PV < SP, wants to heat but might not have budget yet
HEATING, // PV < SP and currently allocated budget (run() called)
ERROR_BUDGET_LIMITED // PV < SP for extended period, but consistently denied budget
};
// Structure to hold information about each managed device.
struct ManagedDevice {
OmronE5* device = nullptr;
ManagedState state = ManagedState::UNKNOWN;
uint8_t originalIndex = 0; // Index when added, for stable sorting
uint8_t priority = DEFAULT_DEVICE_PRIORITY; // Lower value = higher priority
uint32_t lastGrantedTime = 0; // millis() when budget was last granted
uint32_t heatRequestStartTime = 0; // millis() when wantsHeat first became true
bool wantsHeat = false; // Cached desire based on PV/SP
uint32_t consumption = 0; // Cached consumption
};
/**
* @brief Constructor for the AmperageBudgetManager.
* @param wattBudget The maximum total wattage allowed for active heaters.
* @param minOnTimeMs Minimum time (milliseconds) a device should run once started.
* @param maxContiguousRunTimeMs Max time (ms) a device can run continuously before being preempted for fairness (0=disabled).
*/
AmperageBudgetManager(uint32_t wattBudget = DEFAULT_POWER_BUDGET_WATTS,
uint32_t minOnTimeMs = DEFAULT_MIN_ON_TIME_MS,
uint32_t maxContiguousRunTimeMs = DEFAULT_MAX_CONTIGUOUS_RUN_TIME_MS);
virtual ~AmperageBudgetManager() = default;
/**
* @brief Registers an OmronE5 device to be managed by the budget controller.
* @param device Pointer to the OmronE5 instance.
* @param priority Priority for budget allocation (lower number is higher priority).
* @return True if successfully added, false if the manager is full.
*/
bool addManagedDevice(OmronE5* device, uint8_t priority = DEFAULT_DEVICE_PRIORITY);
/**
* @brief Sets the priority for an already added device.
* @param device Pointer to the OmronE5 instance.
* @param priority The new priority value.
* @return True if device was found and priority set, false otherwise.
*/
bool setDevicePriority(OmronE5* device, uint8_t priority);
// --- Component Interface ---
virtual short setup() override;
virtual short loop() override;
virtual short info() override; // For printing status
private:
uint32_t _wattBudget;
uint32_t _minOnTimeMs; // Minimum time to keep a heater on once started
uint32_t _maxContiguousRunTimeMs; // Max time to run before preemption for fairness
ManagedDevice _managedDevices[MAX_MANAGED_DEVICES];
uint8_t _numDevices;
uint8_t _nextDeviceIndex; // Index for round-robin starting point
millis_t _lastLoopTime;
String _name;
// --- Internal Helper Methods ---
/**
* @brief Updates the state (wantsHeat, consumption) of all managed devices.
*/
void _updateDeviceStates();
/**
* @brief Allocates the wattage budget to devices requesting heat.
* Implements the round-robin scheduling with preemption and min-on-time.
*/
void _allocateBudget();
/**
* @brief Gets the state of a managed device as a string.
* @param state The state enum value.
* @return A const char* representation of the state.
*/
static const char* _getStateStr(ManagedState state);
};
#endif // AMPERAGE_BUDGET_MANAGER_H
+191
View File
@@ -0,0 +1,191 @@
#ifndef CONFIG_MODBUS_H
#define CONFIG_MODBUS_H
//////////////////////////////////////////////////////////////////
//
// Commons
//
#define MB_PRINT_ERRORS false
#define MAX_MODBUS_COMPONENTS 256
#define MB_RTU_PROCESS_INTERVAL_MS 20
//////////////////////////////////////////////////////////////////
//
// SYNC Settings
//
#define TEMPERATURE_PROFILE_SYNC_INTERVAL_MS 500
//////////////////////////////////////////////////////////////////
//
// Modbus RTU
//
// Serial configuration
#define MODBUS_SERIAL_MODE SERIAL_8N1
#define MODBUS_SERIAL_TIMEOUT 1500 // 3 seconds timeout for Modbus client
#define MAX_MODBUS_SLAVES 16
#define MAX_ADDRESSES_PER_SLAVE 64
#define MAX_PENDING_OPERATIONS 64
#define MAX_HIGH_PRIORITY_OPERATIONS 32
#define MAX_TCP_MAPPINGS 256
#define MAX_RTU_MAPPINGS 256
// Delay constants - these should be avoided when possible
#define DELAY_SERIAL_INIT 500 // ms to wait for serial initialization
#define DELAY_CLIENT_INIT 1000 // ms to wait for client initialization
#define DELAY_RESET_PAUSE 200 // ms to pause during reset
#define RS485_LOOP_INTERVAL_MS 10 // Interval in milliseconds for RS485 loop processing
#define MAX_READ_BLOCKS 4
#define PRIORITY_HIGHEST 100
#define PRIORITY_HIGH 80
#define PRIORITY_MEDIUM 60
#define PRIORITY_LOW 40
#define PRIORITY_LOWEST 20
#define MAX_MODBUS_DEVICES 16
// Maximum registers per device
#define MAX_INPUT_REGISTERS 10
#define MAX_OUTPUT_REGISTERS 10
// Modbus coil values
#define COIL_ON 0xFF00 // Proper Modbus value for coil ON
#define COIL_OFF 0x0000 // Proper Modbus value for coil OFF
// Operation timing
#define OPERATION_TIMEOUT 5500
// Define pins for RS485 communication
// Define the actual GPIO numbers used for RX and TX on your ESP32 board for Serial1
#define TXD1 17
#define RXD1 18
#define RXD1_PIN RXD1
#define TXD1_PIN TXD1
// Define the HardwareSerial instance to use for RS485
#define RS485_SERIAL_PORT Serial1 // Use Serial1
// Try with an explicit DE/RE pin - GPIO_NUM_4 is often used for this
#define REDEPIN_MODBUS GPIO_NUM_4
// Baudrate for RS485/Modbus communication
#define MB_RTU_BAUDRATE 9600
// Increase queue size to avoid queue full errors
#define MODBUS_QUEUE_SIZE 256
// Define maximum array sizes for fixed arrays if not already defined
#ifndef MAX_ADDRESSES_PER_SLAVE
#define MAX_ADDRESSES_PER_SLAVE 64
#endif
// Add constants from ModbusRTU.h
#define MAX_PENDING_OPERATIONS 64
#define MAX_HIGH_PRIORITY_OPERATIONS 32
#define MAX_RETRIES 2
// Define flags for ModbusOperation status
#define OP_USED_BIT 0 // Bit for Used flag
#define OP_HIGH_PRIORITY_BIT 1 // Bit for High Priority flag
#define OP_IN_PROGRESS_BIT 2 // Bit for In Progress flag
#define OP_BROADCAST_BIT 3 // Bit for Broadcast flag
#define OP_SYNCHRONIZED_BIT 4 // Bit for Synchronized flag
// Define flags for ModbusValueEntry status - Replaced with bit positions
// #define VALUE_FLAG_USED 0x01
// #define VALUE_FLAG_SYNCHRONIZED 0x02
#define VALUE_USED_BIT 0
#define VALUE_SYNCHRONIZED_BIT 1
// Define flags for ModbusReadBlock status
#define BLOCK_USED_BIT 0
//////////////////////////////////////////////////////////////////
//
// Modbus TCP Port
//
#define MODBUS_PORT 502 // Standard Modbus TCP port
//////////////////////////////////////////////////////////////////
//
// System Level Registers - TCP
#define MAX_REGISTERS 125
#define MB_ADDR_SYSTEM_ERROR 0 // R - System-wide error code
#define MB_ADDR_ECHO_TEST 8 // R/W - Echo test
#define MB_ADDR_APP_STATE 9 // R - Application state (see PHApp::APP_STATE)
#define MB_ADDR_RESET_CONTROLLER 100 // W - Write any value to reset
#define MB_ADDR_SYSTEM_END 10
// Auxiliary Registers
#define MB_ADDR_AUX_0 MB_ADDR_SYSTEM_END
#define MB_ADDR_AUX_1 MB_ADDR_AUX_0 + 4
#define MB_ADDR_AUX_2 MB_ADDR_AUX_0 + 8
#define MB_ADDR_AUX_3 MB_ADDR_AUX_0 + 12
#define MB_ADDR_AUX_4 MB_ADDR_AUX_0 + 16
#define MB_ADDR_AUX_5 MB_ADDR_AUX_0 + 20
#define MB_ADDR_AUX_6 MB_ADDR_AUX_0 + 24
#define MB_ADDR_AUX_7 MB_ADDR_AUX_0 + 28
#define MB_ADDR_AUX_8 MB_ADDR_AUX_0 + 32
#define MB_ADDR_AUX_9 MB_ADDR_AUX_0 + 36
#define MB_COIL_RELAY_0 51 // R/W - Address for Relay with ID COMPONENT_KEY_MB_RELAY_0
#define MB_COIL_RELAY_1 52 // R/W - Address for Relay with ID COMPONENT_KEY_MB_RELAY_1
#define MB_COIL_RELAY_2 53 // R/W - Address for Relay with ID COMPONENT_KEY_MB_RELAY_2
#define MB_IREG_ANALOG_0 400 // R - Address for Analog Input 0
#define MB_IREG_ANALOG_1 401 // R - Address for Analog Input 1
#define MB_IREG_ANALOG_2 402 // R - Address for Analog Input 2
#define MB_IREG_3POS_SWITCH_0 501 // R - Address for 3-Pos Switch 0
#define MB_IREG_3POS_SWITCH_1 502 // R - Address for 3-Pos Switch 1
#define MB_ADDR_AUX_END MB_ADDR_AUX_0 + 40
#define MB_HREG_PID_0_PV 100 // R - PID 0 Process Value
#define MB_HREG_PID_0_SP 101 // R/W - PID 0 Setpoint
#define MB_HREG_PID_0_STATE 102 // R - PID 0 State
#define MB_HREG_PID_1_PV 103 // R - PID 1 Process Value
#define MB_HREG_PID_1_SP 104 // R/W - PID 1 Setpoint
#define MB_HREG_PID_1_STATE 105 // R - PID 1 State
// Example: Battle Test Registers
#define MB_HREG_BATTLE_COUNTER 20 // R/W - Counter for battle test
#define MB_HREG_BATTLE_TIMESTAMP 21 // R - Timestamp for battle test
// Example: Modbus Client Tracking Registers
#define MB_HREG_CLIENT_COUNT 22 // R - Current number of connected clients
#define MB_HREG_CLIENT_MAX 23 // R - Maximum number of clients seen
#define MB_HREG_CLIENT_TOTAL 24 // R - Total client connections since start
// Monitoring & Feedback Addresses (placeholder addresses, verify usage)
// These were originally calculated relative to other offsets in enums.h
// Define them explicitly here for now.
#define MB_MONITORING_STATUS_FEEDBACK_0 701 // R? R/W? - Address for StatusLight 0
#define MB_MONITORING_STATUS_FEEDBACK_1 702 // R? R/W? - Address for StatusLight 1
////////////////////////////////////////////////////////////////////////////////
//
// Omron Pids - E5.x series - Modbus interface
//
#define OMRON_MB_TCP_OFFSET 1000
////////////////////////////////////////////////////////////////////////////////
//
// Built-in PIDs
//
#define MB_HREG_PID_2_BASE_ADDRESS 6100
#define PID_2_REGISTER_COUNT 12
#endif // CONFIG_MODBUS_H
+263
View File
@@ -0,0 +1,263 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <Arduino.h>
#include <stdint.h>
#include "config_adv.h"
////////////////////////////////////////////////////////////////////////////////
//
// Component IDs
//
typedef enum COMPONENT_KEY
{
COMPONENT_KEY_RELAY_0 = 300,
COMPONENT_KEY_RELAY_1 = 301,
COMPONENT_KEY_RELAY_2 = 302,
COMPONENT_KEY_RELAY_3 = 303,
COMPONENT_KEY_RELAY_4 = 304,
COMPONENT_KEY_RELAY_5 = 305,
COMPONENT_KEY_PID_0 = 100,
COMPONENT_KEY_PID_1 = 101,
COMPONENT_KEY_PID_2 = 102,
COMPONENT_KEY_ANALOG_0 = 350,
COMPONENT_KEY_ANALOG_1 = 351,
COMPONENT_KEY_ANALOG_2 = 352,
COMPONENT_KEY_ANALOG_3POS_SWITCH_0 = 420,
COMPONENT_KEY_ANALOG_3POS_SWITCH_1 = 421,
COMPONENT_KEY_ANALOG_LEVEL_SWITCH_0 = 450,
COMPONENT_KEY_ANALOG_LEVEL_SWITCH_1 = 451,
COMPONENT_KEY_JOYSTICK_0 = 500,
COMPONENT_KEY_STEPPER_0 = 601,
COMPONENT_KEY_STEPPER_1 = 602,
COMPONENT_KEY_PID = 620,
COMPONENT_KEY_EXTRUDER = 650,
COMPONENT_KEY_PLUNGER = 670,
COMPONENT_KEY_FEEDBACK_0 = 701,
COMPONENT_KEY_FEEDBACK_1 = 702,
COMPONENT_KEY_SAKO_VFD = 750,
COMPONENT_KEY_RS485_TESTER = 800,
COMPONENT_KEY_RS485 = 801,
COMPONENT_KEY_GPIO_MAP = 810,
COMPONENT_KEY_REST_SERVER = 900,
COMPONENT_KEY_PROFILE_START = 910,
COMPONENT_KEY_END = 1000,
} COMPONENT_KEY;
////////////////////////////////////////////////////////////////////////////////
//
// Platform
// Automatic platform detection
#if defined(ARDUINO_PORTENTA_H7_M7)
#define PLATFORM_PORTENTA_H7_M7
#define BOARD_NAME "Portenta H7 M7"
#include <Arduino_MachineControl.h>
using namespace machinecontrol;
#elif defined(ARDUINO_CONTROLLINO_MEGA)
#define PLATFORM_CONTROLLINO_MEGA
#define BOARD_NAME "Controllino Mega"
#include <Controllino.h>
#elif defined(ESP32)
#define PLATFORM_ESP32
#define BOARD_NAME "ESP32"
#elif defined(ARDUINO_AVR_UNO) // Detect Arduino Uno
#define PLATFORM_ARDUINO_UNO
#define BOARD_NAME "Arduino Uno"
#else
#error "Unsupported platform"
#endif
////////////////////////////////////////////////////////////////////////////////
//
// Debugging
#ifndef DISABLE_SERIAL_LOGGING
// Serial Bridge Debugging Switches
#define BRIDGE_DEBUG_REGISTER
#define BRIDGE_DEBUG_CALL_METHOD
// Serial Command Messaging Debugging Switches
#define DEBUG_MESSAGES_PARSE
#endif
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// App Features
// When printing Modbus registers (via serial), print register descriptions
// #define HAS_MODBUS_REGISTER_DESCRIPTIONS 1
///////////////////////////////////////////////////////
//
// Status Feedback Settings
#define STATUS_WARNING_PIN GPIO_PIN_CH6
#define STATUS_ERROR_PIN GPIO_PIN_CH5
#define STATUS_BLINK_INTERVAL 800
///////////////////////////////////////////////////////
//
// Serial Port Settings
/** @brief Serial port baud rate */
#define SERIAL_BAUD_RATE 115200
/** @brief Parse commands every 100ms */
#define SERIAL_COMMAND_PARSE_INTERVAL 100
////////////////////////////////////////////////////////////////
//
// Auxiliary Settings : Relays
#ifdef PLATFORM_ESP32
// Waveshare - CH6 Board
#define GPIO_PIN_CH1 1 // CH1 Control GPIO
#define GPIO_PIN_CH2 2 // CH2 Control GPIO
#define GPIO_PIN_CH3 41 // CH3 Control GPIO
#define GPIO_PIN_CH4 42 // CH4 Control GPIO
#define GPIO_PIN_CH5 45 // CH5 Control GPIO
#define GPIO_PIN_CH6 46 // CH6 Control GPIO
#define GPIO_PIN_RGB 38 // RGB Control GPIO
#define GPIO_PIN_Buzzer 21 // Buzzer Control GPIO
#define AUX_RELAY_0 GPIO_PIN_CH6
#define AUX_RELAY_1 GPIO_PIN_CH5
#endif
////////////////////////////////////////////////////////////////
//
// Auxiliary Settings : Analog Inputs (POTs)
//
// #define MB_ANALOG_0 15
// #define MB_ANALOG_1 7
#define POT_RAW_MAX_VALUE 4095 // Max raw ADC value
#define POT_SCALED_MAX_VALUE 100 // Max scaled value (0-100)
//#define MB_GPIO_MB_MAP_7 7
//#define MB_GPIO_MB_MAP_15 15
////////////////////////////////////////////////////////////////
//
// Auxiliary Settings : Analog Level Switch
//
#ifdef PLATFORM_ESP32
//#define PIN_ANALOG_LEVEL_SWITCH_0 34 // <<< CHOOSE YOUR ADC PIN
#endif
/** @brief Analog Level Switch 0 */
#define ID_ANALOG_LEVEL_SWITCH_0 200
#define ALS_0_NUM_LEVELS 4 // Number of slots
#define ALS_0_ADC_STEP 800 // ADC counts per slot
#define ALS_0_ADC_OFFSET 200 // ADC value for the start of the first slot
#define ALS_0_MB_ADDR 60 // Modbus base address (Uses 60-65 for 4 slots)
////////////////////////////////////////////////////////////////
//
// Auxiliary Settings : LED Feedback
//
//#define PIN_LED_FEEDBACK_0 1 // <<< CHOOSE NEOPIXEL DATA PIN
#define LED_PIXEL_COUNT_0 1
#define ID_LED_FEEDBACK_0 210
#define LED_FEEDBACK_0_MB_ADDR 70
// #define LED_UPDATE_INTERVAL_MS 20
////////////////////////////////////////////////////////////////
//
// Auxiliary Settings : Analog Switch Inputs (3 Position Switches)
//
#ifdef PLATFORM_ESP32
// #define AUX_ANALOG_3POS_SWITCH_0 34
// #define AUX_ANALOG_3POS_SWITCH_1 35
#endif
////////////////////////////////////////////////////////////////
//
// Auxiliary Settings : Joystick
//
#ifdef PLATFORM_ESP32
#define PIN_JOYSTICK_UP 47 // UP direction pin
#define PIN_JOYSTICK_DOWN 48 // DOWN direction pin
#define PIN_JOYSTICK_LEFT 40 // LEFT direction pin
#define PIN_JOYSTICK_RIGHT 39 // RIGHT direction pin
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Network
//
#define NETWORK_CONFIG_FILENAME "/network.json"
// Static IP Addresses for STA mode (and STA part of AP_STA)
static IPAddress local_IP(192, 168, 1, 250);
static IPAddress gateway(192, 168, 1, 1);
static IPAddress subnet(255, 255, 0, 0);
static IPAddress primaryDNS(8, 8, 8, 8);
static IPAddress secondaryDNS(8, 8, 4, 4);
#define WIFI_SSID "Livebox6-EBCD"
#define WIFI_PASSWORD "c4RK35h4PZNS"
// AP_STA Mode Configuration
// To enable AP_STA mode, define ENABLE_AP_STA.
// If ENABLE_AP_STA is defined, the device will act as both an Access Point
// and a WiFi client (Station) simultaneously.
// The AP will have its own SSID and IP configuration.
// The STA part will connect to the WiFi network defined by WIFI_SSID and WIFI_PASSWORD.
#define ENABLE_AP_STA // Uncomment to enable AP_STA mode
#ifdef ENABLE_AP_STA
#define AP_SSID "PolyMechAP"
#define AP_PASSWORD "poly1234" // Password must be at least 8 characters
static IPAddress ap_local_IP(192, 168, 4, 1);
static IPAddress ap_gateway(192, 168, 4, 1); // Typically the same as ap_local_IP for the AP
static IPAddress ap_subnet(255, 255, 255, 240); // Changed to .240 (/28 subnet)
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Main switches : features
#define ENABLE_WIFI
#define ENABLE_WEBSERVER
#define ENABLE_MODBUS_TCP
// #define ENABLE_JOYSTICK (4P Joystick)
//#define ENABLE_PROFILE_TEMPERATURE
//#define PROFILE_TEMPERATURE_COUNT 1
// #define ENABLE_RS485
// #define ENABLE_RS485_DEVICES
// #define ENABLE_STATUS
// #define ENABLE_PID
// CPU & Memory Profiling
// #define ENABLE_PROFILER
// #define ENABLE_RELAYS
// Experimental : Modbus Scripting
#define ENABLE_MB_SCRIPT
// #define ENABLE_EXTRUDER
// #define ENABLE_PLUNGER
// #define ENABLE_SAKO_VFD
// #define MB_SAKO_VFD_SLAVE_ID 10
// #define MB_SAKO_VFD_READ_INTERVAL 200
#define ENABLE_OMRON_E5
#ifndef NUM_OMRON_DEVICES
#define NUM_OMRON_DEVICES 0
#define OMRON_E5_SLAVE_ID_BASE 1
#endif
#if (defined(ENABLE_WIFI) && defined(ENABLE_MODBUS_TCP))
#ifndef ENABLE_WEBSERVER
#endif
#endif
////////////////////////////////////////////////////////////////
//
// Web Server Features
//
#define ENABLE_REST_SERVER
#define ENABLE_LITTLEFS
#define ENABLE_WEBSOCKET
#define ENABLE_WEBSERVER_WIFI_SETTINGS
#endif
+132
View File
@@ -0,0 +1,132 @@
#ifndef CONFIG_ADV_H
#define CONFIG_ADV_H
////////////////////////////////////////////////////////////////////////////////
//
// Power settings
// optional current sensor to validate primary power is there
// #define POWER_CSENSOR_PRIMARY CONTROLLINO_A15
// optional current sensor to validate primary power is there
// #define POWER_CSENSOR_SECONDARY CONTROLLINO_A14
/////////////////////////////////////////////////////////////
//
// Motor load settings, this requires a current sensor or can be
// taken from the VFD's output.
// the interval to read the current
#define MOTOR_LOAD_READ_INTERVAL 100
// the current measured when the motor runs idle, min - max range
#define MOTOR_IDLE_LOAD_RANGE_MIN 5
#define MOTOR_IDLE_LOAD_RANGE_MAX 20
// the current measured when the motor is under load, min - max range
#define MOTOR_LOAD_RANGE_MIN 20
#define MOTOR_LOAD_RANGE_MAX 60
// the current measured when the motor is overloaded, min - max range
#define MOTOR_OVERLOAD_RANGE_MIN 80
#define MOTOR_OVERLOAD_RANGE_MAX 800
// #define MOTOR_MIN_DT 2500
/////////////////////////////////////////////////////////////
//
// Error codes
//
#define E_MSG_OK "Ok"
// power failures
#define E_POWER_PRIM_ON 145 // Power is on whilst it shouldn't be
#define E_POWER_SEC_ON 147 // Power is on whilst it shouldn't be
#define E_POWER 150 // Nothing is online
#define E_VFD_OFFLINE 0x102 // VFD should be online (Used 0x102 directly)
// sensor failures
#define E_VFD_CURRENT 200 // VFD current abnormal: below or above average
#define E_OPERATING_SWITCH 220 // Operating switch invalid value
////////////////////////////
//
// Sub system failures
//
// bridge
#define E_BRIDGE_START 3000 // base offset for custom bridge errors
#define E_BRIDGE_CUSTOM(A) E_BRIDGE_START+A // Custom bridge error
#define E_BRIDGE_PARITY E_BRIDGE_CUSTOM(1) // @todo, parity check failure
#define E_BRIDGE_CRC E_BRIDGE_CUSTOM(2) // @todo, crc failure
#define E_BRIDGE_FLOOD E_BRIDGE_CUSTOM(3) // @todo, msg queue
// extrusion
#define E_EX_BASE 4000 // base offset extruder
#define E_EX_CUSTOM(A) E_EX_BASE+A // Custom bridge error
///////////////////////////////////////////////////////////////////////
//
// I/O Advanced Settings - Board/Platform specific
#define ANALOG_POT_READ_INTERVAL 5 // The interval to read the analog switch
#define ANALOG_SWITCH_READ_INTERVAL 20 // The interval to read the analog switch
#define ANALOG_INPUT_MAX_LEVEL_0 750 // The trigger value to detect an analog input value as HIGH or ON
#define ANALOG_INPUT_MAX_LEVEL_1 810 // The maximum value for the analog input (POT)
#define ANALOG_INPUT_THRESHOLD_1 500 // The trigger value to detect an analog input value as HIGH or ON
#define ANALOG_INPUT_MIN_THRESHOLD_0 700 // Minimum threshold for 3Pos switches
#define ANALOG_INPUT_MIN_DT_0 3 // Minimum difference to detect a change in the analog input
///////////////////////////////////////////////////////////////////////
//
// Stepper Advanced Settings
//
#define STEPPER_MAX_SPEED_0 100
#define STEPPER_MODUBUS_RANGE 4
#define STEPPER_DEFAULT_SPEED_0 50
#define STEPPER_DEFAULT_DIR_0 0
#define STEPPER_PULSE_WIDTH_0 20
#define STEPPER_OVERLOAD_THRESHOLD_0 800
#define STEPPER_PULSE_WIDTH_1 20
#define STEPPER_DEFAULT_SPEED_1 50
#define STEPPER_DEFAULT_DIR_1 0
#define STEPPER_MAX_SPEED_1 100
#define ANALOG_3POS_SWITCH_LEFT_RANGE 50
#define ANALOG_3POS_SWITCH_CENTER_RANGE 100
#define ANALOG_3POS_SWITCH_RIGHT_RANGE 100
#define ANALOG_3POS_SWITCH_DEBOUNCE 500
// MotorLoad : Number of samples to make an average of
#define MOTOR_LOAD_SAMPLES 10
#define MOTOR_LOAD_ALARM_LIMIT 900 // 0-1000
#define MOTOR_LOAD_UPDATE_INTERVAL 50 // ms
//////////////////////////////////////////////////////////////////////////////////////////////////
//
// Error codes
// Common error codes
#define E_INVALID_PARAMETERS 0x1001
#define E_POWER_PRIM_OFF 0x102 // Main power problem
#define E_POWER_SEC_OFF 0x103 // Secondary power problem
#define E_POWER_SEC_LOW 0x104 // Second power problem, low batteries
#define E_POWER_PRIM_LOW 0x105 // Main power problem, voltage low
#define E_POWER_PRIM_HIGH 0x106 // Main power problem, voltage high
// Removed E_VFD_* error codes
// Thermal Faults
#define E_THERM_AMBIENT_HIGH 0x130 // Ambient temperature high
#define E_THERM_MOTOR_HIGH 0x131 // motor temperature high
#define E_THERM_CONTROL_HIGH 0x132 // Control box over heating
#define E_CURRENT 200 // abnormal current: below or above average
#endif
+100
View File
@@ -0,0 +1,100 @@
#ifndef FEATURES_H
#define FEATURES_H
#include "config.h"
#define HAS_LOGGER
#ifdef ENABLE_STATUS
#include <components/StatusLight.h>
#endif
#if (defined(AUX_ANALOG_3POS_SWITCH_0) && defined(AUX_ANALOG_3POS_SWITCH_1))
#include <components/3PosAnalog.h>
#endif
#ifdef ENABLE_RELAYS
#if defined(AUX_RELAY_0) || defined(AUX_RELAY_1) || defined(AUX_RELAY_2) || defined(AUX_RELAY_3)
#include <components/Relay.h>
#endif
#endif
#if defined(MB_ANALOG_0) || defined(MB_ANALOG_1) || defined(MB_ANALOG_2)
#include <components/POT.h>
#endif
#if defined(MB_GPIO_MB_MAP_7)
#include <components/GPIO.h>
#endif
#ifdef PIN_ANALOG_LEVEL_SWITCH_0
#include <components/AnalogLevelSwitch.h>
#endif
// For LEDFeedback
#ifdef PIN_LED_FEEDBACK_0 // Include if configured
#include <components/LEDFeedback.h>
#endif
#ifdef ENABLE_JOYSTICK
#include <components/Joystick.h>
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Network
//
#ifdef ENABLE_MODBUS_TCP
#include <ModbusServerTCPasync.h>
#include <modbus/ModbusTCP.h>
#endif
#ifdef ENABLE_MODBUS_TCP
#include <WiFi.h>
#endif
#ifdef ENABLE_REST_SERVER
#include <components/RestServer.h>
#endif
#ifdef ENABLE_LITTLEFS
#include <LittleFS.h>
#endif
#ifdef ENABLE_PID
#include "./pid/PIDController.h"
#endif
#ifdef ENABLE_PROFILER
#ifdef PLATFORM_ESP32
#include <esp_cpu.h>
#endif
#endif
#ifdef NUM_OMRON_DEVICES
#include <components/OmronE5.h>
#endif
#ifdef ENABLE_RS485
#include <components/RS485.h>
#endif
#ifdef ENABLE_PROFILE_TEMPERATURE
#include <profiles/TemperatureProfile.h>
#endif
#ifdef ENABLE_SAKO_VFD
#include <components/SAKO_VFD.h>
#endif
#ifdef ENABLE_EXTRUDER
#include <components/Extruder.h>
#endif
#ifdef ENABLE_PLUNGER
#include <components/Plunger.h>
#endif
#include <Vector.h>
#endif
+30
View File
@@ -0,0 +1,30 @@
#ifndef LOG_LEVEL
#define LOG_LEVEL LOG_LEVEL_SILENT
#endif
#include <Arduino.h>
#include <ArduinoLog.h>
#include <Component.h>
#include <App.h>
#include <CommandMessage.h>
#include <Bridge.h>
#include <SerialMessage.h>
#include "config.h"
#include "macros.h"
#include "xtypes.h"
#include "StringUtils.h"
#include "PHApp.h"
PHApp testApp;
void setup()
{
testApp.setup();
}
void loop()
{
testApp.loop();
}
+383
View File
@@ -0,0 +1,383 @@
#include "PIDController.h"
#include "Logger.h"
#include <modbus/ModbusTCP.h>
PIDController::PIDController(uint8_t id, const char *name, int8_t thermoDO, int8_t thermoCS, int8_t thermoCLK, int8_t outputPin)
: Component(name, id),
thermocouple(thermoCLK, thermoCS, thermoDO), // CLK, CS, DO
_thermoDO(thermoDO), _thermoCS(thermoCS), _thermoCLK(thermoCLK),
_setpoint(25.0), _input(0.0), _output(0.0), // Default setpoint
_kp(2.0), _ki(5.0), _kd(1.0), // Default PID gains (example values)
_pid(&_input, &_output, &_setpoint, _kp, _ki, _kd, DIRECT), // DIRECT or REVERSE depending on heating/cooling
_aTune(&_input, &_output),
_autotuning(false),
_autotuneStatus(AUTOTUNE_OFF),
_aTuneStartValue(0.0),
_aTuneNoiseBand(0.5), // Noise band for autotune (adjust based on system)
_aTuneLookbackSec(20), // Autotune lookback seconds (adjust)
_lastKp(0.0), _lastKi(0.0), _lastKd(0.0),
_outputPin(outputPin),
_windowStartTime(0),
_pidModeAuto(false), // Start in MANUAL mode
_sensorError(false)
{
}
PIDController::~PIDController()
{
// Nothing specific to delete here
}
short PIDController::setup()
{
Log.infoln("Setting up PIDController '%s'...", name);
pinMode(_outputPin, OUTPUT);
digitalWrite(_outputPin, LOW); // Ensure output is off initially
// Initialize PID
_pid.SetOutputLimits(0, 255); // PID output range (for PWM-like control)
_pid.SetSampleTime(_windowSize); // Set PID sample time = window size
setPIDMode(_pidModeAuto); // Apply initial mode
// Initialize Autotune
_aTune.SetNoiseBand(_aTuneNoiseBand);
_aTune.SetOutputStep(100); // Example output step for autotune
_aTune.SetLookbackSec(_aTuneLookbackSec);
_aTune.SetControlType(1); // PID type
// Wait for MAX6675 to stabilize (optional, may remove if blocking is bad)
delay(500);
updateTemperature(); // Initial temperature read
if (_sensorError)
{
Log.errorln("PID %s: Initial thermocouple read failed!", name);
}
else
{
Log.infoln("PID %s: Initial temp = %.2f C", name, _input);
}
_windowStartTime = millis(); // Initialize window timing
Log.infoln("PIDController '%s' setup complete.", name);
return E_OK;
}
short PIDController::loop()
{
unsigned long now = millis();
updateTemperature(); // Read temperature input
if (_autotuning)
{
runAutotune();
}
else if (_pidModeAuto)
{
if (!_sensorError)
{
_pid.Compute(); // Only compute if temperature reading is valid
}
else
{
_output = 0; // Turn off output if sensor fails
}
}
// In MANUAL mode, _output is set via Modbus and not changed here
applyOutput(); // Apply the calculated or manually set output
return E_OK;
}
void PIDController::updateTemperature()
{
double temp = thermocouple.readCelsius();
if (isnan(temp))
{
if (!_sensorError)
{ // Log only on transition to error state
Log.errorln("PID %s: Failed to read temperature from MAX6675!", name);
_sensorError = true;
_input = -999.0; // Indicate error state clearly
// Optionally handle sensor error (e.g., turn off PID, set alarm)
setPIDMode(false); // Force manual mode on sensor error
}
}
else
{
if (_sensorError)
{ // Log recovery
Log.noticeln("PID %s: Thermocouple reading recovered.", name);
}
_sensorError = false;
_input = temp;
}
}
void PIDController::runAutotune()
{
unsigned long now = millis();
if (!_autotuning)
return; // Should not happen, but safety check
byte val = _aTune.Runtime();
if (val != 0)
{ // Autotune finished
bool success = (val == 1);
finishAutotune(success);
}
else
{
// Autotune still running, apply its output directly
applyOutput(); // Use the _output value calculated by aTune.Runtime()
}
}
void PIDController::applyOutput()
{
unsigned long now = millis();
// Time-proportional output control (PWM-like over _windowSize)
if (now - _windowStartTime > _windowSize)
{
_windowStartTime += _windowSize;
}
double currentOutput = _autotuning ? _output : (_pidModeAuto ? _output : _output); // Use ATune output if running, else PID/Manual output
// Map 0-255 output to on/off time within the window
if (currentOutput > (now - _windowStartTime) * 255.0 / _windowSize)
{
digitalWrite(_outputPin, HIGH);
}
else
{
digitalWrite(_outputPin, LOW);
}
}
void PIDController::setPIDMode(bool autoMode)
{
if (_pidModeAuto == autoMode && !_sensorError)
return; // No change needed unless recovering from error
// If switching to auto mode, ensure sensor is working
if (autoMode && _sensorError)
{
Log.errorln("PID %s: Cannot switch to AUTO mode, sensor error active.", name);
_pidModeAuto = false; // Stay in MANUAL
_pid.SetMode(MANUAL);
return;
}
_pidModeAuto = autoMode;
_pid.SetMode(_pidModeAuto ? AUTOMATIC : MANUAL);
Log.infoln("PID %s: Mode set to %s", name, _pidModeAuto ? "AUTO" : "MANUAL");
// If switching to MANUAL, potentially reset output or keep last auto value?
// Current behavior: manual output must be set via Modbus.
}
void PIDController::startAutotune()
{
if (_autotuning)
{
Log.warningln("PID %s: Autotune already running.", name);
return;
}
if (_sensorError)
{
Log.errorln("PID %s: Cannot start Autotune, sensor error active.", name);
_autotuneStatus = AUTOTUNE_FAILED;
return;
}
Log.noticeln("PID %s: Starting Autotune...", name);
_aTuneStartValue = _output; // Remember the output value before starting
_aTune.Cancel(); // Reset autotune logic just in case
_autotuning = true;
_autotuneStatus = AUTOTUNE_RUNNING;
// ATune output range might need adjustment depending on expected tuning behavior
_aTune.SetOutputStep(abs(_setpoint - _input) > 10 ? 80 : 40); // Example dynamic step
}
void PIDController::cancelAutotune()
{
if (!_autotuning)
return;
Log.noticeln("PID %s: Cancelling Autotune.", name);
_aTune.Cancel();
_autotuning = false;
_autotuneStatus = AUTOTUNE_OFF;
_output = _aTuneStartValue; // Restore output to value before autotune
applyOutput(); // Apply the restored output immediately
setPIDMode(_pidModeAuto); // Re-apply original PID mode
}
void PIDController::finishAutotune(bool success)
{
_autotuning = false;
if (success)
{
_lastKp = _aTune.GetKp();
_lastKi = _aTune.GetKi();
_lastKd = _aTune.GetKd();
Log.noticeln("PID %s: Autotune finished successfully!", name);
Log.noticeln(" > Kp: %.2f, Ki: %.2f, Kd: %.2f", _lastKp, _lastKi, _lastKd);
_autotuneStatus = AUTOTUNE_FINISHED_OK;
// Optionally apply the new tunings immediately
_kp = _lastKp;
_ki = _lastKi;
_kd = _lastKd;
_pid.SetTunings(_kp, _ki, _kd);
}
else
{
Log.errorln("PID %s: Autotune failed!", name);
_autotuneStatus = AUTOTUNE_FAILED;
// Keep old PID values
}
_output = _aTuneStartValue; // Restore output
applyOutput();
setPIDMode(_pidModeAuto); // Re-apply original PID mode
}
// Scale Modbus value (e.g., 1234 means 12.34) to double
double PIDController::scalePIDParam(uint16_t modbusValue)
{
return (double)modbusValue / 100.0;
}
// Scale double (e.g., 12.34) to Modbus value (1234)
uint16_t PIDController::unscalePIDParam(double pidValue)
{
return (uint16_t)(pidValue * 100.0 + 0.5); // Add 0.5 for rounding
}
// --- Modbus Read/Write ---
short PIDController::mb_tcp_read(short address)
{
// Calculate offset from base address
short offset = address - MB_HREG_PID_2_BASE_ADDRESS;
switch (offset)
{
case 0: // PV (Process Value * 100)
return _sensorError ? 0xFFFF : (uint16_t)(_input * 100.0 + 0.5);
case 1: // SP (Setpoint * 100)
return (uint16_t)(_setpoint * 100.0 + 0.5);
case 2: // Output (0-255)
return (uint16_t)(_output + 0.5);
case 3: // Kp (* 100)
return unscalePIDParam(_kp);
case 4: // Ki (* 100)
return unscalePIDParam(_ki);
case 5: // Kd (* 100)
return unscalePIDParam(_kd);
case 6: // Mode (0:Manual, 1:Auto)
return _pidModeAuto ? 1 : 0;
case 7: // Autotune Status
return (uint16_t)_autotuneStatus;
case 8: // Autotune Control (Read is always 0)
return 0;
case 9: // Autotune Kp Result (* 100)
return unscalePIDParam(_lastKp);
case 10: // Autotune Ki Result (* 100)
return unscalePIDParam(_lastKi);
case 11: // Autotune Kd Result (* 100)
return unscalePIDParam(_lastKd);
default:
Log.warningln("PID %s: Read from unhandled address offset %d (Abs: %d)", name, offset, address);
return 0xFFFF; // Indicate invalid address
}
}
short PIDController::mb_tcp_write(short address, short value)
{
// Calculate offset from base address
short offset = address - MB_HREG_PID_2_BASE_ADDRESS;
switch (offset)
{
case 1: // SP (Setpoint * 100)
_setpoint = (double)value / 100.0;
Log.verboseln("PID %s: Setpoint updated to %.2f", name, _setpoint);
return E_OK;
case 2: // Output (0-255) - Only allowed in MANUAL mode
if (!_pidModeAuto && !_autotuning)
{
// Clamp value to 0-255
_output = constrain(value, 0, 255);
Log.verboseln("PID %s: Manual output set to %.1f", name, _output);
applyOutput(); // Apply manual change immediately if possible
return E_OK;
}
else
{
Log.warningln("PID %s: Cannot set output manually while in AUTO mode or during Autotune.", name);
return E_INVALID_PARAMETER; // Use defined error code
}
case 3: // Kp (* 100)
_kp = scalePIDParam(value);
_pid.SetTunings(_kp, _ki, _kd);
Log.verboseln("PID %s: Kp updated to %.2f", name, _kp);
return E_OK;
case 4: // Ki (* 100)
_ki = scalePIDParam(value);
_pid.SetTunings(_kp, _ki, _kd);
Log.verboseln("PID %s: Ki updated to %.2f", name, _ki);
return E_OK;
case 5: // Kd (* 100)
_kd = scalePIDParam(value);
_pid.SetTunings(_kp, _ki, _kd);
Log.verboseln("PID %s: Kd updated to %.2f", name, _kd);
return E_OK;
case 6: // Mode (0:Manual, 1:Auto)
setPIDMode(value == 1);
return E_OK;
case 8: // Autotune Control (Write 1 to start, 0 to stop)
if (value == 1)
{
startAutotune();
}
else
{
cancelAutotune();
}
return E_OK;
// Read-only registers or invalid address
case 0: // PV
case 7: // Autotune Status
case 9: // Autotune Kp Result
case 10: // Autotune Ki Result
case 11: // Autotune Kd Result
Log.warningln("PID %s: Attempt to write read-only address offset %d (Abs: %d)", name, offset, address);
return E_INVALID_PARAMETER; // Use defined error code
default:
Log.warningln("PID %s: Write to unhandled address offset %d (Abs: %d)", name, offset, address);
return E_INVALID_PARAMETER; // Use defined error code
}
}
void PIDController::mb_tcp_register(ModbusTCP *manager) const
{
if (!manager)
{
Log.errorln("PID %s: Cannot register Modbus blocks - ModbusTCP is null.", name);
return;
}
// Define the Modbus block information for this PID controller
MB_Registers info(MB_HREG_PID_2_BASE_ADDRESS,
PID_2_REGISTER_COUNT,
E_FN_CODE::FN_READ_COIL,
MB_ACCESS_READ_WRITE); // Assuming most registers are R/W, actual access enforced in read/write methods
// Attempt to register the block
if (!manager->registerModbus(const_cast<PIDController *>(this), info))
{
Log.errorln("PID %s: Failed to register Modbus block (Addr: %d, Count: %d)", name, info.startAddress, info.count);
}
}
+70
View File
@@ -0,0 +1,70 @@
#ifndef PIDCONTROLLER_H
#define PIDCONTROLLER_H
#include <Component.h>
#include "config-modbus.h"
#include <enums.h>
#include <max6675.h>
#include <PID_v1.h> // From br3ttb/PID library
#include <PID_AutoTune_v0.h> // From br3ttb/PID Autotune Library
// Define Autotune Status Enum (aligned with Modbus register)
enum AutotuneStatus : uint8_t {
AUTOTUNE_OFF = 0,
AUTOTUNE_RUNNING = 1,
AUTOTUNE_FINISHED_OK = 2,
AUTOTUNE_FAILED = 3
};
class PIDController : public Component {
public:
PIDController(uint8_t id, const char* name, int8_t thermoDO, int8_t thermoCS, int8_t thermoCLK, int8_t outputPin);
~PIDController();
short setup() override;
short loop() override;
short mb_tcp_read(short address) override;
short mb_tcp_write(short address, short value) override;
void mb_tcp_register(ModbusTCP* manager) const override;
private:
// MAX6675 Thermocouple Sensor
MAX6675 thermocouple;
int8_t _thermoDO, _thermoCS, _thermoCLK;
// PID Controller
double _setpoint, _input, _output;
double _kp, _ki, _kd;
PID _pid;
// PID Autotune
PID_ATune _aTune;
bool _autotuning;
AutotuneStatus _autotuneStatus;
double _aTuneStartValue; // Where the output was when AT started
double _aTuneNoiseBand;
unsigned int _aTuneLookbackSec;
double _lastKp, _lastKi, _lastKd; // Store results from autotune
// Output Control
int8_t _outputPin;
unsigned long _windowStartTime;
const unsigned long _windowSize = 1000; // PID cycle time (e.g., 1 second)
bool _pidModeAuto; // true = AUTO, false = MANUAL
// Helper methods
void updateTemperature();
void runAutotune();
void applyOutput();
void setPIDMode(bool autoMode);
void startAutotune();
void cancelAutotune();
void finishAutotune(bool success);
double scalePIDParam(uint16_t modbusValue);
uint16_t unscalePIDParam(double pidValue);
// Error handling
bool _sensorError;
};
#endif // PIDCONTROLLER_H