rf88_plugin

pub.flutter-io.cn License: MIT Platform

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 β€” onTagsRead stream 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 call singleSearch() and releasing it will call stopSearch() β€” 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:

  1. Restart the device β€” Resets the UART hardware
  2. Make sure the RF88 sled is physically attached to the pistol grip
  3. Check adb logcat -s RF88_PLUGIN:D for detailed connection logs

Trigger press is not detected

  • Ensure initScanner() is called before open()
  • The OnHardwareKeyListener is registered during initScanner(). If it is called after open(), 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.