From 7cb4fe0944b839f28dfd96a88a772cd6a8b58019 Mon Sep 17 00:00:00 2001 From: Selene ToyKeeper Date: Thu, 2 Nov 2023 17:16:25 -0600 Subject: reorganized project files (part 1) (just moved files, didn't change the contents yet, and nothing will work without updating #includes and build scripts and stuff) --- ui/anduril/Makefile | 14 + ui/anduril/anduril.c | 396 ++++ ui/anduril/aux-leds.c | 210 ++ ui/anduril/aux-leds.h | 65 + ui/anduril/battcheck-mode-fsm.h | 18 + ui/anduril/battcheck-mode.c | 82 + ui/anduril/battcheck-mode.h | 12 + ui/anduril/beacon-mode.c | 53 + ui/anduril/beacon-mode.h | 9 + ui/anduril/candle-mode.c | 136 ++ ui/anduril/candle-mode.h | 13 + ui/anduril/channel-modes.c | 237 +++ ui/anduril/channel-modes.h | 26 + ui/anduril/config-default.h | 207 ++ ui/anduril/config-mode.c | 196 ++ ui/anduril/config-mode.h | 24 + ui/anduril/factory-reset-fsm.h | 10 + ui/anduril/factory-reset.c | 73 + ui/anduril/factory-reset.h | 8 + ui/anduril/ff-strobe-modes.c | 62 + ui/anduril/ff-strobe-modes.h | 15 + ui/anduril/load-save-config-fsm.h | 139 ++ ui/anduril/load-save-config.c | 33 + ui/anduril/load-save-config.h | 173 ++ ui/anduril/lockout-mode-fsm.h | 11 + ui/anduril/lockout-mode.c | 219 ++ ui/anduril/lockout-mode.h | 16 + ui/anduril/misc.c | 42 + ui/anduril/misc.h | 10 + ui/anduril/momentary-mode.c | 67 + ui/anduril/momentary-mode.h | 11 + ui/anduril/off-mode.c | 384 ++++ ui/anduril/off-mode.h | 12 + ui/anduril/ramp-mode-fsm.h | 38 + ui/anduril/ramp-mode.c | 741 +++++++ ui/anduril/ramp-mode.h | 224 ++ ui/anduril/smooth-steps.c | 47 + ui/anduril/smooth-steps.h | 19 + ui/anduril/sos-mode.c | 56 + ui/anduril/sos-mode.h | 11 + ui/anduril/strobe-modes-fsm.h | 55 + ui/anduril/strobe-modes.c | 332 +++ ui/anduril/strobe-modes.h | 71 + ui/anduril/sunset-timer.c | 60 + ui/anduril/sunset-timer.h | 17 + ui/anduril/tactical-mode.c | 109 + ui/anduril/tactical-mode.h | 22 + ui/anduril/tempcheck-mode.c | 56 + ui/anduril/tempcheck-mode.h | 12 + ui/anduril/tint-ramping.c | 86 + ui/anduril/tint-ramping.h | 21 + ui/anduril/version-check-mode.c | 31 + ui/anduril/version-check-mode.h | 19 + ui/anduril/version.h | 4 + ui/baton/baton.c | 188 ++ ui/baton/baton.txt | 21 + ui/darkhorse/darkhorse.c | 367 ++++ ui/fireflies-ui/Makefile | 7 + ui/fireflies-ui/build-all.sh | 13 + ui/fireflies-ui/cfg-ff-e01.h | 44 + ui/fireflies-ui/cfg-ff-e07-2.h | 27 + ui/fireflies-ui/fireflies-ui.c | 2386 +++++++++++++++++++++ ui/meteor/meteor.c | 556 +++++ ui/momentary/momentary.c | 80 + ui/ramping-ui/ramping-ui.c | 359 ++++ ui/rampingios/Makefile | 7 + ui/rampingios/build-all.sh | 13 + ui/rampingios/rampingios-v3.html | 501 +++++ ui/rampingios/rampingios-v3.md | 262 +++ ui/rampingios/rampingios-v3.txt | 324 +++ ui/rampingios/rampingiosv3-ui.png | Bin 0 -> 240749 bytes ui/rampingios/rampingiosv3.c | 1253 +++++++++++ ui/rampingios/rampingiosv3.svg | 4113 +++++++++++++++++++++++++++++++++++++ ui/werner/Makefile | 7 + ui/werner/README | 56 + ui/werner/build-all.sh | 15 + ui/werner/werner.c | 715 +++++++ 77 files changed, 16328 insertions(+) create mode 100644 ui/anduril/Makefile create mode 100644 ui/anduril/anduril.c create mode 100644 ui/anduril/aux-leds.c create mode 100644 ui/anduril/aux-leds.h create mode 100644 ui/anduril/battcheck-mode-fsm.h create mode 100644 ui/anduril/battcheck-mode.c create mode 100644 ui/anduril/battcheck-mode.h create mode 100644 ui/anduril/beacon-mode.c create mode 100644 ui/anduril/beacon-mode.h create mode 100644 ui/anduril/candle-mode.c create mode 100644 ui/anduril/candle-mode.h create mode 100644 ui/anduril/channel-modes.c create mode 100644 ui/anduril/channel-modes.h create mode 100644 ui/anduril/config-default.h create mode 100644 ui/anduril/config-mode.c create mode 100644 ui/anduril/config-mode.h create mode 100644 ui/anduril/factory-reset-fsm.h create mode 100644 ui/anduril/factory-reset.c create mode 100644 ui/anduril/factory-reset.h create mode 100644 ui/anduril/ff-strobe-modes.c create mode 100644 ui/anduril/ff-strobe-modes.h create mode 100644 ui/anduril/load-save-config-fsm.h create mode 100644 ui/anduril/load-save-config.c create mode 100644 ui/anduril/load-save-config.h create mode 100644 ui/anduril/lockout-mode-fsm.h create mode 100644 ui/anduril/lockout-mode.c create mode 100644 ui/anduril/lockout-mode.h create mode 100644 ui/anduril/misc.c create mode 100644 ui/anduril/misc.h create mode 100644 ui/anduril/momentary-mode.c create mode 100644 ui/anduril/momentary-mode.h create mode 100644 ui/anduril/off-mode.c create mode 100644 ui/anduril/off-mode.h create mode 100644 ui/anduril/ramp-mode-fsm.h create mode 100644 ui/anduril/ramp-mode.c create mode 100644 ui/anduril/ramp-mode.h create mode 100644 ui/anduril/smooth-steps.c create mode 100644 ui/anduril/smooth-steps.h create mode 100644 ui/anduril/sos-mode.c create mode 100644 ui/anduril/sos-mode.h create mode 100644 ui/anduril/strobe-modes-fsm.h create mode 100644 ui/anduril/strobe-modes.c create mode 100644 ui/anduril/strobe-modes.h create mode 100644 ui/anduril/sunset-timer.c create mode 100644 ui/anduril/sunset-timer.h create mode 100644 ui/anduril/tactical-mode.c create mode 100644 ui/anduril/tactical-mode.h create mode 100644 ui/anduril/tempcheck-mode.c create mode 100644 ui/anduril/tempcheck-mode.h create mode 100644 ui/anduril/tint-ramping.c create mode 100644 ui/anduril/tint-ramping.h create mode 100644 ui/anduril/version-check-mode.c create mode 100644 ui/anduril/version-check-mode.h create mode 100644 ui/anduril/version.h create mode 100644 ui/baton/baton.c create mode 100644 ui/baton/baton.txt create mode 100644 ui/darkhorse/darkhorse.c create mode 100644 ui/fireflies-ui/Makefile create mode 100755 ui/fireflies-ui/build-all.sh create mode 100644 ui/fireflies-ui/cfg-ff-e01.h create mode 100644 ui/fireflies-ui/cfg-ff-e07-2.h create mode 100644 ui/fireflies-ui/fireflies-ui.c create mode 100644 ui/meteor/meteor.c create mode 100644 ui/momentary/momentary.c create mode 100644 ui/ramping-ui/ramping-ui.c create mode 100644 ui/rampingios/Makefile create mode 100755 ui/rampingios/build-all.sh create mode 100644 ui/rampingios/rampingios-v3.html create mode 100644 ui/rampingios/rampingios-v3.md create mode 100644 ui/rampingios/rampingios-v3.txt create mode 100644 ui/rampingios/rampingiosv3-ui.png create mode 100644 ui/rampingios/rampingiosv3.c create mode 100644 ui/rampingios/rampingiosv3.svg create mode 100644 ui/werner/Makefile create mode 100644 ui/werner/README create mode 100755 ui/werner/build-all.sh create mode 100644 ui/werner/werner.c (limited to 'ui') diff --git a/ui/anduril/Makefile b/ui/anduril/Makefile new file mode 100644 index 0000000..332f0f3 --- /dev/null +++ b/ui/anduril/Makefile @@ -0,0 +1,14 @@ +all: + ./build-all.sh + +clean: + rm -f *.hex *~ *.elf *.o + +todo: + @egrep 'TODO:|FIXME:' *.[ch] + +models: + @./models.py > MODELS + @cat MODELS + +.phony: clean todo diff --git a/ui/anduril/anduril.c b/ui/anduril/anduril.c new file mode 100644 index 0000000..e46eeaf --- /dev/null +++ b/ui/anduril/anduril.c @@ -0,0 +1,396 @@ +// Anduril: Narsil-inspired UI for SpaghettiMonster. +// (Anduril is Aragorn's sword, the blade Narsil reforged) +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +/* + * Usually a program would be structured like this... + * - Library headers + * - App headers + * - App code + * + * ... in each source file. + * ... and each library and part of the program would be linked together. + * + * But this doesn't follow that pattern, because it's using the + * -fwhole-program + * flag to reduce the compiled size. It lets us fit more features + * in a tiny MCU chip's ROM. + * + * So the structure is like this instead... + * - App-level configuration headers + * - Default config + * - Per build target config + * - Library-level configuration headers + * - Library code (FSM itself) + * - App headers + * - App code (all of it, inline) + * + * Don't do this in regular programs. It's weird and kind of gross. + * But in this case it gives us a bunch of much-needed space, so... woot. + * + * Also, there are a ton of compile-time options because it needs to build + * a bunch of different versions and each one needs to be trimmed as small + * as possible. These are mostly "USE" flags. + */ + +/********* User-configurable options *********/ +#include "config-default.h" + +/********* specific settings for known driver types *********/ +// Anduril config file name (set it here or define it at the gcc command line) +//#define CFG_H cfg-blf-q8.h + +#include "tk.h" +#include incfile(CFG_H) + + +/********* Include headers which need to be before FSM *********/ + +// enable FSM features needed by basic ramping functions +#include "ramp-mode-fsm.h" + +#ifdef USE_FACTORY_RESET +#include "factory-reset-fsm.h" +#endif + +#ifdef USE_BATTCHECK_MODE +#include "battcheck-mode-fsm.h" +#endif + +#ifdef USE_LOCKOUT_MODE +#include "lockout-mode-fsm.h" +#endif + +// enable FSM features needed by strobe modes +#include "strobe-modes-fsm.h" + +// figure out how many bytes of eeprom are needed, +// based on which UI features are enabled +// (include this one last) +#include "load-save-config-fsm.h" + + +/********* bring in FSM / SpaghettiMonster *********/ +#define USE_IDLE_MODE // reduce power use while awake and no tasks are pending + +#include "spaghetti-monster.h" + +/********* does this build target have special code to include? *********/ +#ifdef HWDEF_C_FILE +#include incfile(HWDEF_C_FILE) +#endif +#ifdef CFG_C_FILE +#include incfile(CFG_C_FILE) +#endif + + +/********* Include all the regular app headers *********/ + +#include "off-mode.h" +#include "ramp-mode.h" +#include "config-mode.h" +#include "aux-leds.h" +#include "misc.h" + +#ifdef USE_SUNSET_TIMER +#include "sunset-timer.h" +#endif + +#ifdef USE_VERSION_CHECK +#include "version-check-mode.h" +#endif + +#ifdef USE_BATTCHECK_MODE +#include "battcheck-mode.h" +#endif + +#ifdef USE_BEACON_MODE +#include "beacon-mode.h" +#endif + +#ifdef USE_THERMAL_REGULATION +#include "tempcheck-mode.h" +#endif + +#ifdef USE_LOCKOUT_MODE +#include "lockout-mode.h" +#endif + +#ifdef USE_MOMENTARY_MODE +#include "momentary-mode.h" +#endif + +#ifdef USE_TACTICAL_MODE +#include "tactical-mode.h" +#endif + +// allow the channel mode handler even when only 1 mode +// (so a tint ramp light could still use 3H even if there's no other mode) +#if defined(USE_CHANNEL_MODES) +#include "channel-modes.h" +#endif + +#ifdef USE_FACTORY_RESET +#include "factory-reset.h" +#endif + +// this one detects its own enable/disable settings +#include "strobe-modes.h" + +#ifdef USE_SOS_MODE +#include "sos-mode.h" +#endif + +#ifdef USE_SMOOTH_STEPS +#include "smooth-steps.h" +#endif + +// this should be last, so other headers have a chance to declare values +#include "load-save-config.h" + + +/********* Include all the app logic source files *********/ +// (is a bit weird to do things this way, +// but it saves a lot of space by letting us use the -fwhole-program flag) + +#include "off-mode.c" +#include "ramp-mode.c" +#include "load-save-config.c" +#include "config-mode.c" +#include "aux-leds.c" +#include "misc.c" + +#ifdef USE_SUNSET_TIMER +#include "sunset-timer.c" +#endif + +#ifdef USE_VERSION_CHECK +#include "version-check-mode.c" +#endif + +#ifdef USE_BATTCHECK_MODE +#include "battcheck-mode.c" +#endif + +#ifdef USE_BEACON_MODE +#include "beacon-mode.c" +#endif + +#ifdef USE_THERMAL_REGULATION +#include "tempcheck-mode.c" +#endif + +#ifdef USE_LOCKOUT_MODE +#include "lockout-mode.c" +#endif + +#ifdef USE_MOMENTARY_MODE +#include "momentary-mode.c" +#endif + +#ifdef USE_TACTICAL_MODE +#include "tactical-mode.c" +#endif + +#if defined(USE_CHANNEL_MODES) +#include "channel-modes.c" +#endif + +#ifdef USE_FACTORY_RESET +#include "factory-reset.c" +#endif + +#ifdef USE_STROBE_STATE +#include "strobe-modes.c" +#endif + +#ifdef USE_SOS_MODE +#include "sos-mode.c" +#endif + +#ifdef USE_SMOOTH_STEPS +#include "smooth-steps.c" +#endif + + +// runs one time at boot, when power is connected +void setup() { + + #ifndef START_AT_MEMORIZED_LEVEL + + // regular e-switch light, no hard clicky power button + + // blink at power-on to let user know power is connected + blink_once(); + + #ifdef USE_FACTORY_RESET + if (button_is_pressed()) + factory_reset(); + #endif + + load_config(); + + #if defined(USE_MANUAL_MEMORY) && defined(USE_MANUAL_MEMORY_TIMER) + // without this, initial boot-up brightness is wrong + // when manual mem is enabled with a non-zero timer + if (cfg.manual_memory) manual_memory_restore(); + #endif + + #if defined(USE_CHANNEL_MODES) + // add channel mode functions underneath every other state + push_state(channel_mode_state, 0); + #endif + + push_state(off_state, 1); + + #else // if START_AT_MEMORIZED_LEVEL + + // dual switch: e-switch + power clicky + // power clicky acts as a momentary mode + load_config(); + + #if defined(USE_CHANNEL_MODES) + // add channel mode functions underneath every other state + push_state(channel_mode_state, 0); + #endif + + if (button_is_pressed()) + // hold button to go to moon + push_state(steady_state, 1); + else + // otherwise use memory + push_state(steady_state, memorized_level); + + #endif // ifdef START_AT_MEMORIZED_LEVEL + +} + + +// runs repeatedly whenever light is "on" (not in standby) +void loop() { + + // "current_state" is volatile, so cache it to reduce code size + StatePtr state = current_state; + + #ifdef USE_AUX_RGB_LEDS_WHILE_ON + // display battery charge on RGB button during use + if (state == steady_state) + rgb_led_voltage_readout(actual_level > USE_AUX_RGB_LEDS_WHILE_ON); + #endif + + if (0) {} // placeholder + + #ifdef USE_VERSION_CHECK + else if (state == version_check_state) { + version_check_iter(); + } + #endif + + #ifdef USE_STROBE_STATE + else if ((state == strobe_state) + #ifdef USE_MOMENTARY_MODE + // also handle momentary strobes + || (( + (state == momentary_state) + #ifdef USE_TACTICAL_MODE + || (state == tactical_state) + #endif + ) + && (momentary_mode == 1) && (momentary_active)) + #endif + ) { + strobe_state_iter(); + } + #endif // #ifdef USE_STROBE_STATE + + #ifdef USE_BORING_STROBE_STATE + else if (state == boring_strobe_state) { + boring_strobe_state_iter(); + } + #endif + + #ifdef USE_BATTCHECK + else if (state == battcheck_state) { + battcheck(); + #ifdef USE_SIMPLE_UI + // in simple mode, turn off after one readout + // FIXME: can eat the next button press + // (state changes in loop() act weird) + if (cfg.simple_ui_active) set_state_deferred(off_state, 0); + else nice_delay_ms(1000); + #endif + } + #endif + + #ifdef USE_THERMAL_REGULATION + // TODO: blink out therm_ceil during thermal_config_state? + else if (state == tempcheck_state) { + blink_num(temperature); + nice_delay_ms(1000); + } + #endif + + #ifdef USE_BEACON_MODE + else if (state == beacon_state) { + beacon_mode_iter(); + } + #endif + + #if defined(USE_SOS_MODE) && defined(USE_SOS_MODE_IN_BLINKY_GROUP) + else if (state == sos_state) { + sos_mode_iter(); + } + #endif + + #ifdef USE_SMOOTH_STEPS + else if (cfg.smooth_steps_style && smooth_steps_in_progress) { + smooth_steps_iter(); + } + #endif + + #ifdef USE_IDLE_MODE + else { + // doze until next clock tick + idle_mode(); + } + #endif + +} + + +// instead of handling EV_low_voltage in each mode, +// it's handled globally here to make the code smaller and simpler +void low_voltage() { + + // "current_state" is volatile, so cache it to reduce code size + StatePtr state = current_state; + + // TODO: turn off aux LED(s) when power is really low + + if (0) {} // placeholder + + #ifdef USE_STROBE_STATE + // "step down" from strobe to something low + else if (state == strobe_state) { + set_state(steady_state, RAMP_SIZE/6); + } + #endif + + // in normal mode, step down or turn off + else if (state == steady_state) { + if (actual_level > 1) { + uint8_t lvl = (actual_level >> 1) + (actual_level >> 2); + set_level_and_therm_target(lvl); + } + else { + set_state(off_state, 0); + } + } + // all other modes, just turn off when voltage is low + else { + set_state(off_state, 0); + } + +} + diff --git a/ui/anduril/aux-leds.c b/ui/anduril/aux-leds.c new file mode 100644 index 0000000..af59aa6 --- /dev/null +++ b/ui/anduril/aux-leds.c @@ -0,0 +1,210 @@ +// aux-leds.c: Aux LED functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "aux-leds.h" + + +#if defined(USE_INDICATOR_LED) +void indicator_led_update(uint8_t mode, uint8_t tick) { + //uint8_t volts = voltage; // save a few bytes by caching volatile value + // turn off when battery is too low + #ifdef DUAL_VOLTAGE_FLOOR + if (((voltage < VOLTAGE_LOW) && (voltage > DUAL_VOLTAGE_FLOOR)) + || (voltage < DUAL_VOLTAGE_LOW_LOW)) { + #else + if (voltage < VOLTAGE_LOW) { + #endif + indicator_led(0); + } + //#ifdef USE_INDICATOR_LOW_BAT_WARNING + #ifndef DUAL_VOLTAGE_FLOOR // this isn't set up for dual-voltage lights like the Sofirn SP10 Pro + // fast blink a warning when battery is low but not critical + else if (voltage < VOLTAGE_RED) { + indicator_led(mode & (((tick & 0b0010)>>1) - 3)); + } + #endif + //#endif + // normal steady output, 0/1/2 = off / low / high + else if ((mode & 0b00001111) < 3) { + indicator_led(mode); + } + // beacon-like blinky mode + else { + #ifdef USE_OLD_BLINKING_INDICATOR + + // basic blink, 1/8th duty cycle + if (! (tick & 7)) { + indicator_led(2); + } + else { + indicator_led(0); + } + + #else + + // fancy blink, set off/low/high levels here: + static const uint8_t seq[] = {0, 1, 2, 1, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0}; + indicator_led(seq[tick & 15]); + + #endif // ifdef USE_OLD_BLINKING_INDICATOR + } +} +#endif + +#if defined(USE_AUX_RGB_LEDS) && defined(TICK_DURING_STANDBY) +uint8_t voltage_to_rgb() { + static const uint8_t levels[] = { + // voltage, color + 0, 0, // black + #ifdef DUAL_VOLTAGE_FLOOR + // AA / NiMH voltages + 9, 1, // R + 10, 2, // R+G + 11, 3, // G + 12, 4, // G+B + 13, 5, // B + 14, 6, // R + B + 15, 7, // R+G+B + 20, 0, // black + #endif + // li-ion voltages + 29, 1, // R + 33, 2, // R+G + 35, 3, // G + 37, 4, // G+B + 39, 5, // B + 41, 6, // R + B + 44, 7, // R+G+B // skip; looks too similar to G+B + 255, 7, // R+G+B + }; + uint8_t volts = voltage; + //if (volts < VOLTAGE_LOW) return 0; + + uint8_t i; + for (i = 0; volts >= levels[i]; i += 2) {} + uint8_t color_num = levels[(i - 2) + 1]; + return pgm_read_byte(rgb_led_colors + color_num); +} + +// do fancy stuff with the RGB aux LEDs +// mode: 0bPPPPCCCC where PPPP is the pattern and CCCC is the color +// arg: time slice number +void rgb_led_update(uint8_t mode, uint16_t arg) { + static uint8_t rainbow = 0; // track state of rainbow mode + static uint8_t frame = 0; // track state of animation mode + + // turn off aux LEDs when battery is empty + // (but if voltage==0, that means we just booted and don't know yet) + uint8_t volts = voltage; // save a few bytes by caching volatile value + #ifdef DUAL_VOLTAGE_FLOOR + if ((volts) && (((voltage < VOLTAGE_LOW) && (voltage > DUAL_VOLTAGE_FLOOR)) || (voltage < DUAL_VOLTAGE_LOW_LOW))) { + #else + if ((volts) && (volts < VOLTAGE_LOW)) { + #endif + rgb_led_set(0); + #ifdef USE_BUTTON_LED + button_led_set(0); + #endif + return; + } + + uint8_t pattern = (mode>>4); // off, low, high, blinking, ... more? + uint8_t color = mode & 0x0f; + + // always preview in high mode + if (setting_rgb_mode_now) { pattern = 2; } + + #ifdef USE_POST_OFF_VOLTAGE + // use voltage high mode for a few seconds after initial poweroff + // (but not after changing aux LED settings and other similar actions) + else if ((arg < (cfg.post_off_voltage * SLEEP_TICKS_PER_SECOND)) + && (ticks_since_on < (cfg.post_off_voltage * SLEEP_TICKS_PER_SECOND)) + && (ticks_since_on > 0) // don't blink red on 1st frame + ) { + // use high mode if regular aux level is high or prev level was high + pattern = 1 + ((2 == pattern) | (prev_level >= POST_OFF_VOLTAGE_BRIGHTNESS)); + // voltage mode + color = RGB_LED_NUM_COLORS - 1; + } + #endif + + const uint8_t *colors = rgb_led_colors + 1; + uint8_t actual_color = 0; + if (color < 7) { // normal color + actual_color = pgm_read_byte(colors + color); + } + else if (color == 7) { // disco + rainbow = (rainbow + 1 + pseudo_rand() % 5) % 6; + actual_color = pgm_read_byte(colors + rainbow); + } + else if (color == 8) { // rainbow + uint8_t speed = 0x03; // awake speed + if (go_to_standby) speed = RGB_RAINBOW_SPEED; // asleep speed + if (0 == (arg & speed)) { + rainbow = (rainbow + 1) % 6; + } + actual_color = pgm_read_byte(colors + rainbow); + } + else { // voltage + // show actual voltage while asleep... + if (go_to_standby) { + actual_color = voltage_to_rgb(); + // choose a color based on battery voltage + //if (volts >= 38) actual_color = pgm_read_byte(colors + 4); + //else if (volts >= 33) actual_color = pgm_read_byte(colors + 2); + //else actual_color = pgm_read_byte(colors + 0); + } + // ... but during preview, cycle colors quickly + else { + actual_color = pgm_read_byte(colors + (((arg>>1) % 3) << 1)); + } + } + + // pick a brightness from the animation sequence + if (pattern == 3) { + // uses an odd length to avoid lining up with rainbow loop + static const uint8_t animation[] = {2, 1, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 1}; + frame = (frame + 1) % sizeof(animation); + pattern = animation[frame]; + } + uint8_t result; + #ifdef USE_BUTTON_LED + uint8_t button_led_result; + #endif + switch (pattern) { + case 0: // off + result = 0; + #ifdef USE_BUTTON_LED + button_led_result = 0; + #endif + break; + case 1: // low + result = actual_color; + #ifdef USE_BUTTON_LED + button_led_result = 1; + #endif + break; + default: // high + result = (actual_color << 1); + #ifdef USE_BUTTON_LED + button_led_result = 2; + #endif + break; + } + rgb_led_set(result); + #ifdef USE_BUTTON_LED + button_led_set(button_led_result); + #endif +} + +void rgb_led_voltage_readout(uint8_t bright) { + uint8_t color = voltage_to_rgb(); + if (bright) color = color << 1; + rgb_led_set(color); +} +#endif + diff --git a/ui/anduril/aux-leds.h b/ui/anduril/aux-leds.h new file mode 100644 index 0000000..fa97e6b --- /dev/null +++ b/ui/anduril/aux-leds.h @@ -0,0 +1,65 @@ +// aux-leds.h: Aux LED functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#if defined(USE_INDICATOR_LED) && defined(TICK_DURING_STANDBY) +void indicator_led_update(uint8_t mode, uint8_t tick); +#endif +#if defined(USE_AUX_RGB_LEDS) && defined(TICK_DURING_STANDBY) +uint8_t setting_rgb_mode_now = 0; +void rgb_led_update(uint8_t mode, uint16_t arg); +void rgb_led_voltage_readout(uint8_t bright); +/* + * 0: R + * 1: RG + * 2: G + * 3: GB + * 4: B + * 5: R B + * 6: RGB + * 7: rainbow + * 8: voltage + */ +const PROGMEM uint8_t rgb_led_colors[] = { + 0b00000000, // 0: black + 0b00000001, // 1: red + 0b00000101, // 2: yellow + 0b00000100, // 3: green + 0b00010100, // 4: cyan + 0b00010000, // 5: blue + 0b00010001, // 6: purple + 0b00010101, // 7: white +}; +// intentionally 1 higher than total modes, to make "voltage" easier to reach +// (at Hank's request) +#define RGB_LED_NUM_COLORS 11 +#define RGB_LED_NUM_PATTERNS 4 +#ifndef RGB_LED_OFF_DEFAULT +#define RGB_LED_OFF_DEFAULT 0x19 // low, voltage +//#define RGB_LED_OFF_DEFAULT 0x18 // low, rainbow +#endif +#ifndef RGB_LED_LOCKOUT_DEFAULT +#define RGB_LED_LOCKOUT_DEFAULT 0x39 // blinking, voltage +//#define RGB_LED_LOCKOUT_DEFAULT 0x37 // blinking, disco +#endif +#ifndef RGB_RAINBOW_SPEED +#define RGB_RAINBOW_SPEED 0x0f // change color every 16 frames +#endif +#endif + +//#define USE_OLD_BLINKING_INDICATOR +//#define USE_FANCIER_BLINKING_INDICATOR +#ifdef USE_INDICATOR_LED + // bits 2-3 control lockout mode + // bits 0-1 control "off" mode + // modes are: 0=off, 1=low, 2=high, 3=blinking (if TICK_DURING_STANDBY enabled) + #ifndef INDICATOR_LED_DEFAULT_MODE + #ifdef USE_INDICATOR_LED_WHILE_RAMPING + #define INDICATOR_LED_DEFAULT_MODE ((2<<2) + 1) + #else + #define INDICATOR_LED_DEFAULT_MODE ((3<<2) + 1) + #endif + #endif +#endif + diff --git a/ui/anduril/battcheck-mode-fsm.h b/ui/anduril/battcheck-mode-fsm.h new file mode 100644 index 0000000..5f8e8ec --- /dev/null +++ b/ui/anduril/battcheck-mode-fsm.h @@ -0,0 +1,18 @@ +// battcheck-mode-fsm.h: FSM config for battery check mode in Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#define USE_BATTCHECK + +#ifdef USE_AUX_RGB_LEDS + // show voltage colors for a few seconds after going to standby + #define USE_POST_OFF_VOLTAGE + #ifndef DEFAULT_POST_OFF_VOLTAGE_SECONDS + #define DEFAULT_POST_OFF_VOLTAGE_SECONDS 4 + #endif + #ifndef POST_OFF_VOLTAGE_BRIGHTNESS + // level at which to switch from low to high aux brightness + #define POST_OFF_VOLTAGE_BRIGHTNESS (RAMP_SIZE/10) + #endif +#endif diff --git a/ui/anduril/battcheck-mode.c b/ui/anduril/battcheck-mode.c new file mode 100644 index 0000000..462540e --- /dev/null +++ b/ui/anduril/battcheck-mode.c @@ -0,0 +1,82 @@ +// battcheck-mode.c: Battery check mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "battcheck-mode.h" + +uint8_t battcheck_state(Event event, uint16_t arg) { + ////////// Every action below here is blocked in the simple UI ////////// + #ifdef USE_SIMPLE_UI + if (cfg.simple_ui_active) { + return EVENT_NOT_HANDLED; + } + #endif + + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + + // 2 clicks: next blinky mode + else if (event == EV_2clicks) { + #if defined(USE_THERMAL_REGULATION) + set_state(tempcheck_state, 0); + #elif defined(USE_BEACON_MODE) + set_state(beacon_state, 0); + #elif defined(USE_SOS_MODE) && defined(USE_SOS_MODE_IN_BLINKY_GROUP) + set_state(sos_state, 0); + #endif + return EVENT_HANDLED; + } + + #ifdef DEFAULT_BLINK_CHANNEL + // 3 clicks: next channel mode (specific to number blinky modes) + else if (event == EV_3clicks) { + cfg.blink_channel = (cfg.blink_channel + 1) % NUM_CHANNEL_MODES; + save_config(); + return EVENT_HANDLED; + } + #endif // ifdef DEFAULT_BLINK_CHANNEL + + #ifdef USE_VOLTAGE_CORRECTION + // 7H: voltage config mode + else if (event == EV_click7_hold) { + push_state(voltage_config_state, 0); + return EVENT_HANDLED; + } + #endif + + return EVENT_NOT_HANDLED; +} + +#ifdef USE_VOLTAGE_CORRECTION +// the user can adjust the battery measurements... on a scale of 1 to 13 +// 1 = subtract 0.30V +// 2 = subtract 0.25V +// ... +// 7 = no effect (add 0V) +// 8 = add 0.05V +// ... +// 13 = add 0.30V +void voltage_config_save(uint8_t step, uint8_t value) { + #ifdef USE_POST_OFF_VOLTAGE + if (2 == step) cfg.post_off_voltage = value; + else + #endif + if (value) cfg.voltage_correction = value; +} + +uint8_t voltage_config_state(Event event, uint16_t arg) { + #ifdef USE_POST_OFF_VOLTAGE + #define VOLTAGE_CONFIG_STEPS 2 + #else + #define VOLTAGE_CONFIG_STEPS 1 + #endif + return config_state_base(event, arg, + VOLTAGE_CONFIG_STEPS, + voltage_config_save); +} +#endif // #ifdef USE_VOLTAGE_CORRECTION + diff --git a/ui/anduril/battcheck-mode.h b/ui/anduril/battcheck-mode.h new file mode 100644 index 0000000..b505b68 --- /dev/null +++ b/ui/anduril/battcheck-mode.h @@ -0,0 +1,12 @@ +// battcheck-mode.h: Battery check mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +uint8_t battcheck_state(Event event, uint16_t arg); + +#ifdef USE_VOLTAGE_CORRECTION +void voltage_config_save(uint8_t step, uint8_t value); +uint8_t voltage_config_state(Event event, uint16_t arg); +#endif + diff --git a/ui/anduril/beacon-mode.c b/ui/anduril/beacon-mode.c new file mode 100644 index 0000000..5aff508 --- /dev/null +++ b/ui/anduril/beacon-mode.c @@ -0,0 +1,53 @@ +// beacon-mode.c: Beacon mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "beacon-mode.h" + +inline void beacon_mode_iter() { + // one iteration of main loop() + if (! button_last_state) { + set_level(memorized_level); + nice_delay_ms(100); + set_level(0); + nice_delay_ms(((cfg.beacon_seconds) * 1000) - 100); + } +} + +uint8_t beacon_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // TODO: use sleep ticks to measure time between pulses, + // to save power + + // 2 clicks: next blinky mode + else if (event == EV_2clicks) { + #if defined(USE_SOS_MODE) && defined(USE_SOS_MODE_IN_BLINKY_GROUP) + set_state(sos_state, 0); + #elif defined(USE_BATTCHECK) + set_state(battcheck_state, 0); + #elif defined(USE_THERMAL_REGULATION) + set_state(tempcheck_state, 0); + #endif + return EVENT_HANDLED; + } + // hold: configure beacon timing + else if (event == EV_click1_hold) { + if (0 == (arg % TICKS_PER_SECOND)) { + blink_once(); + } + return EVENT_HANDLED; + } + // release hold: save new timing + else if (event == EV_click1_hold_release) { + cfg.beacon_seconds = 1 + (arg / TICKS_PER_SECOND); + save_config(); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + diff --git a/ui/anduril/beacon-mode.h b/ui/anduril/beacon-mode.h new file mode 100644 index 0000000..df047ad --- /dev/null +++ b/ui/anduril/beacon-mode.h @@ -0,0 +1,9 @@ +// beacon-mode.h: Beacon mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +// beacon mode +uint8_t beacon_state(Event event, uint16_t arg); +inline void beacon_mode_iter(); + diff --git a/ui/anduril/candle-mode.c b/ui/anduril/candle-mode.c new file mode 100644 index 0000000..ab47c34 --- /dev/null +++ b/ui/anduril/candle-mode.c @@ -0,0 +1,136 @@ +// candle-mode.c: Candle mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "candle-mode.h" + +#ifdef USE_SUNSET_TIMER +#include "sunset-timer.h" +#endif + +uint8_t candle_mode_state(Event event, uint16_t arg) { + static int8_t ramp_direction = 1; + #define MAX_CANDLE_LEVEL (MAX_LEVEL-CANDLE_AMPLITUDE-15) + static uint8_t candle_wave1 = 0; + static uint8_t candle_wave2 = 0; + static uint8_t candle_wave3 = 0; + static uint8_t candle_wave2_speed = 0; + // these should add up to 100 + #define CANDLE_WAVE1_MAXDEPTH 30 + #define CANDLE_WAVE2_MAXDEPTH 45 + #define CANDLE_WAVE3_MAXDEPTH 25 + static const uint8_t candle_wave1_depth = CANDLE_WAVE1_MAXDEPTH * CANDLE_AMPLITUDE / 100; + static uint8_t candle_wave2_depth = CANDLE_WAVE2_MAXDEPTH * CANDLE_AMPLITUDE / 100; + static uint8_t candle_wave3_depth = CANDLE_WAVE3_MAXDEPTH * CANDLE_AMPLITUDE / 100; + static uint8_t candle_mode_brightness = 24; + + #ifdef USE_SUNSET_TIMER + // let the candle "burn out" and shut itself off + // if the user told it to + // cache this in case it changes when the timer is called + uint8_t sunset_active = sunset_timer; + // clock tick + sunset_timer_state(event, arg); + // if the timer just expired, shut off + if (sunset_active && (! sunset_timer)) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + #endif // ifdef USE_SUNSET_TIMER + + + if (event == EV_enter_state) { + ramp_direction = 1; + return EVENT_HANDLED; + } + #ifdef USE_SUNSET_TIMER + // 2 clicks: cancel timer + else if (event == EV_2clicks) { + // parent state just rotated through strobe/flasher modes, + // so cancel timer... in case any time was left over from earlier + sunset_timer = 0; + return EVENT_HANDLED; + } + #endif // ifdef USE_SUNSET_TIMER + // hold: change brightness (brighter) + else if (event == EV_click1_hold) { + // ramp away from extremes + if (! arg) { + if (candle_mode_brightness >= MAX_CANDLE_LEVEL) { ramp_direction = -1; } + else if (candle_mode_brightness <= 1) { ramp_direction = 1; } + } + // change brightness, but not too far + candle_mode_brightness += ramp_direction; + if (candle_mode_brightness < 1) candle_mode_brightness = 1; + else if (candle_mode_brightness > MAX_CANDLE_LEVEL) candle_mode_brightness = MAX_CANDLE_LEVEL; + return EVENT_HANDLED; + } + // reverse ramp direction on hold release + else if (event == EV_click1_hold_release) { + ramp_direction = -ramp_direction; + return EVENT_HANDLED; + } + // click, hold: change brightness (dimmer) + else if (event == EV_click2_hold) { + ramp_direction = 1; + if (candle_mode_brightness > 1) + candle_mode_brightness --; + return EVENT_HANDLED; + } + // clock tick: animate candle brightness + else if (event == EV_tick) { + // un-reverse after 1 second + if (arg == AUTO_REVERSE_TIME) ramp_direction = 1; + + // 3-oscillator synth for a relatively organic pattern + uint8_t add; + add = ((triangle_wave(candle_wave1) * candle_wave1_depth) >> 8) + + ((triangle_wave(candle_wave2) * candle_wave2_depth) >> 8) + + ((triangle_wave(candle_wave3) * candle_wave3_depth) >> 8); + uint16_t brightness = candle_mode_brightness + add; + + // self-timer dims the light during the final minute + #ifdef USE_SUNSET_TIMER + if (1 == sunset_timer) { + brightness = brightness + * ((TICKS_PER_MINUTE>>5) - (sunset_ticks>>5)) + / (TICKS_PER_MINUTE>>5); + } + #endif // ifdef USE_SUNSET_TIMER + + set_level(brightness); + + // wave1: slow random LFO + // TODO: make wave slower and more erratic? + if ((arg & 1) == 0) candle_wave1 += pseudo_rand() & 1; + // wave2: medium-speed erratic LFO + candle_wave2 += candle_wave2_speed; + // wave3: erratic fast wave + candle_wave3 += pseudo_rand() % 37; + // S&H on wave2 frequency to make it more erratic + if ((pseudo_rand() & 0b00111111) == 0) + candle_wave2_speed = pseudo_rand() % 13; + // downward sawtooth on wave2 depth to simulate stabilizing + if ((candle_wave2_depth > 0) && ((pseudo_rand() & 0b00111111) == 0)) + candle_wave2_depth --; + // random sawtooth retrigger + if (pseudo_rand() == 0) { + // random amplitude + //candle_wave2_depth = 2 + (pseudo_rand() % ((CANDLE_WAVE2_MAXDEPTH * CANDLE_AMPLITUDE / 100) - 2)); + candle_wave2_depth = pseudo_rand() % (CANDLE_WAVE2_MAXDEPTH * CANDLE_AMPLITUDE / 100); + //candle_wave3_depth = 5; + candle_wave2 = 0; + } + // downward sawtooth on wave3 depth to simulate stabilizing + if ((candle_wave3_depth > 2) && ((pseudo_rand() & 0b00011111) == 0)) + candle_wave3_depth --; + if ((pseudo_rand() & 0b01111111) == 0) + // random amplitude + //candle_wave3_depth = 2 + (pseudo_rand() % ((CANDLE_WAVE3_MAXDEPTH * CANDLE_AMPLITUDE / 100) - 2)); + candle_wave3_depth = pseudo_rand() % (CANDLE_WAVE3_MAXDEPTH * CANDLE_AMPLITUDE / 100); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + diff --git a/ui/anduril/candle-mode.h b/ui/anduril/candle-mode.h new file mode 100644 index 0000000..aab237d --- /dev/null +++ b/ui/anduril/candle-mode.h @@ -0,0 +1,13 @@ +// candle-mode.h: Candle mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#ifndef CANDLE_AMPLITUDE +#define CANDLE_AMPLITUDE 25 +#endif + +uint8_t candle_mode_state(Event event, uint16_t arg); +// moved to fsm-misc.c because it's also used for tint ramping power correction +//uint8_t triangle_wave(uint8_t phase); + diff --git a/ui/anduril/channel-modes.c b/ui/anduril/channel-modes.c new file mode 100644 index 0000000..b2fc8d1 --- /dev/null +++ b/ui/anduril/channel-modes.c @@ -0,0 +1,237 @@ +// channel-modes.c: Multi-channel functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "channel-modes.h" + +uint8_t channel_mode_state(Event event, uint16_t arg) { + #ifdef USE_CHANNEL_MODE_ARGS + static int8_t tint_ramp_direction = 1; + static uint8_t prev_tint = 0; + // don't activate auto-tint modes unless the user hits the edge + // and keeps pressing for a while + static uint8_t past_edge_counter = 0; + // bugfix: click-click-hold from off to strobes would invoke tint ramping + // in addition to changing state... so ignore any tint-ramp events which + // don't look like they were meant to be here + static uint8_t active = 0; + uint8_t tint = cfg.channel_mode_args[channel_mode]; + #endif + + // it's possible that a light may need 3H but not 3C, + // so try to detect if 3C is needed + #if NUM_CHANNEL_MODES > 1 + // 3 clicks: next channel mode + if (event == EV_3clicks) { + uint8_t next = channel_mode; + // go to next channel mode until we find one which is enabled + // (and don't do any infinite loops if the user disabled them all) + uint8_t count = 0; + do { + count ++; + next = (next + 1) % NUM_CHANNEL_MODES; + } while ((! channel_mode_enabled(next)) && count < NUM_CHANNEL_MODES); + //} while ((! channel_modes_enabled[next]) && count < NUM_CHANNEL_MODES); + + // undo change if infinite loop detected (redundant?) + //if (NUM_CHANNEL_MODES == count) next = channel_mode; + + // if mode hasn't changed, abort + if (channel_mode == next) + return EVENT_NOT_HANDLED; + + set_channel_mode(next); + + // remember after battery changes + cfg.channel_mode = channel_mode; + save_config(); + return EVENT_HANDLED; + } else + #endif // if NUM_CHANNEL_MODES > 1 + + #ifdef USE_CUSTOM_CHANNEL_3H_MODES + // defer to mode-specific function if defined + if (channel_3H_modes[channel_mode]) { + StatePtr tint_func = channel_3H_modes[channel_mode]; + uint8_t err = tint_func(event, arg); + if (EVENT_HANDLED == err) return EVENT_HANDLED; + // else let the default handler run + } + #endif + #ifdef USE_CHANNEL_MODE_ARGS + #ifndef DONT_USE_DEFAULT_CHANNEL_ARG_MODE + // click, click, hold: change the current channel's arg (like tint) + if (event == EV_click3_hold) { + ///// adjust value from 0 to 255 + // reset at beginning of movement + if (! arg) { + active = 1; // first frame means this is for us + past_edge_counter = 0; // doesn't start until user hits the edge + } + // ignore event if we weren't the ones who handled the first frame + if (! active) return EVENT_NOT_HANDLED; + + #ifdef USE_STEPPED_TINT_RAMPING + if ((tint_ramp_direction > 0 && tint < 255) || + (tint_ramp_direction < 0 && tint > 0)) { + // ramp slower in stepped mode + if (cfg.tint_ramp_style && (arg % HOLD_TIMEOUT != 0)) + return EVENT_HANDLED; + + const uint8_t step_size = (cfg.tint_ramp_style < 2) + ? 1 : 254 / (cfg.tint_ramp_style-1); + tint = nearest_tint_value( + tint + ((int16_t)step_size * tint_ramp_direction) + ); + } + #else // smooth tint ramping only + if ((tint_ramp_direction > 0) && (tint < 255)) { tint ++; } + else + if ((tint_ramp_direction < 0) && (tint > 0)) { tint --; } + #endif // ifdef USE_STEPPED_TINT_RAMPING + + // if tint change stalled, let user know we hit the edge + else if (prev_tint == tint) { + if (past_edge_counter == 0) blip(); + past_edge_counter = 1; + } + prev_tint = tint; + cfg.channel_mode_args[channel_mode] = tint; + set_level(actual_level); + return EVENT_HANDLED; + } + + // click, click, hold, release: reverse direction for next ramp + else if (event == EV_click3_hold_release) { + active = 0; // ignore next hold if it wasn't meant for us + // reverse + tint_ramp_direction = -tint_ramp_direction; + if (0 == tint) tint_ramp_direction = 1; + else if (255 == tint) tint_ramp_direction = -1; + // remember tint after battery change + cfg.channel_mode_args[channel_mode] = tint; + save_config(); + // bug?: for some reason, brightness can seemingly change + // from 1/150 to 2/150 without this next line... not sure why + set_level(actual_level); + return EVENT_HANDLED; + } + #endif // ifndef DONT_USE_DEFAULT_CHANNEL_ARG_MODE + #endif // ifdef USE_CHANNEL_MODE_ARGS + + #if defined(USE_SIMPLE_UI) + // remaining mappings aren't "simple", so stop here + if (cfg.simple_ui_active) { + return EVENT_NOT_HANDLED; + } + #endif + + #if NUM_CHANNEL_MODES > 1 + // channel toggle menu on ... 9H? + else if (event == EV_click9_hold) { + push_state(channel_mode_config_state, 0); + return EVENT_HANDLED; + } + #endif + + return EVENT_NOT_HANDLED; +} + + +#if NUM_CHANNEL_MODES > 1 +void channel_mode_config_save(uint8_t step, uint8_t value) { + // 1 menu item per channel mode, to enable or disable that mode + step --; // step is 1-based, channel modes are 0-based + if (value) channel_mode_enable(step); + else channel_mode_disable(step); +} + +uint8_t channel_mode_config_state(Event event, uint16_t arg) { + uint8_t ret; + // make config steps match channel modes + config_color_per_step = true; + // 1 menu item per channel mode, to enable or disable that mode + ret = config_state_base( + event, arg, + NUM_CHANNEL_MODES, + channel_mode_config_save + ); + // no other menu needs this + config_color_per_step = false; + return ret; +} +#endif + + +#if defined(USE_CHANNEL_MODE_ARGS) && defined(USE_STEPPED_TINT_RAMPING) +uint8_t nearest_tint_value(const int16_t target) { + // const symbols for more readable code, will be removed by the compiler + const uint8_t tint_min = 0; + const uint8_t tint_max = 255; + const uint8_t tint_range = tint_max - tint_min; + + // only equal mix of both channels + if (1 == cfg.tint_ramp_style) return (tint_min + tint_max) >> 1; + + if (target < tint_min) return tint_min; + if (target > tint_max) return tint_max; + if (0 == cfg.tint_ramp_style) return target; // smooth ramping + + const uint8_t step_size = tint_range / (cfg.tint_ramp_style-1); + + uint8_t tint_result = tint_min; + for (uint8_t i=0; i>1)) return tint_result; + } + return tint_result; +} +#endif + +#ifdef USE_CIRCULAR_TINT_3H +uint8_t circular_tint_3h(Event event, uint16_t arg) { + static int8_t tint_ramp_direction = 1; + // bugfix: click-click-hold from off to strobes would invoke tint ramping + // in addition to changing state... so ignore any tint-ramp events which + // don't look like they were meant to be here + static uint8_t active = 0; + uint8_t tint = cfg.channel_mode_args[channel_mode]; + + // click, click, hold: change the current channel's arg (like tint) + if (event == EV_click3_hold) { + ///// adjust value from 0 to 255 in a circle + // reset at beginning of movement + if (! arg) { + active = 1; // first frame means this is for us + } + // ignore event if we weren't the ones who handled the first frame + if (! active) return EVENT_NOT_HANDLED; + + // smooth tint ramping only + tint += tint_ramp_direction; + + cfg.channel_mode_args[channel_mode] = tint; + set_level(actual_level); + return EVENT_HANDLED; + } + + // click, click, hold, release: reverse direction for next ramp + else if (event == EV_click3_hold_release) { + active = 0; // ignore next hold if it wasn't meant for us + // reverse + tint_ramp_direction = -tint_ramp_direction; + // remember tint after battery change + save_config(); + // bug?: for some reason, brightness can seemingly change + // from 1/150 to 2/150 without this next line... not sure why + set_level(actual_level); + return EVENT_HANDLED; + } + + return EVENT_NOT_HANDLED; +} +#endif diff --git a/ui/anduril/channel-modes.h b/ui/anduril/channel-modes.h new file mode 100644 index 0000000..b51721d --- /dev/null +++ b/ui/anduril/channel-modes.h @@ -0,0 +1,26 @@ +// channel-modes.h: Multi-channel functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// not actually a mode, more of a fallback under other modes +uint8_t channel_mode_state(Event event, uint16_t arg); + +#if NUM_CHANNEL_MODES > 1 +uint8_t channel_mode_config_state(Event event, uint16_t arg); +#endif + +#if defined(USE_CHANNEL_MODE_ARGS) && defined(USE_STEPPED_TINT_RAMPING) +// calculate the nearest tint value which would be valid at the moment +uint8_t nearest_tint_value(const int16_t target); +#endif + +#ifdef USE_CUSTOM_CHANNEL_3H_MODES +StatePtr channel_3H_modes[NUM_CHANNEL_MODES]; +#endif + +#ifdef USE_CIRCULAR_TINT_3H +uint8_t circular_tint_3h(Event event, uint16_t arg); +#endif + diff --git a/ui/anduril/config-default.h b/ui/anduril/config-default.h new file mode 100644 index 0000000..899bc4a --- /dev/null +++ b/ui/anduril/config-default.h @@ -0,0 +1,207 @@ +// config-default.h: Default configuration for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +/* + * This file specifies the default settings for Anduril. + * + * These settings can be overridden per build target, in cfg-*.h files... + * ... but most are not. So changing one here will make it change in + * almost every build target. + * + * Some configurable settings are also in other *.h files. + */ + +/********* User-configurable options *********/ +// low voltage protection (also required for battery check mode) +#define USE_LVP + +// overheat protection +#define USE_THERMAL_REGULATION +#if (ATTINY==85) || (ATTINY==1634) +// sloppy temperature sensor needs bigger error margin +#define DEFAULT_THERM_CEIL 45 // try not to get hotter than this (in C) +#else +// more accurate temperature sensor can regulate higher safely +#define DEFAULT_THERM_CEIL 50 // try not to get hotter than this (in C) +#endif +// Comment out to disable automatic calibration on factory reset +// - If so, be sure to set THERM_CAL_OFFSET to the correct calibration offset +// - Calibration can still be overridden in temperature check mode +// Or uncomment to use the default auto-calibrate on factory reset +// +// To determine THERM_CAL_OFFSET, comment out USE_THERM_AUTOCALIBRATE to +// disable auto-calibration, compile and flash, let flashlight rest at a known +// temperature, then enter temp check mode (do NOT enter calibration mode). +// +// THERM_CAL_OFFSET = known_temperature - temp_check_blinks + THERM_CAL_OFFSET +// +// (include THERM_CAL_OFFSET in sum as it might already be a non-zero number) +#define USE_THERM_AUTOCALIBRATE + +// Include a simplified UI for non-enthusiasts? +#define USE_SIMPLE_UI + + +///// Ramp mode options ///// + +// button timing for turning light on/off: +// B_PRESS_T: activate as soon as button is pressed +// B_RELEASE_T: activate when user lets go of button +// B_TIMEOUT_T: activate when we're sure the user won't double-click +// defaults are release on, timeout off +#define B_TIMING_ON B_RELEASE_T +#define B_TIMING_OFF B_TIMEOUT_T + +// default ramp style: 0 = smooth, 1 = stepped +#define RAMP_STYLE 0 + +// smooth ramp speed: 1, 2, 3, 4, ... for 1X speed, 1/2, 1/3rd, 1/4th, ... +#define USE_RAMP_SPEED_CONFIG + +// after ramping, how long until the direction resets to "up"? +#define AUTO_REVERSE_TIME (TICKS_PER_SECOND * 2 / 3) + +// add runtime option for whether hold-from-off should ramp or stay at moon +#define USE_RAMP_AFTER_MOON_CONFIG + +// short blip when crossing from "click" to "hold" from off +// (helps the user hit moon mode exactly, instead of holding too long +// or too short) +#define MOON_TIMING_HINT // only applies if B_TIMING_ON == B_PRESS_T +// short blips while ramping +#define BLINK_AT_RAMP_MIDDLE +//#define BLINK_AT_RAMP_FLOOR +#define BLINK_AT_RAMP_CEIL +//#define BLINK_AT_STEPS // whenever a discrete ramp mode is passed in smooth mode + +// Uncomment for Anduril1 "Ramp 2C" behavior: +// - Ramp 2C goes to turbo (advanced UI) or ceiling (simple UI), like in Anduril1 +// Or comment out to use Anduril2 behavior instead: +// - Ramp 2C goes to ceiling, unless already at ceiling or in simple UI. +// (Advanced UI ceiling 2C goes to turbo) +//#define USE_2C_MAX_TURBO +// Or uncomment to let the user decide which style they want: +#define USE_2C_STYLE_CONFIG +// 0 = no turbo +// 1 = A1 style: Off 2C = ceil, On 2C = turbo +// 2 = A2 style: Off 2C = ceil, On 2C = ceil, Ramped ceil 2C = turbo +// All styles allow momentary turbo in advanced UI +//#define DEFAULT_2C_STYLE 2 // default to Anduril 2 style +//#define DEFAULT_2C_STYLE_SIMPLE 0 // no turbo at all + +// make the ramps configurable by the user +#define USE_RAMP_CONFIG + +// adds a runtime option to switch between automatic memory (default) +// and manual memory (only available if compiled in) +// (manual memory makes 1-click-from-off start at the same level each time) +// (the level can be set explicitly with 10 clicks from on, +// or the user can go back to automatic with 10H) +#define USE_MANUAL_MEMORY +// if enabled, user can use "hybrid memory" +// The light will use automatic or manual memory level, depending on how long +// the light was off. Short off = automatic, long off = manual. +// This also remaps 10C/10H: +// - 10C toggles manual mem on/off at current level. +// - 10H configures the timer value. +#define USE_MANUAL_MEMORY_TIMER + +// enable sunset timer (ramp-down and automatic shutoff) +// timer is available in regular ramp mode and candle mode +#define USE_SUNSET_TIMER + + +///// What to do when power is connected ///// +// factory reset function erases user's runtime configuration in eeprom +#define USE_FACTORY_RESET +//#define USE_SOFT_FACTORY_RESET // only needed on models which can't use hold-button-at-boot + +// dual-switch support (second switch is a tail clicky) +// (currently incompatible with factory reset) +//#define START_AT_MEMORIZED_LEVEL + + +///// extra modes (enable / disable / configure each mode) ///// + +// include a function to blink out the firmware version +#define USE_VERSION_CHECK + +// enable the battery check mode (shows remaining charge) (requires USE_LVP) +#define USE_BATTCHECK_MODE +// battery readout style (pick one) +// TODO: allow VpT and 4-bar simultaneously, +// so one can be in "simple mode" and the other in "advanced mode" +#define BATTCHECK_VpT +//#define BATTCHECK_8bars // FIXME: breaks build +//#define BATTCHECK_4bars // FIXME: breaks build +// allow the user to calibrate the voltage readings? +// (adjust in 0.05V increments from -0.30V to +0.30V) +// (1 = -0.30V, 2 = -0.25V, ... 7 = 0V, ... 13 = +0.30V) +#define USE_VOLTAGE_CORRECTION + +// enable beacon mode +#define USE_BEACON_MODE + +// enable/disable various strobe modes +#define USE_BIKE_FLASHER_MODE +#define USE_PARTY_STROBE_MODE +#define USE_TACTICAL_STROBE_MODE +#define USE_LIGHTNING_MODE +#define USE_CANDLE_MODE + +// boring strobes nobody really likes, but sometimes flashlight companies want +// (these replace the fun strobe group, +// so don't enable them at the same time as any of the above strobes) +//#define USE_POLICE_STROBE_MODE +#define USE_SOS_MODE +//#define USE_SOS_MODE_IN_FF_GROUP // put SOS in the "boring strobes" mode +#define USE_SOS_MODE_IN_BLINKY_GROUP // put SOS in the blinkies mode group + +// enable a mode for locking the light for safe carry +#define USE_LOCKOUT_MODE +// should lockout mode function as a momentary moon mode? +#define USE_MOON_DURING_LOCKOUT_MODE +// add an optional setting to lock the light after being off for a while +#define USE_AUTOLOCK + +// enable momentary mode +#define USE_MOMENTARY_MODE + +// enable tactical mode +#define USE_TACTICAL_MODE + + +// enable a shortcut for +10 in number entry mode +// (click for +1, hold for +10) +#define USE_NUMBER_ENTRY_PLUS10 + +// cut clock speed at very low modes for better efficiency +// (defined here so config files can override it) +#define USE_DYNAMIC_UNDERCLOCKING + +// if the aux LEDs oscillate between "full battery" and "empty battery" +// while in "voltage" mode, enable this to reduce the amplitude of +// those oscillations +#if (ATTINY==1616) || (ATTINY==1634) +#define USE_LOWPASS_WHILE_ASLEEP +#endif + +// if there's tint ramping, allow user to set it smooth or stepped +#define USE_STEPPED_TINT_RAMPING +#define DEFAULT_TINT_RAMP_STYLE 0 // smooth + +// Use "smooth steps" to soften on/off and step changes +// on MCUs with enough room for extra stuff like this +#if (ATTINY==1616) || (ATTINY==1634) +#define USE_SMOOTH_STEPS +#endif +// 0 = none, 1 = smooth, 2+ = undefined +#define DEFAULT_SMOOTH_STEPS_STYLE 1 + +// by default, allow user to set the channel for each strobe-group mode +// (but allow disabling this feature per build) +#define USE_CHANNEL_PER_STROBE + diff --git a/ui/anduril/config-mode.c b/ui/anduril/config-mode.c new file mode 100644 index 0000000..71b0d69 --- /dev/null +++ b/ui/anduril/config-mode.c @@ -0,0 +1,196 @@ +// config-mode.c: Config mode base functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "config-mode.h" + +// general helper function for config modes +uint8_t number_entry_state(Event event, uint16_t arg); +// return value from number_entry_state() +volatile uint8_t number_entry_value; + + +#if defined(USE_CONFIG_COLORS) && (NUM_CHANNEL_MODES > 1) +// TODO: promote this to fsm-channels.c ? +void set_chan_if(bool cond, uint8_t chan) { + if ((cond) && (chan != channel_mode)) + set_channel_mode(chan); +} +#endif + +// allow the user to set a new value for a config option +// can be called two ways: +// - with a "click" action: Configures first menu item only. +// - with a "hold" action: Sets user select a menu item and then +// choose a new value for it. User should hold button until light +// blinks N times, to choose menu item N. Then let go, and light +// should give a buzzing prompt to enter a number. Click N times +// at the prompt to set the new value to N. +// after completing this process, config state calls the savefunc callback +// and then returns to caller/parent state +uint8_t config_state_base( + Event event, + uint16_t arg, + uint8_t num_config_steps, + void (*savefunc)(uint8_t step, uint8_t value)) { + + static uint8_t config_step; + #ifdef USE_CONFIG_COLORS + static uint8_t orig_channel; + #endif + if (event == EV_enter_state) { + #if defined(USE_CONFIG_COLORS) && (NUM_CHANNEL_MODES > 1) + orig_channel = channel_mode; + #endif + config_step = 0; + set_level(0); + // if button isn't held, configure first menu item + if (! button_last_state) { + config_step ++; + push_state(number_entry_state, 0); + } + } + + // if initial "hold" event still active + // blink while holding to indicate option number + #define B_CLICK_FLAGS (B_CLICK|B_HOLD|B_PRESS|B_RELEASE|B_TIMEOUT) + #define B_ANY_HOLD (B_CLICK|B_HOLD|B_PRESS) + #define B_ANY_HOLD_RELEASE (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT) + else if ((event & B_CLICK_FLAGS) == B_ANY_HOLD) { + if (config_step <= num_config_steps) { + #if defined(USE_CONFIG_COLORS) && (NUM_CHANNEL_MODES > 1) + uint8_t chan = config_step - 1; + if (chan < NUM_CHANNEL_MODES) + set_chan_if(config_color_per_step, chan); + #endif + if ((TICKS_PER_SECOND/10) == (arg % (TICKS_PER_SECOND*3/2))) { + config_step ++; + // blink when config step advances + if (config_step <= num_config_steps) { + #ifdef CONFIG_BLINK_CHANNEL + set_chan_if(!config_color_per_step, CONFIG_BLINK_CHANNEL); + #endif + set_level(RAMP_SIZE * 3 / 8); + } + } + else { + // stay on at a low level to indicate menu is active + #ifdef CONFIG_WAITING_CHANNEL + set_chan_if(!config_color_per_step, CONFIG_WAITING_CHANNEL); + #endif + set_level(RAMP_SIZE * 1 / 8); + } + } else { + // turn off when end of menu is reached + set_level(0); + } + } + + // button release: activate number entry for one menu item + else if ((event & B_CLICK_FLAGS) == B_ANY_HOLD_RELEASE) { + // ask the user for a number, if they selected a menu item + if (config_step && config_step <= num_config_steps) { + #if defined(USE_CONFIG_COLORS) && (NUM_CHANNEL_MODES > 1) + // put the colors back how they were + set_channel_mode(orig_channel); + #endif + push_state(number_entry_state, 0); + } + // exit after falling out of end of menu + else { + pop_state(); + } + } + + // an option was set (return from number_entry_state) + else if (event == EV_reenter_state) { + // process value with parent's callback + savefunc(config_step, number_entry_value); + // make changes persist in eeprom + save_config(); + pop_state(); + } + + #if defined(USE_CONFIG_COLORS) && (NUM_CHANNEL_MODES > 1) + else if (event == EV_leave_state) { + // put the colors back how they were + set_channel_mode(orig_channel); + } + #endif + + // eat all other events; don't pass any through to parent + return EVENT_HANDLED; +} + +uint8_t number_entry_state(Event event, uint16_t arg) { + static uint8_t entry_step; + + if (event == EV_enter_state) { + number_entry_value = 0; + entry_step = 0; + set_level(0); // initial pause should be dark + } + + // advance through the process: + // 0: wait a moment + // 1: "buzz" while counting clicks + // 2: save and exit + else if (event == EV_tick) { + // wait a moment + if (entry_step == 0) { + if (arg > TICKS_PER_SECOND/2) { + entry_step ++; + empty_event_sequence(); // reset tick counter to 0 + } + } + // buzz while waiting for a number to be entered + else if (entry_step == 1) { + // time out and exit after 3 seconds + if (arg > TICKS_PER_SECOND*3) { + entry_step ++; + set_level(0); + } + // buzz for N seconds after last event + // (flicker every other frame, + // except first frame (so we can see flashes after each click)) + else if (arg) { + #ifdef CONFIG_WAITING_CHANNEL + set_chan_if(1, CONFIG_WAITING_CHANNEL); + #endif + set_level( (RAMP_SIZE/8) + + ((arg&2)<<2) ); + } + } + // all done, save result and return to parent state + else { + pop_state(); + } + return EVENT_HANDLED; + } + + // count clicks: click = +1, hold = +10 + else if ((event == EV_click1_release) + #ifdef USE_NUMBER_ENTRY_PLUS10 + || (event == EV_click1_hold_release) + #endif + ) { + entry_step = 1; // in case user clicked during initial delay + #ifdef USE_NUMBER_ENTRY_PLUS10 + if (event == EV_click1_hold_release) number_entry_value += 10; + else + #endif + number_entry_value ++; // update the result + empty_event_sequence(); // reset FSM's click count + #ifdef CONFIG_BLINK_CHANNEL + set_channel_mode(CONFIG_BLINK_CHANNEL); + #endif + set_level(RAMP_SIZE/2); // flash briefly + return EVENT_HANDLED; + } + + // eat all other events; don't pass any through to parent + return EVENT_HANDLED; +} + diff --git a/ui/anduril/config-mode.h b/ui/anduril/config-mode.h new file mode 100644 index 0000000..d4a7652 --- /dev/null +++ b/ui/anduril/config-mode.h @@ -0,0 +1,24 @@ +// config-mode.h: Config mode base functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// menus can use 2 colors +#if defined (CONFIG_WAITING_CHANNEL) || defined(CONFIG_BLINK_CHANNEL) +#define USE_CONFIG_COLORS +#endif + +#if NUM_CHANNEL_MODES > 1 +// when true, changes the channel mode for each config step +bool config_color_per_step = false; +#endif + +// config menu +uint8_t config_state_base( + Event event, + uint16_t arg, + uint8_t num_config_steps, + void (*savefunc)(uint8_t step, uint8_t value) + ); + diff --git a/ui/anduril/factory-reset-fsm.h b/ui/anduril/factory-reset-fsm.h new file mode 100644 index 0000000..3cb0875 --- /dev/null +++ b/ui/anduril/factory-reset-fsm.h @@ -0,0 +1,10 @@ +// factory-reset-fsm.h: FSM config options to enable factory reset in Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#ifdef USE_SOFT_FACTORY_RESET +#define USE_REBOOT +#endif + diff --git a/ui/anduril/factory-reset.c b/ui/anduril/factory-reset.c new file mode 100644 index 0000000..f9fb472 --- /dev/null +++ b/ui/anduril/factory-reset.c @@ -0,0 +1,73 @@ +// factory-reset.c: Factory reset functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "factory-reset.h" + +// allows setting channel mode per animation stage, +// so it can ramp up in red then explode in white (as one example) + +void factory_reset() { + // display a warning for a few seconds before doing the actual reset, + // so the user has time to abort if they want + #define SPLODEY_TIME 2500 + #define SPLODEY_STEPS 64 + #define SPLODEY_TIME_PER_STEP (SPLODEY_TIME/SPLODEY_STEPS) + uint8_t bright; + uint8_t reset = 1; + // wind up to an explosion + #ifdef FACTORY_RESET_WARN_CHANNEL + set_channel_mode(FACTORY_RESET_WARN_CHANNEL); + #endif + for (bright=0; bright>1); + nice_delay_ms(SPLODEY_TIME_PER_STEP/2); + if (! button_is_pressed()) { + reset = 0; + break; + } + } + // explode, if button pressed long enough + if (reset) { + #if defined(FACTORY_RESET_WARN_CHANNEL) && defined(DEFAULT_CHANNEL_MODE) + // return to default channel before saving + set_channel_mode(DEFAULT_CHANNEL_MODE); + #endif + + // auto-calibrate temperature + // AVR 1-Series has factory calibrated thermal sensor, always remove the offset on reset + #if defined(USE_THERMAL_REGULATION) && defined(AVRXMEGA3) + // this will cancel out the offset + thermal_config_save(1, temperature - cfg.therm_cal_offset); + #elif defined(USE_THERMAL_REGULATION) && defined(USE_THERM_AUTOCALIBRATE) + // assume current temperature is 21 C + thermal_config_save(1, 21); + #endif + + // save all settings to eeprom + // (assuming they're all at default because we haven't loaded them yet) + save_config(); + + // explosion animation + #ifdef FACTORY_RESET_SUCCESS_CHANNEL + set_channel_mode(FACTORY_RESET_SUCCESS_CHANNEL); + #endif + bright = MAX_LEVEL; + for (; bright > 0; bright--) { + set_level(bright); + nice_delay_ms(SPLODEY_TIME_PER_STEP/8); + } + } + // explosion cancelled, fade away + else { + for (; bright > 0; bright--) { + set_level(bright); + nice_delay_ms(SPLODEY_TIME_PER_STEP/3); + } + } +} + diff --git a/ui/anduril/factory-reset.h b/ui/anduril/factory-reset.h new file mode 100644 index 0000000..63c25cd --- /dev/null +++ b/ui/anduril/factory-reset.h @@ -0,0 +1,8 @@ +// factory-reset.h: Factory reset functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +void factory_reset(); + diff --git a/ui/anduril/ff-strobe-modes.c b/ui/anduril/ff-strobe-modes.c new file mode 100644 index 0000000..b7a7303 --- /dev/null +++ b/ui/anduril/ff-strobe-modes.c @@ -0,0 +1,62 @@ +// ff-strobe-modes.c: Fireflies Flashlights strobe modes for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "ff-strobe-modes.h" + +uint8_t boring_strobe_state(Event event, uint16_t arg) { + // police strobe and SOS, meh + // 'st' reduces ROM size slightly + uint8_t st = boring_strobe_type; + + if (event == EV_enter_state) { + return EVENT_HANDLED; + } + // 1 click: off + else if (event == EV_1click) { + // reset to police strobe for next time + boring_strobe_type = 0; + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: rotate through strobe/flasher modes + else if (event == EV_2clicks) { + boring_strobe_type = (st + 1) % NUM_BORING_STROBES; + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + +inline void boring_strobe_state_iter() { + switch(boring_strobe_type) { + #ifdef USE_POLICE_STROBE_MODE + case 0: // police strobe + police_strobe_iter(); + break; + #endif + + #ifdef USE_SOS_MODE_IN_FF_GROUP + default: // SOS + sos_mode_iter(); + break; + #endif + } +} + +#ifdef USE_POLICE_STROBE_MODE +inline void police_strobe_iter() { + // one iteration of main loop() + // flash at 16 Hz then 8 Hz, 8 times each + for (uint8_t del=41; del<100; del+=41) { + for (uint8_t f=0; f<8; f++) { + set_level(STROBE_BRIGHTNESS); + nice_delay_ms(del >> 1); + set_level(0); + nice_delay_ms(del); + } + } +} +#endif + diff --git a/ui/anduril/ff-strobe-modes.h b/ui/anduril/ff-strobe-modes.h new file mode 100644 index 0000000..d7adfec --- /dev/null +++ b/ui/anduril/ff-strobe-modes.h @@ -0,0 +1,15 @@ +// ff-strobe-modes.h: Fireflies Flashlights strobe modes for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +uint8_t boring_strobe_state(Event event, uint16_t arg); +inline void boring_strobe_state_iter(); +uint8_t boring_strobe_type = 0; +void sos_blink(uint8_t num, uint8_t dah); +#ifdef USE_POLICE_STROBE_MODE +inline void police_strobe_iter(); +#endif +#define NUM_BORING_STROBES 2 + diff --git a/ui/anduril/load-save-config-fsm.h b/ui/anduril/load-save-config-fsm.h new file mode 100644 index 0000000..d189d3a --- /dev/null +++ b/ui/anduril/load-save-config-fsm.h @@ -0,0 +1,139 @@ +// load-save-config-fsm.h: FSM config for eeprom configuration in Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#define USE_EEPROM +// load into a custom RAM location instead of FSM's default byte array +#define EEPROM_OVERRIDE + +#ifdef USE_SIMPLE_UI +#define NUM_RAMPS 3 +#else +#define NUM_RAMPS 2 +#endif + +// let FSM know this config struct exists +#define USE_CFG + +typedef struct Config { + + ///// ramp vars + uint8_t ramp_style; + #ifdef USE_2C_STYLE_CONFIG + uint8_t ramp_2c_style; + #endif + #ifdef USE_RAMP_CONFIG + uint8_t ramp_floors[NUM_RAMPS]; + uint8_t ramp_ceils [NUM_RAMPS]; + uint8_t ramp_stepss[NUM_RAMPS]; + #endif + #ifdef USE_SIMPLE_UI + uint8_t simple_ui_active; + #ifdef USE_2C_STYLE_CONFIG + uint8_t ramp_2c_style_simple; + #endif + #endif + #ifdef USE_RAMP_AFTER_MOON_CONFIG + uint8_t dont_ramp_after_moon; + #endif + #ifdef USE_MANUAL_MEMORY + uint8_t manual_memory; + #ifdef USE_MANUAL_MEMORY_TIMER + uint8_t manual_memory_timer; + #endif + #endif + + ///// channel modes / color modes + #if NUM_CHANNEL_MODES > 1 + uint8_t channel_mode; + uint16_t channel_modes_enabled; + #ifdef USE_MANUAL_MEMORY + uint8_t manual_memory_channel_mode; + #endif + #ifdef DEFAULT_BLINK_CHANNEL + uint8_t blink_channel; + #endif + #endif + #ifdef USE_CHANNEL_MODE_ARGS + // this is an array, needs a few bytes + uint8_t channel_mode_args[NUM_CHANNEL_MODES]; + #ifdef USE_MANUAL_MEMORY + uint8_t manual_memory_channel_args[NUM_CHANNEL_MODES]; + #endif + #ifdef USE_STEPPED_TINT_RAMPING + uint8_t tint_ramp_style; + #endif + #endif + + ///// Smooth animation between steps, and for on/off + #ifdef USE_SMOOTH_STEPS + uint8_t smooth_steps_style; + #endif + + ///// strobe / blinky mode settings + #ifdef USE_STROBE_STATE + uint8_t strobe_type; + #if (NUM_CHANNEL_MODES > 1) && defined(USE_CHANNEL_PER_STROBE) + uint8_t strobe_channels[NUM_STROBES]; + #endif + #endif + #if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) + uint8_t strobe_delays[2]; + #endif + #ifdef USE_BIKE_FLASHER_MODE + uint8_t bike_flasher_brightness; + #endif + #ifdef USE_BEACON_MODE + uint8_t beacon_seconds; + #endif + + ///// voltage and temperature + #ifdef USE_VOLTAGE_CORRECTION + uint8_t voltage_correction; + #endif + #ifdef USE_THERMAL_REGULATION + uint8_t therm_ceil; + int8_t therm_cal_offset; + #endif + + ///// aux LEDs + #ifdef USE_INDICATOR_LED + uint8_t indicator_led_mode; + #endif + #ifdef USE_AUX_RGB_LEDS + uint8_t rgb_led_off_mode; + uint8_t rgb_led_lockout_mode; + #ifdef USE_POST_OFF_VOLTAGE + uint8_t post_off_voltage; + #endif + #endif + + ///// misc other mode settings + #ifdef USE_AUTOLOCK + uint8_t autolock_time; + #endif + #ifdef USE_TACTICAL_MODE + uint8_t tactical_levels[3]; + #endif + + ///// hardware config / globals menu + #ifdef USE_JUMP_START + uint8_t jump_start_level; + #endif + +} Config; + +// auto-detect how many eeprom bytes +#define EEPROM_BYTES sizeof(Config) + +// declare this so FSM can see it, +// but define its values in a file which loads later +Config cfg; + +#ifdef START_AT_MEMORIZED_LEVEL +#define USE_EEPROM_WL +#define EEPROM_WL_BYTES 1 +#endif + diff --git a/ui/anduril/load-save-config.c b/ui/anduril/load-save-config.c new file mode 100644 index 0000000..aa772e1 --- /dev/null +++ b/ui/anduril/load-save-config.c @@ -0,0 +1,33 @@ +// load-save-config.c: Load/save/eeprom functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "load-save-config-fsm.h" +#include "load-save-config.h" + +void load_config() { + eeprom = (uint8_t *)&cfg; + + if (! load_eeprom()) return; + + #ifdef START_AT_MEMORIZED_LEVEL + if (load_eeprom_wl()) { + memorized_level = eeprom_wl[0]; + } + #endif +} + +void save_config() { + eeprom = (uint8_t *)&cfg; + save_eeprom(); +} + +#ifdef START_AT_MEMORIZED_LEVEL +void save_config_wl() { + eeprom_wl[0] = memorized_level; + save_eeprom_wl(); +} +#endif + diff --git a/ui/anduril/load-save-config.h b/ui/anduril/load-save-config.h new file mode 100644 index 0000000..514fcbb --- /dev/null +++ b/ui/anduril/load-save-config.h @@ -0,0 +1,173 @@ +// load-save-config.h: Load/save/eeprom functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// remember stuff even after battery was changed +void load_config(); +void save_config(); +#ifdef START_AT_MEMORIZED_LEVEL +void save_config_wl(); +#endif + +// a struct to hold config values +Config cfg = { + + ///// ramp vars + + // smooth vs discrete ramping + .ramp_style = RAMP_STYLE, // 0 = smooth, 1 = discrete + #ifdef USE_2C_STYLE_CONFIG + // 1 = A1 style, 2 = A2 style + .ramp_2c_style = DEFAULT_2C_STYLE, + #endif + + // settings for each ramp (smooth, stepped, simple UI) + #ifdef USE_RAMP_CONFIG + .ramp_floors = { + RAMP_SMOOTH_FLOOR, + RAMP_DISCRETE_FLOOR, + #ifdef USE_SIMPLE_UI + SIMPLE_UI_FLOOR, + #endif + }, + .ramp_ceils = { + RAMP_SMOOTH_CEIL, + RAMP_DISCRETE_CEIL, + #ifdef USE_SIMPLE_UI + SIMPLE_UI_CEIL, + #endif + }, + .ramp_stepss = { + DEFAULT_RAMP_SPEED, + RAMP_DISCRETE_STEPS, + #ifdef USE_SIMPLE_UI + SIMPLE_UI_STEPS, + #endif + }, + #endif + + #ifdef USE_SIMPLE_UI + // whether to enable the simplified interface or not + .simple_ui_active = SIMPLE_UI_ACTIVE, + #ifdef USE_2C_STYLE_CONFIG + // 0 = no turbo, 1 = A1 style, 2 = A2 style + .ramp_2c_style_simple = DEFAULT_2C_STYLE_SIMPLE, + #endif + #endif + + #ifdef USE_RAMP_AFTER_MOON_CONFIG + .dont_ramp_after_moon = DEFAULT_DONT_RAMP_AFTER_MOON, + #endif + + #ifdef USE_MANUAL_MEMORY + .manual_memory = DEFAULT_MANUAL_MEMORY, + #ifdef USE_MANUAL_MEMORY_TIMER + .manual_memory_timer = DEFAULT_MANUAL_MEMORY_TIMER, + #endif + #endif + + ///// channel modes / color modes + + #if NUM_CHANNEL_MODES > 1 + // current multi-channel mode + .channel_mode = DEFAULT_CHANNEL_MODE, + // user can take unwanted modes out of the rotation (bitmask) + .channel_modes_enabled = CHANNEL_MODES_ENABLED, + #ifdef USE_MANUAL_MEMORY + // reset w/ manual memory + .manual_memory_channel_mode = DEFAULT_CHANNEL_MODE, + #endif + #ifdef DEFAULT_BLINK_CHANNEL + // blink numbers in a specific channel (user configurable) + .blink_channel = DEFAULT_BLINK_CHANNEL, + #endif + #endif + #ifdef USE_CHANNEL_MODE_ARGS + // one byte of extra data per channel mode, like for tint value + .channel_mode_args = { CHANNEL_MODE_ARGS }, + #ifdef USE_MANUAL_MEMORY + // remember and reset 1 extra parameter per channel mode (like tint) + .manual_memory_channel_args = { CHANNEL_MODE_ARGS }, + #endif + #ifdef USE_STEPPED_TINT_RAMPING + .tint_ramp_style = DEFAULT_TINT_RAMP_STYLE, + #endif + #endif + + ///// Smooth animation between steps, and for on/off + #ifdef USE_SMOOTH_STEPS + .smooth_steps_style = DEFAULT_SMOOTH_STEPS_STYLE, + #endif + + ///// strobe / blinky mode settings + + #ifdef USE_STROBE_STATE + .strobe_type = DEFAULT_STROBE, + #if (NUM_CHANNEL_MODES > 1) && defined(USE_CHANNEL_PER_STROBE) + // channel mode saved per strobe-group mode + #ifdef DEFAULT_STROBE_CHANNELS + .strobe_channels = { DEFAULT_STROBE_CHANNELS }, + #endif + #endif + #endif + #if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) + // party / tactical strobe timing + // party strobe 24 Hz, tactical strobe 10 Hz + .strobe_delays = { 41, 67 }, + #endif + #ifdef USE_BIKE_FLASHER_MODE + .bike_flasher_brightness = MAX_1x7135, + #endif + #ifdef USE_BEACON_MODE + // beacon timing + .beacon_seconds = 2, + #endif + + ///// voltage and temperature + + #ifdef USE_VOLTAGE_CORRECTION + // same 0.05V units as fudge factor, + // but 7 is neutral, and the expected range is from 1 to 13 + .voltage_correction = 7, + #endif + #ifdef USE_THERMAL_REGULATION + .therm_ceil = DEFAULT_THERM_CEIL, + .therm_cal_offset = 0, + #endif + + ///// aux LEDs + + #ifdef USE_INDICATOR_LED + // bits 2-3 control lockout mode + // bits 0-1 control "off" mode + // modes are: 0=off, 1=low, 2=high, 3=blinking (if TICK_DURING_STANDBY enabled) + .indicator_led_mode = INDICATOR_LED_DEFAULT_MODE, + #endif + #ifdef USE_AUX_RGB_LEDS + .rgb_led_off_mode = RGB_LED_OFF_DEFAULT, + .rgb_led_lockout_mode = RGB_LED_LOCKOUT_DEFAULT, + #ifdef USE_POST_OFF_VOLTAGE + // display voltage readout for a while after turning off? + .post_off_voltage = DEFAULT_POST_OFF_VOLTAGE_SECONDS, + #endif + #endif + + ///// misc other mode settings + + #ifdef USE_AUTOLOCK + .autolock_time = DEFAULT_AUTOLOCK_TIME, + #endif + #ifdef USE_TACTICAL_MODE + .tactical_levels = { TACTICAL_LEVELS }, + #endif + + ///// hardware config / globals menu + + #ifdef USE_JUMP_START + .jump_start_level = DEFAULT_JUMP_START_LEVEL, + #endif + +}; + diff --git a/ui/anduril/lockout-mode-fsm.h b/ui/anduril/lockout-mode-fsm.h new file mode 100644 index 0000000..ede251c --- /dev/null +++ b/ui/anduril/lockout-mode-fsm.h @@ -0,0 +1,11 @@ +// lockout-mode-fsm.h: FSM config for lockout mode in Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// autolock function requires the ability to measure time while "off" +#ifdef USE_AUTOLOCK +#define TICK_DURING_STANDBY +#endif + diff --git a/ui/anduril/lockout-mode.c b/ui/anduril/lockout-mode.c new file mode 100644 index 0000000..422d081 --- /dev/null +++ b/ui/anduril/lockout-mode.c @@ -0,0 +1,219 @@ +// lockout-mode.c: Lockout mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "lockout-mode.h" + +uint8_t lockout_state(Event event, uint16_t arg) { + #ifdef USE_MOON_DURING_LOCKOUT_MODE + // momentary(ish) moon mode during lockout + // button is being held + #ifdef USE_AUX_RGB_LEDS + // don't turn on during RGB aux LED configuration + if (event == EV_click7_hold) { set_level(0); } else + #endif + if ((event & (B_CLICK | B_PRESS)) == (B_CLICK | B_PRESS)) { + // hold: lowest floor + // click, hold: highest floor (or manual mem level) + uint8_t lvl = cfg.ramp_floors[0]; + if (1 == (event & 0x0f)) { // first click + if (cfg.ramp_floors[1] < lvl) lvl = cfg.ramp_floors[1]; + } else { // 2nd click or later + if (cfg.ramp_floors[1] > lvl) lvl = cfg.ramp_floors[1]; + #ifdef USE_MANUAL_MEMORY + if (cfg.manual_memory) lvl = cfg.manual_memory; + #endif + } + set_level(lvl); + } + // button was released + else if ((event & (B_CLICK | B_PRESS)) == (B_CLICK)) { + set_level(0); + } + #endif // ifdef USE_MOON_DURING_LOCKOUT_MODE + + // regular event handling + // conserve power while locked out + // (allow staying awake long enough to exit, but otherwise + // be persistent about going back to sleep every few seconds + // even if the user keeps pressing the button) + if (event == EV_enter_state) { + ticks_since_on = 0; + #ifdef USE_INDICATOR_LED + // redundant, sleep tick does the same thing + // indicator_led_update(cfg.indicator_led_mode >> 2, 0); + #elif defined(USE_AUX_RGB_LEDS) + rgb_led_update(cfg.rgb_led_lockout_mode, 0); + #endif + } + + else if (event == EV_tick) { + if (arg > HOLD_TIMEOUT) { + go_to_standby = 1; + #ifdef USE_INDICATOR_LED + // redundant, sleep tick does the same thing + //indicator_led_update(cfg.indicator_led_mode >> 2, arg); + #elif defined(USE_AUX_RGB_LEDS) + rgb_led_update(cfg.rgb_led_lockout_mode, arg); + #endif + } + return EVENT_HANDLED; + } + + #if defined(TICK_DURING_STANDBY) && (defined(USE_INDICATOR_LED) || defined(USE_AUX_RGB_LEDS)) + else if (event == EV_sleep_tick) { + if (ticks_since_on < 255) ticks_since_on ++; + #ifdef USE_MANUAL_MEMORY_TIMER + // reset to manual memory level when timer expires + if (cfg.manual_memory && + (arg >= (cfg.manual_memory_timer * SLEEP_TICKS_PER_MINUTE))) { + manual_memory_restore(); + } + #endif + #if defined(USE_INDICATOR_LED) + indicator_led_update(cfg.indicator_led_mode >> 2, arg); + #elif defined(USE_AUX_RGB_LEDS) + rgb_led_update(cfg.rgb_led_lockout_mode, arg); + #endif + return EVENT_HANDLED; + } + #endif + + // 3 clicks: exit and turn off + else if (event == EV_3clicks) { + blink_once(); + set_state(off_state, 0); + return EVENT_HANDLED; + } + + // 4 clicks: exit and turn on + else if (event == EV_4clicks) { + #if defined(USE_MANUAL_MEMORY) && !defined(USE_MANUAL_MEMORY_TIMER) + // this clause probably isn't used by any configs any more + // but is included just in case someone configures it this way + if (cfg.manual_memory) + set_state(steady_state, cfg.manual_memory); + else + #endif + set_state(steady_state, memorized_level); + return EVENT_HANDLED; + } + + // 4 clicks, but hold last: exit and start at floor + else if (event == EV_click4_hold) { + //blink_once(); + blip(); + // reset button sequence to avoid activating anything in ramp mode + current_event = 0; + // ... and back to ramp mode + set_state(steady_state, 1); + return EVENT_HANDLED; + } + + // 5 clicks: exit and turn on at ceiling level + else if (event == EV_5clicks) { + set_state(steady_state, MAX_LEVEL); + return EVENT_HANDLED; + } + + #if NUM_CHANNEL_MODES > 1 + // 3H: next channel mode + else if (event == EV_click3_hold) { + if (0 == (arg % TICKS_PER_SECOND)) { + // pretend the user clicked 3 times to change channels + return channel_mode_state(EV_3clicks, 0); + } + } + #endif + + ////////// Every action below here is blocked in the (non-Extended) Simple UI ////////// + + #if defined(USE_SIMPLE_UI) && !defined(USE_EXTENDED_SIMPLE_UI) + if (cfg.simple_ui_active) { + return EVENT_NOT_HANDLED; + } + #endif // if simple UI but not extended simple UI + + #if defined(USE_INDICATOR_LED) + // 7 clicks: rotate through indicator LED modes (lockout mode) + else if (event == EV_7clicks) { + #if defined(USE_INDICATOR_LED) + uint8_t mode = cfg.indicator_led_mode >> 2; + #ifdef TICK_DURING_STANDBY + mode = (mode + 1) & 3; + #else + mode = (mode + 1) % 3; + #endif + #ifdef INDICATOR_LED_SKIP_LOW + if (mode == 1) { mode ++; } + #endif + cfg.indicator_led_mode = (mode << 2) + (cfg.indicator_led_mode & 0x03); + // redundant, sleep tick does the same thing + //indicator_led_update(cfg.indicator_led_mode >> 2, arg); + #elif defined(USE_AUX_RGB_LEDS) + #endif + save_config(); + return EVENT_HANDLED; + } + #elif defined(USE_AUX_RGB_LEDS) + // 7 clicks: change RGB aux LED pattern + else if (event == EV_7clicks) { + uint8_t mode = (cfg.rgb_led_lockout_mode >> 4) + 1; + mode = mode % RGB_LED_NUM_PATTERNS; + cfg.rgb_led_lockout_mode = (mode << 4) | (cfg.rgb_led_lockout_mode & 0x0f); + rgb_led_update(cfg.rgb_led_lockout_mode, 0); + save_config(); + blink_once(); + return EVENT_HANDLED; + } + // 7H: change RGB aux LED color + else if (event == EV_click7_hold) { + setting_rgb_mode_now = 1; + if (0 == (arg & 0x3f)) { + uint8_t mode = (cfg.rgb_led_lockout_mode & 0x0f) + 1; + mode = mode % RGB_LED_NUM_COLORS; + cfg.rgb_led_lockout_mode = mode | (cfg.rgb_led_lockout_mode & 0xf0); + //save_config(); + } + rgb_led_update(cfg.rgb_led_lockout_mode, arg); + return EVENT_HANDLED; + } + // 7H, release: save new color + else if (event == EV_click7_hold_release) { + setting_rgb_mode_now = 0; + save_config(); + return EVENT_HANDLED; + } + #endif + + #if defined(USE_EXTENDED_SIMPLE_UI) && defined(USE_SIMPLE_UI) + ////////// Every action below here is blocked in the Extended Simple UI ////////// + if (cfg.simple_ui_active) { + return EVENT_NOT_HANDLED; + } + #endif // if extended simple UI + + #ifdef USE_AUTOLOCK + // 10H: configure the autolock option + else if (event == EV_click10_hold) { + push_state(autolock_config_state, 0); + return EVENT_HANDLED; + } + #endif + + return EVENT_NOT_HANDLED; +} + +#ifdef USE_AUTOLOCK +// set the auto-lock timer to N minutes, where N is the number of clicks +void autolock_config_save(uint8_t step, uint8_t value) { + cfg.autolock_time = value; +} + +uint8_t autolock_config_state(Event event, uint16_t arg) { + return config_state_base(event, arg, 1, autolock_config_save); +} +#endif // #ifdef USE_AUTOLOCK + diff --git a/ui/anduril/lockout-mode.h b/ui/anduril/lockout-mode.h new file mode 100644 index 0000000..c2703a0 --- /dev/null +++ b/ui/anduril/lockout-mode.h @@ -0,0 +1,16 @@ +// lockout-mode.h: Lockout mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// soft lockout +uint8_t lockout_state(Event event, uint16_t arg); + +#ifdef USE_AUTOLOCK +#ifndef DEFAULT_AUTOLOCK_TIME +#define DEFAULT_AUTOLOCK_TIME 0 // autolock time in minutes, 0 = disabled +#endif +uint8_t autolock_config_state(Event event, uint16_t arg); +#endif + diff --git a/ui/anduril/misc.c b/ui/anduril/misc.c new file mode 100644 index 0000000..1b92d6f --- /dev/null +++ b/ui/anduril/misc.c @@ -0,0 +1,42 @@ +// misc.c: Misc extra functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "misc.h" + +/* no longer used +void blink_confirm(uint8_t num) { + uint8_t brightness = actual_level; + uint8_t bump = actual_level + BLINK_BRIGHTNESS; + if (bump > MAX_LEVEL) bump = 0; + for (; num>0; num--) { + set_level(bump); + delay_4ms(10/4); + set_level(brightness); + if (num > 1) { delay_4ms(100/4); } + } +} +*/ + +// make a short, visible pulse +// (either brighter or darker, depending on current brightness) +void blink_once() { + uint8_t brightness = actual_level; + uint8_t bump = brightness + BLINK_BRIGHTNESS; + if (bump > MAX_LEVEL) bump = 0; + + set_level(bump); + delay_4ms(BLINK_ONCE_TIME/4); + set_level(brightness); +} + +// Just go dark for a moment to indicate to user that something happened +void blip() { + uint8_t temp = actual_level; + set_level(0); + delay_4ms(3); + set_level(temp); +} + diff --git a/ui/anduril/misc.h b/ui/anduril/misc.h new file mode 100644 index 0000000..0f2992a --- /dev/null +++ b/ui/anduril/misc.h @@ -0,0 +1,10 @@ +// misc.h: Misc extra functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +//void blink_confirm(uint8_t num); // no longer used +void blink_once(); +void blip(); + diff --git a/ui/anduril/momentary-mode.c b/ui/anduril/momentary-mode.c new file mode 100644 index 0000000..a765142 --- /dev/null +++ b/ui/anduril/momentary-mode.c @@ -0,0 +1,67 @@ +// momentary-mode.c: Momentary mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "momentary-mode.h" + +uint8_t momentary_state(Event event, uint16_t arg) { + // init strobe mode, if relevant + #ifdef USE_STROBE_STATE + if ((event == EV_enter_state) && (momentary_mode != 0)) { + strobe_state(event, arg); + } + #endif + + // light up when the button is pressed; go dark otherwise + // button is being held + if ((event & (B_CLICK | B_PRESS)) == (B_CLICK | B_PRESS)) { + momentary_active = 1; + // 0 = ramping, 1 = strobes + if (momentary_mode == 0) { + set_level(memorized_level); + } + return EVENT_HANDLED; + } + // button was released + else if ((event & (B_CLICK | B_PRESS)) == (B_CLICK)) { + momentary_active = 0; + set_level(0); + //go_to_standby = 1; // sleep while light is off + return EVENT_HANDLED; + } + + // Sleep, dammit! (but wait a few seconds first) + // (because standby mode uses such little power that it can interfere + // with exiting via tailcap loosen+tighten unless you leave power + // disconnected for several seconds, so we want to be awake when that + // happens to speed up the process) + else if (event == EV_tick) { + #ifdef USE_STROBE_STATE + if (momentary_active) { + // 0 = ramping, non-zero = strobes + if (momentary_mode != 0) { + return strobe_state(event, arg); + } + } + else { + #endif + if (arg > TICKS_PER_SECOND*5) { // sleep after 5 seconds + go_to_standby = 1; // sleep while light is off + // turn off lighted button + #ifdef USE_INDICATOR_LED + indicator_led(0); + #elif defined(USE_AUX_RGB_LEDS) + rgb_led_update(0, 0); + #endif + } + #ifdef USE_STROBE_STATE + } + #endif + return EVENT_HANDLED; + } + + return EVENT_NOT_HANDLED; +} + diff --git a/ui/anduril/momentary-mode.h b/ui/anduril/momentary-mode.h new file mode 100644 index 0000000..d774801 --- /dev/null +++ b/ui/anduril/momentary-mode.h @@ -0,0 +1,11 @@ +// momentary-mode.h: Momentary mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// momentary / signalling mode +uint8_t momentary_state(Event event, uint16_t arg); +uint8_t momentary_mode = 0; // 0 = ramping, 1 = strobe +uint8_t momentary_active = 0; // boolean, true if active *right now* + diff --git a/ui/anduril/off-mode.c b/ui/anduril/off-mode.c new file mode 100644 index 0000000..0a381b7 --- /dev/null +++ b/ui/anduril/off-mode.c @@ -0,0 +1,384 @@ +// off-mode.c: "Off" mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "off-mode.h" + +#ifdef USE_SUNSET_TIMER +#include "sunset-timer.h" +#endif + +// set level smooth maybe +void off_state_set_level(uint8_t level); + + +uint8_t off_state(Event event, uint16_t arg) { + + // turn emitter off when entering state + if (event == EV_enter_state) { + // turn off + off_state_set_level(0); + #ifdef USE_SMOOTH_STEPS + // don't go to sleep while animating + arg |= smooth_steps_in_progress; + #endif + ticks_since_on = 0; + #if NUM_CHANNEL_MODES > 1 + // reset to ramp mode's channel when light turns off + channel_mode = cfg.channel_mode; + #endif + #ifdef USE_INDICATOR_LED + // redundant, sleep tick does the same thing + //indicator_led_update(cfg.indicator_led_mode & 0x03, 0); + #elif defined(USE_AUX_RGB_LEDS) + // redundant, sleep tick does the same thing + //rgb_led_update(cfg.rgb_led_off_mode, 0); + #endif + #ifdef USE_SUNSET_TIMER + sunset_timer = 0; // needs a reset in case previous timer was aborted + #endif + // sleep while off (lower power use) + // (unless delay requested; give the ADC some time to catch up) + if (! arg) { go_to_standby = 1; } + return EVENT_HANDLED; + } + + // go back to sleep eventually if we got bumped but didn't leave "off" state + else if (event == EV_tick) { + if (arg > HOLD_TIMEOUT + #ifdef USE_SMOOTH_STEPS + && (! smooth_steps_in_progress) + #endif + ) { + go_to_standby = 1; + #ifdef USE_INDICATOR_LED + // redundant, sleep tick does the same thing + //indicator_led_update(cfg.indicator_led_mode & 0x03, arg); + #elif defined(USE_AUX_RGB_LEDS) + // redundant, sleep tick does the same thing + //rgb_led_update(cfg.rgb_led_off_mode, arg); + #endif + } + return EVENT_HANDLED; + } + + #if defined(TICK_DURING_STANDBY) + // blink the indicator LED, maybe + else if (event == EV_sleep_tick) { + if (ticks_since_on < 255) ticks_since_on ++; + #ifdef USE_MANUAL_MEMORY_TIMER + // reset to manual memory level when timer expires + if (cfg.manual_memory && + (arg >= (cfg.manual_memory_timer * SLEEP_TICKS_PER_MINUTE))) { + manual_memory_restore(); + } + #endif + #ifdef USE_INDICATOR_LED + indicator_led_update(cfg.indicator_led_mode & 0x03, arg); + #elif defined(USE_AUX_RGB_LEDS) + rgb_led_update(cfg.rgb_led_off_mode, arg); + #endif + + #ifdef USE_AUTOLOCK + // lock the light after being off for N minutes + uint16_t ticks = cfg.autolock_time * SLEEP_TICKS_PER_MINUTE; + if ((cfg.autolock_time > 0) && (arg > ticks)) { + set_state(lockout_state, 0); + } + #endif // ifdef USE_AUTOLOCK + return EVENT_HANDLED; + } + #endif + + #if (B_TIMING_ON == B_PRESS_T) + // hold (initially): go to lowest level (floor), but allow abort for regular click + else if (event == EV_click1_press) { + off_state_set_level(nearest_level(1)); + return EVENT_HANDLED; + } + #endif // B_TIMING_ON == B_PRESS_T + + // hold: go to lowest level + else if (event == EV_click1_hold) { + #if (B_TIMING_ON == B_PRESS_T) + #ifdef MOON_TIMING_HINT + if (arg == 0) { + // let the user know they can let go now to stay at moon + blip(); + } else + #endif + #else // B_RELEASE_T or B_TIMEOUT_T + off_state_set_level(nearest_level(1)); + #endif + #ifdef USE_RAMP_AFTER_MOON_CONFIG + if (cfg.dont_ramp_after_moon) { + return EVENT_HANDLED; + } + #endif + // don't start ramping immediately; + // give the user time to release at moon level + //if (arg >= HOLD_TIMEOUT) { // smaller + if (arg >= (!cfg.ramp_style) * HOLD_TIMEOUT) { // more consistent + set_state(steady_state, 1); + } + return EVENT_HANDLED; + } + + // hold, release quickly: go to lowest level (floor) + else if (event == EV_click1_hold_release) { + set_state(steady_state, 1); + return EVENT_HANDLED; + } + + #if (B_TIMING_ON != B_TIMEOUT_T) + // 1 click (before timeout): go to memorized level, but allow abort for double click + else if (event == EV_click1_release) { + #if defined(USE_MANUAL_MEMORY) && !defined(USE_MANUAL_MEMORY_TIMER) + // this clause probably isn't used by any configs any more + // but is included just in case someone configures it this way + if (cfg.manual_memory) { + manual_memory_restore(); + } + #endif + off_state_set_level(nearest_level(memorized_level)); + return EVENT_HANDLED; + } + #endif // if (B_TIMING_ON != B_TIMEOUT_T) + + // 1 click: regular mode + else if (event == EV_1click) { + #if (B_TIMING_ON != B_TIMEOUT_T) + set_state(steady_state, memorized_level); + #else + // FIXME: B_TIMEOUT_T breaks manual_memory and manual_memory_timer + // (need to duplicate manual mem logic here, probably) + set_state(steady_state, memorized_level); + #endif + return EVENT_HANDLED; + } + + // click, hold: momentary at ceiling or turbo + else if (event == EV_click2_hold) { + ticks_since_on = 0; // momentary turbo is definitely "on" + uint8_t turbo_level; // how bright is "turbo"? + + #if defined(USE_2C_STYLE_CONFIG) // user can choose 2C behavior + uint8_t style_2c = cfg.ramp_2c_style; + #ifdef USE_SIMPLE_UI + // simple UI has its own turbo config + if (cfg.simple_ui_active) style_2c = cfg.ramp_2c_style_simple; + #endif + // 0 = ceiling + // 1+ = full power + if (0 == style_2c) turbo_level = nearest_level(MAX_LEVEL); + else turbo_level = MAX_LEVEL; + #else + // simple UI: ceiling + // full UI: full power + #ifdef USE_SIMPLE_UI + if (cfg.simple_ui_active) turbo_level = nearest_level(MAX_LEVEL); + else + #endif + turbo_level = MAX_LEVEL; + #endif + + off_state_set_level(turbo_level); + return EVENT_HANDLED; + } + else if (event == EV_click2_hold_release) { + off_state_set_level(0); + return EVENT_HANDLED; + } + + // 2 clicks: highest mode (ceiling) + else if (event == EV_2clicks) { + set_state(steady_state, MAX_LEVEL); + return EVENT_HANDLED; + } + + // 3 clicks (initial press): off, to prep for later events + else if (event == EV_click3_press) { + #ifdef USE_SMOOTH_STEPS + // immediately cancel any animations in progress + smooth_steps_in_progress = 0; + #endif + off_state_set_level(0); + return EVENT_HANDLED; + } + + #ifdef USE_BATTCHECK + // 3 clicks: battcheck mode / blinky mode group 1 + else if (event == EV_3clicks) { + set_state(battcheck_state, 0); + return EVENT_HANDLED; + } + #endif + + #ifdef USE_LOCKOUT_MODE + // 4 clicks: soft lockout + else if (event == EV_4clicks) { + blink_once(); + set_state(lockout_state, 0); + return EVENT_HANDLED; + } + #endif + + #if defined(USE_FACTORY_RESET) && defined(USE_SOFT_FACTORY_RESET) + // 13 clicks and hold the last click: invoke factory reset (reboot) + else if (event == EV_click13_hold) { + reboot(); + return EVENT_HANDLED; + } + #endif + + #ifdef USE_VERSION_CHECK + // 15+ clicks: show the version number + else if (event == EV_15clicks) { + set_state(version_check_state, 0); + return EVENT_HANDLED; + } + #endif + + #ifdef USE_SIMPLE_UI + // 10 clicks, but hold last click: turn simple UI off (or configure it) + else if ((event == EV_click10_hold) && (!arg)) { + if (cfg.simple_ui_active) { // turn off simple UI + blink_once(); + cfg.simple_ui_active = 0; + save_config(); + } + else { // configure simple UI ramp + push_state(simple_ui_config_state, 0); + } + return EVENT_HANDLED; + } + + ////////// Every action below here is blocked in the (non-Extended) Simple UI ////////// + + #ifndef USE_EXTENDED_SIMPLE_UI + if (cfg.simple_ui_active) { + return EVENT_NOT_HANDLED; + } + #endif // ifndef USE_EXTENDED_SIMPLE_UI + #endif // ifdef USE_SIMPLE_UI + + // click, click, long-click: strobe mode + #ifdef USE_STROBE_STATE + else if (event == EV_click3_hold) { + set_state(strobe_state, 0); + return EVENT_HANDLED; + } + #elif defined(USE_BORING_STROBE_STATE) + else if (event == EV_click3_hold) { + set_state(boring_strobe_state, 0); + return EVENT_HANDLED; + } + #endif + + #ifdef USE_INDICATOR_LED + // 7 clicks: change indicator LED mode + else if (event == EV_7clicks) { + uint8_t mode = (cfg.indicator_led_mode & 3) + 1; + #ifdef TICK_DURING_STANDBY + mode = mode & 3; + #else + mode = mode % 3; + #endif + #ifdef INDICATOR_LED_SKIP_LOW + if (mode == 1) { mode ++; } + #endif + cfg.indicator_led_mode = (cfg.indicator_led_mode & 0b11111100) | mode; + // redundant, sleep tick does the same thing + //indicator_led_update(cfg.indicator_led_mode & 0x03, arg); + save_config(); + return EVENT_HANDLED; + } + #elif defined(USE_AUX_RGB_LEDS) + // 7 clicks: change RGB aux LED pattern + else if (event == EV_7clicks) { + uint8_t mode = (cfg.rgb_led_off_mode >> 4) + 1; + mode = mode % RGB_LED_NUM_PATTERNS; + cfg.rgb_led_off_mode = (mode << 4) | (cfg.rgb_led_off_mode & 0x0f); + rgb_led_update(cfg.rgb_led_off_mode, 0); + save_config(); + blink_once(); + return EVENT_HANDLED; + } + // 7 clicks (hold last): change RGB aux LED color + else if (event == EV_click7_hold) { + setting_rgb_mode_now = 1; + if (0 == (arg & 0x3f)) { + uint8_t mode = (cfg.rgb_led_off_mode & 0x0f) + 1; + mode = mode % RGB_LED_NUM_COLORS; + cfg.rgb_led_off_mode = mode | (cfg.rgb_led_off_mode & 0xf0); + //save_config(); + } + rgb_led_update(cfg.rgb_led_off_mode, arg); + return EVENT_HANDLED; + } + else if (event == EV_click7_hold_release) { + setting_rgb_mode_now = 0; + save_config(); + return EVENT_HANDLED; + } + #endif // end 7 clicks + + ////////// Every action below here is blocked in the Extended Simple UI ////////// + + #ifdef USE_SIMPLE_UI + #ifdef USE_EXTENDED_SIMPLE_UI + if (cfg.simple_ui_active) { + return EVENT_NOT_HANDLED; + } + #endif // ifdef USE_EXTENDED_SIMPLE_UI + + // 10 clicks: enable simple UI + else if (event == EV_10clicks) { + blink_once(); + cfg.simple_ui_active = 1; + save_config(); + return EVENT_HANDLED; + } + #endif // ifdef USE_SIMPLE_UI + + #ifdef USE_MOMENTARY_MODE + // 5 clicks: momentary mode + else if (event == EV_5clicks) { + blink_once(); + set_state(momentary_state, 0); + return EVENT_HANDLED; + } + #endif + + #ifdef USE_TACTICAL_MODE + // 6 clicks: tactical mode + else if (event == EV_6clicks) { + blink_once(); + set_state(tactical_state, 0); + return EVENT_HANDLED; + } + #endif + + #ifdef USE_GLOBALS_CONFIG + // 9 clicks, but hold last click: configure misc global settings + else if ((event == EV_click9_hold) && (!arg)) { + push_state(globals_config_state, 0); + return EVENT_HANDLED; + } + #endif + + return EVENT_NOT_HANDLED; +} + + +void off_state_set_level(uint8_t level) { + // this pattern gets used a few times, so reduce duplication + #ifdef USE_SMOOTH_STEPS + if (cfg.smooth_steps_style) set_level_smooth(level, 8); + else + #endif + set_level(level); +} + diff --git a/ui/anduril/off-mode.h b/ui/anduril/off-mode.h new file mode 100644 index 0000000..d07fff1 --- /dev/null +++ b/ui/anduril/off-mode.h @@ -0,0 +1,12 @@ +// off-mode.h: "Off" mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// was the light in an "on" mode within the past second or so? +uint8_t ticks_since_on = 0; + +// when the light is "off" or in standby +uint8_t off_state(Event event, uint16_t arg); + diff --git a/ui/anduril/ramp-mode-fsm.h b/ui/anduril/ramp-mode-fsm.h new file mode 100644 index 0000000..edfd6db --- /dev/null +++ b/ui/anduril/ramp-mode-fsm.h @@ -0,0 +1,38 @@ +// ramp-mode-fsm.h: FSM config for ramping functions in Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// enable FSM's ramping features +#define USE_RAMPING + +// do smooth adjustments when compensating for temperature +#ifdef USE_THERMAL_REGULATION +#define USE_SET_LEVEL_GRADUALLY // isn't used except for thermal adjustments +#endif + +// brightness to use when no memory is set +// FIXME: this is only here to stop an error in fsm-ramping.c, +// which should be fixed by using a different symbol instead +// (like BUTTON_LED_BRIGHT_LEVEL or RAMP_HALFWAY_LEVEL or something) +#ifndef DEFAULT_LEVEL +#define DEFAULT_LEVEL MAX_1x7135 +#endif + +// requires the ability to measure time while "off" +#ifdef USE_MANUAL_MEMORY_TIMER +#define TICK_DURING_STANDBY +#endif + +// ensure the jump start feature gets compiled in if needed +#ifdef DEFAULT_JUMP_START_LEVEL +#define USE_JUMP_START +#endif + +// include an extra config mode for random stuff which doesn't fit elsewhere +#if defined(USE_JUMP_START) || \ + (defined(USE_CHANNEL_MODE_ARGS) && defined(USE_STEPPED_TINT_RAMPING)) +#define USE_GLOBALS_CONFIG +#endif + diff --git a/ui/anduril/ramp-mode.c b/ui/anduril/ramp-mode.c new file mode 100644 index 0000000..4611b4f --- /dev/null +++ b/ui/anduril/ramp-mode.c @@ -0,0 +1,741 @@ +// ramp-mode.c: Ramping functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "ramp-mode.h" + +#ifdef USE_SUNSET_TIMER +#include "sunset-timer.h" +#endif + +#ifdef USE_SMOOTH_STEPS +#include "smooth-steps.h" +#endif + + +uint8_t steady_state(Event event, uint16_t arg) { + static int8_t ramp_direction = 1; + #if (B_TIMING_OFF == B_RELEASE_T) + // if the user double clicks, we need to abort turning off, + // and this stores the level to return to + static uint8_t level_before_off = 0; + #endif + + #if NUM_CHANNEL_MODES > 1 + channel_mode = cfg.channel_mode; + #endif + + // make sure ramp globals are correct... + // ... but they already are; no need to do it here + //ramp_update_config(); + //nearest_level(1); // same effect, takes less space + + uint8_t mode_min = ramp_floor; + uint8_t mode_max = ramp_ceil; + uint8_t step_size; + if (cfg.ramp_style) { step_size = ramp_discrete_step_size; } + else { step_size = 1; } + + // how bright is "turbo"? + uint8_t turbo_level; + #if defined(USE_2C_STYLE_CONFIG) // user can choose 2C behavior + uint8_t style_2c = cfg.ramp_2c_style; + #ifdef USE_SIMPLE_UI + // simple UI has its own turbo config + if (cfg.simple_ui_active) style_2c = cfg.ramp_2c_style_simple; + #endif + // 0 = no turbo + // 1 = Anduril 1 direct to turbo + // 2 = Anduril 2 direct to ceiling, or turbo if already at ceiling + if (0 == style_2c) turbo_level = mode_max; + else if (1 == style_2c) turbo_level = MAX_LEVEL; + else { + if (memorized_level < mode_max) { turbo_level = mode_max; } + else { turbo_level = MAX_LEVEL; } + } + #elif defined(USE_2C_MAX_TURBO) // Anduril 1 style always + // simple UI: to/from ceiling + // full UI: to/from turbo (Anduril1 behavior) + #ifdef USE_SIMPLE_UI + if (cfg.simple_ui_active) turbo_level = mode_max; + else + #endif + turbo_level = MAX_LEVEL; + #else // Anduril 2 style always + // simple UI: to/from ceiling + // full UI: to/from ceiling if mem < ceiling, + // or to/from turbo if mem >= ceiling + if ((memorized_level < mode_max) + #ifdef USE_SIMPLE_UI + || cfg.simple_ui_active + #endif + ) { turbo_level = mode_max; } + else { turbo_level = MAX_LEVEL; } + #endif + + #ifdef USE_SUNSET_TIMER + // handle the shutoff timer first + uint8_t sunset_active = sunset_timer; // save for comparison + // clock tick + sunset_timer_state(event, arg); + // if the timer was just turned on + if (sunset_timer && (! sunset_active)) { + sunset_timer_orig_level = actual_level; + } + // if the timer just expired, shut off + else if (sunset_active && (! sunset_timer)) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + #endif // ifdef USE_SUNSET_TIMER + + // turn LED on when we first enter the mode + if ((event == EV_enter_state) || (event == EV_reenter_state)) { + #if defined(USE_MOMENTARY_MODE) && defined(USE_STROBE_STATE) + momentary_mode = 0; // 0 = ramping, 1 = strobes + #endif + // if we just got back from config mode, go back to memorized level + if (event == EV_reenter_state) { + arg = memorized_level; + } + // remember this level, unless it's moon or turbo + if ((arg > mode_min) && (arg < mode_max)) + memorized_level = arg; + // use the requested level even if not memorized + arg = nearest_level(arg); + set_level_and_therm_target(arg); + ramp_direction = 1; + return EVENT_HANDLED; + } + #if (B_TIMING_OFF == B_RELEASE_T) + // 1 click (early): off, if configured for early response + else if (event == EV_click1_release) { + level_before_off = actual_level; + set_level_and_therm_target(0); + return EVENT_HANDLED; + } + // 2 clicks (early): abort turning off, if configured for early response + else if (event == EV_click2_press) { + set_level_and_therm_target(level_before_off); + return EVENT_HANDLED; + } + #endif // if (B_TIMING_OFF == B_RELEASE_T) + // 1 click: off + else if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: go to/from highest level + else if (event == EV_2clicks) { + if (actual_level < turbo_level) { + set_level_and_therm_target(turbo_level); + } + else { + set_level_and_therm_target(memorized_level); + } + #ifdef USE_SUNSET_TIMER + reset_sunset_timer(); + #endif + return EVENT_HANDLED; + } + + #ifdef USE_LOCKOUT_MODE + // 4 clicks: shortcut to lockout mode + else if (event == EV_4clicks) { + set_level(0); + set_state(lockout_state, 0); + return EVENT_HANDLED; + } + #endif + + // hold: change brightness (brighter, dimmer) + // click, hold: change brightness (dimmer) + else if ((event == EV_click1_hold) || (event == EV_click2_hold)) { + // ramp infrequently in stepped mode + if (cfg.ramp_style && (arg % HOLD_TIMEOUT != 0)) + return EVENT_HANDLED; + #ifdef USE_RAMP_SPEED_CONFIG + // ramp slower if user configured things that way + if ((! cfg.ramp_style) && (arg % ramp_speed)) + return EVENT_HANDLED; + #endif + #ifdef USE_SMOOTH_STEPS + // if a brightness transition is already happening, + // don't interrupt it + // (like 2C for full turbo then 1H to smooth ramp down + // ... without this clause, it flickers because it trips + // the "blink at ramp ceil" clause below, over and over) + if (smooth_steps_in_progress) return EVENT_HANDLED; + #endif + // fix ramp direction on first frame if necessary + if (!arg) { + // click, hold should always go down if possible + if (event == EV_click2_hold) { ramp_direction = -1; } + // make it ramp down instead, if already at max + else if (actual_level >= mode_max) { ramp_direction = -1; } + // make it ramp up if already at min + // (off->hold->stepped_min->release causes this state) + else if (actual_level <= mode_min) { ramp_direction = 1; } + } + // if the button is stuck, err on the side of safety and ramp down + else if ((arg > TICKS_PER_SECOND * 5 + #ifdef USE_RAMP_SPEED_CONFIG + // FIXME: count from time actual_level hits mode_max, + // not from beginning of button hold + * ramp_speed + #endif + ) && (actual_level >= mode_max)) { + ramp_direction = -1; + } + #ifdef USE_LOCKOUT_MODE + // if the button is still stuck, lock the light + else if ((arg > TICKS_PER_SECOND * 10 + #ifdef USE_RAMP_SPEED_CONFIG + // FIXME: count from time actual_level hits mode_min, + // not from beginning of button hold + * ramp_speed + #endif + ) && (actual_level <= mode_min)) { + blink_once(); + set_state(lockout_state, 0); + } + #endif + memorized_level = nearest_level((int16_t)actual_level \ + + (step_size * ramp_direction)); + #if defined(BLINK_AT_RAMP_CEIL) || defined(BLINK_AT_RAMP_MIDDLE) + // only blink once for each threshold + // FIXME: blinks at beginning of smooth_steps animation instead + // of the end, so it should blink when actual_level reaches a + // threshold, instead of when memorized_level does + // (one possible fix is to just remove mid-ramp blinks entirely, + // and just blink only when it hits the top while going up) + if ((memorized_level != actual_level) && ( + 0 // for easier syntax below + #ifdef BLINK_AT_RAMP_MIDDLE_1 + || (memorized_level == BLINK_AT_RAMP_MIDDLE_1) + #endif + #ifdef BLINK_AT_RAMP_MIDDLE_2 + || (memorized_level == BLINK_AT_RAMP_MIDDLE_2) + #endif + #ifdef BLINK_AT_RAMP_CEIL + // FIXME: only blink at top when going up, not down + || (memorized_level == mode_max) + #endif + #ifdef BLINK_AT_RAMP_FLOOR + || (memorized_level == mode_min) + #endif + )) { + blip(); + } + #endif + #if defined(BLINK_AT_STEPS) + uint8_t foo = cfg.ramp_style; + cfg.ramp_style = 1; + uint8_t nearest = nearest_level((int16_t)actual_level); + cfg.ramp_style = foo; + // only blink once for each threshold + if ((memorized_level != actual_level) && + (cfg.ramp_style == 0) && + (memorized_level == nearest) + ) + { + blip(); + } + #endif + set_level_and_therm_target(memorized_level); + #ifdef USE_SUNSET_TIMER + reset_sunset_timer(); + #endif + return EVENT_HANDLED; + } + // reverse ramp direction on hold release + else if ((event == EV_click1_hold_release) + || (event == EV_click2_hold_release)) { + ramp_direction = -ramp_direction; + #ifdef START_AT_MEMORIZED_LEVEL + save_config_wl(); + #endif + return EVENT_HANDLED; + } + + else if (event == EV_tick) { + // un-reverse after 1 second + if (arg == AUTO_REVERSE_TIME) ramp_direction = 1; + + #ifdef USE_SUNSET_TIMER + // reduce output if shutoff timer is active + if (sunset_timer) { + uint8_t dimmed_level = sunset_timer_orig_level * sunset_timer / sunset_timer_peak; + uint8_t dimmed_level_next = sunset_timer_orig_level * (sunset_timer-1) / sunset_timer_peak; + uint8_t dimmed_level_delta = dimmed_level - dimmed_level_next; + dimmed_level -= dimmed_level_delta * (sunset_ticks/TICKS_PER_SECOND) / 60; + if (dimmed_level < 1) dimmed_level = 1; + + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(dimmed_level); + target_level = dimmed_level; + #else + set_level_and_therm_target(dimmed_level); + #endif + } + #endif // ifdef USE_SUNSET_TIMER + + #ifdef USE_SET_LEVEL_GRADUALLY + int16_t diff = gradual_target - actual_level; + static uint16_t ticks_since_adjust = 0; + ticks_since_adjust++; + if (diff) { + uint16_t ticks_per_adjust = 256; + if (diff < 0) { + //diff = -diff; + if (actual_level > THERM_FASTER_LEVEL) { + #ifdef THERM_HARD_TURBO_DROP + ticks_per_adjust >>= 2; + #endif + ticks_per_adjust >>= 2; + } + } else { + // rise at half speed + ticks_per_adjust <<= 1; + } + while (diff) { + ticks_per_adjust >>= 1; + //diff >>= 1; + diff /= 2; // because shifting produces weird behavior + } + if (ticks_since_adjust > ticks_per_adjust) + { + gradual_tick(); + ticks_since_adjust = 0; + } + } + #endif // ifdef USE_SET_LEVEL_GRADUALLY + return EVENT_HANDLED; + } + + #ifdef USE_THERMAL_REGULATION + // overheating: drop by an amount proportional to how far we are above the ceiling + else if (event == EV_temperature_high) { + #if 0 + blip(); + #endif + #ifdef THERM_HARD_TURBO_DROP + //if (actual_level > THERM_FASTER_LEVEL) { + if (actual_level == MAX_LEVEL) { + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(THERM_FASTER_LEVEL); + target_level = THERM_FASTER_LEVEL; + #else + set_level_and_therm_target(THERM_FASTER_LEVEL); + #endif + } else + #endif + if (actual_level > MIN_THERM_STEPDOWN) { + int16_t stepdown = actual_level - arg; + if (stepdown < MIN_THERM_STEPDOWN) stepdown = MIN_THERM_STEPDOWN; + else if (stepdown > MAX_LEVEL) stepdown = MAX_LEVEL; + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(stepdown); + #else + set_level(stepdown); + #endif + } + return EVENT_HANDLED; + } + // underheating: increase slowly if we're lower than the target + // (proportional to how low we are) + else if (event == EV_temperature_low) { + #if 0 + blip(); + #endif + if (actual_level < target_level) { + //int16_t stepup = actual_level + (arg>>1); + int16_t stepup = actual_level + arg; + if (stepup > target_level) stepup = target_level; + else if (stepup < MIN_THERM_STEPDOWN) stepup = MIN_THERM_STEPDOWN; + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(stepup); + #else + set_level(stepup); + #endif + } + return EVENT_HANDLED; + } + #ifdef USE_SET_LEVEL_GRADUALLY + // temperature is within target window + // (so stop trying to adjust output) + else if (event == EV_temperature_okay) { + // if we're still adjusting output... stop after the current step + if (gradual_target > actual_level) + gradual_target = actual_level + 1; + else if (gradual_target < actual_level) + gradual_target = actual_level - 1; + return EVENT_HANDLED; + } + #endif // ifdef USE_SET_LEVEL_GRADUALLY + #endif // ifdef USE_THERMAL_REGULATION + + ////////// Every action below here is blocked in the simple UI ////////// + // That is, unless we specifically want to enable 3C for smooth/stepped selection in Simple UI + #if defined(USE_SIMPLE_UI) && !defined(USE_SIMPLE_UI_RAMPING_TOGGLE) + if (cfg.simple_ui_active) { + return EVENT_NOT_HANDLED; + } + #endif + + // 3 clicks: toggle smooth vs discrete ramping + // (and/or 6 clicks when there are multiple channel modes) + // (handle 3C here anyway, when all but 1 mode is disabled) + else if ((event == EV_3clicks) + #if NUM_CHANNEL_MODES > 1 + || (event == EV_6clicks) + ) { + // detect if > 1 channel mode is enabled, + // and if so, fall through so channel mode code can handle it + // otherwise, change the ramp style + if (event == EV_3clicks) { + uint8_t enabled = 0; + for (uint8_t m=0; m 1) + return EVENT_NOT_HANDLED; + } + #else + ) { + #endif + + cfg.ramp_style = !cfg.ramp_style; + save_config(); + #ifdef START_AT_MEMORIZED_LEVEL + save_config_wl(); + #endif + blip(); + memorized_level = nearest_level(actual_level); + set_level_and_therm_target(memorized_level); + #ifdef USE_SUNSET_TIMER + reset_sunset_timer(); + #endif + return EVENT_HANDLED; + } + + // If we allowed 3C in Simple UI, now block further actions + #if defined(USE_SIMPLE_UI) && defined(USE_SIMPLE_UI_RAMPING_TOGGLE) + if (cfg.simple_ui_active) { + return EVENT_NOT_HANDLED; + } + #endif + + // 3H: momentary turbo (on lights with no tint ramping) + // (or 4H when tint ramping is available) + else if ((event == EV_click3_hold) + #ifdef USE_CHANNEL_MODE_ARGS + || (event == EV_click4_hold) + #endif + ) { + #ifdef USE_CHANNEL_MODE_ARGS + // ramp tint if tint exists in this mode + if ((event == EV_click3_hold) + && (channel_has_args(channel_mode))) + return EVENT_NOT_HANDLED; + #endif + if (! arg) { // first frame only, to allow thermal regulation to work + #ifdef USE_2C_STYLE_CONFIG + uint8_t tl = style_2c ? MAX_LEVEL : turbo_level; + set_level_and_therm_target(tl); + #else + set_level_and_therm_target(turbo_level); + #endif + } + return EVENT_HANDLED; + } + else if ((event == EV_click3_hold_release) + #ifdef USE_CHANNEL_MODE_ARGS + || (event == EV_click4_hold_release) + #endif + ) { + #ifdef USE_CHANNEL_MODE_ARGS + // ramp tint if tint exists in this mode + if ((event == EV_click3_hold_release) + && (channel_has_args(channel_mode))) + return EVENT_NOT_HANDLED; + #endif + set_level_and_therm_target(memorized_level); + return EVENT_HANDLED; + } + + #ifdef USE_MOMENTARY_MODE + // 5 clicks: shortcut to momentary mode + else if (event == EV_5clicks) { + set_level(0); + set_state(momentary_state, 0); + return EVENT_HANDLED; + } + #endif + + #ifdef USE_RAMP_CONFIG + // 7H: configure this ramp mode + else if (event == EV_click7_hold) { + push_state(ramp_config_state, 0); + return EVENT_HANDLED; + } + #endif + + #ifdef USE_MANUAL_MEMORY + else if (event == EV_10clicks) { + // turn on manual memory and save current brightness + manual_memory_save(); + save_config(); + blink_once(); + return EVENT_HANDLED; + } + else if (event == EV_click10_hold) { + #ifdef USE_RAMP_EXTRAS_CONFIG + // let user configure a bunch of extra ramp options + push_state(ramp_extras_config_state, 0); + #else // manual mem, but no timer + // turn off manual memory; go back to automatic + if (0 == arg) { + cfg.manual_memory = 0; + save_config(); + blink_once(); + } + #endif + return EVENT_HANDLED; + } + #endif // ifdef USE_MANUAL_MEMORY + + return EVENT_NOT_HANDLED; +} + + +#ifdef USE_RAMP_CONFIG +void ramp_config_save(uint8_t step, uint8_t value) { + + // 0 = smooth ramp, 1 = stepped ramp, 2 = simple UI's ramp + uint8_t style = cfg.ramp_style; + #ifdef USE_SIMPLE_UI + if (current_state == simple_ui_config_state) style = 2; + #endif + + #if defined(USE_SIMPLE_UI) && defined(USE_2C_STYLE_CONFIG) + // simple UI config is weird... + // has some ramp extras after floor/ceil/steps + if (4 == step) { + cfg.ramp_2c_style_simple = value; + } + else + #endif + + // save adjusted value to the correct slot + if (value) { + // ceiling value is inverted + if (step == 2) value = MAX_LEVEL + 1 - value; + + // which option are we configuring? + // TODO? maybe rearrange definitions to avoid the need for this table + // (move all ramp values into a single array?) + uint8_t *steps[] = { cfg.ramp_floors, cfg.ramp_ceils, cfg.ramp_stepss }; + uint8_t *option; + option = steps[step-1]; + option[style] = value; + } +} + +uint8_t ramp_config_state(Event event, uint16_t arg) { + #ifdef USE_RAMP_SPEED_CONFIG + const uint8_t num_config_steps = 3; + #else + uint8_t num_config_steps = cfg.ramp_style + 2; + #endif + return config_state_base(event, arg, + num_config_steps, ramp_config_save); +} + +#ifdef USE_SIMPLE_UI +uint8_t simple_ui_config_state(Event event, uint16_t arg) { + #if defined(USE_2C_STYLE_CONFIG) + #define SIMPLE_UI_NUM_MENU_ITEMS 4 + #else + #define SIMPLE_UI_NUM_MENU_ITEMS 3 + #endif + return config_state_base(event, arg, + SIMPLE_UI_NUM_MENU_ITEMS, + ramp_config_save); +} +#endif +#endif // #ifdef USE_RAMP_CONFIG + +#ifdef USE_RAMP_EXTRAS_CONFIG +void ramp_extras_config_save(uint8_t step, uint8_t value) { + // item 1: disable manual memory, go back to automatic + if (manual_memory_config_step == step) { + cfg.manual_memory = 0; + } + + #ifdef USE_MANUAL_MEMORY_TIMER + // item 2: set manual memory timer duration + // FIXME: should be limited to (65535 / SLEEP_TICKS_PER_MINUTE) + // to avoid overflows or impossibly long timeouts + // (by default, the effective limit is 145, but it allows up to 255) + else if (manual_memory_timer_config_step == step) { + cfg.manual_memory_timer = value; + } + #endif + + #ifdef USE_RAMP_AFTER_MOON_CONFIG + // item 3: ramp up after hold-from-off for moon? + // 0 = yes, ramp after moon + // 1+ = no, stay at moon + else if (dont_ramp_after_moon_config_step == step) { + cfg.dont_ramp_after_moon = value; + } + #endif + + #ifdef USE_2C_STYLE_CONFIG + // item 4: Anduril 1 2C turbo, or Anduril 2 2C ceiling? + // 1 = Anduril 1, 2C turbo + // 2+ = Anduril 2, 2C ceiling + else if (ramp_2c_style_config_step == step) { + cfg.ramp_2c_style = value; + } + #endif + + #ifdef USE_SMOOTH_STEPS + else if (smooth_steps_style_config_step == step) { + cfg.smooth_steps_style = value; + } + #endif +} + +uint8_t ramp_extras_config_state(Event event, uint16_t arg) { + return config_state_base(event, arg, + ramp_extras_config_num_steps - 1, + ramp_extras_config_save); +} +#endif + +#ifdef USE_GLOBALS_CONFIG +void globals_config_save(uint8_t step, uint8_t value) { + if (0) {} + #if defined(USE_CHANNEL_MODE_ARGS) && defined(USE_STEPPED_TINT_RAMPING) + else if (step == tint_style_config_step) { cfg.tint_ramp_style = value; } + #endif + #ifdef USE_JUMP_START + else if (step == jump_start_config_step) { cfg.jump_start_level = value; } + #endif +} + +uint8_t globals_config_state(Event event, uint16_t arg) { + return config_state_base(event, arg, + globals_config_num_steps - 1, + globals_config_save); +} +#endif + +// find the ramp level closest to the target, +// using only the levels which are allowed in the current state +uint8_t nearest_level(int16_t target) { + // using int16_t here saves us a bunch of logic elsewhere, + // by allowing us to correct for numbers < 0 or > 255 in one central place + + // ensure all globals are correct + ramp_update_config(); + + // bounds check + uint8_t mode_min = ramp_floor; + uint8_t mode_max = ramp_ceil; + uint8_t num_steps = cfg.ramp_stepss[1 + #ifdef USE_SIMPLE_UI + + cfg.simple_ui_active + #endif + ]; + // special case for 1-step ramp... use halfway point between floor and ceiling + if (cfg.ramp_style && (1 == num_steps)) { + uint8_t mid = (mode_max + mode_min) >> 1; + return mid; + } + if (target < mode_min) return mode_min; + if (target > mode_max) return mode_max; + // the rest isn't relevant for smooth ramping + if (! cfg.ramp_style) return target; + + uint8_t ramp_range = mode_max - mode_min; + ramp_discrete_step_size = ramp_range / (num_steps-1); + uint8_t this_level = mode_min; + + for(uint8_t i=0; i>1)) + return this_level; + } + return this_level; +} + +// ensure ramp globals are correct +void ramp_update_config() { + uint8_t which = cfg.ramp_style; + #ifdef USE_SIMPLE_UI + if (cfg.simple_ui_active) { which = 2; } + #endif + + ramp_floor = cfg.ramp_floors[which]; + ramp_ceil = cfg.ramp_ceils[which]; +} + +#if defined(USE_THERMAL_REGULATION) || defined(USE_SMOOTH_STEPS) +void set_level_and_therm_target(uint8_t level) { + #ifdef USE_THERMAL_REGULATION + target_level = level; + #endif + #ifdef USE_SMOOTH_STEPS + // if adjusting by more than 1 ramp level, + // animate the step change (if smooth steps enabled) + uint8_t diff = (level > actual_level) + ? (level - actual_level) : (actual_level - level); + if (smooth_steps_in_progress + || (cfg.smooth_steps_style && (diff > 1))) + set_level_smooth(level, 4); + else + #endif + set_level(level); +} +#else +#define set_level_and_therm_target(level) set_level(level) +#endif + +void manual_memory_restore() { + memorized_level = cfg.manual_memory; + #if NUM_CHANNEL_MODES > 1 + channel_mode = cfg.channel_mode = cfg.manual_memory_channel_mode; + #endif + #ifdef USE_CHANNEL_MODE_ARGS + for (uint8_t i=0; i 1 + cfg.manual_memory_channel_mode = channel_mode; + #endif + #ifdef USE_CHANNEL_MODE_ARGS + for (uint8_t i=0; i= 3 + #ifndef BLINK_AT_RAMP_MIDDLE_1 + #define BLINK_AT_RAMP_MIDDLE_1 MAX_Nx7135 + #ifndef BLINK_AT_RAMP_MIDDLE_2 + #define BLINK_AT_RAMP_MIDDLE_2 MAX_1x7135 + #endif + #endif + #else + #ifndef BLINK_AT_RAMP_MIDDLE_1 + #define BLINK_AT_RAMP_MIDDLE_1 MAX_1x7135 + #endif + #endif +#endif + + +// ramping mode and its related config mode +uint8_t steady_state(Event event, uint16_t arg); + +#ifdef USE_RAMP_CONFIG +uint8_t ramp_config_state(Event event, uint16_t arg); +void ramp_config_save(uint8_t step, uint8_t value); +#ifdef USE_SIMPLE_UI +uint8_t simple_ui_config_state(Event event, uint16_t arg); +#endif +#endif + +#if defined(USE_MANUAL_MEMORY_TIMER) || defined(USE_RAMP_AFTER_MOON_CONFIG) || defined(USE_2C_STYLE_CONFIG) || defined(USE_AUTO_SUNSET) +#define USE_RAMP_EXTRAS_CONFIG +#endif +#ifdef USE_RAMP_EXTRAS_CONFIG +uint8_t ramp_extras_config_state(Event event, uint16_t arg); +#endif + +// calculate the nearest ramp level which would be valid at the moment +// (is a no-op for smooth ramp, but limits discrete ramp to only the +// correct levels for the user's config) +uint8_t nearest_level(int16_t target); + +// ensure ramp globals are correct +void ramp_update_config(); + +#if defined(USE_THERMAL_REGULATION) || defined(USE_SMOOTH_STEPS) +// brightness before thermal step-down +uint8_t target_level = 0; +void set_level_and_therm_target(uint8_t level); +#else +#define set_level_and_therm_target(level) set_level(level) +#endif + + +// brightness control +uint8_t memorized_level = DEFAULT_LEVEL; +#ifdef USE_MANUAL_MEMORY + void manual_memory_restore(); + void manual_memory_save(); + #ifndef DEFAULT_MANUAL_MEMORY + #define DEFAULT_MANUAL_MEMORY 0 + #endif + #ifdef USE_MANUAL_MEMORY_TIMER + #ifndef DEFAULT_MANUAL_MEMORY_TIMER + #define DEFAULT_MANUAL_MEMORY_TIMER 0 + #endif + #endif +#endif + +#ifndef DEFAULT_2C_STYLE_SIMPLE + #define DEFAULT_2C_STYLE_SIMPLE 0 +#endif + +#ifdef USE_2C_STYLE_CONFIG +#ifndef DEFAULT_2C_STYLE +#define DEFAULT_2C_STYLE 2 +#endif + +#ifdef USE_2C_MAX_TURBO +#error Cannot use USE_2C_MAX_TURBO and USE_2C_STYLE_CONFIG at the same time. +#endif +#endif + +#ifdef USE_RAMP_SPEED_CONFIG +#define ramp_speed (cfg.ramp_stepss[0]) +#endif +#ifdef USE_RAMP_AFTER_MOON_CONFIG +#ifndef DEFAULT_DONT_RAMP_AFTER_MOON +#define DEFAULT_DONT_RAMP_AFTER_MOON 0 +#endif +#endif +// current values, regardless of style +uint8_t ramp_floor = RAMP_SMOOTH_FLOOR; +uint8_t ramp_ceil = RAMP_SMOOTH_CEIL; + +uint8_t ramp_discrete_step_size; // don't set this + +#ifdef USE_SUNSET_TIMER +uint8_t sunset_timer_orig_level = 0; +void reset_sunset_timer(); +#endif + +#ifdef USE_RAMP_EXTRAS_CONFIG +typedef enum { + ramp_extras_cfg_zero = 0, + manual_memory_config_step, + #ifdef USE_MANUAL_MEMORY_TIMER + manual_memory_timer_config_step, + #endif + #ifdef USE_RAMP_AFTER_MOON_CONFIG + dont_ramp_after_moon_config_step, + #endif + #ifdef USE_2C_STYLE_CONFIG + ramp_2c_style_config_step, + #endif + #ifdef USE_SMOOTH_STEPS + smooth_steps_style_config_step, + #endif + ramp_extras_config_num_steps +} ramp_extras_config_steps_e; +#endif + +#ifdef USE_GLOBALS_CONFIG +typedef enum { + globals_cfg_zero = 0, + #if defined(USE_CHANNEL_MODE_ARGS) && defined(USE_STEPPED_TINT_RAMPING) + tint_style_config_step, + #endif + #ifdef USE_JUMP_START + jump_start_config_step, + #endif + globals_config_num_steps +} globals_config_steps_e; + +void globals_config_save(uint8_t step, uint8_t value); +uint8_t globals_config_state(Event event, uint16_t arg); +#endif + diff --git a/ui/anduril/smooth-steps.c b/ui/anduril/smooth-steps.c new file mode 100644 index 0000000..9631e41 --- /dev/null +++ b/ui/anduril/smooth-steps.c @@ -0,0 +1,47 @@ +// smooth-steps.c: Smooth step adjustments for Anduril. +// Copyright (C) 2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "smooth-steps.h" + +#ifdef USE_SMOOTH_STEPS + +// one iteration of main loop() +void smooth_steps_iter() { + if (actual_level == smooth_steps_target) { + set_level(smooth_steps_target); + smooth_steps_in_progress = 0; + // restore prev_level when animation ends + prev_level = smooth_steps_start; + } + else if (smooth_steps_target > actual_level) { + // power-linear(ish) ascent + // (jump by ~20% of remaining distance on each frame) + uint8_t diff = smooth_steps_target - actual_level; + uint8_t this = diff / smooth_steps_speed; + if (!this) this = 1; + set_level(actual_level + this); + nice_delay_ms(10); + } else { + // ramp-linear descent + // (jump by 1 on each frame, frame rate gives constant total time) + uint8_t diff = smooth_steps_start - smooth_steps_target; + uint16_t delay = 1 + (30 * smooth_steps_speed / diff); + set_level(actual_level - 1); + // TODO? if delay < one PWM cycle, this can look a little weird + nice_delay_ms(delay); + } +} + +void set_level_smooth(uint8_t level, uint8_t speed) { + smooth_steps_target = level; + smooth_steps_speed = speed; // higher = slower + smooth_steps_in_progress = 1; + // for setting prev_level after animation ends + smooth_steps_start = actual_level; +} + +#endif + diff --git a/ui/anduril/smooth-steps.h b/ui/anduril/smooth-steps.h new file mode 100644 index 0000000..a553af2 --- /dev/null +++ b/ui/anduril/smooth-steps.h @@ -0,0 +1,19 @@ +// smooth-steps.h: Smooth step adjustments for Anduril. +// Copyright (C) 2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#ifdef USE_SMOOTH_STEPS + +uint8_t smooth_steps_start; +uint8_t smooth_steps_target; +uint8_t smooth_steps_in_progress; +uint8_t smooth_steps_speed; + +void smooth_steps_iter(); + +void set_level_smooth(uint8_t level, uint8_t speed); + +#endif + diff --git a/ui/anduril/sos-mode.c b/ui/anduril/sos-mode.c new file mode 100644 index 0000000..2a4e97c --- /dev/null +++ b/ui/anduril/sos-mode.c @@ -0,0 +1,56 @@ +// sos-mode.c: SOS mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "sos-mode.h" + +#ifdef USE_SOS_MODE_IN_BLINKY_GROUP +uint8_t sos_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: next blinky mode + else if (event == EV_2clicks) { + #if defined(USE_BATTCHECK_MODE) + set_state(battcheck_state, 0); + #elif defined(USE_THERMAL_REGULATION) + set_state(tempcheck_state, 0); + #elif defined(USE_BEACON_MODE) + set_state(beacon_state, 0); + #endif + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} +#endif + +void sos_blink(uint8_t num, uint8_t dah) { + #define DIT_LENGTH 200 + for (; num > 0; num--) { + set_level(memorized_level); + nice_delay_ms(DIT_LENGTH); + if (dah) { // dah is 3X as long as a dit + nice_delay_ms(DIT_LENGTH*2); + } + set_level(0); + // one "off" dit between blinks + nice_delay_ms(DIT_LENGTH); + } + // three "off" dits (or one "dah") between letters + // (except for SOS, which is collectively treated as a single "letter") + //nice_delay_ms(DIT_LENGTH*2); +} + +inline void sos_mode_iter() { + // one iteration of main loop() + //nice_delay_ms(1000); + sos_blink(3, 0); // S + sos_blink(3, 1); // O + sos_blink(3, 0); // S + nice_delay_ms(2000); +} + diff --git a/ui/anduril/sos-mode.h b/ui/anduril/sos-mode.h new file mode 100644 index 0000000..5af61be --- /dev/null +++ b/ui/anduril/sos-mode.h @@ -0,0 +1,11 @@ +// sos-mode.h: SOS mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#ifdef USE_SOS_MODE_IN_BLINKY_GROUP +// automatic SOS emergency signal +uint8_t sos_state(Event event, uint16_t arg); +#endif + diff --git a/ui/anduril/strobe-modes-fsm.h b/ui/anduril/strobe-modes-fsm.h new file mode 100644 index 0000000..4d948ed --- /dev/null +++ b/ui/anduril/strobe-modes-fsm.h @@ -0,0 +1,55 @@ +// strobe-modes-fsm.h: FSM config for strobe modes in Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// enable the random number generator if we need it +#if defined(USE_LIGHTNING_MODE) || defined(USE_CANDLE_MODE) +#define USE_PSEUDO_RAND +#endif + +// party strobe uses really short pulses +#ifdef USE_PARTY_STROBE_MODE +#define USE_DELAY_ZERO +#endif + +// candle mode is basically a bunch of stacked random triangle waves +#if defined(USE_CANDLE_MODE) +#define USE_TRIANGLE_WAVE +#endif + +// the presence of strobe mode(s) affects how many eeprom bytes we need, +// so it's relevant for FSM configuration +#if defined(USE_CANDLE_MODE) || defined(USE_BIKE_FLASHER_MODE) || defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) || defined(USE_LIGHTNING_MODE) +#define USE_STROBE_STATE +#endif + +// internal numbering for strobe modes +#ifdef USE_STROBE_STATE +typedef enum { + #ifdef USE_PARTY_STROBE_MODE + party_strobe_e, + #endif + #ifdef USE_TACTICAL_STROBE_MODE + tactical_strobe_e, + #endif + #ifdef USE_POLICE_COLOR_STROBE_MODE + police_color_strobe_e, + #endif + #ifdef USE_LIGHTNING_MODE + lightning_storm_e, + #endif + #ifdef USE_CANDLE_MODE + candle_mode_e, + #endif + #ifdef USE_BIKE_FLASHER_MODE + bike_flasher_e, + #endif + strobe_mode_END +} strobe_mode_te; + +//const int NUM_STROBES = strobe_mode_END; +#define NUM_STROBES strobe_mode_END +#endif + diff --git a/ui/anduril/strobe-modes.c b/ui/anduril/strobe-modes.c new file mode 100644 index 0000000..ad17964 --- /dev/null +++ b/ui/anduril/strobe-modes.c @@ -0,0 +1,332 @@ +// strobe-modes.c: Strobe modes for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "strobe-modes.h" + +#ifdef USE_STROBE_STATE +uint8_t strobe_state(Event event, uint16_t arg) { + static int8_t ramp_direction = 1; + + // 'st' reduces ROM size slightly + strobe_mode_te st = current_strobe_type; + + #ifdef USE_MOMENTARY_MODE + momentary_mode = 1; // 0 = ramping, 1 = strobes + #endif + + #ifdef USE_CANDLE_MODE + // pass all events to candle mode, when it's active + // (the code is in its own pseudo-state to keep things cleaner) + if (st == candle_mode_e) { + candle_mode_state(event, arg); + } + #endif + + if (0) {} // placeholder + // init anything which needs to be initialized + else if (event == EV_enter_state) { + current_strobe_type = cfg.strobe_type; + ramp_direction = 1; + return EVENT_HANDLED; + } + // 1 click: off + else if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: rotate through strobe/flasher modes + else if (event == EV_2clicks) { + current_strobe_type = cfg.strobe_type = (st + 1) % NUM_STROBES; + save_config(); + return EVENT_HANDLED; + } + #if (NUM_CHANNEL_MODES > 1) && defined(USE_CHANNEL_PER_STROBE) + // 3 clicks: rotate through channel modes for the current strobe + else if (event == EV_3clicks) { + // TODO: maybe skip aux modes? + set_channel_mode((channel_mode + 1) % NUM_CHANNEL_MODES); + cfg.strobe_channels[st] = channel_mode; + save_config(); + return EVENT_HANDLED; + } + #endif + // 4 clicks: rotate backward through strobe/flasher modes + else if (event == EV_4clicks) { + current_strobe_type = cfg.strobe_type = (st - 1 + NUM_STROBES) % NUM_STROBES; + save_config(); + return EVENT_HANDLED; + } + // hold: change speed (go faster) + // or change brightness (brighter) + else if (event == EV_click1_hold) { + if (0) {} // placeholder + + // party / tactical strobe faster + #if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) + #ifdef USE_TACTICAL_STROBE_MODE + else if (st <= tactical_strobe_e) { + #else + else if (st == party_strobe_e) { + #endif + if ((arg & 1) == 0) { + uint8_t d = cfg.strobe_delays[st]; + d -= ramp_direction; + if (d < 8) d = 8; + else if (d > 254) d = 254; + cfg.strobe_delays[st] = d; + } + } + #endif + + // lightning has no adjustments + //else if (st == lightning_storm_e) {} + + // biking mode brighter + #ifdef USE_BIKE_FLASHER_MODE + else if (st == bike_flasher_e) { + cfg.bike_flasher_brightness += ramp_direction; + if (cfg.bike_flasher_brightness < 2) cfg.bike_flasher_brightness = 2; + else if (cfg.bike_flasher_brightness > MAX_BIKING_LEVEL) cfg.bike_flasher_brightness = MAX_BIKING_LEVEL; + set_level(cfg.bike_flasher_brightness); + } + #endif + + return EVENT_HANDLED; + } + // reverse ramp direction on hold release + // ... and save new strobe settings + else if (event == EV_click1_hold_release) { + ramp_direction = -ramp_direction; + save_config(); + return EVENT_HANDLED; + } + // click, hold: change speed (go slower) + // or change brightness (dimmer) + else if (event == EV_click2_hold) { + ramp_direction = 1; + + if (0) {} // placeholder + + // party / tactical strobe slower + #if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) + #ifdef USE_TACTICAL_STROBE_MODE + else if (st <= tactical_strobe_e) { + #else + else if (st == party_strobe_e) { + #endif + if ((arg & 1) == 0) { + if (cfg.strobe_delays[st] < 255) cfg.strobe_delays[st] ++; + } + } + #endif + + // lightning has no adjustments + //else if (st == lightning_storm_e) {} + + // biking mode dimmer + #ifdef USE_BIKE_FLASHER_MODE + else if (st == bike_flasher_e) { + if (cfg.bike_flasher_brightness > 2) + cfg.bike_flasher_brightness --; + set_level(cfg.bike_flasher_brightness); + } + #endif + + return EVENT_HANDLED; + } + // release hold: save new strobe settings + else if (event == EV_click2_hold_release) { + save_config(); + return EVENT_HANDLED; + } + #ifdef USE_MOMENTARY_MODE + // 5 clicks: go to momentary mode (momentary strobe) + else if (event == EV_5clicks) { + set_state(momentary_state, 0); + set_level(0); + return EVENT_HANDLED; + } + #endif + #if defined(USE_LIGHTNING_MODE) || defined(USE_CANDLE_MODE) + // clock tick: bump the random seed + else if (event == EV_tick) { + // un-reverse after 1 second + if (arg == AUTO_REVERSE_TIME) ramp_direction = 1; + + pseudo_rand_seed += arg; + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} + +// runs repeatedly in FSM loop() whenever UI is in strobe_state or momentary strobe +inline void strobe_state_iter() { + uint8_t st = current_strobe_type; // can't use switch() on an enum + + #if (NUM_CHANNEL_MODES > 1) && defined(USE_CHANNEL_PER_STROBE) + // remember channel mode for each strobe + channel_mode = cfg.strobe_channels[st]; + #endif + + switch(st) { + #if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) + #ifdef USE_PARTY_STROBE_MODE + case party_strobe_e: + #endif + #ifdef USE_TACTICAL_STROBE_MODE + case tactical_strobe_e: + #endif + party_tactical_strobe_mode_iter(st); + break; + #endif + + #ifdef USE_POLICE_COLOR_STROBE_MODE + case police_color_strobe_e: + police_color_strobe_iter(); + break; + #endif + + #ifdef USE_LIGHTNING_MODE + case lightning_storm_e: + lightning_storm_iter(); + break; + #endif + + #ifdef USE_BIKE_FLASHER_MODE + case bike_flasher_e: + bike_flasher_iter(); + break; + #endif + } +} +#endif // ifdef USE_STROBE_STATE + +#if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) +inline void party_tactical_strobe_mode_iter(uint8_t st) { + // one iteration of main loop() + uint8_t del = cfg.strobe_delays[st]; + // TODO: make tac strobe brightness configurable? + set_level(STROBE_BRIGHTNESS); + if (0) {} // placeholder + #ifdef USE_PARTY_STROBE_MODE + else if (st == party_strobe_e) { // party strobe + #ifdef PARTY_STROBE_ONTIME + nice_delay_ms(PARTY_STROBE_ONTIME); + #else + if (del < 42) delay_zero(); + else nice_delay_ms(1); + #endif + } + #endif + #ifdef USE_TACTICAL_STROBE_MODE + else { //tactical strobe + nice_delay_ms(del >> 1); + } + #endif + set_level(STROBE_OFF_LEVEL); + nice_delay_ms(del); // no return check necessary on final delay +} +#endif + +#ifdef USE_POLICE_COLOR_STROBE_MODE +inline void police_color_strobe_iter() { + // one iteration of main loop() + uint8_t del = 66; + // TODO: make police strobe brightness configurable + uint8_t bright = memorized_level; + //uint8_t channel = channel_mode; + + for (uint8_t i=0; i<10; i++) { + if (0 == i) set_channel_mode(POLICE_COLOR_STROBE_CH1); + else if (5 == i) set_channel_mode(POLICE_COLOR_STROBE_CH2); + set_level(bright); + nice_delay_ms(del >> 1); + set_level(STROBE_OFF_LEVEL); + nice_delay_ms(del); + } + + // restore the channel when done + //set_channel_mode(channel); + channel_mode = cfg.channel_mode; +} +#endif + +#ifdef USE_LIGHTNING_MODE +inline void lightning_storm_iter() { + // one iteration of main loop() + int16_t brightness; + uint16_t rand_time; + + // turn the emitter on at a random level, + // for a random amount of time between 1ms and 32ms + //rand_time = 1 << (pseudo_rand() % 7); + rand_time = pseudo_rand() & 63; + brightness = 1 << (pseudo_rand() % 7); // 1, 2, 4, 8, 16, 32, 64 + brightness += 1 << (pseudo_rand() % 5); // 2 to 80 now + brightness += pseudo_rand() % brightness; // 2 to 159 now (w/ low bias) + if (brightness > MAX_LEVEL) brightness = MAX_LEVEL; + set_level(brightness); + nice_delay_ms(rand_time); + + // decrease the brightness somewhat more gradually, like lightning + uint8_t stepdown = brightness >> 3; + if (stepdown < 1) stepdown = 1; + while(brightness > 1) { + nice_delay_ms(rand_time); + brightness -= stepdown; + if (brightness < 0) brightness = 0; + set_level(brightness); + /* + if ((brightness < MAX_LEVEL/2) && (! (pseudo_rand() & 15))) { + brightness <<= 1; + set_level(brightness); + } + */ + if (! (pseudo_rand() & 3)) { + nice_delay_ms(rand_time); + set_level(brightness>>1); + } + } + + // turn the emitter off, + // for a random amount of time between 1ms and 8192ms + // (with a low bias) + rand_time = 1 << (pseudo_rand() % 13); + rand_time += pseudo_rand() % rand_time; + set_level(0); + nice_delay_ms(rand_time); // no return check necessary on final delay +} +#endif + +#ifdef USE_BIKE_FLASHER_MODE +#ifndef BIKE_STROBE_ONTIME +#define BIKE_STROBE_ONTIME 0 +#endif +inline void bike_flasher_iter() { + // one iteration of main loop() + uint8_t burst = cfg.bike_flasher_brightness << 1; + if (burst > MAX_LEVEL) burst = MAX_LEVEL; + for(uint8_t i=0; i<4; i++) { + set_level(burst); + nice_delay_ms(5 + BIKE_STROBE_ONTIME); + set_level(cfg.bike_flasher_brightness); + nice_delay_ms(65); + } + nice_delay_ms(720); // no return check necessary on final delay + set_level(0); +} +#endif + +#ifdef USE_CANDLE_MODE +#include "candle-mode.c" +#endif + + +#ifdef USE_BORING_STROBE_STATE +#include "ff-strobe-modes.c" +#endif + diff --git a/ui/anduril/strobe-modes.h b/ui/anduril/strobe-modes.h new file mode 100644 index 0000000..7dc1df4 --- /dev/null +++ b/ui/anduril/strobe-modes.h @@ -0,0 +1,71 @@ +// strobe-modes.h: Strobe modes for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#ifdef USE_STROBE_STATE + +strobe_mode_te current_strobe_type; + +// which strobe mode is active? +#ifdef USE_CANDLE_MODE + #define DEFAULT_STROBE candle_mode_e +#else + #define DEFAULT_STROBE 0 +#endif + +#endif // ifdef USE_STROBE_STATE + + +// full FET strobe can be a bit much... use max regulated level instead, +// if there's a bright enough regulated level +#ifndef STROBE_BRIGHTNESS +#ifdef MAX_Nx7135 +#define STROBE_BRIGHTNESS MAX_Nx7135 +#else +#define STROBE_BRIGHTNESS MAX_LEVEL +#endif +#endif + +// some drivers need to keep the regulator chip on between pulses, +// so set this to 1 to keep the light on at moon mode between pulses, +// and thus keep the regulator powered up for the next flash +#ifndef STROBE_OFF_LEVEL +#define STROBE_OFF_LEVEL 0 +#endif + +// party and tactical strobes +#ifdef USE_STROBE_STATE +uint8_t strobe_state(Event event, uint16_t arg); +inline void strobe_state_iter(); +#endif + +#if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) +inline void party_tactical_strobe_mode_iter(uint8_t st); +#endif + +#ifdef USE_POLICE_COLOR_STROBE_MODE +inline void police_color_strobe_iter(); +#endif + +#ifdef USE_LIGHTNING_MODE +inline void lightning_storm_iter(); +#endif + +// bike mode config options +#ifdef USE_BIKE_FLASHER_MODE +#define MAX_BIKING_LEVEL 120 // should be 127 or less +inline void bike_flasher_iter(); +#endif + +#ifdef USE_CANDLE_MODE +#include "candle-mode.h" +#endif + + +#if defined(USE_POLICE_STROBE_MODE) || defined(USE_SOS_MODE_IN_FF_GROUP) +#define USE_BORING_STROBE_STATE +#include "ff-strobe-modes.h" +#endif + diff --git a/ui/anduril/sunset-timer.c b/ui/anduril/sunset-timer.c new file mode 100644 index 0000000..e4fc512 --- /dev/null +++ b/ui/anduril/sunset-timer.c @@ -0,0 +1,60 @@ +// sunset-timer.c: Sunset / candle auto-shutoff functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "sunset-timer.h" + +uint8_t sunset_timer_state(Event event, uint16_t arg) { + + #if defined(USE_SIMPLE_UI) && !defined(USE_EXTENDED_SIMPLE_UI) + // No timer functions in Simple UI + if (cfg.simple_ui_active) return EVENT_NOT_HANDLED; + #endif + + // reset on start + if (event == EV_enter_state) { + sunset_timer = 0; + sunset_ticks = 0; + return EVENT_HANDLED; + } + // hold: maybe "bump" the timer if it's active and almost expired + else if (event == EV_hold) { + // ramping up should "bump" the timer to extend the deadline a bit + if ((sunset_timer > 0) && (sunset_timer < 4)) { + sunset_timer = 3; // 3 minutes + sunset_timer_peak = 3; + sunset_ticks = 0; // re-start current "minute" + } + } + // 5H: add 5m to timer, per second, until released + else if (event == EV_click5_hold) { + if (0 == (arg % TICKS_PER_SECOND)) { + if (sunset_timer < (255 - SUNSET_TIMER_UNIT)) { + // add a few minutes to the timer + sunset_timer += SUNSET_TIMER_UNIT; + sunset_timer_peak = sunset_timer; // reset ceiling + sunset_ticks = 0; // reset phase + // let the user know something happened + blink_once(); + } + } + return EVENT_HANDLED; + } + // tick: count down until time expires + else if (event == EV_tick) { + // time passed + sunset_ticks ++; + // did we reach a minute mark? + if (sunset_ticks >= TICKS_PER_MINUTE) { + sunset_ticks = 0; + if (sunset_timer > 0) { + sunset_timer --; + } + } + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + diff --git a/ui/anduril/sunset-timer.h b/ui/anduril/sunset-timer.h new file mode 100644 index 0000000..963804e --- /dev/null +++ b/ui/anduril/sunset-timer.h @@ -0,0 +1,17 @@ +// sunset-timer.h: Sunset / candle auto-shutoff functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// how many minutes to add each time the user "bumps" the timer? +#define SUNSET_TIMER_UNIT 5 + +#define TICKS_PER_MINUTE (TICKS_PER_SECOND*60) + +// automatic shutoff timer +uint8_t sunset_timer = 0; // minutes remaining in countdown +uint8_t sunset_timer_peak = 0; // total minutes in countdown +uint16_t sunset_ticks = 0; // counts from 0 to TICKS_PER_MINUTE, then repeats +uint8_t sunset_timer_state(Event event, uint16_t arg); + diff --git a/ui/anduril/tactical-mode.c b/ui/anduril/tactical-mode.c new file mode 100644 index 0000000..0035496 --- /dev/null +++ b/ui/anduril/tactical-mode.c @@ -0,0 +1,109 @@ +// tactical-mode.c: Tactical (ish) mode for Anduril. +// Copyright (C) 2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "tactical-mode.h" + + +uint8_t tactical_state(Event event, uint16_t arg) { + // momentary(ish) tactical mode + uint8_t mem_lvl = memorized_level; // save this to restore it later + uint8_t ret = EVENT_NOT_HANDLED; + + // button is being held + if ((event & (B_CLICK | B_PRESS)) == (B_CLICK | B_PRESS)) { + // 1H: 1st level + // 2H: 2nd level + // 3H: 3rd level + // 4+: nothing + momentary_active = 0; + ret = EVENT_HANDLED; + uint8_t click = event & 0x0f; // click number + if (click <= 3) { + momentary_active = 1; + uint8_t lvl; + lvl = cfg.tactical_levels[click-1]; + if ((1 <= lvl) && (lvl <= RAMP_SIZE)) { // steady output + memorized_level = lvl; + momentary_mode = 0; + #if NUM_CHANNEL_MODES > 1 + // use ramp mode's channel + channel_mode = cfg.channel_mode; + #endif + } else { // momentary strobe mode + momentary_mode = 1; + if (lvl > RAMP_SIZE) { + current_strobe_type = (lvl - RAMP_SIZE - 1) % strobe_mode_END; + } + } + } + } + // button was released + else if ((event & (B_CLICK | B_PRESS)) == (B_CLICK)) { + momentary_active = 0; + set_level(0); + interrupt_nice_delays(); // stop animations in progress + } + + // delegate to momentary mode while button is pressed + if (momentary_active) { + momentary_state(event, arg); + } + + memorized_level = mem_lvl; // restore temporarily overridden mem level + + // copy lockout mode's aux LED and sleep behaviors + if (event == EV_enter_state) { + lockout_state(event, arg); + } + else if (event == EV_tick) { + if (! momentary_active) { + return lockout_state(event, arg); + } + return EVENT_HANDLED; + } + else if (event == EV_sleep_tick) { + return lockout_state(event, arg); + } + + // 6 clicks: exit and turn off + else if (event == EV_6clicks) { + blink_once(); + set_state(off_state, 0); + return EVENT_HANDLED; + } + + ////////// Every action below here is blocked in the simple UI ////////// + // (unnecessary since this entire mode is blocked in simple UI) + /* + #ifdef USE_SIMPLE_UI + if (cfg.simple_ui_active) { + return EVENT_NOT_HANDLED; + } + #endif + */ + + // 7H: configure tactical mode + else if (event == EV_click7_hold) { + push_state(tactical_config_state, 0); + return EVENT_HANDLED; + } + + return ret; +} + +void tactical_config_save(uint8_t step, uint8_t value) { + // update tac mode values + // 3 values + // each value is 1 to 150, or other: + // - 1..150 is a ramp level + // - other means "strobe mode" + cfg.tactical_levels[step - 1] = value; +} + +uint8_t tactical_config_state(Event event, uint16_t arg) { + return config_state_base(event, arg, 3, tactical_config_save); +} + diff --git a/ui/anduril/tactical-mode.h b/ui/anduril/tactical-mode.h new file mode 100644 index 0000000..528a796 --- /dev/null +++ b/ui/anduril/tactical-mode.h @@ -0,0 +1,22 @@ +// tactical-mode.h: Tactical mode for Anduril. +// Copyright (C) 2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#ifndef TACTICAL_LEVELS + // high, low, tactical strobe + // only do color strobe here if it's main LEDs, not aux LEDs + #if defined(USE_POLICE_COLOR_STROBE_MODE) && !defined(POLICE_STROBE_USES_AUX) + // 2-color police style strobe + #define TACTICAL_LEVELS 120,30,(RAMP_SIZE+3) + #else + // regular tactical strobe (1 color) + #define TACTICAL_LEVELS 120,30,(RAMP_SIZE+2) + #endif +#endif + +// tactical(ish) mode +uint8_t tactical_state(Event event, uint16_t arg); +uint8_t tactical_config_state(Event event, uint16_t arg); + diff --git a/ui/anduril/tempcheck-mode.c b/ui/anduril/tempcheck-mode.c new file mode 100644 index 0000000..5d160bd --- /dev/null +++ b/ui/anduril/tempcheck-mode.c @@ -0,0 +1,56 @@ +// tempcheck-mode.c: Temperature check mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "tempcheck-mode.h" + +uint8_t tempcheck_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: next blinky mode + else if (event == EV_2clicks) { + #if defined(USE_BEACON_MODE) + set_state(beacon_state, 0); + #elif defined(USE_SOS_MODE) && defined(USE_SOS_MODE_IN_BLINKY_GROUP) + set_state(sos_state, 0); + #elif defined(USE_BATTCHECK) + set_state(battcheck_state, 0); + #endif + return EVENT_HANDLED; + } + // 7H: thermal config mode + else if (event == EV_click7_hold) { + push_state(thermal_config_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + +void thermal_config_save(uint8_t step, uint8_t value) { + if (value) { + // item 1: calibrate room temperature + if (step == 1) { + int8_t rawtemp = temperature - cfg.therm_cal_offset; + cfg.therm_cal_offset = value - rawtemp; + adc_reset = 2; // invalidate all recent temperature data + } + + // item 2: set maximum heat limit + else { + cfg.therm_ceil = 30 + value - 1; + } + } + + if (cfg.therm_ceil > MAX_THERM_CEIL) cfg.therm_ceil = MAX_THERM_CEIL; +} + +uint8_t thermal_config_state(Event event, uint16_t arg) { + return config_state_base(event, arg, + 2, thermal_config_save); +} + diff --git a/ui/anduril/tempcheck-mode.h b/ui/anduril/tempcheck-mode.h new file mode 100644 index 0000000..15dd03e --- /dev/null +++ b/ui/anduril/tempcheck-mode.h @@ -0,0 +1,12 @@ +// tempcheck-mode.h: Temperature check mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#define USE_BLINK_NUM // FIXME: this only matters in an earlier header + +uint8_t tempcheck_state(Event event, uint16_t arg); +uint8_t thermal_config_state(Event event, uint16_t arg); +void thermal_config_save(uint8_t step, uint8_t value); + diff --git a/ui/anduril/tint-ramping.c b/ui/anduril/tint-ramping.c new file mode 100644 index 0000000..9418113 --- /dev/null +++ b/ui/anduril/tint-ramping.c @@ -0,0 +1,86 @@ +// tint-ramping.c: Tint ramping functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "tint-ramping.h" + +uint8_t tint_ramping_state(Event event, uint16_t arg) { + static int8_t tint_ramp_direction = 1; + static uint8_t prev_tint = 0; + // don't activate auto-tint modes unless the user hits the edge + // and keeps pressing for a while + static uint8_t past_edge_counter = 0; + // bugfix: click-click-hold from off to strobes would invoke tint ramping + // in addition to changing state... so ignore any tint-ramp events which + // don't look like they were meant to be here + static uint8_t active = 0; + + // click, click, hold: change the tint + if (event == EV_click3_hold) { + ///// tint-toggle mode + // toggle once on first frame; ignore other frames + if (tint_style) { + // only respond on first frame + if (arg) return EVENT_NOT_HANDLED; + + // force tint to be 1 or 254 + if (tint != 254) { tint = 1; } + // invert between 1 and 254 + tint = tint ^ 0xFF; + set_level(actual_level); + return EVENT_HANDLED; + } + + ///// smooth tint-ramp mode + // reset at beginning of movement + if (! arg) { + active = 1; // first frame means this is for us + past_edge_counter = 0; // doesn't start until user hits the edge + } + // ignore event if we weren't the ones who handled the first frame + if (! active) return EVENT_HANDLED; + + // change normal tints + if ((tint_ramp_direction > 0) && (tint < 254)) { + tint += 1; + } + else if ((tint_ramp_direction < 0) && (tint > 1)) { + tint -= 1; + } + // if the user kept pressing long enough, go the final step + if (past_edge_counter == 64) { + past_edge_counter ++; + tint ^= 1; // 0 -> 1, 254 -> 255 + blip(); + } + // if tint change stalled, let user know we hit the edge + else if (prev_tint == tint) { + if (past_edge_counter == 0) blip(); + // count up but don't wrap back to zero + if (past_edge_counter < 255) past_edge_counter ++; + } + prev_tint = tint; + set_level(actual_level); + return EVENT_HANDLED; + } + + // click, click, hold, release: reverse direction for next ramp + else if (event == EV_click3_hold_release) { + active = 0; // ignore next hold if it wasn't meant for us + // reverse + tint_ramp_direction = -tint_ramp_direction; + if (tint <= 1) tint_ramp_direction = 1; + else if (tint >= 254) tint_ramp_direction = -1; + // remember tint after battery change + save_config(); + // bug?: for some reason, brightness can seemingly change + // from 1/150 to 2/150 without this next line... not sure why + set_level(actual_level); + return EVENT_HANDLED; + } + + return EVENT_NOT_HANDLED; +} + diff --git a/ui/anduril/tint-ramping.h b/ui/anduril/tint-ramping.h new file mode 100644 index 0000000..19b8dde --- /dev/null +++ b/ui/anduril/tint-ramping.h @@ -0,0 +1,21 @@ +// tint-ramping.h: Tint ramping functions for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// 0: smooth tint ramp +// 1: toggle tint only between two extremes +#ifdef TINT_RAMP_TOGGLE_ONLY +uint8_t tint_style = 1; +#else +uint8_t tint_style = 0; +#endif + +#ifdef USE_MANUAL_MEMORY +uint8_t manual_memory_tint; +#endif + +// not actually a mode, more of a fallback under other modes +uint8_t tint_ramping_state(Event event, uint16_t arg); + diff --git a/ui/anduril/version-check-mode.c b/ui/anduril/version-check-mode.c new file mode 100644 index 0000000..a47706f --- /dev/null +++ b/ui/anduril/version-check-mode.c @@ -0,0 +1,31 @@ +// version-check-mode.c: Version check mode for Anduril. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "version-check-mode.h" + +// empty state; logic is handled in FSM loop() instead +uint8_t version_check_state(Event event, uint16_t arg) { + return EVENT_NOT_HANDLED; +} + +// this happens in FSM loop() +inline void version_check_iter() { + for (uint8_t i=0; i. + */ + +#include "hwdef-Emisar_D4.h" +#define USE_LVP +#define USE_THERMAL_REGULATION +#define USE_DELAY_MS +#include "spaghetti-monster.h" + +// FSM states +uint8_t off_state(Event event, uint16_t arg); +uint8_t steady_state(Event event, uint16_t arg); +uint8_t lockout_state(Event event, uint16_t arg); + +// brightness control +uint8_t memorized_level = 1; +uint8_t actual_level = 0; +#ifdef USE_THERMAL_REGULATION +uint8_t target_level = 0; +#endif + +// moon + ../../bin/level_calc.py 2 6 7135 18 10 150 FET 1 10 1500 +uint8_t pwm1_levels[] = { 3, 18, 110, 255, 255, 255, 0, }; +uint8_t pwm2_levels[] = { 0, 0, 0, 9, 58, 138, 255, }; +#define MAX_LEVEL (sizeof(pwm1_levels)-1) + +// set LED brightness +void set_level(uint8_t lvl) { + actual_level = lvl; + PWM1_LVL = pwm1_levels[lvl]; + PWM2_LVL = pwm2_levels[lvl]; +} + +uint8_t off_state(Event event, uint16_t arg) { + // turn emitter off when entering state + if (event == EV_enter_state) { + go_to_standby = 1; // sleep while off (lower power use) + return EVENT_HANDLED; + } + // hold (initially): go to lowest level, but allow abort for regular click + else if (event == EV_click1_press) { + set_level(0); + return EVENT_HANDLED; + } + // hold (longer): go to lowest level + else if (event == EV_click1_hold) { + set_state(steady_state, 0); + return EVENT_HANDLED; + } + // 1 click (before timeout): go to memorized level, but allow abort for double click + else if (event == EV_click1_release) { + set_level(memorized_level); + return EVENT_HANDLED; + } + // 1 click: regular mode + else if (event == EV_1click) { + set_state(steady_state, memorized_level); + return EVENT_HANDLED; + } + // 2 clicks: highest mode + else if (event == EV_2clicks) { + set_state(steady_state, MAX_LEVEL); + return EVENT_HANDLED; + } + // 4 clicks: soft lockout + else if (event == EV_4clicks) { + set_state(lockout_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + +uint8_t steady_state(Event event, uint16_t arg) { + // turn LED on when we first enter the mode + if (event == EV_enter_state) { + // remember this level, unless it's moon or turbo + if ((arg > 0) && (arg < MAX_LEVEL)) memorized_level = arg; + // use the requested level even if not memorized + #ifdef USE_THERMAL_REGULATION + target_level = arg; + #endif + set_level(arg); + return EVENT_HANDLED; + } + // 1 click: off + else if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: go to/from highest level + else if (event == EV_2clicks) { + if (actual_level < MAX_LEVEL) { // go to turbo + memorized_level = actual_level; // in case we're on moon + #ifdef USE_THERMAL_REGULATION + target_level = MAX_LEVEL; + #endif + set_level(MAX_LEVEL); + } + else { // return from turbo + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + set_level(memorized_level); + } + return EVENT_HANDLED; + } + // hold: change brightness + else if (event == EV_click1_hold) { + if ((arg % HOLD_TIMEOUT) == 0) { + memorized_level = (actual_level+1) % (MAX_LEVEL+1); + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + set_level(memorized_level); + } + return EVENT_HANDLED; + } + #ifdef USE_THERMAL_REGULATION + // overheating: drop by 1 level + else if (event == EV_temperature_high) { + if (actual_level > 1) { set_level(actual_level - 1); } + return EVENT_HANDLED; + } + // underheating: increase by 1 level if we're lower than the target + else if (event == EV_temperature_low) { + if (actual_level < target_level) { set_level(actual_level + 1); } + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} + +uint8_t lockout_state(Event event, uint16_t arg) { + // stay asleep while locked + if (event == EV_tick) { + PWM1_LVL = 0; PWM2_LVL = 0; // make sure emitters are off + // sleep 1 second after user stops pressing buttons + if (arg > TICKS_PER_SECOND) { go_to_standby = 1; } + return EVENT_HANDLED; + } + // 4 clicks: exit, and turn on at "low" level + else if (event == EV_4clicks) { + set_state(steady_state, 1); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + +void low_voltage() { + // step down by one level or turn off + if (actual_level > 0) { + set_level(actual_level - 1); + #ifdef USE_THERMAL_REGULATION + target_level = actual_level; // don't let low temperature override LVP + #endif + } + else { + set_state(off_state, 0); + } +} + +void setup() { + // blink when power is connected + set_level(MAX_LEVEL/2); + delay_ms(10); + set_level(0); + + push_state(off_state, 0); +} + +void loop() { +} diff --git a/ui/baton/baton.txt b/ui/baton/baton.txt new file mode 100644 index 0000000..2f0c22f --- /dev/null +++ b/ui/baton/baton.txt @@ -0,0 +1,21 @@ +This is a very simple clone of the Olight Baton interface. It is not +exact, but it has the basics. Mostly, it exists for the purposes of +demonstrating how to create interfaces in FSM. + +While off: + + - 1 click: Turn on (at memorized level). + - Hold: Turn on (at moon level). + - 2 clicks: Turn on (at highest level). + - 4 clicks: Soft lockout mode. + +While on: + + - 1 click: Turn off. + - Hold: Change the brightness. Goes up in steps, then wraps around. + - 2 clicks: Go to/from highest level. + +While locked: + + - 4 clicks: Exit lockout mode. + diff --git a/ui/darkhorse/darkhorse.c b/ui/darkhorse/darkhorse.c new file mode 100644 index 0000000..aa37b92 --- /dev/null +++ b/ui/darkhorse/darkhorse.c @@ -0,0 +1,367 @@ +/* + * DarkHorse: Improved ZebraLight clone UI for 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 "hwdef-Emisar_D4.h" +#define USE_LVP +#define USE_THERMAL_REGULATION +#define DEFAULT_THERM_CEIL 45 +#define USE_RAMPING +#define RAMP_LENGTH 150 +#define USE_BATTCHECK +#define BATTCHECK_4bars +#define DONT_DELAY_AFTER_BATTCHECK +#define USE_EEPROM +#define EEPROM_BYTES 5 +#include "spaghetti-monster.h" + +// FSM states +uint8_t off_state(Event event, uint16_t arg); +uint8_t low_mode_state(Event event, uint16_t arg); +uint8_t med_mode_state(Event event, uint16_t arg); +uint8_t hi_mode_state(Event event, uint16_t arg); +uint8_t strobe_beacon_state(Event event, uint16_t arg); +#ifdef USE_BATTCHECK +uint8_t battcheck_state(Event event, uint16_t arg); +#endif +// Not a FSM state, just handles stuff common to all low/med/hi states +uint8_t any_mode_state(Event event, uint16_t arg, uint8_t *primary, uint8_t *secondary, uint8_t *modes); + +void load_config(); +void save_config(); + +// toggle between L1/L2, M1/M2, H1/H2 +uint8_t L1 = 1; +uint8_t M1 = 1; +uint8_t H1 = 1; +// brightness for L2, M2, H2 (valid range 1 to 3 inclusive) +uint8_t L2 = 1; +uint8_t M2 = 1; +uint8_t H2 = 1; +// mode groups, ish +uint8_t low_modes[] = {12, 3, 5, 9}; // 3.3 lm, 2.0 lm, 0.8 lm, 0.3 lm +uint8_t med_modes[] = {56, 21, 29, 37}; // 101 lm, 35 lm, 20 lm, 10 lm +uint8_t hi_modes[] = {MAX_LEVEL, 81, 96, 113}; // 1500 lm, 678 lm, 430 lm, 270 lm +// strobe/beacon modes: +// 0: 0.2 Hz beacon at L1 +// 1: 0.2 Hz beacon at H1 +// 2: 4 Hz strobe at H1 +// 3: 19 Hz strobe at H1 +uint8_t strobe_beacon_mode = 0; + +#ifdef USE_THERMAL_REGULATION +// brightness before thermal step-down +uint8_t target_level = 0; +#endif + +void set_any_mode(uint8_t primary, uint8_t secondary, uint8_t *modes) { + // primary (H1/M1/L1) + if (primary) { + set_level(modes[0]); + } + // secondary (H2/M2/L2) + else { + set_level(modes[secondary]); + } + #ifdef USE_THERMAL_REGULATION + target_level = actual_level; + #endif +} + +inline void set_low_mode() { set_any_mode(L1, L2, low_modes); } +inline void set_med_mode() { set_any_mode(M1, M2, med_modes); } +inline void set_hi_mode() { set_any_mode(H1, H2, hi_modes); } + + +uint8_t off_state(Event event, uint16_t arg) { + // turn emitter off when entering state + if (event == EV_enter_state) { + set_level(0); + // sleep while off (lower power use) + go_to_standby = 1; + return EVENT_HANDLED; + } + // hold (initially): go to lowest level, but allow abort for regular click + else if (event == EV_click1_press) { + set_low_mode(); + return EVENT_HANDLED; + } + // 1 click (before timeout): go to high level, but allow abort for double click + else if (event == EV_click1_release) { + set_hi_mode(); + return EVENT_HANDLED; + } + // 1 click: high mode + else if (event == EV_1click) { + set_state(hi_mode_state, 0); + return EVENT_HANDLED; + } + // click, press (initially): go to medium mode, but allow abort + else if (event == EV_click2_press) { + set_med_mode(); + return EVENT_HANDLED; + } + // 2 clicks: medium mode + else if (event == EV_2clicks) { + set_state(med_mode_state, 0); + return EVENT_HANDLED; + } + // click, click, press (initially): light off, prep for blinkies + else if (event == EV_click3_press) { + set_level(0); + return EVENT_HANDLED; + } + // 3 clicks: strobe mode + else if (event == EV_3clicks) { + set_state(strobe_beacon_state, 0); + return EVENT_HANDLED; + } + #ifdef USE_BATTCHECK + // 4 clicks: battcheck mode + else if (event == EV_4clicks) { + set_state(battcheck_state, 0); + return EVENT_HANDLED; + } + #endif + // hold: go to low mode, but allow ramping up + else if (event == EV_click1_hold) { + // don't start ramping immediately; + // give the user time to release at low mode + if (arg >= HOLD_TIMEOUT) + set_state(low_mode_state, 0); + return EVENT_HANDLED; + } + // hold, release quickly: go to low mode + else if (event == EV_click1_hold_release) { + set_state(low_mode_state, 0); + return EVENT_HANDLED; + } + /* TODO: implement + // click-release-hold: discrete ramp through all levels + else if (event == EV_click2_hold) { + set_state(steady_state, MAX_LEVEL); + return EVENT_HANDLED; + } + */ + return EVENT_NOT_HANDLED; +} + + +uint8_t any_mode_state(Event event, uint16_t arg, uint8_t *primary, uint8_t *secondary, uint8_t *modes) { + // turn on LED when entering the mode + if (event == EV_enter_state) { + set_any_mode(*primary, *secondary, modes); + return EVENT_HANDLED; + } + // 1 click: off + else if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // hold: change brightness (low, med, hi, always starting at low) + else if (event == EV_click1_hold) { + uint8_t which = arg % (HOLD_TIMEOUT * 3) / HOLD_TIMEOUT; + switch(which) { + case 0: + set_state(low_mode_state, 0); + break; + case 1: + set_state(med_mode_state, 0); + break; + case 2: + set_state(hi_mode_state, 0); + break; + } + return EVENT_HANDLED; + } + // 2 clicks: toggle primary/secondary level + else if (event == EV_2clicks) { + *primary ^= 1; + set_any_mode(*primary, *secondary, modes); + save_config(); + return EVENT_HANDLED; + } + // click-release-hold: change secondary level + else if (event == EV_click2_hold) { + if (arg % HOLD_TIMEOUT == 0) { + *secondary = (*secondary + 1) & 3; + if (! *secondary) *secondary = 1; + *primary = 0; + set_any_mode(*primary, *secondary, modes); + } + return EVENT_HANDLED; + } + // click, hold, release: save secondary level + else if (event == EV_click2_hold_release) { + save_config(); + } + #ifdef USE_THERMAL_REGULATION + // TODO: test this on a real light + // overheating: drop by an amount proportional to how far we are above the ceiling + else if (event == EV_temperature_high) { + if (actual_level > MAX_LEVEL/4) { + uint8_t stepdown = actual_level - arg; + if (stepdown < MAX_LEVEL/4) stepdown = MAX_LEVEL/4; + set_level(stepdown); + } + return EVENT_HANDLED; + } + // underheating: increase slowly if we're lower than the target + // (proportional to how low we are) + else if (event == EV_temperature_low) { + if (actual_level < target_level) { + uint8_t stepup = actual_level + (arg>>1); + if (stepup > target_level) stepup = target_level; + set_level(stepup); + } + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} + +uint8_t low_mode_state(Event event, uint16_t arg) { + return any_mode_state(event, arg, &L1, &L2, low_modes); +} + +uint8_t med_mode_state(Event event, uint16_t arg) { + return any_mode_state(event, arg, &M1, &M2, med_modes); +} + +uint8_t hi_mode_state(Event event, uint16_t arg) { + return any_mode_state(event, arg, &H1, &H2, hi_modes); +} + + +#ifdef USE_BATTCHECK +uint8_t battcheck_state(Event event, uint16_t arg) { + return EVENT_NOT_HANDLED; +} +#endif + + +uint8_t strobe_beacon_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 1 click (initially): cancel current blink + // FIXME: this is no longer necessary; FSM does this automatically now + if (event == EV_click1_release) { + interrupt_nice_delays(); + return EVENT_HANDLED; + } + // 2 clicks: rotate through blinky modes + else if (event == EV_2clicks) { + strobe_beacon_mode = (strobe_beacon_mode + 1) & 3; + save_config(); + interrupt_nice_delays(); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + + +void low_voltage() { + if (current_state == hi_mode_state) { + set_state(med_mode_state, 0); + } + else if (current_state == med_mode_state) { + set_state(low_mode_state, 0); + } + else if (current_state == low_mode_state) { + set_state(off_state, 0); + } + // "step down" from blinkies to low + else if (current_state == strobe_beacon_state) { + set_state(low_mode_state, 0); + } +} + +void strobe(uint8_t level, uint16_t ontime, uint16_t offtime) { + set_level(level); + if (! nice_delay_ms(ontime)) return; + set_level(0); + nice_delay_ms(offtime); +} + +void load_config() { + if (load_eeprom()) { + H1 = !(!(eeprom[0] & 0b00000100)); + M1 = !(!(eeprom[0] & 0b00000010)); + L1 = !(!(eeprom[0] & 0b00000001)); + H2 = eeprom[1]; + M2 = eeprom[2]; + L2 = eeprom[3]; + strobe_beacon_mode = eeprom[4]; + } +} + +void save_config() { + eeprom[0] = (H1<<2) | (M1<<1) | (L1); + eeprom[1] = H2; + eeprom[2] = M2; + eeprom[3] = L2; + eeprom[4] = strobe_beacon_mode; + + save_eeprom(); +} + +void setup() { + set_level(RAMP_SIZE/8); + delay_4ms(3); + set_level(0); + + load_config(); + + push_state(off_state, 0); +} + +void loop() { + if (current_state == strobe_beacon_state) { + switch(strobe_beacon_mode) { + // 0.2 Hz beacon at L1 + case 0: + strobe(low_modes[0], 500, 4500); + break; + // 0.2 Hz beacon at H1 + case 1: + strobe(hi_modes[0], 500, 4500); + break; + // 4 Hz tactical strobe at H1 + case 2: + strobe(hi_modes[0], 83, 167); + break; + // 19 Hz tactical strobe at H1 + case 3: + strobe(hi_modes[0], 17, 35); + break; + } + } + + #ifdef USE_BATTCHECK + else if (current_state == battcheck_state) { + nice_delay_ms(500); // wait a moment to measure voltage + battcheck(); + set_state(off_state, 0); + } + #endif +} + + diff --git a/ui/fireflies-ui/Makefile b/ui/fireflies-ui/Makefile new file mode 100644 index 0000000..0b59898 --- /dev/null +++ b/ui/fireflies-ui/Makefile @@ -0,0 +1,7 @@ +all: + ./build-all.sh + +clean: + rm -f *.hex cfg-ff-[pr]*.h *~ *.elf *.o + +.phony: clean diff --git a/ui/fireflies-ui/build-all.sh b/ui/fireflies-ui/build-all.sh new file mode 100755 index 0000000..81ebd97 --- /dev/null +++ b/ui/fireflies-ui/build-all.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +cp -av --no-clobber ../anduril/cfg-ff*.h . + +UI=fireflies-ui + +for TARGET in cfg-*.h ; do + NAME=$(echo "$TARGET" | perl -ne '/cfg-(.*).h/ && print "$1\n";') + echo "===== $NAME =====" + echo ../../../bin/build.sh 85 "$UI" "-DCONFIGFILE=${TARGET}" + ../../../bin/build.sh 85 "$UI" "-DCONFIGFILE=${TARGET}" + mv -f "$UI".hex "$UI".$NAME.hex +done diff --git a/ui/fireflies-ui/cfg-ff-e01.h b/ui/fireflies-ui/cfg-ff-e01.h new file mode 100644 index 0000000..42c23b2 --- /dev/null +++ b/ui/fireflies-ui/cfg-ff-e01.h @@ -0,0 +1,44 @@ +// Fireflies EDC thrower config options for Fireflies UI +// (uses PL47 driver) +#include "hwdef-FF_PL47.h" + +// disable indicator LED; it's hardwired +#ifdef USE_INDICATOR_LED +#undef USE_INDICATOR_LED +#endif + +// don't do this +#undef BLINK_AT_RAMP_MIDDLE +#undef BLINK_AT_RAMP_CEILING + +// ramp shape and size +#define RAMP_LENGTH 150 + +// driver is a FET + 3x7135, ~413 lm at highest regulated level +// ../../../bin/level_calc.py seventh 2 150 7135 1 12 414 FET 2 10 1930 +#define PWM1_LEVELS 1,1,2,2,3,3,4,4,5,5,6,6,7,8,8,9,10,10,11,12,13,14,15,15,16,17,18,20,21,22,23,24,26,27,28,30,31,33,34,36,38,39,41,43,45,47,49,51,53,56,58,60,63,65,68,71,74,77,80,83,86,89,93,96,100,103,107,111,115,119,124,128,132,137,142,147,152,157,163,168,174,180,186,192,198,204,211,218,225,232,240,247,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0 +#define PWM2_LEVELS 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,5,7,9,12,14,16,19,22,24,27,30,33,36,39,42,45,48,52,55,58,62,66,69,73,77,81,85,90,94,98,103,107,112,117,122,127,132,137,143,148,154,160,166,172,178,184,191,197,204,211,218,225,232,240,247,255 +#define MAX_1x7135 93 +#define HALFSPEED_LEVEL 14 +#define QUARTERSPEED_LEVEL 7 + +#define MIN_THERM_STEPDOWN 65 // lowest value it'll step down to + + +// ceiling is level 130/150 (50% power) +#define RAMP_SMOOTH_FLOOR 1 +#define RAMP_SMOOTH_CEIL 130 + +// 20, 56, [93], 130 (93 is highest regulated) +// (8 / 102 / 413 / 1163 + 1930 lm) +#define RAMP_DISCRETE_FLOOR 20 +#define RAMP_DISCRETE_CEIL RAMP_SMOOTH_CEIL +#define RAMP_DISCRETE_STEPS 4 + +// ~25 lm to ~400 lm +#define MUGGLE_FLOOR 30 +#define MUGGLE_CEILING MAX_1x7135 + +// throttle back faster when high +#define THERM_FASTER_LEVEL 130 + diff --git a/ui/fireflies-ui/cfg-ff-e07-2.h b/ui/fireflies-ui/cfg-ff-e07-2.h new file mode 100644 index 0000000..48f9c15 --- /dev/null +++ b/ui/fireflies-ui/cfg-ff-e07-2.h @@ -0,0 +1,27 @@ +// Fireflies E07-2 config options for Anduril / FFUI +// mostly the same as PL47 +#include "cfg-ff-pl47.h" + +// ceiling is level 130/150 (50% power) +#undef RAMP_SMOOTH_CEIL +#define RAMP_SMOOTH_CEIL 130 + +// 20, 56, 93, 130 (83 is highest regulated) +// (requested config is 1%, 5%, 25%, 50%, double-click-turbo) +// (but this doesn't allow us to hit level 83) +#undef RAMP_DISCRETE_FLOOR +#define RAMP_DISCRETE_FLOOR 20 +#undef RAMP_DISCRETE_CEIL +#define RAMP_DISCRETE_CEIL RAMP_SMOOTH_CEIL +#undef RAMP_DISCRETE_STEPS +#define RAMP_DISCRETE_STEPS 4 + +// regulate down faster when the FET is active, slower otherwise +#undef THERM_FASTER_LEVEL +#define THERM_FASTER_LEVEL 130 // throttle back faster when high + +// play it safe, don't try to regulate above the recommended safe level +#ifndef THERM_HARD_TURBO_DROP +#define THERM_HARD_TURBO_DROP +#endif + diff --git a/ui/fireflies-ui/fireflies-ui.c b/ui/fireflies-ui/fireflies-ui.c new file mode 100644 index 0000000..34f8293 --- /dev/null +++ b/ui/fireflies-ui/fireflies-ui.c @@ -0,0 +1,2386 @@ +/* + * Fireflies UI: A custom UI for Fireflies-brand flashlights. + * (based on Anduril by ToyKeeper) + * + * Copyright (C) 2019 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 . + */ + +/********* User-configurable options *********/ +// UI config file name (set it here or define it at the gcc command line) +//#define CONFIGFILE cfg-ff-pl47.h + +#define USE_LVP // FIXME: won't build when this option is turned off + +// parameters for this defined below or per-driver +#define USE_THERMAL_REGULATION +#define DEFAULT_THERM_CEIL 45 // try not to get hotter than this + +// short blip when crossing from "click" to "hold" from off +// (helps the user hit moon mode exactly, instead of holding too long +// or too short) +#define MOON_TIMING_HINT +// short blips while ramping +#define BLINK_AT_RAMP_MIDDLE +//#define BLINK_AT_RAMP_FLOOR +#define BLINK_AT_RAMP_CEILING +//#define BLINK_AT_STEPS // whenever a discrete ramp mode is passed in smooth mode + +// ramp down via regular button hold if a ramp-up ended <1s ago +// ("hold, release, hold" ramps down instead of up) +#define USE_REVERSING + +// battery readout style (pick one) +#define BATTCHECK_VpT +//#define BATTCHECK_8bars // FIXME: breaks build +//#define BATTCHECK_4bars // FIXME: breaks build + +// enable/disable various strobe modes +#define USE_BIKE_FLASHER_MODE +#define USE_PARTY_STROBE_MODE +#define USE_TACTICAL_STROBE_MODE +#define USE_LIGHTNING_MODE +#define USE_CANDLE_MODE + +// enable sunset (goodnight) mode +#define USE_GOODNIGHT_MODE +#define GOODNIGHT_TIME 60 // minutes (approximately) +#define GOODNIGHT_LEVEL 24 // ~11 lm + +// enable beacon mode +#define USE_BEACON_MODE + +//Muggle mode for easy UI +#define USE_MUGGLE_MODE + +// make the ramps configurable by the user +#define USE_RAMP_CONFIG + +// boring strobes nobody really likes, but sometimes flashlight companies want +// (these replace the fun strobe group, +// so don't enable them at the same time as any of the above strobes) +//#define USE_POLICE_STROBE_MODE +//#define USE_SOS_MODE + +// dual-switch support (second switch is a tail clicky) +//#define START_AT_MEMORIZED_LEVEL + +/***** specific settings for known driver types *****/ +#include "tk.h" +#include incfile(CONFIGFILE) + +///// Fireflies-specific configuration +// disable ramp config +#ifdef USE_RAMP_CONFIG +#undef USE_RAMP_CONFIG +#endif + +// no muggle mode +#ifdef USE_MUGGLE_MODE +#undef USE_MUGGLE_MODE +#endif + +// turn off strobe mode entirely; we're replacing it +#ifdef USE_BIKE_FLASHER_MODE +#undef USE_BIKE_FLASHER_MODE +#endif +#ifdef USE_PARTY_STROBE_MODE +#undef USE_PARTY_STROBE_MODE +#endif +#ifdef USE_TACTICAL_STROBE_MODE +#undef USE_TACTICAL_STROBE_MODE +#endif +#ifdef USE_LIGHTNING_MODE +#undef USE_LIGHTNING_MODE +#endif +#ifdef USE_CANDLE_MODE +#undef USE_CANDLE_MODE +#endif + +// remove other blinkies too +#ifdef USE_GOODNIGHT_MODE +#undef USE_GOODNIGHT_MODE +#endif +#ifdef USE_BEACON_MODE +#undef USE_BEACON_MODE +#endif + +// use these strobes instead +#define USE_POLICE_STROBE_MODE +#define USE_SOS_MODE + +// thermal config mode on 10 clicks from off +#define USE_TENCLICK_THERMAL_CONFIG + +///// end Fireflies-specific configuration + +// thermal properties, if not defined per-driver +#ifndef MIN_THERM_STEPDOWN +#define MIN_THERM_STEPDOWN MAX_1x7135 // lowest value it'll step down to +#endif +#ifndef THERM_FASTER_LEVEL + #ifdef MAX_Nx7135 + #define THERM_FASTER_LEVEL MAX_Nx7135 // throttle back faster when high + #else + #define THERM_FASTER_LEVEL (RAMP_SIZE*4/5) // throttle back faster when high + #endif +#endif +#ifdef USE_THERMAL_REGULATION +#define USE_SET_LEVEL_GRADUALLY // isn't used except for thermal adjustments +#endif + + +/********* Configure SpaghettiMonster *********/ +#define USE_DELAY_ZERO +#define USE_RAMPING +#ifndef RAMP_LENGTH +#define RAMP_LENGTH 150 // default, if not overridden in a driver cfg file +#endif +#define MAX_BIKING_LEVEL 120 // should be 127 or less +#define USE_BATTCHECK + +#if defined(USE_MUGGLE_MODE) +#ifndef MUGGLE_FLOOR +#define MUGGLE_FLOOR 22 +#endif +#ifndef MUGGLE_CEILING +#define MUGGLE_CEILING (MAX_1x7135+20) +#endif +#endif +#define USE_IDLE_MODE // reduce power use while awake and no tasks are pending +#define USE_DYNAMIC_UNDERCLOCKING // cut clock speed at very low modes for better efficiency + +// full FET strobe can be a bit much... use max regulated level instead, +// if there's a bright enough regulated level +#ifdef MAX_Nx7135 +#define STROBE_BRIGHTNESS MAX_Nx7135 +#else +#define STROBE_BRIGHTNESS MAX_LEVEL +#endif + +#if defined(USE_CANDLE_MODE) || defined(USE_BIKE_FLASHER_MODE) || defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) || defined(USE_LIGHTNING_MODE) +#define USE_STROBE_STATE +#endif + +#if defined(USE_POLICE_STROBE_MODE) || defined(USE_SOS_MODE) +#define USE_BORING_STROBE_STATE +#endif + +// auto-detect how many eeprom bytes +#define USE_EEPROM +typedef enum { + ramp_style_e, + #ifdef USE_RAMP_CONFIG + ramp_smooth_floor_e, + ramp_smooth_ceil_e, + ramp_discrete_floor_e, + ramp_discrete_ceil_e, + ramp_discrete_steps_e, + #endif + #ifdef USE_TINT_RAMPING + tint_e, + #endif + #ifdef USE_STROBE_STATE + strobe_type_e, + #endif + #if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) + strobe_delays_0_e, + strobe_delays_1_e, + #endif + #ifdef USE_BIKE_FLASHER_MODE + bike_flasher_brightness_e, + #endif + #ifdef USE_BEACON_MODE + beacon_seconds_e, + #endif + #ifdef USE_MUGGLE_MODE + muggle_mode_active_e, + #endif + #ifdef USE_THERMAL_REGULATION + therm_ceil_e, + therm_cal_offset_e, + #endif + #ifdef USE_INDICATOR_LED + indicator_led_mode_e, + #endif + eeprom_indexes_e_END +} eeprom_indexes_e; +#define EEPROM_BYTES eeprom_indexes_e_END + +#ifdef START_AT_MEMORIZED_LEVEL +#define USE_EEPROM_WL +#define EEPROM_WL_BYTES 1 +#endif + +// auto-configure other stuff... +#if defined(USE_LIGHTNING_MODE) || defined(USE_CANDLE_MODE) +#define USE_PSEUDO_RAND +#endif + +#if defined(USE_CANDLE_MODE) +#ifndef USE_TRIANGLE_WAVE +#define USE_TRIANGLE_WAVE +#endif +#endif + +#include "spaghetti-monster.h" + + +// FSM states +uint8_t off_state(Event event, uint16_t arg); +// simple numeric entry config menu +uint8_t config_state_base(Event event, uint16_t arg, + uint8_t num_config_steps, + void (*savefunc)()); +#define MAX_CONFIG_VALUES 3 +uint8_t config_state_values[MAX_CONFIG_VALUES]; +// ramping mode and its related config mode +uint8_t steady_state(Event event, uint16_t arg); +#ifdef USE_RAMP_CONFIG +uint8_t ramp_config_state(Event event, uint16_t arg); +#endif +#ifdef USE_TINT_RAMPING +// not actually a mode, more of a fallback under other modes +uint8_t tint_ramping_state(Event event, uint16_t arg); +#endif +// party and tactical strobes +#ifdef USE_STROBE_STATE +uint8_t strobe_state(Event event, uint16_t arg); +#endif +#ifdef USE_BORING_STROBE_STATE +uint8_t boring_strobe_state(Event event, uint16_t arg); +volatile uint8_t boring_strobe_type = 0; +void sos_blink(uint8_t num, uint8_t dah); +#define strobe_state boring_strobe_state // use the right strobes +#define NUM_BORING_STROBES 2 +#endif +#ifdef USE_BATTCHECK +uint8_t battcheck_state(Event event, uint16_t arg); +#endif +#ifdef USE_THERMAL_REGULATION +uint8_t tempcheck_state(Event event, uint16_t arg); +uint8_t thermal_config_state(Event event, uint16_t arg); +#endif +#ifdef USE_GOODNIGHT_MODE +// 1-hour ramp down from low, then automatic off +uint8_t goodnight_state(Event event, uint16_t arg); +#endif +#ifdef USE_BEACON_MODE +// beacon mode and its related config mode +uint8_t beacon_state(Event event, uint16_t arg); +uint8_t beacon_config_state(Event event, uint16_t arg); +#endif +// soft lockout +#define MOON_DURING_LOCKOUT_MODE +// if enabled, 2nd lockout click goes to the other ramp's floor level +#define LOCKOUT_MOON_FANCY +uint8_t lockout_state(Event event, uint16_t arg); +// momentary / signalling mode +uint8_t momentary_state(Event event, uint16_t arg); +uint8_t momentary_mode = 0; // 0 = ramping, 1 = strobe +uint8_t momentary_active = 0; // boolean, true if active *right now* +#ifdef USE_MUGGLE_MODE +// muggle mode, super-simple, hard to exit +uint8_t muggle_state(Event event, uint16_t arg); +uint8_t muggle_mode_active = 0; +#endif + +// general helper function for config modes +uint8_t number_entry_state(Event event, uint16_t arg); +// return value from number_entry_state() +volatile uint8_t number_entry_value; + +void blink_confirm(uint8_t num); +void blip(); +#if defined(USE_INDICATOR_LED) && defined(TICK_DURING_STANDBY) +void indicator_blink(uint8_t arg); +#endif + +// remember stuff even after battery was changed +void load_config(); +void save_config(); +#ifdef START_AT_MEMORIZED_LEVEL +void save_config_wl(); +#endif + +// default ramp options if not overridden earlier per-driver +#ifndef RAMP_STYLE +#define RAMP_STYLE 0 // smooth default +#endif +#ifndef RAMP_SMOOTH_FLOOR + #define RAMP_SMOOTH_FLOOR 1 +#endif +#ifndef RAMP_SMOOTH_CEIL + #if PWM_CHANNELS == 3 + #define RAMP_SMOOTH_CEIL MAX_Nx7135 + #else + #define RAMP_SMOOTH_CEIL MAX_LEVEL - 30 + #endif +#endif +#ifndef RAMP_DISCRETE_FLOOR + #define RAMP_DISCRETE_FLOOR 20 +#endif +#ifndef RAMP_DISCRETE_CEIL + #define RAMP_DISCRETE_CEIL RAMP_SMOOTH_CEIL +#endif +#ifndef RAMP_DISCRETE_STEPS + #define RAMP_DISCRETE_STEPS 7 +#endif + +// mile marker(s) partway up the ramp +// default: blink only at border between regulated and FET +#ifdef BLINK_AT_RAMP_MIDDLE + #if PWM_CHANNELS >= 3 + #ifndef BLINK_AT_RAMP_MIDDLE_1 + #define BLINK_AT_RAMP_MIDDLE_1 MAX_Nx7135 + #ifndef BLINK_AT_RAMP_MIDDLE_2 + #define BLINK_AT_RAMP_MIDDLE_2 MAX_1x7135 + #endif + #endif + #else + #ifndef BLINK_AT_RAMP_MIDDLE_1 + #define BLINK_AT_RAMP_MIDDLE_1 MAX_1x7135 + #endif + #endif +#endif + +// brightness control +#ifndef DEFAULT_LEVEL +#define DEFAULT_LEVEL MAX_1x7135 +#endif +uint8_t memorized_level = DEFAULT_LEVEL; +// smooth vs discrete ramping +volatile uint8_t ramp_style = RAMP_STYLE; // 0 = smooth, 1 = discrete +volatile uint8_t ramp_smooth_floor = RAMP_SMOOTH_FLOOR; +volatile uint8_t ramp_smooth_ceil = RAMP_SMOOTH_CEIL; +volatile uint8_t ramp_discrete_floor = RAMP_DISCRETE_FLOOR; +volatile uint8_t ramp_discrete_ceil = RAMP_DISCRETE_CEIL; +volatile uint8_t ramp_discrete_steps = RAMP_DISCRETE_STEPS; +uint8_t ramp_discrete_step_size; // don't set this + +#ifdef USE_INDICATOR_LED + // bits 2-3 control lockout mode + // bits 0-1 control "off" mode + // modes are: 0=off, 1=low, 2=high, 3=blinking (if TICK_DURING_STANDBY enabled) + #ifdef INDICATOR_LED_DEFAULT_MODE + uint8_t indicator_led_mode = INDICATOR_LED_DEFAULT_MODE; + #else + #ifdef USE_INDICATOR_LED_WHILE_RAMPING + //uint8_t indicator_led_mode = (1<<2) + 2; + uint8_t indicator_led_mode = (2<<2) + 1; + #else + uint8_t indicator_led_mode = (3<<2) + 1; + #endif + #endif +#endif + +// calculate the nearest ramp level which would be valid at the moment +// (is a no-op for smooth ramp, but limits discrete ramp to only the +// correct levels for the user's config) +uint8_t nearest_level(int16_t target); + +#ifdef USE_THERMAL_REGULATION +// brightness before thermal step-down +uint8_t target_level = 0; +#endif + +// internal numbering for strobe modes +#ifdef USE_STROBE_STATE +typedef enum { + #ifdef USE_PARTY_STROBE_MODE + party_strobe_e, + #endif + #ifdef USE_TACTICAL_STROBE_MODE + tactical_strobe_e, + #endif + #ifdef USE_LIGHTNING_MODE + lightning_storm_e, + #endif + #ifdef USE_CANDLE_MODE + candle_mode_e, + #endif + #ifdef USE_BIKE_FLASHER_MODE + bike_flasher_e, + #endif + strobe_mode_END +} strobe_mode_te; + +const int NUM_STROBES = strobe_mode_END; + +// which strobe mode is active? +#ifdef USE_CANDLE_MODE +volatile strobe_mode_te strobe_type = candle_mode_e; +#else +volatile strobe_mode_te strobe_type = 0; +#endif +#endif + +#if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) +// party / tactical strobe timing +volatile uint8_t strobe_delays[] = { 40, 67 }; // party strobe, tactical strobe +#endif + +// bike mode config options +#ifdef USE_BIKE_FLASHER_MODE +volatile uint8_t bike_flasher_brightness = MAX_1x7135; +#endif + +#ifdef USE_CANDLE_MODE +uint8_t candle_mode_state(Event event, uint16_t arg); +uint8_t triangle_wave(uint8_t phase); +#ifndef CANDLE_AMPLITUDE +#define CANDLE_AMPLITUDE 25 +#endif +#endif + +#ifdef USE_BEACON_MODE +// beacon timing +volatile uint8_t beacon_seconds = 2; +#endif + + +uint8_t off_state(Event event, uint16_t arg) { + // turn emitter off when entering state + if (event == EV_enter_state) { + set_level(0); + #ifdef USE_INDICATOR_LED + indicator_led(indicator_led_mode & 0x03); + #endif + // sleep while off (lower power use) + go_to_standby = 1; + return EVENT_HANDLED; + } + // go back to sleep eventually if we got bumped but didn't leave "off" state + else if (event == EV_tick) { + if (arg > TICKS_PER_SECOND*2) { + go_to_standby = 1; + #ifdef USE_INDICATOR_LED + indicator_led(indicator_led_mode & 0x03); + #endif + } + return EVENT_HANDLED; + } + #if defined(TICK_DURING_STANDBY) && defined(USE_INDICATOR_LED) + // blink the indicator LED, maybe + else if (event == EV_sleep_tick) { + if ((indicator_led_mode & 0b00000011) == 0b00000011) { + indicator_blink(arg); + } + return EVENT_HANDLED; + } + #endif + // hold (initially): go to lowest level (floor), but allow abort for regular click + else if (event == EV_click1_press) { + set_level(nearest_level(1)); + return EVENT_HANDLED; + } + // hold: go to lowest level + else if (event == EV_click1_hold) { + #ifdef MOON_TIMING_HINT + if (arg == 0) { + // let the user know they can let go now to stay at moon + blip(); + } else + #endif + // don't start ramping immediately; + // give the user time to release at moon level + //if (arg >= HOLD_TIMEOUT) { // smaller + if (arg >= (!ramp_style) * HOLD_TIMEOUT) { // more consistent + set_state(steady_state, 1); + } + return EVENT_HANDLED; + } + // hold, release quickly: go to lowest level (floor) + else if (event == EV_click1_hold_release) { + set_state(steady_state, 1); + return EVENT_HANDLED; + } + // 1 click (before timeout): go to memorized level, but allow abort for double click + else if (event == EV_click1_release) { + set_level(nearest_level(memorized_level)); + return EVENT_HANDLED; + } + // 1 click: regular mode + else if (event == EV_1click) { + set_state(steady_state, memorized_level); + return EVENT_HANDLED; + } + // click, hold: go to highest level (ceiling) (for ramping down) + else if (event == EV_click2_hold) { + set_state(steady_state, MAX_LEVEL); + return EVENT_HANDLED; + } + // 2 clicks: highest mode (ceiling) + else if (event == EV_2clicks) { + set_state(steady_state, MAX_LEVEL); + return EVENT_HANDLED; + } + // 3 clicks (initial press): off, to prep for later events + else if (event == EV_click3_press) { + set_level(0); + return EVENT_HANDLED; + } + #ifdef USE_BATTCHECK + // 3 clicks: battcheck mode / blinky mode group 1 + else if (event == EV_3clicks) { + set_state(battcheck_state, 0); + return EVENT_HANDLED; + } + #endif + // click, click, long-click: strobe mode + #ifdef USE_STROBE_STATE + else if (event == EV_click3_hold) { + set_state(strobe_state, 0); + return EVENT_HANDLED; + } + #elif defined(USE_BORING_STROBE_STATE) + else if (event == EV_click3_hold) { + set_state(boring_strobe_state, 0); + return EVENT_HANDLED; + } + #endif + // 4 clicks: soft lockout + else if (event == EV_4clicks) { + blink_confirm(2); + set_state(lockout_state, 0); + return EVENT_HANDLED; + } + // 5 clicks: momentary mode + else if (event == EV_5clicks) { + blink_confirm(1); + set_state(momentary_state, 0); + return EVENT_HANDLED; + } + #ifdef USE_MUGGLE_MODE + // 6 clicks: muggle mode + else if (event == EV_6clicks) { + blink_confirm(1); + set_state(muggle_state, 0); + return EVENT_HANDLED; + } + #endif + #ifdef USE_INDICATOR_LED + // 7 clicks: change indicator LED mode + else if (event == EV_7clicks) { + uint8_t mode = (indicator_led_mode & 3) + 1; + #ifdef TICK_DURING_STANDBY + mode = mode & 3; + #else + mode = mode % 3; + #endif + #ifdef INDICATOR_LED_SKIP_LOW + if (mode == 1) { mode ++; } + #endif + indicator_led_mode = (indicator_led_mode & 0b11111100) | mode; + indicator_led(mode); + save_config(); + return EVENT_HANDLED; + } + #endif + // 8 clicks: temperature check + else if (event == EV_8clicks) { + set_state(tempcheck_state, 0); + return EVENT_HANDLED; + } + #ifdef USE_TENCLICK_THERMAL_CONFIG + // 10 clicks: thermal config mode + else if (event == EV_10clicks) { + push_state(thermal_config_state, 0); + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} + + +uint8_t steady_state(Event event, uint16_t arg) { + uint8_t mode_min = ramp_smooth_floor; + uint8_t mode_max = ramp_smooth_ceil; + uint8_t ramp_step_size = 1; + #ifdef USE_REVERSING + static int8_t ramp_direction = 1; + #endif + if (ramp_style) { + mode_min = ramp_discrete_floor; + mode_max = ramp_discrete_ceil; + ramp_step_size = ramp_discrete_step_size; + } + + // turn LED on when we first enter the mode + if ((event == EV_enter_state) || (event == EV_reenter_state)) { + momentary_mode = 0; // 0 = ramping, 1 = strobes + // if we just got back from config mode, go back to memorized level + if (event == EV_reenter_state) { + arg = memorized_level; + } + // remember this level, unless it's moon or turbo + if ((arg > mode_min) && (arg < mode_max)) + memorized_level = arg; + // use the requested level even if not memorized + arg = nearest_level(arg); + #ifdef USE_THERMAL_REGULATION + target_level = arg; + #endif + set_level(arg); + #ifdef USE_REVERSING + ramp_direction = 1; + #endif + return EVENT_HANDLED; + } + // 1 click: off + else if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: go to/from highest level + else if (event == EV_2clicks) { + if (actual_level < MAX_LEVEL) { + #ifdef USE_THERMAL_REGULATION + target_level = MAX_LEVEL; + #endif + // true turbo, not the mode-specific ceiling + set_level(MAX_LEVEL); + } + else { + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + set_level(memorized_level); + } + return EVENT_HANDLED; + } + // 3 clicks: toggle smooth vs discrete ramping + else if (event == EV_3clicks) { + ramp_style = !ramp_style; + memorized_level = nearest_level(actual_level); + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #ifdef USE_SET_LEVEL_GRADUALLY + //set_level_gradually(lvl); + #endif + #endif + save_config(); + #ifdef START_AT_MEMORIZED_LEVEL + save_config_wl(); + #endif + blip(); + set_level(memorized_level); + return EVENT_HANDLED; + } + #ifdef USE_RAMP_CONFIG + // 4 clicks: configure this ramp mode + else if (event == EV_4clicks) { + push_state(ramp_config_state, 0); + return EVENT_HANDLED; + } + #endif + // hold: change brightness (brighter) + else if (event == EV_click1_hold) { + // ramp slower in discrete mode + if (ramp_style && (arg % HOLD_TIMEOUT != 0)) { + return EVENT_HANDLED; + } + #ifdef USE_REVERSING + // fix ramp direction on first frame if necessary + if (!arg) { + // make it ramp down instead, if already at max + if (actual_level >= mode_max) { ramp_direction = -1; } + // make it ramp up if already at min + // (off->hold->stepped_min->release causes this state) + else if (actual_level <= mode_min) { ramp_direction = 1; } + } + memorized_level = nearest_level((int16_t)actual_level \ + + (ramp_step_size * ramp_direction)); + #else + memorized_level = nearest_level((int16_t)actual_level + ramp_step_size); + #endif + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + #if defined(BLINK_AT_RAMP_CEILING) || defined(BLINK_AT_RAMP_MIDDLE) + // only blink once for each threshold + if ((memorized_level != actual_level) && ( + 0 // for easier syntax below + #ifdef BLINK_AT_RAMP_MIDDLE_1 + || (memorized_level == BLINK_AT_RAMP_MIDDLE_1) + #endif + #ifdef BLINK_AT_RAMP_MIDDLE_2 + || (memorized_level == BLINK_AT_RAMP_MIDDLE_2) + #endif + #ifdef BLINK_AT_RAMP_CEILING + || (memorized_level == mode_max) + #endif + #if defined(USE_REVERSING) && defined(BLINK_AT_RAMP_FLOOR) + || (memorized_level == mode_min) + #endif + )) { + blip(); + } + #endif + #if defined(BLINK_AT_STEPS) + uint8_t foo = ramp_style; + ramp_style = 1; + uint8_t nearest = nearest_level((int16_t)actual_level); + ramp_style = foo; + // only blink once for each threshold + if ((memorized_level != actual_level) && + (ramp_style == 0) && + (memorized_level == nearest) + ) + { + blip(); + } + #endif + set_level(memorized_level); + return EVENT_HANDLED; + } + #if defined(USE_REVERSING) || defined(START_AT_MEMORIZED_LEVEL) + // reverse ramp direction on hold release + else if (event == EV_click1_hold_release) { + #ifdef USE_REVERSING + ramp_direction = -ramp_direction; + #endif + #ifdef START_AT_MEMORIZED_LEVEL + save_config_wl(); + #endif + return EVENT_HANDLED; + } + #endif + // click, hold: change brightness (dimmer) + else if (event == EV_click2_hold) { + #ifdef USE_REVERSING + ramp_direction = 1; + #endif + // ramp slower in discrete mode + if (ramp_style && (arg % HOLD_TIMEOUT != 0)) { + return EVENT_HANDLED; + } + // TODO? make it ramp up instead, if already at min? + memorized_level = nearest_level((int16_t)actual_level - ramp_step_size); + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + #if defined(BLINK_AT_RAMP_FLOOR) || defined(BLINK_AT_RAMP_MIDDLE) + // only blink once for each threshold + if ((memorized_level != actual_level) && ( + 0 // for easier syntax below + #ifdef BLINK_AT_RAMP_MIDDLE_1 + || (memorized_level == BLINK_AT_RAMP_MIDDLE_1) + #endif + #ifdef BLINK_AT_RAMP_MIDDLE_2 + || (memorized_level == BLINK_AT_RAMP_MIDDLE_2) + #endif + #ifdef BLINK_AT_RAMP_FLOOR + || (memorized_level == mode_min) + #endif + )) { + blip(); + } + #endif + #if defined(BLINK_AT_STEPS) + uint8_t foo = ramp_style; + ramp_style = 1; + uint8_t nearest = nearest_level((int16_t)actual_level); + ramp_style = foo; + // only blink once for each threshold + if ((memorized_level != actual_level) && + (ramp_style == 0) && + (memorized_level == nearest) + ) + { + blip(); + } + #endif + set_level(memorized_level); + return EVENT_HANDLED; + } + #ifdef START_AT_MEMORIZED_LEVEL + // click, release, hold, release: save new ramp level (if necessary) + else if (event == EV_click2_hold_release) { + save_config_wl(); + return EVENT_HANDLED; + } + #endif + #if defined(USE_SET_LEVEL_GRADUALLY) || defined(USE_REVERSING) + else if (event == EV_tick) { + #ifdef USE_REVERSING + // un-reverse after 1 second + if (arg == TICKS_PER_SECOND) ramp_direction = 1; + #endif + #ifdef USE_SET_LEVEL_GRADUALLY + // make thermal adjustment speed scale with magnitude + if ((arg & 1) && (actual_level < THERM_FASTER_LEVEL)) { + return EVENT_HANDLED; // adjust slower when not a high mode + } + #ifdef THERM_HARD_TURBO_DROP + else if ((! (actual_level < THERM_FASTER_LEVEL)) + && (actual_level > gradual_target)) { + gradual_tick(); + } + else { + #endif + // [int(62*4 / (x**0.8)) for x in (1,2,4,8,16,32,64,128)] + //uint8_t intervals[] = {248, 142, 81, 46, 26, 15, 8, 5}; + // [int(62*4 / (x**0.9)) for x in (1,2,4,8,16,32,64,128)] + //uint8_t intervals[] = {248, 132, 71, 38, 20, 10, 5, 3}; + // [int(62*4 / (x**0.95)) for x in (1,2,4,8,16,32,64,128)] + uint8_t intervals[] = {248, 128, 66, 34, 17, 9, 4, 2}; + uint8_t diff; + static uint8_t ticks_since_adjust = 0; + ticks_since_adjust ++; + if (gradual_target > actual_level) diff = gradual_target - actual_level; + else { + diff = actual_level - gradual_target; + } + uint8_t magnitude = 0; + #ifndef THERM_HARD_TURBO_DROP + // if we're on a really high mode, drop faster + if (actual_level >= THERM_FASTER_LEVEL) { magnitude ++; } + #endif + while (diff) { + magnitude ++; + diff >>= 1; + } + uint8_t ticks_per_adjust = intervals[magnitude]; + if (ticks_since_adjust > ticks_per_adjust) + { + gradual_tick(); + ticks_since_adjust = 0; + } + //if (!(arg % ticks_per_adjust)) gradual_tick(); + #ifdef THERM_HARD_TURBO_DROP + } + #endif + #endif + return EVENT_HANDLED; + } + #endif + #ifdef USE_THERMAL_REGULATION + // overheating: drop by an amount proportional to how far we are above the ceiling + else if (event == EV_temperature_high) { + #if 0 + blip(); + #endif + #ifdef THERM_HARD_TURBO_DROP + if (actual_level > THERM_FASTER_LEVEL) { + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(THERM_FASTER_LEVEL); + #else + set_level(THERM_FASTER_LEVEL); + #endif + target_level = THERM_FASTER_LEVEL; + } else + #endif + if (actual_level > MIN_THERM_STEPDOWN) { + int16_t stepdown = actual_level - arg; + if (stepdown < MIN_THERM_STEPDOWN) stepdown = MIN_THERM_STEPDOWN; + else if (stepdown > MAX_LEVEL) stepdown = MAX_LEVEL; + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(stepdown); + #else + set_level(stepdown); + #endif + } + return EVENT_HANDLED; + } + // underheating: increase slowly if we're lower than the target + // (proportional to how low we are) + else if (event == EV_temperature_low) { + #if 0 + blip(); + #endif + if (actual_level < target_level) { + //int16_t stepup = actual_level + (arg>>1); + int16_t stepup = actual_level + arg; + if (stepup > target_level) stepup = target_level; + else if (stepup < MIN_THERM_STEPDOWN) stepup = MIN_THERM_STEPDOWN; + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(stepup); + #else + set_level(stepup); + #endif + } + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} + + +#ifdef USE_TINT_RAMPING +uint8_t tint_ramping_state(Event event, uint16_t arg) { + static int8_t tint_ramp_direction = 1; + static uint8_t prev_tint = 0; + // don't activate auto-tint modes unless the user hits the edge + // and keeps pressing for a while + static uint8_t past_edge_counter = 0; + // bugfix: click-click-hold from off to strobes would invoke tint ramping + // in addition to changing state... so ignore any tint-ramp events which + // don't look like they were meant to be here + static uint8_t active = 0; + + // click, click, hold: change the tint + if (event == EV_click3_hold) { + // reset at beginning of movement + if (! arg) { + active = 1; // first frame means this is for us + past_edge_counter = 0; // doesn't start until user hits the edge + } + // ignore event if we weren't the ones who handled the first frame + if (! active) return EVENT_HANDLED; + + // change normal tints + if ((tint_ramp_direction > 0) && (tint < 254)) { + tint += 1; + } + else if ((tint_ramp_direction < 0) && (tint > 1)) { + tint -= 1; + } + // if the user kept pressing long enough, go the final step + if (past_edge_counter == 64) { + past_edge_counter ++; + tint ^= 1; // 0 -> 1, 254 -> 255 + blip(); + } + // if tint change stalled, let user know we hit the edge + else if (prev_tint == tint) { + if (past_edge_counter == 0) blip(); + // count up but don't wrap back to zero + if (past_edge_counter < 255) past_edge_counter ++; + } + prev_tint = tint; + set_level(actual_level); + return EVENT_HANDLED; + } + + // click, click, hold, release: reverse direction for next ramp + else if (event == EV_click3_hold_release) { + active = 0; // ignore next hold if it wasn't meant for us + // reverse + tint_ramp_direction = -tint_ramp_direction; + if (tint == 0) tint_ramp_direction = 1; + else if (tint == 255) tint_ramp_direction = -1; + // remember tint after battery change + save_config(); + return EVENT_HANDLED; + } + + return EVENT_NOT_HANDLED; +} +#endif // ifdef USE_TINT_RAMPING + + +#ifdef USE_STROBE_STATE +uint8_t strobe_state(Event event, uint16_t arg) { + static int8_t ramp_direction = 1; + + // 'st' reduces ROM size by avoiding access to a volatile var + // (maybe I should just make it nonvolatile?) + strobe_mode_te st = strobe_type; + + momentary_mode = 1; // 0 = ramping, 1 = strobes + + #ifdef USE_CANDLE_MODE + // pass all events to candle mode, when it's active + // (the code is in its own pseudo-state to keep things cleaner) + if (st == candle_mode_e) { + candle_mode_state(event, arg); + } + #endif + + if (0) {} // placeholder + // init anything which needs to be initialized + else if (event == EV_enter_state) { + ramp_direction = 1; + return EVENT_HANDLED; + } + // 1 click: off + else if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: rotate through strobe/flasher modes + else if (event == EV_2clicks) { + strobe_type = (st + 1) % NUM_STROBES; + save_config(); + return EVENT_HANDLED; + } + // hold: change speed (go faster) + // or change brightness (brighter) + else if (event == EV_click1_hold) { + if (0) {} // placeholder + + // party / tactical strobe faster + #if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) + #ifdef USE_TACTICAL_STROBE_MODE + else if (st <= tactical_strobe_e) { + #else + else if (st == party_strobe_e) { + #endif + if ((arg & 1) == 0) { + uint8_t d = strobe_delays[st]; + d -= ramp_direction; + if (d < 8) d = 8; + else if (d > 254) d = 254; + strobe_delays[st] = d; + } + } + #endif + + // lightning has no adjustments + //else if (st == lightning_storm_e) {} + + // biking mode brighter + #ifdef USE_BIKE_FLASHER_MODE + else if (st == bike_flasher_e) { + bike_flasher_brightness += ramp_direction; + if (bike_flasher_brightness < 2) bike_flasher_brightness = 2; + else if (bike_flasher_brightness > MAX_BIKING_LEVEL) bike_flasher_brightness = MAX_BIKING_LEVEL; + set_level(bike_flasher_brightness); + } + #endif + + return EVENT_HANDLED; + } + // reverse ramp direction on hold release + // ... and save new strobe settings + else if (event == EV_click1_hold_release) { + ramp_direction = -ramp_direction; + save_config(); + return EVENT_HANDLED; + } + // click, hold: change speed (go slower) + // or change brightness (dimmer) + else if (event == EV_click2_hold) { + ramp_direction = 1; + + if (0) {} // placeholder + + // party / tactical strobe slower + #if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) + #ifdef USE_TACTICAL_STROBE_MODE + else if (st <= tactical_strobe_e) { + #else + else if (st == party_strobe_e) { + #endif + if ((arg & 1) == 0) { + if (strobe_delays[st] < 255) strobe_delays[st] ++; + } + } + #endif + + // lightning has no adjustments + //else if (st == lightning_storm_e) {} + + // biking mode dimmer + #ifdef USE_BIKE_FLASHER_MODE + else if (st == bike_flasher_e) { + if (bike_flasher_brightness > 2) + bike_flasher_brightness --; + set_level(bike_flasher_brightness); + } + #endif + + return EVENT_HANDLED; + } + // release hold: save new strobe settings + else if (event == EV_click2_hold_release) { + save_config(); + return EVENT_HANDLED; + } + #if defined(USE_LIGHTNING_MODE) || defined(USE_CANDLE_MODE) + // clock tick: bump the random seed + else if (event == EV_tick) { + // un-reverse after 1 second + if (arg == TICKS_PER_SECOND) ramp_direction = 1; + + pseudo_rand_seed += arg; + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} + +#if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) +inline void party_tactical_strobe_mode_iter(uint8_t st) { + // one iteration of main loop() + uint8_t del = strobe_delays[st]; + // TODO: make tac strobe brightness configurable? + set_level(STROBE_BRIGHTNESS); + if (0) {} // placeholde0 + #ifdef USE_PARTY_STROBE_MODE + else if (st == party_strobe_e) { // party strobe + if (del < 42) delay_zero(); + else nice_delay_ms(1); + } + #endif + #ifdef USE_TACTICAL_STROBE_MODE + else { //tactical strobe + nice_delay_ms(del >> 1); + } + #endif + set_level(0); + nice_delay_ms(del); // no return check necessary on final delay +} +#endif + +#ifdef USE_LIGHTNING_MODE +inline void lightning_storm_iter() { + // one iteration of main loop() + int16_t brightness; + uint16_t rand_time; + + // turn the emitter on at a random level, + // for a random amount of time between 1ms and 32ms + //rand_time = 1 << (pseudo_rand() % 7); + rand_time = pseudo_rand() & 63; + brightness = 1 << (pseudo_rand() % 7); // 1, 2, 4, 8, 16, 32, 64 + brightness += 1 << (pseudo_rand() % 5); // 2 to 80 now + brightness += pseudo_rand() % brightness; // 2 to 159 now (w/ low bias) + if (brightness > MAX_LEVEL) brightness = MAX_LEVEL; + set_level(brightness); + nice_delay_ms(rand_time); + + // decrease the brightness somewhat more gradually, like lightning + uint8_t stepdown = brightness >> 3; + if (stepdown < 1) stepdown = 1; + while(brightness > 1) { + nice_delay_ms(rand_time); + brightness -= stepdown; + if (brightness < 0) brightness = 0; + set_level(brightness); + /* + if ((brightness < MAX_LEVEL/2) && (! (pseudo_rand() & 15))) { + brightness <<= 1; + set_level(brightness); + } + */ + if (! (pseudo_rand() & 3)) { + nice_delay_ms(rand_time); + set_level(brightness>>1); + } + } + + // turn the emitter off, + // for a random amount of time between 1ms and 8192ms + // (with a low bias) + rand_time = 1 << (pseudo_rand() % 13); + rand_time += pseudo_rand() % rand_time; + set_level(0); + nice_delay_ms(rand_time); // no return check necessary on final delay +} +#endif + +#ifdef USE_BIKE_FLASHER_MODE +inline void bike_flasher_iter() { + // one iteration of main loop() + uint8_t burst = bike_flasher_brightness << 1; + if (burst > MAX_LEVEL) burst = MAX_LEVEL; + for(uint8_t i=0; i<4; i++) { + set_level(burst); + nice_delay_ms(5); + set_level(bike_flasher_brightness); + nice_delay_ms(65); + } + nice_delay_ms(720); // no return check necessary on final delay +} +#endif + +#endif // ifdef USE_STROBE_STATE + +#ifdef USE_CANDLE_MODE +uint8_t candle_mode_state(Event event, uint16_t arg) { + static int8_t ramp_direction = 1; + #define MAX_CANDLE_LEVEL (RAMP_LENGTH-CANDLE_AMPLITUDE-15) + static uint8_t candle_wave1 = 0; + static uint8_t candle_wave2 = 0; + static uint8_t candle_wave3 = 0; + static uint8_t candle_wave2_speed = 0; + // these should add up to 100 + #define CANDLE_WAVE1_MAXDEPTH 30 + #define CANDLE_WAVE2_MAXDEPTH 45 + #define CANDLE_WAVE3_MAXDEPTH 25 + static const uint8_t candle_wave1_depth = CANDLE_WAVE1_MAXDEPTH * CANDLE_AMPLITUDE / 100; + static uint8_t candle_wave2_depth = CANDLE_WAVE2_MAXDEPTH * CANDLE_AMPLITUDE / 100; + static uint8_t candle_wave3_depth = CANDLE_WAVE3_MAXDEPTH * CANDLE_AMPLITUDE / 100; + static uint8_t candle_mode_brightness = 24; + static uint8_t candle_mode_timer = 0; + #define TICKS_PER_CANDLE_MINUTE 4096 // about 65 seconds + #define MINUTES_PER_CANDLE_HALFHOUR 27 // ish + + if (event == EV_enter_state) { + candle_mode_timer = 0; // in case any time was left over from earlier + ramp_direction = 1; + return EVENT_HANDLED; + } + // 2 clicks: cancel timer + else if (event == EV_2clicks) { + // parent state just rotated through strobe/flasher modes, + // so cancel timer... in case any time was left over from earlier + candle_mode_timer = 0; + return EVENT_HANDLED; + } + // hold: change brightness (brighter) + else if (event == EV_click1_hold) { + // ramp away from extremes + if (! arg) { + if (candle_mode_brightness >= MAX_CANDLE_LEVEL) { ramp_direction = -1; } + else if (candle_mode_brightness <= 1) { ramp_direction = 1; } + } + // change brightness, but not too far + candle_mode_brightness += ramp_direction; + if (candle_mode_brightness < 1) candle_mode_brightness = 1; + else if (candle_mode_brightness > MAX_CANDLE_LEVEL) candle_mode_brightness = MAX_CANDLE_LEVEL; + return EVENT_HANDLED; + } + // reverse ramp direction on hold release + else if (event == EV_click1_hold_release) { + ramp_direction = -ramp_direction; + return EVENT_HANDLED; + } + // click, hold: change brightness (dimmer) + else if (event == EV_click2_hold) { + ramp_direction = 1; + if (candle_mode_brightness > 1) + candle_mode_brightness --; + return EVENT_HANDLED; + } + // 3 clicks: add 30m to candle timer + else if (event == EV_3clicks) { + if (candle_mode_timer < (255 - MINUTES_PER_CANDLE_HALFHOUR)) { + // add 30m to the timer + candle_mode_timer += MINUTES_PER_CANDLE_HALFHOUR; + // blink to confirm + set_level(actual_level + 32); + delay_4ms(2); + } + return EVENT_HANDLED; + } + // clock tick: animate candle brightness + else if (event == EV_tick) { + // un-reverse after 1 second + if (arg == TICKS_PER_SECOND) ramp_direction = 1; + + // self-timer dims the light during the final minute + uint8_t subtract = 0; + if (candle_mode_timer == 1) { + subtract = ((candle_mode_brightness+CANDLE_AMPLITUDE) + * ((arg & (TICKS_PER_CANDLE_MINUTE-1)) >> 4)) + >> 8; + } + // we passed a minute mark, decrease timer if it's running + if ((arg & (TICKS_PER_CANDLE_MINUTE-1)) == (TICKS_PER_CANDLE_MINUTE - 1)) { + if (candle_mode_timer > 0) { + candle_mode_timer --; + //set_level(0); delay_4ms(2); + // if the timer ran out, shut off + if (! candle_mode_timer) { + set_state(off_state, 0); + } + } + } + // 3-oscillator synth for a relatively organic pattern + uint8_t add; + add = ((triangle_wave(candle_wave1) * candle_wave1_depth) >> 8) + + ((triangle_wave(candle_wave2) * candle_wave2_depth) >> 8) + + ((triangle_wave(candle_wave3) * candle_wave3_depth) >> 8); + int8_t brightness = candle_mode_brightness + add - subtract; + if (brightness < 0) { brightness = 0; } + set_level(brightness); + + // wave1: slow random LFO + // TODO: make wave slower and more erratic? + if ((arg & 1) == 0) candle_wave1 += pseudo_rand() & 1; + // wave2: medium-speed erratic LFO + candle_wave2 += candle_wave2_speed; + // wave3: erratic fast wave + candle_wave3 += pseudo_rand() % 37; + // S&H on wave2 frequency to make it more erratic + if ((pseudo_rand() & 0b00111111) == 0) + candle_wave2_speed = pseudo_rand() % 13; + // downward sawtooth on wave2 depth to simulate stabilizing + if ((candle_wave2_depth > 0) && ((pseudo_rand() & 0b00111111) == 0)) + candle_wave2_depth --; + // random sawtooth retrigger + if (pseudo_rand() == 0) { + // random amplitude + //candle_wave2_depth = 2 + (pseudo_rand() % ((CANDLE_WAVE2_MAXDEPTH * CANDLE_AMPLITUDE / 100) - 2)); + candle_wave2_depth = pseudo_rand() % (CANDLE_WAVE2_MAXDEPTH * CANDLE_AMPLITUDE / 100); + //candle_wave3_depth = 5; + candle_wave2 = 0; + } + // downward sawtooth on wave3 depth to simulate stabilizing + if ((candle_wave3_depth > 2) && ((pseudo_rand() & 0b00011111) == 0)) + candle_wave3_depth --; + if ((pseudo_rand() & 0b01111111) == 0) + // random amplitude + //candle_wave3_depth = 2 + (pseudo_rand() % ((CANDLE_WAVE3_MAXDEPTH * CANDLE_AMPLITUDE / 100) - 2)); + candle_wave3_depth = pseudo_rand() % (CANDLE_WAVE3_MAXDEPTH * CANDLE_AMPLITUDE / 100); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} +#endif // #ifdef USE_CANDLE_MODE + + +#ifdef USE_BORING_STROBE_STATE +uint8_t boring_strobe_state(Event event, uint16_t arg) { + // police strobe and SOS, meh + // 'st' reduces ROM size by avoiding access to a volatile var + // (maybe I should just make it nonvolatile?) + uint8_t st = boring_strobe_type; + + momentary_mode = 1; // 0 = ramping, 1 = strobes + + if (event == EV_enter_state) { + return EVENT_HANDLED; + } + // 1 click: off + else if (event == EV_1click) { + // reset to police strobe for next time + boring_strobe_type = 0; + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: rotate through strobe/flasher modes + else if (event == EV_2clicks) { + boring_strobe_type = (st + 1) % NUM_BORING_STROBES; + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + +#ifdef USE_POLICE_STROBE_MODE +inline void police_strobe_iter() { + // one iteration of main loop() + // flash at 16 Hz then 8 Hz, 8 times each + for (uint8_t del=41; del<100; del+=41) { + for (uint8_t f=0; f<8; f++) { + set_level(STROBE_BRIGHTNESS); + nice_delay_ms(del >> 1); + set_level(0); + nice_delay_ms(del); + } + } +} +#endif + +#ifdef USE_SOS_MODE +void sos_blink(uint8_t num, uint8_t dah) { + #define DIT_LENGTH 200 + for (; num > 0; num--) { + set_level(memorized_level); + nice_delay_ms(DIT_LENGTH); + if (dah) { // dah is 3X as long as a dit + nice_delay_ms(DIT_LENGTH*2); + } + set_level(0); + // one "off" dit between blinks + nice_delay_ms(DIT_LENGTH); + } + // three "off" dits (or one "dah") between letters + nice_delay_ms(DIT_LENGTH*2); +} + +inline void sos_mode_iter() { + // one iteration of main loop() + nice_delay_ms(1000); + sos_blink(3, 0); // S + sos_blink(3, 1); // O + sos_blink(3, 0); // S + nice_delay_ms(1000); +} +#endif // #ifdef USE_SOS_MODE +#endif // #ifdef USE_BORING_STROBE_STATE + + +#ifdef USE_BATTCHECK +uint8_t battcheck_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + #if defined(USE_GOODNIGHT_MODE) || defined(USE_BEACON_MODE) + // 2 clicks: next mode + else if (event == EV_2clicks) { + #ifdef USE_GOODNIGHT_MODE + set_state(goodnight_state, 0); + #elif defined(USE_BEACON_MODE) + set_state(beacon_state, 0); + #endif + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} +#endif + + +#ifdef USE_THERMAL_REGULATION +uint8_t tempcheck_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + #if 0 // not part of a loop in this UI + // 2 clicks: battcheck mode + else if (event == EV_2clicks) { + set_state(battcheck_state, 0); + return EVENT_HANDLED; + } + #endif + // 4 clicks: thermal config mode + else if (event == EV_4clicks) { + push_state(thermal_config_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} +#endif + + +#ifdef USE_BEACON_MODE +uint8_t beacon_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // TODO: use sleep ticks to measure time between pulses, + // to save power + // 2 clicks: tempcheck mode + else if (event == EV_2clicks) { + #ifdef USE_THERMAL_REGULATION + set_state(tempcheck_state, 0); + #else + set_state(battcheck_state, 0); + #endif + return EVENT_HANDLED; + } + // 4 clicks: beacon config mode + else if (event == EV_4clicks) { + push_state(beacon_config_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} +#endif // #ifdef USE_BEACON_MODE + + +#ifdef USE_GOODNIGHT_MODE +#define GOODNIGHT_TICKS_PER_STEPDOWN (GOODNIGHT_TIME*TICKS_PER_SECOND*60L/GOODNIGHT_LEVEL) +uint8_t goodnight_state(Event event, uint16_t arg) { + static uint16_t ticks_since_stepdown = 0; + // blink on start + if (event == EV_enter_state) { + ticks_since_stepdown = 0; + blink_confirm(2); + set_level(GOODNIGHT_LEVEL); + return EVENT_HANDLED; + } + // 1 click: off + else if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: beacon mode + else if (event == EV_2clicks) { + #ifdef USE_BEACON_MODE + set_state(beacon_state, 0); + #elif defined(USE_TEMPCHECK_MODE) + set_state(tempcheck_state, 0); + #endif + return EVENT_HANDLED; + } + // tick: step down (maybe) or off (maybe) + else if (event == EV_tick) { + if (++ticks_since_stepdown > GOODNIGHT_TICKS_PER_STEPDOWN) { + ticks_since_stepdown = 0; + set_level(actual_level-1); + if (! actual_level) { + #if 0 // test blink, to help measure timing + set_level(MAX_LEVEL>>2); + delay_4ms(8/2); + set_level(0); + #endif + set_state(off_state, 0); + } + } + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} +#endif + + +uint8_t lockout_state(Event event, uint16_t arg) { + #ifdef MOON_DURING_LOCKOUT_MODE + // momentary(ish) moon mode during lockout + // button is being held + if ((event & (B_CLICK | B_PRESS)) == (B_CLICK | B_PRESS)) { + #ifdef LOCKOUT_MOON_LOWEST + // Use lowest moon configured + uint8_t lvl = ramp_smooth_floor; + if (ramp_discrete_floor < lvl) lvl = ramp_discrete_floor; + set_level(lvl); + #elif defined(LOCKOUT_MOON_FANCY) + uint8_t levels[] = { ramp_smooth_floor, ramp_discrete_floor }; + if ((event & 0x0f) == 2) { + set_level(levels[ramp_style^1]); + } else { + set_level(levels[ramp_style]); + } + #else + // Use moon from current ramp + set_level(nearest_level(1)); + #endif + } + // button was released + else if ((event & (B_CLICK | B_PRESS)) == (B_CLICK)) { + set_level(0); + } + #endif + + // regular event handling + // conserve power while locked out + // (allow staying awake long enough to exit, but otherwise + // be persistent about going back to sleep every few seconds + // even if the user keeps pressing the button) + #ifdef USE_INDICATOR_LED + if (event == EV_enter_state) { + indicator_led(indicator_led_mode >> 2); + } else + #endif + if (event == EV_tick) { + if (arg > TICKS_PER_SECOND*2) { + go_to_standby = 1; + #ifdef USE_INDICATOR_LED + indicator_led(indicator_led_mode >> 2); + #endif + } + return EVENT_HANDLED; + } + #if defined(TICK_DURING_STANDBY) && defined(USE_INDICATOR_LED) + else if (event == EV_sleep_tick) { + if ((indicator_led_mode & 0b00001100) == 0b00001100) { + indicator_blink(arg); + } + return EVENT_HANDLED; + } + #endif + #ifdef USE_INDICATOR_LED + // 3 clicks: rotate through indicator LED modes (lockout mode) + else if (event == EV_3clicks) { + uint8_t mode = indicator_led_mode >> 2; + #ifdef TICK_DURING_STANDBY + mode = (mode + 1) & 3; + #else + mode = (mode + 1) % 3; + #endif + #ifdef INDICATOR_LED_SKIP_LOW + if (mode == 1) { mode ++; } + #endif + indicator_led_mode = (mode << 2) + (indicator_led_mode & 0x03); + indicator_led(mode); + save_config(); + return EVENT_HANDLED; + } + #if 0 // old method, deprecated in favor of "7 clicks from off" + // click, click, hold: rotate through indicator LED modes (off mode) + else if (event == EV_click3_hold) { + #ifndef USE_INDICATOR_LED_WHILE_RAMPING + // if main LED obscures aux LEDs, turn it off + set_level(0); + #endif + #ifdef TICK_DURING_STANDBY + uint8_t mode = (arg >> 5) & 3; + #else + uint8_t mode = (arg >> 5) % 3; + #endif + #ifdef INDICATOR_LED_SKIP_LOW + if (mode == 1) { mode ++; } + #endif + indicator_led_mode = (indicator_led_mode & 0b11111100) | mode; + #ifdef TICK_DURING_STANDBY + if (mode == 3) + indicator_led(mode & (arg&3)); + else + indicator_led(mode); + #else + indicator_led(mode); + #endif + //save_config(); + return EVENT_HANDLED; + } + // click, click, hold, release: save indicator LED mode (off mode) + else if (event == EV_click3_hold_release) { + save_config(); + return EVENT_HANDLED; + } + #endif + #endif + // 4 clicks: exit + else if (event == EV_4clicks) { + blink_confirm(1); + set_state(off_state, 0); + return EVENT_HANDLED; + } + + return EVENT_NOT_HANDLED; +} + + +uint8_t momentary_state(Event event, uint16_t arg) { + // TODO: momentary strobe here? (for light painting) + + // init strobe mode, if relevant + if ((event == EV_enter_state) && (momentary_mode == 1)) { + strobe_state(event, arg); + } + + // light up when the button is pressed; go dark otherwise + // button is being held + if ((event & (B_CLICK | B_PRESS)) == (B_CLICK | B_PRESS)) { + momentary_active = 1; + // 0 = ramping, 1 = strobes + if (momentary_mode == 0) { + set_level(memorized_level); + } + return EVENT_HANDLED; + } + // button was released + else if ((event & (B_CLICK | B_PRESS)) == (B_CLICK)) { + momentary_active = 0; + set_level(0); + //go_to_standby = 1; // sleep while light is off + return EVENT_HANDLED; + } + + // Sleep, dammit! (but wait a few seconds first) + // (because standby mode uses such little power that it can interfere + // with exiting via tailcap loosen+tighten unless you leave power + // disconnected for several seconds, so we want to be awake when that + // happens to speed up the process) + else if (event == EV_tick) { + if (momentary_active) { + // 0 = ramping, 1 = strobes + if (momentary_mode == 1) { + return strobe_state(event, arg); + } + } + else { + if (arg > TICKS_PER_SECOND*15) { // sleep after 15 seconds + go_to_standby = 1; // sleep while light is off + // TODO: lighted button should use lockout config? + } + } + return EVENT_HANDLED; + } + + return EVENT_NOT_HANDLED; +} + + +#ifdef USE_MUGGLE_MODE +uint8_t muggle_state(Event event, uint16_t arg) { + static int8_t ramp_direction; + static int8_t muggle_off_mode; + + // turn LED off when we first enter the mode + if (event == EV_enter_state) { + ramp_direction = 1; + + #ifdef START_AT_MEMORIZED_LEVEL + memorized_level = arg; + muggle_off_mode = 0; + set_level(memorized_level); + + if (! muggle_mode_active) { // don't write eeprom at every boot + muggle_mode_active = 1; + save_config(); + } + #else + muggle_mode_active = 1; + save_config(); + + muggle_off_mode = 1; + //memorized_level = MAX_1x7135; + memorized_level = (MUGGLE_FLOOR + MUGGLE_CEILING) / 2; + #endif + return EVENT_HANDLED; + } + // initial press: moon hint + else if (event == EV_click1_press) { + if (muggle_off_mode) + set_level(MUGGLE_FLOOR); + } + // initial release: direct to memorized level + else if (event == EV_click1_release) { + if (muggle_off_mode) + set_level(memorized_level); + } + // if the user keeps pressing, turn off + else if (event == EV_click2_press) { + muggle_off_mode = 1; + set_level(0); + } + // 1 click: on/off + else if (event == EV_1click) { + muggle_off_mode ^= 1; + if (muggle_off_mode) { + set_level(0); + } + /* + else { + set_level(memorized_level); + } + */ + return EVENT_HANDLED; + } + // hold: change brightness + else if (event == EV_click1_hold) { + // ramp at half speed + if (arg & 1) return EVENT_HANDLED; + + // if off, start at bottom + if (muggle_off_mode) { + muggle_off_mode = 0; + ramp_direction = 1; + set_level(MUGGLE_FLOOR); + } + else { + uint8_t m; + m = actual_level; + // ramp down if already at ceiling + if ((arg <= 1) && (m >= MUGGLE_CEILING)) ramp_direction = -1; + // ramp + m += ramp_direction; + if (m < MUGGLE_FLOOR) + m = MUGGLE_FLOOR; + if (m > MUGGLE_CEILING) + m = MUGGLE_CEILING; + memorized_level = m; + set_level(m); + } + return EVENT_HANDLED; + } + // reverse ramp direction on hold release + else if (event == EV_click1_hold_release) { + ramp_direction = -ramp_direction; + #ifdef START_AT_MEMORIZED_LEVEL + save_config_wl(); // momentary use should retain brightness level + #endif + return EVENT_HANDLED; + } + /* + // click, hold: change brightness (dimmer) + else if (event == EV_click2_hold) { + ramp_direction = 1; + if (memorized_level > MUGGLE_FLOOR) + memorized_level = actual_level - 1; + set_level(memorized_level); + return EVENT_HANDLED; + } + */ + // 6 clicks: exit muggle mode + else if (event == EV_6clicks) { + blink_confirm(1); + muggle_mode_active = 0; + save_config(); + set_state(off_state, 0); + return EVENT_HANDLED; + } + // tick: housekeeping + else if (event == EV_tick) { + // un-reverse after 1 second + if (arg == TICKS_PER_SECOND) ramp_direction = 1; + + // turn off, but don't go to the main "off" state + if (muggle_off_mode) { + if (arg > TICKS_PER_SECOND*1) { // sleep after 1 second + go_to_standby = 1; // sleep while light is off + } + } + return EVENT_HANDLED; + } + #ifdef USE_THERMAL_REGULATION + // overheating is handled specially in muggle mode + else if(event == EV_temperature_high) { + #if 0 + blip(); + #endif + // step down proportional to the amount of overheating + uint8_t new = actual_level - arg; + if (new < MUGGLE_FLOOR) { new = MUGGLE_FLOOR; } + set_level(new); + return EVENT_HANDLED; + } + #endif + // low voltage is handled specially in muggle mode + else if(event == EV_voltage_low) { + uint8_t lvl = (actual_level >> 1) + (actual_level >> 2); + if (lvl >= MUGGLE_FLOOR) { + set_level(lvl); + } else { + muggle_off_mode = 1; + } + return EVENT_HANDLED; + } + + return EVENT_NOT_HANDLED; +} +#endif + + +// ask the user for a sequence of numbers, then save them and return to caller +uint8_t config_state_base(Event event, uint16_t arg, + uint8_t num_config_steps, + void (*savefunc)()) { + static uint8_t config_step; + if (event == EV_enter_state) { + config_step = 0; + set_level(0); + return EVENT_HANDLED; + } + // advance forward through config steps + else if (event == EV_tick) { + if (config_step < num_config_steps) { + push_state(number_entry_state, config_step + 1); + } + else { + // TODO: blink out some sort of success pattern + savefunc(); + save_config(); + //set_state(retstate, retval); + pop_state(); + } + return EVENT_HANDLED; + } + // an option was set (return from number_entry_state) + else if (event == EV_reenter_state) { + config_state_values[config_step] = number_entry_value; + config_step ++; + return EVENT_HANDLED; + } + //return EVENT_NOT_HANDLED; + // eat all other events; don't pass any through to parent + return EVENT_HANDLED; +} + +#ifdef USE_RAMP_CONFIG +void ramp_config_save() { + // parse values + uint8_t val; + if (ramp_style) { // discrete / stepped ramp + + val = config_state_values[0]; + if (val) { ramp_discrete_floor = val; } + + val = config_state_values[1]; + if (val) { ramp_discrete_ceil = MAX_LEVEL + 1 - val; } + + val = config_state_values[2]; + if (val) ramp_discrete_steps = val; + + } else { // smooth ramp + + val = config_state_values[0]; + if (val) { ramp_smooth_floor = val; } + + val = config_state_values[1]; + if (val) { ramp_smooth_ceil = MAX_LEVEL + 1 - val; } + + } +} + +uint8_t ramp_config_state(Event event, uint16_t arg) { + uint8_t num_config_steps; + num_config_steps = 2 + ramp_style; + return config_state_base(event, arg, + num_config_steps, ramp_config_save); +} +#endif // #ifdef USE_RAMP_CONFIG + + +#ifdef USE_THERMAL_REGULATION +void thermal_config_save() { + // parse values + uint8_t val; + + // calibrate room temperature + val = config_state_values[0]; + if (val) { + int8_t rawtemp = temperature - therm_cal_offset; + therm_cal_offset = val - rawtemp; + reset_thermal_history = 1; // invalidate all recent temperature data + } + + val = config_state_values[1]; + if (val) { + // set maximum heat limit + therm_ceil = 30 + val - 1; + } + if (therm_ceil > MAX_THERM_CEIL) therm_ceil = MAX_THERM_CEIL; +} + +uint8_t thermal_config_state(Event event, uint16_t arg) { + return config_state_base(event, arg, + 2, thermal_config_save); +} +#endif // #ifdef USE_THERMAL_REGULATION + + +#ifdef USE_BEACON_MODE +void beacon_config_save() { + // parse values + uint8_t val = config_state_values[0]; + if (val) { + beacon_seconds = val; + } +} + +uint8_t beacon_config_state(Event event, uint16_t arg) { + return config_state_base(event, arg, + 1, beacon_config_save); +} + +inline void beacon_mode_iter() { + // one iteration of main loop() + set_level(memorized_level); + nice_delay_ms(100); + set_level(0); + nice_delay_ms(((beacon_seconds) * 1000) - 100); +} +#endif // #ifdef USE_BEACON_MODE + + +uint8_t number_entry_state(Event event, uint16_t arg) { + static uint8_t value; + static uint8_t blinks_left; + static uint8_t entry_step; + static uint16_t wait_ticks; + if (event == EV_enter_state) { + value = 0; + blinks_left = arg; + entry_step = 0; + wait_ticks = 0; + return EVENT_HANDLED; + } + // advance through the process: + // 0: wait a moment + // 1: blink out the 'arg' value + // 2: wait a moment + // 3: "buzz" while counting clicks + // 4: save and exit + else if (event == EV_tick) { + // wait a moment + if ((entry_step == 0) || (entry_step == 2)) { + if (wait_ticks < TICKS_PER_SECOND/2) + wait_ticks ++; + else { + entry_step ++; + wait_ticks = 0; + } + } + // blink out the option number + else if (entry_step == 1) { + if (blinks_left) { + if ((wait_ticks & 31) == 10) { + set_level(RAMP_SIZE/4); + } + else if ((wait_ticks & 31) == 20) { + set_level(0); + } + else if ((wait_ticks & 31) == 31) { + blinks_left --; + } + wait_ticks ++; + } + else { + entry_step ++; + wait_ticks = 0; + } + } + else if (entry_step == 3) { // buzz while waiting for a number to be entered + wait_ticks ++; + // buzz for N seconds after last event + if ((wait_ticks & 3) == 0) { + set_level(RAMP_SIZE/6); + } + else if ((wait_ticks & 3) == 2) { + set_level(RAMP_SIZE/8); + } + // time out after 3 seconds + if (wait_ticks > TICKS_PER_SECOND*3) { + //number_entry_value = value; + set_level(0); + entry_step ++; + } + } + else if (entry_step == 4) { + number_entry_value = value; + pop_state(); + } + return EVENT_HANDLED; + } + // count clicks + else if (event == EV_click1_release) { + empty_event_sequence(); + if (entry_step == 3) { // only count during the "buzz" + value ++; + wait_ticks = 0; + // flash briefly + set_level(RAMP_SIZE/2); + delay_4ms(8/2); + set_level(0); + } + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + + +// find the ramp level closest to the target, +// using only the levels which are allowed in the current state +uint8_t nearest_level(int16_t target) { + // bounds check + // using int16_t here saves us a bunch of logic elsewhere, + // by allowing us to correct for numbers < 0 or > 255 in one central place + uint8_t mode_min = ramp_smooth_floor; + uint8_t mode_max = ramp_smooth_ceil; + if (ramp_style) { + mode_min = ramp_discrete_floor; + mode_max = ramp_discrete_ceil; + } + if (target < mode_min) return mode_min; + if (target > mode_max) return mode_max; + // the rest isn't relevant for smooth ramping + if (! ramp_style) return target; + + uint8_t ramp_range = ramp_discrete_ceil - ramp_discrete_floor; + ramp_discrete_step_size = ramp_range / (ramp_discrete_steps-1); + uint8_t this_level = ramp_discrete_floor; + + for(uint8_t i=0; i>1)) + return this_level; + } + return this_level; +} + + +void blink_confirm(uint8_t num) { + for (; num>0; num--) { + set_level(MAX_LEVEL/4); + delay_4ms(10/4); + set_level(0); + delay_4ms(100/4); + } +} + +// Just go dark for a moment to indicate to user that something happened +void blip() { + uint8_t temp = actual_level; + set_level(0); + delay_4ms(3); + set_level(temp); +} + + +#if defined(USE_INDICATOR_LED) && defined(TICK_DURING_STANDBY) +// beacon-like mode for the indicator LED +void indicator_blink(uint8_t arg) { + #define USE_FANCIER_BLINKING_INDICATOR + #ifdef USE_FANCIER_BLINKING_INDICATOR + + // fancy blink, set off/low/high levels here: + uint8_t seq[] = {0, 1, 2, 1, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0}; + indicator_led(seq[arg & 15]); + + #else // basic blink, 1/8th duty cycle + + if (! (arg & 7)) { + indicator_led(2); + } + else { + indicator_led(0); + } + + #endif +} +#endif + + +void load_config() { + if (load_eeprom()) { + ramp_style = eeprom[ramp_style_e]; + #ifdef USE_RAMP_CONFIG + ramp_smooth_floor = eeprom[ramp_smooth_floor_e]; + ramp_smooth_ceil = eeprom[ramp_smooth_ceil_e]; + ramp_discrete_floor = eeprom[ramp_discrete_floor_e]; + ramp_discrete_ceil = eeprom[ramp_discrete_ceil_e]; + ramp_discrete_steps = eeprom[ramp_discrete_steps_e]; + #endif + #ifdef USE_TINT_RAMPING + tint = eeprom[tint_e]; + #endif + #if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) + strobe_type = eeprom[strobe_type_e]; // TODO: move this to eeprom_wl? + strobe_delays[0] = eeprom[strobe_delays_0_e]; + strobe_delays[1] = eeprom[strobe_delays_1_e]; + #endif + #ifdef USE_BIKE_FLASHER_MODE + bike_flasher_brightness = eeprom[bike_flasher_brightness_e]; + #endif + #ifdef USE_BEACON_MODE + beacon_seconds = eeprom[beacon_seconds_e]; + #endif + #ifdef USE_MUGGLE_MODE + muggle_mode_active = eeprom[muggle_mode_active_e]; + #endif + #ifdef USE_THERMAL_REGULATION + therm_ceil = eeprom[therm_ceil_e]; + therm_cal_offset = eeprom[therm_cal_offset_e]; + #endif + #ifdef USE_INDICATOR_LED + indicator_led_mode = eeprom[indicator_led_mode_e]; + #endif + } + #ifdef START_AT_MEMORIZED_LEVEL + if (load_eeprom_wl()) { + memorized_level = eeprom_wl[0]; + } + #endif +} + +void save_config() { + eeprom[ramp_style_e] = ramp_style; + #ifdef USE_RAMP_CONFIG + eeprom[ramp_smooth_floor_e] = ramp_smooth_floor; + eeprom[ramp_smooth_ceil_e] = ramp_smooth_ceil; + eeprom[ramp_discrete_floor_e] = ramp_discrete_floor; + eeprom[ramp_discrete_ceil_e] = ramp_discrete_ceil; + eeprom[ramp_discrete_steps_e] = ramp_discrete_steps; + #endif + #ifdef USE_TINT_RAMPING + eeprom[tint_e] = tint; + #endif + #if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) + eeprom[strobe_type_e] = strobe_type; // TODO: move this to eeprom_wl? + eeprom[strobe_delays_0_e] = strobe_delays[0]; + eeprom[strobe_delays_1_e] = strobe_delays[1]; + #endif + #ifdef USE_BIKE_FLASHER_MODE + eeprom[bike_flasher_brightness_e] = bike_flasher_brightness; + #endif + #ifdef USE_BEACON_MODE + eeprom[beacon_seconds_e] = beacon_seconds; + #endif + #ifdef USE_MUGGLE_MODE + eeprom[muggle_mode_active_e] = muggle_mode_active; + #endif + #ifdef USE_THERMAL_REGULATION + eeprom[therm_ceil_e] = therm_ceil; + eeprom[therm_cal_offset_e] = therm_cal_offset; + #endif + #ifdef USE_INDICATOR_LED + eeprom[indicator_led_mode_e] = indicator_led_mode; + #endif + + save_eeprom(); +} + +#ifdef START_AT_MEMORIZED_LEVEL +void save_config_wl() { + eeprom_wl[0] = memorized_level; + save_eeprom_wl(); +} +#endif + + +void low_voltage() { + StatePtr state = current_state; + + // TODO: turn off aux LED(s) when power is really low + + if (0) {} // placeholder + + #ifdef USE_STROBE_STATE + // "step down" from strobe to something low + else if (state == strobe_state) { + set_state(steady_state, RAMP_SIZE/6); + } + #endif + + // in normal or muggle mode, step down or turn off + //else if ((state == steady_state) || (state == muggle_state)) { + else if (state == steady_state) { + if (actual_level > 1) { + uint8_t lvl = (actual_level >> 1) + (actual_level >> 2); + set_level(lvl); + #ifdef USE_THERMAL_REGULATION + target_level = lvl; + #ifdef USE_SET_LEVEL_GRADUALLY + // not needed? + //set_level_gradually(lvl); + #endif + #endif + } + else { + set_state(off_state, 0); + } + } + // all other modes, just turn off when voltage is low + else { + set_state(off_state, 0); + } +} + + +void setup() { + #ifdef START_AT_MEMORIZED_LEVEL + // dual switch: e-switch + power clicky + // power clicky acts as a momentary mode + load_config(); + + #ifdef USE_MUGGLE_MODE + if (muggle_mode_active) + push_state(muggle_state, memorized_level); + else + #endif + if (button_is_pressed()) + // hold button to go to moon + push_state(steady_state, 1); + else + // otherwise use memory + push_state(steady_state, memorized_level); + + #else // if not START_AT_MEMORIZED_LEVEL + + // blink at power-on to let user know power is connected + set_level(RAMP_SIZE/8); + delay_4ms(3); + set_level(0); + + load_config(); + + #ifdef USE_TINT_RAMPING + // add tint ramping underneath every other state + push_state(tint_ramping_state, 0); + #endif // ifdef USE_TINT_RAMPING + + #ifdef USE_MUGGLE_MODE + if (muggle_mode_active) + push_state(muggle_state, (MUGGLE_FLOOR+MUGGLE_CEILING)/2); + else + #endif + push_state(off_state, 0); + + #endif // ifdef START_AT_MEMORIZED_LEVEL +} + + +void loop() { + + StatePtr state = current_state; + + if (0) {} + + #ifdef USE_STROBE_STATE + else if ((state == strobe_state) + || ((state == momentary_state) && (momentary_mode == 1) && (momentary_active)) ) { // also handle momentary strobes + uint8_t st = strobe_type; + + switch(st) { + #if defined(USE_PARTY_STROBE_MODE) || defined(USE_TACTICAL_STROBE_MODE) + #ifdef USE_PARTY_STROBE_MODE + case party_strobe_e: + #endif + #ifdef USE_TACTICAL_STROBE_MODE + case tactical_strobe_e: + #endif + party_tactical_strobe_mode_iter(st); + break; + #endif + + #ifdef USE_LIGHTNING_MODE + case lightning_storm_e: + lightning_storm_iter(); + break; + #endif + + #ifdef USE_BIKE_FLASHER_MODE + case bike_flasher_e: + bike_flasher_iter(); + break; + #endif + } + + } + #endif // #ifdef USE_STROBE_STATE + + #ifdef USE_BORING_STROBE_STATE + else if ((state == boring_strobe_state) + || ((state == momentary_state) && (momentary_mode == 1) && (momentary_active)) ) { // also handle momentary strobes + switch(boring_strobe_type) { + #ifdef USE_POLICE_STROBE_MODE + case 0: // police strobe + police_strobe_iter(); + break; + #endif + + #ifdef USE_SOS_MODE + default: // SOS + sos_mode_iter(); + break; + #endif + } + } + #endif // #ifdef USE_BORING_STROBE_STATE + + #ifdef USE_BATTCHECK + else if (state == battcheck_state) { + battcheck(); + } + #endif + + #ifdef USE_BEACON_MODE + else if (state == beacon_state) { + beacon_mode_iter(); + } + #endif + + #ifdef USE_THERMAL_REGULATION + // TODO: blink out therm_ceil during thermal_config_state? + else if (state == tempcheck_state) { + blink_num(temperature); + nice_delay_ms(1000); + } + #endif + + #ifdef USE_IDLE_MODE + else { + // doze until next clock tick + idle_mode(); + } + #endif + +} diff --git a/ui/meteor/meteor.c b/ui/meteor/meteor.c new file mode 100644 index 0000000..9c1c000 --- /dev/null +++ b/ui/meteor/meteor.c @@ -0,0 +1,556 @@ +/* + * Meteor: Meteor M43 clone UI for SpaghettiMonster. + * (in progress, not really in a usable state yet) + * + * 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 "hwdef-Emisar_D4.h" +#define USE_LVP +#define USE_THERMAL_REGULATION +#define DEFAULT_THERM_CEIL 45 +#define USE_RAMPING +#define RAMP_LENGTH 150 +#define USE_BATTCHECK +#define BATTCHECK_6bars +#define DONT_DELAY_AFTER_BATTCHECK +//#define USE_EEPROM +//#define EEPROM_BYTES 5 +#include "spaghetti-monster.h" + +// FSM states +uint8_t base_off_state(Event event, uint16_t arg); +uint8_t ui1_off_state(Event event, uint16_t arg); +uint8_t ui2_off_state(Event event, uint16_t arg); +uint8_t ui3_off_state(Event event, uint16_t arg); +uint8_t base_on_state(Event event, uint16_t arg, uint8_t *mode, uint8_t *group); +uint8_t ui1_on_state(Event event, uint16_t arg); +uint8_t ui2_on_state(Event event, uint16_t arg); +uint8_t ui3_on_state(Event event, uint16_t arg); +uint8_t beacon_state(Event event, uint16_t arg); +uint8_t battcheck_state(Event event, uint16_t arg); +uint8_t strobe_state(Event event, uint16_t arg); +uint8_t biking_state(Event event, uint16_t arg); +uint8_t lockout_state(Event event, uint16_t arg); +uint8_t momentary_state(Event event, uint16_t arg); +// Not a FSM state, just handles stuff common to all low/med/hi states +uint8_t any_mode_state(Event event, uint16_t arg, uint8_t *primary, uint8_t *secondary, uint8_t *modes); + +#ifdef USE_EEPROM +void load_config(); +void save_config(); +#endif + +// fixed output levels +uint8_t levels[] = {3, 16, 30, 43, 56, 70, 83, 96, 110, 123, 137, MAX_LEVEL}; +// select an interface +uint8_t UI = 1; // 1, 2, or 3 +// UI1 +uint8_t UI1_mode = 0; +uint8_t UI1_mode1 = 1; +uint8_t UI1_mode2 = 1; +uint8_t UI1_group1[] = {0, 2}; +uint8_t UI1_group2[] = {6, 9}; +// UI2 +uint8_t UI2_mode = 0; +uint8_t UI2_mode1 = 1; +uint8_t UI2_mode2 = 0; +uint8_t UI2_mode3 = 0; +uint8_t UI2_mode4 = 0; // doesn't matter, makes other code easier +uint8_t UI2_group1[] = { 0, 2}; // moon, low +uint8_t UI2_group2[] = { 4, 6}; // mid1, mid2 +uint8_t UI2_group3[] = { 8, 10}; // high1, high2 +uint8_t UI2_group4[] = {11, 11}; // turbo only +// UI3 can access all levels, with 3 different mode memory slots +uint8_t UI3_mode = 0; +uint8_t UI3_mode1 = 2; +uint8_t UI3_mode2 = 5; +uint8_t UI3_mode3 = 8; + +#ifdef USE_THERMAL_REGULATION +// brightness before thermal step-down +uint8_t target_level = 0; +#endif + +void set_any_mode(uint8_t mode, uint8_t *group) { + set_level(levels[group[mode]]); + #ifdef USE_THERMAL_REGULATION + target_level = actual_level; + #endif +} + +void blink_fast() { + set_level(MAX_LEVEL/2); + delay_4ms(8/4); + set_level(0); +} + +uint8_t base_off_state(Event event, uint16_t arg) { + // turn emitter off when entering state + if (event == EV_enter_state) { + set_level(0); + // sleep while off (lower power use) + go_to_standby = 1; + // ensure we're in a real off state, not the base + switch(UI) { + case 1: set_state(ui1_off_state, 0); break; + case 2: set_state(ui2_off_state, 0); break; + default: set_state(ui3_off_state, 0); break; + } + return EVENT_HANDLED; + } + // 3 clicks: strobe mode + else if (event == EV_3clicks) { + set_state(beacon_state, 0); + return EVENT_HANDLED; + } + // 4 clicks: battcheck mode + else if (event == EV_4clicks) { + set_state(battcheck_state, 0); + return EVENT_HANDLED; + } + // 5 clicks: battcheck mode + else if (event == EV_5clicks) { + set_state(biking_state, 0); + return EVENT_HANDLED; + } + // 6 clicks: soft lockout mode + else if (event == EV_6clicks) { + set_state(lockout_state, 0); + return EVENT_HANDLED; + } + // 9 clicks: activate UI1 + else if (event == EV_9clicks) { + blink_fast(); + set_state(ui1_off_state, 0); + return EVENT_HANDLED; + } + // 10 clicks: activate UI2 + else if (event == EV_10clicks) { + blink_fast(); + set_state(ui2_off_state, 0); + return EVENT_HANDLED; + } + // 11 clicks: activate UI3 + else if (event == EV_11clicks) { + blink_fast(); + set_state(ui3_off_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + +uint8_t ui1_off_state(Event event, uint16_t arg) { + UI = 1; + if (event == EV_enter_state) { + return EVENT_HANDLED; + } + // 1 click: low modes + if (event == EV_1click) { + set_any_mode(UI1_mode1, UI1_group1); + set_state(ui1_on_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: high modes + else if (event == EV_2clicks) { + set_any_mode(UI1_mode2, UI1_group2); + set_state(ui1_on_state, 1); + return EVENT_HANDLED; + } + // hold: turbo + else if (event == EV_hold) { + if (arg == 0) { + set_level(MAX_LEVEL); + } + //set_state(ui1_on_state, 3); + return EVENT_HANDLED; + } + // release hold: off + else if (event == EV_click1_hold_release) { + set_state(base_off_state, 0); + return EVENT_HANDLED; + } + return base_off_state(event, arg); +} + +uint8_t ui2_off_state(Event event, uint16_t arg) { + UI = 2; + if (event == EV_enter_state) { + return EVENT_HANDLED; + } + // 1 click: low modes + if (event == EV_1click) { + set_any_mode(UI2_mode1, UI2_group1); + set_state(ui2_on_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: high modes + else if (event == EV_2clicks) { + set_any_mode(UI2_mode3, UI2_group3); + set_state(ui2_on_state, 2); + return EVENT_HANDLED; + } + // hold: turbo + else if (event == EV_hold) { + if (arg == 0) { + set_level(MAX_LEVEL); + } + //set_state(ui1_on_state, 3); + return EVENT_HANDLED; + } + // release hold: off + else if (event == EV_click1_hold_release) { + set_state(base_off_state, 0); + return EVENT_HANDLED; + } + return base_off_state(event, arg); +} + +uint8_t ui3_off_state(Event event, uint16_t arg) { + UI = 3; + if (event == EV_enter_state) { + return EVENT_HANDLED; + } + // 1 click: memory slot 1 + if (event == EV_1click) { + set_level(levels[UI3_mode1]); + set_state(ui3_on_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: memory slot 2 + else if (event == EV_2clicks) { + set_level(levels[UI3_mode2]); + set_state(ui3_on_state, 1); + return EVENT_HANDLED; + } + // Click, hold: memory slot 3 + else if (event == EV_click2_hold) { + set_level(levels[UI3_mode3]); + set_state(ui3_on_state, 2); + return EVENT_HANDLED; + } + // hold: turbo + else if (event == EV_hold) { + if (arg == 0) { + set_level(MAX_LEVEL); + } + //set_state(ui1_on_state, 3); + return EVENT_HANDLED; + } + // release hold: off + else if (event == EV_click1_hold_release) { + set_state(base_off_state, 0); + return EVENT_HANDLED; + } + return base_off_state(event, arg); +} + +uint8_t base_on_state(Event event, uint16_t arg, uint8_t *mode, uint8_t *group) { + // 1 click: off + if (event == EV_1click) { + set_state(base_off_state, 0); + return EVENT_HANDLED; + } + #ifdef USE_THERMAL_REGULATION + // overheating: drop by an amount proportional to how far we are above the ceiling + else if (event == EV_temperature_high) { + if (actual_level > MAX_LEVEL/4) { + uint8_t stepdown = actual_level - arg; + if (stepdown < MAX_LEVEL/4) stepdown = MAX_LEVEL/4; + set_level(stepdown); + } + return EVENT_HANDLED; + } + // underheating: increase slowly if we're lower than the target + // (proportional to how low we are) + else if (event == EV_temperature_low) { + if (actual_level < target_level) { + uint8_t stepup = actual_level + (arg>>1); + if (stepup > target_level) stepup = target_level; + set_level(stepup); + } + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} + +uint8_t ui1_on_state(Event event, uint16_t arg) { + // turn on LED when entering the mode + static uint8_t *mode = &UI1_mode1; + static uint8_t *group = UI1_group1; + if (event == EV_enter_state) { + UI1_mode = arg; + } + if (UI1_mode == 0) { + mode = &UI1_mode1; + group = UI1_group1; + } + else { + mode = &UI1_mode2; + group = UI1_group2; + } + + if (event == EV_enter_state) { + set_any_mode(*mode, group); + return EVENT_HANDLED; + } + // 2 clicks: toggle moon/low or mid/high + else if (event == EV_2clicks) { + *mode ^= 1; + set_any_mode(*mode, group); + return EVENT_HANDLED; + } + // hold: turbo + else if (event == EV_hold) { + if (arg == 0) set_level(MAX_LEVEL); + return EVENT_HANDLED; + } + // release: exit turbo + else if (event == EV_click1_hold_release) { + set_any_mode(*mode, group); + return EVENT_HANDLED; + } + return base_on_state(event, arg, mode, group); +} + +uint8_t ui2_on_state(Event event, uint16_t arg) { + // turn on LED when entering the mode + static uint8_t *mode = &UI2_mode1; + static uint8_t *group = UI2_group1; + if (event == EV_enter_state) { + UI2_mode = arg; + } + switch (UI2_mode) { + case 0: + mode = &UI2_mode1; + group = UI2_group1; + break; + case 1: + mode = &UI2_mode2; + group = UI2_group2; + break; + case 2: + mode = &UI2_mode3; + group = UI2_group3; + break; + default: // turbo only + mode = &UI2_mode4; + group = UI2_group4; + break; + } + + if (event == EV_enter_state) { + set_any_mode(*mode, group); + return EVENT_HANDLED; + } + // 2 clicks: toggle moon/low, mid1/mid2, or high1/high2 + else if (event == EV_2clicks) { + *mode ^= 1; + set_any_mode(*mode, group); + return EVENT_HANDLED; + } + // hold: rotate through low/mid/high/turbo + else if (event == EV_hold) { + if (arg % HOLD_TIMEOUT == 0) { + UI2_mode = (UI2_mode + 1) & 3; + } + else if (arg % HOLD_TIMEOUT == 1) { + set_any_mode(*mode, group); + } + return EVENT_HANDLED; + } + return base_on_state(event, arg, mode, group); +} + +uint8_t ui3_on_state(Event event, uint16_t arg) { + // turn on LED when entering the mode + static uint8_t *mode = &UI3_mode1; + if (event == EV_enter_state) { + UI3_mode = arg; + } + // 2 clicks: rotate through mode1/mode2/mode3 + else if (event == EV_2clicks) { + UI3_mode = (UI3_mode + 1) % 3; + } + // short click, long click: rotate through mode3/mode2/mode1 + /* + else if (event == EV_click1_hold) { + if (arg % HOLD_TIMEOUT == 0) + UI3_mode = (UI3_mode + 4) % 3; + } + */ + switch (UI3_mode) { + case 0: + mode = &UI3_mode1; + break; + case 1: + mode = &UI3_mode2; + break; + default: + mode = &UI3_mode3; + break; + } + + if ((event == EV_enter_state) || (event == EV_2clicks)) { + set_level(levels[*mode]); + return EVENT_HANDLED; + } + // short click, long click: rotate through mode3/mode2/mode1 + /* + else if (event == EV_click1_hold) { + set_level(levels[*mode]); + return EVENT_HANDLED; + } + */ + // hold: turbo + // Click, hold: ramp up + // release hold, hold again: ramp in opposite direction + return base_on_state(event, arg, mode, levels); +} + + +uint8_t blinky_base_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(base_off_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + +uint8_t beacon_state(Event event, uint16_t arg) { + return blinky_base_state(event, arg); +} + +uint8_t battcheck_state(Event event, uint16_t arg) { + return EVENT_NOT_HANDLED; +} + +uint8_t strobe_state(Event event, uint16_t arg) { + return blinky_base_state(event, arg); +} + +uint8_t biking_state(Event event, uint16_t arg) { + return blinky_base_state(event, arg); +} + +uint8_t lockout_state(Event event, uint16_t arg) { + return blinky_base_state(event, arg); +} + +uint8_t momentary_state(Event event, uint16_t arg) { + return blinky_base_state(event, arg); +} + + +void low_voltage() { + if ((current_state == ui1_on_state) || + (current_state == ui2_on_state) || + (current_state == ui3_on_state)) { + if (actual_level > 5) { + set_level(actual_level >> 1); + } + else { + set_state(base_off_state, 0); + } + } + /* + // "step down" from blinkies to low + else if (current_state == strobe_beacon_state) { + set_state(low_mode_state, 0); + } + */ +} + +void strobe(uint8_t level, uint16_t ontime, uint16_t offtime) { + set_level(level); + if (! nice_delay_ms(ontime)) return; + set_level(0); + nice_delay_ms(offtime); +} + +#ifdef USE_EEPROM +void load_config() { + if (load_eeprom()) { + H1 = !(!(eeprom[0] & 0b00000100)); + M1 = !(!(eeprom[0] & 0b00000010)); + L1 = !(!(eeprom[0] & 0b00000001)); + H2 = eeprom[1]; + M2 = eeprom[2]; + L2 = eeprom[3]; + strobe_beacon_mode = eeprom[4]; + } +} + +void save_config() { + eeprom[0] = (H1<<2) | (M1<<1) | (L1); + eeprom[1] = H2; + eeprom[2] = M2; + eeprom[3] = L2; + eeprom[4] = strobe_beacon_mode; + + save_eeprom(); +} +#endif + +void setup() { + set_level(RAMP_SIZE/8); + delay_4ms(3); + set_level(0); + + #ifdef USE_EEPROM + load_config(); + #endif + + push_state(base_off_state, 0); +} + +void loop() { + if (0) {} + /* + if (current_state == strobe_beacon_state) { + switch(strobe_beacon_mode) { + // 0.2 Hz beacon at L1 + case 0: + strobe(low_modes[0], 500, 4500); + break; + // 0.2 Hz beacon at H1 + case 1: + strobe(hi_modes[0], 500, 4500); + break; + // 4 Hz tactical strobe at H1 + case 2: + strobe(hi_modes[0], 83, 167); + break; + // 19 Hz tactical strobe at H1 + case 3: + strobe(hi_modes[0], 17, 35); + break; + } + } + */ + + #ifdef USE_BATTCHECK + else if (current_state == battcheck_state) { + nice_delay_ms(500); // wait a moment to measure voltage + battcheck(); + set_state(base_off_state, 0); + } + #endif +} + + diff --git a/ui/momentary/momentary.c b/ui/momentary/momentary.c new file mode 100644 index 0000000..c7a8cf1 --- /dev/null +++ b/ui/momentary/momentary.c @@ -0,0 +1,80 @@ +/* + * 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 . + */ + +#include "hwdef-Emisar_D4.h" +#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(Event event, uint16_t arg) { + + if (event == EV_click1_press) { + brightness = 255; + light_on(); + empty_event_sequence(); // don't attempt to parse multiple clicks + return 0; + } + + else if (event == EV_release) { + light_off(); + empty_event_sequence(); // don't attempt to parse multiple clicks + go_to_standby = 1; // sleep while light is off + return 0; + } + + return 1; // event not handled +} + +// LVP / low-voltage protection +void low_voltage() { + if (brightness > 0) { + debug_blink(3); + brightness >>= 1; + if (on_now) light_on(); + } else { + debug_blink(8); + light_off(); + go_to_standby = 1; + } +} + +void setup() { + debug_blink(2); + push_state(momentary_state, 0); +} + +void loop() { } + diff --git a/ui/ramping-ui/ramping-ui.c b/ui/ramping-ui/ramping-ui.c new file mode 100644 index 0000000..583498a --- /dev/null +++ b/ui/ramping-ui/ramping-ui.c @@ -0,0 +1,359 @@ +/* + * Ramping-UI: Ramping UI for 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 "hwdef-Emisar_D4.h" +#define USE_LVP +#define USE_THERMAL_REGULATION +#define DEFAULT_THERM_CEIL 32 +#define USE_DELAY_MS +#define USE_DELAY_ZERO +#define USE_RAMPING +#define USE_BATTCHECK +#define BATTCHECK_VpT +#define RAMP_LENGTH 150 +#include "spaghetti-monster.h" + +// FSM states +uint8_t off_state(Event event, uint16_t arg); +uint8_t steady_state(Event event, uint16_t arg); +uint8_t strobe_state(Event event, uint16_t arg); +#ifdef USE_BATTCHECK +uint8_t battcheck_state(Event event, uint16_t arg); +uint8_t tempcheck_state(Event event, uint16_t arg); +#endif + +// brightness control +uint8_t memorized_level = MAX_1x7135; +// smooth vs discrete ramping +uint8_t ramp_step_size = 1; + +#ifdef USE_THERMAL_REGULATION +// brightness before thermal step-down +uint8_t target_level = 0; +#endif + +// strobe timing +volatile uint8_t strobe_delay = 67; +volatile uint8_t strobe_type = 0; // 0 == party strobe, 1 == tactical strobe + + +uint8_t off_state(Event event, uint16_t arg) { + // turn emitter off when entering state + if (event == EV_enter_state) { + set_level(0); + // sleep while off (lower power use) + go_to_standby = 1; + return EVENT_HANDLED; + } + // hold (initially): go to lowest level, but allow abort for regular click + else if (event == EV_click1_press) { + set_level(1); + return EVENT_HANDLED; + } + // 1 click (before timeout): go to memorized level, but allow abort for double click + else if (event == EV_click1_release) { + set_level(memorized_level); + return EVENT_HANDLED; + } + // 1 click: regular mode + else if (event == EV_1click) { + set_state(steady_state, memorized_level); + return EVENT_HANDLED; + } + // 2 clicks (initial press): off, to prep for later events + else if (event == EV_click2_press) { + set_level(0); + return EVENT_HANDLED; + } + // 2 clicks: highest mode + else if (event == EV_2clicks) { + set_state(steady_state, MAX_LEVEL); + return EVENT_HANDLED; + } + // 3 clicks: strobe mode + else if (event == EV_3clicks) { + set_state(strobe_state, 0); + return EVENT_HANDLED; + } + #ifdef USE_BATTCHECK + // 4 clicks: battcheck mode + else if (event == EV_4clicks) { + set_state(battcheck_state, 0); + return EVENT_HANDLED; + } + #endif + // hold: go to lowest level + else if (event == EV_click1_hold) { + // don't start ramping immediately; + // give the user time to release at moon level + if (arg >= HOLD_TIMEOUT) + set_state(steady_state, 1); + return EVENT_HANDLED; + } + // hold, release quickly: go to lowest level + else if (event == EV_click1_hold_release) { + set_state(steady_state, 1); + return EVENT_HANDLED; + } + // click, hold: go to highest level (for ramping down) + else if (event == EV_click2_hold) { + set_state(steady_state, MAX_LEVEL); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + + +uint8_t steady_state(Event event, uint16_t arg) { + // turn LED on when we first enter the mode + if (event == EV_enter_state) { + // remember this level, unless it's moon or turbo + if ((arg > 1) && (arg < MAX_LEVEL)) + memorized_level = arg; + // use the requested level even if not memorized + #ifdef USE_THERMAL_REGULATION + target_level = arg; + #endif + set_level(arg); + return EVENT_HANDLED; + } + // 1 click: off + else if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: go to/from highest level + else if (event == EV_2clicks) { + if (actual_level < MAX_LEVEL) { + memorized_level = actual_level; // in case we're on moon + #ifdef USE_THERMAL_REGULATION + target_level = MAX_LEVEL; + #endif + set_level(MAX_LEVEL); + } + else { + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + set_level(memorized_level); + } + return EVENT_HANDLED; + } + // 3 clicks: go to strobe modes + else if (event == EV_3clicks) { + set_state(strobe_state, 0); + return EVENT_HANDLED; + } + // 4 clicks: toggle smooth vs discrete ramping + else if (event == EV_4clicks) { + if (ramp_step_size == 1) ramp_step_size = MAX_LEVEL/6; + else ramp_step_size = 1; + set_level(0); + delay_4ms(20/4); + set_level(memorized_level); + return EVENT_HANDLED; + } + // hold: change brightness (brighter) + else if (event == EV_click1_hold) { + // ramp slower in discrete mode + if (arg % ramp_step_size != 0) { + return EVENT_HANDLED; + } + // FIXME: make it ramp down instead, if already at max + if (actual_level + ramp_step_size < MAX_LEVEL) + memorized_level = actual_level + ramp_step_size; + else memorized_level = MAX_LEVEL; + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + // only blink once for each threshold + if ((memorized_level != actual_level) + && ((memorized_level == MAX_1x7135) + || (memorized_level == MAX_LEVEL))) { + set_level(0); + delay_4ms(8/4); + } + set_level(memorized_level); + return EVENT_HANDLED; + } + // click, hold: change brightness (dimmer) + else if (event == EV_click2_hold) { + // ramp slower in discrete mode + if (arg % ramp_step_size != 0) { + return EVENT_HANDLED; + } + // FIXME: make it ramp up instead, if already at min + if (actual_level > ramp_step_size) + memorized_level = (actual_level-ramp_step_size); + else + memorized_level = 1; + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + // only blink once for each threshold + if ((memorized_level != actual_level) + && ((memorized_level == MAX_1x7135) + || (memorized_level == 1))) { + set_level(0); + delay_4ms(8/4); + } + set_level(memorized_level); + return EVENT_HANDLED; + } + #ifdef USE_THERMAL_REGULATION + // TODO: test this on a real light + // overheating: drop by an amount proportional to how far we are above the ceiling + else if (event == EV_temperature_high) { + if (actual_level > MAX_LEVEL/4) { + uint8_t stepdown = actual_level - arg; + if (stepdown < MAX_LEVEL/4) stepdown = MAX_LEVEL/4; + set_level(stepdown); + } + return EVENT_HANDLED; + } + // underheating: increase slowly if we're lower than the target + // (proportional to how low we are) + else if (event == EV_temperature_low) { + if (actual_level < target_level) { + uint8_t stepup = actual_level + (arg>>1); + if (stepup > target_level) stepup = target_level; + set_level(stepup); + } + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} + + +uint8_t strobe_state(Event event, uint16_t arg) { + if (event == EV_enter_state) { + return EVENT_HANDLED; + } + // 1 click: off + else if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: toggle party strobe vs tactical strobe + else if (event == EV_2clicks) { + strobe_type ^= 1; + return EVENT_HANDLED; + } + // 3 clicks: go back to regular modes + else if (event == EV_3clicks) { + set_state(steady_state, memorized_level); + return EVENT_HANDLED; + } + // hold: change speed (go faster) + else if (event == EV_click1_hold) { + if ((arg & 1) == 0) { + if (strobe_delay > 8) strobe_delay --; + } + return EVENT_HANDLED; + } + // click, hold: change speed (go slower) + else if (event == EV_click2_hold) { + if ((arg & 1) == 0) { + if (strobe_delay < 255) strobe_delay ++; + } + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + + +#ifdef USE_BATTCHECK +uint8_t battcheck_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: tempcheck mode + else if (event == EV_2clicks) { + set_state(tempcheck_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + +uint8_t tempcheck_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} +#endif + + +void low_voltage() { + // "step down" from strobe to something low + if (current_state == strobe_state) { + set_state(steady_state, RAMP_SIZE/6); + } + // in normal mode, step down by half or turn off + else if (current_state == steady_state) { + if (actual_level > 1) { + set_level(actual_level >> 1); + } + else { + set_state(off_state, 0); + } + } + // all other modes, just turn off when voltage is low + else { + set_state(off_state, 0); + } +} + + +void setup() { + set_level(RAMP_SIZE/8); + delay_4ms(3); + set_level(0); + + push_state(off_state, 0); +} + + +void loop() { + if (current_state == strobe_state) { + set_level(MAX_LEVEL); + if (strobe_type == 0) { // party strobe + if (strobe_delay < 30) delay_zero(); + else delay_ms(1); + } else { //tactical strobe + nice_delay_ms(strobe_delay >> 1); + } + set_level(0); + nice_delay_ms(strobe_delay); + } + #ifdef USE_BATTCHECK + else if (current_state == battcheck_state) { + battcheck(); + } + else if (current_state == tempcheck_state) { + blink_num(temperature); + nice_delay_ms(1000); + } + #endif +} diff --git a/ui/rampingios/Makefile b/ui/rampingios/Makefile new file mode 100644 index 0000000..8db198e --- /dev/null +++ b/ui/rampingios/Makefile @@ -0,0 +1,7 @@ +all: + ./build-all.sh + +clean: + rm -f *.hex cfg-*.h *~ *.elf *.o + +.phony: clean diff --git a/ui/rampingios/build-all.sh b/ui/rampingios/build-all.sh new file mode 100755 index 0000000..106dc15 --- /dev/null +++ b/ui/rampingios/build-all.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +cp -av ../anduril/cfg-emisar*.h . + +UI=rampingiosv3 + +for TARGET in cfg-*.h ; do + NAME=$(echo "$TARGET" | perl -ne '/cfg-(.*).h/ && print "$1\n";') + echo "===== $NAME =====" + echo ../../../bin/build.sh 85 "$UI" "-DCONFIGFILE=${TARGET}" + ../../../bin/build.sh 85 "$UI" "-DCONFIGFILE=${TARGET}" + mv -f "$UI".hex "$UI".$NAME.hex +done diff --git a/ui/rampingios/rampingios-v3.html b/ui/rampingios/rampingios-v3.html new file mode 100644 index 0000000..f72d1ec --- /dev/null +++ b/ui/rampingios/rampingios-v3.html @@ -0,0 +1,501 @@ + + + + + + + RampingIOS V3 Manual :: Phil! Gold + + + + + + + + + + + + + + + + + + + +
+

Tue, 28 Aug 2018

+ +
+

RampingIOS V3 Manual

+ +
+ +

+ + + + +
RampingIOS V3 UI diagram
+

+ +

The Emisar D4S flashlights use a firmware named RampingIOS +V3. (The Emisar D4, D1, and D1S +all use RampingIOS V2.) There's not really a manual; the +only thing we get is the diagram on the right. It's reasonably +comprehensive, but there's a fair amount of detail it merely summarizes, +so I thought a textual manual would be nice.

+ +

The Emisar D4S only works when the head and tailcap are tightened fully. +You can physically lock it out--prevent it from turning on +accidentally--by simply loosening the tailcap a small amount. A quarter +turn will do it.

+ +

Emisar lights are known for their ramping interfaces. Rather than have a +small number of distinct brightness levels, they can vary their brightness +anywhere between their lowest and highest levels, like a light on a +dimmer. The D4S is in ramping mode by default, but it also has a stepped +mode that can be configured to be closer to how non-ramping lights work.

+ +

Each mode--ramping and stepped--can have differently-configured brightness +floors and ceilings.

+ +

The driver for the D4S has two different chipsets. At low brightness +levels, a fairly-efficient but low-power chipset (called a 7135) is +used. These lowest brightness levels are called the "regulated levels". +Each regulated level will always be the same brightness regardless of how +much charge the battery has. Above a particular brightness level, the +light switches over to a less-efficient but high-power chipset (called a +FET). These levels are called "direct-drive". The brightness of the +direct-drive levels is directly related to the battery's charge level; the +more charged the battery, the brighter the levels. The light is at its +most efficient, in terms of power used for every lumen generated, at the +brightest regulated level. When the light is first powered by tightening +the tailcap, it will default to this level.

+ +

At higher brightness levels, the light's LEDs generate a lot of heat. If +the light exceeds its configured maximum temperature, it will begin +dimming itself automatically until the temperature drops below the allowed +maximum.

+ +

The D4S has a set of cyan-colored auxiliary LEDs that can be on when the +main LEDs are off. You can configure the behavior of the aux LEDs.

+ +

Basic Usage

+ +

The default mode for the light is ramping mode. Triple-pressing the +button (3 clicks) while the light is on will toggle between ramping +and stepped mode.

+ +

While the light is off, press and release the button (1 click) to turn +it on. It will turn on at the last-used brightness level. (This is +called "mode memory".) Immediately after loosening and tightening the +tailcap (or after changing the battery), the memorized level will be the +light's max regulated level.

+ +

When the light is on, 1 click will turn it off. The current brightness +level will be memorized for future use. There's a fraction of a second +delay between pressing the button and the light actually turning off. +That's because of the way the light processes input; it's waiting to make +sure you're only going to press the button once (since multiple presses +will trigger other actions).

+ +

When the light is on, holding the button down will brighten the light. In +ramping mode, the brightness will increase gradually ("ramping up"). In +stepped mode, the light will jump through increasing brightness levels. +If you press, release, and then hold the button, it will begin dimming. +In ramping mode, the brightness will decrease gradually ("ramping +down"). In stepped mode, the light will jump through decreasing +brightness levels. While the light is changing, if you release the button +and immediately hold it again, the direction (dimming or brightening) will +switch.

+ +

In ramping mode, while the light is ramping, it'll briefly blink off and +on again at two different brightness levels: the maximum regulated level +and the brightness ceiling.

+ +

While the light is off, double-pressing the button (2 clicks) will +immediately jump to the brightness ceiling.

+ +

While the light is on, 2 clicks will jump to the maximum brightness +level, regardless of the configured brightness ceiling. Another two +clicks will go back to the previous brightness level.

+ +

While the light is off, if you hold the button the light will turn on at +its lowest level. If you continue holding the button, the light will +begin brightening from there.

+ +
Configuration Menus
+ +

The light has several different configuration modes. Each of those modes +works more or less the same way. The mode will have a series of menu +items that it will go through. For each menu item, the light will first +blink a number of times corresponding to the item number (first, second, +etc.) After that, the light will begin fluttering on and off fairly +quickly. While the light is fluttering, you can click the button a number +of times; the light will count the number of button presses and use that +number as its new configuration for that menu item. After a short period +of time, the fluttering will stop and the light will move on to the next +menu item. After the light has gone through all of the menu items, it +will return to whatever mode it was in before entering the configuration +mode.

+ +

If you don't press the button during a particular menu item's fluttering, +that item will remain unchanged.

+ +
Configuring the Basic Modes
+ +

While the light is on, 4 clicks will enter ramping or stepped +configuration mode, depending on which mode the light was in before the 4 +clicks.

+ +

For ramping mode, there are two menu options:

+ +
    +
  1. Brightness floor (default 1/150)
  2. +
  3. Brightness ceiling (default 150/150)
  4. +
+ +

During the floor configuration, press the button equal to the number of +ramping levels (out of 150) at which the floor should be. To set the +lowest possible floor, click the button once.

+ +

The ceiling is configured similarly, but you press the button equal to the +number of steps away from maximum brightness. To set the highest possible +ceiling (at max brightness), click the button once.

+ +

For stepped mode, there are three menu options:

+ +
    +
  1. Brightness floor (default 20/150)
  2. +
  3. Brightness ceiling (default 120/150)
  4. +
  5. Number of steps (default 7)
  6. +
+ +

Other Modes

+ +

The other modes largely involve multiple clicks from off. Most of them +are not generally needed for everyday use, but they supplement the light's +basic operations.

+ +
BattCheck/TempCheck Modes
+ +

From off, 3 clicks will enter "BattCheck" mode, which blinks out the +current battery voltage. First it blinks the number of volts, then it +pauses, then it blinks out the tenths of volts. Thus, if the battery were +at 3.5 volts, the light would blink three times, pause, then five times. +For zeroes, it gives a very short blink.

+ +

A fully-charged lithium-ion battery is 4.2 volts. The light considers 2.8 +volts to be an empty battery and won't turn on if the battery is at or +below 2.8 volts.

+ +

The voltage sequence will continue blinking until you turn off the light +with a single click.

+ +

While the light is in BattCheck mode, 2 clicks will enter TempCheck +mode. Instead of blinking out the battery voltage, the light will start +blinking out its current temperature in degrees Celsius, first the tens +digit then the units digit. Like BattCheck mode, the light will continue +blinking out the temperature until you turn it off with a single click.

+ +

While the light is in TempCheck mode, 4 clicks will enter thermal +configuration mode. See the thermal configuration mode documentation +below for how that works.

+ +
Tactical Mode
+ +

From off, 4 clicks will enter "tactical" or "momentary" mode. The +light will flash once to show that it's entered the mode. The auxiliary +LEDs will turn off (if they were on). In tactical mode, the light will +turn on at its memorized brightness for as long as the button is being +held down. It will turn off as soon as the button is released.

+ +

There's no button press combination that will exit tactical mode. To exit +it, you will have to partially unscrew and retighten the tailcap.

+ +
Lockout Mode
+ +

From off, 6 clicks will enter lockout mode. The light will flash +twice to show that it's entered the mode. There's a separate aux LED mode +for lockout mode, so you can tell whether the light is in lockout or not.

+ +

In lockout mode, pressing the button will turn on the light at its lowest +brightness ("moonlight mode") for as long as the button is held down.

+ +

Another 6 clicks will exit lockout mode. The light will flash twice to +show that it's left the mode.

+ +

While in lockout mode, 3 clicks will cycle through the various +settings for the aux LEDs in lockout mode. The four modes are, in order: +low, high, blink (on high), and off. The default mode is blink.

+ +

Remember that loosening the tailcap a quarter turn will also lock out the +light. Using the 6 clicks is called "electronic lockout", while turning +the tailcap is "physical lockout".

+ +
Aux LED Configuration
+ +

From off, 7 clicks will cycle to the next aux LED mode. The four +modes are, in order: low, high, blink (on high), and off. The default +mode is low.

+ +
Beacon Mode
+ +

From off, 8 clicks will enter beacon mode. In beacon mode, the light +will blink on and off every few seconds.

+ +

By default, the light will blink every two seconds. To change the timing, +use 4 clicks while in beacon mode. The light will enter a one-item +menu. During the flickering for input, press the button a number of times +equal to the number of seconds between blinks.

+ +

1 click will exit beacon mode.

+ +
Thermal Configuration Mode
+ +

From off, 10 clicks will enter thermal configuration mode.

+ +

The menu items here are:

+ +
    +
  1. Current temperature (every click is one degree Celsius)
  2. +
  3. Temperature ceiling (every click is one degree above 30°C)
  4. +
+ +

The "current temperature" item can be used to adjust the calibration of +the light's temperature sensor. To use it, make sure the light has been +off long enough that all of its components have cooled (or warmed) to the +ambient temperature. Check the ambient temperature using a thermometer +you trust. Go to thermal configuration mode, and enter the current +temperature by clicking the button a number of times equal to the +temperature in degrees Celsius. (If it's 22°C, click the button 22 +times.)

+ +

You can check the default calibration by entering TempCheck mode from a +room-temperature light. The D4Ss are supposed to go through a temperature +calibration at the factory, so hopefully most of them won't need manual +thermal calibration.

+ +

The temperature ceiling is simply the highest temperature the light should +be allowed to reach. Once it hits its temperature ceiling, it will +progressively dim itself until the temperature stabilizes below the +ceiling. Note that the number of clicks in that menu option is added to +30 to reach the actual ceiling. (Thus, you can't set a ceiling below +31°C.) The maximum allowed ceiling is 70°C.

+ +

The default temperature ceiling is 45°C.

+ + +
+ + + +

+
Phil! Gold
+ + + + diff --git a/ui/rampingios/rampingios-v3.md b/ui/rampingios/rampingios-v3.md new file mode 100644 index 0000000..bc0e2b7 --- /dev/null +++ b/ui/rampingios/rampingios-v3.md @@ -0,0 +1,262 @@ +RampingIOS V3 Manual + +This Markdown-formatted manual was contributed by phil_g under a +Creative Commons CC0 waiver: + http://aperiodic.net/phil/archives/Geekery/rampingios-v3.html + https://creativecommons.org/publicdomain/zero/1.0/ + + +
+ + + + +
RampingIOS V3 UI diagram
+
+ +The Emisar [D4S][emisar-d4s] flashlights use a firmware named RampingIOS +V3. (The Emisar [D4][emisar-d4], [D1][emisar-d1], and [D1S][emisar-d1s] +all use [RampingIOS V2][rampingios-v2].) There's not really a manual; the +only thing we get is the diagram on the right. It's reasonably +comprehensive, but there's a fair amount of detail it merely summarizes, +so I thought a textual manual would be nice. + + [emisar-d4]: https://intl-outdoor.com/emisar-d4-high-power-led-flashlight-p-921.html + [emisar-d1]: https://intl-outdoor.com/emisar-d1-mini-thrower-p-922.html + [emisar-d1s]: https://intl-outdoor.com/emisar-d1s-thrower-p-926.html + [emisar-d4s]: https://intl-outdoor.com/emisar-d4s-26650-high-power-led-flashlight-p-932.html + [rampingios-v2]: http://aperiodic.net/phil/archives/Geekery/rampingios-v2.html + +The Emisar D4S only works when the head and tailcap are tightened fully. +You can physically lock it out--prevent it from turning on +accidentally--by simply loosening the tailcap a small amount. A quarter +turn will do it. + +Emisar lights are known for their ramping interfaces. Rather than have a +small number of distinct brightness levels, they can vary their brightness +anywhere between their lowest and highest levels, like a light on a +dimmer. The D4S is in ramping mode by default, but it also has a stepped +mode that can be configured to be closer to how non-ramping lights work. + +Each mode--ramping and stepped--can have differently-configured brightness +floors and ceilings. + +The driver for the D4S has two different chipsets. At low brightness +levels, a fairly-efficient but low-power chipset (called a *7135*) is +used. These lowest brightness levels are called the "*regulated levels*". +Each regulated level will always be the same brightness regardless of how +much charge the battery has. Above a particular brightness level, the +light switches over to a less-efficient but high-power chipset (called a +*FET*). These levels are called "*direct-drive*". The brightness of the +direct-drive levels is directly related to the battery's charge level; the +more charged the battery, the brighter the levels. The light is at its +most efficient, in terms of power used for every lumen generated, at the +brightest regulated level. When the light is first powered by tightening +the tailcap, it will default to this level. + +At higher brightness levels, the light's LEDs generate a lot of heat. If +the light exceeds its configured maximum temperature, it will begin +dimming itself automatically until the temperature drops below the allowed +maximum. + +The D4S has a set of cyan-colored auxiliary LEDs that can be on when the +main LEDs are off. You can configure the behavior of the aux LEDs. + +#### Basic Usage + +The default mode for the light is ramping mode. Triple-pressing the +button (**3 clicks**) while the light is on will toggle between ramping +and stepped mode. + +While the light is off, press and release the button (**1 click**) to turn +it on. It will turn on at the last-used brightness level. (This is +called "*mode memory*".) Immediately after loosening and tightening the +tailcap (or after changing the battery), the memorized level will be the +light's max regulated level. + +When the light is on, 1 click will turn it off. The current brightness +level will be memorized for future use. There's a fraction of a second +delay between pressing the button and the light actually turning off. +That's because of the way the light processes input; it's waiting to make +sure you're only going to press the button once (since multiple presses +will trigger other actions). + +When the light is on, holding the button down will brighten the light. In +ramping mode, the brightness will increase gradually ("*ramping up*"). In +stepped mode, the light will jump through increasing brightness levels. +If you press, release, and then hold the button, it will begin dimming. +In ramping mode, the brightness will decrease gradually ("*ramping +down*"). In stepped mode, the light will jump through decreasing +brightness levels. While the light is changing, if you release the button +and immediately hold it again, the direction (dimming or brightening) will +switch. + +In ramping mode, while the light is ramping, it'll briefly blink off and +on again at two different brightness levels: the maximum regulated level +and the brightness ceiling. + +While the light is off, double-pressing the button (**2 clicks**) will +immediately jump to the brightness ceiling. + +While the light is on, **2 clicks** will jump to the maximum brightness +level, regardless of the configured brightness ceiling. Another two +clicks will go back to the previous brightness level. + +While the light is off, if you hold the button the light will turn on at +its lowest level. If you continue holding the button, the light will +begin brightening from there. + +##### Configuration Menus + +The light has several different configuration modes. Each of those modes +works more or less the same way. The mode will have a series of menu +items that it will go through. For each menu item, the light will first +blink a number of times corresponding to the item number (first, second, +etc.) After that, the light will begin fluttering on and off fairly +quickly. While the light is fluttering, you can click the button a number +of times; the light will count the number of button presses and use that +number as its new configuration for that menu item. After a short period +of time, the fluttering will stop and the light will move on to the next +menu item. After the light has gone through all of the menu items, it +will return to whatever mode it was in before entering the configuration +mode. + +If you don't press the button during a particular menu item's fluttering, +that item will remain unchanged. + +##### Configuring the Basic Modes + +While the light is on, **4 clicks** will enter ramping or stepped +configuration mode, depending on which mode the light was in before the 4 +clicks. + +For ramping mode, there are two menu options: + + 1. Brightness floor (default 1/150) + 2. Brightness ceiling (default 150/150) + +During the floor configuration, press the button equal to the number of +ramping levels (out of 150) at which the floor should be. To set the +lowest possible floor, click the button once. + +The ceiling is configured similarly, but you press the button equal to the +number of steps away from maximum brightness. To set the highest possible +ceiling (at max brightness), click the button once. + +For stepped mode, there are three menu options: + + 1. Brightness floor (default 20/150) + 2. Brightness ceiling (default 120/150) + 3. Number of steps (default 7) + +#### Other Modes + +The other modes largely involve multiple clicks from off. Most of them +are not generally needed for everyday use, but they supplement the light's +basic operations. + +##### BattCheck/TempCheck Modes + +From off, **3 clicks** will enter "BattCheck" mode, which blinks out the +current battery voltage. First it blinks the number of volts, then it +pauses, then it blinks out the tenths of volts. Thus, if the battery were +at 3.5 volts, the light would blink three times, pause, then five times. +For zeroes, it gives a very short blink. + +A fully-charged lithium-ion battery is 4.2 volts. The light considers 2.8 +volts to be an empty battery and won't turn on if the battery is at or +below 2.8 volts. + +The voltage sequence will continue blinking until you turn off the light +with a single click. + +While the light is in BattCheck mode, **2 clicks** will enter TempCheck +mode. Instead of blinking out the battery voltage, the light will start +blinking out its current temperature in degrees Celsius, first the tens +digit then the units digit. Like BattCheck mode, the light will continue +blinking out the temperature until you turn it off with a single click. + +While the light is in TempCheck mode, **4 clicks** will enter thermal +configuration mode. See the thermal configuration mode documentation +below for how that works. + +##### Tactical Mode + +From off, **4 clicks** will enter "tactical" or "momentary" mode. The +light will flash once to show that it's entered the mode. The auxiliary +LEDs will turn off (if they were on). In tactical mode, the light will +turn on at its memorized brightness for as long as the button is being +held down. It will turn off as soon as the button is released. + +There's no button press combination that will exit tactical mode. To exit +it, you will have to partially unscrew and retighten the tailcap. + +##### Lockout Mode + +From off, **6 clicks** will enter lockout mode. The light will flash +twice to show that it's entered the mode. There's a separate aux LED mode +for lockout mode, so you can tell whether the light is in lockout or not. + +In lockout mode, pressing the button will turn on the light at its lowest +brightness ("*moonlight mode*") for as long as the button is held down. + +Another 6 clicks will exit lockout mode. The light will flash twice to +show that it's left the mode. + +While in lockout mode, **3 clicks** will cycle through the various +settings for the aux LEDs in lockout mode. The four modes are, in order: +low, high, blink (on high), and off. The default mode is blink. + +Remember that loosening the tailcap a quarter turn will also lock out the +light. Using the 6 clicks is called "*electronic lockout*", while turning +the tailcap is "*physical lockout*". + +##### Aux LED Configuration + +From off, **7 clicks** will cycle to the next aux LED mode. The four +modes are, in order: low, high, blink (on high), and off. The default +mode is low. + +##### Beacon Mode + +From off, **8 clicks** will enter beacon mode. In beacon mode, the light +will blink on and off every few seconds. + +By default, the light will blink every two seconds. To change the timing, +use **4 clicks** while in beacon mode. The light will enter a one-item +menu. During the flickering for input, press the button a number of times +equal to the number of seconds between blinks. + +1 click will exit beacon mode. + +##### Thermal Configuration Mode + +From off, **10 clicks** will enter thermal configuration mode. + +The menu items here are: + + 1. Current temperature (every click is one degree Celsius) + 2. Temperature ceiling (every click is one degree *above 30°C*) + +The "current temperature" item can be used to adjust the calibration of +the light's temperature sensor. To use it, make sure the light has been +off long enough that all of its components have cooled (or warmed) to the +ambient temperature. Check the ambient temperature using a thermometer +you trust. Go to thermal configuration mode, and enter the current +temperature by clicking the button a number of times equal to the +temperature in degrees Celsius. (If it's 22°C, click the button 22 +times.) + +You can check the default calibration by entering TempCheck mode from a +room-temperature light. The D4Ss are supposed to go through a temperature +calibration at the factory, so hopefully most of them won't need manual +thermal calibration. + +The temperature ceiling is simply the highest temperature the light should +be allowed to reach. Once it hits its temperature ceiling, it will +progressively dim itself until the temperature stabilizes below the +ceiling. Note that the number of clicks in that menu option is added to +*30* to reach the actual ceiling. (Thus, you can't set a ceiling below +31°C.) The maximum allowed ceiling is 70°C. + +The default temperature ceiling is 45°C. diff --git a/ui/rampingios/rampingios-v3.txt b/ui/rampingios/rampingios-v3.txt new file mode 100644 index 0000000..4598a76 --- /dev/null +++ b/ui/rampingios/rampingios-v3.txt @@ -0,0 +1,324 @@ +RampingIOS V3 Manual +http://aperiodic.net/phil/archives/Geekery/rampingios-v3.html + +Tue, 28 Aug 2018 +9:47AM | Geekery | # + + +RampingIOS V3 Manual +-------------------- + +[rampingiosv3-ui.png] RampingIOS V3 UI diagram + +The Emisar D4S flashlights use a firmware named RampingIOS V3. (The Emisar D4, +D1, and D1S all use RampingIOS V2.) There's not really a manual; the only thing +we get is the diagram on the right. It's reasonably comprehensive, but there's +a fair amount of detail it merely summarizes, so I thought a textual manual +would be nice. + +The Emisar D4S only works when the head and tailcap are tightened fully. You +can physically lock it out--prevent it from turning on accidentally--by simply +loosening the tailcap a small amount. A quarter turn will do it. + +Emisar lights are known for their ramping interfaces. Rather than have a small +number of distinct brightness levels, they can vary their brightness anywhere +between their lowest and highest levels, like a light on a dimmer. The D4S is +in ramping mode by default, but it also has a stepped mode that can be +configured to be closer to how non-ramping lights work. + +Each mode--ramping and stepped--can have differently-configured brightness +floors and ceilings. + +The driver for the D4S has two different chipsets. At low brightness levels, a +fairly-efficient but low-power chipset (called a 7135) is used. These lowest +brightness levels are called the "regulated levels". Each regulated level will +always be the same brightness regardless of how much charge the battery has. +Above a particular brightness level, the light switches over to a +less-efficient but high-power chipset (called a FET). These levels are called " +direct-drive". The brightness of the direct-drive levels is directly related to +the battery's charge level; the more charged the battery, the brighter the +levels. The light is at its most efficient, in terms of power used for every +lumen generated, at the brightest regulated level. When the light is first +powered by tightening the tailcap, it will default to this level. + +At higher brightness levels, the light's LEDs generate a lot of heat. If the +light exceeds its configured maximum temperature, it will begin dimming itself +automatically until the temperature drops below the allowed maximum. + +The D4S has a set of cyan-colored auxiliary LEDs that can be on when the main +LEDs are off. You can configure the behavior of the aux LEDs. + + +Basic Usage +----------- + +The default mode for the light is ramping mode. Triple-pressing the button (3 +clicks) while the light is on will toggle between ramping and stepped mode. + +While the light is off, press and release the button (1 click) to turn it on. +It will turn on at the last-used brightness level. (This is called "mode memory +".) Immediately after loosening and tightening the tailcap (or after changing +the battery), the memorized level will be the light's max regulated level. + +When the light is on, 1 click will turn it off. The current brightness level +will be memorized for future use. There's a fraction of a second delay between +pressing the button and the light actually turning off. That's because of the +way the light processes input; it's waiting to make sure you're only going to +press the button once (since multiple presses will trigger other actions). + +When the light is on, holding the button down will brighten the light. In +ramping mode, the brightness will increase gradually ("ramping up"). In stepped +mode, the light will jump through increasing brightness levels. If you press, +release, and then hold the button, it will begin dimming. In ramping mode, the +brightness will decrease gradually ("ramping down"). In stepped mode, the light +will jump through decreasing brightness levels. While the light is changing, if +you release the button and immediately hold it again, the direction (dimming or +brightening) will switch. + +In ramping mode, while the light is ramping, it'll briefly blink off and on +again at two different brightness levels: the maximum regulated level and the +brightness ceiling. + +While the light is off, double-pressing the button (2 clicks) will immediately +jump to the brightness ceiling. + +While the light is on, 2 clicks will jump to the maximum brightness level, +regardless of the configured brightness ceiling. Another two clicks will go +back to the previous brightness level. + +While the light is off, if you hold the button the light will turn on at its +lowest level. If you continue holding the button, the light will begin +brightening from there. + + +Configuration Menus +------------------- + +The light has several different configuration modes. Each of those modes works +more or less the same way. The mode will have a series of menu items that it +will go through. For each menu item, the light will first blink a number of +times corresponding to the item number (first, second, etc.) After that, the +light will begin fluttering on and off fairly quickly. While the light is +fluttering, you can click the button a number of times; the light will count +the number of button presses and use that number as its new configuration for +that menu item. After a short period of time, the fluttering will stop and the +light will move on to the next menu item. After the light has gone through all +of the menu items, it will return to whatever mode it was in before entering +the configuration mode. + +If you don't press the button during a particular menu item's fluttering, that +item will remain unchanged. + + +Configuring the Basic Modes + +While the light is on, 4 clicks will enter ramping or stepped configuration +mode, depending on which mode the light was in before the 4 clicks. + +For ramping mode, there are two menu options: + + 1. Brightness floor (default 1/150) + 2. Brightness ceiling (default 150/150) + +During the floor configuration, press the button equal to the number of ramping +levels (out of 150) at which the floor should be. To set the lowest possible +floor, click the button once. + +The ceiling is configured similarly, but you press the button equal to the +number of steps away from maximum brightness. To set the highest possible +ceiling (at max brightness), click the button once. + +For stepped mode, there are three menu options: + + 1. Brightness floor (default 20/150) + 2. Brightness ceiling (default 120/150) + 3. Number of steps (default 7) + + +Other Modes +----------- + +The other modes largely involve multiple clicks from off. Most of them are not +generally needed for everyday use, but they supplement the light's basic +operations. + + +BattCheck/TempCheck Modes + +From off, 3 clicks will enter "BattCheck" mode, which blinks out the current +battery voltage. First it blinks the number of volts, then it pauses, then it +blinks out the tenths of volts. Thus, if the battery were at 3.5 volts, the +light would blink three times, pause, then five times. For zeroes, it gives a +very short blink. + +A fully-charged lithium-ion battery is 4.2 volts. The light considers 2.8 volts +to be an empty battery and won't turn on if the battery is at or below 2.8 +volts. + +The voltage sequence will continue blinking until you turn off the light with a +single click. + +While the light is in BattCheck mode, 2 clicks will enter TempCheck mode. +Instead of blinking out the battery voltage, the light will start blinking out +its current temperature in degrees Celsius, first the tens digit then the units +digit. Like BattCheck mode, the light will continue blinking out the +temperature until you turn it off with a single click. + +While the light is in TempCheck mode, 4 clicks will enter thermal configuration +mode. See the thermal configuration mode documentation below for how that +works. + + +Tactical Mode + +From off, 4 clicks will enter "tactical" or "momentary" mode. The light will +flash once to show that it's entered the mode. The auxiliary LEDs will turn off +(if they were on). In tactical mode, the light will turn on at its memorized +brightness for as long as the button is being held down. It will turn off as +soon as the button is released. + +There's no button press combination that will exit tactical mode. To exit it, +you will have to partially unscrew and retighten the tailcap. + + +Lockout Mode + +From off, 6 clicks will enter lockout mode. The light will flash twice to show +that it's entered the mode. There's a separate aux LED mode for lockout mode, +so you can tell whether the light is in lockout or not. + +In lockout mode, pressing the button will turn on the light at its lowest +brightness ("moonlight mode") for as long as the button is held down. + +Another 6 clicks will exit lockout mode. The light will flash twice to show +that it's left the mode. + +While in lockout mode, 3 clicks will cycle through the various settings for the +aux LEDs in lockout mode. The four modes are, in order: low, high, blink (on +high), and off. The default mode is blink. + +Remember that loosening the tailcap a quarter turn will also lock out the +light. Using the 6 clicks is called "electronic lockout", while turning the +tailcap is "physical lockout". + + +Aux LED Configuration + +From off, 7 clicks will cycle to the next aux LED mode. The four modes are, in +order: low, high, blink (on high), and off. The default mode is low. + + +Beacon Mode + +From off, 8 clicks will enter beacon mode. In beacon mode, the light will blink +on and off every few seconds. + +By default, the light will blink every two seconds. To change the timing, use 4 +clicks while in beacon mode. The light will enter a one-item menu. During the +flickering for input, press the button a number of times equal to the number of +seconds between blinks. + +1 click will exit beacon mode. + + +Thermal Configuration Mode + +From off, 10 clicks will enter thermal configuration mode. + +The menu items here are: + + 1. Current temperature (every click is one degree Celsius) + 2. Temperature ceiling (every click is one degree above 30?C) + +The "current temperature" item can be used to adjust the calibration of the +light's temperature sensor. To use it, make sure the light has been off long +enough that all of its components have cooled (or warmed) to the ambient +temperature. Check the ambient temperature using a thermometer you trust. Go to +thermal configuration mode, and enter the current temperature by clicking the +button a number of times equal to the temperature in degrees Celsius. (If it's +22?C, click the button 22 times.) + +You can check the default calibration by entering TempCheck mode from a +room-temperature light. The D4Ss are supposed to go through a temperature +calibration at the factory, so hopefully most of them won't need manual thermal +calibration. + +The temperature ceiling is simply the highest temperature the light should be +allowed to reach. Once it hits its temperature ceiling, it will progressively +dim itself until the temperature stabilizes below the ceiling. Note that the +number of clicks in that menu option is added to 30 to reach the actual +ceiling. (Thus, you can't set a ceiling below 31?C.) The maximum allowed +ceiling is 70?C. + +The default temperature ceiling is 45?C. + + +Static + + * zsh prompt + * PGP + * SSH + * MTA + * tutorials + * config files + * desktop + * books I own + * stuff I'm giving away + * Dr. Who eps I have + * bookmarks + * photos + * about + +Directory + + * Root (143) + + Books (32) + + Events (7) + o Burning Man (3) + o Camping (2) + o PDF (2) + + Geekery (36) + o Test (3) + + General (24) + + Links (12) + o Slashdot (1) + + MTA (22) + + Recipes (4) + + Video Games (6) + o FFXI (1) + +Archive + + ?August? +Sun Mon Tue Wed Thu Fri Sat + 1 2 3 4 +5 6 7 8 9 10 11 +12 13 14 15 16 17 18 +19 20 21 22 23 24 25 +26 27 28 29 30 31 + + ?2018? + Months +Jan Feb Mar Apr May Jun +Jul Aug Sep Oct Nov Dec + +Search + +[ ] +Powered by Google + +Currently Reading + +Recent Books + +------------------------------------------------------------------------------- + +Copyright (C) 2018 Phil Gold + +Back to main page. + + * Valid HTML 4.01 + * Valid CSS 2 + * RSS syndication + * Valid RSS 1.0 + diff --git a/ui/rampingios/rampingiosv3-ui.png b/ui/rampingios/rampingiosv3-ui.png new file mode 100644 index 0000000..d02dbf6 Binary files /dev/null and b/ui/rampingios/rampingiosv3-ui.png differ diff --git a/ui/rampingios/rampingiosv3.c b/ui/rampingios/rampingiosv3.c new file mode 100644 index 0000000..e990a5a --- /dev/null +++ b/ui/rampingios/rampingiosv3.c @@ -0,0 +1,1253 @@ +/* + * RampingIOS V3: FSM-based version of RampingIOS V2 UI, with upgrades. + * + * Copyright (C) 2018-2019 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 . + */ + +/********* User-configurable options *********/ +// Anduril config file name (set it here or define it at the gcc command line) +//#define CONFIGFILE cfg-emisar-d4s.h + +#define USE_LVP // FIXME: won't build when this option is turned off + +// parameters for this defined below or per-driver +#define USE_THERMAL_REGULATION +#define DEFAULT_THERM_CEIL 45 // try not to get hotter than this +#define USE_TENCLICK_THERMAL_CONFIG // ten clicks from off -> thermal config mode + +// short blip when crossing from "click" to "hold" from off +// (helps the user hit moon mode exactly, instead of holding too long +// or too short) +#define MOON_TIMING_HINT +// short blips while ramping +#define BLINK_AT_RAMP_MIDDLE +//#define BLINK_AT_RAMP_FLOOR +#define BLINK_AT_RAMP_CEILING +//#define BLINK_AT_STEPS // whenever a discrete ramp mode is passed in smooth mode + +// ramp down via regular button hold if a ramp-up ended <1s ago +// ("hold, release, hold" ramps down instead of up) +#define USE_REVERSING + +// battery readout style (pick one) +#define BATTCHECK_VpT +//#define BATTCHECK_8bars // FIXME: breaks build +//#define BATTCHECK_4bars // FIXME: breaks build + +// enable beacon mode +#define USE_BEACON_MODE + +// make the ramps configurable by the user +#define USE_RAMP_CONFIG + +/***** specific settings for known driver types *****/ +#include "tk.h" +#include incfile(CONFIGFILE) + + +// thermal properties, if not defined per-driver +#ifndef MIN_THERM_STEPDOWN +#define MIN_THERM_STEPDOWN MAX_1x7135 // lowest value it'll step down to +#endif +#ifndef THERM_FASTER_LEVEL + #ifdef MAX_Nx7135 + #define THERM_FASTER_LEVEL MAX_Nx7135 // throttle back faster when high + #else + #define THERM_FASTER_LEVEL (RAMP_SIZE*4/5) // throttle back faster when high + #endif +#endif +#ifdef USE_THERMAL_REGULATION +#define USE_SET_LEVEL_GRADUALLY // isn't used except for thermal adjustments +#endif + + +/********* Configure SpaghettiMonster *********/ +#define USE_DELAY_ZERO +#define USE_RAMPING +#ifndef RAMP_LENGTH +#define RAMP_LENGTH 150 // default, if not overridden in a driver cfg file +#endif +#define USE_BATTCHECK +#define USE_IDLE_MODE // reduce power use while awake and no tasks are pending +#define USE_DYNAMIC_UNDERCLOCKING // cut clock speed at very low modes for better efficiency + +// try to auto-detect how many eeprom bytes +#define USE_EEPROM +#define EEPROM_BYTES_BASE 7 + +#ifdef USE_INDICATOR_LED +#define EEPROM_INDICATOR_BYTES 1 +#else +#define EEPROM_INDICATOR_BYTES 0 +#endif + +#ifdef USE_THERMAL_REGULATION +#define EEPROM_THERMAL_BYTES 2 +#else +#define EEPROM_THERMAL_BYTES 0 +#endif + +#define EEPROM_BYTES (EEPROM_BYTES_BASE+EEPROM_INDICATOR_BYTES+EEPROM_THERMAL_BYTES) + + +#include "spaghetti-monster.h" + + +// FSM states +uint8_t off_state(Event event, uint16_t arg); +// simple numeric entry config menu +uint8_t config_state_base(Event event, uint16_t arg, + uint8_t num_config_steps, + void (*savefunc)()); +#define MAX_CONFIG_VALUES 3 +uint8_t config_state_values[MAX_CONFIG_VALUES]; +// ramping mode and its related config mode +uint8_t steady_state(Event event, uint16_t arg); +#ifdef USE_RAMP_CONFIG +uint8_t ramp_config_state(Event event, uint16_t arg); +#endif +#ifdef USE_BATTCHECK +uint8_t battcheck_state(Event event, uint16_t arg); +#endif +#ifdef USE_THERMAL_REGULATION +#define USE_BLINK_NUM +uint8_t tempcheck_state(Event event, uint16_t arg); +uint8_t thermal_config_state(Event event, uint16_t arg); +#endif +#ifdef USE_BEACON_MODE +// beacon mode and its related config mode +uint8_t beacon_state(Event event, uint16_t arg); +uint8_t beacon_config_state(Event event, uint16_t arg); +#endif +// soft lockout +#define MOON_DURING_LOCKOUT_MODE +// if enabled, 2nd lockout click goes to the other ramp's floor level +//#define LOCKOUT_MOON_FANCY +uint8_t lockout_state(Event event, uint16_t arg); +// momentary / signalling mode +uint8_t momentary_state(Event event, uint16_t arg); + +// general helper function for config modes +uint8_t number_entry_state(Event event, uint16_t arg); +// return value from number_entry_state() +volatile uint8_t number_entry_value; + +void blink_confirm(uint8_t num); +#if defined(USE_INDICATOR_LED) && defined(TICK_DURING_STANDBY) +void indicator_blink(uint8_t arg); +#endif + +// remember stuff even after battery was changed +void load_config(); +void save_config(); + +// default ramp options if not overridden earlier per-driver +#ifndef RAMP_SMOOTH_FLOOR + #define RAMP_SMOOTH_FLOOR 1 +#endif +#ifndef RAMP_SMOOTH_CEIL + #if PWM_CHANNELS == 3 + #define RAMP_SMOOTH_CEIL MAX_Nx7135 + #else + #define RAMP_SMOOTH_CEIL MAX_LEVEL - 30 + #endif +#endif +#ifndef RAMP_DISCRETE_FLOOR + #define RAMP_DISCRETE_FLOOR 20 +#endif +#ifndef RAMP_DISCRETE_CEIL + #define RAMP_DISCRETE_CEIL RAMP_SMOOTH_CEIL +#endif +#ifndef RAMP_DISCRETE_STEPS + #define RAMP_DISCRETE_STEPS 7 +#endif + +// mile marker(s) partway up the ramp +// default: blink only at border between regulated and FET +#ifdef BLINK_AT_RAMP_MIDDLE + #if PWM_CHANNELS >= 3 + #ifndef BLINK_AT_RAMP_MIDDLE_1 + #define BLINK_AT_RAMP_MIDDLE_1 MAX_Nx7135 + #ifndef BLINK_AT_RAMP_MIDDLE_2 + #define BLINK_AT_RAMP_MIDDLE_2 MAX_1x7135 + #endif + #endif + #else + #ifndef BLINK_AT_RAMP_MIDDLE_1 + #define BLINK_AT_RAMP_MIDDLE_1 MAX_1x7135 + #endif + #endif +#endif + +// brightness control +#ifndef DEFAULT_LEVEL +#define DEFAULT_LEVEL MAX_1x7135 +#endif +uint8_t memorized_level = DEFAULT_LEVEL; +// smooth vs discrete ramping +volatile uint8_t ramp_style = 0; // 0 = smooth, 1 = discrete +volatile uint8_t ramp_smooth_floor = RAMP_SMOOTH_FLOOR; +volatile uint8_t ramp_smooth_ceil = RAMP_SMOOTH_CEIL; +volatile uint8_t ramp_discrete_floor = RAMP_DISCRETE_FLOOR; +volatile uint8_t ramp_discrete_ceil = RAMP_DISCRETE_CEIL; +volatile uint8_t ramp_discrete_steps = RAMP_DISCRETE_STEPS; +uint8_t ramp_discrete_step_size; // don't set this + +#ifdef USE_INDICATOR_LED + // bits 2-3 control lockout mode + // bits 0-1 control "off" mode + // modes are: 0=off, 1=low, 2=high, 3=blinking (if TICK_DURING_STANDBY enabled) + #ifdef INDICATOR_LED_DEFAULT_MODE + uint8_t indicator_led_mode = INDICATOR_LED_DEFAULT_MODE; + #else + #ifdef USE_INDICATOR_LED_WHILE_RAMPING + //uint8_t indicator_led_mode = (1<<2) + 2; + uint8_t indicator_led_mode = (2<<2) + 1; + #else + uint8_t indicator_led_mode = (3<<2) + 1; + #endif + #endif +#endif + +// calculate the nearest ramp level which would be valid at the moment +// (is a no-op for smooth ramp, but limits discrete ramp to only the +// correct levels for the user's config) +uint8_t nearest_level(int16_t target); + +#ifdef USE_THERMAL_REGULATION +// brightness before thermal step-down +uint8_t target_level = 0; +#endif + +#ifdef USE_BEACON_MODE +// beacon timing +volatile uint8_t beacon_seconds = 2; +#endif + + +uint8_t off_state(Event event, uint16_t arg) { + // turn emitter off when entering state + if (event == EV_enter_state) { + set_level(0); + #ifdef USE_INDICATOR_LED + indicator_led(indicator_led_mode & 0x03); + #endif + // sleep while off (lower power use) + go_to_standby = 1; + return EVENT_HANDLED; + } + // go back to sleep eventually if we got bumped but didn't leave "off" state + else if (event == EV_tick) { + if (arg > TICKS_PER_SECOND*2) { + go_to_standby = 1; + #ifdef USE_INDICATOR_LED + indicator_led(indicator_led_mode & 0x03); + #endif + } + return EVENT_HANDLED; + } + #if defined(TICK_DURING_STANDBY) && defined(USE_INDICATOR_LED) + // blink the indicator LED, maybe + else if (event == EV_sleep_tick) { + if ((indicator_led_mode & 0b00000011) == 0b00000011) { + indicator_blink(arg); + } + return EVENT_HANDLED; + } + #endif + // hold (initially): go to lowest level (floor), but allow abort for regular click + else if (event == EV_click1_press) { + set_level(nearest_level(1)); + return EVENT_HANDLED; + } + // hold: go to lowest level + else if (event == EV_click1_hold) { + #ifdef MOON_TIMING_HINT + if (arg == 0) { + // let the user know they can let go now to stay at moon + uint8_t temp = actual_level; + set_level(0); + delay_4ms(3); + set_level(temp); + } else + #endif + // don't start ramping immediately; + // give the user time to release at moon level + //if (arg >= HOLD_TIMEOUT) { // smaller + if (arg >= (!ramp_style) * HOLD_TIMEOUT) { // more consistent + set_state(steady_state, 1); + } + return EVENT_HANDLED; + } + // hold, release quickly: go to lowest level (floor) + else if (event == EV_click1_hold_release) { + set_state(steady_state, 1); + return EVENT_HANDLED; + } + // 1 click (before timeout): go to memorized level, but allow abort for double click + else if (event == EV_click1_release) { + set_level(nearest_level(memorized_level)); + return EVENT_HANDLED; + } + // 1 click: regular mode + else if (event == EV_1click) { + set_state(steady_state, memorized_level); + return EVENT_HANDLED; + } + // click, hold: go to highest level (ceiling) (for ramping down) + else if (event == EV_click2_hold) { + set_state(steady_state, MAX_LEVEL); + return EVENT_HANDLED; + } + // 2 clicks: highest mode (ceiling) + else if (event == EV_2clicks) { + set_state(steady_state, MAX_LEVEL); + return EVENT_HANDLED; + } + // 3 clicks (initial press): off, to prep for later events + else if (event == EV_click3_press) { + set_level(0); + return EVENT_HANDLED; + } + #ifdef USE_BATTCHECK + // 3 clicks: battcheck mode / blinky mode group 1 + else if (event == EV_3clicks) { + set_state(battcheck_state, 0); + return EVENT_HANDLED; + } + #endif + // 4 clicks: momentary + else if (event == EV_4clicks) { + blink_confirm(1); + set_state(momentary_state, 0); + return EVENT_HANDLED; + } + // 6 clicks: lockout mode + else if (event == EV_6clicks) { + blink_confirm(2); + set_state(lockout_state, 0); + return EVENT_HANDLED; + } + #ifdef USE_INDICATOR_LED + // 7 clicks: next aux LED mode + else if (event == EV_7clicks) { + blink_confirm(1); + uint8_t mode = (indicator_led_mode & 3) + 1; + #ifdef TICK_DURING_STANDBY + mode = mode & 3; + #else + mode = mode % 3; + #endif + #ifdef INDICATOR_LED_SKIP_LOW + if (mode == 1) { mode ++; } + #endif + indicator_led_mode = (indicator_led_mode & 0b11111100) | mode; + indicator_led(mode); + save_config(); + return EVENT_HANDLED; + } + #endif + // 8 clicks: beacon mode + else if (event == EV_8clicks) { + set_state(beacon_state, 0); + return EVENT_HANDLED; + } + #ifdef USE_TENCLICK_THERMAL_CONFIG + // 10 clicks: thermal config mode + else if (event == EV_10clicks) { + push_state(thermal_config_state, 0); + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} + + +uint8_t steady_state(Event event, uint16_t arg) { + uint8_t mode_min = ramp_smooth_floor; + uint8_t mode_max = ramp_smooth_ceil; + uint8_t ramp_step_size = 1; + #ifdef USE_REVERSING + static int8_t ramp_direction = 1; + #endif + if (ramp_style) { + mode_min = ramp_discrete_floor; + mode_max = ramp_discrete_ceil; + ramp_step_size = ramp_discrete_step_size; + } + + // turn LED on when we first enter the mode + if ((event == EV_enter_state) || (event == EV_reenter_state)) { + // if we just got back from config mode, go back to memorized level + if (event == EV_reenter_state) { + arg = memorized_level; + } + // remember this level, unless it's moon or turbo + if ((arg > mode_min) && (arg < mode_max)) + memorized_level = arg; + // use the requested level even if not memorized + arg = nearest_level(arg); + #ifdef USE_THERMAL_REGULATION + target_level = arg; + #endif + set_level(arg); + #ifdef USE_REVERSING + ramp_direction = 1; + #endif + return EVENT_HANDLED; + } + // 1 click: off + else if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: go to/from highest level + else if (event == EV_2clicks) { + if (actual_level < MAX_LEVEL) { + #ifdef USE_THERMAL_REGULATION + target_level = MAX_LEVEL; + #endif + // true turbo, not the mode-specific ceiling + set_level(MAX_LEVEL); + } + else { + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + set_level(memorized_level); + } + return EVENT_HANDLED; + } + // 3 clicks: toggle smooth vs discrete ramping + else if (event == EV_3clicks) { + ramp_style = !ramp_style; + memorized_level = nearest_level(actual_level); + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #ifdef USE_SET_LEVEL_GRADUALLY + //set_level_gradually(lvl); + #endif + #endif + save_config(); + set_level(0); + delay_4ms(20/4); + set_level(memorized_level); + return EVENT_HANDLED; + } + #ifdef USE_RAMP_CONFIG + // 4 clicks: configure this ramp mode + else if (event == EV_4clicks) { + push_state(ramp_config_state, 0); + return EVENT_HANDLED; + } + #endif + // hold: change brightness (brighter) + else if (event == EV_click1_hold) { + // ramp slower in discrete mode + if (ramp_style && (arg % HOLD_TIMEOUT != 0)) { + return EVENT_HANDLED; + } + #ifdef USE_REVERSING + // make it ramp down instead, if already at max + if ((arg <= 1) && (actual_level >= mode_max)) { + ramp_direction = -1; + } + memorized_level = nearest_level((int16_t)actual_level \ + + (ramp_step_size * ramp_direction)); + #else + memorized_level = nearest_level((int16_t)actual_level + ramp_step_size); + #endif + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + #if defined(BLINK_AT_RAMP_CEILING) || defined(BLINK_AT_RAMP_MIDDLE) + // only blink once for each threshold + if ((memorized_level != actual_level) && ( + 0 // for easier syntax below + #ifdef BLINK_AT_RAMP_MIDDLE_1 + || (memorized_level == BLINK_AT_RAMP_MIDDLE_1) + #endif + #ifdef BLINK_AT_RAMP_MIDDLE_2 + || (memorized_level == BLINK_AT_RAMP_MIDDLE_2) + #endif + #ifdef BLINK_AT_RAMP_CEILING + || (memorized_level == mode_max) + #endif + #if defined(USE_REVERSING) && defined(BLINK_AT_RAMP_FLOOR) + || (memorized_level == mode_min) + #endif + )) { + set_level(0); + delay_4ms(8/4); + } + #endif + #if defined(BLINK_AT_STEPS) + uint8_t foo = ramp_style; + ramp_style = 1; + uint8_t nearest = nearest_level((int16_t)actual_level); + ramp_style = foo; + // only blink once for each threshold + if ((memorized_level != actual_level) && + (ramp_style == 0) && + (memorized_level == nearest) + ) + { + set_level(0); + delay_4ms(8/4); + } + #endif + set_level(memorized_level); + return EVENT_HANDLED; + } + #if defined(USE_REVERSING) + // reverse ramp direction on hold release + else if (event == EV_click1_hold_release) { + #ifdef USE_REVERSING + ramp_direction = -ramp_direction; + #endif + return EVENT_HANDLED; + } + #endif + // click, hold: change brightness (dimmer) + else if (event == EV_click2_hold) { + #ifdef USE_REVERSING + ramp_direction = 1; + #endif + // ramp slower in discrete mode + if (ramp_style && (arg % HOLD_TIMEOUT != 0)) { + return EVENT_HANDLED; + } + // TODO? make it ramp up instead, if already at min? + memorized_level = nearest_level((int16_t)actual_level - ramp_step_size); + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + #if defined(BLINK_AT_RAMP_FLOOR) || defined(BLINK_AT_RAMP_MIDDLE) + // only blink once for each threshold + if ((memorized_level != actual_level) && ( + 0 // for easier syntax below + #ifdef BLINK_AT_RAMP_MIDDLE_1 + || (memorized_level == BLINK_AT_RAMP_MIDDLE_1) + #endif + #ifdef BLINK_AT_RAMP_MIDDLE_2 + || (memorized_level == BLINK_AT_RAMP_MIDDLE_2) + #endif + #ifdef BLINK_AT_RAMP_FLOOR + || (memorized_level == mode_min) + #endif + )) { + set_level(0); + delay_4ms(8/4); + } + #endif + #if defined(BLINK_AT_STEPS) + uint8_t foo = ramp_style; + ramp_style = 1; + uint8_t nearest = nearest_level((int16_t)actual_level); + ramp_style = foo; + // only blink once for each threshold + if ((memorized_level != actual_level) && + (ramp_style == 0) && + (memorized_level == nearest) + ) + { + set_level(0); + delay_4ms(8/4); + } + #endif + set_level(memorized_level); + return EVENT_HANDLED; + } + #if defined(USE_SET_LEVEL_GRADUALLY) || defined(USE_REVERSING) + else if (event == EV_tick) { + #ifdef USE_REVERSING + // un-reverse after 1 second + if (arg == TICKS_PER_SECOND) ramp_direction = 1; + #endif + #ifdef USE_SET_LEVEL_GRADUALLY + // make thermal adjustment speed scale with magnitude + if ((arg & 1) && (actual_level < THERM_FASTER_LEVEL)) { + return EVENT_HANDLED; // adjust slower when not a high mode + } + #ifdef THERM_HARD_TURBO_DROP + else if ((! (actual_level < THERM_FASTER_LEVEL)) + && (actual_level > gradual_target)) { + gradual_tick(); + } + else { + #endif + // [int(62*4 / (x**0.8)) for x in (1,2,4,8,16,32,64,128)] + //uint8_t intervals[] = {248, 142, 81, 46, 26, 15, 8, 5}; + // [int(62*4 / (x**0.9)) for x in (1,2,4,8,16,32,64,128)] + //uint8_t intervals[] = {248, 132, 71, 38, 20, 10, 5, 3}; + // [int(62*4 / (x**0.95)) for x in (1,2,4,8,16,32,64,128)] + uint8_t intervals[] = {248, 128, 66, 34, 17, 9, 4, 2}; + uint8_t diff; + static uint8_t ticks_since_adjust = 0; + ticks_since_adjust ++; + if (gradual_target > actual_level) diff = gradual_target - actual_level; + else { + diff = actual_level - gradual_target; + } + uint8_t magnitude = 0; + #ifndef THERM_HARD_TURBO_DROP + // if we're on a really high mode, drop faster + if (actual_level >= THERM_FASTER_LEVEL) { magnitude ++; } + #endif + while (diff) { + magnitude ++; + diff >>= 1; + } + uint8_t ticks_per_adjust = intervals[magnitude]; + if (ticks_since_adjust > ticks_per_adjust) + { + gradual_tick(); + ticks_since_adjust = 0; + } + //if (!(arg % ticks_per_adjust)) gradual_tick(); + #ifdef THERM_HARD_TURBO_DROP + } + #endif + #endif + return EVENT_HANDLED; + } + #endif + #ifdef USE_THERMAL_REGULATION + // overheating: drop by an amount proportional to how far we are above the ceiling + else if (event == EV_temperature_high) { + #if 0 + uint8_t foo = actual_level; + set_level(0); + delay_4ms(2); + set_level(foo); + #endif + #ifdef THERM_HARD_TURBO_DROP + if (actual_level > THERM_FASTER_LEVEL) { + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(THERM_FASTER_LEVEL); + #else + set_level(THERM_FASTER_LEVEL); + #endif + target_level = THERM_FASTER_LEVEL; + } else + #endif + if (actual_level > MIN_THERM_STEPDOWN) { + int16_t stepdown = actual_level - arg; + if (stepdown < MIN_THERM_STEPDOWN) stepdown = MIN_THERM_STEPDOWN; + else if (stepdown > MAX_LEVEL) stepdown = MAX_LEVEL; + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(stepdown); + #else + set_level(stepdown); + #endif + } + return EVENT_HANDLED; + } + // underheating: increase slowly if we're lower than the target + // (proportional to how low we are) + else if (event == EV_temperature_low) { + #if 0 + uint8_t foo = actual_level; + set_level(0); + delay_4ms(2); + set_level(foo); + #endif + if (actual_level < target_level) { + //int16_t stepup = actual_level + (arg>>1); + int16_t stepup = actual_level + arg; + if (stepup > target_level) stepup = target_level; + else if (stepup < MIN_THERM_STEPDOWN) stepup = MIN_THERM_STEPDOWN; + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(stepup); + #else + set_level(stepup); + #endif + } + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} + + +#ifdef USE_BATTCHECK +uint8_t battcheck_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: tempcheck mode + else if (event == EV_2clicks) { + set_state(tempcheck_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} +#endif + + +#ifdef USE_THERMAL_REGULATION +uint8_t tempcheck_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 4 clicks: thermal config mode + else if (event == EV_4clicks) { + push_state(thermal_config_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} +#endif + + +#ifdef USE_BEACON_MODE +uint8_t beacon_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // TODO: use sleep ticks to measure time between pulses, + // to save power + // 4 clicks: beacon config mode + else if (event == EV_4clicks) { + push_state(beacon_config_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} +#endif // #ifdef USE_BEACON_MODE + + +uint8_t lockout_state(Event event, uint16_t arg) { + #ifdef MOON_DURING_LOCKOUT_MODE + // momentary(ish) moon mode during lockout + // button is being held + if ((event & (B_CLICK | B_PRESS)) == (B_CLICK | B_PRESS)) { + #ifdef LOCKOUT_MOON_LOWEST + // Use lowest moon configured + uint8_t lvl = ramp_smooth_floor; + if (ramp_discrete_floor < lvl) lvl = ramp_discrete_floor; + set_level(lvl); + #elif defined(LOCKOUT_MOON_FANCY) + uint8_t levels[] = { ramp_smooth_floor, ramp_discrete_floor }; + if ((event & 0x0f) == 2) { + set_level(levels[ramp_style^1]); + } else { + set_level(levels[ramp_style]); + } + #else + // Use moon from current ramp + set_level(nearest_level(1)); + #endif + } + // button was released + else if ((event & (B_CLICK | B_PRESS)) == (B_CLICK)) { + set_level(0); + } + #endif + + // regular event handling + // conserve power while locked out + // (allow staying awake long enough to exit, but otherwise + // be persistent about going back to sleep every few seconds + // even if the user keeps pressing the button) + #ifdef USE_INDICATOR_LED + if (event == EV_enter_state) { + indicator_led(indicator_led_mode >> 2); + } else + #endif + if (event == EV_tick) { + if (arg > TICKS_PER_SECOND*2) { + go_to_standby = 1; + #ifdef USE_INDICATOR_LED + indicator_led(indicator_led_mode >> 2); + #endif + } + return EVENT_HANDLED; + } + #if defined(TICK_DURING_STANDBY) && defined(USE_INDICATOR_LED) + else if (event == EV_sleep_tick) { + if ((indicator_led_mode & 0b00001100) == 0b00001100) { + indicator_blink(arg); + } + return EVENT_HANDLED; + } + #endif + #ifdef USE_INDICATOR_LED + // 3 clicks: rotate through indicator LED modes (lockout mode) + else if (event == EV_3clicks) { + uint8_t mode = indicator_led_mode >> 2; + #ifdef TICK_DURING_STANDBY + mode = (mode + 1) & 3; + #else + mode = (mode + 1) % 3; + #endif + #ifdef INDICATOR_LED_SKIP_LOW + if (mode == 1) { mode ++; } + #endif + indicator_led_mode = (mode << 2) + (indicator_led_mode & 0x03); + indicator_led(mode); + save_config(); + return EVENT_HANDLED; + } + #endif + // 6 clicks: exit + else if (event == EV_6clicks) { + blink_confirm(1); + set_state(off_state, 0); + return EVENT_HANDLED; + } + + return EVENT_NOT_HANDLED; +} + + +uint8_t momentary_state(Event event, uint16_t arg) { + // TODO: momentary strobe here? (for light painting) + + // light up when the button is pressed; go dark otherwise + // button is being held + if ((event & (B_CLICK | B_PRESS)) == (B_CLICK | B_PRESS)) { + set_level(memorized_level); + return EVENT_HANDLED; + } + // button was released + else if ((event & (B_CLICK | B_PRESS)) == (B_CLICK)) { + set_level(0); + //go_to_standby = 1; // sleep while light is off + return EVENT_HANDLED; + } + + // Sleep, dammit! (but wait a few seconds first) + // (because standby mode uses such little power that it can interfere + // with exiting via tailcap loosen+tighten unless you leave power + // disconnected for several seconds, so we want to be awake when that + // happens to speed up the process) + else if ((event == EV_tick) && (actual_level == 0)) { + if (arg > TICKS_PER_SECOND*15) { // sleep after 15 seconds + go_to_standby = 1; // sleep while light is off + // TODO: lighted button should use lockout config? + } + return EVENT_HANDLED; + } + + return EVENT_NOT_HANDLED; +} + + +// ask the user for a sequence of numbers, then save them and return to caller +uint8_t config_state_base(Event event, uint16_t arg, + uint8_t num_config_steps, + void (*savefunc)()) { + static uint8_t config_step; + if (event == EV_enter_state) { + config_step = 0; + set_level(0); + return EVENT_HANDLED; + } + // advance forward through config steps + else if (event == EV_tick) { + if (config_step < num_config_steps) { + push_state(number_entry_state, config_step + 1); + } + else { + // TODO: blink out some sort of success pattern + savefunc(); + save_config(); + //set_state(retstate, retval); + pop_state(); + } + return EVENT_HANDLED; + } + // an option was set (return from number_entry_state) + else if (event == EV_reenter_state) { + config_state_values[config_step] = number_entry_value; + config_step ++; + return EVENT_HANDLED; + } + //return EVENT_NOT_HANDLED; + // eat all other events; don't pass any through to parent + return EVENT_HANDLED; +} + +#ifdef USE_RAMP_CONFIG +void ramp_config_save() { + // parse values + uint8_t val; + if (ramp_style) { // discrete / stepped ramp + + val = config_state_values[0]; + if (val) { ramp_discrete_floor = val; } + + val = config_state_values[1]; + if (val) { ramp_discrete_ceil = MAX_LEVEL + 1 - val; } + + val = config_state_values[2]; + if (val) ramp_discrete_steps = val; + + } else { // smooth ramp + + val = config_state_values[0]; + if (val) { ramp_smooth_floor = val; } + + val = config_state_values[1]; + if (val) { ramp_smooth_ceil = MAX_LEVEL + 1 - val; } + + } +} + +uint8_t ramp_config_state(Event event, uint16_t arg) { + uint8_t num_config_steps; + num_config_steps = 2 + ramp_style; + return config_state_base(event, arg, + num_config_steps, ramp_config_save); +} +#endif // #ifdef USE_RAMP_CONFIG + + +#ifdef USE_THERMAL_REGULATION +void thermal_config_save() { + // parse values + uint8_t val; + + // calibrate room temperature + val = config_state_values[0]; + if (val) { + int8_t rawtemp = temperature - therm_cal_offset; + therm_cal_offset = val - rawtemp; + reset_thermal_history = 1; // invalidate all recent temperature data + } + + val = config_state_values[1]; + if (val) { + // set maximum heat limit + therm_ceil = 30 + val - 1; + } + if (therm_ceil > MAX_THERM_CEIL) therm_ceil = MAX_THERM_CEIL; +} + +uint8_t thermal_config_state(Event event, uint16_t arg) { + return config_state_base(event, arg, + 2, thermal_config_save); +} +#endif // #ifdef USE_THERMAL_REGULATION + + +#ifdef USE_BEACON_MODE +void beacon_config_save() { + // parse values + uint8_t val = config_state_values[0]; + if (val) { + beacon_seconds = val; + } +} + +uint8_t beacon_config_state(Event event, uint16_t arg) { + return config_state_base(event, arg, + 1, beacon_config_save); +} + +inline void beacon_mode_iter() { + // one iteration of main loop() + set_level(memorized_level); + nice_delay_ms(100); + set_level(0); + nice_delay_ms(((beacon_seconds) * 1000) - 100); +} +#endif // #ifdef USE_BEACON_MODE + + +uint8_t number_entry_state(Event event, uint16_t arg) { + static uint8_t value; + static uint8_t blinks_left; + static uint8_t entry_step; + static uint16_t wait_ticks; + if (event == EV_enter_state) { + value = 0; + blinks_left = arg; + entry_step = 0; + wait_ticks = 0; + return EVENT_HANDLED; + } + // advance through the process: + // 0: wait a moment + // 1: blink out the 'arg' value + // 2: wait a moment + // 3: "buzz" while counting clicks + // 4: save and exit + else if (event == EV_tick) { + // wait a moment + if ((entry_step == 0) || (entry_step == 2)) { + if (wait_ticks < TICKS_PER_SECOND/2) + wait_ticks ++; + else { + entry_step ++; + wait_ticks = 0; + } + } + // blink out the option number + else if (entry_step == 1) { + if (blinks_left) { + if ((wait_ticks & 31) == 10) { + set_level(RAMP_SIZE/4); + } + else if ((wait_ticks & 31) == 20) { + set_level(0); + } + else if ((wait_ticks & 31) == 31) { + blinks_left --; + } + wait_ticks ++; + } + else { + entry_step ++; + wait_ticks = 0; + } + } + else if (entry_step == 3) { // buzz while waiting for a number to be entered + wait_ticks ++; + // buzz for N seconds after last event + if ((wait_ticks & 3) == 0) { + set_level(RAMP_SIZE/6); + } + else if ((wait_ticks & 3) == 2) { + set_level(RAMP_SIZE/8); + } + // time out after 3 seconds + if (wait_ticks > TICKS_PER_SECOND*3) { + //number_entry_value = value; + set_level(0); + entry_step ++; + } + } + else if (entry_step == 4) { + number_entry_value = value; + pop_state(); + } + return EVENT_HANDLED; + } + // count clicks + else if (event == EV_click1_release) { + empty_event_sequence(); + if (entry_step == 3) { // only count during the "buzz" + value ++; + wait_ticks = 0; + // flash briefly + set_level(RAMP_SIZE/2); + delay_4ms(8/2); + set_level(0); + } + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + + +// find the ramp level closest to the target, +// using only the levels which are allowed in the current state +uint8_t nearest_level(int16_t target) { + // bounds check + // using int16_t here saves us a bunch of logic elsewhere, + // by allowing us to correct for numbers < 0 or > 255 in one central place + uint8_t mode_min = ramp_smooth_floor; + uint8_t mode_max = ramp_smooth_ceil; + if (ramp_style) { + mode_min = ramp_discrete_floor; + mode_max = ramp_discrete_ceil; + } + if (target < mode_min) return mode_min; + if (target > mode_max) return mode_max; + // the rest isn't relevant for smooth ramping + if (! ramp_style) return target; + + uint8_t ramp_range = ramp_discrete_ceil - ramp_discrete_floor; + ramp_discrete_step_size = ramp_range / (ramp_discrete_steps-1); + uint8_t this_level = ramp_discrete_floor; + + for(uint8_t i=0; i>1)) + return this_level; + } + return this_level; +} + + +void blink_confirm(uint8_t num) { + for (; num>0; num--) { + set_level(MAX_LEVEL/4); + delay_4ms(10/4); + set_level(0); + delay_4ms(100/4); + } +} + + +#if defined(USE_INDICATOR_LED) && defined(TICK_DURING_STANDBY) +// beacon-like mode for the indicator LED +void indicator_blink(uint8_t arg) { + #define USE_FANCIER_BLINKING_INDICATOR + #ifdef USE_FANCIER_BLINKING_INDICATOR + + // fancy blink, set off/low/high levels here: + uint8_t seq[] = {0, 1, 2, 1, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0}; + indicator_led(seq[arg & 15]); + + #else // basic blink, 1/8th duty cycle + + if (! (arg & 7)) { + indicator_led(2); + } + else { + indicator_led(0); + } + + #endif +} +#endif + + +void load_config() { + if (load_eeprom()) { + ramp_style = eeprom[0]; + #ifdef USE_RAMP_CONFIG + ramp_smooth_floor = eeprom[1]; + ramp_smooth_ceil = eeprom[2]; + ramp_discrete_floor = eeprom[3]; + ramp_discrete_ceil = eeprom[4]; + ramp_discrete_steps = eeprom[5]; + #endif + #ifdef USE_BEACON_MODE + beacon_seconds = eeprom[6]; + #endif + #ifdef USE_THERMAL_REGULATION + therm_ceil = eeprom[EEPROM_BYTES_BASE]; + therm_cal_offset = eeprom[EEPROM_BYTES_BASE+1]; + #endif + #ifdef USE_INDICATOR_LED + indicator_led_mode = eeprom[EEPROM_BYTES_BASE+EEPROM_THERMAL_BYTES]; + #endif + } +} + +void save_config() { + eeprom[0] = ramp_style; + #ifdef USE_RAMP_CONFIG + eeprom[1] = ramp_smooth_floor; + eeprom[2] = ramp_smooth_ceil; + eeprom[3] = ramp_discrete_floor; + eeprom[4] = ramp_discrete_ceil; + eeprom[5] = ramp_discrete_steps; + #endif + #ifdef USE_BEACON_MODE + eeprom[6] = beacon_seconds; + #endif + #ifdef USE_THERMAL_REGULATION + eeprom[EEPROM_BYTES_BASE ] = therm_ceil; + eeprom[EEPROM_BYTES_BASE+1] = therm_cal_offset; + #endif + #ifdef USE_INDICATOR_LED + eeprom[EEPROM_BYTES_BASE+EEPROM_THERMAL_BYTES] = indicator_led_mode; + #endif + + save_eeprom(); +} + +void low_voltage() { + StatePtr state = current_state; + + // TODO: turn off aux LED(s) when power is really low + + if (0) {} // placeholder + + // in normal mode, step down or turn off + else if (state == steady_state) { + if (actual_level > 1) { + uint8_t lvl = (actual_level >> 1) + (actual_level >> 2); + set_level(lvl); + #ifdef USE_THERMAL_REGULATION + target_level = lvl; + #ifdef USE_SET_LEVEL_GRADUALLY + // not needed? + //set_level_gradually(lvl); + #endif + #endif + } + else { + set_state(off_state, 0); + } + } + // all other modes, just turn off when voltage is low + else { + set_state(off_state, 0); + } +} + + +void setup() { + // blink at power-on to let user know power is connected + set_level(RAMP_SIZE/8); + delay_4ms(3); + set_level(0); + + load_config(); + + push_state(off_state, 0); +} + + +void loop() { + + StatePtr state = current_state; + + if (0) {} + + #ifdef USE_BATTCHECK + else if (state == battcheck_state) { + battcheck(); + } + #endif + + #ifdef USE_BEACON_MODE + else if (state == beacon_state) { + beacon_mode_iter(); + } + #endif + + #ifdef USE_THERMAL_REGULATION + // TODO: blink out therm_ceil during thermal_config_state? + else if (state == tempcheck_state) { + blink_num(temperature); + nice_delay_ms(1000); + } + #endif + + #ifdef USE_IDLE_MODE + else { + // doze until next clock tick + idle_mode(); + } + #endif + +} diff --git a/ui/rampingios/rampingiosv3.svg b/ui/rampingios/rampingiosv3.svg new file mode 100644 index 0000000..bc9e6b3 --- /dev/null +++ b/ui/rampingios/rampingiosv3.svg @@ -0,0 +1,4113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + Ramps: + + + ThermalCfg + + + + + BeaconCfg + + + + + + Ramp + Ceil + Floor + + + Turbo + + + + + Mem + Regulated Hybrid -------------- Direct Drive + + + + + + + + + + + + Ramp + + Cfg + + + + + + Actions + 1 Fast Click + Hold + 3 Fast Clicks + Other Action + + + + 2 Fast Clicks + Click, Hold + RampingIOS V3 + + + + 7 Clicks + + + + OFF + + + + OFF + + + + + + + 4 Clicks + 4 Clicks + Click, Click, Hold + + 6 Clicks + + + + Smooth + + + + Ramp Cfg + + 4 Clicks + + + + 4 Clicks + + + + 1. Floor (click N times for level N)2. Ceiling (click N times for 1 + Turbo - N)3. Number of steps (stepped ramp only) + 1. Current temperature (click N times for N deg C)2. Temperature limit (click N times for 30 C + N) + 1. Beacon speed (click N times for N seconds per flash) + Thermal Cfg + Beacon Cfg + + 4 Clicks + + + + + + Stepped + + + + Tactical + + + BattCheck + + Lockout + + TempCheck + Beacon + + ThermalCfg + + + OFF + + + + + + + + + + + (momentary) + 3 Clicks + 4 Clicks + 6 Clicks + 8 Clicks + 10 Clicks + + + Aux LED + mode + next + + + + + + + + lockout LED + mode + next + 4 Clicks + 4 Clicks + 4 Clicks + + + + + diff --git a/ui/werner/Makefile b/ui/werner/Makefile new file mode 100644 index 0000000..21d85f7 --- /dev/null +++ b/ui/werner/Makefile @@ -0,0 +1,7 @@ +all: + ./build-all.sh + +clean: + rm -f cfg-*.h *.hex *~ *.elf *.o + +.phony: clean diff --git a/ui/werner/README b/ui/werner/README new file mode 100644 index 0000000..5fe392d --- /dev/null +++ b/ui/werner/README @@ -0,0 +1,56 @@ +This is a Werner-style interface for dual-switch lights +(e-switch + clicky switch). What that means is: + +While the clicky switch is off: + + - Click the clicky switch: Turn on, at the last-used level. The clicky + switch works as a momentary mode. + + - Click the clicky switch while holding the e-switch: Go into sort of a + utility mode. + +While on, in a normal steady mode: + + - Click the clicky switch: Turn off. + + - Click the e-switch: Brighter. One step per click. + + - Hold the e-switch: Dimmer. Keep holding to go down multiple steps. + +While in standby, in utility mode: + + - Click the e-switch: Turn on. + + - Hold the e-switch: Turn on at lowest level. + + - Double-click the e-switch: Turn on at highest level. + + - Triple-click the e-switch: Battery check mode. + + - Quad-click the e-switch: Ramp config mode. + +While in battery check mode: + + - Click either switch: Turn off. + + - Double-click the e-switch: Go to temperature check mode. + +While in temperature check mode: + + - Click either switch: Turn off. + + - Double-click the e-switch: Go to battery check mode. + + - Quad-click the e-switch: Go to thermal config mode. + +Ramp config mode and thermal config mode work the same as in Anduril or +RampingIOS V3. The options are: + + - Ramp config mode: + 1. Floor level. + 2. Ceiling level. + 3. Number of steps. + + - Thermal config mode: + 1. Calibrate sensor by entering current temperature in C. + 2. Set temperature limit to 30 C + N clicks. diff --git a/ui/werner/build-all.sh b/ui/werner/build-all.sh new file mode 100755 index 0000000..b114101 --- /dev/null +++ b/ui/werner/build-all.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +cp -av ../anduril/cfg*.h . + +UI=werner + +for TARGET in cfg-*.h ; do + NAME=$(echo "$TARGET" | perl -ne '/cfg-(.*).h/ && print "$1\n";') + echo "===== $NAME =====" + ATTINY=$(grep 'ATTINY:' $TARGET | awk '{ print $3 }') + if [ -z "$ATTINY" ]; then ATTINY=85 ; fi + echo ../../../bin/build.sh $ATTINY "$UI" "-DCONFIGFILE=${TARGET}" + ../../../bin/build.sh $ATTINY "$UI" "-DCONFIGFILE=${TARGET}" + mv -f "$UI".hex "$UI".$NAME.hex +done diff --git a/ui/werner/werner.c b/ui/werner/werner.c new file mode 100644 index 0000000..f3241ee --- /dev/null +++ b/ui/werner/werner.c @@ -0,0 +1,715 @@ +/* + * Werner: Werner-style dual-switch UI for SpaghettiMonster. + * Side click to go up, side hold to go down, tail click for on/off. + * + * Copyright (C) 2018 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 . + */ + +/********* User-configurable options *********/ +// Physical driver type (uncomment one of the following or define it at the gcc command line) +//#define CONFIGFILE cfg-emisar-d4.h + +#define USE_LVP // FIXME: won't build when this option is turned off + +// parameters for this defined below or per-driver +#define USE_THERMAL_REGULATION +#define DEFAULT_THERM_CEIL 45 // try not to get hotter than this + +// battery readout style (pick one) +#define BATTCHECK_VpT +//#define BATTCHECK_8bars // FIXME: breaks build +//#define BATTCHECK_4bars // FIXME: breaks build + +// cut clock speed at very low modes for better efficiency +// (defined here so config files can override it) +#define USE_DYNAMIC_UNDERCLOCKING + +/***** specific settings for known driver types *****/ +#ifdef CONFIGFILE +#include "tk.h" +#include incfile(CONFIGFILE) +#else +#error You need to define CONFIGFILE +#endif + +// thermal properties, if not defined per-driver +#ifndef MIN_THERM_STEPDOWN +#define MIN_THERM_STEPDOWN MAX_1x7135 // lowest value it'll step down to +#endif +#ifndef THERM_FASTER_LEVEL + #ifdef MAX_Nx7135 + #define THERM_FASTER_LEVEL MAX_Nx7135 // throttle back faster when high + #else + #define THERM_FASTER_LEVEL (RAMP_SIZE*4/5) // throttle back faster when high + #endif +#endif +#ifdef USE_THERMAL_REGULATION +#define USE_SET_LEVEL_GRADUALLY // isn't used except for thermal adjustments +#endif + + +/********* Configure SpaghettiMonster *********/ +#define USE_DELAY_ZERO +#define USE_RAMPING +#define RAMP_LENGTH 150 // default, if not overridden in a driver cfg file +#define USE_BATTCHECK +#define USE_IDLE_MODE // reduce power use while awake and no tasks are pending + +// auto-detect how many eeprom bytes +#define USE_EEPROM +#ifdef USE_THERMAL_REGULATION +#define EEPROM_BYTES 5 +#else +#define EEPROM_BYTES 3 +#endif +// for mode memory on tail switch +#define USE_EEPROM_WL +#define EEPROM_WL_BYTES 1 + +#include "spaghetti-monster.h" + + +// FSM states +uint8_t off_state(Event event, uint16_t arg); +// simple numeric entry config menu +uint8_t config_state_base(Event event, uint16_t arg, + uint8_t num_config_steps, + void (*savefunc)()); +#define MAX_CONFIG_VALUES 3 +uint8_t config_state_values[MAX_CONFIG_VALUES]; +// ramping mode and its related config mode +uint8_t steady_state(Event event, uint16_t arg); +uint8_t ramp_config_state(Event event, uint16_t arg); +#ifdef USE_BATTCHECK +uint8_t battcheck_state(Event event, uint16_t arg); +#endif +#ifdef USE_THERMAL_REGULATION +uint8_t tempcheck_state(Event event, uint16_t arg); +uint8_t thermal_config_state(Event event, uint16_t arg); +#endif + +// general helper function for config modes +uint8_t number_entry_state(Event event, uint16_t arg); +// return value from number_entry_state() +volatile uint8_t number_entry_value; + +void blink_confirm(uint8_t num); + +// remember stuff even after battery was changed +void load_config(); +void save_config(); +void save_config_wl(); + +// default ramp options if not overridden earlier per-driver +#ifndef RAMP_DISCRETE_FLOOR + #define RAMP_DISCRETE_FLOOR 1 +#endif +#ifndef RAMP_DISCRETE_CEIL + #define RAMP_DISCRETE_CEIL RAMP_SIZE +#endif +#ifndef RAMP_DISCRETE_STEPS + #define RAMP_DISCRETE_STEPS 7 +#endif + +// brightness control +uint8_t memorized_level = MAX_1x7135; +// smooth vs discrete ramping +volatile uint8_t ramp_discrete_floor = RAMP_DISCRETE_FLOOR; +volatile uint8_t ramp_discrete_ceil = RAMP_DISCRETE_CEIL; +volatile uint8_t ramp_discrete_steps = RAMP_DISCRETE_STEPS; +uint8_t ramp_discrete_step_size; // don't set this + +// calculate the nearest ramp level which would be valid at the moment +// (is a no-op for smooth ramp, but limits discrete ramp to only the +// correct levels for the user's config) +uint8_t nearest_level(int16_t target); + +#ifdef USE_THERMAL_REGULATION +// brightness before thermal step-down +uint8_t target_level = 0; +#endif + + +uint8_t off_state(Event event, uint16_t arg) { + // turn emitter off when entering state + if ((event == EV_enter_state) || (event == EV_reenter_state)) { + // let the user know the power is connected + blink_confirm(1); + // but otherwise stay off + set_level(0); + // sleep while off (lower power use) + go_to_standby = 1; + return EVENT_HANDLED; + } + // go back to sleep eventually if we got bumped but didn't leave "off" state + else if (event == EV_tick) { + if (arg > TICKS_PER_SECOND*2) { + go_to_standby = 1; + } + return EVENT_HANDLED; + } + // hold (initially): go to lowest level, but allow abort for regular click + else if (event == EV_click1_press) { + set_level(nearest_level(1)); + return EVENT_HANDLED; + } + // hold: go to lowest level + else if (event == EV_click1_hold) { + // don't start ramping immediately; + // give the user time to release at moon level + if (arg >= HOLD_TIMEOUT) { + set_state(steady_state, 1); + } + return EVENT_HANDLED; + } + // hold, release quickly: go to lowest level + else if (event == EV_click1_hold_release) { + set_state(steady_state, 1); + return EVENT_HANDLED; + } + // 1 click (before timeout): go to memorized level, but allow abort for double click + else if (event == EV_click1_release) { + set_level(nearest_level(memorized_level)); + return EVENT_HANDLED; + } + // 1 click: regular mode + else if (event == EV_1click) { + set_state(steady_state, memorized_level); + return EVENT_HANDLED; + } + // 2 clicks (initial press): off, to prep for later events + else if (event == EV_click2_press) { + set_level(0); + return EVENT_HANDLED; + } + // click, hold: go to highest level (for ramping down) + else if (event == EV_click2_hold) { + set_state(steady_state, MAX_LEVEL); + return EVENT_HANDLED; + } + // 2 clicks: highest mode + else if (event == EV_2clicks) { + set_state(steady_state, nearest_level(MAX_LEVEL)); + return EVENT_HANDLED; + } + #ifdef USE_BATTCHECK + // 3 clicks: battcheck mode / blinky mode group + else if (event == EV_3clicks) { + set_state(battcheck_state, 0); + return EVENT_HANDLED; + } + #endif + // 4 clicks: configure ramp + else if (event == EV_4clicks) { + push_state(ramp_config_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + + +uint8_t steady_state(Event event, uint16_t arg) { + uint8_t mode_min = ramp_discrete_floor; + uint8_t mode_max = ramp_discrete_ceil; + uint8_t ramp_step_size = ramp_discrete_step_size; + + // turn LED on when we first enter the mode + if ((event == EV_enter_state) || (event == EV_reenter_state)) { + // if we just got back from config mode, go back to memorized level + if (event == EV_reenter_state) { + arg = memorized_level; + } + // remember this level, unless it's moon or turbo + if ((arg > mode_min) && (arg < mode_max)) + memorized_level = arg; + // use the requested level even if not memorized + #ifdef USE_THERMAL_REGULATION + target_level = arg; + #endif + set_level(nearest_level(arg)); + return EVENT_HANDLED; + } + // click: brighter + else if (event == EV_click1_release) { + memorized_level = nearest_level((int16_t)actual_level + ramp_step_size); + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + set_level(memorized_level); + // make sure next click will respond quickly + empty_event_sequence(); + // remember mode for later + save_config_wl(); + return EVENT_HANDLED; + } + // hold: dimmer + else if (event == EV_click1_hold) { + // ramp slower in discrete mode + if (arg % HOLD_TIMEOUT != 0) { + return EVENT_HANDLED; + } + memorized_level = nearest_level((int16_t)actual_level - ramp_step_size); + #ifdef USE_THERMAL_REGULATION + target_level = memorized_level; + #endif + set_level(memorized_level); + return EVENT_HANDLED; + } + // reverse ramp direction on hold release + else if (event == EV_click1_hold_release) { + save_config_wl(); + return EVENT_HANDLED; + } + #if defined(USE_SET_LEVEL_GRADUALLY) + // gradual thermal regulation + else if (event == EV_tick) { + #ifdef USE_SET_LEVEL_GRADUALLY + // make thermal adjustment speed scale with magnitude + if ((arg & 1) && (actual_level < THERM_FASTER_LEVEL)) { + return EVENT_HANDLED; // adjust slower when not a high mode + } + #ifdef THERM_HARD_TURBO_DROP + else if ((! (actual_level < THERM_FASTER_LEVEL)) + && (actual_level > gradual_target)) { + gradual_tick(); + } + else { + #endif + // [int(62*4 / (x**0.95)) for x in (1,2,4,8,16,32,64,128)] + uint8_t intervals[] = {248, 128, 66, 34, 17, 9, 4, 2}; + uint8_t diff; + static uint8_t ticks_since_adjust = 0; + ticks_since_adjust ++; + if (gradual_target > actual_level) diff = gradual_target - actual_level; + else { + diff = actual_level - gradual_target; + } + uint8_t magnitude = 0; + #ifndef THERM_HARD_TURBO_DROP + // if we're on a really high mode, drop faster + if (actual_level >= THERM_FASTER_LEVEL) { magnitude ++; } + #endif + while (diff) { + magnitude ++; + diff >>= 1; + } + uint8_t ticks_per_adjust = intervals[magnitude]; + if (ticks_since_adjust > ticks_per_adjust) + { + gradual_tick(); + ticks_since_adjust = 0; + } + //if (!(arg % ticks_per_adjust)) gradual_tick(); + #ifdef THERM_HARD_TURBO_DROP + } + #endif + #endif + return EVENT_HANDLED; + } + #endif + #ifdef USE_THERMAL_REGULATION + // overheating: drop by an amount proportional to how far we are above the ceiling + else if (event == EV_temperature_high) { + #ifdef THERM_HARD_TURBO_DROP + if (actual_level > THERM_FASTER_LEVEL) { + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(THERM_FASTER_LEVEL); + #else + set_level(THERM_FASTER_LEVEL); + #endif + } else + #endif + if (actual_level > MIN_THERM_STEPDOWN) { + int16_t stepdown = actual_level - arg; + if (stepdown < MIN_THERM_STEPDOWN) stepdown = MIN_THERM_STEPDOWN; + else if (stepdown > MAX_LEVEL) stepdown = MAX_LEVEL; + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(stepdown); + #else + set_level(stepdown); + #endif + } + return EVENT_HANDLED; + } + // underheating: increase slowly if we're lower than the target + // (proportional to how low we are) + else if (event == EV_temperature_low) { + if (actual_level < target_level) { + //int16_t stepup = actual_level + (arg>>1); + int16_t stepup = actual_level + arg; + if (stepup > target_level) stepup = target_level; + else if (stepup < MIN_THERM_STEPDOWN) stepup = MIN_THERM_STEPDOWN; + #ifdef USE_SET_LEVEL_GRADUALLY + set_level_gradually(stepup); + #else + set_level(stepup); + #endif + } + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} + + +#ifdef USE_BATTCHECK +uint8_t battcheck_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + #ifdef USE_THERMAL_REGULATION + // 2 clicks: tempcheck mode + else if (event == EV_2clicks) { + blink_confirm(2); + set_state(tempcheck_state, 0); + return EVENT_HANDLED; + } + #endif + return EVENT_NOT_HANDLED; +} +#endif + +#ifdef USE_THERMAL_REGULATION +uint8_t tempcheck_state(Event event, uint16_t arg) { + // 1 click: off + if (event == EV_1click) { + set_state(off_state, 0); + return EVENT_HANDLED; + } + // 2 clicks: battcheck mode + else if (event == EV_2clicks) { + blink_confirm(1); + set_state(battcheck_state, 0); + return EVENT_HANDLED; + } + // 4 clicks: thermal config mode + else if (event == EV_4clicks) { + push_state(thermal_config_state, 0); + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} +#endif + + +// ask the user for a sequence of numbers, then save them and return to caller +uint8_t config_state_base(Event event, uint16_t arg, + uint8_t num_config_steps, + void (*savefunc)()) { + static uint8_t config_step; + if (event == EV_enter_state) { + config_step = 0; + set_level(0); + return EVENT_HANDLED; + } + // advance forward through config steps + else if (event == EV_tick) { + if (config_step < num_config_steps) { + push_state(number_entry_state, config_step + 1); + } + else { + // TODO: blink out some sort of success pattern + savefunc(); + save_config(); + //set_state(retstate, retval); + pop_state(); + } + return EVENT_HANDLED; + } + // an option was set (return from number_entry_state) + else if (event == EV_reenter_state) { + config_state_values[config_step] = number_entry_value; + config_step ++; + return EVENT_HANDLED; + } + //return EVENT_NOT_HANDLED; + // eat all other events; don't pass any through to parent + return EVENT_HANDLED; +} + +void ramp_config_save() { + // parse values + uint8_t val; + + val = config_state_values[0]; + if (val) { ramp_discrete_floor = val; } + + val = config_state_values[1]; + if (val) { ramp_discrete_ceil = MAX_LEVEL + 1 - val; } + + val = config_state_values[2]; + if (val) ramp_discrete_steps = val; +} + +uint8_t ramp_config_state(Event event, uint16_t arg) { + uint8_t num_config_steps; + num_config_steps = 3; + return config_state_base(event, arg, + num_config_steps, ramp_config_save); +} + + +#ifdef USE_THERMAL_REGULATION +void thermal_config_save() { + // parse values + uint8_t val; + + // calibrate room temperature + val = config_state_values[0]; + if (val) { + int8_t rawtemp = temperature - therm_cal_offset; + therm_cal_offset = val - rawtemp; + reset_thermal_history = 1; // invalidate all recent temperature data + } + + val = config_state_values[1]; + if (val) { + // set maximum heat limit + therm_ceil = 30 + val - 1; + } + if (therm_ceil > MAX_THERM_CEIL) therm_ceil = MAX_THERM_CEIL; +} + +uint8_t thermal_config_state(Event event, uint16_t arg) { + return config_state_base(event, arg, + 2, thermal_config_save); +} +#endif + + +uint8_t number_entry_state(Event event, uint16_t arg) { + static uint8_t value; + static uint8_t blinks_left; + static uint8_t entry_step; + static uint16_t wait_ticks; + if (event == EV_enter_state) { + value = 0; + blinks_left = arg; + entry_step = 0; + wait_ticks = 0; + return EVENT_HANDLED; + } + // advance through the process: + // 0: wait a moment + // 1: blink out the 'arg' value + // 2: wait a moment + // 3: "buzz" while counting clicks + // 4: save and exit + else if (event == EV_tick) { + // wait a moment + if ((entry_step == 0) || (entry_step == 2)) { + if (wait_ticks < TICKS_PER_SECOND/2) + wait_ticks ++; + else { + entry_step ++; + wait_ticks = 0; + } + } + // blink out the option number + else if (entry_step == 1) { + if (blinks_left) { + if ((wait_ticks & 31) == 10) { + set_level(RAMP_SIZE/4); + } + else if ((wait_ticks & 31) == 20) { + set_level(0); + } + else if ((wait_ticks & 31) == 31) { + blinks_left --; + } + wait_ticks ++; + } + else { + entry_step ++; + wait_ticks = 0; + } + } + else if (entry_step == 3) { // buzz while waiting for a number to be entered + wait_ticks ++; + // buzz for N seconds after last event + if ((wait_ticks & 3) == 0) { + set_level(RAMP_SIZE/6); + } + else if ((wait_ticks & 3) == 2) { + set_level(RAMP_SIZE/8); + } + // time out after 3 seconds + if (wait_ticks > TICKS_PER_SECOND*3) { + //number_entry_value = value; + set_level(0); + entry_step ++; + } + } + else if (entry_step == 4) { + number_entry_value = value; + pop_state(); + } + return EVENT_HANDLED; + } + // count clicks + else if (event == EV_click1_release) { + empty_event_sequence(); + if (entry_step == 3) { // only count during the "buzz" + value ++; + wait_ticks = 0; + // flash briefly + set_level(RAMP_SIZE/2); + delay_4ms(8/2); + set_level(0); + } + return EVENT_HANDLED; + } + return EVENT_NOT_HANDLED; +} + + +// find the ramp level closest to the target, +// using only the levels which are allowed in the current state +uint8_t nearest_level(int16_t target) { + // bounds check + // using int16_t here saves us a bunch of logic elsewhere, + // by allowing us to correct for numbers < 0 or > 255 in one central place + uint8_t mode_min = ramp_discrete_floor; + uint8_t mode_max = ramp_discrete_ceil; + if (target < mode_min) return mode_min; + if (target > mode_max) return mode_max; + + uint8_t ramp_range = ramp_discrete_ceil - ramp_discrete_floor; + ramp_discrete_step_size = ramp_range / (ramp_discrete_steps-1); + uint8_t this_level = ramp_discrete_floor; + + for(uint8_t i=0; i>1)) + return this_level; + } + return this_level; +} + + +void blink_confirm(uint8_t num) { + for (; num>0; num--) { + set_level(MAX_LEVEL/4); + delay_4ms(10/4); + set_level(0); + delay_4ms(100/4); + } +} + + +void load_config() { + if (load_eeprom()) { + ramp_discrete_floor = eeprom[0]; + ramp_discrete_ceil = eeprom[1]; + ramp_discrete_steps = eeprom[2]; + #ifdef USE_THERMAL_REGULATION + therm_ceil = eeprom[3]; + therm_cal_offset = eeprom[4]; + #endif + } + if (load_eeprom_wl()) { + memorized_level = eeprom_wl[0]; + } +} + + +void save_config() { + eeprom[0] = ramp_discrete_floor; + eeprom[1] = ramp_discrete_ceil; + eeprom[2] = ramp_discrete_steps; + #ifdef USE_THERMAL_REGULATION + eeprom[3] = therm_ceil; + eeprom[4] = therm_cal_offset; + #endif + + save_eeprom(); +} + + +void save_config_wl() { + eeprom_wl[0] = memorized_level; + save_eeprom_wl(); +} + + +void low_voltage() { + StatePtr state = current_state; + + // in normal mode, step down or turn off + if (state == steady_state) { + if (actual_level > 1) { + uint8_t lvl = (actual_level >> 1) + (actual_level >> 2); + set_level(lvl); + #ifdef USE_THERMAL_REGULATION + target_level = lvl; + #endif + } + else { + set_state(off_state, 0); + } + } + // all other modes, just turn off when voltage is low + else { + set_state(off_state, 0); + } +} + + +void setup() { + // dual switch: e-switch + power clicky + // power clicky acts as a momentary mode + load_config(); + + if (button_is_pressed()) + // hold button to go to moon + push_state(off_state, 0); + else + // otherwise use memory + push_state(steady_state, memorized_level); +} + + +void loop() { + + StatePtr state = current_state; + + if (0) {} + + #ifdef USE_BATTCHECK + else if (state == battcheck_state) { + battcheck(); + } + #endif + #ifdef USE_THERMAL_REGULATION + // TODO: blink out therm_ceil during thermal_config_state + else if (state == tempcheck_state) { + blink_num(temperature); + nice_delay_ms(1000); + } + #endif + + #ifdef USE_IDLE_MODE + else { + // doze until next clock tick + idle_mode(); + } + #endif + +} -- cgit v1.2.3