manual-rtu.js 19 KB

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