Compare commits
5 Commits
4e5396f231
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| db4c76a110 | |||
| ab2c1b34b4 | |||
| a96f378c9c | |||
| ebf01531c5 | |||
| e9a12f66a9 |
+5
-4
@@ -43,6 +43,7 @@ Button handling uses the [OneButton](https://github.com/mathertel/OneButton) lib
|
||||
|
||||
- **Single tap** → next show
|
||||
- **Double tap** → previous show
|
||||
- **Triple tap** → lights off; any subsequent tap or hold resumes the current show
|
||||
- **Long press** → reset to show 0
|
||||
|
||||
The key timing values (configured via `setClickMs` and `setPressMs`):
|
||||
@@ -104,12 +105,12 @@ Edit `config.h`:
|
||||
// WS2812B (default)
|
||||
#define LED_TYPE WS2812B
|
||||
#define COLOR_ORDER GRB
|
||||
#define LED_DATA_PIN 6
|
||||
#define LED_DATA_PIN 5
|
||||
|
||||
// APA102 / Dotstar (two-wire)
|
||||
// #define LED_TYPE APA102
|
||||
// #define COLOR_ORDER BGR
|
||||
// #define LED_DATA_PIN 6
|
||||
// #define LED_DATA_PIN 5
|
||||
// #define LED_CLOCK_PIN 7
|
||||
```
|
||||
|
||||
@@ -129,8 +130,8 @@ static CRGB leds_a[NUM_LEDS];
|
||||
static CRGB leds_b[NUM_LEDS];
|
||||
|
||||
void leds_begin() {
|
||||
FastLED.addLeds<LED_TYPE, 6, COLOR_ORDER>(leds_a, NUM_LEDS);
|
||||
FastLED.addLeds<LED_TYPE, 5, COLOR_ORDER>(leds_b, NUM_LEDS);
|
||||
FastLED.addLeds<LED_TYPE, 5, COLOR_ORDER>(leds_a, NUM_LEDS);
|
||||
FastLED.addLeds<LED_TYPE, 6, COLOR_ORDER>(leds_b, NUM_LEDS);
|
||||
FastLED.setBrightness(MAX_BRIGHTNESS);
|
||||
FastLED.clear(true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# Getting Started
|
||||
|
||||
Step-by-step setup guide for Windows and macOS.
|
||||
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
---
|
||||
|
||||
## What you're installing
|
||||
|
||||
| Tool | Why |
|
||||
|------|-----|
|
||||
| Python 3 | Converts your `.txt` show files into Arduino code |
|
||||
| Arduino CLI | Compiles and uploads the sketch to the Arduino |
|
||||
| FastLED library | LED strip control (installed via Arduino CLI) |
|
||||
| OneButton library | Button gesture detection (installed via Arduino CLI) |
|
||||
|
||||
On **Windows** you'll also install `make` so the one-liner `make upload` command works.
|
||||
On **macOS** `make` is already available once you install the developer tools.
|
||||
|
||||
---
|
||||
|
||||
## Windows
|
||||
|
||||
### 1. Install Python
|
||||
|
||||
1. Go to **https://www.python.org/downloads/** and download the latest Python 3 installer.
|
||||
2. Run the installer.
|
||||
**Important:** on the first screen, tick **"Add python.exe to PATH"** before clicking *Install Now*.
|
||||
3. Open **Command Prompt** (search "cmd") and verify:
|
||||
```
|
||||
python --version
|
||||
```
|
||||
You should see something like `Python 3.12.3`.
|
||||
|
||||
### 2. Install Arduino CLI
|
||||
|
||||
Open **Command Prompt** and run:
|
||||
```
|
||||
winget install ArduinoSA.ArduinoCLI
|
||||
```
|
||||
|
||||
Close and reopen Command Prompt, then verify:
|
||||
```
|
||||
arduino-cli version
|
||||
```
|
||||
|
||||
> If `winget` is not available, download the Windows `.zip` from
|
||||
> https://arduino.github.io/arduino-cli/latest/installation/
|
||||
> extract it, and add the folder to your PATH.
|
||||
|
||||
### 3. Install the AVR board package
|
||||
|
||||
```
|
||||
arduino-cli core update-index
|
||||
arduino-cli core install arduino:avr
|
||||
```
|
||||
|
||||
This downloads everything needed to compile for Arduino Uno/Nano. It may take a minute.
|
||||
|
||||
### 4. Install the required libraries
|
||||
|
||||
```
|
||||
arduino-cli lib install "FastLED"
|
||||
arduino-cli lib install "OneButton"
|
||||
```
|
||||
|
||||
### 5. Install make
|
||||
|
||||
The `make upload` command requires `make`. Install it with:
|
||||
```
|
||||
winget install GnuWin32.Make
|
||||
```
|
||||
|
||||
After installation, add `C:\Program Files (x86)\GnuWin32\bin` to your PATH:
|
||||
|
||||
1. Search for **"Edit the system environment variables"**.
|
||||
2. Click **Environment Variables**.
|
||||
3. Under *User variables*, select **Path** and click *Edit*.
|
||||
4. Click *New* and paste `C:\Program Files (x86)\GnuWin32\bin`.
|
||||
5. Click *OK* on all dialogs.
|
||||
|
||||
Close and reopen your terminal, then verify:
|
||||
```
|
||||
make --version
|
||||
```
|
||||
|
||||
> **Alternative — skip make entirely:**
|
||||
> If you'd rather not install `make`, you can run the three steps manually in Command Prompt from the project folder:
|
||||
> ```
|
||||
> python converter\convert_all.py
|
||||
> arduino-cli compile --fqbn arduino:avr:uno --build-path build arduino/cosplay_lights
|
||||
> arduino-cli upload --fqbn arduino:avr:uno --port COM3 --input-dir build arduino/cosplay_lights
|
||||
> ```
|
||||
> Replace `COM3` with your actual port (see step 6 below).
|
||||
|
||||
### 6. Find your Arduino port (Windows)
|
||||
|
||||
Plug in the Arduino via USB, then run:
|
||||
```
|
||||
arduino-cli board list
|
||||
```
|
||||
|
||||
Look for a line showing `arduino:avr:uno` — the port will be something like `COM3` or `COM4`.
|
||||
|
||||
### 7. Upload
|
||||
|
||||
From the project folder:
|
||||
```
|
||||
make upload
|
||||
```
|
||||
|
||||
Or with a specific port:
|
||||
```
|
||||
make upload PORT=COM3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## macOS
|
||||
|
||||
### 1. Install developer tools
|
||||
|
||||
Open **Terminal** (Spotlight → "Terminal") and run:
|
||||
```
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
Click *Install* in the dialog that appears. This gives you `git`, `make`, and other command-line tools. Skip this step if you already have them.
|
||||
|
||||
### 2. Install Python
|
||||
|
||||
macOS ships with an older Python, so install a current version.
|
||||
|
||||
**Option A — using Homebrew (recommended):**
|
||||
|
||||
If you don't have Homebrew, install it first:
|
||||
```
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
|
||||
Then install Python:
|
||||
```
|
||||
brew install python
|
||||
```
|
||||
|
||||
**Option B — installer:**
|
||||
Download from **https://www.python.org/downloads/** and run the `.pkg`.
|
||||
|
||||
Verify:
|
||||
```
|
||||
python3 --version
|
||||
```
|
||||
|
||||
### 3. Install Arduino CLI
|
||||
|
||||
**Option A — Homebrew:**
|
||||
```
|
||||
brew install arduino-cli
|
||||
```
|
||||
|
||||
**Option B — installer script:**
|
||||
```
|
||||
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
|
||||
```
|
||||
Then move the binary somewhere on your PATH, e.g. `sudo mv bin/arduino-cli /usr/local/bin/`.
|
||||
|
||||
Verify:
|
||||
```
|
||||
arduino-cli version
|
||||
```
|
||||
|
||||
### 4. Install the AVR board package
|
||||
|
||||
```
|
||||
arduino-cli core update-index
|
||||
arduino-cli core install arduino:avr
|
||||
```
|
||||
|
||||
### 5. Install the required libraries
|
||||
|
||||
```
|
||||
arduino-cli lib install "FastLED"
|
||||
arduino-cli lib install "OneButton"
|
||||
```
|
||||
|
||||
### 6. Find your Arduino port (macOS)
|
||||
|
||||
Plug in the Arduino via USB, then run:
|
||||
```
|
||||
arduino-cli board list
|
||||
```
|
||||
|
||||
The port will look like `/dev/cu.usbmodem14101` or `/dev/cu.usbserial-*`.
|
||||
|
||||
### 7. Upload
|
||||
|
||||
From the project folder:
|
||||
```
|
||||
make upload
|
||||
```
|
||||
|
||||
Or with a specific port:
|
||||
```
|
||||
make upload PORT=/dev/cu.usbserial-14101
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## You're set up
|
||||
|
||||
Once the upload finishes, the lights start immediately. Use the button to cycle through shows.
|
||||
See [README.md](README.md) for how to write your own show files and configure the hardware.
|
||||
@@ -19,7 +19,7 @@ PORT ?= $(shell arduino-cli board list 2>/dev/null | awk '/arduino:avr:uno/{prin
|
||||
|
||||
# ---- Targets -----------------------------------------------------------
|
||||
|
||||
.PHONY: all shows build upload port clean
|
||||
.PHONY: all shows simulate build upload port clean
|
||||
|
||||
all: build
|
||||
|
||||
@@ -27,6 +27,10 @@ all: build
|
||||
shows:
|
||||
python converter/convert_all.py
|
||||
|
||||
## Preview shows in the terminal without hardware.
|
||||
simulate:
|
||||
python converter/simulate.py
|
||||
|
||||
## Compile the sketch.
|
||||
build:
|
||||
arduino-cli compile --fqbn $(FQBN) --build-path $(BUILD_DIR) $(SKETCH)
|
||||
@@ -40,7 +44,7 @@ upload: shows build
|
||||
echo ""; \
|
||||
exit 1; \
|
||||
fi
|
||||
arduino-cli upload --fqbn $(FQBN) --port $(PORT) $(SKETCH)
|
||||
arduino-cli upload --fqbn $(FQBN) --port $(PORT) --input-dir $(BUILD_DIR) $(SKETCH)
|
||||
@echo ""
|
||||
@echo " Uploaded to $(PORT). The show starts immediately."
|
||||
|
||||
|
||||
@@ -26,10 +26,11 @@ SPDX-License-Identifier: BSD-2-Clause
|
||||
### LED strip
|
||||
|
||||
```
|
||||
Arduino WS2812B strip
|
||||
─────── ─────────────
|
||||
GND ──────────────────── GND
|
||||
D6 ──── [470Ω] ──────── DIN (data in)
|
||||
Arduino WS2812B strip wire colors
|
||||
─────── ──────────────────────────
|
||||
5V ──────────────────── red (VCC)
|
||||
GND ──────────────────── white (GND)
|
||||
D5 ──── [470Ω] ──────── green (DIN — data in)
|
||||
```
|
||||
|
||||
Power the strip separately — see the power warning below.
|
||||
@@ -51,6 +52,7 @@ Uses the internal pull-up resistor. No external resistor needed. Pin 2 can be ch
|
||||
|-------|--------|
|
||||
| 1 tap | Next show |
|
||||
| 2 taps | Previous show |
|
||||
| 3 taps | Lights off (any tap or hold resumes) |
|
||||
| Hold (~0.8s) | Reset to show 0 (blue breath) |
|
||||
|
||||
### Power warning — read this
|
||||
@@ -70,6 +72,8 @@ A 60-LED strip at full white = **3.6A** — far more than the Arduino's 5V pin c
|
||||
|
||||
## Quick start
|
||||
|
||||
> **New to this?** See [GETTING_STARTED.md](GETTING_STARTED.md) for step-by-step instructions on installing Python, Arduino CLI, and all dependencies on Windows or macOS.
|
||||
|
||||
### 1. Install required libraries
|
||||
|
||||
Arduino IDE: **Sketch → Include Library → Manage Libraries**, then install both:
|
||||
@@ -80,7 +84,7 @@ Arduino IDE: **Sketch → Include Library → Manage Libraries**, then install b
|
||||
### 2. Configure your hardware
|
||||
|
||||
Edit `arduino/cosplay_lights/config.h`:
|
||||
- `LED_DATA_PIN` — pin connected to the strip's DIN (default: 6)
|
||||
- `LED_DATA_PIN` — pin connected to the strip's DIN (default: 5)
|
||||
- `BUTTON_PIN` — pin the button is wired to (default: 2)
|
||||
- `NUM_LEDS` — total LED count on your strip (default: 60)
|
||||
- `MAX_BRIGHTNESS` — global brightness cap (default: 150)
|
||||
@@ -185,6 +189,7 @@ Amirine_Cosplay_Lights/
|
||||
│ └── shows/ — .txt show files (edit these)
|
||||
├── Makefile — build, upload, and show conversion targets
|
||||
├── README.md — this file
|
||||
├── GETTING_STARTED.md — first-time setup for Windows and macOS
|
||||
├── DETAILS.md — architecture and modification guide
|
||||
├── PLAN.md — original project brief
|
||||
└── LICENSE — BSD 2-Clause
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
// -- Wiring --------------------------------------------------
|
||||
// Pin on the Arduino connected to the strip's DIN (data-in).
|
||||
#define LED_DATA_PIN 6
|
||||
#define LED_DATA_PIN 5
|
||||
|
||||
// Clock pin — only needed for APA102/SK9822 strips.
|
||||
// #define LED_CLOCK_PIN 7
|
||||
@@ -37,11 +37,20 @@
|
||||
// Uses INPUT_PULLUP — no external resistor needed.
|
||||
// 1 tap → next show
|
||||
// 2 taps → previous show
|
||||
// 3 taps → lights off (any tap/hold resumes)
|
||||
// hold → reset to show 0 (blue breath)
|
||||
#define BUTTON_PIN 2
|
||||
|
||||
// -- Active pattern ------------------------------------------
|
||||
// Controls how the current color is mapped to the LEDs.
|
||||
// Only PATTERN_SOLID is included; see DETAILS.md for adding more.
|
||||
// See DETAILS.md for adding more patterns.
|
||||
#define PATTERN_SOLID 0
|
||||
#define ACTIVE_PATTERN PATTERN_SOLID
|
||||
|
||||
// -- Sparkle tuning ------------------------------------------
|
||||
// Applied to shows that have the SHOW_FLAG_SPARKLE flag set.
|
||||
// SPARKLE_CHANCE : 0–255 probability of a new sparkle each frame (~60fps).
|
||||
// 80 ≈ 31%, giving roughly 2–3 simultaneous sparkles on 60 LEDs.
|
||||
// SPARKLE_FRAMES : how many frames each sparkle stays white (1 frame ≈ 16ms).
|
||||
#define SPARKLE_CHANCE 80
|
||||
#define SPARKLE_FRAMES 6
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* A single button navigates between shows:
|
||||
* 1 tap — next show
|
||||
* 2 taps — previous show
|
||||
* hold — reset to show 0 (blue breath)
|
||||
* 3 taps — lights off (any tap or hold resumes)
|
||||
* hold — reset to show 0 (red heartbeat)
|
||||
*
|
||||
* To add or update shows:
|
||||
* 1. Add or edit a .txt file in converter/shows/
|
||||
@@ -28,8 +29,10 @@ static ShowDef s_show; // PROGMEM cache for the active sh
|
||||
static uint16_t s_step_index = 0;
|
||||
static uint32_t s_step_start = 0;
|
||||
static CRGB s_from_color = CRGB::Black;
|
||||
static bool s_lights_off = false;
|
||||
|
||||
static void load_show(uint8_t index) {
|
||||
s_lights_off = false;
|
||||
s_show_index = index;
|
||||
s_show = read_show_def(&SHOWS[index]);
|
||||
s_step_index = 0;
|
||||
@@ -76,6 +79,7 @@ void setup() {
|
||||
s_button.setPressMs(800);
|
||||
s_button.attachClick([]() { load_show((s_show_index + 1) % SHOW_COUNT); });
|
||||
s_button.attachDoubleClick([]() { load_show((s_show_index + SHOW_COUNT - 1) % SHOW_COUNT); });
|
||||
s_button.attachMultiClick([]() { if (s_button.getNumberClicks() == 3) { s_lights_off = true; leds_apply_color(CRGB::Black, false); leds_show(); } });
|
||||
s_button.attachLongPressStart([]() { load_show(0); });
|
||||
}
|
||||
|
||||
@@ -87,11 +91,14 @@ void loop() {
|
||||
if (now - s_last_frame < 16) return;
|
||||
s_last_frame = now;
|
||||
|
||||
if (s_lights_off) return;
|
||||
|
||||
Step step = read_step(&s_show.steps[s_step_index]);
|
||||
CRGB to = CRGB(step.r, step.g, step.b);
|
||||
uint8_t t8 = step_progress(step, now);
|
||||
|
||||
leds_apply_color(blend(s_from_color, to, t8));
|
||||
bool sparkle = (s_show.flags & SHOW_FLAG_SPARKLE) != 0;
|
||||
leds_apply_color(blend(s_from_color, to, t8), sparkle);
|
||||
leds_show();
|
||||
|
||||
if (t8 == 255) advance_step(to, now);
|
||||
|
||||
@@ -10,14 +10,21 @@ void leds_begin() {
|
||||
FastLED.clear(true); // clear + show: strip starts fully off
|
||||
}
|
||||
|
||||
void leds_apply_color(CRGB color) {
|
||||
void leds_apply_color(CRGB color, bool sparkle) {
|
||||
#if ACTIVE_PATTERN == PATTERN_SOLID
|
||||
// All LEDs show the same color.
|
||||
fill_solid(leds, NUM_LEDS, color);
|
||||
|
||||
// To add a new pattern, add a new #define in config.h and a new branch here.
|
||||
// The pattern receives 'leds', 'NUM_LEDS', and the blended 'color' for this frame.
|
||||
#endif
|
||||
if (!sparkle) return;
|
||||
static uint8_t sparkle_timer[NUM_LEDS];
|
||||
if (random8() < SPARKLE_CHANCE) {
|
||||
sparkle_timer[random8(NUM_LEDS)] = SPARKLE_FRAMES;
|
||||
}
|
||||
for (uint8_t i = 0; i < NUM_LEDS; i++) {
|
||||
if (sparkle_timer[i] > 0) {
|
||||
leds[i] = CRGB::White;
|
||||
sparkle_timer[i]--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void leds_show() {
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
void leds_begin();
|
||||
|
||||
// Apply 'color' to all LEDs using the pattern defined in config.h (ACTIVE_PATTERN).
|
||||
// If 'sparkle' is true, randomly flash individual LEDs white on top of the base fill.
|
||||
// Call leds_show() afterwards to push the update to the physical strip.
|
||||
void leds_apply_color(CRGB color);
|
||||
void leds_apply_color(CRGB color, bool sparkle = false);
|
||||
|
||||
// Flush the LED buffer to the physical strip. Call once per frame after leds_apply_color().
|
||||
void leds_show();
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
#define SHOW_LOOP 0 // repeat the show indefinitely
|
||||
#define SHOW_SINGLE 1 // play once, then auto-advance to the next show
|
||||
|
||||
// Show flags (bitfield for ShowDef.flags).
|
||||
#define SHOW_FLAG_SPARKLE 0x01 // overlay random white sparkles on this show
|
||||
|
||||
// One step in a lightshow: a target color and the time to reach it.
|
||||
struct Step {
|
||||
uint8_t r, g, b; // Target RGB color (0–255 each)
|
||||
@@ -27,6 +30,7 @@ struct ShowDef {
|
||||
const Step* steps; // Pointer to a PROGMEM Step array
|
||||
uint16_t length; // Number of steps in the array
|
||||
uint8_t mode; // SHOW_LOOP or SHOW_SINGLE
|
||||
uint8_t flags; // Bitfield: see SHOW_FLAG_* above
|
||||
};
|
||||
|
||||
// Read one Step from PROGMEM into a regular struct.
|
||||
@@ -47,6 +51,6 @@ inline ShowDef read_show_def(const ShowDef* ptr) {
|
||||
sd.steps = (const Step*)pgm_read_word(&ptr->steps);
|
||||
sd.length = pgm_read_word(&ptr->length);
|
||||
sd.mode = pgm_read_byte(&ptr->mode);
|
||||
sd.flags = pgm_read_byte(&ptr->flags);
|
||||
return sd;
|
||||
}
|
||||
`
|
||||
}
|
||||
+6
-7
@@ -1,24 +1,23 @@
|
||||
// Generated by convert_all.py from: 001_heartbeat.txt
|
||||
// Generated by convert_all.py from: 001_heartbeat_red.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_001_HEARTBEAT[] PROGMEM = {
|
||||
const Step SHOW_001_HEARTBEAT_RED[] PROGMEM = {
|
||||
{ 5, 0, 0, 0}, // #050000, 0ms
|
||||
{209, 14, 41, 120}, // #D10E29, 120ms
|
||||
{ 48, 0, 0, 100}, // #300000, 100ms
|
||||
{209, 14, 41, 100}, // #D10E29, 100ms
|
||||
{ 5, 0, 0, 700}, // #050000, 700ms
|
||||
{ 5, 0, 0, 600}, // #050000, 600ms
|
||||
{209, 14, 41, 120}, // #D10E29, 120ms
|
||||
{ 48, 0, 0, 100}, // #300000, 100ms
|
||||
{209, 14, 41, 100}, // #D10E29, 100ms
|
||||
{ 5, 0, 0, 700}, // #050000, 700ms
|
||||
{ 5, 0, 0, 600}, // #050000, 600ms
|
||||
{209, 14, 41, 120}, // #D10E29, 120ms
|
||||
{ 48, 0, 0, 100}, // #300000, 100ms
|
||||
{209, 14, 41, 100}, // #D10E29, 100ms
|
||||
{ 5, 0, 0, 700}, // #050000, 700ms
|
||||
{153, 40, 58, 2000}, // #99283A, 2000ms
|
||||
{ 5, 0, 0, 600}, // #050000, 600ms
|
||||
};
|
||||
const uint16_t SHOW_001_HEARTBEAT_LENGTH = 14;
|
||||
const uint16_t SHOW_001_HEARTBEAT_RED_LENGTH = 13;
|
||||
@@ -1,13 +0,0 @@
|
||||
// Generated by convert_all.py from: 002_breath_red.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_002_BREATH_RED[] PROGMEM = {
|
||||
{153, 40, 58, 0}, // #99283A, 0ms
|
||||
{217, 30, 30, 2500}, // #D91E1E, 2500ms
|
||||
{153, 40, 58, 2500}, // #99283A, 2500ms
|
||||
};
|
||||
const uint16_t SHOW_002_BREATH_RED_LENGTH = 3;
|
||||
@@ -0,0 +1,12 @@
|
||||
// Generated by convert_all.py from: 002_heartbeat_breathe.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_002_HEARTBEAT_BREATHE[] PROGMEM = {
|
||||
{209, 14, 41, 3600}, // #D10E29, 3600ms
|
||||
{ 5, 0, 0, 3600}, // #050000, 3600ms
|
||||
};
|
||||
const uint16_t SHOW_002_HEARTBEAT_BREATHE_LENGTH = 2;
|
||||
+3
-3
@@ -1,15 +1,15 @@
|
||||
// Generated by convert_all.py from: blue_breath.txt
|
||||
// Generated by convert_all.py from: 003_blue_sparkle.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_BLUE_BREATH[] PROGMEM = {
|
||||
const Step SHOW_003_BLUE_SPARKLE[] PROGMEM = {
|
||||
{ 5, 0, 24, 0}, // #050018, 0ms
|
||||
{ 32, 32, 255, 2500}, // #2020FF, 2500ms
|
||||
{ 32, 32, 255, 400}, // #2020FF, 400ms
|
||||
{ 5, 0, 24, 2500}, // #050018, 2500ms
|
||||
{ 5, 0, 24, 800}, // #050018, 800ms
|
||||
};
|
||||
const uint16_t SHOW_BLUE_BREATH_LENGTH = 5;
|
||||
const uint16_t SHOW_003_BLUE_SPARKLE_LENGTH = 5;
|
||||
@@ -1,12 +0,0 @@
|
||||
// Generated by convert_all.py from: 003_solid_red.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_003_SOLID_RED[] PROGMEM = {
|
||||
{153, 40, 58, 0}, // #99283A, 0ms
|
||||
{153, 40, 58, 30000}, // #99283A, 30000ms
|
||||
};
|
||||
const uint16_t SHOW_003_SOLID_RED_LENGTH = 2;
|
||||
@@ -0,0 +1,13 @@
|
||||
// Generated by convert_all.py from: 004_pulse_yellow.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_004_PULSE_YELLOW[] PROGMEM = {
|
||||
{ 32, 32, 0, 0}, // #202000, 0ms
|
||||
{255, 255, 0, 1200}, // #FFFF00, 1200ms
|
||||
{ 32, 32, 0, 1200}, // #202000, 1200ms
|
||||
};
|
||||
const uint16_t SHOW_004_PULSE_YELLOW_LENGTH = 3;
|
||||
@@ -0,0 +1,12 @@
|
||||
// Generated by convert_all.py from: 005_green_static.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_005_GREEN_STATIC[] PROGMEM = {
|
||||
{ 0, 204, 0, 0}, // #00CC00, 0ms
|
||||
{ 0, 204, 0, 5000}, // #00CC00, 5000ms
|
||||
};
|
||||
const uint16_t SHOW_005_GREEN_STATIC_LENGTH = 2;
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
// Generated by convert_all.py from: example_party.txt
|
||||
// Generated by convert_all.py from: 006_party.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_EXAMPLE_PARTY[] PROGMEM = {
|
||||
const Step SHOW_006_PARTY[] PROGMEM = {
|
||||
{255, 0, 0, 0}, // #FF0000, 0ms
|
||||
{255, 0, 0, 150}, // #FF0000, 150ms
|
||||
{255, 136, 0, 0}, // #FF8800, 0ms
|
||||
@@ -24,4 +24,4 @@ const Step SHOW_EXAMPLE_PARTY[] PROGMEM = {
|
||||
{255, 255, 255, 150}, // #FFFFFF, 150ms
|
||||
{ 0, 0, 0, 500}, // #000000, 500ms
|
||||
};
|
||||
const uint16_t SHOW_EXAMPLE_PARTY_LENGTH = 17;
|
||||
const uint16_t SHOW_006_PARTY_LENGTH = 17;
|
||||
@@ -1,16 +0,0 @@
|
||||
// Generated by convert_all.py from: example_fade.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_EXAMPLE_FADE[] PROGMEM = {
|
||||
{255, 0, 0, 0}, // #FF0000, 0ms
|
||||
{ 0, 255, 0, 3000}, // #00FF00, 3000ms
|
||||
{ 0, 0, 255, 0}, // #0000FF, 0ms
|
||||
{255, 0, 255, 2000}, // #FF00FF, 2000ms
|
||||
{255, 128, 0, 1000}, // #FF8000, 1000ms
|
||||
{ 0, 0, 0, 5000}, // #000000, 5000ms
|
||||
};
|
||||
const uint16_t SHOW_EXAMPLE_FADE_LENGTH = 6;
|
||||
@@ -1,13 +0,0 @@
|
||||
// Generated by convert_all.py from: example_pulse.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_EXAMPLE_PULSE[] PROGMEM = {
|
||||
{ 32, 0, 0, 0}, // #200000, 0ms
|
||||
{255, 0, 0, 1200}, // #FF0000, 1200ms
|
||||
{ 32, 0, 0, 1200}, // #200000, 1200ms
|
||||
};
|
||||
const uint16_t SHOW_EXAMPLE_PULSE_LENGTH = 3;
|
||||
@@ -1,10 +1,10 @@
|
||||
// =====================================================================
|
||||
// shows.h — Master show index.
|
||||
// Generated by: make shows (converter/convert_all.py)
|
||||
// Do not edit manually — add .txt files to converter/shows/ instead.
|
||||
// Do not edit manually — add NNN_<name>.txt files to converter/shows/ instead.
|
||||
// =====================================================================
|
||||
//
|
||||
// Show 0 is always the home/reset show (blue breath).
|
||||
// Show 0 is the lowest-numbered .txt file (home/reset show).
|
||||
// Holding the button resets back to show 0.
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
@@ -13,23 +13,21 @@
|
||||
#include "lightshow_format.h"
|
||||
|
||||
// ---- Individual show data ----------------------------------------------
|
||||
#include "show_blue_breath.h"
|
||||
#include "show_001_heartbeat.h"
|
||||
#include "show_002_breath_red.h"
|
||||
#include "show_003_solid_red.h"
|
||||
#include "show_example_fade.h"
|
||||
#include "show_example_party.h"
|
||||
#include "show_example_pulse.h"
|
||||
#include "show_001_heartbeat_red.h"
|
||||
#include "show_002_heartbeat_breathe.h"
|
||||
#include "show_003_blue_sparkle.h"
|
||||
#include "show_004_pulse_yellow.h"
|
||||
#include "show_005_green_static.h"
|
||||
#include "show_006_party.h"
|
||||
|
||||
// ---- Show index (PROGMEM) ----------------------------------------------
|
||||
const ShowDef SHOWS[] PROGMEM = {
|
||||
{SHOW_BLUE_BREATH, SHOW_BLUE_BREATH_LENGTH, SHOW_LOOP}, // 0 — home show
|
||||
{SHOW_001_HEARTBEAT, SHOW_001_HEARTBEAT_LENGTH, SHOW_SINGLE}, // 1
|
||||
{SHOW_002_BREATH_RED, SHOW_002_BREATH_RED_LENGTH, SHOW_LOOP}, // 2
|
||||
{SHOW_003_SOLID_RED, SHOW_003_SOLID_RED_LENGTH, SHOW_LOOP}, // 3
|
||||
{SHOW_EXAMPLE_FADE, SHOW_EXAMPLE_FADE_LENGTH, SHOW_LOOP}, // 4
|
||||
{SHOW_EXAMPLE_PARTY, SHOW_EXAMPLE_PARTY_LENGTH, SHOW_LOOP}, // 5
|
||||
{SHOW_EXAMPLE_PULSE, SHOW_EXAMPLE_PULSE_LENGTH, SHOW_LOOP}, // 6
|
||||
{SHOW_001_HEARTBEAT_RED, SHOW_001_HEARTBEAT_RED_LENGTH, SHOW_SINGLE, 0}, // 0 — home show
|
||||
{SHOW_002_HEARTBEAT_BREATHE, SHOW_002_HEARTBEAT_BREATHE_LENGTH, SHOW_LOOP, 0}, // 1
|
||||
{SHOW_003_BLUE_SPARKLE, SHOW_003_BLUE_SPARKLE_LENGTH, SHOW_LOOP, SHOW_FLAG_SPARKLE}, // 2
|
||||
{SHOW_004_PULSE_YELLOW, SHOW_004_PULSE_YELLOW_LENGTH, SHOW_LOOP, 0}, // 3
|
||||
{SHOW_005_GREEN_STATIC, SHOW_005_GREEN_STATIC_LENGTH, SHOW_LOOP, 0}, // 4
|
||||
{SHOW_006_PARTY, SHOW_006_PARTY_LENGTH, SHOW_SINGLE, 0}, // 5
|
||||
};
|
||||
|
||||
const uint8_t SHOW_COUNT = 7;
|
||||
const uint8_t SHOW_COUNT = 6;
|
||||
|
||||
+34
-32
@@ -10,8 +10,12 @@ Usage:
|
||||
python converter/convert_all.py
|
||||
(or via Makefile: make shows)
|
||||
|
||||
Show 0 is always 'blue_breath' — the home/reset show.
|
||||
All other shows are sorted alphabetically and follow after it.
|
||||
Show files must be named NNN_<name>.txt (e.g. 001_heartbeat_red.txt).
|
||||
They run in numeric order; show 0 (the home/reset show) is the lowest-numbered file.
|
||||
|
||||
Show file directives (in comment lines):
|
||||
// mode: single — play once, then advance to the next show (default: loop)
|
||||
// flags: sparkle — overlay random white sparkles on this show
|
||||
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
"""
|
||||
@@ -27,9 +31,6 @@ ROOT_DIR = SCRIPT_DIR.parent
|
||||
SHOWS_DIR = SCRIPT_DIR / "shows"
|
||||
SKETCH_DIR = ROOT_DIR / "arduino" / "cosplay_lights"
|
||||
|
||||
# The special first show — always placed at index 0.
|
||||
HOME_SHOW = "blue_breath"
|
||||
|
||||
# Regex to match a valid step line: #RRGGBB, duration_ms
|
||||
STEP_PATTERN = re.compile(r'^\s*#([0-9A-Fa-f]{6})\s*,\s*(\d+)')
|
||||
|
||||
@@ -46,21 +47,29 @@ def hex_to_rgb(hex_str: str) -> tuple[int, int, int]:
|
||||
return int(hex_str[0:2], 16), int(hex_str[2:4], 16), int(hex_str[4:6], 16)
|
||||
|
||||
|
||||
def parse_show_file(filepath: Path) -> tuple[list[tuple[int, int, int, int]], str]:
|
||||
def parse_show_file(filepath: Path) -> tuple[list[tuple[int, int, int, int]], str, int]:
|
||||
"""
|
||||
Parse a .txt show file in one pass. Returns (steps, mode_constant).
|
||||
Parse a .txt show file in one pass. Returns (steps, mode_constant, flags).
|
||||
steps: list of (r, g, b, duration_ms) tuples
|
||||
mode_constant: 'SHOW_LOOP' or 'SHOW_SINGLE' (default SHOW_LOOP if not set)
|
||||
flags: integer bitmask (0x01 = sparkle, matches SHOW_FLAG_SPARKLE in lightshow_format.h)
|
||||
Raises ValueError on malformed lines or empty files.
|
||||
"""
|
||||
steps = []
|
||||
mode = "SHOW_LOOP"
|
||||
flags = 0
|
||||
with open(filepath) as f:
|
||||
for lineno, raw_line in enumerate(f, 1):
|
||||
m = re.match(r'^\s*//\s*mode:\s*(\w+)', raw_line, re.IGNORECASE)
|
||||
if m:
|
||||
if re.match(r'^\s*//\s*mode:\s*(\w+)', raw_line, re.IGNORECASE):
|
||||
m = re.match(r'^\s*//\s*mode:\s*(\w+)', raw_line, re.IGNORECASE)
|
||||
mode = "SHOW_SINGLE" if m.group(1).lower() == "single" else "SHOW_LOOP"
|
||||
continue
|
||||
mf = re.match(r'^\s*//\s*flags:\s*(.+)', raw_line, re.IGNORECASE)
|
||||
if mf:
|
||||
flag_tokens = [t.strip().lower() for t in mf.group(1).split(",")]
|
||||
if "sparkle" in flag_tokens:
|
||||
flags |= 0x01
|
||||
continue
|
||||
line = raw_line.split("//")[0].strip()
|
||||
if not line:
|
||||
continue
|
||||
@@ -75,7 +84,7 @@ def parse_show_file(filepath: Path) -> tuple[list[tuple[int, int, int, int]], st
|
||||
steps.append((r, g, b, duration))
|
||||
if not steps:
|
||||
raise ValueError(f"{filepath.name}: file contains no steps.")
|
||||
return steps, mode
|
||||
return steps, mode, flags
|
||||
|
||||
|
||||
def render_show_header(steps: list, source_name: str, symbol: str) -> str:
|
||||
@@ -101,23 +110,23 @@ def render_show_header(steps: list, source_name: str, symbol: str) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_shows_index(ordered: list[tuple[str, str]]) -> str:
|
||||
"""Render the master shows.h index file. ordered = [(stem, mode_constant), ...]"""
|
||||
includes = "\n".join(f'#include "show_{stem}.h"' for stem, _ in ordered)
|
||||
def render_shows_index(ordered: list[tuple[str, str, int]]) -> str:
|
||||
"""Render the master shows.h index file. ordered = [(stem, mode_constant, flags), ...]"""
|
||||
includes = "\n".join(f'#include "show_{stem}.h"' for stem, _, __ in ordered)
|
||||
entries = "\n".join(
|
||||
f" {{{filename_to_symbol(s)}, {filename_to_symbol(s)}_LENGTH, {mode}}},"
|
||||
f" {{{filename_to_symbol(s)}, {filename_to_symbol(s)}_LENGTH, {mode}, {'SHOW_FLAG_SPARKLE' if flags & 0x01 else '0'}}},"
|
||||
+ (" // 0 — home show" if i == 0 else f" // {i}")
|
||||
for i, (s, mode) in enumerate(ordered)
|
||||
for i, (s, mode, flags) in enumerate(ordered)
|
||||
)
|
||||
count = len(ordered)
|
||||
return f"""\
|
||||
// =====================================================================
|
||||
// shows.h — Master show index.
|
||||
// Generated by: make shows (converter/convert_all.py)
|
||||
// Do not edit manually — add .txt files to converter/shows/ instead.
|
||||
// Do not edit manually — add NNN_<name>.txt files to converter/shows/ instead.
|
||||
// =====================================================================
|
||||
//
|
||||
// Show 0 is always the home/reset show (blue breath).
|
||||
// Show 0 is the lowest-numbered .txt file (home/reset show).
|
||||
// Holding the button resets back to show 0.
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
@@ -140,20 +149,12 @@ const uint8_t SHOW_COUNT = {count};
|
||||
# ---- Main --------------------------------------------------------------
|
||||
|
||||
def main() -> None:
|
||||
txt_files = sorted(SHOWS_DIR.glob("*.txt"), key=lambda p: p.stem.lower())
|
||||
txt_files = sorted(SHOWS_DIR.glob("*.txt"), key=lambda p: p.name.lower())
|
||||
if not txt_files:
|
||||
print(f"No .txt files found in {SHOWS_DIR}")
|
||||
sys.exit(1)
|
||||
|
||||
# Separate the home show from the rest; sort the rest alphabetically.
|
||||
home_file = SHOWS_DIR / f"{HOME_SHOW}.txt"
|
||||
other_files = [f for f in txt_files if f.stem != HOME_SHOW]
|
||||
|
||||
if not home_file.exists():
|
||||
print(f"Warning: home show '{HOME_SHOW}.txt' not found — show 0 will be {txt_files[0].name}")
|
||||
ordered_files = txt_files
|
||||
else:
|
||||
ordered_files = [home_file] + other_files
|
||||
ordered_files = txt_files
|
||||
|
||||
SKETCH_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -164,12 +165,13 @@ def main() -> None:
|
||||
stem = txt_path.stem
|
||||
symbol = filename_to_symbol(stem)
|
||||
try:
|
||||
steps, mode = parse_show_file(txt_path)
|
||||
steps, mode, flags = parse_show_file(txt_path)
|
||||
header = render_show_header(steps, txt_path.name, symbol)
|
||||
out = SKETCH_DIR / f"show_{stem}.h"
|
||||
out.write_text(header, encoding="utf-8")
|
||||
print(f" OK {txt_path.name} → show_{stem}.h ({len(steps)} steps, {mode})")
|
||||
converted.append((stem, mode))
|
||||
flag_str = " sparkle" if flags & 0x01 else ""
|
||||
print(f" OK {txt_path.name} → show_{stem}.h ({len(steps)} steps, {mode}{flag_str})")
|
||||
converted.append((stem, mode, flags))
|
||||
except ValueError as e:
|
||||
print(f" ERR {e}")
|
||||
errors.append(stem)
|
||||
@@ -182,7 +184,7 @@ def main() -> None:
|
||||
sys.exit(1)
|
||||
|
||||
# Remove stale show_*.h files no longer in the converted list.
|
||||
expected = {SKETCH_DIR / f"show_{stem}.h" for stem, _ in converted}
|
||||
expected = {SKETCH_DIR / f"show_{stem}.h" for stem, _, __ in converted}
|
||||
for stale in SKETCH_DIR.glob("show_*.h"):
|
||||
if stale not in expected:
|
||||
stale.unlink()
|
||||
@@ -191,7 +193,7 @@ def main() -> None:
|
||||
# Regenerate shows.h
|
||||
index_path = SKETCH_DIR / "shows.h"
|
||||
index_path.write_text(render_shows_index(converted), encoding="utf-8")
|
||||
stems = [s for s, _ in converted]
|
||||
stems = [s for s, _, __ in converted]
|
||||
print(f"\n OK shows.h updated ({len(converted)} shows, show 0 = {stems[0]})")
|
||||
print( " Run 'make upload' to compile and send to the Arduino.")
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// Monster Hunter cosplay — heartbeat intro
|
||||
// Three heartbeats in bright red, then fades into the base color of the breathing show.
|
||||
// Auto-advances to 002_breath_red when done.
|
||||
//
|
||||
// mode: single
|
||||
// Format: #RRGGBB, duration_ms
|
||||
|
||||
#050000, 0 // start near black
|
||||
|
||||
// beat 1
|
||||
#d10e29, 120 // lub — fast rise to bright red
|
||||
#300000, 100 // dip between beats
|
||||
#d10e29, 100 // dub
|
||||
#050000, 700 // rest
|
||||
|
||||
// beat 2
|
||||
#d10e29, 120 // lub
|
||||
#300000, 100 // dip
|
||||
#d10e29, 100 // dub
|
||||
#050000, 700 // rest
|
||||
|
||||
// beat 3
|
||||
#d10e29, 120 // lub
|
||||
#300000, 100 // dip
|
||||
#d10e29, 100 // dub
|
||||
#050000, 700 // rest
|
||||
|
||||
// 2-second fade into the base color of the breathing show
|
||||
#99283a, 2000
|
||||
@@ -0,0 +1,23 @@
|
||||
// Red Heartbeat × 2 then Red Breathing — 2 lub-dub beats, then slow breathing, repeat.
|
||||
//
|
||||
// mode: single
|
||||
|
||||
#050000, 0 // start near black
|
||||
|
||||
// beat 1
|
||||
#d10e29, 120 // lub — fast rise to bright red
|
||||
#300000, 100 // dip between beats
|
||||
#d10e29, 100 // dub
|
||||
#050000, 600 // rest
|
||||
|
||||
// beat 2
|
||||
#d10e29, 120 // lub
|
||||
#300000, 100 // dip
|
||||
#d10e29, 100 // dub
|
||||
#050000, 600 // rest into breathing
|
||||
|
||||
// beat 3
|
||||
#d10e29, 120 // lub
|
||||
#300000, 100 // dip
|
||||
#d10e29, 100 // dub
|
||||
#050000, 600 // rest into breathing
|
||||
@@ -1,10 +0,0 @@
|
||||
// Monster Hunter cosplay — breathing red
|
||||
// Slow breathing between dark maroon (#99283a) and bright red (#d91e1e).
|
||||
// One breath cycle lasts 5 seconds. Loops continuously.
|
||||
//
|
||||
// mode: loop
|
||||
// Format: #RRGGBB, duration_ms
|
||||
|
||||
#99283a, 0 // snap to dim base color
|
||||
#d91e1e, 2500 // breathe in — slow rise to bright red
|
||||
#99283a, 2500 // breathe out — slow fall back to dim
|
||||
@@ -0,0 +1,7 @@
|
||||
// Red Heartbeat × 2 then Red Breathing — 2 lub-dub beats, then slow breathing, repeat.
|
||||
//
|
||||
// mode: loop
|
||||
|
||||
// breathing
|
||||
#d10e29, 3600 // fade up to bright red
|
||||
#050000, 3600 // fade back down to near black
|
||||
@@ -0,0 +1,10 @@
|
||||
// Blue breath with sparkle overlay
|
||||
//
|
||||
// flags: sparkle
|
||||
// mode: loop
|
||||
|
||||
#050018, 0 // snap to near-black blue
|
||||
#2020FF, 2500 // breathe in — slow rise to bright blue
|
||||
#2020FF, 400 // hold at peak
|
||||
#050018, 2500 // breathe out — slow fall to near-black
|
||||
#050018, 800 // pause before next breath
|
||||
@@ -1,8 +0,0 @@
|
||||
// Monster Hunter cosplay — solid deep red
|
||||
// Constant dark maroon (#99283a). Stays on until the button is pressed.
|
||||
//
|
||||
// mode: loop
|
||||
// Format: #RRGGBB, duration_ms
|
||||
|
||||
#99283a, 0 // snap on instantly
|
||||
#99283a, 30000 // hold (30s loop is invisible — same color each cycle)
|
||||
@@ -0,0 +1,7 @@
|
||||
// Yellow pulse — slow breathing effect
|
||||
//
|
||||
// mode: loop
|
||||
|
||||
#202000, 0 // start at dim yellow
|
||||
#FFFF00, 1200 // fade up to bright yellow
|
||||
#202000, 1200 // fade back down to dim yellow
|
||||
@@ -0,0 +1,6 @@
|
||||
// Solid green
|
||||
//
|
||||
// mode: loop
|
||||
|
||||
#00CC00, 0 // snap to green
|
||||
#00CC00, 5000 // hold (fade from green to green is invisible)
|
||||
@@ -0,0 +1,22 @@
|
||||
// Party mode — rapid rainbow color flashes
|
||||
// Plays once then auto-advances to the next show.
|
||||
//
|
||||
// mode: single
|
||||
|
||||
#FF0000, 0 // snap to red
|
||||
#FF0000, 150 // hold red
|
||||
#FF8800, 0 // snap to orange
|
||||
#FF8800, 150 // hold orange
|
||||
#FFFF00, 0 // snap to yellow
|
||||
#FFFF00, 150 // hold yellow
|
||||
#00FF00, 0 // snap to green
|
||||
#00FF00, 150 // hold green
|
||||
#00FFFF, 0 // snap to cyan
|
||||
#00FFFF, 150 // hold cyan
|
||||
#0000FF, 0 // snap to blue
|
||||
#0000FF, 150 // hold blue
|
||||
#FF00FF, 0 // snap to magenta
|
||||
#FF00FF, 150 // hold magenta
|
||||
#FFFFFF, 0 // snap to white
|
||||
#FFFFFF, 150 // hold white
|
||||
#000000, 500 // fade to off — brief pause before advancing
|
||||
@@ -1,12 +0,0 @@
|
||||
// Blue breath — home / reset show (always index 0)
|
||||
// A slow, natural breathing effect in deep blue.
|
||||
// Loops continuously as the default show.
|
||||
//
|
||||
// mode: loop
|
||||
// Format: #RRGGBB, duration_ms
|
||||
|
||||
#050018, 0 // snap to near-black blue — sets the starting point
|
||||
#2020FF, 2500 // breathe in — slow rise to bright blue
|
||||
#2020FF, 400 // hold at peak
|
||||
#050018, 2500 // breathe out — slow fall to near-black
|
||||
#050018, 800 // pause before next breath
|
||||
@@ -1,12 +0,0 @@
|
||||
// Example: slow color cycle
|
||||
// Smooth fades between colors with different speeds.
|
||||
//
|
||||
// Format: #RRGGBB, duration_ms
|
||||
// duration_ms = time to fade FROM the previous color TO this one.
|
||||
|
||||
#FF0000, 0 // snap to red immediately
|
||||
#00FF00, 3000 // 3 second fade to green
|
||||
#0000FF, 0 // snap to blue
|
||||
#FF00FF, 2000 // 2 second fade to purple
|
||||
#FF8000, 1000 // 1 second fade to orange
|
||||
#000000, 5000 // 5 second slow fade to off
|
||||
@@ -1,24 +0,0 @@
|
||||
// Example: party mode — rapid color flashes
|
||||
//
|
||||
// Trick: to HOLD a color for N ms, add a second step with the same color
|
||||
// and set that step's duration to N. The "fade from red to red" is invisible.
|
||||
//
|
||||
// Format: #RRGGBB, duration_ms
|
||||
|
||||
#FF0000, 0 // snap to red
|
||||
#FF0000, 150 // hold red 150ms
|
||||
#FF8800, 0 // snap to orange
|
||||
#FF8800, 150 // hold orange 150ms
|
||||
#FFFF00, 0 // snap to yellow
|
||||
#FFFF00, 150 // hold yellow 150ms
|
||||
#00FF00, 0 // snap to green
|
||||
#00FF00, 150 // hold green 150ms
|
||||
#00FFFF, 0 // snap to cyan
|
||||
#00FFFF, 150 // hold cyan 150ms
|
||||
#0000FF, 0 // snap to blue
|
||||
#0000FF, 150 // hold blue 150ms
|
||||
#FF00FF, 0 // snap to magenta
|
||||
#FF00FF, 150 // hold magenta 150ms
|
||||
#FFFFFF, 0 // snap to white
|
||||
#FFFFFF, 150 // hold white 150ms
|
||||
#000000, 500 // fade to off — brief pause before looping
|
||||
@@ -1,10 +0,0 @@
|
||||
// Example: slow red pulse / heartbeat effect
|
||||
//
|
||||
// Fades from dim red to bright red and back, repeating.
|
||||
// Shows how to create an organic-feeling breathing effect.
|
||||
//
|
||||
// Format: #RRGGBB, duration_ms
|
||||
|
||||
#200000, 0 // start at dim red (no fade — sets the initial state)
|
||||
#FF0000, 1200 // fade up to bright red
|
||||
#200000, 1200 // fade back down to dim red
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
simulate.py — Preview lightshows in the terminal without hardware.
|
||||
|
||||
Reads the same .txt show files the Arduino firmware uses and runs the
|
||||
same playback logic: blending, step timing, loop/single mode, and
|
||||
SHOW_SINGLE auto-advance.
|
||||
|
||||
Controls:
|
||||
→ or Space next show
|
||||
← previous show
|
||||
r reset to show 0 (home)
|
||||
q / Ctrl-C quit
|
||||
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
"""
|
||||
|
||||
import sys
|
||||
import select
|
||||
import termios
|
||||
import time
|
||||
import tty
|
||||
from pathlib import Path
|
||||
|
||||
# Reuse the show parser from the converter.
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from convert_all import parse_show_file, SHOWS_DIR, HOME_SHOW
|
||||
|
||||
# How many colored blocks to draw for the strip preview.
|
||||
STRIP_WIDTH = 44
|
||||
|
||||
|
||||
# ---- Show loading ----------------------------------------------------------
|
||||
|
||||
def load_shows() -> list[dict]:
|
||||
txt_files = sorted(SHOWS_DIR.glob("*.txt"), key=lambda p: p.stem.lower())
|
||||
home = SHOWS_DIR / f"{HOME_SHOW}.txt"
|
||||
others = [f for f in txt_files if f.stem != HOME_SHOW]
|
||||
ordered = ([home] + others) if home.exists() else txt_files
|
||||
|
||||
shows = []
|
||||
for path in ordered:
|
||||
try:
|
||||
steps, mode = parse_show_file(path)
|
||||
shows.append({"name": path.stem, "steps": steps, "mode": mode})
|
||||
except ValueError as e:
|
||||
print(f" Warning: {e}", file=sys.stderr)
|
||||
return shows
|
||||
|
||||
|
||||
# ---- Playback --------------------------------------------------------------
|
||||
|
||||
def blend_color(c1: tuple, c2: tuple, t: float) -> tuple:
|
||||
return tuple(int(a + (b - a) * t) for a, b in zip(c1, c2))
|
||||
|
||||
|
||||
# ---- Terminal rendering ----------------------------------------------------
|
||||
|
||||
RESET = "\x1b[0m"
|
||||
|
||||
|
||||
def ansi_bg(r: int, g: int, b: int) -> str:
|
||||
return f"\x1b[48;2;{r};{g};{b}m"
|
||||
|
||||
|
||||
def render(color: tuple, show_name: str, step_idx: int, step_count: int, mode: str) -> None:
|
||||
r, g, b = color
|
||||
# Dim the foreground text so it's readable on any background.
|
||||
bar = ansi_bg(r, g, b) + " " * STRIP_WIDTH + RESET
|
||||
name = show_name.replace("_", " ")
|
||||
info = f" {name} [{mode}] step {step_idx + 1}/{step_count} #{r:02X}{g:02X}{b:02X}"
|
||||
sys.stdout.write(f"\r{bar}{info} ")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
# ---- Non-blocking keyboard input -------------------------------------------
|
||||
|
||||
def get_key() -> str | None:
|
||||
if not select.select([sys.stdin], [], [], 0)[0]:
|
||||
return None
|
||||
ch = sys.stdin.read(1)
|
||||
if ch == "\x1b":
|
||||
more = select.select([sys.stdin], [], [], 0.02)[0]
|
||||
rest = sys.stdin.read(2) if more else ""
|
||||
if rest == "[C":
|
||||
return "right"
|
||||
if rest == "[D":
|
||||
return "left"
|
||||
return "esc"
|
||||
return ch
|
||||
|
||||
|
||||
# ---- Main ------------------------------------------------------------------
|
||||
|
||||
def main() -> None:
|
||||
shows = load_shows()
|
||||
if not shows:
|
||||
print(f"No .txt files found in {SHOWS_DIR}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Loaded {len(shows)} show(s). Controls: → next ← prev r reset q quit\n")
|
||||
|
||||
# Playback state — mirrors the Arduino sketch variables.
|
||||
show_idx = 0
|
||||
step_idx = 0
|
||||
step_start = time.monotonic()
|
||||
from_color = (0, 0, 0)
|
||||
|
||||
def switch_show(idx: int) -> None:
|
||||
nonlocal show_idx, step_idx, step_start, from_color
|
||||
show_idx = idx % len(shows)
|
||||
step_idx = 0
|
||||
step_start = time.monotonic()
|
||||
from_color = (0, 0, 0)
|
||||
|
||||
if not sys.stdin.isatty():
|
||||
print("simulate.py must be run in an interactive terminal.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
old_settings = termios.tcgetattr(sys.stdin)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
|
||||
while True:
|
||||
# ---- Button input ----
|
||||
key = get_key()
|
||||
if key in ("q", "\x03"): # q or Ctrl-C
|
||||
break
|
||||
elif key in ("right", " "):
|
||||
switch_show(show_idx + 1)
|
||||
elif key == "left":
|
||||
switch_show(show_idx - 1)
|
||||
elif key in ("r", "R"):
|
||||
switch_show(0)
|
||||
|
||||
# ---- Playback ----
|
||||
show = shows[show_idx]
|
||||
steps = show["steps"]
|
||||
mode = show["mode"]
|
||||
r, g, b, duration_ms = steps[step_idx]
|
||||
to = (r, g, b)
|
||||
|
||||
now = time.monotonic()
|
||||
elapsed_ms = (now - step_start) * 1000.0
|
||||
|
||||
if duration_ms == 0:
|
||||
t = 1.0
|
||||
else:
|
||||
t = min(elapsed_ms / duration_ms, 1.0)
|
||||
|
||||
render(blend_color(from_color, to, t), show["name"], step_idx, len(steps), mode)
|
||||
|
||||
# ---- Advance step when complete ----
|
||||
if t >= 1.0:
|
||||
from_color = to
|
||||
next_step = step_idx + 1
|
||||
if next_step >= len(steps):
|
||||
if mode == "SHOW_SINGLE":
|
||||
switch_show(show_idx + 1)
|
||||
else:
|
||||
step_idx = 0
|
||||
step_start = time.monotonic()
|
||||
else:
|
||||
step_idx = next_step
|
||||
step_start = time.monotonic()
|
||||
|
||||
time.sleep(0.016) # ~60 fps
|
||||
|
||||
finally:
|
||||
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|
||||
sys.stdout.write(f"\r{RESET}\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user