A LED Edit .tol file is a plain, unencrypted recording: a 14-byte header followed by every frame of the animation as raw R,G,B bytes. Once you know that, any microcontroller or desktop app can play it.
The "Circle" zip we started from held two kinds of files:
• 9 .tol files (Nov 2018): "Round 3", "Pink & Round 4", "Arrow Left & Right" and others. These are LED Edit's own export format, and we decoded them fully.
• 24 .led files (00_1.led to 23_1.led): the SD-card files LED Edit writes for a hardware controller. They are encrypted, and we did not decode them (see the last section).
The result is two players built on the same understanding of the format: LedEditTolPlayer.ino (ESP32 or Mega 2560, plays from an SD card with FastLED) and TolPlayer (a Java 8 Swing desktop player with wiring modes and export). This tutorial walks through how the format was worked out, step by step, so you can repeat the method on other files.
Step 1 — First look: sizes and hex dumps
The first clue is in the hex dump: .tol bytes are only 00 and 7f in neat repeating groups of three, while .led bytes look random. Always start an unknown format with the file sizes and the first few hundred bytes.
1. List the sizes. The .led files are all exact multiples of 512 bytes (one SD-card sector), so they are written for a controller reading raw sectors. The .tol sizes end in odd numbers like 1,881,014, so they have a small header in front of evenly sized data.
2. Dump the start of each file (od -A x -t x1z file | head, or any hex editor).
3. Count the distinct byte values. Every .tol file uses only 2 values: 0x00 and 0x7F. That means there is no compression or encryption, just colour levels.
The start of Round 3.tol looks like this:
000000 00 00 04 15 00 0a 00 3c 00 23 00 16 03 00 00 00
000010 00 00 00 00 00 00 00 7f 00 7f 7f 00 7f 7f 00 7f
000020 7f 00 7f 7f 00 7f 7f 00 7f 00 00 00 00 00 00 00
The first 14 bytes are small big-endian numbers; after that the bytes come in groups of three, which is what RGB pixel data looks like.
Step 2 — Decoding the 14-byte header
The header gives the frame count and the pixel grid, and the file size proves it: size = 14 + frames × pixels × 3, exactly, for all 9 files.
All multi-byte numbers are big-endian (most significant byte first).
|
Offset |
Size |
Example (Round 3) |
Meaning |
|
0 |
4 bytes |
00 00 04 15 = 1045 |
Frame count |
|
4 |
2 bytes |
00 0a = 10 |
Dimension A: rings (rows) |
|
6 |
2 bytes |
00 3c = 60 |
Dimension B: pixels per ring (columns) |
|
8 |
2 bytes |
00 23 = 35 |
Unknown, same in every file (maybe speed) |
|
10 |
2 bytes |
00 16 = 22 |
Unknown, same in every file |
|
12 |
1 byte |
03 |
Bytes per pixel (R, G, B) |
|
13 |
1 byte |
00 |
Reserved |
How we proved it, using Round 3.tol:
• Pixels = 10 × 60 = 600, so one frame = 600 × 3 = 1,800 bytes.
• 1,045 frames × 1,800 = 1,881,000 bytes, plus the 14-byte header = 1,881,014.
• The real file size is 1,881,014 bytes. Exact match.
The same check holds for the short file: Left & Right.tol has 109 frames, and 14 + 109 × 1,800 = 196,214 bytes, its exact size. When a guessed layout makes the size formula come out exact for every file, the guess is right.
Step 3 — Reading the pixel data
Frame f, pixel p starts at byte 14 + (f × pixels + p) × 3, and its three bytes are R, G, B, each from 0 to 127.
\text{offset}(f, p) = 14 + (f \cdot N + p) \cdot 3, \qquad N = A \cdot B = 600
Colour order. The bytes are in R, G, B order. LED Edit's preview colours (pink = R+B, cyan = G+B) come out right in that order. Most WS2812B strips want GRB on the wire, but that swap belongs in the driver (COLOR_ORDER in FastLED), not in the file reader.
7-bit values. LED Edit stores each channel as 0 to 127, not 0 to 255. Every file here uses only 0 and 127, so sending the raw value gives half brightness. Scale it to 8 bits by shifting left one bit and copying the top bit into the bottom:
out = (v << 1) | (v >> 6); // 0 -> 0, 64 -> 129, 127 -> 255
Proof by picture. The quickest test that a decode is right is to draw it. We drew one row of 600 pixels per frame, frame after frame downwards. A wrong frame size gives slanted noise; the right one gives clean diagonal chase stripes, shown below for Round 3.tol. The stripes break at multiples of 60 pixels, which is the first hint that the data is organised as 10 groups of 60.
Step 4 — Working out the wheel geometry
The 600 pixels are a wheel of 60 spokes × 10 rings, and the file stores the outer rim ring first and the hub ring last. Each block of 60 pixels (180 bytes) is one ring, going around the wheel.
The header only says 10 × 60; it does not say which number is rings and which is spokes, or which ring comes first. We settled it by drawing the same frames four ways and comparing them with stills from the YouTube video of the real wheel:
|
Variant |
Pixel i is placed at |
Matched the video? |
|
A: rings, hub first |
ring = i ÷ 60, spoke = i mod 60 |
No: the solid ring appears at the rim |
|
B: rings, rim first |
ring = 9 − (i ÷ 60), spoke = i mod 60 |
Yes, every still |
|
C: spokes |
spoke = i ÷ 10, ring = i mod 10 |
No: smeared spirals |
|
D: spokes, zig-zag |
as C, odd spokes reversed |
No: smeared spirals |
Variant B reproduced all three stills: the solid inner ring with groups of 3–4 spokes ("Round 3"), green at the hub with pink and blue outside ("Blue & Round 6"), and pink and cyan alternating spokes ("Pink & Round 4"). In TolPlayer this is the Hub↔Rim flip option, on by default.
Two settings the file cannot tell you, so the player leaves them adjustable: which spoke is at the top (the Rotate control) and whether your strip is RGB or GRB (the Colour control).
The comparison sheet below shows the four variants side by side for Round 3.tol.
Step 5 — Wiring modes: from picture order to strip order
The .tol file stores the picture ring by ring; the wiring mode only decides which order the physical LEDs are chained in. So a player builds a chain map: chain[k] = the file pixel that LED number k on the strip must show.
On a wheel, a row is a ring (going around) and a column is a spoke (going outwards). With R rows and C columns, chain position s becomes row r and column c like this:
|
Mode |
Row r |
Column c |
On the wheel |
|
SINGLE_ROW |
s ÷ C |
s mod C |
Around each ring the same way, then step out |
|
RETURN_ROW |
s ÷ C |
s mod C, reversed on odd rows |
Rings zig-zag: around, then back |
|
SINGLE_COLUMN |
s mod R |
s ÷ R |
Each spoke hub to rim, all the same way |
|
RETURN_COLUMN |
s mod R, reversed on odd columns |
s ÷ R |
Spokes zig-zag: out, then back in |
Two more switches pick the corner the chain starts from:
• Start: hub or rim. Rim flips the row: r = R − 1 − r.
• Direction: clockwise or anticlockwise. Anticlockwise flips the column: c = C − 1 − c.
That gives 4 modes × 2 starts × 2 directions = 16 wirings, plus Rotate for where spoke 0 sits. Finally the player looks up which file pixel sits at (r, c), using the geometry from step 4, and stores it in chain[k].
There are two ways to use the map:
1. Export wired .tol: TolPlayer rewrites every frame so pixel k = original pixel chain[k]. The file keeps the same format, and the Arduino plays it straight from the SD card.
2. Export map .h: TolPlayer writes WIRING_MAP[] into flash, and the Arduino remaps each frame as it reads it (USE_WIRING_MAP 1).
Raw preview does the reverse: it shows what the wheel displays if you send the file unmapped to a given wiring. If one of those previews matches what your hardware shows today, that is your wiring.
Check it with RETURN_COLUMN, hub start, clockwise: LED 0 is the hub of spoke 0 (file pixel 540), it runs out to the rim (pixel 0), then LED 10 is the rim of spoke 1 (pixel 1) and comes back in. The exported file matched the original pixel-for-pixel through that map on every frame checked.
The code: reading a .tol file
The same few lines read the format in every language: read 14 bytes, decode the big-endian numbers, then read pixels × 3 bytes per frame.
Python (quick check on a PC)
def read_tol(path):
d = open(path, 'rb').read()
frames = int.from_bytes(d[0:4],
'big')
a, b = int.from_bytes(d[4:6], 'big'),
int.from_bytes(d[6:8], 'big')
n = a * b # 600 pixels
for f in range(frames):
o = 14 + f * n * 3
yield [tuple(min(255, v * 2) for
v in d[o + 3*p : o + 3*p + 3]) for p in range(n)]
Java 8 (from TolPlayer)
int frames = ((all[0] & 0xFF)
<< 24) | ((all[1] & 0xFF) << 16) | ((all[2] & 0xFF)
<< 8) | (all[3] & 0xFF);
int dimA = ((all[4] & 0xFF) << 8) | (all[5] & 0xFF);
int dimB = ((all[6] & 0xFF) << 8) | (all[7] & 0xFF);
int pixels = dimA * dimB;
// pixel p of frame f:
int o = 14 + (f * pixels + p) * 3;
int v = all[o] & 0x7F;
// red, 0..127
int red = (v << 1) | (v >> 6); // 0..255
Arduino (from LedEditTolPlayer.ino)
The Arduino never loads the whole file. It reads one 1,800-byte frame at a time from the SD card straight into the FastLED buffer, which works because CRGB is stored as r, g, b in memory:
f.seek(14); // skip the header
for (uint32_t fr = 0; fr < frames; fr++) {
f.read((uint8_t *)leds, pixels *
3); // one frame
uint8_t *p = (uint8_t *)leds;
for (uint16_t i = 0; i < pixels * 3;
i++) p[i] = (p[i] << 1) | (p[i] >> 6);
FastLED.show();
delay(FRAME_MS); // 40 ms = 25
fps
}
Memory is the limit on small boards: 600 pixels need 1,800 bytes of LED buffer plus about 600 bytes for the SD library. That fits an ESP32 or a Mega 2560 (8 KB RAM) but not a Uno or Nano (2 KB).
Why the .led files were not decoded
The .led files are LED Edit's output for a hardware SD-card controller. We worked out their layout, but the pixel bytes are scrambled with a per-file code we could not reverse reliably. The practical fix is to open the project in LED Edit and save it as .tol.
What we did learn:
|
Part |
What it holds |
|
Bytes 0–6 |
Text marker rzbv001 |
|
Bytes 12–13 |
Frame count, big-endian (e.g. 01 a0 = 416 for 00_1.led) |
|
Bytes 0x2E–0x1FF |
Random-looking bytes, probably an encrypted settings block |
|
Rest of sector 0 |
Header, 512 bytes in total |
|
Each frame |
6,144 bytes = 12 sectors = 8 outputs × 256 pixels × 3 colours |
|
End of file |
40 sectors of zeros (padding) |
The size check works here too: 512 + frames × 6,144 + 40 × 512 = the file size, for all 24 files.
Why it stops there:
• The 6,144-byte frames look like bit-planes: each byte carries one bit for 8 outputs at once, so the controller can drive 8 strips in parallel.
• The bytes are then substituted through a table that changes from file to file. A file with one colour still shows two different byte values, and those two values differ between files (0x86/0x45 in one, 0xE4/0xA3 in another).
• Without a known plain frame to compare against, there is no reliable way to recover that table.
Quick reference
To play any LED Edit .tol file: skip 14 bytes, read A × B × 3 bytes per frame, double each value, and place pixel i on ring 9 − (i ÷ 60), spoke i mod 60.
The same method works for decoding any unknown LED file:
1. Check the sizes: a constant header plus equal frames shows up as an exact formula.
2. Hex-dump the start and count the distinct byte values. A handful of values means raw data; 256 evenly spread values means compressed or encrypted.
3. Guess the header fields, then prove them with the size formula on every file.
4. Draw the frames as a picture (one row per frame). The right frame size gives clean stripes.
5. Try every plausible geometry and compare with a photo or video of the real display.
Still unknown:
• Header bytes 8–11 (35 and 22 in every file). One may be LED Edit's playback speed; for now the speed is set by hand, 25 fps by default.
• Which spoke is spoke 0 on the real wheel, and whether the strip is RGB or GRB. Set these with Rotate and Colour.
files are from this : https://www.youtube.com/watch?v=JuB4x3bWlXc
j java and arduino codes : https://github.com/stark9000/led-edit-tol-decoder


