![image](https://habrastorage.org/webt/yq/gb/cs/yqgbcsglidkja0zh62mzaqgixme.png)
Продолжаем разбирать программную составляющую платформы 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](https://habrastorage.org/webt/z8/ie/nk/z8ienkgagi7itbygujwbur1as4a.png)
Роботы имели интерфейс для доступа по TELNET и систему команд для удаленного управления. Документ с системой команд был где-то то-ли спрятан, то-ли закодирован. IP-адреса и открытые порты на роботах участники сканировали сами. При удачном подключении, роботы выдали приглашение, и участники понимали, что они «вошли». Ну а дальше командами доводили роботов по трассе до финиша. Изначально хотели сделать, чтобы вся трасса с роботами была где-то в изолированной комнате с установленной IP-камерой, но у организаторов возникли какие-то проблемы с IP-камерой и потеряло часть шарма.
На этом пока все. Вполне возможно, что по ходу развития программная модель претерпит изменения. К слову, она недавно уже была немного изменена, после того, как код глянул чуть более опытный ООП-щик.
Впереди пятая часть — поговорим об энкодерах, углах и калибровках.