Integrasi Flutter dengan UHF Integrated Reader

Gambaran Umum

Salah satu metode komunikasi antara UHF Reader dan aplikasi Flutter adalah dengan memanfaatkan bahasa pemrograman Rust sebagai jembatan penghubung. Dalam hal ini, Flutter dapat memanggil kode yang ditulis dalam Rust, yang akan bertugas menangani interaksi dengan UHF Reader.

Integrasi Flutter dengan Rust dapat dilakukan dengan menggunakan FFI (Foreign Function Interface), yang memungkinkan Flutter (melalui Dart) memanggil fungsi-fungsi yang ditulis dalam Rust. Dengan pendekatan ini, aplikasi Flutter dapat berfungsi untuk membaca informasi tag UHF, mengirim perintah ke reader, serta menerima dan memproses data yang dikembalikan oleh reader, baik melalui komunikasi Serial Port atau TCP/IP, sesuai dengan kebutuhan aplikasi.

Pada tutorial kali ini akan menggunakan flutter_rust_bridge | Package, adalah salah satu Flutter package yang dapat mengenerate kode Dart untuk berinteraksi dengan kode Rust.

Tahap

Kode lebih lengkap, dapat di download pada link berikut: https://cloud.electron.id/s/86XnjWpQA3Pm4N6

Buat project baru

cargo install flutter_rust_bridge_codegen && flutter_rust_bridge_codegen create flutter_uhf_reader && cd flutter_uhf_reader

Jika open folder dengan Visual Studio Code, akan banyak folder asing di dalam, sederhananya kita hanya perlu melihat:

  • Folder lib/: Untuk kode Dart
  • Folder rust/: Untuk kode Rust

🦀 Kode Rust

Untuk berkomunikasi dengan reader via Serial Port, memerlukan dependencies serialport, tambahkan di file rust/Cargo.toml.

rust/src/api/transport.rs
use anyhow::{Context, Error};
use serialport::{available_ports, ClearBuffer, SerialPort};
use std::io::prelude::*;
use std::io::Read;
use std::net::TcpStream;
use std::sync::{Arc, Mutex};
use std::time::Duration;
 
type Result<T> = std::result::Result<T, Error>;
 
pub trait Transport: Send + Sync {
    fn read(&mut self, size: u16) -> Result<Vec<u8>>;
    fn write(&mut self, buffer: &Vec<u8>) -> Result<()>;
    fn clear_buffer(&mut self) -> Result<()>;
}
 
#[non_exhaustive]
pub struct SerialTransport {
    serial: Arc<Mutex<Box<dyn SerialPort + Send>>>,
 
    pub port: String,
    pub baud_rate: u32,
    pub timeout: u64,
}
 
impl Transport for SerialTransport {
    fn read(&mut self, size: u16) -> Result<Vec<u8>> {
        let mut serial = self.serial.lock().unwrap();
 
        let mut data: Vec<u8> = vec![0u8; size as usize];
        serial
            .read(data.as_mut_slice())
            .with_context(|| "Can't read buffer data.".to_string())?;
 
        Ok(data)
    }
 
    fn write(&mut self, buffer: &Vec<u8>) -> Result<()> {
        let mut serial = self.serial.lock().unwrap();
 
        Ok(serial
            .write_all(buffer)
            .with_context(|| "Can't write buffer.".to_string())?)
    }
 
    fn clear_buffer(&mut self) -> Result<()> {
        let serial = self.serial.lock().unwrap();
 
        Ok(serial
            .clear(ClearBuffer::All)
            .with_context(|| "Can't clear all buffer.".to_string())?)
    }
}
 
impl SerialTransport {
    pub fn new(port: String, baud_rate: u32, timeout: Option<u64>) -> Result<Self> {
        let port_clone: String = port.clone();
        let timeout: u64 = timeout.unwrap_or(3);
 
        let serial = serialport::new(port_clone, baud_rate)
            .timeout(Duration::from_secs(timeout))
            .open()
            .with_context(|| format!("Can't connect {}", port))?;
 
        Ok(SerialTransport {
            serial: Arc::new(Mutex::new(serial)),
            port,
            baud_rate,
            timeout,
        })
    }
 
    pub fn scan() -> Result<Vec<String>> {
        let mut ports: Vec<String> = vec![];
        for port in available_ports()? {
            // Windows
            if port.port_name.contains("COM") {
                ports.push(port.port_name);
            }
            // Linux
            else if port.port_name.contains("/dev/ttyUSB") {
                ports.push(port.port_name);
            }
        }
        Ok(ports)
    }
}
 
#[non_exhaustive]
pub struct TcpIpTransport {
    tcp_stream: TcpStream,
 
    pub ip_address: String,
    pub port: u16,
    pub timeout: u64,
}
 
impl Transport for TcpIpTransport {
    fn read(&mut self, size: u16) -> Result<Vec<u8>> {
        let mut data: Vec<u8> = vec![0u8; size as usize];
        self.tcp_stream
            .read(data.as_mut_slice())
            .with_context(|| "Can't read buffer data.".to_string())?;
 
        Ok(data)
    }
 
    fn write(&mut self, buffer: &Vec<u8>) -> Result<()> {
        Ok(self
            .tcp_stream
            .write_all(buffer)
            .with_context(|| "Can't write buffer.".to_string())?)
    }
 
    fn clear_buffer(&mut self) -> Result<()> {
        todo!()
    }
}
 
impl TcpIpTransport {
    pub fn new(ip_address: String, port: u16, timeout: Option<u64>) -> Result<Self> {
        let timeout: u64 = timeout.unwrap_or(3);
 
        let ip_address_port: String = format!("{}:{}", ip_address, port);
        let tcp_stream = TcpStream::connect(&ip_address_port)
            .with_context(|| format!("Can't connect {}", &ip_address_port))?;
 
        // Set timeout
        tcp_stream.set_read_timeout(Some(Duration::from_secs(timeout)))?;
        tcp_stream.set_write_timeout(Some(Duration::from_secs(timeout)))?;
 
        Ok(TcpIpTransport {
            tcp_stream,
            ip_address,
            port,
            timeout,
        })
    }
}
rust/src/api/mod.rs
pub mod simple;
pub mod transport; // Add transport
Run Command

flutter_rust_bridge_codegen generate

Command ini wajib di eksekusi setiap ada perubahan kode Rust.

Hasil kode generate bahasa Dart akan muncul di /lib/src/rust/api/transport.dart.

🐦️ Kode Dart (Flutter)

Kode Dart yang dibuat dapat disesuai kebutuhan, pada tutorial ini kami menggunakan Reader HW-VX6330K (HW-VX63 Series), sehingga kode Dart disesuaikan dengan dokumentasi protokol Reader HW-VX63 Series.

Jika menggunakan reader jenis lain, seperti EL-UHF-RC4 Series dapat disesuaikan class untuk pengiriman dan penerimaan data bytes berdasarkan format protokol reader tersebut.

lib/reader/command.dart
import 'dart:typed_data';
 
const int cmdInventory = 0x01;
const int cmdReadMemory = 0x02;
const int cmdWriteMemory = 0x03;
const int cmdSetPower = 0x2F;
 
class Command {
  final int command;
  final int readerAddress;
  late Uint8List data;
  late int frameLength;
  List<int> baseData = [];
 
  Command(this.command, {this.readerAddress = 0xFF, dynamic data}) {
    // Convert data to Uint8List
    if (data is int) {
      this.data = Uint8List.fromList([data]);
    } else if (data is Uint8List) {
      this.data = data;
    } else {
      this.data = Uint8List(0);
    }
 
    frameLength = 4 + this.data.length;
    baseData = [frameLength, readerAddress, command];
    baseData.addAll(this.data);
  }
 
  Uint8List serialize() {
    List<int> serialize = baseData;
 
    // CRC-16/MCRF4XX Checksum Calculation
    int value = 0xFFFF;
    for (int d in serialize) {
      value ^= d;
      for (int i = 0; i < 8; i++) {
        value = (value & 0x0001) != 0 ? (value >> 1) ^ 0x8408 : value >> 1;
      }
    }
 
    int crcMsb = value >> 8;
    int crcLsb = value & 0xFF;
 
    // Append CRC to serialized data
    serialize.add(crcLsb);
    serialize.add(crcMsb);
 
    return Uint8List.fromList(serialize);
  }
}
 
lib/reader/response.dart
import 'dart:typed_data';
 
class Response {
  Uint8List responseBytes;
  int length;
  int readerAddress;
  int command;
  int status;
  Uint8List data;
  Uint8List checksum;
 
  Response(this.responseBytes)
      : length = responseBytes[0],
        readerAddress = responseBytes[1],
        command = responseBytes[2],
        status = responseBytes[3],
        data = Uint8List.fromList(
            responseBytes.sublist(4, responseBytes.length - 2)),
        checksum =
            Uint8List.fromList(responseBytes.sublist(responseBytes.length - 2));
 
  @override
  String toString() {
    String returnValue = '';
    String value;
 
    value = '>>> START RESPONSE ================================';
    returnValue = '$returnValue\n$value';
 
    value = 'RESPONSE       >> ${hexReadable(responseBytes)}';
    returnValue = '$returnValue\n$value';
 
    value = 'READER ADDRESS >> ${hexReadable(readerAddress)}';
    returnValue = '$returnValue\n$value';
 
    value = 'COMMAND        >> ${hexReadable(command)}';
    returnValue = '$returnValue\n$value';
 
    value = 'STATUS         >> ${hexReadable(status)}';
    returnValue = '$returnValue\n$value';
 
    if (data.isNotEmpty) {
      value = 'DATA           >> ${hexReadable(data)}';
      returnValue = '$returnValue\n$value';
    }
 
    value = 'CHECKSUM       >> ${hexReadable(checksum)}';
    returnValue = '$returnValue\n$value';
 
    value = '>>> END RESPONSE   ================================';
    returnValue = '$returnValue\n$value';
 
    return returnValue.trim();
  }
}
 
String hexReadable(dynamic data, {String bytesSeparator = " "}) {
  if (data is int) {
    return data.toRadixString(16).toUpperCase().padLeft(2, '0');
  } else if (data is Uint8List) {
    return data
        .map((x) => x.toRadixString(16).toUpperCase().padLeft(2, '0'))
        .join(bytesSeparator);
  } else {
    throw ArgumentError('Invalid data type for hexReadable');
  }
}
 
lib/reader/reader.dart
import 'dart:typed_data';
import '../src/rust/api/transport.dart';
import 'command.dart';
import 'response.dart';
 
class Reader {
  final Transport transport;
 
  const Reader(this.transport);
 
  Future<Uint8List?> _readFrame() async {
    Uint8List lengthBytes = await transport.read(size: 1);
 
    if (lengthBytes.isEmpty) {
      return null;
    }
 
    int frameLength = lengthBytes[0];
    Uint8List data = Uint8List.fromList(
        lengthBytes + await transport.read(size: frameLength));
    return data;
  }
 
  Future<List<Uint8List>> inventoryAnswerMode() async {
    final Command command = Command(cmdInventory);
    transport.write(buffer: command.serialize());
 
    final Uint8List? rawResponse = await _readFrame();
    if (rawResponse == null) {
      throw Exception("Reader no response.");
    }
 
    final Response response = Response(rawResponse);
    Uint8List data = response.data;
 
    if (data.isEmpty) {
      return [];
    }
 
    List<Uint8List> tags = [];
 
    int tagCount = data[0];
 
    int n = 0;
    int pointer = 1;
    while (n < tagCount) {
      int tagLen = data[pointer];
      int tagDataStart = pointer + 1;
      int tagMainStart = tagDataStart;
      int tagMainEnd = tagMainStart + tagLen;
      int nextTagStart = tagMainEnd;
 
      final Uint8List tag = Uint8List.fromList(
          data.sublist(tagDataStart, tagMainStart) +
              data.sublist(tagMainStart, tagMainEnd) +
              data.sublist(tagMainEnd, nextTagStart));
 
      tags.add(tag);
 
      pointer = nextTagStart;
      n += 1;
    }
 
    return tags;
  }
}

Demo

Pada device Android dapat berjalan via TCP/IP, untuk terhubung melalui Serial Port (OTG), saat ini belum bisa dilakukan dikarenakan dependencies serialport belum support Android. Alternatif agar bisa berjalan gunakan library usb-serial-for-android dan pigeon.