ホワイトボードの買い物リストを外から見えるようにする

ホワイトボードの買い物リスト?

我が家では台所にホワイトボードを設置しており、そこに「買い物リスト」を書いています。

(以前はLINEのチャットボットを使い、スマートフォンで買い物リストを管理していたのですが、在宅勤務中心となったためホワイトボードに移行しました。)

買い物に行くときは、スマートフォンでパシャッと、このホワイトボードのリストを撮影します。

しかし、何かのついでにふらっとスーパーに寄ったときなど、家の買い物リストに何が書かれていたかを知ることができず、必要な買い物ができない場合があります。

また、出かけるたびにホワイトボードをスマートフォンで撮影するのも面倒です。

ということで、「出先からホワイトボードを確認するための仕組み」を作ることにしました。

外から安全にホワイトボードを見たい

安直に考えれば、ホワイトボードが映るようにカメラを設置し、Webサーバか何かを外部に公開して、家の外から見えるようにするのが簡単です。

しかし、家にWebサーバを立てて外からアクセスできるようにするのは、我が家のセキュリティを強固に保つために、できればやりたくありません。

ということで、家にWebサーバを立てる案は却下です。

材料

さて、ここからはこの手法を実現するための材料を紹介します。

Google Drive

家にWebサーバを立てないということで、今回利用したのはGoogle Driveです。

ホワイトボードを撮影するカメラが撮影した画像を定期的にGoogle Driveにアップロードします。

これにより我が家のセキュリティを強固に保ったままホワイトボードの写真を外から見ることができるようになります。

TTGO T-Camera

写真の撮影はTTGO T-Cameraを利用します。

これはESP32-WROVER-Bとカメラモジュールの付いた、WiFiカメラを作るためのモジュールです。

似たようなボードは各種ありますが、今回利用するのは技適を取得しているESP32-WROVER-Bが搭載されたボードです。

(多分これかな? 自分はAliExpressで買いました)

Raspberry Pi 3

TTGO T-Cameraで撮影した映像をRaspberry Piから取得し、Google Driveにアップロードします。

もしかしたらTTGO T-CameraだけでもGoogle Driveにアップロードできるかもしれませんが、今回は簡単のためにRaspberry Piを利用しました。

丁度、我が家には「おうちサイネージ」として動かしているRaspberry Piがあるのでこれを利用することにしました。

inajob.hatenablog.jp

調理

さて、これらの材料を組み合わせてシステムを完成させていきます。

概要

これから構築するシステムの概要です。

TTGO T-Cameraで撮影した映像をRaspberry Piで定期的に取得し、Google Driveにアップロードします。

TTGO T-Cameraのファームウェアを我が家用に変更

TTGO T-CameraをWebカメラとして動作させるためには以下のソースコードを利用しました。 (というか購入時はこのファームウェアが入っていたようです)

github.com

WiFiのAPの設定を修正し家のWiFiに接続できるように再コンパイルして書き込みます。

起動すると画面にIPアドレスが表示されるので、そこにアクセスするとカメラの映像を確認できます。

我が家のルーターと相性が悪いのか、長く起動しているとWiFiの接続が切れてしまう問題が発生しました。

以下の記事を参考にし、WiFi接続が切れた時は自動的に再接続するようにしました。

randomnerdtutorials.com

TTGO T-Cameraを固定するための台

台所の壁に設置したホワイトボードを撮影するためには、TTGO T-Cameraを良い角度で固定する必要があります。

このためのカメラの脚を3Dプリンタで自作しました。

FreeCADを使ってこのような立体を作りました。

このパーツは2つくっつけるとこのようにL字型の蝶番のようになります。

TTGO T-Cameraのケース(写真に写る紫のケース)は 以下のモデルを使いました。

www.thingiverse.com

Raspberry Piにアップロード用スクリプトを仕込む

定期的にTTGO T-Cameraから画像を取得しGoogle Driveにアップロードするスクリプトを用意します。

Google Driveへのアップロードにはrcloneを利用します。

rcloneの初期設定はCLIのウィザードで実施することができます。CLIENT_ID, CLIENT_SECRETを設定するのがちょっと面倒だったのですが、設定しなくても一応動かすことはできます。(ただし共用のIDを使うことになりすぐに制限に引っかかってしまうので、常用するのには向きません)

スクリプトは以下のような感じです。

(convertコマンドにより画像を反転したり、日付を入れたり、過去にアップロードした画像を削除したりもしています。)

#/bin/bash

cd `dirname $0`

curl http://<TTGO T-CameraのIPアドレス>/capture -o todo.jpg
convert todo.jpg -rotate 180 -fill white -pointsize 25 -font Helvetica -draw "text 20,20 '`date`'" -background white todo.jpg

for f in $(rclone lsf "google drive:/ouchi-raspberry-pi/"); do
  echo "delete $f"
  rclone delete "google drive:/ouchi-raspberry-pi/$f"
done;

rclone copyto todo.jpg "google drive:/ouchi-raspberry-pi/todo-$(date "+%Y%m%d-%H%M").jpg"

同じ名前のファイルをどんどん上書きしても良いのですが、iPhoneGoogle Driveのアプリのキャッシュの処理などにより、画像が更新されないという問題が起きたので、日時に合わせて画像ファイル名を変更するようにしました。

Google Driveの共有設定

このようにしてアップロードした写真は、Google Driveの共有機能を利用すると他人と共有することもできます。

我が家でも妻と写真を共有し、夫婦どちらでも家の買い物リストを出先から見られるようにしました。

まとめ

TTGO T-Camera, Rasperry Pi, 3DプリンターなどIoT的な装置をフルに使ってホワイトボードの画像を外から見るためのソリューションを作ることが出来ました。

これでまた生活が便利になりました! 自分の知識を使って、生活を便利にするのは楽しいので、この記事をご覧の方もぜひ挑戦してみてください!

貧者のUSBケーブルチェッカーをセルフパワー化するケースを設計する

先に完成品の紹介

USBケーブルにイライラしたことはありませんか?

というのも市販の製品に同梱されているケーブルの中には、充電のみに対応しているものが結構多くあります。

データ通信に使いたい場面で、この充電のみに対応したケーブルを使っていると、当然通信できず、イライラすることとなります。

という事で、家にあるケーブルが充電のみに対応したものなのか、データ通信もできるのかを調べたくなります。

という事で作ったのがこちらです!↓

貧者のUSBケーブルチェッカーとは

USBケーブルチェッカーと言えばこの製品が非常に有名です。 (便宜上こちらは富者のUSBケーブルチェッカー、と呼んでいます)

この商品を買えば間違いはないのですが、自分の場合は、そこまで細かいスペックを知りたいわけではなく、単に「データ線が結線されていないUSBケーブルか否か」だけがわかれば良いという要件であることに気付きました。

となれば、単に指定したピンが両端のコネクタで結線しているかだけを見ればよいので、もっとシンプルな回路でチェックできそうです。

この観点でインターネットを探してみると・・ ズバリな商品がありました。

ja.aliexpress.com

ということで、早速これを購入してみました。

今回はこの基板の電源付きケースを設計した話です。

「この基板」では味気が無いので、私はこれを「貧者のUSBケーブルチェッカー」と呼ぶことにします。

セルフパワー化

この基板は外からUSBケーブルで電源を得るように設計されています。

そのため、テストしたいケーブルの両端に加えて、外部電源用のケーブルを接続する必要があり、利用する際にケーブルだらけとなってしまい煩雑だと感じました。

そこで基板にCR2032を取り付け直接電源を供給できるようにしました。

CR2032は3Vなので、USB電源とは電圧が違いますが、チェック用のLEDを光らせることが出来ることを確認できたので、結果オーライという事でそのまま結線しています。

基板上にテスト用のパッドが露出していたので、そこに導線をはんだ付けしました。

今回CR2032ホルダーも3Dプリンタで作ってみました。と言っても「口」の形で導線を通す穴を2つ開けただけの簡単なものです。

ここで紹介する設計手法について

私も3Dプリンター向けのデータをどうやって作るかを、まだ模索している段階なのですが、今回のやり方を文書化することで、新たな発見があるかも・・と思い記事にしてることにしました。

さらに良い方法があるよ、という方は是非教えてください。

利用するツール

今回は以下のソフトウェアを使って設計を行います

設計手法の紹介

ではここから具体的な設計方法について紹介します。

LibreCADで基板の部品を書き写す

基板上のでっぱりのあるコネクタのサイズなどを測定し、LibreCADで作図をします。やはりこういう作図はマウスが利用できるGUIのツールが便利です。 (ここは行き当たりばったりでやってたのでレイヤー名とか雑ですね・・)

作図補助レイヤーで垂直・水平のガイドを作図し、でっぱり部分をレイヤーで作図します。 (図で緑色と黄色の線が作図補助のための直線です。)

基板外形、でっぱりの高さが異なる部分は別のレイヤーにします。

OpenSCADではDXFファイルのレイヤーを指定してインポートできるので、便利です。

OpenSCADで作図する

今回はBOSL2というライブラリを使います。

このライブラリは、立体物にアンカーを設定して、そこに他の図形をアタッチする形で立体物を作成する機能を提供してくれます。

DXFファイルからレイヤーを指定して読み込む場合には以下のように書きます。

import (file = "board.dxf", layer = "rect-board");

論理演算で穴をあけるための造形物は以下のように書きます。(モジュール化もしています)

module hole()
  linear_extrude(height=3)
    import (file = "board.dxf", layer = "hole");
module hole2()
  linear_extrude(height=7)
    import (file = "board.dxf", layer = "hole2");

ケースの上部分はこのように書きます。

baseW = 57;  // 基板外形 幅
baseL = 46;  // 基板外形 奥行き
boardThickness = 1.6; // 基板厚
baseThickness = 2; // 底厚
baseSideThickness = 5; // 横壁厚
topH = 8; //ケース高さ

render() // ちらつき防止
diff("cut") // cutというtagの図形を切り抜く
yrot(180) // y軸中心に180度回転させる
cube([baseW, baseL, topH], anchor=[0,0,1]){ // 底面にアンカーを設定したケース外形の直方体を作成
  force_tags("cut"){ // 切り抜く部分をグループ化
    position([-1, -1, -1]){ // ケース外形の左-手前-下を原点とする
      hole();  // 基板上のでっぱりを引き算する(高さ別に4つのデータが存在)
      hole2();
      hole3();
      hole4();
      union(){ // バッテリー収納部を作図する(ここはLibreCADで作っていない)
        translate([44,22,0]) // 位置合わせ
        linear_extrude(height=topH - 1){ // バッテリー部分の壁厚は1mm
          circle(r = 10+1); // CR2032の半径=10mm+マージン1mmの円
          rotate([0,0,-90]) square([22+4, 14], center = true); // バッテリスナップ収納の為の矩形
        }
        translate([33,30,0]) // 位置合わせ
        linear_extrude(height=2) // 配線用のスペースを作図する(ここはLibreCADで作っていない)
          square([10,10]);
      }
    }
    // スナップフィットジョイントの作図
    xflip_copy() // X軸に対称にコピー
    position([-1, 0, -1]) // ケース外形の左-中央-下を原点とする
    yflip_copy()  // Y軸に対称にコピー
    up(baseH - baseThickness - boardThickness) // ジョイントの高さ位置合わせ
    back(10) // ジョイントの前後位置合わせ
    xrot(90) cyl(r=1, l=10, anchor=[0, 1, 0]); // ジョイントをシリンダで作成、アンカーは下面に設定
  }
}

ケース下部分はこのように書きます

baseH = 8;

render() // ちらつき防止
translate([0,50,0]) // 位置合わせ
diff("cut") //cutというtagの図形を切り抜く
cube([baseW+0.2, baseL, baseThickness + boardThickness], anchor=[0,0,-1]){ // 底面にアンカーを設定したケース外形(スナップ用の左右はみ出し部分を除く)の直方体を作成
  position([-1, -1, 1]) // ケース外形の左-手前-上を原点とする
    force_tags("cut") // 切り抜く部分をグループ化
      zflip() linear_extrude(height=boardThickness) // 基板外形を作図(linear_extrudeの向きが期待と逆なのでzflipしている
        board(); //DXFから基板外形を読み込む
  // スナップフィットジョイントと左右はみ出し部分の作図
  xflip_copy() // X軸に対称にコピー
    position([-1, -1, -1]) // ケース外形の左-手前-下を原点とする
      // 左右はみ出し部分は一部角丸の直方体として作図
      cuboid([baseSideThickness, baseL, baseH], anchor=[1,-1,-1], rounding=2, edges=["Z"], except_edges=RIGHT)
      yflip_copy()  // Y軸に対称にコピー
        position([1, 0, 1]) // 右-中央-奥を原点にする
          back(10) // ジョイントの前後位置合わせ
          xrot(90) cyl(r=1, l=10-1, anchor=[0, 1, 0]); // ジョイントをシリンダで作成、アンカーは下面に設定(1mm短くしてうまくハマるようにしている)
}

スナップフィットジョイントと書きましたが、市販品のような矢印状のものでは無く、単にかまぼこのような形の凹凸をはめ込むだけのジョイントです。

しかしこれでも十分に固定でき、かなり力を書けないと外れないケースを作ることが出来ました。

(説明をコメントになるべく書いてみましたが、なかなかわかりづらいですね。)

BOSL2所感

とにかくアタッチができるのが有用と感じました。

直方体を定義すると、その頂点または辺の中点を簡単にアタッチ原点と設定することが出来ます。

これにより相対的な立体図形の配置が出来るようになり、OpenSCAD単体で作図していると変数だらけになってしまう問題を解消できます。 (アタッチが出来ないので辺の長さをすべて変数に逃がす必要があった)

またxflip, yflip, zflipやup, down, left, rightなど軸を1つに限定した操作も可読性が良くなり良いものだと感じました。

cuboidには角丸のオプションがあり、フィレットが苦手なOpenSCADの弱点を少しは緩和させてくれます。 (それでもやはりフィレットは苦手ですが・・)

linear_extrudeの引き延ばした先にアタッチできないのが若干惜しい気持ちになりました。

まとめ

今回初めてLibreCADとOpenSCAD、そしてBOSL2を使った作図をしてみました。

LibreCADを使うとマウスを使って簡単に直感的な作図が出来ました。

立体的な造形はマウスよりコーディングの方が保守性が高いのでOpenSCADが有用だと感じました。

そしてBOSL2というOpenSCADのライブラリを使うことで、立体物の造形を比較的直観的に実現できました。

今回は試験的にツールを組み合わせただけなので、もっと良い方法があるかもしれません。

これより良い方法を思いついた方は是非https://twitter.com/ina_ani に教えてください。

ArduinoとCH9328でお手軽自作キーボード

CH9328とは

UARTを入力として受け取り USB HIDキーボードとして振舞うICのようです。

本来の用途としてはKVMスイッチなどでキーボードのエミュレーションを行うための物のようです。

今回はこのCH9328を搭載したモジュールボードを購入したので少し試食してみました。

f:id:inajob:20220405220253p:plain

自作キーボードとUSBデバイス

USB接続式の自作キーボードを設計するときに悩みの種となるのがUSBです。

まずUSBデバイス機能を搭載したマイコン、またはV-USBなどのソフトウェアエミュレーションに対応しているマイコンを用意する必要があります。

USBのプログラミングは煩雑で、メモリやCPUを結構消費します。

そして次の問題がVID/PIDです。作成したキーボードを個人で使う場合はそこまで問題となりませんが、販売するとなると正式には独自のVIP/PIDを取得しそれを利用するのが通常の流れです。

しかし、正規の方法でVIP/PIDを手に入れるためにはUSB-IFに申請し、結構多くのお金を納める必要があります。

同人ハードウェアなどの場合そこまでコストはかけられない、という事で、いくつかのベンダーがPIDのサブライセンスを行っているので、その仕組みに乗っかるなどしてPIDを取得している人もいるようです。

このあたりの事情は以下の記事が詳しいです。

qiita.com

CH9328とUSBのVID/PID

さて、今回扱うCH9328ですが、これはWCH社が販売しているICで、このICにはWHC社のVIDである0x1A86と、このIC固有の値であるPID 0xE026がデフォルト値として設定されています。 (この値は書き換えツールで変更できるようです)

この値をそのまま使う場合はWCH社のVID/PIDをそのまま使うことが出来ます(多分ライセンスとしてもこれでもないないはず、、USB Serial ICなどとやっていることは同じ)

Arduinoから使ってみる

ArduinoにはまさにCH9328を利用するためのライブラリである「CH9328-Keyboard」があります。

f:id:inajob:20220405212019p:plain

インストール出来たらHelloWorldのサンプルプログラムをArduino UNOに書き込んでさっそく実行してみます。

Arduino UNOで利用する際の罠

CH9328-Keyboardの内部で SERIAL_TX_ONLY という定数を使っていますが、どうもこれはESP8266などでのみ利用できる定数のようでArduino UNOではこのせいでコンパイルが実行できません。

インスール下ライブラリのCH9328Keyboard.cppの該当部分を削ることでコンパイルできるようになりました。

変更前
_Serial->begin(baud, SERIAL_8N1, SERIAL_TX_ONLY);

変更後
_Serial->begin(baud, SERIAL_8N1);

サンプルプログラムの実行

CH9328モジュールとArduino UNOはVCC,GND, TX, RXをそれぞれ接続します。

また動作モードを変更するジャンパのIO1,IO2をHIGHに設定します(このモジュールではジャンパをつけるとHIGHになるようです)

電力はCH9328モジュールからArduino UNOに送るのでArduino UNOはUSB接続する必要はありません。

Win+r押下の後 notepad + Enter を押下、その後Helloの文言が打ち込まれるというキーストロークのシミュレーションが行われ下記画像のようにメモ帳にメッセージが表示されました。

f:id:inajob:20220405212838p:plain

オリジナルキーボードの作成

ここまでくれば後は簡単です。

3キー搭載のマクロパッドを作ってみます。

自分のIDにちなんで「i」「n」「a」の3キーの存在するキーボードという事にします。

全く洗練されていませんがソースコードです。

#include <CH9328Keyboard.h>
#define PINRST 10
#define BAUDRATE 9600              //Default is 9600.
void setup()
{
  Keyboard.begin(&Serial, PINRST, BAUDRATE);
  delay(1000);
  Keyboard.releaseAll();

  pinMode(10, INPUT_PULLUP);
  pinMode(11, INPUT_PULLUP);
  pinMode(12, INPUT_PULLUP);
}

unsigned int trigger_b1 = 0;
unsigned int trigger_b2 = 0;
unsigned int trigger_b3 = 0;

void loop()
{
  int b1 = !digitalRead(10);
  int b2 = !digitalRead(11);
  int b3 = !digitalRead(12);

  if(b1){
    if(trigger_b1){
        Keyboard.press('i');
    }
    trigger_b1 ++;
  }else{
    Keyboard.release('i');
    trigger_b1 = 0;
  }
  if(b2){
    if(trigger_b2){
        Keyboard.press('n');
    }
    trigger_b2 ++;
  }else{
    Keyboard.release('n');
    trigger_b2 = 0;
  }
  if(b3){
    if(trigger_b3){
        Keyboard.press('a');
    }
    trigger_b3 ++;
  }else{
    Keyboard.release('a');
    trigger_b3 = 0;
  }

}

配線は Arduino UNOの10, 11, 12番ピンをタクトスイッチの片方にそれぞれ繋ぎ、タクトスイッチのもう片方はGNDに接続するだけです。

f:id:inajob:20220405220321p:plain

全体像はこんな感じです。

f:id:inajob:20220405220610p:plain

CH9328についてもう少し・・

値段やデータシートはLCSCが参考になります。

lcsc.com

モジュールは600円程しましたが、IC単品だと$1.5程度で買えそうです。AliExpressだともっと安いものも見つかると思います。 データシートは中国語の物しか見つかりませんでした。

しかし基本動作はシリアルから来たコードをHIDキーボードのコードに変換するだけなので、中国語が読めなくてもそこまで困ることはなさそうです。加えてCH9328-Keyboardライブラリがあるので、そもそも仕様を意識する必要もあまりありません。

AliExpressでCH9328で調べると様々なモジュールが販売されています。

私が今回利用したのは以下ですが、ほかにも様々なモジュールが存在します。

ja.aliexpress.com

よく似た型番の別のICや、ジャンパの設定が出来ないもの、水晶発振子が搭載されたもの、など様々な変種が存在するので、目的に応じて適切なものを購入してください。

(同じCH9328でも内部バージョンの違うものがあるようで、古いものは水晶発振子が外付けで必要のようです)

まとめ

CH9328を使うとUSB HID周りの煩わしい問題をすべて肩代わりしてくれるので、非常に簡単にUSBキーボードを自作できることがわかりました。

CH9328と低機能・低価格のマイコンを組み合わせる事で、部品は増えてしまいますが、多機能マイコン1つで作るよりも、安く、わかりやすい自作キーボードを設計することが出来ます。

チップの値段も安いので、新たな自作キーボードの材料として検討してみてはいかがでしょう?

最後に動作の様子の動画を貼っておきます。

6.5秒で起動する日本語対応Vimマシンを作った

これは何?

Raspberry Pi は高性能で比較的安く、面白いボードですが、標準のRaspberry Pi OSだと起動時間が遅いのがネックです。

起動時間を早くする方法はいろいろあるのですが、ここではBuildrootを使ってシンプルなイメージを作る事で、高速起動を実現してみました。

シンプルなイメージということで、今回は「日本語入力ができるVim環境」の構築目指すことにしました。

※と言ってもBuildrootを普通に使っただけでそこまでの最適化は行っていないです。

(今値段を見てびっくり、半導体不足の影響ですね、、今は買わないほうがよさそうです・・)

Buildrootによるイメージの作成

f:id:inajob:20220331222650p:plain

先人の知恵を借りてBuildrootを使ってイメージを作成します。

以下の記事の内容が非常に簡潔でそのまま真似しました。

qiita.com

そのまま、と書きましたが実際にはWindows PCの上で動くDockerの中で実行しました。

コマンドを列挙するとこんな感じです。

# dockerでubuntuの環境を作成
$ docker run --rm -it ubuntu bash

# apt-get update
# apt-get install wget make build-essential gcc g++ bzip2 cpio unzip rsync bc libncurses-dev file
# wget https://buildroot.org/downloads/buildroot-2022.02.tar.xz
# tar xJvf buildroot-2022.02.tar.xz
# cd buildroot-2022.02
# make raspberrypi0_defconfig

ここまでが準備です。

必要なパッケージをインストールして、buildroot本体を取得し展開します。

makeを使ってRaspberry Pi Zeroの設定を読み込みます。

他のRaspberry Pi向けのイメージを作る際はmake list-defconfigsを実行してボードを探してみてください。

次に細かい設定を行います。胃かを実行するとCUIで動くメニューが起動します。

# make menuconfig

キーボードを使っていくつか設定を変更します。

f:id:inajob:20220331221730p:plain

今回は最低限の日本語入力ができることを目的としているため以下の設定変更を行いました。

  • Toolchain
    • C libraryを muslに変更
  • Target packages
    • Show packages that are also provided by busybox のチェックを入れる
    • Text editors and viewers
      • vim にチェックを入れる
    • Graphic libraries and applications (graphic/text)
      • fbterm にチェックを入れる
    • Development tools
      • git にチェックを入れる(これはあってもなくても良い)

このあとmakeを実行するとビルドが始まります(数時間かかります)

# make

ビルドが完了すると output/images/sdcard.img が生成されます。

以下のようなコマンドでコンテナからイメージファイルをホストマシンに取り出します。

$ docker cp コンテナのID:/root/buildroot-2022.02/output/images/sdcard.img ./

後は適当なツールを使ってこのイメージをSDカードに焼きます。 (自分はRaspberry Pi Imagerを使いました)

この状態から日本語入力を実現するためのアイデア

普通に考えるとuimなどの日本語変換の仕組みをインストールするのですが、慣れないBuildrootで、これを実現するのはちょっと面倒そうです(やってみると簡単なのかもしれませんが)。

ということで、今回はvim上で動作するskk.vimというのを試してみることにしました。

日本語表示・入力のための下準備

ここからの操作はext3をマウントできる仕組みが必要です。自分は今回使うのとは別のRaspberry Piに外付けSDカードドライブを取り付けてこの作業を実施しました。 (以降~などのパスが出てきますが、これはSDカードを使ってブートした時のパスを表しています、ホストのRaspberry Piのパスではありません。)

日本語表示はfbterm、日本語入力はvim上で動作するSKKで実現します。

日本語表示に必要なttfファイルを~/.fontsに配置します。

自分は源真ゴシック (げんしんゴシック) | 自家製フォント工房 を使いました。ダウンロードしたのちに等幅フォントの「GenShinGothic-Normal.ttf」だけを取り出し上記パスに配置しました。

fbtermの設定(~/.fbtermrc)でGen Shin Gothic Normalを指定することで、このフォントが利用できます (利用できるフォント名は fbterm -v を実行すると確認できます。)

次にvim上で動作するSKKの設定です。

今回は以下を利用します。

github.com

以下のページを参考にセットアップを実施します。

qiita.com

~/.vim以下にgitリポジトリpluginディレクトリをそのままコピーするだけです。

SKKの辞書も上記記事のまま、wgetで取得しホームディレクトリ直下に解凍しておきます。

今回は上記記事を真似てSKK-JISYO.Lを使いました。

~/.vimrcにも上記記事そのままの設定を書きました

let skk_large_jisyo = '~/SKK-JISYO.L'
let skk_auto_save_jisyo = 1

ブート、日本語入力

さて、ここまで準備出来たら、SDカードをアンマウントしRaspberry Pi Zeroに挿入します。

HDMIケーブルでディスプレイに接続、USB-OTGアダプタを経由してキーボードを接続し、電源USBを接続します。

f:id:inajob:20220331222505p:plain

起動するとHDMIケーブルで接続したモニタにコンソールが表示されます。

私の環境では6.5秒程度でログイン待ちコンソールが表示されました。

f:id:inajob:20220331222526p:plain

ログインコンソールにrootと入力するとログインできます。

さらにfbtermと入力し日本語が表示できるようにします。

そして満を持してvimを実行し、挿入モードに入ってCtrl+Jを押してしばらく待つとskk.vimの日本語入力モードに入ります。

f:id:inajob:20220331222545p:plain

ということで、ここまでで 高速起動する日本語入力できる環境を作ることが出来ました。

まとめ

Buildrootを初めて使いましたが、特に工夫しなくても6.5秒で起動するイメージが作れる素晴らしいものでした。

Raspberry Pi 3/4など、もっと性能の良いボードを使えばさらに高速化できると思います。

まだまだ最適化の余地はありますし、SDカードの種類などでも起動の速度は異なると思います。

さらに、vimだけあれば動作するSKK実装を使うことで、簡単に日本語入力環境を作ることが出来ました。

自作のポメラ的なガジェットを作る際にはこの知見が役立ちそうです。

この記事をご覧になっている方で、似たような挑戦をしたことがある方は、ぜひ https://twitter.com/ina_ani に教えてください。

エンジニアパパの2歳までのクリエイティブなDIY育児の記録

このブログでもちょこちょこクリエイティブな育児情報を発信してきましたが、それらは割と大掛かりなものでした。

 

そこで今回は、記事にするほどでもない日々の小粒なDIY事例を紹介しようと思います。

 

同じようなコンセプトで生後2か月の時に書いた記事はこちら。

inajob.hatenablog.jp

うちの娘ももうすぐ2歳となりますが、ここまでに行ってきた小粒なDIY事例を紹介していきます。

 

3Dプリント失敗作を入れたマラカス

あまり遊んでくれず

f:id:inajob:20220327224149p:plain

おむつのパッケージの赤ちゃんを切り取っておもちゃ箱に貼り付ける

じっと見ていた

f:id:inajob:20220327224233p:plain

なぜか某アニメ風の顔をいろいろな育児グッズに描く(妻)

特に娘は反応せず

f:id:inajob:20220327224307p:plainf:id:inajob:20220327224524p:plain

うんこ棒の反対側を保護するキャップを3Dプリンーーで作る

f:id:inajob:20220328083525p:plain

良かった写真を額縁に入れて飾る(妻)

家にいてふと目に留まるとなごむ

f:id:inajob:20220327224340p:plain

足置きとしての技術書

足が地に着くほうが落ち着くらしいです。

f:id:inajob:20220327224413p:plain

トンネルを自作

結構遊んでくれました。

f:id:inajob:20220327224616p:plain

牛乳パックで積み木

これも結構遊んでくれました

f:id:inajob:20220328083613p:plain

コンセントカバーの隙間を3Dプリンターを使って埋める

別にこの隙間があっても困ることはないですが、不格好だったので・・

f:id:inajob:20220327225604p:plainf:id:inajob:20220327225623p:plain

ニューブロックで親が遊ぶ

親の方が夢中になるパターン

f:id:inajob:20220327224715p:plainf:id:inajob:20220327224740p:plain

気付くと真面目なメモを書き始める落書き大会

娘のために描いてたはずが、突然真面目なアイデアメモを書き始めたりもします。

f:id:inajob:20220327224805p:plain

水で絵が描けるシートが便利

f:id:inajob:20220327224827p:plain

これです。壁とかに落書きされないので安心です。

ドアノブをやたらと開け始めたので対策

ドアノブを外すと、90度回転させて取り付けることが出来ます。こうなると通常のノブより開けるのが難しくなります。(今の所これを開けることはないです

f:id:inajob:20220327224850p:plain

言葉遊びかるたを自作(妻)

f:id:inajob:20220327224908p:plain

緩衝材に顔を描いておもちゃにする

あまり遊んでくれず

f:id:inajob:20220327224939p:plain

マスキングテープを駆使して娘に年賀状を書いてもらう(妻)

悩みの種の年賀状のデザインを娘の落書きで済ませるという作戦。

マスキングテープで白を残した部分のおかげでデザインっぽくなった。

f:id:inajob:20220327225038p:plain

引き出しにイラストを描く(妻)

保育園とかでも良く見かけるアレ

f:id:inajob:20220327225055p:plain

ブロックで親が遊ぶ

親の方が熱中するシリーズ

f:id:inajob:20220327225119p:plainf:id:inajob:20220327225138p:plain

f:id:inajob:20220327225153p:plain

一口ゼリーの容器でテントウムシを作る(妻)

これはよく遊んでいた。コスパの良いおもちゃです。

f:id:inajob:20220327225216p:plain

縄跳びをぶつ切りにしてポットん落としを作る(妻)

ベビーシッターさんに教えてもらったおもちゃ

よく遊んでいる

f:id:inajob:20220327225250p:plain

これを並べて絵のようなものを作る

親の方が遊ぶパターン

f:id:inajob:20220327225316p:plainf:id:inajob:20220327225334p:plain

f:id:inajob:20220327225353p:plainf:id:inajob:20220327225410p:plain

洗面所下の収納をむやみに開かないようにする部品を3Dプリンターで作る

今の所これで開けられなくなりました。

f:id:inajob:20220327225457p:plain

まとめ

生後2か月ごろから 2歳くらいの間に作ったDIY系の事例をざざっと紹介しました。

ここまでの所、このように親も楽しみながら育児出来ています。

 

子供への影響とかそういうのもありますが、まずは親が楽しんで育児をするのが大切だと思っています。

 

引き続きクリエイティブに育児をしていきたいと思います。

キッチン水切り棚の導入と3Dプリンターによる不具合修正

キッチン水切り棚

皆さんの家は洗った食器を置く棚、どのようなものを使っていますか?

我が家はもともと、シンプルな水受けの付いた棚だったのですが、子供が生まれたこともあり、皿の数が増え、少し手狭になってきました。

そこで、いわゆる「水切り棚」を購入することにしました。

ここで言う「水切り棚」というのは、キッチンシンクの上に固定できる棚で、棚から落ちた水がそのままシンクに落下するため、水受けが不要となり、かつスペースが節約できるという便利収納家具のことです。

初回購入のミス

比較的大物家具という事で、長さを計ったりして、良さそうなものを注文したのですが、棚の脚間の内径と外形を取り違えたせいで、絶妙に我が家のシンク上に設置できない棚を購入してしましました・・

幅も足らなければ、高さも大きすぎて、シンク上収納と干渉してしまうという残念な結果です。

返品なども考えたのですが、ちょうど工作室の棚に良いという事に気付いたので、返品せずに活用することにしました。

f:id:inajob:20220320121334p:plain

これ、意外と良いですよ。

2度目の正直

初回購入でミスしたので、今度はしっかりと寸法を測って、適合する商品を探しました。

要件は・・

  • シンク幅制限
  • シンク高さ制限
  • できれば2段

f:id:inajob:20220320121419p:plain

どうやら我が家の要件だと、海外製の安い水切り棚では対応するサイズが無く、少し高めの日本製のものがよさそうだという事がわかりました。

という事で、2度目はこちらを購入。

この商品は横幅、高さともに可変という事で、我が家の要件にマッチしました。

棚部分は、フラットなものとかご状のものをそれぞれ選択できるのですが、フラットなものは端の囲いがなく、食器が落下しそうだったので、かご4つタイプを選択しました。

棚のサイズは良し!しかし・・・

設置してみたところ棚のサイズは問題なく、無事にキッチンに設置することが出来ました。

しかし、新たな問題として、かごと皿の相性問題が発覚しました。

この棚に付属しているかごの幅が、絶妙に我が家の皿とマッチしません。

小さな皿はすり抜けて落ちてしまうし、大きな皿はかごの幅が足りず入りません。

f:id:inajob:20220320121452p:plain f:id:inajob:20220320121514p:plain f:id:inajob:20220320121538p:plain

またしても失敗か・・・

と思ったのですが、代替となりそうな棚も見つからないし、何とか工夫して乗り切ることにしました。

Try1 100均の皿置きを重ねる

収納で利用していた皿置きを棚の上に載せてみました。これで一応皿を収納することが出来ました。

f:id:inajob:20220320121605p:plain

しばらくこれで運用していたのですが、皿が高い位置に収納されてしまうため、上の収納と干渉してしまい、出し入れするのが難しいことがわかりました。

Try2 3Dプリンタでシンデレラフィットの皿置きを作る

Try1 は結構いい感じだったのですが、皿置きの幅と棚の幅が合わないために、無駄にスペースを取ってしまっていたのが問題だと考えました。

そこで我が家の3Dプリンタを活用して、棚のサイズにぴったりの皿置きを作る事にしました。

この棚の問題は大きく2つあります

  • 棚の幅が狭いのに、深すぎて、大きな皿が奥まで入らない
  • 棚の網の間隔が広すぎて、皿が倒れたり、小さな皿が落下する

これを解決するために、以下の対策を考えました

  • 皿置きに高さを持たせることで負傘を底上げする
  • 網の間隔を狭くする、横網により落下を防ぐ

ということで、OpenSCADでささっと立体を設計し、、3Dプリンタで印刷! f:id:inajob:20220320121636p:plain

意外と大きくなってしまったので、形状は単純なのですが印刷に5時間ほどかかる大作になってしまいました・・

f:id:inajob:20220320121655p:plain

何とか使えるものになった

ということで、使い勝手が微妙だった水切り棚に、3Dプリンタで作った皿置きを組み合わせる事で、何とか使い物になるソリューションが完成しました。

f:id:inajob:20220320121724p:plain

まとめ

色々失敗も多かった水切り棚の購入ですが、いろいろと試行錯誤して、それなりに使いやすいものが出来ました。

3Dプリンタはこういうちょっとした生活の工夫が出来て便利です。

TVチューナー(EX-BCTX2)が時々HDDを見失う対策と録画一覧をDLNAで引っこ抜く話

これは何?

我が家ではモニタ一体型TVではなく、TVチューナーをPCモニタにつなげています。

inajob.hatenablog.jp

製品としてはEX-BCTX2です。

まぁ、いわゆるnasne的なTVチューナーです。

この製品はHDDを内蔵しておらずUSB接続HDDを外付けします。

しかし、どうもこれが曲者で、我が家では、1か月に1度くらいの頻度で接続しているHDDを見失うことがありました。

この現象が起きると、録画済みの番組が見れないだけでなく、録画そのものが行われなくなるため、せっかく見たくて録画予約していた番組の録画が失敗しとても悲しい気持ちになります。

前からこの現象には悩まされてきたのですが、頻度が低かったので特に対策していませんでした。

しかし、先日「ダイの大冒険」を録画し損ねて、とても悔しかったので、その気持ちが熱いうちに対策を考えることにしました。

ステータスページなどは無いのか?

ひとまずHDDを見失うのはあきらめて、「HDDを見失ったことに気付く」というのが出来るのかというアプローチで調査を始めました。

このTVチューナーはIPアドレスを持つので、そこにブラウザでアクセスするなどして、HDDの様子がわかるのであれば、スクレイピングなどで、監視が出来そう・・と思ったのですが、、

表示されるのはファームウェアのバージョンやIPアドレスの情報だけで、HDDの接続状態は含まれていませんでした。

f:id:inajob:20220213203427p:plain

メディアプレイヤーから見ると・・?

このチューナーは専用のソフト(Windowsだと「テレキングリモート」「テレキングプレイ」)を使って視聴するのですが、なぜか録画した番組一覧をWindowsのメディアプレイヤーから取得することが出来ることに気付きました。 (※当たり前ですが、著作権保護機能により映像を見ることはできません)

f:id:inajob:20220213204003p:plain

これは、何らかの標準的なプロトコルで、録画した番組情報を取得できるのだろう、とあたりをつけました。

この方法がわかれば、録画した番組情報が取得でき無くなる==HDDの認識が出来なくなっている、と外部のプログラムから気付くことが出来ます。

UPnPDLNA

ちょっと調べるとすぐにわかりました、まずこのTVチューナーはUPnPでサービスを広報する仕組みがありました。 そして、そのアドレスに対してDLNAと呼ばれる仕様でアクセスすることで、メディアサーバーとしてある程度の操作が出来そうという事がわかりました。

UPnP

まずはUPnPを使ってみます。

丁度我が家のLANにはRaspberryPiが常時起動しているので、ここからコマンドを実行してTVチューナーのサービスを調べてみることにします。

下記のようにgupnp-toolsパッケージに含まれるgssdp-discoverというコマンドでこれを調べることが出来ました。 (USNに含まれるuuidを公開するのがまずいのかどうかよくわかっていないのでマスクしています、多分大丈夫だと思うけど・・)

$ sudo apt-get install gupnp-tools
...
$ gssdp-discover -i eth0 --timeout=3
...
Using network interface eth0
Scanning for all resources
Showing "available" messages
resource available
  USN:      uuid:XXXXXXXXXX::upnp:rootdevice
  Location: http://192.168.1.15:55958/drgd/
  USN:      uuid:XXXXXXXXXX
  Location: http://192.168.1.15:55958/drgd/
resource available
  USN:      uuid:XXXXXXXXXX::urn:schemas-digion-com:device:DragD:1
  Location: http://192.168.1.15:55958/drgd/
resource available
  USN:      uuid:XXXXXXXXXX::urn:schemas-digion-com:service:DragPlusLRManager:1
  Location: http://192.168.1.15:55958/drgd/
resource available
  USN:      uuid:XXXXXXXXXX::upnp:rootdevice
  Location: http://192.168.1.15:55959/public/
resource available
  USN:      uuid:XXXXXXXXXX
  Location: http://192.168.1.15:55959/public/
resource available
  USN:      uuid:XXXXXXXXXX::urn:schemas-iodata-jp:device:NetworkTunerCommand:1
  Location: http://192.168.1.15:55959/public/
resource available
  USN:      uuid:XXXXXXXXXX
  Location: http://192.168.1.15:55247/dms/
resource available
  USN:      uuid:XXXXXXXXXX::urn:schemas-upnp-org:device:MediaServer:1
  Location: http://192.168.1.15:55247/dms/
resource available
  USN:      uuid:XXXXXXXXXX::urn:schemas-upnp-org:service:ContentDirectory:1
  Location: http://192.168.1.15:55247/dms/
resource available
  USN:      uuid:XXXXXXXXXX::urn:schemas-upnp-org:service:ConnectionManager:1
  Location: http://192.168.1.15:55247/dms/
resource available
  USN:      uuid:XXXXXXXXXX::urn:schemas-digion-com:service:X_AccessControl:1
  Location: http://192.168.1.15:55247/dms/
resource available
  USN:      uuid:XXXXXXXXXX::urn:schemas-digion-com:service:X_DeviceConfiguration:1
  Location: http://192.168.1.15:55247/dms/
resource available
  USN:      uuid:XXXXXXXXXX::urn:schemas-dlpa-jp:service:X_DtcpPlus:1
  Location: http://192.168.1.15:55247/dms/
...

これで出てきたHTTPのアドレスにはブラウザでアクセスすることが出来ました。

今回目をつけたのはContentDirectoryというものです。どうも、ここにうまいことリクエストすることで、録画した番組一覧などを取得できるようです。

DLNA

ここからはDLNAという仕様に則って通信を行えば良さそうです。 少し調べるといくつか参考になりそうな情報がありました。 DLNAと言っても、その実態はHTTPのGET/POSTのリクエストのようで、Curlなどでも実行できるものでした。

まずはPOSTのリクエストボディを作ります。

読む限りSOAPのようです(よく知らんけど・・)。

<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <SOAP-ENV:Body>
    <m:Browse xmlns:m="urn:schemas-upnp-org:service:ContentDirectory:1">
      <ObjectID>0</ObjectID>
      <BrowseFlag>BrowseDirectChildren</BrowseFlag>
      <Filter>*</Filter>
      <StartingIndex>0</StartingIndex>
      <RequestedCount>200</RequestedCount>
      <SortCriteria></SortCriteria>
</m:Browse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

なんとなく意味するところとしては、

  • ContentDirectoryというサービスにBrowseというActionを要求している。
  • ObjectIDというのが操作対象で、0だとルートを指す。
  • BrowseFlagをBrowseDirectChildrenとするとその操作対象の直下にあるリソース一覧を要求することとなる。
  • Filterは*以外の例を見なかった
  • StartIndexは大量のデータの途中から読む際につかうIndex
  • RequestedCountは取得したい数、しかしこの数取れるわけではないようだ
  • SortCriteria は空文字以外の例を見なかった

という感じ。

さて、これを踏まえてCurlのコマンドを呼び出します。

$ curl -v -H "Content-Type: text/xml; charset=\"utf-8\""  -H "SOAPAction: \"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"" --data-binary @request.xml http://192.168.1.15:55247/dms/control/ContentDirectory

ヘッダにContent-TypeSOAPActionというものを付与しないとエラーが返るようでした。

HTTPのアドレスは UPnPで見つけた http://192.168.1.15:55247/dms/ にアクセスするとそれっぽいURLが書いてありました。

さて、これを実行するとルートのリソース直下にある子供のリソースが取得できるはずです・・

*   Trying 192.168.1.15:55247...
* Connected to 192.168.1.15 (192.168.1.15) port 55247 (#0)
> POST /dms/control/ContentDirectory HTTP/1.1
> Host: 192.168.1.15:55247
> User-Agent: curl/7.73.0
> Accept: */*
> Content-Type: text/xml; charset="utf-8"
> SOAPAction: "urn:schemas-upnp-org:service:ContentDirectory:1#Browse"
> Content-Length: 899
>
* upload completely sent off: 899 out of 899 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: Linux/3.0.8 UPnP/1.0 DiXiM/4.0
< Date: Sun, 13 Feb 2022 09:03:53 GMT
< Connection: close
< EXT:
< Content-Type: text/xml; charset="utf-8"
< Transfer-Encoding: chunked
<
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:BrowseResponse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<Result>&lt;DIDL-Lite xmlns=&quot;urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/&quot; xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot;
xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot; xmlns:dlna=&quot;urn:schemas-dlna-org:metadata-1-0/&quot; xmlns:dixim=&quot;urn:schemas-digion
-com:metadata-1-0/dixim/DIDL-Lite/&quot; xmlns:microsoft=&quot;urn:schemas-microsoft-com:WMPNSS-1-0/&quot; xmlns:lamprey=&quot;http://www.lampreynet
works.com/schema/lamprey_1.0/&quot; xmlns:arib=&quot;urn:schemas-arib-or-jp:elements-1-0/&quot; xmlns:dtcp=&quot;urn:schemas-dtcp-com:metadata-1-0/&
quot; xmlns:dlpa=&quot;urn:schemas-dlpa-jp:metadata-1-0/&quot; xmlns:xsrs=&quot;urn:schemas-xsrs-org:metadata-1-0/x_srs/&quot;&gt;
&lt;container id=&quot;root/XXXXXXXXXX/&quot; parentID=&quot;0&quot; restricted=&quot;0&quot; childCount=&quot;11&quot;&gt;
&lt;dc:title&gt;USB2:大容量&lt;/dc:title&gt;
&lt;upnp:containerUpdateID&gt;3802&lt;/upnp:containerUpdateID&gt;
&lt;upnp:class&gt;object.container&lt;/upnp:class&gt;
&lt;/container&gt;&lt;container id=&quot;root/LIVE_TUNER/&quot; parentID=&quot;0&quot; restricted=&quot;1&quot; childCount=&quot;3&quot;&gt;
&lt;dc:title&gt;ライブチューナー&lt;/dc:title&gt;
&lt;upnp:class&gt;object.container&lt;/upnp:class&gt;
&lt;arib:objectType&gt;&lt;/arib:objectType&gt;
&lt;upnp:containerUpdateID&gt;350&lt;/upnp:containerUpdateID&gt;
&lt;/container&gt;&lt;/DIDL-Lite&gt;
</Result>
<NumberReturned>2</NumberReturned>
<TotalMatches>2</TotalMatches>
<UpdateID>3810</UpdateID>
</u:BrowseResponse>
</s:Body>
</s:Envelope>
* Closing connection 0

うげ、なんだこれ・・

と思いつつそれっぽい情報も含まれているので注意深く読んでみます。

どうやらこれは、XMLの中にさらに文字列としてXMLが入ってしまっているようです。

しかしよく見ると「USB2:大容量」「ライブチューナー」という文字が見えます。

この「USB2:大容量」というのは自分が外付けHDDにつけた名前なので、どうやらルートにあるリソースが出ているようです。

さらに良く見るとこの外付けHDDのIDがroot/XXXXXXXXXX/である事もわかります(よくわからないので一応マスクしています。)

次はObijectIDをこの外付けHDDのIDにして、再度同じコマンドを実行すると、今度は外付けHDD内のディレクトリ一覧を取得できました。

ディレクトリ一覧ですが、その中に「すべて」という名前のディレクトリがあり、そのIDで同じコマンドを実行するとどうも、すべての録画データを取得できるようでした。(確かに純正アプリの「テレキングリモート」もそういう挙動をします)

f:id:inajob:20220213212516p:plain

という事で、無事DLNAを使ってTVチューナで録画した番組の一覧を取得することが出来ました。

HDDの死活監視を設定する

という事で、ちょっとやりすぎた感もありますがDLNAを使って外付けHDD内の録画データの一覧を取得できました。

HDDが認識できていないときは、このデータが取得できないはずなので、これを利用して外付けHDDが認識できないときにアラートを上げることが出来るようになりました。

雑に書いたスクリプトはこんな感じ・・

まずはおおもとのシェルスクリプト。これをcronで実行します。

#!/bin/bash

cd `dirname $0`

MAX_RETRY=5
n=0
until [ $n -ge $MAX_RETRY ]
do
# ====================
n=$[$n+1]
curl -v -H "Content-Type: text/xml; charset=\"utf-8\""  -H "SOAPAction: \"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"" --data-binary @request.xml http://192.168.1.15:55247/dms/control/ContentDirectory > get.txt

python check.py && break

echo "ERROR"
sleep 10

# ====================
done

if [ $n -ge $MAX_RETRY ]; then
  echo "failed: ${@}" >&2
  # SLACKなどにHDDが認識できない旨を通知
  exit 1
else
  echo "OK"
fi

リクエストに使うXML文書(ObjectIDはマスクしています)

<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><m:Browse xmlns:m="urn:schemas-upnp-org:service:ContentDirectory:1"><ObjectID xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string">「外付けHDD内のすべてのリソースを表すID」</ObjectID><BrowseFlag xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string">BrowseDirectChildren</BrowseFlag><Filter xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string">*</Filter><StartingIndex xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="ui4">0</StartingIndex><RequestedCount xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="ui4">200</RequestedCount><SortCriteria xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string"></SortCriteria></m:Browse></SOAP-ENV:Body></SOAP-ENV:Envelope>

curlで得たXMLを解析しちゃんと番組データが入っているかを確認するpythonスクリプト

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import html
import xml.etree.ElementTree as ET

count = 0

with open("get.txt", encoding="utf8") as f:
    s = f.read()
    x = html.unescape(s)
    #print(x)
    root = ET.fromstring(x)
    print(root) # Envelope
    print(root[0]) # Body
    print(root[0][0]) # BrowseResponse
    print(root[0][0][0]) # Result
    print(root[0][0][0][0]) # DITL-Lite
    for item in root[0][0][0][0]:
        print(count, item[0].text) # title
        count = count + 1
    print("count", count)

さて、これで外付けHDDの死活監視を実現できました。

シェルスクリプトで、謎にループしているのは、初回のリクエストは失敗し、その後HDDがスピンアップして安定するとリクエストが成功するためです。

このスクリプトを仕込んでから、まだ1度もHDDの認識を失敗したことがないのでわからないですが、この定期的な外付けHDDのスピンアップのおかげで、認識しなくなる問題も発生頻度が下がっているかもしれません。

蛇足: 録画しているすべての番組を一覧する

ここまでで、表題の問題は解決しましたが、せっかく録画番組一覧が取得できるなら、何かに応用したいな、、と思って取得したデータを見ていました。

しかし、「すべて」のデータを取得しようとしても1度のリクエストでは数十件分のデータしか返却されていないことに気付きました。

どうやら、StartingIndexを取得したデータの個数ずつずらして何度もリクエストすることが必要のようです。

いわゆるWebアプリのAPIの「ページング」的なやつですね。

ここまで来たら、やってしまおうという事で、このページングをすべてたどって、録画データのタイトル一覧を出力するプログラムを書いてみました。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import html
import xml.etree.ElementTree as ET
import urllib.request
import sys

url = 'http://192.168.1.15:55247/dms/control/ContentDirectory'
headers = {
        'Content-Type': 'text/xml; charset="utf-8"',
        'SOAPAction': '"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"'
        }

def createRequestBody(objectID, startIndex):
  return '''<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <SOAP-ENV:Body>
    <m:Browse xmlns:m="urn:schemas-upnp-org:service:ContentDirectory:1">
      <ObjectID>%s</ObjectID>
      <BrowseFlag>BrowseDirectChildren</BrowseFlag>
      <Filter>*</Filter>
      <StartingIndex>%s</StartingIndex>
      <RequestedCount>0</RequestedCount>
      <SortCriteria></SortCriteria>
</m:Browse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>''' % (objectID, startIndex)

def browse(objectID, startIndex):
  reqbody = createRequestBody(objectID, startIndex)
  req = urllib.request.Request(
          url,
          reqbody.encode("utf8"),
          method="POST",
          headers = headers
          )
  count = 0
  with urllib.request.urlopen(req) as res:
      s = res.read().decode("utf8")
      x = html.unescape(s)
      root = ET.fromstring(x)
      return root

def getItems(root):
    body = root.findall('{http://schemas.xmlsoap.org/soap/envelope/}Body')[0]
    browseResponse = body.findall('{urn:schemas-upnp-org:service:ContentDirectory:1}BrowseResponse')[0]
    return browseResponse.findall('Result')[0].findall('{urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/}DIDL-Lite')[0]

root = browse(0, 0)
# find USB disk
usbID = -1
for item in getItems(root):
    if item[0].text.find('USB') != -1:
        usbID = item.attrib['id']

root = browse(usbID, 0)
# find ALL directory
allID = -1
for item in getItems(root):
    if item[0].text.find('すべて') != -1:
        allID = item.attrib['id']

# list all contents
index = 0
while True:
    root = browse(allID, index)
    body = root.findall('{http://schemas.xmlsoap.org/soap/envelope/}Body')[0]
    browseResponse = body.findall('{urn:schemas-upnp-org:service:ContentDirectory:1}BrowseResponse')[0]
    numberReturned = int(browseResponse.findall('NumberReturned')[0].text)
    totalMatches = int(browseResponse.findall('TotalMatches')[0].text)
    # print(index, numberReturned, totalMatches)

    items = getItems(root)
    for i, x in enumerate(items):
        title = x.findall('{http://purl.org/dc/elements/1.1/}title')[0]
        start = x.findall('{http://purl.org/dc/elements/1.1/}date')[0]
        print(index + i, start.text, title.text)
    index = index + numberReturned
    if index + numberReturned == totalMatches:
        break

実行すると、何度もリクエストを行い、すべての録画番組のタイトルを取得します。

f:id:inajob:20220213212235p:plain
出力したデータをgrepして特定の番組を取り出している

まだ残る謎

さて、ここまでTVチューナのDLNA機能を使って、録画番組一覧を取得してきましたが、やっていく中で以下のような疑問が出てきましたが、まだ未解決のままです。

何か情報をご存知の方は、ぜひ教えてください。

  • 「テレキングリモート」から録画番組の削除を行っても、DLNA経由での呼び出しではデータが残り続ける。一度「テレキングリモート」で録画番組の一覧を取得すると(このときものすごく遅い)、以降DLNA経由での呼び出しでもデータが正しく消えるようになる。
  • 「テレキングリモート」のUIでは視聴済みの番組にはマークがつくが、DLNAのレスポンスを見る限りそのようなデータが入っていない

DLNAでできるのかは不明ですが、以下のようなこともやってみたいです

  • 放送が終了しているのに残り続けている繰り返し予約の検知
  • 録画予約の取得
  • 録画予約の実施
  • 録画された番組と繰り返し予約エントリの紐づけの取得

まとめ

ということで、TVチューナーで外付けHDDが突然認識されなくなる問題を、外形監視により検知できるようになりました。 まだ一度も発動していないのでスクリプトにはバグがあるかもしれません。

また、副産物として、TVチューナで録画した番組のタイトル一覧を取得できるようになったので、これも何かに使ってみたいです。

例えば、「テレキングリモート」より一覧性の高い録画番組ビューアとか?(ただし再生はできない・・)

参考

DLNA周りの参考となる記事が少なかったのですが、結局以下のページを丸々真似する感じになりました。

記事を書いてくださった方に感謝します。