Делаем с удовольствием

Несколько лет назад в одной толстой книжке по Питону я прочитал весьма забавное утверждение — по какой причине следует использовать Python. Одной из причин было названо получение удовольствия. Ото ж!

Вот за что я всячески люблю и уважаю Линукс, так за то, что он доставляет массу удовольствия от работы с ним.

Вот смотрите, сейчас передо мной стоит задача — создать программно-аппаратную хрень, которая позволит дистанционно слушать эфир.

Ну, что значит «слушать эфир»? Ну, то есть слушать, что есть в эфире на частотах в диапазоне 2.4 ГГц. Для прослушивания этих частот был создан сниффер (такая коробочка с электроникой), который подключается к компу по USB. Сниффер работает на заданном канале и передает в комп «сырые» пакеты того, что он принял. Дальнейшей обработкой пакетов занимается комп.

А что значит «дистанционно»? Ну, это означает, что оператор может находиться не за тем компом, к которому подключен сниффер (где-нибудь в Воронеже), а где-нибудь далеко. Ну, скажем, в Лондоне. А ещё лучше наоборот! — Сидеть в Саратове и слушать что летает в эфире поблизости от штаб-квартиры М6. Не, ну это же прелестно, правда?

Ну, не важно! Шутки в сторону!

Итак, что мы имеем. Сниффер подключен к компу, которым нужно рулить на расстоянии, и с которого нужно получать кое-какой трафик принимаемых радио-пакетов.

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

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

Пока я предполагаю всего три команды. (Список команд потом всё равно разрастётся. Это всегда так.) Вот эти команды:

  1. Start — запускает процесс приёма радиопакетов и передачу их в комп.
  2. Stop — останавливает радиоприём. Это нужно для изменения номера канала.
  3. Channel — задаёт канал для прослушки.

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

Но у нас консольное приложение. А у консольного приложения устройством ввода-вывода является обычный текстовый терминал. Графических (экранных) кнопок нет. Мышки, можно сказать, тоже  нет. Более того, вводить команды одной клавишей (одним нажатием на клавишу на клавиатуре) тоже так просто не получиться.

И всё это из-за того, что терминал по умолчанию работает в каноническом режиме. А это значит, что когда мы в своей программе обращаемся за информацией к клавиатуре, то система не будет «таскать» по одной букве в нашу программу. Терминал работает по-строчно. То есть сначала накапливает коды нажатых клавиш, а затем передаёт в программу целиком всю строку.

Таким образом, чтобы ввести команду «старт», нам не достаточно будет нажать на клавишу ‘s’. Ввод нужно закончить нажатием на «Enter». То есть вместо одного нажатия придётся выполнять два.

Мдя… Как-то не кошерно. Партнёры из лагеря Шиндовс засмеют. И, впрочем, они будут правы. Писать такие программы — «Это просто какой-то позор, товарищи!» (с).

Линукс тем и хорош, что когда ты с ним дружишь, он помогает тебе решать задачи. В этом нет никакого секрета. Просто свой инструмент нужно знать.

Понятно, что для того, чтобы получать информацию по-символьно, нужно перевести терминал в неканонический режим. Это легко сделать, когда знаешь как это делается. (Об этом чуть ниже.)

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

Итак, модель работы программы следующая:

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

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

Да! Ещё одно небольшое замечание по программе. Прогу можно писать на Си и на любом другом языке, но я предпочёл писать её на Питоне. Во первых, Питон — достаточно лёгкий язык и писать на нём сплошное удовольствие. (См. первый абзац!) Во вторых, Питон по умолчанию присутствует почти в каждом инсталлированном Линуксе.

Итак! Вот, функция, которая позволяет получать информацию с клавиатуры по-символьно:

import sys, termios, tty, select
def getChar():
  old = termios.tcgetattr(sys.stdin)
  tty.setcbreak(sys.stdin.fileno())

  while True:
    try:
      if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
        ch = sys.stdin.read()
        break
    except:
      ch = 'ctrl-c'
      break

  termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old)
  
  return ch

Вызов этой функции весьма незатейлив:

key = getChar()

Обработка команд (нажатых клавиш) тоже не вызывает особых сложностей:

key = getChar()

if key == 's':
  print("Старт")
elif key == 'p':
  print("Стоп")
elif key == 'c':
  print("Канал:", end=' ')
  channel = int(input())
  if 11 <= channel <= 26:      
    print("  Задан канал {}".format(channel))

Вы заметили, неканонический режим работы терминала включается только в функции getChar().

Таким образом мы вводим команду нажатием одной клавиши (без подтверждающего Enter). А когда выполняется команда код команды «выбора канала», терминал работает в каноническом режиме. Таким образом мы можем при вводе номера канала редактировать вводимое нами число. А когда число набрано, то просто нажимаем Enter.

Конечно, для прозрачности работы программы в её коде вместо настоящих обработчиков команд написаны инструкции print(). Разумеется, их нужно будет потом заменить.

Что же касается команды выбора канала, то она сводится к инструкции int(input()), которая вернёт введенное нами число. Это число мы присвоим переменной channel.

Микросхема, которая используется у меня в сниффере работает не со всеми радио-каналами в полосе 2.4 ГГц, а только с 16-ю, номера которых начинаются с 11-го и заканчиваются 26-м включительно. Поэтому в программе стоит фильтр if 11 <= channel <= 26.

Особое умиление вызывает и сам оператор сравнения — 11 <= channel <= 26. Это просто не может быть невкусным, ведь так!?

Кому интересно посмотреть на весь текст программы (точнее на заготовку), то вот он:

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

''' s2520c.py '''


HELP = '''
  Программа управление сниффером радиоканалов
  Версия 0.1 от 15.04.2018
  
  Горячие клавиши:
   Esc : выход из программы
    h  : получение этой подсказки
    s  : послать команду "старт" в сниффер
    p  : послать команду "стоп" в сниффер
    c  : задать номер канала.
        Номер канала должен быть числом в диапазоне от 11 по 26 включительно.
'''


import sys, tty, select, termios
def getChar():
  oldSettings = termios.tcgetattr(sys.stdin)
  tty.setcbreak(sys.stdin.fileno())

  while True:
    try:
      if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
        ch = sys.stdin.read(1)
        break
    except:
      ch = 'ctrl-c'
      break

  termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldSettings)
  
  return ch


if __name__ == "__main__":
  print("Нажмите Esc для выхода из программы или h для получения помощи.")
  while True:
    ch = getChar()
    
    if ch == '\x1B':
      exit()
    elif ch == 'h':
      print(HELP)
    elif ch == 's':
      print("Start")
    elif ch == 'p':
      print("Stop")
    elif ch == 'c':
      print("Channel:", end=' ')
      channel = int(input())
      if 11 <= channel <= 26:      
        print("  Set channel {}".format(channel))
    #if ch == 'ctrl-c':
    #  print("Ctrl-C");

В заключение мне следует сказать, что функцию getChar() я не писал с нуля. Я нашёл на просторах интернета пример класса, который позволяет получать одно-символьные посылки с клавиатуры терминала, и переделал код этого класса в реализацию в виде функции.

Исходный код класса я взял здесь:
https://code-examples.net/ru/q/fc7e9e

Я продублирую код этого класса ещё раз:

import sys, select, tty, termios
class NonBlockingConsole(object):
    def __enter__(self):
        self.old_settings = termios.tcgetattr(sys.stdin)
        tty.setcbreak(sys.stdin.fileno())
        return self

    def __exit__(self, type, value, traceback):
        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)

    def get_data(self):
        try:
            if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
                return sys.stdin.read(1)
        except:
            return '[CTRL-C]'
        return False

Пользоваться классом ничуть не сложнее, чем пользоваться функцией:

if __name__ == "__main__":
  with NonBlockingConsole() as nbc:
    while True:
      ch = nbc.getChar()

      if ch == '\x1b': # Это код символа Esc
        print("Esc")
      elif ch == '\x7f': # код символа backspace (забой)
        print("Backspace")
      elif ch == '[CTRL-C]': # Нажали на Ctrl-C
        print("Ctrl-C")
      elif ch == '\n': # А это -- реакция на нажатие Enter
        print("NewLine")
      
      elif ch == 'a':  # была нажата клавиша 'A'
        print("A")
      elif ch == 'b':   # была нажата клавиша 'B'
        print("B")
      elif ch == 'c':  # была нажата клавиша 'C'
        print("C")
      elif ch == 'h':   # была нажата клавиша 'H'
        print("Help")  
      elif ch == 'q':   # была нажата клавиша 'Q'
        print("Q")
        break

Ну, вы поняли! Всё совсем не сложно.

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

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

И как вишенка на тортике — вывод проги можно легко перенаправить в файл, а потом (в спокойной обстановке и/или на компе с большей вычислительной мощностью, чем у удалённого компа, который, к стати, может быть обычным дешманским Raspberry-Pi) заняться дешифровкой «выловленной» информации.

🙂

Реклама

8 responses to “Делаем с удовольствием

  1. В строке 21 листинга заготовки, наверное, должно быть «import threading»? А в строке 22 не хватает имени класса.

    • Ага. Спасибо! Сейчас поправлю.

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

      В любом случае, это моя грязь. Спасибо, что заметили и сообщили!

  2. Пора уже завести свой телеграм чат, в котором можно обсуждать тонкости реализаций 🙂

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

    • ну-у-у… как бы можно и так, как Вы говорите, сделать.

      У проекта много нюансов, которые я не раскрываю. Поэтому со стороны моё решение может казаться странным и даже не совсем оптимальным.

  3. 2.4, 11, 26… не ZigBee ли часом?

  4. Здравия…
    Уважаемый Александр Антонович, я чуть-чуть позанудствую по коду по мелочам с Вашего позволения.

    Строчка 2: # coding:utf-8 — в третьей версии Питона utf-8 дефолтная кодировка, строчка не имеет смысла.

    Строчка 21: import sys, tty, select, termios — pep8 не одобряет. Рекомендуется каждый модуль импортировать отдельно и придерживаться алфавитного порядка. В своём коде я импорты группирую ещё и по принадлежности: стандартная библиотека, сторонние библиотеки, модули программы.

    Строчка 22: def getChar(): — pep8 не одобряет. Для имён функции и переменных рекомендуется использовать строчные символы и разделять слова символом подчёркивания, если это требуется для читабельности кода.

    Строчки 54, 55.
    print(«Channel:», end=’ ‘)
    channel = int(input())

    Здесь print лишний, потому что input может принимать аргумент. Я бы написал так:
    channel = int(input(«Channel: «)

    Добра…

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

      А в целом, спасибо Вам, уважаемый Феликс, за полезный комментарий!

      И Вам бобра! Хорошо, когда люди помогают друг другу овладевать светом знаний.

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

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

Логотип WordPress.com

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

Google+ photo

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

Фотография Twitter

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

Фотография Facebook

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

w

Connecting to %s