見出し画像

Flutter on DesktopからGPIOを利用する

普段はSingle Board ComputerやMCUのGPIOを使ってアレコレするのですが、ちょっとしたことをする場合に、手元のWindows機でGPIOを利用したくなることがあります。
今回は、そんなことをFlutter on Desktop(Windows)で実現する場合の対応について書いていきたいと思います。


GPIOの生やし方

WindowsマシンでGPIOを利用するためには、FTDI社のUSBシリアル変換のICの中でBit-Bangモードを利用できるものを使うか、Microchip社のMCP2221Aを利用するのが簡便だと思います。
ここではAdafruit社が出している、FTDI社のFT-232Hを使ったブレークアウトボードを利用していきます。

FT-232Hを使うためのライブラリー

USBシリアル変換ICであるFT-232Hの利用方法としては、以下の二つがあります。

libftdi(とlibusb)の組み合わせも良いのですが、ライブラリーのビルドなどが不要であるFTDI社のD2XXを利用します。

ドライバーのインストール

FTDI社のサイトからドライバーを入手して、適宜インストールしてください。この記事を書いた時点では 2.12.36.4 が最新でした。

ドライバのインストールを終えて、USBケーブルでブレークアウトボードをつなぐと、以下のように「USB Serial Converter」として認識されます。

ブレークアウトボードが認識された状態

仮想COMポートとして機能させる場合

仮想COMポート(Virtual COM Port)として利用する場合は「USB Serial Converter」を右クリックして下記のチェックボックスを有効にし、FT-232HにつないでいるUSBケーブルを抜き差ししてください。

仮想COMポートを利用する場合

ライブラリーとドキュメントの入手

ビルドに必要なライブラリー等は、こちらから入手して適宜展開してください。こちらも 2.12.36.4 が最新でした。

  • ヘッダーファイル: ftd2xx.h

  • インポートライブラリ: ftd2xx.lib

  • DLL: ftd2xx.dll (ftd2xx64.dll を適宜リネームしてください)

この記事では細かいことにはふれませんので、ドキュメント(D2XX Programmer’s Guide)はこちらから入手して適宜参照してください。

コマンドでの動作確認

デバイスのリスティング

まずは、接続されているデバイスとして認識されるのかを下記のようなコードで確認します。

#include <cstdio>
#include <cstdlib>
#include <ftd2xx.h>

int
main(int argc, char** argv)
{
    FT_HANDLE handle;
    FT_STATUS status;
    DWORD number_of_devices;
    FT_DEVICE_LIST_INFO_NODE* node_ptr;

    status = FT_CreateDeviceInfoList(&number_of_devices);
    node_ptr = new FT_DEVICE_LIST_INFO_NODE[number_of_devices];

    status = FT_GetDeviceInfoList(node_ptr, &number_of_devices);
    for (int i = 0; i < number_of_devices; i++) {
        printf("Device: %d:\n", i);
        printf("  Flags:        0x%x\n", node_ptr[i].Flags);
        printf("  Type:         0x%x\n", node_ptr[i].Type);
        printf("  ID:           0x%x\n", node_ptr[i].ID);
        printf("  LocId:        0x%x\n", node_ptr[i].LocId);
        printf("  SerialNumber: %s\n", node_ptr[i].SerialNumber);
        printf("  Description:  %s\n", node_ptr[i].Description);
        printf("  ftHandle:     0x%x\n", node_ptr[i].ftHandle);
    }

    return 0;
}

FT-232Hをつないである手元の環境では、以下のような出力が得られました(SerialNumberは置き換えています)。

Device: 0:
  Flags:        0x2
  Type:         0x8
  ID:           0x4036014
  LocId:        0x222
  SerialNumber: xxxxxxxx
  Description:  FT232H
  ftHandle:     0x0

GPIOとしての動作

次は、GPIOとして利用できるかを確認してみます。

  • D7,6,5,4を出力、D3,2,1,0を入力として Bit-Bangモードに設定

  • FT_GetBitMode()で、D3~D0のデータを読み込み

  • 読み込んだD3~D0ピンの値をD7~D4に反映し

  • FT_Write()で、D7~D4に出力

#include <stdio.h>
#include <ftd2xx.h>
#include <windows.h>

int
main(int argc, char** argv)
{
    FT_HANDLE handle;
    BYTE pin = 0xf0;    // D7,6,5,4: output, D3,2,1,0: input

    FT_Open(0, &handle);
    FT_SetBaudRate(handle, FT_BAUD_115200);
    FT_SetBitMode(handle, pin, FT_BITMODE_ASYNC_BITBANG);

    while (1) {
        BYTE in, out;
        DWORD len;
        FT_GetBitMode(handle, &in);
        printf("in: 0x%02x\n", in);

        out = in << 4;
        FT_Write(handle, &out, 1, &len);

        Sleep(1000);
    }

    return 0;
}

D7~D4にLEDをつないでおき、D3~D0をHIGH, LOWに設定すれば、それに応じてD7~D4のLEDが点灯もしくは消灯します。

Flutter on Desktopからの利用方法

Windows機から使うだけでしたら、先ほどの簡単なコードを転用すれば終わりです。今回はせっかくなので、Flutterのプラグインを作成してFlutter on Desktop(Windows)から使ってみたいと思います。

FlutterとVisual Studioのバージョン

利用したバージョンなどは以下の通りですが、特殊なことはしていませんのでバージョンが異なっていても再現可能だと思います。

  • Flutter 3.10.5 • channel stable • https://github.com/flutter/flutter.git

  • Framework • revision 796c8ef792 (3 weeks ago) • 2023-06-13 15:51:02 -0700

  • Engine • revision 45f6e00911

  • Tools • Dart 3.0.5 • DevTools 2.23.1

  • Visual Studio Professional 2022 17.4.2

Pluginのひながた生成

Powershell なりコマンドプロンプトなりで、以下のように実行してプラグインのひながたを生成します。今回はftdi_gpioというプラグイン名としました。

$ flutter create --template=plugin --platforms windows ftdi_gpio

注意点

今回はflutterコマンドを利用してひながたを生成したので大丈夫ですが、
Platform側のwindows/ftdi_gpio_plugin.cppでインスタンスを生成する「MethodChannel」とDart側のlib/ftdi_gpio_method_channel.dartの「MethodChannel」のチャネルは同一である必要があります。

ヘッダー, インポートライブラリー

ftdi_gpio/windows 配下に、D2XX用のヘッダーファイル, インポートライブラリ, DLLをコピーしておいてください。

CMakeLists.txtの編集

Plugin生成とTest生成の二個所で、ftd2xx.lib をリンクするように target_link_directories の指定を追加し、target_link_libraries の方には末尾に ftd2xx を追加します。

# plugin 生成部
target_link_directories(${PLUGIN_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter_wrapper_plugin ftd2xx)

# Test 生成部
target_link_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin ftd2xx)

Platform側の実装

ヘッダーファイルである windows/ftdi_gpio.h にGPIOとしてのメンバー関数とインスタンス変数を追加します。

 private:
  void configBitMode(const int pin, std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
  void getBitMode(std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
  void writeBit(const int value, std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);

 private:
  FT_HANDLE ft_handle_;

実装部である windows/ftdi_gpio.cpp は、コンストラクタ, デストラクター, メソッド呼び出しのディスパッチをする部分を書き換えつつ、先ほど動作確認をしたのとほぼ同じ GPIO 用コードを追加します。

FtdiGpioPlugin::FtdiGpioPlugin() : ft_handle_(nullptr), pin_(0x00) {
  FT_Open(0, &ft_handle_);
}

FtdiGpioPlugin::~FtdiGpioPlugin() {
  FT_Close(ft_handle_);
}

void FtdiGpioPlugin::HandleMethodCall(
    const flutter::MethodCall<flutter::EncodableValue> &method_call,
    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
  if (method_call.method_name().compare("configBitMode") == 0) {
    const flutter::EncodableValue* args = method_call.arguments();
    int pin = std::get<int>(*args);
    configBitMode(pin, std::move(result));
  } else if (method_call.method_name().compare("getBitMode") == 0) {
    getBitMode(std::move(result));
  } else if (method_call.method_name().compare("writeBit") == 0) {
    const flutter::EncodableValue* args = method_call.arguments();
    int value = std::get<int>(*args);
    writeBit(value, std::move(result));
  } else {
    result->NotImplemented();
  }
}

void FtdiGpioPlugin::configBitMode(const int pin, std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
  FT_SetBaudRate(ft_handle_, FT_BAUD_115200);
  FT_SetBitMode(ft_handle_, static_cast<BYTE>(pin), FT_BITMODE_ASYNC_BITBANG);
  result->Success(nullptr);
}
void FtdiGpioPlugin::getBitMode(std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
  BYTE in;
  FT_GetBitMode(ft_handle_, &in);
  result->Success(flutter::EncodableValue(in));
}
void FtdiGpioPlugin::writeBit(const int value, std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
  BYTE out = static_cast<BYTE>(value);
  DWORD len;
  FT_Write(ft_handle_, &out, 1, &len);
  result->Success(nullptr);
}

Dart側の実装

Dart側は lib/ftdi_platform_interface.dart, lib/ftdi_gpio_method_channel.dart, lib/ftdi_gpio.dart の三つのファイルがあります。

まず、Platform側が提供するインターフェースを三つを追加します。

  Future<void> configBitMode(int pin) {
    throw UnimplementedError('configBitMode() has not been implemented.');
  }
  Future<int?> getBitMode() {
    throw UnimplementedError('getBitMode() has not been implemented.');
  }
  Future<void> writeBit(int value) {
    throw UnimplementedError('writeBit() has not been implemented.');
  }

次に、MethodChannelを経由してPlatform側を呼び出す部分である lib/ftdi_gpio_method_channel.dart に対応するコードを追加します。

  @override
  Future<void> configBitMode(int pin) async {
    final result = await methodChannel.invokeMethod<void>('configBitMode', pin);
    return result;
  }
  @override
  Future<int?> getBitMode() async {
    final result = await methodChannel.invokeMethod<int>('getBitMode');
    return result;
  }
  @override
  Future<void> writeBit(int value) async {
    final result = await methodChannel.invokeMethod<void>('writeBit', value);
    return result;
  }

最後に、外部から利用するためのインターフェースである lib/ftdi_gpio.dart に追加して、利用可能となります。

  Future<void> configBitMode(int pin) {
    return FtdiGpioPlatform.instance.configBitMode(pin);
  }
  Future<int?> getBitMode() {
    return FtdiGpioPlatform.instance.getBitMode();
  }
  Future<void> writeBit(int value) {
    return FtdiGpioPlatform.instance.writeBit(value);
  }

実際に使用してみる

ちょっと長くなってしまいますが、example/lib/main.dart に書き下した利用例です。
入力側はボタンを押下するたびに、読み出し値を表示します。出力側の方は、押下のたびに0xf0, 0x0fを交互に切り替えて出力します。

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:ftdi_gpio/ftdi_gpio.dart';

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

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool _configResult = false;
  int _outValue = 0xf0;
  int _inValue = 0x00;
  final _ftdiGpioPlugin = FtdiGpio();
  final _pinDefs = 0xf0;

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  Future<void> initPlatformState() async {
    bool configResult = true;
    try {
      await _ftdiGpioPlugin.configBitMode(_pinDefs);
    } on PlatformException {
      configResult = false;
    }
    if (!mounted) return;
    setState(() {
      _configResult = configResult;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              SizedBox(
                width: 200,
                height: 30,
                child: ElevatedButton(
                  child: Text('getBitMode: ' + _inValue.toRadixString(16)),
                  onPressed: !_configResult ? null : () async {
                    int v = await _ftdiGpioPlugin.getBitMode() ?? 0;
                    setState(() { _inValue = v; });
                  }
                )
              ),
              SizedBox(
                width: 200,
                height: 30,
                child: ElevatedButton(
                  child: Text('writeBit: ' + _outValue.toRadixString(16)),
                  onPressed: !_configResult ? null : () {
                    _ftdiGpioPlugin.writeBit(_outValue);
                    setState(() { _outValue = (~_outValue) & 0xff; });
                  }
                )
              ),
            ]
          )
        ),
      ),
    );
  }
}

最後に

コードとして提示していませんが、Platform側内部でFT_GetBitModeを呼び出すスレッドを生成し、そのスレッドからEventChannel経由でDart側に通知させるなどした方が、よりFlutterらしい使い方ができると思います。

今回もいろいろと端折りがちな記事となってしまいましたが、この記事がWindows機からGPIO制御をするための情報となれば幸いです。


この記事が気に入ったらサポートをしてみませんか?