Add Wi-Fi LED matrix message board
This commit is contained in:
parent
ff38ca8c19
commit
de51255f58
5 changed files with 399 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,6 +10,7 @@ build/
|
||||||
.arduinoIDE/
|
.arduinoIDE/
|
||||||
.arduino15/
|
.arduino15/
|
||||||
sketch.yaml
|
sketch.yaml
|
||||||
|
arduino_secrets.h
|
||||||
|
|
||||||
# Editors and operating systems
|
# Editors and operating systems
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,9 @@ sketches/
|
||||||
Board-specifik dokumentation, forbindelser og nødvendige biblioteker beskrives
|
Board-specifik dokumentation, forbindelser og nødvendige biblioteker beskrives
|
||||||
i en README ved den enkelte sketch.
|
i en README ved den enkelte sketch.
|
||||||
|
|
||||||
|
## Sketches
|
||||||
|
|
||||||
|
- `led_matrix_demo` – animationer til UNO R4 WiFi-matrixen
|
||||||
|
- `led_matrix_scrolling_text` – fast løbetekst på matrixen
|
||||||
|
- `wifi_matrix_message_board` – browserstyret løbetekst via Wi-Fi
|
||||||
|
|
||||||
|
|
|
||||||
55
sketches/wifi_matrix_message_board/README.md
Normal file
55
sketches/wifi_matrix_message_board/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Wi-Fi LED Matrix Message Board
|
||||||
|
|
||||||
|
Et browserstyret message board til den indbyggede 12×8 LED-matrix på Arduino
|
||||||
|
UNO R4 WiFi. Websiden kan ændre både besked og scrollhastighed, mens matrixen
|
||||||
|
kører asynkront.
|
||||||
|
|
||||||
|
## Hurtig start uden lokal Wi-Fi-konfiguration
|
||||||
|
|
||||||
|
Uden en `arduino_secrets.h` opretter boardet automatisk sit eget netværk:
|
||||||
|
|
||||||
|
- Netværk: `Arduino-Matrix`
|
||||||
|
- Adgangskode: `matrix123`
|
||||||
|
- Webadresse: `http://192.168.4.1`
|
||||||
|
|
||||||
|
Forbind en telefon eller computer til netværket, og åbn adressen i en browser.
|
||||||
|
Enheden kan advare om, at netværket ikke har internetforbindelse; det er
|
||||||
|
forventet.
|
||||||
|
|
||||||
|
## Brug boardet på dit eksisterende Wi-Fi
|
||||||
|
|
||||||
|
1. Kopiér `arduino_secrets.example.h` til `arduino_secrets.h` i samme mappe.
|
||||||
|
2. Indsæt SSID og adgangskode til et 2.4 GHz-netværk.
|
||||||
|
3. Kompilér og upload igen.
|
||||||
|
4. Åbn den IP-adresse, der udskrives ved 115200 baud i Serial Monitor.
|
||||||
|
|
||||||
|
`arduino_secrets.h` er ignoreret af Git. Hvis forbindelsen ikke kan oprettes
|
||||||
|
inden for 15 sekunder, falder sketchen automatisk tilbage til access point-mode.
|
||||||
|
|
||||||
|
Matrixfonten er ASCII-baseret. Danske bogstaver konverteres automatisk:
|
||||||
|
`æ → ae`, `ø → oe` og `å → aa`. Beskeder begrænses til 64 tegn.
|
||||||
|
|
||||||
|
## Afhængigheder
|
||||||
|
|
||||||
|
- `WiFiS3`, inkluderet i Arduino UNO R4 board-pakken
|
||||||
|
- `Arduino_LED_Matrix`, inkluderet i board-pakken
|
||||||
|
- `ArduinoGraphics` 1.1.5 eller nyere
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
arduino-cli lib install ArduinoGraphics
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kompilering
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
arduino-cli compile --fqbn arduino:renesas_uno:unor4wifi sketches/wifi_matrix_message_board
|
||||||
|
```
|
||||||
|
|
||||||
|
## Upload
|
||||||
|
|
||||||
|
Upload først efter godkendelse:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
arduino-cli upload --port COM5 --fqbn arduino:renesas_uno:unor4wifi sketches/wifi_matrix_message_board
|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Copy this file to arduino_secrets.h and enter your 2.4 GHz Wi-Fi details.
|
||||||
|
// arduino_secrets.h is ignored by Git and must never be committed.
|
||||||
|
#define SECRET_SSID "your-network-name"
|
||||||
|
#define SECRET_PASS "your-network-password"
|
||||||
|
|
||||||
330
sketches/wifi_matrix_message_board/wifi_matrix_message_board.ino
Normal file
330
sketches/wifi_matrix_message_board/wifi_matrix_message_board.ino
Normal file
|
|
@ -0,0 +1,330 @@
|
||||||
|
#include <ArduinoGraphics.h>
|
||||||
|
#include <Arduino_LED_Matrix.h>
|
||||||
|
#include <TextAnimation.h>
|
||||||
|
#include <WiFiS3.h>
|
||||||
|
|
||||||
|
#if __has_include("arduino_secrets.h")
|
||||||
|
#include "arduino_secrets.h"
|
||||||
|
#define HAS_WIFI_SECRETS 1
|
||||||
|
#else
|
||||||
|
#define HAS_WIFI_SECRETS 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
constexpr char AP_SSID[] = "Arduino-Matrix";
|
||||||
|
constexpr char AP_PASSWORD[] = "matrix123";
|
||||||
|
constexpr uint8_t MAX_MESSAGE_LENGTH = 64;
|
||||||
|
constexpr uint16_t DEFAULT_SCROLL_SPEED_MS = 75;
|
||||||
|
constexpr uint16_t REPEAT_PAUSE_MS = 900;
|
||||||
|
|
||||||
|
// Font_5x7 uses six horizontal pixels per character including spacing. The
|
||||||
|
// extra frames let the text enter from and leave through the matrix edges.
|
||||||
|
constexpr size_t ANIMATION_FRAMES = MAX_MESSAGE_LENGTH * 6 + 24;
|
||||||
|
TEXT_ANIMATION_DEFINE(scrollAnimation, ANIMATION_FRAMES)
|
||||||
|
|
||||||
|
ArduinoLEDMatrix matrix;
|
||||||
|
WiFiServer server(80);
|
||||||
|
|
||||||
|
String message = "Arduino message board er klar";
|
||||||
|
uint16_t scrollSpeedMs = DEFAULT_SCROLL_SPEED_MS;
|
||||||
|
bool accessPointMode = false;
|
||||||
|
bool waitingToRestart = false;
|
||||||
|
unsigned long restartAt = 0;
|
||||||
|
volatile bool animationComplete = false;
|
||||||
|
|
||||||
|
void onAnimationComplete() {
|
||||||
|
// This callback runs in an interrupt, so it must remain very short.
|
||||||
|
animationComplete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ipToString(const IPAddress &ip) {
|
||||||
|
return String(ip[0]) + "." + String(ip[1]) + "." +
|
||||||
|
String(ip[2]) + "." + String(ip[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String urlDecode(const String &encoded) {
|
||||||
|
String decoded;
|
||||||
|
decoded.reserve(encoded.length());
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < encoded.length(); ++i) {
|
||||||
|
const char current = encoded[i];
|
||||||
|
|
||||||
|
if (current == '+') {
|
||||||
|
decoded += ' ';
|
||||||
|
} else if (current == '%' && i + 2 < encoded.length()) {
|
||||||
|
const char hex[] = {encoded[i + 1], encoded[i + 2], '\0'};
|
||||||
|
char *end = nullptr;
|
||||||
|
const long value = strtol(hex, &end, 16);
|
||||||
|
|
||||||
|
if (end == hex + 2) {
|
||||||
|
decoded += static_cast<char>(value);
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
decoded += current;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decoded += current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
String toMatrixText(const String &input) {
|
||||||
|
String output;
|
||||||
|
output.reserve(MAX_MESSAGE_LENGTH);
|
||||||
|
|
||||||
|
for (unsigned int i = 0;
|
||||||
|
i < input.length() && output.length() < MAX_MESSAGE_LENGTH;
|
||||||
|
++i) {
|
||||||
|
const uint8_t current = static_cast<uint8_t>(input[i]);
|
||||||
|
|
||||||
|
if (current == 0xC3 && i + 1 < input.length()) {
|
||||||
|
const uint8_t next = static_cast<uint8_t>(input[++i]);
|
||||||
|
const char *replacement = nullptr;
|
||||||
|
|
||||||
|
switch (next) {
|
||||||
|
case 0xA6: replacement = "ae"; break; // ae
|
||||||
|
case 0xB8: replacement = "oe"; break; // oe
|
||||||
|
case 0xA5: replacement = "aa"; break; // aa
|
||||||
|
case 0x86: replacement = "AE"; break;
|
||||||
|
case 0x98: replacement = "OE"; break;
|
||||||
|
case 0x85: replacement = "AA"; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replacement != nullptr) {
|
||||||
|
for (uint8_t j = 0;
|
||||||
|
replacement[j] != '\0' && output.length() < MAX_MESSAGE_LENGTH;
|
||||||
|
++j) {
|
||||||
|
output += replacement[j];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output += '?';
|
||||||
|
}
|
||||||
|
} else if (current >= 32 && current <= 126) {
|
||||||
|
output += static_cast<char>(current);
|
||||||
|
} else {
|
||||||
|
output += '?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.trim();
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
String htmlEscape(String value) {
|
||||||
|
value.replace("&", "&");
|
||||||
|
value.replace("<", "<");
|
||||||
|
value.replace(">", ">");
|
||||||
|
value.replace("\"", """);
|
||||||
|
value.replace("'", "'");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
String queryValue(const String &query, const String &name) {
|
||||||
|
const String prefix = name + "=";
|
||||||
|
int start = 0;
|
||||||
|
|
||||||
|
while (start < static_cast<int>(query.length())) {
|
||||||
|
int end = query.indexOf('&', start);
|
||||||
|
if (end < 0) {
|
||||||
|
end = query.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
const String part = query.substring(start, end);
|
||||||
|
if (part.startsWith(prefix)) {
|
||||||
|
return urlDecode(part.substring(prefix.length()));
|
||||||
|
}
|
||||||
|
|
||||||
|
start = end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
void startAnimation() {
|
||||||
|
matrix.clear();
|
||||||
|
animationComplete = false;
|
||||||
|
waitingToRestart = false;
|
||||||
|
|
||||||
|
matrix.beginDraw();
|
||||||
|
matrix.stroke(0xFFFFFFFF);
|
||||||
|
matrix.textFont(Font_5x7);
|
||||||
|
matrix.textScrollSpeed(scrollSpeedMs);
|
||||||
|
matrix.beginText(12, 1, 0xFFFFFF);
|
||||||
|
matrix.print(message);
|
||||||
|
matrix.endTextAnimation(SCROLL_LEFT, scrollAnimation);
|
||||||
|
|
||||||
|
matrix.loadTextAnimationSequence(scrollAnimation);
|
||||||
|
matrix.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendRedirect(WiFiClient &client) {
|
||||||
|
client.println(F("HTTP/1.1 303 See Other"));
|
||||||
|
client.println(F("Location: /"));
|
||||||
|
client.println(F("Connection: close"));
|
||||||
|
client.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendPage(WiFiClient &client) {
|
||||||
|
const String escapedMessage = htmlEscape(message);
|
||||||
|
const String ip = ipToString(WiFi.localIP());
|
||||||
|
|
||||||
|
client.println(F("HTTP/1.1 200 OK"));
|
||||||
|
client.println(F("Content-Type: text/html; charset=utf-8"));
|
||||||
|
client.println(F("Cache-Control: no-store"));
|
||||||
|
client.println(F("Connection: close"));
|
||||||
|
client.println();
|
||||||
|
|
||||||
|
client.println(F("<!doctype html><html lang='da'><head>"));
|
||||||
|
client.println(F("<meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"));
|
||||||
|
client.println(F("<title>Arduino Matrix</title><style>"));
|
||||||
|
client.println(F(":root{color-scheme:dark}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;background:#101418;color:#f6f7f8;font:16px system-ui,sans-serif;padding:20px}.card{width:min(560px,100%);background:#1c232a;border:1px solid #34404a;border-radius:18px;padding:28px;box-shadow:0 18px 50px #0008}h1{margin:0 0 8px;color:#ff7a18;font-size:clamp(1.6rem,5vw,2.4rem)}p{color:#b9c3cc;line-height:1.5}label{display:block;margin:20px 0 7px;font-weight:700}input[type=text]{width:100%;padding:13px;border:1px solid #51606d;border-radius:10px;background:#0d1115;color:#fff;font:inherit}input[type=range]{width:100%;accent-color:#ff7a18}.row{display:flex;justify-content:space-between;color:#b9c3cc}button{width:100%;margin-top:24px;padding:14px;border:0;border-radius:10px;background:#ff7a18;color:#17100a;font:700 1rem system-ui;cursor:pointer}.meta{margin-top:22px;padding-top:18px;border-top:1px solid #34404a;font-size:.9rem}code{color:#ffd0ad}</style></head><body>"));
|
||||||
|
client.println(F("<main class='card'><h1>LED Matrix Message Board</h1>"));
|
||||||
|
client.println(F("<p>Skriv en besked, og send den direkte til Arduinoens 12×8 LED-matrix.</p>"));
|
||||||
|
client.println(F("<form action='/set' method='get'><label for='message'>Besked</label>"));
|
||||||
|
client.print(F("<input id='message' name='message' type='text' maxlength='64' required value=\""));
|
||||||
|
client.print(escapedMessage);
|
||||||
|
client.println(F("\">"));
|
||||||
|
client.println(F("<label for='speed'>Hastighed</label><div class='row'><span>Hurtig</span><output id='speedValue'></output><span>Rolig</span></div>"));
|
||||||
|
client.print(F("<input id='speed' name='speed' type='range' min='35' max='180' step='5' value='"));
|
||||||
|
client.print(scrollSpeedMs);
|
||||||
|
client.println(F("' oninput=\"speedValue.value=this.value+' ms'\">"));
|
||||||
|
client.println(F("<button type='submit'>Vis på matrix</button></form>"));
|
||||||
|
client.print(F("<p class='meta'>Netværk: <code>"));
|
||||||
|
client.print(accessPointMode ? AP_SSID : WiFi.SSID());
|
||||||
|
client.print(F("</code><br>Adresse: <code>http://"));
|
||||||
|
client.print(ip);
|
||||||
|
client.println(F("</code></p></main><script>speedValue.value=speed.value+' ms'</script></body></html>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleClient() {
|
||||||
|
WiFiClient client = server.available();
|
||||||
|
if (!client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.setTimeout(750);
|
||||||
|
String requestLine = client.readStringUntil('\n');
|
||||||
|
requestLine.trim();
|
||||||
|
|
||||||
|
while (client.connected()) {
|
||||||
|
String headerLine = client.readStringUntil('\n');
|
||||||
|
headerLine.trim();
|
||||||
|
if (headerLine.length() == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestLine.startsWith("GET /set?")) {
|
||||||
|
const int targetEnd = requestLine.indexOf(' ', 4);
|
||||||
|
const int queryStart = requestLine.indexOf('?');
|
||||||
|
|
||||||
|
if (targetEnd > queryStart) {
|
||||||
|
const String query = requestLine.substring(queryStart + 1, targetEnd);
|
||||||
|
const String requestedMessage = toMatrixText(queryValue(query, "message"));
|
||||||
|
const long requestedSpeed = queryValue(query, "speed").toInt();
|
||||||
|
|
||||||
|
if (requestedMessage.length() > 0) {
|
||||||
|
message = requestedMessage;
|
||||||
|
}
|
||||||
|
if (requestedSpeed >= 35 && requestedSpeed <= 180) {
|
||||||
|
scrollSpeedMs = requestedSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRedirect(client);
|
||||||
|
} else {
|
||||||
|
sendPage(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(1);
|
||||||
|
client.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool connectToConfiguredWiFi() {
|
||||||
|
#if HAS_WIFI_SECRETS
|
||||||
|
Serial.print(F("Forbinder til "));
|
||||||
|
Serial.println(SECRET_SSID);
|
||||||
|
WiFi.begin(SECRET_SSID, SECRET_PASS);
|
||||||
|
|
||||||
|
const unsigned long deadline = millis() + 15000;
|
||||||
|
while (WiFi.status() != WL_CONNECTED &&
|
||||||
|
static_cast<long>(millis() - deadline) < 0) {
|
||||||
|
delay(250);
|
||||||
|
Serial.print('.');
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
|
||||||
|
return WiFi.status() == WL_CONNECTED;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool startAccessPoint() {
|
||||||
|
accessPointMode = true;
|
||||||
|
Serial.print(F("Opretter access point: "));
|
||||||
|
Serial.println(AP_SSID);
|
||||||
|
return WiFi.beginAP(AP_SSID, AP_PASSWORD) == WL_AP_LISTENING;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
const unsigned long serialDeadline = millis() + 2500;
|
||||||
|
while (!Serial && static_cast<long>(millis() - serialDeadline) < 0) {
|
||||||
|
}
|
||||||
|
|
||||||
|
matrix.begin();
|
||||||
|
matrix.setCallback(onAnimationComplete);
|
||||||
|
startAnimation();
|
||||||
|
|
||||||
|
if (WiFi.status() == WL_NO_MODULE) {
|
||||||
|
Serial.println(F("Wi-Fi-modulet svarer ikke."));
|
||||||
|
while (true) {
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connectToConfiguredWiFi()) {
|
||||||
|
WiFi.disconnect();
|
||||||
|
delay(500);
|
||||||
|
|
||||||
|
if (!startAccessPoint()) {
|
||||||
|
Serial.println(F("Kunne ikke starte access point."));
|
||||||
|
while (true) {
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server.begin();
|
||||||
|
|
||||||
|
Serial.println(F("Message board er klar."));
|
||||||
|
Serial.print(F("Aabn http://"));
|
||||||
|
Serial.println(ipToString(WiFi.localIP()));
|
||||||
|
if (accessPointMode) {
|
||||||
|
Serial.print(F("Wi-Fi-kode: "));
|
||||||
|
Serial.println(AP_PASSWORD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
handleClient();
|
||||||
|
|
||||||
|
if (animationComplete) {
|
||||||
|
noInterrupts();
|
||||||
|
animationComplete = false;
|
||||||
|
interrupts();
|
||||||
|
|
||||||
|
waitingToRestart = true;
|
||||||
|
restartAt = millis() + REPEAT_PAUSE_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waitingToRestart && static_cast<long>(millis() - restartAt) >= 0) {
|
||||||
|
waitingToRestart = false;
|
||||||
|
matrix.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Reference in a new issue