const { buildReadFrame, buildWriteMultipleRegistersFrame, buildWriteSingleCoilFrame, MAX_READ_COIL_QUANTITY, MAX_READ_REGISTER_QUANTITY } = require('./modbus-rtu') const paramsPageState = require('./params-page-state') const transport = require('./ble-transport') const { addCoilReadValues, addWordReadValues, floatToWords } = require('./register-value-utils') function getSharedSlaveAddress() { try { return transport.getSlaveAddress() } catch (error) { transport.showCommandAlert('从机地址错误', error.message) return null } } function parseAddress(address) { return parseInt(String(address || '0'), 16) } function getAreaKey(item) { return (item.area && item.area.key) || item.areaKey || 'holding' } function getRegisterCount(item) { return item.registerCount || (item.type === 'float' ? 2 : 1) } function hasWriteValue(value) { return value !== '' && value !== undefined && value !== null && value !== '--' } function toWriteNumber(value) { if (!hasWriteValue(value)) return null const numberValue = Number(value) if (!Number.isFinite(numberValue)) return null return numberValue } function toWord(value) { const numberValue = Number(value) if (!Number.isFinite(numberValue)) return null const word = Math.round(numberValue) return word >= 0 && word <= 0xFFFF ? word : null } function getGroupItems(data, groupKey) { if (groupKey === 'vsp') return data.vspCurveRegisters.concat([data.speedSlopeRegister]) if (groupKey === 'speedLoop') { return data.speedLoopInputDisplayRegisters .concat(data.speedLoopCalculatedDisplayRegisters, data.speedLoopExtraDisplayRegisters) } if (groupKey === 'estimator') { return data.estimatorCalculatedDisplayRegisters.concat(data.atoBandwidthDisplayRegisters) } if (groupKey === 'tailwind') { return data.tailwindControlRegisters .concat(data.tailwindCalculatedDisplayRegisters, data.tailwindAtoBandwidthDisplayRegisters) } if (groupKey === 'preposition') return data.prepositionSwitchRegisters.concat(data.prepositionParameterDisplayRegisters) if (groupKey === 'oil') return data.oilParameterInputRegisters if (groupKey === 'dq') return data.dqGainDisplayRegisters if (groupKey === 'protectionSwitch') return data.protectionSwitchRegisters if (groupKey === 'protection') return data.protectionDisplayRegisters return [] } function expandAtoItems(item) { if (!item.kpAddress || !item.kiAddress) return [item] return [ { address: item.kpAddress, areaKey: 'holding', name: `${item.name} KP`, type: 'uint16_t', writeValue: item.kpWriteValue }, { address: item.kiAddress, areaKey: 'holding', name: `${item.name} KI`, type: 'uint16_t', writeValue: item.kiWriteValue } ] } function expandItems(items) { return items.reduce((result, item) => result.concat(expandAtoItems(item)), []) } function makeReadSpans(entries) { const sortedEntries = entries .map((item) => ({ address: parseAddress(item.address), count: getRegisterCount(item) })) .filter((item) => Number.isFinite(item.address) && item.count > 0) .sort((left, right) => left.address - right.address) const spans = [] sortedEntries.forEach((entry) => { const last = spans[spans.length - 1] if (last && entry.address <= last.address + last.quantity) { const end = Math.max(last.address + last.quantity, entry.address + entry.count) last.quantity = end - last.address return } spans.push({ address: entry.address, quantity: entry.count }) }) return spans } function splitReadSpans(spans, maxQuantity) { return spans.reduce((result, span) => { let address = span.address let remaining = span.quantity while (remaining > 0) { const quantity = Math.min(remaining, maxQuantity) result.push({ address, quantity }) address += quantity remaining -= quantity } return result }, []) } async function readGroup(data, groupKey) { const slaveAddress = getSharedSlaveAddress() if (slaveAddress === null) return false const items = expandItems(getGroupItems(data, groupKey)) const coilItems = items.filter((item) => getAreaKey(item) === 'coil') const holdingItems = items.filter((item) => getAreaKey(item) === 'holding') const inputItems = items.filter((item) => getAreaKey(item) === 'input') const readValues = { coils: {}, words: {} } let sent = false for (const span of splitReadSpans(makeReadSpans(coilItems), MAX_READ_COIL_QUANTITY)) { sent = true const response = await transport.sendManagedFrame( buildReadFrame(slaveAddress, 0x01, span.address, span.quantity), '参数读取', { address: span.address, functionCode: 0x01, kind: 'params-read', quantity: span.quantity, slaveAddress } ) addCoilReadValues(readValues, span.address, span.quantity, response) } for (const span of splitReadSpans(makeReadSpans(holdingItems), MAX_READ_REGISTER_QUANTITY)) { sent = true const response = await transport.sendManagedFrame( buildReadFrame(slaveAddress, 0x03, span.address, span.quantity), '参数读取', { address: span.address, functionCode: 0x03, kind: 'params-read', quantity: span.quantity, slaveAddress } ) addWordReadValues(readValues, span.address, response) } for (const span of splitReadSpans(makeReadSpans(inputItems), MAX_READ_REGISTER_QUANTITY)) { sent = true const response = await transport.sendManagedFrame( buildReadFrame(slaveAddress, 0x04, span.address, span.quantity), '参数读取', { address: span.address, functionCode: 0x04, kind: 'params-read', quantity: span.quantity, slaveAddress } ) addWordReadValues(readValues, span.address, response) } if (!sent) { transport.showCommandAlert('参数读取', '当前分组没有可读取的寄存器') return false } if (!Object.keys(readValues.coils).length && !Object.keys(readValues.words).length) { return false } return paramsPageState.applyReadValues(data, readValues) } async function readSingleHoldingWord(slaveAddress, address) { const response = await transport.sendManagedFrame( buildReadFrame(slaveAddress, 0x03, address, 1), '读取配对寄存器', { address, functionCode: 0x03, kind: 'params-pair-read', quantity: 1, slaveAddress }, {} ) if (!response || !Array.isArray(response.words) || response.words.length < 1) return null return response.words[0] & 0xFFFF } async function buildHoldingWriteEntries(slaveAddress, items) { const normalEntries = [] const byteGroups = {} items.forEach((item) => { if (getAreaKey(item) !== 'holding') return if (item.type === 'uint8_t' && item.bytePosition) { const address = parseAddress(item.address) const group = byteGroups[address] || { address, high: null, low: null } group[item.bytePosition] = item byteGroups[address] = group return } const writeNumber = toWriteNumber(item.writeValue) if (writeNumber === null) return const words = item.type === 'float' ? floatToWords(writeNumber) : [toWord(writeNumber)] if (!words || words.some((word) => word === null)) return normalEntries.push({ address: parseAddress(item.address), label: item.name, values: words }) }) for (const addressText of Object.keys(byteGroups)) { const group = byteGroups[addressText] const highValue = group.high ? toWord(group.high.writeValue) : null const lowValue = group.low ? toWord(group.low.writeValue) : null if (highValue === null && lowValue === null) continue let baseWord = 0 if (highValue === null || lowValue === null) { const readWord = await readSingleHoldingWord(slaveAddress, group.address) if (!Number.isInteger(readWord)) continue baseWord = readWord } const nextHigh = highValue === null ? ((baseWord >> 8) & 0xFF) : highValue const nextLow = lowValue === null ? (baseWord & 0xFF) : lowValue if (nextHigh > 0xFF || nextLow > 0xFF) continue normalEntries.push({ address: group.address, label: '8位参数', values: [(nextHigh << 8) | nextLow] }) } return normalEntries.sort((left, right) => left.address - right.address) } async function writeGroup(data, groupKey) { const slaveAddress = getSharedSlaveAddress() if (slaveAddress === null) return false const items = expandItems(getGroupItems(data, groupKey)) const coilItems = items.filter((item) => getAreaKey(item) === 'coil') const holdingItems = items.filter((item) => getAreaKey(item) === 'holding') let sent = false for (const item of coilItems) { const checked = Number(item.writeValue) !== 0 const address = parseAddress(item.address) sent = true const response = await transport.sendManagedFrame( buildWriteSingleCoilFrame(slaveAddress, address, checked), item.name, { address, functionCode: 0x05, kind: 'params-coil-write', quantity: 1, value: checked ? 0xFF00 : 0x0000, slaveAddress } ) if (!response) return false } const holdingEntries = await buildHoldingWriteEntries(slaveAddress, holdingItems) for (const entry of holdingEntries) { sent = true const response = await transport.sendManagedFrame( buildWriteMultipleRegistersFrame(slaveAddress, entry.address, entry.values), entry.label, { address: entry.address, functionCode: 0x10, kind: 'params-holding-write', quantity: entry.values.length, slaveAddress } ) if (!response) return false } if (!sent) { transport.showCommandAlert('参数写入', '当前分组没有可写入的参数') } return sent } async function writeSwitchRegister(item) { const slaveAddress = getSharedSlaveAddress() if (slaveAddress === null || !item) return false const address = parseAddress(item.address) const checked = Number(item.writeValue) !== 0 return transport.sendManagedFrame( buildWriteSingleCoilFrame(slaveAddress, address, checked), item.name, { address, functionCode: 0x05, kind: 'params-switch-write', quantity: 1, value: checked ? 0xFF00 : 0x0000, slaveAddress } ) } module.exports = { readGroup, writeGroup, writeSwitchRegister }