diff --git a/src/common/api/websocket-api.js b/src/common/api/websocket-api.js index 70e741f..210181a 100644 --- a/src/common/api/websocket-api.js +++ b/src/common/api/websocket-api.js @@ -20,46 +20,82 @@ const OFFSET_VERSION = 4; class WebSocketManager { constructor() { - this.socket = null; + this.sockets = []; // Array to store multiple socket objects } - id = null; - - connect(url, onMessage, onOpen, onClose) { - if (this.socket && this.socket.readyState === WebSocket.OPEN) { + connect(id, url, onMessage, onOpen, onClose) { + const existingSocket = this.sockets.find(s => s.id === id); + if (existingSocket && existingSocket.socket.readyState === WebSocket.OPEN) { console.log('Already connected to:', url); return; } - if (this.socket) { - this.socket.close(); - } + const socket = new WebSocket(url, 'live'); + socket.binaryType = 'arraybuffer'; - this.socket = new WebSocket(url, 'live'); - this.socket.binaryType = 'arraybuffer'; - - this.socket.onopen = onOpen; - this.socket.onmessage = (event) => { + socket.onopen = onOpen; + socket.onmessage = (event) => { const msgs = this.bufferToMessageArray(event.data); - onMessage(msgs); + onMessage(msgs, id); }; - this.socket.onclose = onClose; + socket.onclose = onClose; + + // Store the new socket along with its id + this.sockets.push({ id, socket }); } - disconnect() { - if (this.socket) { - this.socket.close(); - this.socket = null; - console.log('WebSocket connection closed'); + disconnect(id) { + const socket = this.sockets.find(s => s.id === id); + if (socket) { + socket.socket.close(); + this.sockets = this.sockets.filter(s => s.id !== id); + console.log('WebSocket connection closed for id:', id); } } + send(id, message) { + const socket = this.sockets.find(s => s.id === id); + if (socket == null) { + return false; + } + console.log("Sending to IC", id, "message: ", message); + const data = this.messageToBuffer(message); + socket.socket.send(data); + + return true; + } + + messageToBuffer(message) { + const buffer = new ArrayBuffer(16 + 4 * message.length); + const view = new DataView(buffer); + + let bits = 0; + bits |= (message.version & 0xF) << OFFSET_VERSION; + bits |= (message.type & 0x3) << OFFSET_TYPE; + + let source_index = 0; + source_index |= (message.source_index & 0xFF); + + const sec = Math.floor(message.timestamp / 1e3); + const nsec = (message.timestamp - sec * 1e3) * 1e6; + + view.setUint8(0x00, bits, true); + view.setUint8(0x01, source_index, true); + view.setUint16(0x02, message.length, true); + view.setUint32(0x04, message.sequence, true); + view.setUint32(0x08, sec, true); + view.setUint32(0x0C, nsec, true); + + const data = new Float32Array(buffer, 0x10, message.length); + data.set(message.values); + + return buffer; + } + bufferToMessageArray(blob) { - /* some local variables for parsing */ let offset = 0; const msgs = []; - /* for every msg in vector */ while (offset < blob.byteLength) { const msg = this.bufferToMessage(new DataView(blob, offset)); @@ -70,31 +106,39 @@ class WebSocketManager { } return msgs; -} + } bufferToMessage(data) { - // parse incoming message into usable data if (data.byteLength === 0) { - return null; - } - - const source_index = data.getUint8(1); - const bits = data.getUint8(0); - const length = data.getUint16(0x02, 1); - const bytes = length * 4 + 16; - - return { - version: (bits >> OFFSET_VERSION) & 0xF, - type: (bits >> OFFSET_TYPE) & 0x3, - source_index: source_index, - length: length, - sequence: data.getUint32(0x04, 1), - timestamp: data.getUint32(0x08, 1) * 1e3 + data.getUint32(0x0C, 1) * 1e-6, - values: new Float32Array(data.buffer, data.byteOffset + 0x10, length), - blob: new DataView(data.buffer, data.byteOffset + 0x00, bytes), - // id: id - }; -} + return null; + } + + const source_index = data.getUint8(1); + const bits = data.getUint8(0); + const length = data.getUint16(0x02, 1); + const bytes = length * 4 + 16; + + return { + version: (bits >> OFFSET_VERSION) & 0xF, + type: (bits >> OFFSET_TYPE) & 0x3, + source_index: source_index, + length: length, + sequence: data.getUint32(0x04, 1), + timestamp: data.getUint32(0x08, 1) * 1e3 + data.getUint32(0x0C, 1) * 1e-6, + values: new Float32Array(data.buffer, data.byteOffset + 0x10, length), + blob: new DataView(data.buffer, data.byteOffset + 0x00, bytes), + }; + } + + getSocketById(id) { + const socketEntry = this.sockets.find(s => s.id === id); + return socketEntry ? socketEntry.socket : null; + } + + isConnected(id) { + const socket = this.getSocketById(id); + return socket && socket.readyState === WebSocket.OPEN; + } } export const wsManager = new WebSocketManager(); diff --git a/src/pages/dashboards/dashboard.js b/src/pages/dashboards/dashboard.js index ce3fcb9..1cb7efe 100644 --- a/src/pages/dashboards/dashboard.js +++ b/src/pages/dashboards/dashboard.js @@ -1,4 +1,21 @@ -import React, { useState, useEffect, useCallback, useRef } from 'react'; +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React, { useState, useEffect, useCallback, useRef, act } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import Fullscreenable from 'react-fullscreenable'; @@ -71,24 +88,46 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { const [grid, setGrid] = useState(50); const [newHeightValue, setNewHeightValue] = useState(0); + //ics that are included in configurations + const [activeICS, setActiveICS] = useState([]); useEffect(() => { - const wsUrl = 'wss://villas.k8s.eonerc.rwth-aachen.de/ws/ws_sig'; - dispatch(connectWebSocket({ url: wsUrl, id: 547627 })); + let usedICS = []; + for(const config of configs){ + usedICS.push(config.icID); + } + setActiveICS(ics.filter((i) => usedICS.includes(i.id))); + }, [configs]) + + const activeSocketURLs = useSelector((state) => state.websocket.activeSocketURLs); + + //connect to websockets + useEffect(() => { + activeICS.forEach((i) => { + if(i.websocketurl){ + if(!activeSocketURLs.includes(i.websocketurl)) + dispatch(connectWebSocket({ url: i.websocketurl, id: i.id })); + } + }) return () => { - dispatch(disconnect()); + activeICS.forEach((i) => { + dispatch(disconnect({ id: i.id })); + }); }; - }, [dispatch]); + + }, [activeICS]); + //as soon as dashboard is loaded, load widgets, configs, signals and files for this dashboard useEffect(() => { if (dashboard.id) { - //as soon as dashboard is loaded, load widgets, configs, signals and files for this dashboard fetchWidgets(dashboard.id); fetchWidgetData(dashboard.scenarioID); setHeight(dashboard.height); setGrid(dashboard.grid); + + console.log('widgets', widgets); } }, [dashboard]); @@ -471,8 +510,9 @@ const Dashboard = ({ isFullscreen, toggleFullscreen }) => { configs={configs} signals={signals} paused={paused} - ics={ics} - icData={[]} + ics={activeICS} + scenarioID={dashboard.scenarioID} + onSimulationStarted={() => onSimulationStarted()} /> diff --git a/src/pages/dashboards/widget/widget.js b/src/pages/dashboards/widget/widget.js index 8dfbe8c..6f86f54 100644 --- a/src/pages/dashboards/widget/widget.js +++ b/src/pages/dashboards/widget/widget.js @@ -17,7 +17,7 @@ import React from 'react'; import { useState, useEffect } from 'react'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import WidgetLabel from './widgets/label'; import WidgetLine from './widgets/line'; import WidgetBox from './widgets/box'; @@ -31,23 +31,40 @@ import WidgetTimeOffset from './widgets/time-offset'; import WidgetICstatus from './widgets/icstatus'; // import WidgetCustomAction from './widgets/custom-action'; // import WidgetAction from './widgets/action'; -// import WidgetButton from './widgets/button'; -// import WidgetInput from './widgets/input'; -// import WidgetSlider from './widgets/slider'; +import WidgetButton from './widgets/button'; +import WidgetInput from './widgets/input'; +import WidgetSlider from './widgets/slider'; // import WidgetTopology from './widgets/topology'; -// import WidgetPlayer from './widgets/player'; +import WidgetPlayer from './widgets/player'; //import WidgetHTML from './widgets/html'; import '../../../styles/widgets.css'; import { useGetICSQuery, useGetSignalsQuery, useGetConfigsQuery } from '../../../store/apiSlice'; +import { sessionToken } from '../../../localStorage'; +import { useUpdateWidgetMutation } from '../../../store/apiSlice'; +import { sendMessageToWebSocket } from '../../../store/websocketSlice'; +import { useGetResultsQuery } from '../../../store/apiSlice'; -const Widget = ({widget, editing, files, configs, signals, paused, ics, icData}) => { - +const Widget = ({widget, editing, files, configs, signals, paused, ics, scenarioID, onSimulationStarted}) => { + const dispatch = useDispatch(); const { token: sessionToken } = useSelector((state) => state.auth); - + const {data, refetch: refetchResults } = useGetResultsQuery(scenarioID); + const results = data ? data.results : []; const [icIDs, setICIDs] = useState([]); const icdata = useSelector((state) => state.websocket.icdata); + const [websockets, setWebsockets] = useState([]); + const activeSocketURLs = useSelector((state) => state.websocket.activeSocketURLs); + const [update] = useUpdateWidgetMutation(); + + useEffect(() => { + if(activeSocketURLs.length > 0){ + activeSocketURLs.forEach(url => { + setWebsockets(prevState=>([...prevState, { url: url.replace(/^wss:\/\//, "https://"), connected:true}])) + }) + } + }, [activeSocketURLs]) + useEffect(() => { if(signals.length > 0){ let ids = []; @@ -66,387 +83,110 @@ const Widget = ({widget, editing, files, configs, signals, paused, ics, icData}) } }, [signals]) - // const {data: signals, isLoading: signalsLoading} = useGetSignalsQuery({}) + const inputDataChanged = (widget, data, controlID, controlValue, isFinalChange) => { + if (controlID !== '' && isFinalChange) { + let updatedWidget = JSON.parse(JSON.stringify(widget)); + updatedWidget.customProperties[controlID] = controlValue; + updateWidget(updatedWidget); + } - switch(widget.type){ - //Cosmetic widgets - case 'Line': - return - case 'Box': - return - case 'Label': - return ; - case 'Image': - return - //Displaying widgets - case 'Plot': - return - case 'Table': - return - case 'Value': - return - case 'Lamp': - return - case 'Gauge': - return - case 'TimeOffset': - return - case 'ICstatus': - return - //Manipulation widgets - default: - return
Error: Widget not found!
+ let signalID = widget.signalIDs[0]; + let signal = signals.filter(s => s.id === signalID) + if (signal.length === 0){ + console.warn("Unable to send signal for signal ID", signalID, ". Signal not found."); + return; + } + // determine ID of infrastructure component related to signal[0] + // Remark: there is only one selected signal for an input type widget + let icID = icIDs[signal[0].id]; + dispatch(sendMessageToWebSocket({message: {ic: icID, signalID: signal[0].id, signalIndex: signal[0].index, data: signal[0].scalingFactor * data}})); } - - // if (widget.type === 'CustomAction') { - // return - // } else if (widget.type === 'Action') { - // return - // } else if (widget.type === 'Lamp') { - // return - // } else if (widget.type === 'Value') { - // return - // } else if (widget.type === 'Plot') { - // return - // } else if (widget.type === 'Table') { - // return - // } else if (widget.type === 'Label') { - // return - // } else if (widget.type === 'Image') { - // return - // } else if (widget.type === 'Button') { - // return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} - // signals={this.state.signals} - // token={this.state.sessionToken} - // /> - // } else if (widget.type === 'NumberInput') { - // return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} - // signals={this.state.signals} - // token={this.state.sessionToken} - // /> - // } else if (widget.type === 'Slider') { - // return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} - // signals={this.state.signals} - // token={this.state.sessionToken} - // /> - // } else if (widget.type === 'Gauge') { - // return - // //} else if (widget.type === 'HTML') { - // //return - // } else if (widget.type === 'Topology') { - // return - // } else if (widget.type === 'TimeOffset') { - // return - // } else if (widget.type === 'ICstatus') { - // return - // } else if (widget.type === 'Player') { - // return - // } + const updateWidget = async (updatedWidget) => { + try { + await update({ widgetID: widget.id, updatedWidget: { widget: updatedWidget } }).unwrap(); + } catch (err) { + console.log('error', err); + } + } - return null; + + if (widget.type === 'Line') { + return ; + } else if (widget.type === 'Box') { + return ; + } else if (widget.type === 'Label') { + return ; + } else if (widget.type === 'Image') { + return ; + } else if (widget.type === 'Plot') { + return ; + } else if (widget.type === 'Table') { + return ; + } else if (widget.type === 'Value') { + return ; + } else if (widget.type === 'Lamp') { + return ; + } else if (widget.type === 'Gauge') { + return ; + } else if (widget.type === 'TimeOffset') { + return ; + } else if (widget.type === 'ICstatus') { + return ; + } else if (widget.type === 'Button') { + return ( + + inputDataChanged(widget, value, controlID, controlValue, isFinalChange) + } + signals={signals} + token={sessionToken} + /> + ); + } else if (widget.type === 'NumberInput') { + return ( + + inputDataChanged(widget, value, controlID, controlValue, isFinalChange) + } + signals={signals} + token={sessionToken} + /> + ); + } else if (widget.type === 'Slider') { + return ( + + inputDataChanged(widget, value, controlID, controlValue, isFinalChange) + } + signals={signals} + token={sessionToken} + /> + ); + } else if (widget.type === 'Player') { + return ( + + ); + } else { + console.log('Unknown widget type', widget.type); + return
Error: Widget not found!
; + } } -// class Widget extends React.Component { -// static getStores() { -// return [ ICDataStore, ConfigsStore, FileStore, SignalStore, WebsocketStore, ResultStore]; -// } - -// static calculateState(prevState, props) { - -// let websockets = WebsocketStore.getState(); - -// let icData = {}; - -// if (props.paused) { -// if (prevState && prevState.icData) { -// icData = JSON.parse(JSON.stringify(prevState.icData)); -// } -// } else { -// icData = ICDataStore.getState(); -// } - -// // Get the IC IDs and signal indexes for all signals of the widget -// let configs = ConfigsStore.getState().filter(c => c.scenarioID === parseInt(props.scenarioID, 10)); -// // TODO make sure that the signals are only the signals that belong to the scenario at hand -// let signals = SignalStore.getState(); -// let icIDs = []; - -// for (let id of props.data.signalIDs){ -// let signal = signals.find(s => s.id === id); -// if (signal !== undefined) { -// let config = configs.find(m => m.id === signal.configID); -// if (config !== undefined){ -// icIDs[signal.id] = config.icID; -// } -// } -// } - -// let results = ResultStore.getState().filter(r => r.scenarioID === parseInt(props.scenarioID, 10)); -// let files = FileStore.getState().filter(f => f.scenarioID === parseInt(props.scenarioID, 10)); - -// return { -// websockets: websockets, -// icData: icData, -// signals: signals, -// icIDs: icIDs, -// files: files, -// sessionToken: localStorage.getItem("token"), -// results: results, -// }; -// } - -// inputDataChanged(widget, data, controlID, controlValue, isFinalChange) { -// // controlID is the path to the widget customProperty that is changed (for example 'value') - -// // modify the widget customProperty -// if (controlID !== '' && isFinalChange) { -// let updatedWidget = JSON.parse(JSON.stringify(widget)); -// updatedWidget.customProperties[controlID] = controlValue; - -// AppDispatcher.dispatch({ -// type: 'widgets/start-edit', -// token: this.state.sessionToken, -// data: updatedWidget -// }); -// } - -// // The following assumes that a widget modifies/ uses exactly one signal - -// // get the signal with the selected signal ID -// let signalID = widget.signalIDs[0]; -// let signal = this.state.signals.filter(s => s.id === signalID) -// if (signal.length === 0){ -// console.warn("Unable to send signal for signal ID", signalID, ". Signal not found."); -// return; -// } -// // determine ID of infrastructure component related to signal[0] -// // Remark: there is only one selected signal for an input type widget -// let icID = this.state.icIDs[signal[0].id]; -// AppDispatcher.dispatch({ -// type: 'icData/inputChanged', -// ic: icID, -// signalID: signal[0].id, -// signalIndex: signal[0].index, -// data: signal[0].scalingFactor * data -// }); -// } - -// createWidget(widget) { - -// if (widget.type === 'CustomAction') { -// return -// } else if (widget.type === 'Action') { -// return -// } else if (widget.type === 'Lamp') { -// return -// } else if (widget.type === 'Value') { -// return -// } else if (widget.type === 'Plot') { -// return -// } else if (widget.type === 'Table') { -// return -// } else if (widget.type === 'Label') { -// return -// } else if (widget.type === 'Image') { -// return -// } else if (widget.type === 'Button') { -// return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} -// signals={this.state.signals} -// token={this.state.sessionToken} -// /> -// } else if (widget.type === 'NumberInput') { -// return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} -// signals={this.state.signals} -// token={this.state.sessionToken} -// /> -// } else if (widget.type === 'Slider') { -// return this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)} -// signals={this.state.signals} -// token={this.state.sessionToken} -// /> -// } else if (widget.type === 'Gauge') { -// return -// } else if (widget.type === 'Box') { -// return -// //} else if (widget.type === 'HTML') { -// //return -// } else if (widget.type === 'Topology') { -// return -// } else if (widget.type === 'Line') { -// return -// } else if (widget.type === 'TimeOffset') { -// return -// } else if (widget.type === 'ICstatus') { -// return -// } else if (widget.type === 'Player') { -// return -// } - -// return null; -// } - -// render() { -// return this.createWidget(this.props.data); -// } -// } - -// let fluxContainerConverter = require('../common/FluxContainerConverter'); -// export default Container.create(fluxContainerConverter.convert(Widget), { withProps: true }); - export default Widget; diff --git a/src/pages/dashboards/widget/widgets/button.jsx b/src/pages/dashboards/widget/widgets/button.jsx index 7dced13..2c285f4 100644 --- a/src/pages/dashboards/widget/widgets/button.jsx +++ b/src/pages/dashboards/widget/widgets/button.jsx @@ -17,7 +17,6 @@ import React, { useState, useEffect } from 'react'; import { Button } from 'react-bootstrap'; -import AppDispatcher from '../../common/app-dispatcher'; const WidgetButton = (props) => { const [pressed, setPressed] = useState(props.widget.customProperties.pressed); @@ -27,11 +26,11 @@ const WidgetButton = (props) => { widget.customProperties.simStartedSendValue = false; widget.customProperties.pressed = false; - AppDispatcher.dispatch({ - type: 'widgets/start-edit', - token: props.token, - data: widget - }); + // AppDispatcher.dispatch({ + // type: 'widgets/start-edit', + // token: props.token, + // data: widget + // }); // Effect cleanup return () => { diff --git a/src/pages/dashboards/widget/widgets/image.jsx b/src/pages/dashboards/widget/widgets/image.jsx index b5964f6..a9a6ee4 100644 --- a/src/pages/dashboards/widget/widgets/image.jsx +++ b/src/pages/dashboards/widget/widgets/image.jsx @@ -16,20 +16,36 @@ ******************************************************************************/ import React, { useState, useEffect } from "react"; +import { useLazyDownloadImageQuery } from "../../../../store/apiSlice"; +import FileSaver from "file-saver"; const WidgetImage = (props) => { const [file, setFile] = useState(null); + const [objectURL, setObjectURL] = useState(""); const widget = JSON.parse(JSON.stringify(props.widget)); + const [triggerDownloadImage] = useLazyDownloadImageQuery(); + + const handleDownloadFile = async (fileID) => { + try { + const res = await triggerDownloadImage(fileID); + const blob = await res.data; // This is where you get the blob directly + setObjectURL(URL.createObjectURL(blob)) + } catch (error) { + console.error(`Failed to download file with ID ${fileID}`, error); + } + } + + useEffect(() => { + if(file !== null){ + handleDownloadFile(file.id); + } + }, [file]) + useEffect(() => { let widgetFile = widget.customProperties.file; if (widgetFile !== -1 && file === null) { - // AppDispatcher.dispatch({ - // type: "files/start-download", - // data: widgetFile, - // token: props.token, - // }); } }, [file, props.token, widget.customProperties.file]); @@ -38,17 +54,11 @@ const WidgetImage = (props) => { widget.customProperties.update = false; if (file !== null) setFile(null); } else { - console.log("looking in", props.files) let foundFile = props.files.find( (f) => f.id === parseInt(widget.customProperties.file, 10) ); if (foundFile && widget.customProperties.update) { widget.customProperties.update = false; - // AppDispatcher.dispatch({ - // type: "files/start-download", - // data: foundFile.id, - // token: props.token, - // }); setFile(foundFile); } } @@ -58,7 +68,14 @@ const WidgetImage = (props) => { console.error("Image error:", e); }; - let objectURL = file && file.objectURL ? file.objectURL : ""; + //revoke object url when component unmounts + useEffect(() => { + return () => { + if (objectURL) { + URL.revokeObjectURL(objectURL); + } + }; +}, [objectURL]); return (
diff --git a/src/pages/dashboards/widget/widgets/input.jsx b/src/pages/dashboards/widget/widgets/input.jsx index 732c9f9..938e2d7 100644 --- a/src/pages/dashboards/widget/widgets/input.jsx +++ b/src/pages/dashboards/widget/widgets/input.jsx @@ -16,7 +16,6 @@ ******************************************************************************/ import React, { useState, useEffect } from "react"; import { Form, Col, InputGroup } from "react-bootstrap"; -import AppDispatcher from "../../common/app-dispatcher"; function WidgetInput(props) { const [value, setValue] = useState(""); @@ -26,11 +25,11 @@ function WidgetInput(props) { const widget = { ...props.widget }; widget.customProperties.simStartedSendValue = false; - AppDispatcher.dispatch({ - type: "widgets/start-edit", - token: props.token, - data: widget, - }); + // AppDispatcher.dispatch({ + // type: "widgets/start-edit", + // token: props.token, + // data: widget, + // }); }, [props.token, props.widget]); useEffect(() => { diff --git a/src/pages/dashboards/widget/widgets/player.js b/src/pages/dashboards/widget/widgets/player.js index 1cee2b4..100bf1a 100644 --- a/src/pages/dashboards/widget/widgets/player.js +++ b/src/pages/dashboards/widget/widgets/player.js @@ -18,12 +18,10 @@ import React, { Component } from 'react'; import { Container, Row, Col } from 'react-bootstrap'; import JSZip from 'jszip'; -import IconButton from '../../common/buttons/icon-button'; -import IconTextButton from '../../common/buttons/icon-text-button'; -import ParametersEditor from '../../common/parameters-editor'; -import ICAction from '../../ic/ic-action'; -import ResultPythonDialog from "../../pages/scenarios/dialogs/result-python-dialog"; -import AppDispatcher from "../../common/app-dispatcher"; +import IconButton from '../../../../common/buttons/icon-button'; +import IconTextButton from '../../../../common/buttons/icon-text-button'; +import ParametersEditor from '../../../../common/parameters-editor'; +import ResultPythonDialog from '../../../scenarios/dialogs/result-python-dialog'; import { playerMachine } from '../widget-player/player-machine'; import { interpret } from 'xstate'; @@ -33,7 +31,6 @@ function transitionState(currentState, playerEvent) { return playerMachine.transition(currentState, { type: playerEvent }) } - class WidgetPlayer extends Component { constructor(props) { super(props); @@ -131,17 +128,17 @@ class WidgetPlayer extends Component { switch (state.ic.state) { case 'stopping': // if configured, show results if (state.uploadResults) { - AppDispatcher.dispatch({ - type: 'results/start-load', - param: '?scenarioID=' + props.scenarioID, - token: state.sessionToken, - }) + // AppDispatcher.dispatch({ + // type: 'results/start-load', + // param: '?scenarioID=' + props.scenarioID, + // token: state.sessionToken, + // }) - AppDispatcher.dispatch({ - type: 'files/start-load', - token: state.sessionToken, - param: '?scenarioID=' + props.scenarioID, - }); + // AppDispatcher.dispatch({ + // type: 'files/start-load', + // token: state.sessionToken, + // param: '?scenarioID=' + props.scenarioID, + // }); } newState = transitionState(state.playerState, 'FINISH') return { playerState: newState, icState: state.ic.state } @@ -163,14 +160,14 @@ class WidgetPlayer extends Component { clickStart() { let config = this.state.config config.startParameters = this.state.startParameters - ICAction.start([config], '{}', [this.state.ic], new Date(), this.state.sessionToken, this.state.uploadResults) + // dispatch(sendActionToIC({token: sessionToken, id: id, actions: newAction})); let newState = transitionState(this.state.playerState, 'START') this.setState({ playerState: newState }) } clickReset() { - ICAction.reset(this.state.ic.id, new Date(), this.state.sessionToken) + //dispatch(sendActionToIC({token: sessionToken, id: id, actions: newAction})); } openPythonDialog() { @@ -197,11 +194,11 @@ class WidgetPlayer extends Component { } toDownload.forEach(fileid => { - AppDispatcher.dispatch({ - type: 'files/start-download', - data: fileid, - token: this.state.sessionToken - }); + // AppDispatcher.dispatch({ + // type: 'files/start-download', + // data: fileid, + // token: this.state.sessionToken + // }); }); this.setState({ filesToDownload: toDownload }); diff --git a/src/pages/dashboards/widget/widgets/slider.jsx b/src/pages/dashboards/widget/widgets/slider.jsx index 7e2a40b..7c6a262 100644 --- a/src/pages/dashboards/widget/widgets/slider.jsx +++ b/src/pages/dashboards/widget/widgets/slider.jsx @@ -40,11 +40,11 @@ const WidgetSlider = (props) => { if (props.widget.customProperties.simStartedSendValue) { let widget = { ...props.widget }; widget.customProperties.simStartedSendValue = false; - AppDispatcher.dispatch({ - type: "widgets/start-edit", - token: props.token, - data: widget, - }); + // AppDispatcher.dispatch({ + // type: "widgets/start-edit", + // token: props.token, + // data: widget, + // }); // Send value without changing widget props.onInputChanged(widget.customProperties.value, "", "", false); diff --git a/src/store/apiSlice.js b/src/store/apiSlice.js index 9d8608b..3080f15 100644 --- a/src/store/apiSlice.js +++ b/src/store/apiSlice.js @@ -84,5 +84,7 @@ export const { useAuthenticateUserMutation, useLazyGetFilesQuery, useUpdateSignalMutation, - useGetIcDataQuery + useGetIcDataQuery, + useLazyDownloadImageQuery, + useUpdateComponentConfigMutation } = apiSlice; diff --git a/src/store/endpoints/file-endpoints.js b/src/store/endpoints/file-endpoints.js index 7810b19..765c03f 100644 --- a/src/store/endpoints/file-endpoints.js +++ b/src/store/endpoints/file-endpoints.js @@ -40,6 +40,13 @@ export const fileEndpoints = (builder) => ({ responseType: 'blob', }), }), + downloadImage: builder.query({ + query: (fileID) => ({ + url: `files/${fileID}`, + method: 'GET', + responseHandler: (response) => response.blob(), + }), + }), updateFile: builder.mutation({ query: ({ fileID, file }) => { const formData = new FormData(); diff --git a/src/store/websocketSlice.js b/src/store/websocketSlice.js index d147902..240ddcb 100644 --- a/src/store/websocketSlice.js +++ b/src/store/websocketSlice.js @@ -1,14 +1,39 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { wsManager } from "../common/api/websocket-api"; import { current } from "@reduxjs/toolkit"; export const connectWebSocket = createAsyncThunk( 'websocket/connect', - async ({ url, id, length }, { dispatch }) => { + async ({ url, id, length }, { dispatch, getState }) => { + + console.log('Want to connect to', url); + + //check if we are already connected to this socket + if(getState().websocket.activeSocketURLs.includes(url)) return; + return new Promise((resolve, reject) => { + dispatch(addActiveSocket({parameters: {id: id, url: url, length: length}})); wsManager.connect( + id, url, - (msgs) => { + (msgs, id) => { const icdata = { input: { sequence: -1, @@ -63,14 +88,13 @@ export const connectWebSocket = createAsyncThunk( icdata.output.sequence = smp.sequence; } } - + // Dispatch the action to update the Redux state dispatch(updateIcData({ id, newIcData: icdata })); } }, () => { console.log('WebSocket connected to:', url); - dispatch(setConnectedUrl({ url })); // Optional: Track the connected URL resolve(); // Resolve the promise on successful connection }, () => { @@ -86,21 +110,30 @@ export const connectWebSocket = createAsyncThunk( const websocketSlice = createSlice({ name: 'websocket', initialState: { - connectedUrl: null, icdata: {}, + activeSocketURLs: [] }, reducers: { - setConnectedUrl: (state, action) => { - state.connectedUrl = action.payload.url; + addActiveSocket: (state, action) => { + const {url, id, length} = action.payload.parameters; + const currentSockets = current(state.activeSocketURLs); + state.activeSocketURLs = [...currentSockets, url]; + state.icdata[id] = {input: { + sequence: -1, + length: length, + version: 2, + type: 0, + timestamp: Date.now(), + values: new Array(length).fill(0) + }, output: {}}; }, - disconnect: (state) => { - wsManager.disconnect(); // Ensure the WebSocket is disconnected - state.connectedUrl = null; + disconnect: (state, action) => { + wsManager.disconnect(action.payload.id); // Ensure the WebSocket is disconnected }, updateIcData: (state, action) => { const { id, newIcData } = action.payload; const currentICdata = current(state.icdata); - if(currentICdata[id]){ + if(currentICdata[id].output.values){ const {values, ...rest} = newIcData.output; let oldValues = [...currentICdata[id].output.values]; for(let i = 0; i < newIcData.output.values.length; i++){ @@ -119,16 +152,36 @@ const websocketSlice = createSlice({ }; } }, + sendMessageToWebSocket: (state, action) => { + const { ic, signalID, signalIndex, data} = action.payload.message; + const currentICdata = current(state.icdata); + + if (!(ic == null || currentICdata[ic].input == null)) { + const inputAction = JSON.parse(JSON.stringify(currentICdata[ic].input)); + // update message properties + inputAction.timestamp = Date.now(); + inputAction.sequence++; + inputAction.values[signalIndex] = data; + inputAction.length = inputAction.values.length; + inputAction.source_index = signalID; + // The previous line sets the source_index field of the message to the ID of the signal + // so that upon loopback through VILLASrelay the value can be mapped to correct signal + + state.icdata[ic].input = inputAction; + let input = JSON.parse(JSON.stringify(inputAction)); + wsManager.send(ic, input); + } + } }, extraReducers: (builder) => { builder.addCase(connectWebSocket.fulfilled, (state, action) => { // Handle the fulfilled state if needed }); builder.addCase(connectWebSocket.rejected, (state, action) => { - // Handle the rejected state if needed + console.log('error', action); }); }, }); -export const { setConnectedUrl, disconnect, updateIcData } = websocketSlice.actions; +export const { disconnect, updateIcData, addActiveSocket, sendMessageToWebSocket } = websocketSlice.actions; export default websocketSlice.reducer;