Last active
March 6, 2025 02:45
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
// } | |
// } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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