Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Deployment available at: https://telemetry-beta.calgarysolarcar.ca
- [Main Goals of Telemetry ML](./docs/ML.md#main-goals-of-telemetry-ml)
- [Tools Used](./docs/ML.md#we-are-exploring-how-to-do-this-through-tools-like)
- [Other Responsibilities](./docs/ML.md#the-telemetry-ml-team-does-not-solely-focus-on-machine-learning-models-however-there-are-several-considerations-that-need-to-be-made-in-the-following-areas)
- [Grafana + Visualizer Setup](./docs/GRAFANA.md)

### Development Setup

Expand Down
90 changes: 90 additions & 0 deletions docs/GRAFANA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Grafana + Telemetry Visualizer Setup

This document explains how to run the Grafana visualizer together with the Helios telemetry backend and a test MQTT publisher. Follow these steps to run everything locally.

## Overview

You will run three repositories locally (or in Docker):

- `Helios-Telemetry` — backend + Aedes MQTT server
- `Telemetry-Visualizer` — Grafana docker setup that hosts dashboards
- `Helios-Mqtt-Webserver-Test` — MQTT client container to publish test messages

## Prerequisites

- Node 18+
- Docker & `docker-compose`
- Ask your lead for required environment variable files for each repo.

## 1) Helios-Telemetry

1. Get the repo and set the environment variables provided by your lead in `packages/server/.env`.
2. Start the server (runs the Aedes MQTT server and backend):

```bash
yarn dev:server
```

Note: When running test MQTT publishers you may not want those fake packets written into DynamoDB. Locate the packet handler in the server code (the `handlePacketReceive` flow) and comment out the DB insert line when running locally:

```typescript
// this.dynamoDB.insertPacketData(message); // comment out for local testing
```

## 2) Telemetry-Visualizer

1. Clone or open the repo: `https://github.com/UCSolarCarTeam/Telemetry-Visualizer`
2. Start Grafana using docker-compose:

```bash
cd Telemetry-Visualizer/grafana
docker-compose up -d
```

After the container is up, open Grafana at `http://localhost:3000`.

Default login:

- Username: `admin`
- Password: `admin`

## 3) Helios-Mqtt-Webserver-Test (MQTT client)

This repo provides a test MQTT publisher to simulate telemetry.

1. Clone or open the repo: `https://github.com/UCSolarCarTeam/Helios-Mqtt-Webserver-Test`
2. Build and run the test publisher (this will publish test packets to the backend MQTT broker):

```bash
cd Helios-Mqtt-Webserver-Test
docker-compose up --build
```

Do not commit `.env` files containing secrets to the repository. Use a secure secrets manager or share them privately with your teammates.

## Accessing Grafana

Once the Grafana container is running, open:

```
http://localhost:3000
```

Login as `admin` / `admin` and verify the dashboards and data source configuration.

## WebSocket (Grafana) — quick setup

1. Add the WebSocket path environment variable to the server env file used by `Helios-Telemetry` (i.e. `packages/server/.env`):

```bash
GRAFANA_WS_PATH=/grafana-ws
```

2. Restart the backend if it was already running so it picks up the new environment variable.

3. In Grafana (open `http://localhost:3000` and login):
- Click **Explore** in the left-hand menu.
- For **Data source**, choose **WebSocket API** (the WebSocket data source in the Telemetry-Visualizer setup).
- In the **Field** input enter: `$.data`

You should now see the telemetry payload returned by the backend (the `data` object) in the Explore panel. The backend uses the `GRAFANA_WS_PATH` value for the WebSocket endpoint path (default `/grafana-ws`).
61 changes: 61 additions & 0 deletions packages/server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Example environment variables for the server package
# Copy this file to .env.local (or .env) and fill in real values.
# DO NOT commit real secrets. This file contains only example placeholders.

########################
# Database (DynamoDB) #
########################
LAP_TABLE_NAME="your-dynamodb-lap-table-name"
PACKET_TABLE_NAME="your-dynamodb-packet-table-name"
DRIVER_TABLE_NAME="your-dynamodb-driver-table-name"
GPS_CALCULATED_LAP_DATA_TABLE="your-dynamodb-gps-lap-table-name"
GPS_TABLE_NAME="your-dynamodb-gps-table-name"

########################
# Loki / Logger #
########################
# When LOKI_URL is set the server will POST logs to Loki's push API.
LOKI_URL="" # e.g. "https://loki.example.com" or "http://loki:3100"

# Authentication options (choose one):
# 1) Provide a base64 encoded username:password
LOKI_BASIC_AUTH="" # e.g. "dXNlcjpwYXNzd29yZA=="
# OR
# 2) Provide username/password and the logger will encode them automatically
LOKI_USERNAME=""
LOKI_PASSWORD=""

# Optional static labels for Loki streams (comma-separated key=value)
# Example: "app=helios,env=development,team=ucsolarcar"
LOKI_LABELS=""

########################
# MQTT #
########################
MQTT_USERNAME="primaryuser"
MQTT_PASSWORD="changeme"

########################
# App secrets / other #
########################
LAP_POSITION_PASSWORD="changeme"
NEXT_PUBLIC_MAPSAPIKEY="pk.YOUR_PUBLIC_MAP_KEY_HERE"

########################
# Local logging / misc #
########################
LOG_DIR="logs"
LOG_FILE="app.log"
LOG_LEVEL="info" # e.g. "debug", "info", "warn", "error"
LOG_ERROR_STACK="false" # When "true", include error stacks in console output

########################
# Node / Environment #
########################
NODE_ENV="development"
ENV="development"

########################
# Grafana WebSocket #
########################
GRAFANA_WS_PATH=/grafana-ws
11 changes: 5 additions & 6 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"@faker-js/faker": "^9.0.3",
"@godaddy/terminus": "^4.12.1",
"@shared/helios-types": "*",
"@types/sqlite3": "^3.1.11",
"aedes": "^0.51.2",
"axios": "^1.7.2",
"axios-retry": "^4.0.0",
Expand All @@ -26,12 +25,12 @@
"log4js": "^6.9.1",
"module-alias": "^2.2.3",
"mqtt": "^5.8.0",
"rimraf": "^5.0.5",
"socket.io": "^4.7.5",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.5.3"
"typescript": "^5.5.3",
"ws": "^8.18.3"
},
"devDependencies": {
"@timescaledb/core": "^0.0.1",
Expand All @@ -43,6 +42,7 @@
"@types/node": "^20.14.10",
"@types/prettier": "^3.0.0",
"@types/sqlite3": "^3.1.11",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"eslint": "8.57.0",
Expand All @@ -54,7 +54,6 @@
"eslint-plugin-typescript-sort-keys": "^3.2.0",
"globals": "^15.8.0",
"nodemon": "^3.1.0",
"prettier": "^3.3.3",
"rimraf": "^5.0.5"
"prettier": "^3.3.3"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,29 @@ import DynamoDB from "@/datasources/DynamoDB/DynamoDB";
import { SocketIO } from "@/datasources/SocketIO/SocketIO";
import { SolarMQTTClient } from "@/datasources/SolarMQTTClient/SolarMQTTClient";
import { options } from "@/datasources/SolarMQTTClient/SolarMQTTClient.types";
import { DatabaseManager } from "@/database/DatabaseManager";
import { NativeWebSocket } from "@/datasources/WebSocket/WebSocket";

import { DatabaseManager } from "@/database/DatabaseManager";
import { logger } from "@/index";
import { type ITelemetryData } from "@shared/helios-types";

const grafanaWsPath = process.env.GRAFANA_WS_PATH ?? "/grafana-ws";

//getDriverInfo
export class BackendController implements BackendControllerTypes {
public dynamoDB: DynamoDB;
public socketIO: SocketIO;
public lapController: LapController;
public mqtt: SolarMQTTClient;
public webSocket: NativeWebSocket;
public databaseManager: DatabaseManager;
public carLatency: number;
constructor(
httpsServer: Server<typeof IncomingMessage, typeof ServerResponse>,
) {
this.dynamoDB = new DynamoDB(this);
this.socketIO = new SocketIO(httpsServer, this);
this.webSocket = new NativeWebSocket(grafanaWsPath, httpsServer, this);
this.mqtt = new SolarMQTTClient(options, this);
this.lapController = new LapController(this);
this.databaseManager = DatabaseManager.getInstance();
Expand Down Expand Up @@ -66,6 +71,9 @@ export class BackendController implements BackendControllerTypes {
// Broadcast the packet to the frontend
this.socketIO.broadcastPacket(message);

// Broadcast the packet to the native web socket
// this.webSocket.broadcastPacket(message); // only send the ILapData through WS for now

// Handle the packet in the lap controller
await this.lapController.handlePacket(message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { LapController } from "@/controllers/LapController/LapController";
import DynamoDB from "@/datasources/DynamoDB/DynamoDB";
import type { SocketIO } from "@/datasources/SocketIO/SocketIO";
import type { SolarMQTTClient } from "@/datasources/SolarMQTTClient/SolarMQTTClient";
import type { NativeWebSocket } from "@/datasources/WebSocket/WebSocket";

import type { ITelemetryData } from "@shared/helios-types";

Expand All @@ -14,4 +15,5 @@ export interface BackendControllerTypes {
lapController: LapController;
mqtt: SolarMQTTClient;
socketIO: SocketIO;
webSocket: NativeWebSocket;
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export class LapController implements LapControllerType {
// this function is for calling when lap completes via lap digital being true
public async handleLapData(lapData: ILapData) {
await this.backendController.socketIO.broadcastLapData(lapData);
await this.backendController.webSocket.broadcastLapData(lapData);
await this.backendController.dynamoDB.insertLapData(lapData);
}

Expand Down Expand Up @@ -190,7 +191,11 @@ export class LapController implements LapControllerType {
this.handleGeofenceLap(packet.Pi.Rfid, packet.TimeStamp);
}

if (packet.B3.LapDigital && this.lastLapPackets.length > 5) {
if (
//TEST: commented out this condition for broadcasting lap data to grafana
// packet.B3.LapDigital &&
this.lastLapPackets.length > 5
) {
await this.backendController.socketIO.broadcastLapComplete();
// mark lap, calculate lap, and add to lap table in database
// send lap over socket
Expand Down
90 changes: 90 additions & 0 deletions packages/server/src/datasources/WebSocket/WebSocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { IncomingMessage, Server, ServerResponse } from "http";
import { type WebSocket, WebSocketServer } from "ws";

import { type BackendController } from "@/controllers/BackendController/BackendController";

import { type WebSocketType } from "@/datasources/WebSocket/WebSocket.types";

import { createLightweightApplicationLogger } from "@/utils/logger";

import type { ILapData, ITelemetryData } from "@shared/helios-types";

const logger = createLightweightApplicationLogger("WebSocket.ts");

export class NativeWebSocket implements WebSocketType {
wss: WebSocketServer;
backendController: BackendController;
constructor(
wsPath: string,
httpsServer: Server<typeof IncomingMessage, typeof ServerResponse>,
backendController: BackendController,
) {
this.backendController = backendController;
this.wss = new WebSocketServer({
path: wsPath,
server: httpsServer,
});

this.wss.on("connection", (ws: WebSocket, req: IncomingMessage) => {
logger.info(
`WebSocket client connected from ${req.socket.remoteAddress}`,
);
this.initializeWebSocketListeners(ws);
ws.send(
JSON.stringify({
message: "Connected to Helios Telemetry WebSocket",
type: "connection",
}),
);
});
}

public broadcastPacket(packet: ITelemetryData): void {
if (this.wss.clients.size === 0) {
logger.debug("No WebSocket clients connected, skipping broadcast");
return;
}
const message = {
data: packet,
type: "packet",
};
this.wss.clients.forEach((ws) => {
try {
ws.send(JSON.stringify(message));
} catch (error) {
logger.error("Error broadcasting telemetry packet to WS", error);
}
});
}

public broadcastLapData(lapData: ILapData): void {
if (this.wss.clients.size === 0) {
logger.debug("No WebSocket clients connected, skipping broadcast");
return;
}

const message = {
data: lapData,
type: "lapData",
};
this.wss.clients.forEach((ws) => {
try {
ws.send(JSON.stringify(message));
} catch (error) {
logger.error("Error broadcasting lap data to WS", error);
}
});
}

public initializeWebSocketListeners(ws: WebSocket): void {
ws.on("close", () => {
logger.info("WebSocket client disconnected");
});

ws.on("error", (error: Error) => {
logger.error("WebSocket error:", error);
});
}
}

export default NativeWebSocket;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { WebSocketServer } from "ws";

import type { ILapData, ITelemetryData } from "@shared/helios-types";

export interface WebSocketType {
broadcastLapData(lapData: ILapData): void;
broadcastPacket(packet: ITelemetryData): void;
wss: WebSocketServer;
}
Loading