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
LengthTaggedDataencode 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-versionraised to 6.2; new dependency onapple/swift-binary-parsing.Ocp1MutableConnectionis replaced byOcp1MutableSocketAddressConnection, whose currency type isAnySocketAddress. Conformers declare two@_spistorage cells (_deviceAddresses,_connectedDeviceAddress) plus_connectDevice(to:); the extension derives the rest.Data-valued conveniences (deviceAddressData, theDatainitialisers) are retained for compatibility.- The pre-resolved
deviceAddress:TLS/DTLS initialisers are nowpackagerather thanpublic, and theirhostname:parameter is renamedsniHostname:to make clear it sets the TLS server name only. Useinit(host:port:)for the hostname-resolved path. SwiftOCADevice.OcaRootbumped to class version 3, matchingSwiftOCA.OcaRootand 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
_connectionTimeoutis 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:portare consolidated into a singleMutex-protectedOcp1DeviceAddressState, replacing the two separate cells each socket backend declared; presentation accessors now observe one consistent snapshot. - A connection constructed with
host:portre-resolves on each connect attempt —getaddrinfooff theOcaConnectionactor 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 a0.0.0.0placeholder. - For OpenSSL and NW secure backends,
hostdoubles as the TLS server name for SNI and certificate verification. Ocp1FlyingSocksConnectionnow 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
automaticReconnectthat repeated on every advertisement until the device timed out the abandoned sockets. OcaConnectionBrokerpasses 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
NWConnectionon 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;decodeOcp1MessagePdureturns[Ocp1Message]directly rather than[Data]slices for a second pass.Codableremains the inner decoder for message parameters.- Fixes two traps reachable from a single UDP datagram: a
messageCountexceeding the messages actually present hit a precondition, and thepduSizebound 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.
- Fixes two traps reachable from a single UDP datagram: a
- Unvalidated wire-length fields use
Int(throwingOnOverflow:)/Int(exactly:), so a declared size at or aboveInt32.maxthrows 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 newOcp1BlobRepresentable, instead of a byte at a time through the unkeyed container. The generic container implementations remain for foreign coders such asJSONEncoder, and the bulk encoder throwsinvalidMessageSizefor 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. decodeIntegerloads at an offset rather than forming aDataslice and enteringwithUnsafeBytesper integer.
Commands and subscriptions
- Command handles are allocated and claimed in one step on the monitor that owns the outstanding set. Previously
sendCommandRrqregistered 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 fullresponseTimeoutfor 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 underwithTaskCancellationHandler; 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 sender owns its entry for its whole scope and retires it with
- The command handle derives from a 64-bit request counter; it was an
OcaUint32incremented with+=, so a controller crashed after 2^32 requests. isSubscribednow reports whether this object registered its own handler, rather than whether any component holds a subscription for the event. BecauseaddSubscription()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
AddSubscriptionnow rolls back the local entry, which otherwise leftisSubscribed(event:) == truewith 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()andisSubscribednow 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:
- Sensors —
OcaTimeIntervalSensor,OcaFrequencySensor,OcaVoltageSensor,OcaCurrentSensor,OcaImpedanceSensor,OcaGainSensor,OcaPowerSensor,OcaStateSensor. - Actuators —
OcaDelay,OcaDelayExtended,OcaTemperatureActuator,OcaSamplingRateConverter,OcaSignalGenerator,OcaFilterClassical,OcaFilterParametric,OcaFilterPolynomial,OcaFilterFIR,OcaFilterArbitraryCurve,OcaDynamics,OcaDynamicsDetector,OcaDynamicsCurve. - Agents —
OcaPowerSupply.
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 sameOcaNetworkAdvertisingServiceBrowserprotocol as the Darwin and dnssd browsers.ConnectionBroker.swiftpreviously compiled the entire broker out on Android.- Resolution uses
registerServiceInfoCallback(API 34+) rather thanresolveService, which permits only one in-flight resolve perNsdManagerand fails concurrent callers withFAILURE_ALREADY_ACTIVE. - The browser is a stateless proxy and deliberately does not deduplicate — the broker reads a repeat
.addedas an address change. - Gating is split:
SWIFTOCA_ANDROID_NSDdecides whether the target exists, theAndroidNSDtrait decides whether it is linked. SeeDocumentation/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 reachesbootstrap_look_up/register/check_invia@_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.SwiftOCADevicealso calls IOKit, which is not public on iOS, and now takes the existing empty-string fallback. swift-binary-parsingadded to the Xcode project, mirroring the SwiftPM manifest.
OpenSSL
SSL_get_erroris captured immediately after itsSSL_*call rather than after anawait. 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 spuriousnotConnectedandconnectionTimeout. 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 = FULLkeeps power-cut durability),auto_vacuum = NONEon newly created databases, and noincremental_vacuumon 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 thanObjectIdentifier. AnyOcaNetworkAdvertisingServiceInfoerases 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
Ocp1MutableConnectionwithOcp1MutableSocketAddressConnectionand the two@_spistorage cells plus_connectDevice(to:). - Callers constructing TLS/DTLS connections from a resolved
deviceAddress:needinit(host:port:), orpackageaccess;hostname:is nowsniHostname:. - Device implementations subclassing at class version 3 will now resolve correctly; verify any local workarounds for the previous fallback behaviour.
- Android discovery requires the
AndroidNSDtrait andSWIFTOCA_ANDROID_NSD, plusAndroidNsd.configure(context:)at startup.