diff --git a/quadtank/avr/Makefile b/quadtank/avr/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..44e1f07816e54ba4befef1609a8e95a3b5edff05
--- /dev/null
+++ b/quadtank/avr/Makefile
@@ -0,0 +1,21 @@
+TARGETS=quadtank quadtank_32
+
+quadtank.ARCH=avr
+quadtank.CHIP=atmega16
+# 14.7456 MHz crystal, brown out
+quadtank.FUSE=--wr_fuse_l=0x1f --wr_fuse_h=0xd9 --wr_fuse_e=0xff
+quadtank.FUSE_L=0x1f
+quadtank.FUSE_H=0xd9
+quadtank.C=quadtank
+quadtank.H=../../lib/avr/serialio
+
+quadtank_32.ARCH=avr
+quadtank_32.CHIP=atmega32
+# 14.7456 MHz crystal, brown out
+quadtank_32.FUSE=--wr_fuse_l=0x1f --wr_fuse_h=0xd9 --wr_fuse_e=0xff
+quadtank_32.FUSE_L=0x1f
+quadtank_32.FUSE_H=0xd9
+quadtank_32.C=quadtank
+quadtank_32.H=../../lib/avr/serialio
+
+include ../../lib/avr/Makefile.common
diff --git a/quadtank/avr/calibrator b/quadtank/avr/calibrator
new file mode 100755
index 0000000000000000000000000000000000000000..bd76419e3bbf2caa35c24c6306fd6285795b3222
--- /dev/null
+++ b/quadtank/avr/calibrator
@@ -0,0 +1,466 @@
+#!/usr/bin/python3
+
+import serial
+import sys
+import time
+import numpy
+import traceback
+import threading
+import os
+
+class Opcom:
+    """ Layout:
+          Scroll line 1
+          ...
+          Scroll line N-2
+          Pressures
+          Action     Status
+    """
+    def __init__(self, f=sys.stdout):
+        self.f = os.fdopen(f.fileno(), 'bw')
+        self.f.write(b'\n')
+        self._pressures = b''
+        self._action = b''
+        self._status = b''
+        
+    def up_left(self, n):
+       self.f.write(b'\033[%dA\r' % n)
+        
+    def down_left(self, n):
+       self.f.write(b'\033[%dB\r' % n)
+        
+    def progress(self, msg):
+        # Row 0..N-2
+        self.up_left(1)
+        self.f.write(b' ' * len(self._pressures) + b'\r')
+        self.f.write(msg.encode('ascii') + b'\n')
+        self.f.write(b' ' * len(self._action) + b'\r')
+        self.f.write(self._pressures + b'\n')
+        self.f.write(self._action + b' ' * (40 - len(self._action)))
+        self.f.write(self._status + b'\r')
+        self.f.flush()
+        pass
+    
+    def pressures(self, msg):
+        # Row N-1
+        self.up_left(1)
+        self.f.write(msg.encode('ascii') +
+                     b' ' * (len(self._pressures) - len(msg)))
+        self.down_left(1)
+        self._pressures = msg.encode('ascii')
+        self.f.flush()
+        pass
+    
+    def action(self, msg):
+        # Row N, 0..39
+        self.f.write(b'\r')
+        self.f.write(msg.encode('ascii') +
+                     b' ' * (len(self._action) - len(msg)) + b'\r')
+        self._action = msg.encode('ascii')
+        self.f.flush()
+        pass
+    
+    def status(self, msg):
+        # Row N, 40..
+        self.f.write(b'\r')
+        self.f.write(b' ' * (40 + len(self._status)) + b'\r')
+        self.f.write(self._action + b' ' * (40 - len(self._action)))
+        self.f.write(msg.encode('ascii') + b'\r')
+        self._status = msg.encode('ascii')
+        self.f.flush()
+        pass
+    
+KIND = {
+    0: None,
+    1: "DigitalIn",
+    2: "DigitalOut",
+    3: "AnalogIn",
+    4: "AnalogOut",
+    5: "Counter"
+    }
+CMD = {
+    0: "Bits",
+    1: "Min",
+    2: "Max",
+    }
+
+class SerialIO:
+    def __init__(self, port):
+        self.config = None
+        self.channel = {}
+        self.tty = serial.Serial(port, 115200)
+        self.cond = threading.Condition()
+        t = threading.Thread(target=self.read)
+        t.setDaemon(True)
+        t.start()
+        self.pollchannel(31)
+        self.cond.acquire()
+        self.cond.wait(2)
+        self.cond.release()
+
+    def pollchannel(self, index):
+        self.cond.acquire()
+        self.channel[index] = None
+        self.tty.write(bytes([0x60 | index]))
+        self.cond.release()
+
+    def getchannel(self, index):
+        self.cond.acquire()
+        while self.channel[index] == None:
+            self.cond.wait()
+        result = self.channel[index]
+        self.cond.release()
+        return result
+
+    def readchannel(self, index):
+        self.pollchannel(index)
+        return self.getchannel(index)
+    
+    def setchannel(self, index, value, bound=0x3ff):
+        value = int(max(0, min(value, bound)))
+        self.cond.acquire()
+        if value >= (1<<30):
+            self.tty.write(bytes([0x80 | ((value >> 30) & 0x03)]))
+        if value >= (1<<23):
+            self.tty.write(bytes([0x80 | ((value >> 23) & 0x7f)]))
+        if value >= (1<<16):
+            self.tty.write(bytes([0x80 | ((value >> 16) & 0x7f)]))
+        if value >= (1<<9):
+            self.tty.write(bytes([0x80 | ((value >> 9) & 0x7f)]))
+        self.tty.write(bytes([0x80 | ((value >> 2) & 0x7f)]))
+        self.tty.write(bytes([((value << 5) & 0x60) | (index & 0x1f)]))
+        self.cond.release()
+
+    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()
+                    else:
+                        if self.handleconfig(value, config):
+                            self.cond.acquire()
+                            self.config = config
+                            self.cond.notifyAll()
+                            self.cond.release()
+                value = 0
+                n = 0
+                
+    def handleconfig(self, value, config):
+        channel = value & 0x1f
+        kind = (value >> 5) & 0x07
+        cmd = (value >> 8) & 0x03
+        value = value >> 10
+        if KIND[kind] == None:
+            return True
+        else:
+            try:
+                config[KIND[kind]]
+            except:
+                config[KIND[kind]] = {}
+            try:
+                config[KIND[kind]][channel]
+            except:
+                config[KIND[kind]][channel] = {}
+                
+            config[KIND[kind]][channel][CMD[cmd]] = value
+                
+class QuadTank:
+    NOISE = 10
+    A_PUMP = 0
+    B_PUMP = 1
+    A1_LEVEL = 0
+    A2_LEVEL = 1
+    B1_LEVEL = 2
+    B2_LEVEL = 3
+    A_FLOW = 4
+    B_FLOW = 5
+    P_LABEL = [ "A1", "A2", "B1", "B2", "AFlow", "BFlow" ]
+    
+    def __init__(self, port, opcom):
+        self.io = SerialIO(port)
+        self.opcom = opcom
+        self.get_pressures()     
+        
+    def set_pump_voltage(self, voltage_A=0, voltage_B=0):
+        self.voltage_A = voltage_A
+        self.voltage_B = voltage_B
+        self.io.setchannel(self.A_PUMP, voltage_A)
+        self.io.setchannel(self.B_PUMP, voltage_B)
+
+    def get_pressures(self):
+        p = numpy.array([self.io.readchannel(self.A1_LEVEL),
+                         self.io.readchannel(self.A2_LEVEL),
+                         self.io.readchannel(self.B1_LEVEL),
+                         self.io.readchannel(self.B2_LEVEL),
+                         self.io.readchannel(self.A_FLOW),
+                         self.io.readchannel(self.B_FLOW)])
+        msg = ''
+        for i in range(len(p)):
+            msg += 'P%d/%s=%4d ' % (i, self.P_LABEL[i], p[i])
+        self.opcom.pressures(msg)
+        return p
+
+    def attention(self, msg="", voltage_A=1023, voltage_B=1023):
+        self.opcom.action(msg)
+        self.set_pump_voltage(voltage_A, voltage_B)
+        time.sleep(0.3)
+        self.set_pump_voltage(0, 0)
+        time.sleep(2)
+        
+    def drain(self):
+        # Drained when all deltas are below noise for 2 seconds
+        self.set_pump_voltage(0, 0)
+        old = self.get_pressures()
+        steady = 0
+        while steady < 4:
+            time.sleep(0.5)
+            new = self.get_pressures()
+            
+            diff = numpy.abs(new - old)
+            old = new
+            self.opcom.status("Draining %d/4 (diff=%d)" % (steady, diff.max()))
+            if diff.max() < self.NOISE and new[0:4].max() < 200:
+                steady = steady + 1
+                self.opcom.action("")
+            else:
+                steady = 0
+                msg = ""
+                name = [ "AV3", "AV4", "BV3", "BV4" ]
+                for i in range(0,4):
+                    if diff[i] < self.NOISE and new[i] > 200:
+                        msg += name[i] + "/"
+                if len(msg):
+                    self.attention("Open %s" % msg[0:-1])
+                else:
+                    self.opcom.action("")
+        self.opcom.action("")
+        self.opcom.status("")
+
+    def check_valves(self):
+        self.opcom.progress("Checking valves")
+        # Fill tanks with some water
+        self.set_pump_voltage(1023, 1023)
+        time.sleep(3)
+        restart = True
+        while restart:
+            restart = False
+            self.drain()
+            targets = [ type("", (),
+                             dict(voltage_A = 1023,
+                                  voltage_B = 0,
+                                  top = self.A1_LEVEL,
+                                  bottom = self.A2_LEVEL,
+                                  decoupled = self.B2_LEVEL,
+                                  msg_open_upper = "Open AV3",
+                                  msg_open_lower = "Open AV4",
+                                  msg_close = "Close V5/BV1")),
+                        type("", (),
+                             dict(voltage_A = 0,
+                                  voltage_B = 1023,
+                                  top = self.B1_LEVEL,
+                                  bottom = self.B2_LEVEL,
+                                  decoupled = self.A2_LEVEL,
+                                  msg_open_upper = "Open BV3",
+                                  msg_open_lower = "Open BV4",
+                                  msg_close = "Close V5/AV1"))]
+            for t in targets:
+                self.opcom.action("")
+                if restart:
+                    break
+                self.set_pump_voltage(t.voltage_A, t.voltage_B)
+                t0 = time.time()
+                p0 = self.get_pressures()
+                drain_decoupled = False
+                drain_bottom = False
+                while True:
+                    time.sleep(0.2)
+                    p = self.get_pressures()
+                    diff = p - p0
+                    if diff[t.decoupled] > self.NOISE:
+                        self.opcom.action(t.msg_close)
+                        drain_decoupled = True
+                    elif drain_decoupled:
+                        if diff[t.decoupled] < self.NOISE:
+                            restart = True
+                            break
+                    elif diff[t.top] > 400 and diff[t.bottom] < self.NOISE:
+                        self.opcom.action(t.msg_open_upper)
+                    elif not drain_bottom and diff[t.bottom] > 200:
+                        drain_bottom = True
+                        self.set_pump_voltage()
+                        self.opcom.action(t.msg_open_lower)
+                    elif drain_bottom:
+                        if diff[t.bottom] < self.NOISE:
+                            break
+            
+    def calibrated_flow(self):
+        self.opcom.progress("Calibrating flow")
+        voltage_A = 100
+        voltage_B = 100
+        done = False
+        while not done:
+            done = True
+            self.set_pump_voltage(voltage_A, voltage_B)
+            time.sleep(1)
+            p = self.get_pressures()
+            if p[self.A_FLOW] < 180:
+                voltage_A = voltage_A + 20
+                done = False
+            if p[self.B_FLOW] < 180:
+                voltage_B = voltage_B + 20
+                done = False
+        old = self.get_pressures()
+        steady = 0
+        sum = numpy.array([0, 0])
+        n = 0
+        while steady < 4:
+            new = self.get_pressures()
+            sum = sum + new[4:6]
+            n += 1
+            if n % 20 == 0:
+                diff = numpy.abs(new - old)[0:4].max()
+                old = new
+                self.opcom.status("Calibrating flow %d/4 (diff=%d)" %
+                                  (steady, diff))
+                if diff < self.NOISE:
+                    steady = steady + 1
+                else:
+                    steady = 0
+            time.sleep(0.1)
+        self.opcom.status("")
+        return sum / n
+    
+    def max_levels(self):
+        self.opcom.progress("Getting max levels")
+        old = self.get_pressures()[0:4]
+        voltage_A = self.voltage_A
+        voltage_B = self.voltage_B
+        low = True
+        while True:
+            time.sleep(0.5)
+            low = not low
+            if low:
+                self.set_pump_voltage(voltage_A - 40, voltage_B - 40)
+            else:
+                self.set_pump_voltage(voltage_A, voltage_B)
+            new = self.get_pressures()[0:4]
+            diff = (new - old)
+            if diff[self.A2_LEVEL] < 100 and diff[self.B2_LEVEL] < 100:
+                self.opcom.action("Close AV4/BV4")
+            elif diff[self.A2_LEVEL] < 100:
+                self.opcom.action("Close AV4")
+            elif diff[self.B2_LEVEL] < 100:
+                self.opcom.action("Close BV4")
+            else:
+                break
+        self.opcom.action("")
+        self.set_pump_voltage(voltage_A=1023, voltage_B=1023)
+        old = self.get_pressures()[0:4]
+        steady = 0
+        while steady < 4:
+            new = self.get_pressures()[0:4]
+            diff = numpy.abs(new - old).max()
+            old = new
+            self.opcom.status("Calibrating top %d/4 (diff=%d)" % (steady, diff))
+            if diff < self.NOISE:
+                steady = steady + 1
+            else:
+                steady = 0
+            time.sleep(0.5)
+         
+    def calibrate(self):
+        # Calibration steps:
+        #   1. Check valves
+        #   2. Drain
+        #   3. Save min levels
+        #   4. Calibrate flows
+        #   5. Save mid levels
+        #   6. Find max levels
+        #   7. Save max levels
+        #   8. Save configuration
+
+        self.check_valves()
+        self.drain()
+        # All valves except AV2/BV2 are in known positions, tanks are empty
+        min_pressure = self.get_pressures()
+        calibrated_flow = self.calibrated_flow()
+        mid_pressure = self.get_pressures()
+        self.max_levels()
+        max_pressure = self.get_pressures()
+        self.opcom.progress("All measurements done")
+        self.opcom.status("")
+        self.opcom.action("")
+        mid_level = [ 0, 0, 0, 0 ]
+        calib = []
+        for i in range(0,4):
+            # 0V at 0 mm, 10V at 200 mm
+            min = min_pressure[i]
+            mid = mid_pressure[i]
+            max = max_pressure[i]
+            n = max - min
+            step = 197.0 / n
+            low = -min * step
+            high = 197.0 + (1023 - max) * step
+            mid_level[i] = low + mid * step
+            calib.append([low * 10 / 200, high * 10 / 200])
+        for i in range(0,2):
+            # Flow sensors follows the same sqrt law as level
+            # calibration values are such that 5V corresponds to the
+            # flow that gives 200 mm level at stationarity
+            min = min_pressure[i + 4]
+            max = calibrated_flow[i]
+            n = max - min
+            step = mid_level[i * 2] / n
+            low = -min * step
+            high = mid_level[i * 2] + (1023 - max) * step
+            calib.append([low * 5 / 200, high * 5 / 200])
+        for i in range(len(calib)):
+            self.opcom.progress('%d %s' % (i, calib[i]))
+        self.opcom.progress("Min=%s" % min_pressure)
+        self.opcom.progress("Mid=%s" % mid_pressure)
+        self.opcom.progress("Max=%s" % max_pressure)
+        self.opcom.progress("imbalance A %f" %
+                            (mid_pressure[0]/mid_pressure[1]))
+        self.opcom.progress("imbalance B %f" %
+                            (mid_pressure[2]/mid_pressure[3]))
+        self.drain()
+        for i in range(len(calib)):
+            self.write_calibration(i * 2, calib[i][0])
+            self.write_calibration(i * 2 + 1, calib[i][1])
+        self.write_calibration(12, 0)
+        
+    def write_calibration(self, index, value):
+        if value < 0:
+            tmp = (int(-value * 1000) << 8) | 0x80 | index
+        else:
+            tmp = (int(value * 1000) << 8) | index
+        self.opcom.progress("%d %8x" % (index, tmp))
+        self.io.setchannel(31, tmp, 0xffffffff)
+        
+if __name__ == "__main__":
+    opcom = Opcom(sys.stdout)
+    tank = QuadTank(sys.argv[1], opcom)
+    try:
+        tank.calibrate()
+    except:
+        traceback.print_exc()
+
+    tank.io.setchannel(tank.A_PUMP, 0)
+    tank.io.setchannel(tank.B_PUMP, 0)
diff --git a/quadtank/avr/quadtank.c b/quadtank/avr/quadtank.c
new file mode 100644
index 0000000000000000000000000000000000000000..02961ab7ee39dd500636a5b0f2e8944545f58b1b
--- /dev/null
+++ b/quadtank/avr/quadtank.c
@@ -0,0 +1,269 @@
+#include <avr/eeprom.h>
+#include <avr/io.h>
+#include <avr/interrupt.h>
+//#include <avr/signal.h>
+#include <inttypes.h>
+#include "serialio.h"
+
+volatile unsigned char error;
+
+
+volatile unsigned char serial_readbits;
+volatile unsigned char serial_readchannels;
+volatile unsigned char serial_readconfig;
+struct values {
+  int A1_level;
+  int A2_level;
+  int flow_A;
+  int B1_level;
+  int B2_level;
+  int flow_B;
+  uint8_t timer;
+};
+struct bounds {
+  unsigned char not_valid;
+  struct {
+    int min;
+    int max;
+  } A1, A2, flow_A, B1, B2, flow_B;
+};
+volatile struct values global;
+volatile struct bounds bounds;
+
+SIGNAL(ADC_vect)
+{
+  unsigned char channel = ADMUX & 0x0f;
+  unsigned int value = ADCW;
+
+  if (global.timer) {
+    global.timer--;
+    PORTD &= ~0x40;
+  } else {
+    PORTD |= 0x40;
+  }
+  switch (channel) {
+    case 0: { 
+      channel = 1; 
+      global.flow_B = value;
+    } break;
+    case 1: { 
+      channel = 2; 
+      global.B1_level = value;
+    } break;
+    case 2: { 
+      channel = 3; 
+      global.B2_level = value;
+    } break;
+    case 3: { 
+      channel = 4; 
+      global.A1_level = value;
+    } break;
+    case 4: { 
+      channel = 5; 
+      global.A2_level = value;
+    } break;
+    case 5: { 
+      channel = 0; 
+      global.flow_A = value;
+    } break;
+    default: { 
+      channel = 0; 
+    } break;
+  }
+  ADMUX = 0x40 | channel; // Vref = Vcc, right adjust
+  ADCSRA = 0xcf;          // Enable ADC interrupts, Clock/128 
+}
+
+typedef enum { cmd_clear_bit, cmd_set_bit, 
+	       cmd_read_bit, cmd_read_chan } command;
+
+SIGNAL(USART_RXC_vect)
+{
+  char ch = UDR;
+  
+  global.timer = 0xff;
+  switch (serialio_RXC(ch)) {
+    case serialio_clearbit: {
+      switch (serialio_channel) {
+      }
+    } break;
+    case serialio_setbit: {
+      switch (serialio_channel) {
+      }
+    } break;
+    case serialio_pollbit: {
+    } break;
+    case serialio_pollchannel: { 
+      if (serialio_channel < 6) { 
+        serial_readchannels |= (1<<serialio_channel); 
+      } else if (serialio_channel == 31) { 
+        serial_readconfig = 1; 
+      }
+    } break;
+    case serialio_setchannel: {
+      switch (serialio_channel) {
+	case 0: { 
+	  OCR1B = (0x3ff - serialio_value) & 0x3ff;
+	} break;
+	case 1: { 
+	  OCR1A = serialio_value & 0x3ff;
+	} break;
+	case 31: { 
+	  unsigned char pos = serialio_value & 0x7f;
+	  unsigned char sign = serialio_value & 0x80;
+	  int value = serialio_value >> 8;
+	  if (sign) {
+	    value = -value;
+	  }
+	  switch (pos) {
+	    case 0:  bounds.A1.min = value; break;
+	    case 1:  bounds.A1.max = value; break;
+	    case 2:  bounds.A2.min = value; break;
+	    case 3:  bounds.A2.max = value; break;
+	    case 4:  bounds.B1.min = value; break;
+	    case 5:  bounds.B1.max = value; break;
+	    case 6:  bounds.B2.min = value; break;
+	    case 7:  bounds.B2.max = value; break;
+	    case 8:  bounds.flow_A.min = value; break;
+	    case 9:  bounds.flow_A.max = value; break;
+	    case 10: bounds.flow_B.min = value; break;
+	    case 11: bounds.flow_B.max = value; break;
+	    case 12: eeprom_write_block((void*)&bounds, 0, sizeof(bounds));
+	  }
+	} break;
+      } break;
+    } break;
+    case serialio_error: {
+    } break;
+    case serialio_more: {
+    } break;
+  }
+}
+
+static void conf_min(int channel, int value);
+static void conf_max(int channel, int value);
+
+/*
+ * PA0  Flow B
+ * PA1  Level B1
+ * PA2  Level B2
+ * PA3  Level A1
+ * PA4  Level A2
+ * PA5  Flow A
+ *
+ * PD4  Pump A
+ * PD5  Pump B
+ * PD6  LED 1
+ * PD7  LED 2
+ *
+ */
+int main() 
+{
+  serialio_init();
+  serial_readbits = 0;
+  serial_readchannels = 0;
+  serial_readconfig = 0;
+  PORTA = 0x00;     // PortA, pull-ups
+  DDRA = 0x00;      // PortA, all inputs
+  PORTD = 0x40;     // PortD, pull-ups / initial values
+  DDRD = 0xf0;      // PortD, 4-7 outputs
+  TCCR0 = 0x05;	    // Timer0, Clock / 1024 
+  TCCR1A = 0xb3;    // OC1A & OC1B 10 bit PWM (PC), clear A on match
+                    //                              set B on match
+  TCCR1B = 0x01;    // Clock / 1 
+  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
+  ADMUX = 0x40;     // Vref = Vcc, right adjust
+  ADMUX = 0x4e;     // Vref = Vcc, right adjust, read 1.22V (Vbg)
+  ADCSRA = 0xcf;    // Enable ADC interrupts, Clock/128 
+  OCR1A = 0 & 0x3ff;
+  OCR1B = (0x3ff - 0) & 0x3ff;
+
+  eeprom_read_block((void*)&bounds, 0, sizeof(bounds));
+  if (bounds.not_valid) {
+    bounds.A1.min = 0;
+    bounds.A1.max = 10000;
+    bounds.A2.min = 0;
+    bounds.A2.max = 10000;
+    bounds.flow_A.min = 0;
+    bounds.flow_A.max = 10000;
+    bounds.B1.min = 0;
+    bounds.B1.max = 10000;
+    bounds.B2.min = 0;
+    bounds.B2.max = 10000;
+    bounds.flow_B.min = 0;
+    bounds.flow_B.max = 10000;
+    bounds.not_valid = 0;
+  }
+
+  SREG = 0x80;	    // Global interrupt enable 
+  while (1) {
+    unsigned char channels, config;
+    struct values local;
+
+    SREG = 0x00;    // Global interrupt disable 
+    serial_readbits = 0;
+    channels = serial_readchannels;
+    serial_readchannels = 0;
+    config = serial_readconfig;
+    serial_readconfig = 0;
+    local = global;
+    SREG = 0x80;    // Global interrupt enable 
+    if (channels & 0x01) { serialio_putchannel(0, local.A1_level); }
+    if (channels & 0x02) { serialio_putchannel(1, local.A2_level); }
+    if (channels & 0x04) { serialio_putchannel(2, local.B1_level); }
+    if (channels & 0x08) { serialio_putchannel(3, local.B2_level); }
+    if (channels & 0x10) { serialio_putchannel(4, local.flow_A); }
+    if (channels & 0x20) { serialio_putchannel(5, local.flow_B); }
+    if (config) {
+      CONF_ANALOG_IN(0, CONF_RESOLUTION(10));			// Level A1
+      conf_min(0, bounds.A1.min);
+      conf_max(0, bounds.A1.max);
+      CONF_ANALOG_IN(1, CONF_RESOLUTION(10));			// Level A2
+      conf_min(1, bounds.A2.min);
+      conf_max(1, bounds.A2.max);
+      CONF_ANALOG_IN(2, CONF_RESOLUTION(10));			// Level B1
+      conf_min(2, bounds.B1.min);
+      conf_max(2, bounds.B1.max);
+      CONF_ANALOG_IN(3, CONF_RESOLUTION(10));			// Level B2
+      conf_min(3, bounds.B2.min);
+      conf_max(3, bounds.B2.max);
+      CONF_ANALOG_IN(4, CONF_RESOLUTION(10));			// Flow left
+      conf_min(4, bounds.flow_A.min);
+      conf_max(4, bounds.flow_A.max);
+      CONF_ANALOG_IN(5, CONF_RESOLUTION(10));			// Flow right
+      conf_min(5, bounds.flow_B.min);
+      conf_max(5, bounds.flow_B.max);
+      CONF_ANALOG_OUT(0, CONF_RESOLUTION(10));			// Pump A
+      CONF_ANALOG_OUT(0, CONF_MIN(CONF_NEGATIVE_MILLIVOLT(0)));
+      CONF_ANALOG_OUT(0, CONF_MAX(CONF_POSITIVE_MILLIVOLT(10000)));
+      CONF_ANALOG_OUT(1, CONF_RESOLUTION(10));			// Pump B
+      CONF_ANALOG_OUT(1, CONF_MIN(CONF_NEGATIVE_MILLIVOLT(0)));
+      CONF_ANALOG_OUT(1, CONF_MAX(CONF_POSITIVE_MILLIVOLT(10000)));
+
+      CONF_END();
+    }
+  }
+}
+
+static void conf_min(int channel, int value)
+{
+  if (value < 0) {
+    CONF_ANALOG_IN(channel, CONF_MIN(CONF_NEGATIVE_MILLIVOLT(-value)));
+  } else {
+    CONF_ANALOG_IN(channel, CONF_MIN(CONF_POSITIVE_MILLIVOLT(value)));
+  }
+}
+
+static void conf_max(int channel, int value)
+{
+  if (value < 0) {
+    CONF_ANALOG_IN(channel, CONF_MAX(CONF_NEGATIVE_MILLIVOLT(-value)));
+  } else {
+    CONF_ANALOG_IN(channel, CONF_MAX(CONF_POSITIVE_MILLIVOLT(value)));
+  }
+}
+