On the back of my previous post I've modified the LINQPad script to allow me to define a conversation with the display and set it up with the observed conversation from last time.
static int baudRate = 9600;
static int dataBits = 8;
void Main()
{
using (var port = new SerialPort("COM3", baudRate, Parity.None, dataBits, StopBits.One))
{
try {
port.Open();
port.Send(2, 49, 3, 225, 213, 19);
port.Expect(2, 49, 4, 224, 0, 144, 26);
port.Send(2, 49, 39, 229, 229, 32, 156, 170, 57, 228, 32, 40, 166, 76, 97, 110, 103, 115, 116, 111, 110, 101, 32, 66, 117, 115, 105, 110, 101, 115, 115, 228, 40, 78, 166, 80, 97, 114, 107, 254, 152, 188);
port.Expect(2, 49, 3, 232, 68, 1);
port.Close();
}
catch (Exception e) {
e.Dump();
}
}
}
static class Extensions {
public static string ToDebugString(this IEnumerable<byte> bytes) {
return string.Join(", ", bytes.Select(x=>x.ToString()).ToArray());
}
public static void Send(this SerialPort port, params byte[] data) {
port.Write(data, 0, data.Length);
data.ToDebugString().Dump("Sent");
}
public static void Expect(this SerialPort port, params byte[] expectedData) {
var timeout = TimeSpan.FromMilliseconds(500);
var received = new List<byte>();
var dataReceived = new SerialDataReceivedEventHandler((sender, args) => {
while (port.BytesToRead != 0) {
received.Add((byte)port.ReadByte());
}
});
try {
port.DataReceived += dataReceived;
Thread.Sleep(timeout);
if (received.SequenceEqual(expectedData)) {
received.ToDebugString().Dump("Received");
}
else {
throw new DataException("Expected { " + expectedData.ToDebugString() + " } but got { " + received.ToDebugString() + " }");
}
}
finally {
port.DataReceived -= dataReceived;
}
}
}
This will allow me to quickly test ideas about the protocol to see how the display responds.
Idea 1 - Addressing Dots/Pixels
I'm assuming that the protocol sends data about individual pixels and that all the brains behind converting text into pixels sits in the controller, which my code is replacing. In order to address each pixel I would expect some form of xy coordinates to be sent so I'm going to think about how that might be encoded in the command.
This display is 8 dots tall so encoding a y-coordinate could use 3 bits. It's also 90 dots wide, to encode the the x-coordinate would take 7 bits. This makes 10 bits, perhaps an 11th bit to indicate whether the dot should be on or off, meaning each pixel would be 2 bytes long. Turning the pixel at (42,3) could look something like this:
0 0 1 0 1 0 0 1 | 0 0 0 0 0 1 0 1
------x------ | --y-- on/off
Not only is that lot of wasted bits, it also means that just the first L on the screen would take 34 bytes to communicate yet we only have 40 bytes in the command after the initial 2 display address bytes. Interestingly the 3rd byte is 39, could this be representing the size of the message that follows? I'm going to assume yes for now, I can always backtrack.
Another possibility for addressing individual pixels is for it to send a full sweep of data for the whole display. So one byte for every column, with the 8 bits representing each row. This would be more efficient with no wasted bits, however it would still require 90 bytes in the message.
Idea 2 - Hack, Prod, Hack
I don't have another idea, so I'm going to try changing numbers in the command to see what the outcome is.
I choose 101, I think because it's palindromic, I like palindromes. I'll change it to 121. Sending this modified command results in no response from the display and no change in the dots on show, this is not what I expected or wanted. I was hoping for some form of error code if I sent a bad command. Reverting my change and running the original to make sure I've not broken something elsewhere results in no data being returned after the first polling command. But wait...

The display has updated and a character has changed!
This is interesting, I'm assuming that 2, 49, 3, 225, 213, 19 somehow made the modified command I sent take effect. Let's change some more characters and send that extra message afterwards.
port.Send(2, 49, 3, 225, 213, 19);
port.Expect(2, 49, 4, 224, 0, 144, 26);
port.Send(2, 49, 39, 229, 229, 32, 156, 170, 57, 228, 32, 40, 166, 78, 98, 110, 103, 115, 116, 141, 110, 121, 32, 66, 117, 115, 105, 110, 101, 115, 115, 228, 40, 78, 166, 80, 97, 114, 107, 254, 152, 188);
port.Send(2, 49, 3, 225, 213, 19);
port.Expect(2, 49, 3, 232, 68, 1);

ASCII and ye shall receive
It's hit me. These are ASCII codes.
I'm running the original command through a bit of conversion like so:
data.Select(b => new {
dec = b,
hex = b.ToString("X2"),
bin = Convert.ToString(b, 2).PadLeft(8, '0'),
ascii = ASCIIEncoding.ASCII.GetString(new []{ b }),
}).Dump();
| dec |
hex |
bin |
ascii |
| 2 |
02 |
00000010 |
□ |
| 49 |
31 |
00110001 |
1 |
| 39 |
27 |
00100111 |
' |
| 229 |
E5 |
11100101 |
? |
| 229 |
E5 |
11100101 |
? |
| 32 |
20 |
00100000 |
|
| 156 |
9C |
10011100 |
? |
| 170 |
AA |
10101010 |
? |
| 57 |
39 |
00111001 |
9 |
| 228 |
E4 |
11100100 |
? |
| 32 |
20 |
00100000 |
|
| 40 |
28 |
00101000 |
( |
| 166 |
A6 |
10100110 |
? |
| 76 |
4C |
01001100 |
L |
| dec |
hex |
bin |
ascii |
| 97 |
61 |
01100001 |
a |
| 110 |
6E |
01101110 |
n |
| 103 |
67 |
01100111 |
g |
| 115 |
73 |
01110011 |
s |
| 116 |
74 |
01110100 |
t |
| 111 |
6F |
01101111 |
o |
| 110 |
6E |
01101110 |
n |
| 101 |
65 |
01100101 |
e |
| 32 |
20 |
00100000 |
|
| 66 |
42 |
01000010 |
B |
| 117 |
75 |
01110101 |
u |
| 115 |
73 |
01110011 |
s |
| 105 |
69 |
01101001 |
i |
| 110 |
6E |
01101110 |
n |
| dec |
hex |
bin |
ascii |
| 101 |
65 |
01100101 |
e |
| 115 |
73 |
01110011 |
s |
| 115 |
73 |
01110011 |
s |
| 228 |
E4 |
11100100 |
? |
| 40 |
28 |
00101000 |
( |
| 78 |
4E |
01001110 |
N |
| 166 |
A6 |
10100110 |
? |
| 80 |
50 |
01010000 |
P |
| 97 |
61 |
01100001 |
a |
| 114 |
72 |
01110010 |
r |
| 107 |
6B |
01101011 |
k |
| 254 |
FE |
11111110 |
? |
| 152 |
98 |
10011000 |
? |
| 188 |
BC |
10111100 |
? |
The text is quite clear in there, 9 Langstone Business Park. There are spaces between the first two words but some sort of control characters between the rest. Before both the number 9 and the text there is a 32 followed by 2 characters. I think these 2 characters indicate the beginning of some segment of the route and the 229, 229, 32 is the type of command, which is displaying a bus route on the display.
Modifying the Message
I want to display mattscode.com on this, my current thinking is summarised as:
- 2 bytes - display address
- 1 byte - command size
- 3 bytes - type of command
- 2 bytes - control characters indicating start of some command section
- 13 bytes - ASCII string
- 3 bytes - control characters indicating end of command
Putting this together I have:
port.Send(
2, 49, // display address
21, // command size
229, 229, 32, // type of command
40, 166, // start of command section
109, 97, 116, 116, 115, 99, 111, 100, 101, 46, 99, 111, 109, // ASCII string
254, 152, 188 // end of command
);

Excellent!
Next Step
I've now got the ability to write my own text and I know the make-up of a command. Next I need to work out what the different control characters mean for different command sections and why the display resets after 50 seconds.
I switched over to the 90x8 display and flicked through some of the different destinations on the controller. I noticed that they were all showing only the bottom half of the characters, so I'm making the assumption that they were intended for a larger display. I happened upon one particular destination, however, that displayed in the correct line-height - Langstone Business Park.

That's something else that needs figuring out now, how does it determine the line-height to use.
Note
I didn't know the baud rate used so I had to guess. I started off at the high ones and repeated a failed version of the analysis process outlined below for each until I got it right. There any many assumptions in this and it took a lot of trial-and-error and backtracking, I've kept only the details of my work with the correct baud rate in this post as the rest is significantly less interesting.
Sniffing
The USB RS-485 dongle arrived. It presents itself to the PC as a serial port so writing a bit of code to read the data is a fairly straightforward task.
static int baudRate = 9600;
static int dataBits = 8;
void Main()
{
using (var port = new SerialPort("COM3", baudRate, Parity.None, dataBits, StopBits.One))
{
var bytes = new List<int>();
port.DataReceived += (sender, args) => {
while (port.BytesToRead != 0) {
bytes.Add(port.ReadByte());
}
};
port.Open();
Thread.Sleep(TimeSpan.FromSeconds(20));
port.Close();
string.Join(", ", bytes.Select(x=>x.ToString()).ToArray()).Dump();
}
}
I ran the above in LINQPad and then plugged the power in to the display and controller. The resulting data dump was:
0, 0, 2, 49, 3, 225, 213, 19, 2, 49, 3, 225, 213, 19, 2, 49, 3, 225, 213, 19, 2, 49, 3, 225, 213, 19, 2, 49, 3, 225, 213, 19, 2, 49, 3, 225, 213, 19, 2, 49, 3, 225, 213, 19, 2, 49, 4, 224, 0, 144, 26, 2, 49, 39, 229, 229, 32, 156, 170, 57, 228, 32, 40, 166, 76, 97, 110, 103, 115, 116, 111, 110, 101, 32, 66, 117, 115, 105, 110, 101, 115, 115, 228, 40, 78, 166, 80, 97, 114, 107, 254, 152, 188, 2, 49, 3, 232, 68, 1, 2, 50, 3, 225, 240, 11, 2, 50, 3, 225, 240, 11, 2, 50, 3, 225, 240, 11, 2, 51, 3, 225, 10, 3, 2, 51, 3, 225, 10, 3, 2, 51, 3, 225, 10, 3
One thing to bear in mind is that the 2-wire RS-485 protocol is half-duplex. This means that data will go one way first, then the response will be sent on the same 2 wires. As I'm reading all data across those 2 wires there will be no way to determine whether any given byte has come from the display or the controller.
What I'm looking for in the above is any repeated patterns that might indicate the start of a command. I'm going to assume that ignoring 0's is okay for the time being and that they just indicate a break in the messaging. We'll see how that pans out.
2, 49, 3, 225, 213, 19 is repeated at the beginning several times, there was also a period just after power-up where the display sweeps across all of the pixels turning them on and off again. I'm wondering if these repeated bytes at the start are the controller polling for an attached display, if this is the case then for the controller to find one it's going to have to receive a message back from the display. Working with these assumptions the data can be split up a bit:
0, 0
2, 49, 3, 225, 213, 19
2, 49, 3, 225, 213, 19
2, 49, 3, 225, 213, 19
2, 49, 3, 225, 213, 19
2, 49, 3, 225, 213, 19
2, 49, 3, 225, 213, 19
2, 49, 3, 225, 213, 19
2, 49, 4, 224
0
144, 26, 2, 49, 39, 229, 229, 32, 156, 170, 57, 228, 32, 40, 166, 76, 97, 110, 103, 115, 116, 111, 110, 101, 32, 66, 117, 115, 105, 110, 101, 115, 115, 228, 40, 78, 166, 80, 97, 114, 107, 254, 152, 188, 2, 49, 3, 232, 68, 1, 2, 50, 3, 225, 240, 11, 2, 50, 3, 225, 240, 11, 2, 50, 3, 225, 240, 11, 2, 51, 3, 225, 10, 3, 2, 51, 3, 225, 10, 3, 2, 51, 3, 225, 10, 3
That last string of bytes could well be the command to display "Langstone Busin", so I want to try sending it to the display.
Replaying Commands
Here's a bit of code that should send the command through to the display.
static int baudRate = 9600;
static int dataBits = 8;
void Main()
{
using (var port = new SerialPort("COM3", baudRate, Parity.None, dataBits, StopBits.One))
{
var command = new byte[] { 144, 26, 2, 49, 39, 229, 229, 32, 156, 170, 57, 228, 32, 40, 166, 76, 97, 110, 103, 115, 116, 111, 110, 101, 32, 66, 117, 115, 105, 110, 101, 115, 115, 228, 40, 78, 166, 80, 97, 114, 107, 254, 152, 188, 2, 49, 3, 232, 68, 1, 2, 50, 3, 225, 240, 11, 2, 50, 3, 225, 240, 11, 2, 50, 3, 225, 240, 11, 2, 51, 3, 225, 10, 3, 2, 51, 3, 225, 10, 3, 2, 51, 3, 225, 10, 3, };
port.Open();
port.Write(command, 0, command.Length);
port.Close();
}
}
With the controller disconnected the display is powered up. Once it's finished its start-up routine I run the LINQPad script to send the command across. It worked, but I'm not convinced that all of those bytes are part of the command. As there has already been a command/response witnessed, I suspect that only part of this is the command and the rest is a response, perhaps with further command/response pairs. I also notice that after a short period of time (50 seconds, I timed it) the display resets itself to an blank state.
Rather than blindly cut bytes off and send the command again I'm looking for patterns towards the end. I notice two, actually: 2, 50, 3, 225, 240, 11 and 2, 51, 3, 225, 10, 3. Splitting the command down might look like this:
144, 26, 2, 49, 39, 229, 229, 32, 156, 170, 57, 228, 32, 40, 166, 76, 97, 110, 103, 115, 116, 111, 110, 101, 32, 66, 117, 115, 105, 110, 101, 115, 115, 228, 40, 78, 166, 80, 97, 114, 107, 254, 152, 188, 2, 49, 3, 232, 68, 1
2, 50, 3, 225, 240, 11
2, 50, 3, 225, 240, 11
2, 50, 3, 225, 240, 11
2, 51, 3, 225, 10, 3
2, 51, 3, 225, 10, 3
2, 51, 3, 225, 10, 3
Substituting that trimmed-down first line into the LINQPad script still results in the message being shown so I'm on the right track here. Sticking with the assumption that the display is going to send some sort of response back I'm going to merge those 2 scripts so that any response is captured.
static int baudRate = 9600;
static int dataBits = 8;
void Main()
{
using (var port = new SerialPort("COM3", baudRate, Parity.None, dataBits, StopBits.One))
{
var bytes = new List<int>();
port.DataReceived += (sender, args) => {
while (port.BytesToRead != 0) {
bytes.Add(port.ReadByte());
}
};
var command = new byte[] { 144, 26, 2, 49, 39, 229, 229, 32, 156, 170, 57, 228, 32, 40, 166, 76, 97, 110, 103, 115, 116, 111, 110, 101, 32, 66, 117, 115, 105, 110, 101, 115, 115, 228, 40, 78, 166, 80, 97, 114, 107, 254, 152, 188, 2, 49, 3, 232, 68, 1, };
port.Open();
port.Write(command, 0, command.Length);
Thread.Sleep(TimeSpan.FromSeconds(5));
port.Close();
string.Join(", ", bytes.Select(x=>x.ToString()).ToArray()).Dump();
}
}
The response from this is 119, 13, 232, 68, 1 but this doesn't match anything in the original data so I'm going to remove a few more bytes from the message to see it starts looking better. I notice that 2, 49 is in there towards the end. These 2 bytes are at the start of the polling patterns in the original data and also the start of the response from the display. Another assumption to join the party: 2, 49 is the address of the display on the RS-485 bus and it's prefixed to messages to indicate the sender or intended recipient. This being the current assumption I'm going to remove that possible response from the command I'm sending. I'm also going to remove the first 2 bytes as 2, 49 is near the start too and will indicate the start of the actual command. We're now looking at:
2, 49, 39, 229, 229, 32, 156, 170, 57, 228, 32, 40, 166, 76, 97, 110, 103, 115, 116, 111, 110, 101, 32, 66, 117, 115, 105, 110, 101, 115, 115, 228, 40, 78, 166, 80, 97, 114, 107, 254, 152, 188
2, 49, 3, 232, 68, 1
Sending the first line as the command results in the display showing the desired message and also a response of 2, 49, 3, 232, 68, 1
Next Step
I've now got a command that works, the next step is to analyse this to determine its make-up and what the protocol might be, but it's late and I'm tired so I'm going to leave that for tomorrow.