diff --git a/app/src/actions/Settings.ts b/app/src/actions/Settings.ts index d222afc..80282e6 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 c5c210d..4006c90 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 129b24e..253f8f1 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 3fcb439..d71a891 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 ( - - + ) -} \ No newline at end of file +} + +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} + ) +} diff --git a/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx b/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx index c9590e4..fbbc619 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 de3bb7b..d17e3c7 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 4e5d784..232a0ef 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 0a3ce79..6e5bec6 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 e68719d..c7b4172 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 4e7fd6b..523ebba 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 d2847b8..ca7c7b8 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 c522ac4..543c51e 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 7da131c..0859c8f 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 e53d5a2..87c3db6 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 dd10d66..f0aa525 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 7adcd98..f495751 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 d1b2c52..82faa2a 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 d3625b8..91c8936 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 0502ae3..5ca2b1d 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 94118de..28a96ae 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 74813a8..db8a514 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"] +}