From 4c968882d44879c6a5f11526ac4c72c241fd7303 Mon Sep 17 00:00:00 2001
From: Martin Teichmann <martin.teichmann@gmail.com>
Date: Fri, 1 Sep 2023 12:56:44 +0100
Subject: [PATCH] first working version with fmmu

this is tested with read hardware, the tests obviously don't work,
we have to grandfather them.
---
 ebpfcat/ebpfcat.py  | 107 +++++++++++++++++++++++++++++++++-----------
 ebpfcat/ethercat.py |  36 ++++++++++++++-
 2 files changed, 114 insertions(+), 29 deletions(-)

diff --git a/ebpfcat/ebpfcat.py b/ebpfcat/ebpfcat.py
index c54c441..b4d3af1 100644
--- a/ebpfcat/ebpfcat.py
+++ b/ebpfcat/ebpfcat.py
@@ -18,7 +18,7 @@
 """The high-level API for EtherCAT loops"""
 from asyncio import ensure_future, gather, sleep, wait_for, TimeoutError
 from collections import defaultdict
-from contextlib import asynccontextmanager, contextmanager
+from contextlib import asynccontextmanager, AsyncExitStack, contextmanager
 import os
 from struct import pack, unpack, calcsize, pack_into, unpack_from
 from time import time
@@ -238,6 +238,7 @@ class Device(SubProgram):
 class EBPFTerminal(Terminal):
     compatibility = None
     position_offset = {2: 0, 3: 0}
+    use_fmmu = True
 
     async def apply_eeprom(self):
         await super().apply_eeprom()
@@ -255,17 +256,28 @@ class EBPFTerminal(Terminal):
     def allocate(self, packet, readwrite):
         """allocate space in packet for the pdos of this terminal
 
-        return a dict that contains the starting offset for each
-        sync manager"""
+        return a dict that contains the datagram number and
+        starting offset therein for each sync manager.
+
+        Negative datagram numbers are for the future FMMU
+        datagrams."""
         bases = {}
-        if self.pdo_in_sz:
-            bases[3] = packet.size + packet.DATAGRAM_HEADER
-            packet.append(ECCmd.FPRD, b"\0" * self.pdo_in_sz, 0,
-                          self.position, self.pdo_in_off)
-        if readwrite and self.pdo_out_sz:
-            bases[2] = packet.size + packet.DATAGRAM_HEADER
-            packet.append_writer(ECCmd.FPWR, b"\0" * self.pdo_out_sz, 0,
-                                 self.position, self.pdo_out_off)
+        if self.use_fmmu:
+            if self.pdo_in_sz:
+                bases[3] = (1, packet.fmmu_in_size)
+                packet.fmmu_in_size += self.pdo_in_sz
+            if readwrite and self.pdo_out_sz:
+                bases[2] = (2, packet.fmmu_out_size)
+                packet.fmmu_out_size += self.pdo_out_sz
+        else:
+            if self.pdo_in_sz:
+                bases[3] = (0, packet.size)
+                packet.append(ECCmd.FPRD, b"\0" * self.pdo_in_sz, 0,
+                              self.position, self.pdo_in_off)
+            if readwrite and self.pdo_out_sz:
+                bases[2] = (0, packet.size)
+                packet.append_writer(ECCmd.FPWR, b"\0" * self.pdo_out_sz, 0,
+                                     self.position, self.pdo_out_off)
         return bases
 
     def update(self, data):
@@ -377,9 +389,13 @@ class FastEtherCat(SimpleEtherCat):
 
 class SterilePacket(Packet):
     """a sterile packet has all its sets exchanges by NOPs"""
+    next_logical_addr = 0  # global for all packets
+    logical_addr_inc = 0x800
+
     def __init__(self):
         super().__init__()
         self.on_the_fly = []  # list of sterilized positions
+        self.fmmu_out_size = self.fmmu_in_size = 0
 
     def append_writer(self, cmd, *args):
         self.on_the_fly.append((self.size, cmd))
@@ -391,6 +407,19 @@ class SterilePacket(Packet):
             ret[pos] = ECCmd.NOP.value
         return ret
 
+    def append_fmmu(self):
+        SterilePacket.next_logical_addr += 2 * self.logical_addr_inc
+        fmmu_in_pos = self.size
+        if self.fmmu_in_size:
+            self.append(ECCmd.LRD, b"\0" * self.fmmu_in_size, 0,
+                        self.next_logical_addr)
+        fmmu_out_pos = self.size
+        if self.fmmu_out_size:
+            self.append_writer(ECCmd.LWR, b"\0" * self.fmmu_out_size, 0,
+                               self.next_logical_addr + self.logical_addr_inc)
+        return (fmmu_in_pos, fmmu_out_pos, self.next_logical_addr,
+                self.next_logical_addr + self.logical_addr_inc)
+
     def activate(self, ebpf):
         for pos, cmd in self.on_the_fly:
             ebpf.pB[pos + self.ETHERNET_HEADER] = cmd.value
@@ -399,6 +428,7 @@ class SyncGroupBase:
     missed_counter = 0
 
     current_data = None
+    logical_in = logical_out = None
 
     def __init__(self, ec, devices, **kwargs):
         super().__init__(**kwargs)
@@ -418,23 +448,49 @@ class SyncGroupBase:
     async def to_operational(self):
         await gather(*[t.to_operational() for t in self.terminals])
 
+    @asynccontextmanager
+    async def map_fmmu(self):
+        async with AsyncExitStack() as stack:
+            for terminal, bases in self.fmmu_maps.items():
+                base = bases.get(2)
+                if base is not None:
+                    await stack.enter_async_context(
+                            terminal.map_fmmu(base, True))
+                base = bases.get(3)
+                if base is not None:
+                    await stack.enter_async_context(
+                            terminal.map_fmmu(base, False))
+            yield
+
     async def run(self):
         data = self.asm_packet
-        while True:
-            self.ec.send_packet(data)
-            try:
-                data = await wait_for(self.ec.receive_index(self.packet_index),
-                                      timeout=0.1)
-            except TimeoutError:
-                self.missed_counter += 1
-                print("didn't receive in time", self.missed_counter)
-                continue
-            data = self.update_devices(data)
+        async with self.map_fmmu():
+            ensure_future(self.to_operational())
+            while True:
+                self.ec.send_packet(data)
+                try:
+                    data = await wait_for(
+                            self.ec.receive_index(self.packet_index),
+                            timeout=0.1)
+                except TimeoutError:
+                    self.missed_counter += 1
+                    print("didn't receive in time", self.missed_counter)
+                    continue
+                data = self.update_devices(data)
 
     def allocate(self):
         self.packet = SterilePacket()
-        self.terminals = {t: t.allocate(self.packet, rw)
-                          for t, rw in self.terminals.items()}
+        terminals = {t: t.allocate(self.packet, rw)
+                     for t, rw in self.terminals.items()}
+        in_pos, out_pos, logical_in, logical_out = self.packet.append_fmmu()
+        offsets = (0, in_pos, out_pos)
+        self.terminals = {t: {sm: offsets[base] + off + Packet.DATAGRAM_HEADER
+                              for sm, (base, off) in d.items()}
+                          for t, d in terminals.items()}
+        offsets = (None, logical_in, logical_out)
+        self.fmmu_maps = {t: {sm: offsets[base] + off
+                              for sm, (base, off) in d.items() if base != 0}
+                          for t, d in terminals.items()}
 
 
 class SyncGroup(SyncGroupBase):
@@ -453,9 +509,7 @@ class SyncGroup(SyncGroupBase):
         self.packet_index = SyncGroup.packet_index
         SyncGroup.packet_index += 1
         self.asm_packet = self.packet.assemble(self.packet_index)
-        ret = ensure_future(self.run())
-        ensure_future(self.to_operational())
-        return ret
+        return ensure_future(self.run())
 
 
 class FastSyncGroup(SyncGroupBase, XDP):
@@ -493,7 +547,6 @@ class FastSyncGroup(SyncGroupBase, XDP):
     def start(self):
         self.allocate()
         self.task = ensure_future(self.run())
-        ensure_future(self.to_operational())
         return self.task
 
     def cancel(self):
diff --git a/ebpfcat/ethercat.py b/ebpfcat/ethercat.py
index 885473d..f82e9f4 100644
--- a/ebpfcat/ethercat.py
+++ b/ebpfcat/ethercat.py
@@ -26,7 +26,10 @@ Low-level access to EtherCAT
 
 this modules contains the code to actually talk to EtherCAT terminals.
 """
-from asyncio import ensure_future, Event, Future, gather, get_event_loop, Protocol, Queue, Lock
+from asyncio import (
+    ensure_future, Event, Future, gather, get_event_loop, Protocol, Queue,
+    Lock)
+from contextlib import asynccontextmanager
 from enum import Enum
 from itertools import count
 from random import randint
@@ -411,6 +414,8 @@ class Terminal:
         await self.set_state(0x11)
         await self.set_state(1)
 
+        self.fmmu_used = [None] * (await self.read(4, "B"))[0]
+
         self.mbx_cnt = 1
         self.mbx_lock = Lock()
 
@@ -609,7 +614,7 @@ class Terminal:
         return data[:4] + data2
 
     async def eeprom_write_one(self, start, data):
-        """read 2 bytes from the eeprom at `start`"""
+        """write 2 bytes to the eeprom at `start`"""
         while (await self.read(0x502, "H"))[0] & 0x8000:
             pass
         busy = 0x1000
@@ -837,3 +842,30 @@ class Terminal:
                 oe.name = data[8:].decode("utf8")
                 od.entries[i] = oe
         return ret
+
+    @asynccontextmanager
+    async def map_fmmu(self, logical, write):
+        """map the pdo to `logical` address.
+
+        :param write: a boolean indicating whether this is to be used
+            for writing (instead of reading).
+        """
+        if write:
+            offset = self.pdo_out_off
+            size = self.pdo_out_sz
+            start = 1
+        else:
+            offset = self.pdo_in_off
+            size = self.pdo_in_sz
+            start = len(self.fmmu_used)
+
+        index = start - self.fmmu_used[start::-1].index(None) - 1
+
+        self.fmmu_used[index] = logical
+        try:
+            await self.write(0x600 + 0x10 * index, "IHBBHBBB3x", logical, size,
+                             0, 7, offset, 0, 2 if write else 1, 1)
+            yield index
+            await self.write(0x60c + 0x10 * index, "B", 0)
+        finally:
+            self.fmmu_used[index] = None
-- 
GitLab