Это продолжение моей статьи об устройстве для вращения Веб-камеры (предыдущую статью можно найти тут).
В этой части я расскажу о создании программной части: прошивки для микроконтроллера ATtiny85 и программы для компьютера, для того что бы иметь возможность управлять устройством.
Для создания прошивки микроконтроллера и ПО я использовал готовый проект, переработав его под себя.(Эта информация обязательна к прочтению, так как подробности описывать я не буду).
Прошивка микроконтроллера
Прошивка в зависимости от команды программы управления устанавливает ножки 1, 3, 4, 5 в логичную 1 или 0. Также если на протяжении 2 секунд не поступает никакая команда – то все четыре ножки устанавливаются в 0, это сделано для того, что бы при сбое работы программы на ПК устройство не зацикливалось на выполнении последней команды (писал прошивку на AVR Studio4, эта среда не дружит с кириллицей, поэтому проверяйте адреса. Также для тестирования прошивки можно использовать PROTEUS):
...
if(bytesRemaining == 0) // Все данные получены
{ // Выставим значения на PORTB
if ( pdata.b1 ) //Запись сотояния ножки 1 из структуры
PORTB |= _BV(1);
else
PORTB &= ~_BV(1);
if ( pdata.b2 ) //Запись сотояния ножки 3 из структуры
PORTB |= _BV(3);
else
PORTB &= ~_BV(3);
if ( pdata.b3 ) //Запись сотояния ножки 4 из структуры
PORTB |= _BV(4);
else
PORTB &= ~_BV(4);
if ( pdata.b4 ) //Запись сотояния ножки 5 из структуры
PORTB |= _BV(5);
else
PORTB &= ~_BV(5);
}
...
if(i==200) //сброс 1,3,4,5 ножки в 0 через 2сек
{
PORTB &= ~_BV(1);
PORTB &= ~_BV(3);
PORTB &= ~_BV(4);
PORTB &= ~_BV(5);
}
...
Недостача ножки
В Attiny85 всего 8 ножек. Две из них для питания микроконтроллера. Одна для сброса данных, а ещё две задействованы для использования USB интерфейса. Остается три свободных, но мне надо 4 ножки для управления двумя двигателями.
Выйти с этой ситуации можно двумя способами: первый – сдвиговый регистр (с его помощью можно увеличить количество управляемых выходов до нужного, но при этом придется пожертвовать скоростью и надёжностью. См. тут);
Другой – использовать fuse bit, что бы переопределить ножку Reset в обычную вход/выход. (Прочитать можнатут).
Я воспользовался вторым способом и установил в fuses RSTDISBL в 0. (При считывании fuses с помощью SinaProg у меня выскакивала ошибка. Решение которой нашёл тут).
При прошивке микроконтроллера с такими параметрами fuses, снова перепрошить его можно будет только высоковольтным программатором, поэтому необходимо перепроверять настройку, особенно в usbconfig.h. (Содержаться настройки для V-USB)
При неудачных прошивках необходимо вручную в «диспетчере устройств» удалить драйвер, который устанавливает ОС для вашего устройства. Иначе неправильно установленный драйвер будет использоваться дальше для всех последующих прошивок микроконтроллера.
Программа управления
Я использовал готовый код, поэтому проблем с настройкой связи у меня не возникло. Данная часть кода отвечает за формирование и отправку команд, а также за получение информации про состояние устройства (в данном случае за состояние ножек 1 3 4 5. Привожу толька класс):
#define Max 230
class Device{
class dataexchange_t // Описание структуры для передачи данных
{
public:
char b1; // Я решил написать структуру на 4 байта.
char b2; // На каждый байт подцепим ногу из PORTB. Конечно это
char b3; // не рационально (всего то 4 бита нужно).
char b4;
}data;
class Position{
public:
float x;
float y;
}positionTransform, cameraPosition; /* positionTransform - нужно для хранения трансформированных
градусных значений в мою систему cameraPosition - записуеться
текущее положение камеры в градусах */
HIDLibrary <dataexchange_t> hid; // создаем экземпляр класса с типом нашей структуры
unsigned char processTime; // для уменьшения количества ненужного обращения к устройству
FILE *f; // файл для сохранения последнего положения камеры
int SendData(dataexchange_t *);
public:
bool Connect();
void SetCameraPosition(short int *, short int *);
bool MoveCamera();
bool StopMoveCamera();
Device()
{
processTime = 10;
(int&)data = positionTransform.x = positionTransform.y = 0;
f = fopen("CameraPosition.txt","r+");
fscanf(f,"%f %f", &cameraPosition.x, &cameraPosition.y);
if(cameraPosition.x < -Max || cameraPosition.y < -Max || cameraPosition.y > Max || cameraPosition.x > Max)
cameraPosition.x = cameraPosition.y = 0;
}
~Device()
{
fclose(f);
}
};
...
Так как я отказался от вращения камеры на 360 градусов, пришлось програмно ввести ограничение на 230 градусов, то есть камера вращается влево (230 градусов) и вправо (-230 градусов) на 230 градусов относительно начала. (Стоит отметить, что программное ограничение это худшее решение такой проблемы, лучше было бы установить механические выключатели, которые выключали бы одну из цепей питания двигателя).
Также я ввёл специфичную координатную ось для управления поворотом камеры, чтобы не нагружать ОС постоянной отправкой команд. Пользователь водит на сколько градусов надо повернуть камеру относительно текущего положения. Далее введённые градусы преобразуются в несколько команд, которые с интервалом в 2 секунды должны будут отправлены на устройство.
...
// Трансформация градусной координаты в мою по горизонтали(камера вращается после команды 2сек)
if(*x > 0)
positionTransform.x = (*x * 0.20832); // разная скорость вращения по часовой стрелке и против
else positionTransform.x = (*x * 0.225);
// Трансформация градусной координаты в мою по вертикали(камера вращается после команды 2сек)
if(*y > 0)
positionTransform.y = (*y * 0.20832);
else positionTransform.y = (*y * 0.225);
...
Устройство сначала поворачивает камеру по горизонтали, а потом по вертикали, чтобы не перегружать USB порт одновременным питанием двух двигателей.
В результате получилось, что микроконтроллер управляется программой с компьютера, который в свою очередь управляет драйвером двигателя (Я использовал L293), а он уже управляет двумя двигателями.
Передача видео- и аудио-сигнала
Мне не обходимо было передать сигнал с камеры и управлять ей на расстоянии. Выход я нашёл в использовании API Skype (Проблема была только в том, что API Skype больше не поддерживается, поэтому есть баги). Для использования API Skype необходимо на диск, где установлена ОС скопировать файл Skype4COM.dll.
Моя программа для Skype мониторит чат с клиентом – пользователем, который использует моё устройство. При введении пользователем в чате команды старта работы камеры (В данном случае команда «0000») программа сделает видео-звонок и включит устройство. После этого пользователь может управлять камерой путём введения следующих команд в чат:
«StopMove MoveTo('X'@'Y') VideoOn VideoOff Wait'TimeInSec'». (На команду VideoOff скайп сначала выключит камеру, а потом сразу включает, если в ручном режиме сначала включить видео показ, то эта команда работает нормально):
...
char comandName[] = {"###"}; // Логин клиента
char comandStart[] = {"0000"}; // Команда старта
int timeWait = 500; // Максимальное время бездействия клиента
// Инициализмруем COM соединение
CoInitialize(NULL);
// Создаем Skype объект
SKYPE4COMLib::ISkypePtr pSkype(__uuidof(SKYPE4COMLib::Skype));
// Соединяемся с Skype API
pSkype->Attach(6,VARIANT_TRUE);
_bstr_t comand;
IChatMessagePtr curMessage, privMessage = NULL;
COleDateTime startTime(time(NULL));
for(;;)// нужно еще условие выхода допилить
{
curMessage = pSkype->GetMessages((_bstr_t)comandName)->GetItem(1); // Получаем последние сообщение от клиента
comand = curMessage->GetBody(); // Получаем текст сообщения
if(!strcmp((char*)comand,comandStart) && (privMessage == NULL ||
privMessage->GetId() != curMessage->GetId()) && (startTime.m_dt < curMessage->GetTimestamp()))
{
privMessage = curMessage;
curMessage->PutSeen(true); // Отмечаем полученное сообщение как прочитанное
ControlDevice(pSkype, comandName, timeWait, privMessage);
}
else Sleep(5000);
}
...
_bstr_t comand;
IChatMessagePtr curMessage;
int timeToSendMessage = (timeWait/100)*80;
Device device;
ICallPtr call;
// Проверка (Если звоним клиенту, то положить трубку)
for(int i = pSkype->GetActiveCalls()->GetCount(); i != 0 ; i--)
{
call = pSkype->GetActiveCalls()->GetItem(i); // Получить текущие звонки
if(!strcmp((char*)(call->GetPartnerHandle()),comandName))
{
call->Finish(); // Положить трубку
Sleep(2000);
break;
}
}
call = pSkype->PlaceCall(_bstr_t(comandName), L"", L"", L""); // Звоним клиенту
pSkype->SendMessage(_bstr_t(comandName), _bstr_t(L"З'єднання розпочате"));
if(!device.Connect()) // Подключаемся к устройству
pSkype->SendMessage(_bstr_t(comandName), _bstr_t(L"Пристрій управління не підключен"));
for(int i = 0;i < timeWait && call->Status != clsInProgress;i++) // Ожидания поднятия трубки клиентом
Sleep(20);
if(call->Status == clsInProgress)
{
Sleep(2000);
call->StartVideoSend(); // Включить видео(иногда не работает)
for(int time = 0; call->Status == clsInProgress;time++)
{
if(time >= timeWait) // Если время ожидания кончилось - положить трубку
{
call->Finish();
break;
}
curMessage = pSkype->GetMessages((_bstr_t)comandName)->GetItem(1);
if(curMessage->GetId() != privMessage->GetId()) // Ловим сообщения в чате
{
privMessage = curMessage;
if(time > 0)
time = 0;
if(!ComandToDevice((char*)curMessage->GetBody(),&device,pSkype,comandName,&time,call))// Отправка сообщения для обработки
pSkype->SendMessage(_bstr_t(comandName), _bstr_t(L"Команда не виконана"));
curMessage->PutSeen(true);
}
Sleep(200);
if(!device.MoveCamera())
{
pSkype->SendMessage(_bstr_t(comandName), _bstr_t(L"Проблеми з пристроєм управління"));
device.StopMoveCamera();
}
if(timeToSendMessage == time)
pSkype->SendMessage(_bstr_t(comandName), _bstr_t(L"Час очікування команди спливає"));
}
}
pSkype->SendMessage(_bstr_t(comandName), _bstr_t(L"З'єднання завершено"));
}
...
Видео демонстрации работы устройства.
Полностью весь проект можна взять тут.
Автор: Kotovoin