Eclipse Ditto 物模型搭建

需要知道Eclipse Ditto是什么 、怎么安装就点击下面的链接查阅。

Eclipse Ditto 、 Mosquitto MQTT 、 OpenModelica 开发工具-CSDN博客

Eclipse Ditto 开发环境 搭建-CSDN博客

一、主要内容

本文主要讲解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
        }
      }
    }
  }'

(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)检查数据流正常: