//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.
// ============================================================
// ATtiny412 Universal Node v8
// All nodes run identical firmware
// - Sensor reports to ESP32-C6 Teacher
// - ESP32-C6 Teacher handles all sequence logic
// - Optional analog output at ATtiny node without Teacher oversight
// - output ramps up to full power over FADE_IN_TIME
// - fades off after OUTPUT_ON_TIME
// - output turns off immediately if triggered again
// - Optional digital output at ATtiny node without Teacher oversight
//
// Sensor options:
// - SENSOR_ANALOG: A1324 linear Hall sensor
// analogRead + dynamic baseline deviation detection
// responds to both magnet poles
// - SENSOR_DIGITAL: A3144 switch Hall sensor
// digitalRead, output LOW when south pole present
// no baseline needed
//
// volatile used for lastReading and sensorActive — shared between
// main loop and I2C interrupt (sendStatus). noInterrupts() protects
// the sensor read so I2C polling cannot corrupt mid-update values.
// ============================================================
#include <Wire.h>
// ────────────────────────────────────────────────────────────
// STEP 1 — SET UNIQUE ADDRESS FOR THIS NODE
// Sequence nodes: 0x10, 0x11, 0x12
// Output nodes: 0x13, 0x14, 0x15, 0x17, 0x18, 0x19
// Warning node: 0x16 (use attiny412_warning_node.ino)
// ────────────────────────────────────────────────────────────
#define MY_ADDRESS 0x15 // ← CHANGE THIS per node
// ────────────────────────────────────────────────────────────
// STEP 2 — UNCOMMENT ONLY ONE NODE TYPE
// ────────────────────────────────────────────────────────────
//#define NODE_NO_OUTPUT // sensor reading only
#define NODE_WITH_ANALOG // PWM output from ANALOG_PIN
//#define NODE_WITH_DIGITAL // digital output from DIGITAL_PIN
// ────────────────────────────────────────────────────────────
// STEP 3 — UNCOMMENT ONLY ONE SENSOR TYPE
// SENSOR_ANALOG: A1324 linear — analogRead + baseline deviation
// SENSOR_DIGITAL: A3144 switch — digitalRead, south pole only
// ────────────────────────────────────────────────────────────
//#define SENSOR_ANALOG // A1324 linear Hall sensor
#define SENSOR_DIGITAL // A3144 switch Hall sensor
// ────────────────────────────────────────────────────────────
// STEP 4 — CHECK PIN ASSIGNMENTS
// ────────────────────────────────────────────────────────────
#define SENSOR_PIN PIN_PA3 // Hall sensor input
#define ANALOG_PIN PIN_PA7 // analogWrite PWM output
#define DIGITAL_PIN PIN_PA6 // digitalWrite on/off output
// ────────────────────────────────────────────────────────────
// COMPILE-TIME SAFETY CHECKS
// ────────────────────────────────────────────────────────────
#if defined(NODE_WITH_ANALOG) && defined(NODE_WITH_DIGITAL)
#error "Cannot define both NODE_WITH_ANALOG and NODE_WITH_DIGITAL — pick one"
#endif
#if defined(SENSOR_ANALOG) && defined(SENSOR_DIGITAL)
#error "Cannot define both SENSOR_ANALOG and SENSOR_DIGITAL — pick one"
#endif
#if !defined(SENSOR_ANALOG) && !defined(SENSOR_DIGITAL)
#error "Must define either SENSOR_ANALOG or SENSOR_DIGITAL"
#endif
// ────────────────────────────────────────────────────────────
// DEVIATION THRESHOLD (SENSOR_ANALOG only)
// ────────────────────────────────────────────────────────────
#define DEVIATION 10 // ADC counts from baseline — lower = more sensitive
// ────────────────────────────────────────────────────────────
// TIMING
// ────────────────────────────────────────────────────────────
#define OUTPUT_ON_TIME 10000 // ms at full brightness before fade begins
#define FADE_DURATION 2000 // ms to fade out
#define FADE_IN_TIME 2000 // ms to fade in
// ────────────────────────────────────────────────────────────
// STATE
// lastReading and sensorActive are volatile — shared between
// main loop and I2C interrupt handler sendStatus()
// ────────────────────────────────────────────────────────────
volatile int lastReading = 0;
volatile bool sensorActive = false;
bool lastSensorState = false;
bool outputOn = false;
unsigned long lastTriggerTime = 0;
int pwmVoltage = 0;
int baseline = 0; // used by SENSOR_ANALOG only
// ============================================================
// SETUP
// ============================================================
void setup() {
Wire.begin(MY_ADDRESS);
Wire.onRequest(sendStatus);
// ── sensor pin mode ───────────────────────────────────────
#ifdef SENSOR_ANALOG
pinMode(SENSOR_PIN, INPUT);
// baseline calibration — 64 samples over ~320ms
long sum = 0;
for (int i = 0; i < 64; i++) {
sum += analogRead(SENSOR_PIN);
delay(5);
}
baseline = sum / 64;
#endif
#ifdef SENSOR_DIGITAL
pinMode(SENSOR_PIN, INPUT_PULLUP); // A3144 open-collector — needs pullup
#endif
// ── output pin setup ──────────────────────────────────────
#ifdef NODE_WITH_ANALOG
pinMode(ANALOG_PIN, OUTPUT);
analogWrite(ANALOG_PIN, 0);
#endif
#ifdef NODE_WITH_DIGITAL
pinMode(DIGITAL_PIN, OUTPUT);
digitalWrite(DIGITAL_PIN, LOW);
#endif
}
// ============================================================
// MAIN LOOP
// ============================================================
void loop() {
// ── protected sensor read ─────────────────────────────────
// noInterrupts() prevents I2C interrupt from reading
// lastReading mid-update (would split high/low bytes)
#ifdef SENSOR_ANALOG
int reading = analogRead(SENSOR_PIN);
noInterrupts();
lastReading = reading;
sensorActive = abs(reading - baseline) > DEVIATION;
interrupts();
#endif
#ifdef SENSOR_DIGITAL
int reading = digitalRead(SENSOR_PIN);
noInterrupts();
lastReading = reading;
sensorActive = (reading == LOW); // A3144 pulls LOW when south pole present
interrupts();
#endif
// ── ANALOG OUTPUT ─────────────────────────────────────────
#ifdef NODE_WITH_ANALOG
if (sensorActive && !lastSensorState) {
if (!outputOn) {
// first trigger — start fade in
outputOn = true;
lastTriggerTime = millis();
pwmVoltage = 0;
analogWrite(ANALOG_PIN, 0);
} else {
// second trigger while on — turn off immediately
outputOn = false;
pwmVoltage = 0;
analogWrite(ANALOG_PIN, 0);
}
}
// ── output lifecycle ──────────────────────────────────
if (outputOn) {
unsigned long elapsed = millis() - lastTriggerTime;
if (elapsed >= OUTPUT_ON_TIME) {
// fully off
outputOn = false;
pwmVoltage = 0;
analogWrite(ANALOG_PIN, 0);
} else if (elapsed >= OUTPUT_ON_TIME - FADE_DURATION && elapsed > FADE_IN_TIME) {
// fade out over FADE_DURATION
unsigned long fadeElapsed = elapsed - (OUTPUT_ON_TIME - FADE_DURATION);
pwmVoltage = map(fadeElapsed, 0, FADE_DURATION, 255, 0);
pwmVoltage = constrain(pwmVoltage, 0, 255);
analogWrite(ANALOG_PIN, pwmVoltage);
} else if (elapsed < FADE_IN_TIME) {
// fade in over FADE_IN_TIME
pwmVoltage = map(elapsed, 0, FADE_IN_TIME, 0, 255);
pwmVoltage = constrain(pwmVoltage, 0, 255);
analogWrite(ANALOG_PIN, pwmVoltage);
}
}
#endif
// ── DIGITAL OUTPUT ────────────────────────────────────────
#ifdef NODE_WITH_DIGITAL
if (sensorActive && !lastSensorState) {
if (!outputOn) {
outputOn = true;
lastTriggerTime = millis();
digitalWrite(DIGITAL_PIN, HIGH);
} else {
outputOn = false;
digitalWrite(DIGITAL_PIN, LOW);
}
}
if (outputOn && millis() - lastTriggerTime >= OUTPUT_ON_TIME) {
outputOn = false;
digitalWrite(DIGITAL_PIN, LOW);
}
#endif
lastSensorState = sensorActive;
delay(20); // 20ms loop — smooth fade steps
}
// ============================================================
// I2C CALLBACK — ESP32-C6 requests 3 bytes:
// byte 1: outputOn state (0 or 1)
// byte 2: raw ADC high byte
// byte 3: raw ADC low byte
//
// Note: for SENSOR_DIGITAL, lastReading is 0 or 1 (not 0-1023)
// The high/low byte split is harmless for small values.
// ============================================================
void sendStatus() {
Wire.write(outputOn ? 1 : 0);
Wire.write((lastReading >> 8) & 0xFF);
Wire.write(lastReading & 0xFF);
}