GPIO2 on the original ESP32, 46 on the S2/S3 and 8 on the C3/C6 don't stop
a normal boot, but held at the wrong level at reset they keep esptool from
starting the download mode. The WT32-ETH01 wiring puts TX on GPIO2.
Each warning now says what the pin does and what to do: move it for a pin
that stops the boot, disconnect it to flash for one that only blocks USB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The backup record's fallback RX pin is 35, as the radio's default is.
- BootPins.h, pins.js and Transceiver.cpp say to keep the pins, the radio
pins checked and the shared RX/TX rule in step.
- index.js reads the chip model through one chipModel().
- BootPins.RADIO is RADIO_PINS; the P4 test builds its config once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GPIO12 is a strapping pin on the original ESP32: high at reset, it powers
the flash at 1.8 V. A CC1101 can hold GDO2 high at power-up, so a board
with RX on 12 bootloops after a power cut until EN is pressed. It was the
default RX pin.
- RX defaults to GPIO35 on the original ESP32. Saved settings keep their
pins.
- The log warns each time the radio starts, and the radio settings show a
warning, while TX, RX or MISO is on a pin whose level at reset can stop
the chip booting (src/BootPins.h, data-src/js/pins.js).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The field is hasOtaSlot, like hasEthernet and the other "has" bools.
- The web UI shows the device's reason for any failed upload, not only
for /restore, so it shows "no OTA slot" from a page that missed the
header. The update routes' 500 replies give Update.errorString()
instead of an empty reason.
- Native: the writer leaves hasOtaSlot out when true and writes "os":0.
- e2e: header() next to uptime(); the test skips on firmware without
the field instead of failing, and matches the reason loosely. The
README says what the test sends.
- PROTOCOL.md and the comments name only the builds that exist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds with one app slot (C6, debug, and the C5 to come) have nowhere
for /updateFirmware to write. It answered "Error updating firmware: "
and rebooted.
- ConfigSettings::otaSlot, from esp_ota_get_next_update_partition().
- The header sends otaSlot, default 1, so only those builds send it.
- /updateFirmware refuses with 400 and a reason, and doesn't stop the
radio or reboot.
- The web UI's Update Firmware says to flash the onboard image over USB,
with a backup first. Firmware before the field leaves it undefined,
which keeps the uploader.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Saving the security settings threw: the form's values were named
security, hiding the Security object the new token goes to. A save from
None to None no longer reconnects, as the device closes nobody then.
- Each sync redraws the lists and reloads the settings, so a reconnect
shows what changed meanwhile; closing the config panel drops its feeds
even while the socket is down.
- A 401 on backup or an upload opens the login, as a refused socket does
(Security.tokenRefused); a failed firmware upload says so.
- The general and security panels fill in what the device leaves out at
its default, so a save never writes back what the form showed before.
The hostname holds 31 characters, as the device does.
- The views (settings with defaults, a group's shades, frames, scans, the
network and radio panels, op errors) move to data-src/js/views.js, with
tests. runOp is the one op path with the busy retry; repeatOf,
commandArgs, gone(); dead update*List and editedRepeaters removed.
- Tests for ProtoClient.off and a listener removing itself mid-event.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UI talks to the device over the one socket at /ws: the sync fills the
shade model, its events redraw the lists, and every read, command, edit,
setting, scan and reboot is an op. Only /loginContext, /login and the file
transfers stay on HTTP, so a page load opens no other connection.
- The 8080 socket, editJSON/requestResult and the /controller, /rooms,
/shades, /groups, /room, /shade, /group, settings and command routes are
no longer used by the UI.
- An edit's callback gets the item once its event is in (ProtoClient
waitFor); busy edits and saves are sent again.
- The token is kept in sessionStorage. When /ws is refused and
/loginContext says a login is needed, the client stops retrying and the
login panel opens. Backup and uploads send the token.
- A banner shows when the device speaks another protocol version.
- Removed dead code: the /pairShade call and its unused /unpairShade twin,
loadLogin, login.html (broken, linked from nowhere), Secure Config Only's
checkbox (protocol 2 has two modes) and the HTTP helpers nothing uses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- One socket at a time: a socket being replaced is let go without a word,
and connect() or a new token while a reconnect waits opens no second one.
A new token reconnects at once, without a refused.
- Events that came before the schema go as soon as it is whole, ahead of
the sync frames formatted after them, not at syncEnd over newer state.
- A schema's hash counts only once it has come whole, so a sync cut short
gets it again.
- Feeds are subscribed again after a reconnect; an unknown feed is refused.
- A long op refused as busy without its id goes again.
- A sync without repeaters clears them; order changes name their list; a
refused resync is called off.
- refused: a listener can close() to stop the retries, which the UI does
once /loginContext says a login is needed. The client itself can't tell a
refused token from a device that is down.
- localStorage keeps the schema by default. minify.py removes its work
folder even when a bundle fails. ADR 0005 records the two bundles.
- Smaller: ProtoSchema.ROWS/withoutRows, ProtoModel.ITEMS, named type
numbers, unused feeds map and FakeSocket.all gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- data-src/js: ProtoSchema (schema from the sync, short keys and names,
defaults, cached by hash), ProtoModel (rooms, shades, groups, repeaters,
header and status from the sync and the events) and ProtoClient (/ws,
hello, sync with next, op() as a Promise of its reply, events, feeds,
reconnect). Nothing uses them yet: 2b moves the UI onto them.
- minify.py joins js/*.js and index.js into index.js, and the three CSS
files into app.css, so a page load opens fewer connections.
- test/js: node --test on the three modules, replaying the recorded
fixtures; a CI job runs it and the firmware build waits for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>