ottowayi / pycomm3

A Python Ethernet/IP library for communicating with Allen-Bradley PLCs.
MIT License
407 stars 89 forks source link

forward_open failed - Service not supported [Micro800 Support] #20

Closed trevorstahl closed 4 years ago

trevorstahl commented 4 years ago

The functionality you have here looks really cool and I'd love to try it out on our next project, but I'm having some forward_open issues on both PLCs that I have tried with.

I am using a 1756-L72 in slot zero with a 1756-ENBT ethernet/IP card in slot 1 using the newly added CIP routing from 2020/03/10 as '10.101.123.4/backplane/0' but get back:

(venv) PS C:\Python\PyCommApp> & c:/Python/PyCommApp/venv/Scripts/python.exe c:/Python/PyCommApp/LogixApp.py forward_open failed - Service not supported - Extended status info not present Traceback (most recent call last): File "c:/Python/PyCommApp/LogixApp.py", line 3, in with LogixDriver('10.101.123.4/backplane/0') as plc: File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 100, in init self.get_plc_info() File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 55, in wrapped raise DataError(msg) pycomm3.DataError: Target did not connected. get_plc_info will not be executed.

I have python 3.8.2, using VSCode as the IDE with a virtual env. I also tried it IDLE IDE and without a venv, but keep getting the same error. Am I doing something wrong?

I was also trying it with a Micro820 with '10.101.123.x' getting a similar error, but assumed it may not be supported yet.

jjkuhniii commented 4 years ago

You should only need LogixDriver('10.101.123.4'). If your processor was in a slot other than slot 0 then you would use LogixDriver(10.101.123.4/x) where x is the slot number.

trevorstahl commented 4 years ago

You should only need LogixDriver('10.101.123.4'). If your processor was in a slot other than slot 0 then you would use LogixDriver(10.101.123.4/x) where x is the slot number.

I originally tried that first, but it appears that would only be if the processor itself has the ethernet port (such as the 1756-L83E/B which it appears he did the tests on). The 1756-L72 does not have its own ethernet port (as such does not have its own IP, and rather the IP is from the 1756-ENBT) so LogixDriver('10.101.123.4') doesn't work for this processor (I tried it first!), which is why I was hoping the CIP routing would work. I only have the 1756-L72 and Micro820 to try atm.

EDIT: I guess to clarify what I have is my working computer connected to a switch, which is connected to the 1756-ENBT Ethernet/IP (in slot 1) and the 1756-L72 Processor is in slot 0. So my computer communicates to 1756-ENBT which has the IP xxx.xxx.xxx.xxx, and then I am required to route through the backboard to the processor.

jjkuhniii commented 4 years ago

Yes, I have a very similar setup but with 2 processors in one rack. One processor is in slot 0 and the other is slot 7. For the processor in slot 0 I use the IP address assigned to the 1756-EN2T. If I want to access the the processor in the 7th slot I need to use the IP address of the 1756-EN2T with the addidtion of '/7'. I tried a print statement of each type of:

from pycomm3 import LogixDriver
with LogixDriver('10.31.0.11') as plc:
    print(plc)
    print(plc.info)

And

from pycomm3 import LogixDriver
with LogixDriver('10.31.0.11/7') as plc:
    print(plc)
    print(plc.info)

each accesses different processors. Could you provide a sample of what you are trying to do?

trevorstahl commented 4 years ago

I am able to test on my Micro820 right now, and am getting the same issue... My code is just:

from pycomm3 import LogixDriver with LogixDriver('10.101.123.252') as plc: print(plc)

However it is giving me:

(venv) PS C:\Python\PyCommApp> & c:/Python/PyCommApp/venv/Scripts/python.exe c:/Python/PyCommApp/LogixApp.py forward_open failed - Connection Failure (see extended status) - Port not available (01, 311) Traceback (most recent call last): File "c:/Python/PyCommApp/LogixApp.py", line 3, in with LogixDriver('10.101.123.252') as plc: File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 100, in init self.get_plc_info() File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 55, in wrapped raise DataError(msg) pycomm3.DataError: Target did not connected. get_plc_info will not be executed.

So the micro820 gives me "Connection Failure (see extended status) - Port not available (01, 311)" which the 1756-L72 wasn't giving me. Maybe that has more information to help with.

ottowayi commented 4 years ago

Currently Micro800s are not supported. They are slightly different than Compact/Control Logix, so that would be why it's failing. I would like to add support, but my company does not use them so I have no hardware to test on. I can try and add support, but would need someone else to test it for me.

ottowayi commented 4 years ago

Actually, it may be easier that I anticipated. Could you test something for me?

Let me know if that works for connecting using just the IP address, some quick research shows that the only difference is the other controllers need to specify the backplane and slot number in the path. By default, it will add those if you only pass in an IP address. It may be as simple as omitting that part for micro800, so if that works I will add an arg to allow for that.

trevorstahl commented 4 years ago

I made that change and for the micro820 I am now getting:

(venv) PS C:\Python\PyCommApp> & c:/Python/PyCommApp/venv/Scripts/python.exe c:/Python/PyCommApp/LogixApp_pycomm3.py Traceback (most recent call last): File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 140, in get_plc_name raise DataError(f'send_unit_data did not return valid data - {response.error}') pycomm3.DataError: send_unit_data did not return valid data - Service not supported - Extended status info not present During handling of the above exception, another exception occurred: Traceback (most recent call last): File "c:/Python/PyCommApp/LogixApp_pycomm3.py", line 3, in with LogixDriver('10.101.123.252') as plc: File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 102, in init self.get_plc_name() File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 56, in wrapped return func(self, *args, **kwargs) File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 143, in get_plc_name raise DataError(err) pycomm3.DataError: send_unit_data did not return valid data - Service not supported - Extended status info not present

ottowayi commented 4 years ago

Sorry I also missed the part where you were trouble with the L72. If you're connecting through an older comms module (like ENET or ENBT), you need to set large_packets=False in the constructor. By default, it will attempt to use Extended Forward Open (4000 byte packets instead of 500) which is only support on EN2T (or newer) and V20+. It looks like I'm missing that part in the readme, sorry I will get that added.

ottowayi commented 4 years ago

I'm not sure what is/isn't supported on the Micro800, I use this KB article get the controller name. You may need to set init_info=False too if that is not supported on them. I noticed it says it works on Logix controllers, but I don't know if that include Micro800.

trevorstahl commented 4 years ago

Setting large_packets=False in the constructor did it, thanks!

The Micro820 that we have isn't meant to be set up on the network at all so it isn't a large concern, just wanted to see if I was getting the same errors with it. However, its on a different location than the L72 which is working now, so I may get a chance to screw around with it later.

Thanks for the help!

ottowayi commented 4 years ago

Awesome, I will add an option for them. What do you think would be better?

LogixDriver('1.2.3.4', micro800=True) or LogixDriver('1.2.3.4/micro800')

I'm partial to the '/micro800' addition to the path, since it's 1 less variable that has to be stored or passed around. But either option is fine with me.

trevorstahl commented 4 years ago

Sorry, maybe my message was misinterpreted! The L72 is now working with large_packets=False, but the Micro820 isn't working yet.

I tried using init_info=false when I have _path = [] and it is able to run, but I cannot read anything.

Code:

from pycomm3 import LogixDriver with LogixDriver('10.101.123.252', init_info=False) as plc: print(plc) print(plc.info) ret = plc.read('_IO_EM_DO_00') print(ret.error) print(ret.value)

Output:

Program Name: None, Device: None, Revision: None {} unpack requires a buffer of 2 bytes None

If you have anything else you would like me to try let me know!

ottowayi commented 4 years ago

damn, okay a long shot.. but, try setting plc.use_instance_ids = False as the first statement in your with block. I can't seem to find any documentation about if it supports using symbol instance addressing, but if I make a driver for one in IGS it does not have an option for what protocol mode to use like the ControlLogix one does. So maybe they don't?

trevorstahl commented 4 years ago

Nope, still no luck.

ottowayi commented 4 years ago

Hmm.. sorry I wish I could be more helpful, but without the hardware to test on it's kind of hard. What type of tag are you trying to read and the data type? Can you add debug=True to the constructor and reply with the output? Also, can you look at these links, they seem to talk about some of the limitations and see if there is any relevant data to what you're trying? The first link mentions that the multi-request service doesn't work, which definitely could be a problem. Even if you're trying to read a single tag, it will use a multi-request packet since it was simpler to do that always rather than have a separate code path for single tags. If it's related to that, I can maybe send you a modified version where it only doesn't use multi-requests and we can try that out to see if it fixes it.

ottowayi commented 4 years ago

Actually, I went ahead and just made a quick edit to the read for a single tag. Can you try modifying the _read__build_requests method in clx.py like this:

def _read__build_requests(self, parsed_tags):
    if len(parsed_tags) == 1:
        request = self.new_request('read_tag')
        tag_data = list(parsed_tags.values())[0]
        request.add(tag_data['plc_tag'], tag_data['elements'], tag_data['tag_info'])
        requests.append(request)
        return [request, ]
    else:
        ... # move the rest of the method under here
trevorstahl commented 4 years ago

I made the change and it appears to be able to read the tags!

Code change made:

if len(parsed_tags) == 1: requests = [] request = self.new_request('read_tag') tag_data = list(parsed_tags.values())[0] request.add(tag_data['plc_tag'], tag_data['elements'], tag_data['tag_info']) requests.append(request) return [request, ] else: etc....

Running code:

from pycomm3 import LogixDriver with LogixDriver('10.101.123.252', init_info=False,) as plc: plc.use_instance_ids= False print(plc) print(plc.info) result = plc.read('_IO_EM_DO_00') print(result.error) print(result.value) print(result.tag) print(result.type)

And the output:

(venv) PS C:\Python\PyCommApp> & c:/Python/PyCommApp/venv/Scripts/python.exe c:/Python/PyCommApp/LogixApp_pycomm3.py Program Name: None, Device: None, Revision: None {} None False _IO_EM_DO_00 BOOL

That appears to allow it to read the tag!

However, the project we have for the Micro820 starts tonight so I won't be able to mess around with it any more. Thanks for your help with the L72 and the work you are doing on the pycomm3 and pylogix modules! Cheers!

ottowayi commented 4 years ago

Awesome, thank you for helping test it. I think I will add an argument for micro800 and single tag read/write. Also, I think I will make it so if it is a Micro800 it will only perform single read/writes if even you pass in multiple tags.

trevorstahl commented 4 years ago

Turns out there was another 820 available to use! I can continue to be a test dummy if you have anything else!

ottowayi commented 4 years ago

That would be great, Thanks! Can you install from the develop branch and test it out for me please? And first try with using only micro800=True, (and disable large packets if still using an ENBT). Then if it doesn't work, start with disabling the other stuff we tried before. I would like to narrow it down to what does/doesn't work and have that micro800 flag disable anything that doesn't work.

ottowayi commented 4 years ago

@trevorstahl I published v0.5.1 it has the Micro800 option as well as automatic downgrade to a standard forward open (so no need for large_packets=False now)

trevorstahl commented 4 years ago

Sorry about the delay, I guess I must have missed the last message the last time I checked the thread.

I installed v0.5.1, and micro800=True works when I also include init_info=False in the constructor as well as use use_instance_ids=False on the first line...

Here are the different outputs:


Using init_info and use_instance_ids

Code:

from pycomm3 import LogixDriver with LogixDriver('10.101.123.111', micro800=True, init_info=False) as plc: plc.use_instance_ids= False print(plc) print(plc.info) result = plc.read('_IO_EM_DO_00') print(result.error) print(result.value) print(result.tag) print(result.type)

Output:

Program Name: None, Device: None, Revision: None {} None False _IO_EM_DO_00 BOOL


Using only init_info=False

Code:

from pycomm3 import LogixDriver with LogixDriver('10.101.123.111', micro800=True, init_info=False) as plc:

plc.use_instance_ids= False

print(plc) print(plc.info) result = plc.read('_IO_EM_DO_00') print(result.error) print(result.value) print(result.tag) print(result.type)

Output:

Program Name: None, Device: None, Revision: None {} Failed parsing reply data Traceback (most recent call last): File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\packets\responses.py", line 139, in _parse_reply self.value, self.data_type = parse_read_reply(self.data, self.tag_info, self.elements) File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\packets\responses.py", line 321, in parse_read_reply datatype = DATA_TYPE[unpackuint(data[:2])] File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\bytes.py", line 100, in unpack_uint return int(struct.unpack('<H', st[0:2])[0]) struct.error: unpack requires a buffer of 2 bytes Failed to parse reply - unpack requires a buffer of 2 bytes None _IO_EM_DO_00 None


Without using init_info=False

Code:

from pycomm3 import LogixDriver with LogixDriver('10.101.123.111', micro800=True) as plc: plc.use_instance_ids= False print(plc) print(plc.info) result = plc.read('_IO_EM_DO_00') print(result.error) print(result.value) print(result.tag) print(result.type)

Output:

Traceback (most recent call last): File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 540, in get_plc_name raise DataError(f'send_unit_data did not return valid data - {response.error}') pycomm3.DataError: send_unit_data did not return valid data - Service not supported - Extended status info not present During handling of the above exception, another exception occurred: Traceback (most recent call last): File "c:/Python/PyCommApp/LogixApp_pycomm3.py", line 3, in with LogixDriver('10.101.123.111', micro800=True) as plc: File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 169, in init self.get_plc_name() File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 71, in wrapped return func(self, *args, **kwargs) File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 543, in get_plc_name raise DataError(err) pycomm3.DataError: send_unit_data did not return valid data - Service not supported - Extended status info not present


I can try the automatic downgrade for large_packets=false later this week when I am using the L72 and ENBT

ottowayi commented 4 years ago

Okay, so it looks like if we disable instance ids and the program name (the plc info looks fine since it comes before the program name method and isn't crashing) are our problems. Could you try this:

    if init_tags or init_info:
        self.open()
        if init_info:
            self.get_plc_info()
            self.use_instance_ids = (self.info.get('version_major', 0) >= MIN_VER_INSTANCE_IDS) and not micro800
            if not micro800:
                self.get_plc_name()

in the LogixDriver.init ?

trevorstahl commented 4 years ago

Well, that worked great! Whether I use plc.use_instance_ids = False or leave it commented out, the output I get is:

Program Name: None, Device: 2080-LC20-20QWB, Revision: 12.11 {'vendor': 'Rockwell Automation/Allen-Bradley', 'product_type': 'Programmable Logic Controller', 'product_code': 180, 'version_major': 12, 'version_minor': 11, 'revision': '12.11', 'serial': '60ff72d4', 'device_type': '2080-LC20-20QWB', 'keyswitch': 'UNKNOWN'}
None False _IO_EM_DO_00 BOOL

with code:

from pycomm3 import LogixDriver with LogixDriver('10.101.123.111', micro800=True) as plc:

plc.use_instance_ids= False

print(plc) print(plc.info) result = plc.read('_IO_EM_DO_00') print(result.error) print(result.value) print(result.tag) print(result.type)

So it seems to be working perfectly! I am able to read and write the outputs no problem now.

Also maybe you can help me (I may just be stupid here), but when I preview my comment when using the <> for code, it never looks right in the preview, hence why I keep putting the code in as quotes... Shouldn't I just be able to paste it between the apostrophes?

Instead it appears as:

`from pycomm3 import LogixDriver with LogixDriver('10.101.123.111', micro800=True) as plc:

plc.use_instance_ids= True

print(plc)
print(plc.info)
result = plc.read('_IO_EM_DO_00')
print(result.error)
print(result.value)
print(result.tag)
print(result.type)`
ottowayi commented 4 years ago

That's good, thanks for testing it for me. One tip, you can just print the tag result too, no need to print each attribute separately . As for the code formatting, if you're doing multiple lines, you need to wrap it in triple-backticks like:

```
>>> print(tag)
Tag1, DINT, 100, None
```

to get output like:

>>> print(repr(result))  # or use repr too 
Tag(tag='Tag1', type='DINT', value=100, error=None)

Check this out for some more markdown formatting.

ottowayi commented 4 years ago

Micro800 detection is now automatic, micro800=True is only required if init_info=False.

trevorstahl commented 4 years ago

Uh oh, I updated to 0.6.5 and I don't seem to be getting a response in get_plc_info, and I believe it is because the self.attribs['cip_path'} isn't accounting for a micro800 here, but I could be wrong.

Here is what I am getting:

(venv) PS C:\Python\PyCommApp> & c:/Python/PyCommApp/venv/Scripts/python.exe c:/Python/PyCommApp/LogixApp_pycomm3.py
Traceback (most recent call last):
  File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 587, in get_plc_info
    raise DataError(f'send_unit_data did not return valid data - {response.error}')
pycomm3.DataError: send_unit_data did not return valid data - Failed to parse reply - unpack requires a buffer of 2 bytes

During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "c:/Python/PyCommApp/LogixApp_pycomm3.py", line 3, in <module>
    with LogixDriver('10.101.123.111') as plc:
  File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 170, in __init__
    self.get_plc_info()
  File "C:\Python\PyCommApp\venv\lib\site-packages\pycomm3\clx.py", line 590, in get_plc_info      
    raise DataError(err)
pycomm3.DataError: send_unit_data did not return valid data - Failed to parse reply - unpack requires a buffer of 2 bytes

I am back to just a simple code to try to troubleshoot it:

from pycomm3 import LogixDriver

with LogixDriver('10.101.123.111') as plc:
    print(plc)

As before, it works if I disable init_info, enable micro800 and use plc.use_instance_ids= False

ottowayi commented 4 years ago

I think I made a change to the get_plc_info to use an unconnected message so it could detect the controller type before sending a forward open request (since Micro800 require some additional work that Logix PLCs don't). But, it appears the unconnected send portion is throwing an error on Micro800 causing the get_plc_info to fail. I changed it to use a list_identity to detect the Micro800 before sending the get_plc_info and change it to connected if it is one. v0.6.6 should resolve the issue.