diff --git a/LICENSE b/LICENSE.md similarity index 53% rename from LICENSE rename to LICENSE.md index 33232ed..ff4b992 100644 --- a/LICENSE +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Dirtbags +Copyright © 2020 Neale Pickett Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +The software is provided "as is", without warranty of any kind, express or +implied, including but not limited to the warranties of merchantability, +fitness for a particular purpose and noninfringement. In no event shall the +authors or copyright holders be liable for any claim, damages or other +liability, whether in an action of contract, tort or otherwise, arising from, +out of or in connection with the software or the use or other dealings in the +software. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..754818a --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +FQBN = adafruit:samd:adafruit_trinket_m0 + +install: holiday-lights.ino + arduino --upload --board $(FQBN) $@ diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..afe0625 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,12 @@ +TIME FOR SOME INFORMATION THEORY! + +We need to pack morse code. The most efficient packing I can think of is a stream of base 3 ints: + 0 = END, 1 = DIT, 2 = DAH + +We have to encode into 8-bit bytes, so the smallest atomic (quick to index) encoded chunk is: + int('22222', 3) = 242 + +Given the last int must be 0, we can encode anything up to length 4. +But I need to encode the prosign ARK = .-.-.-.- length 8. +So I need two bytes. +Well, if I'm going to use two bytes, I may as well just encode a length and a bitmask. diff --git a/durations.h b/durations.h new file mode 100644 index 0000000..68c6e16 --- /dev/null +++ b/durations.h @@ -0,0 +1,7 @@ +#pragma once + +#define DURATION_MILLISECOND 1L +#define DURATION_SECOND (1000 * DURATION_MILLISECOND) +#define DURATION_MINUTE (60 * DURATION_SECOND) +#define DURATION_HOUR (60 * DURATION_MINUTE) +#define DURATION_DAY (24 * DURATION_HOUR) diff --git a/holiday-lights.ino b/holiday-lights.ino index ecac73b..74ecd9c 100644 --- a/holiday-lights.ino +++ b/holiday-lights.ino @@ -4,7 +4,9 @@ #include #include "bounce2.h" +#include "durations.h" #include "morse.h" +#include "pulse.h" // Do you want it to run forever, or cycle every 24 hours? #ifdef TINY @@ -14,8 +16,10 @@ #endif // Which pin your LED strip is connected to -#ifdef TINY +#if defined(TINY) #define NEOPIXEL_PIN 3 +#elif defined(ADAFRUIT_TRINKET_M0) +#define NEOPIXEL_PIN 2 #else #define NEOPIXEL_PIN 6 #endif @@ -35,42 +39,40 @@ // How many milliseconds between activity in one group #define DELAY_MS 600 +// Each group has this percentage chance of +#define GROUP_UPDATE_PROBABILITY 25 + // What percentage chance a chosen light has of being on #define ACTIVITY 40 // Which pixel to flash messages in morse code: -1 = disable #define MORSE_PIXEL 8 -#define MORSE_COLOR 0x880000 +#define MORSE_COLOR CRGB::Red // How long a dit lasts -#define DIT_DURATION_MS 100 +#define DIT_DURATION_MS 150 // Color for all-white mode #define WHITE 0x886655 -// Some units of time -#define MILLISECOND 1L -#define SECOND (1000 * MILLISECOND) -#define MINUTE (60 * SECOND) -#define HOUR (60 * MINUTE) -#define DAY (24 * HOUR) - // The Neopixel library masks interrupts while writing. // This means you lose time. // How much time do you lose? // I'm guessing 10 minutes a day. -#define SNOSSLOSS_DAY (DAY - (10 * MINUTE)) -#define ON_FOR (5 * HOUR) +#define SNOSSLOSS_DAY (DURATION_DAY - (10 * DURATION_MINUTE)) +#define ON_FOR (5 * DURATION_HOUR) -const char *messages[] = { - "seasons greetings", - "happy holiday", - "merry xmas", - "happy new year", - "CQ CQ OF9X", -}; -const int nmessages = sizeof(messages) / sizeof(*messages); +#define ARK "\x03\x04" +const char *message = ( + + "seasons greetings" ARK + "happy holiday" ARK + "merry xmas" ARK + "happy new year" ARK + "CQ CQ OF9X" ARK + +); // These colors mirror pretty closely some cheapo LED lights we have const uint32_t colors[] = { @@ -100,80 +102,67 @@ void setup() { pinMode(LED_BUILTIN, OUTPUT); } -bool strandUpdate(bool white) { - static unsigned long nextEventMillis = 0; - unsigned long now = millis(); - static int group = 0; +Pulse mainPulse = Pulse(DELAY_MS); - if (now < nextEventMillis) { +bool strandUpdate(bool white) { + if (!mainPulse.Ticked()) { return false; } - int pos = (group * LEDS_PER_GROUP) + random(LEDS_PER_GROUP); - if (random(100) < ACTIVITY) { - uint32_t color; + for (int group = 0; group < NUM_GROUPS; ++group) { + int pos = (group * LEDS_PER_GROUP) + random(LEDS_PER_GROUP); + if (random(100) < GROUP_UPDATE_PROBABILITY) { + if (random(100) < ACTIVITY) { + uint32_t color; - if (white) { - color = WHITE; - } else { - color = colors[random(ncolors)]; + if (white) { + color = WHITE; + } else { + color = colors[random(ncolors)]; + } + leds[pos] = color; + } else { + leds[pos] = 0; + } + group = (group + 1) % NUM_GROUPS; } - leds[pos] = color; - } else { - leds[pos] = 0; } - group = (group + 1) % NUM_GROUPS; - nextEventMillis = now + (DELAY_MS / NUM_GROUPS); return true; } -bool morseUpdate() { - static MorseEncoder enc; - static unsigned long nextEventMillis = 0; - static bool ARK = true; - unsigned long now = millis(); - bool ret = false; - - if (now >= nextEventMillis) { - nextEventMillis = now + DIT_DURATION_MS; - if (!enc.Tick()) { - ARK = !ARK; - if (ARK) { - enc.SetText("ARK"); - enc.Quiet(MORSE_PAUSE_WORD); - } else { -#ifdef TINY - // I have tried twenty different ways to make this work on the ATTINY, - // including a big switch statement. It always freezes the program. - // I give up. Maybe it's a compiler bug having to do with a modulo. - enc.SetText(messages[0]); -#else - enc.SetText(messages[now % nmessages]); -#endif - enc.Quiet(200); - } - } - ret = true; - } - leds[MORSE_PIXEL] = enc.Transmitting ? MORSE_COLOR : 0; - return ret; -} - bool black() { - static unsigned long nextEventMillis = 0; - unsigned long now = millis(); - - if (now < nextEventMillis) { + if (!mainPulse.Ticked()) { return false; } FastLED.clear(); - nextEventMillis = now + (DELAY_MS / NUM_GROUPS); // Keep timing consistent return true; } +bool morseUpdate() { + static MorseEncoder enc; + static Pulse pulse = Pulse(DIT_DURATION_MS); + + leds[MORSE_PIXEL] = enc.Transmitting ? MORSE_COLOR : CRGB::Black; + if (pulse.Ticked()) { + if (!enc.Tick()) { + enc.SetText(message); + } + return true; + } + return false; +} + +bool millisUpdate() { + unsigned long now = millis(); + for (int i = 0; i < sizeof(unsigned long) * 8; ++i) { + leds[i] = CHSV(0, 255, bitRead(now, i) ? 32 : 0); + } + return false; +} + void loop() { static bool white = false; bool update = false; @@ -188,6 +177,7 @@ void loop() { update |= morseUpdate(); } else { update |= black(); + update |= millisUpdate(); } if (update) { diff --git a/morse.cpp b/morse.cpp index cb472ab..adf0701 100644 --- a/morse.cpp +++ b/morse.cpp @@ -1,42 +1,122 @@ #include "morse.h" -// Each morse dit/dah is stored as a nybble - -#define dit 1 -#define dah 3 - -#define Pack(a, b, c, d) ((a << 0) | (b << 2) | (c << 4) | (d << 6)) -#define Unpack(m, pos) ((m >> (pos * 2)) & 0b11) - -const uint8_t MorseLetters[] = { - Pack(dit, dah, 0, 0), // A - Pack(dah, dit, dit, dit), // B - Pack(dah, dit, dah, dit), // C - Pack(dah, dit, dit, 0), // D - Pack(dit, 0, 0, 0), // E - Pack(dit, dit, dah, dit), // F - Pack(dah, dah, dit, 0), // G - Pack(dit, dit, dit, dit), // H - Pack(dit, dit, 0, 0), // I - Pack(dit, dah, dah, dah), // J - Pack(dah, dit, dah, 0), // K - Pack(dit, dah, dit, dit), // L - Pack(dah, dah, 0, 0), // M - Pack(dah, dit, 0, 0), // N - Pack(dah, dah, dah, 0), // O - Pack(dit, dah, dah, dit), // P - Pack(dah, dah, dit, dah), // Q - Pack(dit, dah, dit, 0), // R - Pack(dit, dit, dit, 0), // S - Pack(dah, 0, 0, 0), // T - Pack(dit, dit, dah, 0), // U - Pack(dit, dit, dit, dah), // V - Pack(dit, dah, dah, 0), // W - Pack(dah, dit, dit, dah), // X - Pack(dah, dit, dah, dah), // Y - Pack(dah, dah, dit, dit), // Z +struct MorseSign { + // Length of this sign. If Length is 0, Bits is the duration of a pause. + uint8_t Length; + uint8_t Bits; }; +const struct MorseSign GetMorseSign(char c) { + switch (c) { + case 3: // ETX - End of Text + return (struct MorseSign){8, 0b01010101}; + case 4: // EOT - End of Transmission + return (struct MorseSign){0, MORSE_PAUSE_TRANSMISSION}; + case ' ': + return (struct MorseSign){0, MORSE_PAUSE_WORD}; + case '0': + return (struct MorseSign){5, 0b11111}; + case '1': + return (struct MorseSign){5, 0b01111}; + case '2': + return (struct MorseSign){5, 0b00111}; + case '3': + return (struct MorseSign){5, 0b00011}; + case '4': + return (struct MorseSign){5, 0b00001}; + case 5: + return (struct MorseSign){5, 0b00000}; + case 6: + return (struct MorseSign){5, 0b10000}; + case 7: + return (struct MorseSign){5, 0b11000}; + case 8: + return (struct MorseSign){5, 0b11100}; + case 9: + return (struct MorseSign){5, 0b11110}; + case 'a': + case 'A': + return (struct MorseSign){2, 0b01}; + case 'b': + case 'B': + return (struct MorseSign){4, 0b1000}; + case 'c': + case 'C': + return (struct MorseSign){4, 0b1010}; + case 'd': + case 'D': + return (struct MorseSign){3, 0b100}; + case 'e': + case 'E': + return (struct MorseSign){1, 0b0}; + case 'f': + case 'F': + return (struct MorseSign){4, 0b0010}; + case 'g': + case 'G': + return (struct MorseSign){3, 0b110}; + case 'h': + case 'H': + return (struct MorseSign){4, 0b0000}; + case 'i': + case 'I': + return (struct MorseSign){2, 0b00}; + case 'j': + case 'J': + return (struct MorseSign){4, 0b0111}; + case 'k': + case 'K': + return (struct MorseSign){3, 0b101}; + case 'l': + case 'L': + return (struct MorseSign){4, 0b0100}; + case 'm': + case 'M': + return (struct MorseSign){2, 0b11}; + case 'n': + case 'N': + return (struct MorseSign){2, 0b10}; + case 'o': + case 'O': + return (struct MorseSign){3, 0b111}; + case 'p': + case 'P': + return (struct MorseSign){4, 0b0110}; + case 'q': + case 'Q': + return (struct MorseSign){4, 0b1101}; + case 'r': + case 'R': + return (struct MorseSign){3, 0b010}; + case 's': + case 'S': + return (struct MorseSign){3, 0b000}; + case 't': + case 'T': + return (struct MorseSign){1, 0b1}; + case 'u': + case 'U': + return (struct MorseSign){3, 0b001}; + case 'v': + case 'V': + return (struct MorseSign){4, 0b0001}; + case 'w': + case 'W': + return (struct MorseSign){3, 0b011}; + case 'x': + case 'X': + return (struct MorseSign){4, 0b1001}; + case 'y': + case 'Y': + return (struct MorseSign){4, 0b1011}; + case 'z': + case 'Z': + return (struct MorseSign){4, 0b1100}; + default: + return (struct MorseSign){0, 0}; + } +} + MorseEncoder::MorseEncoder() { SetText(""); } @@ -46,7 +126,7 @@ MorseEncoder::MorseEncoder(const char *s) { void MorseEncoder::SetText(const char *s) { p = s; - crumb = 0; + bit = 0; ticksLeft = 0; Transmitting = false; } @@ -75,45 +155,29 @@ bool MorseEncoder::Tick() { return true; } - // If that was the end of the letter, we have to pause more - if (crumb == 4) { - crumb = 0; - ++p; - ticksLeft = MORSE_PAUSE_LETTER - MORSE_DIT; - return true; - } - - switch (*p) { - case '\0': - return false; - case 'a' ... 'z': - Transmitting = true; - ticksLeft = Unpack(MorseLetters[*p - 'a'], crumb++); - break; - case 'A' ... 'Z': - Transmitting = true; - ticksLeft = Unpack(MorseLetters[*p - 'A'], crumb++); - break; - case ' ': - crumb = 0; - ++p; - Transmitting = false; - ticksLeft = MORSE_PAUSE_WORD - MORSE_DIT; - break; - default: // this should never happen! Transmit for a word pause to indicate weirdness. - crumb = 0; - ++p; - Transmitting = true; - ticksLeft = MORSE_PAUSE_WORD; - break; - } - if (0 == ticksLeft) { - // Unpack can return 0 if there are fewer than 4 emissions for a letter. - // In that case, we 're done with the letter. - crumb = 0; - ++p; + const struct MorseSign sign = GetMorseSign(*p); + if (sign.Length == 0) { + // Pause Transmitting = false; - ticksLeft = MORSE_PAUSE_LETTER - MORSE_DIT; + ticksLeft = sign.Bits - (MORSE_PAUSE_LETTER - MORSE_DIT); + ++p; + bit = 0; + } else if (bit == sign.Length) { + // All done with that sign! + Transmitting = false; + ticksLeft = MORSE_PAUSE_LETTER; + ++p; + bit = 0; + } else { + // Sign + uint8_t bitMask = 1 << (sign.Length - bit - 1); + Transmitting = true; + if (sign.Bits & bitMask) { + ticksLeft = MORSE_DAH; + } else { + ticksLeft = MORSE_DIT; + } + ++bit; } return true; diff --git a/morse.h b/morse.h index 4b1d901..37cbe19 100644 --- a/morse.h +++ b/morse.h @@ -6,6 +6,7 @@ #define MORSE_DAH 3 #define MORSE_PAUSE_LETTER 3 #define MORSE_PAUSE_WORD 6 +#define MORSE_PAUSE_TRANSMISSION 50 class MorseEncoder { public: @@ -35,6 +36,6 @@ class MorseEncoder { private: const char *p; - uint8_t crumb; + uint8_t bit; int ticksLeft; }; diff --git a/pulse.cpp b/pulse.cpp new file mode 100644 index 0000000..8985a27 --- /dev/null +++ b/pulse.cpp @@ -0,0 +1,26 @@ +#include "pulse.h" + +#include + +Pulse::Pulse(unsigned long period) { + this->period = period; + this->nextEventMillis = 0; +} + +bool Pulse::Ticked() { + unsigned long now = millis(); + + if (now >= nextEventMillis) { + Until(period, now); + return true; + } + return false; +} + +void Pulse::Until(unsigned long next, unsigned long now) { + nextEventMillis = now + next; +} + +void Pulse::Until(unsigned long next) { + Until(next, millis()); +} diff --git a/pulse.h b/pulse.h new file mode 100644 index 0000000..e5b6d8f --- /dev/null +++ b/pulse.h @@ -0,0 +1,17 @@ +#pragma once +#include "durations.h" + +class Pulse { +public: + Pulse(unsigned long period); + + /** Ticked tells you if a period has elapsed. */ + bool Ticked(); + + /** Until sets the duration of the next period. */ + void Until(unsigned long next); + void Until(unsigned long next, unsigned long now); + + unsigned long period; + unsigned long nextEventMillis; +}; diff --git a/test b/test index bac98ff..6da8aeb 100755 Binary files a/test and b/test differ diff --git a/test.cpp b/test.cpp index e140cde..81c0b23 100644 --- a/test.cpp +++ b/test.cpp @@ -4,7 +4,7 @@ // gcc -D test_main=main -o test morse.cpp test.cpp && ./test int test_main(int argc, char *argv[]) { - MorseEncoder enc = MorseEncoder("SOS ck ck ark"); + MorseEncoder enc = MorseEncoder("SOS ck ck \x03"); while (enc.Tick()) { if (enc.Transmitting) { printf("#");