diff --git a/boards/shields/olimex_shield_midi/Kconfig.shield b/boards/shields/olimex_shield_midi/Kconfig.shield new file mode 100644 index 0000000000000..c9aa1a04d95f2 --- /dev/null +++ b/boards/shields/olimex_shield_midi/Kconfig.shield @@ -0,0 +1,5 @@ +# Copyright (c) 2025 Titouan Christophe +# SPDX-License-Identifier: Apache-2.0 + +config SHIELD_OLIMEX_SHIELD_MIDI + def_bool $(shields_list_contains,olimex_shield_midi) diff --git a/boards/shields/olimex_shield_midi/doc/index.rst b/boards/shields/olimex_shield_midi/doc/index.rst new file mode 100644 index 0000000000000..e4884c3258418 --- /dev/null +++ b/boards/shields/olimex_shield_midi/doc/index.rst @@ -0,0 +1,44 @@ +.. _olimex_shield_midi: + +Olimex SHIELD-MIDI +################## + +Overview +******** + +This is a MIDI shield which allows Arduino like boards to receive and send MIDI +messages. The shield allows direct wiring of up to 5 piezzo sensors and a +keyboard (with buttons and serial resistors) making it ideal for drums projects. + +.. figure:: olimex_shield_midi.jpg + :align: center + :alt: Olimex SHIELD-MIDI + +* MIDI-IN, MIDI-OUT and MIDI-THRU connectors +* 5 Piezzo sensors for drum implementation +* Keyboard for piano implementation +* Works with both 3.3V and 5V Arduino-like boards + +More information on the `SHIELD-MIDI website`_. + +Peripherals +*********** + +The following peripherals are available in Zephyr: + +- MIDI IN/OUT: on ``midi_serial`` (set at 31.25kb/s) +- leds: ``midi_green_led``, ``midi_red_led`` + +Programming +*********** + +Set ``--shield olimex_shield_midi`` when you invoke ``west build``. For example: + +.. zephyr-app-commands:: + :zephyr-app: samples/net/midi2 + :board: nucleo_f429zi + :shield: olimex_shield_midi + :goals: build + +.. _SHIELD-MIDI website: + https://www.olimex.com/Products/Duino/Shields/SHIELD-MIDI/open-source-hardware diff --git a/boards/shields/olimex_shield_midi/doc/olimex_shield_midi.jpg b/boards/shields/olimex_shield_midi/doc/olimex_shield_midi.jpg new file mode 100644 index 0000000000000..1c892eddbd451 Binary files /dev/null and b/boards/shields/olimex_shield_midi/doc/olimex_shield_midi.jpg differ diff --git a/boards/shields/olimex_shield_midi/olimex_shield_midi.overlay b/boards/shields/olimex_shield_midi/olimex_shield_midi.overlay new file mode 100644 index 0000000000000..3779724366b4c --- /dev/null +++ b/boards/shields/olimex_shield_midi/olimex_shield_midi.overlay @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2025 Titouan Christophe + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + leds { + midi_green_led: midi-green-led { + gpios = <&arduino_header 13 GPIO_ACTIVE_HIGH>; + }; + + midi_red_led: midi-red-led { + gpios = <&arduino_header 12 GPIO_ACTIVE_HIGH>; + }; + }; +}; + +midi_serial: &arduino_serial { + current-speed = <31250>; +}; diff --git a/boards/shields/olimex_shield_midi/shield.yml b/boards/shields/olimex_shield_midi/shield.yml new file mode 100644 index 0000000000000..6bb238be27973 --- /dev/null +++ b/boards/shields/olimex_shield_midi/shield.yml @@ -0,0 +1,7 @@ +shield: + name: olimex_shield_midi + full_name: Olimex SHIELD-MIDI + vendor: olimex + supported_features: + - led + - uart diff --git a/doc/zephyr.doxyfile.in b/doc/zephyr.doxyfile.in index 1dbc5ecd0d2b6..cea0300cfb0ab 100644 --- a/doc/zephyr.doxyfile.in +++ b/doc/zephyr.doxyfile.in @@ -1017,6 +1017,7 @@ INPUT = @ZEPHYR_BASE@/doc/_doxygen/mainpage.md \ @ZEPHYR_BASE@/include/zephyr/sys/atomic.h \ @ZEPHYR_BASE@/include/ \ @ZEPHYR_BASE@/lib/libc/minimal/include/ \ + @ZEPHYR_BASE@/lib/midi2/ \ @ZEPHYR_BASE@/subsys/testsuite/include/ \ @ZEPHYR_BASE@/subsys/testsuite/ztest/include/ \ @ZEPHYR_BASE@/subsys/secure_storage/include/ \ diff --git a/dts/bindings/usb/zephyr,midi2-device.yaml b/dts/bindings/usb/zephyr,midi2-device.yaml index 851916e7e8775..1113c1886efa7 100644 --- a/dts/bindings/usb/zephyr,midi2-device.yaml +++ b/dts/bindings/usb/zephyr,midi2-device.yaml @@ -14,6 +14,10 @@ properties: type: int const: 1 + label: + type: string + description: Name of the UMP (MIDI 2.0) endpoint + child-binding: description: | MIDI2 Group terminal block. @@ -21,6 +25,10 @@ child-binding: device exchange Universal MIDI Packets with the host. properties: + label: + type: string + description: Name of the corresponding UMP Function block + reg: type: array required: true @@ -49,3 +57,9 @@ child-binding: - "output-only" description: | Type (data direction) of Group Terminals in this Block. + + serial-31250bps: + type: boolean + description: | + This represent a physical MIDI1 serial port, which is limited + to a transmission speed of 31.25kb/s. diff --git a/include/zephyr/audio/midi.h b/include/zephyr/audio/midi.h index d564531ecad24..5e59dd444a1b8 100644 --- a/include/zephyr/audio/midi.h +++ b/include/zephyr/audio/midi.h @@ -19,11 +19,22 @@ extern "C" { * @ingroup audio_interface * @since 4.1 * @version 0.1.0 - * @see ump112: "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" - * Document version 1.1.2 * @{ */ +/** + * @defgroup ump112 Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol + * @ingroup midi_ump + * @{ + * @details Definitions based on the following document + * + * Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol + * With MIDI 1.0 Protocol in UMP Format - Document version 1.1.2 + * (MIDI Association Document: M2-104-UM) + * + * @} + */ + /** * @brief Universal MIDI Packet container */ @@ -52,13 +63,17 @@ struct midi_ump { #define UMP_MT_DATA_128 0x05 /** Flex Data Messages */ #define UMP_MT_FLEX_DATA 0x0d -/** UMP Stream Message */ +/** + * UMP Stream Message + * @see midi_ump_stream + */ #define UMP_MT_UMP_STREAM 0x0f /** @} */ /** * @brief Message Type field of a Universal MIDI Packet * @param[in] ump Universal MIDI Packet + * @see midi_ump_mt */ #define UMP_MT(ump) \ ((ump).data[0] >> 28) @@ -86,14 +101,15 @@ struct midi_ump { * @param[in] ump Universal MIDI Packet */ #define UMP_GROUP(ump) \ - (((ump).data[0] >> 24) & 0x0f) + (((ump).data[0] >> 24) & BIT_MASK(4)) /** * @brief Status byte of a MIDI channel voice or system message * @param[in] ump Universal MIDI Packet (containing a MIDI1 event) + * @see midi_ump_sys */ #define UMP_MIDI_STATUS(ump) \ - (((ump).data[0] >> 16) & 0xff) + (((ump).data[0] >> 16) & BIT_MASK(8)) /** * @brief Command of a MIDI channel voice message * @param[in] ump Universal MIDI Packet (containing a MIDI event) @@ -106,19 +122,19 @@ struct midi_ump { * @param[in] ump Universal MIDI Packet (containing a MIDI event) */ #define UMP_MIDI_CHANNEL(ump) \ - (UMP_MIDI_STATUS(ump) & 0x0f) + (UMP_MIDI_STATUS(ump) & BIT_MASK(4)) /** * @brief First parameter of a MIDI1 channel voice or system message * @param[in] ump Universal MIDI Packet (containing a MIDI1 message) */ #define UMP_MIDI1_P1(ump) \ - (((ump).data[0] >> 8) & 0x7f) + (((ump).data[0] >> 8) & BIT_MASK(7)) /** * @brief Second parameter of a MIDI1 channel voice or system message * @param[in] ump Universal MIDI Packet (containing a MIDI1 message) */ #define UMP_MIDI1_P2(ump) \ - ((ump).data[0] & 0x7f) + ((ump).data[0] & BIT_MASK(7)) /** * @brief Initialize a UMP with a MIDI1 channel voice message @@ -143,9 +159,8 @@ struct midi_ump { * @defgroup midi_ump_cmd MIDI commands * @ingroup midi_ump * @see ump112: 7.3 MIDI 1.0 Channel Voice Messages - * - * When UMP_MT(x)=UMP_MT_MIDI1_CHANNEL_VOICE or UMP_MT_MIDI2_CHANNEL_VOICE, then - * UMP_MIDI_COMMAND(x) may be one of: + * @remark When UMP_MT(x)=UMP_MT_MIDI1_CHANNEL_VOICE or UMP_MT_MIDI2_CHANNEL_VOICE, + * then UMP_MIDI_COMMAND(x) may be one of: * @{ */ #define UMP_MIDI_NOTE_OFF 0x8 /**< Note Off (p1=note number, p2=velocity) */ @@ -180,7 +195,8 @@ struct midi_ump { * @ingroup midi_ump * @see ump112: 7.6 System Common and System Real Time Messages * - * When UMP_MT(x)=UMP_MT_SYS_RT_COMMON, UMP_MIDI_STATUS(x) may be one of: + * @remark When UMP_MT(x)=UMP_MT_SYS_RT_COMMON, + * then UMP_MIDI_STATUS(x) may be one of: * @{ */ #define UMP_SYS_MIDI_TIME_CODE 0xf1 /**< MIDI Time Code (no param) */ @@ -195,6 +211,149 @@ struct midi_ump { #define UMP_SYS_RESET 0xff /**< Reset (no param) */ /** @} */ + +/** + * @defgroup midi_ump_stream UMP Stream specific fields + * @ingroup midi_ump + * @see ump112: 7.1 UMP Stream Messages + * + * @{ + */ + +/** + * @brief Format of a UMP Stream message + * @param[in] ump Universal MIDI Packet (containing a UMP Stream message) + * @see midi_ump_stream_format + */ +#define UMP_STREAM_FORMAT(ump) \ + (((ump).data[0] >> 26) & BIT_MASK(2)) + +/** + * @defgroup midi_ump_stream_format UMP Stream format + * @ingroup midi_ump_stream + * @see ump112: 7.1 UMP Stream Messages: Format + * @remark When UMP_MT(x)=UMP_MT_UMP_STREAM, + * then UMP_STREAM_FORMAT(x) may be one of: + * @{ + */ + +/** Complete message in one UMP */ +#define UMP_STREAM_FORMAT_COMPLETE 0x00 +/** Start of a message which spans two or more UMPs */ +#define UMP_STREAM_FORMAT_START 0x01 +/** Continuing a message which spans three or more UMPs. + * There might be multiple Continue UMPs in a single message + */ +#define UMP_STREAM_FORMAT_CONTINUE 0x02 +/** End of message which spans two or more UMPs */ +#define UMP_STREAM_FORMAT_END 0x03 + +/** @} */ + +/** + * @brief Status field of a UMP Stream message + * @param[in] ump Universal MIDI Packet (containing a UMP Stream message) + * @see midi_ump_stream_status + */ +#define UMP_STREAM_STATUS(ump) \ + (((ump).data[0] >> 16) & BIT_MASK(10)) + +/** + * @defgroup midi_ump_stream_status UMP Stream status + * @ingroup midi_ump_stream + * @see ump112: 7.1 UMP Stream Messages + * @remark When UMP_MT(x)=UMP_MT_UMP_STREAM, + * then UMP_STREAM_STATUS(x) may be one of: + * @{ + */ + +/** Endpoint Discovery Message */ +#define UMP_STREAM_STATUS_EP_DISCOVERY 0x00 +/** Endpoint Info Notification Message */ +#define UMP_STREAM_STATUS_EP_INFO 0x01 +/** Device Identity Notification Message */ +#define UMP_STREAM_STATUS_DEVICE_IDENT 0x02 +/** Endpoint Name Notification */ +#define UMP_STREAM_STATUS_EP_NAME 0x03 +/** Product Instance Id Notification Message */ +#define UMP_STREAM_STATUS_PROD_ID 0x04 +/** Stream Configuration Request Message */ +#define UMP_STREAM_STATUS_CONF_REQ 0x05 +/** Stream Configuration Notification Message */ +#define UMP_STREAM_STATUS_CONF_NOTIF 0x06 +/** Function Block Discovery Message */ +#define UMP_STREAM_STATUS_FB_DISCOVERY 0x10 +/** Function Block Info Notification */ +#define UMP_STREAM_STATUS_FB_INFO 0x11 +/** Function Block Name Notification */ +#define UMP_STREAM_STATUS_FB_NAME 0x12 +/** @} */ + +/** + * @brief Filter bitmap of an Endpoint Discovery message + * @param[in] ump Universal MIDI Packet (containing an Endpoint Discovery message) + * @see ump112: 7.1.1 Endpoint Discovery Message + * @see midi_ump_ep_disc + */ +#define UMP_STREAM_EP_DISCOVERY_FILTER(ump) \ + ((ump).data[1] & BIT_MASK(8)) + +/** + * @defgroup midi_ump_ep_disc UMP Stream endpoint discovery message filter bits + * @ingroup midi_ump_stream + * @see ump112: 7.1.1 Fig. 12: Endpoint Discovery Message Filter Bitmap Field + * @remark When UMP_MT(x)=UMP_MT_UMP_STREAM and + * UMP_STREAM_STATUS(x)=UMP_STREAM_STATUS_EP_DISCOVERY, + * then UMP_STREAM_EP_DISCOVERY_FILTER(x) may be an ORed combination of: + * @{ + */ + +/** Requesting an Endpoint Info Notification */ +#define UMP_EP_DISC_FILTER_EP_INFO BIT(0) +/** Requesting a Device Identity Notification */ +#define UMP_EP_DISC_FILTER_DEVICE_ID BIT(1) +/** Requesting an Endpoint Name Notification */ +#define UMP_EP_DISC_FILTER_EP_NAME BIT(2) +/** Requesting a Product Instance Id Notification */ +#define UMP_EP_DISC_FILTER_PRODUCT_ID BIT(3) +/** Requesting a Stream Configuration Notification */ +#define UMP_EP_DISC_FILTER_STREAM_CFG BIT(4) +/** @} */ + +/** + * @brief Filter bitmap of a Function Block Discovery message + * @param[in] ump Universal MIDI Packet (containing a Function Block Discovery message) + * @see ump112: 7.1.7 Function Block Discovery Message + * @see midi_ump_fb_disc + */ +#define UMP_STREAM_FB_DISCOVERY_FILTER(ump) \ + ((ump).data[0] & BIT_MASK(8)) + +/** + * @brief Block number requested in a Function Block Discovery message + * @param[in] ump Universal MIDI Packet (containing a Function Block Discovery message) + * @see ump112: 7.1.7 Function Block Discovery Message + */ +#define UMP_STREAM_FB_DISCOVERY_NUM(ump) \ + (((ump).data[0] >> 8) & BIT_MASK(8)) + +/** + * @defgroup midi_ump_fb_disc UMP Stream Function Block discovery message filter bits + * @ingroup midi_ump_stream + * @see ump112: 7.1.7 Fig. 21: Function Block Discovery Filter Bitmap Field Format + * @remark When UMP_MT(x)=UMP_MT_UMP_STREAM and + * UMP_STREAM_STATUS(x)=UMP_STREAM_STATUS_FB_DISCOVERY, + * then UMP_STREAM_FB_DISCOVERY_FILTER(x) may be an ORed combination of: + * @{ + */ +/** Requesting a Function Block Info Notification */ +#define UMP_FB_DISC_FILTER_INFO BIT(0) +/** Requesting a Function Block Name Notification */ +#define UMP_FB_DISC_FILTER_NAME BIT(1) +/** @} */ + +/** @} */ + /** @} */ #ifdef __cplusplus diff --git a/include/zephyr/net/midi2.h b/include/zephyr/net/midi2.h new file mode 100644 index 0000000000000..4af8edc32582a --- /dev/null +++ b/include/zephyr/net/midi2.h @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2025 Titouan Christophe + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_NET_MIDI2_H_ +#define ZEPHYR_INCLUDE_NET_MIDI2_H_ + +/** + * @defgroup net_midi2 Network MIDI 2.0 + * @since 4.3 + * @version 0.1.0 + * @ingroup networking + * @{ + */ + +/** + * @defgroup netmidi10 User Datagram Protocol for Universal MIDI Packets + * @ingroup net_midi2 + * @{ + * @details Definitions based on the following document + * + * User Datagram Protocol for Universal MIDI Packets + * Network MIDI 2.0 (UDP) Transport Specification - Document version 1.0 + * (MIDI Association Document: M2-124-UM) + * + * @} + */ + + +#include +#include +#include +#include + +/** + * Size, in bytes, of the nonce sent to the client for authentication + * @see netmidi10: 6.7 Invitation Reply: Authentication Required + */ +#define NETMIDI2_NONCE_SIZE 16 + +/** + * @brief Statically declare a Network (UDP) MIDI 2.0 endpoint host + * @param _var_name The name of the variable holding the server data + * @param _ep_name The UMP endpoint name + * @param _piid The UMP Product Instance ID. If NULL, + * HWINFO device ID will be used at runtime. + * @param _port The UDP port to listen to, or 0 for automatic assignment + */ +#define NETMIDI2_EP_DEFINE(_var_name, _ep_name, _piid, _port) \ + static struct netmidi2_ep _var_name = { \ + .name = (_ep_name), \ + .piid = (_piid), \ + .addr4.sin_port = (_port), \ + .auth_type = NETMIDI2_AUTH_NONE, \ + } + +/** + * @brief Statically declare a Network (UDP) MIDI 2.0 endpoint host, + * with a predefined shared secret key for authentication + * @param _var_name The name of the variable holding the server data + * @param _ep_name The UMP endpoint name + * @param _piid The UMP Product Instance ID. If NULL, + * HWINFO device ID will be used at runtime. + * @param _port The UDP port to listen to, or 0 for automatic assignment + * @param _secret The shared secret key clients must provide to connect + */ +#define NETMIDI2_EP_DEFINE_WITH_AUTH(_var_name, _ep_name, _piid, _port, _secret) \ + static struct netmidi2_ep _var_name = { \ + .name = (_ep_name), \ + .piid = (_piid), \ + .addr4.sin_port = (_port), \ + .auth_type = NETMIDI2_AUTH_SHARED_SECRET, \ + .shared_auth_secret = (_secret), \ + } + +/** + * @brief Statically declare a Network (UDP) MIDI 2.0 endpoint host, + * with a predefined list of users/passwords for authentication + * @param _var_name The name of the variable holding the server data + * @param _ep_name The UMP endpoint name + * @param _piid The UMP Product Instance ID. If NULL, + * HWINFO device ID will be used at runtime. + * @param _port The UDP port to listen to, or 0 for automatic assignment + * @param ... The username/password pairs (struct netmidi2_user) + * + * Example usage: + * @code + * NETMIDI2_EP_DEFINE_WITH_USERS(my_server, "endpoint", NULL, 0, + * {.name="user1", .password="passwd1"}, + * {.name="user2", .password="passwd2"}) + * @endcode + */ +#define NETMIDI2_EP_DEFINE_WITH_USERS(_var_name, _ep_name, _piid, _port, ...) \ + static const struct netmidi2_userlist users_of_##_var_name = { \ + .n_users = ARRAY_SIZE(((struct netmidi2_user []) { __VA_ARGS__ })), \ + .users = { __VA_ARGS__ }, \ + }; \ + static struct netmidi2_ep _var_name = { \ + .name = (_ep_name), \ + .piid = (_piid), \ + .addr4.sin_port = (_port), \ + .auth_type = NETMIDI2_AUTH_USER_PASSWORD, \ + .userlist = &users_of_##_var_name, \ + } + +struct netmidi2_ep; + +/** + * @brief A username/password pair for user-based authentication + */ +struct netmidi2_user { + /** The user name for authentication */ + const char *name; + /** The password for authentication */ + const char *password; +}; + +/** + * @brief A list of users for user-based authentication + */ +struct netmidi2_userlist { + /** Number of users in the list */ + size_t n_users; + /** The user/password pairs */ + const struct netmidi2_user users[]; +}; + +/** + * @brief A Network MIDI2 session, representing a connection to a peer + */ +struct netmidi2_session { + /** + * State of this session + * @see netmidi10: 6.1 Session States + */ + enum { + /** The session is not in use */ + NETMIDI2_SESSION_NOT_INITIALIZED = 0, + /** Device may be aware of each other (e.g. through mDNS), + * however neither device has sent an Invitation. + * The two Devices are not in a Session. + */ + NETMIDI2_SESSION_IDLE, + /** Client has sent Invitation, however the Host has not + * accepted the Invitation. A Bye Command has not been + * sent or received. + */ + NETMIDI2_SESSION_PENDING_INVITATION, + /** Host has replied with Invitation Reply: Authentication + * Required or Invitation Reply: User Authentication Required. + * No timeout has yet occurred. This state is different from + * Idle, because the Client and Host need to store the Nonce. + */ + NETMIDI2_SESSION_AUTH_REQUIRED, + /** Invitation accepted, UMP Commands can be exchanged. */ + NETMIDI2_SESSION_ESTABLISHED, + /** One Device has sent the Session Reset Command and is + * waiting for Session Reset Reply, and a timeout has not + * yet occurred. + */ + NETMIDI2_SESSION_PENDING_RESET, + /** one Endpoint has sent Bye and is waiting for Bye Reply, + * and a timeout has not yet occurred. + */ + NETMIDI2_SESSION_PENDING_BYE, + } state; + /** Sequence number of the next universal MIDI packet to send */ + uint16_t tx_ump_seq; + /** Sequence number of the next universal MIDI packet to receive */ + uint16_t rx_ump_seq; + /** Remote address of the peer */ + struct sockaddr_storage addr; + /** Length of the peer's remote address */ + socklen_t addr_len; + /** The Network MIDI2 endpoint to which this session belongs */ + struct netmidi2_ep *ep; +#if defined(CONFIG_NETMIDI2_HOST_AUTH) || defined(__DOXYGEN__) + /** The username to which this session belongs */ + const struct netmidi2_user *user; + /** The crypto nonce used to authorize this session */ + char nonce[NETMIDI2_NONCE_SIZE]; +#endif + /** The transmission buffer for that peer */ + struct net_buf *tx_buf; + /** The transmission work for that peer */ + struct k_work tx_work; +}; + +/** + * @brief Type of authentication in Network MIDI2 + */ +enum netmidi2_auth_type { + /** No authentication required */ + NETMIDI2_AUTH_NONE, + /** Authentication with a shared secret key */ + NETMIDI2_AUTH_SHARED_SECRET, + /** Authentication with username and password */ + NETMIDI2_AUTH_USER_PASSWORD, +}; + +/** + * @brief A Network MIDI2.0 Endpoint + */ +struct netmidi2_ep { + /** The endpoint name */ + const char *name; + /** The endpoint product instance id */ + const char *piid; + /** The local endpoint address */ + union { + struct sockaddr addr; + struct sockaddr_in addr4; + struct sockaddr_in6 addr6; + }; + /** The listening socket wrapped in a poll descriptor */ + struct pollfd pollsock; + /** The function to call when data is received from a client */ + void (*rx_packet_cb)(struct netmidi2_session *session, + const struct midi_ump ump); + /** List of peers to this endpoint */ + struct netmidi2_session peers[CONFIG_NETMIDI2_HOST_MAX_CLIENTS]; + /** The type of authentication required to establish a session + * with this host endpoint + */ + enum netmidi2_auth_type auth_type; +#if defined(CONFIG_NETMIDI2_HOST_AUTH) || defined(__DOXYGEN__) + union { + /** A shared authentication key */ + const char *shared_auth_secret; + /** A list of users/passwords */ + const struct netmidi2_userlist *userlist; + }; +#endif +}; + +/** + * @brief Start hosting a network (UDP) Universal MIDI Packet endpoint + * @param ep The network endpoint to start + * @return 0 on success, -errno on error + */ +int netmidi2_host_ep_start(struct netmidi2_ep *ep); + +/** + * @brief Send a Universal MIDI Packet to all clients connected to the endpoint + * @param ep The endpoint + * @param[in] ump The packet to send + */ +void netmidi2_broadcast(struct netmidi2_ep *ep, const struct midi_ump ump); + +/** + * @brief Send a Universal MIDI Packet to a single client + * @param sess The session identifying the single client + * @param[in] ump The packet to send + */ +void netmidi2_send(struct netmidi2_session *sess, const struct midi_ump ump); + +/** @} */ + +#endif diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index fad0e383c9061..f960141b7af76 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -11,6 +11,7 @@ add_subdirectory_ifdef(CONFIG_CPP cpp) add_subdirectory(hash) add_subdirectory(heap) add_subdirectory(mem_blocks) +add_subdirectory(midi2) add_subdirectory_ifdef(CONFIG_NET_BUF net_buf) add_subdirectory(os) add_subdirectory(utils) diff --git a/lib/Kconfig b/lib/Kconfig index 149c1317e499e..7f2a5b2a15be1 100644 --- a/lib/Kconfig +++ b/lib/Kconfig @@ -15,6 +15,8 @@ source "lib/heap/Kconfig" source "lib/mem_blocks/Kconfig" +source "lib/midi2/Kconfig" + source "lib/net_buf/Kconfig" source "lib/os/Kconfig" diff --git a/lib/midi2/CMakeLists.txt b/lib/midi2/CMakeLists.txt new file mode 100644 index 0000000000000..49a11c2ed8552 --- /dev/null +++ b/lib/midi2/CMakeLists.txt @@ -0,0 +1,8 @@ +# Copyright (c) 2025 Titouan Christophe +# SPDX-License-Identifier: Apache-2.0 + +zephyr_include_directories(.) + +if(CONFIG_MIDI2_UMP_STREAM_RESPONDER) + zephyr_sources(ump_stream_responder.c) +endif() diff --git a/lib/midi2/Kconfig b/lib/midi2/Kconfig new file mode 100644 index 0000000000000..d4ab789c30304 --- /dev/null +++ b/lib/midi2/Kconfig @@ -0,0 +1,13 @@ +# Copyright (c) 2025 Titouan Christophe +# SPDX-License-Identifier: Apache-2.0 + +menu "MIDI2" + +config MIDI2_UMP_STREAM_RESPONDER + bool "MIDI2 UMP Stream responder" + help + Library to respond to UMP Stream discovery messages, as specified + in "Universal MIDI Packet (UMP) Format and MIDI 2.0 Protocol" + version 1.1.2 section 7.1: "UMP Stream Messages" + +endmenu diff --git a/lib/midi2/ump_stream_responder.c b/lib/midi2/ump_stream_responder.c new file mode 100644 index 0000000000000..7ffffbf873255 --- /dev/null +++ b/lib/midi2/ump_stream_responder.c @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2025 Titouan Christophe + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include "ump_stream_responder.h" + +#define BIT_IF(cond, n) ((cond) ? BIT(n) : 0) + +/** + * @brief MIDI-CI version identifier for UMP v1.1 devices + * @see ump112: 7.1.8 FB Info Notification > MIDI-CI Message Version/Format + */ +#define MIDI_CI_VERSION_FORMAT_UMP_1_1 0x01 + +static inline bool ep_has_midi1(const struct ump_endpoint_dt_spec *ep) +{ + for (size_t i = 0; i < ep->n_blocks; i++) { + if (ep->blocks[i].is_midi1) { + return true; + } + } + return false; +} + +static inline bool ep_has_midi2(const struct ump_endpoint_dt_spec *ep) +{ + for (size_t i = 0; i < ep->n_blocks; i++) { + if (!ep->blocks[i].is_midi1) { + return true; + } + } + return false; +} + +/** + * @brief Build an Endpoint Info Notification Universal MIDI Packet + * @see ump112: 7.1.2 Endpoint Info Notification Message + */ +static inline struct midi_ump make_endpoint_info(const struct ump_endpoint_dt_spec *ep) +{ + struct midi_ump res; + + res.data[0] = (UMP_MT_UMP_STREAM << 28) + | (UMP_STREAM_STATUS_EP_INFO << 16) + | 0x0101; /* UMP version 1.1 */ + + res.data[1] = BIT(31) /* Static function blocks */ + | ((ep->n_blocks) << 24) + | BIT_IF(ep_has_midi2(ep), 9) + | BIT_IF(ep_has_midi1(ep), 8); + + return res; +} + +/** + * @brief Build a Function Block Info Notification Universal MIDI Packet + * @see ump112: 7.1.8 Function Block Info Notification + */ +static inline struct midi_ump make_function_block_info(const struct ump_endpoint_dt_spec *ep, + size_t block_num) +{ + const struct ump_block_dt_spec *block = &ep->blocks[block_num]; + struct midi_ump res; + uint8_t midi1_mode = block->is_31250bps ? 2 : block->is_midi1 ? 1 : 0; + + res.data[0] = (UMP_MT_UMP_STREAM << 28) + | (UMP_STREAM_STATUS_FB_INFO << 16) + | BIT(15) /* Block is active */ + | (block_num << 8) + | BIT_IF(block->is_output, 5) /* UI hint Sender */ + | BIT_IF(block->is_input, 4) /* UI hint Receiver */ + | (midi1_mode << 2) + | BIT_IF(block->is_output, 1) /* Function block is output */ + | BIT_IF(block->is_input, 0); /* Function block is input */ + + res.data[1] = (block->first_group << 24) + | (block->groups_spanned << 16) + | (MIDI_CI_VERSION_FORMAT_UMP_1_1 << 8) /* MIDI-CI for UMP v1.1 */ + | 0xff; /* At most 255 simultaneous Sysex streams */ + + return res; +} + +/** + * @brief Copy an ASCII string into a Universal MIDI Packet while leaving + * some most significant bytes untouched, such that the caller can + * set this prefix. + * @param ump The ump into which the string is copied + * @param[in] offset Number of bytes from the most-significant side to leave free + * @param[in] src The source string + * @param[in] len The length of the source string + * @return The number of bytes copied + */ +static inline size_t fill_str(struct midi_ump *ump, size_t offset, + const char *src, size_t len) +{ + size_t i, j; + + if (offset >= sizeof(struct midi_ump)) { + return 0; + } + + for (i = 0; i < len && (j = i + offset) < sizeof(struct midi_ump); i++) { + ump->data[j / 4] |= src[i] << (8 * (3 - (j % 4))); + } + + return i; +} + +/** + * @brief Send a string as UMP Stream, possibly splitting into multiple + * packets if the string length is larger than 1 UMP + * @param[in] cfg The responder configuration + * @param[in] string The string to send + * @param[in] prefix The fixed prefix of UMP packets to send + * @param[in] offset The offset the strings starts in the packet, in bytes + * + * @return The number of packets sent + */ +static inline int send_string(const struct ump_stream_responder_cfg *cfg, + const char *string, uint32_t prefix, size_t offset) +{ + struct midi_ump reply; + size_t stringlen = strlen(string); + size_t strwidth = sizeof(reply) - offset; + uint8_t format; + size_t i = 0; + int res = 0; + + while (i < stringlen) { + memset(&reply, 0, sizeof(reply)); + format = (i == 0) + ? (stringlen - i <= strwidth) + ? UMP_STREAM_FORMAT_COMPLETE + : UMP_STREAM_FORMAT_START + : (stringlen - i > strwidth) + ? UMP_STREAM_FORMAT_CONTINUE + : UMP_STREAM_FORMAT_END; + + reply.data[0] = (UMP_MT_UMP_STREAM << 28) + | (format << 26) + | prefix; + + i += fill_str(&reply, offset, &string[i], stringlen - i); + cfg->send(cfg->dev, reply); + res++; + } + + return res; +} + +/** + * @brief Handle Endpoint Discovery messages + * @param[in] cfg The responder configuration + * @param[in] pkt The discovery packet to handle + * @return The number of UMP sent as reply + */ +static inline int ump_ep_discover(const struct ump_stream_responder_cfg *cfg, + const struct midi_ump pkt) +{ + int res = 0; + uint8_t filter = UMP_STREAM_EP_DISCOVERY_FILTER(pkt); + + /* Request for Endpoint Info Notification */ + if ((filter & UMP_EP_DISC_FILTER_EP_INFO) != 0U) { + cfg->send(cfg->dev, make_endpoint_info(cfg->ep_spec)); + res++; + } + + /* Request for Endpoint Name Notification */ + if ((filter & UMP_EP_DISC_FILTER_EP_NAME) != 0U && cfg->ep_spec->name != NULL) { + res += send_string(cfg, cfg->ep_spec->name, + UMP_STREAM_STATUS_EP_NAME << 16, 2); + } + + /* Request for Product Instance ID */ + if ((filter & UMP_EP_DISC_FILTER_PRODUCT_ID) != 0U && IS_ENABLED(CONFIG_HWINFO)) { + res += send_string(cfg, ump_product_instance_id(), + UMP_STREAM_STATUS_PROD_ID << 16, 2); + } + + return res; +} + +static inline int ump_fb_discover_block(const struct ump_stream_responder_cfg *cfg, + size_t block_num, uint8_t filter) +{ + int res = 0; + const struct ump_block_dt_spec *blk = &cfg->ep_spec->blocks[block_num]; + + if ((filter & UMP_FB_DISC_FILTER_INFO) != 0U) { + cfg->send(cfg->dev, make_function_block_info(cfg->ep_spec, block_num)); + res++; + } + + if ((filter & UMP_FB_DISC_FILTER_NAME) != 0U && blk->name != NULL) { + res += send_string(cfg, blk->name, + (UMP_STREAM_STATUS_FB_NAME << 16) | (block_num << 8), 3); + } + + return res; +} + +/** + * @brief Handle Function Block Discovery messages + * @param[in] cfg The responder configuration + * @param[in] pkt The discovery packet to handle + * @return The number of UMP sent as reply + */ +static inline int ump_fb_discover(const struct ump_stream_responder_cfg *cfg, + const struct midi_ump pkt) +{ + int res = 0; + uint8_t block_num = UMP_STREAM_FB_DISCOVERY_NUM(pkt); + uint8_t filter = UMP_STREAM_FB_DISCOVERY_FILTER(pkt); + + if (block_num < cfg->ep_spec->n_blocks) { + res += ump_fb_discover_block(cfg, block_num, filter); + } else if (block_num == 0xff) { + /* Requesting information for all blocks at once */ + for (block_num = 0; block_num < cfg->ep_spec->n_blocks; block_num++) { + res += ump_fb_discover_block(cfg, block_num, filter); + } + } + + return res; +} + +const char *ump_product_instance_id(void) +{ + static char product_id[43] = ""; + static const char hex[] = "0123456789ABCDEF"; + + if (IS_ENABLED(CONFIG_HWINFO) && product_id[0] == '\0') { + uint8_t devid[sizeof(product_id) / 2]; + ssize_t len = hwinfo_get_device_id(devid, sizeof(devid)); + + if (len == -ENOSYS && hwinfo_get_device_eui64(devid) == 0) { + /* device id unavailable, but there is an eui64, + * which has a fixed length of 8 + */ + len = 8; + } else if (len < 0) { + /* Other hwinfo driver error; mark as empty */ + len = 0; + } + + /* Convert to hex string */ + for (ssize_t i = 0; i < len; i++) { + product_id[2 * i] = hex[devid[i] >> 4]; + product_id[2 * i + 1] = hex[devid[i] & 0xf]; + } + product_id[2*len] = '\0'; + } + + return product_id; +} + +int ump_stream_respond(const struct ump_stream_responder_cfg *cfg, + const struct midi_ump pkt) +{ + if (cfg->send == NULL) { + return -EINVAL; + } + + if (UMP_MT(pkt) != UMP_MT_UMP_STREAM) { + return 0; + } + + switch (UMP_STREAM_STATUS(pkt)) { + case UMP_STREAM_STATUS_EP_DISCOVERY: + return ump_ep_discover(cfg, pkt); + case UMP_STREAM_STATUS_FB_DISCOVERY: + return ump_fb_discover(cfg, pkt); + } + + return 0; +} diff --git a/lib/midi2/ump_stream_responder.h b/lib/midi2/ump_stream_responder.h new file mode 100644 index 0000000000000..420e206de38f3 --- /dev/null +++ b/lib/midi2/ump_stream_responder.h @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2025 Titouan Christophe + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_LIB_MIDI2_UMP_STREAM_RESPONDER_H_ +#define ZEPHYR_LIB_MIDI2_UMP_STREAM_RESPONDER_H_ + +/** + * @brief Respond to UMP Stream message Endpoint or Function Block discovery + * @defgroup ump_stream_responder UMP Stream Responder + * @ingroup midi_ump + * @since 4.3 + * @version 0.1.0 + * @see ump112 7.1: UMP Stream Messages + * @{ + */ + +#include +#include + +/** + * @brief UMP Function Block specification + * @see ump112: 6: Function Blocks + */ +struct ump_block_dt_spec { + /** Name of this function block, or NULL if unnamed */ + const char *name; + /** Number of the first UMP group in this block */ + uint8_t first_group; + /** Number of (contiguous) UMP groups spanned by this block */ + uint8_t groups_spanned; + /** True if this function block is an input */ + bool is_input; + /** True if this function block is an output */ + bool is_output; + /** True if this function block carries MIDI1 data only */ + bool is_midi1; + /** True if this function block is physically wired to a (MIDI1) + * serial interface, where data is transmitted at the standard + * baud rate of 31250 b/s + */ + bool is_31250bps; +}; + +/** + * @brief UMP endpoint specification + */ +struct ump_endpoint_dt_spec { + /** Name of this endpoint, or NULL if unnamed */ + const char *name; + /** Number of function blocks in this endpoint */ + size_t n_blocks; + /** Function blocks in this endpoint */ + struct ump_block_dt_spec blocks[]; +}; + +/** + * @brief Configuration for the UMP Stream responder + */ +struct ump_stream_responder_cfg { + /** The device to send reply packets */ + void *dev; + /** The function to call to send a reply packet */ + void (*send)(void *dev, const struct midi_ump ump); + /** The UMP endpoint specification */ + const struct ump_endpoint_dt_spec *ep_spec; +}; + +/** + * @brief Get a Universal MIDI Packet endpoint function block from its + * device-tree representation + * @param _node The device tree node representing the midi2 block + */ +#define UMP_BLOCK_DT_SPEC_GET(_node) \ +{ \ + .name = DT_PROP_OR(_node, label, NULL), \ + .first_group = DT_REG_ADDR(_node), \ + .groups_spanned = DT_REG_SIZE(_node), \ + .is_input = !DT_ENUM_HAS_VALUE(_node, terminal_type, output_only), \ + .is_output = !DT_ENUM_HAS_VALUE(_node, terminal_type, input_only), \ + .is_midi1 = !DT_ENUM_HAS_VALUE(_node, protocol, midi2), \ + .is_31250bps = DT_PROP(_node, serial_31250bps), \ +} + +#define UMP_BLOCK_SEP_IF_OKAY(_node) \ + COND_CODE_1(DT_NODE_HAS_STATUS_OKAY(_node), \ + (UMP_BLOCK_DT_SPEC_GET(_node),), \ + ()) + +/** + * @brief Get a Universal MIDI Packet endpoint description from + * the device-tree representation of a midi2 device + * @param _node The device tree node representing a midi2 device + */ +#define UMP_ENDPOINT_DT_SPEC_GET(_node) \ +{ \ + .name = DT_PROP_OR(_node, label, NULL), \ + .n_blocks = DT_FOREACH_CHILD_SEP(_node, DT_NODE_HAS_STATUS_OKAY, (+)), \ + .blocks = {DT_FOREACH_CHILD(_node, UMP_BLOCK_SEP_IF_OKAY)}, \ +} + +/** + * @brief Respond to an UMP Stream message + * @param[in] cfg The responder configuration + * @param[in] pkt The message to respond to + * @return The number of UMP packets sent as reply, + * or -errno in case of error + */ +int ump_stream_respond(const struct ump_stream_responder_cfg *cfg, + const struct midi_ump pkt); + +/** + * @return The UMP Product Instance ID of this device, based on the device + * hwinfo if available, otherwise an empty string + */ +const char *ump_product_instance_id(void); + +/** @} */ + +#endif diff --git a/samples/net/midi2/CMakeLists.txt b/samples/net/midi2/CMakeLists.txt new file mode 100644 index 0000000000000..189b31f8f60c2 --- /dev/null +++ b/samples/net/midi2/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2025 Titouan Christophe +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(netmidi2) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) + +include(${ZEPHYR_BASE}/samples/net/common/common.cmake) diff --git a/samples/net/midi2/Kconfig b/samples/net/midi2/Kconfig new file mode 100644 index 0000000000000..07a128aa09ddd --- /dev/null +++ b/samples/net/midi2/Kconfig @@ -0,0 +1,37 @@ +# Copyright (c) 2025 Titouan Christophe +# SPDX-License-Identifier: Apache-2.0 + +menu "Networking MIDI2 sample application" + +choice NET_SAMPLE_MIDI2_AUTH_TYPE + prompt "UMP Endpoint authentication method" + default NET_SAMPLE_MIDI2_AUTH_NONE + +config NET_SAMPLE_MIDI2_AUTH_NONE + bool "No authentication" + +config NET_SAMPLE_MIDI2_AUTH_SHARED_SECRET + bool "Authentication with shared secret" + select NETMIDI2_HOST_AUTH + +config NET_SAMPLE_MIDI2_AUTH_USER_PASSWORD + bool "Authentication with username+password" + select NETMIDI2_HOST_AUTH + +endchoice + +config NET_SAMPLE_MIDI2_SHARED_SECRET + string "Shared secret" + depends on NET_SAMPLE_MIDI2_AUTH_SHARED_SECRET + +config NET_SAMPLE_MIDI2_USERNAME + string "Username" + depends on NET_SAMPLE_MIDI2_AUTH_USER_PASSWORD + +config NET_SAMPLE_MIDI2_PASSWORD + string "Password" + depends on NET_SAMPLE_MIDI2_AUTH_USER_PASSWORD + +endmenu + +source "Kconfig.zephyr" diff --git a/samples/net/midi2/README.rst b/samples/net/midi2/README.rst new file mode 100644 index 0000000000000..ddb28304bbf48 --- /dev/null +++ b/samples/net/midi2/README.rst @@ -0,0 +1,75 @@ +.. zephyr:code-sample:: netmidi2 + :name: MIDI2 network transport + :relevant-api: midi_ump net_midi2 + + Exchange Universal MIDI Packets over the Network MIDI 2.0 protocol + +Overview +******** + +This sample demonstrates usage of the Network MIDI 2.0 stack: + +* start a UMP Endpoint host reachable on the network +* respond to UMP Stream discovery messages, so that clients can discover the + topology described in the device tree +* if ``midi_serial`` port is defined in the device tree, + send MIDI1 data from UMP group 9 there +* if ``midi_green_led`` node is defined in the device tree, + light up the led when sending data on the serial port + +Requirements +************ + +This sample requires a board with IP networking support. To perform anything +useful against the running sample, you will also need a Network MIDI2.0 client +to connect to the target, for example `pymidi2`_ + +Building and Running +******************** + +The easiest way to try out this sample without any hardware is using +``native_sim``. See :ref:`native_sim ethernet driver ` to setup +networking on your computer accordingly + +.. zephyr-app-commands:: + :zephyr-app: samples/net/midi2/ + :board: native_sim + :goals: run + +The Network MIDI 2.0 endpoint should now be reachable on the network: + +.. code-block:: console + + $ pymidi2 find + Zephyr-UDP-MIDI2 (udp://192.0.2.1:45486) + - Block #0 [io : Recv/Send] 'Synthesizer' UMP groups {0, 1, 2, 3} [MIDI1 + MIDI2] + - Block #1 [i- : Recv ] 'Keyboard' UMP groups {8} [MIDI1 only] + - Block #2 [-o : Send] 'External output (MIDI DIN-5)' UMP groups {9} [MIDI1 31.25kb/s] + + +Furthermore, this sample pairs well with the :ref:`olimex_shield_midi`, +that conveniently defines the device tree nodes for the external MIDI OUT +and its led. For example, using this shield on the ST Nucleo F429zi: + +.. zephyr-app-commands:: + :zephyr-app: samples/net/midi2 + :board: nucleo_f429zi + :shield: olimex_shield_midi + :goals: build flash + +Using authentication +******************** + +To enable shared secret authentication to connect to the UMP endpoint host, +enable :kconfig:option:`CONFIG_NET_SAMPLE_MIDI2_AUTH_SHARED_SECRET`, +and then configure the desired shared secret in +:kconfig:option:`CONFIG_NET_SAMPLE_MIDI2_SHARED_SECRET` + +To enable user/password authentication instead, enable +:kconfig:option:`CONFIG_NET_SAMPLE_MIDI2_AUTH_USER_PASSWORD`, and then +configure the desired username/password in +:kconfig:option:`CONFIG_NET_SAMPLE_MIDI2_USERNAME` +and :kconfig:option:`CONFIG_NET_SAMPLE_MIDI2_PASSWORD` + +.. _pymidi2: + https://github.com/titouanc/pymidi2 diff --git a/samples/net/midi2/app.overlay b/samples/net/midi2/app.overlay new file mode 100644 index 0000000000000..bc9b9d29b3a78 --- /dev/null +++ b/samples/net/midi2/app.overlay @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 Titouan Christophe + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + midi2: zephyr-midi2 { + compatible = "zephyr,midi2-device"; + status = "okay"; + #address-cells = <1>; + #size-cells = <1>; + label = "Zephyr-UDP-MIDI2"; + + midi2@0 { + reg = <0 4>; + label = "Synthesizer"; + protocol = "midi2"; + }; + + midi1@8 { + reg = <8 1>; + protocol = "midi1-up-to-64b"; + terminal-type = "input-only"; + label = "Keyboard"; + }; + + ext_midi_out: midi1@9 { + reg = <9 1>; + protocol = "midi1-up-to-64b"; + terminal-type = "output-only"; + label = "External output (MIDI DIN-5)"; + serial-31250bps; + }; + }; +}; diff --git a/samples/net/midi2/prj.conf b/samples/net/midi2/prj.conf new file mode 100644 index 0000000000000..49dcaadd77bde --- /dev/null +++ b/samples/net/midi2/prj.conf @@ -0,0 +1,19 @@ +CONFIG_NETWORKING=y +CONFIG_ZVFS_POLL_MAX=4 +CONFIG_NET_IF_MCAST_IPV6_ADDR_COUNT=5 +CONFIG_NET_CONFIG_SETTINGS=y +CONFIG_NET_CONFIG_NEED_IPV6=y +CONFIG_NET_CONFIG_MY_IPV6_ADDR="2001:db8::1" +CONFIG_NET_CONFIG_NEED_IPV4=y +CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.0.2.1" + +CONFIG_NET_HOSTNAME_ENABLE=y +CONFIG_MDNS_RESPONDER=y +CONFIG_DNS_SD=y + +CONFIG_NETMIDI2_HOST=y +CONFIG_MIDI2_UMP_STREAM_RESPONDER=y + +CONFIG_LOG=y +CONFIG_NET_LOG=y +CONFIG_TEST_RANDOM_GENERATOR=y diff --git a/samples/net/midi2/sample.yaml b/samples/net/midi2/sample.yaml new file mode 100644 index 0000000000000..89d4220802c8c --- /dev/null +++ b/samples/net/midi2/sample.yaml @@ -0,0 +1,20 @@ +common: + harness: net + depends_on: netif + tags: + - net + - midi2 +sample: + name: Network MIDI2.0 sample application + description: Demonstrates usage of the Network MIDI2.0 (udp) host endpoint +tests: + sample.net.midi2.host: {} + sample.net.midi2.host_auth_shared: + extra_configs: + - CONFIG_NET_SAMPLE_MIDI2_AUTH_SHARED_SECRET=y + - CONFIG_NET_SAMPLE_MIDI2_SHARED_SECRET="some-secret" + sample.net.midi2.host_auth_user: + extra_configs: + - CONFIG_NET_SAMPLE_MIDI2_AUTH_USER_PASSWORD=y + - CONFIG_NET_SAMPLE_MIDI2_USERNAME="user" + - CONFIG_NET_SAMPLE_MIDI2_PASSWORD="passwd" diff --git a/samples/net/midi2/src/main.c b/samples/net/midi2/src/main.c new file mode 100644 index 0000000000000..401671e7d7b0c --- /dev/null +++ b/samples/net/midi2/src/main.c @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2025 Titouan Christophe + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include + +#include + +#include +LOG_MODULE_REGISTER(net_midi2_sample, LOG_LEVEL_DBG); + +#define ACT_LED_NODE DT_NODELABEL(midi_green_led) +#define SERIAL_NODE DT_NODELABEL(midi_serial) + +#if !DT_NODE_EXISTS(ACT_LED_NODE) +#define CONFIGURE_LED() +#define SET_LED(_state) +#else /* DT_NODE_EXISTS(ACT_LED_NODE) */ +#include + +static const struct gpio_dt_spec act_led = GPIO_DT_SPEC_GET(ACT_LED_NODE, gpios); + +#define CONFIGURE_LED() gpio_pin_configure_dt(&act_led, GPIO_OUTPUT_INACTIVE) +#define SET_LED(_state) gpio_pin_set_dt(&act_led, (_state)) +#endif /* DT_NODE_EXISTS(ACT_LED_NODE) */ + + +#if !DT_NODE_EXISTS(SERIAL_NODE) +#define send_external_midi1(...) +#else /* DT_NODE_EXISTS(SERIAL_NODE) */ +#include + +static const struct device *const uart_dev = DEVICE_DT_GET(SERIAL_NODE); + +static inline void send_external_midi1(const struct midi_ump ump) +{ + /* Only send MIDI events aimed at the external output */ + if (UMP_GROUP(ump) != DT_REG_ADDR(DT_NODELABEL(ext_midi_out))) { + return; + } + + switch (UMP_MIDI_COMMAND(ump)) { + case UMP_MIDI_PROGRAM_CHANGE: + SET_LED(1); + uart_poll_out(uart_dev, UMP_MIDI_STATUS(ump)); + uart_poll_out(uart_dev, UMP_MIDI1_P1(ump)); + SET_LED(0); + break; + + case UMP_MIDI_NOTE_OFF: + case UMP_MIDI_NOTE_ON: + case UMP_MIDI_AFTERTOUCH: + case UMP_MIDI_CONTROL_CHANGE: + case UMP_MIDI_PITCH_BEND: + SET_LED(1); + uart_poll_out(uart_dev, UMP_MIDI_STATUS(ump)); + uart_poll_out(uart_dev, UMP_MIDI1_P1(ump)); + uart_poll_out(uart_dev, UMP_MIDI1_P2(ump)); + SET_LED(0); + break; + } +} +#endif /* DT_NODE_EXISTS(SERIAL_NODE) */ + + +static const struct ump_endpoint_dt_spec ump_ep_dt = + UMP_ENDPOINT_DT_SPEC_GET(DT_NODELABEL(midi2)); + +static inline void handle_ump_stream(struct netmidi2_session *session, + const struct midi_ump ump) +{ + const struct ump_stream_responder_cfg responder_cfg = { + .dev = session, + .send = (void (*)(void *, const struct midi_ump)) netmidi2_send, + .ep_spec = &ump_ep_dt, + }; + ump_stream_respond(&responder_cfg, ump); +} + +static void netmidi2_callback(struct netmidi2_session *session, + const struct midi_ump ump) +{ + switch (UMP_MT(ump)) { + case UMP_MT_MIDI1_CHANNEL_VOICE: + send_external_midi1(ump); + break; + case UMP_MT_UMP_STREAM: + handle_ump_stream(session, ump); + break; + } +} + +#if defined(CONFIG_NET_SAMPLE_MIDI2_AUTH_NONE) +/* Simple Network MIDI 2.0 endpoint without authentication */ +NETMIDI2_EP_DEFINE(midi_server, ump_ep_dt.name, NULL, 0); + +#elif defined(CONFIG_NET_SAMPLE_MIDI2_AUTH_SHARED_SECRET) +/* Network MIDI 2.0 endpoint with shared secret authentication */ +BUILD_ASSERT( + sizeof(CONFIG_NET_SAMPLE_MIDI2_SHARED_SECRET) > 1, + "CONFIG_NET_SAMPLE_MIDI2_SHARED_SECRET must be not empty" +); + +NETMIDI2_EP_DEFINE_WITH_AUTH(midi_server, ump_ep_dt.name, NULL, 0, + CONFIG_NET_SAMPLE_MIDI2_SHARED_SECRET); + +#elif defined(CONFIG_NET_SAMPLE_MIDI2_AUTH_USER_PASSWORD) +/* Network MIDI 2.0 endpoint with a single user/password*/ +BUILD_ASSERT( + sizeof(CONFIG_NET_SAMPLE_MIDI2_USERNAME) > 1, + "CONFIG_NET_SAMPLE_MIDI2_USERNAME must be not empty" +); +BUILD_ASSERT( + sizeof(CONFIG_NET_SAMPLE_MIDI2_PASSWORD) > 1, + "CONFIG_NET_SAMPLE_MIDI2_PASSWORD must be not empty" +); + +NETMIDI2_EP_DEFINE_WITH_USERS(midi_server, ump_ep_dt.name, NULL, 0, + {.name = CONFIG_NET_SAMPLE_MIDI2_USERNAME, + .password = CONFIG_NET_SAMPLE_MIDI2_PASSWORD}); + +#endif + +DNS_SD_REGISTER_SERVICE(midi_dns, CONFIG_NET_HOSTNAME "-" CONFIG_BOARD, + "_midi2", "_udp", "local", DNS_SD_EMPTY_TXT, + &midi_server.addr4.sin_port); + +int main(void) +{ + CONFIGURE_LED(); + + midi_server.rx_packet_cb = netmidi2_callback; + netmidi2_host_ep_start(&midi_server); + + return 0; +} diff --git a/samples/subsys/usb/midi/app.overlay b/samples/subsys/usb/midi/app.overlay index 7d8ba72ef5cb3..5bf8eef1835c8 100644 --- a/samples/subsys/usb/midi/app.overlay +++ b/samples/subsys/usb/midi/app.overlay @@ -10,10 +10,12 @@ status = "okay"; #address-cells = <1>; #size-cells = <1>; + label = "Zephyr USB-MIDI Sample"; midi_in_out@0 { reg = <0 1>; protocol = "midi1-up-to-128b"; + label = "MIDI-IN-OUT"; }; }; }; diff --git a/samples/subsys/usb/midi/prj.conf b/samples/subsys/usb/midi/prj.conf index 02e2d52c7edd3..ed1e8d7d6988a 100644 --- a/samples/subsys/usb/midi/prj.conf +++ b/samples/subsys/usb/midi/prj.conf @@ -10,3 +10,5 @@ CONFIG_INPUT=y CONFIG_LOG=y CONFIG_USBD_LOG_LEVEL_WRN=y CONFIG_UDC_DRIVER_LOG_LEVEL_WRN=y + +CONFIG_MIDI2_UMP_STREAM_RESPONDER=y diff --git a/samples/subsys/usb/midi/src/main.c b/samples/subsys/usb/midi/src/main.c index 4581d1f01835b..cd1b045c2a2ef 100644 --- a/samples/subsys/usb/midi/src/main.c +++ b/samples/subsys/usb/midi/src/main.c @@ -14,10 +14,14 @@ #include #include +#include + #include LOG_MODULE_REGISTER(sample_usb_midi, LOG_LEVEL_INF); -static const struct device *const midi = DEVICE_DT_GET(DT_NODELABEL(usb_midi)); +#define USB_MIDI_DT_NODE DT_NODELABEL(usb_midi) + +static const struct device *const midi = DEVICE_DT_GET(USB_MIDI_DT_NODE); static struct gpio_dt_spec led = GPIO_DT_SPEC_GET_OR(DT_ALIAS(led0), gpios, {0}); @@ -38,15 +42,29 @@ static void key_press(struct input_event *evt, void *user_data) } INPUT_CALLBACK_DEFINE(NULL, key_press, NULL); +static const struct ump_endpoint_dt_spec ump_ep_dt = + UMP_ENDPOINT_DT_SPEC_GET(USB_MIDI_DT_NODE); + +const struct ump_stream_responder_cfg responder_cfg = { + .dev = midi, + .send = (void (*)(void *, const struct midi_ump)) usbd_midi_send, + .ep_spec = &ump_ep_dt, +}; + static void on_midi_packet(const struct device *dev, const struct midi_ump ump) { LOG_INF("Received MIDI packet (MT=%X)", UMP_MT(ump)); - /* Only send MIDI1 channel voice messages back to the host */ - if (UMP_MT(ump) == UMP_MT_MIDI1_CHANNEL_VOICE) { + switch (UMP_MT(ump)) { + case UMP_MT_MIDI1_CHANNEL_VOICE: + /* Only send MIDI1 channel voice messages back to the host */ LOG_INF("Send back MIDI1 message %02X %02X %02X", UMP_MIDI_STATUS(ump), UMP_MIDI1_P1(ump), UMP_MIDI1_P2(ump)); usbd_midi_send(dev, ump); + break; + case UMP_MT_UMP_STREAM: + ump_stream_respond(&responder_cfg, ump); + break; } } diff --git a/subsys/net/lib/CMakeLists.txt b/subsys/net/lib/CMakeLists.txt index a2b11f44b0bb1..7b7be67015daa 100644 --- a/subsys/net/lib/CMakeLists.txt +++ b/subsys/net/lib/CMakeLists.txt @@ -21,6 +21,7 @@ add_subdirectory_ifdef(CONFIG_NET_DHCPV6 dhcpv6) add_subdirectory_ifdef(CONFIG_PROMETHEUS prometheus) add_subdirectory_ifdef(CONFIG_WIFI_CREDENTIALS wifi_credentials) add_subdirectory_ifdef(CONFIG_OCPP ocpp) +add_subdirectory_ifdef(CONFIG_NETMIDI2_HOST midi2) if (CONFIG_NET_DHCPV4 OR CONFIG_NET_DHCPV4_SERVER) add_subdirectory(dhcpv4) diff --git a/subsys/net/lib/Kconfig b/subsys/net/lib/Kconfig index 9ffb49fbb68e1..4249ca650f75c 100644 --- a/subsys/net/lib/Kconfig +++ b/subsys/net/lib/Kconfig @@ -9,6 +9,8 @@ source "subsys/net/lib/dns/Kconfig" source "subsys/net/lib/latmon/Kconfig" +source "subsys/net/lib/midi2/Kconfig" + source "subsys/net/lib/mqtt/Kconfig" source "subsys/net/lib/mqtt_sn/Kconfig" diff --git a/subsys/net/lib/midi2/CMakeLists.txt b/subsys/net/lib/midi2/CMakeLists.txt new file mode 100644 index 0000000000000..f86aa960b7ec4 --- /dev/null +++ b/subsys/net/lib/midi2/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (c) 2025 Titouan Christophe +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() + +zephyr_library_sources(netmidi2.c) diff --git a/subsys/net/lib/midi2/Kconfig b/subsys/net/lib/midi2/Kconfig new file mode 100644 index 0000000000000..c797d82ff1f5c --- /dev/null +++ b/subsys/net/lib/midi2/Kconfig @@ -0,0 +1,37 @@ +# Copyright (c) 2025 Titouan Christophe +# SPDX-License-Identifier: Apache-2.0 + +config NETMIDI2_HOST + bool "Network MIDI2 (UDP) host [EXPERIMENTAL]" + select EXPERIMENTAL + select NET_UDP + select NET_SOCKETS + select NET_SOCKETS_SERVICE + depends on NET_IPV4 || NET_IPV6 + imply NET_IPV4_MAPPING_TO_IPV6 if NET_IPV4 && NET_IPV6 + help + Host library of User Datagram Protocol for Universal MIDI Packets. + Provides the following features: + - Exposing an UMP endpoint over UDP + - Accepting inbound client invitations + - Sending/Receiving Universal MIDI packets + Following "Network MIDI 2.0 (UDP) Transport Specification" v1.0 + +if NETMIDI2_HOST +config NETMIDI2_HOST_MAX_CLIENTS + int "Maximum number of clients supported by the Network MIDI2 host" + default 5 + +config NETMIDI2_HOST_AUTH + bool "Support for authentication (shared key or user/password)" + select MBEDTLS + select MBEDTLS_CIPHER_CCM_ENABLED + select CRYPTO + select CRYPTO_MBEDTLS_SHIM + +module=NET_MIDI2 +module-dep=NET_LOG +module-str=Log level for network MIDI2 +module-help=Enables midi2 debug messages. +source "subsys/net/Kconfig.template.log_config.net" +endif diff --git a/subsys/net/lib/midi2/netmidi2.c b/subsys/net/lib/midi2/netmidi2.c new file mode 100644 index 0000000000000..4ab8ec6a3a636 --- /dev/null +++ b/subsys/net/lib/midi2/netmidi2.c @@ -0,0 +1,826 @@ +/* + * Copyright (c) 2025 Titouan Christophe + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include + +#if defined(CONFIG_MIDI2_UMP_STREAM_RESPONDER) +#include +#endif + +#include +LOG_MODULE_REGISTER(net_midi2, CONFIG_NET_MIDI2_LOG_LEVEL); + +#define NETMIDI2_BUFSIZE 256 + +NET_BUF_POOL_DEFINE(netmidi2_pool, 2 + CONFIG_NETMIDI2_HOST_MAX_CLIENTS, + NETMIDI2_BUFSIZE, 0, NULL); + +/** + * Size, in bytes, of the digest sent by the client to authenticate (sha256) + */ +#define NETMIDI2_DIGEST_SIZE 32 + +/* See netmidi10: 5.5: Command Codes and Packet Types */ +#define COMMAND_INVITATION 0x01 +#define COMMAND_INVITATION_WITH_AUTH 0x02 +#define COMMAND_INVITATION_WITH_USER_AUTH 0x03 +#define COMMAND_INVITATION_REPLY_ACCEPTED 0x10 +#define COMMAND_INVITATION_REPLY_PENDING 0x11 +#define COMMAND_INVITATION_REPLY_AUTH_REQUIRED 0x12 +#define COMMAND_INVITATION_REPLY_USER_AUTH_REQUIRED 0x13 +#define COMMAND_PING 0x20 +#define COMMAND_PING_REPLY 0x21 +#define COMMAND_RETRANSMIT_REQUEST 0x80 +#define COMMAND_RETRANSMIT_ERROR 0x81 +#define COMMAND_SESSION_RESET 0x82 +#define COMMAND_SESSION_RESET_REPLY 0x83 +#define COMMAND_NAK 0x8F +#define COMMAND_BYE 0xF0 +#define COMMAND_BYE_REPLY 0xF1 +#define COMMAND_UMP_DATA 0xFF + +/* See netmidi10: 6.4 / Table 11: Capabilities for Invitation */ +#define CLIENT_CAP_INV_WITH_AUTH BIT(0) +#define CLIENT_CAP_INV_WITH_USER_AUTH BIT(1) + +/* See netmidi10: 6.7 / Table 15: Values for Authentication State */ +#define AUTH_STATE_FIRST_REQUEST 0x00 +#define AUTH_STATE_INCORRECT_DIGEST 0x01 + +/* See netmidi10: 6.15 / Table 25: List of NAK Reasons */ +#define NAK_OTHER 0x00 +#define NAK_COMMAND_NOT_SUPPORTED 0x01 +#define NAK_COMMAND_NOT_EXPECTED 0x02 +#define NAK_COMMAND_MALFORMED 0x03 +#define NAK_BAD_PING_REPLY 0x20 + +/* Logging macros that include the peer's address:port */ +#define SESS_LOG_DBG(_s, _fmt, ...) SESS_LOG(DBG, _s, _fmt, ##__VA_ARGS__) +#define SESS_LOG_INF(_s, _fmt, ...) SESS_LOG(INFO, _s, _fmt, ##__VA_ARGS__) +#define SESS_LOG_WRN(_s, _fmt, ...) SESS_LOG(WARN, _s, _fmt, ##__VA_ARGS__) +#define SESS_LOG_ERR(_s, _fmt, ...) SESS_LOG(ERR, _s, _fmt, ##__VA_ARGS__) + +#define SESS_LOG(_lvl, _s, _fmt, ...) \ + do { \ + const struct sockaddr *addr = net_sad(&(_s)->addr); \ + const struct sockaddr_in6 *addr6 = net_sin6(addr); \ + char __pn[INET6_ADDRSTRLEN]; \ + net_addr_ntop(addr->sa_family, &addr6->sin6_addr, __pn, sizeof(__pn)); \ + NET_##_lvl("%s:%d " _fmt, __pn, addr6->sin6_port, ##__VA_ARGS__); \ + } while (0) + +#define SESSION_HAS_STATE(session, expected_state) \ + ((session) && (session)->state == expected_state) + +#if defined(CONFIG_NETMIDI2_HOST_AUTH) +#include +#include + +static inline const struct netmidi2_user *netmidi2_find_user(const struct netmidi2_ep *ep, + const char *name, size_t namelen) +{ + if (ep->auth_type != NETMIDI2_AUTH_USER_PASSWORD) { + return NULL; + } + + for (size_t i = 0; i < ep->userlist->n_users; i++) { + if (strncmp(ep->userlist->users[i].name, name, namelen) == 0) { + return &ep->userlist->users[i]; + } + } + + return NULL; +} + +static bool netmidi2_auth_session(const struct netmidi2_session *sess, + struct net_buf *buf, size_t payload_len) +{ + const struct device *hasher = device_get_binding(CONFIG_CRYPTO_MBEDTLS_SHIM_DRV_NAME); + const uint8_t *auth_digest = buf->data; + const struct netmidi2_user *user; + uint8_t output[NETMIDI2_DIGEST_SIZE]; + struct hash_ctx ctx = {.flags = crypto_query_hwcaps(hasher)}; + struct hash_pkt hash = {.out_buf = output, .ctx = &ctx}; + int ret; + + if (hasher == NULL) { + SESS_LOG_ERR(sess, "mbedtls crypto pseudo-device unavailable"); + return false; + } + + if (buf->len < NETMIDI2_DIGEST_SIZE || payload_len < NETMIDI2_DIGEST_SIZE) { + SESS_LOG_ERR(sess, "Incomplete authentication digest"); + return false; + } + + /* Pull authentication digest from the command packet */ + net_buf_pull(buf, NETMIDI2_DIGEST_SIZE); + + ret = hash_begin_session(hasher, &ctx, CRYPTO_HASH_ALGO_SHA256); + if (ret != 0) { + SESS_LOG_ERR(sess, "Unable to begin hash session"); + return false; + } + + /* 1. Start hashing with the session nonce */ + hash.in_buf = (uint8_t *) sess->nonce; + hash.in_len = NETMIDI2_NONCE_SIZE; + ret = hash_update(&ctx, &hash); + if (ret != 0) { + SESS_LOG_ERR(sess, "Unable to hash nonce"); + goto end; + } + + if (sess->ep->auth_type == NETMIDI2_AUTH_SHARED_SECRET) { + /* 2. Finalize hashing with the shared secret */ + hash.in_buf = (uint8_t *) sess->ep->shared_auth_secret; + hash.in_len = strlen(sess->ep->shared_auth_secret); + ret = hash_compute(&ctx, &hash); + if (ret != 0) { + SESS_LOG_ERR(sess, "Unable to hash shared secret"); + goto end; + } + } else if (sess->ep->auth_type == NETMIDI2_AUTH_USER_PASSWORD) { + user = netmidi2_find_user(sess->ep, buf->data, + payload_len - NETMIDI2_DIGEST_SIZE); + if (user == NULL) { + LOG_ERR("No matching user found"); + goto end; + } + + /* Remove username from buffer */ + net_buf_pull(buf, payload_len - NETMIDI2_DIGEST_SIZE); + + /* 2. Continue hashing with the username */ + hash.in_buf = (uint8_t *) user->name; + hash.in_len = strlen(user->name); + ret = hash_update(&ctx, &hash); + if (ret != 0) { + SESS_LOG_ERR(sess, "Unable to hash username"); + goto end; + } + + /* 3. Finalize hashing with the password */ + hash.in_buf = (uint8_t *) user->password; + hash.in_len = strlen(user->password); + ret = hash_compute(&ctx, &hash); + if (ret != 0) { + SESS_LOG_ERR(sess, "Unable to hash password"); + goto end; + } + } + +end: + hash_free_session(hasher, &ctx); + return ret == 0 && memcmp(hash.out_buf, auth_digest, NETMIDI2_DIGEST_SIZE) == 0; +} +#endif /* CONFIG_NETMIDI2_HOST_AUTH */ + +static inline void netmidi2_free_session(struct netmidi2_session *session) +{ + SESS_LOG_INF(session, "Free client session"); + + k_work_cancel(&session->tx_work); + if (session->tx_buf != NULL) { + net_buf_unref(session->tx_buf); + } + memset(session, 0, sizeof(*session) - sizeof(struct k_work)); +} + +static inline struct netmidi2_session *netmidi2_match_session(struct netmidi2_ep *ep, + struct sockaddr *peer_addr, + socklen_t peer_addr_len) +{ + for (size_t i = 0; i < CONFIG_NETMIDI2_HOST_MAX_CLIENTS; i++) { + if (ep->peers[i].addr_len == peer_addr_len && + memcmp(&ep->peers[i].addr, peer_addr, peer_addr_len) == 0) { + LOG_DBG("Found matching client session %d", i); + return &ep->peers[i]; + } + } + + return NULL; +} + +static inline void netmidi2_free_inactive_sessions(struct netmidi2_ep *ep) +{ + struct netmidi2_session *sess; + const uint8_t bye_timeout[] = {COMMAND_BYE, 0, 0x04, 0}; + + for (size_t i = 0; i < CONFIG_NETMIDI2_HOST_MAX_CLIENTS; i++) { + sess = &ep->peers[i]; + if (sess->state != NETMIDI2_SESSION_IDLE && + sess->state != NETMIDI2_SESSION_ESTABLISHED) { + SESS_LOG_WRN(sess, "Cleanup inactive session"); + zsock_sendto(ep->pollsock.fd, bye_timeout, sizeof(bye_timeout), + 0, net_sad(&sess->addr), sess->addr_len); + netmidi2_free_session(sess); + } + } +} + +static inline struct netmidi2_session *netmidi2_try_alloc_session(struct netmidi2_ep *ep, + struct sockaddr *peer_addr, + socklen_t peer_addr_len) +{ + struct netmidi2_session *sess; + + for (size_t i = 0; i < CONFIG_NETMIDI2_HOST_MAX_CLIENTS; i++) { + sess = &ep->peers[i]; + if (sess->state == NETMIDI2_SESSION_NOT_INITIALIZED) { + sess->state = NETMIDI2_SESSION_IDLE; + sess->addr_len = peer_addr_len; + sess->ep = ep; + memcpy(&sess->addr, peer_addr, peer_addr_len); + SESS_LOG_INF(sess, "new client session (%d)", i); + return sess; + } + } + + return NULL; +} + +static inline struct netmidi2_session *netmidi2_alloc_session(struct netmidi2_ep *ep, + struct sockaddr *peer_addr, + socklen_t peer_addr_len) +{ + struct netmidi2_session *session; + + session = netmidi2_try_alloc_session(ep, peer_addr, peer_addr_len); + if (session == NULL) { + netmidi2_free_inactive_sessions(ep); + session = netmidi2_try_alloc_session(ep, peer_addr, peer_addr_len); + } + + if (session == NULL) { + LOG_ERR("No available client session"); + } + + return session; +} + +/** + * @brief Perform transmission work for an endpoint peer's session + * @param work The work item (must be contained in a netmidi2_session) + */ +static void netmidi2_session_tx_work(struct k_work *work) +{ + struct netmidi2_session *session = CONTAINER_OF(work, struct netmidi2_session, tx_work); + struct net_buf *buf = session->tx_buf; + + session->tx_buf = NULL; + + zsock_sendto(session->ep->pollsock.fd, buf->data, buf->len, 0, + net_sad(&session->addr), session->addr_len); + net_buf_unref(buf); +} + +static inline const char *netmidi2_ep_get_name(const struct netmidi2_ep *ep) +{ + return (ep->name == NULL) ? "" : ep->name; +} + +#if defined(CONFIG_MIDI2_UMP_STREAM_RESPONDER) +#define DEFAULT_PIID ump_product_instance_id() +#else +#define DEFAULT_PIID "" +#endif + +static inline const char *netmidi2_ep_get_piid(const struct netmidi2_ep *ep) +{ + return (ep->piid == NULL) ? DEFAULT_PIID : ep->piid; +} + +/** + * @brief Write the header for a CommandPacket into a session tx buffer + * @param sess The peer's session + * @param[in] command_code The command code + * @param[in] command_specific_data The command specific data + * @param[in] payload_len_words Payload length, in 32b words + * @return 0 on success, -errno on error + */ +static inline int sess_buf_add_header(struct netmidi2_session *sess, + const uint8_t command_code, + const uint16_t command_specific_data, + const uint8_t payload_len_words) +{ + if (sess->tx_buf == NULL) { + /* If no current tx buffer, allocate a new one */ + sess->tx_buf = net_buf_alloc(&netmidi2_pool, K_FOREVER); + if (sess->tx_buf == NULL) { + SESS_LOG_ERR(sess, "Unable to allocate Tx buffer"); + return -ENOBUFS; + } + /* Prefix with Network MIDI2.0 UDP header */ + net_buf_add_mem(sess->tx_buf, "MIDI", 4); + } + + if (net_buf_tailroom(sess->tx_buf) < 4*(1 + payload_len_words)) { + SESS_LOG_WRN(sess, "Not enough room in Tx buffer"); + return -ENOMEM; + } + + net_buf_add_u8(sess->tx_buf, command_code); + net_buf_add_u8(sess->tx_buf, payload_len_words); + net_buf_add_be16(sess->tx_buf, command_specific_data); + + return 0; +} + +/** + * @brief Write some bytes into a session tx buffer, and add padding + * zeros at the tail to stay aligned on 4 bytes + * @param session The session to write to + * @param[in] mem The memory to be copied + * @param[in] size The size in bytes of the memory to be copied + */ +static inline void sess_buf_add_mem_padded(struct netmidi2_session *session, + const uint8_t *mem, size_t size) +{ + if (session->tx_buf == NULL) { + return; + } + + net_buf_add_mem(session->tx_buf, mem, size); + + switch (size % 4) { + case 1: + net_buf_add_be24(session->tx_buf, 0); + break; + case 2: + net_buf_add_be16(session->tx_buf, 0); + break; + case 3: + net_buf_add_u8(session->tx_buf, 0); + break; + } +} + +/** + * @brief Send a Command Packet (from words) to a client session + * @remark The Command Packet is appended to the session's tx buffer, + * and transmission is scheduled, so the command packet is not + * transmitted immediately, and may be sent along with others in + * a single Network MIDI2.0 UDP packet. + * @param sess The recipient session + * @param[in] command_code The command code + * @param[in] command_specific_data The command specific data + * @param[in] payload The command payload as words (4B) + * @param[in] payload_len_words Payload length, in words (4B) + * @return 0 on success, -errno otherwise + * + * @see netmidi10: 5.4 Command Packet Header and Payload + */ +static int netmidi2_session_sendcmd(struct netmidi2_session *sess, + const uint8_t command_code, + const uint16_t command_specific_data, + const uint32_t *payload, + const uint8_t payload_len_words) +{ + int ret = sess_buf_add_header(sess, command_code, + command_specific_data, + payload_len_words); + if (ret != 0) { + return ret; + } + + for (size_t i = 0; i < payload_len_words; i++) { + net_buf_add_be32(sess->tx_buf, payload[i]); + } + k_work_submit(&sess->tx_work); + return 0; +} + +/** + * @brief Immediately send a Command Packet to a remote without client session + * @param[in] ep The emitting UMP endpoint + * @param[in] peer_addr The recipient's address + * @param[in] peer_addr_len The recipient's address length + * @param[in] command_specific_data The command specific data + * @param[in] payload The command payload + * @param[in] payload_len_words Payload length, in words (4B) + */ +static int netmidi2_quick_reply(struct netmidi2_ep *ep, + const struct sockaddr *peer_addr, + const socklen_t peer_addr_len, + const uint8_t command_code, + const uint16_t command_specific_data, + const uint32_t *payload, + const uint8_t payload_len_words) +{ + NET_BUF_SIMPLE_DEFINE(txbuf, 28); + + if (4 * (1 + payload_len_words) > txbuf.size) { + return -ENOBUFS; + } + + /* Network MIDI2.0 UDP header */ + net_buf_simple_add_mem(&txbuf, "MIDI", 4); + /* Command packet header */ + net_buf_simple_add_u8(&txbuf, command_code); + net_buf_simple_add_u8(&txbuf, payload_len_words); + net_buf_simple_add_be16(&txbuf, command_specific_data); + + /* Payload */ + for (size_t i = 0; i < payload_len_words; i++) { + net_buf_simple_add_be32(&txbuf, payload[i]); + } + + zsock_sendto(ep->pollsock.fd, txbuf.data, txbuf.len, 0, + peer_addr, peer_addr_len); + return 0; +} + +/** + * @brief Quickly send a NAK message to a remote without client session + * @param[in] ep The endpoint sending the NAK + * @param[in] peer_addr The peer address + * @param[in] peer_addr_len The peer address length + * @param[in] nak_reason The NAK reason + * @param[in] nakd_cmd_header The command packet header this NAK is replying to + */ +static inline int netmidi2_quick_nak(struct netmidi2_ep *ep, + const struct sockaddr *peer_addr, + const socklen_t peer_addr_len, + const uint8_t nak_reason, + const uint32_t nakd_cmd_header) +{ + return netmidi2_quick_reply(ep, peer_addr, peer_addr_len, + COMMAND_NAK, nak_reason << 8, + &nakd_cmd_header, 1); +} + +/** + * @brief Send a message "Invitation reply: ..." to a client. + * The type of Invitation Reply command code depends on the + * session state + * @param session The session to which the message shall be send + * @return 0 on success, -errno on error + * + * @see netmidi10: 6.5 Invitation Reply: Accepted + */ +static int netmidi2_send_invitation_reply(struct netmidi2_session *session, + uint8_t authentication_state) +{ + int ret; + uint8_t command_code; + + const char *name = netmidi2_ep_get_name(session->ep); + const char *piid = netmidi2_ep_get_piid(session->ep); + const size_t namelen = strlen(name); + const size_t piidlen = strlen(piid); + const size_t namelen_words = DIV_ROUND_UP(namelen, 4); + const size_t piidlen_words = DIV_ROUND_UP(piidlen, 4); + const uint16_t specific_data = (namelen_words << 8) | authentication_state; + + size_t total_words = namelen_words + piidlen_words; + + if (session->state == NETMIDI2_SESSION_ESTABLISHED) { + command_code = COMMAND_INVITATION_REPLY_ACCEPTED; + } + +#if defined(CONFIG_NETMIDI2_HOST_AUTH) + else if (session->state == NETMIDI2_SESSION_AUTH_REQUIRED) { + total_words += DIV_ROUND_UP(NETMIDI2_NONCE_SIZE, 4); + + if (session->ep->auth_type == NETMIDI2_AUTH_SHARED_SECRET) { + command_code = COMMAND_INVITATION_REPLY_AUTH_REQUIRED; + } else if (session->ep->auth_type == NETMIDI2_AUTH_USER_PASSWORD) { + command_code = COMMAND_INVITATION_REPLY_USER_AUTH_REQUIRED; + } else { + return -EINVAL; + } + } +#endif /* CONFIG_NETMIDI2_HOST_AUTH */ + + else { + return -EINVAL; + } + + ret = sess_buf_add_header(session, command_code, specific_data, total_words); + if (ret != 0) { + return ret; + } + +#if defined(CONFIG_NETMIDI2_HOST_AUTH) + if (session->state == NETMIDI2_SESSION_AUTH_REQUIRED) { + sys_rand_get(session->nonce, NETMIDI2_NONCE_SIZE); + sess_buf_add_mem_padded(session, session->nonce, NETMIDI2_NONCE_SIZE); + } +#endif /* CONFIG_NETMIDI2_HOST_AUTH */ + + sess_buf_add_mem_padded(session, name, namelen); + sess_buf_add_mem_padded(session, piid, piidlen); + k_work_submit(&session->tx_work); + return 0; +} + +/** + * @brief Consume the leading Command Packet from a buffer that contains + * a Network MIDI 2.0 UDP packet + * @param ep The endpoint that receives the packet + * @param peer_addr The network address of the sender + * @param[in] peer_addr_len The length of the sender's network address + * @param rx The received buffer + * @return non-zero if an error occurred, and the rx buffer is left in an + * unspecified state. 0 on success, in which case the head of the + * rx buffer is at the next Command Packet (or empty) + */ +static int netmidi2_dispatch_cmdpkt(struct netmidi2_ep *ep, + struct sockaddr *peer_addr, + socklen_t peer_addr_len, + struct net_buf *rx) +{ + struct midi_ump ump; + size_t payload_len; + uint32_t cmd_header; + uint8_t cmd_code, payload_len_words; + uint16_t cmd_data; + struct netmidi2_session *session; + + if (rx->len < 4) { + LOG_ERR("Incomplete UDP MIDI command packet header"); + return -1; + } + + cmd_header = net_buf_pull_be32(rx); + cmd_code = cmd_header >> 24; + payload_len_words = (cmd_header >> 16) & 0xff; + payload_len = 4 * payload_len_words; + cmd_data = cmd_header & 0xffff; + + if (payload_len > rx->len) { + netmidi2_quick_nak(ep, peer_addr, peer_addr_len, + NAK_COMMAND_MALFORMED, cmd_header); + LOG_ERR("Incomplete UDP MIDI command packet payload"); + return -1; + } + + switch (cmd_code) { + case COMMAND_PING: + /* See netmidi10: 6.13 Ping */ + if (payload_len_words != 1) { + netmidi2_quick_nak(ep, peer_addr, peer_addr_len, + NAK_COMMAND_MALFORMED, cmd_header); + LOG_ERR("Invalid payload length for PING packet"); + return -1; + } + /* Simple reply with 1 word from the PING request */ + netmidi2_quick_reply(ep, peer_addr, peer_addr_len, + COMMAND_PING_REPLY, 0, + (uint32_t [1]) {net_buf_pull_be32(rx)}, 1); + return 0; + + case COMMAND_INVITATION: + /* See netmidi10: 6.13 Ping + * We currently don't care about the peer's name or product + * instance id. Simply pull the entire payload at once. + */ + net_buf_pull(rx, payload_len); + + session = netmidi2_alloc_session(ep, peer_addr, peer_addr_len); + if (session == NULL) { + return -1; + } + + if (ep->auth_type == NETMIDI2_AUTH_NONE) { + session->state = NETMIDI2_SESSION_ESTABLISHED; + netmidi2_send_invitation_reply(session, AUTH_STATE_FIRST_REQUEST); + } + +#if !CONFIG_NETMIDI2_HOST_AUTH + return 0; +#else + /* See netmidi10: 6.7 Invitation Reply: Authentication Required */ + else { + session->state = NETMIDI2_SESSION_AUTH_REQUIRED; + netmidi2_send_invitation_reply(session, AUTH_STATE_FIRST_REQUEST); + } + + return 0; + + case COMMAND_INVITATION_WITH_AUTH: + case COMMAND_INVITATION_WITH_USER_AUTH: + /* See netmidi10: 6.9 - 6.10 Invitation with (User) Authentication */ + session = netmidi2_match_session(ep, peer_addr, peer_addr_len); + if (!SESSION_HAS_STATE(session, NETMIDI2_SESSION_AUTH_REQUIRED)) { + netmidi2_quick_nak(ep, peer_addr, peer_addr_len, + NAK_COMMAND_NOT_EXPECTED, cmd_header); + LOG_WRN("No session to authenticate found"); + return -1; + } + + if (!netmidi2_auth_session(session, rx, payload_len)) { + SESS_LOG_WRN(session, "Invalid auth digest"); + netmidi2_send_invitation_reply(session, AUTH_STATE_INCORRECT_DIGEST); + return -1; + } + + session->state = NETMIDI2_SESSION_ESTABLISHED; + netmidi2_send_invitation_reply(session, AUTH_STATE_FIRST_REQUEST); + return 0; +#endif /* CONFIG_NETMIDI2_HOST_AUTH */ + + case COMMAND_BYE: + session = netmidi2_match_session(ep, peer_addr, peer_addr_len); + if (session == NULL) { + netmidi2_quick_nak(ep, peer_addr, peer_addr_len, + NAK_COMMAND_NOT_EXPECTED, cmd_header); + LOG_WRN("Receiving BYE without session"); + return -1; + } + net_buf_pull(rx, payload_len); + netmidi2_quick_reply(ep, peer_addr, peer_addr_len, + COMMAND_BYE_REPLY, 0, NULL, 0); + netmidi2_free_session(session); + return 0; + + case COMMAND_UMP_DATA: + session = netmidi2_match_session(ep, peer_addr, peer_addr_len); + if (!SESSION_HAS_STATE(session, NETMIDI2_SESSION_ESTABLISHED)) { + netmidi2_quick_nak(ep, peer_addr, peer_addr_len, + NAK_COMMAND_NOT_EXPECTED, cmd_header); + LOG_WRN("Receiving UMP data without established session"); + return -1; + } + + if (session->rx_ump_seq == cmd_data) { + session->rx_ump_seq++; + } else { + SESS_LOG_WRN(session, "UMP Rx sequence mismatch (got %d, expected %d)", + cmd_data, session->rx_ump_seq); + session->rx_ump_seq = 1 + cmd_data; + } + + if (payload_len_words < 1 || payload_len_words > 4) { + netmidi2_quick_nak(ep, peer_addr, peer_addr_len, + NAK_COMMAND_MALFORMED, cmd_header); + SESS_LOG_ERR(session, "Invalid UMP length"); + return -1; + } + + for (size_t i = 0; i < payload_len_words; i++) { + ump.data[i] = net_buf_pull_be32(rx); + } + + if (UMP_NUM_WORDS(ump) != payload_len_words) { + netmidi2_quick_nak(ep, peer_addr, peer_addr_len, + NAK_COMMAND_MALFORMED, cmd_header); + SESS_LOG_ERR(session, "Invalid UMP payload size for its message type"); + return -1; + } + + if (ep->rx_packet_cb != NULL) { + ep->rx_packet_cb(session, ump); + } + return 0; + + case COMMAND_SESSION_RESET: + session = netmidi2_match_session(ep, peer_addr, peer_addr_len); + if (!SESSION_HAS_STATE(session, NETMIDI2_SESSION_ESTABLISHED)) { + LOG_WRN("Receiving session reset without established session"); + netmidi2_quick_nak(ep, peer_addr, peer_addr_len, + NAK_COMMAND_NOT_EXPECTED, cmd_header); + return -1; + } + + session->tx_ump_seq = 0; + session->rx_ump_seq = 0; + SESS_LOG_INF(session, "Reset session"); + netmidi2_session_sendcmd(session, COMMAND_SESSION_RESET_REPLY, 0, NULL, 0); + return 0; + + default: + LOG_WRN("Unknown command code %02X", cmd_code); + net_buf_pull(rx, payload_len); + netmidi2_quick_nak(ep, peer_addr, peer_addr_len, + NAK_COMMAND_NOT_SUPPORTED, cmd_header); + return 0; + } + return 0; +} + +/** + * @brief Service handler: receive a Network MIDI 2.0 UDP packet, + * and dispatch all the command packets that it contains + * @param pev the service event that triggers this handler + */ +static void netmidi2_service_handler(struct net_socket_service_event *pev) +{ + int ret; + struct netmidi2_ep *ep = pev->user_data; + struct pollfd *pfd = &pev->event; + struct sockaddr peer_addr; + socklen_t peer_addr_len = sizeof(peer_addr); + struct net_buf *rxbuf; + + rxbuf = net_buf_alloc(&netmidi2_pool, K_FOREVER); + if (rxbuf == NULL) { + NET_ERR("Cannot allocate Rx buf"); + return; + } + + ret = zsock_recvfrom(pfd->fd, rxbuf->data, rxbuf->size, 0, + &peer_addr, &peer_addr_len); + if (ret < 0) { + LOG_ERR("Rx error: %d (%d)", ret, errno); + goto end; + } + rxbuf->len = ret; + + NET_HEXDUMP_DBG(rxbuf->data, rxbuf->len, "Received UDP packet"); + + /* Check for magic header */ + if (rxbuf->len < 4 || memcmp(rxbuf->data, "MIDI", 4) != 0) { + LOG_WRN("Not a MIDI packet"); + goto end; + } + + net_buf_pull(rxbuf, 4); + + /* Parse contained command packets */ + ret = 0; + while (ret == 0 && rxbuf->len >= 4) { + ret = netmidi2_dispatch_cmdpkt(ep, &peer_addr, peer_addr_len, rxbuf); + } + +end: + net_buf_unref(rxbuf); +} + +NET_SOCKET_SERVICE_SYNC_DEFINE_STATIC(netmidi2_service, netmidi2_service_handler, 1); + +int netmidi2_host_ep_start(struct netmidi2_ep *ep) +{ + socklen_t addr_len = 0; + int ret, sock, af; + +#if defined(CONFIG_NET_IPV6) + af = AF_INET6; + addr_len = sizeof(struct sockaddr_in6); +#else + af = AF_INET; + addr_len = sizeof(struct sockaddr_in); +#endif + + ep->addr.sa_family = af; + sock = zsock_socket(af, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + LOG_ERR("Unable to create socket: %d", errno); + return -ENOMEM; + } + +#if defined(CONFIG_NET_IPV6) && defined(CONFIG_NET_IPV4) + socklen_t optlen = sizeof(int); + int opt = 0; + + /* Enable sharing of IPv4 and IPv6 on same socket */ + ret = zsock_setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, optlen); + if (ret < 0) { + LOG_WRN("Cannot turn off IPV6_V6ONLY option"); + } +#endif + + ret = zsock_bind(sock, &ep->addr, addr_len); + if (ret < 0) { + zsock_close(sock); + LOG_ERR("Failed to bind UDP socket: %d", errno); + return -EIO; + } + + for (size_t i = 0; i < CONFIG_NETMIDI2_HOST_MAX_CLIENTS; i++) { + k_work_init(&ep->peers[i].tx_work, netmidi2_session_tx_work); + } + + ep->pollsock.fd = sock; + ep->pollsock.events = POLLIN; + ret = net_socket_service_register(&netmidi2_service, &ep->pollsock, 1, ep); + if (ret < 0) { + zsock_close(sock); + LOG_ERR("Failed to register service: %d", ret); + return -EIO; + } + + LOG_INF("Started UDP-MIDI2 server (%d)", ntohs(ep->addr4.sin_port)); + return 0; +} + +void netmidi2_broadcast(struct netmidi2_ep *ep, const struct midi_ump ump) +{ + for (size_t i = 0; i < CONFIG_NETMIDI2_HOST_MAX_CLIENTS; i++) { + if (ep->peers[i].state == NETMIDI2_SESSION_ESTABLISHED) { + netmidi2_send(&ep->peers[i], ump); + } + } +} + +void netmidi2_send(struct netmidi2_session *sess, const struct midi_ump ump) +{ + netmidi2_session_sendcmd(sess, COMMAND_UMP_DATA, sess->tx_ump_seq++, + ump.data, UMP_NUM_WORDS(ump)); +} diff --git a/subsys/usb/device_next/class/usbd_midi2.c b/subsys/usb/device_next/class/usbd_midi2.c index 1afab63b4d92f..78b8fa4ccc41c 100644 --- a/subsys/usb/device_next/class/usbd_midi2.c +++ b/subsys/usb/device_next/class/usbd_midi2.c @@ -555,19 +555,19 @@ void usbd_midi_set_ops(const struct device *dev, const struct usbd_midi_ops *ops #define USBD_MIDI_VALIDATE_INSTANCE(n) \ DT_INST_FOREACH_CHILD(n, USBD_MIDI_VALIDATE_GRPTRM_BLOCK) -#define USBD_MIDI2_INIT_GRPTRM_BLOCK_DESCRIPTOR(node) \ - { \ - .bLength = sizeof(struct usb_midi_grptrm_block_descriptor), \ - .bDescriptorType = CS_GR_TRM_BLOCK, \ - .bDescriptorSubtype = GR_TRM_BLOCK, \ - .bGrpTrmBlkID = GRPTRM_BLOCK_ID(node), \ - .bGrpTrmBlkType = GRPTRM_BLOCK_TYPE(node), \ - .nGroupTrm = DT_REG_ADDR(node), \ - .nNumGroupTrm = DT_REG_SIZE(node), \ - .iBlockItem = 0, \ - .bMIDIProtocol = GRPTRM_PROTOCOL(node), \ - .wMaxInputBandwidth = 0x0000, \ - .wMaxOutputBandwidth = 0x0000, \ +#define USBD_MIDI2_INIT_GRPTRM_BLOCK_DESCRIPTOR(node) \ + { \ + .bLength = sizeof(struct usb_midi_grptrm_block_descriptor), \ + .bDescriptorType = CS_GR_TRM_BLOCK, \ + .bDescriptorSubtype = GR_TRM_BLOCK, \ + .bGrpTrmBlkID = GRPTRM_BLOCK_ID(node), \ + .bGrpTrmBlkType = GRPTRM_BLOCK_TYPE(node), \ + .nGroupTrm = DT_REG_ADDR(node), \ + .nNumGroupTrm = DT_REG_SIZE(node), \ + .iBlockItem = 0, \ + .bMIDIProtocol = GRPTRM_PROTOCOL(node), \ + .wMaxInputBandwidth = sys_cpu_to_le16(DT_PROP(node, serial_31250bps)), \ + .wMaxOutputBandwidth = sys_cpu_to_le16(DT_PROP(node, serial_31250bps)), \ } #define USBD_MIDI2_GRPTRM_TOTAL_LEN(n) \