vMix(など)に対応したStreamDeck-likeな物理コントローラーを自作する

2018年: vMixのAPIとリファレンスを手sed目grepしてAPI URLを作ってた
2019年: URLを生成するやつを作った(vmix-utility)
2021年: StreamDeckネイティブプラグイン化を試みる(現在進行形...)
2024年(イマココ): 直接APIに物理コントローラーからアクセスしようと考える

まずは機能要件。
-
USBコントローラーとして機能するモードと、Ethernet経由で機能するモードが欲しいつまりHIDとEthernet(TCP/IP, DHCP)が必要- これ無理。というかUSB稼働は考えてもいいが、Goと相性が悪そうな分野なので捨ててイーサネットだけで良さそう。
-
前者はStreamDeck(あくまでソフトウェアが本体)、後者はTriCasterやKairosのコンパネ(コントローラー側が本体)のようなイメージ- 前述したとおりなので、両者のアイデアを採用し、ハードウェア上で制御用ページを公開して弄れるようにする。
各モードはスイッチで切り替え可能両モードも共通してアプリから設定を変更するUSBモードはvMixなど外部ソフトウエアとの通信部分もアプリが行うEthernetモードは設定のみアプリが担当し、vMix APIとの通信は基盤で行えるようにする- コントローラーとして使う性質上、複数代の接続がありえる
- 起動は早いほうがいい(刺したら1分以内には使えるようになって欲しい)
- スタジオ使用なので堅牢性が求められる

技術選定。
RaspberryPi4などのシリーズは、起動速度やOS周りがある為管理する対象が多くなってメンテナンス負荷が増えるのを嫌いたいので、要件的には悪くないが今回は外す。
更新: 以下に書いてあることを全部やると、RustやC++でいちからライブラリ群を自作する必要があってちょっとキツい。既存資産のGoを最大限使いまわせるアプローチを採用することにした。
TCPの対応、USB HID対応、ボタンやスイッチ搭載と将来的なLED搭載を考慮してRaspberryPi Pico, Arduino, M5stack, Armなどを候補に入れる。
コスパと拡張性を考えるとArm, Arduino、開発のしやすさを考えるとM5stackとなる。
更新: 何を言っているんだ。普通にラズパイだろ
目指すものがStreamDeckやTriCasterのコンパネのような拡張性が求められるシステムなので、Arm, Arduinoで考える。
また、(使いたいので)Rustで開発したい。これはただの欲。

プロジェクト名は、
Arduino + StreamDeck Or Arm + StreamDeck
RDeck としてみる。
R一文字にArduino, Arm, Rustを詰め込んだ。

どの言語で開発するにしろ、vMix TCP APIのクライアント実装が必要。
また、USBとRJ45、そしてそこそこ処理性能が高いCPUとメモリがいるかもしれない(XMLパーシングを行うならかなりいいのがいる)。
EthernetとUSB HIDが最初から乗ったArmか、Arduino Uno R4 MinimaにEthernet Shield2を載せるのとどちらが良いだろうか。
前者はコスパは程々だが、Arduinoベースだけあり文献は非常に多い。
後者はコスパは良いが、開発資料が少なく難しそうな気がする。悩ましい。

期間が空いてしまった。
取り急ぎMilk-V DuoとUSB/Ethernet拡張ボードを購入した。
64MBモデルでLinuxが動くようなので、C/C++やRustでガッツリIoTするより、Linux上のRustやGoで気軽に書いて楽を出来そう、かも。
必然的にUSBモードは捨てになりそうだけど。

また期間が開いた。
Rustなどを使ってMilkVやM5Stackに組み込み実装するのは骨が折れそうなので、Raspberry Pi上で走るLinuxに実装するアプローチに変更。
StreamDeck用のvMixプラグインが思ったより実装に難航しているので、モチベーションがこっちに移行してきた。

レポジトリを作成。
取り急ぎ、vMixにTCP APIで接続し、対応したLEDを光らせることに成功した。
input[入力] -> connector[制御判断] -> output[出力] というインターフェース分割を行うことで、各種部分がある程度共通で扱えるようにした。
例えば、BMD ATEMのタリー -> タリーライトの制御判断ロジック -> LED の場合、
- ATEMからタリーを受け取り、共通の「タリー入力情報」を作成し、次に渡す
- 受け取ったデータと設定を照合し、「タリーライト出力結果」を作成し、次に渡す
- 受け取ったデータをもとに、光る(又は彩度や色を変更する)
という共通化ができ、ATEMからTriCaster、再生中のみのタリー、タリーライトではなく文字出力、のようにそれぞれの部品の差し替えが実現できる。
問題は、これを稼働途中で行うので、各種APIやLEDへの接続をキャッシュし、ソフトウェア側で完全にコントロールしないといけないという点。
多対多の紐付けを、リトライ処理・非同期パフォーマンスなどを考慮したうえで実装する必要がある(つらい)。

複数のconnectorを実現したい場合、1connectorは1アクションと紐付いて複数インスタンス保持するのか、1connectorが複数アクションを管理するインスタンスとして振る舞いのか、かなり迷い
パフォーマンスとメンテナンス的な問題もある

割といい感じのものが出来てきた。
外部からの設定変更などをどう制御しようかと考えていたが、gobotの仕組みでHTTP APIがホスト出来るらしい。しかもフロントエンドまでついてくるらしい、イケている。
後は手すきの時にairを設定したりしたいな。
あと、gobotでピンのプルアップ/ダウン は設定出来ないような気がする。いちいちraspi-gpio
を叩いている(しかもピン番号ではなくGPIO番号なので読み替えが発生する) ので結構面倒。なんとかできないかなー

robot.AddCommand("stop", func(params map[string]interface{}) interface{} {
cancel()
return nil
})
こんな感じでコマンドを追加してあげると、UI上にも表れて扱えるようになるっぽい。
凄い便利だなーと思いつつ、フロントがすごいバグっぽい。デバッグ用途で良いけど実際にこれで人に渡したいとは思えないので、多分Ginあたりで自作することになるんだろうな...

取り急ぎ、airの設定とTaskfileを使ってちょっと楽を出来る様な仕組みくらいは作ろうかな

UI上で
- vMixやATEMなど接続先ソフトウェアの追加/変更/削除、ステータス確認
- GPIOピンアサインの変更(設定)
- determinerの動作の変更(タリーの条件など)
この辺りが出来たらかなりプロダクトして優秀なものが出来上がりそう。
気が早いけど、3DプリンターやGPIOの結線を簡易化・最適化して、小さい筐体に収まりある程度設定変更などが扱いやすいポータブルシステムになってくれたら最高。

結局ラズパイ4で開発しているけど、Gobotを使っているからうまく扱えばMilk-V Duoとかマルチプラットフォーム対応が出来そう。良いね。

その後。 手元にM5Stack Basicがあったので、息抜きにPlatformIOを使って軽い検証用コードを書いてみた。
下記のコードをほぼそのまま流用、PlatformIO向けに若干の改造を実施している。
ちなみに改造内容は以下の通り。
- Arduino IDEでは関数定義の巻き上げが実施されるが、PlatformIOでは実施されない
- ので、関数定義を移動
- 設定ファイルの追加
- 使うライブラリを明示的に指定出来るのは嬉しい
落ち着いたらRDeckのレポジトリにも追加しようと思う。
C++で書いてみた感想は、「思ったより苦痛じゃない」。
もともとGoの比重が多かった人間なので、高校生ぶりのC++だったが文法的に違和感はなかった。
だが、未定義動作やライブラリのお作法、型周りの扱いは全然詳しくないので、少し不安はある。
が、(POCとはいえ) 思ったよりアッサリ書けてしまったので、Arduino, M5StackやESP系など、ガッツリ組み込み向けに舵を切る可能性が少し出てきた。

あと、筐体や共通パーツに関しての知見がまだない。
流石にLEDやスイッチを生のまま露出させるのは気分が良いものではないので、LED、スイッチ、ラベルやLCDがひとまとまりになった「共通のパーツ」を作成し、試作時に役立てたい。
3Dプリンターとユニバーサル基盤あたりで実現できそうだが、50個単位とかで作ってくれるサービスなどあったりしないか...。

M5Stackを使ったvMixタリーは別プロジェクトとして並行で進めることにした。
レポジトリは以下。
現状で以下をサポートしている。
- vMix TCPへの接続・タリー受信
- タリーの表示
- 接続のリトライ
- WiFi / Captive Portalを使ったスマホからの設定実施
- ACTSの受信(実際に反映することは出来ない)
手持ちのArduinoやラズパイと違い、M5Stack一つで外部パーツなしに機能するのはとてもありがたい。
ACTSタリー、設定メニューの改善などの機能追加やリファクタリングは頑張っているところ。
有線LAN接続も対応したい。

これ書いてて、やっぱりGoで実装するのはやめることにした。
XMLをパースする訳でもないので、PlatformIOでそのまま実装する。
今はM5Stackのタリーシステムに注力しつつ、vMix周りの処理などを共通化させる。
その後、Ethernetベースのシステムに移植してコントローラーとしても開発する。
開発は最初はリプレイコントローラーから(自分がずっとリプレイオペレーターだったので)。
まずはW5100, W5200, W5500を買わないと。マジで金欠過ぎる

M5StackのタリーライトがGuru Meditation Error: Core 1 panic'ed
で初期化時にコケることが増えてきたので、リフレッシュついでにRaspberry Pi 4 + gobot を再度触っている。
LANがついていてGPIO制御も出来てメモリも贅沢に使え、既存の資産が運用できるので結構うれしい。
POC版はRaspberry Pi 4 or MilkV Duoでいい気がする。
スクリーン制御など、実際に運用するときに使う部分や細かい部分をやるにはちと物足りないので、あくまでPOCとして。

Guru Meditation Error: Core 1 panic'ed
は、enum class
の変数をsprite.print
の引数に渡していたことが原因だった。そんなことあるんだ...。
とある配信現場で実際に運用してみたところ、「全く問題なく、一度も再起動などをせずに動作した」とのフィードバックを受けた。
まだActivatorsなどの機能実装が済んでいないが、取り急ぎタリーライトとしては機能することになったのでこれでOKとする。
あと、友人がW5100などパーツの購入補助を申し出てくれた。ありがたい。試作品を渡します。

練習ついでに、M5StackでFreeRTOS/マルチスレッドっぽいコードを書いてみる。
書き心地が案外良い。明確に処理と関数を紐づけられるので、Suspendを使ったモードの切り替えや、vMixへの疎通を1タスクに切り分けれそう。
いいかんじのテンプレート
#include <M5Unified.h>
static void TaskTemplate(void *pvParameters) {
auto coreID = xPortGetCoreID();
auto xLastWakeTime = xTaskGetTickCount();
M5.Lcd.fillScreen(TFT_BLUE);
while (1) {
Serial.printf("coreID:%d", coreID); // 動作確認用出力
M5.Lcd.printf("Hello, world! - coreID:%d\n", coreID);
vTaskDelayUntil(&xLastWakeTime, 1000 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
setCpuFrequencyMhz(240);
auto cfg = M5.config();
M5.begin(cfg);
xTaskCreatePinnedToCore(
TaskTemplate,
"Tally",
4096,
NULL,
1,
NULL,
0
);
}
void loop() {}
キューを使ったいい感じのサンプル
#include <M5Unified.h>
// queue
QueueHandle_t xQueue = xQueueCreate( 10, sizeof( unsigned long ) );
static void TaskSender(void *pvParameters) {
auto coreID = xPortGetCoreID();
auto xLastWakeTime = xTaskGetTickCount();
BaseType_t xStatus;
int32_t SendValue = 0;
M5.Lcd.fillScreen(TFT_BLUE);
while (1) {
++SendValue;
xStatus = xQueueSend(xQueue, &SendValue, 0);
if(xStatus != pdPASS){
Serial.println("rtos queue send error...");
}
vTaskDelayUntil(&xLastWakeTime, 1000 / portTICK_PERIOD_MS);
}
}
static void TaskReceiver(void *pvParameters) {
auto xLastWakeTime = xTaskGetTickCount();
BaseType_t xStatus;
int32_t ReceivedValue = 0;
while (1) {
xStatus = xQueueReceive(xQueue, &ReceivedValue, portTICK_PERIOD_MS);
if(xStatus == pdPASS){
Serial.printf("ReceivedValue:%d\n", ReceivedValue);
} else {
Serial.println("rtos queue receive error...");
}
vTaskDelayUntil(&xLastWakeTime, 1000 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
setCpuFrequencyMhz(240);
auto cfg = M5.config();
M5.begin(cfg);
xTaskCreatePinnedToCore(TaskSender, "Sender", 4096, NULL, 1,NULL, 0);
xTaskCreatePinnedToCore(TaskReceiver, "Receiver", 4096, NULL, 1,NULL, 1);
}
void loop() {}

上記マルチタスク/FreeRTOSの実装を以下PRにて進めている。
あらかたの機能の実装が終わり、あとは利便性や画面上の表示など。
また、移植時に不要なボタンが見つかったので削除した。
ボタンが余ったので、付加機能として対象InputのPreviewと、PreviewをTAKE出来る機能を導入した。
以下オマケで、友人たちと制作発表会(セイ作会議) を行った際の発表資料。
あと、動作している様子。

マイコンのチップ、ボード、プラットフォームの違いをやっと理解してきた。
RP2040でもArduinoが動くらしい。 コードにある程度の互換性を持たせれるのであれば、コスト面を考えた上でRP2040/RP2350の評価ボードで開発し、実機には表面実装(手で...?) するのが良いかもしれない。
M5Stackは裸の状態でも持ち運び出来るが、ジャンパケーブルを差すとかなり厳しい。途中で抜けたりする。
ブレッドボードも大きいので、裸のままタリーライトとして運用している。
RP2040+W5500 あたりの評価基板を購入し、ブレッドボード上で開発、ユニバーサル基板で仮実装...という具合であればかなり希望が見えそう。
目指せ表面実装・製品化...

まずはRaspberry Pi Picoやその互換ボードの拡張基板として扱える回路を設計することにした。
めちゃデカいHATみたいな拡張基板のイメージ。
設計した回路はユニバーサル基板で試作する。
慣れてきたらRP2040を実装した基板を新規にデザインしたい。もう割と勉強しちゃった...

必要なものがRP2040とW5500、LEDやボタンなどセンサー類とかなり整理出来た。
ここでふと思ったが、Rust実装でも問題ないな。
TCPパケットの処理速度や、ボタンやLEDのタリー反映速度が問題なければFreeRTOSじゃなくRustで実装して良いかもしれない。
W5500 FCのrp-halライブラリが存在するかが問題。でもみた感じSPI0の制御だけなので、ドキュメントを読めば実装できる気もする。

ドンピシャでRust(embassy)/RP2040/W5500を全て扱うサンプルがあった。助かる。
これでソフトウェアは書けそう。ハードウェアはPico向けのユニバーサル基板という形で作ろう

試作品のパーツとして、RP2350-ETH(CH9210)と、W55RP20-EVB(友人より奢ってもらった)を確保した。まだ届いていないが。
買ってから気付いたけど、CH9210はEthernetライブラリではなくSerial1などでUARTを使って通信する形らしい。送信先IPの設定も若干面倒。今後は全部W5500にしたい。
本当はWiznetのW55RP20という一体化チップが使いたいけれど、LCSCに取り扱いがない(=JLCPCBなどの表面実装で対応しておらず、自分で実装する必要がある) ので、ちょっとハードルが高い。
仕方ないのでRP2040とW5500を同じ基板上に別々で実装しようと思う。こういう場合って水晶発振器とか電源回りどうすればいいんだろう。教えてえろい人~~

そういえばM5Stackに慣れてたから忘れてたけど、Debug Probeが必要なのか。

CH9210のUARTや初期化を理解すれば、embassy-netのトレイトを実装して使うことも可能なのか

秋葉原まで出向いたがW5500のモジュールがどこにも無かった(W55RP20だけあった)。まじか。まあ皆無線使いたいよね。
イーサネットHATでも動きはするが、無駄に値段が高くて大きいので採用したくない。
結局W5500搭載のモジュールを適当にAmazonでポチった。これでブレッドボードに配置して試してみよう...

W55RP20に関して、LCSCにメールしたら速攻で入荷予約をしてくれた。18営業日で来るらしい。
入荷してしまったので基板もW55RP20でやる事になった。嬉しいけどギリギリだ。
あと、手持ちのPico互換基板が増えたのでDebug Probe化して開発している。かなり良い。
Rustの開発環境も無事用意出来たので、あとはW5500をブレッドボード上で配線して実際に使うだけ。がんばるぞ〜
ちなみに、今Web系の開発案件を3つ受けていてイベント系の稼働も発生している。こんなことしてる場合ではないのかもしれない...

そういえば、StreamDeckは有志がリバースエンジニアリングしまくったおかげで外部ドライバやUSB HIDのプロトコル解析データがかなりあるんだよな。
互換基板を作れるかもしれない

Pico Debug ProbeやW55RP20、W5500-PICO-EVBなど、必要そうなものをまとめて買った。
自作したDebug Probeのワイヤが不安定で時々途切れてしまうので、早く届いてほしい。(まあ明日から2/3まで北海道出張なのだが)
ファームウェアの進捗としては、固定IPではあるがvMix TCP APIとのデータ送受信に成功した。
既にACTS/TALLYの受信と、FUNCTIONの送信に対応している(ただし割り込みではない)。
本当は北海道出張までにIPの設定や、ユニバーサル基板に起こして実地試験がしたかったが、色々と忙しく間に合わなかった。
ミニマム構成だとしても、IP表示/設定用のLCDx1、タリー用のLEDx1?、FUNCTION送信用のボタンx3は欲しいと思っている。
W5500のフットプリントが大きいので、ユニバーサル基板も割と大きくなってしまった。まあ仕方ない。

TODOはこんな感じ。
- W55RP20の差異を確認する
- 若干RP2040+W5500と使い方が違う?
- Embassyで使えるのなら別に良いが...
- SPI0を別のピンでも扱えるようにファームウェアを改修する
- XIAO RP2040などはフットプリントが小さく扱いやすい
- 既存のコードでは、XIAO RP2040には無いGPIO16-20辺りを使っているので改修が必要
- Pico+W5500のユニバーサル基板作成
- 評価機として使ってみる
- 上記ユニバーサル基板版を元に回路図を作成
- PCBと向き合う
- 先延ばしにしている

開発中の様子。
これが最新。
ボタン押下時にvMixへFUNCTION(Cut)を送信している。

北海道出張から帰ってきた。
以下の機材を調達したので、開発に役立てる。
今月中にプロトタイプを(n回目)
- Raspberry Pi デバッグプローブ
- 自作したデバッグプローブは配線の接触が怪しいため。
- W5500-EVB-Pico-PoE
- 今後メインで開発に用いるのはこいつになりそう。
- Raspberry Pi Pico H
- 今思うと純正品はピンヘッダがないやつしか持っていなかった
- Wiz Ethernet HAT(W5500)
- その他
- ユニバーサル基板、スズメッキ線、ブレッドボードやボタンなど

機材が届いた。軽い所感。
Raspberry Pi デバッグプローブ
→かなり良い。配線がスッキリし、ソケットが緩んで接続が切れるようなこともなくなった。
W55RP20-EVB-Pico
→チップが少ないのは悪くない...が、EmbassyだとW5500ベースのものと根本的にコードがかわってくる(SPIの代わりにPIOを使用している為)
詳しくは以下。
W5500-EVB-Pico-PoE
→予想通りだが、こちらも配線がスッキリした。
コード側の変更も必要無かったため、ユニバーサル基板の製作も開始している。
いったん機材選定的にはW5500-EVB-Picoで決めて良さそう。
ブレッドボード開発時にはPico互換基板とW5500-Liteで済む上に、W5500-EVB-Picoも勿論同じコードが走り、何より自作基板を作るとなった場合でも(W55RP20よりかは) 情報が多い為容易。
W55RP20は非常に優秀でポテンシャルも高いチップだが、今回の要件には都合が悪かった。
いつか使ってみたい。