feat:【iot】modbus-tcp 协议接入 20%:初步实现,基于 dynamic-forging-wigderson.md 规划

This commit is contained in:
YunaiV
2026-01-17 19:36:51 +08:00
parent 1960901b79
commit 8587a549c6
7 changed files with 895 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import request from '@/config/axios'
/** Modbus 连接配置 VO */
export interface DeviceModbusConfigVO {
id?: number // 主键
deviceId: number // 设备编号
ip: string // Modbus 服务器 IP 地址
port: number // Modbus 服务器端口
slaveId: number // 从站地址
timeout: number // 连接超时时间,单位:毫秒
retryInterval: number // 重试间隔,单位:毫秒
status: number // 状态
}
/** Modbus 连接配置 API */
export const DeviceModbusConfigApi = {
/** 获取设备的 Modbus 连接配置 */
getModbusConfig: async (deviceId: number) => {
return await request.get<DeviceModbusConfigVO>({
url: `/iot/device-modbus-config/get`,
params: { deviceId }
})
},
/** 保存 Modbus 连接配置 */
saveModbusConfig: async (data: DeviceModbusConfigVO) => {
return await request.post({ url: `/iot/device-modbus-config/save`, data })
}
}

View File

@@ -0,0 +1,48 @@
import request from '@/config/axios'
/** Modbus 点位配置 VO */
export interface DeviceModbusPointVO {
id?: number // 主键
deviceId: number // 设备编号
thingModelId: number // 物模型属性编号
identifier: string // 属性标识符
name: string // 属性名称
functionCode: number // Modbus 功能码
registerAddress: number // 寄存器起始地址
registerCount: number // 寄存器数量
byteOrder: string // 字节序
rawDataType: string // 原始数据类型
scale: number // 缩放因子
pollInterval: number // 轮询间隔,单位:毫秒
status: number // 状态
}
/** Modbus 点位配置 API */
export const DeviceModbusPointApi = {
/** 获取设备的 Modbus 点位分页 */
getModbusPointPage: async (params: any) => {
return await request.get({ url: `/iot/device-modbus-point/page`, params })
},
/** 获取 Modbus 点位详情 */
getModbusPoint: async (id: number) => {
return await request.get<DeviceModbusPointVO>({
url: `/iot/device-modbus-point/get?id=${id}`
})
},
/** 创建 Modbus 点位配置 */
createModbusPoint: async (data: DeviceModbusPointVO) => {
return await request.post({ url: `/iot/device-modbus-point/create`, data })
},
/** 更新 Modbus 点位配置 */
updateModbusPoint: async (data: DeviceModbusPointVO) => {
return await request.put({ url: `/iot/device-modbus-point/update`, data })
},
/** 删除 Modbus 点位配置 */
deleteModbusPoint: async (id: number) => {
return await request.delete({ url: `/iot/device-modbus-point/delete?id=${id}` })
}
}

View File

@@ -0,0 +1,262 @@
<!-- Modbus 配置 -->
<template>
<div>
<!-- 连接配置区域 -->
<ContentWrap>
<div class="flex items-center justify-between mb-4">
<span class="text-lg font-medium">连接配置</span>
<el-button type="primary" @click="handleEditConfig">编辑</el-button>
</div>
<!-- 详情展示 -->
<el-descriptions :column="3" border direction="horizontal">
<el-descriptions-item label="IP 地址">
{{ modbusConfig.ip || '-' }}
</el-descriptions-item>
<el-descriptions-item label="端口">
{{ modbusConfig.port || '-' }}
</el-descriptions-item>
<el-descriptions-item label="从站地址">
{{ modbusConfig.slaveId || '-' }}
</el-descriptions-item>
<el-descriptions-item label="连接超时">
{{ modbusConfig.timeout ? `${modbusConfig.timeout} ms` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="重试间隔">
{{ modbusConfig.retryInterval ? `${modbusConfig.retryInterval} ms` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="modbusConfig.status === 0 ? 'success' : 'danger'">
{{ modbusConfig.status === 0 ? '启用' : '禁用' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
<!-- 点位配置区域 -->
<ContentWrap class="mt-4">
<div class="flex items-center justify-between mb-4">
<span class="text-lg font-medium">点位配置</span>
<el-button type="primary" @click="handleAddPoint">
<Icon icon="ep:plus" class="mr-1" />
新增点位
</el-button>
</div>
<!-- 搜索栏 -->
<el-form :model="queryParams" :inline="true" class="-mb-15px">
<el-form-item label="属性名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入属性名称"
clearable
class="!w-200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="标识符" prop="identifier">
<el-input
v-model="queryParams.identifier"
placeholder="请输入标识符"
clearable
class="!w-200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
重置
</el-button>
</el-form-item>
</el-form>
<!-- 点位列表 -->
<el-table v-loading="pointLoading" :data="pointList" :stripe="true" class="mt-4">
<el-table-column label="属性名称" align="center" prop="name" min-width="100" />
<el-table-column label="标识符" align="center" prop="identifier" min-width="100">
<template #default="scope">
<el-tag size="small" type="primary">{{ scope.row.identifier }}</el-tag>
</template>
</el-table-column>
<el-table-column label="功能码" align="center" prop="functionCode" min-width="140">
<template #default="scope">
{{ formatFunctionCode(scope.row.functionCode) }}
</template>
</el-table-column>
<el-table-column label="寄存器地址" align="center" prop="registerAddress" min-width="100">
<template #default="scope">
{{ formatRegisterAddress(scope.row.registerAddress) }}
</template>
</el-table-column>
<el-table-column label="寄存器数量" align="center" prop="registerCount" min-width="90" />
<el-table-column label="数据类型" align="center" prop="rawDataType" min-width="90">
<template #default="scope">
<el-tag size="small" type="info">{{ scope.row.rawDataType }}</el-tag>
</template>
</el-table-column>
<el-table-column label="字节序" align="center" prop="byteOrder" min-width="80" />
<el-table-column label="缩放因子" align="center" prop="scale" min-width="80" />
<el-table-column label="轮询间隔" align="center" prop="pollInterval" min-width="90">
<template #default="scope"> {{ scope.row.pollInterval }} ms </template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" min-width="80">
<template #default="scope">
<!-- TODO @AI dict-tag -->
<el-tag :type="scope.row.status === 0 ? 'success' : 'danger'" size="small">
{{ scope.row.status === 0 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="120">
<template #default="scope">
<el-button link type="primary" @click="handleEditPoint(scope.row)">编辑</el-button>
<el-button link type="danger" @click="handleDeletePoint(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getPointPage"
/>
</ContentWrap>
<!-- 连接配置弹窗 -->
<DeviceModbusConfigForm ref="configFormRef" :device-id="device.id" @success="getModbusConfig" />
<!-- 点位表单弹窗 -->
<DeviceModbusPointForm
ref="pointFormRef"
:device-id="device.id"
:thing-model-list="thingModelList"
@success="getPointPage"
/>
</div>
</template>
<script lang="ts" setup>
import { DeviceVO } from '@/api/iot/device/device'
import { ProductVO } from '@/api/iot/product/product'
import { ThingModelData } from '@/api/iot/thingmodel'
import { DeviceModbusConfigApi, DeviceModbusConfigVO } from '@/api/iot/device/modbus/config'
import { DeviceModbusPointApi, DeviceModbusPointVO } from '@/api/iot/device/modbus/point'
import { ModbusFunctionCodeOptions } from '@/views/iot/utils/constants'
import DeviceModbusConfigForm from './DeviceModbusConfigForm.vue'
import DeviceModbusPointForm from './DeviceModbusPointForm.vue'
defineOptions({ name: 'DeviceModbusConfig' })
const props = defineProps<{
device: DeviceVO
product: ProductVO
thingModelList: ThingModelData[]
}>()
const message = useMessage()
// ======================= 连接配置 =======================
// TODO @AI默认应该都是空的
const modbusConfig = ref<DeviceModbusConfigVO>({
deviceId: props.device.id,
ip: '',
port: 502,
slaveId: 1,
timeout: 3000,
retryInterval: 1000,
status: 0 // TODO @AI使用 CommonStatus
})
/** 获取连接配置 */
const getModbusConfig = async () => {
modbusConfig.value = await DeviceModbusConfigApi.getModbusConfig(props.device.id)
}
/** 编辑连接配置 */
const configFormRef = ref()
const handleEditConfig = () => {
configFormRef.value?.open(modbusConfig.value)
}
// ======================= 点位配置 =======================
const pointLoading = ref(false)
const pointList = ref<DeviceModbusPointVO[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
deviceId: props.device.id,
name: undefined as string | undefined,
identifier: undefined as string | undefined
})
/** 获取点位分页 */
const getPointPage = async () => {
pointLoading.value = true
try {
const data = await DeviceModbusPointApi.getModbusPointPage(queryParams)
pointList.value = data.list
total.value = data.total
} finally {
pointLoading.value = false
}
}
/** 搜索 */
const handleQuery = () => {
queryParams.pageNo = 1
getPointPage()
}
/** 重置搜索 */
const resetQuery = () => {
queryParams.name = undefined
queryParams.identifier = undefined
handleQuery()
}
/** 格式化功能码 */
const formatFunctionCode = (code: number) => {
const option = ModbusFunctionCodeOptions.find((item) => item.value === code)
return option ? option.label : `${code}`
}
/** 格式化寄存器地址为十六进制 */
const formatRegisterAddress = (address: number) => {
return '0x' + address.toString(16).toUpperCase().padStart(4, '0')
}
/** 新增点位 */
const pointFormRef = ref()
const handleAddPoint = () => {
pointFormRef.value?.open('create')
}
/** 编辑点位 */
const handleEditPoint = (row: DeviceModbusPointVO) => {
pointFormRef.value?.open('update', row.id)
}
/** 删除点位 */
const handleDeletePoint = async (id: number) => {
// TODO @AI最好点位的名字带上。参考别的模块
// TODO @AI参考别的注释。
await message.confirm('确定要删除该点位配置吗?')
await DeviceModbusPointApi.deleteModbusPoint(id)
message.success('删除成功')
await getPointPage()
}
/** 初始化 */
onMounted(async () => {
await getModbusConfig()
await getPointPage()
})
</script>

View File

@@ -0,0 +1,160 @@
<!-- Modbus 连接配置弹窗 -->
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="IP 地址" prop="ip">
<el-input v-model="formData.ip" placeholder="请输入 Modbus 服务器 IP 地址" />
</el-form-item>
<el-form-item label="请输入 Modbus 端口" prop="port">
<el-input-number
v-model="formData.port"
:min="1"
:max="65535"
controls-position="right"
class="!w-full"
/>
</el-form-item>
<el-form-item label="从站地址" prop="slaveId">
<el-input-number
v-model="formData.slaveId"
:min="1"
:max="247"
controls-position="right"
class="!w-full"
/>
<!-- TODO @AIplacehoder 需要写下 -->
</el-form-item>
<el-form-item label="连接超时" prop="timeout">
<el-input-number
v-model="formData.timeout"
:min="1000"
:step="1000"
controls-position="right"
class="!w-full"
/>
<!-- TODO @AIplacehoder 需要写下 -->
<div class="text-xs text-gray-400 mt-1">单位毫秒</div>
<!-- TODO @AI上面的毫秒可以去掉 -->
</el-form-item>
<el-form-item label="重试间隔" prop="retryInterval">
<el-input-number
v-model="formData.retryInterval"
:min="1000"
:step="1000"
controls-position="right"
class="!w-full"
/>
<!-- TODO @AIplacehoder 需要写下 -->
<div class="text-xs text-gray-400 mt-1">单位毫秒</div>
<!-- TODO @AI上面的毫秒可以去掉 -->
</el-form-item>
<el-form-item label="状态" prop="status">
<!-- TODO @AI看看别的禁用应该是怎么样的 -->
<el-switch
v-model="formData.status"
:active-value="0"
:inactive-value="1"
active-text="启用"
inactive-text="禁用"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="formLoading">确定</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { DeviceModbusConfigApi, DeviceModbusConfigVO } from '@/api/iot/device/modbus/config'
defineOptions({ name: 'DeviceModbusConfigForm' })
const props = defineProps<{
deviceId: number
}>()
const emit = defineEmits<{
(e: 'success'): void
}>()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('编辑 Modbus 连接配置')
const formLoading = ref(false)
const formRef = ref()
/** 表单数据 */
// TODO @AI
const formData = ref<DeviceModbusConfigVO>({
deviceId: props.deviceId,
ip: '',
port: 502,
slaveId: 1,
timeout: 3000,
retryInterval: 10000,
status: 0 // TODO @AIcommonstatusenum
})
/** 表单校验规则 */
const formRules = {
ip: [{ required: true, message: '请输入 IP 地址', trigger: 'blur' }],
port: [{ required: true, message: '请输入端口', trigger: 'blur' }],
slaveId: [{ required: true, message: '请输入从站地址', trigger: 'blur' }],
timeout: [{ required: true, message: '请输入连接超时时间', trigger: 'blur' }],
retryInterval: [{ required: true, message: '请输入重试间隔', trigger: 'blur' }]
}
/** 打开弹窗 */
const open = async (data?: DeviceModbusConfigVO) => {
dialogVisible.value = true
resetForm()
// 编辑模式
if (data && data.id) {
formData.value = { ...data }
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
deviceId: props.deviceId,
ip: '',
port: 502,
slaveId: 1,
timeout: 3000,
retryInterval: 1000,
status: 0
}
formRef.value?.resetFields()
}
/** 提交表单 */
const submitForm = async () => {
// TODO @AI注释需要补充下参考别的模块
try {
await formRef.value?.validate()
formLoading.value = true
formData.value.deviceId = props.deviceId
await DeviceModbusConfigApi.saveModbusConfig(formData.value)
message.success('保存成功')
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
/** 暴露方法 */
defineExpose({ open })
</script>

View File

@@ -0,0 +1,315 @@
<!-- Modbus 点位表单弹窗 -->
<template>
<!-- TODO @AIplaceholder 都提供下 -->
<Dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="物模型属性" prop="thingModelId">
<el-select
v-model="formData.thingModelId"
placeholder="请选择物模型属性"
filterable
class="!w-full"
@change="handleThingModelChange"
>
<!-- TODO @AI增加 option 里的告警 -->
<el-option
v-for="item in propertyList"
:key="item.id"
:label="`${item.name} (${item.identifier})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="功能码" prop="functionCode">
<!-- TODO @AIselect -->
<el-radio-group v-model="formData.functionCode">
<el-radio
v-for="item in ModbusFunctionCodeOptions"
:key="item.value"
:value="item.value"
class="!mr-4"
>
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- TODO @AI不要转换直接输入 -->
<el-form-item label="寄存器地址" prop="registerAddress">
<el-input
v-model="registerAddressInput"
placeholder="请输入寄存器地址(支持十进制或十六进制如 0x0000"
@blur="handleRegisterAddressBlur"
>
<template #append>
<span class="text-gray-500">
= {{ formatRegisterAddress(formData.registerAddress) }}
</span>
</template>
</el-input>
</el-form-item>
<el-form-item label="寄存器数量" prop="registerCount">
<el-input-number
v-model="formData.registerCount"
:min="1"
:max="125"
controls-position="right"
class="!w-full"
/>
</el-form-item>
<el-form-item label="原始数据类型" prop="rawDataType">
<el-select
v-model="formData.rawDataType"
placeholder="请选择数据类型"
class="!w-full"
@change="handleRawDataTypeChange"
>
<el-option
v-for="item in ModbusRawDataTypeOptions"
:key="item.value"
:label="`${item.label} - ${item.description}`"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="字节序" prop="byteOrder">
<el-select v-model="formData.byteOrder" placeholder="请选择字节序" class="!w-full">
<el-option
v-for="item in currentByteOrderOptions"
:key="item.value"
:label="`${item.label} - ${item.description}`"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="缩放因子" prop="scale">
<el-input-number
v-model="formData.scale"
:precision="6"
:step="0.1"
controls-position="right"
class="!w-full"
/>
<!-- TODO @AItip使用默认的 el-input-number 里的 -->
<div class="text-xs text-gray-400 mt-1">
读取时实际值 = 原始值 × 缩放因子写入时原始值 = 实际值 ÷ 缩放因子
</div>
</el-form-item>
<el-form-item label="轮询间隔" prop="pollInterval">
<el-input-number
v-model="formData.pollInterval"
:min="100"
:step="1000"
controls-position="right"
class="!w-full"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<!-- TODO @AI参考下别的模块开关 -->
<el-switch
v-model="formData.status"
:active-value="0"
:inactive-value="1"
active-text="启用"
inactive-text="禁用"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="formLoading">确定</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { ThingModelData } from '@/api/iot/thingmodel'
import { DeviceModbusPointApi, DeviceModbusPointVO } from '@/api/iot/device/modbus/point'
import {
ModbusFunctionCodeOptions,
ModbusRawDataTypeOptions,
getByteOrderOptions
} from '@/views/iot/utils/constants'
defineOptions({ name: 'DeviceModbusPointForm' })
const props = defineProps<{
deviceId: number
thingModelList: ThingModelData[]
}>()
const emit = defineEmits<{
(e: 'success'): void
}>()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref<'create' | 'update'>('create')
const formRef = ref()
// TODO @AI注释的风格你看看是不是有些地方要尾注释和别的模块保持一样的风格
/** 表单数据 */
// TODO @AI里面的枚举类型改成直接用枚举
const formData = ref<DeviceModbusPointVO>({
deviceId: props.deviceId,
thingModelId: 0,
identifier: '',
name: '',
functionCode: 3,
registerAddress: 0,
registerCount: 1,
byteOrder: 'AB',
rawDataType: 'INT16',
scale: 1,
pollInterval: 5000,
status: 0
})
/** 寄存器地址输入框(支持十六进制) */
const registerAddressInput = ref('0')
/** 表单校验规则 */
const formRules = {
thingModelId: [{ required: true, message: '请选择物模型属性', trigger: 'change' }],
functionCode: [{ required: true, message: '请选择功能码', trigger: 'change' }],
registerAddress: [{ required: true, message: '请输入寄存器地址', trigger: 'blur' }],
registerCount: [{ required: true, message: '请输入寄存器数量', trigger: 'blur' }],
rawDataType: [{ required: true, message: '请选择数据类型', trigger: 'change' }],
pollInterval: [{ required: true, message: '请输入轮询间隔', trigger: 'blur' }]
}
/** 筛选属性类型的物模型 */
const propertyList = computed(() => {
return props.thingModelList.filter((item) => item.type === 1) // type=1 为属性
})
/** 当前字节序选项(根据数据类型动态变化) */
const currentByteOrderOptions = computed(() => {
return getByteOrderOptions(formData.value.rawDataType)
})
/** 打开弹窗 */
const open = async (type: 'create' | 'update', id?: number) => {
dialogVisible.value = true
formType.value = type
// TODO @AI参考别的模块写法
dialogTitle.value = type === 'create' ? '新增 Modbus 点位' : '编辑 Modbus 点位'
resetForm()
if (type === 'update' && id) {
formLoading.value = true
try {
const data = await DeviceModbusPointApi.getModbusPoint(id)
formData.value = data
registerAddressInput.value = formatRegisterAddress(data.registerAddress)
} finally {
formLoading.value = false
}
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
deviceId: props.deviceId,
thingModelId: 0,
identifier: '',
name: '',
functionCode: 3,
registerAddress: 0,
registerCount: 1,
byteOrder: 'AB',
rawDataType: 'INT16',
scale: 1,
pollInterval: 5000,
status: 0
}
registerAddressInput.value = '0'
formRef.value?.resetFields()
}
/** 物模型属性变化 */
const handleThingModelChange = (thingModelId: number) => {
const thingModel = props.thingModelList.find((item) => item.id === thingModelId)
// TODO @AI这里有 linter 告警,可以看看。
if (thingModel) {
formData.value.identifier = thingModel.identifier
formData.value.name = thingModel.name
}
}
/** 数据类型变化 */
const handleRawDataTypeChange = (rawDataType: string) => {
// 根据数据类型自动设置寄存器数量
const option = ModbusRawDataTypeOptions.find((item) => item.value === rawDataType)
if (option && option.registerCount > 0) {
formData.value.registerCount = option.registerCount
}
// 重置字节序为第一个选项
const byteOrderOptions = getByteOrderOptions(rawDataType)
if (byteOrderOptions.length > 0) {
formData.value.byteOrder = byteOrderOptions[0].value
}
}
/** 寄存器地址输入框失焦 */
const handleRegisterAddressBlur = () => {
const input = registerAddressInput.value.trim()
let address = 0
if (input.toLowerCase().startsWith('0x')) {
// 十六进制
address = parseInt(input, 16)
} else {
// 十进制
address = parseInt(input, 10)
}
if (isNaN(address) || address < 0) {
address = 0
}
formData.value.registerAddress = address
registerAddressInput.value = formatRegisterAddress(address)
}
/** 格式化寄存器地址为十六进制 */
const formatRegisterAddress = (address: number) => {
return '0x' + address.toString(16).toUpperCase().padStart(4, '0')
}
/** 提交表单 */
const submitForm = async () => {
try {
await formRef.value?.validate()
formLoading.value = true
// TODO @AI这里的注释风格可以看看。
if (formType.value === 'create') {
await DeviceModbusPointApi.createModbusPoint(formData.value)
message.success('创建成功')
} else {
await DeviceModbusPointApi.updateModbusPoint(formData.value)
message.success('更新成功')
}
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
/** 暴露方法 */
defineExpose({ open })
</script>

View File

@@ -36,6 +36,18 @@
@success="getDeviceData"
/>
</el-tab-pane>
<el-tab-pane
label="Modbus 配置"
name="modbus"
v-if="product.codecType === 'ModbusTcp'"
>
<DeviceModbusConfig
v-if="activeTab === 'modbus'"
:device="device"
:product="product"
:thing-model-list="thingModelList"
/>
</el-tab-pane>
</el-tabs>
</el-col>
</template>
@@ -50,6 +62,7 @@ import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue'
import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue'
import DeviceDetailConfig from './DeviceDetailConfig.vue'
import DeviceModbusConfig from './DeviceModbusConfig.vue'
defineOptions({ name: 'IoTDeviceDetail' })

View File

@@ -541,3 +541,71 @@ export const JSON_PARAMS_EXAMPLE_VALUES = {
[IoTDataSpecsDataTypeEnum.ARRAY]: { display: '[]', value: [] },
DEFAULT: { display: '""', value: '' }
} as const
// ========== Modbus 相关常量 ==========
/** Modbus 功能码枚举 */
export const ModbusFunctionCodeEnum = {
READ_COILS: 1, // 读线圈
READ_DISCRETE_INPUTS: 2, // 读离散输入
READ_HOLDING_REGISTERS: 3, // 读保持寄存器
READ_INPUT_REGISTERS: 4 // 读输入寄存器
} as const
/** Modbus 功能码选项 */
export const ModbusFunctionCodeOptions = [
{ value: 1, label: '01 - 读线圈 (Coils)', description: '可读写布尔值' },
{ value: 2, label: '02 - 读离散输入 (Discrete Inputs)', description: '只读布尔值' },
{ value: 3, label: '03 - 读保持寄存器 (Holding Registers)', description: '可读写16位数据' },
{ value: 4, label: '04 - 读输入寄存器 (Input Registers)', description: '只读16位数据' }
]
/** Modbus 原始数据类型枚举 */
export const ModbusRawDataTypeEnum = {
INT16: 'INT16',
UINT16: 'UINT16',
INT32: 'INT32',
UINT32: 'UINT32',
FLOAT: 'FLOAT',
DOUBLE: 'DOUBLE',
BOOLEAN: 'BOOLEAN',
STRING: 'STRING'
} as const
/** Modbus 原始数据类型选项 */
export const ModbusRawDataTypeOptions = [
{ value: 'INT16', label: 'INT16', description: '有符号16位整数', registerCount: 1 },
{ value: 'UINT16', label: 'UINT16', description: '无符号16位整数', registerCount: 1 },
{ value: 'INT32', label: 'INT32', description: '有符号32位整数', registerCount: 2 },
{ value: 'UINT32', label: 'UINT32', description: '无符号32位整数', registerCount: 2 },
{ value: 'FLOAT', label: 'FLOAT', description: '32位浮点数', registerCount: 2 },
{ value: 'DOUBLE', label: 'DOUBLE', description: '64位浮点数', registerCount: 4 },
{ value: 'BOOLEAN', label: 'BOOLEAN', description: '布尔值', registerCount: 1 },
{ value: 'STRING', label: 'STRING', description: '字符串', registerCount: 0 }
]
/** Modbus 字节序选项 - 16位 */
export const ModbusByteOrder16Options = [
{ value: 'AB', label: 'AB', description: '大端序' },
{ value: 'BA', label: 'BA', description: '小端序' }
]
/** Modbus 字节序选项 - 32位 */
export const ModbusByteOrder32Options = [
{ value: 'ABCD', label: 'ABCD', description: '大端序' },
{ value: 'CDAB', label: 'CDAB', description: '大端字交换' },
{ value: 'DCBA', label: 'DCBA', description: '小端序' },
{ value: 'BADC', label: 'BADC', description: '小端字交换' }
]
/** 根据数据类型获取字节序选项 */
export const getByteOrderOptions = (rawDataType: string) => {
if (['INT32', 'UINT32', 'FLOAT'].includes(rawDataType)) {
return ModbusByteOrder32Options
}
if (rawDataType === 'DOUBLE') {
// 64位暂时复用32位字节序
return ModbusByteOrder32Options
}
return ModbusByteOrder16Options
}