編集履歴一覧に戻る
daikon.berryのアイコン画像

daikon.berry が 2026年08月15日12時15分30秒 に編集

コメント無し

本文の変更

## 1.はじめに 休みで暇なので、ゲーミングPCの負荷を可視化できる表示器を作成しました。 自作PCのモニタリングといえばデスクトップ上のオーバーレイや液晶モニターが定番ですが、 「やっぱりインテリアとしてもロマンがある物理的なメーターで動かしたい!」と思い、 3DプリンターとArduino(SG90サーボモーター)を使ってPCの4大リソース(CPU・GPU・MEMORY・NETWORK)をリアルタイム表示する4連アナログメーターを自作しました。 この記事では、ハードウェアの構成から、C#(LibreHardwareMonitor)を使ったデータ取得・変換、そしてArduinoのスケッチまで、一連の制作プロセスをまとめます。

-

+

↓クリックするとGIFが動きます

![キャプションを入力できます](https://camo.elchika.com/93a4ce374b9aa84532494c1a341f09ec3b58f48c/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f61663665663662322d613539332d343836632d393532612d3462343137656334396263642f62623434666532612d633964662d343033322d623735632d613536633031356437653236/) ## 2.全体像と仕組み PC側: C#で常駐プログラムを動かし、オープンソースの「LibreHardwareMonitor」経由でCPU使用率・GPU使用率・メモリ使用率・ネットワークスループットを毎秒取得。 通信: シリアル通信(USB)を介して、計算済みの値をArduinoへ送信。 制御側: Arduino Nanoが値を受け取り、SG90サーボモーターを駆動して物理的な針を動かす。 筐体: 3Dプリンターで自作した専用ケース。 ## 3.部品構成(パーツリスト) Arduino Nano (互換機)1個:マイコン制御用 ¥600 SG90 サーボモーター:4個針を動かすアクチュエータ ¥999 3Dプリンター製ケース・メーター部品1式自作 材料代数百円 接続用USBケーブル2本:PCとの通信・給電用 そこらへんに落ちていたやつ プライスレス ちなみに、SG90を4個駆動するとArduinoからの電力供給では不足するため、給電用のUSBケーブルを付けました ## 4.ハードウェアの配線 サーボモーターのPWM信号線は、Arduinoの以下のデジタルピンに接続しました。 サーボ1 (CPU) : D3 番ピン サーボ2 (GPU) : D5 番ピン サーボ3 (Memory): D6 番ピン サーボ4 (Network): D9 番ピン ## 5.Arduino側のスケッチ(プログラム) Arduino側では、起動時にすべての針を一度「0度(原点)」に合わせる初期化処理(原点出し)を行い、シリアル経由で届いた4つのカンマ区切りの数値(0〜100)をサーボの角度にマッピングして動かします。 ``` #include <Servo.h> Servo sCPU; Servo sGPU; Servo sMEM; Servo sNET; void setup() { sCPU.attach(3); sGPU.attach(5); sMEM.attach(6); sNET.attach(9); // 起動時の原点出し(0度にセット) sCPU.write(0); sGPU.write(0); sMEM.write(0); sNET.write(0); delay(1000); Serial.begin(9600); } void loop() { if (Serial.available() > 0) { String data = Serial.readStringUntil('\n'); data.trim(); int firstComma = data.indexOf(','); int secondComma = data.indexOf(',', firstComma + 1); int thirdComma = data.lastIndexOf(','); if (firstComma != -1 && secondComma != -1 && thirdComma != -1) { int cpuVal = data.substring(0, firstComma).toInt(); int gpuVal = data.substring(firstComma + 1, secondComma).toInt(); int memVal = data.substring(secondComma + 1, thirdComma).toInt(); int netVal = data.substring(thirdComma + 1).toInt(); sCPU.write(map(constrain(cpuVal, 0, 100), 0, 100, 0, 180)); sGPU.write(map(constrain(gpuVal, 0, 100), 0, 100, 0, 180)); sMEM.write(map(constrain(memVal, 0, 100), 0, 100, 0, 180)); sNET.write(map(constrain(netVal, 0, 100), 0, 100, 0, 180)); } } } ``` ## 6.PC側アプリ(C#)の実装 実装時に苦労したのがネットワーク速度の取得です。普通に HardwareType.Network のスループットを拾いに行くと、普段使っていない仮想アダプタ(QoS Packet SchedulerやWFPネイティブMAC層フィルタなど)の速度まで合算されてしまい、実際の通信量が正しく取れない現象が発生しました。 そのため、対象とするセンサーを絞り込み、不要な仮想アダプタの名前を除外するフィルター処理を入れています。 ``` using System; using System.IO.Ports; using System.Collections.Generic; using System.Linq; using System.Threading; using LibreHardwareMonitor.Hardware; class Program { static Queue<double>[] history = Enumerable.Range(0, 4).Select(_ => new Queue<double>()).ToArray(); const int filterSize = 5; public class UpdateVisitor : IVisitor { public void VisitComputer(IComputer computer) { computer.Traverse(this); } public void VisitHardware(IHardware hardware) { hardware.Update(); foreach (var subHardware in hardware.SubHardware) subHardware.Accept(this); } public void VisitSensor(ISensor sensor) { } public void VisitParameter(IParameter parameter) { } } static void Main(string[] args) { string portName = "COM9"; // デバイスマネージャーで設定したポートを指定 var computer = new Computer { IsCpuEnabled = true, IsGpuEnabled = true, IsMemoryEnabled = true, IsNetworkEnabled = true }; computer.Open(); computer.Accept(new UpdateVisitor()); using (SerialPort serialPort = new SerialPort(portName, 9600)) { try { serialPort.Open(); while (true) { computer.Accept(new UpdateVisitor()); double cpuUsage = 0, gpuUsage = 0, memUsage = 0, netBytesPerSec = 0; foreach (var hw in computer.Hardware) { hw.Update(); foreach (var s in hw.Sensors) { if (hw.HardwareType == HardwareType.Cpu && s.SensorType == SensorType.Load && s.Name == "CPU Total") cpuUsage = s.Value ?? 0; if ((hw.HardwareType == HardwareType.GpuNvidia || hw.HardwareType == HardwareType.GpuAmd || hw.HardwareType == HardwareType.GpuIntel) && s.SensorType == SensorType.Load && s.Name.Contains("Core")) gpuUsage = Math.Max(gpuUsage, s.Value ?? 0); if (hw.HardwareType == HardwareType.Memory && s.SensorType == SensorType.Load && s.Name == "Memory") memUsage = s.Value ?? 0; // ネットワークスループットの取得と仮想NIC除外フィルター if (hw.HardwareType == HardwareType.Network && s.SensorType == SensorType.Throughput) { if (!hw.Name.Contains("QoS") && !hw.Name.Contains("WFP") && !hw.Name.Contains("Native MAC")) { netBytesPerSec += (s.Value ?? 0); } } } } double netMbps = netBytesPerSec * 8.0 / 1000000.0; double netScaled = CalculateLogNetSpeed(netMbps); // 移動平均で針のブレを抑える double[] rawValues = { cpuUsage, gpuUsage, memUsage, netScaled }; string output = string.Join(",", rawValues.Select((val, i) => { history[i].Enqueue(val); if (history[i].Count > filterSize) history[i].Dequeue(); return (int)Math.Round(history[i].Average()); })); serialPort.WriteLine(output); Console.WriteLine($"Sent: {output}"); Thread.Sleep(1000); } } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); } } } static double CalculateLogNetSpeed(double mbps) { if (mbps < 0.1) return 0; // 1Mbps〜10Gbpsの範囲を対数スケールに変換 double logVal = Math.Log10(Math.Max(mbps, 1)) / Math.Log10(10000.0) * 100.0; return Math.Min(logVal, 100.0); } } ``` ## 7.ケースと表示板のモデリング SG90の寸法を測定し、ケースのモデリング ![キャプションを入力できます](https://camo.elchika.com/ba5767dd979120192588928e043d8eeb0cdcf2f1/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f61663665663662322d613539332d343836632d393532612d3462343137656334396263642f39373266613232662d383239352d343238632d383436372d646264316362623433353739/) ## 8.3Dプリンタで造形 メーター部の文字を表示したかったので、造形途中で一時停止し、フィラメントの色を交換 ↓はケースを造形中の写真です ![キャプションを入力できます](https://camo.elchika.com/a2f218c22c56ec17a396557452c9ee150db0eddc/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f61663665663662322d613539332d343836632d393532612d3462343137656334396263642f65313166613936632d633332622d346337652d626137302d333365313733323732613535/) ## 9.組付け 休日の趣味の工作のため、配線は美しくない。 Arduinoの固定場所作るの忘れたので、両面テープで固定 ![キャプションを入力できます](https://camo.elchika.com/6f3ba9cb346a12fa06664806769392a09b4f0960/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f61663665663662322d613539332d343836632d393532612d3462343137656334396263642f39613634386435392d383833322d346463632d613933342d396534643331303934613866/) ## 10.完成 ![キャプションを入力できます](https://camo.elchika.com/31176587a2cb73bde4444d31ecf3363c1998d810/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f61663665663662322d613539332d343836632d393532612d3462343137656334396263642f39316331663733322d363063362d346264352d623462322d333064383963333164326239/) あくまでも趣味の工作なので、正しくないかもしれません。誰かの参考になれば幸いです。 そして、メモリは16GBだけど、上限張り付きが判明。増設したいけど、高いですな。