2016年2月21日日曜日

ESP-WROOM-02+放射温度計+サーボ = 遠隔温度計測


autolayout面倒くさい

■やりたいこと■

赤外線放射温度計というものがあります。リフロー用に2000円で買ってきて、リフローだけでなく餃子を焼くときのフライパン温度やチャーハンに卵を投入する前のフライパンの温度やカラアゲを作る際の油の温度計測などに幅広く利用しております。

このセンサー部分が手に入りました。センサーを以前使ったサーボモータのチルト&パン台(Crowtail- Pan- Tilt)に固定しておき、遠く離れたところからiPhoneで角度を変えるとその方向に首を振り、計測した温度を送り返してくれる…というものを作ります。

以前サーボモータを動かした時には、単に首振ってるだけで考えてみればESP-WROOM-02を使うメリットというのはまったくなかったのですが、今度はしっかりとセンサーを駆動します。

リモートセンシングやで。

■要素技術の確認■

赤外線放射温度センサーMLX90614のライブラリ、いろいろ試しましたが、一発で動いたのはコレでした。
https://github.com/adafruit/Adafruit-MLX90614-Library

MLX90614のピンの並びは残念ながら手持ちのESPボードと異なっているので、変換ケーブル?を作ります。単にGND-VCC-SDA-SCLをそれぞれの並びに合わせてヘッダピンにハンダ付けしてスミチューブで固定するだけです。

サーボは以前にも使ったElecrowのtilt&panです。サーボのコネクタはマイコンでは使いにくいのでぶった切ってピンヘッダと付け替えます。その際に2ch分をコンパクトに使えるように3ピンと4ピンのヘッダを使い、GNDとVCCは共通、信号線は1つずらして接続します(GND-VCC-SIGNALとGND-VCC-NC-SIGNAL)。こうすればブレッドボードに並べて挿して共通の電源とそれぞれのSIGNAL入力をつなぐだけで済みます。

制御入力と表示にはiPhoneを使います。最初iPhoneをうにうに傾けるとそれに合わせてtilt&panが動くようにしたのですが、表示が大変見づらいという欠点が発覚したので画面上の●をドラッグする方式に変更しました。●の座標をTCP/IPで値を送ります。


あとはESPとiPhoneの通信ですが、ゼロコンフィグとかやると死ぬので固定IP決め打ち一発でTCP/IP接続してデータを送受信します。TCP/IP通信にはCFNetworkのinput/output streamを使ってます。

■サーボとの接続■

ESP-WROOM-02にサーボとMXL90614を接続します(前回記事:ESP + 2軸サーボ)。

ただ、前回ワイヤ直結したら抜けやすくて大変不便だったので、crowtail仕様のコネクタを切断してピンヘッダを取り付けました。その際、3ピンのピンヘッダと4ピンのピンヘッダを用意し、4ピンのは「信号」「空き」「Vcc」「Gnd」、3ピンは「信号」「Vcc」「Gnd」の並びで作りました。こうすれば、ブレッドボードに挿した時に配線が少なくてすみます。
手前が4p、奥が3p。右からVccとGndを引いてます

IO14, IO15はジャンパ経由でESPと接続
テープで剥がれる机の塗装orz

■MLX90614との接続■


これはI2Cなので、Vcc, Gnd, SDA, SCLをそれぞれ接続します。今回購入したモジュールはプルアップ抵抗付きなので、ESPとつなげばおしまいです。これもヘッダピンとワイヤをハンダ付けして専用ケーブルを作りました。ESPのIO4とSDA、ESPのIO5とSCLを接続してください。

なお、私はワイヤの色をSCL白、SDA緑、Vcc赤、Gnd黒と決めています。

こっち見んなw(お約束


■MLX90614とサーボ■

3Dプリンタが届くのを待っていたのですが、3月一杯に延期されてしまったので、とりあえずテグスで括りつけました。ははは(力ない笑い

■ESP用プログラム■

ESP_WiFiTelnetToSerialというサンプルプログラムを参考にしました。大まかな構造としては、

  • iPhoneからデータが届いたら、それを目標座標とする
  • 1秒間に100回のタイマーで、サーボの目標座標目指してゆっくり指示座標を変えていく。これはいきなりiPhoneからの値が飛ぶとサーボが高速で動いて電力消費やらメカ強度やらに悪影響を与えるので、それを防止するためです。通信でメカを動かす場合の基本ですね。
  • 1秒に1回、MLXからデータを読み込んでiPhoneへ送信する

サーボの制御と温度データの送信を切り離しています。こうすることでプログラムが複雑にならないようにしています。

//
// servo
//
#include <Servo.h>
Servo Tilt;
Servo Pan;
const int kTilt = 14; // 10 - 130degree(up-down)
const int kPan = 15; // 40 - 160degree(right-left)
const int kMinTilt = 10;
const int kMaxTilt = 130;
const int kMinPan = 40;
const int kMaxPan = 160;
const int kInitTilt = 10;
const int kInitPan = 100;
float currentTilt, currentPan; // current position
int targetTilt, targetPan; // command from iPhone
ETSTimer Timer;
void ServoController(void *temp) {
if (targetTilt < kMinTilt) targetTilt = kMinTilt;
if (targetTilt > kMaxTilt) targetTilt = kMaxTilt;
if (targetPan < kMinPan) targetPan = kMinPan;
if (targetPan > kMaxPan) targetPan = kMaxPan;
float vTilt = ((float)targetTilt - currentTilt) / 10.0;
float vPan = ((float)targetPan - currentPan) / 10.0;
currentTilt += vTilt;
currentPan += vPan;
Serial.print(currentTilt);
Serial.print(",");
Serial.println(currentPan);
Tilt.write(currentTilt);
Pan.write(currentPan);
}
//
// WiFi
//
#define USE_US_TIMER 1
extern "C" {
#include "user_interface.h"
#include "osapi.h"
}
#include <ESP8266WiFi.h>
const char* ssid = "*************";
const char* password = "*************";
WiFiServer Server(51515);
WiFiClient Client;
const int kMaxBuffer = 80;
//
// IR Sensor
//
#include <Wire.h>
#include <Adafruit_MLX90614.h>
#include <Time.h>
Adafruit_MLX90614 mlx = Adafruit_MLX90614();
int prevSecond = second();
const int LED = 13;
//
// Setup
//
void setup()
{
Serial.begin(115200);
//
// Wifi
Serial.println("start to connect");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(500);
}
Serial.println("wifi connected");
Server.begin();
Server.setNoDelay(true);
Serial.print("receive ready. IP addr:");
Serial.print(WiFi.localIP());
Serial.println(", port:51515");
//
// Temperature sensor
mlx.begin();
prevSecond = second();
//
// Servo
Tilt.attach(kTilt);
Pan.attach(kPan);
currentTilt = kInitTilt;
currentPan = kInitPan;
targetTilt = kInitTilt;
targetPan = kInitPan;
Tilt.write(currentTilt);
Pan.write(currentPan);
os_timer_setfn(&Timer, ServoController, NULL);
os_timer_arm(&Timer, 10, true);
//
// other
pinMode(LED, OUTPUT);
}
//
// loop
//
void loop() {
//
// check connection
if (Server.hasClient()) {
if (!Client || !Client.connected()) {
if (Client) Client.stop();
Client = Server.available();
}
} else {
//no free/disconnected spot so reject
WiFiClient client = Server.available();
client.stop();
}
//
// receive data from iPhone
if (Client && Client.connected()) {
if (Client.available()) {
//get data from the telnet client and push it to the UART
int c;
int p = 0;
int p2 = 0;
char line[80];
do {
c = Client.read();
if (c == ',') {
p2 = p + 1;
c = 0;
}
line[p++] = c;
} while (p < kMaxBuffer - 1 && c != '\n') ;
line[p] = 0;
targetTilt = atoi(line);
targetPan = atoi(line + p2);
}
}
//
// send sensor data
if (prevSecond != second()) {
digitalWrite(LED, HIGH);
float ambient = mlx.readAmbientTempC();
float object = mlx.readObjectTempC();
if (Client && Client.connected()) {
char line[20];
int a, o;
a = ambient * 100;
o = object * 100;
sprintf(line, "%d,%d\n", a, o);
Client.write((const char *)line);
}
digitalWrite(LED, LOW);
prevSecond = second();
}
}


■iPhone用プログラム■

最初swiftで書いてSIOSocketがうまく組み込めずに挫折した(設定ミスorz)とか、その後obj-cでSIOSocket使ってみたけどしっくりこないので捨てた、というのは割愛します。

プロジェクトは後日githubに公開します。とりあえずViewControllerと座標ドラッグ用のControlViewをgistにまとめて貼っておきます。今気づいたけど、ControlViewじゃなくてControllViewだな(恥

// ViewController.m
//
// ViewController.m
// ServoIRViewer
//
// Created by 倉橋浩一 on 2016/02/19.
// Copyright © 2016年 倉橋屋. All rights reserved.
//
#import "ViewController.h"
//#import <CoreMotion/CoreMotion.h>
#import "ControlView.h"
@interface ViewController ()
@property (strong, nonatomic) NSInputStream *inputStream;
@property (strong, nonatomic) NSOutputStream *outputStream;
@property (strong, nonatomic) NSMutableArray *messages;
@property CGRect viewRect;
@property NSString *strShouldSend;
@end
@implementation ViewController
const int BUFSIZE = 128;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.zValue.text = @"";
self.xRaw.text = @"";
self.yRaw.text = @"";
self.xRaw.text = @"";
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(actionPan:)];
[self.dragView addGestureRecognizer:pan];
[self initNetworkCommunication];
}
- (void)viewDidAppear:(BOOL)animated {
_viewRect = self.dragView.frame;
self.dragView.xy = CGPointMake(_viewRect.size.width / 2, 0);
self.dragView.init = YES;
[super viewDidAppear:animated];
[self.dragView setNeedsDisplay];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - gesture
- (void) actionPan:(UIPanGestureRecognizer *)pan {
if ([pan numberOfTouches] > 0) {
CGPoint t = [pan locationOfTouch:0 inView:self.dragView];
double yValue = (t.x / _viewRect.size.height) * 120 + 40;
double xValue = (t.y / _viewRect.size.width) * 120 + 10;
if (yValue >= 40 && yValue <= 160 && xValue >= 10 && xValue <= 130) {
NSString *strX = [NSString stringWithFormat:@"%3.lf", xValue];
self.xValue.text = strX;
NSString *strY = [NSString stringWithFormat:@"%3.lf", yValue];
self.yValue.text = strY;
[self.dragView setXy:t];
[self.dragView setNeedsDisplay];
if (self.outputStream != nil) {
self.strShouldSend = [NSString stringWithFormat:@"%3.0lf,%3.0lf\n", xValue, yValue];
const uint8_t * cstr = (const uint8_t*)[self.strShouldSend UTF8String];
[self.outputStream write:cstr maxLength:strlen((char *)cstr)];
}
}
}
}
#pragma mark - socket
//
// thanks!
// http://internetcom.jp/developer/20100406/26.html
//
- (void)initNetworkCommunication {
uint portNo = 51515;
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)@"192.168.0.68", portNo, &readStream, &writeStream);
self.inputStream = (__bridge NSInputStream *)readStream;
self.outputStream = (__bridge NSOutputStream *)writeStream;
[self.inputStream setDelegate:self];
[self.outputStream setDelegate:self];
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self openStreams];
}
//
// thanks:
// CFNetwork programming guide
// https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/NetworkStreams.html#//apple_ref/doc/uid/20002277-BCIDFCDI
//
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
switch(eventCode) {
case NSStreamEventOpenCompleted:
NSLog(@"stream : connected");
// self.buttonConnect.enabled = NO;
break;
case NSStreamEventEndEncountered:
case NSStreamEventErrorOccurred:
NSLog(@"NSStreamEventErrorOccurred");
[self.outputStream close];
[self.inputStream close];
self.inputStream = nil;
self.outputStream = nil;
self.buttonConnect.enabled = YES;
break;
case NSStreamEventHasBytesAvailable:
NSLog(@"NSStreamEventHasBytesAvailable");
if (stream == self.inputStream) {
uint8_t cstr[BUFSIZE];
[self.inputStream read:cstr maxLength:BUFSIZE];
NSLog(@"received=%s", cstr);
int remote, local;
sscanf((char *)cstr, "%d,%d\n", &local, &remote);
self.remoteTemp.text = [NSString stringWithFormat:@"%4.2lf", (float)remote/100.0];
self.localTemp.text = [NSString stringWithFormat:@"%4.2lf", (float)local /100.0];
}
break;
case NSStreamEventHasSpaceAvailable:
// NSLog(@"NSStreamEventHasSpaceAvailable");
// if (stream == self.outputStream) {
// if (self.strShouldSend != nil) {
// NSLog(@"output should send");
// const uint8_t * cstr = (const uint8_t*)[self.strShouldSend UTF8String];
// [self.outputStream write:cstr maxLength:strlen((char *)cstr)];
// self.strShouldSend = nil;
// }
// }
break;
default:
break;
}
}
- (IBAction)actionConnect:(id)sender {
[self.inputStream close];
[self.outputStream close];
[self initNetworkCommunication];
}
- (void)openStreams {
[self.inputStream open];
[self.outputStream open];
}
@end
// ViewController.h
//
// ViewController.h
// ServoIRViewer
//
// Created by 倉橋浩一 on 2016/02/19.
// Copyright © 2016年 倉橋屋. All rights reserved.
//
#import <UIKit/UIKit.h>
@class ControlView;
@interface ViewController : UIViewController <NSStreamDelegate>
@property (weak, nonatomic) IBOutlet UILabel *xValue;
@property (weak, nonatomic) IBOutlet UILabel *yValue;
@property (weak, nonatomic) IBOutlet UILabel *zValue;
@property (weak, nonatomic) IBOutlet UILabel *xRaw;
@property (weak, nonatomic) IBOutlet UILabel *yRaw;
@property (weak, nonatomic) IBOutlet UILabel *zRaw;
@property (weak, nonatomic) IBOutlet UILabel *remoteTemp;
@property (weak, nonatomic) IBOutlet UILabel *localTemp;
@property (weak, nonatomic) IBOutlet ControlView *dragView;
@property (weak, nonatomic) IBOutlet UIButton *buttonConnect;
- (IBAction)actionConnect:(id)sender;
@end
// ControlView.h
//
// ControlView.m
// ServoIRViewer
//
// Created by 倉橋浩一 on 2016/02/19.
// Copyright © 2016年 倉橋屋. All rights reserved.
//
#import "ControlView.m"
const float r = 30;
@implementation ControlView
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
_init = NO;
}
return self;
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
CGContextStrokeRect(context, rect);
if (_init) {
// NSLog(@"%lf, %lf", self.xy.x, self.xy.y);
CGContextSetFillColorWithColor(context, [UIColor darkGrayColor].CGColor);
CGContextFillEllipseInRect(context, CGRectMake(self.xy.x - r, self.xy.y - r, r*2, r*2));
}
}
@end
// ControlView.h
//
// ControlView.h
// ServoIRViewer
//
// Created by 倉橋浩一 on 2016/02/19.
// Copyright © 2016年 倉橋屋. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface ControlView : UIView
@property CGPoint xy;
@property BOOL init;
@end
view raw ServoIRView hosted with ❤ by GitHub

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。