2017年09月21日

40年前の「子供の科学」誌との再会を通じて

はてなブックマークに登録

子供の科学」は株式会社誠文堂新光社様の発行する小中学生向けの月刊科学雑誌です。創刊は関東大震災の翌年 1924年(大正13年)とふるく、世代をまたいで読み継がれています。この記事をご覧になっている方の中にもかつての読者(現役の読者も?)が少なくないことでしょう。

手元では 1977年前後にこの「子供の科学」(以下 "同誌")を購読していました。それからずっと長い間忘れていたのですが、先日実家に残っていた当時の何冊かを見つけました。
懐かしい気持ちで久しぶりに 40年前の誌面を読み返したところ、子供と同じ目の高さで科学や技術、未来に向き合っていた当時の大人たちの懐の深さと真剣さを現在の年齢なりに強く感じました。商業誌である以上、誌面には小さな読者たちからの要望のいくばくかも反映されていたであろうことを考え合わせるとさらに興味ぶかく感じられます。

この機会に印象に残ったいくつかの記事を電子化しておくことにしました。その内容を「子供の科学」編集部様のご承諾のもとに紹介します。あわせて、40年を経た 2017年現在の誌面と 93年前の創刊号にも目を向けてみたいと思います。


1977年 第12号より

表紙

遺物の写真のまわりに 4つの記事のタイトルのみが淡々と並ぶシンプルな装丁。「マリモはどうして丸くなる?」という取り立てて派手さのないタイトルが堂々と表紙に据えられていることも印象的。

目次

同誌の内容は多岐に渡る。「原子力製鉄」という記事と「実用工作 ちりとり」という記事が何の違和感もなく共存していることに守備範囲の広さが感じられる。

「針のいらないレコードができた」

         :
もし, プレイヤーにレコードをのせ, 回転させるだけで, 針の付いたピックアップがないのに, 実際に演奏を聞くのと少しも変わらない, すばらしい音質(これを超ハイ・ファイと言います)で再生できたら, おそらくみなさんはびっくりさせられることでしょう.
         :
このレーザー光線による微小ビットの記録再生技術は, さらにいろいろな応用が考えられています.
 テレビに接続すると映像の出るレコードとか, 超長時間録音とか, 例えば100曲ほど入ったジュークボックスが, たった1枚のディスクでできるとか, あるいは楽器別に同時記録ができるマルチ・チャネル方式への応用など、研究者達の夢は限りなく広がっています.

PCM レーザサウンドディスク」は、コンパクトディスク(CD)が製品化されるよりも前に三菱電機ティアック東京電化の三社によって開発が進められていたディジタルオーディオシステム。音楽愛好者の重要な音源だった FM 放送の周辺で「PCM 録音」のキーワードが話題になり始めていた時分であり現在が過去の未来であることをあらためて感じさせられる。時代を経てアナログオーディオの価値が再認識された現代ではむしろ「針を使うレコード」のほうが新鮮で贅沢であることもまた興味深い。

関連記事

「自由に曲線が切れるカッター」

ナイフなどで紙や布を曲線に切る事は, たいへんむずかしい事です. この道具は, ゴムのローラーを利用して自由に曲線が切れるようにしたものです.
         :
ローラーが付いているので, 紙や布の上を自由に動かす事ができ, ジグザグにでも, 曲線状にでも切れるというわけです.
         :

世界初のロータリーカッターである オルファ株式会社の「マルカッター」の解説。短くさりげない記事だが曲線の切れるカッターのしくみが丁寧にわかりやすく説明されている。 この記事を書いた青木国夫氏(故人)は当時国立科学博物館工学研究部長の要職にあった著名な研究者。

関連記事

「自動焦点カメラのしくみ」

ひと昔前にはまったくの夢だった自動焦点カメラの出現です.
         :
月の探索用に積み込まれた無人操作カメラや, 8ミリシネマカメラには前から使われていましたが, この種類の普及カメラに組み込まれたのはこれが最初で, 文字どおり「シャッターを押すだけ」のカメラがここで実現したわけです.
         :
その仕組みは, これまでの距離計とレンズの焦点調節用の伸縮を自動連動させ, 二つの距離計映像の合致を2個の電子の目が見くらべて, 同じになるとそこでストップ, 実際には, シャッターを押すだけでピントも露出もピッタリ, その間は一瞬の手早さで, 撮影完了ということになります.
その仕組みを, もう少しくわしく説明しますと, ざっとこんなところです(図1)
         :
この方式は, 短焦点レンズ用(もともとピント幅が広い)には好適ですが,(中略)一眼レフなどの高級機用としては, 今後の研究をまたねばなりません.

コニカ株式会社(現コニカミノルタ株式会社)の C35AF は 1977年に発売された世界初の普及型オートフォーカスカメラ。この記事は発売されたばかりの同製品に用いられている当時最新の AF 技術を子供に理解することの可能な表現と内容で正面から解説している。ちなみに、記事を執筆した故・松田二三男氏は同誌で人気のあった読者投稿写真コーナー 目次 の選者を長らく務めた。

関連記事

「触媒について調べよう」

触媒とは, そのもの自体, 反応の始めと終わりで少しも変化しないで, ほかの物質の反応の速さを変えるものをいうのです.
         :
塩素酸カリウムという薬は, 熱すると酸素を発生するのですが, ガスバーナーで熱するぐらいではなかなか酸素はでてきません.    :
あらかじめ, 二酸化マンガン(触媒)を少し加えておくと, 200℃くらいの温度で完全に分解します.
         :
過酸化水素水(市販品のオキシフルとかオキシドールという消毒薬)を使って触媒の研究をしてみましょう.
         :

特にこれといった脈絡もなく「触媒」を扱う化学実験の話題が急にさらりと出てくるのがまた面白いところ。前述のように同誌は広く自然科学分野全般をカバーしており(時には社会科学方面の話題も)、毎回こうした単発の記事が何本も掲載されていた。当時市立中学校の理科教諭だった執筆者の岩崎幸敏氏は 70年代から 90年代にかけて中学生向けの多くの著書を上梓している。

「ぼくの発明 きみのくふう」(読者投稿コーナー)

● カッターナイフの改良 ●

今までカッターの刃を折る時, 手がしびれることがあったので, つめ切りと同じしくみのものをとりつけ, 手がしびれることなく簡単に刃が折れるようにしました. (B)のねじを回しカッターの刃に, (1)を近づけ, あとはつめを切るのと同じ方法で折る. (川上○○ / 北海道沙流郡)


この案は不安解消策として有効です. ただし, カッターにこのようなしくみを付けると, 価格が高くなったり, 使用しにくくなるなどの問題点も出てきそうです. 小学生にしてはいい着眼です.

ネットも PC もなく一個人が自由に情報発信を行う手段がほぼ皆無だった時代には新聞・雑誌等の投稿欄の存在感が現代よりもずっと大きく、同誌のこのコーナーにも人気があった。

子供が真剣に何かを考え工夫をこらして自分なりの結論を出し、それを第三者が理解できるように説明する努力を経て意見を求めるという一連の構図の素晴らしさをあらためて考えさせられる。 さらに、子供から寄せられたアイディアに決して安易に迎合することなく、むしろ大人である自分の視点での率直な意見を時に助言を添えつつストレートに伝えようとする選者の一貫した姿勢がそのことをさらに際立たせている。この号に掲載された応募作品は以下のとおり。彼らは今どんな大人になっているのだろうか。

  • コンパスの改良
  • つめが飛ばないつめ切り器
  • ボックス・ドライバーの改良
  • カッターナイフの改良
  • 豆英単語練習機
  • 自転車用高圧発生機
  • 小鳥のさえずりまくら
  • 黒板用三角定規
  • 音振動力紙ずもう

余談:エポック社「システム10」の広告ページ

この号には発売から間もないエポック社製の家庭用ビデオゲーム機「システム10」の広告が掲載されている。スペースインベーダーが大ヒットしたのが翌 1978年。ちなみに「機動戦士ガンダム」の初放映は翌々年の 1979年。のち 1983年のファミコンの登場によりビデオゲーム機市場の様相は一変するが、そこに至るにはまだしばらくの時間を要した。

  • 当時この「システム10」がとても欲しかったのですが自分でまかなうことはもちろん子供心にも親にねだれるような値段ではなく、ゼロがひとつ少なければ、、と何度も思っていました (> <)

2017年 6月号より

1977年当時の誌面との印象的な再会からほどなくして、本年 2017年の6月号を購入しました。創刊からすでに 90余年、現在は紙媒体とは別に Kindle 電子版も発行されていることを知って驚きました。

40年ぶりに買った同誌はこの時代相応に表現や文体がマイルドになってはいるものの、ガンとして変わらないロゴマークと同様に、誌面から伝わってくるテイストがあの頃のそれとあまり変わっていないことに安心しました。支障のない範囲で一部を抜粋してみます。

表紙と目次

  

記事より

  • 「水中の食虫植物 タヌキモ」
    定番の水棲動植物の特集記事。前掲の 1977年12号では「マリモ」でしたね :−)
  • 「ジブン専用パソコン 第3回ラズビアン(OS)を設定しよう!」
    食虫植物の解説とラズパイの連載記事がやっぱり普通に共存
  • 「ぼくの発明 きみの工夫」
    ボリュームは減ったもののこのコーナーが今も健在であることが嬉しい

創刊号(1924年 10月 第1巻 第1號)より

この記事の冒頭にもリンクを掲載した「子供の科学」公式サイトの次のページから 1924年(大正13年)の創刊号を閲覧することができます。

非常に貴重な誌面がこのような形で公開されていることはとても興味深く読み入ってしまいます。とりわけ、最初のページに掲載されている「この雑誌の役目」という文章に感銘を受けました。93年前に書かれたその全文を以下に引用します。

巻頭の辞「この雑誌の役目」


www.kodomonokagaku.com

この雑誌の役目  主幹

 愛らしき少年少女諸君!!!子供科学画報は、皆さんのために、次のような役目をもって生まれました。
 およそ天地の間は、びっくりするような不思議なことや、面白いことで、満ちているのでありますが、これを知っているのは学者だけで、その学者のかたは、研究がいそがしいものですから、皆さんにお知らせするひまがありません。したがって、多くのかたは、それを知らずに居ります。そのなかで特に少年少女諸君の喜びそうなことを学者のかたにうかがって、のせて行くのも、この雑誌の役目の一つです。

 皆さんが学校で学んでいる理科を、一そうわかりやすく、面白くするために、その月々に教わることがらについての写真や絵を皆さんのためにそろえるのも、この雑誌の一つの役目でもあります。理科の本にかいてある事がらに限りません。読本のなかにある理科の事がらに関するものも、のせておきます。

 毎日のように見たり使ったりしているもの事について、皆さんは、くわしく知りたいと思われることがありましょう。皆さんの御望みを満たすため、絵を入れてできるだけわかりやすく、そういうもの事を説明するのも、また、一つの役目であります。
 簡単な器械の造りかたをお伝えして皆さんの発明の才をあらわし、面白い理科の遊びのできるようにするのも、役目の一つであります。

 しかし、この雑誌の一ばん大切な目的は、ほんとうの科学というものが、どういうものであるかを、皆さんに知っていただくことであります。ちかごろは、「科学科学」とやかましくいいますが、ほんとうに科学というものを知っている人は、沢山ないようです。人は生まれながら、美しいものを好む心を持っておりますが、それと同じように、自然のもの事についてくわしく知り、深くきわめようとする欲があります。昔から、その欲の強い人々がしらべた結果、自然のもの事のあいだには、沢山の定まった規則のあることがわかりました。科学ということは、この規則を明らかにすることであります。多くの人が科学といっているのは、大ていは、その応用に過ぎません。この規則を知ることによって、人間は、自然にしたがって、無理のないように生き、楽しく暮らすことができ、これを応用して世が文明におもむくのです。
※書き起こしに際し、旧かな遣い・旧漢字・旧かな送りをあらためています

個人的な雑感

久しぶりに「子供の科学」誌に接し、私自身がもっとも強く印象に残ったのは、それぞれの分野の専門家の大人たちがそれぞれに子供たちに本気で向き合い、本気で何事かを伝えようとしている姿勢です。過剰に機嫌をとりながら話を聞かせようとするのではなく、興味をもった子供たちへ度合いに応じた「努力」の余地を残しながら適度な大きさの粒度まで知識や情報を砕いた上でそれを示し、あとは読み手側の好奇心と探求心にゆだねつつ同時にそれらを育んでいこうとする共通の意思のようなものを感じました。

難しいことをわかりやすく説明できることが最良とよく言われますが、そこに相手を受け身にすることなく相手の向上心を呼び誘うための配慮を加える余裕があればさらに素晴らしいことでしょう。そして、そういった配慮こそがその道に精通していなければなかなか果たすことの難しい命題ではないかと思います。同誌の記事の執筆者はすべて第一線の専門家であり、「子供だまし」ではなく真剣に世代のバトンを引き渡すためにはまさに適役でしょう。この誌面に限らずこういったあまり表には出てこない場所にもまたこれまで静かにこの国を支えてきた多くの大切な要素が脈々と息づいているのかもしれません。

どの世代にもそれぞれの役割があります。現役の大人の世代は年齢とともにいずれ順番に次の世代と交代していくことになりますが、科学や技術、学問の分野の話題に限らず、自分を含め今の大人たちが新しい世代に対する役割を適切に果たせているのか? 彼ら彼女らからの問いかけや疑問に対して都合よく言いくるめたりはぐらかしたりせずまっすぐに答えることができているのか? そういったことは大人から子供への一方的なプレゼントではなく、やがて次の世代との交代を終えた未来の自分の生きる世の中を支える礎にもなるものでしょう。つまり、今の子供たちに真剣に向き合うことはこれからの時代へ向けてのメッセージであると同時にこれからの自分自身に対する応援でもあるはずです。誰もが先人たちから受け継いだこの大きなループの中で生きています。その片隅で自分が少年時代に読んだこの雑誌をすっかり大人になった今の年齢でふたたび読み返しふとそんなことを考えました。


(tanabe)
klab_gijutsu2 at 12:45
この記事のURLComments(0)その他 
2017年09月01日

最近のPython-dev(2017-08)

はてなブックマークに登録

バックナンバー:

https://docs.python.org/ja/3/

docs.python.org に言語スイッチのドロップダウンリストが追加されました。docs.python.org は Fastly を使っているので、 docs.python.jp よりも高速に閲覧できると思います。

docs.python.jp にあるセクション単位での英語ドキュメントへのリンク機能などがまだなくて単純な翻訳でしか無いので、すぐには docs.python.jp を止めるつもりはありませんが、将来的には docs.python.jp は docs.python.org/ja/ にリダイレクトすることを考えています。

PEP 550: Execution Context

Flask などのフレームワークではスレッドローカルストレージを利用して「コンテキスト」を作り、現在のユーザーの情報とかリクエストIDとかをそこに格納する事があります。しかし asyncio などのコルーチンではこれが使えません。

そこで、もっと高度に抽象化された、コンテキスト変数を扱うための仕組みが提案されています。

Hide implementation details in the C API

サードパーティーの拡張モジュールが利用している Python/C API が、 Stable ABI 以外は公開/非公開、マクロ/C関数が混ざり合っていて、 Cython なんかは非公開APIも積極的に使って性能を稼いだりしています。

現状のデメリットとして、 Stable ABI は使いにくいので余り使われていないのに対して、 API は雑然としていて他の Python 実装が Python/C API 互換の API を提供するハードルが高くなってしまっていたり、意図せずに実装詳細に依存したサードパーティーのライブラリが増えて将来の性能向上のための内部の大幅な書き換えをする場合に互換性の問題が起こる確率が増えてしまっています。

そこで、 Stable ABI と #include <Python.h> の間を埋める、整理された Python/C API を用意しようという提案がされています。

個人的には、これによって今までマクロだったAPIがC言語の関数になり、RustやGoなどC言語以外の言語から Python/C API を呼び出しやすくなることに期待しています。

PEP 539: A new C API for Thread-Local Storage in CPython

Python の TLS のための C API が pthread_key_t を(90年代から) int 型にキャストして使っているのですが、これは posix 準拠ではありません。メジャーな環境では問題になっていないのですが、 Cygwin や CloudABI で問題になっていたそうです。

しかし、ABIレベルの後方互換性の問題があり、簡単に変えることもできません。ということで新しいAPIが提案されていたのですが、最近 Yamamoto Masayuki (ma8ma)氏がこの PEP に最近取り組んでいてもうそろそろ解決しそうです。

Cレベルの話で Python からは見えないのですが、日本人の活躍情報でした。

songofacandy at 17:27
この記事のURLComments(0)Python 
2017年08月31日

micro:bit と ESP32 でインターネットボタンを作る

はてなブックマークに登録

今月(2017年8月)5日に国内で正式販売の始まった BBC micro:bit を楽しんでいます。遅ればせながら、世界中の子どもと大人に人気のこの素敵なコンピュータのファンになりました。


http://www.bbc.com/
http://www.kitronik.co.uk/

私が micro:bit に関心を持ったのは最近扱っていた話題にちょうどぴったりのデバイスだったことがきっかけでした。今回はその内容と手元での試作を順をおって紹介します。

BLE ボタンデバイスをインターネットボタンとして使うこと

Amazon Dash Button の成功がひとつの契機にもなり この数年の間にインターネットボタンの利用と普及が進みました。手軽さといえば何かとスマホが引き合いに出されることの多い状況あってより単純な操作でより簡単に所定の処理を呼び出せることが専用の機器であることの最大の強みでしょう。

ただその一方で、機器の性質上ボタンデバイスの多くが電池駆動式であることが今後の用途・要件の拡大に伴いより悩ましい要素となる可能性も考えられます。限られた貴重な電力を使って自力でインターネット上の所定のサービスを叩きに行くのは必ずしも効率の良いやり方でありませんから、この点への対処が今後の課題となるかもしれません。

初代の Amazon Dash Button に高容量ではあるものの傍目にもコストバランスの危なげな米エナジャイザー社製の高価なリチウム一次電池が内蔵されていたことは象徴的です

一案として、省電性に優れた BLE ボタンデバイスと仲介役のエージェントデバイスを組み合わせてインターネットボタンを構成する形を想定しました。図にしてみます。

このように考え方はとてもシンプルです。ボタンデバイスは電池駆動、据え置きのエージェントデバイスは AC 電源からの給電を想定しています。 ボタン側は BLE 通信で必要最小限の通知をエージェントへ送り、エージェント側はそれをトリガーに所定のインターネットサービスを呼び出します。インターネットボタンは通常屋内空間での利用が前提ですから、BLE と WiFi の通信距離の違いに留意して機器の設置・運用を行えばこの構成には相応の合理性があります。また、小さなボタンデバイスひとつに複雑な処理を入れ込むことに比べ、機器の組み合わせによって必要な機能を実現するやり方には柔軟性が見込まれるでしょう。

通知の方法と道具立て

通知方法について

通常、BLE ペリフェラルからセントラルへの通知はデバイス間の接続確立後に所定の手続きによりセットアップを行うことで実施可能となります。この設定は両者の接続が維持されている間のみ有効です。

もちろんそれは BLE の仕様上のまっとうな規約なのですが、前掲の案への適用にはあまり気が進まずにいました。理由は以下の三点です。

  1. 「接続の維持」が必須であることは BLE の省電性を最大限に活かそうとする主旨と必ずしも折り合わない
  2. エージェントへの通知は必要最小限の合図のみでよいにもかかわらず仕掛けが豪華すぎる
  3. 技術情報や SDK の公開されていないボタンデバイスを利用したい場合にはグレーな解析が必要

代わりに デバイスからのアドバタイジングパケットを合図として利用することを考えました。この方法ならよりコストが低くデバイス間の接続に拘束されることもありません。また、アドバタイズの内容は文字通りオープンなので一般的なツールで普通に読めるという間口の広さも利点です。
注意の必要な点としては、連続して発生するアドバタイジングパケットにエージェント側が過剰に反応しないようにすることと、当該デバイスを本件専用として扱うべきであることが挙げられるでしょう。

エージェントについて

前掲の案のポイントは BLE 通信とインターネット通信の両方をカバーするエージェントを用意する点にあります。BLE つきの遊んでいるスマホを利用する手もありますが、「据え置き」「常時稼働」を前提とする IoT 装置として利用するにはいささかオーバースペックの感があり、また、視点は異なるもののスマホ系とボタンの連携に関しては「Flic」や「Pochiru」のような先例もあるためもうひとつ食指が動きません。

そこで ESP32 モジュールを使うことにしました。ESP-WROOM-32 は今回の要件を単体ですべて満たしているためまさに適役で価格面での魅力もあります。

ボタンについて

過去にこのブログでピックアップしたもの以外にも手元には複数の BLE ボタンデバイスがあります。それぞれに個性があり並べて使ってみるとなかなか面白いのですが、上述のようにアドバタイジングパケットを利用することを想定すると次のような機能・機構がほしいと考えました。

  1. 給電中にアドバタイズの開始・停止を任意に制御できること
  2. 複数のボタンが装備されていること
    (「ひとつのデバイスでひとつの処理」ではちょっと寂しい。アドバタイズを合図とする以上、ボタンデバイスが本来の通知機能においてダブルプッシュや長押しといったボタンアクションの区別に対応していてもそれを利用することができないという事情もある)
  3. 上記 2. に関連して、アドバタイズデータの内容を変更可能であること

残念ながらこれらの要件を満たすものは手元にありませんでしたが、プロトタイピングであれこれ試したところではどのボタンもそれなりに使えるのでまあこれはこれで・・とも思っていました。そこでたまたま目にしたのが micro:bit 国内販売のニュースです。

それまでこの製品のことは名前しか知らずにいました。あらためて写真をみてふたつのタクトスイッチが目にとまり、さらに記事中の「技適」の文字が気になって情報を調べたところ実は上のみっつの要件をすべて満たしていることがわかったため迷わずこれを利用することにしました。
ちなみにスイッチサイエンスさんが去年 12月にリリースした chibi:bit のこともこの時に知りました。ショップにはよくお世話になっているのですが、一見あまり関係のなさそうなところにしっかり良いものがあったりするものですね。

micro:bit 用に追加購入したパーツ

micro:bit ボードをボタンデバイスとして扱うために欠かせないふたつのパーツを調達しました。 いずれも英 Kitronik 社の製品で、ボタン電池から給電するためのオプションボード「MI:power(直販価格 £4.16)と、同ボードを装着した状態の全体をすっぽり覆うことのできる「MI:pro Protective Case(直販価格 £4.10)です。写真はケースに収めた状態の外観です。


前面
背面
また、今回は使っていませんが、拡張コネクタ用の「Edge Connector Breakout Board(直販価格 £4.15)もあわせて入手しました。

余談ながら Kitronik 社は直販を行ってはいるものの日本への送料が最安でも £30.95 と高いため、送料の安いショップを探して英国内の Pimoroni にたどり着き、そこで「MI:power」と「Edge Connector Breakout Board」を購入しました。送料は £5.50。8月7日に発注し 1週間で到着しました。なお、このふたつの製品はスイッチサイエンスさんのサイトにも掲載されており 8月21日に同社から「ご注文いただけるようになりました」メールが届きました。

どうしても欲しかった「MI:pro Protective Case」は Pimoroni にも見当たらず、しつこい検索を経て結局その時点で送料を含め最も安く販売していた ebay 出品企業から買いました。それでも総額が日本国内での micro:bit 本体価格よりも高額ではありましたが、ちょうど 2週間後の 8月25日に届き嬉々として使っています。

スイッチサイエンスさん、ぜひこの「MI:pro Protective Case」も扱って下さい!Pimoroni さんも今のところ扱っていないようですが、もし入荷されたら少なくとも私は確実に買います!(^^;

実装

micro:bit 側

micro:bit 側のプログラムは Microsoft MakeCode で手早く作成しました。以下の内容としています。

  • 電源が入ると最上段中央の LED を 2秒間隔で点滅開始 (給電状態の確認用)
  • 左側のボタン A が押されるとボタン右の LED を点灯し Eddystone-URL パケットにダミー URL "http://A" をのせて 4秒間アドバタイズする
  • 右側のボタン B が押されるとボタン左の LED を点灯し Eddystone-URL パケットにダミー URL "http://B" をのせて 4秒間アドバタイズする

アドバタイズに Eddystone-URL パケットを使用しているのは MakeCode 上の「Bluetooth」の項に他の選択肢が見当たらなかったためです。これと言って弊害も想定されず、また、どのような形式であれデータの内容で A, B ボタンを区別できれば事足りるのでこれはこれでよしとしました。図はエディタ画面のスクリーンショットです。すでにこういう時代なのですね。 (クリックで可読大表示)

ESP-WROOM-32 側

ESP32 モジュール側のプログラムは ESP-IDF 環境で作成しました。

処理の概要

  1. 所定の WiFi アクセスポイントへ接続
  2. 所定の URL へ HTTPS アクセスを行うためのタスクを生成して待機
    〜 下記 3. での要求発生時にリクエスト送出とレスポンス受信を行い終われば再び待機状態へ
  3. 永続ループで BLE アドバタイジングパケットのスキャンを開始
    〜 対象とする MAC アドレスのデバイスからの対象とする符丁を含む Eddystone-URL パケットを検知すると上記 2. のタスクへ所定の URL へのリクエストを要求

ソースコード

ここでは一台の micro:bit のみを対象としていますが、ソースコード中の Entries テーブルへ適宜エントリを追加することで複数の micro:bit を対象とすることが可能です。

A, B ボタンの押下に呼応するアクションのサンプルとして Gmail 発信, 携帯電話のコールのふたつの処理を紐付けています。これらはいずれも Microsoft Flow 上に作成したフローの URL への GET 要求によって実現しています。ふたつのフローのスクリーンショットを以下に示します。 (クリックで可読大表示)

Gmail 発信

電話機をコール(Twilio API を利用)

動作の様子

動画:1分18秒 (途中で電話着信音が鳴ります。音量に注意して下さい)


(tanabe)
klab_gijutsu2 at 09:06
この記事のURLComments(2)IoT | Bluetooth
2017年06月29日

最近のPython-dev(2017-06)

はてなブックマークに登録

バックナンバー:

PEP 546 -- Backport ssl.MemoryBIO and ssl.SSLObject to Python 2.7

2014年に Python 2.7 をセキュアな状態に保つため、過去に PEP 466 でセキュリティ関連の Python 3.4 の機能が Python 2.7 にバックポートされました。これには ssl モジュールも含まれており、 Python 2.7.9 からは TLS のホストを自動で検証するようになったりシステムの証明書ストアを利用できるようになりました。

今回の PEP 546 は、さらに Python 3.5 で追加された ssl.MemoryBIO と ssl.SSLObject も Python 2.7 にバックポートするものです。これらの API を利用すると、 socket をラップするのではなく、メモリ上のデータに対して TLS の処理ができるようになります。 Windows で winsock2 ではなく IOCP を使ったり、その他非同期IOフレームワークを使うときに socket と TLS を分離できることは重要です。

Python 2.7 自体のメンテは 2020 年に終了しますが、 pip が Python 2.7 のサポートを終了する時期は未定です。実際に今までも、pip はユーザーがほとんどゼロになるまでかなり長い間古い Python をサポートしてきました。

一方で pip が http クライアントとして使っている requests は、将来内部で非同期IOベースにしていくことを検討しています。そして Python 2.7 のためだけに複数の実装をメンテナンスすることには否定的です。

PEP 546 により、 pip と requests が Python 2.7 のサポートを完全に切る前でも、(今年後半にリリースされる) 2.7.14 以降のみのサポートに限定するという形で requests の内部に非同期IO化を実現する余地ができます。

実際にこのシナリオ通りに進まない可能性もあったので、本当に必要なのかどうか議論が白熱しましたが、バックポートが提案されているコードのサイズはCとPythonを足してほんの数百行でしかありません。

最終的に Python 2.7 のリリースマネージャーの Benjamin Peterson さんが、 PEP 466 のときにこのAPIがあったら一緒にバックポートされてたんだし、反対する強い理由は無いよね、といって Accept しました。

PEP 538 warning at startup: please remove it

先月僕が Accept した PEP 538 (C locale のとき LC_COLLATE を C.UTF-8 などに上書きする) が実装され取り込まれました。

さて、この新機能は、 locale の変更に成功したときにはユーザーに最初からUTF-8のロケールを設定するよう促すための warning を、失敗したときは C locale を使ってるから非ASCII文字で問題が出るよという warning を表示していました。

しかし、この warning が単純に鬱陶しいのと、子プロセスを起動して標準入出力を見るようなテストが幾つかのマイナーなプラットフォームで壊れてしまってメンテが面倒になってきました。

ということで、どちらの warning もデフォルトで無効になりました。環境変数に PYTHONCOERCECLOCALE=warn と設定したら有効になるのですが、まぁ誰も使わないでしょうね。

LC_COLLATE を変更して UTF-8 を使えるようにしても問題になることはめったに無いですし、 C locale で非ASCII文字の扱いに制限があるのは Python 3.6 と同じで何かが悪くなったわけは無いので、 warning が無くなることで困るユーザーはほとんど居ないと踏んでいます。

PEP 544 -- Protocols: Structural subtyping (static duck typing)

Python の typing モジュールに structural subtyping のサポートが追加されました。

簡単に説明すると、 typing.Protocol クラスを継承したクラスを定義すると、それがプロトコルになります。プロトコルクラスのメソッドと同じシグネチャのメソッドを持つクラスは、そのプロトコルクラスを継承していなくても、静的チェッカはサブクラスと判断するようになります。

PEP から分かりやすい例を引用します。次の例は、ユーザー定義型 BucketSizedIterable だと型チェッカに教えるために継承を使っています。

from typing import Sized, Iterable, Iterator

class Bucket(Sized, Iterable[int]):
    ...
    def __len__(self) -> int: ...
    def __iter__(self) -> Iterator[int]: ...

PEP 544 が承認されると、 Sized や Iterable が Protocol になるので、この Bucket は継承を外しても SizedIterable[int] の部分型だと型チェッカが判断するようになります。

個人的には、 ABC を継承すると起動が遅くなる(クラスの生成が遅くなる)ので継承しなくても良くなるのは嬉しい一方、最初から typing が ABC を使わないでいてくれたらもっと軽量化できたのになーという気持ちもあります。

typing のオーバーヘッドについては先月の Language Summit でも話題になったらしく、MLなどでも議論されているので、これから最適化オプションで削るなどの改善が提案されるかもしれません。

起動高速化

3月号で紹介したのですが、Pythonの起動を高速化するためのプルリクエストを2件出していて、片方 は3月中にマージされ、もう片方 を一昨日マージしました。特に後者は macOS 上での性能向上が顕著で、クリーンインストールされた Python で起動時間が30msくらいだったのが20msくらいに減ります。

site という、 (Python の -S オプションを使わなかったときに) 自動で読み込まれるモジュールがあり、これが sys.path というライブラリを import するときに探す場所などを準備しています。 pip とか apt-get とかでインストールしたモジュールがすぐに使えるようになるのはこの site のおかげです。

この site が、いくつかのディレクトリの場所を決定するために sysconfig という Python の環境情報を集めたライブラリを読み込んでいるのですが、それがそこそこ重いです。そしてさらに macOS の場合、 Framework 内のディレクトリ名 ("Python" だったり "Python3.7" だったり) を調べるために sysconfig から _sysconfigdata という、 Makefilepyconfig.h から大量の変数をかき集めた dict を持つモジュールを利用していました。

Framework 名を sys._framework に入れて _sysconfigdata の必要性をなくし、さらに sysconfig から必要なごく一部分だけを site 内にコピーすることで、起動時間の短縮だけでなく、 mac の場合は多分起動直後のメモリ使用量も1MB程度減っています。

また、 site からのモジュールの import を減らすことによる最適化は、そのモジュールが functools みたいによく使われるものであれば結局アプリケーションの起動時間には寄与しない事も多いのですが、 sysconfig を使うのは site 以外には pipdistutils などのパッケージング関連のツールやライブラリだけなので、今回の最適化は多くのアプリケーションの起動時間・メモリ使用量にもそのまま寄与するはずです。

songofacandy at 22:29
この記事のURLComments(0)TrackBack(0)Python 
2017年06月23日

音を利用する 5 〜mbed マイコンでの応用:実装編〜 (終)

はてなブックマークに登録

この記事の内容

前回の記事では mbed LPC1768 を使って音声信号の入出力と分析を試みました。今回はその内容をもとに実用性の見込めるものを作ってみます。

※記事では音の分析に以下のアプリケーションを利用しています

1. 音をトリガーに通知を行うしくみを作る

まず、所定の音を鳴らすとそれに反応して所定の連絡先へ通知を行うしくみを想定しました。装置が屋内で常時集音と分析を行い、所定の周波数の連続性のある音声信号を検知するとインターネット経由で所定の対象へ通知を送るイメージです。次のように考えました。

  • 音は電磁波に比べより小さなエネルギーで発生させることができるため何らかの処理を指示するためのトリガーとしての有用性が見込まれる
  • 音は人間の官能に直結しており扱いが比較的容易であるため他の選択肢が閉ざされた状況において意思を伝えるための最後の手段となり得る
  • マイコンは常時稼働を前提とする情報処理装置の構成要素として好適でありこうした使い方は「IoT」そのものでもある

使途はいろいろ考えられそうですが、たとえば日常的に危険の潜在する高齢者世帯や独居世帯において非常時に SOS を発信するための用心棒としても有用でしょう。

どのような音を使うか

通知のトリガーとする音には所定の周波数と振幅の維持が求められます。もっとも簡単にそのような音を出せる身近な道具として「笛」に目を向け 5 種類の製品を試してみました。それぞれに特長がありこれらの笛製品の音をすべてトリガーとして利用したいと考えました。

A&F 社製 サイレントホイッスル 犬笛

  • A&F エイアンドエフ サイレントホイッスル - www.aandfstore.com
    人に聞こえる可聴領域の低音と犬にしか聞こえない超音波領域(16000〜22000Hz以上)の 高音を出すことができる軽量なアルミ合金から削り出して作った、たった7gの犬笛です
  • A&F エイアンドエフ サイレント ホイッスル - www.yodobashi.com
  • 長所
    • 日常的な環境音とはほとんど競合しない高周波の音を簡単に出力できる
    • 振幅の大きな音を「静か」に鳴らすことが可能
    • 少ない息で大きく長く音を出せる
    • 今回の中ではもっとも遠くまで音が伝わる
  • 備考
    • 価格が高め(実売¥2,000前後)
    • 犬笛は音が人間に聞こえにくい性質上一般的な防災・避難用途には適さない ※この製品自体は可聴音域へ切り替えが可能
サンプル(強い息:音量注意)
ピークは 10000Hz 付近
周波数分布

コクヨ社製 防災用救助笛

  • 防災用救助笛ツインウェーブ - 救助・避難用品 - www.kokuyo-st.co.jp
    ツインウェーブは人の耳に最も良く聞こえる約3000Hz〜5000Hzの周波数の中で、2種類の波長を出すため、より広範囲な人への注意喚起が可能となります。 小さな息でも大きな音を出すことができます。
  • コクヨ 防災用救助笛 防災の達人 ツインウェーブ オレンジ DRK-WS1YR - www.amazon.co.jp
  • 特別対談 山村武彦氏×鈴木松美氏(提供:コクヨ S&T 株式会社) - www.webside.jp
    山村氏 この「ツインウェーブ」ですが、一見、笛とはわかりませんね。・・・
    いつでも、どこでも、誰でもが持ち歩けるというのは、非常に大きなポイントだと思います。・・・
    鈴木氏 ひとつの音だけではなく、2種類、3種類の音が出せれば、より助けを求めやすいのではないかと思います。 ツインウェーブは、3200ヘルツと4800ヘルツの音を同時に出します。・・・
    鈴木氏 ツインウェーブは、通常で700〜800メートル先までは音が届きますから、エレベータで閉じ込められた場合なども有効です。 さらに、軽い息で大きな音を鳴らせるので、お年寄りにも最適です。力がなくても同じ周波数が出せるように、ずいぶん苦労しました。息のエネルギーをほとんど音に変換して、非常に効率がいいんです。 山村氏 この笛の効果は、これからの防災対策に欠かせないように思います。高齢者でも、 寝たきりの人でも、笛さえ吹ければ自らの居場所を知らせることができます。
  • 長所
    • 少ない息でより大きな音を出せる
    • プラスティック製かつ蓋つきで小さく携帯性に優れる
    • コストパフォーマンスが高い(実売¥400前後)
  • 備考
    • 手元での確認では製品名の由来でもある「2種類の波長」はあまりはっきりと観察できなかった。吹き方の影響?
サンプル(強い息:音量注意)
ピークは 3200Hz 付近
周波数分布

モルテン社製 電子ホイッスル

サンプル(音量注意)
ピークは 3800Hz 付近
周波数分布

レモン社製 緊急用呼子笛

  • 緊急用呼子笛 - lemon.gr.jp
  • 緊急用 呼子笛 885800 - www.amazon.co.jp
    携帯に便利なペンダントタイプの緊急用呼子笛です。 身元確認のためのIDカードが付いています。
  • 長所
    • 少ない息で大きく長く音を出せる
    • スリムで軽く携帯しやすい
    • コストパフォーマンスがきわめて高い(実売¥100〜200程度)
  • 備考
    • 「アルミ製」だが剛性に若干の不安あり
サンプル(強い息:音量注意)
ピークは 4300Hz 付近
周波数分布

一般的なホイッスル(メーカー不明)

  • 長所
    • 百均店で買えるなど入手性がよく紛失してもダメージが少ない
    • スタンダードであるため一見して笛と認識でき誰でも簡単に扱える
  • 備考
    • 音量の確保と持続に多くの息が必要
サンプル(強い息:音量注意)
ピークは 2300Hz 付近
周波数分布

どのように通知を行うか

緊急連絡用途を想定すると通知は複数の方法を併用することが好ましいと考えました。今回は以下のみっつの方法を想定しサービスの選定を行いました。

電話機への通知

電話はもっとも手早い通知手段でしょう。電話連絡を手近に扱えるサービスとして米国発の「Twilio」を知りこれを利用することにしました。所定の相手に対し所定の音声メッセージを流すことも簡単に実現できますが、ここでは電話機をコールするだけで受け手は何ごとかの発生を察知できるためコストのかかる音声通話機能を利用する必要はないかもしれません。

Twilio

  • Twilio - APIs for Text Messaging, VoIP & Voice in the Cloud - www.twilio.com
  • Twilio for KDDI Web Communications | クラウド電話API - twilio.kddi-web.com
    Twilio は開発者向けのプラットフォームサービス、たった数行のコードを書くだけでネットと電話・SMS をつなぎます。ネット上の仕組みを電話から操作したり、ネット上の情報を電話や SMS に通知することができるようになります。
  • API リファレンス - 全Twilio APIおよびSDKの詳細リファレンス - jp.twilio.com
    Twilio の REST API を使って、ユーザー アカウント、電話番号、通話、 テキスト メッセージ、録音のメタデータにクエリを送信することができます。 発信通話を開始したりテキスト メッセージを送信したりすることができます。
           :
  • 料金 - twilio.kddi-web.com
    電話番号料 1ヶ月あたり
    050 番号 100 円(税別) 108 円(税込)
           :
    発信料 1 分あたり
           :
    携帯電話宛(070 / 080 / 090 番号) 15 円(税別) 16.2 円(税込)
           :

サインアップするとトライアルアカウントとして試用ポイントが提供され(額は非公開)、同ポイントが尽きるとフルアカウントへのアップグレードとクレジットカード番号の登録を促されるしくみです。このポイントで 050 番号を一件取得しました。プログラムから簡単に電話連絡を扱えるのは便利なのでトライアル終了後も利用を継続したいと考えています。ちなみにコールだけなら上記の「発信料」は発生しません。

メッセージプッシュによる通知

スマホ・デスクトップ(ブラウザ)へのプッシュ通知も行います。メジャーな Pushover や Pushbullet はそれぞれ IFTTT に専用チャネルを持っていることもあり便利に扱えます。Pushover は専用アドレスへのメール送信によって通知処理をキックすることもできます。

メールでの通知

手元で使い慣れた SendGrid を利用してメールでの通知を行います。

試作 1

以下の要領で試作を行いました。

  • 使用したソフトウェア資産
  • インターネット接続の利用に伴い装置に以下のアダプタを追加
  • 処理の流れ
    1. サンプリングレート 33330Hz, FFT サイズ 1024 で集音と周波数分析をループ実行する
    2. FFT 処理において次の帯域のピーク周波数が検知されれば 1. を中断し 3. へ移行する
      ただしノイズによる影響を抑えるため所定の回数以上の連続検知を条件とする
      • 9800Hz〜16000Hz(犬笛系)
      • 2000Hz〜4500Hz(ホイッスル全般)
    3. 前回の通知から所定の時間が経過していれば 4. へ。
      経過していなければ 1. へ(通知の頻発を回避)
    4. Google ドライブ上の所定のエントリに対する GET リクエストを試行。
      レスポンスコードが 200 なら 5. へ
      それ以外なら 1. へ(通知の無効化をリモートから可能とするための措置)
      • 所定のアカウントの Google ドライブ上のエントリへの第三者からのアクセスにおいては、当該エントリの共有がオンの状態なら 200 が返され、共有オフの状態なら 302 が返される。この仕様を低コストで取り回せる簡易フラグとして利用
    5. Twilio へ手持ちの電話のコールをリクエスト
      • 今回はコールのみを目的とし短時間でタイムアウトさせる
    6. SendGrid へ手持ち Gmail アドレスへのメール通知をリクエスト
      • Gmail のフィルタ設定により当該メールは Pushover 通知キック用のアドレスへ転送
    7. ふたたび 1. へ

動作の様子

動画:2分31秒  ※音量注意

ソースコード

Sonic06_FFT3

  • main.cpp
    
    // 所定の周波数信号を所定の回数以上連続検知すると通知を行う
    
    #include "mbed.h"
    #include "rtos.h"
    #include "EthernetInterface.h"
    #include "HTTPClient.h"
    #include "FastAnalogIn.h"
    
    #define DEVELOP
    #ifdef DEVELOP
    #define dbg(...) printf(__VA_ARGS__)
    #else
    #define dbg(...)
    #endif
    
    //#define USE_DHCP
    #if !defined(USE_DHCP)
    #define IP      "192.168.0.113"
    #define GATEWAY "192.168.0.1"
    #define MASK    "255.255.255.0"
    #endif
    
    #define SAMPLE_RATE 33330
    // n >= 2, n = power of 2
    #define FFTSIZE 1024
    // length of ip >= 2+sqrt(n/2)
    const int IP_LEN = 4 + sqrt((double)(FFTSIZE/2));
    // 当該サンプリングレートでのサンプリング間隔
    const float SEC_PER_SAMPLEPOINT = 1.0 / SAMPLE_RATE;
    
    // 通知の最短間隔秒
    #define INTERVAL_NOTIFICATION 180
    
    // 犬笛系周波数帯
    #define FREQ_LOW1   9800
    #define FREQ_HIGH1 16000
    
    // ホイッスル系周波数帯
    #define FREQ_LOW2  2000
    #define FREQ_HIGH2 4500
    
    // 対象周波数連続検知回数閾値
    #define MAXHITCOUNT 20
    
    // for Twilio
    #define TWILIO_SID  "AC1f7aa23e****************"
    #define TWILIO_AUTH "ef3638ca7*****************"
    #define URL_POSTDATA_TWILIO \
      "https://api.twilio.com/2010-04-01/Accounts/" TWILIO_SID "/Calls"
    #define REQUEST_HEADER_FOR_TWILIO \
      "Content-Type: "       "application/x-www-form-urlencoded"   "\r\n"
    #define POSTDATA_TWILIO \
      "To=%2B8190********" "&" \
      "From=%2B8150********" "&"  \
      "Url=http://demo.twilio.com/docs/voice.xml"  "&" \
      "Timeout=5"
    
    // for SendGrid
    #define SENDGRID_APIKEY "SG.UNFbQr_DQRmu8J********"
    #define REQUEST_HEADER_FOR_SENDGRID \
      "Authorization: Bearer " SENDGRID_APIKEY "\r\n" \
      "Content-Type: application/x-www-form-urlencoded\r\n"
    #define URL_POSTDATA_SENDGRID "https://api.sendgrid.com/api/mail.send.json"
    #define MAILTO    "********@gmail.com"
    #define MAILFROM  "mkttanabe2012@gmail.com"
    #define MAILSUBJECT "** ALERT [%s] **"
    #define MAILBODY  "%dHz"
    
    // Google ドライブ上のフラグエントリ URL
    #define URL_FLAGFILE \
      "https://docs.google.com/spreadsheets/d/1FR_RaR_AsyPAsIYU****************"
    
    Ticker tick;
    double *recBufp = NULL;
    int countTick = 0;
    FastAnalogIn mic(p20);
    AnalogOut speaker(p18);
    DigitalOut led(LED4);
    
    osThreadId mainThreadID;
    EthernetInterface eth;
    HTTPClient http;
    
    void rdft(int n, int isgn, double *a, int *ip, double *w);
    
    void setDummyTime() {
      struct tm t;
      t.tm_sec = t.tm_min = t.tm_hour = t.tm_mday = t.tm_mon = 1;
      t.tm_year = 100;
      time_t seconds = mktime(&t);
      set_time(seconds);
    }
    
    int initNetwork() {
      int sts;
      led = 1;
      dbg("start eth.init\r\n" );
    #ifdef USE_DHCP
      sts = eth.init();
    #else
      sts = eth.init(IP, MASK, GATEWAY);
    #endif
      if (sts != 0) {
        dbg("ech.init error!\r\n" );
        return -1;
      }
      dbg("start eth.connect\r\n" );
      sts = eth.connect();
      if (sts != 0) {
        dbg("eth.connect error!\r\n" );
        return -2;
      }
      dbg("my IP Address = %s\r\n", eth.getIPAddress());
      led = 0;
      return 0;
    }
    
    // Googleドライブ上の所定のフラグエントリをチェック
    int checkFlagFile() {
      char buf[8];
      for (int i = 0; i < 6; i++) {
        int sts = http.get(URL_FLAGFILE, buf, sizeof(buf),
              HTTP_CLIENT_DEFAULT_TIMEOUT);
        if (sts == HTTP_OK) {
          int rc = http.getHTTPResponseCode();
          // 共有オンなら 200, オフなら 302
          dbg("Flag is %s\r\n", 
            (rc == 200) ? "enabled" :
            (rc == 302) ? "disabled" : "?????");
          return rc;
        }
        dbg("sts=%d retrying..\r\n", sts);
        wait(5);
      }
      return -1;
    }
    
    int httpsPost(const char *targetUrl,
          const char *postData,
          const char *optHeader,
          const char *authId,
          const char *authPass) {
      int sts;
      char buf[64];
      HTTPText dataToPost((char*)postData);
      HTTPText resData(buf, sizeof(buf));
      http.setHeader(optHeader);
    
      for (int i = 0; i < 3; i++) {
        dbg("request to [%s]..\r\n", targetUrl);
        http.basicAuth(authId, authPass);
        sts = http.post(targetUrl, dataToPost, &resData, HTTP_CLIENT_DEFAULT_TIMEOUT);
        if (sts == HTTP_OK) {
          dbg("received HTTPS response OK\r\n");
          dbg("head of data [%s]\r\n", buf);
          dbg("done\r\n\r\n");
          break;
        } else {
          dbg("HTTPS request error=%d, status=%d\r\n", sts, http.getHTTPResponseCode());
          dbg("head of data [%s]\r\n", buf);
          dbg("sts=%d retrying..\r\n", sts);
          wait(5);
        }
      }
      return sts;
    }
    
    int doFFT(double *FFTBuffer) {
      int ip[IP_LEN];
      double *w = (double*)malloc(sizeof(double) * (FFTSIZE / 2));
      ip[0] = 0;
      rdft(FFTSIZE, 1, FFTBuffer, ip, w);
    
      double maxAmp = 0;
      int index = 0;
      int endIdx = FFTSIZE/2;
      for (int i = 0; i < endIdx ; i++) {
        double a = FFTBuffer[i*2]; // 実部
        double b = FFTBuffer[i*2 + 1]; // 虚部
        // a+ib の絶対値 √ a^2 + b^2 = r が振幅値
        double r = sqrt(a*a + b*b);
        if (r > maxAmp) {
          maxAmp = r;
          index = i;
        }
      }
      float freq = index * SAMPLE_RATE / FFTSIZE;
      free(w);
      return (int)freq;
    }
    
    // サンプリング
    void flip() {
      // AnalogIn.read() の値範囲は 0〜1.0 につき中心値を 0 に
      recBufp[countTick++] = mic.read() - 0.5;
      if (countTick == FFTSIZE) {
        tick.detach();
        countTick = 0;
        osSignalSet(mainThreadID, 1); // 集音完了通知
      }
    }
    
    // 通知
    int checkAndNotify(int freq, time_t *lastNotificationTime) {
      int pastSeconds = time(NULL) - *lastNotificationTime;
      // 前回の通知から所定の時間が経過の場合のみ次処理へ
      if (pastSeconds > INTERVAL_NOTIFICATION) {
        // 通信処理のために 集音・FFT用バッファを一旦開放
        free(recBufp);
        recBufp = NULL;
        // Google ドライブ上の所定のフラグエントリをチェック
        // 共有がオンなら 200 が返り オフなら が返る
        dbg("checking flagFile..\r\n");
        int rc = checkFlagFile();
        dbg("rc=%d\r\n", rc);
        if (rc == 200) {
          for (int i = 0; i < 8; i++) {
            led = !led;
            wait_ms(200);
          }
          // Twilio で携帯電話をコール
          httpsPost(URL_POSTDATA_TWILIO, POSTDATA_TWILIO,
            REQUEST_HEADER_FOR_TWILIO, TWILIO_SID, TWILIO_AUTH);
    
          // SendGrid でメール送信
          char b[256];
          snprintf(b, sizeof(b), "from=" MAILFROM
              "&to=" MAILTO "&subject=" MAILSUBJECT
              "&text=" MAILBODY, eth.getIPAddress(), freq);
          dbg("mes=[%s]\r\n", b);
          
          httpsPost(URL_POSTDATA_SENDGRID, b,
            REQUEST_HEADER_FOR_SENDGRID, NULL, NULL);
    
          // 今回通知時刻を保持
          *lastNotificationTime = time(NULL);
        }
        recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
        return 1;
      }
      // 前回の発呼から所定の時間が経過していない
      dbg("pastSeconds=%d..\r\n", pastSeconds);   
      return 0;
    }
    
    int main()
    {
      mainThreadID = osThreadGetId();
      setDummyTime();
      initNetwork();
      // 集音用兼FFT用バッファ
      recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
      time_t lastNotificationTime = 0;
      dbg("sampleRate=%dHz interval=%fsec\r\n", SAMPLE_RATE, SEC_PER_SAMPLEPOINT);
      
      tick.attach(&flip, SEC_PER_SAMPLEPOINT);
      // 対象周波数信号検出数
      int hitCount = 0;
    
      while (1) {
        // サンプリング完了まで待機
        osSignalWait(1, osWaitForever);
        // ピーク周波数を得る
        int freq = doFFT(recBufp);
        if (freq > FREQ_LOW1 && freq <= FREQ_HIGH1 ||
          freq > FREQ_LOW2 && freq <= FREQ_HIGH2) {
          dbg("detected! %dHz\r\n", freq);
          led = 1;
          // 所定回数以上連続検知の場合のみ通知処理へ
          if (hitCount++ > MAXHITCOUNT) {
            hitCount = 0;
            checkAndNotify(freq, &lastNotificationTime);
          }
        } else {
          if (hitCount > 0) {
            dbg("ignored..\r\n");
          }
          hitCount = 0;
          led = 0;
        }
        tick.attach(&flip, SEC_PER_SAMPLEPOINT);
      }
      if (recBufp) {
        free(recBufp);
      }
      return 0;
    }
    
    

試作 1 の問題点について

試作 1 では集音した信号の中でもっとも振幅の大きい周波数(ピーク周波数)を評価しており、また、対象範囲の周波数が FFT 処理において所定の回数以上 "連続して" 検知されることを判定条件としています。そのため周辺音の大きな環境では対象音を取りこぼす可能性があり、装置・音源間の距離が判定に影響を及ぼす度合いも少なくありません。この点の改善が必要と考えました。

下の動画には静かな状況と騒がしい状況での装置の反応を確認した様子を収めています。このように周囲の音が大きいと負けてしまいます。

動画:41秒  ※音量注意!

(似た話:Amazon Echo が反応しないので大声で怒鳴る人:Youtube より)

試作 2

以下の内容でプログラムの修正を行いました。

  1. ピーク周波数ベースの判定を変更
    • ピーク周波数の参照に代え信号に含まれる対象範囲の周波数成分を能動的にチェックする。これにより周辺音による干渉を抑えられる
  2. 検知の考え方
    • 上記のチェックにおいて所定のレベル以上の振幅値を伴うものの発見をもって検知とみなす。これにより対象音が微弱でもピックアップが可能となる
  3. 連続性の判定方法
    • 音の連続性の判定は「対象周波数が所定の回数以上続けてピーク検出されること」を条件としていたが、これを「前回の検出から所定時間内の再検出が所定の回数以上繰り返されること」に変更する。これにより対象音が途切れがちでも切り捨ての可能性を抑えられる
  4. 対象周波数範囲の見直し
    • 従来は 2000Hz〜4500Hz(ホイッスル全般)および 9800Hz〜16000Hz(犬笛系)のふたつのレンジを対象としていたが、前者は 基音として日常生活に登場する機会が少なくないため混乱につながるケースも想定される。そこで「上音」の存在に注目した
      • 倍音 - wikipedia
        上音 音を正弦波に分解したときに、最も低い周波数である基音以外の成分を 上音(じょうおん、overtone)という。この上音には倍音でない音も含まれる。 倍音は、基音の(2以上の)整数倍の周波数の上音であると言い換えることができる。
      • どのような音を使うか」項に掲載の各音源の周波数分布(スペクトル)図を参照すると倍音を含む上音が高周波領域まで豊かに鳴っている様子が見てとれる。抜粋を以下に示す。※図の縦軸最大値は 15kHz
      • 上記 1, 2 の措置により振幅の小さな成分であっても細かく検知可能となる背景もあり、実験的に 10000Hz 以上の音を対象とする改訂を加えた。サンプリングレート 33330Hz で集音しているため理論上 16600Hz 付近まで拾うことが可能  →サンプリング定理
        使用しているマイクモジュールのカバーする音域は公称「100Hz〜15kHz」 →データシート
      • むろん周囲の生活音等が 10000Hz 以上の上音を発生させることもあるが、意図的なものでない限り上記 3 の連続性チェックをすり抜ける可能性は実用上低いと考えられる

動作の様子

動画:2分3秒  ※音量注意!

ソースコード

(利用するプッシュ通知サービスを Pushover から Pushbullet に変更しています)

Sonic06_FFT3

  • main.cpp
    
    // 所定の周波数信号を所定の回数以上連続検知すると通知を行う
    // 改訂版:連続性のある 10kHz 以上の周波数成分をターゲットとする
    
    #include "mbed.h"
    #include "rtos.h"
    #include "EthernetInterface.h"
    #include "HTTPClient.h"
    #include "FastAnalogIn.h"
    
    #define DEVELOP
    #ifdef DEVELOP
    #define dbg(...) printf(__VA_ARGS__)
    #else
    #define dbg(...)
    #endif
    
    //#define USE_DHCP
    #if !defined(USE_DHCP)
    #define IP      "192.168.0.113"
    #define GATEWAY "192.168.0.1"
    #define MASK    "255.255.255.0"
    #endif
    
    #define SAMPLE_RATE 33330
    // n >= 2, n = power of 2
    #define FFTSIZE 1024
    // length of ip >= 2+sqrt(n/2)
    const int IP_LEN = 4 + sqrt((double)(FFTSIZE/2));
    // 当該サンプリングレートでのサンプリング間隔
    const float SEC_PER_SAMPLEPOINT = 1.0 / SAMPLE_RATE;
    
    // 通知の最短間隔秒
    #define INTERVAL_NOTIFICATION 180
    
    #define MAXHITCOUNT 30
    short indexOf10000Hz;
    
    // for Twilio
    #define TWILIO_SID  "AC1f7aa23e****************""
    #define TWILIO_AUTH "ef3638ca7****************""
    #define URL_POSTDATA_TWILIO \
        "https://api.twilio.com/2010-04-01/Accounts/" TWILIO_SID "/Calls"
    #define REQUEST_HEADER_FOR_TWILIO \
      "Content-Type: "       "application/x-www-form-urlencoded"   "\r\n"
    #define POSTDATA_TWILIO \
      "To=%2B8190********"" "&" \
      "From=%2B8150********"" "&"  \
      "Url=http://demo.twilio.com/docs/voice.xml"  "&" \
      "Timeout=5"
    
    // for SendGrid
    #define SENDGRID_APIKEY "SG.UNFbQr_DQRmu8J********"
    #define REQUEST_HEADER_FOR_SENDGRID \
      "Authorization: Bearer " SENDGRID_APIKEY "\r\n" \
      "Content-Type: application/x-www-form-urlencoded\r\n"
    #define URL_POSTDATA_SENDGRID "https://api.sendgrid.com/api/mail.send.json"
    #define MAILTO    "********@gmail.com"
    #define MAILFROM  "mkttanabe2012@gmail.com"
    #define MAILSUBJECT "** ALERT [%s] **"
    #define MAILBODY  "%dHz"
    
    // for IFTTT
    #define IFTTT_KEY "dSIwDguVEYfZ1SgiISL"********@"
    #define IFTTT_EVENT_NAME "ALERT"
    #define REQUEST_HEADER_FOR_IFTTT \
      "Content-Type: application/json\r\n"
    #define URL_POSTDATA_IFTTT  "https://maker.ifttt.com/trigger/" \
          IFTTT_EVENT_NAME "/with/key/" IFTTT_KEY
    
    // Google ドライブ上のフラグエントリ URL
    #define URL_FLAGFILE \
      "https://docs.google.com/spreadsheets/d/1FR_RaR_AsyPAsIYU****************"
    
    Ticker tick;
    double *recBufp = NULL;
    int countTick = 0;
    FastAnalogIn mic(p20);
    AnalogOut speaker(p18);
    DigitalOut led(LED4);
    
    osThreadId mainThreadID;
    EthernetInterface eth;
    HTTPClient http;
    
    void rdft(int n, int isgn, double *a, int *ip, double *w);
    
    void setDummyTime() {
      struct tm t;
      t.tm_sec = t.tm_min = t.tm_hour = t.tm_mday = t.tm_mon = 1;
      t.tm_year = 100;
      time_t seconds = mktime(&t);
      set_time(seconds);
    }
    
    int initNetwork() {
      int sts;
      led = 1;
      dbg("start eth.init\r\n" );
    #ifdef USE_DHCP
      sts = eth.init();
    #else
      sts = eth.init(IP, MASK, GATEWAY);
    #endif
      if (sts != 0) {
        dbg("ech.init error!\r\n" );
        return -1;
      }
      dbg("start eth.connect\r\n" );
      sts = eth.connect();
      if (sts != 0) {
        dbg("eth.connect error!\r\n" );
        return -2;
      }
      dbg("my IP Address = %s\r\n", eth.getIPAddress());
      led = 0;
      return 0;
    }
    
    // Googleドライブ上の所定のフラグエントリをチェック
    int checkFlagFile() {
      char buf[8];
      for (int i = 0; i < 6; i++) {
        int sts = http.get(URL_FLAGFILE, buf, sizeof(buf),
              HTTP_CLIENT_DEFAULT_TIMEOUT);
        if (sts == HTTP_OK) {
          int rc = http.getHTTPResponseCode();
          // 共有オンなら 200, オフなら 302
          dbg("Flag is %s\r\n", 
            (rc == 200) ? "enabled" :
            (rc == 302) ? "disabled" : "?????");
          return rc;
        }
        dbg("sts=%d retrying..\r\n", sts);
        wait(5);
      }
      return -1;
    }
    
    int httpsPost(const char *targetUrl,
          const char *postData,
          const char *optHeader,
          const char *authId,
          const char *authPass) {
      int sts;
      char buf[64];
      HTTPText dataToPost((char*)postData);
      HTTPText resData(buf, sizeof(buf));
      http.setHeader(optHeader);
      
      for (int i = 0; i < 3; i++) {
        //dbg("postData=[%s]\r\n", postData);
        dbg("request to [%s]..\r\n", targetUrl);
        http.basicAuth(authId, authPass);
        sts = http.post(targetUrl, dataToPost, &resData, HTTP_CLIENT_DEFAULT_TIMEOUT);
        if (sts == HTTP_OK) {
          dbg("received HTTPS response OK\r\n");
          dbg("head of data [%s]\r\n", buf);
          dbg("done\r\n\r\n");
          break;
        } else {
          dbg("HTTPS request error=%d, status=%d\r\n", sts, http.getHTTPResponseCode());
          dbg("head of data [%s]\r\n", buf);
          dbg("sts=%d retrying..\r\n", sts);
          wait(5);
        }
      }
      return sts;
    }
    
    int doFFT(double *FFTBuffer) {
      int ip[IP_LEN];
      double *w = (double*)malloc(sizeof(double) * (FFTSIZE / 2));
      ip[0] = 0;
      rdft(FFTSIZE, 1, FFTBuffer, ip, w);
      bool found = false;
      float freq;
      int endIdx = FFTSIZE/2;
      for (int i = endIdx - 1; i >= indexOf10000Hz; i--) {
        freq = i * SAMPLE_RATE / FFTSIZE;
        double a = FFTBuffer[i*2];
        double b = FFTBuffer[i*2 + 1];
        double mag = sqrt(a*a + b*b);
        if (mag > 0.2) {
          dbg("freq=%f mag=%f\r\n", freq, mag);
          found = true;
          break;
        }
      }
      free(w);
      return (found) ? (int)freq : 0;
    }
    
    // サンプリング
    void flip() {
      // AnalogIn.read() の値範囲は 0〜1.0 につき中心値を 0 に
      recBufp[countTick++] = mic.read() - 0.5;
      if (countTick == FFTSIZE) {
        tick.detach();
        countTick = 0;
        osSignalSet(mainThreadID, 1); // 集音完了通知
      }
    }
    
    // 通知
    int checkAndNotify(int freq, time_t *lastNotificationTime) {
      int pastSeconds = time(NULL) - *lastNotificationTime;
      // 前回の通知から所定の時間が経過の場合のみ次処理へ
      if (pastSeconds > INTERVAL_NOTIFICATION) {
        // 通信処理のために 集音・FFT用バッファを一旦開放
        free(recBufp);
        recBufp = NULL;
        // Google ドライブ上の所定のフラグエントリをチェック
        // 共有がオンなら 200 が返り オフなら が返る
        dbg("checking flagFile..\r\n");
        int rc = checkFlagFile();
        dbg("rc=%d\r\n", rc);
        if (rc == 200) {
          for (int i = 0; i < 8; i++) {
            led = !led;
            wait_ms(200);
          }
          // Twilio で携帯電話をコール
          httpsPost(URL_POSTDATA_TWILIO, POSTDATA_TWILIO,
            REQUEST_HEADER_FOR_TWILIO, TWILIO_SID, TWILIO_AUTH);
    
          // SendGrid でメール送信
          char b[256];
          snprintf(b, sizeof(b), "from=" MAILFROM
              "&to=" MAILTO "&subject=" MAILSUBJECT
              "&text=" MAILBODY, eth.getIPAddress(), freq);
          dbg("mes=[%s]\r\n", b);
          httpsPost(URL_POSTDATA_SENDGRID, b,
            REQUEST_HEADER_FOR_SENDGRID, NULL, NULL);
    
          // IFTTT 経由で Pushbullet 通知をキック
          snprintf(b, sizeof(b),
            "{\"value1\":\"ALERT [%s]\", \"value2\":\"%dHz\", \"value3\":\"\"}\r\n",
            eth.getIPAddress(), freq);
          httpsPost(URL_POSTDATA_IFTTT, b,
            REQUEST_HEADER_FOR_IFTTT, NULL, NULL);
    
          // 今回通知時刻を保持
          *lastNotificationTime = time(NULL);
        }
        recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
        return 1;
      }
      // 前回の発呼から所定の時間が経過していない
      dbg("pastSeconds=%d..\r\n", pastSeconds);   
      return 0;
    }
    
    int main()
    {
       dbg("**** TARGET FREQUENCY MODE ****\r\n");
       int endIdx = FFTSIZE/2 - 1;
       for (int i = 0; i <= endIdx; i++) {
        float freq0 = i * SAMPLE_RATE / FFTSIZE;
        // 10kHz 以上の高周波音をターゲットとする
        if (freq0 >= 10000) {
          indexOf10000Hz = i;
          float freq1 = endIdx * SAMPLE_RATE / FFTSIZE;
          dbg("idx0=%d freq=%f\r\nidx1=%d freq=%f\r\n",
              i, freq0, endIdx, freq1);
          break;
        }
      }
      mainThreadID = osThreadGetId();
      setDummyTime();
      initNetwork();
      // 集音用兼FFT用バッファ
      recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
      time_t lastNotificationTime = 0;
      
      dbg("sampleRate=%dHz interval=%fsec\r\n", SAMPLE_RATE, SEC_PER_SAMPLEPOINT);
      
      tick.attach(&flip, SEC_PER_SAMPLEPOINT);
      // 対象周波数信号検出数
      int hitCount = 0;
      time_t prevHitTime = 0;
      while (1) {
        osSignalWait(1, osWaitForever);
        // 監視対象とする周波数領域の成分が所定レベル以上の振幅ならその周波数、
        // そうでなければ 0 が返る
        int freq = doFFT(recBufp);
        if (freq != 0) {
          // 前回検出から1秒以内の再検出ならカウントアップ
          if (time(NULL) - prevHitTime <= 1) {
            // 所定の回数以上検知の場合は通知処理へ
            if (++hitCount >= MAXHITCOUNT) {
              dbg("checkAndNotify\r\n");
              checkAndNotify(freq, &lastNotificationTime);
              hitCount = 0;   
            }
          } else {
            hitCount = 1;
            dbg("restart\r\n");
          }
          prevHitTime = time(NULL);
        }
        tick.attach(&flip, SEC_PER_SAMPLEPOINT);
      }
      if (recBufp) {
        free(recBufp);
      }
      return 0;
    }
    
    

2. 着信音で遠隔操作を行うしくみを作る

自宅の固定電話機の着信音をトリガーに処理を行うしくみを考えました。次のイメージです。

  • 手持ちの携帯電話など所定の電話番号からのコールに対する固有の着信音を自宅固定電話へ設定しておく
  • 電話機の側に設置した装置は当該着信音によるコールを待ち続ける
  • 装置は所定回数のコールを連続して検知すると所定の機器への通電をオン/オフする
使途として防犯目的での屋内照明の遠隔操作などが考えられるでしょう。

過去の試みとの比較

手元で過去に何度か同様の遠隔操作を試みたことがあります。いずれもインターネット上のサービスを利用して装置へ通知を行い、それをトリガーに処理を行うものです。

これらはいずれもおおむね期待通りに動きましたが、一方で次のようなジレンマもありました。

  • 外部サービス側処理の遅延発生や一時停止の可能性を回避することはできない:問題 大
  • 遠隔操作にはインターネット接続環境が必須:問題 中
  • クライアントアプリ(Webブラウザを含む)の操作に手間がかかる:問題 小

それに対して、装置への通知に「音」を利用することには次のメリットが挙げられます。

  • 災害時等の特殊な状況以外での発着信上のトラブルの可能性はほぼ度外視できる
  • 発着信にはネット接続環境も特別なアプリケーションも不要
  • 発信電話番号にこだわらなければ携帯電話機を携行する必要もない

こういった特長を活かしうまく使えば有用なツールとなりそうです。

準備

現在自宅では固定電話機としてユニデン社製の「UCT-002」を使っています。

トリガーとする着信音として、この UCT-002 の「パターン2」を選びました。次のような音です。

  • 音の様子
  • 波形の概観(Audacity による)
  • 周波数分布
特徴をまとめてみます。

  • 1秒間発振 〜 2秒間無音 を繰り返す
  • 複数の周波数の音を小刻みに発振
  • mbed 上で前出のプログラムを使ってピーク周波数を確認すると以下の成分が検出された
    • 1204Hz
    • 1009Hz
    • 976Hz
    • 813Hz
    • 781Hz

試作

以上の内容をもとに次の要領で試作を行いました。

  • この電話機は装置への密接が可能であるためここでは常にピーク周波数に注目する
  • 上に挙げた 5種類の周波数成分を監視対象とする
  • 「対象周波数を所定回数以上連続検知すること」と「その後に 2秒程度の非検知が続くこと」をもって一回のコールと判断する
  • コールを 3回連続して認識した後にもう一度対象周波数を検知するとアクションを実行する
  • アクションの内容は AC電源機器への通電。前に作ったリレーつき電源ケーブルを mbed LPC1768 の p10 に接続し Hi / Low で機器への通電有無を切り替える
  • アクション発生時にはメールで通知する

動作の様子

動画:1分40秒  ※音量注意!

この動画では携帯電話から自宅固定電話をコールして照明の ON/OFF を行っています。

動画:1分24秒  ※音量注意!

PC 上で対象着信音をサンプリング〜改変したものを数種類鳴らし、装置がそれらには応じない様子を収めています。

ソースコード

Sonic09_FFT4

  • main.cpp
    
    // 所定パターンの電話着信音に反応してアクションを起こす
    // ユニデン社製 UCT-002 の着信音「パターン2」への対応版
    // 1秒発振〜2秒無音 の繰り返し
    
    #include "mbed.h"
    #include "rtos.h"
    #include "EthernetInterface.h"
    #include "HTTPClient.h"
    
    #define DEVELOP
    #ifdef DEVELOP
    #define dbg(...) printf(__VA_ARGS__)
    #else
    #define dbg(...)
    #endif
    
    //#define USE_DHCP
    #if !defined(USE_DHCP)
    #define IP      "192.168.0.113"
    #define GATEWAY "192.168.0.1"
    #define MASK    "255.255.255.0"
    #endif
    
    #define SAMPLE_RATE 33330
    // n >= 2, n = power of 2
    #define FFTSIZE 1024
    // length of ip >= 2+sqrt(n/2)
    const int IP_LEN = 4 + sqrt((double)(FFTSIZE/2));
    
    // 発振音に含まれる周波数成分(監視対象)
    int tgtF[] = {1204, 1009, 976, 813, 781};
    #define isTargetFreq(F) \
      (F == tgtF[0] || F == tgtF[1] || F == tgtF[2] || F == tgtF[3] || F == tgtF[4])
    
    // for SendGrid
    #define SENDGRID_APIKEY "SG.UNFbQr_DQRmu8J********"
    #define REQUEST_HEADER_FOR_SENDGRID \
      "Authorization: Bearer " SENDGRID_APIKEY "\r\n" \
      "Content-Type: application/x-www-form-urlencoded\r\n"
    #define URL_POSTDATA_SENDGRID "https://api.sendgrid.com/api/mail.send.json"
    #define MAILTO    "********@gmail.com"
    #define MAILFROM  "mkttanabe2012@gmail.com"
    #define MAILSUBJECT "** Power %s **"
    #define MAILBODY  "callCount:%d"
    
    Ticker tick;
    double *recBufp = NULL;
    int countTick;
    Timer timer;
    bool recDone;
    AnalogIn mic(p20);
    DigitalOut led(LED4);
    DigitalOut relay(p10); // リレーつき電源ケーブルを接続
    
    EthernetInterface eth;
    HTTPClient http;
    osThreadId mainThreadID;
    
    // 当該サンプリングレートでのサンプリング間隔
    const float SEC_PER_SAMPLEPOINT = 1.0 / SAMPLE_RATE;
    // 音声信号は実数データにつき実離散フーリエ変換(Real DFT)関数を使う
    void rdft(int n, int isgn, double *a, int *ip, double *w);
    
    void setDummyTime() {
      struct tm t;
      t.tm_sec = t.tm_min = t.tm_hour = t.tm_mday = t.tm_mon = 1;
      t.tm_year = 100;
      time_t seconds = mktime(&t);
      set_time(seconds);
    }
    
    int initNetwork() {
      int sts;
      led = 1;
      dbg("start eth.init\r\n" );
    #ifdef USE_DHCP
      sts = eth.init();
    #else
      sts = eth.init(IP, MASK, GATEWAY);
    #endif
      if (sts != 0) {
        dbg("ech.init error!\r\n" );
        return -1;
      }
      dbg("start eth.connect\r\n" );
      sts = eth.connect();
      if (sts != 0) {
        dbg("eth.connect error!\r\n" );
        return -2;
      }
      dbg("my IP Address = %s\r\n", eth.getIPAddress());
      led = 0;
      return 0;
    }
    
    int httpsPost(const char *targetUrl,
          const char *postData,
          const char *optHeader,
          const char *authId,
          const char *authPass) {
      int sts;
      char buf[64];
      HTTPText dataToPost((char*)postData);
      HTTPText resData(buf, sizeof(buf));
      http.setHeader(optHeader);
      
      for (int i = 0; i < 3; i++) {
        //dbg("postData=[%s]\r\n", postData);
        dbg("request to [%s]..\r\n", targetUrl);
        http.basicAuth(authId, authPass);
        sts = http.post(targetUrl, dataToPost, &resData, HTTP_CLIENT_DEFAULT_TIMEOUT);
        if (sts == HTTP_OK) {
          dbg("received HTTPS response OK\r\n");
          dbg("head of data [%s]\r\n", buf);
          dbg("done\r\n\r\n");
          break;
        } else {
          dbg("HTTPS request error=%d, status=%d\r\n", sts, http.getHTTPResponseCode());
          dbg("head of data [%s]\r\n", buf);
          dbg("sts=%d retrying..\r\n", sts);
          wait(5);
        }
      }
      return sts;
    }
    
    int doFFT(double *FFTBuffer) {
      int ip[IP_LEN];
      double *w = (double*)malloc(sizeof(double) * (FFTSIZE/2));
      ip[0] = 0;
      rdft(FFTSIZE, 1, FFTBuffer, ip, w);
    
      double maxAmp = 0;
      int index;
      for (int i = 0; i < FFTSIZE/2; i++) {
        double a = FFTBuffer[i*2]; // 実部
        double b = FFTBuffer[i*2 + 1]; // 虚部
        double r = sqrt(a*a + b*b);
        if (r > maxAmp) {
          maxAmp = r;
          index = i;
        }
      }
      double freq = index * SAMPLE_RATE / FFTSIZE;
      if (freq > 0 && maxAmp > 1.0) {
        //dbg("freq=%d mag=%f\r\n", (int)freq, maxAmp);
      }
      free(w);
      return (int)freq;
    }
    
    // サンプリング
    void flip() {
      // AnalogIn.read() の値範囲は 0〜1.0 につき中心値を 0 に
      recBufp[countTick++] = mic.read() - 0.5;
      if (countTick == FFTSIZE) {
      tick.detach();
      osSignalSet(mainThreadID, 1); // 集音完了通知
      }
    }
    
    // メール送信
    void sendMail(int callCount) {
      char b[128];
      snprintf(b, sizeof(b), "from=" MAILFROM
            "&to=" MAILTO "&subject=" MAILSUBJECT
            "&text=" MAILBODY, (relay == 1) ? "ON" : "OFF", callCount+1);
      httpsPost(URL_POSTDATA_SENDGRID, b,
            REQUEST_HEADER_FOR_SENDGRID, NULL, NULL);
    }
    
    int main() {
      time_t lastDetectedTime = 0, lastActionTime = 0;
      time_t lastNonDetectedTime = 0;
      int blankPeriod = 0, ringCount = 0, callCount = 0;
    
      relay = 0;
      setDummyTime();
      mainThreadID = osThreadGetId();
      initNetwork();
      recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
      dbg("start\r\n");
    
      while (1) {
        countTick = 0;
        tick.attach(&flip, SEC_PER_SAMPLEPOINT);
        osSignalWait(1, osWaitForever);
        if (time(NULL) - lastDetectedTime >= 4) {
          // 4秒以上対象音を非検知ならコールカウンタをリセット
          callCount = 0;
        }
        int freq = doFFT(recBufp);
        if (isTargetFreq(freq)) {
          // 前回のアクションから所定の時間が経過していなければ受け流す
          if (lastActionTime > 0) {
            int w = time(NULL) - lastActionTime;
            if (w < 30) {
              dbg("pending.. %dsec\r\n", w);
              continue;
            }
          }
          ringCount++;
          if (callCount >= 3) {
            // アクション実行
            relay = !relay;
            dbg("Power %s\r\n", (relay == 1) ? "On" : "Off");
            free(recBufp);
            sendMail(callCount);
            recBufp = (double*)malloc(sizeof(double) * FFTSIZE);
            callCount = ringCount = 0;
            lastActionTime = time(NULL);
          }
          lastDetectedTime = time(NULL);
        } else { // 非対象音
          blankPeriod += (time(NULL) - lastNonDetectedTime);
          if (blankPeriod > 30) {
            blankPeriod = 0;
          }
          lastNonDetectedTime = time(NULL);
          if (ringCount > 7) {
            if (callCount == 0 ||
              blankPeriod >= 2 && blankPeriod < 4) {
              callCount++;
              dbg("call count=%d\r\n", callCount);
              blankPeriod = 0;
            }
          }
          ringCount = 0;
        }
      }
      if (recBufp) {
        free(recBufp);
      }
      return 0;
    }
    
    

おわりに

音を題材に手元で行ってきた実験と試作の内容を 5 回に分けて紹介しました。音を媒体とする手法は旧くて新しい奥の深い世界です。新たな製品やサービス・アプリケーションを構築する際の道具立てのひとつとして、あるいは、他の方法がすべて利用できない場合の伏兵としても有用でしょう。とても興味深い経験でした。


(tanabe)
klab_gijutsu2 at 11:54
この記事のURLComments(0)TrackBack(0)IoT 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント
Syndicate this site