diff --git a/app/src/actions/Publish.ts b/app/src/actions/Publish.ts index 91130b4..a752251 100644 --- a/app/src/actions/Publish.ts +++ b/app/src/actions/Publish.ts @@ -2,7 +2,7 @@ import { Action, ActionTypes } from '../reducers/Publish' import { AppState } from '../reducers' import { Base64Message } from '../../../backend/src/Model/Base64Message' import { Dispatch } from 'redux' -import { makePublishEvent, rendererEvents } from '../../../events' +import { MqttMessage, makePublishEvent, rendererEvents } from '../../../events' export const setTopic = (topic?: string): Action => { return { @@ -41,7 +41,7 @@ export const publish = (connectionId: string) => (dispatch: Dispatch, ge } const publishEvent = makePublishEvent(connectionId) - const mqttMessage = { + const mqttMessage: Partial = { topic, payload: state.publish.payload ? Base64Message.fromString(state.publish.payload) : null, retain: state.publish.retain, diff --git a/app/src/components/Sidebar/Sidebar.tsx b/app/src/components/Sidebar/Sidebar.tsx index f925c02..a24a981 100644 --- a/app/src/components/Sidebar/Sidebar.tsx +++ b/app/src/components/Sidebar/Sidebar.tsx @@ -1,10 +1,9 @@ import * as q from '../../../../backend/src/Model' import React, { useState, useEffect, useCallback } from 'react' -import ExpandMore from '@material-ui/icons/ExpandMore' import NodeStats from './NodeStats' import ValuePanel from './ValueRenderer/ValuePanel' import { AppState } from '../../reducers' -import { Badge, ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography } from '@material-ui/core' +import { ExpansionPanelDetails } from '@material-ui/core' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { settingsActions, sidebarActions } from '../../actions' diff --git a/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx b/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx index a9d6ce8..d80814b 100644 --- a/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx +++ b/app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx @@ -65,7 +65,7 @@ export const MessageHistory: React.FC = props => { const history = node.messageHistory.toArray() let previousMessage: q.Message | undefined = node.message const historyElements = [...history].reverse().map((message, idx) => { - const value = node.message ? decodeMessage(node.message)?.format()[0] ?? null : null + const value = node.message ? decodeMessage(message)?.message?.format()[0] ?? null : null const element = { value: value ?? '', @@ -96,7 +96,7 @@ export const MessageHistory: React.FC = props => { return element }) - const value = node.message ? decodeMessage(node.message)?.format()[0] ?? null : null + const value = node.message ? decodeMessage(node.message)?.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 10a3020..61c5b2f 100644 --- a/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx +++ b/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx @@ -56,7 +56,7 @@ function ValuePanel(props: Props) { } const getDecodedValue = useCallback(() => { - return node?.message && decodeMessage(node.message)?.toUnicodeString() + return node?.message && decodeMessage(node.message)?.message?.toUnicodeString() }, [node, decodeMessage]) function messageMetaInfo() { diff --git a/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx b/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx index 6addef4..f0b9196 100644 --- a/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx +++ b/app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx @@ -1,12 +1,13 @@ import * as q from '../../../../../backend/src/Model' -import * as React from 'react' +import React, { useMemo } from 'react' import CodeDiff from '../CodeDiff' import { AppState } from '../../../reducers' import { connect } from 'react-redux' import { ValueRendererDisplayMode } from '../../../reducers/Settings' import { Fade } from '@material-ui/core' import { Decoder } from '../../../../../backend/src/Model/Decoder' -import { DecoderFunction, useDecoder } from '../../hooks/useDecoder' +import { useDecoder } from '../../hooks/useDecoder' +import { TopicViewModel } from '../../../model/TopicViewModel' interface Props { message: q.Message @@ -15,86 +16,112 @@ interface Props { renderMode: ValueRendererDisplayMode } -export const ValueRenderer: React.FC = props => { - const decodeMessage = useDecoder(props.treeNode) +type Language = 'json' - function renderDiff(current: string = '', previous: string = '', title?: string, language?: 'json') { - return ( - - ) - } +function renderDiff( + treeNode: q.TreeNode, + compareWithPreviousMessage: boolean, + current: string = '', + previous: string = '', + title?: string, + language?: Language +) { + return ( + + ) +} - function renderDiffMode( - decodeMessage: DecoderFunction, - message: q.Message, - treeNode: q.TreeNode, - compare?: q.Message +function renderDiffMode( + treeNode: q.TreeNode, + currentStr: string | undefined, + compareStr: string | undefined, + currentType: Language | undefined, + compareType: Language | undefined, + compareWithPreviousMessage: boolean +) { + const language = currentType === compareType && compareType === 'json' ? 'json' : undefined + + return
{renderDiff(treeNode, compareWithPreviousMessage, currentStr, compareStr, undefined, language)}
+} + +function renderRawMode( + treeNode: q.TreeNode, + currentStr: string | undefined, + compareStr: string | undefined, + currentType: Language | undefined, + compareType: Language | undefined, + compareWithPreviousMessage: boolean +) { + return ( +
+ {renderDiff(treeNode, compareWithPreviousMessage, currentStr, currentStr, undefined, currentType)} + +
+ {Boolean(compareStr) + ? renderDiff(treeNode, compareWithPreviousMessage, compareStr, compareStr, 'selected', compareType) + : null} +
+
+
+ ) +} + +export const ValueRenderer: React.FC = ({ treeNode, compareWith: compare, message, renderMode }) => { + const decodeMessage = useDecoder(treeNode) + const decodedMessage = useMemo(() => decodeMessage(message), [decodeMessage, message]) + + const previousMessages = treeNode.messageHistory.toArray() + const previousMessage = previousMessages[previousMessages.length - 2] + const compareMessage = compare || previousMessage || message + const compareWithPreviousMessage = !!compare + + const [currentStr, currentType] = useMemo( + () => decodedMessage?.message?.format(treeNode.type) ?? [], + [decodedMessage, treeNode.type] + ) + const [compareStr, compareType] = useMemo( + () => decodeMessage(compareMessage)?.message?.format(treeNode.type) ?? [], + [compareMessage, decodeMessage, treeNode.type] + ) + + function renderValue( + treeNode: q.TreeNode, + currentStr: string | undefined, + compareStr: string | undefined, + currentType: Language | undefined, + compareType: Language | undefined, + renderMode: string, + compareWithPreviousMessage: boolean ) { - if (!message.payload) { - return - } - - const previousMessages = treeNode.messageHistory.toArray() - const previousMessage = previousMessages[previousMessages.length - 2] - const compareMessage = compare || previousMessage || message - - const [currentStr, currentType] = decodeMessage(message)?.format(treeNode.type) ?? [] - const [compareStr, compareType] = decodeMessage(compareMessage)?.format(treeNode.type) ?? [] - - const language = currentType === compareType && compareType === 'json' ? 'json' : undefined - - return
{renderDiff(currentStr, compareStr, undefined, language)}
- } - - function renderRawMode( - decodeMessage: DecoderFunction, - message: q.Message, - treeNode: q.TreeNode, - compare?: q.Message - ) { - if (!message.payload) { - return - } - - const [currentStr, currentType] = decodeMessage(message)?.format(treeNode.type) ?? [] - const [compareStr, compareType] = - compare && compare.payload ? decodeMessage(compare)?.format(treeNode.type) ?? [] : [] - - return ( -
- {renderDiff(currentStr, currentStr, undefined, currentType)} - -
{Boolean(compareStr) ? renderDiff(compareStr, compareStr, 'selected', compareType) : null}
-
-
- ) - } - - function renderValue(decodeMessage: DecoderFunction) { - const { message, treeNode, compareWith, renderMode } = props - if (!message.payload) { + if (!decodedMessage) { return null } switch (renderMode) { case 'diff': - return renderDiffMode(decodeMessage, message, treeNode, compareWith) + return renderDiffMode(treeNode, currentStr, compareStr, currentType, compareType, compareWithPreviousMessage) default: - return renderRawMode(decodeMessage, message, treeNode, compareWith) + return renderRawMode(treeNode, currentStr, compareStr, currentType, compareType, compareWithPreviousMessage) } } + const renderedValue = useMemo( + () => + renderValue(treeNode, currentStr, compareStr, currentType, compareType, renderMode, compareWithPreviousMessage), + [treeNode, currentStr, compareStr, currentType, compareType, renderMode, compareWithPreviousMessage] + ) + return (
- {props.message?.payload?.decoder === Decoder.SPARKPLUG && 'Decoded SparkplugB'} - {renderValue(decodeMessage)} + {decodedMessage?.decoder === Decoder.SPARKPLUG && 'Decoded SparkplugB'} + {renderedValue}
) } diff --git a/app/src/components/TopicPlot.tsx b/app/src/components/TopicPlot.tsx index 61f6014..780c983 100644 --- a/app/src/components/TopicPlot.tsx +++ b/app/src/components/TopicPlot.tsx @@ -30,7 +30,7 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array { - const decoded = decodeMessage(message)?.toUnicodeString() + const decoded = decodeMessage(message)?.message?.toUnicodeString() return { x: message.received.getTime(), y: toPlottableValue(decoded) } }) .filter(data => !isNaN(data.y as any)) as any @@ -46,7 +46,7 @@ function nodeDotPathToHistory( .map((message: q.Message) => { let json: any = {} try { - const decoded = decodeMessage(message) + const decoded = decodeMessage(message)?.message json = decoded ? JSON.parse(decoded.toUnicodeString()) : {} } catch (ignore) {} diff --git a/app/src/components/Tree/TreeNode/TreeNodeTitle.tsx b/app/src/components/Tree/TreeNode/TreeNodeTitle.tsx index cc60394..2dd5474 100644 --- a/app/src/components/Tree/TreeNode/TreeNodeTitle.tsx +++ b/app/src/components/Tree/TreeNode/TreeNodeTitle.tsx @@ -32,7 +32,7 @@ export const TreeNodeTitle = (props: TreeNodeProps) => { if (!props.treeNode.message || !props.treeNode.message.payload) { return '' } - const [value = ''] = decodeMessage(props.treeNode.message)?.format(props.treeNode.type) ?? [] + const [value = ''] = decodeMessage(props.treeNode.message)?.message?.format(props.treeNode.type) ?? [] return value.length > limit ? `${value.slice(0, limit)}…` : value } diff --git a/app/src/components/hooks/useDecoder.ts b/app/src/components/hooks/useDecoder.ts index 4a3d6c5..7c4668c 100644 --- a/app/src/components/hooks/useDecoder.ts +++ b/app/src/components/hooks/useDecoder.ts @@ -1,11 +1,12 @@ import * as q from '../../../../backend/src/Model' import { useCallback, useState } from 'react' import { TopicViewModel } from '../../model/TopicViewModel' -import { Base64Message } from '../../../../backend/src/Model/Base64Message' import { useSubscription } from './useSubscription' import { useViewModel } from '../Tree/TreeNode/effects/useViewModel' +import { DecoderEnvelope } from '../../decoders/DecoderEnvelope' +import { Decoder } from '../../../../backend/src/Model/Decoder' -export type DecoderFunction = (message: q.Message) => Base64Message | null +export type DecoderFunction = (message: q.Message) => DecoderEnvelope | undefined /** * Provides the latest decoder for a topic @@ -21,7 +22,9 @@ export function useDecoder(treeNode: q.TreeNode | undefined): De return useCallback( message => { - return decoder && message.payload ? decoder.decoder.decode(message.payload, decoder.format) : message.payload + return decoder && message.payload + ? decoder.decoder.decode(message.payload, decoder.format) + : { message: message.payload ?? undefined, decoder: Decoder.NONE } }, [decoder] ) diff --git a/app/src/decoders/BinaryDecoder.ts b/app/src/decoders/BinaryDecoder.ts index e558c56..b204cba 100644 --- a/app/src/decoders/BinaryDecoder.ts +++ b/app/src/decoders/BinaryDecoder.ts @@ -1,4 +1,6 @@ import { Base64Message } from '../../../backend/src/Model/Base64Message' +import { Decoder } from '../../../backend/src/Model/Decoder' +import { DecoderEnvelope } from './DecoderEnvelope' import { MessageDecoder } from './MessageDecoder' type BinaryFormats = @@ -18,7 +20,7 @@ type BinaryFormats = */ export const BinaryDecoder: MessageDecoder = { formats: ['int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64', 'float', 'double'], - decode(input: Base64Message, format: BinaryFormats): Base64Message { + decode(input: Base64Message, format: BinaryFormats): DecoderEnvelope { const decodingOption = { int8: [Buffer.prototype.readInt8, 1], int16: [Buffer.prototype.readInt16LE, 2], @@ -37,12 +39,18 @@ export const BinaryDecoder: MessageDecoder = { const buf = input.toBuffer() let str: String[] = [] if (buf.length % bytesToRead !== 0) { - return new Base64Message(undefined, 'Data type does not align with message') + return { + error: 'Data type does not align with message', + decoder: Decoder.NONE, + } } 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)) + return { + message: Base64Message.fromString(JSON.stringify(str.length === 1 ? str[0] : str)), + decoder: Decoder.NONE, + } }, } diff --git a/app/src/decoders/DecoderEnvelope.ts b/app/src/decoders/DecoderEnvelope.ts new file mode 100644 index 0000000..c15fa4f --- /dev/null +++ b/app/src/decoders/DecoderEnvelope.ts @@ -0,0 +1,8 @@ +import { Base64Message } from '../../../backend/src/Model/Base64Message' +import { Decoder } from '../../../backend/src/Model/Decoder' + +export interface DecoderEnvelope { + message?: Base64Message + error?: string + decoder: Decoder +} diff --git a/app/src/decoders/MessageDecoder.ts b/app/src/decoders/MessageDecoder.ts index d5f2ef6..dc96ef6 100644 --- a/app/src/decoders/MessageDecoder.ts +++ b/app/src/decoders/MessageDecoder.ts @@ -1,4 +1,5 @@ import { Base64Message } from '../../../backend/src/Model/Base64Message' +import { DecoderEnvelope } from './DecoderEnvelope' export interface MessageDecoder { /** @@ -8,5 +9,5 @@ export interface MessageDecoder { formats: T[] canDecodeTopic?(topic: string): boolean canDecodeData?(data: Base64Message): boolean - decode(input: Base64Message, format: T | string | undefined): Base64Message + decode(input: Base64Message, format: T | string | undefined): DecoderEnvelope } diff --git a/app/src/decoders/SparkplugBDecoder.ts b/app/src/decoders/SparkplugBDecoder.ts index d6e3f88..b5bedbd 100644 --- a/app/src/decoders/SparkplugBDecoder.ts +++ b/app/src/decoders/SparkplugBDecoder.ts @@ -9,7 +9,7 @@ export const SparkplugDecoder: MessageDecoder = { canDecodeTopic(topic: string) { return !!topic.match(/^spBv1\.0\/[^/]+\/[ND](DATA|CMD|DEATH|BIRTH)\/[^/]+(\/[^/]+)?$/u) }, - decode(input: Base64Message): Base64Message { + decode(input) { try { const message = Base64Message.fromString( JSON.stringify( @@ -17,12 +17,12 @@ export const SparkplugDecoder: MessageDecoder = { sparkplug.decodePayload(new Uint8Array(input.toBuffer())) ) ) - message.decoder = Decoder.SPARKPLUG - return message + return { message, decoder: Decoder.SPARKPLUG } } catch { - const message = new Base64Message(undefined, 'Failed to decode sparkplugb payload') - message.decoder = Decoder.NONE - return message + return { + error: 'Failed to decode sparkplugb payload', + decoder: Decoder.NONE, + } } }, } diff --git a/app/src/decoders/StringDecoder.ts b/app/src/decoders/StringDecoder.ts index 0c3706f..f586bb7 100644 --- a/app/src/decoders/StringDecoder.ts +++ b/app/src/decoders/StringDecoder.ts @@ -1,9 +1,10 @@ import { Base64Message } from '../../../backend/src/Model/Base64Message' +import { Decoder } from '../../../backend/src/Model/Decoder' import { MessageDecoder } from './MessageDecoder' export const StringDecoder: MessageDecoder = { formats: ['string'], - decode(input: Base64Message): Base64Message { - return input + decode(input: Base64Message) { + return { message: input, decoder: Decoder.NONE } }, } diff --git a/backend/src/DataSource/MqttSource.ts b/backend/src/DataSource/MqttSource.ts index 87c3db6..bd55b6f 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?.toBuffer() ?? '', { + this.client.publish(msg.topic, (msg.payload && new Base64Message(msg.payload))?.toBuffer() ?? '', { qos: msg.qos, retain: msg.retain, }) diff --git a/backend/src/Model/Base64Message.ts b/backend/src/Model/Base64Message.ts index 4ab6d56..096f555 100644 --- a/backend/src/Model/Base64Message.ts +++ b/backend/src/Model/Base64Message.ts @@ -1,20 +1,42 @@ import { Base64 } from 'js-base64' -import { Decoder } from './Decoder' import { TopicDataType } from './TreeNode' +export type Base64MessageDTO = Pick + export class Base64Message { public base64Message: string - private unicodeValue: string - public error?: string - public decoder: Decoder - public length: number + private _unicodeValue: string | undefined - 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 + // Todo: Rename to `encodedLength` + public get length(): number { + return this.base64Message.length + } + + private get unicodeValue(): string { + if (!this._unicodeValue) { + this._unicodeValue = Base64.decode(this.base64Message ?? '') + } + + return this._unicodeValue + } + + constructor(base64Str?: string | Base64MessageDTO, error?: string) { + if (typeof base64Str === 'string' || typeof base64Str === 'undefined') { + this.base64Message = base64Str ?? '' + } else { + if (typeof base64Str.base64Message !== 'string') { + throw new Error('Received unexpected type in copy constructor') + } + this.base64Message = base64Str.base64Message + } + } + + /** + * Override default JSON serialization behavior to only return the DTO + * @returns + */ + public toJSON(): Base64MessageDTO { + return { base64Message: this.base64Message } } public toUnicodeString() { diff --git a/backend/src/Model/ChangeBuffer.ts b/backend/src/Model/ChangeBuffer.ts index 018e5ba..4310c73 100644 --- a/backend/src/Model/ChangeBuffer.ts +++ b/backend/src/Model/ChangeBuffer.ts @@ -15,7 +15,7 @@ export class ChangeBuffer { public push(val: MqttMessage) { if (!this.isFull()) { this.buffer.push({ message: val, received: new Date() }) - this.size += this.estimatedMessageOverhead + (val.payload ? val.payload.length : 0) + this.size += this.estimatedMessageOverhead + (val.payload?.base64Message.length ?? 0) this.length += 1 } } diff --git a/backend/src/Model/Message.ts b/backend/src/Model/Message.ts index 5b94304..556861f 100644 --- a/backend/src/Model/Message.ts +++ b/backend/src/Model/Message.ts @@ -1,7 +1,8 @@ import { Base64Message } from './Base64Message' import { QoS } from '../DataSource/MqttSource' +import { MemoryConsumptionExpressedByLength } from './RingBuffer' -export interface Message { +export interface Message extends MemoryConsumptionExpressedByLength { // mqtt based info payload: Base64Message | null messageId?: number diff --git a/backend/src/Model/TreeNodeFactory.ts b/backend/src/Model/TreeNodeFactory.ts index 82faa2a..752bc24 100644 --- a/backend/src/Model/TreeNodeFactory.ts +++ b/backend/src/Model/TreeNodeFactory.ts @@ -32,7 +32,7 @@ export abstract class TreeNodeFactory { node.setMessage({ ...mqttMessage, payload: mqttMessage.payload && new Base64Message(mqttMessage.payload?.base64Message), - length: mqttMessage.payload?.length ?? 0, + length: mqttMessage.payload?.base64Message.length ?? 0, received: receiveDate, messageNumber: this.messageCounter, }) diff --git a/events/Events.ts b/events/Events.ts index 69ce954..418652b 100644 --- a/events/Events.ts +++ b/events/Events.ts @@ -1,4 +1,4 @@ -import { Base64Message } from '../backend/src/Model/Base64Message' +import { Base64MessageDTO } from '../backend/src/Model/Base64Message' import { DataSourceState, MqttOptions } from '../backend/src/DataSource' import { UpdateInfo } from 'builder-util-runtime' import { RpcEvent } from './EventSystem/Rpc' @@ -32,7 +32,7 @@ export const updateAvailable: Event = { export interface MqttMessage { topic: string - payload: Base64Message | null + payload: Base64MessageDTO | null qos: 0 | 1 | 2 retain: boolean // Set if QoS is > 0 on received messages