nyanpyou Note

主な目的は調べたり作ったりしたプログラミング備忘録(予定)

そうだ温度計を作ろう その1

約4か月ぶりです。ご無沙汰しております。

なぜに温度計か

そうだ温度計を作ろう(唐突)
と思い立ったのは去年の10月の終わり頃。なんで急にこんなことを思い立ったのかと言いますと、実はこの辺りにPCを新調しました。今までノートPCしか持ってなかったんですね。趣味で最も使う期間が長いものなのに、いくらゲーミング仕様とはいえ古いノートPC使い続けてるのってどうなのと思い、2~3週間パーツを吟味し天下のドス〇ラさんで購入を決断。いきなり自力は不安があったため組み立てまで合わせて依頼しました。

f:id:nyanpyou106:20210131102358j:plain
でかい(当社比) そして光る 後ろがごちゃごちゃしているのは許し亭許して

かくしてそれなりにお金をかけた大き目なPCが部屋に来ることになったわけなのですが、このPCが部屋に来てからというものの、なんか部屋が暑い。冬に入りかかっているというのに暖房無くてもいいんじゃないかってくらい暑かったんですね。で、一体この部屋どんくらいの温度になってるか確かめたい→そのために部屋用の温度計が欲しい→せっかくだから自分だけの温度計作ってみるか!となったわけです。

本当は作成の進捗具合を逐一記事にしてもよかったのですが、いつも通りわからないことを少しずつ調べながらの作成になるため、きちんとした文章にまとめる作業を並行すると負担が大きいだろうと思ったことに加え、一旦やり始めたはいいものの完成しなかった場合に、あまりにも格好がつかないこともあり今までブログに書くことはしていませんでした(結果4か月空き)。

今回やっとこさ形になったため、制作過程を書き連ねていきます。全3回です。

温度計の全コードは以下のリポジトリに置いてありますので、興味のある方はご覧下さい。

github.com

設計案

やりたいこと

  • 室温を定期的に取得する。
  • 外気温を定期的に取得する。
  • 取得した温度を表示する。
  • デザインは自分の趣味全開にする。

構成(ハードウェア等)

  • Raspberry Pi
    • 自分用が改めて欲しかったから(今まで触ってたものは実は職場に転がってたやつ)
  • 適当な温度センサー
    • ラズパイで温度測定は定番だし、探せばなんかいい感じのあるでしょ(見切り発車)
  • 適当な小型タッチパネル液晶
    • タッチ操作できると便利だし面白い。これも探せばなんかあるでしょ(見切り発車)
  • いい感じのケース
    • 液晶、Raspberry Pi、温度センサーをひとまとめにできるもの。イメージに合うものが売られていればそれを用いるが、見つからなければ自作も視野。
  • その他温度表示画面に使う画像等
    • 動画から切り抜いたり、フリーの素材を組み合わせたりして気合で集める。残念ながら1から作れるような絵画デザインスキルは無い。

構成(ソフトウェア等)

  • Python
    • ラズパイで電子工作をする際の定番で日本語の参考情報が多い&普段使いで慣れているという理由で選択。
  • HTML+JavaScript
    • 一番やりたかった部分。定番のTkinterとかPySimpleGUIを使わなかった理由は、前に使った時に自分好みのデザインを直感的に作ることが難しいと感じたため。ならばどうやって作るか…やっぱり綺麗な見た目を作るといえばHTML+CSS+JavaScriptでしょ、という理由で選択。
      実は1から自分で書いたことが無かったので(既存のコードをちょっと弄るくらい)、この機会に使えるようにしたかったというのも理由の一つ。

見切り発車に加えてやりたいこと組み合わせただけみたいな構成だが、個人作成なら許されるはず。やりたいことやったもん勝ちって光GENJIも言ってっから。
それに作るものに自分が興味持てないと面白くないし続かないからね、仕方ないね(レ)

物資調達

Raspberry Pi

Amazonなどでも購入できるが、1台目はやはり正規代理店で購入するのが安牌だろうということで、RSコンポーネンツが販売するスターターキットを購入。RSコンポーネンツは法人向けの代理店と紹介されていたりもするが、個人の注文も問題なく受け付けて貰えた。しかも早い。注文から1時間くらいで出荷の連絡が入り、次の日に届いた。Amazon顔負けのスピード感である。

Pi-4gb-StarterKit | Raspberry Pi4 B 4GBスターターキット | RS Components

f:id:nyanpyou106:20210131101502j:plain
シャレオツな箱に入って届くので満足感がある

温度センサー

ラズパイで使える温度センサーモジュールを調べた結果、SHT31とBME280が良く使われていることが分かった。両者の測定精度と値段、測定可能な環境情報を比較し、どちらを使用するか検討した。

SHT31の温度の測定精度は±0.2℃で、温度・湿度の測定が可能。秋月電子では950円で購入可能。

SHT31使用 高精度温湿度センサモジュールキット: 組立キット 秋月電子通商-電子部品・ネット通販

BME280の温度の測定精度は±1℃で、温度・湿度・気圧の測定が可能。秋月電子では1080円で購入可能。

BME280使用 温湿度・気圧センサモジュールキット: センサ一般 秋月電子通商-電子部品・ネット通販

いずれも基盤への取り付けがされているため、ラズパイのGPIOに接続すればすぐに使用可能である。
今回は温度を測ることが目的で、気圧の情報は不要なためSHT31を選択することにした。気圧測定機能が無い分値段も安い上に精度も高いので割と即決。

f:id:nyanpyou106:20210131101815j:plain
但しピンヘッダのはんだ付けは必要

小型タッチパネル液晶

こいつの選定が一番苦労した。「Raspberry Pi タッチパネル」を検索ワードにしてgoogle大先生で調べると物自体は色々出てくるのだが、今回必要な要件を満たすものが本当に中々見つからない。
というのも、今回必要な液晶は次の3点を満たす必要があった。

  1. GPIOを埋めないこと
  2. 5インチ以下のサイズであること
  3. (欲を言えば)裏側にラズパイをマウント可能であること

1が最重要で、理由は今回温度センサーを繋ぐために使用する予定があるから。しかしタッチ機能のためにGPIOを使用する製品がほとんどで、加えて多くの製品は使わないGPIOまで埋めてしまう構造になっていた。Amazonやラズパイのパーツを製造販売している各種企業(Adafruitとか)の販売サイトを探し回ったが、これだという製品が中々見つからない。大変困ったが、ダメ元で見に行った日本橋千石電商で、全ての要件を満たすタッチパネルが売られていることを発見。即決で購入しこれを使用することにした。

4.3inch Capacitive Touch Screen LCD, IPS, HDMI, Multi mini-PCs, Multi Systems

f:id:nyanpyou106:20210131104219j:plainf:id:nyanpyou106:20210131104223j:plain
USB接続でタッチパネルが使用可能&丁度いい大きさ&裏側にラズパイをマウント可能
さらにマウントしたラズパイに接続するための小型コネクタ(USB&HDMI)も付属と至れり尽くせり

(残念ながら千石電商では2021/1/30現在欠品中らしい https://www.sengoku.co.jp/mod/sgk_cart/detail.php?code=EEHD-5L6Y)

やっぱりそれなりにお値段のするパーツ(この液晶は6000円した)は実際に実物を見て買うのが良い。今回も自分で実物を触れたからこそ、これならいけるだろうとある程度の確信を持って購入できた。この液晶はサンプルが店頭に出ておらず、箱に入った状態で売られていたのだが、実際に中身がどれくらいのサイズ感か見たいと店員の方に相談したところ、快くその場で中を開けて見せて頂けた。店内に種々様々な電子部品が置いてあるので、眺めているだけでも面白い。

ケース&画像

これらについては後程記載。

次回へ続く

久方ぶりに文章を書き始めたら、文章力が深刻に低下していて死にかけた(半日消費)。
その2はソフトウェア編。

Pythonで送信したメールの添付ファイルが正しく見えない件について

以前書いたPythonを用いたメール送信方法を一斉送信を行う際に利用していたが、問題が発覚したのでその内容と解決方法についてメモ。

症状

Pythonで添付ファイル付きのメールを送り、そのメールをWindows10メールで受け取ると添付ファイルが見えない。Outlookで受け取ると無題の添付ファイル.datとなる。
(添付ファイルの中身がおかしくなるのではなく、添付ファイル自体がそもそも存在しないメールとして受信される。メールソースを見てもやはり添付ファイルの部分が存在しない。)
添付ファイル付きメールとして送れていないわけではないようで、Thunderbirdで受信すると問題なく添付ファイルが添付されているし、ファイルの中身も確認できる。
全く同じ文面のメールをThunderbirdで作成して送信した場合は、問題なく添付ファイルと共にメールが届く。

原因

本現象についてネットを彷徨い調べてみると、どうもメールソース中の添付ファイルの名前の書き方が問題らしいということが分かった。

qiita.com

tearoller.tea-nifty.com

moji-memo.hatenablog.jp

sendgrid.kke.co.jp

これらの情報によると、Windows10メールやOutlookといったMicrosoft製のメーラーで日本語で名前を付けた添付ファイルを確認するためには、Content-Typeのname部分にBase64エンコードされたファイル名が書かれている必要があるそうだ。
その際の書き方は以下のようにする必要がある

=?文字コード?エンコード方式?エンコード後の文字列?=

文字コードの部分にはUTF-8の場合はutf-8エンコード方式の部分にはBase64を用いる場合はbが入る。試した限りでは大文字小文字は区別しないようだった。
但しこれは技術仕様を定めたRFC的には正しくない書き方で、Content-Typeに添付ファイルの名前を書いておく必要は本来はないらしい。
20年ほど前に書かれたものであるが、電子メールに関するRFCをまとめているページがあった。量が多くて原文を読めていないものの、以下に書き留めておく。

www.emaillab.org

今回RFCというものを初めて知ったが、IETFという技術の標準化を推進する団体がまとめている文書だそうだ。

e-words.jp

つまりまとめると、RFCに則るのであれば、メールソース内のContent-DispositionにUTF-8でファイル名が書かれていればそれでよい。が、それでは添付ファイルを認識してくれないメーラーも存在するため、別途対処が必要ということだ。
(そんなんわかるか!w必ずしも従う必要のないルールとはいえ、規格は統一しといて欲しいと本当に思う。)
以上のことが分かったので、Pythonで送ったメールと、Thunderbirdで送ったメールの2つについて、メールソースを確認し、添付ファイルの情報が書かれている部分を実際に見比べてみる。どちらのメールも、添付ファイルには添付ファイル.pdfという名前のファイルを付けている。

#使用したPythonプログラム(再掲)
import smtplib, ssl
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from os.path import basename
from getpass import getpass
 
# SMTP認証情報
account = "メールを送信させたいアドレス"
password = getpass("email password?:")
 
# 送受信先
to_email = "送り先"
from_email = "送り主"
 
# MIMEの作成
subject = "送信テスト"
message = "Pythonプログラムから送信しています。"
msg = MIMEMultipart()
msg.attach(MIMEText(message))
msg["Subject"] = subject
msg["To"] = to_email
msg["From"] = from_email

file_path = "添付したいファイルのパス"
with open(file_path, "rb") as f:
     part = MIMEApplication(f.read(), Name=basename(file_path))
 
part['Content-Disposition'] = 'attachment; filename="%s"' % basename(file_path)
msg.attach(part)
 
server = smtplib.SMTP_SSL("smtpサーバーのアドレス", ポート番号, context=ssl.create_default_context())
 
server.login(account, password)
server.send_message(msg)
server.quit()
#Pythonで送信したメールをThunderbirdで受け取った場合
Content-Type: application/octet-stream;
 Name*=utf-8''%E6%B7%BB%E4%BB%98%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.pdf
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: =?utf-8?b?YXR0YWNobWVudDsgZmlsZW5hbWU9Iua3u+S7mOODlQ==?=
 =?utf-8?b?44Kh44Kk44OrLnBkZiI=?=
#Thunderbirdで送信したメールをThunderbirdで受け取った場合
Content-Type: application/pdf;
 name="=?UTF-8?B?5re75LuY44OV44Kh44Kk44OrLnBkZg==?="
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
 filename*0*=UTF-8''%E6%B7%BB%E4%BB%98%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB;
 filename*1*=%2E%70%64%66

Thunderbirdで送信したメールは確かにContent-TypeのnameにはBase64エンコードされたファイル名が、Content-DispositionにはUTF-8のファイル名が書かれており、このメールはThunderbirdでもWindows10メールでも添付ファイルを見ることが出来る。それに比べ、Pythonで送ったメールのソースコードは随分内容が異なっていることが初めて分かった。(やっぱりブラックボックスをコードに残すのは良くないと再認識・・・)
よってコードの見直しを行い、Thunderbirdで作成したメールと同じソースになるようにプログラムを修正することにした。

修正

修正したコードは以下のようになる。

import smtplib, ssl
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from os.path import basename
from getpass import getpass
 
# SMTP認証情報
account = "メールを送信させたいアドレス"
password = getpass("email password?:")
 
# 送受信先
to_email = "送り先"
from_email = "送り主"
 
# MIMEの作成
subject = "送信テスト"
message = "Pythonプログラムから送信しています。"
msg = MIMEMultipart()
msg.attach(MIMEText(message))
msg["Subject"] = subject
msg["To"] = to_email
msg["From"] = from_email

file_path = "添付したいファイルのパス"

#ファイル名をBase64エンコードしてその結果を文字列に変換する
base64encoded_filename = base64.b64encode(basename(file_path).encode("utf-8")).decode()
        
with open(file_path, "rb") as f:
    part = MIMEApplication(f.read(), name="=?utf-8?b?{}?=".format(base64encoded_filename))

#Content-Dispositionに情報を記載する。ちゃんと改めて公式ドキュメントを調べたら専用のメソッドがあった。
part.add_header("Content-Disposition", "attachment", filename="{}".format(basename(file_path)))
msg.attach(part)
server = smtplib.SMTP_SSL("smtpサーバーのアドレス", ポート番号, context=ssl.create_default_context())

Base64エンコードする部分を細かく見ると以下の手順でエンコードが進む。

x="添付ファイル.pdf".encode("utf-8")
>>x
>>b'\xe6\xb7\xbb\xe4\xbb\x98\xe3\x83\x95\xe3\x82\xa1\xe3\x82\xa4\xe3\x83\xab.pdf'
y=base64.b64encode(a)
>>y
>>b'5re75LuY44OV44Kh44Kk44OrLnBkZg=='
z.decode()
>>'5re75LuY44OV44Kh44Kk44OrLnBkZg=='

Base64はデータをASCII文字(半角英数字などで構成される128文字)に変換するためのエンコード方式である。

qiita.com

decode()は引数に何も指定しない場合utf-8でデコードを行うが、ASCIIを拡張した文字コード(頻繁に用いるUTF-8やShift-JISは全てASCIIを拡張した文字コード。今回初めて知った。)は、ASCIIに含まれる文字については全てASCIIと同じコードのため、ASCIIを引数に指定しなくてもstr型となった半角英数字を得ることが出来る。UTF-8を指定してもShift-JISを指定しても結果は同じ。
以上の手順を踏むことでstr型のエンコード済みファイル名が手に入る。

教訓

コードの不明点は残さないようにしましょう。

余談

これを書くために追加で、手持ちのメールアドレス何個かを用いてPythonから送ったメールの受信を試してみたが、なんとhotmailのアドレスに送った場合は元々のプログラムのままでもWindows10メールで添付ファイルが表示されることを確認した。メーラーだけでなく使うアドレスにもどうやら依存するらしい・・・まだまだ謎は多い・・・