From ec9ee822dae04789e83e36554c9ecbfc0dfcb49e Mon Sep 17 00:00:00 2001 From: Anders Blomdell <anders.blomdell@control.lth.se> Date: Thu, 26 May 2016 17:58:05 +0200 Subject: [PATCH] Python protocol simulator added --- .gitignore | 3 + simulator/.gitignore | 1 + simulator/Makefile | 16 ++ simulator/controller.py | 116 +++++++++++ simulator/ethernet.py | 64 ++++++ simulator/extctrl2014.py | 418 +++++++++++++++++++++++++++++++++++++++ simulator/robot.py | 151 ++++++++++++++ simulator/run_test | 33 ++++ 8 files changed, 802 insertions(+) create mode 100644 .gitignore create mode 100644 simulator/.gitignore create mode 100644 simulator/Makefile create mode 100755 simulator/controller.py create mode 100644 simulator/ethernet.py create mode 100644 simulator/extctrl2014.py create mode 100755 simulator/robot.py create mode 100755 simulator/run_test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b71676b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +*.pyc +__pycache__ diff --git a/simulator/.gitignore b/simulator/.gitignore new file mode 100644 index 0000000..83841b1 --- /dev/null +++ b/simulator/.gitignore @@ -0,0 +1 @@ +/extctrl2014_lc.py diff --git a/simulator/Makefile b/simulator/Makefile new file mode 100644 index 0000000..0e9701f --- /dev/null +++ b/simulator/Makefile @@ -0,0 +1,16 @@ +AUTOGEN=extctrl2014_lc.py + +VPATH=../common + +all: $(AUTOGEN) + +%_lc.py: %.lc + labcomm2014 --python=$@ $< + +.PHONY: clean +clean: + rm -f *~ + +.PHONY: distclean +distclean: clean + rm -f $(AUTOGEN) diff --git a/simulator/controller.py b/simulator/controller.py new file mode 100755 index 0000000..ffcc89b --- /dev/null +++ b/simulator/controller.py @@ -0,0 +1,116 @@ +#!/usr/bin/python + +import argparse +import ethernet +import extctrl2014 +import extctrl2014_lc +import labcomm2014 +import sys + + +class Joint(extctrl2014_lc.ext2irb_joint_offset): + + def __init__(self, + parKp=0.0, + parKv=0.0, + parKi=0.0, + posOffset=0.0, + velOffset=0.0, + trqFfwOffset=0.0): + self.parKp = parKp + self.parKv = parKv + self.parKi = parKi + self.posOffset = posOffset + self.velOffset = velOffset + self.trqFfwOffset = trqFfwOffset + + def __repr__(self): + return 'Joint(posOffset=%f posRef=%f)' % ( + self.posOffset) + +class Arm(extctrl2014_lc.ext2irb_robot_net): + + def __init__(self, joint, mocgendata): + self.joint = joint + self.mocgendata = mocgendata + + def __repr__(self): + return 'Arm(joint=%s mocgendata=%s)' % (self.joint, self.mocgendata) + +class Robot(extctrl2014_lc.ext2irb_net): + + def __init__(self, arms): + self.robot = arms + + def __repr__(self): + return 'Robot(arms=%s)' % (self.robot) + +if __name__ == "__main__": + optParser = argparse.ArgumentParser(usage='%(prog)s [options]') + optParser.add_argument('--listen', + type=ethernet.parse_address, + action='store', + metavar='MAC', + required=True, + help='ethernet MAC address to listen for') + optParser.add_argument('--channel', + type=lambda s: int(s, 0), + action='store', + metavar='CHANNEL', + required=True, + help='CHANNEL to listen for') + optParser.add_argument('--interface', + action='store', + metavar='INTERFACE', + required=True, + help='INTERFACE to use for connection') + optParser.add_argument('--send-loss', + type=float, + action='store', + metavar='FRACTION', + help='FRACTION of send packets to lose [0..1]') + optParser.add_argument('--recv-loss', + type=float, + action='store', + metavar='FRACTION', + help='FRACTION of recv packets to lose [0..1]') + optParser.add_argument('-v', '--verbose', + action='store_true', + help='be verbose') + + options = optParser.parse_args(sys.argv[1:]) + + eth = ethernet.ETH(options.interface, + send_loss=options.send_loss, + recv_loss=options.recv_loss) + extctrl = extctrl2014.ExtCtrl(ethernet=eth, + channel=options.channel, + robot=options.listen) + + encoder = labcomm2014.Encoder(extctrl.writer()) + decoder = labcomm2014.Decoder(extctrl.reader()) + print encoder, decoder + decoder.add_decl(extctrl2014_lc.irb2ext_net.signature) + decoder.add_decl(extctrl2014_lc.force_torque_net.signature) + encoder.add_decl(extctrl2014_lc.ext2irb_net.signature) + + delta = 0.1 + offset = 0.0 + while True: + value,decl = decoder.decode() + if value != None and decl == extctrl2014_lc.irb2ext_net.signature: + offset += delta + if offset > 1.0: + delta = -delta + offset += delta + elif offset < -1.0: + delta = -delta + offset += delta + print "ROBOT", value + feedback = Robot([ Arm(joint=[ Joint(posOffset=offset), + Joint(posOffset=offset*2) ], + mocgendata=[])]) + encoder.encode(feedback, + extctrl2014_lc.ext2irb_net.signature) + + time.sleep(10) diff --git a/simulator/ethernet.py b/simulator/ethernet.py new file mode 100644 index 0000000..259cc7d --- /dev/null +++ b/simulator/ethernet.py @@ -0,0 +1,64 @@ +import fcntl +import socket +import struct +import sys +import random + +ETH_P_ALL = 0x0003 +SIOCGIFHWADDR = 0x8927 # Get hardware address + +def parse_address(addr): + return struct.pack('!6B', *list(int(v, 16) for v in addr.split(':'))) + +class ETH(object): + + def __init__(self, name, send_loss=0.0, recv_loss=0.0): + self.name = name + self.send_loss = send_loss + self.recv_loss = recv_loss + self.socket = socket.socket(socket.AF_PACKET, + socket.SOCK_RAW, + socket.htons(ETH_P_ALL)) + self.socket.bind((name, 0x0000)) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 100000) + ifs = fcntl.ioctl(self.socket, SIOCGIFHWADDR, + struct.pack("16sH14s", + self.name.encode(), + 0, "".encode())) + self.macaddr = struct.unpack("16x2x14s", ifs)[0][0:6] + + def send(self, data): + u = random.uniform(0, 1.0) + if u >= self.send_loss: + data = data[0:6] + self.macaddr + data[12:] + self.socket.send(data) + else: + logger.log('Dropped sent packet') + + def recv(self, maxlength): + def hexdump(data): + pos = 0 + while len(data): + hex = ' '.join(map(lambda c: '%02.2x' % ord(c), data[0:16])) + ascii = ''.join(map(lambda c: '%c' % + (c > ' ' and c <= '\x7f' and c or '.'), + data[0:16])) + print '%04x: %-50s%s' % (pos, hex, ascii) + data = data[16:] + pos += 16 + while True: + result = self.socket.recv(maxlength) + #hexdump(result) + u = random.uniform(0, 1.0) + if u >= self.send_loss: + return result + else: + logger.log('Dropped received packet') + + def __repr__(self): + def mac_to_str(mac): + return ":".join(["%02x" % b for b in self.macaddr]) + + return "ETH(%s, %s)" % (self.name, mac_to_str(self.macaddr)) + + diff --git a/simulator/extctrl2014.py b/simulator/extctrl2014.py new file mode 100644 index 0000000..56e5631 --- /dev/null +++ b/simulator/extctrl2014.py @@ -0,0 +1,418 @@ +#!/usr/bin/python + +import random +import struct +import StringIO +import time +import threading + +# +# Packet format +# +# +----+----+----+----+----+----+ +# | destination (6 bytes) +# +----+----+----+----+----+----+ +# | source (6 bytes) +# +----+----+ +# | eth_type (2 bytes 'EX') +# +----+ +# | kind+flags (1 byte) +# +....+ +# | channel (packed32) +# +----+----+----+----+ +# | cookie (uint32, XOR of both first_indices) +# +----+----+----+----+ +# | index (uint32) +# +....+ +# | frag_num (packed32) +# +....+ +# | frag_length (packed32) +# +....+ +# | data +# + +# + +flag_MASK = 0x07 +flag_LAST_FRAG = 0x01 +flag_NEED_ACK = 0x02 +flag_IS_ACK = 0x04 +kind_MASK = 0x30 +kind_INIT = 0x10 +kind_DATA = 0x20 + +class UnexpectedFragment(Exception): + + pass + +class Decoder(object): + + def __init__(self, data): + self.buf = StringIO.StringIO(data) + + def read(self, count): + return self.buf.read(count) + + def unpack(self, format): + size = struct.calcsize(format) + data = self.buf.read(size) + result = struct.unpack(format, data) + return result[0] + + def decode_uint32(self): + return self.unpack("!I") + + def decode_byte(self): + return self.unpack("!B") + + def decode_packed32(self): + result = 0 + while True: + tmp = self.decode_byte() + result = (result << 7) | (tmp & 0x7f) + if (tmp & 0x80) == 0: + break + return result + +class Encoder(object): + + def __init__(self): + self.buf = StringIO.StringIO() + + def __repr__(self): + return "Codec([%s])" % (" ".join( + map(lambda b: "0x%2.2x" % ord(b), self.getvalue()))) + + def getvalue(self): + return self.buf.getvalue() + + def write(self, data): + self.buf.write(data) + + def pack(self, format, *args): + self.buf.write(struct.pack(format, *args)) + + def encode_uint32(self, value): + self.pack("!I", value) + + def encode_byte(self, value): + self.pack("!B", value) + + def encode_packed32(self, value): + value = value & 0xffffffff + tmp = [ value & 0x7f ] + value = value >> 7 + while value: + tmp.append(value & 0x7f | 0x80) + value= value >> 7 + pass + for c in reversed(tmp): + self.encode_byte(c) + pass + pass + +class Fragmenter(Encoder): + + def __init__(self, destination, flags, channel, cookie, index): + super(Fragmenter, self).__init__() + self.destination = destination + self.flags = flags + self.cookie = cookie + self.channel = channel + self.index = index + + def fragments(self, max_length=1480): + data = self.getvalue() + fragment_number = 0 + while len(data) > 0 or fragment_number == 0: + length = min(len(data), max(1, max_length)) + remaining = data[length:] + data = data[0:length] + fragment = Encoder() + fragment.write(self.destination) + fragment.write('\x00\x00\x00\x00\x00\x00') + fragment.write('EX') + if len(remaining) == 0: + fragment.encode_byte(self.flags | flag_LAST_FRAG) + else: + fragment.encode_byte(self.flags) + fragment.encode_packed32(self.channel) + fragment.encode_uint32(self.cookie) + fragment.encode_uint32(self.index) + fragment.encode_packed32(fragment_number) + fragment.encode_packed32(length) + fragment.write(data) + yield fragment + data = remaining + fragment_number += 1 + + def __repr__(self): + return "Fragmenter([%s])" % (" ".join( + map(lambda b: "0x%2.2x" % ord(b), self.getvalue()))) + +class Packet: + + def __init__(self, data): + buf = Decoder(data) + self.destination = buf.read(6) + self.source = buf.read(6) + self.eth_type = buf.read(2) + flags = buf.decode_byte() + self.flags = flags & flag_MASK + self.kind = flags & kind_MASK + self.channel = buf.decode_packed32() + self.cookie = buf.decode_uint32() + self.index = buf.decode_uint32() + self.frag_num = buf.decode_packed32() + frag_length = buf.decode_packed32() + self.data = buf.read(frag_length) + + def join(self, other): + if self.destination != other.destination: + return None + if self.source != other.source: + return None + if self.kind != other.kind: + return None + if self.channel != other.channel: + return None + if self.cookie != other.cookie: + return None + if self.index != other.index: + return None + if self.frag_num + 1 != other.frag_num: + return None + self.flags = other.flags + self.frag_num = other.frag_num + self.data += other.data + return self + + def __repr__(self): + return "Packet(%s %s %d index=%x flags=%x kind=%x data='%s'" % ( + ":".join(map(lambda b: "%02.2x" % ord(b), self.destination)), + ":".join(map(lambda b: "%02.2x" % ord(b), self.source)), + self.channel, + self.index, + self.flags, + self.kind, + "".join(map(lambda b: "\\x%02.2x" % ord(b), self.data)), + ) + +class ExtCtrl(object): + + def __init__(self, ethernet, channel, controller=None, robot=None, + max_retries=1000, max_length=1480): + if len(filter(None, [ robot, controller])) != 1: + raise Exception('Either robot(%s) or controller(%s) ' + 'should be defined' % + (robot, controller)) + else: + self.other = filter(None, [ robot, controller])[0] + self.ethernet = ethernet + self.channel = channel + self.controller = controller + self.robot = robot + self.max_retries = max_retries + self.max_length = max_length + self.read_data = list() + self.cond = threading.Condition() + self.local_index = random.randint(0,2**32-1) + self.remote_index = None + self.cookie = None + self.ack = None + t = threading.Thread(target=self.recv_loop) + t.daemon = True + t.start() + if self.controller: + self.send_INIT() + elif self.robot: + self.await_INIT() + + + def recv_loop(self): + ack = None + data = None + while True: + fragment = Packet(self.ethernet.recv(2000)) + + # Sanity check fragment + if fragment.eth_type != 'EX': + continue + if fragment.source != self.other: + continue + if fragment.channel != self.channel: + continue + if fragment.cookie != 0 and fragment.cookie != self.cookie: + continue + + # Join fragments + if fragment.flags & flag_IS_ACK != 0: + if fragment.frag_num == 0: + ack = fragment + elif ack != None: + ack = ack.join(fragment) + else: + ack = None + packet = ack + else: + if fragment.frag_num == 0: + data = fragment + elif data != None: + data = data.join(fragment) + else: + data = None + packet = data + + if packet == None: + # Join failed + continue + if fragment.flags & flag_LAST_FRAG == 0: + # Await more fragments + continue + + if packet.flags & flag_IS_ACK != 0: + with self.cond: + if packet.index == self.ack: + self.ack = None + self.cond.notify_all() + if packet.kind == kind_INIT and self.remote_index == None: + decoder = Decoder(packet.data) + with self.cond: + self.remote_index = decoder.decode_uint32() + self.cookie = self.remote_index ^ self.local_index + self.cond.notify_all() + elif packet.kind == kind_INIT: + remote_index = packet.index + if self.remote_index == None: + # Save + with self.cond: + self.remote_index = remote_index + self.cookie = self.remote_index ^ self.local_index + self.cond.notify_all() + if remote_index == self.remote_index: + # ACK first INIT or its resends + self.send_INIT_ACK() + elif packet.kind == kind_DATA: + if self.remote_index != packet.index: + with self.cond: + self.remote_index = packet.index + self.read_data.extend(packet.data) + self.cond.notify_all() + if packet.flags & flag_NEED_ACK: + self.send_ACK(packet) + + def retries(self, index, timeout=1): + with self.cond: + self.ack = index + i = 0 + while i < self.max_retries: + yield i + i += 1 + with self.cond: + if self.ack == index: + self.cond.wait(timeout) + if self.ack != index: + raise StopIteration() + raise Exception() + + def await_INIT(self): + with self.cond: + while self.cookie == None: + self.cond.wait(1) + + def send_INIT(self): + flags = kind_INIT | flag_NEED_ACK + fragmenter = Fragmenter(destination=self.other, + channel=self.channel, + flags=flags, + cookie=0, + index=self.local_index) + for i in self.retries(self.local_index): + for f in fragmenter.fragments(max_length=self.max_length): + self.ethernet.send(f.getvalue()) + self.local_index += 1 + + def send_INIT_ACK(self): + flags = kind_INIT | flag_IS_ACK + fragmenter = Fragmenter(destination=self.other, + channel=self.channel, + flags=flags, + cookie=0, + index=self.remote_index) + fragmenter.encode_uint32(self.local_index) + for f in fragmenter.fragments(max_length=self.max_length): + self.ethernet.send(f.getvalue()) + + def send_ACK(self, packet): + flags = packet.kind | flag_IS_ACK + fragmenter = Fragmenter(destination=self.other, + channel=self.channel, + flags=flags, + cookie=packet.cookie, + index=packet.index) + for f in fragmenter.fragments(max_length=self.max_length): + self.ethernet.send(f.getvalue()) + + def send_DATA_with_ack(self, data): + flags = kind_DATA | flag_NEED_ACK + fragmenter = Fragmenter(destination=self.other, + channel=self.channel, + flags=flags, + cookie=self.cookie, + index=self.local_index) + fragmenter.write(data) + for i in self.retries(self.local_index): + for f in fragmenter.fragments(max_length=self.max_length): + self.ethernet.send(f.getvalue()) + self.local_index += 1 + + def send_DATA(self, data): + flags = kind_DATA + fragmenter = Fragmenter(destination=self.other, + channel=self.channel, + flags=flags, + cookie=self.cookie, + index=self.local_index) + fragmenter.write(data) + for f in fragmenter.fragments(max_length=self.max_length): + self.ethernet.send(f.getvalue()) + self.local_index += 1 + + def writer(self): + class Writer: + def __init__(self, extctrl): + self.extctrl = extctrl + + def mark_begin(self, decl, value): + self.data = "" + + def mark_end(self, decl, value): + if value == None: + self.extctrl.send_DATA_with_ack(data=self.data) + else: + self.extctrl.send_DATA(data=self.data) + + def write(self, data): + self.data += data + pass + + return Writer(self) + + def reader(self): + class Reader: + def __init__(self, extctrl): + self.extctrl = extctrl + + def mark(self, decl, value): + pass + + def read(self, count): + with self.extctrl.cond: + while len(self.extctrl.read_data) == 0: + self.extctrl.cond.wait() + data = self.extctrl.read_data[0:count] + self.extctrl.read_data = self.extctrl.read_data[count:] + return ''.join(data) + + return Reader(self) + + diff --git a/simulator/robot.py b/simulator/robot.py new file mode 100755 index 0000000..4aec359 --- /dev/null +++ b/simulator/robot.py @@ -0,0 +1,151 @@ +#!/usr/bin/python + +import argparse +import ethernet +import extctrl2014 +import extctrl2014_lc +import labcomm2014 +import sys +import threading +import time + +class Joint(extctrl2014_lc.irb2ext_joint_abs): + + def __init__(self, + parKp=0.0, + parKv=0.0, + parKi=0.0, + parTrqMin=0.0, + parTrqMax=0.0, + posRawAbs=0.0, + posRawFb=0.0, + posFlt=0.0, + velRaw=0.0, + velFlt=0.0, + velOut=0.0, + trqRaw=0.0, + trqRefFlt=0.0, + posRef=0.0, + velRef=0.0, + trqFfw=0.0, + trqFfwGrav=0.0): + self.parKp = parKp + self.parKv = parKv + self.parKi = parKi + self.parTrqMin = parTrqMin + self.parTrqMax = parTrqMax + self.posRawAbs = posRawAbs + self.posRawFb = posRawFb + self.posFlt = posFlt + self.velRaw = velRaw + self.velFlt = velFlt + self.velOut = velOut + self.trqRaw = trqRaw + self.trqRefFlt = trqRefFlt + self.posRef = posRef + self.velRef = velRef + self.trqFfw = trqFfw + self.trqFfwGrav = trqFfwGrav + + def __repr__(self): + return 'Joint(posRaw=%f posRef=%f)' % ( + self.posRawAbs, self.posRef) + +class Arm(extctrl2014_lc.irb2ext_robot_net): + + def __init__(self, kind, joint, mocgendata): + self.kind = kind + self.joint = joint + self.mocgendata = mocgendata + + def __repr__(self): + return 'Arm(kind=%s joint=%s mocgendata=%s)' % ( + self.kind, self.joint, self.mocgendata) + +class Robot(extctrl2014_lc.irb2ext_net): + + def __init__(self, arms): + self.obtaining = False + self.manualMode = False + self.controlActive = False + self.robot = arms + + def __repr__(self): + return 'Robot(obtaining=%s manual=%s active=%s arms=%s)' % ( + self.obtaining, self.manualMode, self.controlActive, self.robot) + +if __name__ == '__main__': + optParser = argparse.ArgumentParser(usage='%(prog)s [options]') + optParser.add_argument('--connect', + type=ethernet.parse_address, + action='store', + metavar='MAC', + required=True, + help='ethernet MAC address to connect to') + optParser.add_argument('--channel', + type=lambda s: int(s, 0), + action='store', + metavar='CHANNEL', + required=True, + help='CHANNEL to connect to') + optParser.add_argument('--interface', + action='store', + metavar='INTERFACE', + required=True, + help='INTERFACE to use for connection') + optParser.add_argument('--send-loss', + type=float, + action='store', + metavar='FRACTION', + help='FRACTION of send packets to lose [0..1]') + optParser.add_argument('--recv-loss', + type=float, + action='store', + metavar='FRACTION', + help='FRACTION of recv packets to lose [0..1]') + optParser.add_argument('-v', '--verbose', + action='store_true', + help='be verbose') + + options = optParser.parse_args(sys.argv[1:]) + + print options + eth = ethernet.ETH(options.interface, + send_loss=options.send_loss, + recv_loss=options.recv_loss) + extctrl = extctrl2014.ExtCtrl(ethernet=eth, + channel=options.channel, + controller=options.connect) + encoder = labcomm2014.Encoder(extctrl.writer()) + decoder = labcomm2014.Decoder(extctrl.reader()) + print encoder, decoder + + decoder.add_decl(extctrl2014_lc.ext2irb.signature) + encoder.add_decl(extctrl2014_lc.irb2ext_net.signature) + encoder.add_decl(extctrl2014_lc.force_torque_net.signature) + + robot = Robot([ Arm(kind=0, + joint=[ Joint(), Joint() ], + mocgendata=[])]) + def run_decoder(): + while True: + value,decl = decoder.decode() + if value != None and decl == extctrl2014_lc.ext2irb_net.signature: + print 'FEEDBACK', value + robot.robot[0].joint[0].posRawFb = ( + robot.robot[0].joint[0].posRef + + value.robot[0].joint[0].posOffset) + robot.robot[0].joint[1].posRawFb = ( + robot.robot[0].joint[1].posRef + + value.robot[0].joint[1].posOffset) + print (robot.robot[0].joint[0].posRawFb, + robot.robot[0].joint[1].posRawFb) + t = threading.Thread(target=run_decoder) + t.daemon = True + t.start() + for i in range(100): + time.sleep(0.1) + encoder.encode(robot, + extctrl2014_lc.irb2ext_net.signature) + # Let connection timeout (whenever that gets implemented) + time.sleep(10) diff --git a/simulator/run_test b/simulator/run_test new file mode 100755 index 0000000..178bfcc --- /dev/null +++ b/simulator/run_test @@ -0,0 +1,33 @@ +#!/bin/sh + +set -x + +CHANNEL=0x12 + +./controller.py --interface enp4s0f0 \ + --listen 00:15:17:79:10:89 \ + --channel ${CHANNEL} \ + --send-loss 0.0 \ + --recv-loss 0.0 \ + -v & +#> /tmp/controller.out 2>&1 & +CONTROLLER=$! +sleep 0.5 +./robot.py --interface enp4s0f1 \ + --connect 00:15:17:79:10:88 \ + --channel ${CHANNEL} \ + --send-loss 0.0 \ + --recv-loss 0.0 \ + -v & +#> /tmp/robot.out 2>&1 & +ROBOT=$! + +stop_children() { + kill ${CONTROLLER} ${ROBOT} +} +trap stop_children EXIT + +sleep 30 + + + -- GitLab