1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/web/ synced 2025-03-09 00:00:01 +01:00

implemented websockets and finished dashboard migration

Signed-off-by: Andrii Podriez <andrey5577990@gmail.com>
This commit is contained in:
Andrii Podriez 2024-08-30 16:38:16 +02:00 committed by al3xa23
parent ab81e45ce4
commit 6d2cabb268
11 changed files with 401 additions and 503 deletions

View file

@ -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();

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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()}
/>
</WidgetContainer>
</div>

View file

@ -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 <WidgetLine widget={widget} editing={editing} />
case 'Box':
return <WidgetBox widget={widget} editing={editing} />
case 'Label':
return <WidgetLabel widget={widget}/>;
case 'Image':
return <WidgetImage widget={widget} files={files} token={sessionToken} />
//Displaying widgets
case 'Plot':
return <WidgetPlot widget={widget} data={icdata} signals={signals} icIDs={icIDs} paused={paused} />
case 'Table':
return <WidgetTable widget={widget} data={icdata} signals={signals} icIDs={icIDs} />
case 'Value':
return <WidgetValue widget={widget} data={icdata} signals={signals} icIDs={icIDs} />
case 'Lamp':
return <WidgetLamp widget={widget} data={icdata} signals={signals} icIDs={icIDs} />
case 'Gauge':
return <WidgetGauge widget={widget} data={icdata} signals={signals} icIDs={icIDs} editing={editing} />
case 'TimeOffset':
return <WidgetTimeOffset widget={widget} data={icdata} signals={signals} icIDs={icIDs} editing={editing} />
case 'ICstatus':
return <WidgetICstatus widget={widget} ics={ics} />
//Manipulation widgets
default:
return <div>Error: Widget not found!</div>
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 <WidgetCustomAction
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Action') {
// return <WidgetAction
// widget={widget}
// data={this.state.icData}
// />
// } else if (widget.type === 'Lamp') {
// return <WidgetLamp
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Value') {
// return <WidgetValue
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Plot') {
// return <WidgetPlot
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// paused={this.props.paused}
// />
// } else if (widget.type === 'Table') {
// return <WidgetTable
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Label') {
// return <WidgetLabel
// widget={widget}
// />
// } else if (widget.type === 'Image') {
// return <WidgetImage
// widget={widget}
// files={this.state.files}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Button') {
// return <WidgetButton
// widget={widget}
// editing={this.props.editing}
// onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
// signals={this.state.signals}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'NumberInput') {
// return <WidgetInput
// widget={widget}
// editing={this.props.editing}
// onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
// signals={this.state.signals}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Slider') {
// return <WidgetSlider
// widget={widget}
// editing={this.props.editing}
// onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
// signals={this.state.signals}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Gauge') {
// return <WidgetGauge
// widget={widget}
// data={this.state.icData}
// editing={this.props.editing}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// //} else if (widget.type === 'HTML') {
// //return <WidgetHTML
// // widget={widget}
// // editing={this.props.editing}
// ///>
// } else if (widget.type === 'Topology') {
// return <WidgetTopology
// widget={widget}
// files={this.state.files}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'TimeOffset') {
// return <WidgetTimeOffset
// widget={widget}
// data={this.state.icData}
// websockets={this.state.websockets}
// ics={this.props.ics}
// />
// } else if (widget.type === 'ICstatus') {
// return <WidgetICstatus
// widget={widget}
// ics={this.props.ics}
// />
// } else if (widget.type === 'Player') {
// return <WidgetPlayer
// widget={widget}
// editing={this.props.editing}
// configs={this.props.configs}
// onStarted={this.props.onSimulationStarted}
// ics={this.props.ics}
// results={this.state.results}
// files={this.state.files}
// scenarioID={this.props.scenarioID}
// />
// }
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 <WidgetLine widget={widget} editing={editing} />;
} else if (widget.type === 'Box') {
return <WidgetBox widget={widget} editing={editing} />;
} else if (widget.type === 'Label') {
return <WidgetLabel widget={widget} />;
} else if (widget.type === 'Image') {
return <WidgetImage widget={widget} files={files} token={sessionToken} />;
} else if (widget.type === 'Plot') {
return <WidgetPlot widget={widget} data={icdata} signals={signals} icIDs={icIDs} paused={paused} />;
} else if (widget.type === 'Table') {
return <WidgetTable widget={widget} data={icdata} signals={signals} icIDs={icIDs} />;
} else if (widget.type === 'Value') {
return <WidgetValue widget={widget} data={icdata} signals={signals} icIDs={icIDs} />;
} else if (widget.type === 'Lamp') {
return <WidgetLamp widget={widget} data={icdata} signals={signals} icIDs={icIDs} />;
} else if (widget.type === 'Gauge') {
return <WidgetGauge widget={widget} data={icdata} signals={signals} icIDs={icIDs} editing={editing} />;
} else if (widget.type === 'TimeOffset') {
return <WidgetTimeOffset widget={widget} data={icdata} signals={signals} ics={ics} editing={editing} websockets={websockets} />;
} else if (widget.type === 'ICstatus') {
return <WidgetICstatus widget={widget} ics={ics} />;
} else if (widget.type === 'Button') {
return (
<WidgetButton
widget={widget}
editing={editing}
onInputChanged={(value, controlID, controlValue, isFinalChange) =>
inputDataChanged(widget, value, controlID, controlValue, isFinalChange)
}
signals={signals}
token={sessionToken}
/>
);
} else if (widget.type === 'NumberInput') {
return (
<WidgetInput
widget={widget}
editing={editing}
onInputChanged={(value, controlID, controlValue, isFinalChange) =>
inputDataChanged(widget, value, controlID, controlValue, isFinalChange)
}
signals={signals}
token={sessionToken}
/>
);
} else if (widget.type === 'Slider') {
return (
<WidgetSlider
widget={widget}
editing={editing}
onInputChanged={(value, controlID, controlValue, isFinalChange) =>
inputDataChanged(widget, value, controlID, controlValue, isFinalChange)
}
signals={signals}
token={sessionToken}
/>
);
} else if (widget.type === 'Player') {
return (
<WidgetPlayer
widget={widget}
editing={editing}
configs={configs}
onStarted={onSimulationStarted}
ics={ics}
results={results}
files={files}
scenarioID={scenarioID}
/>
);
} else {
console.log('Unknown widget type', widget.type);
return <div>Error: Widget not found!</div>;
}
}
// 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 <WidgetCustomAction
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Action') {
// return <WidgetAction
// widget={widget}
// data={this.state.icData}
// />
// } else if (widget.type === 'Lamp') {
// return <WidgetLamp
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Value') {
// return <WidgetValue
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Plot') {
// return <WidgetPlot
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// paused={this.props.paused}
// />
// } else if (widget.type === 'Table') {
// return <WidgetTable
// widget={widget}
// data={this.state.icData}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Label') {
// return <WidgetLabel
// widget={widget}
// />
// } else if (widget.type === 'Image') {
// return <WidgetImage
// widget={widget}
// files={this.state.files}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Button') {
// return <WidgetButton
// widget={widget}
// editing={this.props.editing}
// onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
// signals={this.state.signals}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'NumberInput') {
// return <WidgetInput
// widget={widget}
// editing={this.props.editing}
// onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
// signals={this.state.signals}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Slider') {
// return <WidgetSlider
// widget={widget}
// editing={this.props.editing}
// onInputChanged={(value, controlID, controlValue, isFinalChange) => this.inputDataChanged(widget, value, controlID, controlValue, isFinalChange)}
// signals={this.state.signals}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Gauge') {
// return <WidgetGauge
// widget={widget}
// data={this.state.icData}
// editing={this.props.editing}
// signals={this.state.signals}
// icIDs={this.state.icIDs}
// />
// } else if (widget.type === 'Box') {
// return <WidgetBox
// widget={widget}
// editing={this.props.editing}
// />
// //} else if (widget.type === 'HTML') {
// //return <WidgetHTML
// // widget={widget}
// // editing={this.props.editing}
// ///>
// } else if (widget.type === 'Topology') {
// return <WidgetTopology
// widget={widget}
// files={this.state.files}
// token={this.state.sessionToken}
// />
// } else if (widget.type === 'Line') {
// return <WidgetLine
// widget={widget}
// editing={this.props.editing}
// />
// } else if (widget.type === 'TimeOffset') {
// return <WidgetTimeOffset
// widget={widget}
// data={this.state.icData}
// websockets={this.state.websockets}
// ics={this.props.ics}
// />
// } else if (widget.type === 'ICstatus') {
// return <WidgetICstatus
// widget={widget}
// ics={this.props.ics}
// />
// } else if (widget.type === 'Player') {
// return <WidgetPlayer
// widget={widget}
// editing={this.props.editing}
// configs={this.props.configs}
// onStarted={this.props.onSimulationStarted}
// ics={this.props.ics}
// results={this.state.results}
// files={this.state.files}
// scenarioID={this.props.scenarioID}
// />
// }
// 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;

View file

@ -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 () => {

View file

@ -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 (
<div className="full">

View file

@ -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(() => {

View file

@ -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 });

View file

@ -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);

View file

@ -84,5 +84,7 @@ export const {
useAuthenticateUserMutation,
useLazyGetFilesQuery,
useUpdateSignalMutation,
useGetIcDataQuery
useGetIcDataQuery,
useLazyDownloadImageQuery,
useUpdateComponentConfigMutation
} = apiSlice;

View file

@ -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();

View file

@ -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 <http://www.gnu.org/licenses/>.
******************************************************************************/
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;