Pythonロボティクスコース レッスン 44
テーマ.11-3 ネットワークを利用した正確な時刻の取得
Studuino:bitに正確な時刻を設定しよう
チャプター1
このレッスンで学ぶこと
このレッスンでは、「NTP(Network Time Protocol)」という通信プロトコルについて新たに学習します。そして、Studuino:bitのRTC(Real Time Clock)に現在の正確な時刻を設定してデジタル時計を作成します。
チャプター2
インターネットにおける様々な通信プロトコル
このレッスンで学習する「NTP(Network Time Protocol)」は、ネットワーク上に存在しているコンピュータ間で時刻を同期するために開発された通信プロトコルです。みなさんが普段使用しているコンピュータやスマートフォンに正確な時刻が設定されているのもNTPのおかげです。
NTPはテーマ.11-1で紹介したインターネットを構成する階層モデル(DARPAモデル)のアプリケーション層に位置する通信プロトコルです。
ここでは、各層の役割と各層での通信プロトコルについてより詳しく見ていきましょう。
2. 1 4つの階層の役割
まずは、下の図を見て4つの階層の役割についての大枠を理解しておきましょう。
2. 2 各階層の通信プロトコル
ここからは、各階層の通信プロトコルについて詳しく見ていきましょう。
■ アプリケーション層の通信プロトコル
アプリケーション層の通信プロトコルでは、最も利用者に近いソフトウェア(Webブラウザやメーラーなど)間でどのような形式や手順でデータを伝送するのかを定めています。文字コードや画像フォーマット、データの暗号化などもこの層で扱われます。
【 アプリケーション層の通信プロトコル例 】
プロトコル名 | 正式名称 | 説明 |
---|---|---|
HTTP | Hyper Text Transfer Protocol | Webブラウザとサーバーとの通信方法を定めたプロトコル。HTMLの文書の他に画像、音声、映像、JavaScriptで書かれたプログラムなどWebページ内のコンテンツの送受信を行える。 |
SMTP | Simple Mail Transfer Protocol | インターネットを利用して、電子メールを転送するときに使われる通信プロトコル。 |
POP3 | Post Office Protocol | 利用者がメールサーバーに保管されている自分宛の電子メールを取り出すときに使われる通信プロトコル。 |
FTP | File Transfer Protocol | クライアントとサーバーの間でファイルの転送を行うときの通信方法を定めたプロトコル。WebサイトのデータをサーバーにアップロードするときにもFTPが使われる。 |
Telnet | Teletype network | インターネットを利用して、遠隔地にあるサーバーやルーターなどの情報端末を制御するときに使われる通信プロトコル。すべてのデータ通信は暗号化されていないテキスト(Plane text)で行われるため、安全性に問題があり、代わりとしてSSHが使われることも多い。 |
SSH | Secure Shell | 暗号化や認証の技術を利用して、遠隔地にある情報端末と安全に通信するためのプロトコル。データが暗号化されずに伝送されるTelnetに代わり使用されている。 |
NTP | Network Time Protocol | ネットワーク上に存在しているコンピュータ間で時刻を同期するために開発された通信プロトコル。時刻情報を配信するNTPサーバーと時刻を合わせるクライアント間の通信方法を定めている。 |
SSL/TLS | Secure Sockets Layer/Transport Layer Security | ネットワークにおいてセキュリティを要求される通信を行うためのプロトコル。なりすましの排除を行うために発行された証明書の認証や通信の暗号化を行う。現在はSSLの後継であるTLSが使われているが、名残りでSSLと呼ばれることもある。 |
DNS | Domain Name System | ドメイン名をIPアドレスに変換する仕組みを提供するDNSサーバーと通信するためのプロトコル。 |
テーマ.11-1やテーマ.11-2では、HTTPの仕様を詳しく見ていきましたが、このレッスンの後半ではNTPの仕様を掘り下げて説明します。
■ トランスポート層の通信プロトコル
インターネットにけるトランスポート層に位置する通信プロトコルは、「TCP」と「UDP」の2つしかありません。TCPとUDPの違いはデータ通信の信頼性と高速性にあります。
- TCP(Transmission Control Protocol)
TCPは、「コネクション型通信」と呼ばれ、データのやり取りを始める前に相手との間で仮想的な通信経路を確立します。
また、TCPは相手から受信したデータに不足がないかをチェックして、必要であれば再送要求を行うため、欠損なくデータを確実に送り届けることができます。 - UDP(User Datagram Protocol)
一方でUDPは、「コネクションレス型通信」と呼ばれ、TCPのように相手との間で通信経路を確保せずに、データのやり取りを始めます。
不足分のチェックや再送要求も行われないため、届いたデータが欠損している場合もありますが、TCPよりも高速にデータを伝送できるメリットがあります。そのため、リアルタイムに映像や音声を送るときのように、データ量が多くかつ多少データに欠損があっても問題にならない場合にUDPが利用されています。
また、もうひとつトランスポート層で重要なものが「ポート番号」です。IPアドレスによってコンピュータに届けられたデータが、そのコンピュータ上で動作するどのプログラム宛てに届けられたものなのかを判断するために使われます。
ポート番号には、16bitで表される「0~65535」の数値が使われます。これらの数値は次の3種類のポートによって範囲が分けられています。
ポート番号の範囲 | ポートの種類 | 用途 |
---|---|---|
0~1023 | ウェルノウンポート | HTTPやSMTPなどアプリケーション層の通信プロトコルに割り当てられているポート。 |
1024~49151 | 登録ポート | 特定のサービス用に割り当てられているポート。ネットワークを利用する他のソフトウェアと衝突しないように管理されている。 |
49152~65535 | ダイナミック/プライベートポート | 用途が決まっていないポート。どのようなソフトウェアや通信プロトコルからも自由に利用することができる。 |
ウェルノウンポート(Well-known ports)の番号と登録ポート(Registered ports)の番号は、「IANA(Internet Assigned Numbers Authority)」
という機関で管理されています。例として、代表的な通信プロトコルに割り当てられているポート番号を以下の表に示します。
通信プロトコル | ポート番号 | TCP/UDP |
---|---|---|
FTP | 20(データ転送ポート)、21(コントロールポート) | TCP |
SSH | 22 | TCP |
telnet | 23 | TCP |
SMTP | 25/587 | TCP |
DNS | 53 | TCP/UDP |
HTTP | 80 | TCP |
POP3 | 110 | TCP |
NTP | 123 | UDP |
SSL/TLS | 443 | TCP |
TCPで通信している場合、現在どのポートを利用して、誰と接続しているのかを次の方法で簡単に調べることがでいます。
- 使用しているPCのOSがWindowsなら「コマンドプロンプト」を、Mac OSなら「ターミナル」を開きます。
- 「netstat -n」と入力して実行します。「netstat」はTCPの通信状態を調べるためのコマンドです。オプションに「-n」を指定するとIPアドレスやポート番号が全て数値で表示されるようになります。
■ インターネット層の通信プロトコル
インターネット層には、巨大なネットワークのなかから、宛先のコンピュータまでデータを届ける役割があります。そのためには、インターネットに接続された世界中のコンピュータのなかから宛先のコンピュータを特定できる仕組みが必要です。
例えば、住所表記は広い世界のなかで唯一その場所だけを表しています。同じようにインターネットの世界でもコンピュータの住所を表すものがあります。それが「IPアドレス」です。IPアドレスは他のコンピュータと重複しないように固有の番号が割り振られています。インターネットを流れるデータには、必ず送信元と宛先のIPアドレスが付与されています。
<IPアドレスの調べ方>
自分のコンピュータに割り当てられたIPアドレスを確認する方法があります。Windowsの場合は「コマンド プロンプト」を、Macの場合は「ターミナル」を起動して、「ipconfig」コマンドを実行します。
上の例では、「192.128.12.8」がこのコンピュータに割り当てられたIPアドレスになります。現在利用されているインターネット層のプロトコルには「IPv4」と「IPv6」の2つのバージョンがあり、上のIPアドレスは「IPv4」で使われています。
IPv4のIPアドレスは32ビットのデータ量で8ビットずつに分けて10進数で表記します。
この表記方法では、最大でも4,294,967,296(232)個のコンピュータに固有のIPアドレスを割り当てることができます。しかしながら、昨今の世界規模でのインターネットの急速な拡大によって、アドレスの数に余裕がなくなってしまいました。そこで現在、新しい通信プロトコルとして登場した「IPv6」への対応が進められています。IPv6で以下の128ビットのアドレスが使われています。
IPv6には、上記のアドレス数不足の解決以外にも、インターネットの通信速度が改善するなどのメリットもあります。ただし、「IPv4」と「IPv6」には直接の互換性がないため、IPv6のメリットを最大限に活かすためには、WebサイトやWebサービス自体がIPv6に対応している必要があります。
<IPアドレスの種類>
IPアドレスには、「グローバルIPアドレス」と「プライベートIPアドレス(またはローカルIPアドレス)」の2種類があります。
- プライベートIPアドレス
家庭内や会社内などLAN内で利用されるIPアドレスです。このアドレスは、同じLAN内では重複しませんが、異なるLAN間では重複しても構いません。 - グローバルIPアドレス
インターネット(WAN)で利用されるIPアドレスです。このアドレスはインターネットワーク内で重複してはなりません。
LAN内のコンピュータが外のネットワークにあるサーバーと通信するときは、NAT(Network Address Translation)という技術を用いて、プライベートIPアドレスからグローバルIPアドレスに変換されてデータが伝送されます。
<ドメイン名からIPアドレスへの変換>
ここまでで、インターネットで特定のコンピュータと通信するためには、IPアドレスが必要だということを確認しました。しかし、数字の並びであるIPアドレスは人にとっては覚えにくいものです。そこで、IPアドレスの代わりに利用できる「ドメイン名」が考えられました。
IPアドレスとドメイン名の対応づけを管理するシステムを「DNS(Domain Name System)」といいます。ブラウザにURLを指定してサイトへアクセスするときは、最初にDNSサーバーにIPアドレスの問合せを行っています。
実際に「nslookup」コマンドでDNSサーバーへIPアドレスを問い合わせることができます。Windowsの場合は「コマンド プロンプト」を、Macの場合は「ターミナル」を起動します。ArtecRobo2.0の開発元のアーテック社のドメイン「www.artec-kk.co.jp」を指定して、このコマンドを実行してみましょう。
■ ネットワークインターフェース層の通信プロトコル
ネットワークインターフェース層の通信プロトコルには、LANケーブルや無線LANなどで接続された隣接する他の情報機器との間で、データを伝送する役割があります。
OSI参照モデルでは、「物理層」と「データリンク層」とに分けられているように、電圧や電波の信号を扱うハードウェアと、そのハードウェア上でのデータ伝送の仕組みが定義されています。
- データリンク層
LANケーブルなどで直接接続されている機器との間でデータの受け渡しを行うためのプロトコル - 物理層
LANケーブルや無線LANなど物理的な接続や電圧や電波の信号への変換を行うためのハードウェア
<データの宛先を表すMACアドレス>
パソコンやモバイル端末、ルーターなどの情報機器には、有線LANや無線LANでネットワークに接続するために必要な「ネットワークアダプタ」という部品が取り付けられています。
ネットワークアダプタには、製造段階で「MACアドレス」と呼ばれる識別番号が付けられています。MACアドレスは、IPアドレスと同じように全世界で同じ番号のないように割り当てられています。
MACアドレスは、全体で48ビットの数値の並びになっており、前半の24ビットがベンダー(製造元)を表す番号で、後半の24ビットが各ベンダー内で製造したネットワークアダプタに割り当てられた番号となっています。
インターネット層では、IPアドレスによってデータの送り先を指定していましたが、ネットワークインターフェース層では、このMACアドレスにより隣接するどの機器へデータを送るのかを指定します。
IPアドレスとMACアドレスの違いを荷物の配送に例えるなら、IPアドレスは荷物の送り先の住所を表しており、MACアドレスは荷物を運ぶ過程で経由する場所の名前というように理解できます。
そのため、データ本体に付与される宛先のMACアドレスは、中継する他の情報機器を通過するたびに変わります。
<MACアドレスの調べ方>
自分のコンピュータに取り付けられたネットワークアダプタのMACアドレスを確認するときは、IPアドレスを調べたときと同様に、「ipconfig」コマンドを使います。Windowsの場合は「コマンド プロンプト」を立ち上げて、「/all」のオプションを付けてこのコマンドを実行してみましょう。
「物理アドレス」と表記されているものがMACアドレスです。MacOSの場合は「ターミナル」を起動して、「en0」オプションを付けて「ipconfig」コマンドを実行します。表示された一覧のなかにある「ether」の所でMACアドレスを確認することができます。
<MACアドレスの問い合わせ>
上で説明したように、有線LANや無線LANでデータを伝送するには、宛先となるMACアドレスが必要です。しかし、コンピュータは送り先のIPアドレスを知っていても、MACアドレスを知らない場合があります。そのときにMACアドレスの問い合わせに使われるプロトコルが「ARP(Address Resolution Protocol)」です。
ARPでは、ネットワークハブという機器に接続されたすべてのコンピュータに対してMACアドレスの問い合わせを行います。このように、ネットワークに接続するすべての機器に同時にデータを送信することを「ブロードキャスト」といいます。そして、該当するIPアドレスをもつコンピュータからの応答があり、MACアドレスを知ることができます。
<有線LANと無線LANの規格>
有線LANの通信方式や使用されるケーブルは、「イーサネット(Ethernet)」という規格で定められています。イーサネットは、OSI参照モデルのデータリンク層と物理層の両方のプロトコルを定めています。データリンク層のプロトコルはどの規格でもほとんど共通していますが、物理層にあたるケーブルには様々な種類があります。
イーサネットの規格名は次のような数字とアルファベットの並びで表されています。
- データの伝送速度
通信時のデータの伝送速度を表す数値。単位は「bps(bits per second)」。1000Mbpsの場合は1秒あたりに1000メガビットのデータ量が伝送できる。 - 変調方式
ベースになる信号から周波数や振幅などを変化させて(変調して)信号を伝送するときの方式。「ベースバンド伝送」の場合は信号を変調せずにそのまま伝送する。 - ケーブルの種類
銅線を使用したツイストペアケーブルと光ファイバーを使用したケーブルが主に用いられます。また、これらのケーブルは伝送速度によってカテゴリ分けされており、上位カテゴリのケーブルほど伝送速度が高くなります。
【 イーサネットの規格例 】
規格名 | 説明 |
---|---|
100BASE-T | ツイストペアケーブルを使用して、信号を変調せずに100Mbpsでデータを伝送する。 |
1000BASE-T | ツイストペアケーブルをを使用して、信号を変調せずに1000Mbpsでデータを伝送する。 |
1000BASE-LX | 光ファイバーケーブルを使用して、信号を変調せずに1000Mbpsでデータを伝送する。 |
10GBASE-T | ツイストペアケーブルを使用して、信号を変調せずに10Gbpsでデータを伝送する。 |
無線LANもイーサネットと同様にいくつかの規格があり、データ伝送に使用する周波数帯や伝送速度に違いがあります。
【 無線LANの規格 】
規格名 | 説明 |
---|---|
IEEE802.11 | 無線LANの基本となる規格。 |
IEEE802.11a | 5GHz帯を使用して、最大54Mbpsでデータを伝送する。 |
IEEE802.11b | 2.4GHz帯を使用して、最大11Mbpsでデータを伝送する。 |
IEEE802.11g | 2.4GHz帯を使用して、最大54Mbpsでデータを伝送する。 |
IEEE802.11n | 複数のアンテナを束ねることにより、2.4GHz帯または5GHz帯を使用して、最大で600Mbpsでデータを伝送する。 |
IEEE902.11ac | IEEE802.11nよりも多くのアンテナを束ねることにより、5GHz帯を使用して、最大6.9Gbpsでデータを伝送する。 |
チャプター3
デジタル時計の組立て
超音波距離センサで人を感知して現在の時刻を表示するデジタル時計を組立てましょう。
3. 1 組み立てに必要なパーツ
【 パーツ一覧 】
- Studuino:bit×1
- ロボット拡張ユニット×1
- 電池ボックス×1
- 超音波距離センサー×1
- センサー接続コード(3芯15cm)×1
- ブロック基本四角(赤)×2
- ブロック三角(グレー)×2
- ブロックハーフA(グレー)×4
- ブロックハーフC(白)×9
- ブロックハーフD(白)×2
【 アーテックブロックの形状 】
3. 2 組立説明書
以下のリンク先から組立説明書を確認しましょう。
チャプター4
インターネットを利用して時刻のズレを補正する方法
みなさんは、普段使っている腕時計や目覚まし時計の時刻が実際の時刻からズレていたために、待ち合わせに遅刻してしまったり、大好きなテレビ番組の開始を見逃してしまったという苦い経験はありませんか?そんな失敗をしなくて済むように、これからプログラムを作成するデジタル時計には、インターネットを利用して時刻のズレを自動的に補正する機能を付けてみましょう。
4. 1 インターネットに接続されたコンピュータの時刻を同期する通信プロトコル
ここでは、時刻のズレを補正するために「NTP(Netowork Time Protocol)」を利用します。NTPは、極めて高い精度で正確に時間を刻む「原子時計※」に基づいた「協定世界時」という時刻に、ネットワーク内に存在しているコンピュータの時計を同期させるための通信プロトコルです。コンピュータは、NTPサーバーから取得した時刻とローカルな時計の時刻を比較することでズレを補正します。
※高精度な原子時計では、10-15(3000万年に1秒)ほどの極小な誤差で時間を刻みます。
まずは、NTPの仕組みから確認していきましょう。
■ NTPの階層構造
NTPのネットワークは下図のように、最大で15層までの階層構造になっています。1つの層のことを「Stratum(ストラタム)」といい、「Stratum 0」を原子時計として、下位の層にあるNTPサーバーは、より上位の層にあるNTPサーバーへ問い合わせを行い、時刻を同期する仕組みになっています。また、同じ階層にあるNTPサーバーどうしで時刻情報を交換し、相互に時間を調整することもあります。
■ 時刻の補正方法
NTPでは、クライアントからサーバーへリクエストを送ると、サーバーがそのリクエストを受け取った時刻(T2)とレスポンスを発信した時刻(T3)を返します。これらの時刻と、クライアント側がリクエストを発信した時刻(T1)とレスポンスを受信した時刻(T4)を使い、次の計算式でNTPサーバーとの時差(offset)や往復の通信にかかった時間(rtt)を計算します。
実際に上の計算式へ簡単な例を当てはめると次のように計算できます。
※ 上の例では、各時刻の時と分を表す数字はすべて同じです。
この例では、NTPサーバーとの時差が16秒で、往復の通信にかかった時間が8秒となりました。もしも、リクエストとレスポンスの通信にかかった時間が等しい(各4秒)場合は、求めた時差(16秒)は正確な情報になります。しかし、そうでない場合は最大で往復時間の半分だけ取得した時差に誤差(16±4秒)がある可能性があります。
こうした理由や、NTPサーバー自体にも時刻のズレがある可能性から、実際には複数のNTPサーバーから繰り返し時刻情報を取得し、これよりも複雑な処理によって時刻のズレを補正していきます。
このレッスンではプログラムを簡単にするために、1つのNTPサーバーからしか時刻情報を取得しません。そのため、「通信のリクエストとレスポンスにかかった時間が等しいこと」と「NTPサーバーの時刻が正確であること」を前提条件として以下の簡単な計算式でStuduino:bit内部のリアルタイムクロック(RTC)の時刻を補正します。
補正後のRTCの時刻 = 補正前のRTCの時刻 + offset
■ NTPのデータフォーマット
NTPでは、以下のようにリクエストやレスポンスのデータフォーマットが定められています。
※ 以下は最低限必要な情報だけを伝送するためのフォーマットになっており、これをさらに拡張したフォーマットも存在します。
※ ある出来事が発生した日付・時刻を示すものを「タイムスタンプ(timestamp)」といいます。
このフォーマットでは、複数の項目が48バイト(384ビット)のデータとしてまとめられています。それでは、それぞれの項目が表す内容をひとつずつ確認していきましょう。
- LI(Leap Indicator):2ビット
うるう秒指示子。原子時計は極めて高い精度で時を刻んでいますが、一方で地球の自転周期(理想は1周期が24時間)は原子時計ほど厳密ではなく、これら2つの間には時間のズレが生じます。そのため、数年に一度このズレずれを補正するために挿入される時間が「うるう秒」です。うるう秒指示子では、その日の最後の1分においてうるう秒を挿入するかどうかを示しています。時刻をサーバーから取得する場合は「0」を指定します。
LI0(00): 時刻は通常通り LI1(01): 最後の1分を61秒とする LI2(10): 最後の1分を59秒とする LI3(11): 配給されている時刻が正確でないため使うべきでない
- VN(Version Number):3ビット
NTPのバージョン情報。バージョン4であるSNTP(Simple Network Time Protocol)は、上で少し説明したようなNTPの複雑な仕様を簡易化して、クライアントがサーバーと時刻を同期する機能に特化しています。このレッスンでもSNTPを利用します。
VN1(001):バージョン1 VN2(010):バージョン2 VN3(011):バージョン3(通常のNTPサーバー) VN4(100):バージョン4(通常のSNTPサーバー)
- MODE:3ビット
動作モードを表す番号。対称アクティブ/パッシブモードは同じ階層のNTPサーバーどうしで時刻情報を交換し、時刻を調整するモード。ブロードキャストは、サーバーが一方的に時刻情報を送信するモード。時刻をサーバーから取得する場合は「3」のクライアントを指定します。
MODE0(000):予約 MODE1(001):対称アクティブモード ※NTPのみ MODE2(010):対称パッシブモード ※NTPのみ MODE3(011):クライアント MODE4(100):サーバー MODE5(101):ブロードキャスト ※NTPのみ MODE6(110):予約(NTP制御メッセージ用) MODE7(111):予約(プライベート用)
- 階層:8ビット
自身が位置している階層(Stratum)。SNTPのクライアントからリクエストを送る場合は0を指定。
0:不明または有効でない階層。 1:Stratum1 2~15:Stratum2~Stratum15 16~255:予約
- ポーリング間隔:8ビット
上位のNTPサーバーへリクエストを送信して時刻同期を行うときの時間の間隔。2の累乗で表される。SNTPのクライアントからリクエストを送る場合は0を指定。
00000000:0秒間隔 00000001:1秒間隔 00000010:2秒間隔 00000011:4秒間隔 . . .
- 精度:8ビット
ローカル時計の精度を表す8ビットの符号付(正負のある)整数。2の累乗で表される。SNTPのクライアントからリクエストを送る場合は0を指定。 - ルート遅延:32ビット
往復遅延の合計値。上位16ビットが整数部で下位16ビットが小数点以下を表す。SNTPのクライアントからリクエストを送る場合は0を指定。 - ルート分散:32ビット
NTPサーバーが上記のNTPサーバーと通信を行う構成であった場合に、その誤差を示すもの。上位16ビットが整数部で下位16ビットが小数点以下を表す。SNTPのクライアントからリクエストを送る場合は0を指定。 - 参照識別子:32ビット
NTPサーバーの時刻の参照元を示す文字列。SNTPのクライアントからリクエストを送る場合は0を指定。 - 参照タイムスタンプ:64ビット
ローカル時計が最後に設定・修正された時刻。上位32ビットが整数部で下位32ビットが小数点以下を表す。SNTPのクライアントからリクエストを送る場合は0を指定。 - 開始タイムスタンプ:64ビット
クライアントがサーバーへリクエストを送信した時刻。上位32ビットが整数部で下位32ビットが小数点以下を表す。SNTPのクライアントからリクエストを送る場合は0を指定。 - 受信タイムスタンプ:64ビット
サーバーへリクエストが到着した時刻。上位32ビットが整数部で下位32ビットが小数点以下を表す。SNTPのクライアントからリクエストを送る場合は0を指定。 - 送信タイムスタンプ:64ビット
サーバーからクライアントへレスポンスを発信した時刻。上位32ビットが整数部で下位32ビットが小数点以下を表す。SNTPのクライアントからリクエストを送る場合は0を指定。
今回はNTPの簡易版である「SNTP」を利用します。上で説明したように、SNTPの場合はほとんどの項目に「0」を指定することになります。そのため、意識するのは「LI」「VN」「MODE」の3つだけになり、以下のように指定します。
- LI:00 ⇒ 0 ⇒ サーバーから時刻を取得するので「0」
- VN:100 ⇒ 4 ⇒ バージョン4(SNTP)
- MODE:011 ⇒ 3 ⇒ クライアント
また、ちょうどこれら3つの項目を合わせたデータ量が1バイトになっているため、まとめて16進数で表すと「0x23」になります。これは、あとのプログラム作成時に使いますので覚えておきましょう。
4. 2 協定世界時と標準時間帯について
NTPで取得する時刻情報は「協定世界時」であることを説明しましたが、協定世界時とはいったい何でしょうか?ここでは、ある地点の時刻がどのようにして定められているのかを理解するところから見ていきましょう。
■ 時刻の定め方
元々ある地域・地点の時刻は太陽が天球上の最も高い位置に昇ったときを正午とする「太陽時」で決められていました。この太陽時は観測地点によって異なり、ある地点の経度と別の地点の経度が1度違うと4分、15度違うと1時間の時差が発生します。
例えば、日本の沖縄の那覇(東経127度)と北海道の札幌(東経141度)では経度の差が14度となり、太陽時では1時間近くの時差があることになります。
しかし実際には、日本ではどの都道府県に住んでいても同じ時刻が利用されています。これは離れた都市との間に時差があると不便なことが多いからです。もし、新年を迎えるときのカウントダウンが全国でバラバラだとすると困りますよね。
このように、より広い地域で利用できる共通の時刻を「標準時」といいます。また、同じ標準時をもつ地域帯のことを「標準時間帯(タイムゾーン)」といいます。日本の標準時間帯は1つしかありませんが、アメリカ合衆国やロシアなど、国土の広い国では複数の標準時間帯を設けているところもあります。
そして、世界各国の標準時は、ある基準となる時計が刻む時間によって決められています。以前はイギリスのグリニッジ天文台における太陽時である「グリニッジ標準時」をもとに定められていました。現在は原子時計で刻まれる時間である「国際原子時(TAI)」に由来する「協定世界時(UTC)※」が基準になっています。
現在利用されている標準時は、協定世界時との時差が1時間または30分の単位となる経度の時刻となっていることが多く、次のフォーマットで表します。
UTC+9:協定世界時より9時間進んでいる標準時 UTC-3:30:協定世界時より3時間30分遅れている標準時
【 世界の標準時間帯(タイムゾーン) 】
日本の標準時は「UTC+9」です。そのため、NTPサーバーから取得した協定世界時を9時間進めることで現地時刻が得られます。その他の地域の標準時を設定したい場合は、以下の標準時間帯をまとめた表を参考にしてください。
【 世界各国の標準時の一覧 】
参考:wikipedia「時間帯(標準時)」
|標準時|標準時間帯(タイムゾーン)|
|:—|:—|:—|
|UTC+14|キリバス(ライン諸島)|
|UTC+13|キリバス(フェニックス諸島)、サモア、トンガ|
|UTC+12|ニュージーランド、キリバス(ギルバート諸島)、ツバル、ナウル、フィジー、マーシャル諸島、ロシア(カムチャッカ時間)|
|UTC+11|ソロモン諸島、バヌアツ、ロシア(マガダン時間)|
|UTC+10|オーストラリア(東部)、パプアニューギニア、ロシア(ウラジオストク時間)|
|UTC+9:30|オーストラリア(中部)|
|UTC+9|日本、韓国、インドネシア(東部)、ロシア(ヤーツク時間)|
|UTC+8|オーストラリア(西部)、シンガポール、中華人民共和国、香港、マカオ、台湾、フィリピン、モンゴル、マレーシア、ロシア(イルクーツク時間)|
|UTC+7|インドネシア(西部)、カンボジア、タイ、ベトナム、ラオス、ロシア(クラスノヤルスク時間)|
|UTC+6:30|ミャンマー|
|UTC+6|カザフスタン(東部)、バングラデシュ、ギルギスタン、ブータン、ロシア(オムスク時間)|
|UTC+5:45|ネパール|
|UTC+5:30|インド、スリランカ|
|UTC+5|ウズベキスタン、カザフスタン(西部)、タジキスタン、トルクメニスタン、パキスタン、モルディブ、ロシア(エカテリンブルク時間)|
|UTC+4:30|アフガニスタン|
|UTC+4|アゼルバイジャン、アラブ首長国連邦、アルメニア、オマーン、ジョージア、セーシェル、モーリシャス、ロシア(サマラ時間)|
|UTC+3:30|イラン|
|UTC+3|イエメン、イラク、ウガンダ、エチオピア、エリトリア、カタール、クウェート、ケニア、コモロ、サウジアラビア、ジブチ、ソマリア、タンザニア、トルコ、バーレーン、ベラルーシ、マダガスカル、南スーダン、ロシア(モスクワ時間)|
|UTC+2|イスラエル、ウクライナ、コンゴ民主共和国(東部集、西カサイ州、東カサイ州、カタンガ州、北キブ州、マニエマ州、南キブ州)、ザンビア、シリア、ジンバブエ、スーダン、スワジランド、パレスチナ、東ヨーロッパ時間(エストニア、キプロス、ギリシャ、フィンランド、ブルガリア、ラトビア、リトアニア、ルーマニア)、エジプト、スーダン、ブルンジ、ボツワナ、マラウイ、南アフリカ共和国、モザンビーク、モルドバ、ナミビア、ヨルダン、リビア、ルワンダ、レソト、レバノン、ロシア(カリーニングラード時間)|
|UTC+1|アンゴラ、ガボン、カメルーン、コンゴ共和国、コンゴ民主共和国(キンシャサ、赤道州、バ・コンゴ州、バンドゥンドゥ州)、赤道ギニア、チャド、中央アフリカ、中央ヨーロッパ時間(アルジェリア、アルバニア、アンドラ、イタリア、オーストリア、オランダ、クロアチア、サンマリノ、スイス、スウェーデン、スペイン、スロバキア、スロベニア、セルビア、チェコ、チュニジア、デンマーク、ドイツ、ノルウェー、バチカン、ハンガリー、フランス、ベベルギー、ボスニア・ヘルツェゴビナ、ポーランド、マケドニア、マルタ、モナコ、モンテネグロ、リヒテンシュタイン、ルクセンブルク)、ナイジェリア、西サハラ、ニジェール、ベナン、モロッコ|
|UTC|コートジボワール、ガンビア、ガーナ、ギニア、ギニアブサウ、デンマーク領グリーンランド(北東部)、サントメ・プリンシペ、シエラレオネ、セネガル、セントヘレナ、トーゴ、西ヨーロッパ時間(アイスランド、アイルランド、イギリス、スペイン、フェロー諸島、ポルトガル)、ブーベ島、ブルキナファソ、マリ、モーリタニア、リベリア|
|UTC-1|カーボベルデ、デンマーク領グリーンランド(東部)、ポルトガル|
|UTC-2|サウスジョージア・サウスサンドウィッチ諸島、ブラジル(海洋諸島)|
|UTC-3|アルゼンチン、ウルグアイ、デンマーク領グリーンランド(南海岸、南西海岸)、サンピエール・ミクロン、スリナム、チリ(マガジャネス・イ・デ・ラ・アンタルティカ・チレーナ州)、ブラジル(アマパ州、アラゴアス州、エスピリトサント州、ゴイアス州、サンタカタリーナ州、サンパウロ州、セアラ州、セルジッペ州、トカンチンス州、バイーア州、パラ州(東部)、パライバ州、パラナ州、ピアウイ州、ブラジリア連邦直轄区、ペルナンブーコ州、マラニョン州、ミナスジェライス州、リオグランデドスル州、リオグランデドノルチ州、リオデジャネイロ州、フランス領ギアナ|
|UTC-3:30|カナダ(ニューファンドランド・ラブラドール州)|
|UTC-4|オランダ領アルバ、イギリス領アンギラ、アンティグア・バーブーダ、イギリス領ヴァージン諸島、オランダ領アンティル、ガイアナ、フランス領グアドループ、デンマーク領グリーンランド(北西部)、グレナダ、セントクリストファー・ネイビス、セントビンセントおよびグレナディーン諸島、セント・ルシア、ベネズエラ、アメリカ・カナダ大西洋標準時(アメリカ領ヴァージン諸島、カナダ(ケベック州、ニューファンドランド・ラブラドール州、ニューブランズウィック州、ノバスコシア州、プリンスエドワードアイランド州)バミューダ諸島、プエルトリコ)、チリ、ドミニカ国、ドミニカ共和国、トリニダード・トバゴ、パラグアイ、バルバドス、イギリス領フォークランド諸島、ブラジル(アマゾナス州、パラ州、マトグロッソスル州、ロライマ州、ロンドニア州)、ボリビア、フランス領マルティニーク、イギリス領モントセラト|
|UTC-5|エクアドル、キューバ、イギリス領ケイマン諸島、コロンビア、ジャマイカ、イギリス領タークス諸島・カイコス諸島、アメリカ・カナダ東部標準時(アメリカ合衆国(インディアナ州、ウェストバージニア州、オハイオ州、ケンタッキー州、コネチカット州、サウスカロライナ州、ジョージア州、デラウェア州、ニュージャージー州、ニューハンプシャー州、ニューヨーク州、ノースカロライナ州、バージニア州、バーモント州、フロリダ州、ペンシルベニア州、マサチューセッツ州、ミシガン州、メイン州、メリーランド州、ロードアイランド州、ワシントンD.C.)、カナダ(オンタリオ州、ヌナブト準州、ケベック州))メキシコ(キンタナ・ロー州)、ハイチ、パナマ、バハマ、ブラジル(アクレ州、アマゾナス州)、ペルー|
|UTC-6|エクアドル(ガラパゴス諸島)、エルサルバドル、グアテマラ、コスタリカ、アメリカ・カナダ中部標準時(アメリカ合衆国(アーカンソー州、アイオワ州、アラバマ州、イリノイ州、インディアナ州、ウィスコンシン州、オクラホマ州、カンザス州、ケンタッキー州、サウスダコタ州、テキサス州、テネシー州、ネブラスカ州、ノースダコタ州、フロリダ州、ミシガン州、ミシシッピ州、ミズーリ州、ミネソタ州、ルイジアナ州)、カナダ(オンタリオ州、サスカチュワン州、ヌナブト準州、マトニバ州))、チリ(イースター島)、ニカラグア、ベリーズ、ホンジュラス|
|UTC-7|アメリカ・カナダ山岳部標準時(アメリカ合衆国(アイダホ州、アリゾナ州、オクラホマ州、オレゴン州、カンザス州、コロラド州、サウスダコタ州、テキサス州、ニューメキシコ州、ネバダ州、ネブラスカ州、ノースダコタ州、モンタナ州、ユタ州、ワイオミング州)、カナダ(アルバータ州、ブリティッシュコロンビア州、ノースウェスト準州、サスカチュワン州、ヌナブト準州、ユーコン準州))、メキシコ(シナロア州、ソノラ州、ナヤリット州、チワワ州、バハ・カリフォリニア・スル州)|
|UTC-8|アメリカ・カナダ太平洋標準時(アメリカ合衆国(アイダホ州、オレゴン州、カルフォルニア州、ネバダ州、ワシントン州)、カナダ(ブリティッシュコロンビア州)、イギリス領ピトケアン諸島)、メキシコ(バハ・カリフォルニア州)|
|UTC-9|アメリカ合衆国(アラスカ州)、フランス領ポリネシア(ガンビエ諸島)|
|UTC-9:30|フランス領ポリネシア(マルケサス諸島)|
|UTC-10|クック諸島、アメリカ合衆国領ジョンストン島、アメリカ合衆国(ハワイ州)、フランス領ポリネシア(ソシエテ諸島、ツアモツ諸島、トゥブアイ諸島)|
|UTC-11|アメリカ領サモア、ニウエ、アメリカ合衆国領ミッドウェイ諸島|
|UTC-12|アメリカ合衆国領ベーカー島、アメリカ合衆国ハウランド島|
チャプター5
デジタル時計のプログラムの作成
テーマ.11-1やテーマ.11-2では、urequests
モジュールを使ってHTTPでの通信を行いましたが、NTPではこのモジュールは使えません。そこで、代わりにuscoket
モジュールを使います。このモジュールは、次に説明する「ソケット通信」を行うための機能を提供しています。
5. 1 ソケット通信とは
ソケットは簡単に言うと、プログラムからTCPやUDPを利用したネットワーク通信を行うために用意された接続口です。HTTPやNTPなどアプリケーション層の通信プロトコルも実はソケットを利用しています。
これだけでは少し分かりにくいので、ソケットの役割を郵便ポストに例えて考えてみます。郵便ポストに手紙を投函すると、郵便局が間をつなぎ、相手の家のポストまで手紙を届けてくれます。このとき手紙の送り手や受け取り手は、その手紙がどのような工程を経て届けられたのかは知らなくても構いません。
ソケットはソフトウェアにとって郵便ポストと似たような存在です。ソフトウェアはソケットに送信したいデータを渡すだけで、あとはソケットが相手先のソケットまでそれを届けてくれます。ソフトウェアからすると、トランスポート層(TCP/UDP)やインターネット層(IP)のことを考える必要がなく、簡単にインターネットを利用したデータ伝送を行うことができます。
ここからは実際に、ソケットを利用してNTPサーバーと通信を行うプログラムを書いていきましょう。
5. 2 時刻のズレを補正するプログラムの作成
今回利用するのは、日本の国立研究開発法人情報通信研究機構(NICT)が運用しているNTPサーバーです。このNTPサーバーから取得した時刻情報を使い、Studuino:bitのリアルタイムクロック(RTC)の時刻を補正します。
日本標準時(JST)グループのWebサイト
Japan Standard Time (JST) Group(英語版Webサイト)
NICTが運用しているNTPサーバーのドメイン名は以下の通りです。
ntp.nict.jp
このNTPサーバーのStratumは「1」です。利用にあたっては、ポーリング間隔が1時間平均で20回(1日の合計が480回)を超えないように規約で定められています。
また、NTPサーバーから返される時刻(UTC)は1900年1月1日から現在までに経過した時間を「秒」で表したものです。この点にも注意して、次の順番でプログラムを作成しましょう。
■ Wi-Fiへの接続
NTPサーバーから時刻情報を取得してStuduino:bitのRTCの時刻を補正するコードを、correct_time()
という名前の関数にまとめます。まずは、この関数の先頭にWi-Fiに接続する処理を書きましょう。
from mywifi import MyWifi # テーマ.11-1で作成したモジュールからクラスをインポート
SSID = "SSID" # 教室のWi-FiのSSID
PASSWORD = "PASSWORD" # 教室のWi-Fiのパスワード
wifi = MyWifi() # Wi-Fi接続用オブジェクトの作成
def correct_time():
if not wifi.isconnected(): # Wi-Fiに未接続の場合のみ接続を試みる
if not wifi.connect(SSID, PASSWORD):
return
■ ソケット通信に必要なアドレス情報の取得
次に、usocket
モジュールをインポートし、このモジュールのgetaddrinfo()
関数を利用して、NTPサーバーのドメイン名からソケット通信に必要なアドレス情報へ変換します。
getaddrinfo(ホスト名/ドメイン名, ポート番号)
このgetaddrinfo()
関数の返り値は以下の5つの要素を持つタプルが入ったリストです。
[(family, type, proto, canonname, sockaddr)]
family
:アドレスファミリ
アドレスファミリを表す数値。「usocket.AF_INET
(2)」の場合はIPv4、「usocket.AF_INET6
(10)」の場合はIPv6をそれぞれ表します。type
:ソケットタイプ
ソケットタイプを表す数値。usocket.SOCK_STREAM
(1)の場合はTCPを利用したストリームソケットを表し、「usocket.SOCK_DGRAM
(2)」の場合はUDPを利用したデータグラムソケットを表します。proto
:プロトコル番号
IPを利用した通信において、上位のプロトコルが何であるかを表す番号。TCPの場合は「6」でUDPの場合は「17」などこの番号はIANAで定められています。MicroPythonではほとんどの場合、この番号を指定する必要はありません。canonname
:カノニカル名(Canonical Name)
ドメインの正式名。sockaddr
:
ソケット通信用のアドレス情報。(IPアドレス, ポート番号)
のタプル。
ここでは、変換した情報の中からsockaddr
のみを使用します。以下のコードを書いてターミナルから関数を実行し、ドメイン名からアドレス情報へ変換できていることを確認しましょう。
追加【1行目、6行目、14行目~16行目】
import usocket # usocketモジュールを新たにインポート
from mywifi import MyWifi
SSID = "SSID"
PASSWORD = "PASSWORD"
NTP_SERVER = "ntp.nict.jp" # NTPサーバーのドメイン名
wifi = MyWifi()
def correct_time():
if not wifi.isconnected():
if not wifi.connect(SSID, PASSWORD):
return
addrinfo = usocket.getaddrinfo(NTP_SERVER, 123) # ドメイン名からアドレス情報を取得。NTPは123番ポートを使用。
sockaddr = addrinfo[0][-1] # 「-1」を指定することで末尾の要素を取得。
print(sockaddr) # 変換後の情報を表示
(実行結果)
>>> correct_time() . . . Connected. ('61.205.120.130', 123)
■ ソケットの作成とコネクションの確立
ソケットを利用した通信では、始めにコネクションを確立して、そのあとでデータ伝送を行います。
まずは、socket()
関数でソケットオブジェクトを作成します。
socket(アドレスファミリ, ソケットタイプ)
今回はアドレスファミリが「IPv4」で、ソケットタイプは「データグラムソケット(UDP)」を使用するため、それぞれ「usocket.AF_INET
」と「usocket.SOCK_DGRAM
」を指定します。
ソケットオブジェクトのconnect()
メソッドでコネクションを確立します。
変更・追加【16行目~18行目】
import usocket
from mywifi import MyWifi
SSID = "SSID"
PASSWORD = "PASSWORD"
NTP_SERVER = "ntp.nict.jp"
wifi = MyWifi()
def correct_time():
if not wifi.isconnected():
if not wifi.connect(SSID, PASSWORD):
return
addrinfo = usocket.getaddrinfo(NTP_SERVER, 123)
sockaddr = addrinfo[0][-1]
# print(sockaddr) # デバッグ用のコードをコメント化
s = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM) # ソケットの作成
s.connect(sockaddr) # コネクションを確立する
■ データの送受信
ソケットのコネクションを確立したNTPサーバーへ時刻情報のリクエストを送ります。このとき送るデータのフォーマットは上で確認した通りです。最初の3つの項目を以下のように指定し、それ以外は「0」を指定した48バイトのバイト列です。
- LI:00 ⇒ 0 ⇒ サーバーから時刻を取得するので「0」
- VN:100 ⇒ 4 ⇒ バージョン4(SNTP)
- MODE:011 ⇒ 3 ⇒ クライアント
そこで、48バイトのバイト列(bytearray
型)を作成し、最初の1バイト目に「0x23
」を格納した送信用データを用意します。
ソケットへ送信データを渡すときは、ソケットオブジェクトのsend()
関数を使います。反対に、ソケットからデータを受け取るときは、recv()
関数を使います。
send(ソケットに渡すデータ) recv(ソケットから受信するデータの最大サイズ)
NTPサーバーのレスポンスも同じデータフォーマットです。つまり、ソケットから受け取るデータも同じく48バイトになっているため、recv(48)
としましょう。そしてデータの送受信を終えたあとは、ソケットオブジェクトのclose()
メソッドを実行して必ずコネクションを閉じます。
ここまでの説明を受けて、以下のようにコードを追加します。プログラムを実行して、ターミナルに受信したデータを表示して内容を確認しましょう。
追加【19行目~24行目】
import usocket
from mywifi import MyWifi
SSID = "SSID"
PASSWORD = "PASSWORD"
NTP_SERVER = "ntp.nict.jp"
wifi = MyWifi()
def correct_time():
if not wifi.isconnected():
if not wifi.connect(SSID, PASSWORD):
return
addrinfo = usocket.getaddrinfo(NTP_SERVER, 123)
sockaddr = addrinfo[0][-1]
# print(sockaddr)
s = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM)
s.connect(sockaddr)
data = bytearray(48) # NTPサーバーへ送信する48バイトのバイト列を作成
data[0] = 0x23 # 最初の1バイト目で「LI」「VN」「MODE」の3つを指定
s.send(data) # ソケットへデータを渡す
msg = s.recv(48) # ソケットからデータを受け取る
s.close() # コネクションを閉じる
print(msg) # 受信したデータの表示
(実行結果)
>>> correct_time() b'$\x01\x00\xec\x00\x00\x00\x00\x00\x00\x00\x00nict\xe2\xd1\xe98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe2\xd1\xe98\xfeA\x88f\xe2\xd1\xe98\xfeA\x96('
■ 必要な時刻情報の抽出と10進数への進数変換
NTPサーバーから送られてくる情報の中で、時刻の補正を行うために必要なのは、「受信タイムスタンプ」と「送信タイムスタンプ」です。
- 受信タイムスタンプ
サーバーへリクエストが到着した時刻 - 送信タイムスタンプ
サーバーからクライアントへのレスポンスを発信した時刻
どちらも8バイト(64ビット)のデータ量で、受信タイムスタンプが受信データ全体の33バイト目~40バイト目、送信タイムスタンプが41バイト目~48バイト目に位置しています。また、それぞれのデータは8バイトのうち上位4バイトが整数部で、下位4バイトが小数部です。今回は小数点以下の細かい時刻までは追いませんので整数部だけを使います。
上記のデータはバイト列になっています。そのため、ubinascii
モジュールのhexlify()
関数利用して16進数に変換し、さらにint()
関数で10進数の数値へ変換します。次のようにコードを追加しましょう。
変更・追加【2行目、25行目~27行目】
import usocket
import ubinascii # ubinasciiモジュールを新たにインポート
from mywifi import MyWifi
SSID = "SSID"
PASSWORD = "PASSWORD"
NTP_SERVER = "ntp.nict.jp"
wifi = MyWifi()
def correct_time():
if not wifi.isconnected():
if not wifi.connect(SSID, PASSWORD):
return
addrinfo = usocket.getaddrinfo(NTP_SERVER, 123)
sockaddr = addrinfo[0][-1]
# print(sockaddr)
s = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM)
s.connect(sockaddr)
data = bytearray(48)
data[0] = 0x23
s.send(data)
msg = s.recv(48)
s.close()
# print(msg) # デバッグ用のコードをコメント化
server_receive = int(ubinascii.hexlify(msg[32:36]), 16) # 受信タイムスタンプの整数部
server_transmit = int(ubinascii.hexlify(msg[40:44]), 16) # 送信タイムスタンプの整数部
print(server_receive)
print(server_transmit)
※ 配列の開始番号が「0」のため、33バイト目~36バイト目のデータを取り出すときに、msg[32:36]
となっていることに注意してください。
ここまでのプログラムを実行して、動作を確認しましょう。
(実行結果の例)
>>> correct_time() 3805415865 3805415865
※ 通信環境によっては、受信タイムスタンプと送信タイムスタンプの差が1秒未満になる場合もあります。
■ 時差の計算
時差の計算には、クライアント側のリクエストの発信時刻とレスポンスの受信時刻も必要でした。そこで、time
モジュールのtime()
関数を使い、「秒」で表された現在のRTCの時刻をデータ送受信の前後で記録しておきます。
追加【3行目、23行目、26行目】
import usocket
import ubinascii
import time # timeモジュールのインポート
from mywifi import MyWifi
SSID = "SSID"
PASSWORD = "PASSWORD"
NTP_SERVER = "ntp.nict.jp"
wifi = MyWifi()
def correct_time():
if not wifi.isconnected():
if not wifi.connect(SSID, PASSWORD):
return
addrinfo = usocket.getaddrinfo(NTP_SERVER, 123)
sockaddr = addrinfo[0][-1]
# print(sockaddr)
s = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM)
s.connect(sockaddr)
data = bytearray(48)
data[0] = 0x23
client_transmit = time.time() # クライアント側のリクエストの発信時刻
s.send(data)
msg = s.recv(48)
client_receive = time.time() # クライアント側のリクエストの受信時刻
s.close()
# print(msg)
server_receive = int(ubinascii.hexlify(msg[32:36]), 16)
server_transmit = int(ubinascii.hexlify(msg[40:44]), 16)
これで補正に必要な4つの時刻情報が得られましたが、ひとつだけ問題が残っています。それはtime
モジュールが参照しているStuduino:bitのRTCが「2000年1月1日 0時0分0秒」を開始時点としているのに対して、NTPサーバーのUTCは「1900年1月1日 0時0分0秒」を開始時点としており、2つの時刻はそのままでは比較ができないということです。
そこで、RTCの方に開始時間を合わせたるために、「1900年1月1日 0時0分0秒」から「2000年1月1日 0時0分0秒」までの経過時間(秒)をNTPサーバーから取得した時刻から引きます。
この経過時間は「3155673600秒」です。定数DELTA
として定義して、server_receive
と server_transmit
からそれぞれ引きましょう。
変更・追加【9行目、30・31行目】
import usocket
import ubinascii
import time
from mywifi import MyWifi
SSID = "SSID"
PASSWORD = "PASSWORD"
NTP_SERVER = "ntp.nict.jp"
DELTA = 3155673600 # 開始時点を揃えるための時間差(1900-1-1 0:0:0 ~ 2000-1-1 0:0:0)
wifi = MyWifi()
def correct_time():
if not wifi.isconnected():
if not wifi.connect(SSID, PASSWORD):
return
addrinfo = usocket.getaddrinfo(NTP_SERVER, 123)
sockaddr = addrinfo[0][-1]
# print(sockaddr)
s = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM)
s.connect(sockaddr)
data = bytearray(48)
data[0] = 0x23
client_transmit = time.time()
s.send(data)
msg = s.recv(48)
client_receive = time.time()
s.close()
# print(msg)
server_receive = int(ubinascii.hexlify(msg[32:36]), 16) - DELTA # 開始時点を揃える
server_transmit = int(ubinascii.hexlify(msg[40:44]), 16) - DELTA # 開始時点を揃える
これで準備が整いました。前のチャプターで確認した計算式をつくり、時差を求めましょう。
追加【32行目】
server_receive = int(ubinascii.hexlify(msg[32:36]), 16) - DELTA
server_transmit = int(ubinascii.hexlify(msg[40:44]), 16) - DELTA
offset = ((server_receive - client_transmit) + (server_transmit - client_receive)) / 2
■ RTCの時刻のズレを補正する
最後にRTCの時刻のズレを補正します。まずは、RTC
クラスをインポートして、オブジェクトを作成するコードを追加します。
追加【4行目、12行目】
import usocket
import ubinascii
import time
from machine import RTC # RTCクラスのインポート
from mywifi import MyWifi
SSID = "SSID"
PASSWORD = "PASSWORD"
NTP_SERVER = "ntp.nict.jp"
DELTA = 3155673600
rtc = RTC() # RTCのオブジェクトを作成
wifi = MyWifi()
続けて求めた時差(offset
)から現在の正しい時刻を計算します。このときに、標準時間帯を考慮します。日本の場合はUTC+9、つまり、NTPサーバーから取得したUTCよりも9時間先に進んでいるため、その分だけ時間を足します。この時間は定数として定義しておきましょう。
追加【11行目、36行目】
import usocket
import ubinascii
import time
from machine import RTC
from mywifi import MyWifi
SSID = "SSID"
PASSWORD = "PASSWORD"
NTP_SERVER = "ntp.nict.jp"
DELTA = 3155673600
UTC_TIMEZONE_JP = 32400 # 日本の場合はUTC+9のため9時間(3600*9=32400秒)先に進める
server_receive = int(ubinascii.hexlify(msg[32:36]), 16) - DELTA
server_transmit = int(ubinascii.hexlify(msg[40:44]), 16) - DELTA
offset = ((server_receive - client_transmit) + (server_transmit - client_receive)) / 2
current_time = time.time() + offset + UTC_TIMEZONE_JP # 補正後の現在時刻を求める
そして、RTCのinit()
メソッドに現在の日付・時刻を渡して補正完了です。そのためには、秒で表された時刻から「年・月・日・時・分・秒」の情報に変換しなければいかません。そのときに利用できるのが、time
モジュールのlocaltime()
関数です。この関数は2000年1月1日0時0分0秒から経過した時間(秒)を渡すと、現在の日付・時刻を以下の形式で返します。
time
モジュールのlocaltime()
関数の返り値
(年, 月, 日, 時, 分, 秒, 曜日, 年内の通算日数を表す数字1~366)
これをそのままRTC
クラスのinit()
メソッドに渡したいところですが、引数となるタプルの並びが上記と少し違います。
RTC
クラスのinit()
メソッドの引数
(年, 月, 日, 曜日, 時, 分, 秒, ミリ秒)
そのため、順序を入れ替えたタプルを作成してRTC
クラスのinit()
メソッドに引き渡します。以下のようにコードを追加しましょう。
追加【37行目~39行目】
server_receive = int(ubinascii.hexlify(msg[32:36]), 16) - DELTA
server_transmit = int(ubinascii.hexlify(msg[40:44]), 16) - DELTA
offset = ((server_receive - client_transmit) + (server_transmit - client_receive)) / 2
current_time = time.time() + offset + UTC_TIMEZONE_JP
datetime = time.localtime(int(current_time)) # 日付・時刻の形式に変換。引数は整数値のみ受け取るため、int()関数で整数化
datetime = datetime[:3] + (datetime[6],) + datetime[3:6] + (0,) # init()メソッドに合わせた順序の入れ替え
rtc.init(datetime) # 補正後の時刻を新たに設定
これで、時刻のズレを補正するプログラムができました。続けてデジタル時計として時刻を表示する機能と一定時間おきに自動で時刻補正が行われるように、追加のプログラムを書いていきましょう。
5. 3 時刻を表示する機能と定期的に時刻補正を行う機能の作成
まずは、LEDディスプレイに現在の時刻を表示するscroll_time()
関数を定義します。
■ 現在の時刻をLEDディスプレイにスクロール表示する
RTCの現在の時刻を取得するには、RTC
クラスのdatetime()
メソッドか、もしくはtime
モジュールのlocaltime()
メソッドを使います。ここは、どちらを使用しても構いません。以下のコードではRTC
クラスのdatetime()
メソッドを使っています。
追加【5行目、43行目~46行目】
import usocket
import ubinascii
import time
from machine import RTC
from pystubit.board import display # LEDディスプレイの制御用オブジェクト
from mywifi import MyWifi
server_receive = int(ubinascii.hexlify(msg[32:36]), 16) - DELTA
server_transmit = int(ubinascii.hexlify(msg[40:44]), 16) - DELTA
offset = ((server_receive - client_transmit) + (server_transmit - client_receive)) / 2
current_time = time.time() + offset + UTC_TIMEZONE_JP
datetime = time.localtime(int(current_time))
datetime = datetime[:3] + (datetime[6],) + datetime[3:6] + (0,)
rtc.init(datetime)
def scroll_time(): # 現在のRTCの時刻をスクロール表示する関数
datetime = rtc.datetime() # 現在の日付・時刻をタプルで取得
msg = "{}:{}".format(datetime[4], datetime[5]) # 時:分の形式で表示用の文字列を用意
display.scroll(msg, delay=10, color=(31,31,31)) # LEDディスプレイに白色でスクロール表示する。
LEDディスプレイの表示色は白色にしていますが、例えば時間帯によって表示色を変えるなどの工夫も考えられます。
■ 超音波距離センサーに手を近づけると時刻を表示する
デジタル時計に取り付けた超音波距離センサに手を近づけるとscroll_time()
関数が実行されるようにメインのループ処理を作成します。このコードはmain()
関数としてまとめましょう。
追加【6行目、17行目、51行目~56行目】
import usocket
import ubinascii
import time
from machine import RTC
from pystubit.board import display
from pyatcrobo2.parts import UltrasonicSensor # 超音波距離センサのクラス
from mywifi import MyWifi
SSID = "SSID"
PASSWORD = "PASSWORD"
NTP_SERVER = "ntp.nict.jp"
DELTA = 3155673600
UTC_TIMEZONE_JP = 32400
rtc = RTC()
us = UltrasonicSensor("P0") # 超音波距離センサーの制御オブジェクトを作成
wifi = MyWifi()
def scroll_time():
datetime = rtc.datetime()
msg = "{}:{}".format(datetime[4], datetime[5])
display.scroll(msg, delay=10, color=(31,31,31))
def main(): # メインのループ処理
while True:
distance = us.get_distance() # 超音波距離センサの距離の取得
if distance > 0 and distance < 5: # 5cm未満まで手を近づけたら
scroll_time() # RTCの現在時刻をスクロール表示
time.sleep_ms(20) # 少し時間をおいて距離を再取得する
■ 自動で時刻補正を行う
定期的に時刻補正を行うために、Timer
クラスを利用したタイマ割込みを設定します。NICTの利用規約で、ポーリングの間隔は1時間に20回以下に制限されていました。ここでは、1時間に6回(10分に1回)の間隔で時刻補正を行うようにしてみましょう。
【 サンプルコード 5-3-1 】
変更・追加【4行目、16行目、20行目、53・54行目、62行目】
import usocket
import ubinascii
import time
from machine import RTC, Timer # Timerクラスを追加でインポート
from pystubit.board import display
from pyatcrobo2.parts import UltrasonicSensor
from mywifi import MyWifi
SSID = "SSID"
PASSWORD = "PASSWORD"
NTP_SERVER = "ntp.nict.jp"
DELTA = 3155673600
UTC_TIMEZONE_JP = 32400
rtc = RTC()
timer = Timer(1) # Timerクラスのオブジェクトを作成
us = UltrasonicSensor("P0")
wifi = MyWifi()
def correct_time(t=None): # 引数の追加。タイマ割込みで呼び出されるときに、この引数にタイマオブジェクトが渡される。
if not wifi.isconnected():
if not wifi.connect(SSID, PASSWORD):
return
addrinfo = usocket.getaddrinfo(NTP_SERVER, 123)
sockaddr = addrinfo[0][-1]
# print(sockaddr)
s = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM)
s.connect(sockaddr)
data = bytearray(48)
data[0] = 0x23
client_transmit = time.time()
s.send(data)
msg = s.recv(48)
client_receive = time.time()
s.close()
# print(msg)
server_receive = int(ubinascii.hexlify(msg[32:36]), 16) - DELTA
server_transmit = int(ubinascii.hexlify(msg[40:44]), 16) - DELTA
offset = ((server_receive - client_transmit) + (server_transmit - client_receive)) / 2
current_time = time.time() + offset + UTC_TIMEZONE_JP
datetime = time.localtime(int(current_time))
datetime = datetime[:3] + (datetime[6],) + datetime[3:6] + (0,)
rtc.init(datetime)
def scroll_time():
datetime = rtc.datetime()
msg = "{}:{}".format(datetime[4], datetime[5])
display.scroll(msg, delay=10, color=(31,31,31))
def main():
correct_time() # 最初に時刻補正を行う
timer.init(mode=Timer.PERIODIC, period=600000, callback=correct_time) # 10分(600秒)の周期でタイマ割込みを設定
while True:
distance = us.get_distance()
if distance > 0 and distance < 5:
scroll_time()
time.sleep_ms(20)
main() # main()関数の実行
20行目で引数t
を追加しているのは、54行目で設定したタイマ割込みでこの関数が呼び出されるときに、呼び出し元のタイマオブジェクト(timer
)が引数に渡されるためです。また、デフォルト値としてNone
を指定しているのは、53行目のように引数なしで呼び出されても問題ないようにするためです。
これでプログラムの完成です。転送して実行し、動作を確認しましょう。
チャプター6
課題:時計にアラーム機能を追加しよう
このチャプターでは、作成したデジタル時計に以下の簡単な「アラーム機能」を追加する課題に取り組みましょう。
【 アラーム機能の内容 】
- プログラム内で設定された時間になると、繰り返しアラーム音が鳴る。
- アラーム中にStuduino:bitのボタンAが押されると停止する。
- アラーム中以外にボタンAが押されると、アラーム設定のONとOFFが切り替わる。※初期設定はOFF
6. 1 プログラム作成のヒント
上記のアラーム機能を実現するために、新たに以下の2つの関数を定義します。また、アラームの時刻は(時, 分)
のタプルでプログラム内に直接定義し、アラームの起動は新たに作成したタイマオブジェクトを使ったタイマ割込み処理で設定します。
【 追加で定義する関数 】
|関数名|処理内容|
|:—|:—|
|alarm()
関数|Studuino:bitのボタンAが押されるまで、ブザーからアラーム音を繰り返し鳴らす関数。この関数をタイマ割込みで呼び出します。|
|toggle_alarm()
関数|Studuino:bitのボタンAが押されたときに、アラーム設定のONとOFFを切り替える関数。ONからOFFに切り替えるときはタイマ割込みの設定を解除し、OFFからONに切り替えるときは1回のみでタイマ割込みを設定します。|
以上はあくまでも参考程度のヒントです。できるだけ自分で考えてプログラムを作成しましょう。もし手が止まってしまったときは、以下のプログラム例の作成手順を確認してください。
6. 2 プログラム例の作成手順
ヒントの所で紹介した2つの関数を順番に定義していきます。
■ alarm() 関数の作成
ボタンAが押されるまで、繰り返しブザーからアラーム音を鳴らすalarm()
関数を作成します。プログラムの先頭で、使用するボタンAとブザーの制御用オブジェクトをインポートします。
変更【5行目】
import usocket
import ubinascii
import time
from machine import RTC, Timer
from pystubit.board import display, button_a, buzzer # ボタンAとブザーの制御用オブジェクトのインポート
from pyatcrobo2.parts import UltrasonicSensor
from mywifi import MyWifi
次に、アラームの設定がONになっているかどうかを判断するためのフラグを用意します。最初の設定はOFFにしておき、アラームを停止したときもこのフラグをOFFに戻します。
追加【19行目】
rtc = RTC()
timer = Timer(1)
us = UltrasonicSensor("P0")
wifi = MyWifi()
is_alarm_on = False # アラームがONのときはTrue、OFFのときはFalseとするフラグ
アラーム中は、高めの音(C7)を断続的に鳴らします。これをalarm()
関数内でボタンAが押されるまで繰り返します。また、alarm()
関数はタイマ割込みで呼び出されるため、correct_time()
関数と同様に、呼び出し元のタイマオブジェクトを受け取る引数を用意します。こちらも単独で呼び出せるように、引数のデフォルト値にNone
を設定しておきましょう。
追加【54行目~61行目】
def scroll_time():
datetime = rtc.datetime()
msg = "{}:{}".format(datetime[4], datetime[5])
display.scroll(msg, delay=10, color=(31,31,31))
def alarm(t=None): # この関数はタイマ割込み処理で呼び出される
global is_alarm_on # グローバル変数を書き換えるためのグローバル宣言
while not button_a.is_pressed(): # ボタンAが押されるまでアラーム音を断続的に鳴らす
buzzer.on("C7", duration=200)
time.sleep_ms(100)
while button_a.is_pressed(): # ボタンAがはなされるまで待つ
pass
is_alarm_on = False # フラグをOFFに戻す
プログラムを実行して、ターミナルからalarm()
関数を呼び出します。ボタンAを押すとアラーム音が停止することを確認しましょう。
■ toggle_alarm() 関数の作成
この関数には、alarm()
関数のタイマ割込みを設定・解除するコードをまとめます。まずは、新しくタイマオブジェクトを用意します。
追加【17行目】
rtc = RTC()
timer = Timer(1)
timer_for_alarm = Timer(2) # アラーム用のタイマオブジェクト
us = UltrasonicSensor("P0")
wifi = MyWifi()
is_alarm_on = False
次にアラームを起動する時刻(alarm_time
)を定義します。時刻は(時, 分)
のタプルで表しましょう。
追加【21行目】
rtc = RTC()
timer = Timer(1)
timer_for_alarm = Timer(2)
us = UltrasonicSensor("P0")
wifi = MyWifi()
is_alarm_on = False
alarm_time = (7, 30) # (時, 分)
toggle_alarm()
関数を作成し、用意したフラグ「is_alarm_on
」がTrue
の場合はタイマ割込みの解除を行います。
追加【65~71行目】
def toggle_alarm():
global is_alarm_on
if is_alarm_on: # 現在アラームの設定がONになっているとき
timer_for_alarm.deinit() # タイマ割込みを解除
is_alarm_on = False # フラグをOFFに戻す
display.scroll("OFF", delay=10, color=(31, 0, 0)) # OFFの文字をスクロール表示
else: # 現在アラームの設定がOFFになっているとき
# 以下にタイマ割込み設定時のコードを書く
is_alarm_on
がFalse
の場合は新たにタイマ割込みを設定します。この設定には、現在の時刻からアラーム起動の時刻までの時差が必要です。その時差を求める計算式は、現在の時刻とその日に予定していたアラーム起動時刻との関係で変わります。
- 現在時刻が今日のアラーム起動時刻を超えていない場合
「アラームの起動時刻 - 現在の時刻
」がアラーム起動までの時差となる。 - 現在時刻が今日のアラーム起動時刻を超えている場合
アラームは翌日の同じ時刻に設定することになるため、「(アラームの起動時刻 + 24時間 ) - 現在の時刻
」がアラーム起動まで時差となる。
以上の点に注意して以下のようにコードを追加しましょう。
追加【72~81行目】
def alarm(t=None):
global is_alarm_on
while not button_a.is_pressed():
buzzer.on("C7", duration=200)
time.sleep_ms(100)
while button_a.is_pressed():
pass
is_alarm_on = False
def toggle_alarm():
global is_alarm_on
if is_alarm_on:
timer_for_alarm.deinit()
is_alarm_on = False
display.scroll("OFF", delay=10, color=(31, 0, 0))
else:
datetime = rtc.datetime() # 現在のRTCの時刻を取得
secs_now = datetime[4] * 3600 + datetime[5] * 60 + datetime[6] # 現在の時刻をその日の0時0分0秒を開始時点とした経過時間(秒)に変換
secs_alarm = alarm_time[0] * 3600 + alarm_time[1] * 60 # 上の行と同じ変換をアラームの起動時刻についても行う。
if secs_alarm > secs_now: # 今日予定のアラームの起動時刻を現在時刻がまだ超えていないとき
secs = secs_alarm - secs_now # そのまま時差を計算
else: # 既に今日予定のアラーム起動時刻を現在時刻が超えているとき
secs = secs_alarm + 86400 - secs_now # 翌日の同じ時刻までの時差(秒)を計算
timer_for_alarm.init(mode=Timer.ONE_SHOT, period=secs*1000, callback=alarm) # 1度だけのタイマ割込みでアラームを設定。引数periodの単位はミリ秒であることに注意。
is_alarm_on = True # フラグをONにする
display.scroll("ON", delay=10, color=(31, 0, 0)) # ONの文字をスクロール表示する
そして、この関数をボタンAが押されるたびに呼び出して、アラームのONとOFFを切り替えられるようにします。これでプログラムの完成です。
追加【91・92行目】
def main():
correct_time()
timer.init(mode=Timer.PERIODIC, period=600000, callback=correct_time)
while True:
distance = us.get_distance()
if distance > 0 and distance < 5:
scroll_time()
if button_a.is_pressed(): # ボタンAが押されたときに、
toggle_alarm() # アラームのONとOFFを切り替える
time.sleep_ms(20)
完成したプログラム例の全体を以下に示します。うまくエラーを解決できない場合は比較の参考にしてください。
【 サンプルコード 6-2-1 】
import usocket
import ubinascii
import time
from machine import RTC, Timer
from pystubit.board import display, button_a, buzzer # ボタンAとブザーの制御用オブジェクトのインポート
from pyatcrobo2.parts import UltrasonicSensor
from mywifi import MyWifi
SSID = "Rakuten-Casa-A2212"
PASSWORD = "B9NCBRBK"
NTP_SERVER = "ntp.nict.jp"
DELTA = 3155673600
UTC_TIMEZONE_JP = 32400
rtc = RTC()
timer = Timer(1)
timer_for_alarm = Timer(2)
us = UltrasonicSensor("P0")
wifi = MyWifi()
is_alarm_on = False
alarm_time = (21, 15)
def correct_time(t=None):
if not wifi.isconnected():
if not wifi.connect(SSID, PASSWORD):
return
addrinfo = usocket.getaddrinfo(NTP_SERVER, 123)
sockaddr = addrinfo[0][-1]
# print(sockaddr)
s = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM)
s.connect(sockaddr)
data = bytearray(48)
data[0] = 0x23
client_transmit = time.time()
s.send(data)
msg = s.recv(48)
client_receive = time.time()
s.close()
# print(msg)
server_receive = int(ubinascii.hexlify(msg[32:36]), 16) - DELTA
server_transmit = int(ubinascii.hexlify(msg[40:44]), 16) - DELTA
offset = ((server_receive - client_transmit) + (server_transmit - client_receive)) / 2
current_time = time.time() + offset + UTC_TIMEZONE_JP
datetime = time.localtime(int(current_time))
datetime = datetime[:3] + (datetime[6],) + datetime[3:6] + (0,)
rtc.init(datetime)
def scroll_time():
datetime = rtc.datetime()
msg = "{}:{}".format(datetime[4], datetime[5])
display.scroll(msg, delay=10, color=(31,31,31))
def alarm(t=None):
global is_alarm_on
while not button_a.is_pressed():
buzzer.on("C7", duration=200)
time.sleep_ms(100)
while button_a.is_pressed():
pass
is_alarm_on = False
def toggle_alarm():
global is_alarm_on
if is_alarm_on:
timer_for_alarm.deinit()
is_alarm_on = False
display.scroll("OFF", delay=10, color=(31, 0, 0))
else:
datetime = rtc.datetime()
secs_now = datetime[4] * 3600 + datetime[5] * 60 + datetime[6]
secs_alarm = alarm_time[0] * 3600 + alarm_time[1] * 60
if secs_alarm > secs_now:
secs = secs_alarm - secs_now
else:
secs = secs_alarm + 86400 - secs_now
timer_for_alarm.init(mode=Timer.ONE_SHOT, period=secs*1000, callback=alarm)
is_alarm_on = True
display.scroll("ON", delay=10, color=(31, 0, 0))
def main():
correct_time()
timer.init(mode=Timer.PERIODIC, period=600000, callback=correct_time)
while True:
distance = us.get_distance()
if distance > 0 and distance < 5:
scroll_time()
if button_a.is_pressed():
toggle_alarm()
time.sleep_ms(20)
main()
チャプター7
おわりに
7. 1 このレッスンのまとめ
このレッスンでは、インターネットを構成する以下の4つの層における通信プロトコルについて、より詳しく学習しました。
- アプリケーション層
- トランスポート層
- インターネット層
- ネットワークインターフェース層
そして、HTTPと同じくアプリケーション層に位置する「NTP(Network Time Protocol)」という、インターネットに接続したコンピュータの時刻同期用の通信プロトコルを紹介しました。そして、NTPを利用してStuduino:bitのリアルタイムクロックに現在の時刻を設定しました。
このように、コンピュータに正確な時刻を設定できるとインターネットを利用する他のサービスと連携するときにも都合が良いのです。例えば、メール配信を予約している場面を考えてみましょう。もし、このときコンピュータの時計がズレていると、誤った時間にメールを配信してすることになります。こういった問題もNTPを利用して常に正確な時刻に補正できていれば防ぐことができます。
これから自分で制作するロボットへ時刻に関連する機能を持たせることになったときは、ぜひ今回学んだことを活用してみましょう。
7. 2 次回のレッスンについて
次回のレッスンでは、テーマ.11-2とテーマ.11-3で学んだことを踏まえて、「日用品のIoT化」をテーマにインターネットに接続する「傘立てロボット」を制作します。