frontend is fucked
This commit is contained in:
parent
dbf511f27c
commit
ba0b5cdf5c
@ -1,119 +1,85 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-4">
|
<div class="p-grid p-m-2">
|
||||||
<div class="p-grid">
|
|
||||||
<!-- Devices Panel -->
|
|
||||||
<div class="p-col-12 p-md-3">
|
<div class="p-col-12 p-md-3">
|
||||||
<Panel header="Devices">
|
<div class="p-panel p-shadow-3">
|
||||||
|
<div class="p-panel-header">Devices</div>
|
||||||
|
<div class="p-panel-content">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(device, id) in devices" :key="id">
|
<li v-for="(device, id) in devices" :key="id">
|
||||||
<Button
|
<button
|
||||||
:label="id + ' — ' + device.status"
|
class="p-button p-button-text"
|
||||||
:class="{'p-button-outlined': selected !== id}"
|
|
||||||
@click="selectDevice(id)"
|
@click="selectDevice(id)"
|
||||||
/>
|
>
|
||||||
|
{{ id }}
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Panel>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Properties & Commands Panel -->
|
|
||||||
<div class="p-col-12 p-md-9" v-if="selected">
|
<div class="p-col-12 p-md-9" v-if="selected">
|
||||||
<Panel :header="'Properties — ' + selected" class="mb-3">
|
<div class="p-panel p-shadow-3">
|
||||||
<Tree :value="treeData" />
|
<div class="p-panel-header">Properties</div>
|
||||||
</Panel>
|
<div class="p-panel-content">
|
||||||
|
<PropertyTree :properties="devices[selected].property" />
|
||||||
<Panel :header="'Commands — ' + selected">
|
</div>
|
||||||
<Button
|
|
||||||
v-for="(cmd, name) in devices[selected].commands"
|
|
||||||
:key="name"
|
|
||||||
class="p-m-2"
|
|
||||||
:label="name"
|
|
||||||
@click="sendCommand(name)"
|
|
||||||
/>
|
|
||||||
</Panel>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MQTTService from './services/mqtt'
|
import { reactive } from 'vue'
|
||||||
|
import MQTTService from './services/mqtt.js'
|
||||||
|
import PropertyTree from './PropertyTree.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { PropertyTree },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
mqtt: null,
|
devices: reactive({}),
|
||||||
devices: {}, // { deviceId: {status, properties, commands} }
|
|
||||||
selected: null,
|
selected: null,
|
||||||
treeData: [],
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.mqtt = new MQTTService()
|
this.mqtt = new MQTTService()
|
||||||
|
|
||||||
// Device status
|
this.mqtt.client.on('connect', () => {
|
||||||
this.mqtt.subscribe('asset/+/status', (payload, topic) => {
|
console.log('Connected to MQTT broker')
|
||||||
const id = topic.split('/')[1]
|
this.mqtt.subscribe('asset/+/status', this.handleStatus)
|
||||||
if (!this.devices[id]) this.$set(this.devices, id, {status: 'OFFLINE', properties: {}, commands: {}})
|
this.mqtt.subscribe('asset/+/property/#', this.handleProperty)
|
||||||
this.devices[id].status = payload
|
|
||||||
})
|
|
||||||
|
|
||||||
// Device properties
|
|
||||||
this.mqtt.subscribe('asset/+/property/#', (payload, topic) => {
|
|
||||||
const parts = topic.split('/')
|
|
||||||
const id = parts[1]
|
|
||||||
const keyPath = parts.slice(3)
|
|
||||||
|
|
||||||
if (!this.devices[id]) this.$set(this.devices, id, {status: 'OFFLINE', properties: {}, commands: {}})
|
|
||||||
|
|
||||||
let target = this.devices[id].properties
|
|
||||||
keyPath.forEach((k, idx) => {
|
|
||||||
if (idx === keyPath.length - 1) {
|
|
||||||
try { target[k] = JSON.parse(payload) } catch(e) { target[k] = payload }
|
|
||||||
} else {
|
|
||||||
if (!target[k] || typeof target[k] !== 'object') this.$set(target, k, {})
|
|
||||||
target = target[k]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.selected) this.buildTree()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Device commands
|
|
||||||
this.mqtt.subscribe('asset/+/command/#', (payload, topic) => {
|
|
||||||
const id = topic.split('/')[1]
|
|
||||||
const cmdName = topic.split('/')[3]
|
|
||||||
|
|
||||||
if (!this.devices[id]) this.$set(this.devices, id, {status: 'OFFLINE', properties: {}, commands: {}})
|
|
||||||
try { this.$set(this.devices[id].commands, cmdName, JSON.parse(payload)) }
|
|
||||||
catch(e) { this.$set(this.devices[id].commands, cmdName, { raw: payload }) }
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selectDevice(id) {
|
selectDevice(id) {
|
||||||
this.selected = id
|
this.selected = id
|
||||||
this.buildTree()
|
|
||||||
},
|
},
|
||||||
sendCommand(cmdName) {
|
|
||||||
const topic = `asset/${this.selected}/command/${cmdName}`
|
handleStatus(payload, topic) {
|
||||||
this.mqtt.publish(topic, JSON.stringify({args:{}}))
|
const deviceId = topic.split('/')[1]
|
||||||
|
if (!this.devices[deviceId]) this.devices[deviceId] = { property: {}, status: null }
|
||||||
|
this.devices[deviceId].status = payload
|
||||||
},
|
},
|
||||||
buildTree() {
|
|
||||||
const buildNode = (obj, label) => {
|
handleProperty(payload, topic) {
|
||||||
if (typeof obj === 'object' && obj !== null) {
|
const parts = topic.split('/')
|
||||||
return { label, children: Object.entries(obj).map(([k,v]) => buildNode(v,k)) }
|
const deviceId = parts[1]
|
||||||
|
const propertyPath = parts.slice(3) // after asset/device/property/...
|
||||||
|
if (!this.devices[deviceId]) this.devices[deviceId] = { property: {}, status: null }
|
||||||
|
|
||||||
|
// traverse/create tree
|
||||||
|
let node = this.devices[deviceId].property
|
||||||
|
propertyPath.forEach((p, i) => {
|
||||||
|
if (i === propertyPath.length - 1) {
|
||||||
|
// leaf
|
||||||
|
node[p] = { value: payload, updated: Date.now() } // flash on update
|
||||||
} else {
|
} else {
|
||||||
return { label: `${label}: ${obj}` }
|
if (!node[p]) node[p] = {}
|
||||||
}
|
node = node[p]
|
||||||
}
|
|
||||||
this.treeData = Object.entries(this.devices[this.selected].properties).map(([k,v]) => buildNode(v,k))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.p-m-2 { margin: .5rem; }
|
|
||||||
.p-4 { padding: 1rem; }
|
|
||||||
.mb-3 { margin-bottom: 1rem; }
|
|
||||||
</style>
|
|
||||||
70
dashboard/src/PropertyTree.vue
Normal file
70
dashboard/src/PropertyTree.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<Tree
|
||||||
|
:value="treeData"
|
||||||
|
:expansion-keys="expansionKeys"
|
||||||
|
>
|
||||||
|
<template #default="{ node }">
|
||||||
|
<span
|
||||||
|
:style="{
|
||||||
|
backgroundColor: recentlyUpdated(node) ? '#fffa90' : 'transparent',
|
||||||
|
padding: '0 2px',
|
||||||
|
borderRadius: '2px'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ node.label }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Tree>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Tree from 'primevue/tree'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PropertyTree',
|
||||||
|
components: { Tree },
|
||||||
|
props: {
|
||||||
|
properties: { type: Object, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
treeData: [],
|
||||||
|
expansionKeys: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
properties: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.treeData = this.buildTree(this.properties)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
buildTree(obj, path = []) {
|
||||||
|
return Object.entries(obj).map(([key, val]) => {
|
||||||
|
if (val && typeof val === 'object' && 'value' in val) {
|
||||||
|
return {
|
||||||
|
key: [...path, key].join('/'),
|
||||||
|
label: `${key}: ${val.value}`,
|
||||||
|
data: val,
|
||||||
|
}
|
||||||
|
} else if (val && typeof val === 'object') {
|
||||||
|
const children = this.buildTree(val, [...path, key])
|
||||||
|
this.expansionKeys[[...path, key].join('/')] = true
|
||||||
|
return {
|
||||||
|
key: [...path, key].join('/'),
|
||||||
|
label: key,
|
||||||
|
children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
recentlyUpdated(node) {
|
||||||
|
const updated = node.data?.updated
|
||||||
|
return updated && Date.now() - updated < 500
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,19 +1,24 @@
|
|||||||
import mqtt from 'mqtt'
|
import mqtt from 'mqtt'
|
||||||
|
|
||||||
export default class MQTTService {
|
export default class MQTTService {
|
||||||
constructor(brokerUrl = 'ws://localhost:8083/mqtt', clientId = null) {
|
constructor(brokerUrl = 'ws://127.0.0.1:8083/mqtt', clientId = null) {
|
||||||
this.clientId = clientId || 'vue-client-' + Math.random().toString(16).substr(2, 8)
|
this.clientId = clientId || 'vue-client-' + Math.random().toString(16).substr(2, 8)
|
||||||
this.client = mqtt.connect(brokerUrl, { clientId: this.clientId })
|
this.client = mqtt.connect(brokerUrl, { clientId: this.clientId })
|
||||||
this.subscriptions = {}
|
this.subscriptions = [] // array of {topic, callback}
|
||||||
|
|
||||||
this.client.on('connect', () => console.log('Connected to MQTT broker'))
|
this.client.on('connect', () => console.log('Connected to MQTT broker'))
|
||||||
this.client.on('message', (topic, payload) => {
|
this.client.on('message', (topic, payload) => {
|
||||||
if (this.subscriptions[topic]) this.subscriptions[topic](payload.toString(), topic)
|
// iterate over subscriptions and check for matches
|
||||||
|
this.subscriptions.forEach(({topic: subTopic, callback}) => {
|
||||||
|
if (mqttMatch(subTopic, topic)) {
|
||||||
|
callback(payload.toString(), topic)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(topic, callback) {
|
subscribe(topic, callback) {
|
||||||
this.subscriptions[topic] = callback
|
this.subscriptions.push({topic, callback})
|
||||||
this.client.subscribe(topic)
|
this.client.subscribe(topic)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,3 +26,10 @@ export default class MQTTService {
|
|||||||
this.client.publish(topic, message, options)
|
this.client.publish(topic, message, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// helper function for MQTT wildcards
|
||||||
|
function mqttMatch(subTopic, topic) {
|
||||||
|
// replace MQTT wildcards with RegExp
|
||||||
|
const regex = '^' + subTopic.replace('+', '[^/]+').replace('#', '.+') + '$'
|
||||||
|
return new RegExp(regex).test(topic)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user