From 4ac13c534e7de4189731c2ba49d2de1708ae5496 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 6 Dec 2020 18:36:12 -0700 Subject: [PATCH] It does all kinds of shit! --- .vscode/c_cpp_properties.json | 16 ++ Makefile | 35 ++++ build/build.options.json | 12 ++ build/sketch/morse.cpp | 120 ++++++++++++ build/sketch/morse.h | 40 ++++ build/sketch/music.cpp | 210 +++++++++++++++++++++ build/sketch/pitches.h | 91 +++++++++ build/sketch/pulse.cpp | 19 ++ build/sketch/pulse.h | 12 ++ build/sketch/puzzle_box.ino.cpp | 127 +++++++++++++ build/sketch/riddler.h | 11 ++ build/uf2conv.py | 315 ++++++++++++++++++++++++++++++++ morse.cpp | 120 ++++++++++++ morse.h | 40 ++++ musicplayer.cpp | 165 +++++++++++++++++ musicplayer.h | 43 +++++ pitches.h | 91 +++++++++ pulse.cpp | 19 ++ pulse.h | 12 ++ puzzle_box.ino | 130 +++++++++++++ riddler.h | 11 ++ sketch.json | 9 + 22 files changed, 1648 insertions(+) create mode 100644 .vscode/c_cpp_properties.json create mode 100644 Makefile create mode 100644 build/build.options.json create mode 100644 build/sketch/morse.cpp create mode 100644 build/sketch/morse.h create mode 100644 build/sketch/music.cpp create mode 100644 build/sketch/pitches.h create mode 100644 build/sketch/pulse.cpp create mode 100644 build/sketch/pulse.h create mode 100644 build/sketch/puzzle_box.ino.cpp create mode 100644 build/sketch/riddler.h create mode 100755 build/uf2conv.py create mode 100644 morse.cpp create mode 100644 morse.h create mode 100644 musicplayer.cpp create mode 100644 musicplayer.h create mode 100644 pitches.h create mode 100644 pulse.cpp create mode 100644 pulse.h create mode 100644 puzzle_box.ino create mode 100644 riddler.h create mode 100644 sketch.json diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..106a450 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "compilerPath": "/usr/bin/gcc", + "cStandard": "gnu17", + "cppStandard": "gnu++14", + "intelliSenseMode": "gcc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6abc52e --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +FQBN = arduino:avr:micro +UF2_MOUNT = /media/neale/TRELM4BOOT +ARDUINO_DIR = /opt/arduino-1.8.13 + +install: + arduino --upload --board $(FQBN) puzzle_box.ino + +default: build/puzzle_box.ino.uf2 + + +# uf2conv.py is covered by an MIT license. +build/uf2conv.py: + mkdir -p build + curl -L https://raw.githubusercontent.com/microsoft/uf2/master/utils/uf2conv.py > $@ + chmod +x $@ + +%.uf2: %.bin build/uf2conv.py + build/uf2conv.py -b 0x4000 -c -o $@ $< + +build/%.bin: % *.cpp *.h + arduino-builder \ + -build-cache ~/.cache/arduino \ + -build-path build \ + -core-api-version 10813 \ + -fqbn $(FQBN) \ + -hardware ~/.arduino15/packages \ + -tools $(ARDUINO_DIR)/tools-builder \ + -tools ~/.arduino15/packages \ + -hardware $(ARDUINO_DIR)/hardware \ + -hardware ~/.arduino15/packages \ + -built-in-libraries $(ARDUINO_DIR)/libraries \ + -libraries ~/Arduino/libraries \ + -compile \ + $< + diff --git a/build/build.options.json b/build/build.options.json new file mode 100644 index 0000000..0994f87 --- /dev/null +++ b/build/build.options.json @@ -0,0 +1,12 @@ +{ + "additionalFiles": "..,..,..,..,..,..,..", + "builtInLibrariesFolders": "/opt/arduino-1.8.13/libraries", + "builtInToolsFolders": "/opt/arduino-1.8.13/tools-builder,/home/neale/.arduino15/packages", + "compiler.optimization_flags": "", + "customBuildProperties": "", + "fqbn": "arduino:avr:micro", + "hardwareFolders": "/home/neale/.arduino15/packages,/opt/arduino-1.8.13/hardware,/home/neale/.arduino15/packages", + "otherLibrariesFolders": "/home/neale/Arduino/libraries", + "runtime.ide.version": "10813", + "sketchLocation": "/home/neale/Arduino/puzzle_box/puzzle_box.ino" +} \ No newline at end of file diff --git a/build/sketch/morse.cpp b/build/sketch/morse.cpp new file mode 100644 index 0000000..cb472ab --- /dev/null +++ b/build/sketch/morse.cpp @@ -0,0 +1,120 @@ +#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 +}; + +MorseEncoder::MorseEncoder() { + SetText(""); +} +MorseEncoder::MorseEncoder(const char *s) { + SetText(s); +} + +void MorseEncoder::SetText(const char *s) { + p = s; + crumb = 0; + ticksLeft = 0; + Transmitting = false; +} + +void MorseEncoder::Quiet(int ticks) { + Transmitting = false; + ticksLeft = ticks; +} + +bool MorseEncoder::Tick() { + --ticksLeft; + if (ticksLeft > 0) { + return true; + } + + // We're out of ticks + + if (!p || !*p) { + return false; + } + + // If we were just transmitting, we have to stop for at least one dit + if (Transmitting) { + Transmitting = false; + ticksLeft = MORSE_DIT; + 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; + Transmitting = false; + ticksLeft = MORSE_PAUSE_LETTER - MORSE_DIT; + } + + return true; +} diff --git a/build/sketch/morse.h b/build/sketch/morse.h new file mode 100644 index 0000000..4b1d901 --- /dev/null +++ b/build/sketch/morse.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#define MORSE_DIT 1 +#define MORSE_DAH 3 +#define MORSE_PAUSE_LETTER 3 +#define MORSE_PAUSE_WORD 6 + +class MorseEncoder { + public: + MorseEncoder(); + MorseEncoder(const char *s); + + /** SetText resets state with new text. + */ + void SetText(const char *s); + + /** Tick tells the encoder that a dit has elapsed. + * + * Returns true if there's data left to transmit. + * If it returns false, you need to feed it more data. + * + * You should call this every time your dit duration ends. + */ + bool Tick(); + + /** Quiet stops transmitting for this many ticks. + */ + void Quiet(int ticks); + + /** Transmitting is true if you should be transmitting right now. + */ + bool Transmitting; + + private: + const char *p; + uint8_t crumb; + int ticksLeft; +}; diff --git a/build/sketch/music.cpp b/build/sketch/music.cpp new file mode 100644 index 0000000..5d27690 --- /dev/null +++ b/build/sketch/music.cpp @@ -0,0 +1,210 @@ +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +// The following functions are related to Beegees Easter Egg only + +const unsigned int freqs[] = { + 3520, // A + 3729, // A# + 3951, // B + 2093, // C + 2217, // C# + 2349, // D + 2489, // D# + 2637, // E + 2794, // F + 2960, // F# + 3136, // G + 3322, // G# +}; + +const int scale[] = { + 0, 2, 3, 5, 7, 8, 10 +}; + +Player::Player() { + +} + + +Player::PlayNote(int note, int octave, int duration) +void +playNote(int octave, int note, int duration) +{ + unsigned int freq; + int i; + + if (note >= 0) { + freq = freqs[note]; + for (i = octave; i < 7; i += 1) { + freq /= 2; + } + + tone(BUZZER2, freq, duration); + setLEDs(note + 1); + } + delay(duration); + setLEDs(0); +} + +void +play(int bpm, char *tune) +{ + unsigned int baseDuration = 60000 / bpm; + int duration = baseDuration; + int baseOctave = 4; + int octave = baseOctave; + + int note = -2; + + char *p = tune; + + for (; *p; p += 1) { + boolean playNow = false; + + switch (*p) { + case '>': + octave = baseOctave + 1; + break; + case '<': + octave = baseOctave - 1; + break; + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'G': + playNow = true; + note = scale[*p - 'A']; + break; + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + case 'g': + playNow = true; + octave += 1; + note = scale[*p - 'a']; + break; + case '_': + playNow = true; + note = -1; + break; + } + + // Check for sharps or flats + switch (*(p+1)) { + case '#': + case '+': + note += 1; + if (note == 12) { + octave += 1; + note = 0; + } + p += 1; + break; + case '-': + note -= 1; + if (note == -1) { + octave -= 1; + note = 11; + } + p += 1; + break; + } + + // Check for octave + switch (*(p+1)) { + case ',': + octave -= 1; + p += 1; + break; + case '\'': + octave += 1; + p += 1; + break; + } + + // Check for duration + switch (*(p+1)) { + case '2': + duration *= 2; + p += 1; + break; + case '4': + duration *= 4; + p += 1; + break; + case '.': + duration += duration / 2; + p += 1; + break; + } + + if (playNow) { + playNote(octave, note, duration); + note = -1; + octave = baseOctave; + duration = baseDuration; + } + + if (checkButton()) { + noTone(BUZZER2); + return; + } + } + if (note >= 0) { + playNote(octave, note, duration); + } + noTone(BUZZER2); +} + +// Do nothing but play bad beegees music +// This function is activated when user holds bottom right button during power up +void play_beegees() +{ + while(checkButton() == CHOICE_NONE) { + play(104 * 4, "E-F_a-__E-2__C_B-,CE-_B-,C_E-__B-,_C_E-_F_a-_"); + } +} + +void lplay(unsigned int tempo, char *tune, int count) +{ + int i; + + for (i = 0; i < count; i += 1) { + play(tempo * 3, tune); + } +} + + +void play_march() +{ + const int tempo = 80; + + lplay(tempo * 4, "GB-,D", 6); + lplay(tempo * 4, "GB-,D", 6); + lplay(tempo * 4, "GB-,D", 6); + lplay(tempo * 4, "E-G-,B-,", 5); + play(tempo * 4, "B-"); + lplay(tempo * 4, "GB-,D", 6); + lplay(tempo * 4, "E-G-,B-,", 5); + play(tempo * 4, "B-"); + lplay(tempo * 4, "GB-,D", 6); + lplay(tempo * 4, "GB-,D", 6); + + lplay(tempo * 4, "dGB-", 6); + lplay(tempo * 4, "dGB-", 6); + lplay(tempo * 4, "dGB-", 6); + lplay(tempo * 4, "e-G-B-", 5); + play(tempo * 4, "B-"); + lplay(tempo * 4, "G-B-,F", 6); + lplay(tempo * 4, "E-G-,B-,", 5); + play(tempo * 4, "B-"); + lplay(tempo * 4, "GB-,D", 6); + lplay(tempo * 4, "GB-,D", 6); + + return; +} diff --git a/build/sketch/pitches.h b/build/sketch/pitches.h new file mode 100644 index 0000000..db54c6a --- /dev/null +++ b/build/sketch/pitches.h @@ -0,0 +1,91 @@ +#pragma once + +#define NOTE_B0 31 +#define NOTE_C1 33 +#define NOTE_CS1 35 +#define NOTE_D1 37 +#define NOTE_DS1 39 +#define NOTE_E1 41 +#define NOTE_F1 44 +#define NOTE_FS1 46 +#define NOTE_G1 49 +#define NOTE_GS1 52 +#define NOTE_A1 55 +#define NOTE_AS1 58 +#define NOTE_B1 62 +#define NOTE_C2 65 +#define NOTE_CS2 69 +#define NOTE_D2 73 +#define NOTE_DS2 78 +#define NOTE_E2 82 +#define NOTE_F2 87 +#define NOTE_FS2 93 +#define NOTE_G2 98 +#define NOTE_GS2 104 +#define NOTE_A2 110 +#define NOTE_AS2 117 +#define NOTE_B2 123 +#define NOTE_C3 131 +#define NOTE_CS3 139 +#define NOTE_D3 147 +#define NOTE_DS3 156 +#define NOTE_E3 165 +#define NOTE_F3 175 +#define NOTE_FS3 185 +#define NOTE_G3 196 +#define NOTE_GS3 208 +#define NOTE_A3 220 +#define NOTE_AS3 233 +#define NOTE_B3 247 +#define NOTE_C4 262 +#define NOTE_CS4 277 +#define NOTE_D4 294 +#define NOTE_DS4 311 +#define NOTE_E4 330 +#define NOTE_F4 349 +#define NOTE_FS4 370 +#define NOTE_G4 392 +#define NOTE_GS4 415 +#define NOTE_A4 440 +#define NOTE_AS4 466 +#define NOTE_B4 494 +#define NOTE_C5 523 +#define NOTE_CS5 554 +#define NOTE_D5 587 +#define NOTE_DS5 622 +#define NOTE_E5 659 +#define NOTE_F5 698 +#define NOTE_FS5 740 +#define NOTE_G5 784 +#define NOTE_GS5 831 +#define NOTE_A5 880 +#define NOTE_AS5 932 +#define NOTE_B5 988 +#define NOTE_C6 1047 +#define NOTE_CS6 1109 +#define NOTE_D6 1175 +#define NOTE_DS6 1245 +#define NOTE_E6 1319 +#define NOTE_F6 1397 +#define NOTE_FS6 1480 +#define NOTE_G6 1568 +#define NOTE_GS6 1661 +#define NOTE_A6 1760 +#define NOTE_AS6 1865 +#define NOTE_B6 1976 +#define NOTE_C7 2093 +#define NOTE_CS7 2217 +#define NOTE_D7 2349 +#define NOTE_DS7 2489 +#define NOTE_E7 2637 +#define NOTE_F7 2794 +#define NOTE_FS7 2960 +#define NOTE_G7 3136 +#define NOTE_GS7 3322 +#define NOTE_A7 3520 +#define NOTE_AS7 3729 +#define NOTE_B7 3951 +#define NOTE_C8 4186 +#define NOTE_CS8 4435 +#define NOTE_D8 4699 +#define NOTE_DS8 4978 \ No newline at end of file diff --git a/build/sketch/pulse.cpp b/build/sketch/pulse.cpp new file mode 100644 index 0000000..72df45d --- /dev/null +++ b/build/sketch/pulse.cpp @@ -0,0 +1,19 @@ +#include "pulse.h" + +#include + +Pulse::Pulse(unsigned long period) { + this->period = period; + this->nextEventMillis = 0; +} + +bool Pulse::Tick() { + unsigned long now = millis(); + + if (now >= nextEventMillis) { + nextEventMillis = now + period; + return true; + } + + return false; +} diff --git a/build/sketch/pulse.h b/build/sketch/pulse.h new file mode 100644 index 0000000..765a1df --- /dev/null +++ b/build/sketch/pulse.h @@ -0,0 +1,12 @@ +#pragma once + +class Pulse { +public: + Pulse(unsigned long period); + + /** Tick tells you if a period has elapsed. */ + bool Tick(); + + unsigned long period; + unsigned long nextEventMillis; +}; diff --git a/build/sketch/puzzle_box.ino.cpp b/build/sketch/puzzle_box.ino.cpp new file mode 100644 index 0000000..55450cf --- /dev/null +++ b/build/sketch/puzzle_box.ino.cpp @@ -0,0 +1,127 @@ +#include +#line 1 "/home/neale/Arduino/puzzle_box/puzzle_box.ino" +#include +#include +#include +#include +#include +#include "morse.h" +#include "pulse.h" +#include "riddler.h" + +// WS2812 LEDs +#define LEDS_PIN 1 +#define NUM_LEDS 2 +CRGB leds[NUM_LEDS]; + +// Laser +#define LASER_PIN 0 + +// Photoresistor +#define PHOTO_PIN A0 + +// Piezo buzzer +#define BUZZER_PIN 11 + +// Display pin connections. LED needs to be PWM capable. +#define DISPLAY_WIDTH 84 +#define DISPLAY_HEIGHT 48 +#define DISPLAY_SCE 4 +#define DISPLAY_RST 7 +#define DISPLAY_DC 8 +#define DISPLAY_LED 9 +Adafruit_PCD8544 display = Adafruit_PCD8544(DISPLAY_DC, DISPLAY_SCE, DISPLAY_RST); + +// Morse code stuff +#define DIT_DURATION 100 + +void setup() { + FastLED.addLeds(leds, NUM_LEDS); + + // Turn on backlight + pinMode(DISPLAY_LED, OUTPUT); + analogWrite(9, 64); + + // Turn on display + display.begin(); + display.setContrast(30); + display.clearDisplay(); + display.drawBitmap( + (DISPLAY_WIDTH - riddler_width)/2, + (DISPLAY_HEIGHT - riddler_height)/2, + riddler_bits, + riddler_width, + riddler_height, + 0xffff + ); + display.display(); + + pinMode(LASER_PIN, OUTPUT); + pinMode(PHOTO_PIN, INPUT); + pinMode(BUZZER_PIN, OUTPUT); + + tone(BUZZER_PIN, 110); + +} + +CHSV loop_morse(Adafruit_PCD8544 *display) { + static Pulse pulse = Pulse(DIT_DURATION); + static MorseEncoder enc = MorseEncoder("CQ CQ KD7OQI"); + static uint16_t errors = 0; + bool solved = false; + int recv = analogRead(PHOTO_PIN); + bool error = ((recv >= 512) != enc.Transmitting); + CHSV color; + + if (solved) { + color = CHSV(128, 40, 64); + } else if (error) { + ++errors; + color = CHSV(0, 255, recv>>2); + } else { + color = CHSV(128, 255, recv>>2); + } + + if (! pulse.Tick()) { + return color; + } + + if (display) { + display->clearDisplay(); + display->setFont(); + display->setCursor(0, 0); + display->print("The Morse One"); + display->display(); + } + + if (enc.Tick()) { + digitalWrite(LASER_PIN, enc.Transmitting); + } else { + // We've sent the whole thing + if (errors < 500) { + solved = true; + } + enc.SetText("HO HO HO ARK"); + enc.Quiet(30); + errors = 0; + } +} + +void messageHappy() { + display.clearDisplay(); + display.setFont(&FreeSerif9pt7b); + display.setCursor(0, 12); + display.print("Happy"); + display.setCursor(0, 29); + display.print("Holiday,"); + display.setCursor(0, 46); + display.print("Martin!"); + display.display(); +} + +void loop() { + leds[0] = loop_morse(NULL); + + FastLED.show(); +} + diff --git a/build/sketch/riddler.h b/build/sketch/riddler.h new file mode 100644 index 0000000..5789e33 --- /dev/null +++ b/build/sketch/riddler.h @@ -0,0 +1,11 @@ +#define riddler_width 24 +#define riddler_height 34 +static const unsigned char PROGMEM riddler_bits[] = { + 0x01, 0xff, 0x80, 0x0f, 0xff, 0xf0, 0x3f, 0xff, 0xfc, 0x7f, 0x01, 0xfe, 0xfe, 0x00, 0xff, 0xfe, + 0x00, 0xff, 0xfe, 0x00, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xe1, 0xff, 0x3f, 0xc3, 0xfe, 0x1f, 0x87, + 0xf8, 0x00, 0x0f, 0xf0, 0x00, 0x1f, 0xe0, 0x00, 0x3f, 0xc0, 0x00, 0x7f, 0x00, 0x00, 0xfc, 0x00, + 0x01, 0xf8, 0x00, 0x03, 0xf0, 0x00, 0x07, 0xe0, 0x00, 0x07, 0xe0, 0x00, 0x07, 0xf8, 0x00, 0x07, + 0xfc, 0x00, 0x07, 0xfc, 0x00, 0x03, 0xfc, 0x00, 0x01, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0xf8, 0x00, 0x01, 0xfc, 0x00, 0x01, 0xfc, 0x00, 0x01, 0xfc, 0x00, + 0x01, 0xfc, 0x00, 0x00, 0xf8, 0x00 +}; diff --git a/build/uf2conv.py b/build/uf2conv.py new file mode 100755 index 0000000..b32c330 --- /dev/null +++ b/build/uf2conv.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +import sys +import struct +import subprocess +import re +import os +import os.path +import argparse + + +UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" +UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected +UF2_MAGIC_END = 0x0AB16F30 # Ditto + +families = { + 'SAMD21': 0x68ed2b88, + 'SAML21': 0x1851780a, + 'SAMD51': 0x55114460, + 'NRF52': 0x1b57745f, + 'STM32F0': 0x647824b6, + 'STM32F1': 0x5ee21072, + 'STM32F2': 0x5d1a0a2e, + 'STM32F3': 0x6b846188, + 'STM32F4': 0x57755a57, + 'STM32F7': 0x53b80f00, + 'STM32G0': 0x300f5633, + 'STM32G4': 0x4c71240a, + 'STM32H7': 0x6db66082, + 'STM32L0': 0x202e3a91, + 'STM32L1': 0x1e1f432d, + 'STM32L4': 0x00ff6919, + 'STM32L5': 0x04240bdf, + 'STM32WB': 0x70d16653, + 'STM32WL': 0x21460ff0, + 'ATMEGA32': 0x16573617, + 'MIMXRT10XX': 0x4FB2D5BD, + 'GD32F350': 0x31D228C6 +} + +INFO_FILE = "/INFO_UF2.TXT" + +appstartaddr = 0x2000 +familyid = 0x0 + + +def is_uf2(buf): + w = struct.unpack(" 476: + assert False, "Invalid UF2 data size at " + ptr + newaddr = hd[3] + if curraddr == None: + appstartaddr = newaddr + curraddr = newaddr + padding = newaddr - curraddr + if padding < 0: + assert False, "Block out of order at " + ptr + if padding > 10*1024*1024: + assert False, "More than 10M of padding needed at " + ptr + if padding % 4 != 0: + assert False, "Non-word padding size at " + ptr + while padding > 0: + padding -= 4 + outp += b"\x00\x00\x00\x00" + outp.append(block[32 : 32 + datalen]) + curraddr = newaddr + datalen + return b"".join(outp) + +def convert_to_carray(file_content): + outp = "const unsigned char bindata[] __attribute__((aligned(16))) = {" + for i in range(len(file_content)): + if i % 16 == 0: + outp += "\n" + outp += "0x%02x, " % file_content[i] + outp += "\n};\n" + return bytes(outp, "utf-8") + +def convert_to_uf2(file_content): + global familyid + datapadding = b"" + while len(datapadding) < 512 - 256 - 32 - 4: + datapadding += b"\x00\x00\x00\x00" + numblocks = (len(file_content) + 255) // 256 + outp = [] + for blockno in range(numblocks): + ptr = 256 * blockno + chunk = file_content[ptr:ptr + 256] + flags = 0x0 + if familyid: + flags |= 0x2000 + hd = struct.pack(b"= 3 and words[1] == "2" and words[2] == "FAT": + drives.append(words[0]) + else: + rootpath = "/media" + if sys.platform == "darwin": + rootpath = "/Volumes" + elif sys.platform == "linux": + tmp = rootpath + "/" + os.environ["USER"] + if os.path.isdir(tmp): + rootpath = tmp + for d in os.listdir(rootpath): + drives.append(os.path.join(rootpath, d)) + + + def has_info(d): + try: + return os.path.isfile(d + INFO_FILE) + except: + return False + + return list(filter(has_info, drives)) + + +def board_id(path): + with open(path + INFO_FILE, mode='r') as file: + file_content = file.read() + return re.search("Board-ID: ([^\r\n]*)", file_content).group(1) + + +def list_drives(): + for d in get_drives(): + print(d, board_id(d)) + + +def write_file(name, buf): + with open(name, "wb") as f: + f.write(buf) + print("Wrote %d bytes to %s" % (len(buf), name)) + + +def main(): + global appstartaddr, familyid + def error(msg): + print(msg) + sys.exit(1) + parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.') + parser.add_argument('input', metavar='INPUT', type=str, nargs='?', + help='input file (HEX, BIN or UF2)') + parser.add_argument('-b' , '--base', dest='base', type=str, + default="0x2000", + help='set base address of application for BIN format (default: 0x2000)') + parser.add_argument('-o' , '--output', metavar="FILE", dest='output', type=str, + help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible') + parser.add_argument('-d' , '--device', dest="device_path", + help='select a device path to flash') + parser.add_argument('-l' , '--list', action='store_true', + help='list connected devices') + parser.add_argument('-c' , '--convert', action='store_true', + help='do not flash, just convert') + parser.add_argument('-D' , '--deploy', action='store_true', + help='just flash, do not convert') + parser.add_argument('-f' , '--family', dest='family', type=str, + default="0x0", + help='specify familyID - number or name (default: 0x0)') + parser.add_argument('-C' , '--carray', action='store_true', + help='convert binary file to a C array, not UF2') + args = parser.parse_args() + appstartaddr = int(args.base, 0) + + if args.family.upper() in families: + familyid = families[args.family.upper()] + else: + try: + familyid = int(args.family, 0) + except ValueError: + error("Family ID needs to be a number or one of: " + ", ".join(families.keys())) + + if args.list: + list_drives() + else: + if not args.input: + error("Need input file") + with open(args.input, mode='rb') as f: + inpbuf = f.read() + from_uf2 = is_uf2(inpbuf) + ext = "uf2" + if args.deploy: + outbuf = inpbuf + elif from_uf2: + outbuf = convert_from_uf2(inpbuf) + ext = "bin" + elif is_hex(inpbuf): + outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8")) + elif args.carray: + outbuf = convert_to_carray(inpbuf) + ext = "h" + else: + outbuf = convert_to_uf2(inpbuf) + print("Converting to %s, output size: %d, start address: 0x%x" % + (ext, len(outbuf), appstartaddr)) + if args.convert or ext != "uf2": + drives = [] + if args.output == None: + args.output = "flash." + ext + else: + drives = get_drives() + + if args.output: + write_file(args.output, outbuf) + else: + if len(drives) == 0: + error("No drive to deploy.") + for d in drives: + print("Flashing %s (%s)" % (d, board_id(d))) + write_file(d + "/NEW.UF2", outbuf) + + +if __name__ == "__main__": + main() diff --git a/morse.cpp b/morse.cpp new file mode 100644 index 0000000..cb472ab --- /dev/null +++ b/morse.cpp @@ -0,0 +1,120 @@ +#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 +}; + +MorseEncoder::MorseEncoder() { + SetText(""); +} +MorseEncoder::MorseEncoder(const char *s) { + SetText(s); +} + +void MorseEncoder::SetText(const char *s) { + p = s; + crumb = 0; + ticksLeft = 0; + Transmitting = false; +} + +void MorseEncoder::Quiet(int ticks) { + Transmitting = false; + ticksLeft = ticks; +} + +bool MorseEncoder::Tick() { + --ticksLeft; + if (ticksLeft > 0) { + return true; + } + + // We're out of ticks + + if (!p || !*p) { + return false; + } + + // If we were just transmitting, we have to stop for at least one dit + if (Transmitting) { + Transmitting = false; + ticksLeft = MORSE_DIT; + 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; + Transmitting = false; + ticksLeft = MORSE_PAUSE_LETTER - MORSE_DIT; + } + + return true; +} diff --git a/morse.h b/morse.h new file mode 100644 index 0000000..4b1d901 --- /dev/null +++ b/morse.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#define MORSE_DIT 1 +#define MORSE_DAH 3 +#define MORSE_PAUSE_LETTER 3 +#define MORSE_PAUSE_WORD 6 + +class MorseEncoder { + public: + MorseEncoder(); + MorseEncoder(const char *s); + + /** SetText resets state with new text. + */ + void SetText(const char *s); + + /** Tick tells the encoder that a dit has elapsed. + * + * Returns true if there's data left to transmit. + * If it returns false, you need to feed it more data. + * + * You should call this every time your dit duration ends. + */ + bool Tick(); + + /** Quiet stops transmitting for this many ticks. + */ + void Quiet(int ticks); + + /** Transmitting is true if you should be transmitting right now. + */ + bool Transmitting; + + private: + const char *p; + uint8_t crumb; + int ticksLeft; +}; diff --git a/musicplayer.cpp b/musicplayer.cpp new file mode 100644 index 0000000..fa22e88 --- /dev/null +++ b/musicplayer.cpp @@ -0,0 +1,165 @@ +#include "musicplayer.h" +#include + +#define MILLISECOND 1L +#define SECOND (1000 * MILLISECOND) +#define MINUTE (60 * SECOND) +#define REST -1 +#define OCTAVE 7 + +// Frequencies in octave 7 +const uint16_t freqs[] = { + 3520, // A + 3729, // A# + 3951, // B + 2093, // C + 2217, // C# + 2349, // D + 2489, // D# + 2637, // E + 2794, // F + 2960, // F# + 3136, // G + 3322, // G# +}; + +const int scale[] = { + 0, + 2, + 3, + 5, + 7, + 8, + 10, +}; + +MusicPlayer::MusicPlayer(uint8_t pin) { + this->pin = pin; + this->tune = NULL; + pinMode(pin, OUTPUT); +} + +void MusicPlayer::Tone(int note) { + if (REST == note) { + noTone(pin); + } else { + uint16_t freq = freqs[note % 12]; + + for (int o = OCTAVE; note < o * 12; --o) { + freq /= 2; + } + tone(pin, freq); + } +} + +void MusicPlayer::NoTone() { + Tone(REST); +} + +bool MusicPlayer::KeepPlaying() { + unsigned long now = millis(); + uint16_t duration = baseDuration; + int octave = 4; + int note = REST; + + if (!tune) { + return false; + } + if (now < nextNoteMillis) { + // Turn it off for a second in case the note is repeating. + if (now + 20 > nextNoteMillis) { + NoTone(); + } + return true; + } + + // Ignore spaces + while (*tune == ' ') { + ++tune; + } + + for (;;) { + switch (*tune) { + case '>': + ++octave; + ++tune; + break; + case '<': + --octave; + ++tune; + break; + default: + goto OCTAVE_DONE; + } + } +OCTAVE_DONE: + + switch (*tune) { + case '\0': + tune = NULL; + NoTone(); + return false; + case 'a' ... 'g': + ++octave; + note = scale[*tune - 'a'] + (12 * octave); + break; + case 'A' ... 'G': + note = scale[*tune - 'A'] + (12 * octave); + break; + case '_': + case 'P': + default: + note = REST; + break; + } + ++tune; + + // Check for sharps or flats + switch (*tune) { + case '#': + case '+': + ++note; + ++tune; + break; + case '-': + --note; + ++tune; + break; + } + + // Check for duration + for (;;) { + switch (*tune) { + case '/': + duration /= 2; + ++tune; + break; + case '2': + duration *= 2; + ++tune; + break; + case '4': + duration *= 4; + ++tune; + break; + case '.': + duration += duration / 2; + ++tune; + break; + default: + goto DURATION_DONE; + } + } +DURATION_DONE: + + Tone(note); + nextNoteMillis = now + duration; + + return true; +} + +void MusicPlayer::Play(int bpm, const char *tune) { + this->tune = tune; + this->baseDuration = MINUTE / bpm; + this->nextNoteMillis = millis(); +} diff --git a/musicplayer.h b/musicplayer.h new file mode 100644 index 0000000..f812ca8 --- /dev/null +++ b/musicplayer.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +class MusicPlayer { + public: + MusicPlayer(uint8_t pin); + + /** Play begins playing a tune */ + void Play(int bpm, const char *tune); + + /** KeepPlaying should be called once per loop. + * It returns true if a tune is currently being played. + */ + bool KeepPlaying(); + + /** Tone plays a certain tone */ + void Tone(int note); + + /** NoTone stops playing anything */ + void NoTone(); + +private: + uint8_t pin; + char *tune; + unsigned long nextNoteMillis; + uint16_t baseDuration; +}; + +#define TUNE_STAYIN_ALIVE "E- F P a- P2 E-2 P2 C P + +Pulse::Pulse(unsigned long period) { + this->period = period; + this->nextEventMillis = 0; +} + +bool Pulse::Tick() { + unsigned long now = millis(); + + if (now >= nextEventMillis) { + nextEventMillis = now + period; + return true; + } + + return false; +} diff --git a/pulse.h b/pulse.h new file mode 100644 index 0000000..765a1df --- /dev/null +++ b/pulse.h @@ -0,0 +1,12 @@ +#pragma once + +class Pulse { +public: + Pulse(unsigned long period); + + /** Tick tells you if a period has elapsed. */ + bool Tick(); + + unsigned long period; + unsigned long nextEventMillis; +}; diff --git a/puzzle_box.ino b/puzzle_box.ino new file mode 100644 index 0000000..5a38f76 --- /dev/null +++ b/puzzle_box.ino @@ -0,0 +1,130 @@ +#include +#include +#include +#include +#include +#include "morse.h" +#include "pulse.h" +#include "riddler.h" +#include "musicplayer.h" + +// WS2812 LEDs +#define LEDS_PIN 1 +#define NUM_LEDS 2 +CRGB leds[NUM_LEDS]; + +// Laser +#define LASER_PIN 0 + +// Photoresistor +#define PHOTO_PIN A0 + +// Piezo buzzer +#define BUZZER_PIN 11 +MusicPlayer mp = MusicPlayer(BUZZER_PIN); + +// Display pin connections. LED needs to be PWM capable. +#define DISPLAY_WIDTH 84 +#define DISPLAY_HEIGHT 48 +#define DISPLAY_SCE 4 +#define DISPLAY_RST 7 +#define DISPLAY_DC 8 +#define DISPLAY_LED 9 +Adafruit_PCD8544 display = Adafruit_PCD8544(DISPLAY_DC, DISPLAY_SCE, DISPLAY_RST); + +// Morse code stuff +#define DIT_DURATION 100 + +void setup() { + FastLED.addLeds(leds, NUM_LEDS); + + // Turn on backlight + pinMode(DISPLAY_LED, OUTPUT); + analogWrite(9, 64); + + // Turn on display + display.begin(); + display.setContrast(50); + display.clearDisplay(); + display.drawBitmap( + (DISPLAY_WIDTH - riddler_width)/2, + (DISPLAY_HEIGHT - riddler_height)/2, + riddler_bits, + riddler_width, + riddler_height, + 0xffff + ); + display.display(); + + pinMode(LASER_PIN, OUTPUT); + pinMode(PHOTO_PIN, INPUT); + + mp.Play(120*4, TUNE_BOO); +} + +CHSV loop_morse(bool fg) { + static Pulse pulse = Pulse(DIT_DURATION); + static MorseEncoder enc = MorseEncoder("CQ CQ KD7OQI"); + static uint16_t errors = 0; + bool solved = false; + int recv = analogRead(PHOTO_PIN); + bool error = ((recv >= 512) != enc.Transmitting); + CHSV color; + + if (solved) { + color = CHSV(128, 40, 64); + } else if (error) { + ++errors; + color = CHSV(0, 255, recv>>2); + } else { + color = CHSV(128, 255, recv>>2); + } + + if (! pulse.Tick()) { + return color; + } + + if (fg) { + display.clearDisplay(); + display.setFont(); + display.setCursor(0, 0); + display.print("The Morse One"); + display.display(); + } + + if (enc.Tick()) { + digitalWrite(LASER_PIN, enc.Transmitting); + } else { + // We've sent the whole thing + if (errors < 500) { + solved = true; + } + enc.SetText("HO HO HO ARK"); + enc.Quiet(30); + errors = 0; + } +} + +void beHappy() { + if (!mp.KeepPlaying()) { + mp.Play(80*4, TUNE_JINGLEBELLS); + + display.clearDisplay(); + display.setFont(&FreeSerif9pt7b); + display.setCursor(0, 12); + display.print("Happy"); + display.setCursor(0, 29); + display.print("Holiday,"); + display.setCursor(0, 46); + display.print("Martin!"); + display.display(); + } +} + +void loop() { + leds[0] = loop_morse(true); + + beHappy(); + + FastLED.show(); +} diff --git a/riddler.h b/riddler.h new file mode 100644 index 0000000..5789e33 --- /dev/null +++ b/riddler.h @@ -0,0 +1,11 @@ +#define riddler_width 24 +#define riddler_height 34 +static const unsigned char PROGMEM riddler_bits[] = { + 0x01, 0xff, 0x80, 0x0f, 0xff, 0xf0, 0x3f, 0xff, 0xfc, 0x7f, 0x01, 0xfe, 0xfe, 0x00, 0xff, 0xfe, + 0x00, 0xff, 0xfe, 0x00, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xe1, 0xff, 0x3f, 0xc3, 0xfe, 0x1f, 0x87, + 0xf8, 0x00, 0x0f, 0xf0, 0x00, 0x1f, 0xe0, 0x00, 0x3f, 0xc0, 0x00, 0x7f, 0x00, 0x00, 0xfc, 0x00, + 0x01, 0xf8, 0x00, 0x03, 0xf0, 0x00, 0x07, 0xe0, 0x00, 0x07, 0xe0, 0x00, 0x07, 0xf8, 0x00, 0x07, + 0xfc, 0x00, 0x07, 0xfc, 0x00, 0x03, 0xfc, 0x00, 0x01, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x30, 0x00, 0x00, 0xf8, 0x00, 0x01, 0xfc, 0x00, 0x01, 0xfc, 0x00, 0x01, 0xfc, 0x00, + 0x01, 0xfc, 0x00, 0x00, 0xf8, 0x00 +}; diff --git a/sketch.json b/sketch.json new file mode 100644 index 0000000..048e3a8 --- /dev/null +++ b/sketch.json @@ -0,0 +1,9 @@ +{ + "cpu": { + "fqbn": "arduino:avr:micro", + "name": "Arduino Micro", + "type": "serial" + }, + "secrets": [], + "included_libs": [] +} \ No newline at end of file