需要知道Eclipse Ditto是什么 、怎么安装就点击下面的链接查阅。
Eclipse Ditto 、 Mosquitto MQTT 、 OpenModelica 开发工具-CSDN博客
Eclipse Ditto 开发环境 搭建-CSDN博客
Eclipse Ditto:开源物联网数字孪生平台技术深度解析 - 东峰叵,com - 博客园
一、主要内容
本文主要讲解Eclipse Ditto物模型的搭建,及数据传输步骤;供大家参考。
二、界面预览
下发的数据:
对应的控制面板:
三、Eclipse Ditto 物模型
(1)创建物模型
curl -X PUT http://localhost:8080/api/2/things/my-demo:device001 \ -u ditto:ditto \ -H "Content-Type: application/json" \ -d '{ "attributes": { "deviceName": "粉体料仓设备", "model": "TSK-001", "location": "1号车间" }, "features": { "sensor": { "properties": { "pressure": 0, "temperature": 25, "level": 0, "coConcentration": 0 } }, "actuator": { "properties": { "valveOpen": false, "fanRun": false } } } }' 如果需要可以增加: { "thingId": "the.namespace:the-thing-id", "policyId": "the.namespace:the-policy-id" }(2)测试反馈
ian@hang:~$ curl http://localhost:8080/api/2/things -u ditto:ditto [{"thingId":"my-demo:device001","policyId":"my-demo:device001","attributes":{"deviceName":"粉体料仓设备","model":"TSK-001","location":"1号车间"},"features":{"sensor":{"properties":{"pressure":-60,"temperature":35,"level":85,"coConcentration":22}},"actuator":{"properties":{"valveOpen":false,"fanRun":false}}}}]四、代码文件展示
src/utils/dittoWs.ts type DittoMsgCallback = (msg: Record<string, any>) => void export class DittoWebSocket { private ws: WebSocket | null = null private url = '/ditto-ws' private msgCallback: DittoMsgCallback | null = null private reconnectTimer: number | null = null private readonly RECONNECT_DELAY = 3000 connect() { if (this.ws && this.ws.readyState === WebSocket.OPEN) return this.ws = new WebSocket(this.url) this.bindEvent() } private bindEvent() { if (!this.ws) return this.ws.onopen = () => { console.log('✅ Ditto WebSocket 代理连接成功') this.subscribeAllThings() } this.ws.onmessage = (ev) => { try { const data = JSON.parse(ev.data) console.log('收到Ditto原始消息:', data) // 过滤错误消息,不抛给页面 if(data.topic && data.topic.includes('errors')) return this.msgCallback?.(data) } catch (err) { console.error('消息解析失败', ev.data, err) } } this.ws.onclose = () => { console.warn('WS断开,3s后重连') this.reconnect() } this.ws.onerror = (e) => { console.error('WS异常', e) this.ws?.close() } } private reconnect() { if (this.reconnectTimer) clearTimeout(this.reconnectTimer) this.reconnectTimer = window.setTimeout(() => this.connect(), this.RECONNECT_DELAY) } // ✅ 修正订阅格式(符合 Ditto WebSocket API) subscribeAllThings() { const subCmd = { topic: "_/_/things/twin/commands/subscribe", headers: { "response-required": false }, path: "/" } this.send(subCmd) console.log('✅ 标准订阅报文已发送') } // ✅ 新增:检索指定 thing 的完整状态 retrieveThing(thingId: string) { const [ns, id] = thingId.split(':') const cmd = { topic: `${ns}/${id}/things/twin/commands/retrieve`, headers: { "response-required": true }, path: "/" } this.send(cmd) console.log(`✅ 检索报文已发送: ${thingId}`) } send(data: Record<string, any>) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { console.warn('WS未就绪,发送失败') return } this.ws.send(JSON.stringify(data)) } setMessageCallback(cb: DittoMsgCallback) { this.msgCallback = cb } close() { if (this.reconnectTimer) clearTimeout(this.reconnectTimer) this.ws?.close() this.ws = null } // 控制执行器(格式正确,未改动) controlActuator(thingId: string, valveOpen: boolean, fanRun: boolean) { const [ns, id] = thingId.split(':') const cmd = { topic: `${ns}/${id}/things/twin/commands/modify`, path: 'features/actuator/properties', value: { valveOpen, fanRun } } this.send(cmd) } } export const dittoWs = new DittoWebSocket() DigitalTwin.vue文件内容 <template> <div class="twin-container"> <h2 class="title">数字孪生设备实时面板</h2> <div v-if="deviceData" class="device-card"> <p class="row"> <span class="label">设备ID:</span> <span class="val">{{ deviceId }}</span> </p> <p class="row"> <span class="label">仓内负压:</span> <span class="val">{{ deviceData.features?.sensor?.properties?.pressure }} kPa</span> </p> <p class="row"> <span class="label">温度:</span> <span class="val">{{ deviceData.features?.sensor?.properties?.temperature }} ℃</span> </p> <p class="row"> <span class="label">料位:</span> <span class="val">{{ deviceData.features?.sensor?.properties?.level }} %</span> </p> <p class="row"> <span class="label">CO浓度:</span> <span class="val">{{ deviceData.features?.sensor?.properties?.coConcentration }}</span> </p> <div class="divider"></div> <div class="btn-group"> <button @click="setValve(true)">打开卸料阀</button> <button @click="setValve(false)">关闭卸料阀</button> <button @click="setFan(true)">启动风机</button> <button @click="setFan(false)">停止风机</button> </div> </div> <div v-else class="loading"> 正在加载设备数据... </div> </div> </template> <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue' const deviceId = 'my-demo:device001' const deviceData = ref<any>(null) let pollTimer: number | null = null // 拉取设备全量数据 async function fetchThing() { try { const res = await fetch(`/api/2/things/${deviceId}`) if (res.ok) { deviceData.value = await res.json() } } catch (err) { console.error('拉取设备数据失败', err) } } // 修改执行器状态 async function updateActuator(data: Record<string, boolean>) { try { await fetch(`/api/2/things/${deviceId}/features/actuator/properties`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) fetchThing() } catch (err) { console.error('修改执行器失败', err) } } // 按钮操作 const setValve = (open: boolean) => { if (!deviceData.value) return const fanRun = deviceData.value.features?.actuator?.properties?.fanRun ?? false updateActuator({ valveOpen: open, fanRun }) } const setFan = (run: boolean) => { if (!deviceData.value) return const valveOpen = deviceData.value.features?.actuator?.properties?.valveOpen ?? false updateActuator({ valveOpen, fanRun: run }) } onMounted(() => { fetchThing() // 2秒轮询一次,模拟实时更新 pollTimer = window.setInterval(fetchThing, 2000) }) onUnmounted(() => { if (pollTimer) clearInterval(pollTimer) }) </script> <style scoped> .twin-container { padding: 16px; color: #e8f0ff; } .title { margin: 0 0 16px; font-size: 18px; color: #ffffff; } .device-card { font-size: 14px; } .row { display: flex; margin: 8px 0; } .label { width: 100px; color: #a0c4e8; } .val { color: #fff; } .divider { height: 1px; background: rgba(255,255,255,0.2); margin: 16px 0; } .btn-group { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } button { border: none; border-radius: 4px; padding: 8px 0; background: #1f589c; color: white; cursor: pointer; } button:hover { background: #2b72c2; } .loading { color: #b4cce6; text-align: center; margin-top: 40px; } </style> vite.config.js文件内容 import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': resolve(__dirname, 'src') } }, server: { proxy: { // 直接对齐 Ditto 原生 /api 前缀,自动携带鉴权,无需路径重写 '/api': { target: 'http://localhost:8080', changeOrigin: true, headers: { Authorization: 'Basic ZGl0dG86ZGl0dG8=' } }, // WebSocket 通道代理(后续升级实时推送备用) '/ws/2': { target: 'ws://localhost:8080', ws: true, changeOrigin: true, headers: { Authorization: 'Basic ZGl0dG86ZGl0dG8=' } } } } })五、数据联动
(1)点击开卸料阀按钮和启动风机按钮后界面
(2)点击关闭卸料阀按钮和关闭风机按钮后界面
(3)检查数据流正常: