/*
* RoundTable: Generic foundation code for e-switch flashlights.
* Other possible names:
* - Mostly Harmless
* - FSM / SpaghettiMonster
* - ...
*
* Copyright (C) 2017 Selene ToyKeeper
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "tk-attiny.h"
#include
#include
#include
#include
#include
#include
#include
// typedefs
typedef PROGMEM const uint8_t Event;
typedef Event * EventPtr;
typedef uint8_t (*EventCallbackPtr)(EventPtr event, uint16_t arg);
typedef uint8_t EventCallback(EventPtr event, uint16_t arg);
typedef uint8_t State(EventPtr event, uint16_t arg);
typedef State * StatePtr;
typedef struct Emission {
EventPtr event;
uint16_t arg;
} Emission;
volatile StatePtr current_state;
#define EV_MAX_LEN 16
uint8_t current_event[EV_MAX_LEN];
// at 0.016 ms per tick, 255 ticks = 4.08 s
// TODO: 16 bits?
static volatile uint8_t ticks_since_last_event = 0;
#ifdef USE_LVP
// volts * 10
#define VOLTAGE_LOW 30
// MCU sees voltage 0.X volts lower than actual, add X to readings
#define VOLTAGE_FUDGE_FACTOR 2
volatile uint8_t voltage;
void low_voltage();
#endif
#ifdef USE_THERMAL_REGULATION
volatile int16_t temperature;
void low_temperature();
void high_temperature();
#endif
#ifdef USE_DEBUG_BLINK
#define OWN_DELAY
#define USE_DELAY_4MS
#include "tk-delay.h"
#define DEBUG_FLASH PWM1_LVL = 64; delay_4ms(2); PWM1_LVL = 0;
void debug_blink(uint8_t num) {
for(; num>0; num--) {
PWM1_LVL = 32;
delay_4ms(100/4);
PWM1_LVL = 0;
delay_4ms(100/4);
}
}
#endif
#define A_ENTER_STATE 1
#define A_LEAVE_STATE 2
#define A_TICK 3
#define A_PRESS 4
#define A_HOLD_START 5
#define A_HOLD_TICK 6
#define A_RELEASE 7
#define A_RELEASE_TIMEOUT 8
// TODO: add events for over/under-heat conditions (with parameter for severity)
#define A_OVERHEATING 9
#define A_UNDERHEATING 10
// TODO: add events for low voltage conditions
#define A_VOLTAGE_LOW 11
//#define A_VOLTAGE_CRITICAL 12
#define A_DEBUG 255 // test event for debugging
// TODO: maybe compare events by number instead of pointer?
// (number = index in event types array)
// (comparison would use full event content, but send off index to callbacks)
// (saves space by using uint8_t instead of a pointer)
// (also eliminates the need to duplicate single-entry events like for voltage or timer tick)
// Event types
Event EV_debug[] = {
A_DEBUG,
0 } ;
Event EV_enter_state[] = {
A_ENTER_STATE,
0 } ;
Event EV_leave_state[] = {
A_LEAVE_STATE,
0 } ;
Event EV_tick[] = {
A_TICK,
0 } ;
#ifdef USE_LVP
Event EV_voltage_low[] = {
A_VOLTAGE_LOW,
0 } ;
#endif
#ifdef USE_THERMAL_REGULATION
Event EV_temperature_high[] = {
A_OVERHEATING,
0 } ;
Event EV_temperature_low[] = {
A_UNDERHEATING,
0 } ;
#endif
Event EV_click1_press[] = {
A_PRESS,
0 };
// shouldn't normally happen, but UI might reset event while button is down
// so a release with no recorded prior hold could be possible
Event EV_release[] = {
A_RELEASE,
0 };
Event EV_click1_release[] = {
A_PRESS,
A_RELEASE,
0 };
#define EV_1click EV_click1_complete
Event EV_click1_complete[] = {
A_PRESS,
A_RELEASE,
A_RELEASE_TIMEOUT,
0 };
#define EV_hold EV_click1_hold
// FIXME: Should holds use "start+tick" or just "tick" with a tick number?
// Or "start+tick" with a tick number?
Event EV_click1_hold[] = {
A_PRESS,
A_HOLD_START,
0 };
Event EV_click1_hold_release[] = {
A_PRESS,
A_HOLD_START,
A_RELEASE,
0 };
Event EV_click2_press[] = {
A_PRESS,
A_RELEASE,
A_PRESS,
0 };
Event EV_click2_release[] = {
A_PRESS,
A_RELEASE,
A_PRESS,
A_RELEASE,
0 };
#define EV_2clicks EV_click2_complete
Event EV_click2_complete[] = {
A_PRESS,
A_RELEASE,
A_PRESS,
A_RELEASE,
A_RELEASE_TIMEOUT,
0 };
Event EV_click3_press[] = {
A_PRESS,
A_RELEASE,
A_PRESS,
A_RELEASE,
A_PRESS,
0 };
Event EV_click3_release[] = {
A_PRESS,
A_RELEASE,
A_PRESS,
A_RELEASE,
A_PRESS,
A_RELEASE,
0 };
#define EV_3clicks EV_click3_complete
Event EV_click3_complete[] = {
A_PRESS,
A_RELEASE,
A_PRESS,
A_RELEASE,
A_PRESS,
A_RELEASE,
A_RELEASE_TIMEOUT,
0 };
// ... and so on
// A list of event types for easy iteration
EventPtr event_sequences[] = {
EV_click1_press,
EV_release,
EV_click1_release,
EV_click1_complete,
EV_click1_hold,
EV_click1_hold_release,
EV_click2_press,
EV_click2_release,
EV_click2_complete,
EV_click3_press,
EV_click3_release,
EV_click3_complete,
// ...
};
#define events_match(a,b) compare_event_sequences(a,b)
// return 1 if (a == b), 0 otherwise
uint8_t compare_event_sequences(uint8_t *a, const uint8_t *b) {
for(uint8_t i=0; (i=0; i--) {
uint8_t err = state_stack[i](event, arg);
if (! err) return 0;
}
return 1; // event not handled
}
void emit(EventPtr event, uint16_t arg) {
// add this event to the queue for later,
// so we won't use too much time during an interrupt
append_emission(event, arg);
}
// Search the pre-defined event list for one matching what the user just did,
// and emit it if one was found.
void emit_current_event(uint16_t arg) {
//uint8_t err = 1;
for (uint8_t i=0; i<(sizeof(event_sequences)/sizeof(EventPtr)); i++) {
if (events_match(current_event, event_sequences[i])) {
//DEBUG_FLASH;
//err = emit(event_sequences[i], arg);
//return err;
emit(event_sequences[i], arg);
return;
}
}
//return err;
}
void _set_state(StatePtr new_state) {
// call old state-exit hook (don't use stack)
if (current_state != NULL) current_state(EV_leave_state, 0);
// set new state
current_state = new_state;
// call new state-enter hook (don't use stack)
if (new_state != NULL) current_state(EV_enter_state, 0);
}
int8_t push_state(StatePtr new_state) {
if (state_stack_len < STATE_STACK_SIZE) {
// TODO: call old state's exit hook?
// new hook for non-exit recursion into child?
state_stack[state_stack_len] = new_state;
state_stack_len ++;
_set_state(new_state);
return state_stack_len;
} else {
// TODO: um... how is a flashlight supposed to handle a recursion depth error?
return -1;
}
}
StatePtr pop_state() {
// TODO: how to handle pop from empty stack?
StatePtr old_state = NULL;
StatePtr new_state = NULL;
if (state_stack_len > 0) {
state_stack_len --;
old_state = state_stack[state_stack_len];
}
if (state_stack_len > 0) {
new_state = state_stack[state_stack_len-1];
}
_set_state(new_state);
return old_state;
}
uint8_t set_state(StatePtr new_state) {
// FIXME: this calls exit/enter hooks it shouldn't
pop_state();
return push_state(new_state);
}
// TODO? add events to a queue when inside an interrupt
// instead of calling the event functions directly?
// (then empty the queue in main loop?)
// TODO? new delay() functions which handle queue consumption?
// TODO? new interruptible delay() functions?
//static volatile uint8_t button_was_pressed;
#define BP_SAMPLES 16
uint8_t button_is_pressed() {
// debounce a little
uint8_t highcount = 0;
// measure for 16/64ths of a ms
for(uint8_t i=0; i (BP_SAMPLES/2));
//button_was_pressed = result;
return result;
}
//void button_change_interrupt() {
ISR(PCINT0_vect) {
//DEBUG_FLASH;
// something happened
//ticks_since_last_event = 0;
// add event to current sequence
if (button_is_pressed()) {
push_event(A_PRESS);
} else {
push_event(A_RELEASE);
}
// check if sequence matches any defined sequences
// if so, send event to current state callback
emit_current_event(0);
}
// TODO: implement
ISR(WDT_vect) {
/*
// TODO? safety net for PCINT, in case it misses a press or release
uint8_t bp = button_is_pressed();
if (bp != button_was_pressed) {
// TODO: handle missed button event
if (bp) {
push_event(A_PRESS);
} else {
push_event(A_RELEASE);
}
emit_current_event(0);
}
*/
//timer ++; // Is this needed at all?
/*
if (ticks_since_last_event & 0b00000111 ) {
DEBUG_FLASH;
}
*/
//if (ticks_since_last_event < 0xff) ticks_since_last_event ++;
// increment, but loop from 255 back to 128
ticks_since_last_event = (ticks_since_last_event + 1) \
| (ticks_since_last_event & 0x80);
//static uint8_t hold_ticks = 0; // TODO: 16 bits?
// callback on each timer tick
emit(EV_tick, ticks_since_last_event);
// if time since last event exceeds timeout,
// append timeout to current event sequence, then
// send event to current state callback
// //hold_event(ticks)
// //emit(EV_press_hold, hold_ticks);
// emit_current_event(hold_ticks);
// or
// //release_timeout()
// //emit(EV_press_release_timeout, 0);
// emit_current_event(0);
#if defined(USE_LVP) || defined(USE_THERMAL_REGULATION)
// start a new ADC measurement every 4 ticks
static uint8_t adc_trigger = 0;
adc_trigger ++;
if (adc_trigger > 3) {
adc_trigger = 0;
ADCSRA |= (1 << ADSC) | (1 << ADIE);
}
#endif
}
// TODO: implement? (or is it better done in main()?)
ISR(ADC_vect) {
static uint8_t adc_step = 0;
#ifdef USE_LVP
#ifdef USE_LVP_AVG
#define NUM_VOLTAGE_VALUES 4
static int16_t voltage_values[NUM_VOLTAGE_VALUES];
#endif
static uint8_t lvp_timer = 0;
static uint8_t lvp_lowpass = 0;
#define LVP_TIMER_START 50 // ticks between LVP warnings
#define LVP_LOWPASS_STRENGTH 4
#endif
#ifdef USE_THERMAL_REGULATION
#define NUM_THERMAL_VALUES 4
#define ADC_STEPS 4
static int16_t temperature_values[NUM_THERMAL_VALUES];
#else
#define ADC_STEPS 2
#endif
uint16_t measurement = ADC; // latest 10-bit ADC reading
adc_step = (adc_step + 1) & (ADC_STEPS-1);
#ifdef USE_LVP
// voltage
if (adc_step == 1) {
#ifdef USE_LVP_AVG
// prime on first execution
if (voltage == 0) {
for(uint8_t i=0; i> 2;
voltage = (uint16_t)(1.1*1024*10)/total + VOLTAGE_FUDGE_FACTOR;
}
#else // no USE_LVP_AVG
// calculate actual voltage: volts * 10
// ADC = 1.1 * 1024 / volts
// volts = 1.1 * 1024 / ADC
voltage = (uint16_t)(1.1*1024*10)/measurement + VOLTAGE_FUDGE_FACTOR;
#endif
// if low, callback EV_voltage_low / EV_voltage_critical
// (but only if it has been more than N ticks since last call)
if (lvp_timer) {
lvp_timer --;
} else { // it has been long enough since the last warning
if (voltage < VOLTAGE_LOW) {
if (lvp_lowpass < LVP_LOWPASS_STRENGTH) {
lvp_lowpass ++;
} else {
// try to send out a warning
//uint8_t err = emit(EV_voltage_low, 0);
//uint8_t err = emit_now(EV_voltage_low, 0);
emit(EV_voltage_low, 0);
//if (!err) {
// on successful warning, reset counters
lvp_timer = LVP_TIMER_START;
lvp_lowpass = 0;
//}
}
} else {
// voltage not low? reset count
lvp_lowpass = 0;
}
}
}
#endif // ifdef USE_LVP
// TODO: temperature
// start another measurement for next time
#ifdef USE_THERMAL_REGULATION
#ifdef USE_LVP
if (adc_step < 2) ADMUX = ADMUX_VCC;
else ADMUX = ADMUX_THERM;
#else
ADMUX = ADMUX_THERM;
#endif
#else
#ifdef USE_LVP
ADMUX = ADMUX_VCC;
#endif
#endif
}
inline void ADC_on()
{
// read voltage on VCC by default
// disable digital input on VCC pin to reduce power consumption
//DIDR0 |= (1 << ADC_DIDR); // FIXME: unsure how to handle for VCC pin
// VCC / 1.1V reference
ADMUX = ADMUX_VCC;
// enable, start, prescale
ADCSRA = (1 << ADEN) | (1 << ADSC) | ADC_PRSCL;
}
inline void ADC_off() {
ADCSRA &= ~(1<= 1
DDRB |= (1 << PWM1_PIN);
TCCR0B = 0x01; // pre-scaler for timer (1 => 1, 2 => 8, 3 => 64...)
TCCR0A = PHASE;
#elif PWM_CHANNELS >= 2
DDRB |= (1 << PWM2_PIN);
#elif PWM_CHANNELS >= 3
DDRB |= (1 << PWM3_PIN);
// Second PWM counter is ... weird
TCCR1 = _BV (CS10);
GTCCR = _BV (COM1B1) | _BV (PWM1B);
OCR1C = 255; // Set ceiling value to maximum
#elif PWM_CHANNELS == 4
// FIXME: How exactly do we do PWM on channel 4?
DDRB |= (1 << PWM4_PIN);
#endif
// TODO: turn on ADC?
// configure e-switch
PORTB = (1 << SWITCH_PIN); // e-switch is the only input
PCMSK = (1 << SWITCH_PIN); // pin change interrupt uses this pin
// TODO: configure sleep mode
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
// Read config values and saved state
// restore_state(); // TODO
// TODO: handle long press vs short press (or even medium press)?
#ifdef USE_DEBUG_BLINK
//debug_blink(1);
#endif
// all booted -- turn interrupts back on
PCINT_on();
WDT_on();
ADC_on();
sei();
// fallback for handling a few things
push_state(default_state);
// call recipe's setup
setup();
// main loop
while (1) {
// TODO: update e-switch press state?
// TODO: check voltage?
// TODO: check temperature?
// if event queue not empty, process and pop first item in queue?
if (emissions[0].event != NULL) {
emit_now(emissions[0].event, emissions[0].arg);
delete_first_emission();
}
}
}