Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cb55d06
feat(sensor): make pressure and temperature grow linearly with speed …
vasyl-ks Oct 11, 2025
f5ce86c
chore(sensor): rename driving mode from "speed" to "sport" for clarity
vasyl-ks Oct 11, 2025
20ab7ac
chore(config): change default ports to 5173
vasyl-ks Oct 11, 2025
39ba611
fix(sync): ensure config and dependent processes wait for initializat…
vasyl-ks Oct 11, 2025
1d9b3a5
refactor(consumer/logger): implement multiple scoped loggers with rot…
vasyl-ks Oct 11, 2025
6d6f890
feat(): improve backend visibility with structured log messages
vasyl-ks Oct 11, 2025
023d245
refactor(model): rename Min*/Max* fields to Minimum*/Maximum* for cla…
vasyl-ks Oct 11, 2025
ee38503
refactor: change variable naming and insert logs for arriving commands
maximka76667 Oct 12, 2025
af6b783
Merge pull request #1 from maximka76667/backend/vasyl_task4
vasyl-ks Oct 12, 2025
0961f49
chore(config): change default WS port to 3000
vasyl-ks Oct 16, 2025
36af6b4
refactor(main): move main.go and main_test.go to cmd/app/
vasyl-ks Oct 16, 2025
442c690
fix(hub/wshandler): improve error output clarity
vasyl-ks Oct 16, 2025
f2d5a4c
feat(frontend): add frontend as git submodule
vasyl-ks Oct 16, 2025
63bbb6e
feat(deploy): add start.sh to run backend and frontend
vasyl-ks Oct 16, 2025
4439b37
docs: update README
vasyl-ks Oct 16, 2025
c321455
Merge branch 'backend/vasyl_task4' of https://github.com/vasyl-ks/TM-…
vasyl-ks Oct 16, 2025
431d912
fix(frontend): update frontend
vasyl-ks Oct 16, 2025
62fd665
docs: update README
vasyl-ks Oct 16, 2025
09fbbc7
fix(hub): remove debug log
vasyl-ks Oct 16, 2025
4597ecf
docs: update README
vasyl-ks Oct 16, 2025
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "frontend"]
path = frontend
url = https://github.com/maximka76667/TM-software-H11
147 changes: 91 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# TM Software H11: Task 3
# TM Software H11: Final Stage

This Go project simulates a complete vehicle telemetry system.
It connects multiple components — a **Generator**, **Hub**, **Consumer**, and a **Frontend (WebSocket)** — that exchange telemetry and control commands in real time.
It connects multiple components — a **Generator**, **Hub**, **Consumer**, and a **Frontend (WebSocket)** — that exchange telemetry, control commands in real time and visualizes metrics.

> This project was developed over the course of **four weeks** as part of the **Training Month (TM)** as a **Backend Engineer** for **Hyperloop UPV**.
> The different *branches* reflect the project’s progress throughout the four weeks.
> **Backend** components (contained in this repository) were implemented by me, while the **frontend** —included as a Git [*submodule*](https://github.com/maximka76667/TM-software-H11) — was developed by **[maximka76667](https://github.com/maximka76667)**.
> In the final stage, both parts were integrated into a fully functional end-to-end system.

## Table of Contents
* [Features](#features)
Expand All @@ -12,40 +17,53 @@ It connects multiple components — a **Generator**, **Hub**, **Consumer**, and
* [Development Notes](#development-notes)

## Features
* Synthetic **Generator** that simulates a vehicle, producing random sensor data (speed, pressure, temperature).
* Synthetic **Generator** models speed, pressure, and temperature, reacting to `start`, `stop`, `accelerate`, and `mode` commands with mode-aware speed caps.
* **Hub** that routes data and commands between Generator, Consumer, and Frontend:

* fans telemetry to the frontend (WebSocket) and consumer (UDP) while forwarding commands from the frontend to the generator (channels) and consumer (TCP).
* `ResultData` is sent to both the Frontend (WS) and Consumer (UDP).
* `Command` messages flow from the Frontend (WS) to the Generator and Consumer (TCP).
* **Consumer** that receives and logs both telemetry results and commands in rotating `.jsonl` files.
* Real-time **WebSocket communication** with the Frontend for live telemetry and remote control.
* Dynamic **command handling**: the vehicle can start, stop, accelerate, or change driving mode (`eco`, `normal`, `speed`).
* Centralized **config system** controlling intervals, modes, and ports.
* Includes an automated **test suite** that simulates a frontend connection sending commands and logging responses.
* **Consumer** listens on UDP/TCP, autodetects `ResultData` vs `Command` payloads, and rotates structured `.jsonl` logs across `logs/`, `logs/data/`, and `logs/commands/`.
* **React frontend** (Vite + Tailwind) offers connect/disconnect controls, command groups, toast feedback, and metric tiles that track the latest batch stats in real time.
* Central **config package** exposes runtime tuning parameters — settings that define how the system behaves when running, such as sensor cadence, aggregation windows, port bindings, log rotation, and vehicle identity.
* End-to-end **integration test** (`cmd/app/main_test.go`) spins up the stack, drives scripted WebSocket commands, and records the telemetry stream under `test/`.
* `start.sh` boots the Go backend and frontend dev server together for a single command developer experience.

## Repository Layout

```
TM-software-H11/
│ .gitignore
│ .gitmodules
│ config.json
│ go.mod
│ go.sum
│ main_test.go
│ main.go
│ README.md
│ start.sh
├───cmd
│ └───app
│ main.go
│ main_test.go
├───config
│ config.go
├───frontend
│ │ package.json
│ │ package-lock.json
│ └───src
├───internal
│ ├───consumer
│ │ consumer.go
│ │ listener.go
│ │ logger.go
│ │ parser.go
│ │
│ ├───generator
│ │ generator.go
│ │ processor.go
│ │ sensor.go
│ │
│ ├───hub
│ │ hub.go
│ │ tcphandler.go
Expand All @@ -56,79 +74,96 @@ TM-software-H11/
│ command.go
│ resultData.go
│ sensorData.go
├───logs
└───test_logs
│ ├───commands
│ └───data
└───test
test_logs.jsonl
```

## Getting Started

1. Install Go 1.21 or newer.
2. Clone the repository and enter the project directory:
1. Install Go 1.25.1 or newer and Node.js 20+ (with npm).
2. Clone the repository, change into it and update the submodule:

```bash
git clone https://github.com/vasyl-ks/TM-software-H11.git
cd TM-software-H11
git submodule update --init --recursive

```
3. Inspect and adjust `config.json` for your desired intervals, ports, range and mode ratios.
4. Install frontend dependencies:

```bash
cd frontend
npm install
```
5. Run the stack:

```bash
cd ..
./start.sh
```
3. Inspect and adjust `config.json` for your desired intervals, ports, and speed mode ratios.
4. Run the system:

> The script launches `go run ./cmd/app/main.go` and `npm run dev` (Vite). Stop with `Ctrl+C`.
6. To run only the backend:

```bash
go run main.go
go run ./cmd/app/main.go
```
5. Optionally, execute tests to simulate a frontend:

> Then start the frontend separately with `npm run dev` inside `frontend/` (use `-- --host` if you need LAN access).
7. To execute the integration test (writes logs under `test/`):

```bash
go test -v
go test ./cmd/app -run TestFrontendSimulation -v
```
6. Watch logs under `/logs` — telemetry and commands are saved as `.jsonl` files.
8. Inspect telemetry and command logs in `logs/` after running. Files rotate automatically when `maxLines` is reached.

## Configuration
`config.json` defines the runtime behavior and communication parameters:
* **Vehicle**
* `vehicleID`: unique identifier for telemetry.
* **Sensor**
* `intervalMilliSeconds`: how often new sensor data is generated.
* `minSpeed`, `maxSpeed`, `minPressure`, `maxPressure`, `minTemp`, `maxTemp`: generation ranges.
* `ecoMode`, `normalMode`, `speedMode`: scaling ratios for maximum speed behavior.
* **Processor**
* `intervalMilliSeconds`: how often readings are aggregated into statistics.
* **Logger**
* `maxLines`: maximum lines per log file before rotation.
* `fileDir`: directory for log storage.
* **Hub**
* `udpPort`, `tcpPort`, `wsPort`: network ports for communication.
* `bufferSize`: size for UDP/TCP packet buffers.
Configuration is read once at process start; update the file and restart the application to apply changes.
`config.json` governs how the system behaves:
* **vehicle**
* `vehicleID`: identifier stamped on telemetry batches.
* **sensor**
* `intervalMilliSeconds`: cadence for raw SensorData generation.
* `minSpeed`, `maxSpeed`, `minPressure`, `maxPressure`, `minTemp`, `maxTemp`: randomization bounds.
* `ecoMode`, `normalMode`, `speedMode`: relative limits applied when each driving mode is active.
* **processor**
* `intervalMilliSeconds`: aggregation window for computing averages/min/max.
* **logger**
* `maxLines`: number of log entries before a new file is created.
* `fileDir`: root folder for combined, data-only, and command-only `.jsonl` logs.
* **hub**
* `udpPort`, `tcpPort`, `wsPort`: loopback endpoints used by consumer and frontend.
* `bufferSize`: byte buffer used by UDP/TCP readers.

Configuration loads once on startup via `config.LoadConfig()`. Update the file and restart to apply changes.

## System Flow
1. **Generator**
* `Sensor` continuously emits simulated sensor readings.
* Receives `Command` messages to alter vehicle behavior (start, stop, accelerate, mode).
* `Process` aggregates data into `ResultData` summaries and sends them to the Hub.
* `Sensor` emits random-but-mode-aware speed, pressure, and temperature readings and reacts to incoming commands.
* `Process` batches readings for the configured interval, fan-outs calculations across goroutines, and forwards summarized `ResultData`.
2. **Hub**
* Acts as the central bridge between Generator, Frontend, and Consumer.
* Forwards telemetry (`ResultData`) to:
* Consumer (via UDP)
* Frontend (via WebSocket)
* Forwards control commands (`Command`) from the Frontend (via WebSocket) to:
* Generator (via internal channel)
* Consumer (via TCP)
* Registers `/api/stream` and upgrades HTTP requests to WebSocket connections.
* Streams each `ResultData` batch to connected frontend and the consumer (UDP) while duplicating commands to generator (channels) and consumer (TCP).
3. **Consumer**
* Listens for UDP results and TCP commands.
* Parses both `ResultData` and `Command` messages.
* Logs entries into rotating `.jsonl` files with timestamps.
* Opens UDP and TCP listeners (signalling readiness through `consumer.Ready`).
* Differentiates telemetry vs command payloads, then logs each to rotating files with timestamps.
4. **Frontend**
* Connects via WebSocket to `/api/stream`.
* Sends commands (`{"action": "start"}` etc.) and receives live telemetry.
* Uses a WebSocket hook to connect on demand, show connection status, render the latest metrics, and send predefined commands or custom acceleration values.
* Provides toast notifications for connect/disconnect, command results, and validation feedback.
5. **Tests**
* `main_test.go` simulates a frontend connection, sends commands with delays, and validates Hub responses.
* Watch test logs under `/test_logs` — saved as `.jsonl` files.
* `TestFrontendSimulation` spins up the services, drives a scripted command sequence, captures the WebSocket stream, and persists the interaction under `test/test_logs.jsonl`.

## Development Notes
* The system is fully concurrent, using goroutines and channels for communication.
* Goroutines and channels orchestrate concurrency; `consumer.Ready` ensures network listeners are up before the hub dials TCP/UDP.
* The system is fully concurrent, using goroutines and channels for communication.
* Each transport layer (UDP, TCP, WS) runs independently but shares data via the Hub.
* WebSocket handlers handle graceful close frames and distinguish expected vs unexpected disconnects for cleaner logs.
* Generator speed adjusts based on commands in real time.
* Temperature and pressure are randomly generated based on speed, and they increase or decrease at different rates depending on the mode.
* Frontend tests provide an end-to-end check of the communication pipeline.
* Logs in `.jsonl` format are machine- and human-readable, suitable for further analysis.
* At this moment, temperature and pressure values are independent and randomly generated; they are not linked to vehicle state.
* Once the program starts, the vehicle begins sending telemetry automatically, but it must be started and accelerated through commands to simulate motion.
16 changes: 9 additions & 7 deletions main.go → cmd/app/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"log"

"github.com/vasyl-ks/TM-software-H11/config"
consumer "github.com/vasyl-ks/TM-software-H11/internal/consumer"
generator "github.com/vasyl-ks/TM-software-H11/internal/generator"
Expand All @@ -16,23 +18,23 @@ Start loads configuration values, creates an internal channel, and then calls th
The final "select {}" keep the program running indefinitely.
*/
func Start() {
log.Println("[INFO][Main] Running")

// Load configuration (const variables)
config.LoadConfig()

// Wait for config to finish
<-config.Done

// Creates internal channel of ResultData and Command between Generator and Hub.
resultChan := make(chan modelPkg.ResultData)
commandChan := make(chan modelPkg.Command)

// Run Generator, Hub and Consumer.
go generator.Run(commandChan, resultChan)

// Consumer must initialize UDP&TCP listeners, before Hub tries to connect.
ready := make(chan struct{})
go consumer.Run(ready)
<-ready

go consumer.Run()
<-consumer.Ready // Wait for consumer to initialize UDP&TCP listeners, before Hub tries to connect.
go hub.Run(resultChan, commandChan)


select {}
}
Expand Down
5 changes: 4 additions & 1 deletion main_test.go → cmd/app/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ func TestFrontendSimulation(t *testing.T) {
defer logFile.Close()
logger := log.New(logFile, "", log.LstdFlags)

// Wait for config to finish
<-config.Done

// Connect to running Hub
wsURL := fmt.Sprintf("ws://localhost:%d/api/stream", config.Hub.WSPort)
dialer := websocket.Dialer{HandshakeTimeout: 3 * time.Second}
Expand All @@ -56,7 +59,7 @@ func TestFrontendSimulation(t *testing.T) {
{"action": "start"},
{"action": "accelerate", "params": 130},
{"action": "accelerate", "params": -10},
{"action": "mode", "params": "speed"},
{"action": "mode", "params": "sport"},
{"action": "accelerate"},
{"action": "accelerate", "params": 40},
{"action": "mode", "params": "eco"},
Expand Down
2 changes: 1 addition & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"hub": {
"udpPort": 10000,
"tcpPort": 10000,
"wsPort": 10000,
"wsPort": 3000,
"bufferSize": 1024
}
}
13 changes: 10 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package config

import (
"encoding/json"
"fmt"
"log"
"os"
"time"
)
Expand Down Expand Up @@ -49,12 +49,17 @@ var Processor processor
var Logger logger
var Hub hub

// Exported channel to signal when config finishes loading
var Done = make(chan struct{})

// LoadConfig reads config.json and configures
func LoadConfig() {
defer log.Println("[INFO][Config] Loaded.")

// Open the file
file, err := os.Open("config.json")
if err != nil {
fmt.Println("Error loading configuration: ", err)
log.Println("[ERROR][Config] Error opening config file: ", err)
return
}
defer file.Close()
Expand All @@ -72,7 +77,7 @@ func LoadConfig() {
}{}
err = decoder.Decode(&temp)
if err != nil {
fmt.Println("Error loading configuration: ", err)
log.Println("[ERROR][Config] Error decoding config struct: ", err)
return
}

Expand All @@ -86,4 +91,6 @@ func LoadConfig() {
// Derive time.Duration to Seconds
Sensor.Interval = time.Duration(Sensor.I) * time.Millisecond
Processor.Interval = time.Duration(Processor.I) * time.Millisecond

close(Done)
}
1 change: 1 addition & 0 deletions frontend
Submodule frontend added at c07485
8 changes: 6 additions & 2 deletions internal/consumer/consumer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package consumer

import (
"log"

"github.com/vasyl-ks/TM-software-H11/internal/model"
)

Expand All @@ -10,14 +12,16 @@ Consumer initializes the byteChan and jsonChan channels, and calls the Listen, P
- Parse receives a JSON from byteChan, parses it to ResultData and sends it through resultChan.
- Log receives a ResultData from resultChan and logs it.
*/
func Run(ready chan<- struct{}) {
func Run() {
defer log.Println("[INFO][Consumer] Running.")

// Create unbuffered channels.
byteChan := make(chan []byte)
resultChan := make(chan model.ResultData)
commandChan := make(chan model.Command)

// Launch concurrent goroutines.
go Listen(byteChan, ready)
go Listen(byteChan)
go Parse(byteChan, resultChan, commandChan)
go Log(resultChan, commandChan)
}
Loading