const { parseHexInteger } = require('../../utils/base-utils.js') const { bytesToWords } = require('../../utils/binary-utils.js') const transport = require('../../transport/ble-core.js') const modbusClient = require('../modbus-rtu/service.js') const storageAccessProtocolIo = require('../storage-access/protocol-io.js') const { getMemoryTypeFromName, isReadOnlyMemoryType } = require('../storage-access/memory-areas.js') const { decodeRegisterFromByteCache, decodeRegisterFromWordCache, decodeRegisterValue, formatCoilDisplayValue, formatRegisterValue, getDataType, getGroupEncodedBytes, getGroupEncodedWords, getRegisterBytesFromByteCache, getRegisterEncodedBytes, getRegisterEncodedWords, getRegisterWordsFromByteCache, getRegisterWordsFromWordCache, getRegisterWriteValueText, isBitRegisterType, isByteRegister, normalizeRegister, parseCoilValue, registerTypeIsBit, splitWordSpans } = require('../../domain/parameter-groups/model.js') const MAX_STORAGE_ACCESS_ADDRESS = 0xFFFFFFFF const MAX_STORAGE_ACCESS_BYTE_LENGTH = 0xFFFFFFFF function getMemoryType(group = {}) { return getMemoryTypeFromName(group.sourceMemoryArea) } function isMemoryGroup(group = {}) { return getMemoryType(group) !== null } function isByteAddressedGroup(group = {}) { return group.addressUnit === 'byte' || group.addressUnit === 'bytes' || isMemoryGroup(group) } function getMemoryAddress(group = {}) { const sourceAddress = Number(group.sourceAddress) if (Number.isFinite(sourceAddress)) return Math.min(MAX_STORAGE_ACCESS_ADDRESS, Math.max(0, Math.floor(sourceAddress))) return Math.min(MAX_STORAGE_ACCESS_ADDRESS, Math.max(0, Math.floor(Number(group.startAddress) || 0))) } function getMemoryByteLength(group = {}) { const sourceByteLength = Number(group.sourceByteLength) if (Number.isFinite(sourceByteLength) && sourceByteLength > 0) return Math.min(MAX_STORAGE_ACCESS_BYTE_LENGTH, Math.ceil(sourceByteLength)) const byteLength = Number(group.byteLength) if (Number.isFinite(byteLength) && byteLength > 0) return Math.min(MAX_STORAGE_ACCESS_BYTE_LENGTH, Math.ceil(byteLength)) return Math.max(1, Math.ceil((Number(group.wordQuantity) || 1) * 2)) } function shouldUseStorageAccess(options = {}, group = {}) { return !!options.useStorageAccess && isMemoryGroup(group) } function getStorageMaxPacketLength(group = {}, options = {}) { const configuredMaxPacketLength = storageAccessProtocolIo.resolveMaxPacketLength(options.maxPacketLength) const deviceMaxPacketLength = Number(group.codeInfoContext && group.codeInfoContext.maxPacketLength) if (!Number.isFinite(deviceMaxPacketLength) || deviceMaxPacketLength <= 0) return configuredMaxPacketLength if (configuredMaxPacketLength === 0) return Math.round(deviceMaxPacketLength) return Math.min(configuredMaxPacketLength, Math.round(deviceMaxPacketLength)) } function normalizeNumericCache(source = {}) { const cache = {} Object.keys(source || {}).forEach((addressText) => { const numericAddress = Number(addressText) if (Number.isFinite(numericAddress)) { cache[numericAddress] = Number(source[addressText]) & 0xFFFF } }) return cache } function createRegisterStateFromCache(group, register, wordCache) { const rawBytes = registerTypeIsBit(register) ? [] : (isByteAddressedGroup(group) ? getRegisterBytesFromByteCache(register, wordCache) : []) const rawWords = registerTypeIsBit(register) ? [] : (isByteAddressedGroup(group) ? getRegisterWordsFromByteCache(register, wordCache) : getRegisterWordsFromWordCache(register, wordCache)) const rawValue = registerTypeIsBit(register) ? decodeRegisterFromWordCache(register, wordCache) : (isByteAddressedGroup(group) ? decodeRegisterFromByteCache(register, wordCache) : (rawWords ? decodeRegisterValue(register, rawWords) : null)) const displayValue = rawValue === null || rawValue === undefined ? '--' : (registerTypeIsBit(register) ? formatCoilDisplayValue(rawValue) : formatRegisterValue(register, rawValue)) const preNormalizedRegister = { ...register, displayValue, isDirty: false, rawBytes: rawBytes || [], rawValue, rawWords: rawWords || [] } const nextRegister = normalizeRegister(preNormalizedRegister, group, 0, register.address, register.byteOffset) return { ...nextRegister, id: register.id, inputValue: group.writable ? displayValue : register.inputValue } } function applyReadCacheToGroup(group, wordCache) { return { ...group, registers: group.registers.map((register) => createRegisterStateFromCache(group, register, wordCache)) } } function applyWrittenSnapshotToRegister(register, snapshot = {}) { const hasDisplayValue = Object.prototype.hasOwnProperty.call(snapshot, 'displayValue') const hasRawBytes = Object.prototype.hasOwnProperty.call(snapshot, 'rawBytes') const hasRawValue = Object.prototype.hasOwnProperty.call(snapshot, 'rawValue') const hasRawWords = Object.prototype.hasOwnProperty.call(snapshot, 'rawWords') return { ...register, displayValue: hasDisplayValue ? snapshot.displayValue : register.displayValue, inputValue: hasDisplayValue ? snapshot.displayValue : register.inputValue, isDirty: false, rawBytes: hasRawBytes ? snapshot.rawBytes : register.rawBytes, rawValue: hasRawValue ? snapshot.rawValue : register.rawValue, rawWords: hasRawWords ? snapshot.rawWords : register.rawWords } } function applyWrittenSnapshotsToGroup(group, snapshots = []) { let writtenIndex = 0 return { ...group, registers: group.registers.map((register, index) => { const snapshot = snapshots[writtenIndex] || {} writtenIndex += 1 const nextRegister = applyWrittenSnapshotToRegister(register, snapshot) const normalized = normalizeRegister(nextRegister, group, index, nextRegister.address, nextRegister.byteOffset) return { ...normalized, id: register.id, inputValue: group.writable ? nextRegister.displayValue : nextRegister.inputValue } }) } } function applyWrittenSnapshotToGroupRegister(group, registerIndex, snapshot) { return { ...group, registers: group.registers.map((register, currentIndex) => ( currentIndex === registerIndex ? (() => { const nextRegister = applyWrittenSnapshotToRegister(register, snapshot || {}) const normalized = normalizeRegister(nextRegister, group, currentIndex, nextRegister.address, nextRegister.byteOffset) return { ...normalized, id: register.id, inputValue: group.writable ? nextRegister.displayValue : nextRegister.inputValue } })() : register )) } } function getWriteSpanMaxQuantity(totalQuantity, maxPacketLength) { if (maxPacketLength === 0) return Math.max(1, totalQuantity) return Math.max(1, modbusClient.getMaxWriteMultipleRegisterQuantity(maxPacketLength)) } async function readModbusGroup(group, options = {}) { const totalQuantity = Math.max(1, group.wordQuantity || group.quantity || 0) const wordCache = {} const slaveAddress = modbusClient.getSharedSlaveAddress() if (slaveAddress === null) return null const values = await modbusClient.readSpans( slaveAddress, group.functionCode, [{ address: group.startAddress, quantity: totalQuantity }], group.name || '参数组读取', 'parameter-group-read', { maxFrameBytes: options.maxPacketLength, showModal: options.showModal !== false } ) if (!values) return null 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 }) } return wordCache } function getRegisterWordsForModbusWrite(group) { const words = group.isStructLayout ? getGroupEncodedWords(group) : Array.from({ length: Math.max(1, group.wordQuantity || 1) }, () => 0) if (group.isStructLayout) return words 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 } } } return words } function createModbusWrittenRegisterSnapshots(group, words = []) { const writtenWordCache = words.reduce((cache, word, offset) => { cache[group.startAddress + offset] = word return cache }, {}) return group.registers.map((register) => { const rawWords = getRegisterWordsFromWordCache(register, writtenWordCache) || [] const rawValue = decodeRegisterValue(register, rawWords) const displayValue = formatRegisterValue(register, rawValue) return { rawWords, rawValue, displayValue } }) } async function writeModbusCoilGroup(group, options = {}) { const slaveAddress = modbusClient.getSharedSlaveAddress() if (slaveAddress === null) return null const writtenRegisters = [] 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('参数组写入', `${register.name || `寄存器 ${index + 1}`} 没有有效写入值`) return null } const response = await modbusClient.writeSingleCoil( slaveAddress, group.startAddress + index, !!coilValue, register.name || group.name || '参数组写入', 'parameter-group-coil-write', { maxFrameBytes: options.maxPacketLength } ) if (!response) return null writtenRegisters.push({ rawBytes: [], rawValue: coilValue, rawWords: [], displayValue: formatCoilDisplayValue(coilValue) }) } return writtenRegisters } async function writeModbusRegisterGroup(group, options = {}) { const slaveAddress = modbusClient.getSharedSlaveAddress() if (slaveAddress === null) return null let words try { words = getRegisterWordsForModbusWrite(group) } catch (error) { transport.showCommandAlert('参数组写入', error.message || '寄存器组没有有效写入值') return null } const writtenRegisters = createModbusWrittenRegisterSnapshots(group, words) const maxWriteQuantity = getWriteSpanMaxQuantity(words.length, options.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 || '参数组写入', 'parameter-group-write', { maxFrameBytes: options.maxPacketLength } ) if (!response) return null } return writtenRegisters } async function writeModbusGroup(group, options = {}) { return group.registerType === 'coil' ? writeModbusCoilGroup(group, options) : writeModbusRegisterGroup(group, options) } function bytesToPaddedWords(bytes = []) { return bytesToWords(bytes.length % 2 === 0 ? bytes : bytes.concat(0)) } function fillByteCacheFromBytes(byteCache, startAddress, bytes = []) { bytes.forEach((byte, offset) => { byteCache[startAddress + offset] = Number(byte) & 0xFF }) } function fillWordCacheFromBytes(wordCache, startAddress, bytes = []) { const words = bytesToPaddedWords(bytes) words.forEach((word, offset) => { wordCache[startAddress + offset] = Number(word) & 0xFFFF }) } function createStorageWrittenRegisterSnapshots(group, wordCache) { return group.registers.map((register) => { const rawBytes = isByteAddressedGroup(group) ? (getRegisterBytesFromByteCache(register, wordCache) || []) : [] const rawWords = isByteAddressedGroup(group) ? (getRegisterWordsFromByteCache(register, wordCache) || []) : (getRegisterWordsFromWordCache(register, wordCache) || []) const rawValue = isByteAddressedGroup(group) ? decodeRegisterFromByteCache(register, wordCache) : decodeRegisterValue(register, rawWords) return { rawBytes, rawWords, rawValue, displayValue: formatRegisterValue(register, rawValue) } }) } function createStorageWrittenRegisterSnapshot(group, register, byteCache) { const snapshots = createStorageWrittenRegisterSnapshots({ ...group, registers: [register] }, byteCache) return snapshots[0] || null } function groupHasBitFields(group = {}) { return (Array.isArray(group.registers) ? group.registers : []).some((register) => !!register.isBitField) } async function writeMemoryRegister(group, register, options = {}) { const memoryType = getMemoryType(group) const maxPacketLength = getStorageMaxPacketLength(group, options) const byteLength = Math.max(1, Number(register.byteLength) || 1) const address = Math.min(MAX_STORAGE_ACCESS_ADDRESS, Math.max(0, Math.floor(Number(register.address) || getMemoryAddress(group)))) let bytes if (memoryType === null) { transport.showCommandAlert('内存写入', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`) return null } if (isReadOnlyMemoryType(memoryType)) { transport.showCommandAlert('内存写入', `${group.sourceMemoryArea || '该'} 区不可写`) return null } try { if (register.isBitField) { const baseBytes = await storageAccessProtocolIo.readMemory( memoryType, address, byteLength, register.name ? `${register.name} 读改写` : '变量读改写', 'parameter-group-memory-register-rmw-read', { maxFrameBytes: maxPacketLength } ) if (!baseBytes) return null bytes = getGroupEncodedBytes({ ...group, paddedByteLength: byteLength, registers: [{ ...register, address, byteStart: 0 }] }, baseBytes).slice(0, byteLength) } else { bytes = getRegisterEncodedBytes(register) } } catch (error) { transport.showCommandAlert('内存写入', error.message || '变量没有有效写入值') return null } if (!Array.isArray(bytes) || !bytes.length) { transport.showCommandAlert('内存写入', `${register.name || '变量'} 没有有效写入值`) return null } bytes = bytes.slice(0, byteLength) while (bytes.length < byteLength) bytes.push(0) const ok = await storageAccessProtocolIo.writeMemory( memoryType, address, bytes, register.name || group.name || '变量写入', 'parameter-group-memory-register-write', { maxFrameBytes: maxPacketLength } ) if (!ok) return null const byteCache = {} fillByteCacheFromBytes(byteCache, address, bytes) return createStorageWrittenRegisterSnapshot(group, register, byteCache) } async function readMemoryGroup(group, options = {}) { const memoryType = getMemoryType(group) const address = getMemoryAddress(group) const byteLength = getMemoryByteLength(group) const maxPacketLength = getStorageMaxPacketLength(group, options) if (memoryType === null) { transport.showCommandAlert('内存读取', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`) return null } const bytes = await storageAccessProtocolIo.readMemory( memoryType, address, byteLength, group.name || '内存读取', 'parameter-group-memory-read', { maxFrameBytes: maxPacketLength, showModal: options.showModal !== false } ) if (!bytes) return null const wordCache = {} if (isByteAddressedGroup(group)) { fillByteCacheFromBytes(wordCache, address, bytes) } else { fillWordCacheFromBytes(wordCache, address, bytes) } return wordCache } async function writeMemoryGroup(group, options = {}) { const memoryType = getMemoryType(group) const address = getMemoryAddress(group) const byteLength = getMemoryByteLength(group) const maxPacketLength = getStorageMaxPacketLength(group, options) let bytes if (memoryType === null) { transport.showCommandAlert('内存写入', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`) return null } if (isReadOnlyMemoryType(memoryType)) { transport.showCommandAlert('内存写入', `${group.sourceMemoryArea || '该'} 区不可写`) return null } try { let baseBytes = null if (groupHasBitFields(group)) { baseBytes = await storageAccessProtocolIo.readMemory( memoryType, address, byteLength, group.name ? `${group.name} 读改写` : '内存读改写', 'parameter-group-memory-rmw-read', { maxFrameBytes: maxPacketLength } ) if (!baseBytes) return null } bytes = getGroupEncodedBytes(group, baseBytes) } catch (error) { transport.showCommandAlert('内存写入', error.message || '寄存器组没有有效写入值') return null } bytes = bytes.slice(0, byteLength) const ok = await storageAccessProtocolIo.writeMemory( memoryType, address, bytes, group.name || '内存写入', 'parameter-group-memory-write', { maxFrameBytes: maxPacketLength } ) if (!ok) return null const wordCache = {} if (isByteAddressedGroup(group)) { fillByteCacheFromBytes(wordCache, address, bytes) } else { fillWordCacheFromBytes(wordCache, address, bytes) } return createStorageWrittenRegisterSnapshots(group, wordCache) } async function readGroup(group, options = {}) { const wordCache = shouldUseStorageAccess(options, group) ? await readMemoryGroup(group, options) : await readModbusGroup(group, options) if (!wordCache) return null const normalizedCache = normalizeNumericCache(wordCache) return { applyToGroup(currentGroup) { return applyReadCacheToGroup(currentGroup, normalizedCache) } } } async function writeRegister(group, registerIndex, options = {}) { const register = group && Array.isArray(group.registers) ? group.registers[registerIndex] : null if (!register) return null if (shouldUseStorageAccess(options, group)) { const written = await writeMemoryRegister(group, register, options) return written ? { applyToGroup(currentGroup) { return applyWrittenSnapshotToGroupRegister(currentGroup, registerIndex, written) } } : null } return writeGroup(group, options) } async function writeGroup(group, options = {}) { const snapshots = shouldUseStorageAccess(options, group) ? await writeMemoryGroup(group, options) : await writeModbusGroup(group, options) if (!snapshots) return null return { applyToGroup(currentGroup) { return applyWrittenSnapshotsToGroup(currentGroup, snapshots) } } } module.exports = { getMemoryAddress, getMemoryByteLength, getMemoryType, isByteAddressedGroup, isMemoryGroup, readGroup, writeGroup, writeRegister }