Compare commits
No commits in common. "7358670e63cf39ba34f85d03d218e5599e1e3daf" and "7afc9981fe7852bcc37305ab8dd485be7f733254" have entirely different histories.
7358670e63
...
7afc9981fe
56 changed files with 303 additions and 12085 deletions
|
|
@ -75,21 +75,21 @@ The Bluetooth, UART, Switch, and XInput paths now share `ControllerState`:
|
|||
| Automatic Windows/Switch selection | Feasibility complete | Windows enumeration fix is in `db4a860`; real Windows transition and rumble were reported working. |
|
||||
| Windows feasibility test | Complete | `tools/Test-AdapterFeasibility.ps1` checks transition, PnP health, four XInput slots, controls, and rumble isolation. |
|
||||
| Protocol-neutral controller state | Complete | `ControllerState` is shared by Bluetooth, UART, Switch, and XInput paths; analog trigger precision is retained. |
|
||||
| Persistent configuration protocol | Complete | Adapter settings and identity-keyed profiles use separate two-copy CRC/generation stores with bounded transactions and recovery. |
|
||||
| Persistent configuration protocol | Core complete | Two-copy CRC-validated flash storage, chunked transactions, generation recovery, and PC read/write/reset are hardware-tested. |
|
||||
| Production USB VID/PID | Missing | Prototype uses `CAFE:4010`; obtain an appropriate project VID/PID and repeat Windows binding tests. |
|
||||
| DInput output | Missing | Add generic HID descriptor and report driver. |
|
||||
| Mac output mode | Missing | Capture/define compatible descriptor and report semantics. |
|
||||
| PlayStation Classic mode | Missing | Add strict one-controller legacy descriptor/report mode. |
|
||||
| Mega Drive mode | Missing | Add strict one-controller legacy descriptor/report mode. |
|
||||
| Manual output-mode selection | Missing | Add persistent PC command and controller chord. |
|
||||
| General button remapping | Complete | Sixteen positional logical inputs map directly to supported logical outputs per profile. |
|
||||
| Stick sensitivity | Complete | Per-stick center calibration, inner deadzone, outer saturation, fixed-point curve, and inversion run before every output serializer. |
|
||||
| Trigger ranges | Complete for current outputs | Per-trigger deadzone, saturation, curve, and digital threshold preserve analog XInput values and configured Switch thresholds. |
|
||||
| Vibration intensity | Complete | Independent weak/strong profile scales apply to host rumble; local confirmation policy remains separate. |
|
||||
| Macros | Complete | One bounded eight-step deterministic macro per profile supports buttons, D-pad, sticks, triggers, waits, and explicit end. |
|
||||
| Turbo and Auto Burst | Complete | Fixed-point phase accumulation produces 15 activations per second with deterministic cancellation. |
|
||||
| Persistent profiles | Complete | Global fallback plus sixteen stable identities each store four fixed 256-byte profiles in a two-bank atomic database. |
|
||||
| Profile switching | Complete | PC commands and a configurable controller chord persist selection with isolated rumble/onboard/transient controller LED confirmation. |
|
||||
| General button remapping | Partial | Only per-controller ABXY swap exists. |
|
||||
| Stick sensitivity | Missing in AIO | Add inner deadzone, outer saturation, curve, inversion, and center calibration. |
|
||||
| Trigger ranges | Partial | Full analog values are preserved; profile-configurable lower/upper range, curve, and digital threshold remain. |
|
||||
| Vibration intensity | Missing as configuration | Transport works; add per-profile weak/strong scaling. |
|
||||
| Macros | Missing | Add a bounded deterministic macro engine. |
|
||||
| Turbo and Auto Burst | Missing | Add exact 15 Hz behavior and cancellation rules. |
|
||||
| Persistent profiles | Missing | Hotkey state currently resets on disconnect/reboot. |
|
||||
| Profile switching | Missing | Add controller chord, USB command, rumble, and LED confirmation. |
|
||||
| Firmware updater | Partial | UF2 updating works; version query and guided reboot/install tool are missing. |
|
||||
| Switch 2 | Unverified | Requires real-hardware qualification. |
|
||||
| Windows/SteamOS/Linux/Android compatibility | Partial | Windows XInput feasibility passed; other host/output combinations need qualification. |
|
||||
|
|
@ -322,7 +322,7 @@ Core completion evidence:
|
|||
and feasibility firmware reflash, after which reset stored the 60-second
|
||||
default as generation 2
|
||||
|
||||
### Phase 3 — Mapping, tuning, profiles, and macros — Complete
|
||||
### Phase 3 — Mapping, tuning, profiles, and macros
|
||||
|
||||
Use four profile slots per stable controller identity. Resolve identity from Bluetooth transport, identity address, VID, and PID; use a global default when stable identity is unavailable.
|
||||
|
||||
|
|
@ -393,29 +393,6 @@ Acceptance:
|
|||
- No synthetic input remains stuck after any cancellation path.
|
||||
- Four controllers can use different profiles simultaneously.
|
||||
|
||||
Completion evidence:
|
||||
|
||||
- strict 256-byte profile schema and 17,696-byte fixed database support four
|
||||
profiles for the global fallback and each of sixteen stable identities
|
||||
- profile and adapter stores remain separate from each other and BTstack bonds;
|
||||
profile commits use one flash-safe batched inactive-bank replacement
|
||||
- schema-v1 profile databases migrate inherited trigger defaults to schema v2
|
||||
without losing custom thresholds, identities, active profiles, or other data
|
||||
- direct mapping, stick/trigger fixed-point transforms, Switch thresholds,
|
||||
XInput analog values, rumble scaling, macros, Turbo, Auto Burst, and all
|
||||
cancellation paths have deterministic native coverage
|
||||
- the AIO backend uses BTstack GAP connection type for stable Classic/BLE
|
||||
identity, not the stale cached Bluepad protocol field
|
||||
- profile switching is serialized against host writes, applies only after
|
||||
commit, and uses generation-safe per-slot feedback
|
||||
- profile colors/player counts are transient on connect/switch and restore the
|
||||
persistent USB slot indication after the final 75 ms gap
|
||||
- 70 tests passed; UART, AIO, and feasibility variants linked and published
|
||||
- hardware verified separate 8BitDo `057e:2009` and DualSense `054c:0ce6`
|
||||
identities, isolated active profiles and feedback, persistent 2.2-second
|
||||
profile commits, button remapping, stick tuning, 15 Hz Turbo, a releasing
|
||||
macro, and transient profile-to-slot LED restoration
|
||||
|
||||
### Phase 4 — Production USB output modes
|
||||
|
||||
Implement in this order:
|
||||
|
|
@ -559,7 +536,8 @@ Do not mark a host/controller combination complete from descriptor inspection or
|
|||
|
||||
## Next action
|
||||
|
||||
Begin Phase 4 by placing the existing Switch and verified XInput
|
||||
implementations behind one production output-driver boundary. Preserve the
|
||||
Phase 3 transform/runtime seam before serialization, then add DInput, Mac,
|
||||
PlayStation Classic, and Mega Drive descriptors in the documented order.
|
||||
Begin Phase 3 now that the storage and USB transaction boundary is proven.
|
||||
Define the fixed-capacity profile schema first, then add mapping and tuning
|
||||
transforms before macros or Turbo. Wire profile list/import/export/activate
|
||||
commands through the Phase 2 transaction protocol only after each profile
|
||||
field has runtime semantics and deterministic tests.
|
||||
|
|
|
|||
|
|
@ -99,14 +99,6 @@ add_executable(switch-pico
|
|||
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||
target_sources(switch-pico PRIVATE
|
||||
bluepad32_input_backend.cpp
|
||||
controller_identity.cpp
|
||||
controller_profile.cpp
|
||||
controller_profile_transform.cpp
|
||||
controller_synthetic_input.cpp
|
||||
controller_profile_runtime.cpp
|
||||
profile_storage.cpp
|
||||
profile_service.cpp
|
||||
pico_profile_storage.cpp
|
||||
bootsel_pairing_button.cpp
|
||||
adapter_configuration.cpp
|
||||
configuration_storage.cpp
|
||||
|
|
|
|||
50
README.md
50
README.md
|
|
@ -85,7 +85,7 @@ The Pico 2 W onboard LED reports the overall Bluetooth state:
|
|||
- **Pairing window expires**: new authentication and active discovery stop while a controller is active; remembered controllers may still initiate reconnects.
|
||||
- **Clear all pairings**: hold BOOTSEL continuously for 10 seconds, through the initial double blink, until the rapid confirmation blink starts. All controllers are disconnected and must be paired again.
|
||||
|
||||
### Managing configuration, profiles, and pairings from a PC
|
||||
### Managing configuration and pairings from a PC
|
||||
|
||||
Connect the Pico 2 W to the PC while the AIO firmware is running normally; do not enter the ROM BOOTSEL drive. `switch-pico-config` uses versioned private vendor requests on USB endpoint 0, so it does not add an interface or depend on Linux `hidraw` nodes.
|
||||
|
||||
|
|
@ -94,34 +94,25 @@ uv run switch-pico-config status
|
|||
uv run switch-pico-config config show
|
||||
uv run switch-pico-config config set --pairing-window-seconds 90
|
||||
uv run switch-pico-config config reset --yes
|
||||
uv run switch-pico-config profiles list
|
||||
uv run switch-pico-config profiles export 1 profile.json --identity 0
|
||||
uv run switch-pico-config profiles import 2 profile.json --identity 0
|
||||
uv run switch-pico-config profiles activate 2 --identity 0
|
||||
uv run switch-pico-config profiles reset all --identity 0 --yes
|
||||
uv run switch-pico-config pairings list
|
||||
uv run switch-pico-config pairings clear --yes
|
||||
```
|
||||
|
||||
Adapter configuration records use version, size, generation, and CRC fields in two dedicated flash sectors. Profiles use a separate two-bank atomic store before the adapter and Bluepad32 bond regions. Profile writes are chunked, verified before the new bank is committed, recover the previous generation after interruption or corruption, skip unchanged data, and are rate-limited.
|
||||
|
||||
`profiles list` prints identity index `0` for the global fallback plus each stable Bluetooth identity observed by the firmware. Each identity owns four persistent profiles and one active index. Exported JSON contains direct logical button mappings, independent stick and trigger calibration/curves, digital trigger thresholds, weak/strong rumble scales, a profile-switching chord, one bounded eight-step macro, and per-button Turbo modes. Profile numbers shown to users are `1` through `4`; `--identity` uses the zero-based index from `profiles list`.
|
||||
Configuration records use version, size, generation, and CRC fields. Writes are chunked and atomically committed between two dedicated flash sectors; interrupted or corrupt writes fall back to the previous valid generation. Those sectors do not overlap Bluepad32's separate bond store. Rewriting identical configuration does not consume a flash write, and successful writes are rate-limited.
|
||||
|
||||
`pairings list` refreshes and prints stored Bluetooth Classic and BLE addresses. `pairings clear --yes` deletes all bonds, disconnects active controllers, closes new authentication, and resumes discovery because no controllers remain. Destructive commands require `--yes`. If multiple compatible Picos are attached, select one with `--bus N --address N`; the error lists their locations. USB access errors require permission to the matching `/dev/bus/usb` device.
|
||||
|
||||
### Per-controller profiles
|
||||
### Per-controller ABXY layout
|
||||
|
||||
The default profile-switching chord is **L + R + Select + Start**. On DualSense, use **L1 + R1 + Create + Options**. A profile can replace this chord with any nonzero logical-button mask; a stored zero selects the default chord.
|
||||
Each connected AIO controller can toggle its own ABXY layout by pressing **L + R + Select + Start** together. On DualSense, use **L1 + R1 + Create + Options**. The controller gives one short rumble when the toggle is accepted; release the chord before toggling again.
|
||||
|
||||
- The chord cycles persistent profiles `1 → 2 → 3 → 4 → 1`.
|
||||
- Chord buttons are consumed locally and are not forwarded to the host.
|
||||
- The new profile applies only after its atomic flash commit completes.
|
||||
- Confirmation uses one to four 75 ms pulses matching the active profile number.
|
||||
- The profile policy independently enables rumble and LED feedback.
|
||||
- On connection and profile changes, RGB/player LEDs briefly show the active profile color/count, then return to the persistent USB slot color/player number.
|
||||
- Each controller identity and each of the four active USB slots remain isolated.
|
||||
- **Standard**: south→B, east→A, west→Y, north→X.
|
||||
- **Swapped**: south→A, east→B, west→X, north→Y.
|
||||
- The chord is consumed locally and is not forwarded to the Switch.
|
||||
- Other controller slots are unaffected.
|
||||
- Layout returns to the configured default after disconnect or reboot.
|
||||
|
||||
Profile input processing is deterministic: physical input is mapped and tuned first, Turbo or Auto Burst gates configured buttons second, and active macro overrides apply last. Turbo runs at 15 activations per second while held. Auto Burst starts on one press and stops on a second press or the configured macro-cancel button. Macros contain at most eight state steps plus an explicit end and are cancelled on disconnect, profile/output-mode change, configured cancellation, or adapter configuration reset.
|
||||
Edit `controller_hotkey_config.h` to change the chord, default layout, or confirmation pulse.
|
||||
|
||||
### Per-controller motion toggle
|
||||
|
||||
|
|
@ -280,7 +271,7 @@ Filters you can use:
|
|||
- When testing on a PC before plugging into a Switch, you can verify activity with the lightweight `switch_pico_bridge.switch_pico_uart` helper or the Windows "Game Controllers" panel.
|
||||
|
||||
## Building and flashing firmware
|
||||
Prereqs: Pico SDK, Arm GNU toolchain, CMake, and `picotool`.
|
||||
Prereqs: Pico SDK + CMake toolchain set up.
|
||||
|
||||
### Using `build.py`
|
||||
|
||||
|
|
@ -288,14 +279,6 @@ Prereqs: Pico SDK, Arm GNU toolchain, CMake, and `picotool`.
|
|||
were created, copies the release artifacts into `firmware/`, and flashes the ELF
|
||||
with `picotool`.
|
||||
|
||||
`build.py` automatically locates the Pico SDK and Arm GNU toolchain from valid
|
||||
existing `build/`, `build-aio/`, or `build-feasibility/` CMake caches, then from
|
||||
project-local `build/_deps/pico_sdk-src` and `build/toolchain` installs, and
|
||||
finally from conventional user and system locations. A compiler already on
|
||||
`PATH` is used without setting a toolchain override. Explicit `PICO_SDK_PATH`
|
||||
and `PICO_TOOLCHAIN_PATH` values always take precedence; an invalid explicit
|
||||
path is reported instead of silently falling back.
|
||||
|
||||
Before running it:
|
||||
|
||||
1. Install the Pico SDK, CMake toolchain, and `picotool`.
|
||||
|
|
@ -333,19 +316,16 @@ If the tools or artifacts are in non-default locations, use these environment
|
|||
variables:
|
||||
|
||||
```sh
|
||||
PICO_SDK_PATH=/path/to/pico-sdk \
|
||||
PICO_TOOLCHAIN_PATH=/path/to/arm-none-eabi-toolchain \
|
||||
PICOTOOL_PATH=/path/to/picotool \
|
||||
ELF_PATH=/path/to/switch-pico.elf \
|
||||
UF2_PATH=/path/to/switch-pico.uf2 \
|
||||
python3 build.py
|
||||
```
|
||||
|
||||
`PICO_SDK_PATH` and `PICO_TOOLCHAIN_PATH` explicitly select the SDK and
|
||||
cross-compiler installations. `PICOTOOL_PATH` selects the flashing tool,
|
||||
`ELF_PATH` selects the ELF that is checked and flashed, and `UF2_PATH` selects
|
||||
the UF2 that is checked after the build. Their defaults are `picotool` from
|
||||
`PATH`, `build/switch-pico.elf`, and `build/switch-pico.uf2`, respectively.
|
||||
`PICOTOOL_PATH` selects the flashing tool, `ELF_PATH` selects the ELF that is
|
||||
checked and flashed, and `UF2_PATH` selects the UF2 that is checked after the
|
||||
build. Their defaults are `picotool` from `PATH`, `build/switch-pico.elf`, and
|
||||
`build/switch-pico.uf2`, respectively.
|
||||
|
||||
### Manual build
|
||||
```sh
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,8 +3,6 @@
|
|||
#include <stdint.h>
|
||||
|
||||
#include "controller_color.h"
|
||||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
#include "controller_state.h"
|
||||
#include "switch_haptics.h"
|
||||
|
||||
|
|
@ -34,35 +32,16 @@ struct Bluepad32PairingSnapshot {
|
|||
bool overflow;
|
||||
Bluepad32PairingRecord records[BLUEPAD32_PAIRING_RECORD_CAPACITY];
|
||||
};
|
||||
struct Bluepad32SlotSnapshot {
|
||||
bool active;
|
||||
uint32_t connection_generation;
|
||||
ControllerIdentity identity;
|
||||
// Physical logical-button state before backend hotkey consumption.
|
||||
// Valid only for this snapshot's connection generation.
|
||||
uint16_t pre_hotkey_button_mask;
|
||||
ControllerState state;
|
||||
};
|
||||
|
||||
|
||||
|
||||
void bluepad32_input_backend_init();
|
||||
void bluepad32_input_backend_start();
|
||||
void bluepad32_input_backend_open_pairing_window();
|
||||
void bluepad32_input_backend_clear_pairings();
|
||||
void bluepad32_input_backend_snapshot(uint8_t slot,
|
||||
Bluepad32SlotSnapshot* out);
|
||||
bool bluepad32_input_backend_snapshot(uint8_t slot, ControllerState* out);
|
||||
void bluepad32_input_backend_request_pairing_snapshot();
|
||||
void bluepad32_input_backend_pairing_snapshot(
|
||||
Bluepad32PairingSnapshot* out);
|
||||
void bluepad32_input_backend_report_sent(uint8_t slot);
|
||||
void bluepad32_input_backend_queue_rumble(
|
||||
uint8_t slot, const ControllerRumbleOutput& rumble);
|
||||
// Enqueue bounded local profile confirmation for the matching live connection
|
||||
// generation. The two-entry per-slot FIFO preserves initial-then-switch
|
||||
// ordering. Profile lighting is transient and restored to the steady slot
|
||||
// indication after the final gap. Profile numbers are one-based (1..4).
|
||||
void bluepad32_input_backend_queue_profile_feedback(
|
||||
uint8_t slot, uint32_t connection_generation,
|
||||
uint8_t active_profile_number,
|
||||
ControllerProfileConfirmationPolicy policy);
|
||||
|
|
|
|||
232
build.py
232
build.py
|
|
@ -35,232 +35,6 @@ MACROS = tuple(
|
|||
for component in ("R", "G", "B")
|
||||
)
|
||||
|
||||
CMAKE_CACHE_PATHS = tuple(
|
||||
build_dir / "CMakeCache.txt"
|
||||
for build_dir in (BUILD_DIR, AIO_BUILD_DIR, FEASIBILITY_BUILD_DIR)
|
||||
)
|
||||
TOOLCHAIN_COMPILER = (
|
||||
"arm-none-eabi-gcc.exe" if os.name == "nt" else "arm-none-eabi-gcc"
|
||||
)
|
||||
|
||||
|
||||
class BuildEnvironmentError(RuntimeError):
|
||||
"""A required Pico build dependency could not be resolved."""
|
||||
|
||||
|
||||
def parse_cmake_cache(cache_path):
|
||||
"""Return the simple key/value entries from an existing CMake cache."""
|
||||
cache_path = Path(cache_path)
|
||||
try:
|
||||
lines = cache_path.read_text(encoding="utf-8").splitlines()
|
||||
except (OSError, UnicodeError):
|
||||
return {}
|
||||
|
||||
entries = {}
|
||||
for line in lines:
|
||||
if not line or line.startswith(("//", "#")) or "=" not in line:
|
||||
continue
|
||||
key_and_type, value = line.split("=", 1)
|
||||
key = key_and_type.split(":", 1)[0]
|
||||
if key:
|
||||
entries[key] = value
|
||||
return entries
|
||||
|
||||
|
||||
def _cache_path(value, cache_path):
|
||||
path = Path(value).expanduser()
|
||||
if not path.is_absolute():
|
||||
path = cache_path.parent / path
|
||||
return path
|
||||
|
||||
|
||||
def _versioned_candidates(parent):
|
||||
try:
|
||||
return sorted(
|
||||
(path for path in parent.iterdir() if path.is_dir()),
|
||||
key=lambda path: path.name,
|
||||
reverse=True,
|
||||
)
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
|
||||
def _sdk_fallback_candidates():
|
||||
yield ("project-local install", BUILD_DIR / "_deps" / "pico_sdk-src")
|
||||
pico_sdk_home = Path.home() / ".pico-sdk" / "sdk"
|
||||
for path in _versioned_candidates(pico_sdk_home):
|
||||
yield ("user Pico SDK install", path)
|
||||
yield ("user Pico SDK install", Path.home() / "pico" / "pico-sdk")
|
||||
yield ("user Pico SDK install", Path.home() / "pico-sdk")
|
||||
yield ("system Pico SDK install", Path("/opt/pico-sdk"))
|
||||
yield ("system Pico SDK install", Path("/usr/local/pico-sdk"))
|
||||
yield ("system Pico SDK install", Path("/usr/share/pico-sdk"))
|
||||
|
||||
|
||||
def _toolchain_fallback_candidates():
|
||||
yield ("project-local install", BUILD_DIR / "toolchain")
|
||||
pico_toolchain_home = Path.home() / ".pico-sdk" / "toolchain"
|
||||
for path in _versioned_candidates(pico_toolchain_home):
|
||||
yield ("user Pico toolchain install", path)
|
||||
yield (
|
||||
"user Pico toolchain install",
|
||||
Path.home() / "pico" / "arm-none-eabi-gcc",
|
||||
)
|
||||
yield ("user Pico toolchain install", Path.home() / "arm-none-eabi-gcc")
|
||||
yield ("system Pico toolchain install", Path("/opt/arm-none-eabi-gcc"))
|
||||
yield (
|
||||
"system Pico toolchain install",
|
||||
Path("/usr/local/arm-none-eabi-gcc"),
|
||||
)
|
||||
|
||||
|
||||
def _valid_sdk(path):
|
||||
return (path / "pico_sdk_init.cmake").is_file()
|
||||
|
||||
|
||||
def _valid_toolchain(path):
|
||||
return (path / "bin" / TOOLCHAIN_COMPILER).is_file()
|
||||
|
||||
|
||||
def _first_valid_candidate(candidates, validator):
|
||||
for source, candidate in candidates:
|
||||
path = Path(candidate).expanduser()
|
||||
if validator(path):
|
||||
return path, source
|
||||
return None, None
|
||||
|
||||
|
||||
def _cache_candidates(cache_paths, variables):
|
||||
for cache_path in cache_paths:
|
||||
cache_path = Path(cache_path)
|
||||
entries = parse_cmake_cache(cache_path)
|
||||
for variable, compiler_path in variables:
|
||||
value = entries.get(variable)
|
||||
if not value:
|
||||
continue
|
||||
path = _cache_path(value, cache_path)
|
||||
if compiler_path:
|
||||
path = path.parent.parent
|
||||
yield (f"cache {cache_path}", path)
|
||||
|
||||
|
||||
def _explicit_path(environ, variable, validator, expected):
|
||||
if variable not in environ:
|
||||
return None
|
||||
value = environ[variable]
|
||||
if not value:
|
||||
raise BuildEnvironmentError(f"{variable} is set but empty.")
|
||||
path = Path(value).expanduser()
|
||||
if not validator(path):
|
||||
raise BuildEnvironmentError(
|
||||
f"{variable} is set to {path}, but {expected} was not found."
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def configure_pico_environment(
|
||||
*,
|
||||
environ=None,
|
||||
cache_paths=None,
|
||||
sdk_candidates=None,
|
||||
toolchain_candidates=None,
|
||||
which=None,
|
||||
):
|
||||
"""Resolve Pico dependencies and apply auto-detected environment values."""
|
||||
environ = os.environ if environ is None else environ
|
||||
cache_paths = CMAKE_CACHE_PATHS if cache_paths is None else cache_paths
|
||||
|
||||
which = shutil.which if which is None else which
|
||||
explicit_sdk = _explicit_path(
|
||||
environ,
|
||||
"PICO_SDK_PATH",
|
||||
_valid_sdk,
|
||||
"pico_sdk_init.cmake",
|
||||
)
|
||||
explicit_toolchain = _explicit_path(
|
||||
environ,
|
||||
"PICO_TOOLCHAIN_PATH",
|
||||
_valid_toolchain,
|
||||
f"bin/{TOOLCHAIN_COMPILER}",
|
||||
)
|
||||
|
||||
sdk_path = explicit_sdk
|
||||
sdk_source = None
|
||||
if sdk_path is None:
|
||||
sdk_path, sdk_source = _first_valid_candidate(
|
||||
_cache_candidates(
|
||||
cache_paths,
|
||||
(("PICO_SDK_PATH", False),),
|
||||
),
|
||||
_valid_sdk,
|
||||
)
|
||||
if sdk_path is None:
|
||||
sdk_path, sdk_source = _first_valid_candidate(
|
||||
(
|
||||
_sdk_fallback_candidates()
|
||||
if sdk_candidates is None
|
||||
else sdk_candidates
|
||||
),
|
||||
_valid_sdk,
|
||||
)
|
||||
|
||||
compiler_on_path = which(
|
||||
TOOLCHAIN_COMPILER,
|
||||
path=environ.get("PATH", ""),
|
||||
)
|
||||
toolchain_path = explicit_toolchain
|
||||
toolchain_source = None
|
||||
if toolchain_path is None and compiler_on_path is None:
|
||||
toolchain_path, toolchain_source = _first_valid_candidate(
|
||||
_cache_candidates(
|
||||
cache_paths,
|
||||
(
|
||||
("PICO_TOOLCHAIN_PATH", False),
|
||||
("CMAKE_C_COMPILER", True),
|
||||
("PICO_COMPILER_CC", True),
|
||||
),
|
||||
),
|
||||
_valid_toolchain,
|
||||
)
|
||||
if toolchain_path is None:
|
||||
toolchain_path, toolchain_source = _first_valid_candidate(
|
||||
(
|
||||
_toolchain_fallback_candidates()
|
||||
if toolchain_candidates is None
|
||||
else toolchain_candidates
|
||||
),
|
||||
_valid_toolchain,
|
||||
)
|
||||
|
||||
missing = []
|
||||
if sdk_path is None:
|
||||
missing.append("Pico SDK (set PICO_SDK_PATH)")
|
||||
if toolchain_path is None and compiler_on_path is None:
|
||||
missing.append(
|
||||
"Arm GNU toolchain "
|
||||
"(set PICO_TOOLCHAIN_PATH or add arm-none-eabi-gcc to PATH)"
|
||||
)
|
||||
if missing:
|
||||
raise BuildEnvironmentError(
|
||||
"Missing build prerequisite(s): " + "; ".join(missing) + "."
|
||||
)
|
||||
|
||||
updates = {}
|
||||
detected = []
|
||||
if explicit_sdk is None:
|
||||
updates["PICO_SDK_PATH"] = str(sdk_path)
|
||||
detected.append(("PICO_SDK_PATH", sdk_path, sdk_source))
|
||||
if explicit_toolchain is None and compiler_on_path is None:
|
||||
updates["PICO_TOOLCHAIN_PATH"] = str(toolchain_path)
|
||||
detected.append(
|
||||
("PICO_TOOLCHAIN_PATH", toolchain_path, toolchain_source)
|
||||
)
|
||||
|
||||
environ.update(updates)
|
||||
for variable, path, source in detected:
|
||||
print(f"Auto-detected {variable}={path} ({source})")
|
||||
return updates
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build and flash the project, optionally setting grip colors.",
|
||||
|
|
@ -423,12 +197,6 @@ def flash(elf_path, allow_elf_override):
|
|||
|
||||
def main():
|
||||
args = parse_args()
|
||||
try:
|
||||
configure_pico_environment()
|
||||
except BuildEnvironmentError as exc:
|
||||
sys.stderr.write(f"Error: {exc}\n")
|
||||
sys.exit(1)
|
||||
|
||||
color = None
|
||||
|
||||
if args.random_grip_color:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ bool g_prepared = false;
|
|||
ConfigurationStorage g_storage;
|
||||
ConfigurationTransaction g_transaction;
|
||||
ConfigurationServiceSnapshot g_snapshot;
|
||||
uint32_t g_published_reset_generation = 0;
|
||||
bool g_has_committed = false;
|
||||
uint32_t g_last_commit_ms = 0;
|
||||
|
||||
|
|
@ -46,7 +45,6 @@ void configuration_service_prepare() {
|
|||
}
|
||||
critical_section_init(&g_lock);
|
||||
g_snapshot = {};
|
||||
__atomic_store_n(&g_published_reset_generation, 0, __ATOMIC_RELAXED);
|
||||
g_snapshot.configuration = adapter_configuration_default();
|
||||
g_transaction.clear();
|
||||
g_prepared = true;
|
||||
|
|
@ -173,15 +171,7 @@ ConfigurationTransactionStatus configuration_service_reset(
|
|||
if (status == ConfigurationTransactionStatus::kReceiving) {
|
||||
status = g_transaction.finish(transaction_id);
|
||||
}
|
||||
if (status == ConfigurationTransactionStatus::kPending &&
|
||||
g_snapshot.reset_generation != UINT32_MAX) {
|
||||
++g_snapshot.reset_generation;
|
||||
}
|
||||
g_snapshot.transaction = g_transaction.snapshot();
|
||||
if (status == ConfigurationTransactionStatus::kPending) {
|
||||
__atomic_store_n(&g_published_reset_generation,
|
||||
g_snapshot.reset_generation, __ATOMIC_RELEASE);
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return status;
|
||||
}
|
||||
|
|
@ -194,7 +184,3 @@ void configuration_service_snapshot(ConfigurationServiceSnapshot* output) {
|
|||
*output = g_snapshot;
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
uint32_t configuration_service_reset_generation() {
|
||||
return __atomic_load_n(&g_published_reset_generation, __ATOMIC_ACQUIRE);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ struct ConfigurationServiceSnapshot {
|
|||
AdapterConfiguration configuration{};
|
||||
uint32_t generation = 0;
|
||||
uint32_t payload_crc = 0;
|
||||
uint32_t reset_generation = 0;
|
||||
ConfigurationTransactionSnapshot transaction{};
|
||||
};
|
||||
|
||||
|
|
@ -36,7 +35,3 @@ ConfigurationTransactionStatus configuration_service_commit(
|
|||
ConfigurationTransactionStatus configuration_service_reset(
|
||||
uint32_t transaction_id);
|
||||
void configuration_service_snapshot(ConfigurationServiceSnapshot* output);
|
||||
|
||||
// Lock-free publication for the report path. The value changes as soon as a
|
||||
// configuration reset is accepted.
|
||||
uint32_t configuration_service_reset_generation();
|
||||
|
|
|
|||
|
|
@ -2,12 +2,26 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
// Bluepad32 button masks. Default chord: L + R + SELECT + START.
|
||||
#define SWITCH_ABXY_HOTKEY_BUTTON_MASK \
|
||||
(BUTTON_SHOULDER_L | BUTTON_SHOULDER_R)
|
||||
#define SWITCH_ABXY_HOTKEY_MISC_MASK \
|
||||
(MISC_BUTTON_SELECT | MISC_BUTTON_START)
|
||||
|
||||
// Motion toggle chord: D-pad Up + R + START / Options.
|
||||
#define SWITCH_MOTION_HOTKEY_DPAD_MASK DPAD_UP
|
||||
#define SWITCH_MOTION_HOTKEY_BUTTON_MASK BUTTON_SHOULDER_R
|
||||
#define SWITCH_MOTION_HOTKEY_MISC_MASK MISC_BUTTON_START
|
||||
#define SWITCH_MOTION_DEFAULT_ENABLED 1
|
||||
|
||||
// 0 starts each new connection in Nintendo positional layout; 1 starts swapped.
|
||||
#define SWITCH_ABXY_DEFAULT_SWAPPED 0
|
||||
|
||||
// Local confirmation pulse sent only to the controller that toggled.
|
||||
#define SWITCH_ABXY_FEEDBACK_DURATION_MS 120
|
||||
#define SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE 0xFF
|
||||
#define SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE 0xFF
|
||||
|
||||
// A longer pulse confirms disabled; a shorter pulse confirms enabled.
|
||||
#define SWITCH_MOTION_DISABLED_FEEDBACK_DURATION_MS 180
|
||||
#define SWITCH_MOTION_DISABLED_FEEDBACK_WEAK_MAGNITUDE 0xA0
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
#include "controller_identity.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
|
||||
bool controller_identity_valid(const ControllerIdentity& identity) {
|
||||
const uint8_t transport = static_cast<uint8_t>(identity.transport);
|
||||
if (transport > static_cast<uint8_t>(ControllerTransport::kBle)) {
|
||||
return false;
|
||||
}
|
||||
if (identity.stable) {
|
||||
return identity.transport != ControllerTransport::kUnknown;
|
||||
}
|
||||
return controller_identity_is_global(identity);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ControllerIdentity controller_identity_global() {
|
||||
return {};
|
||||
}
|
||||
|
||||
bool controller_identity_is_global(const ControllerIdentity& identity) {
|
||||
const ControllerIdentity global{};
|
||||
return controller_identity_equal(identity, global);
|
||||
}
|
||||
|
||||
bool controller_identity_equal(const ControllerIdentity& first,
|
||||
const ControllerIdentity& second) {
|
||||
return first.stable == second.stable &&
|
||||
first.transport == second.transport &&
|
||||
first.address_type == second.address_type &&
|
||||
memcmp(first.address, second.address, sizeof(first.address)) == 0 &&
|
||||
first.vendor_id == second.vendor_id &&
|
||||
first.product_id == second.product_id;
|
||||
}
|
||||
|
||||
bool controller_identity_encode(const ControllerIdentity& identity,
|
||||
uint8_t* output, size_t output_size) {
|
||||
if (output == nullptr || output_size < CONTROLLER_IDENTITY_ENCODED_SIZE ||
|
||||
!controller_identity_valid(identity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
output[0] = identity.stable ? 1 : 0;
|
||||
output[1] = static_cast<uint8_t>(identity.transport);
|
||||
output[2] = identity.address_type;
|
||||
output[3] = 0;
|
||||
memcpy(&output[4], identity.address, sizeof(identity.address));
|
||||
output[10] = static_cast<uint8_t>(identity.vendor_id);
|
||||
output[11] = static_cast<uint8_t>(identity.vendor_id >> 8);
|
||||
output[12] = static_cast<uint8_t>(identity.product_id);
|
||||
output[13] = static_cast<uint8_t>(identity.product_id >> 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool controller_identity_decode(const uint8_t* input, size_t input_size,
|
||||
ControllerIdentity* output) {
|
||||
if (input == nullptr || output == nullptr ||
|
||||
input_size != CONTROLLER_IDENTITY_ENCODED_SIZE || input[0] > 1 ||
|
||||
input[1] > static_cast<uint8_t>(ControllerTransport::kBle) ||
|
||||
input[3] != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ControllerIdentity decoded{};
|
||||
decoded.stable = input[0] != 0;
|
||||
decoded.transport = static_cast<ControllerTransport>(input[1]);
|
||||
decoded.address_type = input[2];
|
||||
memcpy(decoded.address, &input[4], sizeof(decoded.address));
|
||||
decoded.vendor_id = static_cast<uint16_t>(input[10]) |
|
||||
static_cast<uint16_t>(input[11] << 8);
|
||||
decoded.product_id = static_cast<uint16_t>(input[12]) |
|
||||
static_cast<uint16_t>(input[13] << 8);
|
||||
if (!controller_identity_valid(decoded)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*output = decoded;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
constexpr size_t CONTROLLER_IDENTITY_ENCODED_SIZE = 14;
|
||||
|
||||
enum class ControllerTransport : uint8_t {
|
||||
kUnknown = 0,
|
||||
kClassic = 1,
|
||||
kBle = 2,
|
||||
};
|
||||
|
||||
struct ControllerIdentity {
|
||||
bool stable = false;
|
||||
ControllerTransport transport = ControllerTransport::kUnknown;
|
||||
uint8_t address_type = 0;
|
||||
uint8_t address[6]{};
|
||||
uint16_t vendor_id = 0;
|
||||
uint16_t product_id = 0;
|
||||
};
|
||||
|
||||
ControllerIdentity controller_identity_global();
|
||||
bool controller_identity_is_global(const ControllerIdentity& identity);
|
||||
bool controller_identity_equal(const ControllerIdentity& first,
|
||||
const ControllerIdentity& second);
|
||||
bool controller_identity_encode(const ControllerIdentity& identity,
|
||||
uint8_t* output, size_t output_size);
|
||||
bool controller_identity_decode(const uint8_t* input, size_t input_size,
|
||||
ControllerIdentity* output);
|
||||
|
|
@ -1,741 +0,0 @@
|
|||
#include "controller_profile.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kDatabaseMagic[4] = {'S', 'P', 'D', 'B'};
|
||||
constexpr size_t kFallbackOffset = CONTROLLER_PROFILE_DATABASE_HEADER_SIZE;
|
||||
constexpr size_t kEntriesOffset =
|
||||
kFallbackOffset + CONTROLLER_PROFILE_COUNT *
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
constexpr uint8_t kStickInvertX = 1u << 0;
|
||||
constexpr uint8_t kStickInvertY = 1u << 1;
|
||||
constexpr uint8_t kMacroOverrideMask =
|
||||
kControllerProfileOverrideButtons |
|
||||
kControllerProfileOverrideLeftStick |
|
||||
kControllerProfileOverrideRightStick |
|
||||
kControllerProfileOverrideLeftTrigger |
|
||||
kControllerProfileOverrideRightTrigger;
|
||||
constexpr uint16_t kLegacyDefaultDigitalThreshold = 0x8000;
|
||||
|
||||
uint16_t profile_read_u16(const uint8_t* input) {
|
||||
return static_cast<uint16_t>(input[0]) |
|
||||
(static_cast<uint16_t>(input[1]) << 8);
|
||||
}
|
||||
|
||||
int16_t profile_read_i16(const uint8_t* input) {
|
||||
return static_cast<int16_t>(profile_read_u16(input));
|
||||
}
|
||||
|
||||
void profile_write_u16(uint8_t* output, uint16_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
}
|
||||
|
||||
void profile_write_i16(uint8_t* output, int16_t value) {
|
||||
profile_write_u16(output, static_cast<uint16_t>(value));
|
||||
}
|
||||
|
||||
bool profile_bytes_are_zero(const uint8_t* data, size_t size) {
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
if (data[index] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool valid_button(uint8_t button) {
|
||||
return button < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT ||
|
||||
button == CONTROLLER_PROFILE_NO_BUTTON;
|
||||
}
|
||||
|
||||
bool valid_macro_step(const ControllerProfileMacroStep& step,
|
||||
bool must_end) {
|
||||
if (must_end) {
|
||||
return step.type == ControllerProfileMacroStepType::kEnd &&
|
||||
step.override_flags == 0 && step.duration_ms == 0 &&
|
||||
step.output_button_mask == 0 && step.left_stick_x == 0 &&
|
||||
step.left_stick_y == 0 && step.right_stick_x == 0 &&
|
||||
step.right_stick_y == 0 && step.left_trigger == 0 &&
|
||||
step.right_trigger == 0;
|
||||
}
|
||||
if (step.type != ControllerProfileMacroStepType::kState ||
|
||||
(step.override_flags & ~kMacroOverrideMask) != 0 ||
|
||||
step.duration_ms > CONTROLLER_PROFILE_MAX_WAIT_MS) {
|
||||
return false;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideButtons) == 0 &&
|
||||
step.output_button_mask != 0) {
|
||||
return false;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideLeftStick) == 0 &&
|
||||
(step.left_stick_x != 0 || step.left_stick_y != 0)) {
|
||||
return false;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideRightStick) == 0 &&
|
||||
(step.right_stick_x != 0 || step.right_stick_y != 0)) {
|
||||
return false;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideLeftTrigger) == 0 &&
|
||||
step.left_trigger != 0) {
|
||||
return false;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideRightTrigger) == 0 &&
|
||||
step.right_trigger != 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void copy_overlap(size_t range_offset, uint8_t* output,
|
||||
size_t output_size, size_t field_offset,
|
||||
const uint8_t* field, size_t field_size) {
|
||||
const size_t range_end = range_offset + output_size;
|
||||
const size_t field_end = field_offset + field_size;
|
||||
if (range_offset >= field_end || field_offset >= range_end) {
|
||||
return;
|
||||
}
|
||||
const size_t start = range_offset > field_offset
|
||||
? range_offset
|
||||
: field_offset;
|
||||
const size_t end = range_end < field_end ? range_end : field_end;
|
||||
memcpy(&output[start - range_offset], &field[start - field_offset],
|
||||
end - start);
|
||||
}
|
||||
|
||||
bool read_zero_region(ControllerProfileDatabaseRead read, void* context,
|
||||
size_t offset, size_t size) {
|
||||
uint8_t buffer[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
while (size != 0) {
|
||||
const size_t chunk = size < sizeof(buffer) ? size : sizeof(buffer);
|
||||
if (!read(context, offset, buffer, chunk) ||
|
||||
!profile_bytes_are_zero(buffer, chunk)) {
|
||||
return false;
|
||||
}
|
||||
offset += chunk;
|
||||
size -= chunk;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ControllerProfile controller_profile_default(const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
(void)identity;
|
||||
(void)profile_index;
|
||||
ControllerProfile profile{};
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
|
||||
profile.button_map[index] = index;
|
||||
profile.turbo_modes[index] = ControllerProfileTurboMode::kOff;
|
||||
}
|
||||
for (ControllerProfileStickConfiguration& stick : profile.sticks) {
|
||||
stick.center_x = 0;
|
||||
stick.center_y = 0;
|
||||
stick.inner_deadzone = 0;
|
||||
stick.outer_saturation = 32767;
|
||||
stick.curve_q8_8 = 256;
|
||||
stick.invert_x = false;
|
||||
stick.invert_y = false;
|
||||
}
|
||||
for (ControllerProfileTriggerConfiguration& trigger : profile.triggers) {
|
||||
trigger.lower_deadzone = 0;
|
||||
trigger.upper_saturation = UINT16_MAX;
|
||||
trigger.curve_q8_8 = 256;
|
||||
trigger.digital_threshold =
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
}
|
||||
profile.weak_rumble_scale = UINT8_MAX;
|
||||
profile.strong_rumble_scale = UINT8_MAX;
|
||||
profile.confirmation_policy =
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed;
|
||||
profile.switching_chord = 0;
|
||||
profile.macro_trigger = CONTROLLER_PROFILE_NO_BUTTON;
|
||||
profile.macro_cancel = CONTROLLER_PROFILE_NO_BUTTON;
|
||||
profile.macro_step_count = 1;
|
||||
for (ControllerProfileMacroStep& step : profile.macro_steps) {
|
||||
step = {};
|
||||
step.type = ControllerProfileMacroStepType::kEnd;
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
bool controller_profile_validate(const ControllerProfile& profile) {
|
||||
for (uint8_t output : profile.button_map) {
|
||||
if (!valid_button(output)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const ControllerProfileStickConfiguration& stick : profile.sticks) {
|
||||
if (stick.inner_deadzone >= stick.outer_saturation ||
|
||||
stick.outer_saturation > 32767 || stick.curve_q8_8 == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const ControllerProfileTriggerConfiguration& trigger :
|
||||
profile.triggers) {
|
||||
if (trigger.lower_deadzone >= trigger.upper_saturation ||
|
||||
trigger.curve_q8_8 == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (static_cast<uint8_t>(profile.confirmation_policy) >
|
||||
static_cast<uint8_t>(
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed) ||
|
||||
!valid_button(profile.macro_trigger) ||
|
||||
!valid_button(profile.macro_cancel) ||
|
||||
profile.macro_step_count == 0 ||
|
||||
profile.macro_step_count > CONTROLLER_PROFILE_MACRO_STEP_CAPACITY) {
|
||||
return false;
|
||||
}
|
||||
for (ControllerProfileTurboMode mode : profile.turbo_modes) {
|
||||
if (static_cast<uint8_t>(mode) >
|
||||
static_cast<uint8_t>(ControllerProfileTurboMode::kAutoBurst)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_MACRO_STEP_CAPACITY; ++index) {
|
||||
const bool must_end = index >= profile.macro_step_count - 1;
|
||||
if (!valid_macro_step(profile.macro_steps[index], must_end)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool controller_profile_encode(const ControllerProfile& profile,
|
||||
uint8_t* output, size_t output_size) {
|
||||
if (output == nullptr || output_size != CONTROLLER_PROFILE_ENCODED_SIZE ||
|
||||
!controller_profile_validate(profile)) {
|
||||
return false;
|
||||
}
|
||||
memset(output, 0, output_size);
|
||||
profile_write_u16(&output[0], CONTROLLER_PROFILE_SCHEMA_VERSION);
|
||||
profile_write_u16(&output[2], CONTROLLER_PROFILE_ENCODED_SIZE);
|
||||
memcpy(&output[4], profile.button_map,
|
||||
CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT);
|
||||
for (uint8_t index = 0; index < 2; ++index) {
|
||||
const ControllerProfileStickConfiguration& stick =
|
||||
profile.sticks[index];
|
||||
uint8_t* encoded = &output[20 + index * 16];
|
||||
profile_write_i16(&encoded[0], stick.center_x);
|
||||
profile_write_i16(&encoded[2], stick.center_y);
|
||||
profile_write_u16(&encoded[4], stick.inner_deadzone);
|
||||
profile_write_u16(&encoded[6], stick.outer_saturation);
|
||||
profile_write_u16(&encoded[8], stick.curve_q8_8);
|
||||
encoded[10] = (stick.invert_x ? kStickInvertX : 0) |
|
||||
(stick.invert_y ? kStickInvertY : 0);
|
||||
}
|
||||
for (uint8_t index = 0; index < 2; ++index) {
|
||||
const ControllerProfileTriggerConfiguration& trigger =
|
||||
profile.triggers[index];
|
||||
uint8_t* encoded = &output[52 + index * 10];
|
||||
profile_write_u16(&encoded[0], trigger.lower_deadzone);
|
||||
profile_write_u16(&encoded[2], trigger.upper_saturation);
|
||||
profile_write_u16(&encoded[4], trigger.curve_q8_8);
|
||||
profile_write_u16(&encoded[6], trigger.digital_threshold);
|
||||
}
|
||||
output[72] = profile.weak_rumble_scale;
|
||||
output[73] = profile.strong_rumble_scale;
|
||||
output[74] = static_cast<uint8_t>(profile.confirmation_policy);
|
||||
profile_write_u16(&output[76], profile.switching_chord);
|
||||
output[78] = profile.macro_trigger;
|
||||
output[79] = profile.macro_cancel;
|
||||
output[80] = profile.macro_step_count;
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
|
||||
output[82 + index] =
|
||||
static_cast<uint8_t>(profile.turbo_modes[index]);
|
||||
}
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_MACRO_STEP_CAPACITY; ++index) {
|
||||
const ControllerProfileMacroStep& step = profile.macro_steps[index];
|
||||
uint8_t* encoded = &output[100 + index * 19];
|
||||
encoded[0] = static_cast<uint8_t>(step.type);
|
||||
encoded[1] = step.override_flags;
|
||||
profile_write_u16(&encoded[2], step.duration_ms);
|
||||
profile_write_u16(&encoded[4], step.output_button_mask);
|
||||
profile_write_i16(&encoded[6], step.left_stick_x);
|
||||
profile_write_i16(&encoded[8], step.left_stick_y);
|
||||
profile_write_i16(&encoded[10], step.right_stick_x);
|
||||
profile_write_i16(&encoded[12], step.right_stick_y);
|
||||
profile_write_u16(&encoded[14], step.left_trigger);
|
||||
profile_write_u16(&encoded[16], step.right_trigger);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool controller_profile_decode(const uint8_t* input, size_t input_size,
|
||||
ControllerProfile* output) {
|
||||
if (input == nullptr || output == nullptr ||
|
||||
input_size != CONTROLLER_PROFILE_ENCODED_SIZE) {
|
||||
return false;
|
||||
}
|
||||
const uint16_t schema_version = profile_read_u16(&input[0]);
|
||||
if ((schema_version != CONTROLLER_PROFILE_LEGACY_SCHEMA_VERSION &&
|
||||
schema_version != CONTROLLER_PROFILE_SCHEMA_VERSION) ||
|
||||
profile_read_u16(&input[2]) != CONTROLLER_PROFILE_ENCODED_SIZE ||
|
||||
!profile_bytes_are_zero(&input[31], 5) ||
|
||||
!profile_bytes_are_zero(&input[47], 5) ||
|
||||
!profile_bytes_are_zero(&input[60], 2) ||
|
||||
!profile_bytes_are_zero(&input[70], 2) || input[75] != 0 ||
|
||||
input[81] != 0 || !profile_bytes_are_zero(&input[98], 2) ||
|
||||
!profile_bytes_are_zero(&input[252], 4)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ControllerProfile profile{};
|
||||
memcpy(profile.button_map, &input[4],
|
||||
CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT);
|
||||
for (uint8_t index = 0; index < 2; ++index) {
|
||||
ControllerProfileStickConfiguration& stick = profile.sticks[index];
|
||||
const uint8_t* encoded = &input[20 + index * 16];
|
||||
if ((encoded[10] & ~(kStickInvertX | kStickInvertY)) != 0) {
|
||||
return false;
|
||||
}
|
||||
stick.center_x = profile_read_i16(&encoded[0]);
|
||||
stick.center_y = profile_read_i16(&encoded[2]);
|
||||
stick.inner_deadzone = profile_read_u16(&encoded[4]);
|
||||
stick.outer_saturation = profile_read_u16(&encoded[6]);
|
||||
stick.curve_q8_8 = profile_read_u16(&encoded[8]);
|
||||
stick.invert_x = (encoded[10] & kStickInvertX) != 0;
|
||||
stick.invert_y = (encoded[10] & kStickInvertY) != 0;
|
||||
}
|
||||
for (uint8_t index = 0; index < 2; ++index) {
|
||||
ControllerProfileTriggerConfiguration& trigger =
|
||||
profile.triggers[index];
|
||||
const uint8_t* encoded = &input[52 + index * 10];
|
||||
trigger.lower_deadzone = profile_read_u16(&encoded[0]);
|
||||
trigger.upper_saturation = profile_read_u16(&encoded[2]);
|
||||
trigger.curve_q8_8 = profile_read_u16(&encoded[4]);
|
||||
trigger.digital_threshold = profile_read_u16(&encoded[6]);
|
||||
if (schema_version == CONTROLLER_PROFILE_LEGACY_SCHEMA_VERSION &&
|
||||
trigger.digital_threshold == kLegacyDefaultDigitalThreshold) {
|
||||
trigger.digital_threshold =
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
}
|
||||
}
|
||||
profile.weak_rumble_scale = input[72];
|
||||
profile.strong_rumble_scale = input[73];
|
||||
profile.confirmation_policy =
|
||||
static_cast<ControllerProfileConfirmationPolicy>(input[74]);
|
||||
profile.switching_chord = profile_read_u16(&input[76]);
|
||||
profile.macro_trigger = input[78];
|
||||
profile.macro_cancel = input[79];
|
||||
profile.macro_step_count = input[80];
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
|
||||
profile.turbo_modes[index] =
|
||||
static_cast<ControllerProfileTurboMode>(input[82 + index]);
|
||||
}
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_MACRO_STEP_CAPACITY; ++index) {
|
||||
ControllerProfileMacroStep& step = profile.macro_steps[index];
|
||||
const uint8_t* encoded = &input[100 + index * 19];
|
||||
if (encoded[18] != 0) {
|
||||
return false;
|
||||
}
|
||||
step.type = static_cast<ControllerProfileMacroStepType>(encoded[0]);
|
||||
step.override_flags = encoded[1];
|
||||
step.duration_ms = profile_read_u16(&encoded[2]);
|
||||
step.output_button_mask = profile_read_u16(&encoded[4]);
|
||||
step.left_stick_x = profile_read_i16(&encoded[6]);
|
||||
step.left_stick_y = profile_read_i16(&encoded[8]);
|
||||
step.right_stick_x = profile_read_i16(&encoded[10]);
|
||||
step.right_stick_y = profile_read_i16(&encoded[12]);
|
||||
step.left_trigger = profile_read_u16(&encoded[14]);
|
||||
step.right_trigger = profile_read_u16(&encoded[16]);
|
||||
}
|
||||
if (!controller_profile_validate(profile)) {
|
||||
return false;
|
||||
}
|
||||
*output = profile;
|
||||
return true;
|
||||
}
|
||||
|
||||
void controller_profile_database_default(ControllerProfileDatabase* database) {
|
||||
if (database == nullptr) {
|
||||
return;
|
||||
}
|
||||
database->fallback_active_profile = 0;
|
||||
const ControllerIdentity global = controller_identity_global();
|
||||
for (ControllerProfileDatabaseEntry& entry : database->entries) {
|
||||
entry.used = false;
|
||||
entry.identity = global;
|
||||
entry.active_profile = 0;
|
||||
}
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
database->fallback_profiles[profile_index] =
|
||||
controller_profile_default(global, profile_index);
|
||||
}
|
||||
}
|
||||
|
||||
bool controller_profile_database_validate(
|
||||
const ControllerProfileDatabase& database) {
|
||||
if (database.fallback_active_profile >= CONTROLLER_PROFILE_COUNT) {
|
||||
return false;
|
||||
}
|
||||
for (const ControllerProfile& profile : database.fallback_profiles) {
|
||||
if (!controller_profile_validate(profile)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY; ++index) {
|
||||
const ControllerProfileDatabaseEntry& entry = database.entries[index];
|
||||
if (!entry.used) {
|
||||
continue;
|
||||
}
|
||||
if (!entry.identity.stable ||
|
||||
controller_identity_is_global(entry.identity) ||
|
||||
entry.active_profile >= CONTROLLER_PROFILE_COUNT) {
|
||||
return false;
|
||||
}
|
||||
for (uint8_t prior = 0; prior < index; ++prior) {
|
||||
if (database.entries[prior].used &&
|
||||
controller_identity_equal(database.entries[prior].identity,
|
||||
entry.identity)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const ControllerProfile& profile : entry.profiles) {
|
||||
if (!controller_profile_validate(profile)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool controller_profile_database_encode_range(
|
||||
const ControllerProfileDatabase& database, size_t offset,
|
||||
uint8_t* output, size_t size) {
|
||||
if (output == nullptr || offset > CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
size > CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset ||
|
||||
!controller_profile_database_validate(database)) {
|
||||
return false;
|
||||
}
|
||||
memset(output, 0, size);
|
||||
uint8_t header[CONTROLLER_PROFILE_DATABASE_HEADER_SIZE]{};
|
||||
memcpy(header, kDatabaseMagic, sizeof(kDatabaseMagic));
|
||||
profile_write_u16(&header[4],
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION);
|
||||
profile_write_u16(&header[6], CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
|
||||
header[8] = CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY;
|
||||
header[9] = CONTROLLER_PROFILE_COUNT;
|
||||
header[10] = database.fallback_active_profile;
|
||||
uint8_t used_count = 0;
|
||||
for (const ControllerProfileDatabaseEntry& entry : database.entries) {
|
||||
used_count += entry.used ? 1 : 0;
|
||||
}
|
||||
header[11] = used_count;
|
||||
copy_overlap(offset, output, size, 0, header, sizeof(header));
|
||||
|
||||
uint8_t encoded_profile[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const size_t profile_offset =
|
||||
kFallbackOffset +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
if (offset < profile_offset + CONTROLLER_PROFILE_ENCODED_SIZE &&
|
||||
offset + size > profile_offset) {
|
||||
if (!controller_profile_encode(
|
||||
database.fallback_profiles[profile_index],
|
||||
encoded_profile, sizeof(encoded_profile))) {
|
||||
return false;
|
||||
}
|
||||
copy_overlap(offset, output, size, profile_offset,
|
||||
encoded_profile, sizeof(encoded_profile));
|
||||
}
|
||||
}
|
||||
|
||||
for (uint8_t entry_index = 0;
|
||||
entry_index < CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY;
|
||||
++entry_index) {
|
||||
const ControllerProfileDatabaseEntry& entry =
|
||||
database.entries[entry_index];
|
||||
if (!entry.used) {
|
||||
continue;
|
||||
}
|
||||
const size_t entry_offset =
|
||||
kEntriesOffset + entry_index * CONTROLLER_PROFILE_DATABASE_ENTRY_SIZE;
|
||||
uint8_t entry_header[CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE]{};
|
||||
if (!controller_identity_encode(entry.identity, entry_header,
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
entry_header[14] = entry.active_profile;
|
||||
entry_header[15] = 1;
|
||||
copy_overlap(offset, output, size, entry_offset, entry_header,
|
||||
sizeof(entry_header));
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const size_t profile_offset =
|
||||
entry_offset + CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
if (offset < profile_offset + CONTROLLER_PROFILE_ENCODED_SIZE &&
|
||||
offset + size > profile_offset) {
|
||||
if (!controller_profile_encode(entry.profiles[profile_index],
|
||||
encoded_profile,
|
||||
sizeof(encoded_profile))) {
|
||||
return false;
|
||||
}
|
||||
copy_overlap(offset, output, size, profile_offset,
|
||||
encoded_profile, sizeof(encoded_profile));
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool controller_profile_database_decode(
|
||||
ControllerProfileDatabaseRead read, void* context,
|
||||
ControllerProfileDatabase* output) {
|
||||
if (read == nullptr || output == nullptr) {
|
||||
return false;
|
||||
}
|
||||
uint8_t header[CONTROLLER_PROFILE_DATABASE_HEADER_SIZE]{};
|
||||
if (!read(context, 0, header, sizeof(header))) {
|
||||
return false;
|
||||
}
|
||||
const uint16_t schema_version = profile_read_u16(&header[4]);
|
||||
if (memcmp(header, kDatabaseMagic, sizeof(kDatabaseMagic)) != 0 ||
|
||||
(schema_version !=
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION &&
|
||||
schema_version != CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION) ||
|
||||
profile_read_u16(&header[6]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
header[8] != CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY ||
|
||||
header[9] != CONTROLLER_PROFILE_COUNT ||
|
||||
header[10] >= CONTROLLER_PROFILE_COUNT ||
|
||||
header[11] > CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY ||
|
||||
!profile_bytes_are_zero(&header[12], sizeof(header) - 12)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
controller_profile_database_default(output);
|
||||
output->fallback_active_profile = header[10];
|
||||
uint8_t encoded_profile[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const size_t profile_offset =
|
||||
kFallbackOffset +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
if (!read(context, profile_offset, encoded_profile,
|
||||
sizeof(encoded_profile)) ||
|
||||
!controller_profile_decode(
|
||||
encoded_profile, sizeof(encoded_profile),
|
||||
&output->fallback_profiles[profile_index])) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t decoded_used_count = 0;
|
||||
for (uint8_t entry_index = 0;
|
||||
entry_index < CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY;
|
||||
++entry_index) {
|
||||
const size_t entry_offset =
|
||||
kEntriesOffset + entry_index * CONTROLLER_PROFILE_DATABASE_ENTRY_SIZE;
|
||||
uint8_t entry_header[CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE]{};
|
||||
if (!read(context, entry_offset, entry_header,
|
||||
sizeof(entry_header))) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
if (entry_header[15] == 0) {
|
||||
if (!profile_bytes_are_zero(entry_header, sizeof(entry_header)) ||
|
||||
!read_zero_region(
|
||||
read, context,
|
||||
entry_offset + CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE,
|
||||
CONTROLLER_PROFILE_COUNT *
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE)) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (entry_header[15] != 1 ||
|
||||
entry_header[14] >= CONTROLLER_PROFILE_COUNT) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
ControllerProfileDatabaseEntry& entry = output->entries[entry_index];
|
||||
if (!controller_identity_decode(entry_header,
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE,
|
||||
&entry.identity) ||
|
||||
!entry.identity.stable ||
|
||||
controller_identity_is_global(entry.identity)) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
entry.used = true;
|
||||
entry.active_profile = entry_header[14];
|
||||
++decoded_used_count;
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const size_t profile_offset =
|
||||
entry_offset + CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
if (!read(context, profile_offset, encoded_profile,
|
||||
sizeof(encoded_profile)) ||
|
||||
!controller_profile_decode(encoded_profile,
|
||||
sizeof(encoded_profile),
|
||||
&entry.profiles[profile_index])) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (decoded_used_count != header[11] ||
|
||||
!controller_profile_database_validate(*output)) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const ControllerProfileDatabaseEntry* controller_profile_database_find(
|
||||
const ControllerProfileDatabase& database,
|
||||
const ControllerIdentity& identity) {
|
||||
if (!identity.stable || controller_identity_is_global(identity)) {
|
||||
return nullptr;
|
||||
}
|
||||
for (const ControllerProfileDatabaseEntry& entry : database.entries) {
|
||||
if (entry.used &&
|
||||
controller_identity_equal(entry.identity, identity)) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ControllerProfileDatabaseEntry* controller_profile_database_find(
|
||||
ControllerProfileDatabase* database,
|
||||
const ControllerIdentity& identity) {
|
||||
if (database == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return const_cast<ControllerProfileDatabaseEntry*>(
|
||||
controller_profile_database_find(*database, identity));
|
||||
}
|
||||
|
||||
ControllerProfileDatabaseResult controller_profile_database_ensure(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
ControllerProfileDatabaseEntry** output) {
|
||||
if (database == nullptr || output == nullptr || !identity.stable ||
|
||||
controller_identity_is_global(identity)) {
|
||||
return ControllerProfileDatabaseResult::kInvalidArgument;
|
||||
}
|
||||
if (ControllerProfileDatabaseEntry* found =
|
||||
controller_profile_database_find(database, identity)) {
|
||||
*output = found;
|
||||
return ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
for (ControllerProfileDatabaseEntry& entry : database->entries) {
|
||||
if (!entry.used) {
|
||||
entry.active_profile = 0;
|
||||
entry.used = true;
|
||||
entry.identity = identity;
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
entry.profiles[profile_index] =
|
||||
controller_profile_default(identity, profile_index);
|
||||
}
|
||||
*output = &entry;
|
||||
return ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
}
|
||||
return ControllerProfileDatabaseResult::kFull;
|
||||
}
|
||||
|
||||
const ControllerProfile* controller_profile_database_get(
|
||||
const ControllerProfileDatabase& database,
|
||||
const ControllerIdentity& identity, uint8_t profile_index) {
|
||||
if (profile_index >= CONTROLLER_PROFILE_COUNT) {
|
||||
return nullptr;
|
||||
}
|
||||
if (controller_identity_is_global(identity)) {
|
||||
return &database.fallback_profiles[profile_index];
|
||||
}
|
||||
const ControllerProfileDatabaseEntry* entry =
|
||||
controller_profile_database_find(database, identity);
|
||||
return entry == nullptr ? nullptr : &entry->profiles[profile_index];
|
||||
}
|
||||
|
||||
ControllerProfileDatabaseResult controller_profile_database_set(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
uint8_t profile_index, const ControllerProfile& profile) {
|
||||
if (database == nullptr || profile_index >= CONTROLLER_PROFILE_COUNT ||
|
||||
!controller_profile_validate(profile)) {
|
||||
return ControllerProfileDatabaseResult::kInvalidArgument;
|
||||
}
|
||||
if (controller_identity_is_global(identity)) {
|
||||
database->fallback_profiles[profile_index] = profile;
|
||||
return ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
ControllerProfileDatabaseEntry* entry = nullptr;
|
||||
const ControllerProfileDatabaseResult result =
|
||||
controller_profile_database_ensure(database, identity, &entry);
|
||||
if (result == ControllerProfileDatabaseResult::kOk) {
|
||||
entry->profiles[profile_index] = profile;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ControllerProfileDatabaseResult controller_profile_database_reset(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
if (database == nullptr ||
|
||||
(profile_index != CONTROLLER_PROFILE_ALL &&
|
||||
profile_index >= CONTROLLER_PROFILE_COUNT)) {
|
||||
return ControllerProfileDatabaseResult::kInvalidArgument;
|
||||
}
|
||||
if (controller_identity_is_global(identity)) {
|
||||
for (uint8_t index = 0; index < CONTROLLER_PROFILE_COUNT; ++index) {
|
||||
if (profile_index == CONTROLLER_PROFILE_ALL ||
|
||||
profile_index == index) {
|
||||
database->fallback_profiles[index] =
|
||||
controller_profile_default(identity, index);
|
||||
}
|
||||
}
|
||||
return ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
ControllerProfileDatabaseEntry* entry = nullptr;
|
||||
const ControllerProfileDatabaseResult result =
|
||||
controller_profile_database_ensure(database, identity, &entry);
|
||||
if (result != ControllerProfileDatabaseResult::kOk) {
|
||||
return result;
|
||||
}
|
||||
for (uint8_t index = 0; index < CONTROLLER_PROFILE_COUNT; ++index) {
|
||||
if (profile_index == CONTROLLER_PROFILE_ALL ||
|
||||
profile_index == index) {
|
||||
entry->profiles[index] = controller_profile_default(identity, index);
|
||||
}
|
||||
}
|
||||
return ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
|
||||
ControllerProfileDatabaseResult controller_profile_database_activate(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
if (database == nullptr || profile_index >= CONTROLLER_PROFILE_COUNT) {
|
||||
return ControllerProfileDatabaseResult::kInvalidArgument;
|
||||
}
|
||||
if (controller_identity_is_global(identity)) {
|
||||
database->fallback_active_profile = profile_index;
|
||||
return ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
ControllerProfileDatabaseEntry* entry = nullptr;
|
||||
const ControllerProfileDatabaseResult result =
|
||||
controller_profile_database_ensure(database, identity, &entry);
|
||||
if (result == ControllerProfileDatabaseResult::kOk) {
|
||||
entry->active_profile = profile_index;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_identity.h"
|
||||
|
||||
constexpr uint16_t CONTROLLER_PROFILE_LEGACY_SCHEMA_VERSION = 1;
|
||||
constexpr uint16_t CONTROLLER_PROFILE_SCHEMA_VERSION = 2;
|
||||
constexpr size_t CONTROLLER_PROFILE_ENCODED_SIZE = 256;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_COUNT = 4;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT = 16;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_MACRO_STEP_CAPACITY = 8;
|
||||
constexpr uint16_t CONTROLLER_PROFILE_MAX_WAIT_MS = 10000;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_NO_BUTTON = 0xff;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_ALL = 0xff;
|
||||
// Exact 16-bit counterpart of the existing 358-of-1023 Switch boundary.
|
||||
constexpr uint16_t CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD = 22934;
|
||||
|
||||
constexpr uint8_t CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY = 16;
|
||||
constexpr uint16_t CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION = 1;
|
||||
constexpr uint16_t CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION = 2;
|
||||
constexpr size_t CONTROLLER_PROFILE_DATABASE_HEADER_SIZE = 32;
|
||||
constexpr size_t CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE = 16;
|
||||
constexpr size_t CONTROLLER_PROFILE_DATABASE_ENTRY_SIZE =
|
||||
CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
|
||||
CONTROLLER_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
constexpr size_t CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE =
|
||||
CONTROLLER_PROFILE_DATABASE_HEADER_SIZE +
|
||||
CONTROLLER_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE +
|
||||
CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY *
|
||||
CONTROLLER_PROFILE_DATABASE_ENTRY_SIZE;
|
||||
|
||||
static_assert(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE == 17696,
|
||||
"profile database wire size changed");
|
||||
enum class ControllerProfileLogicalButton : uint8_t {
|
||||
kSouth = 0,
|
||||
kEast = 1,
|
||||
kWest = 2,
|
||||
kNorth = 3,
|
||||
kLeftShoulder = 4,
|
||||
kRightShoulder = 5,
|
||||
kSelect = 6,
|
||||
kStart = 7,
|
||||
kSystem = 8,
|
||||
kCapture = 9,
|
||||
kLeftStick = 10,
|
||||
kRightStick = 11,
|
||||
kDpadUp = 12,
|
||||
kDpadDown = 13,
|
||||
kDpadLeft = 14,
|
||||
kDpadRight = 15,
|
||||
};
|
||||
static_assert(
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kDpadRight) + 1 ==
|
||||
CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT);
|
||||
|
||||
|
||||
enum class ControllerProfileConfirmationPolicy : uint8_t {
|
||||
kNone = 0,
|
||||
kRumble = 1,
|
||||
kLed = 2,
|
||||
kRumbleAndLed = 3,
|
||||
};
|
||||
|
||||
enum class ControllerProfileTurboMode : uint8_t {
|
||||
kOff = 0,
|
||||
kTurbo = 1,
|
||||
kAutoBurst = 2,
|
||||
};
|
||||
|
||||
enum class ControllerProfileMacroStepType : uint8_t {
|
||||
kState = 0,
|
||||
kEnd = 1,
|
||||
};
|
||||
|
||||
enum ControllerProfileMacroOverride : uint8_t {
|
||||
kControllerProfileOverrideButtons = 1u << 0,
|
||||
kControllerProfileOverrideLeftStick = 1u << 1,
|
||||
kControllerProfileOverrideRightStick = 1u << 2,
|
||||
kControllerProfileOverrideLeftTrigger = 1u << 3,
|
||||
kControllerProfileOverrideRightTrigger = 1u << 4,
|
||||
};
|
||||
|
||||
struct ControllerProfileStickConfiguration {
|
||||
int16_t center_x = 0;
|
||||
int16_t center_y = 0;
|
||||
uint16_t inner_deadzone = 0;
|
||||
uint16_t outer_saturation = 32767;
|
||||
uint16_t curve_q8_8 = 256;
|
||||
bool invert_x = false;
|
||||
bool invert_y = false;
|
||||
};
|
||||
|
||||
struct ControllerProfileTriggerConfiguration {
|
||||
uint16_t lower_deadzone = 0;
|
||||
uint16_t upper_saturation = UINT16_MAX;
|
||||
uint16_t curve_q8_8 = 256;
|
||||
// Compared against the transformed uint16 trigger output.
|
||||
uint16_t digital_threshold =
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
};
|
||||
|
||||
struct ControllerProfileMacroStep {
|
||||
ControllerProfileMacroStepType type =
|
||||
ControllerProfileMacroStepType::kEnd;
|
||||
uint8_t override_flags = 0;
|
||||
uint16_t duration_ms = 0;
|
||||
uint16_t output_button_mask = 0;
|
||||
int16_t left_stick_x = 0;
|
||||
int16_t left_stick_y = 0;
|
||||
int16_t right_stick_x = 0;
|
||||
int16_t right_stick_y = 0;
|
||||
uint16_t left_trigger = 0;
|
||||
uint16_t right_trigger = 0;
|
||||
};
|
||||
|
||||
struct ControllerProfile {
|
||||
uint8_t button_map[CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT]{};
|
||||
ControllerProfileStickConfiguration sticks[2]{};
|
||||
ControllerProfileTriggerConfiguration triggers[2]{};
|
||||
uint8_t weak_rumble_scale = UINT8_MAX;
|
||||
uint8_t strong_rumble_scale = UINT8_MAX;
|
||||
ControllerProfileConfirmationPolicy confirmation_policy =
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed;
|
||||
uint16_t switching_chord = 0;
|
||||
uint8_t macro_trigger = CONTROLLER_PROFILE_NO_BUTTON;
|
||||
uint8_t macro_cancel = CONTROLLER_PROFILE_NO_BUTTON;
|
||||
uint8_t macro_step_count = 1;
|
||||
ControllerProfileTurboMode
|
||||
turbo_modes[CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT]{};
|
||||
ControllerProfileMacroStep
|
||||
macro_steps[CONTROLLER_PROFILE_MACRO_STEP_CAPACITY]{};
|
||||
};
|
||||
|
||||
struct ControllerProfileDatabaseEntry {
|
||||
bool used = false;
|
||||
ControllerIdentity identity{};
|
||||
uint8_t active_profile = 0;
|
||||
ControllerProfile profiles[CONTROLLER_PROFILE_COUNT]{};
|
||||
};
|
||||
|
||||
struct ControllerProfileDatabase {
|
||||
uint8_t fallback_active_profile = 0;
|
||||
ControllerProfile fallback_profiles[CONTROLLER_PROFILE_COUNT]{};
|
||||
ControllerProfileDatabaseEntry
|
||||
entries[CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY]{};
|
||||
};
|
||||
|
||||
enum class ControllerProfileDatabaseResult : uint8_t {
|
||||
kOk = 0,
|
||||
kInvalidArgument = 1,
|
||||
kFull = 2,
|
||||
};
|
||||
|
||||
using ControllerProfileDatabaseRead = bool (*)(
|
||||
void* context, size_t offset, uint8_t* output, size_t size);
|
||||
|
||||
ControllerProfile controller_profile_default(const ControllerIdentity& identity,
|
||||
uint8_t profile_index);
|
||||
bool controller_profile_validate(const ControllerProfile& profile);
|
||||
bool controller_profile_encode(const ControllerProfile& profile,
|
||||
uint8_t* output, size_t output_size);
|
||||
bool controller_profile_decode(const uint8_t* input, size_t input_size,
|
||||
ControllerProfile* output);
|
||||
|
||||
void controller_profile_database_default(ControllerProfileDatabase* database);
|
||||
bool controller_profile_database_validate(
|
||||
const ControllerProfileDatabase& database);
|
||||
bool controller_profile_database_encode_range(
|
||||
const ControllerProfileDatabase& database, size_t offset,
|
||||
uint8_t* output, size_t size);
|
||||
bool controller_profile_database_decode(
|
||||
ControllerProfileDatabaseRead read, void* context,
|
||||
ControllerProfileDatabase* output);
|
||||
|
||||
const ControllerProfileDatabaseEntry* controller_profile_database_find(
|
||||
const ControllerProfileDatabase& database,
|
||||
const ControllerIdentity& identity);
|
||||
ControllerProfileDatabaseEntry* controller_profile_database_find(
|
||||
ControllerProfileDatabase* database,
|
||||
const ControllerIdentity& identity);
|
||||
ControllerProfileDatabaseResult controller_profile_database_ensure(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
ControllerProfileDatabaseEntry** output);
|
||||
const ControllerProfile* controller_profile_database_get(
|
||||
const ControllerProfileDatabase& database,
|
||||
const ControllerIdentity& identity, uint8_t profile_index);
|
||||
ControllerProfileDatabaseResult controller_profile_database_set(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
uint8_t profile_index, const ControllerProfile& profile);
|
||||
ControllerProfileDatabaseResult controller_profile_database_reset(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
uint8_t profile_index);
|
||||
ControllerProfileDatabaseResult controller_profile_database_activate(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
uint8_t profile_index);
|
||||
|
|
@ -1,362 +0,0 @@
|
|||
#include "controller_profile_runtime.h"
|
||||
|
||||
#include "configuration_service.h"
|
||||
#include "controller_identity.h"
|
||||
#include "controller_synthetic_input.h"
|
||||
#include "profile_service.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct ControllerProfileRuntimeContext {
|
||||
bool active = false;
|
||||
uint32_t connection_generation = 0;
|
||||
ControllerIdentity identity{};
|
||||
uint32_t database_generation = 0;
|
||||
uint8_t active_profile_index = 0;
|
||||
bool profile_snapshot_valid = false;
|
||||
ControllerProfile profile{};
|
||||
ControllerSyntheticInputContext synthetic{};
|
||||
bool runtime_generations_initialized = false;
|
||||
AdapterUsbMode output_mode = AdapterUsbMode::kSwitchProbe;
|
||||
uint32_t configuration_reset_generation = 0;
|
||||
bool switching_chord_held = false;
|
||||
bool switching_chord_armed = true;
|
||||
bool switching_activation_requested = false;
|
||||
uint16_t held_switching_chord = 0;
|
||||
uint8_t switching_target_profile_index = 0;
|
||||
uint32_t switching_transaction_id = 0;
|
||||
bool profile_change_pending = false;
|
||||
ControllerProfileRuntimeProfileChangeEvent pending_profile_change{};
|
||||
bool initial_profile_indication_resolved = false;
|
||||
bool initial_profile_indication_pending = false;
|
||||
ControllerProfileRuntimeProfileChangeEvent
|
||||
pending_initial_profile_indication{};
|
||||
};
|
||||
|
||||
ControllerProfileRuntimeContext
|
||||
g_contexts[CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT]{};
|
||||
ControllerProfile g_default_profile{};
|
||||
ControllerProfileTransformResult g_neutral_output{};
|
||||
bool g_initialized = false;
|
||||
uint32_t g_next_activation_sequence = 1;
|
||||
|
||||
uint32_t next_activation_transaction_id() {
|
||||
const uint32_t transaction_id =
|
||||
0x80000000u | g_next_activation_sequence;
|
||||
g_next_activation_sequence =
|
||||
g_next_activation_sequence == 0x7fffffffu
|
||||
? 1u
|
||||
: g_next_activation_sequence + 1u;
|
||||
return transaction_id;
|
||||
}
|
||||
|
||||
uint16_t effective_switching_chord(const ControllerProfile& profile) {
|
||||
return profile.switching_chord == 0
|
||||
? CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD
|
||||
: profile.switching_chord;
|
||||
}
|
||||
|
||||
void initialize_defaults() {
|
||||
if (g_initialized) {
|
||||
return;
|
||||
}
|
||||
g_default_profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
g_neutral_output = controller_profile_transform(
|
||||
controller_neutral_state(), g_default_profile);
|
||||
g_initialized = true;
|
||||
}
|
||||
|
||||
void clear_context(ControllerProfileRuntimeContext* context) {
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
*context = {};
|
||||
}
|
||||
|
||||
void refresh_profile(ControllerProfileRuntimeContext* context,
|
||||
const ControllerIdentity& identity,
|
||||
uint32_t connection_generation,
|
||||
uint32_t observed_database_generation,
|
||||
uint16_t current_input_button_mask) {
|
||||
ProfileServiceActiveProfileSnapshot snapshot{};
|
||||
profile_service_active_profile_snapshot(identity, &snapshot);
|
||||
|
||||
const bool same_connection =
|
||||
context->active &&
|
||||
context->connection_generation == connection_generation;
|
||||
const bool same_identity =
|
||||
same_connection &&
|
||||
controller_identity_equal(context->identity, identity);
|
||||
const uint8_t previous_profile_index =
|
||||
context->active_profile_index;
|
||||
const bool previous_profile_valid =
|
||||
context->profile_snapshot_valid;
|
||||
if (!same_connection) {
|
||||
context->runtime_generations_initialized = false;
|
||||
context->switching_chord_held = false;
|
||||
context->switching_chord_armed = true;
|
||||
context->switching_activation_requested = false;
|
||||
context->held_switching_chord = 0;
|
||||
context->switching_target_profile_index = 0;
|
||||
context->switching_transaction_id = 0;
|
||||
context->profile_change_pending = false;
|
||||
context->pending_profile_change = {};
|
||||
context->initial_profile_indication_resolved = false;
|
||||
context->initial_profile_indication_pending = false;
|
||||
context->pending_initial_profile_indication = {};
|
||||
}
|
||||
|
||||
controller_synthetic_input_cancel(&context->synthetic,
|
||||
current_input_button_mask);
|
||||
context->active = true;
|
||||
context->connection_generation = connection_generation;
|
||||
context->identity = identity;
|
||||
context->database_generation = snapshot.valid
|
||||
? snapshot.metadata.generation
|
||||
: observed_database_generation;
|
||||
context->active_profile_index = snapshot.valid ? snapshot.profile_index : 0;
|
||||
context->profile_snapshot_valid = snapshot.valid;
|
||||
context->profile = snapshot.valid ? snapshot.profile : g_default_profile;
|
||||
if (!context->initial_profile_indication_resolved && snapshot.valid) {
|
||||
context->initial_profile_indication_resolved = true;
|
||||
const uint8_t policy = static_cast<uint8_t>(
|
||||
controller_profile_confirmation_policy(context->profile));
|
||||
if ((policy & static_cast<uint8_t>(
|
||||
ControllerProfileConfirmationPolicy::kLed)) != 0) {
|
||||
context->pending_initial_profile_indication = {
|
||||
connection_generation,
|
||||
context->database_generation,
|
||||
static_cast<uint8_t>(context->active_profile_index + 1u),
|
||||
ControllerProfileConfirmationPolicy::kLed,
|
||||
};
|
||||
context->initial_profile_indication_pending = true;
|
||||
}
|
||||
}
|
||||
if (context->switching_chord_held &&
|
||||
!context->switching_activation_requested) {
|
||||
context->switching_target_profile_index =
|
||||
static_cast<uint8_t>(
|
||||
(context->active_profile_index + 1u) %
|
||||
CONTROLLER_PROFILE_COUNT);
|
||||
}
|
||||
if (same_identity && previous_profile_valid && snapshot.valid &&
|
||||
previous_profile_index != context->active_profile_index) {
|
||||
context->pending_profile_change = {
|
||||
connection_generation,
|
||||
context->database_generation,
|
||||
static_cast<uint8_t>(context->active_profile_index + 1u),
|
||||
controller_profile_confirmation_policy(context->profile),
|
||||
};
|
||||
context->profile_change_pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
ControllerProfileRuntimeContext* update_context(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot) {
|
||||
if (slot >= CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ControllerProfileRuntimeContext& context = g_contexts[slot];
|
||||
if (!snapshot.active) {
|
||||
if (context.active) {
|
||||
clear_context(&context);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const uint32_t database_generation =
|
||||
profile_service_database_generation();
|
||||
if (!context.active ||
|
||||
context.connection_generation != snapshot.connection_generation ||
|
||||
!controller_identity_equal(context.identity, snapshot.identity) ||
|
||||
context.database_generation != database_generation) {
|
||||
refresh_profile(
|
||||
&context, snapshot.identity, snapshot.connection_generation,
|
||||
database_generation,
|
||||
controller_profile_extract_button_mask(snapshot.state));
|
||||
}
|
||||
return &context;
|
||||
}
|
||||
void process_profile_switching(
|
||||
ControllerProfileRuntimeContext* context,
|
||||
const ControllerIdentity& identity,
|
||||
uint16_t switching_input_button_mask,
|
||||
uint16_t current_input_button_mask,
|
||||
ControllerState* consumed_input) {
|
||||
if (context == nullptr || consumed_input == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context->switching_chord_held) {
|
||||
if ((switching_input_button_mask &
|
||||
context->held_switching_chord) ==
|
||||
context->held_switching_chord) {
|
||||
controller_profile_apply_button_mask(
|
||||
static_cast<uint16_t>(
|
||||
current_input_button_mask &
|
||||
~context->held_switching_chord),
|
||||
consumed_input);
|
||||
if (!context->switching_activation_requested) {
|
||||
const ConfigurationTransactionStatus status =
|
||||
profile_service_activate_internal(
|
||||
context->switching_transaction_id, identity,
|
||||
context->switching_target_profile_index);
|
||||
if (status != ConfigurationTransactionStatus::kBusy) {
|
||||
context->switching_activation_requested = true;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
context->switching_chord_held = false;
|
||||
context->switching_activation_requested = false;
|
||||
context->held_switching_chord = 0;
|
||||
context->switching_transaction_id = 0;
|
||||
}
|
||||
|
||||
const uint16_t chord =
|
||||
effective_switching_chord(context->profile);
|
||||
const bool chord_fully_held =
|
||||
(switching_input_button_mask & chord) == chord;
|
||||
if (!chord_fully_held) {
|
||||
context->switching_chord_armed = true;
|
||||
return;
|
||||
}
|
||||
if (!context->switching_chord_armed) {
|
||||
return;
|
||||
}
|
||||
|
||||
context->switching_chord_armed = false;
|
||||
context->switching_chord_held = true;
|
||||
context->held_switching_chord = chord;
|
||||
context->switching_target_profile_index =
|
||||
static_cast<uint8_t>(
|
||||
(context->active_profile_index + 1u) %
|
||||
CONTROLLER_PROFILE_COUNT);
|
||||
context->switching_transaction_id =
|
||||
next_activation_transaction_id();
|
||||
controller_profile_apply_button_mask(
|
||||
static_cast<uint16_t>(
|
||||
current_input_button_mask & ~context->held_switching_chord),
|
||||
consumed_input);
|
||||
const ConfigurationTransactionStatus status =
|
||||
profile_service_activate_internal(
|
||||
context->switching_transaction_id, identity,
|
||||
context->switching_target_profile_index);
|
||||
if (status != ConfigurationTransactionStatus::kBusy) {
|
||||
context->switching_activation_requested = true;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void controller_profile_runtime_reset() {
|
||||
g_initialized = false;
|
||||
initialize_defaults();
|
||||
for (ControllerProfileRuntimeContext& context : g_contexts) {
|
||||
clear_context(&context);
|
||||
}
|
||||
g_next_activation_sequence = 1;
|
||||
}
|
||||
|
||||
ControllerProfileTransformResult controller_profile_runtime_transform(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot, uint32_t now_ms,
|
||||
AdapterUsbMode output_mode) {
|
||||
initialize_defaults();
|
||||
ControllerProfileRuntimeContext* context =
|
||||
update_context(slot, snapshot);
|
||||
if (context == nullptr) {
|
||||
return g_neutral_output;
|
||||
}
|
||||
ControllerState consumed_input = snapshot.state;
|
||||
const uint16_t current_input_button_mask =
|
||||
controller_profile_extract_button_mask(snapshot.state);
|
||||
process_profile_switching(
|
||||
context, snapshot.identity, snapshot.pre_hotkey_button_mask,
|
||||
current_input_button_mask, &consumed_input);
|
||||
|
||||
const uint32_t reset_generation =
|
||||
configuration_service_reset_generation();
|
||||
if (!context->runtime_generations_initialized) {
|
||||
context->runtime_generations_initialized = true;
|
||||
context->output_mode = output_mode;
|
||||
context->configuration_reset_generation = reset_generation;
|
||||
} else if (context->output_mode != output_mode ||
|
||||
context->configuration_reset_generation !=
|
||||
reset_generation) {
|
||||
controller_synthetic_input_cancel(
|
||||
&context->synthetic,
|
||||
controller_profile_extract_button_mask(consumed_input));
|
||||
context->output_mode = output_mode;
|
||||
context->configuration_reset_generation = reset_generation;
|
||||
}
|
||||
return controller_synthetic_input_apply(
|
||||
&context->synthetic, consumed_input, context->profile, now_ms);
|
||||
}
|
||||
|
||||
bool controller_profile_runtime_take_initial_profile_indication(
|
||||
uint8_t slot, ControllerProfileRuntimeProfileChangeEvent* output) {
|
||||
if (output == nullptr) {
|
||||
return false;
|
||||
}
|
||||
*output = {};
|
||||
if (slot >= CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ControllerProfileRuntimeContext& context = g_contexts[slot];
|
||||
if (!context.initial_profile_indication_pending) {
|
||||
return false;
|
||||
}
|
||||
*output = context.pending_initial_profile_indication;
|
||||
context.initial_profile_indication_pending = false;
|
||||
context.pending_initial_profile_indication = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
bool controller_profile_runtime_take_profile_change(
|
||||
uint8_t slot, ControllerProfileRuntimeProfileChangeEvent* output) {
|
||||
if (output == nullptr) {
|
||||
return false;
|
||||
}
|
||||
*output = {};
|
||||
if (slot >= CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ControllerProfileRuntimeContext& context = g_contexts[slot];
|
||||
if (!context.profile_change_pending) {
|
||||
return false;
|
||||
}
|
||||
*output = context.pending_profile_change;
|
||||
context.profile_change_pending = false;
|
||||
context.pending_profile_change = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
ControllerRumbleOutput controller_profile_runtime_scale_host_rumble(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||
const ControllerRumbleOutput& rumble) {
|
||||
initialize_defaults();
|
||||
ControllerProfileRuntimeContext* context =
|
||||
update_context(slot, snapshot);
|
||||
const ControllerProfile& profile =
|
||||
context == nullptr ? g_default_profile : context->profile;
|
||||
return controller_profile_scale_host_rumble(rumble, profile);
|
||||
}
|
||||
|
||||
ControllerProfileRuntimeLocalConfirmation
|
||||
controller_profile_runtime_local_confirmation(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||
const ControllerRumbleOutput& rumble) {
|
||||
initialize_defaults();
|
||||
ControllerProfileRuntimeContext* context =
|
||||
update_context(slot, snapshot);
|
||||
const ControllerProfile& profile =
|
||||
context == nullptr ? g_default_profile : context->profile;
|
||||
return {
|
||||
rumble,
|
||||
controller_profile_confirmation_policy(profile),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "adapter_usb_mode.h"
|
||||
|
||||
#include "bluepad32_input_backend.h"
|
||||
#include "controller_profile_transform.h"
|
||||
|
||||
constexpr uint8_t CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT = 4;
|
||||
constexpr uint16_t CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD =
|
||||
static_cast<uint16_t>(
|
||||
(1u << static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kLeftShoulder)) |
|
||||
(1u << static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kRightShoulder)) |
|
||||
(1u << static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kSelect)) |
|
||||
(1u << static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kStart)));
|
||||
|
||||
struct ControllerProfileRuntimeProfileChangeEvent {
|
||||
uint32_t connection_generation = 0;
|
||||
uint32_t database_generation = 0;
|
||||
uint8_t active_profile_number = 0;
|
||||
ControllerProfileConfirmationPolicy policy =
|
||||
ControllerProfileConfirmationPolicy::kNone;
|
||||
};
|
||||
|
||||
// Take the one-shot LED-only indication for the first committed profile
|
||||
// resolved on a connection. Identity promotion and database refresh do not
|
||||
// publish it again.
|
||||
bool controller_profile_runtime_take_initial_profile_indication(
|
||||
uint8_t slot, ControllerProfileRuntimeProfileChangeEvent* output);
|
||||
|
||||
struct ControllerProfileRuntimeLocalConfirmation {
|
||||
ControllerRumbleOutput rumble{};
|
||||
ControllerProfileConfirmationPolicy policy =
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed;
|
||||
};
|
||||
|
||||
// Reset all four fixed slot caches to the default profile.
|
||||
void controller_profile_runtime_reset();
|
||||
|
||||
// Refresh a slot when its identity, connection generation, or database
|
||||
// generation changes; identity-only promotion preserves a held switching
|
||||
// transaction. Consume pre-hotkey switching chords while transforming the
|
||||
// backend-suppressed state through the shared synthetic pipeline. Inactive
|
||||
// snapshots return neutral output and invalidate the slot.
|
||||
ControllerProfileTransformResult controller_profile_runtime_transform(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot, uint32_t now_ms,
|
||||
AdapterUsbMode output_mode);
|
||||
|
||||
// Take the single committed profile-index change observed by the slot. Initial
|
||||
// profile loads, identity promotions, and connection replacements do not
|
||||
// publish an event.
|
||||
bool controller_profile_runtime_take_profile_change(
|
||||
uint8_t slot, ControllerProfileRuntimeProfileChangeEvent* output);
|
||||
|
||||
// Refresh from the current slot snapshot and scale host-originated rumble.
|
||||
ControllerRumbleOutput controller_profile_runtime_scale_host_rumble(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||
const ControllerRumbleOutput& rumble);
|
||||
|
||||
// Preserve local confirmation rumble exactly while exposing profile policy.
|
||||
ControllerProfileRuntimeLocalConfirmation
|
||||
controller_profile_runtime_local_confirmation(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||
const ControllerRumbleOutput& rumble);
|
||||
|
|
@ -1,344 +0,0 @@
|
|||
#include "controller_profile_transform.h"
|
||||
|
||||
#include <limits.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kQ16One = 1u << 16;
|
||||
|
||||
constexpr uint16_t button_bit(ControllerProfileLogicalButton button) {
|
||||
return static_cast<uint16_t>(
|
||||
1u << static_cast<uint8_t>(button));
|
||||
}
|
||||
|
||||
int32_t clamp_centered_axis(int16_t value, int16_t center) {
|
||||
const int32_t adjusted =
|
||||
static_cast<int32_t>(value) - static_cast<int32_t>(center);
|
||||
if (adjusted < INT16_MIN) {
|
||||
return INT16_MIN;
|
||||
}
|
||||
if (adjusted > INT16_MAX) {
|
||||
return INT16_MAX;
|
||||
}
|
||||
return adjusted;
|
||||
}
|
||||
|
||||
uint32_t absolute_axis(int32_t value) {
|
||||
return value < 0 ? static_cast<uint32_t>(-value)
|
||||
: static_cast<uint32_t>(value);
|
||||
}
|
||||
|
||||
// This endpoint-preserving rational curve is x / (c + (1 - c) * x),
|
||||
// evaluated with x in Q16 and c in Q8.8. Values above 1.0 reduce the
|
||||
// response below the linear curve; values below 1.0 increase it.
|
||||
uint32_t apply_curve_q16(uint32_t input_q16, uint16_t curve_q8_8) {
|
||||
if (input_q16 == 0 || input_q16 == kQ16One || curve_q8_8 == 256) {
|
||||
return input_q16;
|
||||
}
|
||||
|
||||
const uint64_t denominator =
|
||||
static_cast<uint64_t>(curve_q8_8) *
|
||||
(kQ16One - input_q16) +
|
||||
static_cast<uint64_t>(256) * input_q16;
|
||||
const uint64_t numerator =
|
||||
static_cast<uint64_t>(input_q16) * 256u * kQ16One;
|
||||
const uint64_t curved = (numerator + denominator / 2u) / denominator;
|
||||
return curved > kQ16One ? kQ16One
|
||||
: static_cast<uint32_t>(curved);
|
||||
}
|
||||
|
||||
bool is_default_stick_configuration(
|
||||
const ControllerProfileStickConfiguration& configuration) {
|
||||
return configuration.center_x == 0 && configuration.center_y == 0 &&
|
||||
configuration.inner_deadzone == 0 &&
|
||||
configuration.outer_saturation == 32767 &&
|
||||
configuration.curve_q8_8 == 256 && !configuration.invert_x &&
|
||||
!configuration.invert_y;
|
||||
}
|
||||
|
||||
bool is_default_trigger_configuration(
|
||||
const ControllerProfileTriggerConfiguration& configuration) {
|
||||
return configuration.lower_deadzone == 0 &&
|
||||
configuration.upper_saturation == UINT16_MAX &&
|
||||
configuration.curve_q8_8 == 256;
|
||||
}
|
||||
|
||||
int16_t scale_stick_axis(int32_t adjusted_axis, uint32_t magnitude,
|
||||
uint32_t response_q16, bool invert) {
|
||||
if (adjusted_axis == 0 || magnitude == 0 || response_q16 == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool negative = adjusted_axis < 0;
|
||||
if (invert) {
|
||||
negative = !negative;
|
||||
}
|
||||
const uint32_t axis_magnitude = absolute_axis(adjusted_axis);
|
||||
const uint32_t output_limit =
|
||||
negative ? static_cast<uint32_t>(-static_cast<int32_t>(INT16_MIN))
|
||||
: static_cast<uint32_t>(INT16_MAX);
|
||||
const uint64_t numerator =
|
||||
static_cast<uint64_t>(axis_magnitude) * output_limit *
|
||||
response_q16;
|
||||
const uint64_t denominator =
|
||||
static_cast<uint64_t>(magnitude) * kQ16One;
|
||||
uint32_t output_magnitude = static_cast<uint32_t>(
|
||||
(numerator + denominator / 2u) / denominator);
|
||||
if (output_magnitude > output_limit) {
|
||||
output_magnitude = output_limit;
|
||||
}
|
||||
|
||||
if (!negative) {
|
||||
return static_cast<int16_t>(output_magnitude);
|
||||
}
|
||||
if (output_magnitude ==
|
||||
static_cast<uint32_t>(-static_cast<int32_t>(INT16_MIN))) {
|
||||
return INT16_MIN;
|
||||
}
|
||||
return static_cast<int16_t>(-static_cast<int32_t>(output_magnitude));
|
||||
}
|
||||
|
||||
void transform_stick(const ControllerProfileStickConfiguration& configuration,
|
||||
int16_t input_x, int16_t input_y, int16_t* output_x,
|
||||
int16_t* output_y) {
|
||||
if (is_default_stick_configuration(configuration)) {
|
||||
*output_x = input_x;
|
||||
*output_y = input_y;
|
||||
return;
|
||||
}
|
||||
|
||||
const int32_t adjusted_x =
|
||||
clamp_centered_axis(input_x, configuration.center_x);
|
||||
const int32_t adjusted_y =
|
||||
clamp_centered_axis(input_y, configuration.center_y);
|
||||
const uint32_t magnitude_x = absolute_axis(adjusted_x);
|
||||
const uint32_t magnitude_y = absolute_axis(adjusted_y);
|
||||
const uint32_t magnitude =
|
||||
magnitude_x > magnitude_y ? magnitude_x : magnitude_y;
|
||||
|
||||
uint32_t response_q16 = 0;
|
||||
if (magnitude <= configuration.inner_deadzone) {
|
||||
response_q16 = 0;
|
||||
} else if (magnitude >= configuration.outer_saturation) {
|
||||
response_q16 = kQ16One;
|
||||
} else {
|
||||
const uint32_t input_range =
|
||||
static_cast<uint32_t>(configuration.outer_saturation) -
|
||||
configuration.inner_deadzone;
|
||||
const uint32_t input_offset =
|
||||
magnitude - configuration.inner_deadzone;
|
||||
const uint32_t normalized_q16 = static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(input_offset) * kQ16One +
|
||||
input_range / 2u) /
|
||||
input_range);
|
||||
response_q16 =
|
||||
apply_curve_q16(normalized_q16, configuration.curve_q8_8);
|
||||
}
|
||||
|
||||
*output_x = scale_stick_axis(adjusted_x, magnitude, response_q16,
|
||||
configuration.invert_x);
|
||||
*output_y = scale_stick_axis(adjusted_y, magnitude, response_q16,
|
||||
configuration.invert_y);
|
||||
}
|
||||
|
||||
uint16_t transform_trigger(
|
||||
uint16_t input,
|
||||
const ControllerProfileTriggerConfiguration& configuration) {
|
||||
if (is_default_trigger_configuration(configuration)) {
|
||||
return input;
|
||||
}
|
||||
if (input <= configuration.lower_deadzone) {
|
||||
return 0;
|
||||
}
|
||||
if (input >= configuration.upper_saturation) {
|
||||
return UINT16_MAX;
|
||||
}
|
||||
|
||||
const uint32_t input_range =
|
||||
static_cast<uint32_t>(configuration.upper_saturation) -
|
||||
configuration.lower_deadzone;
|
||||
const uint32_t input_offset =
|
||||
static_cast<uint32_t>(input) - configuration.lower_deadzone;
|
||||
if (configuration.curve_q8_8 == 256) {
|
||||
return static_cast<uint16_t>(
|
||||
(static_cast<uint64_t>(input_offset) * UINT16_MAX +
|
||||
input_range / 2u) /
|
||||
input_range);
|
||||
}
|
||||
|
||||
const uint32_t normalized_q16 = static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(input_offset) * kQ16One +
|
||||
input_range / 2u) /
|
||||
input_range);
|
||||
const uint32_t curved_q16 =
|
||||
apply_curve_q16(normalized_q16, configuration.curve_q8_8);
|
||||
return static_cast<uint16_t>(
|
||||
(static_cast<uint64_t>(curved_q16) * UINT16_MAX +
|
||||
kQ16One / 2u) /
|
||||
kQ16One);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint16_t controller_profile_extract_button_mask(const ControllerState& state) {
|
||||
uint16_t mask = 0;
|
||||
mask |= state.button_south
|
||||
? button_bit(ControllerProfileLogicalButton::kSouth)
|
||||
: 0;
|
||||
mask |= state.button_east
|
||||
? button_bit(ControllerProfileLogicalButton::kEast)
|
||||
: 0;
|
||||
mask |= state.button_west
|
||||
? button_bit(ControllerProfileLogicalButton::kWest)
|
||||
: 0;
|
||||
mask |= state.button_north
|
||||
? button_bit(ControllerProfileLogicalButton::kNorth)
|
||||
: 0;
|
||||
mask |= state.button_left_shoulder
|
||||
? button_bit(ControllerProfileLogicalButton::kLeftShoulder)
|
||||
: 0;
|
||||
mask |= state.button_right_shoulder
|
||||
? button_bit(ControllerProfileLogicalButton::kRightShoulder)
|
||||
: 0;
|
||||
mask |= state.button_select
|
||||
? button_bit(ControllerProfileLogicalButton::kSelect)
|
||||
: 0;
|
||||
mask |= state.button_start
|
||||
? button_bit(ControllerProfileLogicalButton::kStart)
|
||||
: 0;
|
||||
mask |= state.button_system
|
||||
? button_bit(ControllerProfileLogicalButton::kSystem)
|
||||
: 0;
|
||||
mask |= state.button_capture
|
||||
? button_bit(ControllerProfileLogicalButton::kCapture)
|
||||
: 0;
|
||||
mask |= state.button_left_stick
|
||||
? button_bit(ControllerProfileLogicalButton::kLeftStick)
|
||||
: 0;
|
||||
mask |= state.button_right_stick
|
||||
? button_bit(ControllerProfileLogicalButton::kRightStick)
|
||||
: 0;
|
||||
mask |= state.dpad_up
|
||||
? button_bit(ControllerProfileLogicalButton::kDpadUp)
|
||||
: 0;
|
||||
mask |= state.dpad_down
|
||||
? button_bit(ControllerProfileLogicalButton::kDpadDown)
|
||||
: 0;
|
||||
mask |= state.dpad_left
|
||||
? button_bit(ControllerProfileLogicalButton::kDpadLeft)
|
||||
: 0;
|
||||
mask |= state.dpad_right
|
||||
? button_bit(ControllerProfileLogicalButton::kDpadRight)
|
||||
: 0;
|
||||
return mask;
|
||||
}
|
||||
|
||||
void controller_profile_apply_button_mask(uint16_t button_mask,
|
||||
ControllerState* state) {
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
}
|
||||
state->button_south =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kSouth)) != 0;
|
||||
state->button_east =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kEast)) != 0;
|
||||
state->button_west =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kWest)) != 0;
|
||||
state->button_north =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kNorth)) != 0;
|
||||
state->button_left_shoulder =
|
||||
(button_mask &
|
||||
button_bit(ControllerProfileLogicalButton::kLeftShoulder)) != 0;
|
||||
state->button_right_shoulder =
|
||||
(button_mask &
|
||||
button_bit(ControllerProfileLogicalButton::kRightShoulder)) != 0;
|
||||
state->button_select =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kSelect)) != 0;
|
||||
state->button_start =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kStart)) != 0;
|
||||
state->button_system =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kSystem)) != 0;
|
||||
state->button_capture =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kCapture)) != 0;
|
||||
state->button_left_stick =
|
||||
(button_mask &
|
||||
button_bit(ControllerProfileLogicalButton::kLeftStick)) != 0;
|
||||
state->button_right_stick =
|
||||
(button_mask &
|
||||
button_bit(ControllerProfileLogicalButton::kRightStick)) != 0;
|
||||
state->dpad_up =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kDpadUp)) != 0;
|
||||
state->dpad_down =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kDpadDown)) != 0;
|
||||
state->dpad_left =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kDpadLeft)) != 0;
|
||||
state->dpad_right =
|
||||
(button_mask & button_bit(ControllerProfileLogicalButton::kDpadRight)) != 0;
|
||||
}
|
||||
|
||||
uint16_t controller_profile_map_button_mask(
|
||||
uint16_t input_button_mask, const ControllerProfile& profile) {
|
||||
uint16_t output_button_mask = 0;
|
||||
for (uint8_t input = 0;
|
||||
input < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++input) {
|
||||
if ((input_button_mask & static_cast<uint16_t>(1u << input)) == 0) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t output = profile.button_map[input];
|
||||
if (output < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT) {
|
||||
output_button_mask |= static_cast<uint16_t>(1u << output);
|
||||
}
|
||||
}
|
||||
return output_button_mask;
|
||||
}
|
||||
|
||||
ControllerProfileTransformResult controller_profile_transform(
|
||||
const ControllerState& input, const ControllerProfile& profile) {
|
||||
ControllerProfileTransformResult result{};
|
||||
result.state = input;
|
||||
const uint16_t input_button_mask =
|
||||
controller_profile_extract_button_mask(input);
|
||||
controller_profile_apply_button_mask(
|
||||
controller_profile_map_button_mask(input_button_mask, profile),
|
||||
&result.state);
|
||||
|
||||
transform_stick(profile.sticks[0], input.left_stick_x,
|
||||
input.left_stick_y, &result.state.left_stick_x,
|
||||
&result.state.left_stick_y);
|
||||
transform_stick(profile.sticks[1], input.right_stick_x,
|
||||
input.right_stick_y, &result.state.right_stick_x,
|
||||
&result.state.right_stick_y);
|
||||
result.state.left_trigger = transform_trigger(input.left_trigger,
|
||||
profile.triggers[0]);
|
||||
result.state.right_trigger = transform_trigger(input.right_trigger,
|
||||
profile.triggers[1]);
|
||||
result.left_trigger_digital_threshold =
|
||||
profile.triggers[0].digital_threshold;
|
||||
result.right_trigger_digital_threshold =
|
||||
profile.triggers[1].digital_threshold;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t controller_profile_scale_rumble_magnitude(uint8_t magnitude,
|
||||
uint8_t scale) {
|
||||
const uint32_t scaled =
|
||||
(static_cast<uint32_t>(magnitude) * scale + UINT8_MAX / 2u) /
|
||||
UINT8_MAX;
|
||||
return scaled > UINT8_MAX ? UINT8_MAX
|
||||
: static_cast<uint8_t>(scaled);
|
||||
}
|
||||
|
||||
ControllerRumbleOutput controller_profile_scale_host_rumble(
|
||||
const ControllerRumbleOutput& input, const ControllerProfile& profile) {
|
||||
return {
|
||||
controller_profile_scale_rumble_magnitude(
|
||||
input.low_frequency_magnitude, profile.strong_rumble_scale),
|
||||
controller_profile_scale_rumble_magnitude(
|
||||
input.high_frequency_magnitude, profile.weak_rumble_scale),
|
||||
};
|
||||
}
|
||||
|
||||
ControllerProfileConfirmationPolicy controller_profile_confirmation_policy(
|
||||
const ControllerProfile& profile) {
|
||||
return profile.confirmation_policy;
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_profile.h"
|
||||
#include "controller_state.h"
|
||||
#include "switch_haptics.h"
|
||||
|
||||
struct ControllerProfileTransformResult {
|
||||
ControllerState state{};
|
||||
uint16_t left_trigger_digital_threshold =
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
uint16_t right_trigger_digital_threshold =
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
};
|
||||
|
||||
uint16_t controller_profile_extract_button_mask(const ControllerState& state);
|
||||
void controller_profile_apply_button_mask(uint16_t button_mask,
|
||||
ControllerState* state);
|
||||
uint16_t controller_profile_map_button_mask(
|
||||
uint16_t input_button_mask, const ControllerProfile& profile);
|
||||
|
||||
ControllerProfileTransformResult controller_profile_transform(
|
||||
const ControllerState& input, const ControllerProfile& profile);
|
||||
|
||||
uint8_t controller_profile_scale_rumble_magnitude(uint8_t magnitude,
|
||||
uint8_t scale);
|
||||
ControllerRumbleOutput controller_profile_scale_host_rumble(
|
||||
const ControllerRumbleOutput& input, const ControllerProfile& profile);
|
||||
ControllerProfileConfirmationPolicy controller_profile_confirmation_policy(
|
||||
const ControllerProfile& profile);
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
#include "controller_synthetic_input.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kTurboTransitionsPerSecond = 30;
|
||||
constexpr uint32_t kTurboPhaseUnitsPerTransition = 1000;
|
||||
|
||||
constexpr uint16_t button_bit(uint8_t button) {
|
||||
return static_cast<uint16_t>(1u << button);
|
||||
}
|
||||
|
||||
bool is_bound_button(uint8_t button) {
|
||||
return button < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT;
|
||||
}
|
||||
|
||||
void clear_binding(ControllerSyntheticBindingState* binding) {
|
||||
*binding = {};
|
||||
}
|
||||
|
||||
void start_binding(ControllerSyntheticBindingState* binding,
|
||||
uint32_t now_ms) {
|
||||
binding->active = true;
|
||||
binding->phase_on = true;
|
||||
binding->phase_units = 0;
|
||||
binding->last_update_ms = now_ms;
|
||||
}
|
||||
|
||||
void advance_binding(ControllerSyntheticBindingState* binding,
|
||||
uint32_t now_ms) {
|
||||
const uint32_t elapsed_ms = now_ms - binding->last_update_ms;
|
||||
const uint64_t total_units =
|
||||
static_cast<uint64_t>(binding->phase_units) +
|
||||
static_cast<uint64_t>(elapsed_ms) * kTurboTransitionsPerSecond;
|
||||
const uint64_t transition_count =
|
||||
total_units / kTurboPhaseUnitsPerTransition;
|
||||
if ((transition_count & 1u) != 0) {
|
||||
binding->phase_on = !binding->phase_on;
|
||||
}
|
||||
binding->phase_units = static_cast<uint16_t>(
|
||||
total_units -
|
||||
transition_count * kTurboPhaseUnitsPerTransition);
|
||||
binding->last_update_ms = now_ms;
|
||||
}
|
||||
|
||||
bool deadline_reached(uint32_t now_ms, uint32_t deadline_ms) {
|
||||
return now_ms - deadline_ms < (UINT32_MAX / 2u + 1u);
|
||||
}
|
||||
|
||||
void stop_macro(ControllerSyntheticInputContext* context) {
|
||||
context->macro_active = false;
|
||||
context->macro_step_index = 0;
|
||||
context->macro_deadline_ms = 0;
|
||||
}
|
||||
|
||||
bool start_macro(ControllerSyntheticInputContext* context,
|
||||
const ControllerProfile& profile, uint32_t now_ms) {
|
||||
if (profile.macro_step_count == 0 ||
|
||||
profile.macro_steps[0].type !=
|
||||
ControllerProfileMacroStepType::kState) {
|
||||
stop_macro(context);
|
||||
return false;
|
||||
}
|
||||
context->macro_active = true;
|
||||
context->macro_step_index = 0;
|
||||
context->macro_deadline_ms =
|
||||
now_ms + profile.macro_steps[0].duration_ms;
|
||||
return true;
|
||||
}
|
||||
|
||||
void advance_macro(ControllerSyntheticInputContext* context,
|
||||
const ControllerProfile& profile, uint32_t now_ms) {
|
||||
for (uint8_t transition = 0;
|
||||
transition < CONTROLLER_PROFILE_MACRO_STEP_CAPACITY;
|
||||
++transition) {
|
||||
if (!context->macro_active ||
|
||||
context->macro_step_index >= profile.macro_step_count) {
|
||||
stop_macro(context);
|
||||
return;
|
||||
}
|
||||
const ControllerProfileMacroStep& current =
|
||||
profile.macro_steps[context->macro_step_index];
|
||||
if (current.type != ControllerProfileMacroStepType::kState) {
|
||||
stop_macro(context);
|
||||
return;
|
||||
}
|
||||
if (!deadline_reached(now_ms, context->macro_deadline_ms)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t next_index =
|
||||
static_cast<uint8_t>(context->macro_step_index + 1u);
|
||||
if (next_index >= profile.macro_step_count ||
|
||||
profile.macro_steps[next_index].type ==
|
||||
ControllerProfileMacroStepType::kEnd) {
|
||||
stop_macro(context);
|
||||
return;
|
||||
}
|
||||
context->macro_step_index = next_index;
|
||||
context->macro_deadline_ms +=
|
||||
profile.macro_steps[next_index].duration_ms;
|
||||
}
|
||||
}
|
||||
|
||||
void apply_macro_override(const ControllerProfileMacroStep& step,
|
||||
ControllerState* output) {
|
||||
if ((step.override_flags & kControllerProfileOverrideButtons) != 0) {
|
||||
controller_profile_apply_button_mask(step.output_button_mask,
|
||||
output);
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideLeftStick) != 0) {
|
||||
output->left_stick_x = step.left_stick_x;
|
||||
output->left_stick_y = step.left_stick_y;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideRightStick) != 0) {
|
||||
output->right_stick_x = step.right_stick_x;
|
||||
output->right_stick_y = step.right_stick_y;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideLeftTrigger) != 0) {
|
||||
output->left_trigger = step.left_trigger;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideRightTrigger) != 0) {
|
||||
output->right_trigger = step.right_trigger;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void controller_synthetic_input_cancel(
|
||||
ControllerSyntheticInputContext* context,
|
||||
uint16_t current_input_button_mask) {
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
*context = {};
|
||||
context->previous_input_button_mask = current_input_button_mask;
|
||||
}
|
||||
|
||||
ControllerProfileTransformResult controller_synthetic_input_apply(
|
||||
ControllerSyntheticInputContext* context, const ControllerState& input,
|
||||
const ControllerProfile& profile, uint32_t now_ms) {
|
||||
if (context == nullptr) {
|
||||
return controller_profile_transform(input, profile);
|
||||
}
|
||||
|
||||
const uint16_t input_button_mask =
|
||||
controller_profile_extract_button_mask(input);
|
||||
uint16_t rising_button_mask = static_cast<uint16_t>(
|
||||
input_button_mask & ~context->previous_input_button_mask);
|
||||
const bool cancel_pressed =
|
||||
is_bound_button(profile.macro_cancel) &&
|
||||
(input_button_mask & button_bit(profile.macro_cancel)) != 0;
|
||||
if (cancel_pressed) {
|
||||
controller_synthetic_input_cancel(context, input_button_mask);
|
||||
rising_button_mask = 0;
|
||||
}
|
||||
|
||||
bool macro_started = false;
|
||||
if (!cancel_pressed && is_bound_button(profile.macro_trigger) &&
|
||||
(rising_button_mask & button_bit(profile.macro_trigger)) != 0) {
|
||||
macro_started = start_macro(context, profile, now_ms);
|
||||
}
|
||||
if (!macro_started) {
|
||||
advance_macro(context, profile, now_ms);
|
||||
}
|
||||
|
||||
uint16_t gated_input_button_mask = 0;
|
||||
for (uint8_t input_button = 0;
|
||||
input_button < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT;
|
||||
++input_button) {
|
||||
ControllerSyntheticBindingState& binding =
|
||||
context->bindings[input_button];
|
||||
if (input_button == profile.macro_trigger ||
|
||||
input_button == profile.macro_cancel) {
|
||||
clear_binding(&binding);
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint16_t bit = button_bit(input_button);
|
||||
const bool pressed = (input_button_mask & bit) != 0;
|
||||
const bool rising = (rising_button_mask & bit) != 0;
|
||||
switch (profile.turbo_modes[input_button]) {
|
||||
case ControllerProfileTurboMode::kOff:
|
||||
clear_binding(&binding);
|
||||
if (pressed) {
|
||||
gated_input_button_mask |= bit;
|
||||
}
|
||||
break;
|
||||
case ControllerProfileTurboMode::kTurbo:
|
||||
if (!pressed) {
|
||||
clear_binding(&binding);
|
||||
break;
|
||||
}
|
||||
if (!binding.active) {
|
||||
start_binding(&binding, now_ms);
|
||||
} else {
|
||||
advance_binding(&binding, now_ms);
|
||||
}
|
||||
if (binding.phase_on) {
|
||||
gated_input_button_mask |= bit;
|
||||
}
|
||||
break;
|
||||
case ControllerProfileTurboMode::kAutoBurst:
|
||||
if (rising) {
|
||||
if (binding.active) {
|
||||
clear_binding(&binding);
|
||||
} else {
|
||||
start_binding(&binding, now_ms);
|
||||
}
|
||||
} else if (binding.active) {
|
||||
advance_binding(&binding, now_ms);
|
||||
}
|
||||
if (binding.active && binding.phase_on) {
|
||||
gated_input_button_mask |= bit;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ControllerState gated_input = input;
|
||||
controller_profile_apply_button_mask(gated_input_button_mask,
|
||||
&gated_input);
|
||||
ControllerProfileTransformResult result =
|
||||
controller_profile_transform(gated_input, profile);
|
||||
if (context->macro_active &&
|
||||
context->macro_step_index < profile.macro_step_count) {
|
||||
const ControllerProfileMacroStep& step =
|
||||
profile.macro_steps[context->macro_step_index];
|
||||
if (step.type == ControllerProfileMacroStepType::kState) {
|
||||
apply_macro_override(step, &result.state);
|
||||
} else {
|
||||
stop_macro(context);
|
||||
}
|
||||
}
|
||||
|
||||
context->previous_input_button_mask = input_button_mask;
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_profile_transform.h"
|
||||
|
||||
struct ControllerSyntheticBindingState {
|
||||
bool active = false;
|
||||
bool phase_on = false;
|
||||
uint16_t phase_units = 0;
|
||||
uint32_t last_update_ms = 0;
|
||||
};
|
||||
|
||||
struct ControllerSyntheticInputContext {
|
||||
bool macro_active = false;
|
||||
uint8_t macro_step_index = 0;
|
||||
uint32_t macro_deadline_ms = 0;
|
||||
uint16_t previous_input_button_mask = 0;
|
||||
ControllerSyntheticBindingState
|
||||
bindings[CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT]{};
|
||||
};
|
||||
|
||||
// Clear every synthetic source. Inputs already held at cancellation remain
|
||||
// consumed or physical, but edge-triggered Macro and Auto Burst bindings do not
|
||||
// restart until they are released and pressed again.
|
||||
void controller_synthetic_input_cancel(
|
||||
ControllerSyntheticInputContext* context,
|
||||
uint16_t current_input_button_mask = 0);
|
||||
|
||||
// Apply raw-input consumption, mapped physical contributions, Turbo/Auto Burst
|
||||
// gating, and finally the active macro step's field overrides.
|
||||
ControllerProfileTransformResult controller_synthetic_input_apply(
|
||||
ControllerSyntheticInputContext* context, const ControllerState& input,
|
||||
const ControllerProfile& profile, uint32_t now_ms);
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,150 +0,0 @@
|
|||
#include "pico_profile_storage.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "configuration_storage.h"
|
||||
#include "hardware/flash.h"
|
||||
#include "pico/btstack_flash_bank.h"
|
||||
#include "pico/flash.h"
|
||||
#include "pico/platform.h"
|
||||
|
||||
extern "C" char __flash_binary_end;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t kConfigurationStorageSize =
|
||||
CONFIGURATION_STORAGE_COPY_COUNT * FLASH_SECTOR_SIZE;
|
||||
constexpr uint32_t kConfigurationStorageOffset =
|
||||
PICO_FLASH_BANK_STORAGE_OFFSET - kConfigurationStorageSize;
|
||||
constexpr uint32_t kProfileStorageOffset =
|
||||
kConfigurationStorageOffset - PROFILE_STORAGE_TOTAL_SIZE;
|
||||
|
||||
static_assert(FLASH_SECTOR_SIZE == PROFILE_STORAGE_SECTOR_SIZE,
|
||||
"profile storage sector size does not match Pico flash");
|
||||
static_assert(FLASH_PAGE_SIZE == PROFILE_STORAGE_PAGE_SIZE,
|
||||
"profile storage page size does not match Pico flash");
|
||||
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET >=
|
||||
kConfigurationStorageSize + PROFILE_STORAGE_TOTAL_SIZE,
|
||||
"profile storage offset underflows flash");
|
||||
static_assert(kProfileStorageOffset + PROFILE_STORAGE_TOTAL_SIZE <=
|
||||
kConfigurationStorageOffset,
|
||||
"profile storage overlaps adapter configuration storage");
|
||||
static_assert(kConfigurationStorageOffset + kConfigurationStorageSize <=
|
||||
PICO_FLASH_BANK_STORAGE_OFFSET,
|
||||
"adapter configuration storage overlaps BTstack bonds");
|
||||
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET +
|
||||
PICO_FLASH_BANK_TOTAL_SIZE <=
|
||||
PICO_FLASH_SIZE_BYTES,
|
||||
"BTstack storage exceeds flash");
|
||||
|
||||
struct FlashBankReplacement {
|
||||
uint8_t bank;
|
||||
const uint8_t* payload;
|
||||
size_t payload_size;
|
||||
const uint8_t* header;
|
||||
bool replaced;
|
||||
};
|
||||
|
||||
void perform_flash_bank_replacement(void* context) {
|
||||
auto* replacement =
|
||||
static_cast<FlashBankReplacement*>(context);
|
||||
replacement->replaced = false;
|
||||
const uint32_t bank_offset =
|
||||
kProfileStorageOffset +
|
||||
replacement->bank * PROFILE_STORAGE_BANK_SIZE;
|
||||
|
||||
for (size_t offset = 0; offset < PROFILE_STORAGE_BANK_SIZE;
|
||||
offset += FLASH_SECTOR_SIZE) {
|
||||
flash_range_erase(bank_offset + offset, FLASH_SECTOR_SIZE);
|
||||
}
|
||||
|
||||
uint8_t final_page[FLASH_PAGE_SIZE]{};
|
||||
for (size_t offset = 0; offset < replacement->payload_size;
|
||||
offset += FLASH_PAGE_SIZE) {
|
||||
const size_t remaining =
|
||||
replacement->payload_size - offset;
|
||||
const uint8_t* page = &replacement->payload[offset];
|
||||
if (remaining < FLASH_PAGE_SIZE) {
|
||||
memcpy(final_page, page, remaining);
|
||||
page = final_page;
|
||||
}
|
||||
flash_range_program(
|
||||
bank_offset + PROFILE_STORAGE_RECORD_HEADER_SIZE + offset,
|
||||
page, FLASH_PAGE_SIZE);
|
||||
}
|
||||
|
||||
const auto* stored_payload = reinterpret_cast<const uint8_t*>(
|
||||
XIP_BASE + bank_offset +
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE);
|
||||
if (memcmp(stored_payload, replacement->payload,
|
||||
replacement->payload_size) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
flash_range_program(bank_offset, replacement->header,
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE);
|
||||
const auto* stored_header = reinterpret_cast<const uint8_t*>(
|
||||
XIP_BASE + bank_offset);
|
||||
replacement->replaced =
|
||||
memcmp(stored_header, replacement->header,
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE) == 0;
|
||||
}
|
||||
|
||||
bool storage_region_available() {
|
||||
const uintptr_t binary_end =
|
||||
reinterpret_cast<uintptr_t>(&__flash_binary_end) - XIP_BASE;
|
||||
return binary_end <= kProfileStorageOffset;
|
||||
}
|
||||
|
||||
bool read_storage(void*, uint8_t bank, size_t offset, uint8_t* output,
|
||||
size_t size) {
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || output == nullptr ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
size > PROFILE_STORAGE_BANK_SIZE - offset ||
|
||||
!storage_region_available()) {
|
||||
return false;
|
||||
}
|
||||
const uintptr_t address =
|
||||
XIP_BASE + kProfileStorageOffset +
|
||||
bank * PROFILE_STORAGE_BANK_SIZE + offset;
|
||||
memcpy(output, reinterpret_cast<const void*>(address), size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool replace_storage_bank(void*, uint8_t bank,
|
||||
const uint8_t* payload,
|
||||
size_t payload_size,
|
||||
const uint8_t* header,
|
||||
size_t header_size) {
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || payload == nullptr ||
|
||||
header == nullptr ||
|
||||
payload_size != CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
header_size != PROFILE_STORAGE_RECORD_HEADER_SIZE ||
|
||||
!storage_region_available()) {
|
||||
return false;
|
||||
}
|
||||
FlashBankReplacement replacement{
|
||||
bank,
|
||||
payload,
|
||||
payload_size,
|
||||
header,
|
||||
false,
|
||||
};
|
||||
return flash_safe_execute(
|
||||
perform_flash_bank_replacement, &replacement,
|
||||
UINT32_MAX) == PICO_OK &&
|
||||
replacement.replaced;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProfileStorageIo pico_profile_storage_io() {
|
||||
return {
|
||||
nullptr,
|
||||
PROFILE_STORAGE_BANK_SIZE,
|
||||
FLASH_SECTOR_SIZE,
|
||||
FLASH_PAGE_SIZE,
|
||||
read_storage,
|
||||
replace_storage_bank,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "profile_storage.h"
|
||||
|
||||
ProfileStorageIo pico_profile_storage_io();
|
||||
|
|
@ -1,660 +0,0 @@
|
|||
#include "profile_service.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "pico/critical_section.h"
|
||||
#include "pico_profile_storage.h"
|
||||
#include "profile_storage.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kMinimumCommitIntervalMs = 1000;
|
||||
|
||||
enum class PendingCommandType : uint8_t {
|
||||
kNone = 0,
|
||||
kReset = 1,
|
||||
kActivate = 2,
|
||||
};
|
||||
|
||||
struct PendingCommand {
|
||||
PendingCommandType type = PendingCommandType::kNone;
|
||||
uint32_t transaction_id = 0;
|
||||
ControllerIdentity identity{};
|
||||
uint8_t profile_index = 0;
|
||||
};
|
||||
|
||||
struct ProfileTransaction {
|
||||
ControllerIdentity identity{};
|
||||
uint8_t profile_index = 0;
|
||||
ConfigurationTransactionSnapshot snapshot{};
|
||||
uint8_t payload[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
};
|
||||
|
||||
struct PublishedActiveProfile {
|
||||
ControllerIdentity identity{};
|
||||
uint8_t profile_index = 0;
|
||||
ControllerProfile profile{};
|
||||
};
|
||||
|
||||
critical_section_t g_lock;
|
||||
bool g_prepared = false;
|
||||
ProfileStorage g_storage;
|
||||
ControllerProfileDatabase g_database;
|
||||
ProfileServiceMetadata g_metadata;
|
||||
uint32_t g_published_generation = 0;
|
||||
ProfileServiceListSnapshot g_list;
|
||||
ProfileServiceSelectedSnapshot g_selected;
|
||||
PublishedActiveProfile
|
||||
g_active_profiles[PROFILE_SERVICE_LIST_CAPACITY]{};
|
||||
uint8_t g_active_profile_count = 0;
|
||||
ProfileTransaction g_transaction;
|
||||
PendingCommand g_command;
|
||||
PendingCommand g_internal_activation;
|
||||
alignas(4) uint8_t
|
||||
g_encoded_database[CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE]{};
|
||||
bool g_identity_dirty = false;
|
||||
bool g_has_committed = false;
|
||||
uint32_t g_last_commit_ms = 0;
|
||||
|
||||
bool valid_identity(const ControllerIdentity& identity) {
|
||||
uint8_t encoded[CONTROLLER_IDENTITY_ENCODED_SIZE]{};
|
||||
return (controller_identity_is_global(identity) || identity.stable) &&
|
||||
controller_identity_encode(identity, encoded, sizeof(encoded));
|
||||
}
|
||||
|
||||
void refresh_list_locked() {
|
||||
g_list = {};
|
||||
g_list.metadata = g_metadata;
|
||||
g_list.count = 1;
|
||||
g_list.rows[0].identity = controller_identity_global();
|
||||
g_list.rows[0].active_profile =
|
||||
g_database.fallback_active_profile;
|
||||
for (const ControllerProfileDatabaseEntry& entry : g_database.entries) {
|
||||
if (!entry.used || g_list.count >= PROFILE_SERVICE_LIST_CAPACITY) {
|
||||
continue;
|
||||
}
|
||||
ProfileServiceListRow& row = g_list.rows[g_list.count++];
|
||||
row.identity = entry.identity;
|
||||
row.active_profile = entry.active_profile;
|
||||
}
|
||||
}
|
||||
|
||||
void refresh_active_profiles_locked() {
|
||||
g_active_profile_count = 1;
|
||||
g_active_profiles[0].identity = controller_identity_global();
|
||||
g_active_profiles[0].profile_index =
|
||||
g_database.fallback_active_profile;
|
||||
g_active_profiles[0].profile =
|
||||
g_database.fallback_profiles[g_database.fallback_active_profile];
|
||||
for (const ControllerProfileDatabaseEntry& entry : g_database.entries) {
|
||||
if (!entry.used ||
|
||||
g_active_profile_count >= PROFILE_SERVICE_LIST_CAPACITY) {
|
||||
continue;
|
||||
}
|
||||
PublishedActiveProfile& active =
|
||||
g_active_profiles[g_active_profile_count++];
|
||||
active.identity = entry.identity;
|
||||
active.profile_index = entry.active_profile;
|
||||
active.profile = entry.profiles[entry.active_profile];
|
||||
}
|
||||
}
|
||||
|
||||
void refresh_selected_locked() {
|
||||
g_selected.metadata = g_metadata;
|
||||
const ControllerProfile* profile = controller_profile_database_get(
|
||||
g_database, g_selected.identity, g_selected.profile_index);
|
||||
if (profile == nullptr) {
|
||||
g_selected.valid = false;
|
||||
g_selected.status = ConfigurationTransactionStatus::kMalformed;
|
||||
return;
|
||||
}
|
||||
g_selected.profile = *profile;
|
||||
g_selected.valid = true;
|
||||
g_selected.status = ConfigurationTransactionStatus::kCommitted;
|
||||
}
|
||||
|
||||
void refresh_metadata_locked(ProfileServiceState state) {
|
||||
const ProfileStorageSnapshot& stored = g_storage.snapshot();
|
||||
g_metadata.state = state;
|
||||
g_metadata.generation = stored.valid ? stored.generation : 0;
|
||||
g_metadata.payload_crc = stored.valid ? stored.payload_crc : 0;
|
||||
refresh_active_profiles_locked();
|
||||
refresh_list_locked();
|
||||
refresh_selected_locked();
|
||||
__atomic_store_n(&g_published_generation, g_metadata.generation,
|
||||
__ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus database_result_status(
|
||||
ControllerProfileDatabaseResult result) {
|
||||
switch (result) {
|
||||
case ControllerProfileDatabaseResult::kOk:
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
case ControllerProfileDatabaseResult::kFull:
|
||||
return ConfigurationTransactionStatus::kTooLarge;
|
||||
case ControllerProfileDatabaseResult::kInvalidArgument:
|
||||
return ConfigurationTransactionStatus::kMalformed;
|
||||
}
|
||||
return ConfigurationTransactionStatus::kStorageError;
|
||||
}
|
||||
|
||||
bool mutation_ready(uint32_t now_ms) {
|
||||
return !g_has_committed ||
|
||||
static_cast<uint32_t>(now_ms - g_last_commit_ms) >=
|
||||
kMinimumCommitIntervalMs;
|
||||
}
|
||||
|
||||
void finish_mutation(ConfigurationTransactionStatus status,
|
||||
bool clear_command) {
|
||||
const ProfileStorageSnapshot& stored = g_storage.snapshot();
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
g_transaction.snapshot.status = status;
|
||||
g_transaction.snapshot.stored_generation =
|
||||
stored.valid ? stored.generation : 0;
|
||||
g_transaction.snapshot.stored_crc =
|
||||
stored.valid ? stored.payload_crc : 0;
|
||||
if (clear_command) {
|
||||
g_command = {};
|
||||
}
|
||||
refresh_metadata_locked(
|
||||
status == ConfigurationTransactionStatus::kStorageError
|
||||
? ProfileServiceState::kStorageError
|
||||
: ProfileServiceState::kReady);
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
void finish_internal_activation(
|
||||
ConfigurationTransactionStatus status) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
g_internal_activation = {};
|
||||
refresh_metadata_locked(
|
||||
status == ConfigurationTransactionStatus::kStorageError
|
||||
? ProfileServiceState::kStorageError
|
||||
: ProfileServiceState::kReady);
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void profile_service_prepare() {
|
||||
if (g_prepared) {
|
||||
return;
|
||||
}
|
||||
critical_section_init(&g_lock);
|
||||
g_metadata = {};
|
||||
__atomic_store_n(&g_published_generation, 0, __ATOMIC_RELAXED);
|
||||
g_list = {};
|
||||
g_selected = {};
|
||||
g_active_profiles[0] = {};
|
||||
g_active_profile_count = 0;
|
||||
g_selected.identity = controller_identity_global();
|
||||
g_selected.profile_index = 0;
|
||||
g_transaction = {};
|
||||
g_command = {};
|
||||
g_internal_activation = {};
|
||||
g_identity_dirty = false;
|
||||
g_has_committed = false;
|
||||
g_last_commit_ms = 0;
|
||||
g_prepared = true;
|
||||
}
|
||||
|
||||
void profile_service_initialize_on_storage_core() {
|
||||
if (!g_prepared) {
|
||||
profile_service_prepare();
|
||||
}
|
||||
const bool initialized =
|
||||
g_storage.initialize(pico_profile_storage_io(), &g_database);
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
refresh_metadata_locked(initialized ? ProfileServiceState::kReady
|
||||
: ProfileServiceState::kStorageError);
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
bool profile_service_observe_identity_on_storage_core(
|
||||
const ControllerIdentity& identity) {
|
||||
if (!g_prepared || !identity.stable ||
|
||||
controller_identity_is_global(identity) ||
|
||||
!valid_identity(identity)) {
|
||||
return false;
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_metadata.state != ProfileServiceState::kReady) {
|
||||
critical_section_exit(&g_lock);
|
||||
return false;
|
||||
}
|
||||
ControllerProfileDatabaseEntry* entry =
|
||||
controller_profile_database_find(&g_database, identity);
|
||||
if (entry != nullptr) {
|
||||
critical_section_exit(&g_lock);
|
||||
return true;
|
||||
}
|
||||
const ControllerProfileDatabaseResult result =
|
||||
controller_profile_database_ensure(&g_database, identity, &entry);
|
||||
if (result == ControllerProfileDatabaseResult::kOk) {
|
||||
g_identity_dirty = true;
|
||||
refresh_list_locked();
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return result == ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
|
||||
|
||||
void profile_service_task_on_storage_core(uint32_t now_ms) {
|
||||
PendingCommand command{};
|
||||
bool process_write = false;
|
||||
bool process_internal_activation = false;
|
||||
bool process_identity = false;
|
||||
ControllerIdentity write_identity{};
|
||||
uint8_t write_profile_index = 0;
|
||||
uint8_t write_payload[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (mutation_ready(now_ms)) {
|
||||
if (g_command.type != PendingCommandType::kNone) {
|
||||
command = g_command;
|
||||
} else if (g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kPending) {
|
||||
process_write = true;
|
||||
write_identity = g_transaction.identity;
|
||||
write_profile_index = g_transaction.profile_index;
|
||||
memcpy(write_payload, g_transaction.payload,
|
||||
sizeof(write_payload));
|
||||
} else if (g_internal_activation.type !=
|
||||
PendingCommandType::kNone) {
|
||||
command = g_internal_activation;
|
||||
process_internal_activation = true;
|
||||
} else if (g_identity_dirty) {
|
||||
process_identity = true;
|
||||
}
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
|
||||
if (!process_write && !process_identity &&
|
||||
!process_internal_activation &&
|
||||
command.type == PendingCommandType::kNone) {
|
||||
return;
|
||||
}
|
||||
|
||||
ControllerProfileDatabaseResult database_result =
|
||||
ControllerProfileDatabaseResult::kInvalidArgument;
|
||||
if (process_identity) {
|
||||
database_result = ControllerProfileDatabaseResult::kOk;
|
||||
} else if (process_write) {
|
||||
ControllerProfile profile{};
|
||||
if (controller_profile_decode(write_payload, sizeof(write_payload),
|
||||
&profile)) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
database_result = controller_profile_database_set(
|
||||
&g_database, write_identity, write_profile_index, profile);
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
} else if (command.type == PendingCommandType::kReset) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
database_result = controller_profile_database_reset(
|
||||
&g_database, command.identity, command.profile_index);
|
||||
critical_section_exit(&g_lock);
|
||||
} else if (command.type == PendingCommandType::kActivate) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
database_result = controller_profile_database_activate(
|
||||
&g_database, command.identity, command.profile_index);
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
if (database_result != ControllerProfileDatabaseResult::kOk) {
|
||||
const ConfigurationTransactionStatus error_status =
|
||||
database_result_status(database_result);
|
||||
if (process_internal_activation) {
|
||||
finish_internal_activation(error_status);
|
||||
} else {
|
||||
finish_mutation(error_status, !process_write);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const ProfileStorageResult storage_result = g_storage.commit(
|
||||
g_database, g_encoded_database, sizeof(g_encoded_database));
|
||||
ConfigurationTransactionStatus status =
|
||||
ConfigurationTransactionStatus::kStorageError;
|
||||
if (storage_result == ProfileStorageResult::kOk) {
|
||||
status = ConfigurationTransactionStatus::kCommitted;
|
||||
g_has_committed = true;
|
||||
g_last_commit_ms = now_ms;
|
||||
} else if (storage_result == ProfileStorageResult::kUnchanged) {
|
||||
status = ConfigurationTransactionStatus::kUnchanged;
|
||||
} else {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
const bool restored =
|
||||
g_storage.initialize(pico_profile_storage_io(), &g_database);
|
||||
critical_section_exit(&g_lock);
|
||||
if (!restored) {
|
||||
status = ConfigurationTransactionStatus::kStorageError;
|
||||
}
|
||||
}
|
||||
if (process_identity) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
g_identity_dirty = false;
|
||||
refresh_metadata_locked(
|
||||
status == ConfigurationTransactionStatus::kStorageError
|
||||
? ProfileServiceState::kStorageError
|
||||
: ProfileServiceState::kReady);
|
||||
critical_section_exit(&g_lock);
|
||||
return;
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
g_identity_dirty = false;
|
||||
critical_section_exit(&g_lock);
|
||||
if (process_internal_activation) {
|
||||
finish_internal_activation(status);
|
||||
} else {
|
||||
finish_mutation(status, !process_write);
|
||||
}
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_select(
|
||||
const ControllerIdentity& identity, uint8_t profile_index) {
|
||||
if (!g_prepared) {
|
||||
profile_service_prepare();
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
ConfigurationTransactionStatus status =
|
||||
ConfigurationTransactionStatus::kCommitted;
|
||||
if (!valid_identity(identity) ||
|
||||
profile_index >= CONTROLLER_PROFILE_COUNT) {
|
||||
status = ConfigurationTransactionStatus::kMalformed;
|
||||
g_selected.metadata = g_metadata;
|
||||
g_selected.identity = identity;
|
||||
g_selected.profile_index = profile_index;
|
||||
g_selected.valid = false;
|
||||
g_selected.status = status;
|
||||
} else if (g_metadata.state != ProfileServiceState::kReady) {
|
||||
status = g_metadata.state == ProfileServiceState::kLoading
|
||||
? ConfigurationTransactionStatus::kPending
|
||||
: ConfigurationTransactionStatus::kStorageError;
|
||||
g_selected.metadata = g_metadata;
|
||||
g_selected.identity = identity;
|
||||
g_selected.profile_index = profile_index;
|
||||
g_selected.valid = false;
|
||||
g_selected.status = status;
|
||||
} else {
|
||||
g_selected.identity = identity;
|
||||
g_selected.profile_index = profile_index;
|
||||
refresh_selected_locked();
|
||||
status = g_selected.status;
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return status;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_begin(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index, uint16_t schema_version, size_t payload_size,
|
||||
uint32_t payload_crc) {
|
||||
if (!g_prepared) {
|
||||
profile_service_prepare();
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_command.type != PendingCommandType::kNone ||
|
||||
g_internal_activation.type != PendingCommandType::kNone ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kReceiving ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kPending) {
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kBusy;
|
||||
}
|
||||
g_transaction = {};
|
||||
g_transaction.snapshot.transaction_id = transaction_id;
|
||||
g_transaction.identity = identity;
|
||||
g_transaction.profile_index = profile_index;
|
||||
if (transaction_id == 0 || !valid_identity(identity) ||
|
||||
profile_index >= CONTROLLER_PROFILE_COUNT || payload_size == 0) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kMalformed;
|
||||
} else if (schema_version != CONTROLLER_PROFILE_SCHEMA_VERSION) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kUnsupportedSchema;
|
||||
} else if (payload_size > CONTROLLER_PROFILE_ENCODED_SIZE) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kTooLarge;
|
||||
} else if (payload_size != CONTROLLER_PROFILE_ENCODED_SIZE) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kMalformed;
|
||||
} else {
|
||||
g_transaction.snapshot.expected_size =
|
||||
static_cast<uint16_t>(payload_size);
|
||||
g_transaction.snapshot.expected_crc = payload_crc;
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kReceiving;
|
||||
}
|
||||
const ConfigurationTransactionStatus status =
|
||||
g_transaction.snapshot.status;
|
||||
critical_section_exit(&g_lock);
|
||||
return status;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_append(
|
||||
uint32_t transaction_id, size_t offset, const uint8_t* data,
|
||||
size_t size) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_transaction.snapshot.status !=
|
||||
ConfigurationTransactionStatus::kReceiving) {
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kBusy;
|
||||
}
|
||||
if (transaction_id != g_transaction.snapshot.transaction_id ||
|
||||
data == nullptr || size == 0 ||
|
||||
offset != g_transaction.snapshot.received_size ||
|
||||
offset > g_transaction.snapshot.expected_size ||
|
||||
size > g_transaction.snapshot.expected_size - offset) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kOutOfOrder;
|
||||
} else {
|
||||
memcpy(&g_transaction.payload[offset], data, size);
|
||||
g_transaction.snapshot.received_size =
|
||||
static_cast<uint16_t>(offset + size);
|
||||
}
|
||||
const ConfigurationTransactionStatus status =
|
||||
g_transaction.snapshot.status;
|
||||
critical_section_exit(&g_lock);
|
||||
return status;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_commit(
|
||||
uint32_t transaction_id) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_transaction.snapshot.status !=
|
||||
ConfigurationTransactionStatus::kReceiving ||
|
||||
transaction_id != g_transaction.snapshot.transaction_id ||
|
||||
g_transaction.snapshot.received_size !=
|
||||
g_transaction.snapshot.expected_size) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kOutOfOrder;
|
||||
} else if (profile_storage_crc32(
|
||||
g_transaction.payload,
|
||||
g_transaction.snapshot.expected_size) !=
|
||||
g_transaction.snapshot.expected_crc) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kBadCrc;
|
||||
} else {
|
||||
ControllerProfile profile{};
|
||||
g_transaction.snapshot.status =
|
||||
controller_profile_decode(
|
||||
g_transaction.payload,
|
||||
g_transaction.snapshot.expected_size, &profile)
|
||||
? ConfigurationTransactionStatus::kPending
|
||||
: ConfigurationTransactionStatus::kMalformed;
|
||||
}
|
||||
const ConfigurationTransactionStatus status =
|
||||
g_transaction.snapshot.status;
|
||||
critical_section_exit(&g_lock);
|
||||
return status;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_reset(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
if (!g_prepared) {
|
||||
profile_service_prepare();
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_command.type != PendingCommandType::kNone ||
|
||||
g_internal_activation.type != PendingCommandType::kNone ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kReceiving ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kPending) {
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kBusy;
|
||||
}
|
||||
g_transaction = {};
|
||||
g_transaction.snapshot.transaction_id = transaction_id;
|
||||
g_transaction.identity = identity;
|
||||
g_transaction.profile_index = profile_index;
|
||||
if (transaction_id == 0 || !valid_identity(identity) ||
|
||||
(profile_index != CONTROLLER_PROFILE_ALL &&
|
||||
profile_index >= CONTROLLER_PROFILE_COUNT)) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kMalformed;
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kMalformed;
|
||||
}
|
||||
g_transaction.snapshot.status = ConfigurationTransactionStatus::kPending;
|
||||
g_command.transaction_id = transaction_id;
|
||||
g_command.type = PendingCommandType::kReset;
|
||||
g_command.identity = identity;
|
||||
g_command.profile_index = profile_index;
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_activate(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
if (!g_prepared) {
|
||||
profile_service_prepare();
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_command.type != PendingCommandType::kNone ||
|
||||
g_internal_activation.type != PendingCommandType::kNone ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kReceiving ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kPending) {
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kBusy;
|
||||
}
|
||||
g_transaction = {};
|
||||
g_transaction.snapshot.transaction_id = transaction_id;
|
||||
g_transaction.identity = identity;
|
||||
g_transaction.profile_index = profile_index;
|
||||
if (transaction_id == 0 || !valid_identity(identity) ||
|
||||
profile_index >= CONTROLLER_PROFILE_COUNT) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kMalformed;
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kMalformed;
|
||||
}
|
||||
g_transaction.snapshot.status = ConfigurationTransactionStatus::kPending;
|
||||
g_command.transaction_id = transaction_id;
|
||||
g_command.type = PendingCommandType::kActivate;
|
||||
g_command.identity = identity;
|
||||
g_command.profile_index = profile_index;
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_activate_internal(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
if (!g_prepared) {
|
||||
profile_service_prepare();
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_command.type != PendingCommandType::kNone ||
|
||||
g_internal_activation.type != PendingCommandType::kNone ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kReceiving ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kPending) {
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kBusy;
|
||||
}
|
||||
if ((transaction_id & 0x80000000u) == 0 ||
|
||||
!valid_identity(identity) ||
|
||||
profile_index >= CONTROLLER_PROFILE_COUNT) {
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kMalformed;
|
||||
}
|
||||
g_internal_activation.type = PendingCommandType::kActivate;
|
||||
g_internal_activation.transaction_id = transaction_id;
|
||||
g_internal_activation.identity = identity;
|
||||
g_internal_activation.profile_index = profile_index;
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
void profile_service_list_snapshot(ProfileServiceListSnapshot* output) {
|
||||
if (output == nullptr) {
|
||||
return;
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
*output = g_list;
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
void profile_service_selected_snapshot(
|
||||
ProfileServiceSelectedSnapshot* output) {
|
||||
if (output == nullptr) {
|
||||
return;
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
*output = g_selected;
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
void profile_service_transaction_snapshot(
|
||||
ProfileServiceTransactionSnapshot* output) {
|
||||
if (output == nullptr) {
|
||||
return;
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
output->metadata = g_metadata;
|
||||
output->identity = g_transaction.identity;
|
||||
output->profile_index = g_transaction.profile_index;
|
||||
output->transaction = g_transaction.snapshot;
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
uint32_t profile_service_database_generation() {
|
||||
return __atomic_load_n(&g_published_generation, __ATOMIC_ACQUIRE);
|
||||
}
|
||||
|
||||
void profile_service_active_profile_snapshot(
|
||||
const ControllerIdentity& identity,
|
||||
ProfileServiceActiveProfileSnapshot* output) {
|
||||
if (output == nullptr) {
|
||||
return;
|
||||
}
|
||||
*output = {};
|
||||
if (!valid_identity(identity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
output->metadata = g_metadata;
|
||||
if (g_metadata.state == ProfileServiceState::kReady &&
|
||||
g_active_profile_count != 0) {
|
||||
const PublishedActiveProfile* active = &g_active_profiles[0];
|
||||
for (uint8_t index = 1; index < g_active_profile_count; ++index) {
|
||||
if (controller_identity_equal(
|
||||
g_active_profiles[index].identity, identity)) {
|
||||
active = &g_active_profiles[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
output->profile_index = active->profile_index;
|
||||
output->profile = active->profile;
|
||||
output->valid = true;
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "configuration_transaction.h"
|
||||
#include "controller_profile.h"
|
||||
|
||||
constexpr uint8_t PROFILE_SERVICE_LIST_CAPACITY =
|
||||
CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY + 1;
|
||||
|
||||
enum class ProfileServiceState : uint8_t {
|
||||
kLoading = 0,
|
||||
kReady = 1,
|
||||
kStorageError = 2,
|
||||
};
|
||||
|
||||
struct ProfileServiceMetadata {
|
||||
ProfileServiceState state = ProfileServiceState::kLoading;
|
||||
uint32_t generation = 0;
|
||||
uint32_t payload_crc = 0;
|
||||
};
|
||||
|
||||
struct ProfileServiceListRow {
|
||||
ControllerIdentity identity{};
|
||||
uint8_t active_profile = 0;
|
||||
};
|
||||
|
||||
struct ProfileServiceListSnapshot {
|
||||
ProfileServiceMetadata metadata{};
|
||||
uint8_t count = 0;
|
||||
ProfileServiceListRow rows[PROFILE_SERVICE_LIST_CAPACITY]{};
|
||||
};
|
||||
|
||||
struct ProfileServiceSelectedSnapshot {
|
||||
ProfileServiceMetadata metadata{};
|
||||
bool valid = false;
|
||||
ConfigurationTransactionStatus status =
|
||||
ConfigurationTransactionStatus::kIdle;
|
||||
ControllerIdentity identity{};
|
||||
uint8_t profile_index = 0;
|
||||
ControllerProfile profile{};
|
||||
};
|
||||
|
||||
struct ProfileServiceActiveProfileSnapshot {
|
||||
ProfileServiceMetadata metadata{};
|
||||
bool valid = false;
|
||||
uint8_t profile_index = 0;
|
||||
ControllerProfile profile{};
|
||||
};
|
||||
|
||||
struct ProfileServiceTransactionSnapshot {
|
||||
ProfileServiceMetadata metadata{};
|
||||
ControllerIdentity identity{};
|
||||
uint8_t profile_index = 0;
|
||||
ConfigurationTransactionSnapshot transaction{};
|
||||
};
|
||||
|
||||
void profile_service_prepare();
|
||||
void profile_service_initialize_on_storage_core();
|
||||
void profile_service_task_on_storage_core(uint32_t now_ms);
|
||||
bool profile_service_observe_identity_on_storage_core(
|
||||
const ControllerIdentity& identity);
|
||||
|
||||
ConfigurationTransactionStatus profile_service_select(
|
||||
const ControllerIdentity& identity, uint8_t profile_index);
|
||||
ConfigurationTransactionStatus profile_service_begin(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index, uint16_t schema_version, size_t payload_size,
|
||||
uint32_t payload_crc);
|
||||
ConfigurationTransactionStatus profile_service_append(
|
||||
uint32_t transaction_id, size_t offset, const uint8_t* data,
|
||||
size_t size);
|
||||
ConfigurationTransactionStatus profile_service_commit(
|
||||
uint32_t transaction_id);
|
||||
ConfigurationTransactionStatus profile_service_reset(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index);
|
||||
ConfigurationTransactionStatus profile_service_activate(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index);
|
||||
// Queue a controller-originated activation without replacing the host-visible
|
||||
// transaction snapshot. transaction_id must be nonzero with its high bit set.
|
||||
ConfigurationTransactionStatus profile_service_activate_internal(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index);
|
||||
|
||||
void profile_service_list_snapshot(ProfileServiceListSnapshot* output);
|
||||
void profile_service_selected_snapshot(
|
||||
ProfileServiceSelectedSnapshot* output);
|
||||
void profile_service_transaction_snapshot(
|
||||
ProfileServiceTransactionSnapshot* output);
|
||||
uint32_t profile_service_database_generation();
|
||||
void profile_service_active_profile_snapshot(
|
||||
const ControllerIdentity& identity,
|
||||
ProfileServiceActiveProfileSnapshot* output);
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
#include "profile_storage.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kRecordMagic[4] = {'S', 'P', 'P', 'F'};
|
||||
constexpr uint16_t kRecordFormatVersion = 1;
|
||||
constexpr size_t kHeaderFieldsSize = 24;
|
||||
constexpr size_t kHeaderCrcOffset = 20;
|
||||
|
||||
uint16_t storage_read_u16(const uint8_t* input) {
|
||||
return static_cast<uint16_t>(input[0]) |
|
||||
(static_cast<uint16_t>(input[1]) << 8);
|
||||
}
|
||||
|
||||
uint32_t storage_read_u32(const uint8_t* input) {
|
||||
return static_cast<uint32_t>(input[0]) |
|
||||
(static_cast<uint32_t>(input[1]) << 8) |
|
||||
(static_cast<uint32_t>(input[2]) << 16) |
|
||||
(static_cast<uint32_t>(input[3]) << 24);
|
||||
}
|
||||
|
||||
void storage_write_u16(uint8_t* output, uint16_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
}
|
||||
|
||||
void storage_write_u32(uint8_t* output, uint32_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
output[2] = static_cast<uint8_t>(value >> 16);
|
||||
output[3] = static_cast<uint8_t>(value >> 24);
|
||||
}
|
||||
|
||||
uint32_t crc32_update(uint32_t crc, const uint8_t* data, size_t size) {
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
crc ^= data[index];
|
||||
for (uint8_t bit = 0; bit < 8; ++bit) {
|
||||
crc = (crc >> 1) ^
|
||||
(0xedb88320u &
|
||||
static_cast<uint32_t>(
|
||||
-static_cast<int32_t>(crc & 1u)));
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool generation_is_newer(uint32_t candidate, uint32_t current) {
|
||||
return static_cast<int32_t>(candidate - current) > 0;
|
||||
}
|
||||
|
||||
struct DatabaseReadContext {
|
||||
const ProfileStorageIo* io;
|
||||
uint8_t bank;
|
||||
};
|
||||
|
||||
bool read_database(void* context, size_t offset, uint8_t* output,
|
||||
size_t size) {
|
||||
const auto* read_context =
|
||||
static_cast<const DatabaseReadContext*>(context);
|
||||
return read_context->io->read(
|
||||
read_context->io->context, read_context->bank,
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE + offset, output, size);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t profile_storage_crc32(const uint8_t* data, size_t size) {
|
||||
if (data == nullptr && size != 0) {
|
||||
return 0;
|
||||
}
|
||||
return ~crc32_update(0xffffffffu, data, size);
|
||||
}
|
||||
|
||||
bool ProfileStorage::initialize(const ProfileStorageIo& io,
|
||||
ControllerProfileDatabase* database) {
|
||||
io_ = io;
|
||||
snapshot_ = {};
|
||||
initialized_ =
|
||||
database != nullptr && io_.read != nullptr &&
|
||||
io_.replace_bank != nullptr &&
|
||||
io_.bank_size >= PROFILE_STORAGE_BANK_SIZE &&
|
||||
io_.sector_size == PROFILE_STORAGE_SECTOR_SIZE &&
|
||||
io_.page_size == PROFILE_STORAGE_PAGE_SIZE &&
|
||||
io_.bank_size % io_.sector_size == 0 &&
|
||||
io_.sector_size % io_.page_size == 0;
|
||||
if (!initialized_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
controller_profile_database_default(database);
|
||||
BankHeader headers[PROFILE_STORAGE_BANK_COUNT]{};
|
||||
bool valid[PROFILE_STORAGE_BANK_COUNT]{};
|
||||
for (uint8_t bank = 0; bank < PROFILE_STORAGE_BANK_COUNT; ++bank) {
|
||||
valid[bank] = read_header(bank, &headers[bank]) &&
|
||||
validate_payload(bank, headers[bank].payload_crc);
|
||||
}
|
||||
|
||||
uint8_t first = 0;
|
||||
uint8_t second = 1;
|
||||
if (valid[1] &&
|
||||
(!valid[0] || generation_is_newer(headers[1].generation,
|
||||
headers[0].generation))) {
|
||||
first = 1;
|
||||
second = 0;
|
||||
}
|
||||
const uint8_t order[PROFILE_STORAGE_BANK_COUNT] = {first, second};
|
||||
for (uint8_t candidate : order) {
|
||||
if (!valid[candidate] || !decode_bank(candidate, database)) {
|
||||
continue;
|
||||
}
|
||||
snapshot_.valid = true;
|
||||
snapshot_.generation = headers[candidate].generation;
|
||||
snapshot_.payload_crc = headers[candidate].payload_crc;
|
||||
snapshot_.active_bank = candidate;
|
||||
return true;
|
||||
}
|
||||
controller_profile_database_default(database);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProfileStorageResult ProfileStorage::commit(
|
||||
const ControllerProfileDatabase& database,
|
||||
uint8_t* encoded_database, size_t encoded_database_size) {
|
||||
if (!initialized_ || !controller_profile_database_validate(database) ||
|
||||
encoded_database == nullptr ||
|
||||
encoded_database_size < CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
!controller_profile_database_encode_range(
|
||||
database, 0, encoded_database,
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE)) {
|
||||
return ProfileStorageResult::kInvalidArgument;
|
||||
}
|
||||
if (snapshot_.valid &&
|
||||
payload_matches_encoded(
|
||||
snapshot_.active_bank, encoded_database,
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE)) {
|
||||
return ProfileStorageResult::kUnchanged;
|
||||
}
|
||||
|
||||
const uint32_t crc = profile_storage_crc32(
|
||||
encoded_database, CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
|
||||
const uint8_t target_bank =
|
||||
snapshot_.valid ? snapshot_.active_bank ^ 1u : 0;
|
||||
const uint32_t generation =
|
||||
snapshot_.valid ? snapshot_.generation + 1u : 1u;
|
||||
uint8_t header[PROFILE_STORAGE_RECORD_HEADER_SIZE]{};
|
||||
memcpy(header, kRecordMagic, sizeof(kRecordMagic));
|
||||
storage_write_u16(&header[4], kRecordFormatVersion);
|
||||
storage_write_u16(
|
||||
&header[6], CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION);
|
||||
storage_write_u32(&header[8], generation);
|
||||
storage_write_u32(
|
||||
&header[12], CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
|
||||
storage_write_u32(&header[16], crc);
|
||||
storage_write_u32(
|
||||
&header[kHeaderCrcOffset],
|
||||
profile_storage_crc32(header, kHeaderCrcOffset));
|
||||
|
||||
if (!io_.replace_bank(
|
||||
io_.context, target_bank, encoded_database,
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE, header,
|
||||
sizeof(header))) {
|
||||
return ProfileStorageResult::kIoError;
|
||||
}
|
||||
|
||||
snapshot_.valid = true;
|
||||
snapshot_.generation = generation;
|
||||
snapshot_.payload_crc = crc;
|
||||
snapshot_.active_bank = target_bank;
|
||||
return ProfileStorageResult::kOk;
|
||||
}
|
||||
|
||||
const ProfileStorageSnapshot& ProfileStorage::snapshot() const {
|
||||
return snapshot_;
|
||||
}
|
||||
|
||||
bool ProfileStorage::read_header(uint8_t bank, BankHeader* output) const {
|
||||
uint8_t header[PROFILE_STORAGE_RECORD_HEADER_SIZE]{};
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || output == nullptr ||
|
||||
!io_.read(io_.context, bank, 0, header, sizeof(header)) ||
|
||||
memcmp(header, kRecordMagic, sizeof(kRecordMagic)) != 0 ||
|
||||
storage_read_u16(&header[4]) != kRecordFormatVersion ||
|
||||
(storage_read_u16(&header[6]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION &&
|
||||
storage_read_u16(&header[6]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION) ||
|
||||
storage_read_u32(&header[12]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
profile_storage_crc32(header, kHeaderCrcOffset) !=
|
||||
storage_read_u32(&header[kHeaderCrcOffset])) {
|
||||
return false;
|
||||
}
|
||||
for (size_t offset = kHeaderFieldsSize;
|
||||
offset < sizeof(header); ++offset) {
|
||||
if (header[offset] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
output->generation = storage_read_u32(&header[8]);
|
||||
output->payload_crc = storage_read_u32(&header[16]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProfileStorage::validate_payload(uint8_t bank,
|
||||
uint32_t expected_crc) const {
|
||||
uint8_t page[PROFILE_STORAGE_PAGE_SIZE]{};
|
||||
uint32_t crc = 0xffffffffu;
|
||||
for (size_t offset = 0;
|
||||
offset < CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE;
|
||||
offset += sizeof(page)) {
|
||||
const size_t size =
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset < sizeof(page)
|
||||
? CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset
|
||||
: sizeof(page);
|
||||
if (!io_.read(io_.context, bank,
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE + offset,
|
||||
page, size)) {
|
||||
return false;
|
||||
}
|
||||
crc = crc32_update(crc, page, size);
|
||||
}
|
||||
return ~crc == expected_crc;
|
||||
}
|
||||
|
||||
bool ProfileStorage::decode_bank(
|
||||
uint8_t bank, ControllerProfileDatabase* database) const {
|
||||
DatabaseReadContext context{&io_, bank};
|
||||
return controller_profile_database_decode(read_database, &context,
|
||||
database);
|
||||
}
|
||||
|
||||
bool ProfileStorage::payload_matches_encoded(
|
||||
uint8_t bank, const uint8_t* payload,
|
||||
size_t payload_size) const {
|
||||
uint8_t stored[PROFILE_STORAGE_PAGE_SIZE]{};
|
||||
for (size_t offset = 0; offset < payload_size;
|
||||
offset += sizeof(stored)) {
|
||||
const size_t size =
|
||||
payload_size - offset < sizeof(stored)
|
||||
? payload_size - offset
|
||||
: sizeof(stored);
|
||||
if (!io_.read(
|
||||
io_.context, bank,
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE + offset,
|
||||
stored, size) ||
|
||||
memcmp(stored, &payload[offset], size) != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_profile.h"
|
||||
|
||||
constexpr uint8_t PROFILE_STORAGE_BANK_COUNT = 2;
|
||||
constexpr size_t PROFILE_STORAGE_SECTOR_SIZE = 4096;
|
||||
constexpr size_t PROFILE_STORAGE_SECTORS_PER_BANK = 5;
|
||||
constexpr size_t PROFILE_STORAGE_BANK_SIZE =
|
||||
PROFILE_STORAGE_SECTOR_SIZE * PROFILE_STORAGE_SECTORS_PER_BANK;
|
||||
constexpr size_t PROFILE_STORAGE_TOTAL_SIZE =
|
||||
PROFILE_STORAGE_BANK_COUNT * PROFILE_STORAGE_BANK_SIZE;
|
||||
constexpr size_t PROFILE_STORAGE_PAGE_SIZE = 256;
|
||||
constexpr size_t PROFILE_STORAGE_RECORD_HEADER_SIZE =
|
||||
PROFILE_STORAGE_PAGE_SIZE;
|
||||
|
||||
static_assert(PROFILE_STORAGE_BANK_SIZE == 20 * 1024);
|
||||
static_assert(PROFILE_STORAGE_TOTAL_SIZE == 40 * 1024);
|
||||
static_assert(PROFILE_STORAGE_RECORD_HEADER_SIZE +
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE <=
|
||||
PROFILE_STORAGE_BANK_SIZE,
|
||||
"profile database does not fit a storage bank");
|
||||
|
||||
enum class ProfileStorageResult : uint8_t {
|
||||
kOk = 0,
|
||||
kUnchanged = 1,
|
||||
kInvalidArgument = 2,
|
||||
kIoError = 3,
|
||||
};
|
||||
|
||||
struct ProfileStorageIo {
|
||||
void* context = nullptr;
|
||||
size_t bank_size = 0;
|
||||
size_t sector_size = 0;
|
||||
size_t page_size = 0;
|
||||
bool (*read)(void* context, uint8_t bank, size_t offset,
|
||||
uint8_t* output, size_t size) = nullptr;
|
||||
// Replaces one bank and verifies the payload before publishing the
|
||||
// complete header page.
|
||||
bool (*replace_bank)(void* context, uint8_t bank,
|
||||
const uint8_t* payload, size_t payload_size,
|
||||
const uint8_t* header, size_t header_size) = nullptr;
|
||||
};
|
||||
|
||||
struct ProfileStorageSnapshot {
|
||||
bool valid = false;
|
||||
uint32_t generation = 0;
|
||||
uint32_t payload_crc = 0;
|
||||
uint8_t active_bank = 0;
|
||||
};
|
||||
|
||||
uint32_t profile_storage_crc32(const uint8_t* data, size_t size);
|
||||
|
||||
class ProfileStorage {
|
||||
public:
|
||||
bool initialize(const ProfileStorageIo& io,
|
||||
ControllerProfileDatabase* database);
|
||||
ProfileStorageResult commit(
|
||||
const ControllerProfileDatabase& database,
|
||||
uint8_t* encoded_database,
|
||||
size_t encoded_database_size);
|
||||
const ProfileStorageSnapshot& snapshot() const;
|
||||
|
||||
private:
|
||||
struct BankHeader {
|
||||
uint32_t generation = 0;
|
||||
uint32_t payload_crc = 0;
|
||||
};
|
||||
|
||||
bool read_header(uint8_t bank, BankHeader* output) const;
|
||||
bool validate_payload(uint8_t bank, uint32_t expected_crc) const;
|
||||
bool decode_bank(uint8_t bank,
|
||||
ControllerProfileDatabase* database) const;
|
||||
bool payload_matches_encoded(uint8_t bank,
|
||||
const uint8_t* payload,
|
||||
size_t payload_size) const;
|
||||
|
||||
ProfileStorageIo io_{};
|
||||
ProfileStorageSnapshot snapshot_{};
|
||||
bool initialized_ = false;
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,6 @@
|
|||
#include "hardware/uart.h"
|
||||
#else
|
||||
#include "bluepad32_input_backend.h"
|
||||
#include "controller_profile_runtime.h"
|
||||
#include "bootsel_pairing_button.h"
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
#include "adapter_host_probe.h"
|
||||
|
|
@ -34,10 +33,6 @@
|
|||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT ==
|
||||
BLUEPAD32_INPUT_BACKEND_SLOT_COUNT);
|
||||
static_assert(CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT ==
|
||||
BLUEPAD32_INPUT_BACKEND_SLOT_COUNT);
|
||||
static_assert(SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD);
|
||||
static bool g_last_ready[BLUEPAD32_INPUT_BACKEND_SLOT_COUNT]{};
|
||||
static ControllerState
|
||||
g_user_states[BLUEPAD32_INPUT_BACKEND_SLOT_COUNT]{};
|
||||
|
|
@ -85,11 +80,7 @@ static void on_rumble_from_switch(uint8_t instance,
|
|||
if (instance >= BLUEPAD32_INPUT_BACKEND_SLOT_COUNT) {
|
||||
return;
|
||||
}
|
||||
Bluepad32SlotSnapshot snapshot{};
|
||||
bluepad32_input_backend_snapshot(instance, &snapshot);
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
instance, controller_profile_runtime_scale_host_rumble(
|
||||
instance, snapshot, rumble));
|
||||
bluepad32_input_backend_queue_rumble(instance, rumble);
|
||||
#else
|
||||
if (instance != SWITCH_HID_INSTANCE) {
|
||||
return;
|
||||
|
|
@ -226,7 +217,6 @@ int main() {
|
|||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
bluepad32_input_backend_init();
|
||||
controller_profile_runtime_reset();
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
adapter_host_probe_init();
|
||||
#endif
|
||||
|
|
@ -251,19 +241,13 @@ int main() {
|
|||
switch_pro_set_rumble_callback(instance,
|
||||
on_rumble_from_switch);
|
||||
g_user_states[instance] = neutral_input();
|
||||
switch_pro_set_input(
|
||||
instance, g_user_states[instance],
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD);
|
||||
switch_pro_set_input(instance, g_user_states[instance]);
|
||||
}
|
||||
#else
|
||||
switch_pro_init(instance);
|
||||
switch_pro_set_rumble_callback(instance, on_rumble_from_switch);
|
||||
g_user_states[instance] = neutral_input();
|
||||
switch_pro_set_input(
|
||||
instance, g_user_states[instance],
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD);
|
||||
switch_pro_set_input(instance, g_user_states[instance]);
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
|
|
@ -271,9 +255,7 @@ int main() {
|
|||
switch_pro_set_rumble_callback(SWITCH_HID_INSTANCE,
|
||||
on_rumble_from_switch);
|
||||
g_user_state = neutral_input();
|
||||
switch_pro_set_input(SWITCH_HID_INSTANCE, g_user_state,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(SWITCH_HID_INSTANCE, g_user_state);
|
||||
#endif
|
||||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
|
|
@ -306,62 +288,25 @@ int main() {
|
|||
case BootselPairingButtonEvent::kNone:
|
||||
break;
|
||||
}
|
||||
const uint32_t now_ms =
|
||||
static_cast<uint32_t>(to_ms_since_boot(get_absolute_time()));
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
const AdapterUsbMode output_mode = adapter_host_probe_mode();
|
||||
#else
|
||||
constexpr AdapterUsbMode output_mode = AdapterUsbMode::kSwitchProbe;
|
||||
#endif
|
||||
for (uint8_t instance = 0;
|
||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
||||
Bluepad32SlotSnapshot snapshot{};
|
||||
bluepad32_input_backend_snapshot(instance, &snapshot);
|
||||
const ControllerProfileTransformResult transformed =
|
||||
controller_profile_runtime_transform(
|
||||
instance, snapshot, now_ms, output_mode);
|
||||
ControllerProfileRuntimeProfileChangeEvent
|
||||
initial_profile_indication{};
|
||||
if (controller_profile_runtime_take_initial_profile_indication(
|
||||
instance, &initial_profile_indication)) {
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
instance,
|
||||
initial_profile_indication.connection_generation,
|
||||
initial_profile_indication.active_profile_number,
|
||||
initial_profile_indication.policy);
|
||||
} else {
|
||||
ControllerProfileRuntimeProfileChangeEvent
|
||||
profile_change{};
|
||||
if (controller_profile_runtime_take_profile_change(
|
||||
instance, &profile_change)) {
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
instance, profile_change.connection_generation,
|
||||
profile_change.active_profile_number,
|
||||
profile_change.policy);
|
||||
}
|
||||
}
|
||||
g_user_states[instance] = transformed.state;
|
||||
bluepad32_input_backend_snapshot(instance,
|
||||
&g_user_states[instance]);
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
bool sent = false;
|
||||
if (output_mode == AdapterUsbMode::kXInput) {
|
||||
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
|
||||
xinput_feasibility_set_input(instance,
|
||||
g_user_states[instance]);
|
||||
sent = xinput_feasibility_task(instance);
|
||||
} else {
|
||||
switch_pro_set_input(
|
||||
instance, g_user_states[instance],
|
||||
transformed.left_trigger_digital_threshold,
|
||||
transformed.right_trigger_digital_threshold);
|
||||
switch_pro_set_input(instance, g_user_states[instance]);
|
||||
sent = switch_pro_task(instance);
|
||||
}
|
||||
if (sent) {
|
||||
bluepad32_input_backend_report_sent(instance);
|
||||
}
|
||||
#else
|
||||
switch_pro_set_input(
|
||||
instance, g_user_states[instance],
|
||||
transformed.left_trigger_digital_threshold,
|
||||
transformed.right_trigger_digital_threshold);
|
||||
switch_pro_set_input(instance, g_user_states[instance]);
|
||||
if (switch_pro_task(instance)) {
|
||||
bluepad32_input_backend_report_sent(instance);
|
||||
}
|
||||
|
|
@ -371,9 +316,7 @@ int main() {
|
|||
bool new_data = poll_uart_frames(); // Pull controller state from UART1
|
||||
(void)new_data;
|
||||
ControllerState state = g_user_state;
|
||||
switch_pro_set_input(SWITCH_HID_INSTANCE, state,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(SWITCH_HID_INSTANCE, state);
|
||||
(void)switch_pro_task(SWITCH_HID_INSTANCE);
|
||||
#endif
|
||||
log_usb_state();
|
||||
|
|
|
|||
|
|
@ -43,10 +43,6 @@ struct MotionQuaternion {
|
|||
|
||||
struct SwitchProContext {
|
||||
ControllerState input_state{};
|
||||
uint16_t left_trigger_threshold =
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
uint16_t right_trigger_threshold =
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
uint8_t report_buffer[SWITCH_PRO_ENDPOINT_SIZE]{};
|
||||
SwitchProReport switch_report{};
|
||||
uint8_t last_report_counter = 0;
|
||||
|
|
@ -424,8 +420,6 @@ static ControllerState make_neutral_state() {
|
|||
static void reset_context_runtime(SwitchProContext& context, uint32_t now,
|
||||
bool ready_before_mount) {
|
||||
context.input_state = make_neutral_state();
|
||||
context.left_trigger_threshold = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
context.right_trigger_threshold = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
memset(context.report_buffer, 0x00, sizeof(context.report_buffer));
|
||||
context.switch_report = {};
|
||||
context.switch_report.reportID = 0x30;
|
||||
|
|
@ -753,7 +747,7 @@ static void update_switch_report_from_state(SwitchProContext& context) {
|
|||
inputs.buttonRightSL = 0;
|
||||
inputs.buttonR = state.button_right_shoulder;
|
||||
inputs.buttonZR =
|
||||
state.right_trigger >= context.right_trigger_threshold;
|
||||
state.right_trigger >= SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
inputs.buttonMinus = state.button_select;
|
||||
inputs.buttonPlus = state.button_start;
|
||||
inputs.buttonThumbR = state.button_right_stick;
|
||||
|
|
@ -764,7 +758,7 @@ static void update_switch_report_from_state(SwitchProContext& context) {
|
|||
inputs.buttonLeftSL = 0;
|
||||
inputs.buttonL = state.button_left_shoulder;
|
||||
inputs.buttonZL =
|
||||
state.left_trigger >= context.left_trigger_threshold;
|
||||
state.left_trigger >= SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
|
||||
uint16_t left_x =
|
||||
scale16To12(controller_axis_to_unsigned(state.left_stick_x));
|
||||
|
|
@ -819,14 +813,10 @@ void switch_pro_init(uint8_t instance) {
|
|||
to_ms_since_boot(get_absolute_time()), true);
|
||||
}
|
||||
|
||||
void switch_pro_set_input(uint8_t instance, const ControllerState& state,
|
||||
uint16_t left_trigger_threshold,
|
||||
uint16_t right_trigger_threshold) {
|
||||
void switch_pro_set_input(uint8_t instance, const ControllerState& state) {
|
||||
SwitchProContext* context = context_for(instance);
|
||||
if (context != nullptr) {
|
||||
context->input_state = state;
|
||||
context->left_trigger_threshold = left_trigger_threshold;
|
||||
context->right_trigger_threshold = right_trigger_threshold;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,11 +27,8 @@ constexpr uint16_t SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD =
|
|||
// Initialize one HID instance before entering the main loop.
|
||||
void switch_pro_init(uint8_t instance);
|
||||
|
||||
// Update the desired controller state and digital trigger thresholds for one
|
||||
// HID instance.
|
||||
void switch_pro_set_input(uint8_t instance, const ControllerState& state,
|
||||
uint16_t left_trigger_threshold,
|
||||
uint16_t right_trigger_threshold);
|
||||
// Update the desired controller state for one HID instance.
|
||||
void switch_pro_set_input(uint8_t instance, const ControllerState& state);
|
||||
|
||||
// Drive one Switch Pro USB state machine; returns true only when a regular
|
||||
// 0x30 input report was successfully queued.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,6 @@
|
|||
struct btstack_timer_source_t {
|
||||
void (*handler)(btstack_timer_source_t*);
|
||||
uint32_t timeout_ms;
|
||||
uint32_t add_count;
|
||||
};
|
||||
|
||||
inline void btstack_run_loop_set_timer_handler(
|
||||
|
|
@ -19,8 +18,6 @@ inline void btstack_run_loop_set_timer(btstack_timer_source_t* timer,
|
|||
timer->timeout_ms = timeout_ms;
|
||||
}
|
||||
|
||||
inline void btstack_run_loop_add_timer(btstack_timer_source_t* timer) {
|
||||
++timer->add_count;
|
||||
}
|
||||
inline void btstack_run_loop_add_timer(btstack_timer_source_t*) {}
|
||||
uint32_t btstack_run_loop_get_time_ms();
|
||||
inline void btstack_run_loop_execute() {}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ typedef uint8_t bd_addr_t[6];
|
|||
typedef uint8_t link_key_t[16];
|
||||
typedef uint8_t sm_key_t[16];
|
||||
typedef int link_key_type_t;
|
||||
typedef uint16_t hci_con_handle_t;
|
||||
|
||||
enum bd_addr_type_t {
|
||||
BD_ADDR_TYPE_LE_PUBLIC = 0,
|
||||
|
|
@ -21,13 +20,6 @@ enum hci_link_type_t {
|
|||
HCI_LINK_TYPE_ACL = 1,
|
||||
};
|
||||
|
||||
enum gap_connection_type_t {
|
||||
GAP_CONNECTION_INVALID,
|
||||
GAP_CONNECTION_ACL,
|
||||
GAP_CONNECTION_SCO,
|
||||
GAP_CONNECTION_LE,
|
||||
};
|
||||
|
||||
struct btstack_link_key_iterator_t {
|
||||
int index;
|
||||
};
|
||||
|
|
@ -37,12 +29,6 @@ enum {
|
|||
HCI_EVENT_PACKET = 4,
|
||||
HCI_EVENT_USER_CONFIRMATION_REQUEST = 0x33,
|
||||
HCI_EVENT_USER_PASSKEY_REQUEST = 0x34,
|
||||
SM_EVENT_IDENTITY_RESOLVING_STARTED = 0xcd,
|
||||
SM_EVENT_IDENTITY_RESOLVING_FAILED = 0xce,
|
||||
SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED = 0xcf,
|
||||
SM_EVENT_IDENTITY_CREATED = 0xd3,
|
||||
SM_EVENT_REENCRYPTION_STARTED = 0xd6,
|
||||
SM_EVENT_REENCRYPTION_COMPLETE = 0xd7,
|
||||
SM_STK_GENERATION_METHOD_JUST_WORKS = 0x01,
|
||||
SM_STK_GENERATION_METHOD_OOB = 0x02,
|
||||
SM_STK_GENERATION_METHOD_PASSKEY = 0x04,
|
||||
|
|
@ -128,13 +114,10 @@ enum uni_bt_conn_protocol_t {
|
|||
|
||||
struct uni_bt_conn_t {
|
||||
bd_addr_t btaddr;
|
||||
hci_con_handle_t handle;
|
||||
uni_bt_conn_protocol_t protocol;
|
||||
};
|
||||
|
||||
struct uni_hid_device_t {
|
||||
uint16_t vendor_id;
|
||||
uint16_t product_id;
|
||||
uni_bt_conn_t conn;
|
||||
int idx;
|
||||
bool gamepad;
|
||||
|
|
@ -171,8 +154,6 @@ struct uni_platform {
|
|||
bool uni_hid_device_is_gamepad(const uni_hid_device_t* device);
|
||||
int uni_hid_device_get_idx_for_instance(const uni_hid_device_t* device);
|
||||
void uni_hid_device_disconnect(uni_hid_device_t* device);
|
||||
uni_hid_device_t* uni_hid_device_get_instance_for_connection_handle(
|
||||
hci_con_handle_t handle);
|
||||
void uni_bt_allow_incoming_connections(bool enabled);
|
||||
void uni_bt_start_scanning_and_autoconnect_unsafe();
|
||||
void uni_bt_stop_scanning_unsafe();
|
||||
|
|
@ -181,8 +162,6 @@ void uni_bt_bredr_scan_stop();
|
|||
void uni_bt_le_scan_start();
|
||||
void uni_bt_le_scan_stop();
|
||||
void uni_bt_del_keys_unsafe();
|
||||
gap_connection_type_t gap_get_connection_type(
|
||||
hci_con_handle_t connection_handle);
|
||||
int gap_link_key_iterator_init(btstack_link_key_iterator_t* iterator);
|
||||
int gap_link_key_iterator_get_next(
|
||||
btstack_link_key_iterator_t* iterator, bd_addr_t address,
|
||||
|
|
@ -202,48 +181,10 @@ int gap_ssp_passkey_response(const bd_addr_t address, uint32_t passkey);
|
|||
int gap_ssp_passkey_negative(const bd_addr_t address);
|
||||
void hci_add_event_handler(
|
||||
btstack_packet_callback_registration_t* callback_handler);
|
||||
void sm_add_event_handler(
|
||||
btstack_packet_callback_registration_t* callback_handler);
|
||||
uint8_t hci_event_packet_get_type(const uint8_t* packet);
|
||||
void hci_event_user_confirmation_request_get_bd_addr(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
void hci_event_user_passkey_request_get_bd_addr(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
hci_con_handle_t sm_event_identity_resolving_started_get_handle(
|
||||
const uint8_t* packet);
|
||||
hci_con_handle_t sm_event_identity_resolving_failed_get_handle(
|
||||
const uint8_t* packet);
|
||||
hci_con_handle_t sm_event_identity_resolving_succeeded_get_handle(
|
||||
const uint8_t* packet);
|
||||
uint8_t sm_event_identity_resolving_succeeded_get_addr_type(
|
||||
const uint8_t* packet);
|
||||
void sm_event_identity_resolving_succeeded_get_address(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
uint8_t sm_event_identity_resolving_succeeded_get_identity_addr_type(
|
||||
const uint8_t* packet);
|
||||
void sm_event_identity_resolving_succeeded_get_identity_address(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
hci_con_handle_t sm_event_identity_created_get_handle(
|
||||
const uint8_t* packet);
|
||||
void sm_event_identity_created_get_address(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
uint8_t sm_event_identity_created_get_identity_addr_type(
|
||||
const uint8_t* packet);
|
||||
void sm_event_identity_created_get_identity_address(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
hci_con_handle_t sm_event_reencryption_started_get_handle(
|
||||
const uint8_t* packet);
|
||||
uint8_t sm_event_reencryption_started_get_addr_type(
|
||||
const uint8_t* packet);
|
||||
void sm_event_reencryption_started_get_address(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
hci_con_handle_t sm_event_reencryption_complete_get_handle(
|
||||
const uint8_t* packet);
|
||||
uint8_t sm_event_reencryption_complete_get_addr_type(
|
||||
const uint8_t* packet);
|
||||
void sm_event_reencryption_complete_get_address(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
uint8_t sm_event_reencryption_complete_get_status(
|
||||
const uint8_t* packet);
|
||||
void uni_platform_set_custom(uni_platform* platform);
|
||||
int uni_init(int argc, const char** argv);
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_profile.h"
|
||||
|
||||
constexpr uint8_t kLegacyDefaultProfile[CONTROLLER_PROFILE_ENCODED_SIZE] = {
|
||||
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
|
||||
0x0c, 0x0d, 0x0e, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||
0xff, 0xff, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0xff, 0xff,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
constexpr uint8_t
|
||||
kLegacyNarrowRawRangeProfile[CONTROLLER_PROFILE_ENCODED_SIZE] = {
|
||||
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
|
||||
0x0c, 0x0d, 0x0e, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x30, 0x75, 0x40, 0x9c, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x30, 0x75,
|
||||
0x40, 0x9c, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0xff, 0xff,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
constexpr uint8_t kLegacyCustomThresholdProfile[CONTROLLER_PROFILE_ENCODED_SIZE] = {
|
||||
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
|
||||
0x0c, 0x0d, 0x0e, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00,
|
||||
0xff, 0xff, 0x00, 0x01, 0xcd, 0xab, 0x00, 0x00, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0xff, 0xff,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
|
@ -1,898 +0,0 @@
|
|||
#include "controller_profile_runtime.h"
|
||||
|
||||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
#include "profile_service.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
struct FakeProfileRow {
|
||||
ControllerIdentity identity{};
|
||||
uint8_t active_profile = 0;
|
||||
ControllerProfile profiles[CONTROLLER_PROFILE_COUNT]{};
|
||||
};
|
||||
struct ActivationAttempt {
|
||||
uint32_t transaction_id = 0;
|
||||
ControllerIdentity identity{};
|
||||
uint8_t profile_index = 0;
|
||||
};
|
||||
|
||||
|
||||
std::array<FakeProfileRow, CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT> rows{};
|
||||
uint32_t database_generation = 7;
|
||||
uint32_t configuration_reset_generation = 3;
|
||||
unsigned active_snapshot_count = 0;
|
||||
std::array<ActivationAttempt, 32> activation_attempts{};
|
||||
size_t activation_attempt_count = 0;
|
||||
unsigned activation_busy_attempts = 0;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
ControllerIdentity make_identity(uint8_t value) {
|
||||
ControllerIdentity identity{};
|
||||
identity.stable = true;
|
||||
identity.transport = ControllerTransport::kClassic;
|
||||
identity.address[0] = value;
|
||||
identity.address[5] = static_cast<uint8_t>(value + 0x40u);
|
||||
identity.vendor_id = static_cast<uint16_t>(0x1000u + value);
|
||||
identity.product_id = static_cast<uint16_t>(0x2000u + value);
|
||||
return identity;
|
||||
}
|
||||
|
||||
void prepare_profiles() {
|
||||
database_generation = 7;
|
||||
configuration_reset_generation = 3;
|
||||
active_snapshot_count = 0;
|
||||
activation_attempts = {};
|
||||
activation_attempt_count = 0;
|
||||
activation_busy_attempts = 0;
|
||||
for (uint8_t slot = 0;
|
||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||
FakeProfileRow& row = rows[slot];
|
||||
row = {};
|
||||
row.identity = make_identity(static_cast<uint8_t>(slot + 1u));
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
row.profiles[profile_index] =
|
||||
controller_profile_default(row.identity, profile_index);
|
||||
}
|
||||
row.profiles[0].triggers[0].digital_threshold =
|
||||
static_cast<uint16_t>(1000u + slot);
|
||||
row.profiles[0].triggers[1].digital_threshold =
|
||||
static_cast<uint16_t>(5000u + slot);
|
||||
row.profiles[0].confirmation_policy =
|
||||
slot == 0 ? ControllerProfileConfirmationPolicy::kLed
|
||||
: ControllerProfileConfirmationPolicy::kRumble;
|
||||
}
|
||||
rows[0].profiles[0].strong_rumble_scale = 0;
|
||||
rows[0].profiles[0].weak_rumble_scale = UINT8_MAX;
|
||||
rows[1].profiles[0].strong_rumble_scale = UINT8_MAX;
|
||||
rows[1].profiles[0].weak_rumble_scale = 0;
|
||||
controller_profile_runtime_reset();
|
||||
}
|
||||
|
||||
Bluepad32SlotSnapshot make_snapshot(uint8_t slot,
|
||||
uint32_t connection_generation = 1) {
|
||||
Bluepad32SlotSnapshot snapshot{};
|
||||
snapshot.active = true;
|
||||
snapshot.connection_generation = connection_generation;
|
||||
snapshot.identity = rows[slot].identity;
|
||||
snapshot.state = controller_neutral_state();
|
||||
return snapshot;
|
||||
}
|
||||
void apply_button_masks(uint16_t pre_hotkey_mask,
|
||||
uint16_t output_mask,
|
||||
Bluepad32SlotSnapshot* snapshot) {
|
||||
snapshot->pre_hotkey_button_mask = pre_hotkey_mask;
|
||||
snapshot->state = controller_neutral_state();
|
||||
controller_profile_apply_button_mask(output_mask, &snapshot->state);
|
||||
}
|
||||
void apply_button_mask(uint16_t mask, Bluepad32SlotSnapshot* snapshot) {
|
||||
apply_button_masks(mask, mask, snapshot);
|
||||
}
|
||||
|
||||
ControllerProfileRuntimeProfileChangeEvent take_profile_change(
|
||||
uint8_t slot, bool* available) {
|
||||
ControllerProfileRuntimeProfileChangeEvent event{};
|
||||
*available =
|
||||
controller_profile_runtime_take_profile_change(slot, &event);
|
||||
return event;
|
||||
}
|
||||
|
||||
ControllerProfileRuntimeProfileChangeEvent
|
||||
take_initial_profile_indication(uint8_t slot, bool* available) {
|
||||
ControllerProfileRuntimeProfileChangeEvent event{};
|
||||
*available =
|
||||
controller_profile_runtime_take_initial_profile_indication(
|
||||
slot, &event);
|
||||
return event;
|
||||
}
|
||||
|
||||
constexpr uint16_t logical_button_bit(
|
||||
ControllerProfileLogicalButton button) {
|
||||
return static_cast<uint16_t>(
|
||||
1u << static_cast<uint8_t>(button));
|
||||
}
|
||||
|
||||
|
||||
ControllerProfileTransformResult runtime_transform(
|
||||
uint8_t slot, const Bluepad32SlotSnapshot& snapshot,
|
||||
uint32_t now_ms = 0,
|
||||
AdapterUsbMode output_mode = AdapterUsbMode::kSwitchProbe) {
|
||||
return controller_profile_runtime_transform(slot, snapshot, now_ms,
|
||||
output_mode);
|
||||
}
|
||||
|
||||
bool motion_equal(const ControllerState& first,
|
||||
const ControllerState& second) {
|
||||
return first.motion_sample_count == second.motion_sample_count &&
|
||||
std::memcmp(first.motion_samples, second.motion_samples,
|
||||
sizeof(first.motion_samples)) == 0;
|
||||
}
|
||||
|
||||
void test_four_slot_cache_and_unchanged_generation() {
|
||||
prepare_profiles();
|
||||
std::array<Bluepad32SlotSnapshot,
|
||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
|
||||
snapshots{};
|
||||
std::array<ControllerProfileTransformResult,
|
||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
|
||||
transformed{};
|
||||
for (uint8_t slot = 0;
|
||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||
snapshots[slot] = make_snapshot(slot);
|
||||
transformed[slot] =
|
||||
runtime_transform(slot, snapshots[slot]);
|
||||
require(transformed[slot].left_trigger_digital_threshold ==
|
||||
static_cast<uint16_t>(1000u + slot) &&
|
||||
transformed[slot].right_trigger_digital_threshold ==
|
||||
static_cast<uint16_t>(5000u + slot),
|
||||
"a slot did not receive its own cached profile");
|
||||
}
|
||||
require(active_snapshot_count == CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT,
|
||||
"initial slot loads did not fetch exactly one profile each");
|
||||
|
||||
rows[0].profiles[0].triggers[0].digital_threshold = 65000;
|
||||
for (uint8_t slot = 0;
|
||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||
transformed[slot] =
|
||||
runtime_transform(slot, snapshots[slot]);
|
||||
}
|
||||
require(active_snapshot_count == CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT,
|
||||
"unchanged generations copied profiles on the report path");
|
||||
require(transformed[0].left_trigger_digital_threshold == 1000,
|
||||
"an unchanged generation bypassed the slot cache");
|
||||
|
||||
snapshots[2].connection_generation = 2;
|
||||
transformed[2] =
|
||||
runtime_transform(2, snapshots[2]);
|
||||
require(active_snapshot_count ==
|
||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT + 1 &&
|
||||
transformed[2].left_trigger_digital_threshold == 1002,
|
||||
"one slot connection generation did not refresh in isolation");
|
||||
|
||||
snapshots[3].identity = rows[1].identity;
|
||||
transformed[3] =
|
||||
runtime_transform(3, snapshots[3]);
|
||||
require(active_snapshot_count ==
|
||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT + 2 &&
|
||||
transformed[3].left_trigger_digital_threshold == 1001,
|
||||
"an exact identity change did not refresh only its slot");
|
||||
}
|
||||
|
||||
void test_activation_disconnect_and_default_preservation() {
|
||||
prepare_profiles();
|
||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||
snapshot.state.button_south = true;
|
||||
ControllerProfileTransformResult transformed =
|
||||
runtime_transform(0, snapshot);
|
||||
require(transformed.state.button_south &&
|
||||
transformed.left_trigger_digital_threshold == 1000,
|
||||
"initial active profile was not applied");
|
||||
|
||||
rows[0].active_profile = 1;
|
||||
rows[0].profiles[1].button_map[
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kSouth)] =
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kNorth);
|
||||
rows[0].profiles[1].triggers[0].digital_threshold = 12345;
|
||||
++database_generation;
|
||||
transformed = runtime_transform(0, snapshot);
|
||||
require(!transformed.state.button_south && transformed.state.button_north &&
|
||||
transformed.left_trigger_digital_threshold == 12345,
|
||||
"profile activation generation did not refresh the slot cache");
|
||||
|
||||
const unsigned reads_before_disconnect = active_snapshot_count;
|
||||
snapshot.active = false;
|
||||
transformed = runtime_transform(0, snapshot);
|
||||
require(!transformed.state.button_south && !transformed.state.button_north &&
|
||||
transformed.left_trigger_digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
active_snapshot_count == reads_before_disconnect,
|
||||
"disconnect did not neutralize output without fetching a profile");
|
||||
|
||||
transformed = runtime_transform(0, snapshot);
|
||||
require(active_snapshot_count == reads_before_disconnect,
|
||||
"an unchanged inactive slot repeatedly cleared or fetched state");
|
||||
snapshot.active = true;
|
||||
transformed = runtime_transform(0, snapshot);
|
||||
require(active_snapshot_count == reads_before_disconnect + 1 &&
|
||||
transformed.state.button_north,
|
||||
"reconnection did not reload a cleared slot cache");
|
||||
|
||||
Bluepad32SlotSnapshot default_snapshot = make_snapshot(2);
|
||||
rows[2].profiles[0] =
|
||||
controller_profile_default(rows[2].identity, 0);
|
||||
++database_generation;
|
||||
default_snapshot.state.button_east = true;
|
||||
default_snapshot.state.dpad_left = true;
|
||||
default_snapshot.state.left_trigger = 32123;
|
||||
default_snapshot.state.right_trigger = 54321;
|
||||
default_snapshot.state.left_stick_x = -12345;
|
||||
default_snapshot.state.right_stick_y = 23456;
|
||||
default_snapshot.state.motion_sample_count = 2;
|
||||
default_snapshot.state.motion_samples[0] = {1, 2, 3, 4, 5, 6};
|
||||
default_snapshot.state.motion_samples[1] = {-1, -2, -3, -4, -5, -6};
|
||||
transformed = runtime_transform(2, default_snapshot);
|
||||
require(transformed.state.button_east && transformed.state.dpad_left &&
|
||||
transformed.state.left_trigger == 32123 &&
|
||||
transformed.state.right_trigger == 54321 &&
|
||||
transformed.state.left_stick_x == -12345 &&
|
||||
transformed.state.right_stick_y == 23456 &&
|
||||
motion_equal(transformed.state, default_snapshot.state) &&
|
||||
transformed.left_trigger_digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
transformed.right_trigger_digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
"default runtime profile changed serializer input or motion");
|
||||
}
|
||||
|
||||
void test_analog_thresholds_rumble_and_local_confirmation() {
|
||||
prepare_profiles();
|
||||
Bluepad32SlotSnapshot first = make_snapshot(0);
|
||||
Bluepad32SlotSnapshot second = make_snapshot(1);
|
||||
first.state.left_trigger = second.state.left_trigger = 23456;
|
||||
first.state.right_trigger = second.state.right_trigger = 45678;
|
||||
const ControllerProfileTransformResult first_output =
|
||||
runtime_transform(0, first);
|
||||
const ControllerProfileTransformResult second_output =
|
||||
runtime_transform(1, second);
|
||||
require(first_output.state.left_trigger ==
|
||||
second_output.state.left_trigger &&
|
||||
first_output.state.right_trigger ==
|
||||
second_output.state.right_trigger &&
|
||||
first_output.left_trigger_digital_threshold !=
|
||||
second_output.left_trigger_digital_threshold,
|
||||
"digital thresholds changed XInput-visible analog trigger state");
|
||||
|
||||
const ControllerRumbleOutput host{91, 73};
|
||||
const ControllerRumbleOutput first_host =
|
||||
controller_profile_runtime_scale_host_rumble(0, first, host);
|
||||
const ControllerRumbleOutput second_host =
|
||||
controller_profile_runtime_scale_host_rumble(1, second, host);
|
||||
require(first_host.low_frequency_magnitude == 0 &&
|
||||
first_host.high_frequency_magnitude == 73 &&
|
||||
second_host.low_frequency_magnitude == 91 &&
|
||||
second_host.high_frequency_magnitude == 0,
|
||||
"host rumble was not scaled through each slot profile");
|
||||
|
||||
const ControllerProfileRuntimeLocalConfirmation confirmation =
|
||||
controller_profile_runtime_local_confirmation(0, first, host);
|
||||
require(confirmation.rumble.low_frequency_magnitude == 91 &&
|
||||
confirmation.rumble.high_frequency_magnitude == 73 &&
|
||||
confirmation.policy ==
|
||||
ControllerProfileConfirmationPolicy::kLed,
|
||||
"local confirmation was scaled or lost its profile policy");
|
||||
}
|
||||
|
||||
void test_initial_profile_indication_once_per_connection() {
|
||||
prepare_profiles();
|
||||
std::array<Bluepad32SlotSnapshot,
|
||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
|
||||
snapshots{};
|
||||
for (uint8_t slot = 0;
|
||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||
rows[slot].active_profile = slot;
|
||||
rows[slot].profiles[slot].confirmation_policy =
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed;
|
||||
snapshots[slot] = make_snapshot(slot);
|
||||
(void)runtime_transform(slot, snapshots[slot]);
|
||||
}
|
||||
|
||||
constexpr uint8_t kTakeOrder[CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT] = {
|
||||
2, 0, 3, 1};
|
||||
for (const uint8_t slot : kTakeOrder) {
|
||||
bool available = false;
|
||||
const ControllerProfileRuntimeProfileChangeEvent event =
|
||||
take_initial_profile_indication(slot, &available);
|
||||
require(available &&
|
||||
event.connection_generation == 1 &&
|
||||
event.database_generation == database_generation &&
|
||||
event.active_profile_number == slot + 1u &&
|
||||
event.policy ==
|
||||
ControllerProfileConfirmationPolicy::kLed,
|
||||
"initial profile 1..4 indication was not isolated, "
|
||||
"LED-only, or generation-bound");
|
||||
(void)take_initial_profile_indication(slot, &available);
|
||||
require(!available,
|
||||
"initial profile indication repeated without a new "
|
||||
"connection");
|
||||
(void)take_profile_change(slot, &available);
|
||||
require(!available,
|
||||
"initial profile resolution published switch feedback");
|
||||
}
|
||||
|
||||
++database_generation;
|
||||
for (uint8_t slot = 0;
|
||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||
(void)runtime_transform(slot, snapshots[slot]);
|
||||
bool available = true;
|
||||
(void)take_initial_profile_indication(slot, &available);
|
||||
require(!available,
|
||||
"database refresh repeated initial profile indication");
|
||||
}
|
||||
|
||||
rows[2].identity = controller_identity_global();
|
||||
snapshots[0].identity = controller_identity_global();
|
||||
snapshots[0].connection_generation = 2;
|
||||
(void)runtime_transform(0, snapshots[0]);
|
||||
bool available = false;
|
||||
const ControllerProfileRuntimeProfileChangeEvent unresolved_event =
|
||||
take_initial_profile_indication(0, &available);
|
||||
require(available &&
|
||||
unresolved_event.connection_generation == 2 &&
|
||||
unresolved_event.active_profile_number == 3,
|
||||
"first committed unresolved-identity profile was not "
|
||||
"indicated");
|
||||
|
||||
snapshots[0].identity = rows[1].identity;
|
||||
(void)runtime_transform(0, snapshots[0]);
|
||||
(void)take_initial_profile_indication(0, &available);
|
||||
require(!available,
|
||||
"identity promotion repeated initial profile indication");
|
||||
|
||||
snapshots[0].connection_generation = 3;
|
||||
(void)runtime_transform(0, snapshots[0]);
|
||||
const ControllerProfileRuntimeProfileChangeEvent reconnect_event =
|
||||
take_initial_profile_indication(0, &available);
|
||||
require(available &&
|
||||
reconnect_event.connection_generation == 3 &&
|
||||
reconnect_event.active_profile_number == 2 &&
|
||||
reconnect_event.policy ==
|
||||
ControllerProfileConfirmationPolicy::kLed,
|
||||
"true reconnection did not publish one fresh LED-only "
|
||||
"profile indication");
|
||||
|
||||
rows[1].profiles[1].confirmation_policy =
|
||||
ControllerProfileConfirmationPolicy::kRumble;
|
||||
snapshots[1].connection_generation = 2;
|
||||
(void)runtime_transform(1, snapshots[1]);
|
||||
(void)take_initial_profile_indication(1, &available);
|
||||
require(!available,
|
||||
"rumble-only initial policy disturbed controller feedback");
|
||||
|
||||
rows[3].profiles[3].confirmation_policy =
|
||||
ControllerProfileConfirmationPolicy::kNone;
|
||||
snapshots[3].connection_generation = 2;
|
||||
(void)runtime_transform(3, snapshots[3]);
|
||||
(void)take_initial_profile_indication(3, &available);
|
||||
require(!available,
|
||||
"disabled initial policy disturbed controller feedback");
|
||||
}
|
||||
|
||||
void test_default_switching_retry_commit_and_feedback() {
|
||||
prepare_profiles();
|
||||
ControllerProfile& initial_profile = rows[0].profiles[0];
|
||||
initial_profile.macro_trigger = static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kLeftShoulder);
|
||||
initial_profile.macro_step_count = 2;
|
||||
initial_profile.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
initial_profile.macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
initial_profile.macro_steps[0].duration_ms = 1000;
|
||||
initial_profile.macro_steps[0].output_button_mask =
|
||||
logical_button_bit(ControllerProfileLogicalButton::kNorth);
|
||||
initial_profile.macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kEnd;
|
||||
|
||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||
(void)runtime_transform(0, snapshot, 0);
|
||||
bool event_available = true;
|
||||
(void)take_profile_change(0, &event_available);
|
||||
require(!event_available,
|
||||
"initial profile load published confirmation feedback");
|
||||
|
||||
activation_busy_attempts = 2;
|
||||
const uint16_t held_mask = static_cast<uint16_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD |
|
||||
logical_button_bit(ControllerProfileLogicalButton::kSouth));
|
||||
apply_button_mask(held_mask, &snapshot);
|
||||
ControllerProfileTransformResult output =
|
||||
runtime_transform(0, snapshot, 1);
|
||||
require(activation_attempt_count == 1 &&
|
||||
rows[0].active_profile == 0 &&
|
||||
output.state.button_south &&
|
||||
!output.state.button_left_shoulder &&
|
||||
!output.state.button_right_shoulder &&
|
||||
!output.state.button_select &&
|
||||
!output.state.button_start &&
|
||||
!output.state.button_north,
|
||||
"default chord was not consumed before a busy activation and synthetic trigger");
|
||||
output = runtime_transform(0, snapshot, 2);
|
||||
output = runtime_transform(0, snapshot, 3);
|
||||
require(activation_attempt_count == 3 &&
|
||||
activation_attempts[0].transaction_id ==
|
||||
activation_attempts[1].transaction_id &&
|
||||
activation_attempts[1].transaction_id ==
|
||||
activation_attempts[2].transaction_id &&
|
||||
(activation_attempts[0].transaction_id & 0x80000000u) != 0 &&
|
||||
activation_attempts[0].transaction_id != 0 &&
|
||||
activation_attempts[0].profile_index == 1 &&
|
||||
output.state.button_south && !output.state.button_north,
|
||||
"held busy chord did not retry one internal activation transaction");
|
||||
|
||||
(void)runtime_transform(0, snapshot, 4);
|
||||
require(activation_attempt_count == 3,
|
||||
"accepted activation repeated while the chord remained held");
|
||||
(void)take_profile_change(0, &event_available);
|
||||
require(!event_available,
|
||||
"activation feedback was published before storage commit");
|
||||
|
||||
rows[0].active_profile = 1;
|
||||
rows[0].profiles[1].button_map[
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kSouth)] =
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kNorth);
|
||||
rows[0].profiles[1].confirmation_policy =
|
||||
ControllerProfileConfirmationPolicy::kLed;
|
||||
++database_generation;
|
||||
output = runtime_transform(0, snapshot, 5);
|
||||
const ControllerProfileRuntimeProfileChangeEvent event =
|
||||
take_profile_change(0, &event_available);
|
||||
require(event_available && event.connection_generation == 1 &&
|
||||
event.database_generation == database_generation &&
|
||||
event.active_profile_number == 2 &&
|
||||
event.policy == ControllerProfileConfirmationPolicy::kLed &&
|
||||
output.state.button_north &&
|
||||
!output.state.button_south &&
|
||||
!output.state.button_left_shoulder &&
|
||||
activation_attempt_count == 3,
|
||||
"committed activation did not cancel, transform, and publish exactly one event");
|
||||
(void)take_profile_change(0, &event_available);
|
||||
require(!event_available,
|
||||
"committed profile change feedback was published twice");
|
||||
|
||||
apply_button_mask(0, &snapshot);
|
||||
(void)runtime_transform(0, snapshot, 6);
|
||||
apply_button_mask(CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD,
|
||||
&snapshot);
|
||||
(void)runtime_transform(0, snapshot, 7);
|
||||
require(activation_attempt_count == 4 &&
|
||||
activation_attempts[3].profile_index == 2 &&
|
||||
activation_attempts[3].transaction_id !=
|
||||
activation_attempts[0].transaction_id &&
|
||||
(activation_attempts[3].transaction_id & 0x80000000u) != 0,
|
||||
"release did not re-arm one activation for the next profile");
|
||||
}
|
||||
|
||||
void test_identity_promotion_preserves_held_switching() {
|
||||
prepare_profiles();
|
||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||
snapshot.identity = controller_identity_global();
|
||||
apply_button_mask(
|
||||
CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD, &snapshot);
|
||||
|
||||
const ControllerProfileTransformResult unresolved =
|
||||
runtime_transform(0, snapshot, 0);
|
||||
require(activation_attempt_count == 1 &&
|
||||
controller_identity_is_global(
|
||||
activation_attempts[0].identity) &&
|
||||
unresolved.left_trigger_digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
"unresolved BLE identity did not begin one activation");
|
||||
|
||||
snapshot.identity = rows[0].identity;
|
||||
const ControllerProfileTransformResult promoted =
|
||||
runtime_transform(0, snapshot, 1);
|
||||
bool event_available = true;
|
||||
(void)take_profile_change(0, &event_available);
|
||||
require(activation_attempt_count == 1 &&
|
||||
promoted.left_trigger_digital_threshold == 1000 &&
|
||||
!event_available,
|
||||
"identity-only BLE promotion re-armed a held activation or "
|
||||
"failed to refresh its profile");
|
||||
|
||||
(void)runtime_transform(0, snapshot, 2);
|
||||
require(activation_attempt_count == 1,
|
||||
"promoted BLE identity repeated an accepted held activation");
|
||||
|
||||
apply_button_mask(0, &snapshot);
|
||||
(void)runtime_transform(0, snapshot, 3);
|
||||
apply_button_mask(
|
||||
CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD, &snapshot);
|
||||
(void)runtime_transform(0, snapshot, 4);
|
||||
require(activation_attempt_count == 2 &&
|
||||
controller_identity_equal(
|
||||
activation_attempts[1].identity,
|
||||
rows[0].identity),
|
||||
"physical release did not re-arm the promoted identity");
|
||||
}
|
||||
|
||||
void configure_motion_suppression_probe(ControllerProfile* profile) {
|
||||
profile->macro_trigger = static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kDpadUp);
|
||||
profile->macro_step_count = 2;
|
||||
profile->macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile->macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile->macro_steps[0].duration_ms = 1000;
|
||||
profile->macro_steps[0].output_button_mask =
|
||||
logical_button_bit(ControllerProfileLogicalButton::kNorth);
|
||||
profile->macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kEnd;
|
||||
}
|
||||
|
||||
void test_switching_uses_pre_hotkey_buttons_only() {
|
||||
constexpr uint16_t kMotionHotkeyLogicalMask =
|
||||
static_cast<uint16_t>(
|
||||
logical_button_bit(
|
||||
ControllerProfileLogicalButton::kDpadUp) |
|
||||
logical_button_bit(
|
||||
ControllerProfileLogicalButton::kRightShoulder) |
|
||||
logical_button_bit(
|
||||
ControllerProfileLogicalButton::kStart));
|
||||
constexpr uint16_t kCustomChord =
|
||||
static_cast<uint16_t>(
|
||||
kMotionHotkeyLogicalMask |
|
||||
logical_button_bit(
|
||||
ControllerProfileLogicalButton::kSouth));
|
||||
|
||||
for (uint8_t custom = 0; custom < 2; ++custom) {
|
||||
prepare_profiles();
|
||||
ControllerProfile& profile = rows[0].profiles[0];
|
||||
configure_motion_suppression_probe(&profile);
|
||||
const uint16_t switching_chord =
|
||||
custom != 0 ? kCustomChord
|
||||
: CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD;
|
||||
profile.switching_chord = custom != 0 ? kCustomChord : 0;
|
||||
|
||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||
(void)runtime_transform(0, snapshot, 0);
|
||||
const uint16_t pre_hotkey_mask = static_cast<uint16_t>(
|
||||
switching_chord | kMotionHotkeyLogicalMask);
|
||||
const uint16_t output_mask = static_cast<uint16_t>(
|
||||
pre_hotkey_mask & ~kMotionHotkeyLogicalMask);
|
||||
apply_button_masks(
|
||||
pre_hotkey_mask, output_mask, &snapshot);
|
||||
ControllerProfileTransformResult output =
|
||||
runtime_transform(0, snapshot, 1);
|
||||
require(activation_attempt_count == 1 &&
|
||||
!output.state.dpad_up &&
|
||||
!output.state.button_right_shoulder &&
|
||||
!output.state.button_start &&
|
||||
!output.state.button_left_shoulder &&
|
||||
!output.state.button_select &&
|
||||
!output.state.button_south &&
|
||||
!output.state.button_north,
|
||||
custom != 0
|
||||
? "custom chord hidden by motion suppression"
|
||||
: "default chord hidden by motion suppression");
|
||||
|
||||
output = runtime_transform(0, snapshot, 2);
|
||||
require(activation_attempt_count == 1 &&
|
||||
!output.state.button_north,
|
||||
"held pre-hotkey chord repeated activation or leaked "
|
||||
"into synthetic input");
|
||||
}
|
||||
}
|
||||
|
||||
void test_custom_switching_chord_and_wrap() {
|
||||
prepare_profiles();
|
||||
constexpr uint16_t kCustomChord =
|
||||
static_cast<uint16_t>(
|
||||
logical_button_bit(ControllerProfileLogicalButton::kSouth) |
|
||||
logical_button_bit(ControllerProfileLogicalButton::kCapture));
|
||||
rows[0].profiles[0].switching_chord = kCustomChord;
|
||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||
(void)runtime_transform(0, snapshot, 0);
|
||||
|
||||
apply_button_mask(CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD,
|
||||
&snapshot);
|
||||
ControllerProfileTransformResult output =
|
||||
runtime_transform(0, snapshot, 1);
|
||||
require(activation_attempt_count == 0 &&
|
||||
output.state.button_left_shoulder &&
|
||||
output.state.button_right_shoulder &&
|
||||
output.state.button_select && output.state.button_start,
|
||||
"nonzero switching chord did not replace the default");
|
||||
|
||||
apply_button_mask(kCustomChord, &snapshot);
|
||||
output = runtime_transform(0, snapshot, 2);
|
||||
require(activation_attempt_count == 1 &&
|
||||
activation_attempts[0].profile_index == 1 &&
|
||||
!output.state.button_south &&
|
||||
!output.state.button_capture,
|
||||
"custom switching chord was not consumed or activated");
|
||||
|
||||
prepare_profiles();
|
||||
rows[0].active_profile = 3;
|
||||
rows[0].profiles[3].switching_chord = kCustomChord;
|
||||
snapshot = make_snapshot(0);
|
||||
(void)runtime_transform(0, snapshot, 10);
|
||||
bool event_available = true;
|
||||
(void)take_profile_change(0, &event_available);
|
||||
require(!event_available,
|
||||
"initial profile 4 load published a change event");
|
||||
apply_button_mask(kCustomChord, &snapshot);
|
||||
(void)runtime_transform(0, snapshot, 11);
|
||||
require(activation_attempt_count == 1 &&
|
||||
activation_attempts[0].profile_index == 0,
|
||||
"profile switching did not wrap profile 4 to profile 1");
|
||||
}
|
||||
void test_switching_slot_isolation() {
|
||||
prepare_profiles();
|
||||
std::array<Bluepad32SlotSnapshot,
|
||||
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
|
||||
snapshots{};
|
||||
for (uint8_t slot = 0;
|
||||
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
|
||||
snapshots[slot] = make_snapshot(slot);
|
||||
(void)runtime_transform(slot, snapshots[slot], 0);
|
||||
}
|
||||
|
||||
apply_button_mask(CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD,
|
||||
&snapshots[2]);
|
||||
(void)runtime_transform(2, snapshots[2], 1);
|
||||
snapshots[0].state.button_south = true;
|
||||
const ControllerProfileTransformResult untouched =
|
||||
runtime_transform(0, snapshots[0], 1);
|
||||
require(activation_attempt_count == 1 &&
|
||||
controller_identity_equal(
|
||||
activation_attempts[0].identity,
|
||||
rows[2].identity) &&
|
||||
untouched.state.button_south,
|
||||
"one slot's switching chord affected another slot");
|
||||
|
||||
apply_button_mask(CONTROLLER_PROFILE_DEFAULT_SWITCHING_CHORD,
|
||||
&snapshots[3]);
|
||||
(void)runtime_transform(2, snapshots[2], 2);
|
||||
(void)runtime_transform(3, snapshots[3], 2);
|
||||
require(activation_attempt_count == 2 &&
|
||||
controller_identity_equal(
|
||||
activation_attempts[1].identity,
|
||||
rows[3].identity),
|
||||
"held switching state was shared between controller slots");
|
||||
}
|
||||
|
||||
|
||||
void configure_synthetic_profile(uint8_t slot) {
|
||||
ControllerProfile& profile = rows[slot].profiles[0];
|
||||
profile = controller_profile_default(rows[slot].identity, 0);
|
||||
profile.macro_trigger =
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kSouth);
|
||||
profile.macro_cancel =
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kCapture);
|
||||
profile.macro_step_count = 2;
|
||||
profile.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[0].duration_ms = 1000;
|
||||
profile.macro_steps[0].output_button_mask = static_cast<uint16_t>(
|
||||
1u << static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kNorth));
|
||||
profile.macro_steps[1] = {};
|
||||
profile.macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kEnd;
|
||||
profile.turbo_modes[static_cast<uint8_t>(
|
||||
ControllerProfileLogicalButton::kEast)] =
|
||||
ControllerProfileTurboMode::kAutoBurst;
|
||||
}
|
||||
|
||||
void start_macro(Bluepad32SlotSnapshot* snapshot, uint32_t now_ms,
|
||||
AdapterUsbMode mode = AdapterUsbMode::kSwitchProbe) {
|
||||
snapshot->state = controller_neutral_state();
|
||||
(void)runtime_transform(0, *snapshot, now_ms, mode);
|
||||
snapshot->state.button_south = true;
|
||||
const ControllerProfileTransformResult started =
|
||||
runtime_transform(0, *snapshot, now_ms + 1u, mode);
|
||||
require(started.state.button_north && !started.state.button_south,
|
||||
"runtime fixture did not start its macro");
|
||||
snapshot->state = controller_neutral_state();
|
||||
const ControllerProfileTransformResult held =
|
||||
runtime_transform(0, *snapshot, now_ms + 2u, mode);
|
||||
require(held.state.button_north,
|
||||
"runtime fixture macro did not remain active");
|
||||
}
|
||||
|
||||
void start_auto_burst(Bluepad32SlotSnapshot* snapshot, uint32_t now_ms,
|
||||
AdapterUsbMode mode =
|
||||
AdapterUsbMode::kSwitchProbe) {
|
||||
snapshot->state = controller_neutral_state();
|
||||
(void)runtime_transform(0, *snapshot, now_ms, mode);
|
||||
snapshot->state.button_east = true;
|
||||
const ControllerProfileTransformResult started =
|
||||
runtime_transform(0, *snapshot, now_ms + 1u, mode);
|
||||
require(started.state.button_east,
|
||||
"runtime fixture did not start Auto Burst");
|
||||
snapshot->state = controller_neutral_state();
|
||||
const ControllerProfileTransformResult latched =
|
||||
runtime_transform(0, *snapshot, now_ms + 2u, mode);
|
||||
require(latched.state.button_east,
|
||||
"runtime fixture Auto Burst did not latch");
|
||||
}
|
||||
|
||||
void require_no_synthetic_output(
|
||||
const ControllerProfileTransformResult& output,
|
||||
const char* message) {
|
||||
require(!output.state.button_north && !output.state.button_east &&
|
||||
!output.state.button_south &&
|
||||
!output.state.button_capture,
|
||||
message);
|
||||
}
|
||||
|
||||
void test_all_runtime_cancellation_causes() {
|
||||
prepare_profiles();
|
||||
configure_synthetic_profile(0);
|
||||
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
|
||||
start_macro(&snapshot, 10);
|
||||
snapshot.active = false;
|
||||
ControllerProfileTransformResult output =
|
||||
runtime_transform(0, snapshot, 13);
|
||||
require_no_synthetic_output(
|
||||
output, "disconnect did not cancel synthetic output");
|
||||
snapshot.active = true;
|
||||
output = runtime_transform(0, snapshot, 14);
|
||||
require_no_synthetic_output(
|
||||
output, "reconnection restored stale synthetic output");
|
||||
|
||||
prepare_profiles();
|
||||
configure_synthetic_profile(0);
|
||||
snapshot = make_snapshot(0);
|
||||
start_macro(&snapshot, 20);
|
||||
++snapshot.connection_generation;
|
||||
output = runtime_transform(0, snapshot, 23);
|
||||
require_no_synthetic_output(
|
||||
output, "connection replacement did not cancel before output");
|
||||
|
||||
prepare_profiles();
|
||||
configure_synthetic_profile(0);
|
||||
snapshot = make_snapshot(0);
|
||||
start_macro(&snapshot, 30);
|
||||
rows[0].active_profile = 1;
|
||||
++database_generation;
|
||||
output = runtime_transform(0, snapshot, 33);
|
||||
require_no_synthetic_output(
|
||||
output, "profile/database generation change did not cancel");
|
||||
bool event_available = false;
|
||||
const ControllerProfileRuntimeProfileChangeEvent cancellation_event =
|
||||
take_profile_change(0, &event_available);
|
||||
require(event_available &&
|
||||
cancellation_event.active_profile_number == 2,
|
||||
"profile commit feedback was not observed with cancellation");
|
||||
|
||||
prepare_profiles();
|
||||
configure_synthetic_profile(0);
|
||||
snapshot = make_snapshot(0);
|
||||
start_auto_burst(&snapshot, 40);
|
||||
output = runtime_transform(0, snapshot, 43,
|
||||
AdapterUsbMode::kXInput);
|
||||
require_no_synthetic_output(
|
||||
output, "output-mode change did not cancel Auto Burst");
|
||||
output = runtime_transform(0, snapshot, 44,
|
||||
AdapterUsbMode::kXInput);
|
||||
require_no_synthetic_output(
|
||||
output, "output-mode cancellation left a stuck output");
|
||||
|
||||
prepare_profiles();
|
||||
configure_synthetic_profile(0);
|
||||
snapshot = make_snapshot(0);
|
||||
start_auto_burst(&snapshot, 50);
|
||||
++configuration_reset_generation;
|
||||
output = runtime_transform(0, snapshot, 53);
|
||||
require_no_synthetic_output(
|
||||
output, "configuration reset did not cancel Auto Burst");
|
||||
output = runtime_transform(0, snapshot, 54);
|
||||
require_no_synthetic_output(
|
||||
output, "configuration reset cancellation left a stuck output");
|
||||
}
|
||||
|
||||
void test_runtime_slot_synthetic_isolation() {
|
||||
prepare_profiles();
|
||||
configure_synthetic_profile(0);
|
||||
configure_synthetic_profile(1);
|
||||
Bluepad32SlotSnapshot first = make_snapshot(0);
|
||||
Bluepad32SlotSnapshot second = make_snapshot(1);
|
||||
(void)runtime_transform(0, first, 0);
|
||||
(void)runtime_transform(1, second, 0);
|
||||
first.state.button_east = true;
|
||||
second.state.button_east = true;
|
||||
require(runtime_transform(0, first, 1).state.button_east &&
|
||||
runtime_transform(1, second, 1).state.button_east,
|
||||
"runtime slots did not activate independently");
|
||||
first.state = controller_neutral_state();
|
||||
second.state = controller_neutral_state();
|
||||
require(runtime_transform(0, first, 2).state.button_east &&
|
||||
runtime_transform(1, second, 2).state.button_east,
|
||||
"runtime Auto Burst state did not remain isolated");
|
||||
|
||||
first.state.button_capture = true;
|
||||
const ControllerProfileTransformResult cancelled =
|
||||
runtime_transform(0, first, 3);
|
||||
const ControllerProfileTransformResult untouched =
|
||||
runtime_transform(1, second, 3);
|
||||
require_no_synthetic_output(
|
||||
cancelled, "slot-local configured cancel did not clear its output");
|
||||
require(untouched.state.button_east,
|
||||
"slot-local configured cancel affected another slot");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t profile_service_database_generation() {
|
||||
return database_generation;
|
||||
}
|
||||
|
||||
uint32_t configuration_service_reset_generation() {
|
||||
return configuration_reset_generation;
|
||||
}
|
||||
ConfigurationTransactionStatus profile_service_activate_internal(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
require(activation_attempt_count < activation_attempts.size(),
|
||||
"activation attempt fixture overflow");
|
||||
activation_attempts[activation_attempt_count++] = {
|
||||
transaction_id, identity, profile_index};
|
||||
if (activation_busy_attempts != 0) {
|
||||
--activation_busy_attempts;
|
||||
return ConfigurationTransactionStatus::kBusy;
|
||||
}
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
|
||||
void profile_service_active_profile_snapshot(
|
||||
const ControllerIdentity& identity,
|
||||
ProfileServiceActiveProfileSnapshot* output) {
|
||||
if (output == nullptr) {
|
||||
return;
|
||||
}
|
||||
++active_snapshot_count;
|
||||
*output = {};
|
||||
output->metadata.state = ProfileServiceState::kReady;
|
||||
output->metadata.generation = database_generation;
|
||||
for (const FakeProfileRow& row : rows) {
|
||||
if (!controller_identity_equal(row.identity, identity)) {
|
||||
continue;
|
||||
}
|
||||
output->valid = true;
|
||||
output->profile_index = row.active_profile;
|
||||
output->profile = row.profiles[row.active_profile];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_four_slot_cache_and_unchanged_generation();
|
||||
test_activation_disconnect_and_default_preservation();
|
||||
test_analog_thresholds_rumble_and_local_confirmation();
|
||||
test_initial_profile_indication_once_per_connection();
|
||||
test_default_switching_retry_commit_and_feedback();
|
||||
test_identity_promotion_preserves_held_switching();
|
||||
test_switching_uses_pre_hotkey_buttons_only();
|
||||
test_custom_switching_chord_and_wrap();
|
||||
test_switching_slot_isolation();
|
||||
test_all_runtime_cancellation_causes();
|
||||
test_runtime_slot_synthetic_isolation();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
#include "tests/controller_profile_legacy_fixtures.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
uint8_t encoded_database[CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE]{};
|
||||
ControllerProfileDatabase database{};
|
||||
ControllerProfileDatabase decoded_database{};
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
ControllerIdentity identity(uint8_t suffix) {
|
||||
ControllerIdentity value{};
|
||||
value.stable = true;
|
||||
value.transport = ControllerTransport::kClassic;
|
||||
value.address[5] = suffix;
|
||||
value.vendor_id = 0x057e;
|
||||
value.product_id = static_cast<uint16_t>(0x2000u + suffix);
|
||||
return value;
|
||||
}
|
||||
|
||||
bool read_encoded_database(void*, size_t offset, uint8_t* output,
|
||||
size_t size) {
|
||||
if (offset > sizeof(encoded_database) ||
|
||||
size > sizeof(encoded_database) - offset) {
|
||||
return false;
|
||||
}
|
||||
memcpy(output, &encoded_database[offset], size);
|
||||
return true;
|
||||
}
|
||||
|
||||
void test_profile_wire_schema() {
|
||||
const ControllerProfile profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)),
|
||||
"default profile did not encode");
|
||||
require(encoded[0] == 2 && encoded[1] == 0 &&
|
||||
encoded[2] == 0 && encoded[3] == 1,
|
||||
"profile header is not little-endian v2/256");
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
|
||||
require(encoded[4 + index] == index,
|
||||
"default direct mapping is not identity");
|
||||
}
|
||||
require(encoded[26] == 0xff && encoded[27] == 0x7f &&
|
||||
encoded[30] == 0,
|
||||
"default stick encoding changed");
|
||||
require(encoded[54] == 0xff && encoded[55] == 0xff &&
|
||||
encoded[58] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD) &&
|
||||
encoded[59] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8),
|
||||
"default trigger encoding changed");
|
||||
require(encoded[72] == 0xff && encoded[73] == 0xff &&
|
||||
encoded[74] == 3 && encoded[78] == 0xff &&
|
||||
encoded[79] == 0xff && encoded[80] == 1,
|
||||
"default rumble or macro encoding changed");
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_MACRO_STEP_CAPACITY; ++index) {
|
||||
require(encoded[100 + index * 19] == 1,
|
||||
"unused macro step is not canonical end");
|
||||
}
|
||||
|
||||
ControllerProfile decoded{};
|
||||
require(controller_profile_decode(encoded, sizeof(encoded), &decoded),
|
||||
"default profile did not decode");
|
||||
encoded[252] = 1;
|
||||
require(!controller_profile_decode(encoded, sizeof(encoded), &decoded),
|
||||
"nonzero reserved profile byte was accepted");
|
||||
|
||||
ControllerProfile invalid = profile;
|
||||
invalid.button_map[0] = 16;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"invalid direct output was accepted");
|
||||
invalid = profile;
|
||||
invalid.sticks[0].inner_deadzone =
|
||||
invalid.sticks[0].outer_saturation;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"empty stick range was accepted");
|
||||
ControllerProfile boundary = profile;
|
||||
boundary.triggers[0].lower_deadzone = 30000;
|
||||
boundary.triggers[0].upper_saturation = 40000;
|
||||
boundary.triggers[0].digital_threshold = 0;
|
||||
uint8_t boundary_encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(boundary, boundary_encoded,
|
||||
sizeof(boundary_encoded)) &&
|
||||
controller_profile_decode(boundary_encoded,
|
||||
sizeof(boundary_encoded),
|
||||
&decoded) &&
|
||||
decoded.triggers[0].digital_threshold == 0,
|
||||
"current profile rejected zero transformed trigger threshold");
|
||||
boundary.triggers[0].digital_threshold = UINT16_MAX;
|
||||
require(controller_profile_encode(boundary, boundary_encoded,
|
||||
sizeof(boundary_encoded)) &&
|
||||
controller_profile_decode(boundary_encoded,
|
||||
sizeof(boundary_encoded),
|
||||
&decoded) &&
|
||||
decoded.triggers[0].digital_threshold == UINT16_MAX,
|
||||
"current profile rejected maximum transformed trigger threshold");
|
||||
invalid = profile;
|
||||
invalid.triggers[0].lower_deadzone =
|
||||
invalid.triggers[0].upper_saturation;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"empty raw trigger range was accepted");
|
||||
invalid.triggers[0].lower_deadzone = UINT16_MAX;
|
||||
invalid.triggers[0].upper_saturation = UINT16_MAX - 1;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"reversed raw trigger range was accepted");
|
||||
invalid = profile;
|
||||
invalid.turbo_modes[0] =
|
||||
static_cast<ControllerProfileTurboMode>(3);
|
||||
require(!controller_profile_validate(invalid),
|
||||
"invalid Turbo mode was accepted");
|
||||
invalid = profile;
|
||||
invalid.macro_step_count = 2;
|
||||
invalid.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
invalid.macro_steps[0].duration_ms =
|
||||
CONTROLLER_PROFILE_MAX_WAIT_MS + 1;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"unbounded macro wait was accepted");
|
||||
invalid.macro_steps[0].duration_ms =
|
||||
CONTROLLER_PROFILE_MAX_WAIT_MS;
|
||||
invalid.macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"macro without a final end was accepted");
|
||||
}
|
||||
|
||||
void test_legacy_profile_migration() {
|
||||
ControllerProfile migrated{};
|
||||
require(controller_profile_decode(
|
||||
kLegacyDefaultProfile, sizeof(kLegacyDefaultProfile),
|
||||
&migrated),
|
||||
"legacy default profile did not decode");
|
||||
require(migrated.triggers[0].digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
migrated.triggers[1].digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
"legacy inherited thresholds were not migrated");
|
||||
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(migrated, encoded, sizeof(encoded)),
|
||||
"migrated default profile did not encode");
|
||||
for (size_t index = 0; index < sizeof(encoded); ++index) {
|
||||
const bool schema_byte = index == 0;
|
||||
const bool threshold_byte =
|
||||
(index >= 58 && index < 60) ||
|
||||
(index >= 68 && index < 70);
|
||||
if (!schema_byte && !threshold_byte) {
|
||||
require(encoded[index] == kLegacyDefaultProfile[index],
|
||||
"legacy default profile changed an unrelated byte");
|
||||
}
|
||||
}
|
||||
require(encoded[0] == CONTROLLER_PROFILE_SCHEMA_VERSION &&
|
||||
encoded[58] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD) &&
|
||||
encoded[59] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8) &&
|
||||
encoded[68] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD) &&
|
||||
encoded[69] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8),
|
||||
"migrated default profile did not encode as v2");
|
||||
|
||||
require(controller_profile_decode(
|
||||
kLegacyNarrowRawRangeProfile,
|
||||
sizeof(kLegacyNarrowRawRangeProfile), &migrated),
|
||||
"legacy narrow-raw-range profile did not decode");
|
||||
require(migrated.triggers[0].lower_deadzone == 30000 &&
|
||||
migrated.triggers[0].upper_saturation == 40000 &&
|
||||
migrated.triggers[0].digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
migrated.triggers[1].lower_deadzone == 30000 &&
|
||||
migrated.triggers[1].upper_saturation == 40000 &&
|
||||
migrated.triggers[1].digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
"legacy narrow raw range or inherited threshold was not migrated");
|
||||
require(controller_profile_encode(migrated, encoded, sizeof(encoded)),
|
||||
"migrated narrow-raw-range profile did not encode");
|
||||
for (size_t index = 0; index < sizeof(encoded); ++index) {
|
||||
const bool schema_byte = index == 0;
|
||||
const bool threshold_byte =
|
||||
(index >= 58 && index < 60) ||
|
||||
(index >= 68 && index < 70);
|
||||
if (!schema_byte && !threshold_byte) {
|
||||
require(
|
||||
encoded[index] == kLegacyNarrowRawRangeProfile[index],
|
||||
"narrow-raw-range migration changed unrelated profile data");
|
||||
}
|
||||
}
|
||||
|
||||
require(controller_profile_decode(
|
||||
kLegacyCustomThresholdProfile,
|
||||
sizeof(kLegacyCustomThresholdProfile), &migrated),
|
||||
"legacy custom-threshold profile did not decode");
|
||||
require(migrated.triggers[0].digital_threshold == 0x1234 &&
|
||||
migrated.triggers[1].digital_threshold == 0xabcd,
|
||||
"legacy custom thresholds were not preserved");
|
||||
require(controller_profile_encode(migrated, encoded, sizeof(encoded)),
|
||||
"legacy custom-threshold profile did not re-encode");
|
||||
for (size_t index = 1; index < sizeof(encoded); ++index) {
|
||||
require(encoded[index] == kLegacyCustomThresholdProfile[index],
|
||||
"legacy custom-threshold profile changed data");
|
||||
}
|
||||
|
||||
ControllerProfile current =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
current.triggers[0].digital_threshold = 0x8000;
|
||||
require(controller_profile_encode(current, encoded, sizeof(encoded)) &&
|
||||
controller_profile_decode(encoded, sizeof(encoded),
|
||||
&migrated) &&
|
||||
migrated.triggers[0].digital_threshold == 0x8000,
|
||||
"v2 custom threshold matching the legacy default was migrated");
|
||||
}
|
||||
|
||||
void test_database_round_trip_and_capacity() {
|
||||
controller_profile_database_default(&database);
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY; ++index) {
|
||||
ControllerProfileDatabaseEntry* entry = nullptr;
|
||||
require(controller_profile_database_ensure(
|
||||
&database, identity(static_cast<uint8_t>(index + 1)),
|
||||
&entry) == ControllerProfileDatabaseResult::kOk &&
|
||||
entry != nullptr,
|
||||
"stable identity was not added");
|
||||
entry->active_profile = index % CONTROLLER_PROFILE_COUNT;
|
||||
}
|
||||
ControllerProfileDatabaseEntry* rejected = nullptr;
|
||||
require(controller_profile_database_ensure(
|
||||
&database, identity(99), &rejected) ==
|
||||
ControllerProfileDatabaseResult::kFull,
|
||||
"seventeenth stable identity was not rejected");
|
||||
require(controller_profile_database_find(database, identity(1)) !=
|
||||
nullptr,
|
||||
"full-table rejection evicted an existing identity");
|
||||
|
||||
for (size_t offset = 0; offset < sizeof(encoded_database);
|
||||
offset += CONTROLLER_PROFILE_ENCODED_SIZE) {
|
||||
const size_t size = sizeof(encoded_database) - offset <
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE
|
||||
? sizeof(encoded_database) - offset
|
||||
: CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
require(controller_profile_database_encode_range(
|
||||
database, offset, &encoded_database[offset], size),
|
||||
"database range did not encode");
|
||||
}
|
||||
require(encoded_database[4] ==
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION &&
|
||||
encoded_database[5] == 0,
|
||||
"database encoder did not emit v2");
|
||||
require(controller_profile_database_decode(
|
||||
read_encoded_database, nullptr, &decoded_database),
|
||||
"database did not decode");
|
||||
require(controller_profile_database_find(
|
||||
decoded_database, identity(16)) != nullptr,
|
||||
"last database identity did not round trip");
|
||||
|
||||
encoded_database[12] = 1;
|
||||
require(!controller_profile_database_decode(
|
||||
read_encoded_database, nullptr, &decoded_database),
|
||||
"nonzero database header reservation was accepted");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
int main() {
|
||||
test_profile_wire_schema();
|
||||
test_legacy_profile_migration();
|
||||
test_database_round_trip_and_capacity();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,385 +0,0 @@
|
|||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
#include "controller_profile_transform.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <limits.h>
|
||||
|
||||
namespace {
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
ControllerProfile default_profile() {
|
||||
return controller_profile_default(controller_identity_global(), 0);
|
||||
}
|
||||
|
||||
bool states_equal(const ControllerState& left, const ControllerState& right) {
|
||||
return left.dpad_up == right.dpad_up &&
|
||||
left.dpad_down == right.dpad_down &&
|
||||
left.dpad_left == right.dpad_left &&
|
||||
left.dpad_right == right.dpad_right &&
|
||||
left.button_south == right.button_south &&
|
||||
left.button_east == right.button_east &&
|
||||
left.button_west == right.button_west &&
|
||||
left.button_north == right.button_north &&
|
||||
left.button_left_shoulder == right.button_left_shoulder &&
|
||||
left.button_right_shoulder == right.button_right_shoulder &&
|
||||
left.button_select == right.button_select &&
|
||||
left.button_start == right.button_start &&
|
||||
left.button_system == right.button_system &&
|
||||
left.button_capture == right.button_capture &&
|
||||
left.button_left_stick == right.button_left_stick &&
|
||||
left.button_right_stick == right.button_right_stick &&
|
||||
left.left_trigger == right.left_trigger &&
|
||||
left.right_trigger == right.right_trigger &&
|
||||
left.left_stick_x == right.left_stick_x &&
|
||||
left.left_stick_y == right.left_stick_y &&
|
||||
left.right_stick_x == right.right_stick_x &&
|
||||
left.right_stick_y == right.right_stick_y &&
|
||||
left.motion_sample_count == right.motion_sample_count &&
|
||||
std::memcmp(left.motion_samples, right.motion_samples,
|
||||
sizeof(left.motion_samples)) == 0;
|
||||
}
|
||||
|
||||
ControllerProfileTransformResult transform_left_stick(
|
||||
const ControllerProfileStickConfiguration& configuration, int16_t x,
|
||||
int16_t y) {
|
||||
ControllerProfile profile = default_profile();
|
||||
profile.sticks[0] = configuration;
|
||||
ControllerState state{};
|
||||
state.left_stick_x = x;
|
||||
state.left_stick_y = y;
|
||||
return controller_profile_transform(state, profile);
|
||||
}
|
||||
|
||||
uint16_t transform_left_trigger(
|
||||
const ControllerProfileTriggerConfiguration& configuration,
|
||||
uint16_t value) {
|
||||
ControllerProfile profile = default_profile();
|
||||
profile.triggers[0] = configuration;
|
||||
ControllerState state{};
|
||||
state.left_trigger = value;
|
||||
return controller_profile_transform(state, profile).state.left_trigger;
|
||||
}
|
||||
|
||||
void test_button_masks_and_direct_mapping() {
|
||||
ControllerState state{};
|
||||
controller_profile_apply_button_mask(UINT16_MAX, &state);
|
||||
require(controller_profile_extract_button_mask(state) == UINT16_MAX,
|
||||
"button mask application omitted a logical button");
|
||||
controller_profile_apply_button_mask(0, &state);
|
||||
require(controller_profile_extract_button_mask(state) == 0,
|
||||
"zero button mask did not clear every logical button");
|
||||
controller_profile_apply_button_mask(UINT16_MAX, nullptr);
|
||||
|
||||
for (uint8_t input = 0;
|
||||
input < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++input) {
|
||||
ControllerProfile profile = default_profile();
|
||||
const uint8_t output = static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT - 1u - input);
|
||||
profile.button_map[input] = output;
|
||||
state = {};
|
||||
controller_profile_apply_button_mask(
|
||||
static_cast<uint16_t>(1u << input), &state);
|
||||
const ControllerProfileTransformResult transformed =
|
||||
controller_profile_transform(state, profile);
|
||||
require(controller_profile_extract_button_mask(transformed.state) ==
|
||||
static_cast<uint16_t>(1u << output),
|
||||
"a logical button did not map directly to its output");
|
||||
}
|
||||
|
||||
ControllerProfile profile = default_profile();
|
||||
profile.button_map[0] = 1;
|
||||
profile.button_map[1] = 2;
|
||||
require(controller_profile_map_button_mask(1u, profile) == 2u,
|
||||
"button mapping recursively remapped an output");
|
||||
profile.button_map[1] = 1;
|
||||
require(controller_profile_map_button_mask(3u, profile) == 2u,
|
||||
"duplicate mapped outputs were not combined");
|
||||
profile.button_map[0] = CONTROLLER_PROFILE_NO_BUTTON;
|
||||
require(controller_profile_map_button_mask(1u, profile) == 0,
|
||||
"disabled button mapping still produced output");
|
||||
|
||||
ControllerProfile invalid = default_profile();
|
||||
invalid.button_map[0] = CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"logical output 16 was accepted");
|
||||
invalid.button_map[0] = 0xfe;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"logical output 0xfe was accepted");
|
||||
invalid.button_map[0] = CONTROLLER_PROFILE_NO_BUTTON;
|
||||
require(controller_profile_validate(invalid),
|
||||
"disabled logical output 0xff was rejected");
|
||||
}
|
||||
|
||||
void test_stick_center_boundaries_and_inversion() {
|
||||
ControllerProfileStickConfiguration configuration{};
|
||||
configuration.center_x = 1234;
|
||||
configuration.center_y = -2345;
|
||||
configuration.inner_deadzone = 1000;
|
||||
configuration.outer_saturation = 20000;
|
||||
configuration.curve_q8_8 = 256;
|
||||
ControllerProfileTransformResult transformed = transform_left_stick(
|
||||
configuration, configuration.center_x, configuration.center_y);
|
||||
require(transformed.state.left_stick_x == 0 &&
|
||||
transformed.state.left_stick_y == 0,
|
||||
"center calibration did not precede stick shaping");
|
||||
|
||||
transformed = transform_left_stick(
|
||||
configuration,
|
||||
static_cast<int16_t>(configuration.center_x +
|
||||
configuration.inner_deadzone),
|
||||
configuration.center_y);
|
||||
require(transformed.state.left_stick_x == 0 &&
|
||||
transformed.state.left_stick_y == 0,
|
||||
"inner deadzone boundary was not neutral");
|
||||
transformed = transform_left_stick(
|
||||
configuration,
|
||||
static_cast<int16_t>(configuration.center_x +
|
||||
configuration.inner_deadzone + 1),
|
||||
configuration.center_y);
|
||||
require(transformed.state.left_stick_x > 0,
|
||||
"first value outside inner deadzone stayed neutral");
|
||||
|
||||
configuration.center_x = 0;
|
||||
configuration.center_y = 0;
|
||||
configuration.inner_deadzone = 0;
|
||||
configuration.outer_saturation = 20000;
|
||||
transformed = transform_left_stick(configuration, 19999, 0);
|
||||
require(transformed.state.left_stick_x > 0 &&
|
||||
transformed.state.left_stick_x < INT16_MAX,
|
||||
"value below outer saturation reached an endpoint");
|
||||
transformed = transform_left_stick(configuration, 20000, 0);
|
||||
require(transformed.state.left_stick_x == INT16_MAX,
|
||||
"positive outer saturation boundary missed endpoint");
|
||||
transformed = transform_left_stick(configuration, -20000, 0);
|
||||
require(transformed.state.left_stick_x == INT16_MIN,
|
||||
"negative outer saturation boundary missed endpoint");
|
||||
|
||||
configuration.outer_saturation = 32767;
|
||||
configuration.invert_x = true;
|
||||
configuration.invert_y = true;
|
||||
transformed = transform_left_stick(configuration, INT16_MIN, 0);
|
||||
require(transformed.state.left_stick_x == INT16_MAX,
|
||||
"negative stick endpoint did not invert to positive endpoint");
|
||||
transformed = transform_left_stick(configuration, 0, INT16_MAX);
|
||||
require(transformed.state.left_stick_y == INT16_MIN,
|
||||
"positive stick endpoint did not invert to negative endpoint");
|
||||
|
||||
configuration.invert_x = false;
|
||||
configuration.invert_y = false;
|
||||
configuration.outer_saturation = 20000;
|
||||
transformed = transform_left_stick(configuration, 15000, 15000);
|
||||
require(transformed.state.left_stick_x < INT16_MAX &&
|
||||
transformed.state.left_stick_y < INT16_MAX,
|
||||
"stick magnitude was not Chebyshev magnitude");
|
||||
}
|
||||
|
||||
void test_stick_curves_and_monotonicity() {
|
||||
ControllerProfileStickConfiguration linear{};
|
||||
linear.inner_deadzone = 0;
|
||||
linear.outer_saturation = 32767;
|
||||
linear.curve_q8_8 = 256;
|
||||
ControllerProfileStickConfiguration slow = linear;
|
||||
slow.curve_q8_8 = 512;
|
||||
ControllerProfileStickConfiguration fast = linear;
|
||||
fast.curve_q8_8 = 128;
|
||||
|
||||
const int16_t linear_mid =
|
||||
transform_left_stick(linear, 16384, 0).state.left_stick_x;
|
||||
const int16_t slow_mid =
|
||||
transform_left_stick(slow, 16384, 0).state.left_stick_x;
|
||||
const int16_t fast_mid =
|
||||
transform_left_stick(fast, 16384, 0).state.left_stick_x;
|
||||
require(slow_mid < linear_mid && linear_mid < fast_mid,
|
||||
"stick curve directions are reversed or ineffective");
|
||||
require(transform_left_stick(slow, 0, 0).state.left_stick_x == 0 &&
|
||||
transform_left_stick(fast, 32767, 0)
|
||||
.state.left_stick_x == INT16_MAX,
|
||||
"stick response curve did not preserve endpoints");
|
||||
|
||||
const uint16_t curves[] = {1, 128, 256, 512, UINT16_MAX};
|
||||
for (uint16_t curve : curves) {
|
||||
ControllerProfileStickConfiguration configuration = linear;
|
||||
configuration.curve_q8_8 = curve;
|
||||
int16_t previous = 0;
|
||||
for (int32_t input = 0; input <= INT16_MAX; ++input) {
|
||||
const int16_t output = transform_left_stick(
|
||||
configuration,
|
||||
static_cast<int16_t>(input), 0)
|
||||
.state.left_stick_x;
|
||||
require(output >= previous,
|
||||
"stick response was not monotonic");
|
||||
previous = output;
|
||||
}
|
||||
require(previous == INT16_MAX,
|
||||
"monotonic stick response missed positive endpoint");
|
||||
}
|
||||
}
|
||||
|
||||
void test_trigger_boundaries_curves_and_thresholds() {
|
||||
ControllerProfileTriggerConfiguration configuration{};
|
||||
configuration.lower_deadzone = 1000;
|
||||
configuration.upper_saturation = 60000;
|
||||
configuration.curve_q8_8 = 256;
|
||||
configuration.digital_threshold = 32000;
|
||||
require(transform_left_trigger(configuration, 999) == 0 &&
|
||||
transform_left_trigger(configuration, 1000) == 0,
|
||||
"trigger lower deadzone boundary was not zero");
|
||||
require(transform_left_trigger(configuration, 1001) > 0,
|
||||
"first trigger value above lower deadzone stayed zero");
|
||||
require(transform_left_trigger(configuration, 59999) < UINT16_MAX &&
|
||||
transform_left_trigger(configuration, 60000) == UINT16_MAX &&
|
||||
transform_left_trigger(configuration, UINT16_MAX) ==
|
||||
UINT16_MAX,
|
||||
"trigger upper saturation boundary missed full scale");
|
||||
|
||||
ControllerProfileTriggerConfiguration slow = configuration;
|
||||
slow.curve_q8_8 = 512;
|
||||
ControllerProfileTriggerConfiguration fast = configuration;
|
||||
fast.curve_q8_8 = 128;
|
||||
const uint16_t midpoint = static_cast<uint16_t>(
|
||||
(static_cast<uint32_t>(configuration.lower_deadzone) +
|
||||
configuration.upper_saturation) /
|
||||
2u);
|
||||
const uint16_t linear_mid =
|
||||
transform_left_trigger(configuration, midpoint);
|
||||
const uint16_t slow_mid = transform_left_trigger(slow, midpoint);
|
||||
const uint16_t fast_mid = transform_left_trigger(fast, midpoint);
|
||||
require(slow_mid < linear_mid && linear_mid < fast_mid,
|
||||
"trigger curve directions are reversed or ineffective");
|
||||
require(transform_left_trigger(slow, configuration.lower_deadzone) == 0 &&
|
||||
transform_left_trigger(fast,
|
||||
configuration.upper_saturation) ==
|
||||
UINT16_MAX,
|
||||
"trigger response curve did not preserve endpoints");
|
||||
|
||||
uint16_t previous = 0;
|
||||
for (uint32_t input = 0; input <= UINT16_MAX; ++input) {
|
||||
const uint16_t output = transform_left_trigger(
|
||||
slow, static_cast<uint16_t>(input));
|
||||
require(output >= previous,
|
||||
"trigger response was not monotonic");
|
||||
previous = output;
|
||||
}
|
||||
|
||||
ControllerProfile profile = default_profile();
|
||||
profile.triggers[0] = slow;
|
||||
profile.triggers[0].digital_threshold = 12345;
|
||||
profile.triggers[1].digital_threshold = 54321;
|
||||
ControllerState state{};
|
||||
state.left_trigger = midpoint;
|
||||
const ControllerProfileTransformResult transformed =
|
||||
controller_profile_transform(state, profile);
|
||||
require(transformed.left_trigger_digital_threshold == 12345 &&
|
||||
transformed.right_trigger_digital_threshold == 54321,
|
||||
"profile-owned digital thresholds were not returned");
|
||||
}
|
||||
|
||||
void test_default_whole_state_equivalence() {
|
||||
ControllerState input{};
|
||||
input.dpad_up = true;
|
||||
input.dpad_right = true;
|
||||
input.button_south = true;
|
||||
input.button_west = true;
|
||||
input.button_left_shoulder = true;
|
||||
input.button_select = true;
|
||||
input.button_system = true;
|
||||
input.button_capture = true;
|
||||
input.button_right_stick = true;
|
||||
input.left_trigger = 0;
|
||||
input.right_trigger = UINT16_MAX;
|
||||
input.left_stick_x = INT16_MIN;
|
||||
input.left_stick_y = INT16_MAX;
|
||||
input.right_stick_x = INT16_MAX;
|
||||
input.right_stick_y = INT16_MIN;
|
||||
input.motion_sample_count = CONTROLLER_MOTION_SAMPLE_CAPACITY;
|
||||
input.motion_samples[0] =
|
||||
{INT16_MIN, -30000, -1, 0, 1, INT16_MAX};
|
||||
input.motion_samples[1] = {1, 2, 3, 4, 5, 6};
|
||||
input.motion_samples[2] =
|
||||
{INT16_MAX, 30000, 1, 0, -1, INT16_MIN};
|
||||
|
||||
const ControllerProfile profile = default_profile();
|
||||
const ControllerProfileTransformResult transformed =
|
||||
controller_profile_transform(input, profile);
|
||||
require(states_equal(input, transformed.state),
|
||||
"default profile changed whole controller state or motion");
|
||||
require(transformed.left_trigger_digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
transformed.right_trigger_digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD == 22934,
|
||||
"default digital threshold changed");
|
||||
|
||||
for (uint32_t trigger = 0; trigger <= UINT16_MAX; ++trigger) {
|
||||
ControllerState trigger_state{};
|
||||
trigger_state.left_trigger = static_cast<uint16_t>(trigger);
|
||||
trigger_state.right_trigger = static_cast<uint16_t>(trigger);
|
||||
const ControllerState output =
|
||||
controller_profile_transform(trigger_state, profile).state;
|
||||
require(output.left_trigger == trigger_state.left_trigger &&
|
||||
output.right_trigger == trigger_state.right_trigger,
|
||||
"default profile lost full trigger analog precision");
|
||||
}
|
||||
}
|
||||
|
||||
void test_rumble_scaling_and_confirmation_policy() {
|
||||
ControllerProfile profile = default_profile();
|
||||
for (uint16_t magnitude = 0; magnitude <= UINT8_MAX; ++magnitude) {
|
||||
const ControllerRumbleOutput input{
|
||||
static_cast<uint8_t>(magnitude),
|
||||
static_cast<uint8_t>(UINT8_MAX - magnitude),
|
||||
};
|
||||
const ControllerRumbleOutput output =
|
||||
controller_profile_scale_host_rumble(input, profile);
|
||||
require(output.low_frequency_magnitude ==
|
||||
input.low_frequency_magnitude &&
|
||||
output.high_frequency_magnitude ==
|
||||
input.high_frequency_magnitude,
|
||||
"default rumble scaling was not bit-exact identity");
|
||||
}
|
||||
|
||||
profile.strong_rumble_scale = 0;
|
||||
profile.weak_rumble_scale = 0;
|
||||
ControllerRumbleOutput output = controller_profile_scale_host_rumble(
|
||||
{UINT8_MAX, UINT8_MAX}, profile);
|
||||
require(output.low_frequency_magnitude == 0 &&
|
||||
output.high_frequency_magnitude == 0,
|
||||
"zero rumble scales did not mute both bands");
|
||||
|
||||
profile.strong_rumble_scale = 128;
|
||||
profile.weak_rumble_scale = 64;
|
||||
output = controller_profile_scale_host_rumble({200, 201}, profile);
|
||||
require(output.low_frequency_magnitude == 100 &&
|
||||
output.high_frequency_magnitude == 50,
|
||||
"strong/weak mid-scale rumble mapping was incorrect");
|
||||
require(controller_profile_scale_rumble_magnitude(UINT8_MAX,
|
||||
UINT8_MAX) ==
|
||||
UINT8_MAX,
|
||||
"full rumble scaling did not saturate at uint8 maximum");
|
||||
|
||||
profile.confirmation_policy = ControllerProfileConfirmationPolicy::kLed;
|
||||
require(controller_profile_confirmation_policy(profile) ==
|
||||
ControllerProfileConfirmationPolicy::kLed,
|
||||
"confirmation policy was not exposed unchanged");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
test_button_masks_and_direct_mapping();
|
||||
test_stick_center_boundaries_and_inversion();
|
||||
test_stick_curves_and_monotonicity();
|
||||
test_trigger_boundaries_curves_and_thresholds();
|
||||
test_default_whole_state_equivalence();
|
||||
test_rumble_scaling_and_confirmation_policy();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,465 +0,0 @@
|
|||
#include "controller_synthetic_input.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t button_index(ControllerProfileLogicalButton button) {
|
||||
return static_cast<uint8_t>(button);
|
||||
}
|
||||
|
||||
constexpr uint16_t button_bit(ControllerProfileLogicalButton button) {
|
||||
return static_cast<uint16_t>(1u << button_index(button));
|
||||
}
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
ControllerState state_with_buttons(uint16_t mask) {
|
||||
ControllerState state = controller_neutral_state();
|
||||
controller_profile_apply_button_mask(mask, &state);
|
||||
return state;
|
||||
}
|
||||
|
||||
bool has_button(const ControllerProfileTransformResult& result,
|
||||
ControllerProfileLogicalButton button) {
|
||||
return (controller_profile_extract_button_mask(result.state) &
|
||||
button_bit(button)) != 0;
|
||||
}
|
||||
|
||||
ControllerProfile profile_with_macro(
|
||||
ControllerProfileLogicalButton trigger,
|
||||
ControllerProfileLogicalButton cancel =
|
||||
ControllerProfileLogicalButton::kCapture) {
|
||||
ControllerProfile profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
profile.macro_trigger = button_index(trigger);
|
||||
profile.macro_cancel = button_index(cancel);
|
||||
return profile;
|
||||
}
|
||||
|
||||
void set_end(ControllerProfile* profile, uint8_t index) {
|
||||
profile->macro_steps[index] = {};
|
||||
profile->macro_steps[index].type =
|
||||
ControllerProfileMacroStepType::kEnd;
|
||||
}
|
||||
|
||||
void test_immediate_press_release_dpad_and_explicit_end() {
|
||||
ControllerProfile profile =
|
||||
profile_with_macro(ControllerProfileLogicalButton::kSouth);
|
||||
profile.macro_step_count = 3;
|
||||
profile.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[0].duration_ms = 10;
|
||||
profile.macro_steps[0].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kNorth) |
|
||||
button_bit(ControllerProfileLogicalButton::kDpadUp);
|
||||
profile.macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_steps[1].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[1].duration_ms = 20;
|
||||
profile.macro_steps[1].output_button_mask = 0;
|
||||
set_end(&profile, 2);
|
||||
|
||||
ControllerSyntheticInputContext context{};
|
||||
ControllerState input =
|
||||
state_with_buttons(button_bit(ControllerProfileLogicalButton::kSouth));
|
||||
ControllerProfileTransformResult output =
|
||||
controller_synthetic_input_apply(&context, input, profile, 100);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kNorth) &&
|
||||
has_button(output, ControllerProfileLogicalButton::kDpadUp) &&
|
||||
!has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||
"macro step zero was not immediate or its trigger leaked");
|
||||
|
||||
input = controller_neutral_state();
|
||||
output = controller_synthetic_input_apply(&context, input, profile, 109);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kNorth) &&
|
||||
has_button(output, ControllerProfileLogicalButton::kDpadUp),
|
||||
"macro press state ended before its scheduled deadline");
|
||||
output = controller_synthetic_input_apply(&context, input, profile, 110);
|
||||
require(controller_profile_extract_button_mask(output.state) == 0,
|
||||
"macro release state did not replace the full button mask");
|
||||
|
||||
input =
|
||||
state_with_buttons(button_bit(ControllerProfileLogicalButton::kWest));
|
||||
output = controller_synthetic_input_apply(&context, input, profile, 129);
|
||||
require(!has_button(output, ControllerProfileLogicalButton::kWest),
|
||||
"active macro release state did not override physical buttons");
|
||||
output = controller_synthetic_input_apply(&context, input, profile, 130);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kWest) &&
|
||||
!context.macro_active,
|
||||
"explicit end did not clear overrides and restore physical input");
|
||||
}
|
||||
|
||||
void test_optional_field_overrides_and_motion_preservation() {
|
||||
ControllerProfile profile =
|
||||
profile_with_macro(ControllerProfileLogicalButton::kSelect);
|
||||
profile.macro_step_count = 2;
|
||||
ControllerProfileMacroStep& step = profile.macro_steps[0];
|
||||
step.type = ControllerProfileMacroStepType::kState;
|
||||
step.override_flags = kControllerProfileOverrideLeftStick |
|
||||
kControllerProfileOverrideRightStick |
|
||||
kControllerProfileOverrideLeftTrigger |
|
||||
kControllerProfileOverrideRightTrigger;
|
||||
step.duration_ms = 100;
|
||||
step.left_stick_x = INT16_MIN;
|
||||
step.left_stick_y = 1234;
|
||||
step.right_stick_x = -2345;
|
||||
step.right_stick_y = INT16_MAX;
|
||||
step.left_trigger = 0;
|
||||
step.right_trigger = UINT16_MAX;
|
||||
set_end(&profile, 1);
|
||||
|
||||
ControllerState input = state_with_buttons(
|
||||
button_bit(ControllerProfileLogicalButton::kSelect) |
|
||||
button_bit(ControllerProfileLogicalButton::kSouth));
|
||||
input.left_stick_x = 10;
|
||||
input.left_stick_y = 20;
|
||||
input.right_stick_x = 30;
|
||||
input.right_stick_y = 40;
|
||||
input.left_trigger = 111;
|
||||
input.right_trigger = 222;
|
||||
input.motion_sample_count = 2;
|
||||
input.motion_samples[0] = {1, 2, 3, 4, 5, 6};
|
||||
input.motion_samples[1] = {-1, -2, -3, -4, -5, -6};
|
||||
|
||||
ControllerSyntheticInputContext context{};
|
||||
const ControllerProfileTransformResult output =
|
||||
controller_synthetic_input_apply(&context, input, profile, 5);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kSouth) &&
|
||||
!has_button(output, ControllerProfileLogicalButton::kSelect),
|
||||
"a trigger-only macro override changed buttons or leaked trigger");
|
||||
require(output.state.left_stick_x == INT16_MIN &&
|
||||
output.state.left_stick_y == 1234 &&
|
||||
output.state.right_stick_x == -2345 &&
|
||||
output.state.right_stick_y == INT16_MAX &&
|
||||
output.state.left_trigger == 0 &&
|
||||
output.state.right_trigger == UINT16_MAX,
|
||||
"one or more optional macro fields were not overridden");
|
||||
require(output.state.motion_sample_count == input.motion_sample_count &&
|
||||
std::memcmp(output.state.motion_samples,
|
||||
input.motion_samples,
|
||||
sizeof(input.motion_samples)) == 0,
|
||||
"synthetic processing changed motion samples");
|
||||
}
|
||||
|
||||
void test_zero_max_wait_and_scheduled_catch_up() {
|
||||
ControllerProfile profile =
|
||||
profile_with_macro(ControllerProfileLogicalButton::kSouth);
|
||||
profile.macro_step_count = 4;
|
||||
profile.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[0].duration_ms = 0;
|
||||
profile.macro_steps[0].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kNorth);
|
||||
profile.macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_steps[1].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[1].duration_ms = CONTROLLER_PROFILE_MAX_WAIT_MS;
|
||||
profile.macro_steps[1].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kEast);
|
||||
profile.macro_steps[2].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_steps[2].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[2].duration_ms = 0;
|
||||
profile.macro_steps[2].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kWest);
|
||||
set_end(&profile, 3);
|
||||
|
||||
ControllerSyntheticInputContext context{};
|
||||
ControllerProfileTransformResult output =
|
||||
controller_synthetic_input_apply(
|
||||
&context,
|
||||
state_with_buttons(
|
||||
button_bit(ControllerProfileLogicalButton::kSouth)),
|
||||
profile, 0);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||
"zero-wait step zero was skipped on its trigger report");
|
||||
output = controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 0);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kEast),
|
||||
"zero wait did not advance on the next scheduler observation");
|
||||
output = controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile,
|
||||
CONTROLLER_PROFILE_MAX_WAIT_MS - 1u);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kEast),
|
||||
"maximum legal wait expired early");
|
||||
output = controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile,
|
||||
CONTROLLER_PROFILE_MAX_WAIT_MS);
|
||||
require(controller_profile_extract_button_mask(output.state) == 0 &&
|
||||
!context.macro_active,
|
||||
"zero-duration catch-up did not reach the explicit end");
|
||||
|
||||
profile.macro_step_count = 4;
|
||||
profile.macro_steps[0].duration_ms = 10;
|
||||
profile.macro_steps[0].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kNorth);
|
||||
profile.macro_steps[1].duration_ms = 20;
|
||||
profile.macro_steps[1].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kEast);
|
||||
profile.macro_steps[2].duration_ms = 30;
|
||||
profile.macro_steps[2].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kWest);
|
||||
context = {};
|
||||
(void)controller_synthetic_input_apply(
|
||||
&context,
|
||||
state_with_buttons(button_bit(ControllerProfileLogicalButton::kSouth)),
|
||||
profile, 100);
|
||||
output = controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 145);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kWest),
|
||||
"catch-up used observation time instead of prior deadlines");
|
||||
output = controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 1000);
|
||||
require(!context.macro_active &&
|
||||
controller_profile_extract_button_mask(output.state) == 0,
|
||||
"large time jump did not finish the bounded macro");
|
||||
|
||||
profile.macro_step_count = 2;
|
||||
profile.macro_steps[0].duration_ms = 10;
|
||||
profile.macro_steps[0].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kNorth);
|
||||
set_end(&profile, 1);
|
||||
context = {};
|
||||
constexpr uint32_t macro_near_wrap = UINT32_MAX - 5u;
|
||||
output = controller_synthetic_input_apply(
|
||||
&context,
|
||||
state_with_buttons(button_bit(ControllerProfileLogicalButton::kSouth)),
|
||||
profile, macro_near_wrap);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||
"macro did not start immediately near uint32 wrap");
|
||||
output = controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 3);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||
"macro deadline expired early across uint32 wrap");
|
||||
output = controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 4);
|
||||
require(!context.macro_active &&
|
||||
controller_profile_extract_button_mask(output.state) == 0,
|
||||
"macro deadline was not uint32-wrap safe");
|
||||
}
|
||||
|
||||
void test_consumption_cancel_precedence_and_duplicate_contributors() {
|
||||
ControllerProfile profile =
|
||||
profile_with_macro(ControllerProfileLogicalButton::kSelect,
|
||||
ControllerProfileLogicalButton::kCapture);
|
||||
profile.macro_step_count = 2;
|
||||
profile.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
profile.macro_steps[0].override_flags =
|
||||
kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[0].duration_ms = 100;
|
||||
profile.macro_steps[0].output_button_mask =
|
||||
button_bit(ControllerProfileLogicalButton::kDpadLeft);
|
||||
set_end(&profile, 1);
|
||||
profile.button_map[button_index(ControllerProfileLogicalButton::kSouth)] =
|
||||
button_index(ControllerProfileLogicalButton::kNorth);
|
||||
profile.button_map[button_index(ControllerProfileLogicalButton::kEast)] =
|
||||
button_index(ControllerProfileLogicalButton::kNorth);
|
||||
profile.turbo_modes[button_index(ControllerProfileLogicalButton::kSouth)] =
|
||||
ControllerProfileTurboMode::kTurbo;
|
||||
|
||||
ControllerSyntheticInputContext context{};
|
||||
ControllerState input = state_with_buttons(
|
||||
button_bit(ControllerProfileLogicalButton::kSouth) |
|
||||
button_bit(ControllerProfileLogicalButton::kEast));
|
||||
ControllerProfileTransformResult output =
|
||||
controller_synthetic_input_apply(&context, input, profile, 0);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||
"duplicate mapped contributors were not ORed");
|
||||
output = controller_synthetic_input_apply(&context, input, profile, 34);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||
"Turbo gating erased a duplicate physical contributor");
|
||||
|
||||
input = state_with_buttons(
|
||||
button_bit(ControllerProfileLogicalButton::kSouth) |
|
||||
button_bit(ControllerProfileLogicalButton::kSelect));
|
||||
output = controller_synthetic_input_apply(&context, input, profile, 35);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kDpadLeft) &&
|
||||
!has_button(output, ControllerProfileLogicalButton::kNorth) &&
|
||||
!has_button(output, ControllerProfileLogicalButton::kSelect),
|
||||
"macro button override did not outrank Turbo or consume trigger");
|
||||
|
||||
input = state_with_buttons(
|
||||
button_bit(ControllerProfileLogicalButton::kSouth) |
|
||||
button_bit(ControllerProfileLogicalButton::kCapture));
|
||||
output = controller_synthetic_input_apply(&context, input, profile, 36);
|
||||
require(!context.macro_active &&
|
||||
!has_button(output, ControllerProfileLogicalButton::kCapture) &&
|
||||
has_button(output, ControllerProfileLogicalButton::kNorth),
|
||||
"configured cancel leaked or failed to clear macro state");
|
||||
}
|
||||
|
||||
void test_turbo_rate_release_and_uint32_wrap() {
|
||||
ControllerProfile profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
const uint8_t south =
|
||||
button_index(ControllerProfileLogicalButton::kSouth);
|
||||
profile.turbo_modes[south] = ControllerProfileTurboMode::kTurbo;
|
||||
const ControllerState pressed =
|
||||
state_with_buttons(button_bit(ControllerProfileLogicalButton::kSouth));
|
||||
ControllerSyntheticInputContext context{};
|
||||
ControllerProfileTransformResult output =
|
||||
controller_synthetic_input_apply(&context, pressed, profile, 0);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||
"Turbo did not begin in its ON phase");
|
||||
|
||||
constexpr std::array<uint32_t, 4> deltas = {17, 29, 11, 23};
|
||||
uint32_t now_ms = 0;
|
||||
size_t delta_index = 0;
|
||||
bool previous_on = true;
|
||||
unsigned completed_activations = 0;
|
||||
while (now_ms < 1000) {
|
||||
uint32_t delta = deltas[delta_index++ % deltas.size()];
|
||||
if (delta > 1000 - now_ms) {
|
||||
delta = 1000 - now_ms;
|
||||
}
|
||||
now_ms += delta;
|
||||
output = controller_synthetic_input_apply(
|
||||
&context, pressed, profile, now_ms);
|
||||
const bool on =
|
||||
has_button(output, ControllerProfileLogicalButton::kSouth);
|
||||
if (previous_on && !on) {
|
||||
++completed_activations;
|
||||
}
|
||||
previous_on = on;
|
||||
}
|
||||
require(completed_activations == 15 && previous_on,
|
||||
"irregular ticks did not produce exactly 15 activations per second");
|
||||
|
||||
output = controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 1001);
|
||||
require(!has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||
"Turbo release left a stuck output");
|
||||
output = controller_synthetic_input_apply(&context, pressed, profile, 1002);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||
"Turbo repress did not restart in the ON phase");
|
||||
|
||||
context = {};
|
||||
constexpr uint32_t near_wrap = UINT32_MAX - 10u;
|
||||
output = controller_synthetic_input_apply(
|
||||
&context, pressed, profile, near_wrap);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||
"Turbo wrap test did not start ON");
|
||||
output = controller_synthetic_input_apply(&context, pressed, profile, 23);
|
||||
require(!has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||
"Turbo phase accumulation was not uint32-wrap safe");
|
||||
output = controller_synthetic_input_apply(&context, pressed, profile, 56);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kSouth),
|
||||
"Turbo remainder was lost across uint32 wrap");
|
||||
}
|
||||
|
||||
void test_auto_burst_toggle_cancel_and_external_cancel() {
|
||||
ControllerProfile profile =
|
||||
profile_with_macro(ControllerProfileLogicalButton::kSelect,
|
||||
ControllerProfileLogicalButton::kEast);
|
||||
const uint8_t west = button_index(ControllerProfileLogicalButton::kWest);
|
||||
profile.turbo_modes[west] = ControllerProfileTurboMode::kAutoBurst;
|
||||
const ControllerState pressed =
|
||||
state_with_buttons(button_bit(ControllerProfileLogicalButton::kWest));
|
||||
ControllerSyntheticInputContext context{};
|
||||
|
||||
ControllerProfileTransformResult output =
|
||||
controller_synthetic_input_apply(&context, pressed, profile, 0);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kWest),
|
||||
"Auto Burst did not toggle on in the ON phase");
|
||||
output = controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 1);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kWest),
|
||||
"Auto Burst stopped when its physical input was released");
|
||||
output = controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 34);
|
||||
require(!has_button(output, ControllerProfileLogicalButton::kWest),
|
||||
"Auto Burst did not enter its OFF phase");
|
||||
output = controller_synthetic_input_apply(&context, pressed, profile, 35);
|
||||
require(!has_button(output, ControllerProfileLogicalButton::kWest) &&
|
||||
!context.bindings[west].active,
|
||||
"second Auto Burst rising press did not toggle it off");
|
||||
|
||||
(void)controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 36);
|
||||
output = controller_synthetic_input_apply(&context, pressed, profile, 40);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kWest),
|
||||
"Auto Burst did not toggle on a second time");
|
||||
output = controller_synthetic_input_apply(
|
||||
&context,
|
||||
state_with_buttons(button_bit(ControllerProfileLogicalButton::kEast)),
|
||||
profile, 41);
|
||||
require(!has_button(output, ControllerProfileLogicalButton::kWest) &&
|
||||
!has_button(output, ControllerProfileLogicalButton::kEast) &&
|
||||
!context.bindings[west].active,
|
||||
"macro cancel did not clear all Auto Burst state or was not consumed");
|
||||
|
||||
(void)controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 42);
|
||||
output = controller_synthetic_input_apply(&context, pressed, profile, 50);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kWest),
|
||||
"Auto Burst could not restart after configured cancellation");
|
||||
controller_synthetic_input_cancel(
|
||||
&context, button_bit(ControllerProfileLogicalButton::kWest));
|
||||
output = controller_synthetic_input_apply(&context, pressed, profile, 51);
|
||||
require(!has_button(output, ControllerProfileLogicalButton::kWest),
|
||||
"external cancel retriggered an already-held Auto Burst input");
|
||||
(void)controller_synthetic_input_apply(
|
||||
&context, controller_neutral_state(), profile, 52);
|
||||
output = controller_synthetic_input_apply(&context, pressed, profile, 53);
|
||||
require(has_button(output, ControllerProfileLogicalButton::kWest),
|
||||
"Auto Burst did not restart after release following cancellation");
|
||||
}
|
||||
|
||||
void test_four_contexts_are_isolated() {
|
||||
ControllerProfile profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
profile.turbo_modes[button_index(ControllerProfileLogicalButton::kSouth)] =
|
||||
ControllerProfileTurboMode::kAutoBurst;
|
||||
std::array<ControllerSyntheticInputContext, 4> contexts{};
|
||||
const ControllerState pressed =
|
||||
state_with_buttons(button_bit(ControllerProfileLogicalButton::kSouth));
|
||||
|
||||
ControllerProfileTransformResult slot0 =
|
||||
controller_synthetic_input_apply(&contexts[0], pressed, profile, 0);
|
||||
ControllerProfileTransformResult slot1 = controller_synthetic_input_apply(
|
||||
&contexts[1], controller_neutral_state(), profile, 0);
|
||||
require(has_button(slot0, ControllerProfileLogicalButton::kSouth) &&
|
||||
!has_button(slot1, ControllerProfileLogicalButton::kSouth),
|
||||
"synthetic activation leaked into another slot");
|
||||
(void)controller_synthetic_input_apply(
|
||||
&contexts[0], controller_neutral_state(), profile, 1);
|
||||
slot1 = controller_synthetic_input_apply(&contexts[1], pressed, profile, 10);
|
||||
controller_synthetic_input_cancel(&contexts[0]);
|
||||
slot0 = controller_synthetic_input_apply(
|
||||
&contexts[0], controller_neutral_state(), profile, 11);
|
||||
slot1 = controller_synthetic_input_apply(
|
||||
&contexts[1], controller_neutral_state(), profile, 11);
|
||||
require(!has_button(slot0, ControllerProfileLogicalButton::kSouth) &&
|
||||
has_button(slot1, ControllerProfileLogicalButton::kSouth),
|
||||
"cancelling one slot changed another slot's Auto Burst state");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
test_immediate_press_release_dpad_and_explicit_end();
|
||||
test_optional_field_overrides_and_motion_preservation();
|
||||
test_zero_max_wait_and_scheduled_catch_up();
|
||||
test_consumption_cancel_precedence_and_duplicate_contributors();
|
||||
test_turbo_rate_release_and_uint32_wrap();
|
||||
test_auto_burst_toggle_cancel_and_external_cancel();
|
||||
test_four_contexts_are_isolated();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,412 +0,0 @@
|
|||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
#include "pico_profile_storage.h"
|
||||
#include "profile_service.h"
|
||||
#include "profile_storage.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
struct FakeFlash {
|
||||
uint8_t bytes[PROFILE_STORAGE_BANK_COUNT][PROFILE_STORAGE_BANK_SIZE];
|
||||
int bank_replacements = 0;
|
||||
};
|
||||
|
||||
FakeFlash flash{};
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
bool fake_read(void* context, uint8_t bank, size_t offset,
|
||||
uint8_t* output, size_t size) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || output == nullptr ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
size > PROFILE_STORAGE_BANK_SIZE - offset) {
|
||||
return false;
|
||||
}
|
||||
memcpy(output, &storage->bytes[bank][offset], size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fake_replace_bank(void* context, uint8_t bank,
|
||||
const uint8_t* payload, size_t payload_size,
|
||||
const uint8_t* header, size_t header_size) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || payload == nullptr ||
|
||||
header == nullptr ||
|
||||
payload_size != CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
header_size != PROFILE_STORAGE_RECORD_HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
++storage->bank_replacements;
|
||||
memset(storage->bytes[bank], 0xff, PROFILE_STORAGE_BANK_SIZE);
|
||||
memcpy(
|
||||
&storage->bytes[bank][PROFILE_STORAGE_RECORD_HEADER_SIZE],
|
||||
payload, payload_size);
|
||||
memcpy(storage->bytes[bank], header, header_size);
|
||||
return memcmp(
|
||||
&storage->bytes[bank][
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE],
|
||||
payload, payload_size) == 0 &&
|
||||
memcmp(storage->bytes[bank], header, header_size) == 0;
|
||||
}
|
||||
|
||||
ProfileStorageIo fake_io() {
|
||||
return {
|
||||
&flash,
|
||||
PROFILE_STORAGE_BANK_SIZE,
|
||||
PROFILE_STORAGE_SECTOR_SIZE,
|
||||
PROFILE_STORAGE_PAGE_SIZE,
|
||||
fake_read,
|
||||
fake_replace_bank,
|
||||
};
|
||||
}
|
||||
|
||||
ProfileServiceTransactionSnapshot transaction_snapshot() {
|
||||
ProfileServiceTransactionSnapshot snapshot{};
|
||||
profile_service_transaction_snapshot(&snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
ProfileServiceActiveProfileSnapshot active_profile_snapshot(
|
||||
const ControllerIdentity& identity) {
|
||||
ProfileServiceActiveProfileSnapshot snapshot{};
|
||||
profile_service_active_profile_snapshot(identity, &snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
ControllerProfileDatabase reload_database(
|
||||
const ProfileServiceTransactionSnapshot& transaction,
|
||||
uint32_t expected_generation) {
|
||||
ControllerProfileDatabase recovered{};
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &recovered) &&
|
||||
storage.snapshot().valid &&
|
||||
storage.snapshot().generation == expected_generation &&
|
||||
storage.snapshot().generation ==
|
||||
transaction.transaction.stored_generation &&
|
||||
storage.snapshot().payload_crc ==
|
||||
transaction.transaction.stored_crc,
|
||||
"terminal transaction status did not identify persisted storage");
|
||||
return recovered;
|
||||
}
|
||||
|
||||
void test_pending_commands_are_not_decoded_as_profile_writes() {
|
||||
memset(flash.bytes, 0xff, sizeof(flash.bytes));
|
||||
profile_service_prepare();
|
||||
profile_service_initialize_on_storage_core();
|
||||
ProfileServiceActiveProfileSnapshot active =
|
||||
active_profile_snapshot(controller_identity_global());
|
||||
require(active.valid &&
|
||||
active.metadata.state == ProfileServiceState::kReady &&
|
||||
active.metadata.generation == 0 &&
|
||||
profile_service_database_generation() == 0 &&
|
||||
active.profile_index == 0,
|
||||
"initial active profile snapshot was not coherent");
|
||||
|
||||
const ControllerIdentity identity = controller_identity_global();
|
||||
constexpr uint8_t kProfileIndex = 2;
|
||||
ControllerProfile customized =
|
||||
controller_profile_default(identity, kProfileIndex);
|
||||
customized.strong_rumble_scale = 17;
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(customized, encoded, sizeof(encoded)),
|
||||
"customized profile did not encode");
|
||||
|
||||
constexpr uint32_t kWriteTransactionId = 0x10203040;
|
||||
require(profile_service_begin(
|
||||
kWriteTransactionId, identity, kProfileIndex,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_append(kWriteTransactionId, 0, encoded,
|
||||
sizeof(encoded)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_commit(kWriteTransactionId) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"profile write did not reach pending");
|
||||
profile_service_task_on_storage_core(0);
|
||||
require(transaction_snapshot().transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"profile write baseline did not commit");
|
||||
active = active_profile_snapshot(identity);
|
||||
require(active.valid && active.metadata.generation == 1 &&
|
||||
profile_service_database_generation() == 1 &&
|
||||
active.profile_index == 0,
|
||||
"profile write did not publish one coherent generation");
|
||||
|
||||
constexpr uint32_t kResetTransactionId = 0xa5a55a5a;
|
||||
require(profile_service_reset(kResetTransactionId, identity,
|
||||
kProfileIndex) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"profile reset did not reach pending");
|
||||
ProfileServiceTransactionSnapshot reset = transaction_snapshot();
|
||||
require(reset.transaction.transaction_id == kResetTransactionId &&
|
||||
reset.transaction.status ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"pending reset lost its transaction identity");
|
||||
|
||||
profile_service_task_on_storage_core(1000);
|
||||
reset = transaction_snapshot();
|
||||
require(reset.transaction.transaction_id == kResetTransactionId &&
|
||||
reset.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"one reset tick decoded profile payload or published a malformed result");
|
||||
ControllerProfileDatabase recovered = reload_database(reset, 2);
|
||||
require(recovered.fallback_profiles[kProfileIndex].strong_rumble_scale ==
|
||||
UINT8_MAX,
|
||||
"terminal reset status was published before reset persisted");
|
||||
active = active_profile_snapshot(identity);
|
||||
require(active.valid && active.metadata.generation == 2 &&
|
||||
profile_service_database_generation() == 2 &&
|
||||
active.profile_index == 0,
|
||||
"profile reset did not refresh the active snapshot generation");
|
||||
|
||||
constexpr uint32_t kActivateTransactionId = 0x50607080;
|
||||
constexpr uint8_t kActivatedProfile = 3;
|
||||
require(profile_service_activate(kActivateTransactionId, identity,
|
||||
kActivatedProfile) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"profile activation did not reach pending");
|
||||
ProfileServiceTransactionSnapshot activate = transaction_snapshot();
|
||||
require(activate.transaction.transaction_id == kActivateTransactionId &&
|
||||
activate.transaction.status ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"pending activation lost its transaction identity");
|
||||
active = active_profile_snapshot(identity);
|
||||
require(active.valid && active.metadata.generation == 2 &&
|
||||
active.profile_index == 0,
|
||||
"pending activation leaked an uncommitted active profile");
|
||||
|
||||
profile_service_task_on_storage_core(2000);
|
||||
activate = transaction_snapshot();
|
||||
require(activate.transaction.transaction_id == kActivateTransactionId &&
|
||||
activate.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"one activation tick decoded profile payload or published a malformed result");
|
||||
recovered = reload_database(activate, 3);
|
||||
require(recovered.fallback_active_profile == kActivatedProfile,
|
||||
"terminal activation status was published before activation persisted");
|
||||
active = active_profile_snapshot(identity);
|
||||
require(active.valid && active.metadata.generation == 3 &&
|
||||
profile_service_database_generation() == 3 &&
|
||||
active.profile_index == kActivatedProfile &&
|
||||
active.profile.strong_rumble_scale ==
|
||||
recovered.fallback_profiles[kActivatedProfile]
|
||||
.strong_rumble_scale,
|
||||
"activation did not publish profile, index, and generation together");
|
||||
}
|
||||
|
||||
void test_host_and_controller_mutations_are_serialized() {
|
||||
const ControllerIdentity identity = controller_identity_global();
|
||||
ControllerProfile profile =
|
||||
controller_profile_default(identity, 1);
|
||||
profile.weak_rumble_scale = 23;
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)),
|
||||
"serialization fixture profile did not encode");
|
||||
|
||||
constexpr uint32_t kHostTransactionId = 0x11223344;
|
||||
constexpr uint32_t kInternalTransactionId = 0x80000019;
|
||||
require(profile_service_begin(
|
||||
kHostTransactionId, identity, 1,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kReceiving,
|
||||
"host write did not acquire the profile mutation boundary");
|
||||
require(profile_service_activate_internal(
|
||||
kInternalTransactionId, identity, 2) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"controller activation raced a receiving host write");
|
||||
ProfileServiceTransactionSnapshot snapshot =
|
||||
transaction_snapshot();
|
||||
require(snapshot.transaction.transaction_id ==
|
||||
kHostTransactionId &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kReceiving,
|
||||
"busy controller activation replaced the host transaction");
|
||||
|
||||
require(profile_service_append(
|
||||
kHostTransactionId, 0, encoded, sizeof(encoded)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_commit(kHostTransactionId) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"host write did not reach pending after controller contention");
|
||||
profile_service_task_on_storage_core(3000);
|
||||
require(transaction_snapshot().transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"serialized host write did not commit");
|
||||
|
||||
constexpr uint32_t kHostActivationTransactionId = 0x22334455;
|
||||
require(profile_service_activate(
|
||||
kHostActivationTransactionId, identity, 0) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"host activation did not acquire the released boundary");
|
||||
require(profile_service_activate_internal(
|
||||
kInternalTransactionId, identity, 2) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"controller activation raced a pending host activation");
|
||||
snapshot = transaction_snapshot();
|
||||
require(snapshot.transaction.transaction_id ==
|
||||
kHostActivationTransactionId &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kPending &&
|
||||
active_profile_snapshot(identity).profile_index == 3,
|
||||
"pending host activation was replaced or leaked before commit");
|
||||
profile_service_task_on_storage_core(4000);
|
||||
require(active_profile_snapshot(identity).profile_index == 0,
|
||||
"serialized host activation did not commit");
|
||||
require(profile_service_activate_internal(
|
||||
0x19, identity, 2) ==
|
||||
ConfigurationTransactionStatus::kMalformed,
|
||||
"internal activation admitted a transaction without the high bit");
|
||||
|
||||
require(profile_service_activate_internal(
|
||||
kInternalTransactionId, identity, 2) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"controller activation did not acquire the released boundary");
|
||||
require(profile_service_begin(
|
||||
0x55667788, identity, 0,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
"host write raced a pending controller activation");
|
||||
snapshot = transaction_snapshot();
|
||||
require(snapshot.transaction.transaction_id ==
|
||||
kHostActivationTransactionId &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted &&
|
||||
active_profile_snapshot(identity).profile_index == 0,
|
||||
"pending controller activation replaced host-visible status or leaked before commit");
|
||||
|
||||
profile_service_task_on_storage_core(5000);
|
||||
const ProfileServiceActiveProfileSnapshot active =
|
||||
active_profile_snapshot(identity);
|
||||
snapshot = transaction_snapshot();
|
||||
require(active.valid && active.profile_index == 2 &&
|
||||
snapshot.transaction.transaction_id ==
|
||||
kHostActivationTransactionId &&
|
||||
snapshot.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"controller activation did not publish while preserving host-visible status");
|
||||
}
|
||||
|
||||
void test_completed_write_then_dirty_identity_activation() {
|
||||
const int replacements_before = flash.bank_replacements;
|
||||
const ControllerIdentity global = controller_identity_global();
|
||||
constexpr uint8_t kWrittenProfileIndex = 1;
|
||||
ControllerProfile customized =
|
||||
controller_profile_default(global, kWrittenProfileIndex);
|
||||
customized.strong_rumble_scale = 31;
|
||||
customized.weak_rumble_scale = 47;
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(customized, encoded, sizeof(encoded)),
|
||||
"sequential mutation fixture profile did not encode");
|
||||
|
||||
constexpr uint32_t kWriteTransactionId = 0x31415926;
|
||||
require(profile_service_begin(
|
||||
kWriteTransactionId, global, kWrittenProfileIndex,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_append(kWriteTransactionId, 0, encoded,
|
||||
sizeof(encoded)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_commit(kWriteTransactionId) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"sequential profile write did not reach pending");
|
||||
profile_service_task_on_storage_core(6000);
|
||||
const ProfileServiceTransactionSnapshot written =
|
||||
transaction_snapshot();
|
||||
require(written.transaction.transaction_id == kWriteTransactionId &&
|
||||
written.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted &&
|
||||
flash.bank_replacements == replacements_before + 1,
|
||||
"completed write lost correlation or used multiple bank replacements");
|
||||
|
||||
ControllerIdentity connected{};
|
||||
connected.stable = true;
|
||||
connected.transport = ControllerTransport::kClassic;
|
||||
connected.address[0] = 0x10;
|
||||
connected.address[1] = 0x20;
|
||||
connected.address[2] = 0x30;
|
||||
connected.address[3] = 0x40;
|
||||
connected.address[4] = 0x50;
|
||||
connected.address[5] = 0x60;
|
||||
connected.vendor_id = 0x1234;
|
||||
connected.product_id = 0xabcd;
|
||||
require(profile_service_observe_identity_on_storage_core(connected),
|
||||
"connected identity did not enter the dirty database");
|
||||
|
||||
constexpr uint32_t kActivateTransactionId = 0x27182818;
|
||||
constexpr uint8_t kActivatedProfileIndex = 2;
|
||||
require(profile_service_activate(
|
||||
kActivateTransactionId, connected,
|
||||
kActivatedProfileIndex) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"activation after completed write did not reach pending");
|
||||
const ProfileServiceTransactionSnapshot pending =
|
||||
transaction_snapshot();
|
||||
require(pending.transaction.transaction_id ==
|
||||
kActivateTransactionId &&
|
||||
pending.transaction.status ==
|
||||
ConfigurationTransactionStatus::kPending &&
|
||||
pending.transaction.stored_generation == 0 &&
|
||||
pending.transaction.stored_crc == 0,
|
||||
"pending activation was not correlated to its own transaction");
|
||||
|
||||
profile_service_task_on_storage_core(7000);
|
||||
const ProfileServiceTransactionSnapshot activated =
|
||||
transaction_snapshot();
|
||||
require(activated.transaction.transaction_id ==
|
||||
kActivateTransactionId &&
|
||||
activated.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted &&
|
||||
activated.transaction.stored_generation ==
|
||||
written.transaction.stored_generation + 1 &&
|
||||
flash.bank_replacements == replacements_before + 2,
|
||||
"activation did not complete as one next correlated bank replacement");
|
||||
|
||||
const ControllerProfileDatabase recovered = reload_database(
|
||||
activated, activated.transaction.stored_generation);
|
||||
const ControllerProfileDatabaseEntry* connected_entry =
|
||||
controller_profile_database_find(recovered, connected);
|
||||
require(connected_entry != nullptr &&
|
||||
connected_entry->active_profile ==
|
||||
kActivatedProfileIndex &&
|
||||
recovered.fallback_profiles[kWrittenProfileIndex]
|
||||
.strong_rumble_scale ==
|
||||
customized.strong_rumble_scale &&
|
||||
recovered.fallback_profiles[kWrittenProfileIndex]
|
||||
.weak_rumble_scale ==
|
||||
customized.weak_rumble_scale,
|
||||
"activation did not atomically persist the dirty identity and prior write");
|
||||
const ProfileServiceActiveProfileSnapshot active =
|
||||
active_profile_snapshot(connected);
|
||||
require(active.valid &&
|
||||
active.metadata.generation ==
|
||||
activated.transaction.stored_generation &&
|
||||
active.profile_index == kActivatedProfileIndex,
|
||||
"completed activation did not publish the dirty identity");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProfileStorageIo pico_profile_storage_io() {
|
||||
return fake_io();
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_pending_commands_are_not_decoded_as_profile_writes();
|
||||
test_host_and_controller_mutations_are_serialized();
|
||||
test_completed_write_then_dirty_identity_activation();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,658 +0,0 @@
|
|||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
#include "profile_storage.h"
|
||||
#include "tests/controller_profile_legacy_fixtures.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
struct FakeFlash {
|
||||
uint8_t bytes[PROFILE_STORAGE_BANK_COUNT][PROFILE_STORAGE_BANK_SIZE];
|
||||
int successful_programs = 0;
|
||||
int fail_after_programs = -1;
|
||||
bool corrupt_next_program = false;
|
||||
int corrupt_header_padding_offset = -1;
|
||||
bool fail_reads_after_header_program = false;
|
||||
bool header_programmed = false;
|
||||
int erase_count = 0;
|
||||
int bank_replacements = 0;
|
||||
};
|
||||
|
||||
FakeFlash flash{};
|
||||
ControllerProfileDatabase database{};
|
||||
ControllerProfileDatabase recovered_database{};
|
||||
uint8_t encoded_database[CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE]{};
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void erase_all() {
|
||||
memset(flash.bytes, 0xff, sizeof(flash.bytes));
|
||||
flash.successful_programs = 0;
|
||||
flash.fail_after_programs = -1;
|
||||
flash.corrupt_next_program = false;
|
||||
flash.corrupt_header_padding_offset = -1;
|
||||
flash.fail_reads_after_header_program = false;
|
||||
flash.header_programmed = false;
|
||||
flash.erase_count = 0;
|
||||
flash.bank_replacements = 0;
|
||||
}
|
||||
|
||||
bool fake_read(void* context, uint8_t bank, size_t offset,
|
||||
uint8_t* output, size_t size) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (storage->fail_reads_after_header_program &&
|
||||
storage->header_programmed) {
|
||||
return false;
|
||||
}
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
size > PROFILE_STORAGE_BANK_SIZE - offset) {
|
||||
return false;
|
||||
}
|
||||
memcpy(output, &storage->bytes[bank][offset], size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fake_replace_bank(void* context, uint8_t bank,
|
||||
const uint8_t* payload, size_t payload_size,
|
||||
const uint8_t* header, size_t header_size) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || payload == nullptr ||
|
||||
header == nullptr ||
|
||||
payload_size != CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
header_size != PROFILE_STORAGE_RECORD_HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
++storage->bank_replacements;
|
||||
memset(storage->bytes[bank], 0xff, PROFILE_STORAGE_BANK_SIZE);
|
||||
storage->erase_count +=
|
||||
static_cast<int>(PROFILE_STORAGE_SECTORS_PER_BANK);
|
||||
|
||||
uint8_t final_page[PROFILE_STORAGE_PAGE_SIZE]{};
|
||||
for (size_t offset = 0; offset < payload_size;
|
||||
offset += PROFILE_STORAGE_PAGE_SIZE) {
|
||||
if (storage->fail_after_programs >= 0 &&
|
||||
storage->successful_programs >=
|
||||
storage->fail_after_programs) {
|
||||
return false;
|
||||
}
|
||||
const size_t remaining = payload_size - offset;
|
||||
const uint8_t* page = &payload[offset];
|
||||
if (remaining < PROFILE_STORAGE_PAGE_SIZE) {
|
||||
memcpy(final_page, page, remaining);
|
||||
page = final_page;
|
||||
}
|
||||
memcpy(
|
||||
&storage->bytes[bank][
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE + offset],
|
||||
page, PROFILE_STORAGE_PAGE_SIZE);
|
||||
if (storage->corrupt_next_program) {
|
||||
storage->bytes[bank][
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE + offset] ^= 1;
|
||||
storage->corrupt_next_program = false;
|
||||
}
|
||||
++storage->successful_programs;
|
||||
}
|
||||
if (memcmp(
|
||||
&storage->bytes[bank][PROFILE_STORAGE_RECORD_HEADER_SIZE],
|
||||
payload, payload_size) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (storage->fail_after_programs >= 0 &&
|
||||
storage->successful_programs >= storage->fail_after_programs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(storage->bytes[bank], header, header_size);
|
||||
storage->header_programmed = true;
|
||||
++storage->successful_programs;
|
||||
if (storage->corrupt_header_padding_offset >= 24 &&
|
||||
static_cast<size_t>(
|
||||
storage->corrupt_header_padding_offset) < header_size) {
|
||||
storage->bytes[bank][
|
||||
static_cast<size_t>(
|
||||
storage->corrupt_header_padding_offset)] ^= 1;
|
||||
}
|
||||
return memcmp(storage->bytes[bank], header, header_size) == 0;
|
||||
}
|
||||
|
||||
ProfileStorageIo fake_io() {
|
||||
return {
|
||||
&flash,
|
||||
PROFILE_STORAGE_BANK_SIZE,
|
||||
PROFILE_STORAGE_SECTOR_SIZE,
|
||||
PROFILE_STORAGE_PAGE_SIZE,
|
||||
fake_read,
|
||||
fake_replace_bank,
|
||||
};
|
||||
}
|
||||
|
||||
uint16_t fixture_read_u16(const uint8_t* input) {
|
||||
return static_cast<uint16_t>(input[0]) |
|
||||
static_cast<uint16_t>(input[1] << 8);
|
||||
}
|
||||
|
||||
void fixture_write_u16(uint8_t* output, uint16_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
}
|
||||
|
||||
void fixture_write_u32(uint8_t* output, uint32_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
output[2] = static_cast<uint8_t>(value >> 16);
|
||||
output[3] = static_cast<uint8_t>(value >> 24);
|
||||
}
|
||||
|
||||
void install_legacy_database_bank_fixture() {
|
||||
erase_all();
|
||||
constexpr uint8_t kBank = 1;
|
||||
constexpr uint32_t kGeneration = 41;
|
||||
constexpr size_t kFallbackOffset =
|
||||
CONTROLLER_PROFILE_DATABASE_HEADER_SIZE;
|
||||
constexpr size_t kEntryOffset =
|
||||
kFallbackOffset +
|
||||
CONTROLLER_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
constexpr uint8_t kEntryHeader[CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE] = {
|
||||
1, 1, 7, 0, 1, 2, 3, 4, 5, 6, 0x7e, 0x05, 0x09, 0x20, 3, 1,
|
||||
};
|
||||
|
||||
uint8_t* const record = flash.bytes[kBank];
|
||||
uint8_t* const payload = record + PROFILE_STORAGE_RECORD_HEADER_SIZE;
|
||||
memset(record, 0, PROFILE_STORAGE_RECORD_HEADER_SIZE);
|
||||
memset(payload, 0, CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
|
||||
|
||||
memcpy(payload, "SPDB", 4);
|
||||
fixture_write_u16(&payload[4],
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION);
|
||||
fixture_write_u16(
|
||||
&payload[6],
|
||||
static_cast<uint16_t>(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE));
|
||||
payload[8] = CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY;
|
||||
payload[9] = CONTROLLER_PROFILE_COUNT;
|
||||
payload[10] = 2;
|
||||
payload[11] = 1;
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const uint8_t* fixture =
|
||||
profile_index == 0
|
||||
? kLegacyNarrowRawRangeProfile
|
||||
: profile_index == 1 ? kLegacyCustomThresholdProfile
|
||||
: kLegacyDefaultProfile;
|
||||
memcpy(&payload[kFallbackOffset +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE],
|
||||
fixture, CONTROLLER_PROFILE_ENCODED_SIZE);
|
||||
}
|
||||
|
||||
memcpy(&payload[kEntryOffset], kEntryHeader, sizeof(kEntryHeader));
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const uint8_t* fixture =
|
||||
profile_index == 0
|
||||
? kLegacyNarrowRawRangeProfile
|
||||
: profile_index == 3 ? kLegacyCustomThresholdProfile
|
||||
: kLegacyDefaultProfile;
|
||||
memcpy(&payload[kEntryOffset +
|
||||
CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE],
|
||||
fixture, CONTROLLER_PROFILE_ENCODED_SIZE);
|
||||
}
|
||||
|
||||
const uint32_t payload_crc = profile_storage_crc32(
|
||||
payload, CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
|
||||
memcpy(record, "SPPF", 4);
|
||||
fixture_write_u16(&record[4], 1);
|
||||
fixture_write_u16(&record[6],
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION);
|
||||
fixture_write_u32(&record[8], kGeneration);
|
||||
fixture_write_u32(
|
||||
&record[12],
|
||||
static_cast<uint32_t>(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE));
|
||||
fixture_write_u32(&record[16], payload_crc);
|
||||
fixture_write_u32(&record[20], profile_storage_crc32(record, 20));
|
||||
}
|
||||
|
||||
void test_initialize_requires_batch_replacement() {
|
||||
erase_all();
|
||||
ProfileStorageIo io = fake_io();
|
||||
io.replace_bank = nullptr;
|
||||
ProfileStorage storage;
|
||||
require(!storage.initialize(io, &database),
|
||||
"profile storage initialized without bank replacement");
|
||||
}
|
||||
|
||||
void test_two_bank_recovery() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &database) &&
|
||||
!storage.snapshot().valid,
|
||||
"erased profile storage did not initialize empty");
|
||||
require(storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
storage.snapshot().generation == 1,
|
||||
"first profile database did not commit");
|
||||
const int programs_after_first = flash.successful_programs;
|
||||
require(storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kUnchanged &&
|
||||
flash.successful_programs == programs_after_first,
|
||||
"unchanged profile database consumed flash writes");
|
||||
|
||||
database.fallback_profiles[0].button_map[0] = 1;
|
||||
require(storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
storage.snapshot().generation == 2,
|
||||
"second profile database generation did not commit");
|
||||
ProfileStorage reloaded;
|
||||
require(reloaded.initialize(fake_io(), &recovered_database) &&
|
||||
reloaded.snapshot().generation == 2 &&
|
||||
recovered_database.fallback_profiles[0].button_map[0] == 1,
|
||||
"latest profile database did not survive reload");
|
||||
|
||||
const uint8_t newest_bank = reloaded.snapshot().active_bank;
|
||||
flash.bytes[newest_bank][PROFILE_STORAGE_RECORD_HEADER_SIZE + 4] ^= 1;
|
||||
ProfileStorage after_corruption;
|
||||
require(after_corruption.initialize(fake_io(), &recovered_database) &&
|
||||
after_corruption.snapshot().generation == 1 &&
|
||||
recovered_database.fallback_profiles[0].button_map[0] == 0,
|
||||
"corrupt newest profile bank did not roll back");
|
||||
}
|
||||
|
||||
void test_interrupted_commit_retains_previous_bank() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &database) &&
|
||||
storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk,
|
||||
"interruption baseline did not commit");
|
||||
database.fallback_profiles[1].button_map[2] = 3;
|
||||
flash.fail_after_programs = flash.successful_programs + 1;
|
||||
require(storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kIoError,
|
||||
"interrupted profile write reported success");
|
||||
|
||||
flash.fail_after_programs = -1;
|
||||
ProfileStorage recovered;
|
||||
require(recovered.initialize(fake_io(), &recovered_database) &&
|
||||
recovered.snapshot().generation == 1 &&
|
||||
recovered_database.fallback_profiles[1].button_map[2] == 2,
|
||||
"interrupted profile write replaced previous bank");
|
||||
}
|
||||
|
||||
void test_successful_header_program_is_commit_point() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &database) &&
|
||||
storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk,
|
||||
"commit-point baseline did not commit");
|
||||
|
||||
database.fallback_profiles[1].strong_rumble_scale = 17;
|
||||
flash.header_programmed = false;
|
||||
flash.fail_reads_after_header_program = true;
|
||||
require(storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
storage.snapshot().generation == 2,
|
||||
"successful header program was rolled back by a later read");
|
||||
|
||||
flash.fail_reads_after_header_program = false;
|
||||
ProfileStorage recovered;
|
||||
require(recovered.initialize(fake_io(), &recovered_database) &&
|
||||
recovered.snapshot().generation == 2 &&
|
||||
recovered_database.fallback_profiles[1]
|
||||
.strong_rumble_scale == 17,
|
||||
"committed header did not recover after transient read failure");
|
||||
}
|
||||
|
||||
void test_payload_corruption_prevents_header_publication() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &database) &&
|
||||
storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk,
|
||||
"corruption baseline did not commit");
|
||||
const ProfileStorageSnapshot previous = storage.snapshot();
|
||||
const uint8_t target_bank = previous.active_bank ^ 1u;
|
||||
const int programs_before_corruption = flash.successful_programs;
|
||||
constexpr int kPayloadProgramCount =
|
||||
(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE +
|
||||
PROFILE_STORAGE_PAGE_SIZE - 1) /
|
||||
PROFILE_STORAGE_PAGE_SIZE;
|
||||
|
||||
database.fallback_profiles[1].button_map[2] = 3;
|
||||
flash.corrupt_next_program = true;
|
||||
require(storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kIoError &&
|
||||
flash.successful_programs ==
|
||||
programs_before_corruption + kPayloadProgramCount,
|
||||
"corrupt payload programming reached the header program");
|
||||
for (size_t index = 0; index < PROFILE_STORAGE_RECORD_HEADER_SIZE;
|
||||
++index) {
|
||||
require(flash.bytes[target_bank][index] == 0xff,
|
||||
"rejected corrupt payload published a discoverable header");
|
||||
}
|
||||
require(storage.snapshot().valid == previous.valid &&
|
||||
storage.snapshot().generation == previous.generation &&
|
||||
storage.snapshot().payload_crc == previous.payload_crc &&
|
||||
storage.snapshot().active_bank == previous.active_bank,
|
||||
"rejected corrupt programming changed the storage snapshot");
|
||||
|
||||
ProfileStorage recovered;
|
||||
require(recovered.initialize(fake_io(), &recovered_database) &&
|
||||
recovered.snapshot().generation == previous.generation &&
|
||||
recovered.snapshot().active_bank == previous.active_bank &&
|
||||
recovered_database.fallback_profiles[1].button_map[2] == 2,
|
||||
"headerless corrupt payload was recovered");
|
||||
}
|
||||
|
||||
void test_batched_bank_replacement_is_one_atomic_operation() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
ProfileStorage storage;
|
||||
constexpr int kPayloadProgramCount =
|
||||
(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE +
|
||||
PROFILE_STORAGE_PAGE_SIZE - 1) /
|
||||
PROFILE_STORAGE_PAGE_SIZE;
|
||||
require(storage.initialize(fake_io(), &database) &&
|
||||
storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
flash.bank_replacements == 1 &&
|
||||
flash.erase_count == static_cast<int>(
|
||||
PROFILE_STORAGE_SECTORS_PER_BANK) &&
|
||||
flash.successful_programs ==
|
||||
kPayloadProgramCount + 1,
|
||||
"batched commit did not replace one bank in one operation");
|
||||
|
||||
const ProfileStorageSnapshot previous = storage.snapshot();
|
||||
const uint8_t target_bank = previous.active_bank ^ 1u;
|
||||
const int programs_before_corruption = flash.successful_programs;
|
||||
database.fallback_profiles[1].button_map[2] = 3;
|
||||
flash.corrupt_next_program = true;
|
||||
require(storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kIoError &&
|
||||
flash.bank_replacements == 2 &&
|
||||
flash.successful_programs ==
|
||||
programs_before_corruption +
|
||||
kPayloadProgramCount,
|
||||
"corrupt batched payload reached header publication");
|
||||
for (size_t index = 0; index < PROFILE_STORAGE_RECORD_HEADER_SIZE;
|
||||
++index) {
|
||||
require(flash.bytes[target_bank][index] == 0xff,
|
||||
"failed batched replacement published a header");
|
||||
}
|
||||
require(storage.snapshot().generation == previous.generation &&
|
||||
storage.snapshot().payload_crc ==
|
||||
previous.payload_crc &&
|
||||
storage.snapshot().active_bank ==
|
||||
previous.active_bank,
|
||||
"failed batched replacement changed the committed snapshot");
|
||||
|
||||
ProfileStorage recovered;
|
||||
require(recovered.initialize(fake_io(), &recovered_database) &&
|
||||
recovered.snapshot().generation ==
|
||||
previous.generation &&
|
||||
recovered.snapshot().active_bank ==
|
||||
previous.active_bank &&
|
||||
recovered_database.fallback_profiles[1]
|
||||
.button_map[2] == 2,
|
||||
"headerless batched payload replaced the prior bank");
|
||||
}
|
||||
|
||||
void test_header_padding_corruption_fails_commit_and_recovery() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &database) &&
|
||||
storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk,
|
||||
"header padding baseline did not commit");
|
||||
|
||||
const ProfileStorageSnapshot previous = storage.snapshot();
|
||||
const uint8_t previous_scale =
|
||||
database.fallback_profiles[1].strong_rumble_scale;
|
||||
database.fallback_profiles[1].strong_rumble_scale = 17;
|
||||
for (size_t offset = 24;
|
||||
offset < PROFILE_STORAGE_RECORD_HEADER_SIZE; ++offset) {
|
||||
flash.corrupt_header_padding_offset =
|
||||
static_cast<int>(offset);
|
||||
require(storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kIoError,
|
||||
"corrupt header padding did not fail the commit");
|
||||
|
||||
ProfileStorage recovered;
|
||||
require(recovered.initialize(fake_io(), &recovered_database) &&
|
||||
recovered.snapshot().generation ==
|
||||
previous.generation &&
|
||||
recovered.snapshot().active_bank ==
|
||||
previous.active_bank &&
|
||||
recovered_database.fallback_profiles[1]
|
||||
.strong_rumble_scale == previous_scale,
|
||||
"corrupt header padding was accepted on recovery");
|
||||
}
|
||||
|
||||
flash.corrupt_header_padding_offset = -1;
|
||||
require(storage.commit(database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk,
|
||||
"valid full header page did not commit");
|
||||
for (size_t offset = 24;
|
||||
offset < PROFILE_STORAGE_RECORD_HEADER_SIZE; ++offset) {
|
||||
require(
|
||||
flash.bytes[storage.snapshot().active_bank][offset] == 0,
|
||||
"valid committed header contained nonzero padding");
|
||||
}
|
||||
ProfileStorage recovered;
|
||||
require(recovered.initialize(fake_io(), &recovered_database) &&
|
||||
recovered.snapshot().generation ==
|
||||
previous.generation + 1u &&
|
||||
recovered_database.fallback_profiles[1]
|
||||
.strong_rumble_scale == 17,
|
||||
"valid full header page was rejected on recovery");
|
||||
}
|
||||
|
||||
void test_legacy_database_bank_migration() {
|
||||
install_legacy_database_bank_fixture();
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &recovered_database) &&
|
||||
storage.snapshot().valid &&
|
||||
storage.snapshot().active_bank == 1 &&
|
||||
storage.snapshot().generation == 41,
|
||||
"legacy v1 database bank was not selected");
|
||||
require(flash.erase_count == 0,
|
||||
"legacy bank admission erased flash");
|
||||
require(recovered_database.fallback_active_profile == 2,
|
||||
"legacy fallback active profile was not preserved");
|
||||
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const uint16_t expected_left =
|
||||
profile_index == 1
|
||||
? 0x1234
|
||||
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
const uint16_t expected_right =
|
||||
profile_index == 1
|
||||
? 0xabcd
|
||||
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
require(recovered_database.fallback_profiles[profile_index]
|
||||
.triggers[0]
|
||||
.digital_threshold == expected_left &&
|
||||
recovered_database.fallback_profiles[profile_index]
|
||||
.triggers[1]
|
||||
.digital_threshold == expected_right,
|
||||
"legacy fallback thresholds were not selectively migrated");
|
||||
}
|
||||
require(recovered_database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.lower_deadzone == 30000 &&
|
||||
recovered_database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.upper_saturation == 40000 &&
|
||||
recovered_database.fallback_profiles[0]
|
||||
.triggers[1]
|
||||
.lower_deadzone == 30000 &&
|
||||
recovered_database.fallback_profiles[0]
|
||||
.triggers[1]
|
||||
.upper_saturation == 40000,
|
||||
"legacy fallback raw trigger ranges were not preserved");
|
||||
|
||||
const ControllerProfileDatabaseEntry& entry =
|
||||
recovered_database.entries[0];
|
||||
require(entry.used && entry.active_profile == 3 &&
|
||||
entry.identity.stable &&
|
||||
entry.identity.transport == ControllerTransport::kClassic &&
|
||||
entry.identity.address_type == 7 &&
|
||||
entry.identity.address[0] == 1 &&
|
||||
entry.identity.address[5] == 6 &&
|
||||
entry.identity.vendor_id == 0x057e &&
|
||||
entry.identity.product_id == 0x2009,
|
||||
"legacy entry identity or active profile was not preserved");
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const uint16_t expected_left =
|
||||
profile_index == 3
|
||||
? 0x1234
|
||||
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
const uint16_t expected_right =
|
||||
profile_index == 3
|
||||
? 0xabcd
|
||||
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
|
||||
require(entry.profiles[profile_index]
|
||||
.triggers[0]
|
||||
.digital_threshold == expected_left &&
|
||||
entry.profiles[profile_index]
|
||||
.triggers[1]
|
||||
.digital_threshold == expected_right,
|
||||
"legacy entry thresholds were not selectively migrated");
|
||||
}
|
||||
require(entry.profiles[0].triggers[0].lower_deadzone == 30000 &&
|
||||
entry.profiles[0].triggers[0].upper_saturation == 40000 &&
|
||||
entry.profiles[0].triggers[1].lower_deadzone == 30000 &&
|
||||
entry.profiles[0].triggers[1].upper_saturation == 40000,
|
||||
"legacy entry raw trigger ranges were not preserved");
|
||||
|
||||
recovered_database.fallback_profiles[2].weak_rumble_scale = 17;
|
||||
require(storage.commit(recovered_database, encoded_database,
|
||||
sizeof(encoded_database)) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
storage.snapshot().active_bank == 0 &&
|
||||
storage.snapshot().generation == 42,
|
||||
"mutation after legacy admission did not commit");
|
||||
const uint8_t* const current_record = flash.bytes[0];
|
||||
const uint8_t* const current_payload =
|
||||
current_record + PROFILE_STORAGE_RECORD_HEADER_SIZE;
|
||||
require(fixture_read_u16(¤t_record[6]) ==
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION &&
|
||||
fixture_read_u16(¤t_payload[4]) ==
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION,
|
||||
"post-migration commit did not emit v2 storage schemas");
|
||||
constexpr size_t kFallbackOffset =
|
||||
CONTROLLER_PROFILE_DATABASE_HEADER_SIZE;
|
||||
constexpr size_t kEntryOffset =
|
||||
kFallbackOffset +
|
||||
CONTROLLER_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
require(fixture_read_u16(
|
||||
¤t_payload[kFallbackOffset +
|
||||
profile_index *
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE]) ==
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION &&
|
||||
fixture_read_u16(
|
||||
¤t_payload[
|
||||
kEntryOffset +
|
||||
CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
|
||||
profile_index *
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE]) ==
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION,
|
||||
"post-migration commit retained a v1 profile");
|
||||
}
|
||||
require(fixture_read_u16(&flash.bytes[1][6]) ==
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION &&
|
||||
fixture_read_u16(
|
||||
&flash.bytes[1][PROFILE_STORAGE_RECORD_HEADER_SIZE + 4]) ==
|
||||
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION,
|
||||
"post-migration commit erased or rewrote the admitted legacy bank");
|
||||
|
||||
ProfileStorage reloaded;
|
||||
require(reloaded.initialize(fake_io(), &database) &&
|
||||
reloaded.snapshot().generation == 42 &&
|
||||
database.fallback_profiles[2].weak_rumble_scale == 17 &&
|
||||
database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.lower_deadzone == 30000 &&
|
||||
database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.upper_saturation == 40000 &&
|
||||
database.fallback_profiles[0]
|
||||
.triggers[0]
|
||||
.digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
database.fallback_profiles[1]
|
||||
.triggers[0]
|
||||
.digital_threshold == 0x1234 &&
|
||||
database.fallback_profiles[1]
|
||||
.triggers[1]
|
||||
.digital_threshold == 0xabcd &&
|
||||
database.entries[0].used &&
|
||||
database.entries[0].active_profile == 3 &&
|
||||
database.entries[0]
|
||||
.profiles[0]
|
||||
.triggers[0]
|
||||
.lower_deadzone == 30000 &&
|
||||
database.entries[0]
|
||||
.profiles[0]
|
||||
.triggers[0]
|
||||
.upper_saturation == 40000 &&
|
||||
database.entries[0]
|
||||
.profiles[0]
|
||||
.triggers[0]
|
||||
.digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
database.entries[0]
|
||||
.profiles[3]
|
||||
.triggers[0]
|
||||
.digital_threshold == 0x1234 &&
|
||||
database.entries[0]
|
||||
.profiles[3]
|
||||
.triggers[1]
|
||||
.digital_threshold == 0xabcd,
|
||||
"v2 migration commit did not reload without data loss");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
int main() {
|
||||
test_two_bank_recovery();
|
||||
test_initialize_requires_batch_replacement();
|
||||
test_interrupted_commit_retains_previous_bank();
|
||||
test_successful_header_program_is_commit_point();
|
||||
test_payload_corruption_prevents_header_publication();
|
||||
test_batched_bank_replacement_is_one_atomic_operation();
|
||||
test_header_padding_corruption_fails_commit_and_recovery();
|
||||
test_legacy_database_bank_migration();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -289,9 +289,7 @@ void test_input_reports_and_timers_are_isolated() {
|
|||
states[3].button_west = true;
|
||||
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
switch_pro_set_input(instance, states[instance],
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(instance, states[instance]);
|
||||
}
|
||||
|
||||
now_ms = 15;
|
||||
|
|
@ -330,9 +328,7 @@ void test_input_reports_and_timers_are_isolated() {
|
|||
ControllerState changed_zero = states[0];
|
||||
changed_zero.button_east = false;
|
||||
changed_zero.button_system = true;
|
||||
switch_pro_set_input(0, changed_zero,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(0, changed_zero);
|
||||
now_ms = 30;
|
||||
expect(switch_pro_task(0),
|
||||
"instance 0 did not apply its changed input state");
|
||||
|
|
@ -345,9 +341,7 @@ void test_input_reports_and_timers_are_isolated() {
|
|||
ControllerState changed_three = states[3];
|
||||
changed_three.button_west = false;
|
||||
changed_three.button_capture = true;
|
||||
switch_pro_set_input(3, changed_three,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(3, changed_three);
|
||||
now_ms = 45;
|
||||
expect(switch_pro_task(3),
|
||||
"instance 3 did not apply its changed input state");
|
||||
|
|
@ -376,10 +370,8 @@ void test_callback_send_and_imu_modes_are_isolated() {
|
|||
ControllerState one = zero;
|
||||
one.button_north = true;
|
||||
one.motion_samples[0] = {1001, 2002, 3003, 4004, 5005, 6006};
|
||||
switch_pro_set_input(0, zero, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(1, one, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(0, zero);
|
||||
switch_pro_set_input(1, one);
|
||||
now_ms = 21;
|
||||
expect(switch_pro_task(0), "raw-IMU instance did not send input");
|
||||
expect(switch_pro_task(1), "off-IMU instance timer did not send input");
|
||||
|
|
@ -408,11 +400,8 @@ void test_callback_send_and_imu_modes_are_isolated() {
|
|||
stationary.right_stick_x = stationary.right_stick_y = 0;
|
||||
stationary.motion_sample_count = 1;
|
||||
stationary.motion_samples[0] = {1000, 2000, 3000, 0, 0, 0};
|
||||
switch_pro_set_input(0, moving, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(1, stationary,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(0, moving);
|
||||
switch_pro_set_input(1, stationary);
|
||||
now_ms = 21;
|
||||
expect(switch_pro_task(0), "moving quaternion instance did not report");
|
||||
expect(switch_pro_task(1), "stationary quaternion timer crossed instances");
|
||||
|
|
@ -589,9 +578,7 @@ void test_lifecycle_and_invalid_instances() {
|
|||
ControllerState ignored{};
|
||||
ignored.button_system = true;
|
||||
switch_pro_init(kInvalidInstance);
|
||||
switch_pro_set_input(kInvalidInstance, ignored,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(kInvalidInstance, ignored);
|
||||
switch_pro_set_rumble_callback(kInvalidInstance, rumble_callback);
|
||||
expect(!switch_pro_task(kInvalidInstance),
|
||||
"invalid instance ran a driver task");
|
||||
|
|
@ -611,8 +598,7 @@ void test_protocol_neutral_trigger_threshold() {
|
|||
state.left_trigger =
|
||||
static_cast<uint16_t>(SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD - 1u);
|
||||
state.right_trigger = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
switch_pro_set_input(0, state, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(0, state);
|
||||
now_ms = 15;
|
||||
expect(switch_pro_task(0), "trigger threshold report was not sent");
|
||||
SwitchProReport report = copy_switch_report(latest_regular_report(0));
|
||||
|
|
@ -621,8 +607,7 @@ void test_protocol_neutral_trigger_threshold() {
|
|||
|
||||
state.left_trigger = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
state.right_trigger = CONTROLLER_TRIGGER_MIN;
|
||||
switch_pro_set_input(0, state, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(0, state);
|
||||
now_ms = 30;
|
||||
expect(switch_pro_task(0), "second trigger threshold report was not sent");
|
||||
report = copy_switch_report(latest_regular_report(0));
|
||||
|
|
@ -630,36 +615,13 @@ void test_protocol_neutral_trigger_threshold() {
|
|||
"Switch trigger threshold changed at the upper boundary");
|
||||
}
|
||||
|
||||
void test_custom_trigger_thresholds_are_isolated() {
|
||||
initialize_contexts();
|
||||
ControllerState state{};
|
||||
state.left_trigger = 300;
|
||||
state.right_trigger = 300;
|
||||
switch_pro_set_input(0, state, 300, 301);
|
||||
switch_pro_set_input(1, state, 301, 300);
|
||||
|
||||
now_ms = 15;
|
||||
expect(switch_pro_task(0) && switch_pro_task(1),
|
||||
"custom trigger threshold reports were not sent");
|
||||
const SwitchProReport first =
|
||||
copy_switch_report(latest_regular_report(0));
|
||||
const SwitchProReport second =
|
||||
copy_switch_report(latest_regular_report(1));
|
||||
expect(first.inputs.buttonZL && !first.inputs.buttonZR,
|
||||
"instance 0 did not use its exact left/right trigger thresholds");
|
||||
expect(!second.inputs.buttonZL && second.inputs.buttonZR,
|
||||
"instance 1 trigger thresholds crossed HID contexts");
|
||||
}
|
||||
|
||||
void test_uart_parser_is_pure() {
|
||||
initialize_contexts();
|
||||
ControllerState driver_state{};
|
||||
driver_state.left_stick_x = driver_state.left_stick_y =
|
||||
driver_state.right_stick_x = driver_state.right_stick_y = 0;
|
||||
driver_state.button_north = true;
|
||||
switch_pro_set_input(0, driver_state,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_input(0, driver_state);
|
||||
now_ms = 15;
|
||||
switch_pro_task(0);
|
||||
|
||||
|
|
@ -762,7 +724,6 @@ int main() {
|
|||
test_grip_colors_are_isolated();
|
||||
test_lifecycle_and_invalid_instances();
|
||||
test_protocol_neutral_trigger_threshold();
|
||||
test_custom_trigger_thresholds_are_isolated();
|
||||
test_uart_parser_is_pure();
|
||||
if (failures != 0) {
|
||||
std::cerr << failures << " driver context test(s) failed\n";
|
||||
|
|
|
|||
|
|
@ -42,14 +42,11 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
"lifecycle",
|
||||
"pairing-policy",
|
||||
"slot-lighting",
|
||||
"profile-chord-raw",
|
||||
"profile-feedback",
|
||||
"stateful-rumble",
|
||||
"abxy-hotkey",
|
||||
"motion-hotkey",
|
||||
"analog-state",
|
||||
"rumble-mode",
|
||||
"clear-pairings",
|
||||
"configuration-timer",
|
||||
"flash-core-start",
|
||||
"flash-core-failure",
|
||||
):
|
||||
|
|
|
|||
|
|
@ -1,208 +0,0 @@
|
|||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SPEC = importlib.util.spec_from_file_location("switch_pico_build", ROOT / "build.py")
|
||||
build_script = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(build_script)
|
||||
|
||||
|
||||
def make_sdk(path):
|
||||
path.mkdir(parents=True)
|
||||
(path / "pico_sdk_init.cmake").touch()
|
||||
return path
|
||||
|
||||
|
||||
def make_toolchain(path):
|
||||
bin_dir = path / "bin"
|
||||
bin_dir.mkdir(parents=True)
|
||||
(bin_dir / build_script.TOOLCHAIN_COMPILER).touch()
|
||||
return path
|
||||
|
||||
|
||||
def no_compiler(_name, *, path):
|
||||
return None
|
||||
|
||||
|
||||
def test_parse_cmake_cache_ignores_comments_and_malformed_lines(tmp_path):
|
||||
cache = tmp_path / "CMakeCache.txt"
|
||||
cache.write_text(
|
||||
"// comment\n"
|
||||
"# internal comment\n"
|
||||
"PICO_SDK_PATH:PATH=/sdk\n"
|
||||
"VALUE_WITH_EQUALS:STRING=left=right\n"
|
||||
"malformed\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert build_script.parse_cmake_cache(cache) == {
|
||||
"PICO_SDK_PATH": "/sdk",
|
||||
"VALUE_WITH_EQUALS": "left=right",
|
||||
}
|
||||
|
||||
|
||||
def test_explicit_environment_wins_over_cache_and_fallback(tmp_path):
|
||||
explicit_sdk = make_sdk(tmp_path / "explicit-sdk")
|
||||
explicit_toolchain = make_toolchain(tmp_path / "explicit-toolchain")
|
||||
cached_sdk = make_sdk(tmp_path / "cached-sdk")
|
||||
cached_toolchain = make_toolchain(tmp_path / "cached-toolchain")
|
||||
fallback_sdk = make_sdk(tmp_path / "fallback-sdk")
|
||||
fallback_toolchain = make_toolchain(tmp_path / "fallback-toolchain")
|
||||
cache = tmp_path / "build" / "CMakeCache.txt"
|
||||
cache.parent.mkdir()
|
||||
cache.write_text(
|
||||
f"PICO_SDK_PATH:PATH={cached_sdk}\n"
|
||||
f"PICO_TOOLCHAIN_PATH:PATH={cached_toolchain}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
environ = {
|
||||
"PATH": "",
|
||||
"PICO_SDK_PATH": str(explicit_sdk),
|
||||
"PICO_TOOLCHAIN_PATH": str(explicit_toolchain),
|
||||
}
|
||||
|
||||
updates = build_script.configure_pico_environment(
|
||||
environ=environ,
|
||||
cache_paths=[cache],
|
||||
sdk_candidates=[("fallback", fallback_sdk)],
|
||||
toolchain_candidates=[("fallback", fallback_toolchain)],
|
||||
which=no_compiler,
|
||||
)
|
||||
|
||||
assert updates == {}
|
||||
assert environ["PICO_SDK_PATH"] == str(explicit_sdk)
|
||||
assert environ["PICO_TOOLCHAIN_PATH"] == str(explicit_toolchain)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("variable", "invalid", "expected"),
|
||||
[
|
||||
("PICO_SDK_PATH", "stale-sdk", "pico_sdk_init.cmake"),
|
||||
(
|
||||
"PICO_TOOLCHAIN_PATH",
|
||||
"stale-toolchain",
|
||||
f"bin/{build_script.TOOLCHAIN_COMPILER}",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_stale_explicit_environment_is_an_error(
|
||||
tmp_path, variable, invalid, expected
|
||||
):
|
||||
sdk = make_sdk(tmp_path / "sdk")
|
||||
toolchain = make_toolchain(tmp_path / "toolchain")
|
||||
environ = {
|
||||
"PATH": "",
|
||||
"PICO_SDK_PATH": str(sdk),
|
||||
"PICO_TOOLCHAIN_PATH": str(toolchain),
|
||||
variable: str(tmp_path / invalid),
|
||||
}
|
||||
|
||||
with pytest.raises(build_script.BuildEnvironmentError) as error:
|
||||
build_script.configure_pico_environment(
|
||||
environ=environ,
|
||||
cache_paths=[],
|
||||
sdk_candidates=[],
|
||||
toolchain_candidates=[],
|
||||
which=no_compiler,
|
||||
)
|
||||
|
||||
assert variable in str(error.value)
|
||||
assert expected in str(error.value)
|
||||
|
||||
|
||||
def test_cache_paths_win_over_fallbacks(tmp_path, capsys):
|
||||
cached_sdk = make_sdk(tmp_path / "cached-sdk")
|
||||
cached_toolchain = make_toolchain(tmp_path / "cached-toolchain")
|
||||
fallback_sdk = make_sdk(tmp_path / "fallback-sdk")
|
||||
fallback_toolchain = make_toolchain(tmp_path / "fallback-toolchain")
|
||||
cache = tmp_path / "build" / "CMakeCache.txt"
|
||||
cache.parent.mkdir()
|
||||
cache.write_text(
|
||||
f"PICO_SDK_PATH:PATH={cached_sdk}\n"
|
||||
f"CMAKE_C_COMPILER:FILEPATH="
|
||||
f"{cached_toolchain / 'bin' / build_script.TOOLCHAIN_COMPILER}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
environ = {"PATH": ""}
|
||||
|
||||
updates = build_script.configure_pico_environment(
|
||||
environ=environ,
|
||||
cache_paths=[cache],
|
||||
sdk_candidates=[("fallback", fallback_sdk)],
|
||||
toolchain_candidates=[("fallback", fallback_toolchain)],
|
||||
which=no_compiler,
|
||||
)
|
||||
|
||||
assert updates == {
|
||||
"PICO_SDK_PATH": str(cached_sdk),
|
||||
"PICO_TOOLCHAIN_PATH": str(cached_toolchain),
|
||||
}
|
||||
output = capsys.readouterr().out
|
||||
assert f"Auto-detected PICO_SDK_PATH={cached_sdk}" in output
|
||||
assert f"Auto-detected PICO_TOOLCHAIN_PATH={cached_toolchain}" in output
|
||||
assert str(cache) in output
|
||||
|
||||
|
||||
def test_project_local_fallback_ignores_stale_cache(tmp_path, monkeypatch):
|
||||
project_build = tmp_path / "build"
|
||||
local_sdk = make_sdk(project_build / "_deps" / "pico_sdk-src")
|
||||
local_toolchain = make_toolchain(project_build / "toolchain")
|
||||
cache = tmp_path / "old-build" / "CMakeCache.txt"
|
||||
cache.parent.mkdir()
|
||||
cache.write_text(
|
||||
f"PICO_SDK_PATH:PATH={tmp_path / 'missing-sdk'}\n"
|
||||
f"PICO_TOOLCHAIN_PATH:PATH={tmp_path / 'missing-toolchain'}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(build_script, "BUILD_DIR", project_build)
|
||||
environ = {"PATH": ""}
|
||||
|
||||
updates = build_script.configure_pico_environment(
|
||||
environ=environ,
|
||||
cache_paths=[cache],
|
||||
which=no_compiler,
|
||||
)
|
||||
|
||||
assert updates == {
|
||||
"PICO_SDK_PATH": str(local_sdk),
|
||||
"PICO_TOOLCHAIN_PATH": str(local_toolchain),
|
||||
}
|
||||
|
||||
|
||||
def test_compiler_on_path_avoids_toolchain_override(tmp_path):
|
||||
sdk = make_sdk(tmp_path / "sdk")
|
||||
compiler = tmp_path / "path-bin" / build_script.TOOLCHAIN_COMPILER
|
||||
environ = {"PATH": str(compiler.parent), "PICO_SDK_PATH": str(sdk)}
|
||||
|
||||
updates = build_script.configure_pico_environment(
|
||||
environ=environ,
|
||||
cache_paths=[],
|
||||
sdk_candidates=[],
|
||||
toolchain_candidates=[],
|
||||
which=lambda name, *, path: str(compiler),
|
||||
)
|
||||
|
||||
assert updates == {}
|
||||
assert "PICO_TOOLCHAIN_PATH" not in environ
|
||||
|
||||
|
||||
def test_missing_dependencies_name_only_actionable_overrides():
|
||||
environ = {"PATH": ""}
|
||||
|
||||
with pytest.raises(build_script.BuildEnvironmentError) as error:
|
||||
build_script.configure_pico_environment(
|
||||
environ=environ,
|
||||
cache_paths=[],
|
||||
sdk_candidates=[],
|
||||
toolchain_candidates=[],
|
||||
which=no_compiler,
|
||||
)
|
||||
|
||||
assert str(error.value) == (
|
||||
"Missing build prerequisite(s): Pico SDK (set PICO_SDK_PATH); "
|
||||
"Arm GNU toolchain (set PICO_TOOLCHAIN_PATH or add "
|
||||
"arm-none-eabi-gcc to PATH)."
|
||||
)
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -58,45 +56,7 @@ class FakeDevice:
|
|||
),
|
||||
]
|
||||
self.pairing_generation = 4
|
||||
self.global_identity = config_manager.ControllerIdentity.global_fallback()
|
||||
self.stable_identity = config_manager.ControllerIdentity(
|
||||
True,
|
||||
config_manager.TRANSPORT_CLASSIC,
|
||||
0,
|
||||
bytes.fromhex("102030405060"),
|
||||
0x045E,
|
||||
0x02FD,
|
||||
)
|
||||
self.profile_identities = [
|
||||
self.global_identity,
|
||||
self.stable_identity,
|
||||
]
|
||||
self.active_profiles = {
|
||||
identity.to_bytes(): index
|
||||
for identity, index in zip(self.profile_identities, (0, 1))
|
||||
}
|
||||
default_profile = config_manager.ControllerProfile.default().to_bytes()
|
||||
self.profiles = {
|
||||
(identity.to_bytes(), index): default_profile
|
||||
for identity in self.profile_identities
|
||||
for index in range(config_manager.PROFILE_CAPACITY)
|
||||
}
|
||||
self.selected_profile = (self.global_identity.to_bytes(), 0)
|
||||
self.profile_generation = 7
|
||||
self.profile_transaction_id = 0
|
||||
self.profile_transaction_identity = self.global_identity.to_bytes()
|
||||
self.profile_transaction_index = 0
|
||||
self.profile_transaction_payload = bytearray()
|
||||
self.profile_transaction_expected_size = 0
|
||||
self.profile_transaction_expected_crc = 0
|
||||
self.profile_transaction_status = config_manager.STATUS_OK
|
||||
self.fail_profile_commit_status: int | None = None
|
||||
self.bad_profile_response_crc = False
|
||||
self.requests: list[int] = []
|
||||
self.profile_chunk_sizes: list[int] = []
|
||||
self.pending_profile_mutation: tuple[int, bytes, int] | None = None
|
||||
self.profile_transaction_pending_reads = 0
|
||||
self.profile_status_responses: list[tuple[int, int]] = []
|
||||
|
||||
def _pairing_payload(self) -> bytes:
|
||||
payload = bytearray([len(self.records), 0, 0, 0])
|
||||
|
|
@ -117,71 +77,6 @@ class FakeDevice:
|
|||
stored_crc,
|
||||
)
|
||||
|
||||
def _profile_list_payload(self) -> bytes:
|
||||
payload = bytearray([len(self.profile_identities)])
|
||||
for identity in self.profile_identities:
|
||||
payload.extend(identity.to_bytes())
|
||||
payload.extend((self.active_profiles[identity.to_bytes()], 0))
|
||||
return bytes(payload)
|
||||
|
||||
def _profile_transaction_payload(self) -> bytes:
|
||||
stored = self.profiles.get(
|
||||
(
|
||||
self.profile_transaction_identity,
|
||||
self.profile_transaction_index,
|
||||
),
|
||||
bytes(config_manager.PROFILE_SIZE),
|
||||
)
|
||||
return struct.pack(
|
||||
"<IHHIII",
|
||||
self.profile_transaction_id,
|
||||
len(self.profile_transaction_payload),
|
||||
self.profile_transaction_expected_size,
|
||||
self.profile_transaction_expected_crc,
|
||||
self.profile_generation,
|
||||
zlib.crc32(stored) & 0xFFFFFFFF,
|
||||
)
|
||||
|
||||
def _queue_profile_mutation(self, operation: int, payload: bytes) -> None:
|
||||
assert len(payload) == 19
|
||||
self.profile_transaction_id = struct.unpack_from("<I", payload)[0]
|
||||
assert self.profile_transaction_id != 0
|
||||
self.profile_transaction_identity = payload[4:18]
|
||||
self.profile_transaction_index = payload[18]
|
||||
self.profile_transaction_payload = bytearray()
|
||||
self.profile_transaction_expected_size = 0
|
||||
self.profile_transaction_expected_crc = 0
|
||||
self.profile_transaction_status = config_manager.STATUS_PENDING
|
||||
self.profile_transaction_pending_reads = 1
|
||||
self.pending_profile_mutation = (
|
||||
operation,
|
||||
self.profile_transaction_identity,
|
||||
self.profile_transaction_index,
|
||||
)
|
||||
|
||||
def _complete_profile_mutation(self) -> None:
|
||||
assert self.pending_profile_mutation is not None
|
||||
operation, identity, profile_index = self.pending_profile_mutation
|
||||
if self.fail_profile_commit_status is not None:
|
||||
self.profile_transaction_status = self.fail_profile_commit_status
|
||||
self.pending_profile_mutation = None
|
||||
return
|
||||
if operation == config_manager.OP_PROFILE_RESET:
|
||||
indices = (
|
||||
range(config_manager.PROFILE_CAPACITY)
|
||||
if profile_index == config_manager.PROFILE_NONE_BUTTON
|
||||
else (profile_index,)
|
||||
)
|
||||
default = config_manager.ControllerProfile.default().to_bytes()
|
||||
for reset_index in indices:
|
||||
self.profiles[(identity, reset_index)] = default
|
||||
else:
|
||||
assert operation == config_manager.OP_PROFILE_ACTIVATE
|
||||
self.active_profiles[identity] = profile_index
|
||||
self.profile_generation += 1
|
||||
self.profile_transaction_status = config_manager.STATUS_OK
|
||||
self.pending_profile_mutation = None
|
||||
|
||||
def ctrl_transfer(
|
||||
self,
|
||||
bm_request_type: int,
|
||||
|
|
@ -197,9 +92,7 @@ class FakeDevice:
|
|||
self.requests.append(request)
|
||||
if bm_request_type == 0xC0:
|
||||
if request == config_manager.OP_INFO:
|
||||
return make_response(
|
||||
request, bytes([0, 2, 0, 2, 0, 0, 0, 2])
|
||||
)
|
||||
return make_response(request, bytes([0, 2, 0, 2, 0, 0, 0, 2]))
|
||||
if request == config_manager.OP_CONFIGURATION_READ:
|
||||
return make_response(
|
||||
request,
|
||||
|
|
@ -221,44 +114,6 @@ class FakeDevice:
|
|||
self._pairing_payload(),
|
||||
generation=self.pairing_generation,
|
||||
)
|
||||
if request == config_manager.OP_PROFILE_LIST:
|
||||
return make_response(
|
||||
request,
|
||||
self._profile_list_payload(),
|
||||
schema=config_manager.PROFILE_SCHEMA_VERSION,
|
||||
generation=self.profile_generation,
|
||||
)
|
||||
if request == config_manager.OP_PROFILE_READ:
|
||||
response = bytearray(
|
||||
make_response(
|
||||
request,
|
||||
self.profiles[self.selected_profile],
|
||||
schema=config_manager.PROFILE_SCHEMA_VERSION,
|
||||
generation=self.profile_generation,
|
||||
)
|
||||
)
|
||||
if self.bad_profile_response_crc:
|
||||
response[-1] ^= 1
|
||||
return bytes(response)
|
||||
if request == config_manager.OP_PROFILE_TRANSACTION_STATUS:
|
||||
if self.profile_transaction_status == config_manager.STATUS_PENDING:
|
||||
if self.profile_transaction_pending_reads:
|
||||
self.profile_transaction_pending_reads -= 1
|
||||
elif self.pending_profile_mutation is not None:
|
||||
self._complete_profile_mutation()
|
||||
self.profile_status_responses.append(
|
||||
(
|
||||
self.profile_transaction_id,
|
||||
self.profile_transaction_status,
|
||||
)
|
||||
)
|
||||
return make_response(
|
||||
request,
|
||||
self._profile_transaction_payload(),
|
||||
status=self.profile_transaction_status,
|
||||
schema=config_manager.PROFILE_SCHEMA_VERSION,
|
||||
generation=self.profile_generation,
|
||||
)
|
||||
raise AssertionError(f"unexpected IN request {request}")
|
||||
|
||||
assert bm_request_type == 0x40
|
||||
|
|
@ -310,138 +165,11 @@ class FakeDevice:
|
|||
elif request == config_manager.OP_PAIRING_CLEAR:
|
||||
self.records = []
|
||||
self.pairing_generation += 1
|
||||
elif request == config_manager.OP_PROFILE_SELECT:
|
||||
assert len(payload) == 15
|
||||
self.selected_profile = (payload[:14], payload[14])
|
||||
assert self.selected_profile in self.profiles
|
||||
elif request == config_manager.OP_PROFILE_BEGIN:
|
||||
assert len(payload) == 28
|
||||
self.profile_transaction_id = struct.unpack_from("<I", payload)[0]
|
||||
self.profile_transaction_identity = payload[4:18]
|
||||
(
|
||||
self.profile_transaction_index,
|
||||
reserved,
|
||||
schema,
|
||||
self.profile_transaction_expected_size,
|
||||
self.profile_transaction_expected_crc,
|
||||
) = struct.unpack_from("<BBHHI", payload, 18)
|
||||
assert reserved == 0
|
||||
assert schema == config_manager.PROFILE_SCHEMA_VERSION
|
||||
assert (
|
||||
self.profile_transaction_expected_size
|
||||
== config_manager.PROFILE_SIZE
|
||||
)
|
||||
self.profile_transaction_payload = bytearray()
|
||||
self.profile_transaction_status = config_manager.STATUS_PENDING
|
||||
self.profile_chunk_sizes = []
|
||||
elif request == config_manager.OP_PROFILE_CHUNK:
|
||||
transaction_id, offset, chunk_size = struct.unpack_from(
|
||||
"<IHH", payload
|
||||
)
|
||||
assert transaction_id == self.profile_transaction_id
|
||||
assert offset == len(self.profile_transaction_payload)
|
||||
chunk = payload[8 : 8 + chunk_size]
|
||||
assert len(chunk) == chunk_size
|
||||
self.profile_transaction_payload.extend(chunk)
|
||||
self.profile_chunk_sizes.append(chunk_size)
|
||||
elif request == config_manager.OP_PROFILE_COMMIT:
|
||||
assert (
|
||||
struct.unpack("<I", payload)[0]
|
||||
== self.profile_transaction_id
|
||||
)
|
||||
assert (
|
||||
len(self.profile_transaction_payload)
|
||||
== self.profile_transaction_expected_size
|
||||
)
|
||||
assert (
|
||||
zlib.crc32(self.profile_transaction_payload) & 0xFFFFFFFF
|
||||
) == self.profile_transaction_expected_crc
|
||||
if self.fail_profile_commit_status is None:
|
||||
key = (
|
||||
self.profile_transaction_identity,
|
||||
self.profile_transaction_index,
|
||||
)
|
||||
self.profiles[key] = bytes(self.profile_transaction_payload)
|
||||
self.profile_generation += 1
|
||||
self.profile_transaction_status = config_manager.STATUS_OK
|
||||
else:
|
||||
self.profile_transaction_status = (
|
||||
self.fail_profile_commit_status
|
||||
)
|
||||
elif request == config_manager.OP_PROFILE_RESET:
|
||||
self._queue_profile_mutation(request, payload)
|
||||
assert (
|
||||
self.profile_transaction_index
|
||||
== config_manager.PROFILE_NONE_BUTTON
|
||||
or 0
|
||||
<= self.profile_transaction_index
|
||||
< config_manager.PROFILE_CAPACITY
|
||||
)
|
||||
elif request == config_manager.OP_PROFILE_ACTIVATE:
|
||||
self._queue_profile_mutation(request, payload)
|
||||
assert (
|
||||
0
|
||||
<= self.profile_transaction_index
|
||||
< config_manager.PROFILE_CAPACITY
|
||||
)
|
||||
else:
|
||||
raise AssertionError(f"unexpected OUT request {request}")
|
||||
return len(encoded)
|
||||
|
||||
|
||||
def custom_profile() -> config_manager.ControllerProfile:
|
||||
return config_manager.ControllerProfile(
|
||||
button_map=(
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
15,
|
||||
config_manager.PROFILE_NONE_BUTTON,
|
||||
),
|
||||
left_stick=config_manager.StickConfig(
|
||||
-123, 456, 1000, 30000, 384, True, False
|
||||
),
|
||||
right_stick=config_manager.StickConfig(
|
||||
789, -321, 500, 31000, 192, False, True
|
||||
),
|
||||
left_trigger=config_manager.TriggerConfig(100, 65000, 320, 32000),
|
||||
right_trigger=config_manager.TriggerConfig(200, 64000, 224, 33000),
|
||||
weak_rumble_scale=77,
|
||||
strong_rumble_scale=201,
|
||||
confirmation_policy=2,
|
||||
switching_chord=(1 << 6) | (1 << 7),
|
||||
macro_trigger=0,
|
||||
macro_cancel=1,
|
||||
macro_steps=(
|
||||
config_manager.MacroStep(
|
||||
0,
|
||||
config_manager.MACRO_OVERRIDE_MASK,
|
||||
config_manager.PROFILE_MAXIMUM_WAIT_MS,
|
||||
(1 << 0) | (1 << 12),
|
||||
-32768,
|
||||
32767,
|
||||
-1000,
|
||||
1000,
|
||||
12345,
|
||||
54321,
|
||||
),
|
||||
config_manager.MacroStep.end(),
|
||||
),
|
||||
turbo_modes=(0, 1, 2) + (0,) * 13,
|
||||
)
|
||||
|
||||
|
||||
def test_response_validation() -> None:
|
||||
payload = make_response(config_manager.OP_INFO, b"12345678")
|
||||
envelope = config_manager.parse_response(payload, config_manager.OP_INFO)
|
||||
|
|
@ -477,500 +205,6 @@ def test_configuration_transaction_and_reset() -> None:
|
|||
assert config_manager.read_configuration(device).pairing_window_seconds == 60
|
||||
|
||||
|
||||
def test_identity_and_profile_binary_json_round_trip() -> None:
|
||||
identity = config_manager.ControllerIdentity(
|
||||
True,
|
||||
config_manager.TRANSPORT_BLE,
|
||||
3,
|
||||
bytes.fromhex("A1B2C3D4E5F6"),
|
||||
0x1234,
|
||||
0xABCD,
|
||||
)
|
||||
encoded_identity = identity.to_bytes()
|
||||
assert encoded_identity == bytes.fromhex(
|
||||
"01020300A1B2C3D4E5F63412CDAB"
|
||||
)
|
||||
assert config_manager.ControllerIdentity.from_bytes(encoded_identity) == identity
|
||||
assert (
|
||||
config_manager.ControllerIdentity.global_fallback().to_bytes()
|
||||
== bytes(config_manager.CONTROLLER_IDENTITY_SIZE)
|
||||
)
|
||||
malformed_identity = bytearray(encoded_identity)
|
||||
malformed_identity[3] = 1
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerIdentity.from_bytes(malformed_identity)
|
||||
|
||||
default_profile = config_manager.ControllerProfile.default()
|
||||
assert default_profile.left_trigger.digital_threshold == 22934
|
||||
assert default_profile.right_trigger.digital_threshold == 22934
|
||||
default_wire = default_profile.to_bytes()
|
||||
assert struct.unpack_from("<H", default_wire, 58)[0] == 22934
|
||||
assert struct.unpack_from("<H", default_wire, 68)[0] == 22934
|
||||
|
||||
profile = custom_profile()
|
||||
encoded = profile.to_bytes()
|
||||
assert len(encoded) == config_manager.PROFILE_SIZE
|
||||
assert struct.unpack_from("<HH", encoded) == (
|
||||
config_manager.PROFILE_SCHEMA_VERSION,
|
||||
config_manager.PROFILE_SIZE,
|
||||
)
|
||||
assert encoded[75] == encoded[81] == 0
|
||||
assert encoded[98:100] == b"\x00\x00"
|
||||
assert encoded[252:] == bytes(4)
|
||||
assert config_manager.ControllerProfile.from_bytes(encoded) == profile
|
||||
|
||||
serialized = profile.to_json()
|
||||
assert serialized.startswith('{\n "schema_version": 2,\n "size": 256,')
|
||||
decoded = config_manager.ControllerProfile.from_json(serialized)
|
||||
assert decoded == profile
|
||||
assert decoded.to_json() == serialized
|
||||
|
||||
legacy_default_wire = bytearray(default_wire)
|
||||
struct.pack_into(
|
||||
"<H",
|
||||
legacy_default_wire,
|
||||
0,
|
||||
config_manager.PROFILE_LEGACY_SCHEMA_VERSION,
|
||||
)
|
||||
struct.pack_into(
|
||||
"<HHHH",
|
||||
legacy_default_wire,
|
||||
52,
|
||||
30000,
|
||||
40000,
|
||||
256,
|
||||
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD,
|
||||
)
|
||||
struct.pack_into(
|
||||
"<HHHH",
|
||||
legacy_default_wire,
|
||||
62,
|
||||
30000,
|
||||
40000,
|
||||
256,
|
||||
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD,
|
||||
)
|
||||
migrated_default = config_manager.ControllerProfile.from_bytes(
|
||||
legacy_default_wire
|
||||
)
|
||||
assert (
|
||||
migrated_default.left_trigger.digital_threshold
|
||||
== config_manager.PROFILE_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
assert (
|
||||
migrated_default.right_trigger.digital_threshold
|
||||
== config_manager.PROFILE_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
assert migrated_default.left_trigger.lower_deadzone == 30000
|
||||
assert migrated_default.left_trigger.upper_saturation == 40000
|
||||
assert migrated_default.right_trigger.lower_deadzone == 30000
|
||||
assert migrated_default.right_trigger.upper_saturation == 40000
|
||||
assert migrated_default.to_bytes()[0] == config_manager.PROFILE_SCHEMA_VERSION
|
||||
|
||||
current_old_value_wire = bytearray(default_wire)
|
||||
struct.pack_into(
|
||||
"<H",
|
||||
current_old_value_wire,
|
||||
58,
|
||||
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD,
|
||||
)
|
||||
assert (
|
||||
config_manager.ControllerProfile.from_bytes(
|
||||
current_old_value_wire
|
||||
).left_trigger.digital_threshold
|
||||
== config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
|
||||
legacy_custom_wire = bytearray(encoded)
|
||||
struct.pack_into(
|
||||
"<H",
|
||||
legacy_custom_wire,
|
||||
0,
|
||||
config_manager.PROFILE_LEGACY_SCHEMA_VERSION,
|
||||
)
|
||||
assert (
|
||||
config_manager.ControllerProfile.from_bytes(legacy_custom_wire)
|
||||
== profile
|
||||
)
|
||||
|
||||
legacy_json_object = default_profile.to_json_object()
|
||||
legacy_json_object["schema_version"] = (
|
||||
config_manager.PROFILE_LEGACY_SCHEMA_VERSION
|
||||
)
|
||||
legacy_json_object["triggers"]["left"]["digital_threshold"] = (
|
||||
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
legacy_json_object["triggers"]["right"]["digital_threshold"] = 33000
|
||||
legacy_json_object["triggers"]["left"]["lower_deadzone"] = 30000
|
||||
legacy_json_object["triggers"]["left"]["upper_saturation"] = 40000
|
||||
migrated_json = config_manager.ControllerProfile.from_json_object(
|
||||
legacy_json_object
|
||||
)
|
||||
assert (
|
||||
migrated_json.left_trigger.digital_threshold
|
||||
== config_manager.PROFILE_DEFAULT_DIGITAL_THRESHOLD
|
||||
)
|
||||
assert migrated_json.right_trigger.digital_threshold == 33000
|
||||
assert migrated_json.left_trigger.lower_deadzone == 30000
|
||||
assert migrated_json.left_trigger.upper_saturation == 40000
|
||||
|
||||
|
||||
def test_trigger_threshold_uses_transformed_output_domain() -> None:
|
||||
for threshold in (0, 0xFFFF):
|
||||
trigger = config_manager.TriggerConfig(30000, 40000, 256, threshold)
|
||||
assert config_manager.TriggerConfig.from_bytes(trigger.to_bytes()) == trigger
|
||||
assert (
|
||||
config_manager.TriggerConfig.from_json_object(
|
||||
trigger.to_json_object(), "trigger"
|
||||
)
|
||||
== trigger
|
||||
)
|
||||
|
||||
current_wire = bytearray(
|
||||
config_manager.ControllerProfile.default().to_bytes()
|
||||
)
|
||||
struct.pack_into(
|
||||
"<HHHH", current_wire, 52, 30000, 40000, 256, threshold
|
||||
)
|
||||
current_profile = config_manager.ControllerProfile.from_bytes(
|
||||
current_wire
|
||||
)
|
||||
assert current_profile.left_trigger.digital_threshold == threshold
|
||||
assert current_profile.to_bytes() == current_wire
|
||||
|
||||
for threshold in (-1, 0x10000):
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError,
|
||||
match="trigger digital_threshold",
|
||||
):
|
||||
config_manager.TriggerConfig(30000, 40000, 256, threshold)
|
||||
|
||||
for lower_deadzone, upper_saturation in (
|
||||
(40000, 40000),
|
||||
(40001, 40000),
|
||||
):
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError,
|
||||
match="lower_deadzone must be below upper_saturation",
|
||||
):
|
||||
config_manager.TriggerConfig(
|
||||
lower_deadzone, upper_saturation, 256, 0
|
||||
)
|
||||
|
||||
|
||||
def test_default_timeout_covers_batched_profile_commit() -> None:
|
||||
args = config_manager.build_parser().parse_args(["profiles", "list"])
|
||||
assert (
|
||||
args.timeout
|
||||
== config_manager.DEFAULT_OPERATION_TIMEOUT_SECONDS
|
||||
== 15.0
|
||||
)
|
||||
|
||||
|
||||
def test_profile_list_select_read_and_chunked_commit() -> None:
|
||||
device = FakeDevice()
|
||||
entries = config_manager.list_profiles(device)
|
||||
assert entries == (
|
||||
config_manager.ProfileListEntry(device.global_identity, 0),
|
||||
config_manager.ProfileListEntry(device.stable_identity, 1),
|
||||
)
|
||||
assert (
|
||||
config_manager.read_profile(device, device.stable_identity, 1)
|
||||
== config_manager.ControllerProfile.default()
|
||||
)
|
||||
assert device.requests[-2:] == [
|
||||
config_manager.OP_PROFILE_SELECT,
|
||||
config_manager.OP_PROFILE_READ,
|
||||
]
|
||||
|
||||
profile = custom_profile()
|
||||
status = config_manager.write_profile(
|
||||
device, device.stable_identity, 2, profile, 1.0
|
||||
)
|
||||
assert status.status == config_manager.STATUS_OK
|
||||
assert status.stored_generation == 8
|
||||
assert device.profile_chunk_sizes == [40, 40, 40, 40, 40, 40, 16]
|
||||
assert config_manager.OP_PROFILE_TRANSACTION_STATUS in device.requests
|
||||
assert (
|
||||
config_manager.read_profile(device, device.stable_identity, 2)
|
||||
== profile
|
||||
)
|
||||
|
||||
|
||||
def test_profile_reset_and_activate_wait_for_correlated_transactions(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
generated_ids = iter((0, 0xA5A55A5A))
|
||||
monkeypatch.setattr(
|
||||
config_manager.secrets, "randbits", lambda _bits: next(generated_ids)
|
||||
)
|
||||
identity = device.stable_identity
|
||||
profile_key = (identity.to_bytes(), 2)
|
||||
device.profiles[profile_key] = custom_profile().to_bytes()
|
||||
|
||||
reset = config_manager.reset_profile(device, identity, 2, 1.0)
|
||||
|
||||
assert reset.transaction_id == device.profile_transaction_id == 1
|
||||
assert reset.status == config_manager.STATUS_OK
|
||||
assert reset.stored_generation == 8
|
||||
assert device.profile_status_responses == [
|
||||
(1, config_manager.STATUS_PENDING),
|
||||
(1, config_manager.STATUS_OK),
|
||||
]
|
||||
assert (
|
||||
device.profiles[profile_key]
|
||||
== config_manager.ControllerProfile.default().to_bytes()
|
||||
)
|
||||
|
||||
device.profile_status_responses.clear()
|
||||
activated = config_manager.activate_profile(device, identity, 3, 1.0)
|
||||
|
||||
assert (
|
||||
activated.transaction_id
|
||||
== device.profile_transaction_id
|
||||
== 0xA5A55A5A
|
||||
)
|
||||
assert activated.status == config_manager.STATUS_OK
|
||||
assert activated.stored_generation == 9
|
||||
assert device.profile_status_responses == [
|
||||
(0xA5A55A5A, config_manager.STATUS_PENDING),
|
||||
(0xA5A55A5A, config_manager.STATUS_OK),
|
||||
]
|
||||
assert device.active_profiles[identity.to_bytes()] == 3
|
||||
|
||||
|
||||
def test_profile_cli_surfaces_late_storage_failure(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
device.fail_profile_commit_status = 8
|
||||
monkeypatch.setattr(config_manager, "_candidate_devices", lambda: [device])
|
||||
previous_active = device.active_profiles[device.stable_identity.to_bytes()]
|
||||
|
||||
assert (
|
||||
config_manager.main(
|
||||
["profiles", "activate", "4", "--identity", "1"]
|
||||
)
|
||||
== 1
|
||||
)
|
||||
|
||||
output = capsys.readouterr()
|
||||
assert output.out == ""
|
||||
assert "storage failure" in output.err
|
||||
assert (
|
||||
device.active_profiles[device.stable_identity.to_bytes()]
|
||||
== previous_active
|
||||
)
|
||||
assert device.profile_status_responses == [
|
||||
(device.profile_transaction_id, config_manager.STATUS_PENDING),
|
||||
(device.profile_transaction_id, 8),
|
||||
]
|
||||
|
||||
|
||||
def test_profile_cli_json_round_trip_activate_and_reset(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
monkeypatch.setattr(config_manager, "_candidate_devices", lambda: [device])
|
||||
export_path = tmp_path / "profile.json"
|
||||
|
||||
assert config_manager.main(["profiles", "list"]) == 0
|
||||
output = capsys.readouterr().out
|
||||
assert "0: global fallback (active profile 1)" in output
|
||||
assert "1: Classic 10:20:30:40:50:60" in output
|
||||
|
||||
device.profiles[(device.stable_identity.to_bytes(), 1)] = (
|
||||
custom_profile().to_bytes()
|
||||
)
|
||||
assert (
|
||||
config_manager.main(
|
||||
[
|
||||
"profiles",
|
||||
"export",
|
||||
"2",
|
||||
str(export_path),
|
||||
"--identity",
|
||||
"1",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
_ = capsys.readouterr()
|
||||
exported = config_manager.ControllerProfile.from_json(
|
||||
export_path.read_text(encoding="utf-8")
|
||||
)
|
||||
assert exported == custom_profile()
|
||||
|
||||
device.profiles[(device.stable_identity.to_bytes(), 1)] = (
|
||||
config_manager.ControllerProfile.default().to_bytes()
|
||||
)
|
||||
assert (
|
||||
config_manager.main(
|
||||
[
|
||||
"profiles",
|
||||
"import",
|
||||
"2",
|
||||
str(export_path),
|
||||
"--identity",
|
||||
"1",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
assert (
|
||||
device.profiles[(device.stable_identity.to_bytes(), 1)]
|
||||
== custom_profile().to_bytes()
|
||||
)
|
||||
_ = capsys.readouterr()
|
||||
|
||||
assert (
|
||||
config_manager.main(
|
||||
["profiles", "activate", "4", "--identity", "1"]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
assert device.active_profiles[device.stable_identity.to_bytes()] == 3
|
||||
_ = capsys.readouterr()
|
||||
|
||||
before_reset_requests = len(device.requests)
|
||||
assert (
|
||||
config_manager.main(
|
||||
["profiles", "reset", "2", "--identity", "1"]
|
||||
)
|
||||
== 2
|
||||
)
|
||||
assert "requires --yes" in capsys.readouterr().err
|
||||
assert len(device.requests) == before_reset_requests
|
||||
|
||||
assert (
|
||||
config_manager.main(
|
||||
[
|
||||
"profiles",
|
||||
"reset",
|
||||
"2",
|
||||
"--identity",
|
||||
"1",
|
||||
"--yes",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
assert (
|
||||
device.profiles[(device.stable_identity.to_bytes(), 1)]
|
||||
== config_manager.ControllerProfile.default().to_bytes()
|
||||
)
|
||||
_ = capsys.readouterr()
|
||||
assert (
|
||||
config_manager.main(
|
||||
[
|
||||
"profiles",
|
||||
"reset",
|
||||
"all",
|
||||
"--identity",
|
||||
"1",
|
||||
"--yes",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
|
||||
|
||||
def test_malformed_profiles_are_rejected_before_usb(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
malformed_binary = bytearray(config_manager.ControllerProfile.default().to_bytes())
|
||||
malformed_binary[75] = 1
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError, match="reserved fields"
|
||||
):
|
||||
config_manager.ControllerProfile.from_bytes(malformed_binary)
|
||||
|
||||
profile_object = config_manager.ControllerProfile.default().to_json_object()
|
||||
del profile_object["turbo"]
|
||||
missing_path = tmp_path / "missing.json"
|
||||
missing_path.write_text(json.dumps(profile_object), encoding="utf-8")
|
||||
|
||||
profile_object = config_manager.ControllerProfile.default().to_json_object()
|
||||
profile_object["reserved"] = 0
|
||||
unknown_path = tmp_path / "unknown.json"
|
||||
unknown_path.write_text(json.dumps(profile_object), encoding="utf-8")
|
||||
|
||||
profile_object = config_manager.ControllerProfile.default().to_json_object()
|
||||
profile_object["rumble"]["confirmation_policy"] = "invalid"
|
||||
enum_path = tmp_path / "enum.json"
|
||||
enum_path.write_text(json.dumps(profile_object), encoding="utf-8")
|
||||
|
||||
profile_object = config_manager.ControllerProfile.default().to_json_object()
|
||||
profile_object["sticks"]["left"]["outer_saturation"] = 0
|
||||
range_path = tmp_path / "range.json"
|
||||
range_path.write_text(json.dumps(profile_object), encoding="utf-8")
|
||||
|
||||
usb_lookups = 0
|
||||
|
||||
def candidates() -> list[FakeDevice]:
|
||||
nonlocal usb_lookups
|
||||
usb_lookups += 1
|
||||
return [FakeDevice()]
|
||||
|
||||
monkeypatch.setattr(config_manager, "_candidate_devices", candidates)
|
||||
for path in (missing_path, unknown_path, enum_path, range_path):
|
||||
assert (
|
||||
config_manager.main(["profiles", "import", "1", str(path)]) == 1
|
||||
)
|
||||
assert "error:" in capsys.readouterr().err
|
||||
assert usb_lookups == 0
|
||||
|
||||
|
||||
def test_profile_crc_status_failures_and_identity_bounds(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
device.bad_profile_response_crc = True
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError, match="response CRC mismatch"
|
||||
):
|
||||
config_manager.read_profile(device, device.global_identity, 0)
|
||||
|
||||
failing_device = FakeDevice()
|
||||
failing_device.fail_profile_commit_status = 6
|
||||
with pytest.raises(config_manager.ConfigManagerError, match="CRC mismatch"):
|
||||
config_manager.write_profile(
|
||||
failing_device,
|
||||
failing_device.global_identity,
|
||||
0,
|
||||
custom_profile(),
|
||||
1.0,
|
||||
)
|
||||
|
||||
bounded_device = FakeDevice()
|
||||
monkeypatch.setattr(
|
||||
config_manager, "_candidate_devices", lambda: [bounded_device]
|
||||
)
|
||||
assert (
|
||||
config_manager.main(
|
||||
[
|
||||
"profiles",
|
||||
"export",
|
||||
"1",
|
||||
str(tmp_path / "unused.json"),
|
||||
"--identity",
|
||||
"2",
|
||||
]
|
||||
)
|
||||
== 1
|
||||
)
|
||||
assert "identity index 2 is out of range" in capsys.readouterr().err
|
||||
assert config_manager.OP_PROFILE_SELECT not in bounded_device.requests
|
||||
|
||||
|
||||
def test_status_and_pairing_commands(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_controller_profile_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "controller_profile_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "controller_profile_test.cpp"),
|
||||
str(root / "controller_identity.cpp"),
|
||||
str(root / "controller_profile.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_controller_profile_runtime_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "controller_profile_runtime_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "controller_profile_runtime_test.cpp"),
|
||||
str(root / "controller_identity.cpp"),
|
||||
str(root / "controller_profile.cpp"),
|
||||
str(root / "controller_profile_transform.cpp"),
|
||||
str(root / "controller_synthetic_input.cpp"),
|
||||
str(root / "controller_profile_runtime.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_controller_profile_transform_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "controller_profile_transform_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "controller_profile_transform_test.cpp"),
|
||||
str(root / "controller_identity.cpp"),
|
||||
str(root / "controller_profile.cpp"),
|
||||
str(root / "controller_profile_transform.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_controller_synthetic_input_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "controller_synthetic_input_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "controller_synthetic_input_test.cpp"),
|
||||
str(root / "controller_identity.cpp"),
|
||||
str(root / "controller_profile.cpp"),
|
||||
str(root / "controller_profile_transform.cpp"),
|
||||
str(root / "controller_synthetic_input.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_profile_service_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "profile_service_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "profile_service_test.cpp"),
|
||||
str(root / "controller_identity.cpp"),
|
||||
str(root / "controller_profile.cpp"),
|
||||
str(root / "profile_storage.cpp"),
|
||||
str(root / "profile_service.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_profile_storage_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "profile_storage_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "profile_storage_test.cpp"),
|
||||
str(root / "controller_identity.cpp"),
|
||||
str(root / "controller_profile.cpp"),
|
||||
str(root / "profile_storage.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -11,9 +11,6 @@ namespace {
|
|||
|
||||
Bluepad32PairingSnapshot current_pairings{};
|
||||
ConfigurationServiceSnapshot current_configuration{};
|
||||
ProfileServiceListSnapshot current_profile_list{};
|
||||
ProfileServiceSelectedSnapshot current_profile_selected{};
|
||||
ProfileServiceTransactionSnapshot current_profile_transaction{};
|
||||
bool refresh_requested = false;
|
||||
bool clear_requested = false;
|
||||
std::vector<uint8_t> control_payload;
|
||||
|
|
@ -23,16 +20,6 @@ uint32_t append_transaction_id = 0;
|
|||
uint32_t commit_transaction_id = 0;
|
||||
size_t append_offset = 0;
|
||||
std::vector<uint8_t> appended_bytes;
|
||||
ControllerIdentity profile_identity{};
|
||||
uint8_t profile_index = 0;
|
||||
uint16_t profile_schema = 0;
|
||||
size_t profile_size = 0;
|
||||
uint32_t profile_crc = 0;
|
||||
bool profile_reset_requested = false;
|
||||
uint32_t profile_reset_transaction_id = 0;
|
||||
uint32_t profile_commit_transaction_id = 0;
|
||||
bool profile_activate_requested = false;
|
||||
uint32_t profile_activate_transaction_id = 0;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
|
|
@ -55,13 +42,6 @@ void write_u32(std::vector<uint8_t>* output, size_t offset,
|
|||
(*output)[offset + 3] = static_cast<uint8_t>(value >> 24);
|
||||
}
|
||||
|
||||
uint32_t read_u32(const std::vector<uint8_t>& input, size_t offset) {
|
||||
return static_cast<uint32_t>(input[offset]) |
|
||||
(static_cast<uint32_t>(input[offset + 1]) << 8) |
|
||||
(static_cast<uint32_t>(input[offset + 2]) << 16) |
|
||||
(static_cast<uint32_t>(input[offset + 3]) << 24);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> make_request(
|
||||
UsbConfigurationManagement::Operation operation,
|
||||
const std::vector<uint8_t>& payload) {
|
||||
|
|
@ -233,178 +213,6 @@ void test_vendor_requests() {
|
|||
"request with invalid magic was accepted");
|
||||
}
|
||||
|
||||
void test_profile_vendor_requests() {
|
||||
using namespace UsbConfigurationManagement;
|
||||
ControllerIdentity expected_identity{};
|
||||
expected_identity.stable = true;
|
||||
expected_identity.transport = ControllerTransport::kClassic;
|
||||
expected_identity.address[5] = 7;
|
||||
expected_identity.vendor_id = 0x057e;
|
||||
expected_identity.product_id = 0x2009;
|
||||
|
||||
current_profile_list = {};
|
||||
current_profile_list.metadata.state = ProfileServiceState::kReady;
|
||||
current_profile_list.metadata.generation = 9;
|
||||
current_profile_list.count = 2;
|
||||
current_profile_list.rows[0].identity = controller_identity_global();
|
||||
current_profile_list.rows[1].identity = expected_identity;
|
||||
current_profile_list.rows[1].active_profile = 2;
|
||||
tusb_control_request_t request = setup_request(
|
||||
Operation::kProfileList, TUSB_DIR_IN, kMaximumResponseSize);
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload.size() == kResponseHeaderSize + 33 &&
|
||||
control_payload[5] ==
|
||||
static_cast<uint8_t>(Operation::kProfileList) &&
|
||||
control_payload[10] == CONTROLLER_PROFILE_SCHEMA_VERSION &&
|
||||
control_payload[kResponseHeaderSize] == 2 &&
|
||||
control_payload[kResponseHeaderSize + 31] == 2,
|
||||
"profile list response was not encoded");
|
||||
|
||||
current_profile_selected = {};
|
||||
current_profile_selected.metadata.state =
|
||||
ProfileServiceState::kReady;
|
||||
current_profile_selected.metadata.generation = 9;
|
||||
current_profile_selected.valid = true;
|
||||
current_profile_selected.status =
|
||||
ConfigurationTransactionStatus::kCommitted;
|
||||
current_profile_selected.identity = expected_identity;
|
||||
current_profile_selected.profile_index = 2;
|
||||
current_profile_selected.profile =
|
||||
controller_profile_default(expected_identity, 2);
|
||||
request = setup_request(
|
||||
Operation::kProfileRead, TUSB_DIR_IN, kMaximumResponseSize);
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload.size() ==
|
||||
kResponseHeaderSize +
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE &&
|
||||
control_payload[10] ==
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION &&
|
||||
control_payload[kResponseHeaderSize] ==
|
||||
static_cast<uint8_t>(
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION) &&
|
||||
control_payload[kResponseHeaderSize + 1] == 0 &&
|
||||
control_payload[kResponseHeaderSize + 2] == 0 &&
|
||||
control_payload[kResponseHeaderSize + 3] == 1,
|
||||
"selected profile response was not encoded");
|
||||
|
||||
current_profile_transaction = {};
|
||||
current_profile_transaction.metadata.state =
|
||||
ProfileServiceState::kReady;
|
||||
current_profile_transaction.metadata.generation = 9;
|
||||
current_profile_transaction.transaction.transaction_id = 0x01020304;
|
||||
current_profile_transaction.transaction.status =
|
||||
ConfigurationTransactionStatus::kPending;
|
||||
request = setup_request(
|
||||
Operation::kProfileTransactionStatus, TUSB_DIR_IN,
|
||||
kMaximumResponseSize);
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload.size() == kResponseHeaderSize + 20 &&
|
||||
control_payload[6] ==
|
||||
static_cast<uint8_t>(Status::kPending) &&
|
||||
read_u32(control_payload, kResponseHeaderSize) ==
|
||||
0x01020304,
|
||||
"pending profile transaction status lost its transaction ID");
|
||||
|
||||
current_profile_transaction.transaction.status =
|
||||
ConfigurationTransactionStatus::kCommitted;
|
||||
current_profile_transaction.transaction.stored_generation =
|
||||
0x11223344;
|
||||
current_profile_transaction.transaction.stored_crc = 0xaabbccdd;
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload[6] == static_cast<uint8_t>(Status::kOk) &&
|
||||
read_u32(control_payload, kResponseHeaderSize) ==
|
||||
0x01020304 &&
|
||||
read_u32(control_payload, kResponseHeaderSize + 12) ==
|
||||
0x11223344 &&
|
||||
read_u32(control_payload, kResponseHeaderSize + 16) ==
|
||||
0xaabbccdd,
|
||||
"final profile transaction status lost its commit result");
|
||||
|
||||
std::vector<uint8_t> identity_payload(15);
|
||||
require(controller_identity_encode(
|
||||
expected_identity, identity_payload.data(),
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE),
|
||||
"profile test identity did not encode");
|
||||
identity_payload[14] = 2;
|
||||
perform_out(Operation::kProfileSelect, identity_payload);
|
||||
require(controller_identity_equal(expected_identity,
|
||||
profile_identity) &&
|
||||
profile_index == 2,
|
||||
"profile selection was not dispatched");
|
||||
|
||||
std::vector<uint8_t> begin(28);
|
||||
write_u32(&begin, 0, 0x55667788);
|
||||
require(controller_identity_encode(
|
||||
expected_identity, &begin[4],
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE),
|
||||
"profile begin identity did not encode");
|
||||
begin[18] = 1;
|
||||
write_u16(&begin, 20, CONTROLLER_PROFILE_SCHEMA_VERSION);
|
||||
write_u16(&begin, 22, CONTROLLER_PROFILE_ENCODED_SIZE);
|
||||
write_u32(&begin, 24, 0xaabbccdd);
|
||||
perform_out(Operation::kProfileBegin, begin);
|
||||
require(begin_transaction_id == 0x55667788 &&
|
||||
profile_index == 1 &&
|
||||
profile_schema == CONTROLLER_PROFILE_SCHEMA_VERSION &&
|
||||
profile_size == CONTROLLER_PROFILE_ENCODED_SIZE &&
|
||||
profile_crc == 0xaabbccdd,
|
||||
"profile begin was not dispatched");
|
||||
|
||||
std::vector<uint8_t> chunk(48);
|
||||
write_u32(&chunk, 0, 0x55667788);
|
||||
write_u16(&chunk, 4, 0);
|
||||
write_u16(&chunk, 6, 40);
|
||||
perform_out(Operation::kProfileChunk, chunk);
|
||||
require(append_transaction_id == 0x55667788 &&
|
||||
append_offset == 0 && appended_bytes.size() == 40,
|
||||
"profile chunk was not dispatched");
|
||||
|
||||
std::vector<uint8_t> commit(4);
|
||||
write_u32(&commit, 0, 0x55667788);
|
||||
perform_out(Operation::kProfileCommit, commit);
|
||||
require(profile_commit_transaction_id == 0x55667788,
|
||||
"profile commit was not dispatched");
|
||||
|
||||
std::vector<uint8_t> mutation(19);
|
||||
write_u32(&mutation, 0, 0x10203040);
|
||||
require(controller_identity_encode(
|
||||
expected_identity, &mutation[4],
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE),
|
||||
"profile mutation identity did not encode");
|
||||
mutation[18] = CONTROLLER_PROFILE_ALL;
|
||||
perform_out(Operation::kProfileReset, mutation);
|
||||
require(profile_reset_requested &&
|
||||
profile_reset_transaction_id == 0x10203040,
|
||||
"profile reset transaction was not dispatched");
|
||||
write_u32(&mutation, 0, 0x50607080);
|
||||
mutation[18] = 3;
|
||||
perform_out(Operation::kProfileActivate, mutation);
|
||||
require(profile_activate_requested && profile_index == 3 &&
|
||||
profile_activate_transaction_id == 0x50607080,
|
||||
"profile activation transaction was not dispatched");
|
||||
|
||||
write_u32(&mutation, 0, 0);
|
||||
perform_out(Operation::kProfileActivate, mutation, false);
|
||||
request = setup_request(
|
||||
Operation::kProfileReset, TUSB_DIR_OUT, kRequestHeaderSize + 15);
|
||||
require(!tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request),
|
||||
"legacy profile reset payload was accepted");
|
||||
|
||||
begin[19] = 1;
|
||||
perform_out(Operation::kProfileBegin, begin, false);
|
||||
request = setup_request(
|
||||
Operation::kProfileSelect, TUSB_DIR_OUT,
|
||||
kRequestHeaderSize + 14);
|
||||
require(!tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request),
|
||||
"short profile selection request was accepted");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t configuration_crc32(const uint8_t* data, size_t size) {
|
||||
|
|
@ -448,75 +256,6 @@ ConfigurationTransactionStatus configuration_service_reset(uint32_t) {
|
|||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_select(
|
||||
const ControllerIdentity& identity, uint8_t selected_profile) {
|
||||
profile_identity = identity;
|
||||
profile_index = selected_profile;
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_begin(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t selected_profile, uint16_t schema_version,
|
||||
size_t payload_size, uint32_t payload_crc) {
|
||||
begin_transaction_id = transaction_id;
|
||||
profile_identity = identity;
|
||||
profile_index = selected_profile;
|
||||
profile_schema = schema_version;
|
||||
profile_size = payload_size;
|
||||
profile_crc = payload_crc;
|
||||
return ConfigurationTransactionStatus::kReceiving;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_append(
|
||||
uint32_t transaction_id, size_t offset, const uint8_t* data,
|
||||
size_t size) {
|
||||
append_transaction_id = transaction_id;
|
||||
append_offset = offset;
|
||||
appended_bytes.assign(data, data + size);
|
||||
return ConfigurationTransactionStatus::kReceiving;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_commit(
|
||||
uint32_t transaction_id) {
|
||||
profile_commit_transaction_id = transaction_id;
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_reset(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t selected_profile) {
|
||||
profile_reset_transaction_id = transaction_id;
|
||||
profile_identity = identity;
|
||||
profile_index = selected_profile;
|
||||
profile_reset_requested = true;
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_activate(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t selected_profile) {
|
||||
profile_activate_transaction_id = transaction_id;
|
||||
profile_identity = identity;
|
||||
profile_index = selected_profile;
|
||||
profile_activate_requested = true;
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
void profile_service_list_snapshot(ProfileServiceListSnapshot* output) {
|
||||
*output = current_profile_list;
|
||||
}
|
||||
|
||||
void profile_service_selected_snapshot(
|
||||
ProfileServiceSelectedSnapshot* output) {
|
||||
*output = current_profile_selected;
|
||||
}
|
||||
|
||||
void profile_service_transaction_snapshot(
|
||||
ProfileServiceTransactionSnapshot* output) {
|
||||
*output = current_profile_transaction;
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_request_pairing_snapshot() {
|
||||
refresh_requested = true;
|
||||
}
|
||||
|
|
@ -549,8 +288,6 @@ bool tud_control_status(uint8_t, const tusb_control_request_t*) {
|
|||
}
|
||||
|
||||
#include "../adapter_configuration.cpp"
|
||||
#include "../controller_identity.cpp"
|
||||
#include "../controller_profile.cpp"
|
||||
#include "../usb_configuration_management.cpp"
|
||||
|
||||
int main() {
|
||||
|
|
@ -560,6 +297,5 @@ int main() {
|
|||
test_envelope_encoding();
|
||||
test_pairing_encoding();
|
||||
test_vendor_requests();
|
||||
test_profile_vendor_requests();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,39 +61,17 @@ Status transaction_status(ConfigurationTransactionStatus status) {
|
|||
}
|
||||
return Status::kStorageError;
|
||||
}
|
||||
Status profile_service_status(const ProfileServiceMetadata& metadata) {
|
||||
if (metadata.state == ProfileServiceState::kLoading) {
|
||||
return Status::kPending;
|
||||
}
|
||||
if (metadata.state == ProfileServiceState::kStorageError) {
|
||||
return Status::kStorageError;
|
||||
}
|
||||
return Status::kOk;
|
||||
}
|
||||
|
||||
|
||||
bool valid_out_size(Operation operation, size_t size) {
|
||||
switch (operation) {
|
||||
case Operation::kConfigurationBegin:
|
||||
return size == kRequestHeaderSize + 12;
|
||||
case Operation::kProfileBegin:
|
||||
return size == kRequestHeaderSize + 28;
|
||||
case Operation::kConfigurationChunk:
|
||||
return size > kRequestHeaderSize + 8 &&
|
||||
size <= kMaximumRequestSize;
|
||||
case Operation::kProfileChunk:
|
||||
return size > kRequestHeaderSize + 8 &&
|
||||
size <= kMaximumRequestSize;
|
||||
case Operation::kConfigurationCommit:
|
||||
case Operation::kConfigurationReset:
|
||||
return size == kRequestHeaderSize + 4;
|
||||
case Operation::kProfileCommit:
|
||||
return size == kRequestHeaderSize + 4;
|
||||
case Operation::kProfileSelect:
|
||||
return size == kRequestHeaderSize + 15;
|
||||
case Operation::kProfileReset:
|
||||
case Operation::kProfileActivate:
|
||||
return size == kRequestHeaderSize + 19;
|
||||
case Operation::kPairingRefresh:
|
||||
case Operation::kPairingClear:
|
||||
return size == kRequestHeaderSize;
|
||||
|
|
@ -240,74 +218,6 @@ size_t encode_pairing_snapshot(const Bluepad32PairingSnapshot& snapshot,
|
|||
payload, offset, output, output_size);
|
||||
}
|
||||
|
||||
size_t encode_profile_list(const ProfileServiceListSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size) {
|
||||
if (snapshot.count > PROFILE_SERVICE_LIST_CAPACITY) {
|
||||
return 0;
|
||||
}
|
||||
uint8_t payload[kProfileListPayloadSize]{};
|
||||
payload[0] = snapshot.count;
|
||||
size_t offset = 1;
|
||||
for (uint8_t index = 0; index < snapshot.count; ++index) {
|
||||
if (!controller_identity_encode(snapshot.rows[index].identity,
|
||||
&payload[offset],
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE) ||
|
||||
snapshot.rows[index].active_profile >=
|
||||
CONTROLLER_PROFILE_COUNT) {
|
||||
return 0;
|
||||
}
|
||||
payload[offset + 14] = snapshot.rows[index].active_profile;
|
||||
offset += 16;
|
||||
}
|
||||
return encode_response(
|
||||
Operation::kProfileList, profile_service_status(snapshot.metadata),
|
||||
0, CONTROLLER_PROFILE_SCHEMA_VERSION, snapshot.metadata.generation,
|
||||
payload, offset, output, output_size);
|
||||
}
|
||||
|
||||
size_t encode_profile_read(const ProfileServiceSelectedSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size) {
|
||||
Status status = profile_service_status(snapshot.metadata);
|
||||
if (status == Status::kOk) {
|
||||
status = transaction_status(snapshot.status);
|
||||
}
|
||||
uint8_t payload[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
size_t payload_size = 0;
|
||||
if (snapshot.valid) {
|
||||
if (!controller_profile_encode(snapshot.profile, payload,
|
||||
sizeof(payload))) {
|
||||
return 0;
|
||||
}
|
||||
payload_size = sizeof(payload);
|
||||
}
|
||||
return encode_response(
|
||||
Operation::kProfileRead, status, 0,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, snapshot.metadata.generation,
|
||||
payload, payload_size, output, output_size);
|
||||
}
|
||||
|
||||
size_t encode_profile_transaction(
|
||||
const ProfileServiceTransactionSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size) {
|
||||
const ConfigurationTransactionSnapshot& transaction =
|
||||
snapshot.transaction;
|
||||
uint8_t payload[20]{};
|
||||
write_u32(&payload[0], transaction.transaction_id);
|
||||
write_u16(&payload[4], transaction.received_size);
|
||||
write_u16(&payload[6], transaction.expected_size);
|
||||
write_u32(&payload[8], transaction.expected_crc);
|
||||
write_u32(&payload[12], transaction.stored_generation);
|
||||
write_u32(&payload[16], transaction.stored_crc);
|
||||
Status status = profile_service_status(snapshot.metadata);
|
||||
if (status == Status::kOk) {
|
||||
status = transaction_status(transaction.status);
|
||||
}
|
||||
return encode_response(
|
||||
Operation::kProfileTransactionStatus, status, 0,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, snapshot.metadata.generation,
|
||||
payload, sizeof(payload), output, output_size);
|
||||
}
|
||||
|
||||
} // namespace UsbConfigurationManagement
|
||||
|
||||
namespace {
|
||||
|
|
@ -376,96 +286,6 @@ bool process_out_request() {
|
|||
(static_cast<uint32_t>(payload[2]) << 16) |
|
||||
(static_cast<uint32_t>(payload[3]) << 24));
|
||||
return true;
|
||||
case Operation::kProfileSelect: {
|
||||
ControllerIdentity identity{};
|
||||
if (payload[14] >= CONTROLLER_PROFILE_COUNT ||
|
||||
!controller_identity_decode(
|
||||
payload, CONTROLLER_IDENTITY_ENCODED_SIZE, &identity)) {
|
||||
return false;
|
||||
}
|
||||
const ConfigurationTransactionStatus status =
|
||||
profile_service_select(identity, payload[14]);
|
||||
return status == ConfigurationTransactionStatus::kCommitted ||
|
||||
status == ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
case Operation::kProfileBegin: {
|
||||
ControllerIdentity identity{};
|
||||
if (payload[19] != 0 ||
|
||||
!controller_identity_decode(
|
||||
&payload[4], CONTROLLER_IDENTITY_ENCODED_SIZE,
|
||||
&identity)) {
|
||||
return false;
|
||||
}
|
||||
profile_service_begin(
|
||||
static_cast<uint32_t>(payload[0]) |
|
||||
(static_cast<uint32_t>(payload[1]) << 8) |
|
||||
(static_cast<uint32_t>(payload[2]) << 16) |
|
||||
(static_cast<uint32_t>(payload[3]) << 24),
|
||||
identity, payload[18],
|
||||
static_cast<uint16_t>(payload[20] |
|
||||
(payload[21] << 8)),
|
||||
static_cast<uint16_t>(payload[22] |
|
||||
(payload[23] << 8)),
|
||||
static_cast<uint32_t>(payload[24]) |
|
||||
(static_cast<uint32_t>(payload[25]) << 8) |
|
||||
(static_cast<uint32_t>(payload[26]) << 16) |
|
||||
(static_cast<uint32_t>(payload[27]) << 24));
|
||||
return true;
|
||||
}
|
||||
case Operation::kProfileChunk: {
|
||||
const uint16_t chunk_size =
|
||||
static_cast<uint16_t>(payload[6] |
|
||||
(payload[7] << 8));
|
||||
if (request.payload_size != 8 + chunk_size ||
|
||||
chunk_size > kMaximumChunkSize) {
|
||||
return false;
|
||||
}
|
||||
profile_service_append(
|
||||
static_cast<uint32_t>(payload[0]) |
|
||||
(static_cast<uint32_t>(payload[1]) << 8) |
|
||||
(static_cast<uint32_t>(payload[2]) << 16) |
|
||||
(static_cast<uint32_t>(payload[3]) << 24),
|
||||
static_cast<uint16_t>(payload[4] |
|
||||
(payload[5] << 8)),
|
||||
&payload[8], chunk_size);
|
||||
return true;
|
||||
}
|
||||
case Operation::kProfileCommit:
|
||||
profile_service_commit(
|
||||
static_cast<uint32_t>(payload[0]) |
|
||||
(static_cast<uint32_t>(payload[1]) << 8) |
|
||||
(static_cast<uint32_t>(payload[2]) << 16) |
|
||||
(static_cast<uint32_t>(payload[3]) << 24));
|
||||
return true;
|
||||
case Operation::kProfileReset:
|
||||
case Operation::kProfileActivate: {
|
||||
const uint32_t transaction_id =
|
||||
static_cast<uint32_t>(payload[0]) |
|
||||
(static_cast<uint32_t>(payload[1]) << 8) |
|
||||
(static_cast<uint32_t>(payload[2]) << 16) |
|
||||
(static_cast<uint32_t>(payload[3]) << 24);
|
||||
ControllerIdentity identity{};
|
||||
if (transaction_id == 0 ||
|
||||
(request.operation == Operation::kProfileActivate &&
|
||||
payload[18] >= CONTROLLER_PROFILE_COUNT) ||
|
||||
(request.operation == Operation::kProfileReset &&
|
||||
payload[18] != CONTROLLER_PROFILE_ALL &&
|
||||
payload[18] >= CONTROLLER_PROFILE_COUNT) ||
|
||||
!controller_identity_decode(
|
||||
&payload[4], CONTROLLER_IDENTITY_ENCODED_SIZE,
|
||||
&identity)) {
|
||||
return false;
|
||||
}
|
||||
const ConfigurationTransactionStatus status =
|
||||
request.operation == Operation::kProfileReset
|
||||
? profile_service_reset(
|
||||
transaction_id, identity, payload[18])
|
||||
: profile_service_activate(
|
||||
transaction_id, identity, payload[18]);
|
||||
return status == ConfigurationTransactionStatus::kPending ||
|
||||
status == ConfigurationTransactionStatus::kUnchanged ||
|
||||
status == ConfigurationTransactionStatus::kCommitted;
|
||||
}
|
||||
case Operation::kPairingRefresh:
|
||||
bluepad32_input_backend_request_pairing_snapshot();
|
||||
return true;
|
||||
|
|
@ -547,27 +367,6 @@ extern "C" bool tud_vendor_control_xfer_cb(
|
|||
snapshot, response, sizeof(response));
|
||||
break;
|
||||
}
|
||||
case Operation::kProfileList: {
|
||||
ProfileServiceListSnapshot snapshot{};
|
||||
profile_service_list_snapshot(&snapshot);
|
||||
response_size = encode_profile_list(
|
||||
snapshot, response, sizeof(response));
|
||||
break;
|
||||
}
|
||||
case Operation::kProfileRead: {
|
||||
ProfileServiceSelectedSnapshot snapshot{};
|
||||
profile_service_selected_snapshot(&snapshot);
|
||||
response_size = encode_profile_read(
|
||||
snapshot, response, sizeof(response));
|
||||
break;
|
||||
}
|
||||
case Operation::kProfileTransactionStatus: {
|
||||
ProfileServiceTransactionSnapshot snapshot{};
|
||||
profile_service_transaction_snapshot(&snapshot);
|
||||
response_size = encode_profile_transaction(
|
||||
snapshot, response, sizeof(response));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
#include "bluepad32_input_backend.h"
|
||||
#include "configuration_service.h"
|
||||
#include "profile_service.h"
|
||||
|
||||
namespace UsbConfigurationManagement {
|
||||
|
||||
|
|
@ -17,14 +16,11 @@ constexpr size_t kResponseHeaderSize = 20;
|
|||
constexpr size_t kPairingRecordSize = 8;
|
||||
constexpr size_t kPairingPayloadHeaderSize = 4;
|
||||
constexpr size_t kMaximumRequestSize = 64;
|
||||
constexpr size_t kProfileListPayloadSize =
|
||||
1 + PROFILE_SERVICE_LIST_CAPACITY * 16;
|
||||
constexpr size_t kMaximumResponseSize =
|
||||
kResponseHeaderSize + kProfileListPayloadSize;
|
||||
kResponseHeaderSize + kPairingPayloadHeaderSize +
|
||||
BLUEPAD32_PAIRING_RECORD_CAPACITY * kPairingRecordSize;
|
||||
constexpr size_t kMaximumChunkSize =
|
||||
kMaximumRequestSize - kRequestHeaderSize - 8;
|
||||
static_assert(kMaximumResponseSize == 293,
|
||||
"profile list no longer fits the EP0 response buffer");
|
||||
|
||||
enum class Operation : uint8_t {
|
||||
kInfo = 0x01,
|
||||
|
|
@ -37,15 +33,6 @@ enum class Operation : uint8_t {
|
|||
kPairingRead = 0x20,
|
||||
kPairingRefresh = 0x21,
|
||||
kPairingClear = 0x22,
|
||||
kProfileList = 0x30,
|
||||
kProfileSelect = 0x31,
|
||||
kProfileRead = 0x32,
|
||||
kProfileBegin = 0x33,
|
||||
kProfileChunk = 0x34,
|
||||
kProfileCommit = 0x35,
|
||||
kProfileReset = 0x36,
|
||||
kProfileActivate = 0x37,
|
||||
kProfileTransactionStatus = 0x38,
|
||||
};
|
||||
|
||||
enum class Status : uint8_t {
|
||||
|
|
@ -74,12 +61,5 @@ size_t encode_response(Operation operation, Status status, uint8_t flags,
|
|||
uint8_t* output, size_t output_size);
|
||||
size_t encode_pairing_snapshot(const Bluepad32PairingSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size);
|
||||
size_t encode_profile_list(const ProfileServiceListSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size);
|
||||
size_t encode_profile_read(const ProfileServiceSelectedSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size);
|
||||
size_t encode_profile_transaction(
|
||||
const ProfileServiceTransactionSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size);
|
||||
|
||||
} // namespace UsbConfigurationManagement
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue