複数のボタンの同時押しをうまく検知するにはどうすればよいか考える。

本記事はNanoPi NEOのボタン入力をRustで検知してみる(tokio1.x系)の続きです。

ボタンの状態を保持するタプルを定義して、ボタン押下イベントに応じて更新するようにした。 tokio::select!マクロの後でタプルの要素を参照すれば、同時に押されているか判別が可能になる。

コード

    let mut buttons = (false, false, false);
    let state = |e: EventType| -> bool {
        match e {
            EventType::RisingEdge => true,
            EventType::FallingEdge => false,
        }
    };
    loop {
        //どれか1つのボタンが押されたときに標準出力へ出力する
        tokio::select! {
            Some(Ok(e)) = handle1.next() => {
                buttons.0 = state(e.event_type());
            },
            Some(Ok(e))  = handle2.next() => {
                buttons.1 = state(e.event_type());
            },
            Some(Ok(e))  = handle3.next() => {
                buttons.2 = state(e.event_type());
            },
            else => break,
        };
        println!("{:?}", buttons);
    }

問題点

同時押ししてみると分かるが、1個目のボタン押下イベントが走った後に、他方のボタン押下イベントが走ることになる。 これでは同時押しが成立する前にボタン押下イベントを拾ってしまう。

同時押し
F1ボタンとF2ボタンを同時押しした時のスクリーンショット。F1ボタンのほうが若干早くイベントを拾っている。

対策

一つ目の押下イベントを拾った後に一定時間待機させ、その間のイベントをバッファに溜め込むことにした。 一定時間経過後に読みだして、一番最後のイベントを入力として採用する。 とはいうものの、現状のコードではボタン押下されたら即出力されてしまい、一定時間待つということができない。

30分くらい考えた結果、ボタン押下を待つ処理を別スレッドに逃がし、送られてくるイベントをメインスレッドで待ち受けることにした。

コード

let (tx, mut rx) = mpsc::channel(100);
tokio::spawn(async move {
    let mut state = ButtonState::new();

    loop {
        //どれか1つのボタンが押されたときに標準出力へ出力する
        tokio::select! {
            Some(Ok(e)) = handle1.next() => {
                state.set_f1(e.event_type());
            },
            Some(Ok(e))  = handle2.next() => {
                state.set_f2(e.event_type());
            },
            Some(Ok(e))  = handle3.next() => {
                state.set_f3(e.event_type());
            },
            else => break,
        };
        if tx.send(state).await.is_err() {
            println!("receiver dropped");
            return;
        }
    }
});
while let Some(first) = rx.recv().await {
    sleep(Duration::from_millis(60)).await;
    let mut last = first;
    while let Ok(b) = rx.try_recv() {
        last = b;
    }
    println!("{:?}", last);
}
  • ボタンの状態を保持する変数ををタプルから構造体に変更
  • チャネルを使って別スレッドからデータを受け取る実装に変更
  • 最初のイベントがあってから60ミリ秒待ち、最後のイベントを最終的な入力とするように変更
動作gif