Доброго времени суток, уважаемыее. Не секрет, что одноплатные Linux сомпьютеры на базе SoC на сегоднящний день получили широкое распространение как среди любителей, так и среди более-мелее профессиональных пользователей. Все больше и больше задач можно решить с помощью микрокомпьютеров, и даже тех задач, которые раньше решались исключительно при помощи микроконтроллеров. Казалось бы, использование полноценного, хоть и мелкого компьютера для решения простых задач — это тот еще оверкилл, однако давайте разберемся, так ли это плохо? Эта статья является ответом на наш небольшой спор сином devzona по этому поводу.
Предыстория
Казалось бы, что может быть более явной нишей для применения микроконтроллеров, чем автоматизация школьного звонка? Именно так думал неизвестный разработчик лет 5-7 назад, когда собирал вот такое замечательное устройство.
Собрано, судя по всему, на МК серии 8050, имеет на боту часики реального времени, умеет это время показывать на самодельной светодиодной матрице, и самое главное, умеет вовремя дергать релюшку, включающую школьный звонок. Устройство благополучно работает уже много лет, претензий к нему не было. Однако, все течет и меняется, и однажды простая Харьковская школа с углубленным изучением чего-то там решила пройти переаттестацию в лицей с еще более углубленным изучением того самого. Такая переаттестация, помимо всего прочего, требует перехода с 45-минутных уроков на пары, состоящие из двух академических часов по 40 минут. Тут-то и пришла беда. Разработчик часиков на МК благополучно спился уехал за границу, исходников не оставил, возможности перенастройки не предусмотрел. Именно с этой проблемой постучался ко мне в Скайп одним осенним днем мой друг Костя.
Осмотрев пациента пришло понимание, что быстрее чем за пару недель его переделать под требования заказчика не получится. По сути, нужно переписывать код с нуля. И, внезапно, вечером этого же дня курьер из DHL привез мне очередной Raspberry. Тут и пришла идея сделать свои часики, да не просто часики, а с магией. Ведь у нас есть целый микрокомпьютер с полноценным линуксом не борту, руки развязаны, возможности безграничны!
Постановка задачи
Утром, после переговоров с заказчиком, задача была поставлена так: устройство должно конфигурироваться при помощи любого ПК, без дополнительного софта (дорого), уметь подтягивать точное время из интернета (по звонкам можно синхронизировать часы, все звонки строго с точностью до секунды), уметь работать автономно, и, как дополнительная опция на будущее, должны уметь получать конфигурацию звонков с удаленного сервера. Например, районо может самостоятельно выкладывать конфиг звонков для учебных заведений определенного типа. Задача поставлена, приступаем к реализации.
Для реализации проекта нам нужно следующее:
- Демон, умеющий дергать нужную GPIO ножку в нужное время
- Веб-интерфейс для конфигурирования времени звонков
- Часы реального времени
- Силовая электроника для управления школьными звонками
Я преднамеренно упускаю начальную конфигурацию Raspberry Pi, интернет полон материалами по установке дистрибутива, настройке сети, тайм-зоны и т.д.
Итак, приступим.
Часы реального времени
В качестве часов реального времени для устройства взял мелкую платку на DS1302, просто потому, что она нашлась у меня в кучке заказанного из Китая хлама. В сети обнаружилась замечательная статья, описывающая подключение именно этих часиков к малинке. Подключение довольно простое.
На этой же страничке доступен для скачивания софт, способный получать и записывать данные в эти RTC. Софт я немного переделал под себя, дабы визуализировать показания RTC перед синхронизацией с системным временем.
По правильному, часики должны обновляться в случае успешной синхронизации времени малинки с NTP сервером, и, если доступа к NTP серверу нет, тогда системные часы малинки должны быть синхронизированы с часами реального времени. Такой алгоритм необходим, так как DS1302 имеет привычку уползать на пару секунд в сутки, что неприятно. Однако, как заставить ntpd запускать скрипт после успешной синхронизации, я так и не нашел. Поэтому родился такой вот костыль:
#!/bin/bash
LOG="/var/log/rtc-sync.log"
DATE=`date`
sleep 30
echo "*** $DATE" >>$LOG
until ping -nq -c3 8.8.8.8; do
echo "No network, updating system clock from RTC." >>$LOG
rtc-pi 2>&1
exit
done
echo "Network detected. Updating RTC." >>$LOG
date +%Y%m%d%H%M%S |xargs ./rtc-pi 2>&1
#!/bin/sh
# /etc/init.d/rtc
### BEGIN INIT INFO
# Provides: RTC controll
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Simple script to start RTC sync
# Description: A simple script from prostosergik <serge.liskovsky@gmail.com> which will run script that synchronizes RTC module clock with system clock at startup.
### END INIT INFO
case "$1" in
start)
echo "RTC sync..."
/usr/local/bin/update_rtc& 2>&1
;;
stop)
echo "Stopping RTC Sync..."
# kill application you want to stop
killall update_rtc
;;
*)
echo "Usage: /etc/init.d/rtc {start|stop}"
exit 1
;;
esac
exit 0
… и активируем автозагрузку:
sudo update-rc.d rtc defaults
Эти два файла позволяют синхронизировать системные часики малинки с RTC в случае, если после загрузки не обнаружена сеть, или обновить время в RTC, если есть обнаружена. Через 30 сек после загрузки ntpd должен бы уже успеть обновить системные часы. В худшем случае, в RTC будет записано последнее время, когда Raspberry был включен. Я знаю, что это решение далеко не идеальное, но лучшего придумать не смог. Единственное, что приходит в голову — добавить строчку в крон для обновления RTC раз в 2-3 часа, дабы быть уверенным, что в часах реального времени более-менее точные данные. Если многоуважаемое сообщество подскажет лучшее решение — буду только рад.
Веб-сервер
Тут долго думать не пришлось. Основная задача сервера — показывать две странички и обрабатывать один POST запрос. Хрестоматийная реализация веб-сервера на Python просто напрашивается сама собой.
#!/usr/bin/python
# -*- coding: utf-8 -*-
import cgi, re, json
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import collections
from config import *
class MainRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/':
lessons = readSchedule()
schedule = ''
for lesson in lessons:
schedule += u"<b>Час "+lesson+"</b>: "+lessons[lesson].get('start', '--:--') + " - " + lessons[lesson].get('end', '--:--') + "<br />"
data = {
'schedule': schedule.encode('utf-8')
}
TemplateOut(self, 'index.html', data)
return
elif self.path == '/form.html':
lessons = readSchedule()
form = ''
for lesson in lessons:
form += u"<div class='form_block'><label>Час "+lesson+"</label> <input type='text' name='lesson_"+lesson+"_start' value='"+lessons[lesson].get('start', '--:--') + "'> - <input type='text' name='lesson_"+lesson+"_end' value='"+lessons[lesson].get('end', '--:--') + "'> </div> """
data = {
'form': form.encode('utf-8')
}
TemplateOut(self, 'form.html', data)
return
elif self.path == '/remote.html':
lessons = readScheduleRemote()
form = ''
for lesson in lessons:
form += u"<div class='form_block'><label>Час "+lesson+"</label> <input type='text' name='lesson_"+lesson+"_start' value='"+lessons[lesson].get('start', '--:--') + "'> - <input type='text' name='lesson_"+lesson+"_end' value='"+lessons[lesson].get('end', '--:--') + "'> </div> """
data = {
'form': form.encode('utf-8')
}
TemplateOut(self, 'form.html', data)
return
else:
try:
TemplateOut(self, self.path)
except IOError:
self.send_error(404, 'File Not Found: %s' % self.path)
def do_POST(self):
# Parse the form data posted
form = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
environ={
'REQUEST_METHOD':'POST',
'CONTENT_TYPE':self.headers['Content-Type'],
}
)
lessons = {}
if self.path.endswith('save'):
# Echo back information about what was posted in the form
for field in form.keys():
field_item = form[field]
if type(field_item) == type([]):
pass # no arrays processing now
else:
if field_item.filename:
pass #no files now.
else:
if re.match('lesson_([d]+)_(start|end)', field):
(lesson, state) = re.findall('lesson_([d]+)_(start|end)', field)[0]
try:
lessons[lesson]
except Exception:
lessons[lesson] = {}
lessons[lesson][state] = field_item.value
# printlessons
json_s = json.dumps(lessons)
if json_s:
try:
f = open(JSON_FILE, 'w+')
f.write(json_s)
f.close()
HTMLOut(self, 'Saved OK.' + JS_REDIRECT)
except IOError, e:
# raise e
HTMLOut(self, 'Error saving. IO error. '+e.message)
else:
HTMLOut(self, 'Json Error.')
else:
self.send_error(404, 'Wrong POST url: %s' % self.path)
return
def Redirect(request, location):
request.send_response(301)
request.send_header('Location', location)
request.end_headers()
return
def Headers200(request):
request.send_response(200)
request.send_header('Content-type', 'text/html')
request.end_headers()
return
def TemplateOut(request, out_file, data = {}):
f = open(SCRIPT_DIR + out_file)
out = f.read()
f.close()
#tiny template engine
for key, var in data.items():
out = out.replace("{{"+key+"}}", var)
HTMLOut(request, out)
def HTMLOut(request, html):
Headers200(request)
f = open(SCRIPT_DIR + 'base.html')
out = f.read()
f.close()
out = out.replace("{{content}}", html)
request.wfile.write(out)
def readSchedule():
try:
f = open(JSON_FILE, 'r')
json_s = f.read()
f.close()
except IOError:
return []
try:
lessons = json.loads(json_s)
except Exception:
return []
lessons = collections.OrderedDict(sorted(lessons.items()))
return lessons
def readScheduleRemote():
import urllib2
try:
response = urllib2.urlopen(REMOTE_URL)
json_s = response.read()
except Exception:
return []
try:
lessons = json.loads(json_s)
except Exception:
return []
lessons = collections.OrderedDict(sorted(lessons.items()))
return lessons
def main():
try:
server = HTTPServer(('', 8088), MainRequestHandler)
print 'Started httpserver...'
server.serve_forever()
except KeyboardInterrupt:
print '^C received, shutting down server.'
server.socket.close()
if __name__ == '__main__':
main()
Из скуки и для лучшей расширяемости был даже добавлен простенький шаблонизатор. Обратите внимание, интерпретатор прописан в начале скрипта, так что после установки прав на исполнение, скрипт может быть запущен прямо из командной строки.
Что делает этот скрипт, думаю, понятно и без комментариев. Обработчик GET-запросов попросту отдает клиенту две формочки и главную страницу, заполняя переменную данными про текущее расписание. Обработчик POST-запросов сохраняет данные из формы в JSON-файл, который и является базой звонков.
Собственно, управлятор школьным звонком
Благодаря замечательной библиотеке GPIO для Python, моргать светодиодиком школьным звонком с малинки очень просто. Этим занимается такой скрипт:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import time
import threading
import json
import RPi.GPIO as GPIO
from config import *
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(25, GPIO.OUT)
GPIO.output(25, False)
def read_schedule():
schedule = []
try:
f = open(JSON_FILE, 'r')
json_s = f.read()
f.close()
try:
json_data = json.loads(json_s)
except Exception, e:
json_data = []
for lesson in json_data.values():
start = lesson.get('start', False)
end = lesson.get('end', False)
if start is not False:
# print start.split(":")
(s_h, s_m) = start.split(":")
schedule.append({'h': int(s_h), 'm':int(s_m)})
del s_h
del s_m
if end is not False:
(e_h, e_m) = end.split(":")
schedule.append({'h': int(e_h), 'm':int(e_m)})
del e_h
del e_m
return schedule
# schedule
except IOError, e:
return []
except Exception, e:
return []
class Alarm(threading.Thread):
def __init__(self):
super(Alarm, self).__init__()
self.schedule = read_schedule()
self.keep_running = True
def run(self):
try:
while self.keep_running:
now = time.localtime()
for schedule_item in self.schedule:
if now.tm_hour == schedule_item['h'] and now.tm_min == schedule_item['m']:
print "Ring start..."
GPIO.output(25, True)
time.sleep(5)
print "Ring end..."
GPIO.output(25, False)
self.schedule = read_schedule() #reload schedule if it was changed
time.sleep(55) # more than 1 minute
#print "Check at "+str(now.tm_hour)+':'+str(now.tm_min)+':'+str(now.tm_sec)
time.sleep(1)
except Exception, e:
raise e
# return
def die(self):
self.keep_running = False
alarm = Alarm()
def main():
try:
alarm.start()
print 'Started daemon...'
while True:
continue
except KeyboardInterrupt:
print '^C received, shutting down daemon.'
alarm.die()
if __name__ == '__main__':
main()
Скрипт создает новый поток, в котором проверяет время каждую секунду. Если время найдено в файле расписания, то на 5 секунд включаем звонок (подаем высокий уровень на ножку 25 GPIO). После каждого звонка перечитываем расписание, на случай, если оно было изменено из веб-интерфейса. Все прозрачно и просто.
Демонизируем и дрессируем смотрового пса
Действуя по аналогии с автозапуском синхронизации RTC, создаем следующие файлики:
#!/bin/sh
### BEGIN INIT INFO
# Provides: schedule_daemon
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# description: School Ring Schedule daemon
# processname: School Ring Schedule daemon
### END INIT INFO
export SCHEDULE_ROOT=/home/pi/ring_app
export PATH=$PATH:$SCHEDULE_ROOT
SERVICE_PID=`ps -ef | grep daemon.py | grep -v grep | awk 'END{print $2}'`
usage() {
echo "service schedule_daemon {start|stop|status}"
exit 0
}
case $1 in
start)
if [ $SERVICE_PID ];then
echo "Service is already running. PID: $SERVICE_PID"
else
$SCHEDULE_ROOT/daemon.py& 2>&1
fi
;;
stop)
if [ $SERVICE_PID ];then
kill -9 $SERVICE_PID
else
echo "Service is not running"
fi
;;
status)
if [ $SERVICE_PID ];then
echo "Running. PID: $SERVICE_PID"
else
echo "Not running"
fi
;;
*) usage
;;
esac
#!/bin/sh
### BEGIN INIT INFO
# Provides: schedule_webserver
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# description: School Ring Schedule web-server
# processname: School Ring Schedule web-server
### END INIT INFO
export SCHEDULE_ROOT=/home/pi/ring_app
export PATH=$PATH:$SCHEDULE_ROOT
SERVICE_PID=`ps -ef | grep webserver.py | grep -v grep | awk 'END{print $2}'`
usage() {
echo "service schedule_webserver {start|stop|status}"
exit 0
}
case $1 in
start)
if [ $SERVICE_PID ];then
echo "Service is already running. PID: $SERVICE_PID"
else
$SCHEDULE_ROOT/webserver.py& 2>&1
fi
;;
stop)
if [ $SERVICE_PID ];then
kill -9 $SERVICE_PID
else
echo "Service is not running"
fi
;;
status)
if [ $SERVICE_PID ];then
echo "Running. PID: $SERVICE_PID"
else
echo "Not running"
fi
;;
*) usage
;;
esac
И скрипты «сторожевых собачек» для них. Эти скрипты проверяют, запущен ли сервис, и, при необходимости, запускают его.
#!/bin/sh
### BEGIN INIT INFO
# Provides: schedule_daemon_wd
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# description: School Ring Schedule daemon watchdog
# processname: School Ring Schedule daemon watchdog
### END INIT INFO
export SCHEDULE_ROOT=/home/pi/ring_app
export PATH=$PATH:$SCHEDULE_ROOT
SERVICE_PID=`ps -ef | grep daemon.py | grep -v grep | awk '{print $2}'`
check_service() {
if [ -z $SERVICE_PID ];then
service schedule_daemon start
fi
}
check_service
usage() {
echo "schedule_daemon_wd {start|stop|status}"
exit 0
}
case $1 in
start )
if [ $SERVICE_PID ];then
echo "schedule_daemon is already running. PID: $SERVICE_PID"
else
service schedule_daemon start
fi
;;
stop )
if [ $SERVICE_PID ];then
service schedule_daemon stop
else
echo "schedule_daemon is already stopped"
fi
;;
status)
if [ $SERVICE_PID ];then
echo "schedule_daemon is running. PID: $SERVICE_PID"
else
echo "schedule_daemon is not running"
fi
;;
*) usage
;;
esac
#!/bin/sh
### BEGIN INIT INFO
# Provides: schedule_webserver_wd
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# description: School Ring Schedule web-server watchdog
# processname: School Ring Schedule web-server watchdog
### END INIT INFO
export SCHEDULE_ROOT=/home/pi/ring_app
export PATH=$PATH:$SCHEDULE_ROOT
SERVICE_PID=`ps -ef | grep webserver.py | grep -v grep | awk '{print $2}'`
check_service() {
if [ -z $SERVICE_PID ];then
service schedule_webserver start
fi
}
check_service
usage() {
echo "schedule_webserver_wd {start|stop|status}"
exit 0
}
case $1 in
start )
if [ $SERVICE_PID ];then
echo "schedule_webserver is already running. PID: $SERVICE_PID"
else
service schedule_webserver start
fi
;;
stop )
if [ $SERVICE_PID ];then
service schedule_webserver stop
else
echo "schedule_webserver is already stopped"
fi
;;
status)
if [ $SERVICE_PID ];then
echo "schedule_webserver is running. PID: $SERVICE_PID"
else
echo "schedule_webserver is not running"
fi
;;
*) usage
;;
esac
Аналогично, делаем эти скрипты автоматически загружаемыми при старте системы:
sudo update-rc.d schedule_daemon_wd defaults
sudo update-rc.d schedule_webserver_wd defaults
И добавляем в крон новые задания:
#Watchdog tasks
* * * * * /etc/init.d/schedule_daemon_wd
* * * * * /etc/init.d/schedule_webserver_wd
Теперь мы можем быть уверены, что оба демона запустились и будут стабильно работать. Не забываем добавить новую строчку в конце wd.cron, иначе crond будет его игнорировать!
Немного про силовую электронику
Вся силовая часть собрана совершенно стандартно. Суммарная мощность звонков в школе около 0.5 КВт, так что симистора BC137X в паре с оптроном MOC3061 вполне достаточно для коммутации этого хозяйства. Как показала практика, 3.3 вольта логической единицы достаточно для уверенного включения оптрона.
Можно было бы применить тут и реле, но как-то я не доверяю контактам, когда есть такие замечательные полупроводники. Фотографию макета преднамеренно не выкладываю, т.к. до красивого монтажа так и не дошло.
Чего не хватает
Конечно, имея полноценный Linux-компьютер в своем распоряжении, можно «наворачивать» функциональность до бесконечности, причем времени на разработку уйдет сравнительно мало. Именно это обстоятельство говорит в пользу применения микрокомпьютеров для решения задач, с которыми, казалось бы, справится и микроконтроллер. Однако, все-же перечислю то, чего, по моему мнению, не хватает текущей реализации:
Во первых, безопасность. Стоило бы заморочиться на простой HTTP-Auth хотя бы, или, дописав немного скрипт, сделать базу паролей для входа в «админ-панель» системы. Да и над фильтрацией данных поработать стоит, как до, так и после отправки формы.
Во вторых, нужно бы добавить добавление/удаление академ. часов в форму. Внимательный читатель заметил, что это можно сделать попросту дорисовав в форму на клиентской стороне необходимые поля при помощи, например, простенького JavaScript кода.
В третьих, мне так хотелось сделать «тревожную кнопку» на главной, которая запускала бы звонок за 5-10 секунд. Пусть это будет маленькая задачка для пытливых умов читателей, благо, все необходимое для этого есть в статье.
В четвертых, не хватает блока бесперебойного питания. Ввиду отказа заказчика от разработки, до него мы так и не дошли.
Чем всё закончилось
К сожалению, Харьковская гимназия с углубленным изучением чего-то там решила, что собрать по 3 гривны с каждого родителя это очень, очень трудно, и нам в итоге дали от ворот поворот, поэтому реализация остановилась на действующем прототипе, который не содержит некоторых важных для конечной системы элементов. Но время, потраченное на разработку не прошло даром. Опыт разработки приложений для работы с железом на Python мне, надеюсь, не раз пригодится в жизни, тем более на загородном участке заканчивается строительство дома, в котором предусмотрена возможность управления всем из единого мозгового центра. Если смог управлять звонком, то и лампочки по расписанию включать смогу.
Послесловие
Надеюсь, уважаемые читатели, мне удалось донести до вас главную мысль. Применение микрокомпьютеров для тривиальных, казалось бы, задач может поднять их реализацию на принципиально новый уровень, и вместо простейших реализаций, проприетарных протоколов и сложной поддержки, мы получаем гибкую систему с практически бесконечной расширяемостью, что в будущем выльется не только в удобство использования, но и в существенную экономию средств.
На реализацию всего вышеописанного было потрачено чуть более трех часов. Доведение до ума того, что есть требует еще столько же. Традиционно попрошу не пинать сильно за кривоватый местами код и возможные ошибки. Это моя первая статья на Хабре, и первый реализованный проект на Python. Всегда рад поправкам, пожеланиям и предложениям. Скриншоты и видео работы выложу по требованию.
С нетерпением жду реализации комрадом devzona подобного функционала, но только на основе Arduino. Уверен, мне есть чему у него поучиться в плане разработки устройств на микроконтроллерах. Статья обещает быть воистину захватывающей.
Автор: prostosergik