キノコの自省録

日々適当クリエイト

シアーハートアタックの状態遷移設計

ラズパイで作ったシアーハートアタックの状態遷移がどうなっていて、どう実現しているのかというソフトウェアサイドのお話です。

ソフトウェアの話はいいかーと思っていたのですが、そりゃ自分がソフト屋だからかなあと思い直して文章を書いた次第。

シアーハートアタックについてはこちらをどうぞ。

kinokorori.hatenablog.com

状態遷移概要

図にしました。その方が説明早いので。

f:id:kinokorori:20170320203916p:plain

図の通り、全部で4状態です。

停止状態

ジッと黙っている状態です。

かといって何もしていないわけではなく、室温とターゲット温度の差分を取って、熱源がないかチェックしています。

 

発見状態

熱源を検知している状態です。モーター回して前進します。

前進中も、熱源チェックは継続します。

なお、停止状態から遷移した場合は、LEDランプを光らせて、「コッチヲ見ロッ!」と喋ります。

 

ロスト直後

前進中に熱源をロストした状態です。1秒程度静止します。

その間に熱源を再検知した場合は、そのまま発見状態へ戻ります。

完全にロストした場合は、探索状態へ遷移します。

 

探索

超信地旋回で、熱源を探します。

発見したら発見状態へ、旋回しても見つからなければ、停止状態に移行します。

ソフト設計

上記の通り、そんな複雑ではないです。が、それでも適当にif文で書くと、めちゃくちゃ苦労します。

状態管理と言えばStateパターン。ということで、Stateパターンを使って実装しました。

Stateパターンの親クラスは、こんな感じで実装してました。

STATE_IDLE = 0
STATE_LOST = 1
STATE_LOSTTURN = 2
STATE_FIND = 3
STATE_STAY = 255
    
class SuperState:
    #static
    thermo_ = None

    # 初期化メソッド
    # @param mystate_id 自分のステート番号
    def __init__(self, mystate_id):
        self.mystate_ = mystate_id

    # 自Stateに切り替わった時に呼び出されます
    # @param prev_state 直前のState
    def enterState(self, prev_state):
        pass
        
    # 状態をチェックして、次に遷移すべきStateを決定します
    # @return 次State (STATE_STAYの場合は据え置き)
    def check(self):
        return STATE_STAY
    
    # 自分のステートIDを返します。
    def mystate(self):
        return self.mystate_
    

このステートを保持している側の動作は、以下のようになっています。

def main():
    state_ = state.StateIdle()
    state_.enterState(state.STATE_IDLE)
    
    while(True):
        current_state = state_.mystate()
        next_state = state_.check()
        if next_state == state.STATE_STAY:
            time.sleep(0.5)
            continue
        elif next_state == state.STATE_FIND:
            state_ = state.StateFind()
        elif next_state == state.STATE_LOST:
            state_ = state.StateLost()
        elif next_state == state.STATE_LOSTTURN:
            state_ = state.StateLostTurn()
        elif next_state == state.STATE_IDLE:
            state_ = state.StateIdle()
            
        state_.enterState(current_state)
            
        time.sleep(0.10)

やっていることは非常に単純です。

現在のステートクラスに対して、次に何のステートにするかをチェックさせます(→checkメソッド)。

もし、STATE_STAYならばそのまま継続。別のステートなら、その対応ステートクラスをインスタンス化して、enterStateを呼び出します。

この動作を延々と繰り返しているだけです。

各ステートクラスに次状態の脳みそを持たせているため、上記main()関数内は、機械的にステートIDに対応するインスタンスを生成するだけになっています。

この仕組みに従って、具象Stateクラスを実装すればいいだけです。

例えば、発見状態(STATE_FIND)なら、こんな疑似コードになります。

def enterState(self, prev_state):
    # if prev_stateがSTATE_IDLEなら: LED点灯、コッチヲ見ロッと発声
    # モーターを前進モードにして回転

def check(self):
    # if 室温と赤外線温度が所定値未満: return STATE_LOST
    # そうでなければそのまま継続→ return STATE_STAY 

シンプル。2つのメソッドを実装すればいいだけです。

後はモーター制御だったり、LED点灯だったり、温度センサーだったりのアクセス手段を用意すれば終わり。

 

センサー系制御やると、状態管理が結構必要とされることが多いので、Stateパターンを使うと大変楽になれると思います。

Raspberry piを乗っけて超信地旋回する

この前作ったシアーハートアタックは、熱源探索するために、その場で回転する動作をプログラムしています。

kinokorori.hatenablog.com

キャタピラの旋回

そもそもキャタピラで旋回ってどうやるのか、これ作る前は知らなかったんですが、片側のキャタピラを止めて軸回転する信地旋回と、キャタピラを互いに逆方向に回転する超信地旋回(極地旋回)の2通りがあるんですね。

超信地旋回をするには

要するに、超信地旋回するには、片方を順方向、片方を逆方向に回転するよう、モータ制御をすればいいわけです。

今回はそのモータ制御を、DRV8835というモータドライバを使って制御しました。秋月なら1つ300円で買えます。秋月の受け取りが面倒くさいなら、Amazonでも600円くらいしますが売ってます。

配線

配線はこんな感じです。

f:id:kinokorori:20170318001509p:plain

ざっくり左側がラズパイ、右側がモーターです。モーターの方は見たまんまです。モーター用の電源と、モーターA, Bの2つを接続しています。VMMは今回は使っていません。モーターAを右車輪用、モーターBを左車輪用にして、片方を順回転、もう一方を逆回転させて超信地旋回を行います。

ラズパイは、3.3VとGND、それからGPIOへ4本接続しています。どこでもいいのですが、私は17,27,22,23を使いました。

MODEは、HIGHを入れるとモータ制御がPHASE+ENABLEに、LOWにするとPWM制御になります。今回はHIGHを入れます。ENABLEはモーターをそもそも回すか回さないか、PHASEはHIGH/LOWで順回転、逆回転が変わります。なので、超信地旋回するためには、ENABLEを両モーターともHIGH, PHASEは片方のモーターをHIGH, 片方のモーターをLOWにすれば超信地旋回ができます。

ENABLEを片方止めると信地旋回になります。

超信地旋回の注意点

結構パワーが要ります。ギア比に注意。

スピード重視のギア比にすると、ボディーが重いと全く旋回しなくなります。

また、履帯が外れたり、脱輪したりすることがあります。

動画

ボディーはタミヤのユニバーサルプレートに、ツインモーターボックスとタンク工作基本セットを使用。

さらに、ラズパイとモータードライバー、モーター用電池、ラズパイ用電源(リポバッテリ充電器)が乗ってます。

youtu.be

MLX90614をRaspberry piで使う

前回紹介したシアーハートアタックの熱源探知には、赤外線温度センサーを使用しています。

赤外線温度センサーは、非接触で対象物の温度を測ることのできる温度計です。

kinokorori.hatenablog.com  

今回使用したのは、MelexisのMLX90614という赤外線温度センサーです。

正確に測定するには5cmくらいの距離じゃないとダメですが、

多少離れていても、「なんか熱源がある」くらいは検出できます。

例えば、50cmくらい離れた場所に人がいた場合、室温よりは1℃くらい高くなります。

(レンズを使用したりすると、もっと伸びるかもしれません。)

3.3V(5V品もあるので注意), I2Cで制御可能ということもあり、ラズパイでもArduinoでも扱いやすい。

検出温度のレンジは-40~125℃ほど。

 

ただ、赤外線温度センサーって高いんですよね。

MLX90614は単価2200~3000円くらいです。壊したら泣きます。

ちなみに私は千石電商で2800円で買いました。

評価ボード付きだと7000円くらいですが、素で十分扱えます。

 

Amazonでは、ノーブランド品が\690で売ってるみたいです。

ふーん、これでもよかったかもしれない。

色々調べると、結構安値のものがありますね。ムムム。

例えば、多くの電子部品を安値で販売しているHiLetGoも、赤外線温度センサーを販売している模様。\430也

ちなみに、赤外線温度センサーには、4x4とか8x8とか、点ではなく2Dでデータを取れるものがあります。

こういう2Dセンサーをアレイセンサーと呼んだりします。値段は少々高めです。

買いやすいところではOMRONのD6TとかGrid-Eyeとか。

下の画像はOMRONのD6T-L44です。Grid-EyeはRSオンラインから買えます。

とりあえず、MLX90614の話に戻ります。

Raspberry piで扱う

ラズパイで扱う、というよりI2Cでどう扱うかという話がメインになります。

脚が4本で、Vdd, GND, SCL, SDAと、実に普通のI2Cです。

対応するラズパイのGPIOピンに差すだけです。SlaveAddrは0x5a。

$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- 5a -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --                         

ここまではいいのですが、MLX90614のプロトコルはちょっとした癖があるので、適当にreadを試みても失敗します。

データの読み込み

データシート(これがまためちゃめちゃ分厚い)の8.4.3.1.1あたりですかね。

こんな風に書いてあると思います。

1      7        1
S|Slave Address|Wr|..(略)

これの意味するところは、Slave Addressは7bitで書いてください、ということです。

0x5a=0101 1010を表現するには、1bit左にシフトして、1011 0100=0xb4としなければなりません。

読み込みデータは、8.4.3.1.1にも、以下の文章、

If the access to the MLX90614 is a read operation it will respond with 16 data bits and 8 bit PEC only if its own slave address,

にも書いてある通り、Data-High, Data-Low, PECの3octetです。

The PEC is a CRC-8 with polynomial X8+X2+X1+1. T

ということで、PECはCRC-8です。誤り制御用なので、ホビー用途なら捨てても構わないと思います。

一度に3Byte読む必要があるので、SMbusのread_i2c_block_dataで読み込みましょう。

レジスタアドレスは0x7なので、

bus = smbus.SMBus(1)
t = bus.read_i2c_block_data(0xb4, 0x7, 3)

で読み込めます。なお、アドレス0x6を読むと、外気温が取れます。

シアーハートアタックでは、0x7と0x6の差分を取って熱源判定をしています。

熱源に向かって自走するシアーハートアタックを作った

ジョジョの奇妙な冒険第4部に登場する、遠隔操作型のスタンド「シアーハートアタック」を自作しました。

シアーハートアタックとは

  • 吉良吉影のスタンド「キラークイーン」の左手から発射される爆弾戦車
  • 脚はキャタピラ
  • 熱源に向かって自動追尾する
  • 丸っこい爆弾型ボディーの前面に猫型の頭蓋骨が配置されている
  • 「コッチヲ見ロッ」と喋る

というシロモノです。

爆発は無理ですが、

  • 熱源に向かって自走する
  • キャタピラで駆動
  • 「コッチヲ見ロッ」と喋る

この辺の設定を守って製作してみました。

youtu.be

造形は初めてなので、かなりボッコボコです。 これでもやっているうちに作り方を覚えて、大分マシになりました。

ユーザーストーリーマッピング - Jeff Patton

今年読んだ本の紹介その2。
ユーザーストーリーマッピング - Jeff Patton
https://www.amazon.co.jp/dp/4873117321
素晴らしい本です。超おすすめしておきます。


これは、アジャイル開発手法である、”スクラム”の最初に行う「ユーザストーリー(マッピング)」の解説です。
スクラムのわずか1プロセスについて、なんと1冊の本にしたということです。


基本哲学 - 完璧なドキュメント(要件定義など)は存在しない

0章ではっきり書いています。かなり痛快に。
ソフトウェア開発に携わったことがある人なら、要求とか要件とかという名の妖怪について、みんな一言あるでしょう。
要件定義書というのは、作るべきものが書いてあるものの、本当に作るべきものが十分書かれてはいません。
しかし、間違えたものを作ったら、誰かのせいになります。
それは仕様屋さんだったり、今までの慣習を知らないプログラマーだったり。


語りえぬものについては、沈黙しなければならない

そもそも論として、必要なもの全てが言語化できるわけではありません。
「可愛いキャラクター」がなぜ可愛いか、を言葉で説明できないように、言語化には限界があります
言外の意味に重要なファクターが多分に含まれることは、なんとなくイメージつくと思います。
しかし、言語化したときに、ごっそり抜け落ちます。
論理の外にあるものは、言語化できません。

※この辺りはamazon:リファクタリング ウェットウェアを読むと良いでしょう。


非言語要素をドキュメントで書き表せないのであれば、別の方法を取る必要があります。
それは、「共通理解を作る」ことです。
共通理解=共有ドキュメントではありません。
共通理解とは、関係者みんなが頭の中に思い描く完成図です。


では、共通理解とは、どのように構築すればよいのでしょうか?
その手法の一つが、本書のタイトルである「ユーザーストーリーマッピング」です。


本書のカバー範囲

ユーザーストーリーとは、簡単に言うと、
「プロダクトが描き出すストーリーを語ること」です。
ユーザーストーリーマッピングとは、
「ユーザーストーリーをポストイットでマップ化すること」です。
どんどん会話して、一緒にストーリーマップを作りましょう、というのがコンセプトです。
あくまでも主役はストーリーです。ポストイットマッピングのための補助ツールです。
ポストイットは言語化されたものなので、その段階で情報が落ちます。)
作られたマップは、プロダクトのバックログとして活用されます。


トーリーを語れといっても、様々な切り口があったり、その成果物の使い方があったりします。
また、良いストーリーとは、という側面でも考えるべきところがあります。
このようなストーリーにかかわる諸々が、300ページの本の中身となっています。
僅かアジャイル開発の前段階の1プロセスで300ページです。
ちなみに、「スクラム実践入門」という本では、
関連しそうなページを足しても、ユーザーストーリーについての説明は5ページくらいです。


逆に、ユーザーストーリーマッピングにはポストイットを使用しますが、
それ以外の点については、betterは説明していますが、mustにはしていないような印象を受けます。
(極端な悪手にはNoを出していますが)
メソッドにとらわれるより、ストーリーを語りましょう。


最後に

感銘を受けた言葉を。

私はスコープ(仕事の範囲)がクリープ(知らない間にふくらむ)することはないと思っている。単に理解が進んだだけだ。

その通りだと思います。

フーリエ変換のナイキスト周波数についてもう少し

音(声)の高低を検出する方法を真面目に書く
http://d.hatena.ne.jp/kinokorori/20130328/1364469421

この記事に最近ナイキスト周波数について記述を追加しました。
……追加しましたが、ちょっとだけのつもりが長くなった割に説明不足で、
なんだかよくわからなくなりました。なので、再度書き直しします。


ナイキスト周波数

ナイキスト周波数とは、サンプリング周波数の半分の周波数のことです。
一般的なwav形式のサンプリング周波数44100Hzならば、22050Hzということです。
これが、FFTで正しく検出できる、最大の周波数となります。


つまり、検出したい周波数があるならば、その倍のサンプリング間隔が必要ということです。
これを標本化定理と言います。
標本化とはサンプリングのことですので、サンプリング定理とも言います。


折り返し雑音

44100Hzでは話がしづらいので、もう少し周波数を下げてお話しします。
20Hzのサンプリング間隔ですと、ナイキスト周波数は10Hzになります。
では、ここに15Hzの周波数成分を含む信号が入ったときはどうなるでしょうか?

標本化定理により、15Hzは正しく検出できない、というのは上述の通りです。
15 - 10 = 5Hz分、折り返して跳ね返ってきます。
つまり、10 - 5 = 5Hzの信号に重畳されてしまいます。
本当は5Hzじゃないのに、5Hzの信号として扱われるので、ノイズとして顕在化します。
これを折り返し雑音(エイリアシング)と言います。


折り返し雑音を避けるため、高周波成分は、
サンプリング前にローパスフィルタでカットするということが良く行われるようです。


図解

「15Hzが5Hz?意味わからん。三角関数だし円運動だから折り返してきそうなのはなんとなくわかるが。」
みたいな感想を抱くことでしょう。
このナイキスト周波数は”標本化”定理、つまり連続信号を離散信号に落とした時に発生します。
アナログ世界ではなく、デジタルです。インテグラルではなくシグマです。

図を見ると一発でわかります。
下の図は、1秒間に100個のサンプリングを行った場合(つまりサンプリング周波数100Hz)に、
5Hzと15Hzのサイン波を図示したものです。


点をつないで連続波形にしているので、100Hzということがわかりにくいですが、
離散表現にすると、下の図のようになります。

この棒が立っているところだけが、実際に観測された(デジタル化された)値です。
見た通り、5Hzと15Hzは、明らかに別物だということがわかります。
当たり前だろ何言ってんだこいつレベルです。


さて、サンプリング間隔を1秒間に100個から、1秒間に20個に落としてみましょう。
すなわち、サンプリング周波数20Hzです。
ナイキスト周波数10Hzなので、15Hzは正しく検出できないはずです。
では、まずは5Hzのサイン波から。

次は15Hzのサイン波です。

なんと、15Hzに見えません。5HzをX軸で反転させたように見えます。
これを線で滑らかに補間して図示すると、こんな感じになります。

原信号は15Hzにも関わらず、完全に5Hzの信号に見えます。位相が反転しているだけです。


同じように、12Hzでも考えてみましょう。
ナイキスト周波数10Hzなので、2Hz折り返してきます。
つまり8Hzと12Hzが同じに見えるはずです。
まずは8Hzです。


次に12Hz。


やはりX軸で反転したグラフになります。


これらの図の通り、ナイキスト周波数を上回る周波数の信号は、正確に復元できないことがわかります。
口で説明しても、なんだかわかったようなわからないような感じがしますが、
実際に図示すると割とイメージが掴みやすい現象だと思います。
数学的に、数式からも証明できます(私ができるとは言ってない)。