Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to pack / unpack single BITs in PDO input / output #141

Closed
RobertoRoos opened this issue Jun 10, 2024 · 3 comments
Closed

How to pack / unpack single BITs in PDO input / output #141

RobertoRoos opened this issue Jun 10, 2024 · 3 comments
Labels
help wanted Extra attention is needed

Comments

@RobertoRoos
Copy link
Contributor

RobertoRoos commented Jun 10, 2024

Today I have been struggling to control a stepper motor using a Beckhoff EL7031-0030 terminal.

The problem I'm facing now I don't know how to unpack the EtherCAT slave outputs and how to pack the slave inputs, so I'm not sure if I'm correctly interfacing with the device.

I'm sticking with the default PDO entries for now, which include many BIT types. And I can't find how those translate to the bytes in the input and output.

If somebody can point me to another full working example, in pysoem or SOEM itself, I'd also appreciated it.

The full code I have now:
from typing import Optional
from time import sleep, time
import pysoem
import ctypes
import struct


class PDOInput(ctypes.Structure):
    """Process data object from the slave to the master."""
    _fields_ = [
        ("states1", ctypes.c_uint8),
        ("states2", ctypes.c_uint8),
        ("counter_value", ctypes.c_uint16),
        ("latch_value", ctypes.c_uint16),
        ("states3", ctypes.c_uint8),
        ("states4", ctypes.c_uint8),
    ]


class PDOOutput(ctypes.Structure):
    """Process data object from the master to the slave."""
    _fields_ = [
        # ("dummy", ctypes.c_uint16),
        ("states1", ctypes.c_uint8),
        ("set_counter_value", ctypes.c_uint16),
        ("states2", ctypes.c_uint8),
    ]


class MotorNode:

    def __init__(self):
        self.adapter_name = "\\Device\\NPF_{2D594793-2E69-4C58-90F4-3164E860643B}"

        self._master: Optional[pysoem.Master] = pysoem.Master()
        self._slave_motor = None

        self.start_time = 0

        self._output_bytes = PDOOutput()

    def __del__(self):
        self.close()

    def run(self):
        try:
            self.setup()
            while True:
                self.loop()
        finally:
            self.close()

    def on_config_el7031(self, _slave_idx):
        # Default PDO would be velocity
        pass

    def setup(self):
        self._master = pysoem.Master()
        self._master.open(self.adapter_name)

        dev_count = self._master.config_init()
        if dev_count <= 0:
            raise RuntimeError("Could not find any EtherCAT devices")

        for slave in self._master.slaves:
            if "EL7031" in slave.name:
                self._slave_motor = slave

        if self._slave_motor:
            self._slave_motor.config_func = self.on_config_el7031

        # Move from PREOP to SAFEOP state - each slave's config_func is called:
        self._master.config_map()
        self._assert_state(pysoem.SAFEOP_STATE)

        self._master.state = pysoem.OP_STATE
        self._master.write_state()
        self._assert_state(pysoem.OP_STATE, 1_000_000)

        print("All devices in 'OP' state")

        # Trigger first update to initialize:
        self._master.send_processdata()
        self._master.receive_processdata()

        self.start_time = time()

    def loop(self):

        if time() - self.start_time > 1.0:
            self._output_bytes.states2 = 0b101  # <-- I would expect the drive to enable now, but it doesn't...

        self._slave_motor.output = bytes(self._output_bytes)

        self._master.send_processdata()
        self._master.receive_processdata()

        full_input = self._slave_motor.input

        pdo_in = PDOInput.from_buffer_copy(full_input)
        # ^ I'm not sure this object is a correct map of the bytes...

        stat = self._slave_motor.al_status
        desc = pysoem.al_status_code_to_string(stat)
        # print(f"{self._slave_motor.name} al status: {hex(stat)} ({desc})")

        sleep(0.01)

    def close(self):
        if self._master:
            self._master.close()
            self._master = None

    def _assert_state(self, state, timeout: Optional[int] = None):
        """Check master state and give verbose error if state is not met."""
        args = (state,)
        if timeout is not None:
            args += (timeout,)
        if self._master.state_check(*args) != state:
            self._master.read_state()
            msg = ""
            for slave in self._master.slaves:
                if slave.state != state:
                    msg += slave.name + " - " + hex(slave.state) + ","
                    desc = pysoem.al_status_code_to_string(slave.al_status)
                    print(f"{slave.name} al status: {hex(slave.al_status)} ({desc})")

            raise RuntimeError(f"Not all slaves reached state {hex(state)}: {msg}")


def main():
    node = MotorNode()
    try:
        node.run()
    except KeyboardInterrupt:
        pass  # Ignore


if __name__ == "__main__":
    main()

The slave inputs as displayed in TwinCAT:
image
image
image

@RobertoRoos
Copy link
Contributor Author

RobertoRoos commented Jun 21, 2024

I've pretty much figured this out now, mostly through lot's of trial-and-error.

So in a nutshell: the PDO input and output is packed in a way I can't deduce from the EL7041 documentation, but I can copy the information from the TwinCAT project.
When clicking on an entry, e.g. "Sync Error", there is the "address" field. For this entry the address "40.7". The .7 indicates bit number 7 is being used (the last bit, because counting starts at 0). The 40 indicates the byte number, although it starts at an offset. The lowest address value is 39, so I'm considering address 39 as byte 0, 40 as byte 1, etc. So we can follow this to determine the entire PDO entry.

ctypes.Structure bit fields are well suited to this. See https://docs.python.org/3/library/ctypes.html#bit-fields-in-structures-and-unions

A complete example can be found in my comment here: #135 (comment)

@bnjmnp
Copy link
Owner

bnjmnp commented Jun 30, 2024

Nice job, this is great example.
Can we close this issue?

@bnjmnp bnjmnp added the help wanted Extra attention is needed label Jun 30, 2024
@RobertoRoos
Copy link
Contributor Author

Yeah, we can close this. I'm assuming there is not a less tedious and manual approach to this that I don't know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants