STM32 и Bluetooth или удаленное управление ПК своими руками

в 14:59, , рубрики: bluetooth, dbus, diy или сделай сам, python, stm32, метки: , , ,

Вместо введения

День добрый.
Сегодня я попытаюсь рассказать о своей попытке построить систему удаленного управления ПК в пределах одного помещения.

Сразу отмечу для тех кто скажет велосипед. Да это велосипед. И мне было интересно его построить. По ряду причин. Одна из которых желание сделать своими руками а не купить.

Предыстория

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

Один вариантов использовал привычный для пультов ДУ к современной бытовой технике способ передачи данных — IR он же ИК. Однако ввиду того что это не совсем удобно, да и с большими расстояниями проблема. На самом же деле всё проще чем вы можете подумать, тот модуль что я достал из одного и «трупиков» КПК был предназначен для работы именно как приёмопередатчик, то есть не было даже инвертора сигнала. Поэтому когда я подключил его напрямую к преобразователю USB<->UART на отладочной консоли я увидел непрекращающийся поток случайных символов.

Поэтому я решил использовать ставшим для меня привычным BT приёмопередатчик. Также нарыв в закромах оставшийся от одного из тестов модуль ESD-200 (не берите его, он реально неудобный и немного туповатый, а также дорогой модуль, плюс ко всему на больших расстояниях начинают пропадать пакеты). Чтобы не скучно было подцепил экран от Siemens M55. Также есть блок из шести кнопок на которые можно назначить произвольные команды. В качестве мозга выбрал отладочную плату STM32F4DISCOVERY — и отладчик на борту, и паять ничего не надо.

Общая схема

Внимание: то что здесь описано присутствует в основном в теории. На практике же некоторые моменты упрощены так как это макет и просто потому что так было быстрее сделать.
Со стороны ПК, на котором кстати Linux (у меня это Gentoo Linux, у вас это может быть любой другой дистрибутив) крутится программа — сервер. Она опрашивает список доступных устройств и найдя нужное устанавливает с ним соединение.
Со стороны устройства стоит триггер который контролирует статус соединения. При обнаружении подключения он вызывает модуль первичного опроса (своеобразный пинг). Который при удачном завершении переводит устройство в интерактивный режим.
Этот режим характерен тем что передачу инициирует любой из двух компонентов системы. Выбран такой принцип работы системы по двум причинам — малое время реакции на любые действия, отсутствие дополнительных таймеров (кроме таймера TIMEOUT со стороны демона на ПК) ну и конечно относительно простой протокол обмена. Хотя по идее нужен TIMEOUT со стороны устройства т.к. при работе с BT модулем иногда возникают проблемы.

Реализация

Теперь посмотрим что у нас получилось по выше расписанной схеме:

  • Демон который крутится на стороне ПК. Хотя какой там демон, в текущей реализации это скрипт на пару сотен строчек с минимальной обработкой ошибок. Один из самых интересных компонентов. Целиком написан на языке Python. Одной из особенностью данного модуля является то что для управления программами пользователя он использует систему сообщений DBUS. Кстати имплементация этого протокола для языка Python есть в любом современном дистрибутиве. Далее я подробно распишу как работает этот код.
  • Протокол для полностью асинхронного многопоточного обмена сообщения, реализован поверх последовательного порта с эмуляцией множества соединений используя систему адресации сообщений. Был придуман после прочтения описания сетевых протоколов и стандартов USB, а ещё я его написал т.к. мне было лениво курить MODBUS, а точнее грустно смотреть на его реализацию.
  • Устройство с прошивкой, которое используя модуль Bluetooth передаёт по последовательному порту команды при нажатии пользователем на кнопки устройства. Возможна также обратная связь в виде отображении информации на дисплее устройства. Но к сожалению сделано пока чисто для примера. Передаёт фиксированные команды и больше ничего.

Демон

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

Устроен в прототипе достаточно просто:

#!/usr/bin/python2
# -*- coding: utf-8 -*-
import serial
import dbus
import subprocess
import time
import os

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

# Обычное подключение к последовательному порту
ser = serial.Serial('/dev/rfcomm0', 115200, timeout=1)
# Подключение к сессии шины DBUS, откуда мы подключимся к плееру медиапрограммы Amarok.
bus = dbus.SessionBus()
am = bus.get_object('org.kde.amarok', '/Player')

# Словарь ключами которого выступают команды которые мы получаем из последовательного порта, а в итоге вызываем соответствующие методы объекта плеера. 
commands = {
            'p': [am.PlayPause, []],
            '>': [am.Next, []],
            '<': [am.Prev, []],
            'm': [am.Mute, []],
            '+': [am.VolumeUp, [1,]],
            '-': [am.VolumeDown, [1,]]
            }

print 'Connected'
# бесконечный цикл в котором мы и будем работать.
try:
    while 1:
        try:
            # принимаем первый байт нашего сообщения, при этом ждём его не более секунды, если ничего не пришло за этот промежуток значит нам ничего и не присылали.
            line = ser.read(1)
        # в этой части мы ловим специфичное исключение по которому судим что последовательный порт у нас отвалился
        except serial.serialutil.SerialException:
            # а так как нам надо работать, то мы закрываем этот порт, и ждём пол секунды пока завершится это действие.
            ser.close()
            time.sleep(0.5)
            while 1:
                try: 
                    # позже пытаемся установить соединение вновь
                    ser = serial.Serial('/dev/rfcomm0', 115200, timeout=1)
                    break
                # если же опять всё плохо, т.е. нам так и не удалось подключится к последовательному порту, то мы ожидаем 2 секунды и повторяем снова наш круг.
                except serial.serialutil.SerialException:
                    time.sleep(2)
        # если мы действительно считали байт (а это может быть и не так, потому, что у нас стоит время ожидания этого байта) то,
        if len(line) == 1:
            # проверяем что это за байт, пока реализован только приём команд
            if line[0] == 'C':
                print 'Command'
                # дочитываем оставшиеся 2 байта заголовка - адрес и размер сообщения
                line += ser.read(2)
                # не забываем проверить что мы считали именно 2 байта - т.е. размер заголовка должен быть равен 3 байтам.
                if len(line) == 3:
                    print "0x%02x 0x%02x 0x%02x" % (ord(line[0]), ord(line[1]), ord(line[2]))
                    # это захадкоженный ответ на ping, в реальном модуле он должен быть стандартным для любого адреса, т.к. этот ответ определяет готовность "ящика" к работе с ним.
                    if ord(line[1]) == 0x00 and ord(line[2]) == 0x00:
                        print 'Device ping'
                        ser.write('A')
                        ser.write(chr(0x00))
                        ser.write(chr(0x02))
                        ser.write(chr(ord('O')))
                        ser.write(chr(ord('K')))
                        print 'Ansver to device'
                    # если мы обратились ко второму узлу, то это сообщения общего уровня.
                    if ord(line[1]) == 0x02:
                        # получаем длину и считываем сообщение.
                        mlen = ord(line[2])
                        message = ser.read(mlen)
                        # если сообщение есть в словаре команд то выполняем команду которая соответствует полученному сообщению
                        if message in commands:
                            current = commands[message]
                            current[0](*current[1])
# при получении исключения - прерывание от клавиатуры выходим.
except KeyboardInterrupt:
    ser.close()
    del am
print 'Exiting'
# не забывая закрыть соединение с BT модулем.
# cleaning
cmd = "sudo rfcomm unbind all"
runner(cmd)

Протокол

Протокол достаточно простой, состоит из трёх байтового заголовка и сообщения с максимальной длиной в 255 байт, равно как может и не иметь сообщения — только заголовок. В заголовке указан тип сообщения, адрес получателя и размер сообщения. Более подробное описание протокола изложено в этом документе. Конечно на практике реализовано не всё что описано но по крайней мере оно работает. Хотя до сих пор есть проблемы по приёму сообщений и другие ошибки при обмене.

Устройство

Вот собственно вид устройства которое у меня получилось, также как и модуль со стороны ПК это устройство делалось как прототип, учитывая горький опыт неудач при постройке первой версии системы:
image
Управляется устройство прошивкой, написанной на С, для сборки используется собственная структура проекта которую вы можете увидеть пройдя по ссылке на проект ниже.

/* main work function */
void work(void) {
    unsigned short i, j;
    unsigned char mailbox_num = 0;
    volatile ProtoIOMBox * mbox;
    /* Убрал из примера инициализацию периферии и протокола, а также вывод на дисплей картинок - логотипа. */
    
    // Провека статуса порта перед запуском.
    /* check status */
    check_status();
    // Здесь происходит подготовка "ящиков" сообщений для приём и передачи, а также установка флагов о готовности. после осуществляется вывод сообщения на экран и в случае удачного ответа от сервера переход в режим отправки команд.
    /* send ping */
    mbox->outbox->header = 'C'; /* Command */
    mbox->outbox->size = 0x00; /* 0 for ping request */
    mbox->outbox_s = PROTO_IO_MBOX_READY; /* Box ready */
    mbox->inbox->size = 64; /* buffer len for control */
    mbox->inbox_s = PROTO_IO_MBOX_READY; /* Box ready */
    /* wait connection estabilished */
    while (status == 0);
    /* send ping message */
    proto_send_msg(mailbox_num);
    /* wait to send message */
    while (mbox->outbox_s <= PROTO_IO_MBOX_SEND);
    if (mbox->outbox_s == PROTO_IO_MBOX_COMPLETE)
        LCD_String("Con", 36, 6, 1, WHITE, GLASSY);
    else
        LCD_String("Un", 36, 6, 1, RED, GLASSY);
    /* get ping message */
    /* FIXME wtf? this not work or work parity */
    //proto_get_msg(mailbox_num);
    /* wait to get message */
    while (mbox->inbox_s <= PROTO_IO_MBOX_SEND);
    if (mbox->inbox_s == PROTO_IO_MBOX_COMPLETE) {
        LCD_String("OK", 36 + 3 * 7, 6, 1, GREEN, GLASSY);
        for (i = 0; i < mbox->inbox->size; i++)
            LCD_Char(mbox->inbox->message[i], 70 + i * 6, 6, 1, WHITE, GLASSY);
    }
    else
        LCD_String("ERR", 36 + 3 * 7, 6, 1, RED, GLASSY);
    // Бесконечный цикл в котором мы опрашиваем кнопки и посылаем команды если одна из них нажата. Допускается множественное нажатие кнопок, в этом случае будут отосланы все те команды которые соответствуют нажатым кнопкам. 
    /* infinity loop */
    while (1) {
        if (button_state.state[B_LGHT] == B_CLICK) {
            sender('+');
            button_state.state[B_LGHT] = B_RELEASE;
        }
        /* код аналогичен и для остальных кнопок */
    }
}

Также отдельно хочу упомянуть модуль отправки и приёма сообщений — в проекте это модуль proto.o — исходный код proto.c и заголовочный файл proto.h. Код приводить не буду так как он большой. А вот в целом расскажу как он работает.
Модуль рассчитан целиком на работу от прерываний, однако передача данных корректно не реализована сейчас поэтому требует предварительного вызова инициализации. Приём и отправка сообщения осуществляется с помощью 2 конечных автоматов, которые по мере пересылки байтов меняют своё состояние. Реализована проверка сообщения на валидность и обработка ошибок.

Также прикладываю видео работы системы в целом:

Послесловие

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

Автор: no111u3

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js