const { padHex } = require('../../utils/base-utils.js') const { bytesToHex, bytesToWordsLE, formatHexNumber, readByte, readUint16LE, readUint32LE, splitUint16LE, splitUint32LE, toByteArray } = require('../../utils/binary-utils.js') const { BYTE_ORDER_LOW, appendCrc16Ccitt, crc16Ccitt, hasValidCrc16Ccitt } = require('../../utils/crc.js') const PROTOCOL_NAME = 'storage-access' const CMD_ERR_MASK = 0x80 const CMD_CONTROL_FLAG = 0x40 const CMD_SPECIAL_OP_MASK = 0x3F const CMD_WRITE_MASK = 0x08 const CMD_ADDRESS_MODE_MASK = 0x07 const CMD_RESERVED_MASK = 0x30 const CMD_CONTROL = CMD_CONTROL_FLAG const CONTROL_RESPONSE_HEADER_LENGTH = 1 const ADDRESS16_BYTE_LENGTH = 2 const ADDRESS32_BYTE_LENGTH = 4 const AREA = { CODEINFO: 0x00, ADDR32: 0x07, DATA: 0x01, IDATA: 0x02, XDATA: 0x03, CODE: 0x04 } const CONTROL_OP = { RESET: 0x01 } const AREA_NAMES = { [AREA.CODEINFO]: 'CODEINFO', [AREA.ADDR32]: 'ADDR32', [AREA.DATA]: 'DATA', [AREA.IDATA]: 'IDATA', [AREA.XDATA]: 'XDATA', [AREA.CODE]: 'CODE' } const AREA_BY_NAME = { CODEINFO: AREA.CODEINFO, CODE_INFO: AREA.CODEINFO, ADDR32: AREA.ADDR32, ADDRESS32: AREA.ADDR32, DATA: AREA.DATA, IDATA: AREA.IDATA, XDATA: AREA.XDATA, CODE: AREA.CODE } const EXCEPTION_MESSAGES = { 0x01: '非法命令', 0x02: '非法区域', 0x03: '非法地址', 0x04: '非法长度', 0x05: '写保护', 0x06: '设备忙', 0x07: '格式错误', 0x08: '访问被拒绝', 0x09: '内部错误', 0x0A: '对齐错误', 0x0B: '范围溢出', 0x0C: '不支持的操作' } const DEFAULT_MAX_FRAME_BYTES = 64 const UNLIMITED_FRAME_BYTES = 0 const MAX_UINT16 = 0xFFFF const MAX_UINT32 = 0xFFFFFFFF const MAX_PAYLOAD_BYTES = MAX_UINT16 const READ_REQUEST_LENGTH_16 = 1 + ADDRESS16_BYTE_LENGTH + 2 + 2 const READ_REQUEST_LENGTH_32 = 1 + ADDRESS32_BYTE_LENGTH + 2 + 2 const WRITE_REQUEST_OVERHEAD_16 = 1 + ADDRESS16_BYTE_LENGTH + 2 + 2 const WRITE_REQUEST_OVERHEAD_32 = 1 + ADDRESS32_BYTE_LENGTH + 2 + 2 const READ_RESPONSE_OVERHEAD_16 = 1 + ADDRESS16_BYTE_LENGTH + 2 + 2 const READ_RESPONSE_OVERHEAD_32 = 1 + ADDRESS32_BYTE_LENGTH + 2 + 2 const WRITE_RESPONSE_LENGTH_16 = 1 + ADDRESS16_BYTE_LENGTH + 2 + 2 const WRITE_RESPONSE_LENGTH_32 = 1 + ADDRESS32_BYTE_LENGTH + 2 + 2 const EXCEPTION_RESPONSE_LENGTH = 4 const CODE_INFO_DESCRIPTOR_ADDRESS = 0 const CODE_INFO_DESCRIPTOR_BYTE_LENGTH = 11 const CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH = 1 + CODE_INFO_DESCRIPTOR_BYTE_LENGTH + 2 const MEMORY_ENDIAN_MARK_BIG = 0x55AA const MEMORY_ENDIAN_MARK_LITTLE = 0xAA55 const MEMORY_ENDIAN = { BIG: 'big', LITTLE: 'little' } const STORAGE_CRC_OPTIONS = { byteOrder: BYTE_ORDER_LOW } const VALID_AREAS = [AREA.CODEINFO, AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA, AREA.CODE] const MEMORY_AREAS = [AREA.CODEINFO, AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA, AREA.CODE] const WRITABLE_AREAS = [AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA] const RESERVED_AREAS = [0x05, 0x06] const readWord = readUint16LE const readDword = readUint32LE const splitWord = splitUint16LE const splitDword = splitUint32LE function toByte(value, label) { if (!Number.isInteger(value) || value < 0 || value > 0xFF) { throw new Error(`${label}必须在 0x00 至 0xFF 之间`) } return value } function toWord(value, label) { if (!Number.isInteger(value) || value < 0 || value > 0xFFFF) { throw new Error(`${label}必须在 0x0000 至 0xFFFF 之间`) } return value } function toUint32(value, label) { if (!Number.isInteger(value) || value < 0 || value > MAX_UINT32) { throw new Error(`${label}必须在 0x00000000 至 0xFFFFFFFF 之间`) } return value } function normalizeArea(value) { if (typeof value === 'string') { const area = AREA_BY_NAME[value.trim().toUpperCase()] if (area !== undefined) return area } const area = toByte(Number(value), '存储区域') if (VALID_AREAS.indexOf(area) < 0) { throw new Error('存储区域必须为 codeinfo/data/idata/xdata/code/addr32') } return area } function isAddress32Area(area) { return Number(area) === AREA.ADDR32 } function getAddressFieldByteLength(area) { return isAddress32Area(area) ? ADDRESS32_BYTE_LENGTH : ADDRESS16_BYTE_LENGTH } function getMemoryHeaderLength(area) { return 1 + getAddressFieldByteLength(area) + 2 } function getReadRequestLength(area) { if (Number(area) === AREA.CODEINFO) return 3 return getMemoryHeaderLength(area) + 2 } function getWriteRequestOverhead(area) { return getMemoryHeaderLength(area) + 2 } function getReadResponseOverhead(area) { if (Number(area) === AREA.CODEINFO) return 1 + 2 return getMemoryHeaderLength(area) + 2 } function getWriteResponseLength(area) { return getMemoryHeaderLength(area) + 2 } function isCodeInfoDescriptorArea(area) { return Number(area) === AREA.CODEINFO } function normalizeMemoryArea(value) { const area = normalizeArea(value) if (MEMORY_AREAS.indexOf(area) < 0) { throw new Error('存储访问区域必须为 codeinfo/data/idata/xdata/code/addr32') } return area } function toByteLength(value, label = '字节长度', maxPayload = MAX_PAYLOAD_BYTES) { const byteLength = toWord(Number(value), label) if (byteLength === 0) { throw new Error(`${label}必须大于 0`) } if (maxPayload > 0 && byteLength > maxPayload) { throw new Error(`单帧最多访问 ${maxPayload} 字节`) } return byteLength } function normalizeDescriptorAddressWidth(value) { const numberValue = Number(value) if (numberValue === 16 || numberValue === 32) return numberValue throw new Error('CodeInfo 描述符地址长度必须为 16 或 32') } function parseMemoryEndianMark(bytes, offset) { const firstByte = readByte(bytes, offset) const secondByte = readByte(bytes, offset + 1) const marker = ((firstByte << 8) | secondByte) & 0xFFFF if (firstByte === 0x55 && secondByte === 0xAA) { return { marker: MEMORY_ENDIAN_MARK_BIG, memoryEndian: MEMORY_ENDIAN.BIG } } if (firstByte === 0xAA && secondByte === 0x55) { return { marker: MEMORY_ENDIAN_MARK_LITTLE, memoryEndian: MEMORY_ENDIAN.LITTLE } } return { marker, memoryEndian: '' } } function normalizeMaxFrameBytes(maxFrameBytes = DEFAULT_MAX_FRAME_BYTES) { const numberValue = Number(maxFrameBytes) if (Number.isFinite(numberValue) && Math.round(numberValue) === UNLIMITED_FRAME_BYTES) return UNLIMITED_FRAME_BYTES if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue) return DEFAULT_MAX_FRAME_BYTES } function resolveDescriptorMaxFrameBytes(configuredMaxFrameBytes, descriptorMaxFrameBytes) { const configured = normalizeMaxFrameBytes(configuredMaxFrameBytes) const descriptor = normalizeMaxFrameBytes(descriptorMaxFrameBytes) if (descriptor === UNLIMITED_FRAME_BYTES) return configured if (configured === UNLIMITED_FRAME_BYTES) return descriptor return Math.min(configured, descriptor) } function getPayloadLimitFromFrame(maxFrameBytes, overhead) { const frameBytes = normalizeMaxFrameBytes(maxFrameBytes) if (frameBytes === UNLIMITED_FRAME_BYTES) return MAX_PAYLOAD_BYTES return Math.max(0, Math.min(MAX_PAYLOAD_BYTES, frameBytes - overhead)) } function getMaxReadByteLength(maxFrameBytes = DEFAULT_MAX_FRAME_BYTES, area = AREA.ADDR32) { return getPayloadLimitFromFrame(maxFrameBytes, getReadResponseOverhead(area)) } function getMaxWriteByteLength(maxFrameBytes = DEFAULT_MAX_FRAME_BYTES, area = AREA.ADDR32) { return getPayloadLimitFromFrame(maxFrameBytes, getWriteRequestOverhead(area)) } function buildCommand(area, isWrite = false) { const normalizedArea = normalizeMemoryArea(area) if (RESERVED_AREAS.indexOf(normalizedArea) >= 0) { throw new Error('存储访问区域号 0x05/0x06 暂时保留') } if (isWrite && WRITABLE_AREAS.indexOf(normalizedArea) < 0) { throw new Error(`${AREA_NAMES[normalizedArea] || '该'} 区不可写`) } return (isWrite ? CMD_WRITE_MASK : 0x00) | normalizedArea } function buildSpecialCommand(operation) { const op = toByte(Number(operation), '特殊指令') & CMD_SPECIAL_OP_MASK if (op <= 0) { throw new Error('特殊指令必须在 0x01 至 0x3F 之间') } return CMD_CONTROL_FLAG | op } function decodeCommand(command) { const cmd = toByte(Number(command), '命令字') const sourceCommand = cmd & ~CMD_ERR_MASK const isControl = !!(sourceCommand & CMD_CONTROL_FLAG) const operation = isControl ? (sourceCommand & CMD_SPECIAL_OP_MASK) : 0 const area = isControl ? CMD_CONTROL : (sourceCommand & CMD_ADDRESS_MODE_MASK) const reservedBits = sourceCommand & CMD_RESERVED_MASK return { addressBytes: isControl ? 0 : getAddressFieldByteLength(area), area, command: cmd, hasError: !!(cmd & CMD_ERR_MASK), hasInvalidSpecialOperation: isControl && operation === 0, hasReservedBits: !isControl && reservedBits !== 0, isAddress32: !isControl && isAddress32Area(area), isControl, isWrite: !isControl && !!(sourceCommand & CMD_WRITE_MASK), operation, sourceCommand } } function hasValidStorageCrc(bytes) { const frame = toByteArray(bytes) if (frame.length < 3) return false if (frame.length >= 4) return hasValidCrc16Ccitt(frame, STORAGE_CRC_OPTIONS) const expected = crc16Ccitt(frame.slice(0, -2), STORAGE_CRC_OPTIONS) const received = readWord(frame, frame.length - 2) return expected === received } function appendStorageCrc(bytes) { return appendCrc16Ccitt(bytes, STORAGE_CRC_OPTIONS) } function buildReadFrame(area, address, byteLength, options = {}) { const normalizedArea = normalizeMemoryArea(area) if (isCodeInfoDescriptorArea(normalizedArea)) { return buildCodeInfoDescriptorFrame() } const addressBytes = getAddressFieldByteLength(normalizedArea) const startAddress = addressBytes === ADDRESS32_BYTE_LENGTH ? toUint32(Number(address), '内存地址') : toWord(Number(address), '内存地址') const maxByteLength = getMaxReadByteLength(options.maxFrameBytes, normalizedArea) const length = toByteLength(Number(byteLength), '读取字节长度', maxByteLength || MAX_PAYLOAD_BYTES) const command = buildCommand(normalizedArea, false) const addressParts = addressBytes === ADDRESS32_BYTE_LENGTH ? splitDword(startAddress) : splitWord(startAddress) const lengthParts = splitWord(toWord(length, '读取字节长度')) return appendStorageCrc([command].concat(addressParts, lengthParts)) } function buildWriteFrame(area, address, bytes, options = {}) { const normalizedArea = normalizeMemoryArea(area) if (WRITABLE_AREAS.indexOf(normalizedArea) < 0) { throw new Error(`${AREA_NAMES[normalizedArea] || '该'} 区不可写`) } const addressBytes = getAddressFieldByteLength(normalizedArea) const startAddress = addressBytes === ADDRESS32_BYTE_LENGTH ? toUint32(Number(address), '内存地址') : toWord(Number(address), '内存地址') const dataBytes = toByteArray(bytes).map((byte) => toByte(Number(byte), '写入字节')) const maxByteLength = getMaxWriteByteLength(options.maxFrameBytes, normalizedArea) const length = toByteLength(dataBytes.length, '写入字节长度', maxByteLength || MAX_PAYLOAD_BYTES) const command = buildCommand(normalizedArea, true) const addressParts = addressBytes === ADDRESS32_BYTE_LENGTH ? splitDword(startAddress) : splitWord(startAddress) const lengthParts = splitWord(toWord(length, '写入字节长度')) return appendStorageCrc([command].concat(addressParts, lengthParts, dataBytes)) } function buildControlFrame(operation, dataBytes = []) { const command = buildSpecialCommand(operation) const payload = toByteArray(dataBytes).map((byte) => toByte(Number(byte), '指令数据')) return appendStorageCrc([command].concat(payload)) } function buildCodeInfoDescriptorFrame() { return appendStorageCrc([buildCommand(AREA.CODEINFO, false)]) } function parseCodeInfoDescriptorBytes(bytes) { const dataBytes = toByteArray(bytes) if (dataBytes.length < CODE_INFO_DESCRIPTOR_BYTE_LENGTH) { throw new Error('CodeInfo 描述符长度无效') } const endianMark = parseMemoryEndianMark(dataBytes, 9) if (!endianMark.memoryEndian) { throw new Error('CodeInfo 描述符字节序标记无效') } return { codeInfoAddress: readDword(dataBytes, 0), codeInfoAddressWidth: dataBytes[6] & 0xFF, codeInfoByteLength: readWord(dataBytes, 4), codeInfoDescriptorBytes: dataBytes.slice(0, CODE_INFO_DESCRIPTOR_BYTE_LENGTH), codeInfoMaxPacketLength: readWord(dataBytes, 7), codeInfoMemoryEndian: endianMark.memoryEndian, codeInfoMemoryEndianMark: endianMark.marker } } function formatHex(bytes) { return bytesToHex(bytes, ' ') } function parseStorageAccessResponse(bytes) { const frame = toByteArray(bytes) if (frame.length < 3 || !hasValidStorageCrc(frame)) return null const command = frame[0] & 0xFF const decoded = decodeCommand(command) if (decoded.isControl) return parseStorageControlResponse(frame, decoded) if (decoded.hasError) { if (frame.length !== EXCEPTION_RESPONSE_LENGTH) return null return { area: decoded.area, areaName: AREA_NAMES[decoded.area] || 'UNKNOWN', command, exceptionCode: frame[1] & 0xFF, isException: true, isWrite: decoded.isWrite, protocol: PROTOCOL_NAME, sourceCommand: decoded.sourceCommand } } if (decoded.hasReservedBits) return null if (!AREA_NAMES[decoded.area]) return null if (isCodeInfoDescriptorArea(decoded.area)) { if (decoded.isWrite || frame.length !== CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH) return null const dataBytes = frame.slice(1, 1 + CODE_INFO_DESCRIPTOR_BYTE_LENGTH) const response = { address: CODE_INFO_DESCRIPTOR_ADDRESS, addressWidth: 0, area: decoded.area, areaName: AREA_NAMES[decoded.area] || 'UNKNOWN', byteLength: CODE_INFO_DESCRIPTOR_BYTE_LENGTH, command, dataBytes, isException: false, isAddress32: false, isWrite: false, protocol: PROTOCOL_NAME, words: bytesToWordsLE(dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0)) } return { ...response, ...parseCodeInfoDescriptorBytes(dataBytes) } } const addressBytes = decoded.addressBytes const headerLength = getMemoryHeaderLength(decoded.area) if (decoded.isWrite) { if (frame.length !== getWriteResponseLength(decoded.area)) return null return { address: decoded.isAddress32 ? readDword(frame, 1) : readWord(frame, 1), addressWidth: decoded.isAddress32 ? 32 : 16, area: decoded.area, areaName: AREA_NAMES[decoded.area] || 'UNKNOWN', byteLength: readWord(frame, 1 + addressBytes), command, dataBytes: [], isException: false, isAddress32: decoded.isAddress32, isWrite: true, protocol: PROTOCOL_NAME } } if (frame.length < getReadResponseOverhead(decoded.area)) return null const address = decoded.isAddress32 ? readDword(frame, 1) : readWord(frame, 1) const byteLength = readWord(frame, 1 + addressBytes) const dataStart = headerLength const dataEnd = dataStart + byteLength if (frame.length !== dataEnd + 2) return null const dataBytes = frame.slice(dataStart, dataEnd) const response = { address, addressWidth: decoded.isAddress32 ? 32 : 16, area: decoded.area, areaName: AREA_NAMES[decoded.area] || 'UNKNOWN', byteLength, command, dataBytes, isException: false, isAddress32: decoded.isAddress32, isWrite: false, protocol: PROTOCOL_NAME, words: bytesToWordsLE(dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0)) } return response } function parseStorageControlResponse(frame, decoded) { if (decoded.hasInvalidSpecialOperation) return null if (decoded.hasError) { if (frame.length !== EXCEPTION_RESPONSE_LENGTH) return null return { area: CMD_CONTROL, areaName: 'CONTROL', command: frame[0] & 0xFF, exceptionCode: frame[1] & 0xFF, isControl: true, isException: true, isWrite: false, operation: decoded.operation, protocol: PROTOCOL_NAME, sourceCommand: decoded.sourceCommand } } if (frame.length < CONTROL_RESPONSE_HEADER_LENGTH + 2) return null const dataStart = CONTROL_RESPONSE_HEADER_LENGTH const dataEnd = frame.length - 2 const byteLength = Math.max(0, dataEnd - dataStart) const dataBytes = frame.slice(dataStart, dataEnd) return { area: CMD_CONTROL, areaName: 'CONTROL', byteLength, command: frame[0] & 0xFF, dataBytes, isControl: true, isException: false, isWrite: false, operation: decoded.operation, protocol: PROTOCOL_NAME } } function parseStorageAccessRequest(bytes) { const frame = toByteArray(bytes) if (frame.length < 3 || !hasValidStorageCrc(frame)) return null const command = frame[0] & 0xFF const decoded = decodeCommand(command) if (decoded.isControl) return parseStorageControlRequest(frame, decoded) if (decoded.hasError) return null if (decoded.hasReservedBits) return null if (!AREA_NAMES[decoded.area]) return null if (isCodeInfoDescriptorArea(decoded.area)) { if (decoded.isWrite || frame.length !== getReadRequestLength(decoded.area)) return null return { address: CODE_INFO_DESCRIPTOR_ADDRESS, addressWidth: 0, area: decoded.area, areaName: AREA_NAMES[decoded.area] || 'UNKNOWN', byteLength: CODE_INFO_DESCRIPTOR_BYTE_LENGTH, command, dataBytes: [], isAddress32: false, isWrite: false, kind: 'raw-hex', operation: 'read', protocol: PROTOCOL_NAME, quantity: CODE_INFO_DESCRIPTOR_BYTE_LENGTH } } if (frame.length < READ_REQUEST_LENGTH_16) return null const addressBytes = decoded.addressBytes const headerLength = getMemoryHeaderLength(decoded.area) if (frame.length < headerLength + 2) return null const address = decoded.isAddress32 ? readDword(frame, 1) : readWord(frame, 1) const byteLength = readWord(frame, 1 + addressBytes) const expectedLength = decoded.isWrite ? headerLength + byteLength + 2 : headerLength + 2 if (byteLength <= 0 || frame.length !== expectedLength) return null return { address, addressWidth: decoded.isAddress32 ? 32 : 16, area: decoded.area, areaName: AREA_NAMES[decoded.area] || 'UNKNOWN', byteLength, command, dataBytes: decoded.isWrite ? frame.slice(headerLength, headerLength + byteLength) : [], isAddress32: decoded.isAddress32, isWrite: decoded.isWrite, kind: 'raw-hex', operation: decoded.isWrite ? 'write' : 'read', protocol: PROTOCOL_NAME, quantity: byteLength } } function parseStorageControlRequest(frame, decoded = decodeCommand(frame && frame[0])) { if (frame.length < 3 || decoded.hasInvalidSpecialOperation) return null const dataStart = CONTROL_RESPONSE_HEADER_LENGTH const dataEnd = frame.length - 2 const byteLength = Math.max(0, dataEnd - dataStart) return { area: CMD_CONTROL, areaName: 'CONTROL', byteLength, command: frame[0] & 0xFF, dataBytes: frame.slice(dataStart, dataEnd), isControl: true, isWrite: false, kind: 'storage-control', operation: decoded.operation, protocol: PROTOCOL_NAME } } function getExpectedResponseLength(expected, responseCommand, responseBytes = []) { if (!expected) return 0 const command = Number(responseCommand) & 0xFF if (command === (expected.command | CMD_ERR_MASK)) return EXCEPTION_RESPONSE_LENGTH if (command !== expected.command) return 0 if (expected.isControl) { if (responseBytes.length < CONTROL_RESPONSE_HEADER_LENGTH) return 0 return CONTROL_RESPONSE_HEADER_LENGTH + Number(expected.expectedByteLength || 0) + 2 } if (expected.operation === 'write' || expected.isWrite) { return getWriteResponseLength(expected.area) } if (isCodeInfoDescriptorArea(expected.area)) { return CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH } const headerLength = getMemoryHeaderLength(expected.area) if (responseBytes.length < headerLength) return 0 return headerLength + readWord(responseBytes, 1 + getAddressFieldByteLength(expected.area)) + 2 } function isExpectedResponse(response, expected) { if (!response || !expected) return false const sourceCommand = response.isException ? response.sourceCommand : response.command if (sourceCommand !== expected.command) return false if (expected.isControl) { if (!response.isControl) return false if (response.isException) return true if (response.operation !== expected.operation) return false return true } if (!response.isException && response.area !== expected.area) return false if (response.isException) return true if (response.address !== expected.address) return false if (response.byteLength !== expected.byteLength) return false if (!response.isWrite && (!Array.isArray(response.dataBytes) || response.dataBytes.length !== expected.byteLength)) return false return true } function getExceptionText(code) { return EXCEPTION_MESSAGES[code] || '未知异常' } function formatExceptionMessage(response) { const sourceCommand = response && response.sourceCommand const exceptionCode = response && response.exceptionCode const exceptionText = getExceptionText(exceptionCode) return `设备返回异常帧:命令 0x${padHex(sourceCommand, 2)},异常码 0x${padHex(exceptionCode, 2)}(${exceptionText})` } function getReadBufferHint(expected) { if (!expected) return 0 if (expected.isControl) return expected.responseBufferHint || CONTROL_RESPONSE_HEADER_LENGTH + Number(expected.expectedByteLength || 0) + 2 if (expected.operation === 'write' || expected.isWrite) { return getWriteResponseLength(expected.area) } if (isCodeInfoDescriptorArea(expected.area)) { return CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH } return getReadResponseOverhead(expected.area) + Number(expected.byteLength || expected.quantity || 0) } function alignResponseBuffer(buffer, expected) { if (!Array.isArray(buffer) || !buffer.length || !expected) return const expectedCommands = [expected.command, expected.command | CMD_ERR_MASK] let matchIndex = -1 for (let index = 0; index < buffer.length; index += 1) { if (expectedCommands.indexOf(buffer[index]) < 0) continue matchIndex = index break } if (matchIndex > 0) { buffer.splice(0, matchIndex) } else if (matchIndex < 0 && buffer.length > 1) { buffer.splice(0, buffer.length - 1) } } function readResponseFromBuffer(buffer, expected, options = {}) { if (!Array.isArray(buffer) || !buffer.length || !expected) { return { status: 'pending' } } alignResponseBuffer(buffer, expected) while (buffer.length >= 1) { const responseCommand = buffer[0] const responseLength = getExpectedResponseLength(expected, responseCommand, buffer) if (!responseLength) { return { status: 'pending' } } const frameLimit = normalizeMaxFrameBytes( options.maxFrameBytes === undefined ? expected.maxFrameBytes : options.maxFrameBytes ) if (frameLimit > 0 && responseLength > frameLimit) { return { frameLimit, responseLength, status: 'frame-too-long' } } if (buffer.length < responseLength) { return { status: 'pending' } } const frameBytes = buffer.slice(0, responseLength) const response = parseStorageAccessResponse(frameBytes) if (!response) { return { frameBytes, status: 'invalid' } } if (!isExpectedResponse(response, expected)) { buffer.shift() alignResponseBuffer(buffer, expected) continue } if (response.isException) { return { message: formatExceptionMessage(response), response, status: 'exception' } } buffer.splice(0, responseLength) return { response, status: 'complete' } } return { status: 'pending' } } function createExpected(area, address, byteLength, isWrite, kind, options = {}) { const normalizedArea = normalizeMemoryArea(area) const command = buildCommand(normalizedArea, isWrite) return { address, addressWidth: isAddress32Area(normalizedArea) ? 32 : 16, area: normalizedArea, byteLength, command, isAddress32: isAddress32Area(normalizedArea), isWrite, kind, operation: isWrite ? 'write' : 'read', protocol: PROTOCOL_NAME, quantity: byteLength } } function createControlExpected(operation, kind = 'storage-control', options = {}) { const op = toByte(Number(operation), '特殊指令') const command = buildSpecialCommand(op) return { area: CMD_CONTROL, byteLength: 0, command, expectedByteLength: Number(options.expectedByteLength) || 0, isControl: true, isWrite: false, kind, operation: op, protocol: PROTOCOL_NAME, responseBufferHint: CONTROL_RESPONSE_HEADER_LENGTH + (Number(options.expectedByteLength) || 0) + 2 } } function formatAddress(value) { return formatHexNumber(value, 1).replace(/^0+(?=[0-9A-F])/, '') } function getChunkLabel(label, chunks, chunk) { if (!label || chunks.length <= 1) return label return `${label} ${formatAddress(chunk.address)}-${formatAddress(chunk.address + chunk.quantity - 1)}` } function splitQuantity(startAddress, quantity, maxQuantity) { const chunks = [] let address = Number(startAddress) || 0 let remaining = Math.max(0, Math.floor(Number(quantity) || 0)) const chunkLimit = Math.max(1, Math.floor(Number(maxQuantity) || remaining || 1)) while (remaining > 0) { const chunkQuantity = Math.min(remaining, chunkLimit) chunks.push({ address, quantity: chunkQuantity }) address += chunkQuantity remaining -= chunkQuantity } return chunks } function getReadChunks(startAddress, byteLength, options = {}) { const maxByteLength = getMaxReadByteLength(options.maxFrameBytes, options.area) return splitQuantity(startAddress, byteLength, maxByteLength || byteLength) } function getWriteChunks(startAddress, bytes, options = {}) { const sourceBytes = Array.prototype.slice.call(bytes || []).map((byte) => Number(byte) & 0xFF) const maxByteLength = getMaxWriteByteLength(options.maxFrameBytes, options.area) const chunks = splitQuantity(startAddress, sourceBytes.length, maxByteLength || sourceBytes.length) let offset = 0 return chunks.map((chunk) => { const dataBytes = sourceBytes.slice(offset, offset + chunk.quantity) offset += chunk.quantity return { ...chunk, dataBytes } }) } const response = { createControlExpected, createExpected, formatExceptionMessage, getExceptionText, getExpectedResponseLength, getReadBufferHint, isExpectedResponse, parseStorageAccessRequest, parseStorageAccessResponse, readResponseFromBuffer } module.exports = { AREA, AREA_BY_NAME, AREA_NAMES, ADDRESS16_BYTE_LENGTH, ADDRESS32_BYTE_LENGTH, CMD_CONTROL, CMD_CONTROL_FLAG, CMD_SPECIAL_OP_MASK, CMD_ADDRESS_MODE_MASK, CMD_ERR_MASK, CMD_RESERVED_MASK, CMD_WRITE_MASK, CONTROL_OP, CONTROL_RESPONSE_HEADER_LENGTH, DEFAULT_MAX_FRAME_BYTES, EXCEPTION_MESSAGES, EXCEPTION_RESPONSE_LENGTH, CODE_INFO_DESCRIPTOR_ADDRESS, CODE_INFO_DESCRIPTOR_BYTE_LENGTH, MAX_PAYLOAD_BYTES, MEMORY_ENDIAN, MEMORY_ENDIAN_MARK_BIG, MEMORY_ENDIAN_MARK_LITTLE, PROTOCOL_NAME, READ_REQUEST_LENGTH_16, READ_REQUEST_LENGTH_32, READ_RESPONSE_OVERHEAD_16, READ_RESPONSE_OVERHEAD_32, STORAGE_CRC_OPTIONS, UNLIMITED_FRAME_BYTES, WRITE_REQUEST_OVERHEAD_16, WRITE_REQUEST_OVERHEAD_32, WRITE_RESPONSE_LENGTH_16, WRITE_RESPONSE_LENGTH_32, appendStorageCrc, buildCommand, buildCodeInfoDescriptorFrame, buildControlFrame, buildReadFrame, buildSpecialCommand, buildWriteFrame, createControlExpected, createExpected, decodeCommand, formatHex, getChunkLabel, getReadChunks, getWriteChunks, getMaxReadByteLength, getMaxWriteByteLength, hasValidStorageCrc, normalizeDescriptorAddressWidth, normalizeArea, normalizeMaxFrameBytes, normalizeMemoryArea, parseCodeInfoDescriptorBytes, resolveDescriptorMaxFrameBytes, response, splitDword, splitQuantity, splitWord, toByte, toByteLength, toUint32, toWord }