/*
* This is based on Timer0 running at 1msec period.
*/
// Enable Wokwi simulator specific debug output
//#define WOKWI
// Log pulse detection status via the USI output pins
//#define USI_DEBUG
// Replace mutiple dashes with an extra long dash in numbers
#define MORSE_FAST_NUMBERS
#ifdef WOKWI
#include <TinyDebug.h>
#define WOKWI_DEBUG_PRINT(...) Debug.print(__VA_ARGS__)
#define WOKWI_DEBUG_PRINTLN(...) Debug.println(__VA_ARGS__)
#else
#define WOKWI_DEBUG_PRINT(...)
#define WOKWI_DEBUG_PRINTLN(...)
#endif
#include <avr/io.h>
#include <avr/sleep.h>
#include <avr/eeprom.h>
#include <avr/pgmspace.h>
#include <string.h>
#include <stdfix.h>
#undef min
#undef max
#define FW_VERSION 1
/*************************************************************
* MCU SETTINGS
*************************************************************/
#if defined (__AVR_ATtiny85__) || defined (__AVR_ATtiny45__) || defined (__AVR_ATtiny25__)
#define TIMER_DIVISOR 125 // 1 MHz / 8 / 1000
#define PIN_BUTTON0 PB0
#define PIN_BUTTON1 PB2
#define PIN_SPEAKER PB1
#else
#error Unknown MCU
#endif
#define PIN_BUTTONS_MASK (_BV(PIN_BUTTON0) | _BV(PIN_BUTTON1))
#define BUTTON_TIMER_DIVISOR 4
#define MS_WITH_BUTTON_DIVISOR(value) (((value) + 3) >> 2)
/*************************************************************
* UTILITIES
*************************************************************/
typedef union {
uint16_t count;
uint8_t bytes[2];
} counter16_t;
template<typename T>
inline T sub_sat(T a, T b)
{
return a <= b ? 0 : a - b;
}
template<typename T>
inline T add_sat(T a, T b)
{
T r = a + b;
return r < a ? T(-1) : r;
}
template<typename T>
inline T min(T a, T b)
{
return a < b ? a : b;
}
template<typename T>
inline T max(T a, T b)
{
return a > b ? a : b;
}
const uint16_t pow2_table[4] PROGMEM = {
32768,
38968, // * 2^0.25
46340, // * 2^0.5
55109, // * 2^0.75
};
// Calculates 2^(pow/4)
uint8_t pow2_byte(uint8_t pow)
{
uint8_t sub = pow & 3;
uint8_t bit = pow >> 2;
if (bit >= 8)
return 0xff;
return pgm_read_byte(1 + (uint8_t*)&pow2_table[sub]) >> (7 - bit);
}
uint16_t pow2_word(uint8_t pow)
{
uint8_t sub = pow & 3;
uint8_t bit = pow >> 2;
if (bit >= 16)
return 0xffff;
uint16_t rv = pgm_read_word(&pow2_table[sub]);
return bit < 8 ? uint8_t(rv >> 8) >> (7 - bit) : rv >> (15 - bit);
}
void *pgm_memcpy(void *dst, const void *src, uint8_t cnt)
{
uint8_t *bdst = (uint8_t*)dst;
const uint8_t *bsrc = (const uint8_t*)src;
for (; cnt > 0; cnt--, bdst++, bsrc++)
*bdst = pgm_read_byte(bsrc);
return bdst;
}
// Calculates 4*log2(val)
uint8_t log2(uint16_t val)
{
uint8_t bit = 15;
if (val <= 0xff)
{
val <<= 8;
bit -= 8;
}
while (val < 0x8000)
{
val <<= 1;
bit--;
}
uint8_t sub = 0;
while (sub < 4 && pgm_read_word(&pow2_table[sub]) < val)
sub++;
return (sub - 1) | (bit << 2);
}
#define CONCAT_(prefix, suffix) prefix##suffix
#define CONCAT(prefix, suffix) CONCAT_(prefix, suffix)
#define MAKE_UNIQUE_NAME(prefix) CONCAT(prefix##_, __COUNTER__)
class StrBuf {
char *p;
uint8_t len;
public:
StrBuf(char *p, uint8_t len) : p(p), len(len) {}
void append(char c) {
if (len) {
*p++ = c;
len--;
}
if (len)
*p = 0;
}
void append(const char *str, uint8_t slen = 0xff)
{
for (; len > 0 && slen > 0 && *str; len--, slen--)
*p++ = *str++;
if (len)
*p = 0;
}
void appendPGM(const char *str, uint8_t slen = 0xff) __attribute__ ((noinline, noclone))
{
char c;
for (; len > 0 && slen > 0 && (c = pgm_read_byte(str++)) != 0; len--, slen--)
*p++ = c;
if (len)
*p = 0;
}
void append(const __FlashStringHelper *str) {
appendPGM((char*)str);
}
void appendDec(uint16_t num)
{
char buf[5];
int8_t i;
for (i = 4; i >= 0; i--) {
buf[i] = '0' + (num % 10);
num /= 10;
}
for (i = 0; i < 4 && buf[i] == '0'; i++) {}
append(buf + i, 5 - i);
}
void appendHex(uint8_t num)
{
char buf[2];
for (int8_t i = 1; i >= 0; i--) {
uint8_t digit = num & 0xF;
buf[i] = (digit < 10) ? '0' + digit : 'A' - 10 + digit;
num >>= 4;
}
append(buf, 2);
}
};
void fatalError(const __FlashStringHelper *code);
/*************************************************************
* SETTINGS
*************************************************************/
#define EEPROM_VERSION 1
// EEPROM memory layout - ONLY APPEND TO THE END
struct Settings {
uint8_t version;
uint8_t pulse_len_min; // Minimum acceptable pulse length
uint8_t pulse_len_max; // Maximum acceptable pulse length
uint8_t pulse_band_ratio_min; // Minumum acceptable band ratio
uint8_t pulse_start_threshold; // Minimum peak to start pulse tracking
uint8_t pulse_sensitivity; // Sensitivity of pulse detection (log peak voltage)
uint8_t pulse_gap_min; // Minimum gap between pulses in order to detect two barks
uint8_t dark_threshold; // Threshold beyond which night mode is initiated
uint8_t tone_period_min; // Tone period
uint8_t tone_period_max;
uint8_t tone_warble_rate;
uint8_t tone_max_duty;
uint8_t night_switch_delay[2]; // Delay between dark transition and night mode transition, hours; [is_dark]
uint8_t bark_series_min[2]; // Minimum number of barks before tone
uint8_t bark_series_max[2]; // Number of barks in series for max tone
uint8_t bark_series_over[2]; // Extra number of barks to store over the max limit to delay decay
uint8_t bark_series_decay[2]; // Number of 1/8-seconds between decrements
uint8_t bark_tone_vol_min[2]; // Min tone volume
uint8_t bark_tone_vol_max[2]; // Max tone volume
uint8_t bark_tone_len_min[2]; // Min tone length
uint8_t bark_tone_len_max[2]; // Max tone length
uint8_t bark_decay_delay; // Delay after bark end before decay starts (1/8 sec)
uint8_t bark_series_hysteresis[2]; // Extra number of barks to delay starting tones for a series
uint8_t bark_series_decay_slow[2]; // Extra delay in seconds between decrements at min series
};
Settings settings;
#define MAX_PULSE_SENSITIVITY 63
#define SUBSEC_COUNT 8
#define SUBSEC_LENGTH (256 / SUBSEC_COUNT) /* 1 subsecond == 1/8 sec */
#define MAX_VOLUME 20
#define VOLUME_BIT_SHIFT 5 /* 2^(20/4) == 2^5 == 32 */
#define MAX_TONE_LENGTH 32 /* == 1 second */
const Settings PROGMEM default_settings = {
.version = EEPROM_VERSION,
.pulse_len_min = 15,
.pulse_len_max = 35,
.pulse_band_ratio_min = 22,
.pulse_start_threshold = 15,
.pulse_sensitivity = (7<<2),
.pulse_gap_min = 5,
.dark_threshold = 90,
.tone_period_min = 41,
.tone_period_max = 43,
.tone_warble_rate = 3,
.tone_max_duty = 35,
.night_switch_delay = {1, 1},
.bark_series_min = {1, 1},
.bark_series_max = {10, 10},
.bark_series_over = {1, 1},
.bark_series_decay = {8, 8},
.bark_tone_vol_min = {1, 1},
.bark_tone_vol_max = {MAX_VOLUME, MAX_VOLUME},
.bark_tone_len_min = {1, 1},
.bark_tone_len_max = {6, 6},
.bark_decay_delay = 8,
.bark_series_hysteresis = {0, 0},
.bark_series_decay_slow = {0, 0},
};
#define SETTINGS_MENU \
ITEMI(bark_series_min, 0, "DL", 1, 100, 0) /* =oo o=oo */ \
ITEMI(bark_series_max, 0, "DH", 1, 100, 0) /* =oo oooo */ \
ITEMI(bark_series_over, 0, "DO", 0, 100, 0) /* =oo === */ \
ITEMI(bark_series_hysteresis, 0, "DY", 0, 100, 0) /* =oo =o== */ \
ITEMI(bark_series_decay, 0, "DK", 1, 200, 0) /* =oo =o= (subsec)*/ \
ITEMI(bark_series_decay_slow, 0, "DKL", 0, 200, 0) /* =oo =o= o=oo */ \
ITEM_PAGE_BREAK \
ITEMI(night_switch_delay, 0, "DT", 0, 23, 0) /* =oo = */ \
ITEMI(bark_tone_vol_min, 0, "DVL", 0, MAX_VOLUME, 0) /* =oo ooo= o=oo */ \
ITEMI(bark_tone_vol_max, 0, "DVH", 1, MAX_VOLUME, 0) /* =oo ooo= oooo */ \
ITEMI(bark_tone_len_min, 0, "DUL", 1, MAX_TONE_LENGTH, 0) /* =oo oo= o=oo */ \
ITEMI(bark_tone_len_max, 0, "DUH", 1, MAX_TONE_LENGTH, 0) /* =oo oo= oooo */ \
ITEM_PAGE_BREAK \
ITEMI(bark_series_min, 1, "NL", 1, 20, 0) /* =o o=oo */ \
ITEMI(bark_series_max, 1, "NH", 1, 100, 0) /* =o oooo */ \
ITEMI(bark_series_over, 1, "NO", 0, 100, 0) /* =o === */ \
ITEMI(bark_series_hysteresis, 1, "NY", 0, 100, 0) /* =o =o== */ \
ITEMI(bark_series_decay, 1, "NK", 1, 200, 0) /* =o =o= (subsec) */ \
ITEMI(bark_series_decay_slow, 1, "NKL", 0, 200, 0) /* =o =o= o=oo */ \
ITEM_PAGE_BREAK \
ITEMI(night_switch_delay, 1, "NT", 0, 23, 0) /* =o = */ \
ITEMI(bark_tone_vol_min, 1, "NVL", 0, MAX_VOLUME, 0) /* =o ooo= o=oo */ \
ITEMI(bark_tone_vol_max, 1, "NVH", 1, MAX_VOLUME, 0) /* =o ooo= oooo */ \
ITEMI(bark_tone_len_min, 1, "NUL", 1, MAX_TONE_LENGTH, 0) /* =o oo= o=oo */ \
ITEMI(bark_tone_len_max, 1, "NUH", 1, MAX_TONE_LENGTH, 0) /* =o oo= oooo */ \
ITEM_PAGE_BREAK \
ITEM(pulse_len_min, "PL", 1, 100, FBADC(PulseBadLenMin)) /* o==o o=oo */ \
ITEM(pulse_len_max, "PH", 1, 100, FBADC(PulseBadLenMax)) /* o==o oooo */ \
ITEM(pulse_band_ratio_min, "BL", 0, 64, FBADC(PulseBadRatioMin)) /* =ooo o=oo */ \
ITEM(pulse_start_threshold, "SL", 1, 100, 0) /* ooo o=oo */ \
ITEM(pulse_sensitivity, "SE", 1, MAX_PULSE_SENSITIVITY, FBADC(PulseBadPeakMin)) /* ooo o */ \
ITEM(pulse_gap_min, "GL", 1, 100, 0) /* ==o o=oo */ \
ITEM_PAGE_BREAK \
ITEM(bark_decay_delay, "KL", 1, 100, 0) /* =o= o=oo (subsec) */ \
ITEM(dark_threshold, "KT", 1, 200, 0) /* =o= = */ \
ITEM(tone_period_min, "TL", 10, 255, 0) /* = o=oo */ \
ITEM(tone_period_max, "TH", 10, 255, 0) /* = oooo */ \
ITEM(tone_max_duty, "TD", 1, 255, 0) /* = =oo */ \
ITEM(tone_warble_rate, "TW", 0, 7, 0) /* = o== */ \
void save_settings()
{
eeprom_update_block(&settings, (void*)0, sizeof(Settings));
}
void load_settings(bool reset = false)
{
eeprom_read_block(&settings, (void*)0, sizeof(Settings));
if (settings.version != EEPROM_VERSION)
reset = true;
uint8_t *pd = (uint8_t*)&settings, *ps = (uint8_t*)&default_settings;
for (int8_t i = sizeof(Settings); i > 0; i--, pd++, ps++)
{
if (*pd == 0xff || reset)
*pd = pgm_read_byte(ps);
}
}
/*************************************************************
* EVENT QUEUE
*************************************************************/
#define EV_BTN_BIT 0
#define EV_BTN_MASK _BV(EV_BTN_BIT)
#define EV_BTN_0 0
#define EV_BTN_1 EV_BTN_MASK
#define EV_PRESS_BIT 1
#define EV_PRESS_MASK _BV(EV_PRESS_BIT)
#define EV_PRESS EV_PRESS_MASK
#define EV_RELEASE 0
#define EV_CODE_MASK (EV_PRESS_MASK | EV_BTN_MASK)
// Both buttons were held at some point; cleared only when both are released
#define EV_HOLD_CHORD_BIT 2
#define EV_HOLD_CHORD _BV(EV_HOLD_CHORD_BIT)
// Button 0 is held
#define EV_HOLD_BTN0_BIT 3
#define EV_HOLD_BTN0 _BV(EV_HOLD_BTN0_BIT)
// Button 1 is held
#define EV_HOLD_BTN1_BIT 4
#define EV_HOLD_BTN1 _BV(EV_HOLD_BTN1_BIT)
#define EV_HOLD_BTN_MASK (EV_HOLD_BTN0 | EV_HOLD_BTN1)
// Button 0 is held long
#define EV_HOLD_LONG_BTN0_BIT 5
#define EV_HOLD_LONG_BTN0 _BV(EV_HOLD_LONG_BTN0_BIT)
// Button 1 is held long
#define EV_HOLD_LONG_BTN1_BIT 6
#define EV_HOLD_LONG_BTN1 _BV(EV_HOLD_LONG_BTN1_BIT)
#define EV_HOLD_NONE 0
#define TEST_BITS_EQ(value, mask, test) (((value) & (mask)) == (test))
#define IS_BUTTON_EVENT(ev) ((ev) >= 0)
// The button was pressed.
#define IS_BUTTON_PRESS(ev, btn, held, not_held) \
TEST_BITS_EQ(ev, EV_CODE_MASK | EV_HOLD_LONG_BTN##btn | (held) | (not_held), EV_PRESS | EV_BTN_##btn | (held))
// The button was pressed and the others weren't held.
#define IS_BUTTON_PRESS_NEW(ev, btn) \
TEST_BITS_EQ(ev, EV_CODE_MASK | EV_HOLD_LONG_BTN##btn | EV_HOLD_BTN_MASK, EV_PRESS | EV_BTN_##btn | EV_HOLD_BTN##btn)
// The button was held for the long hold timeout.
#define IS_BUTTON_LONG_HOLD(ev, btn, held, not_held) \
TEST_BITS_EQ(ev, EV_CODE_MASK | EV_HOLD_LONG_BTN##btn | (held) | (not_held), EV_PRESS | EV_BTN_##btn | EV_HOLD_LONG_BTN##btn | (held))
// The button was released.
#define IS_BUTTON_RELEASE(ev, btn, held, not_held) \
TEST_BITS_EQ(ev, EV_CODE_MASK | (held) | (not_held), EV_RELEASE | EV_BTN_##btn | (held))
// The button was released after a short press.
// In the RELEASE event only the EV_HOLD_BTN# flag is cleared, while the other hold flags are updated later.
#define IS_BUTTON_RELEASE_SHORT(ev, btn, held, not_held) \
TEST_BITS_EQ(ev, EV_CODE_MASK | EV_HOLD_LONG_BTN##btn | (held) | (not_held), EV_RELEASE | EV_BTN_##btn | (held))
// The button was released after a long press.
#define IS_BUTTON_RELEASE_LONG(ev, btn, held, not_held) \
TEST_BITS_EQ(ev, EV_CODE_MASK | EV_HOLD_LONG_BTN##btn | (held) | (not_held), EV_RELEASE | EV_BTN_##btn | EV_HOLD_LONG_BTN##btn | (held))
// The button is held.
#define IS_BUTTON_HELD(ev, btn) !TEST_BITS_EQ(ev, EV_HOLD_BTN##btn, 0)
// All buttons are released.
#define IS_BUTTON_RELEASED_ALL(ev) TEST_BITS_EQ(ev, EV_HOLD_BTN_MASK, 0)
typedef int8_t event_code_t;
// Invalid event
#define EV_NONE ((event_code_t)-1)
// Queue overflowed
#define EV_OVERFLOW ((event_code_t)-2)
// Morse code sequence completed, time to update during wrap around
#define EV_MORSE_DONE_LED0 ((event_code_t)-3)
#define EV_MORSE_DONE_LED1 ((event_code_t)-4)
// New ADC results are ready
#define EV_ADC_SAMPLED ((event_code_t)-5)
// EVENT QUEUE
template<uint8_t Size>
class Queue {
volatile uint8_t myCount, myReadPtr, myWritePtr;
volatile event_code_t myBuffer[Size];
public:
event_code_t popWait() {
cli();
uint8_t cnt;
while ((cnt = myCount) == 0)
{
sei();
sleep_cpu();
cli();
}
uint8_t ptr = myReadPtr;
event_code_t rv = myBuffer[ptr];
myReadPtr = (ptr + 1) & (Size - 1);
myCount = cnt - 1;
sei();
return rv;
}
void push(event_code_t ev)
{
#if 0 //def WOKWI
Debug.print(F("E "));
Debug.print(ev, BIN);
Debug.println();
#endif
uint8_t cnt = myCount;
uint8_t wptr = myWritePtr;
if (cnt >= Size)
{
myBuffer[0] = EV_OVERFLOW;
myReadPtr = 0;
wptr = 1;
cnt = 1;
}
myBuffer[wptr] = ev;
myWritePtr = (wptr + 1) & (Size - 1);
myCount = cnt + 1;
}
};
Queue<8> event_queue;
/*************************************************************
* BUTTON MONITOR
*************************************************************/
#define BUTTON_LONG_HOLD_TIMEOUT MS_WITH_BUTTON_DIVISOR(512) // half a second
#define BUTTON_DEBOUNCE_SAMPLES 3
#define BUTTON_DEBOUNCE_MASK ((1 << BUTTON_DEBOUNCE_SAMPLES) - 1)
class ButtonState {
event_code_t status;
struct {
uint8_t log;
uint8_t timer;
} buttons[2];
template<int8_t idx, event_code_t HoldBit, event_code_t HoldLongBit, event_code_t BtnCode>
void doTick() {
if (TEST_BITS_EQ(status, HoldBit | HoldLongBit, HoldBit)) {
if (++buttons[idx].timer >= BUTTON_LONG_HOLD_TIMEOUT) {
status |= HoldLongBit;
event_queue.push(status | BtnCode | EV_PRESS);
}
}
}
template<int8_t idx, event_code_t HoldBit, event_code_t HoldLongBit, event_code_t BtnCode>
void doProcess(uint8_t is_up) {
// Store a history of the input pin state; 1 == up
uint8_t log = (buttons[idx].log << 1) | is_up;
buttons[idx].log = log;
// Convert ...0000 or ...1111 into 1 or 0. Other values mean unstable state.
uint8_t button_down_cur = (log + 1) & BUTTON_DEBOUNCE_MASK;
if (button_down_cur > 1)
return;
if (button_down_cur) {
if (TEST_BITS_EQ(status, HoldBit, 0)) {
status |= HoldBit;
if (TEST_BITS_EQ(status, EV_HOLD_BTN_MASK, EV_HOLD_BTN_MASK))
status |= EV_HOLD_CHORD;
event_queue.push(status | BtnCode | EV_PRESS);
buttons[idx].timer = 0;
}
}
else {
if (!TEST_BITS_EQ(status, HoldBit, 0)) {
event_queue.push((status & ~HoldBit) | BtnCode | EV_RELEASE);
status &= ~(HoldBit | HoldLongBit);
if (TEST_BITS_EQ(status, EV_HOLD_BTN_MASK, 0))
status &= ~EV_HOLD_CHORD;
}
}
}
public:
void init() {
buttons[0].log = buttons[1].log = -1;
}
event_code_t getStatus() {
return status;
}
void tick() {
doTick<0, EV_HOLD_BTN0, EV_HOLD_LONG_BTN0, EV_BTN_0>();
doTick<1, EV_HOLD_BTN1, EV_HOLD_LONG_BTN1, EV_BTN_1>();
}
void process() {
uint8_t pin = PINB;
doProcess<0, EV_HOLD_BTN0, EV_HOLD_LONG_BTN0, EV_BTN_0>((pin >> PIN_BUTTON0) & 1);
doProcess<1, EV_HOLD_BTN1, EV_HOLD_LONG_BTN1, EV_BTN_1>((pin >> PIN_BUTTON1) & 1);
}
} button_state;
/*************************************************************
* MORSE CODE TABLE
*************************************************************/
#define MORSE(c1, c2, c3, c4, c5, c6, c7, c8) \
(c1 | (c2 << 2) | (c3 << 4) | (c4 << 6) | (c5 << 8) | (c6 << 10) | (c7 << 12) | (c8 << 14))
#define DOT 1
#define DASH 2
#define LDASH 3
#define MORSE_TABLE_START '!'
#define MORSE_DASH_LENGTH 3
#define MORSE_LDASH_LENGTH 5
#define MORSE_CHAR_GAP 3
#define MORSE_WORD_GAP 7
const uint16_t morse_codes[] PROGMEM = {
MORSE(DASH, DOT, DASH, DOT, DASH, DASH, 0, 0), // '!'
MORSE(DOT, DASH, DOT, DOT, DASH, DOT, 0, 0), // '"'
MORSE(DOT, DOT, DOT, DOT, DOT, DOT, DOT, DOT), // '#' ERROR
MORSE(DOT, DOT, DASH, DASH, DOT, DASH, 0, 0), // '$'
MORSE(0, 0, 0, 0, 0, 0, 0, 0), // '%'
MORSE(DOT, DASH, DOT, DOT, DOT, 0, 0, 0), // '&'
MORSE(DOT, DASH, DASH, DASH, DASH, DOT, 0, 0), // '\''
MORSE(DASH, DOT, DASH, DASH, DOT, 0, 0, 0), // '('
MORSE(DASH, DOT, DASH, DASH, DOT, DASH, 0, 0), // ')'
MORSE(DOT, DASH, DOT, DASH, DOT, 0, 0, 0), // '*' NEXT MESSAGE
MORSE(DOT, DASH, DOT, DASH, DOT, 0, 0, 0), // '+'
MORSE(DASH, DASH, DOT, DOT, DASH, DASH, 0, 0), // ','
MORSE(DASH, DOT, DOT, DOT, DOT, DASH, 0, 0), // '-'
MORSE(DOT, DASH, DOT, DASH, DOT, DASH, 0, 0), // '.'
MORSE(DASH, DOT, DOT, DASH, DOT, 0, 0, 0), // '/'
#ifdef MORSE_FAST_NUMBERS
MORSE(LDASH, 0, 0, 0, 0, 0, 0, 0), // '0'
MORSE(DOT, LDASH, 0, 0, 0, 0, 0, 0), // '1'
MORSE(DOT, DOT, LDASH, 0, 0, 0, 0, 0), // '2'
MORSE(DOT, DOT, DOT, LDASH, 0, 0, 0, 0), // '3'
MORSE(DOT, DOT, DOT, DOT, DASH, 0, 0, 0), // '4'
MORSE(DOT, DOT, DOT, DOT, DOT, 0, 0, 0), // '5'
MORSE(DASH, DOT, DOT, DOT, DOT, 0, 0, 0), // '6'
MORSE(LDASH, DOT, DOT, DOT, 0, 0, 0, 0), // '7'
MORSE(LDASH, DOT, DOT, 0, 0, 0, 0, 0), // '8'
MORSE(LDASH, DOT, 0, 0, 0, 0, 0, 0), // '9'
#else
MORSE(DASH, DASH, DASH, DASH, DASH, 0, 0, 0), // '0'
MORSE(DOT, DASH, DASH, DASH, DASH, 0, 0, 0), // '1'
MORSE(DOT, DOT, DASH, DASH, DASH, 0, 0, 0), // '2'
MORSE(DOT, DOT, DOT, DASH, DASH, 0, 0, 0), // '3'
MORSE(DOT, DOT, DOT, DOT, DASH, 0, 0, 0), // '4'
MORSE(DOT, DOT, DOT, DOT, DOT, 0, 0, 0), // '5'
MORSE(DASH, DOT, DOT, DOT, DOT, 0, 0, 0), // '6'
MORSE(DASH, DASH, DOT, DOT, DOT, 0, 0, 0), // '7'
MORSE(DASH, DASH, DASH, DOT, DOT, 0, 0, 0), // '8'
MORSE(DASH, DASH, DASH, DASH, DOT, 0, 0, 0), // '9'
#endif
MORSE(DASH, DASH, DASH, DOT, DOT, DOT, 0, 0), // ':'
MORSE(DASH, DOT, DASH, DOT, DASH, DOT, 0, 0), // ';'
MORSE(DASH, DOT, DASH, DOT, DASH, 0, 0, 0), // '<' TRANSMSSION START
MORSE(DASH, DOT, DOT, DOT, DASH, 0, 0, 0), // '='
MORSE(DOT, DOT, DOT, DASH, DOT, DASH, 0, 0), // '>' END OF WORK
MORSE(DOT, DOT, DASH, DASH, DOT, DOT, 0, 0), // '?'
MORSE(DOT, DASH, DASH, DOT, DASH, DOT, 0, 0), // '@'
MORSE(DOT, DASH, 0, 0, 0, 0, 0, 0), // 'A'
MORSE(DASH, DOT, DOT, DOT, 0, 0, 0, 0), // 'B'
MORSE(DASH, DOT, DASH, DOT, 0, 0, 0, 0), // 'C'
MORSE(DASH, DOT, DOT, 0, 0, 0, 0, 0), // 'D'
MORSE(DOT, 0, 0, 0, 0, 0, 0, 0), // 'E'
MORSE(DOT, DOT, DASH, DOT, 0, 0, 0, 0), // 'F'
MORSE(DASH, DASH, DOT, 0, 0, 0, 0, 0), // 'G'
MORSE(DOT, DOT, DOT, DOT, 0, 0, 0, 0), // 'H'
MORSE(DOT, DOT, 0, 0, 0, 0, 0, 0), // 'I'
MORSE(DOT, DASH, DASH, DASH, 0, 0, 0, 0), // 'J'
MORSE(DASH, DOT, DASH, 0, 0, 0, 0, 0), // 'K'
MORSE(DOT, DASH, DOT, DOT, 0, 0, 0, 0), // 'L'
MORSE(DASH, DASH, 0, 0, 0, 0, 0, 0), // 'M'
MORSE(DASH, DOT, 0, 0, 0, 0, 0, 0), // 'N'
MORSE(DASH, DASH, DASH, 0, 0, 0, 0, 0), // 'O'
MORSE(DOT, DASH, DASH, DOT, 0, 0, 0, 0), // 'P'
MORSE(DASH, DASH, DOT, DASH, 0, 0, 0, 0), // 'Q'
MORSE(DOT, DASH, DOT, 0, 0, 0, 0, 0), // 'R'
MORSE(DOT, DOT, DOT, 0, 0, 0, 0, 0), // 'S'
MORSE(DASH, 0, 0, 0, 0, 0, 0, 0), // 'T'
MORSE(DOT, DOT, DASH, 0, 0, 0, 0, 0), // 'U'
MORSE(DOT, DOT, DOT, DASH, 0, 0, 0, 0), // 'V'
MORSE(DOT, DASH, DASH, 0, 0, 0, 0, 0), // 'W'
MORSE(DASH, DOT, DOT, DASH, 0, 0, 0, 0), // 'X'
MORSE(DASH, DOT, DASH, DASH, 0, 0, 0, 0), // 'Y'
MORSE(DASH, DASH, DOT, DOT, 0, 0, 0, 0), // 'Z'
};
uint8_t get_morse_symbol(char c, uint8_t offset)
{
uint8_t index = (c - MORSE_TABLE_START) << 1;
if (index >= sizeof(morse_codes))
return 0;
uint8_t b = pgm_read_byte(((uint8_t*)&morse_codes) + index + ((offset >> 2) & 1));
return (b >> ((offset & 3) << 1)) & 3;
}
/*************************************************************
* LED BLINK CONTROL
*************************************************************/
// 200ms duration seems readable with some focus to untrained eye
#define MORSE_DOT_TIMEOUT MS_WITH_BUTTON_DIVISOR(200)
volatile uint8_t led_state;
class LedBlinker {
uint8_t myIsMorse, myOnTicks, myOffTicks;
uint8_t myMorseIndex;
char myMorseBuffer[10];
void nextMorse(event_code_t ev)// __attribute__ ((noinline, noclone))
{
uint8_t idx = myMorseIndex >> 3;
uint8_t offset = myMorseIndex & 7;
char cc = myMorseBuffer[idx];
uint8_t sym = get_morse_symbol(cc, offset);
myOffTicks = 1;
/* Invalid codes are effectively skipped zero length. */
switch (sym) {
case DOT:
myOnTicks = 1;
break;
case DASH:
myOnTicks = MORSE_DASH_LENGTH;
break;
case LDASH:
myOnTicks = MORSE_LDASH_LENGTH;
break;
}
if (offset == 7 || get_morse_symbol(cc, offset + 1) == 0)
{
idx++;
if (idx >= sizeof(myMorseBuffer) || myMorseBuffer[idx] == 0) {
myMorseIndex = 0;
myOffTicks = MORSE_WORD_GAP;
}
else {
myMorseIndex = idx << 3;
myOffTicks = MORSE_CHAR_GAP;
}
}
else {
myMorseIndex++;
}
}
public:
bool isMorse() {
return myIsMorse;
}
int8_t tickBlink(uint8_t led)// __attribute__ ((noinline, noclone))
{
if (myOnTicks > 0) {
myOnTicks--;
led_state |= led;
return 1;
}
led_state &= ~led;
if (myOffTicks > 0) {
myOffTicks--;
return 2;
}
return 0;
}
void tickMorse(uint8_t led, event_code_t ev) {
int8_t rv = tickBlink(led);
if (rv)
{
if (rv != 1)
return;
}
else
{
nextMorse(ev);
tickBlink(led);
}
if (myOnTicks == 0 && myMorseIndex == 0)
event_queue.push(ev);
}
void blinkNormal(uint8_t duration) __attribute__ ((noinline, noclone))
{
cli();
myIsMorse = myOffTicks = 0;
myOnTicks = duration;
sei();
}
void blinkOff() {
blinkNormal(0);
}
StrBuf getMorseBuf() {
return StrBuf(myMorseBuffer, sizeof(myMorseBuffer));
}
StrBuf blinkMorse(uint8_t delay = MORSE_WORD_GAP) __attribute__ ((noinline, noclone))
{
cli();
myIsMorse = 1;
myOnTicks = 0;
if (delay > 0)
myOffTicks = delay;
myMorseIndex = 0;
myMorseBuffer[0] = 0;
sei();
// A delay of 1 is 200ms, so should be safe to release interrupts before copying data
return getMorseBuf();
}
void blinkMorse(char *str) {
blinkMorse().append(str);
}
void blinkMorse(const __FlashStringHelper *str) {
blinkMorse().append(str);
}
};
LedBlinker led_blinker[2];
/*************************************************************
* ADC
*************************************************************/
enum ADC_Task {
ADC_IDLE, // ADC is disabled
ADC_INIT_11, // ADC is initializing 1.1v reference
ADC_SAMPLE_FULL, // ADC is sampling full volume
ADC_SAMPLE_BAND, // ADC is sampling band volume
ADC_INIT_5, // ADC is initializing 5v reference for volume
ADC_SAMPLE_FULL_5, // ADC is sampling full volume at 5vref
ADC_SAMPLE_BAND_5, // ADC is sampling band volume at 5vref
ADC_INIT_5_RS, // ADC is initializing 5v reference for reset
ADC_SAMPLE_RESET, // ADC is sampling the reset pin at 5vref
};
#define ADC_VOLT_5 0
#define ADC_VOLT_11 _BV(REFS1)
#define ADC_MUX_FULL 3
#define ADC_MUX_BAND 2
#define ADC_MUX_RESET 0
//#define ADC_MUX_RESET 12 // voltage reference
#define ADC_CONVERT _BV(ADEN) | _BV(ADSC) | _BV(ADIE) | _BV(ADPS0) | _BV(ADPS1)
class ADCController {
uint8_t task;
public:
uint16_t vfull, vband;
uint8_t reset_ready, vreset;
void start() {
if (task != ADC_IDLE)
stop();
task = ADC_INIT_11;
PRR &= ~_BV(PRADC);
ADMUX = ADC_VOLT_11 | ADC_MUX_FULL;
ADCSRA = ADC_CONVERT;
}
void stop() {
ADCSRA = 0;
PRR |= _BV(PRADC);
task = ADC_IDLE;
}
void isr()
{
switch (task) {
case ADC_IDLE:
return;
case ADC_INIT_11:
task = ADC_SAMPLE_FULL;
break;
case ADC_SAMPLE_FULL:
vfull = ADC;
// If volume voltage is clipping, switch to 5V reference
if (vfull > 0x3f0) {
ADMUX = ADC_VOLT_5 | ADC_MUX_FULL;
task = ADC_INIT_5;
}
else {
ADMUX = ADC_VOLT_11 | ADC_MUX_BAND;
task = ADC_SAMPLE_BAND;
}
break;
case ADC_SAMPLE_BAND:
vband = ADC;
if (reset_ready)
goto done;
ADMUX = ADC_VOLT_5 | ADC_MUX_RESET;
task = ADC_INIT_5_RS;
break;
case ADC_INIT_5:
task = ADC_SAMPLE_FULL_5;
break;
case ADC_SAMPLE_FULL_5:
vfull = ADC;
vfull = (vfull << 2) + (vfull >> 1); // * 4.5
ADMUX = ADC_VOLT_5 | ADC_MUX_BAND;
task = ADC_SAMPLE_BAND_5;
break;
case ADC_SAMPLE_BAND_5:
vband = ADC;
vband = (vband << 2) + (vband >> 1); // * 4.5
if (reset_ready)
goto done;
ADMUX = ADC_VOLT_5 | ADC_MUX_RESET;
task = ADC_SAMPLE_RESET;
break;
case ADC_INIT_5_RS:
task = ADC_SAMPLE_RESET;
break;
case ADC_SAMPLE_RESET:
vreset = 255 - uint8_t(ADC);
reset_ready = 1;
goto done;
done:
event_queue.push(EV_ADC_SAMPLED);
default:
stop();
return;
}
ADCSRA = ADC_CONVERT;
}
};
ADCController adc;
ISR(ADC_vect)
{
adc.isr();
}
/*************************************************************
* DEBUG SERIAL LOGGING (OCCUPIES BUTTON/LED PINS)
*************************************************************/
#ifdef USI_DEBUG
uint8_t debug_buffer[20];
void usi_debug(uint8_t count)
{
// Uses 2-wire mode in SPI style (no ack bits). Requires ~1k pull ups instead of buttons.
PRR &= ~_BV(PRUSI);
PORTB |= _BV(PB0);
DDRB |= _BV(PB0) | _BV(PB2);
for (uint8_t i = 0; i < count; i++)
{
USIDR = debug_buffer[i];
USISR = _BV(USIOIF) | _BV(USISIF); // Reset counter and interrupt flags
do {
USICR = _BV(USIWM1) | _BV(USICS1) | _BV(USICLK) | _BV(USITC);
}
while ((USISR & _BV(USIOIF)) == 0);
}
}
#define DEBUG_BYTE(i, v) debug_buffer[i] = v
#define DEBUG_WORD(i, v) *(uint16_t*)&debug_buffer[i] = v
#define DEBUG_DWORD(i, v) *(uint32_t*)&debug_buffer[i] = v
#define DEBUG_SEND(i) usi_debug(i)
#else
#define DEBUG_BYTE(i, v)
#define DEBUG_WORD(i, v)
#define DEBUG_DWORD(i, v)
#define DEBUG_SEND(i)
#endif
/*************************************************************
* PULSE DETECTION
*************************************************************/
const uint8_t BOTTOM_CAP_RATE_BAND = 1;
const uint8_t BOTTOM_CAP_RATE_FULL = 1;
const uint8_t MIN_PULSE_START_THRESHOLD = 15;
const uint8_t SHAPE_DOWN = 0;
const uint8_t SHAPE_MIN = 1;
const uint8_t SHAPE_MAX = 2;
const uint8_t SHAPE_UP = 3;
class PulseDetector {
// Exponential moving average smoother for the input curves
struct Smoother {
uint16_t val_cur; // Current smoothed value
uint16_t val_next; // Next smoothed value (processing lags by one sample)
uint16_t val_bottom; // Smoothed min bound value
uint16_t val_rate; // Bottom value increased by fixed rate since last update
uint8_t shape; // Shape of the curve at val_cur
uint8_t rate;
void update(uint16_t val)
{
val_cur = val_next;
#if 1
uint16_t a = (val_next << 2) + val_next; // x * 5
uint16_t b = (val << 1) + val; // x * 3
val_next = (a + b) >> 3; // (a + b) / 8
#else
val_next = (val + val_next + (val_next << 1)) >> 2; // (a + 3*b) / 4
#endif
val_rate += rate;
if (val_next != val_cur)
{
// The shape code is a sequence of two 'up' bit flags
shape = (shape & 1) << 1;
shape = (val_next >= val_cur) ? shape + 1 : shape;
if (shape == SHAPE_MIN || val_cur < val_bottom)
{
uint16_t capped = min<uint16_t>(val_cur, val_rate);
val_bottom = val_rate = (val_bottom + capped) >> 1;
}
}
}
};
#ifdef USI_DEBUG
uint8_t count;
#endif
bool in_pulse;
Smoother sfull, sband;
// Snapshot of val_bottom at the start of the pulse
uint16_t vband_base, vfull_base;
uint16_t sband_prev;
// Last calculated band ratio
uint8_t ratio;
struct CurPulse {
uint8_t len; // Pulse length
uint8_t vband_max_len; // Location of the maximum pulse height point
uint16_t vband_max; // Maximum pulse height
uint32_t vband_sum; // Sum of pulse heights
uint32_t ratio_sum; // Sum of ratio weighted by height
uint32_t vband_max_sum; // Pulse height sum at maximum point
} cur_pulse;
uint8_t detect_pulse() __attribute__ ((noinline, noclone))
{
uint8_t increment = 1;
if (!in_pulse)
{
// Pulse start threshold
if (!((sband.shape == SHAPE_UP || sband.shape == SHAPE_MAX) && sband.val_cur > sband.val_rate + settings.pulse_start_threshold))
{
if (sband.val_cur != sband.val_next)
sband_prev = sband.val_cur;
gap_next = add_sat<uint8_t>(gap_next, 1);
return PulseNone;
}
in_pulse = true;
ratio = 0;
vband_base = sband.val_bottom;
vfull_base = sfull.val_bottom;
memset(&cur_pulse, 0, sizeof(cur_pulse));
// Estimate the already skipped length of the pulse from the derivative and time since last minimum
int16_t rate = max<int16_t>(sband.val_cur - sband_prev, sband.val_next - sband.val_cur);
if (rate > 0)
increment = min<uint8_t>(1 + (sband.val_cur - vband_base) / rate, (sband.val_rate - vband_base) / BOTTOM_CAP_RATE_BAND);
}
uint16_t vband_delta = sub_sat<uint16_t>(sband.val_cur, vband_base);
// Reset the start point upon rapid ramp up
if (cur_pulse.len && vband_delta > (cur_pulse.vband_max << 2))
{
memset(&cur_pulse, 0, sizeof(cur_pulse));
}
if (cur_pulse.len < 0xff)
{
cur_pulse.len += increment;
cur_pulse.vband_sum += vband_delta;
if (vband_delta > cur_pulse.vband_max)
{
cur_pulse.vband_max = vband_delta;
cur_pulse.vband_max_len = cur_pulse.len;
cur_pulse.vband_max_sum = cur_pulse.vband_sum;
}
// Compute and integrate the band ratio (0..64)
uint16_t vfull_delta = sub_sat<uint16_t>(sfull.val_cur, vfull_base);
if (vband_delta > vfull_delta)
vfull_delta = vband_delta;
if ((vband_delta & ~0x3ff) == 0)
{
if (vfull_delta)
ratio = (vband_delta << 6) / vfull_delta;
cur_pulse.ratio_sum += vband_delta * ratio;
}
else
{
if (vfull_delta)
ratio = (vband_delta << 4) / (vfull_delta >> 2);
cur_pulse.ratio_sum += uint32_t(vband_delta) * ratio;
}
}
// Terminate the pulse upon return to the start level, or hitting the rising bottom
if (sband.shape == SHAPE_DOWN && vband_delta <= (cur_pulse.vband_max >> 2) ||
sband.shape == SHAPE_MIN && sband.val_cur <= sband.val_bottom + (cur_pulse.vband_max >> 2))
{
// Reset bottom level to avoid triggering on noise in the pulse trailing end
sband.val_bottom = sband.val_rate = sband_prev = sband.val_cur;
in_pulse = false;
uint8_t rv = PulseGood;
if (cur_pulse.len < settings.pulse_len_min)
rv = PulseBadLenMin;
else if (cur_pulse.len > settings.pulse_len_max)
rv = PulseBadLenMax;
else if (cur_pulse.ratio_sum < cur_pulse.vband_sum * settings.pulse_band_ratio_min)
rv = PulseBadRatioMin;
else if (cur_pulse.vband_max < pow2_word(settings.pulse_sensitivity))
rv = PulseBadPeakMin;
if (rv == PulseGood)
{
gap_prev = gap_next;
pulse_len = cur_pulse.len;
gap_next = 0;
new_pulse = 1;
}
else
{
gap_next = add_sat<uint8_t>(gap_next, cur_pulse.len);
}
return rv;
}
return PulseNone;
}
public:
enum PulseResult {
PulseNone = 0,
PulseBadLenMin,
PulseBadLenMax,
PulseBadRatioMin,
PulseBadPeakMin,
PulseGood,
};
uint8_t gap_prev, pulse_len, gap_next;
uint8_t new_pulse;
PulseDetector()
{
sband.rate = BOTTOM_CAP_RATE_BAND;
sfull.rate = BOTTOM_CAP_RATE_FULL;
}
uint8_t update() //__attribute__ ((noinline, noclone))
{
DEBUG_BYTE(0, count | 0);
DEBUG_WORD(1, adc.vfull);
DEBUG_WORD(3, adc.vband);
DEBUG_SEND(1+4);
sfull.update(adc.vfull);
sband.update(adc.vband);
DEBUG_BYTE(0, count | 1);
DEBUG_WORD(1, sfull.val_next);
DEBUG_WORD(3, sfull.val_bottom);
DEBUG_WORD(5, sband.val_next);
DEBUG_WORD(7, sband.val_bottom);
DEBUG_WORD(9, sband.val_rate);
DEBUG_SEND(1+10);
uint8_t end = detect_pulse();
if (in_pulse || end) {
DEBUG_BYTE(0, count | 2);
DEBUG_BYTE(1, cur_pulse.len);
DEBUG_BYTE(2, ratio);
DEBUG_SEND(3);
}
if (end == PulseGood)
{
DEBUG_BYTE(0, count | 3);
DEBUG_BYTE(1, 0);
DEBUG_BYTE(2, cur_pulse.vband_max_len);
DEBUG_WORD(3, cur_pulse.vband_max);
DEBUG_DWORD(5, cur_pulse.vband_sum);
DEBUG_DWORD(9, cur_pulse.ratio_sum);
DEBUG_DWORD(13, cur_pulse.vband_max_sum);
DEBUG_SEND(3+2+4*3);
}
#ifdef USI_DEBUG
count += 8;
#endif
return end;
}
};
PulseDetector pulse_detector;
/*************************************************************
* CLOCK
*************************************************************/
struct {
uint8_t msec;
uint8_t tick; // <250; 1 tick = 4 msec
uint8_t sec, min, hour; // Time since dark/light switch
uint8_t up_day, up_hour;
} clock;
/*************************************************************
* NIGHT DETECTION
*************************************************************/
const uint8_t NIGHT_DARK_HYSTERESIS = 2;
class NightDetector {
uint16_t avg_val; // average darkness * 8
bool is_dark;
bool update_is_dark()
{
avg_val = (avg_val - dark_level) + adc.vreset; // (7*avg + 1*input)/8
dark_level = avg_val >> 3;
bool was_dark = is_dark;
uint8_t threshold = settings.dark_threshold;
if (was_dark)
is_dark = dark_level > sub_sat<uint8_t>(threshold, NIGHT_DARK_HYSTERESIS);
else
is_dark = dark_level >= add_sat<uint8_t>(threshold, NIGHT_DARK_HYSTERESIS);
WOKWI_DEBUG_PRINTLN(is_dark ? F("Is dark") : F("Is bright"));
return is_dark != was_dark;
}
public:
uint8_t dark_level;
bool is_night;
void init()
{
// When powering on, immediately set average and day/night based on the current value
dark_level = adc.vreset;
avg_val = dark_level << 3;
is_dark = false;
update_is_dark();
is_night = is_dark;
}
bool update()
{
if (adc.reset_ready != 1)
return false;
adc.reset_ready = 2;
if (update_is_dark())
{
cli();
clock.sec = clock.min = clock.hour = 0;
sei();
}
else if (is_dark != is_night && clock.hour >= settings.night_switch_delay[is_dark ? 1 : 0])
{
is_night = is_dark;
WOKWI_DEBUG_PRINTLN(is_night ? F("Is now night") : F("Is now day"));
if (!is_night) {
clock.up_hour = 0;
clock.up_day++;
}
return true;
}
return false;
}
};
NightDetector night_detector;
/*************************************************************
* PIEZO DRIVER
*************************************************************/
#define PIEZO_MIN_CLICK 5
#define PIEZO_LONG_BLINK 50
#define PIEZO_TEST_SHIFT 1
#define PIEZO_BEEP_SHIFT 3
#define PIEZO_BEEP_PERIOD 42
#define PIEZO_BEEP_LONG 100
class PiezoDriver {
enum {
MODE_OFF = 0,
MODE_TONE = 1,
MODE_CLICK = 2
};
uint8_t mode;
uint8_t duty_goal;
uint8_t period_min, period_max, period_tick_mask;
bool period_down;
uint8_t timer;
void enableOutput()
{
if (DDRB & _BV(PIN_SPEAKER))
return;
if ((PINB & _BV(PIN_SPEAKER)) == 0)
{
sei();
fatalError(F("SPKLO"));
}
PORTB |= _BV(PIN_SPEAKER);
DDRB |= _BV(PIN_SPEAKER);
}
void disableTimer()
{
TCCR1 = 0;
PRR |= _BV(PRTIM1);
}
void disable()
{
PORTB |= _BV(PIN_SPEAKER);
DDRB &= ~_BV(PIN_SPEAKER);
disableTimer();
mode = MODE_OFF;
}
void initiateStop()
{
if (mode == MODE_CLICK)
disable();
else
duty_goal = 0;
}
public:
void isr_tick()// __attribute__ ((noinline, noclone))
{
if (timer == 0)
return;
timer--;
if (timer == 0)
{
initiateStop();
}
else if (period_min != period_max && (clock.tick & period_tick_mask) == 0)
{
if (period_down) {
uint8_t new_period = OCR1C - 1;
if (new_period >= period_min) {
while (TCNT1 >= new_period - 5) {}
OCR1C = new_period;
}
if (new_period <= period_min)
period_down = false;
}
else {
uint8_t new_period = OCR1C + 1;
if (new_period <= period_max)
OCR1C = new_period;
if (new_period >= period_max)
period_down = true;
}
}
}
void isr_update()// __attribute__ ((noinline, noclone))
{
if (mode == MODE_TONE)
{
uint8_t duty = OCR1A;
if (duty > duty_goal)
OCR1A = duty - 1;
else if (duty < duty_goal)
OCR1A = duty + 1;
else if (timer == 0)
disable();
}
}
void click(uint8_t time = PIEZO_MIN_CLICK) __attribute__ ((noinline, noclone))
{
cli();
enableOutput();
PORTB &= ~_BV(PIN_SPEAKER);
disableTimer();
period_min = period_max = duty_goal = period_tick_mask = 0;
period_down = false;
timer = time;
mode = MODE_CLICK;
sei();
}
void tone(uint8_t freq_shift, uint8_t per_min, uint8_t per_max, uint8_t duty, uint8_t time, uint8_t wmask) //__attribute__ ((noinline, noclone))
{
cli();
enableOutput();
PRR &= ~_BV(PRTIM1);
timer = time;
period_min = per_min;
period_max = per_max;
duty_goal = min<uint8_t>(duty, period_min - 1);
period_tick_mask = (1 << wmask) - 1;
if (mode == MODE_TONE) {
uint8_t period = OCR1C;
if (period < period_min) {
OCR1C = period = period_min;
period_down = false;
}
else if (period > period_max) {
while (TCNT0 >= period_max - 5) {}
OCR1C = period = period_max;
period_down = true;
}
}
else {
TCNT1 = 0;
OCR1A = 0;
uint8_t period = period_min + ((per_max - per_min) >> 1);
OCR1C = period;
period_down = period >= period_max;
}
TCCR1 = _BV(PWM1A) | _BV(COM1A1)| _BV(COM1A0) | ((freq_shift + 1) & 0xf);
mode = MODE_TONE;
sei();
}
void tone(uint8_t freq_shift, uint8_t period, uint8_t duty, uint8_t time)
{
tone(freq_shift, period, period, duty, time, 0);
}
void beep(uint8_t time, uint8_t period = PIEZO_BEEP_PERIOD)
{
tone(PIEZO_BEEP_SHIFT, period, period >> 1, time);
}
void stop()
{
cli();
if (mode != MODE_OFF) {
timer = 0;
initiateStop();
}
sei();
}
};
PiezoDriver piezo;
/*************************************************************
* TIMER0 INTERRUPT (1 msec)
*************************************************************/
uint8_t morse_timer, morse_tick;
volatile uint8_t sec_timeout;
volatile uint16_t decay_timeout;
ISR(TIM0_COMPA_vect)
{
#ifndef USI_DEBUG
// Configure pins for reading buttons first, to let the voltage settle over a few cycles
DDRB &= ~PIN_BUTTONS_MASK;
PORTB |= PIN_BUTTONS_MASK;
#endif
// Update the timers and cycle through slow button related operations over multiple interrupts
switch ((++clock.msec) & (BUTTON_TIMER_DIVISOR - 1)) {
case 0: // ~20 usec
if (++clock.tick >= 250)
{
clock.tick = 0;
if (sec_timeout > 0)
sec_timeout--;
if (++clock.sec >= 60)
{
clock.sec = 0;
if (++clock.min >= 60)
{
clock.min = 0;
clock.hour++;
clock.up_hour++;
}
}
if (clock.sec == 0 || clock.sec == 30)
adc.reset_ready = 0;
}
if ((clock.tick & (SUBSEC_LENGTH - 1)) == 0 && decay_timeout != 0)
decay_timeout--;
#ifndef USI_DEBUG
// Update the button timer
button_state.tick();
#endif
// Determine if to update LEDs
if (++morse_timer >= MORSE_DOT_TIMEOUT) {
morse_timer = 0;
morse_tick = 1;
}
else
morse_tick = 0;
break;
case 1: // up to ~220 usec
#ifndef USI_DEBUG
if (!led_blinker[0].isMorse())
led_blinker[0].tickBlink(_BV(PIN_BUTTON0));
else if (morse_tick)
led_blinker[0].tickMorse(_BV(PIN_BUTTON0), EV_MORSE_DONE_LED0);
#endif
break;
case 2: // up to ~220 usec
#ifndef USI_DEBUG
if (!led_blinker[1].isMorse())
led_blinker[1].tickBlink(_BV(PIN_BUTTON1));
else if (morse_tick)
led_blinker[1].tickMorse(_BV(PIN_BUTTON1), EV_MORSE_DONE_LED1);
#endif
break;
case 3:
if (clock.tick & 1)
adc.start();
piezo.isr_tick();
break;
}
piezo.isr_update();
#ifndef USI_DEBUG
// Update the button state
button_state.process();
// Configure pins for LEDs
uint8_t leds = led_state;
PORTB &= ~leds;
DDRB |= leds;
#endif
}
/*************************************************************
* BARK DETECTOR
*************************************************************/
class BarkDetector
{
uint8_t series_counter;
uint8_t series_decay_time;
uint8_t hysteresis_cnt;
bool hysteresis_on;
void play_tone(uint8_t night)
{
uint8_t cnt = series_counter;
uint8_t mincnt = settings.bark_series_min[night];
WOKWI_DEBUG_PRINT(F("BS "));
WOKWI_DEBUG_PRINT(cnt);
WOKWI_DEBUG_PRINT(F(" <> "));
WOKWI_DEBUG_PRINTLN(mincnt);
if (cnt < mincnt)
return;
hysteresis_on = true;
uint8_t maxcnt = settings.bark_series_max[night];
uint16_t scale = 256;
if (maxcnt > mincnt && cnt < maxcnt)
scale = uint16_t(cnt - mincnt) * 256 / uint8_t(maxcnt - mincnt);
WOKWI_DEBUG_PRINT(F("S "));
WOKWI_DEBUG_PRINT(maxcnt);
WOKWI_DEBUG_PRINT(F(" = "));
WOKWI_DEBUG_PRINTLN(scale);
uint8_t minvol = settings.bark_tone_vol_min[night];
uint8_t maxvol = settings.bark_tone_vol_max[night];
if (maxvol < minvol)
maxvol = minvol;
uint8_t volume = minvol + uint8_t((maxvol - minvol) * scale >> 8);
volume = max<uint8_t>(0, min<uint8_t>(MAX_VOLUME, volume));
WOKWI_DEBUG_PRINT(F("V "));
WOKWI_DEBUG_PRINT(minvol);
WOKWI_DEBUG_PRINT(F(".."));
WOKWI_DEBUG_PRINT(maxvol);
WOKWI_DEBUG_PRINT(F("="));
WOKWI_DEBUG_PRINTLN(volume);
uint8_t duty = (uint16_t(settings.tone_max_duty) * pow2_byte(volume)) >> VOLUME_BIT_SHIFT;
WOKWI_DEBUG_PRINT(F("V "));
WOKWI_DEBUG_PRINT(minvol);
WOKWI_DEBUG_PRINT(F(".."));
WOKWI_DEBUG_PRINT(maxvol);
WOKWI_DEBUG_PRINT(F("="));
WOKWI_DEBUG_PRINT(volume);
WOKWI_DEBUG_PRINT(F(" D "));
WOKWI_DEBUG_PRINTLN(duty);
uint8_t minlen = settings.bark_tone_len_min[night];
uint8_t maxlen = settings.bark_tone_len_max[night];
if (maxlen < minlen)
maxlen = minlen;
uint8_t len = minlen + uint8_t((maxlen - minlen) * scale >> 8);
uint8_t time = len < MAX_TONE_LENGTH ? len * (256 / MAX_TONE_LENGTH) : 255;
WOKWI_DEBUG_PRINT(F("L "));
WOKWI_DEBUG_PRINT(minlen);
WOKWI_DEBUG_PRINT(F(".."));
WOKWI_DEBUG_PRINT(maxlen);
WOKWI_DEBUG_PRINT(F("="));
WOKWI_DEBUG_PRINT(len);
WOKWI_DEBUG_PRINT(F(" T "));
WOKWI_DEBUG_PRINTLN(time);
piezo.tone(0, settings.tone_period_min, settings.tone_period_max, duty, time, settings.tone_warble_rate);
tone_counter[night]++;
}
void reset_decay(uint8_t night, bool barked)
{
uint16_t timeout = settings.bark_series_decay[night];
if (series_counter <= settings.bark_series_min[night])
timeout += SUBSEC_COUNT * uint16_t(settings.bark_series_decay_slow[night]);
if (barked)
timeout += settings.bark_decay_delay;
decay_timeout = timeout;
}
public:
uint16_t bark_counter[2];
uint16_t tone_counter[2];
bool update() {
uint8_t night = night_detector.is_night ? 1 : 0;
if (pulse_detector.new_pulse && pulse_detector.gap_next >= settings.pulse_gap_min)
{
pulse_detector.new_pulse = 0;
bark_counter[night]++;
bool is_full = series_counter >= settings.bark_series_max[night] + settings.bark_series_over[night];
if ((!hysteresis_on || is_full) && hysteresis_cnt < settings.bark_series_hysteresis[night])
hysteresis_cnt++;
else if (!is_full)
series_counter++;
WOKWI_DEBUG_PRINT(F("++ "));
WOKWI_DEBUG_PRINT(series_counter);
WOKWI_DEBUG_PRINT(F("+"));
WOKWI_DEBUG_PRINT(hysteresis_cnt);
WOKWI_DEBUG_PRINTLN(hysteresis_on ? F(" +h") : F(" -h"));
play_tone(night);
reset_decay(night, true);
return true;
}
else
{
if (decay_timeout == 0)
{
if (series_counter > 0 && !(hysteresis_on && hysteresis_cnt > 0 && series_counter < settings.bark_series_min[night]))
series_counter--;
else if (hysteresis_cnt > 0)
hysteresis_cnt--;
if (series_counter == 0 && hysteresis_cnt == 0)
hysteresis_on = false;
WOKWI_DEBUG_PRINT(F("-- "));
WOKWI_DEBUG_PRINT(series_counter);
WOKWI_DEBUG_PRINT(F("+"));
WOKWI_DEBUG_PRINT(hysteresis_cnt);
WOKWI_DEBUG_PRINTLN(hysteresis_on ? F(" +h") : F(" -h"));
reset_decay(night, false);
}
return false;
}
}
};
BarkDetector bark_detector;
/*************************************************************
* INTERFACE
*************************************************************/
void fatalError(const __FlashStringHelper *code)
{
led_blinker[0].blinkMorse(MORSE_CHAR_GAP).append(F("SOS"));
led_blinker[1].blinkMorse(MORSE_CHAR_GAP).append(code);
for (;;)
sleep_cpu();
}
struct MenuDef {
uint8_t offset;
char name[4];
uint8_t minval;
uint8_t maxval;
uint8_t feedback;
};
const MenuDef menu_items[] PROGMEM = {
#define FBADC(code) PulseDetector::code
#define ITEM(field, name, minval, maxval, feedback) { ((uint8_t*)&settings.field) - ((uint8_t*)&settings), name, minval, maxval, feedback },
#define ITEMI(field, idx, name, minval, maxval, feedback) ITEM(field[idx], name, minval, maxval, feedback)
#define ITEM_PAGE_BREAK { 0x80, "" },
SETTINGS_MENU
#undef ITEM
#undef ITEMI
#undef ITEM_PAGE_BREAK
#undef FBADC
{ 0xFF, "" }
};
namespace menudef {
enum Index {
#define ITEM(field, name, minval, maxval, feedback) field,
#define ITEMI(field, idx, name, minval, maxval, feedback) ITEM(field ## idx, name, minval, maxval, feedback)
#define ITEM_PAGE_BREAK MAKE_UNIQUE_NAME(_dummy),
SETTINGS_MENU
#undef ITEM
#undef ITEMI
#undef ITEM_PAGE_BREAK
Last
};
}
bool set_menu_item_value(uint8_t item, uint8_t &value)
{
switch (item)
{
case menudef::dark_threshold:
value = adc.vreset;
return true;
case menudef::night_switch_delay0:
case menudef::night_switch_delay1:
value = clock.hour;
return true;
default:
return false;
}
}
void set_menu_piezo_tone(uint8_t item, bool refresh)
{
switch (item)
{
case menudef::tone_period_min:
piezo.tone(0, settings.tone_period_min, settings.tone_period_min>>1, 250);
break;
case menudef::tone_period_max:
piezo.tone(0, settings.tone_period_max, settings.tone_period_min>>1, 250);
break;
case menudef::tone_max_duty:
piezo.tone(0, (settings.tone_period_min + settings.tone_period_max) >> 1, settings.tone_max_duty, 250);
break;
case menudef::tone_warble_rate:
piezo.tone(PIEZO_TEST_SHIFT, settings.tone_period_min, settings.tone_period_max, settings.tone_max_duty, 250, settings.tone_warble_rate);
break;
default:
if (!refresh)
piezo.stop();
}
}
void interface_menu(int8_t ev, uint8_t item)
{
int8_t btn_ev = 0;
MenuDef entry;
MenuDef *item_entry;
adc.reset_ready = 0;
#define ITEM_VALUE (*(((uint8_t*)&settings) + entry.offset))
item_entry = &menu_items[item];
if (IS_BUTTON_EVENT(ev))
{
btn_ev = ev;
if (IS_BUTTON_HELD(ev, 0))
{
if (IS_BUTTON_HELD(ev, 1))
goto mode_switch_item;
goto mode_switch_item_release;
}
}
mode_edit_item_enter:
sec_timeout = 240;
pgm_memcpy(&entry, item_entry, sizeof(entry));
led_blinker[0].blinkMorse(MORSE_CHAR_GAP).appendPGM(item_entry->name, sizeof(entry.name));
set_menu_piezo_tone(item, false);
/*WOKWI_DEBUG_PRINT(F("item_enter "));
WOKWI_DEBUG_PRINT(item);
WOKWI_DEBUG_PRINTLN(entry.name);*/
goto mode_refresh_item_value;
mode_edit_item_value:
set_menu_piezo_tone(item, true);
mode_refresh_item_value:
led_blinker[1].blinkMorse().appendDec(ITEM_VALUE);
//WOKWI_DEBUG_PRINTLN(ITEM_VALUE);
mode_edit_item:
for (;;) {
ev = event_queue.popWait();
if (ev == EV_ADC_SAMPLED) {
adc.reset_ready = 0;
uint8_t pulse = pulse_detector.update();
if (entry.feedback) {
if (pulse > entry.feedback)
piezo.click();
// Don't timeout in pulse detector tuning mode
sec_timeout = 240;
}
set_menu_piezo_tone(item, true);
}
else if (ev == EV_MORSE_DONE_LED1)
goto mode_refresh_item_value;
else if (IS_BUTTON_EVENT(ev))
{
btn_ev = ev;
sec_timeout = 240;
if (IS_BUTTON_RELEASE_SHORT(ev, 0, EV_HOLD_NONE, EV_HOLD_CHORD))
{
uint8_t &p = ITEM_VALUE;
if (p > entry.minval) --p;
goto mode_edit_item_value;
}
if (IS_BUTTON_RELEASE_LONG(ev, 0, EV_HOLD_NONE, EV_HOLD_CHORD))
{
uint8_t &p = ITEM_VALUE;
if (p > entry.minval)
p -= min<uint8_t>(10, p - entry.minval);
goto mode_edit_item_value;
}
else if (IS_BUTTON_RELEASE_SHORT(ev, 1, EV_HOLD_NONE, EV_HOLD_CHORD))
{
uint8_t &p = ITEM_VALUE;
if (p < entry.maxval) ++p;
goto mode_edit_item_value;
}
else if (IS_BUTTON_RELEASE_LONG(ev, 1, EV_HOLD_NONE, EV_HOLD_CHORD))
{
uint8_t &p = ITEM_VALUE;
if (p < entry.maxval)
p += min<uint8_t>(10, entry.maxval - p);
goto mode_edit_item_value;
}
else if (IS_BUTTON_PRESS(ev, 1, EV_HOLD_BTN0, 0))
goto mode_switch_item_next;
else if (IS_BUTTON_RELEASE_SHORT(ev, 0, EV_HOLD_BTN1, 0))
{
uint8_t &p = ITEM_VALUE;
if (set_menu_item_value(item, p))
{
if (p < entry.minval)
p = entry.minval;
else if (p > entry.maxval)
p = entry.maxval;
piezo.click();
}
else
piezo.beep(PIEZO_BEEP_LONG);
goto mode_edit_item_value;
}
else if (IS_BUTTON_LONG_HOLD(ev, 0, EV_HOLD_BTN1, 0))
{
piezo.click();
goto exit;
}
}
if (sec_timeout == 0)
{
ev = btn_ev;
goto exit;
}
}
mode_switch_item_release:
/*WOKWI_DEBUG_PRINT(F("item_release "));
WOKWI_DEBUG_PRINTLN(item);*/
led_blinker[1].blinkMorse(MORSE_CHAR_GAP).appendPGM(item_entry->name, sizeof(entry.name));
mode_switch_item:
for (;;) {
ev = event_queue.popWait();
if (IS_BUTTON_EVENT(ev))
{
btn_ev = ev;
if (IS_BUTTON_RELEASED_ALL(ev))
goto mode_edit_item_enter;
if (IS_BUTTON_PRESS(ev, 1, EV_HOLD_BTN0, 0))
{
mode_switch_item_next:
piezo.stop();
item++;
item_entry++;
if (pgm_read_byte(&item_entry->offset) & 0x80)
{
while (item > 0 && (pgm_read_byte(&item_entry[-1].offset) & 0x80) == 0)
{
item--;
item_entry--;
}
}
/*WOKWI_DEBUG_PRINT(F("next "));
WOKWI_DEBUG_PRINTLN(item);*/
}
else if (IS_BUTTON_LONG_HOLD(ev, 1, EV_HOLD_BTN0, EV_HOLD_NONE))
{
piezo.click();
for (;;) {
item++;
item_entry++;
uint8_t c = pgm_read_byte(&item_entry->offset);
if (c & 0x80)
{
if (c == 0xff)
{
item = 0;
item_entry = menu_items;
}
else
{
item++;
item_entry++;
}
break;
}
}
}
else if (IS_BUTTON_RELEASE(ev, 1, EV_HOLD_NONE, EV_HOLD_NONE))
goto mode_switch_item_release;
}
}
#undef ITEM_VALUE
exit:
led_blinker[0].blinkOff();
led_blinker[1].blinkOff();
while (!(IS_BUTTON_EVENT(ev) && IS_BUTTON_RELEASED_ALL(ev)))
ev = event_queue.popWait();
}
struct ValueDef {
void *ptr;
char name[4];
uint8_t size;
bool resettable;
};
const uint8_t fw_version = FW_VERSION;
uint8_t reboot_reason;
const ValueDef valuedefs[] PROGMEM = {
{ &night_detector.is_night, "N", 1, false }, /* =o */
{ &clock.hour, "H", 1, false }, /* oooo */
{ &clock.min, "M", 1, false }, /* == */
{ &clock.up_day, "UD", 1, true }, /* oo= =oo */
{ &clock.up_hour, "UH", 1, true }, /* oo= oooo */
{ &bark_detector.bark_counter[0], "DB", 2, true }, /* =oo =ooo */
{ &bark_detector.tone_counter[0], "DT", 2, true }, /* =oo = */
{ &bark_detector.bark_counter[1], "NB", 2, true }, /* =o =ooo */
{ &bark_detector.tone_counter[1], "NT", 2, true }, /* =o = */
{ &adc.vreset, "K", 1, false }, /* =o= */
{ &fw_version, "V", 1, false }, /* ooo= */
{ &reboot_reason, "RR", 1, false }, /* o=o o=o */
{ nullptr, "", 0, false },
};
void interface_view_values(int8_t ev)
{
int8_t btn_ev = ev;
ValueDef entry;
ValueDef *item_entry = valuedefs;
piezo.click();
while (!(IS_BUTTON_EVENT(ev) && IS_BUTTON_RELEASED_ALL(ev)))
ev = event_queue.popWait();
adc.reset_ready = 0;
next_entry:
sec_timeout = 240;
pgm_memcpy(&entry, item_entry, sizeof(entry));
if (!entry.ptr)
{
item_entry = valuedefs;
goto next_entry;
}
led_blinker[0].blinkMorse(MORSE_CHAR_GAP).append(entry.name, sizeof(entry.name));
next_value:
led_blinker[1].blinkMorse().appendDec(entry.size == 2 ? *(uint16_t*)entry.ptr : *(uint8_t*)entry.ptr);
for (;;) {
ev = event_queue.popWait();
if (ev == EV_ADC_SAMPLED)
{
adc.reset_ready = 0;
pulse_detector.update();
bark_detector.update();
}
else if (ev == EV_MORSE_DONE_LED1)
goto next_value;
else if (IS_BUTTON_EVENT(ev))
{
btn_ev = ev;
if (IS_BUTTON_RELEASE_SHORT(ev, 0, EV_HOLD_NONE, EV_HOLD_CHORD))
{
item_entry++;
goto next_entry;
}
else if (IS_BUTTON_LONG_HOLD(ev, 0, EV_HOLD_NONE, EV_HOLD_CHORD))
{
break;
}
else if (IS_BUTTON_RELEASE_LONG(ev, 1, EV_HOLD_NONE, EV_HOLD_CHORD))
{
if (entry.resettable)
{
memset(entry.ptr, 0, entry.size);
goto next_value;
}
}
}
if (sec_timeout == 0)
{
ev = btn_ev;
break;
}
}
led_blinker[0].blinkOff();
led_blinker[1].blinkOff();
piezo.click();
while (!(IS_BUTTON_EVENT(ev) && IS_BUTTON_RELEASED_ALL(ev)))
ev = event_queue.popWait();
}
void interface_piezo_test(int8_t ev)
{
for (;;) {
piezo.tone(0, settings.tone_period_min, settings.tone_period_max,
settings.tone_max_duty, 250, settings.tone_warble_rate);
ev = event_queue.popWait();
if (ev == EV_ADC_SAMPLED)
{
night_detector.update();
pulse_detector.update();
}
else if (IS_BUTTON_EVENT(ev) && IS_BUTTON_RELEASED_ALL(ev))
break;
}
piezo.stop();
}
void main_loop()
{
for (;;)
{
int8_t ev = event_queue.popWait();
if (ev == EV_ADC_SAMPLED)
{
if (night_detector.is_night && adc.reset_ready == 1)
led_blinker[0].blinkNormal(5);
if (night_detector.update())
led_blinker[0].blinkNormal(250);
uint8_t pulse = pulse_detector.update();
if (bark_detector.update())
led_blinker[1].blinkNormal(50);
else if (pulse == PulseDetector::PulseBadPeakMin)
led_blinker[1].blinkNormal(5);
}
else if (IS_BUTTON_EVENT(ev))
{
if (IS_BUTTON_RELEASE_SHORT(ev, 0, EV_HOLD_NONE, EV_HOLD_CHORD))
{
if (settings.pulse_sensitivity < MAX_PULSE_SENSITIVITY)
{
settings.pulse_sensitivity++;
save_settings();
}
}
else if (IS_BUTTON_RELEASE_SHORT(ev, 1, EV_HOLD_NONE, EV_HOLD_CHORD))
{
if (settings.pulse_sensitivity > 0)
{
settings.pulse_sensitivity--;
save_settings();
}
}
else if (IS_BUTTON_RELEASE(ev, 1, EV_HOLD_BTN0, 0))
{
interface_menu(ev, 0);
save_settings();
}
else if (IS_BUTTON_LONG_HOLD(ev, 0, EV_HOLD_NONE, EV_HOLD_CHORD))
{
interface_view_values(ev);
}
else if (IS_BUTTON_LONG_HOLD(ev, 1, EV_HOLD_NONE, EV_HOLD_CHORD))
{
interface_piezo_test(ev);
}
}
}
}
int main(void)
{
reboot_reason = MCUSR;
MCUSR = 0;
// Ensure 8x clock divider (1MHz clock)
CLKPR = _BV(CLKPCE);
CLKPR = _BV(CLKPS0) | _BV(CLKPS1);
// Set up the timer to fire every msec
TCCR0A = _BV(WGM01); // CTC mode
TCCR0B = _BV(CS01); // clock frequency / 8
OCR0A = TIMER_DIVISOR - 1; // Output compare
OCR0B = 0;
TCNT0 = 0; // Set counter 0 to zero
TIMSK = _BV(OCIE0A); // Enable overflow interrupt
// Disable digital input on analog inputs
DIDR0 = _BV(ADC2D) | _BV(ADC3D);
// Disable unused modules
PRR = _BV(PRTIM1) | _BV(PRUSI) | _BV(PRADC);
// Pull up on speaker control
PORTB = _BV(PIN_SPEAKER);
DDRB = 0;
#ifdef WOKWI
Debug.begin();
#endif
load_settings();
button_state.init();
led_state = _BV(PIN_BUTTON0);
sei(); //Enable global interrupts
// Enable sleep between interrupts
MCUCR = _BV(SE);
// Wait for first ADC results and initialize day/night tracker
while (event_queue.popWait() != EV_ADC_SAMPLED || adc.reset_ready != 1) {}
night_detector.init();
// UI loop
main_loop();
}
Debounce works well in a real circuit
but not in wokwi for some reason,
so bounce is disabled on buttons.
Night/Day