Reverse Engineering Quick 6101A2 Control Protocol - Part 1

Back to Home

This post covers my notes and findings from reverse engineering the Quick 6101A2 fume extractor's command protocol to integrate it into my other tools and control it without the original controller. In part 1, we'll focus on analyzing and parsing the protocol, mapping the sensor/control registers, then build a simple terminal tool to monitor the fume extractor.

For a long time I had one of those cheap desktop fume extractors. They ship with a coarse "filter" that feels more like a fume mixer than a device that actually removes hazardous fumes. For light use they're fine, but lately I've been doing more BGA reballing and rework, so I decided to upgrade to a proper HEPA unit and went with Quick's 6101A2. I'd already been happy with the build quality of their 861DW hot-air station as well, and the price is hobbyist-friendly compared to JBC's models, for example.

Quick 6101A2 Quick 6101A2 main and controller unit.1

The fume extractor consists of the main unit that has the filters, motor, etc., and a wired remote controller that can turn the device on/off, change settings, and show the airflow, filter stats, etc. It also has a connector for external devices, like a soldering iron or a hot air station, so it can turn on automatically when you pick up the iron. Of course each manufacturer has their own proprietary port/protocol to interconnect these devices, so I cannot directly connect my JBC soldering station to my Quick 6101A2.

I decided to reverse engineer the communication protocol to allow me to build a proxy between my solder station and the fume extractor. The obvious starting point would be the external tool connector, but unfortunately I don't have any devices that support it at the moment. So I decided to look into the communication between the main unit and the controller. It wouldn't surprise me if the controller and the external tool speak the same protocol.

Quick 6101A2's connectors The connectors for the controller and an external tool.1

It's Always Serial

The first suspicious observation is that the controller is connected to the unit with a USB-A cable. Using USB for something this simple sounds unlikely, and I did not want to attach Cynthion straight away in case it wasn't actually USB and I'd end up blowing the USB analyzer. So I probed the data pins with an oscilloscope instead. As expected, the signals look like RS-232, not USB, and they decode nicely into serial data at 19200 baud.

Oscilloscope capture of the controller's TX line The decoded frame from the controller unit (19200; 8N1).

The connector has this pinout:

USB-A PinSignalNotes
15VThe base unit provides power
2RS-232Controller's TX (D- in real USB)
3RS-232Controller's RX (D+ in real USB)
4GND

Before diving into figuring out what those bytes mean, I wanted a longer capture. An oscilloscope isn't ideal for that. The simplest option would be a USB-RS232 adapter, but we'd need two to capture TX and RX and then sync them, which feels cumbersome. A logic analyzer is a better fit. Because this is real RS-232 (±6 V in this case), the signal has to be level-shifted to TTL to avoid blowing my logic analyzer. I had a MAX233 (multichannel RS-232 driver/receiver) in my parts bin, which makes the conversion easy since it doesn't need external capacitors or extra circuitry.

MAX233 tapping circuit MAX233 Pin Configuration and Typical Operating Circuit.2

The typical operating circuit is more than enough to tap into an existing RS-232 line as shown above. Here we connect Quick's TX and RX lines into RS-232 inputs (pins 4 and 19), and the logic analyzer to TTL outputs (pins 3 and 20). That setup lets us capture a nice long stretch of communication between the controller and the unit.

Quick 6101A2 Captured traffic between the controller and base unit in Logic 2.

Making Sense of the Bytes

Now we have a long list of meaningless bytes. Let's try to find some patterns in it. The first observation is that the controller always starts the communication by sending a frame to the unit, and the unit responds by sending one frame back. When idle (not touching the controller), it keeps sending the same 8-byte ping/status request, and the unit responds with 53 bytes. Most likely this is the controller querying the unit status, since live data such as airflow and motor speed is shown on the controller's screen.

If we take a close look at the request bytes:

0x02 0x03 0x00 0x00 0x00 0x18 0x45 0xF3

A good guess is that the last two bytes are some kind of checksum. Usually engineers don't want to reinvent the wheel, so it makes sense to try a CRC before diving too deeply into this. crccalc.com is a great tool to quickly calculate the common CRC variants at once, and indeed we have a match. CRC-16/MODBUS outputs 0xF345, which is encoded as little-endian in the request.

As mentioned, you don't want to reinvent the wheel. Since the checksum uses the MODBUS variant of CRC-16, it's a reasonable guess that the application layer would be Modbus RTU as well. Modbus RTU is a simple and common industrial protocol used to let devices talk to each other over serial links such as RS-232 or RS-485.

The Modbus specification defines the general frame structure as a Protocol Data Unit (PDU), independent of the underlying transport. The PDU contains the Function Code (what action the server should perform) and the data (parameters for that action). The PDU is carried inside an Application Data Unit (ADU), which adds fields such as the server address and CRC.

General MODBUS frame General MODBUS frame.3

For RS-232/RS-485, the ADU is defined as

<Server address (1 byte)> <PDU (max 253 bytes)> <CRC (2 bytes)>

In addition, the specification describes these function codes

Public Function Code Definition MODBUS Function Code Definitions.3

Our request frame matches the above definition:

0x02 0x03 0x00 0x00 0x00 0x18 0x45 0xF3
<--> <--> <-----------------> <------->
 |    |    |                   |----- CRC (ADU)
 |    |    |------------------------- Data
 |    |------------------------------ Function Code
 |----------------------------------- Address (ADU)

The Address field in the ADU seems to always be 0x02. Based on the above function code table, 0x03 maps to Read Holding Registers, which is used to read the contents of multiple registers. That matches our hypothesis. The arguments for Read Holding Registers are defined as follows:

Starting Address:      2 bytes
Quantity of Registers: 2 bytes

The register-read response frame has this structure, with each register packed into two bytes.

Function Code:   1 byte
Data length:     1 byte
Register values: <Data length> bytes

Using those definitions, the data in the read request decodes into:

Starting Address:      0x0000
Quantity of Registers: 0x0018

and therefore the response size should be:

PDU = 1 + 1 + 2*0x18 = 50 bytes
ADU = 1 + PDU + 2    = 53 bytes

That matches as well. At this point, we can be pretty sure we're dealing with Modbus RTU. We can treat the link as Modbus RTU and start interpreting the register data rather than raw bytes. One parsed Read Holding Registers response decodes into these values, which don't look random to me:

reg: 0x0000 = 0x0001 (1)
reg: 0x0001 = 0x0023 (35)
reg: 0x0002 = 0x0000 (0)
reg: 0x0003 = 0x0000 (0)
reg: 0x0004 = 0x0000 (0)
reg: 0x0005 = 0x0000 (0)
reg: 0x0006 = 0x00C8 (200)
reg: 0x0007 = 0x04B0 (1200)
reg: 0x0008 = 0x0960 (2400)
reg: 0x0009 = 0x0000 (0)
reg: 0x000A = 0x0001 (1)
reg: 0x000B = 0x0000 (0)
reg: 0x000C = 0x9600 (38400)
reg: 0x000D = 0x0000 (0)
reg: 0x000E = 0x0DFC (3580)
reg: 0x000F = 0x004B (75)
reg: 0x0010 = 0x028A (650)
reg: 0x0011 = 0x02EE (750)
reg: 0x0012 = 0x0000 (0)
reg: 0x0013 = 0x192A (6442)
reg: 0x0014 = 0x0023 (35)
reg: 0x0015 = 0x0000 (0)
reg: 0x0016 = 0x0000 (0)
reg: 0x0017 = 0x0000 (0)

Making Sense of the Registers

There is definitely less chaos now, but we're still missing the meaning of these individual registers. Figuring them out should be straightforward: some values already map directly to what the controller shows, such as target airflow, and in addition, we can use the device and see which registers change (turning it on/off, changing settings, etc.). With that approach, I came up with this mapping. The unknown registers stayed constant across all captures and state changes in this analysis.

AddressNameUnitsNotes
0x0000State-0 = OFF, 1 = ON
0x0001Target flowm3/hTarget airflow
0x0002--Observed 0
0x0003--Observed 0
0x0004--Observed 0
0x0005--Observed 0
0x0006P-Filter limitkm3Maximum airflow rate
0x0007M-Filter limitkm3Maximum airflow rate
0x0008C-Filter limitkm3Maximum airflow rate
0x0009--Observed 0
0x000ACommunication address-Modbus device address
0x000B--Observed 0
0x000CBaud rate-Communication baud rate
0x000DBeeper-Beeper state
0x000ESpeedrpmFan speed
0x000FTube diametermmTube diameter
0x0010--Observed 650
0x0011--Observed 750
0x0012--Observed 0
0x0013--Observed 6442
0x0014Real flowm3/hMeasured airflow
0x0015--Observed 0
0x0016--Observed 0
0x0017--Observed 0

The table still has many unknown registers, most of which are zero. Three of the zero-valued registers likely belong to the filters' airflow counters. However, my counters still show zeros, so it's not possible to map them yet. In addition, the user manual defines three different error states. I have no idea whether there are individual registers for those errors, or just a single one.

Changing the State

There isn't much to setting the registers. The controller uses the Write Single Register (0x06) function to write a 16-bit value into a specific register. The request has these arguments:

Register Address: 2 bytes
Register Value:   2 bytes

The response is an echo of the request. For example, setting the target airflow to 36 m3/h (0x0024) looks like.

0x02 0x06 0x00 0x01 0x00 0x24 0xD8 0x22
<--> <--> <-------> <-------> <------->
 |    |    |         |         |----- CRC (ADU)
 |    |    |         |--------------- Register Value
 |    |    |------------------------- Register Address
 |    |------------------------------ Function Code
 |----------------------------------- Address (ADU)

Similarly, toggling the power uses register 0x0000 with 0x0001 for ON and 0x0000 for OFF.

PoC || GTFO

Let's put the above pieces together and create a PoC terminal monitor to interact with the fume extractor. The modbus-rtu crate actually takes the heavy lifting by implementing the Modbus RTU protocol for us. So, our job is pretty much just to parse the correct registers and show them in the UI. The monitor shows power state, target flow, real flow, fan speed, and register values in real time. You can find the source on GitHub.

Quick Monitor TUI Terminal Monitor for Quick 6101A2.

Future Work

This is the end of part 1 of reverse engineering the Quick 6101A2. Figuring out the controller protocol gives us a solid foothold to understand the device internals. Next I want to take a closer look at the external tool connection and try to dump the main unit firmware to figuring out the purpose of the unknown registers. In addition, I also still need to build the proxy device so my soldering and hot-air stations can turn the fume extractor on automatically.

1

EleShop. Quick 6101A2