Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android-устройства

в 15:38, , рубрики: .net, android, diy или сделай сам, микроконтроллеры, Песочница, метки: , , ,

image
Простой проект с описанием изготовления 4WD машинки с управлением от Android-устройства через Bluetooth канал. Управление машинкой происходит при помощи акселерометра, путем наклона планшета/смартфона. Видео работы смотрите в конце статьи. Все исходные тексты прилагаются.

Инструментарии разработки: Java/Eclipse для Android и .NET Micro Framework/Visual C# Express для микроконтроллера.

Вступление

Я не сторонник различных навороченных смартфонов, айпэдов и пр. крутых технологических гаджетов. С 2001 г. имею всего лишь второй телефон: Samsung B2700 (первый был Siemens C55, работающих до сих пор у бабушки). Но по роду деятельности понадобилось устройство для доступа по SSH к серверам, да и к Web-интерфейсу, когда я был в поездках. В связи с чем был куплен один из самых дешевых китайских Android планшетов: Ainol Aurora. Работал он вполне нормально, и web, и ssh, консоль, и даже ftp-клиент, в общем свои функции выполнял на все 100%. Но нужен он был мне только в поездках, остальное время он тупо валялся выключенным дома, т.к. играть я не играю, читать с экрана не могу, для веб-серфинга есть стационарные ПК. Ну и решил я копнуть глубже, приспособить его еще для чего-нибудь. Первая идея была сделать умный дом, но часть системы уже функционировала, а ремонт закончил недавно, новые девайсы, кабеля, подводку питания и прочее городить не хотелось. Поэтому идею отбросил, однако ради спортивного интереса купил платку Seeeduino ADK Main Board и немного поигрался с передачей информации по USB соединению (Open Accessory и MicroBridge). В процессе работы выяснил, что хоть на планшете и стоит Android 4.0.3, то это не означает, что он поддерживает Open Accessory. Поэтому игрался с режимом MicroBridge, обмен данными происходил успешно, однако все равно остались кое какие непонятные мне моменты…

В итоге, я пришел к 2-ой идее: связать устройство по радиоканалу. Wi-Fi модули дороговаты, да и хотелось начать с чего-нибудь попроще, поэтому купил на ebay парочку Serial Bluetooth модулей по цене 6-7$ за штуку. В качестве реальной задачи решил сделать управляемую машинку с Android'а. На тот момент, в зарубежном интернете было уже немало подобных проектов, но в большей их части устройство управлялось с стандартной программы Bluetooth Terminal, путем ввода команд с клавиатуры. Это конечно все хорошо, но очень неудобно. Попадались проекты и с собственным Android-ПО, но как правило управление происходило при помощи экранных клавиш. Хотелось придумать что-то оригинальное, а т.к. почти все планшеты оснащены встроенным акселерометром, то решено было его и задействовать. Опыта кодинга для Android'а не было, также как и практически не имел дела с современными языками ООП. Но пришлось немного разобраться с Java и особенностями написания приложений по Android. И в данной статье я хочу описать изготовление простой РУ-bluetooth машинки, с управлением от Android-девайса.

Для реализации задуманного понадобится:
1. Android устройство
2. Контроллер (ШИМ, UART)
3. Bluetooth модуль
4. Драйвер двигателей
5. Платформа для машинки

Я думаю, что основная масса посетителей хабра — программисты и люди так или иначе связанные с ИТ. Поэтому им малоинтересны будут варианты, где в качестве контроллера будут использоваться PIC, STM32, Arduino и т.п. Именно поэтому я решил задействовать валявшийся без дела контроллер FEZ Panda II с .NET Micro Framework. Разработка ПО для данного контролера ведется в среде Visual C# Express, т.е. для большинства программистов это будет привычный инструментарий, хотя .NET там урезанный и многих функций нет.

Но обо всем по порядку. Итак, как уже было сказано выше необходим Android планшет или смартфон, желательно с акселерометром (без него наверное сейчас их и не выпускают).

Контроллер — любой под .NET Micro Framework. Т.е. подойдут Netduino, платы GHI Electronics и др. Я использовал FEZ Panda II:
Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android устройства
В качестве Bluetooth модуля использован китайский UART модуль HC-06. Подойдут любые Serial Bluetooth. Лучше брать с штыревыми выводами, чтобы не пришлось паять, т.к. расстояние между выводами очень маленькое. Стоимость такого модуля на AliExpress/eBay составляет в среднем 5-10$.
Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android устройства
В качестве драйвера я заюзал платку с микросхемой L298N, которая представляет собой сдвоенный мостовой драйвер двигателей и предназначена для управления DC и шаговыми двигателями. Цена вопроса 4-5$.
Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android устройства
Для платформы машинки можно использовать все что угодно с 2 или 4 DC моторами. Я купил на eBay готовое 4WD шасси для DIY-проектов с 4-мя DC-моторчиками.
Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android устройства
Ну и дополнительно понадобились аккумуляторы, темроусадка, хомуты, соединительные провода и пр.

Сборка шасси

4WD платформа от DFRobot поставляется в разобранном виде:
Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android устройства
Сборка не сложная, единственное, что колеса очень туго надеваются на оси. Немного поработал катэром и встали как родные.

В отсек с моторчиками я поместил плату L298N, подключил их к ней, и вывел наружу 6 проводов: 4 для управления, GND и питание.
Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android устройства
Подробную сборку 4WD платформы я описывать не буду, все равно у каждого она будет своя и даже если и будет шасси 4WD от DFRobot, то полюбому будут какие-то свои коррективы в зависимости от находящегося дома оборудования и материалов.

Bluetooth модуль я использовал китайский HC-06. Рекомендую брать сразу с штыревыми выводами, чтобы не замарачиваться с пайкой.
Необходимые пины модуля HC-06:
UART_TX (pin 1), UART_RX (pin 2)
3,3V (pin 12) — питание 3.3В.
GND (pin 13) — общий.
PIO1 (pin 24) — индикатор рабочего режима. Если соединение не установлено — то светодиод мигает, если установлено, то постоянно горит.

На плату L298N необходимо от контроллера привести 5 проводов: GND (Общий), IN1, IN2, IN3, IN4 — входы управления двигателями (ШИМ и направление вращения для левого двигателя и тоже самое для правого).

Питание DC двигателей осуществляется от Li-Po аккумуляторов 3.7В 1100 мА. Контроллер питается от отдельного аккумулятора 3.7В (хотя требуется 5В, но прекрасно работает и от 3.7В). Питание Bluetooth модуля берется с платы FEZ.
Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android устройства
В проводах наблюдается большой беспорядок из-за того, что до этого проект собирался на STM32 и был разобран специально для реализации с контроллером FEZ Panda II. В дальнейшем, машинка будет опять на STM32 и Arduino с кое-какими усовершенствованиями. Но это тема другой статьи, возможно…

Схема подключения выглядит следующим образом:
Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android устройства
Плату FEZ к платформе прикрепил при помощи 2-х стороннего скотча:
Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android устройства
Далее, все было собрано и подключено. Получилось так:
Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android устройства

Программное обеспечение

Итак, в приложении для Android я реализовал 3 способа управления:
1. Акселерометром — путем наклона устройства
2. Кнопками — на экране смартфона прорисовывается 4 кнопки: вперед, назад, поворот влево, поворот вправо.
3. Touch-управление. Данный способ я подсмотрел в игре Death Rally, захотелось попробовать реализовать. Честно сказать не очень удобно получилось, но оставил как есть.

Все вычисления происходят в Android-приложении. На контроллер поступают готовые данные для ШИМ, направления вращения и команды для работы с Flash-памятью. Т.о. получилось универсальное приложение с гибкими настройками, которое может подойти под разные типы контроллеров, в разными значениями ШИМ и т.п. Собственно, что уже частично и сделано: данный проект уже реализован на STM32, Arduino и FEZ Panda II. В ближайшее время есть в планах реализовать под другие платформы: MSP430, PIC и др.

Пример команды, передаваемой через Bluetooth:
L-60rR-100r
Символ L означает команды для левого двигателя, знак минус — вращение назад, 60 — значение ШИМ. R — для правого двигателя, 100 — значение ШИМ (максимальное значение для FEZ Panda II). Для Arduino к примеру, максимальное значение ШИМ равно 255, его можно задавать в настройках Android приложения. По данной команде машинка будет двигаться назад, т.к. левые колеса буду вращаться немного медленнее правых, но ее будет слегка поворачивать влево. Как видим, такой способ управления больше подходит для гусеничной платформы.

Fw1035t — команда записи в Flash-память микроконтроллера значения таймаута (через какое время машинка остановиться при потери связи или неполучении команды). Первая цифра 1 означает включение таймаута (соответственно 0 — выключен). Для получения значения в секундах необходимо 035 разделить на 10, т.е. в данном случае таймаут составит 3.5 секунды. Соответственно диапазон значений от 0.1 до 99.9 секунд.

К машинке можно подцепить множетсво дополнительных каналов: фары, сигнал, пиропатрон и многое другое. Команда для доп. канала: H1r — где 1-включить. Соответственно если подать 0, то выключить. В представленном ПО реализован только 1 дополнительный канал, однако по аналогии с ним можно сделать сколько их угодно.

Кстати, символы команд можно также задавать в настройках Android приложения. Сами настройки задаются с главного активити, вот их скриншот:
Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android устройства
Обратите внимание, что MAC-адрес Bluetooth модуля задается в настройках приложения. Узнать его можно при помощи какой-нибудь программы для работы с Bluetooth, к примеру при помощи Bluetooth Terminal или Bluetooth Chat. Конечно удобнее было бы сделать поиск устройств и сопряжение в самом ПО, но честно признаюсь у меня не хватило ума и опыта, провозился с этим вопросом дня 2, читал форумы и Stack Exchange, но так и не получилось. Может быть в дальнейшем вернусь к этому вопросу.

Исходный код приложения для Android я приводить не буду, т.к. он достаточно обьемен. Кому интересно, внизу статьи полный проект для Eclipse. Отмечу лишь, что в приложении используется главное activity с кнопками меню и настройками, а также 4 дополнительных activity — 3 для управления и один для работы с памятью микроконтроллера.
Машинка на контроллере с .NET Micro Framework, управляемая акселерометром Android устройства
Работу с Bluetooth я вынес в отдельный класс cBluetooth. Для приема данных от BT создается отдельный поток thread, который в случае приема данных помещает их в очередь сообщений Handler. Далее, в основном потоке происходит обработка этих сообщений и в активити выводятся надписи и реализуются другие действия.

Исходник для .NET Micro Framework:

using System;
using System.IO.Ports;
using System.Threading;
using System.Text;

using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;

using GHIElectronics.NETMF.Hardware;
using GHIElectronics.NETMF.FEZ;

namespace CxemCAR
{
    public class Program
    {
        public const char cmdL = 'L';       // команда UART для левого двигателя
        public const char cmdR = 'R';       // команда UART для правого двигателя
        public const char cmdH = 'H';       // команда UART для доп. канала 1 (к примеру сигнал Horn)
        public const char cmdF = 'F';       // команда UART для работы с EEPROM памятью МК для хранения настроек
        public const char cmdr = 'r';       // команда UART для работы с EEPROM памятью МК для хранения настроек (чтение)
        public const char cmdw = 'w';       // команда UART для работы с EEPROM памятью МК для хранения настроек (запись)

        //public const int t_TOut = 2500;     // кол-во миллисекунд, через которое машинка останавливается при потери связи
        static int sw_autoOFF;
        static int autoOFF = 2500;
        static byte[] storage = new byte[InternalFlashStorage.Size];                                // переменная для хранения значений FLASH памяти МК

        static OutputPort MotorL_d = new OutputPort((Cpu.Pin)FEZ_Pin.Digital.Di4, false);           // направление вращения двигателя 1
        static OutputPort MotorR_d = new OutputPort((Cpu.Pin)FEZ_Pin.Digital.Di7, false);           // направление вращения двигателя 2
        static OutputPort Channel1 = new OutputPort((Cpu.Pin)FEZ_Pin.Digital.Di8, false);           // доп. канал 1
        static PWM MotorL = new PWM((PWM.Pin)FEZ_Pin.PWM.Di5);                                      // ШИМ вывод для управления двигателем 1 (левый)
        static PWM MotorR = new PWM((PWM.Pin)FEZ_Pin.PWM.Di6);                                      // ШИМ вывод для управления двигателем 2 (правый)
        static SerialPort UART1 = new SerialPort("COM1", 9600);                                     // новый объект UART1 (порт COM1)
        static Timer timerTO;                                                                       // таймер
       
        public static void Main()
        {
            byte[] L_Data = new byte[4];        // строковый массив для данных мотора L
            byte L_index = 0;                   // индекс массива
            byte[] R_Data = new byte[4];        // строковый массив для данных мотора R
            byte R_index = 0;                   // индекс массива
            byte[] H_Data = new byte[1];        // строковый массив для доп. канала
            byte H_index = 0;                   // индекс массива
            byte[] F_Data = new byte[8];        // строковый массив данных для работы с EEPROM
            byte F_index = 0; 
            char command = ' ';                 // команда: передача координат R, L, H, F или конец строки

            int i_tmp_L = 0;
            int i_tmp_R = 0;
            int i_tmp_H = 0;

            byte[] incomingByte = new byte[1];  // байт с UART
          
            UART1.Open();
            UART1.Flush();

            timerTO = new Timer(new TimerCallback(TimeOut), null, autoOFF, autoOFF);  // инициализация таймера потери связи
            timer_init();                                                             // инициализируем программный таймер

            while (true)
            {
                int read_count = UART1.Read(incomingByte, 0, 1);        // считываем данные
                if (read_count > 0)                                     // пришли данные?
                {
                    if (incomingByte[0] == cmdL)                        // если пришли данные для мотора L
                    {
                        command = cmdL;                                 // текущая команда
                        Array.Clear(L_Data, 0, L_Data.Length);          // очистка массива
                        L_index = 0;                                    // сброс индекса массива
                    }
                    else if (incomingByte[0] == cmdR)                   // если пришли данные для мотора R
                    {
                        command = cmdR;                                 // текущая команда
                        Array.Clear(R_Data, 0, R_Data.Length);          // очистка массива
                        R_index = 0;                                    // сброс индекса массива
                    }
                    else if (incomingByte[0] == cmdH)                   // если пришли данные для доп. канала 1
                    {
                        command = cmdH;                                 // текущая команда
                        Array.Clear(H_Data, 0, H_Data.Length);          // очистка массива
                        H_index = 0;                                    // сброс индекса массива
                    }
                    else if (incomingByte[0] == cmdF)                   // если пришли данные для доп. канала 1
                    {
                        command = cmdF;                                 // текущая команда
                        Array.Clear(F_Data, 0, F_Data.Length);          // очистка массива
                        F_index = 0;                                    // сброс индекса массива
                    }
                    else if (incomingByte[0] == 'r') command = 'e';    // конец строки
                    else if (incomingByte[0] == 't') command = 't';    // конец строки для команд работы с памятью


                    if (command == cmdL && incomingByte[0] != cmdL)
                    {
                        if (ValidData(incomingByte[0]))
                        {
                            L_Data[L_index] = incomingByte[0];              // сохраняем каждый принятый байт в массив
                            if (L_index < (L_Data.Length - 1)) L_index++;   // увеличиваем текущий индекс массива
                        }
                    }
                    else if (command == cmdR && incomingByte[0] != cmdR)
                    {
                        if (ValidData(incomingByte[0]))
                        {
                            R_Data[R_index] = incomingByte[0];
                            if (R_index < (R_Data.Length - 1)) R_index++;
                        }
                    }
                    else if (command == cmdH && incomingByte[0] != cmdH)
                    {
                        if (ValidData(incomingByte[0]))
                        {
                            H_Data[H_index] = incomingByte[0];
                            if (H_index < (H_Data.Length - 1)) H_index++;
                        }
                    }
                    else if (command == cmdF && incomingByte[0] != cmdF)
                    {
                        F_Data[F_index] = incomingByte[0];
                        if (F_index < (F_Data.Length - 1)) F_index++;
                     }
                    else if (command == 'e')                                // если приняли конец строки
                    {
                        timerTO.Dispose();                                  // останавливаем таймер потери связи
                        string tmp_L = new string(System.Text.UTF8Encoding.UTF8.GetChars(L_Data));      // формируем строку из массива
                        string tmp_R = new string(System.Text.UTF8Encoding.UTF8.GetChars(R_Data));
                        string tmp_H = new string(System.Text.UTF8Encoding.UTF8.GetChars(H_Data));

                        try
                        {
                            if (tmp_L != null) i_tmp_L = int.Parse(tmp_L);                              // и пытаемся преобразовать в int
                            if (tmp_R != null) i_tmp_R = int.Parse(tmp_R);
                            if (tmp_H != null) i_tmp_H = int.Parse(tmp_H);
                        }
                        catch { 
                            Debug.Print("Error: convert String to Integer"); 
                        }


                        if (i_tmp_L > 100) i_tmp_L = 100;
                        else if (i_tmp_L < -100) i_tmp_L = -100;
                        if (i_tmp_R > 100) i_tmp_R = 100;
                        else if (i_tmp_R < -100) i_tmp_R = -100;

                        Control4WD(i_tmp_L, i_tmp_R, i_tmp_H);
                        timerTO.Change(autoOFF, autoOFF);                                               // таймер считает сначала
                    }
                    else if (command == 't')                                                            // если приняли конец строки для работы с памятью
                    {
                        Flash_Op(F_Data[0], F_Data[1], F_Data[2], F_Data[3], F_Data[4]);
                    }
                }
            }
        }

        static void Flash_Op(byte FCMD, byte z1, byte z2, byte z3, byte z4)
        {
            if (FCMD == cmdr && sw_autoOFF != 255)                              // если команда чтения EEPROM данных 
            {
                byte[] buffer = Encoding.UTF8.GetBytes("FData:");               // подготавливаем байтовый массив для вывода в UART
                UART1.Write(buffer, 0, buffer.Length);                          // запись данных в UART
                byte[] buffer2 = new byte[4] { storage[0], storage[1], storage[2], storage[3] };
                UART1.Write(buffer2, 0, buffer2.Length);
                byte[] buffer3 = Encoding.UTF8.GetBytes("rn");
                UART1.Write(buffer3, 0, buffer3.Length);
            }
            else if (FCMD == cmdw)                                              // если команда записи EEPROM данных
            {
                byte[] varToSave = new byte[InternalFlashStorage.Size];
                varToSave[0] = z1;
                varToSave[1] = z2;
                varToSave[2] = z3;
                varToSave[3] = z4;
                InternalFlashStorage.Write(varToSave);                          // запись данных в FLASH память МК
                timer_init();		                                            // переинициализируем таймер
                byte[] buffer2 = Encoding.UTF8.GetBytes("FWOKrn");            // подготавливаем байтовый массив для вывода в UART
                UART1.Write(buffer2, 0, buffer2.Length);	                    // посылаем сообщение, что данные успешно записаны
            }
        }

        static void timer_init()
        {
            InternalFlashStorage.Read(storage);                                 // чтение данных с FLASH памяти
            sw_autoOFF = storage[0];
            if(sw_autoOFF == '1'){                                              // если таймер останова включен
                byte[] var_Data= new byte[3];
                var_Data[0] = storage[1];
                var_Data[1] = storage[2];
                var_Data[2] = storage[3];
                string tmp_autoOFF = new string(System.Text.UTF8Encoding.UTF8.GetChars(var_Data));
                autoOFF = int.Parse(tmp_autoOFF)*100;
                timerTO.Change(autoOFF, autoOFF);                               // изменяем параметры таймера
            }
            else if(sw_autoOFF == '0'){
                timerTO.Dispose();                                              // выключаем таймер
            } 

            Debug.Print("Timer Init" + autoOFF.ToString());
        }

        static void TimeOut(object o)
        {
            //Debug.Print(DateTime.Now.ToString());
            Control4WD(0, 0, 0);                                                // при таймауте останавливаем машинку
        }

        public static void Control4WD(int mLeft, int mRight, int Horn)
        {
            bool directionL, directionR;                                        // направление вращение для L298N
            int valueL, valueR;                                                 // значение ШИМ M1, M2 (0-100)

            if (mLeft > 0)
            {
                valueL = mLeft;
                directionL = false;
            }
            else if (mLeft < 0)
            {
                valueL = 100 - System.Math.Abs(mLeft);
                directionL = true;
            }
            else
            {
                directionL = false;
                valueL = 0;
            }

            if (mRight > 0)
            {
                valueR = mRight;
                directionR = false;
            }
            else if (mRight < 0)
            {
                valueR = 100 - System.Math.Abs(mRight);
                directionR = true;
            }
            else
            {
                directionR = false;
                valueR = 0;
            }

            if (Horn == 1)
            {
                Channel1.Write(true);
            }
            else Channel1.Write(false);

            //Debug.Print("L:" + valueL.ToString() + ", R:" + valueR.ToString());
            
            MotorL.Set(30000, (byte)(valueL));
            MotorR.Set(30000, (byte)(valueR));

            MotorL_d.Write(directionL);
            MotorR_d.Write(directionR);
        }

        public static bool ValidData(byte chIncom)                  // проверка поступившего символа на принадлежность к "0..9" или "-"
        {
            if ((chIncom >= 0x30 && chIncom <= 0x39) || chIncom == 0x2D) return true;
            else return false;
        }
    }
}

Сама программа под FEZ не очень сложная — в цикле считываем данные с UART и формируем соответствующие массивы. Как только пришел символ окончания передачи (r или t), то данные преобразовываются и передаются в соответствующие функции Control4WD() или Flash_Op(). В функции Control4WD() происходит вычисление направления, а также небольшие расчеты и затем данные выводятся на соответствующие пины контроллера.

Видео работы:

Скачать APK файл для установки на Android устройство
Скачать проект для Eclipse
Скачать проект для Visual C# 2010 Express

Автор: koltykov

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js