0

Это не курсач и не диплом! Я давно уже не студент. Это попытка написать еще одну бесплатную прогу для «домашней» диагностики автомобилей. И я не очень силен в Питоне. Поэтому прошу помощи.

Исходные данные:

Имеется некоторое устройство, которое подключено к компу через USB-порт. Комп и устройство обмениваются данными в символьном виде. По сути, это классическое соединение: устройство — играет роль сервера, а комп — роль терминала. Если кому-то что-то это скажет, то это устройство — это адаптер OBD-II (On-Board Diagnostics — устройство для диагности автомобилей) ELM327.

Система работает по очень простому принципу — на запрос (команду) клиента (терминала), сервер возвращает ответ. Вот пример запроса-ответа:

> ati
ELM327 v1.5
 
> atdp
AUTO, ISO 15765-4 (CAN 11/250)

Вот так выглядит строка ответа «ELM327 v1.5» в hex виде:

45 4C 4D 33 32 37 20 76 31 2E 35 0D 0D 3E

А так строка ответа «AUTO, ISO 15765-4 (CAN 11/250)»:

41 55 54 4F 2C 20 49 53 4F 20 31 35 37 36 35 2D 34 20 28 43 41 4E 20 31 31 2F 32 35 30 29 0D 0D 3E

Во первых, в строке ответа вместо символа ‘\n’ используется символ ‘\r’. (Тут нет проблемы. Это легко исправляется.) Во вторых, ответ заканчивается симвлом ‘>’ (hex-код = 0x3E), что приводит к усложнению программного кода обработки ответа. Тут тоже нет неразрешимой проблемы.

Проблема, которую я не могу одолеть, заключается в том, что в Питоне-3 я не могу толком получить из порта строку символов. В Питоне-2.7 запрос-ответ работает без проблем.

Вот код для второго Питона:

def show_response(port):
  while True:
    resp = ''
    while True:
      ch = port.read(1)
      if len(ch) == 1:
        # print ch
        if ch == '\r':
          resp += '\n'
        else:
          resp += ch
 
      if resp[-2:] == '\n>':
        print resp[:-2],
        break

Для третьего Питона это код (поняное дело!) не подходит. Я не имею в виду замену оператора print для Питон-2 на функцию print() для Питон-3. Это всё легко правится.

Проблема — в приёме данных от последовательного порта. Если во втором Питоне достаточно сделать (упрощенно говоря) так:

ch = port.read(1)
print ch

, то в третьем Питоне функция read() возвращает не строку символов, а строку байт.

Я — Си-шник. И для меня байт— от и в Африке байт. С моей точки зрения — что байт, что ASCII-символ (не многобайтовые символы типа utf-8!!!, а именно одно-байтовый ASCII-символ из первой половины таблицы кодов) — это один и тот же набор битов. Поэтому функция read() в модуле serial для второго Питона, возвращающая строку символов, и функция read() в модуле serial для третьего Питона, возвращающая строку байт, — должны давать одинаковый результат.

Но на практике вместо строки байт «45 4C 4D 33 32 …» (ответ на команду ati во втором Питоне), в третьем Питоне я получаю всего три байта «7F BF ED». И более уже ничего не могу получить. Обмен между ELM327 и компом происходит байтами (символами ?), находящимися в диапазоне 0..0x7F. Ни какой кириллицей и не пахнет! Откуда такие странные коды — 0xBF и 0xED ?

Что я делаю не так?

Вот код для третьего Питона:

def show_response(port):
  #print('show_response')
  while True:
    resp = ''
    while True:
      b = port.read(1)
  if len(b) != 0:
    print('0x{0:02X}'.format(b[0]))
    #if ch == '\r':
    #  resp += '\n'
    #else:
    #  resp += ch
 
  if resp[-2:] == '\n>':
    print(resp[:-2], end='')
    break

Инициализация порта для второго и третьего Питона практически одинаковая:

#!/usr/bin/env python3
#coding:utf8
 
import serial
import time
from multiprocessing import Process
 
...
 
if __name__ == '__main__':
  try:
    port = serial.Serial("/dev/ttyUSB0", 38400, timeout=0.2)
  except serial.SerialException:
    print('Соединение не удалось')
    exit(1)
 
  port.flushOutput()
  port.flushInput()
 
  p1 = Process(target=show_response, args=(port,))
  p1.daemon = True
  p1.start()
  command(port)
  print('Пока-пока!')

Пакет serial устанавливал из стандартных репозиториев — на одном компе (Ubuntu-10.04) это была уставнока пакета для второго Питона ($ sudo apt-get install python-serial), на другом компе (Debian-8) — для третьего Питона (# apt-get install python3-serial).

Не понимаю, куда копать? Кто-нибудь выпишите мне волшебный пендель в нужном направлении, а то ведь так и помру, не поняв сути Питоновских махинаций с байтами.

UPDATE 08.08.2015 — 00:53

Я изменил прогу, благо она вообще микроскопическая. Сейчас тупо открывается порт, тупо дается команда девасу — ‘ati\r’ и тупо выводится на консоль всё, что сыплется из порта в ответ на эту команду — никаких процессов, никаких ухищрений, ничего лишнего! И тем не менее результат вывода на консоль точно такой же.

Вот текст этой проги:

#!/usr/bin/env python3
#coding:utf8
 
import serial
import time
 
 
if __name__ == '__main__':
  try:
    port = serial.Serial("/dev/ttyUSB0", 38400, timeout=1)
  except serial.SerialException:
    print('Соединение не удалось')
    exit(1)
 
  port.flushOutput()
  port.flushInput()
 
  # Послать запрос
  cmd = bytes('ati\r', 'utf-8')
  port.write(cmd)
 
  time.sleep(0.1)
 
  # Принять ответ и вывести его на консоль
  while True:
    print('.')
 
    resp = port.readline()
 
    # Вариант вывода 1
    if len(resp) > 0:
      print('[{0:d}] = '.format(len(resp)), end = '')
      for b in resp:
        print('0x{0:02X} '.format(b), end='')
 
    '''
    # Вариант вывода 2
    string = str(port.readline())
    if len(string) > 0:
      print('[{0:d}] = '.format(len(string)), end = '')
      for ch in string:
        print('0x{0:02X} '.format(ord(ch)), end='')
    '''
 
    '''
    # Вариант вывода 3
    string = str(port.readline())
    if len(string) > 0:
      print('[{0:d}] = '.format(len(string)), end = '')
      print(string)
   '''

Вывод по первому варианту такой (скриншот):

$ ./myOBDm2.py
.
[2] = 0x7F 0xBF .
.
.
.

Вывод по второму варианту — такой:

$ ./myOBDm2.py
.
[3] = 0x62 0x27 0x27 .
[3] = 0x62 0x27 0x27 .
[3] = 0x62 0x27 0x27 .
[3] = 0x62 0x27 0x27 .
[3] = 0x62 0x27 0x27 .
[3] = 0x62 0x27 0x27 .

Вывод по третьему варианту — такой:

$ ./myOBDm2.py
.
[3] = b''
.
[3] = b''
.
[3] = b''
.

Более детальное изучение вывода по третьему варианту намекает, что функция readline() возвращает пустую строку байтов — ведь три символа — это ничто иное как строка байтов в Питоне. Интересно, чтобы это значило? Почему serial в Питон-3 так странно работает?

Также я пробовал перебирать варианты кодировок при посылке запроса:

  # Послать запрос
  cmd = bytes('ati\r', 'utf-8') # 'cp866', 'cp1251', 'ascii'
  port.write(cmd)

Ничего не менялось, кодировка никак не влияет.

На девайсе есть светодиода, которые моргают при приёме и передаче данных по USB-интерфейсу. Судя по морганию — обмен идёт.

UPDATE 08.08.2015 — 16:47

Я сделал два изменения в проге:

...
  # Послать запрос
  cmd = bytes('ati\r\n', 'ascii')  # -1- Добавил '\n'
  port.write(cmd)
 
  time.sleep(1)
 
  # Принять ответ и вывести его на консоль
  while True:
    print('.')
 
    resp = port.read()    # -2- Изменил функцию (была readline)
 
    # Вариант вывода 1
    if len(resp) > 0:
      print('[{0:d}] = '.format(len(resp)), end = '')
      for b in resp:
        print('0x{0:02X} '.format(b), end='')
 
...

В результате на консоль получил правильный результат:

$ ./myOBDm2.py
.
[1] = 0x61 .
[1] = 0x74 .
[1] = 0x69 .
[1] = 0x0D .
[1] = 0x45 .
[1] = 0x4C .
[1] = 0x4D .
[1] = 0x33 .
[1] = 0x32 .
[1] = 0x37 .
[1] = 0x20 .
[1] = 0x76 .
[1] = 0x31 .
[1] = 0x2E .
[1] = 0x35 .
[1] = 0x0D .
[1] = 0x0D .
[1] = 0x3E .
.
.
.
.
.
.
.

Но рапортовать о решении проблемы рано, так как проблема исчезляа только для первого (после включения компа) запуска проги. Повтороное и все последующие запуски проги выдавали одинаковые неправильные результаты:

$ ./myOBDm2.py
.
[1] = 0x7F .
[1] = 0xBF .
[1] = 0xFE .
.
.
.
.

Следует заметить, что при возврате ответа от девайса, строки заканчиваются
символом ‘\r’, а не ‘\n’ и не их комбинацией ‘\r\n’.

По моим представлениям девайс игнорирует символ ‘\n’. В описании вообще-то сказано, что девайс игнорирует также и другие «белые» символы (пробел, табуляция, ..). Например, девайс одинково правильно понимает команды ‘ati\r’ и ‘at i\r’.

Таким образом наличие или отсутствие в конце команда символа ‘\n’ никак не влияет на работоспособность девайса, что и подтверждается на практике.

UPDATE 08.08.2015 — 19:31

Хорошо. Перезагружаю комп и смотрю на настройки порта. Настройки порта перед запуском программы следующие:

$ stty -F /dev/ttyUSB0
speed 57600 baud; line = 0;
eof = ^A; min = 1; time = 0;
-brkint -icrnl -imaxbel
-opost -onlcr
-icanon -echo -echoe

Затем запускаю прогу. Она отрабатывает нормально. Выхожу из проги, и снова смотрю настройки порта:

$ stty -F /dev/ttyUSB0
speed 38400 baud; line = 0;
eof = ^A; min = 0; time = 0;
-brkint -icrnl -imaxbel
-opost -onlcr
-isig -icanon -iexten -echo -echoe -echok -echoctl -echoke

Оп-па! Что называется — «Найди семь отличий!»

Методом перебора включения/выключения настроек порта (точнее сказать — порта_терминала), удалось обнаружить, что «мешается» параметр -iexten.

Если перед запуском проги выключить этот параметр

$ stty -F /dev/ttyUSB0 iexten
$ stty -F /dev/ttyUSB0
  speed 38400 baud; line = 0;
    eof = ^A; min = 0; time = 0;
    -brkint -icrnl -imaxbel
    -opost -onlcr
    -isig -icanon -echo -echoe -echok -echoctl -echoke

, то прога снова нормально отрабатывает.

Иначе говоря, модуль serial в Python-3 при открытие порта меняет его настройки. Модуль serial в Python-2 точно так же меняет настройки порта, но здесь (во втором Питоне) эти настройки не приводят к фатальным последствиям.

Теперь осталось найти способ, как при открытии порта в модуле serial для Python-3 указать, что параметр IEXTEN не нужно устанавливать.

Проблема все еще не решена. Продолжаю копать. По мере «лечения» буду публиковать свои шаги.

UPDATE от 28.08.2015-02:01

В результате неспешных попыток заставить девайс ELM327 работать под Питоном-3, я пришел к пониманию, что возможно это не Питон не способен принять ответ от девайса, а наоборот — Питон не способен передать девайсц команду. Точнее так: Питон передает искаженную команду, девай получает хрен-знает-что и выдает Питону ответ типа «я не понимаю». Поскольку, Питон работает не правильно, то и ответ он тоже понимает в искаженном виде. Косвенным подтверждением этому является то, что через 30 скунд девас отправляет в комп какую-то короткую «отрыжку». Я предположил, что это может быть что-то типа «незаконченная команда снимается по тайм-ауту». По крайней мере такого явления («отрыжки») при работе под Питоном-2 не наблюдается. Другими словами, Питон-2 посылает в девайс правильную команду, а Питон-3 искаженную. Соответственно девас так и реагирует.

Что бы проверить то, что Питон-3 посылает бессмыслицу, я поставил несложный (для электронщиков, а я — электронщик) эксперимент. Я взял два китайских конвертера USB-UART типа CH340G и соединил их по схеме нуль-модема. Затем, подключил один к рабочему компу, на котором прога крутится под Питон-3, а другой к нотиику, на котором точно такая же (ну, за исключением print и некоторых других отличий) прога крутится под Питон-2.

Тогда, если Питон-3 искажает команды, я увижу эти искажения на втором компе.

Да! Существенное дополнение — в девайсе ELM-327 используется микросхема конвертера CH340T, это клон CH340G.

В результате я увидел, что при передаче данных в обоих направдлениях никаких искажений нет и в помине! Пробовал на разных скоростях обмена. Все работает чётко. Отсюда вывод — проблемы в связке девайса и Питона-3.

Устройство ELM327 — неразборное, чем-то напоминает блоки питания для нотиков. Пришлось крушить корпус молотком. Вскрыл более-менее нормально.

А вот далее начинается вторая серия детектива.

Я подключил осциллограф к выходу UART-а микросхемы CH340T, чтобы посмотреть, что она передает непосредственно в микроконтроллер.

Держитесь за стул! Оказывается, что микросхема передает правильные данные, но не на той скорости, какую мы ожидаем. Точнее так, в питоновской программе при инициализации последовательного порта указывается скорость работы. Программа, работающая под Питон-2, настраивает микросхему CH340T на заданную скорость. А вот программа, работающая под Питон-3, по какой-то неведомой причине не может настроить эту микросхему. В результате получается, что прога из-под Питона-2 обращается к микроконтроллеру на скорости 38400, а прога из-под Питона-3 — на скорости 9600 Бод.

Микроконтроллер ожидает, что к нему будут «стучаться» на 38400, и по этому он, естественно, не правильно понимает команду от компа. Далее микроконтроллер выдает ответ типа «Вы что там, совсем охренели?» на скорости 38400, но CH340T принимает это на скорости 9600 и передает в комп мусор. А через 30 секунд микрконтроллер посылает в комп еще один пакет типа «Да ну вас нахрен! Эту команду я снимаю. Вводите следующую!».

Теперь мне нужно понять — почему Питон-3 не способен правильно проинициализировать микросхему конвертера CH340T.

UPDATE от 28.08.2015-04:05

Скажу сразу — проблему победить не удалось, но удалось нащупать «обходной путь». Рецепт прост — в Питоне-3 не следует производить одновременно инициализацию порта и установку скорости.

Так делать не надо:

  try:
    port = serial.Serial("/dev/ttyUSB0", 38400, timeout=0.2)
  except serial.SerialException:
    print('Соединение не удалось')
    exit(1)

Следует делать так:

  try:
    port = serial.Serial("/dev/ttyUSB0")
    port.baudrate = 38400
    port.timeout = 0.2
  except serial.SerialException:
    print('Соединение не удалось')
    exit(1)

Я так понимаю, что это накосячено где-то в конструкторе класса Serial. Но заниматься ремонтом этого дурдома меня что-то больше не прёт.


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