pigpio-dotnet + F# でRaspberry Pi 4の入出力制御

###きっかけ
昨年の10月中旬，職場（県立高校で週２日の非常勤講師です）で「暇そうだし，ラズパイしない？」と煽られ，秋月電子通商の「[Raspberry Piで学ぶ電子工作 パーツセット](https://akizukidenshi.com/catalog/g/gK-15351/)」を渡された。
（ラズパイは貰えないのか...）

80年代前半，学生時代の実習では[NECのTK-85](https://support.nec-lavie.jp/support/product/data/spec/cpu/b368-1.html)とIntel 8255(PPI)でI/Oをお勉強。
[AKI-80](https://akizukidenshi.com/catalog/g/gM-11643/)にハマった後はPIC歴が長く(25年くらい)，ラズパイどころかArduinoさえも触らずにいたのだが，雪国の冬は畑仕事もできなくなるのでチャレンジすることに。
（結局，この冬は除雪作業に時間が奪われた...）

早速，Amazonでラズパイ4(8GB) + ケース + Ｔ型拡張ボードを購入し，OSには絶妙のタイミングでリリースされたubuntu20.10を選択。
ubuntuはWSL2で少しかじっている程度。（お前，大丈夫か？と，自問自答しつつ...）

思いのほかスムーズに環境整備を終え，Python + 人気のライブラリ[pigpio](http://abyz.me.uk/rpi/pigpio/index.html)でLチカして動作確認。
勢いでラズパイに[.NET5をインストール](https://dotnet.microsoft.com/download/dotnet/5.0)して，pigpio-dotnetとF#でラズパイを動かしてみた次第。

素人が趣味の範囲で触ってみた経過と結果ですので，責任を負うことはできませんが，ちょくちょくお世話になるQiitaに投稿してみます。

###pigpio-dotnet と F&#x23;
[pigpio-dotnet](https://github.com/unosquare/pigpio-dotnet)は[Unosquare](https://www.unosquare.com/)社がpigpioを.NET対応にしたパッケージ。ちなみに，同社は[wiringpi-dotnet](https://github.com/unosquare/wiringpi-dotnet)も公開している。

F# インタラクティブ（dotnet fsi もしくは fsi.exe）は，Excel作業の際，VBA代わりに使うことが多い。あとは[IronPython](https://ironpython.net/)も便利。
fsiを使えば，Python同様に対話形式で作業できるし，何よりも，ビットデータやバイトデータを操作する際，データ型を意識して作業する方が安心できる。


###環境
本体：
　Raspberry Pi 4 Model B (8GB) made in UK element14製
OS他：
　ubuntu 20.10 Desktop
　dotnet 5.0.100 + F# 5.0

ラズパイの操作は，ホストPC（Windows10）上のVSCodeからSSH接続により行った。VSCodeには拡張機能としてIonide-fsharpを導入した。
あとは，VSCodeのエディタとターミナルをコピペ往復しながら動作確認。

全体像（食後のテーブルが作業場所　PC画面に部屋干しの洗濯物が...）
<img width="480" alt="ALL.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/48a67a8b-6efe-d4f0-6619-c423a33aec12.jpeg">
ブレッドボード（LEDは抵抗内蔵なので心配御無用）
<img width="480" alt="GPIO.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/0f79f89a-f62b-fb1e-8dbb-86b607ea6621.jpeg">
VSCodeでの作業と動作環境
![VSCode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/94acbeb1-7e24-615c-cc68-85f55ed0fcd2.png)

###動作試験
dotnet fsiを起動する際は`sudo`を忘れずに。忘れるとpigpioに叱られます。

```bash:bash
~$ sudo dotnet fsi
```
まずは，NuGetからpigpio-dotnetパッケージを読み込む。
fsiでは2つのセミコロン(;;)が入力された段階でコードを評価する。
セミコロンは最終行末に置いてもいいが，ここでは区切りを明確にするためセミコロンを分離して最終行に配置した。

```fsharp:F#&#x20;Interactive
#r "nuget: Unosquare.PiGpio"
open Unosquare.PiGpio
;;
```


パッケージの読み込みには，**結構時間がかかる。**（測ってはいないが，10秒程度？）


続いて，[pigpio-dotnetプロジェクトサイト](https://unosquare.github.io/pigpio-dotnet/index.html)にあるC#コード例をF#に書き換えてＬチカ。
例では無限ループだが，本機の場合 `ctrl-C` で中断するとfsiまで終わってしまう（~~今後の課題~~ → 回避策を本稿末尾に記載）ので10回点滅にしておく。

```fsharp:F#&#x20;Interactive
NativeMethods.Setup.GpioInitialise()
let pin = NativeEnums.SystemGpio.Bcm12    // ブレッドボードの都合でLEDをGPIO12に変更
let mode = NativeEnums.PinMode.Output
NativeMethods.IO.GpioSetMode(pin, mode)
for _ in [1..10] do
    NativeMethods.IO.GpioWrite(pin, true) |> ignore
    System.Threading.Thread.Sleep(500)
    NativeMethods.IO.GpioWrite(pin, false) |> ignore
    System.Threading.Thread.Sleep(500)
;;
```

pigpio-dotnetパッケージでは，本家に準拠したネイティブインターフェイスに加えて，"PiGpio.net Managed API"が追加され，非常に使い勝手がよい。
こちらも，Ｌチカの例が示されているが，試しに，`System.Threading.Thread.Sleep`をManaged APIのメソッド（`Board.Timing.SleepMilliseconds`）に置き換えて実行。
500は`uint32`で渡す必要があるので「500u」。
こちらも問題なく動作。

```fsharp:F#&#x20;Interactive
let pin = Board.Pins.[12]
for _ in [1..20] do
    pin.Value <- not pin.Value
    Board.Timing.SleepMilliseconds(500u)
;;
```

続いて，I2Cに取りかかる。以下，Managed APIを使用する。
今回は，パーツセットから[LCD(AQM0802)](https://akizukidenshi.com/catalog/g/gM-11753/)と[温度センサ(ADT7410)](https://akizukidenshi.com/catalog/g/gM-06675/)を選択。
配線が済んだら，`ScanI2cBus`でデバイスの接続を確認。
わかりやすくするため，戻り値を16進表記させる。

```fsharp:F#&#x20;Interactive
Board.Peripherals.ScanI2cBus(NativeEnums.I2cBusId.Bus1)
|> Array.map (fun d -> $"0x{d:X2}")
;;
```
戻り値を見ると２つのデバイスとも，正しいアドレスで認識されている。

```fsharp
val it : string [] = [|"0x3E"; "0x48"|]
```
0x3E → AQM0802　　　0x48 → ADT7410

LCDから動かしてみる。まずはデータシートの設定例どおりに初期化。
コマンドとデータそれぞれに１バイト送信関数を用意して，初期化コマンドを送信。

```fsharp:F#&#x20;Interactive
let aqm0802 = Board.Peripherals.OpenI2cDevice(0x3Euy)
let aqmSendCmd (cmd : byte) =
    aqm0802.Write(0x00uy, cmd)
    Board.Timing.SleepMicroseconds(30u)
let aqmSendData (data : byte) =
    aqm0802.Write(0x40uy, data)
    Board.Timing.SleepMicroseconds(30u)
let aqmInitCmd = [0x38uy; 0x39uy; 0x14uy; 0x70uy; 0x56uy; 0x6Cuy; 0x38uy; 0x0Cuy; 0x01uy]
aqmInitCmd.[0..5] |> List.iter aqmSendCmd
Board.Timing.SleepMilliseconds(200u)
aqmInitCmd.[6..8] |> List.iter aqmSendCmd
;;
```

文字データを送る。

```fsharp:F#&#x20;Interactive
"Oops!!"
|> Seq.map byte
|> Seq.iter aqmSendData
;;
```
１行目が無事表示されたので，２行目には半角カタカナを送ってみる。
ただし，.NET5ではデフォルトでShift_JISに非対応なので，`Encoding.GetEncoding`前に一手間必要。

```fsharp:F#&#x20;Interactive
open System.Text
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)    // 一手間
aqmSendCmd 0xC0uy    // ２行目に表示
"ｺﾘｬﾏｲｯﾀﾅ"
|> Encoding.GetEncoding("Shift_JIS").GetBytes
|> Array.iter aqmSendData
;;
```
<img width="480" alt="LCD.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/b4d8756b-51b3-460b-488c-12980c770f31.jpeg">

LCDの次は温度センサ。
ADT7410は初期設定で分解能13ビットだが，折角なので16ビットに設定。

```fsharp:F#&#x20;Interactive
let adt7410 = Board.Peripherals.OpenI2cDevice(0x48uy)
adt7410.Write(0x03uy, 0x80uy)
;;
```

ADT7410の温度データ(符号付き16ビット)は上位8ビット・下位８ビットの順でReadされるので，`ReadWord`メソッドを使うと上位下位が逆になってしまう。
よくある手順でバイト順序を戻し，温度計算と８桁右寄せ文字列に変換する関数を作る。

```fsharp:F#&#x20;Interactive
let adtToTempstr8 (raw16 : uint16) : string =
    let adtData16 = (raw16 <<< 8) ||| (raw16 >>> 8) |> int16
    $"{(float adtData16) * 0.0078125, 8:F3}"
;;
```

温度データを液晶表示。「℃」の「゜」を半角カナの半濁点で代用して表示する。
もちろん，この程度なら「`Encoding.GetEncoding`」を使わずに半角半濁点のコード(0xDF)を送るように作ってもかまわない。

```fsharp:F#&#x20;Interactive
aqmSendCmd 0x01uy    // 表示のクリア
"TEMP[ﾟC]"
|> Encoding.GetEncoding("Shift_JIS").GetBytes
|> Array.iter aqmSendData
aqmSendCmd 0xC0uy    // ２行目に表示
adt7410.ReadWord(0x00uy)
|> adtToTempstr8
|> Seq.map byte
|> Seq.iter aqmSendData
;;
```
無事，目的達成。ミッション終了。
<img width="480" alt="TEMP.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/1211bd4b-e6c7-bebc-bd1e-fb584dc727ad.jpeg">
事後，後始末をする必要があるかと思うが詳細は未検証。
状況に応じて `Board.Release` なり `NativeMethods.Setup.GpioTerminate` なりの手順を踏むべきだろう。

職場で煽られた結果，変な方向に延焼中...

###ctrl-C によるループ脱出
（2021.2.2 追加）
あらためてドキュメントを確認したら，`GpioSetSignalFunc`で`ctrl-C`をキャッチできることに気づく。（Gpio･･･で始まっていたのでスルーしていた...）
原始的だが，ループ継続のフラグを準備して`ctrl-C`（SIGINT）でフラグを変更する関数を呼び出すことで正常にループから脱出可能になった。
当然ながら，ループ脱出後もfsiにとどまっている間は，`ctrl-C`はこの関数を呼び出す仕事しかしない。

```fsharp:F#&#x20;Interactive
// 以下，ctrl-C でＬチカループを抜けるコード例
let mutable loopFlag = true
let callback = NativeTypes.PiGpioSignalDelegate(fun (_ : int) -> loopFlag <- false)
NativeMethods.Setup.GpioInitialise()
NativeMethods.Utilities.GpioSetSignalFunc(2u, callback)  // 2u = ctrl-C(SIGINT)
;;

let pin = Board.Pins.[12]
while loopFlag do
    pin.Value <- not pin.Value
    Board.Timing.SleepMilliseconds(500u)
pin.Value <- false
loopFlag <- true
;;
```

###参考
#####● [pigpio library](http://abyz.me.uk/rpi/pigpio/)
#####● [Raspberry Pi - libpigpio for .net](https://unosquare.github.io/pigpio-dotnet/index.html)
#####● [Namespace Unosquare.PiGpio](https://unosquare.github.io/pigpio-dotnet/api/Unosquare.PiGpio.html)