Teacher node

//Copyright <2026> <Dorian Fritze>

//Redistribution and use in source and binary forms, with or without 
// modification, are permitted provided that the following conditions are met:

//1. Redistributions of source code must retain the above copyright notice, 
// this list of conditions and the following disclaimer.

//2. Redistributions in binary form must reproduce the above copyright notice,
//  this list of conditions and the following disclaimer in the documentation 
// and/or other materials provided with the distribution.

//3. Neither the name of the copyright holder nor the names of its 
// contributors may be used to endorse or promote products derived from this 
// software without specific prior written permission.

//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.


// ============================================================
//  ESP32-C6 Teacher Node v8
//  Polls all ATtiny412 nodes over I2C.
//
//  Sequence nodes (0x10, 0x11, 0x12):
//    - Forward  (0x10→0x11→0x12): turns transmitter ON for 10 min
//                                  or extends timer another 10 min
//    - Reverse  (0x12→0x11→0x10): turns transmitter OFF immediately
//    - Both must complete within TIME_WINDOW (10 seconds)
//    - COOLDOWN period after any completion prevents double-triggers
//    - Forward times out automatically if not completed
//
//  Warning node (0x16):
//    - ESP32 sends 0x01 at XKT_ON_TIME - ALARM_TIME (9min 30sec)
//    - ESP32 sends 0x00 on transmitter off (any reason)
//    - Warning node handles buzzer + transmitter pulsing locally
//
//  Read-only nodes (0x13, 0x14, 0x15, 0x17, 0x18, 0x19):
//    - ESP32 reads outputOn state only
//    - ATtiny manages its own lifecycle
//    - No reset commands sent
//
//  Serial output streams over WiFi via Telnet (port 23)
//  Connect from your computer:  nc <ESP_IP> 23
//  ─────────────────────────────────────────────────────────
//  SETUP: fill in WIFI_SSID and WIFI_PASS below.
//  IP address prints to USB Serial on boot (once only).
// ============================================================

#include <Wire.h>
#include <WiFi.h>

// ────────────────────────────────────────────────────────────
//  WiFi CREDENTIALS  ← fill these in with the wireless name 
//                      and password you are connected to
// ────────────────────────────────────────────────────────────
#define WIFI_SSID    "Your_Wireless"
#define WIFI_PASS    "Your_Password"
#define TELNET_PORT  23

WiFiServer  telnetServer(TELNET_PORT);
WiFiClient  telnetClient;

// ────────────────────────────────────────────────────────────
//  HELPER: print to USB Serial AND Telnet client
// ────────────────────────────────────────────────────────────
void tprint(const char* msg) {
  Serial.print(msg);
  if (telnetClient && telnetClient.connected())
    telnetClient.print(msg);
}

void tprintln(const char* msg = "") {
  Serial.println(msg);
  if (telnetClient && telnetClient.connected())
    telnetClient.println(msg);
}

void tprintf(const char* fmt, ...) {
  char buf[256];
  va_list args;
  va_start(args, fmt);
  vsnprintf(buf, sizeof(buf), fmt, args);
  va_end(args);
  tprint(buf);
}

// ────────────────────────────────────────────────────────────
//  PIN ASSIGNMENTS
// ────────────────────────────────────────────────────────────
#define SWITCH_PIN  D8    // XKT001X via 2N3906 PNP (GPIO19)

// ────────────────────────────────────────────────────────────
//  NODE ADDRESSES
// ────────────────────────────────────────────────────────────
uint8_t seqNodes[] = {0x10, 0x11, 0x12};
#define WARNING_NODE  0x16

// read-only polled nodes and their labels
uint8_t     readNodes[]  = {0x13, 0x14, 0x15, 0x17, 0x18, 0x19};
const char* readLabels[] = {
  "cream lights",   // 0x13
  "yellow lights",  // 0x14
  "RGB red",        // 0x15
  "RGB green",      // 0x17
  "RGB blue",       // 0x18
  "bugs"            // 0x19
};
const int NUM_SEQ  = 3;
const int NUM_READ = 6;

// ────────────────────────────────────────────────────────────
//  TIMING
// ────────────────────────────────────────────────────────────
#define TIME_WINDOW    10000      // ms — sequence must complete within
#define RECAST_WINDOW  30000UL   // ms — forward sequence locked after activation
                                  //      reverse only between 30s and ALARM_TIME
                                  //      forward re-allowed at ALARM_TIME (9:30)
#define XKT_ON_TIME   600000UL  // ms — transmitter on time (10 minutes)
#define ALARM_TIME     30000UL  // ms — warning duration (must match warning node)

// ────────────────────────────────────────────────────────────
//  FORWARD SEQUENCE STATE
// ────────────────────────────────────────────────────────────
int           nextExpected    = 0;
unsigned long firstTrigger    = 0;

// ────────────────────────────────────────────────────────────
//  REVERSE SEQUENCE STATE
// ────────────────────────────────────────────────────────────
int           nextReverse     = 2;   // expects 0x12 (index 2) first
unsigned long firstReverse    = 0;
unsigned long powerOffTime  = 0;
#define OFF_LOCKOUT  5000UL   // ms — forward locked after power off

// ────────────────────────────────────────────────────────────
//  EDGE DETECTION STATE
//  Tracks last sensor state for each sequence node so sequences
//  only advance on rising edge (inactive → active), not while
//  the magnet is held in place.
// ────────────────────────────────────────────────────────────
bool lastForwardState = false;
bool lastReverseState = false;

// ────────────────────────────────────────────────────────────
//  TRANSMITTER STATE
// ────────────────────────────────────────────────────────────
bool          xktOn           = false;
unsigned long transmitterStart = 0;
bool          warningStarted  = false;

// ────────────────────────────────────────────────────────────
//  READ NODE STATE
// ────────────────────────────────────────────────────────────
bool readState[6] = {false, false, false, false, false, false};


// ============================================================
//  SETUP
// ============================================================
void setup() {
  Serial.begin(115200);
  Wire.begin();   // D4=SDA (GPIO22), D5=SCL (GPIO23)
  Wire.setTimeOut(10);   // — 10ms timeout prevents I2C hang

  pinMode(SWITCH_PIN, OUTPUT);
  digitalWrite(SWITCH_PIN, HIGH);   // HIGH = transmitter OFF at boot

  // ── Connect to WiFi ──────────────────────────────────────
  Serial.print("Connecting to WiFi");
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    Serial.print(WiFi.status());
  }
  Serial.println();
  Serial.print("Connected! IP address: ");
  Serial.println(WiFi.localIP());
  Serial.printf("Telnet: nc %s %d\n",
                WiFi.localIP().toString().c_str(), TELNET_PORT);

  telnetServer.begin();
  telnetServer.setNoDelay(true);

  delay(500);
  tprintln("The dragon is sensing that...");
  tprintln();
}


// ============================================================
//  MAIN LOOP
// ============================================================
void loop() {
  handleTelnetClient();
  pollSequenceNodes();
  pollReadNodes();
  checkTransmitterTimer();
  delay(50);
}


// ============================================================
//  TELNET CLIENT HANDLER
// ============================================================
void handleTelnetClient() {
  if (telnetServer.hasClient()) {
    if (!telnetClient || !telnetClient.connected()) {
      telnetClient = telnetServer.available();
      tprintln("=== The telnet is connected ===");
    } else {
      telnetServer.available().stop();
    }
  }
}


// ============================================================
//  TRANSMITTER TIMER CHECK
// ============================================================
void checkTransmitterTimer() {
  if (!xktOn) return;

  unsigned long elapsed = millis() - transmitterStart;

  if (!warningStarted && elapsed >= XKT_ON_TIME - ALARM_TIME) {
    warningStarted = true;
    sendWarningCommand(0x01);
    tprintln("the spell is losing power, cast again");
  }

  if (elapsed >= XKT_ON_TIME) {
    disableTransmitter();
    tprintln("the spell has been broken");
  }
}


// ============================================================
//  SEQUENCE NODE POLLING (0x10, 0x11, 0x12)
//
//  Gating rules based on transmitter elapsed time:
//    xktOn == false                      → forward allowed
//    xktOn == true, elapsed <  30s       → neither allowed (just cast)
//    xktOn == true, 30s <= elapsed < 9:30 → reverse only
//    xktOn == true, elapsed >= 9:30      → forward allowed (extend/recast)
//
//  Both sequences use rising-edge detection to prevent
//  a held magnet from auto-advancing.
//  Forward times out if stalled past TIME_WINDOW.
// ============================================================
void pollSequenceNodes() {

  unsigned long elapsed = xktOn ? millis() - transmitterStart : 0;

  // ── determine what is allowed ─────────────────────────────
bool forwardAllowed = (!xktOn && millis() - powerOffTime >= OFF_LOCKOUT) ||
                      (xktOn && elapsed >= XKT_ON_TIME - ALARM_TIME);
  bool reverseAllowed =  xktOn &&
                        elapsed >= RECAST_WINDOW &&
                        elapsed <  XKT_ON_TIME - ALARM_TIME;

  // ── forward sequence timeout ──────────────────────────────
  if (nextExpected > 0 && millis() - firstTrigger > TIME_WINDOW) {
    tprintf("sequence timed out waiting for step %d — resetting\n",
            nextExpected + 1);
    resetForward();
  }
//tprintf("DEBUG elapsed=%lu forwardAllowed=%d reverseAllowed=%d\n",
//        elapsed, forwardAllowed, reverseAllowed);

  // ── FORWARD sequence ──────────────────────────────────────
  if (forwardAllowed) {
    uint8_t addr = seqNodes[nextExpected];
    Wire.requestFrom(addr, 3);
    if (Wire.available() == 3) {
      byte state = Wire.read();
      Wire.read(); Wire.read();   // discard ADC bytes

      bool forwardActive = (state == 1);

      if (forwardActive && !lastForwardState) {
        if (nextExpected == 0) {
          firstTrigger = millis();
          tprintln("the Power Spell sequence started");
        }

        if (millis() - firstTrigger <= TIME_WINDOW) {
          nextExpected++;

          if (nextExpected < NUM_SEQ) {
            unsigned long secsRemaining =
              (TIME_WINDOW - (millis() - firstTrigger) + 500) / 1000;
            tprintf("  step %d of %d confirmed — %lu seconds remaining\n",
                    nextExpected, NUM_SEQ, secsRemaining);
          } else {
            // sequence complete
            tprintf("  step %d of %d confirmed\n", nextExpected, NUM_SEQ);
            if (xktOn) {
              transmitterStart = millis();
              warningStarted   = false;
              sendWarningCommand(0x00);
              tprintln(">>> the Power spell has been extended <<<");
            } else {
              enableTransmitter();
              tprintln(">>> powered for 10 min <<<");
            }
            lastForwardState = forwardActive;
            lastReverseState = false;   // clear reverse edge
            resetForward();
            resetReverse();
            return;   // exit — let transmitter timer gate next attempt
          }

        } else {
          tprintf("step %d reached, but the spell was not completed in time — resetting\n",
                  nextExpected);
          resetForward();
        }
      }
      lastForwardState = forwardActive;
    }
  } else {
    // not polling forward — keep edge state fresh so no stale rising edge
    lastForwardState = false;
  }

  // ── REVERSE sequence ──────────────────────────────────────
  if (reverseAllowed && nextExpected == 0) {
    uint8_t raddr = seqNodes[nextReverse];
    Wire.requestFrom(raddr, 3);
    if (Wire.available() == 3) {
      byte state = Wire.read();
      Wire.read(); Wire.read();   // discard ADC bytes

      bool reverseActive = (state == 1);

      if (reverseActive && !lastReverseState) {
        if (nextReverse == 2) {
          firstReverse = millis();
          tprintln("The Reverse Power spell sequence has started");
        }

        if (millis() - firstReverse <= TIME_WINDOW) {
          nextReverse--;
          tprintf("  Reverse step %d of %d confirmed\n",
                  3 - (nextReverse + 1), NUM_SEQ);

          if (nextReverse < 0) {
            disableTransmitter();
            tprintln(">>> the spell has been undone — Power OFF <<<");
            lastReverseState = reverseActive;
            lastForwardState = false;
            resetReverse();
            return;   // exit immediately
          }

        } else {
          tprintln("Reverse spell not completed in time — resetting");
          resetReverse();
        }
      }
      lastReverseState = reverseActive;
    }
  } else {
    // not polling reverse — clear edge state
    lastReverseState = false;
  }
}

// ============================================================
//  READ NODE POLLING (0x13, 0x14, 0x15, 0x17, 0x18, 0x19)
//  Reads outputOn state only — logs on state changes
// ============================================================
void pollReadNodes() {
  for (int i = 0; i < NUM_READ; i++) {
    uint8_t addr = readNodes[i];

    Wire.requestFrom(addr, 3);
    if (Wire.available() == 3) {
      byte state = Wire.read();
      byte hi    = Wire.read();
      byte lo    = Wire.read();
      int  adc   = (hi << 8) | lo;

      bool isOn = (state == 1);
      if (isOn != readState[i]) {
        readState[i] = isOn;
        tprintf("0x%02X %s is %s  ADC=%d\n",
                addr, readLabels[i], isOn ? "ON" : "OFF", adc);
      }
    }
  }
}


// ============================================================
//  HELPERS
// ============================================================
void enableTransmitter() {
  xktOn            = true;
  transmitterStart = millis();
  warningStarted   = false;
  digitalWrite(SWITCH_PIN, LOW);    // LOW = transmitter ON (PNP)
}

void disableTransmitter() {
  xktOn          = false;
  powerOffTime   = millis();    // ← add this
  warningStarted = false;
  digitalWrite(SWITCH_PIN, HIGH);
  sendWarningCommand(0x00);
  resetReverse();
}

void sendWarningCommand(byte cmd) {
  Wire.beginTransmission(WARNING_NODE);
  Wire.write(cmd);
  Wire.endTransmission();
}

void resetForward() {
  nextExpected = 0;
  firstTrigger = 0;
}

void resetReverse() {
  nextReverse  = 2;
  firstReverse = 0;
}