const { formatExportStamp, isCancelError, loadSelectedFile, saveTextFileToChat } = require('../../repositories/file.js') const { getWxApi } = require('../../utils/platform-utils.js') const { parseHexInteger } = require('../../utils/base-utils.js') const transport = require('../../transport/ble-core.js') const settingsService = require('../../store/settings-store.js') const modbusClient = require('../../protocols/modbus-rtu/client.js') const { DATA_TYPE_OPTIONS, REGISTER_TYPE_OPTIONS, cloneImportedGroup, decodeRegisterFromWordCache, decodeRegisterValue, formatCoilDisplayValue, formatRegisterValue, getDataType, getGroupEncodedWords, getRegisterEncodedWords, getRegisterJsonValue, getRegisterWordsFromWordCache, getRegisterWriteValueText, isAddressRangeOverflow, isBitRegisterType, isByteRegister, normalizeGroup, normalizeGroupConfig, parseCoilValue, registerTypeIsBit, splitWordSpans, validateRegisterValue } = require('../../domain/generic-modbus/model.js') const { parseStructDefinition: parseStructDefinitionSource } = require('../../domain/generic-modbus/struct-parser.js') const STORAGE_KEY = 'generic-modbus-groups-json' const JSON_DOCUMENT_TYPE = 'generic-modbus-rtu' const JSON_SCHEMA_VERSION = 2 let initialized = false const subscribers = [] let state = { genericModbusDataTypeOptions: DATA_TYPE_OPTIONS, genericModbusGroups: [], genericModbusRegisterTypeOptions: REGISTER_TYPE_OPTIONS } function notify() { const nextState = getState() subscribers.slice().forEach((subscriber) => { subscriber(nextState) }) } function setState(changedData, options = {}) { state = { ...state, ...changedData } if (options.persist !== false) persistGroups() notify() } function resolveMaxPacketLength(value) { const settings = settingsService.getState() const numberValue = Number(value === undefined ? settings.genericModbusMaxPacketLength : value) if (Number.isFinite(numberValue) && Math.round(numberValue) === 0) return 0 if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue) return 64 } function getWriteSpanMaxQuantity(totalQuantity, maxPacketLength) { if (maxPacketLength === 0) return Math.max(1, totalQuantity) return Math.max(1, modbusClient.getMaxWriteMultipleRegisterQuantity(maxPacketLength)) } function toPersistedGroups(groups) { return groups.map((group) => ({ layout: group.layout, name: group.name, registerType: group.registerType, startAddress: group.startAddress, quantity: group.quantity, registers: group.registers.map((register) => ({ dataType: register.dataType, defaultValue: register.defaultValue, isStructField: register.isStructField, name: register.name, maxValue: register.maxValue, minValue: register.minValue, textByteLength: register.textByteLength, remark: register.remark, unit: register.unit, value: getRegisterJsonValue(register) })) })) } function toJsonData(groups = state.genericModbusGroups, options = {}) { const jsonData = { groups: toPersistedGroups(groups), type: JSON_DOCUMENT_TYPE, version: JSON_SCHEMA_VERSION } if (options.includeExportedAt) { jsonData.exportedAt = new Date().toISOString() } return jsonData } function toJsonText(groups = state.genericModbusGroups, options = {}) { return JSON.stringify(toJsonData(groups, options), null, 2) } function parseJsonGroups(jsonText) { const parsed = typeof jsonText === 'string' ? JSON.parse(jsonText) : jsonText const groups = Array.isArray(parsed) ? parsed : (Array.isArray(parsed && parsed.groups) ? parsed.groups : parsed && parsed.genericModbusGroups) if (parsed && parsed.type && parsed.type !== JSON_DOCUMENT_TYPE) { throw new Error('JSON 文件不是通用Modbus配置') } if (parsed && parsed.version && parsed.version !== JSON_SCHEMA_VERSION) { throw new Error('JSON 版本不兼容') } if (!Array.isArray(groups)) { throw new Error('JSON 中没有找到寄存器组数组') } return groups } function readStoredGroups() { const wxApi = getWxApi() if (typeof wxApi.getStorageSync !== 'function') return [] try { const jsonText = wxApi.getStorageSync(STORAGE_KEY) if (jsonText) return parseJsonGroups(jsonText).map(cloneImportedGroup) } catch (error) { return [] } return [] } function persistGroups() { const wxApi = getWxApi() if (typeof wxApi.setStorageSync !== 'function') return try { wxApi.setStorageSync(STORAGE_KEY, toJsonText()) } catch (error) {} } function init() { if (initialized) return state = { ...state, genericModbusGroups: readStoredGroups().map(normalizeGroup) } initialized = true } function getState() { return { ...state, genericModbusDataTypeOptions: DATA_TYPE_OPTIONS, genericModbusRegisterTypeOptions: REGISTER_TYPE_OPTIONS } } function subscribe(subscriber) { if (typeof subscriber !== 'function') return () => {} init() subscribers.push(subscriber) subscriber(getState()) return () => { const index = subscribers.indexOf(subscriber) if (index >= 0) subscribers.splice(index, 1) } } function getShareFileName() { return `generic-modbus-rtu-${formatExportStamp()}.json` } async function importJsonFromMessageFile() { try { const file = await loadSelectedFile('message', { encoding: 'utf8', extensionMessage: '请选择 .json 寄存器配置文件', extensions: ['json'], fallbackName: 'generic-modbus.json' }) const jsonText = file.text const importedGroups = parseJsonGroups(jsonText).map(cloneImportedGroup).map(normalizeGroup) if (!importedGroups.length) throw new Error('JSON 中没有可导入的寄存器组') setState({ genericModbusGroups: state.genericModbusGroups.concat(importedGroups) }) return importedGroups.length } catch (error) { const message = error && error.message ? error.message : '导入通用Modbus配置失败' transport.showCommandAlert('通用Modbus导入', message) return 0 } } async function saveJsonToChat() { try { if (!state.genericModbusGroups.length) { throw new Error('没有可保存的寄存器组') } const jsonText = toJsonText(state.genericModbusGroups, { includeExportedAt: true }) await saveTextFileToChat(getShareFileName(), jsonText) return state.genericModbusGroups.length } catch (error) { const message = error && error.message ? error.message : '保存通用Modbus配置失败' if (!isCancelError(error)) { transport.showCommandAlert('通用Modbus保存', message) } return 0 } } function addGroupFromConfig(config = {}) { let groupConfig try { groupConfig = normalizeGroupConfig(config) } catch (error) { transport.showCommandAlert('通用Modbus添加', error.message || '寄存器组配置无效') return null } if (isAddressRangeOverflow(groupConfig.startAddress, groupConfig.quantity)) { transport.showCommandAlert('通用Modbus添加', '地址范围超出 0xFFFF') return null } const registers = Array.isArray(config.registers) ? config.registers : [] const group = normalizeGroup({ ...groupConfig, layout: config.layout, ...(registers.length ? { registers } : {}), expanded: false }) if (group.addressOverflow) { transport.showCommandAlert('通用Modbus添加', '地址范围超出 0xFFFF') return null } setState({ genericModbusGroups: state.genericModbusGroups.concat(group) }) return group } function updateGroupConfig(groupId, config = {}) { const group = findGroup(groupId) if (!group) return null let nextConfig try { nextConfig = normalizeGroupConfig({ ...group, ...config }) } catch (error) { transport.showCommandAlert('通用Modbus更新', error.message || '寄存器组配置无效') return null } if (isAddressRangeOverflow(nextConfig.startAddress, nextConfig.quantity)) { transport.showCommandAlert('通用Modbus更新', '地址范围超出 0xFFFF') return null } const registers = Array.isArray(config.registers) ? config.registers : group.registers const updatedGroup = normalizeGroup({ ...group, ...nextConfig, registers }) if (updatedGroup.addressOverflow) { transport.showCommandAlert('通用Modbus更新', '地址范围超出 0xFFFF') return null } setState({ genericModbusGroups: state.genericModbusGroups.map((item) => ( item.id === groupId ? updatedGroup : item )) }) return updatedGroup } function updateGroups(mapper) { setState({ genericModbusGroups: state.genericModbusGroups.map((group, index) => normalizeGroup(mapper(group, index))) }) } function findGroup(groupId) { return state.genericModbusGroups.find((group) => group.id === groupId) } function setGroupExpanded(groupId, expanded) { updateGroups((group) => group.id === groupId ? { ...group, deleteVisible: false, expanded } : group) } function setGroupDeleteVisible(groupId, deleteVisible) { updateGroups((group) => group.id === groupId ? { ...group, deleteVisible } : group) } function removeGroup(groupId) { setState({ genericModbusGroups: state.genericModbusGroups.filter((group) => group.id !== groupId) }) } function reorderRegister(groupId, fromIndex, toIndex) { const group = findGroup(groupId) if (!group) return null if (group.isStructLayout) return group const registers = group.registers.slice() const sourceIndex = Number(fromIndex) const targetIndex = Number(toIndex) if (!Number.isInteger(sourceIndex) || !Number.isInteger(targetIndex)) return null if (sourceIndex < 0 || sourceIndex >= registers.length) return null const safeTargetIndex = Math.min(Math.max(targetIndex, 0), registers.length - 1) if (safeTargetIndex === sourceIndex) return group const moved = registers.splice(sourceIndex, 1)[0] registers.splice(safeTargetIndex, 0, moved) const updatedGroup = normalizeGroup({ ...group, quantity: registers.length, registers }) setState({ genericModbusGroups: state.genericModbusGroups.map((item) => ( item.id === groupId ? updatedGroup : item )) }) return updatedGroup } function updateRegister(groupId, registerIndex, changedData) { updateGroups((group) => { if (group.id !== groupId) return group const shouldResetReadState = Object.prototype.hasOwnProperty.call(changedData, 'dataType') || Object.prototype.hasOwnProperty.call(changedData, 'textByteLength') return { ...group, registers: group.registers.map((register, currentIndex) => ( currentIndex === registerIndex ? { ...register, ...(shouldResetReadState ? { rawValue: null, rawWords: [] } : {}), ...changedData } : register )) } }) } function updateRegisterValue(groupId, registerIndex, value) { updateRegister(groupId, registerIndex, { inputValue: value, isDirty: true }) } function validateRegisterInputValue(groupId, registerIndex, value) { const group = findGroup(groupId) if (!group) return false const register = group.registers[registerIndex] if (!register) return false return validateRegisterValue(register, value) } function parseStructDefinition(sourceText) { return parseStructDefinitionSource(sourceText) } async function readGroup(groupId, options = {}) { const group = findGroup(groupId) const slaveAddress = modbusClient.getSharedSlaveAddress() if (!group || slaveAddress === null) return false if (group.addressOverflow) { transport.showCommandAlert('通用Modbus读取', '寄存器地址范围超出 0xFFFF') return false } const totalQuantity = Math.max(1, group.wordQuantity || group.quantity || 0) const maxPacketLength = resolveMaxPacketLength(options.maxPacketLength) const wordCache = {} const values = await modbusClient.readSpans( slaveAddress, group.functionCode, [{ address: group.startAddress, quantity: totalQuantity }], group.name || '通用Modbus读取', 'generic-modbus-read', { maxFrameBytes: maxPacketLength, showModal: options.showModal !== false } ) if (!values) return false if (isBitRegisterType(group.registerType)) { Object.keys(values.coils || {}).forEach((addressText) => { wordCache[parseHexInteger(addressText)] = Number(values.coils[addressText]) ? 1 : 0 }) } else { Object.keys(values.words || {}).forEach((addressText) => { wordCache[parseHexInteger(addressText)] = Number(values.words[addressText]) & 0xFFFF }) } updateGroups((item) => { if (item.id !== groupId) return item const nextRegisters = item.registers.map((register) => { const rawWords = registerTypeIsBit(register) ? [] : getRegisterWordsFromWordCache(register, wordCache) const rawValue = registerTypeIsBit(register) ? decodeRegisterFromWordCache(register, wordCache) : (rawWords ? decodeRegisterValue(register, rawWords) : null) const displayValue = rawValue === null || rawValue === undefined ? '--' : (registerTypeIsBit(register) ? formatCoilDisplayValue(rawValue) : formatRegisterValue(register, rawValue)) return { ...register, displayValue, inputValue: item.writable ? displayValue : register.inputValue, isDirty: false, rawValue, rawWords: rawWords || [] } }) return { ...item, registers: nextRegisters } }) return true } async function writeGroup(groupId) { const group = findGroup(groupId) const slaveAddress = modbusClient.getSharedSlaveAddress() const maxPacketLength = resolveMaxPacketLength() if (!group || slaveAddress === null) return false if (!group.writable) { transport.showCommandAlert('通用Modbus写入', '当前寄存器组为只读') return false } if (group.addressOverflow) { transport.showCommandAlert('通用Modbus写入', '寄存器地址范围超出 0xFFFF') return false } const writtenRegisters = [] if (group.registerType === 'coil') { for (let index = 0; index < group.registers.length; index += 1) { const register = group.registers[index] const coilValue = parseCoilValue(getRegisterWriteValueText(register)) if (coilValue === null) { transport.showCommandAlert('通用Modbus写入', `${register.name || `寄存器 ${index + 1}`} 没有有效写入值`) return false } const response = await modbusClient.writeSingleCoil( slaveAddress, group.startAddress + index, !!coilValue, register.name || group.name || '通用Modbus写入', 'generic-modbus-coil-write', { maxFrameBytes: maxPacketLength } ) if (!response) return false writtenRegisters.push({ rawValue: coilValue, rawWords: [], displayValue: formatCoilDisplayValue(coilValue) }) } } else { let words try { words = group.isStructLayout ? getGroupEncodedWords(group) : Array.from({ length: Math.max(1, group.wordQuantity || 1) }, () => 0) if (!group.isStructLayout) { for (let index = 0; index < group.registers.length; index += 1) { const register = group.registers[index] const registerWords = getRegisterEncodedWords(register) if (!Array.isArray(registerWords) || !registerWords.length) { throw new Error(`${register.name || `寄存器 ${index + 1}`} 没有有效写入值`) } const dataType = getDataType(register.dataType).key const relativeAddress = Math.max(0, register.address - group.startAddress) if (isByteRegister(dataType)) { const byteValue = Number(registerWords[0]) & 0xFF const currentWord = words[relativeAddress] || 0 words[relativeAddress] = register.byteOffset === 0 ? (((byteValue << 8) | (currentWord & 0x00FF)) & 0xFFFF) : (((currentWord & 0xFF00) | byteValue) & 0xFFFF) } else { for (let offset = 0; offset < register.registerCount; offset += 1) { words[relativeAddress + offset] = Number(registerWords[offset]) & 0xFFFF } } } } } catch (error) { transport.showCommandAlert('通用Modbus写入', error.message || '寄存器组没有有效写入值') return false } const writtenWordCache = words.reduce((cache, word, offset) => { cache[group.startAddress + offset] = word return cache }, {}) group.registers.forEach((register) => { const rawWords = getRegisterWordsFromWordCache(register, writtenWordCache) || [] const rawValue = decodeRegisterValue(register, rawWords) const displayValue = formatRegisterValue(register, rawValue) writtenRegisters.push({ rawWords, rawValue, displayValue }) }) const maxWriteQuantity = getWriteSpanMaxQuantity(words.length, maxPacketLength) const spans = splitWordSpans(group.startAddress, words.length, maxWriteQuantity) let cursor = 0 for (const span of spans) { const spanWords = words.slice(cursor, cursor + span.quantity) cursor += span.quantity const response = await modbusClient.writeMultipleRegisters( slaveAddress, span.address, spanWords, group.name || '通用Modbus写入', 'generic-modbus-write', { maxFrameBytes: maxPacketLength } ) if (!response) return false } } updateGroups((item) => { if (item.id !== groupId) return item let writtenIndex = 0 return { ...item, registers: item.registers.map((register) => { const written = writtenRegisters[writtenIndex] || {} writtenIndex += 1 const hasDisplayValue = Object.prototype.hasOwnProperty.call(written, 'displayValue') const hasRawValue = Object.prototype.hasOwnProperty.call(written, 'rawValue') const hasRawWords = Object.prototype.hasOwnProperty.call(written, 'rawWords') return { ...register, displayValue: hasDisplayValue ? written.displayValue : register.displayValue, inputValue: hasDisplayValue ? written.displayValue : register.inputValue, isDirty: false, rawValue: hasRawValue ? written.rawValue : register.rawValue, rawWords: hasRawWords ? written.rawWords : register.rawWords } }) } }) return true } module.exports = { DATA_TYPE_OPTIONS, REGISTER_TYPE_OPTIONS, addGroupFromConfig, getState, importJsonFromMessageFile, init, parseStructDefinition, readGroup, removeGroup, reorderRegister, saveJsonToChat, setGroupDeleteVisible, setGroupExpanded, subscribe, updateGroupConfig, updateRegister, updateRegisterValue, validateRegisterInputValue, writeGroup }