feat(wifi): make the mDNS name configurable, and stop orphaning DNS-SD instances - #113
Open
skialpine wants to merge 1 commit into
Open
feat(wifi): make the mDNS name configurable, and stop orphaning DNS-SD instances#113skialpine wants to merge 1 commit into
skialpine wants to merge 1 commit into
Conversation
Every scale hardcoded MDNS.begin("hds"), MDNS.setInstanceName("Half Decent
Scale"), and WiFi.setHostname("hds.local"), so two scales on one LAN both
asked for the same identity. The mDNS stack resolves that collision by
suffixing the loser to hds-2.local, but which scale wins follows boot order,
and hostname and instance name are probed independently -- so a browse can
show a scale whose address and label disagree.
Store a device name in the NVS wifi namespace under mdns_name, defaulting to
"hds" so an unrenamed scale still answers at hds.local. The name drives the
mDNS hostname, the DHCP hostname (now a bare label, not "hds.local"), the
DNS-SD instance name, and a new name= TXT record. Rename over POST
/setup/name, from the Scale Name field in the web UI's WiFi setup area.
Also withdraw the DNS-SD registration on deliberate shutdown. stopWifi()
never called MDNS.end(), which is what emits the goodbye, so every rename or
reset orphaned an instance in resolver caches for the PTR TTL (75 min,
against 2 min for SRV/A) -- browsable but unresolvable. That bug predates
this change.
Validation lives in include/mdns_name.h, Arduino-free so the native env can
test it: 1-24 chars of [a-z0-9-], no leading or trailing hyphen, trimmed and
lowercased, empty restores the default. Reposting the current name is a no-op
that does not write NVS and does not restart, so a stray submit or a repeated
POST cannot reboot the scale mid-shot.
Verified on hardware: default boot answers hds.local; rename normalizes and
reboots to <name>.local advertising "Half Decent Scale (<name>)" with TXT
name=<name>; a passive 5353 capture shows the ttl=0 PTR goodbye for the old
instance before the radio drops; repeated no-op posts leave uptime unbroken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
skialpine
force-pushed
the
feat/configurable-mdns-name
branch
from
July 29, 2026 19:36
7a7a5c7 to
e661983
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Every scale asks for the same identity.
src/wifi_setup.cpphardcoded all three strings:The mDNS stack does resolve the collision — the loser gets suffixed to
hds-2.local— so both scales stay reachable. But:hdsvshds-2follows boot order. Power them up the other way round and they swap addresses, so a bookmark or app config cannot pin a specific scale.hds-2tells you nothing. Not which scale is by the grinder.192.168.10.145was athds-2.locallabelledHalf Decent Scale, while192.168.10.241was athds.locallabelledHalf Decent Scale-2. Each won one race and lost the other, so address and browser label disagreed on both units at once.Also,
WiFi.setHostname("hds.local")passed a.localsuffix to a DHCP hostname, where it does not belong.What this changes
A device name in the NVS
wifinamespace undermdns_name, defaulting tohds. A scale that is never renamed behaves exactly as it does today — samehds.local, sameHalf Decent Scaleinstance name, no migration, no schema bump.The name drives:
kitchenMDNS.begin()hdskitchenWiFi.setHostname()hdskitchenHalf Decent ScaleHalf Decent Scale (kitchen)name=hdsname=kitchenRenaming is
POST /setup/name, driven by a "Scale Name" field in the WiFi setup area of the on-device web UI. The name also appears in the/snapshotstatus frame asmdns_name, and on the OLED WiFi status screen so it can be recovered without network access.Second thing: stale DNS-SD instances
stopWifi()never calledMDNS.end(), and that call is what emits the DNS-SD goodbye. So every deliberate reboot orphaned its instance registration: the PTR record sat in every resolver cache on the LAN for its full TTL — 75 minutes by DNS-SD convention, against 2 minutes for SRV/A — which is exactly why a browse would list instances that never resolve.A
_decentscale._tcpbrowse here returned four instances for two scales, two of them ghosts.stopWifi()now withdraws first, while the radio is still up. Every deliberate teardown routes through it — remote and USB reset, rename and wifi-setup reboots, deep sleep — so one call site covers them all. The heap-watchdogesp_restart()stays best-effort by nature.This bug predates the naming feature (the
Half Decent Scale-2ghost came from plain 3.1.12) and is separable if you would rather take it on its own.Worth being explicit about what it does not fix: a goodbye is one unacknowledged multicast, and a crash, flat battery, or unplug sends nothing at all. Clients still have to treat an unresolvable browse hit as "not a scale".
Name rules
include/mdns_name.h, deliberately Arduino-free so thenativeenv can test it: 1-24 characters of[a-z0-9-], no leading or trailing hyphen, trimmed and lowercased, and an empty value restores the defaulthds.Reposting the name a scale already has is a no-op — no NVS write, no restart, response says
"restarting":false. The web UI prefills the field, so without that check a stray submit would reboot the scale mid-shot, and a repeated POST of the current name would hold it in a reboot loop.Firmware constraints respected
/setup/namehandler runs on the AsyncTCP task, so it touches no mDNS entry point and publishes noStringthe main loop reads. It writes NVS and queues the existing deferred restart, the same shape/setup/wifialready uses.WiFi.setHostname()is only read before association, which is why a rename restarts rather than re-advertising live.showWifiStatus()'s existingfirstPage()/nextPage()loop — no nested page loop.wifinamespace, nothds, so the settings schema and its migration contract are untouched.Verification
Host-side:
nightly.ymlnow runs the new contract check and anative-testsjob — neither the unit tests nor the contract script had any CI coverage before, since no workflow ranpio test.On hardware (two scales on one LAN):
hds.local, TXTname=hds, statusmdns_name='hds'.{"name":" Kitchen "}→200 {"name":"kitchen","restarting":true}; after rebootkitchen.localresolves, instance isHalf Decent Scale (kitchen), TXTname=kitchen, status agrees.{"name":""}restoreshds.400:my scale,hds.local,-bad, and a body with noname.A follow-up browse listed only live instances — no
hdstestghost.One observation for the record: ESP-IDF sends the goodbye for the PTR record only, not SRV/TXT/A. That is the right one to withdraw, since PTR carries the 4500 s TTL that causes the ghosts while SRV/A expire on their own in 120 s.
🤖 Generated with Claude Code