【追試】シリアル通信で受信完了をできるだけ早く検知する方法 [Raspberry pi]

はじめに

数年前に ↓ こんな記事を書きました。

bamch0h.hatenablog.com

私のブログで一番見られている記事らしく、みんな意外とシリアル通信するんだなと驚いています。そんな私も折を見て見返すのですが、ブログを読むたびに「本当に早いの?この対応しなかったらどれだけの速度で通信して、この対応をしたらどれくらいの速度になって、差がどれくらいなの?」という疑問があり、ちゃんと測定してみることにしました。

測定環境

測定環境は ESP32-WROOM-32D の UART2 と Raspberry Pi 4B の UART2 を接続しました。

ESP32 Raspberry Pi 4
TXD(IO17) GPIO1(RXD)
RXD(IO16) GPIO0(TXD)
CTS(IO2) GPIO3(RTS)
RTS(IO15) GPIO2(CTS)

測定時間

測定時間は、ESP32から 100 バイト送信してラズパイ側で受信しきったら 1 バイト返してESP32がそれを受信するまでの時間を測定時間とします。

ESP32側のプログラムは Arduino IDE を使用して Serial.write() してから Serial.flush() した後で micro() でティックカウントを取得し、これを送信完了時の時間として記録します。次に Serial.avairable() が 1 になったタイミングで再度ティックカウントを取得し、この時間を受信完了の時間として記録し、これらの差分を計算することで測定時間とします。

これを 1000 回繰り返して、平均、最小、最大を計算します。

測定パターン

今回は以前のブログの検証となるので、検知手法をカーネル修正前と後で比べる必要があります。

カーネルを修正せずに受信完了を検知する方法としては「1. 十分な時間待つ」と「2. 受信バイト数を固定化する」の二つが考えられます。

1. 十分な時間待つ

受信時間までの待ち時間ですが、受信割り込みがかかるまでの時間を十分な時間としました。

ラズパイ4Bの受信割り込みバイト数はデフォルト16バイトです。通信設定は以下の設定としますので 16 (バイト) * 10 (ビット) * 1,000,000 (μs) / 500,000 (bps) = 320 (μs) がウェイト時間となります

通信設定
ボーレート 500,000
データ長 8
パリティ なし
ストップビット 1

2. 受信バイト数を固定化する

こちらは単純に受信したトータルバイト数が既定のバイト数に達した時点で受信完了とする方法です。

今回は 100バイトを送信したときの応答までの時間を計測することとしているので 100バイトが既定バイト数となります。

カーネルを修正版での計測

さて、次に以前のブログで紹介した方法での計測を・・・と思っていたのですが、実測途中で、この方法では受信完了を誤判定することがわかりました。

どのように誤判定するかというと、受信タイムアウトの発生数をカウントしておいて受信開始前と開始後の値を比較することで受信完了を検知していましたが、100バイト受信しきる前に受信タイムアウト割り込みが発生するケースがあり誤判定となる場合がありました。なぜ受信タイムアウトが受信途中で起こるのか、明確なことはわかりませんが、色々調査してみてある程度推測はできました。その推測を以下で説明します。

まず前提として、今回の測定環境では RTS/CTS ハードウェアフロー制御を行っています。というのも通信と同時に動画再生等の重い処理が動作していると UART の FIFO から内部バッファに吸い出す処理が追い付かなくなりオーバーランエラーが発生してしまうケースがあったためです。そしてフロー制御で送信が一旦止まるとその間に受信タイムアウトが発生してしまい予期せぬ受信タイムアウト割り込みが発生してしまっているのではないかと推測しています。

この理由により以前のブログに記載していた方法は使えないことが分かったので別の解法が必要になりました。詳細は 後述 するのですが、ザックリいうと、UART の FIFO が空かどうかをチェックして空であれば受信タイムアウト割り込み時間 (32 * 1000 * 1000 / 500000 = 64 (μs)) だけ待ってから再度空チェックをし、それでも空だった場合は受信が完了したものとみなすようにしました。

よって、今回は以下の3パターンを計測して比較することとしました。

  1. 受信割り込み時間待って判定するパターン
  2. 固定長で判定するパターン
  3. FIFOの空チェック後受信タイムアウト割り込み時間待って判定するパターン

計測結果

計測結果は以下の通りです。

① 受信割り込み時間待って判定
② 固定長で判定
③ 受信タイムアウト割り込み時間待って判定

ave 739.01 μs 165.87 μs 253.86 μs
min 525.00 μs 152.00 μs 236.00 μs
max 1037.00 μs 241.00 μs 531.00 μs

上記の通り、一番早く受信完了できるのは②の「固定長での判定」でした。次点で③、続いて①という結果になりました。

結果として私が考えていた方法は最速の方法ではなかったということになりますが、②の優位性をあえて述べるとするならば、受信するデータが固定長ではなく、受信データからデータサイズを取得できないような場合においては有効なアプローチなのではないかと思います。

グラフを見ると ① に不穏なパターンが見られますね・・・これについてはよくわからないです・・・

まとめ

以前にポストした「シリアル通信で受信完了をできるだけ早く検知する方法」を実際に検証しました。

結果として最速で検知する方法ではないことがわかりましたが、今までもやもやしていた部分が明確になったことはよかったと思いました。

また、もう一つ良かった点としては、最速の方法が固定長での判定だとわかったことです。固定長の判定はカーネルをいじることがないため実装のハードルがぐんと下がりました。今後のシリアル通信処理を書く時の指針にできると思いました。

あと、想像ではカーネルをいじらないともっと遅くなると思っていたので、それについてもいい方向に裏切られたという印象です。受信割り込み時間待つやり方でも最大で約1msで応答が返ってくるのは結構早いという印象です。 LinuxWindows 等の汎用OSはタスクスケジューリングの関係で数msの遅延が当たり前のように発生することが多いので、もしかするとラズパイのOS (Raspbian) が頑張ってチューニングされているのかもしれません。

ということで、追試の結果として、最速の方法は「固定長で判定する」ということでこのブログのまとめとさせていただきます。チャンチャン♪

おまけ

ここからは計測途中で色々わかったことや雑多なことをおまけとして記載したものです。興味のある方はご覧ください :)

動画を再生しながらの計測結果

ave 800.91 μs 340.23 μs 364.62 μs
min 490.00 μs 124.00 μs 194.00 μs
max 4258.00 μs 5806.00 μs 4433.00 μs

動画無しの時と比べると最大値がかなり上がっていますが、最小値が逆に早くなっています。これはなんでかはよくわかってません。

ラズパイのUART PL011のFIFOのサイズ

以前のブログでは PL011 の FIFO のサイズは 16バイトと記載していましたが、今回計測してみると受信割り込みが16バイト単位で掛かっていて、計算と合わないなーとずっと疑問だったんですが、ラズパイのCPUマニュアルを見ると FIFOサイズは 32バイトと記載がありました。なので、その半分の16バイトで受信割り込みがかかるのは正しい動作でした。

参考にしたマニュアルはこちら → https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf

計測に使用したソースコード

計測に使用したソースコード一式は以下のリポジトリにアップしています。ご興味ある方はご覧ください。

GitHub - bamchoh/rpi-serialport-test at blog_20240613

FIFOバッファの空チェックを用いた受信完了待ちについて

さて、今回③として計測したFIFOバッファの空チェックを用いた受信完了待ちの詳細です。

PL011 には FIFO バッファが空かどうかの状態を格納したレジスタがあります。これを ioctl 経由で取得できるようにすればユーザーランドで FIFOバッファの空チェックができるようになります。

ただ、空チェックだけだとタイミングによっては中間バッファにデータが取り残されている可能性もあるため、そちらも ioctl 経由で取得し前回値の差と送受信数が一致しているかもチェックすることで確実な受信完了チェックを行いました。以下がそのコードです。

 // ... 一部省略 ...

    struct serial_icounter_struct icount;
    __u32 rxtotal = 0;
    ioctl(fd, TIOCGICOUNT, &icount);
    rxtotal = icount.rx; // 中間バッファの受信バイト数を取得

    // waiting for receive any data
    int duration = 32 * 1000 * 1000 / atoi(b_optarg);
    printf("duration: %d\n", duration);
    int total = 0;
    int fifo_is_empty;
    while(1) {
        int len = read(fd, buf, sizeof(buf));
        if (len <= 0) {
            fprintf(stderr, "read error!!\n");
            break;
        }
        ioctl(fd, UIOGRXFE, &fifo_is_empty); // FIFOバッファが空かどうかのフラグ取得
        ioctl(fd, TIOCGICOUNT, &icount); // 中間バッファの情報も取得

        for(int i = 0; i < len;i++) {
            rxbuf[total+i] = buf[i];
        }
        total += len;

        if(fifo_is_empty) { 
            // FIFOバッファが空なら duration (64μs) だけスリープ
            usleep(duration);

            // 再度フラグと中間バッファの情報取得
            ioctl(fd, UIOGRXFE, &fifo_is_empty);
            ioctl(fd, TIOCGICOUNT, &icount);
        }

        // スリープから戻っても FIFOバッファが空で中間バッファも取りこぼしがないなら受信完了
        if(fifo_is_empty && (icount.rx - rxtotal) == total) {
            txbuf[0] = (unsigned char)total;
            write(fd, txbuf, 1);

            total = 0;
            rxtotal = icount.rx;
        }

    }

Codeer.Friendly で WPF の ContextMenu を取得する方法

Codeer.Friendly とは

Friendly は 株式会社 Codeer が開発した WindowsGUI を自動テストするためのライブラリです。

DLLインジェクションという方法で自動化するため、アプリケーションの内部のコードも操作でき、他の自動化ツールではできないような細やかな操作が可能となるため重宝しています。ただ、privateメソッドすら実行可能というある意味恐ろしいツールなので用法・用量を守って正しく使わないとえらいことになります。

Friendly の使い方については他の方のブログを見ていただくとして、今回は ContextMenu の取得で手間取ったので備忘録として記事を書きます。

ContextMenu の取得

ContextMenu を VisualTree() を使って取り出そうとして取り出せません。 Friendly では VisualTreeWithPopup() というのが別に用意されていてこちらを使えばContextMenuを取り出せます。 なぜ別々のメソッドになっているのかは謎ですが、これでいけます。

Popup を取得するときは VisualTree() でも VisualTreeWithPopup() でも取り出せるので好きなほうを使いましょう。

var process = Process.Start(@"C:\dev\source\repos\WpfApp3\WpfApp3\bin\Debug\net8.0-windows\WpfApp3.exe");
Thread.Sleep(1000);
var app = new WindowsAppFriend(process);
var shell = app.WaitForIdentifyFromTypeFullName("WpfApp3.MainWindow");
var contextMenus = shell.VisualTreeWithPopup().ByType("System.Windows.Controls.ContextMenu");
var contextMenu = contextMenus[0];
var menuitems = contextMenu.VisualTree().ByType("System.Windows.Controls.MenuItem");
var menuitem = new WPFMenuItem(menuitems[2]);
menuitem.EmulateClick();

動的にコンポーネントを切り替える方法

コンポーネントのオブジェクトの状態によって子コンポーネントを動的に切り替える。かつ、親コンポーネントのマウスイベントから子コンポーネントのメソッドを呼び出す。

useRecoil を使うのがミソか。

import {
  useEffect,
  useState,
  forwardRef,
  useRef,
  useImperativeHandle,
  Children,
} from "react";
import { useRecoilState } from "recoil";
import { flagState, positionState } from "./state.jsx";

const Klass1 = forwardRef((props, ref) => {
  const [position, setPosition] = useRecoilState(positionState);

  useImperativeHandle(ref, () => ({
    updatePosition,
  }));

  const updatePosition = (event) => {
    setPosition({
      x: event.clientX,
      y: event.clientY,
    });
  };

  return <>{props.children}</>;
});

const Klass2 = forwardRef((props, ref) => {
  const [position, setPosition] = useRecoilState(positionState);

  useImperativeHandle(ref, () => ({
    updatePosition,
  }));

  const updatePosition = (event) => {
    setPosition((prev) => {
      return {
        x: prev.x + 1,
        y: prev.y + 1,
      };
    });
  };

  return <>{props.children}</>;
});

const Factory = ({ flag, children, childRef }) => {
  if (flag) {
    return <Klass1 ref={childRef}>{children}</Klass1>;
  } else {
    return <Klass2 ref={childRef}>{children}</Klass2>;
  }
};

const MyComponents = () => {
  const [flag, setFlag] = useRecoilState(flagState);
  const [position, setPostion] = useRecoilState(positionState);

  const childRef = useRef();

  const style = {
    width: 120,
    height: 120,
    backgroundColor: "green",
  };

  return (
    <Factory flag={flag} childRef={childRef}>
      <div
        id="my_components"
        style={style}
        onMouseMove={(event) => childRef.current.updatePosition(event)}
      >
        <label>
          x={position.x}, y={position.y}
        </label>
      </div>
    </Factory>
  );
};

export default MyComponents;

WindowsでKernel#spawnで作った子プロセスを親プロセスが死ぬときに同時に死んでもらう

WindowsだとRubyのspawnメソッドでプロセスを作成すると親プロセスからは切り離されて、親プロセスが終了しても残り続けます。

これが便利な時もありますが、私のユースケースでは不便に働くことがありました。なので、今回は親子共々死んでもらいます。

以前に、「アプリケーションが終了するときに、子プロセスも終了させる方法」ということで、Go言語で実装した記事を投稿しました。

bamch0h.hatenablog.com

こちらの焼き増し記事となります。ご了承を。

Ruby コード

module Kernel32
    require 'fiddle'
    require 'fiddle/import'
    require 'fiddle/types'
    
    extend Fiddle::Importer
    dlload 'kernel32.dll'
    include Fiddle::Win32Types

    typealias 'ULONG_PTR', 'ULONG*'
    typealias 'LONGLONG', 'double'
    typealias 'SIZE_T', 'ULONG_PTR'
    typealias 'ULONGLONG', 'unsigned long long'
    typealias 'LPVOID', '*void'

    SecurityAttributes = ([
        "DWORD nLength",
        "LPVOID lpSecurityDescriptor",
        "BOOL bInheritHandle",
    ])

    typealias 'LPSECURITY_ATTRIBUTES', '*SecurityAttributes'

    LARGE_INTEGER = struct([
        "LONGLONG QuadPart"
    ])

    IoCounters = struct([
        "ULONGLONG ReadOperationCount",
        "ULONGLONG WriteOperationCount",
        "ULONGLONG OtherOperationCount",
        "ULONGLONG ReadTransferCount",
        "ULONGLONG WriteTransferCount",
        "ULONGLONG OtherTransferCount",
    ])

    JobObjectBasicLimitInformation = struct([
        { PerProcessUserTimeLimit: LARGE_INTEGER},
        { PerJobUserTimeLimit: LARGE_INTEGER},
        'DWORD LimitFlags',
        'SIZE_T MinimumWorkingSetSize',
        'SIZE_T MaximumWorkingSetSize',
        'DWORD ActiveProcessLimit',
        'ULONG_PTR Affinity',
        'DWORD PriorityClass',
        'DWORD SchedulingClass'
    ])

    JobObjectExtendedLimitInformation = struct([
        { BasicLimitInformation: JobObjectBasicLimitInformation},
        { IoInfo: IoCounters },
        'SIZE_T ProcessMemoryLimit',
        'SIZE_T JobMemoryLimit',
        'SIZE_T PeakProcessMemoryUsed',
        'SIZE_T PeakJobMemoryUsed',
    ])

    JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000

    extern 'HANDLE CreateJobObjectA(LPSECURITY_ATTRIBUTES, LPCSTR)'
    extern 'HANDLE OpenProcess(DWORD, BOOL, DWORD)'
    extern 'BOOL SetInformationJobObject(HANDLE, int, LPVOID, DWORD)'
    extern 'BOOL AssignProcessToJobObject(HANDLE, HANDLE)'
    extern 'BOOL CloseHandle(HANDLE)'
end

class JobObject
    PROCESS_SET_QUOTA = 0x0100
    PROCESS_TERMINATE = 0x0001
    JobObjectExtendedLimitInformation = 9
    
    def initialize
        @job = Kernel32.CreateJobObjectA(0, 0)

        info = Kernel32::JobObjectExtendedLimitInformation.malloc
        
        info.BasicLimitInformation.LimitFlags = Kernel32::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
        
        Kernel32.SetInformationJobObject(
            @job,
            JobObjectExtendedLimitInformation,
            info,
            Fiddle::Importer.sizeof(Kernel32::JobObjectExtendedLimitInformation)
        )
    end

    def register(pid)
        hProcess = Kernel32.OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, 0, pid)
        Kernel32.AssignProcessToJobObject(@job, hProcess)    
        Kernel32.CloseHandle(hProcess)
    end
end

job_obj = JobObject.new
pids = []
3.times do |i|
    pid = spawn("ruby.exe -e 'loop { p #{i}; sleep 1}'")
    job_obj.register(pid)
end

sleep 5

上記コードを実行すると、rubyのプロセスが3つ起動します。これらプロセスは無限ループで動作しているので通常はCTRL-Cで殺すか、taskkill等で殺すかしないと終了しません。ですが今回は親プロセスが5秒後に終了すると同時に終了します。

苦労した点

Goの場合は、すでにWin32APIのライブラリがあり、それを使えば比較的簡単に実装できましたが、Rubyの場合はそのようなライブラリがなくAPIの定義からする必要がありました。

Rubyには fiddle という ffi を行うための標準ライブラリがあり、それを用いてAPI定義を作成していくのですが、如何せん fiddle の情報が少なく「こういった場合はどうやって定義するんだろう?」という状況が結構起こった感覚があります。

例えば、Windowsでは LONGLONGという型がありますが、定義は以下のようになっているようです。

#if !defined(_M_IX86)
 typedef __int64 LONGLONG; 
#else
 typedef double LONGLONG;
#endif

なので、今動かしているプラットフォームに依存しているということなのですが、これを Ruby で表現する方法がわかりませんでした。おそらく私の環境では double で定義しておけば問題ないだろう。ということで今回のケースでは LONGLONG は double で定義しました。

また、入れ子になっている構造体の定義もどのように定義すればいいか、公式のドキュメントからはわかりませんでした。(ちゃんと読み込んだら書いてあるのかもですが、私は見つけられませんでした)

じゃぁ、どうやって定義したかというと、GithubのPullRequestに例が載っていたのでその通りに実装して動くかどうか試して、動いたのでこれが正解かな?という感じで定義しました。

github.com

これはドキュメントにコントリビュートするチャンスかもしれませんね!!

制限事項

上記のコード例ははじめは notepad.exe で書いていたのですが、なぜか一つだけプロセスが残るという問題がありました。これははじめのメモ帳だけプロセスが二つ起動するため、pidが一つだけJobObjectに登録されないことが問題と考えていますが、それをどう解決できるのかがわからず今回での対応は保留とさせていただきました。どなたかわかる方いらっしゃったら教えてください!!

まとめ

親プロセスが死んだときに子プロセスも死ぬようにJobObjectに登録するRubyプログラムを書いてみました。Fiddle を使ったことがなかったので使い方等含めて勉強になったかなと思います。できればもう書きたくないですが、もし書くことがあればこの記事を参考にできると思うで、未来の自分へのメモ書きということでここに記しておきます。

以上!!

【読書感想ブログ】Go言語プログラミングエッセンス - mattn / 技術評論社

Go言語といえば mattnさん、mattnさんといえばGo言語。それくらいには mattnさんが書かれるGo言語の書籍には信頼がありますが、そんな mattnさんが新たに出版された書籍が「Go言語プログラミングエッセンス」です。しかも単著。これは買うしかないですよね。そして読むしかないですよね。なので読んでみた感想です。

はじめのほうには Go言語がなぜ作られたのか、どういう言語なのかというところ、または文法的なところが書かれていました。mattnさんはブログもされていて、たまにポストされるんですが、私でもわかるような書き口で書いてくださっていて助かるな~といつも思っているんですが、その書き口がこの書籍にも反映されていて、内容がスラスラ入ってくる感じになってます。

この書籍のいいところは、具体的なコードが多く書かれていて手を動かしながら理解できる点が良いと思いました。また、簡単なWebアプリケーションやCLI題材によく使われるパッケージの紹介等もされていてよかったです。

あとは、Go言語に限らないテクニックなんじゃないかな?と思う部分もあり、たとえば「推測するな計測せよ」なんていう思想はほかの言語でも通用しそうですよね。そういったプログラミングをする上での大事な考え方も学べて良い本だなぁ~と思いました。

個人的に今後に役立ちそうだなぁと思った部分の抜き書き

go get でバージョン指定

go get github.com/mattn/go-runewidth@v0.0.12 のようにするとバージョンを指定して go get できる

最新バージョンにバグがあったり、破壊的な変更がされていたりする時にこういう対応をするとよい

vendor ディレクト

go.mod と同じディレクトリにあるvendorというディレクトリに置かれたモジュールのソースコードは優先して参照されます

以下のコマンドを実行すると、go.mod と go.sum に書かれたモジュールが一括で vendor ディレクトリにダウンロードできます

$ go mod vendor

おまじない

おまじないとして次のようにインターフェースIの型を持つ変数_に型fooのポインタ型nilを代入しておくことで、わざわざコンパイルをしなくても、IDEがエラーを検知して間違いに気づくことができます

recoverの使いどころ

通常、意図しないpanicが起きうるということは、そのプログラムにはバグがあることを意味するため、不用意にrecoverを呼び出すべきではありません。プログラムは落ちるべくして落ちたほうがよく、それを止めるべきではありません。

Explicit is better than implicit, Simple is betetr than complex

明示的であることは暗黙的であることよりも優れている。単純であることは複雑であることよりも優れている

Goにおける日付時刻の書式

「1月2日3時4分5秒2006年」と連番になっており、おおよそ表4.1のように理解すると覚えやすくなっています。

書式 連番 意味
1 1
01 1 月のゼロ埋め
2 2
02 2 日のゼロ埋め
3 3
03 3 時のゼロ埋め
15 3 時のゼロ埋め(24時間表記)
4 4
04 4 分のゼロ埋め
5 5
05 5 秒のゼロ埋め
2006 6
+007 7 タイムゾーン
JST - タイムゾーン

timeパッケージに標準で用意されている書式

取り扱いづらそうに見えますが、多くの場合はtimeパッケージに標準で用意されている固定の書式名を使うことができます

書式名 フォーマット
Layout 01/02 03:04:05PM '06 -0700
ANSIC Mon Jan \_2 15:04:05 2006
UnixDate Mon Jan \_2 15:04:05 MST 2006
RubyDate Mon Jan 02 15:04:05 -0700 2006
RFC822 02 Jan 06 15:04 MST
RFC822Z 02 Jan 06 15:04 -0700
RFC850 Monday, 02-Jan-06 15:04:05 MST
RFC1123 Mon, 02 Jan 2006 15:04:05 MST
RFC1123Z Mon, 02 Jan 2006 15:04:05 -0700
RFC3339 2006-01-02T15:04:05Z07:00
RFC3339Nano 2006-01-02T15:04:05.999999999Z07:00
Kitchen 3:04PM
Stamp Jan \_2 15:04:05
StampMilli Jan \_2 15:04:05.000
StampMicro Jan \_2 15:04:05.000000
StampNano Jan \_2 15:04:05.000000000
DateTime 2006-01-02 15:04:05
DateOnly 2006-01-02
TimeOnly 15:04:05

time.ParseDuration

d, err := time.ParseDuration("3s")

3秒であれば time.Second * 3 と書くことができます。この 3s や 4m といった記法は、ユーザーから経過時間を指定してもらう際にとても便利で、例えばUNIXの sleep コマンドにも採用されています

context.Context

context.Context は習慣的に関数の第一引数として渡され、仮想の関数に引き渡されます。

どこに書くかいつも悩んでいたのでこういう慣習を知れたのはよかった

tag付ビルド

これらのソースコードは -tags cat フラグを付けてビルドすると cat.go が有効になり、dog.go は無効になります。

$ go build -tags cat

Functional Options Pattern

Server struct に公開フィールドとして Timeout や Logger を足したり、SetTimeout や SetLogger メソッドを追加したりすることはできます。しかし、利用者にタイムアウト値やロガーを任意のタイミングで変更させたくはありません。できれば New で指定した初期値から変更させないように制限したいですね。

Golang Functional Options Pattern | Golang Cafe

DDDとの親和性高そう

Method Value

少し混乱するかもしれませんが、Goのメソッド呼び出しは、第一引数にレシーバに持った関数の呼び出しと同義であるといえます。

type I int

func (i I) Add(n int) I {
  return i + I(n)
}

func main() {
  n = 1
  fmt.Println(n.Add(2))
  fmt.Println(I.Add(n, 2))
}

I.Add(n, 2) なんて呼び出しができるということを初めて知った

goroutine

goroutine は「ファイル I/O やネットワーク通信でブロックしている間にもランタイムが選んだ CPUコアで別の処理を平行して行うことで単純な並列処理よりも効率的に処理を行う」を主題にしているランタイムの機能です

これを知っているとゴルーチンをむやみやたらに使うことも減りそう

channel by range

ch := make(chan []byte)

for _, b := range ch {
}

channel を for で使えるの初めて知った

テストのパッケージ

テストをする際には、パッケージ名の指定に2つの方法があります。

  • テスト対象と同じパッケージ名称でテストコードを書く
  • テスト対象とは別のパッケージ名称でテストコードを書く

前者はパッケージ名称が同じですので、private関数もテストすることができます。一方、後者はあえて別名を付けることにより、privateな変数や関数にアクセスできないテストを書くことがるため、このパッケージの使用者と同じ実装をサンプルコードとして明示することができます

t.Skip / t.Skipf

テストによっては特定のOSでは実施できないものもあります。そういった場合には t.Skip または t.Skipf を使うことができます。

t.Short

-short を付けて実行した場合には testing.Short() が true を返します。各テストで -short のときにはスキップしたい場合には、t.SkipNow() を呼び出します。

t.Parallel

テストを並列実行したい場合に使うらしい

もう一つの注意点は、Table Driven Tests を平行で実行する際にはループの中で test変数を束縛することです。

for _, test := range tests {
  test := test // ← ★
  t.Run(test.name, func(t *testing.T) {
    t.Parallel()
    got := Add(test.lhs, test.rhs)
    if got != test.want {
      t.Errorf("%v: want %v, but %v:", test.name, test.want, got)
    }
  }
}

★部分の処理を入れないと、タイミングによってはすべてのテストが tests に含まれる最後の項目を参照してしまいます。

これは、今後注意しなくてもよくなるかも?なぜなら、loopvar という機能が今後入る可能性があるから。そのあたりも、mattnさんが Forkwell の Youtubeで話されていた。

Go言語プログラミングエッセンス - Forkwell Library#27 - YouTube

Goの標準外のパッケージ

パッケージ 説明
golang.org/x/arch マシンアーキテクチャ
golang.org/x/crypto 暗号化
golang.org/x/exp 実験的
golang.org/x/image 画像
golang.org/x/mod Go モジュール
golang.org/x/net ネットワーク
golang.org/x/oauth2 OAuth2
golang.org/x/sync 非同期
golang.org/x/sys システムコール
golang.org/x/term 端末
golang.org/x/text テキスト
golang.org/x/time 時間
golang.org/x/tools ツール
golang.org/x/xerrors エラー

flag.Duration / flag.DurationVar

第四章で time.Duration を紹介しましたが、これを flag として受け取ることができます

flag.Text / flag.TextVar

encoding.TextMarshaler は MarshalTextを、encoding.TextUnmarshaler は UnmarshalText メソッドを持ったインターフェースです。

type TestMarshaler interface {
  MarshalText() (text []byte, err error)
}

type TextUnmarshaler interface {
  UnmarshalText(text []byte) error
}

これらインターフェースを実装した型を flag パッケージで扱うことができます。

独自の型をテキストでパースできるのはよさそう

echo.FormFieldBinder / echo.CustomFunc()

echo でフォームデータを型にバインドするときに便利に使えるメソッドがあるみたい。

Binding | Echo

フォーム以外にも、クエリ文字列なんかもあるみたいで便利そう。

また、プリミティブな型以外にもバインドできる echo.CustomFunc() っていうのがあってこれも便利そうだった。

この書籍では inputタグでPOSTされる時刻フォーマットをパースする目的で使われていた。

私もこの問題に直面したことがあって、その時は別の方法で対処したんだけど、今後はこちらを使ったほうが見やすいコードになりそうだなと思った。

テンプレート処理

echo でテンプレートを扱いには Render メソッドを持った実装を e.Renderer に設定する必要があります。

e.Renderer = &Template {
  templates: template.Must(template.New("").
    Funcs(template.FuncMap{
      "FormatDateTime": formatDateTime,
    }).ParseFS(templates, "templates/*")),
}

こうすることで、echoからは以下のようにindexテンプレートにvalueを渡し、FormatDateTime関数を呼び出せるようになります。

return c.Render(http.StatusBadRequest, "index", value)
{{FormatDateTime .}}

echo にテンプレート渡せるのも知らんかったし、テンプレートにカスタム関数を追加できるのも知らんかった。

モジュールのメジャーバージョンアップ

ただし、go get -u は、モジュールのメジャーバージョンアップは行いません。メジャーバージョンアップをするには以下を実行します

$ go mod edit -replace=モジュール名@v=モジュール名@v

ent/ent

Ent (ent/ent) は Facebook 社が開発しているORMです。

今度何かDB関連の個人プロジェクトを作成するときに使ってみようかな。

まとめ

Go言語プログラミングエッセンスの読書感想ブログを書きました。

信頼の mattnさんの書籍で、予想通り良書でした。

Go言語のチュートリアルを一通りやって、次に読む書籍としてよいかと思いますので皆さんも読んでみてはいかがでしょうか?

webview/webiew を使って Linux上でフルスクリーンで表示する

動機

シングルアプリケーションをkioskモードで動かしたい。

そうすることで、ラズパイとかでアプリ作って・・・みたいなことがやりやすそう。

flutter-elinux でもいいんだけど、dart 覚えるの大変そう。

Go でできるなら Go でやりたい。

webview/webview とかでそれっぽいことできないかなぁ~?

昔は webview/webview でも Fullscreen() メソッドがあったけどなぜか最新ではなくなってる。

最新でも Fullscreen() したい!!

そんな感じの動機。

WebKitGtk

webview/webview は linux の場合、 Windowの表示に Gtk3 を使っているみたい。

webview/webview.go at 899018ad0e5cc22a18cd734393ccae4d55e3b2b4 · webview/webview · GitHub

さっきも書いた通り、Webviewにはウィンドウを操作するメソッドは生えてない。あるのはウィンドウへのポインタを返す Window() メソッドがあるのみ。そのポインタを使って勝手に良しなにしてくださいな。ということのようだ。

gotk3/gotk3

Go で Gtk3 を操作するためのライブラリに gotk3/gotk3 というのがある。

GitHub - gotk3/gotk3: Go bindings for GTK3

こいつには Fullscreen() のメソッドがある。

gtk package - github.com/gotk3/gotk3/gtk - Go Packages

なので、このライブラリに webview から取得したポイントを使って Window を作ればフルスクリーンにできるはず。

gotk3/gtk/window.go at master · gotk3/gotk3 · GitHub

ここら辺を見ると、自前で gtk.Window を作れそうだったので、それをベースにコードを書いてみた。

それが以下のコード

コード

package main

import (
    "github.com/webview/webview"
    "github.com/gotk3/gotk3/gtk"
    "github.com/gotk3/gotk3/glib"
    "unsafe"
)

func fullScreen(handle unsafe.Pointer) {
    obj := glib.Take(handle)
    w := &gtk.Window {
        gtk.Bin {
            gtk.Container {
                gtk.Widget {
                    glib.InitiallyUnowned { obj },
                },
            },
        },
    }
    w.Fullscreen()
}

func main() {
    w := webview.New(false)
    defer w.Destroy()

    fullScreen(w.Window())

    w.SetHtml("Thanks for using webview!")

    w.Run()
}

weston を使った フルスクリーンのデモ

weston.ini の [autolaunch] の path に ビルドしたバイナリのフルパスを指定して weston を起動すると以下のように表示されるはず。

VirtualBox 上でのフルスクリーン表示

まとめ

webview/webview を使って Linux上でフルスクリーンで表示してみました。

How to set fullscreen/maximize with newer API (`WebView.SetFullScreen()` have been removed) · Issue #458 · webview/webview · GitHub

ここでも webview の Fullscreen 表示の議論がなされているけど、まだ Open ステータスなので、今のところは 自前で Fullscreen する方法でやっていくしかなさそう。

flutter-elinux を起動するなら [shell] に書くより [autolaunch] に書いたほうが早く起動する

weston を何も指定せずに起動すると、weston-desktop-shell が起動します。

そうじゃなくて、自前のアプリを動かしたい場合は weston.ini の shell セクション の client エントリに実行ファイルのパスを指定してあげると、それが起動します。

flutter-elinux のアプリを起動したい場合は引数を指定する必要があり、clientエントリには引数は指定できないため シェルスクリプトを別途書いて、そいつを clientエントリで呼び出すようにしてあげると起動させることができます。

[shell]
client=/home/bamchoh/run.sh

こんな感じ。

でも結構起動が遅くて、5~10秒かかったりする。

でもさっき気づいたのが、 shellセクションじゃなくて autolaunch セクションに記述するほうほう。 こっちに指定したら即座に起動してびっくりした。

[autolaunch]
path=/home/bamchoh/run.sh

こうすると、weston-desktop-shell が起動した直後に所定のアプリが即座に起動します。 一瞬デスクトップっぽいのが見えたりするけど、それが気にならないならこっちのほうが早く起動できる。

まとめ

weston-desktop-shell が起動するのが気にならない人は [autolaunch] セクションを使ってみよう!