const storageAccessProtocol = require('../../protocols/storage-access/index.js') const settingsService = require('../../store/settings-store.js') const transport = require('../../transport/ble-core.js') const { createGroupsFromCodeInfo, parseCodeInfo } = require('../../domain/storage-access/code-info-parser.js') const { cloneImportedGroup, normalizeGroup } = require('../../domain/parameter-groups/model.js') const { bytesToHex } = require('../../utils/binary-utils.js') const { normalizeHexDwordText, normalizeHexText, normalizeHexWordText, parseHexBytes, parseHexDword, parseHexNumber, validateHexDwordText, validateHexText, validateHexWordText } = require('../../utils/validation.js') const MEMORY_COMMANDS = [ { key: 'read', label: '读取', description: '按字节读取内存' }, { key: 'write', label: '写入', description: '按字节写入内存' } ] const MEMORY_AREAS = [ { key: storageAccessProtocol.AREA.ADDR32, label: 'addr32', name: 'ADDR32', addressWidth: 32 }, { key: storageAccessProtocol.AREA.CODEINFO, label: 'codeinfo', name: 'CODEINFO', readOnly: true }, { key: storageAccessProtocol.AREA.DATA, label: 'data', name: 'DATA' }, { key: storageAccessProtocol.AREA.IDATA, label: 'idata', name: 'IDATA' }, { key: storageAccessProtocol.AREA.XDATA, label: 'xdata', name: 'XDATA' }, { key: storageAccessProtocol.AREA.CODE, label: 'code', name: 'CODE', readOnly: true } ] const MEMORY_READ_INDEX = 0 const MEMORY_WRITE_INDEX = 1 const CONTROL_COMMANDS = [ { key: 'reset', label: '复位', op: storageAccessProtocol.CONTROL_OP.RESET } ] let syncedDeviceCaps = { addressWidth: 0, maxPacketLength: 0, memoryEndian: '' } function resolveMaxPacketLength(value) { const settings = settingsService.getState() const numberValue = Number(value === undefined ? settings.parameterMaxPacketLength : value) const deviceMaxPacketLength = Number(syncedDeviceCaps.maxPacketLength || 0) const configuredMaxPacketLength = Number.isFinite(numberValue) && Math.round(numberValue) === 0 ? 0 : (Number.isFinite(numberValue) && numberValue > 0 ? Math.round(numberValue) : 64) if (!Number.isFinite(deviceMaxPacketLength) || deviceMaxPacketLength <= 0) return configuredMaxPacketLength if (configuredMaxPacketLength === 0) return Math.round(deviceMaxPacketLength) return Math.min(configuredMaxPacketLength, Math.round(deviceMaxPacketLength)) } function resolveConfiguredMaxPacketLength(value) { const settings = settingsService.getState() const numberValue = Number(value === undefined ? settings.parameterMaxPacketLength : value) if (Number.isFinite(numberValue) && Math.round(numberValue) === 0) return 0 if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue) return 64 } function normalizeTransferOptions(options = {}) { const maxPacketLength = options.maxPacketLength === undefined ? options.maxFrameBytes : options.maxPacketLength return { expectedByteLength: options.expectedByteLength, maxFrameBytes: options.useDeviceCaps === false ? resolveConfiguredMaxPacketLength(maxPacketLength) : resolveMaxPacketLength(maxPacketLength), onChunk: options.onChunk, showModal: options.showModal !== false } } function updateSyncedDeviceCaps(caps = {}) { const addressWidth = Number(caps.addressWidth) const maxPacketLength = Number(caps.maxPacketLength) const memoryEndian = String(caps.memoryEndian || '').trim().toLowerCase() syncedDeviceCaps = { addressWidth: addressWidth === 16 || addressWidth === 32 ? addressWidth : 0, maxPacketLength: Number.isFinite(maxPacketLength) && maxPacketLength > 0 ? Math.round(maxPacketLength) : 0, memoryEndian: memoryEndian === 'little' ? 'little' : (memoryEndian === 'big' ? 'big' : '') } } async function readMemory(area, startAddress, byteLength, label, kind, options = {}) { const transferOptions = normalizeTransferOptions(options) const normalizedArea = storageAccessProtocol.normalizeMemoryArea(area) const bytes = [] const chunks = storageAccessProtocol.getReadChunks(startAddress, byteLength, { ...transferOptions, area: normalizedArea }) for (const chunk of chunks) { const response = await transport.sendManagedFrame( storageAccessProtocol.buildReadFrame(normalizedArea, chunk.address, chunk.quantity, { maxFrameBytes: transferOptions.maxFrameBytes }), storageAccessProtocol.getChunkLabel(label, chunks, chunk), storageAccessProtocol.createExpected(normalizedArea, chunk.address, chunk.quantity, false, kind), { maxFrameBytes: transferOptions.maxFrameBytes, showModal: transferOptions.showModal } ) if (!response) return null const dataBytes = Array.isArray(response.dataBytes) ? response.dataBytes : [] dataBytes.forEach((byte, index) => { bytes[chunk.address - startAddress + index] = Number(byte) & 0xFF }) if (typeof transferOptions.onChunk === 'function') { transferOptions.onChunk(response, chunk) } } return bytes } async function writeMemory(area, startAddress, bytes, label, kind, options = {}) { const transferOptions = normalizeTransferOptions(options) const normalizedArea = storageAccessProtocol.normalizeMemoryArea(area) const chunks = storageAccessProtocol.getWriteChunks(startAddress, bytes, { ...transferOptions, area: normalizedArea }) for (const chunk of chunks) { const response = await transport.sendManagedFrame( storageAccessProtocol.buildWriteFrame(normalizedArea, chunk.address, chunk.dataBytes, { maxFrameBytes: transferOptions.maxFrameBytes }), storageAccessProtocol.getChunkLabel(label, chunks, chunk), storageAccessProtocol.createExpected(normalizedArea, chunk.address, chunk.quantity, true, kind), { maxFrameBytes: transferOptions.maxFrameBytes, showModal: transferOptions.showModal } ) if (!response) return false if (typeof transferOptions.onChunk === 'function') { transferOptions.onChunk(response, chunk) } } return true } async function readCodeInfoBlock(label, kind, options = {}) { const transferOptions = normalizeTransferOptions({ ...options, useDeviceCaps: false }) const descriptorBytes = await readMemory( storageAccessProtocol.AREA.CODEINFO, storageAccessProtocol.CODE_INFO_DESCRIPTOR_ADDRESS, storageAccessProtocol.CODE_INFO_DESCRIPTOR_BYTE_LENGTH, label, `${kind}-descriptor`, { ...transferOptions, useDeviceCaps: false, showModal: false } ) if (!descriptorBytes) { if (transferOptions.showModal !== false) { transport.showCommandAlert(label, 'CodeInfo 描述符读取失败或超时') } return null } let descriptorResponse try { descriptorResponse = storageAccessProtocol.parseCodeInfoDescriptorBytes(descriptorBytes) } catch (error) { if (transferOptions.showModal !== false) { transport.showCommandAlert(label, error.message || 'CodeInfo 描述符长度无效') } return null } const codeInfoAddress = Number(descriptorResponse.codeInfoAddress || 0) const codeInfoByteLength = Number(descriptorResponse.codeInfoByteLength || 0) let codeInfoAddressWidth try { codeInfoAddressWidth = storageAccessProtocol.normalizeDescriptorAddressWidth( descriptorResponse.codeInfoAddressWidth ) } catch (error) { if (transferOptions.showModal !== false) { transport.showCommandAlert(label, error.message || 'CodeInfo 描述符地址长度无效') } return null } const codeInfoMemoryEndian = String(descriptorResponse.codeInfoMemoryEndian || 'big').trim() const codeInfoMaxPacketLength = Number(descriptorResponse.codeInfoMaxPacketLength || 0) & 0xFFFF const codeInfoArea = codeInfoAddressWidth === 32 ? storageAccessProtocol.AREA.ADDR32 : storageAccessProtocol.AREA.CODE const codeInfoReadAddress = codeInfoAddressWidth === 32 ? codeInfoAddress : (codeInfoAddress & 0xFFFF) const codeInfoMaxFrameBytes = storageAccessProtocol.resolveDescriptorMaxFrameBytes( transferOptions.maxFrameBytes, codeInfoMaxPacketLength ) if (!codeInfoByteLength || codeInfoByteLength > 0xFFFF) { if (transferOptions.showModal !== false) { transport.showCommandAlert(label, 'CodeInfo 信息块长度无效') } return null } const codeInfoBytes = await readMemory( codeInfoArea, codeInfoReadAddress, codeInfoByteLength, label, kind, { ...transferOptions, maxFrameBytes: codeInfoMaxFrameBytes, useDeviceCaps: false } ) if (!codeInfoBytes) return null return { codeInfoAddress, codeInfoAddressWidth, codeInfoByteLength, codeInfoBytes, codeInfoDescriptorBytes: descriptorBytes, codeInfoMaxPacketLength, codeInfoMemoryEndian, codeInfoMemoryEndianMark: Number(descriptorResponse.codeInfoMemoryEndianMark || 0) & 0xFFFF, codeInfoMemoryType: codeInfoArea } } async function executeControl(operation, dataBytes, label, kind, options = {}) { const transferOptions = normalizeTransferOptions(options) const response = await transport.sendManagedFrame( storageAccessProtocol.buildControlFrame(operation, dataBytes), label, storageAccessProtocol.createControlExpected(operation, kind, { expectedByteLength: transferOptions.expectedByteLength }), { maxFrameBytes: transferOptions.maxFrameBytes, showModal: transferOptions.showModal } ) return response || null } function formatDwordHex(value) { const numberValue = Math.max(0, Math.floor(Number(value) || 0)) return numberValue.toString(16).toUpperCase().padStart(8, '0') } function getAreaAddressWidth(area) { return Number(area && area.addressWidth) === 32 || Number(area && area.key) === storageAccessProtocol.AREA.ADDR32 ? 32 : 16 } function getMemoryAreaOptions() { return MEMORY_AREAS.map((area) => ({ ...area })) } function resolveMemoryCommand(index) { const commandIndex = Number(index) === 2 ? MEMORY_WRITE_INDEX : Number(index) return { command: MEMORY_COMMANDS[commandIndex] || MEMORY_COMMANDS[MEMORY_READ_INDEX], index: commandIndex === MEMORY_WRITE_INDEX ? MEMORY_WRITE_INDEX : MEMORY_READ_INDEX } } function resolveMemoryArea(index, commandKey = 'read') { const memoryAreas = getMemoryAreaOptions() let areaIndex = Number(index) || 0 if (commandKey === 'write' && memoryAreas[areaIndex] && memoryAreas[areaIndex].readOnly) { areaIndex = 0 } if (!memoryAreas[areaIndex]) areaIndex = 0 return { area: memoryAreas[areaIndex] || memoryAreas[0], index: areaIndex, options: memoryAreas } } function normalizeDisplayHexText(value, addressWidth = 16) { const text = addressWidth === 32 ? normalizeHexDwordText(value) : normalizeHexWordText(value) return text.toUpperCase() } function buildMemoryPreview(commandKey, area, address, length, dataBytes) { try { const frameOptions = { maxFrameBytes: 0 } if (commandKey === 'write') { return bytesToHex(storageAccessProtocol.buildWriteFrame(area, address, dataBytes, frameOptions), ' ') } return bytesToHex(storageAccessProtocol.buildReadFrame(area, address, length, frameOptions), ' ') } catch (error) { return '' } } function normalizeMemoryCommandState(current = {}, changed = {}) { const next = { storageAccessAreaIndex: current.storageAccessAreaIndex || 0, storageAccessAddress: current.storageAccessAddress || '', storageAccessCommandIndex: current.storageAccessCommandIndex || 0, storageAccessDataText: current.storageAccessDataText || '', storageAccessLength: current.storageAccessLength || '', ...changed } const resolvedCommand = resolveMemoryCommand(next.storageAccessCommandIndex) const commandIndex = resolvedCommand.index const command = resolvedCommand.command const resolvedArea = resolveMemoryArea(next.storageAccessAreaIndex, command.key) const areaIndex = resolvedArea.index const area = resolvedArea.area const isCodeInfoDescriptor = command.key === 'read' && area.key === storageAccessProtocol.AREA.CODEINFO const addressWidth = getAreaAddressWidth(area) const addressText = isCodeInfoDescriptor ? '0000' : normalizeDisplayHexText(next.storageAccessAddress, addressWidth) const lengthText = isCodeInfoDescriptor ? storageAccessProtocol.CODE_INFO_DESCRIPTOR_BYTE_LENGTH.toString(16).toUpperCase().padStart(4, '0') : normalizeDisplayHexText(next.storageAccessLength, 16) const dataText = normalizeHexText(next.storageAccessDataText) const dataBytes = dataText ? parseHexBytes(dataText) : [] const address = parseInt(addressText || '0', 16) const byteLength = parseInt(lengthText || '0', 16) const hasAddressText = !!addressText.trim() const hasLengthText = !!lengthText.trim() let errorText = '' const addressError = hasAddressText ? (addressWidth === 32 ? validateHexDwordText(addressText, '地址') : validateHexWordText(addressText, '地址')) : '' const lengthError = hasLengthText ? validateHexWordText(lengthText, '长度') : '' if (addressError) { errorText = addressError } else if (lengthError) { errorText = lengthError } else if (hasLengthText && byteLength <= 0) { errorText = '长度必须大于 0' } else if (command.key === 'write') { const hexError = validateHexText(next.storageAccessDataText) if (hexError) { errorText = hexError } else if (area.readOnly) { errorText = `${area.label} 区不可写` } else if (dataBytes.length !== byteLength) { errorText = `写入长度为 ${byteLength} 字节,当前数据为 ${dataBytes.length} 字节` } } const canPreview = !errorText && hasAddressText && hasLengthText && byteLength > 0 const previewHex = canPreview ? buildMemoryPreview(command.key, area.key, address, byteLength, dataBytes) : '' return { storageAccessAddress: addressText, storageAccessAddressMaxLength: addressWidth === 32 ? 8 : 4, storageAccessAddressWidthText: `${addressWidth}bit`, storageAccessAreaLocked: false, storageAccessAreaOptions: resolvedArea.options, storageAccessAreaIndex: areaIndex, storageAccessAreaLabel: area.label, storageAccessCommandIndex: commandIndex, storageAccessCommandLabel: command.label, storageAccessDataText: dataText, storageAccessErrorText: errorText, storageAccessLength: lengthText, storageAccessPreviewHexText: previewHex, storageAccessPreviewText: canPreview ? (isCodeInfoDescriptor ? 'codeinfo descriptor' : `${area.label} 0x${addressWidth === 32 ? formatDwordHex(address) : address.toString(16).toUpperCase().padStart(4, '0')} / ${byteLength} bytes`) : '', storageAccessShowWriteData: command.key === 'write', storageAccessTitleText: `${command.label}命令` } } function getSyncedDeviceCaps() { return { ...syncedDeviceCaps } } function parseMemoryCommandInput(data = {}) { const state = normalizeMemoryCommandState(data) const command = resolveMemoryCommand(state.storageAccessCommandIndex).command const area = resolveMemoryArea(state.storageAccessAreaIndex, command.key).area const addressWidth = getAreaAddressWidth(area) if (state.storageAccessErrorText) { throw new Error(state.storageAccessErrorText) } if (!String(state.storageAccessAddress || '').trim()) { throw new Error('地址请输入十六进制') } if (!String(state.storageAccessLength || '').trim()) { throw new Error('长度请输入十六进制') } return { area, areaValue: area.key, byteLength: parseHexNumber(state.storageAccessLength, '长度', 0xFFFF), command, commandKey: command.key, dataBytes: command.key === 'write' ? parseHexBytes(state.storageAccessDataText || '') : [], startAddress: addressWidth === 32 ? parseHexDword(state.storageAccessAddress, '地址') : parseHexNumber(state.storageAccessAddress, '地址', 0xFFFF), state } } async function executeMemoryCommand(data = {}, options = {}) { const command = parseMemoryCommandInput(data) if (command.commandKey === 'read') { const bytes = await readMemory( command.areaValue, command.startAddress, command.byteLength, options.label || '存储访问协议读取', options.kind || 'communication-storage-read', options ) return { bytes, command, ok: !!bytes, previewHex: bytes ? bytesToHex(bytes, ' ') : '', state: command.state } } const ok = await writeMemory( command.areaValue, command.startAddress, command.dataBytes, options.label || '存储访问协议写入', options.kind || 'communication-storage-write', options ) return { command, ok, state: command.state } } function getControlCommand(commandKey) { return CONTROL_COMMANDS.find((command) => command.key === commandKey) || null } function normalizeControlState() { return {} } function getControlCommands() { return CONTROL_COMMANDS.map((command) => ({ ...command, previewHexText: bytesToHex(storageAccessProtocol.buildControlFrame(command.op), ' ') })) } async function syncCodeInfo(options = {}) { const result = await readCodeInfoBlock( options.label || '同步CodeInfo', options.kind || 'storage-code-info-read', { maxPacketLength: options.maxPacketLength, showModal: options.showModal !== false } ) if (!result) { return { ok: false } } const descriptorCaps = { addressWidth: result.codeInfoAddressWidth, codeInfoByteLength: result.codeInfoByteLength, maxPacketLength: result.codeInfoMaxPacketLength, memoryEndian: result.codeInfoMemoryEndian, memoryEndianMark: result.codeInfoMemoryEndianMark } const codeInfo = parseCodeInfo(result.codeInfoBytes, descriptorCaps) const importedGroups = createGroupsFromCodeInfo(codeInfo, options) .map(cloneImportedGroup) .map(normalizeGroup) updateSyncedDeviceCaps(descriptorCaps) return { codeInfoAddress: result.codeInfoAddress, codeInfoAddressWidth: result.codeInfoAddressWidth, codeInfoAddressText: formatDwordHex(result.codeInfoAddress), codeInfoByteLength: result.codeInfoByteLength, codeInfoByteLengthText: formatDwordHex(result.codeInfoByteLength), codeInfoBytes: result.codeInfoBytes, codeInfo, codeInfoDescriptorBytes: result.codeInfoDescriptorBytes, codeInfoMaxPacketLength: result.codeInfoMaxPacketLength, codeInfoMemoryEndian: result.codeInfoMemoryEndian, codeInfoMemoryEndianMark: result.codeInfoMemoryEndianMark, groupCount: importedGroups.length, importedGroups, codeInfoMemoryType: result.codeInfoMemoryType, ok: true, structCount: codeInfo.structCount } } async function executeControlCommand(commandKey, data = {}, options = {}) { const command = getControlCommand(commandKey) if (!command) { return { errorText: '特殊指令无效', ok: false } } const response = await executeControl( command.op, [], command.label || '特殊指令', `storage-control-${command.key}`, { maxPacketLength: options.maxPacketLength, showModal: options.showModal !== false } ) if (!response) { return { errorText: '指令执行失败或超时', ok: false } } return { command, ok: true, response } } module.exports = { AREA: storageAccessProtocol.AREA, CONTROL_OP: storageAccessProtocol.CONTROL_OP, executeMemoryCommand, executeControl, executeControlCommand, formatDwordHex, getControlCommand, getControlCommands, getMemoryAreaOptions, getSyncedDeviceCaps, normalizeMemoryCommandState, normalizeControlState, readCodeInfoBlock, readMemory, resolveMaxPacketLength, syncCodeInfo, updateSyncedDeviceCaps, writeMemory }