generic-modbus-service.js 17 KB

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