-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from DUDEbehindDUDE/main
Testing websockets (pushing from fork to other branch)
- Loading branch information
Showing
21 changed files
with
449 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"endOfLine": "lf" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
} |
Oops, something went wrong.