A empresa SpaceX, fundada pelo famoso Ilon Mask, lançou um simulador para o acoplamento manual da espaçonave Crew Dragon com a ISS. Se tudo correr conforme o planejado, o encaixe ocorrerá em 27 de maio de 2020. Isso acontecerá no modo totalmente automático, mas a equipe poderá mudar para o controle manual. Na verdade, é o modo manual que é reproduzido no simulador.O simulador em si está localizado no local e é um brinquedo bastante problemático para a primeira cordilheira ... Oônibus espacial tenta voar na direção errada ... E a precisão com a qual você precisa entrar no gateway é de 20 cm ... ao longo de três eixos, além de velocidade angular, velocidade de deslocamento etc.Sentimentos patrióticos começaram a tocar em mim e, de alguma forma, tornou-se uma vergonha para o antigo poder espacial, e tomei esse simulador como um desafio. Como Musk decidiu mostrar a complexidade do encaixe e as dificuldades que seus engenheiros enfrentaram para criar um programa de encaixe automático, decidi escrever, no meu tempo livre, um programa JavaScript que conecta facilmente o Dragon e a ISS neste simulador.Como você gosta disso, Elon Musk?
Fumar é ruim para sua saúdeAtenção! Esse algoritmo é "brincalhão" e não foi projetado para uso em condições reais. O autor não é responsável por quaisquer perdas diretas ou indiretas incorridas pela sua nave espacial ou por outros objetos usando este algoritmo.
Primeiro, um pouco de história.É um fato bem conhecido que nosso ônibus espacial reutilizável Buran era muito semelhante a um ônibus espacial americano. E também se sabe que ele voou apenas uma vez, ao contrário dos "homólogos" americanos. Mas poucas pessoas sabem que seu único vôo não foi tripulado. Ele próprio decolou, aterrissou e fez tudo isso em condições climáticas muito ruins.Os ônibus americanos sempre pousavam apenas no modo manual. Isso foi surpreendente, dado o fato de os computadores não serem mais poderosos que uma calculadora. Isso, em teoria, não deve ser difícil, pensou ontem à noite.
Mas vamos direto ao ponto. O que é um simulador no site da SpaceX .No início, vemos informações gerais de que o desvio em todos os parâmetros deve estar dentro de 0,2 metros (20 cm). Dado o tamanho da estação e do navio, essa é uma limitação bastante séria.Começamos o simulador e vemos.
Acima, à direita e abaixo do círculo central, está o desvio angular da nave ao longo de três eixos.Verde - valor atual.Azul - a velocidade por segundo com a qual muda.Deslocamento à esquerda em relação ao gateway em metros. Não há velocidade de deslocamento ...Os controladores na parte inferior da tela são botões com duplicação no teclado.Então, começaremos com eles a análise do programa como a menos interessante.O layout dos botões do teclado.
O bloco esquerdo é responsável pelo deslocamento em relação ao gateway, mas o bloco direito pelo deslocamento em relação aos eixos.Escrevemos ou encontramos na rede um código que pode emular cliques no teclado no documento. No meu caso, o código ficou assim.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);
}
Escrevemos os códigos dos botões: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;
Qualquer sistema de controle envolve trabalho em um ciclo. Vamos torná-lo o mais fácil, em incrementos de 200 milissegundos. Vamos organizar um balcão para um, ainda precisamos dele.let index = 0;
function A() {
index++;
setTimeout(A, 200);
}
A();
Vamos voltar para a estrutura do site.Sua característica interessante é que a ISS é desenhada sobre tela, mas as informações sobre o estado de nossa espaçonave são desenhadas com a marcação usual. Parece que os desenvolvedores do site presumiram que haveria entusiastas semelhantes que desejam "automatizar" o jogo e dar a eles uma oportunidade ... Ou talvez, ao marcar isso, fosse estúpido, mais fácil de fazer.
Então, vamos adicionar mais algumas linhas de luz para obter informações sobre o estado de nossa espaçonave.
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 você pode ver, tirei nem tudo. Peguei apenas os valores de deslocamento, mas não tomei a taxa de alteração dos valores, e aqui está o porquê ...Na verdade, essa é a terceira iteração do algoritmo. A princípio, é uma opção simples, que a cada 200 milissegundos pega informações sobre as condições do navio e as ajusta a 0.Parecia assim.if (rollError !== -rollSpeed) {
const rollLimit = (Math.abs(rollError) / 10);
if (0 < rollError && rollSpeed < rollLimit) {
keyPress(_rollRigth);
} else if (rollError < -0 && -rollLimit < rollSpeed) {
keyPress(_rollLeft);
}
}
E, de fato, ele era bastante trabalhador. Especialmente para deslocamentos angulares. E para o deslocamento ao longo dos eixos, usei essa opção. const zLimit = (Math.abs(yawError) / 10);
if (0 < zDistance && zSpeed < zLimit) {
keyPress(_translateDown);
} else if (zDistance < 0 && -1 < zSpeed) {
keyPress(_translateUp);
}
A velocidade do deslocamento do navio em relação a cada eixo não é exibida, mas não é 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();
E acabou bem tolerável. Circuito de controle de feedback clássico. E enquanto o navio estava distante da ISS, voamos suavemente para nós mesmos [como me pareceu então]. Mas os problemas começaram perto do próprio navio. De fato, o navio era muito salsicha e era fisicamente impossível obter uma precisão de 0,2 metros. O fato é que a mistura de nossa nave ocorreu ... digamos em espaço contínuo (com grande precisão), mas vimos apenas décimos disso. E, é claro, tentando responder a eles a cada 200 milissegundos, temos ações regulatórias muito fortes. Apertamos os botões muitas vezes ao menor desvio. E quanto mais perto do navio, mais fortes os valores de deslocamento começaram a pular e nós realmente balançamos o navio ainda mais ... Aumentando a amplitude de seu movimento ...Era necessário um lugar para obter a precisão que faltava. Na segunda iteração para resolver esse problema, eu próprio tentei calcular já apenas com base no deslocamento da velocidade. E sim, parecia funcionar muito bem também, mas isso não resolveu o problema do movimento ...Qual é a essência do problema do movimento? Bem, olhamos, encontramos no espaço sideral e, clicando nos botões de controle, damos aceleração à nave em um plano ou outro. Mas assim que soltamos o botão, o movimento não para. No espaço sideral, devido ao vácuo, não há resistência. E assim que demos um impulso (com o apertar de um botão), o navio começou a se mover nessa velocidade ... E precisa ser parado de alguma forma. Parar no simulador é bastante fácil - você precisa dar um impulso reverso.Mas na segunda iteração da solução, o aumento da precisão do erro não me deu uma resposta sobre como ajustar a velocidade ...E aqui precisávamos de uma "bússola". O capitão de qualquer navio / embarcação deve calcular a rota com antecedência. Se ele der o comando de diminuir a velocidade depois de entrar no porto, é improvável que atracar jóias. E nós apenas precisamos disso.Precisamos calcular a rota, os capitães costumam fazer isso com uma bússola com um estado discreto de suas dicas. E nós faremos o mesmo. Vamos calcular a rota por um segundo à frente, que incluirá cinco iterações de pressionamento de botão ou não. if (index % 5 === 0) {
calculatePath(roll, rollError);
calculatePath(pitch, pitchError);
calculatePath(yaw, yawError);
calculatePath(y, yDistance);
calculatePath(z, zDistance);
}
A função carculatePath , com base em seu valor de desvio atual, calcula 5 etapas que, em teoria, devem reduzir esse desvio a 0. Não necessariamente nessa iteração, mas cada vez que devemos abordar o zero estimado em nossa grade mais detalhada.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;
}
Isso é tudo, calculamos a rota a cada segundo "igual" (índice% 5 === 0) e agora você só precisa 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);
}
O único cálculo que sobreviveu desde a primeira iteração é a abordagem ao navio.Isso é tudo, ok, estamos avançando a uma velocidade relativamente baixa const rangeLimit = Math.min(Math.max((Math.abs(range) / 100), 0.05), 2);
if (-rate < rangeLimit) {
keyPress(_accelerator);
} else if (-rangeLimit < -rate) {
keyPress(_brake);
}
Sob o spoiler está o código completo. Você pode verificar seu desempenho no site iss-sim.spacex.comCódigo completofunction 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();
Na verdade tudo. Obrigado a todos pela leitura :)
PS
Sim, eu me diverti um pouco e zombei. Não leve o artigo a sério, eu só queria me sentir como um astronauta [ou flyer espacial], como Gagarin Yuri Alekseevich ... Embora isso seja mais provável que o trabalho de engenharia de Sergey Pavlovich Korolev , que infelizmente nunca tenha visitado o espaço com o qual sonhava ...