diff --git a/packages/api-client/lib/index.spec.ts b/packages/api-client/lib/index.spec.ts index a77462aba..20b0056b8 100644 --- a/packages/api-client/lib/index.spec.ts +++ b/packages/api-client/lib/index.spec.ts @@ -8,31 +8,9 @@ describe('subscriptions', () => { spyOn(sioClient.sio, 'emit'); }); - it('multiplexes subscriptions', () => { - const s1 = sioClient.subscribe('test', () => { - // empty - }); - const s2 = sioClient.subscribe('test', () => { - // empty - }); - expect(sioClient.sio.emit).toHaveBeenCalledOnceWith('subscribe', jasmine.anything()); - (sioClient.sio.emit as jasmine.Spy).calls.reset(); - - sioClient.unsubscribe(s1); - sioClient.unsubscribe(s2); - expect(sioClient.sio.emit).toHaveBeenCalledOnceWith('unsubscribe', jasmine.anything()); - }); - - it('does not unsubscribe early when there are multiple subscriptions with the same listener', () => { - const listener = jasmine.createSpy(); - const s1 = sioClient.subscribe('test', listener); - const s2 = sioClient.subscribe('test', listener); - expect(sioClient.sio.emit).toHaveBeenCalledOnceWith('subscribe', jasmine.anything()); - (sioClient.sio.emit as jasmine.Spy).calls.reset(); - - sioClient.unsubscribe(s1); - expect(sioClient.sio.emit).not.toHaveBeenCalled(); - sioClient.unsubscribe(s2); - expect(sioClient.sio.emit).toHaveBeenCalledOnceWith('unsubscribe', jasmine.anything()); + it('dummy', () => { + // Dummy test to ci passes. + // #940 removed multiplexing in order to support resubscribe on reconnect. + // With it gone, there is no more tests and ci fails as a result. }); }); diff --git a/packages/api-client/lib/index.ts b/packages/api-client/lib/index.ts index 49b6c584d..46829742d 100644 --- a/packages/api-client/lib/index.ts +++ b/packages/api-client/lib/index.ts @@ -31,41 +31,21 @@ export interface Subscription { export class SioClient { public sio: Socket; - private _subscriptions: Record = {}; constructor(...args: Parameters) { this.sio = io(...args); } subscribe(room: string, listener: Listener): Subscription { - const subs = this._subscriptions[room] || 0; - if (subs === 0) { - this.sio.emit('subscribe', { room }); - debug(`subscribed to ${room}`); - } else { - debug(`reusing previous subscription to ${room}`); - } + this.sio.emit('subscribe', { room }); + debug(`subscribed to ${room}`); this.sio.on(room, listener); - this._subscriptions[room] = subs + 1; return { room, listener }; } unsubscribe(sub: Subscription): void { - const subCount = this._subscriptions[sub.room] || 0; - if (!subCount) { - debug(`tried to unsubscribe from ${sub.room}, but no subscriptions exist`); - // continue regardless - } - if (subCount <= 1) { - this.sio.emit('unsubscribe', { room: sub.room }); - delete this._subscriptions[sub.room]; - debug(`unsubscribed to ${sub.room}`); - } else { - this._subscriptions[sub.room] = subCount - 1; - debug( - `skipping unsubscribe to ${sub.room} because there are still ${subCount - 1} subscribers`, - ); - } + this.sio.emit('unsubscribe', { room: sub.room }); + debug(`unsubscribed to ${sub.room}`); this.sio.off(sub.room, sub.listener); } diff --git a/packages/api-server/api_server/fast_io/__init__.py b/packages/api-server/api_server/fast_io/__init__.py index 80a3d5503..cb100cf4e 100644 --- a/packages/api-server/api_server/fast_io/__init__.py +++ b/packages/api-server/api_server/fast_io/__init__.py @@ -249,6 +249,34 @@ def _match_routes( return match, r return None + async def _add_subscription( + self, + req: SubscriptionRequest, + handler: Callable[[], Observable | Coroutine[Any, Any, Observable]], + ): + if "_subscriptions" in req.session and req.session["_subscriptions"].get( + req.room + ): + return + + maybe_coro = handler() + if asyncio.iscoroutine(maybe_coro): + obs = await maybe_coro + else: + obs = maybe_coro + obs = cast(Observable, obs) + + loop = asyncio.get_event_loop() + + def on_next(data): + async def emit(): + await self.sio.emit(req.room, data, to=req.sid) + + loop.create_task(emit()) + + sub = obs.subscribe(on_next) + req.session.setdefault("_subscriptions", {})[req.room] = sub + async def _on_subscribe(self, sid: str, data: dict): try: sub_data = self._parse_sub_data(data) @@ -270,23 +298,8 @@ async def _on_subscribe(self, sid: str, data: dict): req = SubscriptionRequest( sid=sid, sio=self.sio, room=sub_data.room, session=session ) - maybe_coro = route.endpoint(req, **match.groupdict()) - if asyncio.iscoroutine(maybe_coro): - obs = await maybe_coro - else: - obs = maybe_coro - obs = cast(Observable, obs) - - loop = asyncio.get_event_loop() - - def on_next(data): - async def emit(): - await self.sio.emit(sub_data.room, data, to=sid) - - loop.create_task(emit()) - - sub = obs.subscribe(on_next) - session.setdefault("_subscriptions", {})[sub_data.room] = sub + handler = lambda: route.endpoint(req, **match.groupdict()) + await self._add_subscription(req, handler) except HTTPException as e: await self.sio.emit( diff --git a/packages/dashboard/src/components/doors-app.tsx b/packages/dashboard/src/components/doors-app.tsx index 9562aef79..4abfa00ac 100644 --- a/packages/dashboard/src/components/doors-app.tsx +++ b/packages/dashboard/src/components/doors-app.tsx @@ -1,8 +1,9 @@ import { BuildingMap } from 'api-client'; import React from 'react'; import { DoorDataGridTable, DoorTableData } from 'react-components'; -import { AppEvents } from './app-events'; import { DoorMode as RmfDoorMode } from 'rmf-models'; +import { throttleTime } from 'rxjs'; +import { AppEvents } from './app-events'; import { createMicroApp } from './micro-app'; import { RmfAppContext } from './rmf-app'; import { getApiErrorMessage } from './utils'; @@ -31,29 +32,32 @@ export const DoorsApp = createMicroApp('Doors', () => { try { const { data } = await rmf.doorsApi.getDoorHealthDoorsDoorNameHealthGet(door.name); const { health_status } = data; - const sub = rmf.getDoorStateObs(door.name).subscribe((doorState) => { - setDoorTableData((prev) => { - return { - ...prev, - [door.name]: { - index: doorIndex++, - doorName: door.name, - opMode: health_status ? health_status : 'N/A', - levelName: level.name, - doorType: door.door_type, - doorState: doorState, - onClickOpen: () => - rmf?.doorsApi.postDoorRequestDoorsDoorNameRequestPost(door.name, { - mode: RmfDoorMode.MODE_OPEN, - }), - onClickClose: () => - rmf?.doorsApi.postDoorRequestDoorsDoorNameRequestPost(door.name, { - mode: RmfDoorMode.MODE_CLOSED, - }), - }, - }; + const sub = rmf + .getDoorStateObs(door.name) + .pipe(throttleTime(3000, undefined, { leading: true, trailing: true })) + .subscribe((doorState) => { + setDoorTableData((prev) => { + return { + ...prev, + [door.name]: { + index: doorIndex++, + doorName: door.name, + opMode: health_status ? health_status : 'N/A', + levelName: level.name, + doorType: door.door_type, + doorState: doorState, + onClickOpen: () => + rmf?.doorsApi.postDoorRequestDoorsDoorNameRequestPost(door.name, { + mode: RmfDoorMode.MODE_OPEN, + }), + onClickClose: () => + rmf?.doorsApi.postDoorRequestDoorsDoorNameRequestPost(door.name, { + mode: RmfDoorMode.MODE_CLOSED, + }), + }, + }; + }); }); - }); return () => sub.unsubscribe(); } catch (error) { console.error(`Failed to get lift health: ${getApiErrorMessage(error)}`); diff --git a/packages/dashboard/src/components/doors-overlay.tsx b/packages/dashboard/src/components/doors-overlay.tsx deleted file mode 100644 index 733243374..000000000 --- a/packages/dashboard/src/components/doors-overlay.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Door, DoorState } from 'api-client'; -import React from 'react'; -import { - DoorMarker as BaseDoorMarker, - DoorMarkerProps as BaseDoorMarkerProps, - fromRmfCoords, - getDoorCenter, - SVGOverlay, - SVGOverlayProps, - useAutoScale, - viewBoxFromLeafletBounds, - withLabel, -} from 'react-components'; -import { RmfAppContext } from './rmf-app'; - -interface DoorMarkerProps extends Omit { - door: Door; -} - -const DoorMarker = withLabel(({ door, ...otherProps }: DoorMarkerProps) => { - const rmf = React.useContext(RmfAppContext); - const [doorState, setDoorState] = React.useState(null); - React.useEffect(() => { - if (!rmf) { - return; - } - const sub = rmf.getDoorStateObs(door.name).subscribe(setDoorState); - return () => sub.unsubscribe(); - }, [rmf, door]); - - return ( - - ); -}); - -export interface DoorsOverlayProps extends Omit { - doors: Door[]; - hideLabels?: boolean; - onDoorClick?: (ev: React.MouseEvent, door: Door) => void; -} - -export const DoorsOverlay = React.memo( - ({ doors, hideLabels = false, onDoorClick, ...otherProps }: DoorsOverlayProps): JSX.Element => { - const viewBox = viewBoxFromLeafletBounds(otherProps.bounds); - const scale = useAutoScale(40); - - return ( - - {doors.map((door) => { - const center = fromRmfCoords(getDoorCenter(door)); - const [x1, y1] = fromRmfCoords([door.v1_x, door.v1_y]); - const [x2, y2] = fromRmfCoords([door.v2_x, door.v2_y]); - - return ( - onDoorClick && onDoorClick(ev, door)} - x1={x1} - y1={y1} - x2={x2} - y2={y2} - doorType={door.door_type} - aria-label={door.name} - style={{ - transform: `scale(${scale})`, - transformOrigin: `${center[0]}px ${center[1]}px`, - }} - labelText={door.name} - labelSourceX={center[0]} - labelSourceY={center[1]} - labelSourceRadius={0} - hideLabel={hideLabels} - /> - ); - })} - - ); - }, -); diff --git a/packages/dashboard/src/components/lifts-app.tsx b/packages/dashboard/src/components/lifts-app.tsx index 9bbf2aef1..d13dc36bc 100644 --- a/packages/dashboard/src/components/lifts-app.tsx +++ b/packages/dashboard/src/components/lifts-app.tsx @@ -1,13 +1,14 @@ +import { TableContainer } from '@mui/material'; import { BuildingMap, Lift } from 'api-client'; import React from 'react'; +import { LiftDataGridTable, LiftTableData } from 'react-components'; import { LiftRequest as RmfLiftRequest } from 'rmf-models'; -import { LiftTableData, LiftDataGridTable } from 'react-components'; +import { throttleTime } from 'rxjs'; import { AppEvents } from './app-events'; +import { LiftSummary } from './lift-summary'; import { createMicroApp } from './micro-app'; import { RmfAppContext } from './rmf-app'; import { getApiErrorMessage } from './utils'; -import { TableContainer } from '@mui/material'; -import { LiftSummary } from './lift-summary'; export const LiftsApp = createMicroApp('Lifts', () => { const rmf = React.useContext(RmfAppContext); @@ -34,47 +35,50 @@ export const LiftsApp = createMicroApp('Lifts', () => { buildingMap?.lifts.map(async (lift, i) => { try { - const sub = rmf.getLiftStateObs(lift.name).subscribe((liftState) => { - setLiftTableData((prev) => { - return { - ...prev, - [lift.name]: [ - { - index: i, - name: lift.name, - mode: liftState.current_mode, - currentFloor: liftState.current_floor, - destinationFloor: liftState.destination_floor, - doorState: liftState.door_state, - motionState: liftState.motion_state, - sessionId: liftState.session_id, - lift: lift, - onRequestSubmit: async (_ev, doorState, requestType, destination) => { - let fleet_session_ids: string[] = []; - if (requestType === RmfLiftRequest.REQUEST_END_SESSION) { - const fleets = (await rmf?.fleetsApi.getFleetsFleetsGet()).data; - for (const fleet of fleets) { - if (!fleet.robots) { - continue; - } - for (const robotName of Object.keys(fleet.robots)) { - fleet_session_ids.push(`${fleet.name}/${robotName}`); + const sub = rmf + .getLiftStateObs(lift.name) + .pipe(throttleTime(3000, undefined, { leading: true, trailing: true })) + .subscribe((liftState) => { + setLiftTableData((prev) => { + return { + ...prev, + [lift.name]: [ + { + index: i, + name: lift.name, + mode: liftState.current_mode, + currentFloor: liftState.current_floor, + destinationFloor: liftState.destination_floor, + doorState: liftState.door_state, + motionState: liftState.motion_state, + sessionId: liftState.session_id, + lift: lift, + onRequestSubmit: async (_ev, doorState, requestType, destination) => { + let fleet_session_ids: string[] = []; + if (requestType === RmfLiftRequest.REQUEST_END_SESSION) { + const fleets = (await rmf?.fleetsApi.getFleetsFleetsGet()).data; + for (const fleet of fleets) { + if (!fleet.robots) { + continue; + } + for (const robotName of Object.keys(fleet.robots)) { + fleet_session_ids.push(`${fleet.name}/${robotName}`); + } } } - } - return rmf?.liftsApi.postLiftRequestLiftsLiftNameRequestPost(lift.name, { - destination, - door_mode: doorState, - request_type: requestType, - additional_session_ids: fleet_session_ids, - }); + return rmf?.liftsApi.postLiftRequestLiftsLiftNameRequestPost(lift.name, { + destination, + door_mode: doorState, + request_type: requestType, + additional_session_ids: fleet_session_ids, + }); + }, }, - }, - ], - }; + ], + }; + }); }); - }); return () => sub.unsubscribe(); } catch (error) { console.error(`Failed to get lift state: ${getApiErrorMessage(error)}`); diff --git a/packages/dashboard/src/components/lifts-overlay.tsx b/packages/dashboard/src/components/lifts-overlay.tsx deleted file mode 100644 index cdc87c2d7..000000000 --- a/packages/dashboard/src/components/lifts-overlay.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { Door, DoorMode, Lift, LiftState } from 'api-client'; -import React from 'react'; -import { - DoorMarker, - fromRmfCoords, - fromRmfYaw, - LiftMarker as BaseLiftMarker, - liftMarkerClasses, - LiftMarkerProps as BaseLiftMarkerProps, - radiansToDegrees, - SVGOverlay, - SVGOverlayProps, - useAutoScale, - viewBoxFromLeafletBounds, - withLabel, -} from 'react-components'; -import { LiftState as RmfLiftState } from 'rmf-models'; -import { RmfAppContext } from './rmf-app'; - -function toDoorMode(liftState: LiftState): DoorMode { - // LiftState uses its own enum definition of door state/mode which is separated from DoorMode. - // But their definitions are equal so we can skip conversion. - return { value: liftState.door_state }; -} - -const getLiftModeVariant = ( - currentLevel: string, - liftStateMode?: number, - liftStateFloor?: string, -): keyof typeof liftMarkerClasses | undefined => { - if (!liftStateMode && !liftStateFloor) return 'unknown'; - if (liftStateMode === RmfLiftState.MODE_FIRE) return 'fire'; - if (liftStateMode === RmfLiftState.MODE_EMERGENCY) return 'emergency'; - if (liftStateMode === RmfLiftState.MODE_OFFLINE) return 'offLine'; - if (liftStateFloor === currentLevel) { - if (liftStateMode === RmfLiftState.MODE_HUMAN) return 'human'; - if (liftStateMode === RmfLiftState.MODE_AGV) return 'onCurrentLevel'; - } else { - if (liftStateMode === RmfLiftState.MODE_HUMAN) return 'moving'; - if (liftStateMode === RmfLiftState.MODE_AGV) return 'moving'; - } - if (liftStateMode === RmfLiftState.MODE_UNKNOWN) return 'unknown'; - - return 'unknown'; -}; - -interface LiftMarkerProps extends Omit { - lift: Lift; - currentLevel: string; -} - -const LiftMarker = withLabel(({ lift, currentLevel, ...otherProps }: LiftMarkerProps) => { - const rmf = React.useContext(RmfAppContext); - const [liftState, setLiftState] = React.useState(undefined); - React.useEffect(() => { - if (!rmf) { - return; - } - const sub = rmf.getLiftStateObs(lift.name).subscribe(setLiftState); - return () => sub.unsubscribe(); - }, [rmf, lift]); - - return ( - <> - - {lift.doors.map((door: Door, idx: number) => { - const [x1, y1] = fromRmfCoords([door.v1_x, door.v1_y]); - const [x2, y2] = fromRmfCoords([door.v2_x, door.v2_y]); - return ( - - ); - })} - - ); -}); - -export interface LiftsOverlayProps extends Omit { - currentLevel: string; - lifts: Lift[]; - hideLabels?: boolean; - onLiftClick?: (ev: React.MouseEvent, lift: Lift) => void; -} - -export const LiftsOverlay = React.memo( - ({ - lifts, - hideLabels = false, - onLiftClick, - currentLevel, - ...otherProps - }: LiftsOverlayProps): JSX.Element => { - const viewBox = viewBoxFromLeafletBounds(otherProps.bounds); - const scale = useAutoScale(40); - - return ( - - {lifts.map((lift) => { - const pos = fromRmfCoords([lift.ref_x, lift.ref_y]); - return ( - - onLiftClick && onLiftClick(ev, lift)} - cx={pos[0]} - cy={pos[1]} - width={lift.width} - height={lift.depth} - yaw={radiansToDegrees(fromRmfYaw(lift.ref_yaw))} - currentLevel={currentLevel} - style={{ transform: `scale(${scale})`, transformOrigin: `${pos[0]}px ${pos[1]}px` }} - aria-label={lift.name} - labelText={lift.name} - labelSourceX={pos[0]} - labelSourceY={pos[1]} - labelSourceRadius={Math.min(lift.width / 2, lift.depth / 2)} - labelArrowLength={Math.max((lift.width / 3) * scale, (lift.depth / 3) * scale)} - hideLabel={hideLabels} - /> - - ); - })} - - ); - }, -); diff --git a/packages/dashboard/src/components/map-app.tsx b/packages/dashboard/src/components/map-app.tsx index eed3e596c..11b498b91 100644 --- a/packages/dashboard/src/components/map-app.tsx +++ b/packages/dashboard/src/components/map-app.tsx @@ -1,6 +1,7 @@ import { Box, styled, Typography, useMediaQuery } from '@mui/material'; +import { Line } from '@react-three/drei'; +import { Canvas, useLoader } from '@react-three/fiber'; import { BuildingMap, FleetState, Level, Lift } from 'api-client'; -import { Door as DoorModel } from 'rmf-models'; import Debug from 'debug'; import React, { ChangeEvent, Suspense } from 'react'; import { @@ -9,27 +10,25 @@ import { getPlaces, Place, ReactThreeFiberImageMaker, + RobotData, RobotTableData, ShapeThreeRendering, TextThreeRendering, - RobotData, } from 'react-components'; import { ErrorBoundary } from 'react-error-boundary'; -import { EMPTY, merge, scan, Subscription, switchMap } from 'rxjs'; +import { Door as DoorModel } from 'rmf-models'; +import { EMPTY, merge, scan, Subscription, switchMap, throttleTime } from 'rxjs'; +import { Box3, TextureLoader, Vector3 } from 'three'; import appConfig from '../app-config'; import { AppControllerContext, ResourcesContext } from './app-contexts'; import { AppEvents } from './app-events'; +import { DoorSummary } from './door-summary'; +import { LiftSummary } from './lift-summary'; import { createMicroApp } from './micro-app'; import { RmfAppContext } from './rmf-app'; -import { TrajectoryData } from './trajectories-overlay'; import { RobotSummary } from './robots/robot-summary'; -import { Box3, TextureLoader, Vector3 } from 'three'; -import { Canvas, useLoader } from '@react-three/fiber'; -import { Line } from '@react-three/drei'; -import { CameraControl, LayersController } from './three-fiber'; -import { Lifts, Door, RobotThree } from './three-fiber'; -import { DoorSummary } from './door-summary'; -import { LiftSummary } from './lift-summary'; +import { CameraControl, Door, LayersController, Lifts, RobotThree } from './three-fiber'; +import { TrajectoryData } from './trajectories-overlay'; const debug = Debug('MapApp'); @@ -310,7 +309,15 @@ export const MapApp = styled( const sub = rmf.fleetsObs .pipe( switchMap((fleets) => - merge(...fleets.map((f) => (f.name ? rmf.getFleetStateObs(f.name) : EMPTY))), + merge( + ...fleets.map((f) => + f.name + ? rmf + .getFleetStateObs(f.name) + .pipe(throttleTime(500, undefined, { leading: true, trailing: true })) + : EMPTY, + ), + ), ), ) .subscribe((fleetState) => { diff --git a/packages/dashboard/src/components/rmf-app/rmf-ingress.ts b/packages/dashboard/src/components/rmf-app/rmf-ingress.ts index d9a4cb53c..06f07a878 100644 --- a/packages/dashboard/src/components/rmf-app/rmf-ingress.ts +++ b/packages/dashboard/src/components/rmf-app/rmf-ingress.ts @@ -137,8 +137,16 @@ export class RmfIngress { sioSubscribe: (handler: (data: T) => void) => SioSubscription, ): Observable { return new Observable((subscriber) => { - const sioSub = sioSubscribe(subscriber.next.bind(subscriber)); - return () => this._sioClient.unsubscribe(sioSub); + let sioSub: SioSubscription | null = null; + const onConnect = () => { + sioSub = sioSubscribe(subscriber.next.bind(subscriber)); + }; + onConnect(); + this._sioClient.sio.on('connect', onConnect); + return () => { + sioSub && this._sioClient.unsubscribe(sioSub); + this._sioClient.sio.off('connect', onConnect); + }; }).pipe(shareReplay(1)); } diff --git a/packages/dashboard/src/components/robots-overlay.tsx b/packages/dashboard/src/components/robots-overlay.tsx deleted file mode 100644 index 2d3dc9405..000000000 --- a/packages/dashboard/src/components/robots-overlay.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { RobotState } from 'api-client'; -import React from 'react'; -import { - fromRmfCoords, - fromRmfYaw, - RobotMarker as BaseRobotMarker, - RobotMarkerProps as BaseRobotMarkerProps, - SVGOverlay, - SVGOverlayProps, - useAutoScale, - viewBoxFromLeafletBounds, - withLabel, - WithLabelProps, -} from 'react-components'; -import { EMPTY, mergeMap, of } from 'rxjs'; -import { RmfAppContext } from './rmf-app'; - -export interface RobotData { - fleet: string; - name: string; - model: string; - footprint: number; - color: string; - inConflict?: boolean; - iconPath?: string; -} - -const MarkerWithLabel = withLabel(BaseRobotMarker); -type MarkerWithLabelProps = WithLabelProps; - -interface RobotMarkerProps - extends Omit { - robot: RobotData; - scale: number; -} - -const RobotMarker = ({ robot, scale, ...otherProps }: RobotMarkerProps) => { - const rmf = React.useContext(RmfAppContext); - const [robotState, setRobotState] = React.useState(undefined); - React.useEffect(() => { - if (!rmf) { - return; - } - const sub = rmf - .getFleetStateObs(robot.fleet) - .pipe( - mergeMap((state) => - state.robots && state.robots[robot.name] ? of(state.robots[robot.name]) : EMPTY, - ), - ) - .subscribe(setRobotState); - return () => sub.unsubscribe(); - }, [rmf, robot]); - - const [x, y] = robotState?.location - ? fromRmfCoords([robotState.location.x, robotState.location.y]) - : [0, 0]; - const theta = robotState?.location ? fromRmfYaw(robotState.location.yaw) : 0; - - return ( - - ); -}; - -export interface RobotsOverlayProps extends Omit { - robots: RobotData[]; - /** - * The zoom level at which the markers should transition from actual size to fixed size. - */ - markerActualSizeMinZoom?: number; - hideLabels?: boolean; - onRobotClick?: (ev: React.MouseEvent, robot: RobotData) => void; -} - -export const RobotsOverlay = React.memo( - ({ - robots, - hideLabels = false, - onRobotClick, - ...otherProps - }: RobotsOverlayProps): JSX.Element => { - const viewBox = viewBoxFromLeafletBounds(otherProps.bounds); - const scale = useAutoScale(40); - // TODO: hardcoded because footprint is not available in rmf. - const footprint = 0.5; - - return ( - - {robots.map((robot) => { - return ( - onRobotClick && onRobotClick(ev, robot)} - aria-label={robot.name} - labelText={robot.name} - labelSourceRadius={footprint * scale} - hideLabel={hideLabels} - /> - ); - })} - - ); - }, -); diff --git a/packages/dashboard/src/components/robots/robot-info-app.tsx b/packages/dashboard/src/components/robots/robot-info-app.tsx index 2bfbc3a51..848951519 100644 --- a/packages/dashboard/src/components/robots/robot-info-app.tsx +++ b/packages/dashboard/src/components/robots/robot-info-app.tsx @@ -2,7 +2,7 @@ import { Box, CardContent, Typography } from '@mui/material'; import { RobotState, TaskState } from 'api-client'; import React from 'react'; import { RobotInfo } from 'react-components'; -import { combineLatest, EMPTY, mergeMap, of, switchMap } from 'rxjs'; +import { EMPTY, combineLatest, mergeMap, of, switchMap, throttleTime } from 'rxjs'; import { AppEvents } from '../app-events'; import { createMicroApp } from '../micro-app'; import { RmfAppContext } from '../rmf-app'; @@ -24,6 +24,7 @@ export const RobotInfoApp = createMicroApp('Robot Info', () => { } const [fleet, name] = data; return rmf.getFleetStateObs(fleet).pipe( + throttleTime(3000, undefined, { leading: true, trailing: true }), mergeMap((fleetState) => { const robotState = fleetState?.robots?.[name]; const taskObs = robotState?.task_id diff --git a/packages/dashboard/src/components/three-fiber/door-three.tsx b/packages/dashboard/src/components/three-fiber/door-three.tsx index 9a6449c7f..476535343 100644 --- a/packages/dashboard/src/components/three-fiber/door-three.tsx +++ b/packages/dashboard/src/components/three-fiber/door-three.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import { ThreeEvent } from '@react-three/fiber'; import { DoorState, Lift, LiftState } from 'api-client'; -import { Door as DoorModel } from 'rmf-models'; -import { RmfAppContext } from '../rmf-app'; -import { DoorMode } from 'rmf-models'; +import React from 'react'; import { DoorThreeMaker } from 'react-components'; -import { ThreeEvent } from '@react-three/fiber'; +import { DoorMode, Door as DoorModel } from 'rmf-models'; +import { throttleTime } from 'rxjs'; +import { RmfAppContext } from '../rmf-app'; interface DoorProps { door: DoorModel; @@ -35,7 +35,10 @@ export const Door = React.memo(({ ...doorProps }: DoorProps): JSX.Element => { return; } - const sub = rmf.getLiftStateObs(lift.name).subscribe(setLiftState); + const sub = rmf + .getLiftStateObs(lift.name) + .pipe(throttleTime(3000, undefined, { leading: true, trailing: true })) + .subscribe(setLiftState); return () => sub.unsubscribe(); }, [rmf, lift]); diff --git a/packages/dashboard/src/components/three-fiber/lift-three.tsx b/packages/dashboard/src/components/three-fiber/lift-three.tsx index 5f6ab6192..f3b043a90 100644 --- a/packages/dashboard/src/components/three-fiber/lift-three.tsx +++ b/packages/dashboard/src/components/three-fiber/lift-three.tsx @@ -1,8 +1,9 @@ -import React from 'react'; +import { ThreeEvent } from '@react-three/fiber'; import { Lift, LiftState } from 'api-client'; -import { RmfAppContext } from '../rmf-app'; +import React from 'react'; import { LiftThreeMaker } from 'react-components'; -import { ThreeEvent } from '@react-three/fiber'; +import { throttleTime } from 'rxjs'; +import { RmfAppContext } from '../rmf-app'; interface LiftsProps { opacity: number; @@ -49,7 +50,10 @@ export const Lifts = React.memo(({ lift, onLiftClick }: LiftsProps): JSX.Element return; } - const sub = rmf.getLiftStateObs(lift.name).subscribe(setLiftState); + const sub = rmf + .getLiftStateObs(lift.name) + .pipe(throttleTime(3000, undefined, { leading: true, trailing: true })) + .subscribe(setLiftState); return () => sub.unsubscribe(); }, [rmf, lift]); diff --git a/packages/dashboard/src/components/waypoints-overlay.tsx b/packages/dashboard/src/components/waypoints-overlay.tsx deleted file mode 100644 index d6d02f83c..000000000 --- a/packages/dashboard/src/components/waypoints-overlay.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import { - fromRmfCoords, - Place, - SVGOverlay, - SVGOverlayProps, - useAutoScale, - viewBoxFromLeafletBounds, - WaypointMarker as WaypointMarker_, - withLabel, -} from 'react-components'; - -// no need memo since waypoint doesn't have state and should never re-render. -const WaypointMarker = withLabel(WaypointMarker_); - -export interface WaypointsOverlayProps extends Omit { - waypoints: Place[]; - hideLabels?: boolean; -} - -export const WaypointsOverlay = React.memo( - ({ waypoints, hideLabels = false, ...otherProps }: WaypointsOverlayProps): JSX.Element => { - const viewBox = viewBoxFromLeafletBounds(otherProps.bounds); - // Set the size of the waypoint. At least for now we don't want for this to change. We left this here in case we want for this to change in the future. - const size = 0.2; - const scale = useAutoScale(60); - - return ( - - {waypoints.map((waypoint, idx) => { - const [x, y] = fromRmfCoords([waypoint.vertex.x, waypoint.vertex.y]); - return ( - - - - ); - })} - - ); - }, -); diff --git a/packages/dashboard/src/components/workcells-overlay.tsx b/packages/dashboard/src/components/workcells-overlay.tsx deleted file mode 100644 index c0a0cd2b5..000000000 --- a/packages/dashboard/src/components/workcells-overlay.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import { - fromRmfCoords, - SVGOverlay, - SVGOverlayProps, - useAutoScale, - viewBoxFromLeafletBounds, - withLabel, - WorkcellMarker as BaseWorkcellMarker, -} from 'react-components'; - -const WorkcellMarker = withLabel(BaseWorkcellMarker); - -export interface WorkcellData { - guid: string; - location: [x: number, y: number]; - iconPath?: string; -} - -export interface WorkcellsOverlayProps extends Omit { - workcells: WorkcellData[]; - actualSizeMinZoom?: number; - hideLabels?: boolean; - onWorkcellClick?: (event: React.MouseEvent, guid: string) => void; -} - -export const WorkcellsOverlay = React.memo( - ({ - workcells, - hideLabels = false, - onWorkcellClick, - ...otherProps - }: WorkcellsOverlayProps): JSX.Element => { - const viewBox = viewBoxFromLeafletBounds(otherProps.bounds); - const scale = useAutoScale(40); - - return ( - - {workcells.map((workcell) => { - const [x, y] = fromRmfCoords(workcell.location); - return ( - - onWorkcellClick && onWorkcellClick(ev, workcell.guid)} - aria-label={workcell.guid} - style={{ transform: `scale(${scale})`, transformOrigin: `${x}px ${y}px` }} - labelText={workcell.guid} - labelSourceX={x} - labelSourceY={y} - labelSourceRadius={0.5 * scale} - hideLabel={hideLabels} - /> - - ); - })} - - ); - }, -);