Commit 846eb423c2b0c38f7ce168977f9749a40265cc13

Authored by xp.Huang
2 parents 72019892 3b113770

Merge branch 'fix/DEFECT-1855' into 'main_dev'

fix: 修复组态属性下发tcp设备没有缩放因子

See merge request yunteng/thingskit-scada!205
@@ -27,7 +27,7 @@ export interface GenModbusCommandType { @@ -27,7 +27,7 @@ export interface GenModbusCommandType {
27 crc: string 27 crc: string
28 deviceCode: string 28 deviceCode: string
29 method: string 29 method: string
30 - registerAddress: string 30 + registerAddress: number
31 registerNumber?: number 31 registerNumber?: number
32 registerValues?: number[] 32 registerValues?: number[]
33 } 33 }
  1 +import type { Specs, StructJSON } from '@/api/device/model'
  2 +import type { NodeDataDataSourceJsonType } from '@/api/node/model'
  3 +import { type FormSchema, useComponentRegister } from '@/components/Form'
  4 +import { ComponentEnum } from '@/components/Form/src/enum'
  5 +import { ThingsModelForm, validateTCPCustomCommand } from '@/core/Library/components/ThingsModelForm'
  6 +import { getFormSchemas } from '@/core/Library/components/ThingsModelForm/config'
  7 +import { CodeTypeEnum, DataTypeEnum, TransportTypeEnum } from '@/enums/datasource'
  8 +import { TCPObjectModelActionTypeEnum } from '@/enums/objectModelEnum'
  9 +
  10 +useComponentRegister(ComponentEnum.THINGS_MODEL_FORM, ThingsModelForm)
  11 +
  12 +export enum FormFieldsEnum {
  13 + ATTR_VALUE = 'attrValue',
  14 + PASSWORD = 'password',
  15 +}
  16 +
  17 +export interface AttributeDeliverModalOpenParamsType {
  18 + title?: string
  19 + operationPassword?: string
  20 + operationPasswordEnable?: boolean
  21 + dataSourceJson: NodeDataDataSourceJsonType
  22 +}
  23 +
  24 +function getStructJsonFromDataSourceJson(dataSourceJson: NodeDataDataSourceJsonType): StructJSON {
  25 + const { attrInfo } = dataSourceJson
  26 + const { identifier, name, detail } = attrInfo || {}
  27 + return {
  28 + functionName: name,
  29 + identifier,
  30 + dataType: detail.dataType,
  31 + }
  32 +}
  33 +
  34 +function getTCPModbusSchemas({ structJson, required, actionType }: { structJson: StructJSON; required?: boolean; actionType: string }): FormSchema {
  35 + const { dataType } = structJson
  36 + const { specs, type } = dataType || {}
  37 + const { valueRange, length = 10240 } = specs! as Specs
  38 + const { max = Number.MAX_SAFE_INTEGER, min = Number.MIN_SAFE_INTEGER } = valueRange || {}
  39 +
  40 + function createInputNumber({
  41 + identifier,
  42 + functionName,
  43 + }: StructJSON): FormSchema {
  44 + return {
  45 + field: identifier,
  46 + label: functionName,
  47 + component: ComponentEnum.INPUT_NUMBER,
  48 + required,
  49 + componentProps: {
  50 + max: actionType === TCPObjectModelActionTypeEnum.BOOL ? 1 : max,
  51 + min: actionType === TCPObjectModelActionTypeEnum.BOOL ? 0 : min,
  52 + precision: actionType === TCPObjectModelActionTypeEnum.BOOL ? 0 : 2,
  53 + },
  54 + }
  55 + }
  56 +
  57 + const createInput = ({ identifier, functionName }: StructJSON): FormSchema => {
  58 + return {
  59 + field: identifier,
  60 + label: functionName,
  61 + component: ComponentEnum.INPUT,
  62 + rules: [
  63 + {
  64 + required,
  65 + message: `${functionName}是必填项`,
  66 + },
  67 + {
  68 + validator: validateTCPCustomCommand,
  69 + },
  70 + ],
  71 + componentProps: {
  72 + maxLength: length,
  73 + },
  74 + } as FormSchema
  75 + }
  76 +
  77 + return type === DataTypeEnum.STRING ? createInput(structJson) : createInputNumber(structJson)
  78 +}
  79 +
  80 +export const createFormSchemas = ({ operationPassword, operationPasswordEnable, dataSourceJson }: AttributeDeliverModalOpenParamsType): FormSchema[] => {
  81 + const schemas: FormSchema[] = []
  82 +
  83 + const { deviceInfo } = dataSourceJson
  84 + if (deviceInfo?.transportType === TransportTypeEnum.TCP && deviceInfo.codeType === CodeTypeEnum.MODBUS_RTU && dataSourceJson.deviceInfo?.codeType) {
  85 + schemas.push(getTCPModbusSchemas({ required: true, structJson: getStructJsonFromDataSourceJson(dataSourceJson), actionType: dataSourceJson.deviceInfo?.codeType }))
  86 + }
  87 + else {
  88 + const isStructType = dataSourceJson.attrInfo?.detail?.dataType?.type === DataTypeEnum.STRUCT
  89 + schemas.push(
  90 + ...getFormSchemas({ structJSON: isStructType ? dataSourceJson?.attrInfo?.detail?.dataType?.specs as StructJSON[] || [] : [getStructJsonFromDataSourceJson(dataSourceJson)], required: !isStructType }),
  91 + )
  92 + }
  93 +
  94 + if (operationPassword && operationPasswordEnable) {
  95 + schemas.unshift({
  96 + field: FormFieldsEnum.PASSWORD,
  97 + label: '操作密码',
  98 + component: ComponentEnum.INPUT_PAWSSWORD,
  99 + required: true,
  100 + rules: [
  101 + {
  102 + validator(_rule, value) {
  103 + if (value && value !== operationPassword) return Promise.reject(new Error('操作密码不正确'))
  104 + return Promise.resolve()
  105 + },
  106 + },
  107 + ],
  108 + })
  109 + }
  110 +
  111 + return schemas
  112 +}
1 <script setup lang="ts"> 1 <script setup lang="ts">
2 import { Modal } from 'ant-design-vue' 2 import { Modal } from 'ant-design-vue'
3 import { nextTick, ref, unref } from 'vue' 3 import { nextTick, ref, unref } from 'vue'
4 -import type { FormSchema } from '@/components/Form' 4 +import type { AttributeDeliverModalOpenParamsType } from './AttributeDeliverModal.config'
  5 +import { createFormSchemas } from './AttributeDeliverModal.config'
  6 +import { useGetModbusCommand } from './useGetModbusCommand'
5 import { BasicForm, useForm } from '@/components/Form' 7 import { BasicForm, useForm } from '@/components/Form'
6 -import { ComponentEnum, FormLayoutEnum } from '@/components/Form/src/enum' 8 +import { FormLayoutEnum } from '@/components/Form/src/enum'
  9 +import type { NodeDataDataSourceJsonType } from '@/api/node/model'
  10 +import { CodeTypeEnum, DataTypeEnum, TransportTypeEnum } from '@/enums/datasource'
7 11
8 const resolveFn = ref<Fn>() 12 const resolveFn = ref<Fn>()
9 13
@@ -11,61 +15,50 @@ const visible = ref(false) @@ -11,61 +15,50 @@ const visible = ref(false)
11 15
12 const password = ref() 16 const password = ref()
13 17
  18 +const currentDataSourceJson = ref<NodeDataDataSourceJsonType>()
  19 +
14 const [register, { getFieldsValue, resetFields, validate, setProps, clearValidate }] = useForm({ 20 const [register, { getFieldsValue, resetFields, validate, setProps, clearValidate }] = useForm({
15 layout: FormLayoutEnum.VERTICAL, 21 layout: FormLayoutEnum.VERTICAL,
16 showActionButtonGroup: false, 22 showActionButtonGroup: false,
17 }) 23 })
18 24
19 -enum FormFieldsEnum {  
20 - ATTR_VALUE = 'attrValue',  
21 - PASSWORD = 'password',  
22 -}  
23 -  
24 -const createFormSchemas = (title?: string, password?: string, operationPasswordEnable?: boolean): FormSchema[] => {  
25 - const schemas: FormSchema[] = [  
26 - {  
27 - field: FormFieldsEnum.ATTR_VALUE,  
28 - label: title || '属性值',  
29 - component: ComponentEnum.INPUT,  
30 - required: true,  
31 - },  
32 - ]  
33 -  
34 - if (password && operationPasswordEnable) {  
35 - schemas.unshift({  
36 - field: FormFieldsEnum.PASSWORD,  
37 - label: '操作密码',  
38 - component: ComponentEnum.INPUT_PAWSSWORD,  
39 - required: true,  
40 - rules: [  
41 - {  
42 - validator(_rule, value) {  
43 - if (value && value !== password) return Promise.reject(new Error('操作密码不正确'))  
44 - return Promise.resolve()  
45 - },  
46 - },  
47 - ],  
48 - })  
49 - }  
50 -  
51 - return schemas  
52 -}  
53 -  
54 -const open = async ({ title, operationPassword, operationPasswordEnable }: Partial<Record<'operationPassword' | 'title', string>> & { operationPasswordEnable: boolean }) => { 25 +const open = async ({ title, operationPassword, operationPasswordEnable, dataSourceJson }: AttributeDeliverModalOpenParamsType) => {
55 visible.value = true 26 visible.value = true
56 password.value = operationPassword 27 password.value = operationPassword
  28 + currentDataSourceJson.value = dataSourceJson
57 return new Promise((resolve) => { 29 return new Promise((resolve) => {
58 resolveFn.value = resolve 30 resolveFn.value = resolve
59 nextTick(() => { 31 nextTick(() => {
60 - setProps({ schemas: createFormSchemas(title, operationPassword, operationPasswordEnable) }) 32 + setProps({ schemas: createFormSchemas({ title, operationPassword, operationPasswordEnable, dataSourceJson }) })
61 }) 33 })
62 }) 34 })
63 } 35 }
64 36
  37 +async function getResult() {
  38 + const result = getFieldsValue()
  39 + const isTCPModbusDevice = unref(currentDataSourceJson)?.deviceInfo?.transportType === TransportTypeEnum.TCP && unref(currentDataSourceJson)?.deviceInfo?.codeType === CodeTypeEnum.MODBUS_RTU
  40 + const isStructJSON = unref(currentDataSourceJson)?.attrInfo?.detail?.dataType?.type === DataTypeEnum.STRUCT
  41 + const attrKey = unref(currentDataSourceJson)!.attr
  42 + if (!isTCPModbusDevice) { return isStructJSON ? result : result[attrKey] }
  43 + else {
  44 + const value = result[attrKey]
  45 + const isString = unref(currentDataSourceJson)?.attrInfo?.detail?.dataType?.type === DataTypeEnum.STRING
  46 +
  47 + if (isString) return value
  48 +
  49 + const { getModbusCommand, validateCanGetCommand } = useGetModbusCommand()
  50 + if (!validateCanGetCommand(unref(currentDataSourceJson)!.attrInfo.extensionDesc, unref(currentDataSourceJson)!.deviceInfo?.code).flag) return
  51 +
  52 + const res = await getModbusCommand(value as unknown as number, unref(currentDataSourceJson)!.attrInfo.extensionDesc!, unref(currentDataSourceJson)!.deviceInfo!.code!)
  53 + return res
  54 + }
  55 +}
  56 +
65 const handleOk = async () => { 57 const handleOk = async () => {
66 await validate() 58 await validate()
67 - const value = getFieldsValue()  
68 - unref(resolveFn)?.(value[FormFieldsEnum.ATTR_VALUE]) 59 + const result = await getResult()
  60 + if (!result) return
  61 + unref(resolveFn)?.(result)
69 visible.value = false 62 visible.value = false
70 resetFields() 63 resetFields()
71 } 64 }
@@ -80,17 +73,9 @@ defineExpose({ open }) @@ -80,17 +73,9 @@ defineExpose({ open })
80 73
81 <template> 74 <template>
82 <Modal 75 <Modal
83 - v-model:open="visible" title="属性下发" ok-text="确认" :width="400" cancel-text="取消" @cancel="handleCancel" @ok="handleOk" 76 + v-model:open="visible" title="属性下发" ok-text="确认" :width="400" cancel-text="取消" @cancel="handleCancel"
  77 + @ok="handleOk"
84 > 78 >
85 - <!-- <section>  
86 - <FormItem label="操作密码" :label-col="{ span: 24 }">  
87 - <Input v-model:value="value" placeholder="请输入下发值" />  
88 - </FormItem>  
89 - <FormItem :label="fieldTitle" :label-col="{ span: 24 }">  
90 - <Input v-model:value="value" placeholder="请输入下发值" />  
91 - </FormItem>  
92 - </section> -->  
93 -  
94 <BasicForm @register="register" /> 79 <BasicForm @register="register" />
95 </Modal> 80 </Modal>
96 </template> 81 </template>
  1 +import { SingleToHex, getArray } from './config'
  2 +import { type GenModbusCommandType, genModbusCommand } from '@/api/device'
  3 +import type { ExtensionDesc } from '@/api/device/model'
  4 +import { TCPObjectModelActionTypeEnum } from '@/enums/objectModelEnum'
  5 +import { useMessage } from '@/hooks/web/useMessage'
  6 +
  7 +const getFloatPart = (number: string | number) => {
  8 + const isLessZero = Number(number) < 0
  9 + number = number.toString()
  10 + const floatPartStartIndex = number.indexOf('.')
  11 + const value = ~floatPartStartIndex
  12 + ? `${isLessZero ? '-' : ''}0.${number.substring(floatPartStartIndex + 1)}`
  13 + : '0'
  14 + return Number(value)
  15 +}
  16 +
  17 +const REGISTER_MAX_VALUE = Number(0xffff)
  18 +
  19 +export function useGetModbusCommand() {
  20 + const { createMessage } = useMessage()
  21 +
  22 + const getModbusCommand = async (value: number, extensionDesc: ExtensionDesc, deviceAddressCode: string) => {
  23 + const { registerAddress, actionType, zoomFactor } = extensionDesc as Required<ExtensionDesc>
  24 + const params: GenModbusCommandType = {
  25 + crc: 'CRC_16_LOWER',
  26 + registerNumber: 1,
  27 + deviceCode: deviceAddressCode,
  28 + registerAddress,
  29 + method: actionType,
  30 + registerValues: [value],
  31 + }
  32 +
  33 + if (actionType === TCPObjectModelActionTypeEnum.INT) {
  34 + const newValue
  35 + = Math.trunc(value) * zoomFactor
  36 + + getFloatPart(value) * zoomFactor
  37 +
  38 + if (newValue % 1 !== 0) {
  39 + createMessage.warning(`属性下发类型必须是整数,缩放因子为${zoomFactor}`)
  40 + return
  41 + }
  42 +
  43 + if (value * zoomFactor > REGISTER_MAX_VALUE) {
  44 + createMessage.warning(`属性下发值不能超过${REGISTER_MAX_VALUE},缩放因子是${zoomFactor}`)
  45 + return
  46 + }
  47 + }
  48 + else if (actionType === TCPObjectModelActionTypeEnum.DOUBLE) {
  49 + const regex = /^-?\d+(\.\d{0,2})?$/
  50 + const values
  51 + = Math.trunc(value) * zoomFactor
  52 + + getFloatPart(value) * zoomFactor
  53 +
  54 + if (!regex.test(values.toString())) {
  55 + createMessage.warning(`属性下发值精确到两位小数,缩放因子是${zoomFactor}`)
  56 + return
  57 + }
  58 +
  59 + const newValue
  60 + = values === 0 ? [0, 0] : getArray(SingleToHex(values))
  61 + params.registerValues = newValue
  62 + params.registerNumber = 2
  63 + }
  64 +
  65 + return await genModbusCommand(params)
  66 + }
  67 +
  68 + /**
  69 + *
  70 + * @param extensionDesc 物模型拓展描述符
  71 + * @param deviceAddressCode 设备地址码
  72 + */
  73 + const validateCanGetCommand = (extensionDesc?: ExtensionDesc, deviceAddressCode?: string, createValidateMessage = true) => {
  74 + const result = { flag: true, message: '' }
  75 + if (!extensionDesc) {
  76 + result.flag = false
  77 + result.message = '当前物模型扩展描述没有填写'
  78 + }
  79 +
  80 + if (!deviceAddressCode) {
  81 + result.flag = false
  82 + result.message = '当前设备未绑定设备地址码'
  83 + }
  84 +
  85 + if (result.message && createValidateMessage)
  86 + createMessage.warning(result.message)
  87 +
  88 + return result
  89 + }
  90 +
  91 + return {
  92 + getModbusCommand,
  93 + validateCanGetCommand,
  94 + }
  95 +}
@@ -9,7 +9,7 @@ export const getFormSchemas = ({ structJSON: structJson, required, transportType @@ -9,7 +9,7 @@ export const getFormSchemas = ({ structJSON: structJson, required, transportType
9 functionName, 9 functionName,
10 dataType, 10 dataType,
11 }: StructJSON): FormSchema => { 11 }: StructJSON): FormSchema => {
12 - const { specs } = dataType || {} 12 + const { specs, type } = dataType || {}
13 const { valueRange } = specs! as Specs 13 const { valueRange } = specs! as Specs
14 const { max = Number.MAX_SAFE_INTEGER, min = Number.MIN_SAFE_INTEGER } = valueRange || {} 14 const { max = Number.MAX_SAFE_INTEGER, min = Number.MIN_SAFE_INTEGER } = valueRange || {}
15 return { 15 return {
@@ -21,22 +21,11 @@ export const getFormSchemas = ({ structJSON: structJson, required, transportType @@ -21,22 +21,11 @@ export const getFormSchemas = ({ structJSON: structJson, required, transportType
21 required, 21 required,
22 message: `${functionName}是必填项`, 22 message: `${functionName}是必填项`,
23 }, 23 },
24 - {  
25 - type: 'number',  
26 - trigger: 'change',  
27 - validator: (_rule, value) => {  
28 - const reg = /^[0-9]*$/  
29 - if (!reg.test(value)) return Promise.reject(new Error(`${functionName}不是一个有效的数字`))  
30 - if (value < min || value > max)  
31 - return Promise.reject(new Error(`${functionName}取值范围在${min}~${max}之间`))  
32 -  
33 - return Promise.resolve(value)  
34 - },  
35 - },  
36 ], 24 ],
37 componentProps: { 25 componentProps: {
38 max, 26 max,
39 min, 27 min,
  28 + precision: type === DataTypeEnum.NUMBER_INT ? 0 : 2,
40 }, 29 },
41 } as FormSchema 30 } as FormSchema
42 } 31 }
@@ -142,7 +131,6 @@ export const getFormSchemas = ({ structJSON: structJson, required, transportType @@ -142,7 +131,6 @@ export const getFormSchemas = ({ structJSON: structJson, required, transportType
142 } 131 }
143 132
144 const schemas: FormSchema[] = [] 133 const schemas: FormSchema[] = []
145 -  
146 for (const item of structJson) { 134 for (const item of structJson) {
147 const { dataType } = item 135 const { dataType } = item
148 const { type } = dataType || {} 136 const { type } = dataType || {}
  1 +import type { ValidatorRule } from 'ant-design-vue/lib/form/interface'
  2 +
1 export { default as ThingsModelForm } from './index.vue' 3 export { default as ThingsModelForm } from './index.vue'
  4 +
  5 +export const validateTCPCustomCommand: ValidatorRule['validator'] = (_rule, value) => {
  6 + const reg = /^[\s0-9a-fA-F]+$/
  7 + if (reg.test(value)) return Promise.resolve()
  8 + return Promise.reject(new Error('请输入ASCII或HEX服务命令(0~9/A~F)'))
  9 +}
@@ -22,10 +22,6 @@ const props = withDefaults(defineProps<{ @@ -22,10 +22,6 @@ const props = withDefaults(defineProps<{
22 22
23 const thingsModelFormListElMap = reactive<Record<string, { el: InstanceType<typeof ThingsModelForm>; structJSON: StructJSON }>>({}) 23 const thingsModelFormListElMap = reactive<Record<string, { el: InstanceType<typeof ThingsModelForm>; structJSON: StructJSON }>>({})
24 24
25 -// const getLabelWidth = () => {  
26 -// return Math.max(...((props.inputData || [])?.map(item => item?.functionName?.length))) * 12  
27 -// }  
28 -  
29 const [register, formActionType] = useForm({ 25 const [register, formActionType] = useForm({
30 schemas: getFormSchemas({ structJSON: props.inputData || [], required: props.required, transportType: props.transportType }), 26 schemas: getFormSchemas({ structJSON: props.inputData || [], required: props.required, transportType: props.transportType }),
31 showActionButtonGroup: false, 27 showActionButtonGroup: false,
@@ -118,9 +118,9 @@ export function useNodeEvent(eventJson: NodeDataEventJsonType, dataSourceJson: N @@ -118,9 +118,9 @@ export function useNodeEvent(eventJson: NodeDataEventJsonType, dataSourceJson: N
118 else { 118 else {
119 const instance = h(AttributeDeliverModal) 119 const instance = h(AttributeDeliverModal)
120 render(instance, document.body) 120 render(instance, document.body)
121 - const value = await (instance.component?.exposed as InstanceType<typeof AttributeDeliverModal>)?.open({ title: `${alias || deviceName}-${attrInfo.name}`, operationPassword, operationPasswordEnable }) as string  
122 - 121 + const value = await (instance.component?.exposed as InstanceType<typeof AttributeDeliverModal>)?.open({ title: `${alias || deviceName}-${attrInfo.name}`, operationPassword, operationPasswordEnable, dataSourceJson }) as string
123 command.params = transportType === TransportTypeEnum.TCP ? value : { [attr]: value } 122 command.params = transportType === TransportTypeEnum.TCP ? value : { [attr]: value }
  123 + if (!command.params) return
124 await doCommandDelivery({ way, command, deviceId }) 124 await doCommandDelivery({ way, command, deviceId })
125 createMessage.success('命令下发成功') 125 createMessage.success('命令下发成功')
126 } 126 }
  1 +/**
  2 + * @description TCP物模型拓展描述符数据格式
  3 + */
  4 +export enum TCPObjectModelActionTypeEnum {
  5 + BOOL = '05',
  6 + INT = '06',
  7 + DOUBLE = '16',
  8 +}