add code
This commit is contained in:
6
obd2_client/src/vehicle/__init__.py
Normal file
6
obd2_client/src/vehicle/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Vehicle state and polling."""
|
||||
|
||||
from .state import VehicleState
|
||||
from .poller import VehiclePoller
|
||||
|
||||
__all__ = ["VehicleState", "VehiclePoller"]
|
||||
193
obd2_client/src/vehicle/poller.py
Normal file
193
obd2_client/src/vehicle/poller.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""Vehicle PID polling service."""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import List, Optional, Set
|
||||
from enum import Enum
|
||||
|
||||
from ..obd2.protocol import OBD2Protocol
|
||||
from ..obd2.pids import PIDS
|
||||
from ..logger import get_logger
|
||||
from .state import VehicleState
|
||||
|
||||
|
||||
class PollerState(Enum):
|
||||
"""Poller state enum."""
|
||||
|
||||
STOPPED = "stopped"
|
||||
RUNNING = "running"
|
||||
PAUSED = "paused"
|
||||
|
||||
|
||||
class VehiclePoller:
|
||||
"""Background poller for vehicle PIDs.
|
||||
|
||||
Supports priority-based polling with fast and slow intervals:
|
||||
- Fast PIDs (RPM, speed, throttle): polled frequently
|
||||
- Slow PIDs (temperatures, fuel level): polled less frequently
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
protocol: OBD2Protocol,
|
||||
state: VehicleState,
|
||||
fast_interval: float = 0.1,
|
||||
slow_interval: float = 1.0,
|
||||
fast_pids: Optional[List[int]] = None,
|
||||
slow_pids: Optional[List[int]] = None,
|
||||
):
|
||||
"""Initialize poller.
|
||||
|
||||
Args:
|
||||
protocol: OBD2Protocol instance
|
||||
state: VehicleState to update
|
||||
fast_interval: Polling interval for fast PIDs (seconds)
|
||||
slow_interval: Polling interval for slow PIDs (seconds)
|
||||
fast_pids: List of PIDs to poll frequently
|
||||
slow_pids: List of PIDs to poll less frequently
|
||||
"""
|
||||
self.protocol = protocol
|
||||
self.state = state
|
||||
self.fast_interval = fast_interval
|
||||
self.slow_interval = slow_interval
|
||||
|
||||
self.fast_pids: Set[int] = set(fast_pids or [0x0C, 0x0D, 0x11])
|
||||
self.slow_pids: Set[int] = set(slow_pids or [0x05, 0x2F, 0x5C])
|
||||
|
||||
self._state = PollerState.STOPPED
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._stop_event = threading.Event()
|
||||
self._pause_event = threading.Event()
|
||||
self._pause_event.set()
|
||||
|
||||
self._logger = get_logger("obd2_client.poller")
|
||||
self._stats = {"queries": 0, "successes": 0, "failures": 0}
|
||||
|
||||
@property
|
||||
def poller_state(self) -> PollerState:
|
||||
"""Get current poller state."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""Check if poller is running."""
|
||||
return self._state == PollerState.RUNNING
|
||||
|
||||
def set_pids(
|
||||
self,
|
||||
fast_pids: Optional[List[int]] = None,
|
||||
slow_pids: Optional[List[int]] = None,
|
||||
) -> None:
|
||||
"""Update PIDs to poll.
|
||||
|
||||
Args:
|
||||
fast_pids: New list of fast PIDs (or None to keep current)
|
||||
slow_pids: New list of slow PIDs (or None to keep current)
|
||||
"""
|
||||
if fast_pids is not None:
|
||||
self.fast_pids = set(fast_pids)
|
||||
if slow_pids is not None:
|
||||
self.slow_pids = set(slow_pids)
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the polling thread."""
|
||||
if self._thread is not None and self._thread.is_alive():
|
||||
self._logger.warning("Poller already running")
|
||||
return
|
||||
|
||||
self._stop_event.clear()
|
||||
self._pause_event.set()
|
||||
self._state = PollerState.RUNNING
|
||||
|
||||
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
|
||||
self._thread.start()
|
||||
self._logger.info("Poller started")
|
||||
|
||||
def stop(self, timeout: float = 2.0) -> None:
|
||||
"""Stop the polling thread.
|
||||
|
||||
Args:
|
||||
timeout: Maximum time to wait for thread to stop
|
||||
"""
|
||||
if self._thread is None:
|
||||
return
|
||||
|
||||
self._stop_event.set()
|
||||
self._pause_event.set()
|
||||
|
||||
self._thread.join(timeout=timeout)
|
||||
if self._thread.is_alive():
|
||||
self._logger.warning("Poller thread did not stop cleanly")
|
||||
|
||||
self._thread = None
|
||||
self._state = PollerState.STOPPED
|
||||
self._logger.info("Poller stopped")
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause polling."""
|
||||
self._pause_event.clear()
|
||||
self._state = PollerState.PAUSED
|
||||
self._logger.info("Poller paused")
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Resume polling after pause."""
|
||||
self._pause_event.set()
|
||||
self._state = PollerState.RUNNING
|
||||
self._logger.info("Poller resumed")
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""Get polling statistics."""
|
||||
return dict(self._stats)
|
||||
|
||||
def _poll_loop(self) -> None:
|
||||
"""Main polling loop."""
|
||||
last_slow_poll = 0.0
|
||||
|
||||
while not self._stop_event.is_set():
|
||||
self._pause_event.wait()
|
||||
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
for pid_code in self.fast_pids:
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
self._poll_pid(pid_code)
|
||||
|
||||
if (current_time - last_slow_poll) >= self.slow_interval:
|
||||
for pid_code in self.slow_pids:
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
self._poll_pid(pid_code)
|
||||
last_slow_poll = current_time
|
||||
|
||||
sleep_time = self.fast_interval - (time.time() - current_time)
|
||||
if sleep_time > 0:
|
||||
self._stop_event.wait(sleep_time)
|
||||
|
||||
def _poll_pid(self, pid_code: int) -> bool:
|
||||
"""Poll a single PID and update state.
|
||||
|
||||
Args:
|
||||
pid_code: PID to poll
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
self._stats["queries"] += 1
|
||||
|
||||
response = self.protocol.query_pid(pid_code)
|
||||
if response is None:
|
||||
self._stats["failures"] += 1
|
||||
return False
|
||||
|
||||
self._stats["successes"] += 1
|
||||
self.state.update(
|
||||
pid_code=pid_code,
|
||||
name=response.pid.name,
|
||||
value=response.value,
|
||||
unit=response.unit,
|
||||
)
|
||||
return True
|
||||
173
obd2_client/src/vehicle/state.py
Normal file
173
obd2_client/src/vehicle/state.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""Vehicle state management."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Optional, Any
|
||||
from datetime import datetime
|
||||
import threading
|
||||
|
||||
|
||||
@dataclass
|
||||
class PIDValue:
|
||||
"""Single PID value with metadata.
|
||||
|
||||
Attributes:
|
||||
pid_code: PID code
|
||||
name: PID name
|
||||
value: Current value
|
||||
unit: Unit of measurement
|
||||
timestamp: When value was last updated
|
||||
"""
|
||||
|
||||
pid_code: int
|
||||
name: str
|
||||
value: float
|
||||
unit: str
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name}: {self.value:.1f} {self.unit}"
|
||||
|
||||
def age_seconds(self) -> float:
|
||||
"""Get age of value in seconds."""
|
||||
return (datetime.now() - self.timestamp).total_seconds()
|
||||
|
||||
|
||||
class VehicleState:
|
||||
"""Thread-safe container for current vehicle state.
|
||||
|
||||
Stores latest values for all monitored PIDs with timestamps.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._values: Dict[int, PIDValue] = {}
|
||||
self._lock = threading.RLock()
|
||||
self._callbacks: list = []
|
||||
|
||||
def update(
|
||||
self,
|
||||
pid_code: int,
|
||||
name: str,
|
||||
value: float,
|
||||
unit: str,
|
||||
) -> None:
|
||||
"""Update a PID value.
|
||||
|
||||
Args:
|
||||
pid_code: PID code
|
||||
name: PID name
|
||||
value: New value
|
||||
unit: Unit of measurement
|
||||
"""
|
||||
with self._lock:
|
||||
old_value = self._values.get(pid_code)
|
||||
new_value = PIDValue(
|
||||
pid_code=pid_code,
|
||||
name=name,
|
||||
value=value,
|
||||
unit=unit,
|
||||
)
|
||||
self._values[pid_code] = new_value
|
||||
|
||||
for callback in self._callbacks:
|
||||
try:
|
||||
callback(new_value, old_value)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get(self, pid_code: int) -> Optional[PIDValue]:
|
||||
"""Get current value for a PID.
|
||||
|
||||
Args:
|
||||
pid_code: PID code
|
||||
|
||||
Returns:
|
||||
PIDValue if available, None otherwise
|
||||
"""
|
||||
with self._lock:
|
||||
return self._values.get(pid_code)
|
||||
|
||||
def get_all(self) -> Dict[int, PIDValue]:
|
||||
"""Get all current values.
|
||||
|
||||
Returns:
|
||||
Dictionary of PID code to PIDValue
|
||||
"""
|
||||
with self._lock:
|
||||
return dict(self._values)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all stored values."""
|
||||
with self._lock:
|
||||
self._values.clear()
|
||||
|
||||
def add_callback(self, callback) -> None:
|
||||
"""Add a callback for value updates.
|
||||
|
||||
Callback signature: callback(new_value: PIDValue, old_value: Optional[PIDValue])
|
||||
"""
|
||||
self._callbacks.append(callback)
|
||||
|
||||
def remove_callback(self, callback) -> None:
|
||||
"""Remove a callback."""
|
||||
if callback in self._callbacks:
|
||||
self._callbacks.remove(callback)
|
||||
|
||||
@property
|
||||
def rpm(self) -> Optional[float]:
|
||||
"""Get current RPM value."""
|
||||
val = self.get(0x0C)
|
||||
return val.value if val else None
|
||||
|
||||
@property
|
||||
def speed(self) -> Optional[float]:
|
||||
"""Get current speed value (km/h)."""
|
||||
val = self.get(0x0D)
|
||||
return val.value if val else None
|
||||
|
||||
@property
|
||||
def coolant_temp(self) -> Optional[float]:
|
||||
"""Get current coolant temperature (°C)."""
|
||||
val = self.get(0x05)
|
||||
return val.value if val else None
|
||||
|
||||
@property
|
||||
def throttle(self) -> Optional[float]:
|
||||
"""Get current throttle position (%)."""
|
||||
val = self.get(0x11)
|
||||
return val.value if val else None
|
||||
|
||||
@property
|
||||
def fuel_level(self) -> Optional[float]:
|
||||
"""Get current fuel level (%)."""
|
||||
val = self.get(0x2F)
|
||||
return val.value if val else None
|
||||
|
||||
@property
|
||||
def oil_temp(self) -> Optional[float]:
|
||||
"""Get current oil temperature (°C)."""
|
||||
val = self.get(0x5C)
|
||||
return val.value if val else None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert state to dictionary."""
|
||||
with self._lock:
|
||||
return {
|
||||
val.name: {
|
||||
"value": val.value,
|
||||
"unit": val.unit,
|
||||
"timestamp": val.timestamp.isoformat(),
|
||||
}
|
||||
for val in self._values.values()
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Format state as string."""
|
||||
with self._lock:
|
||||
if not self._values:
|
||||
return "No data"
|
||||
|
||||
lines = []
|
||||
for pid_code in sorted(self._values.keys()):
|
||||
val = self._values[pid_code]
|
||||
lines.append(f" {val}")
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user