Пишем прошивку для Arduino на С++ с REST управлением через последовательный порт и экранчиком

imageЭто второй пост про Wi-Fi роботанк. В нем будет написано как не надо делать прошивки, если вы суровый программист микроконтроллеров и как можно сделать, если нужна максимальная наглядность и возможность рулить прошивкой почти как веб-приложением прямо с терминала.

То есть, например, отправив в последовательный порт что-то типа

battery? act=status получим в ответ что-то типа { «status»: «OK», «minValue»: 600, «maxValue»: 900, «value»:750, «percent»: 50 } Для тех, кому лень читать статью, сразу ссылка на github и Яндекс-диск, у кого гитхаб залочен (спасибо MaximChistov).Итак, в какой-то момент я понял, что без ардуины мне никак не обойтись, достал из закромов Arduino Nano и купил к нему экранчик с I2C переходником. Как я пытался подключить экран, это отдельная песня с припевом, но в итоге оказалось, что у меня был перебитый земляной провод и I2C адрес экрана не соответствовал ни одному из описаний в инете. После успешного запуска HelloWorld из примеров я начал думать и что же мне со всем этим делать.

На старте имелось следующее: Arduino Nano LCD экран 16×2 с переходником на I2C как тут Библиотека I2C экрана Список задач: Управление по типу HTTP REST Отображение на экране сообщений, уведомлений (больше приоритет) и иконок по типу system tray Определение уровня заряда батарейки Управление питанием и выстрелом пушки Чуть-чуть про пушку и батарейку. Для определения уровня заряда я подключил плюс батарейки через делитель из 2х резисторов по 43КОм на вход A0. Осталось только откалибровать верхнее и нижнее значения. Пушка у меня включена не постоянно. При подаче единицы на цифровую ногу ардуины через полевой транзистор запитывается сервопривод наведения пушки и заодно лазерный прицел. Для выстрела необходимо постепенно с помощью ШИМа (чтоб снизить помехи по питанию) открыть второй транзистор, включающий мотор самой пушки и также постепенно его выключить при замыкании контакта, который сигнализирует о том, что выстрел случился.Также сразу уточню, что посмотрев на объем RAM у ATMega328, который всего 2 килобайта, я испугался и не стал использовать динамическую память. Только стек, только хардкор.

Поняв, что мне надо от ардуины и прогнав тестовые примеры, я радостно открыл Arduino IDE и завис. Мне явно было мало одного файла и хотелось нормального C++ с подсветкой и автокомплитом. Через некоторое время пришло озарение. Исходник оформляется в виде библиотеки, лежащей в sketchbook/libraries, а сам скетч создает единственный объект с методами Setup () и Loop () и соответственно их вызывает.На всякий случай, уточню, что у Arduino SDK есть две абстракции ввода-вывода. Это Print, на который можно как это ни странно выводить и Stream, который унаследован от Print.

Базовые сущности получились такие:

Команда. Создается из строки вида /commandName? arg1=value1&arg2=value2 и содержит простейший парсер аргументов. Интерфейс class Command { public: Command (char* str);

inline const char* Name () const { return _name; } inline const char* Params () const { return _params; }

const char* GetParam (const char* paramName);

bool GetIntParam (const char* paramName, int* out); bool GetDoubleParam (const char* paramName, double* out); bool GetStringParam (const char* paramName, const char** out);

private: char* _name; char* _params; size_t _paramsLen; }; Обработчик команды. Обрабатывает команду и шлет ответ в виде JSON.Интерфейс class CommandHandler { public: virtual void HandleCommand (Command& cmd, Print &output);

static void SendError (Print &output, const char* err); static void SendOk (Print &output); static void SendErrorFormat (Print &output, const char* fmt, …); }; Обработчик запросов. Создает команду и маршрутизирует ее на соответствующий обработчик, если таковой есть.Интерфейс struct HandlerInfo { const char* command; CommandHandler* handler;

HandlerInfo (): command (0), handler (0) {} HandlerInfo (const char* c, CommandHandler* h): command©, handler (h) {} };

class RequestHandler { public: RequestHandler ();

void SetHandlers (HandlerInfo* handlers, size_t count); void HandleRequest (char* request, Print& output);

private: HandlerInfo* _handlers; size_t _handlersCount; }; Для обработки запросов с последовательного порта (или еще какого Stream’a) был написан

StreamRequestHandler class StreamRequestHandler: public RequestHandler { public: static const size_t BufferSize = 128;

StreamRequestHandler (Stream& stream); void Proceed ();

private: Stream& _stream; char _requestBuf[BufferSize]; size_t _requestLen; }; Настало время все это протестировать. Для этого надо создать экземпляр StreamRequestHandler, передав ему в конструктор Serial (который на самом деле синглтон класса HardwareSerial), передать в SetHandlers массив обработчиков команд и дергать метод Proceed где-то внутри loop ().Первым обработчиком стал

PingHandler class PingHandler: public CommandHandler { public: virtual void HandleCommand (Command& cmd, Print &output) { SendOk (output); } };

После успешного отклика на пинг захотелось прочитать что-нибудь на экранчике и там же увидеть заряд батарейки. Стандартный экран 16×2 имеет, как ни странно, две строки по 16 символов, а также позволяет переопределить изображения для первых восьми символов. Исходя из этого я решил первые 8 знакомест верхней строки отвести под system tray, вторые 8 пока не трогать, а все сообщения выводить в нижнюю строку.Обычные сообщения будут выводиться бегущей строкой, а приоритетные будут не длиннее 16 символов и выводиться по центру, прерывая на время отображения, если надо, текущую бегущую строку. Чтобы не городить связные списки на указателях, максимальное количество сообщений в очереди было ограничено до 8 обычных и 4 приоритетных, что позволило использовать для их хранения обычный кольцевой буфер.Иконками в system tray стали обычные символы с кодами от 0 до 7. Для показа иконки надо сначала ее зарезервировать, получив iconID (который просто код символа), после чего для iconID можно задать само изображение. Анимированную иконку можно получить постоянно меняя картинку. Экран такое издевательство выдерживает без проблем.Что в итоге получилось class DisplayController { public: static const uint8_t MaxIcons = 8; static const uint8_t Rows = 2; static const uint8_t Cols = 16; static const uint8_t IconsRow = 0; static const uint8_t IconsCol = 0; static const int MaxMessages = 8; static const unsigned MessageInterval = 150; static const uint8_t MessageRow = 1; static const uint8_t MessageCol = 0; static const uint8_t MessageLen = 16;

static const int MaxAlerts = 4; static const unsigned AlertInterval = 1000; static const uint8_t AlertRow = 1; static const uint8_t AlertCol = 0; static const uint8_t AlertLen = 16; private: struct Message { const char* text; int length; int position; inline Message () { Clear (); } inline void Clear () { text = 0; length = 0; position = 0; } };

struct Alert { const char* text; int length; bool visible; inline Alert () { Clear (); } inline void Clear () { text = 0; length = 0; visible = false; } }; public: DisplayController (uint8_t displayAddr);

void Init (); int8_t AllocateIcon (); void ReleaseIcon (int8_t iconId);

void ChangeIcon (int8_t iconId, uint8_t iconData[]); void UpdateIcons (); void Proceed (); bool PutMessage (const char* text); bool PutAlert (const char* text); inline bool HasMessages () { return _messages[_messageHead].text!= 0; } inline bool HasAlerts () { return _alerts[_alertHead].text!= 0; } void UpdateMessage (); void UpdateAlert (); private: LiquidCrystal_I2C _lcd; int _iconBusy;

unsigned long _messageTick;

Message _messages[MaxMessages]; int _messageHead; int _messageTail;

unsigned long _alertTick; Alert _alerts[MaxAlerts]; int _alertHead; int _alertTail; }; Осталось только дописать логику обработчиков команд и зарегистрировать их в маршрутизаторе. Обработчиков, кроме PingHandler получилось целых три: батарейка, пушка и общий статус всего и сразу. Особого интереса они не представляют, поэтому, кому интересно, просто гляньте исходник.В планах на будущее допиливание DisplayController, чтоб тот мог показывать не только константные строки, но и например строки, полученные как параметры команды. Проблема тут в том, что нету никакого освобождения памяти и сигнализации о том, что сообщение отображено и удалено из DisplayController.Также планирую дописать обработчик, который будет показывать в трее микрофон и динамик при подключении соответствующего звукового канала.

Собственно, все, спасибо за внимание. Надеюсь, что кому-нибудь мой опыт пригодится. Весь код, кроме скетча лежит на github’е или на Яндекс-диске, а сам скетч выглядит так:

Скетч #include #include #include

Tank tank (19200, 0×3F, A0, 4, 5, 6);

void setup () { tank.Setup (); }

void loop () { tank.Loop (); }

© Habrahabr.ru