MIRO — открытая платформа indoor-робота. Часть 4 — Программная составляющая: ARDUINO (AVR)

    image

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

    Оглавление: Часть 1, Часть 2, Часть 3, Часть 4, Часть 5.

    Программное обеспечение под ARDUINO получилось самое большое из самописного. Вообще, почти вся логика непосредственно работы с исполнительными механизмами и сенсорами робота лежит на AVR. И на этом уровне реализовано API – программная библиотека быстрой разработки для MIRO.

    Структура API описана в wiki-разделе соответствующего репозитория. Пока только на русском языке. И сейчас мы разберем код более подробно. Я умышленно не буду приводить полного объявления классов, сокращая его троеточием "...", оставляя только значимые в данный момент вещи.

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

    class Miro : public Robot {
    public:
    	Miro(byte *PWM_pins, byte *DIR_pins);
    #if defined(ENCODERS_ON)
    	Miro(byte *PWM_pins, byte *DIR_pins, byte *ENCODER_pins);
    #endif
    	~Miro();
            ...
    };
    

    Класс Miro является классом верхнего уровня и описывает полную конфигурацию робота. Этот класс является наследником класса Robot, который описывает только самый основной функционал робота.

    class Robot {
    public:
    	Robot(byte *PWM_pins, byte *DIR_pins);
    #if defined(ENCODERS_ON)
    	Robot(byte *PWM_pins, byte *DIR_pins, byte *ENCODER_pins);
    #endif
    	~Robot();
    	
    	Chassis chassis;
    	void Sync();
    	int attachDevice(Device *dev);
    	int dettachDevice(Device *dev);
            ...
    
    protected:
    	Device* _devices[ROBOT_MAX_DEVICES];
    	byte _device_count;
    };
    

    В конструкторе выполняется первоначальная настройка пинов шасси и начальных значений конфигурации робота.

    Метод Sync() реализует необходимые операции для шасси и для всех подключенных к роботу устройств каждый шаг основного цикла loop() скетча ARDUINO. Методы Sync() класса Miro внутри себя вызывают соответствующие методы Sync() шасси и всех подключенных к роботу устройств.

    Класс Robot также содержит указатель на массив подключенных к роботу устройств, методы для работы с этим массивом (подключить новое устройство, отсоединить, найти по индексу и по имени). Также класс Robot содержит в себе объект класса Chassis – шасси.

    Но начнем с чего попроще — с устройств. Каждое устройство, которое можно подключить к роботу, будь то светодиод, датчик, или исполнительное устройство, не относящееся непосредственно к шасси (тележке), описывается своим классом-наследником общего для всех устройств виртуального класса Device:

    class Device
    {
    public:
    	virtual void Sync();
    	virtual void setParam(byte pnum, byte *pvalue);
            virtual void getParam(byte pnum, byte *pvalue);
    	virtual byte getPinsCount();
    	virtual char* getName();
    	virtual byte getParamCount();
    
    protected:	
    	byte *pins[2];
    };
    

    Виртуальные методы setParam, getParam, getParamCount связаны с назначение, получением, и определением количества параметров устройства. Параметром может являться любое свойство: яркость светодиода, положение сервопривода и пр. Класс-наследник каждого устройства реализует эти методы по-своему. Назначение методов getName, getPinsCount я думаю понятно из названия. Снова встретившийся метод Sync – это специальный метод для неблокирующего управления устройством и автоматизации каких-то операций с устройством, которые должны выполняться регулярно, каждую итерацию главного цикла.

    Давайте теперь рассмотрим какую-то более или менее общую реализацию класса-наследника.

    class MIROUsonic : virtual public Device {
    public:
    	void Sync();
    	void setParam(byte bnum, byte *pvalue);
        	void getParam(byte bnum, byte *pvalue);
    	byte getPinsCount();
    	char* getName();
    	byte getParamCount();
    	
    	void Init(byte trig_pin, byte echo_pin);
    	void On(unsigned int max_dist);
    	void On();
    	void Off();
    
    	int getDist(unsigned int max_dist);
    	unsigned int getMesCount();
    private:
    	bool _isOn;
    	unsigned int _mesCount;
    	unsigned int _dist;
    	unsigned int _max_dist;
    };
    

    В определении класса ультразвукового дальномера (выше), помимо методов родителя, есть также методы:

    • Init – инициализация;
    • On и Off – включение устройства (дальномера);
    • getDist – возвращает расстояние, измеренное дальномером;
    • getMesCount – возвращает количество выполненных измерений с момента включения устройства.

    Для хранения внутреннего состояния устройства служат поля:

    • _isOn (TRUE — устройство включено, управляется методами On и Off);
    • _mesCount (хранит количество измерений, используется в методе getMesCount);
    • _max_dist – максимальное требуемое расстояние для измерения*;
    • _dist – собственно измеренное расстояние.

    Про максимальную дальность измерения
    * Известно, что широко распространенный HC-SR04 по паспорту способен проводить измерение расстояния до 4-х метров. Однако, сам способ измерения предполагает ожидание возврата ультразвукового сигнала с последующем кодированием длительности в сигнале на линии Echo. И на самом деле, если пользователю точно не нужно измерять расстояния в диапазоне до 4-х метров, а достаточно, диапазона, скажем 1 метр, то и ждать отраженного сигнала можно в 4 раза меньше. Сам дальномер выдает сигнал на линии Echo как только примет его и произведет модуляцию. Т.е. на длительность периода между соседними измерениями это может и не повлияет, но длительность однократного измерения таким способом сократить можно.

    А вот теперь пояснение про метод Sync. Если устройство имеет состояние _isOn == TRUE (включено), то сам цикл измерения будет производиться в методе Sync, а результат измерения записываться в поле _dist. В этом случае, при вызове getDist, метод сразу же вернет значение, записанное в _dist, цикла измерения производиться не будет. Если же _isOn == FALSE (выключено), цикл измерения наоборот производится только во время вызова getDist, в методе Sync ничего измеряться не будет. Предполагается, что программист будет вызывать метод Sync всего робота, который в свою очередь вызовет одноименные методы Sync всех подключенных к роботу устройств и объекта класса Chassis (шасси).

    Из устройств в API сейчас реализованы только те вещи, которые есть в MIRO: светодиод, ультразвуковой дальномер, фоторезистивный датчик освещенности, сервопривод, датчик линии.

    Слегка затронем Chassis. Этот класс реализует «абстрактную тележку» робота. Он содержит методы, которые позволяют управлять движетелями.

    class Chassis {
    public:
    
    	Chassis(byte *PWM_pins, byte *DIR_pins);
    #if defined(ENCODERS_ON)	
    	Chassis(byte *PWM_pins, byte *DIR_pins, byte *ENCODER_pins);
    #endif
    	~Chassis();
    	
    	void Sync();
    
    	float getVoltage();
    	
    	int wheelRotatePWMTime(int *speedPWM, unsigned long time);
    	int wheelRotatePWM(int *speedPWM);
    	
    	bool wheelIsMoving(byte wheel) {return this->_wheel_move[wheel];}
    	byte getWheelCount() { return WHEEL_COUNT; }
    	
    #if defined(ENCODERS_ON)
    	int wheelRotateAng(float *speed, float *ang, bool en_break);
    	unsigned long wheelGetEncoder(byte wheel);
            ...
    	
    #endif //ENCODERS_ON
    
    private:
    
    	float _vbat; //Battery volgage
    	bool _wheel_move[WHEEL_COUNT];
    	char _wheelDir[WHEEL_COUNT];
    
    	byte _wheel_PWM_pins[WHEEL_COUNT];
    	byte _wheel_DIR_pins[WHEEL_COUNT];
    
    	void _init(byte *PWM_pins, byte *DIR_pins);
    	
    #if defined(ENCODERS_ON)
    	
    	byte _wheel_ENCODER_pins[WHEEL_COUNT];
    	bool _wheel_sync_move;
    	
    	float _wheelAngSpeed[WHEEL_COUNT];
    	float _wheelSetAng[WHEEL_COUNT];
    	float _wheelSetAngSpeed[WHEEL_COUNT];
            ...
    	
    #endif //ENCODERS_ON
    };
    

    Если мы рассматриваем тележку без энкодеров и вообще без обратной связи – то для этого есть простые методы управления по сигналу ШИМ. Если же в тележке есть энкодеры – класс существенно усложняется. Для упрощения жизни пользователя в нем появляются такие методы как:

    • wheelRotateAng – вращение колес на заданные углы поворота с заданными угловыми скоростями;
    • wheelGetPath – возвращает длину пути, пройденного каждым колесом;
    • wheelGetLinSpeed – возвращает текущую линейную скорость каждого колеса;
    • wheelGetAngSpeed — возвращает текущую угловую скорость каждого колеса;
    • wheelGetEncoder – возвращает количество срабатываний энкодеров каждого колеса.

    И еще ряд вспомогательных методов. А также метод калибровки движителей. Но более подробно ключевые методы класса Chassis рассмотрим в следующий раз.

    Забегая немного вперед, именно в этом месте будет уместно заметить, что всю эту библиотеку Miro можно легко адаптировать или дополнить на любого другого робота с двухколесной дифференциальной схемой движения. А при определенном усилии – и к другим движительно-рулевым конфигурациям. В случае же с дифференциальной схемой, нужно просто правильно описать файл конфигурации config.h. И без всяких RPi. Например, мы меньше чем за час запустили все вот на таких малютках для нашего регионального турнира по кибербезопасности BlackMirrorCTF-2019 в нашем университете (ссылка).

    image

    Роботы имели интерфейс для доступа по TELNET и систему команд для удаленного управления. Документ с системой команд был где-то то-ли спрятан, то-ли закодирован. IP-адреса и открытые порты на роботах участники сканировали сами. При удачном подключении, роботы выдали приглашение, и участники понимали, что они «вошли». Ну а дальше командами доводили роботов по трассе до финиша. Изначально хотели сделать, чтобы вся трасса с роботами была где-то в изолированной комнате с установленной IP-камерой, но у организаторов возникли какие-то проблемы с IP-камерой и потеряло часть шарма.


    На этом пока все. Вполне возможно, что по ходу развития программная модель претерпит изменения. К слову, она недавно уже была немного изменена, после того, как код глянул чуть более опытный ООП-щик.

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

    Подробнее
    Реклама

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое