|
|
@@ -1,31 +1,6 @@
|
|
|
-const {
|
|
|
- buildReadFrame,
|
|
|
- buildWriteMultipleRegistersFrame,
|
|
|
- buildWriteSingleCoilFrame,
|
|
|
- buildWriteSingleRegisterFrame,
|
|
|
- formatHex,
|
|
|
- getReadResponseByteLength,
|
|
|
- MODBUS_CRC_OPTIONS,
|
|
|
- MAX_MODBUS_DMA_BYTES,
|
|
|
- hasValidCrc16Modbus
|
|
|
-} = require('./modbus-rtu')
|
|
|
-const {
|
|
|
- BYTE_ORDER_HIGH,
|
|
|
- hasValidCrc16Ccitt
|
|
|
-} = require('./crc')
|
|
|
-const {
|
|
|
- getBootloaderResponseLength,
|
|
|
- isBootloaderFrame
|
|
|
-} = require('./bootloader-frame')
|
|
|
const {
|
|
|
notifyPageToast
|
|
|
-} = require('./page-toast')
|
|
|
-const {
|
|
|
- padHex
|
|
|
-} = require('./base-utils')
|
|
|
-const {
|
|
|
- bytesToWords
|
|
|
-} = require('./binary-utils')
|
|
|
+} = require('../utils/page-toast.js')
|
|
|
|
|
|
const SCAN_TIMEOUT = 15000
|
|
|
const CONNECT_TIMEOUT = 10000
|
|
|
@@ -43,31 +18,10 @@ const MODULE_PACKET_SIZES = [
|
|
|
]
|
|
|
const RESPONSE_TIMEOUT = 1000
|
|
|
const MAX_RESPONSE_BUFFER_BYTES = 128
|
|
|
+const DEFAULT_MAX_FRAME_BYTES = 64
|
|
|
const MAX_LOG_COUNT = 100
|
|
|
const TARGET_BLE_UUIDS = ['FFE0', 'FFE1']
|
|
|
|
|
|
-const MODBUS_EXCEPTION_MESSAGES = {
|
|
|
- 0x01: '非法功能',
|
|
|
- 0x02: '非法数据地址',
|
|
|
- 0x03: '非法数据值',
|
|
|
- 0x04: '从站设备故障',
|
|
|
- 0x05: '确认',
|
|
|
- 0x06: '从站设备忙',
|
|
|
- 0x08: '存储奇偶性错误',
|
|
|
- 0x0A: '网关路径不可用',
|
|
|
- 0x0B: '网关目标设备响应失败'
|
|
|
-}
|
|
|
-
|
|
|
-const MODBUS_COMMANDS = [
|
|
|
- { key: 'readCoils', label: '01 读取线圈', functionCode: 0x01, inputMode: 'quantity' },
|
|
|
- { key: 'readDiscreteInputs', label: '02 读取离散输入', functionCode: 0x02, inputMode: 'quantity' },
|
|
|
- { key: 'readHolding', label: '03 读取保持寄存器', functionCode: 0x03, inputMode: 'quantity' },
|
|
|
- { key: 'readInput', label: '04 读取输入寄存器', functionCode: 0x04, inputMode: 'quantity' },
|
|
|
- { key: 'writeCoil', label: '05 写单线圈', functionCode: 0x05, inputMode: 'coil' },
|
|
|
- { key: 'writeRegister', label: '06 写单寄存器', functionCode: 0x06, inputMode: 'single' },
|
|
|
- { key: 'writeRegisters', label: '10 写多寄存器', functionCode: 0x10, inputMode: 'multiple' }
|
|
|
-]
|
|
|
-
|
|
|
const bluetoothErrorMap = {
|
|
|
10000: '蓝牙模块未初始化,请重新扫描',
|
|
|
10001: '蓝牙不可用,请开启手机蓝牙',
|
|
|
@@ -98,22 +52,11 @@ const state = {
|
|
|
isSending: false,
|
|
|
logScrollTarget: '',
|
|
|
logs: [],
|
|
|
- commandIndex: 2,
|
|
|
- commandValue: '0001',
|
|
|
- commandValueLabel: '读取数量',
|
|
|
- coilEnabled: true,
|
|
|
- generatedHex: '',
|
|
|
rxCount: 0,
|
|
|
sendHex: '',
|
|
|
sendQueueLength: 0,
|
|
|
- protocolCommands: MODBUS_COMMANDS,
|
|
|
- protocolErrorText: '',
|
|
|
- registerAddress: '0000',
|
|
|
- showCoilValue: false,
|
|
|
- showCommandValue: true,
|
|
|
systemTip: '',
|
|
|
txCount: 0,
|
|
|
- slaveAddress: 'F0',
|
|
|
writeCharacteristicId: '',
|
|
|
writeServiceId: '',
|
|
|
writeType: ''
|
|
|
@@ -133,6 +76,42 @@ let deviceSequence = 0
|
|
|
let logSequence = 0
|
|
|
const subscribers = []
|
|
|
const rawResponseSubscribers = []
|
|
|
+const protocolHelpers = {
|
|
|
+ getResponseBufferHint: null,
|
|
|
+ inspectReceivedBytes: null,
|
|
|
+ parseSendExpected: null,
|
|
|
+ readResponseFromBuffer: null
|
|
|
+}
|
|
|
+let protocolHelpersLoaded = false
|
|
|
+let protocolHelpersLoader = null
|
|
|
+
|
|
|
+function applyProtocolHelpers(helpers = {}) {
|
|
|
+ Object.assign(protocolHelpers, {
|
|
|
+ getResponseBufferHint: typeof helpers.getResponseBufferHint === 'function' ? helpers.getResponseBufferHint : null,
|
|
|
+ inspectReceivedBytes: typeof helpers.inspectReceivedBytes === 'function' ? helpers.inspectReceivedBytes : null,
|
|
|
+ parseSendExpected: typeof helpers.parseSendExpected === 'function' ? helpers.parseSendExpected : null,
|
|
|
+ readResponseFromBuffer: typeof helpers.readResponseFromBuffer === 'function' ? helpers.readResponseFromBuffer : null
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function configureProtocolHelpers(helpers = {}) {
|
|
|
+ if (typeof helpers === 'function') {
|
|
|
+ protocolHelpersLoader = helpers
|
|
|
+ protocolHelpersLoaded = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ protocolHelpersLoader = null
|
|
|
+ protocolHelpersLoaded = true
|
|
|
+ applyProtocolHelpers(helpers)
|
|
|
+}
|
|
|
+
|
|
|
+function ensureProtocolHelpers() {
|
|
|
+ if (protocolHelpersLoaded || typeof protocolHelpersLoader !== 'function') return
|
|
|
+
|
|
|
+ protocolHelpersLoaded = true
|
|
|
+ applyProtocolHelpers(protocolHelpersLoader() || {})
|
|
|
+}
|
|
|
|
|
|
function setState(changedData) {
|
|
|
Object.assign(state, changedData)
|
|
|
@@ -301,102 +280,38 @@ function validateHex(value) {
|
|
|
return ''
|
|
|
}
|
|
|
|
|
|
-function parseHexNumber(value, label, maxValue) {
|
|
|
- const text = String(value || '').trim().replace(/^0x/i, '')
|
|
|
-
|
|
|
- if (!text || !/^[0-9a-fA-F]+$/.test(text)) {
|
|
|
- throw new Error(`${label}请输入十六进制数值`)
|
|
|
- }
|
|
|
-
|
|
|
- const parsedValue = parseInt(text, 16)
|
|
|
- if (parsedValue > maxValue) {
|
|
|
- throw new Error(`${label}超出范围`)
|
|
|
- }
|
|
|
-
|
|
|
- return parsedValue
|
|
|
-}
|
|
|
-
|
|
|
-function parseRegisterValues(value) {
|
|
|
- const text = String(value || '').trim()
|
|
|
- if (!text) throw new Error('请输入寄存器写入值')
|
|
|
-
|
|
|
- return text.split(/[\s,;]+/)
|
|
|
- .filter(Boolean)
|
|
|
- .map((item) => parseHexNumber(item, '写入值', 0xFFFF))
|
|
|
-}
|
|
|
-
|
|
|
function normalizeMaxFrameBytes(maxFrameBytes) {
|
|
|
const numberValue = Number(maxFrameBytes)
|
|
|
if (Number.isFinite(numberValue) && Math.round(numberValue) === 0) return 0
|
|
|
if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue)
|
|
|
|
|
|
- return MAX_MODBUS_DMA_BYTES
|
|
|
+ return DEFAULT_MAX_FRAME_BYTES
|
|
|
}
|
|
|
|
|
|
-function getResponseBufferLimit(expected, maxFrameBytes) {
|
|
|
- const responseLength = expected
|
|
|
- ? getReadResponseByteLength(expected.functionCode, expected.quantity)
|
|
|
- : 0
|
|
|
- const frameLimit = normalizeMaxFrameBytes(maxFrameBytes)
|
|
|
-
|
|
|
- if (frameLimit === 0) {
|
|
|
- return Math.max(MAX_RESPONSE_BUFFER_BYTES, responseLength + 8)
|
|
|
+function getResponseBufferHint(expected, options = {}) {
|
|
|
+ if (Number.isFinite(Number(options.responseBufferHint))) {
|
|
|
+ return Math.max(0, Math.round(Number(options.responseBufferHint)))
|
|
|
}
|
|
|
|
|
|
- return Math.max(MAX_RESPONSE_BUFFER_BYTES, frameLimit + 8, responseLength + 8)
|
|
|
-}
|
|
|
-
|
|
|
-function getCommand(index) {
|
|
|
- return MODBUS_COMMANDS[index] || MODBUS_COMMANDS[0]
|
|
|
-}
|
|
|
-
|
|
|
-function getDefaultCommandValue(command) {
|
|
|
- if (command.inputMode === 'quantity') return '0001'
|
|
|
- if (command.inputMode === 'coil') return 'ON'
|
|
|
- if (command.inputMode === 'multiple') return '0000'
|
|
|
+ ensureProtocolHelpers()
|
|
|
|
|
|
- return '0000'
|
|
|
-}
|
|
|
-
|
|
|
-function generateModbusFrame(command, slaveAddress, registerAddress, commandValue, coilEnabled) {
|
|
|
- const slave = parseHexNumber(slaveAddress, '从站地址', 0xFF)
|
|
|
- const address = parseHexNumber(registerAddress, '协议寄存器', 0xFFFF)
|
|
|
-
|
|
|
- if (command.inputMode === 'quantity') {
|
|
|
- const quantity = parseHexNumber(commandValue, '读取数量', 0xFFFF)
|
|
|
- return buildReadFrame(slave, command.functionCode, address, quantity)
|
|
|
- }
|
|
|
- if (command.inputMode === 'coil') {
|
|
|
- return buildWriteSingleCoilFrame(slave, address, coilEnabled)
|
|
|
- }
|
|
|
- if (command.inputMode === 'single') {
|
|
|
- return buildWriteSingleRegisterFrame(slave, address, parseHexNumber(commandValue, '写入值', 0xFFFF))
|
|
|
+ if (typeof protocolHelpers.getResponseBufferHint === 'function') {
|
|
|
+ return Math.max(0, Math.round(Number(protocolHelpers.getResponseBufferHint(expected)) || 0))
|
|
|
}
|
|
|
|
|
|
- return buildWriteMultipleRegistersFrame(slave, address, parseRegisterValues(commandValue))
|
|
|
+ return 0
|
|
|
}
|
|
|
|
|
|
-function createProtocolState(commandIndex, slaveAddress, registerAddress, commandValue, coilEnabled) {
|
|
|
- const command = getCommand(commandIndex)
|
|
|
- const commandValueLabel = command.inputMode === 'quantity' ? '读取数量' : '写入值'
|
|
|
+function getResponseBufferLimit(expected, options = {}) {
|
|
|
+ const responseLength = getResponseBufferHint(expected, options)
|
|
|
+ const maxFrameBytes = options.maxFrameBytes
|
|
|
+ const frameLimit = normalizeMaxFrameBytes(maxFrameBytes)
|
|
|
|
|
|
- try {
|
|
|
- return {
|
|
|
- commandValueLabel,
|
|
|
- generatedHex: formatHex(generateModbusFrame(command, slaveAddress, registerAddress, commandValue, coilEnabled)),
|
|
|
- protocolErrorText: '',
|
|
|
- showCoilValue: command.inputMode === 'coil',
|
|
|
- showCommandValue: command.inputMode !== 'coil'
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- return {
|
|
|
- commandValueLabel,
|
|
|
- generatedHex: '',
|
|
|
- protocolErrorText: error.message,
|
|
|
- showCoilValue: command.inputMode === 'coil',
|
|
|
- showCommandValue: command.inputMode !== 'coil'
|
|
|
- }
|
|
|
+ if (frameLimit === 0) {
|
|
|
+ return Math.max(MAX_RESPONSE_BUFFER_BYTES, responseLength + 8)
|
|
|
}
|
|
|
+
|
|
|
+ return Math.max(MAX_RESPONSE_BUFFER_BYTES, frameLimit + 8, responseLength + 8)
|
|
|
}
|
|
|
|
|
|
function hexToArrayBuffer(hexText) {
|
|
|
@@ -417,105 +332,21 @@ function arrayBufferToHex(buffer) {
|
|
|
return Array.prototype.map.call(new Uint8Array(buffer), (item) => item.toString(16).padStart(2, '0')).join(' ').toUpperCase()
|
|
|
}
|
|
|
|
|
|
-function parseModbusResponse(bytes) {
|
|
|
- if (!Array.isArray(bytes) || bytes.length < 5 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null
|
|
|
-
|
|
|
- const slaveAddress = bytes[0]
|
|
|
- const functionCode = bytes[1]
|
|
|
-
|
|
|
- if (functionCode & 0x80) {
|
|
|
- return {
|
|
|
- exceptionCode: bytes[2],
|
|
|
- functionCode,
|
|
|
- isException: true,
|
|
|
- slaveAddress,
|
|
|
- sourceFunctionCode: functionCode & 0x7F
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (functionCode === 0x01 || functionCode === 0x02) {
|
|
|
- const byteCount = bytes[2]
|
|
|
- const dataEnd = 3 + byteCount
|
|
|
- if (bytes.length < dataEnd + 2) return null
|
|
|
-
|
|
|
- return {
|
|
|
- byteCount,
|
|
|
- dataBytes: bytes.slice(3, dataEnd),
|
|
|
- functionCode,
|
|
|
- isException: false,
|
|
|
- slaveAddress
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (functionCode === 0x03 || functionCode === 0x04) {
|
|
|
- const byteCount = bytes[2]
|
|
|
- const dataEnd = 3 + byteCount
|
|
|
- if (bytes.length < dataEnd + 2) return null
|
|
|
-
|
|
|
- return {
|
|
|
- byteCount,
|
|
|
- dataBytes: bytes.slice(3, dataEnd),
|
|
|
- functionCode,
|
|
|
- isException: false,
|
|
|
- slaveAddress,
|
|
|
- words: bytesToWords(bytes.slice(3, dataEnd))
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (functionCode === 0x05 || functionCode === 0x06 || functionCode === 0x10) {
|
|
|
- return {
|
|
|
- address: ((bytes[2] << 8) | bytes[3]) & 0xFFFF,
|
|
|
- functionCode,
|
|
|
- isException: false,
|
|
|
- quantityOrValue: ((bytes[4] << 8) | bytes[5]) & 0xFFFF,
|
|
|
- slaveAddress
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- functionCode,
|
|
|
- isException: false,
|
|
|
- slaveAddress
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function parseModbusRequest(bytes) {
|
|
|
- if (!Array.isArray(bytes) || bytes.length < 6 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null
|
|
|
-
|
|
|
- const slaveAddress = bytes[0]
|
|
|
- const functionCode = bytes[1]
|
|
|
- const address = ((bytes[2] << 8) | bytes[3]) & 0xFFFF
|
|
|
- let quantity = 1
|
|
|
- let value
|
|
|
-
|
|
|
- if (functionCode === 0x01 || functionCode === 0x02 || functionCode === 0x03 || functionCode === 0x04 || functionCode === 0x10) {
|
|
|
- quantity = ((bytes[4] << 8) | bytes[5]) & 0xFFFF
|
|
|
- }
|
|
|
- if (functionCode === 0x05 || functionCode === 0x06) {
|
|
|
- value = ((bytes[4] << 8) | bytes[5]) & 0xFFFF
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- address,
|
|
|
- functionCode,
|
|
|
- kind: 'raw-hex',
|
|
|
- quantity,
|
|
|
- value,
|
|
|
- slaveAddress
|
|
|
- }
|
|
|
+function formatFrameHex(bytes) {
|
|
|
+ return Array.prototype.map.call(bytes || [], (item) => Number(item || 0).toString(16).padStart(2, '0')).join(' ').toUpperCase()
|
|
|
}
|
|
|
|
|
|
-function validateDmaFrameLength(bytes, expected) {
|
|
|
- const maxFrameBytes = normalizeMaxFrameBytes(expected && expected.maxFrameBytes)
|
|
|
+function validateDmaFrameLength(bytes, options = {}) {
|
|
|
+ const maxFrameBytes = normalizeMaxFrameBytes(options.maxFrameBytes)
|
|
|
if (maxFrameBytes === 0) return ''
|
|
|
|
|
|
if (bytes.length > maxFrameBytes) {
|
|
|
return `发送帧长度 ${bytes.length} 字节,超过最大包长 ${maxFrameBytes} 字节限制`
|
|
|
}
|
|
|
|
|
|
- if (!expected) return ''
|
|
|
+ if (!options.expected) return ''
|
|
|
|
|
|
- const responseLength = getReadResponseByteLength(expected.functionCode, expected.quantity)
|
|
|
+ const responseLength = getResponseBufferHint(options.expected, options)
|
|
|
|
|
|
if (responseLength > maxFrameBytes) {
|
|
|
return `预计返回帧长度 ${responseLength} 字节,超过最大包长 ${maxFrameBytes} 字节限制`
|
|
|
@@ -554,10 +385,6 @@ function hasTargetCharacteristic(discovery) {
|
|
|
))
|
|
|
}
|
|
|
|
|
|
-function getExceptionText(code) {
|
|
|
- return MODBUS_EXCEPTION_MESSAGES[code] || '未知异常'
|
|
|
-}
|
|
|
-
|
|
|
function addLog(direction, payload, note = '') {
|
|
|
logSequence += 1
|
|
|
const logItem = {
|
|
|
@@ -578,14 +405,17 @@ function addLog(direction, payload, note = '') {
|
|
|
function getReceiveCrcState(rawBytes) {
|
|
|
if (!rawBytes || rawBytes.length < 4) return ''
|
|
|
|
|
|
- if (isBootloaderFrame(rawBytes)) {
|
|
|
- const expectedLength = getBootloaderResponseLength(rawBytes)
|
|
|
- if (expectedLength && rawBytes.length < expectedLength) return '片段'
|
|
|
+ ensureProtocolHelpers()
|
|
|
+
|
|
|
+ if (typeof protocolHelpers.inspectReceivedBytes === 'function') {
|
|
|
+ const note = protocolHelpers.inspectReceivedBytes(rawBytes, {
|
|
|
+ pendingRequest: !!pendingRequest
|
|
|
+ })
|
|
|
|
|
|
- return hasValidCrc16Ccitt(rawBytes, { byteOrder: BYTE_ORDER_HIGH }) ? 'CRC OK' : 'CRC ERR'
|
|
|
+ if (note) return note
|
|
|
}
|
|
|
|
|
|
- return hasValidCrc16Modbus(rawBytes, MODBUS_CRC_OPTIONS) ? 'CRC OK' : (pendingRequest ? '片段' : 'CRC ERR')
|
|
|
+ return pendingRequest ? '片段' : ''
|
|
|
}
|
|
|
|
|
|
function showCommandAlert(title, content) {
|
|
|
@@ -829,76 +659,6 @@ function isConnectionLostError(error) {
|
|
|
return message.includes('disconnect') || message.includes('not connected')
|
|
|
}
|
|
|
|
|
|
-function isExpectedResponse(response, expected) {
|
|
|
- if (response.functionCode === 0x01 || response.functionCode === 0x02) {
|
|
|
- return Array.isArray(response.dataBytes) && response.dataBytes.length >= Math.ceil(expected.quantity / 8)
|
|
|
- }
|
|
|
-
|
|
|
- if (response.functionCode === 0x03 || response.functionCode === 0x04) {
|
|
|
- return Array.isArray(response.words) && response.words.length >= expected.quantity
|
|
|
- }
|
|
|
-
|
|
|
- if (response.functionCode === 0x10) {
|
|
|
- return response.address === expected.address && response.quantityOrValue === expected.quantity
|
|
|
- }
|
|
|
-
|
|
|
- if (response.functionCode === 0x05 || response.functionCode === 0x06) {
|
|
|
- if (response.address !== expected.address) return false
|
|
|
- if (Number.isInteger(expected.value)) return response.quantityOrValue === expected.value
|
|
|
-
|
|
|
- return true
|
|
|
- }
|
|
|
-
|
|
|
- return true
|
|
|
-}
|
|
|
-
|
|
|
-function getExpectedResponseLength(expected, responseFunctionCode, responseBytes) {
|
|
|
- if (!expected) return 0
|
|
|
-
|
|
|
- if (responseFunctionCode === (expected.functionCode | 0x80)) {
|
|
|
- return 5
|
|
|
- }
|
|
|
-
|
|
|
- if (responseFunctionCode === 0x01 || responseFunctionCode === 0x02) {
|
|
|
- if (responseBytes.length < 3) return 0
|
|
|
-
|
|
|
- return 3 + Number(responseBytes[2] || 0) + 2
|
|
|
- }
|
|
|
-
|
|
|
- if (responseFunctionCode === 0x03 || responseFunctionCode === 0x04) {
|
|
|
- if (responseBytes.length < 3) return 0
|
|
|
-
|
|
|
- return 3 + Number(responseBytes[2] || 0) + 2
|
|
|
- }
|
|
|
-
|
|
|
- if (responseFunctionCode === 0x05 || responseFunctionCode === 0x06 || responseFunctionCode === 0x10) {
|
|
|
- return 8
|
|
|
- }
|
|
|
-
|
|
|
- return 0
|
|
|
-}
|
|
|
-
|
|
|
-function alignResponseBuffer(buffer, expected) {
|
|
|
- if (!Array.isArray(buffer) || !buffer.length || !expected) return
|
|
|
-
|
|
|
- const expectedFunctionCodes = [expected.functionCode, expected.functionCode | 0x80]
|
|
|
- let matchIndex = -1
|
|
|
-
|
|
|
- for (let index = 0; index < buffer.length - 1; index += 1) {
|
|
|
- if (buffer[index] !== expected.slaveAddress) continue
|
|
|
- if (!expectedFunctionCodes.includes(buffer[index + 1])) continue
|
|
|
-
|
|
|
- matchIndex = index
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- if (matchIndex > 0) {
|
|
|
- buffer.splice(0, matchIndex)
|
|
|
- } else if (matchIndex < 0 && buffer.length > 2) {
|
|
|
- buffer.splice(0, buffer.length - 1)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
function finishPendingRequest(resolveValue) {
|
|
|
const pending = clearPendingRequest()
|
|
|
|
|
|
@@ -911,19 +671,11 @@ function consumePendingResponseBuffer() {
|
|
|
const pending = pendingRequest
|
|
|
if (!pending || !Array.isArray(pending.responseBuffer)) return
|
|
|
|
|
|
- const buffer = pending.responseBuffer
|
|
|
- alignResponseBuffer(buffer, pending.expected)
|
|
|
-
|
|
|
- if (buffer.length < 2) return
|
|
|
-
|
|
|
- const responseFunctionCode = buffer[1]
|
|
|
- const responseLength = getExpectedResponseLength(pending.expected, responseFunctionCode, buffer)
|
|
|
+ ensureProtocolHelpers()
|
|
|
|
|
|
- if (!responseLength) return
|
|
|
-
|
|
|
- const frameLimit = normalizeMaxFrameBytes(pending.expected && pending.expected.maxFrameBytes)
|
|
|
- if (frameLimit > 0 && responseLength > frameLimit) {
|
|
|
- const content = `${pending.label} 返回帧长度 ${responseLength} 字节,超过最大包长 ${frameLimit} 字节限制,已丢弃`
|
|
|
+ const responseReader = pending.responseReader || protocolHelpers.readResponseFromBuffer
|
|
|
+ if (typeof responseReader !== 'function') {
|
|
|
+ const content = `${pending.label} 未配置响应解析器,已丢弃`
|
|
|
addLog('SYS', content)
|
|
|
finishPendingRequest(false)
|
|
|
if (pending.showModal) {
|
|
|
@@ -932,12 +684,13 @@ function consumePendingResponseBuffer() {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- if (buffer.length < responseLength) return
|
|
|
+ const result = responseReader(pending.responseBuffer, pending.expected, {
|
|
|
+ maxFrameBytes: pending.expected && pending.expected.maxFrameBytes
|
|
|
+ })
|
|
|
+ if (!result || result.status === 'pending') return
|
|
|
|
|
|
- const frameBytes = buffer.slice(0, responseLength)
|
|
|
- const response = parseModbusResponse(frameBytes)
|
|
|
- if (!response) {
|
|
|
- const content = `${pending.label} 收到无效响应帧,已丢弃`
|
|
|
+ if (result.status === 'frame-too-long') {
|
|
|
+ const content = `${pending.label} 返回帧长度 ${result.responseLength} 字节,超过最大包长 ${result.frameLimit} 字节限制,已丢弃`
|
|
|
addLog('SYS', content)
|
|
|
finishPendingRequest(false)
|
|
|
if (pending.showModal) {
|
|
|
@@ -946,17 +699,18 @@ function consumePendingResponseBuffer() {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- const responseCode = response.isException ? response.sourceFunctionCode : response.functionCode
|
|
|
- if (response.slaveAddress !== pending.expected.slaveAddress || responseCode !== pending.expected.functionCode) {
|
|
|
- buffer.shift()
|
|
|
- consumePendingResponseBuffer()
|
|
|
+ if (result.status === 'invalid') {
|
|
|
+ const content = `${pending.label} 收到无效响应帧,已丢弃`
|
|
|
+ addLog('SYS', content)
|
|
|
+ finishPendingRequest(false)
|
|
|
+ if (pending.showModal) {
|
|
|
+ showCommandAlert('通讯异常', content)
|
|
|
+ }
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- if (response.isException) {
|
|
|
- const exceptionText = getExceptionText(response.exceptionCode)
|
|
|
- const content = `设备返回异常帧:功能码 0x${padHex(response.sourceFunctionCode, 2)},异常码 0x${padHex(response.exceptionCode, 2)}(${exceptionText})`
|
|
|
-
|
|
|
+ if (result.status === 'exception') {
|
|
|
+ const content = result.message || `${pending.label} 收到异常响应帧`
|
|
|
addLog('SYS', content)
|
|
|
finishPendingRequest(false)
|
|
|
if (pending.showModal) {
|
|
|
@@ -965,7 +719,7 @@ function consumePendingResponseBuffer() {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- if (!isExpectedResponse(response, pending.expected)) {
|
|
|
+ if (result.status === 'mismatch') {
|
|
|
const content = `${pending.label} 收到不匹配响应,已丢弃`
|
|
|
addLog('SYS', content)
|
|
|
finishPendingRequest(false)
|
|
|
@@ -975,10 +729,19 @@ function consumePendingResponseBuffer() {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- buffer.splice(0, responseLength)
|
|
|
- finishPendingRequest(response)
|
|
|
+ if (result.status !== 'complete') {
|
|
|
+ const content = `${pending.label} 收到未知响应状态,已丢弃`
|
|
|
+ addLog('SYS', content)
|
|
|
+ finishPendingRequest(false)
|
|
|
+ if (pending.showModal) {
|
|
|
+ showCommandAlert('通讯异常', content)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- if (buffer.length) {
|
|
|
+ finishPendingRequest(result.response)
|
|
|
+
|
|
|
+ if (pending.responseBuffer.length) {
|
|
|
consumePendingResponseBuffer()
|
|
|
}
|
|
|
}
|
|
|
@@ -1021,7 +784,8 @@ function createPendingRequest(label, expected, options = {}) {
|
|
|
label,
|
|
|
resolve,
|
|
|
timer,
|
|
|
- responseBufferLimit: getResponseBufferLimit(expected, options.maxFrameBytes),
|
|
|
+ responseBufferLimit: getResponseBufferLimit(expected, options),
|
|
|
+ responseReader: typeof options.responseReader === 'function' ? options.responseReader : null,
|
|
|
showModal: options.showModal !== false,
|
|
|
responseBuffer: []
|
|
|
}
|
|
|
@@ -1503,6 +1267,8 @@ function handleAppHide() {
|
|
|
}
|
|
|
|
|
|
async function handleAppShow() {
|
|
|
+ if (!state.connectedDevice) return
|
|
|
+
|
|
|
init()
|
|
|
const connected = await refreshNativeConnectionState()
|
|
|
if (connected && state.connectedDevice) {
|
|
|
@@ -1517,75 +1283,6 @@ function setSendHex(sendHex) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-function setCommandIndex(value) {
|
|
|
- const commandIndex = Number(value)
|
|
|
- const command = getCommand(commandIndex)
|
|
|
- const commandValue = getDefaultCommandValue(command)
|
|
|
- const nextState = {
|
|
|
- commandIndex,
|
|
|
- commandValue,
|
|
|
- coilEnabled: true
|
|
|
- }
|
|
|
-
|
|
|
- setState({
|
|
|
- ...nextState,
|
|
|
- ...createProtocolState(
|
|
|
- nextState.commandIndex,
|
|
|
- state.slaveAddress,
|
|
|
- state.registerAddress,
|
|
|
- nextState.commandValue,
|
|
|
- nextState.coilEnabled
|
|
|
- )
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function setProtocolInput(changedData) {
|
|
|
- const nextState = {
|
|
|
- commandIndex: state.commandIndex,
|
|
|
- slaveAddress: state.slaveAddress,
|
|
|
- registerAddress: state.registerAddress,
|
|
|
- commandValue: state.commandValue,
|
|
|
- coilEnabled: state.coilEnabled,
|
|
|
- ...changedData
|
|
|
- }
|
|
|
-
|
|
|
- setState({
|
|
|
- ...changedData,
|
|
|
- ...createProtocolState(
|
|
|
- nextState.commandIndex,
|
|
|
- nextState.slaveAddress,
|
|
|
- nextState.registerAddress,
|
|
|
- nextState.commandValue,
|
|
|
- nextState.coilEnabled
|
|
|
- )
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function buildGeneratedExpectedResponse() {
|
|
|
- try {
|
|
|
- const command = getCommand(state.commandIndex)
|
|
|
- const address = parseHexNumber(state.registerAddress, '协议寄存器', 0xFFFF)
|
|
|
- const slaveAddress = parseHexNumber(state.slaveAddress, '从站地址', 0xFF)
|
|
|
- const quantity = command.inputMode === 'quantity'
|
|
|
- ? parseHexNumber(state.commandValue, '读取数量', 0xFFFF)
|
|
|
- : (command.inputMode === 'multiple' ? parseRegisterValues(state.commandValue).length : 1)
|
|
|
- const value = command.inputMode === 'coil'
|
|
|
- ? (state.coilEnabled ? 0xFF00 : 0x0000)
|
|
|
- : (command.inputMode === 'single' ? parseHexNumber(state.commandValue, '写入值', 0xFFFF) : undefined)
|
|
|
-
|
|
|
- return {
|
|
|
- address,
|
|
|
- functionCode: command.functionCode,
|
|
|
- kind: 'manual-rtu',
|
|
|
- quantity,
|
|
|
- value,
|
|
|
- slaveAddress
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- return null
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
function clearInput() {
|
|
|
setState({
|
|
|
sendHex: '',
|
|
|
@@ -1628,7 +1325,7 @@ function enqueueSendFrame(hexFrame, source, options = {}) {
|
|
|
|
|
|
const buffer = hexToArrayBuffer(hexFrame)
|
|
|
const bytes = new Uint8Array(buffer)
|
|
|
- const dmaFrameLengthError = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options.expected)
|
|
|
+ const dmaFrameLengthError = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options)
|
|
|
|
|
|
if (dmaFrameLengthError) {
|
|
|
setState({
|
|
|
@@ -1724,7 +1421,7 @@ async function executeSendFrame(hexFrame, source, options = {}) {
|
|
|
|
|
|
const buffer = hexToArrayBuffer(hexFrame)
|
|
|
const bytes = new Uint8Array(buffer)
|
|
|
- const dmaFrameLengthError = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options.expected)
|
|
|
+ const dmaFrameLengthError = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options)
|
|
|
|
|
|
if (dmaFrameLengthError) {
|
|
|
setState({
|
|
|
@@ -1792,14 +1489,16 @@ async function executeSendFrame(hexFrame, source, options = {}) {
|
|
|
}
|
|
|
|
|
|
function sendManagedFrame(frameBytes, label, expected, options = {}) {
|
|
|
- return enqueueSendFrame(formatHex(frameBytes), label, {
|
|
|
+ return enqueueSendFrame(formatFrameHex(frameBytes), label, {
|
|
|
expected: expected ? {
|
|
|
...expected,
|
|
|
- maxFrameBytes: options.maxFrameBytes === undefined ? MAX_MODBUS_DMA_BYTES : options.maxFrameBytes
|
|
|
+ maxFrameBytes: options.maxFrameBytes === undefined ? DEFAULT_MAX_FRAME_BYTES : options.maxFrameBytes
|
|
|
} : expected,
|
|
|
+ responseBufferHint: options.responseBufferHint,
|
|
|
+ responseReader: options.responseReader,
|
|
|
showModal: options.showModal !== false,
|
|
|
timeout: options.timeout || RESPONSE_TIMEOUT,
|
|
|
- maxFrameBytes: options.maxFrameBytes === undefined ? MAX_MODBUS_DMA_BYTES : options.maxFrameBytes
|
|
|
+ maxFrameBytes: options.maxFrameBytes === undefined ? DEFAULT_MAX_FRAME_BYTES : options.maxFrameBytes
|
|
|
})
|
|
|
}
|
|
|
|
|
|
@@ -1808,7 +1507,7 @@ function sendRawFrameExact(frameBytes, source) {
|
|
|
? frameBytes
|
|
|
: new Uint8Array(frameBytes || [])
|
|
|
|
|
|
- return enqueueSendFrame(formatHex(Array.prototype.slice.call(bytes)), source, {
|
|
|
+ return enqueueSendFrame(formatFrameHex(Array.prototype.slice.call(bytes)), source, {
|
|
|
chunkSize: 0,
|
|
|
skipDmaCheck: true
|
|
|
})
|
|
|
@@ -1816,47 +1515,33 @@ function sendRawFrameExact(frameBytes, source) {
|
|
|
|
|
|
function sendHexFrame() {
|
|
|
const errorText = validateHex(state.sendHex)
|
|
|
- const expected = errorText ? null : parseModbusRequest(Array.prototype.slice.call(new Uint8Array(hexToArrayBuffer(state.sendHex))))
|
|
|
-
|
|
|
- return enqueueSendFrame(state.sendHex, 'HEX', expected ? {
|
|
|
- expected
|
|
|
- } : {})
|
|
|
-}
|
|
|
|
|
|
-function sendGeneratedFrame() {
|
|
|
- if (!state.generatedHex) return false
|
|
|
+ ensureProtocolHelpers()
|
|
|
|
|
|
- const expected = buildGeneratedExpectedResponse()
|
|
|
+ const expected = errorText || typeof protocolHelpers.parseSendExpected !== 'function'
|
|
|
+ ? null
|
|
|
+ : protocolHelpers.parseSendExpected(Array.prototype.slice.call(new Uint8Array(hexToArrayBuffer(state.sendHex))))
|
|
|
|
|
|
- return enqueueSendFrame(state.generatedHex, 'RTU', expected ? {
|
|
|
+ return enqueueSendFrame(state.sendHex, 'HEX', expected ? {
|
|
|
expected
|
|
|
} : {})
|
|
|
}
|
|
|
|
|
|
-setState(createProtocolState(
|
|
|
- state.commandIndex,
|
|
|
- state.slaveAddress,
|
|
|
- state.registerAddress,
|
|
|
- state.commandValue,
|
|
|
- state.coilEnabled
|
|
|
-))
|
|
|
-
|
|
|
module.exports = {
|
|
|
clearDevices,
|
|
|
clearInput,
|
|
|
clearLogs,
|
|
|
+ configureProtocolHelpers,
|
|
|
connectDeviceById,
|
|
|
disconnectDevice,
|
|
|
+ enqueueSendFrame,
|
|
|
getState,
|
|
|
handleAppHide,
|
|
|
handleAppShow,
|
|
|
init,
|
|
|
- sendGeneratedFrame,
|
|
|
sendHexFrame,
|
|
|
sendManagedFrame,
|
|
|
sendRawFrameExact,
|
|
|
- setCommandIndex,
|
|
|
- setProtocolInput,
|
|
|
setSendHex,
|
|
|
showCommandAlert,
|
|
|
startScan,
|