実際に作ったもの
https://github.com/kichinosukey/vertical-compass
なぜ作ったか
microbitの地磁気センサーから算出される角度はmicrobitのロゴマークを水平方向に向けた状態であることを前提に、北を0度とし時計回りに0°~359°を出力する仕様だと思われる。
https://makecode.microbit.org/reference/input/compass-heading
https://microbit.org/ja/projects/make-it-code-it/compass-bearing
https://microbit.org/ja/projects/make-it-code-it/compass-north
それはそれで構わないのだが、今後はmicrobitを垂直方向に固定した状態で方角算出を想定しているためこの前提を変えた状態でも角度算出をできるようにする必要があった。
何をしたいのか
インスピレーションをもらったのは下記の動画。
今年6月に実施したプログラミング講座では「超音波センサを使って迷路を解くことができるマックイーンをつくりたい。」と言う要望を参加者からもらっていたこともあり同じようなことにチャレンジしている人がいないか探してみた結果、上記の動画を見つけたと言うわけだ。
どうやって解決できそうか
どうやら2軸の地磁気センサーの場合はアークタンジェント(atan2)を使った計算で角度を求められるらしい。
とはいえ、調べるだけだとよくわからない。とりあえず、2軸の磁力測定値とatan2による計算で現状microbitで実装されている角度算出のロジックに近い計算結果が出ていれば納得できそうな気がする。その後、microbitを垂直方向に固定した場合の算出の考え方を整理する。
現状のmicrobitの実装と磁力測定値からの算出結果を比較する
現状、makecodeエディターではユーザーが特別な実装をすることなく地磁気センサから得られたデータを用いた角度算出を非常に簡単なUI(入力 -> 方角ブロック)で提供している。
https://microbit.org/ja/projects/make-it-code-it/compass-north
検証方法としてはその計算結果と磁力測定値からの算出結果をシリアル出力で比較してみようと考えた。
実施結果: atan2による算出結果と近い結果が得られた
下記プロットの上段が「角度」ブロックのプロット、中断がXYの磁束測定データをアークタンジェントを使って算出したもの。波形はほぼ同じ。下段が両者の差分をとっているが大きくズレはなさそうに見える。

下記は上記のために書いたコード、atan2の結果はラジアンなので180/piを乗じてやる必要がある。加えて計算結果は東を0°として反時計回りに回る想定らしいので、toCompassLikeにて北を0°として反時計回りにする処理を入れている。
let d = 0
let c_d = 0
let c_d_like = 0
basic.forever(function () {
d = input.compassHeading()
c_d = Math.atan2(input.magneticForce(Dimension.Y), input.magneticForce(Dimension.X)) * (180 / Math.PI)
c_d_like = toCompassLike(c_d)
serial.writeValue("heading_builtin", d)
serial.writeValue("calc_xy_like", c_d_like)
serial.writeValue("diff", d - c_d_like)
basic.pause(1000)
})
function toCompassLike(deg: number) {
deg = deg % 360
if (deg < 0) deg += 360
return (90 - deg + 360) % 360 // E基準→N基準へ
}
垂直固定時の角度算出方法
垂直の場合はatan2(y, x) -> atan2(z, x)に変換する
さて本番はここからで、先ほどの結果はmicrobitを水平に置いてあることが前提であった。これからは垂直に固定した場合について考えていきたい。先ほどまでは地磁気をx, y軸で拾っていた。microbitを垂直に固定する場合を考えると、先ほど拾えていたのと同等の地磁気を拾うためにはx軸は変わらずでy軸の代わりにz軸を使えばいいことになる。
水平に置くときは「左右(X)+前後(Y)」でOKだが、垂直にすると、前後の情報は Y軸ではなく垂直(Z軸) に切り替わるイメージになる。だから X と Z を使って atan2(Z, X)
で角度を出すということ。万人にわかりやすい説明は難しく、それは自身の理解不足ゆえだと思うのでここは改善したいところ。
さらに北0°にmicrobit表面(LED側)を合わせたい
加えて、このままではmicrobitを垂直方向に固定した時の0°はmicrobitの裏面(センサー取り付け面)を北向きにした時になってしまう。これは元々がmicrobitの表面(LED面)を仰向けにした状態で角度を測定しており、これをそのまま垂直方向に起こすとなるとその流れで裏面が北にならざるを得ないからだ。実際にはatan2の計算結果は東を0°として反時計回りに増えていくため先述のコードtoCompassLikeによる基準変換をかける処理を追加しているが。
microbitを垂直に固定する -> atan2(z, x)で東から反時計回りの角度を算出する -> 北側から時計回りの角度に変換する -> 北側0°に表面を合わせるため、東西南北の判定ロジックを反転する。と言うのが大まかにやるべきことになる。
上記を踏まえて作ったコードがこちら。
let a = 0
let letter = ""
let d = 0
let c_d = 0
let c_d_like = 0
let c_v_d = 0
let c_v_like = 0
// E=0°, CCW → N=0°, CW へ
function toCompassLikeWithSense (a: number) {
a = a % 360
if (a < 0) {
a += 360
}
return (90 - a + 360) % 360
}
// XZ(垂直)での atan2 の回り方
input.onButtonPressed(Button.A, function () {
input.calibrateCompass()
})
// 北基準(N=0°, CW)の角度から方角を表示
function showCardinal (deg: number) {
if (deg >= 0 && deg < 23 || deg >= 338 && deg < 360) {
letter = "S"
} else if (deg >= 67 && deg < 113) {
letter = "W"
} else if (deg >= 157 && deg < 203) {
letter = "N"
} else if (deg >= 247 && deg < 292) {
letter = "E"
} else {
letter = ""
}
basic.showString(letter)
return letter
}
// ★ まずは +1 で試す。逆回りなら -1 にする。
basic.forever(function () {
// 参考:micro:bit内蔵の傾き補正済みヘディング(N=0°, CW)
d = input.compassHeading()
// 水平(XY): 標準は atan2(Y, X) で E=0°, CCW
c_d = Math.atan2(input.magneticForce(Dimension.Y), input.magneticForce(Dimension.X)) * 180 / Math.PI
if (c_d < 0) {
c_d += 360
}
c_d_like = toCompassLikeWithSense(c_d)
// 垂直(XZ): ★Zの符号反転は一旦やめるのがコツ(まず素直に試す)
c_v_d = Math.atan2(input.magneticForce(Dimension.Z), input.magneticForce(Dimension.X)) * 180 / Math.PI
if (c_v_d < 0) {
c_v_d += 360
}
c_v_like = toCompassLikeWithSense(c_v_d)
showCardinal(c_v_like)
serial.writeValue("heading_builtin", d)
serial.writeValue("c_v_like", c_v_like)
basic.pause(500)
})
動作イメージ
とりあえず動くようにはなった。