Merge remote-tracking branch 'fb/multi-decoder/master' into feat/use-tahu-for-sparkplug-decoding

This commit is contained in:
Thomas Nordquist
2024-05-18 11:26:39 +02:00
14 changed files with 341 additions and 89 deletions

View File

@@ -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'
@@ -68,7 +68,7 @@ export const selectTopicWithMouseOver = (doSelect: boolean) => (dispatch: Dispat
dispatch(storeSettings())
}
export const setValueDisplayMode = (valueRendererDisplayMode: 'diff' | 'raw') => (dispatch: Dispatch<any>) => {
export const setValueDisplayMode = (valueRendererDisplayMode: ValueRendererDisplayMode) => (dispatch: Dispatch<any>) => {
dispatch({
valueRendererDisplayMode,
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE,

View File

@@ -114,6 +114,7 @@ function TopicChart(props: Props) {
</div>
</div>
<TopicPlot
node={props.treeNode ? props.treeNode : undefined}
color={props.parameters.color}
interpolation={props.parameters.interpolation}
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined}

View File

@@ -69,7 +69,7 @@ function ChartPreview(props: Props) {
<Popper open={open} anchorEl={chartIconRef.current} placement="left-end">
<Fade in={open} timeout={300}>
<Paper style={{ width: '300px' }}>
{open ? <TopicPlot history={props.treeNode.messageHistory} dotPath={props.literal.path} /> : <span />}
{open ? <TopicPlot node={props.treeNode} history={props.treeNode.messageHistory} dotPath={props.literal.path} /> : <span />}
</Paper>
</Fade>
</Popper>

View File

@@ -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<any>; actions: typeof sidebarActions }) => {
const { node } = props
console.log(node && node.path())
const copyTopic = node ? <Copy value={node.path()} /> : null
const deleteTopic = useCallback((topic?: q.TreeNode<any>, recursive: boolean = false) => {
if (!topic) {
return
}
props.actions.clearTopic(topic, recursive)
}, [])
const setTopicType = useCallback((node?: q.TreeNode<any>, type: q.TopicDataType = 'string') => {
if (!node) {
return
}
node.type = type
}, [])
return useMemo(
() => (
<Panel disabled={!Boolean(node)}>
@@ -29,6 +37,7 @@ const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActi
Topic {copyTopic}
<TopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<TopicTypeButton node={node} setTopicType={setTopicType} />
</span>
<Topic node={node} />
</Panel>

View File

@@ -0,0 +1,84 @@
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[] = ['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<any>
setTopicType: (node: q.TreeNode<any>, type: q.TopicDataType) => void
}) => {
const { node } = props
if (!node || !node.message || !node.message.payload) {
return null
}
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [open, setOpen] = React.useState(false)
const handleMenuItemClick = useCallback(
(mouseEvent: React.MouseEvent, element: q.TreeNode<any>, type: q.TopicDataType) => {
if (!element || !type) {
return
}
props.setTopicType(element, type as q.TopicDataType)
setOpen(false)
},
[props.setTopicType]
)
const handleToggle = (event: React.MouseEvent<HTMLElement>) => {
if (open === true) {
return
}
setAnchorEl(event.currentTarget)
setOpen((prevOpen) => !prevOpen)
}
const handleClose = (event: React.MouseEvent<Document, MouseEvent>) => {
if (anchorEl && anchorEl.contains(event.target as HTMLElement)) {
return
}
setOpen(false)
}
return (
<CustomIconButton tooltip="" onClick={handleToggle}>
<Code />
<Popper open={open} anchorEl={anchorEl} role={undefined} transition>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="topicTypeMode">
{options.map((option, index) => (
<MenuItem
key={option}
selected={node && option === node.type}
onClick={(event) => handleMenuItemClick(event, node, option)}
>
{option}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</CustomIconButton>
)
}

View File

@@ -82,7 +82,7 @@ class MessageHistory extends React.PureComponent<Props, State> {
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<Props, State> {
<MessageId message={message} />
</span>
<div style={{ float: 'right' }}>
<Copy value={value} />
<Copy value={value ? value : ''} />
</div>
</span>
),
@@ -112,8 +112,8 @@ class MessageHistory extends React.PureComponent<Props, State> {
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 (
<div>
<History
@@ -131,7 +131,7 @@ class MessageHistory extends React.PureComponent<Props, State> {
}
onClick={this.displayMessage}
>
{isMessagePlottable ? <TopicPlot history={node.messageHistory} /> : null}
{isMessagePlottable ? <TopicPlot node={node} history={node.messageHistory} /> : null}
</History>
</div>
)

View File

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

View File

@@ -38,45 +38,38 @@ class ValueRenderer extends React.Component<Props, State> {
)
}
private convertMessage(msg?: Base64Message): [string | undefined, 'json' | undefined] {
if (!msg) {
return [undefined, undefined]
}
const str = Base64Message.toUnicodeString(msg)
try {
JSON.parse(str)
} catch (error) {
return [str, undefined]
}
return [this.messageToPrettyJson(str), 'json']
}
private messageToPrettyJson(str: string): string | undefined {
try {
const json = JSON.parse(str)
return JSON.stringify(json, undefined, ' ')
} catch {
return undefined
}
}
private renderRawMode(message: q.Message, compare?: q.Message) {
private renderDiffMode(message: q.Message, treeNode: q.TreeNode<any>, 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 previousMessages = treeNode.messageHistory.toArray()
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 language = currentType === compareType && compareType === 'json' ? 'json' : undefined
return <div>{this.renderDiff(currentStr, compareStr, undefined, language)}</div>
}
private renderRawMode(message: q.Message, treeNode: q.TreeNode<any>, compare?: q.Message) {
if (!message.payload) {
return
}
const [currentStr, currentType] = Base64Message.format(message.payload, treeNode.type)
const [compareStr, compareType] =
compare && compare.payload ? Base64Message.format(compare.payload, treeNode.type) : [undefined, undefined]
return (
<div>
{this.renderDiff(value, value, undefined, valueLanguage)}
{this.renderDiff(currentStr, currentStr, undefined, currentType)}
<Fade in={Boolean(compareStr)} timeout={400}>
<div>
{Boolean(compareStr) ? this.renderDiff(compareStr, compareStr, 'selected', compareStrLanguage) : null}
</div>
<div>{Boolean(compareStr) ? this.renderDiff(compareStr, compareStr, 'selected', compareType) : null}</div>
</Fade>
</div>
)
@@ -93,24 +86,16 @@ class ValueRenderer extends React.Component<Props, State> {
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)
}
}
}

View File

@@ -8,6 +8,7 @@ import { PlotCurveTypes } from '../reducers/Charts'
const parseDuration = require('parse-duration')
interface Props {
node?: q.TreeNode<any>
history: q.MessageHistory
dotPath?: string
timeInterval?: string
@@ -25,16 +26,17 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
return data
}
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory) {
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory, type: q.TopicDataType) {
return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => {
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: any = {}
@@ -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]
)

View File

@@ -31,8 +31,8 @@ class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
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() {
@@ -66,8 +66,7 @@ class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
const messages = this.props.treeNode.leafMessageCount()
const topicCount = this.props.treeNode.childTopicCount()
return (
<span key="metadata" className={this.props.classes.collapsedSubnodes}>{` (${topicCount} ${
topicCount === 1 ? 'topic' : 'topics'
<span key="metadata" className={this.props.classes.collapsedSubnodes}>{` (${topicCount} ${topicCount === 1 ? 'topic' : 'topics'
}, ${messages} ${messages === 1 ? 'message' : 'messages'})`}</span>
)
}

View File

@@ -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<Props, {}> {
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<Props, {}> {
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,9 +48,13 @@ class DateFormatter extends React.PureComponent<Props, {}> {
return 'm'
}
if (milliseconds > oneSecond * 0.5) {
return 's'
}
return 'ms'
}
public render() {
const locale = this.props.overrideLocale || this.props.locale
if (this.props.intervalSince) {

View File

@@ -1,5 +1,6 @@
import { Base64 } from 'js-base64'
import { Decoder } from './Decoder'
import { TopicDataType } from './TreeNode'
export class Base64Message {
private base64Message: string
@@ -26,6 +27,167 @@ export class Base64Message {
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]
}
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 '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)
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 => {
let hex = element.toString(16).toUpperCase()
str += `0x${hex.length < 2 ? '0' + hex : hex} `
})
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}`
}

View File

@@ -2,6 +2,9 @@ import { Destroyable } from './Destroyable'
import { Edge, Message, RingBuffer, MessageHistory } from './'
import { EventDispatcher } from '../../../events'
// 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<ViewModel extends Destroyable> {
public sourceEdge?: Edge<ViewModel>
public message?: Message
@@ -17,6 +20,7 @@ export class TreeNode<ViewModel extends Destroyable> {
public onMessage = new EventDispatcher<Message>()
public onDestroy = new EventDispatcher<TreeNode<ViewModel>>()
public isTree = false
public type: TopicDataType = 'json'
private cachedPath?: string
private cachedChildTopics?: Array<TreeNode<ViewModel>>

View File

@@ -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'