Python, последовательный порт и нуль-модемный кабель

Пару дней назад в комментариях к статье «Последовательный порт. Да, поможет нам Python!» http://wp.me/p1H7g0-Mk мне был задан конкретный вопрос на тему соединения двух компьютеров через последовательный порт.

Собственно, проблема соединения двух компов посредством последовательного порта не совсем простая. Особенно для тех, кто ранее с этим не сталкивался. Вот для них я проведу маленький ликбез.

Поскольку в настоящее время я пребываю в праздном ничегонеделании и свободного времени у меня как у дурака, да и материал на эту тему у меня уже кое-какой имеется, то почему бы мне в таком разе не поработать на благо общества?

Ну что, поехали!

Во первых, последовательный порт (COM:) в контексте Виндовс и последовательный порт (/dev/ttyS0) в контексте Линукс — это довольно-таки разные вещи. (Советую пройтись по приведённой выше ссылке и прочитать мой комментарий по этому вопросу.) Соответственно, и методы работы с последовательным портом могут существенно отличаться в этих операционках.

Во вторых, последовательный порт предполагает два независимых друг от друга информационных потока, направленных в противоположные стороны. Это обстоятельство диктует основополагающее правило для создания программ по обмену данными — программы должны быть многопоточными. Пожалуйста, обратите внимание, что я иногда говорю об информационных потоках, а иногда говорю о потоках исполнения. Я стараюсь уточнять, о каких потоках идёт речь. Так что, постарайтесь не путаться в одноимённых терминах!

Грубо говоря: один исполняющий поток нужен для отслеживания и обработки действий пользователя (прием с клавиатуры, отображение на экране, отправка исходящих сообщений в порт). Другой исполняющий поток нужен для ожидания приёма входящих сообщений. Если задачу постараться решать в рамках однопоточной модели (один поток исполнения), то получится очень корявая реализация идеи в программном коде. Будет выглядеть, словно вы снова окунулись в мир DOS.

Как во времена DOS строились такие программы? А вот примерно по такому алгоритму:

while (true)
{
  if (key_pressed() == true)
    proc_keyboard();
  else if (mouse_event() == true)
    proc_mouse();
  else if (inputCOMport() == true)
    proc_COMport();
  ...
}

Ну, я думаю, суть понятна. В бесконечном цикле опрашивались источники информации, и если у них там что-то было, то вызывался соответствующий обработчик. Так строились многие DOS-овские программы и всё как-то работало. Но если какой-нибудь обработчик события захватит время процессора более, чем нужно, то программа временно подвисала. Оператор (тот, кто работал на компьютере) с дуру мог подумать, что нажатие на клавишу не отработало и ещё несколько раз нажать.

Потом, когда обработчик заканчивал свою работу, программа внезапно вываливала целую кучу нажатий. Называется «весёлая бухгалтерия» готовит готовой отчёт. Матерятся все!

Слава богу времена DOS давно канули в лету. Линукс и Виндовс — это многозадачные операционные системы, которые способны одновременно выполнять многие сотни потоков (исполнения). Было бы уж совсем глупо не пользоваться этим инструментом, да?

Хорошо. Давайте рассмотрим проблему построения программы для связи двух компов с другой точки зрения. Мы знаем, что программы бываю графические и бывают программы для работы в текстовом терминале. В лагере Виндовс, последние называю «консольными». Опять же, консоль в контексте Виндовса, и консоль в контексте Линукса — это немного разные понятия. То есть понятие «консольная программа» для Линукса это немного не то, что подразумевается в Виндовсе. Те программы, которые в контексте Виндовс принято называть «консольными», в среде Линукса — это просто программы. То есть обычные программы для работы в текстовой оболочке. Называть вещи терминами из другой среды — это по меньшей мере безграмотно. А по большей — безответственно, так как происходит подмена понятий, и вслед на ней люди перестают понимать вообще саму суть. Поэтому так сложно недавним пользователям Виндовс ориентироваться в Линуксе: названия вроде такие же, но ничего не работает, и куда копать не понятно. На ум приходит метафора Вавилонской башни!

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

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

Надеюсь, я не очень утомил вас банальными вещами. Но это нужно было сказать, потому как…

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

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

Можно, конечно, постараться «подсветить» разными цветами текст от пользователя и текст с другого компа, но устранить перемешивание текста — принципиально нельзя. Это хоть как-то поможет идентифицировать разные тексты. Но это очень далеко от панацеи.

Правда, есть ещё один вариант — «расколоть» текстовый экран на зоны (окна). Этот приём широко использовали во времена DOS, но, ребята! Мы в каком веке-то живём?

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

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

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

Программу будем писать на языке Питон, причем в версии 2.7. Программа будет строиться с учетом принципов ООП и использованием классов. Возможно, для кого-то это будет несколько трудно, но лучше сразу привыкать писать такие программы правильно.

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

Такие вторичные исполняющие потоки принято ещё называть рабочими потоками. Обычно эти потоки закольцованы с помощью бесконечного цикла. Окончание работы нашего потока происходит в момент выхода из программы.

Второй файл — главный файл программы. С него начинает работать программа. Можно сказать — это ничто иное как главный исполняющий поток программы. Если бы это была графическая программа, то такой поток назывался бы главным графическим потоком. Но давайте оставим вопросы графики на потом!

В этом файле создается экземпляр класса (из первого файла). Затем основной исполняющий поток так же входит в бесконечный цикл. Поток получает данные с клавиатуры и при нажатии на Enter отправляет их классу, отвечающему за работу с последовательным портом. Выход из потока осуществляется набором на клавиатуре слова «quit». Выход из цикла заканчивает работу программы.

Первый файл называется conn.py, у второго более гламурное название — tet-a-tet.py. Ниже приведены тексты этих файлов.

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

''' conn.py '''

import threading
import serial

#===============================================================================
class Conn(threading.Thread):

  '''
  Конструктор соединения.
  Открываем и настраиваем последовательный порт.
  '''
  def __init__(self, port = "/dev/ttyS0", baud = 115200):
    threading.Thread.__init__(self)
    self.daemon = True

    try:
      self.ser = serial.Serial(port)
    except: # SerialException:
      print "Проблема: не могу открыть порт", port
      exit(0)

    self.ser.baudrate = baud
    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) >= 1:
          if answer[-1:] == "\n":    # Окончание строки
            print answer[:-1]        # Выводим строку на терминал. Символ '\n'
                                     #   будет добавлен оператором print
            answer = ""              # Заготовка для новой строки          

  '''
  Отправка текстового сообщения на другой комп
  '''
  def send(self, msg):
    self.ser.write(msg)

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

# Задайте правильные для вашего компа параметры:
PORT = "/dev/ttyUSB0"
BAUD = 115200

import conn

################################################################################
if __name__ == "__main__":
  conn = conn.Conn(PORT, BAUD)
  conn.start()

  print "Связь с другим компом через нуль-модемный кабель."
  print "Для отправки сообщения на второй комп наберите сообщение и нажмите Enter."
  print "Для выхода из программы наберите слово QUIT и нажмите Enter."

  while True:
    message = raw_input(">")

    if message.upper().strip() == "QUIT":
      break
    else:
      conn.send(message + "\n")

Файлы выложены в репозиторий на Гитхаб https://github.com/zhevak/tet-a-tet.git.

Команда для клонирования репозитория к себе на комп:

$ git clone https://github.com/zhevak/tet-a-tet.git

Работа файлов была проверена в среде Линукс. Я соединил два компа через USB-UART конвертеры и немного поигрался. Программа работает.

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s