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-камерой и потеряло часть шарма.


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

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

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