diff --git a/client/src/api/missions.api.ts b/client/src/api/missions.api.ts index e8e397e..5ede7b0 100644 --- a/client/src/api/missions.api.ts +++ b/client/src/api/missions.api.ts @@ -126,7 +126,7 @@ export const startMission = async (id: number, time: string) => { headers: { 'Content-Type': 'application/json' }, }, ); - return response.data as { message: string }; + return response; }; export const endMission = async (id: number, time: string) => { diff --git a/client/src/app/missions/[id]/page.tsx b/client/src/app/missions/[id]/page.tsx index d127700..cf099e7 100644 --- a/client/src/app/missions/[id]/page.tsx +++ b/client/src/app/missions/[id]/page.tsx @@ -26,6 +26,22 @@ export default function MissionDetail() { console.log('Updating mission:', id); if (start) { + + try { + // Push update to the database + const response = start + ? await startMission(missionId, time) + : await endMission(missionId, time); + + if (response.status == 200) { + console.log("mission " + missionId + " sent: " + response.data.message); + } + + } catch (error) { + alert(`Mission failed to start: ${error.response?.data?.error || error.message}`); + return; + } + mission!.timeStart = time; } else if (!start) { mission!.timeEnd = time; @@ -49,9 +65,6 @@ export default function MissionDetail() { } setMission(mission!); - // Push update to the database - const response = await (start ? startMission(missionId, time) : endMission(missionId, time)); - console.log(`Mission ${start ? 'started' : 'ended'}:`, response); }; const handleDelete = async (missionId: number, missionName: string) => { diff --git a/server/src/controllers/mission.controller.mjs b/server/src/controllers/mission.controller.mjs index d9b3a11..79bfcde 100644 --- a/server/src/controllers/mission.controller.mjs +++ b/server/src/controllers/mission.controller.mjs @@ -58,7 +58,7 @@ export async function getMissionByIdController(req, res) { res.status(500).json({ error: error.message }); } } - +// call mavlink sendmission corrdinaytion in this method export async function createMissionController(req, res) { try { const body = req.body || {}; diff --git a/server/src/index.mjs b/server/src/index.mjs index 7b77c88..b49e37a 100644 --- a/server/src/index.mjs +++ b/server/src/index.mjs @@ -2,7 +2,7 @@ import express from 'express'; import cors from 'cors'; import { createServer } from 'http'; import { Server } from 'socket.io'; - +import { sendMissionCoordinates, handleMavlinkData } from "./services/mavlink.service.mjs"; import { serverConfig } from './config/server.config.mjs'; import missionRoutes from './routes/mission.routes.js'; import botRoutes from './routes/bot.routes.mjs'; @@ -21,6 +21,12 @@ const io = new Server(httpServer, { } }); + + + + + + // Middleware app.use(cors({ origin: serverConfig.corsOrigin, @@ -34,6 +40,47 @@ app.use('/api/missions', missionRoutes); app.use('/api/bots', botRoutes); app.use('/api/temperature', temperatureRoutes); + +// ================= ROUTES ================= + +app.post('/api/send-coordinates', async (req, res) => { + try { + let coords = req.body; + + if (!coords || Object.keys(coords).length === 0) { + console.log("No coordinates provided. Generating random test coordinates..."); + + coords = { + lat1: 499394300, + lon1: -1193964200, + lat2: 499394500, + lon2: -1193964500, + lat3: 499394700, + lon3: -1193964300, + lat4: 499394900, + lon4: -1193964700 + }; + } + botID = 1; + console.log("Website requested send-coordinates (botID = 1):"); + console.log(coords); + + await sendMissionCoordinates(botID, coords); + + return res.status(200).json({ + success: true, + message: "Mission coordinates successfully sent to robot.", + sent: coords + }); + + } catch (error) { + console.error("Error sending coordinates:", error); + return res.status(500).json({ error: "Failed to send mission coordinates." }); + } +}); + + + // Setup Socket.IO handlers setupSocketHandlers(io); @@ -41,8 +88,9 @@ setupSocketHandlers(io); setStoreMavlinkDataCallback((data) => storeMavlinkData(data, io)); // Start MAVLink data simulation -// handleMavlinkData(); // for real data -simulateMavlinkData(); // for simulated data +handleMavlinkData(); // for real data +// simulateMavlinkData(); // for simulated data + // Start server httpServer.listen(serverConfig.port, () => { @@ -50,3 +98,5 @@ httpServer.listen(serverConfig.port, () => { console.log(`WebSocket server ready`); console.log(`Environment: ${serverConfig.environment}`); }); + + diff --git a/server/src/services/database.service.mjs b/server/src/services/database.service.mjs index a1a164b..aeb1d35 100644 --- a/server/src/services/database.service.mjs +++ b/server/src/services/database.service.mjs @@ -1,5 +1,6 @@ import { pool } from '../config/database.config.mjs'; import assert from 'assert'; +import { sendMissionCoordinates } from "./mavlink.service.mjs"; // Helper to parse JSON columns safely const parseJSON = (value, fallback = null) => { @@ -378,6 +379,43 @@ export async function startMission(missionId, startTime, bots) { `UPDATE bot SET assignmentStatus = 'active' WHERE botID IN (${bots.map(() => '?').join(',')})`, bots ); + + // 1. Get the assigned botIDs +const botIDs = await getAssignmentsForMission(missionId); + + +if (!botIDs || botIDs.length === 0) { + return { success: true }; // nothing to send +} + +// 2. Get mission data +const missionResult = await getMissionByID(missionId); +if (!missionResult.success) { + throw new Error("Mission not found"); +} + +const mission = missionResult.data; + +// Parse coordinates safely +const area = parseJSON(mission.areaCoordinates); + +if (!area) { + throw new Error("Invalid areaCoordinates"); +} + +// 3. Construct coords object (4 corners) +const coords = { + lat1: area.north, lon1: area.west, // top-left + lat2: area.north, lon2: area.east, // top-right + lat3: area.south, lon3: area.east, // bottom-right + lat4: area.south, lon4: area.west // bottom-left +}; + +// 4. Send to each bot +for (const botID of botIDs) { + await sendMissionCoordinates(botID, coords); +} + return { success: true }; } catch (error) { console.error('Error starting mission:', error); diff --git a/server/src/services/mavlink.service.mjs b/server/src/services/mavlink.service.mjs index fb073d0..ec94110 100644 --- a/server/src/services/mavlink.service.mjs +++ b/server/src/services/mavlink.service.mjs @@ -1,13 +1,11 @@ +// mavlinkHandler.mjs import { SerialPort } from "serialport"; import mavlink from "node-mavlink"; +import { MavLinkProtocolV2, send} from 'node-mavlink'; import fetch from "node-fetch"; let storeMavlinkDataCallback = null; -export function setStoreMavlinkDataCallback(callback) { - storeMavlinkDataCallback = callback; -} - const { MavLinkPacketSplitter, MavLinkPacketParser, @@ -17,69 +15,170 @@ const { ardupilotmega, } = mavlink; -//create a registry of mappings between msg id and data +export function setStoreMavlinkDataCallback(callback) { + storeMavlinkDataCallback = callback; +} + +// --- START CHANGE: Singleton Serial Port Setup --- +let serialPort = null; + +function getSerialPort() { + if (!serialPort) { + serialPort = new SerialPort({ + path: "/dev/ttyUSB0", // <-- Change this if you use by-id path + baudRate: 57600, + }); + + serialPort.on("open", () => { + console.log("Serial port open."); + }); + + serialPort.on("error", (err) => { + console.error("Serial port error:", err); + }); + } + return serialPort; +} +// --- END CHANGE: Singleton Serial Port Setup --- + + + +// Registry for message parsing const REGISTRY = { ...minimal.REGISTRY, ...common.REGISTRY, ...ardupilotmega.REGISTRY, }; -// substitute /dev/ttyACM0 with your serial port! +/// +function encodeMissionData(numTempReadings , coords) { -function handleMavlinkData() { + const { lat1, lon1, lat2, lon2, lat3, lon3, lat4, lon4 } = coords; + + const lats = [lat1,lat2,lat3,lat4]; + const lons = [lon1,lon2,lon3,lon4]; + + // Create a buffer for the full MAVLink payload (249 bytes) + const buffer = Buffer.alloc(249); + let offset = 0; - //serialPort.close(); - const portSerialNumber = "/dev/ttyUSB0"; + // Pack number of temperature readings + buffer.writeInt32LE(numTempReadings, offset); + offset += 4; + + // Pack Latitudes (Int32, Little Endian) + lats.forEach(lat => { + buffer.writeInt32LE((lat*1e7), offset); + offset += 4; + }); - const serialPort = new SerialPort({ - path: portSerialNumber, - baudRate: 57600, + // Pack Longitudes (Int32, Little Endian) + lons.forEach(lon => { + buffer.writeInt32LE(lon*1e7, offset); + offset += 4; }); - //constructing a reader that will emit each packet separately - const mavlinkRead = serialPort - .pipe(new MavLinkPacketSplitter()) - .pipe(new MavLinkPacketParser()); - console.log("hello from mavlink"); - //storeMavlinkData(1); + // Return the 249-byte array ready for MAVLink + return Array.from(buffer); +} +/// + +// --- START CHANGE: Reuse serial port for mission upload --- +let receivedAck = false; + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} - //setup mavlink to listen for packets - mavlinkRead.on("data", async (packet) => { - console.log("Packet received"); +async function sendMissionCoordinates(botId, numTempReadings , coords) { +/// + const payload = encodeMissionData(numTempReadings , coords); + + const msg = new common.LoggingDataAcked(); + msg.target_system = botId; + msg.target_component = 0; + msg.seq = 0; + msg.length = 249; + msg.first_message_offset = 0; + msg.data = payload; + + console.log("Sending mission data to bot " + botId + "..."); + + const port = getSerialPort(); + + await send(port, msg, new MavLinkProtocolV2()); + + let timer = 0; + const timeLimit = 5; // maximum wait time(in seconds) to receive acknowledgement + let countSend = 1; + const sendLimit = 3; // limit of amount time for data to be sent until acknowledgement is received + while(receivedAck == false && countSend <= sendLimit) { + while(receivedAck == false && timer <= timeLimit) { + await sleep(1000); + timer++; + } + timer = 0; // reset the timer + + if (receivedAck == true) { + console.log("Received acknowledgement from bot", botId); + } + else { + console.error("Did not receive acknowledgement from bot", botId); + console.log("Trying again..."); + await send(port, msg, new MavLinkProtocolV2()); + } + countSend++; + } + if(receivedAck == false) { + throw new Error("Failed to receive acknowledgement from bot " + botId + + " after trying " + sendLimit + " times"); + } + receivedAck = false; +} +// --- END CHANGE: Reuse serial port for mission upload --- + +// --- START CHANGE: Reuse serial port for reading MAVLink --- +function handleMavlinkData() { + const port = getSerialPort(); // <-- REUSED SINGLETON + + // constructing a reader that will emit each packet separately + const mavlinkRead = port + .pipe(new mavlink.MavLinkPacketSplitter()) + .pipe(new mavlink.MavLinkPacketParser()); + + console.log("MAVLink reader initialized."); + + mavlinkRead.on("data", (packet) => { const clazz = REGISTRY[packet.header.msgid]; if (clazz) { const data = packet.protocol.data(packet.payload, clazz); data.timeBootMs = new Date(); - //process the parsed data based on type switch (clazz.MSG_NAME) { case "GLOBAL_POSITION_INT": - console.log("GLOBAL_POSITION_INT"); + console.log("GLOBAL_POSITION_INT received"); processGlobalPositionMessage(data); break; case "NAMED_VALUE_FLOAT": - console.log("NAMED_VALUE_FLOAT"); + console.log("NAMED_VALUE_FLOAT received"); processTemperatureMessage(data); break; + case "LOGGING_ACK": + console.log("LOGGING_ACK received"); + receivedAck = true; + break; default: console.log("Unknown message type:", clazz.MSG_NAME); } } }); - mavlinkRead.on("error", (error) => { - console.error("Error reading Mavlink data:", error); + mavlinkRead.on("error", (err) => { + console.error("Error reading MAVLink:", err); }); } +// --- END CHANGE: Reuse serial port for reading MAVLink --- -/* - Function to simulate incoming temp/position/battery data, trying to copy the format of data objects that previous devs were processing in below functions (processTemperatureMessage and processGlobalPositionMessage, which I have mostly not touched since then). I assumed that the data format for temp and position data will stay the same as Apr 2024, because I was told that this code worked during the demo last year. - - This function simulates - 1) Every 5 seconds, either position or temperature data with a 50/50 chance, coming in from a bot with random ID between 1-5 - 2) Every 15 seconds, data for percentage battery remaining for all 5 bots. - - NOTE: data format for simulated battery data is arbitrary; i.e. the keys do not correspond to actual incoming data, because battery percentage is new and I do not yet know the format in which battery % will be sent by the bot. Once format is finalized, function processBatteryMessage() as well as the 'battery' table in DB must both be changed, and the battery simulation part of the function will break unless also changed accordingly. -*/ +// Simulate data (unchanged) function simulateMavlinkData() { console.log("Simulating MAVLink data..."); const NUM_SIMULATED_BOTS = 3; @@ -221,5 +320,4 @@ function processBatteryMessage(data) { // storeMavlinkData(batteryData); } - -export { handleMavlinkData, simulateMavlinkData }; +export { handleMavlinkData, simulateMavlinkData, sendMissionCoordinates }; \ No newline at end of file