Code:
#include <Bounce2.h>
#include <cstring>
#include <climits>
#include <InternalTemperature.h>
#include <EEPROM.h>
// TODO https://github.com/pictographer/didah/blob/master/EEPROMAnything.h
#include <Keyboard.h>
// The marvelous FastLED library provides a high-level interface to
// many kinds of RGB LEDs and drives them efficiently. See
// http://fastled.io
#include <FastLED.h>
#if defined(TEENSYDUINO)
#if defined(__MK20DX128__)
#define BOARD "Teensy 3.0"
#elif defined(__MK20DX256__)
#define BOARD "Teensy 3.1/3.2"
#elif defined(__MKL26Z64__)
#define BOARD "Teensy LC"
#elif defined(__MK64FX512__)
#define BOARD "Teensy 3.5"
#elif defined(__MK66FX1M0__)
#define BOARD "Teensy 3.6"
#else
#error "Unknown board"
#endif
#endif
// Desklight
const int PIR_INPUT_YELLOW = 2;
const int MOMENTARY_BUTTON1_WHITE = 9;
const int MOMENTARY_BUTTON2_WHITE = 10;
const int DEBOUNCE_MS = 10;
Bounce momentaryButton = Bounce(MOMENTARY_BUTTON1_WHITE, DEBOUNCE_MS);
// Parameters for our LEDs
#define LED_TYPE APA102
#define COLOR_ORDER BGR
//const unsigned int NUM_LEDS = 96;
const unsigned int NUM_LEDS = 8;
const unsigned int CLOCK_MHZ = 12;
const int TOUCH_CASE_SENSOR_BLUE = 0;
const int TOUCH_CASE_BASELINE = 1613;
const int TOUCH_CASE_MAX = 8258;
const int TOUCH_CASE_MIN = 1388;
const int DATA_PIN = 11;
const int CLOCK_PIN = 14;
const int PHOTODIODE_ANODE_BLACK = 16;
const int PHOTODIODE_CATHODE_RED = 17;
const int PHOTODIODE_MAX = 985;
const int PHOTODIODE_MIN = 25;
const int PHOTODIODE_DAYLIGHT = 818;
const int PHOTODIODE_THRESHOLD = 708;
const int SPEAKER_BLACK = 22;
const int SPEAKER_RED = 23;
// Storage for LED colors.
CRGB leds[NUM_LEDS];
char buffer[64] = { 0 };
// Size of null-terminated command buffer.
const size_t cmax = 256;
static char cmdline[cmax];
// Send Super-L to lock the screen on Ubuntu 20.
// Caution: Do not call this in a tight loop.
void lockScreen() {
Keyboard.set_modifier(0);
Keyboard.set_modifier(MODIFIERKEY_GUI);
Keyboard.set_key1(uint8_t(KEY_L));
Keyboard.send_now();
Keyboard.set_modifier(0);
Keyboard.set_key1(0);
Keyboard.send_now();
}
int readLight() {
const auto count = 1024L;
digitalWriteFast(PHOTODIODE_ANODE_BLACK, 1);
auto sum = 0L;
for (auto i = 0L; i < count; ++i) {
sum += analogRead(PHOTODIODE_CATHODE_RED);
}
digitalWriteFast(PHOTODIODE_ANODE_BLACK, 0);
return sum / count;
}
float readTemperatureF() {
const auto count = 16L;
auto sum = 0.0f;
for (auto i = 0L; i < count; ++i) {
sum += InternalTemperature.readUncalibratedTemperatureF();
}
return sum / count;
}
void rotate(int k) {
auto c = leds[0];
for (auto i = 0U; i < NUM_LEDS; ++i) {
leds[i] = leds[(i + k) % NUM_LEDS];
}
leds[NUM_LEDS - 1] = c;
}
bool pollTouch() {
int touch_reading = touchRead(TOUCH_CASE_SENSOR_BLUE);
return TOUCH_CASE_BASELINE + (TOUCH_CASE_MAX - TOUCH_CASE_BASELINE) / 4 < touch_reading;
}
void basicBeep() {
analogWriteFrequency(SPEAKER_RED, 440);
analogWrite(SPEAKER_RED, 127);
delay(100);
analogWrite(SPEAKER_RED, 0);
}
// Read a line into cmdline. Complain and discard if longer than cmax - 1.
void pollCommand(char cmdline[], size_t cmax) {
size_t cindex = 0;
while (Serial.available()) {
unsigned int r = Serial.read();
if (!cindex && isspace(r)) continue; // Discard leading spaces.
if (isprint(r)) {
cmdline[cindex++] = r;
cmdline[cindex] = 0;
if (cindex == cmax - 1) {
// The input is too long, thus invalid. Discard it.
cindex = 0;
cmdline[cindex] = 0;
while (Serial.available()) Serial.read();
Serial.println("Command buffer overflow!");
basicBeep();
break;
}
}
}
// Trim trailing spaces.
while (cindex && isspace(cmdline[cindex - 1])) {
--cindex;
}
cmdline[cindex] = 0;
}
bool isEqual(const char* a, const char* b) {
return !strcmp(a, b);
}
// Is a a prefix of abc?
bool isPrefix(const char* a, const char* abc) {
while (*a && *abc) {
if (*a != *abc) {
return false;
}
++a;
++abc;
}
return *a == 0;
}
// TODO - With the better implementation, we could put these back.
// Duplicates
// case CRGB::Aqua: return "Aqua";
// case CRGB::DarkGrey: return "DarkGrey";
// case CRGB::DarkSlateGrey: return "DarkSlateGrey";
// case CRGB::DimGrey: return "DimGrey";
// case CRGB::Fuchsia: return "Fuchsia";
// case CRGB::Grey: return "Grey";
// case CRGB::LightGrey: return "LightGrey";
// case CRGB::LightSlateGrey: return "LightSlateGrey";
// case CRGB::SlateGrey: return "SlateGrey";
// TODO - Remove useless color names.
// Many of these colors are obscure or don't look different than more common colors.
const struct {
const CRGB colorCode;
const char* colorName;
} colorName[] = {
{ CRGB::AliceBlue, "AliceBlue" },
{ CRGB::Amethyst, "Amethyst" },
{ CRGB::AntiqueWhite, "AntiqueWhite" },
{ CRGB::Aquamarine, "Aquamarine" },
{ CRGB::Azure, "Azure" },
{ CRGB::Beige, "Beige" },
{ CRGB::Bisque, "Bisque" },
{ CRGB::Black, "Black" },
{ CRGB::BlanchedAlmond, "BlanchedAlmond" },
{ CRGB::Blue, "Blue" },
{ CRGB::BlueViolet, "BlueViolet" },
{ CRGB::Brown, "Brown" },
{ CRGB::BurlyWood, "BurlyWood" },
{ CRGB::CadetBlue, "CadetBlue" },
{ CRGB::Chartreuse, "Chartreuse" },
{ CRGB::Chocolate, "Chocolate" },
{ CRGB::Coral, "Coral" },
{ CRGB::CornflowerBlue, "CornflowerBlue" },
{ CRGB::Cornsilk, "Cornsilk" },
{ CRGB::Crimson, "Crimson" },
{ CRGB::Cyan, "Cyan" },
{ CRGB::DarkBlue, "DarkBlue" },
{ CRGB::DarkCyan, "DarkCyan" },
{ CRGB::DarkGoldenrod, "DarkGoldenrod" },
{ CRGB::DarkGray, "DarkGray" },
{ CRGB::DarkGreen, "DarkGreen" },
{ CRGB::DarkKhaki, "DarkKhaki" },
{ CRGB::DarkMagenta, "DarkMagenta" },
{ CRGB::DarkOliveGreen, "DarkOliveGreen" },
{ CRGB::DarkOrange, "DarkOrange" },
{ CRGB::DarkOrchid, "DarkOrchid" },
{ CRGB::DarkRed, "DarkRed" },
{ CRGB::DarkSalmon, "DarkSalmon" },
{ CRGB::DarkSeaGreen, "DarkSeaGreen" },
{ CRGB::DarkSlateBlue, "DarkSlateBlue" },
{ CRGB::DarkSlateGray, "DarkSlateGray" },
{ CRGB::DarkTurquoise, "DarkTurquoise" },
{ CRGB::DarkViolet, "DarkViolet" },
{ CRGB::DeepPink, "DeepPink" },
{ CRGB::DeepSkyBlue, "DeepSkyBlue" },
{ CRGB::DimGray, "DimGray" },
{ CRGB::DodgerBlue, "DodgerBlue" },
{ CRGB::FireBrick, "FireBrick" },
{ CRGB::FloralWhite, "FloralWhite" },
{ CRGB::ForestGreen, "ForestGreen" },
{ CRGB::Gainsboro, "Gainsboro" },
{ CRGB::GhostWhite, "GhostWhite" },
{ CRGB::Gold, "Gold" },
{ CRGB::Goldenrod, "Goldenrod" },
{ CRGB::Gray, "Gray" },
{ CRGB::Green, "Green" },
{ CRGB::GreenYellow, "GreenYellow" },
{ CRGB::Honeydew, "Honeydew" },
{ CRGB::HotPink, "HotPink" },
{ CRGB::IndianRed, "IndianRed" },
{ CRGB::Indigo, "Indigo" },
{ CRGB::Ivory, "Ivory" },
{ CRGB::Khaki, "Khaki" },
{ CRGB::Lavender, "Lavender" },
{ CRGB::LavenderBlush, "LavenderBlush" },
{ CRGB::LawnGreen, "LawnGreen" },
{ CRGB::LemonChiffon, "LemonChiffon" },
{ CRGB::LightBlue, "LightBlue" },
{ CRGB::LightCoral, "LightCoral" },
{ CRGB::LightCyan, "LightCyan" },
{ CRGB::LightGoldenrodYellow, "LightGoldenrodYellow" },
{ CRGB::LightGreen, "LightGreen" },
{ CRGB::LightPink, "LightPink" },
{ CRGB::LightSalmon, "LightSalmon" },
{ CRGB::LightSeaGreen, "LightSeaGreen" },
{ CRGB::LightSkyBlue, "LightSkyBlue" },
{ CRGB::LightSlateGray, "LightSlateGray" },
{ CRGB::LightSteelBlue, "LightSteelBlue" },
{ CRGB::LightYellow, "LightYellow" },
{ CRGB::Lime, "Lime" },
{ CRGB::LimeGreen, "LimeGreen" },
{ CRGB::Linen, "Linen" },
{ CRGB::Magenta, "Magenta" },
{ CRGB::Maroon, "Maroon" },
{ CRGB::MediumAquamarine, "MediumAquamarine" },
{ CRGB::MediumBlue, "MediumBlue" },
{ CRGB::MediumOrchid, "MediumOrchid" },
{ CRGB::MediumPurple, "MediumPurple" },
{ CRGB::MediumSeaGreen, "MediumSeaGreen" },
{ CRGB::MediumSlateBlue, "MediumSlateBlue" },
{ CRGB::MediumSpringGreen, "MediumSpringGreen" },
{ CRGB::MediumTurquoise, "MediumTurquoise" },
{ CRGB::MediumVioletRed, "MediumVioletRed" },
{ CRGB::MidnightBlue, "MidnightBlue" },
{ CRGB::MintCream, "MintCream" },
{ CRGB::MistyRose, "MistyRose" },
{ CRGB::Moccasin, "Moccasin" },
{ CRGB::NavajoWhite, "NavajoWhite" },
{ CRGB::Navy, "Navy" },
{ CRGB::OldLace, "OldLace" },
{ CRGB::Olive, "Olive" },
{ CRGB::OliveDrab, "OliveDrab" },
{ CRGB::Orange, "Orange" },
{ CRGB::OrangeRed, "OrangeRed" },
{ CRGB::Orchid, "Orchid" },
{ CRGB::PaleGoldenrod, "PaleGoldenrod" },
{ CRGB::PaleGreen, "PaleGreen" },
{ CRGB::PaleTurquoise, "PaleTurquoise" },
{ CRGB::PaleVioletRed, "PaleVioletRed" },
{ CRGB::PapayaWhip, "PapayaWhip" },
{ CRGB::PeachPuff, "PeachPuff" },
{ CRGB::Peru, "Peru" },
{ CRGB::Pink, "Pink" },
{ CRGB::Plaid, "Plaid" },
{ CRGB::Plum, "Plum" },
{ CRGB::PowderBlue, "PowderBlue" },
{ CRGB::Purple, "Purple" },
{ CRGB::Red, "Red" },
{ CRGB::RosyBrown, "RosyBrown" },
{ CRGB::RoyalBlue, "RoyalBlue" },
{ CRGB::SaddleBrown, "SaddleBrown" },
{ CRGB::Salmon, "Salmon" },
{ CRGB::SandyBrown, "SandyBrown" },
{ CRGB::SeaGreen, "SeaGreen" },
{ CRGB::Seashell, "Seashell" },
{ CRGB::Sienna, "Sienna" },
{ CRGB::Silver, "Silver" },
{ CRGB::SkyBlue, "SkyBlue" },
{ CRGB::SlateBlue, "SlateBlue" },
{ CRGB::SlateGray, "SlateGray" },
{ CRGB::Snow, "Snow" },
{ CRGB::SpringGreen, "SpringGreen" },
{ CRGB::SteelBlue, "SteelBlue" },
{ CRGB::Tan, "Tan" },
{ CRGB::Teal, "Teal" },
{ CRGB::Thistle, "Thistle" },
{ CRGB::Tomato, "Tomato" },
{ CRGB::Turquoise, "Turquoise" },
{ CRGB::Violet, "Violet" },
{ CRGB::Wheat, "Wheat" },
{ CRGB::White, "White" },
{ CRGB::WhiteSmoke, "WhiteSmoke" },
{ CRGB::Yellow, "Yellow" },
{ CRGB::YellowGreen, "YellowGreen" },
{ CRGB::FairyLight, "FairyLight" },
{ CRGB::FairyLightNCC, "FairyLightNCC" }
};
const size_t colorNameCount = sizeof colorName / sizeof colorName[0];
const char* colorToString(const CRGB color) {
for (auto i = 0U; i < colorNameCount; ++i) {
if (colorName[i].colorCode == color) return colorName[i].colorName;
}
return NULL;
}
const CRGB colorNameToCode(const char* name) {
for (auto i = 0U; i < colorNameCount; ++i) {
if (isEqual(name, colorName[i].colorName)) return colorName[i].colorCode;
}
return CRGB::Black;
}
void setup() {
while (!Serial && millis() < 2000) ;
pinMode(MOMENTARY_BUTTON1_WHITE, INPUT_PULLDOWN);
pinMode(MOMENTARY_BUTTON2_WHITE, OUTPUT);
pinMode(PIR_INPUT_YELLOW, INPUT_PULLDOWN);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(SPEAKER_BLACK, OUTPUT);
pinMode(SPEAKER_RED, OUTPUT);
digitalWriteFast(SPEAKER_BLACK, 0);
digitalWriteFast(SPEAKER_RED, 0);
pinMode(PHOTODIODE_CATHODE_RED, INPUT_PULLDOWN);
pinMode(PHOTODIODE_ANODE_BLACK, OUTPUT);
digitalWriteFast(PHOTODIODE_ANODE_BLACK, 0);
InternalTemperature.begin(/* TEMPERATURE_NO_ADC_SETTING_CHANGES */);
{
// bool isOkay = InternalTemperature.singlePointCalibrationF(80.0f, 80.0f);
bool isOkay = InternalTemperature.dualPointCalibrationC(73.9f, 74.6f,
80.0f, 84.0f);
if (!isOkay && Serial) {
Serial.printf("Temperature calibration failed.\n");
}
}
// And now the star of the show. Initialize the LEDs.
FastLED.addLeds<LED_TYPE, DATA_PIN, CLOCK_PIN,
COLOR_ORDER, DATA_RATE_MHZ(CLOCK_MHZ)>(leds, NUM_LEDS);
if (Serial) {
Serial.printf("Serial connection time (ms): %d\n", millis());
Serial.printf("Teensy board type: %s\n", BOARD);
Serial.printf("sizeof(long) = %d; sizeof(int) = %d\n", sizeof(long), sizeof(int));
// SIM_UIDMH is x55 on two Teensy LCs
// SIM_UIDML is 0x262014 on my desk lamp, 0x2b2014 on a spare Teensy LC
// SIM_UIDL ia x33974e45 on two Teensy LCs
char id[36];
#if defined(__MKL26Z64__)
sprintf(id, "%08lX-%08lX-%08lX", SIM_UIDMH, SIM_UIDML, SIM_UIDL);
#else
sprintf(id, "%08lX-%08lX-%08lX-%08lX", SIM_UIDH, SIM_UIDMH, SIM_UIDML, SIM_UIDL);
#endif
Serial.printf("CPU ID: %s\n", id);
Serial.printf("CPU Frequency: %d\n", F_CPU);
Serial.printf("CPU Bus Frequency: %d\n", F_BUS);
Serial.printf("CPU Memory Frequency: %d\n", F_MEM);
Serial.printf("Build time: %s\n", __DATE__ " " __TIME__);
Serial.printf("Arduino version: %d\n", ARDUINO);
Serial.printf("Main source file: %s\n", __FILE__);
Serial.printf("Touch Read: %d\n", touchRead(TOUCH_CASE_SENSOR_BLUE));
Serial.printf("Light Read: %d\n", readLight());
Serial.printf("Temperature (°F): ");
Serial.println(readTemperatureF(), 1);
Serial.printf("Temperature uncalibrated (°F): ");
Serial.println(InternalTemperature.readUncalibratedTemperatureF(), 1);
basicBeep();
}
fill_solid(leds, NUM_LEDS, CRGB::White);
FastLED.setBrightness(8);
FastLED.show();
}
elapsedMillis sincePIR;
void loop() {
if (pollTouch()) {
//basicBeep();
}
if (digitalReadFast(PIR_INPUT_YELLOW)) {
fill_rainbow(leds, NUM_LEDS, sincePIR, 8);
FastLED.show();
// Motion was detected. The detector goes to sleep for 2.5 seconds after a detection.
if (2500 < sincePIR) sincePIR = 0;
}
if (1000 * 60 * 29 < sincePIR) {
// 1 minute until locking the screen
fill_solid(leds, NUM_LEDS, CRGB::Red);
FastLED.show();
}
if (1000 * 60 * 30 < sincePIR) {
sincePIR = 0;
// Lock the screen; it's been 30 minutes since the PIR sensor triggered.
delay(1000); // Delay a second just in case.
lockScreen();
}
digitalWriteFast(MOMENTARY_BUTTON2_WHITE, 1);
if (momentaryButton.update() && momentaryButton.fallingEdge()) {
if (strlen(buffer)) {
Keyboard.print(buffer);
} else {
Serial.printf("Enter 'macro <text>' to assign a macro.\n");
}
basicBeep();
}
digitalWriteFast(MOMENTARY_BUTTON2_WHITE, 0);
#if 0
auto light_level = readLight();
if (light_level < PHOTODIODE_MIN) {
Serial.printf("Light = %d\n", light_level);
}
#endif
// Handle USB-serial commands.
pollCommand(cmdline, cmax);
if (cmdline[0]) {
if (strcmp(cmdline, "help") == 0) {
Serial.printf("No help yet!\n");
} else if (isPrefix("macro", cmdline)) {
sscanf(cmdline, "macro %s", &buffer); // TODO - input length limit
Serial.printf("Got '%s'.\n", buffer);
} else if (isEqual("pir", cmdline)) {
Serial.printf("Time since PIR (seconds): %ld\n", sincePIR / 1000);
} else if (isPrefix("rgb", cmdline)) {
unsigned int r, g, b;
int count = sscanf(cmdline, "rgb %u %u %u", &r, &g, &b);
if (count != 3) {
Serial.printf("Expecting red, green, and blue values in the range 0..255.\n");
}
fill_solid(leds, NUM_LEDS, CRGB(r, g, b));
FastLED.show();
} else if (isEqual(cmdline, "ramp")) {
fill_gradient_RGB(leds, 0, CRGB::White, NUM_LEDS - 1, CRGB(1, 1, 1));
FastLED.show();
} else if (colorNameToCode(cmdline) != (CRGB) CRGB::Black) {
fill_solid(leds, NUM_LEDS, colorNameToCode(cmdline));
FastLED.show();
} else if (isEqual(cmdline, "Black")) {
fill_solid(leds, NUM_LEDS, CRGB::Black);
FastLED.show();
} else if (isPrefix("dither", cmdline)) {
bool isEnabled = strstr(cmdline, "on") || strstr(cmdline, "1");
bool isDisabled = strstr(cmdline, "off") || strstr(cmdline, "0");
if (isEnabled != isDisabled) {
FastLED.setDither(isEnabled);
Serial.printf("Set dither %s\n", isEnabled ? "on" : "off");
} else {
Serial.printf("Expecting dither setting of 'on' or 'off'\n");
}
} else if (isEqual(cmdline, "rainbow")) {
int hue_span = 128 / NUM_LEDS;
if (hue_span == 0) hue_span = 1;
for (auto i = 0U; i < (128 + 32) * 4; ++i) {
fill_rainbow(leds, NUM_LEDS, i, hue_span);
FastLED.delay(10);
}
} else if (isEqual(cmdline, "rotate")) {
rotate(1);
} else if (isPrefix("brightness", cmdline)) {
// Note: sizeof below includes the terminating null.
const auto arg = cmdline + sizeof("brightness");
const auto level = atoi(arg);
const auto arglen = strlen(arg);
if (0 < arglen && arglen < 4 && level < 256) {
FastLED.setBrightness(level);
} else if (arglen) {
Serial.printf("Expecting brightness from 0..255. Got '%s'.\n", arg);
}
Serial.printf("Brightness: %u\n", (unsigned int) FastLED.getBrightness());
} else if (isEqual(cmdline, "light")) {
Serial.printf("Light: %d\n", readLight());
} else if (isEqual(cmdline, "temperature")) {
Serial.print("Temperature (°F): ");
Serial.println(readTemperatureF(), 1);
Serial.print("Uncalibrated: ");
Serial.println(InternalTemperature.readUncalibratedTemperatureF(), 1);
} else if (isEqual(cmdline, "colors")) {
for (auto i = 0U; i < NUM_LEDS; ++i) {
auto name = colorToString(leds[i]);
if (!name) name = "";
Serial.printf("%u: rgb(%3u, %3u, %3u) %s\n", i, leds[i].red, leds[i].green, leds[i].blue, name);
}
} else if (isEqual(cmdline, "touch")) {
Serial.printf("Touch case: %u\n", touchRead(TOUCH_CASE_SENSOR_BLUE));
Serial.printf("Touch 1: %u\n", touchRead(1));
Serial.printf("Touch 3: %u\n", touchRead(3));
Serial.printf("Touch 4: %u\n", touchRead(4));
}
cmdline[0] = 0;
FastLED.show();
}
}