zoologist 发表于 2024-3-4 11:09:44

UT331 有线温湿度计转无线

在1876年美国纽约,不同社会地位的人都在此云集,有一位名叫杜安的农场主,娶了一名心灵手巧的女人做妻子,她叫伊丽莎白,他们有一个儿子,名为威利斯·开利(Willis Carrier)。开利和母亲一样,拥有极强的动手能力,母亲为农场维修钟表、缝纫机以及家用器具,开利就在一旁打下手。威利斯·开利不仅爱好动手,更是对数学十分痴迷。常常在工作之余学习数学,深研此道。终于,开利准入了康奈尔大学机械工程专业,并在1901年获得机械工程专业硕士的学位。此后,成为了一名周薪只有10美元的暖通工程师。一方面是因为家庭基础,一方面是因为热爱,开利一直在这个行业中探索前进。不过,任何事情都不是偶然,回头看许多事情也并没有意外。24岁这一年,开利有一次在火车站等火车,只见这天的天气不好,漫天都是白雾。然而开利灵机一动,冒出了一个想法:温度、湿度、雾点之间的联系也可以放在处理空间湿度水平之中,只要空气气温降得足够低,空气中的相对湿度自然就下降了。因此他马上动手实践,改良了已有的“空气调节装置”。这种装置很快被应用了。1906年,布鲁克林印刷厂因为空气湿度太大,无法印刷出清晰的字迹一事找到了暖通公司,开利被指派解决这项问题。开利进入印刷厂后,马上就理清了前因后果。他根据国家气候表计算出适合印刷的完美温度、湿度,然后再次发明,装配出了一台空气温湿度调节装置。布鲁克印刷厂用了这装置之后,大感奇妙!这就是世界上第一台空调。而开利改良空气调节装置的技术,也在当年带给他了一项专利权。世界上第一台空调的专利。从此,开利被誉为“空调之父”。1921年,开利发明出第一台离心式冷水机组,非常适合大型空间制冷,多用于冷气车间。并在同年获得专利。 1924年,他终于将空调从工业场面搬到了民用场景之中。美国国会、众议院、白宫过来照顾了他的生意…【参考1】这次带来的作品是一个将优德利 UT331 数字式温湿度计的结果实时发送到手机上的方案。优德利 UT331 是一款专业的温湿度计。它以高精度数字温湿度模块为传感器,使用低功耗微处理器。它带有USB接口,能够将数据传输到 PC 中,基本功能参数如下:温度测量:-20℃~-70℃(±0.5℃)露点温度:-78.2℃~-70℃(±1℃)湿球温度:-21.6℃~-70℃(±0.5℃)湿度:0~99.9%RH(10%~90%RH ±3%RH;其余±3.5%RH)分辨率:温度:0.1℃/湿度0.1%RH自动/手动数据存储:299组(可查阅)高低温限值背光报警最大/小值测量/数据保持测试数值偏移设置:-6~6配备上位机软件(实时数据查看并可导出存储数据)
USB供电:插入USB可切换至USB供电
UT331UT331 自带的Windows 应用程序
我们使用 ESP32 S2 作为 USB Host 将UT331的数据收取之后,通过 WIFI 传输到 Blinker平台,之后通过手机 Blinker App可以方便的看到当前的温度和湿度信息。
首先进行硬件的设计。整体上来说,使用 ESP32-S2 作为主控,板载一个 18650电池,使用充放电模块来进行电池的管理,包括充电和放电,输出为 5V 。之后再用过TVL1117 转为 3.3V 供ESP32-S2使用。为了保证充放电模块一直工作,还设置了一个负载消耗电路,每隔一段时间拉出一个电流保证充放电模块不会因为负载过低自动断电。PCB 如下:焊接之后如下:接下来就可以进行代码编写了。和之前的设计类似,我们为了通过USB驱动 UT331,需要用USB逻辑分析仪抓取Windows 下 UT331和自带的应用程序通讯的数据。经过研究,UT331使用 USB HID 协议进行通讯,此外经过分析有如下4个需要注意的位置:1.   发送如下的 SET_REPORT 数据给设备,猜测是设置波特率,如果没有这个步骤那么程序是无法直接工作的(但是可以先运行UT331 自带的软件,然后再运行自己的代码)
对应的代码是: UCHAR WriteReportBuffer2 = {
                0x60,0x09,0x00,0x00,0x03 };
                Result = HidD_SetOutputReport(
                        hUsb,
                        WriteReportBuffer2,
                        sizeof(WriteReportBuffer2)
                );
发送打开 USB 的命令对应代码是:
// 第一个命令
        UCHAR WriteReportBuffer0 = {
                0x00,0x01,0x5A,0x00,0x00, 0x00,0x00,0x00,0x00 };
                //使用 WriteFile 发送数据
                Result = WriteFile(hUsb,
                        WriteReportBuffer0,
                        sizeof(WriteReportBuffer0),
                        &lpNumberOfBytesWritten,
                        NULL);
3.   工作起来之后,每隔语段时间需要发送COMMAND 让其保持连接,否则UT331会主动切断USB通讯。对应代码是 // 第二个命令
        UCHAR WriteReportBuffer1 = {
                0x00,0x01,0x02,0x00,0x00, 0x00,0x00, 0x00,0x00 };
                //使用 WriteFile 发送数据
                Result = WriteFile(hUsb,
                        WriteReportBuffer1,
                        sizeof(WriteReportBuffer1),
                        &lpNumberOfBytesWritten,
                        NULL);

1.      4.满足上面的调试之后,UT331就可以向USB Host 发送数据,每次收到的数据是9字节(1ReportID+ 8字节有效数据)。一个数据的例子如下:00 F0 00 00 00 00 00 00 00
00 F0 00 00 00 00 00 00 00
00 F1 00 00 00 00 00 00 00
00 F1 07 00 00 00 00 00 00
00 F1 06 00 00 00 00 00 00
00 F1 04 00 00 00 00 00 00
00 F1 02 00 00 00 00 00 00
00 F1 06 00 00 00 00 00 00
00 F1 09 00 00 00 00 00 00
00 F1 00 00 00 00 00 00 00
00 F1 08 00 00 00 00 00 00
00 F1 09 00 00 00 00 00 00
00 F1 0A 00 00 00 00 00 00
00 F1 19 00 00 00 00 00 00
00 F1 05 00 00 00 00 00 00
00 F1 00 00 00 00 00 00 00
00 F1 00 00 00 00 00 00 00
00 F1 02 00 00 00 00 00 00
00 F1 17 00 00 00 00 00 00
00 F1 0D 00 00 00 00 00 00
00 F1 0A 00 00 00 00 00 00
00 F0 00 00 00 00 00 00 00
00 F0 00 00 00 00 00 00 00
如果有效数据是 F0 开始的,那么这组数据是无效的。换句话说,我们只需要看F1 开头的,F1 后面的一个Byte 就是我们需要的数据。例如,上面的数据就是 00 07 06 04 02 06 09 00 08 09 0A 19 0500 00 02 17 0D 0A。其中 070604 表示湿度为 76.4%,温度为26.9。如果为负数的话,第五位BIT7 为1。因为使用 USB HID 进行通讯,因此我们可以直接参考 ESP32-S2 USBKeyboard 例子编写我们的代码,对数据进行解析。解析后的数据我们会上传到 Blinker 平台。之后我们使用 Blinker App 进行界面的设计。在界面上创建一个新设备,需要记下这个设备的密钥,在代码中需要使用。界面上放置2个组件用于显示温度和湿度,一个命名为 temp 另一个命名为humi。同时选中“实时数据”属性。Blinker 提供了完整的 Arduino 库,安装之后就可以在代码中使用了。最终完整代码如下:#include <elapsedMillis.h>
#include <usb/usb_host.h>
#include "show_desc.hpp"
#include "usbhhelp.hpp"

#define BLINKER_WIFI
#include <Blinker.h>

#define INTERVALINMS 10000UL

// 对应的 Key
char auth[] = "1aeca0f9ca4d";
// 用于 ESP32 通讯的 WIFI 名称
char ssid[] = "CMCC-TSR6739";
// 用于 ESP32 通讯的 WIFI 密码
char pswd[] = "!!1783az";

// 上传2个变量分别是 humi 和 temp 需要和 Blinker 界面上的对应起来
BlinkerNumber HUMI("humi");
BlinkerNumber TEMP("temp");

float CurrentHumidity = 0, CurrentTemperature = 0, LastHumidity, LastTemperature;

bool isKeyboard = false;
bool isKeyboardReady = false;
uint8_t KeyboardInterval;
bool isKeyboardPolling = false;
uint8_t DataIndex = 0;
uint8_t Data;
uint8_t DataLenth;
elapsedMillis KeyboardTimer;
elapsedMillis DataOutElsp;

const size_t KEYBOARD_IN_BUFFER_SIZE = 8;

// 接收 UT331 数据的 usb_transfer
usb_transfer_t *KeyboardIn = NULL;
// 对 UT331 发送 Setup Pakcage的 usb_transfer
usb_transfer_t *KeyboardOut = NULL;
// 设置 UT331 串口参数的 usb_transfer_t
usb_transfer_t *EnableUSBMode = NULL;
// 对 UT331 发送输出命令的 usb_transfer_t
usb_transfer_t *DataOutCommand = NULL;

void rtData()
{
// 只有上次传输的数据和当前不同时才传输数据
if ((LastHumidity != CurrentHumidity) || (LastTemperature != CurrentTemperature)) {
    Blinker.sendRtData("temp", CurrentTemperature);
    Blinker.sendRtData("humi", CurrentHumidity);
    Blinker.printRtData();
    LastHumidity = CurrentHumidity;
    LastTemperature = CurrentTemperature;
}
}

void keyboard_transfer_cb(usb_transfer_t *transfer)
{
if (Device_Handle == transfer->device_handle) {
    isKeyboardPolling = false;
    if (transfer->status == 0) {
      // 这里处理 IN 端口收到的数据
      if (transfer->actual_num_bytes == 8) {
      uint8_t *const p = transfer->data_buffer;
      ESP_LOGI("", "HID report: %02x %02x %02x %02x %02x %02x %02x %02x",
               p, p, p, p, p, p, p, p);

      // 如果第一个Byte 不是 0xF0 表示当前是有效数据
      if (p != 0xf0) {
          Serial.print("Rcev:");
          Serial.println(p, HEX);
          for (int i = 0; i < (p & 0x0F); i++) {
            Data = p;
            Serial.print(Data, HEX);
            DataIndex++;

          }
          Serial.println("");
      }
      }
    }
    else {
      ESP_LOGI("", "Keyboard boot hid transfer too short or long");
    }
}
else {
    ESP_LOGI("", "transfer->status %d", transfer->status);
}
}

void keyboard_transfer_cb2(usb_transfer_t *transfer)
{
}

void check_interface_desc_boot_keyboard(const void *p)
{
const usb_intf_desc_t *intf = (const usb_intf_desc_t *)p;

if ((intf->bInterfaceClass == USB_CLASS_HID) &&
      (intf->bInterfaceSubClass == 0) &&
      (intf->bInterfaceProtocol == 0)) {
    isKeyboard = true;
    ESP_LOGI("", "Claiming a boot keyboard!");
    esp_err_t err = usb_host_interface_claim(Client_Handle, Device_Handle,
                  intf->bInterfaceNumber, intf->bAlternateSetting);
    if (err != ESP_OK) ESP_LOGI("", "usb_host_interface_claim failed: %x", err);
}
}

void prepare_endpoint(const void *p)
{
const usb_ep_desc_t *endpoint = (const usb_ep_desc_t *)p;
esp_err_t err;

// must be interrupt for HID
if ((endpoint->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK) != USB_BM_ATTRIBUTES_XFER_INT) {
    ESP_LOGI("", "Not interrupt endpoint: 0x%02x", endpoint->bmAttributes);
    return;
}
if (endpoint->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK) {
    err = usb_host_transfer_alloc(KEYBOARD_IN_BUFFER_SIZE, 0, &KeyboardIn);
    if (err != ESP_OK) {
      KeyboardIn = NULL;
      ESP_LOGI("", "usb_host_transfer_alloc In fail: %x", err);
      return;
    }

    // 创建处理端点 IN 的 usb_transfer
    KeyboardIn->device_handle = Device_Handle;
    KeyboardIn->bEndpointAddress = endpoint->bEndpointAddress;
    KeyboardIn->callback = keyboard_transfer_cb;
    KeyboardIn->context = NULL;
    isKeyboardReady = true;
    KeyboardInterval = endpoint->bInterval;
    ESP_LOGI("", "USB boot keyboard ready");

    // 创建对UT331 发送命令的 usb_transfer
    err = usb_host_transfer_alloc(16, 0, &KeyboardOut);
    if (err != ESP_OK) {
      KeyboardOut = NULL;
      ESP_LOGI("", "usb_host_transfer_alloc OUT fail: %x", err);
      return;
    }

    // 通过 SET_REPORT 发送设置串口参数的命令
    KeyboardOut->device_handle = Device_Handle;
    KeyboardOut->bEndpointAddress = 0;
    KeyboardOut->context = NULL;
    KeyboardOut->callback = keyboard_transfer_cb2;

    KeyboardOut->data_buffer = 0x21;
    KeyboardOut->data_buffer = 0x09;
    KeyboardOut->data_buffer = 0x00;
    KeyboardOut->data_buffer = 0x03;
    KeyboardOut->data_buffer = 0x00;
    KeyboardOut->data_buffer = 0x00;
    KeyboardOut->data_buffer = 0x05;
    KeyboardOut->data_buffer = 0x00;

    KeyboardOut->data_buffer = 0x60;
    KeyboardOut->data_buffer = 0x09;
    KeyboardOut->data_buffer = 0x00;
    KeyboardOut->data_buffer = 0x00;
    KeyboardOut->data_buffer = 0x03;
    KeyboardOut->num_bytes = sizeof(usb_setup_packet_t) + 5;

    err = usb_host_transfer_submit_control(Client_Handle, KeyboardOut);
    ESP_LOGI("", "SET_REPORT [%x] [%x] %x", Client_Handle, KeyboardOut, err);
    delay(10);
    // 直接对 ENDPOINT2 发送请求数据的命令
    err = usb_host_transfer_alloc(8, 0, &EnableUSBMode);
    if (err != ESP_OK) {
      KeyboardOut = NULL;
      ESP_LOGI("", "usb_host_transfer_alloc EnableUSBMode fail: %x", err);
      return;
    }

    EnableUSBMode->device_handle = Device_Handle;
    EnableUSBMode->bEndpointAddress = 2;
    EnableUSBMode->context = NULL;

    EnableUSBMode->data_buffer = 0x01;
    EnableUSBMode->data_buffer = 0x5a;
    EnableUSBMode->data_buffer = 0x00;
    EnableUSBMode->data_buffer = 0x00;
    EnableUSBMode->data_buffer = 0x00;
    EnableUSBMode->data_buffer = 0x00;
    EnableUSBMode->data_buffer = 0x00;
    EnableUSBMode->data_buffer = 0x00;

    EnableUSBMode->num_bytes = 8;

    EnableUSBMode->callback = keyboard_transfer_cb2;
    err = usb_host_transfer_submit(EnableUSBMode);
    ESP_LOGI("", "EnableUSBMode [%x] %x", EnableUSBMode, err);

    // 准备保持连接的命令
    err = usb_host_transfer_alloc(8, 0, &DataOutCommand);
    if (err != ESP_OK) {
      DataOutCommand = NULL;
      ESP_LOGI("", "usb_host_transfer_alloc EnableUSBMode fail: %x", err);
      return;
    }

    DataOutCommand->device_handle = Device_Handle;
    DataOutCommand->bEndpointAddress = 2;
    DataOutCommand->context = NULL;

    DataOutCommand->data_buffer = 0x01;
    DataOutCommand->data_buffer = 0x02;
    DataOutCommand->data_buffer = 0x00;
    DataOutCommand->data_buffer = 0x00;
    DataOutCommand->data_buffer = 0x00;
    DataOutCommand->data_buffer = 0x00;
    DataOutCommand->data_buffer = 0x00;
    DataOutCommand->data_buffer = 0x00;

    DataOutCommand->num_bytes = 8;

    DataOutCommand->callback = keyboard_transfer_cb2;

}
else {
    ESP_LOGI("", "Ignoring interrupt Out endpoint");
}
}

void show_config_desc_full(const usb_config_desc_t *config_desc)
{
// Full decode of config desc.
const uint8_t *p = &config_desc->val;
static uint8_t USB_Class = 0;
uint8_t bLength;
for (int i = 0; i < config_desc->wTotalLength; i += bLength, p += bLength) {
    bLength = *p;
    if ((i + bLength) <= config_desc->wTotalLength) {
      const uint8_t bDescriptorType = *(p + 1);
      switch (bDescriptorType) {
      case USB_B_DESCRIPTOR_TYPE_DEVICE:
          ESP_LOGI("", "USB Device Descriptor should not appear in config");
          break;
      case USB_B_DESCRIPTOR_TYPE_CONFIGURATION:
          show_config_desc(p);
          break;
      case USB_B_DESCRIPTOR_TYPE_STRING:
          ESP_LOGI("", "USB string desc TBD");
          break;
      case USB_B_DESCRIPTOR_TYPE_INTERFACE:
          USB_Class = show_interface_desc(p);
          check_interface_desc_boot_keyboard(p);
          break;
      case USB_B_DESCRIPTOR_TYPE_ENDPOINT:
          show_endpoint_desc(p);
          if (isKeyboard && KeyboardIn == NULL) prepare_endpoint(p);
          break;
      case USB_B_DESCRIPTOR_TYPE_DEVICE_QUALIFIER:
          // Should not be config config?
          ESP_LOGI("", "USB device qual desc TBD");
          break;
      case USB_B_DESCRIPTOR_TYPE_OTHER_SPEED_CONFIGURATION:
          // Should not be config config?
          ESP_LOGI("", "USB Other Speed TBD");
          break;
      case USB_B_DESCRIPTOR_TYPE_INTERFACE_POWER:
          // Should not be config config?
          ESP_LOGI("", "USB Interface Power TBD");
          break;
      case 0x21:
          if (USB_Class == USB_CLASS_HID) {
            show_hid_desc(p);
          }
          break;
      default:
          ESP_LOGI("", "Unknown USB Descriptor Type: 0x%x", bDescriptorType);
          break;
      }
    }
    else {
      ESP_LOGI("", "USB Descriptor invalid");
      return;
    }
}
}

int tmp;
long int Elsp;
boolean Lighten=false;

void setup()
{
//Serial.begin(115200);
usbh_setup(show_config_desc_full);

BLINKER_DEBUG.stream(Serial);
BLINKER_DEBUG.debugAll();

Blinker.begin(auth, ssid, pswd);
Blinker.attachRTData(rtData);

Elsp=millis();
// LED2 控制
pinMode(33,OUTPUT);
digitalWrite(33,LOW);
// 电力消耗引脚
pinMode(15,OUTPUT);
digitalWrite(15,LOW);
}

void loop()
{
// 处理 USB 事件
usbh_task();
// 处理 Blinker 事件
Blinker.run();

// 输入 r 重启,用于调试
while (Serial.available()) {
    char c = Serial.read();
    if (c == 'r') {
      Serial.println("Reset command");
      esp_restart();
    }
}
// 当前设备已经 Ready 并且到达轮询时间,那么查询数据
if (isKeyboardReady && !isKeyboardPolling && (KeyboardTimer > KeyboardInterval)) {
    KeyboardIn->num_bytes = 8;
    esp_err_t err = usb_host_transfer_submit(KeyboardIn);
    if (err != ESP_OK) {
      ESP_LOGI("", "usb_host_transfer_submit In fail: %x", err);
    }
    ESP_LOGI("", "KB Polling", err);
    isKeyboardPolling = true;
    KeyboardTimer = 0;
}

// 每隔1秒,并且当前未进行数据接收对设备发送一次请求输出的命令
if ((DataOutElsp > 1000) && (DataIndex == 0)) {
    esp_err_t err = usb_host_transfer_submit(DataOutCommand);
    ESP_LOGI("", "DataOutCommand [%x] %x", DataOutCommand, err);
    DataOutElsp = 0;
}

// 如果当前的 Buffer 中出现了 0x0a 0x0d 结尾,那就说明数据接收完成
if ((DataIndex > 1) && (Data == 0x0d) && (Data == 0x0a)) {
    Serial.print("Raw:");
    for (int i = 0; i < DataIndex - 1; i++) {
      Serial.print(Data, HEX);
      Serial.print(",");
    }
    Serial.println("");


    if (Data != 0) {
      Serial.print(Data & 0xF0, HEX);
    }
    tmp = (Data & 0x0F) * 100 + (Data & 0x0F) * 10 + (Data & 0x0F);
    CurrentHumidity = round(tmp / 10);

    Serial.print("Humidity:");
    Serial.print(Data & 0x0F, HEX);
    Serial.print(Data & 0x0F, HEX);
    Serial.print('.');
    Serial.print(Data & 0x0F, HEX);
    Serial.println("%,");

    Serial.print("Temperature:");

    if ((Data & 0x0F) > 9) {
      Data = 0;
    }
    Serial.print(Data & 0x0F, HEX);
    Serial.print('.');
    Serial.print(Data & 0x0F, HEX);
    Serial.println("C");

    tmp = (Data & 0x0F) * 100 + (Data & 0x0F) * 10 + (Data & 0x0F);
    if(Data & 0x80 == 0) {
      Serial.print('-');
      tmp = -tmp;
    }
    CurrentTemperature = round(tmp / 10);

    DataIndex = 0;

    BLINKER_LOG("Humidity: ", CurrentHumidity, " %");
    BLINKER_LOG("Temperature: ", CurrentTemperature, " *C");

}

if (millis()-Elsp>INTERVALINMS) {
      Elsp=millis();
      Lighten=true;
      digitalWrite(15,HIGH);
      digitalWrite(33,HIGH);
    }
if ((Lighten)&&(millis()-Elsp>100)) {
      Lighten=false;
      digitalWrite(15,LOW);
      digitalWrite(33,LOW);   
    }
}
从测试上来看完全能够满足要求,如果对实时性要求更高,可以尝试直接使用Blinker库支持的蓝牙连接。参考:
1.https://www.sohu.com/a/621693178_121480559











zoologist 发表于 2024-3-4 13:47:28

电路图和 PCB:


Arduino 代码:


工作的视频:https://www.bilibili.com/video/BV1NW421P7Py/

zoologist 发表于 2024-12-9 18:01:14

本文提到的源代码
页: [1]
查看完整版本: UT331 有线温湿度计转无线