manvaのエンジニアリング魂

エンジニアリング・ものづくり・DIYをもっと身近にするためのブログ。インスピレーションを刺激します。

マウス内蔵キーボードver.0.4 爆誕

できた。

使ってみた。

ダメだ。

もちろん、前に作ったものよりは格段に良くなったのだが,残念ながらこれを普段使いにしたいとは思えない。慣れれば使えないことはない,というレベル。

一番の問題は、キーをすり鉢状に傾けすぎたせいで,押したときにデバイスが逃げるように動いてしまう点。
f:id:manva:20210425232957p:plain
前に作ったときそれが嫌だったので,手のひらで押さえながら指だけで打てるようにする,というコンセプトで作ったが,やはり手のひらでずっと押さえているというのは使っていて気持ち良くない。最初のキー配置決める時は粘土だったから、ずっしりしてて問題なかったんだよな。おもりつけたらまだいいかも。


前回の記事の後でやったことを書いておく。

もう少しで完成,と思っていたが,最後にまあまあの難関が残っていた。

Arduino IDEが日本語キーボードに対応していない問題

Arduino IDEの標準機能では,日本語キーボードには対応していないのである。すでに解決してくれている人がいるので助かるが,まだいろいろうまく行かず,キーボードの仕様がよくわからなくなって,結局,まあまあちゃんと勉強してしまったので、別記事にまとめておいた。
manva.hatenablog.com

全角/半角キーで日本語に切替えられず,「`」になる問題で苦労したが,これはキーボード側のソフトの問題ではなく,Windows側で英語キーボードとして認識されているせいだった。最近,X-Bows↓という英語キーボードを使っていたので,英語キーボードの設定になっていた。
x-bows.com

Windowsでは,複数のキーボードを接続した場合,日本語と英語の切換えは基本的にキーボードごとの設定ではなく,全てのキーボードが同じ設定になるようだ。レジストリをいじったら日本語キーボードとして使えるようになったのだが,今度はX-Bowsの方まで日本語キーボードとして扱われてしまい、いくつかのキーが刻印と一致しなくなった。この問題はまだ解決できていない。

 
その他は細々した問題。

チャタリングの問題

キーを押したとき,何回かに1回くらい,チャタリングで同じ文字が2個入力される問題があった。

まずは,QMKではどうしているか勉強↓。
Debounce API - QMK

タイマーで実時間で設定できた方が良いのだろうが,自分の作る処理の演算時間がそんなに大幅に変わる予定もないので,普通にループでカウントダウンするだけにしよう。

 QMKでは,Eager(押したらすぐ反応し,しばらくは無視)か,Defer(しばらく押されていたら反応)かを選べるようだが,Eagerの実装にしよう。

 

キーを押したままレイヤー切換えると離しても押しっぱなしになる問題

これは単純にレイヤー切替えたときReleaseAll()することで解決。

void SetLayerTo( int layernum ){
  if( layer!=layernum ){
    layer = layernum;
    Keyboard.releaseAll(); //押したままレイヤー切換えたら押しっぱなしになるので全部離す
  }  
}

 

レイヤーをどうするか

QMK firmwareのLAYOUTマクロをパクってキー配置をビジュアル的に見やすくした。キーの定義もQMKっぽく名前をつけた。

#define LAYOUT( \
  L31, L41, L32, L42, L33, L43,  L24, L34,             R45, R55,  R36, R46, R37, R47, R38, R48, \
  L21, L51, L22, L52, L23, L53,  L14,                       R65,  R26, R56, R27, R57, R28, R58, \
  L11, L61, L12, L62, L13, L63,  L64, L54, L44,   R35, R25, R15,  R16, R66, R17, R67, R18, R68 \
  ) \
  { \
    { L11, L12, L13, L14, R15, R16, R17, R18 }, \
    { L21, L22, L23, L24, R25, R26, R27, R28 }, \
    { L31, L32, L33, L34, R35, R36, R37, R38 }, \
    { L41, L42, L43, L44, R45, R46, R47, R48 }, \
    { L51, L52, L53, L54, R55, R56, R57, R58 }, \
    { L61, L62, L63, L64, R65, R66, R67, R68 } \
  }

レイヤーは、数字シフトキーというのを用意し、押している間は数字レイヤ。数字レイヤでは,ホームポジションの行を数字、その上の行をシフトキー+数字で入力される記号とした。下の行は,キーを減らしたせいではみ出た記号のキーを並べた。もう1つ,ファンクションシフトキーを押している間はファンクションレイヤ。ファンクションキーとかカーソルキーとかを集めた。
f:id:manva:20210425232319p:plain
シフトキーと親指のキーはレイヤーによらず共通。エンターキーは,小指でターン!と叩くのが好きそうな人をよく見かけるので,小指のキーを減らしてもその快感を奪わないよう,親指の端にして手首逆回転でターンとできるようにした。

const byte keymaps[NUM_LAYER][NUM_ROW][NUM_COL_ALL]  = {
LAYOUT( \
  KC_TAB,  KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,     _______, KC_LGUI,                  KC_RALT, KC_RCTL,  KC_Y,    KC_U,    KC_I,    KC_O,    KC_P,    KC_AT,   \
  KC_CAPS, KC_A,    KC_S,    KC_D,    KC_F,    KC_G,     _______,                                    _______,  KC_H,    KC_J,    KC_K,    KC_L,    KC_SCLN, KC_COLN, \
  KC_LSFT, KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,     KC_BSPC, KC_SPC, KC_DEL,   KC_ENT, KC_SPC,  KC_ZKHK,  KC_N,    KC_M,    KC_COMM, KC_DOT,  KC_SLSH, KC_RSFT ),
LAYOUT( \
  KC_ESC,  KC_EXLM, KC_DQOT, KC_HASH, KC_DLR,  KC_PERC,  _______, KC_LGUI,                  KC_RALT, KC_RCTL,  KC_AMPR, KC_QUOT, KC_LPRN, KC_RPRN, KC_P,    KC_EQL,  \
  KC_CAPS, KC_1,    KC_2,    KC_3,    KC_4,    KC_5,     _______,                                    _______,  KC_6,    KC_7,    KC_8,    KC_9,    KC_0,    KC_MINS, \
  KC_LSFT, KC_BSLS, KC_CIRC, KC_YEN,  KC_TILD, KC_PIPE,  KC_BSPC, KC_SPC, KC_DEL,   KC_ENT, KC_SPC,  KC_ZKHK,  KC_LBRC, KC_RBRC, KC_LLBR, KC_RLBR, KC_UNDS, KC_RSFT ),
LAYOUT( \
  KC_F1,   KC_F2,   KC_F3,   KC_F4,   KC_F5,   KC_F6,    _______, KC_LGUI,                  KC_RALT, KC_RCTL,  KC_F7,   KC_F8,   KC_F9,   KC_F10,  KC_F11,  KC_F12,  \
  KC_CAPS, _______, KC_LEFT, KC_UP,   KC_RGHT, _______,  _______,                                    _______,  _______, KC_HOME, KC_PGUP, KC_END,  KC_PSCR, _______, \
  KC_LSFT, _______, _______, KC_DOWN, _______, _______,  KC_BSPC, KC_SPC, KC_DEL,   KC_ENT, KC_SPC,  KC_ZKHK,  _______, _______, KC_PGDN, _______, _______, KC_RSFT )
};

 

通信速度

左右デバイス間の通信速度(ボーレート)を上げた。ArduinoのMax 115200bpsに上げたら動かなかったので,19200bpsにしておいた。元々の設定9600bpsから2倍にしているが,体感では違いがわからない。

 

マウスを動かしたときのザラザラ感

右手用の裏にマウスソール↓(滑りを良くするやつ)を貼った。

f:id:manva:20210425164816j:plain

 

とにかく、これで完成。やりたかったことは大体やったので,これ以上やって完璧を目指してもあまり大幅な改善は期待できない。
結論:マウスはキーボードに内蔵しない方が良い
しばらく別のことしよう。


参考までにソースコードを載せておく。
右手用
KeybouseR.ino

// 右手用
#include <SPI.h>
#include <Mouse.h>
#include <Keyboard.h>
#include "ADNS3050.h"
#define PIN_NUM_PHASE_A 2 //pin2->int1 phaseA
#define PIN_NUM_PHASE_B 3 //pin3->int0 phaseB
#define INT_NUM_PHASE_A 1
#define INT_NUM_PHASE_B 0
#define NUM_ROW 6
#define NUM_COL 4
typedef  struct  {
  int row;
  int col;
} str_keypos;
const int PIN_NUM_ROW[NUM_ROW] = { 4, 5, 6, 7, 8, 9 };
const int PIN_NUM_COL[NUM_COL] = { A3, A2, A1, A0 };
const str_keypos KEYPOS_MOUSE_SHIFT = {5,4};
bool PreviousKeyState[NUM_ROW][NUM_COL];
byte MouseDeltaX = 0;
byte MouseDeltaY = 0;
byte WheelCount = 0;
volatile bool PreviousA = 0;
volatile bool PreviousB = 0;
bool flgMouseMode;

//-----------------------------------------------------------------
void setup(){
  int row, col;
  
  startupImageSensor();
  Mouse.begin();
  Keyboard.begin();
  Serial1.begin(19200); //左右通信用
  pinMode( PIN_NUM_PHASE_A, INPUT_PULLUP );
  pinMode( PIN_NUM_PHASE_B, INPUT_PULLUP );
  attachInterrupt( INT_NUM_PHASE_A, EncoderA_Change, CHANGE );
  attachInterrupt( INT_NUM_PHASE_B, EncoderB_Change, CHANGE );

  for( row = 0; row < NUM_ROW; row++ ){
    pinMode( PIN_NUM_ROW[row], OUTPUT );
  }
  for( row = 0; row < NUM_COL; row++ ){
    pinMode( PIN_NUM_COL[row], INPUT_PULLUP );
  }
  for( row = 0; row < NUM_ROW; row++){
    for( col = 0; col < NUM_COL; col++){
      PreviousKeyState[row][col] = HIGH;
    }
    digitalWrite( PIN_NUM_ROW[row], HIGH );
  }
}
//-----------------------------------------------------------------
void EncoderA_Change(){
  int CurrentA, CurrentB;
  CurrentA = digitalRead( PIN_NUM_PHASE_A );
  CurrentB = digitalRead( PIN_NUM_PHASE_B );
  //※本来,値が変化したときにだけ割込が入るはずなので,Previousを残しておく必要はないし,else ifの条件も要らないはずなのだが,
  //なぜか割込が呼ばれているのにAが変わっていない時がある。チャタリングしていて,割込が入ってから値を読むまでの間に戻った?
  if( ((PreviousA==0)&&(CurrentA==1)&&(CurrentB==0))||((PreviousA==1)&&(CurrentA==0)&&(CurrentB==1)) ){
    WheelCount++;
  }else if( ((PreviousA==0)&&(CurrentA==1)&&(CurrentB==1))||((PreviousA==1)&&(CurrentA==0)&&(CurrentB==0)) ){
    WheelCount--;
  }
  PreviousA = CurrentA;
}
//-----------------------------------------------------------------
void EncoderB_Change(){
  int CurrentA, CurrentB;
  CurrentA = digitalRead( PIN_NUM_PHASE_A );
  CurrentB = digitalRead( PIN_NUM_PHASE_B );
  if( ((PreviousB==0)&&(CurrentB==1)&&(CurrentA==1))||((PreviousB==1)&&(CurrentB==0)&&(CurrentA==0)) ){
    WheelCount++;
  }else if( ((PreviousB==0)&&(CurrentB==1)&&(CurrentA==0))||((PreviousB==1)&&(CurrentB==0)&&(CurrentA==1)) ){
    WheelCount--;
  }
  PreviousB = CurrentB;
}
//-----------------------------------------------------------------
void SendMouseMove( byte MouseDeltaX, byte MouseDeltaY, byte WheelCount ){
  byte sendData;
  // データフォーマット
  // 先頭bit: マウスデータフラグ,2番目bit: 同期フラグ
  // 後ろ 6bit x 4 で 8bitデータ3つを送信
  sendData = 0b11000000 | ( MouseDeltaX >> 2 );
  Serial1.write( sendData );
  sendData = 0b10000000 | ( ( MouseDeltaX & 0b00000011 ) << 4 | MouseDeltaY >> 4 );
  Serial1.write( sendData );
  sendData = 0b10000000 | ( ( MouseDeltaY & 0b00001111 ) << 2 | WheelCount >> 6 );
  Serial1.write( sendData );
  sendData = 0b10000000 | ( WheelCount & 0b00111111 );
  Serial1.write( sendData );
}
//-----------------------------------------------------------------
bool CheckSpecialKey( str_keypos* keypos1, str_keypos* keypos2 ){
  if( ( keypos1->row==keypos2->row ) &&( keypos1->col==keypos2->col ) ){
    return true;
  }else{
    return false;
  }
}
//-----------------------------------------------------------------
void SendKeyPress( str_keypos* keypos ){
  Serial1.write( 1<<6 | keypos->row<<3 | keypos->col );
}
//-----------------------------------------------------------------
void SendKeyRelease( str_keypos* keypos ){
  Serial1.write( keypos->row<<3 | keypos->col );
}
//-----------------------------------------------------------------
void ClearImageSensorDeltaXY( void ){
    MouseDeltaX = 0;
    MouseDeltaY = 0;
    GetImageSensorDeltaX(); // 読取ったら消える
    GetImageSensorDeltaY();
}
//-----------------------------------------------------------------
void loop(){
  int CurrentKeyState, row, col;
  str_keypos keypos;
  bool isMouseShiftKey;
   
  for( row=0; row<NUM_ROW; row++ ){ // キーボードキー状態送信
    digitalWrite( PIN_NUM_ROW[row], LOW );
    for( col=0; col<NUM_COL; col++ ){
      CurrentKeyState = digitalRead( PIN_NUM_COL[col] );
      keypos.row = row;
      keypos.col = col + 4;
      isMouseShiftKey = CheckSpecialKey( &KEYPOS_MOUSE_SHIFT, &keypos );
      if( CurrentKeyState == LOW ){ //押しているとき
        if( PreviousKeyState[row][col] == HIGH ){ //今押した
          SendKeyPress( &keypos );
          if( isMouseShiftKey ){
            ClearImageSensorDeltaXY();
          }
        }
        if( isMouseShiftKey ){
          flgMouseMode = 1;
        }
      }else{ //離しているとき
        if( PreviousKeyState[row][col] == LOW ){ //今離した
          SendKeyRelease( &keypos );
        }
        if( isMouseShiftKey ){
          flgMouseMode = 0;
        }
      }
      PreviousKeyState[row][col] = CurrentKeyState;
    }
    digitalWrite( PIN_NUM_ROW[row], HIGH );
  }
  MouseDeltaX = GetImageSensorDeltaX();
  MouseDeltaY = -GetImageSensorDeltaY();
  //if( flgMouseMode ){ // マウスモード
    SendMouseMove( MouseDeltaX, MouseDeltaY, WheelCount );
    WheelCount = 0; //↑とセット。使った直後にリセット
  //}
}

ADNS3050.h
これは↓のソースを少し書き換えたもの。
Interfacing an Arduino With a Mouse Sensor (ADNS-3050) : 3 Steps - Instructables

#include <SPI.h>
// SPI and misc pins for the ADNS
#define PIN_SCLK   SCK
#define PIN_MISO   MISO
#define PIN_MOSI   MOSI
#define PIN_NCS    10
// レジスタ
#define DELTA_X               0x03
#define DELTA_Y               0x04
#define MOUSE_CTRL            0x0d
#define RESET                 0x3a
#define MOTION_CTRL           0x41
byte ImageSensorRead( byte reg_addr ){
  digitalWrite( PIN_NCS, LOW );//begin communication
  // send address of the register, with MSBit = 0 to say it's reading
  SPI.transfer( reg_addr & 0x7f );
  delayMicroseconds(100);
  // read data
  byte data = SPI.transfer(0);
  delayMicroseconds(30);
  digitalWrite( PIN_NCS, HIGH );//end communication
  delayMicroseconds(30);
  return data;
}

void ImageSensorWrite( byte reg_addr, byte SPIdata ){
  digitalWrite( PIN_NCS, LOW );
  //send address of the register, with MSBit = 1 to say it's writing
  SPI.transfer( reg_addr | 0x80 );
  //send data
  SPI.transfer( SPIdata );
  delayMicroseconds(30);
  digitalWrite( PIN_NCS, HIGH );//end communication
  delayMicroseconds(30);
}

void ImageSensorComStart(){
  digitalWrite( PIN_NCS, HIGH );
  delay(20);
  digitalWrite( PIN_NCS, LOW );
}

void startupImageSensor(){
  //--------Setup SPI Communication---------
  byte out = 0;
  byte read = 0;
  byte bit = 0;
  pinMode( PIN_MISO, INPUT );
  pinMode( PIN_NCS, OUTPUT );
  SPI.begin();
  // set the details of the communication
  SPI.setBitOrder( MSBFIRST ); // transimission order of bits
  SPI.setDataMode( SPI_MODE3 ); // sampling on rising edge
  SPI.setClockDivider( SPI_CLOCK_DIV16 ); // 16MHz/16 = 1MHz
  delay(10);
  //----------------- Power Up and config ---------------
  ImageSensorComStart();
  ImageSensorWrite( RESET, 0x5a ); // force reset
  delay(100); // wait for it to reboot
  ImageSensorWrite( MOUSE_CTRL,  0x20 ); //Setup Mouse Control
  ImageSensorWrite( MOTION_CTRL, 0x00 ); //Clear Motion Control register
  delay(100);
}

byte GetImageSensorDeltaX(){//returns the X acceleration value
  return( ImageSensorRead( DELTA_X ) );
}
byte GetImageSensorDeltaY(){//returns the Y acceleration value
  return( ImageSensorRead( DELTA_Y ) );
}

左手用
KeybouseL.ino

// 左手用
#include <Mouse.h>
#include <Keyboard_jp.h>
#include "KeyDefine.h"
#define NUM_ROW 6
#define NUM_COL 4
#define NUM_COL_ALL 8
#define NUM_LAYER 3 //レイヤの数

#define PRESS   0  //押した(アクティブロー)
#define RELEASE 1 //離した

#define LAYER_MAIN 0 //メインレイヤのレイヤ番号
#define LAYER_NUM  1 //数字レイヤのレイヤ番号
#define LAYER_FN   2 //ファンクションレイヤのレイヤ番号

typedef  struct  {
  int row;
  int col;
} str_keypos;
byte MouseDeltaX = 0;
byte MouseDeltaY = 0;
byte WheelCount = 0;
byte readMouseData[4];
int readMouseDataCount = 0;
int layer = 0;
int PreviousKeyState[NUM_ROW][NUM_COL];
bool flgMouseMode = false;
const int PIN_NUM_ROW[NUM_ROW] = { 9, 8, 7, 6, 5, 4 };
const int PIN_NUM_COL[NUM_COL] = { A0, A1, A2, A3 };
const int debounce = 100;

const byte keymaps[NUM_LAYER][NUM_ROW][NUM_COL_ALL]  = {
LAYOUT( \
  KC_TAB,  KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,     _______, KC_LGUI,                  KC_RALT, KC_RCTL,  KC_Y,    KC_U,    KC_I,    KC_O,    KC_P,    KC_AT,   \
  KC_CAPS, KC_A,    KC_S,    KC_D,    KC_F,    KC_G,     _______,                                    _______,  KC_H,    KC_J,    KC_K,    KC_L,    KC_SCLN, KC_COLN, \
  KC_LSFT, KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,     KC_BSPC, KC_SPC, KC_DEL,   KC_ENT, KC_SPC,  KC_ZKHK,  KC_N,    KC_M,    KC_COMM, KC_DOT,  KC_SLSH, KC_RSFT ),
LAYOUT( \
  KC_ESC,  KC_EXLM, KC_DQOT, KC_HASH, KC_DLR,  KC_PERC,  _______, KC_LGUI,                  KC_RALT, KC_RCTL,  KC_AMPR, KC_QUOT, KC_LPRN, KC_RPRN, KC_P,    KC_EQL,  \
  KC_CAPS, KC_1,    KC_2,    KC_3,    KC_4,    KC_5,     _______,                                    _______,  KC_6,    KC_7,    KC_8,    KC_9,    KC_0,    KC_MINS, \
  KC_LSFT, KC_BSLS, KC_CIRC, KC_YEN,  KC_TILD, KC_PIPE,  KC_BSPC, KC_SPC, KC_DEL,   KC_ENT, KC_SPC,  KC_ZKHK,  KC_LBRC, KC_RBRC, KC_LLBR, KC_RLBR, KC_UNDS, KC_RSFT ),
LAYOUT( \
  KC_F1,   KC_F2,   KC_F3,   KC_F4,   KC_F5,   KC_F6,    _______, KC_LGUI,                  KC_RALT, KC_RCTL,  KC_F7,   KC_F8,   KC_F9,   KC_F10,  KC_F11,  KC_F12,  \
  KC_CAPS, _______, KC_LEFT, KC_UP,   KC_RGHT, _______,  _______,                                    _______,  _______, KC_HOME, KC_PGUP, KC_END,  KC_PSCR, _______, \
  KC_LSFT, _______, _______, KC_DOWN, _______, _______,  KC_BSPC, KC_SPC, KC_DEL,   KC_ENT, KC_SPC,  KC_ZKHK,  _______, _______, KC_PGDN, _______, _______, KC_RSFT )
};

const str_keypos KEYPOS_MOUSE_SHIFT = {5,4};
const str_keypos KEYPOS_LEFT_CLICK  = {4,5};
const str_keypos KEYPOS_RIGHT_CLICK = {1,6};

const str_keypos KEYPOS_NUM_SHIFT = {0,3};
const str_keypos KEYPOS_FN_SHIFT  = {1,3};

//-----------------------------------------------------------------
void setup(){
  int row, col;
  
  Mouse.begin(); //start mouse emulation
  Keyboard.begin();
  Serial1.begin( 19200 ); //左右通信用
  for( row = 0; row < NUM_ROW; row++ ){
    pinMode( PIN_NUM_ROW[row], OUTPUT );
  }
  for( col = 0; col < NUM_COL; col++ ){
    pinMode( PIN_NUM_COL[col], INPUT_PULLUP );
  }
  for( row = 0; row < NUM_ROW; row++){
    for( col = 0; col < NUM_COL; col++){
      PreviousKeyState[row][col] = HIGH; // 全てのキーの前回値を「押してない」にしておく
    }
    digitalWrite( PIN_NUM_ROW[row], HIGH ); // 全てのキーを無効にしておく
  }
}
//-----------------------------------------------------------------
void MousePressRelease( bool isPress, str_keypos* keypos ){
  if( CheckSpecialKey( &KEYPOS_LEFT_CLICK, keypos ) ){
    if( isPress ){
      Mouse.press( MOUSE_LEFT );
    }else{
      Mouse.release( MOUSE_LEFT );
    }
  }
  if( CheckSpecialKey( &KEYPOS_RIGHT_CLICK, keypos ) ){
    if( isPress ){
      Mouse.press( MOUSE_RIGHT );
    }else{
      Mouse.release( MOUSE_RIGHT );
    }
  }
}
//-----------------------------------------------------------------
void KeyboardPress( str_keypos* keypos ){
  Keyboard.press( keymaps[layer][keypos->row][keypos->col] );
}
//-----------------------------------------------------------------
void KeyboardRelease( str_keypos* keypos ){
  Keyboard.release( keymaps[layer][keypos->row][keypos->col] );
}
//-----------------------------------------------------------------
void KeyboardPressRelease( bool isPress, str_keypos* keypos ){
  if( isPress ){
    KeyboardPress( keypos );
  }else{
    KeyboardRelease( keypos );
  }
}
//-----------------------------------------------------------------
bool CheckSpecialKey( str_keypos* keypos1, str_keypos* keypos2 ){
  if( ( keypos1->row==keypos2->row ) &&( keypos1->col==keypos2->col ) ){
    return true;
  }else{
    return false;
  }
}
//-----------------------------------------------------------------
void SetLayer( void ){
  if( PreviousKeyState[KEYPOS_NUM_SHIFT.row][KEYPOS_NUM_SHIFT.col] == PRESS ){
    SetLayerTo( LAYER_NUM );
  }else if( PreviousKeyState[KEYPOS_FN_SHIFT.row][KEYPOS_FN_SHIFT.col] == PRESS ){
    SetLayerTo( LAYER_FN );
  }else{
    SetLayerTo( LAYER_MAIN );
  }
}
//-----------------------------------------------------------------
void SetLayerTo( int layernum ){
  if( layer!=layernum ){
    layer = layernum;
    Keyboard.releaseAll(); //押したままレイヤー切換えたら押しっぱなしになるので全部離す
  }  
}  
//-----------------------------------------------------------------
void CheckMouseModeKey( bool isPress, str_keypos* keypos ){
  if( CheckSpecialKey( &KEYPOS_MOUSE_SHIFT, keypos ) ){
    if( isPress ){
      flgMouseMode = true;
    }else{
      flgMouseMode = false;
    }
  }
}
//-----------------------------------------------------------------
void SetMouseMove( byte readData ){
  if ( readData & 0b01000000 ){ // 同期
    readMouseDataCount = 0;
  }else{
    readMouseDataCount++;
  }
  readMouseData[readMouseDataCount] = readData & 0b00111111; // 上位2ビット消去
  if( readMouseDataCount==3 ){ // データが揃ったら
    MouseDeltaX = (readMouseData[0]<<2) | (readMouseData[1]>>4);
    MouseDeltaY = (readMouseData[1]<<4) | (readMouseData[2]>>2);
    WheelCount  = (signed char)(readMouseData[2]<<6) |  readMouseData[3];
    Mouse.move( MouseDeltaX, MouseDeltaY, WheelCount ); // カーソルとホイール
  }
}
//-----------------------------------------------------------------
void loop() {
  int CurrentKeyState;
  str_keypos keypos;
  byte readData;
  bool isPress;
    
  // 左手キーボード
  SetLayer();
  for( keypos.row=0; keypos.row<NUM_ROW; keypos.row++ ){
    digitalWrite( PIN_NUM_ROW[keypos.row], LOW ); // keypos.row列目のキー入力を有効にする
    for( keypos.col=0; keypos.col<NUM_COL; keypos.col++ ){
      CurrentKeyState = digitalRead( PIN_NUM_COL[keypos.col] ); // keypos.col行目の値を読む
      if( PreviousKeyState[keypos.row][keypos.col]>1 ){              // 離したばかり
        PreviousKeyState[keypos.row][keypos.col]--;     // チャタリング対策のカウントダウン
      }else if( PreviousKeyState[keypos.row][keypos.col]<0 ){        // 押したばかり
        PreviousKeyState[keypos.row][keypos.col]++;     // チャタリング対策のカウントダウン
      }else if( PreviousKeyState[keypos.row][keypos.col]==RELEASE ){ // 離してから一定時間経過
        if( CurrentKeyState==PRESS ){   // 押した
          KeyboardPress( &keypos );
          PreviousKeyState[keypos.row][keypos.col] = -debounce;
        }
      }else if( PreviousKeyState[keypos.row][keypos.col]==PRESS ){   // 押してから一定時間経過
        if( CurrentKeyState==RELEASE ){ // 離した
          KeyboardRelease( &keypos );
          PreviousKeyState[keypos.row][keypos.col] = debounce;
        }
      }
    }
    digitalWrite( PIN_NUM_ROW[keypos.row], HIGH ); // keypos.row列目のキー入力を無効に戻す
  }
  //右手キーボード&マウス
  if( Serial1.available() ){ // 右手から通信データが来ていたら
    readData = Serial1.read(); // 読む
    if ( readData & 0b10000000 ){ // マウスデータなら
      SetMouseMove( readData );
    }else{ // キーボードキー状態データなら
      isPress    = (readData & 0b01000000) >> 6;
      keypos.row = (readData & 0b00111000) >> 3;
      keypos.col =  readData & 0b00000111;
      CheckMouseModeKey( isPress, &keypos );
      if( flgMouseMode ){ // マウスモード
        MousePressRelease( isPress, &keypos );
      }else{ // キーボードモード
        KeyboardPressRelease( isPress, &keypos );
      }
    }
  }
}

KeyDefine.h
ASCIIベースで作ってしまったが,記事に書いたとおり,Usage IDベースの方が良かった。名前はできるだけQMK firmwareと同じにした。

#define NONE       0x00
#define _______    0x00
#define KC_SPC     32
#define KC_EXLM    33 // !
#define KC_DQOT    34 // "
#define KC_HASH    35 // # 
#define KC_DLR     36 // $
#define KC_PERC    37 // %
#define KC_AMPR    38 // &
#define KC_QUOT    39 // '
#define KC_LPRN    40 // (
#define KC_RPRN    41 // )
#define KC_ASTR    42 // *
#define KC_PLS     43 // +
#define KC_COMM    44 // ,
#define KC_MINS    45 // -
#define KC_DOT     46 // .
#define KC_SLSH    47 // /
#define KC_0       0x30
#define KC_1       0x31
#define KC_2       0x32
#define KC_3       0x33
#define KC_4       0x34
#define KC_5       0x35
#define KC_6       0x36
#define KC_7       0x37
#define KC_8       0x38
#define KC_9       0x39
#define KC_COLN    58 // :
#define KC_SCLN    59 // ;
#define KC_LESS    60 // <
#define KC_EQL     61 // =
#define KC_GRAT    62 // >
#define KC_QSTN    63 // ?
#define KC_AT      64 // @
#define KC_LLBR    91 // [
#define KC_YEN     92 // z
#define KC_RLBR    93 // ]
#define KC_CIRC    94 // ^ CIRCUMFLEX
#define KC_UNDS    95 // _
#define KC_A       0x61
#define KC_B       0x62
#define KC_C       0x63
#define KC_D       0x64
#define KC_E       0x65
#define KC_F       0x66
#define KC_G       0x67
#define KC_H       0x68
#define KC_I       0x69
#define KC_J       0x6A
#define KC_K       0x6B
#define KC_L       0x6C
#define KC_M       0x6D
#define KC_N       0x6E
#define KC_O       0x6F
#define KC_P       0x70
#define KC_Q       0x71
#define KC_R       0x72
#define KC_S       0x73
#define KC_T       0x74
#define KC_U       0x75
#define KC_V       0x76
#define KC_W       0x77
#define KC_X       0x78
#define KC_Y       0x79
#define KC_Z       0x7A
#define KC_LBRC   123 // {
#define KC_PIPE   124 // |
#define KC_RBRC   125 // }
#define KC_TILD   126 // ~

//USB の Usage ID に 0x58 を足したキーコード
                         // Usage ID
#define KC_ENT     0x80  // 0x28
#define KC_ESC     0x81  // 0x29
#define KC_BSPC    0x82  // 0x2a
#define KC_TAB     0x83  // 0x2b
#define KC_ZKHK    0x8d  //全角半角
#define KC_CAPS    0x91  // 0x39
#define KC_F1      0x92  // 0x3a
#define KC_F2      0x93  // 0x3b
#define KC_F3      0x94  // 0x3c
#define KC_F4      0x95  // 0x3d
#define KC_F5      0x96  // 0x3e
#define KC_F6      0x97  // 0x3f
#define KC_F7      0x98  // 0x40
#define KC_F8      0x99  // 0x41
#define KC_F9      0x9a  // 0x42
#define KC_F10     0x9b  // 0x43
#define KC_F11     0x9c  // 0x44
#define KC_F12     0x9d  // 0x45
#define KC_PSCR    0x9e  // 0x46
#define KC_INS     0xa1  // 0x49
#define KC_HOME    0xa2  // 0x4a
#define KC_PGUP    0xa3  // 0x4b
#define KC_DEL     0xa4  // 0x4c
#define KC_END     0xa5  // 0x4d
#define KC_PGDN    0xa6  // 0x4e
#define KC_RGHT    0xa7  // 0x4f
#define KC_LEFT    0xa8  // 0x50
#define KC_DOWN    0xa9  // 0x51
#define KC_UP      0xaa  // 0x52
#define KC_BSLS   223 // \ backslash
#define KC_LCTL    0xf8  // 0xe0
#define KC_LSFT    0xf9  // 0xe1
#define KC_LALT    0xfa  // 0xe2
#define KC_LGUI    0xfb  // 0xe3
#define KC_RCTL    0xfc  // 0xe4
#define KC_RSFT    0xfd  // 0xe5
#define KC_RALT    0xfe  // 0xe6
#define KC_RGUI    0xff  // 0xe7

#define LAYOUT( \
  L31, L41, L32, L42, L33, L43,  L24, L34,             R45, R55,  R36, R46, R37, R47, R38, R48, \
  L21, L51, L22, L52, L23, L53,  L14,                       R65,  R26, R56, R27, R57, R28, R58, \
  L11, L61, L12, L62, L13, L63,  L64, L54, L44,   R35, R25, R15,  R16, R66, R17, R67, R18, R68 \
  ) \
  { \
    { L11, L12, L13, L14, R15, R16, R17, R18 }, \
    { L21, L22, L23, L24, R25, R26, R27, R28 }, \
    { L31, L32, L33, L34, R35, R36, R37, R38 }, \
    { L41, L42, L43, L44, R45, R46, R47, R48 }, \
    { L51, L52, L53, L54, R55, R56, R57, R58 }, \
    { L61, L62, L63, L64, R65, R66, R67, R68 } \
  }

Arduino IDEで日本語キーボードを使う方法

Arduino IDEで日本語キーボードを作ろうとしていたら,キーボードの仕様がいろいろわかりにくかったので、まとめておく。

Arduinoの仕様 ASCIIコード

まず,古く(1963年)からある仕様で,ASCIIコードというのがある。

ASCII - Wikipedia

128個(7ビット)の基本的なキーの番号が振られている。

Arduino IDEのKeyboard.press() などの関数で引数で渡すのはこの番号である。

ただ,今どきのキーボードでは、128個では足りないので、Ctrl,Shift,Alt,Windowsキー,カーソルキー,ファンクションキーなどが128番以降に追加されている。Arduino IDEでは、ASCII以外は↓のように定義されている。www.arduino.cc

 

 日本語キーボード対応方法

上記ページを見てみるとわかるが,Arduino IDEの標準機能では,日本語キーボードには対応しておらず,「¥」「全角/半角」など日本語特有のキーは使えなかったり,配置が違ったりする。

が,すでに解決してくれている人がいる。↓の記事にあるコードをコピーしてKeyboard_jp.hとKeyboard_jp.cppを作成し,Keybord.hの代わりにincludeするだけで,日本語キーボードとして使えるようになる。

mgt.blog.ss-blog.jp

 

 Windows側の設定

普段,英語キーボードを使っていた人は,上記でも,全角/半角キーで日本語に切替えられず,「`」になるなどの問題が起こるかもしれない。これはキーボード側のソフトの問題ではなく,Windows側で英語キーボードとして認識されているためだ。

↓に従ってレジストリをいじったら日本語キーボードとして使える。

Windows 8 でキーボードが英語配列キーボードとして認識されるsupport.microsoft.com

ただし,Windowsでは,複数のキーボードを接続した場合,日本語と英語の切換えは基本的にキーボードごとの設定ではなく,全てのキーボードで同じ設定になるようだ。下記記事↓によると、最近まではキーボードごとに設定できたようだが,追記として書かれているように,Windowsのアップデート後,できなくなっているっぽい。やってみたができなかった。

tyheeeee.hateblo.jp

 

ここまでで問題なければ終わりだが,何かうまくいかない場合やキーコードを追加したい場合などのために,もう少し踏み込んで整理しておく。

USBキーボードの仕様 Usage IDとModifier

最近のキーボードはUSBで繋ぐものが多く、USBの仕様の中にキーボードの仕様が含まれている。Pro MicroもUSBキーボードとして扱われる。USBキーボードでASCIIなどのコードに対応する値は、Usage IDという。

HID Usage Tables for Universal Serial Bus ↓ 10章の表にUsage IDがある。

https://usb.org/sites/default/files/hut1_22.pdf

Arduino IDE の Keyboard.press() などの関数も、結局はUsage IDに変換して送っている。

 Ctrl,Shift,Alt,Windowsキー(左右計8個)は,普通のキーとは別扱いになっており、Modifierと呼ばれる8ビット変数のビットがそれぞれに割り当てられている。

ASCIIコードは文字に対してコードが割り当てられていたが,Usage IDではキーボードのキーに対してコードが割り当てられている。例えばアルファベットのAは、ASCIIコードでは大文字と小文字に別のコードが割り当てられているが、USBキーボードではaのUsage ID+シフトキーのModifierビットとして送られる。

Keyboard.cppを読み解く ASCIIコードとUsage IDの対応

ASCIIコード(基本128個)とUsage IDの変換のマップが,標準のArduino IDEでは,

C:\Program Files (x86)\Arduino\libraries\Keyboard\src フォルダにある Keyboard.cpp の
const uint8_t _asciimap[128]
という配列で定義されている。例えば小文字のaのASCIIコードは97(0x61)なので、配列の98個目の値がaのUsage IDである0x04となっている。引数0から127は,この配列でUsage IDに書き換えて送信している。

引数128から135は、Arduino IDEのASCIIコード拡張でModifier(Ctrl,Shift,Alt,Windowsキー)と定義されており,Modifierのビットを立てて送信している。
引数136以上は単純に136差し引いてUsage IDとして送信している。つまり,ASCIIコードの136番以降にUSBキーボードのUsage IDをそのままの順で全部足したような仕様になっている。上記のKeyboard Modifiers - Arduino Referenceの表を確認すると,136以上のものは対応するUsage IDに136を足した値になっている。ということは,表にないものも,Usage IDに136を足した値を引数で渡せば使えるはずだ! ・・残念,引数がuint8_t型(8ビット)なので,255までしか渡せない。Usage IDで255-136=119 (0x77)までだと,バックスラッシュ(ひらがなの「ろ」のキー) など,日本語特有のキーKeyboard International1 (0x87-)に届かない。

 

Keyboard_jp.cppを読み解く 

上記記事の Keyboard_jp.cpp では,日本語キーボードに対応するために,以下の点を修正している。

  • 配列asciimapを日本語キーボードの配置に置き換え

前述のように,ASCIIコードは文字に対してコードが割り当てられるのに対し,Usage IDではキーに対してコードが割り当てられるため,この配列asciimapで対応関係を修正している。例えば 「&」の文字(ASCIIコード=38(0x26))は,USキーボードでは「7」のキー(Usage ID=0x24)のシフトで入力するが,日本語キーボードでは「6」のキー(Usage ID=0x23)のシフトで入力するので配列の39番目を0x23|SHIFTに変更している。

  • Modifierキーを248-255に移動
  • 引数128-247は0x58=88を引いてUsage IDとして送信

Keyboard_jp.hで定義されているキーコードは,Usage ID + 0x58(=88)になっている。足りないキーがあれば,Usage IDに88を足してここに定義しておけば良い。

Keyboard.cppで 136引いていたところを88に減らしている。これにより,128-88=40 (0x28)番 より前のUsage IDは使えなくなるが,Usage IDの始めの方は,ASCIIコードにもあるので問題ない。その分,後ろのUsage IDまで(247-88=159(0x9F)まで)使えるようになっている。それにより、日本語特有のキーもカバーできている。

 

ここまで理解すると,

  最初からUsage IDを引数にしたらよくね?

ということに気づく。

なぜArduino標準ではASCIIコードを引数にしたのだろう。ASCIIコードを丸暗記していてすぐに脳内変換できる,という人以外はメリットないと思われる。

Keyboard_jp.cpp では,抜かりなくKeyboard.pressRaw()等という関数を用意してくれていて,Usage IDを引数にしてKeyboard.press()等と同じことができるので,そちらを使うべきだろう。

 

マウス内蔵キーボードの作り方(ver.0.4) ハードウェア完成

ハードウェアが完成し、全てのキーがちゃんと入力できることを確認した。
f:id:manva:20210322123752j:plain

前回以降の製作過程を書いておく。
まず、3Dプリンタのフィラメントが無くなったので買い替え。前にブログにも書いた通り,今まで使っていたフィラメント↓が良かったので、また同じのを買うつもりだったが、どうも最近Amazonのレビューの評判が悪くなっているようだ。
【Creality 3D 】3Dプリンター用 (1KG)純正品 PLAフィラメントPLA 寸法精度+/- 0.02 mm、1.75mm 3Dペン用 スプール造形材料 樹脂材料 (ブラック)
ライバルメーカが悪評を書き込ませたりもあるので,まともに信じてはいないが,一つ気になる点があった。前に買ったときには,推奨温度が180〜215℃だったはずなのに,今見ると210〜230℃になっている。材料か作り方か,何かが変わって,そのせいで評判が悪くなったのではないかと疑わしくなってきた。なので別のを選ぶことにした。最終的に↓を選んだ。

Amazonのレビューに書かれていたので承知していた通り,巻きはあまりきれいではない↓のだが,絡まりはしなさそうな印象。
f:id:manva:20210307220705j:plain
使ってみると,デフォルトの設定のままで非常に綺麗にプリントできた。大変満足な出来栄え。オススメできるだろう。(スライサソフトQIDI Printをバージョンアップしたら,ノズル温度のデフォルト設定が前は192℃だったのが202℃に上がっていたので202℃でプリント)
f:id:manva:20210307213132j:plain

その後、192℃でもいろいろプリントしてみたが、それらも問題なし。
底面部品をプリント。
f:id:manva:20210322124716j:plain
ここに、Pro Microを固定するのだが、どう固定するか。いつもはPro Micro Socket↓
Pro Micro ソケット – 遊舎工房
を使っており、右手用デバイスでもそのまま使っていたが、Pro Micro Socketは前の記事に書いたように、TRRSジャックの配線 がQMK firmware用の配線になっているので、Arduino IDEでは使えない。そこで、左手用のPro Micro Socket↓
f:id:manva:20210313175752j:plain
を、↓こうした。
f:id:manva:20210313175823j:plain
ちゃんとしようと思ったら自分で基板作るべきだがとりあえず今回はこれで。

それを底面にネジ止めし、スイッチも配線。
f:id:manva:20210322115130j:plain
相変わらず汚い。もし次また作るとしたら、フレキシブル基板でちゃんとしよう。
その時は、今までのカオスに決着をつけます(!?)

ハードウェアはこれで左右とも製作完。
後はキーのレイヤー機能とかのソフト部分を作る。次回で終劇だな。さらば全ての試作キーボウス。

マウス内蔵キーボードの作り方(ver.0.4) 3Dプリンタのトラブル

右手デバイスができたので,左手デバイスを作っているが,ここに来て3Dプリンタが不調。

最初は変にザラザラになったな、くらいの感じ↓だったが、

f:id:manva:20210223152020j:image

不調のまましばらく使っていたら,次第に悪化して,フィラメントの出が悪くなって,かすれたようになり,エクストルーダから変なノック音がし始めた。たぶんフィラメントを押し出そうとしても硬くて進まないので滑っているような感じ。

色々原因を予想しながら試してみた。

仮説1:冬だからノズルの温度が低くなり,フィラメントが溶けていない

ノズルの温度を上げてみたけどダメなので違う。

仮説2:フィラメントが太すぎ

使っているフィラメントのAmazonのレビューを見ていたら、最初は良かったけどしばらく使っていたら詰まるようになった、というようなことを書いている人がいた。その人は、部分的に仕様の1.75mmより太くなっている説。0.01mmの単位まで表示されるデジタルノギスで測ってみたが、少なくとも私の場合は寸法精度は問題無さそう。(ノギスの計測精度も十分ではないので確信はないが。)

仮説3:ノズル詰まり

3Dプリンタのノズル詰まりは普通によくあることらしく,結局,これが原因だった。プリンタに付いてきた予備のノズルに交換したら治った。

↓ノズルを引っこ抜く。

f:id:manva:20210228001225j:image

交換用の工具も付属しており、交換方法の動画もあったので、メンテナンスはスムーズにできた。フィラメントは温度を上げたら溶けるはずなのになんで詰まるんだろうと思ったが,↓焦げつくからか。なるほど。

3Dプリンタのノズル詰まり 考えられる3つのメカニズム

 

治ったのでプリント再開。

使っている3DプリンタX-Makerに付属のスライサソフトQIDI Printは、プリンタ購入時についてきたのはバージョン5.3.0だったが,メーカのサイト↓でバージョン5.5.2がダウンロードできたので一応アップデートしておいた。

www.qd3dprinter.com

バージョンを上げたら,パソコンが古いせいか,処理が結構重くなった。特に,スライスの計算後に自動的にレイヤ表示に切り替わるようになって,このモードが重い。機能は増えたが,前のバージョンに戻すか微妙なレベル。

 

ついでに、プリンタの設定をいじっていて、サポートの設定を「Tree」(ツリーサポート)にすると、プリントの時間をかなり短縮できそうだったので試してみた。QIDI Printは,CURAがベースらしいのでCURAのツリーサポートも同じようなものと思われる。

左手用デバイスのメイン筐体部分で比較すると,以下のような感じ。

 Nomalの場合 プリント時間:1日21時間37分(ソフトによる予測値。実際はプラス5時間くらいかかった),材料の使用量:173g,スライスの計算時間:1分程度

Treeの場合 プリント時間:1日8時間53分(実際はプラス3時間くらい),材料の使用量:112g,スライスの計算時間:3分程度

スライスの計算時間は長くなるが,プリント時間が半日くらい短縮できるのは嬉しい。プリントした結果は↓

f:id:manva:20210227222005j:image

名前はTreeだが、どちらかと言えばエリンギのような形だ。下の方が細く,上に行くと広がるようなサポートを生成してくれる。Nomalのサポートは数ミリ間隔でみっちり詰まっている感じだったが,Treeだとサポートは筒状で中はほぼ空洞になっているのでサポートのボリュームがかなり少なくなっている。筒状なので剥がすときもごっそり固まって取れて楽↓。

f:id:manva:20210227222026j:image

サポートを剥がした跡の比較↓。左がTree、右がNomalでプリントしたもの。Treeだと剥がした跡も残らず、きれい。Nomalだとサポートがついていたところがきれいには取れず、シマシマの凸凹ができる。

f:id:manva:20210227230801j:image

ただ残念なことに,Treeでは,ところどころ問題が発生している。

下面や穴の横が汚くなっている↓(これはサポートのせいじゃないかもしれないが)

f:id:manva:20210227223445j:image

小指スイッチの枠部分が、プリントの途中でくっつかなくて落下したような感じでずれている↓。これではスイッチが入らないので使えない。

f:id:manva:20210227223028j:image

ツリーサポートは,細かい設定を変えたりして問題が解決できるなら,すごく良いかもしれない。だが,1個作るのに1日半くらいかかるので,いろいろ試すには時間がかかりすぎる。良いところが多かっただけに残念だが、Nomal設定に戻して作り直そう。 

 

マウス内蔵キーボードの作り方(ver.0.4) シリアル通信 右から来たものを左で受け止める

左右分割キーボードで、右手デバイスからの情報を左手デバイスで受取る部分ができた。
シリアル通信の配線はこんな感じ。2台のPro MicroのTXとRXをクロスして繋ぐ。
f:id:manva:20210203184732p:plain
左手デバイスはまだ作ってなくて,Pro MicroにTRRSケーブルのジャックだけをつないだもの。左手用Pro MicroがUSBケーブルでパソコンにつながっていて,右から来たもの(情報)をパソコンに受け流している。電源もパソコンからUSBで供給され,TRRSケーブルで右手デバイスにもおすそ分けしている。

右手デバイスで、キーを押した時と離した時に、左手デバイスに情報を送信する。マウスシフトキーを押している間は、マウス情報も送信する。
キーボード部分に関しては、ほぼ↓の記事と同じ。
オリジナルキーボードを作ってみる その9「シリアル通信の実装」 - ゆかりメモeucalyn.hatenadiary.jp

マウスの情報を送る部分を自分で作らなければいけない。
送りたい情報は、

  1. マウスのイメージセンサから読んだ移動量 MouseDeltaX
  2. マウスのイメージセンサから読んだ移動量 MouseDeltaY
  3. マウスホイールエンコーダのカウント値 WheelCount

の3つで、それぞれ8ビットの情報なのだが、Arduino(Pro Micro)のシリアル通信は1回に8ビットしか送れない。キーボードの情報も送るので、何の情報か、も載せないといけないので、8ビット全部をデータに使うこともできない。以下のような仕様にした。

  • 先頭のビットが0ならキーボード情報、1ならマウス情報(上記記事を書いた方も、マウスを載せることも考えていたようで、これと同じ仕様が書かれている。)
  • キーボード情報の場合、上記記事と同じ。2ビット目は、キーを押したか離したかを示すビット。押した時1,離した時0。3,4,5ビット目が行の番号、最後の6,7,8ビット目が列の番号。
  • マウス情報の場合、上記3つのマウス情報を4回に分けて送る。( 8ビット x 3個 = 6ビット x 4回 = 24ビット )
  • 2ビット目は、4回に分けて送るデータの先頭を表すビット、後ろの6ビットをデータとする。

つまりこんな感じ。
f:id:manva:20210203183305p:plain


通信部分のプログラムは以下のようにした。
TX, RXを使ったシリアル通信は,Arduino IDEでは,Serial1という名前で使えるように用意してくれているので,setup( )の中で,

Serial1.begin( 9600 );

などと書いておくだけで使える。

送る側 (右手デバイス)

void SendMouseMove( byte MouseDeltaX, byte MouseDeltaY, byte WheelCount ){
  byte sendData;

  sendData = 0b11000000 | ( MouseDeltaX >> 2 );
  Serial1.write( sendData );
  sendData = 0b10000000 | ( ( MouseDeltaX & 0b00000011 ) << 4 | MouseDeltaY >> 4 );
  Serial1.write( sendData );
  sendData = 0b10000000 | ( ( MouseDeltaY & 0b00001111 ) << 2 | WheelCount >> 6 );
  Serial1.write( sendData );
  sendData = 0b10000000 | ( WheelCount & 0b00111111 );
  Serial1.write( sendData );
}

受取る側 (左手デバイス)

typedef  struct  {
  int row;
  int col;
} str_keypos;

void loop() {
  str_keypos keypos;
  byte readData;
  bool isMouseShiftKey, isPress;
 ︙
  //左手キーボードの処理
 ︙
  //右手キーボード&マウスの処理
  if( Serial1.available() ){
    readData = Serial1.read();
    if ( readData & 0b10000000 ){ // マウスデータ
      SetMouseMove( readData );
    }else{ // キーボードキー状態データ
      isPress    = (readData & 0b01000000) >> 6;
      keypos.row = (readData & 0b00111000) >> 3;
      keypos.col =  readData & 0b00000111;
      isMouseShiftKey = CheckSpecialKey( &KEYPOS_MOUSE_SHIFT, &keypos );
      SetFlgMouseMode( isPress, isMouseShiftKey );
      if( flgMouseMode ){ // マウスモード
        MousePressRelease( isPress, &keypos );
      }else{ // キーボードモード
        KeyboardPressRelease( isPress, &keypos );
      }
    }
  }
}
//-----------------------------------------------------------------
void SetMouseMove( byte readData ){
  if ( readData & 0b01000000 ){ // 先頭データ
    readMouseDataCount = 0;
  }else{
    readMouseDataCount++;
  }
  readMouseData[readMouseDataCount] = readData & 0b00111111; // 上位2ビット消去
  if( readMouseDataCount==3 ){ // データが揃ったら
    MouseDeltaX = (readMouseData[0]<<2) | (readMouseData[1]>>4);
    MouseDeltaY = (readMouseData[1]<<4) | (readMouseData[2]>>2);
    WheelCount  = (signed char)(readMouseData[2]<<6) |  readMouseData[3];
    Mouse.move( MouseDeltaX, MouseDeltaY, WheelCount ); // カーソルとホイール
  }
}

マウスシフトキーを押している間だけマウスの機能が動作するようにしてみたのだが、使ってみると、マウスポインタは常時動いてくれた方が使いやすそう。マウスシフトキーはキーを左右クリックに切り替えるためだけに使うように修正しよう。

マウス内蔵キーボードの作り方(ver.0.4) 右手用の配線

右手用デバイスを配線し,マウスとキーボードを切替えて使えるようになった。切替え用のキー(「マウスシフトキー」と呼ぶことにする)を押している間はマウスとして使え,離しているときはキーボードとして使える。

右手用デバイスは,24キーあって、基本的に3行8列の並びになっているのだが,3行8列のキーマトリクスにすると3+8=11本のピンが必要であり,マウスと共存させるにはPro Microのピン数があと1本だけ足りなかった。そこで、6行4列の配線にした。こうすればピン数は6+4=10本となり、1本節約できるので、ギリギリ実現できた。RAWピン以外全部使う↓。Arduino(Pro Micro含む)では,アナログ入力のピン(A0~A3)も,pinMode( )で設定すれば普通にデジタル入出力として使える(参照:pinMode() - Arduino Reference )。

f:id:manva:20210117002849p:plain

キーマトリクスについては、別記事に詳しく書いておいた↓。

manva.hatenablog.com

実際の配線は、1キーずつのプリント基板を用意すれば少し綺麗にできるだろうが、今回は空中配線にした。元々3行のキーを折り返して2列セットで電気的には6行1列のように配線するので、繋ぎ方がなおさら複雑になり、↓こんなに汚くなったがとりあえずできた。

f:id:manva:20210117110044j:image

Arduino IDEを使ってキーボード部分のプログラムをした。下記が参考になる。

eucalyn.hatenadiary.jp

ソフトに関してはまた別記事に書こう。

右手用デバイスで確認したいことはこれで完了。

 

続いて、左右分割したデバイス間の通信の開発に移ろうとして調べていると、一つ問題があった。Arduino IDEとQMK firmwareでは,シリアル通信の仕組みが違った。配線から違っていて,Arduino IDEでは,TX(送信)とRX(受信)をクロスして繋ぐ,割と昔ながらの一般的な仕組みなのだが,QMK firmwareではRX同士を繋ぐ1本だけで良いようになっている。

QMK firmwareを使った場合の分割キーボードのつなぎ方↓(他の方法もあるが) Split Keyboard - QMKより

f:id:manva:20210117162016p:plain

前のバージョン(マウス内蔵キーボードの作り方7(キーボードとマウスの融合Keybouse))で使っていたProMicroソケット(Pro Micro ソケット | 遊舎工房)ではこのつなぎ方だったので線は3本でよく,TRRSケーブル(4線)ではなく,TRSケーブル(3線)が使えた。この辺は前バージョンから取り外して流用するつもりだったのだが,Arduino IDEでは3線でのシリアル通信は用意されていないようなので,作り直す必要がある。もうひと頑張りいるな。

 

デジタル入力が足りない場合の対処 ~ キーマトリクス

デジタル入力(DI)ピンが足りない場合、キーマトリクスを使う。検索すれば既に情報がいっぱいあるが、めっちゃ丁寧に説明してみる。

Pro MicroなどのArduinoマイコンでは、プルアップ抵抗が内蔵されており(内部プルアップ),それを使えばスイッチをGNDに繋ぐだけでDIができる。
f:id:manva:20210117234713p:plain

この時点で既にピンとこない人のために、もっとデジタル入出力(DIO)の基礎的な話も別記事に書いておいた。
デジタル入力の基礎的な話 - プルアップとかそんなん - manvaのエンジニアリング魂

では,上記の内部プルアップを用いたアクティブローの回路で,GNDではなく、デジタル出力(DO)に繋いだらどうなるか。DOをスイッチのイメージで描くと,↓のようになる。
f:id:manva:20210117235119p:plain
DOにLowを出力した時にはGNDに繋いだのと同じだが、DOにHighを出力した時、スイッチがオンでもオフでもHighであり、スイッチの操作が無効になる。スイッチ1つだと、無効とか要らんわ、と思うだろうが、この後のキーマトリクスで使う。
キーマトリクス回路とは,↓のようなものである。
f:id:manva:20210120064653p:plain
図は,3行3列の例である。とりあえずダイオードは無視して考えると,それぞれのスイッチが、上記のようなアクティブローの回路になっていて、DOで無効化できるようにしていると見なせる。
この例では、3行x3列で9個のスイッチを入力できる。単純にスイッチを1つずつDIに入力したら9ピン必要だが,この回路ならピンの数はDO3本,DI3本の計6本で済む。同様の配線で、行や列を増やすこともでき、行や列の数が多いほどピン数の節約効果は大きくなる。
わかりやすいように電圧がVccになっている線を赤にする↓。
f:id:manva:20210120064706p:plain
スイッチが全てオフならDOはどれもDIにつながっていないので,DOの状態に関係なくDIは全てHとなる。
また、DOが全てHなら、スイッチが全て「無効」の状態となり,スイッチの状態に関係なくDIは全てHとなる。DOで行ごとに無効化できるため、まず1行目のみを有効にしてDIを読み取り、次に2行目のみを有効にしてDIを読み取り、ということを高速に切替えれば全てのスイッチを読み取れる、という仕組みである。
その時の回路の電圧の状態を詳しく見てみる。DO1のみをLow(有効)にした時、回路の状態は↓のようになる。
f:id:manva:20210120064715p:plain
一番上の行のスイッチのみが有効となる。
ここで、SW1-1をオンにすると↓
f:id:manva:20210120064726p:plain
のようになり、DI1がローとなり、SW1-1が押されたことをマイコンで検出できる。
この図を見てみると、このときに、もしSW2-1やSW3-1も同時におされると、ショートすることがわかる(ダイオードが無ければ)。そうならないためにダイオードを入れている。

Arduino IDEで自作キーボードのプログラムをするなら↓こんな感じ。

void loop() {
    int CurrentKeyState;
    int i, j;

    for( i=0; i<NUM_ROW; i++ ){
        digitalWrite( rowPin[i], LOW );
        for( j=0; j<NUM_COL; j++ ){
            CurrentKeyState = digitalRead( colPin[j] );
            if( CurrentKeyState==LOW && PreviousKeyState[i][j]==HIGH ){
                Keyboard.press( keyMap[i][j] );
            }else if( CurrentKeyState==HIGH && PreviousKeyState[i][j]==LOW ){
                Keyboard.release( keyMap[i][j] );
            }
            PreviousKeyState[i][j] = CurrentKeyState;
        }
        digitalWrite( rowPin[i], HIGH );
    }
}