firmware-base/src/components/POT.h
2025-05-23 18:17:08 +02:00

347 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#ifndef POT_H
#define POT_H
#include <Arduino.h> // millis(), pinMode(), analogRead(), map()
#include <ArduinoLog.h>
#include <App.h>
#include <Component.h>
#include <enums.h>
#include "config.h"
#include "config-modbus.h"
#include <modbus/Modbus.h>
#include <modbus/ModbusTCP.h>
/*
* Hardware Smoothing Suggestion (RC Low-Pass Filter):
*
* @link : https://docs.espressif.com/projects/esp-idf/en/v4.4/esp32/api-reference/peripherals/adc.html
* To further reduce noise on the analog input signal before it reaches the ADC,
* a simple RC low-pass filter can be implemented using the potentiometer itself
* as the resistor (R) and adding a capacitor (C) between the potentiometer's
* wiper output and ground.
*
* Potentiometer Resistance (R): 5 kΩ (5000 Ω)
* Input Voltage: 5V
*
* Cutoff Frequency (f_c) = 1 / (2 * π * R * C)
* Capacitor (C) = 1 / (2 * π * R * f_c)
*
* Example Capacitor Values for R = 5 kΩ:
* - For f_c ≈ 30 Hz (Faster response): C ≈ 1 / (2 * π * 5000 * 30) ≈ 1.06 µF -> Use 1 µF
* - For f_c ≈ 10 Hz (Moderate): C ≈ 1 / (2 * π * 5000 * 10) ≈ 3.18 µF -> Use 2.2 µF or 3.3 µF
* - For f_c ≈ 5 Hz (More smoothing): C ≈ 1 / (2 * π * 5000 * 5) ≈ 6.37 µF -> Use 4.7 µF or 6.8 µF
*
* Recommendation: Start with C = 1 µF to 3.3 µF and adjust based on observed
* noise reduction and response time requirements. Use a ceramic capacitor
* rated for at least the input voltage (e.g., 10V or 16V).
*/
/* --------------------------------------------------------------------------
* POT Analogue potentiometer reader with lightweight digital filtering
* --------------------------------------------------------------------------
* ▸ Supports three damping modes: NONE, MOVINGAVERAGE (boxcar) and EMA.
* ▸ Movingaverage implemented as an incremental ring buffer (O(1)).
* ▸ EMA uses a 1pole IIR: y[n] = y[n1] + (x[n] y[n1]) / 2^k.
* ▸ Deadband suppresses ±1LSB chatter after scaling to 0100.
* ▸ Designed for small AVR/ESP32 class MCUs no malloc, no floats.
* ----------------------------------------------------------------------- */
// -------------------------------------------------------------------------
// Compiletime knobs (override in config.h if desired)
// -------------------------------------------------------------------------
#ifndef POT_SAMPLE_INTERVAL
#define POT_SAMPLE_INTERVAL 10 // ms between ADC reads
#endif
#ifndef POT_DA_WIN_LEN
#define POT_DA_WIN_LEN 8 // boxcar window length (must be poweroftwo)
#endif
// Compile-time check for power-of-two window length for moving average
static_assert((POT_DA_WIN_LEN > 0) && ((POT_DA_WIN_LEN & (POT_DA_WIN_LEN - 1)) == 0),
"POT_DA_WIN_LEN must be a power of two for efficient moving average calculation.");
#ifndef POT_DA_EMA_SHIFT
#define POT_DA_EMA_SHIFT 3 // alpha = 1 / (2^shift) (shift = 3 ⇒ α = 0.125)
#endif
#ifndef POT_DEADBAND
#define POT_DEADBAND 1 // change (0100 scale) required to notify
#endif
#ifndef POT_RAW_MAX_VALUE
#define POT_RAW_MAX_VALUE 1023 // 10bit ADC fullscale
#endif
#ifndef POT_SCALED_MAX_VALUE
#define POT_SCALED_MAX_VALUE 100 // applicationlevel fullscale
#endif
#ifndef POT_DAMPING_WINDOW_SIZE
#define POT_DAMPING_WINDOW_SIZE POT_DA_WIN_LEN // alias for legacy code
#endif
// -------------------------------------------------------------------------
// Damping algorithms
// -------------------------------------------------------------------------
enum class POTDampingAlgorithm : uint8_t
{
DAMPING_NONE = 0,
DAMPING_MOVING_AVERAGE,
DAMPING_EMA
};
// -------------------------------------------------------------------------
// Control modes
// -------------------------------------------------------------------------
enum class E_POTControlMode : uint8_t
{
E_AUX_LOCAL = 0,
E_AUX_REMOTE = 1
};
// Address offsets for Modbus registers
enum E_POT_REGISTER_OFFSET : uint16_t
{
OFFSET_VALUE = 0,
OFFSET_MODE = 1,
OFFSET_REMOTE_VALUE = 2
};
class Bridge;
class POT : public Component
{
public:
POT(Component *owner,
uint16_t _pin,
uint16_t _id,
uint16_t _modbusAddress,
POTDampingAlgorithm _algo = POTDampingAlgorithm::DAMPING_NONE)
: Component("POT", _id, Component::COMPONENT_DEFAULT, owner),
pin(_pin),
modbusAddress(_modbusAddress),
algo(_algo)
{
setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS);
// Initialize instance-specific Modbus blocks.
// INIT_MODBUS_BLOCK is assumed to use `this->modbusAddress` and potentially `this->id`
// (e.g., for slaveId if that's how it's intended) from the current POT instance.
m_modbus_blocks[0] = INIT_MODBUS_BLOCK_TCP(modbusAddress, OFFSET_VALUE, E_FN_CODE::FN_READ_HOLD_REGISTER,
MB_ACCESS_READ_ONLY, "POT Value", "aux");
m_modbus_blocks[1] = INIT_MODBUS_BLOCK_TCP(modbusAddress, OFFSET_MODE, E_FN_CODE::FN_READ_HOLD_REGISTER,
MB_ACCESS_READ_WRITE, "POT Mode (0=Local,1=Remote)", "aux");
m_modbus_blocks[2] = INIT_MODBUS_BLOCK_TCP(modbusAddress, OFFSET_REMOTE_VALUE, E_FN_CODE::FN_READ_HOLD_REGISTER,
MB_ACCESS_READ_WRITE, "POT Remote Value", "aux");
// Initialize the view to point to these instance-specific blocks
m_modbus_view.data = m_modbus_blocks; // m_modbus_blocks is MB_Registers[3], so this is MB_Registers*
m_modbus_view.count = sizeof(m_modbus_blocks) / sizeof(m_modbus_blocks[0]);
}
// --------------------------------------------------- lifecycle -------
short setup() override
{
Component::setup();
pinMode(pin, INPUT);
lastSample = millis();
return E_OK;
}
short loop() override
{
uint32_t now = millis();
if (now - lastSample < POT_SAMPLE_INTERVAL) return E_OK;
lastSample = now;
uint16_t raw = analogRead(pin);
// ---------------------- filtering -------------------------------
switch (algo)
{
case POTDampingAlgorithm::DAMPING_MOVING_AVERAGE:
acc -= ring[idx];
acc += raw;
ring[idx] = raw;
idx = (idx + 1) & (POT_DA_WIN_LEN - 1); // poweroftwo length
filtRaw = acc >> log2_WIN_LEN; // divide by window length
break;
case POTDampingAlgorithm::DAMPING_EMA:
// y += (x - y) / 2^k -- shift = k
filtRaw += (raw - filtRaw) >> POT_DA_EMA_SHIFT;
break;
case POTDampingAlgorithm::DAMPING_NONE:
default:
filtRaw = raw;
break;
}
// ------------------- scale & deadband ---------------------------
uint16_t scaledLocalValue = map(filtRaw, 0, POT_RAW_MAX_VALUE, 0, POT_SCALED_MAX_VALUE);
uint16_t newValue = value; // Start with current value
if (controlMode == E_POTControlMode::E_AUX_LOCAL)
{
// LOCAL Mode: Use filtered/scaled value with deadband
if (abs(int16_t(scaledLocalValue) - int16_t(value)) > POT_DEADBAND)
{
newValue = scaledLocalValue;
}
}
else // E_AUX_REMOTE Mode
{
newValue = remoteValue; // Already clamped in mb_tcp_write
}
if (newValue != value)
{
value = newValue;
notifyStateChange();
}
return E_OK;
}
// --------------------------------------------------- diagnostics ----
short debug() override { return info(); }
short info(short = 0, short = 0) override
{
Log.verboseln("POT::info - ID:%d Pin:%d Val:%d Algo:%d Mode:%d RemoteVal:%d Raw:%d Address:%d",
id, pin, value, static_cast<uint8_t>(algo),
static_cast<uint8_t>(controlMode), remoteValue, analogRead(pin), modbusAddress);
return E_OK;
}
// --------------------------------------------------- Modbus ---------
short mb_tcp_write(MB_Registers* reg, short networkValue) override
{
uint16_t addr = reg->startAddress;
bool changed = false;
if (addr == modbusAddress + OFFSET_MODE) // Write to Mode register
{
E_POTControlMode newMode = (networkValue == 0) ? E_POTControlMode::E_AUX_LOCAL : E_POTControlMode::E_AUX_REMOTE;
if (newMode != controlMode)
{
Log.verboseln("POT::mb_write - ID:%d Mode change %d -> %d", id, static_cast<uint8_t>(controlMode), static_cast<uint8_t>(newMode));
controlMode = newMode;
changed = true;
}
}
else if (addr == modbusAddress + OFFSET_REMOTE_VALUE) // Write to Remote Value register
{
// Clamp remote value to valid scaled range (0-100 or defined max)
uint16_t clampedValue = constrain(networkValue, 0, POT_SCALED_MAX_VALUE);
if (clampedValue != remoteValue)
{
Log.verboseln("POT::mb_write - ID:%d Remote value change %d -> %d", id, remoteValue, clampedValue);
remoteValue = clampedValue;
if (controlMode == E_POTControlMode::E_AUX_REMOTE) {
changed = true; // Trigger update in loop if remote
}
}
}
else if (addr == modbusAddress + OFFSET_VALUE) // Writing to Value register (read-only)
{
// Writing to the main value register is not allowed directly
return MODBUS_ERROR_ILLEGAL_FUNCTION;
}
else
{
return E_INVALID_PARAMETER; // Address doesn't match any known register
}
// If in remote mode and either mode or remote value changed, update main value immediately
if (changed && controlMode == E_POTControlMode::E_AUX_REMOTE)
{
if (value != remoteValue)
{
value = remoteValue;
notifyStateChange();
}
}
// If mode changed to local, next loop will update value based on pot
return E_OK;
}
short mb_tcp_read(MB_Registers* reg) override
{
uint16_t addr = reg->startAddress;
if (addr == modbusAddress + OFFSET_VALUE) {
return value;
}
else if (addr == modbusAddress + OFFSET_MODE) { // Read Mode
return static_cast<uint16_t>(controlMode);
}
else if (addr == modbusAddress + OFFSET_REMOTE_VALUE) { // Read Remote Value
return remoteValue;
}
return 0; // Default or error for unknown address
}
void mb_tcp_register(ModbusTCP *mgr) const override
{
ModbusBlockView* view = mb_tcp_blocks();
for (int i=0; i < view->count; ++i)
{
mgr->registerModbus(const_cast<POT*>(this), view->data[i]); // view->data[i] is MB_Registers
}
}
// Returns a pointer to the instance-specific Modbus block definitions.
// The virtual function in Component base class is likely `virtual ModbusBlockView* mb_tcp_blocks() const;`
ModbusBlockView* mb_tcp_blocks() const override
{
// m_modbus_view is initialized in constructor and doesn't change.
// Returning a non-const pointer from a const method is allowed if the member is mutable.
return &m_modbus_view;
}
short serial_register(Bridge *b) override
{
b->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&POT::info);
return E_OK;
}
// --------------------------------------------------- public helpers -
uint16_t getValue() const { return value; }
private:
// ----------------------------- constants ----------------------------
static constexpr uint8_t log2_WIN_LEN =
(POT_DA_WIN_LEN == 1) ? 0 :
(POT_DA_WIN_LEN == 2) ? 1 :
(POT_DA_WIN_LEN == 4) ? 2 :
(POT_DA_WIN_LEN == 8) ? 3 :
(POT_DA_WIN_LEN == 16)? 4 : 0; // extend if larger windows used
// ----------------------------- members ------------------------------
const uint16_t pin;
const uint16_t modbusAddress;
const POTDampingAlgorithm algo;
// --- Control Mode ---
E_POTControlMode controlMode = E_POTControlMode::E_AUX_LOCAL;
uint16_t remoteValue = 0; // Value set via Modbus in E_AUX_REMOTE mode
// ---------------------
uint16_t ring[POT_DA_WIN_LEN] = {0}; // circular buffer
uint32_t acc = 0; // running sum
uint8_t idx = 0; // buffer index
uint16_t filtRaw = 0; // filtered raw sample
uint16_t value = 0; // scaled, debounced value (0100)
uint32_t lastSample = 0; // ms
// Instance-specific storage for Modbus block definitions
MB_Registers m_modbus_blocks[3];
// m_modbus_view needs to be mutable to be returned as ModbusBlockView* from a const method.
mutable ModbusBlockView m_modbus_view;
};
#endif // POT_H