Skip to content

Instantly share code, notes, and snippets.

@badrazizi
Last active March 6, 2025 02:45
Show Gist options
  • Save badrazizi/b4f2bf44c92814c174ff723aaae89472 to your computer and use it in GitHub Desktop.
Save badrazizi/b4f2bf44c92814c174ff723aaae89472 to your computer and use it in GitHub Desktop.
An implementation of a communication protocol for message exchange between a POS payment terminal (e.g., A920 Pro) and an Electronic Cash Register (ECR) over USB
import 'dart:typed_data';
import 'dart:async';
import 'package:quick_usb/quick_usb.dart';
import 'utils.dart';
typedef Listener = void Function(Uint8List msg);
enum ReturnCode {
success,
notFound,
failedToOpen,
noConfiguration,
failedToClaimInterface,
deviceIsNotInitialized,
noINEndpoint,
noOUTEndpoint,
failedToSendMessage,
}
class PosEcrUsbCommunication {
UsbDevice? _device;
UsbInterface? _interface;
UsbEndpoint? _endpointIn;
UsbEndpoint? _endpointOut;
final List<Listener> _listeners = [];
/// Initialize USB and get device list
Future<ReturnCode> initialize(String identifier) async {
await QuickUsb.init();
List<UsbDevice> devices = await QuickUsb.getDeviceList();
if (devices.isEmpty) {
debugPrint("No USB devices found");
return ReturnCode.notFound;
}
for (UsbDevice device in devices) {
if (device.identifier == identifier) {
_device = device;
break;
}
}
if (_device == null) {
debugPrint("No USB device found with identifier: $identifier");
return ReturnCode.notFound;
}
debugPrint("POS Terminal found: ${_device?.identifier}");
return ReturnCode.success;
}
/// Connects to the POS terminal via USB
Future<ReturnCode> connectToPosTerminal() async {
if (_device == null) return ReturnCode.deviceIsNotInitialized;
bool opened = await QuickUsb.openDevice(_device!);
if (!opened) {
debugPrint("Failed to open USB device");
return ReturnCode.failedToOpen;
}
UsbConfiguration config = await QuickUsb.getConfiguration(0);
if (config.interfaces.isEmpty) {
debugPrint("No valid USB configuration found.");
return ReturnCode.noConfiguration;
}
_interface = config.interfaces.first;
bool claimed = await QuickUsb.claimInterface(_interface!);
if (!claimed) {
debugPrint("Failed to claim USB interface");
return ReturnCode.failedToClaimInterface;
}
for (UsbEndpoint endpoint in _interface!.endpoints) {
if (endpoint.direction == UsbEndpoint.DIRECTION_IN) {
_endpointIn = endpoint;
} else if (endpoint.direction == UsbEndpoint.DIRECTION_OUT) {
_endpointOut = endpoint;
}
}
if (_endpointIn == null) {
debugPrint("No valid USB IN endpoints found");
return ReturnCode.noINEndpoint;
}
if (_endpointOut == null) {
debugPrint("No valid USB OUT endpoints found");
return ReturnCode.noOUTEndpoint;
}
debugPrint("Connected to POS Terminal");
return ReturnCode.success;
}
/// Registers a listener for incoming messages
void registerListener(Listener listener) {
_listeners.add(listener);
}
/// Sends a message to the POS terminal with retries
Future<ReturnCode> sendMessage(Uint8List message, {int retries = 3}) async {
if (_endpointOut == null) {
debugPrint("No USB connection established");
return ReturnCode.noOUTEndpoint;
}
for (int attempt = 1; attempt <= retries; attempt++) {
int sentBytes = await QuickUsb.bulkTransferOut(_endpointOut!, message);
if (sentBytes > 0) {
debugPrint("Sent $sentBytes bytes (Attempt $attempt)");
_receiveMessage();
return ReturnCode.success;
} else {
debugPrint("Send attempt $attempt failed, retrying...");
}
}
debugPrint("Failed to send message after $retries attempts");
return ReturnCode.failedToSendMessage;
}
/// Receives a response message from the POS terminal with retries
Future<void> _receiveMessage({int length = 64, int retries = 3, Duration timeout = const Duration(seconds: 15)}) async {
if (_endpointIn == null) {
debugPrint("No USB connection established");
for (Listener listener in _listeners) {
listener(Uint8List(0));
}
return;
}
for (int attempt = 1; attempt <= retries; attempt++) {
try {
Uint8List response = await QuickUsb.bulkTransferIn(_endpointIn!, length);
debugPrint("Received response (Attempt $attempt)");
for (Listener listener in _listeners) {
listener(response);
}
return;
} catch (e) {
debugPrint("Receive attempt $attempt failed: $e, retrying...");
}
}
for (Listener listener in _listeners) {
listener(Uint8List(0));
}
return;
}
/// Disconnects from the POS terminal
Future<void> disconnect() async {
if (_interface != null) {
await QuickUsb.releaseInterface(_interface!);
}
await QuickUsb.closeDevice();
await QuickUsb.exit();
debugPrint("Disconnected from POS Terminal");
}
/// Constructs a message with STX + Data + ETX + LRC
static Uint8List buildMessage(String data) {
const int STX = 0x02;
const int ETX = 0x03;
Uint8List dataBytes = Uint8List.fromList(data.codeUnits);
int lrc = _calculateLRC(dataBytes);
return Uint8List.fromList([STX, ...dataBytes, ETX, lrc]);
}
/// Parses received messages and validates LRC
static String? parseMessage(Uint8List receivedData) {
if (receivedData.length < 4) return null;
int lrcReceived = receivedData.last;
Uint8List payload = receivedData.sublist(1, receivedData.length - 2);
int calculatedLRC = _calculateLRC(payload);
return (lrcReceived == calculatedLRC) ? String.fromCharCodes(payload) : null;
}
/// Calculates the Longitudinal Redundancy Check (LRC)
static int _calculateLRC(Uint8List data) {
int lrc = 0;
for (int byte in data) {
lrc ^= byte;
}
return lrc;
}
}
// void test() async {
// final posEcr = PosEcrUsbCommunication();
// if (await posEcr.initialize("") == ReturnCode.success && await posEcr.connectToPosTerminal() == ReturnCode.success) {
// // Register a listener for incoming messages
// posEcr.registerListener((message) {
// // Receive response
// String? parsedMessage = PosEcrUsbCommunication.parseMessage(message);
// if (parsedMessage != null) {
// debugPrint("Received Message: $parsedMessage");
// }
// // Disconnect
// posEcr.disconnect();
// });
// // Example: Send a message
// Uint8List message = PosEcrUsbCommunication.buildMessage("SALE|100.00|SR");
// await posEcr.sendMessage(message);
// }
// }
import 'package:flutter/foundation.dart';
void debugPrint(String message) {
if (kDebugMode) {
print(message);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment