Take’s diary

Macとマイコンに関すること--ワクワクの製作日記

Intel Edisonを遠隔操作 「Edisonがボール認識!!」 (Rapiro用Edisonキャリアボード+SimpleCV+flask)

 ESP-WROOM-02の記事が続いたので、今日はまたEdisonに戻って、ESPには到底できないようなことを実行してみました。今回はWebでEdisonを操作したり、ボール認識させた動画を配信させたりしてみます。

 f:id:TAKEsan:20160309194421j:plain:w280 f:id:TAKEsan:20160309194420j:plain:w280
左は白い円を認識している。右はOMURON非接触赤外線センサーで16か所の温度を測っている。切り替えはボタン操作)
          
      実際に動かしている動画です。画像にモアレができてますけど、動作は結構早い

 Edisonからの動画配信は、過去の記事により、SimpleCVで実現できています。
takesan.hatenablog.com
OpenCVが簡単に実行できたり、図形を合成してMjpeg配信できたりしてかなり面白いのですが、これだけではEdisonからの一方通行に過ぎません。今回は、クライアント側で動画を表示させながらEdisonを操作することに挑戦してみます。

まず材料

 Edison,USB外部機器が使えるEaglet、i2cセンサー(今回は制御が簡単なOMURON非接触赤外線センサー。電源を5Vから取れば一応動きます)、安いWebCamera、LED(フローラ用のだと抵抗付きなので実験用に最適)今回の材料はカメラ以外全部スイッチサイエンスで手に入れてます。接続は赤外線センサーが4本(SDA,SCL,GND,5V)。LEDは当然2本(+側はラピロボードのIO12(mraaでは20番に指定),GND)。カメラはUSBに差し込むだけです。
f:id:TAKEsan:20160307213444j:plain:w300f:id:TAKEsan:20150809111402j:plain:w200
      左:Rapiro用Edisonキャリアボード 右:OMURON非接触赤外線センサー

f:id:TAKEsan:20160307213445j:plain:w200f:id:TAKEsan:20160307213446j:plain:w300
            floral用抵抗付きLED
 今回はPython主体なので実験用に最適な、Rapiro用Edisonキャリアボードを使いました。理由は、「適度な大きさ、レベルシフターが必要無い(信号は全て3.3V出力)、電源出力が3,3Vと5V、カメラが使える、デジタル・アナログピンが使える。」です。そしてSDカードも外部記憶ディスクとして使えるのもGood!!。とりあえずピンヘッダを半田付けしてから。  
          f:id:TAKEsan:20160307213447j:plain:w300
 Edisonは他のEagletボードに載せ替えOKなので、小さくまとめたいのならカメラが使える、Intel Edison Block - Base+αとかHenryとかが最適です。Edison本体の差し替えは、コネクタの保証範囲である30回以下になっていますが、慎重に抜き差しすれば(私のは30回の保証範囲を軽く超えている)なんとか持つようです。ただし、くれぐれもroot以下のバックアップを怠りなく。そんな場合でもこのボードは、面倒な設定が不要で、/media/sdcardにコピーするだけ。ただし、i2c6が使えないのでArduinoIDEからだとi2cが操作できませんが、デジタルが6本(内PMW2本)****I2S2_FS-->10、I2S2_CLK-->13、I2S2_TXD-->11、I2S2_RXD-->12、IO12-->3 PWM、IO13-->5PWM****使用可能、さらにシリアルが使えます。Arduinoに慣れている方は、私の1月8日の記事とは違う発想で、プロセス間通信を使って時間が掛かる処理をArduino側で実行させるといった様な面白そうなことができそうです。このボードはEDISON FOR PYTHON ボードといった方が良いかも。Pythonからはmraaで全ての端子が使えますが、ピン番号は回路図を見れば分かります。ちょと分かりにくいのですが、ラピロボードの回路図からEdisonのGPIOピン名を調べて、Intel(R) breakout boardの対応表からmraa番号を探します。(例:IO12->GP12->20。mraaの対応表中Pinmode1によりmraa20番はPWMであることが分かります)
https://s3-ap-northeast-1.amazonaws.com/switch-science.public/schematic/Edison_Carrier_Board_for_Rapiro/Edison_Carrier_Board_for_Rapiro%282%29.pdf
mraa: Intel Edison

まずしなければいけないこと

 目標は、ボタンとスライダ操作それに動画の表示Python上で実現するために、いろいろ調べて、簡単そうな「flask 」を使うことにしました。とにかくflaskを使ってSimpleCVのMjpeg動画を送れれば完成したようなもんです。
 じゃーHtmlの知識もないのにどうしたらいいの? 例によってサンプルを徹底的に探しました。その中でも一番簡単に実現できそうなそうなサンプルがこれ。
python - How to send a pygame image over websockets? - Stack Overflow
 この場合はpythonOpencv=cv2を使って、SimpleCVの画像を送ってるようなのですけど、cv2(PythonOpencv)独自のCamera関数は使えないみたい。SimpleCVのCamera関数は簡単に認識するのに不思議。どうやらSimpleCVのCamera関数は、Pygameを使ってる様なので、原因を探るのはまた今度にします(多分Pygameインストールの時のvideo.hあたり)。
 まーそんなわけで、2日かかってSimpleCVのMjpeg動画がflaskで表示できるようになりました。カメラ表示部分の変換ソースは、bottol他のライブラリでも使えそうです。SimpleCV標準コマンドで画像配信するより、スピードが多少早いような気がします。変換部分が出来上がってみるとまー簡単だこと!!。Html部分を省いて、たった5行で出来ちゃいました。とても2日掛かりの労作とは思えませんよね。Html部分も私の能力に合わせて最小限にしました(詳しい方は笑ってください)。

あっそうだ。この「Eaglet」ラピロの頭だったんだ。

 このボードをラピロの頭に入れると、画像の分析、顔認識、さらには画像の加工までできて動画配信可能。おまけに配信先からはラピロを自由に動せる。アイディアがどんどん膨らみます。pi3が発売されたそうですがpi3にすればもっと早くなる。でも電源が!!てな時、Edisonは未だに強〜い味方です。
          f:id:TAKEsan:20160307213443j:plain:w300
 ただ、今回のスライダー操作ではにタイムラグがあるので、さらなる改良が必要。

SimpleCvについて

 この頃Edison向きにそこそこのスピードで画像処理するコツがつかめてきました。画像を分析する(例えば赤や青や白のボールを判断、顔認識するなど)には、よほど細かいものでない限り180X120ドット程度で十分なんです。でもそのままではWeb画面で表示すると画像が甘いので、実用性とEdisonの処理能力を考慮した限界である倍の解像度、すなわち320X240が許容範囲。これを内部的に半分にして処理すると、ロボットなどに使える実用スピードになります。最初のサンプル動画を見てください。これPythonインタプリタですよ。今回この部分もソースに書きましたので是非参考にしてみてください。

ソースの実行について

 まずflaskのインストール。その前にyoctoをクリーンインストールしてSimpleCVをインストールしておくこと。それから
pip install flask
今回追加するライブラリのインストールは、これだけです。

 そしてflaskの要求する実行用のディレクトリを作ります
◯◯◯◯◯/test.py
◯◯◯◯◯/templates/index.html
◯◯◯◯◯は好みのディレクトリとする。

 実行方法は、python main.py >/dev/null 2>&1 でプログラム起動。Mac又はiPhoneのサファリから ◯◯◯◯◯.local:5000 で画面が表示されます。
(>/dev/null 2>&1 の分は、普通にPythonを実行させると、main.pyの実効後SimpleCVからのlogが際限なく画面に表示されるので、余計な表示をストップさせるため)

test.py index.htmlは、以下のソース。
知識のある方はjavascliptを使うなりどうぞご自由に。

以下test.py

#!/usr/bin/env python
import gc
from flask import Flask, render_template, Response,request, redirect
from SimpleCV import *
from cStringIO import StringIO
import mraa
import time
gc.disable()
#************  UI   **************
atai="0.5"
show="Show"
on="ON"
#************PWM-LED**************
x = mraa.Pwm(20)
x.period_us(700)
x.enable(True)
#************  i2c  **************
D6T_addr = 0x0A
D6T_cmd =  0x4C
y = mraa.I2c( 1 )
#***********Main******************

cam=Camera(0,{ "width":320, "height":240 }) 
app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html',pwm="value="+atai,show=show,on=on)
    print atai
def gen(): 
    global show,on
    tdata=[]
    while True:
        if show == "Hide":        #*******Ball find********
            image=cam.getImage() 
            image.drawText("White Ball", 5, 5,(255,255,255),fontsize=30)
            img1 = image.scale(0.5)
            dist=img1.colorDistance((0,0,0))
            segmented=dist.stretch(225,225)
            blobs=segmented.findBlobs()
            if blobs:
                 circles=blobs.filter([b.isCircle(0.3) for b in blobs])
                 if circles:
                      image.drawCircle((circles[-1].x*2,circles[-1].y*2),circles[-1].radius()*2,(255,0,0),2)
        elif on == "OFF":         #********OMURON**********
            image=cam.getImage()
            y.address(D6T_addr)
            y.writeByte(0x4C)
	    z=y.read(35)
            for var in xrange(0, 16):
                   tdata.append((z[(var*2+2)]+(z[(var*2+3)]<<8))/float(10))
            for j in xrange(0,4):
               for i in range(0,4):
                   image.drawText(str(tdata[i+j*4]),20+i*80,15+j*60,(255,94,25),fontsize=30)
            tdata=[]
        else:
            image = cam.getImage()
        fp = StringIO()           #********Mjpeg**********
        image.save(fp, 'JPEG')
        frame=fp.getvalue()
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n\r\n')

@app.route('/vid')
def video_feed():
    return Response(gen(),mimetype='multipart/x-mixed-replace; boundary=frame')

@app.route('/contact', methods=['POST'])
def contact():
    global atai,on,show,x
    if request.method == 'POST':
        if request.form['submit'] == 'Show':
            show="Hide"
            on="ON"
        elif request.form['submit'] == 'Hide':
            show="Show"
       	elif request.form['submit'] == 'OFF':
       	    on="ON"
        elif request.form['submit'] == 'ON':
            on="OFF"
            show="Show"
        elif  request.form['range'] != '300':
            atai=request.form['range']
            xx=float(atai) 
            x.write(xx)
    return redirect('/')

if __name__ == '__main__':
    app.run(host='0.0.0.0')

以下 index.html

<html>
<font face="Chalkduster">
  <head>
    <title>Edison Test!!</title>
  </head>
  <body  onLoad="test()" bgcolor="#E7E8E2">
    <h1>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Edison Test!!</h1>
    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img id="bg" src="{{ url_for('video_feed') }}" width="320" height="240" />
    <form action="/contact" method="post">
      <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;BALL-->&nbsp;<input style="color:#000000;background-color:#CCEEFF 
;font-size:15;width:140px;height:30px" type="submit" name="submit" value={{show}}></p>
      <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TEMP  -->&nbsp;&nbsp;<input 
style="color:#000000;background-color:#00ccff;font-size:15;width:140px;height:30px" type="submit" name="submit" 
value={{on}}></p>
      <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PWM--->&nbsp;&nbsp;<input type="range" name="range"  min="0" max="1" step="0.01" 
{{pwm}}>
         <input style="color:#C000000 ;background-color:#FFEEFF ;font-size:15;width:70px;height:30px" 
type="submit" name="submit" value="Send"></p>
    </form>
  </body>
</font>
</html>

では、また。

ESP-WROOM-02 開発ボード (ESP8266) と iPhone が OSCで双方向通信できた!!

この前までは、

 ESP-WROOM-02(以下ESP)からiPhoneへの1方向だけの通信だけでした。今回は双方向通信を実現してみます。

          f:id:TAKEsan:20160224220921j:plain:w200:left

           
 今回の動画です。テザリングiPhoneのスイッチを押すとESPのLEDとLEPTONのシャッターが動く。

 これを使えば、大概のことが実現可能です。iPhoneからESPを自由に操作できるし、ESPから受け取ったデータをOpenframeworksの豊富な映像処理で高速に加工してiPhoneに表示できます。コツはiPhone側のWifiをONにして、インターネット共有もONにしておくことです。これは、EdisonでのiPhoneテザリング実験をしていなかったら思いつきませんでした。
 OSC通信ってのはそのソースコードを見る限り双方向通信可能なのはわかりますけど、大概のExampleは一方通行の通信例だけです。素人では送受信ソースの合体なんてめんどくさいし、後回しにしようって考えが先にたってしまいます。
 今回は「なんかめんどくさい」ことをすることにしました。理由はOSCのソースは簡潔で私にも容易に理解できたから。
結果、予想通り実現できました。 
 f:id:TAKEsan:20160224214654p:plainf:id:TAKEsan:20160224214655p:plain

OSC通信の大雑把な説明

  1. OSCのアドレスと称するものは /から始まる英数字で、自由に自分でいくらでも設定できる。
  2. 送受信側でOSCアドレスが同一であること。/◯◯/◯◯等入れ子にできるようですが、単純なデータ送受信は/◯◯だけでいい。そのあとに続くのが肝心のデータで、int,string,float他のデータを複数送信できる。これに関してはライブラリによって関数の記述方法が違うので、ライブラリの説明を確認すること。
  3. 受信側はシリアル通信のように面倒な認識手段を考える必要なし。
  4. 送信に関し受信側のipと送信用のポート番号が必要。受信側は受信用のポート番号を設定すれば送信側のipは必要なし。注意点は、送受信別々のポート番号が必要なことです。つまりiPhone側の送信ポートとESP側の受診ポート。iPhone側の受信ポートとESP側の送信ポートが同一名称であること。
  5. ポート番号はほとんど自由に自分で設定できる(規則に従うこと)
  6. ESPからiPhoneの送信に関し、複数のESPから1台のiPhoneに各々別のデータを送ることが可能です。もうお分かりですね。OSCアドレスを変えてやればいいんです。

 これらは、今回のソースを見て貰えば多少ソフトをいじったことがある方なら容易に判別できると思います。ライブラリの中身はどうあれソース作成はすごく簡単です。今回の応用で、iPhone複数のESPとの送受信、そして複雑になるとは思いますがESP通しの送受信も可能になると思います。

iPhonのインターネット通信をON=テザリングを有効にすると...。

 今回調べて新たに分かったことは、iPhoneではテザリング通信において、固定ipだということ。これはどのiPhoneも同じようなので公表できます。「172.20.10.1」です。ESP側は、iPhoneテザリングしている限り、このipに設定してしまえば、iPhonのipアドレスを意識しなくていいんです(ただしパスワードは個人設定!!)。これは大きな収穫で、EdisonのようにiPhone,iot機器で各々bonjourlを使わなくても良いのです。ESPからiPhoneの交信を確立して、自分のipアドレスをOSC通信で、iPhoneに送って知らせてやれば、iPhoneはESPにデータを送ることが可能になります。Edisonとのさらに大きな違いは、めんどくさいWifi選択などしなくても、プログラム中ですんなり繋がってしまうことでした。これはっ.....いい。ただしiPhoneの名称を英数字に修正してから。多分何も意識しないと日本語に設定されてしまうのでiPhoneの名称を英数字に直してから実行してください。
 これで、実行する環境は、家の中の無線ランルーターを意識しなくて済みますので、どこへ外出してもiPhoneとESPはお友達のままです。これも画期的です。
 心配なのはテザリング通信してパケットの使用量がどうかです。同一ラン内で外部に出ていないので多分大丈夫かとは思いましたが、一応昨日実行したLEPTONとの通信をAUで確認したら、問題ありませんでした。PhoneとESPはなんかOSCを実現するために生み出されてきたような感じです。

 さらに前回からの進歩は、SPIについて最後のDelayが必要なくなったことです。SSピンのON,OFF切り替えだけでいけるかもという直感だけでしたが、うまくいきました。これで若干(0.1秒)ですがスピードアップしました。
 てな訳で、今回はその全貌の公開をすることにしました。 ESP側はセンサーのデータを送るだけでいいし(少なくとも今回取り上げた4.8K程度のデータなら瞬時)iPhoneからはESPを思うように操作できます。今の所ESPから送れないのは大量の画像でしょうか?ただ、カメラの代わりに、OMRONの非接触赤外線センサーで人の移動(体温)に合わせてサーボを動かすなんて朝飯前にできます。そして同時にiPhone側にセンサーで捉えた温度分布も表示できます。でも、今回のセンサーの表示は、まさにカメラですよね。このくらいの動画なら、iPhone側のOpenframeworksでOpenCVが使えます。OpenCVが使えたら動体の認識はもちろん様々な画像の加工が簡単に実現可能。しかも暗闇でも画像が認識できるのですからワクワクです。
 
 今回のソースに関し、Xcodeの最初の設定は、初めての方には特にGUIに関して多少面倒くさいかもしれませんが、設定してしまえば後は少しの修正だけで、iPhoneからESPを考えがつく限り最大の表現力でしかもサクサクと実行できます(通信方式がUDPなので反応がとても早い)。すごい。Openframeworksのハードルが高いって?そんなことなし。最新のバージョン(9.2以降)では、今まで私がぶつかった問題点が、ほとんど解消されてます。メジャーなオープンソースってすごいです。資金源はどこなんだろうなんて下衆の勘繰りをしちゃいます。日本語の解説を見れば理解できますし、最小のCの知識で素晴らしい画像が作れます。なんといっても芸術家のための開発環境ですから難しいわけがない。Enjoy!!

5月12日追記

 今回作ったOpenframeworksのソースをフォルダごとダウンロードできます。(今まで「はてなブログ」でのリンク方法が分からなかった)
    WROOM02.zip 直
 これをダウンロードして解凍したら of〇〇ios_release/apps/myApps へコピーして、最初はXcodeiosシュミレーターからiPhone6を選択してコンパイルして見て下さい。今回の画面が表示されるはずです。うまく実行できたら、自分のiPhoneをつないで転送。たたき台にすればきっと面白いものができると思います。
f:id:TAKEsan:20160512124959p:plain
 ios版V0.9.3では、exampleをそのままコンパイルするとエラーになります。原因はbin/dataに本来なければならないpngファイルがなぜか全て消えているからです。exampleを実行したい場合は、一度projectGeneratorでダミーフォルダを作って、bin/data 以下のpngファイルを全てコピーする必要があります。

以下 ofApp.h

基本的なプログラムの作成方法は、前回の記事を参照
takesan.hatenablog.com

#pragma once

#include "ofxiOS.h"
#include "ofxOsc.h"

//#define HOST "172.20.10.7" 今回は設定の必要なし
#define PORT1 8091
#define PORT 8090

#define NUM_MSG_STRINGS 20

class ofApp : public ofxiOSApp {

	public:
		void setup();
		void update();
		void draw();
		void exit();
		
		void touchDown(ofTouchEventArgs & touch);
		void touchMoved(ofTouchEventArgs & touch);
		void touchUp(ofTouchEventArgs & touch);
		void touchDoubleTap(ofTouchEventArgs & touch);
		void touchCancelled(ofTouchEventArgs & touch);

		void lostFocus();
		void gotFocus();
		void gotMemoryWarning();
		void deviceOrientationChanged(int newOrientation);

		ofxOscReceiver receiver;
		ofxOscSender sender;
    
		int current_msg_string;
		string msg_strings[NUM_MSG_STRINGS];
		float timers[NUM_MSG_STRINGS];

         //OSC Data
        int EPS_swA,EPS_swB,EPS_button;
        float EPS_sli;
};

以下 main.mm

#include "ofApp.h"

int main() {
    
    //  here are the most commonly used iOS window settings.
    //------------------------------------------------------
    ofiOSWindowSettings settings;
    settings.enableRetina = true; // レティーナ用にするにははここを true にする
    settings.enableDepth = false; // enables depth buffer for 3d drawing.
    settings.enableAntiAliasing = false; // enables anti-aliasing which smooths out graphics on the screen.
    settings.numOfAntiAliasingSamples = 0; // number of samples used for anti-aliasing.
    settings.enableHardwareOrientation = false; // enables native view orientation.
    settings.enableHardwareOrientationAnimation = false; // enables native orientation changes to be animated.
    settings.glesVersion = OFXIOS_RENDERER_ES1; // type of renderer to use, ES1, ES2, etc.
    
    ofCreateWindow(settings);
    
	return ofRunApp(new ofApp);
}

以下 ofApp.mm

#include "ofApp.h"
#include "EPSView.h"

const int colormap_rainbow[] = {1, 3, 74, 0, 3, 74, 0, 3, 75, 0, 3, 75, 0, 3, 76, 0, 3, 76, 0, 3, 77, 0, 3, 79, 0, 3, 82, 0, 5, 85, 0, 7, 88, 0, 10, 91, 0, 14, 94, 0, 19, 98, 0, 22, 100, 0, 25, 103, 0, 28, 106, 0, 32, 109, 0, 35, 112, 0, 38, 116, 0, 40, 119, 0, 42, 123, 0, 45, 128, 0, 49, 133, 0, 50, 134, 0, 51, 136, 0, 52, 137, 0, 53, 139, 0, 54, 142, 0, 55, 144, 0, 56, 145, 0, 58, 149, 0, 61, 154, 0, 63, 156, 0, 65, 159, 0, 66, 161, 0, 68, 164, 0, 69, 167, 0, 71, 170, 0, 73, 174, 0, 75, 179, 0, 76, 181, 0, 78, 184, 0, 79, 187, 0, 80, 188, 0, 81, 190, 0, 84, 194, 0, 87, 198, 0, 88, 200, 0, 90, 203, 0, 92, 205, 0, 94, 207, 0, 94, 208, 0, 95, 209, 0, 96, 210, 0, 97, 211, 0, 99, 214, 0, 102, 217, 0, 103, 218, 0, 104, 219, 0, 105, 220, 0, 107, 221, 0, 109, 223, 0, 111, 223, 0, 113, 223, 0, 115, 222, 0, 117, 221, 0, 118, 220, 1, 120, 219, 1, 122, 217, 2, 124, 216, 2, 126, 214, 3, 129, 212, 3, 131, 207, 4, 132, 205, 4, 133, 202, 4, 134, 197, 5, 136, 192, 6, 138, 185, 7, 141, 178, 8, 142, 172, 10, 144, 166, 10, 144, 162, 11, 145, 158, 12, 146, 153, 13, 147, 149, 15, 149, 140, 17, 151, 132, 22, 153, 120, 25, 154, 115, 28, 156, 109, 34, 158, 101, 40, 160, 94, 45, 162, 86, 51, 164, 79, 59, 167, 69, 67, 171, 60, 72, 173, 54, 78, 175, 48, 83, 177, 43, 89, 179, 39, 93, 181, 35, 98, 183, 31, 105, 185, 26, 109, 187, 23, 113, 188, 21, 118, 189, 19, 123, 191, 17, 128, 193, 14, 134, 195, 12, 138, 196, 10, 142, 197, 8, 146, 198, 6, 151, 200, 5, 155, 201, 4, 160, 203, 3, 164, 204, 2, 169, 205, 2, 173, 206, 1, 175, 207, 1, 178, 207, 1, 184, 208, 0, 190, 210, 0, 193, 211, 0, 196, 212, 0, 199, 212, 0, 202, 213, 1, 207, 214, 2, 212, 215, 3, 215, 214, 3, 218, 214, 3, 220, 213, 3, 222, 213, 4, 224, 212, 4, 225, 212, 5, 226, 212, 5, 229, 211, 5, 232, 211, 6, 232, 211, 6, 233, 211, 6, 234, 210, 6, 235, 210, 7, 236, 209, 7, 237, 208, 8, 239, 206, 8, 241, 204, 9, 242, 203, 9, 244, 202, 10, 244, 201, 10, 245, 200, 10, 245, 199, 11, 246, 198, 11, 247, 197, 12, 248, 194, 13, 249, 191, 14, 250, 189, 14, 251, 187, 15, 251, 185, 16, 252, 183, 17, 252, 178, 18, 253, 174, 19, 253, 171, 19, 254, 168, 20, 254, 165, 21, 254, 164, 21, 255, 163, 22, 255, 161, 22, 255, 159, 23, 255, 157, 23, 255, 155, 24, 255, 149, 25, 255, 143, 27, 255, 139, 28, 255, 135, 30, 255, 131, 31, 255, 127, 32, 255, 118, 34, 255, 110, 36, 255, 104, 37, 255, 101, 38, 255, 99, 39, 255, 93, 40, 255, 88, 42, 254, 82, 43, 254, 77, 45, 254, 69, 47, 254, 62, 49, 253, 57, 50, 253, 53, 52, 252, 49, 53, 252, 45, 55, 251, 39, 57, 251, 33, 59, 251, 32, 60, 251, 31, 60, 251, 30, 61, 251, 29, 61, 251, 28, 62, 250, 27, 63, 250, 27, 65, 249, 26, 66, 249, 26, 68, 248, 25, 70, 248, 24, 73, 247, 24, 75, 247, 25, 77, 247, 25, 79, 247, 26, 81, 247, 32, 83, 247, 35, 85, 247, 38, 86, 247, 42, 88, 247, 46, 90, 247, 50, 92, 248, 55, 94, 248, 59, 96, 248, 64, 98, 248, 72, 101, 249, 81, 104, 249, 87, 106, 250, 93, 108, 250, 95, 109, 250, 98, 110, 250, 100, 111, 251, 101, 112, 251, 102, 113, 251, 109, 117, 252, 116, 121, 252, 121, 123, 253, 126, 126, 253, 130, 128, 254, 135, 131, 254, 139, 133, 254, 144, 136, 254, 151, 140, 255, 158, 144, 255, 163, 146, 255, 168, 149, 255, 173, 152, 255, 176, 153, 255, 178, 155, 255, 184, 160, 255, 191, 165, 255, 195, 168, 255, 199, 172, 255, 203, 175, 255, 207, 179, 255, 211, 182, 255, 216, 185, 255, 218, 190, 255, 220, 196, 255, 222, 200, 255, 225, 202, 255, 227, 204, 255, 230, 206, 255, 233, 208};
const int colormap_ironblack[] = {255, 255, 255, 253, 253, 253, 251, 251, 251, 249, 249, 249, 247, 247, 247, 245, 245, 245, 243, 243, 243, 241, 241, 241, 239, 239, 239, 237, 237, 237, 235, 235, 235, 233, 233, 233, 231, 231, 231, 229, 229, 229, 227, 227, 227, 225, 225, 225, 223, 223, 223, 221, 221, 221, 219, 219, 219, 217, 217, 217, 215, 215, 215, 213, 213, 213, 211, 211, 211, 209, 209, 209, 207, 207, 207, 205, 205, 205, 203, 203, 203, 201, 201, 201, 199, 199, 199, 197, 197, 197, 195, 195, 195, 193, 193, 193, 191, 191, 191, 189, 189, 189, 187, 187, 187, 185, 185, 185, 183, 183, 183, 181, 181, 181, 179, 179, 179, 177, 177, 177, 175, 175, 175, 173, 173, 173, 171, 171, 171, 169, 169, 169, 167, 167, 167, 165, 165, 165, 163, 163, 163, 161, 161, 161, 159, 159, 159, 157, 157, 157, 155, 155, 155, 153, 153, 153, 151, 151, 151, 149, 149, 149, 147, 147, 147, 145, 145, 145, 143, 143, 143, 141, 141, 141, 139, 139, 139, 137, 137, 137, 135, 135, 135, 133, 133, 133, 131, 131, 131, 129, 129, 129, 126, 126, 126, 124, 124, 124, 122, 122, 122, 120, 120, 120, 118, 118, 118, 116, 116, 116, 114, 114, 114, 112, 112, 112, 110, 110, 110, 108, 108, 108, 106, 106, 106, 104, 104, 104, 102, 102, 102, 100, 100, 100, 98, 98, 98, 96, 96, 96, 94, 94, 94, 92, 92, 92, 90, 90, 90, 88, 88, 88, 86, 86, 86, 84, 84, 84, 82, 82, 82, 80, 80, 80, 78, 78, 78, 76, 76, 76, 74, 74, 74, 72, 72, 72, 70, 70, 70, 68, 68, 68, 66, 66, 66, 64, 64, 64, 62, 62, 62, 60, 60, 60, 58, 58, 58, 56, 56, 56, 54, 54, 54, 52, 52, 52, 50, 50, 50, 48, 48, 48, 46, 46, 46, 44, 44, 44, 42, 42, 42, 40, 40, 40, 38, 38, 38, 36, 36, 36, 34, 34, 34, 32, 32, 32, 30, 30, 30, 28, 28, 28, 26, 26, 26, 24, 24, 24, 22, 22, 22, 20, 20, 20, 18, 18, 18, 16, 16, 16, 14, 14, 14, 12, 12, 12, 10, 10, 10, 8, 8, 8, 6, 6, 6, 4, 4, 4, 2, 2, 2, 0, 0, 0, 0, 0, 9, 2, 0, 16, 4, 0, 24, 6, 0, 31, 8, 0, 38, 10, 0, 45, 12, 0, 53, 14, 0, 60, 17, 0, 67, 19, 0, 74, 21, 0, 82, 23, 0, 89, 25, 0, 96, 27, 0, 103, 29, 0, 111, 31, 0, 118, 36, 0, 120, 41, 0, 121, 46, 0, 122, 51, 0, 123, 56, 0, 124, 61, 0, 125, 66, 0, 126, 71, 0, 127, 76, 1, 128, 81, 1, 129, 86, 1, 130, 91, 1, 131, 96, 1, 132, 101, 1, 133, 106, 1, 134, 111, 1, 135, 116, 1, 136, 121, 1, 136, 125, 2, 137, 130, 2, 137, 135, 3, 137, 139, 3, 138, 144, 3, 138, 149, 4, 138, 153, 4, 139, 158, 5, 139, 163, 5, 139, 167, 5, 140, 172, 6, 140, 177, 6, 140, 181, 7, 141, 186, 7, 141, 189, 10, 137, 191, 13, 132, 194, 16, 127, 196, 19, 121, 198, 22, 116, 200, 25, 111, 203, 28, 106, 205, 31, 101, 207, 34, 95, 209, 37, 90, 212, 40, 85, 214, 43, 80, 216, 46, 75, 218, 49, 69, 221, 52, 64, 223, 55, 59, 224, 57, 49, 225, 60, 47, 226, 64, 44, 227, 67, 42, 228, 71, 39, 229, 74, 37, 230, 78, 34, 231, 81, 32, 231, 85, 29, 232, 88, 27, 233, 92, 24, 234, 95, 22, 235, 99, 19, 236, 102, 17, 237, 106, 14, 238, 109, 12, 239, 112, 12, 240, 116, 12, 240, 119, 12, 241, 123, 12, 241, 127, 12, 242, 130, 12, 242, 134, 12, 243, 138, 12, 243, 141, 13, 244, 145, 13, 244, 149, 13, 245, 152, 13, 245, 156, 13, 246, 160, 13, 246, 163, 13, 247, 167, 13, 247, 171, 13, 248, 175, 14, 248, 178, 15, 249, 182, 16, 249, 185, 18, 250, 189, 19, 250, 192, 20, 251, 196, 21, 251, 199, 22, 252, 203, 23, 252, 206, 24, 253, 210, 25, 253, 213, 27, 254, 217, 28, 254, 220, 29, 255, 224, 30, 255, 227, 39, 255, 229, 53, 255, 231, 67, 255, 233, 81, 255, 234, 95, 255, 236, 109, 255, 238, 123, 255, 240, 137, 255, 242, 151, 255, 244, 165, 255, 246, 179, 255, 248, 193, 255, 249, 207, 255, 251, 221, 255, 253, 235, 255, 255, 24};

uint8_t mat[61][81];
int mathigh[119][159];
float min1,max1,tem1;
string linex;
ofFile datax;
ofTrueTypeFont Font1,Font2;
const int *colormap;
int color=0;
int EPS=0;
EPSView *gui;
string EPS_ip;

//--------------------------------------------------------------
void ofApp::setup(){

    // iOS GUI 設定
    // 田所先生のWEB上のやり方http://yoppa.org/blog/2995.htmlだとXcode自体が少し昔のものなのでうまくいかなかった。他の方も
    // http://www.creativeapplications.net/tutorials/integrating-native-uikit-to-your-existing-openframeworks-ios-project/
    // でさらに詳しく説明されているが、これでも上手くいかない。また、OFに添付されたいるiOSのサンプル(iPhoneGuiExample)は動くが設定の経緯が分からない。
    // 結局  GUI フォルダを 作って 3つのファイルを自動で作成させ 自動で作成される xxxx.m の拡張子を .mm に変えるまでは田所先生の説明、
    //  すなわちnewfile->iOS->Source->Cocoa touch Class->Next->Class名変更、サブクラスはそのまま、Also creat XLB fileにチェック
    //  さらに SWIFT から Objectiv C に変更
    // 次に各ファイルの内容とボタン作成の操作を上記ブログを参考にして変更するとうまくいった。慣れればすごく簡単。以下2行ofApp.mm内での設定
    //  EPSViewはGUIのファイル名。テキトーに修正して使う。
    gui = [[EPSView alloc] initWithNibName:@"EPSView" bundle:nil];
    [ofxiOSGetGLParentView() addSubview:gui.view];
    
	cout << "listening for osc messages on port " << PORT << "\n";
	receiver.setup( PORT );

	current_msg_string = 0;

	Font1.load("cooperBlack.ttf", 18, true, true, true);
    Font2.load("cooperBlack.ttf", 48, true, true, true);
	ofBackground( 0,0,0);
    
    //*******GUI関連*******
    colormap = colormap_rainbow;
    EPS_button=0;
    EPS=0;
    EPS_swA=1;
    EPS_swB=0;
    EPS_sli=1;
    EPS_ip="Not Connect!!!";
}
//--------------------------------------------------------------
void ofApp::update(){
    
    //********iPhone GUI********
    if (EPS_button==1) {
        if (color==0) {
            colormap = colormap_rainbow;
            color=1;
        }
        else if (color==1) {
            colormap = colormap_ironblack;
            color=0;
        }
        EPS_button=0;
    }
    //********OSC Send********
    if (EPS==1) {
    if( EPS_swA==1 ){
        ofxOscMessage mm;
        mm.setAddress( "/led" );
        mm.addIntArg(EPS_swA);
        sender.sendMessage( mm );
        EPS_swA=0;
    }
    if( EPS_swB==0){
        ofxOscMessage mm;
        mm.setAddress( "/led1" );
        mm.addIntArg(EPS_swB );
        sender.sendMessage( mm );
        EPS_swB=1;
    }
    }
	//********OSC Receve********
	for( int i=0; i<NUM_MSG_STRINGS; i++ ){
		if( timers[i] < ofGetElapsedTimef() )
			msg_strings[i] = "";
	}
	while( receiver.hasWaitingMessages() ){
		// get the next message
		ofxOscMessage m;
		receiver.getNextMessage(m);
        if(m.getAddress() == "/img_8"){
            linex = m.getArgAsString(0);
            if(linex[0]==0x3c) linex[0]=0;
            for(int yy=0;yy<82;yy++){
                
                if(yy != 81) mat[linex[0]][yy]=uint8_t(linex[yy+1]);
                else if(yy != 0) mat[linex[0]][yy-1]=uint8_t(linex[yy]);
            }
        }
        else if(m.getAddress()=="/ip")
        {
            if(EPS==0){
            EPS_ip = m.getArgAsString(0);
            sender.setup( EPS_ip , PORT1 );
            EPS=1;
            }
        }
        else if(m.getAddress() == "/minmax" ){
            min1=m.getArgAsFloat(0);
            max1=m.getArgAsFloat(1);
            tem1=m.getArgAsFloat(2);
        }
	}	
}

//--------------------------------------------------------------
void ofApp::draw(){

    string buf;

    ofSetColor(255,210,0);
    Font2.drawString("Thermalcam!!", 70, 170-30);
    Font1.drawString(EPS_ip, 15, 50);

   	buf = "Min=" + ofToString(min1,2);
    ofSetColor(100,100,255);
    Font1.drawString(buf, 15, 360-50);
    
    buf = "Max=" + ofToString(max1,2);
    ofSetColor(255,0,0);
    Font1.drawString(buf, 460, 360-50);

    buf = "Temp=" + ofToString(tem1,2);
    ofSetColor(255,255,255);
    Font1.drawString(buf, 225, 310-50);
    
    ofSetColor(255,110,0);
    Font1.drawString("by Takesan", 460, 1070+30);
    
    ofSetColor(120,120,255);
    ofDrawRectangle(178,48+290-50,260,29);
    
    for(int i=0;i<256;i++) {
        ofSetColor( colormap[3*i], colormap[3*i+1], colormap[3*i+2]);
        ofDrawLine(i+180, 340-50, i+180, 365-50);
    }
    for (int i=0;i<60;i++){
        for (int j=0;j<80;j++) {
            mathigh[i*2][j*2]=mat[i][j];
        }
    }

    for (int i=0;i<119;i=i+2){
        for (int j=1;j<159;j=j+2) {
            mathigh[i][j]=(mathigh[i][j-1]+mathigh[i][j+1])/2;
        }
    }
    for (int i=1;i<119;i=i+2){
        for (int j=0;j<159;j++) {
            mathigh[i][j]=(mathigh[i-1][j]+mathigh[i+1][j])/2;
        }
    }
    for (int i=0;i<119;i++){
        for (int j=0;j<159;j++) {
            ofSetColor( colormap[3*mathigh[i][j]]*EPS_sli, colormap[3*mathigh[i][j]+1], colormap[3*mathigh[i][j]+2]);
            ofDrawCircle(j*4+5, i*4+400-30, 2);
        }
    }
}

//--------------------------------------------------------------
void ofApp::exit(){
}
//--------------------------------------------------------------
void ofApp::touchDown(ofTouchEventArgs &touch){
    if (color==0) {
         colormap = colormap_rainbow;
        color=1;
    }
    else if (color==1) {
        colormap = colormap_ironblack;
        color=0;

    }
}
//--------------------------------------------------------------
void ofApp::touchMoved(ofTouchEventArgs &touch){
}
//--------------------------------------------------------------
void ofApp::touchUp(ofTouchEventArgs &touch){
}
//--------------------------------------------------------------
void ofApp::touchDoubleTap(ofTouchEventArgs &touch){
}
//--------------------------------------------------------------
void ofApp::touchCancelled(ofTouchEventArgs & touch){
}
//--------------------------------------------------------------
void ofApp::lostFocus(){
}
//--------------------------------------------------------------
void ofApp::gotFocus(){
}
//--------------------------------------------------------------
void ofApp::gotMemoryWarning(){
}
//--------------------------------------------------------------
void ofApp::deviceOrientationChanged(int newOrientation){
}

ここからGUI関連

 作成方法はofApp.mmの最初の方のコメント参照。
 EPSView.xlb(ストーリーボード)は以下の画像を参照してください。今回は、スイッチとスライダー、ボタンを作ります。
f:id:TAKEsan:20160224222000p:plain

以下 EPSView.mm

//
//  EPSView.h
//
// とにかく◯◯◯◯◯◯.xlb作成前にコピーしとく。
//  この表記方法ムッとくる。
//
//

#import <UIKit/UIKit.h>

@interface EPSView : UIViewController
@property(retain, nonatomic) IBOutlet UISlider *ESPSlider;
@property(retain, nonatomic) IBOutlet UISwitch *EPSSwitchA;
@property(retain, nonatomic) IBOutlet UISwitch *EPSSwitchB;
@property(retain, nonatomic) IBOutlet UIButton *EPSButton;


@end

以下 EPSView.mm

//
//  EPSView.m
//  とにかく◯◯◯◯◯◯.xlb作成前にコピーしとく。GUIを◯◯◯◯◯◯.xlbで作ったら、選択するだけで済む。
// 以下はスライダー、スイッチ、ボタン Openframeworksへの値の渡し方は以下参照
//  ◯◯◯◯◯◯.xlbでスライダー、スイッチ、ボタンを作成したら、そのGUIを右クリックしてFile's Ownerヘドラックすると以下の関数が
//  表示されるので選択するだけ。
//  ◯◯◯◯◯◯.xlb 操作中、Viewの W=320、H=570として描画すること。でないとOFの画面と合わない。これはSaveするとなぜかリセットされる(多分バグ0
// さらにViewのBackgrand をディフォルトにしておくこと。何もしないと白に設定されてるので。OFでつくった画像が真っ白けになる。最初から白なんてこれもバグ。
//  Created by Takesan on 2016/02/23.
// 
//

#import "EPSView.h"    //ファイル名を変更してれば修正必要
#include "ofxiOSExtras.h"
#include "ofApp.h"

@interface EPSView ()

@end

@implementation EPSView   //ファイル名を変更してれば修正必要

ofApp *myApp;


- (void)viewDidLoad {
    [super viewDidLoad];
    myApp = (ofApp*)ofGetAppPtr();

    // Do any additional setup after loading the view from its nib.
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}


-(IBAction)ESPSlide:(id)sender {
    UISlider *sliderObj = sender;
    myApp->EPS_sli = [sliderObj value];
}

-(IBAction)EPSSwitchA:(id)sender{
    // スイッチのON,OFFを知りたい時は以下のコメント実行
    //UISwitch * switchObj = sender;
    //myApp->EPS_swA =[switchObj isOn];
    myApp->EPS_swA = 1;

}
-(IBAction)EPSSwitchB:(id)sender{
   // スイッチのON,OFFを知りたい時は以下のコメント実行
   // UISwitch * switchObj = sender;
   //myApp->EPS_swA =[switchObj isOn];
    myApp->EPS_swB = 0;
}
-(IBAction)EPSButton:(id)sender{
//ボタンに関しては押されるとイベントが発生してここに来るのでOFの変数に何か返してやればOK
//ここではmyApp->EPS_button =1 でもいい。なーんだ簡単(かなり悩んだ)。
    UIButton * btnObj = sender;
    myApp->EPS_button = btnObj.isTouchInside;
}

@end

以下 ESP-WROOM-02 Arduino ソース(最終)

extern "C"{
  #include <spi.h>
  #include <spi_register.h>
}
#include <Wire.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <OSCMessage.h>

#include <OSCBundle.h>
#include <OSCData.h>

OSCErrorCode error;

#define ADDRESS  (0x2A)
#define AGC (0x01)
#define SYS (0x02)
#define VID (0x03)
#define OEM (0x08)
#define GET (0x00)
#define SET (0x01)
#define RUN (0x02)
#define VOSPI_FRAME_SIZE (164)
#define COMMANDID_REG (0x04)
#define DATALEN_REG (0x06)
#define DATA0 (0x08)

char ssid[] = "YYYY-iPhone";          // your iPhone Name SSID (name)
char pass[] = "XXXXXXXXX";          // your iPhone テザリングパスワード 

 WiFiUDP Udp;                                // A UDP instance to let us send and receive packets over UDP

const IPAddress outIp(172,20,10,1);         //iPhonr テザリング固定 ip どの機種でも同じ。よって変更なし。このままでOK
const unsigned int outPort = 8090;          // remote port to receive OSC このままでOK
const unsigned int localPort = 8091;        // local port to listen for OSC   このままでOK

char zz[82];
unsigned int min = 65536;
unsigned int max = 0;
float diff;
char ip_ESP[16];

void setup()
{
    pinMode(15, OUTPUT);   //  SSピン
    pinMode(16, OUTPUT);   //  LED
    Wire.begin();
    Serial.begin(115200);
  
    spi_init(HSPI);
    spi_clock(HSPI,1.7,2);  //spi_clock(HSPI,5,2);spi_clock(HSPI,1.6666,2)
  
    Serial.println();
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, pass);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    //ローカルipを文字列に変換。時間がかかった。
    sprintf(ip_ESP, "%d.%d.%d.%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]);
    Serial.println(ip_ESP);
    Serial.println("Starting UDP");
    Udp.begin(localPort);
    Serial.print("Local port: ");
    Serial.println(Udp.localPort());

}

static uint16_t lepton_image[63][82]; 

void read_lepton_frame(void)
{
  int i;
  uint16_t data = 0x000f;
  delay(50); //delay(185);
  while ((data & 0x000f == 0x000f))
  { 
    data = spi_rx16(HSPI);
    lepton_image[0][0] =data;
    for (i = 0; i < 81; i++)
    {    
      lepton_image[0][i+1]=spi_rx16(HSPI); 
     //digitalWrite(15, HIGH);    
    }  
  }
  for (int frame_number = 1; frame_number < 60; frame_number++){ 
    for (i = 0; i < 82; i++)
    {
        lepton_image[frame_number][i] = spi_rx16(HSPI);
     }
   }
}

void lepton_sync(void)
{
  int i;
  uint16_t data = 0x000f;
  uint16_t aaa;

  while (data & 0x000f == 0x000f)
{
    data = spi_rx16(HSPI);
    for (i = 0; i < 81; i++)
    {
      aaa= spi_rx16(HSPI);
    }  
  }
}

void print_lepton_frame(void)
{
  int i;
  for (int frame_number = 0; frame_number < 60; frame_number++){ 
    for (i = 0; i < (VOSPI_FRAME_SIZE / 2); i++)
    {
        Serial.print(lepton_image[frame_number][i] ,HEX);
        Serial.print(",");
    }
        Serial.println(" ");
  }
        Serial.println(" "); 
}

void lepton_command(unsigned int moduleID, unsigned int commandID, unsigned int command)
{
  byte error;
  Wire.beginTransmission(ADDRESS);

  // Command Register is a 16-bit register located at Register Address 0x0004
  Wire.write(0x00);
  Wire.write(0x04);

  if (moduleID == 0x08) //OEM module ID
  {
    Wire.write(0x48);
  }
  else
  {
    Wire.write(moduleID & 0x0f);
  }
  Wire.write( ((commandID << 2 ) & 0xfc) | (command & 0x3));

  error = Wire.endTransmission();    // stop transmitting
  if (error != 0)
  {
    Serial.print("error=");
    Serial.println(error);
  }
}

void set_reg(unsigned int reg)
{
  byte error;
  Wire.beginTransmission(ADDRESS); // transmit to device #4
  Wire.write(reg >> 8 & 0xff);
  Wire.write(reg & 0xff);            // sends one byte

  error = Wire.endTransmission();    // stop transmitting
  if (error != 0)
  {
    Serial.print("error=");
    Serial.println(error);
  }
}

int read_reg(unsigned int reg)
{
  int reading = 0;
  set_reg(reg);
  Wire.requestFrom(ADDRESS, 2);
  reading = Wire.read();  // receive high byte (overwrites previous reading)
  reading = reading << 8;    // shift high byte to be high 8 bits
  reading |= Wire.read(); // receive low byte as lower 8 bits
  return reading;
}

int read_data()
{
  int i;
  int data;
  int payload_length;

  while (read_reg(0x2) & 0x01)
  {
    Serial.println("busy");
  }
  payload_length = read_reg(0x6);
  Wire.requestFrom(ADDRESS, payload_length);
  //set_reg(0x08);
  for (i = 0; i < (payload_length / 2); i++)
  {
    data = Wire.read() << 8;
    data |= Wire.read();
  }
  return data;
}

void led(OSCMessage &msg) {
int  ledState = msg.getInt(0);
  //LEDを点灯させる。
  digitalWrite(16, HIGH); 
  delay(500);
  digitalWrite(16, LOW);
}
void led1(OSCMessage &msg) {
int  ledState = msg.getInt(0);
  //LEPTONのシャッターをぱちくりさせるだけ。
  lepton_command(SYS, 0x42 >> 2 , RUN);
  read_data();
}

void loop()
{
  int i,p;
  int reading = 0;
  String debugString;
  unsigned int col;
  float value_min,value_max,temp;
  long do_ffc = 0;

  Serial.println("SYS Telemetry Enable State");
  lepton_command(SYS, 0x19>>2 ,GET);
  read_data(); 
  delay(500);
 while(1){
        //*****************Send OSC data*****************
        //iPhoneが切れている場合があるので常時ip。
        OSCMessage msg("/ip");
        msg.add(ip_ESP);
        Udp.beginPacket(outIp, outPort);
        msg.send(Udp);
        Udp.endPacket();
        msg.empty();   
  //**********Osc Receive**********
  OSCBundle bundle;
  int size = Udp.parsePacket();
  if (size > 0) {
    while (size--) {
      bundle.fill(Udp.read());
    }
    if (!bundle.hasError()) {
      //ここでアドレスと実行関数を記述。羅列でいいみたい。アドレスが一致しないと読み飛ばす。
      bundle.dispatch("/led", led);
      bundle.dispatch("/led1", led1);
    } else {
      error = bundle.getError();
      Serial.print("error: ");
      Serial.println(error);
    }
  }    
  min = 65536;
  max = 0;
  read_lepton_frame();
     for (int frame_number = 0; frame_number < 60; frame_number++){     
       for (i = 0; i < 82; i++)
       {
          p=lepton_image[frame_number][i] ;
          if(i >= 2){ 
              if(p < min) min = p;
              if(p > max) max = p;
          }
          if(i==0) lepton_image[frame_number][i]=(p & 0x00ff);
       }
     }     
     diff = max - min;
     diff = diff / 256.0f;
     if(diff < 0.56f) diff = 0.56f; // 0.66
        lepton_command(SYS, 0x10 >> 2 , GET); //ondo : 0x14 = chip   0x10 = aux
        float aux=read_data() ;
        float fpatemp = aux/ 100.0f;
        temp=fpatemp-273.15 ;
        float fpatemp_f = fpatemp * 1.8f - 459.67f;
        value_min = ((0.05872 * (float)min - 472.22999f + fpatemp_f));
        value_min= (value_min - 32.0f) / 1.8f;
        value_max = ((0.05872 * (float)max - 472.22999f + fpatemp_f));
        value_max = (value_max - 32.0f) / 1.8f;
        Serial.println(value_min);
        Serial.println(value_max);
        //*****************Send osc data*****************
        OSCMessage msg1("/minmax");
        msg1.add(value_min).add(value_max).add(temp);
        Udp.beginPacket(outIp, outPort);
        msg1.send(Udp);
        Udp.endPacket();
        msg1.empty();   
        //***********************************     
        delay(2); 
    for (int frame_number = 0; frame_number < 60; frame_number++){ 
       for (int i = 2; i < 82; i++)
       {
            col=lepton_image[frame_number][i] ;
            col = col - min;
            col = col / diff;
            if(col <= 0) col = 1;  //=0
            if(col > 255) col = 255;
            zz[i-1]=col;
       }
     zz[0]=frame_number; 
     if(frame_number==0) zz[0]=0x3c; //=0 nara 0x3c tosuru.
     zz[82]='\0';   //String End
      //*****************Send osc data*****
      OSCMessage msg2("/img_8");
      msg2.add(zz );
      Udp.beginPacket(outIp, outPort);
      msg2.send(Udp);
      Udp.endPacket();
      msg2.empty();   
      //***********************************
      delay(1);       // <----絶対必要
    }
    digitalWrite(15, HIGH);  // <----絶対必要
   }
}

今までセンサーが特殊すぎたので、次回はもっと手に入りやすいセンサーやサーボを使った物を作ろうと思います。

スイッチサイエンスの ESP-WROOM-02 開発ボードで、FLIR LEPTON 赤外線カメラを動かしてみる(その2)

前回、

 MacとESP-WROOM-02+FLIR LEPTONの動作内容の説明をしました。本来の使い方はiPhoneだと思うので、iPhoneのプログラムを紹介します。前回の最後に紹介した本来のデータを補間して解像度を上げたものです。
f:id:TAKEsan:20160218181114j:plain:w300f:id:TAKEsan:20160218181112j:plain:w300
   画面のタッチでColor Mapを変更できるようにしました。
          
 作ると言っても、OpenframeworksのOSC通信サンプルを修正しているだけなので、簡単に実行できました。
今回ESP-WROOM-02のプログラムを組んでいてFLIR LEPTONの動作不安定で散々苦労しましたが、プログラムを転送した後、電源をeneloopに変えると安定します。かなり電源にシビアのようです。

Openframeworksで画像表示アプリを作成

  • AppleストアでXcodeを手に入れる。
  • 個人用のプログラムなら、今はなにもしなくて良いみたい(試しにSDKの私のAcountsを外しても実機テストできたので)。
  • OpenframeworksのiOS最新版をダウンロードhttp://openframeworks.cc/download/
  • 解凍したフォルダを開けて examples/ios/oscReceiverExample を同じフォルダにコピー。コピーしたフォルダ名を適当な英数字に変えます。レティーナだと標準のビットマップフォントでは小さすぎるので、同じexampleフォルダのfontShapesExample/bin/data からcooperBlack.ttfをコピーしてコピーしたフォルダの同じ位置にペースト。
  • iPhoneMacに接続。できればSleepをOffにする。
  • コピーしたフォルダを開けて、oscReceiverExample.xcodeprojをダブルクリック。以下Xcodeでの操作。
  • まず Development Tagetを2箇所修正(iOS Debeloyment Targetを9.2に。修正しなくても動くが、いっぱい注意されます。)。

f:id:TAKEsan:20160218190255p:plain:w300f:id:TAKEsan:20160218190258p:plain:w300

  • srcの中の main.mm ofApp.mm ofApp.hを後述ソースに修正します。
  • Diviceを自分のiPhoneに変えます。(iPhoneは、5s,6,6+及び以降の機種で実行可能)

          f:id:TAKEsan:20160218193043p:plain:w300

  • ビルドボタンを押せば、多分iPhoneでプログラムが実行されます。

ESP-WROOM-02にプログラムを書きこむ

  • Phoneのipアドレスを調べてメモ。
  • Arduinoソースのsaid,password,iPhoneのip部分を修正。当然PORT番号は、Openframeworks側と同一にします。(テストをしていませんがiPhoneテザリングで実行可能だと思います)あとはESP-WROOM-02に転送するだけ。

今回のデータは一方通行ですが、双方向通信できると思うので、そうすればiPhone側からボタン操作可能となります。前にiPhoneGUIプログラムを紹介しているので、これを使えば今からHTMLやjavaを覚えるより私には簡単かもしれません。
 で、実現できちゃいました。....双方向通信。しかもiPhoneテザリング機能が使えるので、外に持ち出してもOK。ものすごく簡単にiPhoneとつながります。
takesan.hatenablog.com

今回のソースファイル

 以下ofApp.hの内容。最初の方のPORT 番号を変えるだけです(今回は8090にした)

#pragma once

#include "ofxiOS.h"
#include "ofxOsc.h"

// listen on port 12345
#define PORT 8090     // <-----------------ポート番号を変更する。Arduino側と合わせること!!
#define NUM_MSG_STRINGS 20

class ofApp : public ofxiOSApp {

	public:
		void setup();
		void update();
		void draw();
		void exit();
		
		void touchDown(ofTouchEventArgs & touch);
		void touchMoved(ofTouchEventArgs & touch);
		void touchUp(ofTouchEventArgs & touch);
		void touchDoubleTap(ofTouchEventArgs & touch);
		void touchCancelled(ofTouchEventArgs & touch);

		void lostFocus();
		void gotFocus();
		void gotMemoryWarning();
		void deviceOrientationChanged(int newOrientation);

		ofxOscReceiver receiver;

		int current_msg_string;
		string msg_strings[NUM_MSG_STRINGS];
		float timers[NUM_MSG_STRINGS];

        int mouseX;
        int mouseY;
		string mouseButtonState;
};

以下main.mmの内容。ここでは設定を1箇所変えてレティーナを有効にする(日本語の注釈部分)

#include "ofApp.h"

int main() {
    //Openframeworos iOSも以下がだいぶ変わった。レティーナをONにすると6plusで6の画像が拡大される
    //従って6plusを意識しなくていいみたい。6plusは私みたいな年寄り向き?
    //  here are the most commonly used iOS window settings.
    //------------------------------------------------------
    ofiOSWindowSettings settings;
    settings.enableRetina = true; // レティーナ用にするにははここを true にする
    settings.enableDepth = false; // enables depth buffer for 3d drawing.
    settings.enableAntiAliasing = false; // enables anti-aliasing which smooths out graphics on the screen.
    settings.numOfAntiAliasingSamples = 0; // number of samples used for anti-aliasing.
    settings.enableHardwareOrientation = false; // enables native view orientation.
    settings.enableHardwareOrientationAnimation = false; // enables native orientation changes to be animated.
    settings.glesVersion = OFXIOS_RENDERER_ES1; // type of renderer to use, ES1, ES2, etc.
    
    ofCreateWindow(settings);
    
	return ofRunApp(new ofApp);
}

以下ofApp.mm めんどくさいので以下コードを全部入れ替える。

#include "ofApp.h"

const int colormap_rainbow[] = {1, 3, 74, 0, 3, 74, 0, 3, 75, 0, 3, 75, 0, 3, 76, 0, 3, 76, 0, 3, 77, 0, 3, 79, 0, 3, 82, 0, 5, 85, 0, 7, 88, 0, 10, 91, 0, 14, 94, 0, 19, 98, 0, 22, 100, 0, 25, 103, 0, 28, 106, 0, 32, 109, 0, 35, 112, 0, 38, 116, 0, 40, 119, 0, 42, 123, 0, 45, 128, 0, 49, 133, 0, 50, 134, 0, 51, 136, 0, 52, 137, 0, 53, 139, 0, 54, 142, 0, 55, 144, 0, 56, 145, 0, 58, 149, 0, 61, 154, 0, 63, 156, 0, 65, 159, 0, 66, 161, 0, 68, 164, 0, 69, 167, 0, 71, 170, 0, 73, 174, 0, 75, 179, 0, 76, 181, 0, 78, 184, 0, 79, 187, 0, 80, 188, 0, 81, 190, 0, 84, 194, 0, 87, 198, 0, 88, 200, 0, 90, 203, 0, 92, 205, 0, 94, 207, 0, 94, 208, 0, 95, 209, 0, 96, 210, 0, 97, 211, 0, 99, 214, 0, 102, 217, 0, 103, 218, 0, 104, 219, 0, 105, 220, 0, 107, 221, 0, 109, 223, 0, 111, 223, 0, 113, 223, 0, 115, 222, 0, 117, 221, 0, 118, 220, 1, 120, 219, 1, 122, 217, 2, 124, 216, 2, 126, 214, 3, 129, 212, 3, 131, 207, 4, 132, 205, 4, 133, 202, 4, 134, 197, 5, 136, 192, 6, 138, 185, 7, 141, 178, 8, 142, 172, 10, 144, 166, 10, 144, 162, 11, 145, 158, 12, 146, 153, 13, 147, 149, 15, 149, 140, 17, 151, 132, 22, 153, 120, 25, 154, 115, 28, 156, 109, 34, 158, 101, 40, 160, 94, 45, 162, 86, 51, 164, 79, 59, 167, 69, 67, 171, 60, 72, 173, 54, 78, 175, 48, 83, 177, 43, 89, 179, 39, 93, 181, 35, 98, 183, 31, 105, 185, 26, 109, 187, 23, 113, 188, 21, 118, 189, 19, 123, 191, 17, 128, 193, 14, 134, 195, 12, 138, 196, 10, 142, 197, 8, 146, 198, 6, 151, 200, 5, 155, 201, 4, 160, 203, 3, 164, 204, 2, 169, 205, 2, 173, 206, 1, 175, 207, 1, 178, 207, 1, 184, 208, 0, 190, 210, 0, 193, 211, 0, 196, 212, 0, 199, 212, 0, 202, 213, 1, 207, 214, 2, 212, 215, 3, 215, 214, 3, 218, 214, 3, 220, 213, 3, 222, 213, 4, 224, 212, 4, 225, 212, 5, 226, 212, 5, 229, 211, 5, 232, 211, 6, 232, 211, 6, 233, 211, 6, 234, 210, 6, 235, 210, 7, 236, 209, 7, 237, 208, 8, 239, 206, 8, 241, 204, 9, 242, 203, 9, 244, 202, 10, 244, 201, 10, 245, 200, 10, 245, 199, 11, 246, 198, 11, 247, 197, 12, 248, 194, 13, 249, 191, 14, 250, 189, 14, 251, 187, 15, 251, 185, 16, 252, 183, 17, 252, 178, 18, 253, 174, 19, 253, 171, 19, 254, 168, 20, 254, 165, 21, 254, 164, 21, 255, 163, 22, 255, 161, 22, 255, 159, 23, 255, 157, 23, 255, 155, 24, 255, 149, 25, 255, 143, 27, 255, 139, 28, 255, 135, 30, 255, 131, 31, 255, 127, 32, 255, 118, 34, 255, 110, 36, 255, 104, 37, 255, 101, 38, 255, 99, 39, 255, 93, 40, 255, 88, 42, 254, 82, 43, 254, 77, 45, 254, 69, 47, 254, 62, 49, 253, 57, 50, 253, 53, 52, 252, 49, 53, 252, 45, 55, 251, 39, 57, 251, 33, 59, 251, 32, 60, 251, 31, 60, 251, 30, 61, 251, 29, 61, 251, 28, 62, 250, 27, 63, 250, 27, 65, 249, 26, 66, 249, 26, 68, 248, 25, 70, 248, 24, 73, 247, 24, 75, 247, 25, 77, 247, 25, 79, 247, 26, 81, 247, 32, 83, 247, 35, 85, 247, 38, 86, 247, 42, 88, 247, 46, 90, 247, 50, 92, 248, 55, 94, 248, 59, 96, 248, 64, 98, 248, 72, 101, 249, 81, 104, 249, 87, 106, 250, 93, 108, 250, 95, 109, 250, 98, 110, 250, 100, 111, 251, 101, 112, 251, 102, 113, 251, 109, 117, 252, 116, 121, 252, 121, 123, 253, 126, 126, 253, 130, 128, 254, 135, 131, 254, 139, 133, 254, 144, 136, 254, 151, 140, 255, 158, 144, 255, 163, 146, 255, 168, 149, 255, 173, 152, 255, 176, 153, 255, 178, 155, 255, 184, 160, 255, 191, 165, 255, 195, 168, 255, 199, 172, 255, 203, 175, 255, 207, 179, 255, 211, 182, 255, 216, 185, 255, 218, 190, 255, 220, 196, 255, 222, 200, 255, 225, 202, 255, 227, 204, 255, 230, 206, 255, 233, 208};
const int colormap_ironblack[] = {255, 255, 255, 253, 253, 253, 251, 251, 251, 249, 249, 249, 247, 247, 247, 245, 245, 245, 243, 243, 243, 241, 241, 241, 239, 239, 239, 237, 237, 237, 235, 235, 235, 233, 233, 233, 231, 231, 231, 229, 229, 229, 227, 227, 227, 225, 225, 225, 223, 223, 223, 221, 221, 221, 219, 219, 219, 217, 217, 217, 215, 215, 215, 213, 213, 213, 211, 211, 211, 209, 209, 209, 207, 207, 207, 205, 205, 205, 203, 203, 203, 201, 201, 201, 199, 199, 199, 197, 197, 197, 195, 195, 195, 193, 193, 193, 191, 191, 191, 189, 189, 189, 187, 187, 187, 185, 185, 185, 183, 183, 183, 181, 181, 181, 179, 179, 179, 177, 177, 177, 175, 175, 175, 173, 173, 173, 171, 171, 171, 169, 169, 169, 167, 167, 167, 165, 165, 165, 163, 163, 163, 161, 161, 161, 159, 159, 159, 157, 157, 157, 155, 155, 155, 153, 153, 153, 151, 151, 151, 149, 149, 149, 147, 147, 147, 145, 145, 145, 143, 143, 143, 141, 141, 141, 139, 139, 139, 137, 137, 137, 135, 135, 135, 133, 133, 133, 131, 131, 131, 129, 129, 129, 126, 126, 126, 124, 124, 124, 122, 122, 122, 120, 120, 120, 118, 118, 118, 116, 116, 116, 114, 114, 114, 112, 112, 112, 110, 110, 110, 108, 108, 108, 106, 106, 106, 104, 104, 104, 102, 102, 102, 100, 100, 100, 98, 98, 98, 96, 96, 96, 94, 94, 94, 92, 92, 92, 90, 90, 90, 88, 88, 88, 86, 86, 86, 84, 84, 84, 82, 82, 82, 80, 80, 80, 78, 78, 78, 76, 76, 76, 74, 74, 74, 72, 72, 72, 70, 70, 70, 68, 68, 68, 66, 66, 66, 64, 64, 64, 62, 62, 62, 60, 60, 60, 58, 58, 58, 56, 56, 56, 54, 54, 54, 52, 52, 52, 50, 50, 50, 48, 48, 48, 46, 46, 46, 44, 44, 44, 42, 42, 42, 40, 40, 40, 38, 38, 38, 36, 36, 36, 34, 34, 34, 32, 32, 32, 30, 30, 30, 28, 28, 28, 26, 26, 26, 24, 24, 24, 22, 22, 22, 20, 20, 20, 18, 18, 18, 16, 16, 16, 14, 14, 14, 12, 12, 12, 10, 10, 10, 8, 8, 8, 6, 6, 6, 4, 4, 4, 2, 2, 2, 0, 0, 0, 0, 0, 9, 2, 0, 16, 4, 0, 24, 6, 0, 31, 8, 0, 38, 10, 0, 45, 12, 0, 53, 14, 0, 60, 17, 0, 67, 19, 0, 74, 21, 0, 82, 23, 0, 89, 25, 0, 96, 27, 0, 103, 29, 0, 111, 31, 0, 118, 36, 0, 120, 41, 0, 121, 46, 0, 122, 51, 0, 123, 56, 0, 124, 61, 0, 125, 66, 0, 126, 71, 0, 127, 76, 1, 128, 81, 1, 129, 86, 1, 130, 91, 1, 131, 96, 1, 132, 101, 1, 133, 106, 1, 134, 111, 1, 135, 116, 1, 136, 121, 1, 136, 125, 2, 137, 130, 2, 137, 135, 3, 137, 139, 3, 138, 144, 3, 138, 149, 4, 138, 153, 4, 139, 158, 5, 139, 163, 5, 139, 167, 5, 140, 172, 6, 140, 177, 6, 140, 181, 7, 141, 186, 7, 141, 189, 10, 137, 191, 13, 132, 194, 16, 127, 196, 19, 121, 198, 22, 116, 200, 25, 111, 203, 28, 106, 205, 31, 101, 207, 34, 95, 209, 37, 90, 212, 40, 85, 214, 43, 80, 216, 46, 75, 218, 49, 69, 221, 52, 64, 223, 55, 59, 224, 57, 49, 225, 60, 47, 226, 64, 44, 227, 67, 42, 228, 71, 39, 229, 74, 37, 230, 78, 34, 231, 81, 32, 231, 85, 29, 232, 88, 27, 233, 92, 24, 234, 95, 22, 235, 99, 19, 236, 102, 17, 237, 106, 14, 238, 109, 12, 239, 112, 12, 240, 116, 12, 240, 119, 12, 241, 123, 12, 241, 127, 12, 242, 130, 12, 242, 134, 12, 243, 138, 12, 243, 141, 13, 244, 145, 13, 244, 149, 13, 245, 152, 13, 245, 156, 13, 246, 160, 13, 246, 163, 13, 247, 167, 13, 247, 171, 13, 248, 175, 14, 248, 178, 15, 249, 182, 16, 249, 185, 18, 250, 189, 19, 250, 192, 20, 251, 196, 21, 251, 199, 22, 252, 203, 23, 252, 206, 24, 253, 210, 25, 253, 213, 27, 254, 217, 28, 254, 220, 29, 255, 224, 30, 255, 227, 39, 255, 229, 53, 255, 231, 67, 255, 233, 81, 255, 234, 95, 255, 236, 109, 255, 238, 123, 255, 240, 137, 255, 242, 151, 255, 244, 165, 255, 246, 179, 255, 248, 193, 255, 249, 207, 255, 251, 221, 255, 253, 235, 255, 255, 24};

uint8_t mat[61][81];
int mathigh[119][159];
float min1,max1,tem1;
string linex;
ofFile datax;
ofTrueTypeFont Font1,Font2;
const int *colormap;
int color=0;
//--------------------------------------------------------------
void ofApp::setup(){
	//ofSetOrientation(OF_ORIENTATION_90_LEFT);

	// listen on the given port
	cout << "listening for osc messages on port " << PORT << "\n";
	receiver.setup( PORT );

	current_msg_string = 0;
	mouseX = 0;
	mouseY = 0;
	mouseButtonState = "";
	Font1.load("cooperBlack.ttf", 18, true, true, true);
    Font2.load("cooperBlack.ttf", 48, true, true, true);
	ofBackground( 0,0,0);
     colormap = colormap_rainbow;
}

//--------------------------------------------------------------
void ofApp::update(){
	//You might want to have a heatbeat ofxOscSender here
	//sending every 60 frames or so.
	
	// hide old messages
	for( int i=0; i<NUM_MSG_STRINGS; i++ ){
		if( timers[i] < ofGetElapsedTimef() )
			msg_strings[i] = "";
	}

	// check for waiting messages
	while( receiver.hasWaitingMessages() ){
		// get the next message
		ofxOscMessage m;
		receiver.getNextMessage(m);
        if(m.getAddress() == "/img_8"){
            linex = m.getArgAsString(0);
            if(linex[0]==0x3c) linex[0]=0;
            for(int yy=0;yy<82;yy++){
                
                if(yy != 81) mat[linex[0]][yy]=uint8_t(linex[yy+1]);
                else if(yy != 0) mat[linex[0]][yy-1]=uint8_t(linex[yy]);
            }
        }
        else if(m.getAddress() == "/minmax" ){
            min1=m.getArgAsFloat(0);
            max1=m.getArgAsFloat(1);
            tem1=m.getArgAsFloat(2);
        }

	}	
}

//--------------------------------------------------------------
void ofApp::draw(){

    string buf;

    ofSetColor(255,210,0);
    Font2.drawString("Thermalcam!!", 65, 170);

   	buf = "Min=" + ofToString(min1,2);
    ofSetColor(100,100,255);
    Font1.drawString(buf, 15, 360);
    
    buf = "Max=" + ofToString(max1,2);
    ofSetColor(255,0,0);
    Font1.drawString(buf, 460, 360);

    buf = "Temp=" + ofToString(tem1,2);
    ofSetColor(255,255,255);
    Font1.drawString(buf, 220, 310);
    
    ofSetColor(255,110,0);
    Font1.drawString("by Takesan", 460, 1070);
    
    ofSetColor(120,120,255);
    ofDrawRectangle(178,48+290,260,29);
    
    for(int i=0;i<256;i++) {      //<--------温度確認用の帯を描画
        ofSetColor( colormap[3*i], colormap[3*i+1], colormap[3*i+2]);
        ofDrawLine(i+180, 340, i+180, 365);
    }
    
    for (int i=0;i<60;i++){      //<---------まず縦横を拡張
        for (int j=0;j<80;j++) {
            mathigh[i*2][j*2]=mat[i][j];
            
        }
    }
   
    for (int i=0;i<119;i=i+2){    //<--------横を1つおきに左右の平均値を代入
        for (int j=1;j<159;j=j+2) {
            mathigh[i][j]=(mathigh[i][j-1]+mathigh[i][j+1])/2;
            
        }
    }
    for (int i=1;i<119;i=i+2){   //<---------縦の平均値を代入
        for (int j=0;j<159;j++) {
            mathigh[i][j]=(mathigh[i-1][j]+mathigh[i+1][j])/2;
            
        }
    }
    
    for (int i=0;i<119;i++){   //<-----------Circleを描画
        for (int j=0;j<159;j++) {
            
            ofSetColor( colormap[3*mathigh[i][j]], colormap[3*mathigh[i][j]+1], colormap[3*mathigh[i][j]+2]);

            ofDrawCircle(j*3.8+20, i*3.8+400, 3);

        }
    }
    
}
//--------------------------------------------------------------
void ofApp::exit(){
}
//--------------------------------------------------------------
void ofApp::touchDown(ofTouchEventArgs &touch){   //タッチしたらカラーマップを変更
    if (color==0) {
         colormap = colormap_rainbow;
        color=1;
    }
    else if (color==1) {
        colormap = colormap_ironblack;
        color=0;
    }
}
//--------------------------------------------------------------
void ofApp::touchMoved(ofTouchEventArgs &touch){
}
//--------------------------------------------------------------
void ofApp::touchUp(ofTouchEventArgs &touch){
}
//--------------------------------------------------------------
void ofApp::touchDoubleTap(ofTouchEventArgs &touch){
}
//--------------------------------------------------------------
void ofApp::touchCancelled(ofTouchEventArgs & touch){    
}
//--------------------------------------------------------------
void ofApp::lostFocus(){
}

//--------------------------------------------------------------
void ofApp::gotFocus(){
}
//--------------------------------------------------------------
void ofApp::gotMemoryWarning(){
}
//--------------------------------------------------------------
void ofApp::deviceOrientationChanged(int newOrientation){
}

以下ESP-WROOM-02側のArduinoソース。
sip及びOSCのライブラリは前回紹介したサイトからダウンロードして、自分のコンピューターのArduino/libralyにフォルダを作ってコピーする。

extern "C"{            //<------spi.h,spi.c,spi_register.hは Arduino/liblaly の中にフォルダを作って入れておく
  #include <spi.h>
  #include <spi_register.h>
}
#include <Wire.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <OSCMessage.h>

#define ADDRESS  (0x2A)
#define AGC (0x01)
#define SYS (0x02)
#define VID (0x03)
#define OEM (0x08)
#define GET (0x00)
#define SET (0x01)
#define RUN (0x02)
#define VOSPI_FRAME_SIZE (164)
#define COMMANDID_REG (0x04)
#define DATALEN_REG (0x06)
#define DATA0 (0x08)

  char ssid[] = "XXXXXXXXXXXXXXX";          // 無線ランのSSID名称:文字列。
  char pass[] = "YYYYYYYYYYY";                   //同上 password
  WiFiUDP Udp;                                // A UDP instance to let us send and receive packets over UDP
  const IPAddress outIp(XXX,XXX,X,X);        // iPhoneのipアドレス 調べ方は検索してね。これがこのソフトの欠点だが
                                                                         //iPhoneのテザリングアドレスは固定みたいなので今度挑戦
  const unsigned int outPort = 8090;          // OSC 送信用ポート番号Openframeworksと合わせること
  const unsigned int localPort = 8080;        //OSC受信用ポートのアドレス今回は使わないが使い分ければ送受信可能

  char zz[82];
  unsigned int min = 65536;
  unsigned int max = 0;
  float diff;

void setup()
{
    Wire.begin();
    Serial.begin(115200);
  
    spi_init(HSPI);
    spi_clock(HSPI,1.7,2);  //spi_clock(HSPI,5,2);spi_clock(HSPI,1.6666,2)
  
    Serial.println();
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, pass);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
    Serial.println("Starting UDP");
    Udp.begin(localPort);
    Serial.print("Local port: ");
    Serial.println(Udp.localPort());
}

static uint16_t lepton_image[63][82]; 

void read_lepton_frame(void) //<---SPIリードコマンド。同期と組み合わせないとうまく行かない
{
  int i;
  uint16_t data = 0x000f;
  delay(50); //delay(185);
  while ((data & 0x000f == 0x000f))
  { 
    data = spi_rx16(HSPI);
    lepton_image[0][0] =data;
    for (i = 0; i < 81; i++)
    {    
      lepton_image[0][i+1]=spi_rx16(HSPI); 
     //digitalWrite(15, HIGH);    
    }  
  }
  for (int frame_number = 1; frame_number < 60; frame_number++){ 
    for (i = 0; i < 82; i++)
    {
        lepton_image[frame_number][i] = spi_rx16(HSPI);
     }
   }
}

void lepton_sync(void)//<--デバッグ用=SPI同期コマンド
{
  int i;
  uint16_t data = 0x000f;
  uint16_t aaa;

  while (data & 0x000f == 0x000f)
{
    data = spi_rx16(HSPI);
    for (i = 0; i < 81; i++)
    {
      aaa= spi_rx16(HSPI);
    }  
  }
}

void print_lepton_frame(void) //<--デバッグ用=コンソール表示コマンド
{
  int i;
  for (int frame_number = 0; frame_number < 60; frame_number++){ 
    for (i = 0; i < (VOSPI_FRAME_SIZE / 2); i++)
    {
        Serial.print(lepton_image[frame_number][i] ,HEX);
        Serial.print(",");
    }
        Serial.println(" ");
  }
        Serial.println(" "); 
}

void lepton_command(unsigned int moduleID, unsigned int commandID, unsigned int command)
{
  byte error;
  Wire.beginTransmission(ADDRESS);

  // Command Register is a 16-bit register located at Register Address 0x0004
  Wire.write(0x00);
  Wire.write(0x04);

  if (moduleID == 0x08) //OEM module ID
  {
    Wire.write(0x48);
  }
  else
  {
    Wire.write(moduleID & 0x0f);
  }
  Wire.write( ((commandID << 2 ) & 0xfc) | (command & 0x3));

  error = Wire.endTransmission();    // stop transmitting
  if (error != 0)
  {
    Serial.print("error=");
    Serial.println(error);
  }
}

void set_reg(unsigned int reg)
{
  byte error;
  Wire.beginTransmission(ADDRESS); // transmit to device #4
  Wire.write(reg >> 8 & 0xff);
  Wire.write(reg & 0xff);            // sends one byte

  error = Wire.endTransmission();    // stop transmitting
  if (error != 0)
  {
    Serial.print("error=");
    Serial.println(error);
  }
}

int read_reg(unsigned int reg)
{
  int reading = 0;
  set_reg(reg);
  Wire.requestFrom(ADDRESS, 2);
  reading = Wire.read();  // receive high byte (overwrites previous reading)
  reading = reading << 8;    // shift high byte to be high 8 bits
  reading |= Wire.read(); // receive low byte as lower 8 bits
  return reading;
}

int read_data()
{
  int i;
  int data;
  int payload_length;

  while (read_reg(0x2) & 0x01)
  {
    Serial.println("busy");
  }
  payload_length = read_reg(0x6);
  Wire.requestFrom(ADDRESS, payload_length);
  //set_reg(0x08);
  for (i = 0; i < (payload_length / 2); i++)
  {
    data = Wire.read() << 8;
    data |= Wire.read();
  }
  return data;
}


void loop()
{
  int i,p;
  int reading = 0;
  String debugString;
  unsigned int col;
  float value_min,value_max,temp;
  long do_ffc = 0;

  Serial.println("SYS Telemetry Enable State");
  lepton_command(SYS, 0x19>>2 ,GET);
  read_data(); 
    delay(500);
    
    while(1){
      min = 65536;
      max = 0;

     read_lepton_frame();

     for (int frame_number = 0; frame_number < 60; frame_number++){     
       for (i = 0; i < 82; i++)
       {
          p=lepton_image[frame_number][i] ;
          if(i >= 2){ 
              if(p < min) min = p;
              if(p > max) max = p;
          }
          if(i==0) lepton_image[frame_number][i]=(p & 0x00ff);
       }
     }     

     diff = max - min;

     diff = diff / 256.0f;
     if(diff < 0.56f) diff = 0.56f; // 0.66
  //    delay(100);
        lepton_command(SYS, 0x10 >> 2 , GET); //ondo : 0x14 = chip   0x10 = aux
        float aux=read_data() ;
  //    delay(100);
        float fpatemp = aux/ 100.0f;  
        temp=fpatemp-273.15 ;         // <-----------センサー温度 単位=℃
        float fpatemp_f = fpatemp * 1.8f - 459.67f;
        value_min = ((0.05872 * (float)min - 472.22999f + fpatemp_f));
        value_min= (value_min - 32.0f) / 1.8f;
        value_max = ((0.05872 * (float)max - 472.22999f + fpatemp_f));
        value_max = (value_max - 32.0f) / 1.8f;
       Serial.println(value_min);
       Serial.println(value_max);
        //*****************Send osc data*****<------温度をOSCで送る
        OSCMessage msg("/minmax");
        msg.add(value_min).add(value_max).add(temp);
        Udp.beginPacket(outIp, outPort);
        msg.send(Udp);
        Udp.endPacket();
        msg.empty();   
        //***********************************     
        delay(2); 
   //*******************14bitを8bit階調に変換*********************
    for (int frame_number = 0; frame_number < 60; frame_number++){ 
       for (int i = 2; i < 82; i++)
       {
            col=lepton_image[frame_number][i] ;
            col = col - min;
            col = col / diff;
            if(col <= 0) col = 1;  //=0  <----0にすると文字列がそこで途切れるので最低は1とした
            if(col > 255) col = 255;
            zz[i-1]=col;
       }
     zz[0]=frame_number;   //<----センサーから受け取ったデータは2byte目がフレームナンバーだが改めて振り直し
     if(frame_number==0) zz[0]=0x3c; //  <--行番号0だと文字列が切れるため、0x3c=60に変更 
     zz[82]='\0';   //  <-----------------------文字列なので最後に必ず入れる!!

      //*****************Send osc data*****<--行番号を最初に入れた文字列を送る
      OSCMessage msg("/img_8");
      msg.add(zz );
      Udp.beginPacket(outIp, outPort);
      msg.send(Udp);
      Udp.endPacket();
      msg.empty();   
      //***********************************
      delay(1);       // <------- 必ず必要これがないとうまく通信できない
    }
      delay(100);       // <----必ず必要
   }
}

最後に

疲れた..........................。年だぁ......................。

スイッチサイエンスの ESP-WROOM-02 開発ボードで、FLIR LEPTON 赤外線カメラを動かしてみる(その1)

今回は,

こんなのを作ってみました。  

   f:id:TAKEsan:20160217151437j:plain:w500
             こんな風につないで(電源はeneloop電池)

        
Macで画像表示させると(赤外線カメラ->ESP-WROOM-02->WiFi(OSC)->Mac(Openframeworksアプリ))


 前回の記事でちょっと紹介しましたが、赤外線カメラ(FLIR LEPTON 80X60 ドット)を手に入れました。これを使った商品はiPhone用にも販売されていて、とってもかっこ良くて性能の割には比較的安いのですが、センサーそのものの方が若干価格が高いというような逆転現象が起きています。
    f:id:TAKEsan:20160216140354j:plain:w200f:id:TAKEsan:20160216140351j:plain:w200
 しかし高い。いつものように「どこから買ってもいいや」でなく、購入先を検討。とても慎重になってしまいます。(スイッチサイエンス、秋月、 Degi-Keyで扱ってます)
どうせならということで、あまり値段が変わらないシャッター付きにしました。表面に付いているシャッター部品は、簡単に取り外せますが、取り付けている爪が折れやすいので注意。定期的にシャッターを閉じて、センサーのキャリブレーションを行っています。シャッターなしのセンサーも販売されていますが、時間の経過とともに画像のノイズが増えてくるので長時間の使用には向かないようです。また、シャッター無しとシャッター付きでは部品形状やファームウェアに違いがあるようで、両者間でシャッター部品の使い回しはできません。

          f:id:TAKEsan:20160205115549j:plain:w300    

FLIR LEPTON 赤外線センサーについて

 i2cとSPIを使っていて、i2cは制御用、SPIは画像配信用に使っているようです。とりあえずのお試し用ならSPIだけでいいみたいですが、私の持っているIoT制御機器で実行できるのかどうかが疑問ではありますが、ざっとわかった範囲で処理の流れを書くと、

  1. センサー(カメラ)に組み込まれている温度センサーを基準にして、センサーで読み取れる範囲の最高最低温度を計算。
  2. 最高最低温度を基準にして、各60X80=4800ピクセルで読み取った値を加味した濃淡データを計算(結果が14bit=8192階調なのでかなり精密)。
  3. 2+80word(=164byte 最初の2wordは行番号など)のデータを1フレームあたり60回SPIで流す。
  4. 出力されたデータを8192階調から256階調にホスト側で修正。(i2cの制御で8bitRGBに変換して出力できますが、今回は無視)

 センサー周囲又はセンサーの発熱で温度が変わっても、あくまでも基準ですからデータの信憑性は変わりません。画像配信だけならセンサー温度を考慮しなくてもOK。
 じゃー読み取ったデータの正確な温度を知るには? i2cで現在のセンサー温度を読み取って、逆算してやれば4800ピクセルのどの位置でも温度が数値で出てきます。
 このセンサと、Webカメラから赤外線フィルタを取っただけのものと大きな違いは、赤外線ライトが必要ない事。4800ドットのすべてのピクセルで温度が測定できる点です。
 他の非接触赤外線センサーと1ピクセルあたりの金額を比較すると、LEPTON=7円、OMURON≒400円、MXL90621 ≒150円 と、このセンサーは圧倒的に安いけれども個体ではやっぱり高い。 
       f:id:TAKEsan:20150809111402j:plain:w100      [f:id:TAKEsan:20160111134338j:plain:w200[
     OMURON非接触赤外線センサー           MLX90621

現在(2017/11/20)は限りなくバージョンアップしてます

LEPTON1は元より
takesan.hatenablog.com
LEPTON3ではもっと素晴らしくなりました
takesan.hatenablog.com
マザーボードは、これを使えば、WiFiの飛距離が大きく伸びます。
信じられますかー!!。100m飛ぶんですよ!!
www.switch-science.com
まさにミラクル!!

Pi2でさっそく実験!!

 Sparkfunにチュートリアルがあったので、https://learn.sparkfun.com/tutorials/flir-lepton-hookup-guideで試してみたら簡単に実行できちゃいました。でも...Piは、Iotとして使うには大きすぎる。

        
         Pi2用のExample結構早い。このくらいのスピードが理想的

Edisonでは?

 では、充分ちっちゃいEDISONでは? 同じサイトにEDISONのソースも入っていたので、Spiもi2cも考えられるすべてのことを試して見てもまともにつながりません。特にi2cは、Edison内部のソフトPullupを最大限活用してもダメでした。 調べてみるとEdisonとLeputonは、i2c接続が不可能の模様です。つまり制御ができない。
 このセンサーのEdison接続に関して2015年の1年間Webの書き込みがないので、これは絶対無理の模様。前の記事で紹介したセンサーもそうでしたが(かえって結構すごいことができた)Edisonのi2cは、癖があるようです。一般的なセンサーは問題ないけど、特殊なやつだとちょっと.....。
 実験しただけでも2日かかったので、Edisonでは断念。SPI接続に関しては今後挑戦して行こうと思ってます。

ところで、本題のESP-WROOM-02開発ボード

         f:id:TAKEsan:20160216224046j:plain:w300
これは本格的にいい!!理想的です。
 今まで私が考えてたこの種のボードの理想的最小限構成。つまりSPI、i2c、最低限のデジタルピン、それにArduino Ideで動く。そしてWiFi。そこそこのスピードと容量。ちっちゃい。安い。全部網羅しています。さらに付け加えるなら安定してる。下手に私の生理的に嫌いなBluetoothまで手を出してないところがさらに良い!!。あの手強いMLX90621でさえ、試してみたらサクサク動く(これが動けば感覚的に他のi2cセンサーは全く問題なし)、Arduino Ideの利用方法はスイッチサイエンスさんが紹介してますので、このまま設定するだけ。とても簡単です。
 コンパイルと転送に少し時間がかかりますが、許容範囲。その昔興味本位で試したCPM のBDS C(なんと30年前の代物。その頃確かコンパイルスピードが早いともてはやされていた)の体感したスピードより早い。.....比べる時代がおかしいか。
 ダメ元と思ってこの「ESP-WROOM-02開発ボード」でFLIR LEPTON に挑戦してみました。メーカーのソースはこのページ-->https://github.com/groupgets/LeptonModule
 まず今回のセンサーのArduino成功例を探して実行。i2cは問題なしで繋がるが、案の定肝心のSPI画像配信信号の読み取りは全くダメ。
 Edisonと同じです。SS信号(hight,low)の高速化とクロックスピードが鍵となるようですけど、ArduinoのSystem変数を使ってるので、Edisonと同じようにESP-WROOM-02でもそのままコンパイルできません。仕方がないのでSSピンを強制的にOn Offすることにしましたが、これでも全然ダメ。でも確かに読み込んでいる挙動があります。
 Arduino標準SPIライブラリでこのセンサーを操作するのは無理と判断したので、SPIに関する外部ライブラリーを探してみました。で、発見。おまけにESP-WROOM-02のArduinoライブラリまで入っていました。
https://github.com/MetalPhreak/ESP8266_SPI_Driver
 最初はこれを使っても全く動かない。このライブラリのSPIの選択では、HSPI,SPIの指定があるので、ESP-WROOMのマニュアルに書いてあるHSPIにしてみると、やったー。それらしいのを表示。SPI読み込みはArduinoのSPIライブラリと違って1行で済むので、ソースの見た目も納得。
 ただFLIR LEPTONから出力された最後の画像の12ラインをどうしても読み取れない。これはスピードとタイミングの問題という勘が働いて、ライブラリのコマンドを調べてみたら、speedコマンドがあることを発見。多分クロックスピードの調整用と思い、試しに spi_clock(HSPI,1.7,2) にすると なんと全てのラインを読み込んでしまいました。
 ただし、要所に入れたDerayの長さによって通信が不安定になることもわかりました。このライブラリと今回のセンサーを使う限りスピードの調整が必要です。SPIに関して全く無知なので、反復学習するしかありません。学習結果のテストプログラムは次のようになりました。温度の計算方法は、この方の 0.ht - Simple thermal camera using a FLIR Lepton modulehttp://0.ht/thermal
プログラムを使わせていただきました。
 今回の記事を応用して、最終的にこうなりました。自分的にはかなり満足です。     takesan.hatenablog.com

FLIR LEPTON から読み取ったデータを行単位でコンソールへ16進表示
extern "C"{            //<------spi.h,spi.c,spi_register.hは Arduino/liblaly の中にフォルダを作って入れておく
  #include <spi.h>
  #include <spi_register.h>
}
#include <Wire.h>

byte x = 0;
#define ADDRESS  (0x2A)
#define AGC (0x01)
#define SYS (0x02)
#define VID (0x03)
#define OEM (0x08)

#define GET (0x00)
#define SET (0x01)
#define RUN (0x02)

#define VOSPI_FRAME_SIZE (164)
#define COMMANDID_REG (0x04)
#define DATALEN_REG (0x06)
#define DATA0 (0x08)
#define IMAGE_SIZE (800)
byte image[IMAGE_SIZE];
int image_index;
uint16_t lepton_frame_packet[VOSPI_FRAME_SIZE];
  unsigned int min = 65536;
  unsigned int max = 0;
  unsigned int pixel;
  float diff;

void setup()
{
 //pinMode(15, OUTPUT);
  Wire.begin();
  Serial.begin(115200);
  spi_init(HSPI);         // <-----------------FSPI or SPI
  spi_clock(HSPI,1.7,2);  // <-----------------FSPI or SPI 1~2,2
  Serial.println("setup complete");
}

static uint16_t lepton_image[63][82]; 

void read_lepton_frame(void)
{
  int i;
  uint16_t data = 0x000f;
  delay(50);             // <-----------------重要!! 
  while (data & 0x000f == 0x000f)
  {
    data = spi_rx16(HSPI);
    lepton_image[0][0] =data;
     for (i = 0; i < ((VOSPI_FRAME_SIZE - 2) / 2); i++)
    {
      lepton_image[0][i+1]=spi_rx16(HSPI);  
    }  
  }

  for (int frame_number = 1; frame_number < 60; frame_number++){ 
    for (int i = 0; i < 82; i++)
    {
        lepton_image[frame_number][i] = spi_rx16(HSPI); //<----SP_I read
     }
   }
  }
  
void lepton_sync(void)
{
  int i;
  uint16_t data = 0x000f;
  uint16_t aaa;
delay(10);
  while (data & 0x000f == 0x000f)
{
    data = spi_rx8(HSPI)<< 8;
    data |= spi_rx8(HSPI);
     for (i = 0; i < ((VOSPI_FRAME_SIZE - 2) / 2); i++)
    {
      spi_rx8(HSPI);
      spi_rx8(HSPI);
    }  
  }

}

void print_lepton_frame(void)
{
  int i;

  for (int frame_number = 0; frame_number < 60; frame_number++){ 
    for (i = 0; i < (VOSPI_FRAME_SIZE / 2); i++)
    {
        Serial.print(lepton_image[frame_number][i] ,HEX);
        Serial.print(",");
    }
        Serial.println(" ");
  }
        Serial.println(" "); 
}

void lepton_command(unsigned int moduleID, unsigned int commandID, unsigned int command)
{
  byte error;
  Wire.beginTransmission(ADDRESS);

  // Command Register is a 16-bit register located at Register Address 0x0004
  Wire.write(0x00);
  Wire.write(0x04);

  if (moduleID == 0x08) //OEM module ID
  {
    Wire.write(0x48);
  }
  else
  {
    Wire.write(moduleID & 0x0f);
  }
  Wire.write( ((commandID << 2 ) & 0xfc) | (command & 0x3));

  error = Wire.endTransmission();    // stop transmitting
  if (error != 0)
  {
    Serial.print("error=");
    Serial.println(error);
  }
}

void set_reg(unsigned int reg)
{
  byte error;
  Wire.beginTransmission(ADDRESS); // transmit to device #4
  Wire.write(reg >> 8 & 0xff);
  Wire.write(reg & 0xff);            // sends one byte

  error = Wire.endTransmission();    // stop transmitting
  if (error != 0)
  {
    Serial.print("error=");
    Serial.println(error);
  }
}

int read_reg(unsigned int reg)
{
  int reading = 0;
  set_reg(reg);
  Wire.requestFrom(ADDRESS, 2);
  reading = Wire.read();  // receive high byte (overwrites previous reading)
  //Serial.println(reading);
  reading = reading << 8;    // shift high byte to be high 8 bits
  reading |= Wire.read(); // receive low byte as lower 8 bits
  return reading;
}

int read_data()
{
  int i;
  int data;
  int payload_length;

  while (read_reg(0x2) & 0x01)
  {
    Serial.println("busy");
  }

  payload_length = read_reg(0x6);

  Wire.requestFrom(ADDRESS, payload_length);
  //set_reg(0x08);
  for (i = 0; i < (payload_length / 2); i++)
  {
    data = Wire.read() << 8;
    data |= Wire.read();
  }
  return data;
}

void loop()
{
  int i,p;
  int reading = 0;
  String debugString;
  unsigned int col;
  float value_min,value_max;
  
  Serial.println("Start!!");
  lepton_command(SYS, 0x19>>2 ,GET);
  read_data();
    while(1){
      min = 65536;
      max = 0;
      
     read_lepton_frame();
     
     for (int frame_number = 0; frame_number < 60; frame_number++){     
       for (i = 0; i < 82; i++)
       {
          p=lepton_image[frame_number][i] ;
          if(i >= 2){ 
              if(p < min) min = p;
              if(p > max) max = p;
          }
          if(i==0) lepton_image[frame_number][i]=(p & 0x00ff);
       }
     }     
     diff = max - min;
     diff = diff / 256.0f;
     if(diff < 0.56f) diff = 0.56f; // 0.66

        lepton_command(SYS, 0x10 >> 2 , GET); //センサー温度i2cから取得
        float aux=read_data() ;

        float fpatemp = aux/ 100.0f;
        float fpatemp_f = fpatemp * 1.8f - 459.67f;

       Serial.println(fpatemp-273.15);
        value_min = ((0.05872 * (float)min - 472.22999f + fpatemp_f));
        value_min= (value_min - 32.0f) / 1.8f;    
        value_max = ((0.05872 * (float)max - 472.22999f + fpatemp_f));
        value_max = (value_max - 32.0f) / 1.8f;

       Serial.println(value_min);
       Serial.println(value_max);
       
    for (int frame_number = 0; frame_number < 60; frame_number++){ 
       for (int i = 2; i < (VOSPI_FRAME_SIZE / 2); i++)
       {
            col=lepton_image[frame_number][i] ;
            col = col - min;
            col = col / diff;
            if(col <= 0) col = 0;
            if(col > 255) col = 255;
            lepton_image[frame_number][i] =col;
       }
    }
      print_lepton_frame();
      delay(100);           // <-----------------重要 100以上
   }  
}

製造元で公開しているArduinoソースコードの内容とは肝心のSPI信号読み取り部分(read_lepton_frame)がだいぶ違っています。当方勘と実験だけでプログラムを動かしてますので、この筋の専門家の方々はもっとスマートにできるんでしょうね。あーうらやましい。あと30歳若かったらなんて思ったりしますが、私の性格では遡ったって多分同じでしょうね。
 で、これを実行させると、ちゃんとデータをすべて受け取っているみたいです。だって文字表示だけなのに、赤外線をで読み取った画像の移動がわかります。
 ここまで来たら、もうこっちのもの。まだメモリを半分ぐらいしか使ってないみたいなので、本格的な赤外線画像をWEB配信するなり、手持ちの小型ディスプレイに表示するなり面白そうな実験ができそうです。
 そこでまずMacとESP-WROOM-02の通信方法を検討。 https://www.mgo-tec.com
 この方は非常に魅力的なブログを書いています。よく読んでみると、どうやら今回のような比較的容量の大きなデータ送受信は難しい模様。さらにiPhoneであまり良い結果が出てないようなので今回は使わないことに。ただしこの方のアイディア・表現力・手軽さはピカイチです。今後の進展に期待してしまいます。
 じゃーどうする? せめてOSCが使えたらなーなんて探したらこんなのありました(前に別のArduino版OSCライブラリがまともに動かなかった)。しかもESP8266用のも入っています。
   OSC for ARDUINO-->https://github.com/CNMAT/OSC
 問題なくコンパイルできたので、MacのOpenframeworksのOSCサンプルをちょっと修正して通信を試したら、Good!。 これをiPhoneに載せ替えたら 超小型遠隔操作赤外線カメラができそう。
 OSCでは画像ファイルが送られるようなのですが、Openframeworksとこのライブラリでは仕様が違うので送れない。そもそも256色だけのデータをjpeg等にエンコードするだけでもメモリと実行時間が無駄にかかりそう。しょうがないので文字配列として1行80文字分のデータ60行を1画面として送ることにしました。(いろいろ試してみるとStringの場合最大で450文字ぐらいは送れるようなので5ライン分くらいはまとめて送信できる)最終的にでき上がったものは1秒あたり3フレーム程度です。このセンサーの利用方法を考えたら十分許容範囲だと思います。

さらに補間して詳細表示

 一応Macで作ったアプリとESP-WROOM-02からWiFi経由で送ってみたのが、冒頭の画像です。改めて感激!!。しかもeneloop電池で動いてるんですよ。満充電以降2ヶ月以上ほったらかしにしていた電源でも連続で4時間以上稼働します。
 気を良くしたので、画像を倍に補間してみます。今までの赤外線センサーとは違い、画素が多いので、ラグランジェの曲線補間などしなくても単純な前後左右の平均だけで充分と思い、Openframeworks側で倍に上げてみました。比べてみるとこんなに違います。
   f:id:TAKEsan:20160216214109p:plain
センサーのピクセル数のまま80X60ドット表示。Openframeworksでは1ピクセル5X5ドットの四角形として描画。表示がギザギザ。PIのExampleと同じです。Piと画像の雰囲気が違うのは、カラーマップを変えたから。
   f:id:TAKEsan:20160216214110p:plain
152X119に補間して半径1.5ドットの円として表示。全然違うでしょ。キュッと引き締まります。しかもMac側で処理しているのでスピードはほとんど変わらない!!
 256階調でこんな感じなのですから、このセンサーの本来の性能である8192階調で表示したらすんごいことになりそうです。ただ、どのようにして色で表現するのか......が、問題。         
        
                 倍に補間した画像。
※電源をコンピューターから取ると、かなり画像が乱れるので、バッテリーがオススメです。

次回は、実際のプログラムを紹介します。 iPhoneでも実行できそうなので、できたら一緒に紹介します。
takesan.hatenablog.com
Pi zeroでLEPTONの動画配信できました。スピードは倍以上!!
takesan.hatenablog.com

質問への対応

インストールに成功したライブラリは以下からアップできます。(SPI及びOSC)
id:TAKEsan の Driver.zip
ライブラリの保存場所は
f:id:TAKEsan:20170112234157p:plain
です。Macの場合  書類/Arduino/libraries に入れます。

  • Macでテストする場合のOpenFrameworksのソースをダウンロード可能にしておきました。解凍後apps/myApps へフォルダごと移動させビルドしてください。

id:TAKEsan の oscReceive-LEPTON.zip
WROOM02側のソースは次のページに書いてあるソースを参考にしてください。無線LANのIPアドレスとパスワード、MacのIPアドレスを自分の環境に合わせて修正することをお忘れなく。



CEL ROBOX その後(その2)

プリント ヘッド到着!!

 ノズルが詰まって新しいヘッドを注文してから16日目。やっと手元に届きました。

f:id:TAKEsan:20160205104156j:plain

 届いたヘッドはノズルが真鍮でなくステンレス風。パッキングとかも微妙に違ってました。今回は、本体が手に入った時、省略した調整方法もさっと取り上げてみます。

  ヘッドの調整は、ベッドやヘッドが所定の温度まで上がるにのに時間がかかるABSより、PLAの方がいいみたい。

  まず、日本の代理店の推奨であるレベルカントリー、パージマテリアルを行います。

      f:id:TAKEsan:20160205104152j:plain

 パージマテリアルは、こんな感じで太さの違う両方のヘッドから材料を交互に押し出します。この操作で、ヘッドの中にある古いフィラメントを排出します(違う種類の材料を使う時実施)。今回は製品検査のためか、白いフィラメントが入っていました。

  ROBOXは出荷時キャリブレーションテストをしているみたいですが、遠くから運ばれてきているので、パージマテリアルを行って様子がおかしかったら、以下のキャリブレーションテストを実行した方が良いかもしれません。

キャリブレーションプログラムの順番は、次の通りです。

   f:id:TAKEsan:20160205110539p:plain

1…Nozzle Opening(ノズル解放)

 これ適当にやってしまうと、太い方のノズルからフィラメントが途中で漏れ出す危険あり。

 最終的に私に届いたヘッドでは、左0.55、右0.7くらいでした。少しずつゆっくりとボタンを押して、ほんの少しフィラメントが出てきたくらいがちょうどいいみたいです。

      f:id:TAKEsan:20160205104155j:plain

 写真のようにノズルからフィラメントが出すぎた時点だと、押し出し圧力がちょっと強すぎるようです(左側の細い方の圧力を強めると、ノズルが内部で繋がっているので太い方のノズルに影響が出る)。最初は気にもしなかったのですが、試験プリントを実施して、途中から太い方のノズルからフィラメントが漏れ出してしまい、造形物がめちゃくちゃになってしまいました。なので慎重に!!

  こんな記事で酷評してる人がいますが、

www.itpro.co.uk

特に記事の中の犬の模型。これは相当難しいプリントで、この人は記事の中身はどうあれサポートをONにしてません。さらに足の部分が相当細いのにRAFTやプリムもONにしていない様です。ヘッドの調整がうまくいっていない様にも見えます。 ABSが詰まってソフトがなんとかかんとかといってますが、ヘッドを外せば簡単にフィラメントは取れるし、少し慣れれば現状はほとんど問題なし。WEB怖いですね。

 2…Nozzle Height(ノズルの高さ調整)

 最初はコピー用紙を使ってましたが、プリント中に糸を引くことが多くなったため(ノズルの先が造形物に当たる)、名刺を使ったら改善されました(最終的に0.15mm程度になった)。これはベッドの上の樹脂カバーを取ってアルミの印刷台の上で実行します。

     f:id:TAKEsan:20160205104154j:plain

3…XandY Offset

 テストプリントした造形物を確認して X Yの正確な位置決めをするメンテナンスだそうです。

こんな感じ。パターンのずれがない部分をソフトに入力します。

     f:id:TAKEsan:20160205104153j:plain

 そして、

 Pi用のIGZO液晶パネルを手に入れました。裸のままではフラットケーブルが危なすぎるので、早速ケースを作ってみました。

     f:id:TAKEsan:20160205104246j:plain

 モデルはここを利用させていただきました。

www.thingiverse.com

 そのままでは、縦置きのプリントになってしまい、ROBOXのプリント限度を超えてしまうので、3DCADで修正。平置きにしました。

   f:id:TAKEsan:20160205112131p:plain

 この3Dモデルは、裏蓋の爪の部分が華奢で、ROBOXのNOMALモードでの印刷が最初失敗してしまいました(ノズル解放をうまく調整していなかったので太い方から滲み出てきたフィラメントが悪さをした)。プリント時間がかかりますが、ノズル解放を再調整して、FINEにしてみても失敗。

        f:id:TAKEsan:20160205104247j:plain

 よく見てみると爪の部分を作っている時、まだ固まっていないうちに次の層を作っているので、柔らかい直前の層ごと破壊されているようです。小さな爪を造形中でもヘッドの移動速度が普通と変わりません。つまりWAITが必要。

 以前の記事で、テスト用の小さなモデルをプリントしている時は、三角錐の頂点付近になるとヘッドの移動スピードが極端に落ちて、うまくプリントしてました。多分ソフト側の問題だと思います。

       f:id:TAKEsan:20151114213803j:plain

ここで頭を使います。発想を変えて裏返しにしたらどうなるか?

   f:id:TAKEsan:20160205104326p:plain

 RAFTやプリムを使うんです。平面部分が持ち上がってるんでサポートはON。設定はこうしました。

          f:id:TAKEsan:20160205104331p:plain

 材料と作成時間は無駄に消費しますが、小さな部品プリント時の冷却時間は稼げます。そして予想通りOK!!

     f:id:TAKEsan:20160205104245j:plain

 3Dプリンタ道。技術系の鉄則ですけどノウハウが結果に繋がります。深いですねー。これがあるからやめられない。

て、ことでまたまた面白かった一時でした。

f:id:TAKEsan:20160205104333j:plainf:id:TAKEsan:20160205104332j:plain

ROBOXは、NOMALモードでも最下層と最上層は手を抜いてないので、仕上がりが綺麗。表側の枠は磨いていますのでもっと綺麗ですけど。

 

エエッ!!

 この頃ついてません。せっかくプリントがうまくいったのに、今度はIGZO液晶パネルのフリップロックがいかれてしまいました。(酔っ払って組み立てていたバチが当たった)問い合わせしてますが、基盤だけ入手できるかどうか?

 注文している2色ヘッドが来るまで3Dプリンタ記事はしばらく休憩。

 次回は

 赤外線温度センサーに、はまってしまい、とうとう高級品に手を出してしまいました。80x60ドットです。

            f:id:TAKEsan:20160205115549j:plain

 Pi2ではかなりうまく動作しますけど、Edisonでは全くダメ。スイッチサイエンスの

ESP-WROOM-02開発ボードで動いたりしたら........。 今度こそ自分の不注意で壊さないようにせねば。

 

  動いてしまいました!!その後iPhoneから高速でESP-WROOM-02を制御できちゃいました。

 

takesan.hatenablog.com

 では、また...........。

 

 

 

 

 

 

 

 

 

CEL ROBOX その後

姪が家を建てたので

何か贈り物をと思いました。

そこで、電子工作。結構モダンなものに敏感な夫婦なので、ちょっと洒落たものをと思って探してみると、こんな時計がありました。

www.thingiverse.com

 試しにスイッチサイエンスで部品を揃えて、3Dプリントしてみました。手に入れた木質フィラメントで作ってみたのがこれです。

      f:id:TAKEsan:20160121113534j:plain

ROBOX専用木質フィラメントでプリント。100%PLAだがNomalでプリントすると微妙な色の濃淡があり、質感が木に近い。

 言葉で時を知らせるというコンセプトで、とても微笑ましく、何よりかなり美しい表現なので、これだと思いました。ただし、このままでは見た目がちょっと。

 調べたら金属質感を出せるフィラメントがあったので早々青銅の質感をもったものを手に入れました。(ROBOX標準フィラメントであるColorFaab製)欠点は、金属含有率が高く、材料が脆いためヘッドや、送り出し部分で詰まりやすいとか。 でも、めげずに早速フィラメントの台をプリント。

      f:id:TAKEsan:20160121113531j:plain

 いよいよと思ったら、縦に置いたフィラメントが倒れてしまい、あまりにも重い自重でカバーが破損!!せっかく作った部品が使えなくなった。 何か悪い予感が....。

      f:id:TAKEsan:20160121113529j:plain

 同種のフィラメントを本体に入れておかないと印刷できないので、PLAをセットして特殊フィラメントを挿入。自動的に引き込まれるまで手で押し続けると、材料が2回も折れてしまい、さらに嫌な予感...。

 パージノズルを実行後(でもちょっとフィラメント排出の様子がいつもと違う)、造形開始。順調にプリントしていたのですが、底を厚くしたかったので中断し、再度造形開始。ものすごいブザー音がして本体停止!! 予想的中です。

      f:id:TAKEsan:20160121114538j:plain

          中断したもの。磨いていないが、重い!!

 この時は、再度パージなどをすればなんとかなると思いましたが、エラーメッセージが出るだけで、2つのノズル共フィラメント排出しません。完全に詰まってしまいました。

 しょうがなくマニュアルに沿ってヘッドを取り外し(とても簡単)。まず、フィラメントが正常に送り出されているかチェック。一応ヘッドまで自動的に送り出しているので、一安心。

      f:id:TAKEsan:20160121113533j:plain

    ヘッドを取り外した状態。フィラメントを再挿入すると、先がヘッドまで出てきた。

 メーカー保証外のこと(未知のフィラメント挿入)を実施しているので、最悪ヘッド交換すれば良いと思い、こんな人もいるので

imgur.com

強行分解を決断。

 ヘッドを開けてみると、途中に分断されたフィラメントがないので、最悪となるノズルの詰まり以外は考えられないことが判明。(何回も無理なパージを繰り返した結果、内部圧力により結構いろんなところからフィラメントが漏れていた)

f:id:TAKEsan:20160121113528j:plain

フィラメント挿入部がノズルの真上ではなく、横に存在。今回この部分にはフィラメントのかけらが無かった。2つのフィラメント挿入口の内、左側からフィラメントを挿入し、内部で繋がっている模様(右側はダミー)。このヘッドは最初から2色出力を意識してたんでしょうね。新しい2色用のヘッドを調べてみるとヘッドが完全に分離し、ヒーターが各々に付いています。そうなると溶解温度の違う2種類の素材が使えることになります。

f:id:TAKEsan:20160121113552j:plain

ノズルの下についているバネ内部に多分ノズルの穴まで届く針が入っている様子。左右のノズルが上下に動く仕組みが理解できない。ノズル手前の穴は多分メンテナンス用で、本来ネジで閉鎖されている。左下はステッピングモーターで右側は全部ヒートシンク件シャーシ。冷却している部分が絶妙ですね!。ヘッドのファンが動いている限り、ノズル挿入直前までフィラメントが融けないようになってます。リード線が出ているところはヒーターです。

 NET上ではノズルが詰まった時は、針金を突っ込むとか色々な方法が書いてありますが、ROBOXの場合構造が複雑なため、素人では(ノズルの穴を内部から針で塞いだり開けたりする構造->多分材料が瞬時に遮断されるため、無駄な材料の吐出や吸引による時間的無駄が無く、糸が出にくくなっている反面、構造が複雑でフィラメントの品質にノズルトラブルが左右されると思われる)修理不能。

 色々迷った挙句、イギリスまでの送料修理代を考えるとヘッドを修理するより、再注文した方が良いと決断しました。RSオンラインで取り扱っていましたが、到着が15日後とか。あーあーあーあー。でも良い方に考えれば、今回ヘッドの分解方法や構造がわかったので、ノズル詰まり以外なら自前で修理可能ということです。

 この際なので、まだ出荷未定の2色用のヘッドをイギリスへ注文(今なら大体1万円安)しました。今後は、0.3+0.8mmノズルと0.4mmのノズルが2本となるので各部の注油さえ真面目に行なっていれば当面大きなトラブルが起きても大丈夫です。現行のヘッドは、基本的に0.3mmを主に使ってプリントしていて、納得の品質ですが、使っていてスピード的には限界も感じます。2色用のノズルは0.4mmとなり、これは一般的に流通している多くの製品と同一です。この機種独特のノズル機構で、本来のスピードが出るであろうことに期待するところです。

 今回のノズル詰りの原因は、動作を中断し、ヘッドを冷却している過程で、ノズルの中で溶けていた材料に含まれている金属粉がノズルの穴とそれを開閉する針の間に集中したか、両方のノズル共材料が排出されないことから、ノズル後方の針の貫通部に金属粉が詰まり針(ノズルの開閉弁)が動かなくなったかのいずれかの様です。

 一般的で単純なノズル上方からフィラメントを挿入するヘッドの場合は、障害となるのはノズルの穴だけなので、今回使用した特殊フィラメントでもクリヤできるのでしょう。

ROBOX 3Dプリンタに関し今回わかったこと

絶対に安物のフィラメントや、木片や金属粉末など異物が混入されたフィラメントを使わない!!

 

 今回の件は、すべて私の不注意から引き起こしたことです。同じようなトラブル(特殊フィラメント使用)とならない様、あえて記事としました。そして今回の分解にあたり、一般的なABSやPLAを使用して、定期的にAutoMaker(ROBOXの造形ソフト)でメンテナンスを実行すれば、ほとんど問題が起きないことを改めて再確認できました。今後もこの機械を大切に使って行こうと思います。

3Dソフトその後

 3Dソフトについて色々試してみました。123D desinがタダで有名で、確かに使い勝手はとても良いのですが、私のMacではあまりにもフリーズしすぎて使い物になりません。他の市販品を調べてみると、価格的にとても趣味の領域で手に入れられるものではありません。そんな中で、AutoDiskのfusion360 は、市販品と同じ内容で、実質タダ。試してみると、選択肢はこれしかないと思いました。123D desinと同じ感覚でしかも痒いところに手の届く使い勝手です。このソフトも私のMacでたまにフリーズしますが、まー我慢できる範囲なので良しとしました。123D desinをプロ用にしたとでもいいましょうか。

 

Intel Edison で ArduinoとPythonのプロセス間通信 その2(動画配信)

 前回は1つのEdison の中で動く Arduinoネイティブプログラムと、Pythonプログラムのプロセス間通信で、MLX90621センサーから4X16のデータを取り出しました。
 今回はそのデータを利用してEdisonから画像配信してみます。最終的にはこの写真のようになりました。(Henry基盤に接続したWeb Cameraとセンサーを使って、Edisonで合成して配信!!)
     f:id:TAKEsan:20160111133222p:plain
             右端の赤い点がダメにした測点

今回は2種類のEdison用Eagletボードを使いました

 Arduino側では前回出力した温度データではなく、温度データをRGBカラーに変換する下準備まで実行させます。これは画像処理をPythonで行うため、時間ロスを少しでも少なくするためです。
 温度データを色データにする方法は、測定した最大温度と最小温度の差を255等分。それから各点の温度を0〜255に変換して、Python側でRGBに振り分けてます。
 Python側ではSimpleCVでカメラ画像と温度データを合成したmjpeg動画をWeb配信させます。4X16 すなわち64個の四角形を画像と合成して配信すればどうなるか(どれだけ遅くなるか!!)が見ものです。
でーーーー、実行してみると、画像の大きさはともかく、思いの外早いので驚きです。下の動画を見て下さい。中央のSafariに赤青の濃淡でセンサーの読み取り結果が表示されています。どうですか?実験してみたくなったでしょ。
       
 今回はWebCameraの接続が簡単で、私の環境で3.3V I2cが使えるようにしたHenryボード上で最終的に動かします。このボードは前にも書きましたがArduinoのコードが転送できないので、コンパイルまでをスイッチサイエンスEagletで行い、できた実行形式ファイルをHenryに乗せたEdisonにコピーして使います。今回の場合、センサーがArduinoコードでしか動かないためEdison Arduinoのi2c接続先であるi2c-6の使えるボードであることが条件です。
 最初はブレッドボードでテストしていましたが、接触不良になる場合が多いので、前回の回路を基板にしてみました。Henry基盤のUSBにWeb Cameraを接続しています。
f:id:TAKEsan:20160111130945j:plain
センサーはこんな感じ
f:id:TAKEsan:20160111134338j:plain

では、コード紹介

こちらはPython部分

 相変わらずこんなめんどくさそうな処理をしているのに簡単に書けます。(Arduinoで間違って256分割してしまいRGB変換時エラーになってしまうので、255以上にならないように3を引いてます。)カメラ画像と合わせるためにセンサーの表示は左右反転しています。今後直すとすれば強制終了させた時にondo.elfをkillする処理でしょうか。

from SimpleCV import *
import os
import time

os.system("mke2fs /dev/ram0")
os.system("mount -t ext2 /dev/ram0 /mnt/ram0")
os.system("rm /mnt/ram0/*")
os.system("mkfifo /mnt/ram0/pipe1")
os.system("mkfifo /mnt/ram0/pipe2")
os.system("/sketch/./ondo.elf /dev/ttyMFD0 &")

a=0
toArduino=open("/mnt/ram0/pipe1","w")
fromArduino=open("/mnt/ram0/pipe2","r")
print toArduino
print fromArduino

c=Camera(0, { "width":320, "height":240 })
print "Start!!"
print "width:320, height:240"
js = JpegStreamer("TakeEE.local:8090")
while(True):
  toArduino.write("C")
  toArduino.flush()
  data=fromArduino.readline()
  odata=map(int,data.split(","))   #odata Color list 64
  img=c.getImage()
  for y in range(4):
      for x in range(16):  
          col1=odata[x+16*y]-3
          if not(col1 >=0 and col1 <= 255):
                 col1=255
          img.dl().rectangle((300-x*20,80+y*20),(20,20),color=(col1,0,255-col1),filled=True,alpha=220)
  img.save(js)

こちらはArduinoコード

 前回から多少変更しました。今後のためにPythonから"C"が送られてきた時は、カラーコード用の数値。そして"A"が送られてきた時は温度を送るようにしました。出来上がったネイティブコードsketch.elfをondo.elfとして、Edison の /sketch 以下にコピーしました。

#include <Arduino.h>
#include <Wire.h>
#include "MLX906211.h"

MLX90621 sensor; // create an instance of the Sensor classaaaa

FILE *to_python;
FILE *from_python;
char buffer[32];
char buf1[500];
String str1;
char inChar;
char s[64];

void setup(){
    from_python = fopen("/mnt/ram0/pipe1", "r");
    to_python = fopen("/mnt/ram0/pipe2", "w");
    if (from_python==NULL)
        Serial.println("Error: PIPE 1");
    if (to_python==NULL)
        Serial.println("Error: PIPE 2");
    sensor.initialise (4);
}

void loop(){   
    fgets(buffer, 2, from_python);
    if(buffer[0]=='A'){ //温度表示切り替え
        xread(1);
    }
    else if(buffer[0]=='C'){ //カラー表示切り替え最低から最高の間256段階
        xread(2);
    }
}

void xread(int xx){
    str1="";
    sensor.measure(true); //get new readings from the sensor
    for(int y=0;y<4;y++){ //go through all the rows
        // str1=str1+String(char(65+y))+',';
        for(int x=0;x<16;x++){ //go through all the columns
            if (xx==2){
                float min=sensor.getMinTemp();
                float max=sensor.getMaxTemp();
                int sss=(256/(max-min))*( sensor.getTemperature(y+x*4)-min);
                sprintf(s,"%i",sss);
            }
            else if(xx==1){
                float ttt=sensor.getTemperature(y+x*4);
                sprintf(s,"%2.2f",ttt);
            }
            str1=String(str1+s+',');
        }
    }
    str1=str1.substring(0, str1.length()-1); //最後の,を取る 
    str1=String(str1+'\n');
    str1.toCharArray(buf1, str1.length()+1);
    fputs(buf1, to_python); 
    fflush(to_python);
}

最後に

 たったこれだけのことを実験するだけなのに、買ったばかりの3dプリンタもほったらかしにして、かなりの時間を消費しました。ただ、すごく面白かったのも事実です。
 結構メモリを使う処理だと思いますが、問題なく動いて、スピードもそこそこだったことに結構満足しています。SimpleCV内部でnumpyやscipyを使っているのでかなりの解像度で補間できそうな気がしますが、ダメ元で今後トライしてみます。(ただし狂ってしまったセンサーを買い直すお金が貯まってから!!)

補足

 i2cセンサーに関するArduino資産は膨大な数です。いちいちEdison用にコーディングし直さなくとも、スピードを損なわずにそのまま動くということは、ある意味とんでもないことだと思いません?