Ограничения библиотеки

Эта страница описывает границы возможностей библиотеки UNI: что важно знать заранее, чтобы поведение робота не стало сюрпризом. Большинство пунктов — не ошибки, а осознанные компромиссы, и для каждого указано, как с ним работать.

Движение и команды

Одновременно выполняется только одна команда движения

Очереди команд нет: новая команда движения молча отменяет текущую. Код moveDistAsync(50, 500); rotateAsync(50, 90); выполнит только поворот — движение по дистанции будет прервано сразу после запуска.

Как работать: между асинхронными командами дожидайтесь завершения через waitMove() или используйте блокирующие версии (moveDist, rotate), которые ждут сами.

Минимальная мощность движения — minPower

Мощность ниже minPower (по умолчанию 15) в командах движения автоматически поднимается до него: на меньшей мощности моторы не страгиваются с места. Ехать медленнее ~15% командами moveDist/rotate и т.п. нельзя.

Как работать: прямые команды моторов motors() и motorsSync() это ограничение не применяют — для очень медленного движения используйте их. Порог настраивается в TuningConfig::minPower.

Скорость задается в процентах, а не в мм/с

Параметр power — процент от максимума, а не физическая скорость. Реальная скорость зависит от заряда батареи: на свежей батарее робот едет быстрее, чем на подсевшей. Замкнутые контуры компенсируют это по положению (точность остановки сохраняется), но время выполнения команды плавает.

Как работать: не завязывайте логику на время движения — проверяйте результат по одометрии (getDistance(), getAngle()).

Таймаут безопасности завершает движение молча

Если робот не достиг цели за ~3× расчетного времени (уперся в стену, колеса заблокированы), команда завершается сама — без кода ошибки. Блокирующий вызов просто вернет управление, как будто движение закончилось.

Как работать: там, где застревание вероятно, используйте moveDistAsync() + waitMove(timeout): возврат false означает, что робот не доехал за отведенное время. После этого сравните getDistance() с ожидаемой дистанцией.

Ограничения дуг

  • moveArcRadius() требует радиус не меньше половины колеи (~53 мм). Для разворота на месте используйте rotate().
  • В moveArcDist() / moveArcTime() / motorsArc() параметр angle с |angle| > 90 заставляет внутреннее колесо вращаться назад — траектория остается дугой, но поведение менее предсказуемо.
  • moveTime() и moveArcTime() останавливаются по времени без профиля замедления — остановка резче, чем у команд по дистанции.

Точность и одометрия

Физическая точность равна точности калибровки

Повороты физически точны настолько, насколько точно задана колея (trackLengthMM), дистанции — насколько точны диаметр колеса (wheelDiameterMM) и тики на оборот (ticksPerRev).

Правило калибровки: если одометрия показывает ровно цель (getAngle() = 90 после rotate(50, 90)), а физически робот повернул больше или меньше — калибруйте геометрию в UniConfig, а не коэффициенты ПИД.

Одометрия накапливает дрейф

Позиция считается только по энкодерам колес: проскальзывание, неровности пола и удары одометрия не видит. Гироскопа или внешней коррекции нет. moveTo() и setPosition() хорошо работают на маршрутах в несколько метров, но ошибка растет с пробегом и количеством резких маневров.

Как работать: на длинных маршрутах периодически «заземляйте» позицию по внешнему ориентиру (стена, линия) и вызывайте setPosition() с известными координатами.

Допуски завершения и отсутствие обратной доводки

Команды движения завершаются при входе в допуск по одометрии: rotateTolDeg (по умолчанию 1°) для поворотов и moveTolMM (1 мм) для дистанций. Если робот проскочил цель, он фиксируется тормозом — назад для коррекции он принципиально не сдает (это убирает дерганье в конце движения).

Как работать: нужна точность выше — уменьшайте допуск и moveAccel/rotateAccel в TuningConfig (плавнее торможение — меньше перелет).

moveTo: боковой снос пропорционален допуску поворота

moveTo() сначала доворачивает на курс к цели (с точностью rotateTolDeg), затем едет по прямой, удерживая курс, — но не подруливает на точку по ходу движения. Ошибка курса в 1° дает боковой снос ~5 мм на каждые 300 мм пути.

Как работать: для повышения точности уменьшите rotateTolDeg или разбейте длинный отрезок на два moveTo() — промежуточная точка пересчитает курс.

Система координат и сбросы

  • Ось X направлена вперед от стартовой позы робота, Y — вбок, положительный угол — поворот направо.
  • resetDistance() и resetAngle() сбрасывают только счетчики пройденного пути и угла — позу X/Y они не трогают.
  • Чтобы привязать одометрию к полю (или обнулить все сразу), используйте setPosition(x, y, angle).

Поведение и блокировки

Блокирующие вызовы

Некоторые функции останавливают выполнение программы:

  • stop(HARD) — блокирует ~50 мс (время торможения);
  • waitButton() — ждет нажатия бесконечно;
  • ultraSonic() — до ~70 мс, если эхо не приходит (3 попытки);
  • все эффекты светового кольца (pixelsRainbow и др.) — блокируют на всю длительность duration.

Как работать: в цикле, который следит за датчиками во время движения, не вызывайте длинные эффекты; для подсветки во время езды используйте базовые pixel()/pixelsAll().

Ленивая инициализация

begin() вызывать не обязательно — любая первая команда инициализирует робота сама. Но у этого есть следствие: скетч, который не вызывает ни одного метода робота, не оживит экран и одометрию.

Как работать: если робот должен «просто стоять и показывать экран» — вызовите robot.begin() явно в setup().

API рассчитан на вызовы из setup() и loop()

Библиотека сама использует второе ядро ESP32 для одометрии и регуляторов, а вызовы API синхронизированы в расчете на одно пользовательское ядро. Вызовы методов робота из собственных задач FreeRTOS пользователя не поддерживаются и могут привести к гонкам.

Периферия

Дисплей

  • Строки displayPrint() — максимум 15 символов, длиннее обрезаются.
  • Экран обновляется ~10 раз в секунду: значения, меняющиеся быстрее, частично теряются — это нормально.

Сервоприводы

  • Максимум 12 серво одновременно (16 PWM-каналов ESP32 минус 4 на моторы).
  • PWM-канал закрепляется за портом при первом использовании и не переиспользуется для другого порта (но servoDetach() каналы не расходует).
  • Импульсы фиксированы: 0.5–2.5 мс при 50 Гц. Серво с нестандартной калибровкой подстроить нельзя.
  • Скорость движения качалки не контролируется — серво идет к углу с максимальной скоростью. Плавность достигается серией мелких шагов угла с задержками.

Ультразвуковой датчик

Рабочий диапазон ultraSonic() — 20–4000 мм. Возврат 0 означает «нет эха»: цель слишком близко, слишком далеко или не отражает звук.

Важно: всегда проверяйте ноль отдельно, иначе условие dist < порог ложно сработает как «препятствие вплотную»:

int dist = module.ultraSonic(P6, P7);
if (dist > 0 && dist < 150) {
    robot.stop(HARD); // препятствие действительно близко
}

Аналоговый вход недоступен на P5 и P6

При попытке аналогового чтения с P5/P6 библиотека выводит предупреждение в Serial.

Как работать: для датчика линии и любых аналоговых датчиков используйте порты с АЦП — P1, P2, P3, P4 (ADC2) или P7 (ADC1). Цифровые датчики (digitalSensor()) работают на любом порту, включая P5/P6.

Примечание про Wi-Fi: P1–P4 используют ADC2, который недоступен, пока активен Wi-Fi. Сама библиотека Wi-Fi не использует, поэтому P1–P4 безопасны; но если вы добавите Wi-Fi в свой скетч, единственный независимый аналоговый вход — P7 (ADC1).