Skip to content

Advertise API 1.17: serial proxy ownership and port mode - #34

Merged
bbangert merged 5 commits into
mainfrom
feat/api-1-17
Sep 16, 2026
Merged

bbangert merged 5 commits into
mainfrom
feat/api-1-17

Conversation

@bbangert

@bbangert bbangert commented Sep 15, 2026 •

Copy link
Copy Markdown
Owner
  • Advertise ESPHome Native API 1.17 (@api_version_minor 17); package version 0.11.0
  • Serial proxy single-owner rule: the SUBSCRIBEd connection owns the instance; CONFIGURE / SET_MODEM_PINS / FLUSH / SET_MODE from a non-owner are acknowledged PORT_IN_USE, a non-owner's WRITE is dropped, GET_MODEM_PINS stays ungated
  • Client-facing change: the rule applies before anyone subscribes, so a client that CONFIGUREs or WRITEs without a prior SUBSCRIBE is now refused or dropped, as on real 1.17 firmware
  • Ownership tracked in Espex.Server beside BLE address ownership: claim_serial_owner/3, release_serial_owner/3, release_all_serial_owners/2, serial_owner/2; one monitor per connection pid shared by both kinds; DOWN sweep releases both; dead recorded owner is taken over by the next claim
  • SUBSCRIBE / UNSUBSCRIBE become {:serial_subscribe, i} / {:serial_unsubscribe, i} actions resolved by the Connection (claim, record intent, lazy open, ack); Dispatch gates on the local intent only
  • UNSUBSCRIBE and disconnect close the handle before releasing ownership; cleanup releases tolerate a dead Server
  • SerialProxySetModeRequest (RAW / PROTOCOL) acknowledged with SET_MODE, backed by new optional Espex.SerialProxy.set_mode/2; RAW always OK, PROTOCOL NOT_SUPPORTED without the callback, out-of-range mode INVALID_ARGUMENT
  • Mode is per connection and instance: reapplied after a CONFIGURE reopen (a failed reapply drops the recorded mode); the mode dies with the handle — UNSUBSCRIBE and disconnect close it, adapters tear their protocol handler down in close/1
  • Inbound serial data is delivered to the owning connection only; teardown runs once per connection and releases both ownership kinds in one Server call
  • Espex.SerialProxy moduledoc: new "Ownership" and "Port mode" sections, updated acknowledgement table
  • README, architecture guide, entity-types guide updated for 1.17
  • New test/espex/api_1_17_test.exs; existing serial integration tests subscribe first; Espex.Test.TcpClient.subscribe/3 and Espex.Test.ModeTrackingSerialProxy added

🤖 Generated with Claude Code

The connection that SUBSCRIBEs a serial proxy instance owns it, as on
ESPHome 2026.10. CONFIGURE, SET_MODEM_PINS, FLUSH and SET_MODE from any
other connection are acknowledged PORT_IN_USE, a non-owner's WRITE is
dropped, and GET_MODEM_PINS stays ungated. This applies before anyone
has subscribed too, so a client that used to CONFIGURE or WRITE first
must now SUBSCRIBE first.

Ownership lives in Espex.Server beside BLE address ownership, with one
monitor per connection pid shared by both kinds and a DOWN sweep that
releases both; a claim against an owner whose process has died takes
over. Dispatch gates on the local subscribe intent, which the
Connection records only after a successful claim, so the gate stays
pure. UNSUBSCRIBE and disconnect close the handle before releasing
ownership so a free instance is really free for exclusive-open
adapters, and teardown tolerates a Server that is already down.

SerialProxySetModeRequest is backed by a new optional set_mode/2
adapter callback. RAW is always OK, PROTOCOL is NOT_SUPPORTED without
the callback, and the mode belongs to the session: it is reapplied
after a CONFIGURE reopen and reset to raw through the adapter on
UNSUBSCRIBE.

Version 0.11.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@bbangert
bbangert requested a balanced review from Copilot September 15, 2026 15:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Disconnect paths can expose or re-close serial handles after ownership has been released.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 19/19 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread lib/espex/connection.ex Outdated
Comment thread lib/espex/server.ex
cleanup/1 ran twice on in-process close paths with the same
opened_ports, so the second pass could close a handle the next owner
had already reopened. It now returns the cleared state, which the
close sites thread into {:close, _} and {:stop, _}.

A connection killed outright never reaches close/1 while its ownership
is still swept, so a handle must not outlive the subscriber pid it was
opened for; documented on open/3 and in the Ownership section.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

SET_MODE validation violates owner-first semantics, and BLE cleanup can skip adapter disconnects when the Server is unavailable.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

lib/espex/connection.ex:1148

  • When the Server is already down (the :rest_for_one case described above), release_all_owners/2 catches the exit and returns []. Cleanup then skips adapter.disconnect/1 even for addresses still present in state.bluetooth_owned, leaking adapter-side BLE connections during Server restarts. Union the locally owned addresses with the Server result so teardown still reaches the adapter while retaining the Server-side drift protection.
    lib/espex/dispatch.ex:291
  • Validate ownership before the mode value. As written, an unsubscribed client can send mode 7 and receive INVALID_ARGUMENT, even though this PR's single-owner rule says every non-owner SET_MODE is PORT_IN_USE; upstream likewise checks is_subscriber_ before validating the mode. This leaks a different externally visible result for the same non-owner operation.
  • Files reviewed: 19/19 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

…E addresses on teardown

Upstream set_mode_from_client refuses a non-owner before looking at
the mode, so a non-owner is PORT_IN_USE whatever it sent; the enum
check now runs after the owner gate, as for CONFIGURE.

BLE teardown disconnected only the addresses the Server returned, so
a Server that was already down left adapter-side connections open.
The locally owned set is unioned in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-process ownership, monitoring, and adapter teardown semantics warrant final human validation despite comprehensive coverage.

Review details
  • Files reviewed: 19/19 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

ThousandIsland routes every close through exactly one handle_*
callback, each of which already ran cleanup/1; the pre-close calls
made it run twice and the cleared-state threading only papered over
that. Teardown now runs once, closes serial handles, releases both
ownership kinds in one Server call, then disconnects BLE addresses.

Inbound bytes were forwarded to any connection holding a handle, which
a non-owner can obtain through the ungated GET_MODEM_PINS; only the
subscribed connection receives them now. Closing the handle on
UNSUBSCRIBE made the explicit set_mode(:raw) redundant, so the mode
dies with the handle and adapters tear down in close/1. UNSUBSCRIBE
acks OK once torn down, RAW is OK without a handle, and the docs state
the keepalive window before a vanished peer's port is taken over.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Mode-reapply failures can leave the adapter in RAW mode while reporting success and retaining PROTOCOL state, and teardown behavior contradicts the PR description.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

lib/espex/serial_proxy.ex:252

  • This says every first operation opens and every operation except GET_MODEM_PINS requires prior ownership, but SUBSCRIBE acquires ownership itself and a non-owner's idempotent UNSUBSCRIBE neither owns nor opens the port. Rewording the lifecycle avoids giving adapter authors and clients an incorrect sequencing contract.

lib/espex/connection.ex:1292

  • After a client successfully selects PROTOCOL, CONFIGURE opens a fresh raw handle, but a valid failure from this reapply is only logged. The caller then acknowledges CONFIGURE as OK and keeps serial_modes[instance] == :protocol, even though the adapter is actually raw, so subsequent traffic violates the advertised session mode. Propagate the reapply failure into the CONFIGURE result and either close the unusable handle or update the recorded mode.
        case set_serial_mode({:ok, handle}, adapter, :protocol) do
          {:ok, :ok} ->
            :ok

          other ->
  • Files reviewed: 20/20 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread lib/espex/connection.ex
A CONFIGURE reopen that could not re-enter PROTOCOL kept the mode
recorded while the fresh handle was raw. The recorded mode is now
dropped so state matches the adapter; the client can re-send
SET_MODE. The lazy-open paragraph no longer says SUBSCRIBE needs
prior ownership.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

Ownership, lifecycle ordering, protocol behavior, documentation, and integration coverage are consistent.

Review details
  • Files reviewed: 20/20 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@bbangert
bbangert merged commit 2cd1be7 into main Sep 16, 2026
4 checks passed
@bbangert
bbangert deleted the feat/api-1-17 branch September 16, 2026 02:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants