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 …とは?
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-bufferaddress: 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だけですね。どっち使ってもいいように、両方に送ってるってことかな。
…と、少々の謎を残しつつ、ひとまずはソース読み切りました!