#include "input/switch_native_output.h" #include "input/native_output_scheduler.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { // These are rumble payloads at the parser's native-send boundary, not simulated // HCI packets or evidence of physical playback. Only accepted sends advance the // independent decoder representing the controller's received command history. constexpr uint8_t kNeutral[8] = {0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40}; constexpr uint8_t kSeed[8] = {0x00, 0x21, 0x40, 0x48, 0x00, 0x01, 0x40, 0x40}; constexpr uint8_t kThree[8] = {0x78, 0x77, 0x1c, 0xe9, 0x00, 0x01, 0x40, 0x40}; constexpr uint8_t kDifferent[8] = {0xa8, 0x89, 0xe3, 0x4d, 0x80, 0x00, 0x40, 0x52}; constexpr SwitchHapticsSample kSteps[3] = { {65, 65, 2139, 2282}, {65, 66, 2139, 2093}, {65, 66, 2093, 2093}, }; constexpr uint32_t kGeneration = 7; const char* scenario = "startup"; uint64_t now_us = 1000000; bool poll_requested = false; bool writable = true; std::deque send_results; std::vector sources; std::vector timers; std::map radio_devices; std::deque permission_requests; uint16_t next_cid = 0x40; bool credit_event_only = false, in_credit_event = false; struct WireFrame { uni_hid_device_t* device; uint64_t submitted_us; std::array bytes; ControllerRumbleOutput decoded; }; struct CompatibilityCall { uni_hid_device_t* device; uint16_t delay_ms; uint16_t duration_ms; uint8_t weak; uint8_t strong; size_t wire_position; uint64_t submitted_us; }; std::vector wire; std::vector compatibility; std::map physical; void require(bool condition, const char* message) { if (!condition) { std::cerr << scenario << ": " << message << '\n'; std::exit(1); } } // Run the actual registered data-source and timer callbacks. The IRQ wake is // deliberately deferred: submitting on the producer never calls the owner. void run_until(uint64_t target_us) { require(target_us >= now_us, "fake clock moved backwards"); for (unsigned dispatches = 0; dispatches < 10000; ++dispatches) { if (writable && !credit_event_only && !permission_requests.empty()) { const auto cid = permission_requests.front(); permission_requests.pop_front(); require(!native_output_scheduler_on_can_send_now(radio_devices[cid], cid), "Nintendo arbitration swallowed the generic LED FIFO event"); continue; } if (poll_requested) { poll_requested = false; const auto ready = sources; for (auto* source : ready) { if (source->callbacks & DATA_SOURCE_CALLBACK_POLL) { require(source->handler != nullptr, "data source lacks handler"); source->handler(source, DATA_SOURCE_CALLBACK_POLL); } } continue; } for (const auto& entry : radio_devices) require(!native_output_scheduler_granted(entry.second), "native owner held a grant across an event-loop wait"); const auto next = std::min_element(timers.begin(), timers.end(), [](const auto* a, const auto* b) { return a->due_us < b->due_us; }); if (next == timers.end() || (*next)->due_us > target_us) { now_us = target_us; return; } auto* timer = *next; timers.erase(next); require(timer->due_us >= now_us && timer->handler != nullptr, "invalid timer deadline or callback"); now_us = timer->due_us; timer->handler(timer); } require(false, "runloop did not quiesce within bounded dispatches"); } void flush() { run_until(now_us); } void advance_ms(uint32_t milliseconds) { run_until(now_us + uint64_t{milliseconds} * 1000); } void conventional(uni_hid_device_t* device, uint16_t delay_ms, uint16_t duration_ms, uint8_t weak, uint8_t strong) { require(device->conn.connected && !device->native_owned, "compatibility output ran before native ownership was released"); compatibility.push_back({device, delay_ms, duration_ms, weak, strong, wire.size(), now_us}); } uni_hid_device_t device() { uni_hid_device_t result{}; result.report_parser.play_dual_rumble = conventional; return result; } ControllerIdentity identity(uint8_t address = 1, uint16_t product = 0x2009) { ControllerIdentity result{}; result.stable = true; result.transport = ControllerTransport::kClassic; result.address[0] = 0x24; result.address[1] = 0x68; result.address[5] = address; result.vendor_id = 0x057e; result.product_id = product; return result; } AdapterConfiguration persisted(std::initializer_list identities) { auto configuration = adapter_configuration_default(); for (const auto& approved : identities) { configuration.native_switch_controllers[configuration.native_switch_controller_count++] = approved; } std::array bytes{}; require(adapter_configuration_encode(configuration, bytes.data(), bytes.size()), "could not persist approval fixture through real configuration codec"); AdapterConfiguration restored{}; require(adapter_configuration_decode(ADAPTER_CONFIGURATION_SCHEMA_VERSION, bytes.data(), bytes.size(), &restored), "could not restore persisted approval fixture"); return restored; } void attach_approved(uni_hid_device_t& target) { switch_native_output_configure(persisted({identity()}), 1); switch_native_output_attach(0, kGeneration, &target, identity()); require(switch_native_output_owns(&target), "persisted physical approval did not acquire owner"); } SwitchNativeOutputDiagnostics diagnostics(uint8_t slot = 0) { SwitchNativeOutputDiagnostics result{}; switch_native_output_snapshot(slot, &result); return result; } void submit(const ControllerRumbleOutput& rumble, bool stateful = false, uint8_t slot = 0, uint32_t generation = kGeneration) { require(switch_native_output_submit(slot, generation, now_us, rumble, stateful), "current approved host command was rejected"); } ControllerRumbleOutput three_steps() { SwitchHapticsDecoder host; host.decode(kSeed); return host.decode(kThree); } const WireFrame& last_frame(uni_hid_device_t& target) { const auto found = std::find_if(wire.rbegin(), wire.rend(), [&](const auto& frame) { return frame.device == ⌖ }); require(found != wire.rend(), "controller has no accepted output"); return *found; } size_t frame_count(uni_hid_device_t& target) { return std::count_if(wire.begin(), wire.end(), [&](const auto& frame) { return frame.device == ⌖ }); } bool is_neutral(const WireFrame& frame) { return std::memcmp(frame.bytes.data(), kNeutral, sizeof(kNeutral)) == 0; } void expect_bytes(const WireFrame& frame, const uint8_t expected[8], const char* message) { require(std::memcmp(frame.bytes.data(), expected, 8) == 0, message); } void expect_sample(const SwitchHapticsSample& actual, const SwitchHapticsSample& expected) { require(actual.low_frequency_index == expected.low_frequency_index && actual.high_frequency_index == expected.high_frequency_index && actual.low_amplitude_q15 == expected.low_amplitude_q15 && actual.high_amplitude_q15 == expected.high_amplitude_q15, "accepted output has wrong actuator band amplitude or frequency"); } void expect_state(uni_hid_device_t& target, SwitchHapticsSample left, SwitchHapticsSample right = {}) { const auto& frame = last_frame(target).decoded.hd; require(frame.actuators[0].sample_count > 0 && frame.actuators[1].sample_count > 0, "accepted payload has no decoded endpoint"); expect_sample(frame.actuators[0].samples[frame.actuators[0].sample_count - 1], left); expect_sample(frame.actuators[1].samples[frame.actuators[1].sample_count - 1], right); } void expect_three_steps(uni_hid_device_t& target) { const auto& left = last_frame(target).decoded.hd.actuators[0]; require(left.sample_count == 3, "bounded schedule collapsed ordered host substeps"); for (uint8_t i = 0; i < 3; ++i) expect_sample(left.samples[i], kSteps[i]); expect_state(target, kSteps[2]); } void expect_silence_since(size_t first) { for (size_t i = first; i < wire.size(); ++i) { require(is_neutral(wire[i]), "stopped or disconnected host vibration reappeared"); } } void test_approval() { auto target = device(); switch_native_output_configure(persisted({}), 1); switch_native_output_attach(0, kGeneration, &target, identity()); require(!switch_native_output_owns(&target) && target.acquisitions == 0, "Nintendo VID/PID automatically enabled native output"); require(!switch_native_output_submit(0, kGeneration, now_us, {255, 255}, true) && !switch_native_output_feedback(&target, 255, 255, 10), "unapproved native host or feedback output was accepted"); flush(); require(wire.empty() && compatibility.empty(), "unapproved attach disturbed conventional output"); switch_native_output_configure(persisted({identity(2)}), 2); require(!switch_native_output_owns(&target) && wire.empty(), "approval leaked to another physical address of the same model"); switch_native_output_configure(persisted({identity()}), 3); require(switch_native_output_owns(&target) && target.acquisitions == 1, "persisted matching identity did not acquire native output"); require(is_neutral(last_frame(target)), "approval handoff did not establish physical neutral"); SwitchHapticsDecoder host; const size_t before = wire.size(); submit(host.decode(kSeed)); require(wire.size() == before, "producer submission bypassed deferred runloop wake"); flush(); expect_bytes(last_frame(target), kSeed, "approved unity output changed a safe native payload"); } void test_model_gate() { auto mismatch = device(); mismatch.controller_type = 1; switch_native_output_configure(persisted({identity()}), 1); switch_native_output_attach(0, kGeneration, &mismatch, identity()); require(!switch_native_output_owns(&mismatch) && mismatch.acquisitions == 0 && wire.empty(), "approved identity overrode mismatched parser controller type"); auto unavailable = device(); unavailable.info_ready = false; switch_native_output_attach(0, kGeneration, &unavailable, identity()); require(!switch_native_output_owns(&unavailable) && wire.empty(), "missing parser device-info enabled native output"); auto unstable = device(); auto transient = identity(); transient.stable = false; switch_native_output_attach(0, kGeneration, &unstable, transient); require(!switch_native_output_owns(&unstable) && wire.empty(), "unstable identity inherited a persisted approval"); switch_native_output_detach(&unstable); auto refused = device(); refused.acquire_allowed = false; switch_native_output_attach(0, kGeneration, &refused, identity()); require(!switch_native_output_owns(&refused) && !switch_native_output_submit(0, kGeneration, now_us, {255, 0}, true) && wire.empty(), "failed parser acquisition swallowed compatibility host commands"); } void test_revocation(bool expired) { auto target = device(); attach_approved(target); SwitchHapticsDecoder host; submit(host.decode(kSeed)); flush(); advance_ms(5); const auto latest = host.decode(kDifferent); const uint64_t receipt = now_us; submit(latest); // Revoke before the queued replacement reaches the owner. const size_t before = wire.size(); writable = false; switch_native_output_configure(persisted({}), 2); require(switch_native_output_owns(&target) && target.releases == 0 && compatibility.empty(), "revocation released owner before congestion allowed neutral"); require(!switch_native_output_submit(0, kGeneration, now_us, {255, 255}, true), "revoked identity continued accepting native host updates"); advance_ms(expired ? 55 : 10); require(wire.size() == before && compatibility.empty(), "blocked neutral leaked output or resumed compatibility early"); writable = true; advance_ms(1); require(!switch_native_output_owns(&target) && target.releases == 1 && compatibility.size() == 1, "neutral completion did not release and resume conventional output"); const auto& resumed = compatibility.back(); require(resumed.device == &target && resumed.delay_ms == 0 && resumed.wire_position > before && is_neutral(wire[resumed.wire_position - 1]), "compatibility did not follow the accepted neutral barrier"); if (expired) { require(resumed.duration_ms == 0 && resumed.weak == 0 && resumed.strong == 0, "revocation resurrected an expired host effect"); } else { const uint16_t remaining = static_cast((receipt + 50000 - resumed.submitted_us + 999) / 1000); require(resumed.duration_ms == remaining && resumed.weak == latest.high_frequency_magnitude && resumed.strong == latest.low_frequency_magnitude, "compatibility resumed stale magnitudes or restarted the 50ms host lifetime"); } const size_t released = wire.size(); advance_ms(100); require(wire.size() == released && compatibility.size() == 1, "retired native timer wrote after compatibility resumed"); } void test_generation() { auto old = device(); auto other = device(); auto replacement = device(); switch_native_output_configure(persisted({identity(), identity(2)}), 1); switch_native_output_attach(0, kGeneration, &old, identity()); switch_native_output_attach(1, 21, &other, identity(2)); submit({255, 0}, true); flush(); submit(three_steps()); old.conn.connected = false; const size_t old_count = frame_count(old); switch_native_output_detach(&old); switch_native_output_attach(0, kGeneration + 1, &replacement, identity()); const size_t replacement_count = frame_count(replacement); require(is_neutral(last_frame(replacement)), "reconnect inherited a previous physical baseline"); require(!switch_native_output_submit(0, kGeneration, now_us, {255, 255}, true), "old generation was accepted after same-address reconnect"); submit({0, 255}, true, 1, 21); advance_ms(60); // Includes the outstanding producer wake and old refresh/expiry deadlines. require(frame_count(old) == old_count && frame_count(replacement) == replacement_count, "old queued command or timer touched disconnected/replacement device"); expect_state(other, {}, {64, 64, 0, 17867}); submit({255, 0}, true, 0, kGeneration + 1); flush(); expect_state(replacement, {64, 64, 17867, 0}); expect_state(other, {}, {64, 64, 0, 17867}); require(diagnostics(0).completed_commands == 1 && diagnostics(1).completed_commands == 1, "slot or generation completion accounting crossed controllers"); } void test_overflow() { auto target = device(); attach_approved(target); const auto before_diagnostics = diagnostics(); const size_t before = wire.size(); SwitchHapticsDecoder host; for (unsigned i = 0; i < 39; ++i) submit(host.decode((i & 1u) ? kDifferent : kSeed)); submit(host.decode(kThree)); require(wire.size() == before && diagnostics().queue_depth <= 16, "producer bypassed bounded deferred command queue"); flush(); require(wire.size() > before && wire.size() <= before + 3 && is_neutral(wire[before]), "queue loss replayed a backlog instead of neutral plus bounded newest schedule"); expect_three_steps(target); const auto after = diagnostics(); require(after.received_commands == 40 && after.dropped_commands == 39 && after.completed_commands == 1 && after.queue_depth == 0 && after.resynchronizations > before_diagnostics.resynchronizations, "queue pressure did not distinguish discarded commands from the completed newest state"); const size_t drained = wire.size(); advance_ms(12); require(wire.size() == drained, "owner caught up obsolete queued vibrations after draining"); } void test_retry() { auto target = device(); attach_approved(target); const size_t before = wire.size(); const auto before_diagnostics = diagnostics(); send_results = {false}; submit(three_steps()); flush(); require(wire.size() == before && diagnostics().completed_commands == 0, "failed first schedule packet counted as physical output or completion"); send_results = {true, false}; advance_ms(1); require(wire.size() == before + 1 && diagnostics().completed_commands == 0, "baseline-only partial submission counted as complete host command"); advance_ms(1); expect_three_steps(target); const auto after = diagnostics(); require(wire.size() == before + 2 && after.completed_commands == 1 && after.dropped_commands == 0 && after.congested_attempts == 2 && after.resynchronizations == before_diagnostics.resynchronizations, "short congestion retry duplicated baseline, dropped history, or miscounted completion"); } void test_partial_replacement() { auto target = device(); attach_approved(target); send_results = {true, false}; submit(three_steps()); flush(); require(diagnostics().completed_commands == 0, "partial schedule was already complete"); const size_t before = wire.size(); SwitchHapticsDecoder latest_host; submit(latest_host.decode(kDifferent)); flush(); require(wire.size() == before + 2 && is_neutral(wire[before]), "replacing prepared/partially sent schedule omitted the physical reset barrier"); expect_bytes(last_frame(target), kDifferent, "old prepared tail replaced newest host output"); const auto after = diagnostics(); require(after.completed_commands == 1 && after.dropped_commands == 1, "discarded partial command was reported as completed or vanished from loss accounting"); const size_t replaced = wire.size(); advance_ms(12); require(wire.size() == replaced, "discarded prepared tail was retried after replacement"); } void test_stalled_schedule(bool partial) { auto target = device(); attach_approved(target); writable = partial; if (partial) send_results = {true, false}; const size_t before = wire.size(); submit(three_steps()); flush(); require(wire.size() == before + (partial ? 1 : 0), "incorrect initial blocked schedule setup"); writable = false; advance_ms(20); require(diagnostics().completed_commands == 0, "unsubmitted stalled schedule was counted complete"); const size_t stalled = wire.size(); writable = true; advance_ms(1); require(wire.size() > stalled, "current endpoint was not submitted after congestion"); if (partial) require(is_neutral(wire[stalled]), "partially submitted expired timeline lacked physical resynchronization"); for (size_t i = stalled; i < wire.size(); ++i) { if (is_neutral(wire[i])) continue; const auto& left = wire[i].decoded.hd.actuators[0]; require(left.sample_count == 1, "congestion replayed host substeps whose time had passed"); expect_sample(left.samples[0], kSteps[2]); } expect_state(target, kSteps[2]); require(diagnostics().completed_commands == 1, "resynchronized latest endpoint did not finish its current command"); } void test_feedback_resume() { auto target = device(); attach_approved(target); SwitchHapticsDecoder host; submit(host.decode(kSeed)); flush(); const uint64_t receipt = now_us; require(switch_native_output_feedback(&target, 0, 255, 6), "approved local feedback was rejected"); submit(host.decode(kThree)); flush(); expect_state(target, {}, {64, 64, 0, 17867}); advance_ms(5); expect_state(target, {}, {64, 64, 0, 17867}); const size_t before_resume = wire.size(); advance_ms(1); require(wire.size() > before_resume && is_neutral(wire[before_resume]), "local-feedback handoff omitted host resynchronization"); require(last_frame(target).decoded.hd.actuators[0].sample_count == 1, "feedback resumed host timeline from its beginning instead of current substep"); expect_state(target, kSteps[2]); run_until(receipt + 49000); expect_state(target, kSteps[2]); run_until(receipt + 50000); require(is_neutral(last_frame(target)), "Switch host effect survived its original 50ms deadline"); const size_t expired = wire.size(); advance_ms(100); expect_silence_since(expired); } void test_feedback_outlives_host() { auto target = device(); attach_approved(target); require(switch_native_output_feedback(&target, 0, 255, 60), "local feedback was rejected"); const uint64_t receipt = now_us; submit(three_steps()); flush(); run_until(receipt + 49000); expect_state(target, {}, {64, 64, 0, 17867}); const size_t before_expiry = wire.size(); run_until(receipt + 50000); require(wire.size() == before_expiry, "expiry of suppressed host command interrupted active local feedback"); run_until(receipt + 59000); expect_state(target, {}, {64, 64, 0, 17867}); run_until(receipt + 60000); require(is_neutral(last_frame(target)), "feedback completion resurrected already expired host output"); const size_t finished = wire.size(); advance_ms(100); expect_silence_since(finished); } void test_feedback_congestion() { auto target = device(); attach_approved(target); SwitchHapticsDecoder host; submit(host.decode(kSeed)); flush(); send_results = {true, false}; // Neutral reaches the sink; prepared feedback does not. require(switch_native_output_feedback(&target, 0, 255, 30), "local feedback was rejected"); require(is_neutral(last_frame(target)), "feedback congestion fixture did not accept its neutral barrier"); submit(host.decode(kThree)); flush(); advance_ms(1); expect_state(target, {}, {64, 64, 0, 17867}); advance_ms(28); expect_state(target, {}, {64, 64, 0, 17867}); advance_ms(1); expect_state(target, kSteps[2]); require(last_frame(target).decoded.hd.actuators[0].sample_count == 1, "congested feedback restarted suppressed host substeps on resume"); } void test_stateful() { auto target = device(); attach_approved(target); submit({255, 0}, true); flush(); expect_state(target, {64, 64, 17867, 0}); advance_ms(120); expect_state(target, {64, 64, 17867, 0}); require(diagnostics().completed_commands == 1, "stateful refreshes were counted as additional host commands"); require(switch_native_output_feedback(&target, 0, 255, 5), "stateful overlay feedback was rejected"); expect_state(target, {}, {64, 64, 0, 17867}); advance_ms(5); expect_state(target, {64, 64, 17867, 0}); advance_ms(200); expect_state(target, {64, 64, 17867, 0}); const size_t before_zero = wire.size(); submit({}, true); flush(); require(wire.size() > before_zero && is_neutral(wire[before_zero]), "explicit XInput zero did not immediately stop physical output"); expect_silence_since(before_zero); const size_t stopped = wire.size(); advance_ms(200); require(wire.size() == stopped && diagnostics().completed_commands == 2, "stateful zero kept refreshing or changed host completion accounting"); } void test_credit_driven_delivery() { auto target = device(); attach_approved(target); credit_event_only = true; SwitchHapticsDecoder host; const auto before = wire.size(); submit(host.decode(kSeed)); flush(); advance_ms(5); require(wire.size() == before, "output attempted without an available credit window"); require(!permission_requests.empty(), "native owner did not request credit notification"); const auto cid = permission_requests.front(); permission_requests.pop_front(); in_credit_event = true; require(!native_output_scheduler_on_can_send_now(&target, cid), "Nintendo arbitration swallowed the generic LED FIFO event"); in_credit_event = false; expect_bytes(last_frame(target), kSeed, "credit window did not submit current native state"); } void test_held_state_coalescing() { auto target = device(); attach_approved(target); SwitchHapticsDecoder host; submit(host.decode(kSeed)); flush(); const auto started = wire.size(); for (unsigned i = 0; i < 10; ++i) { advance_ms(8); submit(host.decode(kSeed)); flush(); } require(diagnostics().received_commands == 11 && diagnostics().completed_commands == 1 && diagnostics().coalesced_commands == 10 && diagnostics().dropped_commands == 0, "held state was lost or incorrectly counted as new radio submissions"); require(wire.size() <= started + 2, "identical reports caused redundant radio traffic"); for (size_t i = started; i < wire.size(); ++i) expect_bytes(wire[i], kSeed, "refresh changed a held native effect"); advance_ms(49); require(!is_neutral(last_frame(target)), "coalescing failed to extend the host watchdog"); advance_ms(2); require(is_neutral(last_frame(target)), "held-state watchdog did not expire"); } void test_pending_hold_preserves_initial_latency() { auto target = device(); attach_approved(target); writable = false; SwitchHapticsDecoder host; const uint64_t first = now_us; for (unsigned i = 0; i < 3; ++i) { submit(host.decode(kSeed)); flush(); advance_ms(8); } writable = true; advance_ms(1); expect_bytes(last_frame(target), kSeed, "pending coalescence changed the current effect"); require(diagnostics().completed_commands == 1 && diagnostics().coalesced_commands == 2 && diagnostics().dropped_commands == 0 && diagnostics().max_latency_us == last_frame(target).submitted_us - first, "coalescing hid the initial wait or counted redundant commands as loss"); } void test_stop_priority(bool stopping) { auto quiet = device(); auto periodic = device(); switch_native_output_configure(persisted({identity(), identity(2)}), 1); switch_native_output_attach(0, kGeneration, &quiet, identity()); switch_native_output_attach(1, 21, &periodic, identity(2)); if (stopping) { submit({255, 0}, true); flush(); } writable = false; submit({0, 255}, true, 1, 21); flush(); advance_ms(1); submit({}, true); // A true stop only when the first controller was active. flush(); const size_t before = wire.size(); writable = true; advance_ms(1); require(wire.size() >= before + 2, "one controller's grant prevented the other pending output"); require(wire[before].device == (stopping ? &quiet : &periodic), stopping ? "real stop failed to preempt an older vibration deadline" : "already-silent host report hijacked urgent stop priority"); if (stopping) require(is_neutral(wire[before]), "urgent stop was not physical neutral"); expect_state(periodic, {}, {64, 64, 0, 17867}); require(!native_output_scheduler_granted(&quiet) && !native_output_scheduler_granted(&periodic), "completed synchronous callbacks leaked a scheduler grant"); } void test_reused_device_pending_credit() { auto target = device(); attach_approved(target); writable = false; submit({255, 0}, true); flush(); const uint16_t old_cid = target.conn.interrupt_cid; switch_native_output_detach(&target); target.native_owned = false; // Parser teardown, before its object is reused. switch_native_output_attach(0, kGeneration + 1, &target, identity()); require(target.conn.interrupt_cid == old_cid, "fixture did not reuse the physical CID"); const size_t before = wire.size(); writable = true; advance_ms(1); require(wire.size() == before + 1 && is_neutral(wire.back()), "stale credit replayed output across a reused device generation"); submit({0, 255}, true, 0, kGeneration + 1); flush(); expect_state(target, {}, {64, 64, 0, 17867}); require(diagnostics().completed_commands == 1 && !native_output_scheduler_granted(&target), "reconnect lost its new command or retained a stale grant"); } } // namespace uint64_t time_us_64() { return now_us; } uint32_t time_us_32() { return static_cast(now_us); } void btstack_run_loop_set_data_source_handler( btstack_data_source_t* source, void (*handler)(btstack_data_source_t*, btstack_data_source_callback_type_t)) { source->handler = handler; } void btstack_run_loop_enable_data_source_callbacks(btstack_data_source_t* source, uint16_t callbacks) { source->callbacks |= callbacks; } void btstack_run_loop_add_data_source(btstack_data_source_t* source) { require(std::find(sources.begin(), sources.end(), source) == sources.end(), "data source was registered twice"); sources.push_back(source); } void btstack_run_loop_poll_data_sources_from_irq() { poll_requested = true; } void btstack_run_loop_set_timer_handler(btstack_timer_source_t* timer, void (*handler)(btstack_timer_source_t*)) { timer->handler = handler; } void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t timeout_ms) { // Pico runloop adds a tick; deadlines use its millisecond clock, not a busy // callback loop at the current instant when the owner requests timeout zero. timer->due_us = (now_us / 1000 + uint64_t{timeout_ms} + 1) * 1000; } void btstack_run_loop_add_timer(btstack_timer_source_t* timer) { require(std::find(timers.begin(), timers.end(), timer) == timers.end(), "timer added while already scheduled"); timers.push_back(timer); } bool btstack_run_loop_remove_timer(btstack_timer_source_t* timer) { const auto found = std::find(timers.begin(), timers.end(), timer); if (found == timers.end()) return false; timers.erase(found); return true; } bool l2cap_can_send_packet_now(uint16_t) { return writable && (!credit_event_only || in_credit_event); } int hci_number_free_acl_slots_for_handle(uint16_t) { return writable ? 8 : 0; } uint8_t l2cap_request_can_send_now_event(uint16_t cid) { if (writable && !credit_event_only) native_output_scheduler_on_can_send_now(radio_devices[cid], cid); else if (std::find(permission_requests.begin(), permission_requests.end(), cid) == permission_requests.end()) permission_requests.push_back(cid); return 0; } bool uni_hid_parser_switch_native_info(uni_hid_device_t* target, uint8_t* type, uint8_t* firmware_hi, uint8_t* firmware_lo) { if (!target || !target->info_ready) return false; if (!target->conn.interrupt_cid) target->conn.interrupt_cid = next_cid++; target->conn.handle = target->conn.interrupt_cid; radio_devices[target->conn.interrupt_cid] = target; if (type) *type = target->controller_type; if (firmware_hi) *firmware_hi = 5; if (firmware_lo) *firmware_lo = 1; return true; } bool uni_hid_parser_switch_native_acquire(uni_hid_device_t* target) { if (!target->conn.connected || !target->info_ready || !target->acquire_allowed) return false; require(!target->native_owned, "parser acquired twice without release"); target->native_owned = true; ++target->acquisitions; return true; } bool uni_hid_parser_switch_native_send(uni_hid_device_t* target, const uint8_t rumble[8]) { require(target && target->conn.connected && target->native_owned, "native send reached disconnected or unowned parser"); require(native_output_scheduler_granted(target), "native parser send bypassed the shared scheduler grant"); require(!credit_event_only || in_credit_event, "native sender polled outside the notified credit window"); bool accepted = writable; if (!send_results.empty()) { accepted = send_results.front(); send_results.pop_front(); } if (!accepted) return false; WireFrame frame{target, now_us, {}, physical[target].decode(rumble)}; std::memcpy(frame.bytes.data(), rumble, frame.bytes.size()); wire.push_back(frame); return true; } void uni_hid_parser_switch_native_release(uni_hid_device_t* target) { require(target && target->conn.connected && target->native_owned, "parser released while disconnected or already unowned"); target->native_owned = false; ++target->releases; } int main(int argc, char** argv) { require(argc == 2, "one regression scenario is required"); scenario = argv[1]; native_output_scheduler_prepare(); switch_native_output_prepare(); if (std::strcmp(scenario, "approval") == 0) test_approval(); else if (std::strcmp(scenario, "model-gate") == 0) test_model_gate(); else if (std::strcmp(scenario, "revocation") == 0) test_revocation(false); else if (std::strcmp(scenario, "revocation-expired") == 0) test_revocation(true); else if (std::strcmp(scenario, "generation") == 0) test_generation(); else if (std::strcmp(scenario, "overflow") == 0) test_overflow(); else if (std::strcmp(scenario, "retry") == 0) test_retry(); else if (std::strcmp(scenario, "partial-replacement") == 0) test_partial_replacement(); else if (std::strcmp(scenario, "stalled") == 0) test_stalled_schedule(false); else if (std::strcmp(scenario, "stalled-partial") == 0) test_stalled_schedule(true); else if (std::strcmp(scenario, "feedback-resume") == 0) test_feedback_resume(); else if (std::strcmp(scenario, "feedback-outlives-host") == 0) test_feedback_outlives_host(); else if (std::strcmp(scenario, "feedback-congestion") == 0) test_feedback_congestion(); else if (std::strcmp(scenario, "stateful") == 0) test_stateful(); else if (std::strcmp(scenario, "credit-driven") == 0) test_credit_driven_delivery(); else if (std::strcmp(scenario, "held-state") == 0) test_held_state_coalescing(); else if (std::strcmp(scenario, "pending-hold") == 0) test_pending_hold_preserves_initial_latency(); else if (std::strcmp(scenario, "silent-priority") == 0) test_stop_priority(false); else if (std::strcmp(scenario, "stop-priority") == 0) test_stop_priority(true); else if (std::strcmp(scenario, "reused-credit") == 0) test_reused_device_pending_credit(); else require(false, "unknown regression scenario"); return 0; }