【Flutter】arduinoとUSBシリアル通信をしてみる

2024/03/19に公開

概要

Flutterのusb_serialパッケージを使用し、arduino uno R3とUSBシリアル通信を行い、送受信できることを確認します。

準備

  • Android端末(usb_serialパッケージはAndroid専用です)
  • arduino uno R3
  • USBケーブル(typeC-typeB)


        ※筆者は、typeA-typeC変換コネクタ(100均)を間に入れてます。

パッケージ

https://pub.dev/packages/usb_serial

Flutter

  1. ターミナルで下記を実行
 flutter pub add usb_serial
 flutter pub get
  1. AndroidManifest.xml
    android/app/src/main/AndroidManifest.xmlの<activity>に下記追記。
AndroidManifest.xml
<intent-filter>
    <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" />
  1. device_filter.xml
    android/app/src/res/xml/device_filter.xml
device_filter.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 0x0403 / 0x6001: FTDI FT232R UART -->
    <usb-device vendor-id="1027" product-id="24577" />

    <!-- 0x0403 / 0x6015: FTDI FT231X -->
    <usb-device vendor-id="1027" product-id="24597" />

    <!-- 0x2341 / Arduino -->
    <usb-device vendor-id="9025" />

    <!-- 0x16C0 / 0x0483: Teensyduino  -->
    <usb-device vendor-id="5824" product-id="1155" />

    <!-- 0x10C4 / 0xEA60: CP210x UART Bridge -->
    <usb-device vendor-id="4292" product-id="60000" />

    <!-- 0x067B / 0x2303: Prolific PL2303 -->
    <usb-device vendor-id="1659" product-id="8963" />

    <!-- 0x1366 / 0x0105: Segger JLink -->
    <usb-device vendor-id="4966" product-id="261" />

    <!-- 0x1366 / 0x0105: CH340 JLink -->
    <usb-device vendor-id="1A86" product-id="7523" />

</resources>
  1. main.dartへの実装
    usb_serialパッケージのサンプルコードをArduinoとの通信用に改変しています。おおよその画面構成は、サンプルコードのものを使用させて頂いてます。
main.dart
import 'dart:async';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:usb_serial/transaction.dart';
import 'package:usb_serial/usb_serial.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'UsbSerial通信サンプル',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: UsbSerialSample(title: 'UsbSerial通信サンプル'),
    );
  }
}

class UsbSerialSample extends StatefulWidget {
  const UsbSerialSample({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<UsbSerialSample> createState() => _UsbSerialSampleState();
}

class _UsbSerialSampleState extends State<UsbSerialSample> {
  UsbPort? _port;
  String _status = "Idle";
  bool _init = false;
  List<Widget> _serialData = [];
  StreamSubscription<String>? _subscription;
  Transaction<String>? _transaction;
  UsbDevice? _device;

  final TextEditingController _controller = TextEditingController();
  String reply = "";
  
  Future<bool> _connectTo(device) async {
    print('connectTo() start.');
    _serialData.clear();

    if (_subscription != null) {
      _subscription!.cancel();
      _subscription = null;
    }

    if (_transaction != null) {
      _transaction!.dispose();
      _transaction = null;
    }

    if (_port != null) {
      _port!.close();
      _port = null;
    }

    if (device == null) {
      _device = null;
      setState(() {
        _status = "Disconnected";
      });
      print('connectTo() device=null return.');
      return true;
    }

    _port = await device.create();
    if (await (_port!.open()) != true) {
      setState(() {
        _status = "Failed to open port";
      });
      print('connectTo() failed to open port.');
      return false;
    }
    print('connectTo() device=$device.');
    _device = device;

    await _port!.setDTR(true);
    await _port!.setRTS(true);
    await _port!.setPortParameters(
        9600, UsbPort.DATABITS_8, UsbPort.STOPBITS_1, UsbPort.PARITY_NONE);

    _transaction = Transaction.stringTerminated(
        _port!.inputStream as Stream<Uint8List>, Uint8List.fromList([13, 10]));

    _subscription = _transaction!.stream.listen((String line) {
      setState(() {
        _serialData.add(Text(line));
        if (_serialData.length > 20) {
          _serialData.removeAt(0);
        }
      });

    });

    setState(() {
      _status = "Connected";
    });
    print('connectTo() end.');
    return true;
  }

  void _getPorts() async {
    print('getPorts() start.');
    List<UsbDevice> devices = await UsbSerial.listDevices();

    print('getPorts() devices=$devices.');
    if (devices.isEmpty) {
      _status = "No devices";
    } else {
      _status = "Searching";
    }

    Iterator<UsbDevice>? deviceIterator = devices.iterator;
    UsbDevice? searchDevice = null;
    bool searchRet = false;

    while (deviceIterator.moveNext()) {
      searchDevice = deviceIterator.current;


      // ArduinoのVendorIdだよ。
      if (searchDevice.vid == 9025) {
        searchRet = true;
        break;
      }
    }

    if (!searchRet) {
      _connectTo(null);
    } else {
      _connectTo(searchDevice);
    }

    setState(() {});
    print('getPorts() end.');
  }

  @override
  void initState() {
    super.initState();
    print('initState() start.');
    UsbSerial.usbEventStream!.listen((UsbEvent event) {
      _getPorts();
    });

    _getPorts();
    _init = true;
    setState(() {});
    print('initState() end.');
  }

  @override
  void dispose() {
    super.dispose();
    print('dispose() start.');
    _connectTo(null);
    print('dispose() end.');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          child: Column(children: <Widget>[
        Text(
            _init == false
                ? "No serial devices available"
                : "Available Serial Ports",
            style: Theme.of(context).textTheme.titleLarge),
        Text('Status: $_status\n'),

        Text('info: ${_port.toString()}\n'),
        ListTile(
          title: TextField(
            controller: _controller,
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: 'Text To Send',
            ),
          ),
          trailing: ElevatedButton(
            child: Text("Send"),
            onPressed: _port == null
                ? null
                : () async {
                    if (_port == null) {
                      return;
                    }
                    String data = _controller.text + "\r\n";
                    await _port!.write(Uint8List.fromList(data.codeUnits));
                    _controller.text = "";
                  },
          ),
        ),
        Text("Result Data", style: Theme.of(context).textTheme.titleLarge),
        ..._serialData,
      ])),
    );
  }
}
  1. 実行
    arduinoに接続しないまま実行すると下記ようにDisconnected状態でアプリが起動します。

  2. コード説明
    arduinoのベンダーID:9025と一致するデバイスを見つけたら、connectTo関数内で9600bps・8bitでUSBシリアル通信を開始し、受信したデータは"Result Data"の_serialDataに追記されます。

arduino

arduinoでエコーバックするだけの簡単なコードを記載します。

sketch.ino
int data = 0;

void setup() {
 // put your setup code here, to run once:
 Serial.begin(9600);  
}

void loop() {
 // put your main code here, to run repeatedly:
 if (Serial.available() > 0) {
   data = Serial.read();
   Serial.write(data);
   Serial.flush();
 }
}

接続して実行

テキストに文字入力後Sendボタンを押すと、"Result Data"にarduinoからエコーバックされた文字が記載されます。

おわりに

最後まで、記事を読んでもらいまして大変ありがとうございました。
android studio環境でUSBシリアル通信(usb serial for androidパッケージ)を使用し開発を行ったことがあったので、最近興味のあるflutterでも簡単にできたらいいなというのが今回の記事の始まりでした。エコーバックでの通信のやり取りでは特に問題もなく使用できていました。ただ、連続通信をやり続けた場合や、ハブを介した場合どうなるかまでは検証していないのでどこまでこのパッケージが信頼できるかは今のところ未知数ですのであしからず…
話はかわりますがandroid開発案件でUSBシリアル通信のテストするときに、arduinoは簡単にUSBシリアル通信できテスト信号をやり取りできたので、arduinoをお勧めしておきます。また、【Flutter】loggingで自由な表示形式でログを保存するの記事で、USBシリアル通信のデータを通信トレースしていますので、FlutterでarduinoとUSBシリアル通信をさせようと思っている方や興味のある方は見て頂けるとありがたいです。

参考

https://pub.dev/packages/usb_serial
http://independence-sys.net/main/?p=5421
https://www.arduino.cc/

Discussion