LoginSignup
2

More than 1 year has passed since last update.

posted at

登山の新しいお供?やまみちあんしんボタン。

概要

SORACOM Button を使って山登りがちょっと安全になる?デバイスつくりました。

機能

  • ボタンを押した位置を地図上に表示する
  • 緊急時に特的の連絡先にメッセージを送る

言い訳

今回は SORACOM Button のメリットを生かすため、極力スマホに頼らない構成にしました。

と、言うのは半分言い訳で正直なとこ今回の機能はスマホのアプリで出来ちゃいます...
( YAMAP さんめっちゃ便利だからみんなも使おう!僕もいつもお世話になってます!)

でもスマホってバッテリーの持ちが悪かったり、モバイルバッテリーは重たかったりしますよね?

SORACOM Button なら...

電池式だから予備電源の確保は簡単!
持ち運びも便利な軽量設計!

なんて山登りに最適なんだ!
と言うことで、宜しくお願いします。

インデックス

なぜ SORACOM LTE-M Button なのか

今回はボタンが持つ以下3点の特徴に着目してみました。

  • 電池式
  • 軽量
  • 操作が簡単

電池式

自然の中では、当然ながら電源の確保は難しいです。
その点、電池は持ち運びが容易です。
また安価で現地調達しやすいこともメリットとして挙げられます。

軽量

電源確保にはモバイルバッテリーという手もありますが、
登山では極力荷物は軽くしたいものです。
電池はモバイルバッテリーに比べ持ち運びに優れている(と思います)。

操作が簡単

登山は危険と隣り合わせです。
初心者向けとされる山でも、厳しい岩場や滑りやすい斜面があったりします。
もし何かあったとき、ワンクリックで連絡できるのは便利かなと思います。
(※何が起きても大丈夫なように、しっかり準備して登山に臨みましょう。)

構成

スクリーンショット 2020-08-25 23.36.26.png

機能は上記したように2種類です。
それぞれの機能について説明します。

ボタンを押した位置を地図上に表示する

  1. SORACOM LTE-M Button をシングルクリックする。
  2. 位置情報(緯度と経度)を SORACOM Funk に送信し、 Lambda を起動する。
  3. Lambda にて Raspberry Pi に 位置情報を publish する。
  4. Raspberry Pi にて位置情報を元に地図に現在地をマッピングする。

今回は地図の作成に folium を利用しています。
folium を動かす端末として Raspberry Pi を採用しました。
採用理由は、すでに AWS IoT Core に接続済みのラズパイが手元にあったからです笑

Raspberry Pi 側のコードはこんな感じ。
Subscriber 兼 地図作成マシン」として利用します。(Lambda は後述)

import os
import configparser
import time
import json
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
import folium
import glob
import gpxpy
import datetime

CLIENT_ID = "{YOUR_CLIENT_ID}"
ENDPOINT = "{YOUR_ENDPOINT}"
PORT = {YOUR_PORT}
ROOT_CA = "{YOUR_ROOT_CA}.pem"
PRIVATE_KEY = "{YOUR_PRIVATE_KEY}.key"
CERTIFICATE = "{YOUR_CERTIFICATE}.crt"
TOPIC = "{YOUR_TOPIC}"

copyright_osm = '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
_map = folium.Map(location=[Mountain latitude, Mountain longitude],
                  attr = copyright_osm,
                  zoom_start=13
                  )

def main():

    load_route()

    client = AWSIoTMQTTClient(CLIENT_ID)
    client.configureEndpoint(ENDPOINT, PORT)
    client.configureCredentials(ROOT_CA, PRIVATE_KEY, CERTIFICATE)

    client.configureAutoReconnectBackoffTime(1, 32, 20)
    client.configureOfflinePublishQueueing(-1)
    client.configureDrainingFrequency(2)
    client.configureConnectDisconnectTimeout(10)
    client.configureMQTTOperationTimeout(5)

    client.connect()
    client.subscribe(TOPIC, 1, subscribe_callback)

    while True:
        time.sleep(5)


def subscribe_callback(client, userdata, message):
    print("Received a new message: ")
    print(message.payload)
    print("from topic: ")
    print(message.topic)

    params = json.loads(message.payload.decode(encoding="utf-8"))

    make_map(params)

    print("--------------\n\n")


def load_route():
    points = []

    # add map tiling options
    folium.TileLayer('Mapbox Bright').add_to(_map)
    folium.TileLayer('cartodbdark_matter').add_to(_map)
    folium.TileLayer('openstreetmap').add_to(_map)
    folium.LayerControl().add_to(_map)

    # draw log data
    for filename in glob.glob('yamareco/Tenranzan/*.gpx'):
        gpx_file = open(filename, 'r')
        gpx = gpxpy.parse(gpx_file)
        for track in gpx.tracks:
            for segment in track.segments:
                for point in segment.points:
                    points.append([point.latitude, point.longitude])
        folium.PolyLine(points).add_to(_map)
        points = []


def make_map(params):
    print(params["lat"])
    print(params["lon"])

    lat = params["lat"]
    lon = params["lon"]

    now = datetime.datetime.now()
    dt_now = now.strftime("%Y/%m/%d %H:%M:%S")

    folium.Marker([lat, lon], popup = dt_now).add_to(_map)
    _map.save('yamareco/Tenranzan/sample.html')


if __name__ == "__main__":
    main()

やってることは単純で、最初に gpx ファイルを元にルートの Marker を打ちます。

ボタン押下時は AWS IoT からデータを Subscribe して 現在地の Marker を打ちます。
同時に html に出力しています。

地図を作成する

上述の通り folium を利用しました。
Python でさっと地図を作成できるイケてるやつです。
緯度と経度を指定することで、 Marker を打つことが出来ます。
ボタンから取得した緯度と経度を元に、現在地をマッピングしています。

登山ルートを表示する

今回は以下のブログを参考に、登山ルートを出力しました。
https://chari-ngo.hatenablog.com/entry/2018/12/17/150803
先人様、いつもありがとうございます🙏

登山ルートはヤマレコで取得しました。

また folium ではどこを中心に地図を表示されるかの location を指定します。
登りたい山の location は Geocoding などで調べましょう。

緊急時に特定の連絡先にメッセージを送る

  1. SORACOM LTE-M Button をロングクリックする。
  2. 位置情報(緯度と経度)を SORACOM Funk に送信し、 Lambda を起動する。
  3. Lambda にて位置情報を LINE を送信する。

今回は連絡手段として、 LINE Notify を採用しました。

緯度と経度を LINE と Raspberry Pi に送る

今回の Lambda はこんな感じ。

# coding: utf-8

import json
import subprocess
import datetime
import boto3
import os

def lambda_handler(event, context):

  #pull out custom
  customData = context.client_context.custom

  clickType = event["clickType"]

  send_line(event, customData)
  iot_publish(event, customData)

  return {
    'statusCode': 200,
    'body': json.dumps('Hello from Lambda!')
  }

def iot_publish(event, customData):

  iot = boto3.client('iot-data')
  topic = '{YOUR_TOPIC_NAME}'

  lat = customData["location"]["lat"]
  lon = customData["location"]["lon"]

  payload = {
    "lat": lat,
    "lon": lon
  }

  try:
    iot.publish(
      topic=topic,
      qos=0,
      payload=json.dumps(payload, ensure_ascii=False)
    )

    return "Succeeded."

  except Exception as e:
    print(e)
    return "Failed."


def send_line(event, customData):

  now = datetime.datetime.now()
  time = now.strftime("%Y/%m/%d %H:%M:%S")

  clickType = event["clickType"]
  lat = customData["location"]["lat"]
  lon = customData["location"]["lon"]

  if clickType == 1:
    out_text = "Now " + '\n' + "lat: " + str(lat) + '\n' + "lon:" + str(lon)
  elif clickType == 2:
    out_text = "Now " + '\n' + "lat: " + str(lat) + '\n' + "lon: " + str(lon)
  elif clickType == 3:
    out_text = "Help! " + '\n' + "lat: " + str(lat) + '\n' + "lon: " + str(lon)
  else:
    out_text = "No clickType"

  #LINE Notify
  LINE_NOTIFY_URL = os.environ.get("{YOUR_LINE_NOTIFY_URL}")
  LINE_NOTIFY_TOKEN = os.environ.get("{YOUR_LINE_NOTIFY_TOKEN}")
  header = "Authorization: Bearer " + LINE_NOTIFY_TOKEN

  payload = "message=" + out_text

  comm = ['curl','-k','-X','POST',LINE_NOTIFY_URL,'-H',header,'-F',payload]
  out = subprocess.run(comm,stdout=subprocess.PIPE)

今回の詰まりポイント

自分への戒めとして、今回詰まったポイントを列挙します。

第一章 Lambda との戦い

まさかの Lambda から LINE Notify への通知でつまづきました。

Python 3.8 ランタイム
Response:
{
  "errorMessage": "[Errno 2] No such file or directory: 'curl'",
  "errorType": "FileNotFoundError",
  "stackTrace": [
    "  File \"/var/task/lambda_function.py\", line 31, in lambda_handler\n    out = subprocess.run(comm,stdout=subprocess.PIPE)\n",
    "  File \"/var/lang/lib/python3.8/subprocess.py\", line 489, in run\n    with Popen(*popenargs, **kwargs) as process:\n",
    "  File \"/var/lang/lib/python3.8/subprocess.py\", line 854, in __init__\n    self._execute_child(args, executable, preexec_fn, close_fds,\n",
    "  File \"/var/lang/lib/python3.8/subprocess.py\", line 1702, in _execute_child\n    raise child_exception_type(errno_num, err_msg, err_filename)\n"
  ]
}

あれ Python 3.8 の実行環境には curl 入ってない...?
ということで Python 3.7 で実行することにしました。

CloudWatch のロググループが作成されない

以下の記事を参考に解決しました。
https://qiita.com/arara_tepi/items/98f23d91eb805c6d4d71

結論を先に言うと、ポリシーの設定が正しく出来ていませんでした。
最初に Lambda 用のロールを作成する際に、 AWSLambdaBasicExecutionRole をアタッチしたので、これでログも出してくれるのかな〜と思ってたのですが、ロググループ作るには個別のロールが必要なんですかね?
今回作成した Lambda 用にポリシーを作成し、アタッチすることでロググループが作成されました。

第二章 位置情報が送れない

SORACOM Button が赤くなる

SORACOM Button ど素人だったので、赤くなったら「ネットワークが悪いのかな?」とか考えてました。
結論から言うと、赤くなったときは大体 Lambda でエラーが起きていました。
CloudWatch は偉大です。困ったらログをみましょう笑

セッション状態がオフラインになる

結論:Button の セッション状態は基本オンライン
https://dev.soracom.io/jp/plus_button/how-it-works/
公式の記事をちゃんと読みましょう(戒め)。

緯度と経度がどこにあるかわからない

clickTypeevent の中にあります。
参考:https://dev.soracom.io/jp/docs/location_service/
latloncontext の中にあります。
context.client_context.customjson が取り出せます。
先駆者様:https://qiita.com/kkimura/items/c33962e426e84720dc31
ここに気が付くのに時間がかかりました...

LINEが送れない

困ったときは CloudWatch を見よう!
LINE Notify で詰まることはありませんでしたが、まさかの JSON で詰まりました...

clickType と lat と lonは int 型

[ERROR] TypeError: can only concatenate str (not "int") to str Traceback (most recent call last):
エラーの通りですね... str() で回避しましょう。

[ERROR] TypeError: string indices must be integers Traceback (most recent call last):
json.dumps → 配列になっちゃった?

第三章 gpxpy がインストールできない

pip install で詰まる

今回、ヤマレコで取得した登山ルート(gpxファイル)を読み込むために gpxpy をインストールしました。
が、どうやら python3.6 以上でなければいけなかったようで、Raspberry Pi の初期環境ではインストールできませんでした。
せっかくなので Pipenv + pyenv で開発環境を作成しました。
(ここら辺から「別にラズパイじゃなくて、PC で良くね?」ってなったので Mac 上に環境構築してます。)

以下の記事を参考しました。

実践

結果を先に言うと...思ったようには行きませんでした🥺

理想

スクリーンショット 2020-08-25 23.43.42.png

現実

スクリーンショット 2020-08-25 23.51.04.png
(※赤、青、緑の矢印で示したボタンを押した位置はイメージです。実際の結果は多少異なる可能性がございます。)

移動しているのに、同じ位置情報しか返ってこない...

詳しいことは理解できていないのですが、簡易位置測位機能では
LTE通信のセッション確立時点でのおおよその位置情報が取得されます。
そのため基地局の位置によっては、少し移動しても同じ位置情報が返却されてしまうようでした。

まとめ

位置情報は便利!と思いつつも LTE 通信から取れる情報には限界がありそうです...

おや?こんなところに GPS の情報も取れるイケてるデバイスが...!
image.png
https://soracom.jp/products/kit/gps_multiunit/

次はこれでチャレンジしてみようかしら。

あとがき

今回やりたかったことが実現できているデバイス(しかもサービスとして提供されている)
を見つけてしまいました🥺🥺
image.png
https://trektrack.jp/

SORACOM さんのおかげで IoT が民主化された今、
アイデア力と実装スピードがモノを言う時代になるのかもしれませんね!(僕も日々精進します)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2