1
0

manual-rtu.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. const transport = require('../../transport/ble-core.js')
  2. const {
  3. buildReadFrame,
  4. buildWriteMultipleRegistersFrame,
  5. buildWriteSingleCoilFrame,
  6. buildWriteSingleRegisterFrame,
  7. formatHex
  8. } = require('../../protocols/modbus-rtu/index.js')
  9. const {
  10. parseHexNumber
  11. } = require('../../utils/validation.js')
  12. const {
  13. DATA_TYPE_OPTIONS,
  14. getDataType,
  15. getRegisterEncodedWords,
  16. isByteRegister,
  17. isTextRegister,
  18. normalizeRegister,
  19. validateRegisterValue
  20. } = require('../../domain/parameter-groups/model.js')
  21. const MODBUS_COMMANDS = [
  22. { key: 'readCoils', label: '01 读取线圈', functionCode: 0x01, inputMode: 'quantity' },
  23. { key: 'readDiscreteInputs', label: '02 读取离散输入', functionCode: 0x02, inputMode: 'quantity' },
  24. { key: 'readHolding', label: '03 读取保持寄存器', functionCode: 0x03, inputMode: 'quantity' },
  25. { key: 'readInput', label: '04 读取输入寄存器', functionCode: 0x04, inputMode: 'quantity' },
  26. { key: 'writeCoil', label: '05 写单线圈', functionCode: 0x05, inputMode: 'coil' },
  27. { key: 'writeRegister', label: '06 写单寄存器', functionCode: 0x06, inputMode: 'single' },
  28. { key: 'writeRegisters', label: '10 写多寄存器', functionCode: 0x10, inputMode: 'multiple' }
  29. ]
  30. const state = {
  31. commandIndex: 2,
  32. commandRegisterQuantity: '0001',
  33. commandValue: '0001',
  34. commandValueLabel: '读取数量',
  35. coilEnabled: true,
  36. generatedHex: '',
  37. protocolCommands: MODBUS_COMMANDS,
  38. protocolDataTypeOptions: DATA_TYPE_OPTIONS,
  39. protocolErrorText: '',
  40. protocolMultipleDialog: {
  41. visible: false
  42. },
  43. protocolMultipleExpanded: false,
  44. protocolMultipleValues: [],
  45. protocolResponseText: '',
  46. protocolStatusText: '',
  47. registerAddress: '0000',
  48. showCoilValue: false,
  49. showCommandValue: true,
  50. showRegisterQuantity: false,
  51. slaveAddress: 'F0'
  52. }
  53. const subscribers = []
  54. function parseRegisterValues(value) {
  55. const text = String(value || '').trim()
  56. if (!text) throw new Error('请输入寄存器写入值')
  57. return text.split(/[\s,;]+/)
  58. .filter(Boolean)
  59. .map((item) => parseHexNumber(item, '写入值', 0xFFFF))
  60. }
  61. function getCommand(index) {
  62. return MODBUS_COMMANDS[index] || MODBUS_COMMANDS[0]
  63. }
  64. function getDefaultCommandValue(command) {
  65. if (command.inputMode === 'quantity') return '0001'
  66. if (command.inputMode === 'coil') return 'ON'
  67. if (command.inputMode === 'multiple') return '0000'
  68. return '0000'
  69. }
  70. function generateModbusFrame(command, slaveAddress, registerAddress, commandValue, coilEnabled, commandRegisterQuantity) {
  71. const slave = parseHexNumber(slaveAddress, '从站地址', 0xFF)
  72. const address = parseHexNumber(registerAddress, '起始地址', 0xFFFF)
  73. if (command.inputMode === 'quantity') {
  74. const quantity = parseHexNumber(commandValue, '读取数量', 0xFFFF)
  75. return buildReadFrame(slave, command.functionCode, address, quantity)
  76. }
  77. if (command.inputMode === 'coil') {
  78. return buildWriteSingleCoilFrame(slave, address, coilEnabled)
  79. }
  80. if (command.inputMode === 'single') {
  81. return buildWriteSingleRegisterFrame(slave, address, parseHexNumber(commandValue, '写入值', 0xFFFF))
  82. }
  83. const words = parseRegisterValues(commandValue)
  84. const quantity = parseHexNumber(commandRegisterQuantity, '寄存器个数', 0xFFFF)
  85. if (quantity !== words.length) {
  86. throw new Error(`写入值数量应为 ${quantity} 个寄存器`)
  87. }
  88. return buildWriteMultipleRegistersFrame(slave, address, words)
  89. }
  90. function createProtocolState(commandIndex, slaveAddress, registerAddress, commandValue, coilEnabled, commandRegisterQuantity) {
  91. const command = getCommand(commandIndex)
  92. const commandValueLabel = command.inputMode === 'quantity' ? '读取数量' : '写入值'
  93. try {
  94. return {
  95. commandValueLabel,
  96. generatedHex: formatHex(generateModbusFrame(command, slaveAddress, registerAddress, commandValue, coilEnabled, commandRegisterQuantity)),
  97. protocolErrorText: '',
  98. showCoilValue: command.inputMode === 'coil',
  99. showRegisterQuantity: command.inputMode === 'multiple',
  100. showCommandValue: command.inputMode !== 'coil'
  101. }
  102. } catch (error) {
  103. return {
  104. commandValueLabel,
  105. generatedHex: '',
  106. protocolErrorText: error.message,
  107. showCoilValue: command.inputMode === 'coil',
  108. showRegisterQuantity: command.inputMode === 'multiple',
  109. showCommandValue: command.inputMode !== 'coil'
  110. }
  111. }
  112. }
  113. function buildExpectedResponse(sourceState = {}) {
  114. try {
  115. const command = getCommand(sourceState.commandIndex)
  116. const address = parseHexNumber(sourceState.registerAddress, '起始地址', 0xFFFF)
  117. const slaveAddress = parseHexNumber(sourceState.slaveAddress, '从站地址', 0xFF)
  118. const quantity = command.inputMode === 'quantity'
  119. ? parseHexNumber(sourceState.commandValue, '读取数量', 0xFFFF)
  120. : (command.inputMode === 'multiple' ? parseHexNumber(sourceState.commandRegisterQuantity, '寄存器个数', 0xFFFF) : 1)
  121. const value = command.inputMode === 'coil'
  122. ? (sourceState.coilEnabled ? 0xFF00 : 0x0000)
  123. : (command.inputMode === 'single' ? parseHexNumber(sourceState.commandValue, '写入值', 0xFFFF) : undefined)
  124. return {
  125. address,
  126. functionCode: command.functionCode,
  127. kind: 'manual-rtu',
  128. protocol: 'modbus-rtu',
  129. quantity,
  130. value,
  131. slaveAddress
  132. }
  133. } catch (error) {
  134. return null
  135. }
  136. }
  137. function formatResponseHex(value, width) {
  138. return Number(value || 0).toString(16).toUpperCase().padStart(width, '0')
  139. }
  140. function formatGeneratedResponse(response) {
  141. if (!response) return ''
  142. const slave = formatResponseHex(response.slaveAddress, 2)
  143. const functionCode = formatResponseHex(response.functionCode, 2)
  144. if (Array.isArray(response.words) && response.words.length) {
  145. return `从机 0x${slave} / 功能码 0x${functionCode} / 字 ${response.words.map((word) => formatResponseHex(word, 4)).join(' ')}`
  146. }
  147. if (Array.isArray(response.dataBytes) && response.dataBytes.length) {
  148. return `从机 0x${slave} / 功能码 0x${functionCode} / 数据 ${formatHex(response.dataBytes)}`
  149. }
  150. if (Number.isInteger(response.address)) {
  151. return `从机 0x${slave} / 功能码 0x${functionCode} / 地址 0x${formatResponseHex(response.address, 4)} / 值 0x${formatResponseHex(response.quantityOrValue, 4)}`
  152. }
  153. return `从机 0x${slave} / 功能码 0x${functionCode}`
  154. }
  155. function normalizeManualMultipleQuantity(value) {
  156. const text = String(value === undefined || value === null ? '' : value).trim()
  157. if (!text) return 1
  158. if (/^[0-9a-fA-F]+$/.test(text)) return Math.max(1, Math.min(parseInt(text, 16), 0x007B))
  159. const numberValue = Number(text)
  160. return Number.isFinite(numberValue) ? Math.max(1, Math.min(Math.round(numberValue), 0x007B)) : 1
  161. }
  162. function formatManualMultipleQuantity(quantity) {
  163. return Number(quantity || 1).toString(16).toUpperCase().padStart(4, '0')
  164. }
  165. function createManualMultipleRegister(index, value = {}) {
  166. const dataType = getDataType(value.dataType || 'hex')
  167. const register = normalizeRegister({
  168. dataType: dataType.key,
  169. inputValue: value.inputValue === undefined ? '' : value.inputValue,
  170. name: `寄存器 ${index + 1}`,
  171. textByteLength: value.textByteLength || (isTextRegister(dataType.key) ? '32' : '')
  172. }, {
  173. registerType: 'holding'
  174. }, index, Number(value.address || 0), 0)
  175. return {
  176. ...register,
  177. dataTypeIndex: DATA_TYPE_OPTIONS.findIndex((item) => item.key === register.dataType),
  178. inputValue: value.inputValue === undefined ? '' : value.inputValue
  179. }
  180. }
  181. function getManualRegisterWordCount(register) {
  182. return Math.max(1, Number(register && register.registerCount) || 1)
  183. }
  184. function normalizeManualMultipleValues(wordQuantity, values = [], startAddress = 0) {
  185. const result = []
  186. let address = Number(startAddress) || 0
  187. const endAddress = address + Math.max(1, Number(wordQuantity) || 1)
  188. let sourceIndex = 0
  189. while (address < endAddress) {
  190. const current = values[sourceIndex] || {}
  191. let register = createManualMultipleRegister(result.length, {
  192. ...current,
  193. address
  194. })
  195. const remainingWords = endAddress - address
  196. if (getManualRegisterWordCount(register) > remainingWords) {
  197. register = createManualMultipleRegister(result.length, {
  198. ...current,
  199. address,
  200. dataType: 'hex',
  201. inputValue: ''
  202. })
  203. }
  204. result.push(register)
  205. address += getManualRegisterWordCount(register)
  206. sourceIndex += 1
  207. }
  208. return result
  209. }
  210. function getManualMultipleWords(values = []) {
  211. const words = []
  212. values.forEach((register) => {
  213. if (isByteRegister(register.dataType)) {
  214. const registerWords = getRegisterEncodedWords(register)
  215. if (!Array.isArray(registerWords) || !registerWords.length) throw new Error(`${register.name} 输入值无效`)
  216. words.push(Number(registerWords[0]) & 0x00FF)
  217. return
  218. }
  219. const registerWords = getRegisterEncodedWords(register)
  220. if (!Array.isArray(registerWords) || !registerWords.length) throw new Error(`${register.name} 输入值无效`)
  221. registerWords.forEach((word) => words.push(Number(word) & 0xFFFF))
  222. })
  223. return words
  224. }
  225. function getManualMultipleValueText(values = []) {
  226. try {
  227. return getManualMultipleWords(values).map((word) => word.toString(16).toUpperCase().padStart(4, '0')).join(' ')
  228. } catch (error) {
  229. return ''
  230. }
  231. }
  232. function getManualMultipleDataType(dataTypeIndex) {
  233. return DATA_TYPE_OPTIONS[Number(dataTypeIndex)] || DATA_TYPE_OPTIONS[0]
  234. }
  235. function updateManualMultipleValue(values = [], index, value) {
  236. const registerIndex = Number(index)
  237. return values.map((register, currentIndex) => (
  238. currentIndex === registerIndex
  239. ? {
  240. ...register,
  241. inputValue: value
  242. }
  243. : register
  244. ))
  245. }
  246. function updateManualMultipleType(values = [], index, dataTypeIndex) {
  247. const registerIndex = Number(index)
  248. const dataType = getManualMultipleDataType(dataTypeIndex)
  249. return values.map((register, currentIndex) => (
  250. currentIndex === registerIndex
  251. ? createManualMultipleRegister(currentIndex, {
  252. ...register,
  253. dataType: dataType.key,
  254. inputValue: '',
  255. textByteLength: isTextRegister(dataType.key) ? (register.textByteLength || '32') : ''
  256. })
  257. : register
  258. ))
  259. }
  260. function updateManualMultipleTextLength(values = [], index, value) {
  261. const registerIndex = Number(index)
  262. return values.map((register, currentIndex) => (
  263. currentIndex === registerIndex
  264. ? createManualMultipleRegister(currentIndex, {
  265. ...register,
  266. textByteLength: value
  267. })
  268. : register
  269. ))
  270. }
  271. function validateManualMultipleValue(values = [], index, value) {
  272. const register = values[Number(index)]
  273. if (!register) return false
  274. return validateRegisterValue(register, value)
  275. }
  276. function setState(changedData) {
  277. Object.assign(state, changedData)
  278. subscribers.slice().forEach((subscriber) => {
  279. subscriber(getState())
  280. })
  281. }
  282. function getState() {
  283. return {
  284. ...state,
  285. protocolCommands: state.protocolCommands.slice(),
  286. protocolDataTypeOptions: state.protocolDataTypeOptions.slice(),
  287. protocolMultipleDialog: {
  288. ...state.protocolMultipleDialog
  289. },
  290. protocolMultipleExpanded: !!state.protocolMultipleExpanded,
  291. protocolMultipleValues: state.protocolMultipleValues.map((item) => ({
  292. ...item
  293. })),
  294. protocolStatusText: state.protocolStatusText || ''
  295. }
  296. }
  297. function subscribe(subscriber) {
  298. if (typeof subscriber !== 'function') return () => {}
  299. subscribers.push(subscriber)
  300. subscriber(getState())
  301. return () => {
  302. const index = subscribers.indexOf(subscriber)
  303. if (index >= 0) subscribers.splice(index, 1)
  304. }
  305. }
  306. function setProtocolInput(changedData) {
  307. const command = getCommand(changedData.commandIndex === undefined ? state.commandIndex : changedData.commandIndex)
  308. let nextMultipleValues = changedData.protocolMultipleValues
  309. let nextCommandValue = changedData.commandValue
  310. if (
  311. command.inputMode === 'multiple'
  312. && Object.prototype.hasOwnProperty.call(changedData, 'registerAddress')
  313. && !Object.prototype.hasOwnProperty.call(changedData, 'protocolMultipleValues')
  314. ) {
  315. try {
  316. const startAddress = parseHexNumber(changedData.registerAddress, '起始地址', 0xFFFF)
  317. nextMultipleValues = normalizeManualMultipleValues(
  318. normalizeManualMultipleQuantity(state.commandRegisterQuantity),
  319. state.protocolMultipleValues,
  320. startAddress
  321. )
  322. nextCommandValue = getManualMultipleValueText(nextMultipleValues)
  323. } catch (error) {}
  324. }
  325. const nextState = {
  326. commandIndex: state.commandIndex,
  327. commandRegisterQuantity: state.commandRegisterQuantity,
  328. slaveAddress: state.slaveAddress,
  329. registerAddress: state.registerAddress,
  330. commandValue: state.commandValue,
  331. coilEnabled: state.coilEnabled,
  332. ...changedData,
  333. ...(nextMultipleValues ? { protocolMultipleValues: nextMultipleValues } : {}),
  334. ...(nextCommandValue !== undefined ? { commandValue: nextCommandValue } : {})
  335. }
  336. setState({
  337. ...changedData,
  338. ...(nextMultipleValues ? { protocolMultipleValues: nextMultipleValues } : {}),
  339. ...(nextCommandValue !== undefined ? { commandValue: nextCommandValue } : {}),
  340. protocolResponseText: '',
  341. protocolStatusText: '',
  342. ...createProtocolState(
  343. nextState.commandIndex,
  344. nextState.slaveAddress,
  345. nextState.registerAddress,
  346. nextState.commandValue,
  347. nextState.coilEnabled,
  348. nextState.commandRegisterQuantity
  349. )
  350. })
  351. }
  352. function setCommandIndex(index) {
  353. const commandIndex = Number(index)
  354. const command = getCommand(commandIndex)
  355. let protocolMultipleValues = state.protocolMultipleValues
  356. let commandValue = getDefaultCommandValue(command)
  357. if (command.inputMode === 'multiple') {
  358. let startAddress = 0
  359. try {
  360. startAddress = parseHexNumber(state.registerAddress, '起始地址', 0xFFFF)
  361. } catch (error) {}
  362. protocolMultipleValues = normalizeManualMultipleValues(
  363. normalizeManualMultipleQuantity(state.commandRegisterQuantity),
  364. state.protocolMultipleValues,
  365. startAddress
  366. )
  367. commandValue = getManualMultipleValueText(protocolMultipleValues)
  368. }
  369. setProtocolInput({
  370. commandIndex,
  371. commandValue,
  372. coilEnabled: true,
  373. commandRegisterQuantity: state.commandRegisterQuantity,
  374. ...(command.inputMode === 'multiple' ? { protocolMultipleValues } : {})
  375. })
  376. }
  377. function openProtocolMultipleDialog() {
  378. let startAddress = 0
  379. try {
  380. startAddress = parseHexNumber(state.registerAddress, '起始地址', 0xFFFF)
  381. } catch (error) {
  382. setState({
  383. protocolErrorText: error.message || '起始地址无效'
  384. })
  385. return
  386. }
  387. const quantity = normalizeManualMultipleQuantity(state.commandRegisterQuantity)
  388. const values = normalizeManualMultipleValues(quantity, state.protocolMultipleValues, startAddress)
  389. setState({
  390. commandRegisterQuantity: formatManualMultipleQuantity(quantity),
  391. protocolMultipleDialog: {
  392. title: '写多个寄存器',
  393. visible: true
  394. },
  395. protocolMultipleExpanded: false,
  396. protocolMultipleValues: values
  397. })
  398. }
  399. function closeProtocolMultipleDialog() {
  400. setState({
  401. protocolMultipleDialog: {
  402. visible: false
  403. }
  404. })
  405. }
  406. function toggleProtocolMultipleExpanded() {
  407. setState({
  408. protocolMultipleExpanded: !state.protocolMultipleExpanded
  409. })
  410. }
  411. function setProtocolMultipleQuantity(value) {
  412. const quantity = normalizeManualMultipleQuantity(value)
  413. let startAddress = 0
  414. try {
  415. startAddress = parseHexNumber(state.registerAddress, '起始地址', 0xFFFF)
  416. } catch (error) {
  417. setState({
  418. commandRegisterQuantity: value,
  419. protocolErrorText: error.message || '起始地址无效'
  420. })
  421. return
  422. }
  423. const values = normalizeManualMultipleValues(quantity, state.protocolMultipleValues, startAddress)
  424. const commandValue = getManualMultipleValueText(values)
  425. setProtocolInput({
  426. commandRegisterQuantity: value,
  427. commandValue,
  428. protocolMultipleValues: values
  429. })
  430. }
  431. function setProtocolMultipleValue(index, value) {
  432. const values = updateManualMultipleValue(state.protocolMultipleValues, index, value)
  433. let startAddress = 0
  434. try {
  435. startAddress = parseHexNumber(state.registerAddress, '起始地址', 0xFFFF)
  436. } catch (error) {
  437. setState({ protocolErrorText: error.message || '起始地址无效' })
  438. return
  439. }
  440. const normalizedValues = normalizeManualMultipleValues(normalizeManualMultipleQuantity(state.commandRegisterQuantity), values, startAddress)
  441. const commandValue = getManualMultipleValueText(normalizedValues)
  442. setProtocolInput({
  443. commandValue,
  444. protocolMultipleValues: normalizedValues
  445. })
  446. }
  447. function setProtocolMultipleType(index, dataTypeIndex) {
  448. const changedValues = updateManualMultipleType(state.protocolMultipleValues, index, dataTypeIndex)
  449. let startAddress = 0
  450. try {
  451. startAddress = parseHexNumber(state.registerAddress, '起始地址', 0xFFFF)
  452. } catch (error) {
  453. setState({ protocolErrorText: error.message || '起始地址无效' })
  454. return
  455. }
  456. const values = normalizeManualMultipleValues(normalizeManualMultipleQuantity(state.commandRegisterQuantity), changedValues, startAddress)
  457. setProtocolInput({
  458. commandValue: getManualMultipleValueText(values),
  459. protocolMultipleValues: values
  460. })
  461. }
  462. function setProtocolMultipleTextLength(index, value) {
  463. const changedValues = updateManualMultipleTextLength(state.protocolMultipleValues, index, value)
  464. let startAddress = 0
  465. try {
  466. startAddress = parseHexNumber(state.registerAddress, '起始地址', 0xFFFF)
  467. } catch (error) {
  468. setState({ protocolErrorText: error.message || '起始地址无效' })
  469. return
  470. }
  471. const values = normalizeManualMultipleValues(normalizeManualMultipleQuantity(state.commandRegisterQuantity), changedValues, startAddress)
  472. setProtocolInput({
  473. commandValue: getManualMultipleValueText(values),
  474. protocolMultipleValues: values
  475. })
  476. }
  477. function validateProtocolMultipleValue(index, value) {
  478. return validateManualMultipleValue(state.protocolMultipleValues, index, value)
  479. }
  480. function buildGeneratedExpectedResponse() {
  481. return buildExpectedResponse(state)
  482. }
  483. async function sendGeneratedFrame() {
  484. if (!state.generatedHex) {
  485. setState({
  486. protocolStatusText: state.protocolErrorText || '请先生成有效帧'
  487. })
  488. return false
  489. }
  490. const expected = buildGeneratedExpectedResponse()
  491. setState({
  492. protocolResponseText: '',
  493. protocolStatusText: ''
  494. })
  495. const response = await transport.enqueueSendFrame(state.generatedHex, 'RTU', expected ? {
  496. expected
  497. } : {})
  498. if (response) {
  499. setState({
  500. protocolResponseText: formatGeneratedResponse(response),
  501. protocolStatusText: ''
  502. })
  503. } else {
  504. setState({
  505. protocolStatusText: '未收到回复'
  506. })
  507. }
  508. return response
  509. }
  510. setState(createProtocolState(
  511. state.commandIndex,
  512. state.slaveAddress,
  513. state.registerAddress,
  514. state.commandValue,
  515. state.coilEnabled,
  516. state.commandRegisterQuantity
  517. ))
  518. module.exports = {
  519. MODBUS_COMMANDS,
  520. buildGeneratedExpectedResponse,
  521. closeProtocolMultipleDialog,
  522. formatGeneratedResponse,
  523. generateModbusFrame,
  524. getState,
  525. openProtocolMultipleDialog,
  526. sendGeneratedFrame,
  527. setCommandIndex,
  528. setProtocolInput,
  529. setProtocolMultipleQuantity,
  530. setProtocolMultipleTextLength,
  531. setProtocolMultipleType,
  532. setProtocolMultipleValue,
  533. subscribe,
  534. toggleProtocolMultipleExpanded,
  535. validateProtocolMultipleValue
  536. }