Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@ForestKatsch
Last active May 24, 2020 01:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ForestKatsch/01069f29e8316df117740258313665f1 to your computer and use it in GitHub Desktop.
Save ForestKatsch/01069f29e8316df117740258313665f1 to your computer and use it in GitHub Desktop.
Automatically dock with the ISS in SpaceX's docking game.
// 1. Go to iss-sim.spacex.com
// 2. Open the developer console and paste in the contents of this file
// 3. Press the "start autopilot" button.
(function() {
class PID {
constructor(options) {
options = {
itermMin: -10000,
itermMax: 10000,
outputMin: -10000,
outputMax: 10000,
...options
};
this.gains = {
p: options.p || 0,
i: options.i || 0,
d: options.d || 0
};
this.measure = 0;
this.setpoint = 0;
this.iterm = 0;
this.itermLimits = {
min: options.itermMin,
max: options.itermMax
};
this.outputLimits = {
min: options.outputMin,
max: options.outputMax
};
this.previousError = 0;
this.value = 0;
}
setMeasure(measure) {
this.measure = measure;
}
setSetpoint(setpoint) {
this.setpoint = setpoint;
}
get() {
return this.value;
}
clamp(value, low, high) {
return Math.max(Math.min(value, high), low);
}
tick() {
let error = this.setpoint - this.measure;
let p = error * this.gains.p;
let d = (error - this.previousError) * this.gains.d;
this.previousError = error;
this.iterm += error * this.gains.i;
this.iterm = this.clamp(this.iterm, this.itermLimits.min, this.itermLimits.max);
this.value = p + this.iterm + d;
this.value = this.clamp(this.value, this.outputLimits.min, this.outputLimits.max);
return this.value;
}
}
let performInput = (direction) => {
document.dispatchEvent(new KeyboardEvent("keyup", { which: direction }));
};
class Control {
constructor(negative, positive, ratio, toleranceBasis) {
this.negative = negative;
this.positive = positive;
this.currentValue = 0;
this.desiredValue = 0;
this.lastPerformTick = 0;
this.ratio = ratio;
this.tolerance = 10;
this.toleranceBasis = toleranceBasis;
}
setValue(value) {
let smooth = 0.7;
this.desiredValue = this.desiredValue * smooth + ((value * this.ratio) * (1 / smooth));
}
perform(offset) {
this.lastPerformTick = 0;
this.currentValue += offset;
let which = this.negative;
if(offset > 0) {
which = this.positive;
}
document.dispatchEvent(new KeyboardEvent("keydown", { which: which, keyCode: which }));
setTimeout(() => {
document.dispatchEvent(new KeyboardEvent("keyup", { which: which, keyCode: which }));
}, 1);
}
tick(value) {
this.setValue(value);
if(Math.abs(this.desiredValue - this.currentValue) <= this.tolerance) {
return;
}
this.lastPerformTick += 1;
if(this.lastPerformTick < 3) {
return;
}
if(this.desiredValue < this.currentValue) {
this.perform(-1);
} else if(this.desiredValue > this.currentValue) {
this.perform(1);
}
}
}
class Autopilot {
constructor() {
this.pid = {};
this.control = {};
this.step = 'align';
//this.step = 'approach';
//this.step = 'hold';
//this.step = 'dock';
this.createControls();
this.createRotationPids();
}
createControls() {
this.control.roll = new Control(103, 105, 5, 2);
this.control.pitch = new Control(104, 101, 5, 2);
this.control.yaw = new Control(100, 102, 5, 2);
this.control.x = new Control(69, 81, 5, 1);
this.control.y = new Control(65, 68, 5, 1);
this.control.z = new Control(83, 87, 5, 1);
}
createRotationPids() {
let speed = 3;
this.pid.roll = new PID({
p: 0.75 * speed,
i: 0.02,
d: 0.1,
itermMin: -0.5,
itermMax: 0.5,
/*
outputMin: -1,
outputMax: 1
*/
});
this.pid.rollRate = new PID({
p: 0.75,
i: 0.05,
d: 0.0,
itermMin: -1,
itermMax: 1,
});
// Pitch
this.pid.pitch = new PID({
p: 0.75 * speed,
i: 0.02,
d: 0.01,
itermMin: -0.025,
itermMax: 0.025,
/*
outputMin: -1,
outputMax: 1
*/
});
this.pid.pitchRate = new PID({
p: 1,
i: 0.05,
d: 0.0,
itermMin: -1,
itermMax: 1,
});
// Yaw
this.pid.yaw = new PID({
p: 0.75 * speed,
i: 0.02,
d: 0.01,
itermMin: -0.025,
itermMax: 0.025,
/*
outputMin: -1,
outputMax: 1
*/
});
this.pid.yawRate = new PID({
p: 0.5,
i: 0.05,
d: 0.0,
itermMin: -1,
itermMax: 1,
});
speed = 1.5;
let limit = 5;
let pTranslate = 0.3 * speed;
let iTranslate = 0.005;
let dTranslate = 0.0;
let pVelocity = 2;
let iVelocity = 0.05;
let dVelocity = 0.0;
// Left/right
this.pid.y = new PID({
p: pTranslate,
i: iTranslate,
d: dTranslate,
itermMin: -0.2,
itermMax: 0.2,
outputMin: -10 * limit,
outputMax: 10 * limit
});
this.pid.yVelocity = new PID({
p: pVelocity,
i: iVelocity,
d: dVelocity,
itermMin: -0.5,
itermMax: 0.5,
});
// Up/down
this.pid.z = new PID({
p: pTranslate,
i: iTranslate,
d: dTranslate,
itermMin: -0.2,
itermMax: 0.2,
outputMin: -10 * limit,
outputMax: 10 * limit
});
this.pid.zVelocity = new PID({
p: pVelocity,
i: iVelocity,
d: dVelocity,
itermMin: -0.5,
itermMax: 0.5,
});
limit = 50;
// In/out
this.pid.x = new PID({
p: 0.1 * 3,
i: 0.05,
d: 0.0,
itermMin: -2,
itermMax: 2,
outputMin: -10 * limit,
outputMax: 10 * limit
});
this.pid.xVelocity = new PID({
p: 1,
i: 0.1,
d: 0.0,
itermMin: -0.5,
itermMax: 0.5,
});
}
tick() {
let measured = {
roll: parseFloat(fixedRotationZ),
yaw: parseFloat(fixedRotationY),
pitch: parseFloat(fixedRotationX),
rollRate: rateRotationZ,
yawRate: rateRotationY,
pitchRate: rateRotationX,
x: camera.position.z - issObject.position.z,
y: camera.position.x - issObject.position.x,
z: camera.position.y - issObject.position.y,
xVelocity: motionVector.z * 60,
yVelocity: motionVector.x * 60,
zVelocity: motionVector.y * 60,
};
window.measured = measured;
for(let control of Object.values(this.control)) {
control.tolerance = Math.min(8, Math.max((Math.max(measured.x, 0) - 2) / 20 * 8, 2)) * control.toleranceBasis;
}
measured.hold = 20;
this.pid.x.outputLimits.min = -10;
if((Math.abs(measured.y) < 0.3) && (Math.abs(measured.z) < 0.3) &&
(Math.abs(measured.yVelocity < 0.25) && (Math.abs(measured.zVelocity < 0.25)))) {
measured.hold = 0;
this.pid.x.outputLimits.min = -5;
if(measured.x < 5) {
this.pid.x.outputLimits.min = -0.1;
} else if(measured.x < 10) {
this.pid.x.outputLimits.min = -3;
}
}
this.tickRotation(measured);
if((Math.abs(measured.roll) < 5) && (Math.abs(measured.yaw) < 4) && (Math.abs(measured.pitch) < 4)) {
this.tickTranslation(measured);
this.tickInOut(measured);
}
}
tickTranslation(measured) {
//
this.pid.y.setMeasure(measured.y);
this.pid.y.setSetpoint(0);
this.pid.y.tick();
this.pid.yVelocity.setMeasure(measured.yVelocity);
this.pid.yVelocity.setSetpoint(this.pid.y.get());
this.pid.yVelocity.tick();
this.control.y.tick(this.pid.yVelocity.get());
//
this.pid.z.setMeasure(measured.z);
this.pid.z.setSetpoint(0);
this.pid.z.tick();
this.pid.zVelocity.setMeasure(measured.zVelocity);
this.pid.zVelocity.setSetpoint(this.pid.z.get());
this.pid.zVelocity.tick();
this.control.z.tick(this.pid.zVelocity.get());
}
tickInOut(measured) {
this.pid.x.setMeasure(measured.x);
this.pid.x.setSetpoint(measured.hold);
this.pid.x.tick();
this.pid.xVelocity.setMeasure(measured.xVelocity);
this.pid.xVelocity.setSetpoint(this.pid.x.get());
this.pid.xVelocity.tick();
this.control.x.tick(this.pid.xVelocity.get());
}
tickRotation(measured) {
// Roll
this.pid.roll.setMeasure(measured.roll);
this.pid.roll.setSetpoint(0);
this.pid.roll.tick();
this.pid.rollRate.setMeasure(measured.rollRate);
this.pid.rollRate.setSetpoint(-this.pid.roll.get());
this.pid.rollRate.tick();
this.control.roll.tick(this.pid.rollRate.get());
// Pitch
this.pid.pitch.setMeasure(measured.pitch);
this.pid.pitch.setSetpoint(0);
this.pid.pitch.tick();
this.pid.pitchRate.setMeasure(measured.pitchRate);
this.pid.pitchRate.setSetpoint(-this.pid.pitch.get());
this.pid.pitchRate.tick();
this.control.pitch.tick(this.pid.pitchRate.get());
// Yaw
this.pid.yaw.setMeasure(measured.yaw);
this.pid.yaw.setSetpoint(0);
this.pid.yaw.tick();
this.pid.yawRate.setMeasure(measured.yawRate);
this.pid.yawRate.setSetpoint(-this.pid.yaw.get());
this.pid.yawRate.tick();
this.control.yaw.tick(this.pid.yawRate.get());
}
}
class AutopilotManager {
constructor() {
this.autopilot = new Autopilot();
this.createControl();
this.running = true;
this.enabled = false;
this.pidElements = {};
this.tick();
console.log('Autopilot injection complete.');
}
createToggleButton(label, class_) {
let element = document.createElement('button');
element.classList.add(class_);
element.textContent = label;
return element;
}
createPidDisplay(name) {
let element = document.createElement('div');
element.classList.add('pid-display');
element.setAttribute('data-name', name);
element.textContent = `${name}`;
return element;
}
updatePidDisplay(name) {
let element = this.controlElement.querySelector(`div.pid-display[data-name=${name}]`);
let pid = this.autopilot.pid[name];
element.textContent = `${name} =${pid.get().toFixed(2)} i=${pid.iterm.toFixed(2)} ${pid.measure.toFixed(2)}->${pid.setpoint.toFixed(2)}`;
}
createControl() {
this.controlElement = document.createElement('div');
this.controlElement.id = 'autopilot';
let style = document.createElement('style');
style.innerHTML = `
#autopilot {
position: absolute;
left: 50%;
bottom: 0;
min-width: 200px;
z-index: 100;
transform: translateX(-50%);
background-color: #000;
border-radius: 10px 10px 0 0;
color: #fff;
display: flex;
flex-direction: column;
align-items: stretch;
font-family: Lato, Verdana, Arial, geneva, sans-serif;
padding: 4px 0;
}
#autopilot button {
padding: 15px 0;
margin: 4px 8px;
border-radius: 5px;
border: 2px solid #455253;
background: #151c23;
color: #fff;
text-transform: uppercase;
}
#autopilot button:hover {
cursor: pointer;
}
#autopilot.enabled button.toggle-autopilot {
border-color: #f42;
background-color: #f42;
color: #fff;
}
#autopilot .pid-display {
width: 300px;
margin: -4px 0;
padding: 0 8px;
font-family: monospace;
font-size: 0.8em;
color: #ddd;
}
`;
this.controlElement.appendChild(style);
this.toggleButton = this.createToggleButton('Start Autodock', 'toggle-autopilot');
this.toggleButton.addEventListener('mouseup', () => {
this.setEnabled(!this.enabled);
});
this.controlElement.appendChild(this.toggleButton);
for(let name of Object.keys(this.autopilot.pid)) {
//this.controlElement.appendChild(this.createPidDisplay(name));
}
document.body.appendChild(this.controlElement);
}
setEnabled(enabled) {
this.enabled = enabled;
if(this.enabled) {
this.autopilot = new Autopilot();
this.controlElement.classList.add('enabled');
this.toggleButton.textContent = 'Stop Autodock';
} else {
this.controlElement.classList.remove('enabled');
this.toggleButton.textContent = 'Start Autodock';
}
}
tick() {
if(!this.running) {
return;
}
requestAnimationFrame(this.tick.bind(this));
if(this.enabled) {
this.tickAutopilot();
}
for(let name of Object.keys(this.autopilot.pid)) {
try {
this.updatePidDisplay(name);
} catch(e) {
}
}
}
tickAutopilot() {
this.autopilot.tick();
}
destroy() {
this.running = false;
this.controlElement.remove();
}
}
if(window.autopilot) {
try {
window.autopilot.destroy();
} catch(e) {
console.warn(`Couldn't destroy old autopilot; you may need to reload the page.`);
}
}
window.autopilot = new AutopilotManager();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment