Следящая платформа
Следящие, они же наклонно-поворотные (pan and tilt) платформы широко используются для установки на них телекамер и создания следящих систем. Такая платформа представляет собой устройство из двух сервоприводов, системы управления и элементов крепежа. В нашем случае вместо телекамеры будет использоваться модуль TrackingCam, в качестве системы управления — робототехнический контроллер CM-530, в качестве сервоприводов — DYNAMIXEL AX-12W, а само крепление модуля к сервоприводам напечатано на 3D принтере.
Цель практического занятия: освоение применения технического зрения в качестве датчика следящих систем управления; повышение навыков работы с микроконтроллерными модульными вычислительными устройствами и датчиками, навыков программирования в области обмена данными между ними; получение частных навыков: освоение модуля TrackingCam и повышение навыков работы с робототехническими конструкторами Robotis.
Задача: Из конструктивных элементов от Robotis, сервоприводов Dynamixel и модуля TrackingCam собрать и запрограммировать платформу, направляющую телекамеру на заданный предмет.
Последовательность выполнения задания:
- Собрать платформу на основе контроллера CM-530, приводов Dynamixel и модуля технического зрения TrackingCam .
- Настроить контроллер CM-530 и приводы Dynamixel в утилите RoboPlus Manager.
- Настроить телекамеру TrackingCam на распознавание мяча в программе TrackingCam App.
- Написать программу системы управления для контроллера CM-530 в среде R+Task 2.0.
- Проверить работу следящей платформы.
Сборка платформы.
Процесс сборки такой следящей платформы (рис. 8.1 (а), (б)) достаточно прост и показан далее (рис. 8.2)
Шаг 1.
Соединяем 2 пластины 9х12 как показано на рисунке 8.2 (а)
а)
б)
Рис. 8.1 Внешний вид следящей платформы а) - вид спереди, б)- вид сбоку.
Шаг 2.
Устанавливаем на пластины контроллер СМ-530 и держатель аккумулятора (рис. 8.2 (б))
Шаг 3.
Выполняем подключение 2х Dynamixel-кабелей к первому сервоприводу (рис. 8.2 (в))
а)
б)
в)
Рис. 8.2 Сборка следящей платформы
а) - Шаг 1, б) - Шаг 2, в) - Шаг 3
Шаг 4. Устанавливаем и закрепляем на сервоприводе крепежную пластину (рис. 8.2. (г))
Шаг 5. Закрепляем собранную конструкцию на платформе с контроллером и аккумулятором (рис. 8.2. (д))
Шаг 6. Устанавливаем на первый сервопривод пластину для крепления второго сервопривода (рис. 8.2. (е))
г)![[media/image48.png]]
д)![[media/image49.png]]
е)
Рис. 8.2 Сборка следящей платформы
г) - Шаг 4, д) - Шаг 5, е) - Шаг 6
Шаг 7. Устанавливаем второй сервопривод (рис. 8.2. (ж), (з))
Шаг 8. Устанавливаем на второй сервопривод поворотную платформу для крепления модуля технического зрения (рис. 8.2. (и))
Шаг 9. Соединяем проводами сервоприводы и контроллер, подключаем питание от аккумулятора. Платформа готова (рис. 8.2. (к))
ж)
з)
и)
к)
Рис. 8.2** Сборка следящей платформы
ж) - Шаг 7, з) - Шаг 7, и) - Шаг 8, к) - Шаг 9
На верхнюю часть платформы любым доступным способом крепится модуль TrackingCam любым удобным способом. Например, с помощью распечатанного на 3D принтере основания.
Основная задача pan and tilt платформы — обеспечить телекамере, в нашем случае модулю TrackingCam, возможность следить за определенным объектом, а именно всегда направлять объектив модуля точно на требуемый объект с помощью сервоприводов.
Чтобы система могла следить за объектом — оранжевым мячом — она должна обладать информацией о его текущем положении. В данном случае, в качестве датчика положения объекта используется система технического зрения в виде модуля TrackingCam, который необходимо настроить на распознавание мяча. Для этого необходимо воспользоваться утилитой TrackingCam App, задать цветовые и морфологические признаки объекта, настроить параметры порта ввода-вывода информации и сохранить настройки как было разобрано ранее.
После настройки телекамеры приступим к настройке самой платформы. Поскольку каждый сервопривод DYNAMIXEL адресуется по уникальному в системе ID-номеру, зададим, например, нижнему сервоприводу ID равный 1, а верхнему ID равный 2. , Сервоприводы нужно переключить в режим работы в качестве шарнира (Joint). Эти настройки выполняются в утилите RoboPlus Manager (рис. 8.3), входящей в набор стандартных приложений RoboPlus.
Рис. 8.3 Окно утилиты RoboPlus Manager
Запустим утилиту PoboPlus Manager. Подключим контроллер к компьютеру с помощью USB кабеля и включим его. Обратите внимание — для работы контроллера необходимо подать на него внешнее питание, либо от аккумулятора, либо от сети. В левом верхнем углу RoboPlus Manager необходимо выбрать COM-порт, под которым контроллер установился в системе и нажать на соседнюю кнопку — Connect. Произойдет подключение утилиты к контроллеру, сопровождающееся звуковым сигналом. В случае, если подключение выполнилось успешно, содержимое окна RoboPlus Manager изменится и в левой части окна можно будет наблюдать подключенные к контроллеру DYNAMIXEL — совместимые устройства, а в центральной — параметры этих устройств. Для настройки сервоприводов необходимо выбрать нужный привод и указать ему в правой стороне окна режим работы — Joint.
Узнать настройки какого привода (верхнего или нижнего) редактируются в данный момент можно, изменив положение его вала. Для этого нужно выбрать параметр Goal Position и с помощью появившегося джойстика в повращать привод. Затем нужно установить ему соответствующий параметр ID с помощью выпадающего списка в правом нижнем углу окна. После установки ID Нажать кнопку Apply. Для другого сервопривода действия следует повторить. После этого платформа готова к использованию.
Напишем управляющую программу. Для этого создадим новый проект в среде R+Task 2.0 под контроллер CM530. Для опроса телекамеры добавим в программу функцию TrackingCamParseBlobs, подробно рассмотренную ранее. Эта функция является основной для работы с модулем технического зрения, и в дальнейшем все программы будут завязаны именно на ее использование. Для сокращения объема кода удалим из этой функции все строки, отвечающие за вывод данных на экран. В итоге внешний вид функции будет как на рисунке 8.4.
Реализуем алгоритм управления платформой. Слежение за объектом будет выполняться следующим образом. С модуля TrackingCam запрашиваются данные о распознанном объекте. Если он виден в центральной части изображения, то модуль наведен на объект и действий не требуется. Иначе происходит поворот модуля на фиксированный угол вокруг одной из осей в ту сторону, в которую мяч смещен относительно оптической оси телекамеры наибольшим образом.
Для реализации алгоритма управления добавим в тело главной функции START PROGRAMM программы бесконечный цикл ENDLESS LOOP. Внутри этого цикла мы и разместим ту часть кода, которая будет отвечать за проверку условий и управлять сервоприводами. Алгоритм, по которому будет выполняться слежение за объектом можно описать следующим образом: Происходит опрос данных о распознанной области с модуля TrackingCam. Если центр распознанной области находится в центральной зоне видимости модуля, то считается что модуль наведен на объект корректно и никаких действий не происходит. Если же центр области ока-
Рис. 8.4 Вид функции TrackingCamParseBlobs
зывается смещен относительно центральной части зоны видимости модуля, то происходит доворот модуля в соответствующую сторону. Поворот для компенсации смещения объекта вдоль оси Х изображения будет осуществлять сервопривод с ID 1, а компенсации смещения вдоль оси Y сервопривод с ID 2. Эти команды будут выполняются следующим образом:
В первом случае осуществляется поворот сервопривода с ID 1 против часовой стрелки на
10 единиц (минимальных возможных углов поворота сервопривода), во втором случае этот же сервопривод поворачивается по часовой стрелке на 10
Данные команды реализованы в среде R+Task с помощью инструкции
COMPUTE.
Таким образом, при изменении положения распознанного объекта относительно оптической оси телекамеры будет осуществляться поворот каждого сервопривода в нужную сторону. Для этого создадим конструкцию из серии условных переходов с оператороами IF — ELSE после вызова функции TrackingCamParseBlobs и для оператора в качестве условия укажем проверку положения BlobCx или BlobСy относительно границ центральной зоны видимости телекамеры (рис. 8.5).
В данном примере в качестве условий было определено следующее - BlobCx сравнивается со значениями 106 и 212, а BlobCy — со значениями
Рис. 8.5 Схема разметки области видимости TrackingCam
80 и 160. В зависимости от условия в тело каждого условного перехода IF добавляем команду управления сервоприводами — если BlobCx меньше 106, то поворачиваем платформу против часовой стрелки, если боль-
ше 212, то по часовой стрелке. Если BlobCy меньше 80 то поворачиваем платформу вверх, если больше 160, то поворачиваем платформу вниз. После последней инструкции ELSE добавим команды остановки сервоприводов. Это необходимо в том случае, если модуль потеряет объект из видимости. В результате получившийся код будет выглядеть следующим образом (рис. 8.6)
Рис. 8.6 Код управляющей программы, отвечающий за движение сервоприводов
Для удобства добавим перед бесконечным циклом ENDLESS LOOP две инструкции инициализации переменных LOAD с указанием установки каждого сервопривода в начальную позицию. Итоговый вид основной функции показан на рисунке 8.7
Управляющая программа для следящей платформы готова. Если ее загрузить в контроллер платформы и запустить контроллер в автономном режиме, то модуль технического зрения TrackingCam будет всегда направлен на оранжевый мяч, как бы его не смещали относительно центра зоны видимости модуля.
Рис. 8.7 Основная функция управляющей программы
Следование вдоль сложной линии
Одной из типовых соревновательных задач, связанных со следование вдоль линии является задача следования вдоль линии, состоящей из отдельных объектов разной формы, которые могут быть так же и разного цвета. Такая задача до недавнего времени являлась задачей для самых старших участников, так как ее решение требовало достаточно глубоких знаний в области работы с одноплатными компьютерами, операционной системой Linux и специализированными библиотеками для обработки видео сигнала, такими как OpenCV. Такой высокий порог вхождения в эти соревнования отсеивал большой процент участников, желающих принять в нем участие, в первую очередь школьников, не имеющих должной базовой подготовки. Использование готового модуля TrackingCam в настоящее время сильно понижает порог вхождения в данные соревнования, и позволяет принимать в них участие всем желающим.
В данном примере мы рассмотрим процесс подготовки платформы и разработки управляющей программы для автоматического следования платформы вдоль сложной линии, ориентируясь на ней с помощью TrackingCam. В данном примере мы рассмотрим следующие вопросы:
- Сборка и подготовка типовой платформы
- Настройка модуля технического зрения TrackingCam
- Разработка управляющей программы для движения вдоль сложной линии
- Проверка системы на практике
Для решения задачи следования по сложной линии достаточно использовать какую-либо типовую платформу, для удобства — дифференциального типа. Выбор такой платформы облегчит задачу разработки управляющей программы, т.к. в случае двухколесной дифференциальной платформы описание кинематики будет довольно простым. Таким образом соберем платформу, подобную показанной на рисунке 8.8.
В качестве управляющего контроллера воспользуемся arduino-подобной аппаратной платформой OpenCM, установленной на плате расширения Expansion Board 485, либо же на Stem Board. Питание обеспечим с помощью стандартного LiPo аккумулятора, выдающего номинальное напряжение в 11.1В.
Соединение сервоприводов рекомендуется выполнить последовательно и подключить получившуюся цепь к плате расширения Expansion Board.
Подключение модуля технического зрения TrackingCam выполняется с помощью стандартного 3х пинового Dynamixel кабеля к любом из свободных портов платы расширения.
Перед тем как приступить к разработке управляющей программы необходимо уточнить ID сервоприводов, установленных на роботе. Для
Рис. 8.8 Пример внешнего вида платформы для следования вдоль сложной линии
удобства условимся, что левый сервопривод имеет ID равным 1, а правый сервопривод имеет ID равный 2. Для того, чтобы уточнить и изменить сервопривод можно воспользоваться способом, рассмотренном в примере разработки следящей платформы. В отличии от следящей платформы, где сервоприводы использовались в режиме шарнира (сочленения) в данном примере сервоприводы должны использоваться в режиме колеса. Однако, поскольку в отличии от предыдущего примера здесь управляющим контроллером является OpenCM, являющийся более продвинутым по сравнению с CM530, то данный режим работы будет устанавливаться напрямую в коде управляющей программы.
Отличием от предыдущего примера, в котором модуль технического зрения отслеживал только одну область, и, соответственно, выдавал информацию только об одной области, в данном примере модуль должен отслеживать сразу несколько областей, соответствующих одному шаблону — в нашем случае одному цвету. Линия, вдоль которой необходимо двигаться роботу в данном случае представлена разрозненными черными фигурами разной формой — прямоугольниками со скругленными краями и кругами. Для отслеживание этих фигур настроим модуль по аналогии со случае отслеживание одной однотонной области. Поскольку линия представлена хоть и разными фигурами, но имеющими один цвет, то мы не будем задаваться вопросом их формы — в данном случае это для нас не важно. Изображения с камеры модуля и результат обработки отдельных элементов траектории представлены на рисунках 8.9 и 8.10
Как видно на представленных изображениях в кадр попадает сразу несколько распознаваемых областей, и, соответственно необходимо в дальнейшем для лучшего движения вдоль такой линии использовать информацию о всех областях, распознанных на одном кадре.
Рис. 8.9 Часть траектории из прямоугольников
Рис 8.10 Часть траектории из кругов
Сохраним настройки модуля в его память и перейдем к разработке управляющей программы.
В модели данного робота в качестве управляющего контроллера используется Arduino-подобный контроллер OpenCM. Для работы с ним используется среда разработки Robotis IDE. Разработку управляющей программы разобьем на 2 этапа:
На первом этапе напишем необходимые функции для движения робота вперед и поворотов, на втором добавим в программу работу с модулем технического зрения.
Для инициализации соединения для обмена данными по протоколу Dynamixel в коде программы необходимо указать шину — другими словами порты, на плате, к которым будет выполнено подключение сервоприводов и модуля технического зрения. Далее необходимо указать ID сервоприводов, которыми необходимо управлять. Все это делается в коде программы до функции setup() следующими командами:
#define DXL_BUS_SERIAL3 3
#define ID_NUM_1 1
#define ID_NUM_2 2
Dynamixel Dxl(DXL_BUS_SERIAL3);
В функции setup() производится непосредственно инициализация соединения командой
Dxl.begin(3);
И задание режима работы сервоприводов в качестве колес командами
Dxl.wheelMode(ID_NUM_1); Dxl.wheelMode(ID_NUM_2);
Затем создадим основные фукции, отвечающие за движение робота —
forvard, left, right, stop. Разместить их необходимо после функции loop.
void forward (){ Dxl.goalSpeed(ID_NUM_1, 400); Dxl.goalSpeed(ID_NUM_2, 400 | 0x400);
}
Налево:
void left (){
Dxl.goalSpeed(ID_NUM_1, 400 | 0x400); Dxl.goalSpeed(ID_NUM_2, 400 | 0x400);
}
Направо:
void right (){ Dxl.goalSpeed(ID_NUM_1, 400 );
Dxl.goalSpeed(ID_NUM_2, 400 );
}
Стоп:
void stop() { Dxl.goalSpeed(ID_NUM_1, 0);
Dxl.goalSpeed(ID_NUM_2, 0);
}
Для проверки правильности движения робота в функцию loop добавим вызов полученных функций в определенном порядке:
void loop() {
forward();
delay(2000);
right();
delay(2000);
left();
delay(2000);
stop();
delay(5000);
}
После загрузки кода в контроллер робот должен начать движение вперед и двигаться в течении 2х секунд, затем вращаться по часовой стрелке в течении 2х секунд и против часовой стрелки так же в течении двух секунд. После этого робот должен остановится на 5 секунд. Если робот повел себя именно так, значит платформа собрана верно и команды управления движением указаны так же верно. На текущем этапе код управляющей программы должен выглядеть следующим образом:
#define DXL_BUS_SERIAL3 3
#define ID_NUM_1 1
#define ID_NUM_2 2
Dynamixel Dxl(DXL_BUS_SERIAL3);
void setup() {
Dxl.begin(3);
Dxl.wheelMode(ID_NUM_1);
Dxl.wheelMode(ID_NUM_2);
}
void loop() {
forward();
delay(2000);
right();
delay(2000);
left();
delay(2000);
stop();
delay(5000);
}
void forward (){
Dxl.goalSpeed(ID_NUM_1, 400);
Dxl.goalSpeed(ID_NUM_2, 400 | 0x400);
}
void left (){
Dxl.goalSpeed(ID_NUM_1, 400 | 0x400);
Dxl.goalSpeed(ID_NUM_2, 400 | 0x400);
}
void right (){ Dxl.goalSpeed(ID_NUM_1, 400 );
Dxl.goalSpeed(ID_NUM_2, 400 );
}
void stop() { Dxl.goalSpeed(ID_NUM_1, 0);
Dxl.goalSpeed(ID_NUM_2, 0);
}
Добавим в самое начало программы код, отвечающий за инициализацию структуры, необходимой нам в дальнейшем:
struct TrackingCamBlobInfo_t
{
uint8 type;
uint16 cx;
uint16 cy;
uint32 area;
uint16 left;
uint16 right;
uint16 top;
uint16 bottom;
};
После нее проинициализируем массив, в котором будут хранится данных о всех распознанных областях:
TrackingCamBlobInfo_t blob[15];
Так же укажем ID камеры и проинициализируем переменную для хранения адреса считывания данных:
#define ID_NUM 51 uint16 addr = 16;
Для получения данных с модуля технического зрения о всех распознанных областях поместим в самый конец программы функцию работы с камерой, подробно разобранную выше в соответствующем примере:
void TrackingCamGetBlobs (){
uint16 curAddr = addr;
for (uint8 i = 0; i < 16; i++){
SerialUSB.print(i);
SerialUSB.print(« «);
blob[i].type = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2;
SerialUSB.print(blob[i].type);
SerialUSB.print(« «);
if (blob[i].type == 255 || blob[i].type == -1 || blob[i].type == 248){ SerialUSB.println(« «);
break;
}
blob[i].cx = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].cx);
SerialUSB.print(« «);
blob[i].cy = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].cy);
SerialUSB.print(« «);
blob[i].area = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].area);
SerialUSB.print(« «);
blob[i].left = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].left);
SerialUSB.print(« «);
blob[i].right = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].right);
SerialUSB.print(« «);
blob[i].top = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].top);
SerialUSB.print(« «);
blob[i].bottom = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.println(blob[i].bottom);
}
}
Теперь перейдем к самому алгоритму движения. Поскольку в область видимости камеры попадает сразу несколько объектов, которые можно использовать для определения характера (изгиба) траектории, то можно придумать некий хитрый алгоритм, который будет это учитывать. В нашем же примере для навигации мы будем использовать только один (ближайший) объект, на который и будет ехать робот. Поскольку мы всегда может получить его центр (с помощью переменной blob[i].cx, то при движении будем проверять нахождение центра этого объекта в определенных рамках. Для удобства разобьем область видимости камеры по оси х на 3 равные части и получим границы нашей области — 106 и 213 пикселей.
Внутри функции loop() заменим все ранее написанное на следующий код:
TrackingCamGetBlobs(); delay(30);
if (blob[0].cx < 106) left();
else if (blob[0].cx > 213) right();
else forward();
На этом разработка программы для движения робота вдоль линии окончена. Полный код программы представлен ниже:
struct TrackingCamBlobInfo_t
{
uint8 type;
uint16 cx;
uint16 cy;
uint32 area;
uint16 left;
uint16 right;
uint16 top;
uint16 bottom;
};
TrackingCamBlobInfo_t blob[15];
#define DXL_BUS_SERIAL3 3
#define ID_NUM_1 1
#define ID_NUM_2 2
#define ID_NUM 51
uint16 addr = 16;
Dynamixel Dxl(DXL_BUS_SERIAL3);
void setup() {
Dxl.begin(3);
Dxl.wheelMode(ID_NUM_1);
Dxl.wheelMode(ID_NUM_2);
}
void loop() {
TrackingCamGetBlobs();
delay(30);
if (blob[0].cx < 106) left();
else if (blob[0].cx > 213) right();
else forward();
}
void forward (){
Dxl.goalSpeed(ID_NUM_1, 100);
Dxl.goalSpeed(ID_NUM_2, 100 | 0x400);
}
void left (){
Dxl.goalSpeed(ID_NUM_1, 100 | 0x400);
Dxl.goalSpeed(ID_NUM_2, 100 | 0x400);
}
void right (){ Dxl.goalSpeed(ID_NUM_1, 100 );
Dxl.goalSpeed(ID_NUM_2, 100 );
}
void stop() { Dxl.goalSpeed(ID_NUM_1, 0);
Dxl.goalSpeed(ID_NUM_2, 0);
}
void TrackingCamGetBlobs (){ uint16 curAddr = addr;
for (uint8 i = 0; i < 16; i++){
SerialUSB.print(i);
SerialUSB.print(« «);
blob[i].type = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].type);
SerialUSB.print(« «);
if (blob[i].type == 255 || blob[i].type == -1 || blob[i].type == 248){ SerialUSB.println(« «);
break;
}
blob[i].cx = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].cx);
SerialUSB.print(« «);
blob[i].cy = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].cy);
SerialUSB.print(« «);
blob[i].area = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].area);
SerialUSB.print(« «);
blob[i].left = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].left);
SerialUSB.print(« «);
blob[i].right = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].right);
SerialUSB.print(« «);
blob[i].top = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.print(blob[i].top);
SerialUSB.print(« «);
blob[i].bottom = Dxl.readByte(ID_NUM, curAddr);
curAddr = curAddr + 2; SerialUSB.println(blob[i].bottom);
}
}
Теперь проверим систему на практике. После загрузки программы в робота и установки его на поле робот будет двигаться вдоль траектории, отмеченной черными метками. Но при тестировании на реальном поле сразу возникают моменты, на которые обязательно необходимо обращать внимание.
Во-первых это расположение и сила внешнего источника освящения. Для корректного распознавания всех меток на всем поле необходимо чтобы поле находилось в области равномерного освещения. Если в одной области поля будет засветы или, наоборот, тени, то модуль технического зрения может из-за этого некорректно распознавать метки.
Во-вторых, это опять же связано с освещением, если на поле будут светить яркие точечные источники освещения, то на поле будет падать тень от самого робота, и в некоторых ситуациях, таких как при нахождении робота спиной к источнику освещения, модуль может некорректно распознавать метки из-за падающей на белое поле тени.
В-третьих — необходимо понимать, что процесс получения данных от модуля не мгновенный и требует некоторого времени. Из-за этого при определенной скорости движения может возникнуть ситуация, при которой основной контролер робота не будет успевать получать актуальные данные от модуля технического зрения и в результате робот будет перемещаться некорректно.
Таким образом подобное решение можно использовать при реализации движения вдоль траектории обозначенных разными объектами и даже объектами разных цветов. Такие задачи часто встречаются в различных робототехнических соревнованиях, ориентированных на продвинутых участников и использование серьезного аппаратного и программного обеспечения.
Center-nav