MIDI2PE

Property Exchange Module

High-level async/await API for MIDI 2.0 Property Exchange: GET, SET, and Subscribe operations with automatic chunking and timeout handling.

PEManager

The main actor for Property Exchange operations. Handles request/response lifecycle, multi-chunk assembly, timeouts, and subscriptions.

public actor PEManager

Initialization

let peManager = PEManager(
    transport: transport,
    sourceMUID: ciManager.muid,
    maxInflightPerDevice: 2,      // Max concurrent requests per device
    logger: ConsoleLogger()        // Optional logging
)

// Connect to CIManager for automatic destination resolution
peManager.destinationResolver = ciManager.makeDestinationResolver()

// Start receiving MIDI data
await peManager.startReceiving()

Lifecycle Methods

MethodDescription
startReceiving()Start processing incoming MIDI data
stopReceiving()Stop receiving and cancel all pending requests

GET Operations

Retrieve property values from a device.

Basic GET

// Using MUID (requires destinationResolver)
let response = try await peManager.get("DeviceInfo", from: device.muid)

// Using PEDeviceHandle (explicit destination)
let handle = PEDeviceHandle(muid: device.muid, destination: destID)
let response = try await peManager.get("DeviceInfo", from: handle)

GET with Parameters

// Channel-specific resource
let response = try await peManager.get(
    "ChannelSettings",
    channel: 1,
    from: device.muid
)

// Paginated resource
let response = try await peManager.get(
    "ProgramList",
    offset: 0,
    limit: 10,
    from: device.muid
)

// Custom timeout
let response = try await peManager.get(
    "LargeResource",
    from: device.muid,
    timeout: .seconds(30)
)

Convenience Methods

// Get DeviceInfo (parsed)
let deviceInfo = try await peManager.getDeviceInfo(from: device.muid)
print("Product: \(deviceInfo.productName ?? "Unknown")")
print("Manufacturer: \(deviceInfo.manufacturerName ?? "Unknown")")

// Get ResourceList (parsed)
let resources = try await peManager.getResourceList(from: device.muid)
for resource in resources {
    print("\(resource.resource): GET=\(resource.canGet ?? false)")
}

SET Operations

Write property values to a device.

// Set raw data
let data = try JSONEncoder().encode(["volume": 80])
let response = try await peManager.set(
    "ChannelSettings",
    data: data,
    to: device.muid
)

// Check success
if response.isSuccess {
    print("Settings updated")
} else {
    print("Error: \(response.status)")
}

Subscriptions

Subscribe to property change notifications.

Subscribe

// Subscribe to a resource
let response = try await peManager.subscribe(
    to: "CurrentProgram",
    on: device.muid
)

if response.isSuccess, let subscribeId = response.subscribeId {
    print("Subscribed with ID: \(subscribeId)")
}

Receive Notifications

// Start notification stream (single listener)
for await notification in peManager.startNotificationStream() {
    print("Resource changed: \(notification.resource)")
    print("Subscribe ID: \(notification.subscribeId)")
    print("Data: \(notification.data.count) bytes")
}

Unsubscribe

// Unsubscribe using the subscribeId
try await peManager.unsubscribe(subscribeId: subscribeId)

// Check active subscriptions
let subs = await peManager.subscriptions
for sub in subs {
    print("\(sub.resource) [\(sub.subscribeId)]")
}

Typed JSON API

Type-safe GET and SET with automatic JSON encoding/decoding.

Typed GET

// Define your response type
struct ProgramInfo: Decodable {
    let name: String
    let bankMSB: Int
    let bankLSB: Int
    let program: Int
}

// GET with automatic decoding
let program: ProgramInfo = try await peManager.getJSON(
    "CurrentProgram",
    from: device.muid
)
print("Current program: \(program.name)")

Typed SET

// Define your request type
struct VolumeSettings: Encodable {
    let volume: Int
    let pan: Int
}

// SET with automatic encoding
let settings = VolumeSettings(volume: 80, pan: 64)
let response = try await peManager.setJSON(
    "ChannelSettings",
    value: settings,
    channel: 1,
    to: device.muid
)

Error Handling

public enum PEError: Error, Sendable
CaseDescription
timeout(resource:)Request timed out
cancelledRequest was cancelled
requestIDExhaustedAll 128 request IDs in use
deviceError(status:message:)Device returned error status
deviceNotFound(MUID)Destination resolver returned nil
invalidResponse(String)Response parsing failed
transportError(Error)MIDI transport error
nak(PENAKDetails)Device rejected request

Error Handling Example

do {
    let response = try await peManager.get("DeviceInfo", from: device.muid)
    // Handle success
} catch PEError.timeout(let resource) {
    print("Timeout waiting for: \(resource)")
} catch PEError.deviceError(let status, let message) {
    print("Device error \(status): \(message ?? "unknown")")
} catch PEError.deviceNotFound(let muid) {
    print("Device not found: \(muid)")
} catch {
    print("Unexpected error: \(error)")
}

PEResponse

public struct PEResponse: Sendable
PropertyTypeDescription
statusIntHTTP-style status code (200, 404, etc.)
headerPEHeader?Parsed response header
bodyDataRaw response body
decodedBodyDataMcoded7-decoded body
bodyStringString?Body as UTF-8 string
isSuccessBoolTrue if status 200-299
isErrorBoolTrue if status 400+

💡 Tip: Use decodedBody instead of body when parsing JSON responses. Property Exchange data is Mcoded7-encoded for MIDI transmission, and decodedBody handles the decoding automatically.