So at the highest level, you have a function that handles the raw message from the wire, and does some initial validation and maybe sends a NACK if it finds a problem with the message (like a checksum failure or an invalid message type); this would be the highest level message handler.
Personally I would probably refer to this as the lowest level handler - if you were to follow a similar kind of layered approach as the OSI model for example.
The physical aspects of a network are considered to be the lowest level, while things like protocols and applications are increasingly higher levels respectively.
Yes, I mentioned this in the paragraph after the one you quoted
Anyway it's tangential to the point I was making since I was talking about code organization, not protocol structure (which has not been given). Even within one layer of the OSI model, given any complexity you likely end up having some sort of `receiveMessage()` function at the top level of a module (the part that's exposed as the API) which dispatches messages to more specific functions like `receiveFooMessage()` and `receiveBarMessage()`, and the latter may further delegate processing to `receiveBarMessageSubtypeBaz()`.
One way to get rid of the switch statement may be dynamic class loading. Don't know how well it works in C, but I know it would work in Java.
In Java I would look for classes then load them up into a HashMap. The switch statement would be replaced by looking for "commands" in the HashMap.
Here is some info on C
https://pubs.opengroup.org/onlinepubs/009695399/functions/dlsym.html
C doesn't have dynamic class loading because it doesn't have classes
What you'd typically do instead is have each code module provide some sort of description (usually a struct) of the functionality it offers and then register that description with some sort of dispatcher at init. That dispatcher then needs to have some way of figuring out how to resolve events or invocations to functionalities from the correct module. This does require coordination between the module being loaded and the dispatcher--the description of functionality has to be an object type that the dispatcher understands, so probably a struct type defined by the dispatcher module--so it's generally specific to a type of functionality. A sort of related example would be the way that LWIP handles network interfaces, where an implementation provides an instance of the `netif` struct (defined by LWIP) that holds state information as well as driver functions for a given network interface. That struct gets handed off to LWIP which then handles the interface's state and invokes its drivers as necessary (usually indirectly via calls into the APIs for the other modules in the stack).
This obviously isn't as flexible or as expressive as what you can do in object oriented languages, but generally works well in embedded applications.
The payload for each Class and ID, which identifies what the packet holds/does, has fields aligned to 4-byte boundaries, so the payload can easily be mapped to a union of structures to pick out the data.
So one small state machine to receive the packet, and if the checksums match, then it's a single switch statement for each Class and ID to extract the data directly from the structures.
This is worth highlighting, because the layout of structures in memory is not guaranteed to be contiguous. So in structures that have mixed types with different storage sizes you can end up with padding that will make the struct layout differ from the packet layout. Depending on the platform/compiler/language version there are some options to control this, but it can trip you up if you don't account for it. If you have the same platform at both ends you can sometimes ignore this and just copy the struct from memory straight into the message byte-for-byte, but if you need to communicate between different platforms (or even software versions) it's probably better to be a bit more deliberate, which may involve manually marshalling fields from whatever data object you have in memory into and out of the message buffer.
My other question is each handler function. I guess there are two ways to implement it. One where you parse every byte coming in and one where you parse it only after the entire message has been received (obviously assuming you know the length from the first (or first few) bytes.
Any suggestions, pros, cons for each solution?
Thank you
There are a couple of cons to doing the byte-for-byte thing:
- if you are handing off each byte to a specific handler as it comes in, then you need to first identify which handler to invoke, which probably means you need a generic handler to parse the header and make that handoff
- the handoff between the generic handler and the specific one requires relaying any necessary state, which may be more or less of a problem depending on the protocol complexity
- the specific handler shouldn't do anything with the message contents until it knows the message is valid, which requires the whole message to be received so the checksum can be validated
- each specific handler has to be able to validate the checksum
- if the specific handler DOES do anything with the partial message, you may need to be able to undo those things if the message turns out to be invalid
So given those there's not much point to handing each byte off to a specific handler. It's going to generally be a lot simpler to have one function that gets and validates the complete message and then hands the whole thing off to the appropriate specific handler.
As to whether that initial handler should get a byte at a time, that depends on the protocol. If you have variable message lengths, or addressing that needs to be checked, you probably need to hand it a byte at a time at least for the header so it can figure out how many more bytes to wait for, and maybe it also checks the destination address and just ignores the rest of the message if it doesn't match. Once it knows the destination is correct and how many bytes to wait for you could then switch to DMA for the rest of the message, or continue to process each byte as it comes in. The latter may make sense if you need to enforce an inter-byte timeout or something, or a per-byte ACK.