M5Stackでカメラのタイマーリモコン(タイマーレリーズ)を作ってみた

この記事は M5Stack Advent Calendar 2020 の19日目の記事です。


以前、M5Stackをカメラの有線リモコン化する実験を行った。

今回、(ブレッドボードではなく)プロトモジュールを使った実装と、簡単なタイマーリモコンとして使うためのプログラムを組んだので紹介したい。

目次 [hide]

作りたいもの

よくあるタイマーリモコンはこんな感じ:

タイマーリモコンはカメラメーカーが純正品を出しているが、Amazonとかを探せば安い互換品がたくさん出てくる。筆者が持っているのもそういうものの一つだ。

タイマーリモコンとして必要な機能は

  • ディレイ(セルフタイマー)
  • シャッタースピード(押下時間)
  • インターバル
  • 回数

の4項目の調整と、それをスタート・ストップできることである。

これらの4項目と、実際にシャッターが押下されるタイミングの関係は次のようになる:

このほか、普通の有線リモコン(レリーズ)にある

  • シャッターの押下
  • シャッターの押しっぱなし(ロック、バルブ)

も欲しい。

M5StackにはLCDディスプレイが備わっているので、設定項目の表示は問題ない。一方、操作用のボタンは3つだけでは心許ない。そこで、独自にレバースイッチを実装することにした。

完成品

出来上がったハードウェアはこちら

M5Stackと積み重ねた様子は以下のようになる:

カメラとの接続には、別途2.5mmステレオ端子のついたケーブルを使う。カメラ側も2.5mmステレオ端子であれば

のような汎用品が使えるし、そうでなければAmazonでそれっぽいケーブルを入手できる。以下の写真の右上が2.5mmステレオケーブルで、左下は、オリンパスのRM-UC1互換ケーブルだ。

カメラに接続した様子は以下のようになる:

Canon製一眼レフに繋いだ様子
オリンパスのコンデジに繋いだ様子

動作の様子を簡単な動画にしてみた:

以下、実装について。

プロトモジュールを使った実装

今回は試作ということで、公式のプロトモジュールを使って実装することにした。

まず、有線リモコンとして使うために

  • 2.5mmステレオジャック
    • aitendoで買った表面実装用のものを使用。数年前に購入したもので、型番はよくわからない
  • フォトカプラ(2回路)
    • 数年前に購入したTLP621(生産終了)を使用
  • フォトカプラ用の(電流制限)抵抗×2
    • 手持ちの200Ωを使用

は最低限実装する必要がある。この他、操作用のインターフェースがボタン3つだけでは心許ないので、追加の操作インターフェースとして

を実装した。

回路図は

という感じになる。

当初、シャッター制御用のピンに19番と23番を使っていたが、それはMISO/MOSIで使用されるピンで、LCDの制御と干渉してしまった。その場のノリで適当に配線したらダメですね。

タイマーリモコン化プログラム

やっつけで書いたので、かなり汚いコードになっている(せめてコメントを書け)。

#include <M5Stack.h>
#include <utility/M5Timer.h>
const int SHUTTER_PIN = 12;
const int FOCUS_PIN = 5;
const int LEVER_UP = 13;
const int LEVER_PUSH = 0;
const int LEVER_DOWN = 34;
Button LeverUp(LEVER_UP, true, 10);
Button LeverPush(LEVER_PUSH, true, 10);
Button LeverDown(LEVER_DOWN, true, 10);
bool changed = true;
// LCD: 320x240
const uint8_t TEXT_SIZE = 3;
const int16_t LINE_HEIGHT = TEXT_SIZE * 8;
const int16_t CHAR_WIDTH = TEXT_SIZE * 6;
enum Selection {
SEL_DELAY = 0,
SEL_LONG,
SEL_INTERVAL,
SEL_REPEAT,
SEL_MAX = SEL_REPEAT
} selection = SEL_DELAY;
bool valueSelected = false;
int delayValue = 0;
int longValue = 1;
int intervalValue = 1;
int repeatValue = 1;
M5Timer timer;
bool isTimerRunning = false;
bool isLocked = false;
bool isShutterPressed = false;
int timerId = -1;
int count = 0;
void setup() {
M5.begin();
M5.Power.begin();
pinMode(SHUTTER_PIN, OUTPUT);
pinMode(FOCUS_PIN, OUTPUT);
M5.Lcd.setTextSize(TEXT_SIZE);
}
void stopShutter()
{
digitalWrite(SHUTTER_PIN, LOW);
digitalWrite(FOCUS_PIN, LOW);
isShutterPressed = false;
if (repeatValue == 0 || count < repeatValue) {
int delta = intervalValue - longValue;
timerId = timer.setTimeout(delta <= 0 ? 1 : (long)delta * 1000L, startShutter);
} else {
timerId = -1;
isTimerRunning = false;
}
changed = true;
}
void startShutter()
{
++count;
changed = true;
digitalWrite(FOCUS_PIN, HIGH);
digitalWrite(SHUTTER_PIN, HIGH);
isShutterPressed = true;
timerId = timer.setTimeout(longValue == 0 ? 1 : (long)longValue * 1000L, stopShutter);
}
void loop() {
timer.run();
M5.update();
LeverUp.read();
LeverPush.read();
LeverDown.read();
if (LeverPush.wasPressed()) {
valueSelected = !valueSelected;
changed = true;
}
if (LeverUp.wasPressed()) {
if (valueSelected) {
switch (selection) {
case SEL_DELAY:
delayValue += 1;
break;
case SEL_LONG:
longValue += 1;
break;
case SEL_INTERVAL:
intervalValue += 1;
break;
case SEL_REPEAT:
repeatValue += 1;
break;
}
changed = true;
} else {
if (selection > 0) {
selection = (Selection)(selection - 1);
changed = true;
}
}
} else if (LeverDown.wasPressed()) {
if (valueSelected) {
switch (selection) {
case SEL_DELAY:
if (delayValue > 0) {
delayValue -= 1;
}
break;
case SEL_LONG:
if (longValue > 0) {
longValue -= 1;
}
break;
case SEL_INTERVAL:
if (intervalValue > 0) {
intervalValue -= 1;
}
break;
case SEL_REPEAT:
if (intervalValue > 0) {
repeatValue -= 1;
}
break;
}
changed = true;
} else {
if (selection < SEL_MAX) {
selection = (Selection)(selection + 1);
changed = true;
}
}
}
if (M5.BtnA.wasPressed() || M5.BtnA.wasReleased() || M5.BtnB.wasPressed() || M5.BtnB.wasReleased() || M5.BtnC.wasPressed() || M5.BtnC.wasReleased()) {
changed = true;
}
if (changed) {
M5.Lcd.setTextColor(TFT_RED);
M5.Lcd.fillScreen(TFT_BLACK);
// M5.Lcd.fillRect(0, 0, CHAR_WIDTH * 8, LINE_HEIGHT * 4, TFT_DARKGREY);
M5.Lcd.setCursor(0, 0);
if (selection == SEL_DELAY && !valueSelected) {
M5.Lcd.setTextColor(TFT_BLACK, TFT_RED);
} else {
M5.Lcd.setTextColor(TFT_RED, TFT_BLACK);
}
M5.Lcd.print("DELAY");
M5.Lcd.setCursor(CHAR_WIDTH * 9, LINE_HEIGHT * 0);
if (selection == SEL_DELAY && valueSelected) {
M5.Lcd.setTextColor(TFT_BLACK, TFT_RED);
} else {
M5.Lcd.setTextColor(TFT_RED, TFT_BLACK);
}
M5.Lcd.printf("%d s", delayValue);
M5.Lcd.println();
if (selection == SEL_LONG && !valueSelected) {
M5.Lcd.setTextColor(TFT_BLACK, TFT_RED);
} else {
M5.Lcd.setTextColor(TFT_RED, TFT_BLACK);
}
M5.Lcd.print("LONG");
M5.Lcd.setCursor(CHAR_WIDTH * 9, LINE_HEIGHT * 1);
if (selection == SEL_LONG && valueSelected) {
M5.Lcd.setTextColor(TFT_BLACK, TFT_RED);
} else {
M5.Lcd.setTextColor(TFT_RED, TFT_BLACK);
}
M5.Lcd.printf("%d s", longValue); // or inf
M5.Lcd.println();
if (selection == SEL_INTERVAL && !valueSelected) {
M5.Lcd.setTextColor(TFT_BLACK, TFT_RED);
} else {
M5.Lcd.setTextColor(TFT_RED, TFT_BLACK);
}
M5.Lcd.print("INTERVAL");
M5.Lcd.setCursor(CHAR_WIDTH * 9, LINE_HEIGHT * 2);
if (selection == SEL_INTERVAL && valueSelected) {
M5.Lcd.setTextColor(TFT_BLACK, TFT_RED);
} else {
M5.Lcd.setTextColor(TFT_RED, TFT_BLACK);
}
M5.Lcd.printf("%d s", intervalValue);
M5.Lcd.println();
if (selection == SEL_REPEAT && !valueSelected) {
M5.Lcd.setTextColor(TFT_BLACK, TFT_RED);
} else {
M5.Lcd.setTextColor(TFT_RED, TFT_BLACK);
}
M5.Lcd.print("REPEAT");
M5.Lcd.setCursor(CHAR_WIDTH * 9, LINE_HEIGHT * 3);
if (selection == SEL_REPEAT && valueSelected) {
M5.Lcd.setTextColor(TFT_BLACK, TFT_RED);
} else {
M5.Lcd.setTextColor(TFT_RED, TFT_BLACK);
}
if (repeatValue == 0) {
M5.Lcd.println("INF");
} else {
M5.Lcd.println(repeatValue);
}
M5.Lcd.setCursor(60 - 5 * CHAR_WIDTH / 2, 240 - LINE_HEIGHT);
if (M5.BtnA.isPressed()) {
M5.Lcd.setTextColor(TFT_BLACK, TFT_RED);
} else {
M5.Lcd.setTextColor(TFT_RED, TFT_BLACK);
}
if (!isTimerRunning) {
M5.Lcd.print("START");
} else {
M5.Lcd.print("STOP");
}
M5.Lcd.setCursor(160 - CHAR_WIDTH / 2, 240 - LINE_HEIGHT);
if (M5.BtnB.isPressed() || isShutterPressed) {
M5.Lcd.fillCircle(160, 240 - LINE_HEIGHT, (int32_t)(LINE_HEIGHT * 0.8), TFT_RED);
} else {
M5.Lcd.drawCircle(160, 240 - LINE_HEIGHT, (int32_t)(LINE_HEIGHT * 0.8), TFT_RED);
}
M5.Lcd.setCursor(260 - (isLocked ? 6 : 4) * CHAR_WIDTH / 2, 240 - LINE_HEIGHT);
if (M5.BtnC.isPressed()) {
M5.Lcd.setTextColor(TFT_BLACK, TFT_RED);
} else {
M5.Lcd.setTextColor(TFT_RED, TFT_BLACK);
}
if (isLocked) {
M5.Lcd.print("UNLOCK");
} else {
M5.Lcd.print("LOCK");
}
changed = false;
}
if (M5.BtnA.wasReleased()) {
if (isTimerRunning) {
timer.deleteTimer(timerId);
timerId = -1;
isTimerRunning = false;
changed = true;
isShutterPressed = false;
digitalWrite(SHUTTER_PIN, LOW);
digitalWrite(FOCUS_PIN, LOW);
} else {
count = 0;
isTimerRunning = true;
timerId = timer.setTimeout((long)delayValue * 1000L, startShutter);
changed = true;
}
}
if (M5.BtnB.wasPressed()) {
isShutterPressed = true;
digitalWrite(FOCUS_PIN, HIGH);
digitalWrite(SHUTTER_PIN, HIGH);
} else if (M5.BtnB.wasReleased() && !isLocked) {
isShutterPressed = false;
digitalWrite(SHUTTER_PIN, LOW);
digitalWrite(FOCUS_PIN, LOW);
changed = true;
}
if (M5.BtnC.wasPressed()) {
if (isLocked) {
if (M5.BtnB.isReleased()) {
isShutterPressed = false;
digitalWrite(SHUTTER_PIN, LOW);
digitalWrite(FOCUS_PIN, LOW);
}
isLocked = false;
} else {
if (M5.BtnB.isReleased()) {
isShutterPressed = true;
digitalWrite(FOCUS_PIN, HIGH);
digitalWrite(SHUTTER_PIN, HIGH);
}
isLocked = true;
}
changed = true;
}
delayMicroseconds(10);
}

今後の方向性

タイマーリモコンとしての機能は揃ったが、ここで留まって良いものか。否である。「最強のカメラリモコン」を作るためにできることはまだまだある。

まず、せっかくESP32の載ったM5Stackを使っているのだから、Wi-Fiを使ってスマホからの操作を可能にしたい。これはプログラム次第でできるだろう。

関連研究:

次に、M5Stackを手持ちしたりカメラからぶら下げるのではなく、カメラのアクセサリーシューに装着できると良さそうである。これはアクリル板等で適当に工作すればできそうだ。

カメラを制御する方法には今回使った有線(3極)以外にもいくつかある。具体的には赤外線、USB、Wi-Fi/Bluetoothだ。

このうち、(プロトコルが広く知られているという意味で)扱いが楽そうなのは赤外線だ。今回作ったモジュールに赤外線LEDをつけて無線リモコンとして使えるようにすると「最強のカメラリモコン」に一歩近づけるだろう。

(USBはソフトウェア的に大変そうな気がするし、Wi-Fi/Bluetoothはごく最近のカメラじゃないと搭載されていない&解析の必要があるという欠点がある。)

関連:

Spread the love