Inactive!
This project is no longer active. The code moved to https://github.com/marcv81-test/robochicken and the CI builds no longer run. I won’t fix each post :)

@theartofmadeline
NASA

ellievsbear

oozey mess
hello vonnie
One Nice Bug Per Day

Origami Around

Kaledo Art
$LAYYYTER
"I'm Dorothy Gale from Kansas"
RMH

Product Placement
2025 on Tumblr: Trends That Defined the Year
Mike Driver
styofa doing anything
art blog(derogatory)
I'd rather be in outer space 🛸
trying on a metaphor
Lint Roller? I Barely Know Her
cherry valley forever
seen from Serbia

seen from Lithuania

seen from United Kingdom

seen from Türkiye
seen from Serbia

seen from Belgium
seen from Romania
seen from Maldives

seen from United States

seen from Malaysia
seen from Australia
seen from United Kingdom
seen from T1

seen from China
seen from Australia

seen from Germany

seen from Australia

seen from Malaysia
seen from United States

seen from Italy
@robokitchen
Inactive!
This project is no longer active. The code moved to https://github.com/marcv81-test/robochicken and the CI builds no longer run. I won’t fix each post :)

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
PS2 controller interface - Part 2
In the previous post we polled the PS2 controller for whichever data it would provide, but we can get more out of it. I'm particularly interested in the rumble motors: we could get feedback without having to look at LEDs or a screen.
The PS2 controller supports different commands. We previously relied on a single command: poll (0x42). To access the advanced features we have to send other commands. Curious Inventor lists them all.
I first tried to force the controller into analog mode as it looked easier than configuring the rumble motors: we have to send "enter config" (0x43), then "enable analog mode" (0x44), and finally "exit config" (0x43). I significantly reworked the library to support arbitrary commands. In the process I discovered that my earlier estimate for the delay between frames (15-20us) was seriously underestimated: 4ms seems to yield much better results. Shorter delays may result in a 3rd byte of the header response that is different from the expected 0x5a. I believe the other values I encountered (0x00, 0xff, 0xfe) might be error codes but I did not investigate further.
It started as a simple test but forcing the controller into analog mode is actually a nice feature. We don't have to worry about the mode: we can just read the analog sticks whenever. Configuring the controller at the beginning of the sketch works well, but problems may arise if it gets disconnected/reconnected or if the mode changes for other unforeseen reasons. In order to address this I added a counter to reconfigure the controller on a regular basis. I preferred this strategy to reconfiguring only if the mode was not analog: I didn't want to make too many assumptions on the problems which might arise.
At this stage adding support for the rumble motors is trivial. We just need to call "enable rumble" (0x4d) during our configuration sequence and map the motors to the first 2 bytes transmitted to the controller during the poll command (0x42). This requires a minor change to the poll() function and adding 2 functions to control a motor each. I also updated the sketch so that the X and O buttons would make the gamepad vibrate.
The implementation uses a lot of delays, one of which blocks the microcontroller for 4ms at a time. This is inefficient and we may want to run other code while the gamepad is getting ready. I divided the configuration sequence in different states and implemented them as a finite-state machine. Each call to update() checks that enough time has elapsed and runs the next command in the sequence according to the following flowchart.
We now have a PS2 controller library which supports hotplug and allows us to control the rumble motors. The update() function takes a maximum of 270us at a time, which allows us to run plenty of other code.
PS2 controller hardware
So far we connected the PS2 controller to an Arduino Nano using a breadboard. This isn't ideal to use in the field. We could create a shield, but instead we're going to directly solder the PS2 controller extension lead to a Pro Mini.
Below are the soldering points.
Pin 10 = SS (yellow)
Pin 11 = MOSI (orange)
Pin 12 = MISO (brown) + 1k pull-up
Pin 13 = SCLK (blue)
VCC = Power (red) + 1k pull-up
GND = Ground (black) + battery ground
RAW = Motors power (grey) + battery power
The clock pin cannot be accessed once the 1k pull-up is mounted, so we soldered it first. Also the battery wires were too large so we made a smaller gauge extension. I used a 5V Arduino Pro Mini, maybe I would have been better of with a 3.3V one.
Despite some questionable soldering technique it all works fine with the existing sketch.
PS2 controller interface - Part 1
A few articles and libraries explain how to interface an Arduino with a PS2 controller. None of them worked for me because they didn't explain how to connect the gamepad to the microcontroller, but I didn't realise that so I started a new implementation from scratch.
Curious Inventor was my main source of information.
Technical decisions
The PS2 controller uses a variant of the the SPI protocol. We can either bitbang the protocol or use the AVR hardware implementation.
Bitbang SPI pros:
Can use any pins
Hardware SPI pros:
Fast
Saves program memory
Known to work reliably
The only acceptable excuse to bitbang the protocol is if you need to use some of the hardware SPI pins for something else (e.g: PWM). I chose to rely on the AVR hardware implementation.
Connection
The PS2 controllers are reported to work around 3.3V when connected to the console. This is obviously the preferred supply voltage. However both my original and aftermarket controllers seem to work fine at 5V so I didn't bother with a level converter.
The SPI protocol requires 4 pins. On the Arduino Uno/Nano the SPI hardware pins are as follows.
SS: any pin will do, pin 10 is generally used
MOSI: pin 11
MISO: pin 12
CLK: pin 13
MISO needs a pull-up resistor. The integrated Arduino pull-up is said to be more than 20k which is way too high for this application. I used a 1k external resistor. The pull-up resistor was the reason I had no success with other people's libraries, but I only realised too late.
The PS2 controller SPI wiring is as follows.
Command = MOSI
Data = MISO
Acknowledge = SS
Clock = SCLK
The PS2 provides an extra signal wire (acknowledge) which is not part of the SPI protocol. We can ignore it.
Low-level communication
The low-level communication protocol is as follows.
Set SS low
Wait for 10us (determined empirically)
Send/receive a 8-bit word using SPI mode 3 LSB first at 500kHz
If there are more words to send/receive go to 2
Set SS high
Wait for 14us since setting SS high (or for 21us since the end of the last word) before sending another message (determined empirically)
During 6 we do not have to actively wait, we can do whatever else as long as it takes long enough.
The waiting times were determined empirically with a genuine controller. My aftermarket one tolerates slightly shorter delays. The waiting times were shortened until the point where reducing them any further would result in an unreliable connection. The exact delays were then measured using a logic analyser.
The above captures illustrates a real message with the channels in the following order.
SCLK
MISO
MOSI
SS
High-level communication
The low-level protocol allows us to send arbitrary messages made of several 8-bit words. The high-level protocol defines which messages are valid and their meaning. I only implemented one command (0x42). It does everything I require for now. The message is as follows.
Transmit 0x01, receive 0xFF
Transmit 0x42, receive 0x73 in analog mode or 0x41 in standard mode
Transmit 0x00, receive 0x5A
If some of the above data is invalid terminate the message now
If in analog mode read 6 words, if in standard mode only read 2 words
During 5 we send the word 0x00 enough times to read the required number of words.
In standard mode the 2 words contain the buttons status. In analog mode the first 2 words serve the same purpose, and the next 4 words contain the analog joystick values.
On average the implementation reads the analog mode status in 248us, or the standard mode status in 144us.
Genuine vs. aftermarket controller
This is a comparison with only one make of aftermarket controllers. There are other ones and you might be more lucky than me.
The aftermarket controller is more tolerant to shorter transmission delays
The original controller has 8-bit joysticks with values evenly spread across the entire stick movement range. The aftermarket controller has very poor joysticks: the values does not cover the full range of stick movement, and there are only 32 counted possible values instead of 255. I would avoid aftermarket controllers for that reason.
PS2 controller pinout
I would like to use a PS2 controller with the quadcopter and maybe future projects. The analog sticks may not be as precise as a dedicated RC transmitter, but a dual shock is less thank half the size and much more durable for traveling.
I purchased a controller extension lead. I cut the cable in half: I'm only interested in the controller side. I then crushed the console side with a vise grip to identify the wires colors. The colors might differ from cable to cable, don't trust my findings.
From left to right, based on Curious Inventor's pinout:
Green: Acknowledge
N/C
Blue: Clock
Yellow: Attention
Red: Power (3.3V)
Black: Ground
Grey: Motors power
Orange: Command
Brown: Data

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
Moment of inertia with respect to the yaw axis
Same as just before, but with respect to the yaw axis this time.
Oscillation period of the bifilar pendulum:
20T = 23.486 - 2.848, T = 1.0319s
20T = 44.056 - 23.486, T = 1.0285s
20T = 64.630 - 44.056, T = 1.0287s
Average T = 1.0297s
Other experimental quantities:
m = 0.958kg
b = 0.235m
L = 0.70m
Moment of inertia:
$$I = \frac{m g T^2 b^2}{4 \pi^2 L}$$
$$I = 0.0199 \:kg\:m^2$$
Moment of inertia with respect to the roll/pitch axes
We're doing the same experiment as this guy. The moment of inertia was measured with respect to the pitch axis, but we would expect the same result with respect to the roll axis because of the quadcopter symmetries. Conveniently the roll, pitch and yaw axes are principal axes of rotation.
Oscillation period of the bifilar pendulum:
20T = 15.867 - 2.555, T = 0.6656s
20T = 29.010 - 15.867, T = 0.65715s
20T = 42.154 - 29.010, T = 0.6572s
20T = 55.231 - 42.154, T = 0.65385s
Average T = 0.65845s
Other experimental quantities:
m = 0.958kg
b = 0.235m
L = 0.51m
Moment of inertia:
$$I = \frac{m g T^2 b^2}{4 \pi^2 L}$$
$$I = 0.0112 \:kg\:m^2$$
Arduino Continuous Integration
Arduino projects should not require continuous integration. Most of them define a single sketch with only a few files in one folder: it's easy to verify that the code compiles, and a developer is unlikely to miss files while committing. This said Robokitchen has configurable libraries reused across different sketches. It would be good to check that changes in the libraries don't break any of the sketches which depend on them. The Linux version of the Arduino IDE 1.5.2+ supports command line compilation. We're going to set up a Jenkins server to compile all the sketches whenever a change is pushed to GitHub. We use VirtualBox and Ubuntu Server 14.04.1 LTS, but any other virtualisation software or distribution should work equally well. VM setup We keep the default settings for the VM (512Mb RAM, 8Gb HDD) and Ubuntu. We just add OpenSSH server and Tomcat at the end. Once the setup is complete we turn the machine off and add a host-only network adapter. It operates on the 192.168.56.0/24 subnetwork by default in VirtualBox. We restart and add the following lines in /etc/network/interfaces. auto eth1 iface eth1 inet static address 192.168.56.10 netmask 255.255.255.0 After restarting we should be able to SSH the machine on 192.168.56.10. After confirming this works we can reboot and run the VM headless, holding shift as we click start. Arduino IDE headless Let's now download and install the latest Arduino IDE. wget http://downloads.arduino.cc/arduino-1.5.7-linux64.tgz tar -xvf arduino-1.5.7-linux64.tgz sudo mv arduino-1.5.7/ /usr/local/ sudo chown root:root -R /usr/local/arduino-1.5.7/ Let's clone the Robokitchen repository and compile a test sketch. sudo apt-get install git git clone https://github.com/marcv81/robokitchen.git /usr/local/arduino-1.5.7/arduino -\-verify robokitchen/sketches/Quadcopter/Quadcopter.ino Bad news, it does not work. As explained in this issue the Arduino command line interface still requires a X server to work. No problem we can install Xvfb as a stub. sudo apt-get install xvfb sudo apt-get install xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic We also need to create the Upstart configuration in /etc/init/xvfb.conf. description \"Xvfb X Server\" start on (net-device-up and local-filesystems and runlevel [2345]) stop on runlevel [016] exec /usr/bin/Xvfb :99 -screen 0 640x480x8 Ubuntu Server only provides openjdk-7-jre-headless, so we also need to install the regular openjdk-7-jre. sudo apt-get install openjdk-7-jre Once we restart we can export DISPLAY=:99 and try to compile again. Appart from an irrelevant Xvfb error it now works. Maven I quite like the combination of Maven and Jenkins: the modules are displayed nicely. Let's define our sketches as modules of a parent project. For portability we rely on the ARDUINO_HOME environment variable. We can now install Maven and compile all the sketches at once. sudo apt-get install maven cd robokitchen/sketches export DISPLAY=:99 export ARDUINO_HOME=/usr/local/arduino-1.5.7/ mvn compile Jenkins We are going to deploy Jenkins in Tomcat. The Arduino builds should be lightweight, but it's a good practice to set the tomcat7 home directory to a partition with enough space. sudo service tomcat7 stop sudo mkdir /home/tomcat7 sudo chown -R tomcat7:tomcat7 /home/tomcat7/ sudo usermod -d /home/tomcat7 -m tomcat7 sudo service tomcat7 start Let's now download and deploy Jenkins. wget http://mirrors.jenkins-ci.org/war-stable/latest/jenkins.war sudo chown tomcat7:tomcat7 jenkins.war sudo mv jenkins.war /var/lib/tomcat7/webapps/ The Jenkins URL is http://192.168.56.10:8080/jenkins. Let's first go to Manage Jenkins > Configure System > Global properties and define the following environment variables.
DISPLAY=:99
ARDUINO_HOME=/usr/local/arduino-1.5.7/
On the same screen we should define a Maven installation. On Ubuntu Server 14.04.1 LTS the default is Maven 3.0.5 installed in /usr/share/maven/. Now is a good time to install the GitHub plugin and restart Jenkins. We finally have all we need to create a Maven job. The main parameters are as follows.
Git repository URL: https://github.com/marcv81/robokitchen.git
Root POM: sketches/pom.xml
Goals: clean compile
One run this job should lead to a nice screen full of blue balls.
Lift force measurement
Let's measure how much lift force each motor/propeller is generating.
Measure The idea is to put the quadcopter on a scale and observe how much less weight is measured when a motor spins. To reduce the disturbances this should be done indoors and with enough clearance to avoid any ground effect. As all the motors/propellers should produce similar results I decided to measure the front one only. I modified the QuadMotorTest sketch to help us with the measure.
The PWM high-level time is now printed on the serial port.
The PWM is now adjustable on the AUX channel which I set to a knob on the transmitter.
The elevator and ailerons now activate the motors independently, and the throttle and rudder activate the pairs of opposite motors.
It is now easy to control exactly the input of each ESC/motor. It will help us relate the PWM to the generated lift force.
I spent £12 on a small kitchen scale which can weigh objects up to 3kg. I taped 2kg worth of books to the quadcopter but I could only test the 200-900ms range before it would fall from the scale. ASDA makes a fantastic £8 kitchen scale which can weigh up to 10kg. I taped the quadcopter to a 8kg box of books and I could measure the whole 200-2000ms range without any problem.
In order to acquire as many samples as fast as possible, I setup a camera on a tripod to take photos of the scale and the computer screen as I increased the motor speed. I ran two data acquisition rounds: each produced more than 100 samples in 3-4 minutes. I noted the unloaded battery voltage at the end of the measure to differentiate the data sets: these are not the voltages under load while the motor was spinning.
Analysis
Apparently the laws of physics predict that the force magnitude should be proportional to the square of the RPMs. The SimonK ESC firmware claims to have a linear throttle response, which I suppose is meant to be understood in terms of RPMs. If this claim (and physics) are true, then the square root of the lift force could be expressed as a linear function of the PWM. This turns out to be a reasonable estimate, especially if we exclude the extremely high and low PWM values.
f(x) = 0.00118415*x + 0.30756716
The linear RPMs estimate leads to the following quadratic force estimate. Again it's not too bad, especially if we exclude the extremely high PWM values.
The motors nuts were coming loose because of the vibrations. Let's see if using two makes it better. I could not find a small enough wrench so I'm using pliers to tighten them: not ideal.

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
Putting things together - Part 2: Controller
From previous experiments and code we can achieve the following.
Control the speed of each motor
Read the orientation accurately while the motors are spinning
Read the transmitter sticks
What we need next is to implement controllers which ensure the motors make the orientation follow the sticks. Just like in the previous servo gimbal sketches we'll use PID controllers.
Before anything else we rewrote the PID class using the velocity algorithm. We also removed the output bounds and included the sampling time in the formulas. The velocity algorithm is a convenient implementation which does not change anything in the PID behaviour.
Transmitter sticks
The sticks values range from -100 to 100. We programmed the controller so that a switch kills the throttle.
When the throttle stick is bellow a certain threshold (-80) we kill the motors altogether. Good for safety.
When the sticks are within a certain range of the center (+/-5), we set them to zero. This shall help to dissociate software and control issues from sticks centering or trim issues.
Motor mix
We are using a +4 configuration and already decided of the following.
Front: CW
Back: CW
Left: CCW
Right: CCW
This means that the motor mix shall be as follows.
Front = Idle + Throttle + Pitch - Yaw
Back = Idle + Throttle - Pitch - Yaw
Left = Idle + Throttle + Roll + Yaw
Right = Idle + Throttle - Roll + Yaw
The potential problem with the above formulas is that we might attempt to drive some motors with values which are out of bounds: we might be asking the motors to spin faster or slower than they possibly can. This would cause the PIDs to wind up. In order to remedy this issue, we can search for out of bounds control values and add the same constant to all the control values to achieve the following.
Keep all the control values within bounds.
Maintain the difference between each of the control values, so that the roll and pitch can still be controlled.
Idle should be adjusted so that the quadcopter does not gain or loose much altitude when the throttle is centered. This is only a rough adjustment.
Because our ESCs run the SimonK firmware we don't necessarily need to drive them with high level signals between 1ms and 2ms. Instead we configured them to handle high level signals between 0.2 and 2ms. This almost doubles the ESCs control resolution. Idle was set to 1ms.
Roll and pitch rates (rate mode)
The most simple mode to implement is the rate mode. Because a +4 quadcopter is symmetric we can tune the roll and pitch rates controllers at the same time. These controllers shall ensure the rotation rates on the roll and pitch axes are proportional to the ailerons and elevator sticks.
We tuned these controllers as PDs. I held the quadcopter above me, with a hand at the extremity of two opposed booms, and I controlled the transmitter with my feet. A proper test rig would have been more appropriate.
First we set $K_d$ to zero and increase $K_p$ progressively until we get light oscillations. Then we increase $K_d$ progressively until the oscillations dampen, but we stop before the quadcopter gets twitchy. We obtained $K_p = 150$ and $K_d = 8$.
Yaw rate
The yaw rate controller ensures the rotation rate on the yaw axis is proportional to the rudder stick.
We tuned this controller as a simple P. Because the motors torque control the yaw +4 quasdcopters aren't too responsive on this axis, so using only P shall be good enough. I simply put the quadcopter on the floor and tried to yaw.
We increased $K_p$ progressively until the response felt acceptable. We couldn't reach oscillations. We obtained $K_p = 500$.
Roll and pitch angles (attitude mode)
The attitude mode is more elaborate than the rate mode. Instead of controlling the roll and pitch rotation rates, the ailerons and elevator sticks control the roll and pitch angles. We reused all the controllers from the rate mode. However we drove the roll and pitch rate controllers with new roll and pitch angles controllers instead of the sticks.
We tuned these new controllers as PIs. They were tuned while flying.
First we set $K_i$ to zero and increase $K_p$ progressively until we get light oscillations. We then reduce $K_p$ until the quadcopter is stable again. We now increase $K_i$ until we get light oscillations, and reduce $K_i$ until the quadcopter is stable again. We got $K_p = 5$, $K_i = 20$.
Performance
The quadcopter performs well indoors. Outdoors it may be unstable when there are sudden wind gusts or when there are rapid stick changes from one extreme angle to the other. The quadcopter is not useable, but it can stay airborne reliably enough to allow characterisation and further tune the controllers.
First acceptable controller and settings!
490Hz ESC control
Due to some potentially unrelated issues I researched how to better implement the PWMs controlling the ESCs. I'm not sure it was absolutely required but now it's there, it works, and for the moment I'm not reverting to see what would happen.
The new implementation supports a 490Hz refresh rate instead of 50Hz: this might help to better recover from an out-of-timing high-level pulse, but I wouldn't expect any. We use hardware rather than software PWM: I can't find my Saleae anymore, but I would expect a better timing accuracy. Also the timing resolution is now 8ms instead of 4ms: this could yield poorer results and I may revert to the old library to compare the results in the future.
The implementation is very straightforward. The ATmega328p timers are configured to the following values:
Timer 1 (pins 9 and 10):
WGM1 = 0001: PWM, Phase Correct, 8-bit, top = 0xFF.
CS1 = 011: Prescaler set to 64.
COM1A = 10: Clear OC1A on Compare Match when up-counting. Set OC1A on Compare Match when down-counting.
COM1B = 10: Clear OC1B on Compare Match when up-counting. Set OC1B on Compare Match when down-counting.
Timer 2 (pins 11 and 2):
WGM2 = 001: PWM, Phase Correct, 8-bit, top = 0xFF.
CS2 = 100: Prescaler set to 64.
COM2A = 10: Clear OC2A on Compare Match when up-counting. Set OC2A on Compare Match when down-counting.
COM2B = 10: Clear OC2B on Compare Match when up-counting. Set OC2B on Compare Match when down-counting.
The above initialises a 490Hz PWM on the pins 9, 10, 11, and 3. We control the duty cycles setting OCR1AL, OCR1BL, OCR2AL, or OCR2BL. The pins cannot be changed, they are hardwired to the timers. There is no need to program interrupts, once set the PWMs and duty cycles remain until the micro-controller is power cycled (danger in case of a software crash!). The updated motors test sketch works without any problem.
Putting things together - Part 1: IMU
The IMU algorithm has been working great with separate sensors and under ideal circumstances. Let's try to get it to work using the MultiWii board and with the motors running.
Axes
First I checked the IMU, motors off, using the USB serial port. I tried to visualise the orientation but the axes were incorrect. I naively expected the Multiwii board to have its axes aligned with the little painted arrow but it's not the case.
I first tried to look at the Multiwii source code but it's weird. I know the MPU-6050 accelerometer and gyroscope axes are aligned in the hardware, however the Multiwii code defines different axes mapping for each (for CRIUS_SE_v2_0 for instance). This means Multiwii uses different axes for the accelerometer and the gyroscope: I'd be curious if someone had a good explanation!
After a few tries I figured the following.
X => Y
Y => X
Z => -Z
Motors on
We cannot use the USB serial port when the motors are running (we would be connecting power supplies with possible voltage differences in parallel). We'll use the Bluetooth serial port instead. I modified the Python scripts so that the serial port and the baud rate are specified as command line arguments. This allows to switch between USB and Bluetooth easily.
To visualise the orientation over Bluetooth:
Run stty -f /dev/tty.Robokitchen-DevB 115200 (in OSX)
Run Orientation.py /dev/tty.Robokitchen-DevB 155200 (in OSX)
Vibrations
The first obvious observation is that the orientation is off when the motors are running. This is probably because of vibrations.
I enabled the MPU6050 digital low pass filter (DLPF) at its most aggressive setting: 5Hz cut-off frequency. At first I thought this would be pointless because the IMU complementary filter already filtered out the accelerometer high-frequencies (i.e.: vibrations), but the DLPF removes vibrations form both the gyroscope and the accelerometer. This brings back the orientation estimation to a useable accuracy.
The next step will be to implement controllers to drive the motors based on the orientation estimation and its desired value.
Ouch
It was a bad idea to connect the FTDI programmer before unplugging the LiPo. I had not even connected the USB to a computer. The motors started suddenly, and stopped shortly after a propeller cut the USB cable, which for all I know is probably unrelated anyway.
The quadcopter only suffered minor damage, with a few bent pins.
And I didn't cut myself too bad either.
Lesson not really learnt as I can't see myself removing the propellers every time I program the Arduino. I just have to be careful and disconnect the LiPo after every use, which I knew anyway but wasn't careful enough about.

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
Matplotlib on OSX
Matplotlib was easy to install on Ubuntu.
sudo apt-get install python-matplotlib
The same cannot be said of OSX. Matplotlib 1.3.1 for Python 2.7 suffers from a numpy version mismatch when used with the latest Python 2.7.6. I had to install Matplotlib 1.2.1 instead, which by the way requires XQuartz.
I rebuilt the frame with two 150x2mm perspex sheets rather than a single 100x5mm one. I used M3 bolts rather than M5. It measures 470mm motor to motor, I dremeled the arms to size.
This version provides more space for the electronics and allows to velcro the battery underneath. I couldn't properly attach a battery to the previous one.
There is a tiny crack on the bottom sheet from overtightening one of the bolts. I expect it to get worse with the vibrations. We'll see.