How to read Data from USB CallerID deivce in Flutter

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:

  1. 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.

  2. Parse the number The raw stream usually contains control characters and formatting codes. Strip those out and extract a clean phone number string.

  3. Bridge to Flutter Use Platform Channel or Event Channel to pass the parsed phone number from the native layer to the Dart side.

  4. 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

  1. Scan devices by USB connection
  2. Connet the device
  3. Setup the thread for listening data from incoming call
  4. 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.