//Copyright <2026> //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 // ──────────────────────────────────────────────────────────── // 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 0x10 // ← CHANGE THIS for each 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 — DOUBLE 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); }