service.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. const {
  2. formatExportStamp,
  3. isCancelError,
  4. loadSelectedFile,
  5. saveTextFileToChat
  6. } = require('../../repositories/file.js')
  7. const {
  8. getWxApi
  9. } = require('../../utils/platform-utils.js')
  10. const {
  11. parseHexInteger
  12. } = require('../../utils/base-utils.js')
  13. const transport = require('../../transport/ble-core.js')
  14. const settingsService = require('../../store/settings-store.js')
  15. const modbusClient = require('../../protocols/modbus-rtu/client.js')
  16. const {
  17. DATA_TYPE_OPTIONS,
  18. REGISTER_TYPE_OPTIONS,
  19. cloneImportedGroup,
  20. decodeRegisterFromWordCache,
  21. decodeRegisterValue,
  22. formatCoilDisplayValue,
  23. formatRegisterValue,
  24. getDataType,
  25. getGroupEncodedWords,
  26. getRegisterEncodedWords,
  27. getRegisterJsonValue,
  28. getRegisterWordsFromWordCache,
  29. getRegisterWriteValueText,
  30. isAddressRangeOverflow,
  31. isBitRegisterType,
  32. isByteRegister,
  33. normalizeGroup,
  34. normalizeGroupConfig,
  35. parseCoilValue,
  36. registerTypeIsBit,
  37. splitWordSpans,
  38. validateRegisterValue
  39. } = require('../../domain/generic-modbus/model.js')
  40. const {
  41. parseStructDefinition: parseStructDefinitionSource
  42. } = require('../../domain/generic-modbus/struct-parser.js')
  43. const STORAGE_KEY = 'generic-modbus-groups-json'
  44. const JSON_DOCUMENT_TYPE = 'generic-modbus-rtu'
  45. const JSON_SCHEMA_VERSION = 2
  46. let initialized = false
  47. const subscribers = []
  48. let state = {
  49. genericModbusDataTypeOptions: DATA_TYPE_OPTIONS,
  50. genericModbusGroups: [],
  51. genericModbusRegisterTypeOptions: REGISTER_TYPE_OPTIONS
  52. }
  53. function notify() {
  54. const nextState = getState()
  55. subscribers.slice().forEach((subscriber) => {
  56. subscriber(nextState)
  57. })
  58. }
  59. function setState(changedData, options = {}) {
  60. state = {
  61. ...state,
  62. ...changedData
  63. }
  64. if (options.persist !== false) persistGroups()
  65. notify()
  66. }
  67. function resolveMaxPacketLength(value) {
  68. const settings = settingsService.getState()
  69. const numberValue = Number(value === undefined ? settings.genericModbusMaxPacketLength : value)
  70. if (Number.isFinite(numberValue) && Math.round(numberValue) === 0) return 0
  71. if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue)
  72. return 64
  73. }
  74. function getWriteSpanMaxQuantity(totalQuantity, maxPacketLength) {
  75. if (maxPacketLength === 0) return Math.max(1, totalQuantity)
  76. return Math.max(1, modbusClient.getMaxWriteMultipleRegisterQuantity(maxPacketLength))
  77. }
  78. function toPersistedGroups(groups) {
  79. return groups.map((group) => ({
  80. layout: group.layout,
  81. name: group.name,
  82. registerType: group.registerType,
  83. startAddress: group.startAddress,
  84. quantity: group.quantity,
  85. registers: group.registers.map((register) => ({
  86. dataType: register.dataType,
  87. defaultValue: register.defaultValue,
  88. isStructField: register.isStructField,
  89. name: register.name,
  90. maxValue: register.maxValue,
  91. minValue: register.minValue,
  92. textByteLength: register.textByteLength,
  93. remark: register.remark,
  94. unit: register.unit,
  95. value: getRegisterJsonValue(register)
  96. }))
  97. }))
  98. }
  99. function toJsonData(groups = state.genericModbusGroups, options = {}) {
  100. const jsonData = {
  101. groups: toPersistedGroups(groups),
  102. type: JSON_DOCUMENT_TYPE,
  103. version: JSON_SCHEMA_VERSION
  104. }
  105. if (options.includeExportedAt) {
  106. jsonData.exportedAt = new Date().toISOString()
  107. }
  108. return jsonData
  109. }
  110. function toJsonText(groups = state.genericModbusGroups, options = {}) {
  111. return JSON.stringify(toJsonData(groups, options), null, 2)
  112. }
  113. function parseJsonGroups(jsonText) {
  114. const parsed = typeof jsonText === 'string' ? JSON.parse(jsonText) : jsonText
  115. const groups = Array.isArray(parsed)
  116. ? parsed
  117. : (Array.isArray(parsed && parsed.groups) ? parsed.groups : parsed && parsed.genericModbusGroups)
  118. if (parsed && parsed.type && parsed.type !== JSON_DOCUMENT_TYPE) {
  119. throw new Error('JSON 文件不是通用Modbus配置')
  120. }
  121. if (parsed && parsed.version && parsed.version !== JSON_SCHEMA_VERSION) {
  122. throw new Error('JSON 版本不兼容')
  123. }
  124. if (!Array.isArray(groups)) {
  125. throw new Error('JSON 中没有找到寄存器组数组')
  126. }
  127. return groups
  128. }
  129. function readStoredGroups() {
  130. const wxApi = getWxApi()
  131. if (typeof wxApi.getStorageSync !== 'function') return []
  132. try {
  133. const jsonText = wxApi.getStorageSync(STORAGE_KEY)
  134. if (jsonText) return parseJsonGroups(jsonText).map(cloneImportedGroup)
  135. } catch (error) {
  136. return []
  137. }
  138. return []
  139. }
  140. function persistGroups() {
  141. const wxApi = getWxApi()
  142. if (typeof wxApi.setStorageSync !== 'function') return
  143. try {
  144. wxApi.setStorageSync(STORAGE_KEY, toJsonText())
  145. } catch (error) {}
  146. }
  147. function init() {
  148. if (initialized) return
  149. state = {
  150. ...state,
  151. genericModbusGroups: readStoredGroups().map(normalizeGroup)
  152. }
  153. initialized = true
  154. }
  155. function getState() {
  156. return {
  157. ...state,
  158. genericModbusDataTypeOptions: DATA_TYPE_OPTIONS,
  159. genericModbusRegisterTypeOptions: REGISTER_TYPE_OPTIONS
  160. }
  161. }
  162. function subscribe(subscriber) {
  163. if (typeof subscriber !== 'function') return () => {}
  164. init()
  165. subscribers.push(subscriber)
  166. subscriber(getState())
  167. return () => {
  168. const index = subscribers.indexOf(subscriber)
  169. if (index >= 0) subscribers.splice(index, 1)
  170. }
  171. }
  172. function getShareFileName() {
  173. return `generic-modbus-rtu-${formatExportStamp()}.json`
  174. }
  175. async function importJsonFromMessageFile() {
  176. try {
  177. const file = await loadSelectedFile('message', {
  178. encoding: 'utf8',
  179. extensionMessage: '请选择 .json 寄存器配置文件',
  180. extensions: ['json'],
  181. fallbackName: 'generic-modbus.json'
  182. })
  183. const jsonText = file.text
  184. const importedGroups = parseJsonGroups(jsonText).map(cloneImportedGroup).map(normalizeGroup)
  185. if (!importedGroups.length) throw new Error('JSON 中没有可导入的寄存器组')
  186. setState({
  187. genericModbusGroups: state.genericModbusGroups.concat(importedGroups)
  188. })
  189. return importedGroups.length
  190. } catch (error) {
  191. const message = error && error.message ? error.message : '导入通用Modbus配置失败'
  192. transport.showCommandAlert('通用Modbus导入', message)
  193. return 0
  194. }
  195. }
  196. async function saveJsonToChat() {
  197. try {
  198. if (!state.genericModbusGroups.length) {
  199. throw new Error('没有可保存的寄存器组')
  200. }
  201. const jsonText = toJsonText(state.genericModbusGroups, {
  202. includeExportedAt: true
  203. })
  204. await saveTextFileToChat(getShareFileName(), jsonText)
  205. return state.genericModbusGroups.length
  206. } catch (error) {
  207. const message = error && error.message ? error.message : '保存通用Modbus配置失败'
  208. if (!isCancelError(error)) {
  209. transport.showCommandAlert('通用Modbus保存', message)
  210. }
  211. return 0
  212. }
  213. }
  214. function addGroupFromConfig(config = {}) {
  215. let groupConfig
  216. try {
  217. groupConfig = normalizeGroupConfig(config)
  218. } catch (error) {
  219. transport.showCommandAlert('通用Modbus添加', error.message || '寄存器组配置无效')
  220. return null
  221. }
  222. if (isAddressRangeOverflow(groupConfig.startAddress, groupConfig.quantity)) {
  223. transport.showCommandAlert('通用Modbus添加', '地址范围超出 0xFFFF')
  224. return null
  225. }
  226. const registers = Array.isArray(config.registers) ? config.registers : []
  227. const group = normalizeGroup({
  228. ...groupConfig,
  229. layout: config.layout,
  230. ...(registers.length ? { registers } : {}),
  231. expanded: false
  232. })
  233. if (group.addressOverflow) {
  234. transport.showCommandAlert('通用Modbus添加', '地址范围超出 0xFFFF')
  235. return null
  236. }
  237. setState({
  238. genericModbusGroups: state.genericModbusGroups.concat(group)
  239. })
  240. return group
  241. }
  242. function updateGroupConfig(groupId, config = {}) {
  243. const group = findGroup(groupId)
  244. if (!group) return null
  245. let nextConfig
  246. try {
  247. nextConfig = normalizeGroupConfig({
  248. ...group,
  249. ...config
  250. })
  251. } catch (error) {
  252. transport.showCommandAlert('通用Modbus更新', error.message || '寄存器组配置无效')
  253. return null
  254. }
  255. if (isAddressRangeOverflow(nextConfig.startAddress, nextConfig.quantity)) {
  256. transport.showCommandAlert('通用Modbus更新', '地址范围超出 0xFFFF')
  257. return null
  258. }
  259. const registers = Array.isArray(config.registers) ? config.registers : group.registers
  260. const updatedGroup = normalizeGroup({
  261. ...group,
  262. ...nextConfig,
  263. registers
  264. })
  265. if (updatedGroup.addressOverflow) {
  266. transport.showCommandAlert('通用Modbus更新', '地址范围超出 0xFFFF')
  267. return null
  268. }
  269. setState({
  270. genericModbusGroups: state.genericModbusGroups.map((item) => (
  271. item.id === groupId ? updatedGroup : item
  272. ))
  273. })
  274. return updatedGroup
  275. }
  276. function updateGroups(mapper) {
  277. setState({
  278. genericModbusGroups: state.genericModbusGroups.map((group, index) => normalizeGroup(mapper(group, index)))
  279. })
  280. }
  281. function findGroup(groupId) {
  282. return state.genericModbusGroups.find((group) => group.id === groupId)
  283. }
  284. function setGroupExpanded(groupId, expanded) {
  285. updateGroups((group) => group.id === groupId
  286. ? {
  287. ...group,
  288. deleteVisible: false,
  289. expanded
  290. }
  291. : group)
  292. }
  293. function setGroupDeleteVisible(groupId, deleteVisible) {
  294. updateGroups((group) => group.id === groupId
  295. ? {
  296. ...group,
  297. deleteVisible
  298. }
  299. : group)
  300. }
  301. function removeGroup(groupId) {
  302. setState({
  303. genericModbusGroups: state.genericModbusGroups.filter((group) => group.id !== groupId)
  304. })
  305. }
  306. function reorderRegister(groupId, fromIndex, toIndex) {
  307. const group = findGroup(groupId)
  308. if (!group) return null
  309. if (group.isStructLayout) return group
  310. const registers = group.registers.slice()
  311. const sourceIndex = Number(fromIndex)
  312. const targetIndex = Number(toIndex)
  313. if (!Number.isInteger(sourceIndex) || !Number.isInteger(targetIndex)) return null
  314. if (sourceIndex < 0 || sourceIndex >= registers.length) return null
  315. const safeTargetIndex = Math.min(Math.max(targetIndex, 0), registers.length - 1)
  316. if (safeTargetIndex === sourceIndex) return group
  317. const moved = registers.splice(sourceIndex, 1)[0]
  318. registers.splice(safeTargetIndex, 0, moved)
  319. const updatedGroup = normalizeGroup({
  320. ...group,
  321. quantity: registers.length,
  322. registers
  323. })
  324. setState({
  325. genericModbusGroups: state.genericModbusGroups.map((item) => (
  326. item.id === groupId ? updatedGroup : item
  327. ))
  328. })
  329. return updatedGroup
  330. }
  331. function updateRegister(groupId, registerIndex, changedData) {
  332. updateGroups((group) => {
  333. if (group.id !== groupId) return group
  334. const shouldResetReadState = Object.prototype.hasOwnProperty.call(changedData, 'dataType')
  335. || Object.prototype.hasOwnProperty.call(changedData, 'textByteLength')
  336. return {
  337. ...group,
  338. registers: group.registers.map((register, currentIndex) => (
  339. currentIndex === registerIndex
  340. ? {
  341. ...register,
  342. ...(shouldResetReadState ? { rawValue: null, rawWords: [] } : {}),
  343. ...changedData
  344. }
  345. : register
  346. ))
  347. }
  348. })
  349. }
  350. function updateRegisterValue(groupId, registerIndex, value) {
  351. updateRegister(groupId, registerIndex, {
  352. inputValue: value,
  353. isDirty: true
  354. })
  355. }
  356. function validateRegisterInputValue(groupId, registerIndex, value) {
  357. const group = findGroup(groupId)
  358. if (!group) return false
  359. const register = group.registers[registerIndex]
  360. if (!register) return false
  361. return validateRegisterValue(register, value)
  362. }
  363. function parseStructDefinition(sourceText) {
  364. return parseStructDefinitionSource(sourceText)
  365. }
  366. async function readGroup(groupId, options = {}) {
  367. const group = findGroup(groupId)
  368. const slaveAddress = modbusClient.getSharedSlaveAddress()
  369. if (!group || slaveAddress === null) return false
  370. if (group.addressOverflow) {
  371. transport.showCommandAlert('通用Modbus读取', '寄存器地址范围超出 0xFFFF')
  372. return false
  373. }
  374. const totalQuantity = Math.max(1, group.wordQuantity || group.quantity || 0)
  375. const maxPacketLength = resolveMaxPacketLength(options.maxPacketLength)
  376. const wordCache = {}
  377. const values = await modbusClient.readSpans(
  378. slaveAddress,
  379. group.functionCode,
  380. [{
  381. address: group.startAddress,
  382. quantity: totalQuantity
  383. }],
  384. group.name || '通用Modbus读取',
  385. 'generic-modbus-read',
  386. {
  387. maxFrameBytes: maxPacketLength,
  388. showModal: options.showModal !== false
  389. }
  390. )
  391. if (!values) return false
  392. if (isBitRegisterType(group.registerType)) {
  393. Object.keys(values.coils || {}).forEach((addressText) => {
  394. wordCache[parseHexInteger(addressText)] = Number(values.coils[addressText]) ? 1 : 0
  395. })
  396. } else {
  397. Object.keys(values.words || {}).forEach((addressText) => {
  398. wordCache[parseHexInteger(addressText)] = Number(values.words[addressText]) & 0xFFFF
  399. })
  400. }
  401. updateGroups((item) => {
  402. if (item.id !== groupId) return item
  403. const nextRegisters = item.registers.map((register) => {
  404. const rawWords = registerTypeIsBit(register) ? [] : getRegisterWordsFromWordCache(register, wordCache)
  405. const rawValue = registerTypeIsBit(register)
  406. ? decodeRegisterFromWordCache(register, wordCache)
  407. : (rawWords ? decodeRegisterValue(register, rawWords) : null)
  408. const displayValue = rawValue === null || rawValue === undefined
  409. ? '--'
  410. : (registerTypeIsBit(register)
  411. ? formatCoilDisplayValue(rawValue)
  412. : formatRegisterValue(register, rawValue))
  413. return {
  414. ...register,
  415. displayValue,
  416. inputValue: item.writable ? displayValue : register.inputValue,
  417. isDirty: false,
  418. rawValue,
  419. rawWords: rawWords || []
  420. }
  421. })
  422. return {
  423. ...item,
  424. registers: nextRegisters
  425. }
  426. })
  427. return true
  428. }
  429. async function writeGroup(groupId) {
  430. const group = findGroup(groupId)
  431. const slaveAddress = modbusClient.getSharedSlaveAddress()
  432. const maxPacketLength = resolveMaxPacketLength()
  433. if (!group || slaveAddress === null) return false
  434. if (!group.writable) {
  435. transport.showCommandAlert('通用Modbus写入', '当前寄存器组为只读')
  436. return false
  437. }
  438. if (group.addressOverflow) {
  439. transport.showCommandAlert('通用Modbus写入', '寄存器地址范围超出 0xFFFF')
  440. return false
  441. }
  442. const writtenRegisters = []
  443. if (group.registerType === 'coil') {
  444. for (let index = 0; index < group.registers.length; index += 1) {
  445. const register = group.registers[index]
  446. const coilValue = parseCoilValue(getRegisterWriteValueText(register))
  447. if (coilValue === null) {
  448. transport.showCommandAlert('通用Modbus写入', `${register.name || `寄存器 ${index + 1}`} 没有有效写入值`)
  449. return false
  450. }
  451. const response = await modbusClient.writeSingleCoil(
  452. slaveAddress,
  453. group.startAddress + index,
  454. !!coilValue,
  455. register.name || group.name || '通用Modbus写入',
  456. 'generic-modbus-coil-write',
  457. {
  458. maxFrameBytes: maxPacketLength
  459. }
  460. )
  461. if (!response) return false
  462. writtenRegisters.push({
  463. rawValue: coilValue,
  464. rawWords: [],
  465. displayValue: formatCoilDisplayValue(coilValue)
  466. })
  467. }
  468. } else {
  469. let words
  470. try {
  471. words = group.isStructLayout
  472. ? getGroupEncodedWords(group)
  473. : Array.from({ length: Math.max(1, group.wordQuantity || 1) }, () => 0)
  474. if (!group.isStructLayout) {
  475. for (let index = 0; index < group.registers.length; index += 1) {
  476. const register = group.registers[index]
  477. const registerWords = getRegisterEncodedWords(register)
  478. if (!Array.isArray(registerWords) || !registerWords.length) {
  479. throw new Error(`${register.name || `寄存器 ${index + 1}`} 没有有效写入值`)
  480. }
  481. const dataType = getDataType(register.dataType).key
  482. const relativeAddress = Math.max(0, register.address - group.startAddress)
  483. if (isByteRegister(dataType)) {
  484. const byteValue = Number(registerWords[0]) & 0xFF
  485. const currentWord = words[relativeAddress] || 0
  486. words[relativeAddress] = register.byteOffset === 0
  487. ? (((byteValue << 8) | (currentWord & 0x00FF)) & 0xFFFF)
  488. : (((currentWord & 0xFF00) | byteValue) & 0xFFFF)
  489. } else {
  490. for (let offset = 0; offset < register.registerCount; offset += 1) {
  491. words[relativeAddress + offset] = Number(registerWords[offset]) & 0xFFFF
  492. }
  493. }
  494. }
  495. }
  496. } catch (error) {
  497. transport.showCommandAlert('通用Modbus写入', error.message || '寄存器组没有有效写入值')
  498. return false
  499. }
  500. const writtenWordCache = words.reduce((cache, word, offset) => {
  501. cache[group.startAddress + offset] = word
  502. return cache
  503. }, {})
  504. group.registers.forEach((register) => {
  505. const rawWords = getRegisterWordsFromWordCache(register, writtenWordCache) || []
  506. const rawValue = decodeRegisterValue(register, rawWords)
  507. const displayValue = formatRegisterValue(register, rawValue)
  508. writtenRegisters.push({
  509. rawWords,
  510. rawValue,
  511. displayValue
  512. })
  513. })
  514. const maxWriteQuantity = getWriteSpanMaxQuantity(words.length, maxPacketLength)
  515. const spans = splitWordSpans(group.startAddress, words.length, maxWriteQuantity)
  516. let cursor = 0
  517. for (const span of spans) {
  518. const spanWords = words.slice(cursor, cursor + span.quantity)
  519. cursor += span.quantity
  520. const response = await modbusClient.writeMultipleRegisters(
  521. slaveAddress,
  522. span.address,
  523. spanWords,
  524. group.name || '通用Modbus写入',
  525. 'generic-modbus-write',
  526. {
  527. maxFrameBytes: maxPacketLength
  528. }
  529. )
  530. if (!response) return false
  531. }
  532. }
  533. updateGroups((item) => {
  534. if (item.id !== groupId) return item
  535. let writtenIndex = 0
  536. return {
  537. ...item,
  538. registers: item.registers.map((register) => {
  539. const written = writtenRegisters[writtenIndex] || {}
  540. writtenIndex += 1
  541. const hasDisplayValue = Object.prototype.hasOwnProperty.call(written, 'displayValue')
  542. const hasRawValue = Object.prototype.hasOwnProperty.call(written, 'rawValue')
  543. const hasRawWords = Object.prototype.hasOwnProperty.call(written, 'rawWords')
  544. return {
  545. ...register,
  546. displayValue: hasDisplayValue ? written.displayValue : register.displayValue,
  547. inputValue: hasDisplayValue ? written.displayValue : register.inputValue,
  548. isDirty: false,
  549. rawValue: hasRawValue ? written.rawValue : register.rawValue,
  550. rawWords: hasRawWords ? written.rawWords : register.rawWords
  551. }
  552. })
  553. }
  554. })
  555. return true
  556. }
  557. module.exports = {
  558. DATA_TYPE_OPTIONS,
  559. REGISTER_TYPE_OPTIONS,
  560. addGroupFromConfig,
  561. getState,
  562. importJsonFromMessageFile,
  563. init,
  564. parseStructDefinition,
  565. readGroup,
  566. removeGroup,
  567. reorderRegister,
  568. saveJsonToChat,
  569. setGroupDeleteVisible,
  570. setGroupExpanded,
  571. subscribe,
  572. updateGroupConfig,
  573. updateRegister,
  574. updateRegisterValue,
  575. validateRegisterInputValue,
  576. writeGroup
  577. }