I think I understand now better after your comment. What you need is not a binary to ASCII-string conversion, but a binary transmission format.
Note that this is an XY-problem: asking for X, but requiring Y.
For this, you just have to preceed each data element with a type-prefix (byte).
// This enum must be known to all transmitters and receivers, of course!
typedef enum {
DATA_TYPE_uint16 = 0,
DATA_TYPE_int8,
...
} DataType;
So, for instance, an uint16_t is transfered as:
uint8_t tx_buffer[MAX_BUFFER_SIZE];
uint16_t internal_var1;
...
tx_buffer[0] = DATA_TYPE_uint16; // type being sent
tx_buffer[1] = (uint8_t)internal_var1; // lower 8 bits
tx_buffer[2] = (uint8_t)(internal_var1>>8); // lower 8 bits
send_i2c(3, tx_buffer);
Receivers' code:
uint8_t rx_buffer[MAX_BUFFER_SIZE];
uint16_t internal_var1;
// read the next frame (i2c delimts frames automatically)
receive(MAX_BUFFER_SIZE, rx_buffer); // do not overflow the buffer
switch ( rx_buffer[0] ) {
case DATA_TYPE_uint16:
internal_var1 = (uint16_t)rx_buffer[1] | (uint16_t)rx_buffer[2] << 8;
break;
...
default:
// invalid frame format (error handling!)
}
Note that the data is sent little endian (lowest byte first). This is common practice in embedded programming. It is much more compact (i.e. faster sent) than packing each bit into a byte and much easier&faster to process on either side.
You can extend this to any kind of datatype actually. So you can send a struct as a single type by packing its single elements as I showed for the uint16_t. Do not follow the temptation to pass data as a binary string! This is the way to desaster in communication protocols.
This is actually called "marshalling" (serializing structured data). Note to use only portable operations.