
すべての設計の最初にarduinoを習得するか、温度および(または)他の環境パラメーターを測定するためのデバイスを繰り返し使用するように思われます。 残念ながら、そのようなデザインの大部分だけが家庭でほとんど役に立たない-それはトレーニングとして機能しますが、利点はありません。 この欠陥を修正してみましょう。 記事では、温度、湿度、気圧センサーから測定値を収集する例を使用して、データを測定および保存するための複合体の作成について説明します。 デバイスの要件と交換プロトコルの説明から始め、データベースからデータを受信するためのWebサービスで終わります。 詳細な計算やウォークスルーはありませんが、少しの理論と多くのコードがあります。
この話は、家の気象観測所のリモートセンサーのワイヤーがペットの1人に噛まれた日から始まりました。 私はワイヤーを修理しましたが、その事件の後、デバイスは定期的に嘘を見せ始めました。 少量の露出でデータを完全に歪めるのに十分な場合、アナログ測定には欠点があります。 この事件により、私自身も同様の複合施設を作るようになりました。
問題を熟考した後、次のTKが得られました。
- センサーは、妥当な精度のデジタルでなければなりません。 温度-DS1820、湿度-DHT22、気圧-BMP085。 センサーの選択は、ビンにあるためです。 ところで、これら3つのタイプすべてに温度測定機能がありますが、DS1820は並列に接続できるため、DS1820を使用します。
- これらのセンサーは、即座に接続する必要があります。 オペレーターの介入は必要ありません。
- センサーを接続する必要があるコントローラーにアクセスできる必要があります。 私が選んだのはArduinoでした。なぜなら、それは安価で、最低レベルのエントリーだからです。
- コントローラーは、シリアルポート経由でコンピューターに接続する必要があります。 一般的で安価なソリューションとしてUSB2Serialアダプターを使用します。
- コントローラはコンピュータからある程度離れた場所に配置でき、1つのポートに複数のコントローラが存在する可能性があるため、交換プロトコルはデータの歪みやアドレス指定デバイスの可能性に対する保護を提供する必要があります。
- 複合体は、すべての測定の履歴をデータベースに保存する必要があります。 私の選択はSQLiteです。
- コントローラーを操作するためのすべてのプログラムは移植可能である必要があります。 大幅な変更なしで、異なるプラットフォームで同等に動作します。 私の選択はPython 2.7です。
コンピューターとコントローラー間の通信プロトコルの説明
コントローラとのデータ交換は非同期であり、以前は長さが未知のパケットであったため、
SLIPはチャネルプロトコルの基盤として採用されました。 これは、SLIPフレームを使用してデータが送信されるプロトコルです。 SLIPフレームの境界は、ENDフラグ(0xC0)です。 バイト0xC0がフレーム内で発生した場合、0xDB、0xDC ESCシーケンスに置き換えられ、ESCバイト(0xDB)が見つかった場合、シーケンス(0xDB、0xDD)に置き換えられます。 逆変換は対称です。
SLIPフレームでは、事前に計算されたチェックサムでメッセージをラップします。
チェックサムを計算するために、多項式0xA001(modbus)を使用したCRC16アルゴリズムが使用されました。
- 16ビットレジスタ(CRC)に0xFFFFがロードされます。
- メッセージの最初のバイトは、CRCレジスタの内容とともに排他的に追加されます。 結果はCRCレジスタに格納されます。
- CRCレジスタは右に1ビットシフトされ、最上位ビットは0で埋められます。
- (最下位ビットが1の場合):CRCの内容は、多項式番号0xA001で排他的論理和に追加されます。
- 手順3と4を8回繰り返します。
- 後続のすべての送信バイトについて、手順2〜5が繰り返されます。
- CRCレジスタの最終的な内容はチェックサムです。
CRCは、最初に低バイト、次に高バイトの形式でメッセージの最後に追加されます。
アプリケーションプロトコル
機器リクエストの形式
device_address(1バイト)クラス(1バイト)[メソッド(1バイト)] [データ(Nバイト)]
機器の応答形式:
device_address(1バイト)データ(Nバイト)
クラス0(PING)
0x55 0xAA 0x55 0xAAを返します
クラス1(情報)
方法
0-温度センサーの数の要求
戻り値:(符号なし文字)量
1-温度センサーからの読み取り値とシリアル番号の要求
戻り値:((フロート)温度(8バイト)sernum)*センサー数
2-圧力センサーからの読み取りを要求する
戻り値:(int32_t)圧力(char)sernum
3-湿度センサーからの読み取り値を要求する
戻り値:(浮動)湿度(バイト)sernum
クラスは2つしかありませんが、読者は、必要に応じて、必要なパラメーターを使用してプロトコルを拡張することができます。例
リクエスト
デバイスアドレス-00
クラス-00(PING)
チェックサム-01 B0
最終パッケージ-C0 00 00 B0 01 C0
答え
機器の回答-C0 00 55 AA 55 AA C3 AA C0
デバイスアドレス-00
チェックサム-AA C3
メッセージ-55 AA 55 AA(PINGへの返信)
計器回路

写真のレイアウト

これは、Nokiaの画面サポート付きの中間バージョンの写真です(コードは記事の最後にあるリンクからリポジトリにあります)。
完成品の写真


ソースコード:
Arduinoのスケッチ#include <DallasTemperature.h> #include <Adafruit_BMP085.h> #include <OneWire.h> #include <DHT.h> #define ONE_WIRE_BUS 10 #define TEMPERATURE_PRECISION 9 #define DHTPIN 2 #define DHTTYPE DHT22 OneWire oneWire(ONE_WIRE_BUS); DallasTemperature sensors(&oneWire); Adafruit_BMP085 bmp; const unsigned char MAXNUMBERS = 10; DeviceAddress addresses[MAXNUMBERS]; unsigned char numbers; DHT dht(DHTPIN, DHTTYPE); char readbuf[50]; char writebuf[130]; char tmpbuf[50]; int msglen = 0; const int bufLength = 8; const char SLIP_END = '\xC0'; const char SLIP_ESC = '\xDB'; const char SLIP_ESC_END = '\xDC'; const char SLIP_ESC_ESC = '\xDD'; const char CS_PING = '\x00'; const char CS_INFO = '\x01'; const char LOC_ADR = '\x00'; int transferData(char *buf, unsigned char cnt) { Serial.print(SLIP_END); for (int i = 0; i < cnt; i++) { switch (buf[i]) { case SLIP_END: Serial.print(SLIP_ESC); Serial.print(SLIP_ESC_END); break; case SLIP_ESC: Serial.print(SLIP_ESC); Serial.print(SLIP_ESC_ESC); break; default: Serial.print(buf[i]); break; } } Serial.print(SLIP_END); } unsigned short getCRC(char *buf, unsigned char cnt) { unsigned short temp, temp2, flag; temp = 0xFFFF; for (int i = 0; i < cnt; i++) { temp ^= (unsigned char) buf[i]; for (int j = 1; j <= 8; j++) { flag = temp & 0x0001; temp >>= 1; if (flag) temp ^= 0xA001; } } temp2 = temp >> 8; temp = (temp << 8) | temp2; temp &= 0xFFFF; return temp; } int addCRC(char *buf, unsigned char cnt) { unsigned short crc = getCRC(buf, cnt); memcpy(&buf[cnt], &crc, 2); return cnt + 2; } void setup() { Serial.begin(9600); bmp.begin(); sensors.begin(); dht.begin(); } void loop() { float humidity = dht.readHumidity(); int32_t pressure = (int32_t)(bmp.readPressure() / 133.3224); numbers = 0; for (int i = 0; i < MAXNUMBERS; i++) { if (!sensors.getAddress(addresses[i], i)) break; numbers++; } for (unsigned char i = 0; i < numbers; i++) { sensors.setResolution(addresses[i], TEMPERATURE_PRECISION); } sensors.requestTemperatures(); if (msglen) { unsigned short msgcrc; memcpy(&msgcrc, &readbuf[msglen-2], 2); unsigned short crc = getCRC(readbuf, msglen-2); if (crc == msgcrc) { char adr = readbuf[0]; char cs = readbuf[1]; char mtd = readbuf[2]; int len; unsigned char n; float temp; if (adr == LOC_ADR) { switch (cs) { case CS_PING: writebuf[0] = LOC_ADR; writebuf[1] = '\x55'; writebuf[2] = '\xAA'; writebuf[3] = '\x55'; writebuf[4] = '\xAA'; len = addCRC(writebuf, 5); delay(100); transferData(writebuf, len); break; case CS_INFO: switch (mtd) { case 0: writebuf[0] = LOC_ADR; writebuf[1] = numbers; len = addCRC(writebuf, 2); delay(100); transferData(writebuf, len); break; case 1: writebuf[0] = LOC_ADR; writebuf[1] = numbers; for (int i=0; i < numbers; i++) { temp = sensors.getTempC(addresses[i]); memcpy(&writebuf[i*12+2], &temp, 4); memcpy(&writebuf[i*12+6], &addresses[i], 8); } len = addCRC(writebuf, numbers*12+2); delay(100); transferData(writebuf, len); break; case 2: writebuf[0] = LOC_ADR; memcpy(&writebuf[1], &pressure, 4); writebuf[5] = 0; len = addCRC(writebuf, 6); delay(100); transferData(writebuf, len); break; case 3: writebuf[0] = LOC_ADR; memcpy(&writebuf[1], &humidity, 4); writebuf[5] = 0; len = addCRC(writebuf, 6); delay(100); transferData(writebuf, len); break; } break; } } } msglen = 0; } } void serialEvent() { msglen = readCommand(readbuf); } int readCommand(char *buf) { int i = 0; bool escaped = false; char c = (char) Serial.read(); if (c == SLIP_END) { bool beginflag = true; while (beginflag) { char c1 = (char) Serial.read(); switch (c1) { case SLIP_END: return i; break; case SLIP_ESC: escaped = true; break; case SLIP_ESC_END: if (escaped) { buf[i] = SLIP_END; escaped = false; } else buf[i] = c1; i++; break; case SLIP_ESC_ESC: if (escaped) { buf[i] = SLIP_ESC; escaped = false; } else buf[i] = c1; i++; break; default: if (escaped) { return 0; } else buf[i] = c1; i++; break; } } } return i; }
クラスslip.py class SlipConv: def __init__(self): self.started = False self.escaped = False self.packet = '' self.SLIP_END = '\xc0' self.SLIP_ESC = '\xdb' self.SLIP_ESC_END = '\xdc' self.SLIP_ESC_ESC = '\xdd' self.serialComm = None def __getcrc(self, buf): temp = 0xffff for c in buf: i = ord(c) temp ^= i j = 1 while j <= 8: flag = temp & 0x0001 temp >>= 1 if flag > 0: temp ^= 0xa001 j += 1 temp2 = temp >> 8 temp = (temp << 8) | temp2 temp &= 0xffff return temp def addcrc(self, packet): crc = self.__getcrc(packet) return packet + chr(crc & 0xff) + chr(crc >> 8) def checkcrc(self, packet): tmpcrc = self.__getcrc(self.getmsgpart(packet)) msgcrc = self.getcrcpart(packet) return (chr(tmpcrc & 0xff) + chr(tmpcrc >> 8)) == msgcrc def getcrcpart(self, packet): return packet[len(packet)-2:len(packet)] def getmsgpart(self, packet): return packet[0:len(packet)-2] def unslip(self, stream): packetlist = '' for char in stream: if char == self.SLIP_END: if self.started: packetlist += self.packet else: self.started = True self.packet = '' elif char == self.SLIP_ESC: self.escaped = True elif char == self.SLIP_ESC_END: if self.escaped: self.packet += self.SLIP_END self.escaped = False else: self.packet += char elif char == self.SLIP_ESC_ESC: if self.escaped: self.packet += self.SLIP_ESC self.escaped = False else: self.packet += char else: if self.escaped: self.packet = '' self.escaped = False return '' else: self.packet += char self.started = True self.started = False return packetlist def slip(self, packet): encoded = self.SLIP_END for char in packet: if char == self.SLIP_END: encoded += self.SLIP_ESC + self.SLIP_ESC_END elif char == self.SLIP_ESC: encoded += self.SLIP_ESC + self.SLIP_ESC_ESC else: encoded += char encoded += self.SLIP_END return encoded
ホストコンピューターについて少し
ホストとして、絶対にPython 2.7とSQLiteがインストールされた任意のコンピューターを使用できます。 動作するには、
pyserialライブラリをインストールする必要があります。
選択肢は、すでにかなり古いAsus WL-500gpルーターにありました。
OpenWrtをインストールし、USBフラッシュをマウントし、Python、SQLite、およびライブラリをインストールしました。
テストスクリプトを使用して、デバイスの状態を確認できます。
すべてが正常に機能する場合、出力は次のようになります。
Opened /dev/ttyUSB0 at 9600. Ping adr=0 Sent 6 bytes: C0 00 00 B0 01 C0 Received 9 bytes: C0 00 55 AA 55 AA C3 AA C0 CRC - OK Ping to adr=0 - OK Get the atmospheric pressure. Sent 7 bytes: C0 00 01 02 91 F1 C0 Received 10 bytes: C0 00 EB 02 00 00 00 B4 25 C0 CRC - OK 747 mmHg on the sensor with the serial number 00 Pressure - 747 mmHg Get a humidity. Sent 7 bytes: C0 00 01 03 51 30 C0 Received 10 bytes: C0 00 9A 99 33 42 00 34 B6 C0 CRC - OK 44.9000015259% on the sensor with the serial number 00 Humidity - 44.9000015259% Get a temperature from sensors. Sent 7 bytes: C0 00 01 01 90 B1 C0 Received 19 bytes: C0 00 01 00 80 BD 41 10 60 3B 4F 00 08 00 DB DC 21 1B C0 CRC - OK It has 1 temperature sensors: 23.7C on the sensor with the serial number 10 60 3B 4F 00 08 00 C0 T1 - 23.7 C, sensor 10 60 3B 4F 00 08 00 C0
ここで、測定結果をデータベースに保存する必要があります。
次の構造を作成します。

データベースには、センサータイプ、センサーおよび計測(タイプ、センサー、測定値)およびすべてのレコードの1つのビュー(フラット測定テーブル)の3つのメインテーブルが含まれています。
hourlyrecordsおよびdailyrecordsテーブルには、時間別および日別の平均データが含まれています。
dbversionテーブルで、データベースのバージョン。
私はすぐに予約します-3.8.2 SQLiteがあり、更新する理由がなかったため、最新バージョンに登場したSQL-92の制限を超える組み込みSQLite機能を使用しませんでした。 しかし、プラスがあります。このコードは、最小限の変更で任意のデータベースで使用できます。
データベースを操作するために、小さなクラスが作成されました。
次のステップでは、センサーの調査とデータベースへの保存を組み合わせます
次に、受信したファイルをホストコンピューターにコピーし、スケジューラにタスクを追加してgetweater.pyを5分ごとに実行し、デバイスから統計を収集します。
ここで、何らかの方法でこのデータを取得する必要があります。 APIを開発します。
/ws.pyは、データベースの最後のエントリを含むhtmlページを返します。
/ws.py?mtd=last-json-string形式のデータベースの最後のエントリ。
/ws.py?mtd=intervalmin=XXmax=YY-json-string形式の最小日付と最大日付の間のレコードの範囲。
/ws.py?mtd=all-json-string形式のすべてのエントリ。
/ws.py?mtd=version-json-string形式のデータベースバージョン。
/sensors.py-センサーのリストを含むHTMLページ。
これを行うために、単純なWebサービスとセンサーエディターを作成します。
これらの2つのスクリプトは、Webサーバーのcgi-binディレクトリ(私の場合、これは/ www / cgi-bin)に配置し、実行可能にし、実行許可を与える必要があります。
chmod -R 755 /www/cgi-bin chmod -R +x /www/cgi-bin
気象データを収集するために別のコンピューターを割り当てたくない、および(または)唯一のサーバーに本格的なWebサーバーをインストールしたくない場合は、このスクリプトをお勧めします。
コマンドラインで実行することにより、CGIスクリプトとシンプルなページをデバッグできるWebサーバーが得られます。 その結果、次のようになります。



すべてのコードは
githubで入手できます。
おわりに
測定されたプロセスを修正するためのシステムを構築する方法-交換プロトコルを構築する方法、送信中の歪みからデータを保護する方法、およびデータベースに保存する方法-の気象データを収集するタスクの例について話しました。 読者は、必要に応じて、このプロジェクトを自分のニーズに適合させることができます。
後でこのデータを使用する方法については、別の記事を書くことができます。たとえば、古い放棄されたAndroidタブレットから観測日記を表示するためのリモートコントロールの作成方法などです。
ご清聴ありがとうございました。おもしろかったと思います。