Arduino MKR WAN 1310 で Meshtastic メッセージを送信する

Meshtastic について

Meshtastic は LoRa を使ってインターネットに依存せずにメッセージをやり取りできるメッシュネットワークを作る、オープンソースのオフグリッド通信プロジェクトです。低価格なデバイスで、資格等不要で数km以上の長距離通信が可能な点が魅力です。
日本では狭さと高いモバイルの普及率、電波法の規制等から、この手のものは全然流行らなくて寂しいところですが。。M5 から Unit C6L が発売され、技適も通っていることから、手軽に使えるようになりました。
M5Stack C6L Meshtasticユニット(SX1262、ESP32-C6)www.switch-science.com

スマートフォンアプリからBluetooth接続してメッセージの送信を行います。受信時は本体からブザーで知らせてくれます。

海外だとキーボード付きのT-Deck Plus等もよく使われていますが、例によって技適が課題…
LilyGo T-Deck Plus

Arduino (MKR WAN 1310) からメッセージを送信できる?

Meshtastic firmware 自体にもセンサーを読み取ってデータ送信する機能はあるのですが、ESP32よりももっと低消費電力なノードから一方的に送信したいといった用途だと、同じく LoRa 等を積んだ Arduino MKR 等を使いたくなるかもしれません。
まあ別に Meshtastic じゃなくてもいいじゃんという気もしますが、同じ LoRa なんだから送信くらいできるはずという理屈でやってみました。

Arduino MKR WAN 1310

結論から言えばすでに meshtastic-lite という Meshtastic パケットを解析・構築するヘッダーオンリーライブラリを作っている人がおり、これを使ってパケットを組み立て、MKR WAN 用の LoRa 送信ライブラリで送信することで可能でした。

ただしソフトウェアによるAES暗号化にロジックにちょっとしたバグがあり、これを修正しないと12Bより長い文字列が文字化けしたり無視される問題を見つけたので、これを修正しています。
fix: correct AES-CTR counter increment direction in software fallback by NeoCat · Pull Request #1 · jstockdale/meshtastic-lite · GitHub

ソースコードは以下のような感じ。この例では、JPリージョンのデフォルトのチャンネル(LongFast)に、指定したテキストを30秒ごとに送信し続けます。
特定のプライベートチャンネル等も指定すれば送信可能なはず。
なお通信ノードIDはデバイスに書き込まれたシリアル番号から適当にXORで生成しています。

続きを読む

迷惑メールをローカルLLMに判別させてみた

迷惑メールが増えているとのこと。前からな気もしますが、ドメインが明らかに偽装されたメールがフィルタをすり抜けてきます。*1
cf. 迷惑メールが爆増中。Yahoo!メールでも対策へ | マイナビニュース

それならというわけでMac上で動くローカルLLMに判別させてみることにしました。*2。人間なら送信元(ドメイン)と件名を見れば判別可能なので、高速化・複雑化抑止のため、入力はこれだけとします。


モデルは Gemma-4-E4B を使用しました。(最初 1-bit Bonsai を試しましたが日本に関する知識が乏しすぎてプロンプトの工夫レベルではどうにもならなさそうでした。)

実行には llama.cpp を利用。セットアップは以下の手順に従うだけです。
unsloth.ai

以下コマンドでサーバーを起動しておきます。高速化のため reasoning はオフにして代わりにプロンプトで根拠を述べさせています(現在の説明によると --chat-template-kwargs '{"enable_thinking":false}' とすべきかもです)。後で述べますが複数回実行させており、その際に観点が少し変わることがあるので temperature は少し高めを指定しました。

export LLAMA_CACHE="unsloth/gemma-4-E4B-it-GGUF"
./build/bin/llama-server -hf unsloth/gemma-4-E4B-it-GGUF:Q8_0 --temp 0.5 --top-p 0.95 --top-k 64 --reasoning off &

evaluate_mail_sender.sh として、送信元とタイトルを引数に渡すと OK/BAD のどちらかを結果として返すコマンドを用意します。自信がないのか回答がブレることもあるため、最大3回実行して多数決を取っています。判断の根拠はログファイルに記録されます。

#!/bin/zsh
set -e

prompt='次のメールで、FROM の名称("名前 <...@...>"の"名前"部分)もしくはSubjectに含まれる送信元名称と、FROMのドメインを比較して、全く関係ない送信元を偽装したメールかを判定して。特にFROMに記述された名称とドメインの不一致は問題のある可能性が高い。Subjectは「【送信元】〜」のように名称であることを強調している場合には重視する。名称かが曖昧な場合や、確実でない場合は偽装だと判断してはならない。根拠を述べた後、最後の行に、送信元偽装の可能性が高い場合には「BAD」と、問題ない場合・不確かな場合は「OK」とプレインテキストで出力して。'

target="From: $from / Subject: \"$subj\""
bad=0
ok=0
for i in {{1..3}}; do
    result=$(curl -fs http://127.0.0.1:8080/v1/chat/completions -H "Content-Type: application/json" -d '{"messages":[{"role":"system","content":"'"${prompt//\"/\\\"}"'"},{"role":"user","content":"'"${target//\"/\\\"}"'"}],"temperature":0.5}' | jq -r '.choices[].message.content')
    result_str=$(tail -1 <<<"$result")
    echo "$target\n$result\n" >>/tmp/llm-result.log

    grep -q BAD <<<"$result_str" && bad=$(($bad+1))
    if [[ "$bad" -eq 2 ]]; then
        echo BAD
        exit
    fi
    grep -q OK <<<"$result_str" && ok=$(($ok+1))
    if [[ "$ok" -eq 2 ]]; then
        echo OK
        exit
    fi
done

あとはこれを使ってメールのフィルタリングをする設定をします。なお、メールアプリとしては Mac の Mail.app で動作させることにします。

まずはスクリプトエディタで次の AppleScript を作成し、 ~/Library/Application\ Scripts/com.apple.mail/ に eval-with-llm.scpt として保存します(形式はスクリプト)。
これは受信メールに対してルール評価時に先ほどのコマンドを使って結果が BAD なら迷惑メールに移動させるものです。
以前の Mail.app ではもっと簡単に動いていたんですが、どこかのバージョンから Mail.app の AppleScript フィルタ機能がバグってしまっているらしく、perform mail action に正しい受信メールが渡ってきません。。。仕方がないので、適当に少し待ってから自分自身を run し直し、INBOX内の全メールを調べて、未チェックのもの(チェック済みの問題ないメールには背景色をつけて区別)に対してフィルタしています。
色々テキスト置換とかしているのは実行コマンド引数をサニタイズしているためです。

using terms from application "Mail"
	on perform mail action with messages theMessages for rule theRule
		try
			-- 不正なメッセージ一覧が渡されてしまうため、少し待って非同期でINBOX内のメールに対して実行
			-- tell me to evaluate(theMessages)
			do shell script "zsh -c 'sleep 3; osascript ~/Library/Containers/com.apple.mail/Data/Library/Application\\ Scripts/com.apple.mail/eval-with-llm.scpt' &"
		on error e number n
			display dialog "eval-with-llm Error: " & e & " (" & n & ")" buttons "OK" default button 1
		end try
	end perform mail action with messages

	on run
		tell application "Mail"
			repeat with theAccount in every account
				try
					try -- INBOXを持たないアカウントは無視
						set theMessages to messages of mailbox "INBOX" of theAccount
					end try
					my evaluate(theMessages)
				on error e number n
					display dialog "eval-with-llm Error: " & e & " (" & n & ")" buttons "OK" default button 1
				end try
			end repeat
		end tell
	end run

	on evaluate(message)
		tell application "Mail"
			repeat with eachMessage in theMessages
				if background color of eachMessage is none then
					set theSubject to subject of eachMessage
					set theSender to sender of eachMessage
					set theSubject to my replaceText(theSubject, return, " ")
					set theSender to my replaceText(theSender, return, " ")
					
					set theResult to (do shell script "~/bin/evaluate_mail_sender.sh " & (quoted form of theSender) & " " & (quoted form of theSubject))
					
					if theResult contains "BAD" then
						move eachMessage to junk mailbox
					else
						set background color of eachMessage to blue
					end if
				end if
			end repeat
		end tell
	end evaluate
	
	on replaceText(target, a, b)
		set oldDelimiters to AppleScript's text item delimiters
		set AppleScript's text item delimiters to a
		set target to text items of target
		set AppleScript's text item delimiters to b
		set target to target as string
		set AppleScript's text item delimiters to oldDelimiters
		return target
	end replaceText
end using terms from

Mac の Mail.app で設定から 「ルール > ルールを追加」 ですべてのメッセージに対して AppleScript を実行を選び、先ほどのスクリプト名を選ぶことで、受信時に実行されるようになります。


実行時間は1回につき5秒弱 (多数決のため計10-15秒) くらいのようです。精度としてはそこそこの様子。多数決させてもたまに素通りすることもあります。まあ "強い確信がないならOKに倒せ" と指示していますからそんなものでしょう。


なお今多数来ているメールは大体 'Content-Type: text/html; charset="GBK"' などというヘッダがついているので、中国からメールを受け取る機会がないのならこれでフィルタしてしまうのが簡単だったりします。

*1:こんなインターネット黎明期の原始的な仕組みをいまだに使っているのが悪い気がする

*2:Gmailの方がフィルタリングは優秀なので転送してしまうという手もあるんでしょうが…

ワンボタンでiPhoneのヘルスケアにカフェイン摂取記録をつけるデバイスを作った

普段スマートリングでiPhoneのヘルスケアに睡眠記録をとったりしているのですが、ヘルスケアにはカフェインの項目もあり、影響を見てみたいなと思いました。

カフェイン記録をとるアプリ等もありますが、コーヒーを飲むたびにiPhoneを手に取ってアプリを開いて記録を…というのは手間がかかりすぎて続かないので、簡単に1ボタンで記録を取れるようにしてみます。

記録ボタン

簡単さ最重視ということで、1プッシュするとコーヒー1杯分のカフェイン量50mgを記録する方式にします。

ボタンはたまたまあった M5StickS3 を使いました。電源が入ったら、HTTPリクエストを送信して、電源が切れるという簡単なスケッチを入れておきます。*1

#include <M5Unified.h>
#include <WiFi.h>
#include <HTTPClient.h>

char ssid[] = "...";
char pass[] = "...";

HTTPClient http;

void setup() {
  auto cfg = M5.config();
  cfg.internal_spk  = true;
  M5.begin(cfg);
  M5.Speaker.begin();
  M5.Speaker.setVolume(160);

  M5.Lcd.setBrightness(100);
  M5.Lcd.setFont(&fonts::Font4);
  M5.Lcd.setTextColor(WHITE, BLACK);

  WiFi.mode(WIFI_STA);
  //手動でIPアドレスを割り振ればより高速に接続が完了してバッテリーの減りも軽減される
  //WiFi.config(ip,gateway,subnet);
  WiFi.begin(ssid, pass);
  M5.Lcd.drawString("WiFi on..", 4, 160);
  while (true) {
     if (WiFi.status() == WL_CONNECTED) break;
     delay(200);
  }

  Serial.println("sending...");
  M5.Lcd.drawString("sending..", 4, 160);
  http.begin("http://192.168.x.y/button");
  int httpCode = http.GET();
  M5.Lcd.drawString(String("result: ") + httpCode + "   ", 4, 160);

  if (httpCode == HTTP_CODE_OK) {
    M5.Speaker.tone(659, 100);
    delay(100);
    M5.Speaker.tone(523, 100);
    delay(100);
  } else {
    M5.Speaker.tone(440, 100);
    delay(100);
    M5.Speaker.tone(440, 100);
    delay(100);
  }

  M5.Power.powerOff();
}

void loop() {
}

適当なWebサーバーを用意し、以下の簡易な呼び出されたら時刻を記録するだけの CGI シェルスクリプトを /button として置きます。

button

#!/bin/sh
echo "Content-Type: text/plain"
echo
echo "OK"

date +%F\ %T >> button.txt

もう一つ、この結果を取得すると同時に削除するスクリプトも /button_data として置きます。

button_data

#!/bin/sh
echo "Content-Type: text/plain"
echo
cat button.txt
:>button.txt

そして iPhone に以下のようなショートカットを作ります。

オートメーションタブでこのショートカットを毎日決まった時間に実行するように設定すれば出来上がりです。


あとはキッチンにこれを置いておいて、コーヒーを淹れるたびに電源ボタンをポチッと押せば記録が取れます。

余談

M5StickS3 なら振って電源ONにすることもできるらしい。
https://docs.m5stack.com/ja/arduino/m5sticks3/m5pm1

*1:Amazon Dash Button があった頃ならそれを使いたいところですが…

Cornix + 往年のAppleリスペクトな LAK キーキャップセット

Cornix LP キーボードを購入し、せっかくなのでキーキャップを本体と同じくJezailFunderから発売されているLAKプロファイルのものと交換してみました。

日本のショップでは売り切れとなっているため、AliExpressで購入。

高品質な仕上がりで、交換前と比べると色は白のままですが、刻印が全てグレーになったことで非常に落ち着きのある統一された印象になりました。

f / j はホームポジションの突起があるものをつけていますが、突起のないものも付属しています。他に u t および h n にも突起付きが付属しており、こちらは Dovorak 配列用、Colemak / Workman / Norman 配列用でこだわりを感じます。

左下アラインでイタリック表記されたアルファベットは、1980-90年代ごろのAppleのキーボードを彷彿とさせるものがあります。
書体も Univers 47 Condensed Light Oblique で合わせてきていそうです。
Appleのものは大文字でしたが、こちらは小文字でちょっとかわいい感じ。

そういえば中央が四角形に盛り上がったキー形状も下記左の1986年発売の初代ADBキーボード (Apple IIGS用) を連想させます。



Apple Desktop Bus Keyboard

https://en.wikipedia.org/wiki/Apple_Extended_Keyboard
Apple Extended Keyboard

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

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

続きを読む