diff --git a/.gitignore b/.gitignore
index a7bccfd0e171fc3c29870381ffdf3bcc468a72bf..3db5e2b95b1280774cf7f2d4e7b9ac41247bd072 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 */avr/compiled
 */linux/compiled
 */matlab/*.autosave
-*~
\ No newline at end of file
+*~
+__pycache__
\ No newline at end of file
diff --git a/Makefile b/Makefile
index dcd58050f3ce9a322eeeb55aa1dbeec04d000936..fb29921c3d8ae3b4b3570f80c4b01dcb60666e5e 100644
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,9 @@ all:
 	    make -C $${d} $@ ; \
 	done
 
+%.LINK:
+	make -C $$(./tools/find_avr_targets $*) $@
+
 %.LOAD:
 	make -C $$(./tools/find_avr_targets $*) $@
 
diff --git a/china_io-2019/avr/Makefile b/china_io-2019/avr/Makefile
index b658ee4533bfaf5f61963942e28951740aa1a367..2e6b2c84a3343360588bd7f6531e53583c49a6de 100644
--- a/china_io-2019/avr/Makefile
+++ b/china_io-2019/avr/Makefile
@@ -7,7 +7,7 @@ china-io.FUSE=--wr_fuse_l=0x1f --wr_fuse_h=0xd9 --wr_fuse_e=0xff
 china-io.FUSE_L=0x1f
 china-io.FUSE_H=0xd9
 china-io.C=china-io
-china-io.H=../lib/serialio i2c_master
+china-io.H=../../lib/avr/serialio i2c_master
 
 china-io-test.ARCH=avr
 china-io-test.CHIP=atmega32
diff --git a/china_io-2019/avr/china-io.c b/china_io-2019/avr/china-io.c
new file mode 100644
index 0000000000000000000000000000000000000000..be0854334f5a3a88901ff19f7413223a3b567f2a
--- /dev/null
+++ b/china_io-2019/avr/china-io.c
@@ -0,0 +1,331 @@
+#include <avr/eeprom.h>
+#include <avr/io.h>
+#include <avr/interrupt.h>
+#include <inttypes.h>
+#include "i2c_master.h"
+#include "serialio.h"
+
+/*
+ * Serial I/O assignments:
+ *   
+ *   AI 0 -- Analog In 0
+ *   AI 1 -- Analog In 1
+ *   AI 2 -- Analog In 2
+ *   AI 3 -- Analog In 3
+ *
+ *   AO 0 -- Analog Out 0
+ *   AO 1 -- Analog Out 1
+ *
+ */
+
+/*
+ * Used I/O pins
+ *
+ * PB4  SPI SS           / ADS8694 SPI CS
+ * PB5  Programming MOSI / ADS8694 SPI SDI
+ * PB6  Programming MISO / ADS8694 SPI SDO
+ * PB7  Programming SCK  / ADS8694 SPI SCLK
+ *
+ * PC0  I2C SCL
+ * PC1  I2C SDA
+ *
+ * PD0  Serial In
+ * PD1  Serial Out
+ *
+ */
+
+#define DDR_SPI DDRB
+#define DD_MISO PB6
+#define DD_MOSI PB5
+#define DD_SCK PB7
+#define DD_SS PB4
+#define PORT_SPI PORTB
+
+static volatile uint8_t serial_readchannels;
+static volatile uint8_t serial_writechannels;
+static volatile uint8_t serial_readconfig;
+
+static volatile struct dac {
+  uint16_t value[2];
+} dac;
+
+static volatile struct adc {
+  uint32_t value[4];
+} adc;
+
+static volatile struct calibration {
+  uint8_t not_valid; // Non-zero if eeprom not written 
+  struct {
+    uint16_t offset;
+    int16_t min;
+    int16_t max;
+  } channel[2];
+} calibration;
+  
+static uint8_t ADS8694_W(
+  uint8_t length,
+  const __flash uint8_t *data)
+{
+  DDR_SPI |= (1<<DD_MOSI) | (1<<DD_SCK) | (1<<DD_SS);
+  SPCR &= (1<<SPIE);
+  SPCR = (1<<SPE) | (1<<MSTR) | (1<<CPHA);
+  SPSR = (1<<SPI2X);
+  PORT_SPI |= (1<<DD_MISO); // Pull up
+  
+  PORT_SPI &= ~(1<<DD_SS);
+  for (uint8_t i = 0 ; i < length ; i++) {
+    SPDR = data[i];
+    while ((SPSR & (1<<SPIF)) == 0) {}
+  }
+  uint8_t dummy = SPDR; // Clear SPIF flag
+  PORT_SPI |= (1<<DD_SS);
+  return dummy;
+}
+
+/*
+ * DAC8574 4-channel D/A convert
+ */
+
+#define DAC8574ADDR 0x4c
+
+static volatile struct dac8574 {
+  union dac8574_channel {
+    uint8_t bytes[3];
+    struct {
+      uint8_t control;
+      uint8_t data[2];
+    } reg;
+  } a, b, c, d;
+} dac8574 = {
+  .a={ .reg={ .control=0x10 } },
+  .b={ .reg={ .control=0x12 } },
+  .c={ .reg={ .control=0x14 } },
+  .d={ .reg={ .control=0x16 } },
+};
+
+static void dac8574_set_chan(
+  volatile union dac8574_channel *channel,
+  int value)
+{
+  channel->reg.data[0] = (value & 0xff00) >> 8;
+  channel->reg.data[1] = (value & 0x00ff);
+}
+
+static const __flash struct i2c_transcation write_all[] = {
+  I2C_WRITE_RAM(DAC8574ADDR, 3, dac8574.a.bytes),
+  I2C_WRITE_RAM(DAC8574ADDR, 3, dac8574.b.bytes),
+  I2C_WRITE_RAM(DAC8574ADDR, 3, dac8574.c.bytes),
+  I2C_WRITE_RAM(DAC8574ADDR, 3, dac8574.d.bytes),
+  I2C_END()
+};
+
+static void handle_calibration(uint32_t value)
+{
+  unsigned char pos = value & 0x7f;
+  unsigned char sign = (value >> 7) & 0x01;
+  int32_t v = value >> 8;
+  if (sign) {
+    v = -v;
+  }
+  switch (pos) {
+    case 0: calibration.channel[0].offset = v; break;
+    case 1: calibration.channel[1].offset = v; break;
+    case 2: calibration.channel[0].min = v; break;
+    case 3: calibration.channel[0].max = v; break;
+    case 4: calibration.channel[1].min = v; break;
+    case 5: calibration.channel[1].max = v; break;
+    case 6: eeprom_write_block((void*)&calibration, 0, sizeof(calibration)); break;
+  }
+}
+
+SIGNAL(USART_RXC_vect)
+{
+  char ch = UDR;
+  
+  switch (serialio_RXC(ch)) {
+    case serialio_clearbit: {
+      // Ignored
+    } break;
+   case serialio_setbit: {
+     // Ignored
+    } break;
+    case serialio_pollbit: {
+      // Ignored
+    } break;
+    case serialio_pollchannel: { 
+      if (serialio_channel < 4) {
+        serial_readchannels |= (1<<serialio_channel); 
+      } else if (serialio_channel == 31) { 
+        serial_readconfig = 1; 
+      }
+    } break;
+    case serialio_setchannel: {
+      if (serialio_channel < 2) {
+        dac.value[serialio_channel] = serialio_value;
+        serial_writechannels |= (1<<serialio_channel); 
+      } else if (serialio_channel == 31) {
+        handle_calibration(serialio_value);
+      }
+    } break;
+    case serialio_error: {
+    } break;
+    case serialio_more: {
+    } break;
+  }
+}
+
+SIGNAL(TIMER1_COMPA_vect)
+{
+  unsigned char data[5];
+
+  SPCR = (1<<SPE) | (1<<MSTR) | (1<< CPHA);
+  PORT_SPI &= ~(1<<DD_SS); // Chip select
+  SPDR = 0x00; // NOP
+  for (int i = 0 ; i < 5 ; i++) {
+    while ((SPSR & 0x80) == 0) { }
+    data[i] = SPDR;
+    if (i < 4) {
+      // More to read
+      SPDR = 0;
+    }
+  }
+  PORT_SPI |= (1<<DD_SS); // Chip deselect
+
+  union {
+    int32_t i;
+    unsigned char b[4];
+  } v;
+  uint8_t chan;
+  v.i = 0;
+  v.b[2] = data[2];
+  v.b[1] = data[3];
+  v.b[0] = data[4];
+  chan = (v.i>>2) & 0xf;
+  adc.value[chan] = (v.i>>6);
+}
+
+static uint32_t conf_millivolt(uint32_t value)
+{
+  if (value < 0) {
+    return CONF_NEGATIVE_MILLIVOLT(value);
+  } else {
+    return CONF_POSITIVE_MILLIVOLT(value);
+  }
+}
+
+int main() 
+{
+  eeprom_read_block((void*)&calibration, 0, sizeof(calibration));
+  if (calibration.not_valid) {
+    calibration.not_valid = 0;
+    calibration.channel[0].offset = 0x8000;
+    calibration.channel[0].min = -10000;
+    calibration.channel[0].max = 10000;
+    calibration.channel[1].offset = 0x8000;
+    calibration.channel[1].min = -10000;
+    calibration.channel[1].max = 10000;
+  }
+
+  /* Setup serial port */
+  UCSRA = 0x00;     // USART: 
+  UCSRB = 0x98;     // USART: RxIntEnable|RxEnable|TxEnable 
+  UCSRC = 0x86;     // USART: 8bit, no parity 
+  UBRRH = 0;        // USART: 115200 @ 14.7456MHz
+  UBRRL = 7;        // USART: 115200 @ 14.7456MHz 
+
+  /* Setup timer1 */
+  TCCR1A = 0x00;    // Normal port mode (no output)
+  TCCR1B = 0x09;    // Clock / 1
+  OCR1A = 0x400;    // 14.7456MHz / 1024 = 14.4kHz
+  TIMSK =  0x10;    // Enable Timer1 compare A interrupts 
+
+  /* Setup I2C (TWI), used by DAC8574 */
+  TWBR = 0x0a;      // Max allowed master speed 
+  TWSR = 0x00;
+
+  /* Setup DAC8574 (SPI setup is done in ADS8694_W) */
+  static const __flash uint8_t RST[] = { 0x85, 0x00, 0x00 };
+  static const __flash uint8_t SEQUENCE[] = { (0x01<<1)|0x01, 0x0f, 0x00 };
+  static const __flash uint8_t FEATURES[] = { (0x03<<1)|0x01, 0x03, 0x00 };
+  static const __flash uint8_t AUTO_RST[] = { 0xa0, 0x00, 0x00, 0x00, 0x00 };
+  ADS8694_W(3, RST);
+  ADS8694_W(3, SEQUENCE);
+  ADS8694_W(3, FEATURES);
+  ADS8694_W(5, AUTO_RST);
+
+  /* Setup serialio communication */
+  serialio_init();
+  serial_readchannels = 0;
+  serial_readconfig = 0;
+
+  /* Setup DAC values */
+  dac.value[0] = 0x8000;
+  dac.value[1] = 0x8000;
+  
+  sei(); // Global interrupt enable 
+
+  unsigned char readchannels = 0;
+  while (1) {
+    unsigned char config;
+    struct adc local_adc;
+
+    cli(); // Global interrupt disable
+    if (readchannels == 0) {
+      readchannels = serial_readchannels;
+      serial_readchannels = 0;
+    }
+    config = serial_readconfig;
+    serial_readconfig = 0;
+    local_adc = adc;
+    sei(); // Global interrupt enable 
+    if (readchannels & 0x01) {
+      serialio_putchannel(0, local_adc.value[0]);
+      readchannels &= ~0x01;
+    } else if (readchannels & 0x02) {
+      serialio_putchannel(1, local_adc.value[1]);
+      readchannels &= ~0x02;
+    } else if (readchannels & 0x04) {
+      serialio_putchannel(2, local_adc.value[2]);
+      readchannels &= ~0x04;
+    } else if (readchannels & 0x08) {
+      serialio_putchannel(3, local_adc.value[3]);
+      readchannels &= ~0x08;
+    }
+    if (i2c_idle()) {
+      cli();
+      dac8574_set_chan(&dac8574.a, calibration.channel[0].offset); 
+      dac8574_set_chan(&dac8574.b, calibration.channel[0].offset);
+      dac8574_set_chan(&dac8574.c, dac.value[0]);
+      dac8574_set_chan(&dac8574.d, dac.value[1]);
+      sei();
+      i2c_start(write_all);
+    }
+   
+    if (config) {
+      CONF_ANALOG_IN(0, CONF_RESOLUTION(18));	// Analog In 0
+      CONF_ANALOG_IN(0, CONF_MIN(CONF_NEGATIVE_MILLIVOLT(10000)));
+      CONF_ANALOG_IN(0, CONF_MAX(CONF_POSITIVE_MILLIVOLT(10000)));
+      CONF_ANALOG_IN(1, CONF_RESOLUTION(18));	// Analog In 1
+      CONF_ANALOG_IN(1, CONF_MIN(CONF_NEGATIVE_MILLIVOLT(10000)));
+      CONF_ANALOG_IN(1, CONF_MAX(CONF_POSITIVE_MILLIVOLT(10000)));
+      CONF_ANALOG_IN(2, CONF_RESOLUTION(18));	// Analog In 2
+      CONF_ANALOG_IN(2, CONF_MIN(CONF_NEGATIVE_MILLIVOLT(10000)));
+      CONF_ANALOG_IN(2, CONF_MAX(CONF_POSITIVE_MILLIVOLT(10000)));
+      CONF_ANALOG_IN(3, CONF_RESOLUTION(18));	// Analog In 3
+      CONF_ANALOG_IN(3, CONF_MIN(CONF_NEGATIVE_MILLIVOLT(10000)));
+      CONF_ANALOG_IN(3, CONF_MAX(CONF_POSITIVE_MILLIVOLT(10000)));
+
+      CONF_ANALOG_OUT(0, CONF_RESOLUTION(16));	// Analog Out 0
+      //CONF_ANALOG_OUT(0, CONF_MIN(CONF_NEGATIVE_MILLIVOLT(10000)));
+      //CONF_ANALOG_OUT(0, CONF_MAX(CONF_POSITIVE_MILLIVOLT(10000)));
+      CONF_ANALOG_OUT(0, CONF_MIN(conf_millivolt(calibration.channel[0].min)));
+      CONF_ANALOG_OUT(0, CONF_MAX(conf_millivolt(calibration.channel[0].max)));
+      CONF_ANALOG_OUT(1, CONF_RESOLUTION(16));	// Analog Out 1
+      CONF_ANALOG_OUT(1, CONF_MIN(conf_millivolt(calibration.channel[1].min)));
+      CONF_ANALOG_OUT(1, CONF_MAX(conf_millivolt(calibration.channel[1].max)));
+
+      CONF_END();
+    }
+  }
+}
+
diff --git a/china_io-2019/python/calibrator.py b/china_io-2019/python/calibrator.py
new file mode 100755
index 0000000000000000000000000000000000000000..1a0965ca477446b7253ccd9e50a0236d3f9ee775
--- /dev/null
+++ b/china_io-2019/python/calibrator.py
@@ -0,0 +1,237 @@
+#!/usr/bin/python3
+
+import sys
+import time
+import numpy
+import traceback
+from serialio import SerialIO
+import math
+
+class Welford:
+
+    def __init__(self, value):
+        self.k = 0
+        self.M = 0
+        self.S = 0
+        self.update(value)
+        pass
+    
+    def update(self, value):
+        self.k += 1
+        newM = self.M + (value - self.M) / self.k
+        newS = self.S + (value - self.M) * (value - newM)
+        self.M = newM
+        self.S = newS
+
+    def mean(self):
+        return self.M
+    
+    def stddev(self):
+        if self.k==1:
+            return 0
+        return math.sqrt(self.S/(self.k-1))
+
+    def __repr__(self):
+        return "%f +- %f" % (self.mean(), self.stddev())
+    
+
+class ChinaIO:
+    ANALOG_OUT_0 = 0
+    ANALOG_OUT_1 = 1
+    AIN_STEP = 20/(1<<18) # 20V, 18 bit resolution
+    AOUT_STEP = 20/(1<<16) # 20V, 18 bit resolution
+
+    def __init__(self, port):
+        self.io = SerialIO(port)
+        self.action_msg = ""
+        self.status_msg = ""
+        pass
+    
+    def close(self):
+        self.io.setchannel(self.ANALOG_OUT_0, 0x8000)
+        self.io.setchannel(self.ANALOG_OUT_1, 0x8000)
+
+    def measure(self, analogIn, N=100, message=''):
+        time.sleep(0.1)
+        stat = {}
+        for b in range(N):
+            print(message, '[',  end='')
+            for i,c in analogIn.items():
+                value = (self.io.getchannel(i) - (1<<17)) * self.AIN_STEP
+                if not i in stat:
+                    stat[i] = Welford(value)
+                    pass
+                else:
+                    stat[i].update(value)
+                    pass
+                print('%s ' % stat[i], end='')
+                pass
+            print(']', end='\r')
+            pass
+        print()
+        return [ stat[i] for i in sorted(stat.keys()) ]
+
+    def setCalibration(self, index, value):
+        if value > 0:
+            self.io.setchannel(31, (value << 8) | index & 0x3f)
+            pass
+        else:
+            self.io.setchannel(31, (-value << 8) | 0x80 | index & 0x7f)
+            pass
+        pass
+    
+    def setVoltage(self, channel, value):
+        self.io.setchannel(channel, int(value/ self.AOUT_STEP + (1<<15)))
+        
+    def expect(self, analogIn, ref, expect, N, message):
+        for i,r in zip(range(100), ref):
+            if r != None:
+                self.setVoltage(i, r)
+                pass
+            pass
+        time.sleep(0.1) # Let values settle
+        sample = self.measure(analogIn, N=N, message=message)
+        for e,v in zip(expect, sample):
+            if e != None and (v.mean() < e[0] or e[1] < v.mean()):
+                print("      Expected:", expect)
+                return False
+            pass
+        return True
+        
+    def calibrate(self):
+        # Calibration steps:
+        #   1. Sanity check returned channels
+        #   2. Set offset calibration to default value
+        #   3. Read values from grounded inputs
+        #   4. Connect AOut0 -> AIn0,AIn2 (+9V)
+        #   5. Connect AOut1 -> AIn1,AIn3 (-9V)
+        #   6. Zero adjust
+        #   7. Determine min and max values
+        
+        # Sanity check
+        analogIn = self.io.analogIn()
+        analogOut = self.io.analogOut()
+        if len(analogIn) != 4:
+            raise Exception('Did not find 4 analogIn channels')
+        if len(analogOut) != 2:
+            raise Exception('Did not find 2 analogOut channels')
+        for i,c in analogIn.items():
+            if c.bits != 18:
+                raise Exception('analogIn[%d] is not 18 bit (%d)' % (i, c.bits))
+            pass
+
+        # Set offset calibration to default value
+        # self.io.setchannel(31, (0x8000 << 8) | 0x00)
+        # self.io.setchannel(31, (0x8000 << 8) | 0x00)
+        self.setCalibration(0, 0x8000)
+        self.setCalibration(1, 0x8000)
+
+        # Read values from grounded inputs
+        while not self.expect(analogIn,
+                              [ -9, 9 ], 
+                              [ (-0.1, 0.1), (-0.1, 0.1),
+                                (-0.1, 0.1), (-0.1, 0.1) ],
+                              N=500,
+                              message='Ground all input pins'):
+            break
+            pass
+
+        # Connect AOut0 -> AIn0,AIn2 (+9V)
+        while not self.expect(analogIn,
+                              [ 9, None ], 
+                              [ (8.7,9.3), None,
+                                (8.7,9.3), None ],
+                              N=500,
+                              message='Connect: AOut0 -> AIn0, AIn2'):
+            pass
+
+        # Connect AOut1 -> AIn1,AIn3 (-9V)
+        while not self.expect(analogIn,
+                              [ None, -9 ], 
+                              [ None, (-9.3,-8.7),
+                                None, (-9.3,-8.7) ],
+                              N=500,
+                              message='Connect: AOut1 -> AIn1, AIn3'):
+            pass
+
+        # Zero adjust
+        while not self.expect(analogIn,
+                              [ 0, 0 ], 
+                              [ (-0.1, 0.1), (-0.1, 0.1),
+                                (-0.1, 0.1),(-0.1, 0.1) ],
+                              N=500,
+                              message='Calibrating zero'):
+            pass
+
+        #   5. Zero adjust
+        v0 = 0x8000
+        v1 = 0x8000
+        delta = 0x4000
+        self.io.setchannel(0, 0x8000)
+        self.io.setchannel(1, 0x8000)
+        self.setCalibration(0, v0)
+        self.setCalibration(1, v1)
+        sample = self.measure(analogIn, N=100, message='Sampling baseline')
+        def nextv(oldv, delta, sample):
+            if sample.mean() > 0:
+                return oldv + delta
+            else:
+                return oldv - delta
+            pass
+        candidate = [[], []]
+        while delta >= 1:
+            v0 = nextv(v0, delta, sample[0])
+            v1 = nextv(v1, delta, sample[1])
+            self.setCalibration(0, v0)
+            self.setCalibration(1, v1)
+            sample = self.measure(analogIn, N=200,
+                                  message='Zero (v0=%x, v1=%x)' % (v0, v1))
+            candidate[0].append([v0, sample[0]])
+            candidate[1].append([v1, sample[1]])
+            delta = delta // 2
+            pass
+        best = [ sorted(candidate[0], key=lambda x: abs(x[1].mean())),
+                 sorted(candidate[1], key=lambda x: abs(x[1].mean())) ]
+        print("Done %x %x : %x %x" % (v0, v1, best[0][0][0], best[1][0][0]))
+        v0 = best[0][0][0]
+        v1 = best[0][0][0]
+        self.setCalibration(0, v0)
+        self.setCalibration(1, v1)
+
+        # Determine min and max values
+        self.setVoltage(0, -9)
+        self.setVoltage(1, -9)
+        min_sample = self.measure(analogIn, N=200, message='Min (-9V)')
+        self.setVoltage(0, 9)
+        self.setVoltage(1, 9)
+        max_sample = self.measure(analogIn, N=200, message='Max (+9V)')
+        print("0: Min: %f, Max: %f" % (min_sample[0].mean() / 9 * 10,
+                                       max_sample[0].mean() / 9 * 10))
+        print("1: Min: %f, Max: %f" % (min_sample[1].mean() / 9 * 10,
+                                       max_sample[1].mean() / 9 * 10))
+        def millivolt(v):
+            return int(v * 1000)
+        self.setCalibration(2, millivolt(min_sample[0].mean() / 9 * 10))
+        self.setCalibration(3, millivolt(max_sample[0].mean() / 9 * 10))
+        self.setCalibration(4, millivolt(min_sample[1].mean() / 9 * 10))
+        self.setCalibration(5, millivolt(max_sample[1].mean() / 9 * 10))
+        self.setCalibration(6, 0);
+        pass
+        
+    def write_calibration(self, index, value):
+        if value < 0:
+            tmp = (int(-value * 1000) << 8) | 0x80 | index
+        else:
+            tmp = (int(value * 1000) << 8) | index
+        print("%d %8x" % (index, tmp))
+        self.io.setchannel(31, tmp, 0xffffffff)
+        
+if __name__ == "__main__":
+    io = ChinaIO(sys.argv[1])
+    try:
+        io.calibrate()
+    except:
+        traceback.print_exc()
+        pass
+
+    io.close()
diff --git a/china_io-2019/python/serialio.py b/china_io-2019/python/serialio.py
new file mode 100644
index 0000000000000000000000000000000000000000..c2ff52622f61b65055ce249a4c860a98eafe23da
--- /dev/null
+++ b/china_io-2019/python/serialio.py
@@ -0,0 +1,227 @@
+#!/usr/bin/python3
+
+import serial
+import threading
+import struct
+
+class IO:
+
+    def __init__(self):
+        self.min = None
+        self.max = None
+        self.bits = None
+
+    def parseMinMax(self, bits):
+        unit = bits & 0x07
+        sign = (bits >> 3) & 0x01
+        value = (bits >> 4)
+        if unit == 0b000 or unit == 0b100:
+            result = value
+            pass
+        elif unit == 0b001:
+            result = 1e-3 * value
+            pass
+        elif unit == 0b010:
+            result = 1e-6 * value
+            pass
+        else:
+            result = value
+            pass
+        if sign == 1:
+            result = -result
+        return result
+    
+    def setBits(self, bits):
+        self.bits = bits
+        
+    def setMin(self, min):
+        self.min = self.parseMinMax(min)
+        
+    def setMax(self, max):
+        self.max = self.parseMinMax(max)
+
+    def __repr__(self):
+        return "bits:%s min:%s max:%s" % (self.bits, self.min, self.max)
+        
+class DigitalIn(IO):
+    pass
+
+class DigitalOut(IO):
+    pass
+
+class AnalogIn(IO):
+    pass
+
+class AnalogOut(IO):
+    pass
+
+class Counter(IO):
+    pass
+
+class SerialIOConfig:
+    CMD = {
+        0: IO.setBits,
+        1: IO.setMin,
+        2: IO.setMax,
+    }
+
+    KIND = {
+        0: None,
+        1: DigitalIn,
+        2: DigitalOut,
+        3: AnalogIn,
+        4: AnalogOut,
+        5: Counter
+    }
+
+    def __init__(self, serialio):
+        self.serialio = serialio
+        self.config = {}
+        self.cond = threading.Condition()
+        pass
+
+    def digitalIn(self):
+        return self.config[DigitalIn]
+
+    def digitalOut(self):
+        return self.config[DigitalOut]
+
+    def analogIn(self):
+        return self.config[AnalogIn]
+
+    def analogOut(self):
+        return self.config[AnalogOut]
+
+    def counter(self):
+        return self.config[Counter]
+
+    def __str__(self):
+        return "%s" % self.config
+    
+    def get(self):
+        self.serialio.pollchannel(31)
+        self.cond.acquire()
+        self.cond.wait(2)
+        self.cond.release()
+        pass
+    
+    def handle(self, value):
+        channel = value & 0x1f
+        kind = (value >> 5) & 0x07
+        cmd = (value >> 8) & 0x03
+        value = value >> 10
+        kind = self.KIND[kind]
+        if kind == None:
+            self.cond.acquire()
+            self.cond.notifyAll()
+            self.cond.release()
+            return True
+        else:
+            if not kind in self.config:
+                self.config[kind] = {}
+                pass
+            if not channel in self.config[kind]:
+                self.config[kind][channel] = kind()
+                pass
+            cmd = self.CMD[cmd]
+            cmd(self.config[kind][channel], value)
+            pass
+        pass
+
+    pass
+    
+    
+class SerialIO:
+    def __init__(self, port):
+        self.channel = {}
+        self.tty = serial.Serial(port, 115200)
+        self.config = SerialIOConfig(self)
+        self.cond = threading.Condition()
+        t = threading.Thread(target=self.read)
+        t.setDaemon(True)
+        t.start()
+        self.config.get()
+
+    def digitalIn(self):
+        return self.config.digitalIn()
+
+    def digitalOut(self):
+        return self.config.digitalOut()
+
+    def analogIn(self):
+        return self.config.analogIn()
+
+    def analogOut(self):
+        return self.config.analogOut()
+
+    def counter(self):
+        return self.config.counter()
+
+    def pollchannel(self, index):
+        self.cond.acquire()
+        self.channel[index] = None
+        self.tty.write(struct.pack('B', (0x60 | index)))
+        self.cond.release()
+
+    def waitchannel(self, index):
+        self.cond.acquire()
+        while self.channel[index] == None:
+            self.cond.wait()
+        result = self.channel[index]
+        self.cond.release()
+        return result
+
+    def getchannel(self, index):
+        self.pollchannel(index)
+        return self.waitchannel(index)
+    
+    def setchannel(self, index, value):
+        #value = int(max(0, min(value, bound)))
+        self.cond.acquire()
+        if value >= (1<<30):
+            self.tty.write(struct.pack('B', 0x80 | ((value >> 30) & 0x03)))
+        if value >= (1<<23):
+            self.tty.write(struct.pack('B', 0x80 | ((value >> 23) & 0x7f)))
+        if value >= (1<<16):
+            self.tty.write(struct.pack('B', 0x80 | ((value >> 16) & 0x7f)))
+        if value >= (1<<9):
+            self.tty.write(struct.pack('B', 0x80 | ((value >> 9) & 0x7f)))
+        self.tty.write(struct.pack('B', 0x80 | ((value >> 2) & 0x7f)))
+        self.tty.write(struct.pack('B', ((value << 5) & 0x60) | (index & 0x1f)))
+        self.cond.release()
+        pass
+
+    def read(self):
+        value = 0
+        n = 0
+        config = {}
+        while True:
+            ch = self.tty.read(1)
+            
+            value = value << 7 | ord(ch) & 0x7f
+            n += 1
+            if ord(ch) & 0x80 == 0:
+                # Last byte, so lets handle it
+                if n == 1:
+                    # Digital I/O or poll
+                    pass
+                else:
+                    channel = value & 0x1f
+                    value = value >> 5
+                    if channel != 31:
+                        self.cond.acquire()
+                        self.channel[channel] = value
+                        self.cond.notifyAll()
+                        self.cond.release()
+                        pass
+                    else:
+                        self.config.handle(value)
+                        pass
+                    pass
+                value = 0
+                n = 0
+                pass
+            pass
+        pass
+    
+    pass