Skip to content

MIB Extension Proposal #142

@amcgee

Description

@amcgee

I would like to propose the following extension to the MIB protocol. This extension is intended to support two additional features:

  • Arbitrary buffer sizes for both RPC arguments and return values.
  • Asynchronous callback execution to prevent the bus from locking up if an RPC handler runs for a long time (i.e. turning on the GSM module and acquiring a network signal can take more than 2 minutes, which essentially hamstrings all the other modules for an unreasonable amount of time)

There are a few important considerations in this protocol extension, namely:

Message Size Limitations

Since some chips have very limited RAM, supporting arbitrarily large message sizes can be challenging. To accomodate this limitation, I propose that during the initial RPC handshake the two parties should agree on a maximum "chunk size" to be used when sending the buffer. The caller then sends the message in chunks of this size, waiting for the callee to process each chunk before proceeding.

This could theoretically be done asynchronously as well so as not to lock the bus during message transmission, but this significantly increases the protocol overhead and I think it is more reasonable to expect developers to design the module APIs in a way that minimizes data chunk processing time.

On another note, supporting arbitrary buffer sizes would be problematic with the existing RPC Queue feature on the controller. I propose having a "pointer"-style queue, where each queued RPC's data buffer is saved to flash (if it's long enough), with the first 16 or so bytes actually stored in the in-memory queue structure. The controller can load each subsequent chunk of the stored buffer into memory while the slave is processing the previous one.

In the existing MIB protocol, the param_spec byte can specify up to 3 integer parameters and a buffer parameter of size up to 31 bytes. This is insufficient for our purposes, and I don't think the parameter typing is particularly useful (we already "hack" it multiple places to pass more than 3 integers). I think the callback definition API can grab "typed" arguments even if the parameters are untyped at the MIB protocol layer. Very very basic type-checking can be done by just confirming that the RPC's parameter buffer size matches what the callback expects (i.e. 4 bytes for 2 integer parameters). Removing this protocol-layer type enforcement means we have a full 8 bits to specify message length, bumping our maximum to 255 which seems reasonable.

Callback state

Supporting asynchronous RPCs requires state about previous MIB calls to be stored by the individual modules. The callee needs to know what callback address to hit when the handler has finished doing its thing. This should be straightforward on the 16-bit chips because they already have state in the RPC queue (it will just need to support removal of elements that aren't at the "top" of the queue), and I think 8-bit chips should only need to support one callback at a time. The easiest way to implement this should be to reserve a particular feature and command (say 0xFF:0xFF, or something less banal) as the universal "callback address", and have it expect the first byte of the callback data to be the "identifier" of the RPC as specified by the original call.

The Heart of the Matter

The "arbitrary message length" protocol extension

  1. The caller (master) sends <start write>+(address, feature, command, message_size)+checksum+<repeated start read> onto the bus
  2. The callee (slave at the specified address) receivs this, checks to make sure the feature/command is legit. If this check succeeds, it responds with (max_chunk_size)+checksum, otherwise (0,error_code)+checksum
  3. Now the caller knows the max chunk size supported by the callee. It then starts sending chunks of size min(caller_max_chunk_size, callee_max_chunk_size). A chunk with size n look like this: <r. start write>+(data[n])+checksum+<r. start read>.
  4. The slave receives this chunk, buffers it in memory, then performs a clock stretch while it is processed. A syntax for defining the "process chunk" handler is proposed below.
  5. When the slave is done processing, it releases the clock and sends an ack/nack byte.
  6. (4) and (5) are repeated until the entire message has been sent. Since the callee knows the entire message size, it should now know that the message phase is complete. If it does not, it can determine as much by noticing that the caller follows the last ack/nack with a <r. start read> (below) instead of <r. start write> (above).
  7. At this point the entire arbitrary-length message had been received and the Async extension (below) takes over.

After the RPC handler has executed, the return logic could also use this same method to support arbitrary return value buffer size.

The "asynchronous RPC" protocol extension

Once the RPC call has been made, it would traditionally be the callee's turn to execute the callback and specify a return status+value. This can still supported by the extension (advantageous for handlers that run quickly and would suffer from the additional overhead of orchestrating the callback).

  1. After the initial call specifications and parameter buffer have been sent (above), the caller sends a <repeated start read>
  2. The slave can now start executing the RPC handler, but if it is taking a while (or this is predetermined) then it can send a "pending" return status. If the handler finishes, the return status is known and no callback negotiation is necessary. Instead of a bit-packed return status + return value length, the callee should always send a full byte of return status. This allows for more fine-grained status codes (including some reserved as handler-specified error codes) and arbitrarily long response buffers.
  3. If the return status was anything but "pending", the callee now sends the return value size followed by the return value itself, chunked as above (the chunk size is known, so it shouldn't need to be re-negotiated. Otherwise, the following steps are performed
  4. Now that we know the return value is pending, the caller needs to give the callee a way to notify it of completion later. The caller sends <r. start write>(origin_address, rpc_identifier)+checksum, where origin_address is the caller's i2c address and rpc_identifier is the ID the caller has assigned to this "pending" call.
  5. The callee stores these values and waits for the handler to finish, and the caller releases the bus.
  6. (some time passes, while other RPCs can be executing)
  7. Now roles are reversed, and the former callee makes a new RPC call to the former caller, specifying (origin_address, callback_feature, callback_command) where callback_feature and callback_command are well-known, and specifying the rpc_identifier as the first byte of the data buffer.
  8. The callback handler should never return a "pending" status, though if this were allowed it might be able to support a possibly interesting "shared session" feature. Or maybe not.

Handler Definition Syntax

To facilitate processing long messages in small-sized chunks, the following C API could be used (written assuming 16-bit compiler, it would have to be optimized for 8-bit:

typedef struct {
    uint8 length;
    BYTE* data;
} MIB_BUFFER;

uint8 mib_read_integer(); // reads 2 bytes from the beginning of the data buffer, and increments the data buffer pointer.  If this reads the last byte of cached data, receives a new chunk from the wire.
MIB_BUFFER mib_read_buffer_chunk(); // returns a pointer to the cached buffer and the length of that buffer.
bool mib_next_buffer_chunk(); // collects the next chunk of data off the wire.  Returns false if no more chunks exist.
void force_synchronous_handler(); // Otherwise we will automatically go async when the last chunk has been sent.

void mib_handler(void) { // Arguments are not passed directly to the function
    // First, read some integer parameters.  These can automatically perform
    //  chunking in the background and change 
    uint8 x = mib_read_integer();
    uint8 y = mib_read_integer();
    while ( mib_next_buffer_chunk() ) {
        MIB_BUFFER buffer = mib_read_buffer_chunk();
        write_to_flash( buffer.data, buffer.length ); // pseudo-function used to exemplify streaming
    }
    // Now we're asynchronous, this task can run as long as it wants.  Ideally there would be an extension to the API to allow such long-running handlers to, i.e., not block the task loop on pic24, but that's a project for another day.
}

And that's it. Let me know if anything is unclear (I'm sure something is).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions