Skip to content
Snippets Groups Projects
Commit 183eba67 authored by Sandro Weber's avatar Sandro Weber
Browse files

Merged in NRRPLT-8183-running-simulation-view (pull request #28)

NRRPLT-8183 running simulation view

Approved-by: Antoine Detailleur
parents c4b94ef0 c81c9c74
No related branches found
No related tags found
No related merge requests found
Showing
with 2806 additions and 2744 deletions
source diff could not be displayed: it is too large. Options to address this: view the blob.
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
"@material-ui/icons": "4.11.2", "@material-ui/icons": "4.11.2",
"@material-ui/lab": "4.0.0-alpha.57", "@material-ui/lab": "4.0.0-alpha.57",
"bootstrap": "4.5", "bootstrap": "4.5",
"flexlayout-react": "0.5.5",
"jszip": "3.2.0", "jszip": "3.2.0",
"jquery": "3.6.0", "jquery": "3.6.0",
"react": "^17.0.1", "react": "^17.0.1",
...@@ -23,7 +24,7 @@ ...@@ -23,7 +24,7 @@
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-icons": "4.1.0", "react-icons": "4.1.0",
"react-router-dom": "5.2.0", "react-router-dom": "5.2.0",
"react-scripts": "4.0.0", "react-scripts": "4.0.3",
"react-tabs": "3.1.2", "react-tabs": "3.1.2",
"roslib": "1.1.0", "roslib": "1.1.0",
"rxjs": "6.6.3", "rxjs": "6.6.3",
......
...@@ -2,25 +2,23 @@ import React from 'react'; ...@@ -2,25 +2,23 @@ import React from 'react';
import { HashRouter, Switch, Route } from 'react-router-dom'; import { HashRouter, Switch, Route } from 'react-router-dom';
import EntryPage from './components/entry-page/entry-page.js'; import EntryPage from './components/entry-page/entry-page';
import ErrorDialog from './components/dialog/error-dialog.js'; import ErrorDialog from './components/dialog/error-dialog.js';
import ExperimentOverview from './components/experiment-overview/experiment-overview';
import SimulationView from './components/simulation-view/simulation-view';
import NotificationDialog from './components/dialog/notification-dialog.js'; import NotificationDialog from './components/dialog/notification-dialog.js';
import ExperimentOverview from './components/experiment-overview/experiment-overview.js';
class App extends React.Component { class App extends React.Component {
render() { render() {
return( return (
<div> <div>
<ErrorDialog /> <ErrorDialog />
<NotificationDialog/> <NotificationDialog/>
<HashRouter> <HashRouter>
<Switch> <Switch>
<Route path='/experiments-overview'> <Route path='/experiments-overview' component={ExperimentOverview} />
<ExperimentOverview /> <Route path='/simulation-view/:serverIP/:simulationID' component={SimulationView} />
</Route> <Route path='/' component={EntryPage} />
<Route path='/'>
<EntryPage />
</Route>
</Switch> </Switch>
</HashRouter> </HashRouter>
</div> </div>
......
...@@ -64,7 +64,8 @@ class NotificationDialog extends React.Component{ ...@@ -64,7 +64,8 @@ class NotificationDialog extends React.Component{
<strong className='mr-auto'>{notification.type}</strong> <strong className='mr-auto'>{notification.type}</strong>
</Toast.Header> </Toast.Header>
<Toast.Body> <Toast.Body>
{notification.message} <h6>{notification.message}</h6>
{notification.details}
</Toast.Body> </Toast.Body>
</Toast> </Toast>
</li> </li>
......
...@@ -10,9 +10,9 @@ ...@@ -10,9 +10,9 @@
top: 15%; top: 15%;
box-shadow: 5px 5px 5px #000; box-shadow: 5px 5px 5px #000;
z-index: 10001; z-index: 10001;
} }
.import-button { .import-button {
cursor: pointer; cursor: pointer;
height: 50%; height: 50%;
} }
\ No newline at end of file \ No newline at end of file
import React from 'react'; import React from 'react';
import { FaStop, FaStopCircle } from 'react-icons/fa'; import { FaStop, FaStopCircle } from 'react-icons/fa';
import { ImEnter } from 'react-icons/im'; import { ImEnter } from 'react-icons/im';
import { withRouter } from 'react-router-dom';
import timeDDHHMMSS from '../../utility/time-filter.js'; import timeDDHHMMSS from '../../utility/time-filter.js';
import { EXPERIMENT_STATE } from '../../services/experiments/experiment-constants.js'; import { EXPERIMENT_STATE } from '../../services/experiments/experiment-constants.js';
...@@ -8,7 +9,7 @@ import ExperimentExecutionService from '../../services/experiments/execution/exp ...@@ -8,7 +9,7 @@ import ExperimentExecutionService from '../../services/experiments/execution/exp
import './simulation-details.css'; import './simulation-details.css';
export default class SimulationDetails extends React.Component { class SimulationDetails extends React.Component {
constructor() { constructor() {
super(); super();
...@@ -54,6 +55,12 @@ export default class SimulationDetails extends React.Component { ...@@ -54,6 +55,12 @@ export default class SimulationDetails extends React.Component {
}); });
} }
joinSimulation(simulationInfo) {
this.props.history.push({
pathname: '/simulation-view/' + simulationInfo.server + '/' + simulationInfo.runningSimulation.simulationID
});
}
render() { render() {
return ( return (
<div className='simulations-details-wrapper'> <div className='simulations-details-wrapper'>
...@@ -78,7 +85,10 @@ export default class SimulationDetails extends React.Component { ...@@ -78,7 +85,10 @@ export default class SimulationDetails extends React.Component {
ng-click="(simulation.runningSimulation.state === STATE.CREATED) || ng-click="(simulation.runningSimulation.state === STATE.CREATED) ||
simulation.stopping || joinExperiment(simulation, exp);"*/ simulation.stopping || joinExperiment(simulation, exp);"*/
type="button" className='nrp-btn btn-default' type="button" className='nrp-btn btn-default'
disabled={this.isJoinDisabled(simulation)}> disabled={this.isJoinDisabled(simulation)}
onClick={() => {
this.joinSimulation(simulation);
}}>
<ImEnter className='icon' />Join <ImEnter className='icon' />Join
</button> </button>
{/* Stop button enabled provided simulation state is consistent */} {/* Stop button enabled provided simulation state is consistent */}
...@@ -106,3 +116,4 @@ export default class SimulationDetails extends React.Component { ...@@ -106,3 +116,4 @@ export default class SimulationDetails extends React.Component {
); );
} }
} }
export default withRouter(SimulationDetails);
.leave-simulation-dialog-header {
color: black;
background-color: white;
}
\ No newline at end of file
import React from 'react';
import {Button } from 'react-bootstrap';
import Modal from 'react-bootstrap/Modal';
import './leave-simulation-dialog.css';
class LeaveSimulationDialog extends React.Component{
render(){
return (
<div>
<div>
<Modal show={this.props.visible} onHide={() => this.props.setVisibility(false)}>
<Modal.Header closeButton className="leave-simulation-dialog-header">
<Modal.Title>Exit menu</Modal.Title>
</Modal.Header>
<Modal.Body>Would you like to leave or stop the simulation?</Modal.Body>
<Modal.Footer>
<div>
<Button variant="light" onClick={() => this.props.leaveSimulation()}>
Leave
</Button>
<Button variant="danger" onClick={() => this.props.stopSimulation()}>
Stop
</Button>
</div>
</Modal.Footer>
</Modal>
</div>
</div>
);
}
}
export default LeaveSimulationDialog;
\ No newline at end of file
let _instance = null;
const SINGLETON_ENFORCER = Symbol();
/**
* Service handling server resources for simulating experiments.
*/
class SimulationToolsService {
constructor(enforcer) {
if (enforcer !== SINGLETON_ENFORCER) {
throw new Error('Use ' + this.constructor.name + '.instance');
}
this.tools = new Map();
for (const toolEntry in SimulationToolsService.TOOLS) {
this.registerToolConfig(SimulationToolsService.TOOLS[toolEntry]);
}
}
static get instance() {
if (_instance == null) {
_instance = new SimulationToolsService(SINGLETON_ENFORCER);
}
return _instance;
}
registerToolConfig(toolConfig) {
let id = toolConfig.flexlayoutNode.component;
if (this.tools.has(id)) {
console.warn('SimulationToolsService.registerToolConfig() - tool with ID ' + id + ' already exists');
return;
}
this.tools.set(id, toolConfig);
}
flexlayoutNodeFactory(node) {
var component = node.getComponent();
let toolConfig = this.tools.get(component);
if (toolConfig && toolConfig.flexlayoutFactoryCb) {
return toolConfig.flexlayoutFactoryCb();
}
if (component === 'button') {
return <button>{node.getName()}</button>;
}
else if (component === 'nest_wiki') {
return <iframe src='https://en.wikipedia.org/wiki/NEST_(software)' title='nest_wiki'
className='flexlayout-iframe'></iframe>;
}
}
startToolDrag(flexlayoutNode, layoutReference) {
layoutReference.current.addTabWithDragAndDrop(flexlayoutNode.name, flexlayoutNode);
}
}
SimulationToolsService.TOOLS = Object.freeze({
NEST_DESKTOP: {
singleton: true,
flexlayoutNode: {
'type': 'tab',
'name': 'NEST Desktop',
'component': 'nest-desktop'
},
flexlayoutFactoryCb: () => {
return <iframe src='http://localhost:8000' title='NEST Desktop' />;
},
getIcon: () => {
return <div>
<img src={'https://www.nest-simulator.org/wp-content/uploads/2015/03/nest_logo.png'}
alt="NEST Desktop"
style={{width: 40+ 'px', height: 20 + 'px'}} />
<span>Desktop</span>
</div>;
}
},
TEST_NRP_CORE_DOCU: {
singleton: true,
flexlayoutNode: {
'type': 'tab',
'name': 'NRP-Core Docs',
'component': 'nrp-core-docu'
},
flexlayoutFactoryCb: () => {
return <iframe src='https://hbpneurorobotics.bitbucket.io/index.html'
title='NRP-Core Documentation' />;
},
getIcon: () => {
return <span>NRP-Core Docs</span>;
}
}
});
SimulationToolsService.CONSTANTS = Object.freeze({
CATEGORY: {
EXTERNAL_IFRAME: 'EXTERNAL_IFRAME',
REACT_COMPONENT: 'REACT_COMPONENT'
}
});
export default SimulationToolsService;
.simulation-view-wrapper {
display: grid;
grid-template-rows: auto auto;
grid-template-columns: 68px auto;
grid-template-areas:
"simulation-view-header simulation-view-header"
"simulation-view-sidebar simulation-view-mainview";
}
.simulation-view-header {
grid-area: simulation-view-header;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: yellow;
padding: 3px;
}
.simulation-view-sidebar {
padding: 2px;
grid-area: simulation-view-sidebar;
background-color: green;
}
.simulation-view-mainview {
grid-area: simulation-view-mainview;
background-color: red;
}
.flexlayout__layout {
position: relative;
height: calc(100vh - 63px);
}
.flexlayout__tab {
overflow: hidden;
}
.simulation-view-controls {
display: flex;
flex-direction: row;
align-items: center;
}
.simulation-view-control-buttons {
padding: 0px 10px 0px 10px;
}
.simulation-view-time-info {
display: grid;
gap: 3px;
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
padding: 0px 10px 0px 10px;
font-size: 0.7em;
}
.simulation-view-experiment-title {
padding: 0px 10px 0px 10px;
font-weight: bold;
}
.simulation-tool-button {
width: 60px;
height: 60px;
margin: 2px;
padding: 2px;
font-size: 0.7em;
color: black;
font-weight: bold;
background-color: lightgray;
}
iframe {
width: 100%;
height: 100%;
}
\ No newline at end of file
import React from 'react';
import FlexLayout from 'flexlayout-react';
import { OverlayTrigger, Tooltip, Button } from 'react-bootstrap';
import { RiPlayFill, RiPauseFill, RiLayout6Line } from 'react-icons/ri';
import { GiExitDoor } from 'react-icons/gi';
import { TiMediaRecord } from 'react-icons/ti';
import { VscDebugRestart } from 'react-icons/vsc';
import SimulationToolsService from './simulation-tools-service';
import ServerResourcesService from '../../services/experiments/execution/server-resources-service.js';
import ExperimentStorageService from '../../services/experiments/files/experiment-storage-service';
import RunningSimulationService from '../../services/experiments/execution/running-simulation-service';
import { EXPERIMENT_STATE } from '../../services/experiments/experiment-constants';
import timeDDHHMMSS from '../../utility/time-filter';
import LeaveSimulationDialog from './leave-simulation-dialog';
import '../../../node_modules/flexlayout-react/style/light.css';
import './simulation-view.css';
const jsonBaseLayout = {
global: {},
borders: [],
layout:{
'type': 'row',
'weight': 100,
'children': [
{
'type': 'tabset',
'weight': 50,
'selected': 0,
'children': [
{
'type': 'tab',
'name': 'NEST wiki page',
'component':'nest_wiki'
}
]
}
]
}
};
export default class SimulationView extends React.Component {
constructor(props) {
super(props);
const {serverIP, simulationID} = props.match.params;
//console.info('SimulationView ' + serverIP + ' ' + simulationID);
this.serverIP = serverIP;
this.simulationID = simulationID;
this.serverURL = 'http://' + this.serverIP + ':8080'; // this should probably be part of some config
this.state = {
modelFlexLayout: FlexLayout.Model.fromJson(jsonBaseLayout),
showLeaveDialog: false
};
this.refLayout = React.createRef();
}
async componentDidMount() {
await this.updateSimulationInfo();
let experiments = await ExperimentStorageService.instance.getExperiments();
this.experimentInfo = experiments.find(experiment => experiment.id === this.state.simulationInfo.experimentID);
console.info('SimulationView - experimentInfo');
console.info(this.experimentInfo);
let experimentName = this.experimentInfo.configuration.name;
this.setState({experimentName: experimentName});
let server = this.experimentInfo.joinableServers.find(
server => server.runningSimulation.creationUniqueID === this.state.simulationInfo.creationUniqueID);
this.serverConfig = await ServerResourcesService.instance.getServerConfig(server.server);
console.info('this.serverConfig');
console.info(this.serverConfig);
RunningSimulationService.instance.addRosStatusInfoCallback(
this.serverConfig.rosbridge.websocket,
(data) => {
this.onStatusInfoROS(data);
}
);
}
async updateSimulationInfo() {
let simInfo = await RunningSimulationService.instance.getInfo(this.serverURL, this.simulationID);
this.setState({simulationInfo: simInfo});
console.info('SimulationView.updateSimulationInfo - simulationInfo');
console.info(this.state.simulationInfo);
}
onStatusInfoROS(message) {
this.setState({
timingRealtime: timeDDHHMMSS(message.realTime),
timingSimulationTime: timeDDHHMMSS(message.simulationTime),
timingTimeout: timeDDHHMMSS(message.timeout)
});
}
async onButtonStartPause() {
let newState = this.state.simulationInfo.state === EXPERIMENT_STATE.PAUSED
? EXPERIMENT_STATE.STARTED
: EXPERIMENT_STATE.PAUSED;
await RunningSimulationService.instance.updateState(this.serverURL, this.simulationID, newState);
this.updateSimulationInfo();
}
onButtonLayout() {
console.info(this.state.modelFlexLayout.toJson());
}
showLeaveDialog(show) {
this.setState({showLeaveDialog: show});
}
leaveSimulation() {
this.props.history.push({
pathname: '/experiments-overview'
});
}
render() {
return (
<div>
<LeaveSimulationDialog visible={this.state.showLeaveDialog}
setVisibility={(visible) => this.showLeaveDialog(visible)}
stopSimulation={async () => {
await RunningSimulationService.instance.updateState(this.serverURL, this.simulationID,
EXPERIMENT_STATE.STOPPED);
this.leaveSimulation();
}}
leaveSimulation={() => {
this.leaveSimulation();
}} />
<div className='simulation-view-wrapper'>
<div className='simulation-view-header'>
<div className='simulation-view-controls'>
<div className='simulation-view-control-buttons'>
<button className='nrp-btn btn-default' onClick={() => this.showLeaveDialog(true)}>
<GiExitDoor className='icon' />
</button>
<button disabled={true} className='nrp-btn btn-default'><VscDebugRestart className='icon' /></button>
<button className='nrp-btn btn-default' onClick={() => {
this.onButtonStartPause();
}}>
{this.state.simulationInfo && this.state.simulationInfo.state === EXPERIMENT_STATE.PAUSED
? <RiPlayFill className='icon' />
: <RiPauseFill className='icon' />}
</button>
<button disabled={true} className='nrp-btn btn-default'><TiMediaRecord className='icon' /></button>
</div>
<div className='simulation-view-time-info'>
<div>Simulation time:</div>
<div>{this.state.timingSimulationTime}</div>
<div>Real time:</div>
<div>{this.state.timingRealtime}</div>
<div>Real timeout:</div>
<div>{this.state.timingTimeout}</div>
</div>
</div>
<div className='simulation-view-experiment-title'>{this.state.experimentName}</div>
<button className='nrp-btn btn-default' onClick={() => {
this.onButtonLayout();
}}><RiLayout6Line className='icon' /></button>
</div>
<div className='simulation-view-sidebar'>
{Array.from(SimulationToolsService.instance.tools.values()).map(tool => {
return (
<OverlayTrigger
key={`overlaytrigger-${tool.flexlayoutNode.component}`}
placement={'right'}
overlay={
<Tooltip id={`tooltip-${tool.flexlayoutNode.component}`}>
{tool.flexlayoutNode.name}
</Tooltip>
}
>
<Button key={tool.flexlayoutNode.component}
className="simulation-tool-button"
onMouseDown={() => {
SimulationToolsService.instance.startToolDrag(
tool.flexlayoutNode,
this.refLayout);
}}>{tool.getIcon && tool.getIcon()}</Button>
</OverlayTrigger>
);
})}
</div>
<div className='simulation-view-mainview'>
<FlexLayout.Layout ref={this.refLayout} model={this.state.modelFlexLayout}
factory={(node) => {
return SimulationToolsService.instance.flexlayoutNodeFactory(node);
}} />
</div>
</div>
</div>
);
}
}
SimulationView.CONSTANTS = Object.freeze({
INTERVAL_INTERNAL_UPDATE_MS: 1000
});
...@@ -137,7 +137,7 @@ test('respects settings for specific dev server to launch and single brain proce ...@@ -137,7 +137,7 @@ test('respects settings for specific dev server to launch and single brain proce
test('can launch an experiment given a specific server + configuration', async () => { test('can launch an experiment given a specific server + configuration', async () => {
jest.spyOn(ExperimentExecutionService.instance, 'httpRequestPOST').mockImplementation(); jest.spyOn(ExperimentExecutionService.instance, 'httpRequestPOST').mockImplementation();
jest.spyOn(RunningSimulationService.instance, 'registerForRosStatusInformation').mockImplementation(); jest.spyOn(RunningSimulationService.instance, 'addRosStatusInfoCallback').mockImplementation();
let simulationReadyResult = Promise.resolve(MockSimulations[0]); let simulationReadyResult = Promise.resolve(MockSimulations[0]);
jest.spyOn(RunningSimulationService.instance, 'simulationReady').mockImplementation(() => { jest.spyOn(RunningSimulationService.instance, 'simulationReady').mockImplementation(() => {
return simulationReadyResult; return simulationReadyResult;
...@@ -202,9 +202,9 @@ test('should be able to stop an experiment', async () => { ...@@ -202,9 +202,9 @@ test('should be able to stop an experiment', async () => {
await ExperimentExecutionService.instance.stopExperiment(simulation); await ExperimentExecutionService.instance.stopExperiment(simulation);
expect(RunningSimulationService.instance.updateState).toHaveBeenCalledTimes(2); expect(RunningSimulationService.instance.updateState).toHaveBeenCalledTimes(2);
expect(RunningSimulationService.instance.updateState).toHaveBeenCalledWith( expect(RunningSimulationService.instance.updateState).toHaveBeenCalledWith(
expect.any(String), expect.any(String), { state: EXPERIMENT_STATE.INITIALIZED }); expect.any(String), expect.any(String), EXPERIMENT_STATE.INITIALIZED);
expect(RunningSimulationService.instance.updateState).toHaveBeenCalledWith( expect(RunningSimulationService.instance.updateState).toHaveBeenCalledWith(
expect.any(String), expect.any(String), { state: EXPERIMENT_STATE.STOPPED }); expect.any(String), expect.any(String), EXPERIMENT_STATE.STOPPED);
expect(simulation.stopping).toBe(true); expect(simulation.stopping).toBe(true);
// stop a STARTED simulation // stop a STARTED simulation
...@@ -213,7 +213,7 @@ test('should be able to stop an experiment', async () => { ...@@ -213,7 +213,7 @@ test('should be able to stop an experiment', async () => {
await ExperimentExecutionService.instance.stopExperiment(simulation); await ExperimentExecutionService.instance.stopExperiment(simulation);
expect(RunningSimulationService.instance.updateState).toHaveBeenCalledTimes(1); expect(RunningSimulationService.instance.updateState).toHaveBeenCalledTimes(1);
expect(RunningSimulationService.instance.updateState).toHaveBeenCalledWith( expect(RunningSimulationService.instance.updateState).toHaveBeenCalledWith(
expect.any(String), expect.any(String), { state: EXPERIMENT_STATE.STOPPED }); expect.any(String), expect.any(String), EXPERIMENT_STATE.STOPPED);
// stop a PAUSED simulation // stop a PAUSED simulation
RunningSimulationService.instance.updateState.mockClear(); RunningSimulationService.instance.updateState.mockClear();
...@@ -221,7 +221,7 @@ test('should be able to stop an experiment', async () => { ...@@ -221,7 +221,7 @@ test('should be able to stop an experiment', async () => {
await ExperimentExecutionService.instance.stopExperiment(simulation); await ExperimentExecutionService.instance.stopExperiment(simulation);
expect(RunningSimulationService.instance.updateState).toHaveBeenCalledTimes(1); expect(RunningSimulationService.instance.updateState).toHaveBeenCalledTimes(1);
expect(RunningSimulationService.instance.updateState).toHaveBeenCalledWith( expect(RunningSimulationService.instance.updateState).toHaveBeenCalledWith(
expect.any(String), expect.any(String), { state: EXPERIMENT_STATE.STOPPED }); expect.any(String), expect.any(String), EXPERIMENT_STATE.STOPPED);
// stop a HALTED simulation // stop a HALTED simulation
RunningSimulationService.instance.updateState.mockClear(); RunningSimulationService.instance.updateState.mockClear();
...@@ -229,7 +229,7 @@ test('should be able to stop an experiment', async () => { ...@@ -229,7 +229,7 @@ test('should be able to stop an experiment', async () => {
await ExperimentExecutionService.instance.stopExperiment(simulation); await ExperimentExecutionService.instance.stopExperiment(simulation);
expect(RunningSimulationService.instance.updateState).toHaveBeenCalledTimes(1); expect(RunningSimulationService.instance.updateState).toHaveBeenCalledTimes(1);
expect(RunningSimulationService.instance.updateState).toHaveBeenCalledWith( expect(RunningSimulationService.instance.updateState).toHaveBeenCalledWith(
expect.any(String), expect.any(String), { state: EXPERIMENT_STATE.STOPPED }); expect.any(String), expect.any(String), EXPERIMENT_STATE.STOPPED);
// stop a simulation in an undefined state, error // stop a simulation in an undefined state, error
RunningSimulationService.instance.updateState.mockClear(); RunningSimulationService.instance.updateState.mockClear();
......
...@@ -110,8 +110,8 @@ test('register for ROS status information', () => { ...@@ -110,8 +110,8 @@ test('register for ROS status information', () => {
let progressMessageCallback = jest.fn(); let progressMessageCallback = jest.fn();
// we register twice to check that original sub is destroyed and re-created without error // we register twice to check that original sub is destroyed and re-created without error
RunningSimulationService.instance.registerForRosStatusInformation('test-ros-ws-url', progressMessageCallback); RunningSimulationService.instance.addRosStatusInfoCallback('test-ros-ws-url', progressMessageCallback);
RunningSimulationService.instance.registerForRosStatusInformation('test-ros-ws-url', progressMessageCallback); RunningSimulationService.instance.addRosStatusInfoCallback('test-ros-ws-url', progressMessageCallback);
expect(RoslibService.instance.getConnection.mock.calls.length).toBe(2); expect(RoslibService.instance.getConnection.mock.calls.length).toBe(2);
expect(mockStatusListener.removeAllListeners.mock.calls.length).toBe(1); expect(mockStatusListener.removeAllListeners.mock.calls.length).toBe(1);
......
...@@ -61,8 +61,18 @@ class ExperimentExecutionService extends HttpService { ...@@ -61,8 +61,18 @@ class ExperimentExecutionService extends HttpService {
let brainProcesses = launchSingleMode ? 1 : experiment.configuration.brainProcesses; let brainProcesses = launchSingleMode ? 1 : experiment.configuration.brainProcesses;
//TODO: placeholder, register actual progress callback later //TODO: placeholder, register actual progress callback later
let progressCallback = () => { let progressCallback = (msg) => {
DialogService.instance.progressNotification({message:'The experiment is loading'}); if (msg && msg.progress) {
if (msg.progress.done) {
DialogService.instance.progressNotification({message:'The experiment is loading'});
}
else {
DialogService.instance.progressNotification({
message: msg.progress.task,
details: msg.progress.subtask
});
}
}
}; };
let launchOnNextServer = async () => { let launchOnNextServer = async () => {
...@@ -154,7 +164,7 @@ class ExperimentExecutionService extends HttpService { ...@@ -154,7 +164,7 @@ class ExperimentExecutionService extends HttpService {
progressCallback({ main: 'Initialize Simulation...' }); progressCallback({ main: 'Initialize Simulation...' });
// register for messages during initialization // register for messages during initialization
SimulationService.instance.registerForRosStatusInformation( SimulationService.instance.addRosStatusInfoCallback(
serverConfiguration.rosbridge.websocket, serverConfiguration.rosbridge.websocket,
progressCallback progressCallback
); );
...@@ -200,7 +210,7 @@ class ExperimentExecutionService extends HttpService { ...@@ -200,7 +210,7 @@ class ExperimentExecutionService extends HttpService {
return SimulationService.instance.updateState( return SimulationService.instance.updateState(
serverURL, serverURL,
simulationID, simulationID,
{ state: state } state
); );
} }
......
...@@ -8,7 +8,7 @@ import config from '../../../config.json'; ...@@ -8,7 +8,7 @@ import config from '../../../config.json';
let _instance = null; let _instance = null;
const SINGLETON_ENFORCER = Symbol(); const SINGLETON_ENFORCER = Symbol();
let rosStatusListeners = new Map(); let rosStatusTopics = new Map();
const INTERVAL_CHECK_SIMULATION_READY = 1000; const INTERVAL_CHECK_SIMULATION_READY = 1000;
/** /**
...@@ -101,43 +101,54 @@ class SimulationService extends HttpService { ...@@ -101,43 +101,54 @@ class SimulationService extends HttpService {
* @param {string} rosbridgeWebsocket - ROS websocket URL * @param {string} rosbridgeWebsocket - ROS websocket URL
* @param {*} setProgressMessage - callback to be called with new status info * @param {*} setProgressMessage - callback to be called with new status info
*/ */
registerForRosStatusInformation(rosbridgeWebsocket, setProgressMessage) { startRosStatusInformation(rosbridgeWebsocket) {
let destroyCurrentConnection = () => { this.stopRosStatusInformation(rosbridgeWebsocket);
if (rosStatusListeners.has(rosbridgeWebsocket)) {
let statusListener = rosStatusListeners.get(rosbridgeWebsocket);
// remove the progress bar callback only, unsubscribe terminates the rosbridge
// connection for any other subscribers on the status topic
statusListener.removeAllListeners();
rosStatusListeners.delete(rosbridgeWebsocket);
}
};
destroyCurrentConnection();
let rosConnection = RoslibService.instance.getConnection(rosbridgeWebsocket); let rosConnection = RoslibService.instance.getConnection(rosbridgeWebsocket);
let statusListener = RoslibService.instance.createStringTopic( let statusTopic = RoslibService.instance.createStringTopic(
rosConnection, rosConnection,
config['ros-topics'].status config['ros-topics'].status
); );
rosStatusListeners.set(rosbridgeWebsocket, statusListener); rosStatusTopics.set(rosbridgeWebsocket, statusTopic);
statusListener.subscribe((data) => { this.addRosStatusInfoCallback(rosbridgeWebsocket, (msg) => {
let message = JSON.parse(data.data); if (msg.state && msg.state === EXPERIMENT_STATE.STOPPED) {
if (message && message.progress) { this.stopRosStatusInformation(rosbridgeWebsocket);
if (message.progress.done) {
destroyCurrentConnection();
setProgressMessage({ main: 'Simulation initialized.' });
}
else {
setProgressMessage({
main: message.progress.task,
sub: message.progress.subtask
});
}
} }
}); });
}; };
stopRosStatusInformation(rosbridgeWebsocket) {
let statusTopic = rosStatusTopics.get(rosbridgeWebsocket);
if (!statusTopic) {
return;
}
// remove the progress bar callback only, unsubscribe terminates the rosbridge
// connection for any other subscribers on the status topic
statusTopic.unsubscribe(); // fully disconnects rosbridge
statusTopic.removeAllListeners();
rosStatusTopics.delete(rosbridgeWebsocket);
}
startRosCleErrorInfo(rosbridgeWebsocket) {
//TODO
}
addRosStatusInfoCallback(rosbridgeWebsocket, infoCallback) {
if (!rosStatusTopics.has(rosbridgeWebsocket)) {
this.startRosStatusInformation(rosbridgeWebsocket);
}
let statusTopic = rosStatusTopics.get(rosbridgeWebsocket);
statusTopic.subscribe((data) => {
let message = JSON.parse(data.data);
if (message) {
infoCallback(message);
}
});
}
/** /**
* Get the state the simulation is currently in. * Get the state the simulation is currently in.
* @param {string} serverURL URL of the server the simulation is running on * @param {string} serverURL URL of the server the simulation is running on
...@@ -164,13 +175,30 @@ class SimulationService extends HttpService { ...@@ -164,13 +175,30 @@ class SimulationService extends HttpService {
async updateState(serverURL, simulationID, state) { async updateState(serverURL, simulationID, state) {
let url = serverURL + '/simulation/' + simulationID + '/state'; let url = serverURL + '/simulation/' + simulationID + '/state';
try { try {
let response = await this.httpRequestPUT(url, JSON.stringify(state)); let response = await this.httpRequestPUT(url, JSON.stringify({ state: state }));
return response; return response;
} }
catch (error) { catch (error) {
DialogService.instance.simulationError(error); DialogService.instance.simulationError(error);
} }
} }
/**
* Get simulation information.
* @param {string} serverURL The full URL of the server the simulation is running on
* @param {string} simulationID The simulation ID
* @returns The simulation information
*/
async getInfo(serverURL, simulationID) {
let url = serverURL + '/simulation/' + simulationID;
try {
let response = await (await this.httpRequestGET(url)).json();
return response;
}
catch (error) {
DialogService.instance.networkError(error);
}
}
} }
export default SimulationService; export default SimulationService;
...@@ -2,7 +2,7 @@ import { HttpService } from '../http-service.js'; ...@@ -2,7 +2,7 @@ import { HttpService } from '../http-service.js';
import endpoints from '../proxy/data/endpoints.json'; import endpoints from '../proxy/data/endpoints.json';
import config from '../../config.json'; import config from '../../config.json';
import ErrorHandlerService from '../error-handler-service'; import DialogService from '../dialog-service';
const storageModelsURL = `${config.api.proxy.url}${endpoints.proxy.models.url}`; const storageModelsURL = `${config.api.proxy.url}${endpoints.proxy.models.url}`;
const allCustomModelsURL = `${config.api.proxy.url}${endpoints.proxy.storage.allCustomModels.url}`; const allCustomModelsURL = `${config.api.proxy.url}${endpoints.proxy.storage.allCustomModels.url}`;
...@@ -48,7 +48,7 @@ class ModelsStorageService extends HttpService { ...@@ -48,7 +48,7 @@ class ModelsStorageService extends HttpService {
this.verifyModelType(modelType); this.verifyModelType(modelType);
} }
catch (error) { catch (error) {
ErrorHandlerService.instance.dataError(error); DialogService.instance.dataError(error);
} }
try { try {
...@@ -58,7 +58,7 @@ class ModelsStorageService extends HttpService { ...@@ -58,7 +58,7 @@ class ModelsStorageService extends HttpService {
this.models = await (await this.httpRequestGET(modelsWithTypeURL)).json(); this.models = await (await this.httpRequestGET(modelsWithTypeURL)).json();
} }
catch (error) { catch (error) {
ErrorHandlerService.instance.networkError(error); DialogService.instance.networkError(error);
} }
} }
...@@ -80,7 +80,7 @@ class ModelsStorageService extends HttpService { ...@@ -80,7 +80,7 @@ class ModelsStorageService extends HttpService {
return (await this.httpRequestGET(customModelsURL)).json(); return (await this.httpRequestGET(customModelsURL)).json();
} }
catch (error) { catch (error) {
ErrorHandlerService.instance.networkError(error); DialogService.instance.networkError(error);
} }
} }
...@@ -115,7 +115,7 @@ class ModelsStorageService extends HttpService { ...@@ -115,7 +115,7 @@ class ModelsStorageService extends HttpService {
return (await this.httpRequestDELETE(deleteCustomModelURL)).json(); return (await this.httpRequestDELETE(deleteCustomModelURL)).json();
} }
catch (error) { catch (error) {
ErrorHandlerService.instance.dataError(error); DialogService.instance.dataError(error);
} }
} }
...@@ -136,7 +136,7 @@ class ModelsStorageService extends HttpService { ...@@ -136,7 +136,7 @@ class ModelsStorageService extends HttpService {
return (await this.httpRequestPOST(setCustomModelURL, fileContent)).json(); return (await this.httpRequestPOST(setCustomModelURL, fileContent)).json();
} }
catch (error) { catch (error) {
ErrorHandlerService.instance.networkError(error); DialogService.instance.networkError(error);
} }
} }
} }
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment