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 ...@@ -28,7 +28,7 @@ from time import time
from .arraymap import ArrayMap, ArrayGlobalVarDesc from .arraymap import ArrayMap, ArrayGlobalVarDesc
from .ethercat import ( from .ethercat import (
ECCmd, EtherCat, MachineState, Packet, Terminal, EtherCatError, ECCmd, EtherCat, MachineState, Packet, Terminal, EtherCatError,
SyncManager) Struct, SyncManager)
from .ebpf import FuncId, MemoryDesc, SubProgram, prandom from .ebpf import FuncId, MemoryDesc, SubProgram, prandom
from .xdp import XDP, XDPExitCode, PacketVar as XDPPacketVar from .xdp import XDP, XDPExitCode, PacketVar as XDPPacketVar
from .bpf import ( from .bpf import (
...@@ -37,6 +37,18 @@ from .bpf import ( ...@@ -37,6 +37,18 @@ from .bpf import (
class PacketDesc: 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): def __init__(self, sm, position, size):
self.sm = sm self.sm = sm
self.position = position self.position = position
...@@ -146,39 +158,28 @@ class PacketVar(MemoryDesc): ...@@ -146,39 +158,28 @@ class PacketVar(MemoryDesc):
self._start(device) + Packet.ETHERNET_HEADER) self._start(device) + Packet.ETHERNET_HEADER)
class Struct: class TerminalVar:
"""Define repetitive structures in a PDO """a device variable to be linked to a process variable
Some terminals, especially multi-channel terminals, Whithin a :class:`Device`, one can refer to process variables that should
have repetitive structures in their PDO. Inherit from this later be linked to process variables of a terminal. Within the device, one
class to create a structure for them. Each instance can access the process variable generically. Upon instantiation one would
will then define one channel. It takes one parameter, which then assign a :class:`ProcessDesc` (or :class:`PacketDesc`) to it to link
is the offset in the CoE address space from the template the variable to an actual terminal.
structure to the one of the channel.
"""
device = None
def __new__(cls, *args): For example::
return StructDesc(cls, *args)
class MyDevice(Device):
the_output = TerminalVar()
class StructDesc: def program(self):
def __init__(self, struct, sm3=0, sm2=None): self.the_output = 5 # write 5 to whatever variable linked
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
terminal = MyTerminal()
device = MyDevice()
device.the_output = terminal.output5 # link the_output to output5
"""
class TerminalVar:
def __set__(self, instance, value): def __set__(self, instance, value):
if isinstance(value, PacketVar): if isinstance(value, PacketVar):
instance.__dict__[self.name] = value instance.__dict__[self.name] = value
...@@ -204,6 +205,28 @@ class TerminalVar: ...@@ -204,6 +205,28 @@ class TerminalVar:
class DeviceVar(ArrayGlobalVarDesc): 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): def __init__(self, size="I", write=False):
super().__init__(FastSyncGroup.properties, size) super().__init__(FastSyncGroup.properties, size)
self.write = write self.write = write
...@@ -319,6 +342,23 @@ class EBPFTerminal(Terminal): ...@@ -319,6 +342,23 @@ class EBPFTerminal(Terminal):
class EtherXDP(XDP): 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" license = "GPL"
minimumPacketSize = 30 minimumPacketSize = 30
...@@ -368,6 +408,7 @@ class SimpleEtherCat(EtherCat): ...@@ -368,6 +408,7 @@ class SimpleEtherCat(EtherCat):
class FastEtherCat(SimpleEtherCat): class FastEtherCat(SimpleEtherCat):
"""An EtherCAT driver class for fast and slow sync groups"""
MAX_PROGS = 64 MAX_PROGS = 64
def __init__(self, network): def __init__(self, network):
......
...@@ -302,6 +302,7 @@ class EtherCat(Protocol): ...@@ -302,6 +302,7 @@ class EtherCat(Protocol):
""" """
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.wait_futures = {} self.wait_futures = {}
self.used_addresses = set()
async def connect(self): async def connect(self):
"""connect to the EtherCAT loop""" """connect to the EtherCAT loop"""
...@@ -429,14 +430,29 @@ class EtherCat(Protocol): ...@@ -429,14 +430,29 @@ class EtherCat(Protocol):
return no return no
async def find_free_address(self): 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: while True:
i = randint(1000, 30000) i = randint(1000, 30000)
if i in self.used_addresses:
continue
self.used_addresses.add(i)
try: try:
await self.roundtrip(ECCmd.FPRD, i, 0x10, "H", 0) await self.roundtrip(ECCmd.FPRD, i, 0x10, "H", 0)
except EtherCatError: except EtherCatError:
return i # this address is not in use 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): async def eeprom_read(self, position, start):
"""read 4 bytes from the eeprom of terminal `position` at `start`""" """read 4 bytes from the eeprom of terminal `position` at `start`"""
while (await self.roundtrip(ECCmd.APRD, position, while (await self.roundtrip(ECCmd.APRD, position,
...@@ -461,23 +477,77 @@ class EtherCat(Protocol): ...@@ -461,23 +477,77 @@ class EtherCat(Protocol):
self.wait_futures[index].set_result(data) 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: class Terminal:
"""Represent one terminal (*SubDevice* or *slave*) in the loop""" """Represent one terminal (*SubDevice* or *slave*) in the loop"""
def __init__(self, ethercat): def __init__(self, ethercat):
self.ec = ethercat self.ec = ethercat
async def initialize(self, relative, absolute): async def initialize(self, relative=None, absolute=None):
"""Initialize the terminal """Initialize the terminal
this sets up the connection to the terminal we represent. this sets up the connection to the terminal we represent.
:param relative: the position of the terminal in the loop, :param relative: the position of the terminal in the loop,
a negative number counted down from 0 for the first terminal 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 :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 This also reads the EEPROM and sets up the sync manager as defined
therein. It still leaves the terminal in the init state. """ therein. It still leaves the terminal in the init state.
await self.ec.roundtrip(ECCmd.APWR, relative, 0x10, "H", absolute) """
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 self.position = absolute
await self.set_state(0x11) await self.set_state(0x11)
...@@ -599,6 +669,22 @@ class Terminal: ...@@ -599,6 +669,22 @@ class Terminal:
await parse(parse_eeprom(self.eeprom[50]), SyncManager.IN) await parse(parse_eeprom(self.eeprom[50]), SyncManager.IN)
if 50 in self.eeprom else 0) 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): async def set_state(self, state):
"""try to set the state, and return the new 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)
...@@ -880,8 +966,11 @@ class Terminal: ...@@ -880,8 +966,11 @@ class Terminal:
oe.valueInfo, dataType, oe.bitLength, oe.objectAccess = \ oe.valueInfo, dataType, oe.bitLength, oe.objectAccess = \
unpack_from("<BxHHH", data) unpack_from("<BxHHH", data)
assert subidx == oe.valueInfo assert subidx == oe.valueInfo
oe.dataTypeOriginal = dataType
if dataType < 2048: if dataType < 2048:
oe.dataType = ECDataType(dataType) oe.dataType = ECDataType(dataType)
elif oe.bitLength == 8:
oe.dataType = ECDataType.UNSIGNED8
else: else:
oe.dataType = dataType oe.dataType = dataType
oe.name = data[8:].decode("utf8") oe.name = data[8:].decode("utf8")
......
...@@ -48,12 +48,11 @@ async def info(): ...@@ -48,12 +48,11 @@ async def info():
terms = [Terminal(ec) for t in terminals] terms = [Terminal(ec) for t in terminals]
for t in terms: for t in terms:
t.ec = ec t.ec = ec
await asyncio.gather(*(t.initialize(-i, i + 7) await asyncio.gather(*(t.initialize(-i)
for i, t in zip(terminals, terms))) for i, t in zip(terminals, terms)))
else: else:
free = await ec.find_free_address()
term = Terminal(ec) term = Terminal(ec)
await term.initialize(-args.terminal, free) await term.initialize(-args.terminal)
terms = [term] terms = [term]
for i, t in enumerate(terms, args.terminal if args.terminal else 0): for i, t in enumerate(terms, args.terminal if args.terminal else 0):
...@@ -120,20 +119,9 @@ async def eeprom(): ...@@ -120,20 +119,9 @@ async def eeprom():
if args.terminal is None: if args.terminal is None:
return 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) t = Terminal(ec)
await t.initialize(-args.terminal, 7) await t.initialize(-args.terminal)
if args.read or args.check is not None: 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))
...@@ -165,7 +153,7 @@ async def create_test(): ...@@ -165,7 +153,7 @@ async def create_test():
for i in range(no): for i in range(no):
t = Terminal() t = Terminal()
t.ec = ec t.ec = ec
await t.initialize(-i, await ec.find_free_address()) await t.initialize(-i)
sdo = {} sdo = {}
if t.has_mailbox(): if t.has_mailbox():
await t.to_operational(MachineState.PRE_OPERATIONAL) await t.to_operational(MachineState.PRE_OPERATIONAL)
......
...@@ -15,7 +15,8 @@ ...@@ -15,7 +15,8 @@
# with this program; if not, write to the Free Software Foundation, Inc., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 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): class Generic(EBPFTerminal):
...@@ -136,10 +137,16 @@ class EL5042(EBPFTerminal): ...@@ -136,10 +137,16 @@ class EL5042(EBPFTerminal):
status = ProcessDesc(0x6000, 1, "H") status = ProcessDesc(0x6000, 1, "H")
invalid = ProcessDesc(0x6000, 0xE) 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) channel1 = Channel(0)
channel2 = Channel(0x10) channel2 = Channel(0x10)
class EL6022(EBPFTerminal): class EL6022(EBPFTerminal):
class Channel(Struct): class Channel(Struct):
transmit_accept = PacketDesc(3, 0, 0) transmit_accept = PacketDesc(3, 0, 0)
...@@ -172,6 +179,13 @@ class EL7041(EBPFTerminal): ...@@ -172,6 +179,13 @@ class EL7041(EBPFTerminal):
low_switch = ProcessDesc(0x6010, 0xd) low_switch = ProcessDesc(0x6010, 0xd)
stepcounter = ProcessDesc(0x6010, 0x14) 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): class EL7332(EBPFTerminal):
compatibility = {(2, 0x1CA43052)} compatibility = {(2, 0x1CA43052)}
......