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.

PEResponder

MIDI-CI Property Exchange responder for testing and device simulation. New in v1.0.5.

public actor PEResponder

PEResponder handles incoming PE Inquiry messages (GET, SET, Subscribe) and sends appropriate Reply messages. It manages an in-memory resource store and subscription state.

Resource Types

TypeDescription
InMemoryResourceMutable resource stored in memory (for testing)
StaticResourceImmutable resource with fixed value
ComputedResourceDynamic resource computed on each GET
ListResourceReturns a JSON array (e.g., ResourceList)

Usage with MockDevice

PEResponder is typically used through MockDevice, which combines CIManager and PEResponder for complete device simulation.

import MIDI2Kit
import MIDI2PE

// PEResponder is usually created internally by MockDevice
let mockDevice = MockDevice(transport: transport, preset: .korgModulePro)

// Add custom resource
await mockDevice.peResponder.setResource("CustomProp", value: ["key": "value"])

// Resource is now available via PE GET
let response = try await peManager.get("CustomProp", from: mockDevice.handle)

Custom Resource Registration

// Register a computed resource
await peResponder.registerResource(
    ComputedResource(name: "Time") {
        return ["timestamp": Date().timeIntervalSince1970]
    }
)

// Register a static resource
await peResponder.registerResource(
    StaticResource(name: "Version", value: ["version": "1.0.0"])
)