2018/2/25 Thermal Cam がバージョンアップされました
ESP8266のソースを入れ替える 必要があります。(このページの最後に付けたソースでは動かない可能性があります)詳しくは以下のURLをご覧下さい。
takesan.hatenablog.com
iPhone側のソースをビルドして動かせる様になるには、
かなりつまづくことも多いかと思うので、先週からアプリストア登録作業を行うことにしました。メイカーフェア( Maker Faire Tokyo 2017)に展示するので当然無料配布です。
と言っても、iPhone単独では画像が再現できないことと、私はまだ1度も登録作業を行ったことがないので、今のところなんとも言えないのですが.......。同じ様なこと「アプリ単独では、実行が確認できないアプリのアプリストア登録?? 」を考えている方もいると思うので、申請の過程をメモすることにしました。アプリストア登録完了が、Maker Faire Tokyoに間に合わなかったらすいません。ただその場合は、実機テスト用のインストール説明を書くつもりです。
まず、本申請の前のテストフライトまでの要点 を説明します。OpenFrameWorksを使ったアプリの登録紹介記事がほとんどないので、特に引っかかった点などをまとめてみます。
今回の基本的な登録の方法は、次のブログを参考にさせていただきました。 痒いところに手が届く素晴らしい記事が満載です。テストフライトもこの記事に沿って実行していくとバッチリ!!
OpenFrameWorks(以降OF)上での問題点
標準UIの問題
OFでiOSのGUIを使うため、基本的には多摩美の田所先生の記事を参考にさせていただきましたが、iOSがバージョンアップしているので、そのままではすんなり実行できません。以下iOS特有のUI環境構築手段です。
newfile->iOS->Source->Cocoa touch Class->Next->Class 名変更、サブクラスはそのまま、Also creat XLB fileにチェック。3つのファイル(この場合EPSView.h,EPSView.m,EPSView.xlb )は自動で作成されます。そしてxxxx.m の拡張子を .mm に変更。
一番厄介なのは、iOSのUI環境に関して、OFでも、ビジュアルCを使用しなければない点です。(自動作成されるxxxx.h,xxxx.m2つのファイル)他の言語に慣れている者にとっては何もかも勝手が違うので、この部分を極力少なく記述するのが一番無難な手段だと思われました。今回のアプリは、画像上の位置を取得する必要がありますが、通常のiOSでは、UIのタップイベントに焦点を当てているため、画面の座標取得方法がかなり厄介です。なので、UIを最小限にまとめてディバイスに合わせて縦方向に伸び縮みさせる手段を取っています。
ボタン等のリンク方法は田所先生の記事
iOSのGUIをopenFrameworksのプロジェクトに追加する | yoppa org
を参考にしてください。ただ、setup()内の初期設定は現状少し変更の必要があります。以下の様にします。EPSViewは固有値です。
gui = [[ EPSView alloc ] initWithNibName : @"EPSView" bundle : nil ];
[ ofxiOSGetGLParentView () addSubview : gui . view ];
標準OSCアドオン応用方法の改善
過去の記事に書いていますが、iPhoneとWROOM02の通信手段はUDPです。これを使ったOSCという通信プロトコルを使って、画像配信を実現しています。たまたまOFと、WROOM02にライブラリが存在していたので、比較的簡単に双方向通信が実現できました。OSCは、音響機器の通信制御用に開発されたものなので、画像通信にはあまり適していません。今回はプログラムの作成が簡単なOSCの文字列送信を使い、LEPTON1の低解像バイナリデータを余計な変換なしで送っています。通常OSCでバイナリデータを送る場合は一旦文字に変更する必要がありますが、それでは単純にデータ量が4倍近くなるため、WROOM02の処理能力では問題が出て来ます。今回の画像データは、256階調のグレースケールであること(ASCIIコード内で収まる)をうまく利用して、一番問題となるデータ上の、ゼロを1に変更することでなんとかなってます。さらにLEPTONのX方向1行分のデータ164ビットを、String処理の限界に近い5行分送ることで、動画スピードを大幅に改善させました。
位置情報の取得
これはplist への追加と、OFの標準関数を使えば簡単にデータを読み取ることができました。実現するまで時間がかかりましたが、最終的にはOFのサンプルソースにヒントがありました。----->CoreLocationExample
plistにLocationを追加して(今回は2つ)
ofApp.h
ofxiOSCoreLocation * coreLocation; を追加。
ofApp.mm
coreLocation -> getLatitude ()
coreLocation -> getLongitude () で緯度経度の値を取り出せます。
動画の保存
今回は、OF唯一のiOS動画保存addonを使用しています。内部的にはFBOを使って動画保存を実現している様です。ただ、iPhoneのフォトライブラリフォルダに保存しないと使い物にならないので、仕方なくビジュアルCでコードを記述しています。とにかくこれ(ofxiOSVideoWriter )を作った方には感謝しか言葉がありません。
GitHub - julapy/ofxiOSVideoWriter: ofxiOSVideoWriter allows to screen record OF applications on iOS. (still very much a work in progress)
ただし今回はofVbo(3D画像処理に使用)がうまく表示できないため、main.cpp でglesVersion を ES1 に指定する必要がありますが、実行時エラーが出るようです。
[ error ] ofShader: sorry , it looks like you can't run 'ARB_shader_objects'
[ error ] ofShader: please check the capabilites of your graphics card:
他。
アプリが中断することはないので、無視して良いと思います。また、カメラロールに保存させるために
ofxiOSVideoWriter/src/ofxiOSVideoWriter.mm の中のfinishRecording() 関数を以下の様に変更する必要があります。
void ofxiOSVideoWriter::finishRecording() {
if ((videoWriter == nil ) ||
[videoWriter isWriting] == NO ) {
return ;
}
[videoWriter finishRecording];
killTextureCache();
//****************************by Takesan ******************************************
// ローカルフォルダに保存してから、カメラロールにセーブする。
NSString * docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES ) objectAtIndex: 0 ];
NSString * docVideoPath = [docPath stringByAppendingPathComponent: @"/video.mov" ];
UISaveVideoAtPathToSavedPhotosAlbum(docVideoPath, nil , nil , nil );
//********************************************************************************
}
さらにplist に2項目追加する必要がありました。
動画の表示方法
今までは、画像上で各ピクセルに対応した小さな円を書いて画像を表示していたのですが、あまりにも遅く、非力なWROOM02から送られてくる画像データでも、4倍補間と円の描画に手間取り、iPhone側の処理が間に合いません でした。今回は、LEPTON1の3D画像を実現させた時に使っている図形描画が非常に早いことに目を付け、高さ方向を0にして2D通常画像を表示させています。円描画に比べると詳細感は出ませんが、スピードを優先させました 。ピクセル中に黒を背景に入れると全体が引き締まり、にじみ が出にくいのですがが、今回使っているVbo でピクセルを小さくするとモアレが出る様です。今回はモアレが発生するギリギリの大きさで、各ディバイスの動画表示を調整しています。ただこれだけでは惜しいので特別にロゴを利用したボタンで詳細感を味わえる様にしています。
各画素を円にして背景を黒にすると、画像が引き締まり詳細感(にじみが出にくい)が出るが、表示が遅くなる。
テザリング中のiPhoneとWROOM02の電波干渉問題
今回はテザリングを利用して通信していますが、不思議なことに定期的・時間帯によって画像が乱れました。原因が暫く掴めず、この部分でも10日近く調整を行いましたが、結局はiPhone側のBlueTooth (特にApplewatch 使用時)とGPS が原因 だとわかりました。データ配信量の多いLEPTON3の場合は、これが顕著で決定的に乱れた画像になりますが、今回のLEPTON1ではさほど問題となりません。したがって、より綺麗な画像を求めるなら、両方をiPhone側でOFF にすることをお勧めします。今回は位置情報の同時録画が必要になる場面があると考え、位置情報をONにした場合、位置情報を表示させる様にしています。
アプリ名称の変更
OFではプロジェクト名がアプリの名称になり、このアプリ名称変更について誰しも悩むところですが、1箇所だけの修正でなんとかなりました。
Plist のBundle display nameを変更する!!
iOSのソースは
今回のアプリは、アプリストアからダウンロードできる様になりました。W&T で検索。
ソースが必要な方は、必要であることを下のコメントに書き込んでください。
テストフライトエラー対策
アイコン登録でのエラー
各ディバイスのアイコンが全て存在しないとエラーになります。
OFではアイコン表示に特殊な処理をしているので、修正方法がなかなか見つけられませんでしたが、基本は至って簡単でした。参考にさせていただいた記事は、
iOSな日々: iOS7:アイコンサイズの追加対応について
1種類のアイコン画像を作って各サイズに変換。それぞれ大きさの違う画像をMedia.xcassetsに貼り付けるだけでした。
さらにplistのアイコン名称は全て消す必要があります、ですから一般的にWEBで紹介されているアイコンの追加方法ではうまくいきませんでした
オリエンテーションの修正部分
傾けるとUIが追従する機能ですが、これも正しく設定しないとエラーになります。General-->Deployment info-->Devise Orientation 部分を下図のように全てONにするか、
下図の様にすることが必要。今回のアプリはiPhoneの傾きに追従すると、ドローンの操作中めまぐるしく画面が回転する可能性があるため、下図のようにUI追従を外しました。
Devise OrientationはPortaitのみON 、Status Bar StyleはRepuires full screenのみON
ディバイス対応方法
基本的にOFではdraw()内で座標を指定する数値を変更するだけでOKですが、この時、OF標準関数でディバイス名を確認する手段があります。今回はそれを使っています。
string devtype= ofxiOSGetDeviceRevision ();
今回のアプリは、iPhone6,6s,7 iPhone plus 6,6s,7 iPhone SE iPad=5th generation以降(mini含む)に対応しています。
以上の対応で、テストフライトは一応成功 しました。共同開発者wiwao さんに手伝っていただいて、5機種のインストールと表示確認を行い、バグッフィックス終了。いよいよ明日からアプリストア登録作業開始です!!。
WROOM02ソースについて
SPI読み取り部分の改善と単純化
今回は標準SPIライブラリを使い、少し中途半端なクロック数にしていますが、この設定が最も安定していました。またバイト単位の読み込み関数では、スピードが間に合わないため、LEPTON1データの1行分を全部読み込む処理を行っています。またCS信号のON OFFスピードが決め手となるため、この方のブログ
ESP8266 ( ESP-WROOM-02 ) SPI 通信の高速化に挑戦 | mgo-tec電子工作
を参考にできるだけCS信号の切り替えスピードを速めて います。
Yield(),Delay(),などの使い分け
WROOM02のArduino IDEを使う上で一番厄介な部分です。WIFIやSPIを使う場合これを適度に散りばめないと、yield()やdelay()時間内でWIFI処理を行っている関係上、頻繁にリセットする原因 になります。今回は通常考えられないところにyield()やdelay()が入っていますが、各行に入れたり抜いたりして調整した結果がこの位置になっています。今回はLEPTONの上限10FPSが達成し、リセットしないところが最適位置ということになります。色々なブログに対策方法が載っていますが、繰り返し文以外にもどうやらプログラム内容によって確実に必要な場合があり、決定的な決め手がない ことにご注意ください。現状での最終的な調整方法は、コンパイルを繰り返し、リセットを起こさない最適値・最適位置を探すだけです。この部分だけでもLEPTON3版開発も含めて、合計1ヶ月間は潰された唯一のアイマイで厄介な所 でした。
ダイナミックレンジ変更について。
LEPTON1には14bit階調のグレースケールデータを出力するモードと、8bit 256階調のカラーデータを出力するモードがあります。カラースケールモードを利用すれば画像を再現する上で最も簡単なのですが、単純にデータ量が3倍になる点とセンサーが感知した温度を計算することが難しいので、今回は不採用。したがって、今回は14bitグレースケール出力を採用しました。ただし、これを採用すると、画像表示上8bitデータに変換する必要があり、確実に階調が落ちます。また、例えば火などが一部に入った画像を確認する時など、温度差がありすぎて背景が消えてしまいます。これらを避けるため測定温度範囲を調整できる様にして256階調表示の欠点を改善しています。
WROOM側のソース他
下に提示したソースをWROOM02にインストールし、iPhone側アプリを立ち上げることで、最良のスピードかつ、iPhone側で指定した温度の計測や、先に書いたダイナミックレンジの調整ができる様になります 。今回私たちが作成するボードに頼らなくてもWROOM02市販基盤で実現が可能です。たった300行足らずののソースで、50m以上離れていても動画が鮮明に再現できて、付加価値情報も取得できる点は、驚くべきところだと思います(WROOM02が...です)。
LEPTON1とWROOM02の接続方法
以下の配線は、スイッチサイエンス版ESPr Developerを使用した場合です。
LEPTON基盤 ESPr Developer
CS----------> IO15
MOSI-------> IO13
MISO-------> IO12
CLK -------> IO14
GND-------> GND
VIN---------> 3V3
SDA--------> IO4
SCL--------> IO5
wiwaoさん設計ボードの場合
今回の最新試作基盤 です(共同開発者wiwao さん設計)。当然ながらLEPTON基盤にダイレクト接続可能。写真ではLEPTON3を接続しています。
必要部品は、wiwao さん設計ボード(キットとしてMaker Faireで販売予定)LEPTON1(シャッター付き)、LEPTONブレイクアウトボード、Lipo電池またはUSB電源、USBシリアルアダプタ、スルーホール用テストワイヤー(マルツパーツ)です。
ピンヘッダを取り付けるとドローンなどに載せるとき邪魔になるので、基盤に直接接続できるスルーホール用テストワイヤーを使います。
配線状況です。Arduinoソースをインストールするときは、この状態でスイッチをONにします。インストール完了後ワイヤーを外せばOK。iPhoneから30m以内(見通しの良いところで50~60m)ならどこにでも持ち運び自由かつ画像表示可能です。
Arduino IDE について
ESP8266 Arduino IDEのバージョンは2.2.0 を設定してください。最新版の場合Delay()やYield()、SPIの再調整が必要のようです。
ESP8266の設定は以下の通りです。Flash FrepuencyとCPU Frepuencyのスピードを上げたいところですがうまくいきませんでした。また、Upload Speed も115200推奨です。これ以上に設定すると、バイナリコードの転送がうまく行かない場合があります。
WROOM02ソース
SSID、パスワード、必要ならポート番号を変更(localPortはoutPort+1として下さい。変更した場合は、iPhone側でも変更が必要です)、基板に合わせてLEDのGPIO番号を最初の方で設定してから、ビルド転送して下さい。変更部分は以下の通り4箇所です。
//####################################################################### //******1.OSCライブラリを以下よりダウンロードして所定の場所にコピーしてください******* // https://github.com/sandeepmistry/esp8266-OSC //======================================================================== //*************** 2.自分の環境に合わせて以下の設定を変更してください ************** //======================================================================== #define SSID_X "XXXX-iPhone" //iPhone #define PASS_X "99999999999" //iPhone #define IP_X 172,20,10,1 //iPhone "なし,仕切り。テザリングはこのアドレス固定 //#define SSID_X "YYY-iPad" //iPad //#define PASS_X "88888888" //iPad //#define IP_X 172,20,10,1 //iPad "なし,仕切り。テザリングはこのアドレス固定 //#define SSID_X "router ssid" //WiFiを使用する場合 //#define PASS_X "router pass" //WiFi //#define IP_X 192,168,XX,XX //WiFi "なし,仕切り //======================================================================== //***************** 3.ポート番号を設定してください **************************** //======================================================================== const unsigned int outPort = 8090; // 送信は標準で8090 iPhone側は8091 const unsigned int localPort = 8091; // 受信は標準で8091 iPhone側は8090 //======================================================================== //***************** 4.LEDのGPIOピンを設定してください ************************ #define LEDpin 16 //======================================================================== #include <ESP8266WiFi.h> #include <WiFiUdp.h> #include <SPI.h> #include <OSCBundle.h> #include <OSCData.h> #include <OSCMatch.h> #include <OSCMessage.h> #include <Wire.h> //iPhoneテザリングLAN設定 char ssid[] = SSID_X; // 無線ランのSSID名称:文字列。 char pass[] = PASS_X; //同上 password const IPAddress outIp(IP_X); //macの場合 //**************スピード調整****************** //2017/3/14 安定板 スピードは以下に決まり。5行まとめて送信版 #define CLOCK_TAKE 11000000 #define DELAY_READ1 6 //6 1回目の読み取り //****************************************** OSCErrorCode error; //************LEPTON コマンド用*************** #define ADDRESS (0x2A) #define AGC (0x01) #define SYS (0x02) #define VID (0x03) #define OEM (0x08) #define GET (0x00) #define SET (0x01) #define RUN (0x02) //**************CS信号高速化用***************** #define PIN_OUT *(volatile uint32_t *)0x60000300 #define PIN_ENABLE *(volatile uint32_t *)0x6000030C #define PIN_15 *(volatile uint32_t *)0x60000364 //****************************************** WiFiUDP Udp; char zz[81*5+1]; //5LINE分一挙に送るため Stringの限界450程度 unsigned int min = 65536; unsigned int max = 0; float diff; char ip_ESP[16]; static uint8_t lepton_image1[82*2*60]; int touchX = 39; int touchY = 29; //*************OSCアドレス******************** OSCMessage msg1("/minmax"); OSCMessage msg2("/img_8"); float sl_256=0.0f; //################################# 初期設定 ######################################## void setup() { pinMode(LEDpin, OUTPUT); Wire.begin(); Serial.begin(115200); pinMode(2, OUTPUT); //使わないピンはOUTPUT SPI.begin(); SPI.setClockDivider(0); //念のため delay(1000); SPI.setFrequency(CLOCK_TAKE); //SPIクロック設定 Serial.println("Wait!! Now SPI set up........."); PIN_OUT = (1<<15); PIN_ENABLE = (1<<15); delay(500); PIN_15 = 1; //LOW delay(1000); Serial.println("setup complete"); Serial.println(); Serial.println(); Serial.print("Connecting to "); Serial.println(ssid); WiFi.begin(ssid, pass); while (WiFi.status() != WL_CONNECTED) { digitalWrite(LEDpin, HIGH); delay(50); digitalWrite(LEDpin, LOW); delay(50); Serial.print("."); } digitalWrite(LEDpin, HIGH); Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); //ローカルipを文字列に変換moziretunihennkann 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()); Serial.println("LEPTON START!!"); } //################################# SPI データ 読み取りコマンド ######################################## void read_lepton_frame(void) { int i; uint16_t data = 0x0f; delay(DELAY_READ1); //********************************<-----肝心!! 6固定 while ((data & 0x0f) == 0x0f) { PIN_15 = 1; //LOW SPI.transferBytes(0x0000,lepton_image1,164); PIN_15 = 0; //High data = (lepton_image1[0]<<8 | lepton_image1[1]); } for (int frame_number = 1; frame_number < 60; frame_number++){ PIN_15 = 1; //LOW SPI.transferBytes(0x0000,&lepton_image1[frame_number*164],164); PIN_15 = 0; //High } } //#################################メーカーSDK i2C 操作コマンド######################################## 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); } } //######################LEPTON SDK i2C 操作コマンド############################# 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); } } //######################LEPTON SDK i2C 操作コマンド############################# 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; } //######################LEPTON SDK i2C 操作コマンド############################# 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; } //############################# iPhone Touch XY ############################# void touch(OSCMessage &msg) { float sli=sl_256; touchX = msg.getInt(0); touchY = msg.getInt(1); sl_256 = msg.getFloat(2); if (sl_256 > 0.95 || sl_256 < -0.95) sl_256=sli; } //########################### メインルーチン ################################### void loop() { int p,tempXY1; int reading = 0; String debugString; int col; float value_min,value_max,tempXY; //**********Osc Receive********** OSCBundle bundle; int size = Udp.parsePacket(); if (size > 0) { while (size--) { bundle.fill(Udp.read()); } if (!bundle.hasError()) { bundle.dispatch("/tmpXY",touch); } else Serial.print("error!!"); } //********** 温度計算 ********** min = 65536; max = 0; read_lepton_frame(); yield(); for (int frame_number = 0; frame_number < 60; frame_number++){ for (int i = 2; i < 82; i++) { p=(lepton_image1[frame_number*164+2*i] <<8 | lepton_image1[frame_number*164+2*i+1]); if(frame_number==touchY && i-2==touchX) tempXY1=p; //iPhoneをタッチしたアドレスの温度データ if(p < min) min = p; if(p > max) max = p; } } int rrr=(int)(max - min)*(1.0f-abs(sl_256)); //高温側を256等分 iPhoneスライダーに連動 標準は0。最高で0.9 diff = rrr/ 256.0f; rrr=(float)(max - min)*sl_256; //開始低温度側温度 lepton_command(SYS, 0x14 >> 2 , GET); //ondo : 0x14 = chip 0x10 = aux float aux=read_data() ; ESP.wdtFeed(); //大事!! if(sl_256>0) min=(float)min+rrr; //最低温度表示も開始温度に連動させる if(sl_256<0) {max=(float)max+rrr;} //最高温度表示も開始温度に連動させる minはそのまま float fpatemp_f =- 472.22999f + (aux/ 100.0f) * 1.8f - 459.67f; value_min = (((0.05872 * (float)min + fpatemp_f)) - 32.0f) / 1.8f; value_max = (((0.05872 * (float)max + fpatemp_f)) - 32.0f) / 1.8f; tempXY = (((0.05872 * (float)tempXY1 + fpatemp_f)) - 32.0f) / 1.8f; //*****************Send osc data***************** msg1.add(value_min).add(value_max).add(tempXY).add(ip_ESP); if(Udp.beginPacket(outIp, outPort)==1) { msg1.send(Udp); Udp.endPacket(); msg1.empty(); } yield(); //*********************************************** for (int frame_number = 0; frame_number < 60; frame_number+=5){ for (int ii = 0; ii< 5 ; ii++) { for (int i = 2; i < 82; i++) { int ax=(frame_number+ii)*164+2*i; col = (int)((lepton_image1[ax]<<8 | lepton_image1[ax+1]) - min)/ diff; //(int)を入れないと符号無し演算になる!!1; //=0 if(col <= 0) col = 1; //=0 if(col > 255) col = 255; zz[ii*81+i-1]=col; yield(); //これがデータ安定の決め手 } zz[ii*81]=frame_number+ii; if(frame_number==0) zz[0]=0x3c; //=0 nara 0x3c tosuru. } zz[81*5+1]='\0'; //String の最終処理 //*****************Send osc data************ msg2.add(zz ); if( Udp.beginPacket(outIp, outPort)==1) { msg2.send(Udp); Udp.endPacket(); msg2.empty(); } //****************************************** yield(); //絶対必要!! } }
iPhoneアプリはアプリストアからダウンロードできます!!
W&T で検索。
使い方は
VIDEO www.youtube.com
Maker Faire Tokyo 2017 では
こんな感じで展示します。
では、また。