Toshiba Air Conditioner IR signal Reverse Engineering
Introduction
I am in the process of installing some Toshiba air conditioners in my house, and am planning to control them remotely using OpenHab (http://www.openhab.org/).
The first step to be able to control the air conditioner is to be able to emulate IR signals as they are sent by the AC remote.
In order to do that I have modified an open source IR library (https://github.com/r45635/HVAC-IR-Control) to add support for the Toshiba AC units.
Air Conditioning remotes work in a slightly different way than standard remotes as they usually transmit a packet containing all information related to the AC as opposed to, say, TV remotes where every keypress transmits only a single information like a button press.
This is a typical packet sent from a Toshiba Remote (in Hexadecimal):
# Key Hex # Bits 1 fan1 F2 0D 03 FC 01 00 40 00 41 F2 0D 03 FC 01 00 40 00 41 144
it is a 72 bit packet that is transmitted twice (I imagine to rule out transmission errors), this is the binary representation:
11110010 00001101 00000011 11111100 00000001 00000000 01000000 00000000 01000001 11110010 00001101 00000011 11111100 00000001 00000000 01000000 00000000
and this is the graphic representation, with timings for the packet header:
In this post I'll try to explain how I proceeded to analyse different signals and discover how to generate them using code as opposed to memorise all possible signals and repeat them back (not doable given all the possible permutations and the limited memory available on the ESP8266/Arduino platforms)
Tools used for the reverse engineering:
- AnalysIR (paid version): a tool that really makes decoding IR signals easy for moderately competent software engineers
The tool facilitates recording and analising IR signals, it already has supports for a lot of IR protocols (TOSHIBA AC remotes included) and is able to:
- Record IR signals using multiple IR sensors hardware (arduino,esp8266,raspberry, A.IR shields) on two separate channels for easy comparison
- Analyze visually the signal and quickly calculate IR base settings like timings for marks, ones and zeros to e.g. configure common IR libraries
- Export the signal to source code for various platform
- help decoding the signal and identifying CRC methods
- quickly find variations on different IR signals
- A.IR Shield: an arduino nano shield that is able to receive and log IR signals to AnalysIR. It is not necessary for AnalysIr, but for 20 euro for the shield and an Arduino nano clone it can get you started really quickly in the IR decoding business. The A.IR shield also has IR sending capabilities if needed
- SendIR module: an arduino/esp8266 controllable module that has two high power/high range IR emitters, used to replicate the AC remote IR signals
- ESP8266 based dev board: used to control the SendIR module, any arduino will do, in my case I will use the ESP8266 to control multiple SendIr modules by forwarding data to it from Openhab
Protocol Basics
The Toshiba IR protocol, as far as I could understand uses three possible signal lengths
- 144 bits for signals that control
- power
- temperature
- ac mode (auto/hot/dry/cool)
- fan speed
- 160 bits for signals that control eco/hi power mode
- 112 bits for signals that control swing mode
I have worked in decoding and reproducing only the 144 bit signals, so far
The 144 bit signal is in reality a 72 bit (9byte signal) repeated twice
Let's consider this signal:
# | Key | Hex | # Bits | ||
1 | fan1 | F2 0D 03 FC 01 00 40 00 41 F2 0D 03 FC 01 00 40 00 41 | 144 |
the first 9 bytes are:
F2 0D 03 FC 01 00 40 00 41
- F2 0D 03 FC 01 : bytes 1-5 are fixed for the 144 bit signal
- 00 : byte 6 stores two kind of information
- first 4 bits: temperature in the range: 17-30
17 | F2 0D 03 FC 01 00 00 00 01 F2 0D 03 FC 01 00 00 00 01 | 144 | |||
18 | F2 0D 03 FC 01 10 00 00 11 F2 0D 03 FC 01 10 00 00 11 | 144 | |||
19 | F2 0D 03 FC 01 20 00 00 21 F2 0D 03 FC 01 20 00 00 21 | 144 | |||
20 | F2 0D 03 FC 01 30 00 00 31 F2 0D 03 FC 01 30 00 00 31 | 144 | |||
21 | F2 0D 03 FC 01 40 00 00 41 F2 0D 03 FC 01 40 00 00 41 | 144 | |||
22 | F2 0D 03 FC 01 50 00 00 51 F2 0D 03 FC 01 50 00 00 51 | 144 | |||
23 | F2 0D 03 FC 01 60 00 00 61 F2 0D 03 FC 01 60 00 00 61 | 144 | |||
24 | F2 0D 03 FC 01 70 00 00 71 F2 0D 03 FC 01 70 00 00 71 | 144 | |||
25 | F2 0D 03 FC 01 80 00 00 81 F2 0D 03 FC 01 80 00 00 81 | 144 | |||
26 | F2 0D 03 FC 01 90 00 00 91 F2 0D 03 FC 01 90 00 00 91 | 144 | |||
27 | F2 0D 03 FC 01 A0 00 00 A1 F2 0D 03 FC 01 A0 00 00 A1 | 144 | |||
28 | F2 0D 03 FC 01 B0 00 00 B1 F2 0D 03 FC 01 B0 00 00 B1 | 144 | |||
29 | F2 0D 03 FC 01 C0 00 00 C1 F2 0D 03 FC 01 C0 00 00 C1 | 144 | |||
26 | ON->30 | F2 0D 03 FC 01 D0 00 00 D1 F2 0D 03 FC 01 D0 00 00 D1 |
- second 4 bits: on/off state
- 00 is on
- 02 is off
- 40: byte 7 stores two kind of information
- first 4 bits: fan speed
7 17-fan auto F2 0D 03 FC 01 00 00 00 01 F2 0D 03 FC 01 00 00 00 01 144
8 fan5 F2 0D 03 FC 01 00 C0 00 C1 F2 0D 03 FC 01 00 C0 00 C1 144
9 fan4 F2 0D 03 FC 01 00 A0 00 A1 F2 0D 03 FC 01 00 A0 00 A1 144
10 fan3 F2 0D 03 FC 01 00 80 00 81 F2 0D 03 FC 01 00 80 00 81 144
11 fan2 F2 0D 03 FC 01 00 60 00 61 F2 0D 03 FC 01 00 60 00 61 144
12 fan1 F2 0D 03 FC 01 00 40 00 41 F2 0D 03 FC 01 00 40 00 41 144
- second 4 bits: AC mode
3 17-fan auto F2 0D 03 FC 01 00 00 00 01 F2 0D 03 FC 01 00 00 00 01 144
4 hot F2 0D 03 FC 01 50 03 00 52 F2 0D 03 FC 01 50 03 00 52 144
5 dry F2 0D 03 FC 01 50 02 00 53 F2 0D 03 FC 01 50 02 00 53 144
6 cold F2 0D 03 FC 01 50 01 00 50 F2 0D 03 FC 01 50 01 00 50 144
- 00: byte 8 unused as far as I can tell
- 41: checksum, obtained by XORing the previous eight bytes
- second 4 bits: on/off state
- 00 is on
- 02 is off
- 40: byte 7 stores two kind of information
- first 4 bits: fan speed
7 | 17-fan auto | F2 0D 03 FC 01 00 00 00 01 F2 0D 03 FC 01 00 00 00 01 | 144 | ||
8 | fan5 | F2 0D 03 FC 01 00 C0 00 C1 F2 0D 03 FC 01 00 C0 00 C1 | 144 | ||
9 | fan4 | F2 0D 03 FC 01 00 A0 00 A1 F2 0D 03 FC 01 00 A0 00 A1 | 144 | ||
10 | fan3 | F2 0D 03 FC 01 00 80 00 81 F2 0D 03 FC 01 00 80 00 81 | 144 | ||
11 | fan2 | F2 0D 03 FC 01 00 60 00 61 F2 0D 03 FC 01 00 60 00 61 | 144 | ||
12 | fan1 | F2 0D 03 FC 01 00 40 00 41 F2 0D 03 FC 01 00 40 00 41 | 144 |
- second 4 bits: AC mode
3 | 17-fan auto | F2 0D 03 FC 01 00 00 00 01 F2 0D 03 FC 01 00 00 00 01 | 144 | ||
4 | hot | F2 0D 03 FC 01 50 03 00 52 F2 0D 03 FC 01 50 03 00 52 | 144 | ||
5 | dry | F2 0D 03 FC 01 50 02 00 53 F2 0D 03 FC 01 50 02 00 53 | 144 | ||
6 | cold | F2 0D 03 FC 01 50 01 00 50 F2 0D 03 FC 01 50 01 00 50 | 144 |
- 00: byte 8 unused as far as I can tell
- 41: checksum, obtained by XORing the previous eight bytes
Reverse Engineering Process
I started by recording multiple signals from the Toshiba remote and assigning an identifier like hot-dry-cold-auto when cycling the AC mode setting
after that I used the reverse engineering tool to identify patterns in the signal:
# | Key | Hex | # Bits | ||
1 | fan1 | F2 0D 03 FC 01 00 40 00 41 F2 0D 03 FC 01 00 40 00 41 | 144 | ||
2 | 17-fan auto | F2 0D 03 FC 01 00 00 00 01 F2 0D 03 FC 01 00 00 00 01 | 144 | ||
3 | 17-fan auto | F2 0D 03 FC 01 00 00 00 01 F2 0D 03 FC 01 00 00 00 01 | 144 | ||
4 | hot | F2 0D 03 FC 01 50 03 00 52 F2 0D 03 FC 01 50 03 00 52 | 144 | ||
5 | dry | F2 0D 03 FC 01 50 02 00 53 F2 0D 03 FC 01 50 02 00 53 | 144 | ||
6 | cold | F2 0D 03 FC 01 50 01 00 50 F2 0D 03 FC 01 50 01 00 50 | 144 | ||
7 | 17-fan auto | F2 0D 03 FC 01 00 00 00 01 F2 0D 03 FC 01 00 00 00 01 | 144 | ||
8 | fan5 | F2 0D 03 FC 01 00 C0 00 C1 F2 0D 03 FC 01 00 C0 00 C1 | 144 | ||
9 | fan4 | F2 0D 03 FC 01 00 A0 00 A1 F2 0D 03 FC 01 00 A0 00 A1 | 144 | ||
10 | fan3 | F2 0D 03 FC 01 00 80 00 81 F2 0D 03 FC 01 00 80 00 81 | 144 | ||
11 | fan2 | F2 0D 03 FC 01 00 60 00 61 F2 0D 03 FC 01 00 60 00 61 | 144 | ||
12 | fan1 | F2 0D 03 FC 01 00 40 00 41 F2 0D 03 FC 01 00 40 00 41 | 144 | ||
13 | 17 | F2 0D 03 FC 01 00 00 00 01 F2 0D 03 FC 01 00 00 00 01 | 144 | ||
14 | 18 | F2 0D 03 FC 01 10 00 00 11 F2 0D 03 FC 01 10 00 00 11 | 144 | ||
15 | 19 | F2 0D 03 FC 01 20 00 00 21 F2 0D 03 FC 01 20 00 00 21 | 144 | ||
16 | 20 | F2 0D 03 FC 01 30 00 00 31 F2 0D 03 FC 01 30 00 00 31 | 144 | ||
17 | 21 | F2 0D 03 FC 01 40 00 00 41 F2 0D 03 FC 01 40 00 00 41 | 144 | ||
18 | 22 | F2 0D 03 FC 01 50 00 00 51 F2 0D 03 FC 01 50 00 00 51 | 144 | ||
19 | 23 | F2 0D 03 FC 01 60 00 00 61 F2 0D 03 FC 01 60 00 00 61 | 144 | ||
20 | 24 | F2 0D 03 FC 01 70 00 00 71 F2 0D 03 FC 01 70 00 00 71 | 144 | ||
21 | 25 | F2 0D 03 FC 01 80 00 00 81 F2 0D 03 FC 01 80 00 00 81 | 144 | ||
22 | 26 | F2 0D 03 FC 01 90 00 00 91 F2 0D 03 FC 01 90 00 00 91 | 144 | ||
23 | 27 | F2 0D 03 FC 01 A0 00 00 A1 F2 0D 03 FC 01 A0 00 00 A1 | 144 | ||
24 | 28 | F2 0D 03 FC 01 B0 00 00 B1 F2 0D 03 FC 01 B0 00 00 B1 | 144 | ||
25 | 29 | F2 0D 03 FC 01 C0 00 00 C1 F2 0D 03 FC 01 C0 00 00 C1 | 144 | ||
26 | ON->30 | F2 0D 03 FC 01 D0 00 00 D1 F2 0D 03 FC 01 D0 00 00 D1 | 144 |
and the checksum calculator tool to verify that the Checksum byte was indeed calculated with an XOR algorithm
Then I added a new method to the HVAC IR library that would translate this findings into an actual IR signal.
To do that I had to define a couple of constants:
#define HVAC_TOSHIBA_HDR_MARK 4400
#define HVAC_TOSHIBA_HDR_SPACE 4300
#define HVAC_TOSHIBA_BIT_MARK 543
#define HVAC_TOSHIBA_ONE_SPACE 1623
#define HVAC_MISTUBISHI_ZERO_SPACE 472
#define HVAC_TOSHIBA_RPT_MARK 440
#define HVAC_TOSHIBA_RPT_SPACE 7048 // Above original iremote limit
- HVAC_TOSHIBA_HDR_MARK is the time duration of an header mark (in microseconds): it is a HIGH signal sent at the beginning of the signal to identify the start point
- HVAC_TOSHIBA_HDR_SPACE is the time duration of an header space (in microseconds): it is a LOW signal sent after the mark and precedes the bit stream
- HVAC_TOSHIBA_BIT_MARK is the time duration of a mark for a bit (either 0 or 1,in microseconds): it is a HIGH signal sent at the beginning of the bit to indicate a new bit is incoming
- HVAC_TOSHIBA_ONE_SPACE is the time duration of a space for a bit with value 1 (in microseconds): it is a LOW signal sent after the bit mark
- HVAC_TOSHIBA_ZERO_SPACE is the time duration of a space for a bit with value 0 (in microseconds): it is a LOW signal sent after the bit mark
And here is the actual code that implements the protocol above:
- HVAC_TOSHIBA_RPT MARK is the time duration of an header mark (in microseconds): it is a HIGH signal sent at the end of the first half of the signal to identify the start point of the repetition
- HVAC_TOSHIBA_RPT SPACE is the time duration of an header mark (in microseconds): it is a LOW signal sent after the repetition mark and marks the beginning of the second half of the bit stream
/****************************************************************************
/* Send IR command to Toshiba HVAC - sendHvacToshiba
/***************************************************************************/
void sendHvacToshiba(
HvacMode HVAC_Mode, // Example HVAC_HOT
int HVAC_Temp, // Example 21 (°c)
HvacFanMode HVAC_FanMode, // Example FAN_SPEED_AUTO
int OFF // Example false
)
{
#define HVAC_TOSHIBA_DATALEN 9
#define HVAC_TOSHIBA_DEBUG; // Un comment to access DEBUG information through Serial Interface
byte mask = 1; //our bitmask
//F20D03FC0150000051
byte data[HVAC_TOSHIBA_DATALEN] = { 0xF2, 0x0D, 0x03, 0xFC, 0x01, 0x00, 0x00, 0x00, 0x00 };
// data array is a valid trame, only byte to be chnaged will be updated.
byte i;
#ifdef HVAC_TOSHIBA_DEBUG
Serial.println("Packet to send: ");
for (i = 0; i < HVAC_TOSHIBA_DATALEN; i++) {
Serial.print("_");
Serial.print(data[i], HEX);
}
Serial.println(".");
#endif
data[6] = 0x00;
// Byte 7 - Mode
switch (HVAC_Mode)
{
case HVAC_HOT: data[6] = (byte) B00000011; break;
case HVAC_COLD: data[6] = (byte) B00000001; break;
case HVAC_DRY: data[6] = (byte) B00000010; break;
case HVAC_AUTO: data[6] = (byte) B00000000; break;
default: break;
}
// Byte 7 - On / Off
if (OFF) {
data[6] = (byte) 0x07; // Turn OFF HVAC
} else {
// Turn ON HVAC (default)
}
// Byte 6 - Temperature
// Check Min Max For Hot Mode
byte Temp;
if (HVAC_Temp > 30) { Temp = 30;}
else if (HVAC_Temp < 17) { Temp = 17; }
else { Temp = HVAC_Temp; };
data[5] = (byte) Temp - 17<<4;
// Byte 10 - FAN / VANNE
switch (HVAC_FanMode)
{
case FAN_SPEED_1: data[6] = data[6] | (byte) B01000000; break;
case FAN_SPEED_2: data[6] = data[6] | (byte) B01100000; break;
case FAN_SPEED_3: data[6] = data[6] | (byte) B10000000; break;
case FAN_SPEED_4: data[6] = data[6] | (byte) B10100000; break;
case FAN_SPEED_5: data[6] = data[6] | (byte) B11000000; break;
case FAN_SPEED_AUTO: data[6] = data[6] | (byte) B00000000; break;
case FAN_SPEED_SILENT: data[6] = data[6] | (byte) B00000000; break;//No FAN speed SILENT for TOSHIBA so it is consider as Speed AUTO
default: break;
}
// Byte 9 - CRC
data[8] = 0;
for (i = 0; i < HVAC_TOSHIBA_DATALEN - 1; i++) {
data[HVAC_TOSHIBA_DATALEN-1] = (byte) data[i] ^ data[HVAC_TOSHIBA_DATALEN -1]; // CRC is a simple bits addition
}
#ifdef HVAC_TOSHIBA_DEBUG
Serial.println("Packet to send: ");
for (i = 0; i < HVAC_TOSHIBA_DATALEN; i++) {
Serial.print("_"); Serial.print(data[i], HEX);
}
Serial.println(".");
for (i = 0; i < HVAC_TOSHIBA_DATALEN ; i++) {
Serial.print(data[i], BIN); Serial.print(" ");
}
Serial.println(".");
#endif
enableIROut(38); // 38khz
space(0);
for (int j = 0; j < 2; j++) { // For Toshiba IR protocol we have to send two time the packet data
// Header for the Packet
mark(HVAC_TOSHIBA_HDR_MARK);
space(HVAC_TOSHIBA_HDR_SPACE);
for (i = 0; i < HVAC_TOSHIBA_DATALEN; i++) {
// Send all Bits from Byte Data in Forward Order (MSB)
for (mask = 10000000; mask > 0; mask >>= 1) { //iterate through bit mask
if (data[i] & mask) { // Bit ONE
mark(HVAC_TOSHIBA_BIT_MARK);
space(HVAC_TOSHIBA_ONE_SPACE);
}
else { // Bit ZERO
mark(HVAC_TOSHIBA_BIT_MARK);
space(HVAC_MISTUBISHI_ZERO_SPACE);
}
//Next bits
}
}
// End of Packet and retransmission of the Packet
mark(HVAC_TOSHIBA_RPT_MARK);
space(HVAC_TOSHIBA_RPT_SPACE);
space(0); // Just to be sure
}
}
To debug my code without having a real AC unit go crazy because of fake/wrong signals I used AnalysIr to check that the generated signals are the same with respect to the ones recorder from the real AC remote
The full version of the code (and a test ESP8266 sketch) is available on Github
Commenti
Arduino: 1.8.13 (Windows 10), Board: "Arduino Nano, ATmega328P"
In file included from sketch\IRremote2.cpp:24:0:
sketch\IRremoteInt2.h:153:0: warning: "HVAC_MISTUBISHI_ZERO_SPACE" redefined
#define HVAC_MISTUBISHI_ZERO_SPACE 472
sketch\IRremoteInt2.h:143:0: note: this is the location of the previous definition
#define HVAC_MISTUBISHI_ZERO_SPACE 420
sketch\IRremote2.cpp:668:28: warning: ISO C++11 requires whitespace after the macro name
#define HVAC_TOSHIBA_DEBUG; // Un comment to access DEBUG information through Serial Interface
^
C:\Users\Ranni\Documents\Arduino\toshaaaa\toshaaaa.ino:21:28: warning: ISO C++11 requires whitespace after the macro name
#define HVAC_TOSHIBA_DEBUG; // Un comment to access DEBUG information through Serial Interface
^
toshaaaa:12:23: error: variable or field 'sendHvacToshiba' declared void
void sendHvacToshiba(
^
toshaaaa:12:23: error: 'HvacMode' was not declared in this scope
sketch\IRremote2.cpp: In function 'void sendHvacToshiba(HvacMode, int, HvacFanMode, int)':
IRremote2.cpp:744:3: error: 'enableIROut' was not declared in this scope
enableIROut(38); // 38khz
^~~~~~~~~~~
C:\Users\Ranni\Documents\Arduino\toshaaaa\toshaaaa.ino:12:23: note: suggested alternative: 'pinMode'
void sendHvacToshiba(
^
pinMode
toshaaaa:12:43: error: expected primary-expression before 'int'
void sendHvacToshiba(
^
toshaaaa:12:58: error: 'HvacFanMode' was not declared in this scope
void sendHvacToshiba(
^
IRremote2.cpp:745:3: error: 'space' was not declared in this scope
space(0);
^~~~~
toshaaaa:12:84: error: expected primary-expression before 'int'
void sendHvacToshiba(
^
toshaaaa:13:3: error: variable or field 'sendHvacToshiba' declared void
HvacMode HVAC_Mode, // Example HVAC_HOT
^~~~~~~~
toshaaaa:13:3: error: 'HvacMode' was not declared in this scope
sketch\IRremote2.cpp:745:3: note: suggested alternative: 'isSpace'
space(0);
^~~~~
isSpace
IRremote2.cpp:748:5: error: 'mark' was not declared in this scope
mark(HVAC_TOSHIBA_HDR_MARK);
^~~~
sketch\IRremote2.cpp:748:5: note: suggested alternative: 'mask'
mark(HVAC_TOSHIBA_HDR_MARK);
^~~~
mask
sketch\IRremote2.cpp:752:19: warning: large integer implicitly truncated to unsigned type [-Woverflow]
for (mask = 10000000; mask > 0; mask >>= 1) { //iterate through bit mask
^~~~~~~~
C:\Users\Ranni\Documents\Arduino\toshaaaa\toshaaaa.ino:13:3: note: suggested alternative: 'pinMode'
HvacMode HVAC_Mode, // Example HVAC_HOT
^~~~~~~~
pinMode
toshaaaa:14:3: error: expected primary-expression before 'int'
int HVAC_Temp, // Example 21 (°c)
^~~
toshaaaa:15:3: error: 'HvacFanMode' was not declared in this scope
HvacFanMode HVAC_FanMode, // Example FAN_SPEED_AUTO
^~~~~~~~~~~
toshaaaa:16:3: error: expected primary-expression before 'int'
int OFF // Example false
^~~
exit status 1
variable or field 'sendHvacToshiba' declared void
This report would have more information with
"Show verbose output during compilation"
option enabled in File -> Preferences.
This program can be great if you're looking to have more control about the video’s quality, whether you only want the audio, and where to store the file, like a phone’s internal memory or an external SD card.
Posta un commento