Skip to content

Commit

Permalink
Merge pull request #2 from DUDEbehindDUDE/main
Browse files Browse the repository at this point in the history
Testing websockets (pushing from fork to other branch)
  • Loading branch information
DUDEbehindDUDE authored Nov 21, 2024
2 parents 5d4fa78 + 68d54c1 commit 4f82c90
Show file tree
Hide file tree
Showing 21 changed files with 449 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ jobs:
cache: "yarn"
cache-dependency-path: react/yarn.lock
- run: yarn
- run: npx prettier . --check
- run: yarn build --if-present
- run: yarn test
- run: yarn prettier . --check
4 changes: 4 additions & 0 deletions react/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Ignore yarn.lock and other Yarn-specific files
yarn.lock
package.json
**/node_modules/*
3 changes: 3 additions & 0 deletions react/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"endOfLine": "lf"
}
4 changes: 3 additions & 1 deletion react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@
"@types/node": "^16.7.13",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"date-fns": "^4.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
"typescript": "^4.4.2",
"web-vitals": "^2.1.0"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11"
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"prettier": "^3.3.3"
},
"scripts": {
"start": "react-scripts start",
Expand Down
38 changes: 0 additions & 38 deletions react/src/App.css

This file was deleted.

26 changes: 0 additions & 26 deletions react/src/App.tsx

This file was deleted.

44 changes: 44 additions & 0 deletions react/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
import "css/App.css";
import Button from "components/Button";
import { Status } from "types/status";
import Console from "components/Console";
import { WebSocketProvider } from "components/WebSocketContext";

function App() {
const [botStatus, setBotStatus] = useState(Status.Offline);

function startBot() {
if (botStatus === Status.Ok) return;
if (botStatus === Status.Offline) {
setBotStatus(Status.Loading);
// set to Status.Ok in 5s
}
}
function stopBot() {
if (botStatus !== Status.Ok) return;
setBotStatus(Status.Offline);
}

useEffect(() => {
if (botStatus === Status.Loading) {
const timeoutId = setTimeout(() => {
setBotStatus(Status.Ok);
}, 5000);

return () => clearTimeout(timeoutId); // Cleanup function
}
}, [botStatus]);

return (
<div className="App">
<WebSocketProvider>
<Button name="Start" status={botStatus} onClick={startBot} />
<Button name="Bad Button" onClick={stopBot} />
<Console />
</WebSocketProvider>
</div>
);
}

export default App;
14 changes: 14 additions & 0 deletions react/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ButtonProps } from "types/button";
import "css/Button.css";
import StatusIndicator from "./StatusIndicator";

function Button(props: ButtonProps) {
return (
<button onClick={props.onClick} className="Button">
{<StatusIndicator status={props.status} />}
{props.name}
</button>
);
}

export default Button;
93 changes: 93 additions & 0 deletions react/src/components/Console.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useEffect, useRef, useState } from "react";
import { format } from "date-fns";
import { useWebSocket } from "./WebSocketContext";
import "css/Console.css";

function Console() {
const consoleOutputRef = useRef<HTMLDivElement>(null);
const [socketHistory, setSocketHistory] = useState<string[]>([]);
const [autoScroll, setAutoScroll] = useState(true);
const maxHistoryLength = 1000; // in lines
const { data } = useWebSocket({
// All the topics we want to subscribe to. This should be pretty
// much every topic that exists on ros. Currently topic and topic2
// for testing but it won't error if the topics don't exist.
includeTopics: ["/topic", "/topic2"],
});

// update history when receiving new data
useEffect(() => {
if (data === null) return;

setSocketHistory((prev: string[]) => {
const time = format(new Date(), "HH:mm:ss.SSS");
const topic = data.topic;
const body = data.msg.data;
const nextMsg = `[${time}] [${topic}]: ${body}`;
const newHistory = [...prev, nextMsg];
if (newHistory.length > maxHistoryLength) {
newHistory.splice(0, 1);
}
return newHistory;
});
}, [data]);

// Console scroll stuff
useEffect(() => {
if (!consoleOutputRef.current) return;

// Keeps the console scrolled at the bottom
if (autoScroll) {
const { current } = consoleOutputRef;
current.scrollTop = current.scrollHeight;
}

// Scrolls up when console history is full and more lines get added to prevent it from shifting down
if (!autoScroll && socketHistory.length === maxHistoryLength) {
const { current } = consoleOutputRef;
current.scrollTop -= 14; // 14px represents roughly 1 line
}
}, [socketHistory, autoScroll]);

function checkEnableAutoScroll() {
if (consoleOutputRef.current) {
const { current } = consoleOutputRef;
// this confused me, it's total height - amount scrolled - height of what's visible
let distFromBottom = current.scrollHeight - current.scrollTop;
distFromBottom -= current.clientHeight;

// 10px to add a little bit of margin
if (distFromBottom < 10) {
// Re-enable auto scroll when the user scrolls to the bottom
setAutoScroll(true);
} else {
// Disable auto scroll when the user scrolls manually
setAutoScroll(false);
}
}
}

function renderConsoleText() {
let text = socketHistory.join("\n");
if (text === "") text = "Waiting for messages...";
if (socketHistory.length === maxHistoryLength) {
text = `...history limited to ${maxHistoryLength} lines\n${text}`;
}
return text;
}

return (
<div className="Console">
<h1 className="ConsoleTitle">Console</h1>
<div
className="ConsoleBody"
ref={consoleOutputRef}
onScroll={checkEnableAutoScroll}
>
<pre>{renderConsoleText()}</pre>
</div>
</div>
);
}

export default Console;
7 changes: 7 additions & 0 deletions react/src/components/StatusIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Status } from "types/button";

function StatusIndicator(props: { status?: Status }) {
return <>{props.status && <span className={props.status} />}</>;
}

export default StatusIndicator;
106 changes: 106 additions & 0 deletions react/src/components/WebSocketContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import {
RosDataType,
SocketResponse,
WebSocketContextType,
} from "types/websocket";

const WebSocketContext = createContext<WebSocketContextType | undefined>(
undefined,
);

export function WebSocketProvider({ children }: { children: React.ReactNode }) {
const [socket, setSocket] = useState<WebSocket | null>(null);
const listeners = useRef<Set<(data: SocketResponse) => void>>(new Set());

useEffect(() => {
// if it already exists it already has all this stuff set
if (socket !== null) return;

const toSubscribe = [
{ topic: "/topic", type: RosDataType.String },
{ topic: "/topic2", type: RosDataType.String },
];

const _socket = new WebSocket("ws://127.0.0.1:9090");
_socket.onopen = () => {
console.log("Connected to ROS bridge");
toSubscribe.forEach((item) => {
_socket.send(JSON.stringify({ op: "subscribe", ...item }));
});
};

_socket.onmessage = (event) => {
const data: SocketResponse = JSON.parse(event.data);
listeners.current.forEach((listener) => listener(data));
};

_socket.onerror = (error) => {
console.error("WebSocket error", error);
};

_socket.onclose = () => {
console.log("Socket closed");
setSocket(null);
};

setSocket(_socket);
}, [socket]);

function sendMessage(msg: object) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(msg));
}
}

const addMessageListener = useCallback((callback: (data: any) => void) => {
listeners.current.add(callback);
return () => {
listeners.current.delete(callback);
};
}, []);

return (
<WebSocketContext.Provider value={{ sendMessage, addMessageListener }}>
{children}
</WebSocketContext.Provider>
);
}

// Custom hook to use WebSocket data with filtering options
export function useWebSocket(filterOptions: {
includeTopics?: string[];
excludeTopics?: string[];
}) {
const context = useContext(WebSocketContext);
if (!context) {
throw new Error("useWebSocket must be used within a WebSocketProvider");
}

const { addMessageListener, sendMessage } = context;
const [data, setData] = useState<any | null>(null);

useEffect(() => {
function handleData(data: SocketResponse) {
const { includeTopics, excludeTopics } = filterOptions;
const topic = data.topic;

if (includeTopics && !includeTopics.includes(topic)) return;
if (excludeTopics && excludeTopics.includes(topic)) return;

setData(data);
}

const removeListener = addMessageListener(handleData);
return removeListener;
}, [addMessageListener, filterOptions]);

return { data, sendMessage };
}
Loading

0 comments on commit 4f82c90

Please sign in to comment.