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ケーブルを抜き差ししてください。
ライブラリーとドキュメントの入手
ビルドに必要なライブラリー等は、こちらから入手して適宜展開してください。こちらも 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制御をするための情報となれば幸いです。