LoginSignup
11
19

More than 5 years have passed since last update.

ArduinoでUSBジョイスティックマウス作ってみた

Last updated at Posted at 2017-07-07

こんなやつです。
P_20170704_223842.jpg
途中ですけど普通に動いてます。いい感じ。

参考にしたページ

Arduino Micro を使って、試しに USB スライドパッド マウスを作ってみた(小型USBキーボード自作のための準備色々)
Arduinoでジョイスティックを使う


動機

省スペースのマウスが欲しかったので作ってみました。
あと、C++11的な機能を使ってどんな風に書けるか試してみたかった、というのもあります。
関数型プログラミングのタグを付けてますが、できる範囲でなるべくそっちっぽく、というほどの意味です。

考えたこと

もともとスケッチの例にJoystickMouseControlというのが入ってますが、そこから思った通りになるように調整するのに皆さん苦労されているようです。
スイッチも可変抵抗もあらかじめ平滑化してノイズを取っておけば、もっと楽になるのでは?

あと、止ってる時、Mouse.move(0,0,0)は送信しなくてもいいかな?
その方がエコだし。

スケッチ

※初出からちょこっと修正しました。本旨は変わりません。

JoystickMouse.ino
#include <Mouse.h>
#include "myLambdas.h"  //使用する関数を別ファイルにまとめました。

constexpr auto MOUSE_BUTTON ( 2 );  //マウスボタンをピン2につないでます。
constexpr auto X_AX ( A0 ); // X軸の出力をピンA0につないでます。
constexpr auto Y_AX ( A1 ); // Y軸の出力をピンA1につないでます。

constexpr auto THRESHOLD ( 1 ); //マウスを動かす最低距離。これ未満は反応しない。
constexpr auto RANGE ( 14 );    //マウスを動かす最大距離
constexpr auto RESPONSE_TIME ( 5 ); // 動作安定と速度調整のための遅延
constexpr auto CENTER_X ( 526 );    // X軸の中心値。別途計測ししました。
constexpr auto CENTER_Y ( 504 );    // Y軸の中心値。別途計測しました。

auto smoothX ( myLambdas::smoothF() );  // X軸のデータを平滑化する関数
auto smoothY ( myLambdas::smoothF() );  //  Y軸のデータを平滑化する関数

auto debounceButton ( myLambdas::debounceF() ); // ボタンのデータをデバウンスする関数
auto isRaisedButton ( myLambdas::isRaisedF() ); //ボタンのデータが falseー>true に変化したか を返す関数
auto isDroppedButton ( myLambdas::isDroppedF() ); //ボタンのデータが trueー>false に変化したか を返す関数

//ボタンのデータによってボタンを動かす関数
auto buttonActionByData (
  [](const bool DATA_BUTTON) -> void {
    if( isRaisedButton( DATA_BUTTON ) )   Mouse.release( MOUSE_LEFT );
    if( isDroppedButton( DATA_BUTTON ) )  Mouse.press( MOUSE_LEFT );
    return;    
  }
);

//X軸、Y軸のデータとTHRESHOLDよってマウスを動かす関数
auto mouseActionByDataF (
  []( const int THRESHOLD ){
    return [=]( const int DATA_X, const int DATA_Y ) mutable -> void {
      if( abs( DATA_X ) < THRESHOLD && abs( DATA_Y ) < THRESHOLD ) return;
      Mouse.move( DATA_X, -DATA_Y, 0 );
      return;    
    };
  }
);
auto mouseActionByData ( mouseActionByDataF( THRESHOLD ) );

//0 ~ CENTER ~ 1023 のデータを-RANGE ~ 0 ~ RANGE にマッピングする関数
//正負それぞれ3ブロックに分け、傾きを変えることでAカーブを近似する
auto mapToRangeF ( 
  []( const int CENTER, const int RANGE ){
    return [=]( const int DATA ) mutable -> int {
      const auto OFFSET (DATA - CENTER );
      const auto IN_MAX ( 1024 );
      const auto EDGE_M3 ( - CENTER );
      const auto EDGE_M2 (  EDGE_M3 * 3 / 4);
      const auto EDGE_M1 (  EDGE_M3 / 2 );
      const auto EDGE_P3 ( IN_MAX - CENTER );
      const auto EDGE_P2 ( EDGE_P3 * 3 /4 );
      const auto EDGE_P1 ( EDGE_P3 / 2 ); 
      if( OFFSET < EDGE_M2 )  return map( OFFSET, EDGE_M2, EDGE_M3, - RANGE / 2, - RANGE     ); 
      if( OFFSET < EDGE_M1 )  return map( OFFSET, EDGE_M1, EDGE_M2, - RANGE / 4, - RANGE / 2 );
      if( OFFSET < 0       )  return map( OFFSET,       0, EDGE_M1,           0, - RANGE / 4 );
      if( OFFSET <= EDGE_P1 ) return map( OFFSET,       0, EDGE_P1,           0,   RANGE / 4 );
      if( OFFSET <= EDGE_P2 ) return map( OFFSET, EDGE_P1, EDGE_P2,   RANGE / 4,   RANGE / 2 );  
                              return map( OFFSET, EDGE_P2, EDGE_P3,   RANGE / 2,   RANGE     );
    };
  }
);
auto mapToRangeX (mapToRangeF( CENTER_X, RANGE ) );
auto mapToRangeY (mapToRangeF( CENTER_Y, RANGE ) );

void setup() {
  pinMode( MOUSE_BUTTON, INPUT_PULLUP );  
  Mouse.begin();    
}

void loop() {
  //マウスボタンのピンを読んでデバウンスしたデータでボタンを動かす
    buttonActionByData( debounceButton( digitalRead( MOUSE_BUTTON ) ) ); 
  //X軸、Y軸のピンを読んで平滑化して移動距離にマッピングしたデータでマウスを動かす
    mouseActionByData( mapToRangeX( smoothX( analogRead( X_AX ) ) )
                    , mapToRangeY( smoothY( analogRead( Y_AX ) ) ) );
    delay( RESPONSE_TIME );
}
myLambdas.h
namespace myLambdas
{
  //以前の値と新しい値の加重平均を返すクロージャ
  auto smoothF ( 
    []( const long RATE = 20 ){
      long previousValue ( 0 );
      return [=]( const long CURRENT_VALUE ) mutable -> long {       
        return previousValue = (RATE * CURRENT_VALUE + (100 - RATE) * previousValue) / 100;
      };
    } 
  );

  //以前の値かける100と新しい値かける100の荷重平均を取り、
  //その値と閾値によってtrueまたはfalseを返すクロージャ
  auto debounceF (
    []( const long RATE = 25, const long THRESHOLD_L = 10, const long THRESHOLD_H = 90 ){
      long previousValueMul100 ( 100 );
      bool debouncedValue ( true ); 
      return [=]( const long CURRENT_VALUE ) mutable -> bool {
        previousValueMul100 = RATE * CURRENT_VALUE + (100 - RATE) * previousValueMul100 / 100;
        if( debouncedValue ) {
          if( previousValueMul100 < THRESHOLD_L ) return debouncedValue = false;
          return true;
        }
        if( previousValueMul100 > THRESHOLD_H ) return debouncedValue = true;
        return false;
      };
    }
  );

  //以前の値がfalseで新しい値がtrueか? を返すクロージャ
  auto isRaisedF(
    [](){
      bool previousValue ( false );
      return [=]( const bool CURRENT_VALUE ) mutable -> bool{
        const bool RESULT ( ! previousValue && CURRENT_VALUE );
        previousValue = CURRENT_VALUE;
        return RESULT;      
      };
    }
  );

  //以前の値がtrueで新しい値がfalseか? を返すクロージャ
  auto isDroppedF(
    [](){
      bool previousValue ( false );
      return [=]( const bool CURRENT_VALUE ) mutable -> bool{
        const bool RESULT ( previousValue && !CURRENT_VALUE );
        previousValue = CURRENT_VALUE;
        return RESULT;    
      };
    }
  );

}

C++11的なこと

  • autoによる型推論
  • ラムダ式
  • constexpr

関数型的なこと

定数で済むものは定数で、と心に念じて始めたらほとんど定数になってしまいました。
再代入しているのはクロージャで環境を持ち回る変数だけです。

初期化は()で、ということにしてみました。見た目が慣れないですけど。
再代入は = で、という風に区別してます。

面倒っぽい処理を関数にしていったら、本体のloop()が短かくなっていって、最終的には三行(整形したので増えてますけど)になりました。
再掲になりますがloop()です。

void loop() {
    buttonActionByData( debounceButton( digitalRead( MOUSE_BUTTON ) ) );
    mouseActionByData( mapToRangeX( smoothX( analogRead( X_AX ) ) )
                    , mapToRangeY( smoothY( analogRead( Y_AX ) ) ) );
    delay( RESPONSE_TIME );
}

それぞれ、

  • マウスボタンのピンを読んで -> デバウンスして -> ボタンを動かす

  • X軸、Y軸のピンを読んで -> 平滑化して -> 移動距離にマッピングして -> マウスを動かす

  • ちょっと待つ

と、言葉の通りの関数(の連続適用)になってます。

一方、処理を丸投げされた関数の方は:

  • いい感じに定数を作って、
  • それをネタに条件分岐して、
  • 何かしたり、値を返したりする

というパターンを繰り返しています。
僕の気持的にはパターンマッチないしガードのイメージです。

はまったこと

再書き込みできない->ブートローダー書き込みで修復

画像の通りA-Star32U4 microを使っているのですが、USBを繋ぐとすぐ動き出す(リセット後も同様)ので書き込みできない事態に陥りました。
Leonardoとしてブートローダーを書き込み直したら、リセット時に時間的余裕ができて書きこみできるようになりました。

関数の前方宣言->別ファイルにしてインクルード

Arduinoだと、loop()で使う関数をloop()の後に書いても使えます。
うっかりそれが普通だと思っていたのですが、戻り値を後置した関数とラムダ式はダメなようです。
使うところよりも前に書かないとエラーになります。
で、別ファイルにしてインクルードしてみました。

map()が思った通りに丸めてくれない->順序を変える

マイナスの除算は今は小数を0の方向にむかって丸めてくれるそうです(昔は違った?)。
でもなぜかmap()はそうなってません。
0にむかって丸めてくれるといいんだけどな...

C++
long map(long x, long in_min, long in_max, long out_min, long out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

内部的にはこうなってるんだそうです。
普通に使うときには
in_min <= x <= in_max
out_min < out_maxなので、
x - in_min >= 0, out_max-out_min > 0, in_max - in_min > 0
xが負の数でも除算は正の除算になるからのようです。
ここでin_minとin_max、out_minとout_maxに入れる数値をそれぞれ交換すれば、
in_min >= x >= in_max
out_min > out_maxなので、
x - in_min <= 0, out_max - out_min < 0, in_max - in_min < 0
マイナスかけるマイナス割るマイナスで全体が負の除算になりました。
これでxが負数の場合に0に向って丸めてくれます。

...ややこしい...すなおに条件分岐したほうがいいかも。

今後

いい感じの箱に入れて使えるようにします。

11
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
19