Skip to content
Snippets Groups Projects
Commit 4d5ddc4e authored by Martin Teichmann's avatar Martin Teichmann
Browse files

add some documentation

parent cd579568
No related branches found
No related tags found
No related merge requests found
"""The high-level API for EtherCAT loops"""
from asyncio import ensure_future, gather, sleep from asyncio import ensure_future, gather, sleep
from struct import pack, unpack, calcsize, pack_into, unpack_from from struct import pack, unpack, calcsize, pack_into, unpack_from
from time import time from time import time
...@@ -89,6 +90,11 @@ class DeviceVar(ArrayGlobalVarDesc): ...@@ -89,6 +90,11 @@ class DeviceVar(ArrayGlobalVarDesc):
class Device(SubProgram): class Device(SubProgram):
"""A device is a functional unit in an EtherCAT loop
A device aggregates data coming in and going to terminals
to serve a common goal. A terminal may be used by several
devices. """
def get_terminals(self): def get_terminals(self):
ret = set() ret = set()
for pv in self.__dict__.values(): for pv in self.__dict__.values():
...@@ -194,6 +200,8 @@ class FastEtherCat(EtherCat): ...@@ -194,6 +200,8 @@ class FastEtherCat(EtherCat):
class SyncGroup: class SyncGroup:
"""A group of devices communicating at the same time"""
packet_index = 1000 packet_index = 1000
current_data = False # None is used to indicate FastSyncGroup current_data = False # None is used to indicate FastSyncGroup
...@@ -259,55 +267,3 @@ class FastSyncGroup(XDP): ...@@ -259,55 +267,3 @@ class FastSyncGroup(XDP):
self.ec.send_packet(self.packet.assemble(index)) self.ec.send_packet(self.packet.assemble(index))
self.monitor = ensure_future(gather(*[t.to_operational() self.monitor = ensure_future(gather(*[t.to_operational()
for t in self.terminals])) for t in self.terminals]))
def script():
fd = create_map(MapType.HASH, 4, 4, 7)
update_elem(fd, b"AAAA", b"BBBB", 0)
e = EBPF(ProgType.XDP, "GPL")
e.r1 = e.get_fd(fd)
e.r2 = e.r10
e.r2 += -8
e.m32[e.r10 - 8] = 0x41414141
e.call(1)
with e.If(e.r0 != 0):
e.r1 = e.get_fd(fd)
e.r2 = e.r10
e.r2 += -8
e.r3 = e.m32[e.r0]
e.r3 -= 1
e.m32[e.r10 - 16] = e.r3
e.r3 = e.r10
e.r3 += -16
e.r4 = 0
e.call(2)
e.r0 = 2 # XDP_PASS
e.exit()
return fd, e
async def logger(map_fd):
lasttime = time()
lastno = 0x42424242
while True:
r = lookup_elem(map_fd, b"AAAA", 4)
no, = unpack("i", r)
t = time()
print(f"L {no:7} {lastno-no:7} {t-lasttime:7.3f} {(lastno-no)/(t-lasttime):7.1f}")
lasttime = t
lastno = no
await sleep(0.1)
async def install_ebpf(network):
map_fd, e = script()
fd, disas = e.load(log_level=1)
print(disas)
prog_test_run(fd, 512, 512, 512, 512, repeat=10)
ensure_future(logger(map_fd))
await set_link_xdp_fd("eth0", fd)
return map_fd
if __name__ == "__main__":
from asyncio import get_event_loop
loop = get_event_loop()
loop.run_until_complete(install_ebpf("eth0"))
"""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 from asyncio import ensure_future, Event, Future, gather, get_event_loop, Protocol, Queue
from enum import Enum from enum import Enum
from random import randint from random import randint
...@@ -115,6 +119,12 @@ def datasize(args, data): ...@@ -115,6 +119,12 @@ def datasize(args, data):
class Packet: class Packet:
"""An EtherCAT packet representation
A packet contains one or more datagrams which are sent as EtherNet
packets. We implicitly add a datagram in the front which later serves
as an identifier for the packet.
"""
MAXSIZE = 1000 # maximum size we use for an EtherCAT packet MAXSIZE = 1000 # maximum size we use for an EtherCAT packet
ETHERNET_HEADER = 14 ETHERNET_HEADER = 14
DATAGRAM_HEADER = 10 DATAGRAM_HEADER = 10
...@@ -125,10 +135,27 @@ class Packet: ...@@ -125,10 +135,27 @@ class Packet:
self.size = 14 self.size = 14
def append(self, cmd, data, idx, *address): def append(self, cmd, data, idx, *address):
"""Append a datagram to the packet
:param cmd: EtherCAT command
:type cmd: ECCmd
:param data: the data in the datagram
:param idx: the datagram index, unchanged by terminals
Depending on the command, one or two more parameters represent the
address, either terminal and offset for position or node addressing,
or one value for logical addressing."""
self.data.append((cmd, data, idx) + address) self.data.append((cmd, data, idx) + address)
self.size += len(data) + self.DATAGRAM_HEADER + self.DATAGRAM_TAIL self.size += len(data) + self.DATAGRAM_HEADER + self.DATAGRAM_TAIL
def assemble(self, index): def assemble(self, index):
"""Assemble the datagrams into a packet
:param index: an identifier for the packet
An implicit empty datagram is added at the beginning of the packet
that may be used as an identifier for the packet.
"""
ret = [pack("<HBBiHHH", self.size | 0x1000, 0, 0, index, 1 << 15, 0, 0)] ret = [pack("<HBBiHHH", self.size | 0x1000, 0, 0, index, 1 << 15, 0, 0)]
for i, (cmd, data, *dgram) in enumerate(self.data, start=1): for i, (cmd, data, *dgram) in enumerate(self.data, start=1):
ret.append(pack("<BBhHHH" if len(dgram) == 3 else "<BBiHH", ret.append(pack("<BBhHHH" if len(dgram) == 3 else "<BBiHH",
...@@ -154,27 +181,37 @@ class Packet: ...@@ -154,27 +181,37 @@ class Packet:
return ''.join(f"{i}: {c} {f} {d}\n" for i, (c, d, f) in enumerate(ret)) return ''.join(f"{i}: {c} {f} {d}\n" for i, (c, d, f) in enumerate(ret))
def full(self): def full(self):
return self.size > self.MAXSIZE """Is the data limit reached?"""
return self.size > self.MAXSIZE or len(self.data) > 14
class AsyncBase: class EtherCat(Protocol):
async def __new__(cls, *args, **kwargs): """The EtherCAT connection
ret = super().__new__(cls)
await ret.__init__(*args, **kwargs)
return ret
An object of this class represents one connection to an EtherCAT loop.
It keeps the socket, and eventually all data flows through it.
class EtherCat(Protocol): This class supports both to send individual datagrams and wait for their
response, but also to send and receive entire packets. """
def __init__(self, network): def __init__(self, network):
"""
:param network: the name of the network adapter, like "eth0"
"""
self.addr = (network, 0x88A4, 0, 0, b"\xff\xff\xff\xff\xff\xff") self.addr = (network, 0x88A4, 0, 0, b"\xff\xff\xff\xff\xff\xff")
self.send_queue = Queue() self.send_queue = Queue()
self.wait_futures = {} self.wait_futures = {}
async def connect(self): async def connect(self):
"""connect to the EtherCAT loop"""
await get_event_loop().create_datagram_endpoint( await get_event_loop().create_datagram_endpoint(
lambda: self, family=AF_PACKET, proto=0xA488) lambda: self, family=AF_PACKET, proto=0xA488)
async def sendloop(self): async def sendloop(self):
"""the eternal datagram sending loop
This method runs while we are connected, takes the datagrams
to be sent from a queue, packs them in a packet and ships them
out. """
packet = Packet() packet = Packet()
dgrams = [] dgrams = []
while True: while True:
...@@ -190,6 +227,10 @@ class EtherCat(Protocol): ...@@ -190,6 +227,10 @@ class EtherCat(Protocol):
packet = Packet() packet = Packet()
async def roundtrip_packet(self, packet): async def roundtrip_packet(self, packet):
"""Send a packet and return the response
Send the `packet` to the loop and wait that it comes back,
and return that to the caller. """
index = randint(2000, 1000000000) index = randint(2000, 1000000000)
while index in self.wait_futures: while index in self.wait_futures:
index = randint(2000, 1000000000) index = randint(2000, 1000000000)
...@@ -197,6 +238,7 @@ class EtherCat(Protocol): ...@@ -197,6 +238,7 @@ class EtherCat(Protocol):
return await self.receive_index(index) return await self.receive_index(index)
async def receive_index(self, index): async def receive_index(self, index):
"""Wait for packet identified by `index`"""
future = Future() future = Future()
self.wait_futures[index] = future self.wait_futures[index] = future
try: try:
...@@ -205,9 +247,24 @@ class EtherCat(Protocol): ...@@ -205,9 +247,24 @@ class EtherCat(Protocol):
del self.wait_futures[index] del self.wait_futures[index]
def send_packet(self, packet): def send_packet(self, packet):
"""simply send the `packet`, fire-and-forget"""
self.transport.sendto(packet, self.addr) self.transport.sendto(packet, self.addr)
async def roundtrip(self, cmd, pos, offset, *args, data=None, idx=0): async def roundtrip(self, cmd, pos, offset, *args, data=None, idx=0):
"""Send a datagram and wait for its response
:param cmd: the EtherCAT command
:type cmd: ECCmd
:param pos: the positional address of the terminal
:param offset: the offset within the terminal
:param idx: the EtherCAT datagram index
:param data: the data to be sent, or and integer for the number of
zeros to be sent as placeholder
Any additional parameters will be interpreted as follows: every `str` is
interpreted as a format for a `struct.pack`, everything else is the data
for those format. Upon returning, the received data will be unpacked
accoding to the format strings. """
future = Future() future = Future()
fmt = "<" + "".join(arg for arg in args[:-1] if isinstance(arg, str)) fmt = "<" + "".join(arg for arg in args[:-1] if isinstance(arg, str))
out = pack(fmt, *[arg for arg in args if not isinstance(arg, str)]) out = pack(fmt, *[arg for arg in args if not isinstance(arg, str)])
...@@ -230,17 +287,31 @@ class EtherCat(Protocol): ...@@ -230,17 +287,31 @@ class EtherCat(Protocol):
return ret return ret
def connection_made(self, transport): def connection_made(self, transport):
"""start the send loop once the connection is made"""
transport.get_extra_info("socket").bind(self.addr) transport.get_extra_info("socket").bind(self.addr)
self.transport = transport self.transport = transport
ensure_future(self.sendloop()) ensure_future(self.sendloop())
def datagram_received(self, data, addr): def datagram_received(self, data, addr):
"""distribute received packets to the recipients"""
index, = unpack("<I", data[4:8]) index, = unpack("<I", data[4:8])
self.wait_futures[index].set_result(data) self.wait_futures[index].set_result(data)
class Terminal: class Terminal:
"""Represent one terminal ("slave") in the loop"""
async def initialize(self, relative, absolute): async def initialize(self, relative, absolute):
"""Initialize the terminal
this sets up the connection to the terminal we represent.
:param relative: the position of the terminal in the loop,
a negative number counted down from 0 for the first terminal
:param absolute: the number used to identify the terminal henceforth
This also reads the EEPROM and sets up the sync manager as defined
therein. It still leaves the terminal in the init state. """
await self.ec.roundtrip(ECCmd.APWR, relative, 0x10, "H", absolute) await self.ec.roundtrip(ECCmd.APWR, relative, 0x10, "H", absolute)
self.position = absolute self.position = absolute
...@@ -300,16 +371,23 @@ class Terminal: ...@@ -300,16 +371,23 @@ class Terminal:
parse_pdo(self.eeprom[51]) parse_pdo(self.eeprom[51])
async def set_state(self, state): async def set_state(self, state):
"""try to set the state, and return the new state"""
await self.ec.roundtrip(ECCmd.FPWR, self.position, 0x0120, "H", state) await self.ec.roundtrip(ECCmd.FPWR, self.position, 0x0120, "H", state)
ret, = await self.ec.roundtrip(ECCmd.FPRD, self.position, 0x0130, "H") ret, = await self.ec.roundtrip(ECCmd.FPRD, self.position, 0x0130, "H")
return ret return ret
async def get_state(self): async def get_state(self):
"""get the current state"""
ret, = await self.ec.roundtrip(ECCmd.FPRD, self.position, 0x0130, "H") ret, = await self.ec.roundtrip(ECCmd.FPRD, self.position, 0x0130, "H")
return ret return ret
async def to_operational(self): async def to_operational(self):
"""try to bring the terminal to operational state""" """try to bring the terminal to operational state
this tries to push the terminal through its state machine to the
operational state. Note that even if it reaches there, the terminal
will quickly return to pre-operational if no packets are sent to keep
it operational. """
order = [1, 2, 4, 8] order = [1, 2, 4, 8]
ret, error = await self.ec.roundtrip( ret, error = await self.ec.roundtrip(
ECCmd.FPRD, self.position, 0x0130, "H2xH") ECCmd.FPRD, self.position, 0x0130, "H2xH")
...@@ -326,23 +404,31 @@ class Terminal: ...@@ -326,23 +404,31 @@ class Terminal:
while s != state: while s != state:
s, error = await self.ec.roundtrip(ECCmd.FPRD, self.position, s, error = await self.ec.roundtrip(ECCmd.FPRD, self.position,
0x0130, "H2xH") 0x0130, "H2xH")
print('State', self.position, s, error)
if error != 0: if error != 0:
raise RuntimeError(f"AL register {error}") raise RuntimeError(f"AL register {error}")
async def get_error(self): async def get_error(self):
"""read the error register"""
return (await self.ec.roundtrip(ECCmd.FPRD, self.position, return (await self.ec.roundtrip(ECCmd.FPRD, self.position,
0x0134, "H"))[0] 0x0134, "H"))[0]
async def read(self, start, *args, **kwargs): async def read(self, start, *args, **kwargs):
"""read data from the terminal at offset `start`
see `EtherCat.roundtrip` for details on more parameters. """
return (await self.ec.roundtrip(ECCmd.FPRD, self.position, return (await self.ec.roundtrip(ECCmd.FPRD, self.position,
start, *args, **kwargs)) start, *args, **kwargs))
async def write(self, start, *args, **kwargs): async def write(self, start, *args, **kwargs):
"""write data from the terminal at offset `start`
see `EtherCat.roundtrip` for details on more parameters"""
return (await self.ec.roundtrip(ECCmd.FPWR, self.position, return (await self.ec.roundtrip(ECCmd.FPWR, self.position,
start, *args, **kwargs)) start, *args, **kwargs))
async def eeprom_read_one(self, start): async def eeprom_read_one(self, start):
"""read 8 bytes from the eeprom at start""" """read 8 bytes from the eeprom at `start`"""
while (await self.read(0x502, "H"))[0] & 0x8000: while (await self.read(0x502, "H"))[0] & 0x8000:
pass pass
await self.write(0x502, "HI", 0x100, start) await self.write(0x502, "HI", 0x100, start)
...@@ -373,6 +459,7 @@ class Terminal: ...@@ -373,6 +459,7 @@ class Terminal:
eeprom[hd] = await get_data(ws * 2) eeprom[hd] = await get_data(ws * 2)
async def mbx_send(self, type, *args, data=None, address=0, priority=0, channel=0): async def mbx_send(self, type, *args, data=None, address=0, priority=0, channel=0):
"""send data to the mailbox"""
status, = await self.read(0x805, "B") # always using mailbox 0, OK? status, = await self.read(0x805, "B") # always using mailbox 0, OK?
if status & 8: if status & 8:
raise RuntimeError("mailbox full, read first") raise RuntimeError("mailbox full, read first")
...@@ -385,6 +472,7 @@ class Terminal: ...@@ -385,6 +472,7 @@ class Terminal:
self.mbx_cnt = self.mbx_cnt % 7 + 1 # yes, we start at 1 not 0 self.mbx_cnt = self.mbx_cnt % 7 + 1 # yes, we start at 1 not 0
async def mbx_recv(self): async def mbx_recv(self):
"""receive data from the mailbox"""
status = 0 status = 0
while status & 8 == 0: while status & 8 == 0:
status, = await self.read(0x80D, "B") # always using mailbox 1, OK? status, = await self.read(0x80D, "B") # always using mailbox 1, OK?
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment