Public Access
Restructure into a single PlatformIO project with nano and esp32 targets
- Shared BROSE9323 driver in lib/, ESPSERIAL selected via build_flags - Nano firmware in src/nano, ESP32 firmware in src/esp32 - Move WiFi/MQTT credentials to gitignored secrets.h (template provided) - Pin home-assistant-integration to 2.1.0 from the registry - Add esp32_usb environment for first flash / recovery - Add READMEs and CLAUDE.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
0e4d39c47b
18 files changed
+1905
No files matched your search
+12
@@ -0,0 +1,12 @@
|
|||||||
|
# PlatformIO
|
||||||
|
.pio/
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
|
|
||||||
|
# Local credentials (template: src/esp32/secrets.example.h)
|
||||||
|
src/esp32/secrets.h
|
||||||
|
|
||||||
|
# Claude Code local settings
|
||||||
|
.claude/settings.local.json
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Flipdot display: notes for Claude
|
||||||
|
|
||||||
|
A PlatformIO project with two firmware targets for an 84×19 Brose flipdot clock.
|
||||||
|
See README.md for the overview.
|
||||||
|
|
||||||
|
## Layout and build
|
||||||
|
|
||||||
|
- `platformio.ini` has the environments `nano`, `esp32` (OTA to 10.1.1.198) and
|
||||||
|
`esp32_usb` (COM11). The sources are split with `build_src_filter` (`src/nano/`,
|
||||||
|
`src/esp32/`).
|
||||||
|
- `lib/BROSE9323/` is the shared driver. Its behaviour depends on build flags set in
|
||||||
|
`platformio.ini`, not in the header:
|
||||||
|
- `ESPSERIAL` (ESP32): draw calls become serial commands.
|
||||||
|
- `FLIPDOT_PLCC_ADAPTER` (Nano, optional): alternative pinout.
|
||||||
|
- PlatformIO CLI is not on PATH. Run `~/.platformio/penv/Scripts/pio.exe run` (Git Bash)
|
||||||
|
or `$env:USERPROFILE\.platformio\penv\Scripts\pio.exe run` (PowerShell).
|
||||||
|
- If an ESP32 build fails with `No module named 'intelhex'`, run
|
||||||
|
`~/.platformio/penv/Scripts/python.exe -m pip install intelhex`.
|
||||||
|
- After any change, build both targets: `pio run -e nano -e esp32`.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- **Nano RAM is tight** (ATmega328P, 2 KB). The frame buffers are allocated with
|
||||||
|
`calloc`. Avoid `String`, large stack buffers and new globals in Nano code. Put constant
|
||||||
|
data in `PROGMEM`.
|
||||||
|
- The Nano and the ESP32 share one UART. On the ESP32, `Serial` is the display link, so
|
||||||
|
**don't print debug output on the ESP32's `Serial`**: the Nano would read it as
|
||||||
|
commands. The Nano ignores unknown command bytes, but text that starts with
|
||||||
|
s/S/F/D/P/B/R/T will be run as a command.
|
||||||
|
- `src/esp32/secrets.h` holds the real credentials and is gitignored. Never commit it.
|
||||||
|
Add new secrets to `secrets.example.h` as placeholders.
|
||||||
|
- Hardware is not available here. Builds can be checked, but runtime behaviour can't.
|
||||||
|
|
||||||
|
## Known quirks in the serial protocol (Nano `src/nano/main.cpp`)
|
||||||
|
|
||||||
|
- `T` waits for more than 6 bytes before parsing, but `T225\n` has only 4 after the `T`.
|
||||||
|
It completes only when the next command arrives.
|
||||||
|
- The `T` value is stored with `EEPROM.update` in one byte, so values above 255 µs are
|
||||||
|
truncated after a reboot.
|
||||||
|
- `P` (print text) busy-waits for a `\n` and has no timeout.
|
||||||
|
|
||||||
|
## Git
|
||||||
|
|
||||||
|
- Remote: https://mangavalk.com/mangavalk/Flipdot-display.git, branch `main`.
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Flipdot Display
|
||||||
|
|
||||||
|
Firmware for an 84 × 19 Brose flipdot display (three 28 × 19 panels) that runs as a
|
||||||
|
Home Assistant–controlled clock.
|
||||||
|
|
||||||
|
The system uses two microcontrollers:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ WiFi / MQTT ┌────────────────┐ UART 115200 ┌──────────────┐ GPIO ┌──────────────┐
|
||||||
|
│ Home │ ◄─────────────► │ ESP32 │ ──────────────► │ Arduino Nano │ ───────► │ Brose │
|
||||||
|
│ Assistant │ │ (controller) │ text commands │ (driver) │ │ flipdot │
|
||||||
|
└──────────────┘ └───────┬────────┘ └──────────────┘ │ panels 84x19 │
|
||||||
|
│ GPIO 13 └──────▲───────┘
|
||||||
|
▼ │
|
||||||
|
┌─────────────┐ display power │
|
||||||
|
│ Relay │ ───────────────────────────────────────────────────┘
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
| Part | Folder | What it does |
|
||||||
|
|------|--------|--------------|
|
||||||
|
| **Arduino Nano** | [src/nano](src/nano/README.md) | Drives the panel's row and column electronics directly. Keeps a frame buffer and flips only the dots that changed. Takes simple text commands over serial. |
|
||||||
|
| **ESP32** | [src/esp32](src/esp32/README.md) | Connects to WiFi, gets the time over NTP, shows a clock with Pikachu bitmaps, exposes switches to Home Assistant over MQTT, switches display power with a relay and supports OTA updates. |
|
||||||
|
| **BROSE9323 driver** | [lib/BROSE9323](lib/BROSE9323/README.md) | Shared `Adafruit_GFX` display driver. On the Nano it toggles GPIO pins. On the ESP32 (`-D ESPSERIAL`) it turns every drawing call into a serial command for the Nano. |
|
||||||
|
|
||||||
|
Because the driver is an `Adafruit_GFX` subclass on both boards, the ESP32 can use the
|
||||||
|
normal GFX API (`print`, `drawBitmap`, `fillScreen`, ...). Each call is sent to the
|
||||||
|
Nano and drawn there.
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
```
|
||||||
|
platformio.ini one PlatformIO project, multiple environments
|
||||||
|
lib/BROSE9323/ shared flipdot driver (used by both targets)
|
||||||
|
src/nano/ Arduino Nano firmware
|
||||||
|
src/esp32/ ESP32 firmware
|
||||||
|
secrets.example.h template for WiFi / MQTT credentials
|
||||||
|
archive/ old, unused sketches kept for reference
|
||||||
|
CLAUDE.md project notes for Claude Code
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building and flashing
|
||||||
|
|
||||||
|
Install [PlatformIO](https://platformio.org/) (the VS Code extension is easiest) and open
|
||||||
|
this folder.
|
||||||
|
|
||||||
|
1. Copy `src/esp32/secrets.example.h` to `src/esp32/secrets.h` and fill in your WiFi and
|
||||||
|
MQTT details. `secrets.h` is gitignored.
|
||||||
|
2. Check the COM ports and the OTA IP in `platformio.ini`.
|
||||||
|
3. Build or upload:
|
||||||
|
|
||||||
|
| Environment | Board | Upload method |
|
||||||
|
|-------------|-------|---------------|
|
||||||
|
| `nano` | Arduino Nano (ATmega328P) | USB (`COM10`) |
|
||||||
|
| `esp32` | ESP32 DevKit v1 | OTA over WiFi (`10.1.1.198`) |
|
||||||
|
| `esp32_usb` | ESP32 DevKit v1 | USB (`COM11`). Use it for the first flash or for recovery. |
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pio run # build nano + esp32
|
||||||
|
pio run -e nano -t upload # flash the Nano
|
||||||
|
pio run -e esp32 -t upload # flash the ESP32 over the air
|
||||||
|
pio run -e esp32_usb -t upload # flash the ESP32 over USB
|
||||||
|
pio device monitor -e nano # serial monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
In VS Code you can also pick the environment in the PlatformIO status bar.
|
||||||
|
|
||||||
|
> **Tip:** The Nano and the ESP32 share a serial line. Disconnect the ESP32 TX → Nano RX
|
||||||
|
> wire before you flash the Nano over USB, or the upload may fail.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. **ESP32 boot.** The ESP32 connects to WiFi and starts ArduinoOTA. After a crash or
|
||||||
|
unexpected reset it stays in an *OTA panic window* for 15 seconds, so you can flash a
|
||||||
|
fix before the main code runs again. It then switches the display relay on, starts NTP
|
||||||
|
and connects to MQTT.
|
||||||
|
2. **Clock loop (every second).**
|
||||||
|
- **07:00–22:00:** Pikachu with a thought bubble showing `HH:MM`. A yawning Pikachu is
|
||||||
|
shown from 17:00.
|
||||||
|
- **22:00–23:30:** the clock is only drawn on the quarter hour. In between, the relay
|
||||||
|
turns the display off.
|
||||||
|
- **Night:** the screen is cleared and power is cut after 30 seconds.
|
||||||
|
3. **Home Assistant switches** (MQTT discovery):
|
||||||
|
- `FlipDot Display Clock`: off shows a Nintendo logo instead of the clock.
|
||||||
|
- `FlipDot Display Power`: off clears the display and cuts power through the relay.
|
||||||
|
4. **Nano.** The Nano reads commands from the serial line and updates its frame buffer.
|
||||||
|
On `D` it flips only the dots that changed. Every 2 minutes it redraws the whole screen
|
||||||
|
to fix dots that did not flip.
|
||||||
|
|
||||||
|
The serial protocol is described in [src/nano/README.md](src/nano/README.md).
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Archive
|
||||||
|
|
||||||
|
Old sketches kept for reference. They are **not** built. The `.txt` extension keeps
|
||||||
|
PlatformIO from compiling them.
|
||||||
|
|
||||||
|
- `SerialAPI_nano_old.cpp.txt`: an earlier version of the Nano serial firmware, from
|
||||||
|
before the EEPROM timing and the default flip time were added.
|
||||||
|
- `marquee_demo.cpp.txt`: a scrolling text (marquee) demo, including an umlaut test.
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include "BROSE9323.h"
|
||||||
|
// fixes
|
||||||
|
#include <SPI.h>
|
||||||
|
#include <Adafruit_I2CDevice.h>
|
||||||
|
|
||||||
|
BROSE9323 display(84, 19, 28);
|
||||||
|
|
||||||
|
uint32_t refresh_at;
|
||||||
|
const uint32_t REFRESH_TIME = 120000L;
|
||||||
|
|
||||||
|
//#define DEBUG
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
display.begin();
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
display.fillScreen(1);
|
||||||
|
display.display();
|
||||||
|
display.fillScreen(0);
|
||||||
|
uint32_t t1 = millis();
|
||||||
|
display.display();
|
||||||
|
uint32_t t2 = millis();
|
||||||
|
Serial.println(t2 - t1);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t hex2dec(char c) {
|
||||||
|
if (c >= '0' && c <= '9') return c - '0';
|
||||||
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||||
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
if (Serial.available()) {
|
||||||
|
char cmd = Serial.read();
|
||||||
|
switch (cmd) {
|
||||||
|
// Ignore CR
|
||||||
|
case 0x0A:
|
||||||
|
break;
|
||||||
|
// Draw Bitmap (must be n x 8 wide)
|
||||||
|
case 'b': case 'B': //BxxyywwhhcBITMAP (xx/ww = 8 bit hex x position/width; yy/hh = 8 bit y position/height; c = 0/1 color; BITMAP = bytes as hex)
|
||||||
|
{
|
||||||
|
// May lead to buffer overflow, you may need to segment the bitmap into multiple frames
|
||||||
|
// Or just use the ser pixel function
|
||||||
|
while (Serial.available() < 9) {
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
uint8_t x = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
uint8_t y = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
uint8_t w = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
if (w % 4) {
|
||||||
|
Serial.println("ERR: Invalid bitmap width (not dividable by 4)");
|
||||||
|
Serial.flush();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint8_t h = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
if (h == 0) {
|
||||||
|
Serial.println("ERR: Invalid bitmap height (0)");
|
||||||
|
Serial.flush();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint8_t c = Serial.read() - '0';
|
||||||
|
uint8_t b = 0;
|
||||||
|
for (uint8_t _y = 0; _y < h; _y++) {
|
||||||
|
for (uint8_t _x = 0; _x < w; _x++) {
|
||||||
|
if ((_x & 0x3) == 0) {
|
||||||
|
if (!Serial.available()) {
|
||||||
|
Serial.println("WARN: OUTOFMEM");
|
||||||
|
}
|
||||||
|
while (!Serial.available()) {
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
b = hex2dec(Serial.read());
|
||||||
|
}
|
||||||
|
if (b & (1 << (0x3 - (_x & 0x3)))) {
|
||||||
|
display.drawPixel(x + _x, y + _y, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Display
|
||||||
|
case 'd': case 'D': //D
|
||||||
|
{
|
||||||
|
display.display();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Fill Screen
|
||||||
|
case 'f': case 'F': //Fc (c = 0/1 color)
|
||||||
|
{
|
||||||
|
while (Serial.available() <= 2) {
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
display.fillScreen(Serial.read() - '0');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Print Text
|
||||||
|
case 'p': case 'P': // PxxyycsTEXT (xx = 8 bit hex x position; yy = 8 bit y position; c = 0/1 color; s = 1/2 size; TEXT = string, newline = escaped \\n)
|
||||||
|
{
|
||||||
|
delay(5);
|
||||||
|
while (Serial.available() <= 6) {
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
uint8_t x = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
uint8_t y = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
uint8_t c = Serial.read() - '0';
|
||||||
|
display.setCursor(x, y);
|
||||||
|
display.setTextColor(c, !c);
|
||||||
|
display.setTextSize(Serial.read() - '0');
|
||||||
|
while (true) {
|
||||||
|
if (!Serial.available()) {
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
if (Serial.peek() == '\n') break;
|
||||||
|
if (Serial.peek() == '\r') {Serial.read();continue;};
|
||||||
|
char c = Serial.read();
|
||||||
|
if (c == '\\' && Serial.peek() == 'n') {
|
||||||
|
display.println();
|
||||||
|
Serial.read();
|
||||||
|
} else {
|
||||||
|
display.write(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Return buffer (show pixels on console)
|
||||||
|
case 'r': case 'R': //R
|
||||||
|
{
|
||||||
|
display.printBuffer();
|
||||||
|
Serial.flush();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Set Pixel
|
||||||
|
case 'S':
|
||||||
|
display.setDirect(true);
|
||||||
|
case 's': // Sxxyyc (xx = 8 bit hex x position; y = 8 bit y position; c = 0/1 color)
|
||||||
|
{
|
||||||
|
while (Serial.available() <= 6) {
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
uint8_t x = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
uint8_t y = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
bool color = hex2dec(Serial.read());
|
||||||
|
display.drawPixel(x, y, color);
|
||||||
|
display.setDirect(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Set Timing
|
||||||
|
case 'T': case 't': // Tttt (ttt = decimal timing in us)
|
||||||
|
{
|
||||||
|
while (Serial.available() <= 6) {
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
uint16_t t = atoi(Serial.readStringUntil('\n').c_str());
|
||||||
|
display.setTiming(t);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
#ifdef DEBUG
|
||||||
|
Serial.print("0x");
|
||||||
|
Serial.print(cmd >> 4, HEX);
|
||||||
|
Serial.print(cmd & 3, HEX);
|
||||||
|
Serial.println(" - command not known.");
|
||||||
|
Serial.flush();
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Refresh content every REFRESH_TIME (by default 2 min)
|
||||||
|
if (refresh_at < millis()) {
|
||||||
|
refresh_at = millis() + REFRESH_TIME;
|
||||||
|
display.display(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include "BROSE9323.h"
|
||||||
|
// fixes
|
||||||
|
#include <SPI.h>
|
||||||
|
#include <Adafruit_I2CDevice.h>
|
||||||
|
|
||||||
|
BROSE9323 display(84, 19, 28);
|
||||||
|
|
||||||
|
/*
|
||||||
|
UMLAUT:
|
||||||
|
Ä \x8E
|
||||||
|
ä \x84
|
||||||
|
Ö \x99
|
||||||
|
ö \x94
|
||||||
|
Ü \x9A
|
||||||
|
ü \x81
|
||||||
|
ß \xE0
|
||||||
|
*/
|
||||||
|
// const char text[] = "This is a test. Testing \x9Aml\x8Aute. ";
|
||||||
|
const char text[] = "This is a test. Testing Ümläute. ";
|
||||||
|
const uint8_t textsize = 2;
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
display.begin();
|
||||||
|
delay(2000);
|
||||||
|
display.fillScreen(0);
|
||||||
|
display.setTextSize(textsize);
|
||||||
|
display.setTextWrap(false);
|
||||||
|
display.setTextColor(1, 0);
|
||||||
|
//display.setDirect(true); //when using setTextColor(1, 0) we don't need a separate display() and the text scrolls more evenly (on *328*)
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
for (int16_t x = display.width(); x > -((int16_t)strlen(text) * textsize * 6); x--)
|
||||||
|
{
|
||||||
|
display.setCursor(x, 0);
|
||||||
|
display.print(text);
|
||||||
|
display.display();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
#include "BROSE9323.h"
|
||||||
|
|
||||||
|
BROSE9323::BROSE9323(uint8_t w, uint8_t h, uint8_t pw, uint16_t ft) : Adafruit_GFX(w, h),
|
||||||
|
_flip_time(ft),
|
||||||
|
_panel_width(pw),
|
||||||
|
_buffer_width((w + 7) / 8),
|
||||||
|
_buffer_size(_buffer_width * h)
|
||||||
|
{
|
||||||
|
// Buffer width should fit at least [width] bits
|
||||||
|
//_buffer_width = (w + 7) / 8;
|
||||||
|
|
||||||
|
//_buffer_size = _buffer_width * h;
|
||||||
|
|
||||||
|
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
|
||||||
|
_old_buffer = (uint8_t *)calloc(_buffer_size, sizeof(uint8_t));
|
||||||
|
#endif
|
||||||
|
_new_buffer = (uint8_t *)calloc(_buffer_size, sizeof(uint8_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ESPSERIAL
|
||||||
|
void BROSE9323::begin(Stream *s)
|
||||||
|
{
|
||||||
|
stream = s;
|
||||||
|
fillScreen(0);
|
||||||
|
setCursor(0, 4);
|
||||||
|
print("ESP Connected");
|
||||||
|
display();
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
void BROSE9323::begin(void)
|
||||||
|
{
|
||||||
|
digitalWrite(ADDR_0, 1);
|
||||||
|
digitalWrite(ADDR_1, 1);
|
||||||
|
digitalWrite(ADDR_2, 1);
|
||||||
|
digitalWrite(COL_0, 1);
|
||||||
|
digitalWrite(COL_1, 1);
|
||||||
|
digitalWrite(COL_2, 1);
|
||||||
|
digitalWrite(COL_3, 1);
|
||||||
|
digitalWrite(COL_4, 1);
|
||||||
|
digitalWrite(ROW_0, 1);
|
||||||
|
digitalWrite(ROW_1, 1);
|
||||||
|
digitalWrite(ROW_2, 1);
|
||||||
|
digitalWrite(ROW_3, 1);
|
||||||
|
digitalWrite(ROW_4, 1);
|
||||||
|
digitalWrite(ROW_RESET, 1);
|
||||||
|
digitalWrite(ROW_SET, 1);
|
||||||
|
digitalWrite(COL_DATA, 1);
|
||||||
|
digitalWrite(ENABLE, 1);
|
||||||
|
pinMode(ADDR_0, OUTPUT);
|
||||||
|
pinMode(ADDR_1, OUTPUT);
|
||||||
|
pinMode(ADDR_2, OUTPUT);
|
||||||
|
pinMode(COL_0, OUTPUT);
|
||||||
|
pinMode(COL_1, OUTPUT);
|
||||||
|
pinMode(COL_2, OUTPUT);
|
||||||
|
pinMode(COL_3, OUTPUT);
|
||||||
|
pinMode(COL_4, OUTPUT);
|
||||||
|
pinMode(ROW_0, OUTPUT);
|
||||||
|
pinMode(ROW_1, OUTPUT);
|
||||||
|
pinMode(ROW_2, OUTPUT);
|
||||||
|
pinMode(ROW_3, OUTPUT);
|
||||||
|
pinMode(ROW_4, OUTPUT);
|
||||||
|
pinMode(ROW_RESET, OUTPUT);
|
||||||
|
pinMode(ROW_SET, OUTPUT);
|
||||||
|
pinMode(COL_DATA, OUTPUT);
|
||||||
|
pinMode(ENABLE, OUTPUT);
|
||||||
|
_setData(1);
|
||||||
|
_setData(0);
|
||||||
|
fillScreen(1);
|
||||||
|
display();
|
||||||
|
fillScreen(0);
|
||||||
|
drawBitmap((width() - 72) / 2, (height() - 16) / 2, _hannio_splash, 72, 16, 1);
|
||||||
|
display();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void BROSE9323::display(bool force)
|
||||||
|
{
|
||||||
|
#ifdef ESPSERIAL
|
||||||
|
stream->print("D\n");
|
||||||
|
stream->flush();
|
||||||
|
#else
|
||||||
|
if (_direct_mode)
|
||||||
|
return;
|
||||||
|
for (uint8_t x = 0; x < width(); x++)
|
||||||
|
{
|
||||||
|
_selectPanel(x / _panel_width);
|
||||||
|
|
||||||
|
_selectColumn(x % _panel_width);
|
||||||
|
|
||||||
|
for (uint8_t y = 0; y < height(); y++)
|
||||||
|
{
|
||||||
|
bool b = _new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7));
|
||||||
|
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
|
||||||
|
if (!force && (bool)(_old_buffer[y * _buffer_width + x / 8] & (1 << (x & 7))) == b)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
_selectRow(y);
|
||||||
|
|
||||||
|
_setData(_new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7)));
|
||||||
|
|
||||||
|
_strobe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
|
||||||
|
// Store currently displayed content in old buffer
|
||||||
|
memcpy(_old_buffer, _new_buffer, _buffer_size);
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void BROSE9323::drawPixel(int16_t x, int16_t y, uint16_t color)
|
||||||
|
{
|
||||||
|
#ifdef ESPSERIAL
|
||||||
|
if (y >= height() || y < 0 || x >= width() || x < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
char s[10];
|
||||||
|
sprintf(s, "%c%02x%02x%d\n", _direct_mode ? 'S' : 's', x, y, (bool)color);
|
||||||
|
stream->print(s);
|
||||||
|
stream->flush();
|
||||||
|
#else
|
||||||
|
if (y >= height() || y < 0 || x >= width() || x < 0)
|
||||||
|
return;
|
||||||
|
if (color)
|
||||||
|
{
|
||||||
|
if (_new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_new_buffer[y * _buffer_width + x / 8] |= 1 << (x & 7);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (~_new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_new_buffer[y * _buffer_width + x / 8] &= ~(1 << (x & 7));
|
||||||
|
}
|
||||||
|
if (_direct_mode)
|
||||||
|
{
|
||||||
|
_selectPanel(x / _panel_width);
|
||||||
|
|
||||||
|
_selectColumn(x % _panel_width);
|
||||||
|
|
||||||
|
_selectRow(y);
|
||||||
|
|
||||||
|
_setData(_new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7)));
|
||||||
|
|
||||||
|
_strobe();
|
||||||
|
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
|
||||||
|
if (color)
|
||||||
|
{
|
||||||
|
if (_new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_new_buffer[y * _buffer_width + x / 8] |= 1 << (x & 7);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (~_new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_new_buffer[y * _buffer_width + x / 8] &= ~(1 << (x & 7));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void BROSE9323::fillScreen(uint16_t color)
|
||||||
|
{
|
||||||
|
#ifdef ESPSERIAL
|
||||||
|
stream->print(color ? "F1\n" : "F0\n");
|
||||||
|
#else
|
||||||
|
memset(_new_buffer, color ? 0xFF : 0x00, _buffer_size);
|
||||||
|
if (_direct_mode)
|
||||||
|
{
|
||||||
|
_setData(color);
|
||||||
|
for (uint8_t x = 0; x < width(); x++)
|
||||||
|
{
|
||||||
|
if (x % _panel_width == 0)
|
||||||
|
{
|
||||||
|
_selectPanel(x / _panel_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectColumn(x % _panel_width);
|
||||||
|
|
||||||
|
for (uint8_t y = 0; y < height(); y++)
|
||||||
|
{
|
||||||
|
_selectRow(y);
|
||||||
|
|
||||||
|
_strobe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
|
||||||
|
memset(_old_buffer, color ? 0xFF : 0x00, _buffer_size);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void BROSE9323::setTiming(uint16_t t)
|
||||||
|
{
|
||||||
|
#ifdef ESPSERIAL
|
||||||
|
stream->write('T');
|
||||||
|
stream->print(t);
|
||||||
|
stream->write('\n');
|
||||||
|
#else
|
||||||
|
_flip_time = t;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void BROSE9323::setDirect(bool d)
|
||||||
|
{
|
||||||
|
_direct_mode = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef ESPSERIAL
|
||||||
|
void BROSE9323::printBuffer(void)
|
||||||
|
{
|
||||||
|
for (uint8_t y = 0; y < height(); y++)
|
||||||
|
{
|
||||||
|
for (uint8_t x = 0; x < width(); x++)
|
||||||
|
{
|
||||||
|
Serial.write(_new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7)) ? '*' : ' ');
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BROSE9323::_selectColumn(uint8_t col)
|
||||||
|
{
|
||||||
|
if (_active_col == col)
|
||||||
|
return;
|
||||||
|
_active_col = col;
|
||||||
|
col += 1 + col / 7;
|
||||||
|
digitalWrite(COL_0, col & 1);
|
||||||
|
digitalWrite(COL_1, col & 2);
|
||||||
|
digitalWrite(COL_2, col & 4);
|
||||||
|
digitalWrite(COL_3, col & 8);
|
||||||
|
digitalWrite(COL_4, col & 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BROSE9323::_selectPanel(uint8_t panel)
|
||||||
|
{
|
||||||
|
if (_active_panel == panel)
|
||||||
|
return;
|
||||||
|
_active_panel = panel;
|
||||||
|
digitalWrite(ADDR_0, panel & 1);
|
||||||
|
digitalWrite(ADDR_1, panel & 2);
|
||||||
|
digitalWrite(ADDR_2, panel & 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BROSE9323::_selectRow(uint8_t row)
|
||||||
|
{
|
||||||
|
if (_active_row == row)
|
||||||
|
return;
|
||||||
|
_active_row = row;
|
||||||
|
row += row / 7;
|
||||||
|
digitalWrite(ROW_0, row & 1);
|
||||||
|
digitalWrite(ROW_1, row & 2);
|
||||||
|
digitalWrite(ROW_2, row & 4);
|
||||||
|
digitalWrite(ROW_3, row & 8);
|
||||||
|
digitalWrite(ROW_4, row & 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BROSE9323::_setData(bool data)
|
||||||
|
{
|
||||||
|
if (_active_data == data)
|
||||||
|
return;
|
||||||
|
_active_data = data;
|
||||||
|
if (data)
|
||||||
|
{
|
||||||
|
digitalWrite(ROW_RESET, 1);
|
||||||
|
digitalWrite(ROW_SET, 0);
|
||||||
|
digitalWrite(COL_DATA, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
digitalWrite(ROW_SET, 1);
|
||||||
|
digitalWrite(ROW_RESET, 0);
|
||||||
|
digitalWrite(COL_DATA, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BROSE9323::_strobe(void)
|
||||||
|
{
|
||||||
|
digitalWrite(ENABLE, 0);
|
||||||
|
delayMicroseconds(_flip_time);
|
||||||
|
digitalWrite(ENABLE, 1);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
size_t BROSE9323::print(const char *ifsh)
|
||||||
|
{
|
||||||
|
// replace these characters
|
||||||
|
/*
|
||||||
|
UMLAUT:
|
||||||
|
Ä \x8E
|
||||||
|
ä \x84
|
||||||
|
Ö \x99
|
||||||
|
ö \x94
|
||||||
|
Ü \x9A
|
||||||
|
ü \x81
|
||||||
|
ß \xE0
|
||||||
|
*/
|
||||||
|
String temp = ifsh;
|
||||||
|
temp.replace("Ä", "\x8E");
|
||||||
|
temp.replace("ä", "\x84");
|
||||||
|
temp.replace("Ö", "\x99");
|
||||||
|
temp.replace("ö", "\x94");
|
||||||
|
temp.replace("Ü", "\x9A");
|
||||||
|
temp.replace("ü", "\x81");
|
||||||
|
temp.replace("ß", "\xE0");
|
||||||
|
|
||||||
|
ifsh = temp.c_str();
|
||||||
|
|
||||||
|
return Adafruit_GFX::print(ifsh);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#ifndef BROSE9323_H
|
||||||
|
#define BROSE9323_H
|
||||||
|
|
||||||
|
#include <Adafruit_GFX.h>
|
||||||
|
|
||||||
|
//#define FLIPDOT_PLCC_ADAPTER
|
||||||
|
// ESPSERIAL is set per target via build_flags in platformio.ini (-D ESPSERIAL for the ESP32)
|
||||||
|
|
||||||
|
const uint8_t _hannio_splash[] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x70,
|
||||||
|
0x3e, 0x00, 0x39, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x70, 0xff, 0x00, 0x39, 0xff, 0xff, 0xff, 0xff,
|
||||||
|
0xfe, 0x71, 0xf7, 0xc0, 0x39, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x73, 0xc1, 0xc0, 0x38, 0x1f, 0x87,
|
||||||
|
0xf0, 0x7f, 0x06, 0x73, 0x80, 0xe0, 0x38, 0x0f, 0x01, 0xc0, 0x3c, 0x02, 0x77, 0x7f, 0x60, 0x38,
|
||||||
|
0xc7, 0x78, 0xc6, 0x1c, 0x60, 0x77, 0x7f, 0x70, 0x39, 0xe3, 0xfc, 0xcf, 0x9c, 0xf8, 0x76, 0x00,
|
||||||
|
0x30, 0x39, 0xf3, 0xfc, 0xcf, 0x9c, 0xf8, 0x77, 0x7f, 0x70, 0x39, 0xf3, 0x80, 0xcf, 0x9c, 0xf8,
|
||||||
|
0x77, 0x7f, 0x60, 0x39, 0xf3, 0x00, 0xcf, 0x9c, 0xf8, 0x73, 0x80, 0xe0, 0x39, 0xf2, 0x3c, 0xcf,
|
||||||
|
0x9c, 0xf8, 0x73, 0xc1, 0xc0, 0x39, 0xf2, 0x7c, 0xcf, 0x9c, 0xf8, 0x71, 0xf7, 0xc0, 0x39, 0xf2,
|
||||||
|
0x3c, 0xcf, 0x9c, 0xf8, 0x70, 0xff, 0x80, 0x39, 0xf3, 0x00, 0xcf, 0x9c, 0xf8, 0x70, 0x3e, 0x00};
|
||||||
|
|
||||||
|
class BROSE9323 : public Adafruit_GFX
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
uint16_t _flip_time;
|
||||||
|
const uint8_t _panel_width;
|
||||||
|
const uint8_t _buffer_width;
|
||||||
|
const uint16_t _buffer_size;
|
||||||
|
uint8_t *_old_buffer = NULL;
|
||||||
|
uint8_t *_new_buffer = NULL;
|
||||||
|
bool _direct_mode = false;
|
||||||
|
|
||||||
|
#ifdef ESPSERIAL
|
||||||
|
Stream *stream;
|
||||||
|
#else
|
||||||
|
uint8_t _active_panel = 255;
|
||||||
|
uint8_t _active_col = 255;
|
||||||
|
uint8_t _active_row = 255;
|
||||||
|
bool _active_data;
|
||||||
|
|
||||||
|
#ifndef FLIPDOT_PLCC_ADAPTER
|
||||||
|
const uint8_t ENABLE = 7,
|
||||||
|
ADDR_0 = 3,
|
||||||
|
ADDR_1 = 5,
|
||||||
|
ADDR_2 = 6,
|
||||||
|
COL_DATA = 4,
|
||||||
|
COL_0 = 8,
|
||||||
|
COL_1 = 10,
|
||||||
|
COL_2 = 9,
|
||||||
|
COL_3 = 11,
|
||||||
|
COL_4 = 12,
|
||||||
|
ROW_0 = A3,
|
||||||
|
ROW_1 = A4,
|
||||||
|
ROW_2 = A2,
|
||||||
|
ROW_3 = A0,
|
||||||
|
ROW_4 = A1,
|
||||||
|
ROW_RESET = 13,
|
||||||
|
ROW_SET = A5;
|
||||||
|
#else
|
||||||
|
const uint8_t RELAY = 2,
|
||||||
|
ENABLE = 3,
|
||||||
|
ADDR_0 = 7,
|
||||||
|
ADDR_1 = 6,
|
||||||
|
ADDR_2 = 5,
|
||||||
|
COL_DATA = 4,
|
||||||
|
COL_0 = 12,
|
||||||
|
COL_1 = 11,
|
||||||
|
COL_2 = 10,
|
||||||
|
COL_3 = 9,
|
||||||
|
COL_4 = 8,
|
||||||
|
ROW_0 = 13, //1 A1 CHECK
|
||||||
|
ROW_1 = A0, //2 A2
|
||||||
|
ROW_2 = A1, //4 A3
|
||||||
|
ROW_3 = A2, //8 13 CHECK
|
||||||
|
ROW_4 = A3, //F A0
|
||||||
|
ROW_RESET = A4,
|
||||||
|
ROW_SET = A5;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void _selectColumn(uint8_t);
|
||||||
|
void _selectPanel(uint8_t);
|
||||||
|
void _selectRow(uint8_t);
|
||||||
|
void _setData(bool);
|
||||||
|
void _strobe(void);
|
||||||
|
#endif
|
||||||
|
public:
|
||||||
|
BROSE9323(uint8_t, uint8_t, uint8_t, uint16_t ft = 280);
|
||||||
|
#ifdef ESPSERIAL
|
||||||
|
void begin(Stream * = &Serial);
|
||||||
|
#endif
|
||||||
|
void begin(void);
|
||||||
|
void display(bool force = false);
|
||||||
|
void drawPixel(int16_t x, int16_t y, uint16_t color);
|
||||||
|
void fillScreen(uint16_t);
|
||||||
|
void setDirect(bool);
|
||||||
|
void setTiming(uint16_t);
|
||||||
|
#ifndef ESPSERIAL
|
||||||
|
void printBuffer(void);
|
||||||
|
#endif
|
||||||
|
size_t print(const char *ifsh);
|
||||||
|
};
|
||||||
|
#endif //BROSE9323_H
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# BROSE9323 driver
|
||||||
|
|
||||||
|
An `Adafruit_GFX` driver for Brose flipdot panels, shared by both firmware targets.
|
||||||
|
The constructor is `BROSE9323(width, height, panel_width, flip_time_us = 280)`. This
|
||||||
|
project uses `BROSE9323 display(84, 19, 28)`.
|
||||||
|
|
||||||
|
## Build modes
|
||||||
|
|
||||||
|
| Flag | Used by | Behaviour |
|
||||||
|
|------|---------|-----------|
|
||||||
|
| *(none)* | Nano | Drives the panel through GPIO. Keeps an old and a new frame buffer and flips only the dots that changed. |
|
||||||
|
| `-D ESPSERIAL` | ESP32 | Holds no buffer. `drawPixel`, `fillScreen`, `display` and `setTiming` are sent as serial commands to the Stream passed to `begin(&Serial)`. |
|
||||||
|
| `-D FLIPDOT_PLCC_ADAPTER` | Nano (optional) | Uses the pin mapping of the PLCC adapter board. |
|
||||||
|
|
||||||
|
The flags are set per environment in `platformio.ini` through `build_flags`. Don't
|
||||||
|
`#define` them in the header.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- In direct mode (`setDirect(true)`) every pixel is flipped right away, without a call to
|
||||||
|
`display()`.
|
||||||
|
- `print()` maps German umlauts (ÄäÖöÜüß) to the code page 437 glyphs used by the GFX
|
||||||
|
font.
|
||||||
|
- On an ATmega168 the old buffer is left out to save RAM. Every `display()` call then
|
||||||
|
flips all dots.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
; Flipdot display - one PlatformIO project, two firmware targets.
|
||||||
|
;
|
||||||
|
; nano Arduino Nano that drives the Brose flipdot panels directly
|
||||||
|
; esp32 ESP32 "brain" (WiFi, NTP clock, Home Assistant) - OTA upload
|
||||||
|
; esp32_usb Same ESP32 firmware, flashed over USB (first flash / recovery)
|
||||||
|
;
|
||||||
|
; Build all: pio run
|
||||||
|
; Build one: pio run -e nano
|
||||||
|
; Upload one: pio run -e nano -t upload
|
||||||
|
; Serial monitor: pio device monitor -e nano
|
||||||
|
;
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[platformio]
|
||||||
|
default_envs = nano, esp32
|
||||||
|
|
||||||
|
[env]
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
lib_deps =
|
||||||
|
adafruit/Adafruit GFX Library@^1.10.12
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; Arduino Nano - display driver
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
[env:nano]
|
||||||
|
platform = atmelavr
|
||||||
|
board = nanoatmega328
|
||||||
|
board_build.mcu = atmega328p
|
||||||
|
board_build.f_cpu = 16000000L
|
||||||
|
build_src_filter = +<nano/>
|
||||||
|
; Uncomment when using the PLCC adapter board (different pinout, see lib/BROSE9323/BROSE9323.h)
|
||||||
|
; build_flags = -D FLIPDOT_PLCC_ADAPTER
|
||||||
|
upload_port = COM10
|
||||||
|
monitor_port = COM10
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; ESP32 - controller, talks to the Nano over serial
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
[env:esp32]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32doit-devkit-v1
|
||||||
|
build_src_filter = +<esp32/>
|
||||||
|
; ESPSERIAL turns the BROSE9323 driver into a serial client: every draw call is
|
||||||
|
; sent to the Nano as a text command instead of toggling GPIO pins.
|
||||||
|
build_flags = -D ESPSERIAL
|
||||||
|
lib_deps =
|
||||||
|
${env.lib_deps}
|
||||||
|
https://github.com/robjen/GFX_fonts.git
|
||||||
|
dawidchyrzynski/home-assistant-integration@^2.1.0
|
||||||
|
knolleary/PubSubClient@^2.8.0
|
||||||
|
upload_protocol = espota
|
||||||
|
upload_port = 10.1.1.198
|
||||||
|
monitor_port = COM11
|
||||||
|
|
||||||
|
[env:esp32_usb]
|
||||||
|
extends = env:esp32
|
||||||
|
upload_protocol = esptool
|
||||||
|
upload_port = COM11
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# ESP32: controller
|
||||||
|
|
||||||
|
PlatformIO environments: `esp32` (OTA) and `esp32_usb` (USB)
|
||||||
|
|
||||||
|
The ESP32 decides what is shown on the display. It never touches the panel itself. The
|
||||||
|
BROSE9323 driver is built with `-D ESPSERIAL`, so every GFX call is sent over `Serial`
|
||||||
|
(115200 baud) to the Nano.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Contents |
|
||||||
|
|------|----------|
|
||||||
|
| `main.cpp` | setup, OTA, clock / power logic, Home Assistant callbacks |
|
||||||
|
| `libs.h` | includes and global objects (display, MQTT, WiFi, pins, timezone) |
|
||||||
|
| `bitmaps.h` | 1-bit bitmaps (Pikachu, thought bubble, Nintendo logo, ...) |
|
||||||
|
| `secrets.example.h` | template for `secrets.h` (WiFi and MQTT credentials, **gitignored**) |
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **OTA updates** through ArduinoOTA. Hostname: `FlipDotDisplay`.
|
||||||
|
- **OTA panic window:** after a crash or watchdog reset, the ESP32 runs only the OTA
|
||||||
|
handler for 15 seconds before it starts the main code. A firmware that crashes on boot
|
||||||
|
can still be replaced over the air.
|
||||||
|
- **NTP time** with timezone `CET-1CEST,M3.5.0,M10.5.0/3` (Europe/Amsterdam).
|
||||||
|
- **Home Assistant** through MQTT (`home-assistant-integration` library) with two switches:
|
||||||
|
- `FlipDotDisplayClock`: clock on, or the Nintendo logo when off
|
||||||
|
- `FlipDotDisplayPower`: turns the display off through the relay
|
||||||
|
- **Relay on GPIO 13** switches the display's power supply. `LOW` turns it on.
|
||||||
|
- **Status LED** (built-in) blinks fast when WiFi is down and slowly when connected. It is
|
||||||
|
off at night or when the display is powered off.
|
||||||
|
|
||||||
|
## Schedule
|
||||||
|
|
||||||
|
| Time | Display |
|
||||||
|
|------|---------|
|
||||||
|
| 07:00 – 17:00 | Pikachu and the clock, updated every minute |
|
||||||
|
| 17:00 – 22:00 | Yawning Pikachu and the clock |
|
||||||
|
| 22:00 – 23:30 | Clock drawn only on the quarter hour; power is off in between |
|
||||||
|
| night | Screen cleared, power off |
|
||||||
|
|
||||||
|
## Adding bitmaps
|
||||||
|
|
||||||
|
Convert an image with [image2cpp](https://javl.github.io/image2cpp/) to "Arduino code",
|
||||||
|
horizontal, 1 bit per pixel. Paste the array into `bitmaps.h` and draw it with
|
||||||
|
`display.drawBitmap(x, y, bitmap, w, h, 1)`. The display is 84 × 19 pixels.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// 'Nintendo logo', 84x19px
|
||||||
|
const unsigned char nintendo_bitmap_Nintendo_new[] PROGMEM = {
|
||||||
|
0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff,
|
||||||
|
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x80, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x67, 0x8e, 0x70, 0x00,
|
||||||
|
0x70, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x60, 0x67, 0x8e, 0x70, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1c,
|
||||||
|
0x00, 0x60, 0xc7, 0x8e, 0x07, 0x39, 0xfc, 0xfc, 0x73, 0x83, 0xdc, 0x7c, 0x30, 0xc7, 0x4e, 0x77,
|
||||||
|
0xfc, 0x71, 0xee, 0x7f, 0xc7, 0xfc, 0xfe, 0x30, 0xc7, 0x6e, 0x77, 0x8e, 0x73, 0xc7, 0x78, 0xef,
|
||||||
|
0x3d, 0xc7, 0x30, 0xc7, 0x2e, 0x77, 0x0e, 0x73, 0xff, 0x70, 0xef, 0x3d, 0xc7, 0x30, 0xc7, 0x3e,
|
||||||
|
0x77, 0x0e, 0x73, 0xc0, 0x70, 0xef, 0x3d, 0xc7, 0x30, 0xc7, 0x1e, 0x77, 0x0e, 0x73, 0xc7, 0x70,
|
||||||
|
0xef, 0x3d, 0xc7, 0x30, 0xc7, 0x0e, 0x77, 0x0e, 0x71, 0xee, 0x70, 0xe7, 0xfc, 0xfe, 0x30, 0x67,
|
||||||
|
0x0e, 0x77, 0x0e, 0x70, 0xfc, 0x70, 0xe3, 0xdc, 0x7c, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0,
|
||||||
|
0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x0f, 0xff, 0xff, 0xff, 0xff,
|
||||||
|
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc,
|
||||||
|
0x00};
|
||||||
|
|
||||||
|
// 'nintendo border', 84x19px
|
||||||
|
const unsigned char nintendo_bitmap_nintendo_border[] PROGMEM = {
|
||||||
|
0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff,
|
||||||
|
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x80, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x60, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x60, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xc0, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x30, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xc0, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x30, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x60,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x60, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0,
|
||||||
|
0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x0f, 0xff, 0xff, 0xff, 0xff,
|
||||||
|
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc,
|
||||||
|
0x00};
|
||||||
|
|
||||||
|
// 'panda', 17x18px
|
||||||
|
const unsigned char nintendo_bitmap_panda_inverted[] PROGMEM = {
|
||||||
|
0xfe, 0x7f, 0x80, 0xfc, 0x3c, 0x80, 0xfc, 0x38, 0x00, 0xdd, 0xfe, 0x00, 0xbb, 0xff, 0x80, 0x73,
|
||||||
|
0xff, 0x80, 0x77, 0x3f, 0x80, 0x76, 0x33, 0x00, 0x76, 0x73, 0x00, 0x33, 0xfb, 0x00, 0x31, 0x8e,
|
||||||
|
0x00, 0x90, 0xdc, 0x00, 0x80, 0xf0, 0x00, 0x80, 0x00, 0x00, 0xc4, 0x78, 0x80, 0xe4, 0x70, 0x80,
|
||||||
|
0xfe, 0x11, 0x80, 0xff, 0x1f, 0x80};
|
||||||
|
const unsigned char nintendo_bitmap_panda[] PROGMEM = {
|
||||||
|
0x01, 0x80, 0x00, 0x03, 0xc3, 0x00, 0x03, 0xc7, 0x80, 0x22, 0x01, 0x80, 0x44, 0x00, 0x00, 0x8c,
|
||||||
|
0x00, 0x00, 0x88, 0xc0, 0x00, 0x89, 0xcc, 0x80, 0x89, 0x8c, 0x80, 0xcc, 0x04, 0x80, 0xce, 0x71,
|
||||||
|
0x80, 0x6f, 0x23, 0x80, 0x7f, 0x0f, 0x80, 0x7f, 0xff, 0x80, 0x3b, 0x87, 0x00, 0x1b, 0x8f, 0x00,
|
||||||
|
0x01, 0xee, 0x00, 0x00, 0xe0, 0x00};
|
||||||
|
|
||||||
|
// 'yawning_pikachu', 35x19px
|
||||||
|
const unsigned char yawning_pikachu_bitmap_yawning_pikachu [] PROGMEM = {
|
||||||
|
0xff, 0xfc, 0x07, 0xff, 0xe0, 0xff, 0xe3, 0xf8, 0xff, 0xe0, 0xfe, 0x1f, 0xff, 0x0f, 0xe0, 0xf9,
|
||||||
|
0xff, 0xff, 0xf3, 0xe0, 0xe7, 0xff, 0xff, 0xfc, 0xe0, 0xdf, 0x7f, 0xff, 0xdf, 0x60, 0x9f, 0x73,
|
||||||
|
0xf9, 0xdf, 0x20, 0x0c, 0xcf, 0xbe, 0x66, 0x00, 0x02, 0xff, 0xff, 0xe8, 0x00, 0xfe, 0xff, 0x1f,
|
||||||
|
0xef, 0xe0, 0xfd, 0x9e, 0x0f, 0x37, 0xe0, 0xfd, 0x0e, 0xee, 0x17, 0xe0, 0xfd, 0x0e, 0xee, 0x17,
|
||||||
|
0xe0, 0xfd, 0x9e, 0xe3, 0x37, 0xe0, 0xfd, 0xff, 0x0d, 0xf7, 0xe0, 0xfe, 0xff, 0xee, 0xef, 0xe0,
|
||||||
|
0xfe, 0xff, 0xf7, 0x6f, 0xe0, 0xfe, 0x7f, 0xfb, 0x8f, 0xe0, 0xfe, 0xfe, 0x0b, 0xaf, 0xe0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 'yawning_pikachu_black_border', 35x19px
|
||||||
|
const unsigned char yawning_pikachu_bitmap_yawning_pikachu_black_border [] PROGMEM = {
|
||||||
|
0x00, 0x03, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0x00, 0x00, 0x01, 0xff, 0xff, 0xf0, 0x00, 0x07,
|
||||||
|
0xff, 0xff, 0xfc, 0x00, 0x1f, 0xff, 0xff, 0xff, 0x00, 0x3f, 0x7f, 0xff, 0xdf, 0x80, 0x7f, 0x73,
|
||||||
|
0xf9, 0xdf, 0xc0, 0xfe, 0xcf, 0xbe, 0x6f, 0xe0, 0xfd, 0xff, 0xff, 0xf7, 0xe0, 0x01, 0xff, 0x1f,
|
||||||
|
0xf0, 0x00, 0x03, 0x9e, 0x0f, 0x38, 0x00, 0x03, 0x0e, 0xee, 0x18, 0x00, 0x03, 0x0e, 0xee, 0x18,
|
||||||
|
0x00, 0x03, 0x9e, 0xe3, 0x38, 0x00, 0x03, 0xff, 0x0d, 0xf8, 0x00, 0x01, 0xff, 0xee, 0xf0, 0x00,
|
||||||
|
0x01, 0xff, 0xf7, 0x70, 0x00, 0x00, 0x7f, 0xfb, 0x80, 0x00, 0x01, 0xfe, 0x0b, 0xb0, 0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
// 'clock thought bubble', 46x19px
|
||||||
|
const unsigned char clock_thought_bubble_bitmap_clock_thought_bubble [] PROGMEM = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0x00, 0x10, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x80, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x20, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40,
|
||||||
|
0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x20, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x00, 0x00, 0x00, 0x80, 0x08, 0x3f,
|
||||||
|
0xff, 0xff, 0xff, 0x00, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x21, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00
|
||||||
|
};
|
||||||
|
|
||||||
|
// 'pikachu_black_border', 35x19px
|
||||||
|
const unsigned char pikachu_bitmap_pikachu_black_border [] PROGMEM = {
|
||||||
|
0x00, 0x03, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0x00, 0x00, 0x01, 0xff, 0xff, 0xf0, 0x00, 0x07,
|
||||||
|
0xff, 0xff, 0xfc, 0x00, 0x1f, 0xff, 0xff, 0xff, 0x00, 0x3f, 0x73, 0xf9, 0xdf, 0x80, 0x7f, 0x69,
|
||||||
|
0xf4, 0xdf, 0xc0, 0xfe, 0xe1, 0xf0, 0xef, 0xe0, 0xfd, 0xf3, 0xf9, 0xf7, 0xe0, 0x01, 0xff, 0xbf,
|
||||||
|
0xf0, 0x00, 0x03, 0x9f, 0xff, 0x38, 0x00, 0x03, 0x0f, 0xfe, 0x18, 0x00, 0x03, 0x0f, 0xfe, 0x18,
|
||||||
|
0x00, 0x03, 0x9d, 0xb7, 0x38, 0x00, 0x03, 0xfe, 0x4f, 0xf8, 0x00, 0x01, 0xff, 0xff, 0xf0, 0x00,
|
||||||
|
0x01, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x7f, 0xff, 0x80, 0x00, 0x01, 0xfe, 0x0f, 0xf0, 0x00
|
||||||
|
};
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include "BROSE9323.h"
|
||||||
|
#include "bitmaps.h"
|
||||||
|
|
||||||
|
// WiFi / MQTT credentials (gitignored, copy secrets.example.h -> secrets.h)
|
||||||
|
#if __has_include("secrets.h")
|
||||||
|
#include "secrets.h"
|
||||||
|
#else
|
||||||
|
#error "Missing src/esp32/secrets.h - copy src/esp32/secrets.example.h to secrets.h and fill it in"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// fixes
|
||||||
|
#include <SPI.h>
|
||||||
|
#include <Adafruit_I2CDevice.h>
|
||||||
|
// fonts
|
||||||
|
#include <Fonts/FreeSerif12pt7b.h>
|
||||||
|
#include <Fonts/FreeSerifItalic12pt7b.h>
|
||||||
|
// #include <Font2x5FixedMonoNum.h>
|
||||||
|
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <ESPmDNS.h>
|
||||||
|
#include <WiFiUdp.h>
|
||||||
|
#include <ArduinoOTA.h>
|
||||||
|
#include <WiFiMulti.h>
|
||||||
|
|
||||||
|
// wdt?
|
||||||
|
#include <esp_task_wdt.h>
|
||||||
|
#include <rom/rtc.h>
|
||||||
|
|
||||||
|
// hassio
|
||||||
|
#include <ArduinoHA.h>
|
||||||
|
|
||||||
|
byte mac[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28};
|
||||||
|
|
||||||
|
// EthernetClient client;
|
||||||
|
WiFiClient client;
|
||||||
|
HADevice device(mac, sizeof(mac));
|
||||||
|
HAMqtt mqtt(client, device);
|
||||||
|
|
||||||
|
HASwitch flipDotDisplay("FlipDotDisplayClock");
|
||||||
|
bool hassioDisplayStatus = true;
|
||||||
|
|
||||||
|
HASwitch powerSwitch("FlipDotDisplayPower");
|
||||||
|
bool power = true;
|
||||||
|
|
||||||
|
const int led1 = BUILTIN_LED; // Pin of the LED
|
||||||
|
const int relay = 13; // Pin of the relay
|
||||||
|
|
||||||
|
int hour = 0;
|
||||||
|
int minute = 0;
|
||||||
|
int second = 0;
|
||||||
|
|
||||||
|
//
|
||||||
|
WiFiMulti wifiMulti;
|
||||||
|
|
||||||
|
const char *ssid = WIFI_SSID;
|
||||||
|
const char *password = WIFI_PASSWORD;
|
||||||
|
|
||||||
|
BROSE9323 display(84, 19, 28);
|
||||||
|
|
||||||
|
uint32_t next_display;
|
||||||
|
|
||||||
|
// ntp
|
||||||
|
#define MYTZ "CET-1CEST,M3.5.0,M10.5.0/3"
|
||||||
|
const char *ntpServer = "pool.ntp.org";
|
||||||
|
const long gmtOffset_sec = 3600;
|
||||||
|
const int daylightOffset_sec = 3600;
|
||||||
|
// const int daylightOffset_sec = 0;
|
||||||
|
|
||||||
|
bool wifiStatus = false;
|
||||||
|
bool otaPanicMode = true;
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
#include "libs.h"
|
||||||
|
|
||||||
|
void toggleLED(void *parameter);
|
||||||
|
|
||||||
|
uint32_t power_off_delay;
|
||||||
|
int triggered_power_off = false;
|
||||||
|
int turn_hassio_power_off = false;
|
||||||
|
void turnPowerOff(uint32_t delay = 30000, bool _turn_hassio_power_off = false)
|
||||||
|
{
|
||||||
|
if(triggered_power_off == false)
|
||||||
|
{
|
||||||
|
// start new countdown
|
||||||
|
turn_hassio_power_off = _turn_hassio_power_off;
|
||||||
|
power_off_delay = millis() + delay;
|
||||||
|
|
||||||
|
triggered_power_off = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePower()
|
||||||
|
{
|
||||||
|
if(millis() > power_off_delay && triggered_power_off == true)
|
||||||
|
{
|
||||||
|
// finished countdown
|
||||||
|
triggered_power_off = false;
|
||||||
|
|
||||||
|
// OFF
|
||||||
|
digitalWrite(relay, HIGH);
|
||||||
|
|
||||||
|
if(turn_hassio_power_off)
|
||||||
|
{
|
||||||
|
power = false;
|
||||||
|
powerSwitch.setState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSwitchStateChanged(bool state, HASwitch *s)
|
||||||
|
{
|
||||||
|
hassioDisplayStatus = (state ? HIGH : LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPowerSwitchStateChanged(bool state, HASwitch *s)
|
||||||
|
{
|
||||||
|
power = (state ? HIGH : LOW);
|
||||||
|
|
||||||
|
if(power == false)
|
||||||
|
{
|
||||||
|
display.fillScreen(0);
|
||||||
|
display.display();
|
||||||
|
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
turnPowerOff(0);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_reset_reason(RESET_REASON reason)
|
||||||
|
{
|
||||||
|
switch (reason)
|
||||||
|
{
|
||||||
|
case POWERON_RESET:
|
||||||
|
Serial.println("POWERON_RESET");
|
||||||
|
break; /**<1, Vbat power on reset*/
|
||||||
|
case 3:
|
||||||
|
Serial.println("SW_RESET");
|
||||||
|
break; /**<3, Software reset digital core*/
|
||||||
|
case 4:
|
||||||
|
Serial.println("OWDT_RESET");
|
||||||
|
break; /**<4, Legacy watch dog reset digital core*/
|
||||||
|
case 5:
|
||||||
|
Serial.println("DEEPSLEEP_RESET");
|
||||||
|
break; /**<5, Deep Sleep reset digital core*/
|
||||||
|
case 6:
|
||||||
|
Serial.println("SDIO_RESET");
|
||||||
|
break; /**<6, Reset by SLC module, reset digital core*/
|
||||||
|
case 7:
|
||||||
|
Serial.println("TG0WDT_SYS_RESET");
|
||||||
|
break; /**<7, Timer Group0 Watch dog reset digital core*/
|
||||||
|
case 8:
|
||||||
|
Serial.println("TG1WDT_SYS_RESET");
|
||||||
|
break; /**<8, Timer Group1 Watch dog reset digital core*/
|
||||||
|
case 9:
|
||||||
|
Serial.println("RTCWDT_SYS_RESET");
|
||||||
|
break; /**<9, RTC Watch dog Reset digital core*/
|
||||||
|
case 10:
|
||||||
|
Serial.println("INTRUSION_RESET");
|
||||||
|
break; /**<10, Instrusion tested to reset CPU*/
|
||||||
|
case 11:
|
||||||
|
Serial.println("TGWDT_CPU_RESET");
|
||||||
|
break; /**<11, Time Group reset CPU*/
|
||||||
|
case SW_CPU_RESET:
|
||||||
|
Serial.println("SW_CPU_RESET");
|
||||||
|
break; /**<12, Software reset CPU*/
|
||||||
|
case 13:
|
||||||
|
Serial.println("RTCWDT_CPU_RESET");
|
||||||
|
break; /**<13, RTC Watch dog Reset CPU*/
|
||||||
|
case EXT_CPU_RESET:
|
||||||
|
Serial.println("EXT_CPU_RESET");
|
||||||
|
break; /**<14, for APP CPU, reseted by PRO CPU*/
|
||||||
|
case 15:
|
||||||
|
Serial.println("RTCWDT_BROWN_OUT_RESET");
|
||||||
|
break; /**<15, Reset when the vdd voltage is not stable*/
|
||||||
|
case 16:
|
||||||
|
Serial.println("RTCWDT_RTC_RESET");
|
||||||
|
break; /**<16, RTC Watch dog reset digital core and rtc module*/
|
||||||
|
default:
|
||||||
|
Serial.println("NO_MEAN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkResetStatus()
|
||||||
|
{
|
||||||
|
Serial.println("CPU0 reset reason: ");
|
||||||
|
RESET_REASON reason0 = rtc_get_reset_reason(0);
|
||||||
|
print_reset_reason(reason0);
|
||||||
|
|
||||||
|
// if(reason0 == SW_CPU_RESET)
|
||||||
|
// return true;
|
||||||
|
|
||||||
|
Serial.println("CPU1 reset reason: ");
|
||||||
|
RESET_REASON reason1 = rtc_get_reset_reason(1);
|
||||||
|
print_reset_reason(reason1);
|
||||||
|
|
||||||
|
// if(reason1 == SW_CPU_RESET)
|
||||||
|
// return true;
|
||||||
|
|
||||||
|
if (reason0 != POWERON_RESET && reason1 != EXT_CPU_RESET)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
otaPanicMode = true;
|
||||||
|
pinMode(led1, OUTPUT);
|
||||||
|
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
WiFi.setHostname("FlipDotDisplay");
|
||||||
|
wifiMulti.addAP(ssid, password);
|
||||||
|
|
||||||
|
Serial.println("Booting");
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
while (WiFi.waitForConnectResult() != WL_CONNECTED)
|
||||||
|
{
|
||||||
|
Serial.println("Connection Failed! Rebooting...");
|
||||||
|
delay(5000);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
ArduinoOTA
|
||||||
|
.onStart([]()
|
||||||
|
{
|
||||||
|
String type;
|
||||||
|
if (ArduinoOTA.getCommand() == U_FLASH)
|
||||||
|
type = "sketch";
|
||||||
|
else // U_SPIFFS
|
||||||
|
type = "filesystem";
|
||||||
|
|
||||||
|
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
|
||||||
|
// USE_SERIAL.write(0x0C); //CLEAR SCREEN CMD
|
||||||
|
// Serial.println("Start updating " + type);
|
||||||
|
|
||||||
|
// if (!otaPanicMode)
|
||||||
|
// {
|
||||||
|
// display.begin(&Serial);
|
||||||
|
// display.fillScreen(0);
|
||||||
|
// display.setTextSize(1);
|
||||||
|
// display.setCursor(0, 0);
|
||||||
|
// display.print("ota starting!");
|
||||||
|
// display.display();
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
.onEnd([]()
|
||||||
|
{
|
||||||
|
// USE_SERIAL.write(0x0C); //CLEAR SCREEN CMD
|
||||||
|
// Serial.println("\nEnd");
|
||||||
|
})
|
||||||
|
.onProgress([](unsigned int progress, unsigned int total)
|
||||||
|
{
|
||||||
|
// USE_SERIAL.write(0x0C); //CLEAR SCREEN CMD
|
||||||
|
// USE_SERIAL.printf("Progress: %u%%\r", (progress / (total / 100)));
|
||||||
|
// if (!otaPanicMode)
|
||||||
|
// {
|
||||||
|
// display.begin(&Serial);
|
||||||
|
// display.fillScreen(0);
|
||||||
|
// display.setTextSize(1);
|
||||||
|
// display.setCursor(0, 0);
|
||||||
|
// display.printf("ota: %u%%\r", (progress / (total / 100)));
|
||||||
|
// display.display();
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
.onError([](ota_error_t error)
|
||||||
|
{
|
||||||
|
// // USE_SERIAL.write(0x0C); //CLEAR SCREEN CMD
|
||||||
|
// Serial.printf("Error[%u]: ", error);
|
||||||
|
// if (error == OTA_AUTH_ERROR)
|
||||||
|
// Serial.println("Auth Failed");
|
||||||
|
// else if (error == OTA_BEGIN_ERROR)
|
||||||
|
// Serial.println("Begin Failed");
|
||||||
|
// else if (error == OTA_CONNECT_ERROR)
|
||||||
|
// Serial.println("Connect Failed");
|
||||||
|
// else if (error == OTA_RECEIVE_ERROR)
|
||||||
|
// Serial.println("Receive Failed");
|
||||||
|
// else if (error == OTA_END_ERROR)
|
||||||
|
// Serial.println("End Failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
ArduinoOTA.begin();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
ArduinoOTA.handle();
|
||||||
|
|
||||||
|
digitalWrite(led1, HIGH);
|
||||||
|
|
||||||
|
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||||
|
|
||||||
|
// Turn the LED off
|
||||||
|
digitalWrite(led1, LOW);
|
||||||
|
|
||||||
|
vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||||
|
|
||||||
|
} while (checkResetStatus() && millis() < 1000 * 15);
|
||||||
|
otaPanicMode = false;
|
||||||
|
|
||||||
|
pinMode(relay, OUTPUT);
|
||||||
|
digitalWrite(relay, LOW);
|
||||||
|
|
||||||
|
delay(4000);
|
||||||
|
|
||||||
|
display.begin(&Serial);
|
||||||
|
// display.setTiming(200);
|
||||||
|
// display.setTiming(180);
|
||||||
|
// display.setTiming(220); // works best so far
|
||||||
|
display.setTiming(225);
|
||||||
|
|
||||||
|
// delay(4000);
|
||||||
|
display.fillScreen(0);
|
||||||
|
display.setCursor(0, 0);
|
||||||
|
display.setTextSize(1);
|
||||||
|
display.println("Connecting...");
|
||||||
|
display.display();
|
||||||
|
|
||||||
|
// display.setFont(&FreeSerif12pt7b);
|
||||||
|
// display.setFont(&FreeSerifItalic12pt7b);
|
||||||
|
|
||||||
|
//init and get the time
|
||||||
|
// configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
|
||||||
|
configTzTime(MYTZ, ntpServer);
|
||||||
|
|
||||||
|
xTaskCreate(
|
||||||
|
toggleLED, // Function that should be called
|
||||||
|
"Toggle LED", // Name of the task (for debugging)
|
||||||
|
1000, // Stack size (bytes)
|
||||||
|
NULL, // Parameter to pass
|
||||||
|
1, // Task priority
|
||||||
|
NULL // Task handle
|
||||||
|
);
|
||||||
|
|
||||||
|
next_display = millis() / 1000 * 1000;
|
||||||
|
|
||||||
|
// // TEST CODE
|
||||||
|
// do{
|
||||||
|
// ArduinoOTA.handle();
|
||||||
|
|
||||||
|
// digitalWrite(led1, HIGH);
|
||||||
|
|
||||||
|
// vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||||
|
|
||||||
|
// // Turn the LED off
|
||||||
|
// digitalWrite(led1, LOW);
|
||||||
|
|
||||||
|
// vTaskDelay(50 / portTICK_PERIOD_MS);
|
||||||
|
|
||||||
|
// }while(true);
|
||||||
|
|
||||||
|
// hassio
|
||||||
|
// you don't need to verify return status
|
||||||
|
// client.begin(mac);
|
||||||
|
|
||||||
|
// set device's details (optional)
|
||||||
|
device.setName("FlipDotDisplay");
|
||||||
|
device.setSoftwareVersion("1.0.0");
|
||||||
|
|
||||||
|
// set icon (optional)
|
||||||
|
flipDotDisplay.setIcon("mdi:lightbulb");
|
||||||
|
flipDotDisplay.setName("FlipDot Display Clock");
|
||||||
|
|
||||||
|
// handle switch state
|
||||||
|
flipDotDisplay.onCommand(onSwitchStateChanged);
|
||||||
|
|
||||||
|
|
||||||
|
powerSwitch.setIcon("mdi:lightbulb");
|
||||||
|
powerSwitch.setName("FlipDot Display Power");
|
||||||
|
powerSwitch.onCommand(onPowerSwitchStateChanged);
|
||||||
|
|
||||||
|
mqtt.begin(BROKER_ADDR, MQTT_USER, MQTT_PASSWORD);
|
||||||
|
|
||||||
|
display.fillScreen(0);
|
||||||
|
display.display();
|
||||||
|
|
||||||
|
hour = millis() / 1000 / 60 / 60;
|
||||||
|
minute = (millis() / 1000 / 60) % 60;
|
||||||
|
second = (millis() / 1000 % 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleLED(void *parameter)
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{ // infinite loop
|
||||||
|
if((power == false) || (!(hour >= 7 && hour < 22)))
|
||||||
|
{
|
||||||
|
// OFF
|
||||||
|
digitalWrite(led1, LOW);
|
||||||
|
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn the LED on
|
||||||
|
digitalWrite(led1, HIGH);
|
||||||
|
|
||||||
|
if (wifiStatus)
|
||||||
|
{
|
||||||
|
// Pause the task for 500ms
|
||||||
|
vTaskDelay(500 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||||
|
|
||||||
|
// Turn the LED off
|
||||||
|
digitalWrite(led1, LOW);
|
||||||
|
|
||||||
|
if (wifiStatus)
|
||||||
|
{
|
||||||
|
// Pause the task again for 500ms
|
||||||
|
vTaskDelay(500 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#define DEG2RAD 0.0174532925
|
||||||
|
void drawRotatedBitmap(int16_t x, int16_t y, const uint8_t *bitmap, int16_t w, int16_t h, bool color, uint16_t angle)
|
||||||
|
{
|
||||||
|
int16_t newx, newy;
|
||||||
|
uint8_t data = 0;
|
||||||
|
|
||||||
|
float cosa = cos(angle * DEG2RAD), sina = sin(angle * DEG2RAD);
|
||||||
|
|
||||||
|
x = x - ((w * cosa / 2) - (h * sina / 2));
|
||||||
|
y = y - ((h * cosa / 2) + (w * sina / 2));
|
||||||
|
|
||||||
|
for (int16_t j = 0; j < h; j++)
|
||||||
|
{
|
||||||
|
for (int16_t i = 0; i < w; i++)
|
||||||
|
{
|
||||||
|
if ((j * w + i) & 7)
|
||||||
|
data <<= 1;
|
||||||
|
else
|
||||||
|
data = pgm_read_byte(bitmap++);
|
||||||
|
|
||||||
|
newx = 0.5 + x + ((i * cosa) - (j * sina));
|
||||||
|
newy = 0.5 + y + ((j * cosa) + (i * sina));
|
||||||
|
|
||||||
|
if (data & 0x80)
|
||||||
|
display.drawPixel(newx, newy, color);
|
||||||
|
//else display.drawPixel(newx, newy, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void turnPowerOn(bool overide = false)
|
||||||
|
{
|
||||||
|
if(power == true || overide == true)
|
||||||
|
{
|
||||||
|
// ON
|
||||||
|
digitalWrite(relay, LOW);
|
||||||
|
delay(3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
ArduinoOTA.handle();
|
||||||
|
mqtt.loop();
|
||||||
|
updatePower();
|
||||||
|
|
||||||
|
wifiStatus = WiFi.waitForConnectResult() == WL_CONNECTED;
|
||||||
|
|
||||||
|
if(next_display < millis())
|
||||||
|
{
|
||||||
|
if (hassioDisplayStatus)
|
||||||
|
// if (false)
|
||||||
|
{
|
||||||
|
char t[20];
|
||||||
|
char timeStringBuff[50]; //50 chars should be enough
|
||||||
|
|
||||||
|
struct tm timeinfo;
|
||||||
|
if (!getLocalTime(&timeinfo))
|
||||||
|
{
|
||||||
|
// printFromStart("Failed to get time");
|
||||||
|
// return;
|
||||||
|
|
||||||
|
// sprintf(t, "%02d:%02d:%02d\0", millis() / 1000 / 60 / 60, (millis() / 1000 / 60) % 60, (millis() / 1000 % 60));
|
||||||
|
// sprintf(t, "%02d%02d%02d\0", millis() / 1000 / 60 / 60, (millis() / 1000 / 60) % 60, (millis() / 1000 % 60));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// const char *format = "%H:%M:%S";
|
||||||
|
const char *format = "%H:%M";
|
||||||
|
|
||||||
|
// strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
|
||||||
|
strftime(timeStringBuff, sizeof(timeStringBuff), format, &timeinfo);
|
||||||
|
String asString(timeStringBuff);
|
||||||
|
|
||||||
|
hour = timeinfo.tm_hour;
|
||||||
|
minute = timeinfo.tm_min;
|
||||||
|
second = timeinfo.tm_sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// display.setFont(&FreeSerif12pt7b);
|
||||||
|
|
||||||
|
// if ((millis() / 1000 % 60) <= 30)
|
||||||
|
// display.setFont(&FreeSerif12pt7b);
|
||||||
|
// else
|
||||||
|
// display.setFont(&FreeSerifItalic12pt7b);
|
||||||
|
|
||||||
|
if(hour <= 7 || (hour >= 23 && minute > 30))
|
||||||
|
{
|
||||||
|
display.fillScreen(0);
|
||||||
|
display.display();
|
||||||
|
|
||||||
|
turnPowerOff();
|
||||||
|
}
|
||||||
|
// show time between 7 and 23 with updates every minute, and after that; every 15 minutes.
|
||||||
|
else if((hour >= 7 && hour < 22) || (minute % 15 == 0))
|
||||||
|
// else if((hour >= 7 && hour < 19) || (minute % 5 == 0))
|
||||||
|
// else if(true)
|
||||||
|
{
|
||||||
|
turnPowerOn();
|
||||||
|
|
||||||
|
int xoffset = 3;
|
||||||
|
|
||||||
|
display.fillScreen(0);
|
||||||
|
// display.setTextSize(2);
|
||||||
|
display.setTextSize(1);
|
||||||
|
// display.setCursor(7, 3);
|
||||||
|
display.setCursor(xoffset + 0, 19 - 2);
|
||||||
|
display.setTextWrap(false);
|
||||||
|
// display.print(t);
|
||||||
|
|
||||||
|
// if(minute % 2 == 0)
|
||||||
|
if(hour >= 17 || hour <= 6)
|
||||||
|
display.drawBitmap(xoffset + 0, 0, yawning_pikachu_bitmap_yawning_pikachu_black_border, 35, 19, 1);
|
||||||
|
else
|
||||||
|
display.drawBitmap(xoffset + 0, 0, pikachu_bitmap_pikachu_black_border, 35, 19, 1);
|
||||||
|
|
||||||
|
display.drawBitmap(xoffset + 35, 0, clock_thought_bubble_bitmap_clock_thought_bubble, 46, 19, 1);
|
||||||
|
|
||||||
|
display.setCursor(xoffset + 43, (19/2) - 5);
|
||||||
|
display.print(timeStringBuff);
|
||||||
|
|
||||||
|
display.display();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
turnPowerOff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
display.fillScreen(0);
|
||||||
|
display.drawBitmap(0, 0, nintendo_bitmap_Nintendo_new, 84, 19, 1);
|
||||||
|
|
||||||
|
// display.drawBitmap(0, 0, nintendo_bitmap_panda_inverted, 17, 18, 1);
|
||||||
|
// display.drawBitmap(18, 0, nintendo_bitmap_panda_inverted, 17, 18, 1);
|
||||||
|
// display.drawBitmap(18*2, 0, nintendo_bitmap_panda_inverted, 17, 18, 1);
|
||||||
|
// display.drawBitmap(18*3, 0, nintendo_bitmap_panda_inverted, 17, 18, 1);
|
||||||
|
// display.drawBitmap(18*4, 0, nintendo_bitmap_panda_inverted, 17, 18, 1);
|
||||||
|
|
||||||
|
// drawRotatedBitmap(0, 0, nintendo_bitmap_Nintendo_new, 17, 18, 1, 90);
|
||||||
|
|
||||||
|
// Text inside border.
|
||||||
|
// display.drawBitmap(0, 0, nintendo_bitmap_nintendo_border, 84, 19, 1);
|
||||||
|
// display.setFont();
|
||||||
|
// display.setTextSize(1);
|
||||||
|
// display.setTextWrap(false);
|
||||||
|
// display.setCursor(5, 4);
|
||||||
|
// display.print("Nintendo");
|
||||||
|
|
||||||
|
// display.setFont();
|
||||||
|
// // display.setTextSize(2);
|
||||||
|
// display.setTextSize(1);
|
||||||
|
// display.setCursor(0, 0);
|
||||||
|
// display.setTextWrap(true);
|
||||||
|
// // display.print(t);
|
||||||
|
// display.print("Carla & Jan Kragt");
|
||||||
|
|
||||||
|
display.display();
|
||||||
|
|
||||||
|
turnPowerOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
next_display += 1000;
|
||||||
|
// next_display += 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copy this file to secrets.h and fill in your own values.
|
||||||
|
// secrets.h is gitignored so credentials never end up in the repository.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define WIFI_SSID "your-wifi-ssid"
|
||||||
|
#define WIFI_PASSWORD "your-wifi-password"
|
||||||
|
|
||||||
|
// Home Assistant MQTT broker
|
||||||
|
#define BROKER_ADDR IPAddress(192, 168, 1, 10)
|
||||||
|
#define MQTT_USER "mqtt-user"
|
||||||
|
#define MQTT_PASSWORD "mqtt-password"
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Arduino Nano: flipdot driver
|
||||||
|
|
||||||
|
PlatformIO environment: `nano`
|
||||||
|
|
||||||
|
The Nano is connected directly to the Brose panel electronics. It keeps an 84 × 19 frame
|
||||||
|
buffer and takes drawing commands over serial (115200 baud). It has no clock or network
|
||||||
|
logic.
|
||||||
|
|
||||||
|
## Behaviour
|
||||||
|
|
||||||
|
- **Boot:** the Nano flips the whole display on and then off, shows the splash bitmap and
|
||||||
|
prints the time a full redraw took.
|
||||||
|
- **`display()`:** compares the new buffer with the last one that was shown and strobes
|
||||||
|
only the dots that changed.
|
||||||
|
- **Every 2 minutes** (`REFRESH_TIME`) it forces a full redraw to fix dots that got stuck.
|
||||||
|
- **Flip pulse length** (µs) is set with the `T` command and stored in EEPROM, so it is
|
||||||
|
kept after a reboot.
|
||||||
|
|
||||||
|
## Serial protocol
|
||||||
|
|
||||||
|
Each command is one ASCII line. Coordinates are 2-digit **hex**. Color is `0` (black) or
|
||||||
|
`1` (yellow). The ESP32 driver (`ESPSERIAL`) only uses `s`, `S`, `F`, `D` and `T`.
|
||||||
|
|
||||||
|
| Cmd | Format | Meaning |
|
||||||
|
|-----|--------|---------|
|
||||||
|
| `s` | `sXXYYc` | Set pixel in the buffer |
|
||||||
|
| `S` | `SXXYYc` | Set pixel and flip it right away (direct mode) |
|
||||||
|
| `F` | `Fc` | Fill the whole buffer |
|
||||||
|
| `D` | `D` | Push the buffer to the display (changed dots only) |
|
||||||
|
| `P` | `PXXYYcsTEXT` | Print text at (XX,YY), color `c`, size `s`. `\n` in TEXT starts a new line. |
|
||||||
|
| `B` | `BXXYYWWHHcHEX...` | Draw a bitmap. WW must be a multiple of 4. Pixel data is one hex digit per 4 pixels. |
|
||||||
|
| `R` | `R` | Print the buffer as ASCII art on the serial port |
|
||||||
|
| `T` | `Tnnn` | Set the flip pulse length in µs (decimal), stored in EEPROM |
|
||||||
|
|
||||||
|
Example (what the ESP32 sends for one pixel and a refresh):
|
||||||
|
|
||||||
|
```
|
||||||
|
s0a051
|
||||||
|
D
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wiring (default pinout)
|
||||||
|
|
||||||
|
The pins are defined in [lib/BROSE9323/BROSE9323.h](../../lib/BROSE9323/BROSE9323.h).
|
||||||
|
If you use the PLCC adapter board, build with `-D FLIPDOT_PLCC_ADAPTER` to switch to its
|
||||||
|
pinout.
|
||||||
|
|
||||||
|
| Signal | Nano pin |
|
||||||
|
|--------|----------|
|
||||||
|
| ENABLE | D7 |
|
||||||
|
| ADDR_0 / 1 / 2 (panel select) | D3 / D5 / D6 |
|
||||||
|
| COL_DATA | D4 |
|
||||||
|
| COL_0 … COL_4 | D8, D10, D9, D11, D12 |
|
||||||
|
| ROW_0 … ROW_4 | A3, A4, A2, A0, A1 |
|
||||||
|
| ROW_RESET | D13 |
|
||||||
|
| ROW_SET | A5 |
|
||||||
|
| Serial RX | D0 (from ESP32 TX) |
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include "BROSE9323.h"
|
||||||
|
// fixes
|
||||||
|
#include <SPI.h>
|
||||||
|
#include <Adafruit_I2CDevice.h>
|
||||||
|
|
||||||
|
#include <EEPROM.h>
|
||||||
|
|
||||||
|
BROSE9323 display(84, 19, 28, 200);
|
||||||
|
|
||||||
|
uint32_t refresh_at;
|
||||||
|
const uint32_t REFRESH_TIME = 120000L; // 600000L // 10 min
|
||||||
|
|
||||||
|
uint16_t _flip_time = 200;
|
||||||
|
unsigned int address = 0;
|
||||||
|
unsigned int address2 = 1;
|
||||||
|
|
||||||
|
//#define DEBUG
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.println("Hello World!");
|
||||||
|
|
||||||
|
uint16_t read1 = EEPROM.read(address);
|
||||||
|
uint16_t read2 = EEPROM.read(address2);
|
||||||
|
|
||||||
|
if(read1 == read2)
|
||||||
|
_flip_time = read1;
|
||||||
|
|
||||||
|
display.setTiming(_flip_time);
|
||||||
|
|
||||||
|
display.begin();
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
display.fillScreen(1);
|
||||||
|
display.display();
|
||||||
|
display.fillScreen(0);
|
||||||
|
uint32_t t1 = millis();
|
||||||
|
display.display();
|
||||||
|
uint32_t t2 = millis();
|
||||||
|
Serial.println(t2 - t1);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t hex2dec(char c)
|
||||||
|
{
|
||||||
|
if (c >= '0' && c <= '9')
|
||||||
|
return c - '0';
|
||||||
|
if (c >= 'A' && c <= 'F')
|
||||||
|
return c - 'A' + 10;
|
||||||
|
if (c >= 'a' && c <= 'f')
|
||||||
|
return c - 'a' + 10;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
if (Serial.available())
|
||||||
|
{
|
||||||
|
char cmd = Serial.read();
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
// Ignore CR
|
||||||
|
case 0x0A:
|
||||||
|
break;
|
||||||
|
// Draw Bitmap (must be n x 8 wide)
|
||||||
|
case 'b':
|
||||||
|
case 'B': //BxxyywwhhcBITMAP (xx/ww = 8 bit hex x position/width; yy/hh = 8 bit y position/height; c = 0/1 color; BITMAP = bytes as hex)
|
||||||
|
{
|
||||||
|
// May lead to buffer overflow, you may need to segment the bitmap into multiple frames
|
||||||
|
// Or just use the ser pixel function
|
||||||
|
while (Serial.available() < 9)
|
||||||
|
{
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
uint8_t x = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
uint8_t y = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
uint8_t w = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
if (w % 4)
|
||||||
|
{
|
||||||
|
Serial.println("ERR: Invalid bitmap width (not dividable by 4)");
|
||||||
|
Serial.flush();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint8_t h = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
if (h == 0)
|
||||||
|
{
|
||||||
|
Serial.println("ERR: Invalid bitmap height (0)");
|
||||||
|
Serial.flush();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint8_t c = Serial.read() - '0';
|
||||||
|
uint8_t b = 0;
|
||||||
|
for (uint8_t _y = 0; _y < h; _y++)
|
||||||
|
{
|
||||||
|
for (uint8_t _x = 0; _x < w; _x++)
|
||||||
|
{
|
||||||
|
if ((_x & 0x3) == 0)
|
||||||
|
{
|
||||||
|
if (!Serial.available())
|
||||||
|
{
|
||||||
|
Serial.println("WARN: OUTOFMEM");
|
||||||
|
}
|
||||||
|
while (!Serial.available())
|
||||||
|
{
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
b = hex2dec(Serial.read());
|
||||||
|
}
|
||||||
|
if (b & (1 << (0x3 - (_x & 0x3))))
|
||||||
|
{
|
||||||
|
display.drawPixel(x + _x, y + _y, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Display
|
||||||
|
case 'd':
|
||||||
|
case 'D': //D
|
||||||
|
{
|
||||||
|
display.display();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Fill Screen
|
||||||
|
case 'f':
|
||||||
|
case 'F': //Fc (c = 0/1 color)
|
||||||
|
{
|
||||||
|
while (Serial.available() <= 2)
|
||||||
|
{
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
display.fillScreen(Serial.read() - '0');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Print Text
|
||||||
|
case 'p':
|
||||||
|
case 'P': // PxxyycsTEXT (xx = 8 bit hex x position; yy = 8 bit y position; c = 0/1 color; s = 1/2 size; TEXT = string, newline = escaped \\n)
|
||||||
|
{
|
||||||
|
delay(5);
|
||||||
|
while (Serial.available() <= 6)
|
||||||
|
{
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
uint8_t x = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
uint8_t y = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
uint8_t c = Serial.read() - '0';
|
||||||
|
display.setCursor(x, y);
|
||||||
|
display.setTextColor(c, !c);
|
||||||
|
display.setTextSize(Serial.read() - '0');
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (!Serial.available())
|
||||||
|
{
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
if (Serial.peek() == '\n')
|
||||||
|
break;
|
||||||
|
if (Serial.peek() == '\r')
|
||||||
|
{
|
||||||
|
Serial.read();
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
char c = Serial.read();
|
||||||
|
if (c == '\\' && Serial.peek() == 'n')
|
||||||
|
{
|
||||||
|
display.println();
|
||||||
|
Serial.read();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
display.write(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Return buffer (show pixels on console)
|
||||||
|
case 'r':
|
||||||
|
case 'R': //R
|
||||||
|
{
|
||||||
|
display.printBuffer();
|
||||||
|
Serial.flush();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Set Pixel
|
||||||
|
case 'S':
|
||||||
|
display.setDirect(true);
|
||||||
|
case 's': // Sxxyyc (xx = 8 bit hex x position; y = 8 bit y position; c = 0/1 color)
|
||||||
|
{
|
||||||
|
while (Serial.available() <= 6)
|
||||||
|
{
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
uint8_t x = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
uint8_t y = (hex2dec(Serial.read()) << 4) | hex2dec(Serial.read());
|
||||||
|
bool color = hex2dec(Serial.read());
|
||||||
|
display.drawPixel(x, y, color);
|
||||||
|
display.setDirect(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Set Timing
|
||||||
|
case 'T':
|
||||||
|
case 't': // Tttt (ttt = decimal timing in us)
|
||||||
|
{
|
||||||
|
while (Serial.available() <= 6)
|
||||||
|
{
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
uint16_t t = atoi(Serial.readStringUntil('\n').c_str());
|
||||||
|
|
||||||
|
EEPROM.update(address, t);
|
||||||
|
EEPROM.update(address2, t);
|
||||||
|
|
||||||
|
display.setTiming(t);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
#ifdef DEBUG
|
||||||
|
Serial.print("0x");
|
||||||
|
Serial.print(cmd >> 4, HEX);
|
||||||
|
Serial.print(cmd & 3, HEX);
|
||||||
|
Serial.println(" - command not known.");
|
||||||
|
Serial.flush();
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Refresh content every REFRESH_TIME (by default 2 min)
|
||||||
|
if (refresh_at < millis())
|
||||||
|
{
|
||||||
|
refresh_at = millis() + REFRESH_TIME;
|
||||||
|
display.display(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user