UDP Discovery
Periodically broadcast a "hello" datagram to a multicast group and listen for the same from peers. Deploy to two or more boards on the same WiFi network and watch them find each other. Uses the wifi, udp, and sys APIs.
Hardware
- Two or more ESP32 boards with WiFi (one is enough to see the announcer logs, but you need at least two for actual discovery)
- USB cables
- A WiFi network. Both boards must join the same SSID.
Setup
Set your WiFi credentials as environment variables:
pnpm mikro env set WIFI_SSID YourNetworkName --no-secret
pnpm mikro env set WIFI_PASSPHRASEnpx mikro env set WIFI_SSID YourNetworkName --no-secret
npx mikro env set WIFI_PASSPHRASEyarn mikro env set WIFI_SSID YourNetworkName --no-secret
yarn mikro env set WIFI_PASSPHRASEbunx mikro env set WIFI_SSID YourNetworkName --no-secret
bunx mikro env set WIFI_PASSPHRASECode
import {env} from 'mikro/env'
import {sleep} from 'mikro/sleep'
import {deviceId} from 'mikro/sys'
import {bind} from 'mikro/udp'
import {wifi} from 'mikro/wifi'
const GROUP = '224.0.0.251'
const PORT = 7654
const ANNOUNCE_MS = 2000
const ssid = env.require('WIFI_SSID')
const passphrase = env.require('WIFI_PASSPHRASE')
const connected = await wifi.connect({ssid, passphrase})
if (connected.ok) {
const bound = await bind({port: PORT, family: 'ipv4'})
if (bound.ok) {
const sock = bound.value
const joined = sock.joinMulticastGroup({address: GROUP})
if (joined.ok) {
sock.onMessage.subscribe(({msg, from}) => {
const text = new TextDecoder().decode(msg)
if (text === deviceId) return
console.log('discovered %s at %s', text, from.address)
})
while (true) {
await sock.send(deviceId, {address: GROUP, port: PORT, family: 'ipv4'})
await sleep(ANNOUNCE_MS)
}
}
}
}Walkthrough
Multicast group.
224.0.0.251is the mDNS group. Home routers reliably forward it because Bonjour, AirPlay, and Chromecast all depend on it; less common groups in224.0.0.0/24get silently dropped by some APs. The example uses port7654so it doesn't collide with system mDNS responders on5353. Datagrams sent to the group are delivered to every host on the LAN that joined it, with no central server.Why join.
joinMulticastGrouptells the kernel and switch fabric to deliver matching datagrams to this socket. Without it, the socket would only see unicast packets sent to its bound port. The group must be joined after the network interface is up, which is why it lives inside thewifi.connectok-branch.Self-filter. Multicast loops back to the sender on the same host (and the same socket can join its own group), so
onMessagewill see the device's own announcements. Skip them by comparing againstdeviceId.Backpressure. Each socket has a bounded receive queue. A busy responder can override the default with
bind({port, family, recvQueue: 32}). Drops are observable viasocket.dropped.Reach. UDP multicast travels as far as the LAN allows: most home routers forward
224.0.0.0/4within a single broadcast domain but not across VLANs or the public internet. For mesh-wide discovery, use Thread (IPv6 multicast routes across the mesh).
Run it
pnpm install
pnpm mikro flash # only needed once per board
pnpm mikro devnpm install
npx mikro flash # only needed once per board
npx mikro devyarn install
yarn mikro flash # only needed once per board
yarn mikro devbun install
bunx mikro flash # only needed once per board
bunx mikro devDeploy to a second board and watch each console log the other's deviceId.
No second board? Run a Node peer
The example ships with scripts/peer.ts, a small Node program that joins the same multicast group, announces a name, and logs everything it hears. Requires Node 24+. Run it on a laptop attached to the same WiFi:
pnpm run peer # name defaults to laptop-<pid>
pnpm run peer -- --name desk # custom namenpm run peer
npm run peer -- --name deskyarn run peer
yarn run peer --name deskbun run peer
bun run peer -- --name deskThe board will log discovered laptop-... at 192.168.x.y and the laptop will log the board's deviceId. Stop the peer with Ctrl-C.