壊れたブルーレイレコーダーを何とかしようとしてできなかった

家族が使っていたSharpAQUOS ブルーレイディスクレコーダー BD-T1300 が壊れた、と伝えられた。HDDが壊れたっぽいメッセージがずっと表示されて、本体のHDDランプがとても強く光り続けていた。

これ、テレビチューナーが3つもついているのでこのまま捨ててしまうのはもったいない。古いパソコンから取り出したHDDが余っているので載せ換えて使えないだろうか。

面倒なので故障した機体を調べずにとりあえずインターネットに聞いてみた。HDD換装の記録がいくつも見付けられた。やってみた記事らによると、

  • HDDはAVコマンドに対応していること
  • 元のHDDをクローンしてたり、したかどうか明記してなかったり
  • HDDを交換したらサービスモードで登録する必要がある
  • サービスモードを呼び出すリモコン信号は AA 5A 8F 30 F5 01 他2つ

らしい。

AQUOSの「設定」>「初期化・更新」> 「システム動作テスト」を表示させた状態で特定のリモコン信号を受信させるとサービスモード画面が表示される、らしい。

サービスモードはサービスマンモードと書かれている場合もあった。サービスモードを呼び出すための専用リモコンがメルカリなどで販売されているようだ。

Raspberry PiArduino で赤外線リモコンの送受信

Arduinoで赤外線リモコン信号を出すのを作ったことがあった。そのときは既存のリモコン信号のon/offの時間間隔を記録して、それを再生することで信号を送信した。今度は、送る信号がバイト列として与えられている。バイト列を赤外線LEDのon/offの時間間隔に変換する必要がある。

Raspberry Pi で赤外線リモコン信号を受信する

まずは、純正リモコンの信号を受信してバイト列で表現(デコード)する。そのバイト列をリモコン信号にエンコードしてArduinoから送信する。

AQUOSリモコン GB079PA の選局 ^ ボタンを受信してVCDファイルに記録した。

vcdファイルからリモコン信号を取り出す

家電協フォーマットと呼ばれるものらしいとインターネットで既に聞いていた。 on時間とoff時間を組みにすると、1信号は0信号の2倍の長さになる。01の列を4つずつ16進で表記すると選局 ^ ボタンは 55 5A F1 0C B1 8F になった。

Arduino Uno で赤外線リモコン信号を送信する

シリアル通信でHEX文字列を送るとそれをリモコン信号として送信するプログラムを書いた。 55 5A F1 0C B1 8F を送信してみた、成功した。

サービスモードに入れなかった

AA 5A 8F 30 F5 01 を「システム動作テスト」画面で送信してみたけど反応なしだった。 他にネットで見付けた AA 5A 8F 30 F5 11、AA 5A 8F 30 F5 11、AA 5A 8F 30 F5 21、AA 5A 8F 31 F5 11、AA 5A 8F 32 F5 21 も送信してみたけどダメだった。

簡単にできるならと思ってやってみたけど、雑にやってもできなかったのであきらめた。HDDのパーティションとかの問題かもしれないし、リモコンのコマンドが違ってたのかもしれない。

そもそも、リモコンコードが書かれたやってみた記事はどれも、特定のリモコンkit向けプログラムへの入力として挙げられていた。コードの解釈を間違えたのかもしれない。

とりあえず、ここまでやってあきらめた。

WSL2でrustup docしたらWindowsのChromeが開くようにした

WSL2でディストリビューションDebian。 rustup docしてドキュメント(documentation)を読みたいがエラーメッセージが表示されるだけ。

ドキュメントをブラウザで開く

  • rustup docでは openerによって環境変数BROWSERに設定されたコマンドがHTMLファイルパスを引数にして呼ばれる
  • explorer.exeにHTMLファイルのパスを渡せばChromeで表示される
  • rustup docから渡されるファイルパスはWSL内部でのパスなので、wslpathコマンドでWindowsパスに変換する
rustup doc

rustup docはローカルのHTMLファイルをWebブラウザーで開こうとする。 rustup docはopenerを使ってHTMLファイルを開く。

openerは渡されたファイルパスまたはURLを環境に合せた方法で開く。 openerはシステムのxdg-open、なければopenerが内部で持つxdg-openスクリプトを呼ぶ。 xdg-openは環境変数BROWSERでWebブラウザーを起動するコマンドを指定できる。

HTMLファイルを開く

WindowsエクスプローラーでHTMLファイルをダブルクリックするとChromeで開くようになってた。 たぶん既定のウェブブラウザが開くように設定されてるから。 WSLからHTMLファイルパスを引数にexplorer.exeを呼べばWindowsのデスクトップ側でHTMLファイルが読み込またChromeが表示される。 ただし、explorer.exeにはWindowsのパスを渡す必要がある。

やったこと

#!/bin/sh

/mnt/c/Windows/explorer.exe $(wslpath -w "$@")

exit 0
  • export BROWSER=open-rustup-doc
その他
  • wslpath -w にURLを渡すとエラーになるので、URLを「開く」に対応するには引数が「"https://"や"http://"で始まったら」などの分岐が必要になる
  • rustup docを呼んだ場合のHTMLファイルのパスは ~/.rustup/toolchains/<toolchain name>/share/doc/rust/html/index.html
  • rustup doc --path はHTMLファイルのパスを表示できる
  • Google Cloud Shellでは、BROWSER=echoだった
  • xdg-openではスペースが含まれた環境変数BROWSERの値に対応していない
  • インストールしたままの~/.profileには、~/binディレクトリがあればPATHに含めるように書かれている

Ruby:slimにdiscordrbをインストールできた

Rubyの公式dockerイメージの一つruby:slimにDiscord botライブラリのdiscordrbをインストールしたかった。

解決

  • makeとg++パッケージをインストールする
    • C++のライブラリが足りてないっぽい
# apt update
# apt install g++ make
# gem install discordrb
  • build-essential にg++もmakeも含まれるので、build-essentialでも可
    • g++とmake (151MB)
    • build-essential (221MB)

エラー

$ docker run -it --rm ruby:slim bash
# gem install discordrb

Building native extensions. This could take a while...
ERROR: Error installing discordrb:
ERROR: Failed to build gem native extension.

current directory: /usr/local/bundle/gems/unf_ext-0.0.7.7/ext/unf_ext
/usr/local/bin/ruby -I /usr/local/lib/ruby/2.7.0 -r ./siteconf20200406-597-121o8d0.rb extconf.rb
checking for -lstdc++... extconf.rb failed
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers. Check the mkmf.log file for more details. You may
need configuration options.
Provided configuration options:
--with-opt-dir
--without-opt-dir
--with-opt-include
--without-opt-include=${opt-dir}/include
--with-opt-lib
--without-opt-lib=${opt-dir}/lib
--with-make-prog
--without-make-prog
--srcdir=.
--curdir
--ruby=/usr/local/bin/$(RUBY_BASE_NAME)
--with-static-libstdc++
--without-static-libstdc++
--with-stdc++-dir
--without-stdc++-dir
--with-stdc++-include
--without-stdc++-include=${stdc++-dir}/include
--with-stdc++-lib
--without-stdc++-lib=${stdc++-dir}/lib
--with-stdc++lib
--without-stdc++lib
/usr/local/lib/ruby/2.7.0/mkmf.rb:471:in try_do': The compiler failed to generate an executable file. (RuntimeError)<br> <br> You have to install development tools first.<br> from /usr/local/lib/ruby/2.7.0/mkmf.rb:564:intry_link0'
from /usr/local/lib/ruby/2.7.0/mkmf.rb:582:in try_link'<br> from /usr/local/lib/ruby/2.7.0/mkmf.rb:801:intry_func'
from /usr/local/lib/ruby/2.7.0/mkmf.rb:1029:in block in have_library'<br> from /usr/local/lib/ruby/2.7.0/mkmf.rb:971:inblock in checking_for'
from /usr/local/lib/ruby/2.7.0/mkmf.rb:361:in block (2 levels) in postpone'<br> from /usr/local/lib/ruby/2.7.0/mkmf.rb:331:inopen'
from /usr/local/lib/ruby/2.7.0/mkmf.rb:361:in block in postpone'<br> from /usr/local/lib/ruby/2.7.0/mkmf.rb:331:inopen'
from /usr/local/lib/ruby/2.7.0/mkmf.rb:357:in postpone'<br> from /usr/local/lib/ruby/2.7.0/mkmf.rb:970:inchecking_for'
from /usr/local/lib/ruby/2.7.0/mkmf.rb:1024:in have_library'<br> from extconf.rb:6:in

'

To see why this extension failed to compile, please check the mkmf.log which can be found here:

/usr/local/bundle/extensions/x86_64-linux/2.7.0/unf_ext-0.0.7.7/mkmf.log

extconf failed, exit code 1

em files will remain installed in /usr/local/bundle/gems/unf_ext-0.0.7.7 for inspection.
Results logged to /usr/local/bundle/extensions/x86_64-linux/2.7.0/unf_ext-0.0.7.7/gem_make.out

logファイル

"gcc -o conftest -I/usr/local/include/ruby-2.7.0/x86_64-linux -I/usr/local/include/ruby-2.7.0/ruby/backward -I/usr/local/include/ruby-2.7.0 -I.    -O3 -ggdb3 -Wall -Wextra -Wdeprecated-declarations -Wduplicated-cond -Wimplicit-function-declaration -Wimplicit-int -Wmisleading-indentation -Wpointer-arith -Wwrite-strings -Wimplicit-fallthrough=0 -Wmissing-noreturn -Wno-cast-function-type -Wno-constant-logical-operand -Wno-long-long -Wno-missing-field-initializers -Wno-overlength-strings -Wno-packed-bitfield-compat -Wno-parentheses-equality -Wno-self-assign -Wno-tautological-compare -Wno-unused-parameter -Wno-unused-value -Wsuggest-attribute=format -Wsuggest-attribute=noreturn -Wunused-variable  -fPIC conftest.c  -L. -L/usr/local/lib -Wl,-rpath,/usr/local/lib -L. -fstack-protector-strong -rdynamic -Wl,-export-dynamic     -Wl,-rpath,/usr/local/lib -L/usr/local/lib -lruby  -lm   -lc"
 checked program was:
/* begin */
1: #include "ruby.h"
2:
3: int main(int argc, char **argv)
4: {
5:   return !!argv[argc];
6: }
/* end */

Azure SignalR Service の REST API では URL区切り文字を含んだユーザーIDがあて先のメッセージを送れない

Azure SignalR Service は serverless モードにして Azure Functions と使うことができる。その場合には Azure Functions から REST API1 で Azure SignalR Service へ メッセージを発行する。 現時点(2019/12/29)の Azure SignalR Service では URL区切り文字を含んだユーザーIDが有効である。しかし、REST API から URL区切り文字を含んだユーザーIDをあて先としたメッセージ送信ができない。 URL区切り文字を含んだユーザーIDとは例えば、"user/1"のような文字列。

原因は、Azure SignalR Service が REST API のパラメーターとして送られた URLエンコードされたユーザーIDをデコードせずにあて先ユーザーIDとするから。

ユーザーIDが user/1 の場合、REST API へ URL {prefix}/user%2F1 としてリクエストするとメッセージがユーザーIDが user%2F1 のクライアントへメッセージが送信される。 URLエンコードをせずに REST API へ URL {prefix}/user/1 としてリクエストすると、URL が API のルートに一致せずに 404 Not Found レスポンスが返る。

なお、この REST API では リクエスト URL は ヘッダーで送信する JWT の aud claim と一致する必要がある。ユーザーIDを URLエンコードした場合であっても、aud claim はエンコード前の文字列と一致して有効な API リクエストとして扱われる。

Azure SignalR Service へは、REST API 以外に websocket で接続してメッセージ送信することができる。その場合にはユーザーID に / スラッシュなどの URL区切り文字が含まれていてもメッセージのあて先にできる。

接続したクライアントのユーザーID は Hub の接続イベントで確認できる。Azure SignalR Service では EventGrid を通して Hub への connected イベントが購読できる。

ためしたこと

Azure SignalR Service SDK のコードを変更して Azure SignalR Service へ接続して試してみた。Microsoft.Azure.SignalR.Management.ServiceTransportType.Transient を指定した ServiceManager が REST API を使用する。API への HTTP リクエスト URL を組み立てるコードを変更して、ユーザーID を URLエンコードして実行した。

public RestApiEndpoint GetSendToUserEndpoint(string userId, TimeSpan? lifetime = null)
{
    var path_token = $"/users/{userId}";

    // user/1 -> user%2F1
    var path_url = $"/users/{Uri.EscapeDataString(userId)}";
    
    var token = _restApiAccessTokenGenerator.Generate($"{_audiencePrefix}{path_token}", lifetime);
    return new RestApiEndpoint($"{_requestPrefix}{path_url}", token);
}

https://github.com/Azure/azure-signalr/blob/c0875fcb8c90befb8dc07d69a826cb66219127de/src/Microsoft.Azure.SignalR.Management/RestApiProvider.cs#L44-L47

  • "user/1"だとする
  • Connected イベントでは、"user/1"
  • access_token.id では、 "{prefix}/user/1"
  • rest api
    • url = {prefix}/user/1 , token.aud = "{prefix}/user/1"
    • url = {prefix}/user/1 , token.aud = "{prefix}/user%2F1"
    • url = {prefix}/user%2F1 , token.aud = "{prefix}/user%2F1"
      • client "user%2F1" でメッセージを受信した
    • url = {prefix}/user%2F1 , token.aud = "{prefix}/user/1"
      • client "user%2F1" でメッセージを受信した

OBS Studio の browser source に スマホのカメラ映像を出す

OBSのブラウザソースは、Chromium Embedded Framework (CEF)で出来ているそうだ。ローカルファイルのHTMLをロードすることもできる。それを使ってAndroidスマホのカメラからの映像を表示しようと思った。WebRTCだ。

OBSのブラウザソースで表示したHTMLは、ボタンをクリックしたりテキスト入力したりできるがやりにくい。ページが読み込まれたら自動でWebRTCの接続をするようにした。

github.com

Pythonでこの用途限定のシグナリングサーバーを書いた。

  • GET /offer スマホ側がofferポーリングする
  • POST /offer OBS側がofferをポストする
  • GET /answer スマホ側がanswerをポーリングする
  • POST /answer OBS側がanswerをポストする

スマホ側もOBS側もローカルファイルからHTMLを読み込むので origin が "null"になる。CORSヘッダーで許可する必要がある。"null"の場合は、"*"ワイルドカードでの許可はできない。

from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    offer = None
    answer = None

    def send_corsHeaders(self):
        self.send_header("Access-Control-Allow-Origin", "null")
        self.send_header("Access-Control-Allow-Method", "POST, GET")
        self.send_header("Access-Control-Allow-Headers", "*")

    def do_OPTIONS(self):
        self.send_response(200)
        self.send_corsHeaders()
        self.end_headers()

    def do_GET(self):

        if self.path == '/offer' and self.offer:
            size, data = self.offer
        elif self.path == '/answer' and self.answer:
            size, data = self.answer
        else:
            self.send_response(204)
            self.send_corsHeaders()
            self.end_headers()
            return

        self.send_response(200)
        self.send_corsHeaders()
        self.send_header("Content-Type","application/json")
        self.send_header("Content-Length", size)
        self.end_headers()

        self.wfile.write(data)

    def do_POST(self):
        
        size = int(self.headers["content-length"])
        data = self.rfile.read(size)

        if self.path == '/offer':
            self.__class__.offer = [size, data]
        elif self.path == '/answer':
            self.__class__.answer = [size, data]
        else:
            self.send_response(204)
            self.send_corsHeaders()
            self.end_headers()
            return
            
        self.send_response(200)
        self.send_corsHeaders()
        self.end_headers()


HTTPServer.allow_reuse_address = True
HTTPServer.timeout = None
with HTTPServer(("0.0.0.0", 8080), Handler) as server:
    server.serve_forever()

スマホ側のJavaScriptコード。OBS側もほぼ一緒。Androidの保存場所はダウンロードフォルダ(Download)にした、他の場所は許可のこととかわからんかったから。Chromeからは file:///sdcard/Download/camera.htmlでアクセスできる。file:///はスラッシュ3つ。開発中はパソコンにUSBで繋いでChromeのdevtoolsのremote devicesから開いて操作した。

SDPのメッセージには改行が含まれているので、JSON.stringifyしてもJSON.parseでエラーになってしまう。stringify、parseメソッドの第2引数にreplacer, reviver関数を渡して改行を処理した。

LAN内で接続するだけなので、RTCPeerConnectionにTURNサーバーを設定しない。それでもiceGatheringのために関数を抜ける必要があるので、awaitでiceGatheringStateがcompleteになるのを待つ。

const pc = new RTCPeerConnection();

const mediaConstraints = {
  video: { facingMode: "user", width: 200, height: 200 }
};

const url_offer = "http://192.168.1.2:8080/offer";
const url_answer = "http://192.168.1.2:8080/answer";

const sleep = ms => new Promise(r => setTimeout(r, ms));

const replacer = (_, v) => {
  if (typeof v == "string") {
    return v.replace(/\r\n/g, "\n");
  }

  return v;
};

const reviver = (_, v) => {
  if (typeof v == "string") {
    return v.replace(/\n/g, "\r\n");
  }

  return v;
};

/* main */
(async () => {
  // attach stream
  const ms = await navigator.mediaDevices.getUserMedia(mediaConstraints);
  pc.addTrack(ms.getTracks()[0], ms);

  // build SDP
  pc.setLocalDescription(await pc.createOffer());

  const msg = await new Promise(rslv => {
    const f = () => {
      if (pc.iceGatheringState == "complete") {
        pc.removeEventListener("icegatheringstatechange", f);
        rslv(pc.localDescription);
      }
    };

    pc.addEventListener("icegatheringstatechange", f);
  });

  // send SDP
  let res = await fetch(url_offer, {
    mode: "cors",
    method: "POST",
    body: JSON.stringify(msg, replacer),
    headers: {
      "Content-Type": "application/json"
    }
  });

  // polling answer
  let ans;
  while (true) {
    const res = await fetch(url_answer, {
      mode: "cors",
      method: "GET"
    });

    if (res.status === 200) {
      ans = await res.text().then(txt => JSON.parse(txt, reviver));
      break;
    }

    await sleep(1000);
  }

  pc.setRemoteDescription(ans);
})();

その他

Chromeのビデオの自動再生は、mutedの場合だけ有効。1

fetch api の response.json()に reviverがない理由はBody.jsonがネイティブで実装されているから2

Pythonでwindows apiを呼ぶ

呼ぶ = call

ウィンドウサイズをどうかしたかった。

Jupyter と Pythonwindows api 触れたら楽だね。pywin32と足りない分は自分でctypes使えばできる。

miniconda をインストールしたらもうpywin32も入ってた。

ウィンドウのサイズはスナップしたときとそうでないときで違うらしい、よくわからん。

  • DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS )
  • GetWindowRect()
  • GetClientRect()

どれもそのままではMoveWindowで使える値は取れないみたいだった。

gist.github.com