From de51255f585e522bbfa20bae414d73038b74305b Mon Sep 17 00:00:00 2001 From: christian Date: Sat, 24 Oct 2026 06:07:28 +0200 Subject: [PATCH] Add Wi-Fi LED matrix message board --- .gitignore | 1 + README.md | 6 + sketches/wifi_matrix_message_board/README.md | 55 +++ .../arduino_secrets.example.h | 7 + .../wifi_matrix_message_board.ino | 330 ++++++++++++++++++ 5 files changed, 399 insertions(+) create mode 100644 sketches/wifi_matrix_message_board/README.md create mode 100644 sketches/wifi_matrix_message_board/arduino_secrets.example.h create mode 100644 sketches/wifi_matrix_message_board/wifi_matrix_message_board.ino diff --git a/.gitignore b/.gitignore index 7e59a40..3d01d72 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ build/ .arduinoIDE/ .arduino15/ sketch.yaml +arduino_secrets.h # Editors and operating systems .vscode/ diff --git a/README.md b/README.md index 2544f1e..f7ec98c 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,9 @@ sketches/ Board-specifik dokumentation, forbindelser og nødvendige biblioteker beskrives 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 + diff --git a/sketches/wifi_matrix_message_board/README.md b/sketches/wifi_matrix_message_board/README.md new file mode 100644 index 0000000..f62eeda --- /dev/null +++ b/sketches/wifi_matrix_message_board/README.md @@ -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 +``` + diff --git a/sketches/wifi_matrix_message_board/arduino_secrets.example.h b/sketches/wifi_matrix_message_board/arduino_secrets.example.h new file mode 100644 index 0000000..d8c3850 --- /dev/null +++ b/sketches/wifi_matrix_message_board/arduino_secrets.example.h @@ -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" + diff --git a/sketches/wifi_matrix_message_board/wifi_matrix_message_board.ino b/sketches/wifi_matrix_message_board/wifi_matrix_message_board.ino new file mode 100644 index 0000000..fdae24e --- /dev/null +++ b/sketches/wifi_matrix_message_board/wifi_matrix_message_board.ino @@ -0,0 +1,330 @@ +#include +#include +#include +#include + +#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(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(input[i]); + + if (current == 0xC3 && i + 1 < input.length()) { + const uint8_t next = static_cast(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(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(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("")); + client.println(F("")); + client.println(F("Arduino Matrix")); + client.println(F("

LED Matrix Message Board

")); + client.println(F("

Skriv en besked, og send den direkte til Arduinoens 12×8 LED-matrix.

")); + client.println(F("
")); + client.print(F("")); + client.println(F("
HurtigRolig
")); + client.print(F("")); + client.println(F("
")); + client.print(F("

Netværk: ")); + client.print(accessPointMode ? AP_SSID : WiFi.SSID()); + client.print(F("
Adresse: http://")); + client.print(ip); + client.println(F("

")); +} + +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(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(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(millis() - restartAt) >= 0) { + waitingToRestart = false; + matrix.play(); + } +} +