Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/mdns.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# mdns from a bare clone: the unix port's own test, the unix port against
# python-zeroconf both ways, and the windows port cross-built (it needs
# Windows' mDNS service to run, which a Linux runner hasn't got).
name: mdns

on:
push:
branches: [main]
paths:
- 'modules/mdns/**'
- 'build_mp.py'
- '.github/workflows/mdns.yml'
pull_request:
paths:
- 'modules/mdns/**'
- 'build_mp.py'
- '.github/workflows/mdns.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
unix:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v7

- name: Install the unix port's build dependencies
run: |
sudo apt-get update
sudo apt-get install -y libffi-dev pkg-config

- name: Build the unix port with mdns
run: python3 build_mp.py --port unix --modules mdns

- name: The API, its errors, and a service found by the process that advertised it
run: ./builds/unix/standard/micropython modules/mdns/tests/test_mdns.py

- name: zeroconf finds MicroPython's service, and MicroPython finds zeroconf's
run: |
python3 -m pip install zeroconf
python3 modules/mdns/tests/check_mdns_zeroconf.py ./builds/unix/standard/micropython

windows:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v7

- name: Install the MinGW cross toolchain
run: |
sudo apt-get update
sudo apt-get install -y gcc-mingw-w64-x86-64 libtool libltdl-dev autoconf automake

- name: Cross-build the windows port with mdns
run: python3 build_mp.py --port windows --modules mdns
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
!/modules/tsmux/
!/modules/ppa/
!/modules/jpegio/
!/modules/mdns/
!/modules/pngio/
!/modules/tflite/
!/modules/vision/
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ New here? The [newcomer's guide](docs/newcomers.md) explains the model; the
Display, or serves them as live HLS.
- [`h264enc/`](modules/h264enc/): RGB565 frames to H.264 on the ESP32-P4's hardware encoder.
- [`jpegio/`](modules/jpegio/): JPEG decode and encode, with CircuitPython's `jpegio` API.
- [`mdns/`](modules/mdns/): find and advertise services on the local network, with
CircuitPython's `mdns` API (esp32, rp2, unix, windows).
- [`pngio/`](modules/pngio/): PNG encode and decode, RGB565 in and out.
- [`ppa/`](modules/ppa/): scale, rotate, convert and fill pixel buffers, on the
ESP32-P4's Pixel Processing Accelerator or in portable C elsewhere.
Expand Down
1 change: 1 addition & 0 deletions docs/build-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ micropython-pydevices/
│ ├── all/manifest.py tracked: every sibling with a root manifest.py, except opt-in ones
│ ├── castif/ tracked: lives only here (moves from usermods/); ESP32-P4 only
│ ├── jpegio/ tracked: lives only here (moved from displayif, 2026-10-06); every port
│ ├── mdns/ tracked: lives only here; esp32, rp2, unix, windows
│ ├── audiodsp -> ../../audiodsp ignored symlinks to sibling checkouts,
│ ├── audioif -> ../../audioif clones at the modules.lock commit anywhere else
│ ├── audiocomponents, cameraif, displayif, lvgl-micropython, palettes,
Expand Down
1 change: 1 addition & 0 deletions docs/newcomers.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ way autosize grows the app partition when the image doesn't fit.
| [`modules/castif/`](../modules/castif/) | Casts an ESP32-P4's screen and sound to a TV over Wi-Fi Display, or serves them as live HLS. |
| [`modules/h264enc/`](../modules/h264enc/) | RGB565 frames to H.264 on the ESP32-P4's hardware encoder. |
| [`modules/jpegio/`](../modules/jpegio/) | JPEG decode and encode, with CircuitPython's `jpegio` API. |
| [`modules/mdns/`](../modules/mdns/) | Find and advertise services on the local network, with CircuitPython's `mdns` API. |
| [`modules/pngio/`](../modules/pngio/) | PNG encode and decode, RGB565 in and out. |
| [`modules/ppa/`](../modules/ppa/) | Scale, rotate, convert and fill pixel buffers, on the ESP32-P4's Pixel Processing Accelerator or in portable C elsewhere. |
| [`modules/tsmux/`](../modules/tsmux/) | MPEG-TS and HLS segments from H.264. |
Expand Down
2 changes: 1 addition & 1 deletion modules.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ displayif https://github.com/PyDevices/displayif.git be7669ebd3
lvgl-micropython https://github.com/PyDevices/lvgl-micropython.git e8aea6672566ff0bbcb62f2f8e91733c31ed4ca1
palettes https://github.com/PyDevices/palettes.git f3dfad0bac281f1e8816b30861445498b4137de6
pdwidgets https://github.com/PyDevices/pdwidgets.git e92267f2ef938b60ba3b97177ceb2bd0e0d402d3
pydevices https://github.com/PyDevices/pydevices.git 883775566fe85543aca883bcd7825bbe3f9c738a
pydevices https://github.com/PyDevices/pydevices.git 81290b13573b51889def33e65559fe64fb5471c1
pygraphics https://github.com/PyDevices/pygraphics.git 2d7ca35847db78d450428cdc0cb5aa69ab81c809
ulab https://github.com/v923z/micropython-ulab.git 1d3ddd8f5228e2c7335dac71f4846e6abb0ec9f9
usbif https://github.com/PyDevices/usbif.git d1d1812513df03e0e1b90413af4b5dcde272e525
124 changes: 124 additions & 0 deletions modules/mdns/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# mdns — find and advertise services on the local network

`import mdns` lets a board or a desktop announce what it offers on the
network (a web page, a speaker, a remote control) and find what other devices
offer, the way phones and laptops do. It's CircuitPython's `mdns` module: the
same classes, arguments, defaults and errors, so one program runs on either.

```python
import mdns, network

server = mdns.Server(network.WLAN(network.STA_IF)) # None on a computer
server.instance_name = "Kitchen display"
server.advertise_service(service_type="_http", protocol="_tcp", port=80,
txt_records=["path=/"])

for s in server.find(service_type="_spotify-connect", protocol="_tcp"):
print(s.instance_name, s.hostname, s.ipv4_address, s.port)
```

It builds on four ports, each on what that system already has:

| Port | How |
|---|---|
| esp32 | ESP-IDF's mDNS component, the one that answers for the board's `<hostname>.local` |
| rp2 with a CYW43 radio (Pico W, Pico 2 W) | lwIP's mDNS responder and search |
| unix | a responder and a querier of its own |
| windows | Windows' own mDNS service, through its DNS-SD API |

`--modules all` includes it. The browser build can't send multicast, so it
has no `mdns`, and CircuitPython builds keep CircuitPython's own. On CPython,
`mdns.py` in pydevices-desktop gives the same API over python-zeroconf.

## API

### `mdns.Server(network_interface)`

On a board, `network_interface` is a `network.WLAN` (or `network.LAN` on
esp32), or `None` for whichever has an address. On unix and Windows it's
`None`, or on a computer with more than one network the IPv4 address of the
one to use (`"192.168.1.20"`). Anything else raises `ValueError`.

One `Server` may be live at a time: a second raises
`RuntimeError("mDNS already initialized")` until the first is `deinit()`ed.

### `hostname`, `instance_name`

`hostname` is the name the device answers to as `<hostname>.local`. It
starts as the board's `network.hostname()`, or the computer's name. Setting
it renames the device for mDNS (on Windows it stays the computer's name,
because Windows' mDNS service answers for no other; setting another raises
`NotImplementedError`).

`instance_name` is the human-readable name advertised services go by. It's
the hostname until you set it.

### `find(*, service_type, protocol, timeout=1) -> tuple`

Every service of `service_type` (`"_http"`) over `protocol` (`"_tcp"` or
`"_udp"`) that answers within `timeout` seconds, as `RemoteService`s. The
arguments are keyword-only, as CircuitPython's binding has them. Ctrl-C ends
the wait.

### `advertise_service(*, service_type, protocol, port, txt_records=None)`

Answer for a service on `port`. `txt_records` is up to 32 strings, each
`"key=value"` or a key alone. Calling it again for the same `service_type`
and `protocol` changes the port and the records. A `Server` offers up to 8
services at a time (lwIP on rp2, one).

### `deinit()`

Withdraws every service the `Server` advertised and frees its place. A soft
reset does this too. On esp32 the board goes on answering for its hostname,
as it did before.

### `RemoteService`

Read-only, made only by `find()`: `hostname` (without `.local`),
`instance_name`, `service_type`, `protocol`, `port`, and `ipv4_address`, a
dotted string or `None` if no address came back. (CircuitPython's is an
`ipaddress.IPv4Address`; `str()` of either is the dotted address.)

## Tests

`tests/test_mdns.py` runs on a unix or Windows build: the API, its errors,
and a service the process advertises and finds itself.
`tests/check_mdns_zeroconf.py` runs that build against python-zeroconf in
another process, both ways.

```bash
./build_mp.py --port unix --modules mdns
./builds/unix/standard/micropython modules/mdns/tests/test_mdns.py
pip install zeroconf
python3 modules/mdns/tests/check_mdns_zeroconf.py ./builds/unix/standard/micropython
```

## How

**unix** binds 224.0.0.251:5353 beside whatever else holds it (Avahi,
systemd-resolved) and answers from a thread: for the hostname, each service's
PTR, SRV and TXT, the DNS-SD service list, and questions asked from other
ports, straight back to the asker. It announces a service twice when it
appears or changes and says goodbye when it goes. It doesn't probe for name
conflicts. `find()` asks from 5353 too, and asks again halfway through, since
multicast gets lost on Wi-Fi. Where 5353 can't be had, `find()` still works,
from a spare port, and `advertise_service()` raises `OSError(EADDRINUSE)`.

Under WSL with mirrored networking, the Linux side and Windows don't hear
each other's multicast: a unix build and a Windows program on the same
computer can't find each other, though both find, and are found by, every
other device on the network.

**windows** hands services to Windows' mDNS service (`DnsServiceRegister`),
which holds 5353 and answers for them, and finds with `DnsServiceBrowse` and
`DnsServiceResolve`. Windows may answer for a withdrawn service for a second
or so after `deinit()`.

**esp32** shares ESP-IDF's mDNS component with the port, which starts it
when the network gets an address. A `Server` starts it only if the port
hasn't, and never stops it.

**rp2** gives lwIP's responder the station's interface, which the port
doesn't, and takes it off again at `deinit()`. It builds, but hasn't been
run on a Pico W yet.
4 changes: 4 additions & 0 deletions modules/mdns/manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# mdns: CircuitPython's mdns API (find and advertise services on the local
# network). C only; esp32, rp2 with a network, unix and windows. On CPython,
# pydevices-desktop's mdns.py gives the same API over python-zeroconf.
c_module(".")
11 changes: 11 additions & 0 deletions modules/mdns/micropython.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# mdns CMake glue (see micropython.mk): ESP-IDF's mDNS component on esp32,
# which the port already depends on, and lwIP's mDNS on rp2 boards with a
# CYW43 radio. Elsewhere it compiles to nothing.
add_library(usermod_mdns INTERFACE)
target_sources(usermod_mdns INTERFACE
${CMAKE_CURRENT_LIST_DIR}/src/mod_mdns.c
${CMAKE_CURRENT_LIST_DIR}/src/mdns_esp32.c
${CMAKE_CURRENT_LIST_DIR}/src/mdns_lwip.c
)
target_include_directories(usermod_mdns INTERFACE ${CMAKE_CURRENT_LIST_DIR}/src)
target_link_libraries(usermod INTERFACE usermod_mdns)
23 changes: 23 additions & 0 deletions modules/mdns/micropython.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# mdns: CircuitPython's mdns API. On the make ports it builds for unix (our
# own responder and querier) and windows (the system's DNS-SD API) only: the
# browser can't send multicast, and CircuitPython has its own mdns module.
MDNS_DIR := $(USERMOD_DIR)/src
MDNS_PORT_DIR := $(abspath $(CURDIR))

ifeq ($(wildcard $(TOP)/py/circuitpy_mpconfig.h),)
ifneq ($(findstring /ports/unix,$(MDNS_PORT_DIR))$(findstring /ports/windows,$(MDNS_PORT_DIR)),)

CFLAGS_USERMOD += -I$(MDNS_DIR)
SRC_USERMOD_C += $(MDNS_DIR)/mod_mdns.c
# No qstrs: they stay out of the QSTR scan.
SRC_USERMOD_LIB_C += $(MDNS_DIR)/mdns_posix.c
SRC_USERMOD_LIB_C += $(MDNS_DIR)/mdns_windows.c

ifneq ($(findstring /ports/windows,$(MDNS_PORT_DIR)),)
LIBS_USERMOD += -ldnsapi -lws2_32
else
LIBS_USERMOD += -lpthread
endif

endif
endif
Loading
Loading