I made my own controller!

This commit is contained in:
Jesse Smick
2026-07-20 03:05:31 -06:00
parent 3394c880e2
commit c667858460
3 changed files with 334 additions and 49 deletions
+80 -11
View File
@@ -1,11 +1,13 @@
# Exploring Christmas Lights
## Table of Contents
- [Table of Contents](#table-of-contents)
- [Holidays](#holidays)
- [Exploring and Learning](#exploring-and-learning)
- [Exploring and Learning](#exploring-and-learning)
- [Summary](#summary)
- [Taking over the lights](#taking-over-the-lights)
- [Hidden modes](#hidden-modes)
- [Code](#code)
## Holidays
@@ -21,7 +23,7 @@ Another several weeks go by and I get to thinking how can I repurpose the Christ
I thought the circuit board would be in the same case as the power brick, but it turns out there is a little case along the wire with a button and everything I needed was in there!
| ![PCB](./images/pcb.jpg) |
|:--:|
| :--: |
| *The PCB. The top is flipped vertically so the through-holes line up with the bottom* |
The L- and L+ are the terminals connected to the LED string. I tried to find out what's connected to those terminals to see what is sending the signals. I noticed the chip in the upper left (of the bottom side) labeled IC2. That seems to be the only thing connected to the L+ terminal. I thought that must be what sends the signals. After looking up "NCE55P05S", which is the code that's printed on it, I found [its datasheet](https://datasheet.lcsc.com/lcsc/1912111437_Wuxi-NCE-Power-Semiconductor-NCE55P05S_C414142.pdf). It says it's a [MOSFET](https://en.wikipedia.org/wiki/MOSFET), but I had no idea what that is. So again I looked it up and now I know it's a certain type of transistor that can be used as an electronic switch. Now I thought that means it's not the part that sends the signal, it's just a switch and something else must tell it to switch on and off. I learned that it has a source, a drain, and a gate. Depending on the voltage of the gate, the source will connect or disconnect from the drain. In this case all four upper pins are the drain, the botton left three pins are the source, and the bottom rightmost pin is the gate. So all I needed to do was figure out what the gate connects to in order to know where the signals come from.
@@ -35,25 +37,25 @@ The HiLetgo doesn't come with software but some reviews said that Sigrok PulseVi
I tried different protocol decoders, but none of them showed any match to the signals. So I just added a basic Timing decoder which only shows the length of time of the signals. I found that easier to view when zoomed out a bit.
| ![zoomed all the way out](./images/sample-signals.png) |
|:--:|
| :--: |
| *Some signals, zoomed all the way out* |
After recording some samples while changing the lights with my phone, it looks like the signal is "on" most of the time and there are clusters of "off" pulses. The custers happened at the same time as when I switched the color or pattern. Let's zoom a bit more into a single cluster.
| ![a single cluster](./images/sample-cluster.png) |
|:--:|
| :--: |
| *A single cluster* |
Zooming even further into a single pulse.
| ![a single pulse](./images/sample-pulse.png) |
|:--:|
| :--: |
| *A single pulse* |
This pulse is 104 microseconds, and when I looked at more it does range a little bit how long the pulse is but it's in the range of 100-110µs. Then there are two different pause lengths between pulses.
| ![two pauses](./images/sample-pauses.png) |
|:--:|
| :--: |
| *The two different pause lengths* |
The shorter pause is 4964µs and the long pause is 17000µs. Again, these range but are within about 25µs of the ones seen in the image.
@@ -65,14 +67,15 @@ If I had to guess at this point, the first three bits would be for the pattern!
## Summary
Here's a summary of the command format that I've found:
- "On" by default
- Short "off" pulses separate bits
- Pulses are 105±5µs. `0` bit has 4965±5µs pause and `1` bit has 16998±6µs pause
- Every command is 6 bits and a leading `0` (7 bits total)
- After start bit
- Two bits for pattern
- One bit for multi-color mode
- Three bits for color (BGR)
- Two bits for pattern
- One bit for multi-color mode
- Three bits for color (BGR)
- A full sequence is: `0 PP M BGR` (spaces are for reading only, not for timing)
- Patterns are
- `00`: Steady
@@ -81,6 +84,7 @@ Here's a summary of the command format that I've found:
- `11`: Firefly
Also some notes about the behaviour of some modes and commands that I observed:
- Even when LEDs are off, the signal is still fully in "on" position, it's up to the LED to show light or not
- The Power Off button sends all zeros: `0 00 0 000`
- The Power On button flashes white/off three times with about 330ms flashes, then waits about 475ms then sets the current color and pattern
@@ -90,6 +94,7 @@ Also some notes about the behaviour of some modes and commands that I observed:
- In multi-color mode, when a light turns off and back on, it always switches colors
Some example commands:
- Steady white: `0 00 0 111`
- Steady multi-color `0 00 1 000`
- Fade red: `0 01 0 001`
@@ -101,6 +106,70 @@ Some example commands:
It was super fun to be able to figure out how the lights work. But to get back to my original goal, I wanted to be able to customize the patterns. I have less options than I thought I would since the patterns are hardcoded into the bulbs directly. But I can still customize switching between patterns and colors. At this time I don't have the skills or hardware to reproduce the MOSFET switching needed to drive the LEDs, but that is the next step in this project!
*(Update: I did get there! See [Taking over the lights](#taking-over-the-lights). Also that leading `0` was not quite what I thought. See [Hidden modes](#hidden-modes).)*
## Taking over the lights
Fast forward a few years and I finally came back to the "drive these myself" part. The original controller box unplugs from both the brick and the string, so I left that board alone and built a little replacement brain.
The power supply is labeled **29V 45W**. The string still wants that high voltage, and an ESP8266/ESP32 only wants 3.3V logic. So the setup is:
1. Keep the original 29V brick for the lights
2. A buck converter steps 29V down to 5V for a NodeMCU (or just use USB while tinkering, but share ground with the brick)
3. An N-channel logic-level MOSFET on the **low side** (string `L-`) does the same job the OEM high-side switch did: briefly cut power to encode bits
4. GPIO idle **HIGH** = lights powered; short **LOW** pulses = the protocol
I used a gate resistor (~150Ω) between the GPIO and the MOSFET gate, and a 10kΩ pulldown from gate to ground so the FET doesn't float on during boot. Source to ground / brick negative, drain to string `L-`, brick positive straight to string `L+`.
Once power toggling worked I ported the protocol from the earlier Arduino sketch onto the NodeMCU. Steady red, then a full pattern/color demo, and it worked! Watching the board LED and the string flip together never gets old.
I also poked at multi-color a bit. If you keep resending Steady Multi, most bulbs reshuffle pretty randomly, but some hang on one color for a while or flip between two colors before eventually wandering through everything. Re-sending Multi also includes those short power-off pulses in the frame, so the reshuffle might be "multi mode plus little power glitches" as much as "decoded the Multi bit again."
## Hidden modes
Remember how I said the first pattern bit was always `0` and there were only four patterns? That was true for everything the *phone app* sends. It was not true for what the bulbs actually understand.
I wrote a small sweeper that walked the 7-bit commands the app never uses, with toggle switches to step forward/back so I could stare at the string and fill in a table ([command-sweep.md](./command-sweep.md)). Turns out the format is better thought of as:
`PPP M BGR`
- **PPP** (3 bits): pattern. There are **eight**, not four!
- **M** (1 bit): solid color vs palette mode
- **BGR** (3 bits): either a normal color, or which palette when `M=1`
That "leading `0`" was just the high pattern bit. The app never set it, so I never saw patterns `100` through `111`.
### Patterns
| PPP | Name (what I'm calling it) | What it looks like |
| ----- | ---------------------------- | -------------------- |
| `000` | Steady | Solid color / palette |
| `001` | Fade | OEM Fade |
| `010` | Sparkle | OEM Sparkle (sparkles toward white) |
| `011` | Firefly | OEM Firefly |
| `100` | Twinkle | Mostly dark; random bulbs flash on in color |
| `101` | Fast Sparkle | Like Sparkle but faster |
| `110` | Anti-Sparkle | Base color on; sparkles turn bulbs *off* (black sparkles!) |
| `111` | Fast Twinkle | Faster Twinkle |
### Colors and palettes
When `M=0`, `BGR` is the same solid colors as before (Off, R, G, Y, B, M, C, W).
When `M=1`, those bits pick a **palette** instead of "red plus green equals yellow":
| BGR | Palette |
| ----- | --------- |
| `000` | Classic multi (random per bulb) |
| `001` | Christmas: red / white / green |
| `010` | July 4th: red / white / blue |
| `011` | Same as classic multi, as far as I can tell |
| `100` through `111` | Often just off, or weird pattern-dependent leftovers |
So yeah, the bulbs had Christmas and Fourth of July palettes sitting in there unused the whole time. Also a whole second set of sparkle-ish patterns. The Flip mode on the phone is still just software, but a lot of the "I wish I had more modes" problem was already solved in the silicon.
Full notes from the sweep are in [command-sweep.md](./command-sweep.md).
## Code
Even though I can't test it yet, I have gone ahead and written a sample program to produce the same signals to control the lights. I wrote it for the Arduino platform and it's available as [christmas-lights.c.ino](./christmas-lights.c.ino). View these signals in a similar way I can see they it matches pretty well with the official signals, although the Arduino is not as precise and there is more variance. The current program just loops through all the possible colors and patterns to replicate a "demo" mode.
The Arduino demo that walks the meaningful patterns and colors/palettes is [christmas-lights.c.ino](./christmas-lights.c.ino). It sends full 7-bit `PPP M BGR` frames (pattern in the top three bits). On ESP8266/ESP32 it defaults to `D1` for the MOSFET gate.
+77 -38
View File
@@ -1,42 +1,73 @@
// Mr. Christmas powerline RGB protocol
// 7-bit command: PPP M BGR (MSB first)
//
// Idle HIGH = string powered; brief LOW pulses encode bits.
// Short pause ~= 0, long pause ~= 1.
#if defined(ESP8266) || defined(ESP32)
const int command_pin = D1; // MOSFET gate
#else
const int command_pin = 8;
#endif
const int PULSE_DURATION = 100; // microseconds
const int SHORT_PAUSE = 4964; // bit 0
const int LONG_PAUSE = 16992; // bit 1
// 3bits: PPP
enum Pattern {
STEADY = 0,
FADE = 0x10,
SPARKLE = 0x20,
FIREFLY= 0x30
STEADY = 0, // 000
FADE = 1, // 001
SPARKLE = 2, // 010
FIREFLY = 3, // 011
TWINKLE = 4, // 100 - dark + colored flashes
FAST_SPARKLE = 5, // 101
ANTI_SPARKLE = 6, // 110 - black sparkles
FAST_TWINKLE = 7, // 111
};
// 4bits: MBGR
// M=0: BGR (solid colors)
// M=1: palette
enum Color {
OFF,
RED,
GREEN,
YELLOW,
BLUE,
MAGENTA,
CYAN,
WHITE,
MULTICOLOR
COLOR_OFF = 0, // 0000
RED = 1, // 0001
GREEN = 2, // 0010
YELLOW = 3, // 0011
BLUE = 4, // 0100
MAGENTA = 5, // 0101
CYAN = 6, // 0110
WHITE = 7, // 0111
MULTI = 8, // 1000 - multi-color
CHRISTMAS = 9, // 1001 - red/white/green
JULY4 = 10, // 1010 - red/white/blue
};
Pattern patterns[] = {STEADY, FADE, SPARKLE, FIREFLY};
Color colors[] = {RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, MULTICOLOR, OFF};
Pattern patterns[] = {
STEADY, FADE, SPARKLE, FIREFLY, TWINKLE, FAST_SPARKLE, ANTI_SPARKLE, FAST_TWINKLE,
};
const char *pattern_names[] = {
"Steady", "Fade", "Sparkle", "Firefly", "Twinkle", "FastSparkle", "AntiSparkle", "FastTwinkle",
};
int PULSE_DURATION = 100; // microseconds
int SHORT_PAUSE = 4964; // microseconds
int LONG_PAUSE = 16992; // miccroseconds
// output pin to send the signals to
int command_pin = 8;
Color colors[] = {
COLOR_OFF, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, MULTI, CHRISTMAS, JULY4,
};
const char *color_names[] = {
"Off", "Red", "Green", "Yellow", "Blue", "Magenta", "Cyan", "White", "Multi", "Christmas", "July4",
};
/**
* Send a single short pulse of "off".
*/
void send_pulse() {
noInterrupts();
digitalWrite(command_pin, LOW);
delayMicroseconds(PULSE_DURATION);
digitalWrite(command_pin, HIGH);
interrupts();
}
/**
* Send a single bit.
*/
@@ -45,45 +76,53 @@ void send_bit(bool b) {
delayMicroseconds(b ? LONG_PAUSE : SHORT_PAUSE);
}
/**
* Send a whole command. Commands start with a 0 bit then six bits of data.
* Send a whole 7-bit command (PPP M BGR), then an end pulse.
*/
void send_command(int cmd) {
// start command
send_bit(0);
for (int i = 0x20; i > 0; i >>= 1) {
send_bit(cmd & i);
for (int mask = 0x40; mask > 0; mask >>= 1) {
send_bit(cmd & mask);
}
// end command
send_pulse();
// minimum time before sending another command
delayMicroseconds(LONG_PAUSE);
}
/**
* Send a whole command based on a pattern and color.
* Send a whole command based on a pattern and color/palette.
*/
void send_command(Pattern pattern, Color color) {
send_command(pattern | color);
send_command((pattern << 4) | color);
}
void setup() {
pinMode(command_pin, OUTPUT);
digitalWrite(command_pin, HIGH);
Serial.begin(9600);
delay(500);
Serial.println();
Serial.println("Full demo");
delay(1000);
}
void loop() {
delay(1000);
// Demo: each pattern x colors/palettes.
for (unsigned p = 0; p < sizeof(patterns) / sizeof(patterns[0]); p++) {
for (unsigned c = 0; c < sizeof(colors) / sizeof(colors[0]); c++) {
if (colors[c] == COLOR_OFF && patterns[p] != FAST_SPARKLE) {
// Off is only meaningful with FastSparkle (dark + fast white sparkles).
continue;
}
// Sample "demo" mode where it cycles between all the colors of all the patterns
for (int p = 0; p < sizeof(patterns) / sizeof(Pattern); p++) {
for (int c = 0; c < sizeof(colors) / sizeof(Color); c++) {
send_command(patterns[p], colors[c]);
Serial.print(pattern_names[p]);
Serial.print(" + ");
Serial.println(color_names[c]);
delay(10000);
}
}
send_command(STEADY, COLOR_OFF);
Serial.println("Off");
delay(5000);
}
+177
View File
@@ -0,0 +1,177 @@
# 7-bit command sweep notes
## Revised protocol (after sweep)
Layout: `PPP M BGR` (7 bits, MSB first). Same pulse timing as before.
| Field | Bits | Meaning |
| ------- | ------ | --------- |
| PPP | 3 | Pattern (8 modes — not 4) |
| M | 1 | Palette / multi flag |
| BGR | 3 | Color or palette select |
The old write-up treated this as `0 PP M BGR` (leading bit always 0 + 2 pattern bits). The leading bit is actually **pattern bit 2**; the app simply never used patterns `100``111`.
### Patterns (`PPP`)
| PPP | Name (working) | Behavior |
| ----- | ---------------- | ---------- |
| `000` | Steady | Solid color / palette |
| `001` | Fade | Fade (OEM “Fade”) |
| `010` | Sparkle | Sparkle to white (OEM) |
| `011` | Firefly | Firefly (OEM) |
| `100` | Twinkle | Mostly off; random bulbs flash on in color (colored sparkles on dark) |
| `101` | Fast sparkle | Like Sparkle but faster |
| `110` | Anti-sparkle | Base color on; sparkles turn bulbs **off** (black sparkles) |
| `111` | Fast twinkle | Faster version of Twinkle (`100`) |
### Color / palette (`M` + `BGR`)
When `M=0`, `BGR` is a normal solid color (same as before):
| BGR | Color |
| ----- | ------- |
| `000` | Off |
| `001` | Red |
| `010` | Green |
| `011` | Yellow |
| `100` | Blue |
| `101` | Magenta |
| `110` | Cyan |
| `111` | White |
When `M=1`, `BGR` selects a **palette** (not a single RGB mix):
| BGR | Palette |
| ----- | --------- |
| `000` | Classic multi (per-bulb random across colors) |
| `001` | Christmas — red / white / green |
| `010` | July 4th — red / white / blue |
| `011` | Same as classic multi (indistinguishable from `000` in testing) |
| `100``111` | Often **off** or pattern-dependent (see tables; Fade/Firefly sometimes treat these as solid B/M/C/W) |
Palette behavior depends on pattern (e.g. Steady+Multi+`1xx` → off; Fade+Multi+`1xx` → solid colors; Sparkle+Multi+`1xx` → dark with white sparkles).
### OEM / app subset
The phone app only used `PPP` in `000``011` with either solid colors (`M=0`) or Multi (`M=1`, `BGR=000`). Hidden: palettes `001`/`010`/`011` and patterns `100``111`.
---
## Known (skipped by early sweep sketch)
- `PPP` `000``011`, `M=0`, any `BGR` → published Steady/Fade/Sparkle/Firefly × colors
- `PPP` `000``011`, `M=1`, `BGR=000` → Multi with those patterns
---
## Sweep results
### A) Multi + nonzero BGR (`PPP` `000``011`, `M=1`, `BGR≠000`)
| Dec | Binary `PPP M BGR` | PPP | M | BGR | Observed |
| ----: | -------------------- | ----- | --- | ----- | ---------- |
| 9 | `000 1 001` | Steady | 1 | 001 | red/white/green |
| 10 | `000 1 010` | Steady | 1 | 010 | red/white/blue |
| 11 | `000 1 011` | Steady | 1 | 011 | classic multi (same as `000`) |
| 12 | `000 1 100` | Steady | 1 | 100 | off |
| 13 | `000 1 101` | Steady | 1 | 101 | off |
| 14 | `000 1 110` | Steady | 1 | 110 | off |
| 15 | `000 1 111` | Steady | 1 | 111 | off |
| 25 | `001 1 001` | Fade | 1 | 001 | red/white/green |
| 26 | `001 1 010` | Fade | 1 | 010 | red/white/blue |
| 27 | `001 1 011` | Fade | 1 | 011 | classic multi (same as `000`) |
| 28 | `001 1 100` | Fade | 1 | 100 | blue |
| 29 | `001 1 101` | Fade | 1 | 101 | magenta |
| 30 | `001 1 110` | Fade | 1 | 110 | cyan |
| 31 | `001 1 111` | Fade | 1 | 111 | white |
| 41 | `010 1 001` | Sparkle | 1 | 001 | red/white/green |
| 42 | `010 1 010` | Sparkle | 1 | 010 | red/white/blue |
| 43 | `010 1 011` | Sparkle | 1 | 011 | classic multi (same as `000`) |
| 44 | `010 1 100` | Sparkle | 1 | 100 | all off but still white sparkle |
| 45 | `010 1 101` | Sparkle | 1 | 101 | ditto |
| 46 | `010 1 110` | Sparkle | 1 | 110 | ditto |
| 47 | `010 1 111` | Sparkle | 1 | 111 | ditto |
| 57 | `011 1 001` | Firefly | 1 | 001 | red/white/green |
| 58 | `011 1 010` | Firefly | 1 | 010 | red/white/blue |
| 59 | `011 1 011` | Firefly | 1 | 011 | classic multi (same as `000`) |
| 60 | `011 1 100` | Firefly | 1 | 100 | blue |
| 61 | `011 1 101` | Firefly | 1 | 101 | magenta |
| 62 | `011 1 110` | Firefly | 1 | 110 | cyan |
| 63 | `011 1 111` | Firefly | 1 | 111 | white |
### B) Alternate patterns (`PPP` `100``111`)
| Dec | Binary `PPP M BGR` | PPP | M | BGR | Observed |
| ----: | -------------------- | ----- | --- | ----- | ---------- |
| 64 | `100 0 000` | Twinkle | 0 | 000 | off |
| 65 | `100 0 001` | Twinkle | 0 | 001 | all off but red sparkle |
| 66 | `100 0 010` | Twinkle | 0 | 010 | ditto but green sparkle |
| 67 | `100 0 011` | Twinkle | 0 | 011 | ditto but yellow |
| 68 | `100 0 100` | Twinkle | 0 | 100 | ditto blue |
| 69 | `100 0 101` | Twinkle | 0 | 101 | ditto magenta |
| 70 | `100 0 110` | Twinkle | 0 | 110 | ditto cyan |
| 71 | `100 0 111` | Twinkle | 0 | 111 | ditto white |
| 72 | `100 1 000` | Twinkle | 1 | 000 | ditto but the sparkles are multi |
| 73 | `100 1 001` | Twinkle | 1 | 001 | ditto but sparkles are red/white/green |
| 74 | `100 1 010` | Twinkle | 1 | 010 | ditto but red/white/blue |
| 75 | `100 1 011` | Twinkle | 1 | 011 | ditto classic multi (same as `000`) |
| 76 | `100 1 100` | Twinkle | 1 | 100 | off |
| 77 | `100 1 101` | Twinkle | 1 | 101 | off |
| 78 | `100 1 110` | Twinkle | 1 | 110 | off |
| 79 | `100 1 111` | Twinkle | 1 | 111 | off |
| 80 | `101 0 000` | Fast sparkle | 0 | 000 | all off but fast white sparkles |
| 81 | `101 0 001` | Fast sparkle | 0 | 001 | red and fast white sparkles |
| 82 | `101 0 010` | Fast sparkle | 0 | 010 | ditto green |
| 83 | `101 0 011` | Fast sparkle | 0 | 011 | ditto yellow |
| 84 | `101 0 100` | Fast sparkle | 0 | 100 | ditto blue |
| 85 | `101 0 101` | Fast sparkle | 0 | 101 | ditto magenta |
| 86 | `101 0 110` | Fast sparkle | 0 | 110 | ditto cyan |
| 87 | `101 0 111` | Fast sparkle | 0 | 111 | ditto but white (normal white is slightly dim and sparkles are brighter) |
| 88 | `101 1 000` | Fast sparkle | 1 | 000 | multi and fast sparkles |
| 89 | `101 1 001` | Fast sparkle | 1 | 001 | red/white/green and fast sparkles |
| 90 | `101 1 010` | Fast sparkle | 1 | 010 | ditto red/white/blue |
| 91 | `101 1 011` | Fast sparkle | 1 | 011 | classic multi and fast sparkles (same as `000`) |
| 92 | `101 1 100` | Fast sparkle | 1 | 100 | off and fast sparkles |
| 93 | `101 1 101` | Fast sparkle | 1 | 101 | ditto |
| 94 | `101 1 110` | Fast sparkle | 1 | 110 | ditto |
| 95 | `101 1 111` | Fast sparkle | 1 | 111 | ditto |
| 96 | `110 0 000` | Anti-sparkle | 0 | 000 | off |
| 97 | `110 0 001` | Anti-sparkle | 0 | 001 | red and anti-sparkles (black sparkles) |
| 98 | `110 0 010` | Anti-sparkle | 0 | 010 | ditto green |
| 99 | `110 0 011` | Anti-sparkle | 0 | 011 | ditto yellow |
| 100 | `110 0 100` | Anti-sparkle | 0 | 100 | ditto blue |
| 101 | `110 0 101` | Anti-sparkle | 0 | 101 | ditto magenta |
| 102 | `110 0 110` | Anti-sparkle | 0 | 110 | ditto cyan |
| 103 | `110 0 111` | Anti-sparkle | 0 | 111 | ditto white |
| 104 | `110 1 000` | Anti-sparkle | 1 | 000 | ditto multi |
| 105 | `110 1 001` | Anti-sparkle | 1 | 001 | ditto red/white/green |
| 106 | `110 1 010` | Anti-sparkle | 1 | 010 | ditto red/white/blue |
| 107 | `110 1 011` | Anti-sparkle | 1 | 011 | ditto classic multi (same as `000`) |
| 108 | `110 1 100` | Anti-sparkle | 1 | 100 | off |
| 109 | `110 1 101` | Anti-sparkle | 1 | 101 | off |
| 110 | `110 1 110` | Anti-sparkle | 1 | 110 | off |
| 111 | `110 1 111` | Anti-sparkle | 1 | 111 | off |
| 112 | `111 0 000` | Fast twinkle | 0 | 000 | off |
| 113 | `111 0 001` | Fast twinkle | 0 | 001 | all off but fast red sparkles |
| 114 | `111 0 010` | Fast twinkle | 0 | 010 | ditto green |
| 115 | `111 0 011` | Fast twinkle | 0 | 011 | ditto yellow |
| 116 | `111 0 100` | Fast twinkle | 0 | 100 | ditto blue |
| 117 | `111 0 101` | Fast twinkle | 0 | 101 | ditto magenta |
| 118 | `111 0 110` | Fast twinkle | 0 | 110 | ditto cyan |
| 119 | `111 0 111` | Fast twinkle | 0 | 111 | ditto white |
| 120 | `111 1 000` | Fast twinkle | 1 | 000 | ditto multi |
| 121 | `111 1 001` | Fast twinkle | 1 | 001 | ditto red/white/green |
| 122 | `111 1 010` | Fast twinkle | 1 | 010 | ditto red/white/blue |
| 123 | `111 1 011` | Fast twinkle | 1 | 011 | ditto classic multi (same as `000`) |
| 124 | `111 1 100` | Fast twinkle | 1 | 100 | off |
| 125 | `111 1 101` | Fast twinkle | 1 | 101 | off |
| 126 | `111 1 110` | Fast twinkle | 1 | 110 | off |
| 127 | `111 1 111` | Fast twinkle | 1 | 111 | off |
## Summary
- **8 patterns**, not 4 — high bit selects the alternate set (twinkle / fast sparkle / anti-sparkle / fast twinkle).
- **`M=1` is palette mode**: `000`/`011` classic multi, `001` Christmas R/W/G, `010` July 4th R/W/B; `1xx` is pattern-dependent (often off).
- Fade/Firefly + Multi + `1xx` oddly map to solid B/M/C/W — possible alias or shared decode path.
- Names above (Twinkle, Fast sparkle, etc.) are working labels from observation, not OEM names.