Intro
In the restaurant ordering scenario, phone orders are still quite common (though to be honest, I personally think this approach feels pretty traditional, even a bit outdated). When a restaurant takes a call, the staff usually has to ask for the phone number, address, and name, then manually type everything into the system. This process is not only slow but also prone to mistakes.
If we could recognize the phone number right when the call comes in, and directly match it with the customer’s past information (like delivery address or frequently ordered dishes), we could save a lot of repetitive work. That’s exactly what I’m trying to do here: integrate a CallerID device into a Flutter app. When a customer calls, the system instantly recognizes the number, and if the customer already exists in the database, their info pops up automatically—no manual input needed.
CallerID is basically a small hardware device that connects to the telephone line. When a call comes in, it captures the number and sends the data to the computer via USB or serial port. When buying such a device, you need to check what platforms it supports. For example, the Artech110 only works on Windows, so I ended up using the CID EASY device for my development.
Here’s the catch: Flutter is essentially a cross-platform UI framework, and it can’t directly talk to low-level hardware like this. A CallerID device continuously pushes out USB/serial data streams, which Dart simply can’t access. I couldn’t find any ready-made Flutter plugin for this either. So, the only option was to implement it myself: read the raw USB data, parse it into a plain phone number, and then pass it over to Flutter using a Platform Channel or Event Channel. Once Flutter receives the number, it can check the database, and if it’s an existing customer, show their info right away.
The Flow: How to realise
To make this work in practice, the overall flow looks like this:
-
Capture the raw data On the native side (Windows/Linux/macOS/Android), read the raw stream from the CallerID device via USB or serial port.
-
Parse the number The raw stream usually contains control characters and formatting codes. Strip those out and extract a clean phone number string.
-
Bridge to Flutter Use Platform Channel or Event Channel to pass the parsed phone number from the native layer to the Dart side.
-
Your Own logic & Display customer info Once Flutter receives the number, query the local or remote database to check if this number belongs to an existing customer. If the customer exists, show their saved information (address, past orders, etc.) right away, skipping manual entry. If not, proceed with normal input.
Implementation
- Scan devices by USB connection
- Connet the device
- Setup the thread for listening data from incoming call
- Catch the Data from USB, pass it to your own flutter app
Plugin
Here is the project Structure.
This is the main entry — flutter_callerid.dart, It usually exports the public-facing API that app developers will us
flutter_callerid_method_channel.dart — provides the default implementation (talks to native).
flutter_callerid_platform_interface.dart – defines the abstract interface.
Set up an EventChannel to receive streamed data
In this step, need to use a native USB library on each platform to scan and open the CallerID device, then start a background listener that reads the raw bytes continuously. The native layer pushes the parsed phone number upstream via an EventChannel, so Flutter can subscribe to the stream and react in real time.
What happens under the hood (high level):
- Use the platform’s native USB package to enumerate devices and select the CallerID.
- Open the device and start a background thread to read the incoming data.
- Parse the raw stream (strip control codes → get a clean number).
- Emit each parsed number through the EventChannel → Flutter listens and updates the UI / queries the DB.
1<!-- devices_service.dart -->
2 Future<void> _getUSBDevices() async {
3 try {
4 final devices = await FlutterCalleridPlatform.instance.startUsbScan();
5 List<DeviceModel> usbPrinters = [];
6 for (var map in devices) {
7 final device = DeviceModel(
8 vendorId: map['vendorId'].toString(),
9 productId: map['productId'].toString(),
10 name: map['name'],
11 connectionType: ConnectionType.USB,
12 address: map['vendorId'].toString(),
13 ....
14 );
15 usbPrinters.add(device);
16 }
17
18 _devices.addAll(usbPrinters);
19
20 // Start listening to USB events
21 _usbSubscription = _deviceEventChannel.receiveBroadcastStream().listen((
22 event,
23 ) {
24 final map = Map<String, dynamic>.from(event);
25 _updateOrAddPrinter(
26 DeviceModel(
27 vendorId: map['vendorId'].toString(),
28 productId: map['productId'].toString(),
29 name: map['name'],
30 connectionType: ConnectionType.USB,
31 address: map['vendorId'].toString(),
32 ...
33 ),
34 );
35 });
36 // remove Duplicate deivces
37 _sortDevices();
38 } catch (e) {
39 log("$e [USB Connection]");
40 }
41 }
FlutterCalleridPlatform.instance.startUsbScan();
method need to define in flutter_callerid_platform_interface.dart
flutter_callerid_method_channel.dart
and flutter_callerid.dart
1
2
3 // flutter_callerid_platform_interface.dart
4
5 Future<dynamic> startUsbScan() {
6 throw UnimplementedError('startUsbScan() has not been implemented.');
7 }
8
9 // flutter_callerid_method_channel.dart
10 @override
11 Future<dynamic> startUsbScan() async {
12 return await methodChannel.invokeMethod('getAvailableDevices');
13 }
14
15
16 // flutter_callerid.dart, you can ignore cloudPrinterNum and androidUsesFineLocation, this is for my printer scanning
17
18 Future<void> getDevices({
19 List<ConnectionType> connectionTypes = const [ConnectionType.USB],
20 bool androidUsesFineLocation = false,
21 int cloudPrinterNum = 1,
22 }) async {
23 DevicesService().getDevices(
24 connectionTypes: connectionTypes,
25 androidUsesFineLocation: androidUsesFineLocation,
26 cloudPrinterNum: cloudPrinterNum,
27 );
28 }
In the Android side, define method in onMethodCall to get usb devices
1 @Override
2 public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
3 switch (call.method) {
4 case "getAvailableDevices":
5 result.success(flutterCallerIdMethod.getUsbDevicesList());
6 break;
7 ....
8 }
9 }
Connet the device
1
2
3 // flutter_callerid_platform_interface.dart
4 Future<bool> connectToHidDevice(String vid, String pid) {
5 throw UnimplementedError('connectToHidDevice() has not been implemented.');
6 }
7
8 // flutter_callerid_method_channel.dart
9 @override
10 Future<bool> connectToHidDevice(String vid, String pid) async {
11 return await methodChannel.invokeMethod('connectToHidDevice', {'vendorId': vid, 'productId': pid});
12 }
13
14 // flutter_callerid.dart, callerID is HID device
15 /// Connect to a specific HID device by device ID
16 Future<bool> connectToHidDevice(DeviceModel device) async {
17 return await DevicesService().connect(device);
18 }
In the Android side, define method in onMethodCall to connect HID device
1 @Override
2 public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
3 switch (call.method) {
4 case "connectToHidDevice": {
5 String vendorId = call.argument("vendorId");
6 String productId = call.argument("productId");
7 flutterCallerIdMethod.connect(vendorId, productId);
8 result.success(false);
9 break;
10 }
11 ....
12 }
13 }
FlutterCallerIdMethod.java Implementation here. Usb device will pop the syetem connetion dialog when connect in the first time , then it will detect automatically
1
2 // Connect using VendorId and ProductId
3 public void connect(String vendorId, String productId) {
4 connectionVendorId = vendorId;
5 connectionProductId = productId;
6 UsbManager m = (UsbManager) context.getSystemService(Context.USB_SERVICE);
7 UsbDevice device = findDevice(m, vendorId, productId);
8
9 if (device == null) {
10 AppLogger.d(TAG, "when connect but Device not found.");
11 return;
12 }
13
14 if (!m.hasPermission(device)) {
15 AppLogger.d(TAG, "Requesting permission for device...");
16 PendingIntent permissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE);
17 m.requestPermission(device, permissionIntent);
18 } else {
19 AppLogger.d(TAG, "Permission already granted. Proceeding.");
20 sendDevice(device, false); // Proceed directly if permission exists
21 }
22 }
Setup the thread for listening data from incoming call
Connect the USB device, then launch a background listener thread to read incoming data.
1
2Class xx{
3 UsbDevice listeningDevice = null;
4
5 // Method to start listening on the USB device for CallerID data
6
7 public void startListening(String vendorId, String productId) {
8 AppLogger.d(TAG, "Attempting to connect to device...");
9
10 UsbManager m = (UsbManager) context.getSystemService(Context.USB_SERVICE);
11 UsbDevice currentDevice = null;
12 for (UsbDevice device : m.getDeviceList().values()) {
13 if (String.valueOf(device.getVendorId()).equals(vendorId) && String.valueOf(device.getProductId()).equals(productId)) {
14 currentDevice = device;
15 break;
16 }
17 }
18
19 if (currentDevice == null) {
20 AppLogger.e(TAG, "No connected device.");
21 return;
22 }
23 if (!m.hasPermission(currentDevice)) {
24 AppLogger.e(TAG, "No permission for device. Please request it via broadcast.");
25 return;
26 }
27 listeningDevice = currentDevice;
28 UsbInterface intf = currentDevice.getInterface(0);
29 String deviceType = getDeviceType(intf);
30 AppLogger.d("USB", deviceType);
31
32 connection = m.openDevice(currentDevice);
33 if (connection == null) {
34 AppLogger.e(TAG, "Failed to open or claim interface.");
35 return;
36 }
37
38 mIntf = currentDevice.getInterface(0);
39 if (!connection.claimInterface(mIntf, true)) {
40 AppLogger.e(TAG, "Failed to claim interface.");
41 return;
42 }
43
44 AppLogger.d(TAG, " Interface Class: " + mIntf.getInterfaceClass());
45
46 // Dynamically pick endpoints by direction
47 for (int i = 0; i < mIntf.getEndpointCount(); i++) {
48 UsbEndpoint ep = mIntf.getEndpoint(i);
49 if (ep.getDirection() == UsbConstants.USB_DIR_IN) rEndpoint = ep;
50 else if (ep.getDirection() == UsbConstants.USB_DIR_OUT) wEndpoint = ep;
51 AppLogger.d(TAG, "Endpoint #" + i + " type=" + ep.getType() + ", direction=" + (ep.getDirection() == UsbConstants.USB_DIR_IN ? "IN" : "OUT") + ", address=" + ep.getAddress() + ", maxPacketSize=" + ep.getMaxPacketSize());
52 }
53
54
55 if (rEndpoint == null) {
56 AppLogger.e(TAG, "No readable endpoint found.");
57 return;
58 }
59
60 AppLogger.d(TAG, "Claimed interface and endpoints. Starting read loop...");
61 sendData("AT+VCID=1\\r");
62 readThread = new Thread(this::readLoop);
63 readThread.start();
64 reading = true;
65 }
66
67
68
69 // Thread
70 private void readLoop() {
71 byte[] buffer = new byte[64];
72
73 while (reading) {
74 int len = connection.bulkTransfer(rEndpoint, buffer, buffer.length, TIMEOUT);
75 if (len > 0) {
76 analyzePackage(buffer);
77 } else if (len == -1) {
78 AppLogger.w(TAG, "No data or timeout.");
79 }
80 Sleep(SLEEP);
81 }
82 }
83
84
85 // Parse data
86
87 private String sDateTime = "";
88 private String sCaller = "";
89 private String sCallee = "";
90 private String sOther = "";
91 private char sPort = 0;
92
93 private void analyzePackage(byte[] bytes) {
94 try {
95 final String strPackage = composeString(bytes);
96
97 if (strPackage.contains("ENQ") ||strPackage.contains("ETB")){
98 sendData(ACK);
99 }
100 else {
101 sendData(DCK);
102 if (testCliPackage(bytes)) {
103 //Pass data to flutter
104 Map<String, Object> callInfo = new HashMap<>();
105 callInfo.put("caller", sCaller);
106 callInfo.put("callee", sCallee);
107 callInfo.put("datetime", sDateTime);
108 callInfo.put("port", String.valueOf(sPort));
109 if (callerIdEventSink != null)
110 mainHandler.post(() -> callerIdEventSink.success(callInfo));
111
112
113 }
114 }
115 } catch (Exception e) {
116 }
117 }
118
119 private boolean testCliPackage(byte[] Package) {
120 boolean res = false;
121 try {
122 byte[] portNames = {'A', 'B', 'C', 'D', 'S'};
123 int[] pckTypes = {0x04, 0x80};
124
125 byte pPort = Package[0];
126 int pType = Math.abs((int) Package[1]);
127 int pLen = Math.abs((int) Package[2]);
128
129 if ((pLen > 0) && (pLen < 65) && Arrays.binarySearch(portNames, pPort) >= 0 && Arrays.binarySearch(pckTypes, pType) >= 0) {
130 if (pType == 0x80) {
131 res = parseMDMF(Package);
132 }
133 if (pType == 0x04) {
134 res = parseSDMF(Package);
135 }
136 }
137 } catch (Exception e) {
138 }
139 return res;
140 }
141
142 private boolean parseSDMF(byte[] Package) {
143 int packlength = 0;
144 char theChar = 0;
145
146 try {
147 packlength = Package[2] + 4;
148 sPort = (char) Package[0];
149 sCaller = "";
150 sCallee = "";
151 sDateTime = "";
152 sOther = "";
153
154 for (int i = 3; i <= packlength - 2; i++) {
155 if (i < Package.length) {
156 theChar = (char) Package[i];
157 if (i < 11) {
158 sDateTime = sDateTime + (char) Package[i];
159 } else {
160 sCaller = sCaller + theChar;
161 }
162 }
163 }
164 } catch (Exception e) {
165 }
166
167 return (!enableCheckDigitControl || testCheckDigit(Package));
168 }
169
170 private boolean parseMDMF(byte[] Package) {
171 int packlength = 0;
172 int datalength = 0;
173 char theChar = 0;
174 int i;
175
176 try {
177 packlength = Package[2] + 4;
178 sPort = (char) Package[0];
179 sCaller = "";
180 sCallee = "";
181 sDateTime = "";
182 sOther = "";
183
184 if (packlength > Package.length) {
185 packlength = Package.length;
186 }
187
188 if (packlength > 0) {
189 i = 3;
190 datalength = 0;
191 while (i < Package.length) {
192 if (Package[i] == 1) // Date Field
193 {
194 i++;
195 if (i < packlength - 1) {
196 datalength = Package[i];
197 if (datalength != 8) datalength = 8;
198 if (datalength == 8) {
199 while ((datalength > 0) && (datalength < packlength)) {
200 i++;
201 theChar = (char) Package[i];
202 sDateTime = sDateTime + theChar;
203 datalength--;
204 }
205 }
206 }
207 } else if (Package[i] == 2) // Number Field
208 {
209 i++;
210 if (i < packlength - 1) {
211 datalength = Package[i];
212 if (datalength > (packlength - i - 1))
213 datalength = (packlength - i - 1);
214 while ((datalength > 0) && (datalength < packlength)) {
215 i++;
216 theChar = (char) Package[i];
217 sCaller = sCaller + theChar;
218 datalength--;
219 }
220 }
221 } else if (Package[i] == 34) //Callee field
222 {
223 // Other Fields
224 i++;
225 if (i < packlength - 1) {
226 datalength = Package[i];
227 while ((datalength > 0) && (datalength < packlength)) {
228 i++;
229 theChar = (char) Package[i];
230 sCallee = sCallee + theChar;
231 datalength--;
232 }
233 }
234 } else {
235 // Other Fields
236 i++;
237 if (i < packlength - 1) {
238 datalength = Package[i];
239 while ((datalength > 0) && (datalength < packlength)) {
240 i++;
241 theChar = (char) Package[i];
242 sOther = sOther + theChar;
243 datalength--;
244 }
245 }
246 }
247 i++;
248 }
249 }
250 } catch (Exception e) {
251 }
252
253 return (!enableCheckDigitControl || testCheckDigit(Package));
254 }
255
256 private static final boolean enableCheckDigitControl = true;
257
258 private static boolean testCheckDigit(byte[] inputReport) {
259 int pLen = 0;
260 int cDigit = 123;
261 int pDigit = 0;
262
263 try {
264 pLen = inputReport[2] + 3;
265 cDigit = inputReport[pLen] & 255;
266 for (int i = 1; i < pLen; i++)
267 pDigit = (pDigit + Math.abs((int) inputReport[i] & 255)) & 255;
268 pDigit = Math.abs((int) (0x100 - pDigit)) & 255;
269 } catch (Exception e) {
270 }
271
272 return pDigit == cDigit;
273 }
274
275 private String composeString(byte[] bytes) {
276 String strPackage = "";
277
278 try {
279 StringBuilder builder = new StringBuilder();
280 for (byte b : bytes) {
281 if (b > 0) {
282 char c = (char) b;
283 builder.append(c);
284 }
285 }
286 strPackage = builder.toString();
287 } catch (Exception e) {
288 }
289
290 return strPackage;
291 }
292
293
294 private void Sleep(int milliseconds) {
295 try {
296 Thread.sleep(milliseconds);
297 } catch (InterruptedException e) {
298 e.printStackTrace();
299 }
300 }
301}
Logic in Your flutter app
Connect the CallerID device → start the listener → receive the stream data → process it according to your business logic.
1 // Import package
2
3import 'package:flutter_callerid/model/usb_device_model.dart' as callerid;
4
5class YourClassName {
6
7 final _flutterCalleridPlugin = FlutterCallerid();
8 StreamSubscription<Map<String, dynamic>>? _callerIdStreamSubscription;
9 StreamSubscription<List<callerid.DeviceModel>>? _devicesStreamSubscription;
10
11 callerid.DeviceModel? _connectedCallerIdDevice;
12 List<callerid.ConnectionType> getScanConnectionType() {
13 return [
14 <!-- callerid.ConnectionType.BLE, -->
15 callerid.ConnectionType.USB,
16 <!-- callerid.ConnectionType.NETWORK, -->
17 ];
18 }
19 // Method Strat Scan devices
20 Future<void> startScan() async {
21 _devices = [];
22 await _devicesStreamSubscription?.cancel();
23 await _flutterCalleridPlugin.getDevices(
24 connectionTypes: getScanConnectionType());
25 _devicesStreamSubscription = _flutterCalleridPlugin.devicesStream.listen((List<callerid.DeviceModel> event) {
26 // Only keep devices with a non-empty name containing 'caller'
27 _devices = event
28 .where((element) =>
29 (element.name?.isNotEmpty ?? false) &&
30 ((element.name?.toLowerCase().contains('caller') ?? false)))
31 .toList();
32 _devices = _devices.where((element) => element.isRemove == null || element.isRemove == false).toList();
33 });
34 }
35
36
37 Future<void> onConnectedCallerId(callerid.DeviceModel findDevice) async {
38 bool res = await _flutterCalleridPlugin.connectToHidDevice(findDevice);
39 if (res) {
40 _connectedCallerIdDevice = findDevice;
41 await startListening();
42 }
43 }
44 Future<void> startListening() async {
45 _callerIdStreamSubscription?.cancel();
46
47 // stream
48 _callerIdStreamSubscription = _flutterCalleridPlugin.callerIdStream.listen((callInfo) async {
49 // Recive data from the plugin
50
51 // Your own logic to parse Map to Model , and search from DB
52 CallerIdModel callerIdModel = CallerIdModel.fromJson(callInfo);
53 if (callerIdModel.caller == null || (callerIdModel.caller ?? "").isEmpty) {
54 return;
55 }
56 YOUR_OWN_HANDLE_MTHOD(callerIdModel);
57
58
59 });
60 await _flutterCalleridPlugin.startListening(_connectedCallerIdDevice!);
61 }
62
63
64}
65
My logic is straightforward: as soon as the number comes in, I look it up in the database and show the result inside a draggable overlay card.
Repo Source Code
https://github.com/Amber916Young/flutter_callerID
This repo actually combines CallerID and printer support (USB/WiFi/BLE), which is why it covers quite a lot of things.
But the core idea is really simple:
- Connect the device over USB
- Start a thread to listen for incoming data from the landline
- Parse the raw stream
- Return the clean phone number back to Flutter
That’s the essential flow. Everything else is just implementation details that vary depending on your hardware and platform.