MIDI2PE
Property Exchange ModuleHigh-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
| Method | Description |
|---|---|
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
| Case | Description |
|---|---|
timeout(resource:) | Request timed out |
cancelled | Request was cancelled |
requestIDExhausted | All 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
| Property | Type | Description |
|---|---|---|
status | Int | HTTP-style status code (200, 404, etc.) |
header | PEHeader? | Parsed response header |
body | Data | Raw response body |
decodedBody | Data | Mcoded7-decoded body |
bodyString | String? | Body as UTF-8 string |
isSuccess | Bool | True if status 200-299 |
isError | Bool | True 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.