| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648 |
- const {
- parseHexInteger
- } = require('../../utils/base-utils.js')
- const {
- bytesToWords
- } = require('../../utils/binary-utils.js')
- const transport = require('../../transport/ble-core.js')
- const modbusClient = require('../modbus-rtu/service.js')
- const storageAccessService = require('../storage-access/service.js')
- const {
- decodeRegisterFromByteCache,
- decodeRegisterFromWordCache,
- decodeRegisterValue,
- formatCoilDisplayValue,
- formatRegisterValue,
- getDataType,
- getGroupEncodedBytes,
- getGroupEncodedWords,
- getRegisterBytesFromByteCache,
- getRegisterEncodedBytes,
- getRegisterEncodedWords,
- getRegisterWordsFromByteCache,
- getRegisterWordsFromWordCache,
- getRegisterWriteValueText,
- isBitRegisterType,
- isByteRegister,
- normalizeRegister,
- parseCoilValue,
- registerTypeIsBit,
- splitWordSpans
- } = require('../../domain/parameter-groups/model.js')
- const STORAGE_ACCESS_CODE_AREA = storageAccessService.AREA.CODE
- const MAX_STORAGE_ACCESS_ADDRESS = 0xFFFFFFFF
- const MAX_STORAGE_ACCESS_BYTE_LENGTH = 0xFFFFFFFF
- function getMemoryType(group = {}) {
- const memoryArea = String(group.sourceMemoryArea || '').trim().toUpperCase()
- if (memoryArea === 'ADDR32' || memoryArea === 'ADDRESS32') return storageAccessService.AREA.ADDR32
- if (memoryArea === 'BIT') return storageAccessService.AREA.DATA
- const area = storageAccessService.AREA[memoryArea]
- return area || null
- }
- function isMemoryGroup(group = {}) {
- return getMemoryType(group) !== null
- }
- function isByteAddressedGroup(group = {}) {
- return group.addressUnit === 'byte' || group.addressUnit === 'bytes' || isMemoryGroup(group)
- }
- function getMemoryAddress(group = {}) {
- const sourceAddress = Number(group.sourceAddress)
- if (Number.isFinite(sourceAddress)) return Math.min(MAX_STORAGE_ACCESS_ADDRESS, Math.max(0, Math.floor(sourceAddress)))
- return Math.min(MAX_STORAGE_ACCESS_ADDRESS, Math.max(0, Math.floor(Number(group.startAddress) || 0)))
- }
- function getMemoryByteLength(group = {}) {
- const sourceByteLength = Number(group.sourceByteLength)
- if (Number.isFinite(sourceByteLength) && sourceByteLength > 0) return Math.min(MAX_STORAGE_ACCESS_BYTE_LENGTH, Math.ceil(sourceByteLength))
- const byteLength = Number(group.byteLength)
- if (Number.isFinite(byteLength) && byteLength > 0) return Math.min(MAX_STORAGE_ACCESS_BYTE_LENGTH, Math.ceil(byteLength))
- return Math.max(1, Math.ceil((Number(group.wordQuantity) || 1) * 2))
- }
- function shouldUseStorageAccess(options = {}, group = {}) {
- return !!options.useStorageAccess && isMemoryGroup(group)
- }
- function getStorageMaxPacketLength(group = {}, options = {}) {
- const configuredMaxPacketLength = storageAccessService.resolveMaxPacketLength(options.maxPacketLength)
- const deviceMaxPacketLength = Number(group.codeInfoContext && group.codeInfoContext.maxPacketLength)
- if (!Number.isFinite(deviceMaxPacketLength) || deviceMaxPacketLength <= 0) return configuredMaxPacketLength
- if (configuredMaxPacketLength === 0) return Math.round(deviceMaxPacketLength)
- return Math.min(configuredMaxPacketLength, Math.round(deviceMaxPacketLength))
- }
- function normalizeNumericCache(source = {}) {
- const cache = {}
- Object.keys(source || {}).forEach((addressText) => {
- const numericAddress = Number(addressText)
- if (Number.isFinite(numericAddress)) {
- cache[numericAddress] = Number(source[addressText]) & 0xFFFF
- }
- })
- return cache
- }
- function createRegisterStateFromCache(group, register, wordCache) {
- const rawBytes = registerTypeIsBit(register)
- ? []
- : (isByteAddressedGroup(group) ? getRegisterBytesFromByteCache(register, wordCache) : [])
- const rawWords = registerTypeIsBit(register)
- ? []
- : (isByteAddressedGroup(group)
- ? getRegisterWordsFromByteCache(register, wordCache)
- : getRegisterWordsFromWordCache(register, wordCache))
- const rawValue = registerTypeIsBit(register)
- ? decodeRegisterFromWordCache(register, wordCache)
- : (isByteAddressedGroup(group)
- ? decodeRegisterFromByteCache(register, wordCache)
- : (rawWords ? decodeRegisterValue(register, rawWords) : null))
- const displayValue = rawValue === null || rawValue === undefined
- ? '--'
- : (registerTypeIsBit(register)
- ? formatCoilDisplayValue(rawValue)
- : formatRegisterValue(register, rawValue))
- const preNormalizedRegister = {
- ...register,
- displayValue,
- isDirty: false,
- rawBytes: rawBytes || [],
- rawValue,
- rawWords: rawWords || []
- }
- const nextRegister = normalizeRegister(preNormalizedRegister, group, 0, register.address, register.byteOffset)
- return {
- ...nextRegister,
- id: register.id,
- inputValue: group.writable ? displayValue : register.inputValue
- }
- }
- function applyReadCacheToGroup(group, wordCache) {
- return {
- ...group,
- registers: group.registers.map((register) => createRegisterStateFromCache(group, register, wordCache))
- }
- }
- function applyWrittenSnapshotToRegister(register, snapshot = {}) {
- const hasDisplayValue = Object.prototype.hasOwnProperty.call(snapshot, 'displayValue')
- const hasRawBytes = Object.prototype.hasOwnProperty.call(snapshot, 'rawBytes')
- const hasRawValue = Object.prototype.hasOwnProperty.call(snapshot, 'rawValue')
- const hasRawWords = Object.prototype.hasOwnProperty.call(snapshot, 'rawWords')
- return {
- ...register,
- displayValue: hasDisplayValue ? snapshot.displayValue : register.displayValue,
- inputValue: hasDisplayValue ? snapshot.displayValue : register.inputValue,
- isDirty: false,
- rawBytes: hasRawBytes ? snapshot.rawBytes : register.rawBytes,
- rawValue: hasRawValue ? snapshot.rawValue : register.rawValue,
- rawWords: hasRawWords ? snapshot.rawWords : register.rawWords
- }
- }
- function applyWrittenSnapshotsToGroup(group, snapshots = []) {
- let writtenIndex = 0
- return {
- ...group,
- registers: group.registers.map((register, index) => {
- const snapshot = snapshots[writtenIndex] || {}
- writtenIndex += 1
- const nextRegister = applyWrittenSnapshotToRegister(register, snapshot)
- const normalized = normalizeRegister(nextRegister, group, index, nextRegister.address, nextRegister.byteOffset)
- return {
- ...normalized,
- id: register.id,
- inputValue: group.writable ? nextRegister.displayValue : nextRegister.inputValue
- }
- })
- }
- }
- function applyWrittenSnapshotToGroupRegister(group, registerIndex, snapshot) {
- return {
- ...group,
- registers: group.registers.map((register, currentIndex) => (
- currentIndex === registerIndex
- ? (() => {
- const nextRegister = applyWrittenSnapshotToRegister(register, snapshot || {})
- const normalized = normalizeRegister(nextRegister, group, currentIndex, nextRegister.address, nextRegister.byteOffset)
- return {
- ...normalized,
- id: register.id,
- inputValue: group.writable ? nextRegister.displayValue : nextRegister.inputValue
- }
- })()
- : register
- ))
- }
- }
- function getWriteSpanMaxQuantity(totalQuantity, maxPacketLength) {
- if (maxPacketLength === 0) return Math.max(1, totalQuantity)
- return Math.max(1, modbusClient.getMaxWriteMultipleRegisterQuantity(maxPacketLength))
- }
- async function readModbusGroup(group, options = {}) {
- const totalQuantity = Math.max(1, group.wordQuantity || group.quantity || 0)
- const wordCache = {}
- const slaveAddress = modbusClient.getSharedSlaveAddress()
- if (slaveAddress === null) return null
- const values = await modbusClient.readSpans(
- slaveAddress,
- group.functionCode,
- [{
- address: group.startAddress,
- quantity: totalQuantity
- }],
- group.name || '参数组读取',
- 'parameter-group-read',
- {
- maxFrameBytes: options.maxPacketLength,
- showModal: options.showModal !== false
- }
- )
- if (!values) return null
- if (isBitRegisterType(group.registerType)) {
- Object.keys(values.coils || {}).forEach((addressText) => {
- wordCache[parseHexInteger(addressText)] = Number(values.coils[addressText]) ? 1 : 0
- })
- } else {
- Object.keys(values.words || {}).forEach((addressText) => {
- wordCache[parseHexInteger(addressText)] = Number(values.words[addressText]) & 0xFFFF
- })
- }
- return wordCache
- }
- function getRegisterWordsForModbusWrite(group) {
- const words = group.isStructLayout
- ? getGroupEncodedWords(group)
- : Array.from({ length: Math.max(1, group.wordQuantity || 1) }, () => 0)
- if (group.isStructLayout) return words
- for (let index = 0; index < group.registers.length; index += 1) {
- const register = group.registers[index]
- const registerWords = getRegisterEncodedWords(register)
- if (!Array.isArray(registerWords) || !registerWords.length) {
- throw new Error(`${register.name || `寄存器 ${index + 1}`} 没有有效写入值`)
- }
- const dataType = getDataType(register.dataType).key
- const relativeAddress = Math.max(0, register.address - group.startAddress)
- if (isByteRegister(dataType)) {
- const byteValue = Number(registerWords[0]) & 0xFF
- const currentWord = words[relativeAddress] || 0
- words[relativeAddress] = register.byteOffset === 0
- ? (((byteValue << 8) | (currentWord & 0x00FF)) & 0xFFFF)
- : (((currentWord & 0xFF00) | byteValue) & 0xFFFF)
- } else {
- for (let offset = 0; offset < register.registerCount; offset += 1) {
- words[relativeAddress + offset] = Number(registerWords[offset]) & 0xFFFF
- }
- }
- }
- return words
- }
- function createModbusWrittenRegisterSnapshots(group, words = []) {
- const writtenWordCache = words.reduce((cache, word, offset) => {
- cache[group.startAddress + offset] = word
- return cache
- }, {})
- return group.registers.map((register) => {
- const rawWords = getRegisterWordsFromWordCache(register, writtenWordCache) || []
- const rawValue = decodeRegisterValue(register, rawWords)
- const displayValue = formatRegisterValue(register, rawValue)
- return {
- rawWords,
- rawValue,
- displayValue
- }
- })
- }
- async function writeModbusCoilGroup(group, options = {}) {
- const slaveAddress = modbusClient.getSharedSlaveAddress()
- if (slaveAddress === null) return null
- const writtenRegisters = []
- for (let index = 0; index < group.registers.length; index += 1) {
- const register = group.registers[index]
- const coilValue = parseCoilValue(getRegisterWriteValueText(register))
- if (coilValue === null) {
- transport.showCommandAlert('参数组写入', `${register.name || `寄存器 ${index + 1}`} 没有有效写入值`)
- return null
- }
- const response = await modbusClient.writeSingleCoil(
- slaveAddress,
- group.startAddress + index,
- !!coilValue,
- register.name || group.name || '参数组写入',
- 'parameter-group-coil-write',
- {
- maxFrameBytes: options.maxPacketLength
- }
- )
- if (!response) return null
- writtenRegisters.push({
- rawBytes: [],
- rawValue: coilValue,
- rawWords: [],
- displayValue: formatCoilDisplayValue(coilValue)
- })
- }
- return writtenRegisters
- }
- async function writeModbusRegisterGroup(group, options = {}) {
- const slaveAddress = modbusClient.getSharedSlaveAddress()
- if (slaveAddress === null) return null
- let words
- try {
- words = getRegisterWordsForModbusWrite(group)
- } catch (error) {
- transport.showCommandAlert('参数组写入', error.message || '寄存器组没有有效写入值')
- return null
- }
- const writtenRegisters = createModbusWrittenRegisterSnapshots(group, words)
- const maxWriteQuantity = getWriteSpanMaxQuantity(words.length, options.maxPacketLength)
- const spans = splitWordSpans(group.startAddress, words.length, maxWriteQuantity)
- let cursor = 0
- for (const span of spans) {
- const spanWords = words.slice(cursor, cursor + span.quantity)
- cursor += span.quantity
- const response = await modbusClient.writeMultipleRegisters(
- slaveAddress,
- span.address,
- spanWords,
- group.name || '参数组写入',
- 'parameter-group-write',
- {
- maxFrameBytes: options.maxPacketLength
- }
- )
- if (!response) return null
- }
- return writtenRegisters
- }
- async function writeModbusGroup(group, options = {}) {
- return group.registerType === 'coil'
- ? writeModbusCoilGroup(group, options)
- : writeModbusRegisterGroup(group, options)
- }
- function bytesToPaddedWords(bytes = []) {
- return bytesToWords(bytes.length % 2 === 0 ? bytes : bytes.concat(0))
- }
- function fillByteCacheFromBytes(byteCache, startAddress, bytes = []) {
- bytes.forEach((byte, offset) => {
- byteCache[startAddress + offset] = Number(byte) & 0xFF
- })
- }
- function fillWordCacheFromBytes(wordCache, startAddress, bytes = []) {
- const words = bytesToPaddedWords(bytes)
- words.forEach((word, offset) => {
- wordCache[startAddress + offset] = Number(word) & 0xFFFF
- })
- }
- function createStorageWrittenRegisterSnapshots(group, wordCache) {
- return group.registers.map((register) => {
- const rawBytes = isByteAddressedGroup(group)
- ? (getRegisterBytesFromByteCache(register, wordCache) || [])
- : []
- const rawWords = isByteAddressedGroup(group)
- ? (getRegisterWordsFromByteCache(register, wordCache) || [])
- : (getRegisterWordsFromWordCache(register, wordCache) || [])
- const rawValue = isByteAddressedGroup(group)
- ? decodeRegisterFromByteCache(register, wordCache)
- : decodeRegisterValue(register, rawWords)
- return {
- rawBytes,
- rawWords,
- rawValue,
- displayValue: formatRegisterValue(register, rawValue)
- }
- })
- }
- function createStorageWrittenRegisterSnapshot(group, register, byteCache) {
- const snapshots = createStorageWrittenRegisterSnapshots({
- ...group,
- registers: [register]
- }, byteCache)
- return snapshots[0] || null
- }
- function groupHasBitFields(group = {}) {
- return (Array.isArray(group.registers) ? group.registers : []).some((register) => !!register.isBitField)
- }
- async function writeMemoryRegister(group, register, options = {}) {
- const memoryType = getMemoryType(group)
- const maxPacketLength = getStorageMaxPacketLength(group, options)
- const byteLength = Math.max(1, Number(register.byteLength) || 1)
- const address = Math.min(MAX_STORAGE_ACCESS_ADDRESS, Math.max(0, Math.floor(Number(register.address) || getMemoryAddress(group))))
- let bytes
- if (memoryType === null) {
- transport.showCommandAlert('内存写入', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`)
- return null
- }
- if (memoryType === STORAGE_ACCESS_CODE_AREA) {
- transport.showCommandAlert('内存写入', 'code 区暂不支持写入')
- return null
- }
- try {
- if (register.isBitField) {
- const baseBytes = await storageAccessService.readMemory(
- memoryType,
- address,
- byteLength,
- register.name ? `${register.name} 读改写` : '变量读改写',
- 'parameter-group-memory-register-rmw-read',
- {
- maxFrameBytes: maxPacketLength
- }
- )
- if (!baseBytes) return null
- bytes = getGroupEncodedBytes({
- ...group,
- paddedByteLength: byteLength,
- registers: [{
- ...register,
- address,
- byteStart: 0
- }]
- }, baseBytes).slice(0, byteLength)
- } else {
- bytes = getRegisterEncodedBytes(register)
- }
- } catch (error) {
- transport.showCommandAlert('内存写入', error.message || '变量没有有效写入值')
- return null
- }
- if (!Array.isArray(bytes) || !bytes.length) {
- transport.showCommandAlert('内存写入', `${register.name || '变量'} 没有有效写入值`)
- return null
- }
- bytes = bytes.slice(0, byteLength)
- while (bytes.length < byteLength) bytes.push(0)
- const ok = await storageAccessService.writeMemory(
- memoryType,
- address,
- bytes,
- register.name || group.name || '变量写入',
- 'parameter-group-memory-register-write',
- {
- maxFrameBytes: maxPacketLength
- }
- )
- if (!ok) return null
- const byteCache = {}
- fillByteCacheFromBytes(byteCache, address, bytes)
- return createStorageWrittenRegisterSnapshot(group, register, byteCache)
- }
- async function readMemoryGroup(group, options = {}) {
- const memoryType = getMemoryType(group)
- const address = getMemoryAddress(group)
- const byteLength = getMemoryByteLength(group)
- const maxPacketLength = getStorageMaxPacketLength(group, options)
- if (memoryType === null) {
- transport.showCommandAlert('内存读取', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`)
- return null
- }
- const bytes = await storageAccessService.readMemory(
- memoryType,
- address,
- byteLength,
- group.name || '内存读取',
- 'parameter-group-memory-read',
- {
- maxFrameBytes: maxPacketLength,
- showModal: options.showModal !== false
- }
- )
- if (!bytes) return null
- const wordCache = {}
- if (isByteAddressedGroup(group)) {
- fillByteCacheFromBytes(wordCache, address, bytes)
- } else {
- fillWordCacheFromBytes(wordCache, address, bytes)
- }
- return wordCache
- }
- async function writeMemoryGroup(group, options = {}) {
- const memoryType = getMemoryType(group)
- const address = getMemoryAddress(group)
- const byteLength = getMemoryByteLength(group)
- const maxPacketLength = getStorageMaxPacketLength(group, options)
- let bytes
- if (memoryType === null) {
- transport.showCommandAlert('内存写入', `暂不支持 ${group.sourceMemoryArea || '未知'} 内存区域`)
- return null
- }
- if (memoryType === STORAGE_ACCESS_CODE_AREA) {
- transport.showCommandAlert('内存写入', 'code 区暂不支持写入')
- return null
- }
- try {
- let baseBytes = null
- if (groupHasBitFields(group)) {
- baseBytes = await storageAccessService.readMemory(
- memoryType,
- address,
- byteLength,
- group.name ? `${group.name} 读改写` : '内存读改写',
- 'parameter-group-memory-rmw-read',
- {
- maxFrameBytes: maxPacketLength
- }
- )
- if (!baseBytes) return null
- }
- bytes = getGroupEncodedBytes(group, baseBytes)
- } catch (error) {
- transport.showCommandAlert('内存写入', error.message || '寄存器组没有有效写入值')
- return null
- }
- bytes = bytes.slice(0, byteLength)
- const ok = await storageAccessService.writeMemory(
- memoryType,
- address,
- bytes,
- group.name || '内存写入',
- 'parameter-group-memory-write',
- {
- maxFrameBytes: maxPacketLength
- }
- )
- if (!ok) return null
- const wordCache = {}
- if (isByteAddressedGroup(group)) {
- fillByteCacheFromBytes(wordCache, address, bytes)
- } else {
- fillWordCacheFromBytes(wordCache, address, bytes)
- }
- return createStorageWrittenRegisterSnapshots(group, wordCache)
- }
- async function readGroup(group, options = {}) {
- const wordCache = shouldUseStorageAccess(options, group)
- ? await readMemoryGroup(group, options)
- : await readModbusGroup(group, options)
- if (!wordCache) return null
- const normalizedCache = normalizeNumericCache(wordCache)
- return {
- applyToGroup(currentGroup) {
- return applyReadCacheToGroup(currentGroup, normalizedCache)
- }
- }
- }
- async function writeRegister(group, registerIndex, options = {}) {
- const register = group && Array.isArray(group.registers) ? group.registers[registerIndex] : null
- if (!register) return null
- if (shouldUseStorageAccess(options, group)) {
- const written = await writeMemoryRegister(group, register, options)
- return written
- ? {
- applyToGroup(currentGroup) {
- return applyWrittenSnapshotToGroupRegister(currentGroup, registerIndex, written)
- }
- }
- : null
- }
- return writeGroup(group, options)
- }
- async function writeGroup(group, options = {}) {
- const snapshots = shouldUseStorageAccess(options, group)
- ? await writeMemoryGroup(group, options)
- : await writeModbusGroup(group, options)
- if (!snapshots) return null
- return {
- applyToGroup(currentGroup) {
- return applyWrittenSnapshotsToGroup(currentGroup, snapshots)
- }
- }
- }
- module.exports = {
- getMemoryAddress,
- getMemoryByteLength,
- getMemoryType,
- isByteAddressedGroup,
- isMemoryGroup,
- readGroup,
- writeGroup,
- writeRegister
- }
|