/**
* MechBase PLC β Ladder Logic Interpreter v2 Implementation
*
* IEC 61131-3 ladder logic interpreter for ESP32.
*
* Contact instructions: NO, NC, Rising Edge, Falling Edge, Timer bits (EN/TT/DN)
* Output instructions: OTE, OTL, OTU, TON, TOF, TP
* Network structure: Parallel branching, series/parallel evaluation
*/
#include "ladder_interpreter.h"
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Branch evaluation
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
bool Branch::evaluate(LadderInterpreter* interp, bool is_output) {
power_flow = true;
for (auto& elem : elements) {
bool result = false;
if (is_output) {
// ββ Output instructions ββββββββββββββββββββββββββββββββββββββ
switch (static_cast<OutputType>(elem.type)) {
case OT_OTE: {
// Output Energize β follows input power
power_flow = false; // OTE is terminal, no more flow
interp->writeBool(elem.tag, true);
return true;
}
case OT_OTL: {
// Output Latch (SET) β stays ON once set
power_flow = false;
if (this->power_flow) {
interp->writeBool(elem.tag, true);
}
return true;
}
case OT_OTU: {
// Output Unlatch (RESET) β clears latched output
power_flow = false;
if (this->power_flow) {
interp->writeBool(elem.tag, false);
}
return true;
}
default:
break;
}
}
else {
// ββ Contact instructions βββββββββββββββββββββββββββββββββββββ
switch (static_cast<ContactType>(elem.type)) {
case CT_NO: {
// Normally Open β passes when TRUE
result = interp->readBool(elem.tag);
break;
}
case CT_NC: {
// Normally Closed β passes when FALSE
result = !interp->readBool(elem.tag);
break;
}
case CT_RISING: {
// Rising Edge β single scan TRUEβFALSE
bool current = interp->readBool(elem.tag);
result = (current && !elem.last_state);
elem.last_state = current;
break;
}
case CT_FALLING: {
// Falling Edge β single scan FALSEβTRUE
bool current = interp->readBool(elem.tag);
result = (!current && elem.last_state);
elem.last_state = current;
break;
}
case CT_TON_DN:
case CT_TON_TT:
case CT_TON_EN: {
// TON status bits
uint8_t tidx = interp->parseTimerIndex(elem.tag);
LadderTimer& t = interp->getTimer(tidx);
switch (elem.type) {
case CT_TON_DN: result = t.dn; break;
case CT_TON_TT: result = t.tt; break;
case CT_TON_EN: result = t.en; break;
default: result = false;
}
break;
}
case CT_TOF_DN:
case CT_TOF_TT:
case CT_TOF_EN: {
// TOF status bits
uint8_t tidx = interp->parseTimerIndex(elem.tag);
LadderTimer& t = interp->getTimer(tidx);
switch (elem.type) {
case CT_TOF_DN: result = t.dn; break;
case CT_TOF_TT: result = t.tt; break;
case CT_TOF_EN: result = t.en; break;
default: result = false;
}
break;
}
case CT_TP_DN:
case CT_TP_TT:
case CT_TP_EN: {
// TP status bits
uint8_t tidx = interp->parseTimerIndex(elem.tag);
LadderTimer& t = interp->getTimer(tidx);
switch (elem.type) {
case CT_TP_DN: result = t.dn; break;
case CT_TP_TT: result = t.tt; break;
case CT_TP_EN: result = t.en; break;
default: result = false;
}
break;
}
default:
result = false;
break;
}
}
// For contacts, if any element fails, branch fails
if (!is_output && !result) {
power_flow = false;
break;
}
// For outputs, we've already handled the instruction
if (is_output) {
break;
}
}
return power_flow;
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Network evaluation
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
bool Network::evaluate(LadderInterpreter* interp) {
power_flow = false;
// ββ Phase 1: Evaluate input branches (OR'd together) βββββββββββββββββ
// Any branch with power flow means the network has power
for (auto& branch : input_branches) {
if (branch.evaluate(interp, false)) {
power_flow = true;
break; // One branch is enough for OR logic
}
}
// ββ Phase 2: If power flows, execute output branches βββββββββββββββββ
if (power_flow) {
for (auto& branch : output_branches) {
branch.power_flow = true; // Pass power to output branch
branch.evaluate(interp, true);
}
}
return power_flow;
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// LadderInterpreter constructor / destructor
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
LadderInterpreter::LadderInterpreter()
: running(false), simulating(false), scan_start(0) {
// Zero out all arrays
for (int i = 0; i < MAX_DIGITAL_INPUTS; i++) {
di[i] = false;
}
for (int i = 0; i < MAX_DIGITAL_OUTPUTS; i++) {
do_[i] = false;
do_prev[i] = false;
}
for (int i = 0; i < MAX_BOOL_REGISTERS; i++) {
m_bool[i] = false;
}
for (int i = 0; i < MAX_INT_REGISTERS; i++) {
m_int[i] = 0;
}
for (int i = 0; i < MAX_TIMER_COUNT; i++) {
timers[i] = LadderTimer{
(uint8_t)i, 0, 0, false, false, false
};
}
for (int i = 0; i < MAX_COUNTER_COUNT; i++) {
counters[i] = LadderCounter{
(uint8_t)i, 0, 0, false, false, false, false, false, false
};
}
}
LadderInterpreter::~LadderInterpreter() {
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Lifecycle
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void LadderInterpreter::begin() {
scan_start = millis();
Serial.println(F("MechBase PLC v2 β Ladder Interpreter Initialized"));
Serial.println(F("Instructions: NO NC Rising Falling | OTE OTL OTU | TON TOF TP"));
}
void LadderInterpreter::scan() {
if (!running) return;
scan_start = millis();
// Phase 1: Evaluate all networks (rung-by-rung)
for (auto& net : networks) {
net.evaluate(this);
}
// Phase 2: Update timers
// Timers are updated in the output branch evaluation,
// but we also need to track elapsed time per scan
for (auto& timer : timers) {
if (timer.tt) {
timer.elapsed = min(timer.elapsed + (millis() - scan_start),
(uint32_t)(timer.preset + 1));
}
}
// Phase 3: Update counters
for (auto& counter : counters) {
counter.update();
}
// Phase 4: Write physical outputs (skip in simulation)
if (!simulating) {
for (int i = 0; i < MAX_DIGITAL_OUTPUTS; i++) {
if (do_[i] != do_prev[i]) {
digitalWrite(i, do_[i] ? HIGH : LOW);
do_prev[i] = do_[i];
}
}
}
}
void LadderInterpreter::run() {
running = true;
Serial.println(F("PLC Running"));
}
void LadderInterpreter::stop() {
running = false;
Serial.println(F("PLC Stopped"));
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// I/O access
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void LadderInterpreter::setDI(uint8_t idx, bool val) {
if (idx < MAX_DIGITAL_INPUTS) di[idx] = val;
}
bool LadderInterpreter::getDI(uint8_t idx) {
if (idx < MAX_DIGITAL_INPUTS) return di[idx];
return false;
}
bool LadderInterpreter::getDO(uint8_t idx) {
if (idx < MAX_DIGITAL_OUTPUTS) return do_[idx];
return false;
}
void LadderInterpreter::setDO(uint8_t idx, bool val) {
if (idx < MAX_DIGITAL_OUTPUTS) do_[idx] = val;
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Memory access
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void LadderInterpreter::setM(bool idx, bool val) {
// idx is a bool, cast to int for indexing
uint8_t i = (uint8_t)idx;
if (i < MAX_BOOL_REGISTERS) m_bool[i] = val;
}
bool LadderInterpreter::getM(bool idx) {
uint8_t i = (uint8_t)idx;
if (i < MAX_BOOL_REGISTERS) return m_bool[i];
return false;
}
void LadderInterpreter::setINT(uint8_t idx, int16_t val) {
if (idx < MAX_INT_REGISTERS) m_int[idx] = val;
}
int16_t LadderInterpreter::getINT(uint8_t idx) {
if (idx < MAX_INT_REGISTERS) return m_int[idx];
return 0;
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Tag helpers β parse "I0", "Q0", "M0", "T0", "INT0", "T0.DN", "T0.TT", etc.
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
bool LadderInterpreter::readBool(const char* tag) {
// Format: PREFIX[INDEX] or PREFIX[INDEX].BIT
// e.g. "I0", "Q3", "M10", "T0.DN", "T0.TT", "T0.EN"
if (tag[0] == 'I' || tag[0] == 'i') {
int idx = atoi(&tag[1]);
return getDI((uint8_t)idx);
}
if (tag[0] == 'Q' || tag[0] == 'q') {
int idx = atoi(&tag[1]);
return getDO((uint8_t)idx);
}
if (tag[0] == 'M' || tag[0] == 'm') {
int idx = atoi(&tag[1]);
return (bool)idx ? (idx < MAX_BOOL_REGISTERS && m_bool[idx]) : m_bool[0];
}
if (tag[0] == 'T' || tag[0] == 't') {
int idx = atoi(&tag[1]);
LadderTimer& t = getTimer((uint8_t)idx);
// Check for bit suffix
if (strlen(tag) > 2) {
const char* dot = strchr(tag, '.');
if (dot) {
if (strcmp(dot + 1, "DN") == 0) return t.dn;
if (strcmp(dot + 1, "TT") == 0) return t.tt;
if (strcmp(dot + 1, "EN") == 0) return t.en;
}
}
// Default: return DN bit
return t.dn;
}
if (tag[0] == 'C' || tag[0] == 'c') {
int idx = atoi(&tag[1]);
LadderCounter& c = getCounter((uint8_t)idx);
return c.dn;
}
return false;
}
void LadderInterpreter::writeBool(const char* tag, bool val) {
if (tag[0] == 'Q' || tag[0] == 'q') {
int idx = atoi(&tag[1]);
setDO((uint8_t)idx, val);
}
else if (tag[0] == 'M' || tag[0] == 'm') {
int idx = atoi(&tag[1]);
if (idx < MAX_BOOL_REGISTERS) m_bool[idx] = val;
}
// Timers/counters written by their own update logic
}
int16_t LadderInterpreter::readInt(const char* tag) {
if (tag[0] == 'I' && tag[1] == 'N') {
// INT0..INT31
int idx = atoi(&tag[3]);
return getINT((uint8_t)idx);
}
return 0;
}
void LadderInterpreter::writeInt(const char* tag, int16_t val) {
if (tag[0] == 'I' && tag[1] == 'N') {
int idx = atoi(&tag[3]);
setINT((uint8_t)idx, val);
}
}
uint8_t LadderInterpreter::parseTimerIndex(const char* tag) {
if (tag[0] == 'T' || tag[0] == 't') {
return (uint8_t)atoi(&tag[1]);
}
return 0;
}
uint8_t LadderInterpreter::parseCounterIndex(const char* tag) {
if (tag[0] == 'C' || tag[0] == 'c') {
return (uint8_t)atoi(&tag[1]);
}
return 0;
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Timer / Counter access
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
LadderTimer& LadderInterpreter::getTimer(uint8_t idx) {
if (idx < MAX_TIMER_COUNT) return timers[idx];
static LadderTimer dummy = {};
return dummy;
}
void LadderInterpreter::resetTimer(uint8_t idx) {
if (idx < MAX_TIMER_COUNT) timers[idx].reset();
}
LadderCounter& LadderInterpreter::getCounter(uint8_t idx) {
if (idx < MAX_COUNTER_COUNT) return counters[idx];
static LadderCounter dummy = {};
return dummy;
}
void LadderInterpreter::resetCounter(uint8_t idx) {
if (idx < MAX_COUNTER_COUNT) counters[idx].reset();
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Program loading
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Network& LadderInterpreter::addNetwork(uint16_t num) {
Network net;
net.number = num;
net.power_flow = false;
networks.push_back(net);
return networks.back();
}
void LadderInterpreter::loadFromJSON(const char* json) {
// TODO: JSON parsing for program loading
Serial.println(F("loadFromJSON() called β JSON parser not yet implemented"));
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Debug
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void LadderInterpreter::dumpState() {
Serial.println(F("=== PLC State Dump ==="));
Serial.print(F("Networks: ")); Serial.println(networks.size());
Serial.print(F("Running: ")); Serial.println(running ? "YES" : "NO");
Serial.print(F("Inputs (I0-I15): "));
for (int i = 0; i < MAX_DIGITAL_INPUTS; i++) {
Serial.print(di[i] ? '1' : '0');
Serial.print(' ');
}
Serial.println();
Serial.print(F("Outputs (Q0-Q15): "));
for (int i = 0; i < MAX_DIGITAL_OUTPUTS; i++) {
Serial.print(do_[i] ? '1' : '0');
Serial.print(' ');
}
Serial.println();
Serial.print(F("Memory (M0-M15): "));
for (int i = 0; i < 16; i++) {
Serial.print(m_bool[i] ? '1' : '0');
Serial.print(' ');
}
Serial.println();
Serial.print(F("Timers: "));
for (int i = 0; i < 4; i++) {
Serial.print(F("T")). print(i);
Serial.print(F("[EN=")). print(timers[i].en ? '1' : '0');
Serial.print(F(" TT=")). print(timers[i].tt ? '1' : '0');
Serial.print(F(" DN=")). print(timers[i].dn ? '1' : '0');
Serial.print(F(" el=")). print(timers[i].elapsed);
Serial.print(F(" pr=")). print(timers[i].preset);
Serial.print(F("] "));
}
Serial.println();
Serial.print(F("Scan: "));
Serial.print(getScanTime());
Serial.println(F("ms"));
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Simulation mode
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
void LadderInterpreter::enableSimulation() {
simulating = true;
Serial.println(F("Simulation mode enabled β GPIO writes disabled"));
}
void LadderInterpreter::disableSimulation() {
simulating = false;
Serial.println(F("Simulation mode disabled β GPIO writes active"));
}
bool LadderInterpreter::getSimulation() {
return simulating;
}