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) \