io.js 20 KB

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