> ## 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**.

***

### 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:_:_:_:)` |
| `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>
  Clear any existing pairing before connecting. The facade passes `false`.
</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?
```

***

### 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>
