Pythonロボティクスコース レッスン 39
テーマ.10-2 光を使った通信装置の制作
コンピュータ上での文字データの表し方を学ぼう
チャプター1
このレッスンで学ぶこと
このレッスンでは、コンピュータ上で扱うデータ量の表し方や、文字データをコンピュータ上で扱うための仕組みを学習します。
チャプター2
コンピュータで扱うデータ量の表し方
テーマ.10-1では、コンピュータ内部の計算は、簡単に言えば、電子回路のスイッチのオン/オフの切り替えによって行われていることを説明しました。
そして、スイッチのオン/オフによって、回路にかかる電圧は低い状態(LOW)と高い状態(HIGH)に変化し、それぞれをデジタルの「0(LOW)」と「1(HIGH)」に置き換えることで、コンピュータは2進数で数を扱うことができました。
実は数だけでなく、文字や写真、動画などコンピュータが扱うすべてのデータが、「0」と「1」の2つの数字で表されています。
コンピュータ上では1桁の「0」と「1」で表される情報がデータの最小単位となり、この単位を「bit(ビット)」といいます。bitは「binary(2進数)」と「digit(桁)」を組み合わせた造語です。また、コンピュータ上でデータ量を表すときは、bitではなく「byte(バイト)」という単位が使用されます。一般的に、1byteは8bitで表されるデータ量を表しています。(bitは桁数)
パソコンやスマートフォンで使用するデータの通信量を表すときに使われる「G(ギガ)」という言葉は、「Gigabyte(ギガバイト)」の略称です。「Giga」という接頭辞が10億(109)を表しているため、「1G」は「10億byte」のデータ量を指しています。
*-*-*-*-
(注意:2進数の考え方)
1byte=8bit
1KiB(キビバイト)=1024byte(2^10)
1MiB(メビバイト)=1024*1024byte
1GiB(ギビバイト)=1024*1024*1024byte =1,073,741,824byte
実際には、コンピュータにおける1キロバイト (KB) は、1024バイトではなく、1000バイトで定義される場合が一般的です。この定義はSI接頭語の規定に基づいています。
具体的には、以下の通りです:
- 1キロバイト (KB) = 1000バイト
これはSI接頭語の「キロ」が1000を意味するためです。この定義は国際標準化機構(ISO)や国際単位系(SI)によって規定されています。
ただし、コンピュータの技術用語としては、2進数の性質から「キロバイト」を1024バイトとする用語も使われてきました。これは、2^10 (1024) が近似的に1キロバイトに相当するためです。
しかし、近年では国際的な標準化の進展により、正確な意味での「キロバイト」は1000バイトとされることが多くなっています。したがって、正確な場面では1000バイトが用いられるべきですが、歴史的には1024バイトを指す場合も見られます。
このように、コンピュータ業界における用語の定義は時とともに変化しており、正確な文脈に応じてどちらの定義が適用されているかを理解する必要があります。
【 データ量を表すときに使われる接頭辞 】
接頭辞 | 意味 |
---|---|
K(キロ) | 1000(103) |
M(メガ) | 100万(106) |
G(ギガ) | 10億(109) |
T(テラ) | 1兆(1012) |
P(ペタ) | 1000兆(1015) |
チャプター3
コンピュータ上での文字データの表し方
上では、コンピュータが扱う文字データも「0」と「1」で表されていることを紹介しました。例えば、英語のアルファベット1文字は、Pythonの標準的な設定では、1byte(8bit)のデータ量で表されています。
↓2進数は「01000001」です。(画像は間違い)
この1byteが表す数値(番号)は、「文字コード」または、「コードポイント」と呼ばれています。つまり、コンピュータ上では直接文字データを扱うことができないため、代わりに固有の番号に置き換えているということです。
そして、どの文字に対してどの文字コードを割り当てるのかを定めているのが「文字集合」です。元々は言語によって異なる文字集合が利用されていましたが、現在ではインターネットが普及し、世界中の様々な人々が共通のWebサイトやWebサービスを利用するようになったため、多言語に対応した「Unicode(ユニコード)」という文字集合が広く使われるようになりました。UnicodeはPythonでも標準の文字集合として採用されています。
下の表は、Unicodeで英語のアルファベットや記号に割り振られている文字コードの一例です。
【 Unicodeの文字コードの一例 】
アルファベット・記号 | 文字コード(16進数) |
---|---|
! | 33(0x21) |
? | 63(0x3F) |
A | 65(0x41) |
B | 66(0x42) |
C | 67(0x43) |
a | 97(0x61) |
b | 98(0x62) |
c | 99(0x63) |
文字コードは、ord()
関数を使って確認することもできます。
Pythonのord()
関数のord
は、「ordinal」(序数)を略したものです。この関数は、文字のUnicodeコードポイント(整数値)を返します。例えば、ord('A')
は65を返します。
【 サンプルコード 3-1-1 】
A = "A"
a = "a"
print(ord(A))
print(ord(a))
(実行結果)
65 97
また、反対にchr()
関数を使うと、文字コードから文字に変換することもできます。
Pythonのchr()
関数のchr
は、「character」(文字)を略したものです。この関数は、整数値を対応するUnicode文字に変換します。例えば、chr(65)
は文字「A」を返します。
【 サンプルコード 3-1-2 】
print(chr(65))
print(chr(97))
(実行結果)
A a
このように、特定の文字をUnicodeのような文字集合で定義された文字コードへ変換することを「エンコーディング」といいます。反対に文字コードから文字へ変換することを「デコーディング」といいます。
unicodeでは、代表的なエンコーディング方式として「utf-8」と「utf-16」があります。これら2つの違いは、主に文字コードのデータ量、つまり、何byteで1文字を表すかという点にあります。
「utf-8」は、1文字を1~6byteで表す方式で、英語のアルファベットは1byteに変換されますが、日本語で使う漢字や平仮名、片仮名は3byteで表されます。
一方で、「utf-16」は2byte単位で表す方式で、英語のアルファベットも、日本語の漢字や平仮名、片仮名も、どちらも2byteで表されます。そのため、文章内に英語が多い場合は「utf-8」の方がデータ効率が良く、日本語が多い場合は「utf-16」の方がデータ効率が良いと言えます。
Pythonでは、「utf-8」が標準のエンコーディング方式として設定されています。
そして、エンコーディングを行うときは、文字列のencode()
メソッドを使います。エンコーディング後は「バイト列」というデータ型に変換されます。バイト列についての詳しい説明は次のチャプターで行います。反対にデコーディングのときは、バイト列のdecode()
メソッドを使います。
【 サンプルコード 3-1-3 】
string = "abc"
# 文字のエンコーディング
encoded_str = string.encode("utf-8") # utf-8を設定(デフォルトがutf-8のため省略可能)
print(type(encoded_str)) # バイト列型
print(encoded_str) # バイト列のため先頭にbが付く
# 文字のデコーディング
decoded_str = encoded_str.decode("utf-8") # utf-8を設定(デフォルトがutf-8のため省略可能)
print(type(decoded_str)) # 文字列型
print(decoded_str) # 1行目のstringと同じもの
(実行結果)
<class 'bytes'> b'abc' <class 'str'> abc
実行結果を確認すると、バイト列に変換された証しとして、先頭にb
が付いています。
チャプター4
バイト列
バイト列は1バイトのデータが並んだ集合です。文字列や数値の場合は、同じ1バイト単位のデータであっても、文字や数であるという意味が与えられています。一方でバイト列には、特定の形式や意味が与えられていません。つまり、ただの「0」と「1」の並びという扱いになります。
さきほどは文字のエンコーディングを英語のアルファベットで行ったため、print文で出力した結果を見てもバイト列であることが分かりづらくなっていましたが、日本語をエンコーディングして、出力すると、3byteのバイト列に変換されていることが確認できます。
※ 日本語が入力できない場合は、以下のサンプルコードをそのままコピー&ペーストして実行してください。
【 サンプルコード 4-1-1 】
string = "あ" # 日本語の平仮名
encoded_str = string.encode("utf-8")
print(encoded_str)
(実行結果)
b'\xe3\x81\x82'
バックスラッシュ(\
)で区切られた一画が1byte分のデータです。「x
」は16進数であることを表しているため、その後ろの2文字で1byteとなっています。
具体的には、b'\xe3\x81\x82'
の各バイトは以下のように解釈されます:
\xe3
(227)\x81
(129)\x82
(130)
これらのバイトが組み合わさって、UTF-8 でエンコードされた日本語の文字「\あ\」を表現しています。UTF-8 は、1バイトから4バイトの可変長エンコーディングであり、各文字をユニコードに対応するバイト列にエンコードします。
※ 1桁の16進数は4bitで表せるため、2桁の16進数で1byteになります。
*-*-*-*-*-*-*-*-*-
16進数とビット
16進数(hexadecimal)は、1桁で0から15(0-F)までの値を表現します。1桁の16進数は4ビットで表現できます。
- 0 (0000)
- 1 (0001)
- 2 (0010)
- 3 (0011)
- 4 (0100)
- 5 (0101)
- 6 (0110)
- 7 (0111)
- 8 (1000)
- 9 (1001)
- A (1010)
- B (1011)
- C (1100)
- D (1101)
- E (1110)
- F (1111)
2桁の16進数で1バイト
8ビット(1バイト)は、4ビットの2倍で、2桁の16進数で表現されます。
例:
- 0xE3(16進数) = 11100011(2進数)
0xE3
は2桁の16進数で、これは8ビット(1バイト)になります。
例として b'\xe3\x81\x82'
b'\xe3\x81\x82'
の各バイトは次のように解釈されます:
\xe3
(16進数) = 227(10進数) = 11100011(2進数)\x81
(16進数) = 129(10進数) = 10000001(2進数)\x82
(16進数) = 130(10進数) = 10000010(2進数)
各バイトは8ビットで構成されており、3バイト(24ビット)で1文字の「\あ\」を表現しています。これは、UTF-8エンコーディングの特徴です。UTF-8は、1バイトから4バイトの可変長エンコーディングであり、各文字をユニコードに対応するバイト列にエンコードします。
このように、2桁の16進数は1バイト(8ビット)を表現し、文字列をバイト列として扱う際には非常に重要です。
*-*-*-*-*-*-*-*-*-*-
また、文字列だけでなく、数値とバイト列も相互に変換することができます。それには、整数型のto_bytes()
メソッドとint
のfrom_bytes()
メソッドを使います。
【 サンプルコード 4-1-2 】
num = 15777200
# バイト列へ変換
_bytes = num.to_bytes(3, "big")
print(_bytes)
# バイト列から数値(整数)へ変換
_num = int.from_bytes(_bytes, "big")
print(_num)
(実行結果)
'\xf0\xbd\xb0' 15777200
to_bytes()
メソッドの第1引数で、変換後のbyte数を指定します。「15777200」という数値は、ちょうど3byteで表すことができるため、「3」を渡しています。第2引数に渡した「"big"
」は、バイト列化するときのデータの並び順を表していて、「ビックエンディアン("big"
)」と「リトルエンディアン("little"
)」の2つの方式があります。
【 ビックエンディアンとリトルエンディアンの違い 】
実際にさきほどのサンプルコードをリトルエンディアンに変更してみましょう。
【 サンプルコード 4-1-3 】
変更【3行目、6行目】
num = 15777200
_bytes = num.to_bytes(3, "little")
print(_bytes)
_num = int.from_bytes(_bytes, "little")
print(_num)
(実行結果)
b'\xb0\xbd\xf0' 15777200
実行結果からもバイト列の並び順が入れ替わっていることが分かります。ビッグエンディアンでバイト列化してリトルエンディアンで数値へ戻すと、内容が変わってしまうため、どちらの方式でバイト列化されたものなのかを必ず確認するようにしましょう。
チャプター5
ビット演算
ここまでで、様々なデータがbitやbyteという情報量の単位で表されていることを確認してきました。実はPythonも含めて多くのプログラミング言語では、1bit単位での演算処理をサポートしています。
5. 1 1bit単位での論理演算(AND、OR、XOR)
2つのデータを1bit単位で論理演算するときは、次の演算子を使います。
演算子 | 内容 | |
---|---|---|
& | 論理積(AND) | |
| | 論理和(OR) | |
^ | 排他的論理和(XOR) |
それぞれ、同じ桁のbitの組み合わせで得られる結果は次のようになります。
- 論理積(AND)
A | B | A AND B |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
0 | 1 | 0 |
1 | 1 | 1 |
- 論理和(OR)
A | B | A OR B |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 1 |
- 排他的論理和(XOR)
A | B | A XOR B |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 0 |
実際に8bitの2進数で表したデータを用意してこれらの演算子を使ってみましょう。
前のレッスンでは、"0b101"
や"0x1f"
のように2進数や16進数を文字列として定義して扱いましたが、ここでは""
で囲まずに数値として定義します。
A = 0b10101111 # 175(10進数)
B = 0b11110101 # 245(10進数)
では、上のAとBの2つの数値に対して、1bit単位での論理積(AND)と論理和(OR)、排他的論理和(XOR)をそれぞれ行い、結果を表示してみましょう。
【 サンプルコード 5-1-1 】
追加【4行目~6行目】
A = 0b10101111
B = 0b11110101
print("AND", A & B) # 論理積(AND)
print("OR", A | B) # 論理和(OR)
print("XOR", A ^ B) # 排他的論理和(XOR)
(実行結果)
AND 165 OR 255 XOR 90
AとBは1bit単位で論理演算が行われていますが、結果が数値として扱われているので、print
文で表示すると10進数に書き換えられてしまいます。そこで、数値を2進数の文字列に変換するbin()
関数を使います。上のサンプルコードを以下のように書き直して、もう一度実行してみましょう。
【 サンプルコード 5-1-2 】
変更【4行目~6行目】
A = 0b10101111
B = 0b11110101
print("AND", bin(A & B))
print("OR", bin(A | B))
print("XOR", bin(A ^ B))
(実行結果)
AND 0b10100101 OR 0b11111111 XOR 0b1011010
今度は2進数で結果を確認することができました。
5. 2 1bit単位での反転(NOT)
論理演算には反転の「NOT」もあります。1bit単位で反転を行うときは、演算子「~
」を使います。まずは、以下のサンプルコードを実行して結果を確認してみましょう。
【 サンプルコード 5-2-1 】
C = 0b10101010 # 170(10進数)
print("NOT", bin(~C))
(実行結果)
NOT -0b10101011
もしかすると、ただ各桁の「0」と「1」が反転するだけという結果を予想していたのではないでしょうか。しかし実際はそのような結果とはなりません。
今度は、bin()
関数を使わずに結果を表示してみましょう。
【 サンプルコード 5-2-2 】
変更【3行目】
C = 0b10101010 # 170(10進数)
print("NOT", ~C)
(実行結果)
-171
「~
」で反転を行うと、「x
」を元の数値として「-(x+1)
」が返ってきます。そのため、2進数の文字列で表すと先頭に-
の符号が付き、最下位のビットに「1」が加算されることになります。
5. 3 ビットシフト
Pythonには論理演算以外にも、1bit単位で左や右にデータをシフトするための演算子が用意されています。
左にシフトするときは「<<
」の演算子を使い、右にシフトするときは「>>
」の演算子を使います。また、何桁分シフトするのかをこれらの演算子の後に続けて数値で指定します。実際に以下のサンプルコードを実行して、結果を確認してみましょう。
【 サンプルコード 5-3-1 】
C = 0b1001
print("<< 1", bin(C << 1)) # 左に1bit分シフト
print("<< 2", bin(C << 2)) # 左に2bit分シフト
print(">> 1", bin(C >> 1)) # 右に1bit分シフト
print(">> 2", bin(C >> 2)) # 右に2bit分シフト
(実行結果)
<< 1 0b10010 << 2 0b100100 >> 1 0b100 >> 2 0b10
普段はあまり1bit単位で演算をする機会は少ないかもしれませんが、情報のデータ量を削減したり、計算に必要な処理コストを下げたりするときに、これらの演算が役立ちます。
チャプター6
光通信用の装置の組立て
レッスンの前半では、コンピュータ上で扱う数値や文字、画像などのデータが「0」と「1」の2進数で表されていることや、「bit」や「byte」という情報量の単位があることを確認してきました。このようなデータの表し方は、コンピュータどうしで情報をやり取りするときにもメリットがあります。
コンピュータ間では、データを電気的な信号に変換してお互いに伝達しています。例えば、パソコンで作成したプログラムをStuduino:bitへ転送するときは、USBケーブルを介して、電圧が高い状態(HIGH)と電圧が低い状態(LOW)の変化を電気的な信号として送っています。例えばこの信号で、電圧の高い状態を「1」、電圧の低い状態を「0」としてルールを決めると、1bitずつ情報を送ることができます。
また、インターネット通信では、より多くのデータを高速で送るために、電気的な信号をさらに光の信号に変えて情報を伝達しています。
情報伝達に使うケーブルには、「光ファイバー」という素材が利用されています。光ファイバーは、一定の入射角を超えて入った光をすべて反射する性質(全反射)があり、光を外に漏らさずに内部で反射を繰り返しながら、遠く離れたところまで信号を届けることができます。
【 光ファイバーの構造 】
そして、光を利用した通信では、電圧の高低の代わりに光の点滅によって2進数の情報を送る仕組みになっています。
そこでレッスンの後半では、この光を利用した通信の仕組みを参考にして、「明るさの変化で1byte(8bit)の文字情報を送る光通信システム」を作成します。まずは、このシステムで使用する装置を組み立てましょう。
【 組み立てる装置 】
6. 1 組み立てに必要なパーツ
【 パーツ一覧 】
- Studuino:bit×1
- ブロック基本四角(黒)×4
- ブロック基本四角(赤)×1
- ブロックハーフB(黒)×2
- ブロックハーフC(白)×6
- ブロックハーフD(白)×4
【 アーテックブロックの形状 】
6. 2 組立説明書
以下のリンク先から組立説明書を確認しましょう。
チャプター7
光の信号で文字情報を送受信するプログラムの作成
まずは、組み立てた装置を使って、どのように1byte(8bit)の文字情報の送信や受信を行うのかを確認していきましょう。
7. 1 組み立てた装置を使った通信方法
組み立てた装置は、手動で光の信号を作る仕組みになっています。黒色のブロックを左に押すと、光センサーが露出し、反対に右に引くと隠れます。つまり、この操作で光センサーに光を通す(明るい)/通さない(暗い)を切り替えることができ、疑似的に光の点滅を再現できます。
また、コンピュータ同士で通信を行う場合は、1秒あたりに送受信できる情報量(転送速度)を互いに合わせることで、情報を欠損することなく相手まで届けています。
【 転送速度の設定が異なる場合 】
【 転送速度の設定が同じ場合 】
ですが、今回は信号を手動で送るため、コンピュータどうしのように、転送速度を正確に合わせて通信することはできません。そこで、転送速度は落ちますが、おおまかな点灯状態の時間の長さで2進数の「0」と「1」を表すようにします。1秒以上の点灯状態(光センサーに光を通す)が続いた場合は「1」とし、それよりも短い場合は「0」としましょう。
また、情報の送信を始める前には、必ず相手に対して受信開始を要求しなければいけません。ここでは、2秒以上の点灯状態を続けることで受信開始を要求し、上位の桁から情報を送るようにします。
ここまでに説明した方法で、1byteで表現可能な1文字の情報を送ることができます。しかし、通信においては、何らかの影響によって送受信中に情報が欠落したり、別の情報に置き換えられたりしてしまうケースがあります。そのため、受信した情報に誤りがないかを検出するための技術が開発されています。
その中でも仕組みが簡単でよく知られているのが「パリティチェック」という手法です。パリティチェックは情報の最後に「パリティビット」と呼ばれる1bitの情報を加えることで誤りを検出します。このパリティビットの加え方には、「偶数パリティ」と「奇数パリティ」の2つがあります。
- 偶数パリティ
送信データの各ビットから「1」の個数を数え、その個数が奇数の場合は末尾に「1」のビットを追加し、偶数の場合は「0」のビットを追加することで、「1」のビット数が偶数になるように調整します。これによって、受信データの「1」のビット数の合計が奇数だった場合に誤りを検出できます。 - 奇数パリティ
送信データの各ビットから「1」の個数を数え、その個数が偶数の場合は「1」のビットを追加し、奇数の場合は「0」のビットを追加することで、「1」のビット数が奇数になるように調整します。これによって、受信データの「1」のビット数の合計が偶数だった場合に誤りを検出できます。
パリティチェックのプログラムは簡単に追加できるので、これから作成する光通信のプログラムにも取り入れてみましょう。
7. 2 プログラムの作成手順
では、上で確認した通信方法を実現するためのプログラムを作ります。このプログラムでは、以下の2つの機能を用意します。
- ターミナルに入力された文字情報から送信用の2進数データを作成する機能
- 光の信号を受信してターミナルにその信号が表す文字情報を表示する機能
信号は手動で送りますが、送る信号を自分で計算して用意するのは大変です。1つめの機能は、その手間を省くためのものです。
では、それぞれの機能を以下の関数にまとめていきましょう。
関数名 | 機能 |
---|---|
convert_letter_to_signal() 関数 | ターミナルに入力された文字情報から、送信用の2進数のデータを作成する機能 |
receiver() 関数 | 光の信号を受信して、ターミナルにその信号が表す文字情報を表示する機能 |
■ convert_word_to_signal関数の作成
光の信号は、以下の手順で文字から変換した2進数の文字列を参考にして送ります。convert_letter_to_signal()
関数ではこの変換を行い、パリティビットを追加したデータを作成します。
【 文字からパリティビットを付加した2進数文字列への変換手順 】
では、コードを書きながら順を追ってこの変換を行うコードを書いていきましょう。
まず、ord()
関数で、ターミナルから入力された文字の文字コード(10進数の数値)を取得します。
def convert_letter_to_signal(letter):
char_code = ord(letter) # 文字コードを取得(10進数の数値)
次に、bin()
関数で2進数の文字列に変換します。bin()
関数で変換すると、先頭に2進数の表記であることを示す0b
が追加されます。今回これは不要なため、文字列のスライス操作で0b
の後に続く部分だけを取り出しましょう。
追加【3行目】
def convert_letter_to_signal(letter):
char_code = ord(letter)
binary = bin(char_code)[2:] # `0b`の後に続く2進数文字列だけを取り出す
送信する文字情報はすべて1byte(8bit)に長さに固定します。しかし、bin()
関数では数値が8bit未満で表せる場合、上位ビットの「0」は省略されてしまいます。そこで、文字列のformat()
メソッドの書式変換で、8bitになるまで上位を「"0"
」で埋め、どの文字が入力されても必ず8bitで表されるように調整しましょう。
追加【4行目】
def convert_letter_to_signal(letter):
char_code = ord(letter)
binary = bin(char_code)[2:]
binary = "{:0>8}".format(binary) # 例えば"1000001"の場合は"01000001"に調整される
最後にパリティビット(偶数パリティ)を末尾に加えて、結果を返します。
追加【6行目~11行目】
※ 数を2で割った余り(余剰の%演算子で求められる)が「1」であれば奇数と判断できます。
def convert_letter_to_signal(letter):
char_code = ord(letter)
binary = bin(char_code)[2:]
binary = "{:0>8}".format(binary)
# 末尾にパリティビットを追加
if binary.count("1") % 2 == 1: # 「1」の個数が奇数の場合
signal = binary + "1" # 末尾に「1」を加えて偶数個になるように調整
else: # 「1」の個数が偶数の場合
signal = binary + "0" # 末尾に「0」を加えて偶数個を保つ
return signal # 結果を返す
上のコードを実行して、ターミナルから関数を呼び出してみましょう。送信用の2進数データに変換できていることを確認してください。
>>> convert_letter_to_signal("A") '010000010' >>> convert_letter_to_signal("a") '011000011'
■ receiver関数の作成
次に、受信側の処理をreceiver()
関数にまとめます。
まずは、光センサーが隠れているときと、露出しているときに返す値をそれぞれ確認しておきましょう。新しく別のファイルを作成して、以下のサンプルコードを実行してください。
【 光センサーの値を習得するコード 】
import time
from pystubit.board import lightsensor
while True:
print(lightsensor.get_value())
time.sleep_ms(500)
それぞれの状態で光センサーの値が確認できたら、2つの状態を判別するためのしきい値を決めます。今回はより正確な判断が求められるため、2つのしきい値を設定しましょう。
では、元のプログラムに戻り、先頭にtime
モジュールとlightsensor
オブジェクトをインポートするコードと、決めた2つのしきい値を定義するコードを追加しましょう。
追加【1行目~5行目】
import time
from pystubit.board import lightsensor
threshold_low = 400 # 光センサーの値は環境によって変わるため、
threshold_high = 700 # 必ず自分で決めたしきい値を設定してください
2つのしきい値を使って受信処理のコードを書いていきます。
始めは、受信開始の要求を受けるまで待機します。待機中は一定時間(10ミリ秒)おきに光センサーの値を取得し、threshold_high
より値が大きい場合は、点灯状態が継続しているものと判断します。そして、この継続回数を記録することで、2000ミリ秒以上の点灯状態が続いたかどうかを確認します。もし、途中で消灯状態に変わった場合は、この回数をリセットします。
追加【20行目~30行目】
def receiver():
while True:
print("now waiting...") # "待機中" のメッセージを表示
count = 0 # 点灯状態が継続した回数を数えるための変数
while True:
time.sleep_ms(10)
val = lightsensor.get_value()
if val > threshold_high: # 点灯状態にあるとき
count += 1
elif val < threshold_low: # 消灯状態にあるとき
count = 0
継続回数が200回続いた後、消灯状態に切り替わると、24行目のwhile
文のループを抜けて、データの受信を開始します。
追加【32行目~36行目】
def receiver():
while True:
print("now waiting...")
count = 0
while True:
time.sleep_ms(10)
val = lightsensor.get_value()
if val > threshold_high:
count += 1
elif val < threshold_low:
count = 0
if count == 200: # 点灯状態が200回継続したとき(点灯状態が2秒継続したとき)
while lightsensor.get_value() > threshold_low: # 消灯状態になるまで待つ
time.sleep_ms(10)
break # 24行目のwhile文を抜ける
print("Receiving starts.") # "受信開始"のメッセージを表示
受信開始後は、送られてきた信号を順番に受け取り、2進数に変換します。点灯状態の時間の長さを計測して、1秒以上続いた場合は「"1"
」を、1秒未満の場合は「"0"
」を文字列の末尾に追加しましょう。これをパリティビットを含む9bitのデータを受け取るまで繰り返します。
追加【38行目~52行目】
if count == 200:
while lightsensor.get_value() > threshold_low:
time.sleep_ms(10)
break
print("Receiving starts.")
data = "" # 受信データを格納する変数
while len(data) < 9: # 9bitのデータを受け取るまで繰り返す
while lightsensor.get_value() < threshold_high: # 点灯状態になるまで待つ
time.sleep_ms(1)
pass
start = time.ticks_ms() # 点灯状態の継続時間の計測を開始
while lightsensor.get_value() > threshold_low: # 消灯状態になるまで待つ
time.sleep_ms(1)
pass
diff = time.ticks_diff(time.ticks_ms(), start) # 点灯状態の継続時間を取得
if diff >= 1000: # 1秒以上の場合は「1」を受信
data += "1"
else: # 1秒未満の場合は「0」を受信
data += "0"
print(data) # プログラムの動作確認のためにここまでの受信データを表示
これで9bitのデータが受信できたので、最後にパリティチェックを行います。もし、誤りが検出された場合は、エラーメッセージを表示します。誤りが検出されなかった場合は、受信データ(2進数の文字列)を文字に変換してターミナルに表示します。
【 パリティチェック後の受信データから文字への変換手順 】
追加【54行目~58行目】
if diff >= 1000:
data += "1"
else:
data += "0"
print(data)
if data.count("1") % 2 == 1: # 1の個数が奇数なら誤りがあると判断
print("Data error is Detected.") # エラーメッセージを表示
else: # 誤りが検出されなかったとき
data = data[:8] # スライスでパリティビットを削除
char_code = int(data, 2) # 10進数の文字コードに変換
letter = chr(char_code) # 文字コードから文字に変換
print("Received the letter'{}'.".format(letter)) # 受信した文字の表示
■ main関数の作成
ここまでで、convert_letter_to_signal()
関数とreceiver()
関数が用意できました。最後は、このシステムが使いやすくなるように、ボタンAを押すとターミナルから文字の入力を受け付けて、入力された文字をconvert_letter__to_signal()
関数へ渡し、返ってきた結果を表示する処理と、receiver()
関数をスレッドを立てて実行する処理をmain()
関数にまとめてプログラムの完成です。
追加【2・3行目、64行目~74行目】
import time
import _thread # マルチスレッド用モジュールの追加
from pystubit.board import lightsensor, button_a # ボタンAの制御オブジェクトのインポート
def main():
_thread.start_new_thread(receiver, ()) # 別スレッドで実行
while True:
if button_a.is_pressed(): # ボタンAをおすと入力を求める
letter = input("Input letter >>> ")
if len(letter) == 1: # エラー回避のため2文字以上入力された場合は変換しない
signal = convert_letter_to_signal(letter)
print(signal)
else:
print("Please enter only one letter.")
7. 3 プログラムの動作確認
完成したプログラムを実行して、ターミナルからmain()
関数を呼び出します。練習として以下の文字情報を送信し、正しく受信できるかどうか確かめてみましょう。
文字 | convert_word_to_signal() 関数の返り値 |
---|---|
“A” | “010000010” |
“a” | “011000011” |
”?” | “001111110” |
(実行例)
>>> main() now waiting... Input letter >>> a 011000011 Receiving starts. 0 01 011 0110 01100 011000 0110000 01100001 011000011 Received the letter'a'.
もし、どうしてもエラーが修正できない場合は、自分の書いたコードに誤りがないか、下のプログラムの完成例と1行ずつ見比べてみましょう。
【 サンプルコード 7-3-1 】
import time
import _thread
from pystubit.board import lightsensor, button_a
threshold_low = 400 # 光センサーの値は環境によって変わるため、
threshold_high = 700 # 必ず自分で決めたしきい値を設定してください
def convert_letter_to_signal(letter):
char_code = ord(letter)
binary = bin(char_code)[2:]
binary = "{:0>8}".format(binary)
if binary.count("1") % 2 == 1:
signal = binary + "1"
else:
signal = binary + "0"
return signal
def receiver():
while True:
print("now waiting...")
count = 0
while True:
time.sleep_ms(10)
val = lightsensor.get_value()
if val > threshold_high:
count += 1
elif val < threshold_low:
count = 0
if count == 200:
while lightsensor.get_value() > threshold_low:
time.sleep_ms(10)
break
print("Receiving starts.")
data = ""
while len(data) < 9:
while lightsensor.get_value() < threshold_high:
time.sleep_ms(1)
pass
start = time.ticks_ms()
while lightsensor.get_value() > threshold_low:
time.sleep_ms(1)
pass
diff = time.ticks_diff(time.ticks_ms(), start)
if diff >= 1000:
data += "1"
else:
data += "0"
print(data)
if data.count("1") % 2 == 1:
print("Data error is Detected.")
else:
data = data[:8]
char_code = int(data, 2)
letter = chr(char_code)
print("Received the letter'{}'.".format(letter))
def main():
_thread.start_new_thread(receiver, ())
while True:
if button_a.is_pressed():
letter = input("Input letter >>> ")
if len(letter) == 1:
signal = convert_letter_to_signal(letter)
print(signal)
else:
print("Please enter only one letter.")
チャプター8
課題:光の信号で色の情報を送信する
前のチャプターでは、1文字の情報をパリティビットを含めて9bitのデータとして送信しました。実際の通信では、データ量を増やすことで画像や動画など複雑な情報も送信しています。
そこで、この課題では【 サンプルコード 7-3-1 】を改造して、文字よりも多くのデータ量を必要とする「LEDの点灯色」の情報を送受信するシステムを制作してみましょう。
8. 1 色の情報の表し方
LEDの点灯色は、光の3原色から(R, G, B)
のタプルで表現することができました。Studuino:bitでは、RGBの各色の光の強さを「0~31」の数値で設定します。最大値の「31」は、2進数に変換すると「11111」です。そのため、各色の光の強さは5bitで表すことができ、パリティビットも含めて16bitが(R, G, B)
を表すときのデータ量になります。
以上のことを踏まえた上で、RGBで表された色の情報を受信すると、ディスプレイの左上のLEDがその色で点灯するように、【 サンプルコード 7-3-1 】を改造してみましょう。
8. 2 プログラムの改造手順
基本的な処理の流れは【 サンプルコード 7-3-1 】と同じですが、以下の点を変更します。(
【 プログラムの変更点 】
convert_letter_to_signal()
関数は、受け取るデータをRGBを表すタプルに変更。また、関数名もconvert_rgb_to_signal
へ変更。receiver()
関数は、受け取ったデータが表す色でLEDを点灯するように変更。main()
関数は、ボタンAが押されたときに実行する関数名をconvert_rgb_to_signal
に変更。
では、順番にコードを書き換えていきましょう。
■ convert_letter_to_signal関数の変更
まず、機能が変わるので関数名をconvert_rgb_to_signal
に変更します。そして、引数に渡されるデータが文字ではなく、(R, G, B)
のタプルに変わります。そのため、for
文で要素を1つずつ取り出し、順番に2進数の文字列へ変換します。RGBの1色の情報は長さは5bitで固定することにして、長さが足りない場合は「”0”」で埋めましょう。
変更【8行目~18行目】
def convert_rgb_to_signal(rgb):
signal = "" # 信号用の変数
for val in rgb:
binary = bin(val) [2:] # 2進数の文字列に変換後、"0b"以降を取り出す
binary = "{:0>5}".format(binary) # 5bit未満の場合は上位のビットを"0"で埋める
signal += binary
if signal.count("1") % 2 == 1: # パリティビットの追加
signal += "1"
else:
signal += "0"
return signal
■ receiver関数の変更
データを受信する処理は、全長が9bitから16bitに変更になるだけです。パリティチェックを行い、受信したデータに誤りがなければ、先頭から5bitずつスライスで取り出して、10進数に変換します。そして、その値を使って(R, G, B)
のタプルを用意してディスプレイの左上((0, 0)
)のLEDを点灯します。
変更【3行目、42行目、59行目~64行目】
import time
import _thread
from pystubit.board import button_a, lightsensor, display # LEDディスプレイの制御オブジェクトのインポート
def receiver():
while True:
print("now waiting....")
count = 0
while True:
time.sleep_ms(10)
val = lightsensor.get_value()
if val > threshold_high:
count += 1
else:
count = 0
if count == 200:
while lightsensor.get_value() > threshold_low:
time.sleep_ms(10)
break
print("Receiving starts.")
data = ""
while len(data) < 16: # 16bitに変更
while lightsensor.get_value() < threshold_high:
time.sleep_ms(1)
pass
start = time.ticks_ms()
while lightsensor.get_value() > threshold_low:
time.sleep_ms(1)
pass
diff = time.ticks_diff(time.ticks_ms(), start)
if diff >= 1000:
data += "1"
else:
data += "0"
print(data)
if data.count("1") % 2 == 1:
print("Data error is Detected.")
else:
val_r = int(data[:5], 2) # R値を取り出して10進数に変換
val_g = int(data[5:10], 2) # G値
val_b = int(data[10:15], 2) # B値
print("({}, {} ,{})".format(val_r, val_g, val_b)) # 動作の確認用に受信した色情報をターミナルに表示
display.set_pixel(0, 0, (val_r, val_g, val_b)) # ディスプレイの左上のLEDを点灯
■ main関数の変更
ターミナルから入力される情報が文字からタプルに変わります。しかし、input()
関数で取得したデータは文字列として扱われるため、ターミナルに以下のように入力しても、文字列のデータになります。
Input rgb >>> (31, 31, 31) # "(31, 31, 31)"という文字列になる
この文字列から順番に数字を切り取って、int()
関数で整数に変換して、そしてタプルとしてまとめるということもできますが、少し回りくどい感じがします。実は、Pythonには文字列の式をそのまま評価できる便利なeval()
関数が用意されています。
例えば、eval()
関数の引数に計算式を表す文字列を渡すと、その計算式を評価(演算)して、結果を返します。
>>> eval("2+3") 5
タプルと同じ書き方をした文字列を渡すと、同じく評価してタプルを返します。
>>> eval("(1, 2, 3)") (1, 2, 3) # 結果が"(1, 2, 3)"ではないので、文字列ではなくタプル
そこで、eval()
関数を使って、次のコードでターミナルから入力された文字列をタプルに変換して、convert_rgb_to_signal()
関数に渡すようにしてみましょう。
rgb = input("Input rgb >>> ") rgb = evel(rgb)
この変更を行ったコードが以下になります。入力で受け付けるタプルの要素の数は(R,G,B)
の3つになるので、要素数が3でない場合は入力エラーとして扱いましょう。
変更【72行目~78行目】
def main():
_thread.start_new_thread(receiver, ())
while True:
if button_a.is_pressed():
rgb = input("Input rgb >>> ")
rgb = eval(rgb) # 文字列を式として評価する
if len(rgb) == 3:
signal = convert_rgb_to_signal(rgb)
print(signal)
else: # タプルの要素数が3でない場合は入力エラーとして扱う
print("Enter a tuple with three elements.")
8. 3 プログラムの動作の確認
以上ですべての変更が完了しました。完成したプログラムを実行して、動作を確認してみましょう。
【 サンプルコード 8-3-1 】
import time
import _thread
from pystubit.board import button_a, lightsensor, display
threshold_low = 400
threshold_high = 700
def convert_rgb_to_signal(rgb):
signal = ""
for val in rgb:
binary = bin(val) [2:]
binary = "{:0>5}".format(binary)
signal += binary
if signal.count("1") % 2 == 1:
signal += "1"
else:
signal += "0"
return signal
def receiver():
while True:
print("now waiting....")
count = 0
while True:
time.sleep_ms(10)
val = lightsensor.get_value()
if val > threshold_high:
count += 1
else:
count = 0
if count == 200:
while lightsensor.get_value() > threshold_low:
time.sleep_ms(10)
break
print("Receiving starts.")
data = ""
while len(data) < 16: # データはパリティビットも含めて16bit
while lightsensor.get_value() < threshold_high:
time.sleep_ms(1)
pass
start = time.ticks_ms()
while lightsensor.get_value() > threshold_low:
time.sleep_ms(1)
pass
diff = time.ticks_diff(time.ticks_ms(), start)
if diff >= 1000:
data += "1"
else:
data += "0"
print(data)
if data.count("1") % 2 == 1:
print("Data error is Detected.")
else:
val_r = int(data[:5], 2)
val_g = int(data[5:10], 2)
val_b = int(data[10:15], 2)
print("({}, {} ,{})".format(val_r, val_g, val_b))
display.set_pixel(0, 0, (val_r, val_g, val_b))
def main():
_thread.start_new_thread(receiver, ())
while True:
if button_a.is_pressed():
rgb = input("Input rgb >>> ")
rgb = eval(rgb)
if len(rgb) == 3:
signal = convert_rgb_to_signal(rgb)
print(signal)
else:
print("Enter a tuple with three elements.")
操作方法:
1. main()を入力
2. now waiting… と出るので、ボタンAを押す
3. Input rgb >>> とでるので(20,20,0)など、rgbコードを入力(入力を省いてもOK)
4. 数値変換される。その後スライダーを動かしてセンサーを露出し5秒くらい待ち、隠す。
5. Receiving starts. と出たら、スライダーで0と1を合わせていく(1秒以上明るい状態で1、未満で0)
6. 完成したら色が点灯(間違ってもカラーコードが合致すれば点灯する。なければエラー)
チャプター9
おわりに
9. 1 このレッスンのまとめ
今回は、コンピュータ上で扱うデータ量の表し方と、文字データの表し方について学習しました。
数値や文字、画像や動画コンピュータ上の情報は、すべて「bit」や「byte」という単位でそのデータ量を表すことができました。
また、データは&
や|
、^
、~
の演算子を使うと、1bit単位で論理演算を行うことができました。
レッスンの後半では、実際の通信において、データが電気的な信号や光の信号に変換されて送信されていることを紹介し、光を利用して1byteの文字情報を送受信する簡単なシステムを作成しました。
テーマ.10-1と今回のレッスンを通して、コンピュータが内部でどのようにしてデータを取り扱っているのかを見てきました。難しい内容も多かったと思いますが、プログラミング言語を覚えるだけでなく、その根底にあるコンピュータの仕組みを理解することも、良いエンジニアになるためには必要です。
テーマ.10-3、テーマ.10-4とまた少し難しい話が続きますが、諦めずに取り組みましょう。
9. 2 次のレッスンについて
次回のレッスンでは、データを整理して保管する「データベース」の技術と、特定の文章のパターンを検索するときに使われる「正規表現とマッチング」の技術を学びます。