mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2026-04-21 06:46:38 -04:00
Merge branch 'avb-virtual-tool' into 'master'
module-avb: fix heap corruption in server_destroy_descriptors See merge request pipewire/pipewire!2779
This commit is contained in:
commit
696e7c5d99
18 changed files with 5286 additions and 108 deletions
|
|
@ -174,13 +174,14 @@ static int handle_connect_tx_command(struct acmp *acmp, uint64_t now, const void
|
|||
return 0;
|
||||
|
||||
memcpy(buf, m, len);
|
||||
AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE);
|
||||
|
||||
stream = find_stream(server, SPA_DIRECTION_OUTPUT, ntohs(reply->talker_unique_id));
|
||||
if (stream == NULL) {
|
||||
status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX;
|
||||
goto done;
|
||||
}
|
||||
|
||||
AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE);
|
||||
reply->stream_id = htobe64(stream->id);
|
||||
|
||||
stream_activate(stream, ntohs(reply->talker_unique_id), now);
|
||||
|
|
@ -251,14 +252,14 @@ static int handle_disconnect_tx_command(struct acmp *acmp, uint64_t now, const v
|
|||
return 0;
|
||||
|
||||
memcpy(buf, m, len);
|
||||
AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE);
|
||||
|
||||
stream = find_stream(server, SPA_DIRECTION_OUTPUT, ntohs(reply->talker_unique_id));
|
||||
if (stream == NULL) {
|
||||
status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX;
|
||||
goto done;
|
||||
}
|
||||
|
||||
AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE);
|
||||
|
||||
stream_deactivate(stream, now);
|
||||
|
||||
done:
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ int handle_cmd_lock_entity_milan_v12(struct aecp *aecp, int64_t now, const void
|
|||
|
||||
desc = server_find_descriptor(server, desc_type, desc_id);
|
||||
if (desc == NULL)
|
||||
return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, p, len);
|
||||
return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, m, len);
|
||||
|
||||
entity_state = desc->ptr;
|
||||
lock = &entity_state->state.lock_state;
|
||||
|
|
@ -148,7 +148,7 @@ int handle_cmd_lock_entity_milan_v12(struct aecp *aecp, int64_t now, const void
|
|||
// If the lock is taken again by device
|
||||
if (ctrler_id == lock->locked_id) {
|
||||
lock->base_info.expire_timeout +=
|
||||
AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND;
|
||||
AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND * SPA_NSEC_PER_SEC;
|
||||
|
||||
lock->is_locked = true;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ static int handle_acquire_entity_avb_legacy(struct aecp *aecp, int64_t now,
|
|||
const void *m, int len)
|
||||
{
|
||||
struct server *server = aecp->server;
|
||||
const struct avb_packet_aecp_aem *p = m;
|
||||
const struct avb_ethernet_header *h = m;
|
||||
const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
|
||||
const struct avb_packet_aecp_aem_acquire *ae;
|
||||
const struct descriptor *desc;
|
||||
uint16_t desc_type, desc_id;
|
||||
|
|
@ -53,7 +54,8 @@ static int handle_lock_entity_avb_legacy(struct aecp *aecp, int64_t now,
|
|||
const void *m, int len)
|
||||
{
|
||||
struct server *server = aecp->server;
|
||||
const struct avb_packet_aecp_aem *p = m;
|
||||
const struct avb_ethernet_header *h = m;
|
||||
const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
|
||||
const struct avb_packet_aecp_aem_acquire *ae;
|
||||
const struct descriptor *desc;
|
||||
uint16_t desc_type, desc_id;
|
||||
|
|
|
|||
184
src/modules/module-avb/avb-transport-loopback.h
Normal file
184
src/modules/module-avb/avb-transport-loopback.h
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/* AVB support */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
#ifndef AVB_TRANSPORT_LOOPBACK_H
|
||||
#define AVB_TRANSPORT_LOOPBACK_H
|
||||
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/eventfd.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/if_packet.h>
|
||||
|
||||
#include "internal.h"
|
||||
#include "packets.h"
|
||||
|
||||
#define AVB_LOOPBACK_MAX_PACKETS 64
|
||||
#define AVB_LOOPBACK_MAX_PACKET_SIZE 2048
|
||||
|
||||
struct avb_loopback_packet {
|
||||
uint8_t dest[6];
|
||||
uint16_t type;
|
||||
size_t size;
|
||||
uint8_t data[AVB_LOOPBACK_MAX_PACKET_SIZE];
|
||||
};
|
||||
|
||||
struct avb_loopback_transport {
|
||||
struct avb_loopback_packet packets[AVB_LOOPBACK_MAX_PACKETS];
|
||||
int packet_count;
|
||||
int packet_read;
|
||||
};
|
||||
|
||||
static inline int avb_loopback_setup(struct server *server)
|
||||
{
|
||||
struct avb_loopback_transport *t;
|
||||
static const uint8_t test_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x01 };
|
||||
|
||||
t = calloc(1, sizeof(*t));
|
||||
if (t == NULL)
|
||||
return -errno;
|
||||
|
||||
server->transport_data = t;
|
||||
|
||||
memcpy(server->mac_addr, test_mac, 6);
|
||||
server->ifindex = 1;
|
||||
server->entity_id = (uint64_t)server->mac_addr[0] << 56 |
|
||||
(uint64_t)server->mac_addr[1] << 48 |
|
||||
(uint64_t)server->mac_addr[2] << 40 |
|
||||
(uint64_t)0xff << 32 |
|
||||
(uint64_t)0xfe << 24 |
|
||||
(uint64_t)server->mac_addr[3] << 16 |
|
||||
(uint64_t)server->mac_addr[4] << 8 |
|
||||
(uint64_t)server->mac_addr[5];
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int avb_loopback_send_packet(struct server *server,
|
||||
const uint8_t dest[6], uint16_t type, void *data, size_t size)
|
||||
{
|
||||
struct avb_loopback_transport *t = server->transport_data;
|
||||
struct avb_loopback_packet *pkt;
|
||||
struct avb_ethernet_header *hdr = (struct avb_ethernet_header*)data;
|
||||
|
||||
if (t->packet_count >= AVB_LOOPBACK_MAX_PACKETS)
|
||||
return -ENOSPC;
|
||||
if (size > AVB_LOOPBACK_MAX_PACKET_SIZE)
|
||||
return -EMSGSIZE;
|
||||
|
||||
/* Fill in the ethernet header like the raw transport does */
|
||||
memcpy(hdr->dest, dest, 6);
|
||||
memcpy(hdr->src, server->mac_addr, 6);
|
||||
hdr->type = htons(type);
|
||||
|
||||
pkt = &t->packets[t->packet_count % AVB_LOOPBACK_MAX_PACKETS];
|
||||
memcpy(pkt->dest, dest, 6);
|
||||
pkt->type = type;
|
||||
pkt->size = size;
|
||||
memcpy(pkt->data, data, size);
|
||||
t->packet_count++;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a dummy fd for protocol handlers that create their own sockets.
|
||||
* Uses eventfd so pw_loop_add_io() has a valid fd to work with.
|
||||
*/
|
||||
static inline int avb_loopback_make_socket(struct server *server,
|
||||
uint16_t type, const uint8_t mac[6])
|
||||
{
|
||||
int fd;
|
||||
|
||||
fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return -errno;
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
static inline void avb_loopback_destroy(struct server *server)
|
||||
{
|
||||
free(server->transport_data);
|
||||
server->transport_data = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dummy stream socket using eventfd.
|
||||
* No AF_PACKET, no ioctls, no privileges needed.
|
||||
*/
|
||||
static inline int avb_loopback_stream_setup_socket(struct server *server,
|
||||
struct stream *stream)
|
||||
{
|
||||
int fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return -errno;
|
||||
|
||||
spa_zero(stream->sock_addr);
|
||||
stream->sock_addr.sll_family = AF_PACKET;
|
||||
stream->sock_addr.sll_halen = ETH_ALEN;
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op stream send — pretend the send succeeded.
|
||||
* Audio data is consumed from the ringbuffer but goes nowhere.
|
||||
*/
|
||||
static inline ssize_t avb_loopback_stream_send(struct server *server,
|
||||
struct stream *stream, struct msghdr *msg, int flags)
|
||||
{
|
||||
ssize_t total = 0;
|
||||
for (size_t i = 0; i < msg->msg_iovlen; i++)
|
||||
total += msg->msg_iov[i].iov_len;
|
||||
return total;
|
||||
}
|
||||
|
||||
static const struct avb_transport_ops avb_transport_loopback = {
|
||||
.setup = avb_loopback_setup,
|
||||
.send_packet = avb_loopback_send_packet,
|
||||
.make_socket = avb_loopback_make_socket,
|
||||
.destroy = avb_loopback_destroy,
|
||||
.stream_setup_socket = avb_loopback_stream_setup_socket,
|
||||
.stream_send = avb_loopback_stream_send,
|
||||
};
|
||||
|
||||
/** Get the number of captured sent packets */
|
||||
static inline int avb_loopback_get_packet_count(struct server *server)
|
||||
{
|
||||
struct avb_loopback_transport *t = server->transport_data;
|
||||
return t->packet_count - t->packet_read;
|
||||
}
|
||||
|
||||
/** Read the next captured sent packet, returns packet size or -1 */
|
||||
static inline int avb_loopback_get_packet(struct server *server,
|
||||
void *buf, size_t bufsize)
|
||||
{
|
||||
struct avb_loopback_transport *t = server->transport_data;
|
||||
struct avb_loopback_packet *pkt;
|
||||
|
||||
if (t->packet_read >= t->packet_count)
|
||||
return -1;
|
||||
|
||||
pkt = &t->packets[t->packet_read % AVB_LOOPBACK_MAX_PACKETS];
|
||||
t->packet_read++;
|
||||
|
||||
if (pkt->size > bufsize)
|
||||
return -1;
|
||||
|
||||
memcpy(buf, pkt->data, pkt->size);
|
||||
return pkt->size;
|
||||
}
|
||||
|
||||
/** Clear all captured packets */
|
||||
static inline void avb_loopback_clear_packets(struct server *server)
|
||||
{
|
||||
struct avb_loopback_transport *t = server->transport_data;
|
||||
t->packet_count = 0;
|
||||
t->packet_read = 0;
|
||||
}
|
||||
|
||||
#endif /* AVB_TRANSPORT_LOOPBACK_H */
|
||||
|
|
@ -84,7 +84,7 @@ static void on_socket_data(void *data, int fd, uint32_t mask)
|
|||
}
|
||||
}
|
||||
|
||||
int avb_server_send_packet(struct server *server, const uint8_t dest[6],
|
||||
static int raw_send_packet(struct server *server, const uint8_t dest[6],
|
||||
uint16_t type, void *data, size_t size)
|
||||
{
|
||||
struct avb_ethernet_header *hdr = (struct avb_ethernet_header*)data;
|
||||
|
|
@ -101,6 +101,12 @@ int avb_server_send_packet(struct server *server, const uint8_t dest[6],
|
|||
return res;
|
||||
}
|
||||
|
||||
int avb_server_send_packet(struct server *server, const uint8_t dest[6],
|
||||
uint16_t type, void *data, size_t size)
|
||||
{
|
||||
return server->transport->send_packet(server, dest, type, data, size);
|
||||
}
|
||||
|
||||
static int load_filter(int fd, uint16_t eth, const uint8_t dest[6], const uint8_t mac[6])
|
||||
{
|
||||
struct sock_fprog filter;
|
||||
|
|
@ -136,7 +142,7 @@ static int load_filter(int fd, uint16_t eth, const uint8_t dest[6], const uint8_
|
|||
return 0;
|
||||
}
|
||||
|
||||
int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6])
|
||||
static int raw_make_socket(struct server *server, uint16_t type, const uint8_t mac[6])
|
||||
{
|
||||
int fd, res;
|
||||
struct ifreq req;
|
||||
|
|
@ -209,13 +215,20 @@ error_close:
|
|||
return res;
|
||||
}
|
||||
|
||||
static int setup_socket(struct server *server)
|
||||
int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6])
|
||||
{
|
||||
if (server->transport && server->transport->make_socket)
|
||||
return server->transport->make_socket(server, type, mac);
|
||||
return raw_make_socket(server, type, mac);
|
||||
}
|
||||
|
||||
static int raw_transport_setup(struct server *server)
|
||||
{
|
||||
struct impl *impl = server->impl;
|
||||
int fd, res;
|
||||
static const uint8_t bmac[6] = AVB_BROADCAST_MAC;
|
||||
|
||||
fd = avb_server_make_socket(server, AVB_TSN_ETH, bmac);
|
||||
fd = raw_make_socket(server, AVB_TSN_ETH, bmac);
|
||||
if (fd < 0)
|
||||
return fd;
|
||||
|
||||
|
|
@ -244,6 +257,119 @@ error_no_source:
|
|||
return res;
|
||||
}
|
||||
|
||||
static int raw_stream_setup_socket(struct server *server, struct stream *stream)
|
||||
{
|
||||
int fd, res;
|
||||
char buf[128];
|
||||
struct ifreq req;
|
||||
|
||||
fd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(ETH_P_ALL));
|
||||
if (fd < 0) {
|
||||
pw_log_error("socket() failed: %m");
|
||||
return -errno;
|
||||
}
|
||||
|
||||
spa_zero(req);
|
||||
snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
|
||||
res = ioctl(fd, SIOCGIFINDEX, &req);
|
||||
if (res < 0) {
|
||||
pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname);
|
||||
res = -errno;
|
||||
goto error_close;
|
||||
}
|
||||
|
||||
spa_zero(stream->sock_addr);
|
||||
stream->sock_addr.sll_family = AF_PACKET;
|
||||
stream->sock_addr.sll_protocol = htons(ETH_P_TSN);
|
||||
stream->sock_addr.sll_ifindex = req.ifr_ifindex;
|
||||
|
||||
if (stream->direction == SPA_DIRECTION_OUTPUT) {
|
||||
struct sock_txtime txtime_cfg;
|
||||
|
||||
res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &stream->prio,
|
||||
sizeof(stream->prio));
|
||||
if (res < 0) {
|
||||
pw_log_error("setsockopt(SO_PRIORITY %d) failed: %m", stream->prio);
|
||||
res = -errno;
|
||||
goto error_close;
|
||||
}
|
||||
|
||||
txtime_cfg.clockid = CLOCK_TAI;
|
||||
txtime_cfg.flags = 0;
|
||||
res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg,
|
||||
sizeof(txtime_cfg));
|
||||
if (res < 0) {
|
||||
pw_log_error("setsockopt(SO_TXTIME) failed: %m");
|
||||
res = -errno;
|
||||
goto error_close;
|
||||
}
|
||||
} else {
|
||||
struct packet_mreq mreq;
|
||||
|
||||
res = bind(fd, (struct sockaddr *) &stream->sock_addr, sizeof(stream->sock_addr));
|
||||
if (res < 0) {
|
||||
pw_log_error("bind() failed: %m");
|
||||
res = -errno;
|
||||
goto error_close;
|
||||
}
|
||||
|
||||
spa_zero(mreq);
|
||||
mreq.mr_ifindex = req.ifr_ifindex;
|
||||
mreq.mr_type = PACKET_MR_MULTICAST;
|
||||
mreq.mr_alen = ETH_ALEN;
|
||||
memcpy(&mreq.mr_address, stream->addr, ETH_ALEN);
|
||||
res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
|
||||
&mreq, sizeof(struct packet_mreq));
|
||||
|
||||
pw_log_info("join %s", avb_utils_format_addr(buf, 128, stream->addr));
|
||||
|
||||
if (res < 0) {
|
||||
pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m");
|
||||
res = -errno;
|
||||
goto error_close;
|
||||
}
|
||||
}
|
||||
return fd;
|
||||
|
||||
error_close:
|
||||
close(fd);
|
||||
return res;
|
||||
}
|
||||
|
||||
static ssize_t raw_stream_send(struct server *server, struct stream *stream,
|
||||
struct msghdr *msg, int flags)
|
||||
{
|
||||
return sendmsg(stream->source->fd, msg, flags);
|
||||
}
|
||||
|
||||
int avb_server_stream_setup_socket(struct server *server, struct stream *stream)
|
||||
{
|
||||
return server->transport->stream_setup_socket(server, stream);
|
||||
}
|
||||
|
||||
ssize_t avb_server_stream_send(struct server *server, struct stream *stream,
|
||||
struct msghdr *msg, int flags)
|
||||
{
|
||||
return server->transport->stream_send(server, stream, msg, flags);
|
||||
}
|
||||
|
||||
static void raw_transport_destroy(struct server *server)
|
||||
{
|
||||
struct impl *impl = server->impl;
|
||||
if (server->source)
|
||||
pw_loop_destroy_source(impl->loop, server->source);
|
||||
server->source = NULL;
|
||||
}
|
||||
|
||||
const struct avb_transport_ops avb_transport_raw = {
|
||||
.setup = raw_transport_setup,
|
||||
.send_packet = raw_send_packet,
|
||||
.make_socket = raw_make_socket,
|
||||
.destroy = raw_transport_destroy,
|
||||
.stream_setup_socket = raw_stream_setup_socket,
|
||||
.stream_send = raw_stream_send,
|
||||
};
|
||||
|
||||
struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props)
|
||||
{
|
||||
struct server *server;
|
||||
|
|
@ -266,10 +392,14 @@ struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props)
|
|||
|
||||
spa_hook_list_init(&server->listener_list);
|
||||
spa_list_init(&server->descriptors);
|
||||
spa_list_init(&server->streams);
|
||||
|
||||
server->debug_messages = false;
|
||||
|
||||
if ((res = setup_socket(server)) < 0)
|
||||
if (server->transport == NULL)
|
||||
server->transport = &avb_transport_raw;
|
||||
|
||||
if ((res = server->transport->setup(server)) < 0)
|
||||
goto error_free;
|
||||
|
||||
|
||||
|
|
@ -315,12 +445,10 @@ void avdecc_server_add_listener(struct server *server, struct spa_hook *listener
|
|||
|
||||
void avdecc_server_free(struct server *server)
|
||||
{
|
||||
struct impl *impl = server->impl;
|
||||
|
||||
server_destroy_descriptors(server);
|
||||
spa_list_remove(&server->link);
|
||||
if (server->source)
|
||||
pw_loop_destroy_source(impl->loop, server->source);
|
||||
if (server->transport)
|
||||
server->transport->destroy(server);
|
||||
pw_timer_queue_cancel(&server->timer);
|
||||
spa_hook_list_clean(&server->listener_list);
|
||||
free(server->ifname);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
#ifndef AVB_INTERNAL_H
|
||||
#define AVB_INTERNAL_H
|
||||
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include <pipewire/pipewire.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
|
@ -17,6 +19,22 @@ struct avb_mrp;
|
|||
#define AVB_TSN_ETH 0x22f0
|
||||
#define AVB_BROADCAST_MAC { 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 };
|
||||
|
||||
struct stream;
|
||||
|
||||
struct avb_transport_ops {
|
||||
int (*setup)(struct server *server);
|
||||
int (*send_packet)(struct server *server, const uint8_t dest[6],
|
||||
uint16_t type, void *data, size_t size);
|
||||
int (*make_socket)(struct server *server, uint16_t type,
|
||||
const uint8_t mac[6]);
|
||||
void (*destroy)(struct server *server);
|
||||
|
||||
/* stream data plane ops */
|
||||
int (*stream_setup_socket)(struct server *server, struct stream *stream);
|
||||
ssize_t (*stream_send)(struct server *server, struct stream *stream,
|
||||
struct msghdr *msg, int flags);
|
||||
};
|
||||
|
||||
struct impl {
|
||||
struct pw_loop *loop;
|
||||
struct pw_timer_queue *timer_queue;
|
||||
|
|
@ -77,12 +95,16 @@ struct server {
|
|||
uint64_t entity_id;
|
||||
int ifindex;
|
||||
|
||||
const struct avb_transport_ops *transport;
|
||||
void *transport_data;
|
||||
|
||||
struct spa_source *source;
|
||||
struct pw_timer timer;
|
||||
|
||||
struct spa_hook_list listener_list;
|
||||
|
||||
struct spa_list descriptors;
|
||||
struct spa_list streams;
|
||||
|
||||
unsigned debug_messages:1;
|
||||
|
||||
|
|
@ -102,7 +124,6 @@ static inline void server_destroy_descriptors(struct server *server)
|
|||
struct descriptor *d, *t;
|
||||
|
||||
spa_list_for_each_safe(d, t, &server->descriptors, link) {
|
||||
free(d->ptr);
|
||||
spa_list_remove(&d->link);
|
||||
free(d);
|
||||
}
|
||||
|
|
@ -145,11 +166,17 @@ void avdecc_server_free(struct server *server);
|
|||
void avdecc_server_add_listener(struct server *server, struct spa_hook *listener,
|
||||
const struct server_events *events, void *data);
|
||||
|
||||
extern const struct avb_transport_ops avb_transport_raw;
|
||||
|
||||
int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]);
|
||||
|
||||
int avb_server_send_packet(struct server *server, const uint8_t dest[6],
|
||||
uint16_t type, void *data, size_t size);
|
||||
|
||||
int avb_server_stream_setup_socket(struct server *server, struct stream *stream);
|
||||
ssize_t avb_server_stream_send(struct server *server, struct stream *stream,
|
||||
struct msghdr *msg, int flags);
|
||||
|
||||
struct aecp {
|
||||
struct server *server;
|
||||
struct spa_hook server_listener;
|
||||
|
|
|
|||
|
|
@ -302,6 +302,8 @@ const char *avb_mrp_notify_name(uint8_t notify)
|
|||
const char *avb_mrp_send_name(uint8_t send)
|
||||
{
|
||||
switch(send) {
|
||||
case 0:
|
||||
return "none";
|
||||
case AVB_MRP_SEND_NEW:
|
||||
return "new";
|
||||
case AVB_MRP_SEND_JOININ:
|
||||
|
|
|
|||
|
|
@ -88,13 +88,13 @@ struct avb_packet_mrp_footer {
|
|||
#define AVB_MRP_ATTRIBUTE_EVENT_LV 5
|
||||
#define AVB_MRP_ATTRIBUTE_EVENT_LVA 6
|
||||
|
||||
#define AVB_MRP_SEND_NEW 0
|
||||
#define AVB_MRP_SEND_JOININ 1
|
||||
#define AVB_MRP_SEND_IN 2
|
||||
#define AVB_MRP_SEND_JOINMT 3
|
||||
#define AVB_MRP_SEND_MT 4
|
||||
#define AVB_MRP_SEND_LV 5
|
||||
#define AVB_MRP_SEND_LVA 6
|
||||
#define AVB_MRP_SEND_NEW 1
|
||||
#define AVB_MRP_SEND_JOININ 2
|
||||
#define AVB_MRP_SEND_IN 3
|
||||
#define AVB_MRP_SEND_JOINMT 4
|
||||
#define AVB_MRP_SEND_MT 5
|
||||
#define AVB_MRP_SEND_LV 6
|
||||
#define AVB_MRP_SEND_LVA 7
|
||||
|
||||
#define AVB_MRP_NOTIFY_NEW 1
|
||||
#define AVB_MRP_NOTIFY_JOIN 2
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ static int encode_talker(struct msrp *msrp, struct attr *a, void *m)
|
|||
*t = a->attr.attr.talker;
|
||||
|
||||
ev = SPA_PTROFF(t, sizeof(*t), uint8_t);
|
||||
*ev = a->attr.mrp->pending_send * 6 * 6;
|
||||
*ev = (a->attr.mrp->pending_send - 1) * 6 * 6;
|
||||
|
||||
f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
|
||||
f->end_mark = 0;
|
||||
|
|
@ -170,7 +170,7 @@ static int encode_listener(struct msrp *msrp, struct attr *a, void *m)
|
|||
*l = a->attr.attr.listener;
|
||||
|
||||
ev = SPA_PTROFF(l, sizeof(*l), uint8_t);
|
||||
*ev = a->attr.mrp->pending_send * 6 * 6;
|
||||
*ev = (a->attr.mrp->pending_send - 1) * 6 * 6;
|
||||
|
||||
ev = SPA_PTROFF(ev, sizeof(*ev), uint8_t);
|
||||
*ev = a->attr.param * 4 * 4 * 4;
|
||||
|
|
@ -226,7 +226,7 @@ static int encode_domain(struct msrp *msrp, struct attr *a, void *m)
|
|||
*d = a->attr.attr.domain;
|
||||
|
||||
ev = SPA_PTROFF(d, sizeof(*d), uint8_t);
|
||||
*ev = a->attr.mrp->pending_send * 36;
|
||||
*ev = (a->attr.mrp->pending_send - 1) * 36;
|
||||
|
||||
f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
|
||||
f->end_mark = 0;
|
||||
|
|
@ -332,7 +332,8 @@ static void msrp_notify(void *data, uint64_t now, uint8_t notify)
|
|||
{
|
||||
struct attr *a = data;
|
||||
struct msrp *msrp = a->msrp;
|
||||
return dispatch[a->attr.type].notify(msrp, now, a, notify);
|
||||
if (dispatch[a->attr.type].notify)
|
||||
dispatch[a->attr.type].notify(msrp, now, a, notify);
|
||||
}
|
||||
|
||||
static const struct avb_mrp_attribute_events mrp_attr_events = {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ static int encode_vid(struct mvrp *mvrp, struct attr *a, void *m)
|
|||
*d = a->attr.attr.vid;
|
||||
|
||||
ev = SPA_PTROFF(d, sizeof(*d), uint8_t);
|
||||
*ev = a->attr.mrp->pending_send * 36;
|
||||
*ev = (a->attr.mrp->pending_send - 1) * 36;
|
||||
|
||||
f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
|
||||
f->end_mark = 0;
|
||||
|
|
@ -171,7 +171,8 @@ static void mvrp_notify(void *data, uint64_t now, uint8_t notify)
|
|||
{
|
||||
struct attr *a = data;
|
||||
struct mvrp *mvrp = a->mvrp;
|
||||
return dispatch[a->attr.type].notify(mvrp, now, a, notify);
|
||||
if (dispatch[a->attr.type].notify)
|
||||
dispatch[a->attr.type].notify(mvrp, now, a, notify);
|
||||
}
|
||||
|
||||
static const struct avb_mrp_attribute_events mrp_attr_events = {
|
||||
|
|
|
|||
|
|
@ -116,9 +116,10 @@ static int flush_write(struct stream *stream, uint64_t current_time)
|
|||
p->timestamp = ptime;
|
||||
p->dbc = dbc;
|
||||
|
||||
n = sendmsg(stream->source->fd, &stream->msg, MSG_NOSIGNAL);
|
||||
n = avb_server_stream_send(stream->server, stream,
|
||||
&stream->msg, MSG_NOSIGNAL);
|
||||
if (n < 0 || n != (ssize_t)stream->pdu_size) {
|
||||
pw_log_error("sendmsg() failed %zd != %zd: %m",
|
||||
pw_log_error("stream send failed %zd != %zd: %m",
|
||||
n, stream->pdu_size);
|
||||
}
|
||||
txtime += stream->pdu_period;
|
||||
|
|
@ -331,6 +332,8 @@ struct stream *server_create_stream(struct server *server, struct stream *stream
|
|||
stream->talker_attr->attr.talker.rank = AVB_MSRP_RANK_DEFAULT;
|
||||
stream->talker_attr->attr.talker.accumulated_latency = htonl(95);
|
||||
|
||||
spa_list_append(&server->streams, &stream->link);
|
||||
|
||||
return stream;
|
||||
|
||||
error_free_stream:
|
||||
|
|
@ -348,82 +351,7 @@ void stream_destroy(struct stream *stream)
|
|||
|
||||
static int setup_socket(struct stream *stream)
|
||||
{
|
||||
struct server *server = stream->server;
|
||||
int fd, res;
|
||||
char buf[128];
|
||||
struct ifreq req;
|
||||
|
||||
fd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(ETH_P_ALL));
|
||||
if (fd < 0) {
|
||||
pw_log_error("socket() failed: %m");
|
||||
return -errno;
|
||||
}
|
||||
|
||||
spa_zero(req);
|
||||
snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
|
||||
res = ioctl(fd, SIOCGIFINDEX, &req);
|
||||
if (res < 0) {
|
||||
pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname);
|
||||
res = -errno;
|
||||
goto error_close;
|
||||
}
|
||||
|
||||
spa_zero(stream->sock_addr);
|
||||
stream->sock_addr.sll_family = AF_PACKET;
|
||||
stream->sock_addr.sll_protocol = htons(ETH_P_TSN);
|
||||
stream->sock_addr.sll_ifindex = req.ifr_ifindex;
|
||||
|
||||
if (stream->direction == SPA_DIRECTION_OUTPUT) {
|
||||
struct sock_txtime txtime_cfg;
|
||||
|
||||
res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &stream->prio,
|
||||
sizeof(stream->prio));
|
||||
if (res < 0) {
|
||||
pw_log_error("setsockopt(SO_PRIORITY %d) failed: %m", stream->prio);
|
||||
res = -errno;
|
||||
goto error_close;
|
||||
}
|
||||
|
||||
txtime_cfg.clockid = CLOCK_TAI;
|
||||
txtime_cfg.flags = 0;
|
||||
res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg,
|
||||
sizeof(txtime_cfg));
|
||||
if (res < 0) {
|
||||
pw_log_error("setsockopt(SO_TXTIME) failed: %m");
|
||||
res = -errno;
|
||||
goto error_close;
|
||||
}
|
||||
} else {
|
||||
struct packet_mreq mreq;
|
||||
|
||||
res = bind(fd, (struct sockaddr *) &stream->sock_addr, sizeof(stream->sock_addr));
|
||||
if (res < 0) {
|
||||
pw_log_error("bind() failed: %m");
|
||||
res = -errno;
|
||||
goto error_close;
|
||||
}
|
||||
|
||||
spa_zero(mreq);
|
||||
mreq.mr_ifindex = req.ifr_ifindex;
|
||||
mreq.mr_type = PACKET_MR_MULTICAST;
|
||||
mreq.mr_alen = ETH_ALEN;
|
||||
memcpy(&mreq.mr_address, stream->addr, ETH_ALEN);
|
||||
res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
|
||||
&mreq, sizeof(struct packet_mreq));
|
||||
|
||||
pw_log_info("join %s", avb_utils_format_addr(buf, 128, stream->addr));
|
||||
|
||||
if (res < 0) {
|
||||
pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m");
|
||||
res = -errno;
|
||||
goto error_close;
|
||||
}
|
||||
}
|
||||
return fd;
|
||||
|
||||
error_close:
|
||||
close(fd);
|
||||
return res;
|
||||
return avb_server_stream_setup_socket(stream->server, stream);
|
||||
}
|
||||
|
||||
static void handle_iec61883_packet(struct stream *stream,
|
||||
|
|
@ -548,3 +476,24 @@ int stream_deactivate(struct stream *stream, uint64_t now)
|
|||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int stream_activate_virtual(struct stream *stream, uint16_t index)
|
||||
{
|
||||
struct server *server = stream->server;
|
||||
int fd;
|
||||
|
||||
if (stream->source == NULL) {
|
||||
fd = setup_socket(stream);
|
||||
if (fd < 0)
|
||||
return fd;
|
||||
|
||||
stream->source = pw_loop_add_io(server->impl->loop, fd,
|
||||
SPA_IO_IN, true, on_socket_data, stream);
|
||||
if (stream->source == NULL) {
|
||||
close(fd);
|
||||
return -errno;
|
||||
}
|
||||
}
|
||||
pw_stream_set_active(stream->stream, true);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,5 +78,6 @@ void stream_destroy(struct stream *stream);
|
|||
|
||||
int stream_activate(struct stream *stream, uint16_t index, uint64_t now);
|
||||
int stream_deactivate(struct stream *stream, uint64_t now);
|
||||
int stream_activate_virtual(struct stream *stream, uint16_t index);
|
||||
|
||||
#endif /* AVB_STREAM_H */
|
||||
|
|
|
|||
|
|
@ -95,6 +95,50 @@ if build_pw_cat
|
|||
summary({'Build pw-cat with FFmpeg integration': build_pw_cat_with_ffmpeg}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump tool')
|
||||
endif
|
||||
|
||||
build_avb_virtual = get_option('avb').require(
|
||||
host_machine.system() == 'linux',
|
||||
error_message: 'AVB support is only available on Linux'
|
||||
).allowed()
|
||||
|
||||
if build_avb_virtual
|
||||
avb_tool_inc = include_directories('../modules')
|
||||
avb_tool_sources = [
|
||||
'pw-avb-virtual.c',
|
||||
'../modules/module-avb/avb.c',
|
||||
'../modules/module-avb/adp.c',
|
||||
'../modules/module-avb/acmp.c',
|
||||
'../modules/module-avb/aecp.c',
|
||||
'../modules/module-avb/aecp-aem.c',
|
||||
'../modules/module-avb/aecp-aem-cmds-resps/cmd-available.c',
|
||||
'../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-control.c',
|
||||
'../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-name.c',
|
||||
'../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-clock-source.c',
|
||||
'../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-sampling-rate.c',
|
||||
'../modules/module-avb/aecp-aem-cmds-resps/cmd-deregister-unsolicited-notifications.c',
|
||||
'../modules/module-avb/aecp-aem-cmds-resps/cmd-register-unsolicited-notifications.c',
|
||||
'../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-format.c',
|
||||
'../modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c',
|
||||
'../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-configuration.c',
|
||||
'../modules/module-avb/aecp-aem-cmds-resps/reply-unsol-helpers.c',
|
||||
'../modules/module-avb/es-builder.c',
|
||||
'../modules/module-avb/avdecc.c',
|
||||
'../modules/module-avb/descriptors.c',
|
||||
'../modules/module-avb/maap.c',
|
||||
'../modules/module-avb/mmrp.c',
|
||||
'../modules/module-avb/mrp.c',
|
||||
'../modules/module-avb/msrp.c',
|
||||
'../modules/module-avb/mvrp.c',
|
||||
'../modules/module-avb/srp.c',
|
||||
'../modules/module-avb/stream.c',
|
||||
]
|
||||
executable('pw-avb-virtual',
|
||||
avb_tool_sources,
|
||||
install: true,
|
||||
include_directories: [configinc, avb_tool_inc],
|
||||
dependencies: [mathlib, dl_lib, rt_lib, pipewire_dep],
|
||||
)
|
||||
endif
|
||||
|
||||
if dbus_dep.found()
|
||||
executable('pw-reserve',
|
||||
'reserve.h',
|
||||
|
|
|
|||
283
src/tools/pw-avb-virtual.c
Normal file
283
src/tools/pw-avb-virtual.c
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
/* PipeWire */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
/**
|
||||
* pw-avb-virtual: Create virtual AVB audio devices in the PipeWire graph.
|
||||
*
|
||||
* This tool creates virtual AVB talker/listener endpoints that appear
|
||||
* as Audio/Source and Audio/Sink nodes in the PipeWire graph (visible
|
||||
* in tools like Helvum). No AVB hardware or network access is needed —
|
||||
* the loopback transport is used for all protocol and stream operations.
|
||||
*
|
||||
* The sink node consumes audio silently (data goes nowhere).
|
||||
* The source node produces silence (no network data to receive).
|
||||
*/
|
||||
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <signal.h>
|
||||
#include <getopt.h>
|
||||
#include <locale.h>
|
||||
|
||||
#include <spa/utils/result.h>
|
||||
|
||||
#include <pipewire/pipewire.h>
|
||||
|
||||
#include "module-avb/internal.h"
|
||||
#include "module-avb/stream.h"
|
||||
#include "module-avb/avb-transport-loopback.h"
|
||||
#include "module-avb/descriptors.h"
|
||||
#include "module-avb/mrp.h"
|
||||
#include "module-avb/adp.h"
|
||||
#include "module-avb/acmp.h"
|
||||
#include "module-avb/aecp.h"
|
||||
#include "module-avb/maap.h"
|
||||
#include "module-avb/mmrp.h"
|
||||
#include "module-avb/msrp.h"
|
||||
#include "module-avb/mvrp.h"
|
||||
|
||||
struct data {
|
||||
struct pw_main_loop *loop;
|
||||
struct pw_context *context;
|
||||
struct pw_core *core;
|
||||
struct spa_hook core_listener;
|
||||
|
||||
struct impl impl;
|
||||
struct server *server;
|
||||
|
||||
const char *opt_remote;
|
||||
const char *opt_name;
|
||||
bool opt_milan;
|
||||
};
|
||||
|
||||
static void do_quit(void *data, int signal_number)
|
||||
{
|
||||
struct data *d = data;
|
||||
pw_main_loop_quit(d->loop);
|
||||
}
|
||||
|
||||
static void on_core_error(void *data, uint32_t id, int seq,
|
||||
int res, const char *message)
|
||||
{
|
||||
struct data *d = data;
|
||||
|
||||
pw_log_error("error id:%u seq:%d res:%d (%s): %s",
|
||||
id, seq, res, spa_strerror(res), message);
|
||||
|
||||
if (id == PW_ID_CORE && res == -EPIPE)
|
||||
pw_main_loop_quit(d->loop);
|
||||
}
|
||||
|
||||
static const struct pw_core_events core_events = {
|
||||
PW_VERSION_CORE_EVENTS,
|
||||
.error = on_core_error,
|
||||
};
|
||||
|
||||
static struct server *create_virtual_server(struct data *data)
|
||||
{
|
||||
struct impl *impl = &data->impl;
|
||||
struct server *server;
|
||||
struct stream *stream;
|
||||
uint16_t idx;
|
||||
char name_buf[256];
|
||||
|
||||
server = calloc(1, sizeof(*server));
|
||||
if (server == NULL)
|
||||
return NULL;
|
||||
|
||||
server->impl = impl;
|
||||
server->ifname = strdup("virtual0");
|
||||
server->avb_mode = data->opt_milan ? AVB_MODE_MILAN_V12 : AVB_MODE_LEGACY;
|
||||
server->transport = &avb_transport_loopback;
|
||||
|
||||
spa_list_append(&impl->servers, &server->link);
|
||||
spa_hook_list_init(&server->listener_list);
|
||||
spa_list_init(&server->descriptors);
|
||||
spa_list_init(&server->streams);
|
||||
|
||||
if (server->transport->setup(server) < 0)
|
||||
goto error;
|
||||
|
||||
server->mrp = avb_mrp_new(server);
|
||||
if (server->mrp == NULL)
|
||||
goto error;
|
||||
|
||||
avb_aecp_register(server);
|
||||
server->maap = avb_maap_register(server);
|
||||
server->mmrp = avb_mmrp_register(server);
|
||||
server->msrp = avb_msrp_register(server);
|
||||
server->mvrp = avb_mvrp_register(server);
|
||||
avb_adp_register(server);
|
||||
avb_acmp_register(server);
|
||||
|
||||
server->domain_attr = avb_msrp_attribute_new(server->msrp,
|
||||
AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN);
|
||||
server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT;
|
||||
server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT;
|
||||
server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN);
|
||||
|
||||
avb_maap_reserve(server->maap, 1);
|
||||
|
||||
init_descriptors(server);
|
||||
|
||||
/* Update stream properties and activate */
|
||||
idx = 0;
|
||||
spa_list_for_each(stream, &server->streams, link) {
|
||||
if (stream->direction == SPA_DIRECTION_INPUT) {
|
||||
snprintf(name_buf, sizeof(name_buf), "%s.source.%u",
|
||||
data->opt_name, idx);
|
||||
pw_stream_update_properties(stream->stream,
|
||||
&SPA_DICT_INIT_ARRAY(((struct spa_dict_item[]) {
|
||||
{ PW_KEY_NODE_NAME, name_buf },
|
||||
{ PW_KEY_NODE_DESCRIPTION, "AVB Virtual Source" },
|
||||
{ PW_KEY_NODE_VIRTUAL, "true" },
|
||||
})));
|
||||
} else {
|
||||
snprintf(name_buf, sizeof(name_buf), "%s.sink.%u",
|
||||
data->opt_name, idx);
|
||||
pw_stream_update_properties(stream->stream,
|
||||
&SPA_DICT_INIT_ARRAY(((struct spa_dict_item[]) {
|
||||
{ PW_KEY_NODE_NAME, name_buf },
|
||||
{ PW_KEY_NODE_DESCRIPTION, "AVB Virtual Sink" },
|
||||
{ PW_KEY_NODE_VIRTUAL, "true" },
|
||||
})));
|
||||
}
|
||||
|
||||
if (stream_activate_virtual(stream, idx) < 0)
|
||||
pw_log_warn("failed to activate stream %u", idx);
|
||||
|
||||
idx++;
|
||||
}
|
||||
|
||||
return server;
|
||||
|
||||
error:
|
||||
spa_list_remove(&server->link);
|
||||
free(server->ifname);
|
||||
free(server);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void show_help(const char *name)
|
||||
{
|
||||
printf("%s [options]\n"
|
||||
" -h, --help Show this help\n"
|
||||
" --version Show version\n"
|
||||
" -r, --remote NAME Remote daemon name\n"
|
||||
" -n, --name PREFIX Node name prefix (default: avb-virtual)\n"
|
||||
" -m, --milan Use Milan v1.2 mode (default: legacy)\n",
|
||||
name);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
struct data data = { 0 };
|
||||
struct pw_loop *l;
|
||||
int res = -1;
|
||||
static const struct option long_options[] = {
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{ "version", no_argument, NULL, 'V' },
|
||||
{ "remote", required_argument, NULL, 'r' },
|
||||
{ "name", required_argument, NULL, 'n' },
|
||||
{ "milan", no_argument, NULL, 'm' },
|
||||
{ NULL, 0, NULL, 0 }
|
||||
};
|
||||
int c;
|
||||
|
||||
setlocale(LC_ALL, "");
|
||||
pw_init(&argc, &argv);
|
||||
|
||||
data.opt_name = "avb-virtual";
|
||||
|
||||
while ((c = getopt_long(argc, argv, "hVr:n:m", long_options, NULL)) != -1) {
|
||||
switch (c) {
|
||||
case 'h':
|
||||
show_help(argv[0]);
|
||||
return 0;
|
||||
case 'V':
|
||||
printf("%s\n"
|
||||
"Compiled with libpipewire %s\n"
|
||||
"Linked with libpipewire %s\n",
|
||||
argv[0],
|
||||
pw_get_headers_version(),
|
||||
pw_get_library_version());
|
||||
return 0;
|
||||
case 'r':
|
||||
data.opt_remote = optarg;
|
||||
break;
|
||||
case 'n':
|
||||
data.opt_name = optarg;
|
||||
break;
|
||||
case 'm':
|
||||
data.opt_milan = true;
|
||||
break;
|
||||
default:
|
||||
show_help(argv[0]);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
data.loop = pw_main_loop_new(NULL);
|
||||
if (data.loop == NULL) {
|
||||
fprintf(stderr, "can't create main loop: %m\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
l = pw_main_loop_get_loop(data.loop);
|
||||
pw_loop_add_signal(l, SIGINT, do_quit, &data);
|
||||
pw_loop_add_signal(l, SIGTERM, do_quit, &data);
|
||||
|
||||
data.context = pw_context_new(l, NULL, 0);
|
||||
if (data.context == NULL) {
|
||||
fprintf(stderr, "can't create context: %m\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
data.core = pw_context_connect(data.context,
|
||||
pw_properties_new(
|
||||
PW_KEY_REMOTE_NAME, data.opt_remote,
|
||||
NULL),
|
||||
0);
|
||||
if (data.core == NULL) {
|
||||
fprintf(stderr, "can't connect to PipeWire: %m\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
pw_core_add_listener(data.core, &data.core_listener,
|
||||
&core_events, &data);
|
||||
|
||||
/* Initialize the AVB impl */
|
||||
data.impl.loop = l;
|
||||
data.impl.timer_queue = pw_context_get_timer_queue(data.context);
|
||||
data.impl.context = data.context;
|
||||
data.impl.core = data.core;
|
||||
spa_list_init(&data.impl.servers);
|
||||
|
||||
/* Create the virtual AVB server with streams */
|
||||
data.server = create_virtual_server(&data);
|
||||
if (data.server == NULL) {
|
||||
fprintf(stderr, "can't create virtual AVB server: %m\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
fprintf(stdout, "Virtual AVB device running (%s mode). Press Ctrl-C to stop.\n",
|
||||
data.opt_milan ? "Milan v1.2" : "legacy");
|
||||
|
||||
pw_main_loop_run(data.loop);
|
||||
|
||||
res = 0;
|
||||
exit:
|
||||
if (data.server)
|
||||
avdecc_server_free(data.server);
|
||||
if (data.core)
|
||||
pw_core_disconnect(data.core);
|
||||
if (data.context)
|
||||
pw_context_destroy(data.context);
|
||||
if (data.loop)
|
||||
pw_main_loop_destroy(data.loop);
|
||||
pw_deinit();
|
||||
|
||||
return res;
|
||||
}
|
||||
143
test/avb-bugs.md
Normal file
143
test/avb-bugs.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# AVB Module Bugs Found via Test Suite
|
||||
|
||||
The following bugs were discovered by building a software test harness
|
||||
for the AVB protocol stack. All have been fixed in the accompanying
|
||||
patch series.
|
||||
|
||||
## 1. Heap corruption in server_destroy_descriptors
|
||||
|
||||
**File:** `src/modules/module-avb/internal.h`
|
||||
**Commit:** `69c721006`
|
||||
|
||||
`server_destroy_descriptors()` called `free(d->ptr)` followed by
|
||||
`free(d)`, but `d->ptr` points into the same allocation as `d`
|
||||
(set via `SPA_PTROFF(d, sizeof(struct descriptor), void)` in
|
||||
`server_add_descriptor()`). This is a double-free / heap corruption
|
||||
that could cause crashes or memory corruption when tearing down an
|
||||
AVB server.
|
||||
|
||||
**Fix:** Remove the erroneous `free(d->ptr)` call.
|
||||
|
||||
## 2. NULL pointer dereference in MSRP notify dispatch
|
||||
|
||||
**File:** `src/modules/module-avb/msrp.c`, `src/modules/module-avb/mvrp.c`
|
||||
**Commit:** `b056e9f85`
|
||||
|
||||
`msrp_notify()` unconditionally calls `dispatch[a->attr.type].notify()`
|
||||
but the dispatch table entry for `AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED`
|
||||
has `notify = NULL`. If a talker-failed attribute receives a registrar
|
||||
state change (e.g., `RX_NEW` triggers `NOTIFY_NEW`), this crashes with
|
||||
a NULL pointer dereference. The same unguarded pattern exists in
|
||||
`mvrp_notify()`.
|
||||
|
||||
**Fix:** Add `if (dispatch[a->attr.type].notify)` NULL check before
|
||||
calling, matching the defensive pattern already used in the encode path.
|
||||
|
||||
## 3. MRP NEW messages never transmitted
|
||||
|
||||
**File:** `src/modules/module-avb/mrp.h`, `src/modules/module-avb/mrp.c`,
|
||||
`src/modules/module-avb/msrp.c`, `src/modules/module-avb/mvrp.c`
|
||||
**Commit:** `bc2c41daa`
|
||||
|
||||
`AVB_MRP_SEND_NEW` was defined as `0`. The MSRP and MVRP event handlers
|
||||
skip attributes with `if (!a->attr.mrp->pending_send)`, treating `0` as
|
||||
"no pending send". Since the MRP state machine sets `pending_send` to
|
||||
`AVB_MRP_SEND_NEW` (0) when an attribute in state VN or AN receives a
|
||||
TX event, NEW messages were silently dropped instead of being
|
||||
transmitted. This violates IEEE 802.1Q which requires NEW messages to
|
||||
be sent when an attribute is first declared.
|
||||
|
||||
In practice, the attribute would cycle through VN -> AN -> AA over
|
||||
successive TX events, eventually sending a JOINMT instead of the
|
||||
initial NEW. The protocol still functioned because JOINMT also
|
||||
registers the attribute, but the initial declaration was lost.
|
||||
|
||||
**Fix:** Shift all `AVB_MRP_SEND_*` values to start at 1, so that 0
|
||||
unambiguously means "no send pending". Update MSRP and MVRP encoders
|
||||
to subtract 1 when encoding to the IEEE 802.1Q wire format.
|
||||
|
||||
## 4. ACMP error responses sent with wrong message type
|
||||
|
||||
**File:** `src/modules/module-avb/acmp.c`
|
||||
**Commit:** `9f4147104`
|
||||
|
||||
In `handle_connect_tx_command()` and `handle_disconnect_tx_command()`,
|
||||
`AVB_PACKET_ACMP_SET_MESSAGE_TYPE()` is called after the `goto done`
|
||||
jump target. When `find_stream()` fails (returns NULL), the code jumps
|
||||
to `done:` without setting the message type, so the error response is
|
||||
sent with the original command message type (e.g.,
|
||||
`CONNECT_TX_COMMAND = 0`) instead of the correct response type
|
||||
(`CONNECT_TX_RESPONSE = 1`).
|
||||
|
||||
A controller receiving this malformed response would not recognize it
|
||||
as a response to its command and would eventually time out.
|
||||
|
||||
**Fix:** Move `AVB_PACKET_ACMP_SET_MESSAGE_TYPE()` before the
|
||||
`find_stream()` call so the response type is always set correctly.
|
||||
|
||||
## 5. ACMP pending_destroy skips controller cleanup
|
||||
|
||||
**File:** `src/modules/module-avb/acmp.c`
|
||||
**Commit:** `9f4147104`
|
||||
|
||||
`pending_destroy()` iterates with `list_id < PENDING_CONTROLLER`
|
||||
(where `PENDING_CONTROLLER = 2`), which only cleans up
|
||||
`PENDING_TALKER` (0) and `PENDING_LISTENER` (1) lists, skipping
|
||||
`PENDING_CONTROLLER` (2). Any pending controller requests leak on
|
||||
shutdown.
|
||||
|
||||
**Fix:** Change `<` to `<=` to include the controller list.
|
||||
|
||||
## 6. Legacy AECP handlers read payload at wrong offset
|
||||
|
||||
**File:** `src/modules/module-avb/aecp-aem.c`
|
||||
|
||||
`handle_acquire_entity_avb_legacy()` and `handle_lock_entity_avb_legacy()`
|
||||
assign `const struct avb_packet_aecp_aem *p = m;` where `m` is the full
|
||||
ethernet frame (starting with `struct avb_ethernet_header`). The handlers
|
||||
then access `p->payload` to read the acquire/lock fields, but this reads
|
||||
from `m + offsetof(avb_packet_aecp_aem, payload)` instead of the correct
|
||||
`m + sizeof(avb_ethernet_header) + offsetof(avb_packet_aecp_aem, payload)`.
|
||||
This causes `descriptor_type` and `descriptor_id` to be read from the
|
||||
wrong position, leading to incorrect descriptor lookups.
|
||||
|
||||
All other AEM command handlers (e.g., `handle_read_descriptor_common`)
|
||||
correctly derive `p` via `SPA_PTROFF(h, sizeof(*h), void)`.
|
||||
|
||||
**Fix:** Change `const struct avb_packet_aecp_aem *p = m;` to properly
|
||||
skip the ethernet header:
|
||||
```c
|
||||
const struct avb_ethernet_header *h = m;
|
||||
const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
|
||||
```
|
||||
|
||||
## 7. Milan LOCK_ENTITY error response uses wrong packet pointer
|
||||
|
||||
**File:** `src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c`
|
||||
|
||||
In `handle_cmd_lock_entity_milan_v12()`, when `server_find_descriptor()`
|
||||
returns NULL, `reply_status()` is called with `p` (the AEM packet pointer
|
||||
past the ethernet header) instead of `m` (the full ethernet frame).
|
||||
`reply_status()` assumes its third argument is the full frame and casts
|
||||
it as `struct avb_ethernet_header *`. With the wrong pointer, the
|
||||
response ethernet header (including destination MAC) is corrupted.
|
||||
|
||||
**Fix:** Change `reply_status(aecp, ..., p, len)` to
|
||||
`reply_status(aecp, ..., m, len)`.
|
||||
|
||||
## 8. Lock entity re-lock timeout uses wrong units
|
||||
|
||||
**File:** `src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c`
|
||||
|
||||
When a controller that already holds the lock sends another lock request
|
||||
(to refresh it), the expire timeout is extended by:
|
||||
```c
|
||||
lock->base_info.expire_timeout +=
|
||||
AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND;
|
||||
```
|
||||
`AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND` is `60` (raw seconds),
|
||||
but `expire_timeout` is in nanoseconds. This adds only 60 nanoseconds
|
||||
instead of 60 seconds. The initial lock correctly uses
|
||||
`AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND * SPA_NSEC_PER_SEC`.
|
||||
|
||||
**Fix:** Multiply by `SPA_NSEC_PER_SEC` to match the nanosecond units.
|
||||
|
|
@ -163,3 +163,42 @@ if valgrind.found()
|
|||
env : valgrind_env,
|
||||
timeout_multiplier : 3)
|
||||
endif
|
||||
|
||||
if build_module_avb
|
||||
avb_test_inc = [pwtest_inc, include_directories('../src/modules')]
|
||||
avb_module_sources = [
|
||||
'../src/modules/module-avb/avb.c',
|
||||
'../src/modules/module-avb/adp.c',
|
||||
'../src/modules/module-avb/acmp.c',
|
||||
'../src/modules/module-avb/aecp.c',
|
||||
'../src/modules/module-avb/aecp-aem.c',
|
||||
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-available.c',
|
||||
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-control.c',
|
||||
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-name.c',
|
||||
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-clock-source.c',
|
||||
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-sampling-rate.c',
|
||||
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-deregister-unsolicited-notifications.c',
|
||||
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-register-unsolicited-notifications.c',
|
||||
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-format.c',
|
||||
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c',
|
||||
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-configuration.c',
|
||||
'../src/modules/module-avb/aecp-aem-cmds-resps/reply-unsol-helpers.c',
|
||||
'../src/modules/module-avb/es-builder.c',
|
||||
'../src/modules/module-avb/avdecc.c',
|
||||
'../src/modules/module-avb/descriptors.c',
|
||||
'../src/modules/module-avb/maap.c',
|
||||
'../src/modules/module-avb/mmrp.c',
|
||||
'../src/modules/module-avb/mrp.c',
|
||||
'../src/modules/module-avb/msrp.c',
|
||||
'../src/modules/module-avb/mvrp.c',
|
||||
'../src/modules/module-avb/srp.c',
|
||||
'../src/modules/module-avb/stream.c',
|
||||
]
|
||||
test('test-avb',
|
||||
executable('test-avb',
|
||||
['test-avb.c'] + avb_module_sources,
|
||||
include_directories: avb_test_inc,
|
||||
dependencies: [spa_dep, pipewire_dep, mathlib, dl_lib, rt_lib],
|
||||
link_with: pwtest_lib)
|
||||
)
|
||||
endif
|
||||
|
|
|
|||
356
test/test-avb-utils.h
Normal file
356
test/test-avb-utils.h
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
/* AVB test utilities */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
#ifndef TEST_AVB_UTILS_H
|
||||
#define TEST_AVB_UTILS_H
|
||||
|
||||
#include <pipewire/pipewire.h>
|
||||
|
||||
#include "module-avb/internal.h"
|
||||
#include "module-avb/packets.h"
|
||||
#include "module-avb/adp.h"
|
||||
#include "module-avb/acmp.h"
|
||||
#include "module-avb/mrp.h"
|
||||
#include "module-avb/msrp.h"
|
||||
#include "module-avb/mvrp.h"
|
||||
#include "module-avb/mmrp.h"
|
||||
#include "module-avb/maap.h"
|
||||
#include "module-avb/aecp.h"
|
||||
#include "module-avb/aecp-aem.h"
|
||||
#include "module-avb/aecp-aem-descriptors.h"
|
||||
#include "module-avb/aecp-aem-state.h"
|
||||
#include "module-avb/iec61883.h"
|
||||
#include "module-avb/aaf.h"
|
||||
#include "module-avb/stream.h"
|
||||
#include "module-avb/descriptors.h"
|
||||
#include "module-avb/avb-transport-loopback.h"
|
||||
|
||||
#define server_emit_message(s,n,m,l) \
|
||||
spa_hook_list_call(&(s)->listener_list, struct server_events, message, 0, n, m, l)
|
||||
#define server_emit_periodic(s,n) \
|
||||
spa_hook_list_call(&(s)->listener_list, struct server_events, periodic, 0, n)
|
||||
|
||||
/**
|
||||
* Create a test AVB server with loopback transport.
|
||||
* All protocol handlers are registered. No network access required.
|
||||
*/
|
||||
static inline struct server *avb_test_server_new(struct impl *impl)
|
||||
{
|
||||
struct server *server;
|
||||
|
||||
server = calloc(1, sizeof(*server));
|
||||
if (server == NULL)
|
||||
return NULL;
|
||||
|
||||
server->impl = impl;
|
||||
server->ifname = strdup("test0");
|
||||
server->avb_mode = AVB_MODE_LEGACY;
|
||||
server->transport = &avb_transport_loopback;
|
||||
|
||||
spa_list_append(&impl->servers, &server->link);
|
||||
spa_hook_list_init(&server->listener_list);
|
||||
spa_list_init(&server->descriptors);
|
||||
spa_list_init(&server->streams);
|
||||
|
||||
if (server->transport->setup(server) < 0)
|
||||
goto error;
|
||||
|
||||
server->mrp = avb_mrp_new(server);
|
||||
if (server->mrp == NULL)
|
||||
goto error;
|
||||
|
||||
avb_aecp_register(server);
|
||||
server->maap = avb_maap_register(server);
|
||||
server->mmrp = avb_mmrp_register(server);
|
||||
server->msrp = avb_msrp_register(server);
|
||||
server->mvrp = avb_mvrp_register(server);
|
||||
avb_adp_register(server);
|
||||
avb_acmp_register(server);
|
||||
|
||||
server->domain_attr = avb_msrp_attribute_new(server->msrp,
|
||||
AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN);
|
||||
server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT;
|
||||
server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT;
|
||||
server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN);
|
||||
|
||||
avb_mrp_attribute_begin(server->domain_attr->mrp, 0);
|
||||
avb_mrp_attribute_join(server->domain_attr->mrp, 0, true);
|
||||
|
||||
/* Add a minimal entity descriptor so ADP can advertise.
|
||||
* We skip init_descriptors() because it creates streams that
|
||||
* need a pw_core connection. */
|
||||
{
|
||||
struct avb_aem_desc_entity entity;
|
||||
memset(&entity, 0, sizeof(entity));
|
||||
entity.entity_id = htobe64(server->entity_id);
|
||||
entity.entity_model_id = htobe64(0x0001000000000001ULL);
|
||||
entity.entity_capabilities = htonl(
|
||||
AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED |
|
||||
AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED |
|
||||
AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED);
|
||||
entity.talker_stream_sources = htons(1);
|
||||
entity.talker_capabilities = htons(
|
||||
AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED |
|
||||
AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE);
|
||||
entity.listener_stream_sinks = htons(1);
|
||||
entity.listener_capabilities = htons(
|
||||
AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED |
|
||||
AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK);
|
||||
entity.configurations_count = htons(1);
|
||||
server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0,
|
||||
sizeof(entity), &entity);
|
||||
}
|
||||
|
||||
return server;
|
||||
|
||||
error:
|
||||
free(server->ifname);
|
||||
free(server);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static inline void avb_test_server_free(struct server *server)
|
||||
{
|
||||
avdecc_server_free(server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject a raw packet into the server's protocol dispatch.
|
||||
* This simulates receiving a packet from the network.
|
||||
*/
|
||||
static inline void avb_test_inject_packet(struct server *server,
|
||||
uint64_t now, const void *data, int len)
|
||||
{
|
||||
server_emit_message(server, now, data, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the periodic callback with a given timestamp.
|
||||
* Use this to advance time and test timeout/readvertise logic.
|
||||
*/
|
||||
static inline void avb_test_tick(struct server *server, uint64_t now)
|
||||
{
|
||||
server_emit_periodic(server, now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an ADP entity available packet.
|
||||
* Returns the packet size, or -1 on error.
|
||||
*/
|
||||
static inline int avb_test_build_adp_entity_available(
|
||||
uint8_t *buf, size_t bufsize,
|
||||
const uint8_t src_mac[6],
|
||||
uint64_t entity_id,
|
||||
int valid_time)
|
||||
{
|
||||
struct avb_ethernet_header *h;
|
||||
struct avb_packet_adp *p;
|
||||
size_t len = sizeof(*h) + sizeof(*p);
|
||||
static const uint8_t bmac[6] = AVB_BROADCAST_MAC;
|
||||
|
||||
if (bufsize < len)
|
||||
return -1;
|
||||
|
||||
memset(buf, 0, len);
|
||||
|
||||
h = (struct avb_ethernet_header *)buf;
|
||||
memcpy(h->dest, bmac, 6);
|
||||
memcpy(h->src, src_mac, 6);
|
||||
h->type = htons(AVB_TSN_ETH);
|
||||
|
||||
p = (struct avb_packet_adp *)(buf + sizeof(*h));
|
||||
AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP);
|
||||
AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH);
|
||||
AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE);
|
||||
AVB_PACKET_ADP_SET_VALID_TIME(p, valid_time);
|
||||
p->entity_id = htobe64(entity_id);
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an ADP entity departing packet.
|
||||
*/
|
||||
static inline int avb_test_build_adp_entity_departing(
|
||||
uint8_t *buf, size_t bufsize,
|
||||
const uint8_t src_mac[6],
|
||||
uint64_t entity_id)
|
||||
{
|
||||
struct avb_ethernet_header *h;
|
||||
struct avb_packet_adp *p;
|
||||
size_t len = sizeof(*h) + sizeof(*p);
|
||||
static const uint8_t bmac[6] = AVB_BROADCAST_MAC;
|
||||
|
||||
if (bufsize < len)
|
||||
return -1;
|
||||
|
||||
memset(buf, 0, len);
|
||||
|
||||
h = (struct avb_ethernet_header *)buf;
|
||||
memcpy(h->dest, bmac, 6);
|
||||
memcpy(h->src, src_mac, 6);
|
||||
h->type = htons(AVB_TSN_ETH);
|
||||
|
||||
p = (struct avb_packet_adp *)(buf + sizeof(*h));
|
||||
AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP);
|
||||
AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH);
|
||||
AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING);
|
||||
p->entity_id = htobe64(entity_id);
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an ADP entity discover packet.
|
||||
*/
|
||||
static inline int avb_test_build_adp_entity_discover(
|
||||
uint8_t *buf, size_t bufsize,
|
||||
const uint8_t src_mac[6],
|
||||
uint64_t entity_id)
|
||||
{
|
||||
struct avb_ethernet_header *h;
|
||||
struct avb_packet_adp *p;
|
||||
size_t len = sizeof(*h) + sizeof(*p);
|
||||
static const uint8_t bmac[6] = AVB_BROADCAST_MAC;
|
||||
|
||||
if (bufsize < len)
|
||||
return -1;
|
||||
|
||||
memset(buf, 0, len);
|
||||
|
||||
h = (struct avb_ethernet_header *)buf;
|
||||
memcpy(h->dest, bmac, 6);
|
||||
memcpy(h->src, src_mac, 6);
|
||||
h->type = htons(AVB_TSN_ETH);
|
||||
|
||||
p = (struct avb_packet_adp *)(buf + sizeof(*h));
|
||||
AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP);
|
||||
AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH);
|
||||
AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER);
|
||||
p->entity_id = htobe64(entity_id);
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an AECP AEM command packet for injection.
|
||||
* Returns packet size, or -1 on error.
|
||||
*/
|
||||
static inline int avb_test_build_aecp_aem(
|
||||
uint8_t *buf, size_t bufsize,
|
||||
const uint8_t src_mac[6],
|
||||
uint64_t target_guid,
|
||||
uint64_t controller_guid,
|
||||
uint16_t sequence_id,
|
||||
uint16_t command_type,
|
||||
const void *payload, size_t payload_size)
|
||||
{
|
||||
struct avb_ethernet_header *h;
|
||||
struct avb_packet_aecp_aem *p;
|
||||
size_t len = sizeof(*h) + sizeof(*p) + payload_size;
|
||||
|
||||
if (bufsize < len)
|
||||
return -1;
|
||||
|
||||
memset(buf, 0, len);
|
||||
|
||||
h = (struct avb_ethernet_header *)buf;
|
||||
memcpy(h->dest, (uint8_t[]){ 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 }, 6);
|
||||
memcpy(h->src, src_mac, 6);
|
||||
h->type = htons(AVB_TSN_ETH);
|
||||
|
||||
p = SPA_PTROFF(h, sizeof(*h), void);
|
||||
AVB_PACKET_SET_SUBTYPE(&p->aecp.hdr, AVB_SUBTYPE_AECP);
|
||||
AVB_PACKET_AECP_SET_MESSAGE_TYPE(&p->aecp, AVB_AECP_MESSAGE_TYPE_AEM_COMMAND);
|
||||
AVB_PACKET_AECP_SET_STATUS(&p->aecp, 0);
|
||||
AVB_PACKET_SET_LENGTH(&p->aecp.hdr, payload_size + 12);
|
||||
p->aecp.target_guid = htobe64(target_guid);
|
||||
p->aecp.controller_guid = htobe64(controller_guid);
|
||||
p->aecp.sequence_id = htons(sequence_id);
|
||||
AVB_PACKET_AEM_SET_COMMAND_TYPE(p, command_type);
|
||||
|
||||
if (payload && payload_size > 0)
|
||||
memcpy(p->payload, payload, payload_size);
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test AVB server in Milan v1.2 mode with loopback transport.
|
||||
* The entity descriptor is properly sized for Milan state (lock, unsol).
|
||||
*/
|
||||
static inline struct server *avb_test_server_new_milan(struct impl *impl)
|
||||
{
|
||||
struct server *server;
|
||||
|
||||
server = calloc(1, sizeof(*server));
|
||||
if (server == NULL)
|
||||
return NULL;
|
||||
|
||||
server->impl = impl;
|
||||
server->ifname = strdup("test0");
|
||||
server->avb_mode = AVB_MODE_MILAN_V12;
|
||||
server->transport = &avb_transport_loopback;
|
||||
|
||||
spa_list_append(&impl->servers, &server->link);
|
||||
spa_hook_list_init(&server->listener_list);
|
||||
spa_list_init(&server->descriptors);
|
||||
spa_list_init(&server->streams);
|
||||
|
||||
if (server->transport->setup(server) < 0)
|
||||
goto error;
|
||||
|
||||
server->mrp = avb_mrp_new(server);
|
||||
if (server->mrp == NULL)
|
||||
goto error;
|
||||
|
||||
avb_aecp_register(server);
|
||||
server->maap = avb_maap_register(server);
|
||||
server->mmrp = avb_mmrp_register(server);
|
||||
server->msrp = avb_msrp_register(server);
|
||||
server->mvrp = avb_mvrp_register(server);
|
||||
avb_adp_register(server);
|
||||
avb_acmp_register(server);
|
||||
|
||||
server->domain_attr = avb_msrp_attribute_new(server->msrp,
|
||||
AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN);
|
||||
server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT;
|
||||
server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT;
|
||||
server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN);
|
||||
|
||||
avb_mrp_attribute_begin(server->domain_attr->mrp, 0);
|
||||
avb_mrp_attribute_join(server->domain_attr->mrp, 0, true);
|
||||
|
||||
/* Add Milan-sized entity descriptor with lock/unsol state */
|
||||
{
|
||||
struct aecp_aem_entity_milan_state entity_state;
|
||||
memset(&entity_state, 0, sizeof(entity_state));
|
||||
entity_state.state.desc.entity_id = htobe64(server->entity_id);
|
||||
entity_state.state.desc.entity_model_id = htobe64(0x0001000000000001ULL);
|
||||
entity_state.state.desc.entity_capabilities = htonl(
|
||||
AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED |
|
||||
AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED |
|
||||
AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED);
|
||||
entity_state.state.desc.talker_stream_sources = htons(1);
|
||||
entity_state.state.desc.talker_capabilities = htons(
|
||||
AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED |
|
||||
AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE);
|
||||
entity_state.state.desc.listener_stream_sinks = htons(1);
|
||||
entity_state.state.desc.listener_capabilities = htons(
|
||||
AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED |
|
||||
AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK);
|
||||
entity_state.state.desc.configurations_count = htons(1);
|
||||
server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0,
|
||||
sizeof(entity_state), &entity_state);
|
||||
}
|
||||
|
||||
return server;
|
||||
|
||||
error:
|
||||
free(server->ifname);
|
||||
free(server);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#endif /* TEST_AVB_UTILS_H */
|
||||
4017
test/test-avb.c
Normal file
4017
test/test-avb.c
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue