ЦИФРОВОЙ USB ТЕРМОДАТЧИК
ODTEMP-1
Маленький размер и лёгкое подключение
Удобный интерфейс и богатый функционал
Множество задач опирается на температуру, как на один из важнейших показателей.
Например, по температуре устройства можно определить режим его работы, по температуре помещения - включить вентиляцию или обогрев.

Цифровой USB термометр ODTEMP-1 позволяет определять температуру окружающей среды и передавать значения в удобном для пользователя виде.

Данные можно считывать, как в графическом виде, так и в текстовом, а также передавать по сети, что делает его удобным участником мира IoT.
Характеристики
1. Интерфейс подключения: USB
2. Шаг измерения температуры: 0.5С
3. Габариты: 13х18х58мм
4. Программа под Win7+/Linux/macOS
5. Возможность передавать показания по сети с помощью управляющей программы.

Содержание
Подробная инструкция с картинками
  • Установка USB термометра ODTEMP-1
  • Web-версия программы на технологии PWA (Progressive Web Application) - может работать, как онлайн, так и локально на ПК.
    Работает с браузерами на движке Chromium (Chrome/Edge/Opera).
  • odtemp-logger: мини-программа для сохранения логов температуры в файл и/или показа на экране. Работает в графическом и консольном режиме через USB HID интерфейс (не нужен драйвер последовательного порта).
  • Для случаев, когда драйвер из ОС не заработал
  • Примеры работы с устройством без программ с помощью BASH/BAT-скриптов или Python.
  • Пример запуска ODTEMP-1 с Zabbix в репозитории.
  • Плагин для работы ODTEMP-1 с FanControl в репозитории.
  • Дополнительные материалы: документация на устройства, ссылки на программы.
Установка USB термометра ODTEMP-1
1. Термодатчик устанавливается в USB разъём управляющего ПК.
2. При выборе места установки следует избегать способов подключения, при которых сам датчик нагревается от материнской платы ПК, т.к. это может негативно сказаться на точности показаний.
3. Для работы с устройством доступно кроссплатформенное ПО. Также можно работать с помощью BAT/BASH скриптов.
ODTEMP Web
Онлайн версия монитора-логгера
Мы разработали небольшую программу для максимальной легкого начала работы с нашим устройством.
Программа доступна на https://odtemp.unitx.pro
Программа позволяет мониторить температуру, вести небольшой лог и изменить настройки опроса датчика или перевести устройство в загрузчик.
Но нужно иметь в виду - это web-приложение и ОС будет относиться к нему соответствующе: притормаживать, останавливать и выгружать по своему усмотрению.
Поэтому web-приложение подходит для периодического мониторинга, а для постоянной работы нужно использовать десктопную версию.
ODTEMP-logger
Маленькая программа для показа температуры и сохранения логов температуры в файл.
Работает через USB HID интерфейс.
Не требует настроек и драйверов.
Скачайте под свою ОС по ссылке.
Исходный код на Github.
Оконный режим программы odtemp-logger
В графическом режиме программа отображает окно для удобного визуального контроля.

Режимы работы определяются параметрами командной строки:
-cli
запуск без GUI
-path <строка>
переопределяет путь записи лога
-period <число>
период записи в секундах
-silent
не писать лог
Окно вывода консольной программы odtemp-logger
При каждом запуске программы создается новый лог с текущим временем.

Программа работает через HID интерфейс, так что ей не понадобятся CDC драйвера для устройства.

В Linux, вероятно, понадобится отдельное udev-правило (либо повышение прав для программы).

С версии 1.5S прошивки и 1.1.0 программы можно установить период меньше 2 с (до 0.2).
Пример создания ярлыка с параметрами в Windows
Программа сохраняет данные каждые 60 секунд.
Время можно изменить параметром -period X, где X - время в секундах между запросами.
Например odtemp-logger -period 5 -cli задаст команду на опрос каждые 5 секунд.

На картинке пример создания ярлыка в Windows с параметром программы (выделено синим).
Про тайминги в логах
Проблема с "плывущими" таймерами в логах программы - это известная особенность работы в операционной системе Windows.

Причины нестабильности интервалов
1. При низкой активности система может переводить CPU в режим энергосбережения, что приводит к задержкам в выполнении программы и сдвигам временных интервалов. Может быть актуально также для свернутой в фон программы или для заблокированного экрана.
2. Операционная система распределяет процессорное время между всеми запущенными приложениями. При высокой загрузке системы программа может получать процессорное время с задержками, что влияет на точность таймеров.

Рекомендации по устранению:
Закройте ненужные программы во время работы с нашим ПО
Проверьте план электропитания - установите режим "Высокая производительность"
Отключите энергосбережение для USB-устройств в Диспетчере устройств
Проверьте загрузку процессора и оперативной памяти через Диспетчер задач

Ожидаемое поведение
Колебания интервала записи в пределах ±2-3 секунд являются нормальными для Windows-приложений.
CDC драйвер
В Windows 8, Windows 10, Linux, macOS установка CDC драйвера не требуется.
Перед работой с уcтройством в Windows7 понадобится установить драйвер CDC.
Подробнее про установку CDC-драйверов по ссылке.

Если вы всё же хотите запустить устройство на другом оборудовании - напишите нам и мы расскажем, возможно ли это сделать!
Скрипты и примеры
Для тех, кто хочет использовать текстовый протокол для интеграции со своим программным обеспечением
Получить температуру/температуру-влажность
Однократно получить значение темературы
Запрос:
~G

Ответ:
 ~G25.0
где 25.0 - температура в градусах Цельсия
Однократно получить значение темературы-влажности
Запрос:
~G

Ответ:
~G26.6;13.5
где 26.6 - температура в градусах Цельсия, 13.5 - измеренная влажность в процентах
Задать передачу температуры/температуры-влажности по таймеру
Термодатчик будет передавать значение температуры раз в 1000 мс (раз 1 секунду).
Настройки режима работы сохраняются во внутренней памяти.
Запрос:
~W1000

Ответ:
~F1000
~G24.0
~G24.0
...
Передача параметров с помощью echo
Пример загрузки параметров с помощью echo в Linux
echo "~W1000" > /dev/ttyACM0
Передача данных по сети в UDP формате (Linux)
В режиме автоматической передачи значений термодатчиком
cat USBPORT | sed 's/~G//' | socat - udp-sendto:127.0.0.1:5000
Однострочный скрипт для выполнения каких-либо действий при превышении температуры для датчиков температуры (Linux)
В режиме автоматической передачи значений термодатчиком. Сравнивается с 30 градусами.
cat /dev/ttyACM1 | sed 's/~G//' | { read temp; if [[ -z "$temp" ]]; then echo 'No data'; elif [[ $(echo "$temp>30" | bc -l) -ne 0 ]]; then echo 'Overtemp'; else echo 'Normal'; fi; }
Однострочный скрипт для выполнения каких-либо действий при превышении температуры для датчиков температуры(Windows Powershell)
В режиме автоматической передачи значений термодатчиком. Сравнивается с 30 градусами.
$port = new-object System.IO.Ports.SerialPort COM8, 9600, None, 8, One; $port.Open(); $data = $port.ReadLine(); $port.Close(); if (-not $data) { "no data" } else { $temp = $data -replace '~G', ''; if ([float]$temp -gt 30) { "Overtemp" } else { "Normal" } }
Остановить автоматическую передачу температуры/температуры-влажности
Термодатчик перестанет передавать значение температуры раз в ХХ мс и будет передавать только по запросу ~G.
Настройки режима работы сохраняются во внутренней памяти.
Запрос:
~W0

Ответ:
~F0
Работа с устройством по сети через socat
Пример socat tcp-сервера, который подключается к порту ACM0 устройства, обеспечивает прозрачную передачу данных и не требует установки автоматического режима
socat -d -d TCP4-LISTEN:6000,fork /dev/ttyACM0,raw,echo=0
Получение температуры, температуры-влажности
с помощью Python
Пример программы для получения температуры от датчика. Не забудьте установить pyserial.

Запуск в терминале:
python3 odtemp_poll.py --port /dev/ttyUSB0 --interval 1

Пример вывода:
[16-02-2026 12:10:01] Температура: 24.63 °C, Влажность: 41.20 %
[16-02-2026 12:10:02] Температура: 24.62 °C
[16-02-2026 12:10:03] Ошибка получения данных с сенсора
import serial
import time
import argparse
import datetime


def parse_g_response(data: str):
    # Ожидаемые форматы:
    # ~GE
    # ~G<temp>
    # ~G<temp>;<hum>
    if not data.startswith("~G"):
        return ("unexpected", data)

    payload = data[2:]

    if payload == "E":
        return ("error", None)

    try:
        if ";" in payload:
            temp_str, hum_str = payload.split(";", 1)
            temp = float(temp_str)
            hum = float(hum_str)
            return ("temp_hum", (temp, hum))
        else:
            temp = float(payload)
            return ("temp_only", temp)
    except ValueError:
        return ("unexpected", data)


def main():
    parser = argparse.ArgumentParser(description="Опрос ODTEMP командой ~G")
    parser.add_argument(
        "--port",
        required=True,
        help="Последовательный порт (например, COM3 или /dev/ttyUSB0)",
    )
    parser.add_argument(
        "--interval",
        type=float,
        default=1.0,
        help="Интервал опроса в секундах (по умолчанию: 1.0)",
    )
    parser.add_argument(
        "--read-timeout",
        type=float,
        default=1.0,
        help="Таймаут чтения из порта в секундах (по умолчанию: 1.0)",
    )
    args = parser.parse_args()

    ser = serial.Serial(args.port, 115200, timeout=args.read_timeout)

    try:
        print(f"Подключено к {args.port}")
        print(f"Опрос каждые {args.interval} сек")

        while True:
            current_time = datetime.datetime.now().strftime("%d-%m-%Y %H:%M:%S")

            ser.write(b"~G")
            data = ser.readline().decode("utf-8", errors="replace").strip()

            kind, value = parse_g_response(data)

            if kind == "error":
                print(f"[{current_time}] Ошибка получения данных с сенсора")
            elif kind == "temp_hum":
                temp, hum = value
                print(f"[{current_time}] Температура: {temp:.2f} °C, Влажность: {hum:.2f} %")
            elif kind == "temp_only":
                print(f"[{current_time}] Температура: {value:.2f} °C")
            else:
                print(f"[{current_time}] Неожиданный ответ: {value}")

            time.sleep(args.interval)

    except KeyboardInterrupt:
        print("\nОпрос остановлен пользователем")
    except Exception as e:
        print(f"Ошибка: {e}")
    finally:
        ser.close()
        print("Соединение закрыто")


if __name__ == "__main__":
    main()
Python-аналог консольной программы odtemp-logger

Пример небольшой программы для сохранения данных в лог, который работает с интерфейсом HID вместо CDC.

Для работы нужен python3 и библиотека hidapi.

• #!/usr/bin/env python3
  # -*- coding: utf-8 -*-

  import argparse
  import datetime
  import hid
  import re
  import sys
  import time

  # Константы устройства
  OD_VID = 0x0483
  OD_IOT_PID = 0xA26A

  # HID report IDs
  HID_DATA_REPORT_ID = 1
  HID_EVENT_REPORT_ID = 2
  HID_FW_REPORT_ID = 3
  HID_CMD_REPORT_ID = 4

  SENSOR_STATES = {
      0: "NORMAL",
      1: "ACCEPTABLE",
      2: "CRITICAL",
      3: "INTERROR",
      4: "CUSTOM",
  }


  def now_str() -> str:
      return datetime.datetime.now().strftime("%d-%m-%Y %H:%M:%S")


  def available_devices():
      """
      Возвращает список найденных OD HID-устройств.
      """
      devices = hid.enumerate(OD_VID, OD_IOT_PID)
      found_devices = []
      iot_product = re.compile(r"IOT\s+(.+?)(?:\s+HID)?$", re.IGNORECASE)

      for idx, d in enumerate(devices):
          vendor = d.get("manufacturer_string") or ""
          product = d.get("product_string") or ""
          serial_raw = d.get("serial_number") or ""
          serial_show = serial_raw or "S/N"
          path = d.get("path")

          print(f"[{idx}] Найдено устройство: {vendor} / {product} @ {serial_show}")

          match = iot_product.match(product.strip()) if product else None
          if vendor == "Open Development" and match and path:
              found_devices.append(
                  {
                      "index": idx,
                      "device_type": match.group(1).strip(),
                      "serial": serial_raw,
                      "serial_show": serial_show,
                      "path": path,
                      "vendor": vendor,
                      "product": product,
                  }
              )
          else:
              print("  -> Устройство не соответствует критериям OD HID")

      return found_devices


  def choose_device(devices, serial_number=None, index=None):
      if not devices:
          return None

      if serial_number:
          for d in devices:
              if d["serial"] == serial_number or d["serial_show"] == serial_number:
                  return d
          print(f"Устройство с серийным номером {serial_number} не найдено, используем первое
  доступное")

      if index is not None:
          if 0 <= index < len(devices):
              return devices[index]
          print(f"Индекс {index} вне диапазона 0..{len(devices)-1}, используем первое доступное")

      return devices[0]


  def open_device(device_info):
      """
      Открывает HID-устройство по path.
      """
      dev = hid.device()
      dev.open_path(device_info["path"])
      return dev


  def detect_humidity_capability(dev):
      """
      Пытается определить поддержку humidity через Feature Report (ID=4).

      Для temp+hum (например DHT/HDC) в payload есть поля влажности,
      и байты [17:23] обычно не нулевые.
      """
      try:
          fr = bytes(dev.get_feature_report(HID_CMD_REPORT_ID, 64))
          if not fr:
              return None
          if fr[0] != HID_CMD_REPORT_ID:
              return None

          if len(fr) >= 23:
              if any(fr[17:23]):
                  return True
              return None

          if len(fr) <= 16:
              return False

          return None
      except Exception:
          return None


  def parse_data_report(payload, has_humidity_capability=None):
      """
      DATA report payload после report_id.

      Протокол:
        - temp-only: [temp_lo temp_hi]
        - temp+hum : [temp_lo temp_hi hum_lo hum_hi]

      Значения в 0.01 единицах (int16 little-endian).
      """
      if len(payload) < 2:
          return None

      temp_raw = int.from_bytes(payload[0:2], byteorder="little", signed=True)
      temp = temp_raw / 100.0

      hum = None
      if len(payload) >= 4:
          hum_raw = int.from_bytes(payload[2:4], byteorder="little", signed=True)

          if has_humidity_capability is True:
              if 0 <= hum_raw <= 10000:
                  hum = hum_raw / 100.0
          elif has_humidity_capability is None:
              if 0 <= hum_raw <= 10000 and any(payload[2:4]):
                  hum = hum_raw / 100.0

      return {"temperature": temp, "humidity": hum}


  def process_custom_command(cmd, payload):
      print(f"[{now_str()}] Получена нестандартная команда: cmd=0x{cmd:02X},
  payload={payload.hex()}")


  def sensor_state_changed(state):
      state_name = SENSOR_STATES.get(state, f"UNKNOWN({state})")
      print(f"[{now_str()}] Изменение состояния сенсора: {state_name}")


  def decode_fw_report(payload):
      if not payload:
          return ""

      length = payload[0]
      raw = payload[1: 1 + length]

      try:
          return raw.decode("latin1", errors="replace")
      except Exception:
          return raw.hex()


  def main():
      parser = argparse.ArgumentParser(description="Опрос OD HID датчиков")
      parser.add_argument("--serial", help="Серийный номер устройства", default=None)
      parser.add_argument("--index", type=int, default=None, help="Индекс устройства из списка
  enumerate")
      parser.add_argument("--read-timeout-ms", type=int, default=100, help="Таймаут чтения HID в
  мс")
      parser.add_argument("--device-timeout", type=float, default=10.0, help="Таймаут молчания
  устройства, сек")
      args = parser.parse_args()

      devices = available_devices()
      if not devices:
          print("Устройства не найдены")
          sys.exit(0)

      device_info = choose_device(devices, serial_number=args.serial, index=args.index)
      if not device_info:
          print("Подходящее устройство не найдено")
          sys.exit(1)

      print(f"\nОткрываем устройство: {device_info['device_type']}
  ({device_info['serial_show']})")
      dev = open_device(device_info)

      try:
          has_humidity_capability = detect_humidity_capability(dev)
          last_data_time = time.time()

          while True:
              try:
                  report = dev.read(64, args.read_timeout_ms)
                  if report:
                      report = bytes(report)

                      # Некоторые стеки могут вернуть ведущий 0 перед report_id
                      if len(report) > 1 and report[0] == 0 and report[1] in (
                          HID_DATA_REPORT_ID,
                          HID_EVENT_REPORT_ID,
                          HID_FW_REPORT_ID,
                          HID_CMD_REPORT_ID,
                      ):
                          report = report[1:]

                      report_id = report[0]
                      payload = report[1:]
                      last_data_time = time.time()

                      if report_id == HID_DATA_REPORT_ID:
                          value = parse_data_report(payload,
  has_humidity_capability=has_humidity_capability)
                          if value is None:
                              print(f"[{now_str()}] Некорректный DATA report: {report.hex()}")
                              continue

                          temp = value["temperature"]
                          hum = value["humidity"]

                          # Дополнительное автоопределение по фактическому payload
                          if has_humidity_capability is None and hum is None and len(payload) >=
  4:
                              hum_raw = int.from_bytes(payload[2:4], byteorder="little",
  signed=True)
                              if 0 <= hum_raw <= 10000 and any(payload[2:4]):
                                  hum = hum_raw / 100.0
                                  has_humidity_capability = True
                                  print(f"[{now_str()}] Автоопределение: обнаружен канал
  влажности")

                          if hum is None:
                              print(f"[{now_str()}] Температура: {temp:.2f} °C")
                          else:
                              print(f"[{now_str()}] Температура: {temp:.2f} °C, Влажность:
  {hum:.2f} %")

                      elif report_id == HID_EVENT_REPORT_ID:
                          if payload:
                              sensor_state_changed(payload[0])
                          else:
                              print(f"[{now_str()}] Пустой EVENT report")

                      elif report_id == HID_FW_REPORT_ID:
                          fw = decode_fw_report(payload)
                          print(f"[{now_str()}] [FW] Версия прошивки: {fw}")

                      elif report_id == HID_CMD_REPORT_ID:
                          if payload:
                              process_custom_command(payload[0], payload[1:])
                          else:
                              print(f"[{now_str()}] Получен пустой CMD report")
                      else:
                          print(f"[{now_str()}] Неизвестный report id: {report_id}
  ({report.hex()})")

                  if (time.time() - last_data_time) > args.device_timeout:
                      print(f"[{now_str()}] Устройство не отвечает более {args.device_timeout:.1f}
  секунд")
                      break

              except IOError as e:
                  print(f"[{now_str()}] Ошибка чтения: {e}")
                  time.sleep(0.5)

      except KeyboardInterrupt:
          print("\nПрерывание пользователем")
      finally:
          dev.close()
          print("Устройство закрыто")


  if __name__ == "__main__":
      main()
Обновление устройства
Для перевода в режим загрузчика нужно подключиться по CDC интерфейсу и отправить команду "~D".
Можно это сделать через скрипт, например, bat-файл для Windows (укажите в нем COM-порт вашего устройства) или через ODTEMP WEB.
После чего запустится загрузчик и далее по инструкции.
Файлы прошивок
Дополнительные ресуры
  1. Техподдержка,
  2. Инструкция на устройство в pdf.
  3. Инструкция по обновлению.