seilon_voice_plugin 0.1.4
seilon_voice_plugin: ^0.1.4 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.2. It supports Android API 24+ and iOS 15+, providing device discovery and connection, three-step binding, recorder controls, Wi-Fi/TCP networking, file transfer, runtime logs, and raw OPUS frames.
Installation #
dependencies:
seilon_voice_plugin: ^0.1.3
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 |
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 |
Wi-Fi, TCP, and channel selection #
await session.setWifiEnabled(true);
await session.connectWifi(
ssid: deviceHotspotSsid,
password: deviceHotspotPassword,
);
final endpoint = await session.queryTcpEndpoint();
await session.configureTcp(host: endpoint.host, port: endpoint.port);
await session.connectTcp();
await session.setChannel(SeilonChannel.tcp);
| API | Description |
|---|---|
connectWifi(ssid:, password:) / disconnectWifi() |
Connects to or disconnects from the device hotspot |
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 |
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 the current download wait |
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 wait. Received data is retained for an explicit resume.
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 and one device session.
- 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.2 的 Flutter 3.44+ 插件,支持 Android API 24+ 与 iOS 15+。插件提供设备扫描与连接、三步绑定、录音设备控制、Wi-Fi/TCP、文件传输、运行日志和原始 OPUS 帧。
安装 #
dependencies:
seilon_voice_plugin: ^0.1.3
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 |
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) |
开启或关闭设备热点 |
Wi-Fi、TCP 与通道 #
await session.setWifiEnabled(true);
await session.connectWifi(
ssid: deviceHotspotSsid,
password: deviceHotspotPassword,
);
final endpoint = await session.queryTcpEndpoint();
await session.configureTcp(host: endpoint.host, port: endpoint.port);
await session.connectTcp();
await session.setChannel(SeilonChannel.tcp);
| API | 说明 |
|---|---|
connectWifi(ssid:, password:) / disconnectWifi() |
连接或断开设备热点 |
queryTcpEndpoint() |
从设备读取 TCP 地址 |
configureTcp(host:, port:) |
配置后续 TCP 连接实际使用的地址 |
connectTcp() / disconnectTcp() |
连接或断开 TCP |
setChannel(SeilonChannel.ble/tcp) |
切换 SDK 指令通道 |
设备开启热点后,热点账号和密码可从对应日志包中解析:
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() |
取消当前下载等待 |
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()只取消当前等待;已接收数据会保留用于显式断点续传。
状态与事件 #
| 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/。