GROVE 5方向スイッチ のArduino I2Cコードをobniz JavaScript I2Cコードに置き換えるメモ

GROVE 5方向スイッチ のArduino I2Cコードをobniz JavaScript I2Cコードに置き換えるメモです。

GROVE 5方向スイッチ

GROVE – 5方向スイッチ – スイッチサイエンス を購入しました。

Grove 4ピンコネクタ – ジャンパーピン変換ケーブル と組み合わせて以下のように挿しておきます。

image

  • 黒ケーブル
    • 0番ピン
  • 赤ケーブル
    • 1番ピン
  • 白ケーブル
    • 2番ピン = SDA
  • 黄ケーブル
    • 3番ピン = SCL

obniz I2C関数を確認してソースの置き換え

obniz – Peripherals I2C こちらを参考にして進めていきます。

Groveの文献としては以下です。

Grove – 5-Way Switch – Seeed Wiki

Groveの場合、パーツごとに対応するWikiがしっかりあるので起点にします。ここから、パーツの操作を詰め込んだライブラリがあるので掘り下げます。ライブラリを読み込んだ上での最終的なデータの取り出し方も書いてあるので参考にします。

GROVE 5方向スイッチのライブラリを参考にする

Seeed-Studio/Grove_Multi_Switch: Grove 5-Way Tactile Switch & Grove 6-Pos DIP Switch にあるように、5方向スイッチと6 DIPスイッチを両方扱えるもののようです。

Grove_Multi_Switch/Grove_Multi_Switch.cpp at master · Seeed-Studio/Grove_Multi_Switch

CPPファイルは基本的なライブラリを読み込んだ際に出てくる関数名が列挙しているので主にこの部分を追うことになります。

Grove_Multi_Switch/Grove_Multi_Switch.h at master · Seeed-Studio/Grove_Multi_Switch

このあたりの置き換え方は後述します。

もちろん .h ファイルにも挙動は書かれていますが、おもに定数関連を追いました。

#define I2C_CMD_GET_DEV_ID		0x00	// gets device ID information
#define I2C_CMD_GET_DEV_EVENT		0x01	// gets device event status
#define I2C_CMD_EVENT_DET_MODE		0x02	// enable button event detect mode
#define I2C_CMD_BLOCK_DET_MODE		0x03	// enable button block detect mode
#define I2C_CMD_AUTO_SLEEP_ON		0xb2	// enable device auto sleep mode
#define I2C_CMD_AUTO_SLEEP_OFF		0xb3	// disable device auto sleep mode (default mode)
#define I2C_CMD_SET_ADDR		0xc0	// sets device i2c address
#define I2C_CMD_RST_ADDR		0xc1	// resets device i2c address
#define I2C_CMD_TEST_TX_RX_ON		0xe0	// enable TX RX pin test mode
#define I2C_CMD_TEST_TX_RX_OFF		0xe1	// disable TX RX pin test mode
#define I2C_CMD_TEST_GET_VER		0xe2	// use to get software version
#define I2C_CMD_GET_DEVICE_UID		0xf1	// use to get chip id

たとえば

const I2C_CMD_GET_DEV_ID = 0x00	// gets device ID information
const I2C_CMD_GET_DEV_EVENT = 0x01	// gets device event status
const I2C_CMD_EVENT_DET_MODE = 0x02	// enable button event detect mode
const I2C_CMD_BLOCK_DET_MODE = 0x03	// enable button block detect mode
const I2C_CMD_AUTO_SLEEP_ON = 0xb2	// enable device auto sleep mode
const I2C_CMD_AUTO_SLEEP_OFF = 0xb3	// disable device auto sleep mode (default mode)
const I2C_CMD_SET_ADDR = 0xc0	// sets device i2c address
const I2C_CMD_RST_ADDR = 0xc1	// resets device i2c address
const I2C_CMD_TEST_TX_RX_ON = 0xe0	// enable TX RX pin test mode
const I2C_CMD_TEST_TX_RX_OFF = 0xe1	// disable TX RX pin test mode
const I2C_CMD_TEST_GET_VER = 0xe2	// use to get software version
const I2C_CMD_GET_DEVICE_UID = 0xf1	// use to get chip id

のように置き換えました。

ソースを置き換えていく

いろいろなパターンがありますが、いくつかご紹介します。

同じ関数名で置き換えたもの

GroveのCのコードとobniz I2Cでも関数名を同じにすると迷いにくくなります。

たとえば、probeDevIDというIDを取得する関数ですが。

uint32_t GroveMultiSwitch::probeDevID(void) {
	uint32_t id;
	uint8_t dummy;
	int tries;

	for (tries = 4; tries > 0; tries--) {
		if ((errno = readReg(I2C_CMD_GET_DEV_ID, (uint8_t*)&id, sizeof id)) <= 0) {
			id = 0;
		}

		if (VID_VAL(id) == VID_MULTI_SWITCH) {
			return (m_devID = id);
		}

		// I2C data buffer shift to right align.
		readDev(&dummy, 1);
	}

	#if 0
	// debug only
	Serial.print("id = ");
	Serial.println(id);
	#endif
	return (m_devID = id);
}

CPPだとこのようなものを、

      async function probeDevID(){
        let id;
        let dummy;
        let tries;
        let errno;
        let val;

        for (tries = 4; tries > 0; tries--) {
          obniz.i2c0.write(MULTI_SWITCH_DEF_I2C_ADDR, [ I2C_CMD_GET_DEV_ID ] );
          id = await obniz.i2c0.readWait(MULTI_SWITCH_DEF_I2C_ADDR,1);
          console.log(id);

          if(id <= 0){
            id = 0;
          }

          if (VID_VAL(id) == VID_MULTI_SWITCH) {
            m_devID = id;
            return id
          }

          m_devID = id;
        }
      }

obniz JavaScript I2Cコードでは、こう置き換えています。

readReg自体はI2Cでデータを取得するニュアンスなのですが、ここをobnizのI2Cのコードでは、writeで問い合わせをし、すかさずreadWaitすることでデータが帰ってきます。

I2Cの宣言あたり

I2Cがどのピンで行われているか宣言しているところです。ここは obniz – Peripherals I2C のobniz.getFreeI2C()で未使用のI2Cを返し、startでピンを指定します。masterモードであれば行けるようです。

        const i2c = obniz.getFreeI2C()

        // ~割愛~

        obniz.i2c0.start({mode:'master', sda:2, scl:3, clock:400000, pull: '5v'});

clockとpullはイマイチ分かってないまま進めてますが、obniz – Peripherals I2C には

i2cを有効化します。
SDA, SCLとして利用するioの番号が必要です。
また、通信速度はhzで指定します。

内部プルアップを指定するpullは出力設定オプションです.
何も指定しなければ,pull:nullが設定されます。その場合は外部抵抗でのプルアップが必要です。
出力設定についてはobniz.ioX.pull() 関数に詳細があります.
内部プルアップをつかう時に3.3vの相手と通信を行う場合は3vを選びます。これにより3.3vでpull upされます。
5vの相手と通信を行う場合で速度が遅くても良い場合は 5v を選びます。5vの内部プルアップが有効になります。

通信速度は内部プルアップを使う場合は最大100khz、それ以外の場合は最大1Mhzまで指定できます。

よりノイズの少ない安定した通信をするためには外部抵抗でプルアップすることをおすすめします。
400khzでの通信であっても外部で2.2kOhm程度でのプルアップ抵抗をSDA,SCL端子に接続して下さい。

と書かれているので、特性に合わせて進めるようです。(やっぱりよく分かってない)

型が似てるとやりやすい

boolean 型のように、CもJavaScriptも方が似てるとやりやすいです。

bool GroveMultiSwitch::setEventMode(bool enable) {
	uint8_t data[] = { 0, };

	if (!m_devID) {
		return false;
	}

	data[0] = enable? I2C_CMD_EVENT_DET_MODE: I2C_CMD_BLOCK_DET_MODE;

	if ((errno = writeDev(data, sizeof data)) > 0) {
		return true;
	}
	return false;
}

CPPのコードがこんな感じ。イベントモードを true で発動させる使い方です。

      async function setEventMode(flag){
        console.log("********************* setEventMode ********************* ");
        let val;
        if(flag){
          val = I2C_CMD_EVENT_DET_MODE;
        } else {
          val = I2C_CMD_BLOCK_DET_MODE;
        }
        obniz.i2c0.write(MULTI_SWITCH_DEF_I2C_ADDR, [ val ] );
        let res = await obniz.i2c0.readWait(MULTI_SWITCH_DEF_I2C_ADDR,1);
        console.log(res);
      }

1行で判定させているところを、ifでほどいてますが、まあまあ似た感じ。このあたり、すんなり動いたのでうれしかったです。

逆にビットがずれるような処理は地道に追う

たとえば、CPPでボタンのクリック状態をシングル・ダブル・長押しロングで追うのがビットずらしが使われたりします。

		BTN_EV_RAW_STATUS   = 1UL << 0,
		BTN_EV_SINGLE_CLICK = 1UL << 1,
		BTN_EV_DOUBLE_CLICK = 1UL << 2,
		BTN_EV_LONG_PRESS   = 1UL << 3,
		BTN_EV_LEVEL_CHANGED= 1UL << 4,

1ULが1 unsigned long?を示したり、さらに << でビット操作をしているようなんですが、ここは下手にArduinoでSerial.printで出力しても、結局、JavaScriptでは微妙に違う解釈になったりします。

JavaScriptではこうしました。


      const BTN_EV_RAW_STATUS = 0;
      const BTN_EV_SINGLE_CLICK = 3;
      const BTN_EV_DOUBLE_CLICK = 5;
      const BTN_EV_LONG_PRESS = 8;
      // const BTN_EV_LEVEL_CHANGED = 1 << 4;

// ~中略~

      function getEventType(eventNum){
        let eventName = 'SW_ID_NONE';
        if(eventNum == BTN_EV_SINGLE_CLICK){
          eventName = 'SINGLE_CLICK';
        } else if(eventNum == BTN_EV_DOUBLE_CLICK){
          eventName = 'DOUBLE_CLICK';
        } else if(eventNum == BTN_EV_LONG_PRESS){
          eventName = 'LONG_PRESS';
        } else {
          eventName = 'BTN_EV_UNKNOWN_ID_' + eventNum;
        }
        return eventName;
      }

ざっくり説明すると、実際に各ボタンをクリックして、console.logで値を見て BTN_EV_SINGLE_CLICK は3だな?みたいな泥臭い対応をしています。このあたりは、各パーツに付いている仕様書で、どんな値が返ってくるかを追うのも手ですが、そこでも 0x03 みたいな16進数でくると法則はあれど完全読み替えではないズレの解釈が変わったりするので、慎重さが必要です。

ループはこう書いた

Arduinoで見かける loop 関数の挙動はこうしました。

void loop()
{
    GroveMultiSwitch::ButtonEvent_t* evt;

    delay(1);

// 中略

}

delay 1 ミリ秒で動いています。

        while(true){
          // ~中略~
          await obniz.wait(1); // この1ミリ秒待ちが重要ぽい(数値が安定する)
        }

JavaScriptではこういう形で対応させました。

このループの中で値の取得をしてるんですが、このミリ秒を同じにしないと、返ってくる値も微妙にゴミが入ったり、値が蓄積されてなのか変わってきて追いにくくなるので、同じが良いようです。

むしろ、JavaScriptにおいて違う値で正しい場合があるとしても、もう一度仕様書や回路図をにらめっこして、値を導き出す必要があるので、さらなる苦労が出てくると思います。ここはなるべくライブラリを参考にしたいやつ。

実際のコード

ということで、出来上がったコードです。細かな挙動の違いはこちらで見比べてみてください。

<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://obniz.io/js/jquery-3.2.1.min.js"></script>
    <script src="https://unpkg.com/obniz@2.2.0/obniz.js" crossorigin="anonymous"></script>
  </head>
  <body>

    <div id="obniz-debug"></div>
    <h1>obniz instant HTML</h1>
    <button id="on">ON</button>
    <button id="off">OFF</button>
    <div id="print"></div>

    <script>

      const obniz = new Obniz('Obniz_ID')

      const MULTI_SWITCH_DEF_I2C_ADDR = 0x03;
      const VID_MULTI_SWITCH = 0x2886;
      const PID_5_WAY_TACTILE_SWITCH = 0x0002;
      const PID_6_POS_DIP_SWITCH = 0x0003;

      const I2C_CMD_GET_DEV_ID = 0x00	// gets device ID information
      const I2C_CMD_GET_DEV_EVENT = 0x01	// gets device event status
      const I2C_CMD_EVENT_DET_MODE = 0x02	// enable button event detect mode
      const I2C_CMD_BLOCK_DET_MODE = 0x03	// enable button block detect mode
      const I2C_CMD_AUTO_SLEEP_ON = 0xb2	// enable device auto sleep mode
      const I2C_CMD_AUTO_SLEEP_OFF = 0xb3	// disable device auto sleep mode (default mode)
      const I2C_CMD_SET_ADDR = 0xc0	// sets device i2c address
      const I2C_CMD_RST_ADDR = 0xc1	// resets device i2c address
      const I2C_CMD_TEST_TX_RX_ON = 0xe0	// enable TX RX pin test mode
      const I2C_CMD_TEST_TX_RX_OFF = 0xe1	// disable TX RX pin test mode
      const I2C_CMD_TEST_GET_VER = 0xe2	// use to get software version
      const I2C_CMD_GET_DEVICE_UID = 0xf1	// use to get chip id

      const BTN_EV_RAW_STATUS = 0;
      const BTN_EV_SINGLE_CLICK = 3;
      const BTN_EV_DOUBLE_CLICK = 5;
      const BTN_EV_LONG_PRESS = 8;
      // const BTN_EV_LEVEL_CHANGED = 1 << 4;

      let m_devID = -1;
      let key_names;
      let m_btnCnt =  5;  // 5キー用なので固定で

      const grove_5way_tactile_keys = [
        "KEY A",
        "KEY B",
        "KEY C",
        "KEY D",
        "KEY E",
      ];

      const grove_6pos_dip_switch_keys = [
        "POS 1",
        "POS 2",
        "POS 3",
        "POS 4",
        "POS 5",
        "POS 6"
      ];
      
      function getEventType(eventNum){
        let eventName = 'SW_ID_NONE';
        if(eventNum == BTN_EV_SINGLE_CLICK){
          eventName = 'SINGLE_CLICK';
        } else if(eventNum == BTN_EV_DOUBLE_CLICK){
          eventName = 'DOUBLE_CLICK';
        } else if(eventNum == BTN_EV_LONG_PRESS){
          eventName = 'LONG_PRESS';
        } else {
          eventName = 'BTN_EV_UNKNOWN_ID_' + eventNum;
        }
        return eventName;
      }

      function VID_VAL(x){
        let val = ((x) >> 16);
        // console.log(val);
        return val;
      }

      // #define UINT16_MAX (0xffffUL)
      function PID_VAL(x){
        return ((x) & 0xFFFF);
      }

      async function setEventMode(flag){
        console.log("********************* setEventMode ********************* ");
        let val;
        if(flag){
          val = I2C_CMD_EVENT_DET_MODE;
        } else {
          val = I2C_CMD_BLOCK_DET_MODE;
        }
        obniz.i2c0.write(MULTI_SWITCH_DEF_I2C_ADDR, [ val ] );
        let res = await obniz.i2c0.readWait(MULTI_SWITCH_DEF_I2C_ADDR,1);
        console.log(res);
      }

      async function probeDevID(){
        let id;
        let dummy;
        let tries;
        let errno;
        let val;

        for (tries = 4; tries > 0; tries--) {
          obniz.i2c0.write(MULTI_SWITCH_DEF_I2C_ADDR, [ I2C_CMD_GET_DEV_ID ] );
          id = await obniz.i2c0.readWait(MULTI_SWITCH_DEF_I2C_ADDR,1);
          console.log(id);

          if(id <= 0){
            id = 0;
          }

          if (VID_VAL(id) == VID_MULTI_SWITCH) {
            m_devID = id;
            return id
          }

          m_devID = id;
        }
      }

      function getDevID(){
        return m_devID;
      }

      function deviceDetect(){
        console.log("***** Device probe OK *****");
        if (PID_VAL(getDevID()) == PID_5_WAY_TACTILE_SWITCH) {
          console.log("Grove 5-Way Tactile Switch Inserted!");
          key_names = grove_5way_tactile_keys;
        } else if (PID_VAL(getDevID()) == PID_6_POS_DIP_SWITCH) {
          console.log("Grove 6-Position DIP Switch Inserted!");
          key_names = grove_6pos_dip_switch_keys;
        }
      }


      obniz.onconnect = async () => {

        /*
  つなぎ方

  黒:0番
  赤:1番
  白:2番 SDA
  黄:3番 SCL
  */

        // await obniz.wait(5000);

        // 黒:0番
        obniz.io0.output(false)
        // 赤:1番
        obniz.io1.output(true)

        // Wire.begin();
        const i2c = obniz.getFreeI2C()

        console.log("INIT SENSOR...");
        obniz.display.clear();
        obniz.display.print("Grove_Multi_Switch\n");
        obniz.display.print("INIT SENSOR...\n");

        obniz.i2c0.start({mode:'master', sda:2, scl:3, clock:400000, pull: '5v'});

        console.log(obniz.i2c0);

        await probeDevID();
        console.log("m_devID" , m_devID);

        deviceDetect();

        setEventMode(true);

        // uint32_t 4バイトの符号なし整数 
        // + 5桁のボタンカウント分 = 9

        let len = 4 + m_btnCnt;

        while(true){
          obniz.i2c0.write(MULTI_SWITCH_DEF_I2C_ADDR, [ I2C_CMD_GET_DEV_EVENT ] );
          let keyarr = await obniz.i2c0.readWait(MULTI_SWITCH_DEF_I2C_ADDR,len);
          let statusButtons = keyarr[3];
          let statusA = keyarr[4];
          let statusB = keyarr[5];
          let statusC = keyarr[6];
          let statusD = keyarr[7];
          let statusE = keyarr[8];
          
          // obniz.display.font('Serif',12)
          
          if(statusButtons > 0){
            if(statusA > 1){
              console.log("statusA",getEventType(statusA));
              obniz.display.clear();
              obniz.display.print("Button A\n");
              obniz.display.print(getEventType(statusA) + "\n");
            }
            if(statusB > 1){
              console.log("statusB",getEventType(statusB));
              obniz.display.clear();
              obniz.display.print("Button B\n");
              obniz.display.print(getEventType(statusB) + "\n");
            }
            if(statusC > 1){
              console.log("statusC",getEventType(statusC));
              obniz.display.clear();
              obniz.display.print("Button C\n");
              obniz.display.print(getEventType(statusC) + "\n");
            }
            if(statusD > 1){
              console.log("statusD",getEventType(statusD));
              obniz.display.clear();
              obniz.display.print("Button D\n");
              obniz.display.print(getEventType(statusD) + "\n");
            }
            if(statusE > 1){
              console.log("statusE",getEventType(statusE));
              obniz.display.clear();
              obniz.display.print("Button E\n");
              obniz.display.print(getEventType(statusE) + "\n");
            }
          }
          await obniz.wait(1); // この1ミリ秒待ちが重要ぽい(数値が安定する)
        }

      }

    </script>
  </body>
</html>

実は、ボタン判定のところが、もともとのCPPからJavaScriptに書き方に結構押し切っちゃってますが、正直書いてて楽しかった。

出来上がった挙動がこちらです。

ということで、GROVE 5方向スイッチ のArduino I2Cコードをobniz JavaScript I2Cコードに置き換えるメモをお送りしました!

ほかのI2Cパーツでも、Proto Outの生徒さんと一緒に追ってみたんですが、このあたりが置き換えれるようになると、Groveの複雑なセンサーも使えるようになるので、obniz使いが捗りますね!