commit e2a37ce0126d3c21cfe1f0243ade0bb14c580ec9
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-10-15T03:07:01Z |
| subject | Extend exchange timeout for sesame |
commit e2a37ce0126d3c21cfe1f0243ade0bb14c580ec9
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-10-15T03:07:01Z
Extend exchange timeout for sesame
---
build.env | 2 +-
drivers/sesame/README.md | 12 ++++++++++--
drivers/sesame/bluez.go | 12 ++++++++++--
drivers/sesame/bluez_test.go | 26 ++++++++++++++++++--------
drivers/sesame/client.go | 6 +++---
drivers/sesame/client_test.go | 31 +++++++++++++++++++++++++++++++
ha-app/CHANGELOG.md | 4 ++++
ha-app/config.yaml | 2 +-
internal/physical/manager.go | 2 +-
9 files changed, 79 insertions(+), 18 deletions(-)
diff --git a/build.env b/build.env
index cfc1006..fe9401c 100644
--- a/build.env
+++ b/build.env
@@ -1,4 +1,4 @@
IMAGE_NAME=ghcr.io/tgckpg/homekey-go
-IMAGE_TAG=0.0.14
+IMAGE_TAG=0.0.15
APT_PROXY=
diff --git a/drivers/sesame/README.md b/drivers/sesame/README.md
index e6cd5d5..5185f8a 100644
--- a/drivers/sesame/README.md
+++ b/drivers/sesame/README.md
@@ -28,8 +28,8 @@ manufacturer reports, and connection setup requires a report from the current
scan (there is no cached-address fallback). This follows the Android SDK's
ReceivedAdV → connectGatt(TRANSPORT_LE) → service discovery → notifications →
initial-token/login sequence. BlueZ handles the platform-specific GATT setup.
-Connect and GATT setup each have an 8-second limit; transient setup failures
-close the failed link and rescan, up to three attempts within the 30-second
+Connect has an 8-second limit and GATT setup has a 45-second limit; transient setup failures
+close the failed link and rescan, up to three attempts within the 60-second
setup deadline, plus bounded scan/disconnect cleanup. Commands and registration
exchanges are never automatically replayed. Disconnect releases notifications
before closing the device. Connection setup and login have separate deadlines;
@@ -80,3 +80,11 @@ filters, rescan after a failed connection, write-without-response selection,
buffering of an initial token emitted during StartNotify, and StopNotify before
Disconnect. It does not emulate controller radio behavior or establish that
Android and a Linux USB dongle have equivalent connection reliability.
+
+A real Sesame 6 Pro trace showed successful ATT responses roughly one second
+apart at a 500 ms connection interval, with discovery still progressing when
+the old 8-second GATT budget expired. GATT now allows 45 seconds within a
+60-second scan/connect/setup budget. Enrollment allows 90 seconds overall so
+setup does not consume the separate initial-token and registration budgets.
+Login and command timeouts are unchanged. The private-bus regression test also
+simulates discovery lasting longer than eight seconds.
diff --git a/drivers/sesame/bluez.go b/drivers/sesame/bluez.go
index 6e5ad41..3230172 100644
--- a/drivers/sesame/bluez.go
+++ b/drivers/sesame/bluez.go
@@ -15,6 +15,14 @@ import (
"github.com/godbus/dbus/v5"
)
+// A capture from a Sesame 6 Pro showed valid discovery responses about one
+// second apart on a 500 ms connection interval. Eight seconds aborts discovery
+// before BlueZ can finish enumerating characteristics and descriptors.
+const (
+ setupTimeout = 60 * time.Second
+ gattTimeout = 45 * time.Second
+)
+
type Link interface {
Write(context.Context, []byte) error
Packets() <-chan []byte
@@ -354,7 +362,7 @@ func DialBlueZ(ctx context.Context, t Target) (Link, Target, error) {
return nil, Target{}, e
}
// Also bound callers which do not supply the normal Client/Probe deadline.
- setup, cancel := context.WithTimeout(ctx, 30*time.Second)
+ setup, cancel := context.WithTimeout(ctx, setupTimeout)
defer cancel()
return dialSetup(setup, t, dialBlueZOnce)
}
@@ -398,7 +406,7 @@ func dialBlueZConn(ctx context.Context, t Target, c *dbus.Conn) (Link, Target, e
return nil, Target{}, &setupRetryError{fmt.Errorf("connect Sesame %s on %s: %w", found.Address, t.Adapter, e)}
}
log.Printf("Sesame %s at %s on %s: BLE connected; resolving GATT", t.UUID, found.Address, t.Adapter)
- gatt, cancelGATT := context.WithTimeout(ctx, 8*time.Second)
+ gatt, cancelGATT := context.WithTimeout(ctx, gattTimeout)
defer cancelGATT()
var writePath, notifyPath dbus.ObjectPath
mode := ""
diff --git a/drivers/sesame/bluez_test.go b/drivers/sesame/bluez_test.go
index a5565a1..7dd6015 100644
--- a/drivers/sesame/bluez_test.go
+++ b/drivers/sesame/bluez_test.go
@@ -17,11 +17,13 @@ import (
// A private bus exercises the actual transport, without a controller or BlueZ.
type testBlueZ struct {
- mu sync.Mutex
- conn *dbus.Conn
- objects objects
- calls []string
- attempts int
+ mu sync.Mutex
+ conn *dbus.Conn
+ objects objects
+ calls []string
+ attempts int
+ resolutionDelay time.Duration
+ readyAt time.Time
}
const testDevice dbus.ObjectPath = "/org/bluez/hci0/dev_FF_FF_FF_FF_FF_FF"
@@ -32,6 +34,9 @@ const testNotify dbus.ObjectPath = testService + "/char0003"
func (b *testBlueZ) GetManagedObjects() (objects, *dbus.Error) {
b.mu.Lock()
defer b.mu.Unlock()
+ if !b.readyAt.IsZero() && !time.Now().Before(b.readyAt) {
+ b.objects[testDevice]["org.bluez.Device1"]["ServicesResolved"] = dbus.MakeVariant(true)
+ }
// Copy through the D-Bus encoder before unlocking is not guaranteed, so return
// a fresh device map while other objects are immutable.
out := objects{}
@@ -71,7 +76,8 @@ func (b *testBlueZ) Connect() *dbus.Error {
return dbus.NewError("org.bluez.Error.Failed", []interface{}{"le-connection-abort-by-local"})
}
b.objects[testDevice]["org.bluez.Device1"]["Connected"] = dbus.MakeVariant(true)
- b.objects[testDevice]["org.bluez.Device1"]["ServicesResolved"] = dbus.MakeVariant(true)
+ b.readyAt = time.Now().Add(b.resolutionDelay)
+ b.objects[testDevice]["org.bluez.Device1"]["ServicesResolved"] = dbus.MakeVariant(false)
return nil
}
func (b *testBlueZ) Disconnect() *dbus.Error {
@@ -105,6 +111,10 @@ func (b *testBlueZ) WriteValue(data []byte, options map[string]dbus.Variant) *db
return nil
}
func TestBlueZWorkflow(t *testing.T) {
+ t.Run("immediate services", func(t *testing.T) { testBlueZWorkflow(t, 0) })
+ t.Run("discovery exceeds old eight-second limit", func(t *testing.T) { testBlueZWorkflow(t, 8500*time.Millisecond) })
+}
+func testBlueZWorkflow(t *testing.T, resolutionDelay time.Duration) {
daemon, e := exec.LookPath("dbus-daemon")
if e != nil {
t.Skip("dbus-daemon unavailable")
@@ -138,7 +148,7 @@ func TestBlueZWorkflow(t *testing.T) {
t.Fatal(e)
}
advertisement := append([]byte{21, 0, 1}, unhex("00112233445566778899aabbccddeeff")...)
- b := &testBlueZ{conn: conn, objects: objects{
+ b := &testBlueZ{conn: conn, resolutionDelay: resolutionDelay, objects: objects{
testDevice: {"org.bluez.Device1": {
"ManufacturerData": dbus.MakeVariant(map[uint16]dbus.Variant{0x055a: dbus.MakeVariant(advertisement)}),
"Address": dbus.MakeVariant("FF:FF:FF:FF:FF:FF"), "Adapter": dbus.MakeVariant(dbus.ObjectPath("/org/bluez/hci0")),
@@ -153,7 +163,7 @@ func TestBlueZWorkflow(t *testing.T) {
t.Fatal(e)
}
}
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
link, actual, e := dialSetup(ctx, testTarget, func(ctx context.Context, target Target) (Link, Target, error) {
c, err := dbus.Connect(address, dbus.WithAuth(dbus.AuthAnonymous()))
diff --git a/drivers/sesame/client.go b/drivers/sesame/client.go
index 59d3c86..84b20bd 100644
--- a/drivers/sesame/client.go
+++ b/drivers/sesame/client.go
@@ -173,7 +173,7 @@ func login(ctx context.Context, l Link, c Credential, update func(State)) (*sess
// immediately after deriving the credential, before any additional BLE command.
// A registered device must instead be imported; this API never resets a lock.
func Register(ctx context.Context, t Target, dial Dialer, persist func(Credential) error) error {
- connect, cancel := context.WithTimeout(ctx, 30*time.Second)
+ connect, cancel := context.WithTimeout(ctx, setupTimeout)
l, actual, e := dial(connect, t)
cancel()
if e != nil {
@@ -221,7 +221,7 @@ func Register(ctx context.Context, t Target, dial Dialer, persist func(Credentia
return nil
}
func Probe(ctx context.Context, t Target, c Credential, dial Dialer) error {
- connect, cancel := context.WithTimeout(ctx, 30*time.Second)
+ connect, cancel := context.WithTimeout(ctx, setupTimeout)
l, actual, e := dial(connect, t)
cancel()
if e != nil {
@@ -298,7 +298,7 @@ func (c *Client) Run(ctx context.Context) {
}
}
func (c *Client) runSession(ctx context.Context) error {
- connect, cancel := context.WithTimeout(ctx, 30*time.Second)
+ connect, cancel := context.WithTimeout(ctx, setupTimeout)
l, actual, e := c.dial(connect, c.target)
cancel()
if e != nil {
diff --git a/drivers/sesame/client_test.go b/drivers/sesame/client_test.go
index 1652a5e..9f6f08c 100644
--- a/drivers/sesame/client_test.go
+++ b/drivers/sesame/client_test.go
@@ -358,3 +358,34 @@ func TestReconnectUsesNewTokenAndResetsCipher(t *testing.T) {
t.Fatal("unexpected reconnect attempts", attempts.Load())
}
}
+
+// A longer transport timeout is ineffective if any session entry point still
+// wraps it in the old 30-second context.
+func TestSessionEntryPointsAllowSlowDiscovery(t *testing.T) {
+ expected := errors.New("stop before handshake")
+ for _, name := range []string{"register", "probe", "client"} {
+ t.Run(name, func(t *testing.T) {
+ called := false
+ dial := func(ctx context.Context, target Target) (Link, Target, error) {
+ called = true
+ deadline, ok := ctx.Deadline()
+ if !ok || time.Until(deadline) < 59*time.Second || time.Until(deadline) > 60*time.Second {
+ t.Errorf("unexpected setup budget: %v", time.Until(deadline))
+ }
+ return nil, Target{}, expected
+ }
+ var err error
+ switch name {
+ case "register":
+ err = Register(context.Background(), testTarget, dial, func(Credential) error { t.Fatal("unexpected registration"); return nil })
+ case "probe":
+ err = Probe(context.Background(), testTarget, testKey, dial)
+ case "client":
+ err = NewClient(testTarget, testKey, dial).runSession(context.Background())
+ }
+ if !called || !errors.Is(err, expected) {
+ t.Fatal(called, err)
+ }
+ })
+ }
+}
diff --git a/ha-app/CHANGELOG.md b/ha-app/CHANGELOG.md
index 7c908ee..1590d7e 100644
--- a/ha-app/CHANGELOG.md
+++ b/ha-app/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.0.15
+
+- Extend GATT timeout
+
## 0.0.14
- Update sesame connection workflow
diff --git a/ha-app/config.yaml b/ha-app/config.yaml
index d339af4..e27a8b8 100644
--- a/ha-app/config.yaml
+++ b/ha-app/config.yaml
@@ -1,5 +1,5 @@
name: Home Key Go
-version: "0.0.14"
+version: "0.0.15"
slug: homekey_go
description: HomeKit virtual lock with Apple Home Key NFC authentication
url: https://sgit.k8s.astropenguin.net/penguin/homekey-go/
diff --git a/internal/physical/manager.go b/internal/physical/manager.go
index 6ca034f..17c4566 100644
--- a/internal/physical/manager.go
+++ b/internal/physical/manager.go
@@ -290,7 +290,7 @@ func (m *Manager) Add(ctx context.Context, in Enrollment) (Record, error) {
// Leave room for separate BLE discovery/connect, initial-token and
// registration budgets, plus persistence. A slow dial must not exhaust
// the handshake deadline before the lock can answer.
- ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
+ ctx, cancel := context.WithTimeout(ctx, 90*time.Second)
defer cancel()
stop := context.AfterFunc(m.ctx, cancel)
defer stop()