Русский
Русский
English
Статистика
Реклама

Прототип на коленке cоздание приложения для мониторинга датчиков сердечного ритма в спортивном зале


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


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


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


Основным лейтмотивом реализации проекта служит идея совмещения низкоуровневой разработки программы управления устройством на языке C++ и быстрой высокоуровневой разработки сервиса на Python. Базовым программным обеспечением должна быть операционная система Linux. Будем использовать Linux way работа системы должна быть построена на небольших независимых сервисах, работающих под управлением ОС.


Итак, формулируем цель проекта


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


Желаемое поведение системы


Посетитель спортивного зала в начале тренировки получает нагрудный датчик сердечного ритма HRM (Heart Rate Monitor) и регистрирует его у оператора в зале. Затем он перемещается по залу, и показания его датчика автоматически поступают на сервер сбора статистики для отслеживания состояния его здоровья. Такое предложение выгодно отличается от приобретения датчика самим посетителем: данные собираются централизовано и могут быть сопоставлены с данными с различных спортивных тренажеров, а также ретроспективно проанализированы.
В статье описан первый этап создания такого решенния программы, считывающую данные с датчика и с помощью которой можно будет в дальнейшем отправлять данные на сервер.


Технические аспекты


HRM представляет собой автономный датчик (монитор), прикрепленный на тело спортсмена, передающий данные по беспроводной сети. Большинство мониторов, предлагаемых сейчас на рынке, могут работать с использованием открытой сети с частотой 2.4ГГц по протоколам ANT+ и BLE. Показания датчика регистрируются на каком-либо программно-управляемом устройстве: мобильном телефоне или компьютере через USB приемопередатчик.


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


Основная проблема при использовании устройств ANT и BLE заключается в ограниченном радиусе действия сети (максимальный радиус в режиме минимальной мощности для ANT передатчика 1mW составляет всего 1 метр), поэтому решено создать распределенную сеть регистрирующих устройств. Для достижения этой цели выбраны бюджетные одноплатные компьютеры в качестве узлов проводной или беспроводной локальной сети. К такому маломощному компьютеру можно подсоединить одновременно несколько разнородных датчиков через USB разветвитель с дополнительным питанием и разнести на максимальную дальность действия USB кабеля (до 5 метров).


Железо и ПО


Для начала работы важно иметь все необходимые компоненты под рукой.


Перечислим то, что требуется:



Одноплатный компьютер Orange Pi Zero с ARM v7 с 2-х ядерным процессором,
256Мб ОЗУ и 2Gb Micro SD.



Приемопередатчик USB Ant+ Stick (далее USB стик)



Монитор (датчик) сердечного ритма HRM



USB TTL Serial преобразователь интерфейсов для связи с ПК


Итак, выбор железа состоялся. Для реализации программной части будем использовать C++ для взаимодействия с железом и Python версии 3 для сервиса. Выбор базового программного обеспечения остановим на операционной системе Linux. Вариант с использованием Android тоже вполне интересен, но несет больше риска в плане реализации. Что касается Linux для Orange Pi, то это будет Raspbian, наиболее полная и стабильная ОС для этого мини-компьютера. Все необходимые программные компоненты есть в репозитории Raspbian. Впрочем, результат работы можно будет в дальнейшем портировать на другие платформы.


Собираем все вместе и начинаем творить прототип.


Среда разработки


Для упрощения процесса разработки используем x86-64 машину с установленной Ubuntu Linux 18.04, а образ Orange Pi Zero загружаем с сайта https://www.armbian.com и в дальнейшем настраиваем для работы. Сборку проекта под целевую платформу будем производить непосредственно на одноплатнике.


Записываем полученный образ на SD карту, запускам плату, делаем первоначальную конфигурацию LAN / Wi-Fi. Устанавливаем Git, Python3 и GCC, остальное подгружаем по мере необходимости.


Структура приложения


Проведем декомпозицию программного кода, для этого разделим программную часть на уровни абстракции. На нижнем уровне расположим модуль для Python, реализованный на C++, который будет отвечать за взаимодействие ПО верхнего уровня с USB приемопередатчиком. На более высоких уровнях сетевое взаимодействие с сервером приложений. В самом простом случае это может быть WEB-сервер.


Первоначально хотел использовать готовое решение. Однако выяснилось, что большинство проектов использует библиотеку libusb, что требует изменения в образе Raspbian, в котором для данного оборудования уже есть готовый модуль ядра usb_serial_simple. Поэтому взаимодействие с железом осуществили через символьное устройство /dev/ttyUSB на скорости 115200 бод, что оказалось проще и удобнее.

Проект основан на переделке существующего открытого кода с GitHub (https://github.com/akokoshn/AntService). Код проекта был переработан и максимально упрощен для использования совместно с Python. Получившийся прототип можно найти по ссылке.


Сборка проекта будет с использованием CMake и Python Extension. На выходе получим исполняемый файл и динамическую библиотеку модуля Python.


Протокол работы ANT с HRM датчиком


Режим работы протокола ANT для HRM происходит в широковещательном режиме (Broadcast data) обмена данными по каналу между ведущим (master) HRM датчиком и ведомым (slave) USB стиком. Такой режим используется в случае, когда потеря данных не критична.


На аппаратном уровне ведомый осуществляет поиск ведущего в сети и пытается установить соединение, используя следующие характеристики: ключ сети, частоту, период опроса. Если ведущий найден, то устанавливается односторонний канал от ведущего к ведомому, приходят сообщения в виде байтовых посылок с полезной нагрузкой 8 байт на одно сообщение.


На диаграмме показан процесс установления соединения. Здесь Host управляющий компьютер, USB_stick приемопередатчик (ведомое устройство), HRM нагрудный датчик (ведущее устройство)



Последовательность действий:


  • Сброс устройства в первоначальное состояние
    • Настройка соединения
    • Активация канала
    • Периодическое чтение буфера для получения данных

Код приложения будем создавать в объектно-ориентированной парадигме, поэтому первым шагом определим список объектов:


  • Device обеспечивает соединение с драйвером операционной системы, работающим с USB приемо-передатчиком;
    • Stick реализует взаимодействие по протоколу ANT.

Список состояний, в которых могут находится объекты:


  • Device: подключен / не подключен;
  • Stick: подключен / не подключен / неопределенное состояние / инициализирован / не инициализирован.

Список методов объектов, изменяющих состояние объектов:


  • Device: подключить / отключить / отправить данные в устройство / получить данные из устройства;
  • Stick: инициализировать / установить соединение / отправить сообщение / обработать сообщение / выполнить команду.

По результатам анализа взаимодействия и выбора объектов для реализации построим диаграмму классов. Здесь Device будет абстрактным классом, реализующим интерфейс соединения с устройством.



Отправка сообщений происходит через метод do_comand, первым аргументом принимающий сообщение, а вторым обработчик результата (это может быть любой вызываемый объект).


Вот псевдокод, демонстрирующий, как использовать программу:


// Создаем объект класса Stick.Stick stick = Stick();// Создаем устройство TtyUsbDevice и передаем владение в объект класса Stick.stick.AttachDevice(std::unique_ptr<Device>(new TtyUsbDevice("/dev/ttyUSB0")));// Подключаем.stick.Connect();// Устанавливаем в исходное состояние.stick.Reset();// Инициализируем и устанавливаем соединение.stick.Init();// Получаем сообщение с датчика.ExtendedMessage msg;stick.ReadExtendedMsg(msg);

Пример использования Python модуля.


# Создаем объект класса с методом обратного вызова __call__import hrmclass Callable:    def __init__(self):        self.tries = 50    def __call__(self, json):        print(json)        self.tries -= 1        if self.tries <= 0:            return False # Stop        return True # Get next valuecall_back = Callable()# Подключаем файл устройстваhrm.attach('/dev/ttyUSB0')# Инициализируем устройствоstatus = hrm.init()print(f"Initialisation status {status}")if not status:    exit(1)# Передаем полученный объект для обработки модулемhrm.set_callback(call_back)

Здесь все просто и понятно, переходим к детальному описанию особенностей проекта.


Логирование


При разработке приложения не следует упрощать сбор логов и статистики, поэтому используются сторонние библиотеки: Glog, Boost.Log и другие. В нашем случае сборка проекта будет происходить непосредственно на устройстве, поэтому для уменьшения количества кода решено применить собственный логер.


Для отображения точки входа в область видимости и выхода используем простой макрос, который создает объект логгера на стеке. В конструкторе выводится в лог точка входа (имя С++ файла, имя метода, номер строки), в деструкторе точка выхода. В начало каждой интересуемой области видимости ставится макрос. Если логирование не требуется для всей программы, макрос определяется как пустой.


// Show debug info#define DEBUG#if defined(DEBUG)#include <string.h>class LogMessageObject{public:    LogMessageObject(std::string const &funcname, std::string const &path_to_file, unsigned line) {        auto found = path_to_file.rfind("/");        // Extra symbols make the output coloured        std::cout << "+ \x1b[31m" << funcname << " \x1b[33m["                  << (found == std::string::npos ? path_to_file : path_to_file.substr(found + 1))                  << ":" << std::dec << line << "]\x1b[0m" << std::endl;        this->funcname_ = funcname;    };    ~LogMessageObject() {        std::cout << "- \x1b[31m" << this->funcname_ << "\x1b[0m" << std::endl;    };private:    std::string funcname_;};#define LOG_MSG(msg) std::cout << msg << std::endl;#define LOG_ERR(msg) std::cerr << msg << std::endl;#define LOG_FUNC LogMessageObject lmsgo__(__func__, __FILE__, __LINE__);#else // DEBUG#define LOG_MSG(msg)#define LOG_ERR(msg)#define LOG_FUNC#endif // DEBUG

Пример работы логгера:


Attach Ant USB Stick: /dev/ttyUSB0+ AttachDevice [Stick.cpp:26]- AttachDevice+ Connect [Stick.cpp:34]+ Connect [TtyUsbDevice.cpp:46]- Connect- Connect+ reset [Stick.cpp:164]+ Message [Common.h:88]+ MessageChecksum [Common.h:77]- MessageChecksum- Message+ do_command [Stick.cpp:140]Write: 0xa4 0x1 0x4a 0x0 0xef+ ReadNextMessage [Stick.cpp:72]- ReadNextMessageRead: 0xa4 0x1 0x6f 0x20 0xea- do_command- reset+ Init [Stick.cpp:49]+ query_info [Stick.cpp:180]+ get_serial [Stick.cpp:199]+ Message [Common.h:88]+ MessageChecksum [Common.h:77]- MessageChecksum- Message+ do_command [Stick.cpp:140]Write: 0xa4 0x2 0x4d 0x0 0x61 0x8a+ ReadNextMessage [Stick.cpp:72]- ReadNextMessageRead: 0xa4 0x4 0x61 0x83 0x22 0x27 0x12 0x55- do_command- get_serial

Классы и структуры данных


Для уменьшения связности создадим абстрактный класс Device и конкретный класс TtyUsbDevice. Класс Device выступает в роли интерфейса для взаимодействия кода приложения с USB. Класс TtyUsbDevice работает с модулем ядра Linux через файл символьного устройства /dev/ttyUSB.


class Device {public:    virtual bool Read(std::vector<uint8_t> &) = 0;    virtual bool Write(std::vector<uint8_t> const &) = 0;    virtual bool Connect() = 0;    virtual bool IsConnected() = 0;    virtual bool Disconnect() = 0;    virtual ~Device() {}};

В качестве структуры данных для хранения сообщений используем std::vector<uint8_t>. Сообщение в формате ANT состоит из синхро-байта, однобайтного поля размер сообщения, однобайтного идентификатора сообщения, самих данных и контрольной суммы.


inline std::vector<uint8_t> Message(ant::MessageId id, std::vector<uint8_t> const &data){    LOG_FUNC;    std::vector<uint8_t> yield;    yield.push_back(static_cast<uint8_t>(ant::SYNC_BYTE));    yield.push_back(static_cast<uint8_t>(data.size()));    yield.push_back(static_cast<uint8_t>(id));    yield.insert(yield.end(), data.begin(), data.end());    yield.push_back(MessageChecksum(yield));    return yield;}

Класс Stick реализует протокол взаимодействия между хостом и USB стиком.


class Stick {public:    void AttachDevice(std::unique_ptr<Device> && device);    bool Connect();    bool Reset();    bool Init();    bool ReadNextMessage(std::vector<uint8_t> &);    bool ReadExtendedMsg(ExtendedMessage &);private:    ant::error do_command(const std::vector<uint8_t> &message,                          std::function<ant::error (const std::vector<uint8_t>&)> process,                          uint8_t wait_response_message_type);    ant::error reset();    ant::error query_info();    ant::error get_serial(unsigned &serial);    ant::error get_version(std::string &version);    ant::error get_capabilities(unsigned &max_channels, unsigned &max_networks);    ant::error check_channel_response(const std::vector<uint8_t> &response,                                      uint8_t channel, uint8_t cmd, uint8_t status);    ant::error set_network_key(std::vector<uint8_t> const &network_key);    ant::error set_extended_messages(bool enabled);    ant::error assign_channel(uint8_t channel_number, uint8_t network_key);    ant::error set_channel_id(uint8_t channel_number, uint32_t device_number, uint8_t device_type);    ant::error configure_channel(uint8_t channel_number, uint32_t period, uint8_t timeout, uint8_t frequency);    ant::error open_channel(uint8_t channel_number);private:    std::unique_ptr<Device> device_ {nullptr};    std::vector<uint8_t> stored_chunk_ {};    std::string version_ {};    unsigned serial_ = 0;    unsigned channels_ = 0;    unsigned networks_ = 0;};

Интерфейсная часть и реализация для удобства разделены семантически. Класс владеет единственным экземпляром типа Device, владение которым передается через метод AttachDevice.


Отправка и обработка команд происходит через вызов метода do_command, который в качестве первого аргумента принимает байты сообщения, вторым аргументом обработчик, затем тип ожидаемого сообщения. Главное требование для метода do_command заключается в том, что он должен быть точкой входа для всех сообщений и местом синхронизации. Для возможности расширения метода потребуется инкапсулировать его аргументы в новый объект сообщение. Код прототипа не является многопоточным, но подразумевает возможность переработки do_command на основе ворклетов и асинхронной обработки сообщений. Метод отбрасывает сообщения, не соответствующие ожидаемому типу. Это сделано для упрощения кода прототипа. В рабочей версии каждое сообщение будет обрабатываться асинхронно собственным обработчиком.


ant::error Stick::do_command(const std::vector<uint8_t> &message,                             std::function<ant::error (const std::vector<uint8_t>&)> check_func,                             uint8_t response_msg_type){    LOG_FUNC;    LOG_MSG("Write: " << MessageDump(message));    device_->Write(std::move(message));    std::vector<uint8_t> response_msg {};    do {        ReadNextMessage(response_msg);    } while (response_msg[2] != response_msg_type);    LOG_MSG("Read: " << MessageDump(response_msg));    ant::error status = check_func(response_msg);    if (status != ant::NO_ERROR) {        LOG_ERR("Returns with error status: " << status);        return status;    }    return ant::NO_ERROR;}

Структура ExtendedMessage, чтение расширенных сообщений.


Согласно алгоритму работы HRM датчика, данные передаются только в одну строну с использованием расширенного типа сообщения. Для прототипа используется простая схема: после открытия канала и установления соединения клиентское приложение использует метод ReadExtendedMsg для чтения расширенных сообщений.


struct ExtendedMessage {    uint8_t channel_number;    uint8_t payload[8];    uint16_t device_number;    uint8_t device_type;    uint8_t trans_type;};

bool Stick::ReadExtendedMsg(ExtendedMessage& ext_msg){/* Flagged Extended Data Message Format** | 1B   | 1B     | 1B  | 1B      | 8B      | 1B   | 2B     | 1B     | 1B    | 1B    |* |------|--------|-----|---------|---------|------|--------|--------|-------|-------|* | SYNC | Msg    | Msg | Channel | Payload | Flag | Device | Device | Trans | Check |* |      | Length | ID  | Number  |         | Byte | Number | Type   | Type  | sum   |* |      |        |     |         |         |      |        |        |       |       |* | 0    | 1      | 2   | 3       | 4-11    | 12   | 13,14  | 15     | 16    | 17    |*/    LOG_FUNC;    std::vector<uint8_t> buff {};    device_->Read(buff);    if (buff.size() != 18 or buff[2] != 0x4e or buff[12] != 0x80) {        LOG_ERR("This message is not extended data message");        return false;    }    ext_msg.channel_number = buff[3];    for (int j=0; j<8; j++) {        ext_msg.payload[j] = buff[j+4];    };    ext_msg.device_number = (uint16_t)buff[14] << 8 | (uint16_t)buff[13];    ext_msg.device_type = buff[15];    ext_msg.trans_type = buff[16];    return true;}

Модуль hrm


Для создания в Python модуля hrm, предназначенного для работы с ANT, воспользуемся distutils. Создадим два файла: setup.py (для сборки) и hrm.cpp, в котором находится исходный код модуля.


Сборку всего модуля опишем в файле setup.py через создание объект типа Extension. Для сборки вызовем функцию setup над этим объектом.


from distutils.core import setup, Extensionhrm = Extension('hrm',                language = "c++",                sources = ['hrm.cpp', '../src/TtyUsbDevice.cpp', '../src/Stick.cpp'],                extra_compile_args=["-std=c++17"],                include_dirs = ['../include'])setup(    name        = 'hrm',    version     = '1.0',    description = 'HRM python module',    ext_modules = [hrm])

Рассмотрим исходный код модуля.


Объект класса Stick храним в глобальной переменной


static std::shared_ptr<Stick> stick_shared

Далее создаем две структуры типа PyMethodDef и PyModuleDef и инициализируем модуль.


Для работы с USB стиком в Python создадим три функции:


  • attach для подключения файла символьного устройства;
    • init для инициализации соединения;
    • set_callback для установки функции обратного вызова обработки расширенных сообщения.

Теперь можно обобщить и сделать некоторые выводы


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


Для проведения эксперимента по реализации бизнес-идеи не потребовалось использовать большое количество ресурсов и кода. Код приложения специально сделан упрощенным и линейным в первую очередь для уменьшения количества ошибок и демонстрации принципов работы с ANT.


Как результат работы приведу простой алгоритм моих действий для выполнения поставленной задачи:


  1. Понять суть задачи, сформулировать цели, подготовить техническое задание.
  2. Выполнить поиск готовых проектов, разобраться с лицензиями. Найти документацию о протоколах и стандартах. Понять алгоритм работы устройства.
  3. Найти необходимое оборудование, исходя из цены, доступности и технических возможностей.
  4. Продумать архитектуру приложения, выбрать среду разработки.
  5. Реализовать код приложения, заранее продумать критерии, например такие:
    код прототипа сделать однопоточным;
    использовать последний стандарт C++ 17 и стандартную библиотеку, использовать RAII;
    разделить интерфейс и реализацию семантически: методы, относящиеся к интерфейсу, называть в стиле CamelCase, а имена методов, отвечающих за реализацию, в стиле under_score, поля класса в стиле underscore;
    логирование.
  6. Протестировать проект.

Всем удачи во всех начинаниях!

Источник: habr.com
К списку статей
Опубликовано: 03.11.2020 12:06:29
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании auriga

Python

C++

Разработка под linux

Разработка на raspberry pi

Python c++

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru