io.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. const {
  2. parseHexInteger
  3. } = require('../../utils/base-utils.js')
  4. const {
  5. bytesToWords
  6. } = require('../../utils/binary-utils.js')
  7. const transport = require('../../transport/ble-core.js')
  8. const modbusClient = require('../modbus-rtu/service.js')
  9. const storageAccessService = require('../storage-access/service.js')
  10. const {
  11. decodeRegisterFromByteCache,
  12. decodeRegisterFromWordCache,
  13. decodeRegisterValue,
  14. formatCoilDisplayValue,
  15. formatRegisterValue,
  16. getDataType,
  17. getGroupEncodedBytes,
  18. getGroupEncodedWords,
  19. getRegisterBytesFromByteCache,
  20. getRegisterEncodedBytes,
  21. getRegisterEncodedWords,
  22. getRegisterWordsFromByteCache,
  23. getRegisterWordsFromWordCache,
  24. getRegisterWriteValueText,
  25. isBitRegisterType,
  26. isByteRegister,
  27. normalizeRegister,
  28. parseCoilValue,
  29. registerTypeIsBit,
  30. splitWordSpans
  31. } = require('../../domain/parameter-groups/model.js')
  32. const STORAGE_ACCESS_CODE_AREA = storageAccessService.AREA.CODE
  33. const MAX_STORAGE_ACCESS_ADDRESS = 0xFFFFFFFF
  34. const MAX_STORAGE_ACCESS_BYTE_LENGTH = 0xFFFFFFFF
  35. function getMemoryType(group = {}) {
  36. const memoryArea = String(group.sourceMemoryArea || '').trim().toUpperCase()
  37. if (memoryArea === 'ADDR32' || memoryArea === 'ADDRESS32') return storageAccessService.AREA.ADDR32
  38. if (memoryArea === 'BIT') return storageAccessService.AREA.DATA
  39. const area = storageAccessService.AREA[memoryArea]
  40. return area || null
  41. }
  42. function isMemoryGroup(group = {}) {
  43. return getMemoryType(group) !== null
  44. }
  45. function isByteAddressedGroup(group = {}) {
  46. return group.addressUnit === 'byte' || group.addressUnit === 'bytes' || isMemoryGroup(group)
  47. }
  48. function getMemoryAddress(group = {}) {
  49. const sourceAddress = Number(group.sourceAddress)
  50. if (Number.isFinite(sourceAddress)) return Math.min(MAX_STORAGE_ACCESS_ADDRESS, Math.max(0, Math.floor(sourceAddress)))
  51. return Math.min(MAX_STORAGE_ACCESS_ADDRESS, Math.max(0, Math.floor(Number(group.startAddress) || 0)))
  52. }
  53. function getMemoryByteLength(group = {}) {
  54. const sourceByteLength = Number(group.sourceByteLength)
  55. if (Number.isFinite(sourceByteLength) && sourceByteLength > 0) return Math.min(MAX_STORAGE_ACCESS_BYTE_LENGTH, Math.ceil(sourceByteLength))
  56. const byteLength = Number(group.byteLength)
  57. if (Number.isFinite(byteLength) && byteLength > 0) return Math.min(MAX_STORAGE_ACCESS_BYTE_LENGTH, Math.ceil(byteLength))
  58. return Math.max(1, Math.ceil((Number(group.wordQuantity) || 1) * 2))
  59. }
  60. function shouldUseStorageAccess(options = {}, group = {}) {
  61. return !!options.useStorageAccess && isMemoryGroup(group)
  62. }
  63. function getStorageMaxPacketLength(group = {}, options = {}) {
  64. const configuredMaxPacketLength = storageAccessService.resolveMaxPacketLength(options.maxPacketLength)
  65. const deviceMaxPacketLength = Number(group.codeInfoContext && group.codeInfoContext.maxPacketLength)
  66. if (!Number.isFinite(deviceMaxPacketLength) || deviceMaxPacketLength <= 0) return configuredMaxPacketLength
  67. if (configuredMaxPacketLength === 0) return Math.round(deviceMaxPacketLength)
  68. return Math.min(configuredMaxPacketLength, Math.round(deviceMaxPacketLength))
  69. }
  70. function normalizeNumericCache(source = {}) {
  71. const cache = {}
  72. Object.keys(source || {}).forEach((addressText) => {
  73. const numericAddress = Number(addressText)
  74. if (Number.isFinite(numericAddress)) {
  75. cache[numericAddress] = Number(source[addressText]) & 0xFFFF
  76. }
  77. })
  78. return cache
  79. }
  80. function createRegisterStateFromCache(group, register, wordCache) {
  81. const rawBytes = registerTypeIsBit(register)
  82. ? []
  83. : (isByteAddressedGroup(group) ? getRegisterBytesFromByteCache(register, wordCache) : [])
  84. const rawWords = registerTypeIsBit(register)
  85. ? []
  86. : (isByteAddressedGroup(group)
  87. ? getRegisterWordsFromByteCache(register, wordCache)
  88. : getRegisterWordsFromWordCache(register, wordCache))
  89. const rawValue = registerTypeIsBit(register)
  90. ? decodeRegisterFromWordCache(register, wordCache)
  91. : (isByteAddressedGroup(group)
  92. ? decodeRegisterFromByteCache(register, wordCache)
  93. : (rawWords ? decodeRegisterValue(register, rawWords) : null))
  94. const displayValue = rawValue === null || rawValue === undefined
  95. ? '--'
  96. : (registerTypeIsBit(register)
  97. ? formatCoilDisplayValue(rawValue)
  98. : formatRegisterValue(register, rawValue))
  99. const preNormalizedRegister = {
  100. ...register,
  101. displayValue,
  102. isDirty: false,
  103. rawBytes: rawBytes || [],
  104. rawValue,
  105. rawWords: rawWords || []
  106. }
  107. const nextRegister = normalizeRegister(preNormalizedRegister, group, 0, register.address, register.byteOffset)
  108. return {
  109. ...nextRegister,
  110. id: register.id,
  111. inputValue: group.writable ? displayValue : register.inputValue
  112. }
  113. }
  114. function applyReadCacheToGroup(group, wordCache) {
  115. return {
  116. ...group,
  117. registers: group.registers.map((register) => createRegisterStateFromCache(group, register, wordCache))
  118. }
  119. }
  120. function applyWrittenSnapshotToRegister(register, snapshot = {}) {
  121. const hasDisplayValue = Object.prototype.hasOwnProperty.call(snapshot, 'displayValue')
  122. const hasRawBytes = Object.prototype.hasOwnProperty.call(snapshot, 'rawBytes')
  123. const hasRawValue = Object.prototype.hasOwnProperty.call(snapshot, 'rawValue')
  124. const hasRawWords = Object.prototype.hasOwnProperty.call(snapshot, 'rawWords')
  125. return {
  126. ...register,
  127. displayValue: hasDisplayValue ? snapshot.displayValue : register.displayValue,
  128. inputValue: hasDisplayValue ? snapshot.displayValue : register.inputValue,
  129. isDirty: false,
  130. rawBytes: hasRawBytes ? snapshot.rawBytes : register.rawBytes,
  131. rawValue: hasRawValue ? snapshot.rawValue : register.rawValue,
  132. rawWords: hasRawWords ? snapshot.rawWords : register.rawWords
  133. }
  134. }
  135. function applyWrittenSnapshotsToGroup(group, snapshots = []) {
  136. let writtenIndex = 0
  137. return {
  138. ...group,
  139. registers: group.registers.map((register, index) => {
  140. const snapshot = snapshots[writtenIndex] || {}
  141. writtenIndex += 1
  142. const nextRegister = applyWrittenSnapshotToRegister(register, snapshot)
  143. const normalized = normalizeRegister(nextRegister, group, index, nextRegister.address, nextRegister.byteOffset)
  144. return {
  145. ...normalized,
  146. id: register.id,
  147. inputValue: group.writable ? nextRegister.displayValue : nextRegister.inputValue
  148. }
  149. })
  150. }
  151. }
  152. function applyWrittenSnapshotToGroupRegister(group, registerIndex, snapshot) {
  153. return {
  154. ...group,
  155. registers: group.registers.map((register, currentIndex) => (
  156. currentIndex === registerIndex
  157. ? (() => {
  158. const nextRegister = applyWrittenSnapshotToRegister(register, snapshot || {})
  159. const normalized = normalizeRegister(nextRegister, group, currentIndex, nextRegister.address, nextRegister.byteOffset)
  160. return {
  161. ...normalized,
  162. id: register.id,
  163. inputValue: group.writable ? nextRegister.displayValue : nextRegister.inputValue
  164. }
  165. })()
  166. : register
  167. ))
  168. }
  169. }
  170. function getWriteSpanMaxQuantity(totalQuantity, maxPacketLength) {
  171. if (maxPacketLength === 0) return Math.max(1, totalQuantity)
  172. return Math.max(1, modbusClient.getMaxWriteMultipleRegisterQuantity(maxPacketLength))
  173. }
  174. async function readModbusGroup(group, options = {}) {
  175. const totalQuantity = Math.max(1, group.wordQuantity || group.quantity || 0)
  176. const wordCache = {}
  177. const slaveAddress = modbusClient.getSharedSlaveAddress()
  178. if (slaveAddress === null) return null
  179. const values = await modbusClient.readSpans(
  180. slaveAddress,
  181. group.functionCode,
  182. [{
  183. address: group.startAddress,
  184. quantity: totalQuantity
  185. }],
  186. group.name || '参数组读取',
  187. 'parameter-group-read',
  188. {
  189. maxFrameBytes: options.maxPacketLength,
  190. showModal: options.showModal !== false
  191. }
  192. )
  193. if (!values) return null
  194. if (isBitRegisterType(group.registerType)) {
  195. Object.keys(values.coils || {}).forEach((addressText) => {
  196. wordCache[parseHexInteger(addressText)] = Number(values.coils[addressText]) ? 1 : 0
  197. })
  198. } else {
  199. Object.keys(values.words || {}).forEach((addressText) => {
  200. wordCache[parseHexInteger(addressText)] = Number(values.words[addressText]) & 0xFFFF
  201. })
  202. }
  203. return wordCache
  204. }
  205. function getRegisterWordsForModbusWrite(group) {
  206. const words = group.isStructLayout
  207. ? getGroupEncodedWords(group)
  208. : Array.from({ length: Math.max(1, group.wordQuantity || 1) }, () => 0)
  209. if (group.isStructLayout) return words
  210. for (let index = 0; index < group.registers.length; index += 1) {
  211. const register = group.registers[index]
  212. const registerWords = getRegisterEncodedWords(register)
  213. if (!Array.isArray(registerWords) || !registerWords.length) {
  214. throw new Error(`${register.name || `寄存器 ${index + 1}`} 没有有效写入值`)
  215. }
  216. const dataType = getDataType(register.dataType).key
  217. const relativeAddress = Math.max(0, register.address - group.startAddress)
  218. if (isByteRegister(dataType)) {
  219. const byteValue = Number(registerWords[0]) & 0xFF
  220. const currentWord = words[relativeAddress] || 0
  221. words[relativeAddress] = register.byteOffset === 0
  222. ? (((byteValue << 8) | (currentWord & 0x00FF)) & 0xFFFF)
  223. : (((currentWord & 0xFF00) | byteValue) & 0xFFFF)
  224. } else {
  225. for (let offset = 0; offset < register.registerCount; offset += 1) {
  226. words[relativeAddress + offset] = Number(registerWords[offset]) & 0xFFFF
  227. }
  228. }
  229. }
  230. return words
  231. }
  232. function createModbusWrittenRegisterSnapshots(group, words = []) {
  233. const writtenWordCache = words.reduce((cache, word, offset) => {
  234. cache[group.startAddress + offset] = word
  235. return cache
  236. }, {})
  237. return group.registers.map((register) => {
  238. const rawWords = getRegisterWordsFromWordCache(register, writtenWordCache) || []
  239. const rawValue = decodeRegisterValue(register, rawWords)
  240. const displayValue = formatRegisterValue(register, rawValue)
  241. return {
  242. rawWords,
  243. rawValue,
  244. displayValue
  245. }
  246. })
  247. }
  248. async function writeModbusCoilGroup(group, options = {}) {
  249. const slaveAddress = modbusClient.getSharedSlaveAddress()
  250. if (slaveAddress === null) return null
  251. const writtenRegisters = []
  252. for (let index = 0; index < group.registers.length; index += 1) {
  253. const register = group.registers[index]
  254. const coilValue = parseCoilValue(getRegisterWriteValueText(register))
  255. if (coilValue === null) {
  256. transport.showCommandAlert('参数组写入', `${register.name || `寄存器 ${index + 1}`} 没有有效写入值`)
  257. return null
  258. }
  259. const response = await modbusClient.writeSingleCoil(
  260. slaveAddress,
  261. group.startAddress + index,
  262. !!coilValue,
  263. register.name || group.name || '参数组写入',
  264. 'parameter-group-coil-write',
  265. {
  266. maxFrameBytes: options.maxPacketLength
  267. }
  268. )
  269. if (!response) return null
  270. writtenRegisters.push({
  271. rawBytes: [],
  272. rawValue: coilValue,
  273. rawWords: [],
  274. displayValue: formatCoilDisplayValue(coilValue)
  275. })
  276. }
  277. return writtenRegisters
  278. }
  279. async function writeModbusRegisterGroup(group, options = {}) {
  280. const slaveAddress = modbusClient.getSharedSlaveAddress()
  281. if (slaveAddress === null) return null
  282. let words
  283. try {
  284. words = getRegisterWordsForModbusWrite(group)
  285. } catch (error) {
  286. transport.showCommandAlert('参数组写入', error.message || '寄存器组没有有效写入值')
  287. return null
  288. }
  289. const writtenRegisters = createModbusWrittenRegisterSnapshots(group, words)
  290. const maxWriteQuantity = getWriteSpanMaxQuantity(words.length, options.maxPacketLength)
  291. const spans = splitWordSpans(group.startAddress, words.length, maxWriteQuantity)
  292. let cursor = 0
  293. for (const span of spans) {
  294. const spanWords = words.slice(cursor, cursor + span.quantity)
  295. cursor += span.quantity
  296. const response = await modbusClient.writeMultipleRegisters(
  297. slaveAddress,
  298. span.address,
  299. spanWords,
  300. group.name || '参数组写入',
  301. 'parameter-group-write',
  302. {
  303. maxFrameBytes: options.maxPacketLength
  304. }
  305. )
  306. if (!response) return null
  307. }
  308. return writtenRegisters
  309. }
  310. async function writeModbusGroup(group, options = {}) {
  311. return group.registerType === 'coil'
  312. ? writeModbusCoilGroup(group, options)
  313. : writeModbusRegisterGroup(group, options)
  314. }
  315. function bytesToPaddedWords(bytes = []) {
  316. return bytesToWords(bytes.length % 2 === 0 ? bytes : bytes.concat(0))
  317. }
  318. function fillByteCacheFromBytes(byteCache, startAddress, bytes = []) {
  319. bytes.forEach((byte, offset) => {
  320. byteCache[startAddress + offset] = Number(byte) & 0xFF
  321. })
  322. }
  323. function fillWordCacheFromBytes(wordCache, startAddress, bytes = []) {
  324. const words = bytesToPaddedWords(bytes)
  325. words.forEach((word, offset) => {
  326. wordCache[startAddress + offset] = Number(word) & 0xFFFF
  327. })
  328. }
  329. function createStorageWrittenRegisterSnapshots(group, wordCache) {
  330. return group.registers.map((register) => {
  331. const rawBytes = isByteAddressedGroup(group)
  332. ? (getRegisterBytesFromByteCache(register, wordCache) || [])
  333. : []
  334. const rawWords = isByteAddressedGroup(group)
  335. ? (getRegisterWordsFromByteCache(register, wordCache) || [])
  336. : (getRegisterWordsFromWordCache(register, wordCache) || [])
  337. const rawValue = isByteAddressedGroup(group)
  338. ? decodeRegisterFromByteCache(register, wordCache)
  339. : decodeRegisterValue(register, rawWords)
  340. return {
  341. rawBytes,
  342. rawWords,
  343. rawValue,
  344. displayValue: formatRegisterValue(register, rawValue)
  345. }
  346. })
  347. }
  348. function createStorageWrittenRegisterSnapshot(group, register, byteCache) {
  349. const snapshots = createStorageWrittenRegisterSnapshots({
  350. ...group,
  351. registers: [register]
  352. }, byteCache)
  353. return snapshots[0] || null
  354. }
  355. function groupHasBitFields(group = {}) {
  356. return (Array.isArray(group.registers) ? group.registers : []).some((register) => !!register.isBitField)
  357. }
  358. async function writeMemoryRegister(group, register, options = {}) {
  359. const memoryType = getMemoryType(group)
  360. const maxPacketLength = getStorageMaxPacketLength(group, options)
  361. const byteLength = Math.max(1, Number(register.byteLength) || 1)
  362. const address = Math.min(MAX_STORAGE_ACCESS_ADDRESS, Math.max(0, Math.floor(Number(register.address) || getMemoryAddress(group))))
  363. let bytes
  364. if (memoryType === null) {
  365. transport.showCommandAlert('内存写入', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`)
  366. return null
  367. }
  368. if (memoryType === STORAGE_ACCESS_CODE_AREA) {
  369. transport.showCommandAlert('内存写入', 'code 区暂不支持写入')
  370. return null
  371. }
  372. try {
  373. if (register.isBitField) {
  374. const baseBytes = await storageAccessService.readMemory(
  375. memoryType,
  376. address,
  377. byteLength,
  378. register.name ? `${register.name} 读改写` : '变量读改写',
  379. 'parameter-group-memory-register-rmw-read',
  380. {
  381. maxFrameBytes: maxPacketLength
  382. }
  383. )
  384. if (!baseBytes) return null
  385. bytes = getGroupEncodedBytes({
  386. ...group,
  387. paddedByteLength: byteLength,
  388. registers: [{
  389. ...register,
  390. address,
  391. byteStart: 0
  392. }]
  393. }, baseBytes).slice(0, byteLength)
  394. } else {
  395. bytes = getRegisterEncodedBytes(register)
  396. }
  397. } catch (error) {
  398. transport.showCommandAlert('内存写入', error.message || '变量没有有效写入值')
  399. return null
  400. }
  401. if (!Array.isArray(bytes) || !bytes.length) {
  402. transport.showCommandAlert('内存写入', `${register.name || '变量'} 没有有效写入值`)
  403. return null
  404. }
  405. bytes = bytes.slice(0, byteLength)
  406. while (bytes.length < byteLength) bytes.push(0)
  407. const ok = await storageAccessService.writeMemory(
  408. memoryType,
  409. address,
  410. bytes,
  411. register.name || group.name || '变量写入',
  412. 'parameter-group-memory-register-write',
  413. {
  414. maxFrameBytes: maxPacketLength
  415. }
  416. )
  417. if (!ok) return null
  418. const byteCache = {}
  419. fillByteCacheFromBytes(byteCache, address, bytes)
  420. return createStorageWrittenRegisterSnapshot(group, register, byteCache)
  421. }
  422. async function readMemoryGroup(group, options = {}) {
  423. const memoryType = getMemoryType(group)
  424. const address = getMemoryAddress(group)
  425. const byteLength = getMemoryByteLength(group)
  426. const maxPacketLength = getStorageMaxPacketLength(group, options)
  427. if (memoryType === null) {
  428. transport.showCommandAlert('内存读取', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`)
  429. return null
  430. }
  431. const bytes = await storageAccessService.readMemory(
  432. memoryType,
  433. address,
  434. byteLength,
  435. group.name || '内存读取',
  436. 'parameter-group-memory-read',
  437. {
  438. maxFrameBytes: maxPacketLength,
  439. showModal: options.showModal !== false
  440. }
  441. )
  442. if (!bytes) return null
  443. const wordCache = {}
  444. if (isByteAddressedGroup(group)) {
  445. fillByteCacheFromBytes(wordCache, address, bytes)
  446. } else {
  447. fillWordCacheFromBytes(wordCache, address, bytes)
  448. }
  449. return wordCache
  450. }
  451. async function writeMemoryGroup(group, options = {}) {
  452. const memoryType = getMemoryType(group)
  453. const address = getMemoryAddress(group)
  454. const byteLength = getMemoryByteLength(group)
  455. const maxPacketLength = getStorageMaxPacketLength(group, options)
  456. let bytes
  457. if (memoryType === null) {
  458. transport.showCommandAlert('内存写入', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`)
  459. return null
  460. }
  461. if (memoryType === STORAGE_ACCESS_CODE_AREA) {
  462. transport.showCommandAlert('内存写入', 'code 区暂不支持写入')
  463. return null
  464. }
  465. try {
  466. let baseBytes = null
  467. if (groupHasBitFields(group)) {
  468. baseBytes = await storageAccessService.readMemory(
  469. memoryType,
  470. address,
  471. byteLength,
  472. group.name ? `${group.name} 读改写` : '内存读改写',
  473. 'parameter-group-memory-rmw-read',
  474. {
  475. maxFrameBytes: maxPacketLength
  476. }
  477. )
  478. if (!baseBytes) return null
  479. }
  480. bytes = getGroupEncodedBytes(group, baseBytes)
  481. } catch (error) {
  482. transport.showCommandAlert('内存写入', error.message || '寄存器组没有有效写入值')
  483. return null
  484. }
  485. bytes = bytes.slice(0, byteLength)
  486. const ok = await storageAccessService.writeMemory(
  487. memoryType,
  488. address,
  489. bytes,
  490. group.name || '内存写入',
  491. 'parameter-group-memory-write',
  492. {
  493. maxFrameBytes: maxPacketLength
  494. }
  495. )
  496. if (!ok) return null
  497. const wordCache = {}
  498. if (isByteAddressedGroup(group)) {
  499. fillByteCacheFromBytes(wordCache, address, bytes)
  500. } else {
  501. fillWordCacheFromBytes(wordCache, address, bytes)
  502. }
  503. return createStorageWrittenRegisterSnapshots(group, wordCache)
  504. }
  505. async function readGroup(group, options = {}) {
  506. const wordCache = shouldUseStorageAccess(options, group)
  507. ? await readMemoryGroup(group, options)
  508. : await readModbusGroup(group, options)
  509. if (!wordCache) return null
  510. const normalizedCache = normalizeNumericCache(wordCache)
  511. return {
  512. applyToGroup(currentGroup) {
  513. return applyReadCacheToGroup(currentGroup, normalizedCache)
  514. }
  515. }
  516. }
  517. async function writeRegister(group, registerIndex, options = {}) {
  518. const register = group && Array.isArray(group.registers) ? group.registers[registerIndex] : null
  519. if (!register) return null
  520. if (shouldUseStorageAccess(options, group)) {
  521. const written = await writeMemoryRegister(group, register, options)
  522. return written
  523. ? {
  524. applyToGroup(currentGroup) {
  525. return applyWrittenSnapshotToGroupRegister(currentGroup, registerIndex, written)
  526. }
  527. }
  528. : null
  529. }
  530. return writeGroup(group, options)
  531. }
  532. async function writeGroup(group, options = {}) {
  533. const snapshots = shouldUseStorageAccess(options, group)
  534. ? await writeMemoryGroup(group, options)
  535. : await writeModbusGroup(group, options)
  536. if (!snapshots) return null
  537. return {
  538. applyToGroup(currentGroup) {
  539. return applyWrittenSnapshotsToGroup(currentGroup, snapshots)
  540. }
  541. }
  542. }
  543. module.exports = {
  544. getMemoryAddress,
  545. getMemoryByteLength,
  546. getMemoryType,
  547. isByteAddressedGroup,
  548. isMemoryGroup,
  549. readGroup,
  550. writeGroup,
  551. writeRegister
  552. }