chore: decode data in frontend

This commit is contained in:
Thomas Nordquist
2024-05-21 09:22:11 +02:00
parent 10aae59c92
commit 980072f680
22 changed files with 303 additions and 283 deletions

View File

@@ -68,13 +68,14 @@ export const selectTopicWithMouseOver = (doSelect: boolean) => (dispatch: Dispat
dispatch(storeSettings()) dispatch(storeSettings())
} }
export const setValueDisplayMode = (valueRendererDisplayMode: ValueRendererDisplayMode) => (dispatch: Dispatch<any>) => { export const setValueDisplayMode =
dispatch({ (valueRendererDisplayMode: ValueRendererDisplayMode) => (dispatch: Dispatch<any>) => {
valueRendererDisplayMode, dispatch({
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE, valueRendererDisplayMode,
}) type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE,
dispatch(storeSettings()) })
} dispatch(storeSettings())
}
export const toggleHighlightTopicUpdates = () => (dispatch: Dispatch<any>) => { export const toggleHighlightTopicUpdates = () => (dispatch: Dispatch<any>) => {
dispatch({ dispatch({
@@ -117,7 +118,7 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
const messageMatches = const messageMatches =
node.message && node.message &&
node.message.payload && node.message.payload &&
Base64Message.toUnicodeString(node.message.payload).toLowerCase().indexOf(filterStr) !== -1 node.message.payload.toUnicodeString().toLowerCase().indexOf(filterStr) !== -1
return Boolean(messageMatches) return Boolean(messageMatches)
} }

View File

@@ -123,7 +123,7 @@ function renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
return null 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 let value = node.message && node.message.payload ? parseFloat(str) : NaN
value = !isNaN(value) ? abbreviate(value) : str value = !isNaN(value) ? abbreviate(value) : str

View File

@@ -23,13 +23,6 @@ const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActi
props.actions.clearTopic(topic, recursive) props.actions.clearTopic(topic, recursive)
}, []) }, [])
const setTopicType = useCallback((node?: q.TreeNode<any>, type: q.TopicDataType = 'string') => {
if (!node) {
return
}
node.type = type
}, [])
return useMemo( return useMemo(
() => ( () => (
<Panel disabled={!Boolean(node)}> <Panel disabled={!Boolean(node)}>
@@ -37,7 +30,7 @@ const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActi
Topic {copyTopic} Topic {copyTopic}
<TopicDeleteButton node={node} deleteTopicAction={deleteTopic} /> <TopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} /> <RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<TopicTypeButton node={node} setTopicType={setTopicType} /> <TopicTypeButton node={node} />
</span> </span>
<Topic node={node} /> <Topic node={node} />
</Panel> </Panel>

View File

@@ -1,46 +1,59 @@
import React, { useCallback } from 'react' import React, { useCallback, useMemo } from 'react'
import * as q from '../../../../../backend/src/Model' 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 ClickAwayListener from '@material-ui/core/ClickAwayListener'
import Grow from '@material-ui/core/Grow' import Grow from '@material-ui/core/Grow'
import Button from '@material-ui/core/Button'
import Paper from '@material-ui/core/Paper' import Paper from '@material-ui/core/Paper'
import Popper from '@material-ui/core/Popper' import Popper from '@material-ui/core/Popper'
import MenuItem from '@material-ui/core/MenuItem' import MenuItem from '@material-ui/core/MenuItem'
import MenuList from '@material-ui/core/MenuList' 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', '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: { export const TopicTypeButton = (props: { node?: q.TreeNode<any> }) => {
node?: q.TreeNode<any>
setTopicType: (node: q.TreeNode<any>, type: q.TopicDataType) => void
}) => {
const { node } = props const { node } = props
if (!node || !node.message || !node.message.payload) { if (!node || !node.message || !node.message.payload) {
return null return null
} }
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const options = decoders.flatMap(decoder => decoder.formats.map(format => [decoder, format] as const))
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const handleMenuItemClick = useCallback( const selectOption = useCallback((decoder: IDecoder, format: string) => {
(mouseEvent: React.MouseEvent, element: q.TreeNode<any>, type: q.TopicDataType) => { if (!node) {
if (!element || !type) { return
return }
} node.decoder = decoder
props.setTopicType(element, type as q.TopicDataType) node.decoderFormat = format
setOpen(false) setOpen(false)
}, }, [])
[props.setTopicType]
)
const handleToggle = (event: React.MouseEvent<HTMLElement>) => { const handleToggle = (event: React.MouseEvent<HTMLElement>) => {
if (open === true) { if (open === true) {
return return
} }
setAnchorEl(event.currentTarget) setAnchorEl(event.currentTarget)
setOpen((prevOpen) => !prevOpen) setOpen(prevOpen => !prevOpen)
} }
const handleClose = (event: React.MouseEvent<Document, MouseEvent>) => { const handleClose = (event: React.MouseEvent<Document, MouseEvent>) => {
@@ -51,8 +64,8 @@ export const TopicTypeButton = (props: {
} }
return ( return (
<CustomIconButton tooltip="" onClick={handleToggle}> <Button onClick={handleToggle}>
<Code /> {props.node?.decoderFormat ?? props.node?.type}
<Popper open={open} anchorEl={anchorEl} role={undefined} transition> <Popper open={open} anchorEl={anchorEl} role={undefined} transition>
{({ TransitionProps, placement }) => ( {({ TransitionProps, placement }) => (
<Grow <Grow
@@ -64,13 +77,13 @@ export const TopicTypeButton = (props: {
<Paper> <Paper>
<ClickAwayListener onClickAway={handleClose}> <ClickAwayListener onClickAway={handleClose}>
<MenuList id="topicTypeMode"> <MenuList id="topicTypeMode">
{options.map((option, index) => ( {options.map(([decoder, format], index) => (
<MenuItem <MenuItem
key={option} key={format}
selected={node && option === node.type} selected={node && format === node.type}
onClick={(event) => handleMenuItemClick(event, node, option)} onClick={() => selectOption(decoder, format)}
> >
{option} <DecoderStatus decoder={decoder} format={format} node={node} />
</MenuItem> </MenuItem>
))} ))}
</MenuList> </MenuList>
@@ -79,6 +92,22 @@ export const TopicTypeButton = (props: {
</Grow> </Grow>
)} )}
</Popper> </Popper>
</CustomIconButton> </Button>
)
}
function DecoderStatus({ node, decoder, format }: { node: q.TreeNode<any>; decoder: IDecoder; format: string }) {
const decoded = useMemo(() => {
return node.message?.payload && decoder.decode(node.message?.payload, format)
}, [node.message, decoder, format])
return decoded?.error ? (
<Tooltip title={decoded.error}>
<div>
{format} <WarningRounded />
</div>
</Tooltip>
) : (
<>{format}</>
) )
} }

View File

@@ -82,9 +82,10 @@ class MessageHistory extends React.PureComponent<Props, State> {
const history = node.messageHistory.toArray() const history = node.messageHistory.toArray()
let previousMessage: q.Message | undefined = node.message let previousMessage: q.Message | undefined = node.message
const historyElements = [...history].reverse().map((message, idx) => { 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 = { const element = {
value, value: value ?? '',
key: `${message.messageNumber}-${message.received}`, key: `${message.messageNumber}-${message.received}`,
title: ( title: (
<span> <span>
@@ -102,7 +103,7 @@ class MessageHistory extends React.PureComponent<Props, State> {
<MessageId message={message} /> <MessageId message={message} />
</span> </span>
<div style={{ float: 'right' }}> <div style={{ float: 'right' }}>
<Copy value={value ? value : ''} /> <Copy value={value ?? ''} />
</div> </div>
</span> </span>
), ),
@@ -112,7 +113,8 @@ class MessageHistory extends React.PureComponent<Props, State> {
return element 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) const isMessagePlottable = isPlottable(value)
return ( return (
<div> <div>

View File

@@ -85,10 +85,9 @@ function ValuePanel(props: Props) {
[compareMessage] [compareMessage]
) )
const [value, ignore] = node && node.message && node.message.payload ? Base64Message.format(node.message.payload, node.type) : [null, undefined] const [value] =
const copyValue = value ? ( node && node.message && node.message.payload ? node.message.payload?.format(node.type) : [null, undefined]
<Copy value={value} /> const copyValue = value ? <Copy value={value} /> : null
) : null
return ( return (
<Panel> <Panel>

View File

@@ -2,7 +2,6 @@ import * as q from '../../../../../backend/src/Model'
import * as React from 'react' import * as React from 'react'
import CodeDiff from '../CodeDiff' import CodeDiff from '../CodeDiff'
import { AppState } from '../../../reducers' import { AppState } from '../../../reducers'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { ValueRendererDisplayMode } from '../../../reducers/Settings' import { ValueRendererDisplayMode } from '../../../reducers/Settings'
import { Fade } from '@material-ui/core' import { Fade } from '@material-ui/core'
@@ -47,9 +46,8 @@ class ValueRenderer extends React.Component<Props, State> {
const previousMessage = previousMessages[previousMessages.length - 2] const previousMessage = previousMessages[previousMessages.length - 2]
const compareMessage = compare || previousMessage || message const compareMessage = compare || previousMessage || message
const compareValue = compareMessage.payload || message.payload const [currentStr, currentType] = treeNode.decodeMessage(message)?.format(treeNode.type) ?? []
const [currentStr, currentType] = Base64Message.format(message.payload, treeNode.type) const [compareStr, compareType] = treeNode.decodeMessage(compareMessage)?.format(treeNode.type) ?? []
const [compareStr, compareType] = Base64Message.format(compareValue, treeNode.type)
const language = currentType === compareType && compareType === 'json' ? 'json' : undefined const language = currentType === compareType && compareType === 'json' ? 'json' : undefined
@@ -61,9 +59,9 @@ class ValueRenderer extends React.Component<Props, State> {
return return
} }
const [currentStr, currentType] = Base64Message.format(message.payload, treeNode.type) const [currentStr, currentType] = treeNode.decodeMessage(message)?.format(treeNode.type) ?? []
const [compareStr, compareType] = const [compareStr, compareType] =
compare && compare.payload ? Base64Message.format(compare.payload, treeNode.type) : [undefined, undefined] compare && compare.payload ? treeNode.decodeMessage(compare)?.format(treeNode.type) ?? [] : []
return ( return (
<div> <div>

View File

@@ -26,23 +26,28 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
return data return data
} }
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory, type: q.TopicDataType) { function nodeToHistory(startTime: number | undefined, history: q.MessageHistory, node: q.TreeNode<any>) {
return filterUsingTimeRange(startTime, history.toArray()) return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => { .map((message: q.Message) => {
const [value, ignore] = message.payload ? Base64Message.format(message.payload, type) : [NaN, undefined] const decoded = node.decodeMessage(message)?.toUnicodeString()
// const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : NaN return { x: message.received.getTime(), y: toPlottableValue(decoded) }
return { x: message.received.getTime(), y: toPlottableValue(value) }
}) })
.filter(data => !isNaN(data.y as any)) as any .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<any>
) {
return filterUsingTimeRange(startTime, history.toArray()) return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => { .map((message: q.Message) => {
let json: any = {} let json: any = {}
try { try {
json = message.payload ? JSON.parse(Base64Message.toUnicodeString(message.payload)) : {} const decoded = node.decodeMessage(message)
} catch (ignore) { } json = decoded ? JSON.parse(decoded.toUnicodeString()) : {}
} catch (ignore) {}
const value = dotProp.get(json, dotPath) const value = dotProp.get(json, dotPath)
@@ -53,13 +58,15 @@ function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageH
function TopicPlot(props: Props) { function TopicPlot(props: Props) {
const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined
const data = React.useMemo( const data = React.useMemo(() => {
() => if (!props.node) {
props.dotPath return []
? 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] return props.dotPath
) ? nodeDotPathToHistory(startOffset, props.history, props.dotPath, props.node)
: nodeToHistory(startOffset, props.history, props.node)
}, [props.history.last(), startOffset, props.dotPath])
return ( return (
<PlotHistory <PlotHistory

View File

@@ -1,6 +1,5 @@
import * as q from '../../../../../backend/src/Model' import * as q from '../../../../../backend/src/Model'
import React, { memo } from 'react' import React, { memo } from 'react'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { Theme, withStyles } from '@material-ui/core' import { Theme, withStyles } from '@material-ui/core'
import { TopicViewModel } from '../../../model/TopicViewModel' import { TopicViewModel } from '../../../model/TopicViewModel'
@@ -30,8 +29,9 @@ class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
if (!this.props.treeNode.message || !this.props.treeNode.message.payload) { if (!this.props.treeNode.message || !this.props.treeNode.message.payload) {
return '' 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 return value.length > limit ? `${value.slice(0, limit)}` : value
} }
@@ -39,11 +39,11 @@ class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
return this.props.treeNode.message && return this.props.treeNode.message &&
this.props.treeNode.message.payload && this.props.treeNode.message.payload &&
this.props.treeNode.message.length > 0 ? ( this.props.treeNode.message.length > 0 ? (
<span key="value" className={this.props.classes.value}> <span key="value" className={this.props.classes.value}>
{' '} {' '}
= {this.truncatedMessage()} = {this.truncatedMessage()}
</span> </span>
) : null ) : null
} }
private renderExpander() { private renderExpander() {
@@ -66,8 +66,9 @@ class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
const messages = this.props.treeNode.leafMessageCount() const messages = this.props.treeNode.leafMessageCount()
const topicCount = this.props.treeNode.childTopicCount() const topicCount = this.props.treeNode.childTopicCount()
return ( return (
<span key="metadata" className={this.props.classes.collapsedSubnodes}>{` (${topicCount} ${topicCount === 1 ? 'topic' : 'topics' <span key="metadata" className={this.props.classes.collapsedSubnodes}>{` (${topicCount} ${
}, ${messages} ${messages === 1 ? 'message' : 'messages'})`}</span> topicCount === 1 ? 'topic' : 'topics'
}, ${messages} ${messages === 1 ? 'message' : 'messages'})`}</span>
) )
} }

View File

@@ -1,7 +1,7 @@
import * as compareVersions from 'compare-versions' import compareVersions from 'compare-versions'
import * as electron from 'electron' import electron from 'electron'
import * as os from 'os' import os from 'os'
import * as React from 'react' import React from 'react'
import axios from 'axios' import axios from 'axios'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import CloudDownload from '@material-ui/icons/CloudDownload' import CloudDownload from '@material-ui/icons/CloudDownload'

View File

@@ -1,5 +1,5 @@
import * as moment from 'moment' import moment from 'moment'
import * as React from 'react' import React from 'react'
import { AppState } from '../../reducers' import { AppState } from '../../reducers'
import { connect } from 'react-redux' import { connect } from 'react-redux'

View File

@@ -4,35 +4,23 @@
"noImplicitAny": true, "noImplicitAny": true,
"strictNullChecks": true, "strictNullChecks": true,
"strict": true, "strict": true,
"lib": [ "lib": ["es2019", "dom"],
"es2017",
"dom"
],
"moduleResolution": "node", "moduleResolution": "node",
"outDir": "./build/", "outDir": "./build/",
"sourceMap": true, "sourceMap": true,
"module": "esnext", "module": "esnext",
"target": "es2017", "target": "ES2017",
"jsx": "react", "jsx": "react",
"paths": { "paths": {
"react": [ "react": ["./node_modules/@types/react"]
"./node_modules/@types/react"
]
}, },
"types": [ "types": ["react"],
"react"
],
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"skipLibCheck": true "skipLibCheck": true,
"esModuleInterop": true
}, },
"include": [ "include": ["./src/**/*"],
"./src/**/*" "exclude": ["**/*.d.ts", ".src/**/*.png", "./node_modules"],
],
"exclude": [
"**/*.d.ts",
".src/**/*.png",
"./node_modules"
],
"awesomeTypescriptLoaderOptions": { "awesomeTypescriptLoaderOptions": {
"useCache": true, "useCache": true,
"transpileModule": true, "transpileModule": true,

View File

@@ -54,7 +54,15 @@ module.exports = {
// All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
{ {
test: /\.tsx?$/, 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'. // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' }, { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
@@ -96,4 +104,7 @@ module.exports = {
// "react": "React", // "react": "React",
// "react-dom": "ReactDOM" // "react-dom": "ReactDOM"
}, },
cache: {
type: 'filesystem',
},
} }

View File

@@ -1,7 +1,7 @@
import * as FileAsync from 'lowdb/adapters/FileAsync' import FileAsync from 'lowdb/adapters/FileAsync'
import * as fs from 'fs-extra' import fs from 'fs-extra'
import * as lowdb from 'lowdb' import lowdb from 'lowdb'
import * as path from 'path' import path from 'path'
import { backendRpc } from '../../events' import { backendRpc } from '../../events'
import { storageClearEvent, storageLoadEvent, storageStoreEvent } from '../../events/StorageEvents' import { storageClearEvent, storageLoadEvent, storageStoreEvent } from '../../events/StorageEvents'

View File

@@ -98,7 +98,7 @@ export class MqttSource implements DataSource<MqttOptions> {
public publish(msg: MqttMessage) { public publish(msg: MqttMessage) {
if (this.client) { 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, qos: msg.qos,
retain: msg.retain, retain: msg.retain,
}) })

View File

@@ -3,93 +3,55 @@ import { Decoder } from './Decoder'
import { TopicDataType } from './TreeNode' import { TopicDataType } from './TreeNode'
export class Base64Message { export class Base64Message {
private base64Message: string public base64Message: string
private unicodeValue: string private unicodeValue: string
public error?: string
public decoder: Decoder public decoder: Decoder
public length: number public length: number
private constructor(base64Str: string) { constructor(base64Str?: string, error?: string) {
this.base64Message = base64Str this.base64Message = base64Str ?? ''
this.unicodeValue = Base64.decode(base64Str) this.error = error
this.length = base64Str.length this.unicodeValue = Base64.decode(base64Str ?? '')
this.length = base64Str?.length ?? 0
this.decoder = Decoder.NONE this.decoder = Decoder.NONE
} }
public static toUnicodeString(message: Base64Message) { public toUnicodeString() {
return message.unicodeValue || '' return this.unicodeValue || ''
} }
public static fromBuffer(buffer: Buffer) { public static fromBuffer(buffer: Buffer) {
return new Base64Message(buffer.toString('base64')) return new Base64Message(buffer.toString('base64'))
} }
public toBuffer(): Buffer {
return Buffer.from(this.base64Message, 'base64')
}
public static fromString(str: string) { public static fromString(str: string) {
return new Base64Message(Base64.encode(str)) return new Base64Message(Base64.encode(str))
} }
/* Raw message conversions ('uint8' | 'uint16' | 'uint32' | 'uint64' | 'int8' | 'int16' | 'int32' | 'int64' | 'float' | 'double') */ /* 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] { public format(type: TopicDataType = 'string'): [string, 'json' | undefined] {
if (!message) {
return ['', undefined]
}
try { try {
switch (type) { switch (type) {
case 'json': { case 'json': {
const json = JSON.parse(Base64Message.toUnicodeString(message)) const json = JSON.parse(this.toUnicodeString())
return [JSON.stringify(json, undefined, ' '), 'json'] return [JSON.stringify(json, undefined, ' '), 'json']
} }
case 'hex': { case 'hex': {
const hex = Base64Message.toHex(message) const hex = Base64Message.toHex(this)
return [hex, undefined] 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: { default: {
const str = Base64Message.toUnicodeString(message) const str = this.toUnicodeString()
return [str, undefined] return [str, undefined]
} }
} }
} catch (error) { } catch (error) {
const str = Base64Message.toUnicodeString(message) const str = this.toUnicodeString()
return [str, undefined] return [str, undefined]
} }
} }
@@ -105,89 +67,6 @@ export class Base64Message {
return str.trimRight() 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) { public static toDataUri(message: Base64Message, mimeType: string) {
return `data:${mimeType};base64,${message.base64Message}` return `data:${mimeType};base64,${message.base64Message}`
} }

View File

@@ -1,9 +1,31 @@
import { Destroyable } from './Destroyable' import { Destroyable } from './Destroyable'
import { Edge, Message, RingBuffer, MessageHistory } from './' import { Edge, Message, RingBuffer, MessageHistory } from './'
import { EventDispatcher } from '../../../events' 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' | '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<T extends Destroyable>(node: TreeNode<T>): IDecoder | undefined {
return decoders.find(
decoder =>
decoder.canDecodeTopic?.(node.path()) || (node.message?.payload && decoder.canDecodeData?.(node.message?.payload))
)
}
export class TreeNode<ViewModel extends Destroyable> { export class TreeNode<ViewModel extends Destroyable> {
public sourceEdge?: Edge<ViewModel> public sourceEdge?: Edge<ViewModel>
@@ -22,6 +44,28 @@ export class TreeNode<ViewModel extends Destroyable> {
public isTree = false public isTree = false
public type: TopicDataType = 'json' 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 cachedPath?: string
private cachedChildTopics?: Array<TreeNode<ViewModel>> private cachedChildTopics?: Array<TreeNode<ViewModel>>
private cachedLeafMessageCount?: number private cachedLeafMessageCount?: number
@@ -157,7 +201,7 @@ export class TreeNode<ViewModel extends Destroyable> {
public path(): string { public path(): string {
if (!this.cachedPath) { if (!this.cachedPath) {
return this.branch() this.cachedPath = this.branch()
.map(node => node.sourceEdge && node.sourceEdge.name) .map(node => node.sourceEdge && node.sourceEdge.name)
.filter(name => name !== undefined) .filter(name => name !== undefined)
.join('/') .join('/')

View File

@@ -1,6 +1,7 @@
import { Destroyable } from './Destroyable' import { Destroyable } from './Destroyable'
import { Edge, Tree, TreeNode } from './' import { Edge, Tree, TreeNode } from './'
import { MqttMessage } from '../../../events' import { MqttMessage } from '../../../events'
import { Base64Message } from './Base64Message'
export abstract class TreeNodeFactory { export abstract class TreeNodeFactory {
private static messageCounter = 0 private static messageCounter = 0
@@ -30,6 +31,7 @@ export abstract class TreeNodeFactory {
mqttMessage.retain mqttMessage.retain
node.setMessage({ node.setMessage({
...mqttMessage, ...mqttMessage,
payload: mqttMessage.payload && new Base64Message(mqttMessage.payload?.base64Message),
length: mqttMessage.payload?.length ?? 0, length: mqttMessage.payload?.length ?? 0,
received: receiveDate, received: receiveDate,
messageNumber: this.messageCounter, messageNumber: this.messageCounter,

View File

@@ -1,21 +1,98 @@
import { Base64Message } from './Base64Message' import { Base64Message } from './Base64Message'
import { Decoder } from './Decoder' import { Decoder } from './Decoder'
import { get } from 'sparkplug-payload' import { get } from 'sparkplug-payload'
var sparkplug = get("spBv1.0") var sparkplug = get('spBv1.0')
export const SparkplugDecoder = { export interface IDecoder<T = string> {
decode(input: Buffer): Base64Message { /**
* 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 { try {
const message = Base64Message.fromString(JSON.stringify( const message = Base64Message.fromString(
// @ts-ignore JSON.stringify(
sparkplug.decodePayload(new Uint8Array(input))) // @ts-ignore
sparkplug.decodePayload(new Uint8Array(input.toBuffer()))
)
) )
message.decoder = Decoder.SPARKPLUG message.decoder = Decoder.SPARKPLUG
return message return message
} catch { } catch {
const message = Base64Message.fromString("Failed to decode sparkplugb payload") const message = new Base64Message(undefined, 'Failed to decode sparkplugb payload')
message.decoder = Decoder.NONE message.decoder = Decoder.NONE
return message 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<BinaryFormats> = {
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

View File

@@ -48,12 +48,7 @@ export class ConnectionManager {
} }
let decoded_payload = null let decoded_payload = null
// spell-checker: disable-next-line decoded_payload = Base64Message.fromBuffer(buffer)
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)
}
backendEvents.emit(messageEvent, { backendEvents.emit(messageEvent, {
topic, topic,

View File

@@ -19,7 +19,7 @@ registerCrashReporter()
// const electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5', buildOptions) // const electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5', buildOptions)
// } // }
app.commandLine.appendSwitch('--no-sandbox') app.commandLine.appendSwitch('--no-sandbox --disable-dev-shm-usage')
app.whenReady().then(() => { app.whenReady().then(() => {
backendRpc.on(makeOpenDialogRpc(), async request => { backendRpc.on(makeOpenDialogRpc(), async request => {
return dialog.showOpenDialog(BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0], request) return dialog.showOpenDialog(BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0], request)
@@ -70,7 +70,7 @@ async function createWindow() {
}) })
console.log('icon path', iconPath) console.log('icon path', iconPath)
mainWindow.webContents.openDevTools({ mode: 'detach' })
// Load the index.html of the app. // Load the index.html of the app.
if (isDev()) { if (isDev()) {
mainWindow.loadURL('http://localhost:8080') mainWindow.loadURL('http://localhost:8080')

View File

@@ -9,15 +9,11 @@
"moduleResolution": "node", "moduleResolution": "node",
"sourceRoot": "src/", "sourceRoot": "src/",
"target": "ES2017", "target": "ES2017",
"lib": [ "lib": ["ES2017", "dom"],
"es2017",
"dom"
],
"sourceMap": true, "sourceMap": true,
"types": [ "types": ["node"],
"node" "skipLibCheck": true,
], "esModuleInterop": true
"skipLibCheck": true
}, },
"include": [ "include": [
"src/electron.ts", "src/electron.ts",
@@ -26,7 +22,5 @@
"src/spec/leakTest.ts", "src/spec/leakTest.ts",
"scripts/*.ts" "scripts/*.ts"
], ],
"exclude": [ "exclude": ["node_modules"]
"node_modules"
]
} }