リモートコントロール付きRaspberry Piのスクールベル

こんにちは、カブロビテス。 シングルボードSoCベースのLinuxコンピューターが、アマチュアと多かれ少なかれプロのユーザーの間で今日広く使用されていることは秘密ではありません。 ますます多くのタスクをマイクロコンピューターの助けを借りて解決することができ、以前はマイクロコントローラーの助けを借りて排他的に解決したタスクさえも解決できます。 単純な問題を解決するための小さなコンピューターではありますが、本格的なコンピューターを使用するのはまだやり過ぎのように思えますが、考えてみましょう。 この記事は、これに関するhabrovchanin devzonaとの小さな論争に対する答えです。



背景



マイクロコントローラの使用には、学校の鐘の自動化よりも明確なニッチがあるかもしれません。 それはまさに、未知の開発者が5〜7年前にそのような素晴らしいデバイスを組み立てていたときに考えたことです。



MK 8050シリーズで組み立てられ、リアルタイムクロックが搭載されており、この時間を自家製のLEDマトリックスで表示でき、最も重要なことは、学校の鐘を含むリレーを時間内に引くことができるようです。 このデバイスは長年にわたって安全に機能しており、苦情はありませんでした。 しかし、すべてが流れ、変化します。そして、あるものについて綿密な研究を行った単純なハリコフの学校は、同じものについてさらに詳しく研究して、ライセウムで再認定を受けることにしました。 このような再認定には、とりわけ、45分のレッスンから40分の2アカデミック時間で構成されるペアへの移行が必要です。 そして、トラブルが起こりました。 安全に飲まれたMKの時計の開発者は海外に行き、ソースコードを残さず、再構成の可能性を提供しませんでした。 私の友人Kostyaが、ある秋の日にSkypeをノックしたのは、この問題が原因でした。



患者を診察した後、顧客の要求に応じて数週間以内にそれを作り直すことは不可能であるという理解に至りました。 基本的に、コードを最初から書き直す必要があります。 そして、突然、同じ日の夕方、DHLの宅配便業者が別のラズベリーを持ってきました。 それから、時計を作るだけでなく、魔法で時計を作るというアイデアが生まれました。 結局のところ、完全なLinuxが搭載されたマイクロコンピューター全体があり、手は解かれ、可能性は無限です!



問題の声明



朝、顧客と交渉した後、タスクは次のように設定されました:追加のソフトウェア(高価な)なしで任意のPCを使用してデバイスを設定し、インターネットから正確な時間を取得できるようにします(通話のクロックを同期でき、すべての通話は厳密に最大1秒まで可能です)自律的に動作し、将来の追加オプションとして、リモートサーバーから呼び出し構成を受信できる必要があります。 たとえば、地区は、特定のタイプの教育機関向けにコール構成を個別にレイアウトできます。 タスクが設定されたら、実装に進みます。



プロジェクトを実装するには、次のものが必要です。







Raspberry Piの初期構成を意図的に欠場しています。インターネットには、配布キットのインストール、ネットワークのセットアップ、タイムゾーンなどの資料がたくさんあります。



それでは始めましょう。



リアルタイムクロック



このデバイスのリアルタイムクロックとして、私はDS1302に小さなスカーフを取り付けました。中国から注文した大量のジャンクで見つけたからです。 これらの特定の時計とラズベリーとの接続について説明したすばらしい記事がネットワーク上見つかりました。 接続はとても簡単です。











同じページで、これらのRTCにデータを送受信できるソフトウェアをダウンロードできます。 システム時刻と同期する前にRTCの読み取り値を視覚化するために、ソフトウェアを少し自分用に再作成しました。



正確には、ラズベリータイムとNTPサーバーの同期に成功した場合に時計を更新する必要があります。NTPサーバーにアクセスできない場合は、ラズベリーシステムクロックをリアルタイムクロックと同期する必要があります。 DS1302は1日に数秒間クロールする癖があるため、このようなアルゴリズムが必要です。 ただし、同期が成功した後にntpdにスクリプトを実行させる方法は見つかりませんでした。 したがって、そのような松葉杖が生まれました:



/ usr / local / bin / update_rtc
#!/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
      
      







/etc/init.d/rtc
 #!/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
      
      







これら2つのファイルを使用すると、ロード後にネットワークが検出されない場合にラズベリーシステムクロックをRTCと同期したり、ネットワークが検出された場合にRTCの時刻を更新したりできます。 起動後30秒で、ntpdはシステムクロックを更新する時間をすでに持っているはずです。 最悪の場合、RTCはラズベリーが最後にオンにされた時間を記録します。 この解決策は理想とはほど遠いことを知っていますが、より良い解決策を思い付くことができませんでした。 頭に浮かぶ唯一のことは、リアルタイムの時間に多かれ少なかれ正確なデータがあることを確認するために、クラウンにラインを追加して2〜3時間ごとにRTCを更新することです。 尊敬されているコミュニティがあなたに最高の解決策を教えてくれるなら、私は嬉しいだけです。



Webサーバー



長い間考えもありませんでした。 サーバーの主なタスクは、2つのページを表示し、1つのPOST要求を処理することです。 PythonでのWebサーバーの教科書の実装は、一目瞭然です。



webserver.py
 #!/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リクエストプロセッサは、クライアントに2つのフォームとメインページを与えるだけで、現在のスケジュールに関するデータを変数に入力します。 POST要求ハンドラーは、フォームのデータをJSONファイルに保存します。JSONファイルは呼び出しのベースです。



実際には、学校のベルマネージャー



Python用のすばらしいGPIOライブラリのおかげで、LEDでラズベリーの学校の鐘を点滅させるのは非常に簡単です。 このスクリプトはこれを行います:



daemon.py
 #!/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レッグに高レベルを適用します)。 Webインターフェースから変更された場合、各呼び出しの後に、スケジュールを再読み取りします。 すべてが透明でシンプルです。



閲覧犬のデモとトレーニング



RTC自動実行の類推を使用して、次のファイルを作成します。



/etc/init.d/schedule_daemon
 #!/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
      
      







/etc/init.d/schedule_webserver
 #!/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
      
      









そして彼らのための「番犬」の台本。 これらのスクリプトは、サービスが実行されているかどうかを確認し、必要に応じて開始します。



/etc/init.d/schedule_daemon_wd
 #!/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
      
      







/etc/init.d/schedule_webserver_wd
 #!/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
      
      





そして、新しいタスクをクラウンに追加します。



/etc/cron.d/wd.cron
 #Watchdog tasks * * * * * /etc/init.d/schedule_daemon_wd * * * * * /etc/init.d/schedule_webserver_wd
      
      







これで、両方のデーモンが起動し、安定して動作することを確認できます。 wd.cronの最後に新しい行を追加することを忘れないでください。そうしないと、crondはそれを無視します!



パワーエレクトロニクスについて少し



パワーユニット全体は完全に標準で組み立てられています。 学校での通話の総電力は約0.5 kWであるため、この農場を切り替えるには、BC137XトライアックとMOC3061オプトカプラーを組み合わせれば十分です。 実践が示しているように、オプトカプラーを確実に含めるには3.3ボルトの論理ユニットで十分です。









ここにリレーを適用することは可能ですが、そのような素晴らしい半導体がある場合、どういうわけか私は連絡先を信頼しません。 レイアウトの写真を意図的に投稿しません。 美しいインストールには至りませんでした。



不足しているもの



もちろん、自由に使える本格的なLinuxコンピューターを使用すれば、機能を無期限に「ねじる」ことができ、開発時間は比較的短くなります。 マイクロコンピューターが対処すると思われる問題を解決するためにマイクロコンピューターを使用することを支持するのは、この状況です。 ただし、私の意見では、現在の実装に欠けているものを引き続きリストします。



まず 、セキュリティ。 少なくとも単純なHTTP-Authに悩む価値があるか、または小さなスクリプトを追加した後、システムの「管理パネル」に入るためのパスワードデータベースを作成する必要があります。 はい。フォームを送信する前と送信した後の両方で、データフィルタリングに取り組む価値があります。

第二に 、アカデミックを追加/削除する必要があります。 形に数時間。 気配りのある読者は、たとえば単純なJavaScriptコードを使用して、クライアント側のフォームに必要なフィールドを追加するだけでこれを実現できることに気付きました。

第三に、メインボタンに「パニックボタン」を作成したかったため、5〜10秒で呼び出しが開始されました。 幸いなことに、これに必要なものはすべて記事にあります。

第四に 、十分な無停電電源装置がありません。 お客様が開発を拒否したため、私たちは彼に連絡しませんでした。



それがすべて終わった方法



残念なことに、ハリコフの体育館では、各親から3つのグリブニアを収集することは非常に困難であることが決定され、最終的にゲートからのターンが与えられたため、実装はいくつかの重要なものを含まない現在のプロトタイプで停止しました要素の有限システム。 しかし、開発に費やした時間は無駄ではありませんでした。 特に、単一のシンクタンクからすべてを管理できる郊外地域で家の建設が完了するので、Pythonで鉄を扱うためのアプリケーションを開発する経験が私の人生で何度も役立つことを願っています。 通話を管理できれば、スケジュールに従って電球を点灯できます。



あとがき



親愛なる読者の皆さん、私はあなたに主要な考えを伝えることができたと思います。 一見些細なタスクにマイクロコンピューターを使用すると、実装がまったく新しいレベルになり、最も単純な実装、独自のプロトコル、複雑なサポートの代わりに、ほぼ無限の拡張性を備えた柔軟なシステムが得られ、将来的には使いやすさだけでなく、大幅なコスト削減。



上記の実装に3時間強が費やされました。 それを念頭に置くには、多くのことが必要です。 従来は、一部の場所での曲がったコードやエラーの可能性に気を取られないようにお願いします。 これはHabréに関する私の最初の記事であり、Pythonで最初に実現されたプロジェクトです。 修正、提案、提案を常に歓迎します。 スクリーンショットとビデオ作品は、オンデマンドでレイアウトされます。



同志devzonaによるこのような機能の実装を楽しみにしていますが、これはArduinoのみに基づいています。 マイクロコントローラでデバイスを開発するという点で、彼から学ぶべきことがあると思います。 この記事は本当にエキサイティングなものになると約束しています。



All Articles