summaryrefslogtreecommitdiff
path: root/backend/src/simulation/robotMovementSimulator.ts
diff options
context:
space:
mode:
authorArne Rief <riearn@proton.me>2025-12-22 21:20:39 +0100
committerArne Rief <riearn@proton.me>2025-12-22 21:20:39 +0100
commite836e7dd4ed5e9fa60e949d159100040b22a8f48 (patch)
treea11954c06e55e8ef53fcb634fa5954dfcb42ffc3 /backend/src/simulation/robotMovementSimulator.ts
parentd1b64ddd78d8b8dc3eca76038a75071ab2a575d9 (diff)
Movement simulator for all and single robot, project v1 ready
Diffstat (limited to 'backend/src/simulation/robotMovementSimulator.ts')
-rw-r--r--backend/src/simulation/robotMovementSimulator.ts241
1 files changed, 241 insertions, 0 deletions
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.",
+ };
+}