# Copyright (c) 2010-2024 Emmanuel Blot # Copyright (c) 2008-2016, Neotion # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """Miscellaneous helpers""" # pylint: disable=invalid-name # pylint: disable=import-outside-toplevel from array import array from copy import deepcopy from re import match from typing import Any, Iterable, Optional, Sequence, Union # String values evaluated as true boolean values TRUE_BOOLEANS = ['on', 'true', 'enable', 'enabled', 'yes', 'high', '1'] # String values evaluated as false boolean values FALSE_BOOLEANS = ['off', 'false', 'disable', 'disabled', 'no', 'low', '0'] # ASCII or '.' filter ASCIIFILTER = ''.join([((len(repr(chr(_x))) == 3) or (_x == 0x5c)) and chr(_x) or '.' for _x in range(128)]) + '.' * 128 ASCIIFILTER = bytearray(ASCIIFILTER.encode('ascii')) def hexdump(data: Union[bytes, bytearray, Iterable[int]], full: bool = False, abbreviate: bool = False) -> str: """Convert a binary buffer into a hexadecimal representation. Return a multi-line strings with hexadecimal values and ASCII representation of the buffer data. :param data: binary buffer to dump :param full: use `hexdump -Cv` format :param abbreviate: replace identical lines with '*' :return: the generated string """ try: if isinstance(data, (bytes, array)): src = bytearray(data) elif not isinstance(data, bytearray): # data may be a list/tuple src = bytearray(b''.join(data)) else: src = data except Exception as exc: raise TypeError(f"Unsupported data type '{type(data)}'") from exc length = 16 result = [] last = b'' abv = False for i in range(0, len(src), length): s = src[i:i+length] if abbreviate: if s == last: if not abv: result.append('*\n') abv = True continue abv = False hexa = ' '.join((f'{x:02x}' for x in s)) printable = s.translate(ASCIIFILTER).decode('ascii') if full: hx1, hx2 = hexa[:3*8], hexa[3*8:] hl = length//2 result.append(f'{i:08x} {hx1:<{hl*3}} {hx2:<{hl*3}} ' f'|{printable}|\n') else: result.append(f'{i:06x} {hexa:<{length*3}} {printable}\n') last = s return ''.join(result) def hexline(data: Union[bytes, bytearray, Iterable[int]], sep: str = ' ') -> str: """Convert a binary buffer into a hexadecimal representation. Return a string with hexadecimal values and ASCII representation of the buffer data. :param data: binary buffer to dump :param sep: the separator string/char :return: the formatted string """ try: if isinstance(data, (bytes, array)): src = bytearray(data) elif not isinstance(data, bytearray): # data may be a list/tuple src = bytearray(b''.join(data)) else: src = data except Exception as exc: raise TypeError(f"Unsupported data type '{type(data)}'") from exc hexa = sep.join((f'{x:02x}' for x in src)) printable = src.translate(ASCIIFILTER).decode('ascii') return f'({len(data)}) {hexa} : {printable}' def to_int(value: Union[int, str]) -> int: """Parse a value and convert it into an integer value if possible. Input value may be: - a string with an integer coded as a decimal value - a string with an integer coded as a hexadecimal value - a integral value - a integral value with a unit specifier (kilo or mega) :param value: input value to convert to an integer :return: the value as an integer :rtype: int :raise ValueError: if the input value cannot be converted into an int """ if not value: return 0 if isinstance(value, int): return value mo = match(r'^\s*(\d+)\s*(?:([KMkm]i?)?B?)?\s*$', value) if mo: mult = {'K': (1000), 'KI': (1 << 10), 'M': (1000 * 1000), 'MI': (1 << 20)} value = int(mo.group(1)) if mo.group(2): value *= mult[mo.group(2).upper()] return value return int(value.strip(), value.startswith('0x') and 16 or 10) def to_bool(value: Union[int, bool, str], permissive: bool = True, allow_int: bool = False) -> bool: """Parse a string and convert it into a boolean value if possible. Input value may be: - a string with an integer value, if `allow_int` is enabled - a boolean value - a string with a common boolean definition :param value: the value to parse and convert :param permissive: default to the False value if parsing fails :param allow_int: allow an integral type as the input value :raise ValueError: if the input value cannot be converted into an bool """ if value is None: return False if isinstance(value, bool): return value if isinstance(value, int): if allow_int: return bool(value) if permissive: return False raise ValueError(f"Invalid boolean value: '{value}'") if value.lower() in TRUE_BOOLEANS: return True if permissive or (value.lower() in FALSE_BOOLEANS): return False raise ValueError(f"Invalid boolean value: '{value}'") def to_bps(value: str) -> int: """Parse a string and convert it into a baudrate value. The function accepts common multipliers as K, M and G :param value: the value to parse and convert :type value: str or int or float :rtype: float :raise ValueError: if the input value cannot be converted into a float """ if isinstance(value, float): return int(value) if isinstance(value, int): return value mo = match(r'^(?P[-+]?[0-9]*\.?[0-9]+(?:[Ee][-+]?[0-9]+)?)' r'(?P[KkMmGg])?$', value) if not mo: raise ValueError('Invalid frequency') frequency = float(mo.group(1)) if mo.group(2): mult = {'K': 1E3, 'M': 1E6, 'G': 1E9} frequency *= mult[mo.group(2).upper()] return int(frequency) def xor(_a_: bool, _b_: bool) -> bool: """XOR logical operation. :param _a_: first argument :param _b_: second argument :return: xor-ed value """ # pylint: disable=superfluous-parens return bool((not (_a_) and _b_) or (_a_ and not (_b_))) def is_iterable(obj: Any) -> bool: """Tells whether an instance is iterable or not. :param obj: the instance to test :type obj: object :return: True if the object is iterable :rtype: bool """ try: iter(obj) return True except TypeError: return False def pretty_size(size, sep: str = ' ', lim_k: int = 1 << 10, lim_m: int = 10 << 20, plural: bool = True, floor: bool = True) -> str: """Convert a size into a more readable unit-indexed size (KiB, MiB) :param size: integral value to convert :param sep: the separator character between the integral value and the unit specifier :param lim_k: any value above this limit is a candidate for KiB conversion. :param lim_m: any value above this limit is a candidate for MiB conversion. :param plural: whether to append a final 's' to byte(s) :param floor: how to behave when exact conversion cannot be achieved: take the closest, smaller value or fallback to the next unit that allows the exact representation of the input value :return: the prettyfied size """ size = int(size) if size > lim_m: ssize = size >> 20 if floor or (ssize << 20) == size: return f'{ssize}{sep}MiB' if size > lim_k: ssize = size >> 10 if floor or (ssize << 10) == size: return f'{ssize}{sep}KiB' return f'{size}{sep}byte{plural and "s" or ""}' def add_custom_devices(ftdicls=None, vpstr: Optional[Sequence[str]] = None, force_hex: bool = False) -> None: """Helper function to add custom VID/PID to FTDI device identifer map. The string to parse should match the following format: [vendor_name=]:[product_name=] * vendor_name and product_name are optional strings, they may be omitted as they only serve as human-readable aliases for vendor and product names. * vendor_id and product_id are mandatory strings that should resolve as 16-bit integer (USB VID and PID values). They may be expressed as decimal or hexadecimal syntax. ex: * ``0x403:0x9999``, vid:pid short syntax, with no alias names * ``mycompany=0x666:myproduct=0xcafe``, vid:pid complete syntax with aliases :param vpstr: typically, a option switch string describing the device to add :param ftdicls: the Ftdi class that should support the new device. :param force_hex: if set, consider that the pid/vid string are hexadecimal encoded values. """ from inspect import isclass if not isclass(ftdicls): raise ValueError('Expect Ftdi class, not instance') for vidpid in vpstr or []: vidpids = {vid: set() for vid in ftdicls.PRODUCT_IDS} vname = '' pname = '' try: vid, pid = vidpid.split(':') if '=' in vid: vname, vid = vid.split('=', 1) if '=' in pid: pname, pid = pid.split('=', 1) if force_hex: vid, pid = [int(v, 16) for v in (vid, pid)] else: vid, pid = [to_int(v) for v in (vid, pid)] except ValueError as exc: raise ValueError('Invalid VID:PID value') from exc if vid not in vidpids: ftdicls.add_custom_vendor(vid, vname) vidpids[vid] = set() if pid not in vidpids[vid]: ftdicls.add_custom_product(vid, pid, pname) vidpids[vid].add(pid) def show_call_stack(): """Print the current call stack, only useful for debugging purpose.""" from sys import _current_frames from threading import current_thread from traceback import print_stack print_stack(_current_frames()[current_thread().ident]) class classproperty(property): """Getter property decorator for a class""" # pylint: disable=invalid-name def __get__(self, obj: Any, objtype=None) -> Any: return super().__get__(objtype) class EasyDict(dict): """Dictionary whose members can be accessed as instance members """ def __init__(self, dictionary=None, **kwargs): super().__init__(self) if dictionary is not None: self.update(dictionary) self.update(kwargs) def __getattr__(self, name): try: return self.__getitem__(name) except KeyError as exc: raise AttributeError(f"'{self.__class__.__name__}' object has no " f"attribute '{name}'") from exc def __setattr__(self, name, value): self.__setitem__(name, value) @classmethod def copy(cls, dictionary): def _deep_copy(obj): if isinstance(obj, list): return [_deep_copy(v) for v in obj] if isinstance(obj, dict): return EasyDict({k: _deep_copy(obj[k]) for k in obj}) return deepcopy(obj) return cls(_deep_copy(dictionary)) def mirror(self) -> 'EasyDict': """Instanciate a mirror EasyDict.""" return EasyDict({v: k for k, v in self.items()})