Claude Haiku × Windows Forms で便利な診断ツール 2 本を制作 - デバイス自動識別機能付き
作成者: AI Assistant (Claude Haiku 4.5)
公開日: 2025年12月20日
カテゴリ: C# / Windows Forms / .NET / 診断ツール
対象者: embedded システムエンジニア、IoT 開発者、ネットワーク管理者
【写真 1】ComPortScanner 実行画面
- 説明: COM ポート検出画面、M5Stamp-S3 が自動識別されている状態
- 位置: 「動作デモ」セクション直後
【写真 2】DualSubnetScanner 実行画面
- 説明: 192.168.0.x と 192.168.1.x の ping スキャン結果
- 位置: 「DualSubnetScanner」セクション直後
はじめに
M5Stack や Arduino などの embedded システムを複数使用していると、どの COM ポートがどのデバイスなのか判別するのは面倒です。また、ローカルネットワーク上のデバイスを一覧表示する必要もあります。
そこで AI(Claude Haiku)の支援を受けながら、Windows 上で動作する 2 つの診断ツールを開発しました:
- ComPortScanner - COM ポート検出とデバイス自動識別
- DualSubnetScanner - ローカルネットワーク ICMP ping スキャンツール
本記事では、開発の背景、実装の詳細、そして AI が自動化した部分について解説します。
開発背景
課題
- ✗ 複数の M5Stack デバイスを接続すると COM ポートが不規則
- ✗ デバイスの VID/PID だけでは複数製品を区別できない
- ✗ 毎回デバイス名を手動登録するのは面倒
- ✗ ネットワーク内のデバイスを素早く把握したい
- ✗ ダークテーマ UI で見やすく管理したい
解決方法
- ✓ シリアル番号ベースの動的デバイスマッピング
- ✓ JSON 設定ファイルで管理(コード再コンパイル不要)
- ✓ Windows Forms での モダンダークテーマ実装
- ✓ 新規デバイスは VID/PID で自動推定
ツール概要
1. ComPortScanner
機能:
- COM ポート自動スキャン(リアルタイム、2 秒間隔)
- M5Stack 各製品の自動識別(M5Stamp-S3, M5Stack Core S3 など)
- 新規デバイス接続時の自動推定名表示
- Windows Registry と WMI による多層検出
対応デバイス:
- M5Stamp-S3 (VID:303A, PID:1001) - 7 台登録済み
- M5Stack Core S3 (VID:303A, PID:1001) - 1 台登録済み
- その他 M5Stack ファミリー
出力例:
Port Device Name
---- ----------------
COM3 FTDI FT232 - USB Serial Port (COM3)
COM30 M5Stack Core S3 - USB シリアル デバイス (COM30)
COM59 M5Stamp-S3 - USB シリアル デバイス (COM59)
COM62 M5Stamp-S3 - USB シリアル デバイス (COM62)
COM65 M5Stamp-S3 (新規) - USB シリアル デバイス (COM65)
2. DualSubnetScanner
機能:
- 複数サブネット同時 ICMP ping スキャン(192.168.0.x, 192.168.1.x)
- ホスト名逆引き機能(DNS 500ms タイムアウト)
- 並列処理による高速スキャン(800ms タイムアウト)
- 自動刷新機能(カスタマイズ可能な間隔)
- ウィンドウレイアウト・列幅の自動保存
スキャン結果例:
IP Address Device / Hostname RTT (ms)
----------- ------------------------- --------
192.168.0.1 ntti.setup OK
192.168.0.2 192.168.0.2 OK
192.168.0.6 192.168.0.6 2
192.168.0.8 i8-fleta-east.jp OK
192.168.0.9 192.168.0.9 4
192.168.0.10 192.168.0.10 4
192.168.0.11 192.168.0.11 23
192.168.1.1 192.168.1.1 OK
192.168.1.100 i8-fleta-east.jp OK
192.168.1.101 192.168.1.101 48
スキャン結果例:
IP Address Device / Hostname RTT (ms)
----------- ------------------------- --------
192.168.0.1 ntti.setup OK
192.168.0.2 192.168.0.2 OK
192.168.0.3 192.168.0.3 50
192.168.0.4 192.168.0.4 45
192.168.0.5 192.168.0.5 55
192.168.0.6 192.168.0.6 1
192.168.0.8 i8-fleta-east.jp OK
192.168.0.9 192.168.0.9 3
192.168.0.10 M5Stack-01 3
192.168.0.11 M5Stack-02 120
192.168.0.21 192.168.0.21 52
192.168.1.1 192.168.1.1 1
192.168.1.100 M5Stack-WiFi OK
192.168.1.101 192.168.1.101 50
表示の優先度が機能している例:
-
192.168.0.1→ntti.setup(ホスト名逆引き成功) -
192.168.0.10→M5Stack-01(IP マッピング)← device_mapping.json に登録済み -
192.168.0.11→M5Stack-02(IP マッピング)← device_mapping.json に登録済み -
192.168.0.8→i8-fleta-east.jp(ホスト名逆引き成功) -
192.168.1.100→M5Stack-WiFi(IP マッピング)← device_mapping.json に登録済み -
192.168.0.2,192.168.0.3など → IP アドレスのままの表示 (ホスト名逆引き失敗時の フォールバック)
ホスト名について:
- ✓ ホスト名逆引き成功時:
ntti.setup,i8-fleta-east.jpなどを表示 - ✓ ホスト名逆引き失敗時: IP アドレスをそのまま表示
- ✓ IP マッピング設定済み時: M5Stack デバイス名を優先表示(手動登録が必要)
- 📝 IP ベースの自動デバイス判別は技術的に困難なため、device_mapping.json への手動マッピング登録を推奨
IP → デバイス名マッピング機能
DualSubnetScanner で IP アドレス → M5Stack デバイス名 のマッピング表示を実現するには、device_mapping.json への手動登録が必要です。
IP ベースのネットワークスキャンでは、IP アドレスだけからデバイスの機種・用途を自動判別することは技術的に困難です(MAC アドレスの OUI テーブルを使用しても、一般的な ESP32 ボードは判別不可)。
そのため、以下の方針で実装しました:
- ComPortScanner: VID/PID で自動判別可能(USB デバイス識別情報がある)
- DualSubnetScanner: IP マッピング(手動登録) > ホスト名逆引き > IP アドレス表示
device_mapping.json の IP マッピングセクション
M5Stack デバイスに静的 IP アドレスを割り当てている場合、device_mapping.json の ipMappings セクションに以下のように登録してください:
{
"ipMappings": [
{
"ipAddress": "192.168.0.10",
"deviceName": "M5Stack-01",
"description": "Living Room Controller",
"notes": "StaticIP設定済み"
},
{
"ipAddress": "192.168.0.11",
"deviceName": "M5Stack-02",
"description": "Kitchen Display",
"notes": "StaticIP設定済み"
},
{
"ipAddress": "192.168.0.50",
"deviceName": "M5Stamp-S3-Monitor",
"description": "DMX Monitor",
"notes": "DMX監視用デバイス"
},
{
"ipAddress": "192.168.1.100",
"deviceName": "M5Stack-WiFi",
"description": "Remote Control",
"notes": "802.11b/g/n対応"
}
]
}
優先度の仕組み
DualSubnetScanner のデバイス表示は以下の優先度で決定されます:
← 手動登録
↓
2️⃣ ホスト名逆引き (DNS 500ms タイムアウト) ← mDNS が有効な場合のみng.json ipMappings)
↓
2️⃣ ホスト名逆引き (DNS 500ms タイムアウト)
↓
3️⃣ IP アドレス (フォールバック)
**実装コード**:
```csharp
private string GetDeviceNameForIp(string ipAddress)
{
// IP マッピングテーブルから検索(優先度 1)
if (IpDeviceMapping.TryGetValue(ipAddress, out var deviceName))
{
return deviceName;
}
// マッピングない場合は IP を返す(ホスト名逆引きは呼び出し元で実行)
return ipAddress;
}
// DoScanAsync() 内での使用例
string hostname = GetDeviceNameForIp(ip);
if (hostname == ip)
{
// IPマッピングにない場合は、ホスト名逆引きを試行
hostname = await GetHostnameAsync(ip, 500);
}
新規デバイスの追加方法
M5Stack の IP アドレスを確認してから、device_mapping.json の ipMappings に追加してください:
-
M5Stack デバイスで IP アドレスを確認
- M5Stack LCD に表示されている IP アドレスを確認
- または
arp -aコマンドで MAC アドレスから IP を特定
-
device_mapping.json に追加
{
"ipAddress": "192.168.0.100",
"deviceName": "M5Stamp-S3-LED",
"description": "LED Controller",
"notes": "RGB LED制御用"
}
-
アプリ再起動で反映
- アプリを再起動するとマッピングが有効になります
実装の詳細
アーキテクチャ図
┌─────────────────────────────────────────────────┐
│ ComPortScanner / DualSubnetScanner │
│ (Windows Forms UI - Dark Theme) │
└──────────────────┬──────────────────────────────┘
│
┌──────────┴──────────┐
│ │
┌───▼────────┐ ┌────▼──────────┐
│ WMI │ │ Registry & │
│ Queries │ │ Win32 APIs │
└────────────┘ └───────────────┘
│ │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ device_mapping.json │
│ (Serial Number Map) │
└─────────────────────┘
device_mapping.json の構造
新しい JSON ベースの設定ファイルを使用することで、コード変更なしにデバイスマッピングを追加可能にしました。
{
"deviceMappings": [
{
"name": "M5Stamp-S3",
"description": "M5Stack Stamp S3 (ESP32-S3)",
"patterns": [
{
"type": "serial",
"value": "8&15adb4ef&2&0000",
"comPort": "COM59"
},
{
"type": "serial",
"value": "8&2738fdf9&2&0000",
"comPort": "COM47"
},
{
"type": "serial",
"value": "8&817f273&1&0000",
"comPort": "COM63"
},
{
"type": "serial",
"value": "8&2e247081&1&0000",
"comPort": "COM64"
},
{
"type": "serial",
"value": "8&19cc43dc&0&0000",
"comPort": "COM65"
}
],
"commonPattern": "8&[HEX]&[1-2]&0000"
},
{
"name": "M5Stack Core S3",
"description": "M5Stack Core S3 (ESP32-S3)",
"patterns": [
{
"type": "serial",
"value": "8&a19aef4&2&0000",
"comPort": "COM30"
}
]
}
],
"vidPidAutoDetect": {
"303A1001": {
"defaultDevice": "M5Stamp-S3",
"hints": [
"VID 303A / PID 1001は複数のM5製品で使用",
"未登録の新規デバイス場合の推定: M5Stamp-S3"
]
}
}
}
【写真 3】JSON ファイルの構造を表示
ComPortScanner のコア実装
1. デバイスマッピングの動的読み込み
// Form1.cs
private static readonly string DeviceMappingFile = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "device_mapping.json");
private static Dictionary<string, string> DeviceMapping = new();
private void LoadDeviceMapping()
{
try
{
if (File.Exists(DeviceMappingFile))
{
var json = File.ReadAllText(DeviceMappingFile);
Console.WriteLine($"[DEBUG] device_mapping.json loaded from: {DeviceMappingFile}");
using (JsonDocument doc = JsonDocument.Parse(json))
{
var root = doc.RootElement;
if (root.TryGetProperty("deviceMappings", out var mappings))
{
foreach (var device in mappings.EnumerateArray())
{
if (device.TryGetProperty("name", out var name) &&
device.TryGetProperty("patterns", out var patterns))
{
string deviceName = name.GetString() ?? "Unknown";
foreach (var pattern in patterns.EnumerateArray())
{
if (pattern.TryGetProperty("type", out var type) &&
type.GetString() == "serial" &&
pattern.TryGetProperty("value", out var value))
{
string? serial = value.GetString();
if (!string.IsNullOrEmpty(serial))
{
// 小文字で統一(Case-insensitive マッチング)
string normalizedSerial = serial.ToLower();
DeviceMapping[normalizedSerial] = deviceName;
Console.WriteLine(
$"[DEBUG] Loaded mapping: {normalizedSerial} -> {deviceName}");
}
}
}
}
}
}
}
Console.WriteLine($"[DEBUG] Total device mappings loaded: {DeviceMapping.Count}");
}
}
catch (Exception ex)
{
Console.WriteLine($"[DEBUG] Error loading device_mapping.json: {ex.Message}");
}
}
2. シリアル番号の抽出と正規化
private string? ExtractSerialNumberPart(string deviceId)
{
// DeviceID の最後の部分を抽出
// USB\VID_303A&PID_1001&MI_00\8&15adb4ef&2&0000 から
// 8&15adb4ef&2&0000 を取得
var parts = deviceId.Split('\\');
if (parts.Length > 0)
{
string serialPart = parts[^1].ToLower(); // 小文字に正規化
Console.WriteLine($"[DEBUG] ExtractSerialNumberPart: {deviceId} -> {serialPart}");
return serialPart;
}
return null;
}
3. デバイス識別ロジック(3 段階)
private string IdentifyDeviceByVidPid(string deviceId, string deviceName)
{
var nameLower = deviceName.ToLower();
// ステップ 1: デバイス名からの直接判定
if (nameLower.Contains("m5din"))
return "M5DIN Meter - " + deviceName;
if (nameLower.Contains("m5stack") && nameLower.Contains("core") && nameLower.Contains("s3"))
return "M5Stack Core S3 - " + deviceName;
// ステップ 2: VID/PID テーブルマッチング
var deviceIdUpper = deviceId.ToUpper();
if (deviceIdUpper.Contains("303A"))
{
if (deviceIdUpper.Contains("1001"))
// 未登録デバイスは M5Stamp-S3 と推定(多数派)
return "M5Stamp-S3 (新規) - " + deviceName;
else if (deviceIdUpper.Contains("8120"))
return "M5DIN Meter - " + deviceName;
else
return "ESP32 - " + deviceName;
}
// ステップ 3: フォールバック(デフォルト名)
return "Unknown Device - " + deviceName;
}
4. WMI + Registry マルチソース検出
private async Task DoScanAsync()
{
var portDeviceInfo = new Dictionary<string, string>();
// ソース 1: Win32_SerialPort (COM ポート一覧)
try
{
using (var searcher = new ManagementObjectSearcher(
"SELECT * FROM Win32_SerialPort WHERE Availability = 2"))
{
foreach (var obj in searcher.Get())
{
string? description = obj["Description"]?.ToString();
string? name = obj["Name"]?.ToString();
if (name != null)
portDeviceInfo[name] = description ?? "Unknown";
}
}
}
catch { }
// ソース 2: Registry HARDWARE\DEVICEMAP\SERIALCOMM
try
{
using (var regKey = Registry.LocalMachine.OpenSubKey(
@"HARDWARE\DEVICEMAP\SERIALCOMM"))
{
if (regKey != null)
{
foreach (var valueName in regKey.GetValueNames())
{
var portName = regKey.GetValue(valueName)?.ToString();
if (portName != null)
{
string deviceInfo = GetDeviceInfoFromRegistry(valueName);
if (!string.IsNullOrEmpty(deviceInfo))
portDeviceInfo[portName] = deviceInfo;
}
}
}
}
}
catch { }
// ソース 3: Win32_PnPEntity (VID/PID 情報付き)
try
{
using (var searcher = new ManagementObjectSearcher(
"SELECT * FROM Win32_PnPEntity WHERE ClassGuid=\"{4D36E978-E325-11CE-BFC1-08002BE10318}\""))
{
foreach (var obj in searcher.Get())
{
string? name = obj["Name"]?.ToString();
string? deviceId = obj["DeviceID"]?.ToString();
if (name != null && name.Contains("COM") && deviceId != null)
{
var match = Regex.Match(name, @"(COM\d+)");
if (match.Success)
{
string comPort = match.Groups[1].Value;
// 優先度 1: シリアル番号マッピング
string? serialPart = ExtractSerialNumberPart(deviceId);
if (serialPart != null && DeviceMapping.TryGetValue(serialPart, out var mappedName))
{
// 成功!マッピングされた名前を使用
portDeviceInfo[comPort] = $"{mappedName} ({comPort})";
Console.WriteLine($"[DEBUG] Found mapping for {comPort}: {mappedName}");
}
else
{
// 優先度 2: VID/PID 推定
var vidPidMatch = Regex.Match(deviceId,
@"VID_([0-9A-F]+).*PID_([0-9A-F]+)",
RegexOptions.IgnoreCase);
if (vidPidMatch.Success)
{
string vid = vidPidMatch.Groups[1].Value;
string pid = vidPidMatch.Groups[2].Value;
string device = IdentifyDeviceByVidPid($"{vid}{pid}", name);
portDeviceInfo[comPort] = device;
}
}
}
}
}
}
}
catch { }
// UI を更新
this.Invoke(() =>
{
_dataGridView?.Rows.Clear();
foreach (var kvp in portDeviceInfo.OrderBy(x => x.Key))
{
_dataGridView?.Rows.Add(kvp.Key, kvp.Value);
}
_statusLabel!.Text = $"Found {portDeviceInfo.Count} ports";
});
}
.csproj の設定ファイル自動コピー
重要なポイント: JSON ファイルがビルド出力ディレクトリに自動的にコピーされないため、手動で設定を追加:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- 設定ファイルを自動的にビルド出力にコピー -->
<None Update="device_mapping.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="scanner_settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
DualSubnetScanner の実装
ICMP Ping スキャンと非同期ホスト名逆引き
private async Ta!.Text = "Scanning...";
var results = new List<(string IP, string Hostname, string RTT)>();
var tasks = new List<Task>();
var lockObj = new object();
foreach (var network in Networks)
{
for (int i = 1; i <= 254; i++)
{
string ip = $"{network}.{i}";
tasks.Add(Task.Run(async () =>
{
if (token.IsCancellationRequested) return;
try
{
using var ping = new Ping();
var reply = await ping.SendPingAsync(ip, 800);
if (reply.Status == IPStatus.Success)
{
string rtt = reply.RoundtripTime < 1 ? "OK" : $"{reply.RoundtripTime}";
// デバイス名表示: IPマッピング > ホスト名逆引き > IP
string hostname = GetDeviceNameForIp(ip); // ← IP マッピングをチェック(優先度 1)
if (hostname == ip)
{
// IPマッピングにない場合は、ホスト名逆引きを試行
hostname = await GetHostnameAsync(ip, 500); // ← DNS 逆引き(優先度 2)
}
if (reply.Status == IPStatus.Success)
{
string rtt = reply.RoundtripTime < 1 ? "OK" : $"{reply.RoundtripTime}";
// ホスト名逆引き(非同期、タイムアウト付き)
string hostname = await GetHostnameAsync(ip, 500);
lock (lockObj)
{
results.Add((ip, hostname, rtt));
}
}
}
catch { }
}, token));
}
}
if (token.IsCancellationRequested) return;
// IPアドレスでソート
results.Sort((a, b) => CompareIpAddresses(a.IP, b.IP));
// UI更新
this.Invoke(() =>
{
_dataGridView!.Rows.Clear();
foreach (var (ip, hostname, rtt) in results)
{
_dataGridView.Rows.Add(ip, hostname, rtt);
}
_statusLabel!.Text = $"Found {results.Count} hosts - {DateTime.Now:HH:mm:ss}";
});
}
private string GetDeviceNameForIp(string ipAddress)
{
// IP マッピングテーブルから検索(優先度 1)
if (IpDeviceMapping.TryGetValue(ipAddress, out var deviceName))
{
return deviceName;
}
// マッピングない場合は IP を返す(ホスト名逆引きは呼び出し元で実行)
return ipAddress {
_dataGridView.Rows.Add(ip, hostname, rtt);
}
_statusLabel!.Text = $"Found {results.Count} hosts - {DateTime.Now:HH:mm:ss}";
});
}
private async Task<string> GetHostnameAsync(string ipAddress, int timeoutMs)
{
try
{
// 500ms タイムアウト付きで DNS 逆引きを実行
var cts = new CancellationTokenSource(timeoutMs);
var hostname = await System.Net.Dns.GetHostEntryAsync(ipAddress);
return hostname.HostName;
}
catch
{
// ホスト名取得失敗時は IP アドレスのみ返す
return ipAddress;
}
}
private static int CompareIpAddresses(string ip1, string ip2)
{
var parts1 = ip1.Split('.');
var parts2 = ip2.Split('.');
if (parts1.Length == 4 && parts2.Length == 4 &&
int.TryParse(parts1[3], out int last1) &&
int.TryParse(parts2[3], out int last2))
{
return last1.CompareTo(last2);
}
return ip1.CompareTo(ip2);
}
重要ポイント:
- 🔹 並列処理: 508 個の IP アドレスを同時に ping(スキャン時間 ~5 秒)
- 🔹 非同期 DNS: 各ホストの DNS 逆引きは並列で実行(スキャン速度を損なわない)
- 🔹 タイムアウト保護: DNS 逆引きは 500ms でタイムアウト(スローなホストによる遅延防止)
- 🔹 フォールバック: ホスト名取得失敗時は IP アドレスをそのまま表示
設定ファイル(JSON)の保存と復元
private void SaveSettings()
{
try
{
var settings = new Settings
{
X = this.Location.X,
Y = this.Location.Y,
Width = this.Size.Width,
Height = this.Size.Height,
IpColumnWidth = _dataGridView?.Columns[0].Width ?? 200,
RttColumnWidth = _dataGridView?.Columns[1].Width ?? 100
};
var json = JsonSerializer.Serialize(settings,
new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(SettingsFile, json);
}
catch { }
}
private void LoadSettings()
{
try
{
if (File.Exists(SettingsFile))
{
var json = File.ReadAllText(SettingsFile);
var settings = JsonSerializer.Deserialize<Settings>(json);
if (settings != null)
{
this.Location = new Point(settings.X, settings.Y);
this.Size = new Size(settings.Width, settings.Height);
if (_dataGridView != null)
{
_dataGridView.Columns[0].Width = settings.IpColumnWidth;
_dataGridView.Columns[1].Width = settings.RttColumnWidth;
}
}
}
}
catch { }
}
ダークテーマ UI の実装
両ツールで同じダークテーマを採用し、一貫性を保ちました。
// フォーム背景とテキスト色
this.BackColor = Color.FromArgb(45, 45, 48); // VS Code ダークグレー
this.ForeColor = Color.FromArgb(220, 220, 220); // 明るいグレー
// DataGridView のカスタムスタイリング
_dataGridView = new DataGridView
{
Dock = DockStyle.Fill,
BackgroundColor = Color.FromArgb(45, 45, 48),
ForeColor = Color.FromArgb(220, 220, 220),
BorderStyle = BorderStyle.Fixed3D,
ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize,
ColumnHeadersDefaultCellStyle = new DataGridViewCellStyle
{
BackColor = Color.FromArgb(37, 37, 38),
ForeColor = Color.FromArgb(220, 220, 220),
Font = new Font("Segoe UI", 10, FontStyle.Bold)
},
DefaultCellStyle = new DataGridViewCellStyle
{
BackColor = Color.FromArgb(45, 45, 48),
ForeColor = Color.FromArgb(220, 220, 220)
},
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
ReadOnly = true
};
// 交互に異なる背景色(可読性向上)
_dataGridView.AlternatingRowsDefaultCellStyle = new DataGridViewCellStyle
{
BackColor = Color.FromArgb(55, 55, 58)
};
// セル境界線
_dataGridView.CellBorderStyle = DataGridViewCellBorderStyle.Single;
_dataGridView.GridColor = Color.FromArgb(70, 70, 70);
【写真 1】ComPortScanner 実行画面 - ダークテーマ UI
【写真 2】DualSubnetScanner 実行画面 - ダークテーマ UI
AI(Claude Haiku)が自動化した部分
開発プロセスで Claude Haiku の支援により、以下が効率化されました:
1. コード生成と最適化
- WMI + Registry マルチソース検出の複雑なロジックの自動生成
- JSON 解析コードの実装
- 例外処理とエラーハンドリングの追加
2. デバッグとトラブルシューティング
-
ケース 1: device_mapping.json がビルド出力にコピーされない問題
- 原因: .csproj に
<CopyToOutputDirectory>設定がない - 解決: .csproj に適切な ItemGroup を追加
- 原因: .csproj に
-
ケース 2: COM62 が誤った名前で識別される問題
- 原因: Registry のシリアル番号大文字 vs JSON ファイル小文字の不一致
- 解決:
ToLower()で統一的に小文字化
3. 機能拡張
- シリアル番号マッピング機能の設計
- VID/PID 自動推定ロジックの実装
- 設定ファイルの永続化
4. Git コミット管理
- セマンティックコミットメッセージの自動生成
- ファイル変更の整理と段階的なコミット
使用方法
インストール
- リポジトリをクローン:
git clone <repository-url>
cd tool_cs
- 各ツールをビルド:
# ComPortScanner
cd ComPortScanner
dotnet build
# DualSubnetScanner
cd ../DualSubnetScanner
dotnet build
- 実行ファイルを起動:
# ComPortScanner
./ComPortScanner/bin/Debug/net10.0-windows/ComPortScanner.exe
# DualSubnetScanner
./DualSubnetScanner/bin/Debug/net8.0-windows/DualSubnetScanner.exe
device_mapping.json への新規デバイス追加
# PowerShell で新規デバイスのシリアルを取得
Get-PnpDevice -PresentOnly | Where-Object { $_.FriendlyName -match "COM99" } | ForEach-Object { $_.InstanceId }
# 出力例:
# USB\VID_303A&PID_1001&MI_00\8&NEWSERIAL&1&0000
device_mapping.json に追加:
{
"type": "serial",
"value": "8&newserial&1&0000",
"comPort": "COM99"
}
アプリを再起動すると自動的に認識されます!
x] IP マッピング機能(device_mapping.json ベース)- 完了!(手動登録)
- CSV エクスポート機能
- ping ラウンドトリップタイムのグラフ表示
- ARP キャッシュからの MAC アドレス取得と表示
- mDNS (.local) 自動検出機能
- サブネット自動検出機能
- 複数ネットワークインターフェース対応
- メモリ使用量: ~40MB
DualSubnetScanner
- スキャン範囲: 192.168.0.0/24 + 192.168.1.0/24 (508 アドレス)
- スキャン時間: ~5 秒(800ms タイムアウト)
- 並列度: 508 並列タスク
今後の拡張予定
- ホスト名逆引き機能(DNS 解決)- 完了!
- IP アドレス → M5Stack デバイス名マッピング(device_mapping.json 拡張)
- CSV エクスポート機能
- ping ラウンドトリップタイムのグラフ表示
- サブネット自動検出機能
- 複数ネットワークインターフェース対応
- ARP キャッシュからの MAC アドレス取得と表示
まとめ
Claude Haiku の支援により、わずかなセッションで 2 つの実用的な診断ツールを完成させることができました。
このプロジェクトのポイント:
- ✓ JSON ベース設定: コード変更なしにデバイス追加が可能
- ✓ マルチソース検出: WMI + Registry での堅牢な識別
- ✓ 自動推定機能: 未登録デバイスでも使用可能
- ✓ ダークテーマ UI: 長時間使用での目の疲労軽減
- ✓ Git 管理: セマンティックコミットで履歴が明確
embedded システムエンジニアにとって、このようなユーティリティツールの開発は日常的な課題です。AI アシスタントを活用することで、開発時間を大幅に短縮でき、本来のシステム開発に時間を割くことができました。
ライセンス
MIT License
関連リンク
- GitHub リポジトリ: [リンク]
- M5Stack 公式サイト: https://m5stack.com/
- .NET 公式ドキュメント: https://learn.microsoft.com/dotnet/
追記: このツール開発で遭遇した「case sensitivity」の問題は、embedded システムで多く発生するおなじみのバグです。Windows Registry と JSON ファイル間のデータ型の不一致に気をつけましょう!

