Last active
May 24, 2020 01:38
-
-
Save ForestKatsch/01069f29e8316df117740258313665f1 to your computer and use it in GitHub Desktop.
Automatically dock with the ISS in SpaceX's docking game.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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