Browse Source

Interfaces mostly working.

Tomislav Cvetic 5 years ago
parent
commit
211bc52361

+ 2 - 0
.gitignore

@@ -1,6 +1,8 @@
+backend/src/openocd*
 backend/static/
 node_modules/
 venv/
+bin/
 .DS_Store
 *.log
 haters/

+ 20 - 4
backend/index.js

@@ -14,13 +14,29 @@ const cookieParser = require('cookie-parser')
 const bodyParser = require('body-parser')
 const cors = require('cors')
 const express = require('express')
-const { resolvers } = require('./src/resolvers')
+const { merge } = require('lodash')
 const { db, populateUser } = require('./src/db')
 const { uploadMiddleware, handleFile } = require('./src/file')
 const { authenticate } = require('./src/authenticate')
 
+const prismaResolvers = require('./src/resolvers')
+const system = require('./src/system')
+const interfaces = require('./src/interfaces')
+
+const typeDefs = [
+  './schema.graphql',
+  system.typeDefs,
+  interfaces.typeDefs
+]
+
+const resolvers = merge(
+  system.resolvers,
+  interfaces.resolvers,
+  prismaResolvers.resolvers
+)
+
 const server = new GraphQLServer({
-  typeDefs: './schema.graphql',
+  typeDefs,
   resolvers,
   context: req => ({
     ...req,
@@ -31,11 +47,11 @@ const server = new GraphQLServer({
 
 server.express.use(cookieParser())
 server.express.use(bodyParser.json())
+server.express.use(authenticate)
+server.express.use(populateUser)
 server.express.use(cors({ origin: process.env.FRONTEND_URL, credentials: true }))
 server.express.use('/static', express.static('static'))
 server.express.post('/upload', uploadMiddleware.single('file'), handleFile)
-server.express.use(authenticate)
-server.express.use(populateUser)
 
 server.start(
   {

+ 5 - 0
backend/package-lock.json

@@ -6143,6 +6143,11 @@
       "resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz",
       "integrity": "sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg=="
     },
+    "semaphore-async-await": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/semaphore-async-await/-/semaphore-async-await-1.5.1.tgz",
+      "integrity": "sha1-hXvvXjZEYBykuVcLh+nfXKEpdPo="
+    },
     "semver": {
       "version": "5.7.0",
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",

+ 1 - 0
backend/package.json

@@ -20,6 +20,7 @@
     "prisma-binding": "^2.3.10",
     "prisma-client-lib": "^1.30.1",
     "python-shell": "^1.0.7",
+    "semaphore-async-await": "^1.5.1",
     "standard": "^12.0.1"
   },
   "devDependencies": {

+ 0 - 1
backend/schema.graphql

@@ -3,7 +3,6 @@ scalar Upload
 type Query {
   projects: [Project]!
   uploads: [File]
-  connectionCommand(connectionId: String!, command: String!): String!
   me: User!
 }
 

+ 212 - 0
backend/src/interfaces.js

@@ -0,0 +1,212 @@
+const fs = require('fs')
+const md5 = require('md5')
+const { promisify } = require('util')
+const PythonWorker = require('./pythonWorker')
+
+const readdir = promisify(fs.readdir)
+
+const state = {
+  workers: [],
+  interfaces: [],
+  ports: [],
+  connections: []
+}
+
+const typeDefs = `
+  type Option {
+    name: String!
+    type: String!
+    description: String
+    values: [String!]
+  }
+
+  type Port {
+    id: String!
+    device: String!
+    interfaceName: String!
+    name: String
+    description: String
+  }
+
+  type Connection {
+    id: ID!
+    interfaceName: String!
+    device: String!
+  }
+
+  type Worker {
+    interfaceName: String!
+    workerScript: String!
+  }
+
+  type Interface {
+    interfaceName: String!
+    workerScript: String!
+    ports: [Port]!
+    connections: [Connection]!
+    options: [Option]!
+  }
+
+  extend type Query {
+    interfaces(force: Boolean): [Interface]!
+    ports(interfaceName: String!, force: Boolean): [Port]!
+    connections: [Connection]!
+  }
+
+  extend type Mutation {
+    connect(interfaceName: String!, device: String!): Connection!
+    connectionCommand(connectionId: ID!, type: String!, string: String!, options: String): String!
+  }
+`
+
+async function findWorkers () {
+  // Find all files in ./python_workers that end in _worker.py
+  const fileNames = await readdir(`${__dirname}/python_workers`)
+  const workerFiles = fileNames.filter(fileName => fileName.includes('_worker.py'))
+
+  // Find the added workers
+  workerFiles.forEach(workerFile => {
+    const interfaceName = workerFile.replace(/_worker\.py/, '')
+    if (state.workers.find(worker => worker.interfaceName === interfaceName)) return null
+    const workerScript = `${__dirname}/python_workers/${workerFile}`
+    state.workers.push({
+      interfaceName,
+      workerScript
+    })
+  })
+}
+
+async function findInterfaces () {
+  // Try to identify workers if necessary
+  if (!state.workers.length) await findWorkers()
+
+  // Generate an interface for each worker
+  state.workers.forEach(worker => {
+    const { workerScript, interfaceName } = worker
+    // Skip existing interfaces
+    if (state.interfaces.find(iface => iface.interfaceName === interfaceName)) return null
+    state.interfaces.push({
+      interfaceName,
+      workerScript,
+      worker: new PythonWorker(workerScript),
+      ports: [],
+      connections: [],
+      options: []
+    })
+  })
+}
+
+async function interfaces (parent, args, ctx, info) {
+  const { force } = args
+  // Try to identify interfaces if necessary
+  if (!state.interfaces.length || force) await findInterfaces()
+
+  return state.interfaces.map(iface => {
+    const { workerScript, interfaceName } = iface
+    return {
+      interfaceName,
+      workerScript,
+      ports: ports(interfaceName, args, ctx, info),
+      connections: connections(interfaceName, args, ctx, info),
+      options: options(interfaceName, args, ctx, info)
+    }
+  })
+}
+
+async function findPorts (interfaceName) {
+  // Generate all ports for the interface
+  const iface = state.interfaces.find(iface => iface.interfaceName === interfaceName)
+
+  const { data, error } = await iface.worker.send({ type: 'ports' })
+  if (error) throw new Error(JSON.stringify(error))
+  data.forEach(port => {
+    const id = port.name || port.device
+    // Skip existing ports
+    if (state.ports.find(port => port.id === id)) return null
+    const newPort = {
+      id,
+      interfaceName,
+      ...port
+    }
+    state.ports.push(newPort)
+    iface.ports.push(newPort)
+  })
+}
+
+async function ports (parent, args, ctx, info) {
+  const { force, interfaceName } = args
+  const ifName = interfaceName || parent
+
+  if (ifName) {
+    const iface = state.interfaces.find(iface => iface.interfaceName === ifName)
+
+    // Try to find ports if necessary
+    if (!iface.ports.length || force) await findPorts(ifName)
+    return iface.ports
+  } else {
+    return state.ports
+  }
+}
+
+async function findOptions (interfaceName) {
+  const iface = state.interfaces.find(iface => iface.interfaceName === interfaceName)
+  const { data, error } = await iface.worker.send({ type: 'options' })
+  if (error) throw new Error(JSON.stringify(error))
+  iface.options.push(...data)
+}
+
+async function options (parent, args, ctx, info) {
+  const iface = state.interfaces.find(iface => iface.interfaceName === parent)
+
+  // Try to find options if necessary
+  if (!iface.options.length) await findOptions(parent)
+  return iface.options
+}
+
+async function connections (parent, args, ctx, info) {
+  if (parent) {
+    const iface = state.interfaces.find(iface => iface.interfaceName === parent)
+    return iface.connections
+  } else {
+    return state.connections
+  }
+}
+
+async function connect (parent, args, ctx, info) {
+  const { interfaceName, device } = args
+  const iface = state.interfaces.find(iface => iface.interfaceName === interfaceName)
+  const id = md5(interfaceName + device)
+  if (iface.connections.find(connection => connection.id === id)) throw new Error('already connected.')
+  const connection = {
+    id,
+    device,
+    interfaceName,
+    worker: new PythonWorker(iface.workerScript)
+  }
+  const { error } = await connection.worker.send({ type: 'connect', device })
+  if (error) throw new Error(error)
+  iface.connections.push(connection)
+  state.connections.push(connection)
+  return connection
+}
+
+async function connectionCommand (parent, args, ctx, info) {
+  const { connectionId, type, string, options } = args
+  const connection = state.connections.find(connection => connection.id === connectionId)
+  const { data } = await connection.worker.send({ type, string, options })
+  return data.response
+}
+
+const resolvers = {
+  Query: {
+    interfaces,
+    ports,
+    connections
+  },
+  Mutation: {
+    connect,
+    connectionCommand
+  }
+}
+
+module.exports = { typeDefs, resolvers }

+ 37 - 0
backend/src/pythonWorker.js

@@ -0,0 +1,37 @@
+const { PythonShell } = require('python-shell')
+const { Lock } = require('semaphore-async-await')
+
+const PYTHON_PATH = `${__dirname}/python_workers/venv/Scripts/python.exe`
+
+const pythonShellDefaultOptions = {
+  pythonPath: PYTHON_PATH,
+  mode: 'json'
+}
+
+function PythonWorker (workerScript, shellOptions) {
+  this.toPythonLock = new Lock()
+  this.fromPythonLock = new Lock()
+  this.data = []
+  this.workerScript = workerScript
+  this.pythonShellOptions = { ...pythonShellDefaultOptions, shellOptions }
+  this.pythonShell = new PythonShell(this.workerScript, this.pythonShellOptions)
+  this.pythonShell.on('message', message => {
+    this.data.push(message)
+    this.fromPythonLock.release()
+  })
+  this.pythonShell.on('error', error => {
+    this.error = error
+  })
+  this.send = async (command) => {
+    await this.toPythonLock.acquire()
+    await this.fromPythonLock.acquire()
+    this.pythonShell.send(command)
+    await this.fromPythonLock.acquire()
+    const res = this.data.pop()
+    this.fromPythonLock.release()
+    this.toPythonLock.release()
+    return res
+  }
+}
+
+module.exports = PythonWorker

+ 3 - 0
backend/src/python_workers/.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "python.pythonPath": "venv\\Scripts\\python.exe"
+}

+ 166 - 0
backend/src/python_workers/JTAG.py

@@ -0,0 +1,166 @@
+from abc import ABCMeta, abstractmethod
+
+class STATE:
+    """
+    Class containing all supported JTAG states.
+    """
+    RESET = 0
+    IDLE = 1
+
+class JTAG(metaclass=ABCMeta):
+    """
+    Abstract Class that defines a general JTAG interface.
+
+    It offers four methods for usage:
+    idle()
+        Brings the JTAG state machine in the idle state
+    reset()
+        Brings the JTAG state machine to the reset state
+    setIR(data, minLength=None)
+        Sets the instruction register to *data*, where *data* is a string containing
+        the binary data for the IR. The first character of *data* is shifted in first.
+
+        If data is an unsigned integer, it is first converted to a binary string.
+
+        *minLength* can be set to shift out data of a certain length. The IR data can be
+        accessed in the objects *dataReg* attribute.
+
+        The state machine will be in the idle state after this command.
+    setDR(data, minLength=None)
+        Sets the data register to *data*, where *data* is a string containing the
+        binary data for the DR. The first character is shifted in last! So you can use MSB first
+        notation.
+
+        If data is an unsigned integer, it is first converted to a binary string.
+
+        *minLength* can be set to shift out data of a certain length. The IR data can be
+        accessed in the objects *dataReg* attribute.
+
+        The state machine will be in the idle state after this command.
+
+    Two methods need to be implemented by a child class, which implement the low level
+    functionality of the JTAG interface:
+    _shiftTAP(self, data)
+        Needs to shift out data to the TAP.
+        The function can assume, that a valid binary string is sent. It has to process the '1' and '0'
+        character and ignore all other characters.
+    _shiftReg(self, data)
+        Needs to shift out data to a Register and record the response.
+        The function can assume, that a valid binary string is sent. It has to process the '1' and '0'
+        character and ignore all other characters.
+    """
+
+    state = STATE.RESET
+    dataReg = ""
+    instReg = ""
+
+    def __init__(self, IRbefore=0, IRafter=0, DRbefore=0, DRafter=0, skipReset=False):
+        """Constructor for the JTAG class."""
+
+        # To be JTAG compliant, allow any number of instruction and data registers before and after the actual data.
+        self.IRbefore = IRbefore
+        self.IRafter = IRafter
+        self.DRbefore = DRbefore
+        self.DRafter = DRafter
+
+        # Start with a clean reset state, unless requested differently
+        if (not skipReset) is True:
+            self.reset()
+
+    def idle(self):
+        """Brings the state machine to the idle state."""
+        if STATE.RESET == self.state:
+            self._shiftTAP("0")
+        elif STATE.IDLE == self.state:
+            pass
+
+    def reset(self):
+        """Brings the state machine to the reset state."""
+        self._shiftTAP("11111111111111111111")
+        self.state = STATE.RESET
+
+    def setIR(self, data, minLen=None):
+        """Sets the instruction register of the JTAG."""
+        data = self.__checkData(data, minLen)
+        if data is None:
+            return
+        if len(data) < minLen:
+            data = "0" * (minLen - len(data)) + data
+
+        # get safely into idle state before doing anything else.
+        self.idle()
+        # get into Shift-IR state
+        self._shiftTAP("01100")
+        # toggle in the before-IR data,
+        out=self._shiftReg("1"*self.IRafter+data+"1"*self.IRbefore)
+        # ... then the actual IR data
+        #out = self._shiftReg(data)
+        # ... and the after IR data.
+        #self._shiftReg("1"*self.IRafter)
+        # get into Pause-IR state
+        self._shiftTAP("10")
+        # store the shifted out IR data.
+        out = out[::-1]
+        out = out[self.IRafter:len(out)-self.IRbefore:]
+
+        self.instReg = out
+        return out
+
+    def setDR(self, data, minLen=None):
+        """Sets the data register of the JTAG."""
+        data = self.__checkData(data, minLen)
+        if data is None:
+            return
+        if len(data) < minLen:
+            # pad data with zeros to fill minimum length.
+            data += "0" * (minLen - len(data))
+
+        # get safely into idle state before doing anything else.
+        self.idle()
+        # get into Shift-DR state
+        self._shiftTAP("0100")
+        # toggle in the before-DR data,
+        #out=self._shiftReg("1"*self.DRafter+data+"1"*self.DRbefore)
+        out=self._shiftReg("1"*self.DRafter+data+"1"*self.DRbefore)
+        # ... then the actual DR data
+        #out = self._shiftReg(data)
+        # ... and the after DR data.
+        #self._shiftReg("1"*self.IRafter)
+        # get into Pause-DR state
+        self._shiftTAP("10")
+        # store the shifted out DR data.
+        out = out[::-1]
+        out = out[self.DRafter:len(out)-self.DRbefore]
+
+        self.dataReg = out
+        return out
+
+    @staticmethod
+    def __checkData(data, minLen):
+        """Checks the data to be sent."""
+        if isinstance(data, object):
+            data = str(data)
+        if isinstance(data, str):
+            validSyms = "01 _'\t"
+            lenValSyms = 0
+            for sym in validSyms:
+                lenValSyms += data.count(sym)
+            if len(data) != lenValSyms:
+                print("Error: Data is not a valid binary string. Data ignored.")
+                return None
+        elif isinstance(data, int) and data >= 0:
+            data = bin(data)[2:]
+        else:
+            print("Error: Data needs to be a binary string or a non negative integer. Data ignored.")
+            return None
+        return data
+
+    @abstractmethod
+    def _shiftTAP(self, data):
+        """Low level function to be implemented by the JTAG device driver."""
+        pass
+
+    @abstractmethod
+    def _shiftReg(self, data):
+        """Low level function to be implemented by the JTAG device driver."""
+        pass

+ 38 - 0
backend/src/python_workers/README.md

@@ -0,0 +1,38 @@
+# Python Workers
+
+A Python worker is expected to do the following:
+
+Interpret information commands
+  * `option` returns possible options for the interface
+  * `ports` returns a list of available ports
+
+Interpret operation commands
+  * `read`
+  * `write`
+  * `ask`
+
+The worker accepts JSON strings and returns JSON strings
+
+The response JSON contains at least one of the following sections:
+* `data` for the results
+* `error` with information about what failed
+
+## Options
+
+The option object shall contain at least the following information:
+* `name`: Name of the option
+* `type`: Type (Int, Float, String)
+
+It can also contain any of these additional informations:
+* `description`: A short description of the option
+* `values`: List of allowed values. If the list contains the empty element, any value of the type is allowed
+
+## Ports
+
+Ports shall contain at least the following information:
+* `device`: information for the driver, how to connect to the device
+* `name`: System name for the device
+
+It can also contain any of the following informations:
+* `description`: Short description
+* `specific`: Interface specific information

+ 360 - 0
backend/src/python_workers/USBBlaster.py

@@ -0,0 +1,360 @@
+import time
+from ctypes import (cdll, byref, c_int, c_uint8, c_char_p, c_void_p, c_uint16, cast,
+                    create_string_buffer, Structure, pointer, POINTER, memmove)
+
+from JTAG import JTAG
+from pylibftdi.device import Device
+from pylibftdi._base import FtdiError
+from pylibftdi.driver import ftdi_device_list
+
+
+class DataLine:
+    """DataLine class supplies the constants that define which data line (DataLine) has which functionality"""
+    TCK = 0x01
+    TMS = 0x02
+    NCE = 0x04
+    NCS = 0x08
+    TDI = 0x10
+    LED = 0x20
+    READ = 0x40
+    SHMODE = 0x80
+    READ_TDO = 0x01
+
+
+# To access libftdi functions in Python use:
+# Device.fdll.ftdi_xxx(1, 2, 3)
+
+# Comment from OpenOCD usb_blaster.c:
+#  Actually, the USB-Blaster offers a byte-shift mode to transmit up to 504 data
+#  bits (bidirectional) in a single USB packet. A header byte has to be sent as
+#  the first byte in a packet with the following meaning:
+#
+#    Bit 7 (0x80): Must be set to indicate byte-shift mode.
+#    Bit 6 (0x40): If set, the USB-Blaster will also read data, not just write.
+#    Bit 5..0:     Define the number N of following bytes
+#
+#  All N following bytes will then be clocked out serially on TDI. If Bit 6 was
+#  set, it will afterwards return N bytes with TDO data read while clocking out
+#  the TDI data. LSB of the first byte after the header byte will appear first
+#  on TDI.
+#
+#
+#  Simple bit banging mode:
+#
+#    Bit 7 (0x80): Must be zero (see byte-shift mode above)
+#    Bit 6 (0x40): If set, you will receive a byte indicating the state of TDO
+#                  in return.
+#    Bit 5 (0x20): Output Enable/LED.
+#    Bit 4 (0x10): TDI Output.
+#    Bit 3 (0x08): nCS Output (not used in JTAG mode).
+#    Bit 2 (0x04): nCE Output (not used in JTAG mode).
+#    Bit 1 (0x02): TMS Output.
+#    Bit 0 (0x01): TCK Output.
+#
+#  For transmitting a single data bit, you need to write two bytes (one for
+#  setting up TDI/TMS/TCK=0, and one to trigger TCK high with same TDI/TMS
+#  held). Up to 64 bytes can be combined in a single USB packet.
+#  It isn't possible to read a data without transmitting data.
+
+class Status:
+    """Status class supplies the constants that define which state (Status) has which meaning"""
+    nFault = 0x08
+    Select = 0x10
+    PError = 0x20
+    nAck = 0x40
+
+
+class USBBlaster(JTAG, Device):
+    """This class is used to communicate with the USB Blaster over the USB port."""
+
+    def __init__(self, debug=False, vid=0x09FB, pid=0x6001, IRbefore=0, IRafter=0, DRbefore=0, DRafter=0,
+                 skipReset=False):
+        # We pass lazy_open=True - meaning, the constructor of Device will not
+        # try to open the device - we will do it ourselves
+        self.debug = debug
+
+        Device.__init__(self, lazy_open=True)
+
+        self.ctx = create_string_buffer(1024)
+        ret_val = self.fdll.ftdi_init(byref(self.ctx))
+        if ret_val != 0:
+            msg = "%s (%d)" % (self.get_error_string(), ret_val)
+            del self.ctx
+            raise FtdiError(msg)
+
+        self.__open_device(vid, pid)
+
+        self.vid = vid
+        self.pid = pid
+
+        # Reset the device
+        self.fdll.ftdi_usb_reset(byref(self.ctx))
+
+        if self.fdll.ftdi_set_latency_timer(byref(self.ctx), 2) < 0:
+            pass #print("Unable to set latency timer")
+
+        latency_timer = c_uint8()
+        if self.fdll.ftdi_get_latency_timer(byref(self.ctx),
+                                            byref(latency_timer)) < 0:
+            pass #print("Unable to get latency timer")
+
+        self.fdll.ftdi_disable_bitbang(byref(self.ctx))
+        # Init the JTAG
+        self.dataLine = 0x00
+
+        # Call JTAG constructor only after USB connection is set-up (it calls
+        # reset so the device must already be setup
+
+        JTAG.__init__(self, IRbefore, IRafter, DRbefore, DRafter, skipReset)
+
+    def __exit__(self):
+        self.__del__()
+
+    def __del__(self):
+        # self.ctx is cleared in the parent class destructor
+        Device.__del__(self)
+
+    def __open_device(self, vid, pid):
+        # Search for all devices with VID & PID - try to open all of them, if
+        # it is opened correctly first one is our device - if not it means
+        # we have also Altera USB Blaster connected (same VID & PID), but with
+        # original Altera's driver - we want to keep that device for Altera tools
+        devlistptrtype = POINTER(ftdi_device_list)
+        dev_list_ptr = devlistptrtype()
+        ret_val = self.fdll.ftdi_usb_find_all(byref(self.ctx),
+                                              byref(dev_list_ptr),
+                                              vid, pid)
+        if ret_val < 0:
+            msg = "%s (%d)" % (self.get_error_string(), ret_val)
+            self.fdll.ftdi_deinit(byref(self.ctx))
+            del self.ctx
+            raise FtdiError(msg)
+        elif ret_val == 0:
+            msg = "No devices found for VID=0x%04x, PID=0x%04x" % (vid, pid)
+            self.fdll.ftdi_deinit(byref(self.ctx))
+            del self.ctx
+            raise FtdiError(msg)
+        else:
+            pass #print("Number of devices found: %d (VID & PID: 0x%04x,0x%04x)" % (
+                 #ret_val, vid, pid))
+            manuf = create_string_buffer(128)
+            desc = create_string_buffer(128)
+            serial = create_string_buffer(128)
+
+            while dev_list_ptr:
+                ret_val = self.fdll.ftdi_usb_get_strings(byref(self.ctx),
+                                                         dev_list_ptr.contents.dev,
+                                                         manuf, 127, desc, 127, serial, 127)
+                if ret_val < 0:
+                    # step to next in linked-list
+                    dev_list_ptr = cast(dev_list_ptr.contents.next,
+                                        devlistptrtype)
+                    continue
+
+                ret_val = self.fdll.ftdi_usb_open_dev(byref(self.ctx),
+                                                      dev_list_ptr.contents.dev)
+                if ret_val == 0:
+                    pass #print("Opened device: %s, %s, Serial: %s" % (
+                         #manuf.value, desc.value, serial.value))
+                    self._opened = True
+                    break
+
+                pass #print("Failed to open device (will try others): %s (%d)" % (
+                     #self.get_error_string(), ret_val))
+                # step to next in linked-list
+                dev_list_ptr = cast(dev_list_ptr.contents.next,
+                                    devlistptrtype)
+
+            if not self._opened:
+                msg = "No suitable devices could be opened"
+                self.fdll.ftdi_deinit(byref(self.ctx))
+                del self.ctx
+                raise FtdiError(msg)
+
+    def __wr(self, data, readBack=False):
+        if isinstance(data, list):
+            if readBack:
+                raise FtdiError("__wr() Readback supported only for 1 bit access")
+
+            w_data = (c_uint8 * len(data))(*data)
+            w_len = len(w_data)
+            if self.debug:
+                pass #print("__wr() writing %d bytes, data=[" % (w_len), end='')
+                for i in range(0, w_len):
+                    if i != w_len - 1:
+                        pass #print("0x%x," % (w_data[i]), end='')
+                    else:
+                        pass #print("0x%x" % (w_data[i]), end='')
+                pass #print("]")
+        else:
+            w_len = 1
+            if self.debug:
+                pass #print("__wr() writing 1 byte, data= [ 0x%x ]" % (data))
+            if readBack:
+                w_data = c_uint8(data | DataLine.READ)
+            else:
+                w_data = c_uint8(data)
+
+        ret_val = self.fdll.ftdi_write_data(byref(self.ctx),
+                                            byref(w_data), w_len)
+        if ret_val < 0:
+            msg = "ftdi_write_data(): %s (%d)" % (self.get_error_string(), ret_val)
+            raise FtdiError(msg)
+
+        if readBack:        
+            numBytes = 1
+            rd_buf = (c_uint8 * (numBytes & 0x3f))()
+            ret_val = self.fdll.ftdi_read_data(byref(self.ctx), byref(rd_buf), numBytes)
+            if ret_val < 0:
+                msg = "ftdi_read_data(): %s (%d)" % (self.get_error_string(), ret_val)
+                raise FtdiError(msg)
+            return rd_buf[0]
+
+    def __rd(self, numBytes=1):
+        # Usually one doesn't want to use this function (shift-mode) but rather
+        # bitbang mode - so check if you want really __wr(data, readBack=True);
+        wr_dat = [DataLine.SHMODE | DataLine.READ | (numBytes & 0x3F)] + [self.dataLine] * numBytes
+
+        if numBytes > 0x3f:
+            msg = "__rd() number of supported bytes is 0x3f (requsted: %x)" % (numBytes)
+            raise FtdiError(msg)
+
+        self.__wr(wr_dat)
+
+        rd_buf = (c_uint8 * (numBytes & 0x3f))()
+        ret_val = self.fdll.ftdi_read_data(byref(self.ctx),
+                                           byref(rd_buf), numBytes)
+        if ret_val < 0:
+            msg = "ftdi_read_data(): %s (%d)" % (self.get_error_string(), ret_val)
+            raise FtdiError(msg)
+        elif ret_val == 0:
+            return []
+        elif ret_val == 1:
+            ret_data = rd_buf[0]
+            if self.debug:
+                pass #print("__rd() returned 1 byte, data=[ 0x%x ]" %ret_data)
+        else:
+            ret_data = []
+            for i in range(0, len(rd_buf)):
+                ret_data.append(int(rd_buf[i]))
+            if self.debug:
+                pass #print("__rd() returned %d bytes, data=[" % (len(ret_data)), end='')
+                for i in range(0, len(ret_data)):
+                    if i != len(ret_data) - 1:
+                        pass #print("0x%x," % (ret_data[i]), end='')
+                    else:
+                        pass #print("0x%x" % (ret_data[i]), end='')
+                pass #print("]")
+        return ret_data
+
+    def _shiftTAP(self, data):
+        for bit in data:
+            if bit not in "01":
+                continue
+            out = (self.dataLine & ~(DataLine.TDI | DataLine.TMS)) | DataLine.NCE | DataLine.NCS | DataLine.LED
+            if bit == '1':
+                out |= DataLine.TMS
+            self.dataLine = out
+            self.__wr(out)
+            self.__wr(out | DataLine.TCK)
+
+    def _shiftReg(self, data):
+        dataOut = ""
+
+        for idx, bit in enumerate(data[::-1]):
+            if bit not in "01":
+                continue
+            out = (self.dataLine & ~(DataLine.TDI | DataLine.TMS)) | DataLine.NCE | DataLine.NCS | DataLine.LED
+            if bit == '1':
+                out |= DataLine.TDI
+            if idx == len(data) - 1:
+                out |= DataLine.TMS
+            self.dataLine = out
+            tmp = self.__wr(out, readBack=True)
+
+            tmp = self.__wr(out | DataLine.TCK, readBack=True)
+            if tmp & DataLine.READ_TDO:
+                dataOut += "1"
+            else:
+                dataOut += "0"
+
+        return dataOut
+
+    def setBit(self, bit, value):
+        if value:
+            self.dataLine |= bit
+        else:
+            self.dataLine &= ~bit
+        self.__wr(self.dataLine)
+
+    def reset(self):
+        JTAG.reset(self)
+        self.__wr(DataLine.NCE | DataLine.NCS | DataLine.LED)
+
+    def toggleJTAGClk(self, times=1, seconds=None):
+        if type(times) is not int or times < 1:
+            pass #print("Error: times has to be a positive integer. No clock cycles applied.")
+            return
+        if seconds:
+            while seconds:
+                self._shiftTAP("0")
+                time.sleep(1)
+                seconds -= 1
+        else:
+            while times:
+                self._shiftTAP("0")
+                times -= 1
+
+    def scanChain(self):
+        # Very stupid hard-coded scan chain - sequence from OpenOCD
+        r_size = 128
+        r_buf = (c_uint8 * r_size)()
+
+        self.reset()
+
+        # Clear first the buffer (if anything was in)
+        ret_val = self.fdll.ftdi_usb_purge_rx_buffer(byref(self.ctx))
+
+        w_data = [0x2f, 0x2c, 0x2d, 0x2e, 0x2f, 0x2c, 0x2d, 0x2c, 0x2d, 0x2c]
+        self.__wr(w_data)
+
+        # Read 0x30 bytes out
+        r_buf = self.__rd(0x30)
+
+        # No answer, not very good reverse-engineered jtag stream :-)
+        r_size = len(r_buf)
+        dev_ids = []
+        dev_cnts = -1
+        non_zero = 0
+        for i in range(0, r_size):
+            if r_buf[i] == 0xff or r_buf[i] == self.dataLine:
+                break
+
+            if i % 4 == 0:
+                dev_cnts += 1
+                dev_ids.append(0)
+
+            if r_buf[i] != 0:
+                non_zero = 1
+            dev_ids[dev_cnts] |= ((r_buf[i]) << (8 * (i % 4)))
+
+        self.reset()
+
+        # Clear first the buffer (if anything was in)
+        ret_val = self.fdll.ftdi_usb_purge_rx_buffer(byref(self.ctx))
+
+        if non_zero == 1:
+            return dev_ids
+        else:
+            return []
+
+
+if __name__ == '__main__':
+    jDev = USBBlaster()
+    dev_ids = jDev.scanChain()
+    if len(dev_ids) > 0:
+        pass #print("Devices found:")
+        for i in range(0, len(dev_ids)):
+            pass #print("tap%d ID=0x%08x" % (i, dev_ids[i]))
+    else:
+        pass #print("No taps/devices found!")

+ 110 - 0
backend/src/python_workers/lauterbach_workerX.py

@@ -0,0 +1,110 @@
+import json
+import ctypes
+from utils import handle_exception
+
+t32api = ctypes.cdll.LoadLibrary('t32api.dll')
+
+class LauterbachWorker:
+
+    connection = None
+
+    @staticmethod
+    def ports():
+        try:
+
+            a = t32api.T32_Config(b'NODE=', b'localhost')
+            b = t32api.T32_Config(b'PACKLEN=', b'1042')
+            c = t32api.T32_Config(b'PORT=', b'20000')
+            d = t32api.T32_Init()
+            e = t32api.T32_Attach(1)
+            f = t32api.T32_Nop()
+            g = t32api.T32_Exit()
+
+            #print(a, b, c, d ,e ,f ,g)
+
+            ports = []
+
+            for port in driverPorts:
+                obj = {
+                    "address": port.address,
+                    "bus": port.bus,
+                    "manufacturer": port.manufacturer,
+                    "product": port.product,
+                    "serial_number": port.serial_number,
+                    "idProduct": port.idProduct,
+                    "idVendor": port.idVendor
+                }
+                ports.append(obj)
+
+            return json.dumps({
+                "data": ports
+            })
+
+        except Exception as error:
+            return handle_exception(error)
+
+    def timeout(self, value):
+        try:
+            self.connection.timeout = value
+            return json.dumps({
+                "data": {
+                    "timeout": value
+                }
+            })
+
+        except Exception as error:
+            return handle_exception(error)
+
+    def connect(self, idVendor, idProduct):
+        try:
+            self.connection = usbtmc.Instrument(idVendor, idProduct)
+            return json.dumps({
+                "data": {
+                    "device": [idVendor, idProduct]
+                }
+            })
+        except Exception as error:
+            return handle_exception(error)
+            
+    def write(self, string):
+        try:
+            self.connection.write(string)
+            return json.dumps({
+                "data": {
+                    "string": string
+                }
+            })
+        except Exception as error:
+            return handle_exception(error)
+
+    def read(self):
+        try:
+            response = self.connection.read()
+
+            return json.dumps({
+                "data": {
+                    "response": response
+                }
+            })
+        except Exception as error:
+            return handle_exception(error)
+
+    def close(self):
+        try:
+            self.connection.close()
+            return json.dumps({
+                "data": {}
+            })
+        except Exception as error:
+            return handle_exception(error)
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        if self.connection is not None:
+            self.connection.close()
+            del self.connection
+            self.connection = None
+
+worker = LauterbachWorker()
+
+with open("lauterbach.json", "w") as file:
+    file.write(worker.ports()+',\n')

+ 4 - 0
backend/src/python_workers/requirements.txt

@@ -0,0 +1,4 @@
+pyusb
+python-usbtmc
+pyserial
+pylibftdi

+ 121 - 0
backend/src/python_workers/serial_worker.py

@@ -0,0 +1,121 @@
+import sys
+import json
+import serial
+import serial.tools.list_ports
+from utils import handle_exception
+
+
+class SerialWorker:
+    """
+    Options offered by the PySerial module
+    * https://pythonhosted.org/pyserial/pyserial_api.html
+    * https://github.com/pyserial/pyserial/tree/master/serial
+    """
+    OPTIONS = [
+        {"name": "port", "type": "String"},
+        {"name": "baudrate", "type": "Int", "values": [50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, 9600, 19200, 38400, 57600, 115200]},
+        {"name": "bytesize", "type": "Int", "values": [5, 6, 7, 8]},
+        {"name": "parity", "type": "String", "values": ["None", "Even", "Odd", "Mark", "Space"]},
+        {"name": "timeout", "type": "Float"},
+        {"name": "write_timeout", "type": "Float"},
+        {"name": "xonxoff", "type": "Bool"},
+        {"name": "rtscts", "type": "Bool"},
+        {"name": "dsrdtr", "type": "Bool"},
+        {"name": "inter_byte_timeout", "type": "Float"},
+        {"name": "newline", "type": "String", "values": ["\r\n", "\n", ""]}
+    ]
+
+    connection = None
+    newLine = "\r\n"
+
+    @staticmethod
+    def ports():
+        driverPorts = serial.tools.list_ports.comports()
+
+        ports = []
+
+        for port in driverPorts:
+            # https://pythonhosted.org/pyserial/tools.html#module-serial.tools.list_ports
+            obj = {
+                "device": port.device,
+                "name": port.name,
+                "description": port.description,
+                "specific": {
+                    "hwid": port.hwid,
+                    "usb": {
+                        "vid": port.vid,
+                        "pid": port.vid,
+                        "serial_number": port.serial_number,
+                        "location": port.location,
+                        "manufacturer": port.manufacturer,
+                        "product": port.product,
+                        "interface": port.interface
+                    }
+                }
+            }
+            ports.append(obj)
+
+        return ports
+
+    def connect(self, device, **kwargs):
+        self.connection = serial.Serial(device, **kwargs)
+        return {
+            "device": device
+        }
+            
+    def write(self, string):
+        encodedString = (string + self.newLine).encode('ascii')
+        self.connection.write(encodedString)
+        return {
+            "string": string,
+            "encodedString": str(encodedString)
+        }
+
+    def read(self):
+        original_response = self.connection.readline()
+        response = original_response.decode('ascii').strip()
+        garbage = []
+        while self.connection.in_waiting:
+            garbage.append(str(self.connection.readline()))
+
+        return {
+            "response": response,
+            "originalResponse": str(original_response),
+            "garbage": garbage
+        }
+
+    def ask(self, string):
+        self.write(string)
+        return self.read()
+
+    def close(self):
+        self.connection.close()
+        return {}
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        if self.connection is not None:
+            self.connection.close()
+            self.connection = None
+
+
+worker = SerialWorker()
+
+for line in sys.stdin:
+    try:
+        command = json.loads(line)
+        if command['type'] == 'ports':
+            res = SerialWorker.ports()
+        elif command['type'] == 'options':
+            res = SerialWorker.OPTIONS
+        elif command['type'] == 'connect':
+            options = command['options'] if 'options' in command else {}
+            res = worker.connect(command['device'], **options)
+        elif command['type'] == 'read':
+            res = worker.read()
+        elif command['type'] == 'write':
+            res = worker.write(command['string'])
+        elif command['type'] == 'ask':
+            res = worker.ask(command['string'])
+        print(json.dumps({ "data": res }), flush=True)
+    except Exception as error:
+        print(handle_exception(error), flush=True)

+ 95 - 0
backend/src/python_workers/usbblaster_workerX.py

@@ -0,0 +1,95 @@
+import json
+import USBBlaster
+from utils import handle_exception
+
+ 
+class USBBlasterWorker:
+
+    connection = None
+
+    @staticmethod
+    def ports():
+        try:
+            connection = USBBlaster.USBBlaster(IRbefore=4, IRafter=0, DRbefore=1, DRafter=0)
+            driverPorts = connection.scanChain()
+            del connection
+
+            ports = []
+
+            for index, port in enumerate(driverPorts):
+                obj = {
+                    "tap": index,
+                    "ID": port,
+                }
+                ports.append(obj)
+
+            return json.dumps({
+                "data": ports
+            })
+
+        except Exception as error:
+            return handle_exception(error)
+
+    def connect(self):
+        try:
+            self.connection = USBBlaster.USBBlaster(IRbefore=4, IRafter=0, DRbefore=1, DRafter=0)
+            return json.dumps({
+                "data": {
+                    "device": True
+                }
+            })
+        except Exception as error:
+            return handle_exception(error)
+            
+    def write(self, string):
+        try:
+            encodedString = string.encode('ascii')
+            self.connection.write(encodedString)
+            return json.dumps({
+                "data": {
+                    "string": string,
+                    "encodedString": str(encodedString)
+                }
+            })
+        except Exception as error:
+            return handle_exception(error)
+
+    def read(self):
+        try:
+            original_response = self.connection.readline()
+            response = original_response.decode('ascii').strip()
+            garbage = []
+            while self.connection.in_waiting:
+                garbage.append(str(self.connection.readline()))
+
+            return json.dumps({
+                "data": {
+                    "response": response,
+                    "originalResponse": str(original_response),
+                    "garbage": garbage
+                }
+            })
+        except Exception as error:
+            return handle_exception(error)
+
+    def close(self):
+        try:
+            self.connection = False
+            return json.dumps({
+                "data": {}
+            })
+        except Exception as error:
+            return handle_exception(error)
+
+worker = USBBlasterWorker()
+
+print(USBBlasterWorker.ports())
+with open("usb_blaster.json", "w") as file:
+    file.write(worker.connect())
+    file.write(worker.write("*IDN?\r\n")+',\n')
+    file.write(worker.read()+',\n')
+    file.write(worker.write("MEAS?\r\n")+',\n')
+    file.write(worker.read()+',\n')
+    file.write(worker.write("VAL?\r\n")+',\n')
+    file.write(worker.read()+',\n')
+    file.write(worker.close()+',\n')

+ 95 - 0
backend/src/python_workers/usbtmc_worker.py

@@ -0,0 +1,95 @@
+import sys
+import json
+import usbtmc
+from utils import handle_exception
+ 
+
+class USBTMCWorker:
+    OPTIONS = []
+
+    connection = None
+
+    @staticmethod
+    def ports():
+            driverPorts = usbtmc.list_devices()
+
+            ports = []
+
+            for port in driverPorts:
+                obj = {
+                    "device": ':'.join([str(port.idVendor), str(port.idProduct)]),
+                    "name": port.product,
+                    "address": port.address,
+                    "bus": port.bus,
+                    "manufacturer": port.manufacturer,
+                    "product": port.product,
+                    "serial_number": port.serial_number,
+                    "idProduct": port.idProduct,
+                    "idVendor": port.idVendor
+                }
+                ports.append(obj)
+
+            return ports
+
+    def timeout(self, value):
+            self.connection.timeout = value
+            return {
+                "timeout": value
+            }
+
+    def connect(self, device):
+            [idVendor, idProduct] = device.split(':')
+            self.connection = usbtmc.Instrument(int(idVendor), int(idProduct))
+            return {
+                    "device": device
+                }
+            
+    def write(self, string):
+            self.connection.write(string)
+            return {
+                    "string": string
+                }
+
+    def read(self):
+            response = self.connection.read(string)
+            return {
+                    "response": response
+                }
+
+    def ask(self, string):
+      response = self.connection.ask(string)
+      return {
+        "response": response
+      }
+
+    def close(self):
+            self.connection.close()
+            return {}
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        if self.connection is not None:
+            self.connection.close()
+            del self.connection
+            self.connection = None
+
+worker = USBTMCWorker()
+
+for line in sys.stdin:
+    try:
+        command = json.loads(line)
+        if command['type'] == 'ports':
+            res = USBTMCWorker.ports()
+        elif command['type'] == 'options':
+            res = USBTMCWorker.OPTIONS
+        elif command['type'] == 'connect':
+            options = command['options'] if 'options' in command else {}
+            res = worker.connect(command['device'], **options)
+        elif command['type'] == 'read':
+            res = worker.read()
+        elif command['type'] == 'write':
+            res = worker.write(command['string'])
+        elif command['type'] == 'ask':
+            res = worker.ask(command['string'])
+        print(json.dumps({ "data": res }), flush=True)
+    except Exception as error:
+        print(handle_exception(error), flush=True)

+ 15 - 0
backend/src/python_workers/utils.py

@@ -0,0 +1,15 @@
+import json
+import sys
+import os
+
+def handle_exception(error):
+    exc_type, exc_obj, exc_tb = sys.exc_info()
+    fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
+    return json.dumps({
+        "error": {
+          "string": str(error),
+          "type": error.__class__.__name__,
+          "fileName": fname,
+          "lineNumber": exc_tb.tb_lineno
+        }
+    })

+ 2 - 7
backend/src/resolvers.js

@@ -1,14 +1,9 @@
-const { merge } = require('lodash')
-
 const { forwardTo } = require('prisma-binding')
 const bcrypt = require('bcryptjs')
 const jwt = require('jsonwebtoken')
-// const { randomBytes } = require('crypto')
-// const { promisify } = require('util')
 
 const Query = {
   projects: forwardTo('db'),
-  connectionCommand: (parent, args, context, info) => 'Hello!',
   me: (parent, args, context, info) => {
     if (!context.request.userId) throw new Error('Not logged in.')
     return context.db.query.user({ where: { id: context.request.userId } }, info)
@@ -57,9 +52,9 @@ const Mutation = {
   }
 }
 
-const resolvers = merge({
+const resolvers = {
   Query,
   Mutation
-})
+}
 
 module.exports = { resolvers }

+ 80 - 0
backend/src/system.js

@@ -0,0 +1,80 @@
+const os = require('os')
+
+const typeDefs = `
+  type CPUTime {
+    user: Int!
+    nice: Int!
+    sys: Int!
+    idle: Int!
+    irq: Int!
+  }
+
+  type CPU {
+    model: String!
+    speed: Int!
+    times: CPUTime!
+  }
+
+  type NetworkAddress {
+    address: String!
+    netmask: String!
+    family: String!
+    mac: String!
+    internal: Boolean!
+    cidr: String!
+    scopied: Int
+  }
+
+  type NetworkInterface {
+    name: String!
+    addresses: [ NetworkAddress ]!
+  }
+
+  extend type Query {
+    apiVersion: String!
+    hostname: String!
+    type: String!
+    platform: String!
+    arch: String!
+    release: String!
+    uptime: String!
+    loadavg: [ Float! ]!
+    totalmem: String!
+    freemem: String!
+    cpus: [ CPU! ]!
+    networkInterfaces: [ NetworkInterface ]!
+  }
+  
+  extend type Mutation {
+    hello: String!
+  }
+  `
+
+const resolvers = {
+  Query: {
+    apiVersion: (_) => '0.1',
+    hostname: os.hostname,
+    type: os.type,
+    platform: os.platform,
+    arch: os.arch,
+    release: os.release,
+    uptime: os.uptime,
+    loadavg: os.loadavg,
+    totalmem: os.totalmem,
+    freemem: os.freemem,
+    cpus: os.cpus,
+    networkInterfaces: (_) => {
+      const interfaces = os.networkInterfaces()
+      const ifaceArray = []
+      for (let key in interfaces) {
+        ifaceArray.push({
+          name: key,
+          addresses: interfaces[key]
+        })
+      }
+      return ifaceArray
+    }
+  }
+}
+
+module.exports = { typeDefs, resolvers }

+ 19 - 31
frontend/components/Connection.js

@@ -1,6 +1,7 @@
 import styled from 'styled-components'
 import gql from 'graphql-tag'
-import { ApolloConsumer } from 'react-apollo'
+import { Mutation } from 'react-apollo'
+import {INTERFACE_LIST} from './InterfaceList'
 
 const StyledConnection = styled.div`
   fieldset {
@@ -18,27 +19,15 @@ const StyledConnection = styled.div`
   }
 `
 
-const CONNECTION_SEND_QUERY = gql`
-  query CONNECTION_SEND_QUERY($connectionId: String!, $command: String!) {
-    connectionCommand(connectionId: $connectionId, command: $command)
+const CONNECTION_COMMAND = gql`
+  mutation CONNECTION_COMMAND($connectionId: ID!, $type: String!, $string: String!, $options: String) {
+    connectionCommand(connectionId: $connectionId, type: $type, string: $string, options: $options)
   }
 `
 
 class Connection extends React.Component {
   state = {
-    connectionId: '',
-    command: '',
-    result: ''
-  }
-
-  sendCommand = async (event, client) => {
-    event.preventDefault()
-    const { data } = await client.query({
-      query: CONNECTION_SEND_QUERY,
-      variables: this.state
-    })
-    const { connectionCommand } = data
-    this.setState({ result: connectionCommand })
+    command: ''
   }
 
   changeInput = event => {
@@ -46,29 +35,28 @@ class Connection extends React.Component {
   }
 
   render() {
-    const { connectionId } = this.props
-
+    const { id, device, interfaceName } = this.props.data
     return (
-      <ApolloConsumer>
-        {(client) => (
+      <Mutation 
+      mutation={CONNECTION_COMMAND} 
+      variables={{
+        connectionId: this.props.data.id, 
+        type: 'ask',
+        string: this.state.command
+      }}
+      refetchQueries={[{query: INTERFACE_LIST}]}>
+        {(connectionCommand, {data, error, loading}) => (
           <StyledConnection>
             <h1>Connection</h1>
             <fieldset>
-
-              {!connectionId && (
-                <>
-                  <label htmlFor='connectionId'>Connection ID</label>
-                  <input type='text' value={this.state.connectionId} onChange={this.changeInput} id='connectionId' placeholder='Connection ID' />
-                </>
-              )}
               <label htmlFor='command'>Command</label>
               <input type='text' value={this.state.command} onChange={this.changeInput} id='command' placeholder='Command' />
             </fieldset>
-            <button type='submit' onClick={event => this.sendCommand(event, client)}>Send</button>
-            <textarea id='response' value={this.state.result} readOnly={true} />
+            <button type='submit' onClick={connectionCommand} disabled={loading}>Send</button>
+            <textarea id='response' value={data && data.connectionCommand} readOnly={true} />
           </StyledConnection>
         )}
-      </ApolloConsumer>
+      </Mutation>
     )
   }
 }

+ 17 - 3
frontend/components/Interface.js

@@ -1,5 +1,19 @@
-const Interface = props => (
-  <div>Interface</div>
-)
+import Gallery from './Gallery'
+import Port from './Port'
+import Connection from './Connection'
+import InterfaceOption from './InterfaceOption'
+
+const Interface = props => {
+  const { workerScript, interfaceName, options, ports, connections } = props.data
+  return (
+    <div>
+      <h2>{interfaceName}</h2>
+      <p>Script:</p><p>{workerScript}</p>
+      <Gallery title='ports' items={ports.map(port => <Port key={port.device} data={port} />)} />
+      <Gallery title='connections' items={connections.map(connection => <Connection key={connection.id} data={connection} />)} />
+      <Gallery title='options' items={options.map(option => <InterfaceOption key={option.name} data={option} />)} />
+    </div>
+  )
+}
 
 export default Interface

+ 71 - 0
frontend/components/InterfaceList.js

@@ -0,0 +1,71 @@
+import gql from 'graphql-tag'
+import { Query } from 'react-apollo'
+import Interface from './Interface'
+
+const INTERFACE_LIST = gql`
+  query INTERFACE_LIST {
+    interfaces {
+      interfaceName
+      workerScript
+      ports {
+        id
+        device
+        interfaceName
+        name
+        description
+      }
+      connections {
+        id
+        device
+        interfaceName
+      }
+      options {
+        name
+        type
+        description
+        values
+      }
+    }
+  }
+`
+
+class InterfaceList extends React.Component {
+  state = {
+    connectionId: '',
+    command: '',
+    result: ''
+  }
+
+  sendCommand = async (event, client) => {
+    event.preventDefault()
+    const { data, error } = await client.query({
+      query: CONNECTION_SEND_QUERY,
+      variables: this.state
+    })
+    console.log(data, error)
+    const { result } = data
+    this.setState({ result })
+  }
+
+  changeInput = event => {
+    this.setState({ [event.target.id]: event.target.value })
+  }
+
+  render() {
+    return (
+      <Query query={INTERFACE_LIST}>
+        {({data}, loading, error) => {
+          const {interfaces} = data
+          return (
+            <div>
+            <h1>Interface List</h1>
+            {interfaces.map(iface => <Interface key={iface.interfaceName} data={iface} />)}
+            </div>
+        )}}
+      </Query>
+    )
+  }
+}
+
+export default InterfaceList
+export {INTERFACE_LIST}

+ 5 - 0
frontend/components/InterfaceOption.js

@@ -0,0 +1,5 @@
+const InterfaceOption = props => (
+  <p>This is an option</p>
+)
+
+export default InterfaceOption

+ 37 - 0
frontend/components/Port.js

@@ -0,0 +1,37 @@
+import { Mutation } from 'react-apollo'
+import gql from 'graphql-tag'
+import { INTERFACE_LIST } from './InterfaceList'
+
+const CONNECT_PORT = gql`
+  mutation CONNECT_PORT($interfaceName: String!, $device: String!) {
+    connect(interfaceName: $interfaceName, device: $device) {
+      id
+      device
+      interfaceName
+    }
+  }
+`
+
+class Port extends React.Component {
+  render () {
+    const { interfaceName, device, name, description } = this.props.data
+    return (
+      <Mutation
+        mutation={CONNECT_PORT}
+        variables={{ interfaceName, device }}
+        refetchQueries={[{ query: INTERFACE_LIST }]}
+      >
+        {connect => (
+          <div>
+            <h1>{device}</h1>
+            <p>Name:</p><p>{name}</p>
+            <p>{description}</p>
+            <button onClick={connect}>Connect</button>
+          </div>
+        )}
+      </Mutation>
+    )
+  }
+}
+
+export default Port

+ 2 - 1
frontend/pages/interfaces.js

@@ -1,7 +1,8 @@
 import Connection from '../components/Connection'
+import InterfaceList from '../components/InterfaceList'
 
 const InterfacePage = props => (
-  <Connection />
+  <InterfaceList />
 )
 
 export default InterfacePage