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 (4)
......@@ -28,7 +28,7 @@ from time import time
from .arraymap import ArrayMap, ArrayGlobalVarDesc
from .ethercat import (
ECCmd, EtherCat, MachineState, Packet, Terminal, EtherCatError,
SyncManager)
Struct, SyncManager)
from .ebpf import FuncId, MemoryDesc, SubProgram, prandom
from .xdp import XDP, XDPExitCode, PacketVar as XDPPacketVar
from .bpf import (
......@@ -37,6 +37,18 @@ from .bpf import (
class PacketDesc:
"""A single value in a process data
This describes some data in the process data coming from or sent to
a terminal. This is the low-level version of :class:`ProcessDesc`, which
can be used if the terminal's self-desciption is lacking.
:param sm: the sync manager, either :attr:`SyncManager.IN` or
:attr:`SyncManager.OUT`.
:param position: the byte position in the process data
:param size: either a :mod:`python:struct` definition of a data type,
or an integer denoting the bit within a byte to be adressed.
"""
def __init__(self, sm, position, size):
self.sm = sm
self.position = position
......@@ -146,39 +158,28 @@ class PacketVar(MemoryDesc):
self._start(device) + Packet.ETHERNET_HEADER)
class Struct:
"""Define repetitive structures in a PDO
class TerminalVar:
"""a device variable to be linked to a process variable
Some terminals, especially multi-channel terminals,
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
Whithin a :class:`Device`, one can refer to process variables that should
later be linked to process variables of a terminal. Within the device, one
can access the process variable generically. Upon instantiation one would
then assign a :class:`ProcessDesc` (or :class:`PacketDesc`) to it to link
the variable to an actual terminal.
def __new__(cls, *args):
return StructDesc(cls, *args)
For example::
class MyDevice(Device):
the_output = TerminalVar()
class StructDesc:
def __init__(self, struct, sm3=0, sm2=None):
self.struct = struct
if sm2 is None:
sm2 = sm3
self.position_offset = {SyncManager.OUT: sm2, SyncManager.IN: sm3}
def __get__(self, instance, owner):
if instance is None:
return self
ret = object.__new__(self.struct)
ret.position_offset = self.position_offset
ret.terminal = instance
return ret
def program(self):
self.the_output = 5 # write 5 to whatever variable linked
terminal = MyTerminal()
device = MyDevice()
device.the_output = terminal.output5 # link the_output to output5
"""
class TerminalVar:
def __set__(self, instance, value):
if isinstance(value, PacketVar):
instance.__dict__[self.name] = value
......@@ -204,6 +205,28 @@ class TerminalVar:
class DeviceVar(ArrayGlobalVarDesc):
"""A variable in a device for higher-level use
define a variable within a device which the device's user can
access. This is especially important for fast devices, this is the
way data is communicated to and from the EBPF program.
For non-fast devices, this acts like normal Python variables.
:param size: the size of a variable in :mod:`python:struct` letters
:param write: whether the variable will be written to by the user
For example::
class MyDevice(Device):
my_data = DeviceVar()
def program(self):
self.my_data = 7
device = MyDevice()
print(self.my_data) # should print 7 once the program is running
"""
def __init__(self, size="I", write=False):
super().__init__(FastSyncGroup.properties, size)
self.write = write
......@@ -319,6 +342,23 @@ class EBPFTerminal(Terminal):
class EtherXDP(XDP):
"""The EtherCat packet dispatcher
This class creates an EBPF program that receives EtherCAT packet
from the network and dispatches them to the sync group they belong
to, or passes them on to user space if they do not belong to them.
For each sync group, there are always two packets on the wire, one
that only reads value from the terminals, the other one also writes.
Usually only the read-write packet is handed over to the sync group's
program. If, however, that packet gets lost, the next read-only
packet is handed over.
User space is supposed to constantly feed in new packets, and the
then-superfluous packets are sent back to user space. This way user
space can constantly read data independent of the EBPF program. It
cannot write, however, as this would cause priority issues.
"""
license = "GPL"
minimumPacketSize = 30
......@@ -368,6 +408,7 @@ class SimpleEtherCat(EtherCat):
class FastEtherCat(SimpleEtherCat):
"""An EtherCAT driver class for fast and slow sync groups"""
MAX_PROGS = 64
def __init__(self, network):
......
......@@ -302,6 +302,7 @@ class EtherCat(Protocol):
"""
self.addr = (network, 0x88A4, 0, 0, b"\xff\xff\xff\xff\xff\xff")
self.wait_futures = {}
self.used_addresses = set()
async def connect(self):
"""connect to the EtherCAT loop"""
......@@ -429,14 +430,29 @@ class EtherCat(Protocol):
return no
async def find_free_address(self):
"""Find an absolute address currently not in use"""
"""Find an absolute address not in use
an address once returned by this method is assumed to be used in the
future and will never be handed out again"""
while True:
i = randint(1000, 30000)
if i in self.used_addresses:
continue
self.used_addresses.add(i)
try:
await self.roundtrip(ECCmd.FPRD, i, 0x10, "H", 0)
except EtherCatError:
return i # this address is not in use
async def assigned_address(self, position):
"""return the set adress of terminal at position, if none set one"""
ret, = await self.roundtrip(ECCmd.APRD, position, 0x10, "H", 0)
if ret != 0:
return ret
ret = await self.find_free_address()
await self.roundtrip(ECCmd.APWR, position, 0x10, "H", ret)
return ret
async def eeprom_read(self, position, start):
"""read 4 bytes from the eeprom of terminal `position` at `start`"""
while (await self.roundtrip(ECCmd.APRD, position,
......@@ -461,23 +477,77 @@ class EtherCat(Protocol):
self.wait_futures[index].set_result(data)
class ServiceDesc:
def __init__(self, index, subidx):
self.index = index
self.subidx = subidx
class Struct:
"""Define repetitive structures in CoE objects
Some terminals, especially multi-channel terminals,
have repetitive structures in their CoE. 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
def __new__(cls, *args):
return StructDesc(cls, *args)
class StructDesc:
def __init__(self, struct, sm3=0, sm2=None):
self.struct = struct
if sm2 is None:
sm2 = sm3
self.position_offset = {SyncManager.OUT: sm2, SyncManager.IN: sm3}
def __get__(self, instance, owner):
if instance is None:
return self
if (ret := instance.__dict__.get(self.name)) is not None:
return ret
ret = object.__new__(self.struct)
ret.position_offset = self.position_offset
ret.terminal = instance
instance.__dict__[self.name] = ret
return ret
def __set_name__(self, owner, name):
self.name = name
class Terminal:
"""Represent one terminal (*SubDevice* or *slave*) in the loop"""
def __init__(self, ethercat):
self.ec = ethercat
async def initialize(self, relative, absolute):
async def initialize(self, relative=None, absolute=None):
"""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
If None, we assume the address is already initialized
:param absolute: the number used to identify the terminal henceforth
If None take a free one
If only one parameter is given, it is taken to be an absolute
position, the terminal address is supposed to be already initialized.
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)
therein. It still leaves the terminal in the init state.
"""
assert relative is not None or absolute is not None
if absolute is None:
absolute = await self.ec.find_free_address()
if relative is not None:
await self.ec.roundtrip(ECCmd.APWR, relative, 0x10, "H", absolute)
self.position = absolute
await self.set_state(0x11)
......@@ -599,6 +669,22 @@ class Terminal:
await parse(parse_eeprom(self.eeprom[50]), SyncManager.IN)
if 50 in self.eeprom else 0)
async def parse_sdos(self):
sdos = {}
for cls in self.__class__.__mro__:
for k, v in cls.__dict__.items():
if isinstance(v, ServiceDesc):
setattr(self, k,
await self.read_object_entry(v.index, v.subidx))
elif isinstance(v, StructDesc):
struct = getattr(self, k)
offset = struct.position_offset[SyncManager.IN]
for kk, vv in struct.__class__.__dict__.items():
if isinstance(vv, ServiceDesc):
setattr(struct, kk,
await self.read_object_entry(
vv.index + offset, vv.subidx))
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)
......@@ -880,8 +966,11 @@ class Terminal:
oe.valueInfo, dataType, oe.bitLength, oe.objectAccess = \
unpack_from("<BxHHH", data)
assert subidx == oe.valueInfo
oe.dataTypeOriginal = dataType
if dataType < 2048:
oe.dataType = ECDataType(dataType)
elif oe.bitLength == 8:
oe.dataType = ECDataType.UNSIGNED8
else:
oe.dataType = dataType
oe.name = data[8:].decode("utf8")
......
......@@ -48,12 +48,11 @@ async def info():
terms = [Terminal(ec) for t in terminals]
for t in terms:
t.ec = ec
await asyncio.gather(*(t.initialize(-i, i + 7)
await asyncio.gather(*(t.initialize(-i)
for i, t in zip(terminals, terms)))
else:
free = await ec.find_free_address()
term = Terminal(ec)
await term.initialize(-args.terminal, free)
await term.initialize(-args.terminal)
terms = [term]
for i, t in enumerate(terms, args.terminal if args.terminal else 0):
......@@ -120,20 +119,9 @@ async def eeprom():
if args.terminal is None:
return
terminals = range(await ec.count())
else:
# former terminal: don't listen!
# this does not work with all terminals, dunno why
try:
await ec.roundtrip(ECCmd.FPRW, 7, 0x10, "H", 0)
except EtherCatError:
print('fine: no not used yet')
else:
print('had to silence former listener')
terminals = [args.terminal]
t = Terminal(ec)
await t.initialize(-args.terminal, 7)
await t.initialize(-args.terminal)
if args.read or args.check is not None:
r, = unpack("<4xI", await t._eeprom_read_one(0xc))
......@@ -165,7 +153,7 @@ async def create_test():
for i in range(no):
t = Terminal()
t.ec = ec
await t.initialize(-i, await ec.find_free_address())
await t.initialize(-i)
sdo = {}
if t.has_mailbox():
await t.to_operational(MachineState.PRE_OPERATIONAL)
......
......@@ -15,7 +15,8 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from .ebpfcat import EBPFTerminal, PacketDesc, ProcessDesc, Struct
from .ethercat import ServiceDesc, Struct
from .ebpfcat import EBPFTerminal, PacketDesc, ProcessDesc
class Generic(EBPFTerminal):
......@@ -136,10 +137,16 @@ class EL5042(EBPFTerminal):
status = ProcessDesc(0x6000, 1, "H")
invalid = ProcessDesc(0x6000, 0xE)
statusbits = ServiceDesc(0x8008, 2)
crc_invert = ServiceDesc(0x8008, 3)
multiturn = ServiceDesc(0x8008, 0x15)
singleturn = ServiceDesc(0x8008, 0x16)
frequency = ServiceDesc(0x8008, 0x13)
polynomial = ServiceDesc(0x8008, 0x11)
channel1 = Channel(0)
channel2 = Channel(0x10)
class EL6022(EBPFTerminal):
class Channel(Struct):
transmit_accept = PacketDesc(3, 0, 0)
......@@ -172,6 +179,13 @@ class EL7041(EBPFTerminal):
low_switch = ProcessDesc(0x6010, 0xd)
stepcounter = ProcessDesc(0x6010, 0x14)
max_current = ServiceDesc(0x8010, 1)
max_voltage = ServiceDesc(0x8010, 3)
coil_resistance = ServiceDesc(0x8010, 4)
motor_emf = ServiceDesc(0x8010, 5)
invLogicLim1 = ServiceDesc(0x8012, 0x30)
invLogicLim2 = ServiceDesc(0x8012, 0x31)
class EL7332(EBPFTerminal):
compatibility = {(2, 0x1CA43052)}
......