Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(desktop): traffic rate monitor support #1795

Merged
merged 1 commit into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ echarts.use([
])

@Component
export default class AreaLine extends Vue {
export default class LineChartExtent extends Vue {
@Prop({ required: true }) public id!: string
@Prop({ default: '450px' }) public height!: string
@Prop({ default: '100%' }) public width!: string
@Prop({ default: 'transparent' }) public backgroundColor!: string
@Prop({ default: '' }) public chartTitle!: string
@Prop({ default: null }) public formatter?: (value: number) => string
@Prop({ default: 'area' }) public type!: 'area' | 'line'
@Prop({
default: () => ({
xData: [],
Expand Down Expand Up @@ -113,16 +114,17 @@ export default class AreaLine extends Vue {
},
},
],
series: this.chartData.seriesData.map((item) => {
return {
name: item.name,
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 0,
},
showSymbol: false,
series: this.chartData.seriesData.map((item) => ({
name: item.name,
type: 'line',
stack: 'Total',
smooth: this.type === 'area',
showSymbol: false,
lineStyle: {
width: this.type === 'line' ? 2 : 0,
color: item.areaStyle.colorFrom,
},
...(this.type === 'area' && {
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
Expand All @@ -136,12 +138,12 @@ export default class AreaLine extends Vue {
},
]),
},
emphasis: {
focus: 'series',
},
data: item.data,
}
}),
}),
emphasis: {
focus: 'series',
},
data: item.data,
})),
}
}

Expand Down
40 changes: 40 additions & 0 deletions src/components/charts/NumberCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<el-card class="number-card" shadow="never">
<div class="value">
{{ value }}
<i v-if="icon" :class="['ml-2', icon]"></i>
</div>
<div class="label">{{ label }}</div>
</el-card>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'

@Component
export default class NumberCard extends Vue {
@Prop({ required: true }) public value!: number | string
@Prop({ required: true }) public label!: string
@Prop({ default: '' }) public icon!: string
}
</script>

<style lang="scss" scoped>
.number-card {
padding: 16px 0px;
text-align: center;
border: 0px;

.value {
font-size: 24px;
font-weight: bold;
color: #fff;
}

.label {
margin-top: 8px;
font-size: 14px;
color: #fff;
}
}
</style>
21 changes: 21 additions & 0 deletions src/lang/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,25 @@ export default {
ja: '累計送信トラフィック',
hu: 'Összesített Küldött',
},
receivedTrafficRate: {
zh: '接收流量速率',
en: 'Received Rate',
tr: 'Alınan Hız',
ja: '受信速度',
hu: 'Fogadott sebesség',
},
sentTrafficRate: {
zh: '发送流量速率',
en: 'Sent Rate',
tr: 'Gönderilen Hız',
ja: '送信速度',
hu: 'Küldött sebesség',
},
current: {
zh: '当前',
en: 'Current ',
tr: 'Şu an ',
ja: '現在',
hu: 'Jelenlegi ',
},
}
4 changes: 2 additions & 2 deletions src/types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ declare global {

interface MetricsModel {
label: string
received: number | null
sent: number | null
received: number
sent: number
}

// System
Expand Down
23 changes: 19 additions & 4 deletions src/utils/formatter.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
/**
* Format bytes into human readable string
* @param bytes Number of bytes
* @returns Formatted string like "1.5 MB"
* @returns Formatted string like "1.54 MB"
*/
export const formatBytes = (bytes: number): string => {
if (!Number.isFinite(bytes) || bytes < 0 || Number.isNaN(bytes)) {
return '0 B'
}

if (bytes === 0) return '0 B'
if (bytes === 0) {
return '0 B'
}

if (bytes < 1) {
return `${Number(bytes.toFixed(2))} B`
}

const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))

if (i >= sizes.length) return '∞ TB'

if (i === 0) return `${Math.floor(bytes)} B`
if (i === 0) {
return `${Math.floor(bytes)} B`
}

const value = bytes / Math.pow(k, i)
const hasDecimal = Math.abs(value - Math.floor(value)) > 0.001

if (!hasDecimal) {
return `${Math.floor(value)} ${sizes[i]}`
}

return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
return `${Number(value.toFixed(2))} ${sizes[i]}`
}
143 changes: 95 additions & 48 deletions src/utils/systemTopic.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,119 @@
import moment from 'moment'

const METRICS_BYTES_PREFIX = '/metrics/bytes/'
const RECEIVED_TOPIC = `${METRICS_BYTES_PREFIX}received`
const SENT_TOPIC = `${METRICS_BYTES_PREFIX}sent`
const UPTIME_TOPIC = '/uptime'
const VERSION_TOPIC = '/version'

/**
* Check if a topic is a system topic
* @param topic The MQTT topic to check
* @returns True if the topic starts with '$SYS', false otherwise
* Merge received and sent data with close timestamps
* @param metrics Array of MetricsModel to merge
* @returns Merged array of MetricsModel
*/
export const isSystemTopic = (topic: string): boolean => topic.startsWith('$SYS')
export const mergeMetrics = (metrics: MetricsModel[]): MetricsModel[] => {
const merged: MetricsModel[] = []
let current: MetricsModel | null = null

for (const metric of metrics) {
if (!current) {
current = { ...metric }
continue
}

// If timestamps are close (within 1 second), merge the data
const currentTime = moment(current.label, 'YYYY-MM-DD HH:mm:ss:SSS')
const metricTime = moment(metric.label, 'YYYY-MM-DD HH:mm:ss:SSS')
const timeDiff = Math.abs(currentTime.diff(metricTime, 'milliseconds'))

if (timeDiff <= 1000) {
// Merge data, keep non-zero values
current.received = current.received || metric.received
current.sent = current.sent || metric.sent

// If both data points are present, add to results and reset current
if (current.received && current.sent) {
merged.push({ ...current })
current = null
}
} else {
// If time difference is too large, save current data and start a new merge
if (current.received || current.sent) {
merged.push({ ...current })
}
current = { ...metric }
}
}

// Handle the last group of data
if (current && (current.received || current.sent)) {
merged.push(current)
}

return merged
}

/**
* Extract data from a message for a given topic
* @param message The MQTT message to extract data from
* @param topic The topic to extract data for
* @returns The extracted data as a string, or null if the topic is not found in the message
* Parse and merge traffic metrics from raw messages
* @param messages Raw MQTT messages to process
* @returns Merged array of MetricsModel
*/
export const extractData = (message: MessageModel, topic: string): string | null => {
return message.topic.includes(topic) ? message.payload : null
export const transformTrafficMessages = (messages: MessageModel[]): MetricsModel[] => {
const metrics = messages
.sort((a, b) => a.createAt.localeCompare(b.createAt))
.map((m) => ({
label: m.createAt,
received: m.topic.includes(RECEIVED_TOPIC) ? parseFloat(m.payload) : 0,
sent: m.topic.includes(SENT_TOPIC) ? parseFloat(m.payload) : 0,
}))

return mergeMetrics(metrics)
}

/**
* Parse traffic metrics data
* @param message The MQTT message to parse
* @param defaultMetrics The default metrics model to use as base
* @returns The parsed data as a MetricsModel, or null if the message is not a system topic
* Calculate average rate of change per second between two values
* @param current Current value
* @param previous Previous value
* @param seconds Time difference in seconds
* @returns Average rate per second, returns 0 if negative
*/
export const getTrafficMetrics = (message: MessageModel, defaultMetrics: MetricsModel): MetricsModel | null => {
if (!isSystemTopic(message.topic)) {
return null
}
export const calculateAverageRate = (current: number, previous: number, seconds: number): number => {
if (seconds <= 0) return 0
return Math.max(0, (current - previous) / seconds)
}

const metrics: MetricsModel = {
label: message.createAt,
received: defaultMetrics.received,
sent: defaultMetrics.sent,
}
/**
* Calculate transmission rate between two measurement points
* @param current Current MetricsModel
* @param previous Previous MetricsModel
* @returns Calculated rate as MetricsModel or null if invalid
*/
export const calculateRate = (current: MetricsModel, previous: MetricsModel): MetricsModel => {
const currentTime = moment(current.label, 'YYYY-MM-DD HH:mm:ss:SSS')
const previousTime = moment(previous.label, 'YYYY-MM-DD HH:mm:ss:SSS')

// Try to parse received bytes
const receivedBytes = extractData(message, RECEIVED_TOPIC)
if (receivedBytes) {
metrics.received = parseInt(receivedBytes, 10)
return metrics
}
const timeDiff = currentTime.diff(previousTime, 'seconds')
if (timeDiff <= 0) return { label: current.label, received: 0, sent: 0 }

// Try to parse sent bytes
const sentBytes = extractData(message, SENT_TOPIC)
if (sentBytes) {
metrics.sent = parseInt(sentBytes, 10)
return metrics
}
return {
label: current.label,

return null
received: calculateAverageRate(current.received, previous.received, timeDiff),
sent: calculateAverageRate(current.sent, previous.sent, timeDiff),
}
}

/**
* Get the uptime from a message
* @param message The MQTT message to get the uptime from
* @returns The uptime as a string, or null if the topic is not found in the message
* Calculate rates for a set of measurement data
* @param metrics Array of MetricsModel to calculate rates for
* @returns Array of calculated rates as MetricsModel
*/
export const getUptime = (message: MessageModel): string | null => extractData(message, UPTIME_TOPIC)
export const calculateTrafficRates = (metrics: MetricsModel[]): MetricsModel[] => {
if (metrics.length < 2) return []
const rates: MetricsModel[] = []

/**
* Get the version from a message
* @param message The MQTT message to get the version from
* @returns The version as a string, or null if the topic is not found in the message
*/
export const getVersion = (message: MessageModel): string | null => extractData(message, VERSION_TOPIC)
for (let i = 1; i < metrics.length; i++) {
rates.push(calculateRate(metrics[i], metrics[i - 1]))
}
return rates
}

export default {}
4 changes: 2 additions & 2 deletions src/views/viewer/TopicTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { globalEventBus } from '@/utils/globalEventBus'
import TreeView from '@/components/widgets/TreeView.vue'
import TreeView from '@/widgets/TreeView.vue'
import { updateTopicTreeNode } from '@/utils/topicTree'
import { Packet } from 'mqtt-packet/types'
import TreeNodeInfo from '@/components/widgets/TreeNodeInfo.vue'
import TreeNodeInfo from '@/widgets/TreeNodeInfo.vue'
import { ignoreQoS0Message } from '@/utils/mqttUtils'
import { MessageQueue } from '@/utils/messageQueue'
import useServices from '@/database/useServices'
Expand Down
Loading
Loading