Giter VIP home page Giter VIP logo

Comments (75)

timyhac avatar timyhac commented on June 24, 2024

Hi @IanRobo75 - thanks for trying this wrapper!

The wrapper should be able to support anything that the native runtimes support, so it could be possible libplctag native doesn't support this - I'm not really sure.

It is possible that the wrapper has some issues, you can make use of the libplctag.NativeImport package that provides raw access to the libplctag native functionality without any abstractions over the top - could be useful to try this.

Also, are you using the latest prerelease? These packages are still in alpha.

MicroLogic and SLC are different CPU types, could you try with both and see what the result is?
https://github.com/libplctag/libplctag/wiki/Tag-String-Attributes

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

Sorry for the email response. One thing to try is a different size. I think strings on PCCC-based PLCs are either 82 or 84 bytes, not 88. Best, Kyle

On Mon, Jul 20, 2020, 12:26 PM IanRobo75 @.> wrote: I see that the wrapper does not expose the MicroLogix CPU Type. I’ll look at using the NativeImport and investigate. Cheers, Ian Ian Robinson Machine Safety & Automation Engineer [swarmIQ_colour 20%] From: timyhac @.> Sent: Monday, July 20, 2020 4:44 PM To: libplctag/libplctag.NET @.> Cc: IanRobo75 @.>; Mention @.***> Subject: Re: [libplctag/libplctag.NET] SLC Strings - PLCTAG_ERR_TOO_SMALL (#58) Hi @IanRobo75https://github.com/IanRobo75 - thanks for trying this wrapper! The wrapper should be able to support anything that the native runtimes support, so it could be possible libplctag native doesn't support this - I'm not really sure. It is possible that the wrapper has some issues, you can make use of the libplctag.NativeImport package that provides raw access to the libplctag native functionality without any abstractions over the top - could be useful to try this. Also, are you using the latest prerelease? These packages are still in alpha. MicroLogic and SLC are different CPU types, could you try with both and see what the result is? https://github.com/libplctag/libplctag/wiki/Tag-String-Attributes — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub< #58 (comment)>, or unsubscribe< https://github.com/notifications/unsubscribe-auth/AIY6RY3MLTPIP5TTGOJZJPDR4PDQDANCNFSM4PB255VQ>. — You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub <#58 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4LC2OI7XVMRQR5UN2YY3R4SK5RANCNFSM4PB255VQ .

OK will do. I see the latest alpha has the MicroLogix PLC type as well, which was missing from the release version. So I'll try that as well.

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

This is interesting - it means we will have to have different marshallers for different CPU types.

@jkoplo

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

OK, so I believe the base library does not easily support strings for PLC5/ SLC/ MicroLogix comms. I can read a string if I put the correct length in for the string (i.e. if the string in the PLC is "ABC" and I set the tag length as 3 then it works). Getting the length attribute dynamically is too hard for me and not worth the effort.
For reference - I can read I, O, N files as integer (16 bit), and L files as Long (32 bit), but they have to have the N prefix (e.g. N100:1, N9:3 (for L9:3), N0:4 (for O:4), N1:5 (for I:5). B and F files are read as expected (e.g. B3:6, F8:7).

My next task is to communicate with a CompactLogix, I expect strings will be fine there.

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

So reads of DINT are not working? That's not good! Can you please create a dump when trying to read a L file (debug level 4)? I can try to see if there is something obvious about the error code. I only have a PLC/5 to test on, so all that is guess work on my part.

Strings are quite painful. However, I can see what I can do to make the reads easier in the core library. With Control/Compact Logix systems, I just allocate the tag buffer as I am reading in the data. For PCCC-based systems (MicroLogix, SLC, PLC/5), I do not. I will see if there is anything I can do to make that work as well. Then you could read a string in. The problem is when you want to read a string in and then write out a longer string. Then there is no buffer space.

I may need to special case strings. They are not an atomic type like INT and they are not an array and they are not a UDT. And they seem to be quite different on Micro800 PLCs too.

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

This is interesting - it means we will have to have different marshallers for different CPU types.

Thank AB. There are three string formats that I am aware of:

  1. ControlLogix/CompactLogix: 88 bytes with a 4-byte length count and 82 bytes of data and two bytes of padding.
  2. PCCC PLCs: 82 bytes of data and 2-byte count word. Total of 84 bytes. I think. I'll need to double check.
  3. Micro800: one byte count word and up to 255 bytes of data, I think. It really is not clear and I have only had about five minutes of time with one.

AB is NOT a single family. It is at least three. Multiple times I have come close to splitting the code into three completely separate sections for the different PLC types. And there are significant differences between PLC/5, SLC and MicroLogix. The latter two are quite similar but seem to support different maximum packet sizes. But commands for a PLC/5 rarely work for SLC/MicroLogix and vice versa.

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

Thanks Kyle!

@IanRobo75 - RSLogix provides the tag size when you browse the controller data types, at least for CompactLogix, I'm not sure how this works for other CPU types (or other versions of RSLogix for that matter).
image

@kyle-github - One thing I was thinking about doing was providing some guidance on how to reverse-engineer the binary format of a tag (assuming you already know the tag size). I've ended up using a combination of Rockwell manuals and hex-format viewing to get this working for an array of UDTs with embedded Structures - seems to work OK but it's not straightforward.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

@IanRobo75 , for some reason that is not coming through for me.

Strings are clearly a problem. They are a special UDT that is common enough that there should probably be a way to deal with them that is not so painful.

Right now, the only way to deal with them is to allocate the full 82 characters plus space for the length word. But on ControlLogix (and Micro800), we do not actually know that we are dealing with STRING types until we read the tag. At least with PCCC PLCs, you have the data file type, so the tag size could be set initially.

I'll have to think about this a bit.

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

The case where String tags are static in size would be relatively easy to handle in the wrapper, the Encoder/Decoder/Marshaller just needs to know which Cpu type its targeting, and change its logic accordingly.

My understanding (based on some tests of string arrays), is that even though the LEN property tells us how many characters are in the string, the size of the entire tag is always 88 bytes, so if LEN was 12, then there would be 12 ASCII characters, and then 70 null characters.

If the actual size of the tag changes depending on the data in it, then that would be tricky... It would have to be two round trips.... Hmmm...

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

I seem to remember cases where some PCCC PLCs return just the string data and the length without any padding. Does anyone have a MicroLogix or SLC to test that theory?

If that is true, then the core library needs to have some additions in order to recognize string types and behave correctly. For *Logix PLCs, you get the whole string buffer, so there is always space. But in cases where you do not get the whole buffer the library will need to change behavior because PCCC PLCs do not return a status indicating that there is more data left. You just get what you get in the packet.

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

I definitely want to avoid multiple round trips, so the important check here is to see if anything returns variable length results.

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

This is interesting - it means we will have to have different marshallers for different CPU types.

Thank AB. There are three string formats that I am aware of:

  1. ControlLogix/CompactLogix: 88 bytes with a 4-byte length count and 82 bytes of data and two bytes of padding.
  2. PCCC PLCs: 82 bytes of data and 2-byte count word. Total of 84 bytes. I think. I'll need to double check.
  3. Micro800: one byte count word and up to 255 bytes of data, I think. It really is not clear and I have only had about five minutes of time with one.

AB is NOT a single family. It is at least three. Multiple times I have come close to splitting the code into three completely separate sections for the different PLC types. And there are significant differences between PLC/5, SLC and MicroLogix. The latter two are quite similar but seem to support different maximum packet sizes. But commands for a PLC/5 rarely work for SLC/MicroLogix and vice versa.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

Thanks for checking this Ian. Very interesting that the characters are byte swapped!

I have no experience with MicroLogix - are "N" tags 16 bits (element size = 2)?

Update: 16bits not bytes

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

@IanRobo75 - please not I've just released a new (still alpha) version of NativeImport. Main difference for you is the names of the methods have changed to 100% reflect the C API.
Also included are constants (StatusCodes etc) so you don't have to define these yourself.

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

@IanRobo75, thanks for the checks.

I had a report a while back of byte swapped character pairs in strings but I never was able to replicate it. That says to me that I need to treat strings as a completely different data type in the core library. Note that strings are different across different PLCs.

I had to update some PLC/5 code recently because the words within a REAL were byte swapped.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

@IanRobo75 - please not I've just released a new (still alpha) version of NativeImport. Main difference for you is the names of the methods have changed to 100% reflect the C API.
Also included are constants (StatusCodes etc) so you don't have to define these yourself.

Yep, makes sense, I've updated my ML test example accordingly.

Still can't read L9:5 though (using libplctag), even as N9:5; get PLCTAG_ERR_BAD_PARAM.

Here's the level 4 debug...

2389-06-22 12:11:25.236 thread(1) tag(0) INFO find_tag_create_func:95 Matched protocol=ab_eip
2389-06-22 12:11:25.236 thread(1) tag(0) INFO ab_tag_create:197 Starting.
2389-06-22 12:11:25.236 thread(1) tag(0) INFO rc_alloc_impl:111 Starting, called from ab_tag_create:204
2389-06-22 12:11:25.236 thread(1) tag(0) INFO rc_alloc_impl:130 Done
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL rc_alloc_impl:135 Returning memory pointer 011965B8
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL ab_tag_create:211 tag=011965B8
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL get_plc_type:1695 Found SLC 500 PLC.
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL get_plc_type:1695 Found SLC 500 PLC.
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL session_find_or_create:226 Starting
2389-06-22 12:11:25.236 thread(2) tag(0) INFO tag_tickler_func:187 Starting.
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL session_find_or_create:250 Creating new session.
2389-06-22 12:11:25.236 thread(1) tag(0) INFO session_create_unsafe:432 Starting
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL session_create_unsafe:434 Warning: not using passed port 44818
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL session_create_unsafe:439 Session should not use connected messaging.
2389-06-22 12:11:25.236 thread(1) tag(0) INFO rc_alloc_impl:111 Starting, called from session_create_unsafe:442
2389-06-22 12:11:25.236 thread(1) tag(0) INFO rc_alloc_impl:130 Done
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL rc_alloc_impl:135 Returning memory pointer 090824E0
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL add_session_unsafe:306 Starting
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL add_session_unsafe:316 Done
2389-06-22 12:11:25.236 thread(1) tag(0) INFO session_create_unsafe:537 Done
2389-06-22 12:11:25.236 thread(1) tag(0) INFO session_init:553 Starting.
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL thread_create:612 Starting.
2389-06-22 12:11:25.236 thread(1) tag(0) DETAIL thread_create:646 Done.
2389-06-22 12:11:25.237 thread(1) tag(0) INFO session_init:567 Done.
2389-06-22 12:11:25.237 thread(1) tag(0) DETAIL session_find_or_create:296 Done
2389-06-22 12:11:25.237 thread(1) tag(0) DETAIL ab_tag_create:293 using session=090824E0
2389-06-22 12:11:25.237 thread(1) tag(0) DETAIL ab_tag_create:324 Setting up SLC/MicroLogix tag.
2389-06-22 12:11:25.237 thread(1) tag(0) DETAIL slc_encode_tag_name:316 Starting.
2389-06-22 12:11:25.237 thread(1) tag(0) DETAIL parse_pccc_logical_address:743 Starting.
2389-06-22 12:11:25.237 thread(1) tag(0) WARN parse_pccc_file_type:906 Bad format or unsupported logical address L9:5!
2389-06-22 12:11:25.237 thread(1) tag(0) DETAIL parse_pccc_file_type:912 Done.
2389-06-22 12:11:25.237 thread(1) tag(0) WARN parse_pccc_logical_address:747 Unable to parse PCCC-style tag for data-table type! Error PLCTAG_ERR_BAD_PARAM!
2389-06-22 12:11:25.238 thread(3) tag(0) INFO session_handler:925 Starting thread for session 090824E0
2389-06-22 12:11:25.238 thread(1) tag(0) DETAIL parse_pccc_logical_address:767 Starting.
2389-06-22 12:11:25.238 thread(1) tag(0) WARN slc_encode_tag_name:327 Unable to parse SLC logical addresss!
2389-06-22 12:11:25.238 thread(3) tag(0) DETAIL session_handler:944 in SESSION_OPEN_SOCKET state.
2389-06-22 12:11:25.238 thread(1) tag(0) WARN check_tag_name:1758 parse of SLC-style tag name L9:5 failed!
2389-06-22 12:11:25.238 thread(3) tag(0) INFO session_open_socket:583 Starting.
2389-06-22 12:11:25.238 thread(1) tag(0) INFO ab_tag_create:437 Bad tag name!
2389-06-22 12:11:25.238 thread(3) tag(0) DETAIL socket_create:835 Starting.
2389-06-22 12:11:25.238 thread(1) tag(0) WARN plc_tag_create:567 Error PLCTAG_ERR_BAD_PARAM while trying to create tag!
2389-06-22 12:11:25.238 thread(1) tag(0) DETAIL rc_dec_impl:242 Calling cleanup functions due to call at plc_tag_create:568 for 011965B8.
2389-06-22 12:11:25.238 thread(1) tag(0) INFO refcount_cleanup:256 Starting
2389-06-22 12:11:25.238 thread(1) tag(0) INFO ab_tag_destroy:753 Starting.
2389-06-22 12:11:25.238 thread(1) tag(0) DETAIL ab_tag_destroy:765 Getting ready to release tag session 090824E0
2389-06-22 12:11:25.238 thread(1) tag(0) DETAIL ab_tag_destroy:767 Removing tag from session.
2389-06-22 12:11:25.238 thread(1) tag(0) DETAIL rc_dec_impl:242 Calling cleanup functions due to call at ab_tag_destroy:768 for 090824E0.
2389-06-22 12:11:25.238 thread(1) tag(0) INFO refcount_cleanup:256 Starting
2389-06-22 12:11:25.238 thread(1) tag(0) INFO session_destroy:722 Starting.
2389-06-22 12:11:25.238 thread(1) tag(0) DETAIL remove_session:367 Starting.
2389-06-22 12:11:25.238 thread(1) tag(0) DETAIL remove_session_unsafe:343 Starting
2389-06-22 12:11:25.238 thread(1) tag(0) DETAIL remove_session_unsafe:358 Done
2389-06-22 12:11:25.238 thread(1) tag(0) DETAIL remove_session:375 Done.
2389-06-22 12:11:25.238 thread(1) tag(0) INFO session_destroy:733 Session sent 0 packets.
2389-06-22 12:11:25.240 thread(3) tag(0) DETAIL socket_create:854 Done.
2389-06-22 12:11:25.240 thread(3) tag(0) DETAIL socket_connect_tcp:874 Starting.
2389-06-22 12:11:25.249 thread(3) tag(0) DETAIL socket_connect_tcp:923 Found numeric IP address: 192.168.5.151
2389-06-22 12:11:25.250 thread(3) tag(0) DETAIL socket_connect_tcp:1014 Done.
2389-06-22 12:11:25.250 thread(3) tag(0) INFO session_open_socket:600 Done.
2389-06-22 12:11:25.250 thread(3) tag(0) DETAIL session_handler:1137 Critical block.
2389-06-22 12:11:25.256 thread(1) tag(0) INFO session_close_socket:703 Starting.
2389-06-22 12:11:25.259 thread(1) tag(0) INFO session_close_socket:711 Done.
2389-06-22 12:11:25.259 thread(1) tag(0) INFO session_destroy:806 Done.
2389-06-22 12:11:25.259 thread(1) tag(0) INFO refcount_cleanup:268 Done.
2389-06-22 12:11:25.259 thread(1) tag(0) INFO ab_tag_destroy:789 Finished releasing all tag resources.
2389-06-22 12:11:25.259 thread(1) tag(0) INFO ab_tag_destroy:791 done
2389-06-22 12:11:25.259 thread(1) tag(0) INFO refcount_cleanup:268 Done.

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

Interesting - what is the full attribute string for this tag? I don't think I can help but it might clarify.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

Here are my results reading different MicroLogix data types with libplctag.NativeImport ...

How to read the data successfully...
O:0.0 - Read as O0:0 as element length 2
O:0.1 - Read as O0:0 as element length 4 and use bytes 2 & 3
O:1.1 - Read as O0:1 as element length 2
O:1.2 - Read as O0:1 as element length 4 and use bytes 2 & 3
I:0 - Read as I1:0
S:4 - Read as S2:4
B3:0 - Read as B3:0 [same]
T4:0.PRE - Read as T4:0.PRE [same]
C5:0.PRE - Read as C5:0.PRE [same]
N7:0 - Read as N7:0 [same]
L9:5 - !!! can't read in any form !!!
ST102:0 - Read as ST102:0 [same] (then decode bytes 0 & 1 as length, and that length of characters from byte 2 onwards)

Note all the working ones are 16-bit integers [element size 2] + the string [element size 84].

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

Well that points to the problem!

2389-06-22 12:11:25.237 thread(1) tag(0) WARN parse_pccc_file_type:906 Bad format or unsupported logical address L9:5!

Looks like I am not handling the L type. And it looks like I do not have the data type byte I need for it. My DF1 docs are so old that I do not see a 32-bit integer type in them. I will need to hunt around to see if there is an updated version of the PCCC protocol docs.

I am using PLC/5-specific commands to read my PLC/5 and with those (but not the commands I was using!) I see the byte swapped characters as well. But the count/LEN field is little-endian. Oh how I love AB.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from here... https://www.sciencedirect.com/science/article/pii/S1742287617301998

looks like the L file is file type 0x91 in the PCCC protocol...

Test Cases | Classified File-type

Data Files/New/select Type:Binary | – | New file B9 | 0x85
Data Files/New/select Type:Integer | – | New file N10 | 0x89
Data Files/New/select Type:Long | – | New file L11 | 0x91
Data Files/New/select Type:Message | – | New file MSG12 | 0x92
Data Files/New/select Type:PID | – | New file PI13 | 0x93

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

@kyle-github - if you'd prefer to handle strings in the C library that is Ok, but it is relatively easy to develop marshalling logic based on Cpu type - although it would mean that all of the wrappers would need to implement this separately.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

@IanRobo75, thanks.! Where did you get that table? I was able to fill in three gaps from it. Here is what I have now:

        case PCCC_FILE_ASCII: return 0x8e; break;
        case PCCC_FILE_BIT: return 0x85; break;
        case PCCC_FILE_BLOCK_TRANSFER: break;
        case PCCC_FILE_COUNTER: return 0x87; break;
        case PCCC_FILE_BCD: return 0x8f; break;
        case PCCC_FILE_FLOAT: return 0x8a; break;
        case PCCC_FILE_INPUT: return 0x8c; break;
        case PCCC_FILE_LONG_INT: return 0x91; break;
        case PCCC_FILE_MESSAGE: return 0x92; break;
        case PCCC_FILE_INT: return 0x89; break;
        case PCCC_FILE_OUTPUT: return 0x8b; break;
        case PCCC_FILE_PID: return 0x93; break;
        case PCCC_FILE_CONTROL: return 0x88; break;
        case PCCC_FILE_STATUS: return 0x84; break;
        case PCCC_FILE_SFC: break;
        case PCCC_FILE_STRING: return 0x8d; break;
        case PCCC_FILE_TIMER: return 0x86; break;

Any idea on block transfer or SFC?

I have a proposed fix in the fix_pccc_long_int branch. It would be great if you could give it a try.

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

@timyhac, I would prefer to tackle strings in the C library. They are a fundamental type and not having support in the core library would mean that support would differ based on the wrappers. While there will be varying degrees of ease of use due to the wrappers, I do want to make sure that all wrappers can support the same common base functionality.

Unlike UDTs, strings are a known (if somewhat annoying) quantity.

There are things that are going to be easier to do in the library, such as handling string lengths. For instance, suppose you read a string from a ControlLogix. You get all 88 bytes all the time. IIRC, on some of the PCCC PLCs, you only get as many bytes as the count and the valid bytes of the string. I think that Micro800 may play games there too. Then there is the problem with the byte swapping. Hiding that from the application would help a lot. Yes, it can easily be solved in the application, but then that is code that every single application or wrapper will need to write. Might as well get it right once in the core library.

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

Sounds good @kyle-github

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

I created two new issues in the core library to track these changes:

#163 - Support Long Int on PCCC PLCs.
#162 - Support strings in the core library.

The string issue is a tricky one. In order to make this low-friction for users it needs to use the string data type from the application. C strings are relatively well supported by most languages with C interop. At least as parameters. The initial idea I have is two new API functions, a getter and a setter that handle C strings directly.

One huge decision to make here is how to handle the memory involved. None of the PLC string types map well to C strings. Thus, I might need to allocate additional memory in which to populate the return string of the getter. If I have to allocate additional memory, then somehow it needs to be released. I can do that with additional trickery in the library, but that is not ideal.

Alternatively, I can translate the data in situ as all of the string types that I know of use more (or the same) space than an equivalent length C string. So I could translate it in place in the internal tag buffer. Then I could overload the plc_tag_get_size() call or something to return the string length. That does not sound like a great level of abstraction.

I would appreciate it if all of you would weigh in on the issue itself in the core library issues. I at least want to explore the possibility of adding API calls to the library to support this. Perhaps that is the wrong thing to do, but I want to make sure that if we decide not to do that, that there are well understood reasons why it is not a good idea.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

For the wrapper, libplctag, I believe this section of the C library is failing for strings. Assumedly data_end - data is not the 84 bytes I asked for.

eip_slc_pccc.c

    /* did we get the right amount of data? */
    if((data_end - data) != tag->size) {
        if((int)(data_end - data) > tag->size) {
            pdebug(DEBUG_WARN,"Too much data received!  Expected %d bytes but got %d bytes!", tag->size, (int)(data_end - data));
            rc = PLCTAG_ERR_TOO_LARGE;
        } else {
            pdebug(DEBUG_WARN,"Too little data received!  Expected %d bytes but got %d bytes!", tag->size, (int)(data_end - data));
            rc = PLCTAG_ERR_TOO_SMALL;
        }
        break;
    }

When using the libplctag.NativeImport it doesn't go wrong for a reason that's above my level of understanding.

I'm assuming selection of Micrologix plc type means using the above eip_slc file (no real difference between an SLC and a ML).

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

@IanRobo75, string size is PLC-dependent. From the comments made above, it sounded like the wrapper is using a single value of 88. So, it will set the tag size to that, which will then fail the main if check.

This check against the size is something that is not needed when talking with a Control/CompactLogix PLC. In that protocol, there is a status indicator that tells you whether you have received all the data the PLC will send or not. No such thing in PCCC.

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

Oops, and yes, right now the SLC and MicroLogix code are all the same. At least according to the DF1 docs I have, they use the same protocol commands.

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

Yes the wrapper has 88 hardcoded against the STRING DataType.

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

I updated the core issue for string support. It is a little lower level than I wanted, but at the same time does abstract away the PLC-specific byte ordering and detailed handling:

int plc_tag_get_string_length(int32_t tag_id, int string_start_offset);
int plc_tag_set_string_length(int32_t tag_id, int string_start_offset, int string_len);
int plc_tag_get_string_char(int32_t tag_id, int string_start_offset, int char_index);
int plc_tag_set_string_char(int32_t tag_id, int string_start_offset, int char_index, int char_val);

This allows the core library to continue to access the tag data as the raw bytes from the PLC. If you want to read a string, you figure out the offset where your string field starts and then call plc_tag_get_string_length(). Then you call plc_tag_get_string_char() for each character with the same offset and an index for each character within the string. The library will handle the real offset calculation based on the PLC and string type.

If you want to write a string, you write the length with plc_tag_set_string_length() first and then successive calls to plc_tag_set_string_char() out to the length of the string.

While this is definitely not as ergonomic as handling C strings directly, it appears to be possible to support counted and non-counted strings and varying byte order and count word sizes transparently to the application or wrapper.

PLCs that use variable length strings are still going to be interesting, but at least there is a solid hook for handling them on a per-PLC basis with this API.

All wrappers should be able to use this API regardless of PLC type. That said, there are clear limits:

  1. As mentioned, variable length strings get real interesting.
  2. Arrays of variable length strings are even more interesting.
  3. The int value returned will be 0-255 for the byte value and negative for an error. No wchar_t. But at least UTF-8 is supported.
  4. A variable length string inside a UDT is probably going to stay in the category of unsupported for a long time. Note that these exist: the tag listing raw data contains exactly this.

Thoughts? It is not as ergonomic, but it removes most, perhaps all, of the PLC-specific code in the wrappers.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

I like it

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

Hi @IanRobo75 - It is not. You can follow the discussion on Strings here libplctag/libplctag#162

Kyle has a proposed change for the Integer problem. To use this you'll need to compile the https://github.com/libplctag/libplctag/tree/Fix_pccc_long_int branch and then replace the plctag.dll in your application folder.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

I built and attached the Windows ZIP files with the core DLL in them here. Let me know if you need the other ones!

libplctag_2.1.10_windows_x64.zip
libplctag_2.1.10_windows_x86.zip

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

Hi @IanRobo75 ,

It is this dll that you need to replace (or insert if you are running it for the first time):
image

If there is no file called "plctag.dll" (On Windows), then NativeImport will extract one from itself and desposit it in the filesystem.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

I am able to reproduce that error if I use the x86 dll while running on an x64 machine.

I don't have an x86 machine to try the x86 dll on (if this is relevant to you).

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

Do you need the .pdb file too? Not a Windows programmer, nor do I play one on TV.

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

Thanks Ian - is it possible to create a minimal example, it could be some issue with P/Invoke.

The API call for destroy should be plc_tag_destroy(tagHandle) -

var tagHandle = plctag.plc_tag_create("protocol=ab_eip&gateway=192.168.0.10&path=1,0&cpu=LGX&elem_size=4&elem_count=1&name=MY_DINT", 1000);

while (plctag.plc_tag_status(tagHandle) == 1)
{
    Thread.Sleep(100);
}
var statusBeforeRead = plctag.plc_tag_status(tagHandle);
if (statusBeforeRead != 0)
{
    Console.WriteLine($"Something went wrong {statusBeforeRead}");
}

plctag.plc_tag_read(tagHandle, 1000);
while (plctag.plc_tag_status(tagHandle) == 1)
{
    Thread.Sleep(100);
}
var statusAfterRead = plctag.plc_tag_status(tagHandle);
if (statusAfterRead != 0)
{
    Console.WriteLine($"Something went wrong {statusAfterRead}");
}

var theValue = plctag.plc_tag_get_uint32(tagHandle, 0);

plctag.plc_tag_destroy(tagHandle);

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

I will be releasing the core library with the above changes for Long Int over the weekend. Hopefully @jkoplo or @timyhac can release a new version of the C# library including that soon after I release the core library.

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

Version 2.1.10 is released. This should have support for PCCC long integers in the C core library.

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

0.0.9-alpha of libplctag.NativeImport has been released that includes these binaries.

@IanRobo75 - in some testing I've found that if you set the target platform to x86 then NativeImport will not be able to interop with the x64 binary despite running on an x64 machine. This could be related to your BadImageFormatException.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

Thanks for testing!

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

Will the NativeImport wrapper get the string code soon-ish? And I assume the final C# wrapper would run the tag_get_string_length, then plc_tag_get_string_char for those number of characters and return the complete c# string to the user?

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

Hi @IanRobo75, the string API is still being discussed. I have not started implementing it yet in the core library.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

OK.
In my main app I'm now testing libplctag 0.0.27-alpha10.
in my build folder (...\bin\Debug) I renamed libtag.dll and plctag.dll and rebuilt the app; the new files were created correctly (libplctag.dll dated Friday, and plctag.dll dated at build time).
When I run my program it adds the tag "B3:0" ok, then when adding the tag "F8:0" the program [exe] crashes, error code 0xc0000409 [= STATUS_STACK_BUFFER_OVERRUN].

The offending line...
plcReadGroups.Add(new Tag(System.Net.IPAddress.Parse(IPAddress), "", (libplctag.CpuType)CPUType, TagSize(TypeStart), CheckName(groupStart), TIMEOUT, readIndex + 1));

Note: here I'm actually creating and adding the tag to a list, but that's been working fine up to now. So the important tag part, putting the parsed parts in square brackets for display here, is...
Tag([192.168.5.151], "", [MicroLogix], 4, "F8:0", 5000, 1)

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

Hi Ian - that seems to be a .NET exception - is that right?

Does it repeatedly throw the error on adding the same tag (i.e. on consecutive runs)?
Can you create that tag by itself in a standalone console application?
If you add a different tag at the same point in time (even duplicating another tag should be ok) - does it throw?
Are tags being added in parallel/multithreaded?
Are you able to create that Tag using NativeImport?

I haven't come across this error before. It seems to be something related to the native binaries..
https://devblogs.microsoft.com/oldnewthing/20190108-00/?p=100655

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

Hi @IanRobo75 - I've put together a string marshalling class and would appreciate if you could take a look at the implementation before it is released.
https://github.com/libplctag/libplctag.NET/blob/generics-timyhac/src/libplctag/DataTypes/StringMarshaller.cs#L178

You can see that there is different logic for each PLC type. There isn't an implementation for Plc5, Slc500, or Micro800 - but there is for ControlLogix, LogixPccc and MicroLogix.

Any feedback you've got for this would be great.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

will test at some point, just really busy currently.
Also. aren't PLC5, & SLC500 PCCC anyway so would be the same as MicroLogix?

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

OK, just getting back onto this.

Frist issue, just reading through StringMarsahhler.cs before I start....

    override public int? ElementSize
    {
        get
        {
            switch (PlcType)
            {
                case PlcType.ControlLogix: return 88;
                case PlcType.Plc5: return 88;
                case PlcType.Slc500: return 25;
                case PlcType.LogixPccc: return 84;
                case PlcType.Micro800: return 256;
                case PlcType.MicroLogix: return 256;
                default: throw new NotImplementedException();
            }
        }
    }

PLC5, SLC500, and MicroLogix all use strings of 82 characters. With the length bytes header assumedly they are all 84 bytes long, which makes sense because they all are LogixPCCC really.

I'll modify accordingly for my tests with MicroLogix.

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

Thanks @IanRobo75 !

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

So, I've cloned the github code, libplctag/libplctag.NET, (libplctag alpha 13 & libplctag.NativeImport 11 beta).

Doing a new test referencing & using the LibPlcTag class in a forms app, just reading an integer, N7:2, from a MicroLogix...

using System;
using System.Windows.Forms;
using System.Threading;
using libplctag;
......

        var myTag = new Tag()
        {
            Name = "N7:2",
            Gateway = "192.168.5.151",
            Path = "",
            PlcType = PlcType.MicroLogix,
            Protocol = Protocol.ab_eip,
            ElementSize = 2,
            ElementCount = 1
        };

        //Check that tag gets created properly
       while (myTag.GetStatus() == Status.Pending)
            Thread.Sleep(100);

        if (myTag.GetStatus() != Status.Ok)
            throw new LibPlcTagException(myTag.GetStatus());  //<<<<<< GET THE ERROR HERE

        myTag.Read();

.....

I get the error 'libplctag.LibPlcTagException: 'ErrorNotFound'
[HResult: -2146233088 ]

However, my NativeImport 11 beta test app read data fine from the same PLC....

using libplctag.NativeImport;
...

            int elementCount = 1;
            int elementSize = int.Parse(tbSize.Text); //=2
            string tagname = tbAddress.Text;  //="N7:2"

            string path = "protocol=ab_eip" +
                "&gateway=192.168.5.151" +
                "&cpu=micrologix" +
                "&elem_size=" + elementSize.ToString() +
                "&elem_count =" + elementCount.ToString() +
                "&name=" + tagname +
                "&debug=4";

            //clear result textbox on form 
            tbResult.Clear();

            // check the library version.
            if (plctag.plc_tag_check_lib_version(VER_MAJOR, VER_MINOR, VER_PATCH) != (int)STATUS_CODES.PLCTAG_STATUS_OK)
            {
                Debug.Print("Required compatible library version 2.1.0 not available!\n");
                return;
            }

            //create tag
            Int32 tag = plctag.plc_tag_create(path, 5000);

            //check could be created
            if (tag < 0)
            {
                Debug.Print("ERROR " + plctag.plc_tag_decode_error(tag) + " : Could not create tag!\n");
                tbResult.Text += "ERROR " + plctag.plc_tag_decode_error(tag) + " : Could not create tag!" + Environment.NewLine;
                return;
            }

            //check for error
            if (plctag.plc_tag_status(tag) != (int)STATUS_CODES.PLCTAG_STATUS_OK)
            {
                Debug.Print("Error setting up tag internal state.  Error " + plctag.plc_tag_decode_error(tag) + "\n");
                plctag.plc_tag_destroy(tag);
                return;
            }

            int rc;

            if (elementSize <= 4)
            {
                rc = plctag.plc_tag_read(tag, 5000);
                if (rc != (int)STATUS_CODES.PLCTAG_STATUS_OK)
                {
                    Debug.Print("ERROR: Unable to read the data! Got error code " + rc.ToString() + " : " + plctag.plc_tag_decode_error(rc) + "\n");
                    plctag.plc_tag_destroy(tag);
                    return;
                }

            }


            Int32 iData = 0;

            switch (elementSize)
            {
                case 2:
                    iData = plctag.plc_tag_get_int16(tag, 0);
                    tbResult.Text += tagname + " data = " + iData.ToString() + Environment.NewLine;
                    break;
                case 4:
                    iData = plctag.plc_tag_get_int32(tag, 0);
                    tbResult.Text += tagname + " data = " + iData.ToString() + Environment.NewLine;
                    break;
                default: //string
                    //rc = plctag.plc_tag_get_string_Length(tag, 0);
                    break;
            }

....

from libplctag.net.

timyhac avatar timyhac commented on June 24, 2024

Hi @IanRobo75 - seems like the tag creation isn't working.

What is in master at the moment doesn't have a version number. If you cloned this commit then you'll also need to make a call to Initialize().

What is the attribute string generated by the library? You can set a breakpoint here to find out.

You don't need to provide a blank string if the path is intended to be unset, leaving it as null is OK.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

Yes, Initialise sorted it.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

OK, testing reading strings...

Relevant parts of my code:
//-----------------------------------------------------------------------------

        var myTag = new Tag()
        {
            Name = "ST102:0",
            Gateway = "192.168.5.151",
            Path = "",
            PlcType = PlcType.MicroLogix,
            Protocol = Protocol.ab_eip,
            ElementSize = 84,
            ElementCount = 1
        };  //84 = 2 bytes length + 82 chars

        myTag.Initialize(); 

        myTag.Read();

        StringMarshaller str = new StringMarshaller()
        {
            PlcType = PlcType.MicroLogix
        };
        string myResultString = str.DecodeOne(myTag, 0, out int strSize);

//-----------------------------------------------------------------------------

Works great once I changed the following in StringMarsheller.cs, particularly MicroLogixDecode:

//----------------------------------------------------------------------------

    const int MAX_CONTROLLOGIX_STRING_LENGTH = 82; //82 characters max in string
    const int MAX_LOGIXPCCC_STRING_LENGTH = 82;        //82 characters max in string 
    const int MAX_MICRO800_STRING_LENGTH = 80;         //80 characters max in string 

    override public int? ElementSize
    {
        get
        {
            switch (PlcType)
            {
                case PlcType.Plc5: 
                case PlcType.Slc500:
                case PlcType.LogixPccc: 
                case PlcType.MicroLogix: 
                    return 84; //2 bytes length + 82 chars
                case PlcType.ControlLogix: 
                    return 88; //4 bytes length + 82 chars + 2 padding;
                case PlcType.Micro800: 
                    return 82; //2 bytes length + 80 chars
                default: throw new NotImplementedException();
            }
        }
    }

    string MicroLogixDecode(Tag tag, int offset)
    {
        var apparentStringLength = (int)tag.GetInt16(offset);

        var actualStringLength = Math.Min(apparentStringLength, MAX_LOGIXPCCC_STRING_LENGTH);

        var readLength = actualStringLength + (actualStringLength % 2); //read 1 more if odd number

        var asciiEncodedString = new byte[actualStringLength];

        for (int ii = 0; ii < readLength - 1; ii+=2)
        {
            asciiEncodedString[ii] = tag.GetUInt8(offset + 2 + ii + 1);
            if ((apparentStringLength % 2 == 0) || (ii < (actualStringLength - 1))) 
                //don't do for last char in odd number string (i.e. don't get the /0)
            {
                asciiEncodedString[ii + 1] = tag.GetUInt8(offset + 2 + ii);
            }
        }

        return Encoding.ASCII.GetString(asciiEncodedString);
    }

//-----------------------------------------------------------------------------

The fixes in MicroLogixDecode are required otherwise you get errors writing 1 byte past the length of asciiEncodedString. Also I've stepped in 2 bytes and dealt with the odd number strings.

I believe MicroLogixDecode should also work for PLC5 & SLC (also PCCC). I assume they are byte-swapped as well.

The size of a Micro800 string (atomic type SHORT_STRING) I got from reading a Kepware Micro800 driver manual. 256 chars did seem excessive and unlikely.

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

I will set up the C API to use 82 characters for Micro800, but I thought I saw them return a single byte for the count word. I have not had access to a Micro800 for a long time.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

Do, you are right. Just opened up a CCW project for a Micro800 and strings are 255 characters, so assumedly the first byte is the length as you thought. Micro800's are terrible anyway, should be avoided.

from libplctag.net.

kyle-github avatar kyle-github commented on June 24, 2024

Thanks for checking. I have not had the "pleasure" of using one in a project.

I remember that at least one PLC type returned strings without any padding. Might have been Micro800?

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

SLC500 String:
Just tested strings with an old SLC5/05 we have. They work exactly the same as for MicroLogix, as expected. In fact, I selected a MicroLogix at the plc type to prove it.
I noted however, that SLC's don't have the long Integer type, I'd forgotten that, been a while.
That will be the same for PLC5's as well (i.e. strings the same, and no long integers).

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

here is my updated StringPlcmapper.cs file, tested reading & writing strings to/from both a MicroLogix & an SLC500:

using System;
using System.Text;

namespace libplctag.DataTypes
{
public class StringPlcMapper : PlcMapperBase, IPlcMapper, IPlcMapper<string[]>
{

    const int MAX_CONTROLLOGIX_STRING_LENGTH = 82;
    const int MAX_LOGIXPCCC_STRING_LENGTH = 82;

    override public int? ElementSize
    {
        get
        {
            switch (PlcType)
            {
                case PlcType.ControlLogix: return 88;
                case PlcType.Plc5: return 84;
                case PlcType.Slc500: return 84;
                case PlcType.LogixPccc: return 84;
                case PlcType.Micro800: return 256; //To be Confirmed
                case PlcType.MicroLogix: return 84;
                default: throw new NotImplementedException();
            }
        }
    }


    override public string Decode(Tag tag, int offset)
    {
        switch (PlcType)
        {
            case PlcType.Plc5:
            case PlcType.Slc500: 
            case PlcType.LogixPccc: 
            case PlcType.MicroLogix: 
                return LogixPcccDecode(tag, offset);
            case PlcType.Micro800: 
                return Micro800Decode(tag, offset);
            case PlcType.ControlLogix: 
                return ControlLogixDecode(tag, offset);
            default: throw new NotImplementedException();
        }
    }

    override public void Encode(Tag tag, int offset, string value)
    {
        switch (PlcType)
        {
            case PlcType.Plc5:
            case PlcType.Slc500:
            case PlcType.LogixPccc: 
            case PlcType.MicroLogix: 
                LogixPcccEncode(tag, offset, value); break;
            case PlcType.Micro800: 
                Micro800Encode(tag, offset, value); break;
            case PlcType.ControlLogix: 
                ControlLogixEncode(tag, offset, value); break;
            default: break;
        }
    }



    string ControlLogixDecode(Tag tag, int offset)
    {
        var apparentStringLength = tag.GetInt32(offset);

        var actualStringLength = Math.Min(apparentStringLength, MAX_CONTROLLOGIX_STRING_LENGTH);

        var asciiEncodedString = new byte[actualStringLength];
        for (int ii = 0; ii < actualStringLength; ii++)
        {
            asciiEncodedString[ii] = tag.GetUInt8(offset + 4 + 2 + ii);
        }

        return Encoding.ASCII.GetString(asciiEncodedString);
    }

    void ControlLogixEncode(Tag tag, int offset, string value)
    {
        if (value.Length > MAX_CONTROLLOGIX_STRING_LENGTH)
            throw new ArgumentException("String length exceeds maximum for a tag of type STRING");

        var asciiEncodedString = Encoding.ASCII.GetBytes(value);

        tag.SetInt16(offset, Convert.ToInt16(value.Length));

        for (int ii = 0; ii < asciiEncodedString.Length; ii++)
        {
            tag.SetUInt8(offset + 4 + 2 + ii, Convert.ToByte(asciiEncodedString[ii]));
        }
    }


    string Micro800Decode(Tag tag, int offset)
    {
        throw new NotImplementedException();
    }

    void Micro800Encode(Tag tag, int offset, string value)
    {
        throw new NotImplementedException();
    }


    string LogixPcccDecode(Tag tag, int offset)
    {
        var apparentStringLength = (int)tag.GetInt16(offset);

        var actualStringLength = Math.Min(apparentStringLength, MAX_LOGIXPCCC_STRING_LENGTH);

        var readLength = actualStringLength + (actualStringLength % 2); //read 1 more if odd number

        var asciiEncodedString = new byte[actualStringLength];

        for (int ii = 0; ii < readLength - 1; ii+=2)
        {
            asciiEncodedString[ii] = tag.GetUInt8(offset + 2 + ii + 1);
            if ((apparentStringLength % 2 == 0) || (ii < (actualStringLength - 1))) 
                //don't do for last char in odd number string (i.e. don't get the /0)
            {
                asciiEncodedString[ii + 1] = tag.GetUInt8(offset + 2 + ii);
            }
        }

        return Encoding.ASCII.GetString(asciiEncodedString);
    }


    void LogixPcccEncode(Tag tag, int offset, string value)
    {
        if (value.Length > MAX_LOGIXPCCC_STRING_LENGTH)
            throw new ArgumentException("String length exceeds maximum for a tag of type STRING");

        var writeLength = value.Length + (value.Length % 2); //add 1 to write length if odd

        var asciiEncodedString = Encoding.ASCII.GetBytes(value);

        tag.SetInt16(offset, Convert.ToInt16(value.Length));

        for (int ii = 0; ii < (writeLength - 1); ii+=2)
        {
            if ((value.Length % 2 == 0) || (ii < (writeLength - 2)))
            //if odd number string then set penultimate char (1 after string end) as /00
            {
                tag.SetUInt8(offset + 2 + ii, Convert.ToByte(asciiEncodedString[ii + 1]));
            }
            else 
            {
                tag.SetUInt8(offset + 2 + ii, 0x00);
            }
            tag.SetUInt8(offset + 2 + ii + 1, Convert.ToByte(asciiEncodedString[ii]));
        }
    }


}

}

from libplctag.net.

jkoplo avatar jkoplo commented on June 24, 2024

Very nice! Do you want to open a PR for this or do you want me to just copy it from here?

Your timing is good, we are very close to releasing a public beta on nuget that's closer to a first 'stable' release.

from libplctag.net.

IanRobo75 avatar IanRobo75 commented on June 24, 2024

from libplctag.net.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.