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/
|
||||
.arduino15/
|
||||
sketch.yaml
|
||||
arduino_secrets.h
|
||||
|
||||
# Editors and operating systems
|
||||
.vscode/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
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