Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • teichman/ebpfcat
1 result
Show changes
Commits on Source (8)
......@@ -14,7 +14,7 @@ author = 'Martin Teichmann'
release = "0.1"
version = "0.1.0"
language = None
language = "en"
exclude_patterns = ['_build']
pygments_style = 'sphinx'
todo_include_todos = False
......
......@@ -27,7 +27,8 @@ from struct import pack, unpack, calcsize, pack_into, unpack_from
from time import time
from .arraymap import ArrayMap, ArrayGlobalVarDesc
from .ethercat import (
ECCmd, EtherCat, Packet, Terminal, EtherCatError, SyncManager)
ECCmd, EtherCat, MachineState, Packet, Terminal, EtherCatError,
SyncManager)
from .ebpf import FuncId, MemoryDesc, SubProgram, prandom
from .xdp import XDP, XDPExitCode, PacketVar as XDPPacketVar
from .bpf import (
......@@ -68,8 +69,8 @@ class ProcessDesc:
in the terminal's documentation
:param subindex: the subindex, also found in the documentation
:param size: usually the size is taken from the PDO mapping. A
different size as in a `struct` definition may be given here,
or the number of a bit for a bit field.
different size as in a :mod:`python:struct` definition may be
given here, or the number of a bit for a bit field.
"""
def __init__(self, index, subindex, size=None):
self.index = index
......@@ -149,8 +150,11 @@ class Struct:
"""Define repetitive structures in a PDO
Some terminals, especially multi-channel terminals,
have repetitive structures in their PDO. Use this to group
them.
have repetitive structures in their PDO. Inherit from this
class to create a structure for them. Each instance
will then define one channel. It takes one parameter, which
is the offset in the CoE address space from the template
structure to the one of the channel.
"""
device = None
......@@ -240,6 +244,12 @@ class Device(SubProgram):
class EBPFTerminal(Terminal):
"""This is the base class for all supported terminal types
inheriting classes should define a ``compatibility`` class variable
which is a set of tuples, each of which is a pair of Ethercat vendor and
product id of all supported terminal types.
"""
compatibility = None
position_offset = {SyncManager.OUT: 0, SyncManager.IN: 0}
use_fmmu = True
......@@ -250,7 +260,7 @@ class EBPFTerminal(Terminal):
(self.vendorId, self.productCode) not in self.compatibility):
raise EtherCatError(
f"Incompatible Terminal: {self.vendorId}:{self.productCode}")
await self.to_operational(2)
await self.to_operational(MachineState.PRE_OPERATIONAL)
self.pdos = {}
outbits, inbits = await self.parse_pdos()
self.pdo_out_sz = int((outbits + 7) // 8)
......@@ -533,16 +543,22 @@ class SyncGroup(SyncGroupBase):
return self.current_data
async def to_operational(self):
r = await gather(*[t.to_operational() for t in self.terminals])
while True:
for t in self.terminals:
state, error = await t.get_state()
if state != 8: # operational
logging.warning(
"terminal is not operational, state is %i", error)
await t.to_operational()
await sleep(1)
try:
await gather(*[t.to_operational() for t in self.terminals])
while True:
r = await gather(*[t.to_operational() for t in self.terminals])
for t, (state, error, status) in zip(self.terminals, r):
if state is not MachineState.OPERATIONAL:
logging.warning(
"terminal %s was not operational, status was %i",
t, status)
await sleep(1)
except CancelledError:
raise
except Exception:
logging.exception('to_operational failed')
raise
def start(self):
self.allocate()
......
......@@ -27,8 +27,8 @@ 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)
CancelledError, ensure_future, Event, Future, gather, get_event_loop,
Protocol, Queue, Lock)
from contextlib import asynccontextmanager
from enum import Enum, IntEnum
from itertools import count
......@@ -135,6 +135,19 @@ class EEPROM(IntEnum):
REVISION = 12
SERIAL_NO = 14
class MachineState(Enum):
"""The states of the EtherCAT state machine
The states are in the order in which they should
be taken, BOOTSTRAP is at the end as this is a
state we usually do not go to.
"""
INIT = 1
PRE_OPERATIONAL = 2
SAFE_OPERATIONAL = 4
OPERATIONAL = 8
BOOTSTRAP = 3
class SyncManager(Enum):
OUT = 2
IN = 3
......@@ -292,26 +305,41 @@ class EtherCat(Protocol):
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()
dgrams = []
while True:
*dgram, future = await self.send_queue.get()
lastsize = packet.size
packet.append(*dgram)
dgrams.append((lastsize + 10, packet.size - 2, future))
if packet.full() or self.send_queue.empty():
data = await self.roundtrip_packet(packet)
for start, stop, future in dgrams:
wkc, = unpack("<H", data[stop:stop+2])
if wkc == 0:
future.set_exception(
EtherCatError("datagram was not processed"))
elif not future.done():
future.set_result(data[start:stop])
else:
logging.info("future already done, dropped datagram")
dgrams = []
packet = Packet()
try:
packet = Packet()
dgrams = []
while True:
*dgram, future = await self.send_queue.get()
lastsize = packet.size
packet.append(*dgram)
dgrams.append((lastsize + 10, packet.size - 2, future))
if packet.full() or self.send_queue.empty():
ensure_future(self.process_packet(dgrams, packet))
dgrams = []
packet = Packet()
except CancelledError:
raise
except Exception:
logging.exception("sendloop failed")
raise
async def process_packet(self, dgrams, packet):
try:
data = await self.roundtrip_packet(packet)
for start, stop, future in dgrams:
wkc, = unpack("<H", data[stop:stop+2])
if wkc == 0:
future.set_exception(
EtherCatError("datagram was not processed"))
elif not future.done():
future.set_result(data[start:stop])
else:
logging.info("future already done, dropped datagram")
except CancelledError:
raise
except Exception:
logging.exception("process_packet failed")
raise
async def roundtrip_packet(self, packet):
"""Send a packet and return the response
......@@ -417,7 +445,7 @@ class EtherCat(Protocol):
class Terminal:
"""Represent one terminal ("slave") in the loop"""
"""Represent one terminal (*SubDevice* or *slave*) in the loop"""
def __init__(self, ethercat):
self.ec = ethercat
......@@ -561,43 +589,35 @@ class Terminal:
return ret
async def get_state(self):
"""get the current state and error flags"""
state, error = await self.ec.roundtrip(ECCmd.FPRD, self.position,
0x0130, "H2xH")
return state, error
"""get the current state, error flag and status word"""
state, status = await self.ec.roundtrip(ECCmd.FPRD, self.position,
0x0130, "H2xH")
return MachineState(state & 0xf), bool(state & 0x10), status
async def to_operational(self, target=8):
async def to_operational(self, target=MachineState.OPERATIONAL):
"""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
target 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]
ret, error = await self.ec.roundtrip(
ECCmd.FPRD, self.position, 0x0130, "H2xH")
if ret & 0x10:
it operational.
return the state, error flag and status before the operation."""
order = list(MachineState)
state, error, status = ret = await self.get_state()
if error:
await self.ec.roundtrip(ECCmd.FPWR, self.position,
0x0120, "H", 0x11)
ret, error = await self.ec.roundtrip(ECCmd.FPRD, self.position,
0x0130, "H2xH")
pos = order.index(ret)
s = 0x11
for state in order[pos+1:]:
state = MachineState.INIT
for current in order[order.index(state) + 1:]:
if state.value >= target.value:
return ret
await self.ec.roundtrip(ECCmd.FPWR, self.position,
0x0120, "H", state)
while s != state:
s, error = await self.ec.roundtrip(ECCmd.FPRD, self.position,
0x0130, "H2xH")
if error != 0:
raise EtherCatError(f"AL register {error}")
if state >= target:
return
async def get_error(self):
"""read the error register"""
return (await self.ec.roundtrip(ECCmd.FPRD, self.position,
0x0134, "H"))[0]
0x0120, "H", current.value)
while current is not state:
state, error, status = await self.get_state()
if error:
raise EtherCatError(f"AL register error {status}")
async def read(self, start, *args, **kwargs):
"""read data from the terminal at offset `start`
......@@ -713,6 +733,11 @@ class Terminal:
return b"".join(ret)
async def sdo_read(self, index, subindex=None):
"""read a single SDO entry
given an adress for a CoE entry like 6020:12, you may read
the value like ``await master.sdo_read(0x6020, 0x12)``.
"""
async with self.mbx_lock:
await self.mbx_send(
MBXType.COE, "HBHB4x", CoECmd.SDOREQ.value << 12,
......@@ -765,6 +790,13 @@ class Terminal:
return b"".join(ret)
async def sdo_write(self, data, index, subindex=None):
"""write a single SDO entry
given a CoE address like 1200:2, one may write the value as
in ``await master.sdo_write(b'abc', 0x1200, 0x2)``. Note that the
data needs to already be a binary string matching the binary type of
the parameter.
"""
if len(data) <= 4 and subindex is not None:
async with self.mbx_lock:
await self.mbx_send(
......
......@@ -4,31 +4,36 @@ The EtherCAT master
Getting started
---------------
Ethercat terminals are usually connected in a loop with the EtherCAT master.
The EtherCAT master has to know the order and function of these terminals.
The list of terminals then has to be given in correct order to the constructor
of the EtherCAT master object as follows::
Ethercat terminals are usually connected in a loop with the EtherCAT master,
via an ethernet interface. So we create a master object, and connect to that
interface an scan the loop. This takes time, so in a good asyncronous fashion
we need to use await, which can only be done in an async function::
from ebpfcat.ebpfcat import FastEtherCat
from ebpfcat.terminals import EL4104, Generic
out = EL4104()
unknown = Generic() # use "Generic" for terminals of unknown type
# later, in an async function:
master = FastEtherCat("eth0")
await master.connect()
await master.scan_bus()
master = FastEtherCat("eth0", [out, unknown])
Next we create an object for each terminal that we want to use. As an example,
take some Beckhoff output terminal::
Once we have defined the order of devices, we can connect to the loop and
scan it to actually find all terminals. This takes time, so in a good
asyncronous fashion we need to use await, which can only be done in an
async function::
from ebpfcat.terminals import EL4104, Generic
await master.connect()
await master.scan_bus()
out = EL4104(master)
The terminals usually control some devices, where one terminal may control
several devices, or one device is controlled by several terminals. The devices
are represented by `Device` objects. Upon instantiation, they are connected to
the terminals::
This terminal needs to be initialized. The initialization method takes two
arguments, the relative position in the loop, starting with 0 for the terminal
directly connected to the interface, counting downwards to negative values. The
second argument is the absolute address this terminal should be assigned to::
await out.initialize(-1, 20) # assign address 20 to the first terminal
The terminals are usually controlled by devices, where one terminal may be
controlled by several devices, or one device controls several terminals. The
devices are represented by `Device` objects. Upon instantiation, they are
connected to the terminals::
from ebpfcat.devices import AnalogOutput
......@@ -73,7 +78,7 @@ Before they can be used, their `TerminalVar`\ s need to be initialized::
motor.position = encoderTerminal.value
whenever new data is read from the loop, the `update` method of the device is
called, in which one can evaluate the `TerminalVar`\ s::
called, in which one can evaluate the `TerminalVar`\ s, or set them::
def update(self):
"""a idiotic speed controller"""
......@@ -84,7 +89,7 @@ Three methods of control
The communication with the terminals can happen in three different ways:
- out-of-order: the communication happens ad-hoc whenever needed. This is
- asynchronous: the communication happens ad-hoc whenever needed. This is
done during initialization and for reading and writing configuration data,
like CoE.
- slow: the data is sent, received and processed via Python. This is good
......@@ -93,6 +98,74 @@ The communication with the terminals can happen in three different ways:
Kernel. Only very limited operations can be done, but the loop cycle
frequency exceeds 10 kHz.
Adding new terminals
--------------------
The elements of an EtherCat loop were used to be called *slaves*, but nowadays
are referred to as *SubDevices*. As in a typical installation most of them are
simply terminals, we call them such.
Everything in a terminal is controlled by reading or writing parameters in the
CoE address space. These addresses are a pair of a 16 bit and an 8 bit number,
usually seperated by a colon, as in 6010:13. Most terminals allow these
parameters to be set asynchronously. Some of the parameters may be read or
written synchronously, so with every communication cycle.
The meaning of all these parameters can usually be found in the documentation of the terminal. Additionally, terminals often have a self-description, which can be read with the command line tool `ec-info`::
$ ec-info eth0 --terminal -1 --sdo
this reads the first (-1th) terminal's self description (``--sdo``). Add a
``--value`` to also get the current values of the parameters. This prints out
all known self descriptions of CoE parameters.
Once we know the meaning of parameters, they may be read or written
asynchronously using :meth:`~ebpfcat.ethercat.Terminal.sdo_read` and
:meth:`~ebpfcat.ethercat.Terminal.sdo_write`.
For synchronous data access, a class needs to be defined that defines the
parameters one want to use synchronously. The parameters available for
synchronous operations can be found with the ``--pdo`` parameter of the
``ec-info`` command. The class should inherit from
:class:`~ebpfcat.ebpfcat.EBPFTerminal` and define a set of tuples called
``comptibility``. The tuples should be the pairs of Ethercat product and vendor
id for all terminals supported by this class. Those can be found out with the
``--ids`` parameter of the ``ec-info`` command.
Within the class, the synchronous parameters are defined via
:class:`~ebpfcat.ebpfcat.ProcessDesc`. This descriptor takes the two parts of
the CoE address as parameters, plus an optional size parameter. This is usually
determined automatically, but this sometimes fails, in which case it may either
be defined via a format string like in the :mod:`python:struct` module, or it
is an integer which is then a reference to the position of the bit in the
parameter to define a boolean flag.
For terminals which have several equivalent channels, one can define a
structure by inheriting from :class:`~ebpfcat.ebpfcat.Struct`. Within this
class one defines the first set of parameters the same way one would do it
without. Once the class is defined, it can be instantiated in the terminal
class with a single argument which defines the offset in the CoE address space
for this structure. As an example, if on a two-channel terminal the first
channel has an address of ``0x6000:12`` and the following two ``0x6010:12`` and
``0x6020:12``, one would instantiate three structs with arguments ``0``,
``0x10`` and ``0x20``.
A complete example of a four channel terminal looks as follows::
class EL3164(EBPFTerminal):
compatibility = {(2, 0x0c5c3052)}
class Channel(Struct):
attrs = ProcessDesc(0x6000, 1, 'H') # this is 2 bytes ('H')
value = ProcessDesc(0x6000, 0x11)
factor = 10/32767 # add bonus information as desired
offset = 0
channel1 = Channel(0) # adress 0x6000
channel2 = Channel(0x10) # address 0x6010
channel3 = Channel(0x20)
channel4 = Channel(0x30)
Reference Documentation
-----------------------
......@@ -102,3 +175,6 @@ Reference Documentation
.. automodule:: ebpfcat.ethercat
:members:
.. automodule:: ebpfcat.ebpfcat
:members:
......@@ -25,7 +25,7 @@ from unittest import TestCase, main, skip
from .devices import AnalogInput, AnalogOutput, Motor
from .terminals import EL4104, EL3164, EK1814, Skip
from .ethercat import ECCmd, Terminal
from .ethercat import ECCmd, MachineState, Terminal
from .ebpfcat import (
FastSyncGroup, SyncGroup, TerminalVar, Device, EBPFTerminal, PacketDesc,
SterilePacket)
......@@ -82,11 +82,14 @@ class MockTerminal(Terminal):
await self.apply_eeprom()
async def to_operational(self, state=8):
async def to_operational(self, state=MachineState.OPERATIONAL):
assert isinstance(state, MachineState)
before = self.operational
self.operational = state
return state, 0, before
async def sdo_read(self, index, subindex=None):
assert self.operational >= 2
assert self.operational.value >= 2
if subindex is None:
r = b''
for i in count(1):
......@@ -159,7 +162,6 @@ class Tests(TestCase):
"04000200801110000000000000000000000000000000000000000000"
"3333"), # padding
0x66554433, # index
(ECCmd.FPRD, 2, 304, 'H2xH'), # get_state
H("2a10" # EtherCAT Header, length & type
"0000334455660280000000000000" # ID datagram
# in datagram
......@@ -173,7 +175,6 @@ class Tests(TestCase):
# in datagram
"04000400801110000000123456780000000000000000000000000100"
"3333"), # padding
(8, 0), # return state 8, no error
]
with self.assertNoLogs():
await self.new_data()
......@@ -214,7 +215,6 @@ class Tests(TestCase):
"0500030000110800000000000000000000000000" # out datagram
"33333333333333333333"), # padding
0x55443322, # index
(ECCmd.FPRD, 3, 304, 'H2xH'), # get_state
H("2210" # EtherCAT Header, length & type
"0000223344550280000000000000" # ID datagram
"0500030000110800000076980000000000000000" # out datagram
......@@ -231,7 +231,6 @@ class Tests(TestCase):
"0000223344550280000000000000" # ID datagram
"0500030000110800000000000000000000000100" # out datagram
"33333333333333333333"), # padding
(8, 0), # return state 8, no error
H("2210" # EtherCAT Header, length & type
"0000223344550280000000000000" # ID datagram
"0500030000110800000000000000000000000100" # out datagram
......
......@@ -6,7 +6,7 @@ from pprint import PrettyPrinter
from struct import unpack
import sys
from .ethercat import EtherCat, Terminal, ECCmd, EtherCatError
from .ethercat import EtherCat, MachineState, Terminal, ECCmd, EtherCatError
def entrypoint(func):
@wraps(func)
......@@ -73,7 +73,7 @@ async def info():
print(f"{k:2}: {v}\n {v.hex()}")
if args.sdo:
await t.to_operational(2)
await t.to_operational(MachineState.PRE_OPERATIONAL)
ret = await t.read_ODlist()
for k, v in ret.items():
print(f"{k:X}:")
......@@ -91,7 +91,7 @@ async def info():
print(f" {r}")
print(f" {r!r}")
if args.pdo:
await t.to_operational(2)
await t.to_operational(MachineState.PRE_OPERATIONAL)
await t.parse_pdos()
for (idx, subidx), (sm, pos, fmt) in t.pdos.items():
print(f"{idx:4X}:{subidx:02X} {sm} {pos} {fmt}")
......@@ -136,7 +136,7 @@ async def eeprom():
await t.initialize(-args.terminal, 7)
if args.read or args.check is not None:
r, = unpack("<4xI", await t.eeprom_read_one(0xc))
r, = unpack("<4xI", await t._eeprom_read_one(0xc))
if args.check is not None:
c = encode(args.check)
print(f"{r:8X} {c:8X} {r == c}")
......@@ -168,7 +168,7 @@ async def create_test():
await t.initialize(-i, await ec.find_free_address())
sdo = {}
if t.has_mailbox():
await t.to_operational(2)
await t.to_operational(MachineState.PRE_OPERATIONAL)
odlist = await t.read_ODlist()
for k, v in odlist.items():
......
......@@ -98,6 +98,8 @@ class EL4104(EBPFTerminal):
class EL3164(EBPFTerminal):
compatibility = {(2, 0x0c5c3052)}
class Channel(Struct):
attrs = ProcessDesc(0x6000, 1, 'H')
value = ProcessDesc(0x6000, 0x11)
......@@ -165,6 +167,22 @@ class EL7041(EBPFTerminal):
high_switch = ProcessDesc(0x6010, 0xd)
class EL7332(EBPFTerminal):
compatibility = {(2, 0x1CA43052)}
class Channel(Struct):
moving_positive = ProcessDesc(0x6020, 5)
moving_negative = ProcessDesc(0x6020, 6)
low_switch = ProcessDesc(0x6020, 0xc)
high_switch = ProcessDesc(0x6020, 0xd)
enable = ProcessDesc(0x7020, 1)
velocity = ProcessDesc(0x7020, 0x21, "h")
channel1 = Channel(0)
channel2 = Channel(0x10)
class TurboVac(EBPFTerminal):
compatibility = {(0x723, 0xb5)}
pump_on = ProcessDesc(0x20D3, 0, 0)
......
......@@ -7,5 +7,7 @@ XDP to achieve real-time response times. As a corollary, it contains a
Python based EBPF code generator.
.. toctree::
:maxdepth: 2
ebpfcat/ebpf.rst
ebpfcat/ethercat.rst