| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622 |
- const {
- formatExportStamp,
- isCancelError,
- loadSelectedFile,
- saveTextFileToChat
- } = require('./file-service')
- const {
- getWxApi
- } = require('./platform-utils')
- const {
- parseHexInteger
- } = require('./base-utils')
- const transport = require('./ble-transport')
- const settingsService = require('./settings-service')
- const modbusAccess = require('./modbus-access')
- const {
- DATA_TYPE_OPTIONS,
- REGISTER_TYPE_OPTIONS,
- cloneImportedGroup,
- decodeRegisterFromWordCache,
- decodeRegisterValue,
- formatCoilDisplayValue,
- formatRegisterValue,
- getDataType,
- getRegisterEncodedWords,
- getRegisterJsonValue,
- getRegisterWordsFromWordCache,
- getRegisterWriteValueText,
- isAddressRangeOverflow,
- isBitRegisterType,
- isByteRegister,
- normalizeGroup,
- normalizeGroupConfig,
- parseCoilValue,
- registerTypeIsBit,
- splitWordSpans,
- validateRegisterValue
- } = require('./generic-modbus-model')
- const STORAGE_KEY = 'generic-modbus-groups-json'
- const JSON_DOCUMENT_TYPE = 'generic-modbus-rtu'
- const JSON_SCHEMA_VERSION = 2
- let initialized = false
- const subscribers = []
- let state = {
- genericModbusDataTypeOptions: DATA_TYPE_OPTIONS,
- genericModbusGroups: [],
- genericModbusRegisterTypeOptions: REGISTER_TYPE_OPTIONS
- }
- function notify() {
- const nextState = getState()
- subscribers.slice().forEach((subscriber) => {
- subscriber(nextState)
- })
- }
- function setState(changedData, options = {}) {
- state = {
- ...state,
- ...changedData
- }
- if (options.persist !== false) persistGroups()
- notify()
- }
- function resolveMaxPacketLength(value) {
- const settings = settingsService.getState()
- const numberValue = Number(value === undefined ? settings.genericModbusMaxPacketLength : value)
- if (Number.isFinite(numberValue) && Math.round(numberValue) === 0) return 0
- if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue)
- return 64
- }
- function getWriteSpanMaxQuantity(totalQuantity, maxPacketLength) {
- if (maxPacketLength === 0) return Math.max(1, totalQuantity)
- return Math.max(1, modbusAccess.getMaxWriteMultipleRegisterQuantity(maxPacketLength))
- }
- function toPersistedGroups(groups) {
- return groups.map((group) => ({
- name: group.name,
- registerType: group.registerType,
- startAddress: group.startAddress,
- quantity: group.quantity,
- registers: group.registers.map((register) => ({
- dataType: register.dataType,
- defaultValue: register.defaultValue,
- name: register.name,
- maxValue: register.maxValue,
- minValue: register.minValue,
- textByteLength: register.textByteLength,
- remark: register.remark,
- unit: register.unit,
- value: getRegisterJsonValue(register)
- }))
- }))
- }
- function toJsonData(groups = state.genericModbusGroups, options = {}) {
- const jsonData = {
- groups: toPersistedGroups(groups),
- type: JSON_DOCUMENT_TYPE,
- version: JSON_SCHEMA_VERSION
- }
- if (options.includeExportedAt) {
- jsonData.exportedAt = new Date().toISOString()
- }
- return jsonData
- }
- function toJsonText(groups = state.genericModbusGroups, options = {}) {
- return JSON.stringify(toJsonData(groups, options), null, 2)
- }
- function parseJsonGroups(jsonText) {
- const parsed = typeof jsonText === 'string' ? JSON.parse(jsonText) : jsonText
- const groups = Array.isArray(parsed)
- ? parsed
- : (Array.isArray(parsed && parsed.groups) ? parsed.groups : parsed && parsed.genericModbusGroups)
- if (parsed && parsed.type && parsed.type !== JSON_DOCUMENT_TYPE) {
- throw new Error('JSON 文件不是通用Modbus配置')
- }
- if (parsed && parsed.version && parsed.version !== JSON_SCHEMA_VERSION) {
- throw new Error('JSON 版本不兼容')
- }
- if (!Array.isArray(groups)) {
- throw new Error('JSON 中没有找到寄存器组数组')
- }
- return groups
- }
- function readStoredGroups() {
- const wxApi = getWxApi()
- if (typeof wxApi.getStorageSync !== 'function') return []
- try {
- const jsonText = wxApi.getStorageSync(STORAGE_KEY)
- if (jsonText) return parseJsonGroups(jsonText).map(cloneImportedGroup)
- } catch (error) {
- return []
- }
- return []
- }
- function persistGroups() {
- const wxApi = getWxApi()
- if (typeof wxApi.setStorageSync !== 'function') return
- try {
- wxApi.setStorageSync(STORAGE_KEY, toJsonText())
- } catch (error) {}
- }
- function init() {
- if (initialized) return
- state = {
- ...state,
- genericModbusGroups: readStoredGroups().map(normalizeGroup)
- }
- initialized = true
- }
- function getState() {
- return {
- ...state,
- genericModbusDataTypeOptions: DATA_TYPE_OPTIONS,
- genericModbusRegisterTypeOptions: REGISTER_TYPE_OPTIONS
- }
- }
- function subscribe(subscriber) {
- if (typeof subscriber !== 'function') return () => {}
- init()
- subscribers.push(subscriber)
- subscriber(getState())
- return () => {
- const index = subscribers.indexOf(subscriber)
- if (index >= 0) subscribers.splice(index, 1)
- }
- }
- function getShareFileName() {
- return `generic-modbus-rtu-${formatExportStamp()}.json`
- }
- async function importJsonFromMessageFile() {
- try {
- const file = await loadSelectedFile('message', {
- encoding: 'utf8',
- extensionMessage: '请选择 .json 寄存器配置文件',
- extensions: ['json'],
- fallbackName: 'generic-modbus.json'
- })
- const jsonText = file.text
- const importedGroups = parseJsonGroups(jsonText).map(cloneImportedGroup).map(normalizeGroup)
- if (!importedGroups.length) throw new Error('JSON 中没有可导入的寄存器组')
- setState({
- genericModbusGroups: state.genericModbusGroups.concat(importedGroups)
- })
- return importedGroups.length
- } catch (error) {
- const message = error && error.message ? error.message : '导入通用Modbus配置失败'
- transport.showCommandAlert('通用Modbus导入', message)
- return 0
- }
- }
- async function saveJsonToChat() {
- try {
- if (!state.genericModbusGroups.length) {
- throw new Error('没有可保存的寄存器组')
- }
- const jsonText = toJsonText(state.genericModbusGroups, {
- includeExportedAt: true
- })
- await saveTextFileToChat(getShareFileName(), jsonText)
- return state.genericModbusGroups.length
- } catch (error) {
- const message = error && error.message ? error.message : '保存通用Modbus配置失败'
- if (!isCancelError(error)) {
- transport.showCommandAlert('通用Modbus保存', message)
- }
- return 0
- }
- }
- function addGroupFromConfig(config = {}) {
- let groupConfig
- try {
- groupConfig = normalizeGroupConfig(config)
- } catch (error) {
- transport.showCommandAlert('通用Modbus添加', error.message || '寄存器组配置无效')
- return null
- }
- if (isAddressRangeOverflow(groupConfig.startAddress, groupConfig.quantity)) {
- transport.showCommandAlert('通用Modbus添加', '地址范围超出 0xFFFF')
- return null
- }
- const group = normalizeGroup({
- ...groupConfig,
- expanded: false
- })
- if (group.addressOverflow) {
- transport.showCommandAlert('通用Modbus添加', '地址范围超出 0xFFFF')
- return null
- }
- setState({
- genericModbusGroups: state.genericModbusGroups.concat(group)
- })
- return group
- }
- function updateGroupConfig(groupId, config = {}) {
- const group = findGroup(groupId)
- if (!group) return null
- let nextConfig
- try {
- nextConfig = normalizeGroupConfig({
- ...group,
- ...config
- })
- } catch (error) {
- transport.showCommandAlert('通用Modbus更新', error.message || '寄存器组配置无效')
- return null
- }
- if (isAddressRangeOverflow(nextConfig.startAddress, nextConfig.quantity)) {
- transport.showCommandAlert('通用Modbus更新', '地址范围超出 0xFFFF')
- return null
- }
- const updatedGroup = normalizeGroup({
- ...group,
- ...nextConfig
- })
- if (updatedGroup.addressOverflow) {
- transport.showCommandAlert('通用Modbus更新', '地址范围超出 0xFFFF')
- return null
- }
- setState({
- genericModbusGroups: state.genericModbusGroups.map((item) => (
- item.id === groupId ? updatedGroup : item
- ))
- })
- return updatedGroup
- }
- function updateGroups(mapper) {
- setState({
- genericModbusGroups: state.genericModbusGroups.map((group, index) => normalizeGroup(mapper(group, index)))
- })
- }
- function findGroup(groupId) {
- return state.genericModbusGroups.find((group) => group.id === groupId)
- }
- function setGroupExpanded(groupId, expanded) {
- updateGroups((group) => group.id === groupId
- ? {
- ...group,
- deleteVisible: false,
- expanded
- }
- : group)
- }
- function setGroupDeleteVisible(groupId, deleteVisible) {
- updateGroups((group) => group.id === groupId
- ? {
- ...group,
- deleteVisible
- }
- : group)
- }
- function removeGroup(groupId) {
- setState({
- genericModbusGroups: state.genericModbusGroups.filter((group) => group.id !== groupId)
- })
- }
- function updateRegister(groupId, registerIndex, changedData) {
- updateGroups((group) => {
- if (group.id !== groupId) return group
- const shouldResetReadState = Object.prototype.hasOwnProperty.call(changedData, 'dataType')
- || Object.prototype.hasOwnProperty.call(changedData, 'textByteLength')
- return {
- ...group,
- registers: group.registers.map((register, currentIndex) => (
- currentIndex === registerIndex
- ? {
- ...register,
- ...(shouldResetReadState ? { rawValue: null, rawWords: [] } : {}),
- ...changedData
- }
- : register
- ))
- }
- })
- }
- function updateRegisterValue(groupId, registerIndex, value) {
- updateRegister(groupId, registerIndex, {
- inputValue: value,
- isDirty: true
- })
- }
- function validateRegisterInputValue(groupId, registerIndex, value) {
- const group = findGroup(groupId)
- if (!group) return false
- const register = group.registers[registerIndex]
- if (!register) return false
- return validateRegisterValue(register, value)
- }
- async function readGroup(groupId, options = {}) {
- const group = findGroup(groupId)
- const slaveAddress = modbusAccess.getSharedSlaveAddress()
- if (!group || slaveAddress === null) return false
- if (group.addressOverflow) {
- transport.showCommandAlert('通用Modbus读取', '寄存器地址范围超出 0xFFFF')
- return false
- }
- const totalQuantity = Math.max(1, group.wordQuantity || group.quantity || 0)
- const maxPacketLength = resolveMaxPacketLength(options.maxPacketLength)
- const wordCache = {}
- const values = await modbusAccess.readSpans(
- slaveAddress,
- group.functionCode,
- [{
- address: group.startAddress,
- quantity: totalQuantity
- }],
- group.name || '通用Modbus读取',
- 'generic-modbus-read',
- {
- maxFrameBytes: maxPacketLength,
- showModal: options.showModal !== false
- }
- )
- if (!values) return false
- 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
- })
- }
- updateGroups((item) => {
- if (item.id !== groupId) return item
- const nextRegisters = item.registers.map((register) => {
- const rawWords = registerTypeIsBit(register) ? [] : getRegisterWordsFromWordCache(register, wordCache)
- const rawValue = registerTypeIsBit(register)
- ? decodeRegisterFromWordCache(register, wordCache)
- : (rawWords ? decodeRegisterValue(register, rawWords) : null)
- const displayValue = rawValue === null || rawValue === undefined
- ? '--'
- : (registerTypeIsBit(register)
- ? formatCoilDisplayValue(rawValue)
- : formatRegisterValue(register, rawValue))
- return {
- ...register,
- displayValue,
- inputValue: item.writable ? displayValue : register.inputValue,
- isDirty: false,
- rawValue,
- rawWords: rawWords || []
- }
- })
- return {
- ...item,
- registers: nextRegisters
- }
- })
- return true
- }
- async function writeGroup(groupId) {
- const group = findGroup(groupId)
- const slaveAddress = modbusAccess.getSharedSlaveAddress()
- const maxPacketLength = resolveMaxPacketLength()
- if (!group || slaveAddress === null) return false
- if (!group.writable) {
- transport.showCommandAlert('通用Modbus写入', '当前寄存器组为只读')
- return false
- }
- if (group.addressOverflow) {
- transport.showCommandAlert('通用Modbus写入', '寄存器地址范围超出 0xFFFF')
- return false
- }
- const writtenRegisters = []
- if (group.registerType === 'coil') {
- 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('通用Modbus写入', `${register.name || `寄存器 ${index + 1}`} 没有有效写入值`)
- return false
- }
- const response = await modbusAccess.writeSingleCoil(
- slaveAddress,
- group.startAddress + index,
- !!coilValue,
- register.name || group.name || '通用Modbus写入',
- 'generic-modbus-coil-write',
- {
- maxFrameBytes: maxPacketLength
- }
- )
- if (!response) return false
- writtenRegisters.push({
- rawValue: coilValue,
- rawWords: [],
- displayValue: formatCoilDisplayValue(coilValue)
- })
- }
- } else {
- const words = Array.from({ length: Math.max(1, group.wordQuantity || 1) }, () => 0)
- 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) {
- transport.showCommandAlert('通用Modbus写入', `${register.name || `寄存器 ${index + 1}`} 没有有效写入值`)
- return false
- }
- 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
- }
- }
- }
- const writtenWordCache = words.reduce((cache, word, offset) => {
- cache[group.startAddress + offset] = word
- return cache
- }, {})
- group.registers.forEach((register) => {
- const rawWords = getRegisterWordsFromWordCache(register, writtenWordCache) || []
- const rawValue = decodeRegisterValue(register, rawWords)
- const displayValue = formatRegisterValue(register, rawValue)
- writtenRegisters.push({
- rawWords,
- rawValue,
- displayValue
- })
- })
- const maxWriteQuantity = getWriteSpanMaxQuantity(words.length, 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 modbusAccess.writeMultipleRegisters(
- slaveAddress,
- span.address,
- spanWords,
- group.name || '通用Modbus写入',
- 'generic-modbus-write',
- {
- maxFrameBytes: maxPacketLength
- }
- )
- if (!response) return false
- }
- }
- updateGroups((item) => {
- if (item.id !== groupId) return item
- let writtenIndex = 0
- return {
- ...item,
- registers: item.registers.map((register) => {
- const written = writtenRegisters[writtenIndex] || {}
- writtenIndex += 1
- const hasDisplayValue = Object.prototype.hasOwnProperty.call(written, 'displayValue')
- const hasRawValue = Object.prototype.hasOwnProperty.call(written, 'rawValue')
- const hasRawWords = Object.prototype.hasOwnProperty.call(written, 'rawWords')
- return {
- ...register,
- displayValue: hasDisplayValue ? written.displayValue : register.displayValue,
- inputValue: hasDisplayValue ? written.displayValue : register.inputValue,
- isDirty: false,
- rawValue: hasRawValue ? written.rawValue : register.rawValue,
- rawWords: hasRawWords ? written.rawWords : register.rawWords
- }
- })
- }
- })
- return true
- }
- module.exports = {
- DATA_TYPE_OPTIONS,
- REGISTER_TYPE_OPTIONS,
- addGroupFromConfig,
- getState,
- importJsonFromMessageFile,
- init,
- readGroup,
- removeGroup,
- saveJsonToChat,
- setGroupDeleteVisible,
- setGroupExpanded,
- subscribe,
- updateGroupConfig,
- updateRegister,
- updateRegisterValue,
- validateRegisterInputValue,
- writeGroup
- }
|