/*
This decodes commands from the Lennox mini-split air conditioner remote.
The remote is labelled RG57F1(B)/BGEFU1 and looks like this one:
https://www.amazon.com/Control-RAC-PD1412CRU-RAC-PD1411CWU-RAC-PD1411HRU-Condtioner/dp/B09333FDR4/
(for Toshiba, Keystone and many others)
I have not found any other instances of this protocol on the net, although if the Lennox systems
are just rebranded from another manufacturer and use the same remotes, the information may be out
there and I just haven't found it yet.
The closest I've found to a suite that handles multiple mini-split systems is this Arduino
code at https://github.com/ToniA/arduino-heatpumpir/
(I'm working on a Raspberry Pi.)
Below is a text description of what I decoded by looking at the signals,
followed by an implementation in C that reads the mark/space timings and
decodes to a bitstream and then a 6 byte data packet.
After the C code is the python program which fetches the bit timings (and
is invoked in C by a "popen" call)
0 is active (38Mhz signal, mark)
1 is passive (gap, no signal, space)
Timings here are notional, in practice the values vary a lot. I haven't yet
pinned them down to the exact ideal value, but the values in the accompanying
sender program (send-lennox-code.c) are accepted by the Lennox unit.
It's obvious in hindsight that I decoded the bitstream in reverse order and that
decoding and assembling packets would be easier if I reversed all the bits in a
byte, but it's too late for that now and it all works as long as it's self-consistent.
Transmission consists of a long mark+space to start:
0{4330} 4000 < t < 10000
1{4500} 4000 < t < 5000
followed by first set of data, encoded normally
* 48 bits
0{470} t < 700
followed by either:
1{580} data=0 t < 700
or
1{1690} data=1 700 < t < 1800
Then a block delimiter:
0{470} t < 550
1{5323} 5000 < t < 10000
And start of second block
0{4320} 4000 < t < 10000
1{4520} 4000 < t < 5000
followed by second set of data, same as first but with inverse encoding
(to simplify the code, these are read as-is and checked later -
if the complemented bits don't match, reject the whole transmission)
* 48 bits
0{470} t < 700
followed by either:
1{580} data=0(1) t < 700
or
1{1690} data=1(0) 700 < t < 1800
And finally end of transmission marker
0{470} t < 550
Since the final item is 'space', the last signal may not be visible:
1 5000 < t < 10000
===================================================================
These are decoded into 6 bytes. The first bit to be received in
each packet of 8 bits is the lowest order bit in the byte.
for (bitp = byte = 0; byte < 6; byte++) {
code[byte] = 0; for (shift = 0; shift < 8; shift++) code[byte] |= bit[bitp++] << shift;
}
===================================================================
25 xx xx xx CC xx
/\
bbbbbbNN
NN=00 Follow=OFF
NN=11 Follow=ON
45 NX xx xx xx xx function toggles.
X=0
N=1 LED
N=4 SWING
N=8 DIRECT
N=9 TURBO
N=B CLEAN
85 AB CD EF GH IJ
b4 = AB&32 ? 4 : 0;
b2 = AB&64 ? 2 : 0;
b1 = AB&128 ? 1 : 0;
MODE = b4+b2+b1
0:"cool"
1:"dry"
2:"auto"
3:"heat"
4:"fan"
AB&1=1 ON
AB&1=0 OFF
AB&2=2 Sleep
AB&2=0 (not sleep)
AB&4=4 Fan Auto
AB&4=0
b4 = AB&4 ? 4 : 0;
b2 = AB&8 ? 2 : 0;
b1 = AB&16 ? 1 : 0;
FAN = b4+b2+b1
CD TEMP
b16 = CD&8 ? 16 : 0;
b8 = CD&16 ? 8 : 0;
b4 = CD&32 ? 4 : 0;
b2 = CD&64 ? 2 : 0;
b1 = CD&128 ? 1 : 0;
TEMP = 62 + b16+b8+b4+b2+b1
EF TIMER OFF
GH TIMER ON
if (EF/GH&64) halfhr = ".00"; else halfhr = ".30"; // half-hr time plus half-hr offset
b16 = EF/GH&2 ? 16 : 0;
b8 = EF/GH&4 ? 8 : 0;
b4 = EF/GH&8 ? 4 : 0;
b2 = EF/GH&16 ? 2 : 0;
b1 = EF/GH&32 ? 1 : 0;
hr = b1+b2+b4+b8+b16;
if (EF/GH&64) hr += 1; // half-hr time plus half-hr offset increments hour
TIMER = hr halfhr
IJ most likely is checksum.
*/
#include <stdio.h>
#include <stdlib.h>
int decode_time(int Byte, int *halfhr) {
int b1, b2, b4, b8, b16, b32, hr;
// Displayed times are calculated times + half hr.
if (Byte&64) *halfhr = 0; else *halfhr = 1; // half-hr time plus half-hr offset
b16 = Byte&2 ? 16 : 0;
b8 = Byte&4 ? 8 : 0;
b4 = Byte&8 ? 4 : 0;
b2 = Byte&16 ? 2 : 0;
b1 = Byte&32 ? 1 : 0;
hr = b16+b8+b4+b2+b1;
if (Byte&64) hr += 1; // half-hr time plus half-hr offset increments hour
return hr;
}
int decode_temperature(int Byte2, int Byte5) {
int b1, b2, b4, b8, b16;
//assert((Byte2&7) == 6);
//assert((Byte5&7) == 6);
b16 = Byte2&8 ? 16 : 0;
b8 = Byte2&16 ? 8 : 0;
b4 = Byte2&32 ? 4 : 0;
b2 = Byte2&64 ? 2 : 0;
b1 = Byte2&128 ? 1 : 0;
return b16+b8+b4+b2+b1;
}
int decode_mode(int Byte1, int Byte5) {
int b1, b2, b4, b8, b16;
//assert((Byte2&7) == 6);
//assert((Byte5&7) == 6);
b4 = Byte1&32 ? 4 : 0;
b2 = Byte1&64 ? 2 : 0;
b1 = Byte1&128 ? 1 : 0;
return b1+b2+b4;
}
int decode_fan(int Byte1, int Byte5) {
int b1, b2, b4, b8, b16;
//assert((Byte2&7) == 6);
//assert((Byte5&7) == 6);
b4 = Byte1&4 ? 4 : 0;
b2 = Byte1&8 ? 2 : 0;
b1 = Byte1&16 ? 1 : 0;
return b4+b2+b1;
}
int crc1(int code[6]) {
int i, x = 0xff;
for (i = 0; i < 5; i++) {
x = x+code[i];
}
return (x^0xff)&255;
}
void logger(char *s) {
static char command[1024];
sprintf(command, "logger --server 192.168.2.251 -t Data: \"`date +\\\"%%a %%b %%d %%T %%Z %%Y\\\"` Airco: %s\"", s);
fprintf(stderr, "\n%s\n", command); system(command);
}
static int Follow = 0;
static void Display(int *bit) {
char param[1024];
int shift, bitp, byte, code[6];
fflush(stderr);
for (bitp = byte = 0; byte < 6; byte++) {
code[byte] = 0; for (shift = 0; shift < 8; shift++) code[byte] |= bit[bitp++] << shift;
}
sprintf(param, "CODE %02x %02x %02x %02x %02x %02x %02x",
code[0], code[1], code[2], code[3], code[4], code[5], crc1(code));
logger(param);
switch (code[0]) {
case 0x25:
fprintf(stdout, "Follow.");
if (code[4] == 0xCC) {
int i = code[3]&0x03;
if (i == 0) {
printf("[Follow=OFF]");
Follow = 0;
} else if (i == 3) {
printf("[Follow=ON]");
Follow = 1;
} else {
printf("[Follow=???]");
}
} else {
printf("[Follow???]");
}
break;
case 0x45:
fprintf(stdout, "Function toggle.");
switch (code[1]) {
case 0x10: printf("[FN=LED]"); break;
case 0x40: printf("[FN=SWING]"); break;
case 0x80: printf("[FN=DIRECT]"); break;
case 0x90: printf("[FN=TURBO]"); break;
case 0xB0: printf("[FN=CLEAN]"); break;
default: printf("[FN=UNKNOWN]"); break;
}
break;
case 0x85:
fprintf(stdout, "Full state change.");
char *mode[8] = {"cool", "dry", "auto", "heat", "fan","","",""};
if ((code[1]&1) == 1) {
fprintf(stdout, "[ON]");
} else {
fprintf(stdout, "[OFF]");
}
if ((code[1]&2) == 2) {
fprintf(stdout, "[Sleep]");
}
if (code[4] != 0xFF) {
int half;
int Hr = decode_time(code[4], &half);
fprintf(stdout, "[T-On=%d%s]", Hr, half ?".5":"");
}
if (code[3] != 0xFF) {
int half;
int Hr = decode_time(code[3], &half);
fprintf(stdout, "[T-Off=%d%s]", Hr, half ?".5":"");
}
if (Follow) { // global state and a toggle
fprintf(stdout, "[Follow]");
}
fprintf(stdout, "[M=%s]", mode[decode_mode(code[1], code[5])]);
if ((code[1]&4) == 4) {
fprintf(stdout, "[Fan=Auto]");
} else {
fprintf(stdout, "[Fan=%d]", decode_fan(code[1], code[5]));
}
fprintf(stdout, "[T=%d]", decode_temperature(code[2], code[5])+62);
break;
default:
fprintf(stdout, "Unknown decode\n");
break;
}
fprintf(stdout, "\n\n");
fflush(stdout);
}
#define MARK 0
#define SPACE 1
#define UNCONNECTED 0 // A
#define DRAIN 1 // B
#define INITIALISED0 2 // C
#define INITIALISED1 3 // D
#define GETPBITS0 4 // E
#define GETPBITS1 5 // F
#define GETNBITS0 6 // G
#define GETNBITS1 7 // H
#define MIDPOINT0 8 // I
#define MIDPOINT1 9 // J
#define SECOND_BLOCK0 10 // K
#define SECOND_BLOCK1 11 // L
#define ENDPOINT 12 // M
#define TRAILING_SPACE 13 // N
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv) {
FILE *rawdata, *python;
char command[1024];
int bit[6*8+2];
int i, fnum, rc, sense, t, bitpos, PREV_STATE, STATE = UNCONNECTED, align=0;
const char *pythoncode[] = {
"import RPi.GPIO as GPIO\n",
"import math\n",
"import os\n",
"from datetime import datetime\n",
"from time import sleep\n",
//"# This is for revision 1 of the Raspberry Pi, Model B\n",
//"# This pin is also referred to as GPIO17\n",
"INPUT_WIRE = 11\n",
"GPIO.setmode(GPIO.BOARD)\n",
"GPIO.setup(INPUT_WIRE, GPIO.IN)\n",
"while True:\n",
" value = 1\n",
" while value:\n",
" value = GPIO.input(INPUT_WIRE)\n",
//" # Grab the start time of the command\n",
" startTime = datetime.now()\n",
//" # Used to buffer the command pulses\n",
" command = []\n",
//" # The end of the "command" happens when we read more than\n",
//" # a certain number of 1s (1 is off for my IR receiver)\n",
" numOnes = 0\n",
//" # Used to keep track of transitions from 1 to 0\n",
" previousVal = 0\n",
" while True:\n",
" if value != previousVal:\n",
" now = datetime.now()\n",
" pulseLength = now - startTime\n",
" startTime = now\n",
" command.append((previousVal, pulseLength.microseconds))\n",
" if value:\n",
" numOnes = numOnes + 1\n",
" else:\n",
" numOnes = 0\n",
//" # 10000 is arbitrary, adjust as necessary\n",
" if numOnes > 22000:\n",
" break\n",
" previousVal = value\n",
" value = GPIO.input(INPUT_WIRE)\n",
" \n",
//" # pass the timeout up to the next layer...\n",
" print 0, 0\n",
" for (val, pulse) in command:\n",
" print val, pulse\n",
NULL
};
sprintf(command, "/tmp/%08d.py", getpid());
python = fopen(command, "w");
i = 0;
while (pythoncode[i] != NULL) {
fprintf(python, "%s", pythoncode[i]);
i++;
}
fclose(python);
sprintf(command, "python2 -u /tmp/%08d.py", getpid());
rawdata = popen(command, "r");
if (rawdata == NULL) {
fprintf(stderr, "[popen=%p]\n", rawdata);
fflush(stderr);
exit(1);
}
// Decode the bitstream to 6 bytes.
for (;;) {
PREV_STATE = STATE;
rc = fscanf(rawdata, "%d %d", &sense, &t);
if (rc != 2) {
fprintf(stderr, "[fscanf rc=%d]\n", rc);
fflush(stderr);
exit(1); // end of file?
}
if ((sense == 0) && (t == 0)) {
// shortens state machine considerably.
fprintf(stderr, "\n(+++++)\n\n");
STATE = INITIALISED0;
continue;
}
switch(STATE) {
case UNCONNECTED: // a0,0B; b0,4370B;
// Expect a MARK
if (sense != MARK) STATE = DRAIN;
break;
case DRAIN: // ignore everything until next "0 0" (which is trapped above)
break;
case INITIALISED0: // We have the initial "0 0". Now start.
bitpos = 0; // A0,0 B0,4344 B1,4484
if ((sense == 0) && ((4000 < t) && (t <= 10000))) STATE = INITIALISED1; else STATE = DRAIN;
break;
case INITIALISED1:
if ((sense == 1) && ((4000 < t) && (t <= 5000))) STATE = GETPBITS0; else STATE = DRAIN;
break;
case GETPBITS0:
if ((sense == 0) && ((300 < t) && (t <= 700))) STATE = GETPBITS1; else STATE = DRAIN;
break;
case GETPBITS1:
if ((sense == 1) && (((300 < t) && (t <= 1800)))) {
if (bitpos < 6*8) bit[bitpos++] = t <= 700 ? 0 : 1;
if (bitpos == 6*8) STATE = MIDPOINT0; else STATE = GETPBITS0;
} else STATE = DRAIN;
break;
case MIDPOINT0:
// At this point we *could* accept the data if the checksum is good,
// even if the follow-up copy of the data is different...
bitpos = 0;
if ((sense == 0) && ((300 < t) && (t <= 700))) STATE = MIDPOINT1; else STATE = DRAIN;
break;
case MIDPOINT1:
if ((sense == 1) && ((4000 < t) && (t <= 10000))) STATE = SECOND_BLOCK0; else STATE = DRAIN;
break;
case SECOND_BLOCK0:
if ((sense == 0) && ((4000 < t) && (t <= 10000))) STATE = SECOND_BLOCK1; else STATE = DRAIN;
break;
case SECOND_BLOCK1:
if ((sense == 1) && ((4000 < t) && (t <= 5000))) STATE = GETNBITS0; else STATE = DRAIN;
break;
case GETNBITS0:
if ((sense == 0) && ((300 < t) && (t <= 700))) STATE = GETNBITS1; else STATE = DRAIN;
break;
case GETNBITS1:
if ((sense == 1) && (((300 < t) && (t <= 1800)))) {
if (bit[bitpos++] != (t <= 700 ? 1 : 0)) STATE = DRAIN; // Test inverted data stream matches original
else if (bitpos == 6*8) STATE = ENDPOINT; else STATE = GETNBITS0;
} else STATE = DRAIN;
break;
case ENDPOINT:
if ((sense == 0) && ((300 < t) && (t <= 700))) {
// We have a full set of bits. Publish them.
// Both initial and negated data match, strictly speaking we should check the checksum
// as well in order to accept, but we'll go with the 100% redundancy check for now.
// Note that if the second copy of the data passed the checksum test, we could accept
// it even if the first copy failed the checksum test (and was different from this copy).
fprintf(stderr, "\n(*****)\n\n");
Display(bit);
STATE = TRAILING_SPACE;
} else STATE = DRAIN;
break;
case TRAILING_SPACE:
if ((sense == 1) && (((5000 < t) && (t <= 10000)))) {
// We got the elusive trailing space, not that it matters.
fprintf(stderr, "(!!!!!)\n\n");
STATE = UNCONNECTED;
} else STATE = DRAIN;
default:
// Programmer error?
exit(1);
}
fprintf(stderr, "%c%d,%d%c{%d};", PREV_STATE+'a', sense, t, STATE+'A', bitpos);
if (++align==8) {fprintf(stderr, "\n");align=0;}
fflush(stderr);
}
}