Acoplando la ISS usando JavaScript y brújula

La compañía SpaceX, fundada por el famoso Ilon Mask, lanzó un simulador para el acoplamiento manual de la nave espacial Crew Dragon con la ISS. Si todo va según lo planeado, el acoplamiento tendrá lugar el 27 de mayo de 2020. Se llevará a cabo en modo totalmente automático, pero la tripulación podrá cambiar a control manual. En realidad, es el modo manual que se reproduce en el simulador.

El simulador en sí está ubicado en el sitio y es un juguete bastante problemático para la primera cresta ... El

transbordador espacial intenta volar en dirección equivocada ... Y la precisión con la que necesita ingresar a la puerta de enlace es de 20 cm ... a lo largo de tres ejes, así como la velocidad angular, la velocidad de desplazamiento etc.

Los sentimientos patrióticos comenzaron a jugar en mí y de alguna manera se convirtió en una vergüenza para el antiguo poder espacial, y tomé este simulador como un desafío. Como Musk decidió mostrar la complejidad del acoplamiento y las dificultades que tuvieron sus ingenieros para crear un programa de acoplamiento automático, decidí escribir, en mi tiempo libre, un programa JavaScript que conecta fácilmente a Dragon y la ISS en este simulador.

¿Qué te parece eso, Elon Musk?

imagen
Fumar es malo para tu salud

¡Atención! Este algoritmo es "bromista", y no fue diseñado para usarse en condiciones reales. El autor no es responsable de ninguna pérdida directa o indirecta incurrida por su nave espacial u otros objetos usando este algoritmo.




Primero, un poco de historia.

Es un hecho bien conocido que nuestro transbordador espacial reutilizable Buran era muy similar a un transbordador estadounidense. Y también se sabe que voló solo una vez, a diferencia de los "homólogos" estadounidenses. Pero pocas personas saben que su único vuelo fue no tripulado. Él mismo despegó, aterrizó e hizo todo esto en condiciones climáticas muy malas.

Los transbordadores estadounidenses siempre aterrizaban solo en modo manual. Esto fue sorprendente dado el hecho de que las computadoras solían ser no más poderosas que una calculadora. Que, en teoría, no debería ser difícil, pensó anoche.



Pero vayamos al grano. ¿Qué es un simulador en el sitio web de SpaceX ?

Al principio, vemos información general de que la desviación en todos los parámetros debe estar dentro de los 0.2 metros (20 cm). Dado el tamaño de la estación y el barco, esta es una limitación bastante seria.

Iniciamos el simulador y vemos.



Arriba, a la derecha y debajo del círculo central está la desviación angular de la nave a lo largo de tres ejes.
Verde: valor actual.

Azul: la velocidad por segundo con la que cambia.

Desplazamiento a la izquierda en relación con la puerta de enlace en metros. No hay velocidad de desplazamiento ...

Los controladores en la parte inferior de la pantalla son botones con su duplicación en el teclado.

Entonces comenzaremos con ellos el análisis del programa como el menos interesante.

El diseño de los botones del teclado.



El bloque izquierdo es responsable del desplazamiento relativo a la puerta de enlace, pero el bloque derecho del desplazamiento relativo a los ejes.

Escribimos, o encontramos en la red, un código que puede emular los clics del teclado en el documento. En mi caso, el código se veía así.

function simulateKey(keyCode, type, modifiers) {
    var evtName = (typeof (type) === "string") ? "key" + type : "keydown";
    var modifier = (typeof (modifiers) === "object") ? modifier : {};

    var event = document.createEvent("HTMLEvents");
    event.initEvent(evtName, true, false);
    event.keyCode = keyCode;

    for (var i in modifiers) {
        event[i] = modifiers[i];
    }
    document.dispatchEvent(event);
}

function keyPress(keyCode) {
    simulateKey(keyCode)
    setTimeout(() => simulateKey(keyCode, "up"), 15);
}

Escribimos los códigos de los botones:
let _accelerator = 69;
let _brake = 81;
let _translateLeft = 65;
let _translateRigth = 68;
let _translateUp = 87;
let _translateDown = 83;

let _left = 37;
let _rigth = 39;
let _up = 38;
let _down = 40;

let _rollRigth = 105;
let _rollLeft = 103;

Cualquier sistema de control implica trabajar en un ciclo. Hagámoslo más fácil, en incrementos de 200 milisegundos. Organizaremos un contador para uno, todavía lo necesitaremos.

let index = 0;
function A() {

    index++;
    setTimeout(A, 200);
}

A();

Volvamos a la estructura del sitio.

Su característica interesante es que la ISS se dibuja en el lienzo, pero la información sobre el estado de nuestra nave espacial se dibuja con el marcado habitual. Parece que los desarrolladores del sitio asumieron que habría entusiastas similares que querrían "automatizar" el juego y darles esa oportunidad ... O tal vez al marcarlo, fue estúpido, más fácil de hacer.



Y entonces, agreguemos un par de líneas más de luz para obtener información sobre el estado de nuestra nave espacial.


    let range = parseFloat($("#range .rate").outerText.split(' '));

    let yDistance = parseFloat($("#y-range .distance").outerText.split(' ')[0]);
    let zDistance = parseFloat($("#z-range .distance").outerText.split(' ')[0]);

    let rollError = parseFloat($("#roll .error").outerText);
    let pitchError = parseFloat($("#pitch .error").outerText);
    let yawError = parseFloat($("#yaw .error").outerText);

    let rate = parseFloat($("#rate .rate").outerText.split(' ')[0]);

Como puede ver, no saqué todo. Saqué solo los valores de desplazamiento, pero no tomé la tasa de cambio de los valores, y aquí está el por qué ...

En realidad, esta es la tercera iteración del algoritmo. Al principio, es una opción simple, que cada 200 milisegundos toma información sobre el estado del barco y la ajusta a 0. Se

veía así.

if (rollError !== -rollSpeed) {
        const rollLimit = (Math.abs(rollError) / 10);
        if (0 < rollError && rollSpeed < rollLimit) {
            keyPress(_rollRigth);
        } else if (rollError < -0 && -rollLimit < rollSpeed) {
            keyPress(_rollLeft);
        }
    }

Y, de hecho, era un gran trabajador. Especialmente para desplazamientos angulares. Y para el desplazamiento a lo largo de los ejes, utilicé esta opción.

   const zLimit = (Math.abs(yawError) / 10);
        if (0 < zDistance && zSpeed < zLimit) {
            keyPress(_translateDown);
        } else if (zDistance < 0 && -1 < zSpeed) {
            keyPress(_translateUp);
        }

No se muestra la velocidad del desplazamiento del barco con respecto a cada eje, pero no es difícil de calcular.

function carculateSpeed() {
    let yDistance = parseFloat($("#y-range .distance").outerText.split(' ')[0]);
    let zDistance = parseFloat($("#z-range .distance").outerText.split(' ')[0]);

    ySpeed = yPrev - yDistance;
    yPrev = yDistance;

    zSpeed = zPrev - zDistance;
    zPrev = zDistance;
    setTimeout(carculateSpeed, 1000);
}
carculateSpeed();

Y resultó bastante tolerable. Circuito clásico de control de retroalimentación. Y mientras el barco estaba a una distancia de la ISS, volamos sin problemas hacia nosotros [como me pareció a mí entonces]. Pero los problemas comenzaron cerca del barco en sí. De hecho, el barco era muy salchicha y era físicamente imposible obtener una precisión de 0.2 metros. El hecho es que la mezcla de nuestra nave tuvo lugar ... digamos en un espacio continuo (con gran precisión), pero solo vimos décimas. Y, por supuesto, tratando de responder a ellos cada 200 milisegundos, obtuvimos acciones regulatorias muy fuertes. Hemos pulsado los botones demasiadas veces a la menor desviación. Y cuanto más cerca de la nave, más fuertes eran los valores de compensación y comenzamos a sacudir la nave aún más ... Aumentando la amplitud de su movimiento ...

Era necesario un lugar para tomar la precisión faltante. En la segunda iteración para resolver este problema, yo mismo intenté calcular ya solo en función del desplazamiento de la velocidad. Y sí, parecía funcionar bastante bien también, pero esto no resolvió el problema con el movimiento ...

¿Cuál es la esencia del problema del movimiento? Bueno, mira, nos encontramos en el espacio exterior y al hacer clic en los botones de control le damos aceleración a la nave en un avión u otro. Pero tan pronto como soltamos el botón, el movimiento no se detiene. En el espacio exterior, debido al vacío, no hay resistencia. Y tan pronto como dimos un impulso (con solo presionar un botón), la nave comenzó a moverse a esa velocidad ... Y debe detenerse de alguna manera. Pararse en el simulador es bastante fácil: debe dar un impulso inverso.

Pero en la segunda iteración de la solución, la mayor precisión del error no me dio una respuesta sobre cómo ajustar la velocidad ...

Y aquí necesitábamos una "brújula". El capitán de cualquier barco / embarcación debe calcular la ruta por adelantado. Si le da la orden de reducir la velocidad después de ingresar al puerto, es poco probable que amarre las joyas. Y solo necesitamos esto.

Necesitamos calcular la ruta, los capitanes generalmente hacen esto con una brújula con un estado discreto de sus puntas. Y haremos lo mismo. Calcularemos la ruta por un segundo más adelante, que incluirá cinco iteraciones de presionar botones o no.

    if (index % 5 === 0) {
        calculatePath(roll, rollError);
        calculatePath(pitch, pitchError);
        calculatePath(yaw, yawError);
        calculatePath(y, yDistance);
        calculatePath(z, zDistance);
    }

La función carculatePath , basada en su valor de desviación actual, calcula 5 pasos que en teoría deberían reducir esta desviación a 0. No necesariamente en esta iteración, pero cada vez debemos acercarnos al cero atesorado en nuestra propia cuadrícula más detallada.

function calculatePath(data, value) {
    data.path = [];
    if (data.prev === value) {
        data.speed = 0;
    }
    for (let i = 0; i < 5; i++) {
        if (0 < value + data.speed * (i + 1)) {
            data.speed -= 0.1;
            data.path.push(-1);
        } else if (value + data.speed * (i + 1) < -0) {
            data.speed += 0.1;
            data.path.push(1);
        } else if (i > 0) {
            if (0 < data.speed) {
                data.speed -= 0.1;
                data.path.push(-1);
            } else if (data.speed < 0) {
                data.speed += 0.1;
                data.path.push(1);
            } else {
                data.path.push(0);
            }
        } else {
            data.path.push(0);
        }
    }
    data.prev = value;
}

Eso es todo, calculamos la ruta cada segundo "igual" (índice% 5 === 0) y ahora solo necesita seguir este curso.


 let rollStep = roll.path[index % 5];
    if (0 < rollStep) {
        keyPress(_rollLeft);
    } else if (rollStep < 0) {
        keyPress(_rollRigth);
    }

    let pitchStep = pitch.path[index % 5];
    if (0 < pitchStep) {
        keyPress(_up);
    } else if (pitchStep < 0) {
        keyPress(_down);
    }

    let yawStep = yaw.path[index % 5];
    if (0 < yawStep) {
        keyPress(_left);
    } else if (yawStep < 0) {
        keyPress(_rigth);
    }

    let yStep = y.path[index % 5];
    if (0 < yStep) {
        keyPress(_translateRigth);
    } else if (yStep < 0) {
        keyPress(_translateLeft);
    }

    let zStep = z.path[index % 5];
    if (0 < zStep) {
        keyPress(_translateUp);
    } else if (zStep < 0) {
        keyPress(_translateDown);
    }

El único cálculo que ha sobrevivido desde la primera iteración es el acercamiento a la nave.

Eso es todo, de acuerdo, estamos avanzando a una velocidad relativamente baja

    const rangeLimit = Math.min(Math.max((Math.abs(range) / 100), 0.05), 2);
    if (-rate < rangeLimit) {
        keyPress(_accelerator);
    } else if (-rangeLimit < -rate) {
        keyPress(_brake);
    }

Debajo del spoiler está el código completo. Puede verificar su rendimiento usted mismo en el sitio web iss-sim.spacex.com

Código completo
function simulateKey(keyCode, type, modifiers) {
    var evtName = (typeof (type) === "string") ? "key" + type : "keydown";
    var modifier = (typeof (modifiers) === "object") ? modifier : {};

    var event = document.createEvent("HTMLEvents");
    event.initEvent(evtName, true, false);
    event.keyCode = keyCode;

    for (var i in modifiers) {
        event[i] = modifiers[i];
    }
    document.dispatchEvent(event);
}

function keyPress(keyCode) {
    simulateKey(keyCode)
    setTimeout(() => simulateKey(keyCode, "up"), 15);
}

let _accelerator = 69;
let _brake = 81;
let _translateLeft = 65;
let _translateRigth = 68;
let _translateUp = 87;
let _translateDown = 83;

let _left = 37;
let _rigth = 39;
let _up = 38;
let _down = 40;

let _rollRigth = 105;
let _rollLeft = 103;

let index = 0;

roll = {
    path: [0, 0, 0, 0, 0],
    prev: 0,
    speed: 0,
}

pitch = {
    path: [0, 0, 0, 0, 0],
    prev: 0,
    speed: 0,
}

yaw = {
    path: [0, 0, 0, 0, 0],
    prev: 0,
    speed: 0,
}

z = {
    path: [0, 0, 0, 0, 0],
    prev: 0,
    speed: 0,
}

y = {
    path: [0, 0, 0, 0, 0],
    prev: 0,
    speed: 0,
}

function calculatePath(data, value) {
    data.path = [];
    if (data.prev === value) {
        data.speed = 0;
    }
    for (let i = 0; i < 5; i++) {
        if (0 < value + data.speed * (i + 1)) {
            data.speed -= 0.1;
            data.path.push(-1);
        } else if (value + data.speed * (i + 1) < -0) {
            data.speed += 0.1;
            data.path.push(1);
        } else if (i > 0) {
            if (0 < data.speed) {
                data.speed -= 0.1;
                data.path.push(-1);
            } else if (data.speed < 0) {
                data.speed += 0.1;
                data.path.push(1);
            } else {
                data.path.push(0);
            }
        } else {
            data.path.push(0);
        }
    }
    data.prev = value;
}

function A() {
    let range = parseFloat($("#range .rate").textContent.split(' '));

    let yDistance = parseFloat($("#y-range .distance").textContent.split(' ')[0]);
    let zDistance = parseFloat($("#z-range .distance").textContent.split(' ')[0]);

    let rollError = parseFloat($("#roll .error").textContent);
    let pitchError = parseFloat($("#pitch .error").textContent);
    let yawError = parseFloat($("#yaw .error").textContent);

    let rate = parseFloat($("#rate .rate").textContent.split(' ')[0]);

    if (index % 5 === 0) {
        calculatePath(roll, rollError);
        calculatePath(pitch, pitchError);
        calculatePath(yaw, yawError);
        calculatePath(y, yDistance);
        calculatePath(z, zDistance);
    }

    let rollStep = roll.path[index % 5];
    if (0 < rollStep) {
        keyPress(_rollLeft);
    } else if (rollStep < 0) {
        keyPress(_rollRigth);
    }

    let pitchStep = pitch.path[index % 5];
    if (0 < pitchStep) {
        keyPress(_up);
    } else if (pitchStep < 0) {
        keyPress(_down);
    }

    let yawStep = yaw.path[index % 5];
    if (0 < yawStep) {
        keyPress(_left);
    } else if (yawStep < 0) {
        keyPress(_rigth);
    }

    let yStep = y.path[index % 5];
    if (0 < yStep) {
        keyPress(_translateRigth);
    } else if (yStep < 0) {
        keyPress(_translateLeft);
    }

    let zStep = z.path[index % 5];
    if (0 < zStep) {
        keyPress(_translateUp);
    } else if (zStep < 0) {
        keyPress(_translateDown);
    }

    const rangeLimit = Math.min(Math.max((Math.abs(range) / 100), 0.05), 2);
    if (-rate < rangeLimit) {
        keyPress(_accelerator);
    } else if (-rangeLimit < -rate) {
        keyPress(_brake);
    }

    index++;
    setTimeout(A, 200);
}

A();


En realidad todo. Gracias a todos por leer :)



PD


Sí, hice un poco de highpan y me burlé. No tome en serio el artículo, solo quería sentirme como un astronauta [o un volador espacial], como Gagarin Yuri Alekseevich ... Aunque esto es más probable que sea el trabajo de ingeniería de Sergey Pavlovich Korolev , quien desafortunadamente nunca visitó el espacio con el que soñó ...

All Articles