seilon_voice_plugin 0.1.6
seilon_voice_plugin: ^0.1.6 copied to clipboard
Flutter bridge for the Seilon BLE recorder SDK on Android and iOS.
English | 简体中文
Seilon Voice Plugin (English) #
A Flutter 3.44+ plugin for Seilon BLE SDK 1.0.4. It supports Android API 24+ and iOS 15+, providing device discovery and connection, three-step binding, recorder controls, independent Wi-Fi/TCP networking, file transfer, runtime logs, and raw OPUS frames.
Installation #
dependencies:
seilon_voice_plugin: ^0.1.6
import 'package:flutter/foundation.dart';
import 'package:seilon_voice_plugin/seilon_voice_plugin.dart';
Quick start #
final plugin = SeilonVoicePlugin();
await plugin.initialize(
config: const SeilonSdkConfig(
commandTimeout: Duration(seconds: 8),
),
);
final subscription = plugin.events.listen((event) {
switch (event) {
case SeilonDeviceFoundEvent(:final device):
print('Device found: ${device.name} (${device.id})');
case SeilonDisconnectedEvent(:final reason):
print('Device disconnected: $reason');
case SeilonAudioFrameEvent(:final opus):
print('OPUS frame: ${opus.length} bytes');
case SeilonLogEvent(:final entry):
print('[${entry.level.name}] ${entry.source}: ${entry.message}');
default:
break;
}
});
if (defaultTargetPlatform == TargetPlatform.android) {
final report = await plugin.requestPermissions(
SeilonPermissionOperation.scan,
);
if (!report.granted) {
throw StateError('Scan requirements are not satisfied.');
}
}
await plugin.startScan(duration: const Duration(seconds: 30));
// Select a device ID from SeilonDeviceFoundEvent.
final session = await plugin.connect(deviceId);
await session.beginBind();
await session.handshake();
await session.completeBind();
final device = await session.queryDeviceInfo();
print('${device.model} / ${device.firmwareVersion}');
await session.disconnect();
await subscription.cancel();
await plugin.close();
Any omitted SeilonSdkConfig field continues to use the native SDK default. Reinitializing an already initialized SDK with the same configuration is idempotent. Call close() before changing the configuration.
SeilonVoicePlugin #
| API | Description |
|---|---|
initialize(config: ...) |
Initializes the singleton SDK |
startScan(duration: ...) / stopScan() |
Scans for recorders; the vendor-advertised MAC is preferred as the device ID |
connect(deviceId) |
Connects to a device and returns a SeilonRecorderSession |
network |
Returns a SeilonNetworkClient that does not require a BLE session |
disconnect() |
Actively disconnects the current device |
events |
Broadcast stream for scan, connection, disconnection, log, and OPUS events |
checkPermissions(operation) |
Checks Android permissions and system capabilities |
requestPermissions(operation) |
Requests Android runtime permissions and checks again |
clearLogs() |
Clears current SDK logs |
close() |
Releases the SDK and current session |
The session returned by connect() carries a native session token. After reconnecting, an old SeilonRecorderSession is rejected with SeilonErrorCode.notConnected, preventing it from operating on the new session.
SeilonRecorderSession #
Binding and device controls #
| API | Description |
|---|---|
beginBind() |
Starts binding |
handshake() |
Performs the handshake |
completeBind() |
Completes binding |
queryDeviceInfo() |
Returns a complete SeilonDevice |
startRecording() / stopRecording() |
Starts or stops device recording |
powerOff() |
Powers off the device |
setUsbMode(enabled) |
Changes USB mode |
setScreen(enabled:, seconds:, brightness:) |
Changes screen state, duration, and brightness |
setWifiEnabled(enabled) |
Enables or disables the device hotspot |
setWifiCountryCode(code) / queryWifiCountryCode() |
Writes or reads the two-letter Wi-Fi country code |
Wi-Fi, TCP, and channel selection #
// These device commands require the BLE recorder session.
await session.setWifiEnabled(true);
final endpoint = await session.queryTcpEndpoint();
// SDK 1.0.4 platform networking does not require plugin.connect().
final network = plugin.network;
await network.connectWifi(
ssid: deviceHotspotSsid,
password: deviceHotspotPassword,
);
await network.connectTcp(host: endpoint.host, port: endpoint.port);
// Select the device command channel on the BLE session when needed.
await session.setChannel(SeilonChannel.tcp);
| API | Description |
|---|---|
plugin.network.connectWifi(ssid:, password:) / disconnectWifi() |
Connects to or disconnects from the device hotspot without requiring a BLE session |
plugin.network.connectTcp(host:, port:) / disconnectTcp() |
Connects directly to the supplied TCP endpoint without requiring a BLE session |
plugin.network.connectTcp() |
Connects using the endpoint previously passed to network.configureTcp() or the native default |
plugin.network.queryDeviceInfo() and control/file methods |
Uses the connected TCP transport for typed device operations |
queryTcpEndpoint() |
Reads the TCP endpoint from the device |
configureTcp(host:, port:) |
Configures the endpoint that the next TCP connection will actually use |
connectTcp() / disconnectTcp() |
Connects or disconnects TCP |
setChannel(SeilonChannel.ble/tcp) |
Selects the SDK command channel |
The session Wi-Fi/TCP methods remain available for source compatibility. New code should use plugin.network for platform Wi-Fi and TCP setup. setWifiCountryCode() accepts a two-letter ISO 3166-1 alpha-2 code such as CN or US; lowercase input is normalized to uppercase.
After enabling the device hotspot, its SSID and password can be parsed from the corresponding log packet:
if (event case SeilonLogEvent(:final entry)) {
final credentials = SeilonWifiCredentials.tryParsePacketHex(
entry.rawPacketHex,
);
if (credentials != null) {
print('${credentials.ssid} / ${credentials.password}');
}
}
File operations #
| API | Description |
|---|---|
queryFileCount() |
Queries the file count, page count, and per-request length |
queryFiles(pageNumber) |
Queries one file-list page; page numbering starts at 1 |
downloadFile(fileName) |
Downloads a file and returns its local path |
cancelDownload() |
Cancels only the local download wait; it does not send a stop command to the recorder |
stopFileDownload() |
Sends the SDK stop-transfer command over the channel that started the download |
resumeFileDownload(fileName) |
Resumes from the received byte count and returns the local path |
deleteFile(fileName) |
Deletes a device file by name |
final summary = await session.queryFileCount();
for (var pageNumber = 1; pageNumber <= summary.pageCount; pageNumber++) {
final page = await session.queryFiles(pageNumber);
for (final file in page.files) {
print('${file.fileName}: ${file.fileSizeBytes} bytes');
}
}
final localPath = await session.downloadFile(fileName);
print('Saved to: $localPath');
Download progress is provided through log events:
plugin.events.listen((event) {
if (event case SeilonLogEvent()) {
final progress = event.transferProgress;
if (progress != null) {
print('${progress.percent}% '
'(${progress.receivedBytes}/${progress.totalBytes})');
}
}
});
File transfer rules #
- Call
queryFileCount()first. ItsmaxContentLengthis the length of a single retrieval request. - Call
queryFiles(pageNumber)to obtain file names and sizes. downloadFile(fileName)sends the initial retrieval command once and then waits for actual TCP/BLE data.- The SDK writes chunks continuously in arrival order. It does not reorder by chunk sequence or repeatedly request the next chunk.
- By default, the transfer fails after 8 seconds without data. Only an explicit
resumeFileDownload(fileName)requests data again from the received byte count. cancelDownload()only cancels the current local wait. Received data is retained for an explicit resume.stopFileDownload()asks the recorder to stop sending file data and uses the BLE/TCP channel that started the transfer.
State and events #
| Dart event | Data |
|---|---|
SeilonScanStartedEvent |
Scan started |
SeilonScanStoppedEvent |
Scan stopped |
SeilonDeviceFoundEvent |
SeilonDevice |
SeilonConnectedEvent |
Connected SeilonDevice |
SeilonDisconnectedEvent |
Device ID, expected-disconnect flag, and reason |
SeilonScanFailedEvent |
SeilonSdkException |
SeilonLogEvent |
SeilonLogEntry, including optional transferProgress |
SeilonAudioFrameEvent |
Raw device OPUS data as Uint8List |
The Flutter API does not expose Android Flow or the native SeilonSessionState. Read state through typed method results and events. The SDK does not perform ASR, playback, or OPUS/WAV conversion.
SeilonDevice fields #
After a successful queryDeviceInfo(), the following fields are available:
| Field | Description |
|---|---|
id / name / rssi / connected |
Plugin device ID, name, signal strength, and connection state |
mac / model / firmwareVersion / serialNumber |
Device MAC, model, firmware version, and serial number |
batteryPercent |
Battery percentage, from 0 to 100 |
recording / recordingMode / recordingStatus |
Recording flag, mode, and raw device status |
usbModeEnabled / wifiEnabled |
USB and device Wi-Fi state |
storageUsedMb / storageTotalMb |
Used and total EMMC capacity in MB |
recordFormat |
Current recording format; existing devices return Opus |
capacityAlertValue |
Low-capacity alert threshold, from 0 to 20 |
charging |
Whether the device is charging |
recordingFileName |
Current recording file name, empty when not recording |
headsetPrimaryMac |
Primary headset MAC, empty when not transmitted |
systemDurationSeconds |
System duration in seconds |
screenBrightness |
Screen brightness, from 0 to 100 |
Errors and timeouts #
Failed calls consistently throw SeilonSdkException:
try {
await session.queryFiles(1);
} on SeilonSdkException catch (error) {
print('${error.code.name}: ${error.message}');
}
Stable error codes include busy, invalidArgument, notConnected, permissionDenied, bluetoothUnavailable, wifiUnavailable, sendFailed, timeout, parseFailed, disconnected, fileTransferFailed, cancelled, unsupportedFeature, and internal.
- All device replies ignore
seq. - Normal commands match only by source channel and
category + command. - A session permits only one pending normal operation at a time. Concurrent operations return
busy. - Default timeouts are 8 seconds for normal commands, 45 seconds for enabling Wi-Fi, 60 seconds for platform Wi-Fi/TCP connections, and 8 seconds for file-transfer inactivity.
- Override these values with
commandTimeout,wifiCommandTimeout,networkTimeout, andtransferIdleTimeoutinSeilonSdkConfig.
Platform setup #
Android #
- Minimum Flutter version: 3.44 (Dart 3.12).
- Minimum Android API level: 24.
- Android uses the Flutter 3.44 AGP 9 migration layout without directly applying KGP in the plugin or app module.
- The AAR manifest declares the required Bluetooth, location, Wi-Fi, network, and TCP permissions.
- Use
checkPermissions/requestPermissionsfor runtime permissions. The plugin does not automatically enable Bluetooth or Wi-Fi.
iOS #
- Minimum iOS version: 15. CocoaPods and Swift Package Manager are supported.
- The host
Info.plistmust include Bluetooth and local-network usage descriptions:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Used to discover and connect to Seilon recording devices.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Used to connect to the local TCP service provided by Seilon devices.</string>
- Enable the Hotspot Configuration capability for the App ID and target when connecting to the device hotspot.
Lifecycle and limitations #
- One plugin instance manages one native SDK, one device session, and one independent network client.
- The plugin does not bind, handshake, reconnect, or change BLE/TCP channels automatically.
- Cancel the event subscription and call
close()when leaving the owning page or application container. - The SDK does not include OTA, DNR, ASR, sharing, OPUS playback, or OPUS/WAV conversion.
- See
example/for the complete test panel and controlled delete/power-off flows.
English | 简体中文
Seilon Voice Plugin (Simplified Chinese) #
Seilon BLE SDK 1.0.4 的 Flutter 3.44+ 插件,支持 Android API 24+ 与 iOS 15+。插件提供设备扫描与连接、三步绑定、录音设备控制、独立 Wi-Fi/TCP 网络客户端、文件传输、运行日志和原始 OPUS 帧。
安装 #
dependencies:
seilon_voice_plugin: ^0.1.6
import 'package:flutter/foundation.dart';
import 'package:seilon_voice_plugin/seilon_voice_plugin.dart';
基本使用 #
final plugin = SeilonVoicePlugin();
await plugin.initialize(
config: const SeilonSdkConfig(
commandTimeout: Duration(seconds: 8),
),
);
final subscription = plugin.events.listen((event) {
switch (event) {
case SeilonDeviceFoundEvent(:final device):
print('发现设备:${device.name} (${device.id})');
case SeilonDisconnectedEvent(:final reason):
print('设备断开:$reason');
case SeilonAudioFrameEvent(:final opus):
print('收到 OPUS:${opus.length} bytes');
case SeilonLogEvent(:final entry):
print('[${entry.level.name}] ${entry.source}: ${entry.message}');
default:
break;
}
});
if (defaultTargetPlatform == TargetPlatform.android) {
final report = await plugin.requestPermissions(
SeilonPermissionOperation.scan,
);
if (!report.granted) {
throw StateError('扫描权限未满足');
}
}
await plugin.startScan(duration: const Duration(seconds: 30));
// 从 SeilonDeviceFoundEvent 中选择设备 ID。
final session = await plugin.connect(deviceId);
await session.beginBind();
await session.handshake();
await session.completeBind();
final device = await session.queryDeviceInfo();
print('${device.model} / ${device.firmwareVersion}');
await session.disconnect();
await subscription.cancel();
await plugin.close();
未传入的 SeilonSdkConfig 字段继续使用原生 SDK 默认值。SDK 已初始化时,使用相同配置重复初始化为幂等操作;如需更换配置,请先调用 close()。
SeilonVoicePlugin #
| API | 说明 |
|---|---|
initialize(config: ...) |
初始化单例 SDK |
startScan(duration: ...) / stopScan() |
扫描录音设备;设备 ID 优先使用厂商广播 MAC |
connect(deviceId) |
连接指定设备并返回 SeilonRecorderSession |
network |
获取不依赖 BLE 会话的 SeilonNetworkClient |
disconnect() |
主动断开当前设备 |
events |
广播扫描、连接、断连、日志和 OPUS 帧事件 |
checkPermissions(operation) |
Android 权限和系统能力检查 |
requestPermissions(operation) |
Android 运行时权限申请并重新检查 |
clearLogs() |
清空当前 SDK 日志 |
close() |
释放 SDK 和当前会话 |
connect() 返回的会话带有原生会话令牌。设备重连后,旧 SeilonRecorderSession 会以 SeilonErrorCode.notConnected 拒绝操作,不会误操作新会话。
SeilonRecorderSession #
绑定与设备控制 #
| API | 说明 |
|---|---|
beginBind() |
开始绑定 |
handshake() |
执行握手 |
completeBind() |
完成绑定 |
queryDeviceInfo() |
查询并返回完整 SeilonDevice |
startRecording() / stopRecording() |
开始或停止设备录音 |
powerOff() |
关闭设备 |
setUsbMode(enabled) |
设置 USB 模式 |
setScreen(enabled:, seconds:, brightness:) |
设置屏幕开关、持续时间和亮度 |
setWifiEnabled(enabled) |
开启或关闭设备热点 |
setWifiCountryCode(code) / queryWifiCountryCode() |
写入或读取两位 Wi-Fi 国家码 |
Wi-Fi、TCP 与通道 #
// 以下设备指令需要 BLE 会话。
await session.setWifiEnabled(true);
final endpoint = await session.queryTcpEndpoint();
// SDK 1.0.4 的平台网络入口不要求先调用 plugin.connect()。
final network = plugin.network;
await network.connectWifi(
ssid: deviceHotspotSsid,
password: deviceHotspotPassword,
);
await network.connectTcp(host: endpoint.host, port: endpoint.port);
// 需要时再通过 BLE 会话切换设备指令通道。
await session.setChannel(SeilonChannel.tcp);
| API | 说明 |
|---|---|
plugin.network.connectWifi(ssid:, password:) / disconnectWifi() |
无需 BLE 会话即可连接或断开设备热点 |
plugin.network.connectTcp(host:, port:) / disconnectTcp() |
无需 BLE 会话,直接连接传入的 TCP 地址 |
plugin.network.connectTcp() |
使用 network.configureTcp() 预先设置的地址或原生默认值连接 |
plugin.network.queryDeviceInfo() 及控制/文件方法 |
TCP 连接后通过 TCP 执行类型化设备操作 |
queryTcpEndpoint() |
从设备读取 TCP 地址 |
configureTcp(host:, port:) |
配置后续 TCP 连接实际使用的地址 |
connectTcp() / disconnectTcp() |
连接或断开 TCP |
setChannel(SeilonChannel.ble/tcp) |
切换 SDK 指令通道 |
为兼容旧代码,会话对象上的 Wi-Fi/TCP 方法仍然保留。新代码应使用 plugin.network 建立平台 Wi-Fi 和 TCP 连接。setWifiCountryCode() 接收 CN、US 这类 ISO 3166-1 alpha-2 两位国家码,小写输入会自动转换为大写。
设备开启热点后,热点账号和密码可从对应日志包中解析:
if (event case SeilonLogEvent(:final entry)) {
final credentials = SeilonWifiCredentials.tryParsePacketHex(
entry.rawPacketHex,
);
if (credentials != null) {
print('${credentials.ssid} / ${credentials.password}');
}
}
文件操作 #
| API | 说明 |
|---|---|
queryFileCount() |
查询文件总数、页数和单次请求长度 |
queryFiles(pageNumber) |
查询指定页文件列表,页码从 1 开始 |
downloadFile(fileName) |
下载文件并返回本地路径 |
cancelDownload() |
只取消本地下载等待,不向设备发送停止指令 |
stopFileDownload() |
通过启动下载时使用的通道发送 SDK 停止传输指令 |
resumeFileDownload(fileName) |
从已接收字节处继续下载并返回本地路径 |
deleteFile(fileName) |
按文件名删除设备文件 |
final summary = await session.queryFileCount();
for (var pageNumber = 1; pageNumber <= summary.pageCount; pageNumber++) {
final page = await session.queryFiles(pageNumber);
for (final file in page.files) {
print('${file.fileName}: ${file.fileSizeBytes} bytes');
}
}
final localPath = await session.downloadFile(fileName);
print('文件已保存:$localPath');
下载进度通过日志事件提供:
plugin.events.listen((event) {
if (event case SeilonLogEvent()) {
final progress = event.transferProgress;
if (progress != null) {
print('${progress.percent}% '
'(${progress.receivedBytes}/${progress.totalBytes})');
}
}
});
文件传输规则 #
- 先调用
queryFileCount();返回值的maxContentLength是单次获取请求长度。 - 调用
queryFiles(pageNumber)获取文件名与文件大小。 downloadFile(fileName)只发送一次首次获取命令,之后等待 TCP/BLE 实际数据回流。- SDK 按到达顺序连续写入,不依据分片序号重排,也不会为下一片重复发送获取命令。
- 默认连续 8 秒无数据时失败。只有显式调用
resumeFileDownload(fileName)才会从已接收字节处重新请求。 cancelDownload()只取消当前本地等待;已接收数据会保留用于显式断点续传。stopFileDownload()会要求设备停止发送文件数据,并沿用启动传输时的 BLE/TCP 通道。
状态与事件 #
| Dart 事件 | 数据 |
|---|---|
SeilonScanStartedEvent |
扫描已开始 |
SeilonScanStoppedEvent |
扫描已停止 |
SeilonDeviceFoundEvent |
SeilonDevice |
SeilonConnectedEvent |
已连接的 SeilonDevice |
SeilonDisconnectedEvent |
设备 ID、是否主动断开和原因 |
SeilonScanFailedEvent |
SeilonSdkException |
SeilonLogEvent |
SeilonLogEntry,并可读取 transferProgress |
SeilonAudioFrameEvent |
设备原始 OPUS Uint8List |
Flutter 公共 API 不暴露 Android Flow 或原生 SeilonSessionState。状态通过类型化方法结果和 events 获取。SDK 不做 ASR、播放或 OPUS/WAV 转换。
SeilonDevice 字段 #
调用 queryDeviceInfo() 成功后可读取以下字段:
| 字段 | 说明 |
|---|---|
id / name / rssi / connected |
插件设备标识、名称、信号和连接状态 |
mac / model / firmwareVersion / serialNumber |
设备 MAC、型号、版本号和 SN |
batteryPercent |
电量,范围 0~100 |
recording / recordingMode / recordingStatus |
录音状态、模式和设备原始状态值 |
usbModeEnabled / wifiEnabled |
USB 和设备 Wi-Fi 状态 |
storageUsedMb / storageTotalMb |
EMMC 已用/总容量,单位 MB |
recordFormat |
当前录音格式,现有设备返回 Opus |
capacityAlertValue |
容量不足提醒阈值,范围 0~20 |
charging |
是否正在充电 |
recordingFileName |
当前录音文件名,未录音时为空 |
headsetPrimaryMac |
耳机主耳 MAC,未传输时为空 |
systemDurationSeconds |
系统持续时间,单位秒 |
screenBrightness |
屏幕亮度,范围 0~100 |
错误与超时 #
失败方法统一抛出 SeilonSdkException:
try {
await session.queryFiles(1);
} on SeilonSdkException catch (error) {
print('${error.code.name}: ${error.message}');
}
稳定错误码包括 busy、invalidArgument、notConnected、permissionDenied、bluetoothUnavailable、wifiUnavailable、sendFailed、timeout、parseFailed、disconnected、fileTransferFailed、cancelled、unsupportedFeature 和 internal。
- 所有设备回复忽略
seq。 - 普通指令只按来源通道和
category + command匹配。 - 同一会话同一时间只允许一个普通等待操作;并发操作会返回
busy。 - 默认普通指令超时 8 秒、开启 Wi-Fi 指令 45 秒、平台 Wi-Fi/TCP 连接 60 秒、文件空闲 8 秒。
- 超时值可通过
SeilonSdkConfig的commandTimeout、wifiCommandTimeout、networkTimeout和transferIdleTimeout覆盖。
平台配置 #
Android #
- 最低 Flutter 3.44(Dart 3.12)。
- 最低 Android API 24。
- Android 使用 Flutter 3.44 的 AGP 9 迁移结构,插件与 App 模块不再直接应用 KGP。
- AAR Manifest 已声明蓝牙、定位、Wi-Fi、网络和 TCP 所需权限。
- 使用
checkPermissions/requestPermissions处理运行时权限。插件不会自动打开已关闭的蓝牙或 Wi-Fi。
iOS #
- 最低 iOS 15,支持 CocoaPods 与 Swift Package Manager。
- 宿主
Info.plist必须包含蓝牙和本地网络用途说明:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>用于扫描并连接 Seilon 录音设备</string>
<key>NSLocalNetworkUsageDescription</key>
<string>用于连接 Seilon 录音设备提供的本地 TCP 服务</string>
- 连接设备热点时,需要为 App ID 和 target 启用 Hotspot Configuration capability。
生命周期与限制 #
- 一个插件实例只管理一个原生 SDK、一个设备会话和一个独立网络客户端。
- 插件不会自动绑定、握手、重连或切换 BLE/TCP 通道。
- 页面或业务容器退出时,请取消事件订阅并调用
close()。 - SDK 不包含 OTA、DNR、ASR、分享、OPUS 播放或 OPUS/WAV 转换。
- 完整功能入口和受控删除/关机测试流程见
example/。