zebra_rfid_plugin 1.0.0
zebra_rfid_plugin: ^1.0.0 copied to clipboard
A Flutter plugin for Zebra RFID SDK (RFD40XX) with support for tag reading, writing, and locating.
import 'package:flutter/material.dart';
import 'package:zebra_rfid_plugin/zebra_rfid_plugin.dart';
import 'dart:async';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Zebra RFID Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const RfidHomePage(),
);
}
}
class RfidHomePage extends StatefulWidget {
const RfidHomePage({Key? key}) : super(key: key);
@override
State<RfidHomePage> createState() => _RfidHomePageState();
}
class _RfidHomePageState extends State<RfidHomePage> {
bool _isInitialized = false;
bool _isConnected = false;
bool _isScanning = false;
bool _isLocationing = false;
List<ReaderDevice> _readers = [];
ReaderDevice? _selectedReader;
final Map<String, TagReadEvent> _scannedTags = {};
String? _statusMessage;
int _batteryLevel = 0;
StreamSubscription<TagReadEvent>? _tagReadSubscription;
StreamSubscription<StatusEvent>? _statusSubscription;
@override
void initState() {
super.initState();
_initializePlugin();
_setupEventListeners();
}
@override
void dispose() {
_tagReadSubscription?.cancel();
_statusSubscription?.cancel();
ZebraRfidPlugin.dispose();
super.dispose();
}
Future<void> _initializePlugin() async {
try {
final result =
await ZebraRfidPlugin.initialize(transportType: 'SERVICE_USB');
setState(() {
_isInitialized = result;
_statusMessage = result ? 'SDK Initialized' : 'Initialization Failed';
});
if (result) {
await _loadReaders();
}
} catch (e) {
setState(() {
_statusMessage = 'Error: $e';
});
}
}
void _setupEventListeners() {
// Listen to tag read events
_tagReadSubscription = ZebraRfidPlugin.tagReadStream.listen(
(event) {
setState(() {
_scannedTags[event.tagId] = event;
});
},
onError: (error) {
_showSnackBar('Tag read error: $error');
},
);
// Listen to status events
_statusSubscription = ZebraRfidPlugin.statusEventStream.listen(
(event) {
setState(() {
_statusMessage = event.message;
});
// Handle specific status events
switch (event.type) {
case StatusEventType.TRIGGER_PRESSED:
_startScanning();
break;
case StatusEventType.TRIGGER_RELEASED:
_stopScanning();
break;
case StatusEventType.READER_DISCONNECTED:
setState(() {
_isConnected = false;
_isScanning = false;
});
break;
case StatusEventType.BATTERY_LOW:
case StatusEventType.BATTERY_CRITICAL:
if (event.data != null && event.data!['level'] != null) {
setState(() {
_batteryLevel = event.data!['level'];
});
}
break;
default:
break;
}
},
onError: (error) {
_showSnackBar('Status error: $error');
},
);
}
Future<void> _loadReaders() async {
try {
final readers = await ZebraRfidPlugin.getAvailableReaders();
setState(() {
_readers = readers;
if (readers.isNotEmpty && _selectedReader == null) {
_selectedReader = readers.first;
}
});
} catch (e) {
_showSnackBar('Error loading readers: $e');
}
}
Future<void> _connect() async {
if (_selectedReader == null) {
_showSnackBar('Please select a reader first');
return;
}
try {
final result = await ZebraRfidPlugin.connect(_selectedReader!.name);
setState(() {
_isConnected = result;
_statusMessage = result
? 'Connected to ${_selectedReader!.name}'
: 'Connection failed';
});
if (result) {
// Get battery level
try {
_batteryLevel = await ZebraRfidPlugin.getBatteryLevel();
setState(() {});
} catch (e) {
// Battery info might not be available for all readers
}
// Configure reader with default settings
await _configureDefaultSettings();
}
} catch (e) {
_showSnackBar('Connection error: $e');
}
}
Future<void> _configureDefaultSettings() async {
try {
final config = ReaderConfig(
antennaPower: 270, // Medium power
beeperEnabled: true,
session: 'S1',
tagPopulation: 100,
);
await ZebraRfidPlugin.configureReader(config);
} catch (e) {
_showSnackBar('Configuration error: $e');
}
}
Future<void> _disconnect() async {
try {
await ZebraRfidPlugin.disconnect();
setState(() {
_isConnected = false;
_isScanning = false;
_statusMessage = 'Disconnected';
});
} catch (e) {
_showSnackBar('Disconnect error: $e');
}
}
Future<void> _startScanning() async {
if (!_isConnected) {
_showSnackBar('Please connect to a reader first');
return;
}
try {
setState(() {
_scannedTags.clear();
});
final result = await ZebraRfidPlugin.startInventory();
setState(() {
_isScanning = result;
_statusMessage = result ? 'Scanning...' : 'Failed to start scanning';
});
} catch (e) {
_showSnackBar('Scan error: $e');
}
}
Future<void> _stopScanning() async {
try {
final result = await ZebraRfidPlugin.stopInventory();
setState(() {
_isScanning = !result;
_statusMessage =
result ? 'Scanning stopped' : 'Failed to stop scanning';
});
} catch (e) {
_showSnackBar('Stop scan error: $e');
}
}
Future<void> _writeTag(String tagId, String data) async {
if (!_isConnected) {
_showSnackBar('Please connect to a reader first');
return;
}
try {
final result = await ZebraRfidPlugin.writeTag(
tagId: tagId,
data: data,
memoryBank: 'EPC',
offset: 2,
);
_showSnackBar(
result ? 'Tag written successfully' : 'Failed to write tag');
} catch (e) {
_showSnackBar('Write error: $e');
}
}
Future<void> _locateTag(String tagId) async {
if (!_isConnected) {
_showSnackBar('Please connect to a reader first');
return;
}
try {
if (_isLocationing) {
await ZebraRfidPlugin.stopLocationing();
setState(() {
_isLocationing = false;
_statusMessage = 'Locationing stopped';
});
} else {
final result = await ZebraRfidPlugin.startLocationing(tagId);
setState(() {
_isLocationing = result;
_statusMessage =
result ? 'Locating tag $tagId...' : 'Failed to start locationing';
});
}
} catch (e) {
_showSnackBar('Locationing error: $e');
}
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
void _showWriteDialog(String tagId) {
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Write to EPC'),
// 1. Wrap content in SingleChildScrollView to prevent overflow
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Tag ID: $tagId'),
const SizedBox(height: 16),
const Text('Enter hex data (multiple of 4 chars for full words):'),
TextField(
controller: controller,
decoration: const InputDecoration(
labelText: 'Data (Hex)',
hintText: 'e.g., 300833FF (8 chars = 2 words)',
),
// 2. Ensure keyboard doesn't hide input
scrollPadding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom + 20),
onChanged: (value) {
// Note: The UI won't rebuild here to show the "Words:" text
// unless you wrap this part in a StatefulBuilder or
// ValueListenableBuilder.
},
),
// This part logic technically requires a rebuild to show up dynamically
if (controller.text.isNotEmpty)
Text(
'Words: ${controller.text.length ~/ 4} (Offset 0: Ensure <= tag EPC size)',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_writeTag(tagId, controller.text.toUpperCase());
},
child: const Text('Write'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Zebra RFID Demo'),
actions: [
if (_isConnected && _batteryLevel > 0)
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(
Icons.battery_full,
color: _batteryLevel < 20 ? Colors.red : Colors.white,
),
const SizedBox(width: 4),
Text('$_batteryLevel%'),
],
),
),
],
),
body: Column(
children: [
// Status card
Card(
margin: const EdgeInsets.all(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Icon(
_isConnected ? Icons.check_circle : Icons.error,
color: _isConnected ? Colors.green : Colors.grey,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_statusMessage ?? 'Ready',
style: Theme.of(context).textTheme.bodyLarge,
),
),
],
),
const SizedBox(height: 8),
if (_isInitialized && !_isConnected)
DropdownButton<ReaderDevice>(
isExpanded: true,
value: _selectedReader,
hint: const Text('Select Reader'),
items: _readers.map((reader) {
return DropdownMenuItem(
value: reader,
child: Text(reader.name),
);
}).toList(),
onChanged: (reader) {
setState(() {
_selectedReader = reader;
});
},
),
],
),
),
),
// Control buttons
Padding(
padding: const EdgeInsets.all(8),
child: Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [
if (!_isConnected)
ElevatedButton.icon(
onPressed: _isInitialized ? _connect : null,
icon: const Icon(Icons.link),
label: const Text('Connect'),
)
else
ElevatedButton.icon(
onPressed: _disconnect,
icon: const Icon(Icons.link_off),
label: const Text('Disconnect'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
ElevatedButton.icon(
onPressed:
_isConnected && !_isScanning ? _startScanning : null,
icon: const Icon(Icons.play_arrow),
label: const Text('Start Scan'),
),
ElevatedButton.icon(
onPressed: _isConnected && _isScanning ? _stopScanning : null,
icon: const Icon(Icons.stop),
label: const Text('Stop Scan'),
),
ElevatedButton.icon(
onPressed: _isConnected
? () {
setState(() {
_scannedTags.clear();
});
}
: null,
icon: const Icon(Icons.clear),
label: const Text('Clear'),
),
],
),
),
// Tags list
Expanded(
child: Card(
margin: const EdgeInsets.all(8),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Scanned Tags: ${_scannedTags.length}',
style: Theme.of(context).textTheme.titleMedium,
),
Text(
'Total Reads: ${_scannedTags.values.fold(0, (sum, tag) => sum + tag.count)}',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
),
const Divider(height: 1),
Expanded(
child: _scannedTags.isEmpty
? const Center(
child: Text('No tags scanned yet'),
)
: ListView.builder(
itemCount: _scannedTags.length,
itemBuilder: (context, index) {
final tag = _scannedTags.values.elementAt(index);
return ListTile(
leading: CircleAvatar(
child: Text('${tag.count}'),
),
title: Text(
tag.tagId,
style:
const TextStyle(fontFamily: 'monospace'),
),
subtitle: Row(
children: [
Text('RSSI: ${tag.rssi} dBm'),
if (tag.locationInfo != null) ...[
const SizedBox(width: 16),
Text(
'Distance: ${tag.locationInfo!.toStringAsFixed(2)}'),
],
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () =>
_showWriteDialog(tag.tagId),
tooltip: 'Write to tag',
),
IconButton(
icon: Icon(
Icons.location_searching,
color:
_isLocationing ? Colors.blue : null,
),
onPressed: () => _locateTag(tag.tagId),
tooltip: 'Locate tag',
),
],
),
);
},
),
),
],
),
),
),
],
),
);
}
}