2026-04-07 07:06:35 -04:00
|
|
|
/* 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"
|
2026-04-07 13:07:36 -04:00
|
|
|
#include "module-avb/aecp-aem.h"
|
2026-04-07 07:06:35 -04:00
|
|
|
#include "module-avb/aecp-aem-descriptors.h"
|
2026-04-07 13:07:36 -04:00
|
|
|
#include "module-avb/aecp-aem-state.h"
|
2026-04-07 13:13:40 -04:00
|
|
|
#include "module-avb/iec61883.h"
|
|
|
|
|
#include "module-avb/aaf.h"
|
|
|
|
|
#include "module-avb/stream.h"
|
2026-04-07 07:06:35 -04:00
|
|
|
#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);
|
test: add additional AVB protocol coverage tests (phases 7-8)
Add 26 new tests covering protocol areas not yet exercised:
Phase 7 (12 tests):
- MAAP conflict detection: probe/announce conflicts, defend logic
- ACMP disconnect: RX forwarding, TX without stream, pending timeout
- AECP GET_AVB_INFO: success path and wrong descriptor type
- MRP timers: leave-all and periodic timer verification
- MSRP talker-failed: attribute processing with failure info
Phase 8 (14 tests):
- MVRP: attribute lifecycle, VID packet encoding
- MMRP: attribute type verification (MAC + service requirement)
- ADP: duplicate entity, targeted discover, readvertise, departure
- Descriptor lookup: edge cases, data integrity after add
- AECP commands: GET_CONFIGURATION, GET_SAMPLING_RATE, GET_NAME
Total test count: 72 tests across 8 phases.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 17:39:14 -04:00
|
|
|
spa_list_init(&server->streams);
|
2026-04-07 07:06:35 -04:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 13:07:36 -04:00
|
|
|
/**
|
|
|
|
|
* 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);
|
test: add additional AVB protocol coverage tests (phases 7-8)
Add 26 new tests covering protocol areas not yet exercised:
Phase 7 (12 tests):
- MAAP conflict detection: probe/announce conflicts, defend logic
- ACMP disconnect: RX forwarding, TX without stream, pending timeout
- AECP GET_AVB_INFO: success path and wrong descriptor type
- MRP timers: leave-all and periodic timer verification
- MSRP talker-failed: attribute processing with failure info
Phase 8 (14 tests):
- MVRP: attribute lifecycle, VID packet encoding
- MMRP: attribute type verification (MAC + service requirement)
- ADP: duplicate entity, targeted discover, readvertise, departure
- Descriptor lookup: edge cases, data integrity after add
- AECP commands: GET_CONFIGURATION, GET_SAMPLING_RATE, GET_NAME
Total test count: 72 tests across 8 phases.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 17:39:14 -04:00
|
|
|
spa_list_init(&server->streams);
|
2026-04-07 13:07:36 -04:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 07:06:35 -04:00
|
|
|
#endif /* TEST_AVB_UTILS_H */
|