WebSocket JSON-RPC SDK for PatternSpace integration.
PatternSpaceSDK gives calibration tools and automation clients a typed Swift interface for discovering PatternSpace devices, connecting over WebSocket, displaying patterns, querying capabilities, inspecting richer device/display state, adjusting Peak White, discovering output color presets, and receiving live status events.
- Swift Package Manager support
- No third-party dependencies
- Bonjour discovery via
_patternspace._tcp - JSON-RPC 2.0 request and notification envelopes
- WebSocket transport over Network.framework
- Optional bearer-token authentication
- Client API for capabilities, pattern, device, and display namespaces
- Server API for embedding the protocol in PatternSpace-compatible apps
- Swift Testing coverage for core JSON models, dispatch, input validation, WebSocket upgrade, and frame handling
- Swift 5.9+
- macOS 12+
- iOS 15+
- PatternSpace JSON protocol
1.3
Add the package in Xcode:
https://github.com/caplaz/PatternSpaceSDK.git
Or add it to a Swift package:
// swift-tools-version: 5.9
import PackageDescription
letpackage=Package(
name:"MyTool",
platforms:[.macOS(.v12),.iOS(.v15)],
dependencies:[.package(url:"https://github.com/caplaz/PatternSpaceSDK.git", from:"0.7.0")],
targets:[.executableTarget(
name:"MyTool",
dependencies:[.product(name:"PatternSpaceSDKClient",package:"PatternSpaceSDK")])])| Product | Purpose |
|---|---|
PatternSpaceSDKCore | Shared JSON-RPC envelopes, pattern models, device schemas, color types, and events. |
PatternSpaceSDKClient | Client-side discovery, connection, request/response handling, and typed namespaces. |
PatternSpaceSDKServer | Server-side listener, WebSocket upgrade and framing, request dispatch, validation, and event broadcast. |
import PatternSpaceSDKClient
import PatternSpaceSDKCore
letservices=awaitPatternSpaceDiscovery().discover(timeout:3)guardlet service = services.first else{fatalError("No PatternSpace device found")}letclient=PatternSpaceClient(service: service, token:"your-token")
client.connect()Task{forawaiteventin client.events {print("PatternSpace event:", event)}}tryawait client.pattern.displayColor(PSColor(r:1, g:0, b:0), bitDepth:.ten, size:10)letcapabilities=tryawait client.capabilities.list()print(capabilities.namespaces)tryawait client.pattern.displayPatch(
background:PSColor(r:0, g:0, b:0),
rectangles:[PatchRectangle(color:PSColor(r:1, g:1, b:1), x:0.25, y:0.25, width:0.5, height:0.5)],
bitDepth:.ten
)letdisplays=tryawait client.display.list()iflet selected = displays.displays.first(where: \.selected){
_ =tryawait client.display.setPeakWhite(displayId: selected.id, peakWhite:3.0)letpresets=tryawait client.display.listOutputColorPresets(displayId: selected.id)iflet hdr = presets.presets.first(where:{ $0.id ==.hdrBT2020PQ }){letconfig=tryawait client.display.getOutputColorPreset(displayId: selected.id, presetId: hdr.id)if config.preset.supported {
_ =tryawait client.display.setOutputColorPreset(displayId: selected.id, presetId: hdr.id)}}
_ =tryawait client.display.setMeasurementRange(
displayId: selected.id,
measurementRange:.legal
)}tryawait client.pattern.clear()
client.disconnect()import PatternSpaceSDKCore
import PatternSpaceSDKServer
finalclassDelegate:PatternSpaceServerDelegate{func displayPattern(id:String)asyncthrows{print("Display pattern", id)}func displayColor(_ color:PSColor, bitDepth:BitDepth)asyncthrows{print("Display color", color, bitDepth)}func displayPatch(_ params:PatchParams)asyncthrows{print("Display patch", params)}func clearDisplay()asyncthrows{print("Clear pattern")}func listPatterns(category:String?, subcategory:String?)asyncthrows->[PatternInfo]{[]}func getPattern(id:String)asyncthrows->PatternInfo{throwPSDispatchError(.patternNotFound)}func deviceInfo()asyncthrows->DeviceInfo{DeviceInfo(
name:"PatternSpace",
resolution:Resolution(width:3840, height:2160),
colorFormat:"RGB",
bitDepth:10,
hdrMode:"SDR",
refreshRate:60,
outputRange:"full")}func deviceStatus()asyncthrows->DeviceStatus{DeviceStatus(
currentPatternId:nil,
sourceActive:true,
selectedSource:"PatternSpace JSON",
selectedDisplayId:"main",
displayProfileResolved:true,
authRequired:true,
connectedClientCount:1,
appVersion:"1.1.0",
buildNumber:"1",
sdkVersion:PatternSpaceProtocolMetadata.sdkVersion,
protocolVersion:PatternSpaceProtocolMetadata.protocolVersion
)}func capabilities()asyncthrows->CapabilitiesResult{CapabilitiesResult(
protocolVersion:PatternSpaceProtocolMetadata.protocolVersion,
app:AppMetadata(name:"PatternSpace", version:"1.1.0", build:"1"),
sdkVersion:PatternSpaceProtocolMetadata.sdkVersion,
platform:.macOS,
authRequired:true,
namespaces:JSONRPCDispatcher.routeManifest,
features:CapabilityFeatures(
events:true,
displayInventory:true,
peakWhiteControl:true,
outputColorPresets:true,
measurementRange:true,
catalogPatterns:true,
customICCBuilder:false,
httpBridge:false))}func listDisplays()asyncthrows->DisplayListResult{DisplayListResult(
platform:.macOS,
selectedDisplayId:"main",
displays:[DisplayEntry(
id:"main",
name:"Main Display",
selected:true,
connection:.builtIn,
resolution:Resolution(width:3840, height:2160),
refreshRate:60,
colorSpaceName:"Display P3",
cgColorSpaceName:"kCGColorSpaceDisplayP3",
maximumPotentialEDR:4.0,
maximumCurrentEDR:2.0,
peakWhite:2.0,
effectivePeakWhite:2.0,
peakWhiteRange:PeakWhiteRange(maximum:4.0),
supportsPeakWhiteControl:false,
displayProfileResolved:true,
outputColorPresetId:.deviceNative,
selectedMeasurementRange:.full,
supportedOutputColorPresetIds:[.deviceNative,.sdrReferenceSRGB,.hdrP3D65PQ,.extLinearSRGBHDR],
outputColorPresetImplementationStatus:"native")])}func setPeakWhite(_ params:SetPeakWhiteParams)asyncthrows->DisplayEntry{guard params.displayId =="main"else{throwPSDispatchError(.displayNotFound, data:.object(["displayId":.string(params.displayId)]))}guard params.peakWhite >=PeakWhiteRange.absoluteMinimum, params.peakWhite <=4.0else{throwPSDispatchError(.peakWhiteOutOfRange,
data:.object(["displayId":.string(params.displayId),"peakWhite":.double(params.peakWhite),"minimum":.double(PeakWhiteRange.absoluteMinimum),"maximum":.double(4.0)]))}returnDisplayEntry(
id:"main",
name:"Main Display",
selected:true,
connection:.builtIn,
resolution:Resolution(width:3840, height:2160),
refreshRate:60,
colorSpaceName:"Display P3",
cgColorSpaceName:"kCGColorSpaceDisplayP3",
maximumPotentialEDR:4.0,
maximumCurrentEDR:2.0,
peakWhite: params.peakWhite,
effectivePeakWhite:min(params.peakWhite,2.0),
peakWhiteRange:PeakWhiteRange(maximum:4.0),
supportsPeakWhiteControl:false,
displayProfileResolved:true,
outputColorPresetId:.deviceNative,
supportedOutputColorPresetIds:[.deviceNative,.sdrReferenceSRGB,.hdrP3D65PQ,.extLinearSRGBHDR],
outputColorPresetImplementationStatus:"native")}func listOutputColorPresets(displayId:String)asyncthrows->OutputColorPresetList{guard displayId =="main"else{throwPSDispatchError(.displayNotFound, data:.object(["displayId":.string(displayId)]))}returnOutputColorPresetList(
displayId: displayId,
selectedPresetId:.deviceNative,
scope:.host,
catalogRevision:"2026-06-17.1",
presets:[OutputColorPresetSummary(
id:.deviceNative,
label:"Device Native",
group:"device",
family:.device,
supported:true,
requiresPro:false,
implementationStatus:.native
),OutputColorPresetSummary(
id:.extLinearSRGBHDR,
label:"Extended Linear sRGB HDR",
group:"linearHDR",
family:.linearHDR,
supported:true,
requiresPro:true,
implementationStatus:.native
)])}func getOutputColorPreset(_ params:GetOutputColorPresetParams)asyncthrows->GetOutputColorPresetResult{guard params.displayId =="main"else{throwPSDispatchError(.displayNotFound, data:.object(["displayId":.string(params.displayId)]))}returnGetOutputColorPresetResult(
displayId: params.displayId,
catalogRevision:"2026-06-17.1",
preset:OutputColorPresetConfig(
id: params.presetId,
label:"Device Native",
group:"device",
family:.device,
gamut:.displayNative,
whitePoint:.displayNative,
transfer:.displayNative,
dynamicRange:.sdr,
toneMapping:.none,
inputEncoding:.displayCode,
implementationStatus:.native,
supported:true,
requiresPro:false))}varisSourceActive:Bool{true}}letdelegate=Delegate()letserver=PatternSpaceServer(
token:"your-token",
delegate: delegate,
connectionReady:{ authenticated inConnectionReadyParams(
protocolVersion:PatternSpaceProtocolMetadata.protocolVersion,
name:"PatternSpace",
resolution:Resolution(width:3840, height:2160),
colorFormat:"RGB",
bitDepth:10,
hdrMode:"SDR",
refreshRate:60,
outputRange:"full",
currentPatternId:nil,
sourceActive:true,
authenticated: authenticated,)})try server.start(port:7878, deviceName:"PatternSpace")PatternSpaceServer is single-client by design. When a new WebSocket upgrade succeeds, the server drops any existing client before sending connectionReady to the new one.
When a server is configured with a token, clients must send:
Authorization: Bearer <token>The server rejects unauthorized upgrade requests with HTTP 401 Unauthorized before accepting the WebSocket. Token comparison is constant-time. Prefer a high-entropy per-device token stored in the platform keychain or another secure credential store.
Pass nil for the server token to run in insecure mode. Insecure mode should be limited to trusted local networks or test harnesses.
The SDK uses JSON-RPC 2.0 envelopes over WebSocket at:
ws://<host>:7878/patternspace
/patternspace is the canonical path. The server also accepts upgrades on any
path (for example /), because NWProtocolWebSocket clients connecting through
a hostPort or Bonjour service endpoint cannot attach a path. Authentication is
enforced by the bearer token, not the path.
Supported method namespaces:
Patch color methods:
pattern.displayColorpattern.displayPatchpattern.clear
Existing pattern list methods:
pattern.listpattern.displaypattern.get
Capabilities methods:
capabilities.list
Device methods:
device.infodevice.status
Display methods:
display.listdisplay.setPeakWhitedisplay.listOutputColorPresetsdisplay.getOutputColorPresetdisplay.setOutputColorPresetdisplay.setMeasurementRange
Notifications:
connectionReadypattern.changeddevice.statusChangeddisplay.changed
See Documentation for the wire-format overview.
The sourceActive field is a race-condition guard. In normal PatternSpace operation, changing away from the JSON source stops the server and closes the socket rather than keeping a client connected with sourceActive: false.
swift test
swift buildThe package intentionally avoids third-party dependencies so it can be embedded in calibration tools and app targets with minimal supply-chain surface.
PatternSpaceSDK follows semantic versioning. The initial 0.x series may still adjust public API while the JSON protocol stabilizes.
PatternSpaceSDK is available under the Apache License 2.0. See LICENSE.