Initial commit

This commit is contained in:
Tony
2026-04-27 08:00:32 +08:00
commit fbfc24c9e5
20 changed files with 1366 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.
@@ -0,0 +1 @@
{"version":2,"entries":[{"package":"ble_relay_host","rootUri":"../","packageUri":"lib/"}]}
+178
View File
@@ -0,0 +1,178 @@
{
"configVersion": 2,
"packages": [
{
"name": "async",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/async-2.13.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "boolean_selector",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "characters",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/characters-1.4.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "clock",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/clock-1.1.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "collection",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/collection-1.19.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "fake_async",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/fake_async-1.3.3",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "flutter",
"rootUri": "file:///Users/tonylu/git/flutter/packages/flutter",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "flutter_lints",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/flutter_lints-5.0.0",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "flutter_test",
"rootUri": "file:///Users/tonylu/git/flutter/packages/flutter_test",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "leak_tracker",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/leak_tracker-11.0.2",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "leak_tracker_flutter_testing",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.10",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "leak_tracker_testing",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.2",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "lints",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/lints-5.1.1",
"packageUri": "lib/",
"languageVersion": "3.6"
},
{
"name": "matcher",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/matcher-0.12.17",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "material_color_utilities",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1",
"packageUri": "lib/",
"languageVersion": "2.17"
},
{
"name": "meta",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/meta-1.16.0",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "path",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/path-1.9.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "plugin_platform_interface",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/plugin_platform_interface-2.1.8",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "sky_engine",
"rootUri": "file:///Users/tonylu/git/flutter/bin/cache/pkg/sky_engine",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "source_span",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/source_span-1.10.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "stack_trace",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/stack_trace-1.12.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "stream_channel",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/stream_channel-2.1.4",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "string_scanner",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/string_scanner-1.4.1",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "term_glyph",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/term_glyph-1.2.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "test_api",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/test_api-0.7.6",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "vector_math",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/vector_math-2.2.0",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "vm_service",
"rootUri": "file:///Users/tonylu/.pub-cache/hosted/pub.dev/vm_service-15.1.0",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "ble_relay_host",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "3.1"
}
],
"generator": "pub",
"generatorVersion": "3.9.2",
"flutterRoot": "file:///Users/tonylu/git/flutter",
"flutterVersion": "3.35.4",
"pubCache": "file:///Users/tonylu/.pub-cache"
}
+232
View File
@@ -0,0 +1,232 @@
{
"roots": [
"ble_relay_host"
],
"packages": [
{
"name": "ble_relay_host",
"version": "0.1.0",
"dependencies": [
"flutter",
"plugin_platform_interface"
],
"devDependencies": [
"flutter_lints",
"flutter_test"
]
},
{
"name": "flutter_lints",
"version": "5.0.0",
"dependencies": [
"lints"
]
},
{
"name": "flutter_test",
"version": "0.0.0",
"dependencies": [
"clock",
"collection",
"fake_async",
"flutter",
"leak_tracker_flutter_testing",
"matcher",
"meta",
"path",
"stack_trace",
"stream_channel",
"test_api",
"vector_math"
]
},
{
"name": "flutter",
"version": "0.0.0",
"dependencies": [
"characters",
"collection",
"material_color_utilities",
"meta",
"sky_engine",
"vector_math"
]
},
{
"name": "vector_math",
"version": "2.2.0",
"dependencies": []
},
{
"name": "test_api",
"version": "0.7.6",
"dependencies": [
"async",
"boolean_selector",
"collection",
"meta",
"source_span",
"stack_trace",
"stream_channel",
"string_scanner",
"term_glyph"
]
},
{
"name": "stream_channel",
"version": "2.1.4",
"dependencies": [
"async"
]
},
{
"name": "stack_trace",
"version": "1.12.1",
"dependencies": [
"path"
]
},
{
"name": "path",
"version": "1.9.1",
"dependencies": []
},
{
"name": "meta",
"version": "1.16.0",
"dependencies": []
},
{
"name": "matcher",
"version": "0.12.17",
"dependencies": [
"async",
"meta",
"stack_trace",
"term_glyph",
"test_api"
]
},
{
"name": "leak_tracker_flutter_testing",
"version": "3.0.10",
"dependencies": [
"flutter",
"leak_tracker",
"leak_tracker_testing",
"matcher",
"meta"
]
},
{
"name": "fake_async",
"version": "1.3.3",
"dependencies": [
"clock",
"collection"
]
},
{
"name": "collection",
"version": "1.19.1",
"dependencies": []
},
{
"name": "clock",
"version": "1.1.2",
"dependencies": []
},
{
"name": "sky_engine",
"version": "0.0.0",
"dependencies": []
},
{
"name": "material_color_utilities",
"version": "0.11.1",
"dependencies": [
"collection"
]
},
{
"name": "characters",
"version": "1.4.0",
"dependencies": []
},
{
"name": "leak_tracker_testing",
"version": "3.0.2",
"dependencies": [
"leak_tracker",
"matcher",
"meta"
]
},
{
"name": "leak_tracker",
"version": "11.0.2",
"dependencies": [
"clock",
"collection",
"meta",
"path",
"vm_service"
]
},
{
"name": "lints",
"version": "5.1.1",
"dependencies": []
},
{
"name": "term_glyph",
"version": "1.2.2",
"dependencies": []
},
{
"name": "boolean_selector",
"version": "2.1.2",
"dependencies": [
"source_span",
"string_scanner"
]
},
{
"name": "async",
"version": "2.13.1",
"dependencies": [
"collection",
"meta"
]
},
{
"name": "string_scanner",
"version": "1.4.1",
"dependencies": [
"source_span"
]
},
{
"name": "source_span",
"version": "1.10.2",
"dependencies": [
"collection",
"path",
"term_glyph"
]
},
{
"name": "plugin_platform_interface",
"version": "2.1.8",
"dependencies": [
"meta"
]
},
{
"name": "vm_service",
"version": "15.1.0",
"dependencies": []
}
],
"configVersion": 1
}
+1
View File
@@ -0,0 +1 @@
3.35.4
+46
View File
@@ -0,0 +1,46 @@
group 'com.dalimaster.ble_relay_host'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.9.20'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
namespace 'com.dalimaster.ble_relay_host'
compileSdk 34
defaultConfig {
minSdk 21
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
+1
View File
@@ -0,0 +1 @@
include ':ble_relay_host'
@@ -0,0 +1,277 @@
package com.dalimaster.ble_relay_host
import android.bluetooth.*
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.bluetooth.le.BluetoothLeAdvertiser
import android.content.Context
import android.os.ParcelUuid
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import java.util.UUID
private const val TAG = "BleRelayHostPlugin"
private const val CHANNEL = "com.dalimaster.ble_relay_host"
private const val EVENT_CHANNEL = "com.dalimaster.ble_relay_host/clientWrites"
class BleRelayHostPlugin : FlutterPlugin, MethodCallHandler {
private lateinit var methodChannel: MethodChannel
private lateinit var eventChannel: EventChannel
private var eventSink: EventChannel.EventSink? = null
private var context: Context? = null
private var bluetoothManager: BluetoothManager? = null
private var bluetoothAdapter: BluetoothAdapter? = null
private var gattServer: BluetoothGattServer? = null
private var advertiser: BluetoothLeAdvertiser? = null
private var relayCharacteristic: BluetoothGattCharacteristic? = null
private val connectedDevices = mutableSetOf<BluetoothDevice>()
private var pendingStartResult: Result? = null
// -----------------------------------------------------------------------
// FlutterPlugin callbacks
// -----------------------------------------------------------------------
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
context = binding.applicationContext
bluetoothManager =
binding.applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
bluetoothAdapter = bluetoothManager?.adapter
methodChannel = MethodChannel(binding.binaryMessenger, CHANNEL)
methodChannel.setMethodCallHandler(this)
eventChannel = EventChannel(binding.binaryMessenger, EVENT_CHANNEL)
eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
eventSink = sink
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
})
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
stopInternal()
context = null
}
// -----------------------------------------------------------------------
// MethodCallHandler
// -----------------------------------------------------------------------
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"start" -> {
val advertisedName = call.argument<String>("advertisedName") ?: "DALI-Bridge"
val serviceUuidStr = call.argument<String>("serviceUuid")
?: "0000fff0-0000-1000-8000-00805f9b34fb"
val charUuidStr = call.argument<String>("characteristicUuid")
?: "0000fff1-0000-1000-8000-00805f9b34fb"
startInternal(advertisedName, serviceUuidStr, charUuidStr, result)
}
"push" -> {
val data = call.argument<ByteArray>("data")
if (data != null) pushInternal(data)
result.success(null)
}
"stop" -> {
pendingStartResult?.error("CANCELLED", "BLE relay host start cancelled", null)
pendingStartResult = null
stopInternal()
result.success(null)
}
"isSupported" -> result.success(bluetoothAdapter != null && bluetoothAdapter!!.isMultipleAdvertisementSupported)
else -> result.notImplemented()
}
}
// -----------------------------------------------------------------------
// BLE internals
// -----------------------------------------------------------------------
private fun startInternal(
advertisedName: String,
serviceUuidStr: String,
charUuidStr: String,
result: Result,
) {
val ctx = context ?: return result.error("NO_CONTEXT", "No context", null)
val adapter = bluetoothAdapter
?: return result.error("BLE_UNAVAILABLE", "Bluetooth adapter not available", null)
if (!adapter.isMultipleAdvertisementSupported) {
return result.error("NOT_SUPPORTED", "BLE peripheral advertising not supported on this device", null)
}
if (pendingStartResult != null) {
return result.error("START_IN_PROGRESS", "BLE relay host start already in progress", null)
}
stopInternal()
val serviceUuid = UUID.fromString(serviceUuidStr)
val charUuid = UUID.fromString(charUuidStr)
val characteristic = BluetoothGattCharacteristic(
charUuid,
BluetoothGattCharacteristic.PROPERTY_READ or
BluetoothGattCharacteristic.PROPERTY_WRITE or
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ or
BluetoothGattCharacteristic.PERMISSION_WRITE,
)
// Client Characteristic Configuration Descriptor for notifications.
val cccd = BluetoothGattDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),
BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE,
)
characteristic.addDescriptor(cccd)
relayCharacteristic = characteristic
val service = BluetoothGattService(serviceUuid, BluetoothGattService.SERVICE_TYPE_PRIMARY)
service.addCharacteristic(characteristic)
val server = bluetoothManager!!.openGattServer(ctx, gattServerCallback)
?: return result.error("GATT_SERVER_FAILED", "Unable to open GATT server", null)
server.addService(service)
gattServer = server
// Start advertising.
try {
adapter.name = advertisedName
} catch (e: SecurityException) {
Log.w(TAG, "Cannot set device name: ${e.message}")
}
advertiser = adapter.bluetoothLeAdvertiser
?: return result.error("ADVERTISER_UNAVAILABLE", "BLE advertiser not available", null)
val settings = AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
.setConnectable(true)
.setTimeout(0)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
.build()
val data = AdvertiseData.Builder()
.setIncludeDeviceName(true)
.addServiceUuid(ParcelUuid(serviceUuid))
.build()
pendingStartResult = result
advertiser?.startAdvertising(settings, data, advertiseCallback)
}
private fun pushInternal(data: ByteArray) {
val server = gattServer ?: return
val char = relayCharacteristic ?: return
char.value = data
val devsCopy: Set<BluetoothDevice>
synchronized(connectedDevices) { devsCopy = connectedDevices.toSet() }
for (device in devsCopy) {
try {
server.notifyCharacteristicChanged(device, char, false)
} catch (e: Exception) {
Log.e(TAG, "notifyCharacteristicChanged failed: ${e.message}")
}
}
}
private fun stopInternal() {
advertiser?.stopAdvertising(advertiseCallback)
advertiser = null
synchronized(connectedDevices) { connectedDevices.clear() }
gattServer?.close()
gattServer = null
relayCharacteristic = null
}
// -----------------------------------------------------------------------
// GATT server callback
// -----------------------------------------------------------------------
private val gattServerCallback = object : BluetoothGattServerCallback() {
override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.d(TAG, "Client connected: ${device.address}")
synchronized(connectedDevices) { connectedDevices.add(device) }
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.d(TAG, "Client disconnected: ${device.address}")
synchronized(connectedDevices) { connectedDevices.remove(device) }
}
}
override fun onCharacteristicWriteRequest(
device: BluetoothDevice,
requestId: Int,
characteristic: BluetoothGattCharacteristic,
preparedWrite: Boolean,
responseNeeded: Boolean,
offset: Int,
value: ByteArray,
) {
if (responseNeeded) {
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null)
}
// Forward the written frame to Flutter.
eventSink?.success(value)
}
override fun onDescriptorWriteRequest(
device: BluetoothDevice,
requestId: Int,
descriptor: BluetoothGattDescriptor,
preparedWrite: Boolean,
responseNeeded: Boolean,
offset: Int,
value: ByteArray,
) {
if (responseNeeded) {
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null)
}
}
override fun onCharacteristicReadRequest(
device: BluetoothDevice,
requestId: Int,
offset: Int,
characteristic: BluetoothGattCharacteristic,
) {
gattServer?.sendResponse(
device, requestId, BluetoothGatt.GATT_SUCCESS, 0,
characteristic.value ?: ByteArray(0),
)
}
}
// -----------------------------------------------------------------------
// Advertise callback
// -----------------------------------------------------------------------
private val advertiseCallback = object : AdvertiseCallback() {
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
Log.d(TAG, "BLE advertising started")
pendingStartResult?.success(null)
pendingStartResult = null
}
override fun onStartFailure(errorCode: Int) {
Log.e(TAG, "BLE advertising failed: $errorCode")
pendingStartResult?.error(
"ADVERTISE_FAILED",
"BLE advertising failed with code $errorCode",
errorCode,
)
pendingStartResult = null
stopInternal()
}
}
}
+23
View File
@@ -0,0 +1,23 @@
import Foundation
import Flutter
/// iOS stub BLE peripheral (CoreBluetooth CBPeripheralManager) is available on iOS but
/// not yet implemented. `isSupported` returns false; all other methods are no-ops.
public class BleRelayHostPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "com.dalimaster.ble_relay_host",
binaryMessenger: registrar.messenger())
let instance = BleRelayHostPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "isSupported":
result(false)
default:
result(nil)
}
}
}
+18
View File
@@ -0,0 +1,18 @@
/// BLE GATT-server peripheral host adapter for the DALI Toolkit relay feature.
///
/// Usage:
/// ```dart
/// final host = BleRelayHost();
/// if (host.isSupported) {
/// await host.start(
/// advertisedName: 'DALI-Bridge',
/// onClientWrite: (data) async { /* forward to DALI upstream */ },
/// );
/// // Push DALI responses back to connected BLE clients:
/// await host.push(responseBytes);
/// await host.stop();
/// }
/// ```
library ble_relay_host;
export 'src/ble_relay_host.dart';
+101
View File
@@ -0,0 +1,101 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// GATT service UUID shared between DALI Toolkit BLE central (client) and the
/// relay host peripheral. Must match the UUID used in `ble.dart` / `ble_web.dart`.
const String kBleRelayServiceUuid = '0000fff0-0000-1000-8000-00805f9b34fb';
/// GATT characteristic UUID for bidirectional DALI frame data.
const String kBleRelayCharacteristicUuid = '0000fff1-0000-1000-8000-00805f9b34fb';
/// Method-channel name (must match Android/iOS/macOS native implementations).
const String _kChannelName = 'com.dalimaster.ble_relay_host';
/// An abstraction over the platform BLE GATT-server implementation.
///
/// On Android (API 21+) this wires into [BluetoothGattServer] + [BluetoothLeAdvertiser].
/// On all other platforms, [isSupported] returns `false` and every method is a no-op.
class BleRelayHost {
BleRelayHost() : _channel = const MethodChannel(_kChannelName);
@visibleForTesting
BleRelayHost.withChannel(MethodChannel channel) : _channel = channel;
final MethodChannel _channel;
StreamSubscription<dynamic>? _eventSub;
Future<void> Function(Uint8List data)? _onClientWrite;
bool _running = false;
bool get isRunning => _running;
/// Returns true if the current platform has a native implementation.
/// On Web this is always false; on non-Android platforms this calls the
/// native `isSupported` method to let the platform decide.
bool get isSupported {
if (kIsWeb) return false;
// Synchronous check is not possible via MethodChannel; instead we rely on
// the static per-platform capability flags defined at the package root.
// See [BleRelayHostCapability.isSupported].
return defaultTargetPlatform == TargetPlatform.android;
}
/// Starts BLE advertising and opens the GATT server.
///
/// [advertisedName] — the Bluetooth device name visible to scanning clients.
/// [onClientWrite] — callback invoked whenever a connected client writes a
/// frame to the relay characteristic. Forward the bytes to
/// the upstream DALI transport.
Future<void> start({
required String advertisedName,
required Future<void> Function(Uint8List data) onClientWrite,
}) async {
if (!isSupported) return;
_onClientWrite = onClientWrite;
// Listen for inbound writes from BLE clients via event channel.
const eventChannel = EventChannel('$_kChannelName/clientWrites');
_eventSub?.cancel();
_eventSub = eventChannel.receiveBroadcastStream().listen((dynamic event) {
if (event is Uint8List) {
_onClientWrite?.call(event);
} else if (event is List) {
_onClientWrite?.call(Uint8List.fromList(event.cast<int>()));
}
});
try {
await _channel.invokeMethod<void>('start', {
'advertisedName': advertisedName,
'serviceUuid': kBleRelayServiceUuid,
'characteristicUuid': kBleRelayCharacteristicUuid,
});
_running = true;
} catch (_) {
await _eventSub?.cancel();
_eventSub = null;
_onClientWrite = null;
rethrow;
}
}
/// Sends [data] as a GATT notification to all connected clients.
Future<void> push(Uint8List data) async {
if (!_running) return;
await _channel.invokeMethod<void>('push', {'data': data});
}
/// Stops advertising, disconnects clients, and closes the GATT server.
Future<void> stop() async {
await _eventSub?.cancel();
_eventSub = null;
_onClientWrite = null;
if (_running) {
await _channel.invokeMethod<void>('stop');
}
_running = false;
}
}
+27
View File
@@ -0,0 +1,27 @@
cmake_minimum_required(VERSION 3.10)
set(PROJECT_NAME "ble_relay_host")
project(${PROJECT_NAME} LANGUAGES CXX)
cmake_policy(VERSION 3.10...3.24)
set(PLUGIN_NAME "ble_relay_host_plugin")
list(APPEND PLUGIN_SOURCES
"ble_relay_host_plugin.cc"
)
add_library(${PLUGIN_NAME} SHARED
${PLUGIN_SOURCES}
)
apply_standard_settings(${PLUGIN_NAME})
set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter)
target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
set(ble_relay_host_bundled_libraries
""
PARENT_SCOPE
)
+48
View File
@@ -0,0 +1,48 @@
#include "ble_relay_host_plugin.h"
#include <flutter_linux/flutter_linux.h>
#define BLE_RELAY_HOST_PLUGIN(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), ble_relay_host_plugin_get_type(), BleRelayHostPlugin))
struct _BleRelayHostPlugin {
GObject parent_instance;
};
G_DEFINE_TYPE(BleRelayHostPlugin, ble_relay_host_plugin, g_object_get_type())
static void ble_relay_host_plugin_handle_method_call(
BleRelayHostPlugin* self,
FlMethodCall* method_call) {
g_autoptr(FlMethodResponse) response = nullptr;
const gchar* method = fl_method_call_get_name(method_call);
if (strcmp(method, "isSupported") == 0) {
g_autoptr(FlValue) result = fl_value_new_bool(FALSE);
response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
} else {
response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_null()));
}
fl_method_call_respond(method_call, response, nullptr);
}
static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call,
gpointer user_data) {
BleRelayHostPlugin* plugin = BLE_RELAY_HOST_PLUGIN(user_data);
ble_relay_host_plugin_handle_method_call(plugin, method_call);
}
void ble_relay_host_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
BleRelayHostPlugin* plugin = BLE_RELAY_HOST_PLUGIN(
g_object_new(ble_relay_host_plugin_get_type(), nullptr));
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
g_autoptr(FlMethodChannel) channel =
fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),
"com.dalimaster.ble_relay_host",
FL_METHOD_CODEC(codec));
fl_method_channel_set_method_call_handler(channel, method_call_cb,
g_object_ref(plugin),
g_object_unref);
g_object_unref(plugin);
}
static void ble_relay_host_plugin_class_init(BleRelayHostPluginClass* klass) {}
static void ble_relay_host_plugin_init(BleRelayHostPlugin* self) {}
+16
View File
@@ -0,0 +1,16 @@
#ifndef FLUTTER_PLUGIN_BLE_RELAY_HOST_PLUGIN_H_
#define FLUTTER_PLUGIN_BLE_RELAY_HOST_PLUGIN_H_
#include <flutter_linux/flutter_linux.h>
G_BEGIN_DECLS
#define BLE_RELAY_HOST_PLUGIN_TYPE (ble_relay_host_plugin_get_type())
G_DECLARE_FINAL_TYPE(BleRelayHostPlugin, ble_relay_host_plugin, BLE_RELAY_HOST, PLUGIN, GObject)
void ble_relay_host_plugin_register_with_registrar(FlPluginRegistrar* registrar);
G_END_DECLS
#endif // FLUTTER_PLUGIN_BLE_RELAY_HOST_PLUGIN_H_
+22
View File
@@ -0,0 +1,22 @@
import Foundation
import FlutterMacOS
/// macOS stub BLE peripheral mode not yet implemented.
public class BleRelayHostPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "com.dalimaster.ble_relay_host",
binaryMessenger: registrar.messenger)
let instance = BleRelayHostPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "isSupported":
result(false)
default:
result(nil)
}
}
}
+213
View File
@@ -0,0 +1,213 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "5.1.1"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
plugin_platform_interface:
dependency: "direct main"
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
url: "https://pub.dev"
source: hosted
version: "15.1.0"
sdks:
dart: ">=3.8.0-0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
+33
View File
@@ -0,0 +1,33 @@
name: ble_relay_host
description: "BLE GATT-server peripheral host adapter for the DALI Toolkit relay feature. Provides a simple start/stop/push API backed by platform-specific BLE advertising."
version: 0.1.0
publish_to: none
environment:
sdk: '>=3.1.4 <4.0.0'
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
plugin:
platforms:
android:
package: com.dalimaster.ble_relay_host
pluginClass: BleRelayHostPlugin
ios:
pluginClass: BleRelayHostPlugin
macos:
pluginClass: BleRelayHostPlugin
linux:
pluginClass: BleRelayHostPlugin
windows:
pluginClass: BleRelayHostPluginCApi
+28
View File
@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.14)
set(PROJECT_NAME "ble_relay_host")
project(${PROJECT_NAME} LANGUAGES CXX)
cmake_policy(VERSION 3.14...3.25)
set(PLUGIN_NAME "ble_relay_host_plugin")
list(APPEND PLUGIN_SOURCES
"ble_relay_host_plugin.cpp"
)
add_library(${PLUGIN_NAME} SHARED
"include/ble_relay_host/ble_relay_host_plugin_c_api.h"
${PLUGIN_SOURCES}
)
apply_standard_settings(${PLUGIN_NAME})
set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)
set(ble_relay_host_bundled_libraries
""
PARENT_SCOPE
)
+47
View File
@@ -0,0 +1,47 @@
#include "ble_relay_host_plugin.h"
// Windows stub — BLE peripheral not yet implemented.
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
namespace ble_relay_host {
class BleRelayHostPlugin : public flutter::Plugin {
public:
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar) {
auto channel = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
registrar->messenger(), "com.dalimaster.ble_relay_host",
&flutter::StandardMethodCodec::GetInstance());
auto plugin = std::make_unique<BleRelayHostPlugin>();
channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto& call, auto result) {
plugin_pointer->HandleMethodCall(call, std::move(result));
});
registrar->AddPlugin(std::move(plugin));
}
BleRelayHostPlugin() {}
virtual ~BleRelayHostPlugin() {}
private:
void HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
if (method_call.method_name().compare("isSupported") == 0) {
result->Success(flutter::EncodableValue(false));
} else {
result->Success();
}
}
};
} // namespace ble_relay_host
void BleRelayHostPluginCApiRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar) {
ble_relay_host::BleRelayHostPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarManager::GetInstance()
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
}
+23
View File
@@ -0,0 +1,23 @@
#ifndef FLUTTER_PLUGIN_BLE_RELAY_HOST_PLUGIN_H_
#define FLUTTER_PLUGIN_BLE_RELAY_HOST_PLUGIN_H_
#include <flutter/plugin_registrar_windows.h>
#ifdef FLUTTER_PLUGIN_IMPL
#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport)
#else
#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport)
#endif
#if defined(__cplusplus)
extern "C" {
#endif
FLUTTER_PLUGIN_EXPORT void BleRelayHostPluginCApiRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar);
#if defined(__cplusplus)
} // extern "C"
#endif
#endif // FLUTTER_PLUGIN_BLE_RELAY_HOST_PLUGIN_H_