diff options
| author | Arne Rief <riearn@proton.me> | 2025-12-22 21:20:39 +0100 |
|---|---|---|
| committer | Arne Rief <riearn@proton.me> | 2025-12-22 21:20:39 +0100 |
| commit | e836e7dd4ed5e9fa60e949d159100040b22a8f48 (patch) | |
| tree | a11954c06e55e8ef53fcb634fa5954dfcb42ffc3 /backend | |
| parent | d1b64ddd78d8b8dc3eca76038a75071ab2a575d9 (diff) | |
Movement simulator for all and single robot, project v1 ready
Diffstat (limited to 'backend')
| -rw-r--r-- | backend/src/controllers/createRobot.ts | 18 | ||||
| -rw-r--r-- | backend/src/controllers/getRobots.ts | 14 | ||||
| -rw-r--r-- | backend/src/controllers/loginUser.ts | 32 | ||||
| -rw-r--r-- | backend/src/controllers/moveAllRobots.ts | 25 | ||||
| -rw-r--r-- | backend/src/controllers/moveRobot.ts | 30 | ||||
| -rw-r--r-- | backend/src/controllers/stopAllRobots.ts | 21 | ||||
| -rw-r--r-- | backend/src/controllers/stopRobot.ts | 27 | ||||
| -rw-r--r-- | backend/src/routes/router.ts | 10 | ||||
| -rw-r--r-- | backend/src/simulation/robotMovementSimulator.ts | 241 | ||||
| -rw-r--r-- | backend/src/types/express.d.ts | 5 | ||||
| -rw-r--r-- | backend/src/types/robot.ts | 30 | ||||
| -rw-r--r-- | backend/src/types/user.ts | 7 |
12 files changed, 430 insertions, 30 deletions
diff --git a/backend/src/controllers/createRobot.ts b/backend/src/controllers/createRobot.ts index fa483af..dbc1959 100644 --- a/backend/src/controllers/createRobot.ts +++ b/backend/src/controllers/createRobot.ts @@ -1,14 +1,16 @@ import { Request, Response } from "express"; import { QueryResult } from "pg"; +import { Server } from "socket.io"; import db from "../database/postgres.js"; import redisClient from "../database/redis.js"; +import { ErrorResponse } from "../types/error.js"; import { CreateRequest } from "../types/request.js"; -import { Robot } from "../types/robot.js"; +import { CreateRobotResponse, Robot } from "../types/robot.js"; const ROBOTS_CACHE_KEY = "allMyRobots"; async function createRobot(req: Request, res: Response) { - const io = req.app.get("io"); + const io: Server = req.app.get("io"); const { name } = req.body as CreateRequest; @@ -44,16 +46,20 @@ async function createRobot(req: Request, res: Response) { io.emit("robots_update", allRobots); console.log("WebSocket update with newly created robot."); - return res.status(201).json({ + const createRobotResponse: CreateRobotResponse = { message: "Robot successfully created.", robot: newRobot, - }); + }; + + return res.status(201).json(createRobotResponse); } catch (error) { console.error("Error creating the robot: ", error); - return res.status(500).json({ + + const errorResponse: ErrorResponse = { message: "Internal server error during robot creation.", error, - }); + }; + return res.status(500).json(errorResponse); } } diff --git a/backend/src/controllers/getRobots.ts b/backend/src/controllers/getRobots.ts index b634306..80b430f 100644 --- a/backend/src/controllers/getRobots.ts +++ b/backend/src/controllers/getRobots.ts @@ -17,9 +17,14 @@ async function getRobots(_req: Request, res: Response) { console.log("Data served from Redis cache."); const robots: Robot[] = JSON.parse(cachedData); + const simulationRunning = robots?.every( + (robot) => robot.status === "moving" + ); + const response: RobotsResponse = { source: "cache", - data: robots, + robots, + simulationRunning, }; return res.status(200).json(response); @@ -31,6 +36,10 @@ async function getRobots(_req: Request, res: Response) { ); const robots = robotsQuery.rows; + const simulationRunning = robots?.every( + (robot) => robot.status === "moving" + ); + await redisClient.set(ROBOTS_CACHE_KEY, JSON.stringify(robots), { EX: CACHE_TTL, }); @@ -38,7 +47,8 @@ async function getRobots(_req: Request, res: Response) { console.log("Cache miss: data queried from PostgreSQL."); const response: RobotsResponse = { source: "database", - data: robots, + robots, + simulationRunning, }; return res.status(200).json(response); diff --git a/backend/src/controllers/loginUser.ts b/backend/src/controllers/loginUser.ts index 860bce2..9cfb992 100644 --- a/backend/src/controllers/loginUser.ts +++ b/backend/src/controllers/loginUser.ts @@ -3,15 +3,20 @@ import { Request, Response } from "express"; import jwt from "jsonwebtoken"; import { QueryResult } from "pg"; import db from "../database/postgres.js"; +import { ErrorResponse } from "../types/error.js"; import type { LoginRequest } from "../types/request.js"; -import type { AuthorizedUser, DatabaseUser } from "../types/user.js"; +import type { + AuthorizedUser, + DatabaseUser, + LoginResponse, +} from "../types/user.js"; async function loginUser(req: Request, res: Response) { const { email, password } = req.body as LoginRequest; if (!email || !password) { return res.status(400).json({ - message: "E-Mail und Passwort sind erforderlich.", + message: "E-mail and password are required.", }); } @@ -25,7 +30,7 @@ async function loginUser(req: Request, res: Response) { const user = queryResult.rows[0]; if (!user) { - return res.status(401).json({ message: "Login Daten ungültig." }); + return res.status(401).json({ message: "Login data invalid." }); } // Check if password is correct @@ -36,7 +41,7 @@ async function loginUser(req: Request, res: Response) { if (!isValidPassword) { return res.status(401).json({ - message: "Das Passwort ist nicht korrekt.", + message: "The password is incorrect.", }); } @@ -49,16 +54,21 @@ async function loginUser(req: Request, res: Response) { // Create token for authentication const token = jwt.sign(userData, process.env.JWT_SECRET!); - return res.status(200).json({ - message: "Erfolgreiche Anmeldung.", + const loginResponse: LoginResponse = { + message: "Successful login.", user: userData, token, - }); + }; + + return res.status(200).json(loginResponse); } catch (error) { - console.error("Fehler beim Login: ", error); - return res - .status(500) - .json({ message: "Interner Serverfehler beim Login.", error }); + console.error("Error on login attempt: ", error); + + const errorResponse: ErrorResponse = { + message: "Internal server error on login attempt.", + error, + }; + return res.status(500).json(errorResponse); } } diff --git a/backend/src/controllers/moveAllRobots.ts b/backend/src/controllers/moveAllRobots.ts new file mode 100644 index 0000000..e4d1252 --- /dev/null +++ b/backend/src/controllers/moveAllRobots.ts @@ -0,0 +1,25 @@ +import { Request, Response } from "express"; +import { Server } from "socket.io"; +import { setAllRobotsMoving } from "../simulation/robotMovementSimulator"; +import { ErrorResponse } from "../types/error"; +import { SimulationResponse } from "../types/robot"; + +async function moveAllRobots(req: Request, res: Response) { + const io: Server = req.app.get("io"); + + try { + const result: SimulationResponse = await setAllRobotsMoving(io); + return res.status(200).json(result); + } catch (error) { + console.error("Error on trying to set all robots moving:", error); + + const errorResponse: ErrorResponse = { + message: + "Internal server error on trying to set all robots moving.", + error, + }; + return res.status(500).json(errorResponse); + } +} + +export default moveAllRobots; diff --git a/backend/src/controllers/moveRobot.ts b/backend/src/controllers/moveRobot.ts new file mode 100644 index 0000000..1b53f17 --- /dev/null +++ b/backend/src/controllers/moveRobot.ts @@ -0,0 +1,30 @@ +import { Request, Response } from "express"; +import { Server } from "socket.io"; +import { setRobotMoving } from "../simulation/robotMovementSimulator"; +import { ErrorResponse } from "../types/error"; +import { SimulationResponse } from "../types/robot"; + +async function moveRobot(req: Request, res: Response) { + const io: Server = req.app.get("io"); + + const robotId = Number(req.params.id); + + if (!robotId || Number.isNaN(robotId)) { + return res.status(400).json({ message: "Invalid robot ID." }); + } + + try { + const result: SimulationResponse = await setRobotMoving(io, robotId); + return res.status(200).json(result); + } catch (error) { + console.error(`Error on trying to start robot ID ${robotId}: `, error); + + const errorResponse: ErrorResponse = { + message: `Internal server error on trying to start robot ID ${robotId}.`, + error, + }; + return res.status(500).json(errorResponse); + } +} + +export default moveRobot; diff --git a/backend/src/controllers/stopAllRobots.ts b/backend/src/controllers/stopAllRobots.ts new file mode 100644 index 0000000..7218e66 --- /dev/null +++ b/backend/src/controllers/stopAllRobots.ts @@ -0,0 +1,21 @@ +import { Request, Response } from "express"; +import { setAllRobotsIdle } from "../simulation/robotMovementSimulator"; +import { ErrorResponse } from "../types/error"; +import { SimulationResponse } from "../types/robot"; + +async function stopAllRobots(_req: Request, res: Response) { + try { + const result: SimulationResponse = await setAllRobotsIdle(); + return res.status(200).json(result); + } catch (error) { + console.error("Error on trying to stop all robots:", error); + + const errorResponse: ErrorResponse = { + message: "Internal server error on trying to stop all robots.", + error, + }; + return res.status(500).json(errorResponse); + } +} + +export default stopAllRobots; diff --git a/backend/src/controllers/stopRobot.ts b/backend/src/controllers/stopRobot.ts new file mode 100644 index 0000000..d0d7c4f --- /dev/null +++ b/backend/src/controllers/stopRobot.ts @@ -0,0 +1,27 @@ +import { Request, Response } from "express"; +import { setRobotIdle } from "../simulation/robotMovementSimulator"; +import { ErrorResponse } from "../types/error"; +import { SimulationResponse } from "../types/robot"; + +async function stopRobot(req: Request, res: Response) { + const robotId = Number(req.params.id); + + if (!robotId || Number.isNaN(robotId)) { + return res.status(400).json({ message: "Ungültige Roboter ID." }); + } + + try { + const result: SimulationResponse = await setRobotIdle(robotId); + return res.status(200).json(result); + } catch (error) { + console.error(`Error on trying to stop robot ID ${robotId}: `, error); + + const errorResponse: ErrorResponse = { + message: `Internal server error on trying to stop robot ID ${robotId}.`, + error, + }; + return res.status(500).json(errorResponse); + } +} + +export default stopRobot; diff --git a/backend/src/routes/router.ts b/backend/src/routes/router.ts index b526a9b..72fd885 100644 --- a/backend/src/routes/router.ts +++ b/backend/src/routes/router.ts @@ -3,6 +3,10 @@ import createRobot from "../controllers/createRobot.js"; import generateAdmin from "../controllers/generateAdmin.js"; import getRobots from "../controllers/getRobots.js"; import loginUser from "../controllers/loginUser.js"; +import moveAllRobots from "../controllers/moveAllRobots.js"; +import moveRobot from "../controllers/moveRobot.js"; +import stopAllRobots from "../controllers/stopAllRobots.js"; +import stopRobot from "../controllers/stopRobot.js"; import authenticateUser from "../middleware/authCheck.js"; const router = Router(); @@ -15,6 +19,12 @@ router.post("/auth/login", loginUser); router.get("/robots", authenticateUser, getRobots); // Create a new robot; protected route router.post("/robots", authenticateUser, createRobot); +// All robots move or stop; protected routes +router.post("/robots/move", authenticateUser, moveAllRobots); +router.post("/robots/stop", authenticateUser, stopAllRobots); +// Single robot move or stop; protected routes +router.post("/robots/:id/move", authenticateUser, moveRobot); +router.post("/robots/:id/stop", authenticateUser, stopRobot); export default router; diff --git a/backend/src/simulation/robotMovementSimulator.ts b/backend/src/simulation/robotMovementSimulator.ts new file mode 100644 index 0000000..2299f43 --- /dev/null +++ b/backend/src/simulation/robotMovementSimulator.ts @@ -0,0 +1,241 @@ +import { QueryResult } from "pg"; +import { Server } from "socket.io"; +import db from "../database/postgres.js"; +import { + Robot, + RobotPosition, + RobotsUpdateBroadcast, + SimulationResponse, +} from "../types/robot.js"; + +// Coordinates for the boundaries of the frontend map +const LEIPZIG_AREA = { + WEST: 12.22, + SOUTH: 51.26, + EAST: 12.54, + NORTH: 51.44, +} as const; + +const LEIPZIG_CENTER = { + LAT: 51.340863, + LON: 12.375919, +} as const; + +const SIMULATION_INTERVAL_MS = 2000; + +// IDs of moving robots +const movingRobots = new Set<number>(); + +// Global simulation timer +let simulationTimer: NodeJS.Timeout | null = null; + +/* + MOVEMENT FUNCTION + generate random position within Leipzig based on current position +*/ + +function generateRandomPosition(currentLat: string, currentLon: string) { + // 0.0005 degrees ca. 50-70 meters + const deltaLat = (Math.random() - 0.5) * 0.0005; + const deltaLon = (Math.random() - 0.5) * 0.0005; + + let newLat = parseFloat(currentLat) + deltaLat; + let newLon = parseFloat(currentLon) + deltaLon; + + // Check if robot's new position would be outside of Leipzig, if yes set back to center + const outsideLatBoundary = + newLat < LEIPZIG_AREA.SOUTH || newLat > LEIPZIG_AREA.NORTH; + const outsideLonBoundary = + newLon < LEIPZIG_AREA.WEST || newLon > LEIPZIG_AREA.EAST; + + if (outsideLatBoundary || outsideLonBoundary) { + newLat = LEIPZIG_CENTER.LAT; + newLon = LEIPZIG_CENTER.LON; + } + + return { + lat: newLat.toFixed(7), + lon: newLon.toFixed(7), + }; +} + +/* + MAIN FUNCTION: + move a robot, update database, websocket broadcast +*/ + +async function updateRobotPositions(io: Server) { + let client; + + try { + client = await db.connect(); + + const allRobotsQuery: QueryResult<Robot> = await client.query( + "SELECT * FROM robots ORDER BY id;" + ); + const allRobots = allRobotsQuery.rows; + + // Update database + await client.query("BEGIN"); + + const updatedRobots: Robot[] = []; + + for (const robot of allRobots) { + // If not moving, keep position & set idle + if (!movingRobots?.has(robot.id)) { + if (robot.status === "moving") { + const idleQuery: QueryResult<Robot> = await client.query( + ` + UPDATE robots + SET status = 'idle', + updated_at = CURRENT_TIMESTAMP + WHERE id = $1 + RETURNING *; + `, + [robot.id] + ); + updatedRobots.push(idleQuery.rows[0]); + } else { + updatedRobots.push(robot); + } + continue; + } + + // If moving, assign new position and add previous to log + const newPos: RobotPosition = generateRandomPosition( + robot.lat, + robot.lon + ); + + const previousPositionsLog: RobotPosition[] = + robot.robot_positions || []; + + previousPositionsLog.push({ lat: robot.lat, lon: robot.lon }); + + // Limit log to 10 entries, remove oldest when more + if (previousPositionsLog.length > 10) { + previousPositionsLog.shift(); + } + + const updateQuery: QueryResult<Robot> = await client.query( + ` + UPDATE robots + SET + lat = $1, + lon = $2, + status = 'moving', + robot_positions = $3::jsonb, + updated_at = CURRENT_TIMESTAMP + WHERE id = $4 + RETURNING *; + `, + [ + newPos.lat, + newPos.lon, + JSON.stringify(previousPositionsLog), + robot.id, + ] + ); + + updatedRobots.push(updateQuery.rows[0]); + } + + await client.query("COMMIT"); + + // Websocket broadcast, make sure robots stay in correct order + updatedRobots.sort((a, b) => a.id - b.id); + + const broadcastPayload: RobotsUpdateBroadcast = { updatedRobots }; + + io.emit("robots_update", broadcastPayload); + + // Check if this was the final update, setting all to idle with no more robots moving + if (movingRobots.size === 0) { + if (simulationTimer) { + clearInterval(simulationTimer); + simulationTimer = null; + } + } + } catch (error) { + console.error("Error in the global simulation timer: ", error); + if (client) await client.query("ROLLBACK"); + + // Stop simulation on error + if (simulationTimer) { + clearInterval(simulationTimer); + simulationTimer = null; + } + } finally { + if (client) client.release(); + } +} + +/* + SIMULATION FUNCTION + Runs the simulation by repeating the main function in intervals +*/ + +async function startSimulation(io: Server) { + if (simulationTimer) return; // Already running + + // Immediate execution right away without delay + await updateRobotPositions(io); + + // Then interval for subsequent updates + simulationTimer = setInterval(async () => { + await updateRobotPositions(io); + }, SIMULATION_INTERVAL_MS); +} + +/* + PUBLIC CONTROLLER FUNCTIONS: + start or stop movement of one or all robots +*/ + +export async function setRobotMoving( + io: Server, + robotId: number +): Promise<SimulationResponse> { + movingRobots.add(Number(robotId)); + await startSimulation(io); + + return { + message: `Robot ID ${robotId} set in motion.`, + status: "moving", + }; +} + +export async function setRobotIdle( + robotId: number +): Promise<SimulationResponse> { + movingRobots.delete(Number(robotId)); + + return { + message: `Robot ID ${robotId} stopped.`, + status: "idle", + }; +} + +export async function setAllRobotsMoving( + io: Server +): Promise<SimulationResponse> { + const allRobotIDsQuery: QueryResult<{ id: Robot["id"] }> = await db.query( + "SELECT id FROM robots;" + ); + allRobotIDsQuery.rows.forEach((queryObject) => + movingRobots.add(queryObject.id) + ); + + await startSimulation(io); + + return { + message: "All robots set in motion.", + }; +} + +export async function setAllRobotsIdle(): Promise<SimulationResponse> { + movingRobots.clear(); + return { + message: "All robots stopped.", + }; +} diff --git a/backend/src/types/express.d.ts b/backend/src/types/express.d.ts index 266a9f9..7e93345 100644 --- a/backend/src/types/express.d.ts +++ b/backend/src/types/express.d.ts @@ -1,12 +1,7 @@ import type { AuthorizedUser } from "./user.js"; -import { Server } from "socket.io"; declare global { namespace Express { - interface Application { - io: Server; - } - interface Request { user?: AuthorizedUser; } diff --git a/backend/src/types/robot.ts b/backend/src/types/robot.ts index 40ce282..fe20422 100644 --- a/backend/src/types/robot.ts +++ b/backend/src/types/robot.ts @@ -1,14 +1,16 @@ export type RobotPosition = { - lat: number; - lon: number; + lat: string; + lon: string; }; +export type RobotStatus = "idle" | "moving"; + export type Robot = { id: number; name: string; - status: "idle" | "moving"; - lat: number; - lon: number; + status: RobotStatus; + lat: string; + lon: string; robot_positions: RobotPosition[]; created_at: string; updated_at: string; @@ -16,5 +18,21 @@ export type Robot = { export type RobotsResponse = { source: "cache" | "database"; - data: Robot[]; + robots: Robot[]; + simulationRunning: boolean; +}; + +export type CreateRobotResponse = { + message: string; + robot: Robot; +}; + +export type RobotsUpdateBroadcast = { + updatedRobots: Robot[]; +}; + +export type SimulationResponse = { + message: string; + status?: RobotStatus; }; + diff --git a/backend/src/types/user.ts b/backend/src/types/user.ts index b2c7ffc..15b24c3 100644 --- a/backend/src/types/user.ts +++ b/backend/src/types/user.ts @@ -16,3 +16,10 @@ export type DatabaseUser = { password_hash: string; created_at: string; }; + +export type LoginResponse = { + message: string; + user: AuthorizedUser; + token: string; +}; + |
