| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704 |
- const {
- padHex
- } = require('../../utils/base-utils.js')
- const {
- bytesToWords
- } = require('../../utils/binary-utils.js')
- const {
- BYTE_ORDER_LOW,
- appendCrc16Modbus,
- hasValidCrc16Modbus
- } = require('../../utils/crc.js')
- const settingsService = require('../../store/settings-store.js')
- const transport = require('../../transport/ble-core.js')
- const {
- addCoilReadValues,
- addWordReadValues
- } = require('../../utils/register-value-utils.js')
- const PROTOCOL_NAME = 'modbus-rtu'
- const MODBUS_CRC_OPTIONS = {
- byteOrder: BYTE_ORDER_LOW
- }
- const MAX_MODBUS_DMA_BYTES = 64
- const MODBUS_READ_RESPONSE_OVERHEAD = 5
- const MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD = 9
- const MAX_READ_REGISTER_QUANTITY = Math.floor((MAX_MODBUS_DMA_BYTES - MODBUS_READ_RESPONSE_OVERHEAD) / 2)
- const MAX_READ_COIL_QUANTITY = (MAX_MODBUS_DMA_BYTES - MODBUS_READ_RESPONSE_OVERHEAD) * 8
- const MAX_WRITE_MULTIPLE_REGISTER_QUANTITY = Math.floor((MAX_MODBUS_DMA_BYTES - MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD) / 2)
- const UNLIMITED_FRAME_BYTES = 0
- const MODBUS_EXCEPTION_MESSAGES = {
- 0x01: '非法功能',
- 0x02: '非法数据地址',
- 0x03: '非法数据值',
- 0x04: '从站设备故障',
- 0x05: '确认',
- 0x06: '从站设备忙',
- 0x08: '存储奇偶性错误',
- 0x0A: '网关路径不可用',
- 0x0B: '网关目标设备响应失败'
- }
- 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 splitWord(value) {
- return [(value >> 8) & 0xFF, value & 0xFF]
- }
- function normalizeMaxFrameBytes(maxFrameBytes = MAX_MODBUS_DMA_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 MAX_MODBUS_DMA_BYTES
- }
- function getMaxReadQuantity(functionCode, maxFrameBytes = MAX_MODBUS_DMA_BYTES) {
- const frameBytes = normalizeMaxFrameBytes(maxFrameBytes)
- if (frameBytes === UNLIMITED_FRAME_BYTES) return 0xFFFF
- const dataBytes = frameBytes - MODBUS_READ_RESPONSE_OVERHEAD
- if (dataBytes <= 0) return 0
- if (functionCode === 0x01 || functionCode === 0x02) return dataBytes * 8
- if (functionCode === 0x03 || functionCode === 0x04) return Math.floor(dataBytes / 2)
- return 0
- }
- function getMaxWriteMultipleRegisterQuantity(maxFrameBytes = MAX_MODBUS_DMA_BYTES) {
- const frameBytes = normalizeMaxFrameBytes(maxFrameBytes)
- if (frameBytes === UNLIMITED_FRAME_BYTES) return 0xFFFF
- return Math.max(0, Math.floor((frameBytes - MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD) / 2))
- }
- function buildReadFrame(slaveAddress, functionCode, address, quantity, options = {}) {
- const slave = toByte(slaveAddress, '从站地址')
- const command = toByte(functionCode, '功能码')
- const startAddress = toWord(address, '寄存器地址')
- const registerQuantity = toWord(quantity, '读取数量')
- const maxQuantity = getMaxReadQuantity(command, options.maxFrameBytes)
- if ([0x01, 0x02, 0x03, 0x04].indexOf(command) < 0) {
- throw new Error('当前功能码不是读取命令')
- }
- if (registerQuantity === 0) {
- throw new Error('读取数量必须大于 0')
- }
- if ([0x03, 0x04].indexOf(command) >= 0 && maxQuantity > 0 && registerQuantity > maxQuantity) {
- throw new Error(`单帧最多读取 ${maxQuantity} 个寄存器`)
- }
- if ((command === 0x01 || command === 0x02) && maxQuantity > 0 && registerQuantity > maxQuantity) {
- throw new Error(`单帧最多读取 ${maxQuantity} 个位状态`)
- }
- return appendCrc16Modbus(
- [slave, command].concat(splitWord(startAddress), splitWord(registerQuantity)),
- MODBUS_CRC_OPTIONS
- )
- }
- function buildWriteSingleCoilFrame(slaveAddress, address, checked) {
- const slave = toByte(slaveAddress, '从站地址')
- const startAddress = toWord(address, '线圈地址')
- const outputValue = checked ? 0xFF00 : 0x0000
- return appendCrc16Modbus(
- [slave, 0x05].concat(splitWord(startAddress), splitWord(outputValue)),
- MODBUS_CRC_OPTIONS
- )
- }
- function buildWriteSingleRegisterFrame(slaveAddress, address, value) {
- const slave = toByte(slaveAddress, '从站地址')
- const startAddress = toWord(address, '寄存器地址')
- const registerValue = toWord(value, '写入值')
- return appendCrc16Modbus(
- [slave, 0x06].concat(splitWord(startAddress), splitWord(registerValue)),
- MODBUS_CRC_OPTIONS
- )
- }
- function buildWriteMultipleRegistersFrame(slaveAddress, address, values, options = {}) {
- const slave = toByte(slaveAddress, '从站地址')
- const startAddress = toWord(address, '寄存器地址')
- const maxQuantity = getMaxWriteMultipleRegisterQuantity(options.maxFrameBytes)
- if (!Array.isArray(values) || values.length === 0) {
- throw new Error('请输入至少一个寄存器写入值')
- }
- if (maxQuantity > 0 && values.length > maxQuantity) {
- throw new Error(`单帧最多写入 ${maxQuantity} 个寄存器`)
- }
- const registerBytes = values.reduce((result, value) => {
- return result.concat(splitWord(toWord(value, '写入值')))
- }, [])
- return appendCrc16Modbus(
- [slave, 0x10]
- .concat(splitWord(startAddress), splitWord(values.length), [registerBytes.length], registerBytes),
- MODBUS_CRC_OPTIONS
- )
- }
- function formatHex(bytes) {
- return bytes.map((byte) => byte.toString(16).padStart(2, '0').toUpperCase()).join(' ')
- }
- function getReadResponseByteLength(functionCode, quantity) {
- if (functionCode === 0x01 || functionCode === 0x02) return MODBUS_READ_RESPONSE_OVERHEAD + Math.ceil(Number(quantity || 0) / 8)
- if (functionCode === 0x03 || functionCode === 0x04) return MODBUS_READ_RESPONSE_OVERHEAD + Number(quantity || 0) * 2
- return 0
- }
- 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,
- protocol: PROTOCOL_NAME,
- 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,
- protocol: PROTOCOL_NAME,
- 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,
- protocol: PROTOCOL_NAME,
- 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,
- protocol: PROTOCOL_NAME,
- quantityOrValue: ((bytes[4] << 8) | bytes[5]) & 0xFFFF,
- slaveAddress
- }
- }
- return {
- functionCode,
- isException: false,
- protocol: PROTOCOL_NAME,
- slaveAddress
- }
- }
- function parseModbusRequest(bytes) {
- if (!Array.isArray(bytes) || bytes.length < 4 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null
- const slaveAddress = bytes[0]
- const functionCode = bytes[1]
- if (bytes.length < 6) return null
- 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',
- protocol: PROTOCOL_NAME,
- quantity,
- value,
- slaveAddress
- }
- }
- 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 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 getExceptionText(code) {
- return MODBUS_EXCEPTION_MESSAGES[code] || '未知异常'
- }
- function formatExceptionMessage(response) {
- const sourceFunctionCode = response && response.sourceFunctionCode
- const exceptionCode = response && response.exceptionCode
- const exceptionText = getExceptionText(exceptionCode)
- return `设备返回异常帧:功能码 0x${padHex(sourceFunctionCode, 2)},异常码 0x${padHex(exceptionCode, 2)}(${exceptionText})`
- }
- function getReadBufferHint(expected) {
- return expected ? getReadResponseByteLength(expected.functionCode, expected.quantity) : 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.indexOf(buffer[index + 1]) < 0) 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 readResponseFromBuffer(buffer, expected, options = {}) {
- if (!Array.isArray(buffer) || !buffer.length || !expected) {
- return {
- status: 'pending'
- }
- }
- alignResponseBuffer(buffer, expected)
- while (buffer.length >= 2) {
- const responseFunctionCode = buffer[1]
- const responseLength = getExpectedResponseLength(expected, responseFunctionCode, 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 = parseModbusResponse(frameBytes)
- if (!response) {
- return {
- frameBytes,
- status: 'invalid'
- }
- }
- const responseCode = response.isException ? response.sourceFunctionCode : response.functionCode
- if (response.slaveAddress !== expected.slaveAddress || responseCode !== expected.functionCode) {
- buffer.shift()
- alignResponseBuffer(buffer, expected)
- continue
- }
- if (response.isException) {
- return {
- message: formatExceptionMessage(response),
- response,
- status: 'exception'
- }
- }
- if (!isExpectedResponse(response, expected)) {
- return {
- response,
- status: 'mismatch'
- }
- }
- buffer.splice(0, responseLength)
- return {
- response,
- status: 'complete'
- }
- }
- return {
- status: 'pending'
- }
- }
- function getSharedSlaveAddress(title = '从机地址错误') {
- try {
- return settingsService.getModbusSlaveAddress()
- } catch (error) {
- transport.showCommandAlert(title, error.message)
- return null
- }
- }
- function formatAddress(value) {
- return Number(value || 0).toString(16).toUpperCase()
- }
- function getChunkLabel(label, chunks, chunk) {
- if (!label || chunks.length <= 1) return label
- return `${label} ${formatAddress(chunk.address)}-${formatAddress(chunk.address + chunk.quantity - 1)}`
- }
- function isBroadcastAddress(slaveAddress) {
- return Number(slaveAddress) === 0
- }
- function showBroadcastReadAlert(label) {
- transport.showCommandAlert(label || '读取命令错误', '广播地址 0x00 不支持读取')
- }
- 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(functionCode, startAddress, quantity, options = {}) {
- const maxQuantity = getMaxReadQuantity(functionCode, options.maxFrameBytes)
- return splitQuantity(startAddress, quantity, maxQuantity || quantity)
- }
- async function sendReadChunk(slaveAddress, functionCode, chunk, label, kind, options = {}) {
- if (isBroadcastAddress(slaveAddress)) {
- showBroadcastReadAlert(label)
- return false
- }
- return transport.sendManagedFrame(
- buildReadFrame(slaveAddress, functionCode, chunk.address, chunk.quantity, {
- maxFrameBytes: options.maxFrameBytes
- }),
- label,
- {
- address: chunk.address,
- functionCode,
- kind,
- protocol: PROTOCOL_NAME,
- quantity: chunk.quantity,
- slaveAddress
- },
- {
- maxFrameBytes: options.maxFrameBytes,
- showModal: options.showModal
- }
- )
- }
- async function readSpans(slaveAddress, functionCode, spans, label, kind, options = {}) {
- const readValues = {
- coils: {},
- words: {}
- }
- const normalizedSpans = (spans || []).filter((span) => span && span.quantity > 0)
- for (const span of normalizedSpans) {
- const chunks = getReadChunks(functionCode, span.address, span.quantity, options)
- for (const chunk of chunks) {
- const responseValue = await sendReadChunk(
- slaveAddress,
- functionCode,
- chunk,
- getChunkLabel(label, chunks, chunk),
- kind,
- options
- )
- if (!responseValue) return null
- if (functionCode === 0x01 || functionCode === 0x02) {
- addCoilReadValues(readValues, chunk.address, chunk.quantity, responseValue)
- } else {
- addWordReadValues(readValues, chunk.address, responseValue)
- }
- if (typeof options.onChunk === 'function') {
- options.onChunk(responseValue, chunk)
- }
- }
- }
- return readValues
- }
- async function readRegisterWords(slaveAddress, functionCode, startAddress, quantity, label, kind, options = {}) {
- const words = []
- const chunks = getReadChunks(functionCode, startAddress, quantity, options)
- for (const chunk of chunks) {
- const responseValue = await sendReadChunk(
- slaveAddress,
- functionCode,
- chunk,
- getChunkLabel(label, chunks, chunk),
- kind,
- options
- )
- if (!responseValue) return null
- const chunkWords = responseValue.words || []
- chunkWords.forEach((word, index) => {
- words[chunk.address - startAddress + index] = Number(word) & 0xFFFF
- })
- if (typeof options.onChunk === 'function') {
- options.onChunk(responseValue, chunk)
- }
- }
- return words
- }
- async function readBitValues(slaveAddress, functionCode, startAddress, quantity, label, kind, options = {}) {
- const result = await readSpans(
- slaveAddress,
- functionCode,
- [{ address: startAddress, quantity }],
- label,
- kind,
- options
- )
- return result ? result.coils : null
- }
- async function readSingleHoldingWord(slaveAddress, address, label = '读取配对寄存器', kind = 'holding-word-read') {
- const words = await readRegisterWords(slaveAddress, 0x03, address, 1, label, kind)
- return words && Number.isInteger(words[0]) ? words[0] & 0xFFFF : null
- }
- function writeSingleCoil(slaveAddress, address, checked, label, kind = 'coil-write', options = {}) {
- const coilValue = checked ? 0xFF00 : 0x0000
- return transport.sendManagedFrame(
- buildWriteSingleCoilFrame(slaveAddress, address, checked),
- label,
- isBroadcastAddress(slaveAddress) ? null : {
- address,
- functionCode: 0x05,
- kind,
- protocol: PROTOCOL_NAME,
- quantity: 1,
- slaveAddress,
- value: coilValue
- },
- {
- maxFrameBytes: options.maxFrameBytes,
- showModal: options.showModal
- }
- )
- }
- function writeSingleRegister(slaveAddress, address, value, label, kind = 'register-write', options = {}) {
- return transport.sendManagedFrame(
- buildWriteSingleRegisterFrame(slaveAddress, address, value),
- label,
- isBroadcastAddress(slaveAddress) ? null : {
- address,
- functionCode: 0x06,
- kind,
- protocol: PROTOCOL_NAME,
- quantity: 1,
- slaveAddress,
- value
- },
- {
- maxFrameBytes: options.maxFrameBytes,
- showModal: options.showModal
- }
- )
- }
- function writeMultipleRegisters(slaveAddress, address, values, label, kind = 'registers-write', options = {}) {
- return transport.sendManagedFrame(
- buildWriteMultipleRegistersFrame(slaveAddress, address, values, {
- maxFrameBytes: options.maxFrameBytes
- }),
- label,
- isBroadcastAddress(slaveAddress) ? null : {
- address,
- functionCode: 0x10,
- kind,
- protocol: PROTOCOL_NAME,
- quantity: values.length,
- slaveAddress
- },
- {
- maxFrameBytes: options.maxFrameBytes,
- showModal: options.showModal
- }
- )
- }
- const response = {
- MODBUS_EXCEPTION_MESSAGES,
- formatExceptionMessage,
- getExceptionText,
- getExpectedResponseLength,
- getReadBufferHint,
- isExpectedResponse,
- parseModbusRequest,
- parseModbusResponse,
- readResponseFromBuffer
- }
- const client = {
- getReadChunks,
- getSharedSlaveAddress,
- getMaxReadQuantity,
- readBitValues,
- readRegisterWords,
- readSingleHoldingWord,
- readSpans,
- splitQuantity,
- getMaxWriteMultipleRegisterQuantity,
- writeMultipleRegisters,
- writeSingleCoil,
- writeSingleRegister
- }
- module.exports = {
- MAX_MODBUS_DMA_BYTES,
- MAX_READ_COIL_QUANTITY,
- MAX_READ_REGISTER_QUANTITY,
- MAX_WRITE_MULTIPLE_REGISTER_QUANTITY,
- MODBUS_CRC_OPTIONS,
- MODBUS_EXCEPTION_MESSAGES,
- PROTOCOL_NAME,
- UNLIMITED_FRAME_BYTES,
- buildReadFrame,
- buildWriteMultipleRegistersFrame,
- buildWriteSingleCoilFrame,
- buildWriteSingleRegisterFrame,
- client,
- formatHex,
- getMaxReadQuantity,
- getMaxWriteMultipleRegisterQuantity,
- getReadResponseByteLength,
- hasValidCrc16Modbus,
- response
- }
|