libmdr-bt 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mos9527, Amr Satrio and other contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: libmdr-bt
3
+ Version: 0.1.0
4
+ Summary: Python binding for libmdr-bt (Sony headphones platform connection backend)
5
+ License-Expression: MIT
6
+ Project-URL: Source, https://github.com/mos9527/libmdr-py
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/plain
9
+ License-File: LICENSE
10
+ Requires-Dist: libmdr
11
+ Dynamic: license-file
12
+
13
+ ctypes bindings for libmdr-bt (mdr-bt-shared).
@@ -0,0 +1 @@
1
+ # libmdr-bt
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "libmdr-bt"
7
+ version = "0.1.0"
8
+ description = "Python binding for libmdr-bt (Sony headphones platform connection backend)"
9
+ readme = { text = "ctypes bindings for libmdr-bt (mdr-bt-shared).", content-type = "text/plain" }
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ dependencies = ["libmdr"]
14
+
15
+ [project.urls]
16
+ Source = "https://github.com/mos9527/libmdr-py"
17
+
18
+ [tool.setuptools.packages.find]
19
+ where = ["src"]
20
+
21
+ [tool.setuptools.package-data]
22
+ libmdr_bt = ["_native/*"]
23
+
24
+ # See libmdr/pyproject.toml: wheels are py3-none-<platform>, built once per platform.
25
+ [tool.cibuildwheel]
26
+ skip = ["*-musllinux_*", "cp31[1-9]*-manylinux_armv7l"] # armv7l runs under QEMU: test once
27
+ build-verbosity = 1
28
+ test-command = "python -c \"import libmdr_bt, libmdr_bt._dll as d; print(d.load())\""
29
+ manylinux-armv7l-image = "manylinux_2_35"
30
+
31
+ [tool.cibuildwheel.linux]
32
+ # The BlueZ backend talks to the system bluetoothd over D-Bus.
33
+ before-all = "sh {project}/tooling/ci/linux-before-all.sh bt"
34
+ # libdbus-1 / libbluetooth must match the host's dbus-daemon / bluetoothd (and
35
+ # libbluetooth is GPL), so they are used from the system instead of vendored.
36
+ repair-wheel-command = "auditwheel repair --exclude libdbus-1.so.3 --exclude libbluetooth.so.3 -w {dest_dir} {wheel}"
37
+
38
+ [tool.cibuildwheel.macos]
39
+ environment = { MACOSX_DEPLOYMENT_TARGET = "12.0" }
40
+
41
+ [tool.cibuildwheel.windows]
42
+ # No Python extension modules; mdr-bt-shared.dll links the CRT statically.
43
+ repair-wheel-command = ""
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,276 @@
1
+ """Build script for libmdr-bt (also the setuptools entry point).
2
+
3
+ The CMake build helpers that previously lived in a separate ``cmake_build.py``
4
+ are inlined here. Like that module, this is build-time code and is NOT packaged
5
+ into the wheel; it runs only during ``build_py`` / ``editable_wheel`` to compile
6
+ and stage the native shared library into the package's ``_native`` directory.
7
+
8
+ C++ source resolution order:
9
+ 1. ``MDR_SOURCE_DIR`` env var (explicit override)
10
+ 2. ``<repo>/cpp`` (git submodule, preferred)
11
+ 3. ``<repo>/build/cpp-src`` (shallow fetch of ``_SOURCE_COMMIT`` on every build)
12
+
13
+ The third option lets ``pip install .`` / ``python -m build`` work from a
14
+ plain source checkout or sdist without the caller having to run
15
+ ``git submodule update --init`` first.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import configparser
21
+ import os
22
+ import re
23
+ import shutil
24
+ import stat
25
+ import subprocess
26
+ import sys
27
+ import time
28
+ from pathlib import Path
29
+
30
+ from setuptools import Distribution, setup
31
+ from setuptools.command.bdist_wheel import bdist_wheel as _bdist_wheel
32
+ from setuptools.command.build_py import build_py as _build_py
33
+ from setuptools.command.editable_wheel import editable_wheel as _editable_wheel
34
+
35
+ # Python repo root (this file lives in <repo>/<pkg>/setup.py).
36
+ _PY_ROOT = Path(__file__).resolve().parents[1]
37
+ DEFAULT_BUILD_DIR = _PY_ROOT / "build" / "python"
38
+ # Fresh C++ source clone location (separate from the CMake build dir so the
39
+ # build dir can be wiped between builds without discarding the clone).
40
+ _CLONE_DIR = _PY_ROOT / "build" / "cpp-src"
41
+ # Preferred on-disk location of the C++ sources.
42
+ _SUBMODULE_DIR = _PY_ROOT / "cpp"
43
+ # Upstream coordinates, used only when fetching on demand. The URL is
44
+ # overridden by .gitmodules when present. Upstream has no releases yet, so the
45
+ # sources are pinned to a commit matching the generated bindings.
46
+ _SOURCE_URL = "https://github.com/mos9527/SonyHeadphonesClient.git"
47
+ _SOURCE_COMMIT = "2edf09098db27642c89e533bf86c0a6b162f6ad4"
48
+
49
+ # CMake target -> on-disk base name (OUTPUT_NAME set in the C++ CMakeLists).
50
+ _TARGET_BASENAMES = {
51
+ "mdr_shared": "mdr-shared",
52
+ "mdr-bt_shared": "mdr-bt-shared",
53
+ }
54
+ _SUFFIXES = (".dll", ".so", ".dylib")
55
+
56
+
57
+ def _cmake() -> str:
58
+ env = os.environ.get("CMAKE")
59
+ if env:
60
+ return env
61
+ found = shutil.which("cmake")
62
+ if found:
63
+ return found
64
+ raise RuntimeError("cmake not found on PATH; install CMake or set CMAKE.")
65
+
66
+
67
+ def _run(cmd: list[str]) -> None:
68
+ print("+", " ".join(cmd), flush=True)
69
+ subprocess.check_call(cmd)
70
+
71
+
72
+ def _read_gitmodules() -> str | None:
73
+ """Return the url of the 'cpp' submodule from .gitmodules, if present."""
74
+ gm = _PY_ROOT / ".gitmodules"
75
+ if not gm.is_file():
76
+ return None
77
+ cp = configparser.ConfigParser()
78
+ cp.read(gm, encoding="utf-8")
79
+ for section in cp.sections():
80
+ if cp.has_option(section, "path") and cp.get(section, "path") == "cpp":
81
+ return cp.get(section, "url", fallback=_SOURCE_URL)
82
+ return None
83
+
84
+
85
+ def _rmtree(path: Path) -> None:
86
+ """Remove a directory tree, tolerating read-only files (git marks .git
87
+ objects read-only on Windows) and transient file locks."""
88
+ if not path.exists():
89
+ return
90
+
91
+ def _clear_ro(func, p, exc): # noqa: ANN001
92
+ try:
93
+ os.chmod(p, stat.S_IWRITE)
94
+ func(p)
95
+ except OSError:
96
+ raise
97
+
98
+ last_err: OSError | None = None
99
+ for _ in range(3):
100
+ try:
101
+ if sys.version_info >= (3, 12):
102
+ shutil.rmtree(path, onexc=_clear_ro)
103
+ else:
104
+ shutil.rmtree(path, onerror=_clear_ro)
105
+ return
106
+ except OSError as exc:
107
+ last_err = exc
108
+ time.sleep(0.5)
109
+ if last_err:
110
+ raise last_err
111
+
112
+
113
+ def _clone_source(dest: Path) -> None:
114
+ url = _read_gitmodules() or _SOURCE_URL
115
+ _rmtree(dest)
116
+ dest.mkdir(parents=True, exist_ok=True)
117
+ _run(["git", "-C", str(dest), "init", "-q"])
118
+ _run(["git", "-C", str(dest), "fetch", "--depth", "1", url, _SOURCE_COMMIT])
119
+ _run(["git", "-C", str(dest), "checkout", "-q", "FETCH_HEAD"])
120
+
121
+
122
+ def _ensure_source() -> Path:
123
+ """Resolve the C++ source directory, cloning it into the build dir if needed."""
124
+ override = os.environ.get("MDR_SOURCE_DIR")
125
+ if override:
126
+ return Path(override)
127
+ if _SUBMODULE_DIR.is_dir() and any(_SUBMODULE_DIR.iterdir()):
128
+ return _SUBMODULE_DIR
129
+ clone_dir = _CLONE_DIR
130
+ print(f"cpp source not found at {_SUBMODULE_DIR}; fetching {_SOURCE_COMMIT} into {clone_dir}", flush=True)
131
+ _clone_source(clone_dir)
132
+ return clone_dir
133
+
134
+
135
+ def _config() -> str:
136
+ return os.environ.get("MDR_CMAKE_CONFIG", "MinSizeRel")
137
+
138
+
139
+ def _platform_args() -> list[str]:
140
+ args: list[str] = []
141
+ if sys.platform == "darwin":
142
+ # PEP 517 frontends / cibuildwheel request the target architecture(s)
143
+ # via ARCHFLAGS (e.g. "-arch x86_64"), which CMake does not read.
144
+ archs = re.findall(r"-arch\s+(\S+)", os.environ.get("ARCHFLAGS", ""))
145
+ if archs:
146
+ args.append("-DCMAKE_OSX_ARCHITECTURES=" + ";".join(archs))
147
+ return args
148
+
149
+
150
+ def _configure(build_dir: Path | None = None, *, config: str | None = None, extra_args: list[str] | None = None) -> Path:
151
+ build_dir = Path(build_dir or os.environ.get("MDR_PYTHON_BUILD_DIR", DEFAULT_BUILD_DIR))
152
+ # Always start from a clean build dir so a stale CMake cache or an
153
+ # interrupted dependency fetch (e.g. fmt via FetchContent) cannot poison
154
+ # the next configure. The C++ source is re-cloned separately each time.
155
+ _rmtree(build_dir)
156
+ build_dir.mkdir(parents=True, exist_ok=True)
157
+ cmd = [
158
+ _cmake(), "-S", str(_ensure_source()), "-B", str(build_dir),
159
+ # Single-config generators (Makefiles/Ninja) ignore `--build --config`.
160
+ f"-DCMAKE_BUILD_TYPE={config or _config()}",
161
+ "-DMDR_BUILD_TESTING=OFF",
162
+ "-DMDR_BUILD_CLIENT=OFF",
163
+ "-DMDR_BUILD_CODEGEN=OFF",
164
+ "-DMDR_CLIENT_DEBUGGER=OFF",
165
+ "-DMDR_ENABLE_LOG=OFF",
166
+ ]
167
+ cmd.extend(_platform_args())
168
+ if extra_args:
169
+ cmd.extend(extra_args)
170
+ gen = os.environ.get("MDR_CMAKE_GENERATOR")
171
+ if gen:
172
+ cmd[1:1] = ["-G", gen]
173
+ _run(cmd)
174
+ return build_dir
175
+
176
+
177
+ def _build(targets: list[str], build_dir: Path | None = None, *, config: str | None = None, extra_args: list[str] | None = None) -> Path:
178
+ config = config or _config()
179
+ build_dir = _configure(build_dir, config=config, extra_args=extra_args)
180
+ cmd = [_cmake(), "--build", str(build_dir), "--config", config, "--parallel"]
181
+ for target in targets:
182
+ cmd.extend(["--target", target])
183
+ _run(cmd)
184
+ return build_dir
185
+
186
+
187
+ def _find_built(destination: Path, target: str) -> Path:
188
+ base = _TARGET_BASENAMES[target]
189
+ names = {f"{base}{suf}" for suf in _SUFFIXES}
190
+ names |= {f"lib{base}{suf}" for suf in (".so", ".dylib")}
191
+ for path in destination.rglob("*"):
192
+ if path.is_file() and path.name in names:
193
+ return path.resolve()
194
+ raise FileNotFoundError(
195
+ f"Built library for {target} not found under {destination}. "
196
+ f"Looked for: {', '.join(sorted(names))}"
197
+ )
198
+
199
+
200
+ def stage_libraries(targets: list[str], destination: Path) -> list[Path]:
201
+ """Build the shared targets and emit them (flattened) into `destination`."""
202
+ destination = Path(destination).resolve()
203
+ destination.mkdir(parents=True, exist_ok=True)
204
+ # Build into the default build directory (e.g. build/python/) instead of
205
+ # redirecting CMake output directly into _native/. That avoids CMake's
206
+ # per-config subdir (MinSizeRel/ on MSVC) being discovered as a namespace
207
+ # package by setuptools.
208
+ build_dir = _build(targets)
209
+ staged: list[Path] = []
210
+ for target in targets:
211
+ found = _find_built(build_dir, target)
212
+ final = destination / found.name
213
+ if found.resolve() != final.resolve():
214
+ shutil.copy2(found, final)
215
+ staged.append(final)
216
+ return staged
217
+
218
+
219
+ def stage_library(target: str, destination: Path) -> Path:
220
+ """Build one shared target and emit it (flattened) into `destination`."""
221
+ return stage_libraries([target], destination)[0]
222
+
223
+
224
+ def _stage_native() -> None:
225
+ destination = Path(__file__).resolve().parent / "src" / "libmdr_bt" / "_native"
226
+ # libmdr-bt links the core statically (PRIVATE mdr), so mdr-bt-shared is
227
+ # self-contained; the core mdr-shared is provided by the libmdr dependency.
228
+ stage_libraries(["mdr-bt_shared"], destination)
229
+
230
+
231
+ class build_py(_build_py):
232
+ def run(self) -> None:
233
+ _stage_native()
234
+ super().run()
235
+
236
+
237
+ class BinaryDistribution(Distribution):
238
+ """Mark the distribution as platform specific.
239
+
240
+ There are no ext_modules (the native library is package data), so without
241
+ this the wheel is pure and installs into purelib, which auditwheel rejects.
242
+ """
243
+
244
+ def has_ext_modules(self) -> bool:
245
+ return True
246
+
247
+
248
+ class bdist_wheel(_bdist_wheel):
249
+ """Tag wheels ``py3-none-<platform>``.
250
+
251
+ The native library is loaded through ctypes and has no CPython ABI
252
+ dependency, so a single wheel per platform serves every Python 3
253
+ interpreter (CPython of any version, free-threaded builds, PyPy).
254
+ """
255
+
256
+ def get_tag(self) -> tuple[str, str, str]:
257
+ _, _, plat = super().get_tag()
258
+ return "py3", "none", plat
259
+
260
+
261
+ class editable_wheel(_editable_wheel):
262
+ def run(self) -> None:
263
+ # build_py.run() (called internally by super()) already handles
264
+ # the native build via _stage_native(). Calling it here too
265
+ # would trigger a wasteful second configure+build cycle.
266
+ super().run()
267
+
268
+
269
+ setup(
270
+ distclass=BinaryDistribution,
271
+ cmdclass={
272
+ "bdist_wheel": bdist_wheel,
273
+ "build_py": build_py,
274
+ "editable_wheel": editable_wheel,
275
+ }
276
+ )
@@ -0,0 +1,8 @@
1
+ """Python bindings for mdr-bt-shared (libmdr-bt)."""
2
+
3
+ from .connection import PlatformConnection, create_connection
4
+
5
+ __all__ = [
6
+ "PlatformConnection",
7
+ "create_connection",
8
+ ]
@@ -0,0 +1,121 @@
1
+ """ctypes loader for mdr-bt-shared (libmdr-bt)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from ctypes import CDLL
8
+ from pathlib import Path
9
+
10
+ import libmdr._dll as libmdr_dll
11
+
12
+ _LIB: CDLL | None = None
13
+
14
+ _ENV_OVERRIDE = "MDR_BT_DLL"
15
+
16
+ _CANDIDATE_NAMES = (
17
+ "mdr-bt-shared.dll",
18
+ "libmdr-bt-shared.so",
19
+ "libmdr-bt-shared.dylib",
20
+ "mdr-bt-shared.so",
21
+ "mdr-bt-shared.dylib",
22
+ )
23
+
24
+ _CORE_NAMES = (
25
+ "mdr-shared.dll",
26
+ "mdr.dll",
27
+ "libmdr-shared.so",
28
+ "libmdr-shared.dylib",
29
+ "mdr-shared.so",
30
+ "mdr-shared.dylib",
31
+ )
32
+
33
+
34
+ def _package_native_dir() -> Path:
35
+ return Path(__file__).resolve().parent / "_native"
36
+
37
+
38
+ def _candidate_paths() -> list[Path]:
39
+ paths: list[Path] = []
40
+ override = os.environ.get(_ENV_OVERRIDE)
41
+ if override:
42
+ paths.append(Path(override))
43
+
44
+ native = _package_native_dir()
45
+ for name in _CANDIDATE_NAMES:
46
+ paths.append(native / name)
47
+
48
+ repo_build = Path(__file__).resolve().parents[4] / "build" / "python"
49
+ if repo_build.is_dir():
50
+ for name in _CANDIDATE_NAMES:
51
+ paths.extend(repo_build.rglob(name))
52
+
53
+ return paths
54
+
55
+
56
+ def _ensure_core_loaded(platform_dir: Path) -> None:
57
+ if libmdr_dll._LIB is not None: # noqa: SLF001
58
+ return
59
+
60
+ for name in _CORE_NAMES:
61
+ sibling = platform_dir / name
62
+ if sibling.is_file():
63
+ libmdr_dll._LIB = libmdr_dll._load_path(sibling) # noqa: SLF001
64
+ return
65
+
66
+ libmdr_dll.load()
67
+
68
+
69
+ def _load_path(path: Path) -> CDLL:
70
+ path = path.resolve()
71
+ if not path.is_file():
72
+ raise FileNotFoundError(path)
73
+
74
+ _ensure_core_loaded(path.parent)
75
+
76
+ if sys.platform == "win32":
77
+ os.add_dll_directory(str(path.parent))
78
+ core_dir = libmdr_dll._package_native_dir() # noqa: SLF001
79
+ if core_dir.is_dir():
80
+ os.add_dll_directory(str(core_dir))
81
+ return CDLL(str(path))
82
+
83
+ mode = os.RTLD_GLOBAL if hasattr(os, "RTLD_GLOBAL") else 0
84
+ return CDLL(str(path), mode=mode)
85
+
86
+
87
+ def load() -> CDLL:
88
+ global _LIB
89
+ if _LIB is not None:
90
+ return _LIB
91
+
92
+ errors: list[str] = []
93
+ seen: set[Path] = set()
94
+ for candidate in _candidate_paths():
95
+ try:
96
+ resolved = candidate.resolve()
97
+ except OSError as exc:
98
+ errors.append(f"{candidate}: {exc}")
99
+ continue
100
+ if resolved in seen:
101
+ continue
102
+ seen.add(resolved)
103
+ try:
104
+ _LIB = _load_path(resolved)
105
+ return _LIB
106
+ except OSError as exc:
107
+ errors.append(f"{resolved}: {exc}")
108
+
109
+ hint = (
110
+ f"Set {_ENV_OVERRIDE} to the full path of mdr-bt-shared.dll, "
111
+ f"or `pip install -e libmdr-bt` to build it."
112
+ )
113
+ raise FileNotFoundError(
114
+ "Unable to load mdr-bt-shared.\n"
115
+ + "\n".join(errors)
116
+ + ("\n" + hint if errors else hint)
117
+ )
118
+
119
+
120
+ def lib() -> CDLL:
121
+ return load()
@@ -0,0 +1,138 @@
1
+ """Platform connection factories wrapping mdr-bt-shared (libmdr-bt)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from ctypes import c_void_p
7
+ from typing import Literal
8
+
9
+ from libmdr.connection import Connection
10
+
11
+ from . import _dll
12
+
13
+ Backend = Literal["auto", "classic", "ble"]
14
+
15
+
16
+ def _bind_factory(create_name: str, get_name: str, destroy_name: str):
17
+ lib = _dll.lib()
18
+ create = getattr(lib, create_name)
19
+ getter = getattr(lib, get_name)
20
+ destroy = getattr(lib, destroy_name)
21
+
22
+ create.restype = c_void_p
23
+ create.argtypes = []
24
+ getter.restype = c_void_p
25
+ getter.argtypes = [c_void_p]
26
+ destroy.restype = None
27
+ destroy.argtypes = [c_void_p]
28
+ return create, getter, destroy
29
+
30
+
31
+ class PlatformConnection:
32
+ """Owns a platform-specific connection object and exposes the MDRConnection vtable."""
33
+
34
+ def __init__(self, backend: Backend = "auto") -> None:
35
+ self._backend = "classic" if backend == "auto" else backend
36
+ self._owner: c_void_p | None = None
37
+ self._connection: Connection | None = None
38
+ self._destroy = None
39
+ self._create()
40
+
41
+ def _factories(self):
42
+ platform = sys.platform
43
+ if platform == "win32":
44
+ if self._backend == "ble":
45
+ return _bind_factory(
46
+ "mdrConnectionWindowsBLECreate",
47
+ "mdrConnectionWindowsBLEGet",
48
+ "mdrConnectionWindowsBLEDestroy",
49
+ )
50
+ return _bind_factory(
51
+ "mdrConnectionWindowsCreate",
52
+ "mdrConnectionWindowsGet",
53
+ "mdrConnectionWindowsDestroy",
54
+ )
55
+ if platform == "darwin":
56
+ if self._backend == "ble":
57
+ raise RuntimeError("BLE backend is not available on macOS in mdr-bt-shared")
58
+ return _bind_factory(
59
+ "mdrConnectionMacOSCreate",
60
+ "mdrConnectionMacOSGet",
61
+ "mdrConnectionMacOSDestroy",
62
+ )
63
+ if platform.startswith("linux"):
64
+ if self._backend == "ble":
65
+ raise RuntimeError("BLE backend is not available on Linux in mdr-bt-shared")
66
+ return _bind_factory(
67
+ "mdrConnectionLinuxCreate",
68
+ "mdrConnectionLinuxGet",
69
+ "mdrConnectionLinuxDestroy",
70
+ )
71
+ raise RuntimeError(f"Unsupported platform for libmdr-bt bindings: {platform}")
72
+
73
+ def _create(self) -> None:
74
+ create, getter, destroy = self._factories()
75
+ owner = create()
76
+ if not owner:
77
+ raise RuntimeError(f"Failed to create platform connection ({self._backend})")
78
+ connection = getter(owner)
79
+ if not connection:
80
+ destroy(owner)
81
+ raise RuntimeError("Platform connection Get() returned null")
82
+
83
+ self._destroy = destroy
84
+ self._owner = c_void_p(owner)
85
+ self._connection = Connection(connection)
86
+
87
+ @property
88
+ def backend(self) -> str:
89
+ return self._backend
90
+
91
+ @property
92
+ def connection(self) -> Connection:
93
+ if self._connection is None:
94
+ raise RuntimeError("Platform connection is closed")
95
+ return self._connection
96
+
97
+ def close(self) -> None:
98
+ if self._owner is not None and self._destroy is not None:
99
+ self._destroy(self._owner)
100
+ self._owner = None
101
+ self._connection = None
102
+
103
+ def __enter__(self) -> PlatformConnection:
104
+ return self
105
+
106
+ def __exit__(self, *exc: object) -> None:
107
+ self.close()
108
+
109
+ def __del__(self) -> None:
110
+ try:
111
+ self.close()
112
+ except Exception:
113
+ pass
114
+
115
+ def devices(self):
116
+ return self.connection.devices()
117
+
118
+ def connect(self, mac_address: str, service_uuid: str) -> int:
119
+ return self.connection.connect(mac_address, service_uuid)
120
+
121
+ def disconnect(self) -> None:
122
+ self.connection.disconnect()
123
+
124
+ def poll(self, timeout_ms: int = 0) -> int:
125
+ return self.connection.poll(timeout_ms)
126
+
127
+ def last_error(self) -> str:
128
+ return self.connection.last_error()
129
+
130
+
131
+ def create_connection(*, ble: bool = False, backend: Backend | None = None) -> PlatformConnection:
132
+ """Create the host platform connection.
133
+
134
+ Prefer `ble=True` / `backend="ble"` on Windows when talking over GATT.
135
+ """
136
+ if backend is None:
137
+ backend = "ble" if ble else "classic"
138
+ return PlatformConnection(backend=backend)
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: libmdr-bt
3
+ Version: 0.1.0
4
+ Summary: Python binding for libmdr-bt (Sony headphones platform connection backend)
5
+ License-Expression: MIT
6
+ Project-URL: Source, https://github.com/mos9527/libmdr-py
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/plain
9
+ License-File: LICENSE
10
+ Requires-Dist: libmdr
11
+ Dynamic: license-file
12
+
13
+ ctypes bindings for libmdr-bt (mdr-bt-shared).
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ src/libmdr_bt/__init__.py
6
+ src/libmdr_bt/_dll.py
7
+ src/libmdr_bt/connection.py
8
+ src/libmdr_bt.egg-info/PKG-INFO
9
+ src/libmdr_bt.egg-info/SOURCES.txt
10
+ src/libmdr_bt.egg-info/dependency_links.txt
11
+ src/libmdr_bt.egg-info/requires.txt
12
+ src/libmdr_bt.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ libmdr
@@ -0,0 +1 @@
1
+ libmdr_bt