/*
 * This is a driver for the Insteon USB powerline controller V2.
 *
 * "Insteon" is a registered trademark. It is appropriate to use "Insteon"
 * to refer to products that are licensed to use that brand by SmartLabs,
 * such as the Insteon devices sold by Smarthome. You should use another
 * name to refer to Open Source software. My own software for driving
 * Insteon devices is called "Ion".
 *
 * Written by Bruce Perens <bruce@perens.com>.
 * Copyright (C) 2006 Sourcelabs.
 *
 * This is Free Software under the GNU General Public License version
 * 2.1 or any later version. If you wish another license, please write
 * to the author.
 *
 * Sourcelabs pays me to spend half of my work time on Open Source
 * projects of my choice. Thus, they sponsored this development.
 * 
 * Currently, this software writes the ROM version command and dumps the
 * result, then monitors the PLC output until interrupted.
 * 
 * Obviously, I have more plans for it. - Bruce
 */
#include <hid.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define ION_READ_BUFFER_SIZE    8
#define ION_WRITE_BUFFER_SIZE   1024

struct _IonPLC {
  char *                r_head;
  char *                r_tail;
  char *                w_head;
  char *                w_tail;
  HIDInterface *        hid;
  unsigned int          got_ack:1;
  char                  r_data[ION_READ_BUFFER_SIZE];
  char                  w_data[ION_WRITE_BUFFER_SIZE];
};
typedef struct _IonPLC *        IonPLC;

/*
 * ReceiveCount is a table of the length of fixed-length packets.
 * For variable-length packets, it specifies the length of the header data.
 *
 * Labels for PLC packet bytes:
 *
 * A2   Address (high byte, then low byte).
 * ACK  Acknowledge (0x06) or Negative Acknowledge (0x15).
 * AND  AND mask. NOT Bits to clear.
 * C2   Checksum (high byte, then low byte).
 * D2   Device type (high byte, then low byte).
 * EN   Event number.
 * FR   Firmware revision number.
 * I3   Insteon address (three bytes, high first).
 * L    Length (one byte).
 * L2   Length (high byte, then low byte).
 * MF	Message flags.
 * OR   OR mask. Bits to set.
 * TV   Timer value.
 * XT   X10 type: 0 for address, 1 for data.
 * dN   Data bytes. N indicates how many.
 */
static const int ReceiveCount[] = {
        5,      /* 0x40 Write to IBIOS: A2 L2 ACK */
        1,      /* 0x41 IBIOS message: L dL. */
        7,      /* 0x42 Read from IBIOS: A2 L2 C2 ACK */
        0,      /* 0x43 ETX-terminated IBIOS message: d(variable) ETX (3) */
        7,      /* 0x44 Get Checksim: A2 L2 C2 ACK */
        1,      /* 0x45 Event report: EN */
        5,      /* 0x46 Mask (bit-manipulation): A2 OR AND ACK */
        3,      /* 0x47 Simiulated event: EN TV ACK */
        7,      /* 0x48 Get version: I3 D2 FR */
        2,      /* 0x49 Debug reporting next instruction to execute: A2 */
        2,      /* 0x4a X10 recieved: XT d1 */
        -1,     /* 0x4b */
        -1,     /* 0x4c */
        -1,     /* 0x4d */
        -1,     /* 0x4e */
        10,     /* 0x4f Insteon packet: EN I3(source) I3(dest) MF d2 [d14] ACK*/
};

static const char * const	EventExplanations[] = {
	"SALad initialized",	/* 0 */
	"Received the first message in a hop sequence addressed to me",	/* 1 */
	"Received the first message in a hop sequence not addressed to me",/*2*/
	"Received a duplicate message",	/* 3 */
	"Recieved an ACK to my direct message",	/* 4 */
	"Did not get an ACK to my direct message, even after 5 retries",/* 5 */ 
	"Received a message to an unknown address, and censored it",	/* 6 */
	"Received a response to my join-me message",			/* 7 */
	"Received an X10 byte",						/* 8 */
	"Received an X10 extended-message byte",			/* 9 */
	"A SET button tap sequence has begun",			/* a */
	"The SET button is being pressed",				/* b */
	"The SET button has been released",				/* c */
	"The tick counter has expired",					/* d */
	"An alarm tripped",						/* e */
	"The dedicated midnight alarm tripped",				/* f */
	"The dedicated 2:00 AM alarm tripped",				/* 10 */
	"Received a serial byte for SALad processing",			/* 11 */
	"Received an unknown IBIOS serial command",			/* 12 */
	"Received an interrupt from my daughter card",			/* 13 */
	"The load was turned on",					/* 14 */
	"The load was turned off",					/* 15 */
};

static int
fill(IonPLC b)
{
  hid_return    r;

  r = hid_interrupt_read(b->hid, USB_ENDPOINT_IN+1, (char *)b->r_data, ION_READ_BUFFER_SIZE, 100);
  if ( r == HID_RET_SUCCESS ) {
    unsigned char       length = b->r_data[0] & 0x0f;

    if ( b->r_data[0] & 0x80 ) {
      printf("Got CTS\n");
    }

    if ( length > 7 ) {
      fprintf(stderr, "Invalid PLC message length %d.\n", length);
      return 0;
    }

    b->r_tail = (b->r_head = b->r_data + 1) + length;

    return (int)length;
  }
  else
    return 0;
}

inline static int
getByte(IonPLC b)
{
  int   c;

  if ( b->r_head == b->r_tail && fill(b) == 0 )
    return -1;

  c = *(b->r_head)++ & 0xff;

  return c;
}

void
IonPLCClose(IonPLC b)
{
  hid_close(b->hid);
  hid_delete_HIDInterface(&(b->hid));
  hid_cleanup();
  free(b);
}

inline static int
getBytes(IonPLC b, unsigned char * r_data, int length, int * copied, int real_length)
{
  int   i;
  int   c;
  int   cp = *copied;

  for ( i = 0; i < real_length; i++ ) {
    if ( (c = getByte(b)) < 0 )
      return 0;
    if ( cp < length )
      r_data[cp++] = c & 0xff;
  }
  *copied = cp;
  return cp;
}

int
IonPLCRead(IonPLC b, unsigned char * data, int length)
{
    int c;
    int copied = 0;

    while ( (c = getByte(b)) >= 0 && c != 2 ) {
      fprintf(stderr, "Discarding unsynchronized byte %02x\n", c);
    }

    if ( c < 0 )
      return 0;

    c = getByte(b);

    if ( c >= 0x40 && c <= 0x4f ) {
      int   get = ReceiveCount[c & 0x0f];

      if ( copied < length )
        data[0] = c & 0xff;

      if ( get > 0 )
        getBytes(b, &(data[1]), length - copied, &copied, get);
      else {
        fprintf(stderr, "Don't know what to do with command %x\n", c);
        return copied;
      }

      switch ( c ) {
      case 0x41:
        get = data[1] & 0xff;
        break;
      case 0x42:
        get = ((data[3] & 0xff) << 8) | (data[4] & 0xff);
        break;
      case 0x4f:
        if ( data[8] & 0x10 ) {	/* Extended message */
          fprintf(stderr, "Got extended message.\n");
          get = 14;
          break;
        }
        /* Fall through */
      default:
	  return copied;
      }
      getBytes(b, &data[copied], length - copied, &copied, get);
      
    }
    else if ( c >= 0 ) {
      fprintf(stderr, "Don't know what to do with received command %02x.\n",
       c);
    }

    return 0;
}

IonPLC
IonPLCOpenUSB()
{
  static const HIDInterfaceMatcher matcher = { 0x10bf, 0x4, NULL, NULL, 0 };
  hid_return    r;

  IonPLC        b = malloc(sizeof(struct _IonPLC));

  if ( b == 0 || hid_init() != HID_RET_SUCCESS
   || (b->hid = hid_new_HIDInterface()) == 0 ) {
    fprintf(stderr, "Failed to initialize libhid.\n");
    return 0;
  }

  b->r_head = b->r_tail = b->r_data;
  b->w_head = b->w_tail = b->w_data;

  if ( (r = hid_force_open(b->hid, 0, &matcher, 3)) != HID_RET_SUCCESS ) {
    fprintf(stderr, "hid_force_open failed with return code %d\n", r);
    return 0;
  }

  return b;
}

int main(void)
{
  IonPLC        b = IonPLCOpenUSB();
  hid_return    r;
  unsigned char msg[] = { 0x02, 0x02, 0x48 };   /* Get ROM version */
  char          empty = 0;
  int           written = 0;

  if ( b == 0 )
    return 1;
  
  /*
   * For some reason, the first write just disappears.
   * I don't know if this is a libhid or PLC issue.
   * So, I just do an empty write here.
   */
  hid_interrupt_write(b->hid, USB_ENDPOINT_OUT+1, &empty, 0, 100);

  for ( ; ; ) {
    unsigned char       p[1024];
    int                 length;
    unsigned char	event;


    if ( (length = IonPLCRead(b, p, sizeof(p))) > 0 ) {
      switch (p[0]) {
      case 0x45:
        event = p[1];
        printf("Event %x", event);
        if ( event < (sizeof(EventExplanations)/sizeof(*EventExplanations)) )
          printf(": %s", EventExplanations[event]);
        printf("\n");
        break;
      case 0x48:
        printf("PLC address %x.%x.%x, Decice type %x subtype %x, Firmware version %x.\n"
        ,p[1], p[2] ,p[3] ,p[4] ,p[5] ,p[6] ,p[7]);
        break;
      case 0x4f:
        printf("Insteon Message: Event %x, %x.%x.%x %x.%x.%x %x %x %x.\n"
        ,p[1] ,p[2] ,p[3] ,p[4] ,p[5] ,p[6] ,p[7] ,p[8] ,p[9] ,p[10]);
        break;
      default:
        printf("Got command %x.\n", p[0]);
      }
      fflush(stdout);
    }

    if ( written < 1 ) {
      r = hid_interrupt_write(b->hid, USB_ENDPOINT_OUT+1, (char *)msg, sizeof(msg), 100);
      if ( r != 0 )
        fprintf(stderr, "Write returned %d.\n", r);
      written++;
    }
  }
  IonPLCClose(b);

  return 0;
}
