I've spent a fair amount of time examining the Arduino library code as it was supplied by Spinmaster. Here's what I've learned so far.
The communicate() routine seems designed to be called constantly, probably in the sketch's loop() routine. Each time it's called it would elicit a return value from one module on the bus. Hence, it would have to be called 4 times to ensure that the module that you wanted to hear from would get the chance to reply.
The various high level routines (set colour, set position, set servo to LIM mode) didn't call communicate() internally. They would just set up values in the output data array, apparently relying on the sketch to continually poll the communicate() routine.
The first modification I tried was having the high level routine directly call the communicate() routine, passing the module number that I wanted to hear from. communicate() would then send a single data packet and pass back the return value. It all sounded great in theory but it didn't work. It seems that you have to treat the bus as the thing you're communicating with rather than an individual module. So all four modules on the bus have to be polled in turn.
A further modification that does appear to work involved multi byte commands being sent to an individual module. For example, I was able to have communicate() poll a module, ask for its type and finally set its colour (3 data packets for a servo, 4 for an LED) before moving on to the next module. Getting that to work simplified things.
A quirk of the protocol means that it takes longer tao talk to a bus with only a single module on it than it does to talk to a fully populated (4 modules) bus. The timeouts waiting to hear from a module that isn't there take a significant amount of time.
I discovered this when I had each high level routine call communicate() up front before setting its values and calling communicate() a second time. It may seem like an odd thing to do, but what it did was allow for hot plugging modules onto the bus. But it really slowed things down. I ended up only making the up front call if no modules were detected on the bus after the last call. It accomplished exactly the same thing and was a whole lot faster.
The LED module returns its module type (0x02) each time a set colour command is issued.
The servo module returns the actual position of the servo (0x04 to 0xF9) after either a set colour, set position or set to LIM command. The original library code would only send 0x18 to 0xE8 for position data but I modified mine to be able to send the whole range. It seems to work.
Since neither module appears capable of returning a value of 0x03, I'm assuming that it's been reserved for identifying a future type of smart module. Additionally, 0xFB isn't used for either a command or a response so that may well be reserved as a command for that hypothetical future smart module type. I don't know what (if anything) Spinmaster might have in mind for that hypothetical module type, but something that could read and report on some sort of sensor (optical, electrical, or mechanical contact, etc.) would be a huge plus. As things stand now the only input we can read is each servo's position. With an input module, we'd have the ability to move a servo in response to an input signal (think photocell or microswitch, for example).