From 4e0a819b1bed339a0b3c68cd658b7cd32bffff57 Mon Sep 17 00:00:00 2001 From: Andrew Munro Date: Sun, 4 Feb 2024 16:01:51 +0000 Subject: [PATCH] Messing with UDP. Compressing data sent to board. --- arduino/bitmapstream.ino | 123 ++++++++++++++++++++-------------- arduino/bitmapstreamudp.ino | 96 +++++++++++++++++++++++++++ bun.lockb | Bin 74595 -> 75285 bytes package.json | 4 +- src/server.ts | 38 +++++++---- udpserver/go.mod | 8 +++ udpserver/go.sum | 4 ++ udpserver/main.go | 128 ++++++++++++++++++++++++++++++++++++ 8 files changed, 340 insertions(+), 61 deletions(-) create mode 100644 arduino/bitmapstreamudp.ino create mode 100644 udpserver/go.mod create mode 100644 udpserver/go.sum create mode 100644 udpserver/main.go diff --git a/arduino/bitmapstream.ino b/arduino/bitmapstream.ino index eb5d281..334d405 100644 --- a/arduino/bitmapstream.ino +++ b/arduino/bitmapstream.ino @@ -1,34 +1,26 @@ #include #include #include -#include +#include +#include #define PANEL_WIDTH 64 #define PANEL_HEIGHT 32 #define PANELS_NUMBER 2 #define E_PIN_DEFAULT 18 -const char* websockets_connection_string = "ws://rgb.mun.sh/sub"; //Enter server adress -using namespace websockets; +WiFiManagerParameter brightness("brightness", "Display Brightness", "128", 40); +WiFiManagerParameter serverAddress("serverAddress", "Server Address", "rgb.mun.sh", 40); +WiFiManagerParameter serverPath("serverPath", "Server Path", "/sub", 40); +WiFiManagerParameter serverPort("serverPort", "Server Port", "80", 40); +WiFiManagerParameter serverReconnectInterval("serverReconnectInterval", "Server Reconnect Delay", "5000", 40); MatrixPanel_I2S_DMA* dma_display; -WebsocketsClient client; +WebSocketsClient webSocket; -uint16_t* uint16_data; - -void onMessageCallback(WebsocketsMessage message) { - uint16_data = (uint16_t *) message.c_str(); -} - -void onEventsCallback(WebsocketsEvent event, String data) { - if(event == WebsocketsEvent::ConnectionOpened) { - Serial.println("Connnection Opened"); - } else if(event == WebsocketsEvent::ConnectionClosed) { - Serial.println("Connnection Closed"); - delay(2000); - ESP.restart(); - } -} +uLongf destLen = 8192; +Bytef destBuffer[8192]; +Bytef resultBuffer[8192]; void setup() { Serial.begin(115200); @@ -50,50 +42,85 @@ void setup() { dma_display->clearScreen(); dma_display->setTextSize(1); dma_display->setTextWrap(true); - dma_display->setCursor(0,0); + dma_display->setCursor(0, 0); dma_display->print("Connecting to WIFI..."); dma_display->setCursor(16, 16); dma_display->print("SSID: TrainSignAP"); WiFiManager wm; + wm.addParameter(&brightness); + wm.addParameter(&serverAddress); + wm.addParameter(&serverPath); + wm.addParameter(&serverPort); + wm.addParameter(&serverReconnectInterval); + bool connected; connected = wm.autoConnect("TrainSignAP"); - if(!connected) { - Serial.println("Failed to connect"); - dma_display->clearScreen(); - dma_display->setTextSize(1); // size 1 == 8 pixels high - dma_display->setTextWrap(true); // Don't wrap at end of line - will do ourselves - dma_display->setCursor(0,0); - dma_display->print("Wifi failed :,("); - delay(2000); - ESP.restart(); + if (!connected) { + Serial.println("Failed to connect"); + dma_display->clearScreen(); + dma_display->setTextSize(1); // size 1 == 8 pixels high + dma_display->setTextWrap(true); // Don't wrap at end of line - will do ourselves + dma_display->setCursor(0, 0); + dma_display->print("Wifi failed :,("); + delay(2000); + ESP.restart(); } - // run callback when messages are received - client.onMessage(onMessageCallback); - - // run callback when events are occuring - client.onEvent(onEventsCallback); + Serial.println("WiFi Connected"); - // Connect to server - connected = client.connect(websockets_connection_string); - if(!connected) { - Serial.println("Connnection Closed"); - dma_display->clearScreen(); - dma_display->setTextSize(1); - dma_display->setTextWrap(true); - dma_display->setCursor(0,0); - dma_display->print("Stream failed :,("); + String brightnessValue = brightness.getValue(); + int brightnessInt = brightnessValue.toInt(); + if (brightnessInt < 0) brightnessInt = 0; + if (brightnessInt > 255) brightnessInt = 255; + dma_display->setBrightness8(brightnessInt); - delay(2000); - ESP.restart(); + String portValue = serverPort.getValue(); + int port = portValue.toInt(); + String serverReconnectIntervalValue = serverReconnectInterval.getValue(); + int serverReconnect = serverReconnectIntervalValue.toInt(); + + webSocket.begin(serverAddress.getValue(), port, serverPath.getValue()); + webSocket.onEvent(webSocketEvent); + webSocket.setReconnectInterval(serverReconnect); +} + +void webSocketEvent(WStype_t type, uint8_t* payload, size_t length) { + // Decompress the payload + switch (type) { + case WStype_DISCONNECTED: + Serial.println("Disconnected!"); + memset(resultBuffer, 0, 8192); + + dma_display->clearScreen(); + dma_display->setTextSize(1); + dma_display->setTextWrap(true); + dma_display->setCursor(0, 0); + dma_display->print("Reconnecting..."); + break; + case WStype_CONNECTED: + Serial.printf("Connected to url: %s\n", payload); + break; + case WStype_BIN: { + int result = uncompress(destBuffer, &destLen, payload, length); + if (result != Z_OK) { + Serial.println("Decompression failed!"); + } + + // apply result delta to last frame + for (int i = 0; i < 8192; i++) { + resultBuffer[i] = resultBuffer[i] + destBuffer[i]; + } + + break; + } } } void loop() { - client.poll(); - if (uint16_data != nullptr) { - dma_display->drawRGBBitmap(0, 0, uint16_data, 128, 32); + webSocket.loop(); + if (webSocket.isConnected()) { + dma_display->drawRGBBitmap(0, 0, (uint16_t*) resultBuffer, 128, 32); } } diff --git a/arduino/bitmapstreamudp.ino b/arduino/bitmapstreamudp.ino new file mode 100644 index 0000000..b9deb94 --- /dev/null +++ b/arduino/bitmapstreamudp.ino @@ -0,0 +1,96 @@ +#include +#include +// #include +#include +#include +#include + +#define PANEL_WIDTH 64 +#define PANEL_HEIGHT 32 +#define PANELS_NUMBER 2 +#define E_PIN_DEFAULT 18 + +const char* ssid = "xxx"; +const char* password = "xxx"; +const int udpPort = 8080; + +MatrixPanel_I2S_DMA* dma_display; +AsyncUDP udp; + +byte buffers[4][1024]; // Adjust this according to your needs +bool received[4] = {false}; // Flag to track received packets +uint16_t assembledData[4096]; +int expectedSequence = 0; + +void setup() { + Serial.begin(115200); + + HUB75_I2S_CFG mxconfig; + mxconfig.mx_height = PANEL_HEIGHT; // we have 64 pix heigh panels + mxconfig.chain_length = PANELS_NUMBER; // we have 2 panels chained + mxconfig.gpio.e = E_PIN_DEFAULT; // we MUST assign pin e to some free pin on a board to drive 64 pix height panels with 1/32 scan + mxconfig.clkphase = false; + + dma_display = new MatrixPanel_I2S_DMA(mxconfig); + + // Allocate memory and start DMA display + if (not dma_display->begin()) + Serial.println("****** !KABOOM! I2S memory allocation failed ***********"); + + dma_display->setBrightness8(128); // range is 0-255, 0 - 0%, 255 - 100% + + dma_display->clearScreen(); + dma_display->setTextSize(1); + dma_display->setTextWrap(true); + dma_display->setCursor(0, 0); + dma_display->print("Connecting to WIFI..."); + dma_display->setCursor(16, 16); + + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + + while (WiFi.status() != WL_CONNECTED) { + delay(1000); + } + + Serial.println("WiFi Connected"); + + if (udp.connect(IPAddress(0,0,0,0), udpPort)) { + Serial.println("Server Connected"); + udp.onPacket([](AsyncUDPPacket packet) { + int sequence = packet.data()[0]; // Assuming first byte is sequence number + memcpy(buffers[sequence], packet.data() + 1, packet.length() - 1); + received[sequence] = true; + }); + udp.print("hi"); + } else { + Serial.println("Failed to connect"); + dma_display->clearScreen(); + dma_display->setTextSize(1); // size 1 == 8 pixels high + dma_display->setTextWrap(true); // Don't wrap at end of line - will do ourselves + dma_display->setCursor(0, 0); + dma_display->print("Wifi failed :,("); + delay(2000); + ESP.restart(); + } +} + + +void loop() { + bool allReceived = true; + for (int i = 0; i < 4; i++) { + if (!received[i]) { + allReceived = false; + break; + } + } + + if (allReceived) { + for (int i = 0; i < 4; i++) { + memcpy(assembledData + (1024 * i), buffers[i], 1024); + received[i] = false; + } + + dma_display->drawRGBBitmap(0, 0, assembledData, 128, 32); + } +} diff --git a/bun.lockb b/bun.lockb index 833d0feec3f27aa9e188cac131185e077d787311..6e7506425292a061f632ff8c3c4c84d5913e8590 100755 GIT binary patch delta 11451 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 Cg>Etc delta 11073 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 SaoD;f@=j6jgPV~e#Qa|>bo~MV diff --git a/package.json b/package.json index 7ac1901..46b6b2e 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,13 @@ "bun-serve-express": "^1.0.4", "express": "^4.18.2", "gsap": "^3.11.5", + "pako": "^2.1.0", "pixi.js-legacy": "^7.2.1", "vite": "^5.0.12" }, "devDependencies": { "@types/bun": "^1.0.3", - "@types/express": "^4.17.21" + "@types/express": "^4.17.21", + "@types/pako": "^2.0.3" } } \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index ebf7840..98f9836 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,7 @@ import { ServerWebSocket, WebSocketServeOptions } from 'bun'; import bunExpress from 'bun-serve-express'; import express from 'express'; +import pako from 'pako'; // import { webSocketServer } from './ws/websocketServer'; process.env.TZ = 'Europe/London'; @@ -8,27 +9,21 @@ process.env.TZ = 'Europe/London'; type WebSocketData = { url: URL; id: string; - ready: boolean; + firstSend: boolean; }; const randomId = () => { return Math.random().toString(36).substring(2, 7); }; +let lastFrame: Buffer | null = null; let latestFrame: Buffer | null = null; const app = bunExpress({ websocket: { open(ws) { ws.data.id = randomId(); - ws.data.ready = false; - - setTimeout( - () => { - ws.data.ready = true; - }, - process.env.SEND_DELAY ? parseInt(process.env.SEND_DELAY) : 3000 - ); + ws.data.firstSend = false; if (ws.data.url.pathname == '/pub') { pubSockets.push(ws); @@ -37,7 +32,6 @@ const app = bunExpress({ if (ws.data.url.pathname == '/sub') { subSockets.push(ws); - console.log('new subscriber', ws.data.id); } }, @@ -78,8 +72,22 @@ const sendInterval = process.env.SEND_INTERVAL ? parseInt(process.env.SEND_INTER setInterval(() => { // Echo data back to sub sockets if (latestFrame == null) return; + + // find delta between last frame and now + const deltaFrame = Buffer.alloc(latestFrame.length); + for (let i = 0; i < latestFrame.length; i++) { + deltaFrame[i] = latestFrame[i] - (lastFrame ? lastFrame[i] : 0); + } + + lastFrame = latestFrame; + for (const sub of subSockets) { - if (sub.data.ready) sub.send(latestFrame); + if (!sub.data.firstSend) { + sub.data.firstSend = true; + sub.send(pako.deflate(latestFrame)); + } else { + sub.send(pako.deflate(deltaFrame)); + } } }, sendInterval); @@ -195,7 +203,13 @@ app.get('/api/flights/arrivals', async (req, res) => { date: new Date(fl.scheduled_time) }, origin: fl.airport_name, - message: fl.message ? fl.message.replace('Expected', 'exp').replace('Landed', 'lnd').replace('Now ', '') : null, + message: fl.message + ? fl.message + .replace('Expected', 'exp') + .replace('Landed', 'lnd') + .replace('Now ', '') + .replace(/Diverted.*/, 'Diverted') + : null, status: fl.status })) .filter((fl: any) => { diff --git a/udpserver/go.mod b/udpserver/go.mod new file mode 100644 index 0000000..dcbeefc --- /dev/null +++ b/udpserver/go.mod @@ -0,0 +1,8 @@ +module udpserver + +go 1.21.6 + +require ( + github.com/gorilla/websocket v1.5.1 // indirect + golang.org/x/net v0.17.0 // indirect +) diff --git a/udpserver/go.sum b/udpserver/go.sum new file mode 100644 index 0000000..272772f --- /dev/null +++ b/udpserver/go.sum @@ -0,0 +1,4 @@ +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= diff --git a/udpserver/main.go b/udpserver/main.go new file mode 100644 index 0000000..d7b578b --- /dev/null +++ b/udpserver/main.go @@ -0,0 +1,128 @@ +package main + +import ( + "fmt" + "log" + "net" + "net/url" + "os" + "os/signal" + "time" + + "github.com/gorilla/websocket" +) + +func getEnv(key, fallback string) string { + value := os.Getenv(key) + if len(value) == 0 { + return fallback + } + return value +} + +func main() { + port := getEnv("UDP_PORT", "8080") + + // ws setup + u := url.URL{Scheme: "ws", Host: "rgb.mun.sh", Path: "/sub"} + log.Printf("connecting to %s", u.String()) + + c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + log.Fatal("dial:", err) + } + defer c.Close() + + // UDP setup + udpAddr, err := net.ResolveUDPAddr("udp", ":"+port) // Change the UDP address and port accordingly + if err != nil { + log.Fatal("UDP resolve address:", err) + } + + conn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + log.Fatal("UDP listen:", err) + } + defer conn.Close() + + done := make(chan struct{}) + + clients := make(map[string]*net.UDPAddr) + + // Read from websocket and write to UDP + go func() { + for { + _, message, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + return + } + // log.Printf("recv: %s", message) + + // Send data to all clients + for _, client := range clients { + // chunk message into 4 parts, with a sequence number + chunk0 := append([]byte{0}, message[:len(message)/4]...) + chunk1 := append([]byte{1}, message[len(message)/4:len(message)/2]...) + chunk2 := append([]byte{2}, message[len(message)/2:len(message)/4*3]...) + chunk3 := append([]byte{3}, message[len(message)/4*3:]...) + + // Send data to UDP connection + for _, chunk := range [][]byte{chunk0, chunk1, chunk2, chunk3} { + _, err := conn.WriteToUDP(chunk, client) + if err != nil { + fmt.Printf("Error sending data to %s: %v\n", client, err) + // delete client from clients map + delete(clients, client.String()) + } + } + } + } + }() + + // Handle udp clients + go func() { + defer close(done) + buf := make([]byte, 1024) + + for { + // Read from UDP connection + n, clientAddr, err := conn.ReadFromUDP(buf) + if err != nil { + fmt.Println("Error reading from UDP connection:", err) + continue + } + + // Print received data + fmt.Printf("Received from %s: %s\n", clientAddr, buf[:n]) + + // Add client to clients map + clients[clientAddr.String()] = clientAddr + } + }() + + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + for { + select { + case <-done: + return + case <-interrupt: + log.Println("interrupt") + + // Cleanly close the connection by sending a close message and then + // waiting (with timeout) for the server to close the connection. + err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + if err != nil { + log.Println("write close:", err) + return + } + select { + case <-done: + case <-time.After(time.Second): + } + return + } + } +} \ No newline at end of file