Help us understand the problem. What is going on with this article?

MATLAB EXPO 2019 Lightning Talkのメイキングオブ(その2)

2.マウス de カメラ編

2-1 背景

まず、「なぜ、マウスを取り上げたのか」ですが、これはポスターセッションのほうで発表した「移動距離&位置検出」をマウスで検討していたからです。
残念なことに、マウスによる位置検出はあまり上手くいかず(マウスとマウスパッドの距離が厳密すぎる。使ったマウスでは±0.3mmの範囲しかできなかった)あきらめたのですが、その際にいろいろマウスの中身について調べたのが今回の発表につながっています。
ポスターのほうは、どうしてそのまま「移動距離&位置検出」でやったかというと、ライトニングトークのネタバレをしたくなかった、ということですね(せこいな)。

2-2 マウスの内部構造とArduinoによるハッキング

まず、最初にお断りしておきますが、マウスのArduinoによるハッキングは、すでに多くの人がやっており、webで検索すると何例も出てきます。
ですので、今回は、マウスをArduinoによりハッキングしたデータをMATLABでいかに調理するか、ということに絞ります。

LED(もしくはレーザー)マウスの中身は下記のようになっており、

  • 光源であるLED
  • LEDから出た光をマウスパッドに導くためのプリズム(みたいなもの)
  • マウスパッドで反射した光を集束させるレンズ
  • レンズ焦点部に置かれたセンサデバイス

から構成されています。それとクリックボタンのためのスイッチホイール関係です。
image.png

センサデバイスの中には、イメージセンサと、取り込まれた信号を処理するマイクロプロセッサからなっています。
通常、使われるときには、マイクロプロセッサで計算された移動距離(X方向、Y方向)を出力します。
センサデバイスとUSBケーブルの間には、別のプロセッサがあり、移動量とかクリック、ホイールの状況を交通整理してUSBに乗せ、PCに送っています。
で、センサデバイスのインターフェースですが、概ねI2Cが使われており、I2Cの2本の線をハッキングして、Arduinoにつなげれば、センサデバイスをArduinoによりコントロールできるようになります。
センサデバイスの出力には、移動距離情報はもちろん出せますが、それ以外に「イメージセンサで取得した情報をそのまま出力する」ことができます(一部、出せないデバイスもあります)。

ハッキングするマウスはあまり新しいものでないものがよいです。最近のマウスは出力信号がそのままUSBに出ていってしまうので、ハッキングが容易でないです。

今回はADNS-2610というセンサデバイスを搭載したDELLのマウスを使いました。
※ADNS-2610以外にもハッキング可能なデバイスはいくつかありますが、ここでは省略します。また、このハッキングにはハンダ付けとか回路の加工が必要になります。

  1. Arduinoデバイスはマウスの中に入るような小さいものを使いました。ここではBeetleというArduinoを用いています。Leonard互換です。
  2. ADNS-2610のI2Cピン2本を途中から切り、ArduinoのI2Cピンと接続します。
  3. 左クリックスイッチの基板のパターンを切ります。スイッチの端子とArduinoのD0ピンを接続します。
  4. もともとのUSBケーブルを使いたい場合にはマウス基板のUSB信号線(D+とD-)を切り、ArduinoのUSB信号線と接続します(これは、やったほうが見栄えは良くなりますが、ArduinoのUSB信号線は引き出しにくいので注意が必要です)
  5. あと、光源のLEDの電源がヘタる(電圧が降下する)という問題がおきたため、電源に470uFのコンデンサを追加してあります。

image.png
マウス内部の改造は以上のとおりです。

2-3 動作確認

マウスハッキング用のArduinoのプログラムは、いくつもwebか入手可能です。私はArduino Forumにある、JohnWasserさんの書いたサンプルを用いてみました(別途ADNS1260用のライブラリが必要となります)。
Arduino IDEによるプログラムのコンパイル、動作に関してはここでは省略しますので、関連ページを参照してください。
マウスの改造がうまくいっていれば、これらのプログラムによりマウスの移動情報や画像データをPCに表示することができます(ただし、シリアルコンソールなので、数値データのみ)
MATLABとの転送を行なうために下記のようにプログラムを修正します。

sample.c
const byte ADNS2610_SCLK_PIN = 3;
const byte ADNS2610_SDIO_PIN = 2;
//const byte ADNS2610_PD_PIN = 4;//パワーダウンは不要
const byte ADNS2610_SW_PIN = 0;//左クリックのための入力用

const byte REG_CONFIGURATION        = 0x00;
const byte REG_STATUS               = 0x01;
const byte REG_DELTA_X              = 0x02;
const byte REG_DELTA_Y              = 0x03;
const byte REG_SQUAL                = 0x04;
const byte REG_MAXIMUM_PIXEL        = 0x05;
const byte REG_MINIMUM_PIXEL        = 0x06;
const byte REG_PIXEL_SUM            = 0x07;
const byte REG_PIXEL_DATA           = 0x08;
const byte REG_SHUTTER_UPPER        = 0x09;
const byte REG_SHUTTER_LOWER        = 0x0A;
const byte REG_INVERSE_PORODUCT_ID  = 0x11;

const int FRAME_WIDTH = 18;
const int FRAME_HEIGHT = 18;
const int FRAME_SIZE = FRAME_WIDTH * FRAME_HEIGHT;

byte FrameBuffer[FRAME_SIZE];

char serial_recv;
int SWdata;
char dx;
char dy;

//マウスの現在値
signed long x = 0;
signed long y = 0;

void setup() {
  Serial.begin(115200);

  ADNS2610_reset();
  //ADNS2610_SW_PINは割り込み用に使う。ボタンが押されてHigh→Lowになったときに割り込み発生
  attachInterrupt( digitalPinToInterrupt(ADNS2610_SW_PIN), intFunc, FALLING );
  SWdata=0;//通常は0

  byte productId = (~readRegister(REG_INVERSE_PORODUCT_ID)) & 0x0F;

  byte config = readRegister(REG_CONFIGURATION);
  config |= B00000001; // Forced Awake Mode (LED always powered on).
  writeRegister(REG_CONFIGURATION, config);
}

void loop() {
    dx = readRegister(REG_DELTA_X);
    dy = readRegister(REG_DELTA_Y);
    x += dx;
    y += dy;

    while(Serial.available() >0){//シリアル通信が使えるときに
    serial_recv = Serial.read();//読み込みして
    if(serial_recv =='1'){//1だったらマウス位置データを送る
        Serial.print(SWdata);
        Serial.print(" , "); 
        Serial.print(x);
        Serial.print(" , "); 
        Serial.print(y);
        Serial.print("\n");
        if(SWdata>0){//なおかつSWdataが0より大きかったら画像を送る
          dumpFrame();
        }
        SWdata=0;
    }
    if(serial_recv =='2'){//2だったらマウス位置データリセット
      x=0;
      y=0;
    }
    delay(100);
  }
}

void dumpFrame() {
  int count = 0;

  // Start a frame
  writeRegister(REG_PIXEL_DATA, 0);
  do {
    byte data = readRegister(REG_PIXEL_DATA);
    if (data & 0x40){ // Data is valid
      FrameBuffer[count++] = data & 0x3F;
    }
    if ((count != 1) && (data & 0x80)) { // Unexpected Start-of-Frame
      //Serial.print(F("Unexpected Start-of-Frame at index "));
      //Serial.println(count - 1);
    }
  } while (count < FRAME_SIZE);

  // Dump the frame buffer.
  // Pixels read bottom to top and columns read left to right.
  // Display the pixels left to right and rows top to bottom
  //Serial.println("FRAME:");
  for (int row = 0; row < FRAME_HEIGHT; row++) {
    for (int column = 0; column < FRAME_WIDTH; column++) {
      // Top row is FRAME_HEIGHT-1
      // Bottom row is 0;
      int index = column * FRAME_HEIGHT + ((FRAME_HEIGHT - 1) - row);
      byte pix = FrameBuffer[index];
      //if ( pix < 0x10 )
        //Serial.println("00");
      //Serial.println(pix, HEX);
      Serial.println(pix, DEC);
    }  // end for(column)
    // New line after each row
    //Serial.println();
  } // end for(row)
}

void ADNS2610_reset() {
  pinMode(ADNS2610_SCLK_PIN, OUTPUT);
  pinMode(ADNS2610_SDIO_PIN, INPUT);
  pinMode(ADNS2610_SW_PIN, INPUT_PULLUP);

  digitalWrite(ADNS2610_SCLK_PIN, LOW);
  delayMicroseconds(1);
}

byte readRegister(byte address) {
  pinMode (ADNS2610_SDIO_PIN, OUTPUT);

  for (byte i = 0x80; i > 0 ; i >>= 1) {
    digitalWrite (ADNS2610_SCLK_PIN, LOW);
    digitalWrite (ADNS2610_SDIO_PIN, address & i);
    digitalWrite (ADNS2610_SCLK_PIN, HIGH);
  }

  pinMode (ADNS2610_SDIO_PIN, INPUT);

  delayMicroseconds(100); // tHOLD = 100us min.

  byte res = 0;
  for (byte i = 0x80; i; i >>= 1) {
    digitalWrite (ADNS2610_SCLK_PIN, LOW);
    digitalWrite (ADNS2610_SCLK_PIN, HIGH);
    if ( digitalRead (ADNS2610_SDIO_PIN))
      res |= i;
  }

  return res;
}

void writeRegister(byte address, byte data) {
  address |= 0x80; // MSB indicates write mode.
  pinMode (ADNS2610_SDIO_PIN, OUTPUT);

  for (byte i = 0x80; i; i >>= 1) {
    digitalWrite (ADNS2610_SCLK_PIN, LOW);
    digitalWrite (ADNS2610_SDIO_PIN, address & i);
    digitalWrite (ADNS2610_SCLK_PIN, HIGH);
  }

  for (byte i = 0x80; i; i >>= 1) {
    digitalWrite (ADNS2610_SCLK_PIN, LOW);
    digitalWrite (ADNS2610_SDIO_PIN, (data & i) != 0 ? HIGH : LOW);
    digitalWrite (ADNS2610_SCLK_PIN, HIGH);
  }

  delayMicroseconds(100); // tSWW, tSWR = 100us min.
}

void intFunc(){
  SWdata=1;//割り込み処理。単にSWdataを書き換えているだけ
}

2-4 MATLABへのデータ取り込みと画像出力

Arduinoから送られてきたデータは、MATLABのシリアルデータ転送により受け取ることが可能です。
具体例としては、

serial.m
s=serial('COM3');%シリアルポートの指定、ポート番号は実態に合わせる
set(s,'BaudRate',115200);%通信速度の指定
fopen(s);%ポートを開く
pause(0.2);
fprintf(s,'2');%Arduinoに転送(位置リセット)
pause(0.1);

fprintf(s,'1');%Arduinoに転送
out=fscanf(s);%Arduinoから転送
%ここにArduinoからもらったデータ(out)の処理が入る

%終了処理
fclose(s);
delete(s);
clear s; 

 という方法でやり取りします。
また、この場合、MATLAB側に主導権を持たせ、MATLAB側から1を送り
 Arduinoのマウスボタンがクリックされていない⇒移動情報のみを返す
 Arduinoのマウスボタンがクリックされている⇒移動情報と画像データを返す
というようにしておきます。

マウスの位置情報を取り込んで画面に表示するプログラムはmouse1.m です(mファイルのコードはMathworks社のFile Exchangeよりダウンロードいただけます)。プログラムを終了するときは「q」を押してください。
※少し脱線しますが、無限で無限ループ(もしくはそれに類するもの)を動かしておいて、止めたいときに止める方法として、下記のようなものを使っています。

exitsample.m
fig1=figure(1);

while(1)
  %figure(1)を表示しておく。
    if strcmp(get(fig1,'currentcharacter'),'q')
        break
    end
end

こうすると、「q」を押すことでループから抜け出ることができます。

次に、Arduinoからセンサの画像データ1枚分を取り込み、表示するプログラムmouse2.mです。
スイッチに細工をした左ボタンをクリックすることで、画像を取り込み、表示することができます

2-4 MATLAB EXPOのためのスクラッチ画像表示プログラム

ということで、Arduino経由でセンサからの画像データをMATLABに取り込み、表示ができるようになりましたが、
とにかく画素数が少ないので、「映える」画像が出ません。
そこで、さらに改良し、
・左ボタンを押さず、マウスを動かしたときには、マウスの移動情報を拾い、カーソルを動かす
・左ボタンをクリックすると、マウスセンサからその位置の画像を取り込み、figureの中の該当する位置に表示する
・これらを繰り返すことで、大きな画像を表示できるようにする。
プログラムはmouse3.mです。
デモ画像は、このようになります。


マウスの移動情報の取り込みレートが遅いので、移動速度が速い場合に位置に誤差が出ますが、概ね狙った位置に画像を貼り付けることができているかと思います。

おつかれさまでした。

おまけのページへ

その1にもどる

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away