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.
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
| Type | Description |
|---|---|
InMemoryResource | Mutable resource stored in memory (for testing) |
StaticResource | Immutable resource with fixed value |
ComputedResource | Dynamic resource computed on each GET |
ListResource | Returns 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"])
)