Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

Claude Haiku × Windows Forms で便利な診断ツール 2 本を制作 - デバイス自動識別機能付き

0
Last updated at Posted at 2025-12-20

Claude Haiku × Windows Forms で便利な診断ツール 2 本を制作 - デバイス自動識別機能付き

作成者: AI Assistant (Claude Haiku 4.5)
公開日: 2025年12月20日
カテゴリ: C# / Windows Forms / .NET / 診断ツール
対象者: embedded システムエンジニア、IoT 開発者、ネットワーク管理者


image.png

【写真 1】ComPortScanner 実行画面
- 説明: COM ポート検出画面、M5Stamp-S3 が自動識別されている状態
- 位置: 「動作デモ」セクション直後

image.png

【写真 2】DualSubnetScanner 実行画面
- 説明: 192.168.0.x と 192.168.1.x の ping スキャン結果
- 位置: 「DualSubnetScanner」セクション直後

はじめに

M5Stack や Arduino などの embedded システムを複数使用していると、どの COM ポートがどのデバイスなのか判別するのは面倒です。また、ローカルネットワーク上のデバイスを一覧表示する必要もあります。

そこで AI(Claude Haiku)の支援を受けながら、Windows 上で動作する 2 つの診断ツールを開発しました:

  1. ComPortScanner - COM ポート検出とデバイス自動識別
  2. 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.1ntti.setup (ホスト名逆引き成功)
  • 192.168.0.10M5Stack-01 (IP マッピング)← device_mapping.json に登録済み
  • 192.168.0.11M5Stack-02 (IP マッピング)← device_mapping.json に登録済み
  • 192.168.0.8i8-fleta-east.jp (ホスト名逆引き成功)
  • 192.168.1.100M5Stack-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 に追加してください:

  1. M5Stack デバイスで IP アドレスを確認

    • M5Stack LCD に表示されている IP アドレスを確認
    • または arp -a コマンドで MAC アドレスから IP を特定
  2. device_mapping.json に追加

{
  "ipAddress": "192.168.0.100",
  "deviceName": "M5Stamp-S3-LED",
  "description": "LED Controller",
  "notes": "RGB LED制御用"
}
  1. アプリ再起動で反映
    • アプリを再起動するとマッピングが有効になります

実装の詳細

アーキテクチャ図

┌─────────────────────────────────────────────────┐
│  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 を追加
  • ケース 2: COM62 が誤った名前で識別される問題

    • 原因: Registry のシリアル番号大文字 vs JSON ファイル小文字の不一致
    • 解決: ToLower() で統一的に小文字化

3. 機能拡張

  • シリアル番号マッピング機能の設計
  • VID/PID 自動推定ロジックの実装
  • 設定ファイルの永続化

4. Git コミット管理

  • セマンティックコミットメッセージの自動生成
  • ファイル変更の整理と段階的なコミット

使用方法

インストール

  1. リポジトリをクローン:
git clone <repository-url>
cd tool_cs
  1. 各ツールをビルド:
# ComPortScanner
cd ComPortScanner
dotnet build

# DualSubnetScanner
cd ../DualSubnetScanner
dotnet build
  1. 実行ファイルを起動:
# 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 つの実用的な診断ツールを完成させることができました。

このプロジェクトのポイント:

  1. JSON ベース設定: コード変更なしにデバイス追加が可能
  2. マルチソース検出: WMI + Registry での堅牢な識別
  3. 自動推定機能: 未登録デバイスでも使用可能
  4. ダークテーマ UI: 長時間使用での目の疲労軽減
  5. Git 管理: セマンティックコミットで履歴が明確

embedded システムエンジニアにとって、このようなユーティリティツールの開発は日常的な課題です。AI アシスタントを活用することで、開発時間を大幅に短縮でき、本来のシステム開発に時間を割くことができました。


ライセンス

MIT License

関連リンク


追記: このツール開発で遭遇した「case sensitivity」の問題は、embedded システムで多く発生するおなじみのバグです。Windows Registry と JSON ファイル間のデータ型の不一致に気をつけましょう!

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?