7 ms·
Show HN: SpaceX Dragon simulator docking autopilot in Clojure
- tosh 6y agorelated article: https://medium.com/@thedanpetrov/creating-a-spacex-crew-dragon-simulator-autopilot-in-clojure-1ac095d9209b https://medium.com/@thedanpetrov/creating-a-spacex-crew-drag...
- GistNoesis 6y agohttps://www.reddit.com/r/spacex/comments/gigmfh/spacex_iss_docking_simulator/fqfzatb/ https://www.reddit.com/r/spacex/comments/gigmfh/spacex_iss_d... does it in one line but it's kind of cheating.
- Eduardo3rd 6y agobrowser console is a pretty under rated feature. I wish it was used more extensively to teach people how to code. so many cool little hacks you can do on public web pages that cement new concepts quickly.
- yreg 6y agoAnything is a oneliner when you erase all the `\n`s
- jylam 6y agoPython begs to differ
- vulcan01 6y agoYou can add semicolons at the end of python lines to replace \n's when calling from the command line. python3 -c "n=1;print(n)"
- pferde 6y agoHow does it deal with code blocks normally defined by indentation? (As an aside, indentation being part of syntax is the only thing keeping me disgusted with python - I like the language otherwise.)
- uryga 6y agoit doesn't :) though it works fine if your terminal allows multiline input: python -c " for x in stuff: this(x) that(x) " at some point i even wrote a hacky little wrapper that let you use braces and replaced¹ them with spaces+newlines, something like this: pyc-wrap "for x in stuff { this(x); that(x); }" i can dig the code up if you need it :) ======== though after that i sort of went of the deep end adding perl-style "it" ($_) variables for loop variables: for stuff { this($); that($); (yes, closing braces at the end were optional...) and for the result of the previous line: 2; $+1; hex($); # roughly translates to: x = 2; x = x+1; x = hex(x); surprisingly pleasant to use for shell-like tasks actually! edit: almost forgot - if during execution you used a name `foo` not found in locals/globals, `foo` was treated as an external command, and resolved to something like lambda *args: os.system('foo', *args) so that you could (sorta) easily call external commands. fun times :) --- ¹ slightly more involved than a dumb code.replace('{', ':\n') because you need to maintain the indent level... but around that level of sophistication
- deleted 6y ago[deleted]
- barbegal 6y agoDefinitely cheating, it's not autopilot. It is just updating your co-ordinates every 500ms on a straight line. I would like to see a real autopilot written in javascript that you can paste into the console.
- GistNoesis 6y agoIt's cheating but not as much. It's setting the "motion vector" aka setting the speed at current instant. It's just skipping the controls which are normally setting the "motion vector". For the orientation it's totally cheating though using camera.lookat(target).
- zlsa 6y agoI wrote an autopilot[0] for the simulator in JS, which sends key events to the game. [0]: https://gist.github.com/ForestKatsch/01069f29e8316df117740258313665f1 https://gist.github.com/ForestKatsch/01069f29e8316df11774025...
- jcims 6y agoThis is awesome! As a recent student of PIDs, it's even better after uncommenting the createPidDisplay statement on line 566 :) Only feature request is a 'Do a Barrel Roll!' button.
- xaedes 6y agoVery simple P-controller based autopilot derived from the one-liner: let a = setInterval(() => { // retrieve values from simulation let roll = fixedRotationZ; let pitch = fixedRotationX; let yaw = fixedRotationY; let rpy = new THREE.Vector3(roll, pitch, yaw); let rollRate = -rateRotationZ/10.0; let pitchRate = -rateRotationX/10.0; let yawRate = -rateRotationY/10.0; let rpyRate = new THREE.Vector3(rollRate, pitchRate, yawRate); let pos = camera.position.clone(); let posRate = motionVector.clone(); let targetPos = issObject.position.clone(); let targetRpy = new THREE.Vector3(0,0,0); let dRpy = targetRpy.clone().sub(rpy); // P-controller controls target roll,pitch,yaw-rate (targetRpyRate) to reach target orientation let targetRpyRate = new THREE.Vector3( dRpy.x/1.0, dRpy.y/1.0, dRpy.z/1.0 ); let minRpyRate = new THREE.Vector3(-1.5, -1.5, -1.5); let maxRpyRate = new THREE.Vector3(+1.5, +1.5, +1.5); targetRpyRate.clamp(minRpyRate, maxRpyRate) let dRpyRate = targetRpyRate.clone().sub(rpyRate); // don't calculate with displayed x,y,z but the internal coordinates // (z-axis is forward and backward) let dPos = targetPos.clone().sub(pos); let d = dPos.length(); // P-controller controls target motion (targetPosRate) to reach target position let targetPosRate = new THREE.Vector3( dPos.x / 100.0, dPos.y / 100.0, dPos.z / 100.0 ); let minPosRate = new THREE.Vector3(-0.1, -0.1, -0.01); let maxPosRate = new THREE.Vector3(+0.1, +0.1, +0.01); // thresholds for bang contRol let dRpyRateControlThreshold = new THREE.Vector3(0.0, 0.0, 0.0); let dPosRateControlThreshold = new THREE.Vector3(0.001, 0.001, 0.001); // define some phases where we want to have different movement behaviour if (dRpyRate.length() > 0.1) { // not correctly oriented, stop movement targetPosRate.x = 0; targetPosRate.y = 0; targetPosRate.z = 0; } else if (Math.abs(dPos.z) < 20) { // we are very close, slow approach, larger P-control in lateral // directions to correct remaining errors minPosRate.z = -0.005; maxPosRate.z = +0.005; dPosRateControlThreshold.x = 0.01; dPosRateControlThreshold.y = 0.01; targetPosRate.x = dPos.x; targetPosRate.y = dPos.y; } else if (Math.abs(dPos.z) < 50) { // we are getting, close, slow down minPosRate.z = -0.01; maxPosRate.z = +0.01; minPosRate.x = -0.01; minPosRate.y = -0.01; maxPosRate.x = +0.01; maxPosRate.y = +0.01; } else if (Math.abs(dPos.z) < 100) { // slow down minPosRate.z = -0.1; maxPosRate.z = +0.1; } else if ((Math.abs(dPos.x) < 0.05) && (Math.abs(dPos.y) < 0.05)) { // lateral position correct, use maximum approaching speed minPosRate.z = -0.2; maxPosRate.z = +0.2; } targetPosRate.clamp(minPosRate, maxPosRate); let dPosRate = targetPosRate.clone().sub(posRate); // bang control to reach target roll pitch and yaw rate if (dRpyRate.x < -dRpyRateControlThreshold.x) rollRight(); else if (dRpyRate.x > +dRpyRateControlThreshold.x) rollLeft(); if (dRpyRate.y < -dRpyRateControlThreshold.y) pitchDown(); else if (dRpyRate.y > +dRpyRateControlThreshold.y) pitchUp(); if (dRpyRate.z < -dRpyRateControlThreshold.z) yawRight(); else if (dRpyRate.z > +dRpyRateControlThreshold.z) yawLeft(); // bang control to reach target motion if (dPosRate.x < -dPosRateControlThreshold.x) translateLeft(); else if (dPosRate.x > +dPosRateControlThreshold.x) translateRight(); if (dPosRate.y < -dPosRateControlThreshold.y) translateDown(); else if (dPosRate.y > +dPosRateControlThreshold.y) translateUp(); if (dPosRate.z < -dPosRateControlThreshold.z) translateForward(); else if (dPosRate.z > +dPosRateControlThreshold.z) translateBackward(); },100);
- temptemptemp111 6y agoOh cool, they should redo the moon landing in Clojure too... So the boomers have some sort of closure!
- jcadam 6y agoBack when I worked on spacecraft simulation we used Ada, and we liked it (no, we didn't).
- VLM 6y agoI have a question about control flow of the overall system. Note that I haven't actually run this or debugged it. Or frankly, thought about it for more than a minute or two. Its more of an architectural question. So in dragon.clj there's a case statement to execute commands in the translate function such that the command to go down or left in an abstract calculated sense is implemented as hitting a virtual down or left keyboard key. What happens if you have to go down AND left? I was just thinking why not pass a Clojure list to the translate function and when list contains a down it hits the virtual s key AND if the list contains a left command it hits a virtual a key, then you could wiggle "down-left" at the same time. I mean, obviously it works and sometimes architectural decisions have no technical reason and are arbitrary, which would be perfectly OK. I was just idly curious. Clojure is cool, like programmer catnip, so I had to look at it.
- danpetrov 6y agoHeh, never thought about it that way! But good question. Yes, it can translate on 2 axes at the same time, since the control loops for each axis run in separate threads, so there is no control locking. The same is with rotation - it can e.g. pitch, roll and yaw at the same time.
- bloopernova 6y agoJust from a layman, this is so freaking cool, thank you for writing it and sharing it!
- jcims 6y agoSo if you click one of the rotate buttons after it has zeroed out the offsets will it correct? Is it using a PID model or something else?
- loeg 6y agoThe SpaceX simulation is simplistic enough and gives you enough data that you really don't need a PID controller to hit the target numbers.
- bloopernova 6y agoOK so yes the ISS docking sim is old news, but good lord do I love that UX. I really hope someone has modded it into Kerbal Space Program!
- munchbunny 6y agoIt's definitely easier to read than KSP's navigation ball. KSP was also 100% the reason I managed dock with the simulator on the first try, because in a way it was more like the 50th-100th try, just using a different interface.
- karl11 6y agoThat is some nice looking code!
- TeMPOraL 6y agoIf you want the reverse of an autopilot, try this: https://news.ycombinator.com/item?id=23172281 https://news.ycombinator.com/item?id=23172281 ... and pull the appropriate Interstellar music up in a separate tab.
- thom 6y agoNot to say this is bad or incorrect, but this is very unidiomatic Clojure. I would question the amount of global state, and the broad use of loops inside threads. This can’t be fun to try and test! Perhaps better would be to make pure functions to take current telemetry as an argument and return actions as outputs, perhaps all at once or more likely individually for each control. You could then capture all of this inside core.async go-loops to manage the control flow and timeouts, but you could also probably build something simple with one or more agents wrapping the current state and updating a set of commands to be executed later. Another argument for core.async would be to make it easier to funnel all commands into a single channel because I’m not entirely convinced a WebDriver is multithreaded, but perhaps the library manages that for you. Anyway, cool stuff!
- jcadam 6y agoI'm a Clojure fan but I'm not sure a modeling and simulation problem such as this is the best use case for it. Sure, you can do it, but it probably wouldn't be my first choice. Last I checked, Aerospace software is still dominated by C/C++ and Ada (still alive because it found itself a niche), and there's good reasons for it. The satellite simulators I worked on had to be able to execute actual flight code from the vehicle, so you needed to get fairly low level. Nowadays I write web applications like everyone else on the planet and use Java, JS and/or Clojure (when I can get away with it). But cool little project, nonetheless.
- thom 6y agoI would have to google but I’m fairly sure Rich Hickey has expressly state’s this sort of project as a motivation for Clojure. Perhaps not with such real-time demands or For integrating with hardware, but certainly from the exploration and theory side.
- fsloth 6y agoEach time someone complains a working program is not "idiomatic" in what ever language I die a little inside. That makes it sound you approach this from a haughty presumptuous, uncharitable angle. It sounds like you exclude the possibility that the author did not try it "your way" first, for example, but found a simpler way to implement it. "That's not the most obvious to do it IMO" or "I would have preferred to do it like this..." are much better ways to approach this. Programs should be beautiful and understandable, but "idiomatic" sounds it excludes lots of simple beautiful solutions to a problem out of pure dogma.
- frompdx 6y agoFor some reason I expected this to just interact with some form of http API. After digging into the source code I discovered the author is using etaoin. This is a really neat use of the etaoin library for selenium bindings in Clojure.
- fnord77 6y agoReally nice. Made it right up until almost docked. It crashed for me right as it docked with: Syntax error (ExceptionInfo) compiling at (/private/var/folders/8f/wlmmp1rs38966nkmrs_slnjd1vkfkk/T/form- init8037093313867620508.clj:1:125). throw+: {:type :etaoin/timeout, :message "Wait for {:css \"#success > h2\"} element is visible", :timeout 300, :interval 0.33, :times 910, :predicate #object[etaoin.api$wait_visible$fn__4868 0x173b24c4 "etaoin.api$wait_visible$fn__4868@173b24c4"]}
- ggerganov 6y agoFrom all the auto-pilots I've seen so far, I believe mine docks the fastest (without cheating): https://youtu.be/jWQQH2_UGLw https://youtu.be/jWQQH2_UGLw Source code: https://gist.github.com/ggerganov/092b86a59fa34926998953701ae22ca1 https://gist.github.com/ggerganov/092b86a59fa34926998953701a...
- danpetrov 6y agoNice! It will will the award in speed, but not in the amount of fuel it wasted :D
- ggerganov 6y agoHaha true :) Btw, found this clip of another auto-pilot which is even faster, so my statement above is now incorrect :-) https://www.youtube.com/watch?v=WwWccjAD2i8 https://www.youtube.com/watch?v=WwWccjAD2i8