A Dart port of the excellent Python library benlink by Kyle Husmann, designed for controlling Benshi-protocol radios (like the BTech GMRS-PRO) from a Flutter application.
This library provides the low-level protocol implementation and a high-level RadioController
to make it easy to interact with your radio over Bluetooth. The primary goal is to empower developers to create custom, open-source mobile applications for their hardware.
This project is a work in progress, with the ultimate goal of supporting the full feature set of the original benlink
library.
This project is an independent effort and is not affiliated with or endorsed by Benshi, BTech, Vero, or any other radio manufacturer. The protocol has been reverse-engineered. Use this library at your own risk. The authors are not responsible for any damage caused to your radio or other equipment.
The initial version of this library is focused on providing core functionality for Android apps using Bluetooth Classic (RFCOMM).
This library already supports the core, non-audio features of the radio protocol:
-
Connection Management: Connects to the radio via
flutter_bluetooth_serial
. -
High-Level Controller: An easy-to-use
RadioController
that manages state usingChangeNotifier
, making it simple to integrate with your Flutter UI. -
Device Info: Read the radio's model, vendor, version, and supported features.
-
Radio Settings: Read and write the main settings block of the radio.
-
Channel Management:
-
Read and write individual channel configurations (frequency, name, tones, etc.).
-
Fetch a list of all channels from the radio.
-
-
Real-time Status:
-
Get live status updates (Power, TX/RX state, Scan state, GPS lock, etc.).
-
Subscribe to events for changes in status, settings, and the current channel.
-
-
Power & GPS:
-
Read the current battery voltage and percentage.
-
Fetch the last known GPS position from the device.
-
-
VFO Control: Read the VFO channel and set its frequency.
The goal is to implement the entire feature set of the original Python library. Pull requests are welcome!
-
Audio Streaming: Implement real-time audio capture (TX) and playback (RX) over the audio RFCOMM channel. This is the highest priority feature.
-
TNC / Packet Support: Add support for sending and receiving APRS/BSS data packets and managing
BeaconSettings
. -
BLE Support: Add a
BleCommandLink
to allow connections over Bluetooth Low Energy, in addition to the current RFCOMM link. -
Expanded Command Set: Implement the remaining commands from the protocol (e.g., programmable function buttons, advanced settings, etc.).
-
Firmware Flashing: As a long-term goal, investigate and implement the firmware update process.
This package is not yet on pub.dev
. To use it in your project, add it to your pubspec.yaml
as a git dependency:
dependencies:
flutter_benlink:
git:
url: https://github.com/SarahRoseLives/flutter_benlink.git
ref: main
Then, run flutter pub get in your terminal.
Ensure you have flutter_bluetooth_serial set up in your Android project.
Use ChangeNotifierProvider (from the provider package) to make the RadioController available to your widget tree.
import 'package:flutter/material.dart';
import 'package:flutter_benlink/flutter_benlink.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import 'package:provider/provider.dart';
class RadioInfoScreen extends StatefulWidget {
final BluetoothDevice server; // The paired radio device
const RadioInfoScreen({Key? key, required this.server}) : super(key: key);
@override
_RadioInfoScreenState createState() => _RadioInfoScreenState();
}
class _RadioInfoScreenState extends State<RadioInfoScreen> {
RadioController? _radioController;
@override
void initState() {
super.initState();
_connectToDevice();
}
void _connectToDevice() async {
try {
final connection = await BluetoothConnection.toAddress(widget.server.address);
setState(() {
// Initialize the controller, which will start fetching radio state
_radioController = RadioController(connection: connection);
});
} catch (e) {
print('Cannot connect, exception occurred: $e');
}
}
@override
void dispose() {
_radioController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// If the controller isn't initialized, show a loading indicator
if (_radioController == null) {
return Scaffold(
appBar: AppBar(title: const Text('Connecting...')),
body: const Center(child: CircularProgressIndicator()),
);
}
// Use a ChangeNotifierProvider to provide the controller to the widget tree
return ChangeNotifierProvider.value(
value: _radioController,
child: Scaffold(
appBar: AppBar(
title: const Text('Radio Info'),
),
// Use a Consumer to listen for changes and rebuild the UI
body: Consumer<RadioController>(
builder: (context, radio, child) {
// Wait until the radio is ready before showing data
if (!radio.isReady) {
return const Center(child: CircularProgressIndicator());
}
return ListView(
padding: const EdgeInsets.all(16.0),
children: [
Text('Product: ${radio.deviceInfo?.productName ?? "N/A"}'),
Text('Firmware Version: ${radio.deviceInfo?.firmwareVersion ?? "N/A"}'),
Text('Current Channel: ${radio.currentChannelName}'),
Text('Battery: ${radio.batteryLevelAsPercentage}% (${radio.batteryVoltage?.toStringAsFixed(2)}V)'),
Text('GPS Locked: ${radio.isGpsLocked ? "Yes" : "No"}'),
],
);
},
),
),
);
}
}
Kyle Husmann (khusmann) for creating and open-sourcing the original benlink library and for doing the heavy lifting of reverse-engineering the protocol.
@spohtl for help with the audio protocol in the original project.
@na7q for early testing and feedback on the original project.