index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. const {
  2. padHex
  3. } = require('../../utils/base-utils.js')
  4. const {
  5. bytesToWords
  6. } = require('../../utils/binary-utils.js')
  7. const {
  8. BYTE_ORDER_LOW,
  9. appendCrc16Modbus,
  10. hasValidCrc16Modbus
  11. } = require('../../utils/crc.js')
  12. const PROTOCOL_NAME = 'modbus-rtu'
  13. const MODBUS_CRC_OPTIONS = {
  14. byteOrder: BYTE_ORDER_LOW
  15. }
  16. const MAX_MODBUS_DMA_BYTES = 64
  17. const MODBUS_READ_RESPONSE_OVERHEAD = 5
  18. const MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD = 9
  19. const MAX_READ_REGISTER_QUANTITY = Math.floor((MAX_MODBUS_DMA_BYTES - MODBUS_READ_RESPONSE_OVERHEAD) / 2)
  20. const MAX_READ_COIL_QUANTITY = (MAX_MODBUS_DMA_BYTES - MODBUS_READ_RESPONSE_OVERHEAD) * 8
  21. const MAX_WRITE_MULTIPLE_REGISTER_QUANTITY = Math.floor((MAX_MODBUS_DMA_BYTES - MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD) / 2)
  22. const UNLIMITED_FRAME_BYTES = 0
  23. const MODBUS_EXCEPTION_MESSAGES = {
  24. 0x01: '非法功能',
  25. 0x02: '非法数据地址',
  26. 0x03: '非法数据值',
  27. 0x04: '从站设备故障',
  28. 0x05: '确认',
  29. 0x06: '从站设备忙',
  30. 0x08: '存储奇偶性错误',
  31. 0x0A: '网关路径不可用',
  32. 0x0B: '网关目标设备响应失败'
  33. }
  34. function toByte(value, label) {
  35. if (!Number.isInteger(value) || value < 0 || value > 0xFF) {
  36. throw new Error(`${label}必须在 0x00 至 0xFF 之间`)
  37. }
  38. return value
  39. }
  40. function toWord(value, label) {
  41. if (!Number.isInteger(value) || value < 0 || value > 0xFFFF) {
  42. throw new Error(`${label}必须在 0x0000 至 0xFFFF 之间`)
  43. }
  44. return value
  45. }
  46. function splitWord(value) {
  47. return [(value >> 8) & 0xFF, value & 0xFF]
  48. }
  49. function normalizeMaxFrameBytes(maxFrameBytes = MAX_MODBUS_DMA_BYTES) {
  50. const numberValue = Number(maxFrameBytes)
  51. if (Number.isFinite(numberValue) && Math.round(numberValue) === UNLIMITED_FRAME_BYTES) return UNLIMITED_FRAME_BYTES
  52. if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue)
  53. return MAX_MODBUS_DMA_BYTES
  54. }
  55. function getMaxReadQuantity(functionCode, maxFrameBytes = MAX_MODBUS_DMA_BYTES) {
  56. const frameBytes = normalizeMaxFrameBytes(maxFrameBytes)
  57. if (frameBytes === UNLIMITED_FRAME_BYTES) return 0xFFFF
  58. const dataBytes = frameBytes - MODBUS_READ_RESPONSE_OVERHEAD
  59. if (dataBytes <= 0) return 0
  60. if (functionCode === 0x01 || functionCode === 0x02) return dataBytes * 8
  61. if (functionCode === 0x03 || functionCode === 0x04) return Math.floor(dataBytes / 2)
  62. return 0
  63. }
  64. function getMaxWriteMultipleRegisterQuantity(maxFrameBytes = MAX_MODBUS_DMA_BYTES) {
  65. const frameBytes = normalizeMaxFrameBytes(maxFrameBytes)
  66. if (frameBytes === UNLIMITED_FRAME_BYTES) return 0xFFFF
  67. return Math.max(0, Math.floor((frameBytes - MODBUS_WRITE_MULTIPLE_REQUEST_OVERHEAD) / 2))
  68. }
  69. function buildReadFrame(slaveAddress, functionCode, address, quantity, options = {}) {
  70. const slave = toByte(slaveAddress, '从站地址')
  71. const command = toByte(functionCode, '功能码')
  72. const startAddress = toWord(address, '寄存器地址')
  73. const registerQuantity = toWord(quantity, '读取数量')
  74. const maxQuantity = getMaxReadQuantity(command, options.maxFrameBytes)
  75. if ([0x01, 0x02, 0x03, 0x04].indexOf(command) < 0) {
  76. throw new Error('当前功能码不是读取命令')
  77. }
  78. if (registerQuantity === 0) {
  79. throw new Error('读取数量必须大于 0')
  80. }
  81. if ([0x03, 0x04].indexOf(command) >= 0 && maxQuantity > 0 && registerQuantity > maxQuantity) {
  82. throw new Error(`单帧最多读取 ${maxQuantity} 个寄存器`)
  83. }
  84. if ((command === 0x01 || command === 0x02) && maxQuantity > 0 && registerQuantity > maxQuantity) {
  85. throw new Error(`单帧最多读取 ${maxQuantity} 个位状态`)
  86. }
  87. return appendCrc16Modbus(
  88. [slave, command].concat(splitWord(startAddress), splitWord(registerQuantity)),
  89. MODBUS_CRC_OPTIONS
  90. )
  91. }
  92. function buildWriteSingleCoilFrame(slaveAddress, address, checked) {
  93. const slave = toByte(slaveAddress, '从站地址')
  94. const startAddress = toWord(address, '线圈地址')
  95. const outputValue = checked ? 0xFF00 : 0x0000
  96. return appendCrc16Modbus(
  97. [slave, 0x05].concat(splitWord(startAddress), splitWord(outputValue)),
  98. MODBUS_CRC_OPTIONS
  99. )
  100. }
  101. function buildWriteSingleRegisterFrame(slaveAddress, address, value) {
  102. const slave = toByte(slaveAddress, '从站地址')
  103. const startAddress = toWord(address, '寄存器地址')
  104. const registerValue = toWord(value, '写入值')
  105. return appendCrc16Modbus(
  106. [slave, 0x06].concat(splitWord(startAddress), splitWord(registerValue)),
  107. MODBUS_CRC_OPTIONS
  108. )
  109. }
  110. function buildWriteMultipleRegistersFrame(slaveAddress, address, values, options = {}) {
  111. const slave = toByte(slaveAddress, '从站地址')
  112. const startAddress = toWord(address, '寄存器地址')
  113. const maxQuantity = getMaxWriteMultipleRegisterQuantity(options.maxFrameBytes)
  114. if (!Array.isArray(values) || values.length === 0) {
  115. throw new Error('请输入至少一个寄存器写入值')
  116. }
  117. if (maxQuantity > 0 && values.length > maxQuantity) {
  118. throw new Error(`单帧最多写入 ${maxQuantity} 个寄存器`)
  119. }
  120. const registerBytes = values.reduce((result, value) => {
  121. return result.concat(splitWord(toWord(value, '写入值')))
  122. }, [])
  123. return appendCrc16Modbus(
  124. [slave, 0x10]
  125. .concat(splitWord(startAddress), splitWord(values.length), [registerBytes.length], registerBytes),
  126. MODBUS_CRC_OPTIONS
  127. )
  128. }
  129. function formatHex(bytes) {
  130. return bytes.map((byte) => byte.toString(16).padStart(2, '0').toUpperCase()).join(' ')
  131. }
  132. function getReadResponseByteLength(functionCode, quantity) {
  133. if (functionCode === 0x01 || functionCode === 0x02) return MODBUS_READ_RESPONSE_OVERHEAD + Math.ceil(Number(quantity || 0) / 8)
  134. if (functionCode === 0x03 || functionCode === 0x04) return MODBUS_READ_RESPONSE_OVERHEAD + Number(quantity || 0) * 2
  135. return 0
  136. }
  137. function parseModbusResponse(bytes) {
  138. if (!Array.isArray(bytes) || bytes.length < 5 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null
  139. const slaveAddress = bytes[0]
  140. const functionCode = bytes[1]
  141. if (functionCode & 0x80) {
  142. return {
  143. exceptionCode: bytes[2],
  144. functionCode,
  145. isException: true,
  146. protocol: PROTOCOL_NAME,
  147. slaveAddress,
  148. sourceFunctionCode: functionCode & 0x7F
  149. }
  150. }
  151. if (functionCode === 0x01 || functionCode === 0x02) {
  152. const byteCount = bytes[2]
  153. const dataEnd = 3 + byteCount
  154. if (bytes.length < dataEnd + 2) return null
  155. return {
  156. byteCount,
  157. dataBytes: bytes.slice(3, dataEnd),
  158. functionCode,
  159. isException: false,
  160. protocol: PROTOCOL_NAME,
  161. slaveAddress
  162. }
  163. }
  164. if (functionCode === 0x03 || functionCode === 0x04) {
  165. const byteCount = bytes[2]
  166. const dataEnd = 3 + byteCount
  167. if (bytes.length < dataEnd + 2) return null
  168. return {
  169. byteCount,
  170. dataBytes: bytes.slice(3, dataEnd),
  171. functionCode,
  172. isException: false,
  173. protocol: PROTOCOL_NAME,
  174. slaveAddress,
  175. words: bytesToWords(bytes.slice(3, dataEnd))
  176. }
  177. }
  178. if (functionCode === 0x05 || functionCode === 0x06 || functionCode === 0x10) {
  179. return {
  180. address: ((bytes[2] << 8) | bytes[3]) & 0xFFFF,
  181. functionCode,
  182. isException: false,
  183. protocol: PROTOCOL_NAME,
  184. quantityOrValue: ((bytes[4] << 8) | bytes[5]) & 0xFFFF,
  185. slaveAddress
  186. }
  187. }
  188. return {
  189. functionCode,
  190. isException: false,
  191. protocol: PROTOCOL_NAME,
  192. slaveAddress
  193. }
  194. }
  195. function parseModbusRequest(bytes) {
  196. if (!Array.isArray(bytes) || bytes.length < 4 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null
  197. const slaveAddress = bytes[0]
  198. const functionCode = bytes[1]
  199. if (bytes.length < 6) return null
  200. const address = ((bytes[2] << 8) | bytes[3]) & 0xFFFF
  201. let quantity = 1
  202. let value
  203. if (functionCode === 0x01 || functionCode === 0x02 || functionCode === 0x03 || functionCode === 0x04 || functionCode === 0x10) {
  204. quantity = ((bytes[4] << 8) | bytes[5]) & 0xFFFF
  205. }
  206. if (functionCode === 0x05 || functionCode === 0x06) {
  207. value = ((bytes[4] << 8) | bytes[5]) & 0xFFFF
  208. }
  209. return {
  210. address,
  211. functionCode,
  212. kind: 'raw-hex',
  213. protocol: PROTOCOL_NAME,
  214. quantity,
  215. value,
  216. slaveAddress
  217. }
  218. }
  219. function getExpectedResponseLength(expected, responseFunctionCode, responseBytes = []) {
  220. if (!expected) return 0
  221. if (responseFunctionCode === (expected.functionCode | 0x80)) {
  222. return 5
  223. }
  224. if (responseFunctionCode === 0x01 || responseFunctionCode === 0x02) {
  225. if (responseBytes.length < 3) return 0
  226. return 3 + Number(responseBytes[2] || 0) + 2
  227. }
  228. if (responseFunctionCode === 0x03 || responseFunctionCode === 0x04) {
  229. if (responseBytes.length < 3) return 0
  230. return 3 + Number(responseBytes[2] || 0) + 2
  231. }
  232. if (responseFunctionCode === 0x05 || responseFunctionCode === 0x06 || responseFunctionCode === 0x10) {
  233. return 8
  234. }
  235. return 0
  236. }
  237. function isExpectedResponse(response, expected) {
  238. if (response.functionCode === 0x01 || response.functionCode === 0x02) {
  239. return Array.isArray(response.dataBytes) && response.dataBytes.length >= Math.ceil(expected.quantity / 8)
  240. }
  241. if (response.functionCode === 0x03 || response.functionCode === 0x04) {
  242. return Array.isArray(response.words) && response.words.length >= expected.quantity
  243. }
  244. if (response.functionCode === 0x10) {
  245. return response.address === expected.address && response.quantityOrValue === expected.quantity
  246. }
  247. if (response.functionCode === 0x05 || response.functionCode === 0x06) {
  248. if (response.address !== expected.address) return false
  249. if (Number.isInteger(expected.value)) return response.quantityOrValue === expected.value
  250. return true
  251. }
  252. return true
  253. }
  254. function getExceptionText(code) {
  255. return MODBUS_EXCEPTION_MESSAGES[code] || '未知异常'
  256. }
  257. function formatExceptionMessage(response) {
  258. const sourceFunctionCode = response && response.sourceFunctionCode
  259. const exceptionCode = response && response.exceptionCode
  260. const exceptionText = getExceptionText(exceptionCode)
  261. return `设备返回异常帧:功能码 0x${padHex(sourceFunctionCode, 2)},异常码 0x${padHex(exceptionCode, 2)}(${exceptionText})`
  262. }
  263. function getReadBufferHint(expected) {
  264. return expected ? getReadResponseByteLength(expected.functionCode, expected.quantity) : 0
  265. }
  266. function alignResponseBuffer(buffer, expected) {
  267. if (!Array.isArray(buffer) || !buffer.length || !expected) return
  268. const expectedFunctionCodes = [expected.functionCode, expected.functionCode | 0x80]
  269. let matchIndex = -1
  270. for (let index = 0; index < buffer.length - 1; index += 1) {
  271. if (buffer[index] !== expected.slaveAddress) continue
  272. if (expectedFunctionCodes.indexOf(buffer[index + 1]) < 0) continue
  273. matchIndex = index
  274. break
  275. }
  276. if (matchIndex > 0) {
  277. buffer.splice(0, matchIndex)
  278. } else if (matchIndex < 0 && buffer.length > 2) {
  279. buffer.splice(0, buffer.length - 1)
  280. }
  281. }
  282. function readResponseFromBuffer(buffer, expected, options = {}) {
  283. if (!Array.isArray(buffer) || !buffer.length || !expected) {
  284. return {
  285. status: 'pending'
  286. }
  287. }
  288. alignResponseBuffer(buffer, expected)
  289. while (buffer.length >= 2) {
  290. const responseFunctionCode = buffer[1]
  291. const responseLength = getExpectedResponseLength(expected, responseFunctionCode, buffer)
  292. if (!responseLength) {
  293. return {
  294. status: 'pending'
  295. }
  296. }
  297. const frameLimit = normalizeMaxFrameBytes(
  298. options.maxFrameBytes === undefined ? expected.maxFrameBytes : options.maxFrameBytes
  299. )
  300. if (frameLimit > 0 && responseLength > frameLimit) {
  301. return {
  302. frameLimit,
  303. responseLength,
  304. status: 'frame-too-long'
  305. }
  306. }
  307. if (buffer.length < responseLength) {
  308. return {
  309. status: 'pending'
  310. }
  311. }
  312. const frameBytes = buffer.slice(0, responseLength)
  313. const response = parseModbusResponse(frameBytes)
  314. if (!response) {
  315. return {
  316. frameBytes,
  317. status: 'invalid'
  318. }
  319. }
  320. const responseCode = response.isException ? response.sourceFunctionCode : response.functionCode
  321. if (response.slaveAddress !== expected.slaveAddress || responseCode !== expected.functionCode) {
  322. buffer.shift()
  323. alignResponseBuffer(buffer, expected)
  324. continue
  325. }
  326. if (response.isException) {
  327. return {
  328. message: formatExceptionMessage(response),
  329. response,
  330. status: 'exception'
  331. }
  332. }
  333. if (!isExpectedResponse(response, expected)) {
  334. return {
  335. response,
  336. status: 'mismatch'
  337. }
  338. }
  339. buffer.splice(0, responseLength)
  340. return {
  341. response,
  342. status: 'complete'
  343. }
  344. }
  345. return {
  346. status: 'pending'
  347. }
  348. }
  349. function splitQuantity(startAddress, quantity, maxQuantity) {
  350. const chunks = []
  351. let address = Number(startAddress) || 0
  352. let remaining = Math.max(0, Math.floor(Number(quantity) || 0))
  353. const chunkLimit = Math.max(1, Math.floor(Number(maxQuantity) || remaining || 1))
  354. while (remaining > 0) {
  355. const chunkQuantity = Math.min(remaining, chunkLimit)
  356. chunks.push({
  357. address,
  358. quantity: chunkQuantity
  359. })
  360. address += chunkQuantity
  361. remaining -= chunkQuantity
  362. }
  363. return chunks
  364. }
  365. function getReadChunks(functionCode, startAddress, quantity, options = {}) {
  366. const maxQuantity = getMaxReadQuantity(functionCode, options.maxFrameBytes)
  367. return splitQuantity(startAddress, quantity, maxQuantity || quantity)
  368. }
  369. const response = {
  370. MODBUS_EXCEPTION_MESSAGES,
  371. formatExceptionMessage,
  372. getExceptionText,
  373. getExpectedResponseLength,
  374. getReadBufferHint,
  375. isExpectedResponse,
  376. parseModbusRequest,
  377. parseModbusResponse,
  378. readResponseFromBuffer
  379. }
  380. module.exports = {
  381. MAX_MODBUS_DMA_BYTES,
  382. MAX_READ_COIL_QUANTITY,
  383. MAX_READ_REGISTER_QUANTITY,
  384. MAX_WRITE_MULTIPLE_REGISTER_QUANTITY,
  385. MODBUS_CRC_OPTIONS,
  386. MODBUS_EXCEPTION_MESSAGES,
  387. PROTOCOL_NAME,
  388. UNLIMITED_FRAME_BYTES,
  389. buildReadFrame,
  390. buildWriteMultipleRegistersFrame,
  391. buildWriteSingleCoilFrame,
  392. buildWriteSingleRegisterFrame,
  393. formatHex,
  394. getMaxReadQuantity,
  395. getMaxWriteMultipleRegisterQuantity,
  396. getReadChunks,
  397. getReadResponseByteLength,
  398. hasValidCrc16Modbus,
  399. response,
  400. splitQuantity
  401. }