From 0ff43d975df077da770cdd147b73f2b0aa33f382 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Wed, 10 Dec 2025 10:30:32 +0000 Subject: [PATCH 1/3] - added digital zoom functionality on fixed location via web sockets --- src/app/config/wsconfig.ts | 7 +++- src/app/context/WebSocketContext.ts | 13 ++++++ src/app/providers/CameraFeedProvider.tsx | 1 - src/app/providers/WebSocketProvider.tsx | 34 ++++++++++++++- .../CameraSettings/RegionSelector.tsx | 42 ++++++++++++++++++- .../components/Video/VideoFeedGridPainter.tsx | 1 + src/utils/config.ts | 2 + 7 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/app/config/wsconfig.ts b/src/app/config/wsconfig.ts index d6fa33d..958ea01 100644 --- a/src/app/config/wsconfig.ts +++ b/src/app/config/wsconfig.ts @@ -1,5 +1,10 @@ +import { CAMBASE_WS } from "../../utils/config"; + export const wsConfig = { - infoBar: "ws://100.115.125.56/websocket-infobar", + infoBar: `${CAMBASE_WS}/websocket-infobar`, + cameraFeedA: `${CAMBASE_WS}/websocket-CameraA-live-video`, + cameraFeedB: `${CAMBASE_WS}/websocket-CameraB-live-video`, + cameraFeedC: `${CAMBASE_WS}/websocket-CameraC-live-video`, }; export type SocketKey = keyof typeof wsConfig; diff --git a/src/app/context/WebSocketContext.ts b/src/app/context/WebSocketContext.ts index 65799c0..dd89d90 100644 --- a/src/app/context/WebSocketContext.ts +++ b/src/app/context/WebSocketContext.ts @@ -6,10 +6,20 @@ type InfoSocketState = { data: InfoBarData | null; readyState: ReadyState; sendJson: (msg: unknown) => void; + send?: (msg: string) => void; +}; + +type CameraSocketState = { + data: null; + readyState: ReadyState; + send: (msg: string) => void; }; export type WebSocketConextValue = { info: InfoSocketState; + cameraFeedA: CameraSocketState; + cameraFeedB: CameraSocketState; + cameraFeedC: CameraSocketState; }; export const WebsocketContext = createContext(null); @@ -21,3 +31,6 @@ const useWebSocketContext = () => { }; export const useInfoSocket = () => useWebSocketContext().info; +export const useCameraFeedASocket = () => useWebSocketContext().cameraFeedA; +export const useCameraFeedBSocket = () => useWebSocketContext().cameraFeedB; +export const useCameraFeedCSocket = () => useWebSocketContext().cameraFeedC; diff --git a/src/app/providers/CameraFeedProvider.tsx b/src/app/providers/CameraFeedProvider.tsx index 8e818e2..5c0b39e 100644 --- a/src/app/providers/CameraFeedProvider.tsx +++ b/src/app/providers/CameraFeedProvider.tsx @@ -42,7 +42,6 @@ export const CameraFeedProvider = ({ children }: { children: ReactNode }) => { cameraZoomQueryC.refetch(), ]); - console.log(resultA?.data); const zoomLevelAnumber = parseFloat(resultA.data?.propPhysCurrent?.value); const zoomLevelBnumber = parseFloat(resultB.data?.propPhysCurrent?.value); const zoomLevelCnumber = parseFloat(resultC.data?.propPhysCurrent?.value); diff --git a/src/app/providers/WebSocketProvider.tsx b/src/app/providers/WebSocketProvider.tsx index 42ff194..d2346a2 100644 --- a/src/app/providers/WebSocketProvider.tsx +++ b/src/app/providers/WebSocketProvider.tsx @@ -10,7 +10,11 @@ type WebSocketProviderProps = { export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { const [systemData, setSystemData] = useState(null); + const infoSocket = useWebSocket(wsConfig.infoBar, { share: true, shouldReconnect: () => true }); + const cameraFeedASocket = useWebSocket(wsConfig.cameraFeedA, { share: true, shouldReconnect: () => true }); + const cameraFeedBSocket = useWebSocket(wsConfig.cameraFeedB, { share: true, shouldReconnect: () => true }); + const cameraFeedCSocket = useWebSocket(wsConfig.cameraFeedC, { share: true, shouldReconnect: () => true }); useEffect(() => { async function parseData() { @@ -30,8 +34,36 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { readyState: infoSocket.readyState, sendJson: infoSocket.sendJsonMessage, }, + cameraFeedA: { + data: null, + readyState: cameraFeedASocket.readyState, + + send: cameraFeedASocket.sendMessage, + }, + cameraFeedB: { + data: null, + readyState: cameraFeedBSocket.readyState, + + send: cameraFeedBSocket.sendMessage, + }, + cameraFeedC: { + data: null, + readyState: cameraFeedCSocket.readyState, + + send: cameraFeedCSocket.sendMessage, + }, }), - [infoSocket.readyState, infoSocket.sendJsonMessage, systemData], + [ + cameraFeedASocket.readyState, + cameraFeedASocket.sendMessage, + cameraFeedBSocket.readyState, + cameraFeedBSocket.sendMessage, + cameraFeedCSocket.readyState, + cameraFeedCSocket.sendMessage, + infoSocket.readyState, + infoSocket.sendJsonMessage, + systemData, + ], ); return {children}; diff --git a/src/features/cameras/components/CameraSettings/RegionSelector.tsx b/src/features/cameras/components/CameraSettings/RegionSelector.tsx index f23722d..4b78604 100644 --- a/src/features/cameras/components/CameraSettings/RegionSelector.tsx +++ b/src/features/cameras/components/CameraSettings/RegionSelector.tsx @@ -4,6 +4,12 @@ import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext" import { useColourDectection } from "../../hooks/useColourDetection"; import { useBlackBoard } from "../../../../hooks/useBlackBoard"; import { toast } from "sonner"; +import { ReadyState } from "react-use-websocket"; +import { + useCameraFeedASocket, + useCameraFeedBSocket, + useCameraFeedCSocket, +} from "../../../../app/context/WebSocketContext"; type RegionSelectorProps = { regions: Region[]; @@ -29,6 +35,22 @@ const RegionSelector = ({ const { blackboardMutation } = useBlackBoard(); const paintedCells = state.paintedCells[cameraFeedID]; + // Get the socket for the current camera only + const cameraASocket = useCameraFeedASocket(); + const cameraBSocket = useCameraFeedBSocket(); + const cameraCSocket = useCameraFeedCSocket(); + + const getCurrentSocket = () => { + switch (cameraFeedID) { + case "A": + return cameraASocket; + case "B": + return cameraBSocket; + case "C": + return cameraCSocket; + } + }; + const handleChange = (e: { target: { value: string } }) => { dispatch({ type: "CHANGE_MODE", payload: { cameraFeedID: cameraFeedID, mode: e.target.value } }); }; @@ -77,6 +99,23 @@ const RegionSelector = ({ setIsResetModalOpen(true); }; + const textClick = (cameraFeedID: "A" | "B" | "C") => { + const socket = getCurrentSocket(); + + // Check if WebSocket is connected + if (socket.readyState !== ReadyState.OPEN) { + toast.error(`Camera ${cameraFeedID} WebSocket is not connected`); + return; + } + + try { + socket.send("ZOOM=0.3,0.3"); + toast.success(`Zoom command sent to Camera ${cameraFeedID}`); + } catch (error) { + console.error("WebSocket send error:", error); + toast.error(`Failed to send command to Camera ${cameraFeedID}`); + } + }; const handleSaveclick = () => { const regions: ColourData[] = []; const test = Array.from(paintedCells.entries()); @@ -189,12 +228,13 @@ const RegionSelector = ({ className="sr-only" />
- Enlarge image + Magnifier {mode === "zoom" && ( Use mouse to digitally zoom in and out )}
+ diff --git a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx index 31d812e..8d0b264 100644 --- a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx +++ b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx @@ -64,6 +64,7 @@ const VideoFeedGridPainter = () => { map.delete(key); paintLayerRef.current?.batchDraw(); } + return; } diff --git a/src/utils/config.ts b/src/utils/config.ts index a8ef457..98dd355 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1 +1,3 @@ export const CAMBASE = import.meta.env.VITE_BASEURL; + +export const CAMBASE_WS = import.meta.env.VITE_BASE_WS; From 10e2644666e5fccc25bce196ac93ca5fb652c2f9 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Wed, 10 Dec 2025 13:09:07 +0000 Subject: [PATCH 2/3] - improved zoom while clicking on on image to zoom --- src/app/context/WebSocketContext.ts | 4 +- src/app/providers/WebSocketProvider.tsx | 24 +++++-- .../CameraSettings/RegionSelector.tsx | 57 ++++++++------- .../components/Video/VideoFeedGridPainter.tsx | 72 +++++++++++++++++-- src/features/cameras/hooks/useGetVideoFeed.ts | 4 +- .../cameras/hooks/useGetvideoSnapshots.ts | 2 +- src/types/types.ts | 4 ++ 7 files changed, 126 insertions(+), 41 deletions(-) diff --git a/src/app/context/WebSocketContext.ts b/src/app/context/WebSocketContext.ts index dd89d90..551e006 100644 --- a/src/app/context/WebSocketContext.ts +++ b/src/app/context/WebSocketContext.ts @@ -1,6 +1,6 @@ import { createContext, useContext } from "react"; import { ReadyState } from "react-use-websocket"; -import type { InfoBarData } from "../../types/types"; +import type { CameraZoomData, InfoBarData } from "../../types/types"; type InfoSocketState = { data: InfoBarData | null; @@ -10,7 +10,7 @@ type InfoSocketState = { }; type CameraSocketState = { - data: null; + data: CameraZoomData | null; readyState: ReadyState; send: (msg: string) => void; }; diff --git a/src/app/providers/WebSocketProvider.tsx b/src/app/providers/WebSocketProvider.tsx index d2346a2..66ce180 100644 --- a/src/app/providers/WebSocketProvider.tsx +++ b/src/app/providers/WebSocketProvider.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState, type ReactNode } from "react"; import { WebsocketContext, type WebSocketConextValue } from "../context/WebSocketContext"; import useWebSocket from "react-use-websocket"; import { wsConfig } from "../config/wsconfig"; -import type { InfoBarData } from "../../types/types"; +import type { CameraZoomData, InfoBarData } from "../../types/types"; type WebSocketProviderProps = { children: ReactNode; @@ -10,7 +10,7 @@ type WebSocketProviderProps = { export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { const [systemData, setSystemData] = useState(null); - + const [socketData, setSocketData] = useState(null); const infoSocket = useWebSocket(wsConfig.infoBar, { share: true, shouldReconnect: () => true }); const cameraFeedASocket = useWebSocket(wsConfig.cameraFeedA, { share: true, shouldReconnect: () => true }); const cameraFeedBSocket = useWebSocket(wsConfig.cameraFeedB, { share: true, shouldReconnect: () => true }); @@ -23,9 +23,20 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { const data = JSON.parse(text); setSystemData(data); } + if (cameraFeedASocket.lastMessage || cameraFeedBSocket.lastMessage || cameraFeedCSocket.lastMessage) { + const message = cameraFeedASocket.lastMessage || cameraFeedBSocket.lastMessage || cameraFeedCSocket.lastMessage; + const data = await message?.data.text(); + const parsedData: CameraZoomData = JSON.parse(data || ""); + setSocketData(parsedData); + } } parseData(); - }, [infoSocket.lastMessage]); + }, [ + cameraFeedASocket.lastMessage, + cameraFeedBSocket.lastMessage, + cameraFeedCSocket.lastMessage, + infoSocket.lastMessage, + ]); const value = useMemo( () => ({ @@ -35,19 +46,19 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { sendJson: infoSocket.sendJsonMessage, }, cameraFeedA: { - data: null, + data: socketData, readyState: cameraFeedASocket.readyState, send: cameraFeedASocket.sendMessage, }, cameraFeedB: { - data: null, + data: socketData, readyState: cameraFeedBSocket.readyState, send: cameraFeedBSocket.sendMessage, }, cameraFeedC: { - data: null, + data: socketData, readyState: cameraFeedCSocket.readyState, send: cameraFeedCSocket.sendMessage, @@ -62,6 +73,7 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { cameraFeedCSocket.sendMessage, infoSocket.readyState, infoSocket.sendJsonMessage, + socketData, systemData, ], ); diff --git a/src/features/cameras/components/CameraSettings/RegionSelector.tsx b/src/features/cameras/components/CameraSettings/RegionSelector.tsx index 4b78604..a989bd9 100644 --- a/src/features/cameras/components/CameraSettings/RegionSelector.tsx +++ b/src/features/cameras/components/CameraSettings/RegionSelector.tsx @@ -4,7 +4,6 @@ import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext" import { useColourDectection } from "../../hooks/useColourDetection"; import { useBlackBoard } from "../../../../hooks/useBlackBoard"; import { toast } from "sonner"; -import { ReadyState } from "react-use-websocket"; import { useCameraFeedASocket, useCameraFeedBSocket, @@ -34,8 +33,6 @@ const RegionSelector = ({ const { state, dispatch } = useCameraFeedContext(); const { blackboardMutation } = useBlackBoard(); const paintedCells = state.paintedCells[cameraFeedID]; - - // Get the socket for the current camera only const cameraASocket = useCameraFeedASocket(); const cameraBSocket = useCameraFeedBSocket(); const cameraCSocket = useCameraFeedCSocket(); @@ -51,6 +48,16 @@ const RegionSelector = ({ } }; + const socket = getCurrentSocket(); + + const getMagnificationLevel = () => { + const test = socket.data; + if (!socket.data) return null; + + if (!test || !test.magnificationLevel) return "0x"; + return test?.magnificationLevel; + }; + const handleChange = (e: { target: { value: string } }) => { dispatch({ type: "CHANGE_MODE", payload: { cameraFeedID: cameraFeedID, mode: e.target.value } }); }; @@ -99,23 +106,6 @@ const RegionSelector = ({ setIsResetModalOpen(true); }; - const textClick = (cameraFeedID: "A" | "B" | "C") => { - const socket = getCurrentSocket(); - - // Check if WebSocket is connected - if (socket.readyState !== ReadyState.OPEN) { - toast.error(`Camera ${cameraFeedID} WebSocket is not connected`); - return; - } - - try { - socket.send("ZOOM=0.3,0.3"); - toast.success(`Zoom command sent to Camera ${cameraFeedID}`); - } catch (error) { - console.error("WebSocket send error:", error); - toast.error(`Failed to send command to Camera ${cameraFeedID}`); - } - }; const handleSaveclick = () => { const regions: ColourData[] = []; const test = Array.from(paintedCells.entries()); @@ -213,6 +203,25 @@ const RegionSelector = ({ /> Erase mode + - diff --git a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx index 8d0b264..02343b9 100644 --- a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx +++ b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx @@ -2,8 +2,14 @@ import { useEffect, useRef, useState, type RefObject } from "react"; import { Stage, Layer, Image, Shape } from "react-konva"; import type { KonvaEventObject } from "konva/lib/Node"; import { useCreateVideoSnapshot } from "../../hooks/useGetvideoSnapshots"; - import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext"; +import { + useCameraFeedASocket, + useCameraFeedBSocket, + useCameraFeedCSocket, +} from "../../../../app/context/WebSocketContext"; +import { ReadyState } from "react-use-websocket"; +import { toast } from "sonner"; const BACKEND_WIDTH = 640; const BACKEND_HEIGHT = 360; @@ -25,6 +31,7 @@ const VideoFeedGridPainter = () => { const isDrawingRef = useRef(false); const [scale, setScale] = useState(1); const [position, setPosition] = useState({ x: 0, y: 0 }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const stageRef = useRef(null); @@ -34,6 +41,55 @@ const VideoFeedGridPainter = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const paintLayerRef = useRef(null); + const cameraASocket = useCameraFeedASocket(); + const cameraBSocket = useCameraFeedBSocket(); + const cameraCSocket = useCameraFeedCSocket(); + + const getCurrentSocket = () => { + switch (cameraFeedID) { + case "A": + return cameraASocket; + case "B": + return cameraBSocket; + case "C": + return cameraCSocket; + } + }; + + const socket = getCurrentSocket(); + + const getMagnificationLevel = () => { + const test = socket.data; + if (!socket.data) return null; + + if (!test || !test.magnificationLevel) return "0x"; + return test?.magnificationLevel; + }; + + const handleZoomClick = (e: KonvaEventObject, cameraFeedID: "A" | "B" | "C") => { + if (mode !== "zoom") return; + const socket = getCurrentSocket(); + const stage = e.target.getStage(); + const coords = stage?.getPointerPosition(); + if (!coords || !socket) return; + + const newX = coords.x / stageSize.width; + const newY = coords.y / stageSize.height; + + // Check if WebSocket is connected + if (socket.readyState !== ReadyState.OPEN) { + toast.error(`Camera ${cameraFeedID} WebSocket is not connected`); + return; + } + + try { + socket.send(`ZOOM=${newX.toFixed(2)},${newY.toFixed(2)}`); + } catch (error) { + console.error("WebSocket send error:", error); + toast.error(`Failed to send command to Camera ${cameraFeedID}`); + } + }; + const draw = (bmp: RefObject): ImageBitmap | null => { if (!bmp || !bmp.current) { return null; @@ -76,14 +132,14 @@ const VideoFeedGridPainter = () => { }; const handleStageMouseDown = (e: KonvaEventObject) => { - if (!regions[selectedRegionIndex] || mode === "zoom") return; + if (!regions[selectedRegionIndex] || mode === "magnify" || mode === "zoom") return; isDrawingRef.current = true; const pos = e.target.getStage()?.getPointerPosition(); if (pos) paintCell(pos.x, pos.y); }; const handleStageMouseMove = (e: KonvaEventObject) => { - if (!isDrawingRef.current || mode === "zoom") return; + if (!isDrawingRef.current || mode === "magnify") return; if (!regions[selectedRegionIndex]) return; const pos = e.target.getStage()?.getPointerPosition(); if (pos) paintCell(pos.x, pos.y); @@ -94,7 +150,7 @@ const VideoFeedGridPainter = () => { }; const handleMouseEnter = () => { - if (mode !== "zoom") return; + if (mode !== "magnify") return; setScale(2); }; const handleMouseLeave = () => { @@ -171,7 +227,13 @@ const VideoFeedGridPainter = () => { x={position.x} y={position.y} > - + handleZoomClick(e, cameraFeedID)} + /> diff --git a/src/features/cameras/hooks/useGetVideoFeed.ts b/src/features/cameras/hooks/useGetVideoFeed.ts index 44b2796..cbee4ac 100644 --- a/src/features/cameras/hooks/useGetVideoFeed.ts +++ b/src/features/cameras/hooks/useGetVideoFeed.ts @@ -28,14 +28,14 @@ export const useGetVideoFeed = (cameraFeedID: "A" | "B" | "C" | null, mode: stri queryKey: ["getfeed", cameraFeedID], queryFn: () => targetDectionFeed(cameraFeedID), refetchInterval: 500, - enabled: mode !== "zoom", + enabled: mode !== "magnify" && mode !== "zoom", }); const videoFeedQuery = useQuery({ queryKey: ["videoQuery", cameraFeedID, mode], queryFn: () => getVideoFeed(cameraFeedID), refetchInterval: 500, - enabled: mode === "zoom", + enabled: mode === "magnify" || mode === "zoom", }); return { targetDetectionQuery, videoFeedQuery }; diff --git a/src/features/cameras/hooks/useGetvideoSnapshots.ts b/src/features/cameras/hooks/useGetvideoSnapshots.ts index da70292..f503584 100644 --- a/src/features/cameras/hooks/useGetvideoSnapshots.ts +++ b/src/features/cameras/hooks/useGetvideoSnapshots.ts @@ -15,7 +15,7 @@ export const useCreateVideoSnapshot = () => { const videoSnapShot = videoFeedQuery?.data; const isVideoLoading = videoFeedQuery.isPending; - if (isVideoLoading === false && videoSnapShot && mode === "zoom") { + if ((isVideoLoading === false && videoSnapShot && mode === "magnify") || mode === "zoom") { snapShot = videoSnapShot; } diff --git a/src/types/types.ts b/src/types/types.ts index a6c1138..0ecad3d 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -11,6 +11,10 @@ export type InfoBarData = { "thread-count": string; }; +export type CameraZoomData = { + magnificationLevel: string; +}; + export type StatusIndicator = "neutral-quaternary" | "dark" | "info" | "success" | "warning" | "danger"; export type Region = { name: string; From eb45eabde9459c19ee4c7b25a53f9119e37d332a Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Wed, 10 Dec 2025 14:08:44 +0000 Subject: [PATCH 3/3] - improved magnification level text - removed magnification level --- .../components/CameraSettings/RegionSelector.tsx | 2 +- .../cameras/components/Video/VideoFeedGridPainter.tsx | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/features/cameras/components/CameraSettings/RegionSelector.tsx b/src/features/cameras/components/CameraSettings/RegionSelector.tsx index a989bd9..c1e2699 100644 --- a/src/features/cameras/components/CameraSettings/RegionSelector.tsx +++ b/src/features/cameras/components/CameraSettings/RegionSelector.tsx @@ -238,7 +238,7 @@ const RegionSelector = ({ />
Digital Zoom mode -
{getMagnificationLevel()}
+
{`current Zoom: ${getMagnificationLevel()}`}
{mode === "zoom" && Click image to digitally zoom}
diff --git a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx index 02343b9..e6ea83f 100644 --- a/src/features/cameras/components/Video/VideoFeedGridPainter.tsx +++ b/src/features/cameras/components/Video/VideoFeedGridPainter.tsx @@ -56,16 +56,6 @@ const VideoFeedGridPainter = () => { } }; - const socket = getCurrentSocket(); - - const getMagnificationLevel = () => { - const test = socket.data; - if (!socket.data) return null; - - if (!test || !test.magnificationLevel) return "0x"; - return test?.magnificationLevel; - }; - const handleZoomClick = (e: KonvaEventObject, cameraFeedID: "A" | "B" | "C") => { if (mode !== "zoom") return; const socket = getCurrentSocket();