-
Notifications
You must be signed in to change notification settings - Fork 97
Feature/dope 442 unified modbus config #1025
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| /** | ||
| * Equivalence between the retired VPP Modbus screen and the unified server | ||
| * configuration that replaced it (DOPE-442). | ||
| * | ||
| * The firmware contract is a set of macro names `ModbusSlave.cpp` reads, so the | ||
| * only question that matters about the move is whether the same project emits | ||
| * the same macros. Every case here feeds one legacy `vendorScreenData` state | ||
| * through both paths — straight into the emitter, and migrated into the new | ||
| * model and back out through the adapter — and asserts the outputs match. | ||
| * | ||
| * The same comparison covers the debug baud and the debug slave id, where a | ||
| * divergence would not break the build at all: it would make Connect report | ||
| * "No Firmware Detected" on a healthy board. | ||
| */ | ||
|
|
||
| import type { VppModbusScreenState } from '../../../../frontend/utils/modbus/serial-link-config' | ||
| import { | ||
| migrateVendorScreenModbus, | ||
| vppStateFromModbusSlaveConfig, | ||
| } from '../../../../frontend/utils/modbus/serial-link-config' | ||
| import { generateModbusDefines, resolveDebugBaud, resolveDebugSlave } from '../steps/modbus-defines' | ||
|
|
||
| /** Legacy screen states, one per branch the emitter can take. */ | ||
| const LEGACY_STATES: Record<string, Record<string, unknown>> = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Check the declared parameter type of migrateVendorScreenModbus and the TypeScript version.
set -euo pipefail
ast-grep run --pattern 'function migrateVendorScreenModbus($$$) { $$$ }' --lang typescript src/frontend/utils/modbus/serial-link-config.ts
rg -n '"typescript"' package.json
rg -n 'strict|exactOptionalPropertyTypes' -g 'tsconfig*.json'Repository: Autonomy-Logic/openplc-editor Length of output: 167 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- candidate files ---'
fd -t f 'modbus-defines-unified.test.ts|serial-link-config.ts|tsconfig.*json|package.json' .
printf '%s\n' '--- symbols and usages ---'
rg -n -C 4 'LEGACY_STATES|VppModbusScreenState|migrateVendorScreenModbus|legacy as' src package.json tsconfig*.json 2>/dev/null || true
printf '%s\n' '--- target test outline ---'
ast-grep outline src/backend/shared/compile/__tests__/modbus-defines-unified.test.ts
printf '%s\n' '--- target test ---'
cat -n src/backend/shared/compile/__tests__/modbus-defines-unified.test.ts
printf '%s\n' '--- migration source outline ---'
ast-grep outline src/frontend/utils/modbus/serial-link-config.tsRepository: Autonomy-Logic/openplc-editor Length of output: 38914 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- VppModbusScreenState and migration implementation ---'
cat -n src/frontend/utils/modbus/serial-link-config.ts | sed -n '25,235p'
printf '%s\n' '--- TypeScript configuration and scripts ---'
cat -n tsconfig.json
python3 - <<'PY'
import json
from pathlib import Path
for name in ('package.json',):
data = json.loads(Path(name).read_text())
print(f'--- {name} scripts/dependencies ---')
print(json.dumps({
'scripts': data.get('scripts', {}),
'devDependencies': data.get('devDependencies', {}),
'dependencies': data.get('dependencies', {}),
}, indent=2))
PY
printf '%s\n' '--- available TypeScript compiler ---'
command -v tsc || true
tsc --version 2>/dev/null || true
find . -path '*/node_modules/typescript/bin/tsc' -type f -print 2>/dev/null | headRepository: Autonomy-Logic/openplc-editor Length of output: 18152 🏁 Script executed: #!/bin/bash
set -euo pipefail
tsc_bin="${PWD}/node_modules/typescript/bin/tsc"
if [ ! -x "$tsc_bin" ]; then
tsc_bin="$(command -v tsc)"
fi
cat >/tmp/modbus-type-probe.ts <<'TS'
interface VppModbusScreenState {
serial?: { baud_rate?: string }
network?: { enabled?: boolean }
modbus_rtu?: {
enabled?: boolean
rtu_interface?: string
rtu_baud_rate?: string
rtu_slave_id?: number
}
modbus_tcp?: {
enabled?: boolean
tcp_interface?: 'Ethernet' | 'Wi-Fi'
enable_dhcp?: boolean
ip_address?: string
}
}
declare function migrateVendorScreenModbus(
vendorScreenData: Record<string, unknown> | undefined,
): unknown
const LEGACY_STATES: Record<string, VppModbusScreenState> = {
'rtu fixture': {
modbus_rtu: { enabled: true, rtu_interface: 'Serial', rtu_baud_rate: '19200', rtu_slave_id: 7 },
},
'tcp fixture': {
modbus_tcp: { enabled: true, tcp_interface: 'Ethernet', enable_dhcp: false, ip_address: '10.0.0.7' },
},
}
const throughNewModel = (legacy: VppModbusScreenState): VppModbusScreenState => {
migrateVendorScreenModbus(legacy)
return legacy
}
for (const legacy of Object.values(LEGACY_STATES)) {
throughNewModel(legacy)
}
TS
cat >/tmp/modbus-type-probe-alternative.ts <<'TS'
interface VppModbusScreenState {
serial?: { baud_rate?: string }
network?: { enabled?: boolean }
modbus_rtu?: {
enabled?: boolean
rtu_interface?: string
rtu_baud_rate?: string
rtu_slave_id?: number
}
modbus_tcp?: {
enabled?: boolean
tcp_interface?: 'Ethernet' | 'Wi-Fi'
enable_dhcp?: boolean
ip_address?: string
}
}
declare function migrateVendorScreenModbus(
vendorScreenData: Record<string, unknown> | undefined,
): unknown
type LegacyVppModbusScreenState = VppModbusScreenState & Record<string, unknown>
const LEGACY_STATES: Record<string, LegacyVppModbusScreenState> = {
'rtu fixture': {
modbus_rtu: { enabled: true, rtu_interface: 'Serial', rtu_baud_rate: '19200', rtu_slave_id: 7 },
},
'tcp fixture': {
modbus_tcp: { enabled: true, tcp_interface: 'Ethernet', enable_dhcp: false, ip_address: '10.0.0.7' },
},
}
const throughNewModel = (legacy: LegacyVppModbusScreenState): VppModbusScreenState => {
migrateVendorScreenModbus(legacy)
return legacy
}
for (const legacy of Object.values(LEGACY_STATES)) {
throughNewModel(legacy)
}
TS
printf '%s\n' '--- proposed declarations ---'
"$tsc_bin" --version
"$tsc_bin" --strict --noEmit --skipLibCheck --target es2022 --module commonjs /tmp/modbus-type-probe.ts 2>&1 || true
printf '%s\n' '--- intersection/index-signature alternative ---'
"$tsc_bin" --strict --noEmit --skipLibCheck --target es2022 --module commonjs /tmp/modbus-type-probe-alternative.ts 2>&1Repository: Autonomy-Logic/openplc-editor Length of output: 501 Remove the assertion with an index-signature intersection. 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| 'rtu on the default port': { | ||
| modbus_rtu: { | ||
| enabled: true, | ||
| rtu_interface: 'Serial', | ||
| rtu_baud_rate: '19200', | ||
| rtu_slave_id: 7, | ||
| enable_rs485_en_pin: false, | ||
| }, | ||
| }, | ||
| 'rtu on a secondary port': { | ||
| modbus_rtu: { enabled: true, rtu_interface: 'Serial2', rtu_baud_rate: '57600', rtu_slave_id: 3 }, | ||
| }, | ||
| 'rtu with an rs485 driver-enable pin': { | ||
| modbus_rtu: { | ||
| enabled: true, | ||
| rtu_interface: 'Serial', | ||
| rtu_baud_rate: '115200', | ||
| rtu_slave_id: 1, | ||
| enable_rs485_en_pin: true, | ||
| rtu_rs485_en_pin: 'D5', | ||
| }, | ||
| }, | ||
| 'rtu asking for rs485 without naming a pin': { | ||
| modbus_rtu: { enabled: true, enable_rs485_en_pin: true }, | ||
| }, | ||
| 'rtu toggled on and nothing else touched': { | ||
| modbus_rtu: { enabled: true }, | ||
| }, | ||
| 'rtu left off but configured': { | ||
| modbus_rtu: { enabled: false, rtu_interface: 'Serial1', rtu_baud_rate: '9600', rtu_slave_id: 42 }, | ||
| }, | ||
| 'tcp over ethernet with a static host': { | ||
| modbus_tcp: { | ||
| enabled: true, | ||
| tcp_interface: 'Ethernet', | ||
| tcp_mac_address: 'de:ad:be:ef:fe:ed', | ||
| enable_dhcp: false, | ||
| ip_address: '192.168.0.50', | ||
| gateway: '192.168.0.1', | ||
| subnet: '255.255.255.0', | ||
| dns: '8.8.8.8', | ||
| }, | ||
| }, | ||
| 'tcp over ethernet on dhcp': { | ||
| modbus_tcp: { enabled: true, tcp_interface: 'Ethernet', enable_dhcp: true }, | ||
| }, | ||
| 'tcp over wifi': { | ||
| modbus_tcp: { | ||
| enabled: true, | ||
| tcp_interface: 'Wi-Fi', | ||
| tcp_wifi_ssid: 'plant-floor', | ||
| tcp_wifi_password: 'hunter2', | ||
| enable_dhcp: true, | ||
| }, | ||
| }, | ||
| 'tcp toggled on and nothing else touched': { | ||
| modbus_tcp: { enabled: true }, | ||
| }, | ||
| 'both transports on': { | ||
| modbus_rtu: { enabled: true, rtu_interface: 'Serial', rtu_baud_rate: '115200', rtu_slave_id: 2 }, | ||
| modbus_tcp: { enabled: true, tcp_interface: 'Wi-Fi', tcp_wifi_ssid: 'shopfloor', enable_dhcp: true }, | ||
| }, | ||
| 'neither transport on': { | ||
| modbus_rtu: { enabled: false }, | ||
| modbus_tcp: { enabled: false }, | ||
| }, | ||
| // A hand-edited project can name a static host without stating the flag that | ||
| // selects it. The emitter reads a missing `enable_dhcp` as "not DHCP", so the | ||
| // migration has to reach the same conclusion or the address is dropped. | ||
| 'static host with no dhcp flag at all': { | ||
| modbus_tcp: { | ||
| enabled: true, | ||
| tcp_interface: 'Ethernet', | ||
| ip_address: '10.0.0.7', | ||
| gateway: '10.0.0.1', | ||
| subnet: '255.255.255.0', | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| const throughNewModel = (legacy: Record<string, unknown>): VppModbusScreenState => { | ||
| const migrated = migrateVendorScreenModbus(legacy) | ||
| if (!migrated) throw new Error('nothing migrated') | ||
| return vppStateFromModbusSlaveConfig({ | ||
| enabled: true, | ||
| networkInterface: '0.0.0.0', | ||
| port: 502, | ||
| rtu: migrated.rtu, | ||
| tcpLink: migrated.tcpLink, | ||
| }) | ||
| } | ||
|
|
||
| describe.each(Object.entries(LEGACY_STATES))('%s', (_name, legacy) => { | ||
| const legacyState = legacy as VppModbusScreenState | ||
| const unifiedState = throughNewModel(legacy) | ||
|
|
||
| it('emits the same defines.h block', () => { | ||
| expect(generateModbusDefines(unifiedState)).toBe(generateModbusDefines(legacyState)) | ||
| }) | ||
|
|
||
| it('resolves the same debug baud', () => { | ||
| expect(resolveDebugBaud(unifiedState)).toBe(resolveDebugBaud(legacyState)) | ||
| }) | ||
|
|
||
| it('resolves the same debug slave id', () => { | ||
| expect(resolveDebugSlave(unifiedState)).toBe(resolveDebugSlave(legacyState)) | ||
| }) | ||
| }) | ||
|
|
||
| describe('vppStateFromModbusSlaveConfig', () => { | ||
| it('describes no transport when the server has neither block', () => { | ||
| expect(vppStateFromModbusSlaveConfig(undefined)).toEqual({}) | ||
| expect(vppStateFromModbusSlaveConfig({ enabled: true, networkInterface: '0.0.0.0', port: 502 })).toEqual({}) | ||
| // An absent block means "this target has no serial slave", which is not the | ||
| // same as one configured and switched off — so nothing is emitted for it. | ||
| expect(generateModbusDefines(vppStateFromModbusSlaveConfig(undefined))).toBe('') | ||
| }) | ||
|
|
||
| it('maps the medium onto the label the emitter switches on', () => { | ||
| const link = { | ||
| enabled: true, | ||
| medium: 'wifi' as const, | ||
| macAddress: '', | ||
| wifiSsid: 'floor', | ||
| wifiPassword: 'pw', | ||
| useDhcp: true, | ||
| ipAddress: '', | ||
| gateway: '', | ||
| subnet: '', | ||
| dns: '', | ||
| } | ||
| const state = vppStateFromModbusSlaveConfig({ | ||
| enabled: true, | ||
| networkInterface: '0.0.0.0', | ||
| port: 502, | ||
| tcpLink: link, | ||
| }) | ||
|
|
||
| expect(state.modbus_tcp?.tcp_interface).toBe('Wi-Fi') | ||
| expect(generateModbusDefines(state)).toContain('#define MBTCP_WIFI') | ||
| expect( | ||
| generateModbusDefines({ ...state, modbus_tcp: { ...state.modbus_tcp, tcp_interface: 'Ethernet' } }), | ||
| ).toContain('#define MBTCP_ETHERNET') | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -299,11 +299,44 @@ const ModbusSlaveBufferMappingSchema = z.object({ | |||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| type ModbusSlaveBufferMapping = z.infer<typeof ModbusSlaveBufferMappingSchema> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Bare-metal blocks of a Modbus server (DOPE-442). Mirrors ModbusRtuConfig / | ||||||||||||||||||||||||||||||||||
| // ModbusTcpLinkConfig in `middleware/shared/ports/types`; they have to agree, | ||||||||||||||||||||||||||||||||||
| // because this schema is what validates the project file and `z.object` drops | ||||||||||||||||||||||||||||||||||
| // keys it does not declare. | ||||||||||||||||||||||||||||||||||
| const ModbusSerialPortSchema = z.enum(['Serial', 'Serial1', 'Serial2', 'Serial3']) | ||||||||||||||||||||||||||||||||||
| const ModbusBaudRateSchema = z.enum(['9600', '14400', '19200', '38400', '57600', '115200']) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const ModbusRtuConfigSchema = z.object({ | ||||||||||||||||||||||||||||||||||
| enabled: z.boolean(), | ||||||||||||||||||||||||||||||||||
| serialPort: ModbusSerialPortSchema, | ||||||||||||||||||||||||||||||||||
| baudRate: ModbusBaudRateSchema, | ||||||||||||||||||||||||||||||||||
| slaveId: z.number(), | ||||||||||||||||||||||||||||||||||
| useRs485EnPin: z.boolean(), | ||||||||||||||||||||||||||||||||||
| rs485EnPin: z.string(), | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+309
to
+316
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Restrict
Proposed fix- slaveId: z.number(),
+ slaveId: z.number().int().min(1).max(247),📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||||||||||||
| type ModbusRtuConfig = z.infer<typeof ModbusRtuConfigSchema> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const ModbusTcpLinkConfigSchema = z.object({ | ||||||||||||||||||||||||||||||||||
| enabled: z.boolean(), | ||||||||||||||||||||||||||||||||||
| medium: z.enum(['ethernet', 'wifi']), | ||||||||||||||||||||||||||||||||||
| macAddress: z.string(), | ||||||||||||||||||||||||||||||||||
| wifiSsid: z.string(), | ||||||||||||||||||||||||||||||||||
| wifiPassword: z.string(), | ||||||||||||||||||||||||||||||||||
| useDhcp: z.boolean(), | ||||||||||||||||||||||||||||||||||
| ipAddress: z.string(), | ||||||||||||||||||||||||||||||||||
| gateway: z.string(), | ||||||||||||||||||||||||||||||||||
| subnet: z.string(), | ||||||||||||||||||||||||||||||||||
| dns: z.string(), | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| type ModbusTcpLinkConfig = z.infer<typeof ModbusTcpLinkConfigSchema> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const ModbusSlaveConfigSchema = z.object({ | ||||||||||||||||||||||||||||||||||
| enabled: z.boolean(), | ||||||||||||||||||||||||||||||||||
| networkInterface: z.string(), | ||||||||||||||||||||||||||||||||||
| port: z.number(), | ||||||||||||||||||||||||||||||||||
| bufferMapping: ModbusSlaveBufferMappingSchema.optional(), | ||||||||||||||||||||||||||||||||||
| rtu: ModbusRtuConfigSchema.optional(), | ||||||||||||||||||||||||||||||||||
| tcpLink: ModbusTcpLinkConfigSchema.optional(), | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| type ModbusSlaveConfig = z.infer<typeof ModbusSlaveConfigSchema> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
@@ -902,9 +935,11 @@ export { | |||||||||||||||||||||||||||||||||
| ModbusIOGroupSchema, | ||||||||||||||||||||||||||||||||||
| ModbusIOPointSchema, | ||||||||||||||||||||||||||||||||||
| ModbusParitySchema, | ||||||||||||||||||||||||||||||||||
| ModbusRtuConfigSchema, | ||||||||||||||||||||||||||||||||||
| ModbusSlaveBufferMappingSchema, | ||||||||||||||||||||||||||||||||||
| ModbusSlaveConfigSchema, | ||||||||||||||||||||||||||||||||||
| ModbusTcpConfigSchema, | ||||||||||||||||||||||||||||||||||
| ModbusTcpLinkConfigSchema, | ||||||||||||||||||||||||||||||||||
| ModbusTransportTypeSchema, | ||||||||||||||||||||||||||||||||||
| OpcUaAddressSpaceConfigSchema, | ||||||||||||||||||||||||||||||||||
| OpcUaAuthMethodSchema, | ||||||||||||||||||||||||||||||||||
|
|
@@ -966,9 +1001,11 @@ export type { | |||||||||||||||||||||||||||||||||
| ModbusIOGroup, | ||||||||||||||||||||||||||||||||||
| ModbusIOPoint, | ||||||||||||||||||||||||||||||||||
| ModbusParity, | ||||||||||||||||||||||||||||||||||
| ModbusRtuConfig, | ||||||||||||||||||||||||||||||||||
| ModbusSlaveBufferMapping, | ||||||||||||||||||||||||||||||||||
| ModbusSlaveConfig, | ||||||||||||||||||||||||||||||||||
| ModbusTcpConfig, | ||||||||||||||||||||||||||||||||||
| ModbusTcpLinkConfig, | ||||||||||||||||||||||||||||||||||
| ModbusTransportType, | ||||||||||||||||||||||||||||||||||
| OpcUaAddressSpaceConfig, | ||||||||||||||||||||||||||||||||||
| OpcUaAuthMethod, | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Validate legacy Modbus configuration at both consumers. Type assertions do not validate persisted
vendorScreenData. Use one shared Zod schema or type guard before compiler and device-link resolution consume this data.src/backend/editor/compiler/compiler-module.ts#L2971-L2978: validate legacy Modbus sections before assigningvppModbusState.src/frontend/services/device-link-resolution.ts#L110-L113: validate legacy Modbus sections before merging them intoscreens.🧰 Tools
🪛 ast-grep (0.45.1)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
📍 Affects 2 files
src/backend/editor/compiler/compiler-module.ts#L2971-L2978(this comment)src/frontend/services/device-link-resolution.ts#L110-L113🤖 Prompt for AI Agents
Source: Coding guidelines