PLEN:bit:拡張機能のソースを読む

PLEN:bit のベース部分が、micro:bit とどういう通信をしているか気になりました。どのポートでどの仕組みで何を通信しているのか。このへん、記載されている資料が見つけられなかったので、拡張機能のソースから追ってみようと思います。

確認時のバージョンは Ver.0.0.3 です。その後、バージョンアップされていますので、以下とは内容が変わってるかもしれないです。


micro:bit への PLEN:bit 機能追加は、以下のプログラムです。

plenprojectcompany/pxt-PLENbit
https://github.com/plenprojectcompany/pxt-PLENbit

プログラム本体は pxt-PLENbit/plenbit.ts ですね。TypeScriptで記述してあります。

このあたりのドキュメントも参考にしつつ、見ていきます。

Documentation
https://makecode.microbit.org/docs


最初は、この表示の定義です。

//% weight=100 color=#00A654 icon="\uf085" block="PLEN:bit"

//%の行は、MakeCodeで解釈されるっぽいですね。

namespace plenbit {

namespace は plenbit

最初に、export enum で、モーションなどの定数を定義してます。

export enum StdMotions {
  //% block="Walk Forward"
  WalkForward = 0x46,
  //% block="Walk Left Turn"
  WalkLTurn = 0x47,

ブロックの表示が “Walk Forward”、対応する値が 0x46 ということですね。

日本語表示時の表示内容は、こちらのファイルで定義されています。

https://github.com/plenprojectcompany/pxt-PLENbit/tree/master/_locales/ja

"plenbit.StdMotions.WalkForward|block":"前に進む",

プログラムの中で、一番重要となるのは、micro:bit と外部ハードウェアの通信部分。 write8 関数に描かれています。

function write8(addr: number, d: number) {
    let cmd = pins.createBuffer(2);
    cmd[0] = addr;
    cmd[1] = d;
    pins.i2cWriteBuffer(0x6A, cmd, false);
}

名称から、通信にはI2Cを使ってるみたいです。pins …とは?

入出力端子
https://makecode.microbit.org/reference/pins

pinsで入出力関連の命令が使えるらしい。

i2c Write Buffer
function i2cWriteBuffer(address: int32, buf: Buffer, repeat?: boolean): int32;
https://makecode.microbit.org/reference/pins/i2c-write-buffer

Create Bufferで、出力用のバッファを作成して、それを i2c Write Buffer で指定したアドレスに出力。

パラメータは

address: the 7-bit I2C address to read the data from.
buffer: a buffer that contains the data to write to the device at the I2C address.
repeated: if true, don’t send a stop condition after the write. Otherwise, a stop condition is sent when false (the default).

アドレスと、送るバッファと、連続して送るかどうか。

アドレスは 0x6A 固定ですね。これがPLEN:bit 本体の i2c アドレスになるのかな。

送信するコマンドバッファは、関数のパラメータで付けられたアドレス(アドレス同じ名称でややこしい)と、値。

i2c の通信は、すべてこれを通すと思うので、あとはパラメータのアドレスが何を表すのかがわかれば!


それでは、write8 がわかったところで、改めて処理を上から見ていきます。

export function secretIncantation() {
    write8(0xFE, 0x85);//PRE_SCALE
    write8(0xFA, 0x00);//ALL_LED_ON_L
    write8(0xFB, 0x00);//ALL_LED_ON_H
    write8(0xFC, 0x66);//ALL_LED_OFF_L
    write8(0xFD, 0x00);//ALL_LED_OFF_H
    write8(0x00, 0x01);
}

初期化の処理です。「secretIncantation」=「秘密のおまじない」でしょうか…。これはまあ、何かしら初期化されてる、ぐらいの認識でいいでしょう。


次の行から、ブロックの定義になっています。

//% blockId=PLEN:bit_Sensor
//% block="read sensor %num"
export function sensorLR(num: LedLr) {
  let neko = 0;
  if (num == 16) {
    neko = AnalogPin.P2;
  } else {
    neko = AnalogPin.P0;
  }
  return pins.analogReadPin(neko);
}

センサーの値取得です。LedLr の値は、以下が定義されています。

export enum LedLr {
  //% block="A button side"
  AButtonSide = 8,
  //% block="B button side"
  BButtonSide = 16
}

Aボタン側が 8、Bボタン側が 16です。

Aボタン側が引数で指定されたら、AnalogPin.P2の値を返し、Bボタン側が指定されたら、AnalogPin.P0の値を返します。

ピンの位置はこちら。

micro:bit pins
https://makecode.microbit.org/device/pins

AnalogPin.P0 / AnalogPin.P1 / AnalogPin.P2 は、大きい端子でした。前部のセンサーはここに繋がってるんですね。

センサーの3本のピンは、向かって左から順に 信号 / 3.3V / GND に接続されてました。


向いている方角の角度を取得。

//% block
export function direction() {
  return Math.atan2(input.magneticForce(Dimension.X), input.magneticForce(Dimension.Z)) * 180 / 3.14 + 180
}

地磁気センサーの値を元に、コンパスの角度を取得します。


個別のサーボ制御。

//% blockId=PLEN:bit_servo
//% block="servo motor %num|number %degrees|degrees"
//% num.min=0 num.max=11
//% degrees.min=0 degrees.max=180
export function servoWrite(num: number, degrees: number) {
  if (initPCA9865 == false) {
    secretIncantation();
    initPCA9865 = true;
  }
  let highByte = false;
  let pwmVal = degrees * 100 * 226 / 10000;
  pwmVal = Math.round(pwmVal) + 0x66;
  if (pwmVal > 0xFF) {
    highByte = true;
  }
  write8(servoNum + num * 4, pwmVal);
  if (highByte) {
      write8(servoNum + num * 4 + 1, 0x01);
  } else {
      write8(servoNum + num * 4 + 1, 0x00);
  }
}

サーボ番号と、その角度を指定します。initPCA9865 は初期化フラグです。初期化されてなければ初期化処理を実行。

各サーボによって、write8 で書き込むアドレスが決まっているようです。

let servoNum = 0x08;

で定義されている 0x08 がサーボ0、そこから、各サーボ 4 バイトずつでアドレス確保されているみたいです。
サーボ0だと、0x08 + 4 = 0x0c になりますね。

実際に書き込む値は、指定された角度から計算しています。

let pwmVal = degrees * 100 * 226 / 10000;

角度が 0 ~ 180 なので、書き込む値は 0~406…?でいいのかな?なんか中途半端ですけれど。
16進で言えば 0x00~0x196 になります。

(servoNum + num * 4) に、16進の下2桁の値を、(servoNum + num * 4 + 1) に、16進の上1桁の値を書き込んでいます。上一桁は、 0x00~0x196  なので、0x00 か 0x01 どっちかになりますね。

0x196 の場合は、(servoNum + num * 4) に 0x96 を書き込み、(servoNum + num * 4 + 1) に 0x01 を書き込んでいます。


基本 / サッカー / ダンス モーション動作

//% blockId=PLEN:bit_motion_std
//% block="play std motion %fileName"
export function stdMotion(fileName: StdMotions) {
  motion(fileName);
}
//% blockId=PLEN:bit_motion_Soc
//% block="play soccer motion %fileName"
export function soccerMotion(fileName: SocMotions) {
  motion(fileName);
}
// blockId=PLEN:bit_motion_box
// block="play box motion %fileName"
export function boxMotion(fileName: BoxMotions) {
  motion(fileName);
}
//% blockId=PLEN:bit_motion_dan
//% block="play dance motion %fileName"
export function danceMotion(fileName: DanceMotions) {
  motion(fileName);
}
// blockId=PLEN:bit_motion_m
// block="play move motion %fileName"
export function moveMotion(fileName: MoveMotions) {
  motion(fileName);
}

全部同じですね。与えられた値で、motion関数を呼んでいるだけです。

数が合わないですね。BoxMotions と MoveMotions  に該当するブロックが画面にありません。
BoxMotions には、export enum BoxMotions で、ブロック名が付けられてないので、画面には出てこないっぽいです。
MoveMotions には一つもモーションが登録されてないので、これも画面には出てこないです。


モーション番号指定でのモーション動作

これが、PLEN:bit として一番重要な関数じゃないかと思います。上の「基本 / サッカー / ダンス モーション動作」も、これを読んで動作しています。以下、関数です。長いです!

//% blockId=PLEN:bit_motion
//% block="play motion number %fileName"
//% fileName.min=0 fileName.max=73
export function motion(fileName: number) {
  let data = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
  let command = ">";//0x3e
  let listLen = 43;
  let readAdr = 0x32 + 860 * fileName;
  //serial.writeNumber(fileName)
  //serial.writeString(",fileName")
  //serial.writeNumber(readAdr)
  //serial.writeString(",adr")
  let error = 0;
  while (1) {
    if (error == 1) {
      break
    }
    let mBuf = reep(readAdr, listLen);
    readAdr += listLen;
    if (mBuf[0] == 0xff) {
      break
    }
    let mf = ""; //=null ?
    for (let i = 0; i < listLen; i++) {
      let num = mBuf.getNumber(NumberFormat.Int8LE, i);
      mf += numToHex(num);
    }
    //serial.writeString(",Nonull")
    let listNum = 0;
    while (listLen > listNum) {
      if (command != mf[listNum]) {
        listNum += 1;
        continue
      } //serial.writeString(",>OK")
      listNum += 1; // >
      //serial.writeString(mf[listNum]);
      //serial.writeString(mf[listNum] + 1);
      if ("mf" != (mf[listNum] + mf[listNum + 1])) {
        //if (0x4d != (mf[listNum])) {
        listNum += 2;
        continue
      } //serial.writeString(",mfOK")
      listNum += 2; // MF
      //if (fileName != int((_mf[listNum] + _mf[listNum + 1]), 16)) {
      if (fileName != parseIntM(mf[listNum] + mf[listNum + 1])) {
        error = 1;
        break
      }
      //serial.writeString(",fileOK")
      listNum += 4;// slot,flame
      let times = (mf[listNum] + mf[listNum + 1] + mf[listNum + 2] + mf[listNum + 3])
      let time = (parseIntM(times));
      listNum += 4;
      let val = 0;
      while (1) {
        if ((listLen < (listNum + 4)) || (command == mf[listNum]) || (24 < val)) {
          setAngle(data, time);
          break
        }
        let num = (mf[listNum] + mf[listNum + 1] + mf[listNum + 2] + mf[listNum + 3]);
        let numHex = (parseIntM(num));
        if (numHex >= 0x7fff) {
          numHex = numHex - 0x10000;
        } else {
          numHex = numHex & 0xffff;
        }
        data[val] = numHex;
        //serial.writeNumber(data[val]);
        //serial.writeString(",")
        val = val + 1;
        listNum += 4;
      }
    }
  }
}

長かった…!

引数で与えられたモーションのデータを EEPROM から読んで、そのデータに従って、各サーボを制御しています。

そう、モーションデータは、PLEN:bit 本体の EEPROM に入ってるみたいなんですよ。てっきり、micro:bit 側のデータとして保持しているのかと思った。これ、今後のモーション追加は、できないってことなのかな…。EEPROMを書き換える方法があるのだろうか。
→ Ver.0.04で、「function weep(eepAdr: number, num: number)」が追加されてました。これを使うと EEPROM 書き込みができると思われます。

PLEN / PLEN2 には、Motion Editor というのが用意されてて、それでモーションの作成や更新が出来たっぽいんだけど、ハードウェア構成も違ってるから、そのままでは使えなさそうですね。

let listLen = 43;
let readAdr = 0x32 + 860 * fileName;

これで読出し先のアドレスを決定。”fileName”変数には、モーション番号が入っています。

let mBuf = reep(readAdr, listLen);

これで、EEPROM の指定アドレスから、データの読み込み。43 バイトずつ読み込んでいます。

モーションの処理は、長くなりそうなので、もうちょっと確認してから、別エントリで書こうと思います。


ROMからのデータ読出し reep

// blockId=PLEN:bit_reep
// block="readEEPROM %eepAdr| byte%num"
// eepAdr.min=910 eepAdr.max=2000
// num.min=0 num.max=43
export function reep(eepAdr: number, num: number) {
  let data = pins.createBuffer(2);
  data[0] = eepAdr >> 8;
  data[1] = eepAdr & 0xFF;
  // need adr change code
  pins.i2cWriteBuffer(romAdr1, data)
  let value = (pins.i2cReadBuffer(romAdr1, num, false));
  return value
}

blockId 書いてあるので、ブロックになってたっけ?と思ったら、’//%’ではなかったので、単なるコメントでした。開発時にはブロックにしてたのかな?

i2c Read Buffer
https://makecode.microbit.org/reference/pins/i2c-read-buffer

address: the 7-bit I2C address to read the data from.
size: the number of bytes to read into the buffer from the device.
repeated: if true, don’t send a stop condition after the read. Otherwise, a stop condition is sent when false (the default).

読み込み元のI2Cアドレスと、読み込みサイズ、連続してアクセスするかどうか。

let romAdr1 = 0x56;

読み込み元I2Cアドレスは 0x56です。

読み込み元ROMアドレスを data 配列で指定。data[0] に上位バイト、data[1]に下位バイトを入れます。

pins.i2cWriteBuffer(romAdr1, data)

で読み込み元ROMアドレスを指定して、

let value = (pins.i2cReadBuffer(romAdr1, num, false));

で num バイトを読み込み。num の MAX 値は 43 なのかな?ハードウェア的な制限かも?


BLEでスマホから接続。

これは、スペシャルキットだけのやつだと思います。

頭部パーツに、BLEユニットが付いてて、BLEシリアルでスマホと接続できる機能が付いているみたいです。

この頭部パーツだけ、部品で販売してくれないかなー…。


サーボモータ初期値設定

これ実行すると、PLEN:bit が素立ちに戻ります。重要命令!

サーボの初期値はこう定義されています。

let servoSetInit = [1000, 630, 300, 600, 240, 600, 1000, 720];

角度が10倍の値で入ってるみたいです。けっこう細かい値になってますね。微調整の結果かな…。

servoWriteで、各サーボをその値に設定。

//% block="servo motor initial"
export function servoInitialSet() {
  //setAngle([0, 0, 0, 0, 0, 0, 0, 0], 1);//motionSpeed//num=1000
  let sNum = 0;
  servoWrite(sNum, servoSetInit[sNum] / 10);
  sNum++;
  servoWrite(sNum, servoSetInit[sNum] / 10);
  sNum++;
  servoWrite(sNum, servoSetInit[sNum] / 10);
  sNum++;
  servoWrite(sNum, servoSetInit[sNum] / 10);
  sNum++;
  servoWrite(sNum, servoSetInit[sNum] / 10);
  sNum++;
  servoWrite(sNum, servoSetInit[sNum] / 10);
  sNum++;
  servoWrite(sNum, servoSetInit[sNum] / 10);
  sNum++;
  servoWrite(sNum, servoSetInit[sNum] / 10);
}

(なんで for 使わないのかな…とかちょっと思った。)


サーボの力を抜きます。

//% block
export function servoFree() {
  //Power Free!
  write8(0xFA, 0x00);
  write8(0xFB, 0x00);
  write8(0xFC, 0x00);
  write8(0xFD, 0x00);
  write8(0x00, 0x01);
  //write8(0x00, 0x80);
  initPCA9865 == false
}

これも、おまじない的ですね。初期化フラグもOFFされます。


目のLED制御

//% block="eye led is %onoff"
export function eyeLed(ledOnOff: LedOnOff) {
//if (led_lr == 8) {
pins.digitalWritePin(DigitalPin.P8, ledOnOff);
//}
//if (led_lr == 16) {
pins.digitalWritePin(DigitalPin.P16, ledOnOff);
//23 or 15
//}
}

ON / OFF の定義はこちら。

export enum LedOnOff {
  //% block="on"
  On = 0,
  //% block="off"
  Off = 1
}

OFFの方が1なので気を付けて!

DigitalPin.P8 と、 DigitalPin.P16 の2つに信号送ってますね。これ、もしかして、右目と左目では?と思って、片方だけにセットしてみたけど、そうではなかったです。DigitalPin.P16 側だけが有効っぽいです。

うーん?なんで2つセットしてるんだろう?

見てみたら、他の用途で使われていないデジタルの空き、P8とP16だけですね。どっち使ってもいいように、両方に送ってるってことかな。

…と、少々の謎を残しつつ、ひとまずはソース読み切りました!