ブログに書くつもりじゃなかった

フリーのプログラマーが綴る、裏チラ系の備忘録や雑記帳。

DockerとEclipse MosquittoでMQTT

はじめに

MQTTを勉強しないといけないのでDocker上に環境を作って動かすよ。

  • ブローカーはMosquittoの公式イメージをそのまま使う。
  • Subscriber、Publisherはブローカーとコンテナを分ける。tcpdumpも使いたいのでUbuntuから作る。

環境構築

Dockerfile

Subscriber / Publisher側。Ubuntumosquitto-clientstcpdumpを入れる。

FROM ubuntu

RUN apt update \
    && apt install -y mosquitto-clients tcpdump
mosquitto.conf

Mosquittoの設定ファイル。コンテナ間で通信するための最小限の設定をする。

allow_anonymous true
listener 1883
docker-compose.yml

全体の構成を定義。

version: '3'
services:

  mosquitto-publisher:
    build: .
    container_name: mosquitto-publisher
    tty: true

  mosquitto-subscriber:
    build: .
    container_name: mosquitto-subscriber
    tty: true

  mosquitto-broker:
    image: eclipse-mosquitto
    container_name: mosquitto-broker
    volumes:
      - ./mosquitto.conf:/mosquitto/config/mosquitto.conf

動作確認

コンテナを起動する。

$ docker-compose up -d

Subscriberのコンテナに入る。

$ docker exec -it mosquitto-subscriber /bin/bash

トピックをSubscribeする。

$ mosquitto_sub -h mosquitto-broker -t mytopic -d

Publisherのコンテナに入る。

$ docker exec -it mosquitto-publisher /bin/bash

トピックにメッセージをPublishする。

$ mosquitto_pub -h mosquitto-broker -t mytopic -m "This is a test." -d

ユーザID、パスワード認証を有効にしよう

dockerコンテナに入る。

$ docker exec -it mosquitto-broker /bin/sh

パスワードファイルを新規に作成し、user1を追加。

$ mosquitto_passwd -c pwdfile user1
Password: 
Reenter password: 

パスワードファイルにuser2を追加。

$ mosquitto_passwd pwdfile user2
Password: 
Reenter password: 

できたファイルを確認する。

$ cat pwdfile 
user1:$7$101$Lt7Tg/kTMwmZXe6D$mnDP9IpgkkOMQ6EV7E4mytxWvXAMNmPP391/j1YfkrgJENEAJ3W/1jFm20SRijLAYch6fwqnRw8UojVsAFlipw==
user2:$7$101$l/J0c5HiY31XtIFY$4rQkP4egix5w9iEESkLLSCjUmveVUHhjqdAuEb2fGL/4r5dW7aT98DM4+ASI/vnXhQfKWw06FZlcPqOoPbdL3w==

一旦コンテナから抜ける。

$ exit

mosquitto.confを編集。

allow_anonymous false
listener 8883
password_file /pwdfile
cafile /mosquitto/certs/ca.crt
keyfile /mosquitto/certs/server.key
certfile /mosquitto/certs/server.crt

再起動。

$ docker-compose restart

通信内容を確認しよう

Subscriber / Publisherコンテナ上でtcpdumpを実行する。-wオプションで内容をファイル出力する。

$ tcpdump -i any -w test.cap port 1883

ひと通り動かしたらファイルをホストにコピーする。

$ docker cp mosquitto-subscriber:/test.cap .

Wiresharkでファイルを開いて、MQTT Specificationを見ながらお勉強。ふーん。なるほどー。

PostgreSQLで5分単位にレコード集計

やりたいこと

timestamp型のカラムを持つテーブルがあって、5分単位、10分単位にレコードを集計したい。みたいなことが2、3年に一度ある。その度に試行錯誤しているので、未来の俺に向けてここにメモを残す。

 

前提バージョン

9.6.10

 

使用するテーブル

2020年大阪国際女子マラソンのリザルトを管理するテーブルを用意する。面倒だから準備するレコードは上位20名までに限定する。なおfinished_atはゴールした日時でタイムじゃないんだけど、分かりづらかったな。

postgres=# \d osaka_marathon_2020_result
                Table "public.osaka_marathon_2020_result"
  Column   |           Type             | Collation | Nullable | Default
-------------+-----------------------------+-----------+----------+---------
no         | integer                     |           |         |
name       | character varying(32)       |           |         |
finished_at | timestamp without time zone |           |         |

postgres=# select * from osaka_marathon_2020_result;
no |             name             |     finished_at    
----+------------------------------+---------------------
 6 | 松田 瑞生                   | 2020-01-26 14:31:47
 3 | ミミ・ベレテ                 | 2020-01-26 14:32:40
 7 | シンタエフ・レウェテン       | 2020-01-26 14:33:03
 2 | メスケレム・アセファ         | 2020-01-26 14:33:31
10 | リサ・ウェイトマン           | 2020-01-26 14:36:02
 4 | ボルネス・ジェプキルイ       | 2020-01-26 14:36:24
16 | 山口 遥                     | 2020-01-26 14:36:35
 9 | ファツマ・サド               | 2020-01-26 14:37:18
 1 | ハフタムネッシュ・テスファイ | 2020-01-26 14:37:50
13 | 田中 華絵                   | 2020-01-26 14:37:51
41 | 井上 彩花                   | 2020-01-26 14:37:54
19 | ムンフザヤ・バヤルツォグト   | 2020-01-26 14:38:03
 8 | 小原 怜                     | 2020-01-26 14:38:12
14 | カタリナ・スタインラック     | 2020-01-26 14:38:48
11 | 谷本 観月                   | 2020-01-26 14:38:48
42 | 兼重 志帆                   | 2020-01-26 14:38:51
43 | 西田 美咲                   | 2020-01-26 14:38:51
44 | 水口 瞳                     | 2020-01-26 14:42:33
17 | 下門 美春                   | 2020-01-26 14:42:48
12 | 竹地 志帆                   | 2020-01-26 14:44:09
(20 rows)

 

分単位に集計する場合

単純に日単位、時間単位、分単位に集計するのであれば、date_trunc関数が使える。

postgres=# select date_trunc('minute', finished_at), count(*) from osaka_marathon_2020_result group by 1 order by 1;
    date_trunc     | count
---------------------+-------
2020-01-26 14:31:00 |     1
2020-01-26 14:32:00 |     1
2020-01-26 14:33:00 |     2
2020-01-26 14:36:00 |     3
2020-01-26 14:37:00 |     4
2020-01-26 14:38:00 |     6
2020-01-26 14:42:00 |     2
2020-01-26 14:44:00 |     1
(8 rows)

 

5分単位に集計する場合

こんな感じになる。

postgres=# select to_timestamp(trunc(extract('epoch' from finished_at) / 300) * 300), count(*) from osaka_marathon_2020_result group by 1 order by 1;
    to_timestamp     | count
------------------------+-------
2020-01-26 14:30:00+00 |     4
2020-01-26 14:35:00+00 |   13
2020-01-26 14:40:00+00 |     3
(3 rows)

やっていることは以下の通り。

  1. extract関数でタイムスタンプ型からUNIXエポック時間を取得。

  2. UNIXエポック時間を集計単位(例では300秒)で割る。

  3. trunc関数で小数点以下を切り捨てる。

  4. 集計単位(例では300秒)を掛けて、丸めたUNIXエポック時間を得る。

  5. to_timestamp関数でタイムスタンプ型に変換する。

 

ちなみにto_timestampの戻り値はtimestamp with time zone型なので、元のカラムのtimestamp without time zone型と合わせるなら最後にtimezone関数を使ってあげれば良い。

 

跋文

今年の大阪国際女子マラソン長居公園の周回路にコースを変更すると、先ほど公式発表があったようだ。びわ湖毎日マラソンとか名古屋ウィメンズマラソンはどうなるんだろう?

Java + Spatial4jでGeofence

やりたいこと

多角形(ポリゴン)または円(サークル)でGeofenceを定義する。それでデバイスの現在位置がGeofenceの内側にいるか外側にいるかを判定する。

こんなイメージ。 f:id:edts:20210109133554p:plain   © OpenStreetMap contributors

Spatial4jとは

地理空間情報を扱うためのJavaライブラリ。公式サイトはこちら

試してみる

pom.xml
<dependency>
    <groupId>org.locationtech.spatial4j</groupId>
    <artifactId>spatial4j</artifactId>
    <version>0.8</version>
</dependency>
多角形(ポリゴン)型Geofence
import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
import org.locationtech.spatial4j.io.WKTReader;
import org.locationtech.spatial4j.shape.Shape;

public class Spatial4jPolygonTest {

    public static void main(String[] args) {

        JtsSpatialContext ctx = JtsSpatialContext.GEO;  // (1)

        try {
                WKTReader reader = new WKTReader(ctx, null); // (2)
            // 仙台市営地下鉄南北線、東西線のターミナルを頂点としたPolygonをGeoFenceとする
            Shape polygon = reader.read("POLYGON(("
                    + "140.880560 38.324137, "  // 泉中央駅
                    + "140.948398 38.244902, "  // 荒井駅
                    + "140.870713 38.214264, "  // 富沢駅
                    + "140.843720 38.243071, "  // 八木山動物公園駅
                    + "140.880560 38.324137"    // 泉中央駅
                    + "))"); // (3)

            // デバイスが仙台駅にあるケース
            Shape point0 = reader.read("POINT(140.882448 38.260300)");   // (4)
            System.out.println(polygon.relate(point0)); // (5)

            // デバイスが小鶴新田駅にあるケース
            Shape point1 = reader.read("POINT(140.934837 38.273621)");
            System.out.println(polygon.relate(point1));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 何をやるにしてもJtsSpatialContextが必要になるので、まずはこれを取得する。JtsSpatialContext.GEOでシングルトンインスタンスが用意されているので、そのまま使用する。
  2. 今回はGeofenceと位置情報をWKT(Well-known text)で用意する。そのため、WKTReaderインスタンスを作成する。WKTの詳しい説明はウィキペディアが参考になる。
  3. GeofenceをPolygonで表したWKTWKTReaderに読み込ませて、Shapeオブジェクトを作成する。
  4. 比較対象とするデバイスの位置情報はPointで表す。Geofenceと同様にShapeオブジェクトを作成する。
  5. GeofenceのShape.relate()を呼び出し、2つのオブジェクトの関係を求める。

実行するとこうなる。

CONTAINS
DISJOINT

Shape.relate()の戻り値はSpatialRelationEnum。PolygonとPointとの比較だとCONTAINS(比較対象が含まれている)、 DISJOINT(比較対象との接点がない)のどちらかしか返さないが、他にINTERSECTS(比較対象と交差している)、 WITHIN(比較対象の中にある)がある。

ちなみに、180度経線(下の図の赤い線)を跨いで領域を定義した場合、ライブラリによってはうまく結果を返さないものもある。Spatial4jは問題なく動いた。これならフィジー諸島とかロシアでもバッチリ監視できる。

f:id:edts:20210109135109p:plain © OpenStreetMap contributors

円(サークル)型Geofence

Geofenceの中心とデバイスの位置情報の2点間の距離を測れば事足りる気がする。だけど、円を表すGeoCircleクラスがあるので、せっかくなので使ってみる。

import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.io.WKTReader;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.impl.GeoCircle;

public class Spatial4jCircleTest {

    public static void main(String[] args) {

        JtsSpatialContext ctx = JtsSpatialContext.GEO;

        try {
        WKTReader reader = new WKTReader(ctx, null);

            Shape center = reader.read("POINT(140.878100 38.288440)"); // 台原駅
            // 半径2kmをdegreeに変換
            double radiusDEG = DistanceUtils.dist2Degrees(2.0d, DistanceUtils.EARTH_MEAN_RADIUS_KM);  // (1)
            // 台原駅を中心とした半径2kmをGeofenceとする
            GeoCircle circle = new GeoCircle((Point) center, radiusDEG, ctx); // (2)

     // デバイスが旭ヶ丘駅にあるケース
            Shape point0 = reader.read("POINT(140.885062 38.296344)");
            System.out.println(circle.relate(point0));

     // デバイスが八乙女駅にあるケース
            Shape point1 = reader.read("POINT(140.883689 38.312862)");
            System.out.println(circle.relate(point1));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. GeoCircleの半径はdegree(度)で指定するので、「中心から半径2km」のように指定する場合は、DistanceUtils.dist2Degrees()で変換処理を行う必要がある。ちなみにDistanceUtilsEARTH_MEAN_RADIUS_KM(平均の半径?: 約6371km)とEARTH_EQUATORIAL_RADIUS_KM(赤道半径: 約6378km)と2つの半径がある。ここではGoogle Mapの距離測定と結果が近い方のEARTH_MEAN_RADIUS_KMを採用。
  2. 中心と半径、JtsSpatialContentを指定してGeofenceを表すGeoCircleオブジェクトを作成。

以降は先ほどと同様。実行するとこうなる。

CONTAINS
DISJOINT

最後に

Big Brother is watching you.

Python + NLTKで英語を品詞ごとに色分け表示するWebサービスを作ってみる

序文

先日、久しぶりにTOEIC受験を申し込んだので、気合を入れて効率良く英語を身に着ける方法についての勉強を始めた。その中で、英文を品詞ごとに色分けしてくれるWebサービス(https://english.edward.io)を見つけた。

へー、よくできているなー...と感心したので、どうやって実装できるのか調べてみた。すると、自然言語処理(NLP)のライブラリを使えば割とシンプルに実装可能なことが判明。そこで、ちょっと勉強がてら自作してみることに。言語はPython、ライブラリはNLTK(Natural Language TookKit)を使用する。ちなみにPythonは触ったことある程度。

事前準備

作業に使うM1 MacBook Airは買ったばかりで、python周りはまっさらな状態。なので、いくつか事前に準備しておく。

Python 3.xをデフォルトに変更する

とりあえずバージョンを確認。

# macOS
$ sw_vers                                                   
ProductName:    macOS
ProductVersion: 11.1
BuildVersion:   20C69
# python (default)
$ python --version
Python 2.7.16
# python 3.x
$ python3 --version      
Python 3.8.2
# python 2.x
$ python2 --version
Python 2.7.16

デフォルトが2系になっているので、これを3系に変更する。vi ~/.zshrcで以下を追加する。

alias python="python3"

ファイルを保存したらsourceで反映する。

source ~/.zshrc

pipをインストールする

$ curl https://bootstrap.pypa.io/get-pip.py -s -o get-pip.py
$ python get-pip.py

pipを入れたディレクトリをPATHに追加する。vi ~/.zshrcで以下を追加する。

export PATH=$PATH:/Users/****/Library/Python/3.8/bin

新しいバージョンにアップグレード(19.2.3 -> 20.3.3)。

$ python -m pip install --upgrade pip

NLTK

以下、公式ドキュメント(https://www.nltk.org)に従って作業する。

NLTKをインストールする

$ pip install nltk

NLTKで使用するデータをダウンロードする

NLTKを使用する際にはコーパスやその他のデータが必要になる(とドキュメントに書いてあるが意味を理解していない)。pythonインタラクティブモードで起動して以下を実行。

>>> import nltk
>>> nltk.download('all')

自分の環境ではnltk.download()と引数なしで実行するとエラーが発生するので、'all'を付けた。

動作確認してみる

>>> # 赤い色のモビルスーツ... ザクじゃないけど赤い色のモビルスーツ... シャアじゃないのか!?
>>> sentence = "A red mobile suit... It's not a Zaku but good red mobile suit mean... Char is in it!?"
>>> tokens = nltk.word_tokenize(sentence)
>>> tagged = nltk.pos_tag(tokens)
>>> tagged
[('A', 'DT'), ('red', 'JJ'), ('mobile', 'NN'), ('suit', 'NN'), ('...', ':'), ('It', 'PRP'), ("'s", 'VBZ'), ('not', 'RB'), ('a', 'DT'), ('Zaku', 'NNP'), ('but', 'CC'), ('good', 'JJ'), ('red', 'JJ'), ('mobile', 'JJ'), ('suit', 'NN'), ('mean', 'NN'), ('...', ':'), ('Char', 'NNP'), ('is', 'VBZ'), ('in', 'IN'), ('it', 'PRP'), ('!', '.'), ('?', '.')]

英文を入力として単語とその品詞が取得できた。品詞の詳細については以下で確認できるみたい。

>>> nltk.help.upenn_tagset()

次のステップ

で一通り完成するはずだが、ここまで結構な時間を費やしたのに肝心の英語の勉強が全然進んでない!のでこのままで一旦終わりかな。

Oracleの結合演算子

例えば、AB二つの表があって、それを外部結合を使って検索する。外部結合演算子(+)を使うとこんな感じで記述できる。

 

SELECT * FROM A, B WHERE A.col1 = B.col1(+)

 

この程度は自分も知っていた。で、右側の表Bに条件を付けるとする。こう書けるのか?

 

SELECT * FROM A, B WHERE A.col1 = B.col1(+) AND  B.col2 IN (1, 2)

 

いや、col2の値が1, 2以外の行は返されない。これでは外部結合にならない。

これも何となく知っていた。問題はここからだ。上記の問題をどう回避するのか。

 

SELECT * FROM A, B WHERE A.col1 = B.col1 (+) AND  (B.col2 IN (1, 2) OR B.col2 IS NULL)

 

長い間こんな風に書いてきた気がするんだけど、もう少しエレガントな記述ができるようで。

 

SELECT * FROM A, B WHERE A.col1 = B.col1(+) AND  B.col2(+) IN (1, 2)

 

先月手伝ってた現場でこれを知ったとき、期待通りに動作することをこっそりテストしながら、今まで知らなかった恥ずかしさと感動に浸っていた...が。

改めてOracleのリファレンスを読んでみると、結合演算子は制限があるからOUTER JOIN推奨なのだそうで。なら今後、積極的に使うこともないだろう。あぁ、何だか力抜けるなぁ...

 

と、仕事を通じて泣いたり笑ったりした出来事をここに残していこうかと思ってます。でも、まずは脱・3日坊主が目標。ネタに窮してyoutubeの動画を埋め込んでるだけの記事でも、コンスタントに(週一くらいで)アップするつもり。

 

CSSはそのうちカスタマイズしておきます。