Как приручить свой Bluetooth. Часть 4

Продолжаем изучать мир многопоточности.

Вдоволь наиграшись с потоками можно уже переходить к созданию чего-нибудь полезного. На повестке дня — создание программы для работы с модулем Bluetooth типа HC-3 (HC-5).
bluetooth HC05

Как я уже говорил ранее — при создании многопоточной программы функция каждого потока пишется так, как будто бы в программе работает только одна, других потоков нет.

Таким образом, давайте напишем функцию потока, который принимает ответы от модуля Bluetooth и печатает их на экране. Но прежде чем что-то писать, давайте обозначим проблему.

Множество проблем являются таковыми только потому, что они не видны до поры до времени, а когда они внезапно появляются из ниоткуда, возникает паника — «что делать!» Таким образом, зная о неожиданных трудностях заранее, можно понизить их уровень проблемности. Это хорошая профилактика — обзор возможных проблем — для плодотворной результативной работы.

Так вот, наша трудность работы с модулем Bluetooth и выводом от него ответов на консоль состоит в том, чтобы сделать так, чтобы минимизировать пересечение вывода на экран дух потоков. Это легко решается, если вывод на экран будет осуществляться не по одному символу за раз, а целой строкой.

К сожалению, с вводом с клавиатуры мы ничего не сможем сделать — ввод всегда осуществляется по-символьно (по-клавишно). А вот с ответами от Bluetooth мы можем кое-что сотварить.

Известно, что каждый ответ от модуля заканчивается символами «\r\n». Таким образом, получив очередной символ от модуля, мы можем не выводить его тут же на консоль, а накапливать в буфере. И как только в полученных от модуля строке символов образуется последовательность из этих двух символом, мы выведем это буфер на консоль и очистим для следующей строки. Улавливаете, да?

Вообще одно-байтовый обмен между устройствами, соединенными линиями связи, не очень эффективен. При одно-байтовом обмене очень велики накладные расходы. Намного эффективнее обмениваться группами байтов — пакетами.

Весь вопрос в том, как определить начало и конец пакетов в нескончаемом потоке байтов.

Если обмен информацией основан на двоичном формате данных, то удобно конец пакета определять по паузе, которая превышает заданный промежуток времени. В последовательном канале (UART, RS232) обычно принят интервал времени, равный полуторной длительности передачи байта. Если за это время не пришёл очередной байт, то можно считать, что передача пакета закончена.

Но обычно этого способа определения начала-конца пакетов недостаточно. Конец пакета так же определяется по по количеству байт в пакете. Количество байт в пакете может быть жёстко фиксированным, а может передаваться в начале пакета. Всё зависит от системы передачи информации.

В системах обмена текстовой информацией — ещё проще. Количество различный комбинаций у 8-битовой (байтовой) посылки  равно 256, а количество символов — значительно меньше. Поэтому для обозначения конца и начала текстового пакета можно использовать непечатные символы. Ну, например, использовать символ Esc (0x1B) или какой-нибудь другой для обозначения начала пакета.

Но даже и это лишнее. Дело в том, что пара символов «\r\n», идущих друг за другом — это достаточно надежный признак конца одного пакета и начала следующего.

Таким образом, как только в тестовой последовательности от Bluetooth нам встретиться комбинация «\r\n», мы можем смело утверждать, что это конец пакета. Другими словами: одна строка — один пакет.

Это с одной стороны. С другой стороны, возникают проблемы при вызове функции получения данных от последовательного порта. Функция чтения read() не способна проанализировать наличие в последовательности цепочки байт «\r\n».

Функция возвращает управление только тогда, когда получит заданное в её параметре количество байт. Например, функция read(5), не вернёт управление, пока не получит пять байт.

Можно, конечно, задать время ожидания (timeout), но и это тоже будет не совсем правильно. Ведь могут приходить пакеты разные по дине. А если между получаемыми байтами в пакете будут возникать паузы, как угадать заранее, что пакет закончился?

В общем самым простым является указание таймаута равным нулю, что означает, что функция будет ожидать получение байта бесконечно долго, и указания ей (т.е. — функции) параметра, равного 1 (получение одного байта). А после возврата из функции и записи полученного значения в буфер проверять последние два байта в буфере на соответствие «\r\n».

Питон это описывает значительно лаконичнее:

answer = ""                      # Заготовка для новой строки
# Бесконечный цикл
while True:
  byte = str(self.ser.read())    # Читаем один байт
  if len(byte) == 1:
    answer += byte               # Составляем строку

    if len(answer) >= 2:
      if answer[-2:] == "\r\n":  # Окончание строки
        print answer[:-2]        # Выводим строку на консоль без окончания
        answer = ""              # Заготовка для следующей строки

Здесь мы крутимся в бесконечном цикле, принимая символы из последовательного порта, и складывая их в буфер answer. И как только обнаружится, что последние два символа в буфере являются символами «\r\n», распечатываем буфер на консоли и обнуляем его содержимое для приёма следующего пакета.

Заметьте, в этом потке нет даже намёка на то, что в программе где работает еще один поток.

Второй поток исполнения программы — это, собственно, главный поток. Его задача и и в предыдущей программе — запустить вторичный поток и принимать клавиатурный ввод.

  print "Для выхода из программы наберите EXIT"
  while True:
    commad = raw_input("-").upper().strip()
    if command == "EXIT":
      break
    else:
      bt.write(command)
      time.sleep(0.1)  # Небольшая задержка, чтобы успеть напечатать ответ от BT

Пользователь набирает очередную команду и нажимает клавишу «Enter». Результат клавиатурного ввода попадает в переменную command. Если окажется, что пользователь ввел «EXIT», то цикл прервётся и программа закончит свою работу. В противном случае, полученная цепочка символов будет передана в функцию write(). Эта функция является методом нашего класса потока. Она тупо передает полученную команду модулю Bluetooth. Мы скоро рассмотрим, как это происходит.

А пока обратите внимание на кратковременное засыпание на 0.1 секунды. Это сделано специально. Дело в том, что наша программа в качестве подсказки ввода очеоредной команды печатает дефис (символ ‘-‘). Но поскольку прога очень работает быстро, то практически получается, что после окончания ввода очередной команды и нажатия на «Enter» прога тут же на экран выведет подсказку для ввода следующей команды. А через 20-80 миллисекунд от Bluetooth придет ответ и он будет напечатан после подсказки — как раз на том месте, гдем мы должны вводить следующую команду. Это неправильно!

Но если мы искусственно внесём задержку в работу программы, то форматный вывод на консоль будет правильный. Задержка очень короткая, поэтому пользователь её даже не почувствует.

В предыдущей части я говорил о пяти нюансах при написании дочернего класса потока. Вот шестой нюанс.

В шестых, нам никто не запрещает вносить дополнительные методы и данные в наш дочерний класс потока. И мы этой любезностью ООП (Объектно Ориентированного программирования) воспользуемся. Вот метод, который посылает команду в модуль Bluetooth:

  def write(self, cmd):
    if len(cmd) > 0:
      cmd += "\r\n"
      # print "Команда:", cmd
      self.ser.write(cmd)

Здесь мы дописывает пакету его окончание «\r\n» и отправляем в последовательный порт. И опять же, нет никаких намеков на то, что в программе работают какой-то другой поток.

Теперь осталось утрясти вопрос с открытием последовательного порта. Это удобно сделать в конструкторе нашего класса потока:

  def __init__(self, port):
    threading.Thread.__init__(self)
    self.daemon = True
    
    try:
      self.ser = serial.Serial(port)
    except: # SerialException:
      print "Не могу открыть порт", port
      exit(0)
      
    self.ser.baudrate = 38400
    self.ser.bytesize = serial.EIGHTBITS
    self.ser.parity   = serial.PARITY_NONE
    self.ser.stopbits = serial.STOPBITS_ONE
    self.ser.timeout  = None

После создания объекта класса self.ser мы настраиваем порт на заданные параметры — скорость работы, количество стоповых бит, чётность. Указываем так же таймаут.

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

Вот полный текст программы.

#!/usr/bin/env python
#coding: utf-8

import threading
import serial
import time


################################################################################
'''
Класс дочернего потока.
Дочерний поток должен иметь атрибут self.daemon = True, иначе при завершении
главного потока он не завершиться и программа никогда не прекратит свое
выполнение
'''
class Bluetooth(threading.Thread):

  '''
  Обычный конструктор
  '''
  def __init__(self, port):
    threading.Thread.__init__(self)
    self.daemon = True
    
    try:
      self.ser = serial.Serial(port)
    except: # SerialException:
      print "Не могу открыть порт", port
      exit(0)
      
    self.ser.baudrate = 38400
    self.ser.bytesize = serial.EIGHTBITS
    self.ser.parity   = serial.PARITY_NONE
    self.ser.stopbits = serial.STOPBITS_ONE
    self.ser.timeout  = None
  
  '''
  Функция потока.
  Эта функция не должна вызываться напряму из главного потока.
  Запуск функции потока осуществляется вызовом метода start
  '''
  def run(self):
    answer = ""                      # Заготовка для новой строки
    # Бесконечный цикл
    while True:
      byte = str(self.ser.read())    # Читаем один байт
      if len(byte) == 1:
        answer += byte               # Составляем строку
        
        if len(answer) >= 2:         
          if answer[-2:] == "\r\n":  # Окончание строки
            print answer[:-2]        # Выводим строку на консоль без окончания
            answer = ""              # Заготовка для новой строки          

  '''
  Отправка команды в Bletooth
  '''
  def write(self, cmd):
    if len(cmd) > 0:
      cmd += "\r\n"
      # print "Команда:", cmd
      self.ser.write(cmd)

################################################################################
def show_help():
  print """
  Команды Bluetooth
  
  AT             : пустая команда. Ответ от BT должен быть "OK"
  AT+RESET       : программный сброс модуля BT
  AT+ORGL        : вернуться к параметрам по умолчанию
  AT+VERSION?    : поучить имя версии
  AT+STATE?      : получить состояние модуля
  AT+NAME=имя    : задаить имя модуля (не более 20 символов)
  AT+NAME?       : получить имя модуля
  AT+ROLE=роль   : установить роль
  AT+ROLE?       : получить роль                    
  AT+PSWD=пароль : задать пароль (не более 4 символов)
  AT+PSWD?       : получить пароль
  AT+UART=скорость,стопы,чётность : задать параметры работы UART
  AT+UART?       : получить параметры UART
  AT+CMODE=режим : задать режим подключения
  AT+CMODE?      : получить режим подключения
  AT+BIND=адрес  : задать MAC-адрес устройства к которому хотим присоединиться
  AT+BIND?       : получить MAC-адрес устройства
  AT+PMSAD=адрес : удалить из списка адрес подключённого к модулю устройства
  AT+RMAAD       : удалить из списка все адреса
  AT+FSAD=адрес  : искать адрес в списке подключённых к модулю устройств
  AT+ADSN?       : получить количество подключённых к модулю устройств
  AT+MRAD?       : получить MAC-адрес недавно подключённого к модулю устройства
  """
  
################################################################################
if __name__ == "__main__":

  bt = Bluetooth("/dev/ttyUSB0")
  bt.start()
  
  print "Для получения списка Bluetooth-команд наберите HELP"
  print "Для выхода из программы наберите EXIT"
  while True:
    ans = raw_input("-").upper().strip()
    if ans == "EXIT":
      break
    elif ans == "HELP":
      show_help()
    else:
      bt.write(ans)
      time.sleep(0.1)  # Небольшая задержка, чтобы успеть напечатать ответ от BT

Если вы заметили, я добавил сюда только функционал получения помощи. Я думаю, что вы теперь сами сможете разобраться что тут и как работает.

Мне будет весьма лестно, если вы воспользуетесь моими наработками и сделаете что-то полезное для себя. Даже если вы не напишите мне об этом.

Ведь смотрите — ничего трудного в этом деле нет! Программа представляет собой чуть белее сотни строк на Питоне. А как много полезного для понимания как работает Blutooth она даёт!

Я вам скажу по секрету. Работая с этой прогой я периодически ошибался — я работал с ней так, как будто работаю в консольной оболочке — в bash-e. Я периодически пытался вызвать предыдущую команду как будто программа подразумевает наличие механизма истории программ. Собственно, это толстый намёк тем, кто будет с этой программой играться. Попробуйте написать её функционал хранения истории команд.

Что ещё проге не хватает? Ну, вообще-то как-то не очень хорошо, когда имя файла-устройства в программе прописано жёстко. Это ладно, когда прога не большая, и строку «/dev/ttyUSB0″можно легко увидеть и откорректировать для себя. (Мы ведь прогу пишем не на продажи, а для себя, для работы с ней!) Но если прога начнет разрастаться, то вопрос имени файла-устройства встанет серьезно. Наиболее оптимально наверно указывать его в качестве параметра при запуске. Но я особо не уверен. Перфекционизм не нужен! Нужно удобство пользования.

Понимая принцип устройства и работы проги вы может переделать её для работы с автотестером ELM327 или еще с каким либо другим устройством. А будучи знакомыми с принципами построения многопоточных программ вы сможете даже изменить программу в плане автоматизации ввода серии команд. Или наоборот, задумаете сделать логирование (сохранение протокола работы с устройством) в файле на диске.

В общем, так или иначе, сейчас вам становится уже не нужна универсальная супер-пупер программа-терминал, которая «на все случаи жизни». Вы можете легко изменять эту и делать всё, что вам заблагорассудится!

Предвосхищая ваш вопрос по графической версии, скажу, что я не планировал её писать. Я не вижу в ней особого смысла. Эта (текстовая) версия работает же! И работает вполне сносно. Графика, конечно, будет выглядеть круче. Но ответьте себе на вопрос — вам нужно «выглядеть» или вам нужно работать?

Ну вот, пожалуй и всё!
С удовольствием прочитаю ваши критические замечания.

Реклама

2 responses to “Как приручить свой Bluetooth. Часть 4

  1. Я бы так написал:

    class Bluetooth(threading.Thread):
      def __init__(self, port, *args, **kwargs):
        super(Bluetooth, self).__init__(*args, **kwargs)
    
    • Это хорошее предложение

      Но с другой стороны оно потребует от непосвящённых больше усилий, чтобы понять что тут и для чего.

      У меня нет задачи — сделать всё канонически правильно и красиво. У меня несколько иная задача — сделать просто. Настолько просто, чтобы разработчики, читающие блог и мало знакомые с Python, понимали, что этот программный код делает.

      И тем не менее, Руслан, — спасибо за комментарий!

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s