前情

搞定用 ESP32 控制 PWM 風扇轉速後,下一步是建立 PC 與 ESP32 間的傳輸通道,讓 Windows 端程式能傳送溫度資料給 ESP32,依據溫度目標決定風扇轉速高低,將溫度控制在指定範圍。

起初的構想是讓 ESP32 連上 WiFi,跑一個小網站提供目標溫度設定、即時溫度轉速監看 UI,以及傳入目前溫度的 WebAPI。後來想想,要用 WiFi 就得設定基地台 SSID 及密碼,佔用一個 IP,主機的無線網路被限制住不能切換(例如:某些特殊測試需要暫時改連手機上線)... 有管理成本及運用限制,ESP32 支援藍牙,而且風扇絕對會放在主機旁邊,100% 在藍牙傳輸範圍內,為什麼不用藍牙傳輸呢?

之前都是玩 WiFi,第一次嘗試透過藍牙收送資料,現有程式庫已把複雜的部分都包好了,只需要 #include <BluetoothSerial.h>引用程式庫、BluetoothSerial BT;宣告物件,BT.begin("ESP32-Display");註冊名稱,Windows 端找到它完成配對,Windows 便會多出兩個 COM Port:

其中標註 'ESP32SPP' 的可以對 ESP32 傳送及接收資料。不用寫程式,用 Putty 等支援 COM Port 傳輸的軟體就能測試。我寫了一個小測試,接收藍牙 COM Port 傳來的字元顯示在 OLED 上,按 Enter 時透過藍牙 COM Port 將整句傳回:

#include <Arduino.h>
#include <BluetoothSerial.h>

#include <Wire.h>
#include <Adafruit_I2CDevice.h>
#include <Adafruit_SSD1306.h>
#include "bitmap.h"
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(128, 64, &Wire, -1);

BluetoothSerial BT;

void setup()
{
  Serial.begin(115200);
  while (!Serial)
  {
  }
  Wire.setPins(26, 27); // for ESPro Matrix board
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS))
  {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;)
      ; // Don't proceed, loop forever
  }

  // Clear the buffer
  display.clearDisplay();
  display.drawBitmap(32, 0, guineapigBitmap, 64, 64, WHITE);
  display.display();
  delay(1000);
  display.setTextColor(WHITE, BLACK);
  display.setCursor(0, 56);
  display.ttyPrintln();
  display.ttyPrintln("Bluetooth Test");
  display.display();

  BT.begin("ESP32-Display"); //宣告 Bluetooch 物件

}

String inputString = "";
void loop()
{
  if (BT.available())
  {
    char c = BT.read(); // 從藍牙讀取 Windows 傳來的字元
    if (c == '\r') // enter
    {
      // 換行時整行輸出在 OLED
      display.ttyPrintln();
      display.display();
      // 並將整句內容從藍牙傳回去
      BT.println("\r\nEcho=" + inputString);
      inputString = "";
    }
    else
    {
      // 將接收到的字元顯示在 OLED 上,提供打字即時回饋
      inputString += c;
      BT.print(c);
      display.ttyPrint(String(c));
      display.display();
    }
  }
}

實際操作會像這樣:

ESP32 藍牙傳輸展示

所以只需在記錄 SSD、CPU 溫度時,透過 SerialPort 類別 傳送溫度數字(或溫控規則寫在 C# 端,直接設定轉速)給 ESP32 控制風量,智慧型輔助散熱系統就完成了。

其實 ESP32 是用哪個 COM Port,人工查好改設定就好了。但我硬是自找麻煩,花了點時間,試著實現「用藍牙裝置名稱找到 COM Port」。如此設定檔只需提供裝置名稱,由程式自動找到對映的 COM Port。主要參考兩篇 Stackoverflow 討論(連結放在註解處),復習了 WMI 查詢技巧、還用上 ValueTupleyield return,我寫成以下工具類別:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

internal class BluetoothComPortScanner
{
    public static IEnumerable<(string ComPort, string DeviceName, string? Desc, bool Output)> GetBluetoothComPorts()
    {
        //REF: https://stackoverflow.com/a/64443911/288936
        var results = new ManagementObjectSearcher(@"
SELECT PNPClass, PNPDeviceID, Name, HardwareID 
FROM Win32_PnPEntity 
WHERE 
(Name LIKE '%COM%' AND PNPDeviceID LIKE '%BTHENUM%' AND PNPClass = 'Ports') OR 
(PNPClass = 'Bluetooth' AND PNPDeviceID LIKE '%BTHENUM\\DEV%')").Get();

        var comPorts = new List<ManagementObject>();
        var devices = new List<ManagementObject>();

        foreach (ManagementObject mo in results)
        {
            if (mo["PNPClass"].ToString() == "Bluetooth")
            {
                devices.Add(mo);
            }
            else if (mo["PNPClass"].ToString() == "Ports")
            {
                comPorts.Add(mo);
            }
        }

        string extractComName(string text)
        {
            var m = Regex.Match(text, "[(](?<p>COM[0-9]+)[)]");
            if (m.Success) return m.Groups["p"].Value;
            return text;
        }

        foreach (ManagementObject device in devices)
        {
            var m = Regex.Match(device["PNPDeviceID"]!.ToString()!, "_(?<id>[0-9A-F]+)$");
            if (!m.Success) continue;
            var devId = m.Groups["id"].Value;
            var devName = device["Name"].ToString()!;
            foreach (ManagementObject comPort in comPorts)
            {
                var comPortPNPDeviceID = comPort["PNPDeviceID"]!.ToString()!;
                if (comPortPNPDeviceID.Contains(devId))
                {
                    yield return (
                        extractComName(comPort["Name"].ToString()!),
                        devName,
                        GetBusReportedDeviceDesc(comPort["PNPDeviceID"].ToString()!),
                        true);
                    var hwIdPrefix = ((string[])comPort["HardwareID"]).First().Split('_').First();
                    var inComPort = comPorts.Except(new[] { comPort })
                        .FirstOrDefault(o => ((string[])o["HardwareID"])[0].StartsWith(hwIdPrefix));
                    if (inComPort != null)
                        yield return (
                            extractComName(inComPort["Name"].ToString()!),
                            devName,
                            string.Empty,
                            false);
                }
            }
        }
    }

    static string? GetBusReportedDeviceDesc(string devId)
    {
        //REF https://stackoverflow.com/a/69363717/288936
        foreach (var mo in new ManagementObjectSearcher(null!, "SELECT * FROM Win32_PnPEntity").Get().OfType<ManagementObject>())
        {
            if (mo["PNPDeviceID"] as string != devId) continue;
            var args = new object[] { new string[] { "DEVPKEY_Device_BusReportedDeviceDesc" }, null! };
            mo.InvokeMethod("GetDeviceProperties", args);
            var mbos = (ManagementBaseObject[])args[1];
            if (mbos.Length > 0)
            {
                var data = mbos[0].Properties.OfType<PropertyData>().FirstOrDefault(p => p.Name == "Data");
                if (data != null)
                {
                    return data.Value as string;
                }
            }
        }
        return null;
    }
}

得到 Windows 藍牙設定 UI 相同的結果,成功!

Example of how to use C# to enumerate Bluetooth COM ports.


Comments

Be the first to post a comment

Post a comment