t-hom’s diary

主にVBAネタを扱っているブログです。

Raspberry Piでネットワーク対応の電光掲示板(16×96ドット)

今回はRaspberry Pi Zero WHで電光掲示板を動かしてみた。
ただ動かすだけではなく、無線LANに接続して他の端末から受信したメッセージを表示させる。

私がこれを作成した目的は、前回紹介した空気モニターの内容を掲示板に常時表示させるためである。
※前回記事
thom.hateblo.jp

作成するもの

動作イメージはこんな感じ。
f:id:t-hom:20201202221347g:plain
※mp4からGIFアニメ化したときにかなり高速になってしまったけど、実物はだいたいこれの3分の1くらいのスピードでスクロールする。

色々と調べた結果をベースとしてプログラムで空気品質の基準値を定めていて、基準内の項目は緑の文字で、閾値付近はオレンジで、基準オーバーは赤の文字で表示させるようにした。

材料

Raspberry Pi Zero WH

最初はArduinoで動かすことを考えたが、掲示板のドット数からしてメモリが不足するという情報があったのでラズパイにした。
最初はRaspberry Pi 3B+で実験していたが、以前購入したまま眠っているZero WHがあるので私はそちらを使用した。
無線LANに対応したラズパイがあれば何でも動くと思う。

LEDマトリクスパネル(16x32を3枚)

ラズパイZero用ユニバーサル基盤

16ピンソケット(オス) ボックスヘッダー

ビニール被膜の単線ワイヤー

電気ワイヤーは単線タイプと撚り線タイプがある。
針金のように一本だけ入ったのが単線タイプで、細い線が複数ねじりあわされたのが撚り線タイプ。
撚り線のメリットは柔らかいことであるが、今回は配線が細かくてカチっと位置決めしないとハンダ付けしにくいので単線タイプを使用する。

熱収縮チューブ

これはLEDマトリクスに電源を供給するためのコードを改造する為に使用した。
f:id:t-hom:20201202225627p:plain

LEDマトリクスに付属の電源コードは1本がパネル2枚分に枝分かれしていて、それがLEDパネル1枚に1セット入っている。
1本で2パネルなので2本使えば良いのだが、そうすると1パネル分あまって邪魔だし、大本の電源は1本化したいので、1本を3パネルに分岐させるようなコードを作成した。

アルミチャネル材

これはホームセンターとかで売ってるコの字型の長いアルミ棒。
2本買って、3つのパネルを連結するのに使用した。なんでも良いと思うけど私が使ってるのは以下の商品のようだ。(JANコードで検索した)
tcss.vivahome.com
f:id:t-hom:20201202224145p:plain

M3ネジ(長さ8mm)

LEDパネルとアルミ材を連結する為のネジ

工具

アルミ用ヤスリ

切断面を危なくない程度に仕上げるために使用。
私の場合はパネルの上用と下用で切断する長さが2ミリほどずれてしまったのでヤスリで削って合わせた。
たった2ミリだけどヤスリで削るのはとても時間がかかった。。

ボール盤

今回これのためにボール盤を購入。評価は賛否分かれてたけど、普通に使用できて満足。
代用としてはセンターポンチと手持ちタイプの電動ドリルで穴開けする方法もあるが私は何度やってもズレる。これでアルミ材を1つダメにした。
これはネジで止める固定用の穴なので、ズレると3枚のLEDにスキマができたり上下ズレたりで綺麗に1枚にならない。
1万ちょっとでこの手の工具が購入できるということでテンションが上がったこともあり、そのままポチってしまった。

ただ付属のバイスでは縦方向の材料の位置合わせが難しく、他のアルミ材を一緒に挟んで位置を調整したりと、位置合わせにとても苦労した。
細かい位置合わせ用のテーブルが別途あるようなのでそういうのを買うと良いのかもしれない。
ちなみにこの会社、ちゃんと日本で検品してから出荷してくれてるらしく、一度開封されているので未開封品ではない。
また、本体の袋が機械油まみれで届くので新聞紙を敷いたりキッチンペーパー等で余計なところに付いた油を取ったり色々汚れる覚悟は必要。

使用上の注意を守らないとかなり危ないので、一般的なボール盤の注意点や、この製品の注意点についてよく調べてから使うこと。
特に油まみれになるので軍手とかしたくなるが、巻き込み事故の原因になるので絶対に素手で扱うこと。
金属片が飛んでくる恐れがあるのでセーフティーゴーグルをすること。

鉄工用の丸軸ドリルビット

3ミリを使用した。

アルミ用ってのがあればそれで良いけどホームセンターで見つけられなかったので鉄工用を購入。

ハンドドリル用だと六角軸のものが主流で、そのまま使えなくはないがおススメはしない。
ボール盤のドリルチャックは3つの歯でドリルをくわえるような形になっていて、無理な力がかかった時に丸軸ドリルがスリップして本体の破損を防ぐらしい。六角だと滑れないので本体がダメージを受ける。

作成手順

配線計画

細かい配線の前にまずは全体図。
f:id:t-hom:20201203001448p:plain

ラズパイに接続されたユニバーサル基盤に、データ信号用のボックスソケットと電源用のネジ端子が接続されていて、
ネジ端子から各LEDパネルへ電源が供給されている。データ信号はLEDパネルのInputから入り、Outputから出た信号が次のパネルのInputへ接続される。それぞれのパネルをフラットケーブルで接続する際に、Output→InputとつなげばOK。

次にボックスコネクターとラズパイのピンの対応について説明する。
ボックスコネクタをアップで確認するとそれぞれのピンに記号が書かれているが、残念ながら基盤が隠れて右半分しか確認できない。
f:id:t-hom:20201203003333p:plain

ラズパイとの対応も併せて、残りはこちらのサイトで確認することになる。
github.com

最初に出てくる画像は忘れて構わない。2つ目のこれ↓がLEDパネルのInputピンアサインである。
f:id:t-hom:20201203003544p:plain

つまり切り欠きを左としたときに、次のようなピンアサインになっている。
f:id:t-hom:20201203004056p:plain

次に、以下の図を確認する。LEDパネルはラズパイから最大3レーン接続でき、それぞれのレーンに最大12枚ずつチェーンできる。
f:id:t-hom:20201203004449p:plain
今回は1レーンしか使わないので、ニコチャンマークに注目する。

次にラズパイ側のピンとの対応図が出てくる。
f:id:t-hom:20201203004735p:plain

そのままExcelに貼り付けて、余計な記号等を消し込む。
f:id:t-hom:20201203004919p:plain
10番ピンの(for 64 row matrix, 1:32)と、22番ピンの(for 32 row matrix, 1:16)はどちらも消してOK。
rowってのが行数を表すが、今回買ったLEDパネルは16行x32列なので関係ない。

ボックスコネクタ側であるが、GNDはおそらく3か所中1か所繋いでおけばOKなのと、Dは今回使用しないので実際には次のようになる。
f:id:t-hom:20201203005801p:plain

あとはユニバーサル基盤にラズパイと重ね合わせるためのピンヘッダと16ピンソケットを取り付けて、配線の対応どおりに繋いでいくだけ。
f:id:t-hom:20201203013255p:plain


繋ぐ前からひどいことになるのは見えているわけだが。。根気よく頑張る。
f:id:t-hom:20201203013611p:plain

実際の基盤上の配線

ユニバーサル基盤上の配線が完成するとこんな感じになる。

裏面
f:id:t-hom:20201203002101p:plain
※実はユニバーサル基盤の裏と表を間違えたっぽい。。せっかく5Vとかのラズパイのピンアサインが書かれているのに上下左右逆になってしまい余計配線で混乱することになった。

表面
f:id:t-hom:20201203013805p:plain

対応表を見ながらハンダで繋いでいくのだが、ハンダをつけるのに何度も裏返すので、途中で頭がこんがらがってくる。
これだけで3時間かかった。めちゃくちゃ大変。

※実は自分で作らなくても専用ハットが売ってるらしいんだけど、もともと参考にしたサイトがそういうのを使わずにジャンパーピンで全部つないじゃったということだったので、真似してうまくいった成功体験からハットは使わずに自分で作った次第。皆さん真似される方はぜひハットを購入して、それをまた記事にしていただけたらと。。

ちなみにパネル用の電源はラズパイの5Vピンと、余っているGNDピンからネジ端子まで引っ張ってきている。
f:id:t-hom:20201203010507p:plain
ネジ端子の基盤に挿すピンは太くてそのままでは挿せないので、ドリルで少し穴を広げてそのまま配線用のハンダで固定した。

電源コードの配線は配線図通りで良いので写真での解説は割愛する。赤線は赤線同士、黒線は黒線同士くっついていればそれで動くので適当に切断してしかるべき場所の被膜を剥いて熱収縮チューブを通してから半田づけで線同士を結合し、熱収縮チューブをハンダごてで撫でて収縮させて金属部分を覆えば完成。

ラズパイのセットアップ

ラズパイゼロには、Raspberry Pi OS Liteをインストールしておく。
これはGUIを持たないコマンドだけのOSで、余計なリソース消費が無いのでIoTにはおススメ。

Micro USBにOSを書き込んだ後、所定のフォルダにWifi設定を記述したテキストファイルを入れておくとモニターもキーボードも繋ぐことなくインストールできる。「ラズパイZero ヘッドレスインストール」などと検索すると方法が見つかるかと思う。

セットアップして起動まで終わったらSSHで接続して、sudo apt-get update、sudo apt-get upgradeを済ませておく。
メモリが少ないせいか、割と時間がかかる。

次にライブラリ等の入手とインストールを実施。
gitがインストールされてないので、そのインストールコマンドも書いておく。gccは入ってたかどうか忘れたけど一応。。

cd /home/pi/
sudo apt-get install git
sudo apt-get install gcc
git clone https://github.com/hzeller/rpi-rgb-led-matrix/
cd rpi-rgb-led-matrix
make -C examples-api-use
cd /home/pi/
sudo pip3 install Pillow
sudo pip3 install feedparser
sudo apt-get install ttf-takao-mincho

そしてサンプルを実行

sudo  /home/pi/rpi-rgb-led-matrix/examples-api-use/demo --led-no-hardware-pulse --led-rows=16 --led-cols=96 -D 1 -m 20  /home/pi/rpi-rgb-led-matrix/examples-api-use/runtext16.ppm

実行するとこんな感じで文字が流れていく。
f:id:t-hom:20201203012818g:plain

電光掲示板側のプログラムコード

いろんなところからお借りしたコードを混ぜてるので余計なインポートが残ってると思うけど気にせず公開。。
このコードはソケット通信でデータを待ち受けて、データが来たらppmファイルを生成し、先ほどのテキストスクロールデモを開始して作成したppmファイルを再生させている。
データはUnicode文字列・RGBコードで構成されたリストをjson形式で受け取る形式である。ちょうどコード中に「電光掲示板テスト」と書かれているあたりがデータ形式である。

注意点として、コード中のIPアドレスの部分は自分のラズパイのアドレスを入れること。
50007は待ち受けポートなのでそのままでも良い。

プログラムファイル名はserver.pyとしておく。

import socket
import json
from subprocess import Popen
from time import sleep
from PIL import Image, ImageFont, ImageDraw

def createPPM(text):
    font = ImageFont.truetype("/usr/share/fonts/truetype/takao-mincho/TakaoMincho.ttf", 16)
    all_text = ""
    for text_color_pair in text:
        t = text_color_pair[0]
        all_text = all_text + t
    
    width, ignore = font.getsize(all_text)
    im = Image.new("RGB", (width + 30, 16), "black")
    draw = ImageDraw.Draw(im)
    
    x = 0;
    for text_color_pair in text:
        t = text_color_pair[0]
        c = tuple(text_color_pair[1])
        draw.text((x, 0), t, c, font=font)
        x = x + font.getsize(t)[0]
    
    im.save("/home/pi/message.ppm")
    return Popen(["exec /home/pi/rpi-rgb-led-matrix/examples-api-use/demo --led-no-hardware-pulse --led-rows=16 --led-cols=96 -D 1 -m 20 /home/pi/message.ppm"], shell=True)


proc = createPPM([
    [u"    " + "電光掲示板テスト",[255,255,0]],
    [u"    " + "これは初期メッセージです。",[0,255,255]],
    [u"    " + "ネットワーク経由でメッセージを送付してください。",[255,0,255]]])

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(('192.168.1.5', 50007))
    s.listen(1)
    while True:
        try:
            conn, addr = s.accept()
            with conn:
                data = conn.recv(1024)
                if not data:
                    break
                proc.terminate()
                proc = createPPM(json.loads(data.decode('utf-8')))
                conn.sendall(b'Received: ' + data)
        except KeyboardInterrupt:
            proc.terminate()
            s.close()

クライアント側のコード

これは同一ネットワーク上に存在している別のPCなり別のラズパイから実行させるコードである。
ファイル名はclient.pyとしておく。

import socket
import json

msg = ((
	(u"    " + "オレンジ色のメッセージテスト",(255,165,0)),
	(u"    " + "ライムグリーン色のメッセージテスト",(50,205,50)),
	(u"    " + "ターコイズ色のメッセージテスト",(64,224,208))))

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
	s.connect(('192.168.1.5', 50007))
	s.sendall(bytes(json.dumps(msg), 'utf-8'))
	data = s.recv(1024)
	print(repr(data.decode('utf-8')))

実行

まずラズパイゼロでサーバープログラムを実行する

sudo python3 server.py

※一般ユーザーでも実行できるけどなぜかLEDがちらつく。おそらくsudoで安定するのはリソースを優先的に使用できるのではないかと思われる。

これでサーバー側はデータ受信待ちになる。

次にクライアント端末からクライアントプログラムを実行する。

python3 client.py

これでクライアントプログラムで指定した文字が、指定した色で電光掲示板に表示されたら成功。
サーバーは起動しっぱなしでOK。

クライアントのメッセージをいじって再度実行すれば、表示メッセージも変わる。

実行時のトラブル対応

サーバープログラムは記述ミスやなんらかの原因で終了したりすると、demoというプロセスが残り続け、pythonコードが終了しても掲示板メッセージが消えない。この状態で再実行すると、表示が重なったような感じで実行されてしまう。
こうなったらdemoプロセスを終了させればよいので、「ps -All | grep demo」でPIDを特定し、「sudo kill PID」で終了させると改善する。

GUIが無いので基本的にコマンドでなんとかするしかない。Linuxの基本的なコマンド(特にリブートやシャットダウンなど)はここでは解説しないので検索するなどしてトラブルに対処してもらえると良いかと思う。

組立と設置

これも割と苦労したポイントではあるが、どちらかというと材料と加工ツールの選定に時間を取られただけなので、ここでの説明は簡単に。

まずアルミチャネル材をLEDパネル3枚分の長さに切断する。これを2本用意する。
私の場合はあとで壁にネジで取り付ける想定で、LEDパネルよりも4センチほど長めにした。
結局壁の有孔ボードのフックにちょうど引っかかったので、今後ネジ止めするかどうかは不明。

次にヤスリで切断面を均す。

穴を空ける位置に油性マジックで印をつけ、ボール盤で穴あけ。

8ミリ長のM3ネジでアルミチャネルとLEDパネルを止めていく。
LEDパネル側がネジ穴になっているので特にナットとかは使わなくてOK。

任意の方法で壁に取り付けて完成。

主な参考サイト

qiita.com
digirakuda.org

以下再掲
github.com

以下はそもそもこれをやりたくなったキッカケ
dailyportalz.jp
Arduinoでビットパターン書いてるっぽいんだけど、コードの掲載はないのでどうやってるのか不明。

後書き

モノが完成してから記事にするまでずいぶんと時間がかかってしまった。記事を書き終わるまで大体4時間。なかなか大変な作業であるが、人に説明するとなってようやく理解できることもある。
たとえばLEDとラズパイのピン接続について、最初私が作るときは英語サイトで書いてる内容はあんまりちゃんと読んでなくて、ほぼQiitaの丸コピで作ったんだけど、私が意味を分かってないままここで再掲しても単なる劣化版記事になってしまう。わざわざ書くからには、先人が書いた記事を参考にしつつもそこに何等かの付加価値をつけたいと思い、少し余分に解説してみた。

これを見て自分でやりたくなった誰かがまた別の切り口で記事を書き、解説レベルが上がることで真似する方もハードルが下がって応用例が増え、それをまた誰かが真似て発展させるという好循環が生まれると良いなと思う。

以上

Arduino Pro MicroとラズパイとAmbientで空気モニター(温度、湿度、気圧、PM2.5、CO2)

今回は以前プロトタイプを作成した環境センサーを普段使いできる形に整えていく。

以前の記事はこちら。
thom.hateblo.jp

作成するもの

前回のハード

f:id:t-hom:20201108113845p:plain
前回はこんな感じでブレッドボードにセンサー類を挿していただけなのですべてむき出し。
普段使いするにも邪魔になる。

今回のハード

f:id:t-hom:20201129122733p:plain
今回はケースに収めて壁に設置した。見た目にもスッキリしてとても良い。

前回のソフト

f:id:t-hom:20201108114614p:plain
前回はArduinoのシリアルモニタに表示させただけ。PCで常にシリアルモニターを開いていないと見えない。

今回のソフト

f:id:t-hom:20201129123252p:plain
今回はラズパイ経由でAmbientというクラウドサービスに連携することでデータをグラフ化することにした。

材料

センサー類

センサー類は前回の記事と変更ないので説明を省略する。

Arduino

PS4コントローラーの記事では3.3V版だったが、今回は5V版のPro Microを使用した。
二酸化炭素センサーとホコリセンサーが5Vで動作するためである。
※温湿度気圧センサーはレギュレーターが入っているので入力は5Vでも3.3Vでもどちらでも動作するらしい。

ミニブレッドボード

これはArduino Pro Microを差し込む用のブレッドボード。
裏が両面テープになっているのでケースに張りつけられる。

あと、名刺くらいのサイズのブレッドボードの電源ラインだけ分離させて、電源集約用の材料として使っている。

ケース

タカチ電機工業の「PF15-5-15W」というケースを選択。
これ、型番で検索してもドンピシャでヒットせず、シリーズのページにたどり着くので割と困る。
写真と商品が違ってたりするので、同じ物を購入する場合は型番とサイズに注意。
f:id:t-hom:20201129125027p:plain

あとPFとPPFは似てるけど形状が違うのでそこも注意。

ケースのサイドパネル

サイドパネルは付属しているが、穴あけ加工に失敗すると取り返しがつかないため最初から3Dプリンターで作成することにした。
f:id:t-hom:20201129125648p:plain

寸法はこちらのサイトで確認できる。
https://jp.misumi-ec.com/vona2/detail/222005614912/?HissuCode=PF15-5-15W

接着式基盤スタンド

サンハヤトのSPT-300を使用した。
https://www.sunhayato.co.jp/material2/afp03/item_1115

これはセンサー類をケースに固定するパーツである。
両面テープでケースに貼り付けることで、取り付け用のネジ穴になる。

センサーケーブル類

センサ類のケーブルが長いとごちゃっとするので圧着端子と圧着工具を買ってきて自分でジャンプワイヤーを作った。

ワイヤ

ワイヤストリッパー(被膜を剥く工具)

コネクタ

圧着工具

あと埃センサーは日本圧着端子のEHコネクター5ピンで接続されてたのでそちらも購入。
http://www.jst-mfg.com/product/pdf/jpn/EH.pdf

既存のケーブルを切っても良かったけどもったいない気がして。。これは上記のコネクタとはサイズが違う為EHコネクタ用の圧着端子が別途必要である。大阪のシリコンハウス(実店舗)でケーブルに圧着済のものが売られていたので、コネクタと圧着済ケーブルを購入した。

USB

ケースにUSB差し込み口を作りたいのでUSB Micro-DIP化基盤も準備

基盤からケース内部でUSBケーブルに戻してArduino Pro Microに接続するためのアダプタケーブル

ケースファン

ケースファンは冷却の為ではなく、ケース外の空気を取り込んで埃センサーを正しく反応させるためのもの。

ホットボンド

これはケースのサイドパネルにケースファンとUSB基盤を取り付けるのに使用した。
本当は3Dプリンターで固定用の機構まで出力できれば良いのだが、そんな技術力がないのでホットボンドで代用。

配線図

f:id:t-hom:20201129132516p:plain

ピン番号等はほとんど前回と同じであるが、Arduino Pro MicroはSDA、SCLが2番・3番に割り当てられているので、そこだけ違う。
ピンアサインは以下のサイトで調べられる。
qiita.com

実際の配線

f:id:t-hom:20201129140746p:plain

まずケース内にパーツを仮配置してブレッドボード・接着式基盤スタンドを貼り付けてからセンサー類を固定。
その後、ケーブルの長さを決めて圧着し接続。
ケースファンとUSB受け口の基盤はそれぞれホットボンドで固定すれば完成。

サイドパネルの3Dモデリング

Autodesk Fusion 360は非商用ならも無償で利用できる。
本格的な解説はしないけど、雰囲気だけ紹介する。

まず、スケッチ作成ボタンをクリックする。
f:id:t-hom:20201129141716p:plain

次にスケッチする面を選択。ここでは低面をクリック
f:id:t-hom:20201129141822p:plain

線分を選択
f:id:t-hom:20201129141930p:plain

てきとうな台形を書く。書いてる途中で寸法も一応出るけど無視して超テキトーな形でOK。ただし底辺と上辺の平行は出しておきたい。
f:id:t-hom:20201129142023p:plain

寸法ボタンをクリック。
f:id:t-hom:20201129142159p:plain

角度や寸法を入れていく。
f:id:t-hom:20201129142418p:plain

フィレットボタンをクリック。
f:id:t-hom:20201129142534p:plain

フィレットを上辺の隅に設定する。
f:id:t-hom:20201129142634p:plain

スケッチを終了すると視点が斜めにもどる。
f:id:t-hom:20201129142730p:plain

押し出しツールを選択する。
f:id:t-hom:20201129142812p:plain

矢印が現れるので、上に引っ張る。
f:id:t-hom:20201129142844p:plain

最後に寸法を手入力して立体の完成。
f:id:t-hom:20201129142924p:plain

あとは穴ツールでファン用の穴を空けたりするだけ。
f:id:t-hom:20201129143050p:plain

実際にはもう少しだけ複雑なスケッチを書いてるけど雰囲気はこんな感じである。

完成したらSTL形式でエクスポートし、さらにUltimaker Curaというソフトで3Dプリンター用のデータに変換する。
Curaは基本的にSTLを読み込んでそのまま3Dプリンター用データをSDカードに書き込んでるだけなので、特に説明することはない。本当はここで材料設定とかサポートとか密度とかを設定するらしいんだけど、知識不足のためほぼデフォルトでしか使ったことがない。

Arduino側のコーディング

前回との変更点は、データ取得タイミングをまとめて最後にスペース区切りのデータとしてシリアル送信するようにしたことである。他は特に変わっていない。書き込みはArduino Leonardoボードを選択する。ただし、このあとPCからラズパイに接続しなおす再に結構トラブルがあって、データが来てるのかどうか良く分からない事象が発生したので、私の場合はラズパイ自体にArduino IDEをインストールしてそこから書き込むようにした。

//for CO2
#include <SoftwareSerial.h>

//for BME
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#define SEALEVELPRESSURE_HPA (1013.25)

//for CO2
SoftwareSerial swSer(8, 7);
uint8_t cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
uint8_t reset[9] = {0xFF,0x01,0x87,0x00,0x00,0x00,0x00,0x00,0x78};
uint8_t res[9] = {};
uint8_t idx = 0;
bool flag = false;
uint16_t co2=0;

//for BME
Adafruit_BME280 bme;

//for DustSensor
int pin = 9;
unsigned long t0;
unsigned long ts = 30000; // 30000ms
unsigned long lowOc = 0;
float ratio = 0;
float concent = 0;

void setup() {
  //for Common
  Serial.begin(9600);

  //for CO2
  swSer.begin(9600);
  //swSer.write(reset,9);//calib
  //delay(60000UL);

  //for BME
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }

  //for DustSensor
  pinMode(9,INPUT);
  t0 = millis();
}

void loop() {
  uint16_t val=0;
  float t;
  float temperature;
  float pressure;
  float humidity;
  swSer.write(cmd,9);

  //BME
  temperature = bme.readTemperature();
  pressure = bme.readPressure() / 100.0F;
  humidity = bme.readHumidity();

  //DustSensor
  t0 = millis();
  lowOc = 0;
  while((millis() -t0) <= ts) {
    lowOc += pulseIn(pin, LOW);
  }
  ratio = lowOc/(ts*10.0);
  concent = 1.1 * pow(ratio,3) - 3.8 * pow(ratio,2) + 520 * ratio + 0.62;
  
  delay(30000);

  //CO2
  while(swSer.available()==0){
    Serial.println("bad com");
  }
  while(swSer.available()>0){
    res[idx++]=swSer.read();
    flag=true;
  }
  idx = 0;
  if(flag){
    flag=false;
    co2 = 0;
    //delay(100);
    co2 += (uint16_t)res[2] <<8;
    //delay(100);
    co2 += res[3];
    t = res[4];
  }

  Serial.println("data: " +
    String(temperature) + " " +
    String(pressure) + " " +
    String(humidity) + " " +
    String(co2) + " " +
    String(concent));
}

Ambientの登録

Ambientにアカウント登録をしておく。個人利用なら無償のプランで十分である。
チャネルIDとライトキー(書き込み用のID)が入手できれば準備OK。
ambidata.io

ラズパイ側のコーディング

コードの前にラズパイのpythonにambient用のライブラリを導入しておく。

sudo pip install git+https://github.com/AmbientDataInc/ambient-python-lib.git

詳細はAmbientサイトのリファレンスにラズパイでの使用方法が書かれているのでうまくいかなければそちらを参照して欲しい。

ラズパイ側で作成するコードはこちら。ファイル名は任意で良い。
一部可変の箇所があるので後述する。

import ambient
import time
import serial

def pcs2ugm3(pcs):
    pi = 3.14159
    density = 1.65 * pow (10,12)
    r25 = 0.44 * pow(10, -6)
    vol25 = (4/3) * pi * pow (r25, 3)
    mass25 = density * vol25
    K = 3531.5
    return pcs * K * mass25

ser = serial.Serial('/dev/ttyACM0', 9600)
ambi = ambient.Ambient(★チャネルID★, ★"ライトキー"★)

while True:
    if(ser.in_waiting > 0):
        line = ser.readline().split()
        print(line)
        if line[0] == b'data:':
            try:
                r = ambi.send({ \
                    "d1": float(line[1]), \
                    "d2": float(line[2]), \
                    "d3": float(line[3]), \
                    "d4": float(line[4]), \
                    "d5": pcs2ugm3(float(line[5])), \
                    })
            except:
                print("Connection Error")
        else:
            print("Error")
    time.sleep(1)

可変の箇所

以下の2行は環境によって可変となる

ser = serial.Serial('/dev/ttyACM0', 9600)
ambi = ambient.Ambient(★チャネルID★, ★"ライトキー"★)

まずUSBのデバイス名が「/dev/ttyACM0」としているが、これを調べるにはまずUSBを抜いた状態で「ls /dev/tty*」コマンドを実行する。するとデバイスの一覧が出てくる。次にUSBを指した状態で「ls /dev/tty*」を再実行する。ここで出てきたデバイスの一覧と先ほどの実行結果を比べて、増えたデバイスが今挿したUSBである。

まぁ通常は順番に振られるのでArduinoを1台だけ繋いでいたら/dev/ttyACM0になるはず。

次に★チャンネルID★と★"ライトキー"★の部分は自分のチャンネルIDとライトキーに書き換える。
★は目立つようにしただけなので実際には書いてはいけない。

チャンネルIDは整数なのでそのまま記載、ライトキーは文字列なのでダブルクォートで囲む。

実行

Arduino上のセンシングデータはUSBを繋いだ時点で随時ラズパイに送られてるので、あとはラズパイ側で上記のプログラムを動かすだけである。私の場合はコマンド画面を立ち上げっぱなしにしているけど、バックグラウンド実行でも良いかと思う。デーモン化までは面倒なのでやっていない。

Ambient側のグラフ作成は直感的に分かると思うので説明は割愛する。

トラブルシューティング

接続があってるのにArduinoがデータをよこさないことがある。
切り分けとしては、ラズパイ側にArduino IDEを入れてそこでシリアルモニターでデータが来ているか確認する。
ttyデバイス名が合ってるかどうかも再確認。

あとは、電源つないだまま配線いじったりするとプログラムの挙動がおかしくなることが多い。書き込みなおすとうまく動いたりする。USBのつなぎ直しでプログラムがおかしくなるケースにも遭遇したので、やはりラズパイにArduino IDEを入れてしまってそっちから書き込んでしまうのが早いかもしれない。

ちなみに、Arduino Pro Microではそもそも私のPCでは書き込み不可トラブルが発生することがあった。これはNZXTのCAMというソフトを終了させたら改善したので、類似のトラブルが発生した場合は稼働中のソフトウェアとの競合を疑うのも手だと思う。特にCAMのようなハードウェア連携するツールは怪しい。

Ambientのグラフに関してもちょっと変だなと思う挙動がある。最初のグラフ配置はページの左上に収まっていたのにグラフをドラッグしたら左上にマージンができてしまい、元の位置には戻せなくなる。これは気にせずそういうもんだと思ってそのまま使うことにした。

あとがき(余談)

思ったよりボリューム満載の記事になってしまった。モノを作るのって結構大変。
さて、今回はArduinoとラズパイとクラウドサービスAmbientを連携させたが、ラズパイの役割って単にネットワーク機器としてArduinoのデータをAmbientに橋渡ししてるだけなので、無線LAN付きのArduinoを使えばラズパイをかまさなくても同じことができると思う。

そう思って一旦ESP8266という無線付きのマイコンを買ったんだけど、3.3V動作なのでどうもホコリセンサーの信号がうまく拾えず断念した。電圧変換とか色々試したけど今のところうまくいかず。

ただ、最終的には空気の質が落ちたときにアラートを出したりと色々機能を持たせたいのでArduinoはセンシングに専念してラズパイ上でPythonで色々処理できる今回の構成が、ある意味正解かもしれない。

ちなみに技適のことをよく分かってなかったんだけど、そもそも私が買ったESP8266は日本国内で使っちゃダメみたいで、数時間だけ検証したものの廃棄予定である。Amazon.co.jpで売っちゃまずいだろこれ。。

別途技適マーク取得済のESP-WROOM-02というモジュールを買ってきたのでArduino+無線で何かやりたいときはそっちを使うことにする。

参考サイト

概ね前回と同じであるが、今回はラズパイ側でダストセンサーの値をpcs/0.01cfからug/m^3へ変更するのに以下のサイトの関数を使用させていただいた。
algorithm.joho.info

以下は前回と同じ。
algorithm.joho.info
lastminuteengineers.com

以上

Arduino Pro MicroでPS4コントローラーをマクロキーボードとして使う(小型化)

今回は以前に紹介したPS4コントローラーをマクロキーボードとして使う件の小型化について紹介する。
thom.hateblo.jp

f:id:t-hom:20201121010920p:plain

わざわざ私が書かなくても、Arduino Pro MicroとミニUSB Host Sheildを組み合わせた事例はネットで検索できるが、ハマりどころが2か所ほどあったのでそこを強調する意味で記事にしようと思う。

サイズ感

左が従来型で右が今回作成したもので、機能的には全く同じである。
f:id:t-hom:20201120214349p:plain
これでケーブルの向きも良い感じになった。

購入したもの(要注意)

Arduino Pro Micro 3.3V 8MHz

上記は5個セットだが、単品でも購入できるかと思う。
Arduino Pro Microには次の2種類があるので、購入時に要注意である。
今回の目的で使用する場合は、必ず3.3V版を利用する必要がある。

動作電圧 動作クロック
5V版 5V 16MHz
3.3V版 3.3V 8MHz

見た目はほぼ同じで、ルーペで確認しないと分からないレベルなので形で判断はできない。
私は間違えて5V版の5個セットを購入してしまい、全く動作しなかった。

同じページから購入しても2個セットを選択したら5V版だったりするので改めて購入しようとする商品が3.3V版であることを確かめる必要がある。

私が買った商品のAmazon説明では3.3vの16MHzになっていたが、実際には3.3vの8MHzが届いた。
ちなみに表記間違いはAmazonへレポート済である。

なお、単品購入の場合は同じメーカーのものは見つからないかもしれないが、Arduino Pro Microと書いてあって3.3Vで部品の配置が同じだったらメーカーが違えど基盤の色が違えど動作するはず。(自己責任でお願いします。)
大事なことなので何度も繰り返すが、5V版と3.3V版は部品配置もカラーリングも同じなので表記で見分けるしかない。

※拡大写真があれば、水晶発振器に8MHzと書いてあるはずだが、両方販売してる場合に画像を使いまわしてる場合もあるので過信はできない。

届いてから見分ける方法としては、電圧レギュレーターの部品型番と水晶発振器のクロック数表記が違う。
f:id:t-hom:20201120220447p:plain
ルーペがあるなら個包装を開封する前に確認すれば返品できるかもしれない。

ミニUSB ホストシールド

こちらはPrimeじゃなかったので海外から到着するのに約3週間かかった。
単品でPrime商品もあるのでそっちで良いと思う。
これもメーカー違いが複数出てるけど商品画像の部品配置が同じだったら商品も同じだと思う。(自己責任でお願いします。)

加工

パターンカット

今回は3.3V版のArduino Pro Microを使うが、その理由はミニUSBホストシールドのチップが3.3Vにしか対応していない為である。ただそのままだとUSBに給電される電圧も3.3Vなので、通常5Vで動作する機器が動作しなくなる。よって別の場所から5Vを給電するために3.3Vの給電線をカットするという作業が必要になる。

下図に赤線で示したところにカッターナイフで何度も傷をつけて、基盤パターンをカットする。
f:id:t-hom:20201120222155p:plain

下図赤丸で示す抵抗の端のハンダと、もうひとつの赤丸のスルーホールにそれぞれテスターの針を当てて導通してなければ正しくカットできている。
f:id:t-hom:20201120222451p:plain

実際に測っているところ。
f:id:t-hom:20201120223055p:plain

カット跡はこんな感じ。
f:id:t-hom:20201120223207p:plain

ArduinoとUSB Host Shieldの接続

まずは接続箇所を説明し、実際の接続手順はその後に解説する。

接続関係の説明

基本的にはUSB接続口が左右逆向きになるように重ね合わせることができるが、下図の赤×印の部分は上下のホールを繋いではいけない。また、赤線、青線はそれぞれワイヤーで接続する必要がある。
f:id:t-hom:20201120224928p:plain

まず赤線は、ArduinoのRawからUSBホストシールドのUSB接続口へ給電するためのケーブルである。
Rawは生という意味で、Arduino自体のUSBから給電された生の5Vが直接出力されている。これは3.3V版を購入しても同様なので、この電圧をUSB機器へ直接流すことで5V駆動の周辺機器を繋ぐことができる。

青線はArduinoのリセットとUSBホストシールドのリセットを繋ぐケーブルである。

そしてUSBホストシールド側の上段の2つの×印はArduino側ではそれぞれのケーブルが繋がれる位置のため、何も接続しない。
下段の×印はよく見るとリセットと接続されていることが分かると思う。そしてArduino側はGNDになっている。リセットとGNDを接続してしまうと機器にリセットがかかってしまうので、ここは接続しない。

接続手順

接続にはArduino Pro Microに付属する12ピンを利用する。
f:id:t-hom:20201120231316p:plain

このうち1つは、端から3つ目のピンをラジオペンチで抜き取っておく。これは先ほど説明したArduinoのGNDとUSB Host Shieldのリセットを接続させないための処置である。
f:id:t-hom:20201120231510p:plain

ちなみに、ピンを抜く代わりに基盤のパターンをカットすると紹介されているページもあった。どちらの方法でも、GNDとRSTを接続させないという目的は達成できるのでOK。
f:id:t-hom:20201120233148p:plain

さて、もう一方はラジオペンチで9、1、2に分けて、このうち2は破棄する。
f:id:t-hom:20201120231855p:plain

ミニブレッドボードに次のように差し込む。USB側はどうしても浮き上がった形になってしまうが仕方がない
f:id:t-hom:20201120232047p:plain

上段の×印は接続させたくないだけなので、ピンを抜いただけで黒い土台は残している。
下段の×印はそこにケーブルが収まるので土台もない状態。

上からArduino Pro Microを重ねる。
f:id:t-hom:20201120232522p:plain

そしてピンのある個所だけを半田付けする。

半田付けできたら一旦ブレッドボードから抜いて分離する。
f:id:t-hom:20201121001344p:plain

次に、ワイヤーをUSB Host Shieldのスルーホールに半田づけする。
赤線はパターンカットしたUSB給電へ。青線はそこから2つ飛ばしてRSTへ接続する。
裏側から半田付けするが、ケーブルが途中で抜けないようにマスキングテープを活用する。
f:id:t-hom:20201121001534p:plain

次に、赤線をArduinoのRawに、青線をArduinoのRSTへ接続する。この時ケーブルが長すぎるとこのあと2つの基盤を重ねた時に間に収まらなくなるので、以下の写真くらいの長さにとどめておく。
f:id:t-hom:20201121001853p:plain

表からみた写真。
f:id:t-hom:20201121002004p:plain

次に、2つの基盤を重ね合わせる。完全に差し込む前に、ワイヤーの根本をラジオペンチなどでなるべく奥側へ倒しておく(下図矢印の方向)。半田付けされているので割と固いけど押し込んでおかないと基盤を合体させたときに最後までささらない。
f:id:t-hom:20201121002320p:plain

最後まで押し込むとコンパクトにピッタリ収まる。
f:id:t-hom:20201121003111p:plain

次に基盤どおしが浮かないようにマスキングテープで固定する。
f:id:t-hom:20201121003156p:plain

そしてマスキングテープが無い場所からハンダ付けをして、マスキングテープをはがして残りのピンをハンダ付けする。
※ピンが出ていないところはハンダ付けしないので注意。

これで接続は完成。
f:id:t-hom:20201121003347p:plain

参考サイト

https://ht-deko.com/arduino/shield_usbhost_mini.htmlht-deko.com
参考というかまるっとそのまま真似したのだが、初心者の私にとって細かいニュアンスで色々とつまずく点もあったので今回こちらでも解説してみた。

プログラム書き込み

プログラムの書き込みでも重要な注意点がある。
5V版のArduinoでは単にボード選択でLeonardoを選べば良いが、今回使用する3.3V版はLeonardoとして書き込んでしまうと不具合が発生して動かなくなる。復旧は少し面倒なので間違えないようにしたい。

まずボードが登録されていないと思うので環境設定から追加ボードマネージャーのURLを追加しておく。
f:id:t-hom:20201121003732p:plain

追加するURLは次のとおり

https://raw.githubusercontent.com/sparkfun/Arduino_Boards/master/IDE_Board_Manager/package_sparkfun_index.json

ツール→ボード→ボードマネージャーを開き、SparkFun AVR Boards by SparkFun Electronicsをインストールしておく。
f:id:t-hom:20201121004246p:plain

ツールから、ボードをSparkFun Pro Micro、プロセッサをATMega32U4(3.3V, 8MHz)を選択する。ここでプロセッサの電圧と周波数の選択を間違えるとまた苦労するので注意。
f:id:t-hom:20201121004523p:plain

あとはシリアルポートを選択し、普通に書き込むだけである。

トラブルシューティング

違うボードを選択して書き込んでしまった場合、リセット処理が必要になる。
まずArduino IDEでは正しいボードを選びなおしておく。
それから何も追加コードを書いていない新規のArduinoファイルで書き込み処理を開始し、コンパイルが開始された直後にGNDとRSTをすばやく2回ショートさせる。そうするとArduinoは数秒間だけ書き込み可能になる。ちょうどコンパイルが終わって書き込み開始される頃にリセットが完了している必要があり、シビアなタイミングが要求される。というか運ゲーである。10回に1回くらいは成功するので、さほど悲観に暮れる必要はないが面倒なのでボード選択は間違えないようにしたい。

GNDとRSTをショートさせるのはハンダで輪っかを作るとやりやすい。
f:id:t-hom:20201121010300p:plain

コード

前回から少しバージョンUPして、CTRL+マウスホイールによるズームイン・ズームアウトができるようになったのでコードを掲載しておく。コントローラー中央のプレイステーションボタンを押しながら右ジョイスティックを時計回りに回すとズームイン・半時計回りならズームアウトというコードになっている。

また、画面スクロールもアナログレバー(L2・R2)に割り当てた。押し込み具合によってスクロール量が変わる。

#include <Keyboard.h>
#include <Mouse.h>
#include <PS4USB.h>

// Satisfy the IDE, which needs to see the include statment in the ino too.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

// #include "Arduino.h"
// #include "DFRobotDFPlayerMini.h"
// DFRobotDFPlayerMini myDFPlayer;

USB Usb;
PS4USB PS4(&Usb);

bool printAngle, printTouch;
uint8_t oldL2Value, oldR2Value;
uint8_t oldRightHatX, oldRightHatY;
uint8_t redundant;

void setup() {
  if (Usb.Init() == -1) {
    while (1); // Halt
  }
  Mouse.begin();
  redundant = 1;
  /* Serial1.begin(9600);
  if (!myDFPlayer.begin(Serial1)) {  //Use softwareSerial to communicate with mp3.
    while(true);
  }
  myDFPlayer.volume(30);  //Set volume value. From 0 to 30
  */
}

void loop() {
  Usb.Task();

  if (PS4.connected()) {
    if (PS4.getButtonPress(PS)&& ((abs(128 - PS4.getAnalogHat(RightHatX)) + abs(128 - PS4.getAnalogHat(RightHatX))) >= 127)) {
      //TopRight
      if (PS4.getAnalogHat(RightHatX) >= 128 && PS4.getAnalogHat(RightHatY) < 128 ) {
        if (oldRightHatX + redundant < PS4.getAnalogHat(RightHatX) && oldRightHatY + redundant < PS4.getAnalogHat(RightHatY)) {
          //RightRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, 1);
          Keyboard.releaseAll();
        }
        if (oldRightHatX - redundant > PS4.getAnalogHat(RightHatX) && oldRightHatY - redundant > PS4.getAnalogHat(RightHatY)) {
          //LeftRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, -1);
          Keyboard.releaseAll();
        }
      }
      
      //BottomRight
      if (PS4.getAnalogHat(RightHatX) >= 128 && PS4.getAnalogHat(RightHatY) >= 128 ) {
        if (oldRightHatX - redundant > PS4.getAnalogHat(RightHatX) && oldRightHatY + redundant < PS4.getAnalogHat(RightHatY)) {
          //RightRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, 1);
          Keyboard.releaseAll();
        }
        if (oldRightHatX + redundant < PS4.getAnalogHat(RightHatX) && oldRightHatY - redundant > PS4.getAnalogHat(RightHatY)) {
          //LeftRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, -1);
          Keyboard.releaseAll();
        }
      }
      
      //BottomLeft
      if (PS4.getAnalogHat(RightHatX) < 128 && PS4.getAnalogHat(RightHatY) >= 128 ) {
        if (oldRightHatX - redundant > PS4.getAnalogHat(RightHatX) && oldRightHatY - redundant > PS4.getAnalogHat(RightHatY)) {
          //RightRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, 1);
          Keyboard.releaseAll();
        }
        if (oldRightHatX + redundant < PS4.getAnalogHat(RightHatX) && oldRightHatY + redundant < PS4.getAnalogHat(RightHatY)) {
          //LeftRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, -1);
          Keyboard.releaseAll();
        }
      }
      
      //TopLeft
      if (PS4.getAnalogHat(RightHatX) < 128 && PS4.getAnalogHat(RightHatY) < 128 ) {
        if (oldRightHatX + redundant < PS4.getAnalogHat(RightHatX) && oldRightHatY - redundant > PS4.getAnalogHat(RightHatY)) {
          //RightRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, 1);
          Keyboard.releaseAll();
        }
        if (oldRightHatX - redundant > PS4.getAnalogHat(RightHatX) && oldRightHatY + redundant < PS4.getAnalogHat(RightHatY)) {
          //LeftRotate
          Keyboard.press(KEY_LEFT_CTRL);
          Mouse.move(0, 0, -1);
          Keyboard.releaseAll();
        }
      }
    }
    oldRightHatX = PS4.getAnalogHat(RightHatX);
    oldRightHatY = PS4.getAnalogHat(RightHatY);
    
    if (PS4.getAnalogButton(R2)) {
      Keyboard.press(KEY_DOWN_ARROW);
      delay(256-PS4.getAnalogButton(R2));
      Keyboard.releaseAll();
    }
    
    if (PS4.getAnalogButton(L2)) {
      Keyboard.press(KEY_UP_ARROW);
      delay(256-PS4.getAnalogButton(L2));
      Keyboard.releaseAll();
    }

    if (PS4.getButtonClick(PS)) {
      
    }
    
    if (PS4.getButtonClick(TRIANGLE)) {
      Keyboard.press('a');
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(CIRCLE)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(KEY_LEFT_SHIFT);
      Keyboard.press('1');
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(CROSS)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('d');
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(SQUARE)) {
    }

    if (PS4.getButtonPress(UP)) {
      Keyboard.press(KEY_PAGE_UP);
      delay(40);
      Keyboard.releaseAll();
    } if (PS4.getButtonClick(RIGHT)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('.');
      delay(100);
      Keyboard.releaseAll();
    } if (PS4.getButtonPress(DOWN)) {
      Keyboard.press(KEY_PAGE_DOWN);
      delay(40);
      Keyboard.releaseAll();
    } if (PS4.getButtonClick(LEFT)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(',');
      delay(100);
      Keyboard.releaseAll();
    }

    if (PS4.getButtonClick(L1)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(',');
      delay(100);
      Keyboard.releaseAll();
      //myDFPlayer.play(1);
    }
    if (PS4.getButtonClick(L3)) {
      
    }
    if (PS4.getButtonClick(R1)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('.');
      delay(100);
      Keyboard.releaseAll();
      //myDFPlayer.play(2);
    }
    if (PS4.getButtonClick(R3)) {
    }
 
    if (PS4.getButtonClick(SHARE)) {
    }
    if (PS4.getButtonClick(OPTIONS)) {
    }
    if (PS4.getButtonClick(TOUCHPAD)) {
    }
  }
}

以上

Arduino UNOで複数の環境センサー(温度、湿度、気圧、PM2.5、CO2)からデータ取得

今回はマイコンボードArduinoを使って複数の環境センサーからデータを取得するコードを紹介。この記事はシリアルモニター上で観測するところまでなので、これだけで普段使いのツールを作れるというものではないことはご承知いただきたい。

作成の動機

在宅勤務が続くなか、出社時と同等またはそれ以上のパフォーマンスを出すためには環境の見直しが重要だと考えた。オフィスでは事務所衛生基準規則等によって会社側が環境を整えてくれるが、在宅だと自分でなんとかする必要がある。よって、常時環境を監視して基準を外れた場合は対策を促すアラートを出すような仕組みを構築したいと思った。

既製品ではなく自作する動機としては、取得したデータを自分の思い通りに活用したい為。

記事にした動機

センサー単品のサンプルは探せばいくらでも出てくるが、複数取り扱うとなると一気に参考サイトが減る。センサー類の特性によっては読み取り開始から終了までの待ち時間があったり、何十秒か連続で読み取りを継続しなければならなかったりといったプログラム上の制約が出てくるので、これらをうまく取り扱う方法を紹介したいと思った。

下準備

温度・湿度・気圧センサーのハンダジャンパー

購入した温度・湿度・気圧センサーはハンダジャンパーでArduino側の通信ピンを選択できるようになってるらしい。
チップ上の3接点のうち、左と中央をハンダで繋げておく。
f:id:t-hom:20201108103718p:plain
普通は、初期状態で左と中央が繋がっているらしくそれを前提にした解説が多いのだが、私の購入した製品はどこにもジャンパーされてなかった。

Arduinoへライブラリのインストール

ツール→ライブラリを管理からAdafruit BME280 Libraryをインストールしておく。
f:id:t-hom:20201108110154p:plain

配線図

※CO2センサーは端子のないところにつながっているように見えるが、実際にはそこに端子がある。Fritzingというツールで図を作成した際、このセンサーの別のバージョンしかデータを入手できなかったため代用した。
f:id:t-hom:20201108132112p:plain

Arduino UNO内蔵型ブレッドボードを利用して検証した実際の配線がこちら。
ごちゃってて何の参考にもならないと思うが一応。。
f:id:t-hom:20201108113845p:plain

コード

このコードは私が書いたというよりは、ただ参考サイトのコードを組み合わせただけ。
変数名とかも基本的にはそのままだし本当はそれぞれ処理を関数化したいけど一旦動いたところまでで記事にしておこうと思いとりいそぎ公開。後日リファクタリングしたい。

//for CO2
#include <SoftwareSerial.h>

//for BME
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#define SEALEVELPRESSURE_HPA (1013.25)

//for CO2
SoftwareSerial swSer(8, 7);
uint8_t cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
uint8_t reset[9] = {0xFF,0x01,0x87,0x00,0x00,0x00,0x00,0x00,0x78};
uint8_t res[9] = {};
uint8_t idx = 0;
bool flag = false;
uint16_t co2=0;

//for BME
Adafruit_BME280 bme;

//for DustSensor
int pin = 9;
unsigned long t0;
unsigned long ts = 30000; // 30000ms
unsigned long lowOc = 0;
float ratio = 0;
float concent = 0;

void setup() {
  //for Common
  Serial.begin(9600);

  //for CO2
  swSer.begin(9600);
  //swSer.write(reset,9);//calib
  //delay(60000UL);

  //for BME
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }

  //for DustSensor
  pinMode(9,INPUT);
  t0 = millis();
}

void loop() {
  uint16_t val=0;
  float t;
  swSer.write(cmd,9);

  Serial.println("--------------------------------");
  //BME
  Serial.print("Temperature = ");
  Serial.print(bme.readTemperature());
  Serial.println("*C");

  Serial.print("Pressure = ");
  Serial.print(bme.readPressure() / 100.0F);
  Serial.println("hPa");

  Serial.print("Humidity = ");
  Serial.print(bme.readHumidity());
  Serial.println("%");

  //DustSensor
  t0 = millis();
  lowOc = 0;
  while((millis() -t0) <= ts) {
    lowOc += pulseIn(pin, LOW);
  }
  ratio = lowOc/(ts*10.0);
  concent = 1.1 * pow(ratio,3) - 3.8 * pow(ratio,2) + 520 * ratio + 0.62;
  Serial.println("Particulates = " + String(concent) + "pcs/0.01cf");
  
  
  delay(30000);

  //CO2
  while(swSer.available()==0){
    Serial.println("bad com");
  }
  while(swSer.available()>0){
    res[idx++]=swSer.read();
    flag=true;
  }
  idx = 0;
  if(flag){
    flag=false;
    co2 = 0;
    //delay(100);
    co2 += (uint16_t)res[2] <<8;
    //delay(100);
    co2 += res[3];
    t = res[4];
  }
  Serial.print("CO2 = ");
  Serial.print(co2);
  Serial.println("ppm");
}

組み合わせの解説

個別のセンサー処理部分は参考サイトの解説か、ご自身で調べていただくとして、組み合わせ部分だけざっくり説明する。
まず、各センサーごとにデータ取得のタイミングが異なる。即時取得できるセンサーばかりであれば単純に順次取得すれば良いのだが、各参考ページやコードを見ているとそうでもないので一工夫する必要がある。

以下に今回使用したセンサーごとのデータ取得タイミングを記す。

センサー名 データ取得タイミング
温度・湿度・気圧センサー 即時データ取得可能
ダストセンサー 30秒間連続で信号を監視し続け、そのうち信号がLowになっていた時間を累計することで計測
CO2センサー 観測開始のコマンドを発行してから60秒程度Waitしてからデータ取得

順次取得でもできなくはないが、CO2センサーが60秒ただ待ち続けるのは時間のロスなので待ってる間にダストセンサーのデータ収集をするというロジックにした。

タイムラインがこちら。
f:id:t-hom:20201108113230p:plain

実行結果

シリアルモニターを起動しておくとこんな感じでデータが取得できる。
f:id:t-hom:20201108114450p:plain

なおCO2センサーのキャリブレーションは特にやってないので値のズレは生じていると思う。CO2濃度が400ppmに近い屋外でキャリブレーションするらしいのだが、値が大きくずれた場所でやってしまうと逆に狂いが大きくなるらしいので下手にいじらず出荷状態で使っている。

記事にするかどうかは別として今後の改良・応用プラン

  • データ表示のタイミングをそろえる
  • コードのリファクタリング
  • Arduinoとセンサー類をまとめて格納するケースの作成
  • ラズパイへデータ送信してリアルタイムのグラフ表示
  • 電光掲示板に取得した情報を流す
  • 基準値越えをアラームで警告

以上

Arduino LOENARDOでPS4コントローラをマクロキーボードとして使う

今回は、PS4のDualShockコントローラーをマクロキーボードとして使うことに成功したのでご紹介。
絵面はこんな感じ。
f:id:t-hom:20201028201735p:plain

このコントローラーは本来Bluetoothに対応しているが、有線のほうがコードがシンプルになる気がしたのであえて有線で使っている。

マクロキーボードとは

キーボードショートカット等の複数キーの連続入力を一つのキーに割り当てることができるキーボードのこと。
在宅勤務を開始してすぐの頃に9キー登録できるタイプを購入して、Outlookのメールを読むときに重宝している。
例えば次のメールに移動したり、前のメールに移動したり、メールを削除したりといったOutlookに最初から用意されたキーボードショートカットを各マクロキーに割り当てて使用している。
thom.hateblo.jp

パソコン側からみた振る舞いは普通のキーボードなので特にセキュリティ的なリスクはなく、普通のキーボードを繋いでるのと同じ。
ただ会社で使ってると目立つので周辺機器持ち込みOKなユルい職場か、在宅勤務が主な活躍場所となると思われる。

今回作ったものと経緯

既存のマクロキーボードでも十分実用的なんだけど、使っているうちにさらに便利にしたくなってきた。
より誤入力しにくく、操作しやすい形状が良い。さらに卓上だけでなく手にホールドして使いたい。

そこでたどり着いた答えがゲームパッド。
今回作ったのは、単にゲームパッドをマクロキーボードとして使うための変換デバイスである。
専用のマクロ登録ツールなどは存在しないので、マクロは直接Arduino IDEでハードコーディングして基盤に書き込む形である。

もともと自作キーボードとかでArduinoが使われていたのを知っていたので、ゲームパッドを分解して埋め込んだらなんとかなるのではないかと考えていたら、別解としてゲームパッド信号をプログラムで処理してキーコードに変換する手段が見つかったのが今回作成した経緯。

活躍の場は更に絞られ、会社のオフィスで使うのは多分無理。。
真面目にメール閲覧してても遊んでる絵面にしか見えない。在宅専用デバイスである。

ただ今回の事例を応用すれば普通のキーボードをつないでマクロキーボードにすることもできるかと思う。

材料

主な材料

Arduino LEONARDO

UNOはKeyboardライブラリやMouseライブラリに対応しておらず、入力デバイスとして使用することができないのでLEONARDOを選択する。
互換品やNano等、小さい製品も使用できるかもしれないが試してないのでなんとも。


その他

  • USBケーブル 片側micro/片側TypeA を2本(Arduino用とPS4コントローラーのデータ通信用)
  • ハンダ

スキル

真似するだけなら、スキルというほどのスキルは要らない。
ハンダ付けをしたことがない方はYouTubeとかのハンダ付け動画を見よう見まねでやれば良い。
はじめてだと動画の見本のようにはうまくいかず、どちらかといえば「失敗例」に近い仕上がりになるだろう。
まぁつながればなんでも動くので、趣味でやってる分には気にしなくて良い。

Arduinoについては、IDEのインストール、マイコンへの書き込み、ライブラリのインストール、シリアルモニターへの出力ができるようになってれば十分。
入門書とか入門記事でオンボードLEDをチカチカさせられるくらいの経験でもなんとかなる。

作業

USB Host Shieldのハンダジャンパー

ジャンパーというのは、離れた電気回路間をつなぐ電線や端子・ピンのことを指すらしい。
つなぐかつながないかで回路の挙動を変更したりする、一種のスイッチのように使われることも多い。

ハンダジャンパーは下図を見ていただけると早いと思う。青丸の箇所が繋がる前のハンダジャンパー。赤丸が繋げたあと。単にハンダで2接点を繋いだだけ。
f:id:t-hom:20201028211735p:plain

青丸の位置はつなげないので注意。赤丸の部分だけジャンパーさせておく。

ピン曲がりの微修正

前評判どおり、USB Host Shieldは品質が今一つで、とどいた商品の足が曲がっているのでこれをArduinoに挿せる程度には微修正しておく。強い力をかけると折れるリスクがあるので、すこしづつ。

USB Host ShieldをArduinoに接続

ピン曲がりのため微修正したとしても苦労はする。うまくピンを指矯正しながら少しづつ差し込んでいくイメージ。
6ピン(3x2)が最後まで刺さったらOK。他のピンは最後までささらず、2~3ミリくらい接合部からピンが見えるけど、それでOK。

なお、UNOとLEONARDOはピン配置が違うのでそのままでは使えないという記事があったけど、Rev.3というバージョンから共通化されてるらしく、手持ちのLEONARDOではそのまま使えた。

コーディング

まずツール→ライブラリを管理から、USB Host Shield と検索してUSB Host Shield Library 2.0をインストールする。
次に以下のコードを貼り付け。
まだ私もサンプルを改造しながら試してるだけなので、不要なコードも色々残っている。
それでもまぁ一つ達成して熱があるうちに記事にしてしまわないと、あとで面倒になるので書いてしまう。

#include <Keyboard.h>
#include <PS4USB.h>

// Satisfy the IDE, which needs to see the include statment in the ino too.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

USB Usb;
PS4USB PS4(&Usb);

bool printAngle, printTouch;
uint8_t oldL2Value, oldR2Value;

void setup() {
  if (Usb.Init() == -1) {
    while (1); // Halt
  } 
}

void loop() {
  Usb.Task();

  if (PS4.connected()) {
    if (PS4.getAnalogHat(LeftHatX) > 137 || PS4.getAnalogHat(LeftHatX) < 117 || PS4.getAnalogHat(LeftHatY) > 137 || PS4.getAnalogHat(LeftHatY) < 117 || PS4.getAnalogHat(RightHatX) > 137 || PS4.getAnalogHat(RightHatX) < 117 || PS4.getAnalogHat(RightHatY) > 137 || PS4.getAnalogHat(RightHatY) < 117) {
      
    }

    if (PS4.getAnalogButton(L2) || PS4.getAnalogButton(R2)) { // These are the only analog buttons on the PS4 controller
      
    }
    if (PS4.getAnalogButton(L2) != oldL2Value || PS4.getAnalogButton(R2) != oldR2Value) // Only write value if it's different
      PS4.setRumbleOn(PS4.getAnalogButton(L2), PS4.getAnalogButton(R2));
    oldL2Value = PS4.getAnalogButton(L2);
    oldR2Value = PS4.getAnalogButton(R2);

    if (PS4.getButtonClick(PS)) {
      
    }
    if (PS4.getButtonClick(TRIANGLE)) {
      
    }
    if (PS4.getButtonClick(CIRCLE)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(KEY_LEFT_SHIFT);
      Keyboard.press('1');
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(CROSS)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('d');
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(SQUARE)) {
      
    }

    if (PS4.getButtonClick(UP)) {
      Keyboard.press(KEY_PAGE_UP);
      delay(100);
      Keyboard.releaseAll();
    } if (PS4.getButtonClick(RIGHT)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('>');
      delay(100);
      Keyboard.releaseAll();
    } if (PS4.getButtonClick(DOWN)) {
      Keyboard.press(KEY_PAGE_DOWN);
      delay(100);
      Keyboard.releaseAll();
    } if (PS4.getButtonClick(LEFT)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('<');
      delay(100);
      Keyboard.releaseAll();
    }

    if (PS4.getButtonClick(L1)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(KEY_PAGE_UP);
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(L3)) {
      
    }
    if (PS4.getButtonClick(R1)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(KEY_PAGE_DOWN);
      delay(100);
      Keyboard.releaseAll();
    }
    if (PS4.getButtonClick(R3)) {
      
    }
 
    if (PS4.getButtonClick(SHARE))
      
    if (PS4.getButtonClick(OPTIONS)) {
    }
    if (PS4.getButtonClick(TOUCHPAD)) {
    }
  }
}


キーボードマクロの設定は、if (PS4.getButtonClick(●●)){ }の部分。
以下に例として抜き出した。

    if (PS4.getButtonClick(CROSS)) {
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('d');
      delay(100);
      Keyboard.releaseAll();
    }

これは"Ctrl+d"のショートカットを×ボタンに割り当てている部分。
キーボードインプットについてはArduinoのKeyboardライブラリについて検索すればいくつか事例が見つかると思う。


ちなみにもともとのコードはファイル→スケッチ例メニューから以下を探すと新規コードウインドウで出てくるので、コントローラーのバイブ機能とかLED制御とかはそちらを参照すると良い。

f:id:t-hom:20201028215449p:plain
スケッチ例のコードは押したキーがシリアルモニターに表示されるようになっており、IDEのツールからシリアルモニターを起動しておかないと動かないので注意。PS4のほかに、JoystickのサンプルスケッチでPC用のゲームパッドが使えるといった記事があったけどLogicoolの手持ちのゲームパッドでは動かなかった。やはり種類も多いので製品ごとに作りが違う為だろうか。
その点有名なPS4コントローラーはサンプルスケッチもズバリ特化してるので間違いない。

Keyboardライブラリの使い方もざっくり知りたければスケッチ例から探すと良い。

使い方

この記事のコードはあくまで私のマクロ設定なので、コードは使いたいキーマクロに合わせて改造する必要がある。
一度Arduinoに書き込んでしまえばあとはどのPCにつないでも同じように動作する。
PS4コントローラーをUSB Host Shield側のUSBに接続し、Arduino側のUSBは普通にPCとつなぐ。
あとはボタン押すとArduino IDEでコーディングした通りのキーがPCに入力される。

Excel Tips アンケートや調査フォームでデータの入力規則をもう一工夫する。

Excelでアンケートや調査フォームを作成する際によく見かけるのが、入力規則を用いたドロップダウンリスト。

一般的に多いのが固定の選択肢が用意され、それ以外がエラーになる仕組みである。
f:id:t-hom:20200918211250p:plain


自由入力では設問の趣旨が伝わらず、トンチンカンな回答が返ってくる場合がある。
だからあらかじめ予想される回答を選択肢として用意し、そこから選択してもらおうという発想だ。
選択肢が十分に想定できる場合や、そもそも例外が存在しない場合はこれで良い。

しかしこの方法はドロップダウンリストにない項目は選択できないため、当てはまる選択肢がない場合に情報を取りこぼす恐れもある。

そこで私がたまに使うのは、基本的にはリストで選択肢を用意しつつ、手入力も受け付ける方法である。
f:id:t-hom:20200918212352p:plain

まぁ同じデータ入力規則でちょっと別のタブを触るだけなので、ひょっとして知ってる方も多いかもしれないがその割にこれまでの会社員経験で見かけたことがないので初めて知るという方も案外多いかもしれない。

設定箇所はデータ入力規則のエラーメッセージタブ。
デフォルトではスタイルは停止になっており、タイトルやメッセージは設定されていないが、これを警告にすると先に見せたように継続するかどうかを尋ねるプロンプトになる。タイトルやメッセージも適切なものを設定しておこう。
f:id:t-hom:20200918212910p:plain

あと、同じくデータ入力規則の入力時メッセージタブで設定できるメッセージも便利。
f:id:t-hom:20200918213140p:plain

これはどちらかといえばリストよりは、「すべての値」の時に使うことが多い。

データの入力規則機能は「規則」部分だけ注目されてる気がするが、その他の機能も意外と便利なので紹介してみた。

知らなかった!という方はこれを機に活用してみて欲しい。
そんなの知ってるし!という方は、それにしては活用例を見かけないので布教よろ。

以上

VBA Excelガントチャート作成マクロ

今回はVBAでExcelガントチャートを作成するマクロを紹介する。
作成したガントチャート自体はマクロに依存せずExcelの基本機能で動作する。

完成すると以下のようなイメージになる。
f:id:t-hom:20200712012937p:plain

大元のアイデアはこちらのYouTube動画を参考にしている。

動画だと英語の解説で結構操作スピードも速い。また、手動で作成しているので毎回再現するのも面倒だ。
テンプレートを作って使いまわしても良いが、それよりもいつでも再現できるVBAコードの形で残しておこうと思って今回マクロ化した。

オリジナルを参考にしつつ私が新たに追加した機能は次のとおり。

  • 現在進行中のタスクを赤い三角でマーク
  • "Phase"で始まるタスク名を太字と色で強調
  • 計画(PLANNED)と実績(ACTUAL)が入力でき、ガントチャートの方でもPLANNEDが背景塗りつぶし、ACTUALが「≫」で表示
  • 現在のSTATUSはPLANNEDとPROGRESSから自動入力され、Delayはオレンジ系、Over Dueは赤系の色で警告
  • テーマカラー使用の為、ページレイアウトの配色から簡単に好みの色合いに変更可能

使い方

コードが非常に長いので先に使い方を説明する。
マクロを実行すると新規ブックに次のようなフォームが作成される。
f:id:t-hom:20200712014604p:plain
テーマカラーを多用しているのでオフィスのバージョンによって異なると思われる。

次に以下の薄黄色で示した箇所を手入力する。(説明のために塗っただけで、実際は白背景)
f:id:t-hom:20200712015425p:plain

このときタスク名にPhaseで始まる名称を使用すると自動的に強調される。

タスクは手動でインデントするとより見やすくなる。
f:id:t-hom:20200712015644p:plain

ページレイアウトタブの配色から好きな色を選択する。
f:id:t-hom:20200712015740p:plain

あとはファイル名を付けて保存すれば完成。
作成されたガントチャートはVBAを使用しないのでxlsxで保存すればOK。

7/12 10:30 追記

土日及び祝日を網掛けする機能を追加した。祝日はAT列に手動で入力する想定。コードも修正済。
f:id:t-hom:20200712103101p:plain

7/25 22:00 バグ修正

Over Dueの計算式が間違っていたので修正

Before

    sh.Range("I8").FormulaR1C1 = "=IF(OR(ISBLANK(RC[-5]),ISBLANK(RC[-4])),""""," _
        & vbLf & Space(4) & "IF(RC[-1]=1,""Completed""," _
        & vbLf & Space(4 * 2) & "IF(AND(RC[-1]=0,RC[-5]>=TODAY()),""Not Started""," _
        & vbLf & Space(4 * 3) & "IF(AND(RC[-1]<1,RC[-4]<=TODAY()),""Over Due""," _
        & vbLf & Space(4 * 4) & "IF((TODAY()-RC[-5])/(RC[-4]-RC[-5]+1)>=RC[-1],""Delay""," _
        & vbLf & Space(4 * 5) & """In Progress"")))))"

After

    sh.Range("I8").FormulaR1C1 = "=IF(OR(ISBLANK(RC[-5]),ISBLANK(RC[-4])),""""," _
        & vbLf & Space(4) & "IF(RC[-1]=1,""Completed""," _
        & vbLf & Space(4 * 2) & "IF(AND(RC[-1]=0,RC[-5]>=TODAY()),""Not Started""," _
        & vbLf & Space(4 * 3) & "IF(AND(RC[-1]<1,RC[-4]<TODAY()),""Over Due""," _
        & vbLf & Space(4 * 4) & "IF((TODAY()-RC[-5])/(RC[-4]-RC[-5]+1)>=RC[-1],""Delay""," _
        & vbLf & Space(4 * 5) & """In Progress"")))))"

コード

今回は条件分岐等が生じない単なる再現系マクロなので、マクロ記録に毛が生えた程度のコードである。
ある程度コード整理はしたものの、プロシージャ分割等は一切しなかった。

NUMBER_OF_TASKSの値がタスクの行数を表すので、ここを変えると任意のタスク数でガントチャートを作成できる。
他に変更を想定したパラメーターは特にない。
※ちなみにWEEKの値は1週間が7日であることを示す定数なので変更してはならない。

Sub CreateGantt()
    Const WEEK As Integer = 7
    Const NUMBER_OF_TASKS As Integer = 100
    
    '#General Setting
    Dim sh As Worksheet
    Set sh = Workbooks.Add.Sheets(1)
    ActiveWindow.DisplayGridlines = False
    With sh.Cells.Font
        .Name = "Meiryo UI"
        .Size = 9
    End With
    
    '#Header Setting
    sh.Range("A1").Value = "Input Project Name Here"
    With sh.Range("A1").Font
        .Size = 22
        .ThemeColor = xlThemeColorAccent1
        .TintAndShade = -0.25
    End With
    
    sh.Names.Add "R_ProjectStart", sh.Range("C3")
    sh.Names.Add "R_DisplayWeek", sh.Range("C4")
    
    With sh.Range("R_ProjectStart")
        .Value = Date
        .Offset(0, -1).Value = "Project Start:"
        .Offset(0, -1).HorizontalAlignment = xlRight
    End With
    
    With sh.Range("R_DisplayWeek")
        .Value = 1
        .Offset(0, -1).Value = "Display Week:"
        .Offset(0, -1).HorizontalAlignment = xlRight
    End With
    
    Dim headerCursor As Range: Set headerCursor = sh.Range("A7")
    Dim h
    For Each h In Split(",TASK,ASSIGNED TO,START,END,START,END,PROGRESS,STATUS", ",")
        headerCursor.Value = h
        Set headerCursor = headerCursor.Offset(0, 1)
    Next

    Dim dateCursor As Range: Set dateCursor = headerCursor.Offset(-1, 0)
    dateCursor.Formula = "=R_ProjectStart-WEEKDAY(R_ProjectStart)+1+((R_DisplayWeek-1)*7)"
    dateCursor.NumberFormatLocal = "d"
    With dateCursor.Offset(0, 1)
        .FormulaR1C1 = "=RC[-1]+1"
        .AutoFill Destination:=.Resize(1, WEEK * 5 - 1), Type:=xlFillDefault
    End With
    dateCursor.Resize(1, WEEK * 5).EntireColumn.ColumnWidth = 3
    dateCursor.Resize(2, 1).EntireRow.HorizontalAlignment = xlCenter
    
    Dim weekdayCursor As Range
    Set weekdayCursor = dateCursor.Offset(1, 0)
    With weekdayCursor
        .FormulaR1C1 = "=LEFT(TEXT(R[-1]C,""ddd""),1)"
        .AutoFill Destination:=.Resize(1, 7 * 5), Type:=xlFillDefault
    End With

    Dim weekCursor As Range: Set weekCursor = dateCursor.Offset(-1, 0)
    With weekCursor
        .FormulaR1C1 = "=R[1]C"
        .NumberFormatLocal = "yyyy/m/d;@"
        .Font.Size = 12
        With .Resize(1, WEEK)
            .Merge
            .HorizontalAlignment = xlLeft
            .AutoFill .Resize(1, WEEK * 5)
        End With
    End With
    
    'Paint
    Dim headerRange As Range
    Set headerRange = sh.Range(Cells(headerCursor.Row, 1), headerCursor.Offset(0, WEEK * 5 - 1))
    With headerRange.Interior
        .Pattern = xlSolid
        .PatternColorIndex = xlAutomatic
        .ThemeColor = xlThemeColorLight1
        .TintAndShade = 0.5
        .PatternTintAndShade = 0
    End With
    headerRange.Font.Color = rgbWhite

    With weekCursor.Resize(1, WEEK * 5).Interior
        .Pattern = xlSolid
        .PatternColorIndex = xlAutomatic
        .ThemeColor = xlThemeColorAccent1
        .TintAndShade = 0.5
        .PatternTintAndShade = 0
    End With
    
    With dateCursor.Resize(1, WEEK * 5).Interior
        .Pattern = xlSolid
        .PatternColorIndex = xlAutomatic
        .ThemeColor = xlThemeColorAccent1
        .TintAndShade = 0.8
        .PatternTintAndShade = 0
    End With

    Dim r As Range
    Set r = weekCursor
    For i = 1 To 5
        With r.Resize(2, WEEK)
            .Borders(xlDiagonalDown).LineStyle = xlNone
            .Borders(xlDiagonalUp).LineStyle = xlNone
            With .Borders(xlEdgeLeft)
                .LineStyle = xlContinuous
                .ThemeColor = 2
                .TintAndShade = 0.5
                .Weight = xlThin
            End With
            With .Borders(xlEdgeTop)
                .LineStyle = xlContinuous
                .ThemeColor = 2
                .TintAndShade = 0.5
                .Weight = xlThin
            End With
            .Borders(xlEdgeBottom).LineStyle = xlNone
            With .Borders(xlEdgeRight)
                .LineStyle = xlContinuous
                .ThemeColor = 2
                .TintAndShade = 0.5
                .Weight = xlThin
            End With
            .Borders(xlInsideVertical).LineStyle = xlNone
            .Borders(xlInsideHorizontal).LineStyle = xlNone
        End With
        Set r = r.Offset(0, 1)
    Next
    
    Dim bodyRange As Range
    Set bodyRange = headerRange.Offset(1, 0).Resize(NUMBER_OF_TASKS)
    bodyRange.RowHeight = 16.5
    With bodyRange
        .Borders(xlDiagonalDown).LineStyle = xlNone
        .Borders(xlDiagonalUp).LineStyle = xlNone
        .Borders(xlEdgeLeft).LineStyle = xlNone
        With .Borders(xlEdgeTop)
            .LineStyle = xlContinuous
            .ThemeColor = 2
            .TintAndShade = 0.5
            .Weight = xlThin
        End With
        With .Borders(xlEdgeBottom)
            .LineStyle = xlContinuous
            .ThemeColor = 2
            .TintAndShade = 0.5
            .Weight = xlThin
        End With
        .Borders(xlEdgeRight).LineStyle = xlNone
        .Borders(xlInsideVertical).LineStyle = xlNone
        With .Borders(xlInsideHorizontal)
            .LineStyle = xlContinuous
            .ThemeColor = 2
            .TintAndShade = 0.5
            .Weight = xlThin
        End With
    End With
    
    'Gantt Bar
    Dim ganttRange As Range
    Set ganttRange = weekdayCursor.Offset(1, 0).Resize(NUMBER_OF_TASKS, WEEK * 5)
    ganttRange.FormulaR1C1 = "=IF(AND(RC6<=R6C,R6C<=RC7),""≫"","""")"
    With ganttRange
        .HorizontalAlignment = xlCenter
        .VerticalAlignment = xlCenter
    End With
    With ganttRange.Font
        .Size = 16
        .ThemeColor = xlThemeColorAccent1
        .TintAndShade = -0.5
    End With
    
    With ganttRange
        .FormatConditions.AddColorScale ColorScaleType:=2
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        .FormatConditions(1).ColorScaleCriteria(1).Type = xlConditionValueLowestValue
        With .FormatConditions(1).ColorScaleCriteria(1).FormatColor
            .Color = 2650623
            .TintAndShade = 0
        End With
        .FormatConditions(1).ColorScaleCriteria(2).Type = xlConditionValueHighestValue
        With .FormatConditions(1).ColorScaleCriteria(2).FormatColor
            .Color = 10285055
            .TintAndShade = 0
        End With
        .FormatConditions.Add Type:=xlExpression, Formula1:="=AND($D8<=J$6,J$6<=$E8)"
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Interior
            .PatternColorIndex = xlAutomatic
            .ThemeColor = xlThemeColorAccent1
            .TintAndShade = 0.25
        End With
        .FormatConditions(1).StopIfTrue = False
    End With

    'Progress Data Bar
    With sh.Range("H8").Resize(NUMBER_OF_TASKS)
        .HorizontalAlignment = xlCenter
        .NumberFormatLocal = "0%"
        .FormatConditions.AddDatabar
        .FormatConditions(.FormatConditions.Count).ShowValue = True
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1)
            .MinPoint.Modify newtype:=xlConditionValueNumber, newvalue:=0
            .MaxPoint.Modify newtype:=xlConditionValueNumber, newvalue:=1
        End With
        With .FormatConditions(1).BarColor
            .ThemeColor = xlThemeColorAccent1
            .TintAndShade = 0.6
        End With
        .FormatConditions(1).BarFillType = xlDataBarFillSolid
        .FormatConditions(1).Direction = xlContext
        .FormatConditions(1).NegativeBarFormat.ColorType = xlDataBarColor
        .FormatConditions(1).BarBorder.Type = xlDataBarBorderNone
        .FormatConditions(1).AxisPosition = xlDataBarAxisAutomatic
        With .FormatConditions(1).AxisColor
            .Color = 0
            .TintAndShade = 0
        End With
        With .FormatConditions(1).NegativeBarFormat.Color
            .Color = 255
            .TintAndShade = 0
        End With
    End With
    
    'Highlight Today
    With ganttRange.Offset(-2, 0).Resize(ganttRange.Rows.Count + 2)
        .FormatConditions.Add Type:=xlExpression, Formula1:="=J$6=TODAY()"
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Borders(xlLeft)
            .LineStyle = xlContinuous
            .Color = -16776961
            .TintAndShade = 0
            .Weight = xlThin
        End With
        With .FormatConditions(1).Borders(xlRight)
            .LineStyle = xlContinuous
            .Color = -16776961
            .TintAndShade = 0
            .Weight = xlThin
        End With
        .FormatConditions(1).StopIfTrue = False
    End With
    
    'Scroll Bars
    sh.ScrollBars.Add(weekCursor.Left, weekCursor.Top - 16.5, weekCursor.Resize(1, WEEK * 5).Width, 14).Select
    With Selection
        .Value = 1
        .Min = 1
        .Max = 52
        .SmallChange = 1
        .LargeChange = 10
        .LinkedCell = "R_DisplayWeek"
        .Display3DShading = False
    End With

    sh.Range("I8").FormulaR1C1 = "=IF(OR(ISBLANK(RC[-5]),ISBLANK(RC[-4])),""""," _
        & vbLf & Space(4) & "IF(RC[-1]=1,""Completed""," _
        & vbLf & Space(4 * 2) & "IF(AND(RC[-1]=0,RC[-5]>=TODAY()),""Not Started""," _
        & vbLf & Space(4 * 3) & "IF(AND(RC[-1]<1,RC[-4]<TODAY()),""Over Due""," _
        & vbLf & Space(4 * 4) & "IF((TODAY()-RC[-5])/(RC[-4]-RC[-5]+1)>=RC[-1],""Delay""," _
        & vbLf & Space(4 * 5) & """In Progress"")))))"
    
    sh.Range("I8").AutoFill sh.Range("I8").Resize(NUMBER_OF_TASKS)
    
    'Status Format
    With sh.Range("I8").Resize(NUMBER_OF_TASKS)
        .HorizontalAlignment = xlCenter
        .FormatConditions.Add Type:=xlTextString, String:="Completed", TextOperator:=xlContains
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Font
            .Color = rgbGray
            .TintAndShade = 0
        End With
        With .FormatConditions(1).Interior
            .Color = rgbGainsboro
            .TintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
        
        .FormatConditions.Add Type:=xlTextString, String:="In Progress", TextOperator:=xlContains
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Font
            .Color = rgbDarkGreen
            .TintAndShade = 0
        End With
        With .FormatConditions(1).Interior
            .Color = rgbHoneydew
            .TintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
        
        .FormatConditions.Add Type:=xlTextString, String:="Delay", TextOperator:=xlContains
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Font
            .Color = rgbSienna
            .TintAndShade = 0
        End With
        With .FormatConditions(1).Interior
            .Color = rgbBisque
            .TintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
        
        .FormatConditions.Add Type:=xlTextString, String:="Over Due", TextOperator:=xlContains
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Font
            .Color = rgbFireBrick
            .TintAndShade = 0
        End With
        With .FormatConditions(1).Interior
            .Color = rgbPink
            .TintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
    
        .FormatConditions.Add Type:=xlTextString, String:="Not Started", TextOperator:=xlContains
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Font
            .Color = rgbDarkGray
            .TintAndShade = 0
        End With
        With .FormatConditions(1).Interior
            .Color = rgbWhite
            .TintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
    End With
    
    'Format Start End Dates
    With sh.Range("D7:E7").Interior
        .Pattern = xlSolid
        .PatternColorIndex = xlAutomatic
        .ThemeColor = xlThemeColorAccent3
        .TintAndShade = -0.25
        .PatternTintAndShade = 0
    End With
    
    With sh.Range("F7:G7").Interior
        .Pattern = xlSolid
        .PatternColorIndex = xlAutomatic
        .ThemeColor = xlThemeColorAccent4
        .TintAndShade = -0.25
        .PatternTintAndShade = 0
    End With

    sh.Range("D6").Value = "PLANNED"
    sh.Range("D6:E6").Merge
    With sh.Range("D6:E6").Font
        .ThemeColor = xlThemeColorAccent3
        .TintAndShade = -0.25
    End With
    
    sh.Range("F6").Value = "ACTUAL"
    sh.Range("F6:G6").Merge
    With sh.Range("F6:G6").Font
        .ThemeColor = xlThemeColorAccent4
        .TintAndShade = -0.25
    End With
    
    With sh.Range("D7:E7,F7:G7").Borders(xlEdgeLeft)
        .LineStyle = xlContinuous
        .ThemeColor = 1
        .TintAndShade = 0
        .Weight = xlThin
    End With
    With sh.Range("D7:E7,F7:G7").Borders(xlEdgeRight)
        .LineStyle = xlContinuous
        .ThemeColor = 1
        .TintAndShade = 0
        .Weight = xlThin
    End With

    'Current Task Picker
    With sh.Range("A8").Resize(NUMBER_OF_TASKS)
        .Interior.Color = rgbWhiteSmoke
        .FormulaR1C1 = "=IF(AND(NOT(ISBLANK(RC[3])),RC[7]<1,RC[3]<=TODAY()),""▲"","""")"
        .HorizontalAlignment = xlRight
        .VerticalAlignment = xlCenter
        .Orientation = -90
        With .Font
            .Size = 11
            .Color = 192
        End With
    End With

    'Phase Format
    With sh.Range("B8").Resize(NUMBER_OF_TASKS)
        .FormatConditions.Add Type:=xlTextString, String:="Phase", TextOperator:=xlBeginsWith
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Font
            .Bold = True
            .Italic = False
            .ThemeColor = xlThemeColorAccent1
            .TintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
    End With
    
    sh.Range("D8:G8").Resize(NUMBER_OF_TASKS).NumberFormatLocal = "yyyy/m/d;@"
    
    sh.Columns("A:A").ColumnWidth = 3
    sh.Columns("B:B").ColumnWidth = 35
    sh.Columns("C:C").ColumnWidth = 13
    sh.Columns("H:H").ColumnWidth = 10
    sh.Columns("I:I").ColumnWidth = 10
    
    'Holiday Format
    With sh.Range("J8").Resize(NUMBER_OF_TASKS, WEEK * 5)
        .FormatConditions.Add Type:=xlExpression, Formula1:="=OR(J$7=""S"",NOT(ISNA(VLOOKUP(J$6,$AT:$AT,1,FALSE))))"
        .FormatConditions(.FormatConditions.Count).SetFirstPriority
        With .FormatConditions(1).Interior
            .Pattern = xlLightDown
            .PatternColor = 11711154
            .PatternTintAndShade = 0
        End With
        .FormatConditions(1).StopIfTrue = False
    End With
    
    With sh.Range("AT7")
        .Value = "Holidays"
        With .Interior
        .Pattern = xlSolid
        .PatternColorIndex = xlAutomatic
        .ThemeColor = xlThemeColorLight1
        .TintAndShade = 0.5
        .PatternTintAndShade = 0
        End With
        With .Font
            .ThemeColor = xlThemeColorDark1
            .TintAndShade = 0
        End With
    End With

    With sh.Range("AT8").Resize(NUMBER_OF_TASKS)
        .NumberFormatLocal = "yyyy/m/d;@"
        With .Interior
            .Pattern = xlSolid
            .PatternColorIndex = xlAutomatic
            .Color = 13434879
            .TintAndShade = 0
            .PatternTintAndShade = 0
        End With
    End With
End Sub

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。