PLEN:bit:モーションデータ

ダンスなどのモーションデータは、PLEN:bit 本体のEEPROMに入っています。
0番のモーションを取得して、モーション再生のロジックを確認してみます。

ソースに、EEPROMからのデータ取得の関数が用意してあります。export されているので、ユーザプログラムからも呼び出し可能ですね。

// 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
}

読み出しのアドレスと、読み込みバイト数(最大43)を引数に入れると、データの配列を返してくれます。

モーション読み出しのアドレスは

let readAdr = 0x32 + 860 * [モーション番号]

で計算できます。モーション番号0番(左ステップ)のデータだと、0x32ですね。そこから860バイト読み込んでみます。

let listLen = 0
let readAdr = 0
input.onButtonPressed(Button.A, function () {
  readAdr = 50 + 860 * 0
  listLen = 43
  for (let i = 0; i < 20; i++) {
    let mBuf = plenbit.reep(readAdr, listLen)
    for (let index = 0; index <= listLen - 1; index++) {
      serial.writeNumber(readAdr + index)
      serial.writeString(":")
      serial.writeNumber(mBuf[index])
      serial.writeLine("")
    }
    readAdr = readAdr + listLen
   }
})

Aボタンを押した時に、43バイト * 20回 で 860バイトを 読み込んで、読み込んだものを[アドレス] : [値] でシリアルに出力するプログラムです。

ブロックだとこうなります。

「plenbit.reep」は、定義されている関数名のままで表示されました。

では、ターミナルと接続して、実行!

読めてますね。いい感じです。(ごくたまーに、取りこぼしでデータが欠けることあったので受信結果はチェックしてください)

データは、255(0xFF)が出るまでが有効値です。0番モーションの場合、アドレス265から 0xFFになりましたので、アドレス50~アドレス264までの215バイトになります。

データは文字列データとして入っているみたいです。アスキーコードに従って変換すれば、62(0x3E) は「>」、77(0x4D)は「M」という感じ。

全体だとこうですね。

読みやすく成形するとこういう感じ。

>MF0000006400000000000000000000000000000000
>MF000100c800000000fe1f00e8000000000000022b
>MF00020064000000000000fe1f00000000014e01e1
>MF000300c8000000000000fdd50000000001e1ff18
>MF0004006400000000000000000000000000000000

“>” がコマンド開始。”MF”は決まりのヘッダ。

次の2バイト”00″が、モーション番号。

次の2バイト”00″が、フレーム番号?かな?2行目だと”01″、3行目だと”02″になってます。

次の4バイト”0064″が、モーションにかける時間。

残り4バイト*8が、各サーボの値となります。サーボの値は、標準値からの変化値が入ってます。
1行目は全部0なので、標準位置。
2行目は、4バイトずつ区切ってみると、

0: 0000
1: 0000
2: fe1f
3: 00e8
4: 0000
5: 0000
6: 0000
7: 022b

となります。2(左手)、3(左足首)、7(右足首)を動かしてます。0x7FFF を超える値の場合、0x10000をマイナスして、マイナス値とします。

10進にしてみると、0xFE1F = 65055、0x10000 = 65536 なので  -481、0xE8 = 232 という感じです。角度は10倍で指定されているので、実際は -48度と23度になるようです。

これで取得したデータを、1行ずつ、setAngle(data, time); で実際にサーボに反映します。

export function setAngle(angle: number[], msec: number) {
  let step = [0, 0, 0, 0, 0, 0, 0, 0];
  msec = msec / motionSpeed;//now 15//default 10; //speedy 20 Speed Adj
  for (let val = 0; val < 8; val++) {
    let target = (servoSetInit[val] - angle[val]);
    if (target != servoAngle[val]) { // Target != Present
      step[val] = (target - servoAngle[val]) / (msec);
    }
  }
  for (let i = 0; i <= msec; i++) {
    for (let val = 0; val < 8; val++) {
      servoAngle[val] += step[val];
      servoWrite(val, (servoAngle[val] / 10));
    }
    //basic.pause(1); //Nakutei yoi
  }
}

( msec / motionSpeed )回使って、角度を現在の角度から指定された角度に変更します。

motionSpeed は

let motionSpeed = 15;

と定義してありました。

let step = [0, 0, 0, 0, 0, 0, 0, 0];

step配列で、1回で何度動くかを各サーボごとに計算して保持しています。

let target = (servoSetInit[val] - angle[val]);

動く先の角度は、基準の角度から指定された角度を引いたものになります。イメージ的に逆な感じではありますが…。

if (target != servoAngle[val])

servoAngle配列で、現在のサーボの値を保持しています。もし変化先が同じ角度だったら、何もしない。

step[val] = (target - servoAngle[val]) / (msec);

違ってた場合は、1回で動く角度を設定します。動き先の角度から現在の角度を引いたものが、トータルで動くべき角度。これを msec 回 で動かすので、1回で動かすべき角度は上記の式の通り。角度の値を10倍で保持してるのは、ここでの割り算の誤差を減らすためですね。

servoAngle[val] += step[val];
servoWrite(val, (servoAngle[val] / 10));

msec 回ループを回し、毎回、角度に step[val]; を加算して、その値をサーボに書き込み。


モーションデータのフォーマットとそれによるサーボコントロールの仕組みが理解できました!