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 | } | ... | ... |