// ATtiny85 (Digispark-style, 16.5 MHz core)
// P0 (PB0/OC0A) -> 1k -> FemtoBuck DIM/CTRL
// P2 (PB2) -> button to GND (INPUT_PULLUP enabled)
const uint8_t PIN_PWM = 0; // P0: OC0A
const uint8_t PIN_BTN = 2; // P2: button (to GND)
// ---- Timing & behaviour constants ----
const uint16_t DEBOUNCE_MS = 30; // debounce for button edges
const uint16_t HOLD_THRESHOLD_MS = 350; // press longer than this counts as "hold"
const uint16_t ADJUST_WINDOW_MS = 2000; // after turning on, how long quick presses dim in steps
const uint16_t RAMP_TIME_MS = 3000; // 0% -> 100% ramp duration when holding
// Step levels (100%, 75%, 50%, 25%, 0%)
const uint8_t STEP_PWM[] = {255, 191, 128, 64, 0};
const uint8_t NUM_STEPS = sizeof(STEP_PWM);
// ---- State machine ----
enum State : uint8_t {
OFF_STATE = 0,
ON_ADJUST, // within adjust window: quick presses step 100->75->50->25->0
ON_LOCKED, // after adjust window: next press turns OFF
RAMPING // holding from OFF ramps 0->100 over 3s
};
State state = OFF_STATE;
// ---- Book-keeping ----
uint8_t stepIndex = 0; // index into STEP_PWM when in ON_ADJUST
uint32_t lastStableTime = 0; // for debounce
bool btnStable = true; // stable debounced level
bool btnPrevStable = true;
uint32_t pressStartMs = 0; // time when a press started (debounced)
uint32_t adjustDeadline = 0; // time when ON_ADJUST -> ON_LOCKED
uint32_t rampStartMs = 0; // when ramp began
// ---- Helpers ----
// CHANGED: track current PWM so we can lock to it after a hold
volatile uint8_t pwmLevel = 0; // last value written to PWM (0..255)
void setPWM(uint8_t val) {
pwmLevel = val; // CHANGED
analogWrite(PIN_PWM, val);
}
uint8_t currentPWM() {
return pwmLevel; // CHANGED
}
// Debounced button read (returns HIGH when not pressed, LOW when pressed)
bool readButtonDebounced() {
bool raw = digitalRead(PIN_BTN);
if (raw != btnStable) {
if (millis() - lastStableTime >= DEBOUNCE_MS) {
btnStable = raw;
lastStableTime = millis();
}
} else {
lastStableTime = millis();
}
return btnStable;
}
void enterOff() {
state = OFF_STATE;
setPWM(0);
}
void enterOnAdjustAt100() {
state = ON_ADJUST;
stepIndex = 0; // 100%
setPWM(STEP_PWM[stepIndex]);
adjustDeadline = millis() + ADJUST_WINDOW_MS;
}
void stepDimOrOff() {
if (stepIndex + 1 < NUM_STEPS) {
stepIndex++;
setPWM(STEP_PWM[stepIndex]);
if (STEP_PWM[stepIndex] == 0) {
enterOff();
} else {
// Extend the window so rapid multi-press stepping stays responsive
adjustDeadline = millis() + ADJUST_WINDOW_MS;
}
} else {
// Already at 0 -> ensure OFF
enterOff();
}
}
void enterOnLocked(uint8_t pwmVal) {
state = ON_LOCKED;
setPWM(pwmVal);
}
void startRamp() {
state = RAMPING;
rampStartMs = millis();
setPWM(0); // start from 0%
}
void updateRamp() {
uint32_t elapsed = millis() - rampStartMs;
if (elapsed >= RAMP_TIME_MS) {
setPWM(255); // cap at 100%
// Stay in RAMPING until release; no further increase
} else {
// Linear ramp 0..255 over RAMP_TIME_MS
uint16_t pwm = (uint32_t)255 * elapsed / RAMP_TIME_MS;
setPWM((uint8_t)pwm);
}
}
void setup() {
pinMode(PIN_PWM, OUTPUT);
pinMode(PIN_BTN, INPUT_PULLUP);
// Optional: tweak Timer0 PWM freq (uncomment if you want ~1 kHz)
// TCCR0B = (TCCR0B & 0b11111000) | 0x02;
enterOff();
btnStable = digitalRead(PIN_BTN);
btnPrevStable = btnStable;
lastStableTime = millis();
}
void loop() {
bool btn = readButtonDebounced(); // HIGH = released, LOW = pressed
// Detect edges
bool fell = (btnPrevStable == HIGH && btn == LOW);
bool rose = (btnPrevStable == LOW && btn == HIGH);
// State-independent: handle timing transitions
if (state == ON_ADJUST && (int32_t)(millis() - adjustDeadline) >= 0) {
// Adjust window expired -> lock level
enterOnLocked(STEP_PWM[stepIndex]);
}
// Ramping updates while held
if (state == RAMPING && btn == LOW) {
updateRamp();
}
// Edge-driven logic
if (fell) {
// Button pressed
pressStartMs = millis();
if (state == OFF_STATE) {
// We won't act immediately—wait to see if it's a hold.
// If it becomes a hold, we'll startRamp() after HOLD_THRESHOLD_MS in the loop below.
}
}
// Long-press detection from OFF (begin ramp once threshold exceeded and still held)
if (state == OFF_STATE && btn == LOW) {
if ((millis() - pressStartMs) >= HOLD_THRESHOLD_MS) {
startRamp();
}
}
if (rose) {
// Button released
uint32_t pressDur = millis() - pressStartMs;
switch (state) {
case OFF_STATE:
if (pressDur < HOLD_THRESHOLD_MS) {
// Short press from OFF -> ON at 100% and start adjust window
enterOnAdjustAt100();
} else {
// Rare race: lock whatever PWM we actually had (not 0)
enterOnLocked(currentPWM()); // CHANGED
}
break;
case RAMPING: {
// Stop ramp at current brightness; next press should turn off
enterOnLocked(currentPWM()); // CHANGED
break;
}
case ON_ADJUST:
if (pressDur < HOLD_THRESHOLD_MS) {
// Within window: step down 100->75->50->25->0
stepDimOrOff();
} else {
// Long hold while in adjust—no special action
}
break;
case ON_LOCKED:
if (pressDur < HOLD_THRESHOLD_MS) {
// Locked behaviour: any short press turns OFF
enterOff();
} else {
// Long hold while locked: keep as-is (no special behaviour defined)
}
break;
}
}
btnPrevStable = btn;
}