brickZA avatar brickZA committed 6169d9a

Add Silly Networked Vector Adder (SNVA) interface and server

Comments (0)

Files changed (6)

adder_interface/adder_interface.py

+import time
+
+from mysocket import MySocket
+
+class DeadSNVAException(Exception):
+    pass
+
+class AdderInterface(object):
+    """Higher level interface object for Silly Networked Vector Adder (SNVA)"""
+    registers = range(10)
+    register_ports = range(2022, 2032)
+    control_port = 2020
+    status_port = 2021
+    busy_polltime = 0.05                # in seconds
+    busy_timeout = 30                   # in seconds, before giving up
+    log_waitstring = False
+
+    def __init__(self, hostname=None):
+        """Initialise connection to Silly Networked Vector Adder (SNVA)
+
+        parameters
+        ==========
+
+        hostname -- internet hostname of the (SNVA). Defaults to localhost
+
+        """
+        if hostname is None:
+            hostname = 'localhost'
+        self.hostname = hostname
+        self._register_sockets = dict(
+            (sockno, MySocket(hostname, sockport))
+            for sockno, sockport in zip(self.registers, self.register_ports))
+        self._control_socket = MySocket(hostname, self.control_port)
+        self._status_socket = MySocket(hostname, self.status_port)
+
+    def open_connections(self):
+        """Open all socket connections to the SNVA"""
+        self._control_socket.connect()
+        self._status_socket.connect()
+        for reg_sock in self._register_sockets.values():
+            reg_sock.connect()
+        
+    def close_connections(self):
+        """Close all socket connections to the SNVA"""
+        self._control_socket.close()
+        self._status_socket.close()
+        for reg_sock in self._register_sockets.values():
+            reg_sock.close()
+
+    def set_register(self, regno, value):
+        """Set value of SNVA register 'regno' to floating point number 'value'
+
+        Note: register numbers start at 0.
+        """
+        self._regno_valid(regno)
+        regsock = self._register_sockets[regno]
+        value = float(value)            # Ensure we have a valid floating point number
+        # Might want to control the formatting of the number here for
+        # precision purposes, but going with the default format now
+        message = '%s\r\n'% str(value)
+        # Wait, since a past add operation might depend on the current
+        # register
+        self._wait_ready()              
+        regsock.send(message)
+        
+
+    def read_register(self, regno):
+        """Return value in SNVA register 'regno'
+
+        Note: register numbers start at 0.
+        """
+        self._regno_valid(regno)
+        # Wait until we are sure all pending operations are completed
+        self._wait_ready()      
+        ctrl_sock = self._control_socket
+        regsock = self._register_sockets[regno]
+        # First get rid of output created by add operations
+        self.clear_reg(regno)
+        #Now we can request the read
+        message = 'r %s\r\n' % str(regno)
+        ctrl_sock.send(message)
+
+        # Wait until we are sure the register value is written to the socket
+        self._wait_ready()
+        # Read value from register socket
+        reg_string = regsock.receive_line()
+        return float(reg_string.strip())
+
+    def clear_reg(self, regno):
+        self._regno_valid(regno)
+        regsock = self._register_sockets[regno]
+        regsock.blocking(False)
+        msg = ' '
+        while msg != '':
+            msg = regsock.receive_line()
+        regsock.blocking(True)
+
+    def add(self, target_regno, source_regno):
+        """Add source_regno to target_regno, overwiting target_regno with result"""
+        self._regno_valid(target_regno)
+        self._regno_valid(source_regno)
+        # Wait until we are sure all values are written. A future
+        # optimisation may want to check that only operations
+        # involving the current registers are pending
+        self._wait_ready()
+        ctrl_sock = self._control_socket
+        message = 'a %s %s\r\n' % (str(target_regno), str(source_regno))
+        ctrl_sock.send(message)
+        # We don't want buildup of messages
+        self.clear_reg(target_regno)
+
+    def _ready(self):
+        """Check readyness of SNVA. Return True if no operations are pending"""
+        statsock = self._status_socket
+        statsock.send('\r\n')
+        stat_msg = statsock.receive_line()
+        # It seems like the status port will return a '\n' even if
+        # there is no status
+        if self.log_waitstring: print stat_msg
+        if len(stat_msg) > 1:
+            return False
+        else:
+            return True
+
+    def _wait_ready(self):
+        """Wait until the SNVA is ready"""
+        waited = 0.
+        while waited < self.busy_timeout:
+            if self._ready(): break
+            time.sleep(self.busy_polltime)
+            waited = waited + self.busy_polltime
+        if waited >= self.busy_timeout:
+            raise DeadSNVAException("SNVA may be on fire")
+        
+    def _regno_valid(self, regno):
+        if not (regno in self.registers):
+            raise ValueError('Invalid register number; value registers are: %s',
+                             str(self.registers))

adder_interface/bogovec.py

+
+import re
+from twisted.internet import reactor
+from twisted.internet.protocol import Factory
+from twisted.protocols.basic import LineReceiver
+import random
+
+class Calc(object):
+    def __init__(self, noregs, base_timeout):
+        self.regs = [0.0] * noregs
+        self.base_timeout = base_timeout
+        self.statuses = set()
+
+    def compute(self, reg1, reg2, callback):
+        def set_reg(no, v):
+            self.regs[no] = v
+            self.statuses.remove('r%d+r%d' % (reg1, reg2))
+            callback(no)
+        self.statuses.add('r%d+r%d' % (reg1, reg2))
+        reactor.callLater(random.random() * self.base_timeout, set_reg, reg1,
+                          self.regs[reg1] + self.regs[reg2])
+
+class ControlProtocol(LineReceiver):
+    def connectionMade(self):
+        self.factory.control_conns.append(self.transport)
+
+    def lineReceived(self, data):
+        def callback(no):
+            self.factory.do_write(no)
+
+        m = re.match("a (\d+) (\d+)$", data)
+        if m:
+            self.factory.calc.compute(int(m.group(1)), int(m.group(2)),
+                                      callback)
+            return
+        m = re.match("r (\d+)$", data)
+        if m:
+            self.factory.broadcast_read(int(m.group(1)))
+            return
+        print "Bogus message", data
+
+class RegisterProtocol(LineReceiver):
+    def connectionMade(self):
+        self.no = self.transport.getHost().port - self.factory.base_port
+        self.factory.register_conns[self.no].append(self.transport)
+
+    def lineReceived(self, data):
+        self.factory.calc.regs[self.no] = float(data)
+
+class StatusProtocol(LineReceiver):
+    def connectionMade(self):
+        self.factory.status_conns.append(self.transport)
+
+    def lineReceived(self, _):
+        l = ' '.join([i for i in self.factory.calc.statuses])
+        for conn in self.factory.status_conns:
+            conn.write(l + "\n")
+
+class ControlFactory(Factory):
+    protocol = ControlProtocol
+
+    def __init__(self, calc, reg_fac):
+        self.calc = calc
+        self.control_conns = []
+        self.reg_fac = reg_fac
+
+    def do_write(self, no):
+        for conn in self.reg_fac.register_conns[no]:
+            conn.write(str(self.calc.regs[no]) + "\n")
+
+    def broadcast_read(self, no):
+        def do_write():
+            self.calc.statuses.remove('->r%d' % no)
+            self.do_write(no)
+
+        self.calc.statuses.add('->r%d' % no)
+        reactor.callLater(random.random() * self.calc.base_timeout, do_write)
+
+class RegisterFactory(Factory):
+    protocol = RegisterProtocol
+
+    def __init__(self, calc, base_port, no):
+        self.calc = calc
+        self.base_port = base_port
+        self.register_conns = [[] for i in range(no)]
+
+class StatusFactory(Factory):
+    protocol = StatusProtocol
+
+    def __init__(self, calc):
+        self.calc = calc
+        self.status_conns = []
+
+NO_REGS = 10
+BASE_PORT = 2020
+
+def main(argv):
+    if argv:
+        base_timeout = float(argv[0])
+    else:
+        base_timeout = 1
+    calc = Calc(NO_REGS, base_timeout)
+    reg_fac = RegisterFactory(calc, BASE_PORT + 2, NO_REGS)
+    reactor.listenTCP(BASE_PORT, ControlFactory(calc, reg_fac))
+    reactor.listenTCP(BASE_PORT + 1, StatusFactory(calc))
+    for i in range(NO_REGS):
+        reactor.listenTCP(BASE_PORT + 2 + i, reg_fac)
+    print "BogoVec server running on port %d-%d" % (BASE_PORT,
+                                                    BASE_PORT + NO_REGS + 1)
+    reactor.run()
+
+if __name__ == '__main__':
+    import sys
+    main(sys.argv[1:])

adder_interface/experiment.py

+import time
+import adder_interface
+
+adder = adder_interface.AdderInterface()
+adder.open_connections()
+adder.set_register(0,11)
+adder.set_register(1,1000.3)
+for r in range(2,10):
+    adder.set_register(r, 0)
+adder.add(0, 1)
+print '0:', adder.read_register(0)
+print '1:', adder.read_register(1)
+adder.add(4,0)
+adder.add(2,4)
+print '4:', adder.read_register(4)
+print '2:', adder.read_register(2)
+adder.add(2,4)
+#adder.log_waitstring = True
+print '2:', adder.read_register(2)
+time.sleep(1.1)
+print '2:', adder.read_register(2)
+
+adder.close_connections()

adder_interface/mysocket.py

+import socket
+
+class BrokenSocketError(RuntimeError):
+    pass
+
+class MySocket(object):
+    '''Simple socket wrapper
+
+    Partially stolen from here: http://docs.python.org/howto/sockets.html
+    '''
+
+    def __init__(self, host, port):
+        self.host = host
+        self.port = port
+
+    def connect(self):
+        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.sock.connect((self.host, self.port))
+
+    def close(self):
+        self.sock.close()
+
+    def blocking(self, blocking):
+        self.sock.setblocking(bool(blocking))
+
+    def send(self, msg):
+        totalsent = 0
+        while totalsent < len(msg):
+            sent = self.sock.send(msg[totalsent:])
+            if sent == 0:
+                raise BrokenSocketError("socket connection broken")
+            totalsent = totalsent + sent
+
+    def receive_line(self):
+        read_char = ''
+        msg = []
+        while read_char != '\n':
+            try:
+                read_char = self.sock.recv(1)
+                if read_char == '':
+                    raise BrokenSocketError("socket connection broken")
+            except socket.error, s:
+                if s.errno != socket.errno.EAGAIN:
+                    raise
+                else:
+                    break
+            msg.append(read_char)
+        
+        return ''.join(msg)

adder_interface/server_test.py

+import time
+import re
+import socket
+import mysocket
+import adder_interface
+
+class ClientSocket(mysocket.MySocket):
+    def __init__(self, client_socket):
+        self.sock = client_socket
+
+    def connect(self):
+        raise NotImplementedError("Client sockets cannot create connections")
+
+class AdderServer(object):
+    set_re = re.compile(r"s\s+(\d+)\s+([.\d]+)")
+    add_re = re.compile(r"a\s+(\d+)\s+(\d+)")
+    read_re = re.compile(r"r\s+(\d+)")
+    def __init__(self, adder, clientsocket):
+        self.adder = adder
+        self.clientsocket = clientsocket
+
+    def handle_command(self, command):
+        try:
+            m = self.set_re.match(command)
+            if m:
+                self.handle_set(m)
+                return
+            m = self.add_re.match(command)
+            if m:
+                self.handle_add(m)
+                return
+            m = self.read_re.match(command)
+            if m:
+                self.handle_read(m)
+                return
+            self.error_message('Illegal command %s ' % command)
+        except:
+            # Amazing error handling, I know.
+            self.error_message('Illegal command or error')
+            
+    def run(self):
+        while 1:
+            try:
+                command = self.clientsocket.receive_line()
+                if command[0:4] == 'quit':
+                    print 'quitting client thread'
+                    self.clientsocket.close()
+                    return
+                self.handle_command(command)
+            except mysocket.BrokenSocketError:
+                return
+            
+    def error_message(self, message):
+        self.clientsocket.send(message+'\n')
+
+    def handle_set(self, match):
+        regno = int(match.group(1))
+        value = float(match.group(2))
+        self.adder.set_register(regno, value)
+        self.clientsocket.send('Register %d set to %f\n' % (regno, value))
+        
+    def handle_add(self, match):
+        target_regno = int(match.group(1))
+        source_regno = int(match.group(2))
+        self.adder.add(target_regno, source_regno)
+        self.clientsocket.send(
+            'Command issued to add source register %d to target register %d\n'
+            % (source_regno, target_regno))
+
+    def handle_read(self, match):
+        regno = int(match.group(1))
+        value = self.adder.read_register(regno)
+        self.clientsocket.send('%f\n' % value)
+
+        
+        
+#create an INET, STREAMing socket
+serversocket = socket.socket(
+    socket.AF_INET, socket.SOCK_STREAM)
+#bind the socket to a public host and wait for port to become available
+while 1:
+    try:
+        serversocket.bind((socket.gethostname(), 3080))
+        break
+    except socket.error:
+        time.sleep(0.1)
+        pass
+#become a server socket
+serversocket.listen(1)
+print 'server active'
+
+adder = adder_interface.AdderInterface()
+adder.open_connections()
+
+while 1:
+    #accept connections from outside
+    (raw_clientsocket, address) = serversocket.accept()
+    #now do something with the clientsocket
+    #in this case, we'll pretend this is a threaded server
+    clientsocket = ClientSocket(raw_clientsocket)
+    adder_server = AdderServer(adder, clientsocket)
+    print 'starting adder server'
+    adder_server.run()
+serversocket.close()
+
+adder.close_connections()
+

adder_interface/task2.txt

+Task: Create a TCP server to control a low-level device
+=======================================================
+
+A hardware subcontractor for a project you are working on is
+developing a simple vector adding machine. The adding machine has an
+Ethernet port and communicates via TCP but the interface is low-level
+and not very convenient to use. You have been tasked with creating a
+high-level interface that provides more convenient access to the
+device's functionality.
+
+ * The first hardware units won't arrive for many weeks but the vendor
+   has provided a software simulator (bogovec.py) that you can use
+   instead. Unfortunately the simulator is somewhat lacking in
+   documentation.
+
+ * The high-level interface should be implemented as a TCP server that
+   listens on a single port and accepts text commands. It may be
+   written in whatever language you choose although developing it in
+   Python or C would be an advantage (since both of these languages
+   are in use at KAT).
+
+ * If you wish you may make use of the KATCP library. KATCP is a
+   simple protocol on top of TCP that was developed by KAT for
+   controlling and monitoring hardware devices. The latest Python
+   implementation is available at
+   http://pypi.python.org/pypi/katcp/0.3.1 and includes an example
+   server that may provide a good starting point for your own.
+
+ * The high-level interface should expose the full power of the
+   hardware device but the details of how the high-level interface
+   should look are yours to decide.
+
+ * You may assume that your server has sole control of the low-level
+   device.
+
+ * The simulator may be launched using `python bogovec.py [<maximum
+   response time in seconds>]`. The maximum response time refers to
+   the time taken by the simulator to complete individual commands. In
+   addition to Python itself the simulator requires the Twisted
+   package.
+
+
+Description of the device
+-------------------------
+
+The device implements an extremely unrealistic vector calculator. It
+has a number of registers (ten by default) and operations to read,
+write and add registers. There is also (fortunately) a monitoring
+interface.
+
+The adding and reading operations can take an variable amount of
+time. The simulator models this by waiting a random number seconds
+before completing these operations. The maximum wait time for an
+operation is specified on when lauching the simulator.
+
+
+The low-level device interface
+------------------------------
+
+ All communication with the simulator (and the vaporware device) is
+ over TCP and uses a line-based protocol with \r\n delimiting a new
+ line. A telnet client is an easy way to try things out on the
+ simulator.
+
+The device listens on a range of ports, each with a different function:
+
+ * Port 2020 (control port):
+
+   * r <n>: reads register number <n>.
+
+   * a <n1> <n2>: adds register <n1> to <n2> and stores the result in <n1>.
+
+ * Port 2021 (status port):
+
+   * Sending an empty line will return a status message ending with '\n'
+     that contains information about pending reads and additions.
+
+ * Ports 2022 to 2031 (register port):
+
+   * A number written to this port will immediately store the number
+     in the associated register.
+
+   * When a read command is sent to the control port, the result will
+     be sent to all clients connected to the register port for that
+     register. It will take a variable amount of time for this
+     operation to complete.
+
+   * When an add command completes, the result register will print its
+     value to its register port. It will take a variable amount of
+     time for this operation to complete.
+
+The behaviour if other messages are sent is undefined.
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.