> ## Documentation Index
> Fetch the complete documentation index at: https://docs.plaud.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Low-Level iOS SDK Methods

> Low-level Embedded SDK methods for advanced use cases

## High-level Facades

The Plaud Embedded iOS SDK has a set of high-level interfaces for basic usage. These include:

1. **PlaudDeviceAgent**
2. **PlaudWiFiAgent**

These high-level methods and callbacks in these facades should cover most use cases including:

1. **Device Connection**: Connecting to & disconnecting from Plaud devices via mobile app
2. **File Management**: Syncing files from Plaud device to mobile app
3. **Firmware Updates**: Updating Plaud device firmware

See our [iOS SDK reference](/plaud-embedded/ios-sdk) for examples and documentation on these high-level facades.

```
  Your app
     │
     ├── PlaudDeviceAgent  ──────┐   (High-level)
     ├── PlaudWiFiAgent   ───┐   │
     │                       │   │
     │                       ▼   ▼
     │            WiFiAgent      BleAgent
     │           (low-level)    (low-level)
     │                       │   │
     └───────────────────────┴───┴──► device
```

Additionally, using PlaudDeviceAgent & PlaudWiFiAgent abstracts many details that low-level interfaces will not handle.

| Managed Behavior | Purpose |
| - | - |
| Analytics event per call | Logged on every gated call |
| Permission gating | A call that fails the gate silently no-ops instead of reaching the device |
| Parameter validation | Guards on `sessionId`, `outputDir`, `startSessionId`, etc. |
| Partner pre-handshake | `sn-sign` / `gen-key` are run and injected before connect (required on protocol V20+ devices) |
| E2EE decryption | `syncFile` transparently decrypts on protocol V20+ devices |
| Audio format conversion | `exportAudio` decodes to `opus` / `mp3` / `wav` / `pcm` |
| Optional delegate callbacks | `PlaudDeviceAgentProtocol` has **1** required member and optional callbacks for you to override as needed |

<Note>
  You can mix high-level and low-level facades. For example if there's a behavior that `PlaudDeviceAgent` does not include, you can use `BleAgent` to cover that functionality.
</Note>

## Low-level Facades

The iOS SDK has a few lower-level interfaces that our high-level interfaces (PlaudDeviceAgent and PlaudWiFiAgent) wrap over:

1. **BleAgent**: Bluetooth Low Energy (BLE) transport
2. **WiFiAgent**: WiFi agent with low-level methods for WiFi fast transfers
3. **PlaudPartnerApiManager**: Manages the backend endpoints for device signing and RSA keypair generation

**We do not recommend using these low-level facades as main drivers. Use them as coverage for functionality not exposed by PlaudDeviceAgent and PlaudWiFiAgent**.

<Warning>
  The `PlaudWorkflowManager` has been deprecated and will be removed in future SDK versions.
</Warning>

***

### BleAgent Reference

Import `BleAgent` through `PlaudDeviceBasicSDK`. You can then access BleAgent through the shared singleton or through the facade's escape hatch:

```swift Swift icon="swift" theme={"system"}
import PlaudDeviceBasicSDK

// either work
let ble = BleAgent.shared
let ble = PlaudDeviceAgent.shared.bleAgent
```

#### BleAgent State

Read-only state is available directly on the agent, which is useful when you need a synchronous answer rather than waiting for a delegate callback:

| Property | Type | Description |
| - | - | - |
| `isPoweredOn` | `Bool` | The phone's Bluetooth radio is on |
| `isConnected` | `Bool` | A device is currently connected |
| `isBinded` | `Bool` | The connected device is bound |
| `isRecording` | `Bool` | The device is recording |
| `isDownloading` | `Bool` | A file sync is in progress |
| `isWiFiOpen` | `Bool` | The device's hotspot is open |
| `needDecode` | `Bool` | Incoming file data still needs decoding |
| `scene` / `sessionId` | `Int` | Current recording scene and session |
| `bleDevice` | `BleDevice?` | The connected device model |
| `delegate` | `BleAgentProtocol?` | Event sink — see [BleAgentProtocol](#bleagentprotocol) |

#### Device Connection

| `PlaudDeviceAgent` | `BleAgent` |
| - | - |
| `startScan()` | `setFilter(name: nil)` → `startScan()` |
| `stopScan()` | `stopScan()` |
| `isConnected()` | `isDeviceConnect()` |
| `connectBleDevice(bleDevice:deviceToken:)` | partner pre-handshake → `connectBleDevice(bleDevice:_:_:_:)` |
| `recoveryConnectBleDevice(bleDevice:historicalUserId:)` | pin the historical id as the handshake token → `connectBleDevice(bleDevice:_:_:isForceClear: true)` |
| `disconnect()` | `disconnect()` |
| `depair(clear:)` | `depair(clear:)` — pass-through, no added logic |

```swift Swift icon="swift" theme={"system"}
BleAgent.shared.delegate = self

BleAgent.shared.initBluetooth()
BleAgent.shared.setFilter(name: nil)
BleAgent.shared.startScan()

// From your bleScanResult callback:
BleAgent.shared.connectBleDevice(
    bleDevice: device,
    userId,     // devToken
    nil,        // userName
    false       // isForceClear
)
```

<ParamField path="bleDevice" type="BleDevice" required>
  The device to connect to, as delivered by `bleScanResult(bleDevices:)`.
</ParamField>

<ParamField path="devToken" type="String?" default="nil">
  Per-device token. The facade passes the user identifier here and rejects an empty string by emitting `bleConnectState(state: 2)`; the low-level agent performs no such validation.
</ParamField>

<ParamField path="userName" type="String?" default="nil">
  Owner name written to the device. The facade passes `"Plaud"`.
</ParamField>

<ParamField path="isForceClear" type="Bool" required>
  `connectBleDevice(bleDevice:deviceToken:)` passes `false`; `recoveryConnectBleDevice(bleDevice:historicalUserId:)` passes `true`. See [Device Recovery at the BLE Layer](#device-recovery-at-the-ble-layer).
</ParamField>

Other lifecycle and auth methods on `BleAgent`:

```swift Swift icon="swift" theme={"system"}
func initBluetooth()
func disInitBluetooth()
func setUserIdentifier(_ appKey: String, _ bindToken: String, _ hkServer: Bool = false)
func checkAppKey(_ appKey: String)
func setBinding(_ token: String)
func setFilter(name: String?)             // single-name filter, nil to disable
func setFilter(_ names: [String])         // multi-name filter
func startLoopScan()                      // continuous scan
func isAuthOk() -> Bool
func isSNTempChecked() -> Bool
func reCheckSNIfNeed()
func depair(clear: Bool = false)
func openLog(_ opened: Bool, logBlock: ((String) -> Void)? = nil,
             wlogBlock: ((String) -> Void)? = nil)
```

<Warning>
  `BleAgent.connectBleDevice(...)` does **not** run the partner pre-handshake, so connecting a protocol V20+ device (NotePro and newer) through the low-level agent fails unless you inject the handshake material yourself. See [Device Security](#device-security).
</Warning>

#### Recording & Device State

```swift Swift icon="swift" theme={"system"}
// Recording
func startRecord(_ scene: Int = 0)
func stopRecord()
func pauseRecord(_ sessionId: Int)
func resumeRecord(_ sessionId: Int)

// State reads — results arrive on the delegate
func getState()
func getStorage()
func getChargingState()
func readPower()
func getDeviceStatus()
func setHeartBeat(status: Int)
func setDeviceActive(status: Int)
func setPrivacy(onOff: Int)
func restoreFactory()
```

`BleAgent` also exposes the full device-settings surface that the facade only partially wraps. Each setting is a `read…` / `set…` pair, with the result delivered on the corresponding `BleAgentProtocol` callback.

<Accordion title="Device settings available only on BleAgent">
  ```swift Swift icon="swift" theme={"system"}
  // Backlight
  func readBacklightDuration();  func setBacklight(duration: BacklightDuration)
  func readBacklightBright();    func setBacklight(bright: BacklightBright)

  // Language, scene & mode
  func readLanguage();           func setLanguage(type: LanguageType)
  func readRecScene();           func setRecScene(type: RecScene)
  func readRecMode();            func setRecMode(type: RecMode)
  func openVAD(open: Bool)
  func readVadSensitivity();     func setVadSensitivity(sensitivity: VadSensitivity)

  // Gains
  func readVpuGain();            func setVpuGain(gain: VpuGain)
  func readMicGain();            func setMicGain(value: Int)
  func readBatteryMode();        func setBatteryMode(value: Int)

  // Toggles
  func readSwitchHandler();                     func setSwitchHandler(id: Int)
  func readAutoPowerOff();                      func setAutoPowerOff(value: Int)
  func readRawWaveEnabled();                    func setRawWaveEnabled(value: Int)
  func readRecordingAfterDisConnetEnabled();    func setRecordingAfterDisConnetEnabled(value: Int)
  func readSyncWhenIdleEnabled();               func setSyncWhenIdleEnabled(value: Int)
  func readFindMyState();                       func setFindMyState(value: Int)
  func readVPUCLK();                            func setVPUCLK(value: Int)
  func readStopRecordingAfterCharging();        func setStopRecordingAfterCharging(value: Int)
  func readAutoClear();                         func saveAutoClear(_ open: Bool)

  // Naming, LED, alarm recording
  func readBleName();            func setBleName(name: String)
  func getLedState();            func setLedState(onOff: Int)
  func getAlarmRec();            func setAlarmRec(start: Int, duration: Int, repeatMode: Int)

  // Generic channel for settings without a dedicated method
  func getCommonParams(dataType: Int)
  func setCommonParams(dataType: Int, value: String)

  // Device logs, find-my, SoundPlus, SD flash, BLE rate test
  func getDeviceLogList(logType: Int)
  func startSyncDeviceLogFile(logType: Int);  func stopSyncDeviceLogFile()
  func deleteDeviceLogFile(logType: Int)
  func resetFindmy()
  func setSoundPlusToken(licenseKey: String)
  func getSDFLASHCID()
  func startBleRateTest(_ packSize: Int = 80);  func stopBleRateTest()
  ```

  <Note>
    Most setters ship in two forms: an `@objc` variant taking a raw `Int` (e.g. `setRecScene(value: Int)`) and a Swift-only variant taking a typed enum (`setRecScene(type: RecScene)`). Prefer the typed variant.
  </Note>
</Accordion>

#### File Sync

```swift Swift icon="swift" theme={"system"}
func getFileList(uid: Int, sessionId: Int, onlyOne: Bool = false)
func syncFile(sessionId: Int, start: Int, end: Int, decode: Bool)
func stopSyncFile()
func deleteFile(sessionId: Int)
func clearAllFile()
func getMarking(_ sessionId: Int)
func getRecordMarkingTags(uid: Int, startTimestamp: Int, endTimestamp: Int)
```

<ParamField path="uid" type="Int" required>
  Request identifier echoed back on the response. The facade passes a timestamp.
</ParamField>

<ParamField path="sessionId" type="Int" required>
  On `getFileList`, the session ID to start listing from. On `syncFile` / `deleteFile`, the recording to act on.
</ParamField>

<ParamField path="onlyOne" type="Bool" default="false">
  Return a single file rather than the list from `sessionId` onward. This is what `PlaudDeviceAgent.getFile(sessionId:)` sets.
</ParamField>

<ParamField path="start" type="Int" required>
  Start byte offset. Use `0` for the whole file, or a `BleFile.offset` to resume.
</ParamField>

<ParamField path="end" type="Int" required>
  End byte offset. `0` transfers to the end of the file.
</ParamField>

<ParamField path="decode" type="Bool" required>
  Decode the stream in transit. Pass `false` for E2EE (protocol V20+) devices and decrypt the result yourself; `true` otherwise.
</ParamField>

File bytes arrive on `bleData(sessionId:start:data:)` and finish with `bleDataComplete()`. There is no format conversion at this layer — that is what `PlaudDeviceAgent.exportAudio(...)` adds on top.

<Note>
  On a protocol V20+ device, `syncFile(decode: true)` returns bytes that are still E2EE-encrypted. Sync with `decode: false` and pass the file through `AudioFileDecryptor.decryptAudioFile(inputPath:privateKeyPem:)` using the private key from [`gen-key`](#device-security), or use `PlaudDeviceAgent.syncFile(...)`, which does this for you.
</Note>

#### Firmware Push

```swift Swift icon="swift" theme={"system"}
func pushFotaInfo(_ uid: Int, _ fromVersion: String, _ toVersion: String,
                  _ thirdVersion: Int = 0, _ fileSize: Int, _ crc: Int)
func pushFotaPack(_ offset: Int, packData: Data, postDelayUs: NSNumber?)
func pushFotaComplete(_ uid: Int, _ status: Int)
func getUpdateInfo(_ callback: @escaping (Int, UpdateInfo?) -> Void)
```

#### Encryption

```swift Swift icon="swift" theme={"system"}
var isSecureChannelEstablished: Bool { get }
var isEncryptionSupported: Bool { get }

func getEncryptionKey() -> String?
func getEncryptionNonce() -> String?
func getEncryptionAD() -> String?
func getEncryptionParameters() -> [String: String]?

// Transport encryption (ChaCha20-Poly1305 / AES-256)
func decryptFileData(_ encryptedData: Data, key: String?, nonce: String?, ad
: String?) throws -> Data
func decryptFile(inputPath: String, outputPath: String, key: String?, nonce:
 String?, ad: String?) -> Bool

// End-to-end (RSA-wrapped) audio
func decryptE2EEAudioFile(inputPath: String, outputPath: String?, privateKey
Pem: String) throws -> String
func isE2EEEncryptedFile(path: String) -> Bool
func getE2EEFileHeader(path: String) -> PlaudEncryptHeader?
```

#### BleAgentProtocol

`BleAgentProtocol` is `BleAgent`'s delegate. `PlaudDeviceAgent` conforms to it internally and re-emits a filtered, mostly-optional subset to your `PlaudDeviceAgentProtocol`.

| Protocol | Required members | Optional members |
| - | - | - |
| `BleAgentProtocol` (`PlaudBleSDK`) | **96** | 1 (`bleConnectStage`) |
| `PlaudDeviceAgentProtocol` (`PlaudDeviceBasicSDK`) | **1** (`blePenState`) | 49 |
| `PlaudWiFiAgentProtocol` | 0 | all |

<Warning>
  Conforming to `BleAgentProtocol` means implementing 96 members. Unless you need an event the facade does not re-emit, keep your delegate on `PlaudDeviceAgentProtocol`.
</Warning>

Where the two layers differ, the facade narrows the payload:

| Callback | `BleAgentProtocol` | `PlaudDeviceAgentProtocol` |
| - | - | - |
| `blePenState` | + `versionType: String, versionCode: Int` | 7 params, no version fields |
| `bleRecordStart` | `(sessionId:start:status:scene:startTime:)` | + `reason: Int` |
| `bleConnectStage` | `(sn:stage:detail:)` | identical — straight pass-through |

##### Connect stages — `bleConnectStage`

```swift Swift icon="swift" theme={"system"}
@objc optional func bleConnectStage(sn: String?, stage: String, detail: String?)
```

`bleConnectStage` is the diagnostics stream for the connect/handshake sequence — the only way to see *which* layer refused a rejected connection. `sn` may be `nil` before the serial number is known.

| Argument | Values |
| - | - |
| `stage` | `start`, `gatt_connect`, `set_notify`, `set_battery_notify`, `read_battery`, `set_data_notify`, `pre_handshake`, `send_rsa_public`, `first_handshake`, `two_handshake`, `handshake_get_ssn`, `change_handshake_timeout`, `sync_time` |
| `detail` | Stage detail or failure reason — `ok`, `status_<n>` (`status_1` = token mismatch), `sn_signature_empty`, `sn_signature_invalid`, `user_rsa_public_key_empty`, `bind_token_empty` |

The `stage` strings are the raw values of the `BleAgent.ConnectStage` enum, and the callback delivers them as `String`, not as the enum case names.

| `detail` | What it means |
| - | - |
| `sn_signature_empty` | `sn-sign` never ran, or the cached signature was lost |
| `sn_signature_invalid` | The cached signature is stale — re-sign the SN |
| `user_rsa_public_key_empty` | `gen-key` has not completed; the key pair is missing |
| `bind_token_empty` | No handshake token was supplied to the connect |
| `status_1` | Token mismatch — the firmware is locked to another account |

A rejection at `pre_handshake` / `send_rsa_public` / `first_handshake`, or a `detail` of `status_1`, is the signature of a device still locked to a previous account — that is the cue for [Device Recovery](#device-recovery-at-the-ble-layer).

<Note>
  Before SDK 1.0.13 all of these collapsed into an undifferentiated `bleConnectState(state: 2)`. The callback itself is not new — it has been `BleAgentProtocol`'s single optional member all along — but 1.0.13 is the release where `PlaudDeviceAgent` started re-emitting it, so you no longer have to drop to the BLE layer to receive it.
</Note>

***

### WiFiAgent Reference

#### WiFi Fast Transfer

Each `PlaudWiFiAgent` method maps to a single `WiFiAgent` command:

| `PlaudWiFiAgent` | `WiFiAgent` | Command |
| - | - | - |
| `connectWifi(_:_:_:)` | `connectWifi(_:_:_:)` | — |
| `listenPort(_:_:)` | `listenPort(_:_:)` | — |
| `disconnect()` | `disconnect()` | — |
| `isConnectedTo(_:)` | `getCurrentWiFiName() == ssid` | — |
| `getFileList(_:_:_:)` | `appGetFileList(_:_:_:)` | 11 |
| `syncFile(_:_:_:_:)` | `appSyncFile(_:_:_:_:)` | 12 |
| `deleteFile(_:_:)` | `appDeleteFile(_:_:)` | 14 |
| `stopSyncFile(_:_:)` | `appStopSyncFile(_:_:)` | 15 |

The flow is the same as the high-level PlaudWiFiAgent: open the device hotspot, then hand the device to the WiFi layer and wait for `wifiHandshake(0)` before issuing any file command.

```swift Swift icon="swift" theme={"system"}
import PlaudDeviceBasicSDK

WiFiAgent.shared.delegate = self
WiFiAgent.shared.bleDevice = BleAgent.shared.bleDevice   // required before connecting

// From bleWiFiOpen(_:_:_:_:) on PlaudDeviceAgentProtocol:
WiFiAgent.shared.connectWifi(wifiName, wifiPass, 60)

extension SyncManager: WiFiAgentProtocol {
    func wifiHandshake(_ status: Int) {
        guard status == 0 else { return }
        WiFiAgent.shared.appGetFileList(Int(Date().timeIntervalSince1970), 0, false)
    }
    func wifiFileList(_ files: [BleFile]) {
        guard let file = files.first else { return }
        WiFiAgent.shared.appSyncFile(file.sessionId, 0, 0, file.scenes)
    }
    func wifiSyncFileData(_ sessionId: Int, _ offset: Int, _ count: Int, _ binData: Data) {
        // append binData at offset
    }
    func wifiDataComplete() { /* all bytes received */ }
}
```

<ParamField path="ssid" type="String" required>
  The device hotspot SSID, delivered as `wifiName` on `bleWiFiOpen(_:_:_:_:)`.
</ParamField>

<ParamField path="passphrase" type="String" required>
  The hotspot passphrase, delivered as `wifiPass` on `bleWiFiOpen(_:_:_:_:)`.
</ParamField>

<ParamField path="overtimeSec" type="Int" default="60">
  Association timeout in seconds. `listenPort(_:_:)` defaults to `30`.
</ParamField>

<ParamField path="scene" type="Int" default="1">
  On `appSyncFile` / `appStopSyncFile` / `appDeleteFile`, the file's own `BleFile.scenes` value. A mismatch is rejected by the device with a non-zero `wifiSyncFile` status.
</ParamField>

Additional transport controls not surfaced on the facade:

```swift Swift icon="swift" theme={"system"}
func cancelConnectWifi()
func clearAllWiFiConfigurations()      // removes SSIDs this app configured
func getCurrentWiFiNameWithRetry(maxRetries: Int = 3, delay: TimeInterval = 1.0) -> String?
func isWebSocketConnected() -> Bool
func appExtendWifiExitTime()           // keep the hotspot alive longer
func appWiFiRate(_ onOff: Bool, _ packSize: Int)
func appGetLogs(_ begin: Bool)
func startPushOTA(_ uid: Int, _ fileSize: Int, crc: Int, _ toVersion: Int)
```

#### WiFiAgentProtocol

`WiFiAgentProtocol` is the low-level delegate. Unlike `PlaudWiFiAgentProtocol`, which is entirely optional, most of `WiFiAgentProtocol` are required.

| Callback | Description |
| - | - |
| `wifiHandshake(_ status: Int)` | `0` = handshake complete, ready to issue commands |
| `wifiFileList(_ files: [BleFile])` | Result of `appGetFileList(...)` |
| `wifiFileListFail(_ status: Int)` | File listing failed |
| `wifiSyncFile(_ sessionId: Int, _ status: Int)` | `0` = transfer accepted; non-zero = rejected (commonly a scene mismatch) |
| `wifiSyncFileData(_:_:_:_:)` | A chunk of file data at an offset |
| `wifiDataComplete()` | All bytes received |
| `wifiSyncFileStop(_ status: Int)` | Transfer stopped or aborted |
| `wifiFileDelete(_ sessionId: Int, _ status: Int)` | Result of `appDeleteFile(...)` |
| `wifiCommonErr(_ cmd: Int, _ status: Int)` | A command failed, identified by command number |
| `wifiPower(_ power: Int, _ voltage: Int)` | Device battery over the WiFi session |
| `wifiRate(_:_:_:)` / `wifiRateFail(_:)` | Throughput test results |
| `wifiLogs(_ logData: Data?)` / `wifiLogsFail(_:)` | Device log retrieval |
| `wifiClientFail()` / `wifiClose(_ status: Int)` | Session dropped or closed |
| `wifiTips(_ tips: Int)` | Device-side advisory code |
| `penRequestOTAData(start:end:payloadSize:uid:sendRatePPS:)` | Device requesting an OTA chunk over WiFi |
| `wifiOTAStatus(_ status: Int, _ uid: Int)` | OTA-over-WiFi status |

<Note>
  `wifiCommonErr(cmd: 16, status: 0)` at the end of a transfer is expected and indicates a successful sync.
</Note>

***

### PlaudPartnerApiManager

`PlaudPartnerApiManager` wraps the partner authentication endpoints. Like `BleAgent`, reach it via the facade or the singleton:

```swift Swift icon="swift" theme={"system"}
let api = PlaudDeviceAgent.shared.getPartnerApiManager()
// or: PlaudPartnerApiManager.shared
```

```swift Swift icon="swift" theme={"system"}
final public class PlaudPartnerApiManager {
    public static let shared: PlaudPartnerApiManager
    public func setUserAccessToken(_ token: String?)
    public func getUserAccessToken() -> String?

    // → POST …/open/partner/sdk/sn-sign
    public func signDeviceSn(deviceType: String, sn: String,
                             completion: @escaping (Result<PlaudPartnerSnSignResponse, Error>) -> Void)

    // → POST …/open/partner/sdk/gen-key
    public func generateRsaKeyPair(
        completion: @escaping (Result<PlaudPartnerGenKeyResponse, Error>) -> Void)
}
```

<Note>
  There is no `sn-verify` method on iOS. Signature **verification** happens on the device during the BLE pre-handshake, not through a client API call.
</Note>

#### Device Security

Partner endpoints for device authentication:

| Endpoint | Method | Auth header | Driven by |
| - | - | - | - |
| `…/sdk/gen-key` | `POST` | `Authorization: Bearer <user token>` | `PlaudPartnerApiManager.generateRsaKeyPair` |
| `…/sdk/sn-sign` | `POST` | `Authorization: Bearer <user token>` | `PlaudPartnerApiManager.signDeviceSn` |

The high-level `PlaudDeviceAgent` drives this entire flow. You only need these APIs if you are connecting through `BleAgent` directly or minting keys on your own backend.

<Steps>
  <Step title="At init — gen-key">
    `initSDK(userAccessToken:customDomain:)` and every `setUserAccessToken(_:)` call fetch a fresh RSA key pair, store it, and mirror it into the BLE layer. A new key pair **invalidates every cached `sn-sign` signature**, since signatures are bound to the key pair that produced them.

    A response missing `private_key` is a hard failure — the public key alone is not enough to complete the encrypted pre-handshake.
  </Step>

  <Step title="Before connect — sn-sign">
    `connectBleDevice(...)` runs the partner handshake before it reaches `BleAgent`:
  </Step>

  <Step title="After sync — E2EE decryption">
    Methods like `PlaudDeviceAgent.syncFile(...)` will use the symmetric key from the encrypted file header before decoding audio.
  </Step>
</Steps>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.