rf88_plugin
A Flutter plugin for the PointMobile RF88 RFID pistol-grip scanner module. Provides single-shot and trigger-based UHF RFID tag reading over UART on Android.
β οΈ Hardware Requirement: This plugin only works on PointMobile Android devices that support the RF88 RFID scanner module (e.g., PM84). It will not function on standard Android devices.
Features
- π UART Connection β Connects to the RF88 module via the built-in UART serial port
- π‘ Bluetooth Connection β Optional Bluetooth-based connection using MAC address
- π― Single-Shot Scan β Reads exactly one RFID tag and stops automatically
- π« Physical Trigger Support β Detects pistol-grip handle trigger presses via
OnHardwareKeyListener - π Auto Stop β 2.5-second scan timeout to prevent infinite scanning
- π§Ή Tag Debounce β 2-second debounce to prevent duplicate tag events
- π¦ Stream-Based API β
onTagsReadstream for reactive tag delivery
Supported Devices
| Device | Connection | Tested |
|---|---|---|
| PointMobile PM84 | UART (pistol grip) | β |
| PointMobile PM90 | UART | β |
| PointMobile PM30 | UART | β |
| RF88 Bluetooth Sled | Bluetooth | β οΈ |
Getting Started
1. Add the dependency
dependencies:
rf88_plugin: ^0.1.0
2. Android Setup
The plugin bundles the rf88-sdk.jar. No additional Gradle configuration is needed.
Add the following rules to your app's proguard-rules.pro if you use release builds with R8/ProGuard:
-dontwarn device.**
-dontwarn ex.dev.sdk.**
-keep class device.** { *; }
-keep class ex.dev.sdk.** { *; }
3. Permissions
No extra Android permissions are required for UART-based connection. For Bluetooth, make sure BLUETOOTH_CONNECT is declared if targeting Android 12+.
Usage
Basic Setup
import 'package:rf88_plugin/rf88_plugin.dart';
class RfidScannerService {
final _rf88 = Rf88Plugin();
Future<void> initialize() async {
// 1. Initialize the scanner (register listeners)
await _rf88.initScanner();
// 2. Connect via UART (pistol grip)
await _rf88.open();
// 3. Listen for tags
_rf88.onTagsRead.listen((tag) {
print('Tag read: $tag');
});
}
Future<void> dispose() async {
await _rf88.stopSearch();
await _rf88.close();
await _rf88.deinitScanner();
}
}
Single-Shot Scan (Button or Trigger)
// Scan exactly one tag, then stop automatically
await _rf88.singleSearch();
// Or stop manually if needed
await _rf88.stopSearch();
Physical Trigger: When
initScanner()is called, the plugin automatically registers a hardware key listener. Pressing the pistol-grip trigger will callsingleSearch()and releasing it will callstopSearch()β no extra code required.
Bluetooth Connection
// Connect via Bluetooth MAC address instead of UART
await _rf88.initScanner();
await _rf88.connectBluetooth('00:11:22:33:44:55');
_rf88.onTagsRead.listen((tag) {
print('BLE Tag: $tag');
});
Full Widget Example
class ScanScreen extends StatefulWidget {
const ScanScreen({super.key});
@override
State<ScanScreen> createState() => _ScanScreenState();
}
class _ScanScreenState extends State<ScanScreen> {
final _rf88 = Rf88Plugin();
final List<String> _tags = [];
bool _isScanning = false;
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
await _rf88.initScanner();
await _rf88.open();
_rf88.onTagsRead.listen((tag) {
if (!mounted) return;
setState(() {
_tags.insert(0, tag);
_isScanning = false; // auto-stopped after one read
});
});
}
@override
void dispose() {
_rf88.stopSearch();
_rf88.close();
_rf88.deinitScanner();
super.dispose();
}
Future<void> _scan() async {
setState(() => _isScanning = true);
await _rf88.singleSearch();
// Result arrives via onTagsRead stream
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('RFID Scanner')),
body: Column(
children: [
ElevatedButton(
onPressed: _isScanning ? null : _scan,
child: Text(_isScanning ? 'Scanning...' : 'Scan Tag'),
),
Expanded(
child: ListView.builder(
itemCount: _tags.length,
itemBuilder: (ctx, i) => ListTile(title: Text(_tags[i])),
),
),
],
),
);
}
}
API Reference
Rf88Plugin
Methods
| Method | Returns | Description |
|---|---|---|
initScanner() |
Future<bool> |
Registers inventory, connection, and hardware key listeners. Must be called first. |
deinitScanner() |
Future<bool> |
Unregisters all listeners and cleans up resources. Call in dispose(). |
open() |
Future<bool> |
Connects to the RF88 module via UART. Runs asynchronously (non-blocking). |
close() |
Future<bool> |
Disconnects the RF88 module. Runs asynchronously (non-blocking). |
connectBluetooth(mac) |
Future<bool> |
Connects via Bluetooth using a MAC address string. |
singleSearch() |
Future<bool> |
Starts a single-tag inventory. Stops automatically after the first tag or 2.5s timeout. |
stopSearch() |
Future<bool> |
Manually stops an ongoing scan. |
Stream
| Property | Type | Description |
|---|---|---|
onTagsRead |
Stream<String> |
Emits one EPC tag string per successful read. Tag is trimmed to last 15 characters. 2-second debounce prevents duplicate events. |
How It Works
Flutter App
β
β MethodChannel (rf88_plugin/methods)
βΌ
Rf88Plugin.kt βββββββββββββββββββββββββββββββββββ
β β
β rf88Handler (HandlerThread) β
β ββ connect() β UART port open β
β ββ disconnect() β UART port close β
β β
β stopExecutor (background thread) β
β ββ stop() β stops inventory β
β β
β OnHardwareKeyListener β
β ββ onInventoryKeyPressed() β singleSearch()β
β β
β EventChannel (rf88_plugin/tags) β
ββββββββββββββββββββββββββββββββββββββββββββββββΊ
Flutter
onTagsRead stream
Threading Model
| Operation | Thread | Why |
|---|---|---|
connect() |
HandlerThread (background) |
UART openPort() blocks for ~5s; must not block main thread |
disconnect() |
HandlerThread (background) |
Same reason |
stop() |
Executor (background) |
Called from inventory listener callback; can't block the listener thread |
| Tag delivery | Main thread via Handler.post |
Flutter EventSink must be called on the main thread |
Troubleshooting
Connection fails with FAILURE state
The UART serial port may be stuck from a previous session. The plugin automatically calls disconnect() before every connect() to reset the port. If problems persist:
- Restart the device β Resets the UART hardware
- Make sure the RF88 sled is physically attached to the pistol grip
- Check
adb logcat -s RF88_PLUGIN:Dfor detailed connection logs
Trigger press is not detected
- Ensure
initScanner()is called beforeopen() - The
OnHardwareKeyListeneris registered duringinitScanner(). If it is called afteropen(), the listener may not attach correctly - Check logcat for
Pistol trigger PRESSED!to confirm detection
Tags contain extra characters
The plugin trims raw EPC tags to the last 15 characters. If your tags are shorter or need different processing, you can modify the processTag() function in Rf88Plugin.kt.
Duplicate tag reads
A 2-second debounce window prevents the same tag from being emitted twice. If you need a shorter or longer window, adjust DEBOUNCE_MS in Rf88Plugin.kt.
Contributing
Contributions, bug reports, and feature requests are welcome! Please open an issue or submit a pull request on GitHub.
License
MIT License β see LICENSE for details.