polymech - fw latest | web ui
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "PHApp.h"
|
||||
#include "config.h"
|
||||
#include <ArduinoLog.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user