Skip to content

Commit

Permalink
Feature/robot status (#654)
Browse files Browse the repository at this point in the history
* add saga

* More changes

* Add UI

* Add comments
  • Loading branch information
qgolsteyn authored Jun 25, 2019
1 parent 43f1971 commit e1750de
Show file tree
Hide file tree
Showing 17 changed files with 295 additions and 12 deletions.
2 changes: 2 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export {
TOPIC_ROSOUT_TYPE,
TOPIC_PLAY_INFO,
TOPIC_PLAY_INFO_TYPE,
TOPIC_ROBOT_STATUS,
TOPIC_ROBOT_STATUS_TYPE,
} from './topics';
export {
PARAM_RUN_AI,
Expand Down
12 changes: 12 additions & 0 deletions src/constants/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,16 @@ export const theme: IThemeProvider = {
info: Colors.BLUE2,
debug: Colors.GRAY5,
},
qualitativeColorScale: [
'#2965CC',
'#29A634',
'#D99E0B',
'#D13913',
'#8F398F',
'#00B3A4',
'#DB2C6F',
'#9BBF30',
'#96622D',
'#7157D9',
],
};
2 changes: 2 additions & 0 deletions src/constants/topics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export const TOPIC_ROSOUT_TYPE = 'rosgraph_msgs/Log';

export const TOPIC_PLAY_INFO = '/backend/play_info';
export const TOPIC_PLAY_INFO_TYPE = 'thunderbots_msgs/PlayInfo';
export const TOPIC_ROBOT_STATUS = '/backend/robot_status';
export const TOPIC_ROBOT_STATUS_TYPE = 'thunderbots_msgs/RobotStatus';
8 changes: 7 additions & 1 deletion src/pages/Visualizer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import { Portal, PortalLocation } from 'SRC/components/Portal';
import { SPRITESHEET } from 'SRC/constants';
import { Canvas, CanvasManager, LayerReceiver } from 'SRC/containers/Canvas';
import { actions, RootAction } from 'SRC/store';
import { ILayer, IRootState, IROSParamState } from 'SRC/types';
import { ILayer, IRootState, IROSParamState, IRobotStatuses } from 'SRC/types';

import { ParamPanel } from './panels/ParamPanel';
import { LayersPanel } from './panels/LayersPanel';
import { PlayTypePanel } from './panels/PlayTypePanel';
import { RobotStatusPanel } from './panels/RobotStatusPanel/index';

// We request the layer data from the store
const mapStateToProps = (state: IRootState) => ({
Expand All @@ -27,6 +28,7 @@ const mapStateToProps = (state: IRootState) => ({
playName: state.thunderbots.playName,
tactics: state.thunderbots.tactics,
params: state.rosParameters,
robotStatuses: state.robotStatus.statuses,
});

// We request layer related actions
Expand All @@ -47,6 +49,7 @@ interface IVisualizerProps {
playName: string;
tactics: string[];
params: IROSParamState;
robotStatuses: IRobotStatuses;

// Actions
addLayer: typeof actions.canvas.addLayer;
Expand Down Expand Up @@ -103,6 +106,9 @@ class VisualizerInternal extends React.Component<IVisualizerProps> {
<Portal portalLocation={PortalLocation.MAIN}>
<Canvas canvasManager={this.canvasManager} />
</Portal>
<Portal portalLocation={PortalLocation.CONSOLE}>
<RobotStatusPanel robotStatuses={this.props.robotStatuses} />
</Portal>
</>
);
}
Expand Down
59 changes: 59 additions & 0 deletions src/pages/Visualizer/panels/RobotStatusPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/***
* This file defines the UI to view robot statuses
*/

import { Tag } from '@blueprintjs/core';
import { Box } from '@rebass/grid';
import * as React from 'react';

import { Flex } from 'SRC/components/Layout';
import { IRobotStatuses } from 'SRC/types';
import { theme } from 'SRC/constants';

interface IRobotStatusPanelProps {
robotStatuses: IRobotStatuses;
}

/**
* Displays robot statuses sorted by time
*/
export const RobotStatusPanel = (props: IRobotStatusPanelProps) => {
// Sort by time, then by robot number, then by message alphabetically
const sortedRobotStatuses = Object.values(props.robotStatuses).sort((a, b) => {
const timeDelta = a.timestamp - b.timestamp;
const robotDelta = a.robot - b.robot;
if (timeDelta != 0) {
return timeDelta;
} else if (robotDelta != 0) {
return robotDelta;
} else {
return a.message.localeCompare(b.message);
}
});

return (
<Box padding="2px 0">
{sortedRobotStatuses.map((status) => (
<Flex
key={`${status.robot}:${status.message}`}
width="100%"
alignItems="center"
padding="2px 5px"
>
<Tag
style={{
background:
theme.qualitativeColorScale[
status.robot % theme.qualitativeColorScale.length
],
}}
>
Robot {status.robot}
</Tag>
<div style={{ marginLeft: '5px' }}>{status.message}</div>
<Tag style={{ marginLeft: 'auto' }}>{status.timestamp} s. ago</Tag>
</Flex>
))}
</Box>
);
};
6 changes: 5 additions & 1 deletion src/store/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@ import * as canvasActions from './canvas';
import * as rosActions from './ros';
import * as rosParametersActions from './rosParameters';
import * as thunderbotsActions from './thunderbots';
import * as statusActions from './robotStatus';

import { CanvasAction } from '../reducers/canvas';
import { ROSAction } from '../reducers/ros';
import { RosParametersActions } from '../reducers/rosParameters';
import { ThunderbotsAction } from '../reducers/thunderbots';
import { StatusAction } from '../reducers/robotStatus';

export const actions = {
canvas: canvasActions,
thunderbots: thunderbotsActions,
ros: rosActions,
rosParameters: rosParametersActions,
status: statusActions,
};

// We combine all action types for convenient access throughout the application
export type RootAction =
| CanvasAction
| ThunderbotsAction
| ROSAction
| RosParametersActions;
| RosParametersActions
| StatusAction;
17 changes: 17 additions & 0 deletions src/store/actions/robotStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* This file specifies robot_status specific action
*
* We are using the format specified here
* @see https://github.com/piotrwitek/typesafe-actions#createaction
*/

import { createAction } from 'typesafe-actions';

import { IRobotStatuses } from 'SRC/types';

/**
* Update the displayed list of robot statuses
*/
export const updateRobotStatuses = createAction('status_UPDATE', (resolve) => {
return (statuses: IRobotStatuses) => resolve({ statuses });
});
2 changes: 2 additions & 0 deletions src/store/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import canvasReducer from './canvas';
import rosReducer from './ros';
import rosParametersReducer from './rosParameters';
import thunderbotsReducer from './thunderbots';
import robotStatusReducer from './robotStatus';

/**
* Combines all reducers. This is what the Redux accepts when being
Expand All @@ -18,4 +19,5 @@ export default combineReducers({
thunderbots: thunderbotsReducer,
ros: rosReducer,
rosParameters: rosParametersReducer,
robotStatus: robotStatusReducer,
});
30 changes: 30 additions & 0 deletions src/store/reducers/robotStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* This file specifies the robots status reducer
*/

import { ActionType, getType } from 'typesafe-actions';

import { IRobotStatusState } from 'SRC/types';

import * as status from '../actions/robotStatus';

export type StatusAction = ActionType<typeof status>;

const defaultState: IRobotStatusState = {
statuses: {},
};

/**
* Reducer function for robot status
*/
export default (state: IRobotStatusState = defaultState, action: StatusAction) => {
switch (action.type) {
case getType(status.updateRobotStatuses):
return {
...state,
statuses: { ...state.statuses, ...action.payload.statuses },
};
default:
return state;
}
};
8 changes: 5 additions & 3 deletions src/store/sagas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import { spawn } from 'redux-saga/effects';
import initROS from './ros';
import initROSParameters from './rosParameters';
import initThunderbots from './thunderbots';
import initRobotStatus from './robotStatus';

/**
* Starts all application sagas
*/
export function* init() {
yield spawn(initROS);
yield spawn(initROSParameters);
yield spawn(initThunderbots);
yield spawn(initROS);
yield spawn(initROSParameters);
yield spawn(initThunderbots);
yield spawn(initRobotStatus);
}
78 changes: 78 additions & 0 deletions src/store/sagas/robotStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/***
* This file defines the saga for robot statuses
*/

import { channel } from 'redux-saga';
import { put, spawn, take, takeLatest } from 'redux-saga/effects';
import { getType } from 'typesafe-actions';

import { TOPIC_ROBOT_STATUS, TOPIC_ROBOT_STATUS_TYPE } from 'SRC/constants';
import { IRobotStatus, IRobotStatusMessage } from 'SRC/types';
import * as ROS from 'SRC/utils/ros';

import { actions } from '../actions/index';

const statusChannel = channel();

export default function* init() {
// Listen to start actions and start robot status
yield takeLatest(getType(actions.ros.connected), startRobotStatusSaga);

// Start listening to robot status messages
yield spawn(listenToConsoleChannel);
}

/**
* Take any messages received from robot status and push them as Redux actions
*/
function* listenToConsoleChannel() {
while (true) {
const action = yield take(statusChannel);
yield put(action);
}
}

let processedMessages: { [key: string]: IRobotStatus } = {};

/**
* We subscribe to topic robot_status to start receiving messages
*/
function startRobotStatusSaga() {
ROS.subscribeToROSTopic(
TOPIC_ROBOT_STATUS,
TOPIC_ROBOT_STATUS_TYPE,
(message: IRobotStatusMessage) => {
processedMessages = { ...processedMessages, ...processMessage(message) };
},
100,
);

// We update the state once a second
setTimeout(updateTimestampAndPush, 1000);
}

function updateTimestampAndPush() {
// Push current robot status to the state
statusChannel.put(actions.status.updateRobotStatuses(processedMessages));

// And increase timestamp by 1 (one second elapsed)
Object.values(processedMessages).forEach((processedMessage) => {
processedMessage.timestamp = processedMessage.timestamp + 1;
});
setTimeout(updateTimestampAndPush, 1000);
}

function processMessage(message: IRobotStatusMessage) {
const processedMessage: { [key: string]: IRobotStatus } = {};
message.robot_messages.forEach((robotMessage: string) => {
const robotStatus: IRobotStatus = {
message: robotMessage,
robot: message.robot,
timestamp: 0,
};

processedMessage[`${message.robot}: ${robotMessage}`] = robotStatus;
});

return processedMessage;
}
2 changes: 1 addition & 1 deletion src/store/sagas/thunderbots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ function startThunderbotsSaga() {
(message: IPlayInfoMessage) => {
thunderbotsChannel.put(actions.thunderbots.setPlayInformation(message));
},
500,
1000,
);
}
14 changes: 8 additions & 6 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ export { Color } from './primitives';
export { IThemeProvider } from './theme';
export { ShapeType, ISpritesheet, IFrame, IShape } from './spritesheet';
export {
ICanvasState,
IRootState,
IROSState,
IThunderbotsState,
IROSParamState,
ICanvasState,
IRootState,
IROSState,
IThunderbotsState,
IROSParamState,
IRobotStatusState,
} from './state';
export { ILayer, ILayerMessage, ISprite } from './canvas';
export { IPlayInfoMessage } from './thunderbotsROSMessages';
export { IPlayInfoMessage, IRobotStatusMessage } from './thunderbotsROSMessages';
export { IRobotStatus, IRobotStatuses } from './status';
9 changes: 9 additions & 0 deletions src/types/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { ILayer } from './canvas';
import { IROSParam } from './rosParams';
import { IRobotStatus } from './status';

/**
* The application state
Expand All @@ -13,6 +14,7 @@ export interface IRootState {
thunderbots: IThunderbotsState;
rosParameters: IROSParamState;
ros: IROSState;
robotStatus: IRobotStatusState;
}

/**
Expand Down Expand Up @@ -43,3 +45,10 @@ export interface IThunderbotsState {
export interface IROSParamState {
[key: string]: IROSParam;
}

/**
* The robot status state
*/
export interface IRobotStatusState {
statuses: { [key: string]: IRobotStatus };
}
Loading

0 comments on commit e1750de

Please sign in to comment.