From 725c1dcc3031618dceadc44830d4abccdcfc6359 Mon Sep 17 00:00:00 2001 From: Jeykco Date: Sun, 5 Jul 2026 23:30:16 -0500 Subject: [PATCH 1/6] Update Discovery --- resources/sources/Baremetal/ModbusSlave.cpp | 91 ++++++++ .../shared/network/jwplc-device-discovery.ts | 220 +++++++++++++++--- src/middleware/shared/ports/device-port.ts | 6 + 3 files changed, 288 insertions(+), 29 deletions(-) diff --git a/resources/sources/Baremetal/ModbusSlave.cpp b/resources/sources/Baremetal/ModbusSlave.cpp index fc3afc3a5..ff6584bf4 100644 --- a/resources/sources/Baremetal/ModbusSlave.cpp +++ b/resources/sources/Baremetal/ModbusSlave.cpp @@ -14,6 +14,9 @@ Copyright (C) 2022 OpenPLC - Thiago Alves #if defined(JWPLC_BASIC) #include +#if defined(MBTCP_ETHERNET) +#include +#endif #endif //Global Modbus vars @@ -29,6 +32,90 @@ uint16_t mb_t35; // frame delay #if defined(JWPLC_BASIC) EthernetServer mb_server(502); static bool jwplc_mbtcp_ready = false; + + static EthernetUDP jwplc_discovery_udp; + static bool jwplc_discovery_ready = false; + static const uint16_t JWPLC_DISCOVERY_PORT = 54880; + static const char JWPLC_DISCOVERY_REQUEST[] = "JWPLC_DISCOVER_V1"; + + static void jwplc_format_ip(char *buffer, size_t size, IPAddress ip) + { + snprintf(buffer, size, "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]); + } + + static void jwplc_format_mac(char *buffer, size_t size, const uint8_t *mac) + { + if (mac == NULL) + { + snprintf(buffer, size, "00:00:00:00:00:00"); + return; + } + + snprintf( + buffer, + size, + "%02X:%02X:%02X:%02X:%02X:%02X", + mac[0], + mac[1], + mac[2], + mac[3], + mac[4], + mac[5]); + } + + static void jwplc_discovery_begin() + { + jwplc_discovery_ready = (jwplc_discovery_udp.begin(JWPLC_DISCOVERY_PORT) == 1); + } + + static void jwplc_discovery_task() + { + if (!jwplc_discovery_ready) + return; + + int packetSize = jwplc_discovery_udp.parsePacket(); + if (packetSize <= 0) + return; + + char request[40]; + int len = jwplc_discovery_udp.read(request, sizeof(request) - 1); + if (len <= 0) + return; + + request[len] = '\0'; + + for (int i = 0; i < len; i++) + { + if (request[i] == '\r' || request[i] == '\n') + { + request[i] = '\0'; + break; + } + } + + if (strncmp(request, JWPLC_DISCOVERY_REQUEST, strlen(JWPLC_DISCOVERY_REQUEST)) != 0) + return; + + char ipBuffer[16]; + char macBuffer[18]; + jwplc_format_ip(ipBuffer, sizeof(ipBuffer), JWPLC_Ethernet.localIP()); + jwplc_format_mac(macBuffer, sizeof(macBuffer), JWPLC_Ethernet.mac()); + + const char *mode = (JWPLC_Ethernet.mode() == JWPLC_ETH_MODE_DHCP) ? "DHCP" : "STATIC"; + + char response[220]; + snprintf( + response, + sizeof(response), + "JWPLC_DEVICE_V1;vendor=JW Control;model=JWPLC BASIC [2.0.0];ip=%s;mac=%s;modbusTcp=1;port=502;mode=%s", + ipBuffer, + macBuffer, + mode); + + jwplc_discovery_udp.beginPacket(jwplc_discovery_udp.remoteIP(), jwplc_discovery_udp.remotePort()); + jwplc_discovery_udp.write((const uint8_t *)response, strlen(response)); + jwplc_discovery_udp.endPacket(); + } #elif defined(BOARD_ESP32) WiFiServer mb_server(502); WiFiClient mb_serverClients[MAX_SRV_CLIENTS]; @@ -180,6 +267,7 @@ void mbconfig_ethernet_iface(uint8_t *mac, uint8_t *ip, uint8_t *dns, uint8_t *g // Ethernet.begin() directo: JWPLC_Ethernet inicializa CS, SPI, // timeouts, DHCP/IP estatica, link y estado del W5500. jwplc_mbtcp_ready = false; + jwplc_discovery_ready = false; bool ethOk = false; @@ -231,6 +319,7 @@ void mbconfig_ethernet_iface(uint8_t *mac, uint8_t *ip, uint8_t *dns, uint8_t *g if (ethOk || hasIp) { mb_server.begin(); + jwplc_discovery_begin(); jwplc_mbtcp_ready = true; #if defined(JWPLC_MBTCP_DEBUG) Serial.println("[JWPLC][MBTCP] Server started on port 502"); @@ -239,6 +328,7 @@ void mbconfig_ethernet_iface(uint8_t *mac, uint8_t *ip, uint8_t *dns, uint8_t *g else { jwplc_mbtcp_ready = false; + jwplc_discovery_ready = false; #if defined(JWPLC_MBTCP_DEBUG) Serial.println("[JWPLC][MBTCP] Server NOT started"); #endif @@ -324,6 +414,7 @@ void mbtask() if (jwplc_mbtcp_ready) { handle_tcp(); + jwplc_discovery_task(); static uint32_t lastMaintainMs = 0; uint32_t nowMs = millis(); diff --git a/src/backend/shared/network/jwplc-device-discovery.ts b/src/backend/shared/network/jwplc-device-discovery.ts index f4a1bb9ed..87b44f3b4 100644 --- a/src/backend/shared/network/jwplc-device-discovery.ts +++ b/src/backend/shared/network/jwplc-device-discovery.ts @@ -1,4 +1,5 @@ -import net from 'node:net' +import dgram from 'node:dgram' +import net from 'node:net' import { networkInterfaces } from 'node:os' export type JwplcDiscoveredDevice = { @@ -7,17 +8,34 @@ export type JwplcDiscoveredDevice = { label: string interfaceName: string sourceAddress: string + discoveryType: 'jwplc-native' | 'modbus-tcp' + isJwplc: boolean + vendor?: string + model?: string + macAddress?: string + networkMode?: string } export type JwplcDeviceDiscoveryOptions = { port?: number timeoutMs?: number concurrency?: number + udpTimeoutMs?: number +} + +type LocalNetwork = { + interfaceName: string + sourceAddress: string + broadcastAddress: string } const DEFAULT_PORT = 502 const DEFAULT_TIMEOUT_MS = 250 +const DEFAULT_UDP_TIMEOUT_MS = 800 const DEFAULT_CONCURRENCY = 64 +const JWPLC_DISCOVERY_PORT = 54880 +const JWPLC_DISCOVERY_REQUEST = 'JWPLC_DISCOVER_V1' +const JWPLC_DISCOVERY_RESPONSE = 'JWPLC_DEVICE_V1' const ipToLong = (ip: string) => ip.split('.').reduce((acc, octet) => ((acc << 8) + Number(octet)) >>> 0, 0) @@ -26,13 +44,8 @@ const longToIp = (value: number) => const isIpv4 = (family: string | number) => family === 'IPv4' || family === 4 -const getScanTargets = () => { - const targets: Array<{ - ipAddress: string - interfaceName: string - sourceAddress: string - }> = [] - +const getLocalNetworks = (): LocalNetwork[] => { + const networks: LocalNetwork[] = [] const interfaces = networkInterfaces() for (const [interfaceName, entries] of Object.entries(interfaces)) { @@ -42,26 +55,138 @@ const getScanTargets = () => { const sourceAddress = entry.address const sourceLong = ipToLong(sourceAddress) - // Fase 1: escaneo seguro de /24 local. - // Evita barridos enormes si Windows reporta una máscara más amplia. + // Fase 2: mantenemos /24 como alcance seguro para evitar barridos enormes. const networkBase = sourceLong & ipToLong('255.255.255.0') + const broadcastAddress = longToIp((networkBase + 255) >>> 0) - for (let host = 1; host <= 254; host++) { - const ipAddress = longToIp((networkBase + host) >>> 0) - if (ipAddress === sourceAddress) continue + networks.push({ + interfaceName, + sourceAddress, + broadcastAddress, + }) + } + } - targets.push({ - ipAddress, - interfaceName, - sourceAddress, - }) - } + return networks +} + +const getScanTargets = (networks: LocalNetwork[]) => { + const targets: Array<{ + ipAddress: string + interfaceName: string + sourceAddress: string + }> = [] + + for (const network of networks) { + const sourceLong = ipToLong(network.sourceAddress) + const networkBase = sourceLong & ipToLong('255.255.255.0') + + for (let host = 1; host <= 254; host++) { + const ipAddress = longToIp((networkBase + host) >>> 0) + if (ipAddress === network.sourceAddress) continue + + targets.push({ + ipAddress, + interfaceName: network.interfaceName, + sourceAddress: network.sourceAddress, + }) } } return targets } +const parseDiscoveryPayload = (text: string) => { + const parts = text.trim().split(';') + if (parts[0] !== JWPLC_DISCOVERY_RESPONSE) return null + + const payload: Record = {} + + for (const part of parts.slice(1)) { + const separatorIndex = part.indexOf('=') + if (separatorIndex <= 0) continue + + const key = part.slice(0, separatorIndex).trim() + const value = part.slice(separatorIndex + 1).trim() + payload[key] = value + } + + return payload +} + +const discoverNativeOnNetwork = (network: LocalNetwork, timeoutMs: number): Promise => + new Promise((resolve) => { + const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true }) + const found = new Map() + + const closeSocket = () => { + try { + socket.close() + } catch { + // Socket may already be closed. + } + } + + const finish = () => { + closeSocket() + resolve([...found.values()]) + } + + const timer = setTimeout(finish, timeoutMs) + + socket.once('error', () => { + clearTimeout(timer) + finish() + }) + + socket.on('message', (message, rinfo) => { + const payload = parseDiscoveryPayload(message.toString('utf8')) + if (!payload) return + + const ipAddress = payload.ip || rinfo.address + const port = Number(payload.port || DEFAULT_PORT) + const model = payload.model || 'JWPLC Basic' + const vendor = payload.vendor || 'JW Control' + const macAddress = payload.mac + const networkMode = payload.mode + + found.set(`${ipAddress}:${port}`, { + ipAddress, + port, + label: `${model} · ${ipAddress}:${port}`, + interfaceName: network.interfaceName, + sourceAddress: network.sourceAddress, + discoveryType: 'jwplc-native', + isJwplc: true, + vendor, + model, + macAddress, + networkMode, + }) + }) + + socket.bind(0, network.sourceAddress, () => { + try { + socket.setBroadcast(true) + + const request = Buffer.from(JWPLC_DISCOVERY_REQUEST) + const targets = [...new Set(['255.255.255.255', network.broadcastAddress])] + + for (const target of targets) { + socket.send(request, JWPLC_DISCOVERY_PORT, target) + } + } catch { + clearTimeout(timer) + finish() + } + }) + }) + +const discoverNativeJwplcDevices = async (networks: LocalNetwork[], timeoutMs: number) => { + const results = await Promise.all(networks.map((network) => discoverNativeOnNetwork(network, timeoutMs))) + return results.flat() +} + const testTcpPort = (ipAddress: string, port: number, timeoutMs: number) => new Promise((resolve) => { const socket = new net.Socket() @@ -83,14 +208,13 @@ const testTcpPort = (ipAddress: string, port: number, timeoutMs: number) => socket.connect(port, ipAddress) }) -export const discoverJwplcDevices = async ( - options: JwplcDeviceDiscoveryOptions = {}, -): Promise => { - const port = options.port ?? DEFAULT_PORT - const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS - const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY - - const targets = getScanTargets() +const discoverModbusTcpDevices = async ( + networks: LocalNetwork[], + port: number, + timeoutMs: number, + concurrency: number, +) => { + const targets = getScanTargets(networks) const found: JwplcDiscoveredDevice[] = [] let index = 0 @@ -106,9 +230,11 @@ export const discoverJwplcDevices = async ( found.push({ ipAddress: target.ipAddress, port, - label: `JWPLC / Modbus TCP - ${target.ipAddress}:${port}`, + label: `Modbus TCP detectado · ${target.ipAddress}:${port}`, interfaceName: target.interfaceName, sourceAddress: target.sourceAddress, + discoveryType: 'modbus-tcp', + isJwplc: false, }) } } @@ -118,5 +244,41 @@ export const discoverJwplcDevices = async ( await Promise.all(workers) - return found.sort((a, b) => a.ipAddress.localeCompare(b.ipAddress)) + return found +} + +const mergeDiscoveryResults = (nativeDevices: JwplcDiscoveredDevice[], tcpDevices: JwplcDiscoveredDevice[]) => { + const merged = new Map() + + for (const device of nativeDevices) { + merged.set(`${device.ipAddress}:${device.port}`, device) + } + + for (const device of tcpDevices) { + const key = `${device.ipAddress}:${device.port}` + if (!merged.has(key)) { + merged.set(key, device) + } + } + + return [...merged.values()].sort((a, b) => { + if (a.isJwplc !== b.isJwplc) return a.isJwplc ? -1 : 1 + return a.ipAddress.localeCompare(b.ipAddress, undefined, { numeric: true }) + }) +} + +export const discoverJwplcDevices = async ( + options: JwplcDeviceDiscoveryOptions = {}, +): Promise => { + const port = options.port ?? DEFAULT_PORT + const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS + const udpTimeoutMs = options.udpTimeoutMs ?? DEFAULT_UDP_TIMEOUT_MS + const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY + + const networks = getLocalNetworks() + + const nativeDevices = await discoverNativeJwplcDevices(networks, udpTimeoutMs) + const tcpDevices = await discoverModbusTcpDevices(networks, port, timeoutMs, concurrency) + + return mergeDiscoveryResults(nativeDevices, tcpDevices) } diff --git a/src/middleware/shared/ports/device-port.ts b/src/middleware/shared/ports/device-port.ts index b64605d91..013b00d24 100644 --- a/src/middleware/shared/ports/device-port.ts +++ b/src/middleware/shared/ports/device-port.ts @@ -29,6 +29,12 @@ export type JwplcDiscoveredDevice = { label: string interfaceName: string sourceAddress: string + discoveryType: 'jwplc-native' | 'modbus-tcp' + isJwplc: boolean + vendor?: string + model?: string + macAddress?: string + networkMode?: string } export type JwplcDeviceDiscoveryResult = { From 28af2bf26a413da3bc3804e672d3461fffb2705c Mon Sep 17 00:00:00 2001 From: Jeykco Date: Mon, 6 Jul 2026 00:28:26 -0500 Subject: [PATCH 2/6] Update form-layout.tsx --- .../vendor-screen/layouts/form-layout.tsx | 126 +++++++++++++++--- 1 file changed, 106 insertions(+), 20 deletions(-) diff --git a/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/form-layout.tsx b/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/form-layout.tsx index 5b76e6bb8..72894b5ea 100644 --- a/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/form-layout.tsx +++ b/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/form-layout.tsx @@ -89,9 +89,16 @@ function FormLayout({ section }: FormLayoutProps) { label: string interfaceName: string sourceAddress: string + discoveryType: 'jwplc-native' | 'modbus-tcp' + isJwplc: boolean + vendor?: string + model?: string + macAddress?: string + networkMode?: string }> >([]) const [jwplcDiscoveryError, setJwplcDiscoveryError] = useState(null) + const [hasJwplcDiscoverySearched, setHasJwplcDiscoverySearched] = useState(false) const vendorScreenData = useOpenPLCStore((s) => s.deviceDefinitions.configuration.vendorScreenData) const setVendorScreenData = useOpenPLCStore((s) => s.deviceActions.setVendorScreenData) @@ -115,6 +122,11 @@ function FormLayout({ section }: FormLayoutProps) { setVendorScreenData(persistenceKey, { ...storedValues, [id]: value }) } + const updateFields = (updates: Record) => { + if (persistenceKey === null) return + setVendorScreenData(persistenceKey, { ...storedValues, ...updates }) + } + const refreshCommunicationPorts = useCallback( async (e?: MouseEvent) => { e?.preventDefault() @@ -149,6 +161,7 @@ function FormLayout({ section }: FormLayoutProps) { setIsDiscoveringJwplc(true) setJwplcDiscoveryError(null) setJwplcDiscoveryResults([]) + setHasJwplcDiscoverySearched(true) const result = await device.discoverJwplcDevices() @@ -219,7 +232,7 @@ function FormLayout({ section }: FormLayoutProps) { )} ) : field.type === 'jwplc-device-discovery' ? ( -
+
+ + {jwplcDiscoveryResults.length > 0 && ( + + {jwplcDiscoveryResults.length === 1 + ? '1 dispositivo encontrado' + : jwplcDiscoveryResults.length + ' dispositivos encontrados'} + + )}
{jwplcDiscoveryError && {jwplcDiscoveryError}} {jwplcDiscoveryResults.length > 0 && (
- {jwplcDiscoveryResults.map((foundDevice) => ( -
- - {foundDevice.ipAddress}:{foundDevice.port} · {foundDevice.interfaceName} - + {jwplcDiscoveryResults.map((foundDevice) => { + const isDhcpEnabled = values.enable_dhcp === true + const title = foundDevice.isJwplc + ? foundDevice.model || 'JWPLC Basic confirmado' + : 'Equipo Modbus TCP detectado' + const badgeText = foundDevice.isJwplc ? 'JWPLC confirmado' : 'Solo puerto 502' + const networkInfo = [ + foundDevice.macAddress ? 'MAC ' + foundDevice.macAddress : null, + foundDevice.networkMode, + foundDevice.interfaceName, + ] + .filter(Boolean) + .join(' ? ') - -
- ))} +
+
+ + {title} + + + {badgeText} + +
+ +
+ {foundDevice.ipAddress}:{foundDevice.port} +
+ + {networkInfo && ( +
+ {networkInfo} +
+ )} +
+ + +
+ ) + })}
)} - {!isDiscoveringJwplc && jwplcDiscoveryResults.length === 0 && !jwplcDiscoveryError && ( + {!isDiscoveringJwplc && + hasJwplcDiscoverySearched && + jwplcDiscoveryResults.length === 0 && + !jwplcDiscoveryError && ( + + No se encontraron dispositivos. Verifica que el JWPLC est? encendido, conectado a la red y + con Modbus TCP activo. + + )} + + {!isDiscoveringJwplc && !hasJwplcDiscoverySearched && !jwplcDiscoveryError && ( - Busca equipos con Modbus TCP abierto en puerto 502. + Busca JWPLC Basic por discovery nativo. Si no responde, se usa respaldo por Modbus TCP puerto + 502. + + )} + + {values.enable_dhcp === true && !String(values.tcp_debug_ip_address ?? '').trim() && ( + + Con DHCP activo, usa Discovery o escribe la IP asignada en Debug IP Address. )}
From df55528d1029c46a4e76254ce4700c902096d984 Mon Sep 17 00:00:00 2001 From: Jeykco Date: Mon, 6 Jul 2026 01:04:57 -0500 Subject: [PATCH 3/6] feat(jwplc): add native device discovery response --- .../vendor-screen/layouts/form-layout.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/form-layout.tsx b/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/form-layout.tsx index 72894b5ea..ae7eed090 100644 --- a/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/form-layout.tsx +++ b/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/form-layout.tsx @@ -262,13 +262,21 @@ function FormLayout({ section }: FormLayoutProps) { ? foundDevice.model || 'JWPLC Basic confirmado' : 'Equipo Modbus TCP detectado' const badgeText = foundDevice.isJwplc ? 'JWPLC confirmado' : 'Solo puerto 502' + const modeLabel = + foundDevice.networkMode === 'STATIC' + ? 'IP estatica' + : foundDevice.networkMode === 'DHCP' + ? 'DHCP' + : foundDevice.networkMode + const networkInfo = [ + foundDevice.discoveryType === 'jwplc-native' ? 'Discovery nativo' : 'Fallback TCP 502', foundDevice.macAddress ? 'MAC ' + foundDevice.macAddress : null, - foundDevice.networkMode, + modeLabel, foundDevice.interfaceName, ] .filter(Boolean) - .join(' ? ') + .join(' | ') return (
Date: Mon, 6 Jul 2026 01:12:13 -0500 Subject: [PATCH 4/6] chore(jwplc): bump editor version to 4.2.8-jwplc.2 --- package.json | 2 +- release/app/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4aa3c4321..d4d967c79 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "open-plc-editor-jwplc", "description": "OpenPLC Editor - JWPLC Edition, build maintained by JW Control for JWPLC Basic integration", - "version": "4.2.8-jwplc.1", + "version": "4.2.8-jwplc.2", "license": "GPL-3.0", "author": { "name": "Autonomy Logic" diff --git a/release/app/package.json b/release/app/package.json index 29c502359..7dfc9de28 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -1,6 +1,6 @@ { "name": "open-plc-editor-jwplc", - "version": "4.2.8-jwplc.1", + "version": "4.2.8-jwplc.2", "description": "OpenPLC Editor - JWPLC Edition, build maintained by JW Control for JWPLC Basic integration", "license": "GPL-3.0", "author": { From adbfebdb684fa71d7d9e0c951b8bc84c9eee1225 Mon Sep 17 00:00:00 2001 From: Jeykco Date: Mon, 6 Jul 2026 01:26:26 -0500 Subject: [PATCH 5/6] docs(jwplc): document native device discovery phase 2 --- .../JWPLC_NATIVE_DEVICE_DISCOVERY_PHASE2.md | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 docs/jwplc/JWPLC_NATIVE_DEVICE_DISCOVERY_PHASE2.md diff --git a/docs/jwplc/JWPLC_NATIVE_DEVICE_DISCOVERY_PHASE2.md b/docs/jwplc/JWPLC_NATIVE_DEVICE_DISCOVERY_PHASE2.md new file mode 100644 index 000000000..22f047998 --- /dev/null +++ b/docs/jwplc/JWPLC_NATIVE_DEVICE_DISCOVERY_PHASE2.md @@ -0,0 +1,108 @@ +# JWPLC Native Device Discovery - Fase 2 + +## Estado + +Cerrado y validado. + +Esta fase mejora el JWPLC Device Discovery para que el editor no dependa únicamente de detectar equipos con el puerto Modbus TCP 502 abierto. + +Ahora el JWPLC Basic responde con identidad propia mediante discovery nativo UDP y el editor puede diferenciar entre: + +- JWPLC confirmado. +- Equipo Modbus TCP genérico detectado por fallback. + +## Versión + +- Editor: OpenPLC Editor - JWPLC Edition 4.2.8-jwplc.2 +- Hardware validado: JWPLC Basic v2.0.0 +- Board: JWPLC BASIC [2.0.0] +- VPP compatible: com.jwcontrol.jwplc-basic v2.1.0-alpha.11 + +## Instalador validado + +Archivo: + +OpenPLC Editor - JWPLC Edition_4.2.8-jwplc.2.exe + +SHA256: + +0952C753DC11C1AD34FDB3BD6A92E596A1929E024A8AD094A5D1203D4F74EFDA + +## Cambios principales + +### Discovery nativo JWPLC + +Se agregó un responder UDP en el runtime Baremetal para JWPLC Basic. + +Solicitud: + +JWPLC_DISCOVER_V1 + +Respuesta: + +JWPLC_DEVICE_V1 + +La respuesta incluye: + +- Vendor. +- Modelo. +- IP asignada. +- MAC. +- Puerto Modbus TCP. +- Modo de red: DHCP o STATIC. + +### Discovery del lado del editor + +El backend del editor ahora intenta primero discovery nativo JWPLC. + +Si no obtiene respuesta, mantiene fallback por escaneo de puerto TCP 502. + +### UI mejorada + +La pantalla Modbus TCP ahora muestra una lista de dispositivos encontrados con: + +- Nombre/modelo. +- Estado JWPLC confirmado. +- IP y puerto. +- Tipo de discovery. +- MAC. +- Modo DHCP/static. +- Interfaz de red detectada. + +### Comportamiento de Usar para Debug + +Con DHCP activo: + +- Actualiza Debug IP Address. + +Con DHCP desactivado: + +- Actualiza Debug IP Address. +- Actualiza IP Address. + +## Validación realizada + +- [x] Build main OK. +- [x] Build renderer OK. +- [x] Instalador generado. +- [x] Editor abre correctamente. +- [x] Modbus TCP activo con Ethernet W5500. +- [x] DHCP activo. +- [x] Discovery detecta JWPLC Basic. +- [x] UI muestra JWPLC confirmado. +- [x] UI muestra Discovery nativo. +- [x] UI muestra MAC, DHCP e interfaz. +- [x] Usar para Debug actualiza Debug IP Address. +- [x] Debugger TCP probado con IP detectada. + +## No incluido + +- No se cambia la IP del JWPLC desde el editor. +- No se implementa protocolo industrial tipo DCP. +- No se modifica OTA. +- No se modifica el package Arduino base. +- No se modifica el VPP en esta fase. + +## Resultado + +JWPLC Device Discovery pasa de ser un escaneo genérico de Modbus TCP a una identificación nativa de JWPLC Basic, manteniendo compatibilidad con fallback TCP 502. From e5fc39ee20b954b6579ebd21a792cb682c277d74 Mon Sep 17 00:00:00 2001 From: Jeykco Date: Mon, 6 Jul 2026 01:57:40 -0500 Subject: [PATCH 6/6] fix(jwplc): satisfy native discovery typecheck --- src/backend/shared/network/jwplc-device-discovery.ts | 3 +-- src/middleware/adapters/editor/device-adapter.ts | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/backend/shared/network/jwplc-device-discovery.ts b/src/backend/shared/network/jwplc-device-discovery.ts index 87b44f3b4..f8fff2a20 100644 --- a/src/backend/shared/network/jwplc-device-discovery.ts +++ b/src/backend/shared/network/jwplc-device-discovery.ts @@ -169,11 +169,10 @@ const discoverNativeOnNetwork = (network: LocalNetwork, timeoutMs: number): Prom try { socket.setBroadcast(true) - const request = Buffer.from(JWPLC_DISCOVERY_REQUEST) const targets = [...new Set(['255.255.255.255', network.broadcastAddress])] for (const target of targets) { - socket.send(request, JWPLC_DISCOVERY_PORT, target) + socket.send(JWPLC_DISCOVERY_REQUEST, JWPLC_DISCOVERY_PORT, target) } } catch { clearTimeout(timer) diff --git a/src/middleware/adapters/editor/device-adapter.ts b/src/middleware/adapters/editor/device-adapter.ts index b1ace3de8..0a7f00a93 100644 --- a/src/middleware/adapters/editor/device-adapter.ts +++ b/src/middleware/adapters/editor/device-adapter.ts @@ -14,7 +14,7 @@ * util:get-preview-image (invoke) */ -import type { DevicePort } from '../../shared/ports/device-port' +import type { DevicePort, JwplcDeviceDiscoveryResult } from '../../shared/ports/device-port' import type { BoardInfo, CommunicationPort } from '../../shared/ports/types' export function createEditorDeviceAdapter(): DevicePort { @@ -35,8 +35,8 @@ export function createEditorDeviceAdapter(): DevicePort { return window.bridge.refreshCommunicationPorts() }, - discoverJwplcDevices() { - return window.bridge.discoverJwplcDevices() + discoverJwplcDevices(): Promise { + return window.bridge.discoverJwplcDevices() as Promise }, getPreviewImage(imageName: string, packagePath?: string): Promise {