/*

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);
  }
}