From 2a3940d8a36433485a6ef489d5123cd491618b50 Mon Sep 17 00:00:00 2001 From: Duncan Wilkie Date: Mon, 7 Aug 2023 17:29:29 -0500 Subject: slave progress and WCET --- controller/libs/midi_ci.c | 536 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 528 insertions(+), 8 deletions(-) (limited to 'controller/libs/midi_ci.c') diff --git a/controller/libs/midi_ci.c b/controller/libs/midi_ci.c index 7013d7a..cecef52 100644 --- a/controller/libs/midi_ci.c +++ b/controller/libs/midi_ci.c @@ -4,9 +4,13 @@ // - 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 @@ -16,7 +20,7 @@ uint32_t our_muid = rand(); // TODO: think. #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, uint8_t contents_len) { + 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; @@ -106,16 +110,14 @@ void discovery_reply(uint8_t initiator_muid, uint8_t initiator_output_path_id) { } -// TODO: Decide if Inquiry: Endpoint and Reply to Endpoint are necessary for standard conformace if we're over MIDI 1.0. - // TODO: should we set a new MUID inside this function? #define INVALIDATE_MUID_SUBID 0x7e -void invalidate_muid() { +void invalidate_muid(uint8_t muid) { uint8_t *new = malloc(4); - new[0] = our_muid & 0x00'00'00'ff; - new[1] = (our_muid >> 8) & 0x00'00'00'ff; - new[2] = (our_muid >> 16) & 0x00'00'00'ff; - new[3] = (our_muid >> 24) & 0x00'00'00'ff; + 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); @@ -123,6 +125,10 @@ void invalidate_muid() { } +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, @@ -172,3 +178,517 @@ void ci_nack(uint8_t naking_device_id, uint8_t naking_muid, uint8_t original_cla 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 -- cgit v1.2.3