Commit 846eb423c2b0c38f7ce168977f9749a40265cc13
Merge branch 'fix/DEFECT-1855' into 'main_dev'
fix: 修复组态属性下发tcp设备没有缩放因子 See merge request yunteng/thingskit-scada!205
Showing
9 changed files
with
264 additions
and
72 deletions
| 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 | 1 | <script setup lang="ts"> |
| 2 | 2 | import { Modal } from 'ant-design-vue' |
| 3 | 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 | 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 | 12 | const resolveFn = ref<Fn>() |
| 9 | 13 | |
| ... | ... | @@ -11,61 +15,50 @@ const visible = ref(false) |
| 11 | 15 | |
| 12 | 16 | const password = ref() |
| 13 | 17 | |
| 18 | +const currentDataSourceJson = ref<NodeDataDataSourceJsonType>() | |
| 19 | + | |
| 14 | 20 | const [register, { getFieldsValue, resetFields, validate, setProps, clearValidate }] = useForm({ |
| 15 | 21 | layout: FormLayoutEnum.VERTICAL, |
| 16 | 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 | 26 | visible.value = true |
| 56 | 27 | password.value = operationPassword |
| 28 | + currentDataSourceJson.value = dataSourceJson | |
| 57 | 29 | return new Promise((resolve) => { |
| 58 | 30 | resolveFn.value = resolve |
| 59 | 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 | 57 | const handleOk = async () => { |
| 66 | 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 | 62 | visible.value = false |
| 70 | 63 | resetFields() |
| 71 | 64 | } |
| ... | ... | @@ -80,17 +73,9 @@ defineExpose({ open }) |
| 80 | 73 | |
| 81 | 74 | <template> |
| 82 | 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 | 79 | <BasicForm @register="register" /> |
| 95 | 80 | </Modal> |
| 96 | 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 | 9 | functionName, |
| 10 | 10 | dataType, |
| 11 | 11 | }: StructJSON): FormSchema => { |
| 12 | - const { specs } = dataType || {} | |
| 12 | + const { specs, type } = dataType || {} | |
| 13 | 13 | const { valueRange } = specs! as Specs |
| 14 | 14 | const { max = Number.MAX_SAFE_INTEGER, min = Number.MIN_SAFE_INTEGER } = valueRange || {} |
| 15 | 15 | return { |
| ... | ... | @@ -21,22 +21,11 @@ export const getFormSchemas = ({ structJSON: structJson, required, transportType |
| 21 | 21 | required, |
| 22 | 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 | 25 | componentProps: { |
| 38 | 26 | max, |
| 39 | 27 | min, |
| 28 | + precision: type === DataTypeEnum.NUMBER_INT ? 0 : 2, | |
| 40 | 29 | }, |
| 41 | 30 | } as FormSchema |
| 42 | 31 | } |
| ... | ... | @@ -142,7 +131,6 @@ export const getFormSchemas = ({ structJSON: structJson, required, transportType |
| 142 | 131 | } |
| 143 | 132 | |
| 144 | 133 | const schemas: FormSchema[] = [] |
| 145 | - | |
| 146 | 134 | for (const item of structJson) { |
| 147 | 135 | const { dataType } = item |
| 148 | 136 | const { type } = dataType || {} | ... | ... |
| 1 | +import type { ValidatorRule } from 'ant-design-vue/lib/form/interface' | |
| 2 | + | |
| 1 | 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 | 22 | |
| 23 | 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 | 25 | const [register, formActionType] = useForm({ |
| 30 | 26 | schemas: getFormSchemas({ structJSON: props.inputData || [], required: props.required, transportType: props.transportType }), |
| 31 | 27 | showActionButtonGroup: false, | ... | ... |
| ... | ... | @@ -118,9 +118,9 @@ export function useNodeEvent(eventJson: NodeDataEventJsonType, dataSourceJson: N |
| 118 | 118 | else { |
| 119 | 119 | const instance = h(AttributeDeliverModal) |
| 120 | 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 | 122 | command.params = transportType === TransportTypeEnum.TCP ? value : { [attr]: value } |
| 123 | + if (!command.params) return | |
| 124 | 124 | await doCommandDelivery({ way, command, deviceId }) |
| 125 | 125 | createMessage.success('命令下发成功') |
| 126 | 126 | } | ... | ... |