// An implementation of the MIDI Capability Inquiry v1.2 Specification, over a MIDI 1.0 transport, // with only Property Exchange support. // Intended to be read alongside the corresponding specs; specifically in the order: // - MIDI CI Specification, minimum requirements + Property Exchange chapter, // - Common Rules for MIDI CI Property Exchange. #define DISABLE true #ifndef DISABLE #include "base_midi.h" #include "parson.h" uint32_t our_muid = rand(); // TODO: think. uint8_t request_id = 0; #define MIDI_CI_SUBID 0x0d #define PROFILE_CONFIGURATION_MASK 0x20 #define PROPERTY_EXCHANGE_MASK 0x30 #define PROCESS_INQUIRY_MASK 0x40 #define MANAGEMENT_MASK 0x70 #define MIDI_CI_VERSION 0x02 #define TO_FUNCTION_BLOCK_ID 0x7f static void ci(uint8_t source_target, uint8_t message_type, uint32_t destination_miud, uint8_t *contents, size_t contents_len) { uint8_t *new = malloc(1 + 4 + 4 + contents_len); new[0] = MIDI_CI_VERSION; new[1] = our_muid & 0x00'00'00'ff; new[2] = (our_muid >> 8) & 0x00'00'00'ff; new[3] = (our_muid >> 16) & 0x00'00'00'ff; new[4] = (our_muid >> 24) & 0x00'00'00'ff; new[8] = target_muid & 0x00'00'00'ff; new[7] = (target_muid >> 8) & 0x00'00'00'ff; new[6] = (target_muid >> 16) & 0x00'00'00'ff; new[5] = (target_muid >> 24) & 0x00'00'00'ff; memcpy(new + 9, contents, contents_len); universal_nonrealtime(source_target, (MIDI_CI_SUBID << 8) | message_type, contents, 1 + 4 + 4 + contents_len); free(new); } #define DISCOVERY_SUBID (MANAGEMENT_MASK | 0x00) #define CI_CATEGORY_SUPPORTED 0b00011100 #define MAX_SYSEX_SIZE 512 #define BROADCAST_MUID 0x7f'7f'7f'7f #define SINGLE_OUTPUT_PATH 0x00 void discovery() { uint8_t *new = malloc(3 + 2 + 2 + 4 + 1 + 4 + 1); #if (DEVICE_MANUFACTURER <= MAX_DATA_BYTE) new[0] = DEVICE_MANUFACTURER; new[1] = 0; new[2] = 0; #else new[0] = 0; new[1] = DEVICE_MANUFACTURER >> 8; new[2] = DEVICE_MANUFACTURER & 0x00ff; #endif new[3] = DEVICE_FAMILY & 0x00ff; new[4] = DEVICE_FAMILY >> 8; new[5] = DEVICE_MODEL & 0x00ff; new[6] = DEVICE_MODEL >> 8; new[7] = SOFTWARE_REVISION & 0x00'00'00'ff; new[8] = (SOFTWARE_REVISION >> 8) & 0x00'00'00'ff; new[9] = (SOFTWARE_REVISION >> 16) & 0x00'00'00'ff; new[10] = (SOFTWARE_REVISION >> 24) & 0x00'00'00'ff; new[11] = CI_CATEGORY_SUPPORTED; new[12] = MAX_SYSEX_SIZE & 0x00'00'00'ff; new[13] = (MAX_SYSEX_SIZE >> 8) & 0x00'00'00'ff; new[14] = (MAX_SYSEX_SIZE >> 16) & 0x00'00'00'ff; new[15] = (MAX_SYSEX_SIZE >> 24) & 0x00'00'00'ff; new[16] = SINGLE_OUTPUT_PATH; ci(TO_FUNCTION_BLOCK_ID, DISCOVERY_SUBID, BROADCAST_MUID, new, 17); free(new); } #define REPLY_TO_DISCOVERY_SUBID 0x71 #define NO_FUNCTION_BLOCK 0x7f void discovery_reply(uint8_t initiator_muid, uint8_t initiator_output_path_id) { uint8_t *new = malloc(3 + 2 + 2 + 4 + 1 + 4 + 1 + 1); #if (DEVICE_MANUFACTURER <= MAX_DATA_BYTE) new[0] = DEVICE_MANUFACTURER; new[1] = 0; new[2] = 0; #else new[0] = 0; new[1] = DEVICE_MANUFACTURER >> 8; new[2] = DEVICE_MANUFACTURER & 0x00ff; #endif new[3] = DEVICE_FAMILY & 0x00ff; new[4] = DEVICE_FAMILY >> 8; new[5] = DEVICE_MODEL & 0x00ff; new[6] = DEVICE_MODEL >> 8; new[7] = SOFTWARE_REVISION & 0x00'00'00'ff; new[8] = (SOFTWARE_REVISION >> 8) & 0x00'00'00'ff; new[9] = (SOFTWARE_REVISION >> 16) & 0x00'00'00'ff; new[10] = (SOFTWARE_REVISION >> 24) & 0x00'00'00'ff; new[11] = CI_CATEGORY_SUPPORTED; new[12] = MAX_SYSEX_SIZE & 0x00'00'00'ff; new[13] = (MAX_SYSEX_SIZE >> 8) & 0x00'00'00'ff; new[14] = (MAX_SYSEX_SIZE >> 16) & 0x00'00'00'ff; new[15] = (MAX_SYSEX_SIZE >> 24) & 0x00'00'00'ff; new[16] = initiator_output_path_id; new[17] = NO_FUNCTION_BLOCK; ci(TO_FUNCTION_BLOCK_ID, REPLY_TO_DISCOVERY_SUBID, initiator_muid, new, 18); free(new); } // TODO: should we set a new MUID inside this function? #define INVALIDATE_MUID_SUBID 0x7e void invalidate_muid(uint8_t muid) { uint8_t *new = malloc(4); new[0] = muid & 0x00'00'00'ff; new[1] = (muid >> 8) & 0x00'00'00'ff; new[2] = (muid >> 16) & 0x00'00'00'ff; new[3] = (muid >> 24) & 0x00'00'00'ff; ci(TO_FUNCTION_BLOCK_ID, BROADCAST_MUID, new, 4); free(new); } static inline void invalidate_our_muid() { invalidate_muid(our_muid); } // TODO: consider structifying? #define ACK_SUBID 0x7d void ci_ack(uint8_t acking_device_id, uint8_t acking_muid, uint8_t original_classification, uint8_t status_code, uint8_t status_data, uint8_t details1, uint8_t details2, uint8_t details3, uint8_t details4, uint8_t details5, uint16_t length, uint8_t *text) { uint8_t *new = malloc(1 + 1 + 1 + 5 + 2 + length); new[0] = original_classification; new[1] = status_code; new[2] = status_data; new[3] = details1; new[4] = details2; new[5] = details3; new[6] = details4; new[7] = details5; new[8] = length & 0x00ff; new[9] = length >> 8; memcpy(new + 10, text, length); ci(acking_device_id, ACK_SUBID, acking_muid, new, 1 + 1 + 1 + 5 + 2 + length); free(new); } #define NAK_SUBID 0x7f void ci_nack(uint8_t naking_device_id, uint8_t naking_muid, uint8_t original_classification, uint8_t status_code, uint8_t status_data, uint8_t details1, uint8_t details2, uint8_t details3, uint8_t details4, uint8_t details5, uint16_t length, uint8_t *text) { uint8_t *new = malloc(1 + 1 + 1 + 5 + 2 + length); new[0] = original_classification; new[1] = status_code; new[2] = status_data; new[3] = details1; new[4] = details2; new[5] = details3; new[6] = details4; new[7] = details5; new[8] = length & 0x00ff; new[9] = length >> 8; memcpy(new + 10, text, length); ci(naking_device_id, NAK_SUBID, naking_muid, new, 1 + 1 + 1 + 5 + 2 + length); free(new); } #define WHOLE_FUNCTION_BLOCK_ID 0x0f #define PROPERTY_EXCHANGE_MAJOR_VERSION 0x00 #define PROPERTY_EXCHANGE_MINOR_VERSION 0x00 #define SIMULTANEOUS_REQUESTS 0x01 #define INQUIRE_EXCHANGE_CAPABILITIES_SUBID 0x30 void inquire_property_exchange_capabilities(uint8_t destination_muid) { uint8_t *new = malloc(3); new[0] = SIMULTANEOUS_REQUESTS; new[1] = PROPERTY_EXCHANGE_MAJOR_VERSION; new[2] = PROPERTY_EXCHANGE_MINOR_VERSION; ci(WHOLE_FUNCTION_BLOCK_ID, INQUIRE_EXCHANGE_CAPABILITIES_SUBID, destination_muid, new, 3); free(new); } #define REPLY_EXCHANGE_CAPABILITIES_SUBID 0x31 void reply_proprty_exchange_capabilities (uint8_t initiator_muid) { uint8_t *new = malloc(3); new[0] = SIMULTANEOUS_REQUESTS; new[1] = PROPERTY_EXCHANGE_MAJOR_VERSION; new[2] = PROPERTY_EXCHANGE_MINOR_VERSION; ci(WHOLE_FUNCTION_BLOCK_ID, REPLY_EXCHANGE_CAPABILITIES_SUBID, initiator_muid, new, 3); free(new); } // TODO: complain; standard doesn't specify what goes into the "Max SysEx size." E.g. do I include the EOX? #define CI_MESSAGE_NONVARIABLE_SIZE (1 + 1 + 1 + 1 + 1 + 1 + 4 + 4 + 1 + 2 + 2 + 2 + 2 + 1) #define min(X, Y) ((X) < (Y) ? (X) : (Y)) static void chunked_property_exchange_send(uint32_t receiver_max_sysex, uint8_t message_subid, uint32_t destination_muid, uint8_t request_id, uint8_t *header_data, uint16_t header_len, uint8_t *property_data, uint16_t property_len) { uint32_t total_len = CI_MESSAGE_NONVARIABLE_SIZE + header_len + property_len; uint32_t leftover_bytes = total_len % receiver_max_sysex; uint32_t chunk_count = total_len / receiver_max_sysex + (leftover_bytes == 0 ? 0 : 1); uint32_t leftover_property_bytes = leftover_bytes - CI_MESSAGE_NONVARIABLE_SIZE; uint32_t max_per_message_property_bytes = receiver_max_sysex - CI_MESSAGE_NONVARIABLE_SIZE; uint8_t *new = malloc(3 + header_len + 6 + max_per_message_property_bytes); new[0] = request_id; new[1] = header_len & 0x00ff; new[2] = header_len >> 8; memcpy(new + 3, header_data, header_len); for (uint32_t chunk = 1; chunk < chunk_count, i++) { new[3 + header_len] = chunk_count & 0x00ff; new[3 + header_len + 1] = chunk_count >> 8; new[3 + header_len + 2] = chunk & 0x00ff; new[3 + header_len + 3] = chunk >> 8; new[3 + header_len + 4] = max_per_message_property_bytes & 0x00ff; new[3 + header_len + 5] = max_per_message_property_bytes >> 8; memcpy(3 + header_len + 6, property_data + (chunk - 1) * max_per_message_property_bytes, max_per_message_property_bytes); ci(WHOLE_FUNCTION_BLOCK_ID, message_subid, destination_muid, new, 3 + header_len + 6 + max_per_message_property_bytes); } if (leftover_property_bytes != 0) { new[3 + header_len] = chunk_count & 0x00ff; new[3 + header_len + 1] = chunk_count >> 8; new[3 + header_len + 2] = chunk & 0x00ff; new[3 + header_len + 3] = chunk >> 8; new[3 + header_len + 4] = leftover_property_bytes & 0x00ff; new[3 + header_len + 5] = leftover_property_bytes >> 8; memcpy(3 + header_len + 6, property_data + (chunk_count - 1) * max_per_message_property_bytes, leftover_property_bytes); ci(WHOLE_FUNCTION_BLOCK_ID, message_subid, destination_muid, new, 3 + header_len + 6 + leftover_property_bytes); } free(new); } #define INQUIRE_GET_SUBID 0x34 void inquiry_get_property(uint32_t destination_muid, uint8_t request_id, uint8_t *header_data, uint16_t header_len) { uint8_t *new = malloc(3 + header_len + 3); new[0] = request_id; new[1] = header_len & 0x00ff; new[2] = header_len >> 8; memcpy(new + 3, header_data, header_len); // Chunk count. new[3 + header_len] = 0x01; new[3 + header_len + 1] = 0x00; // Current chunk number. new[3 + header_len + 2] = 0x01; new[3 + header_len + 3] = 0x00; // Property length. new[3 + header_len + 4] = 0x00; new[3 + header_len + 5] = 0x00; ci(WHOLE_FUNCTION_BLOCK_ID, INQUIRE_GET_SUBID, destination_muid, new, 3 + header_len + 6); free(new); } #define REPLY_GET_SUBID 0x35 void reply_get_property(uint32_t receiver_max_sysex, uint32_t destination_muid, uint8_t request_id, uint8_t *header_data, uint16_t header_len, uint8_t *property_data, uint16_t property_len) { chunked_property_exchange_send(receiver_max_sysex, REPLY_GET_SUBID, destination_muid, request_id, header_data, header_len, property_data, property_len); } #define INQURIE_SET_SUBID 0x36 void inquiry_set_property(uint32_t receiver_max_sysex, uint32_t destination_muid, uint8_t request_id, uint8_t *header_data, uint16_t header_len, uint8_t *property_data, uint16_t property_len) { chunked_property_exchange_send(receiver_max_sysex, INQUIRE_GET_SUBID, destination_muid, request_id, header_data, header_len, property_data, property_len) } #define REPLY_SET_SUBID 0x37 void reply_set_property(uint32_t receiver_max_sysex, uint32_t destination_muid, uint8_t request_id, uint8_t *header_data, uint16_t header_len, uint8_t *property_data, uint16_t property_len) { chunked_property_exchange_send(receiver_max_sysex, REPLY_SET_SUBID, destination_muid, request_id, header_data, header_len, property_data, property_len); } #define SUBSCRIPTION_SUBID 0x38 void subscription(uint32_t receiver_max_sysex, uint32_t destination_muid, uint8_t request_id, uint8_t *header_data, uint16_t header_len, uint8_t *property_data, uint16_t property_len) { chunked_property_exchange_send(receiver_max_sysex, SUBSCRIPTION_SUBID, destination_muid, request_id, header_data, header_len, property_data, property_len); } #define REPLY_SUBSCRIPTION_SUBID 0x39 void reply_subscription(uint32_t receiver_max_sysex, uint32_t destination_muid, uint8_t request_id, uint8_t *header_data, uint16_t header_len, uint8_t *property_data, uint16_t property_len) { chunked_property_exchange_send(receiver_max_sysex, REPLY_SUBSCRIPTION_SUBID, destination_muid, request_id, header_data, header_len, property_data, property_len); } #define NOTIFY_SUBID 0x3f void notify(uint32_t receiver_max_sysex, uint32_t destination_muid, uint8_t request_id, uint8_t *header_data, uint16_t header_len, uint8_t *property_data, uint16_t property_len) { chunked_property_exchange_send(receiver_max_sysex, NOTIFY_SUBID, destination_muid, request_id, header_data, header_len, property_data, property_len); } // Parsing. typedef enum { DEVICE_ID, SUBID, VERSION, SOURCE, DESTINATION, // Discovery. MANUFACTURER, FAMILY, MODEL, REVISION, CI_CATEGORY, MAX_SYSEX, INITIATOR_OUTPUT_PATH, // Invalidate. TARGET_MUID, // Ack. ACK_ORIG_SUBID, ACK_CODE, ACK_DATA, ACK_DETAILS, ACK_TEXT_LENGTH, ACK_TEXT, // Nak. NAK_ORIG_SUBID, NAK_CODE, NAK_DATA, NAK_DETAILS, NAK_LENGTH, NAK_TEXT, // Property Exchange Capabilites. MAX_SIMUL_EXCHANGE, EXCHANGE_MAJOR_VER, EXCHANGE_MINOR_VER, // Property Exchange messages. REQUEST_ID, HEADER_LEN, HEADER_DATA, CHUNK_COUNT, THIS_CHUNK, PROPERTY_LEN, PROPERTY_DATA } ParseState; const JSON_String supported_resources[] = { {"ResourceList"}, {"DeviceInfo"}, {"ChannelList"}, {"X-KeyLayout"}, /* {"X-"} */ }; typedef struct { uint8_t *stack; size_t top; size_t size; size_t field_position; ParseState state; } CIParser; extern CIParser ci_memory; static inline void next_in_field() { ++ci_memory.field_position; } static inline void new_field(ParseState to) { ci_memory.field_position = 0; ci_memory.state = to; } static inline void push_fn(uint8_t val) { if (ci_memory.top == ci_memory.size - 1) { ci_memory.stack = realloc(ci_memory.stack, ci_memory.size + 128); } ci_memory.stack[ci_memory.top + 1] = val; ++ci_memory.top; } #define push() do { \ push_fn(byte); \ } while(0) static inline void reset() { ci_memory.stack = realloc(ci_memory.stack, 128); ci_memory.size = 128; ci_memory.top = 0; ci_memory.status = WAITING_FOR_STATUS; } static inline uint8_t device_id() { return ci_memory.stack[0]; } static inline uint8_t command_id() { return ci_memory.stack[1]; } static inline uint32_t lsb_32bit_from(size_t start_index) { return (ci_memory.stack[start_index + 3] << 24) | (ci_memory.stack[start_index + 2] << 16) \ | (ci_memory.stack[start_index + 1] << 8) | (ci_memory.stack[start_index]); } static inline uint32_t source_muid() { return lsb_32bit_from(2); } static inline uint32_t dest_muid() { return lsb_32bit_from(6); } #define STACK_FIRST_NONCOMMON_IDX 10 #define push_until(val, then_block) do { \ switch(ci_memory.field_position) { \ case 0 ... (val - 1): \ push(); \ next_in_field(); \ return true; \ case val: \ then_block \ } \ } while(0) #define push_whole_field(val, new) do { \ switch(ci_memory.field_position) { \ case 0 ... (val - 1): \ push(); \ next_in_field(); \ return true; \ case val: \ push(); \ new_field(new); \ return true; \ } \ } while(0) typedef struct { void (*ack_handler)(uint8_t, uint8_t, uint8_t, uint32_t, uint8_t, uint16_t, char*); void (*nak_handler)(uint8_t, uint8_t, uint8_t, uint32_t, uint8_t, uint16_t, char*); } CIConsumerBehavior; extern CIConsumerBehavior ci_pfns; // The user's `universal_system_exclusive_handler` should feed everything into this, device-ID down, // once it recognizes a MIDI-CI message. bool midi_ci_parse(uint8_t byte) { switch (ci_memory.state) { case DEVICE_ID: push(); new_field(SUBID); return true; case SUBID: switch (ci_memory.field_position) { case 0: #ifdef DEBUG if (byte != 0x0d) { while(true); // Non-MIDI-CI Universal SysEx Sub ID 1. } #endif next_in_field(); return true; case 1: push() new_field(VERSION); return true; } case VERSION: #ifdef DEBUGp if (byte > 0x01) { while(true); // Unsupported future MIDI-CI Message Version/Format. } #endif new_field(SOURCE); return true; case SOURCE: push_whole_field(4, DESTINATION); case DESTINATION: push_until(4, { push(); switch (command_id()) { case DISCOVERY_SUBID: new_field(MANUFACTURER); return true; case REPLY_TO_DISCOVERY_SUBID: new_field(MANUFACTURER); return true; case INVALIDATE_MUID_SUBID: new_field(TARGET_MUID); return true; case ACK_SUBID: new_field(ACK_ORIG_SUBID); return true; case NAK_SUBID: new_field(NAK_ORIG_SUBID); return true; case INQUIRE_EXCHANGE_CAPABILITIES_SUBID: new_field(MAX_SIMUL_EXCHANGE); return true; case REPLY_EXCHANGE_CAPABILITIES_SUBID: new_field(MAX_SIMUL_EXCHANGE); return true; case INQUIRE_GET_SUBID: case REPLY_GET_SUBID: case INQUIRE_SET_SUBID: case REPLY_SET_SUBID: case SUBSCRIPTION_SUBID: case REPLY_SUBSCRIPTION_SUBID: case NOTIFY_SUBID: new_field(REQUEST_ID); return true; default: #ifdef DEBUG while(true); // Unsupported MIDI-CI command. #endif return false; }}); // Discovery messages. case MANUFACTURER: push_whole_field(3, FAMILY); case FAMILY: push_whole_field(2, MODEL); case REVISION: push_whole_field(4, CI_SUPPORTED); case CI_SUPPORTED: push_whole_field(1, MAX_SYSEX); case MAX_SYSEX: push_whole_field(4, INITIATOR_OUTPUT_PATH); case INITIATOR_OUTPUT_PATH: switch (command_id()) { case DISCOVERY_SUBID: if (source_muid() == our_muid) { invalidate_our_muid(); our_muid = rand(); // TODO: user function for new MUID? discovery(); reset(); return false; } discovery_reply(source_muid(), byte); reset(); return false; case DISCOVERY_REPLY_SUBID: if (already_seen(source_muid())) { // TODO: already_seen and associated logging. invalidate_muid(source_muid()); discovery(); reset(); return false; } default: #ifdef DEBUG while(true); // Unexpected non-Discovery command id. #endif reset(); return false; } // Invalidate. case TARGET_MUID: push_until(4, { uint32_t to_invalidate = lsb_32bit_from(STACK_FIRST_NONCOMMON_IDX); if (to_invalidate == our_muid) { our_muid = rand(); discovery(); } else if (already_seen(to_invalidate)) { invalidate_stored_data(to_invalidate); // TODO: to_invalidate; see above. } reset(); return false; }); // ACK. case ACK_ORIG_SUBID: push_whole_field(1, ACK_CODE); case ACK_CODE: push_whole_field(1, ACK_DATA); case ACK_DATA: push_whole_field(1, ACK_DETAILS); case ACK_DETAILS: push_whole_field(5, ACK_TEXT_LENGTH); case ACK_TEXT_LENGTH: push_whole_field(2, ACK_TEXT); case ACK_TEXT: push_until((ci_memory.stack[ci_memory.top - 1] << 8) | ci_memory.stack[ci_memory.top], { // TODO: }); reset(); return false; // NAK. case NAK_ORIG_SUBID: push_whole_field(1, ACK_CODE); case NAK_CODE: push_whole_field(1, ACK_DATA); case NAK_DATA: push_whole_field(1, ACK_DETAILS); case NAK_DETAILS: push_whole_field(5, ACK_TEXT_LENGTH); case NAK_LENGTH: push_whole_field(2, ACK_TEXT); case NAK_TEXT: push_until((ci_memory.stack[ci_memory.top - 1] << 8) | ci_memory.stack[ci_memory.top], { // TODO: }); reset(); return false; // TODO: Property Exchange Capabilites // Property Exchange. case REQUEST_ID: push_whole_field(1, HEADER_LEN); case HEADER_LEN: push_whole_field(2, HEADER_DATA); case HEADER_DATA: push_until((ci_memory.stack[ci_memory.top - 1] << 8) | ci_memory.stack[ci_memory.top], { // TODO: }); case CHUNK_COUNT: push_whole_field(2, THIS_CHUNK); case THIS_CHUNK: push_whole_field(2, PROPERTY_LEN); case PROPERTY_LEN: push_whole_field(2, PROPERTY_DATA); case PROPERTY_DATA: push_until((ci_memory.stack[ci_memory.top - 1] << 8) | ci_memory.stack[ci_memory.top], { JSON_Value *header = json_parse_string(ci_memory.stack + \ (ci_memory.stack[ci_memory.top - 1] << 8) | ci_memory.stack[ci_memory.top]); switch (command_id()) { case INQUIRE_GET_SUBID: #ifdef DEBUG if (header->type != JSONObject) { while(true); // Non-object JSON Value. } #endif // TODO: the spec kinda sucks, ngl. I'll get MIDI 1.0, and then return here. case REPLY_GET_SUBID: case INQUIRE_SET_SUBID: case REPLY_SET_SUBID: case SUBSCRIPTION_SUBID: case REPLY_SUBSCRIPTION_SUBID: case NOTIFY_SUBID: } }); } } #endif