From 567f6d2d5083e77cc84033d0a4cfc097517a7a7a Mon Sep 17 00:00:00 2001 From: Max Horsche Date: Mon, 11 Jan 2021 10:11:34 +0100 Subject: [PATCH 01/18] Added support for binary data types - Select data type (string, json, hex, uint, int, float) for each topic individually - Default data type is 'string' - Show milliseconds in message received timestamp --- app/src/actions/Settings.ts | 4 +- app/src/components/ChartPanel/TopicChart.tsx | 1 + .../Sidebar/CodeDiff/ChartPreview.tsx | 22 ++-- .../Sidebar/TopicPanel/TopicPanel.tsx | 13 +- .../Sidebar/TopicPanel/TopicTypeButton.tsx | 83 ++++++++++++ .../Sidebar/ValueRenderer/MessageHistory.tsx | 10 +- .../Sidebar/ValueRenderer/ValuePanel.tsx | 8 +- .../Sidebar/ValueRenderer/ValueRenderer.tsx | 69 +++++----- app/src/components/TopicPlot.tsx | 14 +- .../Tree/TreeNode/TreeNodeTitle.tsx | 17 ++- app/src/components/helper/DateFormatter.tsx | 12 +- backend/src/Model/Base64Message.ts | 120 ++++++++++++++++++ backend/src/Model/TreeNode.ts | 3 + backend/src/Model/index.ts | 2 +- package.json | 4 +- 15 files changed, 297 insertions(+), 85 deletions(-) create mode 100644 app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx diff --git a/app/src/actions/Settings.ts b/app/src/actions/Settings.ts index 0202aff1..f2752979 100644 --- a/app/src/actions/Settings.ts +++ b/app/src/actions/Settings.ts @@ -1,5 +1,5 @@ import * as q from '../../../backend/src/Model' -import { ActionTypes, SettingsStateModel, TopicOrder } from '../reducers/Settings' +import { ActionTypes, SettingsStateModel, TopicOrder, ValueRendererDisplayMode } from '../reducers/Settings' import { AppState } from '../reducers' import { autoExpandLimitSet } from '../components/SettingsDrawer/Settings' import { Base64Message } from '../../../backend/src/Model/Base64Message' @@ -66,7 +66,7 @@ export const selectTopicWithMouseOver = (doSelect: boolean) => (dispatch: Dispat dispatch(storeSettings()) } -export const setValueDisplayMode = (valueRendererDisplayMode: 'diff' | 'raw') => (dispatch: Dispatch) => { +export const setValueDisplayMode = (valueRendererDisplayMode: ValueRendererDisplayMode) => (dispatch: Dispatch) => { dispatch({ valueRendererDisplayMode, type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE, diff --git a/app/src/components/ChartPanel/TopicChart.tsx b/app/src/components/ChartPanel/TopicChart.tsx index 35008600..33a943ae 100644 --- a/app/src/components/ChartPanel/TopicChart.tsx +++ b/app/src/components/ChartPanel/TopicChart.tsx @@ -114,6 +114,7 @@ function TopicChart(props: Props) { ) : ( - - - - ) + + + + ) return ( @@ -69,7 +69,7 @@ function ChartPreview(props: Props) { - {open ? : } + {open ? : } diff --git a/app/src/components/Sidebar/TopicPanel/TopicPanel.tsx b/app/src/components/Sidebar/TopicPanel/TopicPanel.tsx index 24ad2578..129b24e1 100644 --- a/app/src/components/Sidebar/TopicPanel/TopicPanel.tsx +++ b/app/src/components/Sidebar/TopicPanel/TopicPanel.tsx @@ -6,22 +6,30 @@ import Topic from './Topic' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { RecursiveTopicDeleteButton } from './RecursiveTopicDeleteButton' -import { sidebarActions } from '../../../actions' import { TopicDeleteButton } from './TopicDeleteButton' +import { TopicTypeButton } from './TopicTypeButton' +import { sidebarActions } from '../../../actions' const TopicPanel = (props: { node?: q.TreeNode; actions: typeof sidebarActions }) => { const { node } = props console.log(node && node.path()) + const copyTopic = node ? : null const deleteTopic = useCallback((topic?: q.TreeNode, recursive: boolean = false) => { if (!topic) { return } - props.actions.clearTopic(topic, recursive) }, []) + const setTopicType = useCallback((node?: q.TreeNode, type: q.TopicDataType = 'string') => { + if (!node) { + return + } + node.type = type + }, []) + return useMemo( () => ( @@ -29,6 +37,7 @@ const TopicPanel = (props: { node?: q.TreeNode; actions: typeof sidebarActi Topic {copyTopic} + diff --git a/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx b/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx new file mode 100644 index 00000000..d5e80959 --- /dev/null +++ b/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx @@ -0,0 +1,83 @@ +import React, { useCallback } from 'react' +import * as q from '../../../../../backend/src/Model' +import CustomIconButton from '../../helper/CustomIconButton' +import Code from '@material-ui/icons/Code' +import ClickAwayListener from '@material-ui/core/ClickAwayListener' +import Grow from '@material-ui/core/Grow' +import Paper from '@material-ui/core/Paper' +import Popper from '@material-ui/core/Popper' +import MenuItem from '@material-ui/core/MenuItem' +import MenuList from '@material-ui/core/MenuList' + +const options: q.TopicDataType[] = ['string', 'json', 'hex', 'integer', 'unsigned int', 'floating point']; + +export const TopicTypeButton = (props: { + node?: q.TreeNode + setTopicType: (node: q.TreeNode, type: q.TopicDataType) => void +}) => { + const { node } = props + if (!node || !node.message || !node.message.payload) { + return null + } + + const [anchorEl, setAnchorEl] = React.useState(null); + const [open, setOpen] = React.useState(false) + + const handleMenuItemClick = useCallback( + (mouseEvent: React.MouseEvent, element: q.TreeNode, type: q.TopicDataType) => { + if (!element || !type) { + return + } + props.setTopicType(element, type as q.TopicDataType) + setOpen(false) + }, + [props.setTopicType] + ) + + const handleToggle = (event: React.MouseEvent) => { + if (open === true) { + return + } + setAnchorEl(event.currentTarget) + setOpen((prevOpen) => !prevOpen) + } + + const handleClose = (event: React.MouseEvent) => { + if (anchorEl && anchorEl.contains(event.target as HTMLElement)) { + return + } + setOpen(false) + } + + return ( + + + + {({ TransitionProps, placement }) => ( + + + + + {options.map((option, index) => ( + handleMenuItemClick(event, node, option)} + > + {option} + + ))} + + + + + )} + + + ) +} \ No newline at end of file diff --git a/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx b/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx index e3d9dbe1..c9590e43 100644 --- a/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx +++ b/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx @@ -82,7 +82,7 @@ class MessageHistory extends React.PureComponent { const history = node.messageHistory.toArray() let previousMessage: q.Message | undefined = node.message const historyElements = [...history].reverse().map((message, idx) => { - const value = message.payload ? Base64Message.toUnicodeString(message.payload) : '' + const [value, ignore] = Base64Message.format(message.payload, node.type) const element = { value, key: `${message.messageNumber}-${message.received}`, @@ -102,7 +102,7 @@ class MessageHistory extends React.PureComponent {
- +
), @@ -112,8 +112,8 @@ class MessageHistory extends React.PureComponent { return element }) - const isMessagePlottable = - node.message && node.message.payload && isPlottable(Base64Message.toUnicodeString(node.message.payload)) + const [value, ignore] = node.message && node.message.payload ? Base64Message.format(node.message.payload, node.type) : [null, undefined] + const isMessagePlottable = isPlottable(value) return (
{ } onClick={this.displayMessage} > - {isMessagePlottable ? : null} + {isMessagePlottable ? : null}
) diff --git a/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx b/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx index 459f3510..8642a4bc 100644 --- a/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx +++ b/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx @@ -85,10 +85,10 @@ function ValuePanel(props: Props) { [compareMessage] ) - const copyValue = - node && node.message && node.message.payload ? ( - - ) : null + const [value, ignore] = node && node.message && node.message.payload ? Base64Message.format(node.message.payload, node.type) : [null, undefined] + const copyValue = value ? ( + + ) : null return ( diff --git a/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx b/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx index 3a4f9ffc..9d561c3a 100644 --- a/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx +++ b/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx @@ -4,9 +4,8 @@ import CodeDiff from '../CodeDiff' import { AppState } from '../../../reducers' import { Base64Message } from '../../../../../backend/src/Model/Base64Message' import { connect } from 'react-redux' -import { default as ReactResizeDetector } from 'react-resize-detector' import { ValueRendererDisplayMode } from '../../../reducers/Settings' -import { Typography, Fade, Grow } from '@material-ui/core' +import { Fade } from '@material-ui/core' interface Props { message: q.Message @@ -38,44 +37,42 @@ class ValueRenderer extends React.Component { ) } - private convertMessage(msg?: Base64Message): [string | undefined, 'json' | undefined] { - if (!msg) { - return [undefined, undefined] + private renderDiffMode(message: q.Message, treeNode: q.TreeNode, compare?: q.Message) { + if (!message.payload) { + return } - const str = Base64Message.toUnicodeString(msg) - try { - JSON.parse(str) - } catch (error) { - return [str, undefined] - } + const previousMessages = treeNode.messageHistory.toArray() + const previousMessage = previousMessages[previousMessages.length - 2] + const compareMessage = compare || previousMessage || message - return [this.messageToPrettyJson(str), 'json'] - } + const compareValue = compareMessage.payload || message.payload + const [currentStr, currentType] = Base64Message.format(message.payload, treeNode.type) + const [compareStr, compareType] = Base64Message.format(compareValue, treeNode.type) - private messageToPrettyJson(str: string): string | undefined { - try { - const json = JSON.parse(str) - return JSON.stringify(json, undefined, ' ') - } catch { - return undefined - } + const language = currentType === compareType && compareType === 'json' ? 'json' : undefined + + return ( +
+ {this.renderDiff(currentStr, compareStr, undefined, language)} +
+ ) } - private renderRawMode(message: q.Message, compare?: q.Message) { + private renderRawMode(message: q.Message, treeNode: q.TreeNode, compare?: q.Message) { if (!message.payload) { return } - const [value, valueLanguage] = this.convertMessage(message.payload) - const [compareStr, compareStrLanguage] = - compare && compare.payload ? this.convertMessage(compare.payload) : [undefined, undefined] + + const [currentStr, currentType] = Base64Message.format(message.payload, treeNode.type) + const [compareStr, compareType] = compare && compare.payload ? Base64Message.format(compare.payload, treeNode.type) : [undefined, undefined] return (
- {this.renderDiff(value, value, undefined, valueLanguage)} + {this.renderDiff(currentStr, currentStr, undefined, currentType)}
- {Boolean(compareStr) ? this.renderDiff(compareStr, compareStr, 'selected', compareStrLanguage) : null} + {Boolean(compareStr) ? this.renderDiff(compareStr, compareStr, 'selected', compareType) : null}
@@ -88,24 +85,16 @@ class ValueRenderer extends React.Component { public renderValue() { const { message, treeNode, compareWith, renderMode } = this.props - const previousMessages = treeNode.messageHistory.toArray() - const previousMessage = previousMessages[previousMessages.length - 2] - const compareMessage = compareWith || previousMessage || message - - if (renderMode === 'raw') { - return this.renderRawMode(message, compareWith) - } if (!message.payload) { return null } - const compareValue = compareMessage.payload || message.payload - const [current, currentLanguage] = this.convertMessage(message.payload) - const [compare, compareLanguage] = this.convertMessage(compareValue) - - const language = currentLanguage === compareLanguage && compareLanguage === 'json' ? 'json' : undefined - - return this.renderDiff(current, compare, undefined, language) + switch (renderMode) { + case 'diff': + return this.renderDiffMode(message, treeNode, compareWith) + default: + return this.renderRawMode(message, treeNode, compareWith) + } } } diff --git a/app/src/components/TopicPlot.tsx b/app/src/components/TopicPlot.tsx index 56c69d1d..81b64feb 100644 --- a/app/src/components/TopicPlot.tsx +++ b/app/src/components/TopicPlot.tsx @@ -8,6 +8,7 @@ import { PlotCurveTypes } from '../reducers/Charts' const parseDuration = require('parse-duration') interface Props { + node?: q.TreeNode history: q.MessageHistory dotPath?: string timeInterval?: string @@ -25,22 +26,23 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array { - const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : NaN + const [value, ignore] = message.payload ? Base64Message.format(message.payload, type) : [NaN, undefined] + // const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : NaN return { x: message.received.getTime(), y: toPlottableValue(value) } }) .filter(data => !isNaN(data.y as any)) as any } -function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageHistory, dotPath: string) { +function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageHistory, dotPath: string, type: q.TopicDataType) { return filterUsingTimeRange(startTime, history.toArray()) .map((message: q.Message) => { let json = {} try { json = message.payload ? JSON.parse(Base64Message.toUnicodeString(message.payload)) : {} - } catch (ignore) {} + } catch (ignore) { } const value = dotProp.get(json, dotPath) @@ -54,8 +56,8 @@ function TopicPlot(props: Props) { const data = React.useMemo( () => props.dotPath - ? nodeDotPathToHistory(startOffset, props.history, props.dotPath) - : nodeToHistory(startOffset, props.history), + ? nodeDotPathToHistory(startOffset, props.history, props.dotPath, props.node ? props.node.type : 'string') + : nodeToHistory(startOffset, props.history, props.node ? props.node.type : 'string'), [props.history.last(), startOffset, props.dotPath] ) diff --git a/app/src/components/Tree/TreeNode/TreeNodeTitle.tsx b/app/src/components/Tree/TreeNode/TreeNodeTitle.tsx index 07d60a55..0663f9d9 100644 --- a/app/src/components/Tree/TreeNode/TreeNodeTitle.tsx +++ b/app/src/components/Tree/TreeNode/TreeNodeTitle.tsx @@ -31,19 +31,19 @@ class TreeNodeTitle extends React.PureComponent { return '' } - const str = Base64Message.toUnicodeString(this.props.treeNode.message.payload) - return str.length > limit ? `${str.slice(0, limit)}…` : str + const [value, ignore] = Base64Message.format(this.props.treeNode.message.payload, this.props.treeNode.type) + return value.length > limit ? `${value.slice(0, limit)}…` : value } private renderValue() { return this.props.treeNode.message && this.props.treeNode.message.payload && this.props.treeNode.message.length > 0 ? ( - - {' '} + + {' '} = {this.truncatedMessage()} - - ) : null + + ) : null } private renderExpander() { @@ -66,9 +66,8 @@ class TreeNodeTitle extends React.PureComponent { const messages = this.props.treeNode.leafMessageCount() const topicCount = this.props.treeNode.childTopicCount() return ( - {` (${topicCount} ${ - topicCount === 1 ? 'topic' : 'topics' - }, ${messages} ${messages === 1 ? 'message' : 'messages'})`} + {` (${topicCount} ${topicCount === 1 ? 'topic' : 'topics' + }, ${messages} ${messages === 1 ? 'message' : 'messages'})`} ) } diff --git a/app/src/components/helper/DateFormatter.tsx b/app/src/components/helper/DateFormatter.tsx index 9741f5e7..4e7fd6bd 100644 --- a/app/src/components/helper/DateFormatter.tsx +++ b/app/src/components/helper/DateFormatter.tsx @@ -12,6 +12,7 @@ interface Props { } const unitMapping = { + ms: 'milliseconds', s: 'seconds', m: 'minutes', h: 'hours', @@ -21,7 +22,7 @@ class DateFormatter extends React.PureComponent { private intervalSince(intervalSince: Date) { const interval = intervalSince.getTime() - this.props.date.getTime() const unit = this.unitForInterval(interval) - return `${Math.round(moment.duration(interval).as(unit) * 100) / 100} ${unitMapping[unit]}` + return `${moment.duration(interval).as(unit).toFixed(3)} ${unitMapping[unit]}` } private legacyDate() { @@ -31,10 +32,11 @@ class DateFormatter extends React.PureComponent { private localizedDate(locale: string) { return moment(this.props.date) .locale(locale) - .format(this.props.timeFirst ? 'LTS L' : 'L LTS') + .format(this.props.timeFirst ? 'LTS.SSS L' : 'L LTS.SSS') } private unitForInterval(milliseconds: number) { + const oneSecond = 1000 * 1 const oneMinute = 1000 * 60 const oneHour = oneMinute * 60 @@ -46,7 +48,11 @@ class DateFormatter extends React.PureComponent { return 'm' } - return 's' + if (milliseconds > oneSecond * 0.5) { + return 's' + } + + return 'ms' } public render() { diff --git a/backend/src/Model/Base64Message.ts b/backend/src/Model/Base64Message.ts index b3763fed..82ae52c5 100644 --- a/backend/src/Model/Base64Message.ts +++ b/backend/src/Model/Base64Message.ts @@ -1,3 +1,5 @@ +import { TopicDataType } from "./TreeNode" + const { Base64 } = require('js-base64') export class Base64Message { @@ -24,6 +26,124 @@ export class Base64Message { return new Base64Message(Base64.encode(str)) } + /* Raw message conversions (hex, uint, int, float) */ + public static format(message: Base64Message | null, type: TopicDataType = 'string'): [string, 'json' | undefined] { + if (!message) { + return ['', undefined] + } + + try { + switch (type) { + case 'json': + { + const json = JSON.parse(Base64Message.toUnicodeString(message)) + return [JSON.stringify(json, undefined, ' '), 'json'] + } + case 'hex': + { + const hex = Base64Message.toHex(message) + return [hex, undefined] + } + case 'integer': + { + const int = Base64Message.toInt(message) + return [int ? int : '', undefined] + } + case 'unsigned int': + { + const uint = Base64Message.toUInt(message) + return [uint ? uint : '', undefined] + } + case 'floating point': + { + const float = Base64Message.toFloat(message) + return [float ? float : '', undefined] + } + default: + { + const str = Base64Message.toUnicodeString(message) + return [str, undefined] + } + } + } catch (error) { + const str = Base64Message.toUnicodeString(message) + return [str, undefined] + } + } + + public static toHex(message: Base64Message) { + const buf = Buffer.from(message.base64Message, 'base64') + + let str: string = ''; + buf.forEach(element => { + str += `0x${element.toString(16)} ` + }) + return str.trimRight() + } + + public static toUInt(message: Base64Message) { + const buf = Buffer.from(message.base64Message, 'base64') + + let num: Number = 0; + switch (buf.length) { + case 1: + num = buf.readUInt8(0) + break + case 2: + num = buf.readUInt16LE(0) + break + case 4: + num = buf.readUInt32LE(0) + break + case 8: + num = Number(buf.readBigUInt64LE(0)) + break + default: + return undefined + } + return num.toString() + } + + public static toInt(message: Base64Message) { + const buf = Buffer.from(message.base64Message, 'base64') + + let num: Number = 0; + switch (buf.length) { + case 1: + num = buf.readInt8(0) + break + case 2: + num = buf.readInt16LE(0) + break + case 4: + num = buf.readInt32LE(0) + break + case 8: + num = Number(buf.readBigInt64LE(0)) + break + default: + return undefined + } + return num.toString() + } + + public static toFloat(message: Base64Message) { + const buf = Buffer.from(message.base64Message, 'base64') + + let num: Number = 0; + switch (buf.length) { + case 4: + num = buf.readFloatLE(0) + break + case 8: + num = buf.readDoubleLE(0) + break + default: + return undefined + } + return num.toString() + } + public static toDataUri(message: Base64Message, mimeType: string) { return `data:${mimeType};base64,${message.base64Message}` } diff --git a/backend/src/Model/TreeNode.ts b/backend/src/Model/TreeNode.ts index 1576b92b..d9ca465f 100644 --- a/backend/src/Model/TreeNode.ts +++ b/backend/src/Model/TreeNode.ts @@ -2,6 +2,8 @@ import { Destroyable } from './Destroyable' import { Edge, Message, RingBuffer, MessageHistory } from './' import { EventDispatcher } from '../../../events' +export type TopicDataType = 'string' | 'json' | 'hex' | 'integer' | 'unsigned int' | 'floating point' + export class TreeNode { public sourceEdge?: Edge public message?: Message @@ -17,6 +19,7 @@ export class TreeNode { public onMessage = new EventDispatcher() public onDestroy = new EventDispatcher>() public isTree = false + public type: TopicDataType = 'string' private cachedPath?: string private cachedChildTopics?: Array> diff --git a/backend/src/Model/index.ts b/backend/src/Model/index.ts index 047e633b..b0ac6735 100644 --- a/backend/src/Model/index.ts +++ b/backend/src/Model/index.ts @@ -1,5 +1,5 @@ export { Edge } from './Edge' -export { TreeNode } from './TreeNode' +export { TreeNode, TopicDataType } from './TreeNode' export { Message } from './Message' export { TreeNodeFactory } from './TreeNodeFactory' export { Tree } from './Tree' diff --git a/package.json b/package.json index 7153f80d..f6da39f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "MQTT-Explorer", - "version": "0.4.0-beta1", + "version": "0.4.0-beta4", "description": "Explore your message queues", "main": "dist/src/electron.js", "scripts": { @@ -119,4 +119,4 @@ "yarn-run-all": "^3.1.1" }, "optionalDependencies": {} -} +} \ No newline at end of file From 626b9cab7d4a06cb16affd21c17e8e3a7f6659c2 Mon Sep 17 00:00:00 2001 From: mhorsche Date: Tue, 21 Jun 2022 21:14:59 +0200 Subject: [PATCH 02/18] Specific int/uint byte size - possible data types are: 'json', 'string', 'hex', 'uint8', 'uint16', 'uint32', 'uint64', 'int8', 'int16', 'int32', 'int64', 'float', 'double' - default is 'json' --- .../Sidebar/TopicPanel/TopicTypeButton.tsx | 3 +- backend/src/Model/Base64Message.ts | 118 +++++++++++++----- backend/src/Model/TreeNode.ts | 5 +- 3 files changed, 92 insertions(+), 34 deletions(-) diff --git a/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx b/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx index d5e80959..3fcb439c 100644 --- a/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx +++ b/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx @@ -9,7 +9,8 @@ import Popper from '@material-ui/core/Popper' import MenuItem from '@material-ui/core/MenuItem' import MenuList from '@material-ui/core/MenuList' -const options: q.TopicDataType[] = ['string', 'json', 'hex', 'integer', 'unsigned int', 'floating point']; +// const options: q.TopicDataType[] = ['json', 'string', 'hex', 'integer', 'unsigned int', 'floating point'] +const options: q.TopicDataType[] = ['json', 'string', 'hex', 'uint8', 'uint16', 'uint32', 'uint64', 'int8', 'int16', 'int32', 'int64', 'float', 'double'] export const TopicTypeButton = (props: { node?: q.TreeNode diff --git a/backend/src/Model/Base64Message.ts b/backend/src/Model/Base64Message.ts index 82ae52c5..86bf00e1 100644 --- a/backend/src/Model/Base64Message.ts +++ b/backend/src/Model/Base64Message.ts @@ -26,7 +26,7 @@ export class Base64Message { return new Base64Message(Base64.encode(str)) } - /* Raw message conversions (hex, uint, int, float) */ + /* Raw message conversions ('uint8' | 'uint16' | 'uint32' | 'uint64' | 'int8' | 'int16' | 'int32' | 'int64' | 'float' | 'double') */ public static format(message: Base64Message | null, type: TopicDataType = 'string'): [string, 'json' | undefined] { if (!message) { return ['', undefined] @@ -44,19 +44,54 @@ export class Base64Message { const hex = Base64Message.toHex(message) return [hex, undefined] } - case 'integer': + case 'uint8': { - const int = Base64Message.toInt(message) - return [int ? int : '', undefined] + const uint = Base64Message.toUInt(message, 1) + return [uint ? uint : '', undefined] + } + case 'uint16': + { + const uint = Base64Message.toUInt(message, 2) + return [uint ? uint : '', undefined] + } + case 'uint32': + { + const uint = Base64Message.toUInt(message, 4) + return [uint ? uint : '', undefined] } - case 'unsigned int': + case 'uint64': { - const uint = Base64Message.toUInt(message) + const uint = Base64Message.toUInt(message, 8) return [uint ? uint : '', undefined] } - case 'floating point': + case 'int8': + { + const int = Base64Message.toInt(message, 1) + return [int ? int : '', undefined] + } + case 'int16': + { + const int = Base64Message.toInt(message, 2) + return [int ? int : '', undefined] + } + case 'int32': + { + const int = Base64Message.toInt(message, 4) + return [int ? int : '', undefined] + } + case 'int64': + { + const int = Base64Message.toInt(message, 8) + return [int ? int : '', undefined] + } + case 'float': + { + const float = Base64Message.toFloat(message, 4) + return [float ? float : '', undefined] + } + case 'double': { - const float = Base64Message.toFloat(message) + const float = Base64Message.toFloat(message, 8) return [float ? float : '', undefined] } default: @@ -76,72 +111,93 @@ export class Base64Message { let str: string = ''; buf.forEach(element => { - str += `0x${element.toString(16)} ` + let hex = element.toString(16).toUpperCase(); + str += `0x${hex.length < 2 ? "0" + hex : hex} ` }) return str.trimRight() } - public static toUInt(message: Base64Message) { + public static toUInt(message: Base64Message, bytes: number) { const buf = Buffer.from(message.base64Message, 'base64') - let num: Number = 0; - switch (buf.length) { + let str: String[] = []; + switch (bytes) { case 1: - num = buf.readUInt8(0) + for (let index = 0; index < buf.length; index += bytes) { + str.push(buf.readUInt8(index).toString()) + } break case 2: - num = buf.readUInt16LE(0) + for (let index = 0; index < buf.length; index += bytes) { + str.push(buf.readUInt16LE(index).toString()) + } break case 4: - num = buf.readUInt32LE(0) + for (let index = 0; index < buf.length; index += bytes) { + str.push(buf.readUInt32LE(index).toString()) + } break case 8: - num = Number(buf.readBigUInt64LE(0)) + for (let index = 0; index < buf.length; index += bytes) { + str.push(buf.readBigUInt64LE(index).toString()) + } break default: return undefined } - return num.toString() + return str.join(', ') } - public static toInt(message: Base64Message) { + public static toInt(message: Base64Message, bytes: number) { const buf = Buffer.from(message.base64Message, 'base64') - let num: Number = 0; - switch (buf.length) { + let str: String[] = []; + switch (bytes) { case 1: - num = buf.readInt8(0) + for (let index = 0; index < buf.length; index += bytes) { + str.push(buf.readInt8(index).toString()) + } break case 2: - num = buf.readInt16LE(0) + for (let index = 0; index < buf.length; index += bytes) { + str.push(buf.readInt16LE(index).toString()) + } break case 4: - num = buf.readInt32LE(0) + for (let index = 0; index < buf.length; index += bytes) { + str.push(buf.readInt32LE(index).toString()) + } break case 8: - num = Number(buf.readBigInt64LE(0)) + for (let index = 0; index < buf.length; index += bytes) { + str.push(buf.readBigInt64LE(index).toString()) + } break default: return undefined } - return num.toString() + return str.join(', ') } - public static toFloat(message: Base64Message) { + public static toFloat(message: Base64Message, bytes: number) { const buf = Buffer.from(message.base64Message, 'base64') - let num: Number = 0; - switch (buf.length) { + let str: String[] = []; + switch (bytes) { case 4: - num = buf.readFloatLE(0) + for (let index = 0; index < buf.length; index += bytes) { + str.push(buf.readFloatLE(index).toString()) + } break case 8: - num = buf.readDoubleLE(0) + for (let index = 0; index < buf.length; index += bytes) { + str.push(buf.readDoubleLE(index).toString()) + } break default: return undefined } - return num.toString() + return str.join(', ') } public static toDataUri(message: Base64Message, mimeType: string) { diff --git a/backend/src/Model/TreeNode.ts b/backend/src/Model/TreeNode.ts index d9ca465f..7adcd982 100644 --- a/backend/src/Model/TreeNode.ts +++ b/backend/src/Model/TreeNode.ts @@ -2,7 +2,8 @@ import { Destroyable } from './Destroyable' import { Edge, Message, RingBuffer, MessageHistory } from './' import { EventDispatcher } from '../../../events' -export type TopicDataType = 'string' | 'json' | 'hex' | 'integer' | 'unsigned int' | 'floating point' +// export type TopicDataType = 'json' | 'string' | 'hex' | 'integer' | 'unsigned int' | 'floating point' +export type TopicDataType = 'json' | 'string' | 'hex' | 'uint8' | 'uint16' | 'uint32' | 'uint64' | 'int8' | 'int16' | 'int32' | 'int64' | 'float' | 'double' export class TreeNode { public sourceEdge?: Edge @@ -19,7 +20,7 @@ export class TreeNode { public onMessage = new EventDispatcher() public onDestroy = new EventDispatcher>() public isTree = false - public type: TopicDataType = 'string' + public type: TopicDataType = 'json' private cachedPath?: string private cachedChildTopics?: Array> From f4bda3e2428bec337487ca196bb7364c0634ff4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Dalfors?= Date: Fri, 17 May 2024 12:03:35 +0200 Subject: [PATCH 03/18] feat: use tahu for sparkplug decoding --- backend/src/Model/sparkplugb.proto.ts | 204 ---------------------- backend/src/Model/sparkplugb.ts | 14 +- package.json | 3 +- res/sparkplug_b.proto | 197 ---------------------- scripts/sparkplug-client.js | 232 ++++++++++++++++++++++++++ yarn.lock | 61 ++++--- 6 files changed, 269 insertions(+), 442 deletions(-) delete mode 100644 backend/src/Model/sparkplugb.proto.ts delete mode 100644 res/sparkplug_b.proto create mode 100644 scripts/sparkplug-client.js diff --git a/backend/src/Model/sparkplugb.proto.ts b/backend/src/Model/sparkplugb.proto.ts deleted file mode 100644 index 569078c3..00000000 --- a/backend/src/Model/sparkplugb.proto.ts +++ /dev/null @@ -1,204 +0,0 @@ -/* spell-checker: disable */ - -const protocol = ` -syntax = "proto2"; - -// -// To compile: -// cd client_libraries/java -// protoc --proto_path=../../ --java_out=src/main/java ../../sparkplug_b.proto -// -package com.cirruslink.sparkplug.protobuf; - -option java_package = "com.cirruslink.sparkplug.protobuf"; -option java_outer_classname = "SparkplugBProto"; - -message Payload { - /* - // Indexes of Data Types - - // Unknown placeholder for future expansion. - Unknown = 0; - - // Basic Types - Int8 = 1; - Int16 = 2; - Int32 = 3; - Int64 = 4; - UInt8 = 5; - UInt16 = 6; - UInt32 = 7; - UInt64 = 8; - Float = 9; - Double = 10; - Boolean = 11; - String = 12; - DateTime = 13; - Text = 14; - - // Additional Metric Types - UUID = 15; - DataSet = 16; - Bytes = 17; - File = 18; - Template = 19; - - // Additional PropertyValue Types - PropertySet = 20; - PropertySetList = 21; - - */ - - message Template { - - message Parameter { - optional string name = 1; - optional uint32 type = 2; - - oneof value { - uint32 int_value = 3; - uint64 long_value = 4; - float float_value = 5; - double double_value = 6; - bool boolean_value = 7; - string string_value = 8; - ParameterValueExtension extension_value = 9; - } - - message ParameterValueExtension { - extensions 1 to max; - } - } - - optional string version = 1; // The version of the Template to prevent mismatches - repeated Metric metrics = 2; // Each metric is the name of the metric and the datatype of the member but does not contain a value - repeated Parameter parameters = 3; - optional string template_ref = 4; // Reference to a template if this is extending a Template or an instance - must exist if an instance - optional bool is_definition = 5; - extensions 6 to max; - } - - message DataSet { - - message DataSetValue { - - oneof value { - uint32 int_value = 1; - uint64 long_value = 2; - float float_value = 3; - double double_value = 4; - bool boolean_value = 5; - string string_value = 6; - DataSetValueExtension extension_value = 7; - } - - message DataSetValueExtension { - extensions 1 to max; - } - } - - message Row { - repeated DataSetValue elements = 1; - extensions 2 to max; // For third party extensions - } - - optional uint64 num_of_columns = 1; - repeated string columns = 2; - repeated uint32 types = 3; - repeated Row rows = 4; - extensions 5 to max; // For third party extensions - } - - message PropertyValue { - - optional uint32 type = 1; - optional bool is_null = 2; - - oneof value { - uint32 int_value = 3; - uint64 long_value = 4; - float float_value = 5; - double double_value = 6; - bool boolean_value = 7; - string string_value = 8; - PropertySet propertyset_value = 9; - PropertySetList propertysets_value = 10; // List of Property Values - PropertyValueExtension extension_value = 11; - } - - message PropertyValueExtension { - extensions 1 to max; - } - } - - message PropertySet { - repeated string keys = 1; // Names of the properties - repeated PropertyValue values = 2; - extensions 3 to max; - } - - message PropertySetList { - repeated PropertySet propertyset = 1; - extensions 2 to max; - } - - message MetaData { - // Bytes specific metadata - optional bool is_multi_part = 1; - - // General metadata - optional string content_type = 2; // Content/Media type - optional uint64 size = 3; // File size, String size, Multi-part size, etc - optional uint64 seq = 4; // Sequence number for multi-part messages - - // File metadata - optional string file_name = 5; // File name - optional string file_type = 6; // File type (i.e. xml, json, txt, cpp, etc) - optional string md5 = 7; // md5 of data - - // Catchalls and future expansion - optional string description = 8; // Could be anything such as json or xml of custom properties - extensions 9 to max; - } - - message Metric { - - optional string name = 1; // Metric name - should only be included on birth - optional uint64 alias = 2; // Metric alias - tied to name on birth and included in all later DATA messages - optional uint64 timestamp = 3; // Timestamp associated with data acquisition time - optional uint32 datatype = 4; // DataType of the metric/tag value - optional bool is_historical = 5; // If this is historical data and should not update real time tag - optional bool is_transient = 6; // Tells consuming clients such as MQTT Engine to not store this as a tag - optional bool is_null = 7; // If this is null - explicitly say so rather than using -1, false, etc for some datatypes. - optional MetaData metadata = 8; // Metadata for the payload - optional PropertySet properties = 9; - - oneof value { - uint32 int_value = 10; - uint64 long_value = 11; - float float_value = 12; - double double_value = 13; - bool boolean_value = 14; - string string_value = 15; - bytes bytes_value = 16; // Bytes, File - DataSet dataset_value = 17; - Template template_value = 18; - MetricValueExtension extension_value = 19; - } - - message MetricValueExtension { - extensions 1 to max; - } - } - - optional uint64 timestamp = 1; // Timestamp at message sending time - repeated Metric metrics = 2; // Repeated forever - no limit in Google Protobufs - optional uint64 seq = 3; // Sequence number - optional string uuid = 4; // UUID to track message type in terms of schema definitions - optional bytes body = 5; // To optionally bypass the whole definition above - extensions 6 to max; // For third party extensions -} -` - -/* spell-checker: enable */ -export default protocol diff --git a/backend/src/Model/sparkplugb.ts b/backend/src/Model/sparkplugb.ts index b4b460da..d3625b8b 100644 --- a/backend/src/Model/sparkplugb.ts +++ b/backend/src/Model/sparkplugb.ts @@ -1,18 +1,14 @@ -// cSpell:words protobuf -import * as protobuf from 'protobufjs' -import protocol from './sparkplugb.proto' import { Base64Message } from './Base64Message' import { Decoder } from './Decoder' - -const root = protobuf.parse(protocol).root -/* cspell:disable-next-line */ -export let SparkplugPayload = root.lookupType('com.cirruslink.sparkplug.protobuf.Payload') +import { get } from 'sparkplug-payload' +var sparkplug = get("spBv1.0") export const SparkplugDecoder = { decode(input: Buffer): Base64Message { try { - const message = Base64Message.fromString( - JSON.stringify(SparkplugPayload.toObject(SparkplugPayload.decode(new Uint8Array(input)))) + const message = Base64Message.fromString(JSON.stringify( + // @ts-ignore + sparkplug.decodePayload(new Uint8Array(input))) ) message.decoder = Decoder.SPARKPLUG return message diff --git a/package.json b/package.json index 02eaffbb..bb93080b 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "semantic-release": "^23.0.8", "semantic-release-export-data": "^1.0.1", "source-map-support": "^0.5.9", + "sparkplug-client": "^3.2.4", "ts-node": "^10.9.2", "tslint": "^6.1.3", "tslint-config-airbnb": "^5.11.2", @@ -126,8 +127,8 @@ "lowdb": "^1.0.0", "mime": "^2.4.4", "mqtt": "^4.3.6", - "protobufjs": "^6.11.4", "sha1": "^1.1.1", + "sparkplug-payload": "^1.0.3", "uuid": "^8.3.2", "yarn-run-all": "^3.1.1" } diff --git a/res/sparkplug_b.proto b/res/sparkplug_b.proto deleted file mode 100644 index bda645cc..00000000 --- a/res/sparkplug_b.proto +++ /dev/null @@ -1,197 +0,0 @@ -syntax = "proto2"; - -// -// To compile: -// cd client_libraries/java -// protoc --proto_path=../../ --java_out=src/main/java ../../sparkplug_b.proto -// -package com.cirruslink.sparkplug.protobuf; - -option java_package = "com.cirruslink.sparkplug.protobuf"; -option java_outer_classname = "SparkplugBProto"; - -message Payload { - /* - // Indexes of Data Types - - // Unknown placeholder for future expansion. - Unknown = 0; - - // Basic Types - Int8 = 1; - Int16 = 2; - Int32 = 3; - Int64 = 4; - UInt8 = 5; - UInt16 = 6; - UInt32 = 7; - UInt64 = 8; - Float = 9; - Double = 10; - Boolean = 11; - String = 12; - DateTime = 13; - Text = 14; - - // Additional Metric Types - UUID = 15; - DataSet = 16; - Bytes = 17; - File = 18; - Template = 19; - - // Additional PropertyValue Types - PropertySet = 20; - PropertySetList = 21; - - */ - - message Template { - - message Parameter { - optional string name = 1; - optional uint32 type = 2; - - oneof value { - uint32 int_value = 3; - uint64 long_value = 4; - float float_value = 5; - double double_value = 6; - bool boolean_value = 7; - string string_value = 8; - ParameterValueExtension extension_value = 9; - } - - message ParameterValueExtension { - extensions 1 to max; - } - } - - optional string version = 1; // The version of the Template to prevent mismatches - repeated Metric metrics = 2; // Each metric is the name of the metric and the datatype of the member but does not contain a value - repeated Parameter parameters = 3; - optional string template_ref = 4; // Reference to a template if this is extending a Template or an instance - must exist if an instance - optional bool is_definition = 5; - extensions 6 to max; - } - - message DataSet { - - message DataSetValue { - - oneof value { - uint32 int_value = 1; - uint64 long_value = 2; - float float_value = 3; - double double_value = 4; - bool boolean_value = 5; - string string_value = 6; - DataSetValueExtension extension_value = 7; - } - - message DataSetValueExtension { - extensions 1 to max; - } - } - - message Row { - repeated DataSetValue elements = 1; - extensions 2 to max; // For third party extensions - } - - optional uint64 num_of_columns = 1; - repeated string columns = 2; - repeated uint32 types = 3; - repeated Row rows = 4; - extensions 5 to max; // For third party extensions - } - - message PropertyValue { - - optional uint32 type = 1; - optional bool is_null = 2; - - oneof value { - uint32 int_value = 3; - uint64 long_value = 4; - float float_value = 5; - double double_value = 6; - bool boolean_value = 7; - string string_value = 8; - PropertySet propertyset_value = 9; - PropertySetList propertysets_value = 10; // List of Property Values - PropertyValueExtension extension_value = 11; - } - - message PropertyValueExtension { - extensions 1 to max; - } - } - - message PropertySet { - repeated string keys = 1; // Names of the properties - repeated PropertyValue values = 2; - extensions 3 to max; - } - - message PropertySetList { - repeated PropertySet propertyset = 1; - extensions 2 to max; - } - - message MetaData { - // Bytes specific metadata - optional bool is_multi_part = 1; - - // General metadata - optional string content_type = 2; // Content/Media type - optional uint64 size = 3; // File size, String size, Multi-part size, etc - optional uint64 seq = 4; // Sequence number for multi-part messages - - // File metadata - optional string file_name = 5; // File name - optional string file_type = 6; // File type (i.e. xml, json, txt, cpp, etc) - optional string md5 = 7; // md5 of data - - // Catchalls and future expansion - optional string description = 8; // Could be anything such as json or xml of custom properties - extensions 9 to max; - } - - message Metric { - - optional string name = 1; // Metric name - should only be included on birth - optional uint64 alias = 2; // Metric alias - tied to name on birth and included in all later DATA messages - optional uint64 timestamp = 3; // Timestamp associated with data acquisition time - optional uint32 datatype = 4; // DataType of the metric/tag value - optional bool is_historical = 5; // If this is historical data and should not update real time tag - optional bool is_transient = 6; // Tells consuming clients such as MQTT Engine to not store this as a tag - optional bool is_null = 7; // If this is null - explicitly say so rather than using -1, false, etc for some datatypes. - optional MetaData metadata = 8; // Metadata for the payload - optional PropertySet properties = 9; - - oneof value { - uint32 int_value = 10; - uint64 long_value = 11; - float float_value = 12; - double double_value = 13; - bool boolean_value = 14; - string string_value = 15; - bytes bytes_value = 16; // Bytes, File - DataSet dataset_value = 17; - Template template_value = 18; - MetricValueExtension extension_value = 19; - } - - message MetricValueExtension { - extensions 1 to max; - } - } - - optional uint64 timestamp = 1; // Timestamp at message sending time - repeated Metric metrics = 2; // Repeated forever - no limit in Google Protobufs - optional uint64 seq = 3; // Sequence number - optional string uuid = 4; // UUID to track message type in terms of schema definitions - optional bytes body = 5; // To optionally bypass the whole definition above - extensions 6 to max; // For third party extensions -} diff --git a/scripts/sparkplug-client.js b/scripts/sparkplug-client.js new file mode 100644 index 00000000..3b5414da --- /dev/null +++ b/scripts/sparkplug-client.js @@ -0,0 +1,232 @@ +/******************************************************************************** + * Copyright (c) 2016-2018 Cirrus Link Solutions and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Cirrus Link Solutions - initial implementation + ********************************************************************************/ +var SparkplugClient = require('sparkplug-client') + +/* + * Main sample function which includes the run() function for running the sample + */ +var sample = (function () { + var config = { + serverUrl: 'tcp://127.0.0.1:1883', + username: '', + password: '', + groupId: 'Sparkplug Devices', + edgeNode: 'JavaScript Edge Node', + clientId: 'JavaScriptSimpleEdgeNode', + version: 'spBv1.0', + }, + hwVersion = 'Emulated Hardware', + swVersion = 'v1.0.0', + deviceId = 'Emulated Device', + sparkPlugClient, + publishPeriod = 5000, + // Generates a random integer + randomInt = function () { + return 1 + Math.floor(Math.random() * 10) + }, + // Get BIRTH payload for the edge node + getNodeBirthPayload = function () { + return { + timestamp: new Date().getTime(), + metrics: [ + { + name: 'Node Control/Rebirth', + type: 'boolean', + value: false, + }, + { + name: 'Template1', + type: 'template', + value: { + isDefinition: true, + metrics: [ + { name: 'myBool', value: false, type: 'boolean' }, + { name: 'myInt', value: 0, type: 'int' }, + ], + parameters: [ + { + name: 'param1', + type: 'string', + value: 'value1', + }, + ], + }, + }, + ], + } + }, + // Get BIRTH payload for the device + getDeviceBirthPayload = function () { + return { + timestamp: new Date().getTime(), + metrics: [ + { name: 'my_boolean', value: Math.random() > 0.5, type: 'boolean' }, + { name: 'my_double', value: Math.random() * 0.123456789, type: 'double' }, + { name: 'my_float', value: Math.random() * 0.123, type: 'float' }, + { name: 'my_int', value: randomInt(), type: 'int' }, + { name: 'my_long', value: randomInt() * 214748364700, type: 'long' }, + { name: 'Inputs/0', value: true, type: 'boolean' }, + { name: 'Inputs/1', value: 0, type: 'int' }, + { name: 'Inputs/2', value: 1.23, type: 'float' }, + { name: 'Outputs/0', value: true, type: 'boolean' }, + { name: 'Outputs/1', value: 0, type: 'int' }, + { name: 'Outputs/2', value: 1.23, type: 'float' }, + { name: 'Properties/hw_version', value: hwVersion, type: 'string' }, + { name: 'Properties/sw_version', value: swVersion, type: 'string' }, + { + name: 'my_dataset', + type: 'dataset', + value: { + numOfColumns: 2, + types: ['string', 'string'], + columns: ['str1', 'str2'], + rows: [ + ['x', 'a'], + ['y', 'b'], + ], + }, + }, + { + name: 'TemplateInstance1', + type: 'template', + value: { + templateRef: 'Template1', + isDefinition: false, + metrics: [ + { name: 'myBool', value: true, type: 'boolean' }, + { name: 'myInt', value: 100, type: 'int' }, + ], + parameters: [ + { + name: 'param1', + type: 'string', + value: 'value2', + }, + ], + }, + }, + ], + } + }, + // Get data payload for the device + getDataPayload = function () { + return { + timestamp: new Date().getTime(), + metrics: [ + { name: 'my_boolean', value: Math.random() > 0.5, type: 'boolean' }, + { name: 'my_double', value: Math.random() * 0.123456789, type: 'double' }, + { name: 'my_float', value: Math.random() * 0.123, type: 'float' }, + { name: 'my_int', value: randomInt(), type: 'int' }, + { name: 'my_long', value: randomInt() * 214748364700, type: 'long' }, + ], + } + }, + // Runs the sample + run = function () { + // Create the SparkplugClient + sparkplugClient = SparkplugClient.newClient(config) + + // Create Incoming Message Handler + sparkplugClient.on('message', function (topic, payload) { + console.log(topic, payload) + }) + + // Create 'birth' handler + sparkplugClient.on('birth', function () { + // Publish Node BIRTH certificate + sparkplugClient.publishNodeBirth(getNodeBirthPayload()) + // Publish Device BIRTH certificate + sparkplugClient.publishDeviceBirth(deviceId, getDeviceBirthPayload()) + }) + + // Create node command handler + sparkplugClient.on('ncmd', function (payload) { + var timestamp = payload.timestamp, + metrics = payload.metrics + + if (metrics !== undefined && metrics !== null) { + for (var i = 0; i < metrics.length; i++) { + var metric = metrics[i] + if (metric.name == 'Node Control/Rebirth' && metric.value) { + console.log("Received 'Rebirth' command") + // Publish Node BIRTH certificate + sparkplugClient.publishNodeBirth(getNodeBirthPayload()) + // Publish Device BIRTH certificate + sparkplugClient.publishDeviceBirth(deviceId, getDeviceBirthPayload()) + } + } + } + }) + + // Create device command handler + sparkplugClient.on('dcmd', function (deviceId, payload) { + var timestamp = payload.timestamp, + metrics = payload.metrics, + inboundMetricMap = {}, + outboundMetric = [], + outboundPayload + + console.log('Command recevied for device ' + deviceId) + + // Loop over the metrics and store them in a map + if (metrics !== undefined && metrics !== null) { + for (var i = 0; i < metrics.length; i++) { + var metric = metrics[i] + inboundMetricMap[metric.name] = metric.value + } + } + if (inboundMetricMap['Outputs/0'] !== undefined && inboundMetricMap['Outputs/0'] !== null) { + console.log('Outputs/0: ' + inboundMetricMap['Outputs/0']) + outboundMetric.push({ name: 'Inputs/0', value: inboundMetricMap['Outputs/0'], type: 'boolean' }) + outboundMetric.push({ name: 'Outputs/0', value: inboundMetricMap['Outputs/0'], type: 'boolean' }) + console.log('Updated value for Inputs/0 ' + inboundMetricMap['Outputs/0']) + } else if (inboundMetricMap['Outputs/1'] !== undefined && inboundMetricMap['Outputs/1'] !== null) { + console.log('Outputs/1: ' + inboundMetricMap['Outputs/1']) + outboundMetric.push({ name: 'Inputs/1', value: inboundMetricMap['Outputs/1'], type: 'int' }) + outboundMetric.push({ name: 'Outputs/1', value: inboundMetricMap['Outputs/1'], type: 'int' }) + console.log('Updated value for Inputs/1 ' + inboundMetricMap['Outputs/1']) + } else if (inboundMetricMap['Outputs/2'] !== undefined && inboundMetricMap['Outputs/2'] !== null) { + console.log('Outputs/2: ' + inboundMetricMap['Outputs/2']) + outboundMetric.push({ name: 'Inputs/2', value: inboundMetricMap['Outputs/2'], type: 'float' }) + outboundMetric.push({ name: 'Outputs/2', value: inboundMetricMap['Outputs/2'], type: 'float' }) + console.log('Updated value for Inputs/2 ' + inboundMetricMap['Outputs/2']) + } + + outboundPayload = { + timestamp: new Date().getTime(), + metrics: outboundMetric, + } + + // Publish device data + sparkplugClient.publishDeviceData(deviceId, outboundPayload) + }) + + for (var i = 1; i < 101; i++) { + // Set up a device data publish for i*publishPeriod milliseconds from now + setTimeout(function () { + // Publish device data + sparkplugClient.publishDeviceData(deviceId, getDataPayload()) + + // End the client connection after the last publish + if (i === 100) { + sparkplugClient.stop() + } + }, i * publishPeriod) + } + } + + return { run: run } +})() + +// Run the sample +sample.run() diff --git a/yarn.lock b/yarn.lock index d917b265..bd3acaa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1398,7 +1398,7 @@ resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz" integrity sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA== -"@types/long@^4.0.1": +"@types/long@^4.0.0", "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== @@ -5074,7 +5074,7 @@ mqtt-packet@^6.8.0: debug "^4.1.1" process-nextick-args "^2.0.1" -mqtt@^4.3.6: +mqtt@^4.2.8, mqtt@^4.3.6: version "4.3.8" resolved "https://registry.npmjs.org/mqtt/-/mqtt-4.3.8.tgz" integrity sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw== @@ -5635,6 +5635,11 @@ pacote@^18.0.0, pacote@^18.0.1, pacote@^18.0.3: ssri "^10.0.0" tar "^6.1.11" +pako@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -5982,7 +5987,7 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== -protobufjs@^6.11.4: +protobufjs@^6.11.3: version "6.11.4" resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz" integrity sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw== @@ -6619,6 +6624,25 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sparkplug-client@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/sparkplug-client/-/sparkplug-client-3.2.4.tgz#96ae0650339049099272c17a16aae7fe1278f302" + integrity sha512-Yck+cKwoS3RH+UYGyTiH9SXALT9foXQzkHCrJTUCgdS39J21NdVLIMTL9HwkkeunrxtKDakEI+UjTOIXEhWg7A== + dependencies: + debug "^4.3.4" + mqtt "^4.2.8" + pako "^2.0.4" + sparkplug-payload "^1.0.3" + +sparkplug-payload@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sparkplug-payload/-/sparkplug-payload-1.0.3.tgz#b8a54c9acf30b82ec9bae6ad4e2978cf89f7df2a" + integrity sha512-JAQSyuHVQQe/LzIlJdcIaD1F1c+rbXoolII3H1whQkhuZ96+G53RoPWqP9zCPZYFjvfSMHnQNIedGIZw/zjHJw== + dependencies: + "@types/long" "^4.0.0" + long "^4.0.0" + protobufjs "^6.11.3" + spawn-error-forwarder@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz#1afd94738e999b0346d7b9fc373be55e07577029" @@ -6745,16 +6769,7 @@ stream-shift@^1.0.2: resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz" integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6824,7 +6839,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6838,13 +6853,6 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" @@ -7536,7 +7544,7 @@ workerpool@6.2.1: resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -7554,15 +7562,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" From 980072f680324b96da8904a2293f117e789a1e87 Mon Sep 17 00:00:00 2001 From: Thomas Nordquist Date: Tue, 21 May 2024 09:22:11 +0200 Subject: [PATCH 04/18] chore: decode data in frontend --- app/src/actions/Settings.ts | 17 +- .../SettingsDrawer/BrokerStatistics.tsx | 2 +- .../Sidebar/TopicPanel/TopicPanel.tsx | 9 +- .../Sidebar/TopicPanel/TopicTypeButton.tsx | 87 ++++++---- .../Sidebar/ValueRenderer/MessageHistory.tsx | 10 +- .../Sidebar/ValueRenderer/ValuePanel.tsx | 7 +- .../Sidebar/ValueRenderer/ValueRenderer.tsx | 10 +- app/src/components/TopicPlot.tsx | 35 ++-- .../Tree/TreeNode/TreeNodeTitle.tsx | 17 +- app/src/components/UpdateNotifier.tsx | 8 +- app/src/components/helper/DateFormatter.tsx | 4 +- app/tsconfig.json | 30 +--- app/webpack.config.js | 13 +- backend/src/ConfigStorage.ts | 8 +- backend/src/DataSource/MqttSource.ts | 2 +- backend/src/Model/Base64Message.ts | 157 ++---------------- backend/src/Model/TreeNode.ts | 48 +++++- backend/src/Model/TreeNodeFactory.ts | 2 + backend/src/Model/sparkplugb.ts | 91 +++++++++- backend/src/index.ts | 7 +- src/electron.ts | 4 +- tsconfig.json | 18 +- 22 files changed, 303 insertions(+), 283 deletions(-) diff --git a/app/src/actions/Settings.ts b/app/src/actions/Settings.ts index d222afc0..80282e6d 100644 --- a/app/src/actions/Settings.ts +++ b/app/src/actions/Settings.ts @@ -68,13 +68,14 @@ export const selectTopicWithMouseOver = (doSelect: boolean) => (dispatch: Dispat dispatch(storeSettings()) } -export const setValueDisplayMode = (valueRendererDisplayMode: ValueRendererDisplayMode) => (dispatch: Dispatch) => { - dispatch({ - valueRendererDisplayMode, - type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE, - }) - dispatch(storeSettings()) -} +export const setValueDisplayMode = + (valueRendererDisplayMode: ValueRendererDisplayMode) => (dispatch: Dispatch) => { + dispatch({ + valueRendererDisplayMode, + type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE, + }) + dispatch(storeSettings()) + } export const toggleHighlightTopicUpdates = () => (dispatch: Dispatch) => { dispatch({ @@ -117,7 +118,7 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch, get const messageMatches = node.message && node.message.payload && - Base64Message.toUnicodeString(node.message.payload).toLowerCase().indexOf(filterStr) !== -1 + node.message.payload.toUnicodeString().toLowerCase().indexOf(filterStr) !== -1 return Boolean(messageMatches) } diff --git a/app/src/components/SettingsDrawer/BrokerStatistics.tsx b/app/src/components/SettingsDrawer/BrokerStatistics.tsx index c5c210db..4006c90b 100644 --- a/app/src/components/SettingsDrawer/BrokerStatistics.tsx +++ b/app/src/components/SettingsDrawer/BrokerStatistics.tsx @@ -123,7 +123,7 @@ function renderStat(tree: q.Tree, stat: Stats) { return null } - const str = node.message.payload ? Base64Message.toUnicodeString(node.message.payload) : '' + const str = node.message.payload ? node.message.payload.toUnicodeString() : '' let value = node.message && node.message.payload ? parseFloat(str) : NaN value = !isNaN(value) ? abbreviate(value) : str diff --git a/app/src/components/Sidebar/TopicPanel/TopicPanel.tsx b/app/src/components/Sidebar/TopicPanel/TopicPanel.tsx index 129b24e1..253f8f17 100644 --- a/app/src/components/Sidebar/TopicPanel/TopicPanel.tsx +++ b/app/src/components/Sidebar/TopicPanel/TopicPanel.tsx @@ -23,13 +23,6 @@ const TopicPanel = (props: { node?: q.TreeNode; actions: typeof sidebarActi props.actions.clearTopic(topic, recursive) }, []) - const setTopicType = useCallback((node?: q.TreeNode, type: q.TopicDataType = 'string') => { - if (!node) { - return - } - node.type = type - }, []) - return useMemo( () => ( @@ -37,7 +30,7 @@ const TopicPanel = (props: { node?: q.TreeNode; actions: typeof sidebarActi Topic {copyTopic} - + diff --git a/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx b/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx index 3fcb439c..d71a891c 100644 --- a/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx +++ b/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx @@ -1,46 +1,59 @@ -import React, { useCallback } from 'react' +import React, { useCallback, useMemo } from 'react' import * as q from '../../../../../backend/src/Model' -import CustomIconButton from '../../helper/CustomIconButton' -import Code from '@material-ui/icons/Code' import ClickAwayListener from '@material-ui/core/ClickAwayListener' import Grow from '@material-ui/core/Grow' +import Button from '@material-ui/core/Button' import Paper from '@material-ui/core/Paper' import Popper from '@material-ui/core/Popper' import MenuItem from '@material-ui/core/MenuItem' import MenuList from '@material-ui/core/MenuList' +import WarningRounded from '@material-ui/icons/WarningRounded' +import { IDecoder, decoders } from '../../../../../backend/src/Model/sparkplugb' +import { Tooltip } from '@material-ui/core' // const options: q.TopicDataType[] = ['json', 'string', 'hex', 'integer', 'unsigned int', 'floating point'] -const options: q.TopicDataType[] = ['json', 'string', 'hex', 'uint8', 'uint16', 'uint32', 'uint64', 'int8', 'int16', 'int32', 'int64', 'float', 'double'] +const options: q.TopicDataType[] = [ + 'json', + 'string', + 'hex', + 'uint8', + 'uint16', + 'uint32', + 'uint64', + 'int8', + 'int16', + 'int32', + 'int64', + 'float', + 'double', +] -export const TopicTypeButton = (props: { - node?: q.TreeNode - setTopicType: (node: q.TreeNode, type: q.TopicDataType) => void -}) => { +export const TopicTypeButton = (props: { node?: q.TreeNode }) => { const { node } = props if (!node || !node.message || !node.message.payload) { return null } - const [anchorEl, setAnchorEl] = React.useState(null); + const options = decoders.flatMap(decoder => decoder.formats.map(format => [decoder, format] as const)) + + const [anchorEl, setAnchorEl] = React.useState(null) const [open, setOpen] = React.useState(false) - const handleMenuItemClick = useCallback( - (mouseEvent: React.MouseEvent, element: q.TreeNode, type: q.TopicDataType) => { - if (!element || !type) { - return - } - props.setTopicType(element, type as q.TopicDataType) - setOpen(false) - }, - [props.setTopicType] - ) + const selectOption = useCallback((decoder: IDecoder, format: string) => { + if (!node) { + return + } + node.decoder = decoder + node.decoderFormat = format + setOpen(false) + }, []) const handleToggle = (event: React.MouseEvent) => { if (open === true) { return } setAnchorEl(event.currentTarget) - setOpen((prevOpen) => !prevOpen) + setOpen(prevOpen => !prevOpen) } const handleClose = (event: React.MouseEvent) => { @@ -51,8 +64,8 @@ export const TopicTypeButton = (props: { } return ( - - + + ) +} + +function DecoderStatus({ node, decoder, format }: { node: q.TreeNode; decoder: IDecoder; format: string }) { + const decoded = useMemo(() => { + return node.message?.payload && decoder.decode(node.message?.payload, format) + }, [node.message, decoder, format]) + + return decoded?.error ? ( + +
+ {format} +
+
+ ) : ( + <>{format} ) -} \ No newline at end of file +} diff --git a/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx b/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx index c9590e43..fbbc619b 100644 --- a/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx +++ b/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx @@ -82,9 +82,10 @@ class MessageHistory extends React.PureComponent { const history = node.messageHistory.toArray() let previousMessage: q.Message | undefined = node.message const historyElements = [...history].reverse().map((message, idx) => { - const [value, ignore] = Base64Message.format(message.payload, node.type) + const value = node.message ? node.decodeMessage(node.message)?.format()[0] ?? null : null + const element = { - value, + value: value ?? '', key: `${message.messageNumber}-${message.received}`, title: ( @@ -102,7 +103,7 @@ class MessageHistory extends React.PureComponent {
- +
), @@ -112,7 +113,8 @@ class MessageHistory extends React.PureComponent { return element }) - const [value, ignore] = node.message && node.message.payload ? Base64Message.format(node.message.payload, node.type) : [null, undefined] + const value = node.message ? node.decodeMessage(node.message)?.format()[0] ?? null : null + const isMessagePlottable = isPlottable(value) return (
diff --git a/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx b/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx index de3bb7b4..d17e3c79 100644 --- a/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx +++ b/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx @@ -85,10 +85,9 @@ function ValuePanel(props: Props) { [compareMessage] ) - const [value, ignore] = node && node.message && node.message.payload ? Base64Message.format(node.message.payload, node.type) : [null, undefined] - const copyValue = value ? ( - - ) : null + const [value] = + node && node.message && node.message.payload ? node.message.payload?.format(node.type) : [null, undefined] + const copyValue = value ? : null return ( diff --git a/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx b/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx index 4e5d7847..232a0ef6 100644 --- a/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx +++ b/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx @@ -2,7 +2,6 @@ import * as q from '../../../../../backend/src/Model' import * as React from 'react' import CodeDiff from '../CodeDiff' import { AppState } from '../../../reducers' -import { Base64Message } from '../../../../../backend/src/Model/Base64Message' import { connect } from 'react-redux' import { ValueRendererDisplayMode } from '../../../reducers/Settings' import { Fade } from '@material-ui/core' @@ -47,9 +46,8 @@ class ValueRenderer extends React.Component { const previousMessage = previousMessages[previousMessages.length - 2] const compareMessage = compare || previousMessage || message - const compareValue = compareMessage.payload || message.payload - const [currentStr, currentType] = Base64Message.format(message.payload, treeNode.type) - const [compareStr, compareType] = Base64Message.format(compareValue, treeNode.type) + const [currentStr, currentType] = treeNode.decodeMessage(message)?.format(treeNode.type) ?? [] + const [compareStr, compareType] = treeNode.decodeMessage(compareMessage)?.format(treeNode.type) ?? [] const language = currentType === compareType && compareType === 'json' ? 'json' : undefined @@ -61,9 +59,9 @@ class ValueRenderer extends React.Component { return } - const [currentStr, currentType] = Base64Message.format(message.payload, treeNode.type) + const [currentStr, currentType] = treeNode.decodeMessage(message)?.format(treeNode.type) ?? [] const [compareStr, compareType] = - compare && compare.payload ? Base64Message.format(compare.payload, treeNode.type) : [undefined, undefined] + compare && compare.payload ? treeNode.decodeMessage(compare)?.format(treeNode.type) ?? [] : [] return (
diff --git a/app/src/components/TopicPlot.tsx b/app/src/components/TopicPlot.tsx index 0a3ce790..6e5bec64 100644 --- a/app/src/components/TopicPlot.tsx +++ b/app/src/components/TopicPlot.tsx @@ -26,23 +26,28 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array) { return filterUsingTimeRange(startTime, history.toArray()) .map((message: q.Message) => { - const [value, ignore] = message.payload ? Base64Message.format(message.payload, type) : [NaN, undefined] - // const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : NaN - return { x: message.received.getTime(), y: toPlottableValue(value) } + const decoded = node.decodeMessage(message)?.toUnicodeString() + return { x: message.received.getTime(), y: toPlottableValue(decoded) } }) .filter(data => !isNaN(data.y as any)) as any } -function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageHistory, dotPath: string, type: q.TopicDataType) { +function nodeDotPathToHistory( + startTime: number | undefined, + history: q.MessageHistory, + dotPath: string, + node: q.TreeNode +) { return filterUsingTimeRange(startTime, history.toArray()) .map((message: q.Message) => { let json: any = {} try { - json = message.payload ? JSON.parse(Base64Message.toUnicodeString(message.payload)) : {} - } catch (ignore) { } + const decoded = node.decodeMessage(message) + json = decoded ? JSON.parse(decoded.toUnicodeString()) : {} + } catch (ignore) {} const value = dotProp.get(json, dotPath) @@ -53,13 +58,15 @@ function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageH function TopicPlot(props: Props) { const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined - const data = React.useMemo( - () => - props.dotPath - ? nodeDotPathToHistory(startOffset, props.history, props.dotPath, props.node ? props.node.type : 'string') - : nodeToHistory(startOffset, props.history, props.node ? props.node.type : 'string'), - [props.history.last(), startOffset, props.dotPath] - ) + const data = React.useMemo(() => { + if (!props.node) { + return [] + } + + return props.dotPath + ? nodeDotPathToHistory(startOffset, props.history, props.dotPath, props.node) + : nodeToHistory(startOffset, props.history, props.node) + }, [props.history.last(), startOffset, props.dotPath]) return ( { if (!this.props.treeNode.message || !this.props.treeNode.message.payload) { return '' } + const [value = ''] = + this.props.treeNode.decodeMessage(this.props.treeNode.message)?.format(this.props.treeNode.type) ?? [] - const [value, ignore] = Base64Message.format(this.props.treeNode.message.payload, this.props.treeNode.type) return value.length > limit ? `${value.slice(0, limit)}…` : value } @@ -39,11 +39,11 @@ class TreeNodeTitle extends React.PureComponent { return this.props.treeNode.message && this.props.treeNode.message.payload && this.props.treeNode.message.length > 0 ? ( - - {' '} + + {' '} = {this.truncatedMessage()} - - ) : null + + ) : null } private renderExpander() { @@ -66,8 +66,9 @@ class TreeNodeTitle extends React.PureComponent { const messages = this.props.treeNode.leafMessageCount() const topicCount = this.props.treeNode.childTopicCount() return ( - {` (${topicCount} ${topicCount === 1 ? 'topic' : 'topics' - }, ${messages} ${messages === 1 ? 'message' : 'messages'})`} + {` (${topicCount} ${ + topicCount === 1 ? 'topic' : 'topics' + }, ${messages} ${messages === 1 ? 'message' : 'messages'})`} ) } diff --git a/app/src/components/UpdateNotifier.tsx b/app/src/components/UpdateNotifier.tsx index e68719df..c7b41724 100644 --- a/app/src/components/UpdateNotifier.tsx +++ b/app/src/components/UpdateNotifier.tsx @@ -1,7 +1,7 @@ -import * as compareVersions from 'compare-versions' -import * as electron from 'electron' -import * as os from 'os' -import * as React from 'react' +import compareVersions from 'compare-versions' +import electron from 'electron' +import os from 'os' +import React from 'react' import axios from 'axios' import Close from '@material-ui/icons/Close' import CloudDownload from '@material-ui/icons/CloudDownload' diff --git a/app/src/components/helper/DateFormatter.tsx b/app/src/components/helper/DateFormatter.tsx index 4e7fd6bd..523ebbaf 100644 --- a/app/src/components/helper/DateFormatter.tsx +++ b/app/src/components/helper/DateFormatter.tsx @@ -1,5 +1,5 @@ -import * as moment from 'moment' -import * as React from 'react' +import moment from 'moment' +import React from 'react' import { AppState } from '../../reducers' import { connect } from 'react-redux' diff --git a/app/tsconfig.json b/app/tsconfig.json index d2847b8c..ca7c7b8a 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -4,38 +4,26 @@ "noImplicitAny": true, "strictNullChecks": true, "strict": true, - "lib": [ - "es2017", - "dom" - ], + "lib": ["es2019", "dom"], "moduleResolution": "node", "outDir": "./build/", "sourceMap": true, "module": "esnext", - "target": "es2017", + "target": "ES2017", "jsx": "react", "paths": { - "react": [ - "./node_modules/@types/react" - ] + "react": ["./node_modules/@types/react"] }, - "types": [ - "react" - ], + "types": ["react"], "allowSyntheticDefaultImports": true, - "skipLibCheck": true + "skipLibCheck": true, + "esModuleInterop": true }, - "include": [ - "./src/**/*" - ], - "exclude": [ - "**/*.d.ts", - ".src/**/*.png", - "./node_modules" - ], + "include": ["./src/**/*"], + "exclude": ["**/*.d.ts", ".src/**/*.png", "./node_modules"], "awesomeTypescriptLoaderOptions": { "useCache": true, "transpileModule": true, "errorsAsWarnings": true } -} \ No newline at end of file +} diff --git a/app/webpack.config.js b/app/webpack.config.js index c522ac4b..543c51ef 100644 --- a/app/webpack.config.js +++ b/app/webpack.config.js @@ -54,7 +54,15 @@ module.exports = { // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. { test: /\.tsx?$/, - loader: 'ts-loader', + use: [ + { + loader: 'ts-loader', + // options: { + // configFile: './tsconfig.json', + // }, + }, + ], + exclude: /node_modules/, }, // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' }, @@ -96,4 +104,7 @@ module.exports = { // "react": "React", // "react-dom": "ReactDOM" }, + cache: { + type: 'filesystem', + }, } diff --git a/backend/src/ConfigStorage.ts b/backend/src/ConfigStorage.ts index 7da131ca..0859c8f3 100644 --- a/backend/src/ConfigStorage.ts +++ b/backend/src/ConfigStorage.ts @@ -1,7 +1,7 @@ -import * as FileAsync from 'lowdb/adapters/FileAsync' -import * as fs from 'fs-extra' -import * as lowdb from 'lowdb' -import * as path from 'path' +import FileAsync from 'lowdb/adapters/FileAsync' +import fs from 'fs-extra' +import lowdb from 'lowdb' +import path from 'path' import { backendRpc } from '../../events' import { storageClearEvent, storageLoadEvent, storageStoreEvent } from '../../events/StorageEvents' diff --git a/backend/src/DataSource/MqttSource.ts b/backend/src/DataSource/MqttSource.ts index e53d5a25..87c3db65 100644 --- a/backend/src/DataSource/MqttSource.ts +++ b/backend/src/DataSource/MqttSource.ts @@ -98,7 +98,7 @@ export class MqttSource implements DataSource { public publish(msg: MqttMessage) { if (this.client) { - this.client.publish(msg.topic, msg.payload ? Base64Message.toUnicodeString(msg.payload) : '', { + this.client.publish(msg.topic, msg.payload?.toBuffer() ?? '', { qos: msg.qos, retain: msg.retain, }) diff --git a/backend/src/Model/Base64Message.ts b/backend/src/Model/Base64Message.ts index dd10d66d..f0aa525f 100644 --- a/backend/src/Model/Base64Message.ts +++ b/backend/src/Model/Base64Message.ts @@ -3,93 +3,55 @@ import { Decoder } from './Decoder' import { TopicDataType } from './TreeNode' export class Base64Message { - private base64Message: string + public base64Message: string private unicodeValue: string + public error?: string public decoder: Decoder public length: number - private constructor(base64Str: string) { - this.base64Message = base64Str - this.unicodeValue = Base64.decode(base64Str) - this.length = base64Str.length + constructor(base64Str?: string, error?: string) { + this.base64Message = base64Str ?? '' + this.error = error + this.unicodeValue = Base64.decode(base64Str ?? '') + this.length = base64Str?.length ?? 0 this.decoder = Decoder.NONE } - public static toUnicodeString(message: Base64Message) { - return message.unicodeValue || '' + public toUnicodeString() { + return this.unicodeValue || '' } public static fromBuffer(buffer: Buffer) { return new Base64Message(buffer.toString('base64')) } + public toBuffer(): Buffer { + return Buffer.from(this.base64Message, 'base64') + } + public static fromString(str: string) { return new Base64Message(Base64.encode(str)) } /* Raw message conversions ('uint8' | 'uint16' | 'uint32' | 'uint64' | 'int8' | 'int16' | 'int32' | 'int64' | 'float' | 'double') */ - public static format(message: Base64Message | null, type: TopicDataType = 'string'): [string, 'json' | undefined] { - if (!message) { - return ['', undefined] - } - + public format(type: TopicDataType = 'string'): [string, 'json' | undefined] { try { switch (type) { case 'json': { - const json = JSON.parse(Base64Message.toUnicodeString(message)) + const json = JSON.parse(this.toUnicodeString()) return [JSON.stringify(json, undefined, ' '), 'json'] } case 'hex': { - const hex = Base64Message.toHex(message) + const hex = Base64Message.toHex(this) return [hex, undefined] } - case 'uint8': { - const uint = Base64Message.toUInt(message, 1) - return [uint ? uint : '', undefined] - } - case 'uint16': { - const uint = Base64Message.toUInt(message, 2) - return [uint ? uint : '', undefined] - } - case 'uint32': { - const uint = Base64Message.toUInt(message, 4) - return [uint ? uint : '', undefined] - } - case 'uint64': { - const uint = Base64Message.toUInt(message, 8) - return [uint ? uint : '', undefined] - } - case 'int8': { - const int = Base64Message.toInt(message, 1) - return [int ? int : '', undefined] - } - case 'int16': { - const int = Base64Message.toInt(message, 2) - return [int ? int : '', undefined] - } - case 'int32': { - const int = Base64Message.toInt(message, 4) - return [int ? int : '', undefined] - } - case 'int64': { - const int = Base64Message.toInt(message, 8) - return [int ? int : '', undefined] - } - case 'float': { - const float = Base64Message.toFloat(message, 4) - return [float ? float : '', undefined] - } - case 'double': { - const float = Base64Message.toFloat(message, 8) - return [float ? float : '', undefined] - } default: { - const str = Base64Message.toUnicodeString(message) + const str = this.toUnicodeString() return [str, undefined] } } } catch (error) { - const str = Base64Message.toUnicodeString(message) + const str = this.toUnicodeString() return [str, undefined] } } @@ -105,89 +67,6 @@ export class Base64Message { return str.trimRight() } - public static toUInt(message: Base64Message, bytes: number) { - const buf = Buffer.from(message.base64Message, 'base64') - - let str: String[] = [] - switch (bytes) { - case 1: - for (let index = 0; index < buf.length; index += bytes) { - str.push(buf.readUInt8(index).toString()) - } - break - case 2: - for (let index = 0; index < buf.length; index += bytes) { - str.push(buf.readUInt16LE(index).toString()) - } - break - case 4: - for (let index = 0; index < buf.length; index += bytes) { - str.push(buf.readUInt32LE(index).toString()) - } - break - case 8: - for (let index = 0; index < buf.length; index += bytes) { - str.push(buf.readBigUInt64LE(index).toString()) - } - break - default: - return undefined - } - return str.join(', ') - } - - public static toInt(message: Base64Message, bytes: number) { - const buf = Buffer.from(message.base64Message, 'base64') - - let str: String[] = [] - switch (bytes) { - case 1: - for (let index = 0; index < buf.length; index += bytes) { - str.push(buf.readInt8(index).toString()) - } - break - case 2: - for (let index = 0; index < buf.length; index += bytes) { - str.push(buf.readInt16LE(index).toString()) - } - break - case 4: - for (let index = 0; index < buf.length; index += bytes) { - str.push(buf.readInt32LE(index).toString()) - } - break - case 8: - for (let index = 0; index < buf.length; index += bytes) { - str.push(buf.readBigInt64LE(index).toString()) - } - break - default: - return undefined - } - return str.join(', ') - } - - public static toFloat(message: Base64Message, bytes: number) { - const buf = Buffer.from(message.base64Message, 'base64') - - let str: String[] = [] - switch (bytes) { - case 4: - for (let index = 0; index < buf.length; index += bytes) { - str.push(buf.readFloatLE(index).toString()) - } - break - case 8: - for (let index = 0; index < buf.length; index += bytes) { - str.push(buf.readDoubleLE(index).toString()) - } - break - default: - return undefined - } - return str.join(', ') - } - public static toDataUri(message: Base64Message, mimeType: string) { return `data:${mimeType};base64,${message.base64Message}` } diff --git a/backend/src/Model/TreeNode.ts b/backend/src/Model/TreeNode.ts index 7adcd982..f4957519 100644 --- a/backend/src/Model/TreeNode.ts +++ b/backend/src/Model/TreeNode.ts @@ -1,9 +1,31 @@ import { Destroyable } from './Destroyable' import { Edge, Message, RingBuffer, MessageHistory } from './' import { EventDispatcher } from '../../../events' +import { IDecoder, decoders } from './sparkplugb' +import { Base64Message } from './Base64Message' // export type TopicDataType = 'json' | 'string' | 'hex' | 'integer' | 'unsigned int' | 'floating point' -export type TopicDataType = 'json' | 'string' | 'hex' | 'uint8' | 'uint16' | 'uint32' | 'uint64' | 'int8' | 'int16' | 'int32' | 'int64' | 'float' | 'double' +export type TopicDataType = + | 'json' + | 'string' + | 'hex' + | 'uint8' + | 'uint16' + | 'uint32' + | 'uint64' + | 'int8' + | 'int16' + | 'int32' + | 'int64' + | 'float' + | 'double' + +function findDecoder(node: TreeNode): IDecoder | undefined { + return decoders.find( + decoder => + decoder.canDecodeTopic?.(node.path()) || (node.message?.payload && decoder.canDecodeData?.(node.message?.payload)) + ) +} export class TreeNode { public sourceEdge?: Edge @@ -22,6 +44,28 @@ export class TreeNode { public isTree = false public type: TopicDataType = 'json' + private _decoder?: IDecoder + + public decoderFormat?: string + + get decoder(): IDecoder | undefined { + if (!this._decoder) { + this._decoder = findDecoder(this) + } + return this._decoder + } + + set decoder(override: IDecoder | undefined) { + this._decoder = override + this.message && this.onMerge.dispatch() + } + + decodeMessage(message: Message): Base64Message | null { + const decoder = this.decoder + + return this.decoder && message.payload ? this.decoder.decode(message.payload, this.decoderFormat) : message.payload + } + private cachedPath?: string private cachedChildTopics?: Array> private cachedLeafMessageCount?: number @@ -157,7 +201,7 @@ export class TreeNode { public path(): string { if (!this.cachedPath) { - return this.branch() + this.cachedPath = this.branch() .map(node => node.sourceEdge && node.sourceEdge.name) .filter(name => name !== undefined) .join('/') diff --git a/backend/src/Model/TreeNodeFactory.ts b/backend/src/Model/TreeNodeFactory.ts index d1b2c52c..82faa2ad 100644 --- a/backend/src/Model/TreeNodeFactory.ts +++ b/backend/src/Model/TreeNodeFactory.ts @@ -1,6 +1,7 @@ import { Destroyable } from './Destroyable' import { Edge, Tree, TreeNode } from './' import { MqttMessage } from '../../../events' +import { Base64Message } from './Base64Message' export abstract class TreeNodeFactory { private static messageCounter = 0 @@ -30,6 +31,7 @@ export abstract class TreeNodeFactory { mqttMessage.retain node.setMessage({ ...mqttMessage, + payload: mqttMessage.payload && new Base64Message(mqttMessage.payload?.base64Message), length: mqttMessage.payload?.length ?? 0, received: receiveDate, messageNumber: this.messageCounter, diff --git a/backend/src/Model/sparkplugb.ts b/backend/src/Model/sparkplugb.ts index d3625b8b..91c89360 100644 --- a/backend/src/Model/sparkplugb.ts +++ b/backend/src/Model/sparkplugb.ts @@ -1,21 +1,98 @@ import { Base64Message } from './Base64Message' import { Decoder } from './Decoder' import { get } from 'sparkplug-payload' -var sparkplug = get("spBv1.0") +var sparkplug = get('spBv1.0') -export const SparkplugDecoder = { - decode(input: Buffer): Base64Message { +export interface IDecoder { + /** + * Can be used to + * @param topic + */ + formats: T[] + canDecodeTopic?(topic: string): boolean + canDecodeData?(data: Base64Message): boolean + decode(input: Base64Message, format: T | string | undefined): Base64Message + + /** + * If this is just an intermediate decoder, next-decoder can be defined + */ + nextDecoder?: IDecoder +} + +export const SparkplugDecoder: IDecoder = { + formats: ['Sparkplug'], + canDecodeTopic(topic: string) { + return !!topic.match(/spBv1\.0\/[^/]+\/(DDATA|NDATA|NCMD|DCMD|NBIRTH|DBIRTH|NDEATH|DDEATH\/[^/]+\/)/u) + }, + decode(input: Base64Message): Base64Message { try { - const message = Base64Message.fromString(JSON.stringify( - // @ts-ignore - sparkplug.decodePayload(new Uint8Array(input))) + const message = Base64Message.fromString( + JSON.stringify( + // @ts-ignore + sparkplug.decodePayload(new Uint8Array(input.toBuffer())) + ) ) message.decoder = Decoder.SPARKPLUG return message } catch { - const message = Base64Message.fromString("Failed to decode sparkplugb payload") + const message = new Base64Message(undefined, 'Failed to decode sparkplugb payload') message.decoder = Decoder.NONE return message } }, } + +export const StringDecoder: IDecoder = { + formats: ['string'], + decode(input: Base64Message): Base64Message { + return input + }, +} + +type BinaryFormats = + | 'int8' + | 'int16' + | 'int32' + | 'int64' + | 'uint8' + | 'uint16' + | 'uint32' + | 'uint64' + | 'float' + | 'double' + +/** + * Binary decode primitive binary data type and arrays of these + */ +export const BinaryDecoder: IDecoder = { + formats: ['int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64', 'float', 'double'], + decode(input: Base64Message, format: BinaryFormats): Base64Message { + const decodingOption = { + int8: [Buffer.prototype.readInt8, 1], + int16: [Buffer.prototype.readInt16LE, 2], + int32: [Buffer.prototype.readInt32LE, 4], + int64: [Buffer.prototype.readBigInt64LE, 8], + uint8: [Buffer.prototype.readUint8, 1], + uint16: [Buffer.prototype.readUint16LE, 2], + uint32: [Buffer.prototype.readUint32LE, 4], + uint64: [Buffer.prototype.readBigUint64LE, 8], + float: [Buffer.prototype.readFloatLE, 4], + double: [Buffer.prototype.readDoubleLE, 8], + } as const + + const [readNumber, bytesToRead] = decodingOption[format] + + const buf = input.toBuffer() + let str: String[] = [] + if (buf.length % bytesToRead !== 0) { + return new Base64Message(undefined, 'Data type does not align with message') + } + for (let index = 0; index < buf.length; index += bytesToRead) { + str.push((readNumber as any).apply(buf, [index]).toString()) + } + + return Base64Message.fromString(JSON.stringify(str.length === 1 ? str[0] : str)) + }, +} + +export const decoders = [SparkplugDecoder, BinaryDecoder, StringDecoder] as const diff --git a/backend/src/index.ts b/backend/src/index.ts index 0502ae3b..5ca2b1df 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -48,12 +48,7 @@ export class ConnectionManager { } let decoded_payload = null - // spell-checker: disable-next-line - if (topic.match(/spBv1\.0\/[^/]+\/(DDATA|NDATA|NCMD|DCMD|NBIRTH|DBIRTH|NDEATH|DDEATH\/[^/]+\/)/u)) { - decoded_payload = SparkplugDecoder.decode(buffer) - } else { - decoded_payload = Base64Message.fromBuffer(buffer) - } + decoded_payload = Base64Message.fromBuffer(buffer) backendEvents.emit(messageEvent, { topic, diff --git a/src/electron.ts b/src/electron.ts index 94118de8..28a96aeb 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -19,7 +19,7 @@ registerCrashReporter() // const electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5', buildOptions) // } -app.commandLine.appendSwitch('--no-sandbox') +app.commandLine.appendSwitch('--no-sandbox --disable-dev-shm-usage') app.whenReady().then(() => { backendRpc.on(makeOpenDialogRpc(), async request => { return dialog.showOpenDialog(BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0], request) @@ -70,7 +70,7 @@ async function createWindow() { }) console.log('icon path', iconPath) - + mainWindow.webContents.openDevTools({ mode: 'detach' }) // Load the index.html of the app. if (isDev()) { mainWindow.loadURL('http://localhost:8080') diff --git a/tsconfig.json b/tsconfig.json index 74813a89..db8a514c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,15 +9,11 @@ "moduleResolution": "node", "sourceRoot": "src/", "target": "ES2017", - "lib": [ - "es2017", - "dom" - ], + "lib": ["ES2017", "dom"], "sourceMap": true, - "types": [ - "node" - ], - "skipLibCheck": true + "types": ["node"], + "skipLibCheck": true, + "esModuleInterop": true }, "include": [ "src/electron.ts", @@ -26,7 +22,5 @@ "src/spec/leakTest.ts", "scripts/*.ts" ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "exclude": ["node_modules"] +} From 1f23c6548403383006975d254609503a2ae2be2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Dalfors?= Date: Tue, 21 May 2024 15:17:18 +0200 Subject: [PATCH 05/18] Stop click event propagation prevent panel from collapsing --- app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx b/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx index d71a891c..0ab317dc 100644 --- a/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx +++ b/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx @@ -49,6 +49,7 @@ export const TopicTypeButton = (props: { node?: q.TreeNode }) => { }, []) const handleToggle = (event: React.MouseEvent) => { + event.stopPropagation() if (open === true) { return } From 97fedcba082e01fbaf489f352eaddce865066c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Dalfors?= Date: Tue, 21 May 2024 15:26:43 +0200 Subject: [PATCH 06/18] fix sparkplug topic regexp --- backend/src/Model/sparkplugb.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/Model/sparkplugb.ts b/backend/src/Model/sparkplugb.ts index 91c89360..439c0dd3 100644 --- a/backend/src/Model/sparkplugb.ts +++ b/backend/src/Model/sparkplugb.ts @@ -22,7 +22,7 @@ export interface IDecoder { export const SparkplugDecoder: IDecoder = { formats: ['Sparkplug'], canDecodeTopic(topic: string) { - return !!topic.match(/spBv1\.0\/[^/]+\/(DDATA|NDATA|NCMD|DCMD|NBIRTH|DBIRTH|NDEATH|DDEATH\/[^/]+\/)/u) + return !!topic.match(/^spBv1\.0\/[^/]+\/[ND](DATA|CMD|DEATH|BIRTH)\/[^/]+(\/[^/]+)?$/u) }, decode(input: Base64Message): Base64Message { try { From 1ecb53b397e32ad48d6687c6c99e61090758178f Mon Sep 17 00:00:00 2001 From: Thomas Nordquist Date: Wed, 22 May 2024 09:04:06 +0200 Subject: [PATCH 07/18] fix: update react when decoder has been overriden --- app/src/components/Sidebar/Sidebar.tsx | 2 +- backend/src/Model/TreeNode.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/components/Sidebar/Sidebar.tsx b/app/src/components/Sidebar/Sidebar.tsx index 4e257cf9..c6bbdd7e 100644 --- a/app/src/components/Sidebar/Sidebar.tsx +++ b/app/src/components/Sidebar/Sidebar.tsx @@ -31,7 +31,7 @@ function useUpdateNodeWhenNodeReceivesUpdates(node?: q.TreeNode) { const [lastUpdate, setLastUpdate] = useState(0) const updateNode = useCallback( throttle(() => { - setLastUpdate(node ? node.lastUpdate : 0) + setLastUpdate(Date.now()) }, 300), [node] ) diff --git a/backend/src/Model/TreeNode.ts b/backend/src/Model/TreeNode.ts index f4957519..227c27b3 100644 --- a/backend/src/Model/TreeNode.ts +++ b/backend/src/Model/TreeNode.ts @@ -57,7 +57,9 @@ export class TreeNode { set decoder(override: IDecoder | undefined) { this._decoder = override - this.message && this.onMerge.dispatch() + + // Hack to force frontend to update + this.message && this.onMessage.dispatch(this.message) } decodeMessage(message: Message): Base64Message | null { From b3a37e4794caaaeddc3801f8303c721094644ce0 Mon Sep 17 00:00:00 2001 From: Thomas Nordquist Date: Wed, 22 May 2024 14:44:06 +0200 Subject: [PATCH 08/18] chore: refactor --- app/src/actions/Tree.ts | 9 +- .../ConnectionSetup/ProfileList/index.tsx | 2 +- .../Sidebar/TopicPanel/TopicPanel.tsx | 3 +- .../Sidebar/TopicPanel/TopicTypeButton.tsx | 61 +- .../Sidebar/ValueRenderer/MessageHistory.tsx | 174 ++--- .../Sidebar/ValueRenderer/ValuePanel.tsx | 9 +- .../Sidebar/ValueRenderer/ValueRenderer.tsx | 70 +- app/src/components/TopicPlot.tsx | 18 +- .../Tree/TreeNode/TreeNodeTitle.tsx | 57 +- .../Tree/TreeNode/effects/useViewModel.tsx | 18 + .../effects/useViewModelSubscriptions.tsx | 44 +- app/src/components/helper/Copy.tsx | 5 +- app/src/components/hooks/useDecoder.ts | 28 + app/src/components/hooks/useSubscription.ts | 10 + app/src/model/LegacyConnectionSettings.ts | 1 - app/src/model/TopicViewModel.ts | 60 +- app/yarn.lock | 673 ++++++++++++------ backend/src/Model/TreeNode.ts | 31 - backend/src/Model/sparkplugb.ts | 5 - yarn.lock | 291 +++++++- 20 files changed, 1041 insertions(+), 528 deletions(-) create mode 100644 app/src/components/Tree/TreeNode/effects/useViewModel.tsx create mode 100644 app/src/components/hooks/useDecoder.ts create mode 100644 app/src/components/hooks/useSubscription.ts diff --git a/app/src/actions/Tree.ts b/app/src/actions/Tree.ts index 0dd1896b..25ddcc04 100644 --- a/app/src/actions/Tree.ts +++ b/app/src/actions/Tree.ts @@ -33,13 +33,8 @@ const debouncedSelectTopic = debounce( setTopicDispatch = setTopic(topic.path()) } - if (previouslySelectedTopic && previouslySelectedTopic.viewModel) { - previouslySelectedTopic.viewModel.setSelected(false) - } - - if (topic.viewModel) { - topic.viewModel.setSelected(true) - } + previouslySelectedTopic?.viewModel?.setSelected(false) + topic.viewModel?.setSelected(true) const selectTreeTopicDispatch = { selectedTopic: topic, diff --git a/app/src/components/ConnectionSetup/ProfileList/index.tsx b/app/src/components/ConnectionSetup/ProfileList/index.tsx index eb76fc1a..8efa4619 100644 --- a/app/src/components/ConnectionSetup/ProfileList/index.tsx +++ b/app/src/components/ConnectionSetup/ProfileList/index.tsx @@ -7,7 +7,7 @@ import { connect } from 'react-redux' import { connectionManagerActions } from '../../../actions' import { ConnectionOptions } from '../../../model/ConnectionOptions' import { KeyCodes } from '../../../utils/KeyCodes' -import { List, ListSubheader } from '@material-ui/core' +import { List } from '@material-ui/core' import { Theme, withStyles } from '@material-ui/core/styles' import { useGlobalKeyEventHandler } from '../../../effects/useGlobalKeyEventHandler' diff --git a/app/src/components/Sidebar/TopicPanel/TopicPanel.tsx b/app/src/components/Sidebar/TopicPanel/TopicPanel.tsx index 253f8f17..504f860e 100644 --- a/app/src/components/Sidebar/TopicPanel/TopicPanel.tsx +++ b/app/src/components/Sidebar/TopicPanel/TopicPanel.tsx @@ -12,7 +12,6 @@ import { sidebarActions } from '../../../actions' const TopicPanel = (props: { node?: q.TreeNode; actions: typeof sidebarActions }) => { const { node } = props - console.log(node && node.path()) const copyTopic = node ? : null @@ -35,7 +34,7 @@ const TopicPanel = (props: { node?: q.TreeNode; actions: typeof sidebarActi ), - [node, node && node.childTopicCount()] + [node, node?.childTopicCount()] ) } diff --git a/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx b/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx index 0ab317dc..c982f63f 100644 --- a/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx +++ b/app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx @@ -11,23 +11,6 @@ import WarningRounded from '@material-ui/icons/WarningRounded' import { IDecoder, decoders } from '../../../../../backend/src/Model/sparkplugb' import { Tooltip } from '@material-ui/core' -// const options: q.TopicDataType[] = ['json', 'string', 'hex', 'integer', 'unsigned int', 'floating point'] -const options: q.TopicDataType[] = [ - 'json', - 'string', - 'hex', - 'uint8', - 'uint16', - 'uint32', - 'uint64', - 'int8', - 'int16', - 'int32', - 'int64', - 'float', - 'double', -] - export const TopicTypeButton = (props: { node?: q.TreeNode }) => { const { node } = props if (!node || !node.message || !node.message.payload) { @@ -39,34 +22,40 @@ export const TopicTypeButton = (props: { node?: q.TreeNode }) => { const [anchorEl, setAnchorEl] = React.useState(null) const [open, setOpen] = React.useState(false) - const selectOption = useCallback((decoder: IDecoder, format: string) => { - if (!node) { - return - } - node.decoder = decoder - node.decoderFormat = format - setOpen(false) - }, []) + const selectOption = useCallback( + (decoder: IDecoder, format: string) => { + if (!node) { + return + } - const handleToggle = (event: React.MouseEvent) => { - event.stopPropagation() - if (open === true) { - return - } - setAnchorEl(event.currentTarget) - setOpen(prevOpen => !prevOpen) - } + node.viewModel.decoder = { decoder, format } + setOpen(false) + }, + [node] + ) + + const handleToggle = useCallback( + (event: React.MouseEvent) => { + event.stopPropagation() + if (open === true) { + return + } + setAnchorEl(event.currentTarget) + setOpen(prevOpen => !prevOpen) + }, + [open] + ) - const handleClose = (event: React.MouseEvent) => { + const handleClose = useCallback((event: React.MouseEvent) => { if (anchorEl && anchorEl.contains(event.target as HTMLElement)) { return } setOpen(false) - } + }, []) return (