craftui: add config UI

	* with test cases!
	* client and server side config interface
	* update test suite and test sample data

Change-Id: I0dc676db61002606112f8cc9ed4f401cdc6dce46
diff --git a/craftui/.gitignore b/craftui/.gitignore
new file mode 100644
index 0000000..f972e18
--- /dev/null
+++ b/craftui/.gitignore
@@ -0,0 +1,2 @@
+*.swp
+.started
diff --git a/craftui/HOW.restart_if_changed b/craftui/HOW.restart_if_changed
index 1f3343e..8c17154 100644
--- a/craftui/HOW.restart_if_changed
+++ b/craftui/HOW.restart_if_changed
@@ -6,10 +6,13 @@
 
 restart() {
   [ -n "$pid" ] && kill $pid
+  echo "######################################################################"
   echo "# starting craftui"
+  gpylint *.py
+  make test
   ./craftui &
   pid=$!
-  touch started
+  touch .started
 }
 
 onExit() {
@@ -25,7 +28,7 @@
     restart
     continue
   fi
-  f=$(find . -type f -newer started)
+  f=$(find . -name '*.swp' -prune -o -type f -newer .started -print)
   if [ -n "$f" ]; then
     restart
     continue
diff --git a/craftui/Makefile b/craftui/Makefile
index 6b6757b..9f41971 100644
--- a/craftui/Makefile
+++ b/craftui/Makefile
@@ -17,10 +17,10 @@
 
 test: lint
 	set -e; \
-	for pytest in $(wildcard *_test.py); do \
+	for n in $(wildcard ./*_test.*); do \
 		echo; \
-		echo "Testing $$pytest"; \
-		$(PYTHON) $$pytest; \
+		echo "Testing $$n"; \
+		$$n; \
 	done
 
 clean:
diff --git a/craftui/craftui b/craftui/craftui
index c78bc35..2f5e143 100755
--- a/craftui/craftui
+++ b/craftui/craftui
@@ -1,6 +1,6 @@
 #! /bin/sh
 
-bin=/bin
+pycode=/bin/craftui.py
 cw=/usr/catawampus
 devcw=../../../../vendor/google/catawampus
 localwww=./www
@@ -14,7 +14,8 @@
 if [ "$isdev" = 1 ]; then
   cw="$devcw"
   args="$args --port=8888 --sim=./sim"
-  bin=.
+  pycode=./craftui_fortesting.py
+  export PATH="$PWD/sim/bin:$PATH"
 fi
 
 # for debugging on the device, use the local (/tmp/www?) web tree
@@ -22,5 +23,10 @@
   args="$args --www=$localwww"
 fi
 
+# enable debugger
+if [ "$1" = -d ]; then
+  debug="-m pdb"
+fi
+
 export PYTHONPATH="$cw/tr/vendor/tornado:$PYTHONPATH"
-exec python $bin/craftui.py $args
+exec python -u $debug $pycode $args
diff --git a/craftui/craftui.py b/craftui/craftui.py
index 8e06e65..e39faf1 100755
--- a/craftui/craftui.py
+++ b/craftui/craftui.py
@@ -20,15 +20,264 @@
 import getopt
 import json
 import os
+import re
+import subprocess
 import sys
 import urllib2
 import tornado.ioloop
 import tornado.web
 
 
+class ConfigError(Exception):
+  """Configuration errors to pass to browser."""
+
+  def __init__(self, message):
+    super(ConfigError, self).__init__(message)
+
+
+class Validator(object):
+  """Validate the user value and convert to safe config value."""
+  pattern = r'^(.*)$'
+  example = 'any string'
+
+  def __init__(self):
+    self.Reset()
+
+  def Reset(self):
+    self.fields = ()
+    self.config = ''
+
+  def Validate(self, value):
+    self.Reset()
+    self.value = value
+    m = re.search(self.pattern, value)
+    if not m:
+      raise ConfigError('value "%s" does not match pattern "%s", eg: "%s"' %
+                        (value, self.pattern, self.example))
+    self.fields = m.groups()
+    self.config = self.fields[0]
+
+
+class VInt(Validator):
+  """Validate as integer."""
+  pattern = r'^(\d+)$'
+  example = '123'
+
+
+class VRange(VInt):
+  """Validate as integer in a range."""
+
+  def __init__(self, low, high):
+    super(VRange, self).__init__()
+    self.low = low
+    self.high = high
+
+  def Validate(self, value):
+    super(VRange, self).Validate(value)
+    self.CheckInRange(int(self.config), self.low, self.high)
+
+  @staticmethod
+  def CheckInRange(num, low, high):
+    if num < low or num > high:
+      raise ConfigError('number %d is out of range %d-%d' % (num, low, high))
+
+
+class VSlash(Validator):
+  """Validate as slash notation (eg 192.168.1.1/24)."""
+  pattern = r'^((\d+).(\d+).(\d+).(\d+)/(\d+))$'
+  example = '192.168.1.1/24'
+
+  def __init__(self):
+    super(VSlash, self).__init__()
+
+  def Validate(self, value):
+    super(VSlash, self).Validate(value)
+    mask = int(self.fields[5])
+    VRange.CheckInRange(mask, 0, 32)
+    for dotted_quad_part in self.fields[1:4]:
+      num = int(dotted_quad_part)
+      VRange.CheckInRange(num, 0, 255)
+
+
+class VVlan(VRange):
+  """Validate as vlan."""
+
+  def __init__(self):
+    super(VVlan, self).__init__(0, 4095)
+
+
+class VFreqHi(VRange):
+  """Validate as Hi E-Band frequency."""
+
+  def __init__(self):
+    super(VFreqHi, self).__init__(82000000, 85000000)
+
+
+class VFreqLo(VRange):
+  """Validate as Low E-Band frequency."""
+
+  def __init__(self):
+    super(VFreqLo, self).__init__(72000000, 75000000)
+
+
+class VPower(VRange):
+  """Validate as PA power level."""
+
+  def __init__(self):
+    super(VPower, self).__init__(0, 2000000)       # TODO(edjames)
+
+
+class VDict(Validator):
+  """Validate as member of dict."""
+  dict = {}
+
+  def Validate(self, value):
+    super(VDict, self).Validate(value)
+    if value not in self.dict:
+      keys = self.dict.keys()
+      raise ConfigError('value "%s" must be one of "%s"' % (value, keys))
+    self.config = self.dict[value]
+
+
+class VTx(VDict):
+  """Validate: tx/rx."""
+  dict = {'tx': 'tx', 'rx': 'rx'}
+
+
+class VTrueFalse(VDict):
+  """Validate as true or false."""
+  dict = {'true': 'true', 'false': 'false'}
+
+
+class Config(object):
+  """Configure the device after validation."""
+
+  def __init__(self, validator):
+    self.validator = validator()
+
+  def Validate(self, value):
+    self.validator.Validate(value)
+
+  def Configure(self):
+    raise Exception('override Config.Configure')
+
+  @staticmethod
+  def Run(command):
+    """Run a command."""
+    print 'running: %s' % command
+    try:
+      subprocess.check_output(command)
+    except subprocess.CalledProcessError as e:
+      print 'Run: ', str(e)
+      raise ConfigError('command failed with %d' % e.returncode)
+
+
+class PtpConfig(Config):
+  """Configure using ptp-config."""
+
+  def __init__(self, validator, key):
+    super(PtpConfig, self).__init__(validator)
+    self.key = key
+
+  def Configure(self):
+    Config.Run(['ptp-config', '-s', self.key, self.validator.config])
+
+
+class PtpActivate(Config):
+  """Configure using ptp-config."""
+
+  def __init__(self, validator, key):
+    super(PtpActivate, self).__init__(validator)
+    self.key = key
+
+  def Configure(self):
+    Config.Run(['ptp-config', '-i', self.key])
+
+
+class Glaukus(Config):
+  """Configure using glaukus json api."""
+
+  def __init__(self, validator, api, fmt):
+    super(Glaukus, self).__init__(validator)
+    self.api = api
+    self.fmt = fmt
+
+  def Configure(self):
+    """Handle a JSON request to glaukusd."""
+    url = 'http://localhost:8080' + self.api
+    payload = self.fmt % self.validator.config
+    # TODO(edjames)
+    print 'Glaukus: ', url, payload
+    try:
+      fd = urllib2.urlopen(url, payload)
+    except urllib2.URLError as ex:
+      print 'Connection to %s failed: %s' % (url, ex.reason)
+      raise ConfigError('failed to contact glaukus')
+    response = fd.read()
+    j = json.loads(response)
+    print j
+    if j['code'] != 'SUCCESS':
+      if j['message']:
+        raise ConfigError(j.message)
+      raise ConfigError('failed to configure glaukus')
+
+
+class Reboot(Config):
+  """Reboot."""
+
+  def Configure(self):
+    if self.validator.config == 'true':
+      Config.Run(['reboot'])
+
+
 class CraftUI(object):
   """A web server that configures and displays Chimera data."""
 
+  handlers = {
+      'craft_ipaddr': PtpConfig(VSlash, 'craft_ipaddr'),
+      'link_ipaddr': PtpConfig(VSlash, 'local_ipaddr'),
+      'peer_ipaddr': PtpConfig(VSlash, 'peer_ipaddr'),
+
+      'vlan_inband': PtpConfig(VVlan, 'vlan_inband'),
+      'vlan_peer': PtpConfig(VVlan, 'vlan_peer'),
+
+      'craft_ipaddr_activate': PtpActivate(VTrueFalse, 'craft_ipaddr'),
+      'link_ipaddr_activate': PtpActivate(VTrueFalse, 'local_ipaddr'),
+      'peer_ipaddr_activate': PtpActivate(VTrueFalse, 'peer_ipaddr'),
+      'vlan_inband_activate': PtpActivate(VTrueFalse, 'vlan_inband'),
+      'vlan_peer_activate': PtpActivate(VTrueFalse, 'vlan_peer'),
+
+      'freq_hi': Glaukus(VFreqHi, '/api/radio/frequency', '{"hiFrequency":%s}'),
+      'freq_lo': Glaukus(VFreqLo, '/api/radio/frequency', '{"loFrequency":%s}'),
+      'mode_hi': Glaukus(VTx, '/api/radio/hiTransceiver/mode', '%s'),
+      'tx_powerlevel': Glaukus(VPower, '/api/radio/tx/paPowerSet', '%s'),
+      'tx_on': Glaukus(VTrueFalse, '/api/radio/paLnaPowerEnabled', '%s'),
+
+      'reboot': Reboot(VTrueFalse)
+  }
+  ifmap = {
+      'craft0': 'craft',
+      'eth1.inband': 'inband',
+      'eth1.peer': 'link',
+      'br0': 'poe'
+  }
+  ifvlan = [
+      'eth1.inband',
+      'eth1.peer'
+  ]
+  stats = [
+      'multicast',
+      'collisions',
+      'rx_bytes',
+      'rx_packets',
+      'rx_errors',
+      'rx_dropped',
+      'tx_bytes',
+      'tx_packets',
+      'tx_errors',
+      'tx_dropped'
+  ]
+
   def __init__(self, wwwroot, port, sim):
     """initialize."""
     self.wwwroot = wwwroot
@@ -37,7 +286,30 @@
     self.data = {}
     self.data['refreshCount'] = 0
 
+  def ApplyChanges(self, changes):
+    """Apply changes to system."""
+    if 'config' not in changes:
+      raise ConfigError('missing required config array')
+    conf = changes['config']
+    try:
+      # dry run to validate all
+      for c in conf:
+        for k, v in c.items():
+          if k not in self.handlers:
+            raise ConfigError('unknown key "%s"' % k)
+          h = self.handlers[k]
+          h.Validate(v)
+      # do it again for real
+      for c in conf:
+        for k, v in c.items():
+          h = self.handlers[k]
+          h.Validate(v)
+          h.Configure()
+    except ConfigError as e:
+      raise ConfigError('key "%s": %s' % (k, e))
+
   def ReadFile(self, filepath):
+    """cat file."""
     text = ''
     try:
       with open(filepath) as fd:
@@ -55,12 +327,59 @@
     js = '{"platform":' + pj + ',"modem":' + mj + ',"radio":' + rj + '}'
     return js
 
+  def AddIpAddr(self, data):
+    """Run ip addr and parse results."""
+    ipaddr = ''
+    try:
+      ipaddr = subprocess.check_output(['ip', '-o', 'addr'])
+    except subprocess.CalledProcessError as e:
+      print 'warning: "ip -o addr" failed: ', e
+    v = {}
+    for line in ipaddr.splitlines():
+      f = line.split()
+      m = re.search(r'scope (global|link)', line)
+      scope = m.group(1) if m else 'noscope'
+      v[f[1] + ':' + f[2] + ':' + scope] = f[3]
+    for ifname, uiname in self.ifmap.items():
+      for inet in ('inet', 'inet6'):
+        kglobal = ifname + ':' + inet + ':' + 'global'
+        vdata = v.get(kglobal, 'unknown')
+        kdata = 'active_' + uiname + '_' + inet
+        data[kdata] = vdata
+
+  def AddInterfaceStats(self, data):
+    """Get if stats."""
+    for ifname, uiname in self.ifmap.items():
+      d = self.sim + '/sys/class/net/' + ifname + '/statistics/'
+      for stat in self.stats:
+        k = uiname + '_' + stat
+        data[k] = self.ReadFile(d + stat)
+
+  def AddVlans(self, data):
+    """Run ip -d link and parse results for vlans."""
+    iplink = ''
+    try:
+      iplink = subprocess.check_output(['ip', '-o', '-d', 'link'])
+    except subprocess.CalledProcessError as e:
+      print 'warning: "ip -o -d link" failed: ', e
+    v = {}
+    for line in iplink.splitlines():
+      m = re.search(r'^\d+: ([\w\.]+)@\w+: .* vlan id (\w+)', line)
+      if m:
+        v[m.group(1)] = m.group(2)
+    for ifname in self.ifvlan:
+      uiname = self.ifmap[ifname]
+      vdata = v.get(ifname, 'unknown')
+      kdata = 'active_' + uiname + '_vlan'
+      data[kdata] = vdata
+
   def GetPlatformData(self):
     """Get platform data, return a json string."""
     data = self.data
     sim = self.sim
 
     if data['refreshCount'] == 0:
+      data['serialno'] = self.ReadFile(sim + '/etc/serial')
       data['version'] = self.ReadFile(sim + '/etc/version')
       data['platform'] = self.ReadFile(sim + '/etc/platform')
       data['softwaredate'] = self.ReadFile(sim + '/etc/softwaredate')
@@ -69,10 +388,13 @@
     data['ledstate'] = self.ReadFile(sim + '/tmp/gpio/ledstate')
     cs = '/config/settings/'
     data['craft_ipaddr'] = self.ReadFile(sim + cs + 'craft_ipaddr')
-    data['local_ipaddr'] = self.ReadFile(sim + cs + 'local_ipaddr')
+    data['link_ipaddr'] = self.ReadFile(sim + cs + 'local_ipaddr')
     data['peer_ipaddr'] = self.ReadFile(sim + cs + 'peer_ipaddr')
     data['vlan_inband'] = self.ReadFile(sim + cs + 'vlan_inband')
-    data['vlan_peer'] = self.ReadFile(sim + cs + 'vlan_peer')
+    data['vlan_link'] = self.ReadFile(sim + cs + 'vlan_peer')
+    self.AddIpAddr(data)
+    self.AddInterfaceStats(data)
+    self.AddVlans(data)
     return json.dumps(data)
 
   def GetModemData(self):
@@ -104,12 +426,20 @@
     return response
 
   class MainHandler(tornado.web.RequestHandler):
-    """Displays the UI."""
+    """Displays the Craft UI."""
 
     def get(self):
       ui = self.settings['ui']
       print 'GET craft HTML page'
-      self.render(ui.wwwroot + '/index.thtml', peerurl='http://TODO')
+      self.render(ui.wwwroot + '/index.thtml', peerurl='/?peer=1')
+
+  class ConfigHandler(tornado.web.RequestHandler):
+    """Displays the Config page."""
+
+    def get(self):
+      ui = self.settings['ui']
+      print 'GET config HTML page'
+      self.render(ui.wwwroot + '/config.thtml', peerurl='/config/?peer=1')
 
   class RestartHandler(tornado.web.RequestHandler):
     """Restart the box."""
@@ -135,12 +465,40 @@
       self.write(jsonstring)
       self.finish()
 
+    def post(self):
+      print 'POST JSON data for craft page'
+      request = self.request.body
+      result = {}
+      result['error'] = 0
+      result['errorstring'] = ''
+      try:
+        try:
+          json_args = json.loads(request)
+          request = json.dumps(json_args)
+        except ValueError as e:
+          print e
+          raise ConfigError('json format error')
+        ui = self.settings['ui']
+        ui.ApplyChanges(json_args)
+      except ConfigError as e:
+        print e
+        result['error'] += 1
+        result['errorstring'] += str(e)
+
+      response = json.dumps(result)
+      print 'request: ', request
+      print 'response: ', response
+      self.set_header('Content-Type', 'application/json')
+      self.write(response)
+      self.finish()
+
   def RunUI(self):
     """Create the web server and run forever."""
     handlers = [
-        (r'/', CraftUI.MainHandler),
-        (r'/content.json', CraftUI.JsonHandler),
-        (r'/restart', CraftUI.RestartHandler),
+        (r'/', self.MainHandler),
+        (r'/config', self.ConfigHandler),
+        (r'/content.json', self.JsonHandler),
+        (r'/restart', self.RestartHandler),
         (r'/static/([^/]*)$', tornado.web.StaticFileHandler,
          {'path': self.wwwroot + '/static'}),
     ]
diff --git a/craftui/craftui_test.py b/craftui/craftui_fortesting.py
similarity index 75%
rename from craftui/craftui_test.py
rename to craftui/craftui_fortesting.py
index ae20df7..82bf147 100644
--- a/craftui/craftui_test.py
+++ b/craftui/craftui_fortesting.py
@@ -17,6 +17,14 @@
 
 __author__ = 'edjames@google.com (Ed James)'
 
+import traceback
+import craftui
 
 if __name__ == '__main__':
-  print 'TODO(edjames)'
+  try:
+    craftui.main()
+  # pylint: disable=broad-except
+  except Exception as e:
+    traceback.print_exc()
+    # exit cleanly to close the socket so next listen doesn't fail with in-use
+    exit(1)
diff --git a/craftui/craftui_test.sh b/craftui/craftui_test.sh
new file mode 100755
index 0000000..9147945
--- /dev/null
+++ b/craftui/craftui_test.sh
@@ -0,0 +1,107 @@
+#! /bin/sh
+
+# some unit tests for the craft UI
+
+# save stdout to 3, dup stdout to a file
+log=.testlog.$$
+exec 3>&1
+exec >$log 2>&1
+
+failcount=0
+passcount=0
+
+fail() {
+	echo "FAIL: $*" >&3
+	echo "FAIL: $*"
+	((failcount++))
+}
+
+pass() {
+	echo "PASS: $*" >&3
+	echo "PASS: $*"
+	((passcount++))
+}
+
+testname() {
+	test="$*"
+	echo "---------------------------------------------------------"
+	echo "starting test $test"
+}
+
+check_success() {
+	status=$?
+	echo "check_success: last return code was $status, wanted 0"
+	if [ $status = 0 ]; then
+		pass $test
+	else
+		fail $test
+	fi
+}
+
+check_failure() {
+	status=$?
+	echo "check_failure: last return code was $status, wanted not-0"
+	if [ $status != 0 ]; then
+		pass $test
+	else
+		fail $test
+	fi
+}
+
+onexit() {
+	testname "process running at exit"
+	kill -0 $pid
+	check_success
+
+	# cleanup
+	kill -9 $pid
+
+	exec 1>&3
+	echo "SUMMARY: pass=$passcount fail=$failcount"
+	if [ $failcount -eq 0 ]; then
+		echo "SUCCESS: $passcount tests passed."
+	else
+		echo "FAILURE: $failcount tests failed."
+		echo "details follow:"
+		cat $log
+	fi
+	rm -f $log
+
+	exit $failcount
+}
+
+trap onexit 0 1 2 3
+
+testname "server not running"
+curl -s http://localhost:8888/
+check_failure
+
+./craftui > /tmp/LOG 2>&1 &
+pid=$!
+
+testname "process running"
+kill -0 $pid
+check_success
+
+sleep 1
+
+testname true
+true
+check_success
+
+testname false
+false
+check_failure
+
+testname "main web page"
+curl -s http://localhost:8888/ > /dev/null
+check_success
+
+testname "404 not found"
+curl -s http://localhost:8888/notexist | grep '404: Not Found'
+check_success
+
+testname "json"
+curl -s http://localhost:8888/content.json | grep '"platform": "GFCH100"'
+check_success
+
diff --git a/craftui/sim/bin/ip b/craftui/sim/bin/ip
new file mode 100755
index 0000000..0775940
--- /dev/null
+++ b/craftui/sim/bin/ip
@@ -0,0 +1,35 @@
+#! /bin/sh
+
+if [ "$3" = link ]; then
+  cat << EOF
+1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN \	 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
+2: craft0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 532\    link/ether f4:f5:e8:01:a1:01 brd ff:ff:ff:ff:ff:ff
+3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master br0 state UP qlen 532\    link/ether f4:f5:e8:01:a1:02 brd ff:ff:ff:ff:ff:ff
+4: eth1.inband@eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000\    link/ether f4:f5:e8:01:a1:02 brd ff:ff:ff:ff:ff:ff\    vlan id 4090 <REORDER_HDR> 
+5: eth1.peer@eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000\	   link/ether f4:f5:e8:01:a1:02 brd ff:ff:ff:ff:ff:ff\	  vlan id 2000 <REORDER_HDR> 
+6: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000\	link/ether f4:f5:e8:01:a1:02 brd ff:ff:ff:ff:ff:ff\    bridge 
+EOF
+
+else
+  cat << EOF
+1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN \    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
+1: lo    inet 127.0.0.1/32 scope host lo\       valid_lft forever preferred_lft forever
+1: lo    inet 127.0.0.1/8 scope host lo\       valid_lft forever preferred_lft forever
+1: lo    inet6 ::1/128 scope host \       valid_lft forever preferred_lft forever
+2: craft0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 532\    link/ether f4:f5:e8:01:a1:01 brd ff:ff:ff:ff:ff:ff
+2: craft0    inet 192.168.5.99/24 scope global craft0\       valid_lft forever preferred_lft forever
+2: craft0    inet6 fe80::f6f5:e8ff:fe01:a101/64 scope link \       valid_lft forever preferred_lft forever
+3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master br0 state UP qlen 532\    link/ether f4:f5:e8:01:a1:02 brd ff:ff:ff:ff:ff:ff
+3: eth1    inet6 fe80::f6f5:e8ff:fe01:a102/64 scope link \       valid_lft forever preferred_lft forever
+4: eth1.inband@eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000\    link/ether f4:f5:e8:01:a1:02 brd ff:ff:ff:ff:ff:ff
+4: eth1.inband    inet6 fe80::f6f5:e8ff:fe01:a102/64 scope link \       valid_lft forever preferred_lft forever
+5: eth1.peer@eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000\    link/ether f4:f5:e8:01:a1:02 brd ff:ff:ff:ff:ff:ff
+5: eth1.peer    inet 192.168.2.1/24 scope global eth1.peer\       valid_lft forever preferred_lft forever
+5: eth1.peer    inet6 fe80::f6f5:e8ff:fe01:a102/64 scope link \       valid_lft forever preferred_lft forever
+6: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000\    link/ether f4:f5:e8:01:a1:02 brd ff:ff:ff:ff:ff:ff
+6: br0    inet 192.168.1.147/24 brd 192.168.1.255 scope global br0\       valid_lft forever preferred_lft forever
+6: br0    inet6 2605:a601:b17:d600:f6f5:e8ff:fe01:a102/64 scope global dynamic \       valid_lft 3595sec preferred_lft 3595sec
+6: br0    inet6 fe80::f6f5:e8ff:fe01:a102/64 scope link \       valid_lft forever preferred_lft forever
+EOF
+
+fi
diff --git a/craftui/sim/bin/ptp-config b/craftui/sim/bin/ptp-config
new file mode 100755
index 0000000..4031019
--- /dev/null
+++ b/craftui/sim/bin/ptp-config
@@ -0,0 +1,3 @@
+#! /bin/sh
+
+echo "TODO: $0 $*"
diff --git a/craftui/sim/bin/reboot b/craftui/sim/bin/reboot
new file mode 100755
index 0000000..60ee746
--- /dev/null
+++ b/craftui/sim/bin/reboot
@@ -0,0 +1,3 @@
+#! /bin/sh
+
+echo running: $0 $*
diff --git a/craftui/sim/etc/serial b/craftui/sim/etc/serial
new file mode 100644
index 0000000..72a6d63
--- /dev/null
+++ b/craftui/sim/etc/serial
@@ -0,0 +1 @@
+NPAPID1611E0001
diff --git a/craftui/sim/sys/class/net/br0/statistics/collisions b/craftui/sim/sys/class/net/br0/statistics/collisions
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/br0/statistics/collisions
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/br0/statistics/multicast b/craftui/sim/sys/class/net/br0/statistics/multicast
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/br0/statistics/multicast
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/br0/statistics/rx_bytes b/craftui/sim/sys/class/net/br0/statistics/rx_bytes
new file mode 100644
index 0000000..08e3da2
--- /dev/null
+++ b/craftui/sim/sys/class/net/br0/statistics/rx_bytes
@@ -0,0 +1 @@
+3519029
diff --git a/craftui/sim/sys/class/net/br0/statistics/rx_dropped b/craftui/sim/sys/class/net/br0/statistics/rx_dropped
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/br0/statistics/rx_dropped
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/br0/statistics/rx_errors b/craftui/sim/sys/class/net/br0/statistics/rx_errors
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/br0/statistics/rx_errors
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/br0/statistics/rx_packets b/craftui/sim/sys/class/net/br0/statistics/rx_packets
new file mode 100644
index 0000000..9c4044d
--- /dev/null
+++ b/craftui/sim/sys/class/net/br0/statistics/rx_packets
@@ -0,0 +1 @@
+13252
diff --git a/craftui/sim/sys/class/net/br0/statistics/tx_bytes b/craftui/sim/sys/class/net/br0/statistics/tx_bytes
new file mode 100644
index 0000000..3365165
--- /dev/null
+++ b/craftui/sim/sys/class/net/br0/statistics/tx_bytes
@@ -0,0 +1 @@
+2378483
diff --git a/craftui/sim/sys/class/net/br0/statistics/tx_dropped b/craftui/sim/sys/class/net/br0/statistics/tx_dropped
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/br0/statistics/tx_dropped
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/br0/statistics/tx_errors b/craftui/sim/sys/class/net/br0/statistics/tx_errors
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/br0/statistics/tx_errors
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/br0/statistics/tx_packets b/craftui/sim/sys/class/net/br0/statistics/tx_packets
new file mode 100644
index 0000000..bad2b41
--- /dev/null
+++ b/craftui/sim/sys/class/net/br0/statistics/tx_packets
@@ -0,0 +1 @@
+5530
diff --git a/craftui/sim/sys/class/net/craft0/statistics/collisions b/craftui/sim/sys/class/net/craft0/statistics/collisions
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/craft0/statistics/collisions
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/craft0/statistics/multicast b/craftui/sim/sys/class/net/craft0/statistics/multicast
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/craft0/statistics/multicast
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/craft0/statistics/rx_bytes b/craftui/sim/sys/class/net/craft0/statistics/rx_bytes
new file mode 100644
index 0000000..84f168f
--- /dev/null
+++ b/craftui/sim/sys/class/net/craft0/statistics/rx_bytes
@@ -0,0 +1 @@
+3008350
diff --git a/craftui/sim/sys/class/net/craft0/statistics/rx_dropped b/craftui/sim/sys/class/net/craft0/statistics/rx_dropped
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/craft0/statistics/rx_dropped
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/craft0/statistics/rx_errors b/craftui/sim/sys/class/net/craft0/statistics/rx_errors
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/craft0/statistics/rx_errors
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/craft0/statistics/rx_packets b/craftui/sim/sys/class/net/craft0/statistics/rx_packets
new file mode 100644
index 0000000..6fc6a64
--- /dev/null
+++ b/craftui/sim/sys/class/net/craft0/statistics/rx_packets
@@ -0,0 +1 @@
+18701
diff --git a/craftui/sim/sys/class/net/craft0/statistics/tx_bytes b/craftui/sim/sys/class/net/craft0/statistics/tx_bytes
new file mode 100644
index 0000000..924221a
--- /dev/null
+++ b/craftui/sim/sys/class/net/craft0/statistics/tx_bytes
@@ -0,0 +1 @@
+12081830
diff --git a/craftui/sim/sys/class/net/craft0/statistics/tx_dropped b/craftui/sim/sys/class/net/craft0/statistics/tx_dropped
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/craft0/statistics/tx_dropped
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/craft0/statistics/tx_errors b/craftui/sim/sys/class/net/craft0/statistics/tx_errors
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/craft0/statistics/tx_errors
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/craft0/statistics/tx_packets b/craftui/sim/sys/class/net/craft0/statistics/tx_packets
new file mode 100644
index 0000000..5c2b8bb
--- /dev/null
+++ b/craftui/sim/sys/class/net/craft0/statistics/tx_packets
@@ -0,0 +1 @@
+7473
diff --git a/craftui/sim/sys/class/net/eth1.inband/statistics/collisions b/craftui/sim/sys/class/net/eth1.inband/statistics/collisions
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.inband/statistics/collisions
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.inband/statistics/multicast b/craftui/sim/sys/class/net/eth1.inband/statistics/multicast
new file mode 100644
index 0000000..91b629b
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.inband/statistics/multicast
@@ -0,0 +1 @@
+156
diff --git a/craftui/sim/sys/class/net/eth1.inband/statistics/rx_bytes b/craftui/sim/sys/class/net/eth1.inband/statistics/rx_bytes
new file mode 100644
index 0000000..81fa122
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.inband/statistics/rx_bytes
@@ -0,0 +1 @@
+407824
diff --git a/craftui/sim/sys/class/net/eth1.inband/statistics/rx_dropped b/craftui/sim/sys/class/net/eth1.inband/statistics/rx_dropped
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.inband/statistics/rx_dropped
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.inband/statistics/rx_errors b/craftui/sim/sys/class/net/eth1.inband/statistics/rx_errors
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.inband/statistics/rx_errors
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.inband/statistics/rx_packets b/craftui/sim/sys/class/net/eth1.inband/statistics/rx_packets
new file mode 100644
index 0000000..79abba8
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.inband/statistics/rx_packets
@@ -0,0 +1 @@
+1348
diff --git a/craftui/sim/sys/class/net/eth1.inband/statistics/tx_bytes b/craftui/sim/sys/class/net/eth1.inband/statistics/tx_bytes
new file mode 100644
index 0000000..cf34894
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.inband/statistics/tx_bytes
@@ -0,0 +1 @@
+442098
diff --git a/craftui/sim/sys/class/net/eth1.inband/statistics/tx_dropped b/craftui/sim/sys/class/net/eth1.inband/statistics/tx_dropped
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.inband/statistics/tx_dropped
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.inband/statistics/tx_errors b/craftui/sim/sys/class/net/eth1.inband/statistics/tx_errors
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.inband/statistics/tx_errors
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.inband/statistics/tx_packets b/craftui/sim/sys/class/net/eth1.inband/statistics/tx_packets
new file mode 100644
index 0000000..ceb9a4e
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.inband/statistics/tx_packets
@@ -0,0 +1 @@
+1403
diff --git a/craftui/sim/sys/class/net/eth1.peer/statistics/collisions b/craftui/sim/sys/class/net/eth1.peer/statistics/collisions
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.peer/statistics/collisions
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.peer/statistics/multicast b/craftui/sim/sys/class/net/eth1.peer/statistics/multicast
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.peer/statistics/multicast
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.peer/statistics/rx_bytes b/craftui/sim/sys/class/net/eth1.peer/statistics/rx_bytes
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.peer/statistics/rx_bytes
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.peer/statistics/rx_dropped b/craftui/sim/sys/class/net/eth1.peer/statistics/rx_dropped
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.peer/statistics/rx_dropped
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.peer/statistics/rx_errors b/craftui/sim/sys/class/net/eth1.peer/statistics/rx_errors
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.peer/statistics/rx_errors
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.peer/statistics/rx_packets b/craftui/sim/sys/class/net/eth1.peer/statistics/rx_packets
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.peer/statistics/rx_packets
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.peer/statistics/tx_bytes b/craftui/sim/sys/class/net/eth1.peer/statistics/tx_bytes
new file mode 100644
index 0000000..775971e
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.peer/statistics/tx_bytes
@@ -0,0 +1 @@
+648
diff --git a/craftui/sim/sys/class/net/eth1.peer/statistics/tx_dropped b/craftui/sim/sys/class/net/eth1.peer/statistics/tx_dropped
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.peer/statistics/tx_dropped
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.peer/statistics/tx_errors b/craftui/sim/sys/class/net/eth1.peer/statistics/tx_errors
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.peer/statistics/tx_errors
@@ -0,0 +1 @@
+0
diff --git a/craftui/sim/sys/class/net/eth1.peer/statistics/tx_packets b/craftui/sim/sys/class/net/eth1.peer/statistics/tx_packets
new file mode 100644
index 0000000..45a4fb7
--- /dev/null
+++ b/craftui/sim/sys/class/net/eth1.peer/statistics/tx_packets
@@ -0,0 +1 @@
+8
diff --git a/craftui/sim/tmp/glaukus/modem.json b/craftui/sim/tmp/glaukus/modem.json
index ba7f021..9b51b70 100644
--- a/craftui/sim/tmp/glaukus/modem.json
+++ b/craftui/sim/tmp/glaukus/modem.json
@@ -1 +1 @@
-{"firmware":"\/etc\/glaukus\/firmware\/bcm85100mc_1007128.fw","network":{"rxCounters":{"broadcast":15449,"bytes":5662990,"crcErrors":0,"frames":17417,"frames1024_1518":0,"frames128_255":1968,"frames256_511":15449,"frames512_1023":0,"frames64":0,"frames65_127":0,"framesJumbo":0,"framesUndersized":0,"multicast":1968,"unicast":0},"status":0,"statusStr":"UP","txCounters":{"broadcast":0,"bytes":0,"crcErrors":0,"frames":0,"frames1024_1518":0,"frames128_255":0,"frames256_511":0,"frames512_1023":0,"frames64":0,"frames65_127":0,"framesJumbo":0,"framesUndersized":0,"multicast":0,"unicast":0}},"profile":"\/etc\/glaukus\/profiles\/C01_1007128_1500_48_C1_260X300_3c3f2.bin","status":{"absoluteMse":-32768,"acmEngineRxSensorsEnabled":true,"acmEngineTxSwitchEnabled":true,"acquireStatus":0,"acquireStatusStr":"Acquire in progress","carrierOffset":-3749990,"debugIndications":2,"externalAgc":122,"internalAgc":560,"lastAcquireError":3,"lastAcquireErrorStr":"Acquisition failed at frequency sweep.","normalizedMse":-32693,"radialMse":-32663,"resPhNoiseVal":0,"rxAcmProfile":0,"rxSymbolRate":374830203,"txAcmProfile":6,"txSymbolRate":375000000},"temperature":50.19281005859375,"transmitter":{"dcLeakageI":0,"dcLeakageQ":0,"mode":0,"modeStr":"NORMAL","sweepTime":0,"toneFreq":0,"toneSecFreq":0},"version":{"build":128,"chipType":"BCM85100IFSBG","major":100,"minor":7}}
\ No newline at end of file
+{"firmware":"\/etc\/glaukus\/firmware\/bcm85100mc_1007128.fw","network":{"rxCounters":{"broadcast":10859,"bytes":3978230,"crcErrors":0,"frames":12225,"frames1024_1518":0,"frames128_255":1366,"frames256_511":10859,"frames512_1023":0,"frames64":0,"frames65_127":0,"framesJumbo":0,"framesUndersized":0,"multicast":1366,"unicast":0},"status":0,"statusStr":"UP","txCounters":{"broadcast":10857,"bytes":3977010,"crcErrors":0,"frames":12219,"frames1024_1518":0,"frames128_255":1362,"frames256_511":10857,"frames512_1023":0,"frames64":0,"frames65_127":0,"framesJumbo":0,"framesUndersized":0,"multicast":1362,"unicast":0}},"profile":"\/etc\/glaukus\/profiles\/16qam_tx26bo_rx29bo_1500M_v128.bin","status":{"absoluteMse":-275,"acmEngineRxSensorsEnabled":false,"acmEngineTxSwitchEnabled":false,"acquireStatus":1,"acquireStatusStr":"Modem is locked","carrierOffset":35908,"debugIndications":12,"externalAgc":184,"internalAgc":182,"lastAcquireError":0,"lastAcquireErrorStr":"No Error","normalizedMse":-200,"radialMse":-204,"resPhNoiseVal":35,"rxAcmProfile":3,"rxSymbolRate":1499998768,"txAcmProfile":3,"txSymbolRate":1500000000},"temperature":67.972015380859375,"transmitter":{"dcLeakageI":0,"dcLeakageQ":0,"mode":0,"modeStr":"NORMAL","sweepTime":0,"toneFreq":0,"toneSecFreq":0},"version":{"build":128,"chipType":"BCM85100IFSBG","major":100,"minor":7}}
\ No newline at end of file
diff --git a/craftui/sim/tmp/glaukus/radio.json b/craftui/sim/tmp/glaukus/radio.json
index 4946225..eec8519 100644
--- a/craftui/sim/tmp/glaukus/radio.json
+++ b/craftui/sim/tmp/glaukus/radio.json
@@ -1 +1 @@
-{"heaterEnabled":false,"hiTransceiver":{"epot":{"control":"auto","driver":0,"lna":0,"pa":0},"icModel":"BGT80","mode":"tx","pll":{"frequency":85500000,"lockCounts":0,"locked":true},"temp":1481},"loTransceiver":{"epot":{"control":"auto","driver":0,"lna":0,"pa":0},"icModel":"BGT70","mode":"rx","pll":{"frequency":75500000,"lockCounts":0,"locked":true},"temp":1671},"paLnaPowerEnabled":true,"paLnaPowerStatus":"normal","rx":{"agcDigitalGain":42,"agcDigitalGainIndex":3,"lnaCurrentMeas":0,"lnaCurrentSet":0,"rssi":1342},"transceiversPowerEnabled":true,"tx":{"dcI":63,"dcQ":63,"driverCurrentMeas":0,"driverCurrentSet":0,"paCurrentMeas":1428,"paCurrentSet":0,"paTemp":4084,"txPowerControl":"auto","txPowerMeas":2895,"txPowerSet":0,"vgaGain":14},"version":{"hardware":{"major":1,"minor":1,"type":"Chimera V1 000001"},"software":{"build":0,"major":0,"minor":1}}}
\ No newline at end of file
+{"heaterEnabled":false,"hiTransceiver":{"epot":{"control":"auto","driver":3,"lna":10,"pa":10},"icModel":"BGT80","mode":"rx","pll":{"frequency":81500000,"lockCounts":0,"locked":true},"temp":42.2089},"loTransceiver":{"epot":{"control":"auto","driver":126,"lna":10,"pa":126},"icModel":"BGT70","mode":"tx","pll":{"frequency":71500000,"lockCounts":0,"locked":true},"temp":48.507800000000006},"mcuTemp":-128,"paLnaPowerEnabled":true,"paLnaPowerStatus":"normal","rx":{"agcDigitalGain":36,"agcDigitalGainIndex":1,"lnaCurrentMeas":0.049,"lnaCurrentSet":0,"rssi":1578},"transceiversPowerEnabled":true,"tx":{"dcI":41,"dcQ":29,"driverCurrentMeas":0.2157,"driverCurrentSet":0,"paCurrentMeas":0.3201,"paCurrentSet":0,"paPowerMeas":12.6419,"paPowerSet":1146588,"paTemp":125,"txPowerControl":"auto","txPowerMeas":-4.2567000000000006,"txPowerSet":0,"vgaGain":18},"version":{"hardware":{"major":2,"minor":1,"type":"Chimera V2 000001"},"software":{"build":0,"major":0,"minor":2}}}
\ No newline at end of file
diff --git a/craftui/www/config.thtml b/craftui/www/config.thtml
new file mode 100644
index 0000000..ca94b22
--- /dev/null
+++ b/craftui/www/config.thtml
@@ -0,0 +1,167 @@
+<html>
+<head>
+  <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+  <meta content="utf-8" http-equiv="encoding">
+  <script src="static/jquery-2.1.4.min.js"></script>
+  <link rel="stylesheet" type="text/css" href="static/craft.css">
+  <link rel=icon href=static/favicon.ico>
+  <link rel=stylesheet href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700&amp;lang=en">
+  <link rel=stylesheet href=static/default.css>
+</head>
+<body>
+  <header>
+    <section>
+      <h1><img src=static/logo.png alt="Google Fiber"></h1>
+      <nav>
+        <ul>
+          <li ><a href=/>GFCH100</a></li>
+          <li class=active><a href=/config>Configuration</a></li>
+          <li ><a href={{ peerurl }} target=_blank>Peer</a></li>
+        </ul>
+      </nav>
+    </section>
+  </header>
+  <br>
+  <div class="tabs">
+    <div class="tab">
+      <input type="radio" id="tab-1" name="tab-group-1" checked>
+      <label for="tab-1">Site Configuration</label>
+      <div class="content">
+	<b>Platfrom Parameters:</b>
+	<table>
+	  <tr>
+	    <td align=center><b>Parameter
+	    <td align=center><b>Active Value
+	    <td align=center><b>Last Configured
+	    <td align=center><b>Configure
+	    <td align=center><b>Status
+
+	  <tr>
+	    <td><b>Craft IP Address
+	    <td align=right><span id="platform/active_craft_inet">...</span>
+	    <td align=right>
+	      <span id="platform/craft_ipaddr">...</span>
+	      <input type=submit value="Apply Now" onclick="CraftUI.config('craft_ipaddr', 1)">
+	    <td>
+	      <input id=craft_ipaddr type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('craft_ipaddr')">
+	    <td>
+	      <span id=craft_ipaddr_result>...</span>
+
+	  <tr>
+	    <td><b>Link IP Address
+	    <td align=right><span id="platform/active_link_inet">...</span>
+	    <td align=right>
+	      <span id="platform/link_ipaddr">...</span>
+	      <input type=submit value="Apply Now" onclick="CraftUI.config('link_ipaddr', 1)">
+	    <td>
+	      <input id=link_ipaddr type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('link_ipaddr')">
+	    <td>
+	      <span id=link_ipaddr_result>...</span>
+
+	  <tr>
+	    <td><b>Peer IP Address
+	    <td align=right>N/A
+	    <td align=right>
+	      <span id="platform/peer_ipaddr">...</span>
+	      <input type=submit value="Apply Now" onclick="CraftUI.config('peer_ipaddr', 1)">
+	    <td>
+	      <input id=peer_ipaddr type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('peer_ipaddr')">
+	    <td>
+	      <span id=peer_ipaddr_result>...</span>
+
+	  <tr>
+	    <td><b>Link VLAN (to peer)
+	    <td align=right><span id="platform/active_link_vlan">...</span>
+	    <td align=right>
+	      <span id="platform/vlan_link">...</span>
+	      <input type=submit value="Apply Now" onclick="CraftUI.config('vlan_peer', 1)">
+	    <td>
+	      <input id=vlan_peer type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('vlan_peer')">
+	    <td>
+	      <span id=vlan_peer_result>...</span>
+
+	  <tr>
+	    <td><b>In-band Management VLAN
+	    <td align=right><span id="platform/active_inband_vlan">...</span>
+	    <td align=right>
+	      <span id="platform/vlan_inband">...</span>
+	      <input type=submit value="Apply Now" onclick="CraftUI.config('vlan_inband', 1)">
+	    <td>
+	      <input id=vlan_inband type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('vlan_inband')">
+	    <td>
+	      <span id=vlan_inband_result>...</span>
+
+	</table>
+	<b>Radio Parameters:</b>
+	<table>
+	  <tr>
+	    <td align=center><b>Parameter
+	    <td align=center><b>Active Value
+	    <td align=center><b>Configure and Apply
+	    <td align=center><b>Status
+
+	  <tr>
+	    <td><b>High Frequency
+	    <td align=right><span id="radio/hiTransceiver/pll/frequency">...</span>
+	    <td>
+	      <input id=freq_hi type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('freq_hi')">
+	    <td>
+	      <span id=freq_hi_result>...</span>
+
+	  <tr>
+	    <td><b>Low Frequency
+	    <td align=right><span id="radio/loTransceiver/pll/frequency">...</span>
+	    <td>
+	      <input id=freq_lo type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('freq_lo')">
+	    <td>
+	      <span id=freq_lo_result>...</span>
+
+	  <tr>
+	    <td><b>High Frequency Mode
+	    <td align=right><span id="radio/hiTransceiver/mode">...</span>
+	    <td>
+	      <input id=mode_hi type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('mode_hi')">
+	    <td>
+	      <span id=mode_hi_result>...</span>
+
+	  <tr>
+	    <td><b>Power Level
+	    <td align=right><span id="radio/tx/paPowerSet">...</span>
+	    <td>
+	      <input id=tx_powerlevel type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('tx_powerlevel')">
+	    <td>
+	      <span id=tx_powerlevel_result>...</span>
+
+	  <tr>
+	    <td><b>Power Enabled
+	    <td align=right><span id="radio/paLnaPowerEnabled">...</span>
+	    <td>
+	      <input id=tx_on type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('tx_on')">
+	    <td>
+	      <span id=tx_on_result>...</span>
+
+	</table>
+      </div>
+    </div>
+    <div class="tab">
+      <input type="radio" id="tab-2" name="tab-group-1">
+      <label for="tab-2">Debug</label>
+      <div class="content">
+        <b>refreshCount:</b><span class="values" id="platform/refreshCount">...</span><br>
+        <b>unhandled xml:</b><span class="values" id="unhandled"></span>
+      </div>
+    </div>
+  </div>
+  <script src="static/craft.js"></script>
+</body>
+</html>
diff --git a/craftui/www/index.thtml b/craftui/www/index.thtml
index 4388f77..5d3e841 100644
--- a/craftui/www/index.thtml
+++ b/craftui/www/index.thtml
@@ -11,11 +11,11 @@
 <body>
   <header>
     <section>
-      <h1><a href=https://fiber.google.com/myfiber/><img src=static/logo.png alt="Google Fiber"></a></h1>
+      <h1><img src=static/logo.png alt="Google Fiber"></h1>
       <nav>
         <ul>
           <li class=active><a href=/>GFCH100</a></li>
-          <li ><a href=craft/config/>Configuration</a></li>
+          <li ><a href=/config>Configuration</a></li>
           <li ><a href={{ peerurl }} target=_blank>Peer</a></li>
         </ul>
       </nav>
@@ -27,6 +27,7 @@
       <input type="radio" id="tab-1" name="tab-group-1" checked>
       <label for="tab-1">Platform</label>
       <div class="content">
+        <b>Serial Number:</b><span class="values" id="platform/serialno">...</span><br>
         <b>Platform:</b><span class="values" id="platform/platform">...</span><br>
         <b>Software Version:</b><span class="values" id="platform/version">...</span><br>
         <b>Software Date:</b><span class="values" id="platform/softwaredate">...</span><br>
@@ -38,68 +39,227 @@
       <input type="radio" id="tab-2" name="tab-group-1">
       <label for="tab-2">Network</label>
       <div class="content">
-        <b>Craft Port IP Address:</b><span class="values" id="platform/craft_ipaddr">...</span><br>
-        <b>Local IP Address:</b><span class="values" id="platform/local_ipaddr">...</span><br>
-        <b>Peer IP Address:</b><span class="values" id="platform/peer_ipaddr">...</span><br>
-        <b>Inband Vlan:</b><span class="values" id="platform/vlan_inband">...</span><br>
-        <b>Peer Vlan:</b><span class="values" id="platform/vlan_peer">...</span><br>
+        <b>IP Addresses:</b>
+	<table>
+	  <tr>
+            <td align=center><b>Port</b></td>
+            <td align=center><b>IPv4</b></td>
+            <td align=center><b>IPv6</b></td></tr>
+	  <tr>
+            <td><b>Craft</b></td>
+	    <td align=right><span id="platform/active_craft_inet">...</span></td>
+	    <td align=right><span id="platform/active_craft_inet6">...</span></td></tr>
+	  <tr>
+            <td><b>Out-of-Band (PoE)</b></td>
+	    <td align=right><span id="platform/active_poe_inet">...</span></td>
+	    <td align=right><span id="platform/active_poe_inet6">...</span></td></tr>
+	  <tr>
+            <td><b>In-Band</b></td>
+	    <td align=right><span id="platform/active_inband_inet">...</span></td>
+	    <td align=right><span id="platform/active_inband_inet6">...</span></td></tr>
+	  <tr>
+            <td><b>Link (to peer)</b></td>
+	    <td align=right><span id="platform/active_link_inet">...</span></td>
+	    <td align=right><span id="platform/active_link_inet6">...</span></td></tr>
+	</table>
+        <b>Packet Counters:</b>
         <table>
           <tr>
             <td><b></b></td>
-            <td colspan=3><b>received</b></td>
-            <td colspan=3><b>transmitted</b></td></tr>
+            <td colspan=3 align=center><b>received</b></td>
+            <td colspan=3 align=center><b>transmitted</b></td>
+            <td colspan=9 align=center><b>errors</b></td></tr>
           <tr>
-            <td><b>interface</b></td>
-            <td><b>bytes (UC BC MC)</b></td>
-            <td><b>frames (<64 <128 <256 <512 <1024 jumbo)</b></td>
-            <td><b>errors</b></td>
-            <td><b>bytes (UC BC MC)</b></td>
-            <td><b>frames</b></td>
-            <td><b>errors</b></td></tr>
+            <td align=center><b>interface</b></td>
+
+            <td align=center><b>bytes</b></td>
+            <td align=center><b>frames</b></td>
+            <td align=center><b>multicast</b></td>
+
+            <td align=center><b>bytes</b></td>
+            <td align=center><b>frames</b></td>
+            <td align=center><b>multicast</b></td>
+
+            <td align=center><b>rx errors</b></td>
+            <td align=center><b>rx dropped</b></td>
+            <td align=center><b>rx CRC</b></td>
+            <td align=center><b>rx Undersize</b></td>
+            <td align=center><b>tx errors</b></td>
+            <td align=center><b>tx dropped</b></td>
+            <td align=center><b>tx CRC</b></td>
+            <td align=center><b>tx Undersize</b></td>
+            <td align=center><b>collisions</b></td>
           <tr>
-            <td>modem (from/to switch)</td>
-            <td>
-              <span id="modem/network/rxCounters/bytes">...</span>
-              (<span id="modem/network/rxCounters/unicast">...</span>
-              <span id="modem/network/rxCounters/broadcast">...</span>
-              <span id="modem/network/rxCounters/multicast">...</span>)</td>
-            <td>
-              <span id="modem/network/rxCounters/frames">...</span>
-              (<span id="modem/network/rxCounters/frames64">...</span>
-              <span id="modem/network/rxCounters/frames65_127">...</span>
-              <span id="modem/network/rxCounters/frames128_255">...</span>
-              <span id="modem/network/rxCounters/frames256_511">...</span>
-              <span id="modem/network/rxCounters/frames512_1023">...</span>
-              <span id="modem/network/rxCounters/frames1024_1518">...</span>
-              <span id="modem/network/rxCounters/framesJumbo">...</span>)</td>
-            <td>
-              CRC: <span id="modem/network/rxCounters/crcErrors">...</span>
-              Undersize: <span id="modem/network/rxCounters/framesUndersized">...</span></td>
-            <td>
-              <span id="modem/network/txCounters/bytes">...</span>
-              (<span id="modem/network/txCounters/unicast">...</span>
-              <span id="modem/network/txCounters/broadcast">...</span>
-              <span id="modem/network/txCounters/multicast">...</span>)</td>
-            <td>
-              <span id="modem/network/txCounters/frames">...</span>
-              (<span id="modem/network/txCounters/frames64">...</span>
-              <span id="modem/network/txCounters/frames65_127">...</span>
-              <span id="modem/network/txCounters/frames128_255">...</span>
-              <span id="modem/network/txCounters/frames256_511">...</span>
-              <span id="modem/network/txCounters/frames512_1023">...</span>
-              <span id="modem/network/txCounters/frames1024_1518">...</span>
-              <span id="modem/network/txCounters/framesJumbo">...</span>)</td>
-            <td>
-              CRC: <span id="modem/network/txCounters/crcErrors">...</span>
-              Undersize: <span id="modem/network/txCounters/framesUndersized">...</span></td></tr>
+            <td><b>Modem (from/to switch)<b></td>
+            <td align=right><span id="modem/network/rxCounters/bytes">...</span></td>
+            <td align=right><span id="modem/network/rxCounters/frames">...</span></td>
+	    <td align=right><span id="modem/network/rxCounters/multicast">...</span></td>
+
+            <td align=right><span id="modem/network/txCounters/bytes">...</span></td>
+            <td align=right><span id="modem/network/txCounters/frames">...</span></td>
+	    <td align=right><span id="modem/network/txCounters/multicast">...</span></td>
+
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="modem/network/rxCounters/crcErrors">...</span></td>
+            <td align=right><span id="modem/network/rxCounters/framesUndersized">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="modem/network/txCounters/crcErrors">...</span></td>
+            <td align=right><span id="modem/network/txCounters/framesUndersized">...</span></td>
+            <td align=right>-</td></tr>
+
+	  <tr>
+            <td><b>Craft<b></td>
+            <td align=right><span id="platform/craft_rx_bytes">...</span></td>
+            <td align=right><span id="platform/craft_rx_packets">...</span></td>
+            <td align=right><span id="platform/craft_multicast">...</span></td>
+
+            <td align=right><span id="platform/craft_tx_bytes">...</span></td>
+            <td align=right><span id="platform/craft_tx_packets">...</span></td>
+            <td align=right>-</td>
+
+            <td align=right><span id="platform/craft_rx_errors">...</span></td>
+            <td align=right><span id="platform/craft_rx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/craft_tx_errors">...</span></td>
+            <td align=right><span id="platform/craft_tx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/craft_collisions">...</span></td>
+
+	  <tr>
+            <td><b>Out-of-Band (PoE)<b></td>
+            <td align=right><span id="platform/poe_rx_bytes">...</span></td>
+            <td align=right><span id="platform/poe_rx_packets">...</span></td>
+            <td align=right><span id="platform/poe_multicast">...</span></td>
+
+            <td align=right><span id="platform/poe_tx_bytes">...</span></td>
+            <td align=right><span id="platform/poe_tx_packets">...</span></td>
+            <td align=right>-</td>
+
+            <td align=right><span id="platform/poe_rx_errors">...</span></td>
+            <td align=right><span id="platform/poe_rx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/poe_tx_errors">...</span></td>
+            <td align=right><span id="platform/poe_tx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/poe_collisions">...</span></td>
+
+	  <tr>
+            <td><b>In-Band<b></td>
+            <td align=right><span id="platform/inband_rx_bytes">...</span></td>
+            <td align=right><span id="platform/inband_rx_packets">...</span></td>
+            <td align=right><span id="platform/inband_multicast">...</span></td>
+
+            <td align=right><span id="platform/inband_tx_bytes">...</span></td>
+            <td align=right><span id="platform/inband_tx_packets">...</span></td>
+            <td align=right>-</td>
+
+            <td align=right><span id="platform/inband_rx_errors">...</span></td>
+            <td align=right><span id="platform/inband_rx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/inband_tx_errors">...</span></td>
+            <td align=right><span id="platform/inband_tx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/inband_collisions">...</span></td>
+
+	  <tr>
+            <td><b>Link (to peer)<b></td>
+            <td align=right><span id="platform/link_rx_bytes">...</span></td>
+            <td align=right><span id="platform/link_rx_packets">...</span></td>
+            <td align=right><span id="platform/link_multicast">...</span></td>
+
+            <td align=right><span id="platform/link_tx_bytes">...</span></td>
+            <td align=right><span id="platform/link_tx_packets">...</span></td>
+            <td align=right>-</td>
+
+            <td align=right><span id="platform/link_rx_errors">...</span></td>
+            <td align=right><span id="platform/link_rx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/link_tx_errors">...</span></td>
+            <td align=right><span id="platform/link_tx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/link_collisions">...</span></td>
+
           <tr>
-            <td>SOC (from/to switch)</td>
+            <td><b>Switch Port 0/0 (PoE)</b></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+
           <tr>
-            <td>SOC (port 0)</td>
+            <td><b>Switch Port 0/4 (SOC)</b></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+
           <tr>
-            <td>POE (port 0)</td>
+            <td><b>Switch Port 0/24 (modem)</b></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+
           <tr>
-            <td>POE (port 0)</td>
+            <td><b>Switch Port 0/25 (SFP+)</b></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
         </table>
       </div>
     </div>
diff --git a/craftui/www/static/craft.js b/craftui/www/static/craft.js
index 620ff71..04e0f2c 100644
--- a/craftui/www/static/craft.js
+++ b/craftui/www/static/craft.js
@@ -68,9 +68,6 @@
       CraftUI.flattenAndUpdateFields(list, '');
     }
     CraftUI.updateField('unhandled', self.unhandled);
-    if (self.unhandled.length > 0) {
-      console.log(self.unhandled);
-    }
   };
   var payload = [];
   payload.push('checksum=' + encodeURIComponent(CraftUI.info.checksum));
@@ -79,4 +76,35 @@
   xhr.send();
 };
 
+CraftUI.config = function(key, activate) {
+  // POST as json
+  var el = document.getElementById(key);
+  var value = el.value;
+  var xhr = new XMLHttpRequest();
+  xhr.open('post', 'content.json');
+  xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
+  var data;
+  if (activate) {
+    data = { config: [ { [key + "_activate"]: "true" } ] };
+  } else {
+    data = { config: [ { [key]: value } ] };
+  }
+  var txt = JSON.stringify(data);
+  var resultid = key + "_result"
+  var el = document.getElementById(resultid);
+  xhr.onload = function(e) {
+    var json = JSON.parse(xhr.responseText);
+    if (json.error == 0) {
+      el.innerHTML = "Success!";
+    } else {
+      el.innerHTML = "Error: " + json.errorstring;
+    }
+    CraftUI.getInfo();
+  }
+  xhr.onerror = function(e) {
+    el.innerHTML = xhr.statusText + xhr.responseText;
+  }
+  xhr.send(txt);
+};
+
 new CraftUI();