うごくものづくりのために

技術的な備忘録がメインです。

IoT窓センサーを作ることにした

お部屋のIoT化を進めています。
引っ越しして部屋が1Fになったため、防犯対策が喫緊の課題です。というわけで、IoT窓センサーを作ることにしました。

窓の開閉および窓の錠の開閉を検出して、何かしらの形でインターネットに通知を送る、というデバイスです。
お出かけや就寝前、あるいは外出中に、ひと目で自宅の施錠状況を確認できると便利です。(私はよく鍵をかけ忘れてしまうので…。)

コンセプトとしては「Leafee」という商品と全く一緒です。なんですが、こちらの商品は連携がイマイチそうだったので採用しません。APIが公開されていないし、IFTTT等のサービスとの連携もできないようでした。自宅の機器を連動させたスマートホームを実現するにあたって、これでは困ります。

というわけで「無いなら作る」の精神で作っていきたいと思います。

はじめに要件を整理していきます。

要件

  • 電池で駆動すること 有線での給電はなし
    • 窓などの移動するものに取り付けるので線があると取り回しが面倒
  • 頻繁な電池交換なしに、安定して動作すること
  • 取り付け・取り外しは工具無しでできること
  • 窓のすき間に設置できる程度の薄さであること
  • 防犯のため、開閉のあとすぐに通知が発行されること
    • ブザー、ライトなどとの連携を考える
  • Wi-Fi もしくは BLE などの無線通信で外部機器と通信を行えること

要件としてはこんなもんでしょうか。 これを具体化することにします。

  • 180日間電池交換無しで動作
  • 取り付けは両面テープ 跡が残らないもの
  • 厚さ 15mm以下
  • 電源はボタン電池 CR2032
  • 平均消費電力は 50uA 以下
  • 開閉検知時に割り込みを発生させる

概略設計

上記の要件から、だいたいの構成が決まってきました。

まず、メインのマイコンにはESP32を使用します。ほぼこれ一択です。
ESP32とバッテリや各種センサが搭載された M5StickC というステキなプロダクトを見つけましたが、消費電力の関係(※参考)で今回のプロダクトには採用できませんでした。

センサとしては、リードスイッチを用いることにします。窓ガラスに本体、窓の錠のレバーに磁石を貼り付けて、施錠を検知します。
センサの選択肢として ESP32 内蔵のホールセンサもありましたが、こちらは却下です。おそらくマイコンに通電している状態でないと動作しませんので、消費電力が大きくなりそうです。使えればワンチップで機能を実現できてスマートなんですけどね。

電源についてです。
電池は要件のところでも触れましたが、ボタン電池 CR2032 とします。
ESP32の推奨電圧は 3.3V ですので、CR2032 の 3V から昇圧するため DC/DC を入れます。 無負荷時の DC/DC 自体での消費電力にも注意が必要です。 省電力が求められるので、マイコンから DC/DC への電源供給を切れるようにする必要があるかもしれません。このあたりは素子の選定をしてから決まってきそうです。

メカですが、適当に3Dプリンタでケースを作り、3Mの両面テープ(3M コマンドタブ)で貼り付けられるようにしておきます。フリスクケースもありかも。

ソフト面ですが、そんなに大したことはしないのであまり気にしていません。
ESP32での開発が初めてなのでどこまでライブラリが揃っているか不安ですが、幅広く使われている製品ですし、今回の要件は比較的一般的なものだと思うので、おそらく問題ないでしょう。
処理の流れとしては、複雑なことはせず

センサ検知→起動→Wifi/BLE接続→センサ状態読み込み→通知送信→電源OFF/DeepSleep

というのを実装すれば良いかなと思っています。

最後に

今回は概略設計まで終えました。
使うデバイスがおおよそ見えてきたので、部品を買ってブレッドボードでプロトタイピングしていきます。

学生の頃以来のデバイス開発なので、開発の中で勘と腕を取り戻して行きたいところです。

ABC169

2WAで5完でした。
ABC参加4回目、初めての5完、そして初めての水色パフォが出ました。精進の結果が表れていて素直に嬉しいです。
一応茶色コーダーにもなりました。

f:id:tilt_silvie:20200602002745p:plain

ABCに参加するたびにパフォーマンスの色が1つずつ上がっているので、次は青パフォですね!(?)

A問

A - Multiplication 1

掛けるだけ。

a, b = map(int, input().split())
print(a*b)

B問

B - Multiplication 2

何も考えずに実装して1回TLEしました。正直なぜTLEしたのかまだ分かっていません。
とりあえず、要素中に 0 が一度でも出たら結果も 0 なので、要素に 0 があるか先にチェックしてやると計算量減るだろうなと思い実装したらACしました。

n = int(input())
a_list = list(map(int, input().split()))

if 0 in a_list:
    print(0)
    exit()

result = 1
for i in range(n):
    result *= a_list[i]
    if result > 10**18:
        print(-1)
        exit()
print(result)

C問

C - Multiplication 3

こちらも何も考えずに実装して1WA。
バグらせるのが難しいレベルの単純なコードなので、浮動小数の精度絡みの問題でWAしていると推測し、decimal.Decimalを用いて実装し直してAC。
ちょうどこの日に decimal.Decimal の使い方を勉強していました。ラッキー。

from decimal import Decimal, ROUND_FLOOR

a, b = map(Decimal, input().split())
print((a*b).quantize(Decimal('1'), rounding=ROUND_FLOOR))

b * 100 して整数に直してから計算というのも思いつきましたが、変なこと考えずにdecimal.Decimal使いました。

D問

D - Div Game

しばらく考えて、 N が割り切れ、かつ z=p^e を満たす z は、すなわちNの素因数だと気づきました。
あとは素因数分解して、各素因数の指数から、1から順に引き算していけば最大の操作数が取れるという方針を立てて実装し、AC。なんだかんだ25分かかりました。
最初、入力が1のときの例外処理を入れておらずバグらせかけました。サンプルケースにあってよかった。

def factorization(n):
    arr = []
    temp = n
    for i in range(2, int(-(-n**0.5//1))+1):
        if temp%i==0:
            cnt=0
            while temp%i==0:
                cnt+=1
                temp //= i
            arr.append([i, cnt])

    if temp!=1:
        arr.append([temp, 1])

    if arr==[]:
        arr.append([n, 1])

    return arr

n = int(input())
if n == 1:
    print(0)
    exit()

facts = factorization(n)
cnt = 0
for fact in facts:
    for i in range(1, 100000):
        if (fact[1] - i) < 0:
            break
        fact[1] -= i
        cnt += 1
print(cnt)

E問

E - Count Median

すべての X_i が A_i を取るときに中央値が最小になります。これは、1つを除く X_i を A_i にし、1つの X の要素を X_k を A_k より大きくしたときの中央値は、すべての X_i が A_i を取るときの中央値より小さくはならない、ということから分かります。同様に、すべての X_i が B_i を取るときに中央値が最大になります。

あとは、最小と最大の間でどういう中央値を取りうるか、です。中央値に影響を与えるXの要素を1ずつ変化させるという操作が可能なので、最小と最大の間のすべての範囲を取りうるだろう、と考えられます。

というところまで考えて、実装。一発ACして一人でガッツポーズしてました。回答まで30分強かかりました。

from statistics import median

n = int(input())
a_list = [0] * n
b_list = [0] * n

for i in range(n):
     a, b = map(int, input().split())
     a_list[i] = a
     b_list[i] = b
    
min_median = median(a_list)
max_median = median(b_list)

median_range = max_median - min_median

if n%2 == 0:
    print(int(median_range/0.5 + 1))
else:
    print(int(median_range/1 + 1))

F問

F - Knapsack for All Subsets

問題の解読だけで15分ぐらい使ってしまいました。なんとなくDPっぽいな、とは思いましたが結局何も実装できずじまいです。

振り返り

初の5完気持ちよかったです。
B問とC問でWA(TLE)出たのが気持ち悪いので、原因はきちんと勉強します。
また、素因数分解は今回人様のコードを拝借して実装したため、よく使う処理のライブラリ化も進めたいです。

ABC168

4完でした。

ABC参加3回目にして、初めて緑パフォ出ました。嬉しい。

f:id:tilt_silvie:20200517235751p:plain

A問

A - ∴ (Therefore)

文字列として受け取って、最後の1文字を切り出して判定する。

n = input()

c = int(n[-1])
if c == 3:
    print('bon')
    exit()
elif c == 0 or c == 1 or c == 6 or c == 8:
    print('pon') 
    exit()
print('hon')

B問

B - ... (Triple Dots)

これも実装するだけですね…。境界値にだけ注意。

k = int(input())
s = input()

if len(s) <= k:
    print(s)
else:
    print(s[:k] + '...')

C問

C - : (Colon)

各針の先端座標を計算して、三平方の定理を使って距離を出しました。
こういう座標計算はロボット系のプログラムだと頻出なので、見慣れた感があってかなり気楽に実装できました。まさか競プロでロボット系のプログラミングの経験が活きるタイミングが来るとは。。。

コンテスト終わったあと、TLに「余弦定理」というワードが流れてきて、あぁそういえばそんなもんあったな…という気持ちになりました。

from math import pi, cos, sin, sqrt

a, b, h, m = map(int, input().split())

theta_a = 2*pi * (h*60 + m) / (12*60)
theta_b = 2*pi * m / 60

[xa, ya] = [a*cos(theta_a), a*sin(theta_a)]
[xb, yb] = [b*cos(theta_b), b*sin(theta_b)]

distance = sqrt((xa - xb)**2 + (ya - yb)**2)
print(distance)

D問

D - .. (Double Dots)

グラフの問題だな、というのはすぐに分かったんですが、最短経路問題だと明確に気づくまでは少し時間がかかりました。
グラフ系の問題は全く練習していなかったので、以下の記事をその場で見ながらBFSを実装しました。

最短経路問題総特集!!!~BFSから拡張ダイクストラまで~ - Qiita

from queue import Queue

n, m = map(int, input().split())

adj_list = [[] for _ in range(n)]

for i in range(m):
    a, b = map(int, input().split())
    adj_list[a-1].append(b-1)
    adj_list[b-1].append(a-1)

shortest_adj = [None] * n
room_queue = Queue()
room_queue.put(0)
while not room_queue.empty():
    room = room_queue.get()
    for next_room in adj_list[room]:
        if shortest_adj[next_room] is None:
            shortest_adj[next_room] = room
            room_queue.put(next_room)

can_reach = all([x is not None for x in shortest_adj])

if not can_reach:
    print('No')
    exit()

print('Yes')
for i in range(1, n):
    print(shortest_adj[i]+1)

E問

E - ∙ (Bullet)

解けませんでした。

「1000000007っていう数字は れいきゅんのブログで見た!解き方知らね!」となってほぼ諦めムードに。

一応けんちょんさんのこの記事を見たり、自分なりにいろいろ検討してみましたが、「お互いの相性を表す N^2 のテーブルを作って…この時点で O(N^2) で無理じゃん…?」となり、匙を投げました。
あとで解説動画見て勉強します。

F問

F - . (Single Dot)

E問に無理ゲー感を感じ、試しに覗いてみました。
更に無理ゲーでした。俺にはまだ早い。

振り返り

C問が見慣れた問題設定だったので早く解け、余裕を持ってD問に取り組めたのが良かったです。 あと、前回の計算量オーバーを受け、きちんとコードを見直すようにしました。WAが出なくてよかった。

グラフは隣接リストという構造で持つと良い、というのが今回の一番の学びですね。

次のステップとして、「1000000007で割る」系の問題を解けるようになるのと、BFS,DFSあたりの基本的な探索アルゴリズムをすらすら書けるようになろうと思います。

ABC167

3完でした。

A問

A - Registration

t[0:-1] == s という簡単な条件式でOK。 2分ぐらいで提出。

s = input()
t = input()

if t[0:-1] == s:
    print('Yes')
else:
    print('No')

B問

B - Easy Linear Programming

1, 0, -1 の順番でK枚になるまで取ればいいです。 実装がダサい… と思ったけどとりあえず出しました。5分ぐらいで提出。

a, b, c, k = map(int, input().split())

total = 0

if a >= k:
    print(k)
    exit()

total = a
k = k - a

if b >= k:
    print(total)
    exit()

k = k - b
print(total - k)

C問

C - Skill Up

制約を読み間違えて時間を無限に溶かしました。つらい。

1≦N, M≦12 という制約を 1≦NM≦12 という2つの制約だと勘違いし、「Nの範囲無限じゃん、どうすんだこれ…?」となっていました。BFS,DFSやDPを最近勉強したのもあり、そのへんでどうにかなるのか…???と無限に考え込んでしまいました。 最終的に「Nは標準入力の改行数だし、改行しまくる問題作るの大変だから高々100ぐらいだろ(?)」という謎の仮定を置いて、全探索を実装しました。
途中で1回WA出しながら(indexに+1するの忘れた)およそ70分かけて提出…。

from itertools import product

n, m, x = map(int, input().split())
books = [list(map(int, input().split())) for _ in range(n)]

minimum_cost = float('inf')

buy_selections = product([1, 0], repeat=n)
for selection in buy_selections:
    bought_books = [books[i] for i in range(n) if selection[i]]
    
    total_each = [0] * (m+1)
    for book in bought_books:
        for i in range(m+1):
            total_each[i] += book[i]
    
    if any([total < x for total in total_each[1:]]):
        continue
    if total_each[0] < minimum_cost:
        minimum_cost = total_each[0]

if minimum_cost == float('inf'):
    print('-1')
else:
    print(minimum_cost)

D問

D - Teleporter

C問で時間を溶かしまくった結果、残り20分ぐらいで取り組むことに。

Kをループ部の長さで割った余りと、ループ前の部分を足せばOK、ってのは割とすぐ気づいて実装できました。
コンテスト終了直前に提出しましたが、TLE。計算量を削減できずにコンテスト終了。
TLEしたコードはこれ。

n, k = map(int, input().split())
a = list(map(int, input().split()))

history = []

index = 0
cnt = 0
loop_start_city_index = 0
while True:
    if cnt == k:
        print(index+1)
        exit()

    if index in history:    # すでに訪れた街にきた
        loop_start_city_index = index
        break
    history.append(index)
    index = a[index] - 1
    cnt += 1

from_start_to_loop_start = history.index(loop_start_city_index)
loop_length = len(history) - from_start_to_loop_start

print(history[from_start_to_loop_start + (k-from_start_to_loop_start)%loop_length] + 1)

ループの中の if index in history: が悪さしてました。ループごとに配列要素の全探索してたらそりゃ時間かかるわな。
訪れた街かどうかを保存しておく配列を作って、それを参照するコードに変えたらAC。

E問、F問

見てません。多分まだ見ても実力不足で解けない。

反省

  • 制約をちゃんと読む カンマがある→複数の制約と思い込まない
  • in とか index()O(N) かかるから気をつける 組み込み関数だからサクッと動くとか思わない

競技プログラミング始めました

以前から興味のあった競技プログラミングの世界に足を踏み入れてみました。

ふと思い立ってAtCoderのページへ行き、初心者向けの問題集 AtCoder Beginners Selction に一通りトライ。
慣れない標準入力の取り扱いを覚えながら、B問題までは自力で解けました。が、C問題については自力で解けたのは半分。

そんな状態で、たまたまその日に開かれた ABC 166に参加してみました。

A, B問題は割とすんなり解けました。C問題はWAを2回出しながらもなんとかAC。
D問題は取り組んでみたものの、難しく考えすぎて解けませんでした。 最終的な結果は以下。

f:id:tilt_silvie:20200504154523p:plain

こんな状態でソフトウェアでご飯食べてるの申し訳NASA。。。
(実際お仕事のほうはアルゴリズムゴリゴリ書く感じではないのでなんとかなっていますが)


というわけで、これから精進していきたいと思います。

直大さんのブログ を参考に、当面の目標は水色にします。

ぶっちゃけ、私は学生の頃とても成績が悪く、数学のクラスは一番下、赤点も取りまくり、みたいな人間でした。
どこまでやれるかわかりませんが、ソフトウェアエンジニアとしての矜持を保つために頑張ってみます。

とりあえず本買います。

UDP Multicastが届くことをサクッと確認する方法

iperf というツールを使います。

Ubuntu 18.04 だとaptで入りました。

sudo apt install iperf

iperfを入れたあと、以下のコマンドで確認できます。

$ iperf -s -u -B <multicast addr> -p <port> -i 1

マルチキャストが受信できていれば、1秒間隔で受信データのサイズ等のデータが流れてきます。

f:id:tilt_silvie:20200419204050p:plain

参考

networking - How can I test Multicast UDP connectivity between two servers? - Server Fault

WSL2 + VSCode + Dockerで気持ちの良い開発環境を手に入れる

皆さんお待ちかねのWSL2がいよいよ2020年4月のアップデートで来ます。
WSL2の詳細は各自調べて頂きたいのですが、個人的にはDockerが動くようになることが一番大きな嬉しさです。
Dockerが動くとなると、 VSCode Remote Containerを使って、Windows10上のVSCodeからコンテナにアクセスして直接開発ができるようになります。Dockerコンテナに開発環境と動作環境を収めることで、ホストマシンを汚すことなく環境の構築ができます。これはかなり快適です。
また、今回はPythonをターゲットに据えていますが、 C++だろうとGoだろうとRustだろうと、Docker上に環境を作ってしまえば何でもいけます。強い。

WSL2はまだ正式にリリースされていませんが、ひと足先に試してみましょう。
というわけで、WSLすら入っていない素のWindows10 Homeを、WSL2 + VSCode + Docker で Pythonの開発ができる環境に仕立てて行きます。

環境

Windows10 Home

手順

WSL2のインストール

以下のドキュメント通り進めていきます。
WSL 2 のインストール | Microsoft Docs

基本的には上記ドキュメントのとおりに進めればよいはずですが、一部トラブりました。

wsl --set-version <Distro> 2 のコマンドで、ディストリビューションのVerを切り替えようとしたのですが、うまく切り替わりません。

f:id:tilt_silvie:20200405220752p:plain
f:id:tilt_silvie:20200405220849p:plain

何やら怪しげなメッセージが出ています。カーネルを更新しろとのこと。
メッセージにあるURLにアクセスしてみます。

WSL 2 Linux カーネルの更新 | Microsoft Docs

更新プログラムをダウンロードして、インストールします。

インストール後、Powershellを再起動し再度ディストリビューションのVer切り替えにチャレンジします。

f:id:tilt_silvie:20200405221920p:plain

成功しました!

Docker Desktop for Windows Home のインストール

WSL2のカーネルを使ったWindows上でのDocker実行をサポートした Docker Desktop for Windows Home をインストールします。
こちらも2020/04/12現在まだ安定版ではありませんが、ガンガン行きます。

Docker Desktop for Windows Home is here! - Docker Blog

上記記事のこのあたりから、インストーラをダウンロードしてインストールします。

f:id:tilt_silvie:20200412145200p:plain

インストールが完了したらWindowsを再起動します。その後、PowerShellを起動してDockerのhello-worldしてみましょう。

f:id:tilt_silvie:20200412150944p:plain
Windows 10 Home でDockerが使えている!

Windows 10 Home でDockerが使えている! この時点で少し感動モノです。

VSCode Remote Container を使ってコンテナに接続する

まずはVSCodeに拡張をインストールします。
Microsoft製のリモート開発拡張は Remote - SSH, Remote - WSL, Remote - Container の3種類があります。この3つが一緒にインストールされる Remote Develpment という拡張パックが提供されていますので、こちらをインストールします。

f:id:tilt_silvie:20200412152337p:plain

次に、開発用フォルダを作成しましょう。今回はPythonを開発するという状況を想定してみます。

適当なフォルダ (今回私は vscode-container-test としました) をVSCodeで開いて、フォルダ内に Dockerfileapp.py を用意しましょう。

Dockerfile

FROM python:3.7

app.py

print('hello wsl2 docker!')

こんな感じです。

f:id:tilt_silvie:20200412153050p:plain


さて、いよいよコンテナを起動してVSCodeを接続していきます。 

下の画像の手順どおりクリックしていきます。 

f:id:tilt_silvie:20200412160651p:plain

f:id:tilt_silvie:20200412160658p:plain

f:id:tilt_silvie:20200412160708p:plain

すると、コンテナへの接続処理が始まります。完了すると、VSCodeの左下に Dev Container: Existing Dockerfile の文字が表示されるはずです!

f:id:tilt_silvie:20200412161801p:plain

ここまでくれば完了です。すばらしい開発環境を手に入れることができました。 
VSCodeのターミナルはコンテナ内で実行されますし、デバッガも動作します。 

f:id:tilt_silvie:20200412161430p:plain

お疲れさまでした。

まとめ

無事に気持ちの良い開発環境を手に入れることができました。
ホストを汚さずに、ターミナルやデバッガがきちんと動作する開発環境を構築できるのは素晴らしいですね。これでいろんなプロジェクトを心置きなく並行開発できます。

みなさんもお試しあれ。

参考

WSL2 + VScodeでWindowsから一瞬でDockerコンテナ内に引き篭もれる開発環境を整えたかった - Qiita

WSL2で劇的に変わるあなたのWebアプリケーション開発環境【その3:実践編】 | SIOS Tech. Lab

VS CodeのRemote DevelopmentでFlaskアプリをデバッグしてみた | Developers.IO