Browse Source

worked on instrument interface.

Tomi Cvetic 5 years ago
parent
commit
10e93296e2

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

@@ -0,0 +1,3 @@
+{
+  "python.pythonPath": "./src/python_workers/venv/bin/python"
+}

+ 13 - 2
backend/pdfScan.js

@@ -1,20 +1,31 @@
 const pdfExtract = require('pdf-extract')
-const path = '/home/tomi/Downloads/2230G-900-01A_Jun_2018_User.pdf'
+const fs = require('fs')
+// const path = '/home/tomi/Downloads/2230G-900-01A_Jun_2018_User.pdf'
+const path = '/home/tomi/Downloads/MDO4000-B-MSO-DPO4000B-and-MDO3000-Oscilloscope-Programmer-Manual-Rev-A.pdf'
 const options = {
   type: 'text' // or 'ocr'
 }
 
 function getSCPICommands (pages) {
   const scpiCommon = /(\*\w+\??)/g
+  // const header = ``
   const scpi = /((?:\*\w+|(?:\[?\w+\]?)(?=:\w+)\]?)(?:\[?:\w+\]?)*\??)(?:\s+(<?\w+>?)(?:[,|]\s*(<?\w+>?))*)?/g
 
+  const scpiLines = []
   pages.map((page, pageIndex) => {
     const lines = page.split('\n')
     lines.map((line, lineIndex) => {
       const matches = line.match(scpi)
-      if (matches) console.log(pageIndex, lineIndex, matches)
+      if (matches) {
+        console.log(pageIndex, lineIndex, matches)
+        return matches
+      }
     })
   })
+  fs.writeFile('/home/tomi/Downloads/MDO4000.txt', pages.join('\n'), error => {
+    if (error) console.log(error)
+    console.log('file was saved.')
+  })
 }
 
 const processor = pdfExtract(path, options, error => {

+ 1 - 0
backend/schema.graphql

@@ -4,6 +4,7 @@ scalar Upload
 type Query {
   projects(where: ProjectWhereInput, orderBy: ProjectOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Project]!
   projectVersions(where: ProjectVersionWhereInput, orderBy: ProjectVersionOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [ProjectVersion]!
+  instruments(where: InstrumentWhereInput, orderBy: InstrumentOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Instrument]!
   uploads: [File]
   me: User!
 }

+ 131 - 90
backend/src/interfaces.js

@@ -5,15 +5,60 @@ const { promisify } = require('util')
 const PythonWorker = require('./pythonWorker')
 
 const readdir = promisify(fs.readdir)
+const stat = promisify(fs.stat)
+
+/**
+ * INTERFACE BACKEND
+ *
+ * Communication with workers (for now only Python)
+ * The workers are implemented as REPL clients that
+ * communicate with JSON.
+ *
+ * Names:
+ *  * Worker: a Python file that implements a REPL client (e.g. serial)
+ *  * Port:
+ *  * Connection:
+ */
+
+const WORKER_DIR = `${__dirname}/python_workers`
+const HOST = os.hostname()
 
 const state = {
   workers: [],
   interfaces: [],
   ports: [],
-  connections: []
+  connections: [],
+  lastScan: {
+    workers: Date.now() - 100000,
+    ports: Date.now() - 100000,
+    connections: Date.now() - 100000
+  }
 }
 
 const typeDefs = `
+  type Worker {
+    id: ID!
+    interfaceName: String!
+    workerScript: String!
+    mtime: DateTime!
+    updated: DateTime
+    workerProcess: WorkerProcess
+    ports: [Port]!
+    connections: [Connection]!
+    options: [Option!]
+  }
+
+  type WorkerProcess {
+    pid: Int
+    killed: Boolean!
+    signalCode: String
+    exitCode: Int
+    spawnfile: String!
+    spawnargs: [String]!
+    error: [String]!
+    data: [String]!
+  }
+
   type Option {
     name: String!
     type: String!
@@ -30,37 +75,17 @@ const typeDefs = `
     description: String
   }
 
-  type Worker {
-    pid: Int
-    killed: Boolean!
-    signalCode: String
-    exitCode: Int
-    spawnfile: String!
-    spawnargs: [String]!
-    error: Int!
-    data: Int!
-  }
-
   type Connection {
     id: ID!
     interfaceName: String!
     host: String!
     device: String!
-    workerInfo: Worker
-  }
-
-  type Interface {
-    interfaceName: String!
-    host: String!
-    workerScript: String!
-    workerInfo: Worker
-    ports: [Port]!
-    connections: [Connection]!
-    options: [Option]!
+    workerProcess: WorkerProcess
   }
 
   extend type Query {
-    interfaces(force: Boolean): [Interface]!
+    workers: [Worker]!
+    worker(id: ID, interfaceName: String): Worker!
     ports(interfaceName: String, force: Boolean): [Port]!
     connections: [Connection]!
     connection(id: ID!): Connection!
@@ -75,42 +100,43 @@ const typeDefs = `
   }
 `
 
-async function findWorkers() {
-  // Find all files in ./python_workers that end in _worker.py
-  const fileNames = await readdir(`${__dirname}/python_workers`)
+/**
+ * WORKER SECTION
+ */
+async function findWorkers () {
+  // 1. Don't check more frequently than once per second.
+  // if (state.lastScan.workers + 1000 > Date.now()) return null
+  // state.lastScan.workers = Date.now()
+
+  // 2. Find all files in ./python_workers that end in _worker.py
+  const fileNames = await readdir(WORKER_DIR)
   const workerFiles = fileNames.filter(fileName => fileName.includes('_worker.py'))
 
-  // Find the added workers
-  workerFiles.forEach(workerFile => {
+  // 3. For every worker script
+  const workerPromises = workerFiles.map(async 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
-  const workerPromises = state.workers.map(async worker => {
-    const { workerScript, interfaceName } = worker
-    // Skip existing interfaces
-    if (state.interfaces.find(iface => iface.interfaceName === interfaceName)) return null
-    const pythonWorker = new PythonWorker(workerScript)
-    const { res, error } = await pythonWorker.spawn()
-    if (error) {
+    const workerScript = `${WORKER_DIR}/${workerFile}`
+    // a. Find out if it was modified.
+    const { mtime } = await stat(workerScript)
+    const foundWorker = state.workers.find(worker => worker.interfaceName === interfaceName)
+    if (foundWorker) {
+      // b. If it was modified, save the modification time.
+      if (foundWorker.mtime < mtime) foundWorker.updated = mtime
       return
     }
-    state.interfaces.push({
+    // c. Spawn a new worker connection.
+    const workerProcess = new PythonWorker(workerScript)
+    const { data, error, pythonError } = await workerProcess.spawn()
+    if (error) throw new Error(error)
+    if (pythonError) throw new Error(pythonError)
+    // c. Save the worker in the state.
+    state.workers.push({
+      id: md5(`${interfaceName}${workerScript}${mtime}`),
       interfaceName,
       workerScript,
-      worker: pythonWorker,
+      mtime,
+      updated: null,
+      workerProcess,
       ports: [],
       connections: [],
       options: []
@@ -119,33 +145,45 @@ async function findInterfaces() {
   await Promise.all(workerPromises)
 }
 
-async function interfaces(parent, args, ctx, info) {
-  const { force } = args
-  // Try to identify interfaces if necessary
-  //if (!state.interfaces.length || force) 
-  await findInterfaces()
+async function workers (parent, args, context, info) {
+  await findWorkers()
+  return state.workers.map(worker => ({
+    ...worker,
+    workerProcess: workerProcess(worker.workerProcess, args, context, info)
+  }))
+}
 
-  return state.interfaces.map(iface => {
-    const { workerScript, interfaceName, worker } = iface
-    return {
-      interfaceName,
-      host: os.hostname(),
-      workerScript,
-      workerInfo: workerInfo(worker, args, ctx, info),
-      ports: ports(interfaceName, args, ctx, info),
-      connections: connections(interfaceName, args, ctx, info),
-      options: options(interfaceName, args, ctx, info)
-    }
-  })
+async function worker (parent, args, context, info) {
+  await findWorkers()
+  const { id, interfaceName } = args
+  if (!id && !interfaceName) throw new Error('Either id or interfaceName needs to be provided!')
+  const worker = state.workers.find(worker => worker.id === id || worker.interfaceName === interfaceName)
+  if (!worker) throw new Error(`Worker id=${id}, interfaceName=${interfaceName} not found`)
+  return worker
 }
 
-async function findPorts(interfaceName) {
+/**
+ * PORTS SECTION
+ */
+async function findPorts (interfaceName) {
+  console.log('find ports.')
+  // 1. Make sure, workers are updated.
+  await findWorkers()
+
+  // 2. Don't check more frequently than once per second.
+  // if (state.lastScan.ports + 1000 > Date.now()) return null
+  // state.lastScan.ports = Date.now()
+
+  const portsPromises = state.workers.map(worker => {
+
+  })
   // Generate all ports for the interface
   const iface = state.interfaces.find(iface => iface.interfaceName === interfaceName)
 
   const { data, error, pythonError } = await iface.worker.send({ type: 'ports' })
   if (error) throw new Error(error)
   if (pythonError) throw new Error(pythonError)
+  console.log(data)
   data.forEach(port => {
     const id = port.name || port.device
     // Skip existing ports
@@ -159,9 +197,10 @@ async function findPorts(interfaceName) {
     state.ports.push(newPort)
     iface.ports.push(newPort)
   })
+  console.log('found ports.')
 }
 
-async function ports(parent, args, ctx, info) {
+async function ports (parent, args, ctx, info) {
   const { force, interfaceName } = args
   const ifName = interfaceName || parent
 
@@ -176,7 +215,7 @@ async function ports(parent, args, ctx, info) {
   }
 }
 
-async function findOptions(interfaceName) {
+async function findOptions (interfaceName) {
   const iface = state.interfaces.find(iface => iface.interfaceName === interfaceName)
   const { data, error, pythonError } = await iface.worker.send({ type: 'options' })
   if (error) throw new Error(error)
@@ -184,7 +223,7 @@ async function findOptions(interfaceName) {
   iface.options.push(...data)
 }
 
-async function options(parent, args, ctx, info) {
+async function options (parent, args, ctx, info) {
   const iface = state.interfaces.find(iface => iface.interfaceName === parent)
 
   // Try to find options if necessary
@@ -192,8 +231,9 @@ async function options(parent, args, ctx, info) {
   return iface.options
 }
 
-function workerInfo(parent, args, ctx, info) {
+function workerProcess (parent, args, ctx, info) {
   const { killed, exitCode, signalCode, spawnargs, spawnfile, pid } = parent.pythonShell
+  const { error, data } = parent
   return {
     pid,
     killed,
@@ -201,12 +241,12 @@ function workerInfo(parent, args, ctx, info) {
     signalCode,
     spawnfile,
     spawnargs,
-    error: parent.error.length,
-    data: parent.data.length
+    error,
+    data
   }
 }
 
-async function connections(parent, args, ctx, info) {
+async function connections (parent, args, ctx, info) {
   if (parent) {
     const iface = state.interfaces.find(iface => iface.interfaceName === parent)
     return iface.connections
@@ -215,12 +255,12 @@ async function connections(parent, args, ctx, info) {
   }
 }
 
-async function connection(parent, args, context, info) {
+async function connection (parent, args, context, info) {
   const connection = state.connections.find(connection => connection.id === args.id)
   return connection
 }
 
-async function connect(parent, args, ctx, info) {
+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)
@@ -234,7 +274,7 @@ async function connect(parent, args, ctx, info) {
     interfaceName,
     host: os.hostname(),
     worker: pythonWorker,
-    workerInfo: (parent, args, context, info) => workerInfo(pythonWorker, args, context, info)
+    workerProcess: (parent, args, context, info) => workerProcess(pythonWorker, args, context, info)
   }
   const connectionData = await connection.worker.send({ type: 'connect', device })
   if (connectionData.error) throw new Error(connectionData.error)
@@ -244,7 +284,7 @@ async function connect(parent, args, ctx, info) {
   return connection
 }
 
-async function connectionCommand(parent, args, ctx, info) {
+async function connectionCommand (parent, args, ctx, info) {
   const { connectionId, type, string, options } = args
   const connection = state.connections.find(connection => connection.id === connectionId)
   const { data, error, pythonError } = await connection.worker.send({ type, string, options })
@@ -253,17 +293,17 @@ async function connectionCommand(parent, args, ctx, info) {
   return data.response
 }
 
-//TODO Also find connections in interfaces.
-async function endWorker(parent, args, ctx, info) {
+// TODO Also find connections in interfaces.
+async function endWorker (parent, args, ctx, info) {
   const { id } = args
   const connection = state.connections.find(connection => connection.id === id)
   const { data, error, pythonError } = await connection.worker.end()
   if (error) throw new Error(JSON.stringify(error))
   if (pythonError.error !== 0) throw new Error(JSON.stringify(pythonError))
-  return connection.workerInfo()
+  return connection.workerProcess()
 }
 
-async function spawnWorker(parent, args, ctx, info) {
+async function spawnWorker (parent, args, ctx, info) {
   const { id } = args
   const connection = state.connections.find(connection => connection.id === id)
   const { data, error, pythonError } = await connection.worker.spawn()
@@ -273,22 +313,23 @@ async function spawnWorker(parent, args, ctx, info) {
   const connectionData = await connection.worker.send({ type: 'connect', device: connection.device })
   if (connectionData.error) throw new Error(connectionData.error)
   if (connectionData.pythonError) throw new Error(connectionData.pythonError)
-  return connection.workerInfo()
+  return connection.workerProcess()
 }
 
-async function killWorker(parent, args, ctx, info) {
+async function killWorker (parent, args, ctx, info) {
   const { id } = args
   const connection = state.connections.find(connection => connection.id === id)
   const { data, error, pythonError } = await connection.worker.kill()
   console.log(data, error, pythonError)
   if (error) throw new Error(JSON.stringify(error))
   if (pythonError) throw new Error(JSON.stringify(pythonError))
-  return connection.workerInfo()
+  return connection.workerProcess()
 }
 
 const resolvers = {
   Query: {
-    interfaces,
+    workers,
+    worker,
     ports,
     connections,
     connection

+ 65 - 59
backend/src/pythonWorker.js

@@ -1,85 +1,91 @@
 const { spawn } = require('child_process')
 const { Lock } = require('semaphore-async-await')
+const { serializeError } = require('./utils')
 
-
-function PythonWorker(workerScript, shellOptions) {
+function PythonWorker (workerScript, shellOptions) {
   // create a lock for incomming commands
   this.commandLock = new Lock()
   // create a lock for python communication
   this.pythonLock = new Lock()
-
   this.data = []
   this.error = []
   this.workerScript = workerScript
-
   // The python shell is started with this.spawn
   this.pythonShell = null
 
-  // Use send a command to the python worker.
-  // 
-  this.send = async (command) => {
-    if (this.pythonShell.killed) return { error: `process ${this.pythonShell.pid} already killed.` }
-    if (this.pythonShell.exitCode !== null) return { error: `process ${this.pythonShell.pid} already exited with code ${this.pythonShell.exitCode}` }
+  this.transaction = async (func, args) => {
+    console.log('transaction.')
+    // 1. Block new commands
     await this.commandLock.acquire()
+    // 2. Send data to Python shell (lock will be released by data/error event)
     await this.pythonLock.acquire()
-    console.log(command)
-    this.pythonShell.stdin.write(
-      // Write the command as a JSON object. 
-      //!Don't forget the new-line
-      JSON.stringify(command) + '\n'
-    )
+    console.log('function call')
+    func(args)
+    console.log('function ended', this.pythonLock)
+    // 3. Wait for data from the Python shell
     await this.pythonLock.acquire()
     const pythonError = this.error.pop()
-    if (pythonError) {
-      this.pythonLock.release()
-      this.commandLock.release()
-      console.log({ pythonError })
-      return { pythonError }
-    }
-    const { data, error } = this.data.pop()
+    const workerResult = this.data.pop()
     this.pythonLock.release()
+    console.log('results', { pythonError, ...workerResult })
+    // 4. Unblock new commands
     this.commandLock.release()
-    console.log({ data, error })
-    return { data, error }
+    // 5. Return result
+    console.log('transaction done.', { ...workerResult, pythonError })
+    return { ...workerResult, pythonError }
+  }
+
+  this.isProcessRunning = () => (
+    this.pythonShell &&
+    !this.pythonShell.killed &&
+    (this.pythonShell.exitCode !== null)
+  )
+
+  // Use send a command to the python worker.
+  this.send = command => {
+    if (!this.isProcessRunning) return { error: 'Process not running' }
+
+    return this.transaction(command => {
+      this.pythonShell.stdin.write(
+        // Write the command as a JSON object, end with a newline!
+        JSON.stringify(command) + '\n'
+      )
+    }, command)
   }
 
   this.spawn = async () => {
-    if (this.pythonShell) {
-      if (!this.pythonShell.killed && (this.pythonShell.exitCode === null)) {
-        return { error: `process still running with pid = ${this.pythonShell.pid}` }
-      }
-    }
-    await this.commandLock.acquire()
-    await this.pythonLock.acquire()
-    this.pythonShell = spawn(
-      `${process.env.PWD}/${process.env.PYTHON_PATH}`,
-      [this.workerScript]
-    )
-    this.pythonShell.stdout.on('data', message => {
-      this.data.push(JSON.parse(message))
-      this.pythonLock.release()
-    })
-    this.pythonShell.stderr.on('data', error => {
-      const jsonError = JSON.parse(error)
-      this.error.push({ error: jsonError })
-      this.pythonLock.release()
-    })
-    this.pythonShell.on('close', error => {
-      const jsonError = JSON.parse(error)
-      this.error.push({ error: jsonError })
-      this.pythonLock.release()
+    console.log('spawning?')
+    if (this.isProcessRunning()) return { error: 'Process already running' }
+    console.log('spawning!')
+
+    return this.transaction(() => {
+      this.pythonShell = spawn(
+        `${process.env.PWD}/${process.env.PYTHON_PATH}`,
+        [this.workerScript]
+      )
+      this.pythonShell.stdout.on('data', message => {
+        // The python worker returns JSON {data, error}
+        console.log('[DATA]', JSON.parse(message))
+        this.data.push(JSON.parse(message))
+        this.pythonLock.release()
+      })
+      this.pythonShell.stderr.on('data', error => {
+        console.log('[APPERROR]', error)
+        this.error.push(serializeError(new Error(error)))
+        this.pythonLock.release()
+      })
+      this.pythonShell.on('close', error => {
+        console.log('[CLOSE]', error)
+        this.error.push(serializeError(new Error(error)))
+        this.pythonLock.release()
+      })
+      this.pythonShell.on('error', error => {
+        console.log('[ERROR]', error)
+        this.error.push(serializeError(error))
+        this.pythonLock.release()
+      })
+      console.log('spawned.')
     })
-    await this.pythonLock.acquire()
-    const pythonError = this.error.pop()
-    if (pythonError) {
-      this.pythonLock.release()
-      this.commandLock.release()
-      return { pythonError }
-    }
-    const { data, error } = this.data.pop()
-    this.pythonLock.release()
-    this.commandLock.release()
-    return { data, error }
   }
 
   this.end = async () => {

+ 4 - 2
backend/src/python_workers/test_worker.py

@@ -3,8 +3,8 @@ import time
 import random
 import json
 from utils import handle_exception
-
-
+ 
+ 
 class TestClass:
     def __init__(self, device):
         self.timeout = 2
@@ -112,6 +112,8 @@ for line in sys.stdin:
             data = TestWorker.ports()
         elif command['type'] == 'options':
             data = TestWorker.OPTIONS
+        elif command['type'] == 'comands':
+            data = [func for func in dir(TestWorker) if callable(getattr(TestWorker, func))]
         elif command['type'] == 'connect':
             options = command['options'] if 'options' in command else {}
             data = worker.connect(command['device'], **options)

+ 1 - 0
backend/src/resolvers.js

@@ -3,6 +3,7 @@ const bcrypt = require('bcryptjs')
 const jwt = require('jsonwebtoken')
 
 const Query = {
+  instruments: forwardTo('db'),
   projects: forwardTo('db'),
   projectVersions: forwardTo('db'),
   me: (parent, args, context, info) => {

+ 12 - 0
backend/src/utils.js

@@ -0,0 +1,12 @@
+function serializeError (error) {
+  return JSON.stringify({
+    name: error.name,
+    message: error.message,
+    stack: error.stack,
+    filename: error.filename,
+    lineNumber: error.lineNumber,
+    columnNumber: error.columnNumber
+  })
+}
+
+module.exports = { serializeError }

+ 36 - 46
frontend/components/Instrument.js

@@ -3,59 +3,49 @@ import { Query, Mutation } from 'react-apollo'
 import InstrumentSubsystem from './InstrumentSubsystem'
 import Gallery from './Gallery'
 
-const instrumentSubsystems = [{
-  name: 'Source',
-  description: 'The commands in the SOURce subsystem are used to control the output of the power supply.',
-  commands: [{
-    id: '1',
-    tag: 'Apply',
-    name: 'Apply voltage and current',
-    description: 'This command sets voltage and current levels on a specified channel with a single command message.',
-    instrument: null,
-    readString: null,
-    writeString: '[SOURce:]APPLy {CH1|CH2|CH3}, <NRf+>, <NRf+>',
-    parameters: ['channel']
-  },
-  {
-    id: '1',
-    tag: 'Output',
-    name: 'Channel output state',
-    description: 'This command sets the output state of the presently selected channel. The query form of this command returns the output state of the presently selected channel.',
-    instrument: null,
-    readString: '[SOURce:]CHANnel:OUTPut[:STATe]?',
-    writeString: '[SOURce:]CHANnel:OUTPut[:STATe] <BOOL>',
-    parameters: ['channel']
-  }],
-  parameters: [{ channel: ['CH1', 'CH2', 'CH3'] }],
-  subsystems: []
-}]
-
-class Instrument extends React.Component {
-  state = {
-    id: this.props.instrument ? this.props.instrument.id : null,
+const InstrumentForm = props => {
+  const [state, setState] = React.useState({
+    id: null,
     name: '',
     description: '',
     documents: [],
     interfaces: [],
-    subsystems: []
-  }
+    subsystems: [],
+    ...props.instrument
+  })
 
-  toState = event => {
-    this.setState({ [event.target.name]: event.target.value })
+  const toState = event => {
+    setState({ [event.target.name]: event.target.value })
   }
 
-  render() {
-    return (
-      <div>
-        <h1>{this.state.name || 'Keithley 2230-3'}</h1>
-        <p>{this.state.description || 'A really nice 3 output multimeter'}</p>
-        <Gallery title='Documents' items={['Hallo']} />
-        <Gallery title='Interfaces' items={['serial', 'usbtmc'].map(item => <div>{item}</div>)} />
-        <Gallery title='Subsystems' items={instrumentSubsystems.map(instrumentSubsystem =>
-          <InstrumentSubsystem instrumentSubsystem={instrumentSubsystem} />)} />
-      </div>
-    )
-  }
+  return (
+    <form>
+      <fieldset>
+        <label htmlFor='name'>Name</label>
+        <input type='text' name='name' id='name' placeholder='Name' value={state.name} onChange={onChange} />
+        <label htmlFor='description'>Name</label>
+        <textarea name='description' id='description' placeholder='Description' value={state.description} onChange={onChange} />
+      </fieldset>
+
+    </form>
+  )
+}
+
+const Instrument = props => {
+  const { instrument } = props
+  return instrument ? (
+    <div>
+      <h1>{instrument.name}</h1>
+      <p>{instrument.description}</p>
+      <Gallery title='Documents' items={['Hallo']} />
+      <Gallery title='Interfaces' items={['serial', 'usbtmc'].map(item => <div>{item}</div>)} />
+      <Gallery title='Subsystems' items={instrument.subsystems.map(subsystem =>
+        <InstrumentSubsystem subsystem={subsystem} />)} />
+    </div>
+  ) : (
+    <p>Instrument not found.</p>
+  )
 }
 
 export default Instrument
+export { InstrumentForm }

+ 27 - 67
frontend/components/InstrumentCommand.js

@@ -1,16 +1,22 @@
 import gql from 'graphql-tag'
 import { Query, Mutation } from 'react-apollo'
 
-const CREATE_INSTRUMENT_SUBSYSTEM = gql`
-  mutation CREATE_INSTRUMENT_SUBSYSTEM($name: String!, $description: String!, $interfaces: [String]!) {
-    createInstrument(name: $name, description: $description, interfaces: $interfaces) {
-      id
-    }
-  }
-`
+const InstrumentCommandFormFields = props => {
+  const { state, onChange } = props
+
+  return (
+    <fieldset>
+      <label htmlFor='name'>Name</label>
+      <input type='text' name='name' id='name' placeholder='Name' value={state.name} onChange={onChange} />
+      <label htmlFor='description'>Name</label>
+      <textarea name='description' id='description' placeholder='Description' value={state.description} onChange={onChange} />
+    </fieldset>
+  )
+}
 
-class InstrumentCommand extends React.Component {
-  state = {
+const InstrumentCommand = props => {
+  const { command } = props
+  const state = {
     id: null,
     tag: '',
     name: '',
@@ -18,66 +24,20 @@ class InstrumentCommand extends React.Component {
     instrument: null,
     readString: '',
     writeString: '',
-    parameters: [],
-    ...this.props.instrumentCommand
-  }
-
-  toState = event => {
-    this.setState({ [event.target.name]: event.target.value })
-  }
-
-  addComment = event => {
-    event.preventDefault()
-    const newState = { ...this.state }
-    newState.changes.push(this.state.change)
-    newState.change = ''
-    this.setState(newState)
+    parameters: []
   }
 
-  render() {
-    return (
-      <Mutation mutation={CREATE_PROJECT_VERSION} variables={this.state}>
-        {(createProjectVersion, { data, error, loading }) => (
-          <form onSubmit={async event => {
-            event.preventDefault()
-            const { data } = await createProjectVersion()
-            this.state.id = data.createProjectVersion.id
-          }}>
-            {!this.props.title && <h1>Project Version</h1>}
-            <fieldset id='project-generic'>
-              <label htmlFor='name'>Project name</label>
-              <input type='text' name='name' id='name' placeholder='Project version name' value={this.state.name} onChange={this.toState} />
-              <label htmlFor='date'>Date</label>
-              <input type='date' name='date' id='date' placeholder='Project date' value={this.state.date} onChange={this.toState} />
-              <label htmlFor='change'>Comments</label>
-              <input type='text' name='change' id='change' placeholder='Project change' value={this.state.change} onChange={this.toState} /><button onClick={this.addComment}>Add</button>
-              {this.state.changes.map((change, index) => <p key={index}>{change}</p>)}
-              {this.props.project || (
-                <Query query={QUERY_PROJECTS}>
-                  {({ data, error, loading }) => {
-                    if (loading) return <p>Loading projects...</p>
-                    if (error) return <p>Error: {error.message}</p>
-                    if (!data || !data.projects.length) return <p>No projects found.</p>
-                    if (!this.state.project) this.setState({ project: data.projects[0].id })
-                    return (
-                      <label htmlFor="version">
-                        <select name="version" id="version">onChange={this.toState}>
-                        {data.projects.map(project =>
-                          <option key={project.id} value={project.id}>{project.name}</option>)
-                          }
-                        </select>
-                      </label>
-                    )
-                  }}
-                </Query>
-              )}
-            </fieldset>
-            <button type='submit'>Save</button>
-          </form>
-        )}
-      </Mutation>
-    )
-  }
+  return (
+    <div>
+      <h1>{command.name}</h1>
+      <p>{command.description}</p>
+      <p>Tag: {command.tag}</p>
+      <pre>{command.readString}</pre>
+      <pre>{command.writeString}</pre>
+      <div>Here go the parameters</div>
+    </div>
+  )
 }
 
 export default InstrumentCommand
+export { InstrumentCommandFormFields }

+ 44 - 0
frontend/components/InstrumentList.js

@@ -0,0 +1,44 @@
+import gql from 'graphql-tag'
+import { Query } from 'react-apollo'
+import Link from 'next/link'
+
+const INSTRUMENTS = gql`
+  query INSTRUMENTS {
+    instruments {
+      id
+      name
+      description
+    }
+  }
+`
+
+const InstrumentList = props => (
+  <Query query={INSTRUMENTS}>
+    {({ data, loading, error }) => {
+      const content = [
+        <h1>Instrument List</h1>
+      ]
+      if (loading) content.push(<p>Loading instruments...</p>)
+      if (error) content.push(<p>Error loading instruments: {error.message}</p>)
+
+      if (data.instruments.length > 0) {
+        content.push(
+          <ul>
+            {data.instruments.map(instrument => (
+              <li key={instrument.id} title={instrument.description}>
+                <Link href={{ pathname: 'instruments', query: { id: instrument.id } }}>
+                  <a>{instrument.name}</a>
+                </Link>
+              </li>
+            ))}
+          </ul>
+        )
+      } else {
+        content.push(<p>No instruments found.</p>)
+      }
+      return content
+    }}
+  </Query>
+)
+
+export default InstrumentList

+ 21 - 70
frontend/components/InstrumentParameter.js

@@ -1,79 +1,30 @@
 import gql from 'graphql-tag'
 import { Query, Mutation } from 'react-apollo'
 
-const CREATE_INSTRUMENT_PARAMETER = gql`
-  mutation CREATE_INSTRUMENT_PARAMETER($name: String!, $description: String!, $interfaces: [String]!) {
-    createInstrument(name: $name, description: $description, interfaces: $interfaces) {
-      id
-    }
-  }
-`
+const InstrumentParameterFormFields = props => {
+  const { state, onChange } = props
 
-class InstrumentParameter extends React.Component {
-  state = {
-    id: this.props.instrumentParameter ? this.props.instrumentParameter.id : null,
-    tag: '',
-    name: '',
-    description: '',
-    values: []
-  }
-
-  toState = event => {
-    this.setState({ [event.target.name]: event.target.value })
-  }
+  return (
+    <fieldset>
+      <label htmlFor='name'>Name</label>
+      <input type='text' name='name' id='name' placeholder='Name' value={state.name} onChange={onChange} />
+      <label htmlFor='description'>Name</label>
+      <textarea name='description' id='description' placeholder='Description' value={state.description} onChange={onChange} />
+    </fieldset>
+  )
+}
 
-  addComment = event => {
-    event.preventDefault()
-    const newState = { ...this.state }
-    newState.changes.push(this.state.change)
-    newState.change = ''
-    this.setState(newState)
-  }
+const InstrumentParameter = props => {
+  const { parameter } = props
+  console.log(parameter)
 
-  render() {
-    return (
-      <Mutation mutation={CREATE_PROJECT_VERSION} variables={this.state}>
-        {(createProjectVersion, { data, error, loading }) => (
-          <form onSubmit={async event => {
-            event.preventDefault()
-            const { data } = await createProjectVersion()
-            this.state.id = data.createProjectVersion.id
-          }}>
-            {!this.props.title && <h1>Project Version</h1>}
-            <fieldset id='project-generic'>
-              <label htmlFor='name'>Project name</label>
-              <input type='text' name='name' id='name' placeholder='Project version name' value={this.state.name} onChange={this.toState} />
-              <label htmlFor='date'>Date</label>
-              <input type='date' name='date' id='date' placeholder='Project date' value={this.state.date} onChange={this.toState} />
-              <label htmlFor='change'>Comments</label>
-              <input type='text' name='change' id='change' placeholder='Project change' value={this.state.change} onChange={this.toState} /><button onClick={this.addComment}>Add</button>
-              {this.state.changes.map((change, index) => <p key={index}>{change}</p>)}
-              {this.props.project || (
-                <Query query={QUERY_PROJECTS}>
-                  {({ data, error, loading }) => {
-                    if (loading) return <p>Loading projects...</p>
-                    if (error) return <p>Error: {error.message}</p>
-                    if (!data || !data.projects.length) return <p>No projects found.</p>
-                    if (!this.state.project) this.setState({ project: data.projects[0].id })
-                    return (
-                      <label htmlFor="version">
-                        <select name="version" id="version">onChange={this.toState}>
-                        {data.projects.map(project =>
-                          <option key={project.id} value={project.id}>{project.name}</option>)
-                          }
-                        </select>
-                      </label>
-                    )
-                  }}
-                </Query>
-              )}
-            </fieldset>
-            <button type='submit'>Save</button>
-          </form>
-        )}
-      </Mutation>
-    )
-  }
+  return (
+    <div>
+      <h1>{parameter.name}</h1>
+      <p>{parameter.description}</p>
+    </div>
+  )
 }
 
 export default InstrumentParameter
+export { InstrumentParameterFormFields }

+ 30 - 31
frontend/components/InstrumentSubsystem.js

@@ -1,41 +1,40 @@
 import gql from 'graphql-tag'
 import { Query, Mutation } from 'react-apollo'
 import Gallery from './Gallery'
+import InstrumentCommand from './InstrumentCommand'
+import InstrumentParameter from './InstrumentParameter'
 
-const CREATE_INSTRUMENT_SUBSYSTEM = gql`
-  mutation CREATE_INSTRUMENT_SUBSYSTEM($name: String!, $description: String!, $interfaces: [String]!) {
-    createInstrument(name: $name, description: $description, interfaces: $interfaces) {
-      id
-    }
-  }
-`
+const InstrumentSubsystemFormFields = props => {
+  const { state, onChange } = props
 
-class InstrumentSubsystem extends React.Component {
-  state = {
-    id: null,
-    name: '',
-    description: '',
-    commands: [],
-    parameters: [],
-    subsystems: [],
-    ...this.props.instrumentSubsystem
-  }
+  return (
+    <fieldset>
+      <label htmlFor='name'>Name</label>
+      <input type='text' name='name' id='name' placeholder='Name' value={state.name} onChange={onChange} />
+      <label htmlFor='description'>Name</label>
+      <textarea name='description' id='description' placeholder='Description' value={state.description} onChange={onChange} />
+    </fieldset>
+  )
+}
 
-  toState = event => {
-    this.setState({ [event.target.name]: event.target.value })
-  }
+const InstrumentSubsystem = props => {
+  const { subsystem } = props
 
-  render() {
-    return (
-      <div>
-        <h1>{this.state.name}</h1>
-        <p>{this.state.description}</p>
-        <Gallery title='Commands' items={[]} />
-        <Gallery title='Parameters' items={[]} />
-        <Gallery title='Subsystems' items={[]} />
-      </div>
-    )
-  }
+  return subsystem ? (
+    <div>
+      <h1>{subsystem.name}</h1>
+      <p>{subsystem.description}</p>
+      <Gallery title='Commands' items={subsystem.commands.map(command =>
+        <InstrumentCommand command={command} />)} />
+      <Gallery title='Parameters' items={subsystem.parameters.map(parameter =>
+        <InstrumentParameter parameter={parameter} />)} />
+      <Gallery title='Subsystems' items={subsystem.subsystems.map(childSubsystem =>
+        <InstrumentSubsystem subsystem={childSubsystem} />)} />
+    </div>
+  ) : (
+    <p>No data found.</p>
+  )
 }
 
 export default InstrumentSubsystem
+export { InstrumentSubsystemFormFields }

+ 5 - 1
frontend/components/Page.js

@@ -85,10 +85,14 @@ const GlobalStyle = createGlobalStyle`
     padding: 6px;
     margin: 0 8px;
   }
+
+  pre {
+    font-family: 'roboto_mono';
+  }
 `
 
 class Page extends React.Component {
-  render() {
+  render () {
     return (
       <ThemeProvider theme={theme}>
         <StyledPage>

+ 59 - 0
frontend/lib/parseSCPI.js

@@ -0,0 +1,59 @@
+// BNF notation:
+// <> Defined element
+// = is defined as
+// | Exclusive OR
+// {} Group, one element is required
+// [] Optional, can be omitted
+// ... Previous element(s) may be repeated
+
+// Message elements
+// [:]<Header>[<Space><Argument>[<Comma>[<Space>]<Argument>]...]
+
+// Agrument types
+// <NR1> Signed integer value
+// <NR2> Floating point value
+// <NR3> Floating point value with exponent
+// <Nrf> Mixed format, any of <NR1>, <NR2>, <NR3>
+// <Nrf+> Mixed format, any of <Nrf>, MIN, MAX or DEF
+// <bin> Signed or unsigned integer in binary format
+// <Bool> 0|1|ON|OFF
+// <QString> Quoted with single (') or double quotes (")
+// <NZDig> Non-zero digit character 1-9
+// <Dig> Digit character 0-9
+// <DChar> Character with hex equivalent 00-FF (0-255 decimal)
+// <Block> {#<NZDig><Dig>[<Dig>...][<DChar>...]|#0[<DChar>...]<terminator>}
+
+// Mnemonics
+// CH<x> channel specifier, <x> is 1 through 4
+// CURSOR<x> cursor selector, <x> is 1 or 2
+
+const nr1 = `[+-]?\d+`
+const nr2 = `[+-]?(?:\d+\.?\d*|\d*\.\d+)`
+const nr3 = `${nr2}(?:[eE]${nr2})?`
+const NR1 = new RegExp(`(${nr1})`)
+const NR2 = new RegExp(`(${nr2})`)
+const NR3 = new RegExp(`(${nr3})`)
+
+const nfr = `${nr1}|${nr2}|${nr3}`
+const nfrp = `${nfr}|MIN|MAX|DEF`
+const Nfr = new RegExp(`(${nfr})`)
+const Nfrp = new RegExp(`(${nfrp})`)
+
+const bool = `0|1|ON|OFF`
+const Bool = new RegExp(`(${bool})`)
+
+const qString = `(?:["'])(?:(?:\\{2})*|(?:.*?[^\\](?:\\{2})*))\1`
+const QString = new RegExp(`(${qString})`)
+
+const dig = `\d`
+const nzdig = `[1-9]`
+const dchar = `[\\x00-\\xFF]`
+const Dig = new RegExp(`(${dig})`)
+const ZNDig = new RegExp(`(${nzdig})`)
+const DChar = new RegExp(`(${dchar})`)
+
+const block = `#${nzdig}(?:${dig})+(?:${dchar})*|#0(?:${dchar})*`
+const Block = new RegExp(`(${block})`)
+
+const header = ``
+const Header = new RegExp(`(${header})`)

+ 13 - 0
frontend/package-lock.json

@@ -4417,6 +4417,14 @@
         "worker-farm": "1.5.2"
       }
     },
+    "next-link": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/next-link/-/next-link-2.0.0.tgz",
+      "integrity": "sha512-aXT3ge5A97E+8jkjiWXf5uqw4NfT43vWV8YxoO91z8wURg16dlHOc8CmjHYMftaJDj9jQuJSTbg4TyaeiSBijQ==",
+      "requires": {
+        "on-headers": "^1.0.1"
+      }
+    },
     "next-server": {
       "version": "8.0.4",
       "resolved": "https://registry.npmjs.org/next-server/-/next-server-8.0.4.tgz",
@@ -4630,6 +4638,11 @@
         "ee-first": "1.1.1"
       }
     },
+    "on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
+    },
     "once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",

+ 1 - 0
frontend/package.json

@@ -23,6 +23,7 @@
     "graphql": "^14.2.1",
     "graphql-tag": "^2.10.1",
     "next": "^8.0.4",
+    "next-link": "^2.0.0",
     "next-with-apollo": "^3.4.0",
     "nprogress": "^0.2.0",
     "react": "^16.8.6",

+ 46 - 2
frontend/pages/instruments.js

@@ -1,7 +1,51 @@
 import Instrument from '../components/Instrument'
+import InstrumentList from '../components/InstrumentList'
+
+const instrument = {
+  name: 'Keithley 2230-3',
+  description: 'A very nice 3 channel power supply.',
+  subsystems: [{
+    name: 'Source',
+    description: 'The commands in the SOURce subsystem are used to control the output of the power supply.',
+    commands: [{
+      id: '1',
+      tag: 'Apply',
+      name: 'Apply voltage and current',
+      description: 'This command sets voltage and current levels on a specified channel with a single command message.',
+      instrument: null,
+      readString: null,
+      writeString: '[SOURce:]APPLy {CH1|CH2|CH3}, <NRf+>, <NRf+>',
+      parameters: ['channel']
+    },
+    {
+      id: '1',
+      tag: 'Output',
+      name: 'Channel output state',
+      description: 'This command sets the output state of the presently selected channel. The query form of this command returns the output state of the presently selected channel.',
+      instrument: null,
+      readString: '[SOURce:]CHANnel:OUTPut[:STATe]?',
+      writeString: '[SOURce:]CHANnel:OUTPut[:STATe] <BOOL>',
+      parameters: [{
+        name: 'channel',
+        values: ['CH1', 'CH2', 'CH3']
+      }]
+    }],
+    parameters: [{
+      name: 'channel',
+      values: ['CH1', 'CH2', 'CH3']
+    }],
+    subsystems: []
+  }]
+}
 
 const InstrumentsPage = props => (
-  <Instrument />
+  props.query && props.query.id
+    ? <Instrument instrument={instrument} />
+    : <InstrumentList />
 )
 
-export default InstrumentsPage
+InstrumentsPage.getInitialProps = ({ query }) => {
+  return { query }
+}
+
+export default InstrumentsPage