pysoem icon indicating copy to clipboard operation
pysoem copied to clipboard

Read/Write SDOs

Open marcAC opened this issue 4 years ago • 7 comments

Hey,

I managed to got the read_sdo_info example to run and get a "table" with entries like the following, which is correct:

Idx: 0x6005; Code: 9; Type: 42; BitSize: 40; Access: 0x0; Name: "Actual Setpoint" Subindex 0; Type: 5; BitSize: 8; Access: 0x7 Name: "SubIndex 000" Subindex 1; Type: 8; BitSize: 32; Access: 0xbf Name: "Actual Setpoint"

But now the question is how do I read and write and specific subindex?

import sys
import struct
import time
import threading

from collections import namedtuple

import pysoem

class BasicExample:

    def __init__(self, ifname):
        self._ifname = ifname
        self._pd_thread_stop_event = threading.Event()
        self._ch_thread_stop_event = threading.Event()
        self._actual_wkc = 0
        self._master = pysoem.Master()
        self._master.in_op = False
        self._master.do_check_state = False

    def _processdata_thread(self):
        while not self._pd_thread_stop_event.is_set():
            self._master.send_processdata()
            self._actual_wkc = self._master.receive_processdata(10000)
            if not self._actual_wkc == self._master.expected_wkc:
                print('incorrect wkc')
            time.sleep(0.01)

    def _pdo_update_loop(self):

        self._master.in_op = True

        print("Everything in OP.")

        output_len = len(self._master.slaves[0].output)

        print(output_len)

        try:
            while 1:
                # write here
                time.sleep(1)
        except KeyboardInterrupt:
            # ctrl-C abort handling
            print('stopped')

    def run(self):

        self._master.open(self._ifname)

        if not self._master.config_init() > 0:
            self._master.close()
            raise BasicExampleError('no slave found')

        for i, slave in enumerate(self._master.slaves):
            slave.is_lost = False

        print(self._master.config_map())

        if self._master.state_check(pysoem.SAFEOP_STATE, 50000) != pysoem.SAFEOP_STATE:
            self._master.close()
            raise BasicExampleError('not all slaves reached SAFEOP state')

        self._master.state = pysoem.OP_STATE

        check_thread = threading.Thread(target=self._check_thread)
        check_thread.start()
        proc_thread = threading.Thread(target=self._processdata_thread)
        proc_thread.start()

        self._master.write_state()

        all_slaves_reached_op_state = False
        for i in range(40):
            self._master.state_check(pysoem.OP_STATE, 50000)
            if self._master.state == pysoem.OP_STATE:
                all_slaves_reached_op_state = True
                break

        if all_slaves_reached_op_state:
            self._pdo_update_loop()

        self._pd_thread_stop_event.set()
        self._ch_thread_stop_event.set()
        proc_thread.join()
        check_thread.join()
        self._master.state = pysoem.INIT_STATE
        # request INIT state for all slaves
        self._master.write_state()
        self._master.close()

        if not all_slaves_reached_op_state:
            raise BasicExampleError('not all slaves reached OP state')

    @staticmethod
    def _check_slave(slave, pos):
        if slave.state == (pysoem.SAFEOP_STATE + pysoem.STATE_ERROR):
            print(
                'ERROR : slave {} is in SAFE_OP + ERROR, attempting ack.'.format(pos))
            slave.state = pysoem.SAFEOP_STATE + pysoem.STATE_ACK
            slave.write_state()
        elif slave.state == pysoem.SAFEOP_STATE:
            print(
                'WARNING : slave {} is in SAFE_OP, try change to OPERATIONAL.'.format(pos))
            slave.state = pysoem.OP_STATE
            slave.write_state()
        elif slave.state > pysoem.NONE_STATE:
            if slave.reconfig():
                slave.is_lost = False
                print('MESSAGE : slave {} reconfigured'.format(pos))
        elif not slave.is_lost:
            slave.state_check(pysoem.OP_STATE)
            if slave.state == pysoem.NONE_STATE:
                slave.is_lost = True
                print('ERROR : slave {} lost'.format(pos))
        if slave.is_lost:
            if slave.state == pysoem.NONE_STATE:
                if slave.recover():
                    slave.is_lost = False
                    print(
                        'MESSAGE : slave {} recovered'.format(pos))
            else:
                slave.is_lost = False
                print('MESSAGE : slave {} found'.format(pos))

    def _check_thread(self):

        while not self._ch_thread_stop_event.is_set():
            if self._master.in_op and ((self._actual_wkc < self._master.expected_wkc) or self._master.do_check_state):
                self._master.do_check_state = False
                self._master.read_state()
                for i, slave in enumerate(self._master.slaves):
                    if slave.state != pysoem.OP_STATE:
                        self._master.do_check_state = True
                        BasicExample._check_slave(slave, i)
                if not self._master.do_check_state:
                    print('OK : all slaves resumed OPERATIONAL.')
            time.sleep(0.01)


class BasicExampleError(Exception):
    def __init__(self, message):
        super(BasicExampleError, self).__init__(message)
        self.message = message


if __name__ == '__main__':

    print('basic_example started')

    try:
        BasicExample("\\Device\\NPF_{2A30A4C2-1510-4DE3-A4A5-62EC83B2BBCC}").run()
    except BasicExampleError as expt:
        print('basic_example failed: ' + expt.message)
        sys.exit(1)

I managed to get into the _pdo_update_loop but now I don't really know how to read/write some SDOs or PDOs.

EDIT: I found how to read SDOs with the following commands:

                result = self._master.slaves[0].sdo_read(0x6005, 1, 4)
                print(result)

But the result I get is something like b'\x00\x00\xe0@' when it reads the number 7, is there any way to get the result as a decimal number?

Thanks in advance and kind regards Marc

marcAC avatar Aug 05 '20 13:08 marcAC

Hi @marcAC,

good to see that you could find a solution to your initial question.

What sdo_read read returns are the raw bytes of that object. Bytes you will also get for example, if you read from a binary file (doing something like with open('file.bin', 'rb') as f: + f.read()).

If there is an integer behind thees bytes, you could do:

print(int.from_bytes(result, byteorder='little'))

If it's a signed integer you must add signed=True, see here https://docs.python.org/3/library/stdtypes.html#int.from_bytes for details.

You could also use the struct module. But thats a bit more difficult to explain.

If your next step is writing objects, take a look at issue #16 there I elaborate on this a bit. You need the revers of int.from_bytes() there, witch is int.to_bytes().

bnjmnp avatar Aug 05 '20 19:08 bnjmnp

Hey @bnjmnp,

thanks for your reply. Then maybe there is some weird problem with our slave, because the result which should be 7, is b'\x00\x00\xe0@' and 1088421888 after converting it.

EDIT: I was trying to convert "float"-bytes to int, which obviously wasn't going to work, so I was able to manage to handle it with struct.unpack. Thanks for your help!

marcAC avatar Aug 12 '20 07:08 marcAC

Hey @bnjmnp,

writing SDOs is working, are there any examples on how to write PDOs?

Kind regards Marc

marcAC avatar Aug 13 '20 08:08 marcAC

Hi @marcAC,

good to see that you had success. Interesting that your device uses float type parameter.

For PDO, take a look at the _pdo_update_loop function in the basic_example.py. You need to know how the PDO is composed and what is behind those bytes. In the example there is just one bit toggled periodically, which toggles a digital output. The manual of your device should help you here. You need to dig in there a bit. It is not that easy to understand.

bnjmnp avatar Aug 14 '20 05:08 bnjmnp

Hey @bnjmnp,

I think it's because it's not really a standard device.

I already looked at this example but our problem is that we also mapped many values into one byte because there are also boolean-values. The C-structs we used for this looked like the following so as you can see there are 9 parameters in 2 bytes.

typedef struct PACKED {
    BOOLEAN(PulsingEnabled); /* Subindex1 - Pulsing Enabled */
    BOOLEAN(RampingInProgress); /* Subindex2 - Ramping in progress */
    BOOLEAN(ArcDetectionEnabled); /* Subindex3 - Arc Detection Enabled */
    BOOLEAN(ArcReactionEnabled); /* Subindex4 - Arc Reaction Enabled */
    BOOLEAN(CEXEnabled); /* Subindex5 - CEX Enabled */
    BOOLEAN(CEXLocked); /* Subindex6 - CEX Locked */
    BOOLEAN(FrequencyTuned); /* Subindex7 - Frequency Tuned */
    BOOLEAN(JouleModeEnergyLimitReached); /* Subindex8 - Joule Mode: Energy limit reached */
    BOOLEAN(ProcessTimerEndOfProcessTime); /* Subindex9 - Process Timer: End of process time */
    ALIGN7(SI10); /* Subindex10 */
} TOBJ6001;

Do you think there is a possibility to split up the bytearray more to map a struct-like structure like the one above to it?

Thanks for your help!

EDIT:

In case it is interesting for someone. We found a way to do it with ctypes Structures like this:

class TOBJ7004(ctypes.LittleEndianStructure):
    _fields_ = [
            ("RegulationControlMode", c_uint8, 8),
            ("SI2", c_uint8, 8),
        ]


class TOBJ7005(ctypes.LittleEndianStructure):
    _fields_ = [
            ("Setpoint", c_float)
        ]


class TOBJOUT(ctypes.LittleEndianStructure):
    _fields_ = [
            ("tobj7000", TOBJ7000),
            ("tobj7001", TOBJ7001),
            ("tobj7004", TOBJ7004),
            ("tobj7005", TOBJ7005)
        ]

The only trouble we have is with the c_float.

marcAC avatar Aug 18 '20 07:08 marcAC

Oh yes, you can also convert bytes to ctypes using ctypes.

Then you do something like:

tobj7004 = TOBJ7004(8, 8)
foo = bytes(tobj7004)

... to get the bytes of that struct instance.

Or:

tobj7004 = TOBJ7004.from_buffer_copy(bytes([0x55, 0xAA]))

... to convert a byte array to a struct.

I'm pretty sure you have a problem with alignment.

Add a _pack_ = 1 to your struct definition like this:

class TOBJOUT(ctypes.LittleEndianStructure):
    _pack_ = 1
    _fields_ = [
            ("tobj7004", TOBJ7004),
            ("tobj7005", TOBJ7005)
        ]

Then the c_float will be correct non-aligned.

bnjmnp avatar Aug 18 '20 19:08 bnjmnp

Awesome, that did the trick, thank you!

Is my guess correct that it will work the same for PDO read?

EDIT: Unfortunately it doesn't. In the minimal example you read the PDOs and convert the whole bytearray to int16 but in our case we have many different datatypes in the struct. The solution I found for this is to use ctypes.from_buffer_copy but if there is any more elegant solution you know feel free to share.

marcAC avatar Aug 19 '20 07:08 marcAC