Wildlife camera - Raspberry Pi Zero W

Вторая версия моей смарт-камеры видеонаблюдения: после неудачного опыта с Orange Pi перешел на Raspberry Pi Zero W.

Так камера выглядит в раскрытом виде: плата Pi Zero W, конечно, заметно меньше, чем Orange Pi PC Plus - в корпусе осталось много свободного пространства. Зато получилась неплохая платформа, в которой можно тестировать разные платы :)


Веб-камеру я заменил на Raspberry Pi Camera Module v1.3 с разрешением 5 Mp. Для ее подключения к Zero Pi нужно использовать шлейф CSI - microCSI.

Модели переходных плат можно найти в git проекта. Остальные детали остались теми же, что и в первой версии

Для начала не стал подключать PIR-сенсор и сфокусировался на том, чтобы сделать timelapse-камеру, которой можно было бы управлять с помощью web-приложения из любого браузера. Например, с телефона, что особенно актуально при настройке камеры где-нибудь на дереве. 

Связь с камерой - по wi-fi, причем как для настройки / отладки (подключение к домашней сети), так и для работы (точка доступа). Чтобы было удобно переключаться между этими режимами, сделал скрипт wifi_config.sh (все исходники есть в git проекта)

#! /bin/bash

arg1=$1

if [ $arg1 == "ap" ] ; then
cp /etc/dhcpcd.ap.conf /etc/dhcpcd.conf &&
systemctl daemon-reload &&
service dhcpcd restart &&
systemctl start dnsmasq &&
systemctl start hostapd &&
systemctl enable dnsmasq &&
systemctl enable hostapd
elif [ $arg1 == "wifi" ] ; then
systemctl stop dnsmasq &&
systemctl stop hostapd &&
systemctl disable dnsmasq &&
systemctl disable hostapd &&
cp /etc/dhcpcd.wifi.conf /etc/dhcpcd.conf &&
systemctl daemon-reload &&
service dhcpcd restart
else
echo "UNKNOWN input"
fi

для его работы в папке /etc необходимо создать два файла:  

  • dhcpcd.wifi.conf - без dhcp для подключения к существующей wi-fi сети
  • dhcpcd.ap.conf - со статическим адресом для интерфейса wlan0

ими подменяется файл /etc/dhcpcd.conf в зависмости от требуемой настройки сети.

Запускать скрипт необходимо в правами root и одним из параметров ap / wifi:

$ sudo ./wifi_config.sh ap

Можно переходить к web-приложению: оно написано на python3 с помощью фреймворка flask . Структура проекта: 

webapp/ 
|--- app.py
|--- snapshot.sh
|--- timelapse.sh
|--- static/
     |--- style.css
     |--- img/
          |--- snapshot.jpg 
|--- templates/
     |--- index.html

app.py - приложение на python / flask
snapshot.sh - скрипт для захвата изображения предпросмотра /static/img/snapshot.jpg
timelapse.sh - скрипт для захвата серии кадров таймлапса
style.css - стили для единственной страницы index.html

Немного расскажу о составных частях проекта. Сначала - основной модуль app.py - не буду приводить полный листинг, он есть в git.

У приложения одна страница, так она выглядит в браузере:

 

Приложение должно выполнять три задачи:

1. дать возможность настроить камеру в месте установки - захватить кадр по команде,
2. запустить съемку серии кадров с заданными параметрами,
3. дать возможность пользователю отслеживать прогресс по съемке кадров - с помощью обратного отсчета времени, оставшегося до конца съемки.

Непосредственно захват кадров я решил делать с помощью встроенной утилиты raspistill - у нее приличный функционал по работе с модулем камеры, в т.ч. запись таймлапсов. Для удобства сделал bash-скрипты на ее основе, добавив вспомогательные функции - было интересно поработать с ними и научиться запускать из python :) О самих скриптах чуть позже, сначала о способах их запуска из приложения, для чего отлично подходит модуль subprocess.

Захват кадра для предпросмотра:

subprocess.call([app_dir+'/snapshot.sh', awb])

это блокирующий вызов - приложение ожидает завершение метода call, который возвращает код завершения скрипта snapshot.sh. Это удобно, т.к. после нажатия на кнопку SNAPSHOT страница в браузере переходит в загрузку и обновляется после получения нового кадра, что занимает несколько секунд: видно, что процесс идет, есть реакция на нажатие кнопки.

Из параметров камеры, которые бы мне хотелось настраивать - вывел на страницу только white balance (awb) - т.к. в автоматическом режиме при съемке серии он непредсказуемо меняется, что не здорово сказывается на итоговом видео: лучше зафиксировать awb для всей серии.

Запуск скрипта съемки серии кадров:

tl = subprocess.Popen([app_dir+'/timelapse.sh', awb, str(interval_ms), str(period_ms)])
timelapse = True
 
здесь уже используется неблокирующий вызов, т.к. после запуска съемки таймлапса, хочется иметь возможность работать с приложением - в строке статуса должна обновляться информация о том, сколько еще времени осталось до завершения съемки. 

Модуль subprocess содержит метод poll, который возвращает код завершения процесса или None, если тот еще длится:

if timelapse:
if tl.poll() is None:
waiting = end - now
state = "Processing timelapse until: " + end.strftime("%Y-%m-%d %H:%M:%S") + ", wait: " + str(waiting).split('.', 2)[0]
else:
timelapse = False
state = "Waiting"

Приложение содержит всего одну страницу, за отображение которой отвечает функция index():

@app.route("/", methods=['GET', 'POST'])
def index(): 

в ней обрабатываются запросы HTTP: GET и POST. Для отображения страницы используется функция render_template:

return render_template('index.html', **templateData)

параметры отображения ей при этом передаются с помощью словаря templateData - такой способ не ограничивает их количество:

templateData = {
'title' : 'Timelapse camera',
'time': timeString,
'state':state,
'user_image': '/static/img/snapshot.jpg',
'awb_list': awb_list,
'awb_sel': awb
}

Некоторую сложность для меня составило то, что после съемки кадра предпросмотра, он не обновлялся на странице - нашел такое комбинированное решение:

установить параметр конфигурации SEND_FILE_MAX_AGE_DEFAULT:

app = Flask(__name__)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 1

и добавить функцию отключения кэширования данных, выполняемую после каждого запроса:

@app.after_request
def add_header(response):
if 'Cache-Control' not in response.headers:
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
response.headers['Cache-Control'] = 'public, max-age=0'
return response

Работает на Chrome и на Firefox - если изображение /static/img/snapshot.jpg изменяется, то на страницу загружается обновленное.

Что касается скриптов: snapshot.sh просто вызывает raspistill с требуемыми параметрами - ничего особенного; timelapse.sh поинтересней:

#! /bin/bash

awb_value=$1
interval=$2
period=$3

now=$(date +"%T")
echo "Timelapse start: $now"

rm -r /home/pi/timelapse_img/* &&
# pkill -f app.py &&
# sudo ifconfig wlan0 down &&

raspistill -t $interval -tl $period -awb $awb_value -hf -vf -w 1920 -h 1080 -o /home/pi/timelapse_img/img_%06d.jpg &
pid=$!
wait $pid

now=$(date +"%T")
echo "Timelapse done: $now"

# sudo halt

в него я добавил еще пару функций:

  • очистка папки, куда будут записываться новые изображения
  • завершение работы приложения и выключение wi-fi для энергосбережения
  • выключение камеры после съемки - чтобы узнать, сколько заряда тратится на продолжительную съемку (например, за сутки).

Код страницы index.html также есть на git, приводить его здесь полностью нет смысла. Хочу только заметить, что изначально поставил себе цель заполнять шаблон данными из приложения, для чего flask, как мне видится, дает достаточно удобный функционал (правда, это мой первый подобный опыт, так что сравнивать особо не с чем). Например, для заполнения списка  установок баланса белого и сохранения выбранного элемента при обновлении страницы, можно использовать для формирования <select> такой цикл, в который достаточно передать список и имя выбранного элемента:

<label for="awb"> Auto white balance: </label>
<select id="awb" name="awb">
    {% for awb_item in awb_list: %}
        <option value={{ awb_item }} {% if awb_sel==awb_item %} selected {% endif %} >{{ awb_item }}</option>
{% endfor %}
</select>

Стили в style.css я использовал по минимуму: настройка фона, шрифтов и положения элементов на странице, чтобы вынести элементы для настройки и запуска таймлапса на отдельную строку. 

Сделал автозапуск приложения с помощью cron:

$ crontab -e

добавив в конец файла строку:

@reboot $(which python3) /home/pi/webapp/app.py 2>&1

Собственно, о приложении на этом все. Но есть еще один момент, о котором нужно упомянуть.

На модуле камеры есть красный светодиод, который включается на время съемки:

В светлое время это не создает проблем, а вот при съемке ночью, свет от него, отраженный от защитного стекла, начинает создавать очень неприятные блики в правом нижнем углу кадра:

Чтобы его отключить, необходимо в конец файла config.txt

$  sudo nano /boot/config.txt

добавить строку

disable_camera_led=1

после чего перезагрузить raspberry.

Вот такой получился проект :) Напоследок, таймлапс на 14 часов с интервалом съемки 20 секунд:


Let`s go design!

Комментарии