Управление с телеметрией по serial

4 лайка

Да, поэтому дописал слово “например”. Но это не существенно все.
На однозадачных можно вместо задержки вызывать свой main_tick(), штоп управление отдать всем по чуть-чуть. Ну или хотя бы диодиком моргать, чтобы было понятно, где мы сидим :slight_smile:

Имхо, Раневская бы поставила это действие рядом с хоккеем на траве.

Для того, чтобы быть совсем уж уверенным, на Вашу идеализированную математику нужно навесить реальные люфты и упругость элементов конструкции, реальный разброс параметров, реальную нелинейность зависимостей, а также реальные силы и моменты, причем следует сравнить последние, полученные из расчета, с реальными возможностями сервоприводов и с условиями прочности конструкции.

Нашел логическую ошибку - метод parse неправильно определял факт отсутствия запрашиваемого параметра. Исправлено. Кроме того заменил все “служебные” символы (теперь “{ } & ~”) так как предыдущие слишком часто хочется использовать в передаваемом тексте.

class cWirelessLink {

  public:
  
    // открытие порта, проверка работоспособности модуля
    String start(HardwareSerial *_serial, uint8_t _setPin) {
      
      serial = _serial;
      setPin = _setPin;

      String log = "Methood start (pin " + String(setPin) + " might been connected to HC-12 SET pad)";

      pinMode(setPin, OUTPUT); // конфигурация пина активации режима AT
      serial -> begin(115200); // поднятие порта на 115200
      delay(500);
      digitalWrite(setPin, LOW); // активация режима AT команд
     
      // далее идет отправка AT команд в модуль и получение результата
      // по datasheet так и не стало понятно, перед отправкой нужны задержки 40мс или после, или между
      // поэтому используются задержки по 40мс между каждыми взаимодействиями с модулем
     
      // отправка запроса на проверку подключения
      delay(40);
      serial -> print("AT\r");
      delay(40);
     
      if (serial -> readString().indexOf("OK") != -1) {

        // подключение удалось сразу на 11520
        // считаем что модуль уже сконфигурирован
        // однако это лишь допущение
        // так как не проводится проверка других параметров
     
        log += "\r\nSucessfully connected on 115200";
     
        serial -> print("AT+RX\r"); // запрос конфигурации
        delay(40);
        log += "\r\n" + serial -> readString();
     
      } else {
     
        log += "\r\n Failed to connect on 115200, trying configurate on 9600"; // походу подключен новый модуль, попытка настроить на 9600
        
        serial -> end(); // закрытие порта
        serial -> begin(9600); // поднятие порта на стандартнх 9600
     
        delay(40);
        serial -> print("AT+B115200\r"); // отправка конфигурации - скорость порта
        delay(40);
        serial -> print("AT+FU1\r"); // отправка конфигурации - режим
        delay(40);
        serial -> print("AT+P3\r"); // отправка конфигурации - мощность
        delay(40);
     
        digitalWrite(setPin, HIGH);  // выход из режима AT команд
        delay(80); // задержка для применения настроек модулем по datasheet 80мс
     
        serial -> flush(); // очистка буфера порта (там результат применения конфигов, возможно прочий мусор)
        serial -> end(); // закрытие порта
        serial -> begin(9600); // поднятие порта на вожделенных 115200
        
        digitalWrite(setPin, LOW); // запуск режима AT команд
        delay(40);
        serial -> print("AT\r"); // отправка запроса на проверку подключения
        delay(40);
     
        if (serial -> readString().indexOf("OK") != -1) {
     
          log += "\r\nSucessfully connected on 115200 after configurate"; // подключение после настройки удалось
     
          serial -> print("AT+RX\r"); // запрос конфигурации
          delay(40);
          log += "\r\n" + serial -> readString(); // вывод конфигурации
     
        } else log += "\r\nFailed to configure HC-12 wia AT commands, check physical connection"; // попытка настройки не удалась - проверяем провода,
                                                                                                  // подаем питание закоротив SET на GND (отключив от Arduino!!!)
      }
     
      // serial -> print("AT+DEFAULT\r"); delay(40); // сброс модуля на заводские (еси нужно потестить работу метода)

      digitalWrite(setPin, HIGH);  // выход из режима AT команд
      delay(80);

      serial -> flush(); // очистка буфера порта

      return log;
     
    }
    
    // все сообщения начинаются с { и заканчиваются }
    // сразу после { следует строковое представление числа - хеш суммы сообщения
    // все элементы в сообщении разделяются &
    // имя элемента от его значения отделяется ~
    // использование { } & ~ в именах и значениях элементов не допускается,
    // проверки на эти вхождения в методах add нет! (можно добавить, но целесообразно ли?)

    // добавление элемента к предназначенному к отправке сообщению, вернет true только если было добавлено
    bool add (String name, String value) {

      if (outcoming.length() + value.length() + 1 < byteTreshold) {
        // элемент будет добавлен, только если это не приведет к превышению допустимого размера сообщения,
        // а также если элемента с таким же именем еще нет в сообщении
        if (outcoming.indexOf("&" + name + "~") != -1) return false;
        outcoming += name + "~" + value + "&";
        return true;
      } else return false;

    }

    // поиск элемента по его имени в принятом сообщении, возврат значения, либо пустой строки если элемент не нашелся
    String parse (String name) {

      int16_t pos = recieved.indexOf("&" + name + "~");
      if (pos > -1) {
        pos += name.length() + 2;
        return (pos > -1) ? recieved.substring(pos, recieved.indexOf("&", pos)) : "";
      } else return "";

    }

    // отправка сообщения, вернет true если не превышен максимальный размер сообщения
    // строка, содержащая подготовленное сообщение очищается в любом случаае
    bool send () {

      if (outcoming.length() <= byteTreshold) {

        // первая "&" считается частью сообщения (упрощает извлечение первого элемента в сообщении методом parse)
        outcoming = "&" + outcoming;
        // обрамляется в {}, сразу после стартового символа помещается хеш
        serial -> print("{" + hash(outcoming) + outcoming + "}");
        // запоминается момент последней отправки сообщения
        lastTransmittingTime = millis();
        outcoming = "";
        return true;

      } else {

        outcoming = "";
        return false;

      }

    }

    // возвращает время в милисекундах, прошедшее с момента последней состоявшейся отправки исходящего сообщения
    uint16_t silence () { return millis() - lastTransmittingTime; }

    // вернет true, когда получит корректное сообщение целиком
    // что будет поводом вызвать parse() для получения значений ожидаемых элементов
    bool get() {

      // принимаемый символ
      char character = '_';
      // счетчик принятых символов
      uint16_t count = 0;
      // время последнего успешного приема символа
      uint32_t last = millis();

      // ожидание символа начала сообщения
      while (character != '{') {
        if (serial -> available()) {
          character = (char)serial -> read();
          last = millis();
          count++;
          if (count > startByteTreshold) return false; // может быть прервано по превышению количества принятых бит
        } else if (millis() - last > timeTreshold) return false; // может быть прервано по времени "молчания" порта
      }

      // в character будет "<" можно было присвоить константой, или вовсе не записывать,
      recieved = "";
      recieved += character; // но так нагляднее отлаживать

      // ожидание символа конца сообщения, аккумуляция принимаемых данных
      while (character != '}') {
        if (serial -> available()) {
          character = (char)serial -> read();
          recieved += character;
          last = millis();
          count++;
          if (count > byteTreshold) {
            // может быть прервано по превышению количества принятых бит
            recieved = "";
            return false;
          }
        } else if (millis() - last > timeTreshold) {
          // может быть прервано по времени "молчания" порта
          recieved = "";
          return false;
        }
      }
      
      // это последнее место в коде, где сообщение можно посмотреть/вывести целиком
      // сообщение получено, необходимо выделить хеш
      String declared = recieved.substring(1, recieved.indexOf("&"));
      // из сообщения удаляеются хеш, символы начала и конца
      recieved.remove(0, recieved.indexOf("&"));
      recieved.remove(recieved.indexOf("}"));

      // если вычисленый методом класса хеш равен полученному в сообщении, то элементы полученного сообщения остаются в памяти
      if (declared == hash(recieved)) return true;
      else {
        // если хеш не сошелся то сообщение очищается
        recieved = "";
        return false;
      }

    }

  private:
  
    // ссылка на используемый HC-12 serial порт
    HardwareSerial *serial;
    // номер вывода, к которому подключен вход SET платы HC-12
    uint8_t setPin;
    // максимально допустимая длина сообщения
    const uint16_t byteTreshold = 512;
    // максимальное количество перебираемых бит данных при попытке принять сообщение
    // (длина + максимальное кол-во символов хеш + открывающий и закрывающий тег) * 2
    const uint16_t startByteTreshold = (byteTreshold + 5 + 2) * 2;
    // количество миллисекунд неполучения ответа от serial, по достижении которых происходит сброс при приеме данных
    const uint16_t timeTreshold = 10;
    // время последней состоявшейся отправки сообщения (не путать с успешным приемом на стороне получателя)
    uint16_t lastTransmittingTime = 0;
    // строка для хранения принятого сообщения (в случае неудачной попытки будет пустой)
    String recieved = "";
    // строка для хранения подготавливаемых к отправке данных (опустошается после любой попытки отправки)
    String outcoming = "";

    // функция вычисления хеш суммы
    String hash (String& data) {
      // в качестве хеша используем XOR по четным и нечетным символам (байтам) строки
      uint8_t odd = 0, even = 0;
      for (uint8_t i = 0; i < data.length(); i++)
        if (i % 2) odd ^= data[i]; // нечетные
        else even ^= data[i]; // четные
      // четная сумма занимает старший байт в итоговом числе
      return String((uint16_t) even * 256 + odd);
    }

};

способ использования не поменялся.

Если кому интересно, на данный момент реализовал пару менюшек и скролл по ним энкодером. Так же “особый” экран для калибровки сервопривода: позволяет энкодером управлять сервой напрямую, задавать текущее положение в качестве 1 из 2 крайних точек. Это у меня пока что сериал монитор компа вместо экрана пульта, и подключен только энкодер и один аналоговый джойстик с кнопкой.

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

Насколько я себе представляю, эти “варианты” отличаются исключительно тем, где именно будут проводиться вычисления. В одном случае считает сам МК в процессе движения, а во втором - ПК - заранее, после чего складывает результаты расчета в массивы, размещаемые в PROGMEM, откуда МК их извлекает при движении.

А вот в этом случае расчет на ПК выглядит предпочтительнее: есть возможность сгладить эти рывки. Хотя бы итерациями.

Это как?
12 серв при 6 ногах: по 2 сервы на ногу.
Т.е. у ноги только две степени свободы. Этого недостаточно для вменяемого передвижения.

Естественно.
Только причина этому не

а ошибка при проектировании: по 2 сервы на ногу явно недостаточно.

Почти да. Просто с компа дискретные состояния будут покадрово, а в математике на МК все же параметрические кривые, которые более точно определят текущие координаты в зависимости от дельты времени.

одно другому не мешает. С плохими сервами хоть 3dof делай - результат все равно будет обескураживающий

Ну да, с неработающими сервами ошибки проектирования становятся совсем незаметными.

1 лайк

по этому поводу, могу только сказать, что когда были sg90, у меня длины плеча 2 и плеча 3 были 95 и 135 мм соответственно. И оно ходило на хлипкой раме из листового плексигласа. Прям видно было как ему тяжело, но ходило. И реальные движения вполне соответствовали вычисленным. Сейчас у меня плечи 44 и 70 мм, т.е. грубо вдвое короче, что в том числе уменьшает погрешность установки законцовки. Есть поддержка осей вращения (втулки с противоположной стороны выхода вала сервопривода на каждом сочленении), цифровые сервоприводы с хорошей точностью и как минимум втрое большим моментом. Подвоха не вижу

Нет. Люди по всему миру манстрячат ходунов на 2,3,4,8,12,18 сервах. Это уже стандарты. По ногам - двуноги, четвероноги, шестиноги.
… конечно sg90 лучше ограничивать числом в 8 т.е. квадропод ещё нормально.


Не видел паука на 8 ногах, 7, 5 - не ходовые числа.

Дело в том, что пауков великое множество, более 40 тысяч разновидностей, и они могут сильно отличаться друг от друга, но 8 ног — обязательный признак паука

1 лайк

Это к тому, что гекса иногда зовут паук. Но для паука надо 8 ног, а не 6.
… во, какая была ходовая у моего варианта.


1 лайк

пока не могу пруффнуть (просто времени жалко видео записать и выложить). В общем, новые сервы, которые скорее всего оригинал, и правда огонь. Точность и линейность угла поворота относительно входящего ШИМ на глаз достойная, для хоббийного робота хватит однозначно. И порадовал момент: при питании 6 вольт я за 1 лапу поднял робота, на этой лапе 2я серво была включена и выставлена на 90 градусов. В общем, она держит вес всего остального робота без установленных плат и аккумов (корпус, 5 лап и 16 сервоприводов, что тоже весит прилично), и при этом даже не нагревалась в течение минуты.

Из грустного, при маловероятно достижимых нагрузках, а именно вращение против шерсти, с большей силой чем может выдать серва, удалось выжать потребление в почти 1 ампер. Придется использовать бек на 10А, а так хотелось более компактный на 5А.

Гексапод пошёл уже, или не побежал ещё?

привет, еще нет. Я продвигаюсь, но медленно, другие дела навалились

Привет! Я помню ваш вопрос. У меня как раз дошло дело до кинематики при прямолинейном движении, и чтобы не тронуться умом окончательно, прошивая раз за разом нерабочие варианты в контроллер, я накидал анимированную трехмерную html демку, в которой и обкатал алгоритм. Можете заценить. Поскольку .html тут прикладывать нельзя, могу только ее исходник привести:

<HTML>
<HEAD>
<TITLE>Bhla Bhla</TITLE>

<meta charset="utf-8">

<style>

div#circle {
  position: relative;
  display: block;
  border-radius: 50%;
  background: linear-gradient(#fff, transparent 1px), linear-gradient(90deg, #fff, #bdb 1px);
  background-size: 10mm 10mm;
  background-position: 0 0;
  transform: perspective(140mm) rotateX(60deg) translateZ(-8mm);
  transform-style: preserve-3d;
  perspective-origin: center bottom;
}

div#circle>div {
  position: absolute;
  display: block;
  width: 4mm;
  height: 4mm;
  font-size: 3mm;
  line-height: 4mm;
  font-weight: bold;
  border: 1mm solid #777;
  color: #777;
  border-radius: 3mm;
  margin: -3mm 0 0 -3mm;
  text-align: center;
  background: #fff;
}
div#circle>div.floating {
  border-color: #37f;
  color: #37F;
}
div#circle>div.landed {
  border-color: #f33;
  color: #f33;
}
div#circle>div#S {
  border-color: #fa3;
  color: #fa3;
}

div#joystick {
  display: inline-block;
  width: 70mm;
  height: 70mm;
  background: radial-gradient(circle at center, #da0 0, #da0 2.5mm, transparent 2.5mm),
  repeating-linear-gradient(90deg, transparent, transparent 49.75%, #888 50%, transparent 50.25%, transparent 100%),
  repeating-linear-gradient(transparent, transparent 49.75%, #888 50%, transparent 50.25%, transparent 100%), #333;
}

</style>

</HEAD>
<BODY>

<div id="circle">
<div id="A" class="landed">A</div>
<div id="B" class="landed">B</div>
</div>

<div id="joystick"></div>

<script type="text/javascript">

var right_x = 0;
var right_y = 0;
var bg_x = 0;
var bg_y = 0;
document.getElementById('joystick').onmousemove = function(e) {
  var rect = e.target.getBoundingClientRect();
  right_x = Math.round((e.clientX - rect.left - rect.width / 2) / rect.width * 2000);
  right_y = Math.round((rect.height / 2 - e.clientY + rect.top) / rect.height * 2000);
  e.target.style.background = "radial-gradient(circle at " + String(e.clientX - rect.left)+"px "+String(e.clientY - rect.top)+"px, #da0 0, #da0 2.5mm, transparent 2.5mm),"
                            + "repeating-linear-gradient(90deg, transparent, transparent 49.75%, #888 50%, transparent 50.25%, transparent 100%),"
                            + "repeating-linear-gradient(transparent, transparent 49.75%, #888 50%, transparent 50.25%, transparent 100%), #333";
}
document.getElementById('joystick').onmouseout = function(e) {
  right_x = 0;
  right_y = 0;
  e.target.style.background = "radial-gradient(circle at center, #da0 0, #da0 2.5mm, transparent 2.5mm),"
                            + "repeating-linear-gradient(90deg, transparent, transparent 49.75%, #888 50%, transparent 50.25%, transparent 100%),"
                            + "repeating-linear-gradient(transparent, transparent 49.75%, #888 50%, transparent 50.25%, transparent 100%), #333";
}
function circle(r, dx, dy) {
var circ = document.getElementById("circle");
  circ.style.height = r * 2 + "mm";
  circ.style.width = r * 2 + "mm";
  bg_x = (bg_x + dx) % 10;
  bg_y = (bg_y + dy) % 10;
  circ.style.backgroundPosition = bg_x + "mm " + bg_y + "mm";
}
function pos (el, x, y, z = 0) {
  el.style.left = x + "mm";
  el.style.top = y + "mm";
  el.style.transform = "translateZ(" + z + "mm)";
}
function hypot (dx, dy) { return Math.hypot(dx, dy); }
function max (x, y) { return Math.max(x, y); }
function abs (x) { return Math.abs(x); }
function min (x, y) { return Math.min(x, y); }
function sqrt (x) { return Math.sqrt(x); }



// МАГИЯ ТУТ

function div (val, divider) { return divider > 0 ? val / divider : 0; }

var max_radius = 35;
var max_height = 30;
// максимальные скорость в мм/с и ускорение в мм/с², отмасштабированные до периода итераций 20мс
var maximum_velocity_value =  100 / 50; // за 1 / 50 секунды
var maximum_acceleration_value = 75 / 2500; // за 1 / 2500 секунды за секунду
// текущий вектор скорости
var current_velocity_dx = 0;
var current_velocity_dy = 0;
// координаты находящейся на поверхности конечности
var landed_limb_x = 0;
var landed_limb_y = 0;
// координаты поднятой конечности
var floating_limb_x = 0;
var floating_limb_y = 0;
// фаза
var phase = true;

function calc() {
  
  // ПОЛУЧЕНИЕ ДАННЫХ С ДЖОЙСТИКА -1000..1000
  
  var joystick_dx = right_x; 
  var joystick_dy = right_y;
  var joystick_dl = hypot(joystick_dx, joystick_dy);
  // получение единичного вектора желаемого направления
  var setpoint_velocity_cos = div(joystick_dx, joystick_dl);
  var setpoint_velocity_sin = div(joystick_dy, joystick_dl);
  // получение нормализованного значения отклонения джойстика
  var joystick_normalized_value = div(joystick_dl, hypot(min(abs(joystick_dx), abs(joystick_dy)), 1000));
  // получение значения желаемой скорости
  var setpoint_velocity_value = joystick_normalized_value * maximum_velocity_value;
  // преобразование данных джойстика в вектор целевой скорости "setpoint_velocity"
  var setpoint_velocity_dx = setpoint_velocity_cos * setpoint_velocity_value;
  var setpoint_velocity_dy = setpoint_velocity_sin * setpoint_velocity_value;
  
  // ВЫЧИСЛЕНИЕ ВЕКТОРА СКОРОСТИ ПРИЗЕМЛЕННОЙ КОНЕЧНОСТИ
  
  // текущий вектор скорости "current_velocity" переходит в "setpoint_velocity" с заданным ускорением
  // определение вектора из конца "current_velocity" в конец "setpoint_velocity"
  var to_setpoint_dx = setpoint_velocity_dx - current_velocity_dx;
  var to_setpoint_dy = setpoint_velocity_dy - current_velocity_dy;
  var to_setpoint_dl = hypot(to_setpoint_dx, to_setpoint_dy);
  if (to_setpoint_dl > maximum_acceleration_value) {
    // если длина найденного вектора превышает значение максимального ускорения за итерацию
    // вектор ускорения применяется к текущей скорости
    current_velocity_dx += div(to_setpoint_dx, to_setpoint_dl) * maximum_acceleration_value;
    current_velocity_dy += div(to_setpoint_dy, to_setpoint_dl) * maximum_acceleration_value;
  } else {
    // длина вектора меньше максимального ускорения за итерацию,
    // применение вектора длиной maximum_acceleration_value вызовет перемещение дальше требуемого значения
    // поэтому "current_velocity" просто приравнивается к "setpoint_velocity"
    current_velocity_dx = setpoint_velocity_dx;
    current_velocity_dy = setpoint_velocity_dy;
  }
  
  // ВЫЧИСЛЕНИЕ ВЕКТОРА СКОРОСТИ ПОДНЯТОЙ КОНЕЧНОСТИ
  
  var floating_velocity_dx = 0;
  var floating_velocity_dy = 0;
  // текущая скорость
  var current_velocity_value = hypot(current_velocity_dx, current_velocity_dy);
  if (current_velocity_value != 0) {
    // определение целевой позиции поднятой конечности - на краю окружности в направлении current_velocity
    var floating_target_x = div(current_velocity_dx, current_velocity_value) * max_radius;
    var floating_target_y = div(current_velocity_dy, current_velocity_value) * max_radius;
    // текущее расстояние от центра до позиции приземленной конечности
    var center_to_landed_limb_distance = hypot(landed_limb_x, landed_limb_y);
    // конечность движется в направлении, обратном движению - вычисляем косинус угла между векторами landed_limb и -current_velocity
    var cos = div(landed_limb_x * -current_velocity_dx + landed_limb_y * -current_velocity_dy, center_to_landed_limb_distance * current_velocity_value);
    // расстояние - сторона треугольника напротив угла с найденным косинусом, к которому прилегают стороны center_to_landed_limb_distance и max_radius
    var landed_to_out_distance = sqrt(center_to_landed_limb_distance * center_to_landed_limb_distance + max_radius * max_radius - 2 * cos * center_to_landed_limb_distance * max_radius);
    // определение векора из текущего положения поднятой конечности в ее целевую позицию
    var to_floating_target_dx = floating_target_x - floating_limb_x;
    var to_floating_target_dy = floating_target_y - floating_limb_y;
    var to_floating_target_dl = hypot(to_floating_target_dx, to_floating_target_dy);
    // определение значения скорости поднятой конечности, необходимой чтобы успеть переместиться в целевую позицию
    var floating_velocity_value = to_floating_target_dl * current_velocity_value / landed_to_out_distance;
    // определение вектора скорости поднятой конечности
    floating_velocity_dx = div(to_floating_target_dx, to_floating_target_dl) * floating_velocity_value;
    floating_velocity_dy = div(to_floating_target_dy, to_floating_target_dl) * floating_velocity_value;
  }
  
  // СМЕНА ФАЗ ПРИ ПРИБЛИЖЕНИИ ПРИЗЕМЛЕННОЙ КОНЕЧНОСТИ К КРАЮ ОКРУЖНОСТИ
  
  // не перешагнет ли край окружности при приращении координат на значения вектора скорости?
  if (hypot(landed_limb_x - current_velocity_dx, landed_limb_y - current_velocity_dy) >= max_radius) { 
    // смена роли конечностей и фазы
    var temp = landed_limb_x;
    landed_limb_x = floating_limb_x;
    floating_limb_x = temp;
    temp = landed_limb_y;
    landed_limb_y = floating_limb_y;
    floating_limb_y = temp;
    phase = !phase;
  }
  
  // ПЕРЕМЕЩЕНИЕ ПО ПОЛУЧЕННЫМ ВЕКТОРАМ СКОРОСТИ
  
  landed_limb_x -= current_velocity_dx;
  landed_limb_y -= current_velocity_dy;
  floating_limb_x += floating_velocity_dx;
  floating_limb_y += floating_velocity_dy;
  
  // ВЫЧИСЛЕНИЕ ВЫСОТЫ ОТ ПОВЕРХНОСТИ ДЛЯ ПОДНЯТОЙ КОНЕЧНОСТИ
  
  // множитель высоты поднятой конечности f(x) = 1 - (x - 1)² где x = 0..1 удаленность поднятой конечности от радиуса
  var height_scale = (1 - (floating_limb_x * floating_limb_x + floating_limb_y * floating_limb_y) / (max_radius * max_radius));
  // второй множитель - отношение текущей скорости к максимально возможной
  height_scale *= current_velocity_value / maximum_velocity_value;
  // итоговая высота
  var floating_limb_height = height_scale * max_height;
  
  
  
  // ОТОБРАЖЕНИЕ
  
  /* актуальное значение скорости
  console.log(Math.floor(Date.now() / 1000 % 100) + " velocity: " + Math.round(current_velocity_value*50*100)/100 + "mm/s");/**/

  circle(max_radius, -current_velocity_dx, current_velocity_dy);
  if (phase) {
    pos(A, max_radius + landed_limb_x, max_radius - landed_limb_y);
    pos(B, max_radius + floating_limb_x, max_radius - floating_limb_y, floating_limb_height);
    A.className = "landed";
    B.className = floating_limb_height > 0 ? "floating" : "landed";
  } else {
    pos(A, max_radius + floating_limb_x, max_radius - floating_limb_y, floating_limb_height);
    pos(B, max_radius + landed_limb_x, max_radius - landed_limb_y);
    A.className = floating_limb_height > 0 ? "floating" : "landed";
    B.className = "landed";
  }

}

var timerId = null;
timerId = setInterval(calc, 20);

</script>

</BODY>
</HTML>

скопировать в текстовик, изменить расширение на .html и запустить в браузере.

На самом деле, разумеется, точки будет не две, а шесть, и каждая в своем круге своей конечности. Но у всех четных конечностей (0, 2, 4) будет координата точки А в своем круге, а у нечетных (1, 3, 5) - координата точки В.
Почти самое главное, что кинематика здесь не просто в “попугаях” - максимальная скорость ограничена мм/с, ускорение мм/с² - все эти величины можно найти в коде. Гекса с таким управлением будет плавно разгоняться, тормозить и менять курс в любой ситуации.

Здорово, когда хорошо программируешь и математика идёт на ура. Я остановился на переходах-кадрах.
…для иллюстрации превосходства первого над вторым делайте два демонстрационных варианта движения гекса ))…в проморолике.

наура это громко сказано. Я так баловался последний раз в универе, когда физику автомобиля с пробуксовками и заносами пытался напрограммировать… а сейчас приходится гуглить, чем синус от косинуса отличается, и что такое скалярное произведение векторов… забылось всё напрочь

но все равно этим способом многого не получится добиться, результат-то будет, эффект не тот. Первые мои попытки тоже кадровые были. Даже в вопросе “хочу что-то поменять” - будь любезен все кадры перерисовать. Т.ч. если втянет - все равно в геометрию уйдете. Векторные модели не так уж и сложны