Source code for pyjoulescope_driver.device_filter

# SPDX-FileCopyrightText: Copyright 2026 Jetperch LLC
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Select devices from the driver's device paths.

The driver reports all devices, including Joulescope instruments and
other devices, such as a MiniBitty at "u/mb/{serial_number}".  Use the
brand argument to select only Joulescope instruments, or a backend and
model specification such as "u/js320/" to select every JS320 on one
interface.  See :class:`DevicePath` for the device path and specification
formats.

Use this module as a namespace::

    from pyjoulescope_driver import device_filter
    device_path = device_filter.find_one(paths, '31NB', brand='joulescope')

Driver.device_paths() and Driver.find_one_device() provide the same
selection directly.
"""

from .device_path import DevicePath, brand_validate


[docs] class DeviceFilterError(ValueError): """The device specifications did not match exactly one device. :ivar specs: The list of device specifications, or None for all. :ivar brand: The brand, such as "Joulescope", or None for all. :ivar matches: The list of matching device paths. :ivar available: The list of all device paths for the brand. """ def __init__(self, specs, brand, matches, available): self.specs = specs self.brand = brand self.matches = matches self.available = available noun = brand if brand else 'device' name = f'Device "{", ".join(specs)}"' if specs else None if not matches: msg = f'{name} not found in {available}' if name else f'No {noun} found' elif name: msg = f'{name} matched multiple {noun}s: {matches}' else: msg = f'Multiple {noun}s found: {matches}' super().__init__(msg)
def _specs_normalize(specs): if specs is None: return None if isinstance(specs, str): specs = [spec.strip() for spec in specs.split(',')] specs = [spec for spec in specs if spec] elif not isinstance(specs, (list, tuple)): raise TypeError(f'specs must be str or a list of str, not {type(specs).__name__}') elif not all(isinstance(spec, str) for spec in specs): raise TypeError('specs must be str or a list of str') return list(specs) or None def _brand_normalize(brand): return None if brand is None else brand_validate(brand)
[docs] def find(device_paths, specs=None, brand=None): """Find the device paths for the matching devices. :param device_paths: The list of device path strings, such as from Driver.device_paths(). :param specs: The device specifications, which is one of: * None to match all devices. * a string containing one or more comma-separated device specifications, such as "js320" or "31NB, u/js220/000415". Whitespace around each specification is ignored, and an empty string matches all devices. * a list of device specification strings. A device matches if it matches any specification. See :meth:`DevicePath.match` for the device specification format. :param brand: The case-insensitive brand, such as "Joulescope", to only match devices of that brand. None (default) matches all brands. :return: The list of matching :class:`DevicePath` instances, in order. :raise TypeError: If specs or brand has an invalid type. :raise ValueError: If brand is not supported. """ specs = _specs_normalize(specs) brand = _brand_normalize(brand) paths = [DevicePath(p) for p in device_paths] if brand is not None: paths = [p for p in paths if p.brand == brand] if specs is None: return paths return [p for p in paths if any(p.match(spec) for spec in specs)]
[docs] def find_one(device_paths, specs=None, brand=None): """Find the device path for exactly one matching device. :param device_paths: The list of device path strings, such as from Driver.device_paths(). :param specs: The device specifications. See :func:`find`. :param brand: The brand. See :func:`find`. :return: The matching :class:`DevicePath`. :raise DeviceFilterError: If zero or multiple devices match. """ matches = find(device_paths, specs, brand) if len(matches) != 1: raise DeviceFilterError(_specs_normalize(specs), _brand_normalize(brand), matches, find(device_paths, brand=brand)) return matches[0]