はじめに
この記事は、ドワンゴ Advent Calendar 2022 の15日目です。
こんにちは、 @i4M1k0SU (読み: みこす)です。
ドワンゴでは、ニコニコ動画のWebフロントエンド開発をしています。
最近では、AWSの活用に取り組んでいます。
この記事は、2年前のアドベントカレンダー記事の続編のようなものになります。
前回は半分ネタ記事でしたが是非こちらもどうぞ。
→ テレワーク時代だからSlackを光で通知する
テレワークが始まり3年近くが経ちました
ドワンゴでは、2020年2月中旬頃からテレワークを選択できるようになりました。
大勢の従業員が自宅で業務を行っています。
テレワークでのミーティング問題
多くの職場では、ミーティングがあると思います。
テレワークでのミーティングは、当然ながら自宅で行うことになります。
ミーティング中は、実家などの同居人がいる環境では同居人に配慮する必要がありますし、同居人もまた、ミーティングを邪魔しないよう配慮する必要があります。
お互いに気を遣う必要があるわけですが、今現在ミーティング中であることがわからなければ、それは少し難しいでしょう。
一つの解決策?
私の場合、夫婦2人暮らしで2人ともテレワークをしています。
もちろん2人とも毎日ミーティングがあります。
私は会社支給のMacBook Proを利用しているため、必要があれば部屋を移動することができます。
妻の会社はBYOD1なため、私物のデスクトップPCを利用しており移動することができません。
また、終業後ゲームをしながらDiscordで通話をしていることが多いため、私が気を遣う必要のあるケースが多いです。
その解決策に、ミーティング中を知らせる方法として、ON AIRライトを導入しました。
Amazon商品画像より引用: https://amzn.asia/d/gDKyNfS
この商品は、乾電池かUSB Type-Cを電源として点灯することができます。
ボタンで給電をON/OFFできるUSBハブと組み合わせて、デスク横に設置したON AIRライトを必要があれば点灯させるという運用を行っていました。
Amazon商品画像より引用: https://amzn.asia/d/idTG7Ki
行っていました と過去形なのは、実際にはミーティング中に点いていない/終わっても点いたままであることが多く発生し、今では全く点くことがなくなってしまったからです。
やはり、人間の手によりON/OFFするというのは、非常に面倒な作業であり習慣化しない限り忘れてしまうものです。
課題を解決するためには、ミーティングの参加に連動し、点灯・消灯する必要があります。
自動で点灯・消灯するON AIRライトを開発する
まず最初に、成果物をご覧ください。
Slack | Google Meet |
---|---|
ミーティングに参加すると、ON AIRライトが点灯し、退出すると消灯することが確認できますね。
仕組み
テレワークでのミーティングに必ず使うものは、マイクです。
OSがマイクを使用しているかを取得することで、ミーティング中(=通話中・録音中)であるかを判別することができます。
マイクを使用している時に、ON AIRライトを点灯する。使用が終われば、消灯する。という仕組みを作ることで、課題の解決ができるでしょう。
今回は、前回の記事と真逆の方針とし、積極的な電子工作やマイコンの利用を行います。
PCに常駐させたアプリケーションでマイクの使用状況を取得し、状況に応じてUSB接続した制御基板に信号を送ります。
制御基板は、受信した信号を元にON AIRライトを点灯・消灯します。
方針
ON AIRライト自体は既に購入済みのものを使用します。
対応OSは、Windows限定とします。(macOS, Linuxへの対応も不可能ではないでしょう。)
以下の工程が必要になります。
- ハードウェア開発(ON AIRライト改造)
- マイコンプログラム開発
- PC側アプリケーション開発
それぞれの工程ごとに解説します。
ハードウェア開発
ON AIRライトをPCから制御できるようにするべく、改造していきます。
改造行為は自己責任です。
この記事を参考に行った改造で何らかの不利益が生じた場合、私は一切の責任を負いません。
また、ON AIRライトに保証があったか覚えてないですが、もしあったら分解後は無効になりますのでご注意ください。
回路の確認
まずは元の回路を確認します。
特に、乾電池とUSB Type-Cどちらからの給電にも対応しているため、このあたりがどのような仕組みになっているかを確認しておく必要があります。
回路を調べ、回路図に起こしました。
18個のLEDが全て並列で接続されているようです。大きな電流が流れそうです。
さて、給電周りですが結論としては、USBと乾電池を並列に接続しただけの、お行儀の悪い最悪な回路でした。
これでは両方から給電された場合に、もう片方に電流が流れ込むのでよろしくないでしょう。というか危険です。2
電流は、抵抗が低い方に流れ込みますので、USBから乾電池に流れることになります。
それを防ぐためには、必ずどちらかしか給電してはいけません。
USBを使うときは乾電池を外す、乾電池を使うときはUSBケーブルを抜くという作業を、利用者は必ず行う必要があります。これでは、とても使いにくいです。
この手の機能を実装するには、ショットキーバリアダイオード(できれば理想ダイオード)を使いOR回路を組む必要があるでしょう。
残念ながら、このよろしくない回路を構成する原因である、乾電池からの給電機能は外してしまうことにしました。
電池ボックスが不要になったことで生まれた空間は、制御用の基板を組み込むために利用します。
USB給電機能のみとし、USB Type-Cコネクタであることを利用し、USB接続でPCと通信し制御できるようにします。
制御基板の開発
ON AIRライトは、USBケーブルを電源に接続したら光るしか機能がありません。これをPCで制御するためには、何らかのマイコンが載っている必要があります。
今回はArduino Leonardo互換のマイコンボードである、 Pro Micro(5V/16MHz版) を採用します。
自作キーボード界隈でも有名なマイコンボードですので、知っている人も多いのではないでしょうか。
Pro Microは安価であり、載っているATmega32U4はUSBをサポートしているため、手軽にUSB機器を実装することができます。
SparkFun商品画像より引用: https://www.sparkfun.com/products/12640
実際にはこちらの商品ではなく、さらに安価な互換品を使用しています。
電流の測定
ON AIRライトをマイコンボードで制御するためには、どの程度の電流が流れるかを把握する必要があります。
使用したテスター: https://amzn.asia/d/6iYE1JJ
実際に測ってみると、電源にもよりますが、280mA~380mAほど流れるようです。
18個のLEDが全て並列接続になっていますので当然の結果ではありますが、大きめの電流値でしょう。
この電流値となると、Pro MicroのIOピンからの出力で駆動するには、全く足りません。
Pro Microに限らずマイコンボードのIOピンは、流せる電流に制限があります。
それを超えて流そうとすると、ボードが壊れてしまいます。最悪燃えてしまうでしょう。3
そこで半導体を用いた、スイッチングが必要になります。
スイッチングとは
半導体を用いたスイッチングは、小さい電流で、大きい電流をON・OFFすることです。
高速なON・OFFが可能であり、その特性を生かしたスイッチング電源(最近の小さいACアダプタ)など様々なところで利用されています。
今回のケースでは、マイコンボードからの出力(小さい電流)で、LED(大きい電流が必要)を点灯・消灯させることに使います。
マイコンボードとスイッチング素子を用いた方法を調べると、いくつかのサイトではバイポーラトランジスタの2SC18154を用いた方法が紹介されています。
2SC1815はポピュラーなトランジスタですので、Arduino系の解説を行っているサイトでは、とりあえず使われがちな印象があります。
しかし、2SC1815のコレクタ電流は150mAです。これは、LEDに150mAしか流すことができないということです。よって、今回の用途には力不足です。
2SC1815データシート: https://akizukidenshi.com/download/ds/Toshiba/2sc1815.pdf
今回のような用途では、MOSFETが選択肢として挙がります。
バイポーラトランジスタとMOSFET
一般的に、トランジスタと呼称されれば、バイポーラトランジスタのことを指します。
厳密には、トランジスタという大きな枠組みの中の一つが、バイポーラトランジスタです。
信号の増幅や、スイッチングなどあらゆる用途に利用されます。
MOSFETは、絶縁ゲート電界効果トランジスタというトランジスタの仲間です。
MOSFETは、スイッチングを専門としているトランジスタであり、バイポーラトランジスタよりもスイッチング速度が速い特徴があります。
バイポーラトランジスタより、高耐圧で大電流に対応しているものが多いです。
バイポーラトランジスタは、電流駆動であるのに対し、MOSFETは電圧駆動です。
スイッチングは、小さい電流で、大きい電流をON・OFFすることです。
電流駆動は、小さい電流側に流れた電流によって、大きい電流側は流れます。
電圧駆動は、小さい電流側にかかった電圧によって、大きい電流側は流れます。
電圧駆動の場合、小さい電流側にほとんど電流は流れないので、電流駆動に比べ無駄な電流消費が無く、より高効率な回路を作ることができます。
※ あえてベースやゲートといった単語を使わずに説明したことで、電流・電圧の意味が怪しくなっていますがご理解ください。
今回は、Nch MOSFETである、SSM3K345Rを利用します。
このMOSFETのドレイン電流は4Aです。つまり、4Aまで流すことができるので、今回の用途は軽々とこなしてくれるはずです。
SSM3K345Rデータシート: https://toshiba.semicon-storage.com/info/SSM3K345R_datasheet_ja_20221102.pdf?did=55841&prodName=SSM3K345R
使用部材
商品 | 個数 | URL | 備考 |
---|---|---|---|
ON AIRライト | 1 | https://amzn.asia/d/gDKyNfS | |
Nch MOSFET SSM3K345R | 1 | https://akizukidenshi.com/catalog/g/gI-17477/ | 5個入り |
両面ユニバーサル基板 Dタイプ | 1 | https://akizukidenshi.com/catalog/g/gP-11960/ | |
USB Type-C コネクタ | 1 | https://akizukidenshi.com/catalog/g/gC-14356/ | 換装用 |
抵抗 5.1kΩ | 2 | - | USB Type-CのCCプルダウン用 |
抵抗 51Ω | 1 | - | |
抵抗 10kΩ | 1 | - | |
タクトスイッチ | 4 | - | |
ピンヘッダー(オスL型) | 6 | - | |
QIコネクタ | 6 | https://akizukidenshi.com/catalog/g/gC-12160/ | 10個入り |
QIコネクタハウジング 4P | 1 | https://akizukidenshi.com/catalog/g/gC-12153/ | |
QIコネクタハウジング 2P | 1 | https://akizukidenshi.com/catalog/g/gC-12151/ |
URL未記載の項目は、手持ちのパーツ・廃材を利用したため購入場所不明
回路図
タクトスイッチを取り付け、何らかの機能を割り当てることもできるようにします。
組み込み場所である電池ボックスのサイズ制限により、基板は薄く作る必要があります。そこで、Pro Microへの電源供給は、RAWピンから行うこととし、microUSBコネクタは取り外します。microUSBケーブルでの接続は簡単ですが、コネクタが干渉してしまいます。
組み立てた基板
表 | 裏 |
---|---|
Pro MicroのRAWピン経由で給電した場合、USB機能を有効にするにはPro Micro側の回路を改造する必要があります。
表のカプトンテープを貼っている辺りでダイオードをジャンプさせている箇所が、それにあたります。
これにて、制御基板は完成です。
USB Type-Cコネクタの確認
ON AIRライトに実装されているUSB Type-Cコネクタを分解し確認します。
給電用の端子しかついていない安価なものでした。
CC1, CC2端子はついていますが、配線されていません。これは、USBの規格にも違反しています。これでは、一部の充電器では、コールドソケットにより点灯させることができないはずです。(実際に、AnkerのUSB Type-C充電器に接続しましたが点灯しませんでした。)
CC端子とは
USB Type-Cコネクタにある端子です。
挿入されたプラグの向きの判別、接続された機器の電流・電圧の設定、給電の方向など、USB Type-Cの特徴的な機能を実現するために利用される重要な端子です。
コールドソケットとは
正しい規格で実装されたUSB Type-Cでの給電は、コールドソケットという仕組みに基づいています。
これはVBUS(+極)とGND(-極)の間に最初は電圧がかかっておらず、正しい実装をされた機器が接続された時に通電する仕組みです。
5Vの電圧をかける場合、CC1, CC2を5.1kΩの抵抗でプルダウン(GNDと接続すること)する必要があります。
よって、USB Type-Cコネクタをデータ端子付きのものに換装します。
当然ながら、CC1, CC2端子にプルダウン抵抗を実装し、どのような機器に接続しても正しく給電されるようにします。
さて、この換装作業ですが、ハッキリ言って苦行そのものでした。
基板にコネクタと一致するパターンがあれば大したことではないのですが、元のコネクタと新しいコネクタはピッチもパターンも異なります。
そこで配線を直接ハンダ付けして取り出す必要があります。
0.5mmピッチに配線を直接ハンダ付けするのはとても厳しいので、この段階でこの作業をやり始めたことに後悔を感じていました。
万人にオススメできません。
最近は、プリント基板製造を格安で行える業者がありますので、基板を起こしても良かったと思いました。
ON AIRライトに付属していたUSB Type-Cケーブルについて
本体側にデータ端子が存在していなかったのでひょっとしたらと思いましたが、やはり電源線しか結線がありません。
PCとの接続は、データ線が結線されているまともなケーブルに交換する必要があります。
使用したテスター: https://amzn.asia/d/6shiFbc
USB Type-Cは、形だけは統一されていますが、ケーブルには色々な違いがあります。
今回の用途では、比較的安価なUSB2.0に対応しているケーブルであれば問題ありません。
組付け
元通りに戻していきます。
一応、Pro MicroのLEDを外から視認できるようにします。
電池ボックスの蓋に、Pro MicroのLEDと位置が重なるように穴を開け、グレーのUVレジンを充填し光が通るようにします。
これにて、ハードウェアは完成です。
マイコンプログラム開発
USB経由でPCと通信できる必要があります。
今回はPro Microを用いることで可能になった、USB HIDクラスで実装します。
USB HIDクラスとは
USBには、デバイスの種類を表わすクラスというものがあります。
HIDとは、Human Interface Deviceの略称です。
USB HIDクラスは、キーボードやマウス、ゲームパッドやジョイスティックなど、人が利用するデバイス全般に用いられる規格です。
USB HIDの仕様書: https://www.usb.org/sites/default/files/hut1_22.pdf
USB HIDには、LEDも規格に含まれています。
キーボードのNumLockやScrollLockを表すLEDが、それにあたります。
これを利用し、ON AIRライトをUSB HIDクラスのLEDデバイスとして実装します。
HIDクラスは、PluggableUSB.h
を使うことで、実装することができます。
具体的な方法は以下で解説されています。
https://github.com/arduino/Arduino/wiki/PluggableUSB-and-PluggableHID-howto
HID レポートディスクリプタ
USB HIDクラスは、どのようなデバイスで、どのようなデータを送受信するかを定義するレポートディスクリプタというものを持っています。
接続時は、これをPCなどのホスト側に提供します。
今回は、LEDが1個、明るさを0x00~0xFFの256段階で受信する仕様とします。
static const uint8_t PROGMEM hidReport[] = {
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x00, // Usage (Undefined)
0xa1, 0x01, // Collection (Application)
0x15, 0x00, // Logical Minimum (0) 受け取るデータの最小値
0x25, 0xff, // Logical Maximum (255) 受け取るデータの最大値
0x75, 0x08, // Report Size (8) 受け取るデータのサイズ(bit)
0x95, 0x01, // Report Count (1) 受け取るデータの個数
0x05, 0x08, // Usage Page (LED) LEDデバイスである
0x09, 0x4b, // Usage (Generic Indicator) Generic Indicatorを持つ
0x91, 0x02, // Output (Data,Var,Abs)
0xc0 // End Collection
};
これにて、マイコンプログラムは完成です。
PC側アプリケーション開発
対象OSはWindowsです。WindowsでGUIアプリを作るには、Formアプリケーションが簡単です。
今回は、C#.NETでFormアプリケーションを開発します。
機能
最低でも以下の機能を実装する必要があります。
- マイクが使用中かを判定する
- USB HIDクラスのデバイスを制御する
- タスクトレイに常駐させる
マイク使用中の判定
Windowsで、マイクが使用中かを判定するには、Core Audio APIを使うことで実装可能です。
https://learn.microsoft.com/ja-jp/windows/win32/api/_coreaudio/
しかし、Core Audio APIは、COM(Component Object Model)という仕組みで提供されているAPIで、C#.NETからの呼び出しには少し癖があります。
そんな面倒な部分をいい感じにWrapしてくれている、NAudioというライブラリがありますので利用します。
NAudioを用いることで簡単に実装できます。
以下のコードで実現できます。
// マイク使用中を判定する
public static bool GetStateRecording()
{
var mmde = new NAudio.CoreAudioApi.MMDeviceEnumerator();
// mmde.GetDefaultAudioEndpoint で、OS上のデフォルト設定になっているオーディオデバイスを1つ取得することもできる
// しかし、SlackやDiscordなどは任意のオーディオデバイスを指定することが可能なため、全オーディオデバイスに対して判定を行う必要がある
var audioEndpoints = mmde.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active);
return audioEndpoints.Any(device => device.AudioMeterInformation.MasterPeakValue > 0.0f);
}
2023/04/07 追記
上記のコードでの判定は可能ではありますが、場合によってはマイクを使用していないのに使用中と判定されるケースがあります。
Nvidia Container
, svchost
などのプロセスが、音を拾うわけではないのに使用することがあるからです。
また、 Krisp
などのオーディオデバイスをプロキシするソフトウェアを利用する環境の場合は、常に使用中と判定されることがあります。
そこで、プロセス名を認識した上で判定するコードに変更しました。
元のコードより長くなってしまいましたが、より正確に判定できるかと思います。
新しいマイク使用判定実装
Classは省略しています。
// マイク使用を無視するプロセス
private static readonly string[] BlackListProcesses = { "Krisp", "nvcontainer", "svchost" };
// マイク使用中を判定する
public static bool GetStateRecording()
{
var mmde = new NAudio.CoreAudioApi.MMDeviceEnumerator();
var audioEndpoints = mmde.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active);
return audioEndpoints.Any(device =>
{
var sessions = device.AudioSessionManager.Sessions;
for (int i = 0; i < sessions.Count; i++)
{
using (var session = sessions[i])
{
var processId = session.GetProcessID;
var processName = GetProcessName(processId);
if (!string.IsNullOrEmpty(processName) && !BlackListProcesses.Contains(processName))
{
if (session.State == AudioSessionState.AudioSessionStateActive)
{
return true;
}
}
}
}
return false;
});
}
// プロセスIDからプロセス名を取得する
private static string? GetProcessName(uint processId)
{
try
{
using (var process = Process.GetProcessById((int)processId))
{
return process.ProcessName;
}
}
catch (ArgumentException)
{
return null;
}
}
追記終わり
ちなみに、C++であればライブラリを使わず、直接Core Audio APIを呼び出して実装することはそこまで面倒ではありません。
試しにC++でも実装してみました。
C++実装例
#include <iostream>
#include <mmdeviceapi.h>
#include <endpointvolume.h>
#include <system_error>
bool isMicrophoneRecording()
{
IMMDeviceEnumerator* pEnumerator = NULL;
IMMDevice* pDevice = NULL;
HRESULT hr = CoInitialize(NULL);
if (!SUCCEEDED(hr))
{
std::cout << std::system_category().message(hr) << std::endl;
}
hr = ::CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL,
CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),
(void**)&pEnumerator);
if (!SUCCEEDED(hr))
{
std::cout << std::system_category().message(hr) << std::endl;
if (pEnumerator != NULL) pEnumerator->Release();
return false;
}
IMMDeviceCollection* pDevices;
hr = pEnumerator->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &pDevices);
if (!SUCCEEDED(hr) || !pDevices)
{
return false;
}
bool result = false;
UINT count;
pDevices->GetCount(&count);
for (UINT i = 0; i < count; i++)
{
pDevices->Item(i, &pDevice);
IAudioMeterInformation* pMeterInfo = NULL;
hr = pDevice->Activate(__uuidof(IAudioMeterInformation), CLSCTX_ALL, NULL, (void**)&pMeterInfo);
if (!SUCCEEDED(hr))
{
std::cout << std::system_category().message(hr) << std::endl;
if (pMeterInfo != NULL) pMeterInfo->Release();
if (pDevice != NULL) pDevice->Release();
if (pEnumerator != NULL) pEnumerator->Release();
return false;
}
float peakValue;
hr = pMeterInfo->GetPeakValue(&peakValue);
result |= SUCCEEDED(hr) && peakValue > 0.0f;
pMeterInfo->Release();
}
if (pDevice != NULL) pDevice->Release();
if (pEnumerator != NULL) pEnumerator->Release();
return result;
}
int main()
{
while (true)
{
if (isMicrophoneRecording()) {
std::cout << "マイク使用中" << std::endl;
}
else {
std::cout << "マイクは使われていません" << std::endl;
}
Sleep(1000);
}
return 0;
}
USB HIDクラスのデバイスを操作
HIDSharpというライブラリを使うことで、USB HIDデバイスを操作する実装は簡単にできます。
以下は、ON AIRライトを、点灯させるための必要最低限のコードです。
HIDSharpを用いた実装例
using HidSharp;
namespace HIDExample
{
internal class OnAir
{
// Arduino Leonardo
private const ushort VENDOR_ID = 0x2341;
private const ushort PRODUCT_ID = 0x8036;
private const string SERIAL_NUMBER = "OnAir";
public void On()
{
var devices = HidSharp.DeviceList.Local.GetHidDevices(VENDOR_ID, PRODUCT_ID, serialNumber: SERIAL_NUMBER);
if (devices.Count() == 0)
{
Console.WriteLine("Device NotFound");
return;
}
var device = devices.First();
if (device.TryOpen(out var hidStream))
{
hidStream.Write(new byte[] { 0x00, 0xff });
hidStream.Close();
}
}
}
}
以上で、一連のものが実現できました。
成果物
設置しました。とは言っても見た目は同じなので、違いはわかりませんが。
おわりに
久しぶりのAC駆動開発でした。
実は1年前にこれを実現しようとしたのですが、面倒になり途中で投げてしまいました。
アドベントカレンダーに参加することで、期限内に完成させないといけないという義務を生じさせ、作り上げることができました。
こういう面倒事をするのに、アドベントカレンダーはとても便利ですね。
皆さんも、快適なテレワーク環境の実現のために、家族に気を遣ってもらう仕組みを作ってみると良いと思います。
しかし、今回作ったものは非常に難しく面倒事が多かったため、万人にオススメできるものではないですね。
テレワークの普及により、需要は高いと思いますので、このような機能の商品が市販されるようになることを願っています。