Wandering in embedded land, Midea 美的 aircon and Arduino

Part 1, Midea 美的 aircon protocol

I have been a user of Arduino's for a few years now, I use them to control my greenhouse (I grow orchids). This mean collecting data for various parameters (temperature, hygrometry, light) and actionning a collection of devices in reaction (fan, misting pump, fogging machine, a heater). The control part is actually done by an NSLU2 which also collects the data, export them as graph on the internet and allows me to manually jump in and take action if needed even if I'm far away using an ssh connection.

This setup has been working well for me for a few years but since our move to China I have had an airon installed in the greenhouse like in other parts of the home. And that's where I have a problem, this AC of brand Midea (very common home appliance brand in China) can only be controlled though a remote control. And until now that meant I had no way to automtate heating or cooling, which is perfectly unreasonnable :-)

After some googling the most useful reference I found about those is the Tom's Site page on building a remote adapter for those. It explained most parts of the protocol but not all of them, basically he stopped at the core of the interface but didn't went into details, for example didn't explained the commands encoding. The 3 things I really need are:

I don't really need full fan speed control, low speed is quite sufficient for the greenhouse.

Restarting the Arduino development

I hadn't touched the Arduino development environment for the last few years, and I remember it being a bit painful to set up at the time. With Fedora 17, things have changed, a simple

yum install arduino

and launching the arduino tool worked the first time, actually it asked me the permission to tweak groups to allow me as the current user to talk through the USB serial line to the Arduino. Once done, and logging in again everything worked perfectly, congratulation to the packagers, well done ! The only sowtware annoyance is that is often take a dozen seconds between the time an arduino is connnected or powered and when it appears in the ttyUSB? serial ports options in the UI, but that's probably not arduino's fault.

The arduino environment didn't really change in all those years, the two notable exception is the very long list of different boards supoorted now, and the fact that arduino code files are renamed from .pde to .ino !

Learning about the data emitted

The first thing needed was to double check the result from Tom with our own hardware, then learn about the protocol to be able to construct the commands above. To do this I hooked a IR receptor to the Arduino on digital pin 3, the graphic below show the logic, it's very simple:

Then I loaded a modified (for IRpin 3) version of Walter Anderson's IRanalyzer.pde onto the Arduino and started firing the aircon remote control at the receiver and looked at the result: total garbage ! Whatever the key pressed the output had no structure and actually looked as random as input without any key being pressed :-\

It took me a couple of hours of tweaking to find out that the metal enclosure of the receiver had to be grounded too, the GRD pin wasn't connected, and not doing so led to random result !

Once that fixed, the data read by the Arduino started to make some sense and it was looking like the protocol was indeed the same as the one described in Tom's site.

The key to the understanding of how the remote work is that it encodes a digital input (3 Bytes for Midea AC protocol) as a set of 0 and 1 patterns each of them being defined by a 0 analogic duration followed by a short anlogic pulse at 38KHz to encode 0, or a long analogic pulse at 38KHz to encode 1:

Each T delay correspond to 21 pulses on a 38KHz signal, this is then a variable lenght encoding

As I was making progresses on the recognition of the patterns sent by the aircon I was modifying the program to give a more synthetic view of the resulting received frames, you can use my own IRanalyzer.ino it is exended to allow recording of a variable number of transition, detects the start transition as a 3-4 ms up, and the end as a 3-4 ms down from the emitter, then show the transmitted data as bit field and hexadecimal bytes:

Waiting...
Bit stream detected: 102 transitions
D U 1011 0010 0100 1101 1001 1111 0110 0000 1011 0000 0100 1111 dUD Bit stream end : B2 4D 9F 60 B0 4F !
4484 4324 608 1572 604 472 596 1580 600 1580  !
Waiting...

So basically what we find here:

there is a few interesting things to note about this encoding:

The protocol decoding

Once the frame is being decoded properly, we are down to analyzing only 3 bytes of input per command. So I started pressing the buttons in various ways and record the emitted sequences:

Cool 24 fan level 3
1011 0010 0100 1101 0011 1111 1100 0000 0100 0000 1011 1111  B2 4D 3F C0 40 BF
Cool 24 fan level 1
1011 0010 0100 1101 1001 1111 0110 0000 0100 0000 1011 1111  B2 4D 9F 60 40 BF
Cool 20 fan level 1
1011 0010 0100 1101 1001 1111 0110 0000 0010 0000 1101 1111  B2 4D 9F 60 20 DF
Cool 19 fan level 1
1011 0010 0100 1101 1001 1111 0110 0000 0011 0000 1100 1111  B2 4D 9F 60 30 CF
Heat  18 fan level 1
1011 0010 0100 1101 1001 1111 0110 0000 0001 1100 1110 0011  B2 4D 9F 60 1C E3
Heat  17 fan level 1
1011 0010 0100 1101 1001 1111 0110 0000 0000 1100 1111 0011  B2 4D 9F 60 0C F3
Heat  29 fan level 1
1011 0010 0100 1101 1001 1111 0110 0000 1010 1100 0101 0011  B2 4D 9F 60 AC 53
Heat  30 fan level 1
1011 0010 0100 1101 1001 1111 0110 0000 1011 1100 0100 0011  B2 4D 9F 60 BC 43
Stop Heat  30 fan level 1
1011 0010 0100 1101 0111 1011 1000 0100 1110 0000 0001 1111  B2 4D 7B 84 E0 1F
Cool 28 fan 1
1011 0010 0100 1101 1001 1111 0110 0000 1000 0000 0111 1111  B2 4D 9F 60 80 7F
Stop Cool 28 fan 1
1011 0010 0100 1101 0111 1011 1000 0100 1110 0000 0001 1111  B2 4D 7B 84 E0 1F

The immediately obvious information is that the first byte is the constant 0xB2 as noted by Tom's Site. Another thing one can guess is that the command from the control is (in general) absolute, not relative to the current state of the AC, so repeated commands are idempotent, if it failed to catch one key, it will get a correct state if this is repeated, this just makes sense from an UI point of view ! After a bit of analysis and further testing the code for the 3 bytes seems to be:

[1011 0010] [ffff 1111] [ttttcccc]

Where tttt == temperature in Celcius encoded as following:

17: 0000, 18: 0001, 19: 0011, 20: 0010, 21: 0110,
22: 0111, 23: 0101, 24: 0100, 25: 1100, 26: 1101,
27: 1001, 28: 1000, 29: 1010, 30: 1011, off: 1110

I fail to see any logic in the encoding there, I dunno what the Midea guys were thinking when picking those values. What sucks is that the protocol seems to have a hardcoded range 17-30, while basically for the orchids I try to keep in the range 15-35, i.e. I will have to play with the sensors output to do the detection. Moreover my test is that even when asked to keep warm at 17, the AC will continue to heat until well above 19C, I can't trust it to be accurate, best is to keep the control and logic on our side !

cccc == command, 0000 to cool, 1100 to heat, 1000 for automatic selection and 1101 for the mode to remove moisture

Lastly ffff seems to be the fan control, 1001 for low speed, 0101 for medium speed, 0011 for high speed, 1011 automatic, and 1110 for off. There is also a mode which is about minimizing energy, useful at night, where the fan is quite slower than even the low speed, but i didn't yet understood how that actually work.

There is still 4 bytes left undeciphered, they could be related to 2 function that I don't use: a timer and oscilation of the air flow, I didn't try to dig, especially with a remote control and documentation in Chinese !

Last but not least: the stop command is 0xB2 0x7B 0xE0, it's the same whatever the current state might be.

At this point I was relatively confident I would be able to control the AC from an Arduino, using a relatively simple IR LED control, it ought to be a "simple matter of programming", right ?

part 2, Arduino turned remote control

Now with the Midea AC remote control being mostly deciphered, the next step is to emulate the remote, with an arduino since it's the system I use for that embedded greenhouse control. While waiting for my mail ordered IR LED (I didn't want to solder off one from my existing AC controllers), I started doing a bit of code and looking at the integration problems.

The hardware side

One of the challenge is that the Arduino system is already heavy packed, basically I use all the digital Input/Output except 5 (and 0 and 1 which are hooked to the serial support), and 2 of the 6 analog inputs, as the card already drives 2 SHT1x temp/humidity sensors, 2 light sensors, an home made 8 way relay board, and a small LCD display, there isn't much room left physically or in memory for more wires or code ! Fortunately driving a LED requires minimal resources, the schematic is trivial:

I actually used a 220 Ohms resistance since I didn't had a 100 Ohms one, the only effect is how far the signal may be received, really not a problem in my case. Also I initially hooked it on pin 5 which shouldn't had been a problem, and that's the free slot I have available on the Arduino

The software side

My thinking was: well I just need to recreate the same set of light patterns to emulate the remote control and that's done, sounds fairly simple and I started coding royines which would switch the led on or off for 1T, 3T and 4T durations. Thus the core of the code was like:

void emit_midea_start(void) {
    ir_down(T_8);
    ir_up(T_8);
}
void emit_midea_end(void) {
    ir_down(T_1);
    ir_up(T_8);
}
void emit_midea_byte(byte b) {
     int i;
     byte cur = b;

     for (i = 0;i < 8;i++) {
         ir_down(T_1);
         if (cur & 1)
             ir_up(T_3);
         else
             ir_up(T_1);
         cur >>= 1;
     }
     cur = ~b;
     for (i = 0;i < 8;i++) {
         ir_down(T_1);
         if (cur & 1)
             ir_up(T_3);
         else
             ir_up(T_1);
         cur >>= 1;
     }
}

where ir_up() and ir_down() were respectively activating or deactivating the pin 5 set as OUTPUT for the given duration defined as macros.

Playing with 2 arduinos simultaneously

Of course to test my code the simplest was to set up the new module on another arduino positioned in front of the Arduino with the IR receptor and running the same code as used for decoding the protocol.

The nice thing is that you can hook up the arduinos on 2 different USB cables connected to the same machine, they will report as ttyUSB0 and ttyUSB1 and once you have looked at the serial output you can find which is which. The only cumbersome part is having to select the serial port to the other one when you want to switch box either to monitor the output or to upload a new ersion of the code, so far things are rather easy.

Except it just didn't worked !!!

Not the arduino, I actually replaced the IR LED by a normal one from time to time to verify it was firing for a fraction of a second when emitting the sequence, no the problem was that the IR receiver was detecting transitions but none of the expected duration, or order, nothing I could really consider a mapping of what my code was sending. So I tweaked the emitting code over and over rewriting the timing routines in 3 different ways, trying to disable interrupts, etc... Nothing worked!

Clearly there was something I hadn't understood ... and I started searching on google and reading, first about timing issues on the Arduino but things ought to be correct there, and then on existing remote control code for Arduino and others. Then I hit Ken Shirriff's blog on his IR library for the Arduino and realized that the IR LED and the IR Receiver don't operate at the same level. The LED really can just be switched on or off, but the IR Receiver is calibrated for a given frequency (38 KHz in this case) and will not report if it gets the IR light, but report if it gets the 38 KHz pulse carried by the IR light. In a nutshell the IR receiver was decoding my analogic 0's but didn't for the 1's because it was failing to catch a 38 KHz pulse, I was switching the IR led permanently on and that was not recognized as a 1 and generating erroneous transitions.

Emitting the 38KHz pulse

Ken Shirriff has another great article titled Secrets of Arduino PWM explaining the details used to generate a pulse automatically on *selected* Arduino digital output ans explains the details used to set this up. This is rather complex and nicely encapsulated in his infrared library code, but I would suggest to have a look if you're starting advanced developments on the Arduino.

The simplest is then to use Ken's IRremote library by first installing it into the installed arduino environment:

and then use it in the midea_ir.ino program:

#include <IRremote.h>

IRsend irsend;

int IRpin = 3;

This includes the library in the resulting program, define an IRsend object that we will use to drive the IR led. One thing to note is that by default the IRremote library drives only the digital pin 3, you can modify it to change to a couple of other pins, but it is not possible to drive the PWM for digital pin 5 which is the one not used currently on my greenhouse Arduino.

Then the idea is to just replace the ir_down() and ir_up() in the code with the equivalent low level entry points driving the LED in the IRsend object, first by using irsend.enableIROut(38) to enable the pulse at 38 KHz on the default pin (Digital 3) and then use irsend.mark(usec) for the equivalent ir_down() and irsend.space(usec) for the ir_up():

void emit_midea_start(void) {
    irsend.enableIROut(38);
    irsend.mark(4200);
    irsend.space(4500);
}

void emit_midea_end(void) {
    irsend.mark(550);
    irsend.space(4500);
}
void emit_midea_byte(byte b) {
     int i;
     byte cur = b;
     byte mask = 0x80;

     for (i = 0;i < 8;i++) {
         irsend.mark(450);
         if (cur & mask)
             irsend.space(1700);
         else
             irsend.space(600);
         mask >> 1;
     }

...

Checking with a normal led allowed to spot a brief light when emitting the frame so it was basically looking okay...

And this worked, placing the emitting arduino in front of the receiving the IRanalyzer started to decode the frames, as with the real remote control, things were looking good again !

But failed the real test ... when put in from of the AC the hardware didn't react, some improvement is still needed.

Check your timings, theory vs. practice

I suspected some timing issue, not with the 38KHz pulse as the code from Ken was working fine for an array of devices, but rather how my code was emitting, another precious hint was found in the blog about the library:

IR sensors typically cause the mark to be measured as longer than expected and the space to be shorter than expected. The code extends marks by 100us to account for this (the value MARK_EXCESS). You may need to tweak the expected values or tolerances in this case.

remember that the receptor does some logic on the input to detect the pulse at 38 KHz, that means that while a logic 0 can be detected relatively quickly, it will take at least a few beats before the sync to the pulse is recognized and the receiver switch its output to a logic 1. In a nutshell a 1 T low duration takes less time to recognize than an 1 T high duration. I was also afraid that the overall time to send a full frame would drift over the fixed limit needed to transmit it.

So I tweaked the emitting code to count the actual overall duration of the frames, and aslo added to the receiver decoding code the display of the duration of the first 10 durations between transitions. I then reran the receiver looking at the same input from the real remote control and the arduino emulation, and found that in average:

After tweaking the duration accordingly in the emitter code, I got my first successful emulated command to the AC, properly switching it off, SUCCESS !!!

I then finished the code to provide the weird temperature conversions front end routines and then glue that as a test application looping over a minute:

The midea_ir_v1.ino code is available for download, analysis and reuse. I would suggest to not let this run for long in fron of an AC as the very frequent change of mode may not be good for the hardware (nor for the electricity bill !).

is available for download

Generating the 38KHz pulse in software

While the PWM generation has a number of advantages, especially w.r.t. regularity of the pattern and no risk of drift due for example to delays handling interrupts, in my case it has the serious drawback of forcing use of a given pin (3 by default, or 9 if switching to a different timer in the IRremote code), and those are not available, unless getting the soldering iron and changing some of the existing routing in my add-on board. So the next step is to also implement the 38KHz pulse in software. First this should only affect the up phase, the down phase consist of no emission and hence implemented by a simple:

void send_space(int us) {
    digitalWrite(IRpin, LOW);
    delayMicroseconds(us);
}

The up part should divised into HIGH for most of the duration, followed by a small LOW indicating the pulse. 38 KHz means a 26.316 microseconds period. Since the delayMicroseconds() of the arduino indicates it can be reliable only for more than 3 microseconds, it seems reasonable to use a 22us HIGH/ 4us LOW split, and expect the remaining computation to fill the sub-microsecond of the period, that ought to be accurate enough. One of the point of the code below is to try to avoid excessive drift in two ways:

The resulting code doesn't look very nice:

void send_mark(int us) {
    unsigned long e, t = micros();
    e = t + us;
    while (t < e) {
        digitalWrite(IRpin, HIGH);
        if (t - e < 4) {
            while ((t = micros()) < e);
            digitalWrite(IRpin, LOW);
            break;
        }
        if (t - e < 22) {
            delayMicroseconds(t-e);
            digitalWrite(IRpin, LOW);
            break;
        }
        delayMicroseconds(22);
        digitalWrite(IRpin, LOW);
        t = micros();
        if (t - e < 4) {
            while ((t = micros()) < e);
            break;
        }
        delayMicroseconds(4);
        t = micros();
    }
}

But to my surprize once I replaced all the irsend.mark() and irsend.space() by equivalent calls to send_mark() and send_space(), the IRanalyzer running on the second arduino properly understood the sequence, proving that the IR receiver properly picked the signal, yay !

Of course that didn't worked out the first time on the real hardware, after a bit of analysis of the resulting timings exposed by IRanalyzer I noticed the mark at the beginning of bits were all nearly 100us too long, I switched the generation from 450us to 350us, and bingo, that worked with the real aircon !

the midea_ir_v2.ino resulting module is very specific code, but it is tiny, less than 200 lines, and he hardware side is also really minimal, a single resistor and the IR led.

Epilogue

The code is now plugged and working, but v2 just could not work in the real environment with all the other sensors and communication going on. I suspect that the amount of foreign interrupts are breaking the 38KHz pulse generation, switching back to the PWM generated pulse using the IRremote library works in a very reliable way. So I had to unsolder pin 3 and reaffect it to the IR led, but that was a small price to pay in comparison of trying to debug the timing issues in situ !

Daniel Veillard, 29 Nov 2012