/** * Firmware to control a LED matrix display * https://github.com/noahwilliamsson/lamatrix * * -- noah@hack.se, 2018 * */ #ifdef TEENSYDUINO #include #endif #include "FastLED.h" #define HOST_SHUTDOWN_PIN 8 #define BUTTON_PIN 12 #define NUM_LEDS 256 #ifdef TEENSYDUINO #define FastLED_Pin 6 #else #define FastLED_Pin 22 #endif static void put_pixel(int, int, int); static void render_clock(int); #ifdef TEENSYDUINO static time_t getTeensy3Time(); #endif /** * Serial protocol */ enum { FUNC_RESET = 0, /* Initialize display with [pixels & 0xff, (pixels>>8) & 0xff] LEDs */ FUNC_INIT_DISPLAY = 'i', /* Clear display: [dummy byte] */ FUNC_CLEAR_DISPLAY = 'c', /* Update display: [dummy byte] */ FUNC_SHOW_DISPLAY = 's', /* Put pixel at [pixel&0ff, (pixel >> 8) &0xff, R, G, B] */ FUNC_PUT_PIXEL = 'l', /* Set time [t&0xff, (t >> 8) & 0xff, (t >> 16) & 0xff, (t >> 24) & 0xff] */ FUNC_SET_RTC = '@', /* Automatically render time [enable/toggle byte] */ FUNC_AUTO_TIME = 't', /* Suspend host for [seconds & 0xff, (seconds >> 8) & 0xff] */ FUNC_SUSPEND_HOST = 'S', }; /* Computed with pixelfont.py */ static int font_width = 4; static int font_height = 5; static char font_alphabet[] = " %'-./0123456789:cms"; static unsigned char font_data[] = "\x00\x00\x50\x24\x51\x66\x00\x00\x60\x00\x00\x00\x42\x24\x11\x57\x55\x27\x23\x72\x47\x17\x77\x64\x74\x55\x47\x74\x71\x74\x17\x57\x77\x44\x44\x57\x57\x77\x75\x74\x20\x20\x20\x15\x25\x75\x57\x75\x71\x74"; /* Global states */ int state = 0; int debug_serial = 0; /* Debug state issues */ int last_states[8]; unsigned int last_state_counter = 0; /* Non-zero when automatically rendering the current time */ int show_time = 1; /* Non-zero while the host computer is turned off */ time_t reboot_at = 0; /* Accumulator register for use between loop() calls */ unsigned int acc; unsigned int color; CRGB leds[NUM_LEDS]; static volatile int g_button_state; static int button_down_t; static void button_irq(void) { int state = digitalRead(BUTTON_PIN); if(state == HIGH) { /* Start counting when the circuit is broken */ button_down_t = millis(); return; } if(!button_down_t) return; int pressed_for_ms = millis() - button_down_t; if(pressed_for_ms > 500) g_button_state = 2; else if(pressed_for_ms > 100) g_button_state = 1; button_down_t = 0; } void setup() { Serial.begin(460800); /* Initialize FastLED library */ FastLED.addLeds(leds, NUM_LEDS); /* Configure pin used to shutdown Raspberry Pi (connected to GPIO5 on the Pi) */ pinMode(HOST_SHUTDOWN_PIN, OUTPUT); digitalWrite(HOST_SHUTDOWN_PIN, HIGH); /* Configure pin for button */ pinMode(BUTTON_PIN, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), button_irq, CHANGE); #ifdef TEENSYDUINO /* Initialize time library */ setSyncProvider(getTeensy3Time); if (timeStatus() != timeSet) { Serial.println("Unable to sync with the RTC"); } else { Serial.println("RTC has set the system time"); show_time = 1; } Serial.printf("%04d-%02d-%02dT%02d:%02d:%02dZ\n", year(), month(), day(), hour(), minute(), second()); #endif } void loop() { #ifdef TEENSYDUINO time_t now = getTeensy3Time(); #else int now = 42; #endif int button_state = g_button_state; if(button_state) { g_button_state = 0; if(button_state == 1) Serial.println("BUTTON_SHRT_PRESS"); else Serial.println("BUTTON_LONG_PRESS"); } if(reboot_at && now >= reboot_at) { /* Restart host computer */ digitalWrite(HOST_SHUTDOWN_PIN, LOW); delay(1); digitalWrite(HOST_SHUTDOWN_PIN, HIGH); reboot_at = 0; } if(show_time) { /* Automatically render time */ if(show_time != now || button_state) { render_clock(button_state); show_time = now; } } if (Serial.available() <= 0) return; int val = Serial.read(); last_states[last_state_counter++ % (sizeof(last_states)/sizeof(last_states[0]))] = val; switch(state) { case FUNC_RESET: /** * Pyserial sometimes experience write timeouts so we * use a string of zeroes to resynchronize the state. */ state = val; break; case FUNC_INIT_DISPLAY: acc = val; state++; break; case FUNC_INIT_DISPLAY+1: acc |= val << 8; FastLED.addLeds(leds, acc); /* fall through */ case FUNC_SET_RTC: acc = val; state++; break; case FUNC_SET_RTC+1: acc |= val << 8; state++; break; case FUNC_SET_RTC+2: acc |= val << 16; state++; break; case FUNC_SET_RTC+3: acc |= val << 24; #ifdef TEENSYDUINO Teensy3Clock.set(acc); // set the RTC setTime(acc); Serial.printf("RTC synchronized: %04d-%02d-%02dT%02d:%02d:%02dZ\n", year(), month(), day(), hour(), minute(), second()); #endif state = FUNC_RESET; break; case FUNC_CLEAR_DISPLAY: for(int i = 0; i < NUM_LEDS; i++) leds[i].setRGB(0,0,0); /* fall through */ case FUNC_SHOW_DISPLAY: FastLED.show(); state = FUNC_RESET; break; case FUNC_SUSPEND_HOST: acc = val; state++; break; case FUNC_SUSPEND_HOST+1: acc |= val << 8; /* TODO: Suspend host computer */ reboot_at = now + acc; if(reboot_at >= 10) { /* Automatically render time while host computer is offline */ show_time = 1; Serial.printf("Shutting down host computer, reboot scheduled in %ds\n", reboot_at); /* Initiate poweroff on Raspberry Pi */ digitalWrite(HOST_SHUTDOWN_PIN, LOW); delay(1); digitalWrite(HOST_SHUTDOWN_PIN, HIGH); } state = FUNC_RESET; break; case FUNC_AUTO_TIME: if(val == '\r' || val == '\n') show_time = !show_time; /* toggle */ else show_time = val; Serial.printf("Automatic rendering of current time: %d\n", show_time); state = FUNC_RESET; break; case FUNC_PUT_PIXEL: acc = val; state++; break; case FUNC_PUT_PIXEL+1: acc |= val << 8; state++; break; case FUNC_PUT_PIXEL+2: color = val; state++; break; case FUNC_PUT_PIXEL+3: color |= val << 8; state++; break; case FUNC_PUT_PIXEL+4: color |= val << 16; leds[(acc % NUM_LEDS)].setRGB(color & 0xff, (color >> 8) & 0xff, (color >> 16) & 0xff); state = FUNC_RESET; break; default: Serial.printf("Unknown func %d with val %d, resetting\n", state, val); for(unsigned int i = 0; i < sizeof(last_states)/sizeof(last_states[0]) && last_state_counter - i > 0; i++) Serial.printf("Previous state %d: %d\n", i, last_states[(last_state_counter-i) % (sizeof(last_states)/sizeof(last_states[0]))]); state = FUNC_RESET; break; } } /* Pretty much a port of LedMatrix.xy_to_phys() */ static void put_pixel(int x, int y, int lit) { /** * The LEDs are laid out in a long string going from north to south, * one step to the east, and then south to north, before the cycle * starts over */ int cycle = 16; int nssn_block = x / 2; int phys_addr = nssn_block * 16; int brightness_scaler = 48; /* use less power */ if(x % 2) phys_addr += cycle - 1 - y; else phys_addr += y; lit &= 0xff; lit /= brightness_scaler; leds[phys_addr % NUM_LEDS].setRGB(lit, lit, lit); } #ifdef TEENSYDUINO /* Wrapper function for Timelib's sync provider */ static time_t getTeensy3Time(void) { return Teensy3Clock.get(); } #endif /* Render time as reported by the RTC */ static int clock_state = 0x2; static void render_clock(int button_state) { char buf[10]; int x_off; size_t len; if(button_state) { clock_state ^= 1 << (button_state-1); for(int i = 0; i < NUM_LEDS; i++) leds[i].setRGB(0,0,0); } #ifdef TEENSYDUINO if((clock_state & 1) == 0) { sprintf(buf, "%02d:%02d", hour(), minute()); if((clock_state & 2) && second() % 2) buf[2] = ' '; } else { sprintf(buf, "%02d.%02d.%02d", day(), month(), year() % 100); } #else sprintf(buf, "00:00"); #endif if((clock_state & 1) == 0) x_off = 8 - clock_state; else x_off = 2; len = strlen(buf); for(size_t i = 0; i < len; i++) { unsigned char digit = buf[i]; size_t offset; /* Kludge to compress colons and dots to two columns */ if(digit == ':' || digit == '.' || digit == ' ' || (i && (buf[i-1] == ':' || buf[i-1] == '.' || buf[i-1] == ' '))) x_off--; for(offset = 0; offset < strlen(font_alphabet); offset++) { if(font_alphabet[offset] == digit) break; } int font_byte = (offset * font_width * font_height) / 8; int font_bit = (offset * font_width * font_height) % 8; for(int y = 0; y < font_height; y++) { for(int x = 0; x < font_width; x++) { if(font_data[font_byte] & (1<