Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ on:
workflow_dispatch:

jobs:
lint:
name: Lint (ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install "ruff>=0.15,<1"
- run: ruff check .

test:
name: Headless tests (Python ${{ matrix.python-version }})
runs-on: macos-14
Expand Down
24 changes: 10 additions & 14 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
# Build & distribution
dist/
# Build output (py2app / build_dmg.sh)
build/
dist/
*.spec
build_py2app.log

# On ignore tous les fichiers de cache Python
**/__pycache__/
*.pyc

# Fichiers système macOS
.DS_Store
.AppleDouble
.LSOverride

# Fichiers Python (si tu l'utilises)
# Python
__pycache__/
*.py[cod]
*$py.class
venv/
.venv/
env/
.ruff_cache/

# macOS
.DS_Store
.AppleDouble
.LSOverride

# IDE (Visual Studio Code, etc.)
# Editors
.vscode/
.idea/
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,32 @@ switch2bridge-macos/
├── build_dmg.sh # Automated build script (.app + DMG)
├── requirements.txt # Runtime dependencies
├── requirements-build.txt # + py2app, for the .app / DMG
├── ruff.toml # Lint config
├── tests/
│ ├── test_bridge.py # Headless tests (mappings, key dispatch, BLE lifecycle, calibration)
│ ├── test_bridge.py # Headless tests (mappings, key dispatch, BLE lifecycle, calibration, CLI)
│ └── test_dsu.py # DSU server over real UDP
├── .github/
│ ├── workflows/ci.yml # Lint + tests on every PR, DMG build on main
│ ├── workflows/release.yml # Manual release: build, tag, publish
│ └── release-notes/ # One vX.Y.Z.md per release
├── AppIcon.icns # Application icon (used by py2app)
├── LICENSE
└── README.md
```

## 🛠️ Development

```bash
pip install -r requirements.txt
python tests/test_bridge.py # headless: BLE and keyboard are mocked
python tests/test_dsu.py # real UDP round-trips on localhost
pip install ruff && ruff check .
```

CI runs the same on every pull request (macOS, Python 3.10 / 3.12 / 3.13).

**Releasing** — bump `APP_VERSION` in `Switch2Bridge.py` (the single source of truth), add `.github/release-notes/vX.Y.Z.md` (`{{SHA256}}` is replaced by the DMG checksum), merge, then run **Actions → Release → Run workflow** with the tag. It refuses to publish if the tag doesn't match `APP_VERSION` or the release already exists.

## 🔬 Technical Details

### How It Works
Expand Down Expand Up @@ -271,15 +289,15 @@ Every connection also logs the full GATT table (`GATT: N characteristic(s): …`
| LED Control | 🧪 Player 1 only | Set once on connect through the command channel |
| Rumble | ❌ Not implemented | Goes through the command/vibration channels, not `…c0f8` |
| Motion/Gyro | ⚠️ Plumbing ready | DSU motion fields are sent (as zeros) — the gyro bytes in the BLE report are not decoded yet |
| Native HID | ❌ Not possible | Would require DriverKit (kernel-level) |
| Native HID | ❌ Not implemented | Would need a DriverKit system extension (virtual gamepad), which requires a paid Apple Developer account to sign |

## 🤝 Contributing

Contributions welcome! Areas that need work:

1. **Rumble** — the command channel is now used for calibration and LEDs; rumble is the next step (see #14)
2. **Motion controls** — decode gyro/accelerometer data
3. **True analog** — virtual HID device via DriverKit
3. **Native gamepad** — virtual HID device via DriverKit, so games see a real controller (analog already works through DSU)
4. **Cross-platform** — Linux/Windows ports

## 📜 Credits
Expand Down
4 changes: 2 additions & 2 deletions Switch2Bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ def _apply(self, cfg):
try:
threshold = float(sticks.get("threshold", 0.5))
except (TypeError, ValueError):
raise ValueError('"sticks.threshold" must be a number')
raise ValueError('"sticks.threshold" must be a number') from None
clamped = min(max(threshold, self.THRESHOLD_MIN), self.THRESHOLD_MAX)
if clamped != threshold:
warnings.append(f"Stick threshold {threshold} out of range, using {clamped}")
Expand Down Expand Up @@ -474,7 +474,7 @@ def _apply(self, cfg):
try:
port = int(dsu.get("port", 26760))
except (TypeError, ValueError):
raise ValueError('"dsu.port" must be an integer')
raise ValueError('"dsu.port" must be an integer') from None
if not (1024 <= port <= 65535):
warnings.append(f"DSU port {port} out of range, using 26760")
port = 26760
Expand Down
2 changes: 1 addition & 1 deletion dsu_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def _serve(self):
while not self._stop.is_set():
try:
data, addr = self._sock.recvfrom(1024)
except socket.timeout:
except TimeoutError: # the 1 s recv timeout
continue
except OSError:
break # socket closed
Expand Down
5 changes: 5 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
target-version = "py310"

[lint]
# pyflakes, syntax errors, bugbear, pyupgrade
select = ["F", "E9", "B", "UP"]
2 changes: 1 addition & 1 deletion tests/test_dsu.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def crc_ok(pkt):
cli.settimeout(0.5)
cli.recvfrom(1024)
check("stale client pruned", False, "still receiving")
except socket.timeout:
except TimeoutError:
check("stale client pruned", True)
check("client_count zero", srv.client_count() == 0)

Expand Down
Loading