diff --git a/ebpfcat/ebpfcat.py b/ebpfcat/ebpfcat.py index 510b0633fa45e59bd0f6ed134244b181b8048bc2..648243eb7fc14748da45b27b5398f46619420e87 100644 --- a/ebpfcat/ebpfcat.py +++ b/ebpfcat/ebpfcat.py @@ -19,98 +19,143 @@ class PacketDesc: def __get__(self, instance, owner): if instance is None: return self - return PacketVar(instance, self) - + offset = instance.position_offset[self.position[0]] + if isinstance(instance, Struct): + terminal = instance.terminal + device = instance.device + else: + terminal = instance + device = None + ret = PacketVar(terminal, (self.position[0], + self.position[1] + offset), self.size) + if device is None: + return ret + else: + return ret.get(device) -class PacketVar: - def __init__(self, terminal, desc): - self.terminal = terminal - self.desc = desc + def __set__(self, instance, value): + offset = instance.position_offset[self.position[0]] + ret = PacketVar(instance.terminal, + (self.position[0], self.position[1] + offset), + self.size) + return ret.set(instance.device, value) -class TerminalVar(MemoryDesc): +class PacketVar(MemoryDesc): base_register = 9 - def __init__(self): - pass # do not call parent to set self.fmt - def fmt(self): - pv = instance.__dict__[self.name] - if isinstance(pv.desc.size, int): + if isinstance(self.size, int): return "B" else: - return pv.desc.size + return self.size - def __set__(self, instance, value): - if isinstance(value, PacketVar): - instance.__dict__[self.name] = value - elif instance.sync_group.current_data is None: - fmt, _ = self._fmt_start(instance) - if isinstance(fmt, int): + def __init__(self, terminal, position, size): + self.terminal = terminal + self.position = position + self.size = size + + def set(self, device, value): + if device.sync_group.current_data is None: + if isinstance(self.size, int): try: bool(value) except RuntimeError: - e = instance.sync_group + e = device.sync_group with e.wtmp: - e.wtmp = super().__get__(instance, None) + e.wtmp = super().__get__(device, None) with value as cond: - e.wtmp |= 1 << fmt + e.wtmp |= 1 << self.size with cond.Else(): - e.wtmp &= ~(1 << fmt) - super().__set__(instance, e.wtmp) + e.wtmp &= ~(1 << self.size) + super().__set__(device, e.wtmp) return else: - old = super().__get__(instance, None) + old = super().__get__(device, None) if value: - value = old | (1 << fmt) + value = old | (1 << self.size) else: - value = old & ~(1 << fmt) - super().__set__(instance, value) + value = old & ~(1 << self.size) + super().__set__(device, value) else: - data = instance.sync_group.current_data - fmt, start = self._fmt_start(instance) - if isinstance(fmt, int): + data = device.sync_group.current_data + start = self._start(device) + if isinstance(self.size, int): if value: - data[start] |= 1 << fmt + data[start] |= 1 << self.size else: - data[start] &= ~(1 << fmt) + data[start] &= ~(1 << self.size) else: - pack_into("<" + fmt, data, start, value) + pack_into("<" + self.size, data, start, value) + + def get(self, device): + if device.sync_group.current_data is None: + if isinstance(self.size, int): + return super().__get__(device, None) & (1 << self.size) + else: + return super().__get__(device, None) + else: + data = device.sync_group.current_data + start = self._start(device) + if isinstance(self.size, int): + return bool(data[start] & (1 << self.size)) + else: + return unpack_from("<" + self.size, data, start)[0] + + def _start(self, device): + base, offset = self.position + return device.sync_group.terminals[self.terminal][base] + offset + + def fmt_addr(self, device): + return ("B" if isinstance(self.size, int) else self.size, + self._start(device) + Packet.ETHERNET_HEADER) + + +class Struct: + device = None + + def __new__(cls, *args): + return StructDesc(cls, *args) + + +class StructDesc: + def __init__(self, struct, *position_offset): + self.struct = struct + self.position_offset = position_offset def __get__(self, instance, owner): if instance is None: return self - elif self.name not in instance.__dict__: - return None - elif instance.sync_group.current_data is None: - fmt, _ = self._fmt_start(instance) - if isinstance(fmt, int): - return super().__get__(instance, owner) & (1 << fmt) - else: - return super().__get__(instance, owner) + ret = object.__new__(self.struct) + ret.position_offset = self.position_offset + ret.terminal = instance + return ret + + +class TerminalVar: + def __set__(self, instance, value): + if isinstance(value, PacketVar): + instance.__dict__[self.name] = value + elif isinstance(value, Struct): + instance.__dict__[self.name] = value + value.device = instance else: - data = instance.sync_group.current_data - fmt, start = self._fmt_start(instance) - if isinstance(fmt, int): - return bool(data[start] & (1 << fmt)) - else: - return unpack_from("<" + fmt, data, start)[0] + return instance.__dict__[self.name].set(instance, value) - def _fmt_start(self, instance): - pv = instance.__dict__[self.name] - base, offset = pv.desc.position - start = pv.terminal.bases[base] + offset - return pv.desc.size, start + def __get__(self, instance, owner): + if instance is None: + return self + var = instance.__dict__.get(self.name) + if var is None: + return None + elif isinstance(var, Struct): + return var + else: + return instance.__dict__[self.name].get(instance) def __set_name__(self, owner, name): self.name = name - def fmt_addr(self, instance): - fmt, start = self._fmt_start(instance) - if isinstance(fmt, int): - fmt = "B" - return fmt, start + Packet.ETHERNET_HEADER - class DeviceVar(ArrayGlobalVarDesc): def __init__(self, size="I"): @@ -122,7 +167,7 @@ class DeviceVar(ArrayGlobalVarDesc): elif instance.sync_group.current_data is None: return super().__get__(instance, owner) else: - return instance.__dict__[self.name] + return instance.__dict__.get(self.name, 0) def __set__(self, instance, value): if instance.sync_group.current_data is None: @@ -140,13 +185,14 @@ class Device(SubProgram): def get_terminals(self): ret = set() for pv in self.__dict__.values(): - if isinstance(pv, PacketVar): + if isinstance(pv, (PacketVar, Struct)): ret.add(pv.terminal) return ret class EBPFTerminal(Terminal): compatibility = None + position_offset = 0, 0 def __init_subclass__(cls): cls.pdo = {} @@ -163,15 +209,16 @@ class EBPFTerminal(Terminal): def allocate(self, packet): if self.pdo_in_sz: - self.bases = [packet.size + packet.DATAGRAM_HEADER] + bases = [packet.size + packet.DATAGRAM_HEADER] packet.append(ECCmd.FPRD, b"\0" * self.pdo_in_sz, 0, self.position, self.pdo_in_off) else: - self.bases = [None] + bases = [None] if self.pdo_out_sz: - self.bases.append(packet.size + packet.DATAGRAM_HEADER) + bases.append(packet.size + packet.DATAGRAM_HEADER) packet.append(ECCmd.FPWR, b"\0" * self.pdo_out_sz, 0, self.position, self.pdo_out_off) + return bases def update(self, data): pass @@ -239,15 +286,9 @@ class FastEtherCat(EtherCat): self.ebpf.programs = self.programs self.fd = await self.ebpf.attach(self.addr[0]) - -class SyncGroup: - """A group of devices communicating at the same time""" - - packet_index = 1000 - - current_data = False # None is used to indicate FastSyncGroup - +class SyncGroupBase: def __init__(self, ec, devices, **kwargs): + super().__init__(**kwargs) self.ec = ec self.devices = devices @@ -256,7 +297,20 @@ class SyncGroup: terminals.update(dev.get_terminals()) dev.sync_group = self # sorting is only necessary for test stability - self.terminals = sorted(terminals, key=lambda t: t.position) + self.terminals = {t: None for t in + sorted(terminals, key=lambda t: t.position)} + + def allocate(self): + self.packet = Packet() + self.terminals = {t: t.allocate(self.packet) for t in self.terminals} + + +class SyncGroup(SyncGroupBase): + """A group of devices communicating at the same time""" + + packet_index = 1000 + + current_data = False # None is used to indicate FastSyncGroup async def run(self): await gather(*[t.to_operational() for t in self.terminals]) @@ -269,16 +323,14 @@ class SyncGroup: dev.update() def start(self): - self.packet = Packet() - for term in self.terminals: - term.allocate(self.packet) + self.allocate() self.packet_index = SyncGroup.packet_index SyncGroup.packet_index += 1 self.asm_packet = self.packet.assemble(self.packet_index) return ensure_future(self.run()) -class FastSyncGroup(XDP): +class FastSyncGroup(SyncGroupBase, XDP): license = "GPL" current_data = None @@ -286,16 +338,7 @@ class FastSyncGroup(XDP): properties = ArrayMap() def __init__(self, ec, devices, **kwargs): - super().__init__(subprograms=devices, **kwargs) - self.ec = ec - self.devices = devices - - terminals = set() - for dev in self.devices: - terminals.update(dev.get_terminals()) - dev.sync_group = self - # sorting is only necessary for test stability - self.terminals = sorted(terminals, key=lambda t: t.position) + super().__init__(ec, devices, subprograms=devices, **kwargs) def program(self): with self.packetSize >= self.packet.size + Packet.ETHERNET_HEADER as p: @@ -304,9 +347,7 @@ class FastSyncGroup(XDP): self.exit(XDPExitCode.TX) def start(self): - self.packet = Packet() - for term in self.terminals: - term.allocate(self.packet) + self.allocate() index = self.ec.register_sync_group(self) self.ec.send_packet(self.packet.assemble(index)) self.monitor = ensure_future(gather(*[t.to_operational() diff --git a/ebpfcat/ethercat_test.py b/ebpfcat/ethercat_test.py index a6502152e05ccbb9e211ad4e4bd0a28409284f9a..c5e885bb72f74e823defbb05ec4379793997e971 100644 --- a/ebpfcat/ethercat_test.py +++ b/ebpfcat/ethercat_test.py @@ -2,9 +2,10 @@ from asyncio import CancelledError, Future, get_event_loop, sleep, gather from unittest import TestCase, main from .devices import AnalogInput, AnalogOutput -from .terminals import EL4104, EL3164 +from .terminals import EL4104, EL3164, EK1814 from .ethercat import ECCmd -from .ebpfcat import SyncGroup +from .ebpfcat import FastSyncGroup, SyncGroup, TerminalVar, Device +from .ebpf import Instruction, Opcode as O class MockEtherCat: @@ -26,18 +27,22 @@ class MockEtherCat: await sleep(0) return self.results.pop(0) + def register_sync_group(self, sg): + self.rsg = sg + return 0x33 + class Tests(TestCase): def test_input(self): ti = EL3164() ti.pdo_in_sz = 4 - ti.pdo_in_off = 0xABCD + ti.pdo_in_off = 0xABCD ti.position = 0x77 ti.pdo_out_sz = 3 ti.pdo_out_off = 0x4321 ec = MockEtherCat(self) ti.ec = ec - ai = AnalogInput(ti.ch1_value) + ai = AnalogInput(ti.channel1.value) SyncGroup.packet_index = 1000 sg = SyncGroup(ec, [ai]) self.task = sg.start() @@ -73,7 +78,7 @@ class Tests(TestCase): def test_output(self): ti = EL4104() ti.pdo_in_sz = 4 - ti.pdo_in_off = 0xABCD + ti.pdo_in_off = 0xABCD ti.position = 0x77 ti.pdo_out_sz = 3 ti.pdo_out_off = 0x4321 @@ -120,6 +125,97 @@ class Tests(TestCase): with self.assertRaises(CancelledError): get_event_loop().run_until_complete(self.task) + def test_ebpf(self): + ti = EL3164() + ti.pdo_in_sz = 4 + ti.pdo_in_off = 0xABCD + ti.position = 0x77 + ti.pdo_out_sz = None + ti.pdo_out_off = None + to = EL4104() + to.pdo_in_sz = None + to.pdo_in_off = None + to.position = 0x55 + to.pdo_out_sz = 2 + to.pdo_out_off = 0x5678 + td = EK1814() + td.pdo_in_sz = 1 + td.pdo_in_off = 0x7777 + td.position = 0x44 + td.pdo_out_sz = 1 + td.pdo_out_off = 0x8888 + + class D(Device): + ai = TerminalVar() + ao = TerminalVar() + di = TerminalVar() + do = TerminalVar() + + def program(self): + self.do = False + self.do = True + self.do = self.ai + self.ao = self.di + with self.di: + self.ao = self.ai + + d = D() + d.ai = ti.channel1.value + d.ao = to.ch1_value + d.di = td.ch1 + d.do = td.ch5 + + ec = MockEtherCat(self) + sg = FastSyncGroup(ec, [d]) + ec.expected = [ + bytes.fromhex("4610" # EtherCAT Header, length & type + "000033000000008000000000" # ID datagram + "04004400777701800000000000" # digi in + "05004400888801800000000000" # digi out + "0500550078560280000000000000" # ana out + "04007700cdab04000000000000000000") # ana in + ] + task = sg.start() + self.assertEqual(ec.rsg, sg) + task.cancel() + with self.assertRaises(CancelledError): + get_event_loop().run_until_complete(task) + sg.program() + self.maxDiff = None + self.assertEqual(sg.opcodes, [ + Instruction(opcode=O.W+O.LD, dst=9, src=1, off=0, imm=0), + Instruction(opcode=O.W+O.LD, dst=0, src=1, off=4, imm=0), + Instruction(opcode=O.W+O.LD, dst=2, src=1, off=0, imm=0), + Instruction(opcode=O.ADD+O.LONG, dst=2, src=0, off=0, imm=83), + Instruction(opcode=O.JLE+O.REG, dst=0, src=2, off=21, imm=0), + + Instruction(opcode=O.B+O.LD, dst=0, src=9, off=51, imm=0), + Instruction(opcode=O.AND, dst=0, src=0, off=0, imm=-2), + Instruction(opcode=O.STX+O.B, dst=9, src=0, off=51, imm=0), + Instruction(opcode=O.B+O.LD, dst=0, src=9, off=51, imm=0), + Instruction(opcode=O.OR, dst=0, src=0, off=0, imm=1), + Instruction(opcode=O.STX+O.B, dst=9, src=0, off=51, imm=0), + + Instruction(opcode=O.LD+O.B, dst=0, src=9, off=51, imm=0), + Instruction(opcode=O.REG+O.LD, dst=2, src=9, off=80, imm=0), + Instruction(opcode=O.JEQ, dst=2, src=0, off=2, imm=0), + Instruction(opcode=O.OR, dst=0, src=0, off=0, imm=1), + Instruction(opcode=O.JMP, dst=0, src=0, off=1, imm=0), + Instruction(opcode=O.AND, dst=0, src=0, off=0, imm=-2), + Instruction(opcode=O.STX+O.B, dst=9, src=0, off=51, imm=0), + + Instruction(opcode=O.B+O.LD, dst=0, src=9, off=38, imm=0), + Instruction(opcode=O.AND, dst=0, src=0, off=0, imm=1), + Instruction(opcode=O.STX+O.REG, dst=9, src=0, off=64, imm=0), + + Instruction(opcode=O.LD+O.B, dst=0, src=9, off=38, imm=0), + Instruction(opcode=O.JSET, dst=0, src=0, off=1, imm=1), + Instruction(opcode=O.JMP, dst=0, src=0, off=2, imm=0), + Instruction(opcode=O.LD+O.REG, dst=0, src=9, off=80, imm=0), + Instruction(opcode=O.STX+O.REG, dst=9, src=0, off=64, imm=0), + + Instruction(opcode=O.MOV+O.LONG, dst=0, src=0, off=0, imm=3), + Instruction(opcode=O.EXIT, dst=0, src=0, off=0, imm=0)]) if __name__ == "__main__": main() diff --git a/ebpfcat/terminals.py b/ebpfcat/terminals.py index 721d7bc24520684e5f360c9dba232ada0bc126e5..620a43461d0dfc11fbdd082078aa97c1aa849e30 100644 --- a/ebpfcat/terminals.py +++ b/ebpfcat/terminals.py @@ -1,4 +1,4 @@ -from .ebpfcat import EBPFTerminal, PacketDesc +from .ebpfcat import EBPFTerminal, PacketDesc, Struct class Generic(EBPFTerminal): @@ -13,14 +13,15 @@ class EL4104(EBPFTerminal): class EL3164(EBPFTerminal): - ch1_attrs = PacketDesc((0, 0), 'H') - ch2_attrs = PacketDesc((0, 4), 'H') - ch3_attrs = PacketDesc((0, 8), 'H') - ch4_attrs = PacketDesc((0, 12), 'H') - ch1_value = PacketDesc((0, 2), 'H') - ch2_value = PacketDesc((0, 6), 'H') - ch3_value = PacketDesc((0, 10), 'H') - ch4_value = PacketDesc((0, 14), 'H') + class Channel(Struct): + attrs = PacketDesc((0, 0), 'H') + value = PacketDesc((0, 2), 'H') + + channel1 = Channel(0) + channel2 = Channel(4) + channel3 = Channel(8) + channel4 = Channel(12) + class EK1814(EBPFTerminal): ch1 = PacketDesc((0, 0), 0)