Skip to content
This repository was archived by the owner on Jan 9, 2026. It is now read-only.
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ To run the project and start developing or testing:
git clone https://github.com/proveskit/proves_ground_station
cd proves_ground_station
yarn
yarn dev
yarn dev (sudo yarn dev if on linux)
```

Ensure you have [prettier](https://prettier.io/) set up on your editor for code formatting, as CI & the Husky pre-commit hook will fail if code isn't formatted to prettier's standards.
69 changes: 13 additions & 56 deletions packages/frontend/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useContext, useEffect, useState } from "react";
import { FaSync } from "react-icons/fa";
import { useContext, useEffect } from "react";
import { NavLink, Outlet } from "react-router";
import { SatelliteContext } from "../context/SatelliteContext";
import { WebsocketContext } from "../context/WebsocketContext";
Expand All @@ -12,33 +11,29 @@ const LINKS: { name: string; href: string }[] = [
];

export default function Navbar() {
const [usbDevices, setUsbDevices] = useState<string[]>([]);
const [activeDevice, setActiveDevice] = useState<string>("");

const socket = useContext(WebsocketContext);
const connectionCtx = useContext(SatelliteContext)!;

useEffect(() => {
if (socket) {
socket.on("usb-devices", (data) => {
setUsbDevices(data);
setActiveDevice(data[0]);
socket.on("check-proves-connected", (data) => {
connectionCtx.setConnection(data);
});

socket.on("check-connected", (data) => {
socket.on("usb-connected", (data) => {
socket.emit("register-event-listeners");
connectionCtx.setConnection(data);
});

socket.emit("check-connected");
socket.emit("usb-devices");
socket.on("usb-disconnected", () => {
connectionCtx.setConnection({ connected: false });
});

socket.emit("check-proves-connected");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [socket]);

const refreshDevices = () => {
socket?.emit("usb-devices");
};

return (
<div>
<div className="w-full h-14 bg-blue-600 px-6 flex items-center justify-between text-white">
Expand All @@ -51,49 +46,11 @@ export default function Navbar() {
</div>
<div>
<div className="flex gap-3 items-center">
<button onClick={refreshDevices} className="hover:cursor-pointer">
<FaSync />
</button>
{usbDevices.length > 0 ? (
<select
value={activeDevice}
className="border-2 h-8 rounded-sm border-neutral-400"
>
{usbDevices.map((device, idx) => (
<option onClick={() => setActiveDevice(device)} key={idx}>
{device}
</option>
))}
</select>
{connectionCtx.connection.connected ? (
<p>PROVESKit Connected</p>
) : (
<p className="text-white">No devices found!</p>
<p>Plug in PROVESKit to connect</p>
)}
{usbDevices.length > 0 &&
(!connectionCtx.connection.connected ? (
<button
className="bg-white text-black px-3 py-2 rounded-md hover:cursor-pointer"
onClick={() => {
socket?.emit("connect-device", activeDevice);
connectionCtx.setConnection({
connected: true,
deviceName: activeDevice,
connectedAt: Date.now(),
});
}}
>
Connect
</button>
) : (
<button
className="bg-red-500 text-white px-3 py-2 rounded-md hover:cursor-pointer"
onClick={() => {
socket?.emit("disconnect-device");
connectionCtx.setConnection({ connected: false });
}}
>
Disconnect
</button>
))}
</div>
</div>
</div>
Expand Down
10 changes: 5 additions & 5 deletions packages/frontend/src/context/websocket/WebsocketProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { WebsocketContext } from "./context";
export function WebsocketProvider({ children }: { children: React.ReactNode }) {
const [socket, setSocket] = useState<Socket | null>(null);

const connectSocket = () => {
setSocket(io("http://localhost:3000"));
};

useEffect(() => {
if (!socket) {
connectSocket();
setSocket(io("http://localhost:3000"));
}

return () => {
socket?.disconnect();
};
}, [socket]);

return (
Expand Down
4 changes: 4 additions & 0 deletions packages/frontend/src/routes/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export default function Home() {
setMsgs((prev) => [data, ...prev]);
});
}

return () => {
socket?.off("terminal-data");
};
}, [socket]);

return (
Expand Down
112 changes: 80 additions & 32 deletions packages/server/src/socket.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { exec } from "child_process";
import { Server as HttpServer } from "http";
import { ReadlineParser, SerialPort } from "serialport";
import { autoDetect } from "@serialport/bindings-cpp";
import { Server, Socket } from "socket.io";

type ConnectionState = ConnectedConnectionState | DisconnectedConnectionState;
Expand All @@ -26,6 +26,9 @@ class SerialManager {
this.parser = null;
this.connected = { connected: false };
this.listeners = [];

// begin looping until the usb is connected
loopUntilUsbConnected();
}

registerListeners(socket: Socket) {
Expand All @@ -37,22 +40,21 @@ class SerialManager {
this.listeners.push("send-command");
this.listeners.push("enter-repl");
this.listeners.push("exit-repl");
this.listeners.push("check-connected");

// register listeners
socket.on("send-command", (msg) => this.sendMessage(msg));
socket.on("enter-repl", () => this.enterRepl());
socket.on("exit-repl", () => this.exitRepl());
socket.on("check-connected", () => {
this.checkConnected(socket);
});
}

connect(socket: Socket, device: string) {
async connect() {
if (this.conn) return;

const device = await checkProvesConnected();
if (!device.status) return;

this.conn = new SerialPort({
path: device,
path: device.path,
baudRate: 9600,
dataBits: 8,
parity: "none",
Expand All @@ -61,32 +63,39 @@ class SerialManager {

this.parser = this.conn.pipe(new ReadlineParser({ delimiter: "\r\n" }));

this.conn.on("close", () => {
console.log("USB disconnected");
io.emit("usb-disconnected");
this.disconnect();
});

this.connected = {
connected: true,
deviceName: device,
deviceName: device.path,
connectedAt: Date.now(),
};

this.registerListeners(socket);
io.emit("usb-connected", this.connected);
}

disconnect(socket: Socket) {
disconnect() {
if (!this.conn) return;

this.conn?.close();
this.conn = null;
this.parser = null;
this.connected = { connected: false };

for (const l of this.listeners) {
socket.removeAllListeners(l);
io.removeAllListeners(l);
}

this.listeners = [];

// begin looping until the usb is connected again
loopUntilUsbConnected();
}

checkConnected(socket: Socket) {
console.log("???????????????/");
socket.emit("check-connected", this.connected);
}

Expand Down Expand Up @@ -129,29 +138,68 @@ export function initializeWs(server: HttpServer) {
}

// Websocket Events
socket.on("usb-devices", () => {
exec("ls /dev/tty.usbmodem*", (error, stdout) => {
if (error) {
socket.emit("usb-devices", []);
return;
}
socket.emit(
"usb-devices",
stdout.split("\n").filter((d) => d !== ""),
);
});
socket.on("register-event-listeners", () => {
manager.registerListeners(socket);
});

socket.on("connect-device", (device) => {
manager.connect(socket, device);
socket.on("check-proves-connected", async () => {
socket.emit("check-proves-connected", manager.connected);
});
});
}

socket.on("disconnect-device", () => {
manager.disconnect(socket);
});
// Find the PROVESKit serial port
async function findDevicePort() {
const bindings = autoDetect();

socket.on("check-connected", () => {
socket.emit("check-connected", manager.connected);
try {
const ports = await bindings.list();
const device = ports.find((port) => {
// check for the proveskit vendorId and productId
return port.vendorId === "1209" && port.productId === "0011";
});
});

if (device) {
return device.path;
} else {
return null;
}
} catch (e) {
console.error("Failed to search for ports: ", e);
return null;
}
}

async function checkProvesConnected(): Promise<
| {
status: false;
path: null;
}
| { status: true; path: string }
> {
const result = await findDevicePort();
if (result) {
return { status: true, path: result };
} else {
return { status: false, path: null };
}
}

async function loopUntilUsbConnected() {
while (true) {
console.log("Attempting to connect to PROVESKit...");
try {
const result = await checkProvesConnected();
if (result.status) {
console.log("USB connected");
manager.connect();
break;
}

await new Promise((res) => setTimeout(res, 5000));
} catch (e) {
console.error("Failed to check for USB devices: ", e);
await new Promise((res) => setTimeout(res, 5000));
}
}
}