github: https://github.com/slin1923/cublii
updated 9/16/2026
Cubli is the first of my projects for which I will use my portfolio as running documentation for. Cubli is inspired by the project of the same name developed at ETH Zurich by the Institute for Dynamic Systems and Control. Using a set of 3 reaction wheels, this robot should be able to balance on a corner. While on a corner, it should also be able to slowly rotate cw/ccw. ReM-RC’s video demonstrates what this balancing should look like, though he does not track yaw angles. As someone who wants to fly satellites, I figured this project was a decent way to get my hands dirty and get some fun ADCS and firmware experience under my belt.
The emphasis of this project is solely on dynamics, state estimation, and controls. I want to give a HUGE thank you to ReM-RC for open-sourcing the hardware for this awesome project. His version is fundamentally similar to the research-grade hardware of ETH Zurich, except redesigned with cheap, accessible COTS parts. All of the part files and schematics for his project you can find on his github repo. I did make a few hardware modifications to this project which you can find on my repo.
I didn’t just follow ReM-RC’s documentation and work instructions to a T. That would make this project meaningless and no more of an exercise in engineering than a lego set is. I am building custom controllers. A quick glance at RemRC’s controller firmware shows that he used a simple, effective, model-free, and well-tuned proportional controller on both gyroscope and accelerometer readings. While that certainly works, I’m an idiot and intentionally want to shoehorn in advanced controllers. I want to model the dynamics of the cube and at least TRY to implement literally every type of controller I know: P, PI, PID, pole-placement, LQR, MPC, H-$\infty$, Adaptive Control, hell if I design a rig that automatically stands the cube back up to its equilibrium point even a RL policy is on the table! Such ambition… get to work!
When completed, I want this to be my magnum opus personal project. Unlike many of my other posts, the intended audience for this post are people in the robotics/GNC field and contains a lot of deep technical concepts sans explanation.
ReM-RC designed 90% of my cube. Annoyingly, all his original Thingiverse files are already non-parametric .STLs. I reverse-designed them to be parametric Onshape files :). I also designed some additional parts of my own you can find below.
If you have an Onshape account (free), you can export, or copy + modify any of the parts below.
Most parts FDM printed in PLA and joined via heatset threaded inserts. Larger flat plate-like parts laser cut out of aluminum.
Here’s a quick gif to appreciate ReM-RC’s design: the flywheel runs clear and true
Again, credit to ReM-RC for hardware scoping and design. Also, a quick shoutout to oshwlab user malebuffy for design of the cubli breakout board and saving me the headache of soldering a thousand pins myself on a DIY PCB. Manufactured by JLCPCB.
This section onward is where my own work truly begins and I diverge from ReM-RC.
In the past, I always flashed to ESP32s using the USB cable and also used the same cable as my data connection for Serial. I would have gone crazy if I needed to do that for Cubli. Not only because I iterate and flash frequently, but more critically because I would need IMU data streamed back to me without the mechanical attachment of a cable affecting Cubli’s dynamics. Platform.IO is a way to flash over wifi and mosquiTTo is a way to publish and subscribe to OTA messages (not dissimilar to ROS).
The workflow for OTA flashing
platformio.ini)platformio.ini contains at least the following
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
# comment the next two lines to upload via USB
upload_protocol = espota
upload_port = 192.168.0.220
The workflow for OTA IMU data readout
& "C:\Program Files\mosquitto\mosquitto.exe" -c "C:\mosquitto-config\mosquitto.conf" -v. All this does is start mosquiTTo& "C:\Program Files\mosquitto\mosquitto_sub" -h localhost -t esp32/accel for live IMU readouts.python log_imu.py and then run python analyze_imu.py if running a proper system ID test.Reminders
main.cpp wrapper flashed to the ESP32 must contain #include <ArduinoOTA.h>void setup() method needs to contain
ArduinoOTA.setHostname("esp32-imu");
ArduinoOTA.begin();
void loop(), especially where you may get stuck within a conditional, add
ArduinoOTA.handle();
ArduinoOTA.handle(); blind spotsesp32/accel matches the mqtt_topic in main.cppnet stop mosquitto and try again (essentially restart mqtt).This is where the fun begins. A lot of dynamic modeling is analytical derivation. Full derivations can be found on the document linked below. On this page you will find key design decisions and results. Another large chunk of modeling is empirical, and involve experimental design and data collection. You will also find those experiments and their results here.
I defined 4 frames of reference for this project. All are outlined below. All are right-hand convention cartesian.
A few extra notes for clarity
At the center of EVERYTHING for cubli is its state space (SS) realization. Let’s rip the band aid off.
\[\begin{align*} x &:= [\theta, \phi, \psi, \dot{\theta}, \dot{\phi}, \dot{\psi}, \tau_1, \tau_2, \tau_3]^T \in \mathbf{R}_{9\times 1}\\ y &:= [a_x, a_y, a_z, g_x, g_y, g_z]^T_{AF} \in \mathbf{R}_{6\times 1}\\ u &:= [\tau_{1c}, \tau_{2c}, \tau_{3c}]^T_{TF} \in \mathbf{R}_{3\times 1}\\ \dot{x} &= [A_{9\times 9}]x + [B_{9\times 3}]u\\ y &= [C_{6\times 9}]x + [D_{6\times 3}]u \end{align*}\]Getting the easy out of the way first, the definition for $y, u, A ,B, C, D$ should not require too much explanation.
Justifying my choice of state vector definition $x$ requires deeper clarification and rationale.
Now let’s derive $A$ and $B$ matrices. This is where I hide all the algebra, rotation matrices, and partial derivatives in my pdf document and spare you the eyesore. In short, the process is
The final system dynamics written out explicitly is
\[\begin{bmatrix} \dot{\theta} \\ \dot{\phi} \\ \dot{\psi} \\ \ddot{\theta} \\ \ddot{\phi} \\ \ddot{\psi} \\ \dot{\tau_1} \\ \dot{\tau_2} \\ \dot{\tau_3} \end{bmatrix} = \begin{bmatrix} 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0\\ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0\\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0\\ \frac{mgr}{I_1} & 0 & 0 & 0 & -\frac{I_{zz}}{I_1}\dot{\psi} & 0 & -\frac{\sqrt{6}}{6I_1} & \frac{\sqrt{6}}{3I_1} & -\frac{\sqrt{6}}{6I_1}\\ 0 & \frac{mgr}{I_1} & 0 & \frac{I_{zz}}{I_1}\dot{\psi} & 0 & 0 & \frac{\sqrt{2}}{2I_1} & 0 & -\frac{\sqrt{2}}{2I_1}\\ 0 & 0 & 0 & 0 & 0 & 0 & -\frac{\sqrt{3}}{3I_{zz}} & -\frac{\sqrt{3}}{3I_{zz}} & -\frac{\sqrt{3}}{3I_{zz}}\\ 0 & 0 & 0 & 0 & 0 & 0 & -\frac{1}{t_{TC}} & 0 & 0\\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & -\frac{1}{t_{TC}} & 0\\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & -\frac{1}{t_{TC}}\\ \end{bmatrix} \begin{bmatrix} \theta \\ \phi \\ \psi \\ \dot{\theta} \\ \dot{\phi} \\ \dot{\psi} \\ \tau_1 \\ \tau_2 \\ \tau_3 \end{bmatrix} + \begin{bmatrix} 0 & 0 & 0\\ 0 & 0 & 0\\ 0 & 0 & 0\\ 0 & 0 & 0\\ 0 & 0 & 0\\ 0 & 0 & 0\\ \frac{1}{t_{TC}} & 0 & 0\\ 0 & \frac{1}{t_{TC}} & 0\\ 0 & 0 & \frac{1}{t_{TC}}\\ \end{bmatrix} \begin{bmatrix} \tau_{1c} \\ \tau_{2c} \\ \tau_{3c} \end{bmatrix}\]Unlike dynamics modeling, measurement modeling is strictly a kinematics problem. Again, there is no shortage of mathematical gymnastics that you need to find in my pdf, but most of the work here is simply knowing how to apply the golden rule of rotational kinematics
\[a_r = a_s - \alpha \times r - 2 (\omega \times v_r) - \omega \times (\omega \times r)\]The final measurement model written out explicitly is
\[\]Analysis is done (for now) and I need to get to know Cubli nice and personal. Low hanging fruit is the rotational inertia of the flywheel. I need this because eventually I am going to allocate $\alpha_i$ from my motor encoders to $\tau_i \in u$ and to do this I need $I_{w}$ to apply $\tau_i = I_{w} \alpha_i$.
Honestly this step is so trivial I am just going to spam you with my hand-calcs and some pictures I took for documentation purposes. $\boxed{I_{w} \sim 2.3036*10^{-4} ~\text{kg m}^2}$.
The problem of finding the inertia tensor of cubli has allowed me to exercise some creativity in experimental design. Since all of my kinematics/dynamics are calculated wrt $O’$, that is also how I define $I_c$. Now ideally I would have a perfect CAD model with all the right mass properties set or overridden such that a simple computer evaluation would give me $I_c$, but I do NOT have this luxury (this is something I may do in the future ONLY if necessary aka I need a more precise inertia tensor).
The overall problem is that I need to find \(I_c = \begin{bmatrix} I_{xx} & I_{xy} & I_{xz}\\ I_{yx} & I_{yy} & I_{yz}\\ I_{zx} & I_{zy} & I_{zz} \end{bmatrix}\)
Where $I$ is symmetric by nature so that $I_{ij} = I_{ji}$, leaving only 6 unique terms to find. BUT, I make the relatively safe assumption that Cubli is rotationally trisymmetric about the z-axis of $O’$, implying $I_{xx} = I_{yy}$ and also that the z-axis of $O’$ is a principal axis of rotation! This simplifies $I$ to be
\[I_c = \begin{bmatrix} I_{xx} & 0 & 0\\ 0 & I_{xx} & 0\\ 0 & 0 & I_{zz} \end{bmatrix}\]Voila! I need only to identify 2 inertia terms of my Cubli! Observing $A$ we also see that I will need $r_{CM}$ the location of the center of mass of cubli, which per the rotationally symmetric assumption, should also lie on the z-axis of $O’$. The value hunt begins!
$I_{zz}$ is the relatively easiest value to find. Since I know $I_w$ and all 3 motors are encoded,
havent gotten around to documenting yet, but here’s a sneak peak