Skip to content

0.8.5

Latest

Choose a tag to compare

@lhoward lhoward released this 05 Aug 04:54
· 7 commits to main since this release

0.8.5

Changes since 0.8.0.

Highlights

  • Hostname-resolved connections: init(host:port:) on every socket and secure backend, with resolution on each connect attempt and failover across candidate addresses.
  • OCP.1 top-level PDU decoding rewritten on swift-binary-parsing — 2.6-4x faster, and two remotely triggerable traps fixed.
  • Bulk blob coding: 1KB LengthTaggedData encode is 64x faster, decode 288x.
  • A cluster of subscription and command-handle lifecycle fixes, several of which could silently stall properties or hang a caller.
  • Android device discovery via NsdManager.
  • The remaining property-oriented AES70 sensor, actuator and agent classes.

Breaking changes

  • swift-tools-version raised to 6.2; new dependency on apple/swift-binary-parsing.
  • Ocp1MutableConnection is replaced by Ocp1MutableSocketAddressConnection, whose currency type is AnySocketAddress. Conformers declare two @_spi storage cells (_deviceAddresses, _connectedDeviceAddress) plus _connectDevice(to:); the extension derives the rest. Data-valued conveniences (deviceAddressData, the Data initialisers) are retained for compatibility.
  • The pre-resolved deviceAddress: TLS/DTLS initialisers are now package rather than public, and their hostname: parameter is renamed sniHostname: to make clear it sets the TLS server name only. Use init(host:port:) for the hostname-resolved path.
  • SwiftOCADevice.OcaRoot bumped to class version 3, matching SwiftOCA.OcaRoot and AES70.
  • Mach port transport is now gated on #if os(macOS) rather than #if canImport(Darwin).

Connections and device addressing

  • A mutable connection is now address-list aware and connects to the first reachable candidate, the connect itself being the reachability test. The clamped _connectionTimeout is divided across candidates so a black-holed early address cannot consume the whole budget before failover.
  • Candidate addresses, the connected candidate and an optional host:port are consolidated into a single Mutex-protected Ocp1DeviceAddressState, replacing the two separate cells each socket backend declared; presentation accessors now observe one consistent snapshot.
  • A connection constructed with host:port re-resolves on each connect attempt — getaddrinfo off the OcaConnection actor for the socket backends, natively (with Happy Eyeballs) for the Network.framework backend. An unresolved name yields an empty candidate set, treated as not-yet-reachable and retried, rather than a 0.0.0.0 placeholder.
  • For OpenSSL and NW secure backends, host doubles as the TLS server name for SNI and certificate verification.
  • Ocp1FlyingSocksConnection now conforms to the mutable-connection protocol, so its live device address can be re-pointed.
  • Address changes no longer schedule a reconnect while disconnected, which could otherwise flap a connection that had just come up off a deferred address.
  • A live connection is only migrated when the address actually in use is withdrawn. A device advertised by a multi-homed host gains and loses secondary candidates as records expire; previously any change to the set migrated the connection, and with automaticReconnect that repeated on every advertisement until the device timed out the abandoned sockets.
  • OcaConnectionBroker passes all candidates through in a deterministic order (IPv4 first, then bytes) so resolver reordering is not mistaken for a change.
  • NW per-candidate connect cancels the NWConnection on task cancellation, so a per-candidate timeout cannot leave the continuation parked.

OCP.1 wire format

  • Top-level PDU framing is parsed in a single pass over one ParserSpan; decodeOcp1MessagePdu returns [Ocp1Message] directly rather than [Data] slices for a second pass. Codable remains the inner decoder for message parameters.
    • Fixes two traps reachable from a single UDP datagram: a messageCount exceeding the messages actually present hit a precondition, and the pduSize bound was off by two, letting a keep-alive slice run past the buffer. Both now throw. (#22)
    • 1M rounds, release: notification decode 205ms → 78ms; 1-notification PDU 336ms → 97ms; 8-notification PDU 1991ms → 493ms.
  • Unvalidated wire-length fields use Int(throwingOnOverflow:) / Int(exactly:), so a declared size at or above Int32.max throws instead of trapping on 32-bit targets.
  • Ocp1Notification2's minimum-length guard is restored, so a truncated 13-byte notification is rejected rather than surfaced as a valid empty-payload event.
  • Blobs (LengthTaggedData16/32) are encoded and decoded in bulk in the coder dispatch via a new Ocp1BlobRepresentable, instead of a byte at a time through the unkeyed container. The generic container implementations remain for foreign coders such as JSONEncoder, and the bulk encoder throws invalidMessageSize for an oversized blob to match. 1M rounds, release, 1024 bytes: encode 16624ms → 259ms, decode 35206ms → 122ms.
  • Top-level PDU encoding appends directly rather than building a throwaway [UInt8] per single-byte field.
  • decodeInteger loads at an offset rather than forming a Data slice and entering withUnsafeBytes per integer.

Commands and subscriptions

  • Command handles are allocated and claimed in one step on the monitor that owns the outstanding set. Previously sendCommandRrq registered its continuation only after the write completed, so on a fast transport a response decoded concurrently found no continuation and the caller waited out the full responseTimeout for a response that had already arrived — or, if the write stalled past the timeout, suspended on a continuation nobody would ever resume and never returned at all.
    • The sender owns its entry for its whole scope and retires it with defer; response(for:) runs under withTaskCancellationHandler; teardown no longer discards a result that already arrived; and an unmatched response no longer aborts the remaining messages in a batched PDU.
    • The connection now stamps command handles rather than honouring a caller-supplied one, removing a precondition an out-of-tree caller could trip.
  • The command handle derives from a 64-bit request counter; it was an OcaUint32 incremented with +=, so a controller crashed after 2^32 requests.
  • isSubscribed now reports whether this object registered its own handler, rather than whether any component holds a subscription for the event. Because addSubscription() multiplexes handlers per event, one component subscribing to (oNo, PropertyChanged) previously left every other object's handler unregistered and its property subjects permanently quiet.
  • A failed AddSubscription now rolls back the local entry, which otherwise left isSubscribed(event:) == true with nothing registered on the device and no path to retry.
  • After disconnect() with .retainObjectCacheAfterDisconnect, an object holding a cancellable the connection has dropped can re-register; subscribe() and isSubscribed now test the cancellable against what the connection actually holds.

AES70 classes

Controller and device implementations of the remaining classes expressible with the existing property wrappers:

  • SensorsOcaTimeIntervalSensor, OcaFrequencySensor, OcaVoltageSensor, OcaCurrentSensor, OcaImpedanceSensor, OcaGainSensor, OcaPowerSensor, OcaStateSensor.
  • ActuatorsOcaDelay, OcaDelayExtended, OcaTemperatureActuator, OcaSamplingRateConverter, OcaSignalGenerator, OcaFilterClassical, OcaFilterParametric, OcaFilterPolynomial, OcaFilterFIR, OcaFilterArbitraryCurve, OcaDynamics, OcaDynamicsDetector, OcaDynamicsCurve.
  • AgentsOcaPowerSupply.

Where AES70 returns several properties from one method the property is declared without an accessor and the method spelled out (OcaPowerSensor.getReading(), OcaFilterPolynomial.getCoefficients(), the OcaDynamicsCurve list getters). Non-accessor methods — every SetMultiple(), OcaSignalGenerator.start()/stop(), OcaDelayExtended.GetDelayValueConverted() — are open and throw notImplemented. OcaStateSensor's State property is named reading, since OcaSensor already uses state for AES70's ReadingState. OcaImpedance and OcaDelayValue gain Comparable and public memberwise initialisers; OcaTransferFunction's members are made public.

The device-side class-version bump fixes registry lookups: OcaFrequencyActuator, OcaMediaClock3 and OcaTimeSource previously fell back to OcaActuator and OcaAgent.

Android

  • NsdManager-backed device browser behind the same OcaNetworkAdvertisingServiceBrowser protocol as the Darwin and dnssd browsers. ConnectionBroker.swift previously compiled the entire broker out on Android.
  • Resolution uses registerServiceInfoCallback (API 34+) rather than resolveService, which permits only one in-flight resolve per NsdManager and fails concurrent callers with FAILURE_ALREADY_ACTIVE.
  • The browser is a stateless proxy and deliberately does not deduplicate — the broker reads a repeat .added as an address change.
  • Gating is split: SWIFTOCA_ANDROID_NSD decides whether the target exists, the AndroidNSD trait decides whether it is linked. See Documentation/AndroidNSD.md.
  • Verified against real devices on an API 35 emulator: discovery, resolution, IPv4/IPv6 addresses, TXT records and a live connection.

Apple platforms

  • Mach port transport is gated on os(macOS). It reaches bootstrap_look_up/register/check_in via @_extern(c); those symbols are unavailable to sandboxed apps and flagged by App Store binary validation, so an iOS app embedding SwiftOCA was rejected merely for linking the references. SwiftOCADevice also calls IOKit, which is not public on iOS, and now takes the existing empty-string fallback.
  • swift-binary-parsing added to the Xcode project, mirroring the SwiftPM manifest.

OpenSSL

  • SSL_get_error is captured immediately after its SSL_* call rather than after an await. The error queue is thread-local and the cooperative pool can move the task across a suspension, so the code returned could belong to another connection — visible as a PSK handshake failing with "certificate verify failed", an error no party to that handshake produced, plus spurious notConnected and connectionTimeout. Over the pair that reproduced it most reliably this moves 60 runs from 7 failures to none.
  • The diagnostic queue is drained beside the code it belongs to, for the same reason.

Device storage

  • SQLite dataset storage tuned for devices writing frequently to flash: WAL journaling (roughly halving write amplification and fsync count while synchronous = FULL keeps power-cut durability), auto_vacuum = NONE on newly created databases, and no incremental_vacuum on every delete. WAL converts existing rollback-journal databases in place on first open, so this is safe for deployed devices.

Other

  • Controller identity equality uses === rather than ObjectIdentifier.
  • AnyOcaNetworkAdvertisingServiceInfo erases via implicit existential opening instead of two captured closures per discovered service.
  • Tests ignore SIGPIPE, which was terminating roughly one Linux run in four part way through the OpenSSL suites and reporting as a crash with zero failures.
  • README documents hostname-resolved connections.

Upgrade notes

  • Requires a Swift 6.2 toolchain.
  • Replace conformances to Ocp1MutableConnection with Ocp1MutableSocketAddressConnection and the two @_spi storage cells plus _connectDevice(to:).
  • Callers constructing TLS/DTLS connections from a resolved deviceAddress: need init(host:port:), or package access; hostname: is now sniHostname:.
  • Device implementations subclassing at class version 3 will now resolve correctly; verify any local workarounds for the previous fallback behaviour.
  • Android discovery requires the AndroidNSD trait and SWIFTOCA_ANDROID_NSD, plus AndroidNsd.configure(context:) at startup.