[FPV] Analysis of TBS Crossfire, reverse engineering the air link

I started flying FPV quads for a bit of freestyle flying about two years ago. Not very skilled, but also flying every now and then, so no real focused learning.

Just a random crash session where I didn’t expect the VTX to be that bad at 800 mW

Quickly I jumped onto the TBS Crossfire train and bought a TBS Tango 2 and a few Nano RXs. I liked the compact design of the remote and the idea of having a flexible telemetry protocol. Of course I also looked into the Crossfire serial protocol (the one between quad and the receiver), but thats another story.

A Crossfire Nano RX receiver from TBS

I am aware that there is now tracer and even an open source solution ExpressLRS which also features an ESP on the RX side, which is quite cool. Having extra wifi on the Rx is kinda overkill, but its for free.

Anyway. Soon I realized that the Crossfire Nano RX is basically a PIC32 and a SX1272 LoRa modem. So I found it very practical that there is an ESP32 based, easy to use LoRa device with display. The Heltec WiFi LoRa 32 (V2).

A Heltec LoRa 32 v2 that you can get at https://heltec.org/project/wifi-lora-32/

So what can we do with it? Can we sniff Crossfire? Can we Hijack it? Or can we do some DoS attacks?

Well, lets try to find out.

Fist of all we have to determine which features of the LoRa modem Crossfire devices are using and which frequencies are used. One possibility would be dumping the firmware and reverse engineer it as I usually prefer to do. Stumbling a bit around, I managed to get a list of their firmware versions and also some firmware file links available without any authentication. When looking closer at those files, the files seem to be encrypted.

Comparing the binary files using a custom tool I made (think of it like an advanced hex editor), you can see that the encryption is probably a block cipher with 8 byte blocks, or maybe a simple XOR based encryption. It also looks like there is a 10 byte header that contains the hardware ID and firmware revision.

Difference view between two firmware revisions. Red areas differ between two versions

Well, I did break this kind of encryption a few years ago using known plaintext, but this took some time and I didn’t want to spend too much time on it. Spoiler: I still spent quite a lot time though…

So the encryption is spoiling the fun and no quick win there.

Now, shall we dump the firmware from the PIC32MX170F256D using some ChipWhisperer attacks?
Nah, my beloved IDA Pro Advanced doesn’t even support that chip, so it would probably be a bit too much work finding out the registers etc.

So I decided to sniff the SPI communication between the PIC32 and SX1272. Not having a logic analyzer here right now, but a few FPGA boards, the next steps were clear.

Prepared the Crossfire Nano RX for tapping the SPI signal lines

After a bit of soldering, I connected those lines (MOSI, MISO, SCLK, SS) to an CYC1000 board from trenz electronic – which I received from ARROW at an embedded world visit a few years back. Great hardware. Only a few bucks and you have a powerful FPGA board on your desk. Get one of those and play with it.

Connecting the receiver to one of my FPGA boards

First I tried one of those readily available logic analyzer designs that work with sigrok PulseView, but that didn’t really work out. Just received a few bits and then silence. So I decided to design my own SPI sniffer.

Didn’t want to spend a lot of time, do you remember? ;)

After modeling the block schematic in Quartus and writing the SystemVerilog design of the SPI log engine, the hardware part was ready to go. Well, I hate clock domain crossing. Seriously. Especially when a dual-clocked FIFO of Intel’s IP library doesn’t solve the glitches.

After a few additional hours of C# hacking, there was a SPI sniffer tool, that receives the data from the FPGA via USB serial port and parses the SX1272 register reads/writes to show a log of all configurations made. It allows saving the logged binary data and playing back the log, if I add a new parsing or analysis feature later.

A simple SPI sniffer frontend parsing the SX1272 register reads/writes

It also logs the frequencies and builds a map in which order the channels are changed. This channel hopping sequence is most likely only valid for my configuration and my bind key – if something like that applies to Crossfire. For other TX/RX pairs I expect the channel sequence to differ.

Lets interrupt for a second and talk about some terminology
channel” – the used frequencies are all equally spaced from each other, so I assume there is a logical channel numbering, being 0 the channel with the lowest frequency I have seen
uplink” – data sent up to the quad by the TX / remote control
downlink” – data received from the quad

On most modes, the Crossfire protocol uses channels 0-49 for TXing data to the receiver, then switches frequency exactly 50 channels up and the receiver answers on channels 50-99. Then the TX switches to the next channel according to the hopping sequence. This process is repeated 150 times and then the whole hopping sequence repeats.

Hopping sequence example

So in this example above, the TX sends its data on channel 0, switches to channel 50 and waits for the RX to acknowledge the data and send telemetry. Then the TX switches to channel 18, sends its uplink data and waits on channel 68 (18+50) for the RX to answer. After all channels in this sequence were sweeped, it starts over again at channel 0.

RACE modes however, stay on the same channel for uplink and downlink data – but use channels 0-99 for hopping.

The exact frequencies might also be part of the initial binding and so my theory might be wrong. Will test it on other setups too. All of this communication was done in the FSK mode of the LoRa modem, having a frequency shift of 42.48 kHz and a bitrate of 85.1 kBaud.

These are the frequencies my setup uses:

868 915 868
Fmin [MHz]860.165902.165860.165902.165863.093915.165
Fmax [MHz] 885.905927.905885.905927.905868.581927.905
Channels Rx/Tx
or shared
Spacing [kHz]260260260260112260
FreqShift [kHz]42.4842.4842.4842.4842.4842.48
Bitrate [kBaud]
Frequencies differed between two tested hardware setups by ~15 kHz

At this point in time, I started coding an arduino sketch for the Heltec board. I wanted to see the data being griefed out of the air instead from a lame SPI log. Also – although in this post it seems I was sniffing both directions of that SPI communication from the beginning, but in reality at this time I only sniffed the writes to the LoRa chips, committed by the PIC32. This was enough to get the settings, but misses what the Tango 2 TX sent. Only later the full SPI communication was logged.

Although having the hopping sequence, I started an arduino sketch that simply waits on a single channel for data. Hopping is something that can be added later, first configure the SX1276 of that Heltec board to receive data from the TX. Maybe you already noticed – SX1272 and SX1276. One of them is on the Nano RX and the other one on the Heltec board.

Luckily they are quite compatible, just a few registers are different. So no simple replay of the logged data, but code it all manually. Would have to do this anyway, so thats okay.

After a few hours I received the first data from the remote control. The payload (23 byte uplink, 13 byte downlink) was of unknown format, but quite obviously coded. Interesting is the fact that over the air only 10 bits of resolution is used whereas the serial protocol uses 11 bits. I don’t expect this a huge quality issue for Crossfire, but it was unexpected and caused some confusion to the least.

Some other interesting fact:
When you use the 8-channel mode, the TX sends sticks 0-7 on every packet.
If you use the 12-channel mode instead, then the TX sends sticks 0-3 on every packet and alternates sticks 4-7 and 8-11, dividing their update rate to the half.
The alternating packet is marked in the first byte of the uplink data (bit 5 set)

Regarding the timing, having a RX/TX pair every 6.666 ms is due to the 150 Hz rate quite expected. The downlink packet is received at latest 2.6 ms after the uplink packet has finished sending. This period is shorter as the downlink packet is a few bytes shorter. Will do some maths and build a schedule table if I get all numbers right.

From a later SPI log – the receiver’s view on the uplink (RX) and downlink (TX) channels
Some notes about the packet format. Rx means the receiver received this, Tx means the receiver sent this.
First steps, listening on one channel
Visual upgrades make things cooler and more professional

Also the CRC was obviously placed at the end of the packets, but the TX used 16 bit CRC in the uplink packets and the RX answered with 8 bit CRC in the downlink packet.

Unfortunately the CRC never matched and quickly I realized that there must be some seed that is used to initialize the CRC. Even packets with identical payload had, depending on the hop number, different CRCs. After a second, when the hopping sequence repeated, the init values also repeated and so the CRCs did. This means every of these 300 hops per second (150 up/downlink packets per second) must have its own init value for the CRC.

After I noticed that, brute forcing the exact CRC type was just a matter of a few hours of coding.
CRC8: poly 0x07, refin=false refout=false xorout=0x00
CRC16: poly 0x1021, refin=true refout=true xorout=0x0000
But the init value is unknown and depends on the hop number.

When doing a firmware upgrade or binding the receiver, this happens in LoRa mode and uses the CRC8 with init value of zero. Of course is the firmware data over air still encrypted and just the data we have seen above, right after the 10 byte header.

This CRC initialization with the hop number and some seed value is clearly a sign that TBS wanted to make sure multiple TXs in one area cannot interfer with each other in a way that incorrect stick values get interpreted by a RX that should not receive the data.

But is this some sort of security feature?

Definitely not. I can already detect the hopping sequence through listening and timing measurements and also capture the CRC init values from air without anyone noticing. Then I could use those values to fake a TX and gain control over a victim’s racing quad. Not done yet, but easily™ possible from what I know right now.

After struggling with timer issues in the arduino-esp32 library that seem to be a bug in the SDK, the exact timing to follow channel hopping was possible.

Following the channel hops is no big deal
You don’t know your hopping sequence from SPI logging? No worries! Just sniff them by exact packet timing measurement.

If you follow the hopping sequence correctly, you will log all stick values and telemetry metadata properly. Crossfire has a lot of them – even the config menu of the RX itself is transferred via slow rate telemetry channels. Here a decoded config menu of my v6.06 Nano RX showing all menu entries and their possible options.

Serial log of the arduino sketch that sniffs an active crossfire configuration menu content from air

To track down some lost packets, I added plots to show information like RSSI (up/down), frequency correction values and even packet timings.

Looks a bit shabby? Yeah, let’s add some plots

What next?

This worked for my setup. For my bound TX/RX pair. It did not work out of the box for an other pair that a kind person tested for me on his setup. Somewhat expected that. I am sure it has to do with the exact frequencies used. Protocol-wise I don’t expect any surprises as there is no authentication, just a salted CRC and some hopping which both can be calculated back.

The guy who tested it for me on his setup receives the SPI sniffing setup the next days and then we will figure out his FSK config (frequencies, rate, modulation parameters) and will make the sniffer work for his setup, too. I guess thats the final step towards a generic solution.
He received the equipment and after a short log capture session, the reason was that my guess of the first frequency being used was a bit off. So just extending the base frequency 15 kHz down resolved the issue and it worked on his setup as well.

Also note, I only analyzed the 150 Hz mode that uses FSK modulation. There are also other modes the Crossfire system can work with. There is a 50 Hz mode and a even slower LoRa modulation mode for really bad reception situations. Not sure if I will add support for these.

Also the successor tracer may be interesting, but as they share source base, I expect it to behave the same or at least similar.

My personal conclusion:
Safety – GOOD
Security – BAD
Privacy – NONE

However, this matches my expectations, so I am fine with it.

Source code: https://github.com/g3gg0/ESP32_CRSFSniffer

[FPV] Eachine EV800 mod

I own FPV goggles from Eachine and have been very happy with them. Yes, you look like an idiot with a brick on your forehead, but the antennas make you look more like a Johnny 5 fan, which is cool again. At least was 3 decades ago. Don’t worry, these FPV quads are flown in areas where you are less likely to be seen anyway.

Eachine’s EV800 with a price point around 60€

Later I bought some second hand FatShark Dominator v3 goggles.
All the cool guys use them, so they must be a lot better.

FatShark Dominator v3 with a price point at about 300€

Mine came with a Furious FPV True-D diversity VRx module. Man, this all must be a great setup with all the colorful LEDs and OLED thingies.


When first testing the setup, I instantly wanted my EV800 goggles back. While the strap keeps the goggles tight on your head and the much lighter – and especially shorter – setup feels a lot more comfortable, the screen size looks more like a tamagochi. The FOV of 30° diagonal compared to the 140°/120° (h/v) of the EV800.
Not doing maths here, but you can compare it like watching a movie on your 70″ TV versus on you mobile phone. It works. But meh.

So I wanted to add support for the FatShark receiver modules and a mini DVR to the EV800, so I could combine the best of both worlds.

The first step was designing a mount in which I can plug the receiver module into. No photos of the wiring here – have a video instead. I just placed a 2.54mm female header in the slot you can see, looked up some images on the interwebs on what the pinout is here and soldered a pre-crimped 5-pin JST with wires there.

New Product AKK Diversity RX for Fatshark type goggles - RC Groups
One of the many images showing the FatShark VRx module pinout

Then added a mating JST pin header to the board using super glue and some UV curing resin.

First I added the FatShark VRx video and power supply
Later added audio for the DVR and onboard headphone amplifier,
… the video part for the DVR
… and finally the audio part. Edit: please remove the black resistor right below the blue dot

The video signal is fed into the AV input line, so I can easily switch between onboard VRx and the FatShark bay module by pressing one button on the EV800.

For the audio lines you will notice that I added two 68 Ohm resistors before combining L and R channel – this is to divide down the voltages and prevent short circuiting the outputs of the FatShark VRx module and the output of the onboard VRx. This seems still too much amplitude for the DVR module – audio is overdriven far too much. Next time I open the forehead-brick, I will add an extra resistor to ground.

Speaking of that, I did not check if the audio output of the onboard VRx is shut down when switching to AV. Maybe it feeds some white noise into the amp, causing my overdriven audio issue. Will have it check that, too.

Edit: Indeed the onboard VRx was feeding noise into the amp and DVR. Just removing the small resistor below where you tap audio is enough.

Of course I removed the battery from the EV800 to save weight and placed a high quality, premium step down converter which gets its power from an XT30 connector I placed on the right side of the goggles and converts that down to 4.2V. This also gives me more options regarding powering the goggles and no special charger wire etc.

Maybe this information is helpful for someone, so I decided to share it.

[ESP32] A tiny WiFi/BT to (true) RS232 adapter

Ages ago I received a defective Keithley SourceMeter 2400 that I could easily repair. I also have an old AOR A5000. Both are remotely controllable using their RS232 interface. But nowadays long RS232 cables aren’t quite state of the art and placing USB cables through my office is neither. As my computer and those devices are some meters apart, I needed a proper solution.

Unfortunately it doesn’t always work to fake RS232 using 3.3V lines directly off an ESP32, so I decided to make a custom PCB that features an ESP32 plus a true RS232 Transceiver regarding voltage levels.

Rs232 oscilloscope trace.svg(Source)

My PCB named ESP232 is designed to be quite compact, merely bigger than the footprint of the ESP32 itself. Rotating the ESP32 could have made the PCB a millimeter narrower but a bit longer. But the design intentionally has the USB power plug and antenna on the side as the devices usually are quite long and bump the wall behind anyway.

This time I decided to use an inefficient AMS1117 and not the TPS62291 for the 3.3V supply. It wouldn’t run all day long or drain a battery, so KISS. Maybe I could have saved some mm² space, but the size is dictated by the ESP32 anyway.

The software is built using arduino and supports OTA updates – if you enter update mode by bridging Rx and Tx in the connector (loopback). The bridge gets detected on startup and the firmware enters OTA-only mode.

Why that complicated? I found that the OTA module crashes quite often in my network. (AFAIR it fails to allocate somewhere around the common 1400 bytes using new[] in some UDP receive func). To work around that, I just disable OTA by default. Additionally, this is safer to not accidentally flash the wrong device when updating some other device in hurry.
Of course there is also a rudimentary web interface to select baud rate that gets saved via arduino/ESP32 EEPROM driver.

The serial port is exposed via TCP on the telnet port (23), so you can either connect from your application which supports TCP transport. Or you connect a second ESP32-Board to your computer with a firmware that simply bridges its serial data to the TCP-port of the adapter (USB <-> WiFi <-> RS232).
So your computer sees a COM-port that magically forwards data to the remote (real) RS232. Did a similar thing with one of my 3D printers so cura can print over network.

Unfortunately SmuView doesn’t support the SourceMeter 2400’s SCPI flavour out of the box, so I would have to patch it manually. Sigh :( Maybe somewhen later.

This PCB is not a big thing. Literally. But does its job and it’s cheap.

Schematic/PCB: https://oshwlab.com/EFS-GH/esp32-rs232
Sources: https://github.com/g3gg0/ESP232

[ATtiny85] A simple Crossfire USB adapter for FPV

Some time ago I built an aliexpress-part-based FPV quad and started flying around.

Well… A few days later I had to buy the same parts again. Due to winds I totally lost control and the drone went out of range just while I hit the throttle stick hard. It elevated beyond clouds and was gone and never to find again :)

Then I also bought a great remote control, the TBS Tango 2, a quite compact but feature rich remote for quads, so I can train flying skills in a simulator, before losing the next quad. While the Tango 2 is also capable of proper joystick emulation, it always felt a bit weird to have the cable connected, which is always a bit too short, no matter which length your USB-C cable has.

So I decided to pick a small DigiSpark with its neat ATtiny85 plus USB emulation and attach a Crossfire nano receiver and just implement that simple CRSF protocol and use the DigiSpark Joystick emulation example code.

Yeah, wishful thinking.

When I realized that the ATtiny85 has no hardware UART and software bitbanging looked unfeasible in parallel with USB bitbanging, I decided to do a first step using the PPM protocol. The Crossfire receiver allows to output this PWM-like protocol with all its downsides. Noise, latency and resolution are not as good as with crossfire.

Implementing PPM reading was not soooo hard, just a bit glitchy due to noise and USB interrupts inbetween. Those capture timers are also not the highes resolution you can have (i am used to Infineon TriCore Aurix and ARM) but hey, 8 bits resolution should be enough.


No. I could only use less than 7 bits of range due to the timer frequency. Either handle timer overflows and use a higher clock frequency or be happy with the 7 bits. Okay, another day gone, and realizing that the timer overflow interrupts delayed due to USB interrupts cause some dropouts of the signal, giving a really “unsteady flight feeling”. Small, barely noticeable but still present hickups in a linear movement plus the noise you always get when sampling PWMs.

To give USB interrupts a higher priority while allowing timer interrupts, I bridged PB2 and PB3 and reconfigured the USB interrupt to trigger on INT0 (external interrupt) instead of PCINT0 (pin change).

Still, I had a working PPM to USB adapter, for virtually any recent FPV quad Tx system.

I want more!

Nah, let’s focus our initial plan – interpreting the CRSF protocol with its 420 kBaud. We get into the field of playing with nops to get just the right timing. But I had the feeling it could work – I just have to make sure that the USB interrupt and the CRSF bitbanging are not interfering.

Date i 
PicoScope 6 Beta 
Data - 
8,001 kS 
Aktueller Puffer 
2474 us 
1.086 us 
UART (RS-232, RS-422, RS-48S) 
- Ch 8 
Serielle Entschlüsselung 
Verknüpfung Exportieren 
Stop Bit - 
- 42,693 
03 58 
Such en 
. (RS-232, RS-422, RS-485) Ch8 
Läuft Trigger Wiederholen 
3.36 us 
Lineale Notizen
Here the blue spikes show the sampling position of the red CRSF Rx signal

Using the LED pin (blue trace) and a scope I started to get just the right position where to sample the CRSF data line (red trace). If a CRSF packet is detected by its 0xC8 sync byte, I disable the USB interrupt for the duration of the packet, causing maybe one USB slot to be missed. But this seems fine from what I’ve seen.

One thing still riddles me: According to the ATtiny85 documentation, we only should have 4 cycles plus a few instructions latency and should end up with a microsecond. For some unclear reason the PCINT0 handler fires with a latency of about 2.5 microseconds and thus gets executed after start bit and just right for the first data bit.
But it works just fine, as we don’t need the start bit anyway.

Sticks and buttons

After also extending the DigiJoystick driver, the device properly sent 14 “analog stick” 8 bit values (CRSF channels 0 to 3, 10 and 11, a few rx link packet datas) plus 16 buttons. The buttons were calculated from the CRSF channels 4 to 9, giving a total of 12 used buttons and leaving 4 buttons free.

The button calculation is required as crossfire and other protocols sends button as “stick” values with a certain range, depending on the number of positions the button has. Currently there is only 3-way switches implemented, but that could be changed in the source. Alternatively I could simply pass all 12 channels through. Maybe I’ll change that.

The firmware

Download one of these files, remove the .txt extension and flash it using your Digispark uploader (micronucleus). The first file is passing channels 0-3 and the rest comes from link statistics, the second one routes all CRSF channels 0-11 through to the USB HID Joystick.

You can find the sources here: https://github.com/g3gg0/DigiJoystick_FPV

EDIT: the sources on github have some updates which make the USB connection more stable.

This is a library with two examples – a CRSF one and a PPM one Place it into your <Documents>\Arduino\libraries\ folder and use the provided examples.
Hint: PPM_Joystick may work or not – didn’t check it after refactoring for CRSF_Joystick.

Documentation is still missing, but I will update it in future. Maybe :)

Final thoughts

In the end it was a bit more lengthy than expected. CRSF with its high bitrate is not simple to implement along with USB, if you do not dig a bit deeper into timings and fine tune sampling points using nops. To be honest, my code is not really clean. Should have switched to assembler to a larger extent, but I am not used to atmel asm and wanted to get it working fast :)

Also the hardcoded USB HID descriptor lengths of V-USB did cost me time to find out why my device was often not enumerating. Wireshark for the win, I could at least find out that the last message was the HID descriptor and thus it had to be defective.

So i can now enjoy crossfire as it was meant to be: with a untethered remote and (sometimes) wearing the FPV goggles, feeding the VTX with the output of a 12€ HDMI->FBAS converter. Using this setup, I could even stand outside under the trees, flying a (virtual) drone in a more realistic environment.

[MES-WiFi] Bringing a Windhager pellet heater online

Since we own our house with a pellet heating system, it does its job day for day and delivers the warmth you’d expect from it. It rarely fails. Well, rarely. Except when I forget to refill the pellet container which has to be done every 3 to 14 days.

Unfortunately the system is closed and does not offer a simple solution of getting it online to notify me about any interruption, be it a failing component or just a failing human pellet refill bot. I could just upgrade the system to some automatic feeding system, but it would neither be the cheapest solution nor easily installed.

That was the reason for me to look a bit deeper into its guts and what is happening in it and how I could get this thing tell me whats happening. The schematics that come with the system quickly revealed some kind of local bus called LON which later confirmed speaking LONTalk from the 90’s on a RS485 physical layer.

And the best about this, the system has three slots where you can place so called MES-modules, like the UML C1 or others. And only two of those three slots were used in my installation.

Fig. 88 
Y 14 
MESPLUS Heizkreismodul 
XI /X2 
Spannungsversorgung +12 VDC 
S annun sversor un -Masse GND 
nicht belegt 
LON Masse GND 
LON Data + 
LON Data - 
eBus + Signal (20 - 24 VDC) 
eBus - Masse GND 
nicht belegt 
Fühler -Masse GND 
Motormischer - Zu 
Motormischer - Auf 
Snapshot from the MES plugin card system of Windhager heater systems.
Not exactly my revision, but the important signals are the same. [Source, Pg. 48]

Measuring the physical signal confirms the guesswork and gives a signal with 78 kHz, which is biphase manchester coded and results in 38 kbit/s. There are usually 8 or 9 start bits (ones) and then the payload as specified in the LONTalk protocol specification.

A scope trace of the LON bus lines

While the specification defines all packet formats (APDU, TPDU, SPDU, NPDU, PPDU, …) addressing formats etc. this is not enough to get data out of the system. You have to know about the NV (Network Variable) IDs and their meaning, as well as the NM request codes to fetch certain values.

Thus I started with a ESP32 board, directly interfacing the LON bus and dumping all traffic via UDP to my LAN. Better than buying an off-the-shelf solution if you don’t know how far this project will get.

The first revision of MES-Wifi in action. Forgive me the vertical video syndrome, but better a vertical video than a dropped phone.

The MES-WiFi v1.0, equipped with a ESP32-WROVER module with PSRAM, was my first iteration along with a MP1584 for 5V and a AMS1117 for 3.3V. This was the first step to get things started, but not exactly the most efficient setup component-wise. USB and thus the CP2104 shall not be missed. For snooping the LON bus, a MAX1487 was chosen. The first revision had two of them, as I wanted to set up a test bench for stress testing the software by simultaneously forging and parsing packets.

Speaking of packets, the LON packets received from the bus using the first PCB revision were sent to a broadcast address on my LAN and captured using Wireshark. Step by step I built a LUA dissector that decoded the packets into readable form as shown below. A helpful person from mikrocontroller.net gave me a lot of information about similar modules that was neccessary to find out the IDs on the system I had.

After the decoding was done far enough, the Arduino based ESP32 firmware was extended to not just “sniff” the broadcast messages seen above, but also actively ask some other Network Variables within the PMX main control unit and the other UML modules. You can see this REQUEST/RESPONSE pair in the screenshot above. All this information is not only sent as “raw” packets to my LAN, but also interpreted and fed to a really small MQTT broker on my server. Warnings, like low temperatures or an empty pellet reservoir, get pushed to my phone using pushingbox.

You can find my firmware on github.

MQTT is boring. Feed data to grafana to make it visually a bit more attractive.

When cleaning up the PCB – which is now at revision v1.3 – I changed quite a few things. Instead of a large WROVER module, I used the smaller WROOM without the PSRAM that I never used. Also the inefficient MP1584/AMS1117 pair was replaced with TPS62163/TPS62291. WSON soldering with mm-scale components, yeah! I also added some smaller relays to drive some circulation valves. Somewhen :)

Finally I routed some IOs to the APP+ and APP- lines where I will attach the already installed 1-Wire temperature sensors measuring pipe temperatures.

You can get the schematics and the layout on EasyEDA.

The third and last revision of the MES-WiFi board with an antenna attached
Shot with a more artistic touch using a proper DSLR

There is also a 3D printable housing on OnShape for the module, which reveals that I also planned to add an ePaper display, but did not find enough time to continue working on that.

The revision v1.2 was running half a year without any interruption, so I am optimistic that the v1.3 will also do a great job.

Btw – I really love how the JLCPCB blue looks like. I can absolutely recommend it.
The red is also nice, but it misses a bit of this professional touch. If you are one of the brave ones, choose the black. You will never be able to trace your … uh … traces beneath this matte black finish.

The very first revision which was just for mechanical tests and component testing.

So this was the journey of building a 25 € cheap WiFi extension module for a windhager pellet heating system.

[ESP32] LegoRemote v1.2 – a BLE/WiFi remote control for motors using a Steam Controller

Some years ago when I was young, I always wanted to have technic bricks to build some cool stuff with it. Self driving tanks, rotating cannons and shooting smaller bricks around. But back then I had no technic.

Well, not even bricks. Just a friend whose brother had this expensive stuff.

Now I have a son in this age and well… he builds tanks that… stupidly drive until they crash into a wall :)
Time for some ESP32 and soldering wizardry to build a proper REMOTE CONTROL!

No no, I know there are some remote controls around there. But none fits really well. Either 80’s infrared or bulky. Or too expensive.
And most important – none of them of which I can say to my son: “I built it for you!

So lets build a new, better one to rule them all!




  • ESP32-WROOM-32D  |  WiFi, BLE and enough processing power and IOs to control two motors and a LED light
  • DRV8833  |  dual H-bridge for enough power to drive the motors
  • TPS62162  |  step down for up to 17V input voltage, and also for having fun soldering the 2×2 mm WSON-8 package
  • CP2104  |  just for programming the ESP32
  • Power functions connectors to connect the motors and lights.
    Will cut the wire and solder them to the bottom side and glue the brick/connector to the top side

All this in a rather small footprint, here as shown in EasyEDAs 3D view:

The wire you can see on the PCB photo at the top of the article is not to patch some bug, but to feed the USBs 5V to the power supply. It will probably not be enough to power a motor, but unfortunately the connectors from china didn’t arrive yet. So I was testing with LEDs only in the first place. For the photo I just placed the motor connector losely onto the PCB without function.

To see what the motor driver does, the v1.1 had (unlike the v1.2 now on EasyEDA) no LEDs designed onto the PCB, so I soldered two antiparallel diodes to the outputs so I can see whats happening. If you look closely in the video you can see the two 0603 diode pairs alternating, which are indicating forward/backward motion.


For the control I first wanted to build an extra PCB with buttons and another ESP32, a classic remote control.

But I somewhen remembered that the Steam Controllers have a BLE mode. So sat down and a few hours later I was able to receive packets from the controller :D
Basically you just search for a HID device that announces as “SteamController” and connect to it. Then just use some undocumented Valve service and some undocumented command with it to enable packet transmission.

Then there was some also undocumented report format (is this even a HID like report? didn’t make sense to me) which I reverse engineered manually.

After a hour or so, all flags and values made sense and I was able to blink LEDs using a Steam controller and an ESP32. ¯\_(ツ)_/¯
I will upload the sources and update this post later.


Project files:

v1.0: “first try”
– first revision, but had chosen the wrong voltage regulator. TPS62291 only goes up to 6V. had few projects in parallel and simply forgot that this device had to cope with 9V

v1.1: “good enough”
– the version you see in the videos with everything working fine

v1.2: “final”
– added indication LEDs to driver output and optimized layout and PCB size


Here a short video showing the connection phase (1-3 seconds after switching power on) and controlling the motor outputs. The LEGO connector for the headlight was not populated there.
It will go to the empty space marked with the white rectangle right next to the other connectors.


Meanwhile my son uses this device on a regular basis to control his tank like constructions.

During his stress tests, I faced only one issue – the driving mode of the motor I expected to work best – fast decay – caused some serious speed drop after a few seconds of driving.
So I changed to code to use the slow decay mode

I will have to dig deeper into what the DRV exactly does and try to understand why the motor first runs at high speed and gradually slows down after maybe 10 seconds. Maybe MOSFETs are heating up and their resistance is raising a bit too much. Not sure.
But for now this is bypassed by using slow decay.


Maybe this is some inspiration to other people what you can use your arduino playground for without too much effort and get your kids interested in electronics also.


[3D-Print] Geeetech prusa i3 – improvements

Last year i bought a cheap chinese prusa i3 clone for €270 including VAT and shipping which performs very good overall.

After assembling it – which took me about 8 hours – its print results was.. well.. quite okay.
As expected, because reading forums and blogs already explained Z wobble to be a serious problem with this printer.

So the first thing I’ve printed the already posted “Z wobble fix” which came out wobbly of course, but works well :)
I’ve also decoupled the Z axis from the frame by (gently) drilling a bigger hole in the top part of the printer so it can move freely.


anti wobble z axis screw

Then I also added a ball bearing (608Z) “decoupler” to the top end of the thread so it is not moving too much and is guided a bit better.

Aside of that, I replaced the too small Z axis rods with a proper one and the Z axis threaded rod with a TR8 trapezoid one.

After some printing, i wondered why the MOSFETs get so hot. They used P55NF06 with a quite okay R_DS(on) of 0.018 Ohms. But more problematic is the V_GS which is with 5V just barely above the threshold voltage.
This causes an resistance of far more than the 0.018 Ohms which you will only get at a V_GS of 10V. The result – the MOSFETs get hot.


The P55NF06 is at it’s minimum switching voltage when driving it with 5V on gate

Solution? Buy some IRFB7430 which have only 0.001 Ohms R_DS(on) at 10V and ~0.013 Ohms at 5V.

Hard to see, but the second curve from bottom (4.8V) tells us that when driving a 20A load, the voltage drop at the MOSFET will be ~0.25 Volts. Thats approx. 0.013 Ohms internal resistance

As these parts are a bit pricey, I’ve chosen to take the cheaper IRF2804 for the hotends. They still provide a low resistance with ~0.015 Ohms and are good enough for hotend heaters.
Probably also good enough for the heated bed :)


before I was able to swap them, a really bad thing happened – my printer heated permanently until it smoked and i powered it off.

First let’s see how the GT2560 board looks like:


Now when looking at the heat sink of the MOSFET, one will realize that they have

a) a sharp corner
b) are electrically connected to the MOSFET with a metal screw and without any insulation pad
c) (no, you can’t see that on the photos) there is a PCB trace *exactly* below the sharp corner of the heat sink, prone to get scratched


Please ignore the insulation tape I placed there just as a quick hack to get things done.

After a while the PCB was scratched and the heatsink made a short circuit with a trace on PCB, permanently heating the hotend.
Yay. worst case. Even marlin’s “thermal runaway”-protection couldn’t do anything as they are short-circuited on board.
So the result was an overheated hotend with damaged PTFE inliner, died MOSFETs and me, a bit scared of this really bad engineering.

No fire tho, but there was smoke at least.

Okay to be honest, when paying 270€ for a whole box full stuff you can build a printer of, don’t be surprised if that is not at IBM quality level.
But there is a difference if a connector is of cheap quality or a rod is bent, or if a PCB designer doesn’t realize that a sharp-edged aluminum block just micrometers above a PCB trace that might cause malfunction and finally a fire is a frickin’ bad idea.

a) now really buy the MOSFETs i already had chosen
b) along with insulation pads and plastic screws
c) and put some {glue, hot glue, epoxy, cyanacrylate, whatever} on the PCB below the heat sinks (i used a special solder mask pen)
d) put a smoke detector above the printer

Recommendation to all users of a GT2560: ALSO FIX THIS ISSUE, it might cause the printer to catch fire.

The wonderful side effect: you don’t need the devilish loud mainboard fan anymore.

After these changes, my printer works like a charm :)
It happened in october ’16 and geeetech sent me a second mainboard just days later so I could fix the other one properly.
(thanks for this really fast response and good service)


I hope this warning reaches out to all “cheap chinese printer” users out there.

[3D-Print] Custom geeetech i3 printhead

[Update: 04/2018]

Summer ’16 I bought a prusa i3 clone from geeetech, a cheap chinese printer manufacturer.
With 270€, including VAT and shipping, the DIY-kit is quite cheap and 8 hours later i had a prusa i3 clone that prints “quite okay”.

[more about preventing your house burning down here]

This printer has a direct drive, bowdenless MK8-style extruder. While it prints really good, I wanted to have more flexibility.

So I started to design my own printhead with these constraints:

  • exchangeable modules for: E3D v6 hotend, dual E3D v6 hotend, E3D Cyclops, plotter, laser cutter (more laserz, even more), …
  • modules should be easy to change (no soldering, no screws, no levers)
  • no rewiring when changing a module
  • lightweight design
  • easy to service

These lead me to a three-component setup, I designed in OnShape.


a) The X-Axis carrier.
Into this thing go all the screws. The SC8UU housings for the LM8UU linear bearings get mounted there, the belt attachment and the other printhead parts are mounted onto it.


The X-Plate with a lot of holes for M4 screws in all variants

The plate just connects the three ball bearings and provides a stable mount for the more complex baseplate with magnets, contacts and stuff.


b) The thing, I call “Baseplate”
It is the electrical connector that holds the printhead in place with magnetic force.


The Baseplate with Ingun pogo pins and neodymium magnets

The Baseplate holds 30 pogo pins “GKS-967” from Ingun and each two “Q-10-04-02-N” and “Q-15-15-03-N” neodymium magnets from supermagnete.de.
While the magnets are quire cheap to get, the pogo pins and the counterparts were approx. 80€ , which is about 30% of the printer’s initial cost.
Not quite a el-cheapo design, but worth the money. Cost can for sure be reduced, but the first designs are not meant for this kind of experiments.

The layout of the connector is designed so printheads can be plugged in either of the two connectors, having the same pinout on both.


This EAGLE PCB was later redrawn in Altium by a friend who ordered a larger panel anyway

The pinout for every module has 9 “center” pins that are for the important lines – heater, thermistor and fan supply, where the high current traces and pogo pins are used in parallel.
The “outer” pins are for the (extruder) stepper motors and some spares for future use.
But there is a small caveat though – the fan pins are not equal. While one of them is PWM controlled, the other is always-on only. I can’t change this, as there is only one PWM capable output on the GT2560 mainboard.

Sure, could modify the board and add a MOSFET, but thats another story.

Also made a proper mount for the original MK8 extruder that gets snapped into the base plate.


The pads you can see in the upper part of the PCB are the solder pads for the flexband I harvested from Audi/Volkswagen steering angle sensors.
One of the next steps is to use five flexbands and solder them onto the PCBs. I have the same PCB twice, one in the baseplate and one next to the printer’s main PCB.


No idea where to get such flexbands from officially – scrapyard?


c) The printhead modules

The print heads are designed for various purposes, mainly for FFF print heads, but also some other tools.
Meanwhile I designed various print heads, some are listed here:

Mk8 extruder


One module – a classic E3D v6 – plugged into the baseplate. Slim enough for two in parallel.


Two E3D v6 modules and a E3D Cyclops above them for size comparison

An experiment with a BLTouch clone made me realize that strong neodymium magnets are not good for its sensitive GMR sensor (BR, cpt. obvious)

Very valuable helper if you have wedding invitation cards – a plotter :)


Here a video of the current status, feels quite stable thanks to dasfilament PETG which I can absolutely recommend.

This video shows how well the printheads snap into the base plate. The four neodymium magnets leave no play and deliver a solid mechanical feeling.


Here photos of the printhead with flex bands attached

The printhead system prior to mounting. In front there is a spare PCB for the spring loaded Ingun fixtures.


Here the print head system after mounting. The status LEDs (WS2812) which glow dependent on printhead status and temperature are controlled by a separate digispark on the other end.


This photo shows some of my printheads in real hardware:

1. MK8 Extruder
2. Laser 450nm, 1.6W
3. ball pen plotter head
4. E3D  v6 hotend for bowden
5. micrometer dial indicator


After more than a year of use, I am so statisfied with the result. Swapping from the Mk8 extruder to the laser head within 5 seconds and then lasering a piece of wood with ease makes this piece of hardware much more usable.
So my printer is not only for squeezing hot plastic through a brass nozzle anymore, but also for engraving wood using a mill.
Sure, milling with that acrylic frame and wobbly axis will not give the best results (not to speak about the dust this hardware isn’t designed for), but are at least a funny experiment.

The best decision was to use the flex cable, which is not easy to get unfortunately. It really is a good feeling when you don’t have to fear broken cables.
No need for a drag chain, no sloppy cables, helping to reduce the risk of serious fail that could burn your house down.

[ESP8266] CAN library for Arduino

Recently I needed a CAN <-> WiFi router at work. As I didn’t want to use a several thousand euro hardware that sucks a few amps at 12V,
I decided to implement that using the ESP8266 and a CAN transceiver.

The current setup

The current setup

Sending CAN messages with the ESP8266, decoded with PicoScope.

Sending CAN messages with the ESP8266, decoded with PicoScope.

Well, it works quite stable, just a few things here and there. E.g. I cannot ACK messages as I am using I2S to sample the CAN Rx path.
Also the I2S seems not very stable and goes crazy every 20 minutes at 1.5 MHz sampling rate, so that I implemented a detection and restart procedure.

Here is a picture of the ISR execution time – it is quite stable at 725µs – 765µs

Tracing the ISR execution time using a GPIO. When the ISR is active, the signal is high.

Tracing the ISR execution time using a GPIO. When the ISR is active, the signal is high.

But aside of these minor issues, the ESP8266 is now ready for its use case – routing between WiFi and CAN :)

Here is the library: GitHub – g3gg0/esp8266can

[UM2] Laser Addon – Part 2

As previously described, I made a simple adapter to mount a 405nm blue laser diode to expose PCBs. There are for sure other methods that seem simpler, like separating the laser toner from a print or printing on transparent film, but our goal was to make it a “just press start” method using things we had lying around. No “process” that may vary with laser toner properties, chemicals, time involved. Just place it in printer and get a perfectly exposed PCB. Hopefully.

The first print of the laser mount was quite usable as you can see. Just the image is a bit blurry. Module sits tight, mount sits tight, looks proper. Just the “fixation” tab you can see on the 3D sketch was a bit clumsy and I just broke it off.

LaserMount LaserMount2

I searched for some EAGLE script that will output already usable G-code and was successful with PCB-GCODE. Some minor modifications were to do, then it was able to output G-code that switches the fan output whenever a line was to draw and so switch the laser on when exposing and off when moving across the PCB.

Some very careful tests of that G-code later, it showed that all is doing well. I set up the code to just home the Z axis and use the X/Y positions of the current head position as zero. This allows me to move the head manually to the board’s origin before starting, as I just fixate it with scotch tape for now. Played a bit to find the right Z position where the focus of the lens is best. In my case 12.5 mm above the glass plate. Just wrote that into the PCB-GCODE setup dialog, added PCB thickness and ready was the first G-code we ran on a real photosensitive PCB.

After exposing the first PCB, my colleague developed and etched it. The first try was a quite okay. A bit overexposed, but the technique seems to work. Fan output is driving the laser module’s enable pin correctly and it’s perfectly (over-)exposing areas.


This exposure was using 100mm/min at ~80mA laser power. Next time less current.

Also added an aperture plug to reduce the reflections the laser is causing in the metal on its way down. Now the spot is very clear and small, no scattered light. Just some reflections of the light coming back from the PCB, hitting the metal fan shroud and reflecting back to the PCB.


While experimenting with the laser output power, I used a Keithley SourceMeter 2400 for providing stable 3.0V. Using the Ultimaker 2 menu, I played with the fan speed. What I didn’t think of, was the dynamic behavior of both SourceMeter and the constant current driver of the laser module. A few “fan speed” sweeps up and down later, the laser started to blink and go off.

Shock. Did the laser module die? Power consumption went up from ~580mW to > 1W and the SourceMeter limited the current. A short test by driving the laser module directly, showed that the laser itself is still functional. Phew. So the constant current module died. I am quite sure that the (measured) 490Hz PWM of the UM2’s fan output caused the whole setup to oscillate and the RT8285B to catastrophically die.

Well, picked up a AQY221R2V solid state relay from our laboratory and used this to drive the laser module directly with the PWM output. For now I will not use the PWM to control its power, just use 0% and 100% :)

After doing some calculations, based on experiences of other people on the net, some estimations of the laser module’s power (using SLD3132VF as reference) I came to some approximated values of movement speed and laser current. Basically I am trying to get the same amount of energy to the ~50µm focused spot as some people who had success exposing with a 300W halogen lamp all at once. Interesting number: with the laser we have about 8 MW per m² energy concentration.

Here are the first tries with varying speed and current to find out where the sweet spot lies.


The result with 1000mm/min at 40mA laser current is really overwhelming for such a cheapo 2,50€ extra cost setup.


So this was quite successful. A bit more fine tuning to find where the limits are, e.g. the minimum laser current, the perfect focus plane level, the minimum line width and a bit of tool-tuning to get the drill holes laser-marked too. Then design and print some mounting frame to repeatedly hit the same zero offset when fixating PCBs and thus be able to expose two-sided PCBs.

The cool thing is – I just poweroff the laser and am able to print ABS just the minute after, without any de-/setup procedure.
Thanks to my colleague Franz for his support, especially for etching the PCBs.

Next stop: drilling ;)

Update: And here a close-up zoom with a macro lens:


And currently i am uploading a video that shows the setup being in action with a dry-run on paper.