-
-
Notifications
You must be signed in to change notification settings - Fork 47
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
Packet builder #209
base: main
Are you sure you want to change the base?
Packet builder #209
Conversation
I don't know where the best place for this is. I've not committed any tests (but did run some and am using it locally). This is mostly just the initial basic algorithm. And even within that I'm sure some parts could be compressed - there are a few bits of repeated code, especially in the sections handing large data spanning multiple bytes, but honestly I don't think rearranging the code is worth it to save a few lines at the expense of complexity. |
Two big formatting things I've noticed that probably need changing - I've used |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made a first pass through it. Generally I think the library could use something that is similar to this, but it seems like a number of your code's assumptions have made their way into here, and I have concerns about it not working correctly depending on a machine's endianness. It also isn't quite generic enough to fit a library use-case, up to the ETP protocol size limit.
So, I guess where we go from there is kind of up to you. This kind of thing is an ideal candidate for heavy unit testing since it's easy to provide lots of stateless test cases, so if you want to keep working on it and improving it, and if you can write unit tests for it, I think we could revisit adding it in. Or, if you think it's meeting your needs in your software, you are of course welcome to keep using it in your fork.
It can be tough to convey emotion through a GitHub PR review, so I just wanted to say that I do appreciate all your PR submissions, including this one. I probably spent at least a good hour doing this review (it takes time to read the code to come up-to-speed on what it's doing), which I wouldn't do if I didn't think it was valuable.
@@ -0,0 +1,503 @@ | |||
#pragma once |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do add this file, it would need a header comment block like the rest of the files, and generally it would need doxygen style comments for all members, the file, and the class itself so that the doxygen generates with no warnings. If you have doxygen installed, you can check this by running doxygen doxyfile
at the root of the repo and checking for errors in the output.
// I wrote a huge amount of this file once, then somehow it vanished. I don't know how! | ||
class ParameterGroupBuilder | ||
{ | ||
private: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer public stuff at the top. When someone comes to look at the class to learn the interface, they'll start at the top of the file, so making people scroll is just wasting their time.
{ | ||
private: | ||
std::size_t writeOffset = 0; | ||
std::size_t readOffset = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess technically the max message length is 117440505, so a std::uint32_t should be enough. I would suggest using std::uint32_t
instead if possible, since size_t
may be smaller than 32 bits on some (admittedly small) platforms.
|
||
bool write_bits(std::uint8_t const * data, std::size_t bits) | ||
{ | ||
if (bits == 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, prefer the constant on the left of an ==
. This is sort of a MISRA thing, where you reduce the likelihood of mis-typing =
instead of ==
, because =
in that case would not compile instead of creating a runtime bug where =
is always true.
} | ||
|
||
public: | ||
ParameterGroupBuilder() : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can probably be = default
// boundaries, and how to align the bits when a data-type that is not a multiple of eight bits | ||
// crosses said boundaries. Then every single parameter group uses padding to avoid those | ||
// cases! The five-bit tractor command types have three bits of padding to make them exactly | ||
// one byte. Why even specify the scheme if it isn't used? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They do a good job of padding, that is true, but I have often seen SPNs span bytes in an unaligned fashion in proprietary messaging, so it is more common than you might think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more a rant than anything. the code does handle byte spans correctly (at least I believe it does).
template <typename T> | ||
bool read(T & data) | ||
{ | ||
return read_bits((std::uint8_t *)&data, sizeof (T) * 8); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Casting to uint8_t *
from T is not endianness friendly, plus it's a C style cast which should not be used. This happens multiple places, and may require a fairly substantial refactor to fully resolve.
bool read<char *>(char * & data) | ||
{ | ||
// Read until NULL. | ||
return read<std::uint8_t*>((std::uint8_t * &)data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like a dangerous assumption that a uint8_t *
will have null termination here and other places. That's a non-trivial assumption that cannot be enforced. std::vector<std::uint8_t>
is not a std::string()::c_str()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it is, and I know how that bad assumption sneaked in (I did a find/replace on unsigned char
to uint8_t
). TBH I was already unhappy with the way strings were handled in here, so I'll have to revisit that entire part.
while (bits) | ||
{ | ||
// Move in the new data. | ||
*data = (*data & (255 >> space)) | ((buffer[byte] >> input) << output); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
255 in a few places can be replaced with a constant in <limits>
Kudos, SonarCloud Quality Gate passed! 0 Bugs No Coverage information |
Thanks for the comments. I realised it was very far from the rest of the code style, sorry about that. While I'm happy to work on the code style changes, are there any more generic comments about the general approach/API/etc?
And I appreciate your time and effort for reviews. Don't worry, I didn't infer any ill intent/bad emotions at all; I've been on your side of code reviews plenty of times before. |
Overall I think the API is pretty intuitive, you build up a packet sequentially and likewise disassemble it sequentially, and it hides the masking and shifting well. "pad"and "skip" and even "read" and "write" might be not the most clear on what's going on but the core of it is all there for sure. |
Another draft PR for comments, since it is a larger one. I've basically written a class to do all the maths and position tracking for building up packets with lots of compressed data in.
The current code has lots of instances of code to build packets like:
Or deconstruct packets like:
This is (IMHO) very complex to read and error-prone, though admittedly it is very explicit in what is happening and where things are being stored.
Anyway, those two examples rewritten with this PR would look something like:
Might just need a tiny bit of tweaking for types/widths on some of the variables, but otherwise that's complete.
Now I've already mixed up
pad
(for writing) andskip
(for reading) in my own code, so it might make more sense to have the reading and writing capabilities split in to two different classes, but the core algorithms are there. Interesting, one thing I noticed is that the spec goes in to some detail about how odd-bit-width data spans byte boundaries, and this code correctly implements that, but then all the PGNs in the spec use padding bits to explicitly avoid those cases. And thus this code also checks for those common cases where data is byte-aligned or fully contained within a byte to better optimise for those cases.