diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index 5238686..c90ca05 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -1,50 +1,63 @@ name: Build and Push Docker Image on: - push: - branches: [main] + push: + branches: [main] env: - TAG: latest + TAG: latest jobs: - build-and-push: - runs-on: ubuntu-latest - - steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push Docker image - uses: docker/build-push-action@v4 - with: - push: true - tags: ghcr.io/${{ github.repository }}:${{ env.TAG }} - - - name: deploy-dev - uses: th0th/rancher-redeploy-workload@v0.9 - env: - RANCHER_BEARER_TOKEN: ${{ secrets.RANCHER_BEARER_TOKEN }} - RANCHER_URL: 'https://rancher.mun.sh' - RANCHER_CLUSTER_ID: 'local' - RANCHER_PROJECT_ID: 'p-vdwsg' - RANCHER_NAMESPACE: 'default' - RANCHER_WORKLOADS: 'rgb' - - - name: deploy-dev - uses: th0th/rancher-redeploy-workload@v0.9 - env: - RANCHER_BEARER_TOKEN: ${{ secrets.RANCHER_BEARER_TOKEN }} - RANCHER_URL: 'https://rancher.mun.sh' - RANCHER_CLUSTER_ID: 'local' - RANCHER_PROJECT_ID: 'p-vdwsg' - RANCHER_NAMESPACE: 'default' - RANCHER_WORKLOADS: 'rgb-browser' \ No newline at end of file + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install deps + run: bun install + + - name: Build + run: bun run build + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + push: true + tags: ghcr.io/${{ github.repository }}:${{ env.TAG }} + + - name: deploy-dev + uses: th0th/rancher-redeploy-workload@v0.9 + env: + RANCHER_BEARER_TOKEN: ${{ secrets.RANCHER_BEARER_TOKEN }} + RANCHER_URL: 'https://rancher.mun.sh' + RANCHER_CLUSTER_ID: 'local' + RANCHER_PROJECT_ID: 'p-vdwsg' + RANCHER_NAMESPACE: 'default' + RANCHER_WORKLOADS: 'rgb' + + - name: deploy-dev + uses: th0th/rancher-redeploy-workload@v0.9 + env: + RANCHER_BEARER_TOKEN: ${{ secrets.RANCHER_BEARER_TOKEN }} + RANCHER_URL: 'https://rancher.mun.sh' + RANCHER_CLUSTER_ID: 'local' + RANCHER_PROJECT_ID: 'p-vdwsg' + RANCHER_NAMESPACE: 'default' + RANCHER_WORKLOADS: 'rgb-browser' diff --git a/.gitignore b/.gitignore index 3ec544c..b3eb616 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -.env \ No newline at end of file +.env +dist/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cc0063e..f1298ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,7 @@ -FROM node:16 +FROM oven/bun COPY . /app WORKDIR /app -RUN yarn - -CMD [ "yarn", "start" ] \ No newline at end of file +EXPOSE 3000 +CMD [ "bun", "src/server.ts" ] \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..8021484 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json index 2880289..7ac1901 100644 --- a/package.json +++ b/package.json @@ -5,24 +5,21 @@ "type": "module", "scripts": { "start": "vite --host --port 3000", + "serv": "bun --watch src/server.ts", + "build": "vite build", "headless": "docker run -d --rm -e TZ=Europe/London -p 9222:9222 zenika/alpine-chrome --no-sandbox --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 http://mun.sh:3000" }, "license": "MIT", "dependencies": { "@pixi/gif": "^2.1.0", - "@pixi/node": "^7.2.0", - "@types/express": "^4.17.17", - "@types/ws": "^8.5.4", - "discord.js": "^14.8.0", - "dotenv": "^16.0.3", + "bun-serve-express": "^1.0.4", "express": "^4.18.2", - "express-ws": "^5.0.2", "gsap": "^3.11.5", - "node-fetch": "^3.3.1", "pixi.js-legacy": "^7.2.1", - "tsx": "^3.12.5", - "vite": "^4.2.0", - "vite-plugin-mix": "^0.4.0", - "ws": "^8.13.0" + "vite": "^5.0.12" + }, + "devDependencies": { + "@types/bun": "^1.0.3", + "@types/express": "^4.17.21" } -} +} \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 368f884..2bffd69 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,73 +1,88 @@ +import { ServerWebSocket, WebSocketServeOptions } from 'bun'; +import bunExpress from 'bun-serve-express'; import express from 'express'; -import { OPEN, WebSocket, WebSocketServer } from 'ws'; -import { webSocketServer } from './ws/websocketServer'; -import * as dotenv from 'dotenv'; -import fetch from 'node-fetch'; -// import { Client as DiscordClient, Events, GatewayIntentBits, SlashCommandBuilder } from 'discord.js'; -dotenv.config(); +// import { webSocketServer } from './ws/websocketServer'; process.env.TZ = 'Europe/London'; -const app = express(); -const wssPub = webSocketServer(app, '/pub'); -const wssSub = webSocketServer(app, '/sub'); +type WebSocketData = { + url: URL; + id: string; +}; + +const randomId = () => { + return Math.random().toString(36).substring(2, 7); +}; + +const app = bunExpress({ + websocket: { + open(ws) { + ws.data.id = randomId(); + + if (ws.data.url.pathname == '/pub') { + pubSockets.push(ws); + console.log('new publisher', ws.data.id); + } + + if (ws.data.url.pathname == '/sub') { + subSockets.push(ws); + console.log('new subscriber', ws.data.id); + } + }, + message(ws, message) { + if (ws.data.url.pathname == '/pub') { + // Ensure is binary data + if (typeof message == 'string') return; + + // TODO figure out better solution + // Ensure only first connected WS data is forwarded to RGB screen + if (ws.data.id != pubSockets[0].data.id) return; + + // Echo data back to sub sockets + for (const sub of subSockets) { + sub.send(message); + } + } + + if (ws.data.url.pathname == '/sub') { + console.log('message from sub', message); + } + }, + close(ws, code, message) { + if (ws.data.url.pathname == '/pub') { + pubSockets.splice(pubSockets.indexOf(ws), 1); + console.log('publisher disconnected'); + } + + if (ws.data.url.pathname == '/sub') { + subSockets.splice(subSockets.indexOf(ws), 1); + console.log('subscriber disconnected'); + } + }, + drain(ws) {} + } +} as WebSocketServeOptions as any); + const RTTAuth = Buffer.from(`${process.env.RTT_USER}:${process.env.RTT_PASS}`).toString('base64'); const FirstBusAuth = process.env.FIRST_BUS_API_KEY; -const pubSockets: WebSocket[] = []; -const subSockets: WebSocket[] = []; - -wssPub.on('connection', (socket, req) => { - pubSockets.push(socket); - console.log('new publisher'); - - socket.on('message', (data, isBinary) => { - // Ensure is binary data - if (!isBinary) return; - - // TODO figure out better solution - // Ensure only first connected WS data is forwarded to RGB screen - if (socket != pubSockets[0]) return; - - // Echo data back to sub sockets - for (const sub of subSockets) { - sub.send(data); - } - }); - - socket.once('close', () => { - pubSockets.splice(pubSockets.indexOf(socket), 1); - console.log('publisher disconnected'); - }); -}); - -wssSub.on('connection', (socket, req) => { - subSockets.push(socket); - console.log('new subscriber'); +const pubSockets: ServerWebSocket[] = []; +const subSockets: ServerWebSocket[] = []; - socket.on('message', (data, isBinary) => { - // Ensure is binary data - console.log("message from sub", data); - }); - - socket.once('close', () => { - subSockets.splice(subSockets.indexOf(socket), 1); - console.log('subscriber disconnected'); - }); -}); +app.use(express.static('dist')); app.get('/api/train/:station', async (req, res) => { const response = await fetch(`https://api.rtt.io/api/v1/json/search/${req.params.station}`, { headers: { - 'Authorization': `Basic ${RTTAuth}`, + Authorization: `Basic ${RTTAuth}` } }); - let data = await response.json() as any; + let data = (await response.json()) as any; - // console.log(data); + const formatDate = (dateString: string) => { + if (!dateString) return null; - const formatDate = (dateString:string) => { const split = dateString.split(''); const hours = split[0] + split[1]; const mins = split[2] + split[3]; @@ -81,15 +96,15 @@ app.get('/api/train/:station', async (req, res) => { time: `${hours}:${mins}`, date: d }; - } - + }; + // https://www.realtimetrains.co.uk/about/developer/pull/docs/locationlist/ - let formatted = data.services.map((train:any) => ({ + let formatted = data.services.map((train: any) => ({ scheduled: formatDate(train.locationDetail.gbttBookedArrival), - arrives: formatDate(train.locationDetail.realtimeArrival), + arrives: formatDate(train.locationDetail.realtimeArrival) ?? formatDate(train.locationDetail.gbttBookedArrival), origin: train.locationDetail.origin[0].description, destination: train.locationDetail.destination[0].description, - platform: train.locationDetail.platform ?? train.locationDetail.destination[0].description == 'Leeds' ? "1" : "2", + platform: train.locationDetail.platform ?? train.locationDetail.destination[0].description == 'Leeds' ? '1' : '2', displayAs: train.locationDetail.displayAs // CALL, PASS, ORIGIN, DESTINATION, STARTS, TERMINATES, CANCELLED_CALL, CANCELLED_PASS })); @@ -101,82 +116,77 @@ const getBusStopIds = (stopName: string) => { if (stopName == 'kirkstall_lane') return ['450011444', '450011458']; throw new Error('stop not found: ' + stopName); -} +}; const getBusesByStopId = async (stop: string) => { const response = await fetch(`https://prod.mobileapi.firstbus.co.uk/api/v2/bus/stop/${stop}/departure`, { headers: { - 'x-app-key': `${FirstBusAuth}`, + 'x-app-key': `${FirstBusAuth}` } }); - let data = await response.json() as any; + let data = (await response.json()) as any; return data.data.attributes['live-departures'].concat(data.data.attributes['timetable-departures']).map(departure => ({ line: departure.line, scheduled: { - time: new Date(departure['scheduled-time'] || departure['departure-time']).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }), - date: new Date(departure['scheduled-time'] || departure['departure-time']), + time: new Date(departure['scheduled-time'] || departure['departure-time']).toLocaleTimeString('en-GB', { + hour: '2-digit', + minute: '2-digit' + }), + date: new Date(departure['scheduled-time'] || departure['departure-time']) }, expectedArrivalMins: departure['is-live'] ? Math.floor(departure['expected-time-in-seconds'] / 60) : null, occupancy: departure['occupancy'] ? departure['occupancy']['types'][0] : null })); -} +}; app.get('/api/bus/:stop', async (req, res) => { const stops = getBusStopIds(req.params.stop); const busses = (await Promise.all(stops?.map(sid => getBusesByStopId(sid)))).flat(); - const getArrivalOrScheduledTime = (bus) => { + const getArrivalOrScheduledTime = bus => { if (bus.expectedArrivalMins != null) { - return new Date(Date.now() + 1000 * 60 * bus.expectedArrivalMins) + return new Date(Date.now() + 1000 * 60 * bus.expectedArrivalMins); } return bus.scheduled.date; - } - + }; + res.json(busses.sort((a, b) => getArrivalOrScheduledTime(a) - getArrivalOrScheduledTime(b))); }); app.get('/api/flights/arrivals', async (req, res) => { const response = await fetch(`https://lba-flights.production.parallax.dev/arrivals`); - const data = await response.json() as any; - - const flights = data.map(fl => ({ - id: fl.flight_ident, - scheduled: { - time: new Date(fl.scheduled_time).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }), - date: new Date(fl.scheduled_time), - }, - origin: fl.airport_name, - message: fl.message ? fl.message.replace('Expected', 'exp').replace('Landed', 'lnd').replace('Now ', '') : null, - status: fl.status, - })).filter(fl => { - if (fl.status != 'LND') return true; - - const timeString = fl.message.split(" ")[1]; - const date = new Date(); - date.setHours(timeString.split(":")[0]); - date.setMinutes(timeString.split(":")[1]); - - return new Date().getTime() - date.getTime() <= 1000 * 60 * 5; - }); + const data = (await response.json()) as any; + + const flights = data + .map((fl: any) => ({ + id: fl.flight_ident, + scheduled: { + time: new Date(fl.scheduled_time).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }), + date: new Date(fl.scheduled_time) + }, + origin: fl.airport_name, + message: fl.message ? fl.message.replace('Expected', 'exp').replace('Landed', 'lnd').replace('Now ', '') : null, + status: fl.status + })) + .filter((fl: any) => { + if (fl.status != 'LND') return true; + + const timeString = fl.message.split(' ')[1]; + const date = new Date(); + date.setHours(timeString.split(':')[0]); + date.setMinutes(timeString.split(':')[1]); + + return new Date().getTime() - date.getTime() <= 1000 * 60 * 5; + }); res.json(flights); }); -// const client = new DiscordClient({ intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds] }); -// client.on('message', msg => { -// if (msg.content === 'ping') { -// msg.reply('Pong!'); -// } -// }); - -// client.once(Events.ClientReady, c => { -// console.log(`Ready! Logged in as ${c.user.tag}`); -// }); - -// client.login(process.env.DISCORD_CLIENT_TOKEN); -export const handler = app; +app.listen(3000, () => { + console.log('server started on', 3000); +}); diff --git a/src/utils.ts b/src/utils.ts index ea29da1..4a73324 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,29 +1,29 @@ import { Assets, BitmapText, Filter, IBitmapTextStyle } from 'pixi.js-legacy'; export function rgbaToRgb565(rgba: Uint8Array) { - const length = rgba.length / 4; - const rgb565 = new Uint16Array(length); + const length = rgba.length / 4; + const rgb565 = new Uint16Array(length); - for (let i = 0; i < length; i++) { - const r = rgba[i * 4]; - const g = rgba[i * 4 + 1]; - const b = rgba[i * 4 + 2]; + for (let i = 0; i < length; i++) { + const r = rgba[i * 4]; + const g = rgba[i * 4 + 1]; + const b = rgba[i * 4 + 2]; - const r5 = ((r * 31) / 255) | 0; - const g6 = ((g * 63) / 255) | 0; - const b5 = ((b * 31) / 255) | 0; + const r5 = ((r * 31) / 255) | 0; + const g6 = ((g * 63) / 255) | 0; + const b5 = ((b * 31) / 255) | 0; - const rgb = (r5 << 11) | (g6 << 5) | b5; - rgb565[i] = rgb; - } + const rgb = (r5 << 11) | (g6 << 5) | b5; + rgb565[i] = rgb; + } - // Convert Uint16Array to array of hex byte strings - const hexStrings = new Array(rgb565.length); - for (let i = 0; i < rgb565.length; i++) { - hexStrings[i] = '0x' + (rgb565[i] >> 8).toString(16).padStart(2, '0') + (rgb565[i] & 0xff).toString(16).padStart(2, '0'); - } + // Convert Uint16Array to array of hex byte strings + const hexStrings = new Array(rgb565.length); + for (let i = 0; i < rgb565.length; i++) { + hexStrings[i] = '0x' + (rgb565[i] >> 8).toString(16).padStart(2, '0') + (rgb565[i] & 0xff).toString(16).padStart(2, '0'); + } - return rgb565; + return rgb565; } const dotMatrixShader = ` @@ -58,32 +58,31 @@ void main() { // Create the PixiJS filter with the dot matrix shader export const dotMatrixFilter = (width: number, height: number) => - new Filter(undefined, dotMatrixShader, { - uResolution: [width, height], - dotSize: 0.35, // Adjust this value to change the dot size (0.0 - 1.0) - cellSize: 10 // Adjust this value to change the cell size (in pixels) - }); + new Filter(undefined, dotMatrixShader, { + uResolution: [width, height], + dotSize: 0.35, // Adjust this value to change the dot size (0.0 - 1.0) + cellSize: 10 // Adjust this value to change the cell size (in pixels) + }); - -await Assets.load('silkscreen.fnt'); -await Assets.load('pixel7.fnt'); +await Assets.load('https://cdn.mun.sh/silkscreen.fnt'); +await Assets.load('https://cdn.mun.sh/pixel7.fnt'); const textStyle: Partial = { - fontName: 'silkscreen', - fontSize: 8, - tint: 'white', - align: 'left', - letterSpacing: -1 + fontName: 'silkscreen', + fontSize: 8, + tint: 'white', + align: 'left', + letterSpacing: -1 }; -export const createText = (text: string, tint = 'yellow', font: 'silkscreen'|'pixel7' = 'silkscreen') => { - text = text.replaceAll(':', ' :'); - - return new BitmapText(text, { - ...textStyle, - tint, - fontName: font, - fontSize: font == 'silkscreen' ? 8 : 10, - letterSpacing: font == 'silkscreen' ? -1 : 0 - }); -}; \ No newline at end of file +export const createText = (text: string, tint = 'yellow', font: 'silkscreen' | 'pixel7' = 'silkscreen') => { + text = text.replaceAll(':', ' :'); + + return new BitmapText(text, { + ...textStyle, + tint, + fontName: font, + fontSize: font == 'silkscreen' ? 8 : 10, + letterSpacing: font == 'silkscreen' ? -1 : 0 + }); +}; diff --git a/src/views/transportScreen.ts b/src/views/transportScreen.ts index 57e3fde..36e4a5d 100644 --- a/src/views/transportScreen.ts +++ b/src/views/transportScreen.ts @@ -1,37 +1,37 @@ -import { Container } from "pixi.js-legacy"; -import { buses } from "./buses"; -import { flights } from "./flights"; -import { trains } from "./trains"; +import { Container } from 'pixi.js-legacy'; +import { buses } from './buses'; +import { flights } from './flights'; +import { trains } from './trains'; const interval = 1000 * 10; const getScreen = async function* (parent: Container) { - while (true) { - yield await buses(parent, 'kirkstall_lights'); - yield await buses(parent, 'kirkstall_lane'); - yield await trains(parent); - yield await flights(parent); - } -} + while (true) { + yield await buses(parent, 'kirkstall_lane'); + yield await buses(parent, 'kirkstall_lights'); + yield await trains(parent); + yield await flights(parent); + } +}; export const transportScreen = () => { - const display = new Container(); + const display = new Container(); - const it = getScreen(display); - let current: any = null; + const it = getScreen(display); + let current: any = null; - const nextScreen = async () => { - const next = await it.next(); + const nextScreen = async () => { + const next = await it.next(); - if (current) { - current.destroy(); - } + if (current) { + current.destroy(); + } - current = next.value; - } + current = next.value; + }; - nextScreen(); - setInterval(() => nextScreen(), interval) + nextScreen(); + setInterval(() => nextScreen(), interval); - return display; -} + return display; +}; diff --git a/vite.config.ts b/vite.config.ts index 4474be2..1061fe1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,8 @@ import { defineConfig } from 'vite'; -import mix from 'vite-plugin-mix'; export default defineConfig({ - server: { - hmr: !process.env.DISABLE_HMR, - }, - plugins: [ - mix.default({ - handler: './src/server.ts' - }) - ] + build: { + target: 'esnext', + minify: false + } });