Skip to content

Conversation

@PaulMarisOUMary
Copy link
Owner

@PaulMarisOUMary PaulMarisOUMary commented Dec 13, 2025

Summary

  • New Template class for Command's pids parameterization
  • Refactor Command class
    • Remove Command.description
    • Command.formula -> Command.resolver
    • Command.n_bytes -> Command.expected_bytes
    • command_args, name and description has been removed from __init__
  • Refactor GroupCommand class
    • New subclass keyword registry_idfor registering (e.g. class Mode01(GroupCommands, registry_id=Mode.REQUEST): ...)
  • Refactor GroupMode class
  • New BaseEnum parent class
    • get_from classmethod
    • has classmethod

Checklist

  • If code changes were made, they have been tested.
    • Documentation has been updated to reflect the changes.
  • This PR fixes an issue.
  • This PR adds something new (e.g., new method, parameter, or reference to an issue).
  • This PR is a breaking change (e.g., methods or parameters removed/renamed).
  • This PR is not a code change (e.g., documentation, README, ...).

@PaulMarisOUMary PaulMarisOUMary self-assigned this Dec 13, 2025
@PaulMarisOUMary PaulMarisOUMary moved this to 🏗 In Progress in OBDII Dec 13, 2025
@PaulMarisOUMary
Copy link
Owner Author

For reference, the following GroupMode/GroupCommand alternative was considered:
"Instance-less" design

from __future__ import annotations

from enum import Enum, unique
from typing import Container, Dict, Generator, Iterable, Optional, Type, Union, overload


@unique
class Mode(Enum):
    REQUEST = 0x01
    ...
    VEHICLE_INFO = 0x09

class Command:
    def __init__(
        self,
        mode: Union[Mode, int, str],
        pid: Union[int, str],
        expected_bytes: Optional[int]=None,
    ) -> None:
        self.mode = mode
        self.pid = pid
        ...


class GroupCommandsMeta(type, Iterable[Command], Container[Command]):
    def __iter__(cls) -> Generator[Command, None, None]:
        for attr_name in dir(cls):
            if attr_name.startswith('_'):
                continue
            attr = getattr(cls, attr_name)
            if isinstance(attr, Command):
                yield attr

    def __len__(cls) -> int:
        return sum(1 for _ in cls)

    def __contains__(cls, item: Union[Command, str]) -> bool:
        if isinstance(item, str):
            return isinstance(getattr(cls, item.upper(), None), Command)
        elif isinstance(item, Command):
            return any(item is cmd for cmd in cls)
        return False
    
    def __getitem__(cls, key: Union[int, str]) -> Command:
        if isinstance(key, str):
            key_upper = key.upper()
            item = getattr(cls, key_upper, None)
            if not isinstance(item, Command):
                raise KeyError(f"Command '{key}' not found in {cls.__name__}")
            return item
        elif isinstance(key, int):
            for cmd in cls:
                if cmd.pid == key:
                    return cmd
            raise KeyError(f"No command found with PID {hex(key)} in {cls.__name__}")
        raise TypeError(f"Invalid key type: {type(key).__name__}. Expected str or int")

class GroupCommands(metaclass=GroupCommandsMeta):    
    __abstract__ = True
    _registry: Dict[str, Type[GroupCommands]] = {}

    def __init_subclass__(cls, registry_key: Optional[Union[int, Mode]] = None, **kwargs) -> None:
        super().__init_subclass__(**kwargs)

        if registry_key is not None and not cls.__dict__.get("__abstract__", False):
            value = registry_key.value if isinstance(registry_key, Mode) else registry_key

            if value is not None:
                cls._registry[str(value)] = cls


class Mode01(GroupCommands, registry_key=Mode.REQUEST):
    ENGINE_SPEED = Command("01", 0x0C, 4)
    ...

class Mode09(GroupCommands, registry_key=Mode.VEHICLE_INFO):
    SUPPORTED_PIDS_9 = Command("09", 0x00, 0x04)
    ...

... # More modes defined similarly

class Modes(Mode01,Mode09):
    __abstract__ = True

class GroupModesMeta(GroupCommandsMeta):
    """Metaclass for GroupModes that handles both Command and Mode lookups."""
    
    @overload
    def __getitem__(cls, key: str) -> Command: ...
    
    @overload
    def __getitem__(cls, key: Union[Mode, int]) -> Type[GroupCommands]: ...
    
    def __getitem__(cls, key: Union[Mode, int, str]) -> Union[Command, Type[GroupCommands]]:
        """Allows Class[key] syntax for both commands (str) and modes (Mode/int)."""
        if isinstance(key, str):
            return super().__getitem__(key)

        mode_value = key.value if isinstance(key, Mode) else key
        mode_cls = getattr(cls, "_registry", {}).get(str(mode_value))
        if mode_cls is None:
            raise KeyError(f"Mode '{hex(mode_value) if isinstance(mode_value, int) else mode_value}' not found in registry")
        return mode_cls

class GroupModes(Modes, metaclass=GroupModesMeta): ...



if __name__ == "__main__":

    # Allowed syntax

    print(GroupModes)
    print(GroupModes[1], GroupModes[Mode.REQUEST])
    print(GroupModes[9], GroupModes[Mode.VEHICLE_INFO])
    print(GroupModes["ENGINE_SPEED"], GroupModes.ENGINE_SPEED, Mode01["ENGINE_SPEED"], Mode01.ENGINE_SPEED)

    # iterating through GroupModes[MODE_VALUE] or GroupModes[Mode.X] will also work
    for cmd in GroupModes:
        print(f"{cmd}")

    print(len(GroupModes))

    print(GroupModes._registry)

    print(GroupModes["ENGINE_SPEED"] in Mode01)
    print(GroupModes["ENGINE_SPEED"] in Mode09)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 🏗 In Progress

Development

Successfully merging this pull request may close these issues.

1 participant