-
Notifications
You must be signed in to change notification settings - Fork 224
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
API addition: Support strings more cleanly across all PLC types #162
Comments
How would the offset be used in the case of string arrays? Set elem_size to let the application know how big a fixed size string is? While low friction for apps, this may be too high level for the core library. |
Another possible variant of this would be to rewire the existing getters and setters to handle the byte swapping etc. automatically. This gets challenging when a string is a field within a UDT tag. That alone argues for new API calls. |
Perhaps a different, slightly lower level set of API calls: 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 is a little clunky, but does not require any additional memory allocation or in place data transformation. The tag buffer would still contain unchanged raw PLC data. This would allow configuration of the size of the count element, if there was one so that it can be translated in get/set string size. It would allow handling for byte-swapping of the string character indexes. It abstracts the underlying byte representation sufficiently that generic code can be written to work with any PLC type at the application level. |
Yep looks good, are UTF-16 or 32 ever used? |
There are string types that support wide characters, which might be some form of UTF-16, but I have not seen them in practice. I think we can continue to support them using the same API. UTF-32 seems to be non-existent. I have never seen it used. We can relax the restrictions on |
There may be some need to determine the maximum capacity of a string in characters. For instance PLC/5 through ControlLogix support strings up to 82 characters. However, I think that Micro800 PLCs may support up to 255. This is very much PLC specific. We also need a way to skip past a string. So at least two more API functions: int plc_tag_get_string_capacity(int32_t tag_id, int string_start_offset);
int plc_tag_get_string_total_length(int32_t tag_id, int string_start_offset); Again, there are some caveats. The string values in the raw data from a tag listing do NOT conform to a fixed size. I really want to find a solution that will work for those as well. If I can figure that out, then even tag listing is almost completely supported by the library rather than partially as now. This needs more thought. There is a bit of an API explosion due to strings and I am not sure that this makes sense without also thinking about UDTs in general. |
@kyle-github - one option here we could use is to scope out the feature using a wrapper, and then as it becomes clear what to do, swap out the functionality into core project. I'm not super familiar with it but web browsers do this with javascript polyfills. What do you think? |
We could even release it as a separate package so it doesn't cloud up the "real" wrapper. |
@timyhac I tend to agree with the idea of "prototyping" it in a wrapper library first. Right now I am going down a bit of a rabbit hole. I had some thoughts based on the string API above, and realized that I was starting toward a useful extension that would support a lot of UDT use. I am still thinking through it. Hopefully a bit later today I will have something to show and then I can see if it is worth it or not. |
I am dropping the UDT ideas. They'll work and will be feasible but they introduce two completely different APIs. They should be done as either a different library based on the core library or as a parallel API. And the cost of supporting generic UDTs is quite high. I am leaning toward string support in the core library. I do like the idea of prototyping it either in a wrapper or in higher-level C extension library. My reasoning is this: Pro:
Con:
Overall I think the argument that every wrapper will need to implement something is a strong one. While the API I have above will not be the easiest to use directly, it will make wrapper library implementations of easy string use much nicer and hopefully not PLC specific. But it is not as clear cut as the difference between UDTs and primitive types. I think the API still needs some thought. |
The frustrating thing here is that the original API idea using C strings is so much easier to use. The problem with it is memory handling. Writing code using the in-place character-oriented API functions is not much different from the existing API. There is less knowledge required of the PLC data representation, which is the whole point. |
I think I am overthinking this to some extent. There are only a few different attributes of strings that are common:
The key thing that makes this simple is that all this only needs to apply per tag. I do not have to solve this for all tags at once. Each tag has a specific string representation. There could be UDTs with multiple string types included, but I am perfectly fine handing that right back to the end application creator as their problem to solve. Just handling this with one string representation per tag solves the 95% case easily. This also lets me solve the tag listing raw data problem I wanted to solve. For instance
That would describe a PCCC string. The format Suitable defaults for the string format would work for most cases for all tags in a PLC. This makes adding six more API functions more palatable. |
To me the big challenge is to support variable-size tags. Supporting strings themselves aren't a major issue (even if they come in various formats) - it is that we don't know what the elementSize is (on some CPUs), this issue is compounded for arrays or structures. Is my understanding correct here? |
Yes, that is one of the driving problems. At least with Control/CompactLogix systems, the elem_size is something we can skip when defining the tag. The PLC will let us know how big the tag is simply by returning all the elements we ask for. The driving problem for me on this is handling the raw data from tag listing. That has variable length strings that do not have a constant capacity allocated. The string is exactly the length specified without any extra capacity. I think I can make the string support functions I have above (the six character-based ones) work with both constant capacity strings (aka STRING data type) and compact variable length counted strings as we see in the raw tag listing data. But the latter is where I need to have the I do not want to get sucked back into the UDT rabbit hole, so I want to keep this a simple as possible. I think just supporting one string format per tag is going to solve most of the cases. It works for the tag listing raw data. It works for arrays of STRING. It works for many UDTs where either there is a single string type or the user set up the UDT using the system STRING type. It won't work for UDTs where there is more than one string type in use at the same time. For the time being, we can pre-define some common string types so that the
Just those three would solve almost all string handling for AB PLCs. I came up with another syntax for this for my UDT ideas and could borrow it here. These three would be:
At least initially, I think the named string types will work fine as I determine the best way to describe them internally. |
This has been implemented and tested as best I can in the |
And this went nowhere... It is just not simple enough. I have determined how to delete memory allocated in the native library. So I can implement something like the original API. The new API would look like this: char *plc_tag_get_string(int32_t tag_id, int string_start_offset);
int plc_tag_set_string(int32_t tag_id, int string_start_offset, const char *string_val);
int plc_tag_get_string_capacity(int32_t tag_id, int string_start_offset);
int plc_tag_get_string_total_length(int32_t tag_id, int string_start_offset); Any language wrapper would be responsible for freeing the memory allocated in the core library in the first function above. Nearly every language has some form of FFI that will handle C strings, so we do not need functions to get or set the string length. That can be done implicitly via the length of the C string. We need the third and fourth functions as those can vary even within a single PLC. For example, when listing tags in a ControlLogix, the symbol names are counted strings using a 16-bit count and variable length string data bytes. This is very different from a STRING type in the same PLC! |
So that is not going to work either. C#/.Net makes this hard as you cannot easily call Perhaps put all the memory handling in the wrapper? int plc_tag_get_string(int32_t tag_id, int string_start_offset, char *buffer, int buffer_size);
int plc_tag_set_string(int32_t tag_id, int string_start_offset, const char *string_val);
int plc_tag_get_string_capacity(int32_t tag_id, int string_start_offset);
int plc_tag_get_string_total_length(int32_t tag_id, int string_start_offset); First you call Another option is to expose a free function: char *plc_tag_get_string(int32_t tag_id, int string_start_offset);
int plc_tag_free_string_buffer(char *buffer);
int plc_tag_set_string(int32_t tag_id, int string_start_offset, const char *string_val);
int plc_tag_get_string_capacity(int32_t tag_id, int string_start_offset);
int plc_tag_get_string_total_length(int32_t tag_id, int string_start_offset); That allow you to free without knowing how long the string is in advance. |
As surfaced in the .Net wrapper discussion, setting one byte at a time to zero out the remaining bytes of a string might be fairly slow/heavy on platforms with unoptimized mutexes. So add a function to the API to set a range of bytes just like /* base string API */
int plc_tag_get_string(int32_t tag_id, int string_start_offset, char *buffer);
int plc_tag_set_string(int32_t tag_id, int string_start_offset, const char *string_val);
int plc_tag_get_string_capacity(int32_t tag_id, int string_start_offset);
int plc_tag_get_string_total_length(int32_t tag_id, int string_start_offset);
/* byte range/raw byte API */
int plc_tag_clear_bytes(int32_t tag_id, int start_offset, int range_length, uint8 val);
int plc_tag_get_bytes(int32_t tag_id, int start_offset, int range_length, uint8_t *destination_buffer);
int plc_tag_set_bytes(int32_t tag_id, int start_offset, int range_length, uint8_t *source_buffer);
To be continued... |
I'll probably do this in two stages. The first will implement just the string part of the API. If there is any actual need then I will implement the second part with the raw byte handling. Once we have strings, we will be able to directly handle almost all primitive types so the overhead of the mutex will not be too terrible. |
There should be a change to the getter. It makes sense to have the getter take a parameter for the length of the buffer from the application. One open question is whether the getter should zero-terminate the string or not. Since the intent is to provide a C string, then it seems like the getter should. But at the same time, the string length is known to the application code so it can zero terminate. The UX for having the getter terminate is better so I am leaning that way. Here is what the getter would look like with the buffer length: int plc_tag_get_string(int32_t tag_id, int string_start_offset, char *buffer, int buffer_length); This allows the application/wrapper to help ensure that the buffer is not overrun. The guarantee by the library will be that no more than |
In progress. Should be done soon. Might need to add an additional API function to get the string length. |
I think this issue can be closed, right? |
D'oh, yes. Yes, indeed. Thanks for the catch! Closed with PR #227. |
Support strings as a "native" type of the library, in the API. For example:
As with other calls, the
offset
parameter is in bytes. For the getter, return NULL on failure either due to lack of data, an out of bounds offset or other error.Note that this will require some sort of internal byte swapping and other handling to be done as some PLCs return raw string data in various forms and orders.
Challenges:
The text was updated successfully, but these errors were encountered: