From 8d2f442317fd3678213a14307f95d2f5b54c5623 Mon Sep 17 00:00:00 2001
From: Selene ToyKeeper
Date: Sat, 19 Aug 2017 12:17:58 -0600
Subject: Renamed from RoundTable to SpaghettiMonster (FSM).
---
spaghetti-monster/fsm-ramping.h | 26 ++
spaghetti-monster/momentary.c | 85 ++++
spaghetti-monster/spaghetti-monster.h | 737 ++++++++++++++++++++++++++++++++++
3 files changed, 848 insertions(+)
create mode 100644 spaghetti-monster/fsm-ramping.h
create mode 100644 spaghetti-monster/momentary.c
create mode 100644 spaghetti-monster/spaghetti-monster.h
(limited to 'spaghetti-monster')
diff --git a/spaghetti-monster/fsm-ramping.h b/spaghetti-monster/fsm-ramping.h
new file mode 100644
index 0000000..fd4d40b
--- /dev/null
+++ b/spaghetti-monster/fsm-ramping.h
@@ -0,0 +1,26 @@
+/*
+ * fsm-ramping.h: Ramping functions for SpaghettiMonster.
+ * Handles 1- to 4-channel smooth ramping on a single LED.
+ *
+ * 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 .
+ */
+
+// TODO: ramp tables
+// TODO: RAMP_SIZE / MAX_LVL
+// TODO: actual_lvl
+// TODO: target_lvl
+// TODO: set_lvl
+// TODO: set_lvl_smooth
diff --git a/spaghetti-monster/momentary.c b/spaghetti-monster/momentary.c
new file mode 100644
index 0000000..bb74795
--- /dev/null
+++ b/spaghetti-monster/momentary.c
@@ -0,0 +1,85 @@
+/*
+ * Momentary: Very simple example UI for SpaghettiMonster.
+ * Is intended to be the simplest possible FSM e-switch UI.
+ * The light is on while the button is held; off otherwise.
+ *
+ * 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 .
+ */
+
+#define FSM_EMISAR_D4_LAYOUT
+#define USE_LVP
+#define USE_DEBUG_BLINK
+#include "spaghetti-monster.h"
+
+volatile uint8_t brightness;
+volatile uint8_t on_now;
+
+void light_on() {
+ on_now = 1;
+ PWM1_LVL = brightness;
+ PWM2_LVL = 0;
+}
+
+void light_off() {
+ on_now = 0;
+ PWM1_LVL = 0;
+ PWM2_LVL = 0;
+}
+
+uint8_t momentary_state(EventPtr event, uint16_t arg) {
+
+ if (event == EV_click1_press) {
+ brightness = 255;
+ light_on();
+ // don't attempt to parse multiple clicks
+ empty_event_sequence();
+ return 0;
+ }
+
+ else if (event == EV_release) {
+ light_off();
+ // don't attempt to parse multiple clicks
+ empty_event_sequence();
+ return 0;
+ }
+
+ else if (event == EV_debug) {
+ //PWM1_LVL = arg&0xff;
+ DEBUG_FLASH;
+ return 0;
+ }
+
+ // event not handled
+ return 1;
+}
+
+// LVP / low-voltage protection
+void low_voltage() {
+ debug_blink(3);
+ if (brightness > 0) {
+ brightness >>= 1;
+ if (on_now) light_on();
+ } else {
+ light_off();
+ standby_mode();
+ }
+}
+
+void setup() {
+ debug_blink(2);
+
+ push_state(momentary_state);
+}
diff --git a/spaghetti-monster/spaghetti-monster.h b/spaghetti-monster/spaghetti-monster.h
new file mode 100644
index 0000000..af6d828
--- /dev/null
+++ b/spaghetti-monster/spaghetti-monster.h
@@ -0,0 +1,737 @@
+/*
+ * SpaghettiMonster: Generic foundation code for e-switch flashlights.
+ * Other possible names:
+ * - FSM
+ * - RoundTable
+ * - Mostly Harmless
+ * - ...
+ *
+ * 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();
+ }
+ }
+}
--
cgit v1.2.3