Compare commits

...

3 Commits

Author SHA1 Message Date
22a0c86d15 Add information on (mostly reversed) protocol
Also fixes the basic functionality of sending a text to the sign with
the default parameters.
2024-04-01 01:19:30 +02:00
55cc608454 Add Python server script for reverse engineering 2024-04-01 01:17:50 +02:00
09e8e75edc Improve webserver 2024-03-31 20:53:33 +02:00
3 changed files with 362 additions and 19 deletions

View File

@ -1,18 +1,310 @@
#pragma once
enum Brightness {
Normal,
Bright,
// TODO: reverse engineer builtin and custom bitmaps
// seems like our sign listens to messages on this address
// it is also the software's default value
static constexpr auto SIGN_ADDRESS = 128;
enum class FontType {
Font5x6Short = 'q',
Font5x11ShortAndWide = 'r',
Font7x6Default = 's',
Font7x11Wide = 't',
Font7x9 = 'u',
Font7x17ExtraWide = 'v',
FontSmallFont = 'w',
};
void sendTextToSign(String text, Brightness brightness = Bright) {
// debugging
Serial.print("Sending text \"");
Serial.print(text);
Serial.println("\"");
enum class Brightness {
Normal = 'a',
Bright = 'b',
};
// TODO: find out how the brightness attribute in the LED sign is encoded in the UART signal
Serial1.print("f01A\\s");
Serial1.print(text);
Serial1.print("\r\r\r");
enum class Method {
Cyclic = 'A',
Immediate = 'B',
OpenFromRight = 'C',
OpenFromLeft = 'D',
OpenFromCenter = 'E',
OpenToCenter = 'F',
CoverFromCenter = 'G',
CoverFromRight = 'H',
CoverFromLeft = 'I',
CoverToCenter = 'J',
ScrollUp = 'K',
ScrollDown = 'L',
InterlaceToCenter = 'M',
InterlaceCover = 'N',
CoverUp = 'O',
CoverDown = 'P',
ScanLine = 'Q',
Explode = 'R',
PacMan = 'S',
FallAndStack = 'T',
Shoot = 'U',
Flash = 'V',
Random = 'W',
SlideIn = 'X',
};
// macros are literal characters prefixed by ^
enum class Macros {
CurrentTime12hWithoutAmPm = 'D',
CurrentTime12hWithAmPm = 'E',
CurrentTime24h = 'F',
CurrentDateMmDdYyyy = 'A',
CurrentDateYyyy_Mm_Dd = 'B',
CurrentDateMm_Dd_Yyyy = 'C',
CurrentDateDd_Mm_Yyyy = 'a',
Beep1 = 'q',
Beep2 = 'r',
Beep3 = 's',
};
// symbols should be inserted as string representations of their numeric value prefixed by ^
enum class Symbols {
Sun = 66,
CloudOrWind = 67,
UmbrellaRaining = 68,
WallClock = 69,
Telephone = 70,
Glasses = 71,
Faucet = 72,
RocketOrSyringeOrSo = 73,
WhatTheHell = 74,
Key = 75,
Shirt = 76,
Helicopter = 77,
Car = 78,
Tank = 79,
House = 80,
Teapot = 81,
Trees = 82,
Duck = 83,
Scooter = 84,
Bicycle = 85,
Crown = 86,
AppleOrSo = 87,
ArrowRight = 88,
ArrowLeft = 89,
ArrowDownLeft = 90,
ArrowUpLeft = 91,
Cup = 92,
Chair = 93,
HighHeel = 94,
PrizeCup = 95,
};
// cartoons are represented as these characters prefixed by ^
enum class Cartoon {
MerryXMas = 'i',
HappyNewYear = 'j',
FourthOfJuly = 'k',
HappyEaster = 'l',
HappyHalloween = 'm',
DontDrinkAndDrive = 'n',
NoSmoking = 'o',
Welcome = 'p',
};
// the descriptions originate from the UTF-16 specification, https://www.ssec.wisc.edu/~tomw/java/unicode.html
// the extended charset starts where ASCII ends
enum class ExtendedChar {
// Ç
LatinCapitalLetterCWithCedilla = 0x80,
// ü
LatinSmallLetterUWithDiaeresis = 0x81,
// é
LatinSmallLetterEWithAcute = 0x82,
// â
LatinSmallLetterAWithCircumflex = 0x83,
// ä
LatinSmallLetterAWithDiaeresis = 0x84,
// á
LatinSmallLetterAWithGrave = 0x85,
// å
LatinSmallLetterAWithRingAbove = 0x86,
// ç
LatinSmallLetterCWithCedilla = 0x87,
// ê
LatinSmallLetterEWithCircumflex = 0x88,
// ë
LatinSmallLetterEWithDiaeresis = 0x89,
// è
LatinSmallLetterEWithGrave = 0x8a,
// Ï
LatinCapitalLetterIWithDiaeresis = 0x8b,
// Î
LatinCapitalLetterIWithCircumflex = 0x8c,
// Ì
LatinCapitalLetterIWithGrave = 0x8d,
// Ä
LatinCapitalLetterAWithDiaeresis = 0x8f,
// Å
LatinCapitalLetterAWithRingAbove = 0x8f,
// É
LatinCapitalLetterEWithAcute = 0x90,
// æ
LatinSmallLetterAe = 0x91,
// Æ
LatinCapitalLetterAe = 0x92,
// ô
LatinSmallLetterOWithCircumflex = 0x93,
// ö
LatinSmallLetterOWithDiaeresis = 0x94,
// ò
LatinSmallLetterOWithGrave = 0x95,
// û
LatinSmallLetterUWithCircumflex = 0x96,
// ù
LatinSmallLetterUWithGrave = 0x97,
// ÿ
LatinSmallLetterYWithDiaeresis = 0x98,
// Ö
LatinCapitalLLetterOWithDiaeresis = 0x99,
// Ü
LatinCapitalLetterUWithDiaeresis = 0x9a,
// ¢
CentSign = 0x9b,
// £
PoundSign = 0x9c,
// ¥
YenSign = 0x9d,
// urm...
UnknownPCurrencySign = 0x9e,
// ƒ
LatinSmallLetterFWithHook = 0x9f,
// á
LatinSmallLetterAWithAcute = 0xa0,
// Í
LatinCapitalLetterIWithAcute = 0xa1,
// Ó
LatinCapitalLetterOWithAcute = 0xa2,
// ú
LatinSmallLetterUWithAcute = 0xa3,
// ñ
LatinSmallLetterNWithTilde = 0xa4,
// Ñ
LatinCapitalLetterNWithTilde = 0xa5,
// a̱
LatinSmallLetterAWithMacronBelow = 0xa6,
// couldn't find that one in the UTF-16 table
LatinSmallLetterOWithMacronBelow = 0xa7,
// ¿
InvertedQuestionMark = 0xa8,
// you can probably imagine what it'll look like
LatinCapitalLetterZWithRingAbove = 0xa9,
// Ź
LatinCapitalLetterZWithAcute = 0xaa,
// ń
LatinSmallLetterNWithAcute = 0xab,
// some weird L with a diagonal line
AnotherWeirdSign = 0xac,
// Ś or ś, can't tell for sure, let's assume capital
LatinCapitalLetterSWithAcute = 0xab,
// Ć
LatinCapitalLetterCWithAcute = 0xac,
// ȩ
LatinSmallLetterEWithCedilla = 0xad,
// well, you can imagine what it'll look like
LatinSmallLetterAWithCedilla = 0xb0,
// €
EuroSign = 0xb1,
// E with some vertical line below
LatinCapitalLetterEWithSomeLineBelow = 0xb3,
// a with some vertical line below
LatinSmallLetterAWithSomeWeirdLineBelow = 0xb4,
// α
GreekSmallLetterAlpha = 0xe0,
// β
GreekSmallLetterBeta = 0xe1,
// Γ
GreekCapitalLetterGamma = 0xe2,
// π
GreekSmallLetterPi = 0xe3,
// Σ
GreekCapitalLetterSigma = 0xe4,
// σ
GreekSmallLetterSigma = 0xe5,
// μ
GreekSmallLetterMu = 0xe6,
// τ
GreekSmallLetterTau = 0xe7,
// Φ
GreekCapitalLetterPhi = 0xe8,
// θ
GreekSmallLetterTheta = 0xe9,
// Ω
GreekCapitalLetterOmega = 0xea,
// δ
GreekSmallLetterDelta = 0xeb,
// ∞
Infinity = 0xec,
// φ
GreekSmallLetterPhi = 0xed,
};
/*
* Message syntax:
* separator: ~
* address (int string): 128
* separator: ~
* random string(?): f01
* method (single char)
* format string (see below)
* end of message: \r\r\r
*
* the format string contains the message along with control characters that define some of the formatting
* note: we have not tested the UTF-8 capabilities, so far only ASCII
* it supports multiple lines (yet to be tested)
* you can also insert macros (e.g., current time/date), symbols (e.g., a sun) and cartoons
* the specials are preceded by a caret following the respective character
* then, there is an extended charset that supports a small subset of UTF-16 (yes, that's right, it's not just UTF-8)
* it can further contain information that should be used from their occurrence on
* the formatting escape sequences are prefixed by a literal backspace following a char that represents the format
* brightness is \<ab>, speed is Y<1-8>, "stay time" is \Z<1-8>, font type is \[A-X]
* new lines are introduced by a single \r, the end of the message is marked by three of them
*
* default formatting:
* - method: cyclic
* - font type: 7x6
* - speed: 5
* - stay time: 4
* - brightness: bright
*/
void sendTextToSign(
const String &text,
const Method method = Method::Cyclic,
const FontType fontType = FontType::Font7x6Default,
const int speed = 5,
const int stayTime = 4,
const Brightness brightness = Brightness::Bright
) {
if (speed < 0 || speed > 8) {
Serial.println("Invalid speed: " + String(speed, DEC));
return;
}
if (stayTime < 0 || stayTime > 8) {
Serial.println("Invalid stayTime: " + String(stayTime, DEC));
return;
}
Serial.println("Sending text: \"" + text + "\"");
String toSend = "~" + String(SIGN_ADDRESS, DEC) + "~f01" + (char) method + "\\" + (char) brightness + "\\Y" + String(speed, DEC) + "\\Z" + String(stayTime, DEC) + "\\" + (char) fontType + text + "\r\r\r";
Serial1.print(toSend);
Serial.println("Text sent to device: " + toSend);
Serial.print("Hex dump: ");
for (size_t i = 0; i < toSend.length(); ++i) {
auto hexChar = String(toSend[i], HEX);
if (hexChar.length() == 1) {
hexChar = '0' + hexChar;
}
Serial.print(hexChar + ' ');
}
Serial.println();
}

View File

@ -28,11 +28,6 @@ namespace {
<input type="text" name="text">
</div>
<div>
<label for="bright">Bright mode</label>
<input type="checkbox" name="bright">
</div>
<div>
<button>Send</button>
</div>
@ -45,7 +40,14 @@ namespace {
}
void handleFormSend() {
sendTextToSign("HELLO FORM");
String text = webServer.arg("text");
if (text.isEmpty()) {
webServer.send(404, "text/plain", "empty text");
return;
}
sendTextToSign(text);
// redirect back to root page
webServer.sendHeader("Location", "/");
@ -54,7 +56,14 @@ namespace {
// RESTful API endpoint -- returns a proper status code rather than a redirect
void handleApiSend() {
sendTextToSign("HELLO API");
String text = webServer.arg("text");
if (text.isEmpty()) {
webServer.send(404, "text/plain", "empty text");
return;
}
sendTextToSign(text);
webServer.send(200, "text/plain", "ok");
}
}

42
tools/re-server.py Normal file
View File

@ -0,0 +1,42 @@
"""
Simple socket server used to reverse engineer the messaging protocol the bundled software uses to send data to the
LED sign.
The server just dumps incoming messages in a hexdump-like format as well as an easy to read Python bytestring.
"""
import asyncio
import string
async def handle_client(reader, writer):
message = await reader.read(255)
hex_chars = [hex(j)[2:].zfill(2) for j in message]
ascii_chars = []
for j in message:
char = chr(j)
if char in (string.ascii_letters + string.digits + string.punctuation):
ascii_chars.append(char)
else:
ascii_chars.append(".")
print("Message received: ", end="")
print(" ".join(hex_chars), end=" | ")
print("".join(ascii_chars), end=" | ")
print("".join(repr(message)))
writer.close()
async def run_server():
server = await asyncio.start_server(handle_client, '', 3000)
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(run_server())