目に見えない信号やノイズについて

この記事は Akerun Advent Calendar 2020 - Qiita の24日目の記事です。

初めまして、 hiro_kawa です。2020年3月からPhotosynthの開発部で電気設計を担当しています。今年は久々に一担当としてガッツリ電気設計を行うことが出来て、忙しいながらも楽しい一年だったな~と思います。ホントあっという間でした。

さて、私からは電気設計関連のお話をさせて頂きます。 電気設計で取り扱いが難しく注意が必要なのは、目に見えない信号やノイズです。 電波や磁界、静電気など、種類は様々。静電気の場合はパチンと光って見える事もありますが、その後の電流の流れまでは見えません。 Photosynthに入社してからも、いくつか新しい発見や経験ができたので、その事例をご紹介します。

波形測定の際に

NFCカードリーダーの電源波形をオシロで確認していた時に、大きなリップルノイズが確認されることがありました。ICが正常に動作しない、場合によっては壊れてもおかしくないレベルです。プローブをあたる位置でレベルがかなり変動しており、NFCのアンテナに近い位置だと特に大きくなります。

そこで、その波形の時間軸を拡大してみると13.56MHzの信号を発見、NFCの周波数です。

f:id:photosynth-inc:20201224100704p:plainf:id:photosynth-inc:20201224100713p:plain
オシロスコープよりキャプチャしたNFC波形

その時は手軽に信号をモニターするためにパッシブプローブを使い、写真のようなリードクリップでGND接続をしていました。

f:id:photosynth-inc:20201224100701j:plain:w300
リードクリップ

実はこのGND線のループが悪さをしており、NFCのループアンテナとリード線とのかぶり加減でノイズレベルが変動している事がわかりました。パッシブプローブなのでインピーダンスが高く、プローブ自体がアンテナとなってNFCの電波をひろってしまっていた、という訳です。

画にしてみるとこんな感じでしょうか。

f:id:photosynth-inc:20201224100708p:plain:w300
プローブが作るループを、アンテナの磁界が通る

プローブ線のループが大きいほど、その中を通る磁束が増えるため、NFCの信号を拾いやすいという原理です。

そこで、ループを小さくするため、プローブ付属のバネ接点にGNDを変更すると、ノイズレベルはぐっと低減して、安定した信号を確認することができました。

f:id:photosynth-inc:20201224100710j:plain:h250f:id:photosynth-inc:20201224100717p:plain:h250
バネクリップと改善後の波形

これなら問題ないレベルです(これでも若干ノイズを拾っていますが)。 更にアクティブプローブにすればより正確な波形が確認できるでしょう。

ちょっとした波形確認の時でも気をつけないといけないな、と思いました。

静電試験の際に

カードリーダーの静電試験を行っていた時のこと。

※静電試験の概要については以下サイトなどご参照下さい。

カードリーダーを試験卓と水平に横置きし静電気を印加すると読み取りが出来なくなる症状が発生しました。

一方で実際に設置される環境通り、縦置きでの静電気印加では問題ありません。 しかも横置きでもカード読み取り部を上にし下側から静電気を印加しても問題ありません。 カード読み取り部を下にして上から電極に静電気を印加した時にのみ問題が発生します。

ここでのポイントは、試験サイトのテーブルには鉄板が敷かれていることです。図にしてみるとこんな感じです。

f:id:photosynth-inc:20201224100658p:plain
静電試験の模式図

NFCのループアンテナを挟むような形で静電気を印加した場合に問題が発生しています。 そこでアンテナが静電気の誘導ノイズをひろっているのではないか?と想定しました。 NFCのデバイスとアンテナとの接続部分を分断し、同条件で症状確認してみると、問題は発生しなくなりました。

原因が特定出来たので、そこを対策しようとなる訳です。

アンテナの性能が良い方が、よりカードを読み取りやすい訳ですが、その分ノイズ影響も受けやすくなります。 アンテナ性能を保ちながら、ノイズ影響は受けにくくする工夫が必要で、そこが設計の面白いところですね。

終わりに

上の2例は電気設計者ならわかりやすい内容だと思います。 他の事例では、動作中の基板の写真を撮ろうとフラッシュをたくと誤動作が発生、ということもありました。この原因はどのように推察されますか? これも調べると興味深いことがいくつかわかったのですが、そのお話はまたの機会に。。。

仕事の上では問題は起きないに越したことはないですが、私なんかは問題が起きると少しワクワクします。 というのも、その問題を解決することで新しい発見や勉強になることが多々ありエンジニアとして成長できるからです。

Photosynthでは、そんな経験が出来ると思いますので、ご興味持たれた方がいらっしゃいましたら、是非、ご連絡下さい。 ひょっとすると来年この記事を見た方と一緒にお仕事しているかも知れませんね。 そんな事になったら嬉しいです。では、皆さま、良いお年を!!


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com

人に優しい識別子を使おう

この記事は Akerun Advent Calendar 2020 - Qiita の22日目の記事です。

こんにちは。@ps-tsh です。フォトシンスでは API Server などバックエンドシステムの開発を担当しています。先日、設計レビュー中に「識別子として UUID を使うのは妥当か」という話でちょっと盛り上がったので、きょうはそのあたりの話をしたいと思います。

連番IDのデメリット

「データの識別子にはどのような値を使うべきか」というテーマに対し、まずはもっとも一般的な連番IDの話から入りましょう。

たとえば Restful な API であれば、id=123 のユーザがいたとして GET /users/123 のようにアクセスします。実装レベルでは id として RDBMS が生成した連番を使うことが多いので、フレームワークのサポートも手厚く開発するのも楽です。しかし、連番のIDは数値を増減させるだけで他のデータにアクセスすることができるため、いわゆる総当たり攻撃を受けやすいというデメリットがあります。また、商用サービスであれば連番の最大値、すなわちデータ量からおおよその事業規模を推測されてしまうリスクもあるといえるでしょう。

ならばランダムIDだ

そこで、上記の問題を解決するためのアイディアとして「IDをランダムに発番する」という方法が出てきます。ID の本来の役割は識別子(identifier)なので、他と重複しないランダムな文字列を生成して使えばいいんじゃないかと考えるわけです。最近のプログラミング言語であれば、なんらかのランダムな値を生成する方法が提供されていると思います。たとえば Ruby では SecureRandom モジュールのメソッドを使うことで簡単にランダムな文字列を生成することができます。

SecureRandom モジュールの利用例

irb(main):001:0> require 'securerandom'
=> true
irb(main):002:0> SecureRandom.uuid # ランダムなUUIDを生成する
=> "5308b22c-2692-4b99-a404-316eaaf3088a"
irb(main):003:0> SecureRandom.base64 # ランダムなBase64文字列を生成する
=> "e/5gLBPoaxntIDeRwZlWPA=="
irb(main):004:0> SecureRandom.random_number(100000000) # 最大100000000のランダムな数値を生成する
=> 35831984

連番のかわりにランダムな値を使用することで、連番IDで課題となっていた総当たり攻撃やデータ量の推測は難しくなります。

ランダムIDのデメリット

連番のIDをランダム値に置き換えることで、エンジニアリング視点では問題が解決されたように見えます。では、ランダムなIDはあらゆる点でパーフェクトな解決策なのでしょうか。もちろんそんなことはなく、ランダムなIDにもデメリットがあります。

デメリット1: 長い

ランダムなIDは登録時の衝突を回避する必要があるので、連番の数値と比べると長い文字列になってしまいます。コードで処理する分には変わりませんが、人間が目視して入力する必要がある環境では、長いID文字列はどうしても使い勝手が悪くなってしまいます。たとえばUUIDを目視一回で覚えて、ミスせずにすべて入力するのはなかなか難しいですよね。

ランダムなIDの例: UUID

irb(main):009:0> SecureRandom.uuid
=> "033f9297-4393-463d-b63d-d6e3b4b05339"

また、長いランダム値はAPI設計にも影響をおよぼします。APIエンドポイントにパラメータとしてIDを含める場合、数値型のidであれば

GET /organizations/123/members/456?option=789

のようになりますが、これがUUIDだと

GET /organizations/648df1f4-03a7-450a-9d0a-352e742a5d57/members/83f50878-3726-4cb2-ab25-6db4f994ccb1?option=fd1b42bb-226c-4242-a667-13b13523147d

のようにとても長い文字列になってしまいます。頻繁に起こるケースではありませんが、パスパラメータやクエリパラメータに長大なIDが多数並ぶとブラウザやWebサーバが許容するURLの上限を超えてしまうこともあるため、IDはできるだけ短くしておいたほうが良いといえるでしょう。

この他、ストレージ効率が悪いとかインデックスサイズが増えて検索パフォーマンスが落ちるといった話もありますが、これらはあくまで副次的な話で、計算機リソースが安い現在においては最初に考えることではないと思います。

デメリット2: 紛らわしい文字が入る

「ランダムIDの文字列長が長くなってしまうなら短くすればいいじゃないか」とうことで、今度は使用する文字を増やして桁数を減らすことを考えてみましょう。たとえば Base64で使用する範囲の文字(0-9A-Za-z, +, /)を用いたランダムIDを生成してみます。

ランダムなIDの例: Base64

irb(main):008:0> SecureRandom.base64(16)
=> "ClnkCQEMel0XeDLYjgI0bg=="

同じ16byteのデータですが、Base64で表現することで文字列としてはUUIDと比べて(36→24と)短くなりました。しかし、文字の種類が増えたことで今度はIDに紛らわしい文字が含まれてしまいます。これも人間が手入力する上で困難なポイントになりえます。紛らわしい文字はITリテラシーの高くない人ほど間違えやすく、そうでなくてもフォントによっては非常に見分けがつきにくいため、クーポンコードや問い合わせ番号のようなものに採用すべきではありません。以下に、数字と紛らわしい英字の例を挙げます。

数字 紛らわしい英字の例
1 I, i, l
2 Z, z
5 S, s
6 b
9 q, g
8 B
0 O, o

また、英字には大文字・小文字で同じ形のものが多いため、文字種を増やしたいからといって混在させることも避けた方がよいでしょう。

デメリット3: 並べ替えられない

これは必ずしもランダムIDのデメリットとして挙げるべきものではないのですが、連番IDに期待されている性質として「並べ替えができる」というものがあります。たとえば「注文履歴を注文番号で並べ替えたら時系列順に並んで欲しい」といった要件がある場合、ランダムIDを採用してしまうと目的を果たすことができなくなります。もちろんこのケースは「注文日時で並べ替える」という方法で解決すべきですが、「IDに大小関係(並べ替え可能性)が期待されていることは多い」と心得ておいて損はないと思います。

人にやさしいIDをつくろう

ここまで、ランダム値ベースのIDについてデメリットをいくつか挙げてみました。ここからは、ランダム値の性質を保持しつつ、かつ人にもやさしいIDを設計するためのポイントについて紹介したいと思います。

ポイント1: 名前空間のサイズを考える

最初に考えるべきことは名前空間のサイズ想定です。IDを短くするためには必要以上に広い名前空間を使わないことがポイントになります。本記事の前半ではランダムIDの例としてUUIDを紹介しましたが、IDとして本当にUUIDが必要なケースははたして多いのでしょうか。UUIDの名前空間は128bitもあります。対象データの件数が数十万〜数千万程度であれば、32bit(正の整数で約21億)もあれば十分に収まります。多少上振れしても64bitあれば十分であることがほとんどです。

ポイント2: 表記上の桁数を減らす

UUID→Base64のところでも少し触れましたが、ID文字列の桁数を減らすためには、使用する文字数を増やす方法が効果的です。以下に示すように、同じ数値を表す場合でも2進数より8進数、10進数、16進数の方が桁が短くなりますよね。数字と英字の組み合わせであれば36進数まで表現できます。Rubyの場合、Numeric#to_s(n)を使えば36進数までの数を文字列に変換してくれます。

同じ数値(12345678)を2, 8, 10, 16, 36進数で出力したもの

irb(main):010:0> 12345678.to_s(2)
=> "101111000110000101001110"
irb(main):011:0> 12345678.to_s(8)
=> "57060516"
irb(main):012:0> 12345678.to_s(10)
=> "12345678"
irb(main):013:0> 12345678.to_s(16)
=> "bc614e"
irb(main):014:0> 12345678.to_s(36)
=> "7clzi"

ポイント3: 文字を増やした上で紛らわしい文字は除外する

上記で紹介した36進数を使うと、紛らわしい文字が含まれてしまいますよね。そこで、以下のように紛らわしい文字を除外したエンコーダを作ってn進数を出力することを考えます。

# 所定の文字セットを使って数値をn進数表記に変換する
def encode(n)
  # 例として数字の0, 1と英大文字のI, O を除外したものを使う。英小文字は使わない
  # 並べ替え可能性を保つため、文字の順番はASCIIコード順を保っておく
  chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'.split('')
  result = []
  while n > 0 do
    div, mod = n.divmod(chars.length)
    result.unshift(chars[mod])
    n = div
  end
  # 連結して出力
  result.join
end

出力例

irb(main):067:0> encode(12345678)
=> "DSSCG"

ポイント4: 並べ替え可能にする

前述の通りこれはIDとしての必須要件ではありませんが、ランダムIDを並べ替え可能にするためには、先頭に並べ替え可能なパートを付与する必要があります(厳密にはこの時点でもはやランダムではないのですが、そこは置いておきましょう)。以下はタイムスタンプを含めたランダムIDの例です。

# 前半にタイムスタンプのパートが含まれているため並べ替え可能になっている。
# 実際はID文字列を連結する場合「各パートを固定長にする」「タイムスタンプの精度が秒単位でよいか確認する」など
# もうひと手間かける必要がある
irb(main):097:0> encode(Time.now.to_i) + encode(SecureRandom.random_number(100000000))
=> "3HY23UC34C4EF"
irb(main):098:0> encode(Time.now.to_i) + encode(SecureRandom.random_number(100000000))
=> "3HY23UE3AQUGQ"
irb(main):099:0> encode(Time.now.to_i) + encode(SecureRandom.random_number(100000000))
=> "3HY23UF3QBUMH"

ポイント5: 桁区切りを入れる

最後になりますが、人間からみた可読性を上げるテクニックとして、適切な文字数で桁区切りを入れる方法も有効です。たとえば3HY23UF3QBUMH であれば 3HY2-3UF3Q-BUMH のように分割することで入力値の確認がしやすくなると思います。

おわりに

今回は「人に優しい識別子を使おう」ということで、識別子としてランダムIDを採用する場合に考えておきたいことをいくつか紹介しました。id設計においてちょっとでも役立ったと思っていただけたら嬉しいです。


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com

専用アプリはもう要らない?LINE BotからAkerunを操作する(その2)

この記事は Akerun Advent Calendar 2020 - Qiita の13日目の記事です。

WebエンジニアのBunです。主にiOSアプリの開発を担当しています。

ちょうど1年前、「LINE BotからAkerunを操作する(その1)」を書かせていただきました。 LINE Botのグループ機能を使ったBotLINE BOT AWARDS グループトーク部門賞)を作成した経験もあるので、今回はその続きの内容(LINEグループでAkerunを操作する)になります。

akerun.hateblo.jp

17日目の記事(LINE Botから施錠解錠する)も書いたので、こちらも読んでみてください。

akerun.hateblo.jp

事前準備

LINE Botの設定

その1を参考にLINE Botを作成し、Botと友達になります。

Akerun合鍵発行

対象となるAkerunの合鍵を発行します。弊社のAkerun Developersをご参照ください。

developers.akerun.com

LINE Botのリッチメニューからインタラクティブ形式で複数の組織、Akerun、合鍵の設定も可能ですが、長くなってしまうので、別の機会で書きたいと思います。

データ保存用DB

グループ、ユーザー情報を保存する必要があるので、Bot側でデータベースを使う必要があります。Firebase、AWSなど各自の環境に合わせてDBを構築すれば良いと思います。 今回は、軽量ドキュメントデータベースのTinyDBを使いますが、DB周りの詳細処理は割愛します。

LINE Loginチャンネル作成

LINE Developersにログインし(アカウント、Providerが無い場合作成する)、LINE Loginチャンネルを作成します。

  • チャンネル作成

チャンネル作成

  • Channel IDとChannel secret

環境変数に設定する必要があるので、Channel IDとChannel secretをメモします。

Channel ID

Channel secret

  • Callback URLを設定

Botに合わせてドメインとPathを設定します。

Callback URL

LINEのグループ(或いはルーム)を作成

LINEで任意のグループを作成し、Akerunを使うユーザーを招待します。Botと連携してから招待しても問題ありません。

Botをグループに招待する

通常ユーザーの招待と同じ方法で既に友達になったBotをグループに招待します。

  • JoinEvent

Botをグループに追加した場合、JoinEventが発生します。eventからグループID(或いはルームID)を取得し、DBにグループを追加します。

  • LINE Login用Link作成

グループ内のユーザーがBotと友達になるようにLINE LoginのLinkを作成し、グループにメッセージを通知します。 LinkにLINE LoginチャンネルのChannel IDとCallback URLを指定します。 LINE Loginボタン(画像デザイン)について、以下をご参照ください。

developers.line.biz

# BotをGourpに追加した時、/callbackが呼ばれて、その後JointEventが呼ばれる
# 同じグループに複数Bot追加できない。2個目からは招待待ちになる。
@handler.add(JoinEvent)
def handle_join_message(event):
  print(event)
  if event.source.type == 'group':
    gid = event.source.group_id
  elif event.source.type == 'room':
    gid = event.source.room_id
  
  # 既に存在する場合追加されない
  db.add_group(gid, event.source.type)

  msgs=[]

  text = u'下のログインボタンで鍵を申請してください。'
  msgs.append(TextSendMessage(text = text))
  
  link_uri = 'https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id={}&redirect_uri={}&bot_prompt=normal&scope=profile&state={}'.format(line_login_channel_id, urllib.parse.quote(auth_url), gid)

  print(link_uri)
  msgs.append(ImagemapSendMessage(
    base_url=base_url + '/images/LINELogin',
    alt_text='this is an imagemap',
    base_size=BaseSize(height=302, width=1040),
    actions=[
      URIImagemapAction(
        link_uri=link_uri,
        area=ImagemapArea(x=0, y=0, width=1040, height=302)
      ),
    ]
  ))

  line_bot_api.reply_message(event.reply_token, msgs)

LINE Loginでログインし、Botと友達になる

グループ内のユーザーはLINE Loginボタンを押してBotと友達になります。 Botと友達になった場合、LINE Loginチャンネルで設定されたCallback URLにリダイレクトされます。ここでログインユーザー情報を取得し、DB上のグループにユーザーを追加します。LINEグループにもメッセージを通知します。

# グループに入るユーザーにログイン用ボタンLink(clientID/redirectURL)を用意し、ログインしてもらう
# ユーザーがログインボタンを押して、Botと友たちになる時、/authが呼ばれる
# ログイン成功後、/authにcodeが発行される(10min)ので、アクセストークンを取得する
@app.route("/auth", methods=['GET'])
def auth_callback():  
  print(request)
  code = request.args.get('code')
  gid = request.args.get('state')

  # 認証エラー
  if(code is None):
    #print 'Auth error: '
    error = request.args.get('error')
    errorState = request.args.get('state')
    errorMessage = request.args.get('error_description')
    print(error)
    print(errorState)
    print(errorMessage)
    return 'Auth Error'

  # token取得
  token = line_login_get_access_token(code)
  profile = line_login_get_user_profiles(token)
  uid = profile.user_id
  name = profile.display_name

  db.add_user(uid, name)

  msgs = []
  msgs.append(TextSendMessage(text = u'{}さんがグループに入りました'.format(name)))

  line_bot_api.push_message(gid, msgs)

  # ログイン成功時の画面を用意
  return render_template("index.html", title="Akerun Login", message=u"ログイン成功", friend_url=line_friend_url, qr_url=line_qr_url)

Token取得処理は下記通りです。LINE LoginチャンネルのChannel IDとChannel Secretが必要です。

def line_login_get_access_token(code):
  headers = {'Content-Type': 'application/x-www-form-urlencoded'}
  payload = {
    'grant_type': 'authorization_code',
    'client_id': line_login_channel_id,
    'client_secret': line_login_channel_secret,
    'code': code,
    'redirect_uri': auth_url
    }

  obj_request = Request(
          "POST",
          'https://api.line.me/oauth2/v2.1/token',
          headers = headers,
          data = payload
  )

  obj_session = Session()
  obj_prepped = obj_session.prepare_request(obj_request)
  obj_response = obj_session.send(obj_prepped,
                  verify=True,
                  timeout=60
                  )
  print('status_code:' + str(obj_response.status_code))
  print('obj_response.text:' + obj_response.text)
  response_dict = json.loads(obj_response.text)
  print(response_dict)

  #{
  #  "access_token": "xxxx",
  #  "expires_in": 2592000,
  #  "id_token": "xxx",
  #  "refresh_token": "xxx",
  #  "scope": "profile",
  #  "token_type": "Bearer"
  #}

  return response_dict['access_token']

以下でログインユーザーの情報を取得できます。

def line_login_get_user_profiles(token):
  headers = {'Authorization': 'Bearer ' + token}

  obj_request = Request(
          "GET",
          'https://api.line.me/v2/profile',
          headers = headers,
  )
  obj_session = Session()
  obj_prepped = obj_session.prepare_request(obj_request)
  obj_response = obj_session.send(obj_prepped,
                  verify=True,
                  timeout=60
                  )
  print('status_code:' + str(obj_response.status_code))
  # {
  # 'userId': 'xxxx', 
  # 'displayName': 'xxxx', 
  # 'pictureUrl': 'https://profile.line-scdn.net/xxxx'
  # }
  print('obj_response.text:' + obj_response.text)
  response_dict = json.loads(obj_response.text)
  print(response_dict)

  return response_dict

LINEグループでも誰がBotと友達になったか分かります。

ログイン

施錠解錠する

Botと友達になったユーザーは事前に設定されたAkerunに対して施錠解錠操作ができます。DBに施錠解錠時のログを追加すれば、履歴を管理することも可能です。

lock unlock メニュー

施錠解錠処理についてはその1もご参照ください。

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
  reply_msgs = []
  push_msgs = []
  uid = get_id(event.source)

  #profile.display_name #-> 表示名
  #profile.user_id #-> ユーザーID
  #profile.image_url #-> 画像のURL
  #profile.status_message #-> ステータスメッセージ
  profile = line_bot_api.get_profile(event.source.user_id)
  name = profile.display_name
  if event.message.type == 'text':
    if event.message.text[0] == cmd_prefix:
      cmd = event.message.text
      if cmd == cmd_prefix + 'lock':
        job_id = akerun_bot_lock('akerun api token', '組織ID', 'AkerunID')
        reply_msgs.append(TextSendMessage(text=u'施錠'))
        push_msgs.append(TextSendMessage(text = u'{}さんが解錠しました'.format(name)))
      elif cmd == cmd_prefix + 'unlock':
        job_id = akerun_bot_unlock('akerun api token', '組織ID', 'AkerunID')
        reply_msgs.append(TextSendMessage(text=u'解錠'))
        push_msgs.append(TextSendMessage(text = u'{}さんが施錠しました'.format(name)))

  # todo:pooling -> push
  if len(reply_msgs) > 0:
    line_bot_api.reply_message(
      event.reply_token,
      reply_msgs)
  if len(push_msgs) > 0:
    line_bot_api.push_message(test_group_id, push_msgs)

施錠解錠時、LINEグループにもメッセージを通知できるので、誰が操作したが分かります。

施錠解錠

まとめ

  • LINEグループ機能を活用すれば、簡単に複数のユーザーに合鍵を共有し、管理できるので、楽ですね。
  • 追加対応すれば、複数のAkerun、合鍵の設定と管理も全てLINE上で完結できるので、更に便利になります。
  • その1で既にAkerunを施錠解錠するBotを作成したので、グループでの施錠解錠は割と簡単にできました。ただ、DB周りもしっかり作る場合、そこそこ手間がかかりそうです。
  • LINE Bot、グループの制限かもしれませんが、ユーザーは同じBotが所属している複数のグループを同時に使うことができないので、複数のBotを作るなど工夫する必要があります。

そのうちLIFFも試しみたいと思います。


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com

Hello legacy codes!

Intro

When being an engineer longer than one year, having experiences to handle code written by other people is a common issue. With any luck, when handover, well-documented specifications and senior members would help you get over any challenge in the situation.

I'm Tetsu, the Akerun Android team member. Today, let's talk about the unlucky situation and how I faced (and fixed!) it at Photosynth.

Overview

  • Why need to face legacy code?
  • How to handle it?
  • Finally fixed!

Why need to face legacy code?

"Hey, the code is so dirty, let's do refactoring!" or "No, it's impossible to refactor it, making a new one is easier." These kinds of conversations are common for engineers, but why do we need to tackle it? just for code quality?

My answer, the legacy code has provided values to users already-- and maybe from now on. But the legacy code brings challenges to engineers when we want to deliver more values quickly. So we need to consider how to face it. This answer sounds common? However we must tackle it anyway. ;p

(I also recommend a video talking about the importance of having "why" questions in our job. I hope this would worth to take your time) www.ted.com

How to handle it?

Now we confirmed the reason why we need to tackle a legacy code. The next step---we have to sort out the current situation. I would like to show an example at Akerun Android:

  • We have a project to improve the Akerun app's UI/UX.
  • Having not so much time to do code refactoring.
  • No architecture policy and full of smelly code.

Hmm... I want to show an ideal case first, and escape from the real world... Here is an architecture diagram in Akerun Android we want to have. f:id:photosynth-inc:20201222102745p:plain

This looks perfect!

Usually, we choose the domain layer (user story, business logic) or frequently feature (like account register/login) first if no exception in refactoring. But in our case, UI/UX improvement is the most important task we want to deliver to users, and refactoring the domain layer is not the better way with limited time.

Now, back to the real world, we consider a “plan B”, which does not aim to refactor all things at once, but focus on refactoring the UI/UX improvement tasks only.

Finally fixed!

Here is an overview of the “plan B” (which we have actually done): f:id:photosynth-inc:20201222102812p:plain We focused on the UI/UX tasks and refactored it to multiple feature-modules and a UI-component-module without changing almost all domain & data layers. It was unsophisticated yet and was not ideal. But it became better, huh?

Outline the actions:

  • Draw a blueprint that your team want to realize
  • Check the blueprint if it fits your objective
  • Keep enough buffer for unexpected incidents

Hope this article will provide you with any ideas when facing legacy code.

Thanks for your time!

By the way, we always welcome our new colleagues!~ hrmos.co

Please join us and let's innovate peoples’ lifestyle together.

P.S. For the limited time issue, another important part is to secure additional time to improve the team's overall working efficiency.

As examples, we did in the past year:

  • Use the CircleCI tool to run unit tests.
  • Add DeployGate Gradle task to distribute binary automatically.
  • Use Danger + Klint to improve code quality without human review.
  • Make a reusable UI component to keep UI/UX’s same behavior.

This makes our work quickly/easier and we can focus on more challenging tasks.

UICollectionView vs UITableView

この記事は Akerun Advent Calendar 2020 - Qiita の21日目の記事です。

Webエンジニアのbeginer_rider - Qiitaと申します。 主にiOSアプリ開発を担当としています。

はじめ

iOS開発者の皆さんにはよくある話と思いますが、リスト画面を作るときに、UICollectionViewとUITableViewのいずれを検討することがあったと思います。 リストの要素数、セルの色や表示名を動的に変更する中でどれがよいか、悩んだ方も多いのではないでしょうか。 そんな方々に少しでも助けになればと思い、UICollectionViewとUITabeViewのそれぞれのメリデメをご紹介させていただきます。 プチ読み物的なものとしてご一読ください。 ※あくまで個人的観点でのメリデメとなりますので、気になった方がいらっしゃればご指摘いただけますと幸いです。

UIColletionViewのメリデメ

メリット

UICollectionViewLayoutを利用することで、セルのレイアウトを変更することが可能

prepareメソッドをoverrideすることで、セルのレイアウトを変更することができます。 UICollectionViewLayoutのサブクラス、UICollectionViewFlowLayoutにはitemSizesectionInsetなど、セルのUIを変更するのに役立つ様々なプロパティを備えています。 加えて、セルだけでなくHeaderFooterのレイアウトを変更することができます。

また、他にもUICollectionViewFlowLayoutだけでなく、UICollectionViewLayoutAttributesUICollectionViewLayoutInvalidationContextなどのレイアウトを調整するためのクラスが用意されています。 そのため、柔軟にレイアウトを組むことができます。(1行ごとに表示する要素を変えるなど)

アニメーションのあるセルの挿入や削除を行うことができる

performBatchUpdateクロージャの中でreloaddeleteの処理を行うことで、一律でアニメーションのアップデートを行うことができます。 例えば、5つあるリストの1つ目を削除し、1つ目に新規要素を挿入するなら、deleteinsertの処理を行うことで、実現することができます。

デメリット

セルの挿入や削除の制御に手間がかかる

performBatchUpdateクロージャの中で、制御対象とするセルに対して、IndexPathを設定する必要があります。performBatchUpdateでは処理の順番に関わらず、削除→挿入の順に処理が実行されます。この時、削除や挿入する対象のセルがなければエラーが生じてしまうため、設定するIndexPathを正しく選択しておく必要があるので、削除や挿入前後のセルの順番を把握しておく必要があります。 画面を回転させた場合、セルの再生成を行う場合においてもIndexPathを正しく洗濯しておく必要があるため、あらゆる場合で考慮しておく必要がありそうです。

UITableViewのメリデメ

メリット

シンプルなリスト画面を作成しやすい

UICollectionViewとは違い、リスト画面のみを想定して用意されているため、簡単なリスト画面を作成する際には利用しやすいです。 リストに表示したい要素にSwitchButtonToggleBarなどを複数表示するためにそれぞれのセルを作成する必要はありますが、セルを表示する順番さえ決めておけば、あとはシステムがよしなに処理してくれるので楽です。

レイアウトの更新処理はlayoutIfNeededでなんとかなる

UITableViewのセルを更新完了の際にlayoutIfNeededを呼ぶだけで更新を完了させることができ、非常に楽です。ただ、他にreloadDatalayoutSubViewsなどのセルを更新するメソッドがありますが、こちらはUITableView全体を更新してしまい、処理の遅延に繋がることがあるので、利用するには向いてないです。

デメリット

セル数の更新操作が挿入と削除のみ

ネイティブライブラリから提供されているUITableViewのセルに対する操作は挿入と削除のみとなっています。 そのため、セルの移動に関してはUITableViewDelegateとUITableViewDataSourceで定義されている下記メソッドを実装する必要があり、手間がかかります。

・UITableViewDelegate

func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle

・UITableViewDataSource

func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)

最後に

いかがでしたでしょうか。検討する際の一助となれば幸いです。 UICollectionViewとUITableViewにはそれぞれの特徴があるので、うまく使い分けてみてください! 最後までご覧くださりありがとうございました。


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com

Webエンジニアがラズパイでセンサーデータ取得してBlinkでグラフ化するまで

この記事は Akerun Advent Calendar 2020 - Qiita の19日目の記事です。

Webエンジニアのt-ikeと申します。

入社2年目、Akerun Connectの開発などをメインで行っていました。

そのあたりの話をしようかなと思ったのですが、せっかくIoTの会社にいるのだから、それっぽいことに挑戦してみよう! ということで、比較的簡単にできそうなことを考えてみました。

はじまり

唐突ですが、最近鼻詰まりがひどいんです。

まぁ息するのができないというほどではないんですが、 唯一の趣味である飲酒において、香りを楽しみきれないというのは許せんということで、悶々とする日々をすごしていました。

原因はまぁちゃんと掃除してないとかだと思うんですが(←)、判断基準になるものがあったらいいなぁと思い、調べてみることに

調査

市販のものでも、空気質を計るようなスマートホームバイスもあったのですが、

  • リアルタイムな値しかみれず、過去の情報が参照できなさそう
  • 空気質の変化の検知を柔軟にできなさそう
  • せっかくだしもうちょいIoTっぽいことしようよ

などの理由で、ラズパイ&センサーでいけないか調査。

そもそも安価で、ハウスダストを計測できるものなんてあるんか、と調べると普通にありました。

www.seeedstudio.com

どうやらラズパイで使えるらしいということで、購入決定 (恥ずかしながら人生初ラズパイ

しかしながら、この値段でメモリ8GB、4Kも対応とかすごい時代になりましたね...

データの可視化

要件として、過去のデータも参照できて、それをグラフ化したいみたいなのがあって、 知ってる範囲でやるならcloudwatchとかに雑になげこめばいいかなぁと思ったんですが、 取得したデータをスマホで簡単に見れたらいいなぁと思い、調べてみるとよさそうなものがありました。

blynk.io

簡単にまとめると

  • インターネット経由でIoTデバイスを操作できるスマホアプリ
  • Virtual Pinという機能を使うことでGPIOの入力操作や出力データ取得が可能
  • 5つまでデバイス登録無料
  • Virtual Pinで取得したデータはクラウド上で保持
  • グラフ表示も可能

ということで、ちょっと触ってみたいユーザには最適なものがあり感激

センサーデータを送る

ラズパイ動かすとか、センサー取り付けるとか、初心者な障害は多々あったけど、そこらへんは省いて、blynkにセンサーデータを遅れるようにしてみる 上記のダストセンサーのリンクでも紹介されていますが、濃度取得のソースコードをもとに実装してみる。

自分はインタプリタ言語しかまともに触れない悲しいエンジニアなので、pythonでかきました

(抜粋してるのでこのままではうごきません)

import os
import RPi.GPIO as GPIO
import time
import blynklib
from blynktimer import Timer
from dotenv import load_dotenv
import threading

load_dotenv()
BLYNK_AUTH_TOKEN = os.environ['BLYNK_AUTH_TOKEN']
# ダストセンサーの出力PIN
PIN = int(os.environ['PIN'])
VPIN_RATIO = 0
VPIN_CONCENT = 1
VPIN_UGM3 = 2

blynk = blynklib.Blynk(BLYNK_AUTH_TOKEN)
timer = Timer()

GPIO.setmode(GPIO.BCM)
GPIO.setup(PIN, GPIO.IN)
GPIO.setwarnings(False)


@timer.register(vpin_num=0, interval=INTERVAL, run_once=False)
def get_sensor_data(vpin_num):
    th = threading.Thread(target=measure, args=(PIN,))
    th.start()


def measure(PIN):
    t0 = time.time()
    t = 0
    while True:
        # パルス幅計測
        duration = pulseIn(PIN, 0)
        # 計測時間を超えたら濃度などを計算して送信
        if ((time.time() - t0) > MEASURING_TIME):
            ratio = t/MEASURING_TIME * 100
            concentration = 1.1 * pow(ratio,3) - 3.8 * pow(ratio,2) + 520 * ratio + 0.62
            blynk.virtual_write(VPIN_RATIO, ratio)
            blynk.virtual_write(VPIN_CONCENT, concentration)
            blynk.virtual_write(VPIN_UGM3, calc_ugm(concentration))
            break


if __name__ == "__main__":
    while True:
        blynk.run()
        timer.run()

blynk.virtual_writeをつかえば、指定した番号のvpinにデータが保存されていくみたいです。

Blynkを使う

ここまでくればあとはアプリでぽちぽちすれば終わり

説明がめんd...長くなるので、結果だけのせると、こんな感じでグラフ表示が簡単にできました。

f:id:photosynth-inc:20201217213452j:plain

でも本当にUIよいので、困るところそんなに無いと思います。

考察

センサーは別途の横においてるんですが、だいたい朝起きたときから1時間くらいかけて高くなっているようにみえる

以上

結果あんまりわからずw 空気清浄機かけてみるのとみないので変わるかとか試してみたけど、あまり変化せず。

とりあえず布団干したり、シーツ洗ったりする頻度を上げるようにしようと思いました。

感想

身の回りに関するデータを時系列で可視化するっていうのはあまりやったことがなかったので、やってみると結構楽しくできてよかったです。

他にも温度とか湿度とかとって、ログとして残しておくと、特に意味はないけどニヤニヤできそうな気もするのでやってみたいですね。

あとは市販のものでもできるけど、センサーデータトリガーで家電操作したりとかもいいですね

何よりこういったことに興味もって動くことができたのはこの会社に入っていろんなエンジニアの方々と触れ合えたおかげだなぁとしみじみ思います。

ということで

株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 https://recruit.jobcan.jp/photosynth/listrecruit.jobcan.jp

これぞ、完全タッチレスエントリー

17日目の記事です。

WebエンジニアのBunです。主にiOSアプリの開発を担当しています。

ドアに貼り付けるだけでスマホICカード社員証、遠隔から施錠解錠可能な弊社のスマートロックAkerunについて、もっと楽な施錠解錠方法がないか日々考えています。

新型コロナウイルスが広がっている中、ドアすら触れたくないですね。そこで、Apple Watchと弊社の「タッチレスエントリー・ソリューション」を組み合わせて、完全タッチレスでより快適な解錠を実現してみたいと思います。

akerun.com

akerun.hateblo.jp

13日目の記事(LINE Botから施錠解錠する)も書いたので、こちらも読んでみてください。

akerun.hateblo.jp

大分前にApple Watch Series 2を購入しましたが、その時はまだWatchOSのバージョンが2か3になっていて、色々機能制限があったので、結局何も作らずにただの腕時計として使ってきました。 その後、だんだん制限が解除され、特にWatchOS 6から独立したアプリも作成できるようになったので、Apple Watchだけで解錠できるアプリを作ってみました。

※最新のSeries 6を購入しようと思いましたが、今持っているSeires 2でもWatchOS 6までアップデートできたので、新たに購入せず済みました。ラッキ~

基本的にWatchOSのCMDeviceMotionから取得した姿勢データとBLEを使って実現可能なので、試してみたら、予想より簡単にできてしまいました。

WatchOSアプリのプロジェクト作成

プロジェクト新規作成から以下の「Watch App」を選択します。

適宜内容を記載し、プロジェクトを作成します。

以下のフォルダ構成フォルダ構成になります。

今回は特にUIも必要ないので、主にExtensionの方を実装していきます。 ただ、最初はMotionセンサーとBLE処理をデバッグしながら作るので、WatchKit AppのUIにテスト用ボタンなど追加してデバッグを行います。 また、毎回ビルドしてApple Watchにバイナリを転送すると結構時間がかかるので、Akerun本体とのBLE通信を確認するためのiPhoneアプリ(BLETester)も作ります。

Akerun本体とのBLE疎通確認

AkerunはPeripheralとして動作するので、Apple WatchをCentralとして動作させます。 iOS同様WatchOSからも直接BLE通信ができるので、CBCentralManagerを使って、Scan、Connect、Write(解錠コマンド書き込み)処理を実装します。 解錠用BLEコマンドは独自の暗号化処理になっているので、詳細は割愛します。

Motionデータ取得

WatchOSのCoreMotionからattitude(姿勢)、rotationRate(角速度)、gravity(重力加速度)、userAcceleration(加速度)が取れますが、今回はシンプルにattitude(姿勢)を使います。

Attitudeはpitch(ビッチ)、roll(ロール)、yaw(ヨー)の姿勢角があります。 手の動きについて何パターンか考えましたが、ドアノブを回す動きが自然かなと思ったので、pitchの変化タイミングを検知し、適切な閾値を設定して、解錠判定を行います。 ちなみにpitchは以下の定義になります。

  • 時計の3時方向を右、9時方向を左にし、左右X軸に関する回転角
  • 値の範囲は -π/2 から π/2 まで
  • 腕を上げて、画面がActiveになる時は0

具体的に、腕を上げてpitchの値が0前後(+/-0.1)になったら、判定をスタートし、腕を一瞬反時計回りに回した時、pitchの値が-π/3前後で解錠コマンドを送信するようにしています。

Apple Watchは、腕を下げると画面がDeactiveになるので、センサー情報の取得とBLE通信ができなくなります。バックグランドで実行するタスクを作れば、一定時間動作しますが、今回はDeactiveにならないように腕を反時計回りに回した後すぐ戻すことで回避しました。

画面がActiveになった場合、以下でMotionデータ取得開始します。パフォーマンスも考慮して、データ取得Intervalは1/30以下で十分だと思います。

private var motionMan = CMMotionManager()

func start() {
    if self.motionMan.isDeviceMotionAvailable {
        self.motionMan.deviceMotionUpdateInterval = 1.0 / 30.0
        self.motionMan.startDeviceMotionUpdates(to: OperationQueue.current!) { (motion, error) in
            if let m = motion {
                let data = MotionData(
                    attiPitch: m.attitude.pitch,
                    attiRoll: m.attitude.roll,
                    attiYaw: m.attitude.yaw
                )
                self.delegate.motionMamanger(self, data)
            }
        }
    }
}

func stop() {
    self.motionMan.stopDeviceMotionUpdates()
}

Motionデータが定期的に通知されるので、pitchがπ / 20以内になったら解錠判定をスタートし、その後のpitchが反時計回りにπ / 3以上回ったら、Akerunに接続し、解錠コマンドを送信します。

extension InterfaceController: MotionManagerDelegate {
    func motionMamanger(_ motionManager: MotionManager, _ data: MotionData) {
        if fabs(data.attiPitch) < Double.pi / 20 {
            self.isActive = true
        }
        
        if self.isActive {
            if data.attiPitch < -Double.pi / 3 {
                self.isActive = false
                self.akerunMan.connect(to: testUuid)
            }
        }
    }
}

動作

ちょうど出社する日があったので、Officeのドアで試してみました。ドアには電気錠/電磁錠に対応したAkerunコントローラーが設置され、半年前にタッチレスエントリーも導入しました。

普段は社員カードをかざして入退室していますが、下記のようにほぼ完全タッチレスが実現できました。

f:id:photosynth-inc:20201216111241g:plain

※動画撮るために腕を前に出していますが、手首の部分を少し上げてから回しても問題なく動作します。

まとめ

久しぶりにWatchOSを触りましたが、やっぱりIoT製品との相性も良く面白いですね。WatchOSのMotionセンサー、BLEなど基本的な機能だけで簡単にアイデアを具体化できるので、今後も色々試して見たくなります。

ただし、iOSと違ってまだまだ色々制限があるので、工夫しないといけないところもあります。

  • バックグランド処理

画面がDeactiveになって、バックグラウンドに入った時は処理が実行されません。 今回は、腕を早く回すことで何とか回避できましたが、下記の拡張セッションを使えば、完全ではないですが、バックグラウンドでの処理が実行できそうです。

f:id:photosynth-inc:20201215152347p:plain https://developer.apple.com/documentation/watchkit/using_extended_runtime_sessions

WatchOS 8以降でバックグラウンドでも簡単に処理が実行できるようになるかもしれませんが、早めにこういう制限を解除して欲しいですね。

  • NW通信

WiFiモデルの場合、結局iPhone経由でNetwork通信を行うので、独立したWatchアプリではなく、「iOS App with Watch App」でも十分かもしれません。Cellularモデルであればもっと快適になるかと思います。

取り敢えず動作するかどうか試してみただけなので、精度についてはそこまで考えていません。 以下のモーション動作を分類するActivity Classificationを使ってモデル構築し、機械学習すれば、色んな解錠ジェスチャーが実現できて、精度がもっと上がると思います。誤動作も少なくなるはずです。

f:id:photosynth-inc:20201215152435p:plain https://developer.apple.com/videos/play/wwdc2019/426/

また別の機会で試してみたいと思います。


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com