From 8286ed6eaa0edf9a37067176b6ec6370ffcc20a5 Mon Sep 17 00:00:00 2001 From: Andrew Munro Date: Fri, 2 Feb 2024 19:34:42 +0000 Subject: [PATCH] Fix arduino websockets poll lag. Adding weather. --- arduino/bitmapstream.ino | 13 ++--- index.html | 21 +++---- src/reciever.ts | 45 +++++++++++++++ src/{client.ts => sender.ts} | 70 +++++++++++------------ src/server.ts | 47 ++++++++++++++- src/utils.ts | 19 +++++++ src/views/buses.ts | 107 +++++++++++++++++++---------------- src/views/flights.ts | 96 ++++++++++++++++--------------- src/views/trains.ts | 79 ++++++++++++++------------ src/views/weather.ts | 33 +++++++++++ vite.config.ts | 13 +++++ 11 files changed, 352 insertions(+), 191 deletions(-) create mode 100644 src/reciever.ts rename src/{client.ts => sender.ts} (57%) create mode 100644 src/views/weather.ts diff --git a/arduino/bitmapstream.ino b/arduino/bitmapstream.ino index 609e4b7..f5a6f43 100644 --- a/arduino/bitmapstream.ino +++ b/arduino/bitmapstream.ino @@ -14,11 +14,6 @@ using namespace websockets; MatrixPanel_I2S_DMA* dma_display; WebsocketsClient client; -void onMessageCallback(WebsocketsMessage message) { - uint16_t* uint16_data = (uint16_t *) message.c_str(); - dma_display->drawRGBBitmap(0, 0, uint16_data, 128, 32); -} - void onEventsCallback(WebsocketsEvent event, String data) { if(event == WebsocketsEvent::ConnectionOpened) { Serial.println("Connnection Opened"); @@ -68,9 +63,6 @@ void setup() { delay(2000); ESP.restart(); } - - // run callback when messages are received - client.onMessage(onMessageCallback); // run callback when events are occuring client.onEvent(onEventsCallback); @@ -91,5 +83,8 @@ void setup() { } void loop() { - client.poll(); + auto message = client.readBlocking(); + + uint16_t* uint16_data = (uint16_t *) message.c_str(); + dma_display->drawRGBBitmap(0, 0, uint16_data, 128, 32); } diff --git a/index.html b/index.html index 8f812f3..0a854ed 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,13 @@ + + DotMatrix Sandbox + + - - DotMatrix Sandbox - - + +
- -
- - - - - \ No newline at end of file + + + diff --git a/src/reciever.ts b/src/reciever.ts new file mode 100644 index 0000000..f9a630d --- /dev/null +++ b/src/reciever.ts @@ -0,0 +1,45 @@ +import { rgb565ToRGBA } from './utils'; + +const canvas = document.createElement('canvas'); +canvas.width = 1280; +canvas.height = 320; +canvas.style = 'zoom: 10'; +document.body.appendChild(canvas); +const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + +let hasDisconnected = false; + +const connect = () => { + const ws = new WebSocket(`ws://rgb.mun.sh/sub`); + ws.onopen = evt => { + console.log('connected!', evt); + + if (hasDisconnected) { + // Force refresh to get new client + window.location.reload(); + } + }; + + ws.onmessage = async evt => { + const data = (await evt.data.arrayBuffer()) as ArrayBuffer; + const rgba = rgb565ToRGBA(new Uint16Array(data)); + const imageData = new ImageData(new Uint8ClampedArray(rgba), 128, 32); + + ctx.putImageData(imageData, 0, 0); + }; + + ws.onerror = ws.onclose = () => { + console.log('websocket closed! refreshing!'); + + hasDisconnected = true; + + setTimeout(() => { + connect(); + }, 1000); + }; +}; + +// Draw to RGB +setTimeout(() => { + connect(); +}, 1000); diff --git a/src/client.ts b/src/sender.ts similarity index 57% rename from src/client.ts rename to src/sender.ts index b65ed7c..1b0805f 100644 --- a/src/client.ts +++ b/src/sender.ts @@ -4,15 +4,16 @@ import { BaseTexture, Container, Program, Renderer, RenderTexture, Sprite } from import { dotMatrixFilter, rgbaToRgb565 } from './utils'; import { clock } from './views/clock'; import { transportScreen } from './views/transportScreen'; +import { weather } from './views/weather'; BaseTexture.defaultOptions.scaleMode = PIXI.SCALE_MODES.NEAREST; Program.defaultFragmentPrecision = PIXI.PRECISION.HIGH; const app = new PIXI.Application({ - width: 1280, - height: 320, - resolution: 1, - forceCanvas: true, + width: 1280, + height: 320, + resolution: 1, + forceCanvas: false }); document.body.appendChild(app.view as any); @@ -31,51 +32,50 @@ display.addChild(transportScreen()); // app.ticker.add((await gifs(display)).update); // app.ticker.add((await scrollingText(display)).update); // app.ticker.add(await scrollingDot(display)); +display.addChild(weather()); app.ticker.add((await clock(display)).update); // Render app.ticker.add(delta => { - renderer.render(display, { renderTexture }); - renderer.render(app.stage); + renderer.render(display, { renderTexture }); + renderer.render(app.stage); }); let hasDisconnected = false; const connect = () => { - const ws = new WebSocket(`ws://${location.host}/pub`); - ws.onopen = evt => { - console.log('connected!', evt); + const ws = new WebSocket(`ws://${location.host}/pub`); + ws.onopen = evt => { + console.log('connected!', evt); - if (hasDisconnected) { - // Force refresh to get new client - window.location.reload(); - } else { - setInterval(() => { - const pixels = renderer.extract.pixels(renderTexture); - const rgb565 = rgbaToRgb565(pixels); - ws.send(rgb565); - }, 1000 * 1 / app.ticker.maxFPS); - } - }; + if (hasDisconnected) { + // Force refresh to get new client + window.location.reload(); + } else { + setInterval(() => { + const pixels = renderer.extract.pixels(renderTexture); + const rgb565 = rgbaToRgb565(pixels); + ws.send(rgb565); + }, (1000 * 1) / app.ticker.maxFPS); + } + }; - ws.onmessage = evt => { - console.log('message!', evt); - }; + ws.onmessage = evt => { + console.log('message!', evt); + }; - ws.onclose = () => { - console.log('websocket closed! refreshing!'); + ws.onclose = () => { + console.log('websocket closed! refreshing!'); - hasDisconnected = true; - - setTimeout(() => { - connect(); - }, 1000); - }; -} + hasDisconnected = true; + + setTimeout(() => { + connect(); + }, 1000); + }; +}; // Draw to RGB setTimeout(() => { - connect(); - + connect(); }, 1000); - diff --git a/src/server.ts b/src/server.ts index 436ea2f..6e8b8cb 100644 --- a/src/server.ts +++ b/src/server.ts @@ -27,7 +27,10 @@ const app = bunExpress({ } if (ws.data.url.pathname == '/sub') { - subSockets.push(ws); + setTimeout(() => { + subSockets.push(ws); + }, 500); + console.log('new subscriber', ws.data.id); } }, @@ -62,7 +65,7 @@ const app = bunExpress({ } } as WebSocketServeOptions as any); -const sendInterval = process.env.SEND_INTERVAL ? parseInt(process.env.SEND_INTERVAL) : 500; +const sendInterval = process.env.SEND_INTERVAL ? parseInt(process.env.SEND_INTERVAL) : 100; // Throttle updates to RGB screen setInterval(() => { @@ -202,6 +205,46 @@ app.get('/api/flights/arrivals', async (req, res) => { res.json(flights); }); +function mapWeatherToEmoji(wmoCode: number): string { + switch (wmoCode) { + case 0: // Clear sky + return '☀️'; + case 1: // Partly cloudy + case 2: + return '⛅'; + case 3: // Cloudy + return '☁️'; + case 10: // Mist + case 20: // Fog + return '🌫️'; + case 30: // Drizzle + case 40: // Rain + return '🌧️'; + case 60: // Thunderstorm + return '⛈️'; + case 80: // Snow + return '❄️'; + default: + return '❓'; // Unknown or unsupported weather condition + } +} + +app.get('/api/weather', async (req, res) => { + const lat = '53.8193'; + const long = '-1.5990'; + const response = await fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${long}¤t=temperature_2m,weather_code` + ); + + const data = await response.json(); + + const temp = Math.round(data.current.temperature_2m); + const weatherCode = data.current.weather_code; + const emoji = mapWeatherToEmoji(weatherCode); + + res.json({ temp, emoji }); +}); + app.listen(3000, () => { console.log('server started on', 3000); }); diff --git a/src/utils.ts b/src/utils.ts index 4a73324..1090bbe 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -26,6 +26,25 @@ export function rgbaToRgb565(rgba: Uint8Array) { return rgb565; } +export function rgb565ToRGBA(rgb565: Uint16Array) { + const length = rgb565.length; + const rgba = new Uint8Array(length * 4); + + for (let i = 0; i < length; i++) { + const rgb = rgb565[i]; + const r = ((rgb >> 11) * 255) / 31; + const g = (((rgb >> 5) & 0x3f) * 255) / 63; + const b = ((rgb & 0x1f) * 255) / 31; + + rgba[i * 4] = r; + rgba[i * 4 + 1] = g; + rgba[i * 4 + 2] = b; + rgba[i * 4 + 3] = 255; + } + + return rgba; +} + const dotMatrixShader = ` precision mediump float; diff --git a/src/views/buses.ts b/src/views/buses.ts index 5279841..5d85bf2 100644 --- a/src/views/buses.ts +++ b/src/views/buses.ts @@ -1,41 +1,44 @@ import gsap from 'gsap'; -import { Container, Renderer, Text } from 'pixi.js-legacy'; +import { Container, Text } from 'pixi.js-legacy'; import { createText } from '../utils'; -type BusStop = 'kirkstall_lights' | 'kirkstall_lane' +type BusStop = 'kirkstall_lights' | 'kirkstall_lane'; type Bus = { line: string; - scheduled: { + scheduled: { time: string; date: Date; }; - expectedArrivalMins?: number; - occupancy?: { - occupied: number; - capacity: number; - }; + expectedArrivalMins?: number; + occupancy?: { + occupied: number; + capacity: number; + }; }; const formatBusRow = (bus: Bus) => { const row = new Container(); - const scheduled = createText(bus.scheduled.time, 'orange'); - const line = createText(bus.line, 'yellow'); - line.x = scheduled.x + 25; + const scheduled = createText(bus.scheduled.time, 'orange'); + const line = createText(bus.line, 'yellow'); + line.x = scheduled.x + 25; - if (bus.occupancy) { - const seatsIcon = new Text('💺', { fontSize: 4 }); - seatsIcon.x = line.x + 22; - seatsIcon.y = 2; - const seats = createText(`${bus.occupancy.capacity - bus.occupancy.occupied} free`, 'orange'); - seats.x = seatsIcon.x + 6; + if (bus.occupancy) { + const seatsIcon = new Text('💺', { fontSize: 4 }); + seatsIcon.x = line.x + 22; + seatsIcon.y = 2; + const seats = createText(`${bus.occupancy.capacity - bus.occupancy.occupied} free`, 'orange'); + seats.x = seatsIcon.x + 6; - row.addChild(seatsIcon, seats); - } + row.addChild(seatsIcon, seats); + } - const arriving = bus.expectedArrivalMins != null ? createText(bus.expectedArrivalMins > 0 ? bus.expectedArrivalMins + ' MINS' : 'DUE', 'green') : createText(bus.scheduled.time, 'orange') - arriving.x = 95; + const arriving = + bus.expectedArrivalMins != null + ? createText(bus.expectedArrivalMins > 0 ? bus.expectedArrivalMins + ' MINS' : 'DUE', 'green') + : createText(bus.scheduled.time, 'orange'); + arriving.x = 95; row.addChild(scheduled, line, arriving); @@ -46,38 +49,42 @@ const fetchBuses = async (stop: BusStop) => { const res = await fetch(`/api/bus/${stop}`); const data = await res.json(); - const trains = new Container(); - trains.y = 2; + const busses = new Container(); - for (const [i, train] of data.slice(0, 3).entries()) { - const trainRow = formatBusRow(train); - trainRow.y = i * 6; - trains.addChild(trainRow); - } + for (const [i, train] of data.slice(0, 3).entries()) { + const trainRow = formatBusRow(train); + trainRow.y = i * 6; + busses.addChild(trainRow); + } - return trains; -} + return busses; +}; export const buses = async (parent: Container, stop: BusStop = 'kirkstall_lights') => { let buses = await fetchBuses(stop); - parent.addChild(buses); - - const icon = new Text('🚌 🚎', { - fontSize: 7, - }); - icon.x = 132; - icon.y = 20; - buses.addChild(icon); - - const tween = gsap.to(icon, { - x: -50, duration: 10, repeat: -1, yoyo: false, repeatDelay: 0, ease: 'linear' - }) - - return { - update: (dt: number) => {}, - destroy: async (dt: number) => { - tween.kill(); - parent.removeChild(buses); - } - }; + parent.addChild(buses); + + const icon = new Text('🚌 🚎', { + fontSize: 7 + }); + icon.x = 132; + icon.y = 22; + buses.addChild(icon); + + const tween = gsap.to(icon, { + x: -50, + duration: 10, + repeat: -1, + yoyo: false, + repeatDelay: 0, + ease: 'linear' + }); + + return { + update: (dt: number) => {}, + destroy: async (dt: number) => { + tween.kill(); + parent.removeChild(buses); + } + }; }; diff --git a/src/views/flights.ts b/src/views/flights.ts index fd778ce..0d1af46 100644 --- a/src/views/flights.ts +++ b/src/views/flights.ts @@ -1,5 +1,5 @@ import gsap from 'gsap'; -import { Container, Renderer, Text } from 'pixi.js-legacy'; +import { Container, Text } from 'pixi.js-legacy'; import { createText } from '../utils'; const MaxStringLength = 11; @@ -11,27 +11,27 @@ type Flight = { }; origin: string; message?: string; - id: string; + id: string; }; const formatFlightRow = (flight: Flight) => { const row = new Container(); - if (flight.origin.length > MaxStringLength) { - flight.origin = `${flight.origin.slice(0, MaxStringLength)} . . .` - } + if (flight.origin.length > MaxStringLength) { + flight.origin = `${flight.origin.slice(0, MaxStringLength)} . . .`; + } - const scheduled = createText(flight.scheduled.time, 'orange'); - const origin = createText(flight.origin, 'yellow'); - origin.x = 24; + const scheduled = createText(flight.scheduled.time, 'orange'); + const origin = createText(flight.origin, 'yellow'); + origin.x = 24; - row.addChild(scheduled, origin); + row.addChild(scheduled, origin); - if (flight.message) { - const message = createText(flight.message, flight.message.includes('lnd') ? 'green' : 'red') - message.x = 89; - row.addChild(message); - } + if (flight.message) { + const message = createText(flight.message, flight.message.includes('lnd') ? 'green' : 'red'); + message.x = 89; + row.addChild(message); + } return row; }; @@ -40,41 +40,45 @@ const fetchFlights = async () => { const res = await fetch('/api/flights/arrivals'); const data = await res.json(); - const flights = new Container(); - flights.y = 2; + const flights = new Container(); - for (const [i, flight] of data.slice(0, 3).entries()) { - const flightRow = formatFlightRow(flight); - flightRow.y = i * 6; - flights.addChild(flightRow); - } + for (const [i, flight] of data.slice(0, 3).entries()) { + const flightRow = formatFlightRow(flight); + flightRow.y = i * 6; + flights.addChild(flightRow); + } - return flights; -} + return flights; +}; export const flights = async (parent: Container) => { let flights = await fetchFlights(); - parent.addChild(flights); - - const icon = new Text('🚁', { - fontSize: 7, - }); - icon.x = 132; - icon.y = 19; - // icon.anchor.x = icon.anchor.y = 0.5; - // icon.rotation = 5; - // icon.scale.x = -1; - flights.addChild(icon); - - const tween = gsap.to(icon, { - x: -50, duration: 10, repeat: -1, yoyo: false, repeatDelay: 0, ease: 'linear' - }); - - return { - update: (dt: number) => {}, - destroy: async (dt: number) => { - tween.kill(); - parent.removeChild(flights); - } - }; + parent.addChild(flights); + + const icon = new Text('🚁', { + fontSize: 7 + }); + icon.x = 132; + icon.y = 22; + // icon.anchor.x = icon.anchor.y = 0.5; + // icon.rotation = 5; + // icon.scale.x = -1; + flights.addChild(icon); + + const tween = gsap.to(icon, { + x: -50, + duration: 10, + repeat: -1, + yoyo: false, + repeatDelay: 0, + ease: 'linear' + }); + + return { + update: (dt: number) => {}, + destroy: async (dt: number) => { + tween.kill(); + parent.removeChild(flights); + } + }; }; diff --git a/src/views/trains.ts b/src/views/trains.ts index 63d6bae..7b71c59 100644 --- a/src/views/trains.ts +++ b/src/views/trains.ts @@ -1,5 +1,5 @@ import gsap from 'gsap'; -import { Container, Renderer, Text } from 'pixi.js-legacy'; +import { Container, Text } from 'pixi.js-legacy'; import { createText } from '../utils'; const MaxStringLength = 9; @@ -22,17 +22,18 @@ type Train = { const formatTrainRow = (train: Train) => { const row = new Container(); - if (train.destination.length > MaxStringLength) { - train.destination = `${train.destination.slice(0, MaxStringLength)} . . .` - } + if (train.destination.length > MaxStringLength) { + train.destination = `${train.destination.slice(0, MaxStringLength)} . . .`; + } - const scheduled = createText(train.scheduled.time, 'orange'); - const platform = createText(train.platform, 'yellow'); - platform.x = scheduled.x + 25; - const dest = createText(train.destination, 'orange'); - dest.x = platform.x + 10; - const arriving = train.arrives.date > train.scheduled.date ? createText(`Exp ${train.arrives.time}`, 'red') : createText(`On time`, 'green') - arriving.x = 89; + const scheduled = createText(train.scheduled.time, 'orange'); + const platform = createText(train.platform, 'yellow'); + platform.x = scheduled.x + 25; + const dest = createText(train.destination, 'orange'); + dest.x = platform.x + 10; + const arriving = + train.arrives.date > train.scheduled.date ? createText(`Exp ${train.arrives.time}`, 'red') : createText(`On time`, 'green'); + arriving.x = 89; row.addChild(scheduled, platform, dest, arriving); @@ -43,38 +44,42 @@ const fetchTrains = async () => { const res = await fetch('/api/train/HDY'); const data = await res.json(); - const trains = new Container(); - trains.y = 2; + const trains = new Container(); - for (const [i, train] of data.slice(0, 3).entries()) { - const trainRow = formatTrainRow(train); - trainRow.y = i * 6; - trains.addChild(trainRow); - } + for (const [i, train] of data.slice(0, 3).entries()) { + const trainRow = formatTrainRow(train); + trainRow.y = i * 6; + trains.addChild(trainRow); + } - return trains; -} + return trains; +}; export const trains = async (parent: Container) => { let trains = await fetchTrains(); - parent.addChild(trains); + parent.addChild(trains); - const icon = new Text('🚂🚃🚃🚃', { - fontSize: 7, - }); - icon.x = 132; - icon.y = 20; - trains.addChild(icon); + const icon = new Text('🚂🚃🚃🚃', { + fontSize: 7 + }); + icon.x = 132; + icon.y = 22; + trains.addChild(icon); - const tween = gsap.to(icon, { - x: -50, duration: 10, repeat: -1, yoyo: false, repeatDelay: 0, ease: 'linear' - }); + const tween = gsap.to(icon, { + x: -50, + duration: 10, + repeat: -1, + yoyo: false, + repeatDelay: 0, + ease: 'linear' + }); - return { - update: (dt: number) => {}, - destroy: async (dt: number) => { - tween.kill(); - parent.removeChild(trains); - } - }; + return { + update: (dt: number) => {}, + destroy: async (dt: number) => { + tween.kill(); + parent.removeChild(trains); + } + }; }; diff --git a/src/views/weather.ts b/src/views/weather.ts new file mode 100644 index 0000000..59975ad --- /dev/null +++ b/src/views/weather.ts @@ -0,0 +1,33 @@ +import { Container, Graphics, Text } from 'pixi.js-legacy'; +import { createText } from '../utils'; + +export const weather = () => { + const weather = new Container(); + weather.x = 0; + weather.y = 22; + + const weatherIcon = new Text('☁️', { fontSize: 7 }); + weatherIcon.x = 2; + weatherIcon.y = 1; + const temp = createText('9C', 'cyan', 'pixel7'); + temp.x = 13; + + const bg = new Graphics(); + bg.x = -2; + bg.beginFill(0x000000); + bg.drawRect(0, 0, 30, 10); + + weather.addChild(bg, weatherIcon, temp); + + const updateWeather = async () => { + const res = await fetch('/api/weather'); + const data = await res.json(); + weatherIcon.text = data.emoji; + temp.text = `${data.temp}C`; + }; + + setInterval(updateWeather, 1000 * 60 * 5); + updateWeather(); + + return weather; +}; diff --git a/vite.config.ts b/vite.config.ts index 1061fe1..21aa9dd 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,5 +4,18 @@ export default defineConfig({ build: { target: 'esnext', minify: false + }, + server: { + proxy: { + '/pub': { + target: 'ws://localhost:3000', + ws: true + }, + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + secure: false + } + } } });