M5StickCでWeb Radio→S/PDIF

内蔵 LED で S/PDIF 出力

M5StickC を使って、Web Radio を受信し、S/PDIF (TOSLINKとも) で出力するデバイスを作りました。AVアンプに接続しておけば、電源を入れるだけですぐに音楽が楽しめて便利です。放送中の曲名も表示されます。

見ての通り、出力は M5StickC 本体に内蔵されている赤色LEDです。

LEDは少々奥まったところにあるため、軽くドリルでケースを丸く削って穴を広げました。そこにS/PDIFケーブルの端子の先端を軽く差し込んでLEDに密着させることで十分に信号を送ることができます。位置が決まったら抜けないようラフにテープで止めておけばOK(?)。

丸型(Mini-Toslink) ケーブルにも対応(??)。


実装

まず対応ハードウェアについて。M5StickC シリーズの最新は M5StickC Plus2 ですが、これは今回の用途には不向きです。というのも、赤色LEDの位置が上部に移動しているほか、赤外線LEDが同じピンに割り当てられており、しかも赤外線LEDは100mA程度のハイパワー駆動が可能なように、バッテリー電源+トランジスタでドライブする仕様に変更されています。瞬間的にパワーが欲しい赤外線リモコン制作にはとても理想的ですが、これをS/PDIFで常時点灯させるとめちゃくちゃ発熱してしまう恐れがあります。もしやるなら赤色LEDを外付けしないとダメでしょう。*1

1つ前の M5StickC Plus なら大丈夫だと思います。今回は初期版を使用しています。
ちなみに、Plus/初期型のLEDはLOWで点灯と負論理になっていますが、S/PDIFはON↔︎OFFの切り替えタイミングで符号化する仕組みなので、どっちでも平気です。

M5StickC Plus2 のLED回路

M5StickC(初期型/Plus)のLED回路

加えて、初期型とPlusの ESP32-PICO-D4 のI2Sではより高精度なクロックを生成できる APLL が利用できます。


ソフトウェアは Arduino IDE + Arduino-ESP32 v3.3.4 で開発を行いました。

ライブラリとして最初は ESP8266Audio を試したのですが、うまく S/PDIF の出力ができませんでした。
i2s_channel_enable() を呼び出し忘れている箇所があったのを修正 したり、APLL でなくデフォルトのクロックに差し替えたりしたところ、S/PDIF信号自体は出るようになったのですが、なぜかレシーバーが認識してくれず。AudioOutputSPDIF は experimental 扱いなので何か問題があるのかもしれません。

別のライブラリとして arduino-audio-tools を選択。 Test Output to SPDIF example を試してみたところ、こちらはピン番号を10に変えるだけで問題なくsin波がS/PDIF信号として出力されることを確認できたので、これを採用しました。
このライブラリは使っているマイコンSDKを自動的に識別して最適な実装を選択するようになっており、非常に簡単に試せるのが素晴らしいですね。

MP3 のデコードのためには、このライブラリに加えて arduino-libhelix 等のデコーダーも libraries に加えておく必要があります。

とはいえ多少苦労した点も。Web Radio の example では SSID / password を ICYStream クラスに渡す形で書かれていますが、この方法でも動作はするものの、接続に失敗したり、接続に成功してもすぐに途中で途切れたりと、非常に不安定になってしまいました。
このライブラリは Arduino 以外でも利用できるように ESP32 用の WiFi 操作を自前で実装しているのですが、それと何か相性が悪いのかもしれません。ArduinoWiFi 実装ではそうした現象に出会ったことはなかったため、 以下のように Aruidno の WiFiClient インスタンスを使って初期化し、バッファも大きめにしたところ、安定させることができました。

WiFiClient client;
ICYStreamBuffered urlStream(client, 2048*32);

さらに、ICYStream だと数分に一度くらいの頻度で一瞬音が途切れることがあったため、 ICYStreamBuffered というバッファリング機能付きのクラスに差し替えることで、とても安定した動作となりました。

あとは画面出力や M5 ボタンを押した時に別の Web Radio に切り替える機能をつけて完成。ソースコードは以下です。
Web Radio の URL は適当に集めたものをいくつか登録してありますが、AAC など、MP3 + http (ICY) 以外のストリームには対応していない点に注意が必要です。おそらく AAC はPSRAMを搭載していない初期型 M5Stick ではメモリ不足でデコードできないと思われます。

追記
一部のWeb Radioでは曲の間にストリームが10秒止まるものがあり,その際にWDTが作動して再起動してしまっていたので、ライブラリの方に以下の修正を入れました。
Avoid task WDT invoked when the stream is temporarily stopped · NeoCat/arduino-audio-tools@51e531f · GitHub
他に接続エラー時にすぐにハンドリングできるよう URL_CLIENT_TIMEOUT / URL_HANDSHAKE_TIMEOUT を10秒程度に短くしたりしています。


ソースコードは以下:

*1:まあ、初期型・PlusのLEDも、抵抗なしで電源・ピン直結かよ!というツッコミどころはあるのですが。

続きを読む

WinRing0 が Microsoft Defender に引っかかった

以前の記事で、CPU温度計を作るために Libre Hardware Monitor を使用していることを紹介しました。
neocat.hatenablog.com

最近、このソフトがハードウェアアクセスのために使用している WinRing0 が脆弱性が見つかり、Microsoft Defender によって隔離されるようになりました。
経緯などが以下の記事で説明されています。
Windowsの「Microsoft Defender」が一部アプリの起動をブロックする可能性 「Winring0ドライバ」の脆弱性が原因(リスクを受容すれば回避策あり) - ITmedia PC USER

警告の無視が紹介されていますが、Libre Hardware Monitor でこの WinRing0 から PawnIO に置き換えるPull Requestが出されていましたので、これを使用することで問題を解消できます。

github.com

このPull Requestを取り込んだバージョンはリリースされていませんが、GitHub Actions によるCI上でビルドされた Artifact をダウンロード可能になっています。
GitHub へのログインが必要)
Swap WinRing0 to PawnIO (#1857) · LibreHardwareMonitor/LibreHardwareMonitor@eb5e1a2 · GitHub
あるいは、せっかくなので最新のコミットのビルド結果を試しても良いでしょう。

PicoCalc/MMBasicでリーマンゼータ関数をプロットする

PicoCalc を購入しました。

PicoCalc

PicoCalc は Raspberry Pi Pico シリーズを搭載した 320x320 LCD とキーボードを備えたポケットサイズのコンピューターです。
非常にクールな見た目が良いですね。

Kit ($79) を注文して、2ヶ月ちょっとくらいで届きました。非常に良くできた作りで、各パーツをはめ込んでいけば10分くらいで完成できます。

Kit に付属している Pico 1 H には標準で MMBasic が動作する PicoMite という開発環境がファームウェアとして書き込まれており、付属のSDカードから Basic のサンプルコードを読み込んで試すことができます。
ボード上のPSRAMがAドライブ、SDカードはBドライブとなっているので、

B:
files  (ファイル一覧を表示)
run "lorenz

と入力すれば実行されます。これはローレンツ方程式のカオス解を描画するプログラムで、最初の写真のような図形が描画されます。
F4 (EDITコマンド) を押すとプログラムコードを画面上でスクロールしてみたり編集も可能です。

MMBasicはインタプリタなので処理は遅いですが、それがまたノスタルジックでいい味を出しているように思います。
Basic はちょっと…ということなら MicroPython などのファームウェアもあるし、速度が欲しければ Arduino (C++) でもプログラミングは可能です。NES エミュレータなんかも付属していました。

リーマンゼータ関数のプロット

さて、最近『数学ガールリーマン予想』を読みました。

その中で登場人物のリサが、リーマンゼータ関数  \zeta(\frac{1}{2} + ti) t を動かした時の値を変化を複素平面上にプロットするということをやっていました。

これを自分でも見てみたいと思い、PicoCalcでも描画してみることにしました。
といっても、MMBasic は複素数計算にも対応していませんし、リーマンゼータ関数の値を級数計算で求めるのはかなりの計算量になるので、グラフが見たいだけなら決して向いている環境とは言えないのですが、果たしてどのくらいの速度で動くかにも興味があってのあえてのチャレンジです。

ζ関数の計算方法は以下の記事を参考にさせていただきました。
リーマンのゼータ関数で遊び倒そう (Ruby編) - tsujimotterのノートブック

比較的効率的に計算できるとされている下記の級数表示を使っています。
 \displaystyle \zeta(s) = \frac{1}{1 - 2^{\,1-s}} \sum_{m=1}^{\infty} \frac{1}{2^m} \sum_{j=1}^{m} (-1)^{\,j-1} \binom{m-1}{j-1} \frac{1}{j^s}

t = 0 〜 50 を 0.1刻みで描画した結果はこちら。単色だと動きが分かりにくいので t の変化をグラデーションカラーにしてみました。

いい感じです。
実行速度はマイコンPico 2 WH にアップグレードし、クロックスピードを OPTION CPUSPEED 250000000 で 250MHzにした状態でも8分もかかりました。めちゃくちゃ遅いけど複雑な動きをするので不思議と描画過程を見ていても飽きないです。
精度を犠牲にして LOWER_THRESHOLD を大きくすれば多少速くなりますが、複雑な収束の仕方をするのでやりすぎると局所的にひしゃげたりしてしまいます。
描画に使ったプログラムは末尾に置いておきますが、かなり生成AIで補完して楽をしています。
参考にした記事中のrubyコードでは nCm をキャッシュしたりして高速化していましたが、メモリが確保できなかったので普通に毎回計算しています。

描画結果は zeta.bmp というファイルに保存されるので、

load image "zeta

で再表示させることができます。

続きを読む

tmux で 現在の pane をツリーで選んだ window に移動する

tmux で誤って (prefix) ! (break-pane) を押すと、現在アクティブな pane が新しい window に移動されてしまいます。これを元の window に戻したいとき、実は意外と簡単ではありません*1
それ以外にも window 間の pane を整理したいことがあります。pane を移動するには

(prefix) :join-pane -t 0.0

等のようにコマンドを実行する必要があり、window 指定方法を含めてちょっと覚えにくく面倒です。(0.0 は window 0 のデフォルトの pane という意味)


そこで以下のショートカットを .tmux.conf で設定しておくと、(prefix) j を押して現在のpaneを移動したい先のwindowをツリー表示から選ぶだけで良くなり便利です。

# Move pane to specified window
bind-key j choose-tree -w "join-pane -t '%%'"
操作例


ちなみに一時的に現在の pane を大きく表示したいだけであれば、(prefix) z (Zoom the active pane) が使えます。これは単にもう一度押すだけで元に戻ります。

*1:あまりbreak-paneを使わないなら無効化してしまっても良いのですが

ガイガーカウンター・γ線スペクトロメーターを試してみる

Amazonガイガーカウンターのキットが売られていたので購入してみました。

ガイガーカウンター: https://amzn.to/3CVRkr6
https://m.media-amazon.com/images/I/61RhbJXdzgL._SL1500_.jpg


比較用として、ボケットサイズのγ線スペクトロメーター Radiacode 103 と比べてみます。
これは Csl シンチレーターを使った高感度なガンマ線検出機を搭載しています。また、スマートフォンBluetoothで接続して、アプリで計測値を記録したりγ線のエネルギースペクトルを分析、すなわちピークから放射元素を調べたりバックグラウンドから差分を取ったりといったことができます。ちなみに RadiaCode はキプロスの会社だそうです。

Radiacode 103:
www.radiacode.comhttps://d2j6dbq0eux0bg.cloudfront.net/images/94284268/3959291889.png

放射線源としては、ウランガラスのおはじきと,モズナ石(モズナイト)の鉱石サンプルを試してみました。モズナ石は微量のウランやトリウムを含んでいます。

放射線を検知するとカチカチと音が鳴る*1のですが、明らかに Radiacode 103 の方が敏感なようです。

まず、バックグラウンド(測った環境の自然状態)の計測値 0.05μSv でした。
ガイガーカウンターの方はβ線γ線の両方に反応し、ウランガラスをすぐそばに置くと 0.5μSv、モズナ石では 3μSv くらいの数値が出ました。
Radiacode 103 はβ線には反応せずγ線を検出しますが、ウランガラスの方には反応は見られず、モズナ石の方には 2〜3μSv くらいの数値が出ました。
ウランガラスはβ線は出るもののγ線はあまり出さないようですね。

Radiacode 103 でモズナ石のγ線スペクトルを取ってみるとこんな感じでした。アプリ上でスペクトルをタップすると該当する放射性元素のピーク位置が表示されるようになっており、Tl-208 のものが目立っていそうです。これは Th-232 系列 (トリウム系列) だよという情報も表示されています。

なお、動画では警告音が鳴りまくっていて一見危険そうにも見えますが、今回使ったような線源はごく弱いもので、実際少しでも計測器から離すと計測値は急激に下がり(距離の2乗に反比例)、すぐに自然に存在するバックグラウンドの放射線と区別がつかないレベルになる程度のものです。したがって、特に取り扱いに気を使う必要はありません。まあ年中肌身離さず身につけ続けるとか、食べたりとかはしないほうがいいと思いますが 😅。

*1:ちなみにどちらもマイコンで検出してスピーカーを鳴らしているので、いわゆる昔のガイガー管の音としてイメージされるものと言うわけではありません

macOS 15 Sequoia で全ての通知を閉じるスクリプト

以前、以下の記事で全ての通知センターの通知パネルを閉じるAppleSciptを書きました。
neocat.hatenablog.com
これは macOS 14 でもそのまま動作していたのですが、 macOS 15 ではウィンドウの構造が変わったようでうまく動作しなくなってしまったため、書き直しました。

以前は何度かループし直さないとうまく閉じられないパネルが出てきていたのですが、今回はそういうことはない模様。
ただ、パネルが最後の1つになるとグループがなくなるようで、最後に対象を親に変えて performClose を呼び直すということをやっています。。

on performClose(theWindow)
	tell application "System Events"
		tell theWindow
			-- log (description of actions) as list
			try
				repeat with theAction in actions
					set desc to description of theAction
					if desc is "閉じる" or desc is "Close" then
						perform theAction
						exit repeat
					end if
				end repeat
			end try
		end tell
	end tell
end performClose

activate application "NotificationCenter"
tell application "System Events"
	tell process "NotificationCenter"
		if not (exists window "Notification Center") then
			return
		end if
		set theGroup to group 1 of scroll area 1 of group 1 of group 1 of window "Notification Center"
		tell theGroup
			set theWindows to every UI element as list
			set n to (count theWindows)
			repeat with i from n to 1 by -1
				tell me to performClose(item i of theWindows)
			end repeat
			tell me to performClose(theGroup)
		end tell
	end tell
end tell

M5UnifiedでLCDに「℃」を表示する方法

M5Unified を使って M5StickC のLCDに温度を表示しようと思った際に、単位の ℃ を描画する方法の備忘録です。

fonts::Font4 など、widtbl_f16 を使っているフォントでは、 ` (GRAVE・バッククォート)を °(DEGREE・度)に置き換える define マクロが有効になっています。
M5GFX/src/lgfx/Fonts/Font16.h at master · m5stack/M5GFX · GitHub

これを使って、

M5.Lcd.setFont(&fonts::Font4);
M5.Lcd.print("`C");

のように表示すれば、「℃」を描画できます。

下記はENV Hatから取得した温度・湿度・気圧を30秒ごとにLCDに表示しています。(なるべくセンサが温まらないよう、3秒表示してスリープに入っています。)
なお古い M5StickC ENV Hat を使っているので、温湿度センサはDHT12です。

#include <M5Unified.h>
#include <DHT12.h>
#include <Adafruit_BMP280.h>

DHT12 dht12;
Adafruit_BMP280 bme;

void setup() {
  Serial.begin(115200);
  Serial.println("Start ENV HAT");
  // センサの準備ができるまで待機
  M5.Power.lightSleep(2000000LL);

  Wire.begin(0, 26);
  bme.begin(0x76);

  float temp = dht12.readTemperature();
  float hum = dht12.readHumidity();
  float prsr = bme.readPressure() / 100;

  Serial.println(String("temp: ") + temp + " hum: " + hum + " prsr: " + prsr);

  M5.begin();
  M5.Lcd.setBrightness(80);
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);

  M5.Lcd.setCursor(10, 10, &fonts::Font8);
  M5.Lcd.printf("%.1f", temp);
  M5.Lcd.setFont(&fonts::Font4);
  M5.Lcd.print("`C");

  M5.Lcd.setCursor(10, 110);
  M5.Lcd.print(String(int(hum)));
  M5.Lcd.print("%   ");

  M5.Lcd.print(String(prsr));
  M5.Lcd.print("hPa");
}

void loop() {
  delay(3000);
  M5.Power.deepSleep(27000000LL);
}