1
0

service.js 16 KB

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