Note: This information is intended only for hobbyist and educational purposes. I am not affiliated with Luminator in any way.
This document applies to newer signs such as the MAX 3000 and Horizon models, which use a 7-pin circular connector. I believe older signs use a similar protocol, but I haven't had a chance to study it.
This represents my best guess at how everything works, but no guarantees as to the accuracy. Rely on at your own risk.
If you'd prefer running code, my flipdot library (written in Rust) implements this protocol and provides a high-level interface for interacting with signs. For a concrete example that uses the sign as a clock, check out dotclock.
First things first: how to connect to the sign? It uses a TE Connectivity Circular Plastic Connector (CPC) model 788194-1 with the following pinout (looking at the connector on the sign):
||Main power (24V DC)|
I'm not sure what specific pin inserts they're using (there are a lot to choose from), but there are definitely two types. Pins 1 and 4 are beefier since they'll be carrying a few amps. In theory you could build your own mating connector, but I opted to buy original Luminator cables instead to avoid needing to figure that out.
At minimum, you need to connect main power, switched power, ground, and RS-485 +/−. Switched power turns the sign on, and LED power enables the LEDs/backlight (I guess I'm not sure what that does on a Horizon sign). Main power is definitely 24V DC. You're going to want a capable power supply; my sign draws ~2.5A to flip the dots, and another amp or so for the LEDs. I've seen conflicting information on whether switched power and LED power are 12V or 24V. They're basically just switches (draw negligible current), and I have run the sign using 24V on them, but that might not have been wise. You should connect the shield pin to ground in one place (probably the controller) to avoid ground loops.
Signs communicate over an RS-485 bus, configured as 19200 8-N-1 (19200 bps data rate, 8 data bits, no parity bit, one stop bit). Cheap USB adapters are readily available to connect to a PC. Each sign has an address to uniquely identify it on the bus, which is set via DIP switches.
Data is transmitted on the bus using the Intel HEX format (but not its semantics; we'll get into that in a second). The layout looks like this:
Here's an example:
\r\n (though for brevity, I'm going to omit
the carriage return/newline sequence from here on). This is a message of type
1 data byte:
Note that since we're spelling everything out in ASCII, one byte of numeric data requires two bytes to actually encode.
DataLen field describes how many such two-character data byte sequences are present.
Note that since it is represented as a single byte, the data length cannot exceed 255 (0xFF), though in practice
the Luminator protocol only sends 16-byte chunks anyway. If
DataLen is 0, there are
no data bytes, and
MsgType is followed directly by
The checksum is a longitudinal redundancy check
calculated on all numeric fields.
A handful of specific HEX message types make up the protocol:
|Send Data||Data offset||
||16 data bytes||Used to send configuration and pixel data in chunks|
|Data Chunks Sent||# of chunks||
||—||Number of 16-byte chunks sent via Send Data messages|
||Initially discovers signs on the bus and queries their state|
||Tells a sign to blank itself and shut down|
|Query State||Sign address||
||Queries a sign for its current state|
|Report State||Sign address||
||State||Sent by the sign in reply to Query State|
|Request Operation||Sign address||
||Operation||Requests that the sign perform an operation|
|Acknowledge Operation||Sign address||
||Operation||Sent by the sign in reply to Request Operation|
|Pixels Complete||Sign address||
||Notifies the sign to begin normal operation|
Note that the Address field often refers to the sign's bus address, but is repurposed for the SendData and Data Chunks Sent messages (as prior messages will have already indicated which sign should be paying attention to the data).
The sign's possible states (communicated via Report State) are as follows:
||The sign's initial state after power on or reset|
|Config in Progress||
||The sign is waiting to receive configuration data|
||The sign has successfully received and understood the config data|
||There was an error receiving the configuration data, or it was malformed|
|Pixels in Progress||
||The sign is waiting to receive pixel data|
||The sign has successfully received and understood the pixel data|
||There was an error receiving the pixel data, or it was malformed|
||The sign has loaded a page into memory and is ready to display it|
|Page Load in Progress||
||The sign is in the process of loading a page into memory|
||The sign has shown the last loaded page (so memory is empty)|
|Page Show in Progress||
||The sign is in the process of showing the last loaded page|
|Ready to Reset||
||The sign has begun the reset process and is ready to return to Unconfigured|
The following operations can be requested via Request Operation (and are acknowledged with a different value in an Acknowledge Operation):
||Prepare to receive config data; switch to Config in Progress|
||Prepare to receive pixel data; switch to Pixels in Progress|
|Show Loaded Page||
||Show the page currently loaded in memory; switch to Page Show in Progress|
|Load Next Page||
||Begin loading the next page into memory; switch to Page Load in Progress|
||Begin the reset process; switch to Ready to Reset|
||Finish resetting; switch to Unconfigured|
The system is driven by a controller, which in a real bus would be an ODK (Operator's Display and Keyboard). Signs don't initiate communication; they only respond to messages from the controller. Each sign maintains a state machine which is driven by the controller. The typical sequence goes like this:
Here's a concrete example of a typical exchange between a controller and a sign with address 3 comprising discovery, configuration, sending one page of pixels, then showing that page:
|Sender||Raw Message||Decoded Message|
||Hello to sign 3|
||Report State (Unconfigured)|
||Request Operation (Receive Config)|
||Acknowledge Operation (Receive Config)|
||Send Data at offset 0|
||Data Chunks Sent (1)|
||Report State (Config Received)|
||Request Operation (Receive Pixels)|
||Acknowledge Operation (Receive Pixels)|
||Send Data at offset 0|
||Send Data at offset 16|
||Send Data at offset 32|
||Send Data at offset 48|
||Send Data at offset 64|
||Send Data at offset 80|
||Data Chunks Sent (6)|
||Report State (Pixels Received)|
|Page Load/Show Cycle|
||Report State (Page Loaded)|
||Request Operation (Show Loaded Page)|
||Acknowledge Operation (Show Loaded Page)|
||Report State (Page Show in Progress)|
||Report State (Page Shown)|
The full state machine is as follows:
* The green box with the dashed border represents the set of "running" states. It is legal to request Receive Pixels from any of these states in order to update the set of pages.
** The light gray box with the dashed border represents any state. You can always request Query State (or Hello) to see what the current state is, and you can always start fresh by issuing a Start Reset. This is helpful to get back into a known state if you connect to a sign that's been already been running for a while. You can also blank the sign and shut down from any state, but I don't find this very useful since turning off switched power will also blank my sign.
To avoid overloading the sign, a small delay is recommended after each Send Data message, e.g. 30ms.
When loading or showing a page, the sign can take a second or more to complete the operation, depending on factors like how many dots need to flip. While checking if that operation has completed (by polling Query State messages) before moving onto the next one, you may want to insert a delay (say 100ms or so) after each Page Load/Show in Progress response in order to avoid spamming the sign with status requests.
It's not exactly clear to me if this data actually configures the sign in some way, is verified by the sign to ensure the controller has properly identified it, or something else entirely. I've haven't wanted to risk doing any experiments along these lines on my sign. Regardless, you need to send this sign-specific block of 16 bytes as part of establishing communication. The first byte indicates the family of sign, and different families have different formats for the rest of the configuration block.
Note: I'm going to describe sign dimensions using the computer graphics convention of width × height since that's the direction I'm approaching this from.
MAX 3000 signs have an initial byte of
ID is a
unique ID for the particular sign type within the family, e.g. the 90 × 7 side sign has ID
Byte 2 seems to always be zero, and byte 3 is unknown.
H is the height in pixels,
W1 + W2 + W3 + W4 is the total width.
the number of bits per column (either 8 or 16). The remaining bytes appear unused and are always zero.
Horizon signs have an initial byte of
ID is a
unique ID for the particular sign type within the family, e.g. the 96 × 8 side sign has ID
Byte 2 seems to always be zero, and bytes 3 and 4 are unknown.
H is the height
in pixels, and
W is the width. The next four bytes seem to indicate the arrangement
of sub-panels to create the final width:
A1 × B1
A2 × B2. Byte 12 is unknown (generally zero but
0x04 for the 40 ×
12 dash sign). The remaining bytes appear unused and are always zero.
These are the configuration blocks for all the signs I'm aware of:
|MAX 3000||Front||112 × 16||
|MAX 3000||Front||98 × 16||
|MAX 3000||Side||90 × 7||
|MAX 3000||Rear||23 × 10||
|MAX 3000||Rear||30 × 10||
|MAX 3000||Dash||30 × 7||
|Horizon||Front||160 × 16||
|Horizon||Front||140 × 16||
|Horizon||Side||96 × 8||
|Horizon||Rear||48 × 16||
|Horizon||Dash||40 × 12||
When sending pixel data, the following format is used (split into 16-byte chunks per the overall protocol). Note that the offset in the SendData message is relative to the current page, not the total amount of data to send.
The purpose of the 4-byte header isn't clear. The first byte seems to be a page number or ID, though I don't think
it affects sign operation. The other bytes are most frequently
0x10 0x00 0x00.
The interpretation of the data bytes in Figure 6 depends on the dimensions of the sign they're meant for. For example, consider a 7-pixel tall sign and a 16-pixel tall sign. The former needs only one byte for each column, while the latter needs two, so the same data will be interpreted differently depending on the sign:
The bytes are arranged with their least significant bits toward the top of the sign, proceeding top down and then left to right. If the number of pixels in a column isn't a multiple of 8, the extra bits are ignored to maintain alignment.