@@ -439,6 +439,305 @@ static void test_canard_0v1_service_capacity(void)
439439 TEST_ASSERT_EQUAL_size_t (0U , alloc .allocated_fragments );
440440}
441441
442+ // =============================================
443+ // CAN ID specification compliance tests.
444+ // These tests verify CAN ID composition against hardcoded literals independently derived from the specifications,
445+ // ensuring that a systematic bit-field error cannot hide behind recomputation using the same expressions.
446+ // =============================================
447+
448+ // Cyphal v1.0 message broadcast CAN ID compliance.
449+ // Spec layout: [28:26]=prio [25]=0 [24]=0 [23]=0 [22:21]=11 [20:8]=subject_id [7:0]=0 (template, source filled later)
450+ static void test_1v0_publish_can_id_compliance (void )
451+ {
452+ canard_t self ;
453+ test_context_t ctx ;
454+ instrumented_allocator_t alloc ;
455+ const canard_bytes_chain_t payload = { .bytes = { .size = 0U , .data = NULL }, .next = NULL };
456+
457+ // Case A: prio=exceptional(0), subject_id=0
458+ init_canard (& self , & ctx , & alloc , 8U );
459+ TEST_ASSERT_TRUE (
460+ canard_1v0_publish (& self , 1000 , 1U , canard_prio_exceptional , 0U , 0U , payload , CANARD_USER_CONTEXT_NULL ));
461+ {
462+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
463+ TEST_ASSERT_NOT_NULL (tr );
464+ const uint32_t cid = can_id_from_transfer (tr );
465+ TEST_ASSERT_EQUAL_HEX32 (0x00600000UL , cid );
466+ TEST_ASSERT_EQUAL_UINT32 (0U , (cid >> 26U ) & 7U ); // priority
467+ TEST_ASSERT_EQUAL_UINT32 (0U , (cid >> 25U ) & 1U ); // service=0
468+ TEST_ASSERT_EQUAL_UINT32 (0U , (cid >> 24U ) & 1U ); // anonymous=0
469+ TEST_ASSERT_EQUAL_UINT32 (0U , (cid >> 23U ) & 1U ); // reserved
470+ TEST_ASSERT_EQUAL_UINT32 (3U , (cid >> 21U ) & 3U ); // reserved=11
471+ TEST_ASSERT_EQUAL_UINT32 (0U , (cid >> 8U ) & 0x1FFFU ); // subject_id
472+ TEST_ASSERT_EQUAL_UINT32 (0U , cid & 0xFFU ); // bits[7:0]=0
473+ }
474+ free_all_transfers (& self );
475+
476+ // Case B: prio=optional(7), subject_id=8191
477+ init_canard (& self , & ctx , & alloc , 8U );
478+ TEST_ASSERT_TRUE (
479+ canard_1v0_publish (& self , 1000 , 1U , canard_prio_optional , 8191U , 0U , payload , CANARD_USER_CONTEXT_NULL ));
480+ {
481+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
482+ TEST_ASSERT_NOT_NULL (tr );
483+ const uint32_t cid = can_id_from_transfer (tr );
484+ TEST_ASSERT_EQUAL_HEX32 (0x1C7FFF00UL , cid );
485+ TEST_ASSERT_EQUAL_UINT32 (7U , (cid >> 26U ) & 7U );
486+ TEST_ASSERT_EQUAL_UINT32 (8191U , (cid >> 8U ) & 0x1FFFU );
487+ }
488+ free_all_transfers (& self );
489+
490+ // Case C: prio=high(3), subject_id=42
491+ init_canard (& self , & ctx , & alloc , 8U );
492+ TEST_ASSERT_TRUE (canard_1v0_publish (& self , 1000 , 1U , canard_prio_high , 42U , 0U , payload , CANARD_USER_CONTEXT_NULL ));
493+ {
494+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
495+ TEST_ASSERT_NOT_NULL (tr );
496+ const uint32_t cid = can_id_from_transfer (tr );
497+ TEST_ASSERT_EQUAL_HEX32 (0x0C602A00UL , cid );
498+ TEST_ASSERT_EQUAL_UINT32 (3U , (cid >> 26U ) & 7U );
499+ TEST_ASSERT_EQUAL_UINT32 (42U , (cid >> 8U ) & 0x1FFFU );
500+ }
501+ free_all_transfers (& self );
502+ }
503+
504+ // Cyphal v1.0 service request CAN ID compliance.
505+ // Spec layout: [28:26]=prio [25]=1 [24]=1 [23]=0 [22:14]=service_id [13:7]=dest [6:0]=0 (template)
506+ static void test_1v0_request_can_id_compliance (void )
507+ {
508+ canard_t self ;
509+ test_context_t ctx ;
510+ instrumented_allocator_t alloc ;
511+ const canard_bytes_chain_t payload = { .bytes = { .size = 0U , .data = NULL }, .next = NULL };
512+
513+ // Case A: prio=exceptional(0), service_id=0, dest=1
514+ init_canard (& self , & ctx , & alloc , 8U );
515+ self .node_id = 10U ;
516+ TEST_ASSERT_TRUE (
517+ canard_1v0_request (& self , 1000 , canard_prio_exceptional , 0U , 1U , 0U , payload , CANARD_USER_CONTEXT_NULL ));
518+ {
519+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
520+ TEST_ASSERT_NOT_NULL (tr );
521+ const uint32_t cid = can_id_from_transfer (tr );
522+ TEST_ASSERT_EQUAL_HEX32 (0x03000080UL , cid );
523+ TEST_ASSERT_EQUAL_UINT32 (0U , (cid >> 26U ) & 7U ); // priority
524+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 25U ) & 1U ); // service=1
525+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 24U ) & 1U ); // request=1
526+ TEST_ASSERT_EQUAL_UINT32 (0U , (cid >> 23U ) & 1U ); // reserved
527+ TEST_ASSERT_EQUAL_UINT32 (0U , (cid >> 14U ) & 0x1FFU ); // service_id
528+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 7U ) & 0x7FU ); // dest
529+ }
530+ free_all_transfers (& self );
531+
532+ // Case B: prio=optional(7), service_id=511, dest=127
533+ init_canard (& self , & ctx , & alloc , 8U );
534+ self .node_id = 10U ;
535+ TEST_ASSERT_TRUE (
536+ canard_1v0_request (& self , 1000 , canard_prio_optional , 511U , 127U , 0U , payload , CANARD_USER_CONTEXT_NULL ));
537+ {
538+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
539+ TEST_ASSERT_NOT_NULL (tr );
540+ const uint32_t cid = can_id_from_transfer (tr );
541+ TEST_ASSERT_EQUAL_HEX32 (0x1F7FFF80UL , cid );
542+ TEST_ASSERT_EQUAL_UINT32 (7U , (cid >> 26U ) & 7U );
543+ TEST_ASSERT_EQUAL_UINT32 (511U , (cid >> 14U ) & 0x1FFU );
544+ TEST_ASSERT_EQUAL_UINT32 (127U , (cid >> 7U ) & 0x7FU );
545+ }
546+ free_all_transfers (& self );
547+ }
548+
549+ // Cyphal v1.0 service response CAN ID compliance.
550+ // Same as request but bit[24]=0 (response, not request).
551+ static void test_1v0_respond_can_id_compliance (void )
552+ {
553+ canard_t self ;
554+ test_context_t ctx ;
555+ instrumented_allocator_t alloc ;
556+ const canard_bytes_chain_t payload = { .bytes = { .size = 0U , .data = NULL }, .next = NULL };
557+
558+ // Case A: prio=fast(2), service_id=430, dest=24
559+ init_canard (& self , & ctx , & alloc , 8U );
560+ self .node_id = 11U ;
561+ TEST_ASSERT_TRUE (
562+ canard_1v0_respond (& self , 1000 , canard_prio_fast , 430U , 24U , 0U , payload , CANARD_USER_CONTEXT_NULL ));
563+ {
564+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
565+ TEST_ASSERT_NOT_NULL (tr );
566+ const uint32_t cid = can_id_from_transfer (tr );
567+ TEST_ASSERT_EQUAL_HEX32 (0x0A6B8C00UL , cid );
568+ TEST_ASSERT_EQUAL_UINT32 (2U , (cid >> 26U ) & 7U ); // priority
569+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 25U ) & 1U ); // service=1
570+ TEST_ASSERT_EQUAL_UINT32 (0U , (cid >> 24U ) & 1U ); // request=0 (response)
571+ TEST_ASSERT_EQUAL_UINT32 (430U , (cid >> 14U ) & 0x1FFU ); // service_id
572+ TEST_ASSERT_EQUAL_UINT32 (24U , (cid >> 7U ) & 0x7FU ); // dest
573+ }
574+ free_all_transfers (& self );
575+
576+ // Case B: prio=nominal(4), service_id=1, dest=1
577+ init_canard (& self , & ctx , & alloc , 8U );
578+ self .node_id = 11U ;
579+ TEST_ASSERT_TRUE (
580+ canard_1v0_respond (& self , 1000 , canard_prio_nominal , 1U , 1U , 0U , payload , CANARD_USER_CONTEXT_NULL ));
581+ {
582+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
583+ TEST_ASSERT_NOT_NULL (tr );
584+ const uint32_t cid = can_id_from_transfer (tr );
585+ TEST_ASSERT_EQUAL_HEX32 (0x12004080UL , cid );
586+ TEST_ASSERT_EQUAL_UINT32 (4U , (cid >> 26U ) & 7U );
587+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 14U ) & 0x1FFU );
588+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 7U ) & 0x7FU );
589+ }
590+ free_all_transfers (& self );
591+ }
592+
593+ // UAVCAN v0 message broadcast CAN ID compliance.
594+ // Spec layout: [28:24]=v0_prio [23:8]=dtid [7:0]=0 (template), where v0_prio=(cyphal_prio<<2)|3
595+ static void test_0v1_publish_can_id_compliance (void )
596+ {
597+ canard_t self ;
598+ test_context_t ctx ;
599+ instrumented_allocator_t alloc ;
600+ const canard_bytes_chain_t payload = { .bytes = { .size = 0U , .data = NULL }, .next = NULL };
601+
602+ // Case A: prio=exceptional(0), dtid=0
603+ init_canard (& self , & ctx , & alloc , 8U );
604+ self .node_id = 1U ;
605+ TEST_ASSERT_TRUE (
606+ canard_0v1_publish (& self , 1000 , 1U , canard_prio_exceptional , 0U , 0xFFFFU , 0U , payload , CANARD_USER_CONTEXT_NULL ));
607+ {
608+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
609+ TEST_ASSERT_NOT_NULL (tr );
610+ const uint32_t cid = can_id_from_transfer (tr );
611+ TEST_ASSERT_EQUAL_HEX32 (0x03000000UL , cid );
612+ TEST_ASSERT_EQUAL_UINT32 (3U , (cid >> 24U ) & 0x1FU ); // v0_prio = (0<<2)|3 = 3
613+ TEST_ASSERT_EQUAL_UINT32 (0U , (cid >> 8U ) & 0xFFFFU ); // dtid
614+ TEST_ASSERT_EQUAL_UINT32 (0U , cid & 0xFFU ); // bits[7:0]=0
615+ }
616+ free_all_transfers (& self );
617+
618+ // Case B: prio=optional(7), dtid=0xFFFF
619+ init_canard (& self , & ctx , & alloc , 8U );
620+ self .node_id = 1U ;
621+ TEST_ASSERT_TRUE (canard_0v1_publish (
622+ & self , 1000 , 1U , canard_prio_optional , 0xFFFFU , 0xFFFFU , 0U , payload , CANARD_USER_CONTEXT_NULL ));
623+ {
624+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
625+ TEST_ASSERT_NOT_NULL (tr );
626+ const uint32_t cid = can_id_from_transfer (tr );
627+ TEST_ASSERT_EQUAL_HEX32 (0x1FFFFF00UL , cid );
628+ TEST_ASSERT_EQUAL_UINT32 (0x1FU , (cid >> 24U ) & 0x1FU ); // v0_prio = (7<<2)|3 = 31
629+ TEST_ASSERT_EQUAL_UINT32 (0xFFFFU , (cid >> 8U ) & 0xFFFFU );
630+ }
631+ free_all_transfers (& self );
632+
633+ // Case C: prio=nominal(4), dtid=0x040A
634+ init_canard (& self , & ctx , & alloc , 8U );
635+ self .node_id = 1U ;
636+ TEST_ASSERT_TRUE (canard_0v1_publish (
637+ & self , 1000 , 1U , canard_prio_nominal , 0x040AU , 0xFFFFU , 0U , payload , CANARD_USER_CONTEXT_NULL ));
638+ {
639+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
640+ TEST_ASSERT_NOT_NULL (tr );
641+ const uint32_t cid = can_id_from_transfer (tr );
642+ TEST_ASSERT_EQUAL_HEX32 (0x13040A00UL , cid );
643+ TEST_ASSERT_EQUAL_UINT32 (0x13U , (cid >> 24U ) & 0x1FU ); // v0_prio = (4<<2)|3 = 19 = 0x13
644+ TEST_ASSERT_EQUAL_UINT32 (0x040AU , (cid >> 8U ) & 0xFFFFU );
645+ }
646+ free_all_transfers (& self );
647+ }
648+
649+ // UAVCAN v0 service request CAN ID compliance.
650+ // Spec layout: [28:24]=v0_prio [23:16]=dtid [15]=1(request) [14:8]=dest [7]=1 [6:0]=0 (template)
651+ static void test_0v1_request_can_id_compliance (void )
652+ {
653+ canard_t self ;
654+ test_context_t ctx ;
655+ instrumented_allocator_t alloc ;
656+ const canard_bytes_chain_t payload = { .bytes = { .size = 0U , .data = NULL }, .next = NULL };
657+
658+ // Case A: prio=exceptional(0), dti=1, dest=1
659+ init_canard (& self , & ctx , & alloc , 8U );
660+ self .node_id = 10U ;
661+ TEST_ASSERT_TRUE (
662+ canard_0v1_request (& self , 1000 , canard_prio_exceptional , 1U , 0xFFFFU , 1U , 0U , payload , CANARD_USER_CONTEXT_NULL ));
663+ {
664+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
665+ TEST_ASSERT_NOT_NULL (tr );
666+ const uint32_t cid = can_id_from_transfer (tr );
667+ TEST_ASSERT_EQUAL_HEX32 (0x03018180UL , cid );
668+ TEST_ASSERT_EQUAL_UINT32 (3U , (cid >> 24U ) & 0x1FU ); // v0_prio = (0<<2)|3
669+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 16U ) & 0xFFU ); // dtid
670+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 15U ) & 1U ); // request=1
671+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 8U ) & 0x7FU ); // dest
672+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 7U ) & 1U ); // service=1
673+ }
674+ free_all_transfers (& self );
675+
676+ // Case B: prio=optional(7), dti=255, dest=127
677+ init_canard (& self , & ctx , & alloc , 8U );
678+ self .node_id = 10U ;
679+ TEST_ASSERT_TRUE (canard_0v1_request (
680+ & self , 1000 , canard_prio_optional , 255U , 0xFFFFU , 127U , 0U , payload , CANARD_USER_CONTEXT_NULL ));
681+ {
682+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
683+ TEST_ASSERT_NOT_NULL (tr );
684+ const uint32_t cid = can_id_from_transfer (tr );
685+ TEST_ASSERT_EQUAL_HEX32 (0x1FFFFF80UL , cid );
686+ TEST_ASSERT_EQUAL_UINT32 (0x1FU , (cid >> 24U ) & 0x1FU );
687+ TEST_ASSERT_EQUAL_UINT32 (255U , (cid >> 16U ) & 0xFFU );
688+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 15U ) & 1U );
689+ TEST_ASSERT_EQUAL_UINT32 (127U , (cid >> 8U ) & 0x7FU );
690+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 7U ) & 1U );
691+ }
692+ free_all_transfers (& self );
693+ }
694+
695+ // UAVCAN v0 service response CAN ID compliance.
696+ // Same as request but bit[15]=0 (response, not request).
697+ static void test_0v1_respond_can_id_compliance (void )
698+ {
699+ canard_t self ;
700+ test_context_t ctx ;
701+ instrumented_allocator_t alloc ;
702+ const canard_bytes_chain_t payload = { .bytes = { .size = 0U , .data = NULL }, .next = NULL };
703+
704+ // Case A: prio=nominal(4), dti=0x37, dest=24
705+ init_canard (& self , & ctx , & alloc , 8U );
706+ self .node_id = 11U ;
707+ TEST_ASSERT_TRUE (
708+ canard_0v1_respond (& self , 1000 , canard_prio_nominal , 0x37U , 0xFFFFU , 24U , 0U , payload , CANARD_USER_CONTEXT_NULL ));
709+ {
710+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
711+ TEST_ASSERT_NOT_NULL (tr );
712+ const uint32_t cid = can_id_from_transfer (tr );
713+ TEST_ASSERT_EQUAL_HEX32 (0x13371880UL , cid );
714+ TEST_ASSERT_EQUAL_UINT32 (0x13U , (cid >> 24U ) & 0x1FU ); // v0_prio = (4<<2)|3 = 19
715+ TEST_ASSERT_EQUAL_UINT32 (0x37U , (cid >> 16U ) & 0xFFU ); // dtid
716+ TEST_ASSERT_EQUAL_UINT32 (0U , (cid >> 15U ) & 1U ); // request=0 (response)
717+ TEST_ASSERT_EQUAL_UINT32 (24U , (cid >> 8U ) & 0x7FU ); // dest
718+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 7U ) & 1U ); // service=1
719+ }
720+ free_all_transfers (& self );
721+
722+ // Case B: prio=immediate(1), dti=200, dest=42
723+ init_canard (& self , & ctx , & alloc , 8U );
724+ self .node_id = 11U ;
725+ TEST_ASSERT_TRUE (canard_0v1_respond (
726+ & self , 1000 , canard_prio_immediate , 200U , 0xFFFFU , 42U , 0U , payload , CANARD_USER_CONTEXT_NULL ));
727+ {
728+ const canard_txfer_t * const tr = LIST_HEAD (self .tx .agewise , canard_txfer_t , list_agewise );
729+ TEST_ASSERT_NOT_NULL (tr );
730+ const uint32_t cid = can_id_from_transfer (tr );
731+ TEST_ASSERT_EQUAL_HEX32 (0x07C82A80UL , cid );
732+ TEST_ASSERT_EQUAL_UINT32 (7U , (cid >> 24U ) & 0x1FU ); // v0_prio = (1<<2)|3 = 7
733+ TEST_ASSERT_EQUAL_UINT32 (200U , (cid >> 16U ) & 0xFFU );
734+ TEST_ASSERT_EQUAL_UINT32 (0U , (cid >> 15U ) & 1U );
735+ TEST_ASSERT_EQUAL_UINT32 (42U , (cid >> 8U ) & 0x7FU );
736+ TEST_ASSERT_EQUAL_UINT32 (1U , (cid >> 7U ) & 1U );
737+ }
738+ free_all_transfers (& self );
739+ }
740+
442741void setUp (void ) {}
443742
444743void tearDown (void ) {}
@@ -470,5 +769,13 @@ int main(void)
470769 RUN_TEST (test_canard_0v1_service_oom );
471770 RUN_TEST (test_canard_0v1_service_capacity );
472771
772+ // CAN ID specification compliance (hardcoded literals from specs).
773+ RUN_TEST (test_1v0_publish_can_id_compliance );
774+ RUN_TEST (test_1v0_request_can_id_compliance );
775+ RUN_TEST (test_1v0_respond_can_id_compliance );
776+ RUN_TEST (test_0v1_publish_can_id_compliance );
777+ RUN_TEST (test_0v1_request_can_id_compliance );
778+ RUN_TEST (test_0v1_respond_can_id_compliance );
779+
473780 return UNITY_END ();
474781}
0 commit comments