フューチャー技術ブログ

雀を見守るカメラを作ってみた

電子工作/IoT連載 の2本目です。

背景、はじめに。

みなさんこんにちは。フューチャーの矢野です。

もう春ですね。この季節になると実家の壁に雀が巣を作ることがあります。

今年も雀が巣を作ったなら、それを見守れたら楽しいなと思います。

そこで、雀を見守るためのカメラを試作してみました。

概要

作ったのがこれです。(デモ)
デモ

今回実現したのは下記です。

  1. LAN内ならカメラの映像が確認できる
  2. 雀がいない間はカメラの電源を落とすことで節電
  3. カメラが起動したら通知が届く
  4. かわいい雀の写真がLINEに通知される
  5. カメラの設定はスマホからできる

フロー図

フロー図

人感センサーに反応があると、カメラに電源が入ります。
カメラは起動するとまずWiFiに接続を試みて、失敗したらAPモードになり、APに接続することでWiFi設定などができるようになります。設定が終了したら、再起動して、またWiFiの接続を試みます。WiFiの接続に成功したらば、まずLINEに通知を飛ばします。飛ばせない場合は、黙ってカメラのサーバーを立ち上げます。30秒以上センサーに反応がない場合は、カメラの電源が落ちます。ここのロジックはATTiny202のマイコン側でやっていますが、ESP32-CAMだけで実現できると思います。

使用の想定

使用の想定

カメラがアクセスするWiFiなどの設定はスマホからできるようにしました。カメラへのアクセスはLAN内に限定しました。インターネット越しに雀を見守ることはできません。

今回の想定は、ユーザー(私)がLINEの通知をみたあと、同じWiFi内にいるにスマホでカメラに接続する想定です。

回路図

回路図

常時人感センサーだけ給電されていて、人感センサーに反応があると、ほかのモジュールにも電源がいくようになっています。NchMOSFETとPchMOSFETを組み合わせて電源を入れる処理は実現しました。電源が入ったATTiny202がQ2のゲートを開けます。

これによって電源が切れないようなります。電源を切るときはQ2のゲートを閉じます。

ブレッドボードだとこうなります。
ブレッドボード
一枚のブレッドボードでサクッと作れるのは気持ちがいいですね。

パーツリスト

今回の工作で使ったパーツと、購入できるページを載せておきます。
ブレッドボードやジャンパーピンは省略します。

名称 購入先例
U1 焦電型赤外線センサーモジュール(焦電人感センサ) 秋月電子
U2 AVRマイコン ATTINY202-SSNR 秋月電子
U3 ESPカメラモジュール [ESP32-CAM-K] www.aitendo.com
Q1,Q2 NchMOSFET 30V5A IRLML6344TRPBFTR 秋月電子
Q3 PchMOSFET IRLML2246TRPBF 秋月電子
R1,R4,R5 100kΩ抵抗 秋月電子等
R2 39kΩ抵抗 秋月電子等
R3 100Ω抵抗 秋月電子等
LED1 何かしらのLED 秋月電子等

MOSFETやATTiny202などの表面実装パッケージをブレッドボードに差し込むために下記の基板を使いました。

名称 購入先例
ATTiny202用 SOT-23-3 DIP化基板 (5枚入) 秋月電子
MOSFET用 SOP8(1.27mm)DIP変換基板 金フラッシュ (9枚入) 秋月電子

ソースコード

ソースコードは下記です。
ESP32のスケッチ例、CameraWebServerに下記のような変更を加えたものです。

  1. WiFi接続時にWiFiManagerを用いることで、WiFi設定をスマホからできるようにした。
  2. HTTPClientを用いて、LINE NotifyのAPIを叩くようにした。

画像をHTTPClientを用いてLINEにPOSTするのですが、この記事が大変参考になりました。

camera_server.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#include "esp_camera.h"
#include <WiFi.h>
#include <DNSServer.h>
#include <WebServer.h>
#include <WiFiManager.h>
#include <HTTPClient.h>
#define CAMERA_MODEL_AI_THINKER
#include "camera_pins.h"

void startCameraServer();

#define LINE_ACCESS_TOKEN "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
#define BOUNDARY "CHUNCHUNNOTICE20220327"
HTTPClient http;
int32_t linePost( camera_fb_t *fb ) {
String RequestURL="https://notify-api.line.me/api/notify";
http.begin(RequestURL);
if (fb)
{
String stConType ="multipart/form-data; boundary=";
stConType+=BOUNDARY;
http.addHeader("Content-Type", stConType);
String authorization = "Bearer ";
authorization += LINE_ACCESS_TOKEN;
http.addHeader("Authorization", authorization);

String stMHead="--";
stMHead += BOUNDARY;
stMHead += "\r\n";

stMHead += "Content-Disposition: form-data; name=\"message\";\r\n\r\n";
stMHead += "Camera: http://";
stMHead += WiFi.localIP().toString();
stMHead += "\r\n";

stMHead += "--";
stMHead += BOUNDARY;
stMHead += "\r\n";
stMHead += "Content-Disposition: form-data; name=\"imageFile\"; filename=\"./a.jpg\" \r\n";
stMHead += "Content-Type: image/jpeg \r\n";

stMHead += "\r\n";
uint32_t iNumMHead = stMHead.length();

String stMTail="\r\n--";
stMTail += BOUNDARY;
stMTail += "--\r\n\r\n";
uint32_t iNumMTail = stMTail.length();

uint32_t iNumTotalLen = iNumMHead + iNumMTail + fb->len;

uint8_t *uiB = (uint8_t *)malloc(sizeof(uint8_t)*iNumTotalLen);

for(int uilp=0;uilp<iNumMHead;uilp++)
uiB[0+uilp]=stMHead[uilp];
for(int uilp=0;uilp<fb->len;uilp++)
uiB[iNumMHead+uilp]=(fb->buf)[uilp];
for(int uilp=0;uilp<iNumMTail;uilp++)
uiB[iNumMHead+fb->len+uilp]=stMTail[uilp];

int32_t httpResponseCode = (int32_t)http.POST(uiB,iNumTotalLen);
http.end();
free(uiB);
Serial.print("Response Code:");
Serial.println(httpResponseCode);
Serial.print("Response Body:");
Serial.println(http.getString());
return (httpResponseCode);
}
}

void takePhoto() {
camera_fb_t * fb = NULL;
fb = esp_camera_fb_get();
if(!fb) Serial.println("Camera capture failed");
linePost(fb);
esp_camera_fb_return(fb);
}


void setup() {
Serial.begin(115200);
Serial.setDebugOutput(true);
Serial.println();

camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;

// if PSRAM IC present, init with UXGA resolution and higher JPEG quality
// for larger pre-allocated frame buffer.
if(psramFound()){
config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = 10;
config.fb_count = 2;
} else {
config.frame_size = FRAMESIZE_SVGA;
config.jpeg_quality = 12;
config.fb_count = 1;
}

#if defined(CAMERA_MODEL_ESP_EYE)
pinMode(13, INPUT_PULLUP);
pinMode(14, INPUT_PULLUP);
#endif

// camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}

sensor_t * s = esp_camera_sensor_get();
// initial sensors are flipped vertically and colors are a bit saturated
if (s->id.PID == OV3660_PID) {
s->set_vflip(s, 1); // flip it back
s->set_brightness(s, 1); // up the brightness just a bit
s->set_saturation(s, -2); // lower the saturation
}
// drop down frame size for higher initial frame rate
s->set_framesize(s, FRAMESIZE_QVGA);

#if defined(CAMERA_MODEL_M5STACK_WIDE) || defined(CAMERA_MODEL_M5STACK_ESP32CAM)
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
#endif
WiFiManager wifiManager;
wifiManager.setConfigPortalTimeout(30);
if (!wifiManager.autoConnect("BirdWatcher","hogehoge")) {
Serial.println("failed to connect and hit timeout");
}
takePhoto();
startCameraServer();

Serial.print("Camera Ready! Use 'http://");
Serial.print(WiFi.localIP());
Serial.println("' to connect");
}

void loop() {
// put your main code here, to run repeatedly:
delay(10000);
}

ATTINY202側のコードです。人感センサーが反応してから30秒間電源が落ちないように0pin(NchMOSFETに繋がっている)をオンにします。こちらもArduinoのブートローダーを書き込んでArduinoIDEで開発してます。

attiny202.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 /*
PowerController for tiny 202
*/

bool keepPassive(int digiin,int interval){
for(int i=0; i<interval; i++)
{
if (digitalRead(digiin) == HIGH) return false;
delay(1000);
}
return true;
}

void setup() {
pinMode(0, OUTPUT); // POWER FLAG
pinMode(1, INPUT); // POWER BUTTON
digitalWrite(0, HIGH);
}

void loop() {
if ( digitalRead(1)==LOW && keepPassive(1, 30) ) digitalWrite(0, LOW);
}

デモ

demo.gif 消費電力はテスターで測れる範囲で測った結果が下記です。電源の電圧は5Vです。
状態 電流
人感センサのみ 0.0mA以下(※0.0mA以下をテスターで計測することができませんでした)
人感センサ反応後、WiFi起動時 190mA前後
サーバー起動、待機中 120mA前後
カメラ起動、配信開始 200mA以上(※テスターでは200mA以上の測定ができませんでした)

上記の通り、カメラを常時起動させているよりは電気を節約できていそうですね。

まとめ

これでかわいい雀が巣を作っても見守れますね。今回はカメラ部分だけ作りました。

実際に雀を見守るとなると、巣箱と、カメラを動かす電源が必要になります。

電源にはソーラーパネルと鉛蓄電池を使おうかなと思っています。

ありがとうございました。

利用ツール・参考

  1. diagrams.net(フロー図、利用想定図作成)
  2. KiCad(回路図作成)
  3. fritzing(ブレッドボードの図作成)
  4. ffmpeg(デモ動画変換)
  5. QuickTime Player + iPhone(デモ動画撮影)
  6. Arduino HTTPClientでファイルのバイナリ送信
  7. LINE Notify API Document([POST] /api/notify)