mirror of
https://github.com/dragonpilot/dragonpilot.git
synced 2026-04-08 14:03:54 +08:00
feat: Squash all min-features into full
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -95,3 +95,6 @@ Pipfile
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# rick - keep panda_tici standalone
|
||||
panda_tici/
|
||||
|
||||
140
ALKA_DESIGN.md
Normal file
140
ALKA_DESIGN.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# ALKA (Always-on Lane Keeping Assist) Design v3
|
||||
|
||||
## Overview
|
||||
|
||||
ALKA enables lateral control (steering) when ACC Main is ON, without requiring cruise to be engaged. This allows lane keeping assist to function independently of longitudinal control.
|
||||
|
||||
**Simplified Behavior (v3):**
|
||||
- All brands use direct tracking: `lkas_on = acc_main_on`
|
||||
- No button/toggle tracking (removed TJA, LKAS button, LKAS HUD)
|
||||
- ACC Main ON = ALKA enabled, ACC Main OFF = ALKA disabled
|
||||
|
||||
---
|
||||
|
||||
## Per-Brand Summary
|
||||
|
||||
| Brand | Status | ACC Main Source | Notes |
|
||||
|-------|--------|-----------------|-------|
|
||||
| Body | Disabled | - | No steering capability |
|
||||
| Chrysler | Disabled | - | Needs special handling |
|
||||
| Ford | Enabled | EngBrakeData (0x165) CcStat | |
|
||||
| GM | Disabled | - | No ACC Main signal |
|
||||
| Honda Nidec | Enabled | SCM_FEEDBACK (0x326) MAIN_ON | |
|
||||
| Honda Bosch | Enabled | SCM_FEEDBACK (0x326) MAIN_ON | |
|
||||
| Hyundai | Enabled | SCC11 (0x420) bit 0 | |
|
||||
| Hyundai CAN-FD | Enabled | SCC_CONTROL (0x1A0) bit 66 | |
|
||||
| Hyundai Legacy | Enabled | SCC11 (0x420) bit 0 | |
|
||||
| Mazda | Enabled | CRZ_CTRL (0x21C) bit 17 | |
|
||||
| Nissan | Enabled | CRUISE_THROTTLE (0x239) bit 17 | |
|
||||
| PSA | Disabled | - | Not implemented |
|
||||
| Rivian | Disabled | - | Different architecture |
|
||||
| Subaru | Enabled | CruiseControl (0x240) bit 40 | |
|
||||
| Subaru Preglobal | Enabled | CruiseControl (0x144) bit 48 | |
|
||||
| Tesla | Disabled | - | Different architecture |
|
||||
| Toyota | Enabled | PCM_CRUISE_2 (0x1D3) bit 15 | |
|
||||
| Toyota (UNSUPPORTED_DSU) | Enabled | DSU_CRUISE (0x365) bit 0 | |
|
||||
| VW MQB | Enabled | TSK_06 TSK_Status (>=2) | |
|
||||
| VW PQ | Enabled | Motor_5 (0x480) bit 50 (long) | |
|
||||
|
||||
---
|
||||
|
||||
## Permission Model
|
||||
|
||||
Lateral control requires checks at both layers. Normal path uses `controls_allowed`, ALKA path uses additional checks.
|
||||
|
||||
| Check | Panda | openpilot | Notes |
|
||||
|-------|:-----:|:---------:|-------|
|
||||
| **Normal Path** |
|
||||
| `controls_allowed` (cruise engaged) | ✓ | ✓ | Either this OR ALKA path |
|
||||
| **ALKA Path** |
|
||||
| `alka_allowed` (brand supports) | ✓ | ✓ | Set per brand in safety init |
|
||||
| `ALT_EXP_ALKA` (user enabled) | ✓ | ✓ | alternativeExperience flag |
|
||||
| `lkas_on` (ACC Main ON) | ✓ | ✓ | Tracked via CAN messages |
|
||||
| `vehicle_moving` / `!standstill` | ✓ | ✓ | |
|
||||
| **openpilot Additional** |
|
||||
| `gear_ok` (not P/N/R) | ✗ | ✓ | Python layer only |
|
||||
| `calibrated` | ✗ | ✓ | Python layer only |
|
||||
| `seatbelt latched` | ✗ | ✓ | Python layer only |
|
||||
| `doors closed` | ✗ | ✓ | Python layer only |
|
||||
| `!steerFaultTemporary` | ✗ | ✓ | Python layer only |
|
||||
| `!steerFaultPermanent` | ✗ | ✓ | Python layer only |
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ CAN Bus │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
|
||||
│ Safety Layer (panda C code) │ │ Python Layer │
|
||||
│ │ │ │
|
||||
│ rx_hook: │ │ carstate.py: │
|
||||
│ - Parse ACC Main signal │ │ - Parse cruiseState.available │
|
||||
│ - Set lkas_on = acc_main_on │ │ - Set self.lkas_on │
|
||||
│ │ │ │
|
||||
│ lat_control_allowed(): │ └─────────────┬───────────────────┘
|
||||
│ - Check lkas_on + other flags │ │
|
||||
│ - Gate steering commands │ ▼
|
||||
└─────────────────────────────────┘ ┌─────────────────────────────────┐
|
||||
│ card.py: │
|
||||
│ - Publish carStateExt.lkasOn │
|
||||
└─────────────┬───────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────┐
|
||||
│ controlsd.py: │
|
||||
│ - Read carStateExt.lkasOn │
|
||||
│ - Check ALKA conditions │
|
||||
│ - Set CC.latActive │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `custom.capnp` | Defines `CarStateExt` struct with `lkasOn` field |
|
||||
| `log.capnp` | Includes `carStateExt` in event union |
|
||||
| `interfaces.py` | Defines `self.lkas_on = False` default in `CarStateBase` |
|
||||
| `carstate.py` (per brand) | Tracks `lkas_on` based on ACC Main |
|
||||
| `card.py` | Publishes `carStateExt.lkasOn` from `CI.CS.lkas_on` |
|
||||
| `controlsd.py` | Reads `carStateExt.lkasOn` to determine `alka_active` |
|
||||
|
||||
---
|
||||
|
||||
## ACC Main Tracking
|
||||
|
||||
All brands use simple direct tracking:
|
||||
|
||||
```c
|
||||
// Panda (C code)
|
||||
if (alka_allowed && (alternative_experience & ALT_EXP_ALKA)) {
|
||||
lkas_on = acc_main_on; // or GET_BIT(msg, bit_position)
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# Python carstate.py
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
```
|
||||
|
||||
This guard ensures:
|
||||
1. Brand supports ALKA (`alka_allowed`)
|
||||
2. User enabled ALKA (`ALT_EXP_ALKA`)
|
||||
|
||||
Without both conditions, no ACC Main tracking occurs, and ALKA remains disabled.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Safety tests verify:
|
||||
- `alka_allowed` flag set correctly per brand
|
||||
- ACC Main tracking updates `lkas_on` directly
|
||||
- `lat_control_allowed()` returns true only when all conditions met
|
||||
- Steering TX blocked when ALKA conditions not met
|
||||
- Bus routing variants (camera_scc, unsupported_dsu)
|
||||
@@ -196,6 +196,7 @@ Export('messaging')
|
||||
|
||||
# Build other submodules
|
||||
SConscript(['panda/SConscript'])
|
||||
SConscript(['panda_tici/SConscript'])
|
||||
|
||||
# Build rednose library
|
||||
SConscript(['rednose/SConscript'])
|
||||
|
||||
@@ -10,16 +10,50 @@ $Cxx.namespace("cereal");
|
||||
# DO rename the structs
|
||||
# DON'T change the identifier (e.g. @0x81c2f05a394cf4af)
|
||||
|
||||
struct CustomReserved0 @0x81c2f05a394cf4af {
|
||||
struct ControlsStateExt @0x81c2f05a394cf4af {
|
||||
alkaActive @0 :Bool;
|
||||
}
|
||||
|
||||
struct CustomReserved1 @0xaedffd8f31e7b55d {
|
||||
struct CarStateExt @0xaedffd8f31e7b55d {
|
||||
# dp - ALKA: lkasOn state from carstate (mirrors panda's lkas_on)
|
||||
lkasOn @0 :Bool;
|
||||
}
|
||||
|
||||
struct CustomReserved2 @0xf35cc4560bbf6ec2 {
|
||||
struct ModelExt @0xf35cc4560bbf6ec2 {
|
||||
leftEdgeDetected @0 :Bool;
|
||||
rightEdgeDetected @1 :Bool;
|
||||
}
|
||||
|
||||
struct CustomReserved3 @0xda96579883444c35 {
|
||||
struct LiveGPS @0xda96579883444c35 {
|
||||
# Position
|
||||
latitude @0 :Float64; # degrees
|
||||
longitude @1 :Float64; # degrees
|
||||
altitude @2 :Float64; # meters (WGS84)
|
||||
|
||||
# Motion
|
||||
speed @3 :Float32; # m/s (horizontal speed)
|
||||
bearingDeg @4 :Float32; # degrees (heading)
|
||||
|
||||
# Accuracy
|
||||
horizontalAccuracy @5 :Float32; # meters
|
||||
verticalAccuracy @6 :Float32; # meters
|
||||
|
||||
# Status
|
||||
gpsOK @7 :Bool; # livePose valid + GPS fresh
|
||||
status @8 :Status;
|
||||
|
||||
enum Status {
|
||||
noGps @0;
|
||||
initializing @1;
|
||||
calibrating @2;
|
||||
valid @3;
|
||||
recalibrating @4;
|
||||
gpsStale @5;
|
||||
}
|
||||
|
||||
# Metadata
|
||||
unixTimestampMillis @9 :Int64;
|
||||
lastGpsTimestamp @10 :UInt64; # logMonoTime of last GPS
|
||||
}
|
||||
|
||||
struct CustomReserved4 @0x80ae746ee2596b11 {
|
||||
|
||||
@@ -2625,10 +2625,10 @@ struct Event {
|
||||
# DO change the name of the field and struct
|
||||
# DON'T change the ID (e.g. @107)
|
||||
# DON'T change which struct it points to
|
||||
customReserved0 @107 :Custom.CustomReserved0;
|
||||
customReserved1 @108 :Custom.CustomReserved1;
|
||||
customReserved2 @109 :Custom.CustomReserved2;
|
||||
customReserved3 @110 :Custom.CustomReserved3;
|
||||
controlsStateExt @107 :Custom.ControlsStateExt;
|
||||
carStateExt @108 :Custom.CarStateExt;
|
||||
modelExt @109 :Custom.ModelExt;
|
||||
liveGPS @110 :Custom.LiveGPS;
|
||||
customReserved4 @111 :Custom.CustomReserved4;
|
||||
customReserved5 @112 :Custom.CustomReserved5;
|
||||
customReserved6 @113 :Custom.CustomReserved6;
|
||||
|
||||
@@ -102,6 +102,10 @@ _services: dict[str, tuple] = {
|
||||
"customReservedRawData0": (True, 0.),
|
||||
"customReservedRawData1": (True, 0.),
|
||||
"customReservedRawData2": (True, 0.),
|
||||
"controlsStateExt": (False, 100.),
|
||||
"carStateExt": (False, 100.),
|
||||
"modelExt": (True, 20.),
|
||||
"liveGPS": (True, 20.),
|
||||
}
|
||||
SERVICE_LIST = {name: Service(*vals) for
|
||||
idx, (name, vals) in enumerate(_services.items())}
|
||||
|
||||
@@ -131,4 +131,29 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
{"dp_dev_last_log", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
|
||||
{"dp_dev_reset_conf", {CLEAR_ON_MANAGER_START, BOOL, "0"}},
|
||||
{"dp_dev_beep", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_dev_is_rhd", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_lat_alka", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_ui_display_mode", {PERSISTENT, INT, "0"}},
|
||||
{"dp_dev_model_selected", {PERSISTENT, STRING}},
|
||||
{"dp_dev_model_list", {PERSISTENT, STRING}},
|
||||
{"dp_lat_lca_speed", {PERSISTENT, INT, "20"}},
|
||||
{"dp_lat_lca_auto_sec", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"dp_dev_go_off_road", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"dp_ui_hide_hud_speed_kph", {PERSISTENT, INT, "0"}},
|
||||
{"dp_lon_ext_radar", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_lat_road_edge_detection", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_ui_rainbow", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_lon_acm", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_lon_aem", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_lon_dtsc", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_dev_audible_alert_mode", {PERSISTENT, INT, "0"}},
|
||||
{"dp_dev_auto_shutdown_in", {PERSISTENT, INT, "-5"}},
|
||||
{"dp_ui_lead", {PERSISTENT, INT, "0"}},
|
||||
{"dp_dev_dashy", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_dev_delay_loggerd", {PERSISTENT, INT, "0"}},
|
||||
{"dp_dev_disable_connect", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_dev_tethering", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_ui_mici", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_lat_offset_cm", {PERSISTENT, INT, "0"}},
|
||||
};
|
||||
|
||||
0
dragonpilot/dashy/.nojekyll
Normal file
0
dragonpilot/dashy/.nojekyll
Normal file
15
dragonpilot/dashy/LICENSE.md
Normal file
15
dragonpilot/dashy/LICENSE.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Copyright (c) 2025, Rick Lan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
46
dragonpilot/dashy/README.md
Normal file
46
dragonpilot/dashy/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Dashy Release Branch
|
||||
|
||||
This is the production-ready release branch of Dashy - Dragonpilot's All-in-one System Hub for You.
|
||||
|
||||
## 🚀 Quick Installation
|
||||
|
||||
```bash
|
||||
git clone -b release https://github.com/efinilan/dashy
|
||||
cd dashy
|
||||
python3 backend/server.py
|
||||
```
|
||||
|
||||
## 📁 What's Included
|
||||
|
||||
- `backend/` - Python server with all dependencies included
|
||||
- `web/` - Pre-built web interface (minified and optimized)
|
||||
|
||||
## 🌐 Access
|
||||
|
||||
After starting the server, open Chrome browser and navigate to:
|
||||
```
|
||||
http://<device-ip>:5088
|
||||
```
|
||||
|
||||
## 🔧 Requirements
|
||||
|
||||
- Network connection
|
||||
- Port 5088 available
|
||||
|
||||
## 📄 License
|
||||
|
||||
Copyright (c) 2025, Rick Lan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
1
dragonpilot/dashy/backend/.gitignore
vendored
Normal file
1
dragonpilot/dashy/backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
544
dragonpilot/dashy/backend/server.py
Executable file
544
dragonpilot/dashy/backend/server.py
Executable file
@@ -0,0 +1,544 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Copyright (c) 2025, Rick Lan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from urllib.parse import quote
|
||||
|
||||
from aiohttp import web, ClientSession, ClientTimeout
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware import PC, HARDWARE
|
||||
from openpilot.system.ui.lib.multilang import multilang as base_multilang
|
||||
from dragonpilot.settings import SETTINGS
|
||||
|
||||
# --- Configuration ---
|
||||
DEFAULT_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), '..') if PC else '/data/media/0/realdata')
|
||||
WEB_DIST_PATH = os.path.join(os.path.dirname(__file__), "..", "web", "dist")
|
||||
WEBRTC_TIMEOUT = ClientTimeout(total=10)
|
||||
CAR_PARAMS_CACHE_TTL = 30 # seconds
|
||||
|
||||
logger = logging.getLogger("dashy")
|
||||
|
||||
|
||||
# --- Caching Layer ---
|
||||
class AppCache:
|
||||
"""Centralized cache for expensive operations."""
|
||||
|
||||
def __init__(self):
|
||||
self._params = None
|
||||
self._car_params = None
|
||||
self._car_params_time = 0
|
||||
self._context = None
|
||||
self._context_time = 0
|
||||
|
||||
@property
|
||||
def params(self) -> Params:
|
||||
"""Get shared Params instance."""
|
||||
if self._params is None:
|
||||
self._params = Params()
|
||||
return self._params
|
||||
|
||||
def get_car_params(self):
|
||||
"""Get cached CarParams data (brand, longitudinal control)."""
|
||||
now = time.time()
|
||||
if self._car_params is None or (now - self._car_params_time) > CAR_PARAMS_CACHE_TTL:
|
||||
self._car_params = self._parse_car_params()
|
||||
self._car_params_time = now
|
||||
return self._car_params
|
||||
|
||||
def _parse_car_params(self):
|
||||
"""Parse CarParams from Params store."""
|
||||
result = {'brand': '', 'openpilot_longitudinal_control': False}
|
||||
try:
|
||||
car_params_bytes = self.params.get("CarParams")
|
||||
if car_params_bytes:
|
||||
from cereal import car
|
||||
with car.CarParams.from_bytes(car_params_bytes) as cp:
|
||||
result['brand'] = cp.brand
|
||||
result['openpilot_longitudinal_control'] = cp.openpilotLongitudinalControl
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not parse CarParams: {e}")
|
||||
return result
|
||||
|
||||
def get_settings_context(self):
|
||||
"""Get context dict for settings condition evaluation."""
|
||||
now = time.time()
|
||||
if self._context is None or (now - self._context_time) > CAR_PARAMS_CACHE_TTL:
|
||||
car_params = self.get_car_params()
|
||||
self._context = {
|
||||
'brand': car_params['brand'],
|
||||
'openpilotLongitudinalControl': car_params['openpilot_longitudinal_control'],
|
||||
'LITE': os.getenv("LITE") is not None,
|
||||
'MICI': self._check_mici()
|
||||
}
|
||||
self._context_time = now
|
||||
return self._context
|
||||
|
||||
def _check_mici(self):
|
||||
"""Check if device is MICI type."""
|
||||
try:
|
||||
return HARDWARE.get_device_type() == "mici"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_bool_safe(self, key, default=False):
|
||||
"""Safely get a boolean param with default."""
|
||||
try:
|
||||
return self.params.get_bool(key)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def invalidate(self):
|
||||
"""Invalidate all caches."""
|
||||
self._car_params = None
|
||||
self._context = None
|
||||
|
||||
|
||||
# --- Helper Functions ---
|
||||
def api_handler(func):
|
||||
"""Decorator for API handlers with consistent error handling."""
|
||||
@wraps(func)
|
||||
async def wrapper(request):
|
||||
try:
|
||||
return await func(request)
|
||||
except web.HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"{func.__name__} error: {e}", exc_info=True)
|
||||
return web.json_response({'error': str(e)}, status=500)
|
||||
return wrapper
|
||||
|
||||
|
||||
def get_safe_path(requested_path):
|
||||
"""Ensures the requested path is within DEFAULT_DIR."""
|
||||
combined_path = os.path.join(DEFAULT_DIR, requested_path.lstrip('/'))
|
||||
safe_path = os.path.realpath(combined_path)
|
||||
if os.path.commonpath((safe_path, DEFAULT_DIR)) == DEFAULT_DIR:
|
||||
return safe_path
|
||||
return None
|
||||
|
||||
|
||||
def eval_condition(condition, context):
|
||||
"""Safely evaluate a condition string."""
|
||||
if not condition:
|
||||
return True
|
||||
try:
|
||||
return eval(condition, {"__builtins__": {}}, context)
|
||||
except Exception as e:
|
||||
logger.debug(f"Condition evaluation failed: {condition}, error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def resolve_value(value):
|
||||
"""Resolve callable values (lambdas) for JSON serialization."""
|
||||
return value() if callable(value) else value
|
||||
|
||||
|
||||
# --- API Endpoints ---
|
||||
@api_handler
|
||||
async def init_api(request):
|
||||
"""Provide initial data to the client."""
|
||||
cache: AppCache = request.app['cache']
|
||||
car_params = cache.get_car_params()
|
||||
|
||||
return web.json_response({
|
||||
'is_metric': cache.get_bool_safe("IsMetric"),
|
||||
'dp_dev_dashy': cache.get_bool_safe("dp_dev_dashy", True),
|
||||
'openpilot_longitudinal_control': car_params['openpilot_longitudinal_control'],
|
||||
'ublox_available': cache.get_bool_safe("UbloxAvailable", True),
|
||||
'dp_lat_alka': cache.get_bool_safe("dp_lat_alka", False),
|
||||
})
|
||||
|
||||
|
||||
@api_handler
|
||||
async def list_files_api(request):
|
||||
"""List files and folders."""
|
||||
path_param = request.query.get('path', '/')
|
||||
safe_path = get_safe_path(path_param)
|
||||
|
||||
if not safe_path or not os.path.isdir(safe_path):
|
||||
return web.json_response({'error': 'Invalid or Not Found Path'}, status=404)
|
||||
|
||||
items = []
|
||||
for entry in os.listdir(safe_path):
|
||||
full_path = os.path.join(safe_path, entry)
|
||||
try:
|
||||
stat = os.stat(full_path)
|
||||
is_dir = os.path.isdir(full_path)
|
||||
items.append({
|
||||
'name': entry,
|
||||
'is_dir': is_dir,
|
||||
'mtime': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M'),
|
||||
'size': stat.st_size if not is_dir else 0
|
||||
})
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
|
||||
# Sort: directories first (by mtime desc), then files (by mtime desc)
|
||||
dirs = sorted([i for i in items if i['is_dir']], key=lambda x: x['mtime'], reverse=True)
|
||||
files = sorted([i for i in items if not i['is_dir']], key=lambda x: x['mtime'], reverse=True)
|
||||
|
||||
relative_path = os.path.relpath(safe_path, DEFAULT_DIR)
|
||||
return web.json_response({
|
||||
'path': '' if relative_path == '.' else relative_path,
|
||||
'files': dirs + files
|
||||
})
|
||||
|
||||
|
||||
@api_handler
|
||||
async def serve_player_api(request):
|
||||
"""Serve the HLS player page."""
|
||||
file_path = request.query.get('file')
|
||||
if not file_path:
|
||||
return web.Response(text="File parameter is required.", status=400)
|
||||
|
||||
player_html_path = os.path.join(WEB_DIST_PATH, 'pages', 'player.html')
|
||||
try:
|
||||
with open(player_html_path, 'r') as f:
|
||||
html_template = f.read()
|
||||
except FileNotFoundError:
|
||||
return web.Response(text="Player HTML not found.", status=500)
|
||||
|
||||
html = html_template.replace('{{FILE_PATH}}', quote(file_path))
|
||||
return web.Response(text=html, content_type='text/html')
|
||||
|
||||
|
||||
@api_handler
|
||||
async def serve_manifest_api(request):
|
||||
"""Dynamically generate m3u8 playlist."""
|
||||
file_path = request.query.get('file', '').lstrip('/')
|
||||
if not file_path:
|
||||
return web.Response(text="File parameter is required.", status=400)
|
||||
|
||||
encoded_path = quote(file_path)
|
||||
manifest = f"#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:60\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:60.0,\n/media/{encoded_path}\n#EXT-X-ENDLIST\n"
|
||||
return web.Response(text=manifest, content_type='application/vnd.apple.mpegurl')
|
||||
|
||||
|
||||
@api_handler
|
||||
async def get_settings_config_api(request):
|
||||
"""Get the settings configuration from settings.py."""
|
||||
cache: AppCache = request.app['cache']
|
||||
params = cache.params
|
||||
|
||||
# Update language if changed
|
||||
current_lang = params.get("LanguageSetting")
|
||||
if current_lang:
|
||||
lang_str = current_lang.decode() if isinstance(current_lang, bytes) else str(current_lang)
|
||||
lang_str = lang_str.removeprefix("main_")
|
||||
if lang_str != base_multilang.language and lang_str in base_multilang.languages.values():
|
||||
base_multilang._language = lang_str
|
||||
base_multilang.setup()
|
||||
|
||||
context = cache.get_settings_context()
|
||||
settings_with_values = []
|
||||
|
||||
for section in SETTINGS:
|
||||
if not eval_condition(section.get('condition'), context):
|
||||
continue
|
||||
|
||||
section_copy = section.copy()
|
||||
settings_list = []
|
||||
|
||||
for setting in section.get('settings', []):
|
||||
if not eval_condition(setting.get('condition'), context):
|
||||
continue
|
||||
|
||||
setting_copy = setting.copy()
|
||||
key = setting['key']
|
||||
|
||||
# Resolve callable values
|
||||
for field in ['title', 'description', 'suffix', 'special_value_text']:
|
||||
if field in setting_copy:
|
||||
setting_copy[field] = resolve_value(setting_copy[field])
|
||||
if 'options' in setting_copy:
|
||||
setting_copy['options'] = [resolve_value(opt) for opt in setting_copy['options']]
|
||||
|
||||
# Get current value based on type
|
||||
setting_copy['current_value'] = _get_setting_value(params, setting)
|
||||
settings_list.append(setting_copy)
|
||||
|
||||
if settings_list:
|
||||
section_copy['settings'] = settings_list
|
||||
settings_with_values.append(section_copy)
|
||||
|
||||
return web.json_response({'settings': settings_with_values})
|
||||
|
||||
|
||||
def _get_setting_value(params, setting):
|
||||
"""Get current value for a setting from Params."""
|
||||
key = setting['key']
|
||||
setting_type = setting['type']
|
||||
default = setting.get('default', 0)
|
||||
|
||||
try:
|
||||
if setting_type == 'toggle_item':
|
||||
return params.get_bool(key)
|
||||
elif setting_type == 'double_spin_button_item':
|
||||
value = params.get(key)
|
||||
return float(value) if value is not None else float(default)
|
||||
else: # spin_button_item, text_spin_button_item
|
||||
value = params.get(key)
|
||||
return int(value) if value is not None else int(default)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting value for {key}: {e}")
|
||||
if setting_type == 'toggle_item':
|
||||
return False
|
||||
elif setting_type == 'double_spin_button_item':
|
||||
return float(default)
|
||||
return int(default)
|
||||
|
||||
|
||||
@api_handler
|
||||
async def save_param_api(request):
|
||||
"""Save a single param value.
|
||||
|
||||
Usage: POST /api/settings/params/{name}
|
||||
Body: { "value": <value> }
|
||||
"""
|
||||
param_name = request.match_info.get('param_name')
|
||||
if not param_name:
|
||||
return web.json_response({'error': 'param_name is required'}, status=400)
|
||||
|
||||
cache: AppCache = request.app['cache']
|
||||
params = cache.params
|
||||
data = await request.json()
|
||||
|
||||
if 'value' not in data:
|
||||
return web.json_response({'error': 'value is required in body'}, status=400)
|
||||
|
||||
_save_param(params, param_name, data['value'])
|
||||
logger.info(f"Param saved: {param_name}={data['value']}")
|
||||
|
||||
return web.json_response({'status': 'success', 'key': param_name, 'value': data['value']})
|
||||
|
||||
|
||||
def _save_param(params, key, value):
|
||||
"""Save a single param value with proper type handling."""
|
||||
try:
|
||||
param_type = params.get_type(key)
|
||||
|
||||
if param_type == 1: # BOOL
|
||||
params.put_bool(key, bool(value))
|
||||
elif param_type == 2: # INT
|
||||
params.put(key, int(value))
|
||||
elif param_type == 3: # FLOAT
|
||||
params.put(key, float(value))
|
||||
elif isinstance(value, bool):
|
||||
params.put_bool(key, value)
|
||||
else:
|
||||
params.put(key, str(value) if not isinstance(value, str) else value)
|
||||
|
||||
logger.debug(f"Saved {key}={value} (type={param_type})")
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving param {key}={value}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def _get_param_value(params, key):
|
||||
"""Get a single param value with proper type handling."""
|
||||
try:
|
||||
return params.get_bool(key)
|
||||
except Exception:
|
||||
raw_value = params.get(key)
|
||||
if raw_value is None:
|
||||
return None
|
||||
elif isinstance(raw_value, bytes):
|
||||
return raw_value.decode('utf-8')
|
||||
return raw_value
|
||||
|
||||
|
||||
@api_handler
|
||||
async def get_param_api(request):
|
||||
"""Get a single param value."""
|
||||
param_name = request.match_info.get('param_name')
|
||||
if not param_name:
|
||||
return web.json_response({'error': 'param_name is required'}, status=400)
|
||||
|
||||
cache: AppCache = request.app['cache']
|
||||
params = cache.params
|
||||
value = _get_param_value(params, param_name)
|
||||
|
||||
return web.json_response({'key': param_name, 'value': value})
|
||||
|
||||
|
||||
@api_handler
|
||||
async def get_model_list_api(request):
|
||||
"""Get the model list and current selection."""
|
||||
cache: AppCache = request.app['cache']
|
||||
params = cache.params
|
||||
|
||||
# Get model list
|
||||
model_list = {}
|
||||
try:
|
||||
model_list_raw = params.get("dp_dev_model_list")
|
||||
if model_list_raw:
|
||||
model_list = json.loads(model_list_raw)
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not parse dp_dev_model_list: {e}")
|
||||
|
||||
# Get current selection
|
||||
selected_model = ""
|
||||
try:
|
||||
selected_raw = params.get("dp_dev_model_selected")
|
||||
if selected_raw:
|
||||
selected_model = selected_raw.decode('utf-8') if isinstance(selected_raw, bytes) else str(selected_raw)
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not get dp_dev_model_selected: {e}")
|
||||
|
||||
return web.json_response({
|
||||
'model_list': model_list,
|
||||
'selected_model': selected_model
|
||||
})
|
||||
|
||||
|
||||
@api_handler
|
||||
async def save_model_selection_api(request):
|
||||
"""Save the selected model."""
|
||||
cache: AppCache = request.app['cache']
|
||||
params = cache.params
|
||||
data = await request.json()
|
||||
|
||||
selected_model = data.get('selected_model', '')
|
||||
|
||||
if not selected_model or selected_model == "[AUTO]":
|
||||
params.put("dp_dev_model_selected", "")
|
||||
logger.info("Model selection cleared (AUTO mode)")
|
||||
else:
|
||||
params.put("dp_dev_model_selected", selected_model)
|
||||
logger.info(f"Model selection saved: {selected_model}")
|
||||
|
||||
return web.json_response({'status': 'success'})
|
||||
|
||||
|
||||
@api_handler
|
||||
async def webrtc_stream_proxy(request):
|
||||
"""Proxy WebRTC stream requests to webrtcd."""
|
||||
host = request.host.split(':')[0]
|
||||
body = await request.read()
|
||||
session: ClientSession = request.app['http_session']
|
||||
|
||||
async with session.post(
|
||||
f'http://{host}:5001/stream',
|
||||
data=body,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
) as resp:
|
||||
response_body = await resp.read()
|
||||
return web.Response(
|
||||
body=response_body,
|
||||
status=resp.status,
|
||||
content_type=resp.content_type
|
||||
)
|
||||
|
||||
|
||||
# --- CORS Middleware ---
|
||||
@web.middleware
|
||||
async def cors_middleware(request, handler):
|
||||
response = await handler(request)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
|
||||
# Disable caching for web assets
|
||||
path = request.path.lower()
|
||||
if path.endswith(('.html', '.js', '.css')) or path == '/':
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||
response.headers['Pragma'] = 'no-cache'
|
||||
response.headers['Expires'] = '0'
|
||||
|
||||
return response
|
||||
|
||||
|
||||
async def handle_cors_preflight(request):
|
||||
return web.Response(status=200, headers={
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
})
|
||||
|
||||
|
||||
# --- Application Setup ---
|
||||
async def on_startup(app):
|
||||
"""Initialize app-level resources."""
|
||||
app['cache'] = AppCache()
|
||||
app['http_session'] = ClientSession(timeout=WEBRTC_TIMEOUT)
|
||||
logger.info("Dashy server started")
|
||||
|
||||
|
||||
async def on_cleanup(app):
|
||||
"""Cleanup app-level resources."""
|
||||
await app['http_session'].close()
|
||||
logger.info("Dashy server stopped")
|
||||
|
||||
|
||||
def setup_aiohttp_app(host: str, port: int, debug: bool):
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if debug else logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
app = web.Application(middlewares=[cors_middleware])
|
||||
app['port'] = port
|
||||
|
||||
# API routes
|
||||
app.router.add_get("/api/init", init_api)
|
||||
app.router.add_get("/api/files", list_files_api)
|
||||
app.router.add_get("/api/play", serve_player_api)
|
||||
app.router.add_get("/api/manifest.m3u8", serve_manifest_api)
|
||||
app.router.add_get("/api/settings", get_settings_config_api)
|
||||
app.router.add_get("/api/settings/params/{param_name}", get_param_api)
|
||||
app.router.add_post("/api/settings/params/{param_name}", save_param_api)
|
||||
app.router.add_get("/api/models", get_model_list_api)
|
||||
app.router.add_post("/api/models/select", save_model_selection_api)
|
||||
app.router.add_post("/api/stream", webrtc_stream_proxy)
|
||||
app.router.add_route('OPTIONS', '/{tail:.*}', handle_cors_preflight)
|
||||
|
||||
# Static files
|
||||
app.router.add_static('/media', path=DEFAULT_DIR, name='media', show_index=False, follow_symlinks=False)
|
||||
app.router.add_static('/download', path=DEFAULT_DIR, name='download', show_index=False, follow_symlinks=False)
|
||||
app.router.add_get("/", lambda r: web.FileResponse(os.path.join(WEB_DIST_PATH, "index.html")))
|
||||
app.router.add_static("/", path=WEB_DIST_PATH)
|
||||
|
||||
app.on_startup.append(on_startup)
|
||||
app.on_cleanup.append(on_cleanup)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Dashy Server")
|
||||
parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to listen on")
|
||||
parser.add_argument("--port", type=int, default=5088, help="Port to listen on")
|
||||
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
|
||||
args = parser.parse_args()
|
||||
|
||||
app = setup_aiohttp_app(args.host, args.port, args.debug)
|
||||
web.run_app(app, host=args.host, port=args.port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
dragonpilot/dashy/web/dist/css/styles.css
vendored
Normal file
2
dragonpilot/dashy/web/dist/css/styles.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dragonpilot/dashy/web/dist/icons/dashy.png
vendored
Normal file
BIN
dragonpilot/dashy/web/dist/icons/dashy.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
BIN
dragonpilot/dashy/web/dist/icons/icon-192x192.png
vendored
Normal file
BIN
dragonpilot/dashy/web/dist/icons/icon-192x192.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
90
dragonpilot/dashy/web/dist/index.html
vendored
Normal file
90
dragonpilot/dashy/web/dist/index.html
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<title>Dashy by dragonpilot</title>
|
||||
<link rel="icon" href="/icons/icon-192x192.png">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app-container" class="w-full h-full">
|
||||
|
||||
<!-- HUD Page (full-screen, default when enabled) -->
|
||||
<div id="hud-page" class="hud-page">
|
||||
<div id="hud-page-content" class="relative w-full h-full">
|
||||
<video id="videoPlayer" class="absolute inset-0 w-full h-full object-cover" autoplay playsinline muted></video>
|
||||
<canvas id="uiCanvas" class="absolute inset-0 w-full h-full pointer-events-none z-10"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panel Backdrop -->
|
||||
<div id="panel-backdrop" class="panel-backdrop"></div>
|
||||
|
||||
<!-- Slide-up Panel -->
|
||||
<div id="panel" class="panel">
|
||||
<!-- Panel Header with Tabs -->
|
||||
<div class="panel-header">
|
||||
<div class="panel-handle"></div>
|
||||
<div class="flex items-center justify-between w-full gap-2">
|
||||
<div class="join flex-1 max-w-sm">
|
||||
<button id="panel-tab-controls" class="join-item btn btn-primary btn-sm sm:btn-md flex-1">Controls</button>
|
||||
<button id="panel-tab-settings" class="join-item btn btn-ghost btn-sm sm:btn-md flex-1">Settings</button>
|
||||
<button id="panel-tab-files" class="join-item btn btn-ghost btn-sm sm:btn-md flex-1">Files</button>
|
||||
</div>
|
||||
<button id="panel-close" class="btn btn-circle btn-ghost btn-sm sm:btn-md shrink-0" aria-label="Close panel">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panel Content -->
|
||||
<div class="panel-content">
|
||||
<!-- Controls Tab -->
|
||||
<div id="controls-content" class="panel-page active">
|
||||
<div class="max-w-2xl landscape:max-w-5xl mx-auto space-y-4">
|
||||
<div id="controls-content-inner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
<div id="settings-content" class="panel-page">
|
||||
<div class="max-w-2xl landscape:max-w-5xl mx-auto">
|
||||
<div id="local-settings-content" class="space-y-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Files Tab -->
|
||||
<div id="files-content" class="panel-page">
|
||||
<div id="files-breadcrumbs" class="breadcrumbs text-sm mb-4">
|
||||
<ul></ul>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table id="files-table" class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-12"></th>
|
||||
<th>Name</th>
|
||||
<th>Last Modified</th>
|
||||
<th class="text-right">Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main app -->
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
160
dragonpilot/dashy/web/dist/js/app.js
vendored
Normal file
160
dragonpilot/dashy/web/dist/js/app.js
vendored
Normal file
File diff suppressed because one or more lines are too long
15
dragonpilot/dashy/web/dist/js/library-loader.js
vendored
Normal file
15
dragonpilot/dashy/web/dist/js/library-loader.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
// Dynamic Library Loader
|
||||
window.loadLibrary = function(name) {
|
||||
if (name === 'hls') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = '/lib/' + name + '.min.js';
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.loadHls = function() { return window.loadLibrary('hls'); };
|
||||
24
dragonpilot/dashy/web/dist/js/themes/flight.js
vendored
Normal file
24
dragonpilot/dashy/web/dist/js/themes/flight.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025, Rick Lan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
* for non-commercial purposes only, subject to the following conditions:
|
||||
*
|
||||
* - The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
* - Commercial use (e.g. use in a product, service, or activity intended to
|
||||
* generate revenue) is prohibited without explicit written permission from
|
||||
* the copyright holder.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
var C=Object.defineProperty;var B=(w,p,d)=>p in w?C(w,p,{enumerable:!0,configurable:!0,writable:!0,value:d}):w[p]=d;var _=(w,p,d)=>B(w,typeof p!="symbol"?p+"":p,d);(function(){"use strict";class w extends ModelRenderer{static getTopics(){return["modelV2","liveCalibration","carParams","longitudinalPlan","radarState"]}constructor(){super(),this._tunnelOffset=0}_draw_lane_lines(){}_draw_path(t){const h=this._path;if(!h||!h.raw_points||h.raw_points.length<4)return;const e=this.ctx,i=this._car_space_transform,R=this._path_offset_z,b=6,l=100,r=t.longitudinalPlan&&t.longitudinalPlan.allowThrottle||!this._longitudinal_control?{r:13,g:248,b:122}:{r:242,g:242,b:242};e.lineCap="round",e.lineJoin="round";const a=h.raw_points,f=1.2,v=.8,M=a[a.length-1][0];let g=Math.max(b,Math.min(l,M));const P=t.radarState,T=P?P.leadOne:null;if(T&&T.status){const c=T.dRel*2;g=Math.max(0,Math.min(c-Math.min(c*.35,10),g))}const S=10,k=b;if(g<=k)return;const F=(g-k)/S,W=t.carState?t.carState.vEgo:0;for(this._tunnelOffset+=W*.015;this._tunnelOffset>=F;)this._tunnelOffset-=F;const $=[];for(let c=0;c<S;c++){let o=k+c*F-this._tunnelOffset;o<k-1&&(o+=F*S);let m=null;for(let s=0;s<a.length-1;s++)if(a[s][0]<=o&&a[s+1][0]>=o){const D=(o-a[s][0])/(a[s+1][0]-a[s][0]);m=[o,a[s][1]+D*(a[s+1][1]-a[s][1]),a[s][2]+D*(a[s+1][2]-a[s][2])];break}if(!m)continue;const L=m[2]+R,u=L-v*3,A=[[o,m[1]-f,L],[o,m[1]+f,L],[o,m[1]-f,u],[o,m[1]+f,u]].map(s=>{const D=i[0][0]*s[0]+i[0][1]*s[1]+i[0][2]*s[2],O=i[1][0]*s[0]+i[1][1]*s[1]+i[1][2]*s[2],E=i[2][0]*s[0]+i[2][1]*s[1]+i[2][2]*s[2];return Math.abs(E)<1e-6?null:[D/E,O/E]});if(A.some(s=>!s))continue;const y=Math.max(0,1-(o-k)/(g-k));$.push({bottomLeft:A[0],bottomRight:A[1],topLeft:A[2],topRight:A[3],distFactor:y})}for(let c=$.length-1;c>=0;c--){const o=$[c],m=.2+o.distFactor*.5,L=1.5+o.distFactor*2;if(e.strokeStyle=`rgba(${r.r}, ${r.g}, ${r.b}, ${m})`,e.lineWidth=L,e.beginPath(),e.moveTo(o.bottomLeft[0],o.bottomLeft[1]),e.lineTo(o.bottomRight[0],o.bottomRight[1]),e.lineTo(o.topRight[0],o.topRight[1]),e.lineTo(o.topLeft[0],o.topLeft[1]),e.closePath(),e.stroke(),c<$.length-1){const u=$[c+1],I=m*.4;e.strokeStyle=`rgba(${r.r}, ${r.g}, ${r.b}, ${I})`,e.lineWidth=L*.5,e.beginPath(),e.moveTo(o.bottomLeft[0],o.bottomLeft[1]),e.lineTo(u.bottomLeft[0],u.bottomLeft[1]),e.stroke(),e.beginPath(),e.moveTo(o.bottomRight[0],o.bottomRight[1]),e.lineTo(u.bottomRight[0],u.bottomRight[1]),e.stroke(),e.beginPath(),e.moveTo(o.topLeft[0],o.topLeft[1]),e.lineTo(u.topLeft[0],u.topLeft[1]),e.stroke(),e.beginPath(),e.moveTo(o.topRight[0],o.topRight[1]),e.lineTo(u.topRight[0],u.topRight[1]),e.stroke()}}}_draw_lead_indicator(){const t=this.ctx,h=Date.now(),e=this._sm&&this._sm.radarState;this._lead_vehicles.forEach((i,R)=>{if(!i.chevron||i.chevron.length<3)return;const b=i.chevron[1][0],l=Math.abs(i.chevron[0][0]-i.chevron[2][0]),n=Math.abs(i.chevron[0][1]-i.chevron[1][1]),r=Math.max(l,n)*.8,a=i.chevron[1][1]-r*.6,f=e?R===0?e.leadOne:e.leadTwo:null,v=f?f.vRel:0,M=f?f.dRel:100;let g;v<-5?g="#ff3333":v<-2?g="#ffaa00":g="#00ff88";const P=1e3+M/100*3e3,T=.7+.3*Math.sin(h/P*Math.PI*2);this._drawTargetBrackets(t,b,a,r,g,T),this._drawTargetInfo(t,b,a+r*.7,M,v,g)})}_drawTargetInfo(t,h,e,i,R,b){var T,S;t.save(),t.font="bold 16px Arial",t.textAlign="center",t.textBaseline="top";const l=SmUtils.isMetric(),n=l?`${i.toFixed(1)}m`:`${(i*3.28084).toFixed(1)}ft`,a=(((S=(T=this._sm)==null?void 0:T.carState)==null?void 0:S.vEgo)||0)+R,f=Math.max(0,l?a*3.6:a*2.237),v=`${Math.round(f)}`,M=120,g=22,P=6;t.fillStyle="rgba(0, 0, 0, 0.5)",t.beginPath(),t.roundRect(h-M/2,e-2,M,g,4),t.fill(),t.fillStyle=b,t.fillText(`${n} ${v}`,h,e),t.restore()}_drawTargetBrackets(t,h,e,i,R,b){const l=i*.35,n=i*.5;t.strokeStyle=R,t.lineWidth=3,t.lineCap="square",t.save(),t.translate(h,e),t.beginPath(),t.moveTo(-n,-n+l),t.lineTo(-n,-n),t.lineTo(-n+l,-n),t.stroke(),t.beginPath(),t.moveTo(n-l,-n),t.lineTo(n,-n),t.lineTo(n,-n+l),t.stroke(),t.beginPath(),t.moveTo(n,n-l),t.lineTo(n,n),t.lineTo(n-l,n),t.stroke(),t.beginPath(),t.moveTo(-n+l,n),t.lineTo(-n,n),t.lineTo(-n,n-l),t.stroke();const r=i*.12;t.lineWidth=2,t.globalAlpha=b,t.beginPath(),t.moveTo(-r,0),t.lineTo(r,0),t.stroke(),t.beginPath(),t.moveTo(0,-r),t.lineTo(0,r),t.stroke(),t.globalAlpha=1,t.restore()}}window.FlightModelRenderer=w;class p extends HudRenderer{render(t,h,e){return!t||t.width<=0||t.height<=0||(window.EdgeIndicators&&EdgeIndicators.draw(e,t,h),window.FlightHud&&FlightHud.draw(e,t,h)),!1}}window.FlightHudRenderer=p;class d extends BaseTheme{}_(d,"layout",Layouts.fullResponsive),_(d,"requiresVideo",!0),_(d,"modules",["FlightHud","NavMap","EdgeIndicators"]),_(d,"layers",[]),_(d,"minimapConfig",{useGrid:!1,options:{responsiveThird:!0,zoom:16,interactive:!0,scale:1.5}}),_(d,"modelRenderer","FlightModelRenderer"),_(d,"hudRenderer","FlightHudRenderer"),window.FlightPanel=d})();
|
||||
24
dragonpilot/dashy/web/dist/js/themes/nav_free.js
vendored
Normal file
24
dragonpilot/dashy/web/dist/js/themes/nav_free.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025, Rick Lan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
* for non-commercial purposes only, subject to the following conditions:
|
||||
*
|
||||
* - The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
* - Commercial use (e.g. use in a product, service, or activity intended to
|
||||
* generate revenue) is prohibited without explicit written permission from
|
||||
* the copyright holder.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
var v=Object.defineProperty;var w=(t,i,e)=>i in t?v(t,i,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[i]=e;var l=(t,i,e)=>w(t,typeof i!="symbol"?i+"":i,e);var u=(t,i,e)=>new Promise((a,o)=>{var s=d=>{try{r(e.next(d))}catch(c){o(c)}},n=d=>{try{r(e.throw(d))}catch(c){o(c)}},r=d=>d.done?a(d.value):Promise.resolve(d.value).then(s,n);r((e=e.apply(t,i)).next())});(function(){"use strict";class t extends BaseTheme{constructor(){super(),this._mapReady=!1}init(e,a){return u(this,null,function*(){this._canvas=e,this._ctx=a,this._enabled=!0;const o=document.getElementById("videoPlayer");if(o&&(o.style.display="none"),window.NavMap&&NavMap.destroy(),window.NavigationFree&&NavigationFree.init(),window.NavMap){yield NavMap.init();const n=document.getElementById("hud-page-content");n&&(NavMap.show(n,{fullscreen:!0,scale:1.5,interactive:!0,enableRouting:!0,autoTileCache:!0,followResumeDelay:3e3}),this._mapReady=!0)}const s=document.getElementById("hud-page-content");return s&&window.NavSearch&&NavSearch.show(s),!0})}update(e){var s;const a=SmUtils.gps(e),o=SmUtils.speedKmh(e);if(this._mapReady&&window.NavMap&&a.lat!==0&&NavMap.setPosition(a.lat,a.lon,a.heading,o),window.NavSearch&&NavSearch.updatePosition(a.lat,a.lon),(s=window.NavigationFree)!=null&&s.isNavigating()){NavigationFree.updatePosition(a.lat,a.lon,a.heading);const n=NavigationFree.getRoute();n&&window.NavMap&&NavMap.setRoute(n)}}render(e,a,o){if(!this._enabled)return!1;e.clearRect(0,0,a,o);const s=window.sm||{};for(const n of this.constructor.layers){const r=window[n.module];if(r!=null&&r.draw){const d=this._layout.getRect(n.region||"full",a,o);r.draw(e,d,s)}}return!1}destroy(){this._enabled=!1,window.NavMap&&NavMap.destroy(),window.NavigationFree&&NavigationFree.clearRoute(),window.NavSearch&&NavSearch.hide();const e=document.getElementById("videoPlayer");e&&(e.style.display=""),this._mapReady=!1}}l(t,"layout",Layouts.full),l(t,"requiresVideo",!1),l(t,"modules",["NavSidebar","NavHud","NavMap","EdgeIndicators"]),l(t,"layers",[{module:"NavSidebar",region:"full"},{module:"NavHud",region:"full"},{module:"EdgeIndicators",region:"full"}]),window.NavFreeTheme=t})();
|
||||
57
dragonpilot/dashy/web/dist/js/themes/op_split.js
vendored
Normal file
57
dragonpilot/dashy/web/dist/js/themes/op_split.js
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2025, Rick Lan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
* for non-commercial purposes only, subject to the following conditions:
|
||||
*
|
||||
* - The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
* - Commercial use (e.g. use in a product, service, or activity intended to
|
||||
* generate revenue) is prohibited without explicit written permission from
|
||||
* the copyright holder.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
var c=Object.defineProperty;var _=(s,r,t)=>r in s?c(s,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[r]=t;var l=(s,r,t)=>_(s,typeof r!="symbol"?r+"":r,t);var p=(s,r,t)=>new Promise((e,n)=>{var d=a=>{try{i(t.next(a))}catch(h){n(h)}},o=a=>{try{i(t.throw(a))}catch(h){n(h)}},i=a=>a.done?e(a.value):Promise.resolve(a.value).then(d,o);i((t=t.apply(s,r)).next())});(function(){"use strict";class s extends BaseTheme{constructor(){super(),this._splitContainer=null,this._mapContainer=null,this._videoContainer=null,this._videoCanvas=null,this._videoCtx=null,this._opModel=null,this._minimapReady=!1,this._isPortrait=!1,this._resizeHandler=null}init(t,e){return p(this,null,function*(){return this._canvas=t,this._ctx=e,this._enabled=!0,window.Minimap&&Minimap.destroy(),window.NavMap&&NavMap.destroy(),this._createSplitLayout(),yield this._initMap(),this._resizeHandler=()=>this._handleResize(),window.addEventListener("resize",this._resizeHandler),!0})}_handleResize(){const t=window.innerWidth,e=window.innerHeight,n=this._isPortrait;this._isPortrait=this._layout.isPortrait(t,e),n!==this._isPortrait&&this._rebuildLayout()}_rebuildLayout(){const t=this._minimapReady;this._minimapReady&&window.NavMap&&NavMap.destroy(),this._minimapReady=!1;const e=document.getElementById("videoPlayer"),n=document.getElementById("hud-page-content");e&&this._videoContainer&&n&&(e.style.cssText="",n.insertBefore(e,this._videoContainer),this._videoContainer.remove()),this._splitContainer&&this._splitContainer.remove(),this._createSplitLayout(),t&&this._initMap()}_createSplitLayout(){const t=document.getElementById("hud-page-content");if(!t)return;const e=t.offsetWidth||window.innerWidth,n=t.offsetHeight||window.innerHeight;this._isPortrait=this._layout.isPortrait(e,n);const d=this._layout.getRegionRect("primary",e,n),o=this._layout.getRegionRect("secondary",e,n),i=60;this._splitContainer=document.createElement("div"),this._splitContainer.id="op-split-container",this._splitContainer.style.cssText=`
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
display: flex; pointer-events: none; z-index: 1;
|
||||
flex-direction: ${this._isPortrait?"column-reverse":"row-reverse"};
|
||||
`,this._mapContainer=document.createElement("div"),this._mapContainer.id="op-split-map",this._isPortrait?this._mapContainer.style.cssText=`
|
||||
width: ${d.width}px; height: ${d.height}px;
|
||||
position: relative; pointer-events: auto;
|
||||
-webkit-mask-image: linear-gradient(to top, black 0%, black calc(100% - ${i}px), transparent 100%);
|
||||
mask-image: linear-gradient(to top, black 0%, black calc(100% - ${i}px), transparent 100%);
|
||||
`:this._mapContainer.style.cssText=`
|
||||
width: ${d.width}px; height: ${d.height}px;
|
||||
position: relative; pointer-events: auto;
|
||||
-webkit-mask-image: linear-gradient(to left, black 0%, black calc(100% - ${i}px), transparent 100%);
|
||||
mask-image: linear-gradient(to left, black 0%, black calc(100% - ${i}px), transparent 100%);
|
||||
`,this._splitContainer.appendChild(this._mapContainer),t.appendChild(this._splitContainer);const a=document.getElementById("videoPlayer");a&&(this._videoContainer=document.createElement("div"),this._videoContainer.id="op-split-video",this._isPortrait?this._videoContainer.style.cssText=`
|
||||
position: absolute; left: ${o.x}px; top: 0;
|
||||
width: ${o.width}px; height: ${o.height+i}px;
|
||||
overflow: hidden;
|
||||
-webkit-mask-image: linear-gradient(to top, transparent 0%, black ${i}px, black 100%);
|
||||
mask-image: linear-gradient(to top, transparent 0%, black ${i}px, black 100%);
|
||||
`:this._videoContainer.style.cssText=`
|
||||
position: absolute; left: 0; top: ${o.y}px;
|
||||
width: ${o.width+i}px; height: ${o.height}px;
|
||||
overflow: hidden;
|
||||
-webkit-mask-image: linear-gradient(to left, transparent 0%, black ${i}px, black 100%);
|
||||
mask-image: linear-gradient(to left, transparent 0%, black ${i}px, black 100%);
|
||||
`,a.parentNode.insertBefore(this._videoContainer,a),this._videoContainer.appendChild(a),a.style.cssText=`
|
||||
position: absolute; top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
object-fit: cover;
|
||||
`,this._videoCanvas=document.createElement("canvas"),this._videoCanvas.width=o.width,this._videoCanvas.height=o.height,this._videoCanvas.style.cssText=`
|
||||
position: absolute; top: 0; left: 0;
|
||||
width: 100%; height: 100%; pointer-events: none;
|
||||
`,this._videoContainer.appendChild(this._videoCanvas),this._videoCtx=this._videoCanvas.getContext("2d"),window.OpModel&&(this._opModel=OpModel.create()))}_initMap(){return p(this,null,function*(){!this._mapContainer||!window.NavMap||(NavMap.destroy(),yield NavMap.init(),NavMap.show(this._mapContainer,{fullscreen:!0,scale:1.5,zoom:16,interactive:!0}),this._minimapReady=!0)})}update(t){if(this._minimapReady&&window.NavMap&&NavMap.isVisible()){const e=SmUtils.gps(t);e.lat!==0&&NavMap.setPosition(e.lat,e.lon,e.heading,SmUtils.speedKmh(t))}}render(t,e,n){if(!this._enabled)return!1;const d=window.sm||{};this._opModel&&this._videoCtx&&(this._videoCtx.clearRect(0,0,this._videoCanvas.width,this._videoCanvas.height),this._opModel.draw(this._videoCtx,{x:0,y:0,width:this._videoCanvas.width,height:this._videoCanvas.height},d));for(const o of this.constructor.layers){const i=window[o.module];if(!(i!=null&&i.draw))continue;let a;o.region==="hud"?this._isPortrait?a={x:0,y:0,width:e,height:n*.5}:a={x:0,y:0,width:e*.5,height:n}:a={x:0,y:0,width:e,height:n},i.draw(t,a,d)}return!1}destroy(){this._enabled=!1,this._resizeHandler&&(window.removeEventListener("resize",this._resizeHandler),this._resizeHandler=null),this._minimapReady&&window.NavMap&&NavMap.destroy(),this._opModel&&this._opModel.destroy();const t=document.getElementById("videoPlayer"),e=document.getElementById("hud-page-content");t&&this._videoContainer&&e&&(t.style.cssText="",t.className="absolute inset-0 w-full h-full object-cover",e.insertBefore(t,this._videoContainer),this._videoContainer.remove()),this._splitContainer&&this._splitContainer.remove(),this._splitContainer=null,this._mapContainer=null,this._videoContainer=null,this._videoCanvas=null,this._videoCtx=null,this._opModel=null,this._minimapReady=!1,this._isPortrait=!1}}l(s,"layout",Layouts.splitResponsive),l(s,"requiresVideo",!0),l(s,"handlesOwnRendering",!0),l(s,"modules",["NavSidebar","NavMap","OpModel","OpBorder","OpAlerts","EdgeIndicators"]),l(s,"layers",[{module:"NavSidebar",region:"hud"},{module:"OpBorder",region:"full"},{module:"EdgeIndicators",region:"full"},{module:"OpAlerts",region:"full"}]),window.OpSplitTheme=s})();
|
||||
24
dragonpilot/dashy/web/dist/js/themes/openpilot.js
vendored
Normal file
24
dragonpilot/dashy/web/dist/js/themes/openpilot.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025, Rick Lan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
* for non-commercial purposes only, subject to the following conditions:
|
||||
*
|
||||
* - The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
* - Commercial use (e.g. use in a product, service, or activity intended to
|
||||
* generate revenue) is prohibited without explicit written permission from
|
||||
* the copyright holder.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
var n=Object.defineProperty;var u=(r,e,d)=>e in r?n(r,e,{enumerable:!0,configurable:!0,writable:!0,value:d}):r[e]=d;var s=(r,e,d)=>u(r,typeof e!="symbol"?e+"":e,d);(function(){"use strict";class r extends HudRenderer{render(t,i,a){return super.render(t,i,a),window.EdgeIndicators&&EdgeIndicators.draw(a,t,i),!1}}window.OpenpilotHudRenderer=r;class e extends BaseTheme{}s(e,"layout",Layouts.fullResponsive),s(e,"requiresVideo",!0),s(e,"modules",["OpHud","OpBorder","OpAlerts","NavMap","EdgeIndicators"]),s(e,"layers",[]),s(e,"minimapConfig",{useGrid:!1,options:{responsiveThird:!0,zoom:16,interactive:!0,scale:1.5}}),s(e,"hudRenderer","OpenpilotHudRenderer"),window.OpenpilotTheme=e})();
|
||||
2
dragonpilot/dashy/web/dist/lib/hls.min.js
vendored
Normal file
2
dragonpilot/dashy/web/dist/lib/hls.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
25
dragonpilot/dashy/web/dist/pages/player.html
vendored
Normal file
25
dragonpilot/dashy/web/dist/pages/player.html
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>HLS Player</title>
|
||||
<style>
|
||||
body, html { margin: 0; padding: 0; height: 100%; background-color: #000; }
|
||||
#video { width: 100%; height: 100%; }
|
||||
</style>
|
||||
<script src="/lib/hls.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video id="video" controls autoplay></video>
|
||||
<script>
|
||||
var v = document.getElementById('video');
|
||||
var s = '/api/manifest.m3u8?file={{FILE_PATH}}';
|
||||
if (Hls.isSupported()) {
|
||||
var h = new Hls();
|
||||
h.loadSource(s);
|
||||
h.attachMedia(v);
|
||||
} else if (v.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
v.src = s;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
101
dragonpilot/dashy/web/dist/sw-tiles.js
vendored
Normal file
101
dragonpilot/dashy/web/dist/sw-tiles.js
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Tile Cache Service Worker
|
||||
* Caches map tiles from OpenFreeMap for offline use
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'dashy-map-tiles-v1';
|
||||
const TILE_HOSTS = ['tiles.openfreemap.org'];
|
||||
const MAX_CACHE_SIZE = 2000; // Max tiles to cache
|
||||
const TRIM_INTERVAL = 60000; // Only trim cache every 60 seconds
|
||||
|
||||
// Debug mode - can be set via message from main thread
|
||||
let _debug = false;
|
||||
let _lastTrimTime = 0;
|
||||
|
||||
function debugLog(...args) {
|
||||
if (_debug) console.log(...args);
|
||||
}
|
||||
|
||||
// Listen for debug toggle from main thread
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SET_DEBUG') {
|
||||
_debug = event.data.value;
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName.startsWith('dashy-map-tiles-') && cacheName !== CACHE_NAME) {
|
||||
debugLog('[TileCache SW] Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// Only cache tile requests from OpenFreeMap
|
||||
const isTileRequest = TILE_HOSTS.some(host => url.hostname.includes(host));
|
||||
if (!isTileRequest) return;
|
||||
|
||||
event.respondWith(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.match(event.request).then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
// Return cached, but also update cache in background
|
||||
fetchAndCache(event.request, cache);
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
return fetchAndCache(event.request, cache);
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
async function fetchAndCache(request, cache) {
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
if (networkResponse.ok) {
|
||||
cache.put(request, networkResponse.clone());
|
||||
trimCache(cache);
|
||||
}
|
||||
return networkResponse;
|
||||
} catch (e) {
|
||||
// Return cached version if offline
|
||||
const cached = await cache.match(request);
|
||||
if (cached) return cached;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function trimCache(cache) {
|
||||
// Only trim every TRIM_INTERVAL to avoid constant overhead
|
||||
const now = Date.now();
|
||||
if (now - _lastTrimTime < TRIM_INTERVAL) {
|
||||
return;
|
||||
}
|
||||
_lastTrimTime = now;
|
||||
|
||||
const keys = await cache.keys();
|
||||
if (keys.length > MAX_CACHE_SIZE) {
|
||||
// Delete oldest entries
|
||||
const toDelete = keys.slice(0, keys.length - MAX_CACHE_SIZE);
|
||||
for (const key of toDelete) {
|
||||
await cache.delete(key);
|
||||
}
|
||||
debugLog('[TileCache SW] Trimmed', toDelete.length, 'old tiles');
|
||||
}
|
||||
}
|
||||
161
dragonpilot/selfdrive/controls/lib/acm.py
Normal file
161
dragonpilot/selfdrive/controls/lib/acm.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Copyright (c) 2025, Rick Lan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import time
|
||||
import numpy as np
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
# Configuration parameters
|
||||
SPEED_RATIO = 0.98 # Must be within 2% over cruise speed
|
||||
TTC_THRESHOLD = 3.0 # seconds - disable ACM when lead is within this time
|
||||
|
||||
# Emergency thresholds - IMMEDIATELY disable ACM
|
||||
EMERGENCY_TTC = 2.0 # seconds - emergency situation
|
||||
EMERGENCY_RELATIVE_SPEED = 10.0 # m/s (~36 km/h closing speed - only for rapid closing)
|
||||
EMERGENCY_DECEL_THRESHOLD = -1.5 # m/s² - if MPC wants this much braking, emergency disable
|
||||
|
||||
# Safety cooldown after lead detection
|
||||
LEAD_COOLDOWN_TIME = 0.5 # seconds - brief cooldown to handle sensor glitches
|
||||
|
||||
# Speed-based distance scaling - more practical for real traffic
|
||||
SPEED_BP = [0., 10., 20., 30.] # m/s (0, 36, 72, 108 km/h)
|
||||
MIN_DIST_V = [15., 20., 25., 30.] # meters - closer to original 25m baseline
|
||||
|
||||
|
||||
class ACM:
|
||||
def __init__(self):
|
||||
self.enabled = False
|
||||
self._is_speed_over_cruise = False
|
||||
self._has_lead = False
|
||||
self._active_prev = False
|
||||
self._last_lead_time = 0.0 # Track when we last saw a lead
|
||||
|
||||
self.active = False
|
||||
self.just_disabled = False
|
||||
|
||||
def _check_emergency_conditions(self, lead, v_ego, current_time):
|
||||
"""Check for emergency conditions that require immediate ACM disable."""
|
||||
if not lead or not lead.status:
|
||||
return False
|
||||
|
||||
self.lead_ttc = lead.dRel / max(v_ego, 0.1)
|
||||
relative_speed = v_ego - lead.vLead # Positive = closing
|
||||
|
||||
# Speed-adaptive minimum distance
|
||||
min_dist_for_speed = np.interp(v_ego, SPEED_BP, MIN_DIST_V)
|
||||
|
||||
# Emergency disable conditions - only for truly dangerous situations
|
||||
# Require BOTH close distance AND (fast closing OR very short TTC)
|
||||
if lead.dRel < min_dist_for_speed and (
|
||||
self.lead_ttc < EMERGENCY_TTC or
|
||||
relative_speed > EMERGENCY_RELATIVE_SPEED):
|
||||
|
||||
self._last_lead_time = current_time
|
||||
if self.active: # Only log if we're actually disabling
|
||||
cloudlog.warning(f"ACM emergency disable: dRel={lead.dRel:.1f}m, TTC={self.lead_ttc:.1f}s, relSpeed={relative_speed:.1f}m/s")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _update_lead_status(self, lead, v_ego, current_time):
|
||||
"""Update lead vehicle detection status."""
|
||||
if lead and lead.status:
|
||||
self.lead_ttc = lead.dRel / max(v_ego, 0.1)
|
||||
|
||||
if self.lead_ttc < TTC_THRESHOLD:
|
||||
self._has_lead = True
|
||||
self._last_lead_time = current_time
|
||||
else:
|
||||
self._has_lead = False
|
||||
else:
|
||||
self._has_lead = False
|
||||
self.lead_ttc = float('inf')
|
||||
|
||||
def _check_cooldown(self, current_time):
|
||||
"""Check if we're still in cooldown period after lead detection."""
|
||||
time_since_lead = current_time - self._last_lead_time
|
||||
return time_since_lead < LEAD_COOLDOWN_TIME
|
||||
|
||||
def _should_activate(self, user_ctrl_lon, v_ego, v_cruise, in_cooldown):
|
||||
"""Determine if ACM should be active based on all conditions."""
|
||||
self._is_speed_over_cruise = v_ego > (v_cruise * SPEED_RATIO)
|
||||
|
||||
return (not user_ctrl_lon and
|
||||
not self._has_lead and
|
||||
not in_cooldown and
|
||||
self._is_speed_over_cruise)
|
||||
|
||||
def update_states(self, cc, rs, user_ctrl_lon, v_ego, v_cruise):
|
||||
"""Update ACM state with multiple safety checks."""
|
||||
# Basic validation
|
||||
if not self.enabled or len(cc.orientationNED) != 3:
|
||||
self.active = False
|
||||
return
|
||||
|
||||
current_time = time.monotonic()
|
||||
lead = rs.leadOne
|
||||
|
||||
# Check emergency conditions first (highest priority)
|
||||
if self._check_emergency_conditions(lead, v_ego, current_time):
|
||||
self.active = False
|
||||
self._active_prev = self.active
|
||||
return
|
||||
|
||||
# Update normal lead status
|
||||
self._update_lead_status(lead, v_ego, current_time)
|
||||
|
||||
# Check cooldown period
|
||||
in_cooldown = self._check_cooldown(current_time)
|
||||
|
||||
# Determine if ACM should be active
|
||||
self.active = self._should_activate(user_ctrl_lon, v_ego, v_cruise, in_cooldown)
|
||||
|
||||
# Track state changes for logging
|
||||
self.just_disabled = self._active_prev and not self.active
|
||||
if self.active and not self._active_prev:
|
||||
cloudlog.info(f"ACM activated: v_ego={v_ego*3.6:.1f} km/h, v_cruise={v_cruise*3.6:.1f} km/h")
|
||||
elif self.just_disabled:
|
||||
cloudlog.info("ACM deactivated")
|
||||
|
||||
self._active_prev = self.active
|
||||
|
||||
def update_a_desired_trajectory(self, a_desired_trajectory):
|
||||
"""
|
||||
Modify acceleration trajectory to allow coasting.
|
||||
SAFETY: Check for any strong braking request and abort.
|
||||
"""
|
||||
if not self.active:
|
||||
return a_desired_trajectory
|
||||
|
||||
# SAFETY CHECK: If MPC wants significant braking, DON'T suppress it
|
||||
min_accel = np.min(a_desired_trajectory)
|
||||
if min_accel < EMERGENCY_DECEL_THRESHOLD:
|
||||
cloudlog.warning(f"ACM aborting: MPC requested {min_accel:.2f} m/s² braking")
|
||||
self.active = False # Immediately deactivate
|
||||
return a_desired_trajectory # Return unmodified trajectory
|
||||
|
||||
# Only suppress very mild braking (> -1.0 m/s²)
|
||||
# This allows coasting but preserves any meaningful braking
|
||||
modified_trajectory = np.copy(a_desired_trajectory)
|
||||
for i in range(len(modified_trajectory)):
|
||||
if -1.0 < modified_trajectory[i] < 0:
|
||||
# Only suppress very gentle braking for cruise control
|
||||
modified_trajectory[i] = 0.0
|
||||
# Any braking stronger than -1.0 m/s² is preserved!
|
||||
|
||||
return modified_trajectory
|
||||
70
dragonpilot/selfdrive/controls/lib/aem.py
Normal file
70
dragonpilot/selfdrive/controls/lib/aem.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Copyright (c) 2025, Rick Lan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import time
|
||||
import numpy as np
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
|
||||
|
||||
AEM_COOLDOWN_TIME = 0.5 # seconds
|
||||
|
||||
SLOW_DOWN_BP = [0., 2.78, 5.56, 8.34, 11.12, 13.89, 15.28]
|
||||
SLOW_DOWN_DIST = [10, 30., 50., 70., 80., 90., 120.]
|
||||
|
||||
# Allow throttle
|
||||
# ALLOW_THROTTLE_THRESHOLD = 0.4
|
||||
# MIN_ALLOW_THROTTLE_SPEED = 2.5
|
||||
|
||||
|
||||
class AEM:
|
||||
|
||||
def __init__(self):
|
||||
self._active = False
|
||||
self._cooldown_end_time = 0.0
|
||||
|
||||
def _perform_experimental_mode(self):
|
||||
self._active = True
|
||||
self._cooldown_end_time = time.monotonic() + AEM_COOLDOWN_TIME
|
||||
|
||||
def get_mode(self, mode):
|
||||
# override mode
|
||||
if time.monotonic() < self._cooldown_end_time:
|
||||
mode = 'blended'
|
||||
else:
|
||||
self._active = False
|
||||
return mode
|
||||
|
||||
def update_states(self, model_msg, radar_msg, v_ego):
|
||||
|
||||
# Stop sign/light detection
|
||||
if not self._active:
|
||||
if len(model_msg.orientation.x) == len(model_msg.position.x) == ModelConstants.IDX_N and \
|
||||
model_msg.position.x[ModelConstants.IDX_N - 1] < np.interp(v_ego, SLOW_DOWN_BP, SLOW_DOWN_DIST):
|
||||
self._perform_experimental_mode()
|
||||
|
||||
# throttle prob is low and there is no lead ahead (prep for brake?)
|
||||
# if not self._active:
|
||||
# if len(model_msg.meta.disengagePredictions.gasPressProbs) > 1:
|
||||
# throttle_prob = model_msg.meta.disengagePredictions.gasPressProbs[1]
|
||||
# else:
|
||||
# throttle_prob = 1.0
|
||||
|
||||
# allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED
|
||||
# if not allow_throttle and not radar_msg.leadOne.status:
|
||||
# self._perform_experimental_mode()
|
||||
|
||||
145
dragonpilot/selfdrive/controls/lib/dtsc.py
Normal file
145
dragonpilot/selfdrive/controls/lib/dtsc.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Copyright (c) 2025, Rick Lan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
# Physics constants
|
||||
COMFORT_LAT_G = 0.2 # g units - universal human comfort threshold
|
||||
BASE_LAT_ACC = COMFORT_LAT_G * 9.81 # ~2.0 m/s^2
|
||||
SAFETY_FACTOR = 0.9 # 10% safety margin on calculated speeds
|
||||
MIN_CURVE_DISTANCE = 5.0 # meters - minimum distance to react to curves
|
||||
MAX_DECEL = -2.0 # m/s^2 - maximum comfortable deceleration
|
||||
|
||||
|
||||
class DTSC:
|
||||
"""
|
||||
Dynamic Turn Speed Controller - Predictive curve speed management via MPC constraints.
|
||||
|
||||
Core physics: v_max = sqrt(lateral_acceleration / curvature) * safety_factor
|
||||
|
||||
Operation:
|
||||
1. Scans predicted path for curvature (up to ~10 seconds ahead)
|
||||
2. Calculates safe speed for each point using physics + comfort limits
|
||||
3. Identifies critical points where current speed would exceed safe speed
|
||||
4. Calculates required deceleration to reach safe speed at critical point
|
||||
5. Provides deceleration as MPC constraint for smooth trajectory planning
|
||||
"""
|
||||
|
||||
def __init__(self, aggressiveness=1.0):
|
||||
"""
|
||||
Initialize DTSC with user-adjustable aggressiveness.
|
||||
|
||||
Args:
|
||||
aggressiveness: Factor to adjust lateral acceleration limit
|
||||
0.7 = 30% more conservative (slower in curves)
|
||||
1.0 = default balanced behavior
|
||||
1.3 = 30% more aggressive (faster in curves)
|
||||
"""
|
||||
self.aggressiveness = np.clip(aggressiveness, 0.5, 1.5)
|
||||
self.active = False
|
||||
self.debug_msg = ""
|
||||
cloudlog.info(f"DTSC: Initialized with aggressiveness {self.aggressiveness:.2f}")
|
||||
|
||||
def set_aggressiveness(self, value):
|
||||
"""Update aggressiveness factor (0.5 to 1.5)."""
|
||||
self.aggressiveness = np.clip(value, 0.5, 1.5)
|
||||
cloudlog.info(f"DTSC: Aggressiveness updated to {self.aggressiveness:.2f}")
|
||||
|
||||
def get_mpc_constraints(self, model_msg, v_ego, base_a_min, base_a_max):
|
||||
"""
|
||||
Calculate MPC acceleration constraints based on predicted path curvature.
|
||||
|
||||
Args:
|
||||
model_msg: ModelDataV2 containing predicted path
|
||||
v_ego: Current vehicle speed (m/s)
|
||||
base_a_min: Default minimum acceleration constraint
|
||||
base_a_max: Default maximum acceleration constraint
|
||||
|
||||
Returns:
|
||||
(a_min_array, a_max_array): Modified constraints for each MPC timestep
|
||||
"""
|
||||
|
||||
# Initialize with base constraints
|
||||
a_min = np.ones(len(T_IDXS_MPC)) * base_a_min
|
||||
a_max = np.ones(len(T_IDXS_MPC)) * base_a_max
|
||||
|
||||
# Validate model data
|
||||
if not self._is_model_data_valid(model_msg):
|
||||
self.active = False
|
||||
return a_min, a_max
|
||||
|
||||
# Extract predictions for MPC horizon
|
||||
v_pred = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.velocity.x)
|
||||
turn_rates = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.orientationRate.z)
|
||||
positions = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.position.x)
|
||||
|
||||
# Calculate curvature (turn_rate / velocity)
|
||||
curvatures = np.abs(turn_rates / np.clip(v_pred, 1.0, 100.0))
|
||||
|
||||
# Calculate safe speeds
|
||||
lat_acc_limit = BASE_LAT_ACC * self.aggressiveness
|
||||
safe_speeds = np.sqrt(lat_acc_limit / (curvatures + 1e-6)) * SAFETY_FACTOR
|
||||
|
||||
# Find speed violations
|
||||
speed_excess = v_pred - safe_speeds
|
||||
if np.all(speed_excess <= 0):
|
||||
self._deactivate()
|
||||
return a_min, a_max
|
||||
|
||||
# Find critical point (maximum speed excess)
|
||||
critical_idx = np.argmax(speed_excess)
|
||||
critical_distance = positions[critical_idx]
|
||||
critical_safe_speed = safe_speeds[critical_idx]
|
||||
|
||||
# Only act if we have sufficient distance
|
||||
if critical_distance <= MIN_CURVE_DISTANCE:
|
||||
self._deactivate()
|
||||
return a_min, a_max
|
||||
|
||||
# Calculate required deceleration: a = (v_f^2 - v_i^2) / (2*d)
|
||||
required_decel = (critical_safe_speed**2 - v_ego**2) / (2 * critical_distance)
|
||||
required_decel = max(required_decel, MAX_DECEL)
|
||||
|
||||
# Apply constraint progressively until critical point
|
||||
for i in range(len(T_IDXS_MPC)):
|
||||
t = T_IDXS_MPC[i]
|
||||
distance_at_t = v_ego * t + 0.5 * required_decel * t**2
|
||||
|
||||
if distance_at_t < critical_distance:
|
||||
a_max[i] = min(a_max[i], required_decel)
|
||||
|
||||
# Update status
|
||||
self.active = True
|
||||
self.debug_msg = f"Curve in {critical_distance:.0f}m → {critical_safe_speed*3.6:.0f} km/h"
|
||||
cloudlog.info(f"DTSC: {self.debug_msg} (aggr={self.aggressiveness:.1f})")
|
||||
|
||||
return a_min, a_max
|
||||
|
||||
def _is_model_data_valid(self, model_msg):
|
||||
"""Check if model message contains valid prediction data."""
|
||||
return (len(model_msg.position.x) == ModelConstants.IDX_N and
|
||||
len(model_msg.velocity.x) == ModelConstants.IDX_N and
|
||||
len(model_msg.orientationRate.z) == ModelConstants.IDX_N)
|
||||
|
||||
def _deactivate(self):
|
||||
"""Clear active state and debug message."""
|
||||
self.active = False
|
||||
self.debug_msg = ""
|
||||
57
dragonpilot/selfdrive/controls/lib/road_edge_detector.py
Normal file
57
dragonpilot/selfdrive/controls/lib/road_edge_detector.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (c) 2019, rav4kumar, Rick Lan, dragonpilot community, and a number of other of contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
NEARSIDE_PROB = 0.2
|
||||
EDGE_PROB = 0.35
|
||||
|
||||
class RoadEdgeDetector:
|
||||
def __init__(self, enabled = False):
|
||||
self._is_enabled = enabled
|
||||
self.left_edge_detected = False
|
||||
self.right_edge_detected = False
|
||||
|
||||
def update(self, road_edge_stds, lane_line_probs):
|
||||
if not self._is_enabled:
|
||||
return
|
||||
|
||||
left_road_edge_prob = np.clip(1.0 - road_edge_stds[0], 0.0, 1.0)
|
||||
left_lane_nearside_prob = lane_line_probs[0]
|
||||
|
||||
right_road_edge_prob = np.clip(1.0 - road_edge_stds[1], 0.0, 1.0)
|
||||
right_lane_nearside_prob = lane_line_probs[3]
|
||||
|
||||
self.left_edge_detected = bool(
|
||||
left_road_edge_prob > EDGE_PROB and
|
||||
left_lane_nearside_prob < NEARSIDE_PROB and
|
||||
right_lane_nearside_prob >= left_lane_nearside_prob
|
||||
)
|
||||
|
||||
self.right_edge_detected = bool(
|
||||
right_road_edge_prob > EDGE_PROB and
|
||||
right_lane_nearside_prob < NEARSIDE_PROB and
|
||||
left_lane_nearside_prob >= right_lane_nearside_prob
|
||||
)
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
self._is_enabled = enabled
|
||||
|
||||
def is_enabled(self):
|
||||
return self._is_enabled
|
||||
463
dragonpilot/selfdrive/gpsd/gpsd.py
Executable file
463
dragonpilot/selfdrive/gpsd/gpsd.py
Executable file
@@ -0,0 +1,463 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (c) 2026, Rick Lan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
GPS Location Service - Fuses GPS with livePose for smooth position output.
|
||||
|
||||
States:
|
||||
INITIALIZING: Waiting for first GPS fix
|
||||
CALIBRATING: Collecting yaw offset samples (need to be moving > 5 m/s)
|
||||
RUNNING: Outputting calibrated dead-reckoned position
|
||||
RECALIBRATING: Drift detected, blending back to GPS
|
||||
"""
|
||||
import json
|
||||
import numpy as np
|
||||
from enum import Enum
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import config_realtime_process
|
||||
from openpilot.common.transformations.coordinates import geodetic2ecef, ecef2geodetic, LocalCoord
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.gps import get_gps_location_service
|
||||
|
||||
|
||||
class State(Enum):
|
||||
INITIALIZING = 0
|
||||
CALIBRATING = 1
|
||||
RUNNING = 2
|
||||
RECALIBRATING = 3
|
||||
|
||||
|
||||
class LiveGPS:
|
||||
# Calibration
|
||||
CALIB_MIN_SPEED = 5.0 # m/s - need speed for reliable GPS bearing
|
||||
CALIB_MIN_SAMPLES = 5 # yaw samples needed
|
||||
CALIB_MAX_TIME = 30.0 # seconds before timeout
|
||||
|
||||
# Recalibration triggers
|
||||
RECALIB_POS_ERROR = 30.0 # meters - triggers gradual recalib
|
||||
RECALIB_POS_HARD = 500.0 # meters - triggers hard reset
|
||||
RECALIB_YAW_ERROR = 0.785 # 45 degrees in radians
|
||||
RECALIB_YAW_HARD = 1.571 # 90 degrees in radians
|
||||
RECALIB_GPS_LOST = 10.0 # seconds
|
||||
|
||||
# GPS quality
|
||||
GPS_MAX_ACCURACY = 30.0 # meters - reject worse
|
||||
GPS_MAX_JUMP = 50.0 # meters - reject jumps
|
||||
GPS_MAX_SPEED = 100.0 # m/s (~360 km/h)
|
||||
|
||||
# Smoothing
|
||||
MAX_POS_CORRECTION = 10.0 # m/s max correction rate
|
||||
MAX_YAW_CORRECTION = 0.524 # 30 deg/s in radians
|
||||
STATIONARY_SPEED = 0.5 # m/s
|
||||
|
||||
def __init__(self):
|
||||
self.state = State.INITIALIZING
|
||||
|
||||
# GPS raw data
|
||||
self.last_gps_pos = None # [lat, lon, alt]
|
||||
self.gps_speed = 0.0
|
||||
self.gps_bearing = 0.0
|
||||
self.gps_accuracy_h = 100.0
|
||||
self.gps_accuracy_v = 100.0
|
||||
self.gps_quality = 1.0 # 0-1 weight
|
||||
self.unix_timestamp_millis = 0
|
||||
|
||||
# Position tracking (NED frame)
|
||||
self.local_coord = None
|
||||
self.pos_ned = np.zeros(3)
|
||||
self.pos_error = np.zeros(3)
|
||||
self.target_pos = np.zeros(3)
|
||||
|
||||
# livePose data
|
||||
self.orientation_ned = np.zeros(3)
|
||||
self.vel_device = np.zeros(3)
|
||||
|
||||
# Yaw calibration
|
||||
self.yaw_offset = 0.0
|
||||
self.yaw_offset_valid = False
|
||||
self.yaw_samples = []
|
||||
self.target_yaw = 0.0
|
||||
|
||||
# Timing
|
||||
self.last_t = None
|
||||
self.last_gps_t = 0.0
|
||||
self.calib_start_t = 0.0
|
||||
|
||||
def get_yaw(self):
|
||||
"""Get calibrated absolute yaw."""
|
||||
if self.yaw_offset_valid:
|
||||
return (self.orientation_ned[2] + self.yaw_offset) % (2 * np.pi)
|
||||
return np.radians(self.gps_bearing)
|
||||
|
||||
def _check_gps_valid(self, gps):
|
||||
"""Check if GPS data is usable."""
|
||||
if abs(gps.latitude) < 0.1 or abs(gps.longitude) < 0.1:
|
||||
return False
|
||||
if abs(gps.latitude) > 90 or abs(gps.longitude) > 180:
|
||||
return False
|
||||
return gps.hasFix or gps.unixTimestampMillis > 0
|
||||
|
||||
def _check_gps_quality(self, t, gps):
|
||||
"""Check quality and detect jumps. Returns (accept, weight)."""
|
||||
# Unknown accuracy = assume decent
|
||||
accuracy = gps.horizontalAccuracy if gps.horizontalAccuracy > 0 else 8.0
|
||||
|
||||
# Reject known bad accuracy
|
||||
if gps.horizontalAccuracy > self.GPS_MAX_ACCURACY:
|
||||
return False, 0.0
|
||||
|
||||
# Jump detection
|
||||
if self.last_gps_pos is not None and self.last_gps_t > 0:
|
||||
dt = t - self.last_gps_t
|
||||
if dt > 0.01:
|
||||
last_ecef = geodetic2ecef(self.last_gps_pos)
|
||||
curr_ecef = geodetic2ecef([gps.latitude, gps.longitude, gps.altitude])
|
||||
distance = np.linalg.norm(np.array(curr_ecef) - np.array(last_ecef))
|
||||
if distance > max(self.GPS_MAX_JUMP, self.GPS_MAX_SPEED * dt):
|
||||
return False, 0.0
|
||||
|
||||
# Weight by accuracy (5m = 1.0, 30m = 0.17)
|
||||
weight = min(1.0, 5.0 / max(accuracy, 1.0))
|
||||
return True, max(0.1, weight)
|
||||
|
||||
def handle_gps(self, t, gps):
|
||||
"""Process GPS update."""
|
||||
if not self._check_gps_valid(gps):
|
||||
return
|
||||
|
||||
accept, weight = self._check_gps_quality(t, gps)
|
||||
|
||||
# Always store for display (even if rejected)
|
||||
self.last_gps_pos = [gps.latitude, gps.longitude, gps.altitude]
|
||||
self.gps_speed = gps.speed
|
||||
self.gps_bearing = gps.bearingDeg
|
||||
|
||||
if not accept:
|
||||
# Allow poor GPS for initialization only
|
||||
if self.state == State.INITIALIZING:
|
||||
weight = 0.1
|
||||
else:
|
||||
return
|
||||
|
||||
# Store quality data
|
||||
self.gps_accuracy_h = gps.horizontalAccuracy if gps.horizontalAccuracy > 0 else 10.0
|
||||
self.gps_accuracy_v = gps.verticalAccuracy if gps.verticalAccuracy > 0 else 20.0
|
||||
self.gps_quality = weight
|
||||
self.last_gps_t = t
|
||||
self.unix_timestamp_millis = gps.unixTimestampMillis
|
||||
|
||||
# State machine
|
||||
if self.state == State.INITIALIZING:
|
||||
self._init_position(gps)
|
||||
self.state = State.CALIBRATING
|
||||
self.calib_start_t = t
|
||||
self.yaw_samples = []
|
||||
cloudlog.info("LiveGPS: GPS acquired, calibrating")
|
||||
|
||||
elif self.state == State.CALIBRATING:
|
||||
self._calibrate(t, gps)
|
||||
|
||||
elif self.state == State.RUNNING:
|
||||
self._update_running(t, gps)
|
||||
|
||||
elif self.state == State.RECALIBRATING:
|
||||
self._recalibrate(t, gps)
|
||||
|
||||
def _init_position(self, gps):
|
||||
"""Initialize local coordinate frame."""
|
||||
self.local_coord = LocalCoord.from_geodetic([gps.latitude, gps.longitude, gps.altitude])
|
||||
self.pos_ned = np.zeros(3)
|
||||
self.pos_error = np.zeros(3)
|
||||
|
||||
def _collect_yaw_sample(self, gps):
|
||||
"""Collect yaw calibration sample if conditions met."""
|
||||
if gps.speed > self.CALIB_MIN_SPEED and self.gps_quality > 0.3:
|
||||
gps_yaw = np.radians(gps.bearingDeg)
|
||||
pose_yaw = self.orientation_ned[2]
|
||||
offset = np.arctan2(np.sin(gps_yaw - pose_yaw), np.cos(gps_yaw - pose_yaw))
|
||||
self.yaw_samples.append(offset)
|
||||
|
||||
def _calibrate(self, t, gps):
|
||||
"""Calibration state: collect yaw samples."""
|
||||
self._collect_yaw_sample(gps)
|
||||
|
||||
if len(self.yaw_samples) >= self.CALIB_MIN_SAMPLES:
|
||||
self.yaw_offset = float(np.median(self.yaw_samples))
|
||||
self.yaw_offset_valid = True
|
||||
self._init_position(gps)
|
||||
self.state = State.RUNNING
|
||||
cloudlog.info(f"LiveGPS: calibrated, yaw_offset={np.degrees(self.yaw_offset):.1f}deg")
|
||||
|
||||
elif t - self.calib_start_t > self.CALIB_MAX_TIME:
|
||||
if self.yaw_samples:
|
||||
self.yaw_offset = float(np.median(self.yaw_samples))
|
||||
self.yaw_offset_valid = True
|
||||
self._init_position(gps)
|
||||
self.state = State.RUNNING
|
||||
cloudlog.warning("LiveGPS: calibration timeout")
|
||||
|
||||
def _update_running(self, t, gps):
|
||||
"""Running state: update position error and check for drift."""
|
||||
gps_ecef = geodetic2ecef([gps.latitude, gps.longitude, gps.altitude])
|
||||
gps_ned = self.local_coord.ecef2ned(gps_ecef)
|
||||
self.pos_error = gps_ned - self.pos_ned
|
||||
|
||||
pos_error_mag = np.linalg.norm(self.pos_error[:2])
|
||||
gps_age = t - self.last_gps_t
|
||||
|
||||
# Check for hard reset conditions
|
||||
if pos_error_mag > self.RECALIB_POS_HARD or gps_age > self.RECALIB_GPS_LOST * 3:
|
||||
cloudlog.warning(f"LiveGPS: hard reset, error={pos_error_mag:.1f}m")
|
||||
self._init_position(gps)
|
||||
self.yaw_offset_valid = False
|
||||
self.state = State.CALIBRATING
|
||||
self.calib_start_t = t
|
||||
self.yaw_samples = []
|
||||
return
|
||||
|
||||
# Check yaw drift
|
||||
if gps.speed > self.CALIB_MIN_SPEED and self.gps_quality > 0.3:
|
||||
gps_yaw = np.radians(gps.bearingDeg)
|
||||
new_offset = np.arctan2(np.sin(gps_yaw - self.orientation_ned[2]),
|
||||
np.cos(gps_yaw - self.orientation_ned[2]))
|
||||
diff = abs(np.arctan2(np.sin(new_offset - self.yaw_offset),
|
||||
np.cos(new_offset - self.yaw_offset)))
|
||||
|
||||
if diff > self.RECALIB_YAW_HARD:
|
||||
cloudlog.warning(f"LiveGPS: yaw reset, diff={np.degrees(diff):.1f}deg")
|
||||
self.yaw_offset = new_offset
|
||||
self._init_position(gps)
|
||||
elif diff > self.RECALIB_YAW_ERROR:
|
||||
cloudlog.warning(f"LiveGPS: yaw drift, diff={np.degrees(diff):.1f}deg")
|
||||
self.state = State.RECALIBRATING
|
||||
self.calib_start_t = t
|
||||
self.yaw_samples = []
|
||||
self.target_yaw = new_offset
|
||||
self.target_pos = gps_ned
|
||||
else:
|
||||
# Slow adaptation
|
||||
alpha = 0.1 * self.gps_quality
|
||||
self.yaw_offset += alpha * np.arctan2(np.sin(new_offset - self.yaw_offset),
|
||||
np.cos(new_offset - self.yaw_offset))
|
||||
|
||||
# Check position drift
|
||||
if pos_error_mag > self.RECALIB_POS_ERROR:
|
||||
cloudlog.warning(f"LiveGPS: pos drift, error={pos_error_mag:.1f}m")
|
||||
self.state = State.RECALIBRATING
|
||||
self.calib_start_t = t
|
||||
self.yaw_samples = []
|
||||
self.target_pos = gps_ned
|
||||
|
||||
# Reset anchor if drifted too far
|
||||
if np.linalg.norm(self.pos_ned[:2]) > 100:
|
||||
self._init_position(gps)
|
||||
|
||||
def _recalibrate(self, t, gps):
|
||||
"""Recalibrating state: blend back to GPS."""
|
||||
gps_ecef = geodetic2ecef([gps.latitude, gps.longitude, gps.altitude])
|
||||
self.target_pos = self.local_coord.ecef2ned(gps_ecef)
|
||||
|
||||
self._collect_yaw_sample(gps)
|
||||
if len(self.yaw_samples) >= 3:
|
||||
self.target_yaw = float(np.median(self.yaw_samples[-10:]))
|
||||
|
||||
# Check if done
|
||||
pos_error = np.linalg.norm(self.target_pos - self.pos_ned)
|
||||
if pos_error < 5.0 and len(self.yaw_samples) >= self.CALIB_MIN_SAMPLES:
|
||||
self.yaw_offset = self.target_yaw
|
||||
self.state = State.RUNNING
|
||||
cloudlog.info(f"LiveGPS: recalibrated, error={pos_error:.1f}m")
|
||||
elif t - self.calib_start_t > self.CALIB_MAX_TIME:
|
||||
if self.yaw_samples:
|
||||
self.yaw_offset = float(np.median(self.yaw_samples))
|
||||
self.state = State.RUNNING
|
||||
cloudlog.warning(f"LiveGPS: recalib timeout, error={pos_error:.1f}m")
|
||||
|
||||
def handle_pose(self, t, pose):
|
||||
"""Process livePose update - dead-reckon position."""
|
||||
if pose.orientationNED.valid:
|
||||
self.orientation_ned = np.array([pose.orientationNED.x, pose.orientationNED.y, pose.orientationNED.z])
|
||||
if pose.velocityDevice.valid:
|
||||
self.vel_device = np.array([pose.velocityDevice.x, pose.velocityDevice.y, pose.velocityDevice.z])
|
||||
|
||||
if self.state not in (State.RUNNING, State.RECALIBRATING) or self.local_coord is None:
|
||||
self.last_t = t
|
||||
return
|
||||
|
||||
if self.last_t is None:
|
||||
self.last_t = t
|
||||
return
|
||||
|
||||
dt = t - self.last_t
|
||||
if dt <= 0 or dt > 1.0:
|
||||
self.last_t = t
|
||||
return
|
||||
|
||||
# Stationary detection
|
||||
speed = np.linalg.norm(self.vel_device[:2])
|
||||
is_stationary = speed < self.STATIONARY_SPEED and self.gps_speed < self.STATIONARY_SPEED
|
||||
|
||||
# Yaw blending during recalibration
|
||||
if self.state == State.RECALIBRATING and self.yaw_samples:
|
||||
yaw_diff = np.arctan2(np.sin(self.target_yaw - self.yaw_offset),
|
||||
np.cos(self.target_yaw - self.yaw_offset))
|
||||
yaw_rate = 0.9 if abs(yaw_diff) > 0.5 else 0.5
|
||||
correction = np.clip(yaw_rate * dt * yaw_diff, -self.MAX_YAW_CORRECTION * dt, self.MAX_YAW_CORRECTION * dt)
|
||||
self.yaw_offset += correction
|
||||
|
||||
# Transform velocity to NED
|
||||
yaw = self.get_yaw()
|
||||
cos_yaw, sin_yaw = np.cos(yaw), np.sin(yaw)
|
||||
vel_ned = np.array([
|
||||
cos_yaw * self.vel_device[0] - sin_yaw * self.vel_device[1],
|
||||
sin_yaw * self.vel_device[0] + cos_yaw * self.vel_device[1],
|
||||
self.vel_device[2]
|
||||
])
|
||||
|
||||
# Integrate position (skip if stationary)
|
||||
if not is_stationary:
|
||||
self.pos_ned += vel_ned * dt
|
||||
|
||||
# Position correction
|
||||
if is_stationary:
|
||||
correction = self.pos_error * 0.05 * dt
|
||||
elif self.state == State.RECALIBRATING:
|
||||
error = self.target_pos - self.pos_ned
|
||||
rate = 0.95 if np.linalg.norm(error[:2]) > 50 else 0.4
|
||||
correction = error * rate * self.gps_quality * dt
|
||||
else:
|
||||
correction = self.pos_error * 0.8 * self.gps_quality * dt
|
||||
|
||||
# Cap correction
|
||||
mag = np.linalg.norm(correction[:2])
|
||||
max_corr = self.MAX_POS_CORRECTION * dt
|
||||
if mag > max_corr:
|
||||
correction *= max_corr / mag
|
||||
|
||||
self.pos_ned += correction
|
||||
if self.state == State.RUNNING:
|
||||
self.pos_error -= correction
|
||||
|
||||
self.last_t = t
|
||||
|
||||
def get_msg(self, log_mono_time):
|
||||
"""Build liveGPS message."""
|
||||
msg = messaging.new_message('liveGPS')
|
||||
msg.logMonoTime = log_mono_time
|
||||
gps = msg.liveGPS
|
||||
|
||||
t = log_mono_time * 1e-9
|
||||
gps_age = t - self.last_gps_t
|
||||
is_valid = self.state in (State.RUNNING, State.RECALIBRATING)
|
||||
gps_ok = is_valid and gps_age < 5.0
|
||||
|
||||
if is_valid and self.local_coord is not None:
|
||||
pos_ecef = self.local_coord.ned2ecef(self.pos_ned)
|
||||
geodetic = ecef2geodetic(pos_ecef)
|
||||
gps.latitude = float(geodetic[0])
|
||||
gps.longitude = float(geodetic[1])
|
||||
gps.altitude = float(geodetic[2])
|
||||
gps.bearingDeg = float(np.degrees(self.get_yaw()) % 360)
|
||||
gps.speed = float(np.linalg.norm(self.vel_device[:2]))
|
||||
gps.horizontalAccuracy = float(self.gps_accuracy_h + np.linalg.norm(self.pos_ned[:2]) * 0.1)
|
||||
gps.verticalAccuracy = float(self.gps_accuracy_v)
|
||||
gps.status = 'valid' if gps_ok else ('recalibrating' if self.state == State.RECALIBRATING else 'gpsStale')
|
||||
|
||||
elif self.last_gps_pos is not None:
|
||||
gps.latitude = float(self.last_gps_pos[0])
|
||||
gps.longitude = float(self.last_gps_pos[1])
|
||||
gps.altitude = float(self.last_gps_pos[2])
|
||||
gps.speed = float(self.gps_speed)
|
||||
gps.bearingDeg = float(self.gps_bearing)
|
||||
gps.horizontalAccuracy = float(self.gps_accuracy_h) if self.gps_accuracy_h > 0 else 50.0
|
||||
gps.verticalAccuracy = float(self.gps_accuracy_v) if self.gps_accuracy_v > 0 else 50.0
|
||||
gps.status = 'calibrating' if self.state == State.CALIBRATING else 'initializing'
|
||||
|
||||
else:
|
||||
gps.latitude = 0.0
|
||||
gps.longitude = 0.0
|
||||
gps.altitude = 0.0
|
||||
gps.speed = 0.0
|
||||
gps.bearingDeg = 0.0
|
||||
gps.horizontalAccuracy = 100.0
|
||||
gps.verticalAccuracy = 100.0
|
||||
gps.status = 'noGps'
|
||||
|
||||
gps.gpsOK = gps_ok
|
||||
gps.unixTimestampMillis = self.unix_timestamp_millis
|
||||
gps.lastGpsTimestamp = int(self.last_gps_t * 1e9) if self.last_gps_t > 0 else 0
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def main():
|
||||
config_realtime_process([0, 1, 2, 3], 5)
|
||||
|
||||
params = Params()
|
||||
gps_service = get_gps_location_service(params)
|
||||
cloudlog.info(f"LiveGPS: using {gps_service}")
|
||||
|
||||
pm = messaging.PubMaster(['liveGPS'])
|
||||
sm = messaging.SubMaster([gps_service, 'livePose'], poll='livePose', ignore_alive=[gps_service])
|
||||
|
||||
gps = LiveGPS()
|
||||
|
||||
# Load last GPS position or default to Taipei 101
|
||||
try:
|
||||
last_pos = params.get("LastGPSPosition")
|
||||
if last_pos:
|
||||
pos_data = json.loads(last_pos)
|
||||
gps.last_gps_pos = [pos_data['latitude'], pos_data['longitude'], pos_data['altitude']]
|
||||
cloudlog.info(f"LiveGPS: loaded last position: {gps.last_gps_pos}")
|
||||
else:
|
||||
raise ValueError("No saved position")
|
||||
except Exception:
|
||||
gps.last_gps_pos = [25.033976, 121.564472, 10.0] # Taipei 101
|
||||
cloudlog.info("LiveGPS: using default position (Taipei 101)")
|
||||
|
||||
while True:
|
||||
sm.update()
|
||||
|
||||
if sm.updated[gps_service] and sm.valid[gps_service]:
|
||||
gps.handle_gps(sm.logMonoTime[gps_service] * 1e-9, sm[gps_service])
|
||||
|
||||
if sm.updated['livePose']:
|
||||
if sm.valid['livePose']:
|
||||
gps.handle_pose(sm.logMonoTime['livePose'] * 1e-9, sm['livePose'])
|
||||
|
||||
msg = gps.get_msg(sm.logMonoTime['livePose'])
|
||||
pm.send('liveGPS', msg)
|
||||
|
||||
# Save position periodically
|
||||
if sm.frame % 1200 == 0 and gps.state == State.RUNNING and gps.last_gps_pos:
|
||||
if (sm.logMonoTime['livePose'] * 1e-9 - gps.last_gps_t) < 5.0:
|
||||
params.put("LastGPSPosition", json.dumps({
|
||||
'latitude': gps.last_gps_pos[0],
|
||||
'longitude': gps.last_gps_pos[1],
|
||||
'altitude': gps.last_gps_pos[2]
|
||||
}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
152
dragonpilot/selfdrive/ui/beepd.py
Normal file
152
dragonpilot/selfdrive/ui/beepd.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (c) 2025, Rick Lan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import time
|
||||
from cereal import car, messaging
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
import threading
|
||||
|
||||
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
|
||||
|
||||
class Beepd:
|
||||
def __init__(self, test=False):
|
||||
self.current_alert = AudibleAlert.none
|
||||
self._test = test
|
||||
self.enable_gpio()
|
||||
|
||||
def enable_gpio(self):
|
||||
try:
|
||||
if self._test:
|
||||
print("enabling GPIO")
|
||||
subprocess.run("echo 42 | sudo tee /sys/class/gpio/export",
|
||||
shell=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
encoding='utf8')
|
||||
except Exception:
|
||||
if self._test:
|
||||
print("GPIO failed to enable")
|
||||
pass
|
||||
subprocess.run("echo \"out\" | sudo tee /sys/class/gpio/gpio42/direction",
|
||||
shell=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
encoding='utf8')
|
||||
|
||||
def _beep(self, on):
|
||||
val = "1" if on else "0"
|
||||
subprocess.run(f"echo \"{val}\" | sudo tee /sys/class/gpio/gpio42/value",
|
||||
shell=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
encoding='utf8')
|
||||
|
||||
def engage(self):
|
||||
if self._test:
|
||||
print("beepd: engage")
|
||||
self._beep(True)
|
||||
time.sleep(0.05)
|
||||
self._beep(False)
|
||||
|
||||
def disengage(self):
|
||||
if self._test:
|
||||
print("beepd: disengage")
|
||||
for _ in range(2):
|
||||
self._beep(True)
|
||||
time.sleep(0.01)
|
||||
self._beep(False)
|
||||
time.sleep(0.01)
|
||||
|
||||
def prompt(self):
|
||||
if self._test:
|
||||
print("beepd: prompt")
|
||||
for _ in range(3):
|
||||
self._beep(True)
|
||||
time.sleep(0.01)
|
||||
self._beep(False)
|
||||
time.sleep(0.01)
|
||||
|
||||
def warning_immediate(self):
|
||||
if self._test:
|
||||
print("beepd: warning_immediate")
|
||||
for _ in range(5):
|
||||
self._beep(True)
|
||||
time.sleep(0.01)
|
||||
self._beep(False)
|
||||
time.sleep(0.01)
|
||||
|
||||
def dispatch_beep(self, func):
|
||||
threading.Thread(target=func, daemon=True).start()
|
||||
|
||||
def update_alert(self, new_alert):
|
||||
current_alert_played_once = self.current_alert == AudibleAlert.none
|
||||
if self.current_alert != new_alert and (new_alert != AudibleAlert.none or current_alert_played_once):
|
||||
self.current_alert = new_alert
|
||||
if new_alert == AudibleAlert.engage:
|
||||
self.dispatch_beep(self.engage)
|
||||
if new_alert == AudibleAlert.disengage:
|
||||
self.dispatch_beep(self.disengage)
|
||||
if new_alert == AudibleAlert.prompt:
|
||||
self.dispatch_beep(self.prompt)
|
||||
if new_alert == AudibleAlert.warningImmediate:
|
||||
self.dispatch_beep(self.warning_immediate)
|
||||
|
||||
def get_audible_alert(self, sm):
|
||||
if sm.updated['selfdriveState']:
|
||||
new_alert = sm['selfdriveState'].alertSound.raw
|
||||
self.update_alert(new_alert)
|
||||
|
||||
def test_beepd_thread(self):
|
||||
frame = 0
|
||||
rk = Ratekeeper(20)
|
||||
pm = messaging.PubMaster(['selfdriveState'])
|
||||
while True:
|
||||
cs = messaging.new_message('selfdriveState')
|
||||
if frame == 20:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.engage
|
||||
if frame == 40:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.disengage
|
||||
if frame == 60:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.prompt
|
||||
if frame == 80:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.warningImmediate
|
||||
|
||||
pm.send("selfdriveState", cs)
|
||||
frame += 1
|
||||
rk.keep_time()
|
||||
|
||||
def beepd_thread(self):
|
||||
if self._test:
|
||||
threading.Thread(target=self.test_beepd_thread).start()
|
||||
|
||||
sm = messaging.SubMaster(['selfdriveState'])
|
||||
rk = Ratekeeper(20)
|
||||
|
||||
while True:
|
||||
sm.update(0)
|
||||
self.get_audible_alert(sm)
|
||||
rk.keep_time()
|
||||
|
||||
def main():
|
||||
s = Beepd(test=False)
|
||||
s.beepd_thread()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
93
dragonpilot/selfdrive/ui/dashy_qr.py
Normal file
93
dragonpilot/selfdrive/ui/dashy_qr.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import socket
|
||||
import time
|
||||
|
||||
import pyray as rl
|
||||
import qrcode
|
||||
import numpy as np
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
IP_REFRESH_INTERVAL = 5 # seconds
|
||||
|
||||
|
||||
class DashyQR:
|
||||
"""Shared QR code generator for dashy web UI."""
|
||||
|
||||
def __init__(self):
|
||||
self._qr_texture: rl.Texture | None = None
|
||||
self._last_qr_url: str | None = None
|
||||
self._last_ip_check: float = 0
|
||||
|
||||
@property
|
||||
def texture(self):
|
||||
return self._qr_texture
|
||||
|
||||
@property
|
||||
def url(self) -> str | None:
|
||||
return self._last_qr_url
|
||||
|
||||
@staticmethod
|
||||
def get_local_ip() -> str | None:
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(("8.8.8.8", 80))
|
||||
ip = s.getsockname()[0]
|
||||
s.close()
|
||||
return ip
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_web_ui_url() -> str:
|
||||
ip = DashyQR.get_local_ip()
|
||||
return f"http://{ip if ip else 'localhost'}:5088"
|
||||
|
||||
def _generate_qr_code(self, url: str) -> None:
|
||||
try:
|
||||
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=0)
|
||||
qr.add_data(url)
|
||||
qr.make(fit=True)
|
||||
|
||||
pil_img = qr.make_image(fill_color="white", back_color="black").convert('RGBA')
|
||||
img_array = np.array(pil_img, dtype=np.uint8)
|
||||
|
||||
if self._qr_texture and self._qr_texture.id != 0:
|
||||
rl.unload_texture(self._qr_texture)
|
||||
|
||||
rl_image = rl.Image()
|
||||
rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data)
|
||||
rl_image.width = pil_img.width
|
||||
rl_image.height = pil_img.height
|
||||
rl_image.mipmaps = 1
|
||||
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
|
||||
|
||||
self._qr_texture = rl.load_texture_from_image(rl_image)
|
||||
self._last_qr_url = url
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"QR code generation failed: {e}")
|
||||
self._qr_texture = None
|
||||
|
||||
def update(self, force: bool = False) -> bool:
|
||||
"""Update QR code if needed. Returns True if updated."""
|
||||
now = time.monotonic()
|
||||
if not force and now - self._last_ip_check < IP_REFRESH_INTERVAL and self._qr_texture:
|
||||
return False
|
||||
|
||||
self._last_ip_check = now
|
||||
url = self.get_web_ui_url()
|
||||
if url != self._last_qr_url:
|
||||
self._generate_qr_code(url)
|
||||
return True
|
||||
return False
|
||||
|
||||
def force_update(self):
|
||||
"""Force immediate IP check and QR regeneration."""
|
||||
self._last_ip_check = 0
|
||||
|
||||
def cleanup(self):
|
||||
"""Unload texture resources."""
|
||||
if self._qr_texture and self._qr_texture.id != 0:
|
||||
rl.unload_texture(self._qr_texture)
|
||||
self._qr_texture = None
|
||||
|
||||
def __del__(self):
|
||||
self.cleanup()
|
||||
60
dragonpilot/selfdrive/ui/mici/layouts/dashy_qrcode.py
Normal file
60
dragonpilot/selfdrive/ui/mici/layouts/dashy_qrcode.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import MiciLabel
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from dragonpilot.selfdrive.ui.dashy_qr import DashyQR
|
||||
|
||||
|
||||
class DashyQRCode(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._qr = DashyQR()
|
||||
|
||||
self._title_label = MiciLabel(tr("scan to access"), font_size=32, font_weight=FontWeight.BOLD,
|
||||
color=rl.WHITE, wrap_text=True)
|
||||
self._subtitle_label = MiciLabel("dashy", font_size=48, font_weight=FontWeight.DISPLAY,
|
||||
color=rl.WHITE)
|
||||
self._or_label = MiciLabel(tr("or open browser"), font_size=24, font_weight=FontWeight.NORMAL,
|
||||
color=rl.GRAY)
|
||||
self._url_label = MiciLabel("", font_size=24, font_weight=FontWeight.NORMAL,
|
||||
color=rl.GRAY, wrap_text=True)
|
||||
|
||||
def show_event(self):
|
||||
self._qr.force_update()
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Skip if off-screen (scroller renders all items, add small buffer for float precision)
|
||||
if rect.x + rect.width < 1 or rect.x > gui_app.width - 1:
|
||||
return
|
||||
|
||||
if self._qr.update():
|
||||
self._url_label.set_text(self._qr.url or "")
|
||||
|
||||
# Left side: QR code (square, full height)
|
||||
if self._qr.texture:
|
||||
scale = rect.height / self._qr.texture.height
|
||||
pos = rl.Vector2(rect.x, rect.y)
|
||||
rl.draw_texture_ex(self._qr.texture, pos, 0.0, scale, rl.WHITE)
|
||||
|
||||
# Right side: Text
|
||||
text_x = rect.x + rect.height + 16
|
||||
text_width = int(rect.width - text_x)
|
||||
|
||||
# Title: "scan to access"
|
||||
self._title_label.set_width(text_width)
|
||||
self._title_label.set_position(text_x, rect.y)
|
||||
self._title_label.render()
|
||||
|
||||
# Subtitle: "dashy"
|
||||
self._subtitle_label.set_position(text_x, rect.y + 32)
|
||||
self._subtitle_label.render()
|
||||
|
||||
# "or open browser"
|
||||
self._or_label.set_position(text_x, rect.y + rect.height - 24 - 28)
|
||||
self._or_label.render()
|
||||
|
||||
# URL
|
||||
self._url_label.set_width(text_width)
|
||||
self._url_label.set_position(text_x, rect.y + rect.height - 24)
|
||||
self._url_label.render()
|
||||
@@ -28,6 +28,64 @@ SETTINGS = [
|
||||
{
|
||||
"title": "Lateral",
|
||||
"settings": [
|
||||
{
|
||||
"key": "dp_lat_alka",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Always-on Lane Keeping Assist (ALKA)"),
|
||||
"description": lambda: tr("Enable lateral control even when ACC/cruise is disengaged, using ACC Main or LKAS button to toggle. Vehicle must be moving."),
|
||||
"brands": ["toyota", "hyundai", "honda", "volkswagen", "subaru", "mazda", "nissan", "ford"],
|
||||
},
|
||||
{
|
||||
"key": "dp_lat_lca_speed",
|
||||
"type": "spin_button_item",
|
||||
"title": lambda: tr("Lane Change Assist At:"),
|
||||
"description": lambda: tr("Off = Disable LCA.<br>1 mph = 1.2 km/h."),
|
||||
"default": 20,
|
||||
"min_val": 0,
|
||||
"max_val": 100,
|
||||
"step": 5,
|
||||
"suffix": lambda: tr("mph"),
|
||||
"special_value_text": lambda: tr("Off"),
|
||||
"on_change": [{
|
||||
"target": "dp_lat_lca_auto_sec",
|
||||
"action": "set_enabled",
|
||||
"condition": "value > 0"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"key": "dp_lat_lca_auto_sec",
|
||||
"type": "double_spin_button_item",
|
||||
"title": lambda: tr("+ Auto Lane Change after:"),
|
||||
"description": lambda: tr("Off = Disable Auto Lane Change."),
|
||||
"default": 0.0,
|
||||
"min_val": 0.0,
|
||||
"max_val": 5.0,
|
||||
"step": 0.5,
|
||||
"suffix": lambda: tr("sec"),
|
||||
"special_value_text": lambda: tr("Off"),
|
||||
"initially_enabled_by": {
|
||||
"param": "dp_lat_lca_speed",
|
||||
"condition": "value > 0",
|
||||
"default": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "dp_lat_road_edge_detection",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Road Edge Detection (RED)"),
|
||||
"description": lambda: tr("Block lane change assist when the system detects the road edge.<br>NOTE: This will show 'Car Detected in Blindspot' warning."),
|
||||
},
|
||||
{
|
||||
"key": "dp_lat_offset_cm",
|
||||
"type": "spin_button_item",
|
||||
"title": lambda: tr("Position Offset"),
|
||||
"description": lambda: tr("Fine-tune where the car drives within the lane. Positive values move the car left, negative values move right.<br>Recommended to start with small values (±5cm) and adjust based on preference."),
|
||||
"default": 0,
|
||||
"min_val": -15,
|
||||
"max_val": 15,
|
||||
"step": 1,
|
||||
"suffix": lambda: tr("cm"),
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
@@ -35,18 +93,160 @@ SETTINGS = [
|
||||
"title": "Longitudinal",
|
||||
"condition": "openpilotLongitudinalControl",
|
||||
"settings": [
|
||||
{
|
||||
"key": "dp_lon_acm",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Enable Adaptive Coasting Mode (ACM)"),
|
||||
"description": lambda: tr("Adaptive Coasting Mode (ACM) reduces braking to allow smoother coasting when appropriate."),
|
||||
},
|
||||
{
|
||||
"key": "dp_lon_aem",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Adaptive Experimental Mode (AEM)"),
|
||||
"description": lambda: tr("Adaptive mode switcher between ACC and Blended based on driving context."),
|
||||
},
|
||||
{
|
||||
"key": "dp_lon_dtsc",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Dynamic Turn Speed Control (DTSC)"),
|
||||
"description": lambda: tr("DTSC automatically adjusts the vehicle's predicted speed based on upcoming road curvature and grip conditions.<br>Originally from the openpilot TACO branch."),
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "UI",
|
||||
"settings": [
|
||||
{
|
||||
"key": "dp_ui_display_mode",
|
||||
"type": "text_spin_button_item",
|
||||
"title": lambda: tr("Display Mode"),
|
||||
"description": lambda: tr("Std.: Stock behavior.<br>MAIN+: ACC MAIN on = Display ON.<br>OP+: OP enabled = Display ON.<br>MAIN-: ACC MAIN on = Display OFF<br>OP-: OP enabled = Display OFF."),
|
||||
"default": 0,
|
||||
"options": [
|
||||
lambda: tr("Std."),
|
||||
lambda: tr("MAIN+"),
|
||||
lambda: tr("OP+"),
|
||||
lambda: tr("MAIN-"),
|
||||
lambda: tr("OP-"),
|
||||
],
|
||||
"condition": "not MICI",
|
||||
},
|
||||
{
|
||||
"key": "dp_ui_hide_hud_speed_kph",
|
||||
"type": "spin_button_item",
|
||||
"title": lambda: tr("Hide HUD When Moves above:"),
|
||||
"description": lambda: tr("To prevent screen burn-in, hide Speed, MAX Speed, and Steering/DM Icons when the car moves.<br>Off = Stock Behavior<br>1 km/h = 0.6 mph"),
|
||||
"default": 0,
|
||||
"min_val": 0,
|
||||
"max_val": 120,
|
||||
"step": 5,
|
||||
"suffix": lambda: tr("km/h"),
|
||||
"special_value_text": lambda: tr("Off"),
|
||||
},
|
||||
{
|
||||
"key": "dp_ui_rainbow",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Rainbow Driving Path like Tesla"),
|
||||
"description": lambda: tr("Why not?"),
|
||||
"condition": "not MICI",
|
||||
},
|
||||
{
|
||||
"key": "dp_ui_lead",
|
||||
"type": "text_spin_button_item",
|
||||
"title": lambda: tr("Display Lead Stats"),
|
||||
"description": lambda: tr("Display the statistics of lead car and/or radar tracking points.<br>Lead: Lead stats only<br>Radar: Radar tracking point stats only<br>All: Lead and Radar stats<br>NOTE: Radar option only works on certain vehicle models."),
|
||||
"default": 0,
|
||||
"options": [
|
||||
lambda: tr("Off"),
|
||||
lambda: tr("Lead"),
|
||||
lambda: tr("Radar"),
|
||||
lambda: tr("All"),
|
||||
],
|
||||
"condition": "not MICI",
|
||||
},
|
||||
{
|
||||
"key": "dp_ui_mici",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Use MICI (comma four) UI"),
|
||||
"description": lambda: tr("Why not?"),
|
||||
"condition": "not MICI",
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Device",
|
||||
"settings": [
|
||||
{
|
||||
"key": "dp_dev_is_rhd",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Enable Right-Hand Drive Mode"),
|
||||
"description": lambda: tr("Allow openpilot to obey right-hand traffic conventions on right driver seat."),
|
||||
"condition": "LITE",
|
||||
},
|
||||
{
|
||||
"key": "dp_dev_beep",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Enable Beep (Warning)"),
|
||||
"description": lambda: tr("Use Buzzer for audiable alerts."),
|
||||
"condition": "LITE",
|
||||
},
|
||||
{
|
||||
"key": "dp_lon_ext_radar",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Use External Radar"),
|
||||
"description": lambda: tr("See https://github.com/eFiniLan/openpilot-ext-radar-addon for more information."),
|
||||
},
|
||||
{
|
||||
"key": "dp_dev_audible_alert_mode",
|
||||
"type": "text_spin_button_item",
|
||||
"title": lambda: tr("Audible Alert"),
|
||||
"description": lambda: tr("Std.: Stock behaviour.<br>Warning: Only emits sound when there is a warning.<br>Off: Does not emit any sound at all."),
|
||||
"default": 0,
|
||||
"options": [
|
||||
lambda: tr("Std."),
|
||||
lambda: tr("Warning"),
|
||||
lambda: tr("Off"),
|
||||
],
|
||||
"condition": "not LITE",
|
||||
},
|
||||
{
|
||||
"key": "dp_dev_auto_shutdown_in",
|
||||
"type": "spin_button_item",
|
||||
"title": lambda: tr("Auto Shutdown After"),
|
||||
"description": lambda: tr("0 min = Immediately"),
|
||||
"default": -5,
|
||||
"min_val": -5,
|
||||
"max_val": 300,
|
||||
"step": 5,
|
||||
"suffix": lambda: tr("min"),
|
||||
"special_value_text": lambda: tr("Off"),
|
||||
},
|
||||
{
|
||||
"key": "dp_dev_dashy",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("dashy HUD"),
|
||||
"description": lambda: tr("dashy - dragonpilot's all-in-one system hub for you.<br><br>Visit http://<device_ip>:5088 to access.<br><br>Enable this to use HUD feature (live streaming)."),
|
||||
},
|
||||
{
|
||||
"key": "dp_dev_delay_loggerd",
|
||||
"type": "spin_button_item",
|
||||
"title": lambda: tr("Delay Starting Loggerd for:"),
|
||||
"description": lambda: tr("Delays the startup of loggerd and its related processes when the device goes on-road.<br>This prevents the initial moments of a drive from being recorded, protecting location privacy at the start of a trip."),
|
||||
"default": 0,
|
||||
"min_val": 0,
|
||||
"max_val": 300,
|
||||
"step": 5,
|
||||
"suffix": lambda: tr("sec"),
|
||||
"special_value_text": lambda: tr("Off"),
|
||||
},
|
||||
{
|
||||
"key": "dp_dev_disable_connect",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Disable Comma Connect"),
|
||||
"description": lambda: tr("Disable Comma connect service if you do not wish to upload / being tracked by the service."),
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
|
||||
@@ -27,6 +27,34 @@ function agnos_init {
|
||||
fi
|
||||
}
|
||||
|
||||
set_tici_hw() {
|
||||
if grep -q "tici" /sys/firmware/devicetree/base/model 2>/dev/null; then
|
||||
echo "Querying panda MCU type..."
|
||||
MCU_OUTPUT=$(python -c "from panda_tici import Panda; p = Panda(cli=False); print(p.get_mcu_type()); p.close()" 2>/dev/null)
|
||||
|
||||
if [[ "$MCU_OUTPUT" == *"McuType.F4"* ]]; then
|
||||
echo "TICI (DOS) detected"
|
||||
elif [[ "$MCU_OUTPUT" == *"McuType.H7"* ]]; then
|
||||
echo "TICI (TRES) detected"
|
||||
export TICI_TRES=1
|
||||
else
|
||||
echo "TICI (UNKNOWN) detected"
|
||||
fi
|
||||
export TICI_HW=1
|
||||
fi
|
||||
}
|
||||
|
||||
set_lite_hw() {
|
||||
if grep -q "tici" /sys/firmware/devicetree/base/model 2>/dev/null; then
|
||||
output=$(i2cget -y 0 0x10 0x00 2>/dev/null)
|
||||
|
||||
if [ -z "$output" ]; then
|
||||
echo "Lite HW"
|
||||
export LITE=1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function launch {
|
||||
# Remove orphaned git lock if it exists on boot
|
||||
[ -f "$DIR/.git/index.lock" ] && rm -f $DIR/.git/index.lock
|
||||
@@ -71,6 +99,8 @@ function launch {
|
||||
|
||||
# hardware specific init
|
||||
if [ -f /AGNOS ]; then
|
||||
set_tici_hw
|
||||
set_lite_hw
|
||||
agnos_init
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
SConscript(['opendbc/dbc/SConscript'])
|
||||
|
||||
# test files
|
||||
if GetOption('extras'):
|
||||
SConscript('opendbc/safety/tests/libsafety/SConscript')
|
||||
#if GetOption('extras'):
|
||||
SConscript('opendbc/safety/tests/libsafety/SConscript')
|
||||
|
||||
@@ -82,8 +82,8 @@ def can_fingerprint(can_recv: CanRecvCallable) -> tuple[str | None, dict[int, di
|
||||
|
||||
# **** for use live only ****
|
||||
def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback, num_pandas: int,
|
||||
cached_params: CarParamsT | None) -> tuple[str | None, dict, str, list[CarParams.CarFw], CarParams.FingerprintSource, bool]:
|
||||
fixed_fingerprint = os.environ.get('FINGERPRINT', "")
|
||||
cached_params: CarParamsT | None, dp_fingerprint: str = "") -> tuple[str | None, dict, str, list[CarParams.CarFw], CarParams.FingerprintSource, bool]:
|
||||
fixed_fingerprint = os.environ.get('FINGERPRINT', dp_fingerprint)
|
||||
skip_fw_query = os.environ.get('SKIP_FW_QUERY', False)
|
||||
disable_fw_cache = os.environ.get('DISABLE_FW_CACHE', False)
|
||||
ecu_rx_addrs = set()
|
||||
@@ -149,8 +149,8 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu
|
||||
|
||||
|
||||
def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback, alpha_long_allowed: bool,
|
||||
is_release: bool, num_pandas: int = 1, dp_params: int = 0, cached_params: CarParamsT | None = None):
|
||||
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, num_pandas, cached_params)
|
||||
is_release: bool, num_pandas: int = 1, dp_params: int = 0, cached_params: CarParamsT | None = None, dp_fingerprint: str = ""):
|
||||
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, num_pandas, cached_params, dp_fingerprint=dp_fingerprint)
|
||||
|
||||
if candidate is None:
|
||||
carlog.error({"event": "car doesn't match any fingerprints", "fingerprints": repr(fingerprints)})
|
||||
|
||||
@@ -113,6 +113,9 @@ class CarState(CarStateBase):
|
||||
*create_button_events(self.lc_button, prev_lc_button, {1: ButtonType.lkas}),
|
||||
]
|
||||
|
||||
# dp - ALKA: direct tracking - lkas_on follows acc_main (cruiseState.available)
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -219,6 +219,9 @@ class CarState(CarStateBase):
|
||||
*create_button_events(self.cruise_setting, prev_cruise_setting, SETTINGS_BUTTONS_DICT),
|
||||
]
|
||||
|
||||
# dp - ALKA: direct tracking - lkas_on follows acc_main (cruiseState.available)
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
return ret
|
||||
|
||||
def get_can_parsers(self, CP):
|
||||
|
||||
@@ -118,11 +118,11 @@ class CarController(CarControllerBase):
|
||||
can_sends = []
|
||||
|
||||
# HUD messages
|
||||
sys_warning, sys_state, left_lane_warning, right_lane_warning = process_hud_alert(CC.enabled, self.car_fingerprint,
|
||||
sys_warning, sys_state, left_lane_warning, right_lane_warning = process_hud_alert(CC.latActive, self.car_fingerprint,
|
||||
hud_control)
|
||||
|
||||
can_sends.append(hyundaican.create_lkas11(self.packer, self.frame, self.CP, apply_torque, apply_steer_req,
|
||||
torque_fault, CS.lkas11, sys_warning, sys_state, CC.enabled,
|
||||
torque_fault, CS.lkas11, sys_warning, sys_state, CC.latActive,
|
||||
hud_control.leftLaneVisible, hud_control.rightLaneVisible,
|
||||
left_lane_warning, right_lane_warning))
|
||||
|
||||
@@ -148,7 +148,7 @@ class CarController(CarControllerBase):
|
||||
|
||||
# 20 Hz LFA MFA message
|
||||
if self.frame % 5 == 0 and self.CP.flags & HyundaiFlags.SEND_LFA.value:
|
||||
can_sends.append(hyundaican.create_lfahda_mfc(self.packer, CC.enabled))
|
||||
can_sends.append(hyundaican.create_lfahda_mfc(self.packer, CC.latActive))
|
||||
|
||||
# 5 Hz ACC options
|
||||
if self.frame % 20 == 0 and self.CP.openpilotLongitudinalControl:
|
||||
@@ -167,7 +167,7 @@ class CarController(CarControllerBase):
|
||||
lka_steering_long = lka_steering and self.CP.openpilotLongitudinalControl
|
||||
|
||||
# steering control
|
||||
can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN, CC.enabled, apply_steer_req, apply_torque))
|
||||
can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN, CC.latActive, apply_steer_req, apply_torque))
|
||||
|
||||
# prevent LFA from activating on LKA steering cars by sending "no lane lines detected" to ADAS ECU
|
||||
if self.frame % 5 == 0 and lka_steering:
|
||||
@@ -176,7 +176,7 @@ class CarController(CarControllerBase):
|
||||
|
||||
# LFA and HDA icons
|
||||
if self.frame % 5 == 0 and (not lka_steering or lka_steering_long):
|
||||
can_sends.append(hyundaicanfd.create_lfahda_cluster(self.packer, self.CAN, CC.enabled))
|
||||
can_sends.append(hyundaicanfd.create_lfahda_cluster(self.packer, self.CAN, CC.latActive))
|
||||
|
||||
# blinkers
|
||||
if lka_steering and self.CP.flags & HyundaiFlags.ENABLE_BLINKERS:
|
||||
|
||||
@@ -193,6 +193,9 @@ class CarState(CarStateBase):
|
||||
*create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}),
|
||||
*create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas})]
|
||||
|
||||
# dp - ALKA: direct tracking - lkas_on follows acc_main (cruiseState.available)
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
ret.blockPcmEnable = not self.recent_button_interaction()
|
||||
|
||||
# low speed steer alert hysteresis logic (only for cars with steer cut off above 10 m/s)
|
||||
@@ -291,6 +294,13 @@ class CarState(CarStateBase):
|
||||
*create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}),
|
||||
*create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas})]
|
||||
|
||||
# dp - ALKA: direct tracking - lkas_on follows acc_main
|
||||
if not self.CP.openpilotLongitudinalControl:
|
||||
cp_cruise_info = cp_cam if self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else cp
|
||||
self.lkas_on = cp_cruise_info.vl["SCC_CONTROL"]["MainMode_ACC"] == 1
|
||||
else:
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
ret.blockPcmEnable = not self.recent_button_interaction()
|
||||
|
||||
return ret
|
||||
|
||||
@@ -279,6 +279,9 @@ class CarStateBase(ABC):
|
||||
self.cluster_min_speed = 0.0 # min speed before dropping to 0
|
||||
self.secoc_key: bytes = b"00" * 16
|
||||
|
||||
# dp - ALKA: lkas_on state (mirrors panda's lkas_on for Python-panda sync)
|
||||
self.lkas_on = False
|
||||
|
||||
Q = [[0.0, 0.0], [0.0, 100.0]]
|
||||
R = 0.3
|
||||
A = [[1.0, DT_CTRL], [0.0, 1.0]]
|
||||
|
||||
@@ -115,6 +115,9 @@ class CarState(CarStateBase):
|
||||
# TODO: add button types for inc and dec
|
||||
ret.buttonEvents = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
|
||||
|
||||
# dp - ALKA: use ACC main state
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -61,7 +61,8 @@ class CarController(CarControllerBase):
|
||||
# Below are the HUD messages. We copy the stock message and modify
|
||||
if self.CP.carFingerprint != CAR.NISSAN_ALTIMA:
|
||||
if self.frame % 2 == 0:
|
||||
can_sends.append(nissancan.create_lkas_hud_msg(self.packer, CS.lkas_hud_msg, CC.enabled, hud_control.leftLaneVisible, hud_control.rightLaneVisible,
|
||||
# dp - ALKA: use latActive to show HUD when ALKA is active
|
||||
can_sends.append(nissancan.create_lkas_hud_msg(self.packer, CS.lkas_hud_msg, CC.latActive, hud_control.leftLaneVisible, hud_control.rightLaneVisible,
|
||||
hud_control.leftLaneDepart, hud_control.rightLaneDepart))
|
||||
|
||||
if self.frame % 50 == 0:
|
||||
|
||||
@@ -128,6 +128,9 @@ class CarState(CarStateBase):
|
||||
|
||||
ret.buttonEvents = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
|
||||
|
||||
# dp - ALKA: use ACC main state
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
162
opendbc_repo/opendbc/car/radar_interface.py
Normal file
162
opendbc_repo/opendbc/car/radar_interface.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
Copyright (c) 2025, Rick Lan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from opendbc.car.interfaces import RadarInterfaceBase
|
||||
from opendbc.can.parser import CANParser
|
||||
from opendbc.car.structs import RadarData
|
||||
from typing import List, Tuple
|
||||
|
||||
# car head to radar
|
||||
DREL_OFFSET = -1.52
|
||||
|
||||
|
||||
# typically max lane width is 3.7m
|
||||
LANE_WIDTH = 3.8
|
||||
LANE_WIDTH_HALF = LANE_WIDTH/2
|
||||
|
||||
LANE_CENTER_MIN_LAT = 0.
|
||||
LANE_CENTER_MAX_LAT = LANE_WIDTH_HALF
|
||||
LANE_CENTER_MIN_DIST = 5.
|
||||
|
||||
LANE_SIDE_MIN_LAT = LANE_WIDTH_HALF
|
||||
LANE_SIDE_MAX_LAT = LANE_WIDTH_HALF + LANE_WIDTH
|
||||
LANE_SIDE_MIN_DIST = 10.
|
||||
|
||||
|
||||
# lat distance, typically max lane width is 3.7m
|
||||
MAX_LAT_DIST = 6.
|
||||
|
||||
# objects to ignore thats really close to the vehicle (after DREL_OFFSET applied)
|
||||
MIN_DIST = 5.
|
||||
|
||||
# ignore oncoming objects
|
||||
IGNORE_OBJ_STATE = 2
|
||||
|
||||
# ignore objects that we haven't seen for 5 secs
|
||||
NOT_SEEN_INIT = 33
|
||||
|
||||
def _create_radar_parser():
|
||||
return CANParser('u_radar', [("Status", float('nan')), ("ObjectData", float('nan'))], 1)
|
||||
|
||||
class RadarInterface(RadarInterfaceBase):
|
||||
def __init__(self, CP):
|
||||
super().__init__(CP)
|
||||
|
||||
self.updated_messages = set()
|
||||
|
||||
self.rcp = _create_radar_parser()
|
||||
|
||||
self._pts_cache = dict()
|
||||
self._pts_not_seen = {key: 0 for key in range(255)}
|
||||
self._should_clear_cache = False
|
||||
|
||||
# called by card.py, 100hz
|
||||
def update(self, can_strings):
|
||||
vls = self.rcp.update(can_strings)
|
||||
self.updated_messages.update(vls)
|
||||
|
||||
if 1546 in self.updated_messages:
|
||||
self._should_clear_cache = True
|
||||
|
||||
if 1547 in self.updated_messages:
|
||||
all_objects = zip(
|
||||
self.rcp.vl_all['ObjectData']['ID'],
|
||||
self.rcp.vl_all['ObjectData']['DistLong'],
|
||||
self.rcp.vl_all['ObjectData']['DistLat'],
|
||||
self.rcp.vl_all['ObjectData']['VRelLong'],
|
||||
self.rcp.vl_all['ObjectData']['VRelLat'],
|
||||
self.rcp.vl_all['ObjectData']['DynProp'],
|
||||
self.rcp.vl_all['ObjectData']['Class'],
|
||||
self.rcp.vl_all['ObjectData']['RCS'],
|
||||
)
|
||||
|
||||
# clean cache when we see a 0x60a then a 0x60b
|
||||
if self._should_clear_cache:
|
||||
self._pts_cache.clear()
|
||||
self._should_clear_cache = False
|
||||
|
||||
for track_id, dist_long, dist_lat, vrel_long, vrel_lat, dyn_prop, obj_class, rcs in all_objects:
|
||||
|
||||
d_rel = dist_long + DREL_OFFSET
|
||||
y_rel = -dist_lat
|
||||
|
||||
should_ignore = False
|
||||
|
||||
# ignore point (obj_class = 0)
|
||||
if not should_ignore and int(obj_class) == 0:
|
||||
should_ignore = True
|
||||
|
||||
# ignore oncoming objects
|
||||
# @todo remove this because it's always 0 ?
|
||||
if not should_ignore and int(dyn_prop) == IGNORE_OBJ_STATE:
|
||||
should_ignore = True
|
||||
|
||||
# far away lane object, ignore
|
||||
if not should_ignore and abs(y_rel) > LANE_SIDE_MAX_LAT:
|
||||
should_ignore = True
|
||||
|
||||
# close object, ignore, use vision
|
||||
if not should_ignore and LANE_CENTER_MIN_LAT > abs(y_rel) > LANE_CENTER_MAX_LAT and d_rel < LANE_CENTER_MIN_DIST:
|
||||
should_ignore = True
|
||||
|
||||
# close object, ignore, use vision
|
||||
if not should_ignore and LANE_SIDE_MIN_LAT > abs(y_rel) > LANE_SIDE_MAX_LAT and d_rel < LANE_SIDE_MIN_DIST:
|
||||
should_ignore = True
|
||||
|
||||
if not should_ignore and track_id not in self._pts_cache:
|
||||
self._pts_cache[track_id] = RadarData.RadarPoint()
|
||||
self._pts_cache[track_id].trackId = track_id
|
||||
|
||||
if should_ignore:
|
||||
self._pts_not_seen[track_id] = -1
|
||||
else:
|
||||
self._pts_not_seen[track_id] = NOT_SEEN_INIT
|
||||
|
||||
# init cache
|
||||
if track_id not in self._pts_cache:
|
||||
self._pts_cache[track_id] = RadarData.RadarPoint()
|
||||
self._pts_cache[track_id].trackId = track_id
|
||||
|
||||
# add/update to cache
|
||||
self._pts_cache[track_id].dRel = d_rel
|
||||
self._pts_cache[track_id].yRel = y_rel
|
||||
self._pts_cache[track_id].vRel = float(vrel_long)
|
||||
self._pts_cache[track_id].yvRel = float('nan')
|
||||
self._pts_cache[track_id].aRel = float('nan')
|
||||
self._pts_cache[track_id].measured = True
|
||||
|
||||
self.updated_messages.clear()
|
||||
|
||||
# publish to cereal
|
||||
if self.frame % 3 == 0:
|
||||
keys_to_remove = [key for key in self.pts if key not in self._pts_cache]
|
||||
for key in keys_to_remove:
|
||||
self._pts_not_seen[key] -= 1
|
||||
if self._pts_not_seen[key] <= 0:
|
||||
del self.pts[key]
|
||||
|
||||
self.pts.update(self._pts_cache)
|
||||
|
||||
ret = RadarData()
|
||||
if not self.rcp.can_valid:
|
||||
ret.errors.canError = True
|
||||
|
||||
ret.points = list(self.pts.values())
|
||||
return ret
|
||||
|
||||
return None
|
||||
@@ -20,4 +20,6 @@ CarControlT = capnp.lib.capnp._StructModule
|
||||
CarParamsT = capnp.lib.capnp._StructModule
|
||||
|
||||
class DPFlags:
|
||||
LatALKA = 1
|
||||
ExtRadar = 2
|
||||
pass
|
||||
|
||||
@@ -98,7 +98,7 @@ class CarController(CarControllerBase):
|
||||
can_sends.append(subarucan.create_es_dashstatus(self.packer, self.frame // 10, CS.es_dashstatus_msg, CC.enabled,
|
||||
self.CP.openpilotLongitudinalControl, CC.longActive, hud_control.leadVisible))
|
||||
|
||||
can_sends.append(subarucan.create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, CC.enabled, hud_control.visualAlert,
|
||||
can_sends.append(subarucan.create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, CC.latActive, hud_control.visualAlert,
|
||||
hud_control.leftLaneVisible, hud_control.rightLaneVisible,
|
||||
hud_control.leftLaneDepart, hud_control.rightLaneDepart))
|
||||
|
||||
|
||||
@@ -123,6 +123,9 @@ class CarState(CarStateBase):
|
||||
if self.CP.flags & SubaruFlags.SEND_INFOTAINMENT:
|
||||
self.es_infotainment_msg = copy.copy(cp_cam.vl["ES_Infotainment"])
|
||||
|
||||
# dp - ALKA: use ACC main state
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -301,9 +301,10 @@ class CarController(CarControllerBase):
|
||||
send_ui = True
|
||||
|
||||
if self.frame % 20 == 0 or send_ui:
|
||||
# dp - ALKA: use lat_active to show HUD when ALKA is active
|
||||
can_sends.append(toyotacan.create_ui_command(self.packer, steer_alert, pcm_cancel_cmd, hud_control.leftLaneVisible,
|
||||
hud_control.rightLaneVisible, hud_control.leftLaneDepart,
|
||||
hud_control.rightLaneDepart, CC.enabled, CS.lkas_hud))
|
||||
hud_control.rightLaneDepart, lat_active, CS.lkas_hud))
|
||||
|
||||
if (self.frame % 100 == 0 or send_ui) and self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
|
||||
can_sends.append(toyotacan.create_fcw_command(self.packer, fcw_alert))
|
||||
|
||||
@@ -199,6 +199,10 @@ class CarState(CarStateBase):
|
||||
buttonEvents += create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
|
||||
|
||||
ret.buttonEvents = buttonEvents
|
||||
|
||||
# dp - ALKA: Toyota requires main ON to use ACC/LKA, use main as switch
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -4,7 +4,7 @@ from opendbc.car.toyota.carcontroller import CarController
|
||||
from opendbc.car.toyota.radar_interface import RadarInterface
|
||||
from opendbc.car.toyota.values import Ecu, CAR, DBC, ToyotaFlags, CarControllerParams, TSS2_CAR, RADAR_ACC_CAR, NO_DSU_CAR, \
|
||||
MIN_ACC_SPEED, EPS_SCALE, NO_STOP_TIMER_CAR, ANGLE_CONTROL_CAR, \
|
||||
ToyotaSafetyFlags
|
||||
ToyotaSafetyFlags, UNSUPPORTED_DSU_CAR
|
||||
from opendbc.car.disable_ecu import disable_ecu
|
||||
from opendbc.car.interfaces import CarInterfaceBase
|
||||
|
||||
@@ -26,6 +26,9 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.toyota)]
|
||||
ret.safetyConfigs[0].safetyParam = EPS_SCALE[candidate]
|
||||
|
||||
if candidate in UNSUPPORTED_DSU_CAR:
|
||||
ret.safetyConfigs[0].safetyParam |= ToyotaSafetyFlags.UNSUPPORTED_DSU.value
|
||||
|
||||
# BRAKE_MODULE is on a different address for these cars
|
||||
if DBC[candidate][Bus.pt] == "toyota_new_mc_pt_generated":
|
||||
ret.safetyConfigs[0].safetyParam |= ToyotaSafetyFlags.ALT_BRAKE.value
|
||||
|
||||
@@ -56,6 +56,7 @@ class ToyotaSafetyFlags(IntFlag):
|
||||
STOCK_LONGITUDINAL = (2 << 8)
|
||||
LTA = (4 << 8)
|
||||
SECOC = (8 << 8)
|
||||
UNSUPPORTED_DSU = (16 << 8) # dp - use DSU_CRUISE (0x365) for ACC main instead of PCM_CRUISE_2 (0x1D3)
|
||||
|
||||
|
||||
class ToyotaFlags(IntFlag):
|
||||
|
||||
@@ -136,6 +136,9 @@ class CarState(CarStateBase):
|
||||
|
||||
ret.lowSpeedAlert = self.update_low_speed_alert(ret.vEgo)
|
||||
|
||||
# dp - ALKA: use ACC main state
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
self.frame += 1
|
||||
return ret
|
||||
|
||||
@@ -227,6 +230,9 @@ class CarState(CarStateBase):
|
||||
|
||||
ret.lowSpeedAlert = self.update_low_speed_alert(ret.vEgo)
|
||||
|
||||
# dp - ALKA: use ACC main state
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
self.frame += 1
|
||||
return ret
|
||||
|
||||
|
||||
85
opendbc_repo/opendbc/dbc/u_radar.dbc
Normal file
85
opendbc_repo/opendbc/dbc/u_radar.dbc
Normal file
@@ -0,0 +1,85 @@
|
||||
|
||||
|
||||
VERSION ""
|
||||
|
||||
|
||||
NS_ :
|
||||
NS_DESC_
|
||||
CM_
|
||||
BA_DEF_
|
||||
BA_
|
||||
VAL_
|
||||
CAT_DEF_
|
||||
CAT_
|
||||
FILTER
|
||||
BA_DEF_DEF_
|
||||
EV_DATA_
|
||||
ENVVAR_DATA_
|
||||
SGTYPE_
|
||||
SGTYPE_VAL_
|
||||
BA_DEF_SGTYPE_
|
||||
BA_SGTYPE_
|
||||
SIG_TYPE_REF_
|
||||
VAL_TABLE_
|
||||
SIG_GROUP_
|
||||
SIG_VALTYPE_
|
||||
SIGTYPE_VALTYPE_
|
||||
BO_TX_BU_
|
||||
BA_DEF_REL_
|
||||
BA_REL_
|
||||
BA_DEF_DEF_REL_
|
||||
BU_SG_REL_
|
||||
BU_EV_REL_
|
||||
BU_BO_REL_
|
||||
SG_MUL_VAL_
|
||||
|
||||
BS_:
|
||||
|
||||
BU_: RADAR
|
||||
|
||||
BO_ 513 RadarState: 8 XXX
|
||||
SG_ NVMReadStatus : 6|1@0+ (1,0) [0|1] "" XXX
|
||||
SG_ NVMWriteStatus : 7|1@0+ (1,0) [0|1] "" XXX
|
||||
SG_ MaxDistanceCfg : 15|10@0+ (2,0) [0|2046] "m" XXX
|
||||
SG_ RadarPowerCfg : 25|3@0+ (1,0) [0|7] "" XXX
|
||||
SG_ SensorID : 34|3@0+ (1,0) [0|7] "" XXX
|
||||
SG_ SortIndex : 38|3@0+ (1,0) [0|7] "" XXX
|
||||
SG_ CtrlRelayCfg : 41|1@0+ (1,0) [0|1] "" XXX
|
||||
SG_ OutputTypeCfg : 43|2@0+ (1,0) [0|3] "" XXX
|
||||
SG_ QualityInfoCfg : 44|1@0+ (1,0) [0|1] "" XXX
|
||||
SG_ ExtInfoCfg : 45|1@0+ (1,0) [0|1] "" XXX
|
||||
SG_ CANBaudRate : 55|3@0+ (1,0) [0|7] "" XXX
|
||||
SG_ InterfaceType : 57|2@0+ (1,0) [0|3] "" XXX
|
||||
SG_ RCSThreshold : 58|3@1+ (1,0) [0|7] "" XXX
|
||||
SG_ CalibrationEnabled : 63|2@0+ (1,0) [0|3] "" XXX
|
||||
|
||||
VAL_ 513 NVMReadStatus 0 "Failed" 1 "Successful";
|
||||
VAL_ 513 NVMWriteStatus 0 "Failed" 1 "Successful";
|
||||
VAL_ 513 RadarPowerCfg 0 "Standard" 1 "-3dB Gain" 2 "-6dB Gain" 3 "-9dB Gain";
|
||||
VAL_ 513 SortIndex 0 "No Sorting" 1 "Sort By Range" 2 "Sort By RCS";
|
||||
VAL_ 513 CtrlRelayCfg 0 "Off" 1 "On";
|
||||
VAL_ 513 OutputTypeCfg 0 "None" 1 "Objects" 2 "Clusters";
|
||||
VAL_ 513 QualityInfoCfg 0 "Off" 1 "On";
|
||||
VAL_ 513 ExtInfoCfg 0 "Off" 1 "On";
|
||||
VAL_ 513 CANBaudRate 0 "500K" 1 "250K" 2 "1M";
|
||||
VAL_ 513 RCSThreshold 0 "Standard" 1 "High Sensitivity";
|
||||
VAL_ 513 CalibrationEnabled 1 "Enabled" 2 "Initial Recovery";
|
||||
|
||||
BO_ 1546 Status: 8 RADAR
|
||||
SG_ NoOfObjects : 7|8@0+ (1,0) [0|255] "" XXX
|
||||
SG_ MeasCount : 15|16@0+ (1,0) [0|65535] "" XXX
|
||||
SG_ InterfaceVersion : 31|4@0+ (1,0) [0|15] "" XXX
|
||||
|
||||
BO_ 1547 ObjectData: 8 RADAR
|
||||
SG_ ID : 7|8@0+ (1,0) [0|255] "" XXX
|
||||
SG_ DistLong : 15|13@0+ (0.2,-500) [-500|1138.2] "m" XXX
|
||||
SG_ DistLat : 18|11@0+ (0.2,-204.6) [-204.6|204.8] "m" XXX
|
||||
SG_ VRelLong : 39|10@0+ (0.25,-128) [-128|127.75] "m/s" XXX
|
||||
SG_ VRelLat : 45|9@0+ (0.25,-64) [-64|63.75] "m/s" XXX
|
||||
SG_ DynProp : 50|3@0+ (1,0) [0|7] "" XXX
|
||||
SG_ Class : 52|2@0+ (1,0) [0|3] "" XXX
|
||||
SG_ RCS : 63|8@0+ (0.5,-64) [-64|63.75] "dBm2" XXX
|
||||
|
||||
CM_ BO_ 1547 "Object detection and tracking information";
|
||||
VAL_ 1547 DynProp 0 "moving" 1 "stationary" 2 "oncoming" 3 "crossing_left" 4 "crossing_right" 5 "unknown" 6 "stopped";
|
||||
VAL_ 1547 Class 0 "point" 1 "vehicle";
|
||||
@@ -8,3 +8,4 @@ class ALTERNATIVE_EXPERIENCE:
|
||||
DISABLE_STOCK_AEB = 2
|
||||
RAISE_LONGITUDINAL_LIMITS_TO_ISO_MAX = 8
|
||||
ALLOW_AEB = 16
|
||||
ALKA = 1024 # dp - ALKA (Always-on Lane Keeping Assist)
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
static const unsigned char dlc_to_len[] = {0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 12U, 16U, 20U, 24U, 32U, 48U, 64U};
|
||||
|
||||
#define CANPACKET_HEAD_SIZE 6U // non-data portion of CANPacket_t
|
||||
#define CANPACKET_DATA_SIZE_MAX 64U
|
||||
|
||||
#ifdef CANFD
|
||||
#define CANPACKET_DATA_SIZE_MAX 64U
|
||||
#else
|
||||
#define CANPACKET_DATA_SIZE_MAX 8U
|
||||
#endif
|
||||
|
||||
// bump this when changing the CAN packet
|
||||
#define CAN_PACKET_VERSION 4
|
||||
|
||||
@@ -239,7 +239,9 @@ void update_sample(struct sample_t *sample, int sample_new);
|
||||
bool get_longitudinal_allowed(void);
|
||||
int ROUND(float val);
|
||||
void gen_crc_lookup_table_8(uint8_t poly, uint8_t crc_lut[]);
|
||||
#ifdef CANFD
|
||||
void gen_crc_lookup_table_16(uint16_t poly, uint16_t crc_lut[]);
|
||||
#endif
|
||||
bool steer_torque_cmd_checks(int desired_torque, int steer_req, const TorqueSteeringLimits limits);
|
||||
bool steer_angle_cmd_checks(int desired_angle, bool steer_control_enabled, const AngleSteeringLimits limits);
|
||||
bool steer_angle_cmd_checks_vm(int desired_angle, bool steer_control_enabled, const AngleSteeringLimits limits,
|
||||
@@ -310,8 +312,13 @@ extern struct sample_t angle_meas; // last 6 steer angles/curvatures
|
||||
// This flag allows AEB to be commanded from openpilot.
|
||||
#define ALT_EXP_ALLOW_AEB 16
|
||||
|
||||
#define ALT_EXP_ALKA 1024
|
||||
|
||||
extern int alternative_experience;
|
||||
|
||||
extern bool alka_allowed;
|
||||
extern bool lkas_on;
|
||||
|
||||
// time since safety mode has been changed
|
||||
extern uint32_t safety_mode_cnt;
|
||||
|
||||
|
||||
@@ -6,6 +6,16 @@ static const float ISO_LATERAL_ACCEL = 3.0; // m/s^2
|
||||
static const float EARTH_G = 9.81;
|
||||
static const float AVERAGE_ROAD_ROLL = 0.06; // ~3.4 degrees, 6% superelevation
|
||||
|
||||
// dp - ALKA (Always-on Lane Keeping Assist)
|
||||
// check lkas_on to know if we should allow alka or not.
|
||||
// - alka_allowed(bool): to block / allow alka (per vehicle setting in panda)
|
||||
// - alternative_experience & ALT_EXP_ALKA != 0(bool): settings from openpilot
|
||||
// - lkas_on(bool): lkas state track (use acc_main_on or other logic)
|
||||
// - vehicle_moving(bool): only allow alka when vehicle is moving
|
||||
static bool lat_control_allowed(void) {
|
||||
return controls_allowed || (alka_allowed && (alternative_experience & ALT_EXP_ALKA) != 0 && lkas_on && vehicle_moving);
|
||||
}
|
||||
|
||||
// check that commanded torque value isn't too far from measured
|
||||
static bool dist_to_meas_check(int val, int val_last, struct sample_t *val_meas,
|
||||
const int MAX_RATE_UP, const int MAX_RATE_DOWN, const int MAX_ERROR) {
|
||||
@@ -61,7 +71,7 @@ bool steer_torque_cmd_checks(int desired_torque, int steer_req, const TorqueStee
|
||||
bool violation = false;
|
||||
uint32_t ts = microsecond_timer_get();
|
||||
|
||||
if (controls_allowed) {
|
||||
if (lat_control_allowed()) {
|
||||
// Some safety models support variable torque limit based on vehicle speed
|
||||
int max_torque = limits.max_torque;
|
||||
if (limits.dynamic_max_torque) {
|
||||
@@ -96,7 +106,7 @@ bool steer_torque_cmd_checks(int desired_torque, int steer_req, const TorqueStee
|
||||
}
|
||||
|
||||
// no torque if controls is not allowed
|
||||
if (!controls_allowed && (desired_torque != 0)) {
|
||||
if (!lat_control_allowed() && (desired_torque != 0)) {
|
||||
violation = true;
|
||||
}
|
||||
|
||||
@@ -138,7 +148,7 @@ bool steer_torque_cmd_checks(int desired_torque, int steer_req, const TorqueStee
|
||||
}
|
||||
|
||||
// reset to 0 if either controls is not allowed or there's a violation
|
||||
if (violation || !controls_allowed) {
|
||||
if (violation || !lat_control_allowed()) {
|
||||
valid_steer_req_count = 0;
|
||||
invalid_steer_req_count = 0;
|
||||
desired_torque_last = 0;
|
||||
@@ -176,7 +186,7 @@ static bool rt_angle_rate_limit_check(AngleSteeringLimits limits) {
|
||||
bool steer_angle_cmd_checks(int desired_angle, bool steer_control_enabled, const AngleSteeringLimits limits) {
|
||||
bool violation = false;
|
||||
|
||||
if (controls_allowed && steer_control_enabled) {
|
||||
if (lat_control_allowed() && steer_control_enabled) {
|
||||
// convert floating point angle rate limits to integers in the scale of the desired angle on CAN,
|
||||
// add 1 to not false trigger the violation. also fudge the speed by 1 m/s so rate limits are
|
||||
// always slightly above openpilot's in case we read an updated speed in between angle commands
|
||||
@@ -262,12 +272,12 @@ bool steer_angle_cmd_checks(int desired_angle, bool steer_control_enabled, const
|
||||
}
|
||||
|
||||
// No angle control allowed when controls are not allowed
|
||||
if (!controls_allowed) {
|
||||
if (!lat_control_allowed()) {
|
||||
violation |= steer_control_enabled;
|
||||
}
|
||||
|
||||
// reset to current angle if either controls is not allowed or there's a violation
|
||||
if (violation || !controls_allowed) {
|
||||
if (violation || !lat_control_allowed()) {
|
||||
if (limits.inactive_angle_is_zero) {
|
||||
desired_angle_last = 0;
|
||||
} else {
|
||||
@@ -304,7 +314,7 @@ bool steer_angle_cmd_checks_vm(int desired_angle, bool steer_control_enabled, co
|
||||
|
||||
bool violation = false;
|
||||
|
||||
if (controls_allowed && steer_control_enabled) {
|
||||
if (lat_control_allowed() && steer_control_enabled) {
|
||||
// *** ISO lateral jerk limit ***
|
||||
// calculate maximum angle rate per second
|
||||
const float max_curvature_rate_sec = MAX_LATERAL_JERK / (fudged_speed * fudged_speed);
|
||||
@@ -340,12 +350,12 @@ bool steer_angle_cmd_checks_vm(int desired_angle, bool steer_control_enabled, co
|
||||
}
|
||||
|
||||
// No angle control allowed when controls are not allowed
|
||||
if (!controls_allowed) {
|
||||
if (!lat_control_allowed()) {
|
||||
violation |= steer_control_enabled;
|
||||
}
|
||||
|
||||
// reset to current angle if either controls is not allowed or there's a violation
|
||||
if (violation || !controls_allowed) {
|
||||
if (violation || !lat_control_allowed()) {
|
||||
desired_angle_last = SAFETY_CLAMP(angle_meas.values[0], -limits.max_angle, limits.max_angle);
|
||||
}
|
||||
|
||||
|
||||
@@ -155,8 +155,15 @@ static void ford_rx_hook(const CANPacket_t *msg) {
|
||||
brake_pressed = ((msg->data[0] >> 4) & 0x3U) == 2U;
|
||||
|
||||
// Signal: CcStat_D_Actl
|
||||
// 0=Off, 1=Denied, 2=Standby_Denied, 3=Standby, 4=Active_Que_Assist, 5=Active
|
||||
unsigned int cruise_state = msg->data[1] & 0x07U;
|
||||
bool cruise_engaged = (cruise_state == 4U) || (cruise_state == 5U);
|
||||
// dp - ALKA: Ford ACC main on when in Standby (3) or Active states (4, 5)
|
||||
acc_main_on = (cruise_state == 3U) || cruise_engaged;
|
||||
// dp - ALKA: ACC main on = ALKA enabled
|
||||
if (alka_allowed && ((alternative_experience & ALT_EXP_ALKA) != 0)) {
|
||||
lkas_on = acc_main_on;
|
||||
}
|
||||
pcm_cruise_check(cruise_engaged);
|
||||
}
|
||||
}
|
||||
@@ -283,6 +290,8 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
static safety_config ford_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for Ford
|
||||
|
||||
// warning: quality flags are not yet checked in openpilot's CAN parser,
|
||||
// this may be the cause of blocked messages
|
||||
static RxCheck ford_rx_checks[] = {
|
||||
|
||||
@@ -35,7 +35,6 @@ static bool honda_bosch_canfd = false;
|
||||
typedef enum {HONDA_NIDEC, HONDA_BOSCH} HondaHw;
|
||||
static HondaHw honda_hw = HONDA_NIDEC;
|
||||
|
||||
|
||||
static unsigned int honda_get_pt_bus(void) {
|
||||
return ((honda_hw == HONDA_BOSCH) && !honda_bosch_radarless && !honda_bosch_canfd) ? 1U : 0U;
|
||||
}
|
||||
@@ -80,6 +79,10 @@ static void honda_rx_hook(const CANPacket_t *msg) {
|
||||
// 0x326 for all Bosch and some Nidec, 0x1A6 for some Nidec
|
||||
if ((msg->addr == 0x326U) || (msg->addr == 0x1A6U)) {
|
||||
acc_main_on = GET_BIT(msg, ((msg->addr == 0x326U) ? 28U : 47U));
|
||||
// dp - ALKA: ACC main on = ALKA enabled
|
||||
if (alka_allowed && ((alternative_experience & ALT_EXP_ALKA) != 0)) {
|
||||
lkas_on = acc_main_on;
|
||||
}
|
||||
if (!acc_main_on) {
|
||||
controls_allowed = false;
|
||||
}
|
||||
@@ -238,7 +241,8 @@ static bool honda_tx_hook(const CANPacket_t *msg) {
|
||||
|
||||
// STEER: safety check
|
||||
if ((msg->addr == 0xE4U) || (msg->addr == 0x194U)) {
|
||||
if (!controls_allowed) {
|
||||
// dp - ALKA: use lat_control_allowed() instead of controls_allowed
|
||||
if (!lat_control_allowed()) {
|
||||
bool steer_applied = msg->data[0] | msg->data[1];
|
||||
if (steer_applied) {
|
||||
tx = false;
|
||||
@@ -273,6 +277,8 @@ static bool honda_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
static safety_config honda_nidec_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for Honda Nidec
|
||||
|
||||
// 0x1FA is dynamically forwarded based on stock AEB
|
||||
// 0xE4 is steering on all cars except CRV and RDX, 0x194 for CRV and RDX,
|
||||
// 0x1FA is brake control, 0x30C is acc hud, 0x33D is lkas hud
|
||||
@@ -318,6 +324,8 @@ static safety_config honda_nidec_init(uint16_t param) {
|
||||
}
|
||||
|
||||
static safety_config honda_bosch_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for Honda Bosch
|
||||
|
||||
static CanMsg HONDA_BOSCH_TX_MSGS[] = {{0xE4, 0, 5, .check_relay = true}, {0xE5, 0, 8, .check_relay = true}, {0x296, 1, 4, .check_relay = false},
|
||||
{0x33D, 0, 5, .check_relay = true}, {0x33D, 0, 8, .check_relay = true}, {0x33DA, 0, 5, .check_relay = true}, {0x33DB, 0, 8, .check_relay = true}}; // Bosch
|
||||
|
||||
|
||||
@@ -151,6 +151,9 @@ static void hyundai_rx_hook(const CANPacket_t *msg) {
|
||||
hyundai_common_cruise_buttons_check(cruise_button, main_button);
|
||||
}
|
||||
|
||||
// dp - ALKA: BCM_PO_11 (0x391) LKAS button is now handled in hyundai_rx_all_hook
|
||||
// since BCM_PO_11 is not available on all models and can't be in rx_checks
|
||||
|
||||
// gas press, different for EV, hybrid, and ICE models
|
||||
if ((msg->addr == 0x371U) && hyundai_ev_gas_signal) {
|
||||
gas_pressed = (((msg->data[4] & 0x7FU) << 1) | (msg->data[3] >> 7)) != 0U;
|
||||
@@ -249,6 +252,8 @@ static bool hyundai_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
static safety_config hyundai_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for Hyundai
|
||||
|
||||
static const CanMsg HYUNDAI_LONG_TX_MSGS[] = {
|
||||
HYUNDAI_LONG_COMMON_TX_MSGS(0)
|
||||
{0x38D, 0, 8, .check_relay = false}, // FCA11 Bus 0
|
||||
@@ -320,6 +325,8 @@ static safety_config hyundai_init(uint16_t param) {
|
||||
}
|
||||
|
||||
static safety_config hyundai_legacy_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for Hyundai Legacy
|
||||
|
||||
// older hyundai models have less checks due to missing counters and checksums
|
||||
static RxCheck hyundai_legacy_rx_checks[] = {
|
||||
HYUNDAI_COMMON_RX_CHECKS(true)
|
||||
@@ -333,9 +340,13 @@ static safety_config hyundai_legacy_init(uint16_t param) {
|
||||
return BUILD_SAFETY_CFG(hyundai_legacy_rx_checks, HYUNDAI_TX_MSGS);
|
||||
}
|
||||
|
||||
// dp - rx_ext hook for optional messages (placeholder)
|
||||
// dp - ALKA: track ACC main state (SCC11 0x420, bit 0 = MainMode_ACC)
|
||||
static void hyundai_rx_ext_hook(const CANPacket_t *msg) {
|
||||
SAFETY_UNUSED(msg);
|
||||
if (alka_allowed && ((alternative_experience & ALT_EXP_ALKA) != 0)) {
|
||||
if ((msg->addr == 0x420U) && (msg->bus == (hyundai_camera_scc ? 2U : 0U)) && !hyundai_longitudinal) {
|
||||
lkas_on = GET_BIT(msg, 0U); // ACC main on = ALKA enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const safety_hooks hyundai_hooks = {
|
||||
|
||||
@@ -131,6 +131,11 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) {
|
||||
int cruise_status = ((msg->data[8] >> 4) & 0x7U);
|
||||
bool cruise_engaged = (cruise_status == 1) || (cruise_status == 2);
|
||||
hyundai_common_cruise_state_check(cruise_engaged);
|
||||
|
||||
// dp - ALKA: track ACC main state (SCC_CONTROL 0x1A0, bit 66 = MainMode_ACC)
|
||||
if (alka_allowed && (alternative_experience & ALT_EXP_ALKA)) {
|
||||
lkas_on = GET_BIT(msg, 66U); // ACC main on = ALKA enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,6 +221,8 @@ static bool hyundai_canfd_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
static safety_config hyundai_canfd_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for Hyundai CAN-FD
|
||||
|
||||
const uint16_t HYUNDAI_PARAM_CANFD_LKA_STEERING_ALT = 128;
|
||||
const uint16_t HYUNDAI_PARAM_CANFD_ALT_BUTTONS = 32;
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ void hyundai_common_cruise_buttons_check(const int cruise_button, const bool mai
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CANFD
|
||||
uint32_t hyundai_common_canfd_compute_checksum(const CANPacket_t *msg) {
|
||||
int len = GET_LEN(msg);
|
||||
uint32_t address = msg->addr;
|
||||
@@ -136,3 +137,4 @@ uint32_t hyundai_common_canfd_compute_checksum(const CANPacket_t *msg) {
|
||||
|
||||
return crc;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -33,6 +33,11 @@ static void mazda_rx_hook(const CANPacket_t *msg) {
|
||||
// enter controls on rising edge of ACC, exit controls on ACC off
|
||||
if (msg->addr == MAZDA_CRZ_CTRL) {
|
||||
bool cruise_engaged = msg->data[0] & 0x8U;
|
||||
// dp - ALKA: CRZ_AVAILABLE (bit 17) = ACC Main ON
|
||||
acc_main_on = GET_BIT(msg, 17U);
|
||||
if (alka_allowed && (alternative_experience & ALT_EXP_ALKA)) {
|
||||
lkas_on = acc_main_on;
|
||||
}
|
||||
pcm_cruise_check(cruise_engaged);
|
||||
}
|
||||
|
||||
@@ -84,6 +89,8 @@ static bool mazda_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
static safety_config mazda_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for Mazda
|
||||
|
||||
static const CanMsg MAZDA_TX_MSGS[] = {{MAZDA_LKAS, 0, 8, .check_relay = true}, {MAZDA_CRZ_BTNS, 0, 8, .check_relay = false}, {MAZDA_LKAS_HUD, 0, 8, .check_relay = true}};
|
||||
|
||||
static RxCheck mazda_rx_checks[] = {
|
||||
|
||||
@@ -45,6 +45,22 @@ static void nissan_rx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
}
|
||||
|
||||
if (alka_allowed && ((alternative_experience & ALT_EXP_ALKA) != 0)) {
|
||||
// dp - ALKA: Leaf CRUISE_AVAILABLE (bit 17) from CRUISE_THROTTLE (0x239)
|
||||
if ((msg->addr == 0x239U) && (msg->bus == 0U)) {
|
||||
acc_main_on = GET_BIT(msg, 17U);
|
||||
lkas_on = acc_main_on;
|
||||
}
|
||||
|
||||
// dp - ALKA: X-Trail/Altima CRUISE_ON (bit 36) from PRO_PILOT (0x1B6)
|
||||
// GCOV_EXCL_START - X-Trail/Altima path requires multi-bus test infrastructure
|
||||
if ((msg->addr == 0x1B6U) && (msg->bus == (nissan_alt_eps ? 2U : 1U))) {
|
||||
acc_main_on = GET_BIT(msg, 36U);
|
||||
lkas_on = acc_main_on;
|
||||
}
|
||||
// GCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Handle cruise enabled
|
||||
if ((msg->addr == 0x30fU) && (msg->bus == (nissan_alt_eps ? 1U : 2U))) {
|
||||
bool cruise_engaged = (msg->data[0] >> 3) & 1U;
|
||||
@@ -98,6 +114,8 @@ static bool nissan_tx_hook(const CANPacket_t *msg) {
|
||||
|
||||
|
||||
static safety_config nissan_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for Nissan
|
||||
|
||||
static const CanMsg NISSAN_TX_MSGS[] = {
|
||||
{0x169, 0, 8, .check_relay = true}, // LKAS
|
||||
{0x2b1, 0, 8, .check_relay = true}, // PROPILOT_HUD
|
||||
|
||||
@@ -108,6 +108,11 @@ static void subaru_rx_hook(const CANPacket_t *msg) {
|
||||
// enter controls on rising edge of ACC, exit controls on ACC off
|
||||
if ((msg->addr == MSG_SUBARU_CruiseControl) && (msg->bus == alt_main_bus)) {
|
||||
bool cruise_engaged = (msg->data[5] >> 1) & 1U;
|
||||
// dp - ALKA: Cruise_On (bit 40) - ACC main on = ALKA enabled
|
||||
acc_main_on = GET_BIT(msg, 40U);
|
||||
if (alka_allowed && (alternative_experience & ALT_EXP_ALKA)) {
|
||||
lkas_on = acc_main_on;
|
||||
}
|
||||
pcm_cruise_check(cruise_engaged);
|
||||
}
|
||||
|
||||
@@ -204,6 +209,8 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
static safety_config subaru_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for Subaru
|
||||
|
||||
static const CanMsg SUBARU_TX_MSGS[] = {
|
||||
SUBARU_BASE_TX_MSGS(SUBARU_MAIN_BUS, MSG_SUBARU_ES_LKAS)
|
||||
SUBARU_COMMON_TX_MSGS(SUBARU_MAIN_BUS)
|
||||
|
||||
@@ -32,6 +32,11 @@ static void subaru_preglobal_rx_hook(const CANPacket_t *msg) {
|
||||
// enter controls on rising edge of ACC, exit controls on ACC off
|
||||
if (msg->addr == MSG_SUBARU_PG_CruiseControl) {
|
||||
bool cruise_engaged = (msg->data[6] >> 1) & 1U;
|
||||
// dp - ALKA: Cruise_On (bit 48) - use ACC main for LKAS state
|
||||
acc_main_on = GET_BIT(msg, 48U);
|
||||
if (alka_allowed && (alternative_experience & ALT_EXP_ALKA)) {
|
||||
lkas_on = acc_main_on;
|
||||
}
|
||||
pcm_cruise_check(cruise_engaged);
|
||||
}
|
||||
|
||||
@@ -78,6 +83,8 @@ static bool subaru_preglobal_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
static safety_config subaru_preglobal_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for Subaru preglobal
|
||||
|
||||
static const CanMsg SUBARU_PG_TX_MSGS[] = {
|
||||
{MSG_SUBARU_PG_ES_Distance, SUBARU_PG_MAIN_BUS, 8, .check_relay = true},
|
||||
{MSG_SUBARU_PG_ES_LKAS, SUBARU_PG_MAIN_BUS, 8, .check_relay = true}
|
||||
|
||||
@@ -62,6 +62,9 @@ static bool toyota_stock_longitudinal = false;
|
||||
static bool toyota_lta = false;
|
||||
static int toyota_dbc_eps_torque_factor = 100; // conversion factor for STEER_TORQUE_EPS in %: see dbc file
|
||||
|
||||
// dp - use DSU_CRUISE (0x365) for ACC main instead of PCM_CRUISE_2 (0x1D3)
|
||||
static bool toyota_unsupported_dsu = false;
|
||||
|
||||
static uint32_t toyota_compute_checksum(const CANPacket_t *msg) {
|
||||
int len = GET_LEN(msg);
|
||||
uint8_t checksum = (uint8_t)(msg->addr) + (uint8_t)((unsigned int)(msg->addr) >> 8U) + (uint8_t)(len);
|
||||
@@ -344,6 +347,8 @@ static bool toyota_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
static safety_config toyota_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for Toyota
|
||||
|
||||
static const CanMsg TOYOTA_TX_MSGS[] = {
|
||||
TOYOTA_COMMON_TX_MSGS
|
||||
};
|
||||
@@ -373,6 +378,10 @@ static safety_config toyota_init(uint16_t param) {
|
||||
toyota_secoc = GET_FLAG(param, TOYOTA_PARAM_SECOC);
|
||||
#endif
|
||||
|
||||
// dp - use DSU_CRUISE (0x365) for ACC main instead of PCM_CRUISE_2 (0x1D3)
|
||||
const uint32_t TOYOTA_PARAM_UNSUPPORTED_DSU = 16UL << TOYOTA_PARAM_OFFSET;
|
||||
toyota_unsupported_dsu = GET_FLAG(param, TOYOTA_PARAM_UNSUPPORTED_DSU);
|
||||
|
||||
toyota_alt_brake = GET_FLAG(param, TOYOTA_PARAM_ALT_BRAKE);
|
||||
toyota_stock_longitudinal = GET_FLAG(param, TOYOTA_PARAM_STOCK_LONGITUDINAL);
|
||||
toyota_lta = GET_FLAG(param, TOYOTA_PARAM_LTA);
|
||||
@@ -426,7 +435,21 @@ static safety_config toyota_init(uint16_t param) {
|
||||
|
||||
// dp - rx_ext hook for optional messages (placeholder)
|
||||
static void toyota_rx_ext_hook(const CANPacket_t *msg) {
|
||||
SAFETY_UNUSED(msg);
|
||||
if (alka_allowed && ((alternative_experience & ALT_EXP_ALKA) != 0)) {
|
||||
if (toyota_unsupported_dsu) {
|
||||
// DSU_CRUISE (0x365) - Lexus IS, GS_F, RC and other DSU-based cars
|
||||
if ((msg->addr == 0x365U) && (msg->bus == 0U)) {
|
||||
acc_main_on = GET_BIT(msg, 0U);
|
||||
lkas_on = acc_main_on;
|
||||
}
|
||||
} else {
|
||||
// PCM_CRUISE_2 (0x1D3) - most Toyotas (default)
|
||||
if ((msg->addr == 0x1D3U) && (msg->bus == 0U)) {
|
||||
acc_main_on = GET_BIT(msg, 15U);
|
||||
lkas_on = acc_main_on;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const safety_hooks toyota_hooks = {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "opendbc/safety/modes/volkswagen_common.h"
|
||||
|
||||
static safety_config volkswagen_mqb_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for VW MQB
|
||||
|
||||
// Transmit of GRA_ACC_01 is allowed on bus 0 and 2 to keep compatibility with gateway and camera integration
|
||||
// MSG_LH_EPS_03: openpilot needs to replace apparent driver steering input torque to pacify VW Emergency Assist
|
||||
static const CanMsg VOLKSWAGEN_MQB_STOCK_TX_MSGS[] = {{MSG_HCA_01, 0, 8, .check_relay = true}, {MSG_GRA_ACC_01, 0, 8, .check_relay = false}, {MSG_GRA_ACC_01, 2, 8, .check_relay = false},
|
||||
@@ -62,6 +64,10 @@ static void volkswagen_mqb_rx_hook(const CANPacket_t *msg) {
|
||||
int acc_status = (msg->data[3] & 0x7U);
|
||||
bool cruise_engaged = (acc_status == 3) || (acc_status == 4) || (acc_status == 5);
|
||||
acc_main_on = cruise_engaged || (acc_status == 2);
|
||||
// dp - ALKA: use ACC main for LKAS state
|
||||
if (alka_allowed && (alternative_experience & ALT_EXP_ALKA)) {
|
||||
lkas_on = acc_main_on;
|
||||
}
|
||||
|
||||
if (!volkswagen_longitudinal) {
|
||||
pcm_cruise_check(cruise_engaged);
|
||||
|
||||
@@ -47,6 +47,8 @@ static uint32_t volkswagen_pq_compute_checksum(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
static safety_config volkswagen_pq_init(uint16_t param) {
|
||||
alka_allowed = true; // dp - ALKA enabled for VW PQ
|
||||
|
||||
// Transmit of GRA_Neu is allowed on bus 0 and 2 to keep compatibility with gateway and camera integration
|
||||
static const CanMsg VOLKSWAGEN_PQ_STOCK_TX_MSGS[] = {{MSG_HCA_1, 0, 5, .check_relay = true}, {MSG_LDW_1, 0, 8, .check_relay = true},
|
||||
{MSG_GRA_NEU, 0, 4, .check_relay = false}, {MSG_GRA_NEU, 2, 4, .check_relay = false}};
|
||||
@@ -95,16 +97,20 @@ static void volkswagen_pq_rx_hook(const CANPacket_t *msg) {
|
||||
update_sample(&torque_driver, torque_driver_new);
|
||||
}
|
||||
|
||||
if (volkswagen_longitudinal) {
|
||||
if (msg->addr == MSG_MOTOR_5) {
|
||||
// ACC main switch on is a prerequisite to enter controls, exit controls immediately on main switch off
|
||||
// Signal: Motor_5.MO5_GRA_Hauptsch
|
||||
acc_main_on = GET_BIT(msg, 50U);
|
||||
if (!acc_main_on) {
|
||||
controls_allowed = false;
|
||||
}
|
||||
// ACC main switch tracking - applies to both longitudinal and non-longitudinal modes
|
||||
// Signal: Motor_5.MO5_GRA_Hauptsch
|
||||
if (msg->addr == MSG_MOTOR_5) {
|
||||
acc_main_on = GET_BIT(msg, 50U);
|
||||
// dp - ALKA: use ACC main for LKAS state
|
||||
if (alka_allowed && (alternative_experience & ALT_EXP_ALKA)) {
|
||||
lkas_on = acc_main_on;
|
||||
}
|
||||
if (!acc_main_on) {
|
||||
controls_allowed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (volkswagen_longitudinal) {
|
||||
if (msg->addr == MSG_GRA_NEU) {
|
||||
// If ACC main switch is on, enter controls on falling edge of Set or Resume
|
||||
// Signal: GRA_Neu.GRA_Neu_Setzen
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
#include "opendbc/safety/modes/elm327.h"
|
||||
#include "opendbc/safety/modes/body.h"
|
||||
#include "opendbc/safety/modes/psa.h"
|
||||
|
||||
#ifdef CANFD
|
||||
#include "opendbc/safety/modes/hyundai_canfd.h"
|
||||
#endif
|
||||
|
||||
uint32_t GET_BYTES(const CANPacket_t *msg, int start, int len) {
|
||||
uint32_t ret = 0U;
|
||||
@@ -57,6 +60,10 @@ bool acc_main_on = false; // referred to as "ACC off" in ISO 15622:2018
|
||||
int cruise_button_prev = 0;
|
||||
bool safety_rx_checks_invalid = false;
|
||||
|
||||
// dp - alka
|
||||
bool alka_allowed = false;
|
||||
bool lkas_on = false;
|
||||
|
||||
// for safety modes with torque steering control
|
||||
int desired_torque_last = 0; // last desired steer torque
|
||||
int rt_torque_last = 0; // last desired torque for real time check
|
||||
@@ -337,6 +344,7 @@ void gen_crc_lookup_table_8(uint8_t poly, uint8_t crc_lut[]) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CANFD
|
||||
void gen_crc_lookup_table_16(uint16_t poly, uint16_t crc_lut[]) {
|
||||
for (uint16_t i = 0; i < 256U; i++) {
|
||||
uint16_t crc = i << 8U;
|
||||
@@ -350,6 +358,7 @@ void gen_crc_lookup_table_16(uint16_t poly, uint16_t crc_lut[]) {
|
||||
crc_lut[i] = crc;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// 1Hz safety function called by main. Now just a check for lagging safety messages
|
||||
void safety_tick(const safety_config *cfg) {
|
||||
@@ -446,7 +455,9 @@ int set_safety_hooks(uint16_t mode, uint16_t param) {
|
||||
{SAFETY_FORD, &ford_hooks},
|
||||
{SAFETY_RIVIAN, &rivian_hooks},
|
||||
{SAFETY_TESLA, &tesla_hooks},
|
||||
#ifdef CANFD
|
||||
{SAFETY_HYUNDAI_CANFD, &hyundai_canfd_hooks},
|
||||
#endif
|
||||
#ifdef ALLOW_DEBUG
|
||||
{SAFETY_PSA, &psa_hooks},
|
||||
{SAFETY_SUBARU_PREGLOBAL, &subaru_preglobal_hooks},
|
||||
@@ -481,6 +492,10 @@ int set_safety_hooks(uint16_t mode, uint16_t param) {
|
||||
valid_steer_req_count = 0;
|
||||
invalid_steer_req_count = 0;
|
||||
|
||||
// dp - alka
|
||||
alka_allowed = false;
|
||||
lkas_on = false;
|
||||
|
||||
// reset samples
|
||||
reset_sample(&vehicle_speed);
|
||||
reset_sample(&torque_meas);
|
||||
|
||||
@@ -29,6 +29,7 @@ env = Environment(
|
||||
'-fprofile-arcs',
|
||||
'-ftest-coverage',
|
||||
'-DALLOW_DEBUG',
|
||||
'-DCANFD',
|
||||
],
|
||||
LINKFLAGS=[
|
||||
'-fprofile-arcs',
|
||||
|
||||
@@ -42,7 +42,12 @@ void set_gas_pressed_prev(bool);
|
||||
bool get_brake_pressed_prev(void);
|
||||
bool get_regen_braking_prev(void);
|
||||
bool get_steering_disengage_prev(void);
|
||||
void set_steering_disengage(bool c);
|
||||
bool get_acc_main_on(void);
|
||||
bool get_alka_allowed(void);
|
||||
bool get_lkas_on(void);
|
||||
void set_lkas_on(bool c);
|
||||
bool get_lat_control_allowed(void);
|
||||
float get_vehicle_speed_min(void);
|
||||
float get_vehicle_speed_max(void);
|
||||
int get_current_safety_mode(void);
|
||||
|
||||
@@ -77,6 +77,11 @@ bool get_steering_disengage_prev(void){
|
||||
return steering_disengage_prev;
|
||||
}
|
||||
|
||||
// dp - ALKA: setter for steering_disengage to test driver override
|
||||
void set_steering_disengage(bool c){
|
||||
steering_disengage = c;
|
||||
}
|
||||
|
||||
bool get_cruise_engaged_prev(void){
|
||||
return cruise_engaged_prev;
|
||||
}
|
||||
@@ -93,6 +98,23 @@ bool get_acc_main_on(void){
|
||||
return acc_main_on;
|
||||
}
|
||||
|
||||
// dp - ALKA getters/setters
|
||||
bool get_alka_allowed(void){
|
||||
return alka_allowed;
|
||||
}
|
||||
|
||||
bool get_lkas_on(void){
|
||||
return lkas_on;
|
||||
}
|
||||
|
||||
void set_lkas_on(bool c){
|
||||
lkas_on = c;
|
||||
}
|
||||
|
||||
bool get_lat_control_allowed(void){
|
||||
return lat_control_allowed();
|
||||
}
|
||||
|
||||
float get_vehicle_speed_min(void){
|
||||
return vehicle_speed.min / VEHICLE_SPEED_FACTOR;
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ cppcheck() {
|
||||
|
||||
OPTS=" --enable=all --enable=unusedFunction --addon=misra"
|
||||
|
||||
printf "\n${GREEN}** Safety **${NC}\n"
|
||||
cppcheck $OPTS $BASEDIR/opendbc/safety/tests/misra/main.c
|
||||
printf "\n${GREEN}** Safety with CANFD **${NC}\n"
|
||||
cppcheck $OPTS -DCANFD $BASEDIR/opendbc/safety/tests/misra/main.c
|
||||
|
||||
printf "\n${GREEN}Success!${NC} took $SECONDS seconds\n"
|
||||
|
||||
|
||||
862
opendbc_repo/opendbc/safety/tests/test_alka.py
Normal file
862
opendbc_repo/opendbc/safety/tests/test_alka.py
Normal file
@@ -0,0 +1,862 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for ALKA (Always-on Lane Keeping Assist) feature.
|
||||
|
||||
ALKA allows lateral control when:
|
||||
1. alka_allowed is set for the brand
|
||||
2. ALT_EXP_ALKA flag is set in alternative_experience
|
||||
3. lkas_on is true (follows acc_main_on directly)
|
||||
4. vehicle_moving is true
|
||||
|
||||
Simplified behavior (v2):
|
||||
- All brands now use direct tracking: lkas_on = acc_main_on
|
||||
- No button tracking (TJA, LKAS button, LKAS HUD)
|
||||
- ACC main ON = ALKA enabled, ACC main OFF = ALKA disabled
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.toyota.values import ToyotaSafetyFlags
|
||||
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from opendbc.safety.tests.common import CANPackerSafety
|
||||
|
||||
|
||||
class TestALKABase(unittest.TestCase):
|
||||
"""Base test class for ALKA functionality."""
|
||||
|
||||
TX_MSGS = []
|
||||
|
||||
safety: libsafety_py.LibSafety
|
||||
packer: CANPackerSafety
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if cls.__name__ == "TestALKABase":
|
||||
raise unittest.SkipTest("Base class")
|
||||
|
||||
def _reset_safety_hooks(self):
|
||||
self.safety.set_safety_hooks(self.safety.get_current_safety_mode(),
|
||||
self.safety.get_current_safety_param())
|
||||
|
||||
def _rx(self, msg):
|
||||
return self.safety.safety_rx_hook(msg)
|
||||
|
||||
def _tx(self, msg):
|
||||
return self.safety.safety_tx_hook(msg)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
"""Override in subclass to set vehicle moving state via CAN message."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||
"""Override in subclass to create torque command message."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TestALKAToyota(TestALKABase):
|
||||
"""Test ALKA functionality for Toyota."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("toyota_nodsu_pt_generated")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.toyota, 73)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {("WHEEL_SPEED_%s" % n): speed * 3.6 for n in ["FR", "FL", "RR", "RL"]}
|
||||
return self.packer.make_can_msg_safety("WHEEL_SPEEDS", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||
values = {"STEER_TORQUE_CMD": torque, "STEER_REQUEST": steer_req}
|
||||
return self.packer.make_can_msg_safety("STEERING_LKA", 0, values)
|
||||
|
||||
def _torque_meas_msg(self, torque):
|
||||
values = {"STEER_TORQUE_EPS": (torque / 73) * 100.}
|
||||
return self.packer.make_can_msg_safety("STEER_TORQUE_SENSOR", 0, values)
|
||||
|
||||
def _acc_main_msg(self, main_on):
|
||||
"""Create raw ACC main message for Toyota (PCM_CRUISE_2 0x1D3, bit 15)."""
|
||||
dat = bytearray(8)
|
||||
if main_on:
|
||||
dat[1] = 0x80 # bit 15 = byte 1, bit 7
|
||||
s = 8 + 0x1D3 + ((0x1D3 >> 8) & 0xFF)
|
||||
for i in range(7):
|
||||
s += dat[i]
|
||||
dat[7] = s & 0xFF
|
||||
return libsafety_py.make_CANPacket(0x1D3, 0, bytes(dat))
|
||||
|
||||
def _set_prev_torque(self, t):
|
||||
self.safety.set_desired_torque_last(t)
|
||||
self.safety.set_rt_torque_last(t)
|
||||
self.safety.set_torque_meas(t, t)
|
||||
|
||||
def test_alka_allowed_for_toyota(self):
|
||||
"""Verify alka_allowed flag is set in Toyota safety mode init."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
def test_alka_lkas_on_from_acc_main(self):
|
||||
"""Verify lkas_on tracks ACC main state directly for Toyota."""
|
||||
# Initially off
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
self.assertFalse(self.safety.get_acc_main_on())
|
||||
|
||||
# Turn on
|
||||
self._rx(self._acc_main_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
self.assertTrue(self.safety.get_acc_main_on())
|
||||
|
||||
# Turn off
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
self.assertFalse(self.safety.get_acc_main_on())
|
||||
|
||||
def test_alka_lat_control_allowed_conditions(self):
|
||||
"""Verify lat_control_allowed requires all ALKA conditions: flag + lkas_on + moving."""
|
||||
self._reset_safety_hooks()
|
||||
self.safety.set_controls_allowed(False)
|
||||
|
||||
# Without ALKA flag, lat_control_allowed = controls_allowed
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.DEFAULT)
|
||||
self._rx(self._acc_main_msg(1))
|
||||
self._set_vehicle_moving(True)
|
||||
self.assertFalse(self.safety.get_lat_control_allowed())
|
||||
|
||||
# With ALKA flag but vehicle not moving
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
self._set_vehicle_moving(False)
|
||||
self.assertFalse(self.safety.get_lat_control_allowed())
|
||||
|
||||
# With ALKA flag but lkas_on is false
|
||||
self._set_vehicle_moving(True)
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lat_control_allowed())
|
||||
|
||||
# All conditions met: ALKA flag + lkas_on + vehicle_moving
|
||||
self._rx(self._acc_main_msg(1))
|
||||
self._set_vehicle_moving(True)
|
||||
self.assertTrue(self.safety.get_lat_control_allowed())
|
||||
|
||||
def test_alka_allows_steering_without_controls_allowed(self):
|
||||
"""Verify torque TX is allowed via ALKA even when controls_allowed=false."""
|
||||
self._reset_safety_hooks()
|
||||
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
self._rx(self._acc_main_msg(1))
|
||||
self._set_vehicle_moving(True)
|
||||
self.safety.set_controls_allowed(False)
|
||||
|
||||
self._set_prev_torque(0)
|
||||
for _ in range(6):
|
||||
self._rx(self._torque_meas_msg(0))
|
||||
self.assertTrue(self._tx(self._torque_cmd_msg(10, steer_req=1)))
|
||||
|
||||
def test_alka_disabled_when_acc_main_off(self):
|
||||
"""Verify torque TX is blocked when ACC main turns off."""
|
||||
self._reset_safety_hooks()
|
||||
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
self._rx(self._acc_main_msg(1))
|
||||
self._set_vehicle_moving(True)
|
||||
self.safety.set_controls_allowed(False)
|
||||
|
||||
# Turn off ACC main
|
||||
self._rx(self._acc_main_msg(0))
|
||||
|
||||
self._set_prev_torque(0)
|
||||
for _ in range(6):
|
||||
self._rx(self._torque_meas_msg(0))
|
||||
self.assertFalse(self._tx(self._torque_cmd_msg(10, steer_req=1)))
|
||||
|
||||
def test_alka_disabled_when_vehicle_stopped(self):
|
||||
"""Verify torque TX is blocked when vehicle stops."""
|
||||
self._reset_safety_hooks()
|
||||
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
self._rx(self._acc_main_msg(1))
|
||||
self._set_vehicle_moving(True)
|
||||
self.safety.set_controls_allowed(False)
|
||||
|
||||
self._set_vehicle_moving(False)
|
||||
|
||||
self._set_prev_torque(0)
|
||||
for _ in range(6):
|
||||
self._rx(self._torque_meas_msg(0))
|
||||
self.assertFalse(self._tx(self._torque_cmd_msg(10, steer_req=1)))
|
||||
self.assertTrue(self._tx(self._torque_cmd_msg(0, steer_req=1)))
|
||||
|
||||
def test_alka_reset_on_safety_init(self):
|
||||
"""Verify lkas_on resets to false on safety mode re-initialization."""
|
||||
self.safety.set_lkas_on(True)
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
self._reset_safety_hooks()
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKAToyotaDSU(TestALKABase):
|
||||
"""Test ALKA functionality for Toyota with ACC_MAIN_DSU flag."""
|
||||
|
||||
EPS_SCALE = 73
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("toyota_nodsu_pt_generated")
|
||||
self.safety = libsafety_py.libsafety
|
||||
param = self.EPS_SCALE | ToyotaSafetyFlags.UNSUPPORTED_DSU
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.toyota, param)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
self.safety.set_lkas_on(False)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {("WHEEL_SPEED_%s" % n): speed * 3.6 for n in ["FR", "FL", "RR", "RL"]}
|
||||
return self.packer.make_can_msg_safety("WHEEL_SPEEDS", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||
values = {"STEER_TORQUE_CMD": torque, "STEER_REQUEST": steer_req}
|
||||
return self.packer.make_can_msg_safety("STEERING_LKA", 0, values)
|
||||
|
||||
def _dsu_cruise_msg(self, main_on):
|
||||
"""Create DSU_CRUISE message (0x365, bit 0 = MAIN_ON)."""
|
||||
dat = bytearray(7)
|
||||
if main_on:
|
||||
dat[0] = 0x01
|
||||
s = 7 + 0x365 + ((0x365 >> 8) & 0xFF)
|
||||
for i in range(6):
|
||||
s += dat[i]
|
||||
dat[6] = s & 0xFF
|
||||
return libsafety_py.make_CANPacket(0x365, 0, bytes(dat))
|
||||
|
||||
def _pcm_cruise_2_msg(self, main_on):
|
||||
"""Create PCM_CRUISE_2 message (0x1D3, bit 15 = MAIN_ON)."""
|
||||
dat = bytearray(8)
|
||||
if main_on:
|
||||
dat[1] = 0x80
|
||||
s = 8 + 0x1D3 + ((0x1D3 >> 8) & 0xFF)
|
||||
for i in range(7):
|
||||
s += dat[i]
|
||||
dat[7] = s & 0xFF
|
||||
return libsafety_py.make_CANPacket(0x1D3, 0, bytes(dat))
|
||||
|
||||
def test_acc_main_dsu_flag_uses_0x365(self):
|
||||
"""With ACC_MAIN_DSU flag, lkas_on should track DSU_CRUISE (0x365)."""
|
||||
self._rx(self._dsu_cruise_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._dsu_cruise_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._dsu_cruise_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
def test_acc_main_dsu_ignores_0x1d3(self):
|
||||
"""With ACC_MAIN_DSU flag, PCM_CRUISE_2 (0x1D3) should be ignored."""
|
||||
self._rx(self._pcm_cruise_2_msg(1))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._dsu_cruise_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKAHyundai(TestALKABase):
|
||||
"""Test ALKA functionality for Hyundai (Main only tracking)."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("hyundai_kia_generic")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"WHL_SPD_FL": speed, "WHL_SPD_FR": speed, "WHL_SPD_RL": speed, "WHL_SPD_RR": speed}
|
||||
return self.packer.make_can_msg_safety("WHL_SPD11", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||
values = {"CR_Lkas_StrToqReq": torque, "CF_Lkas_ActToi": steer_req}
|
||||
return self.packer.make_can_msg_safety("LKAS11", 0, values)
|
||||
|
||||
def _acc_main_msg(self, main_on):
|
||||
"""Create ACC main message (SCC11 0x420, bit 0 = MainMode_ACC)."""
|
||||
values = {"MainMode_ACC": main_on}
|
||||
return self.packer.make_can_msg_safety("SCC11", 0, values)
|
||||
|
||||
def test_alka_allowed_for_hyundai(self):
|
||||
"""Verify alka_allowed flag is set in Hyundai safety mode init."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
def test_alka_lkas_on_from_acc_main(self):
|
||||
"""Verify lkas_on tracks ACC main state directly (SCC11 0x420)."""
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._acc_main_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKAHyundaiCameraSCC(TestALKABase):
|
||||
"""Test ALKA functionality for Hyundai with camera SCC (ACC main on bus 2)."""
|
||||
|
||||
HYUNDAI_PARAM_CAMERA_SCC = 8
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("hyundai_kia_generic")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, self.HYUNDAI_PARAM_CAMERA_SCC)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"WHL_SPD_FL": speed, "WHL_SPD_FR": speed, "WHL_SPD_RL": speed, "WHL_SPD_RR": speed}
|
||||
return self.packer.make_can_msg_safety("WHL_SPD11", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _acc_main_msg(self, main_on, bus):
|
||||
"""Create ACC main message (SCC11 0x420, bit 0 = MainMode_ACC)."""
|
||||
dat = bytearray(8)
|
||||
if main_on:
|
||||
dat[0] = 0x01
|
||||
return libsafety_py.make_CANPacket(0x420, bus, bytes(dat))
|
||||
|
||||
def test_alka_camera_scc_uses_bus_2(self):
|
||||
"""With camera_scc, ACC main on bus 2 controls lkas_on."""
|
||||
# Bus 0 should be ignored
|
||||
self._rx(self._acc_main_msg(1, bus=0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
# Bus 2 should work
|
||||
self._rx(self._acc_main_msg(1, bus=2))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._acc_main_msg(0, bus=2))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKAHonda(TestALKABase):
|
||||
"""Test ALKA functionality for Honda (Main only tracking)."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("honda_civic_touring_2016_can_generated")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.hondaNidec, 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"XMISSION_SPEED": speed * 3.6}
|
||||
return self.packer.make_can_msg_safety("ENGINE_DATA", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _main_on_msg(self, main_on):
|
||||
"""Create ACC main message (SCM_FEEDBACK 0x326, bit 28 = MAIN_ON)."""
|
||||
values = {"MAIN_ON": main_on}
|
||||
return self.packer.make_can_msg_safety("SCM_FEEDBACK", 0, values)
|
||||
|
||||
def test_alka_allowed_for_honda(self):
|
||||
"""Honda should have alka_allowed set to true."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
def test_alka_lkas_on_from_acc_main(self):
|
||||
"""lkas_on should follow acc_main_on directly for Honda."""
|
||||
self._rx(self._main_on_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._main_on_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._main_on_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKAHondaBosch(TestALKABase):
|
||||
"""Test ALKA functionality for Honda Bosch."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("honda_civic_hatchback_ex_2017_can_generated")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.hondaBosch, 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"XMISSION_SPEED": speed * 3.6}
|
||||
return self.packer.make_can_msg_safety("ENGINE_DATA", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def test_alka_allowed_for_honda_bosch(self):
|
||||
"""Honda Bosch should have alka_allowed set to true."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
|
||||
class TestALKAHyundaiCanfd(TestALKABase):
|
||||
"""Test ALKA functionality for Hyundai CAN-FD (Main only tracking)."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("hyundai_canfd_generated")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"WHEEL_SPEED_1": speed, "WHEEL_SPEED_2": speed, "WHEEL_SPEED_3": speed, "WHEEL_SPEED_4": speed}
|
||||
return self.packer.make_can_msg_safety("WHEEL_SPEEDS", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _acc_main_msg(self, main_on):
|
||||
"""Create ACC main message for CAN-FD (SCC_CONTROL 0x1A0, bit 66 = MainMode_ACC)."""
|
||||
values = {"MainMode_ACC": main_on, "ACCMode": 0}
|
||||
return self.packer.make_can_msg_safety("SCC_CONTROL", 0, values)
|
||||
|
||||
def test_alka_allowed_for_hyundai_canfd(self):
|
||||
"""Hyundai CAN-FD should have alka_allowed set to true."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
def test_alka_lkas_on_from_acc_main(self):
|
||||
"""lkas_on should track ACC main state directly (SCC_CONTROL 0x1A0)."""
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._acc_main_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKAHyundaiLegacy(TestALKABase):
|
||||
"""Test ALKA functionality for Hyundai Legacy (Main only tracking)."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("hyundai_kia_generic")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.hyundaiLegacy, 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"WHL_SPD_FL": speed, "WHL_SPD_FR": speed, "WHL_SPD_RL": speed, "WHL_SPD_RR": speed}
|
||||
return self.packer.make_can_msg_safety("WHL_SPD11", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _acc_main_msg(self, main_on):
|
||||
"""Create ACC main message (SCC11 0x420, bit 0 = MainMode_ACC)."""
|
||||
values = {"MainMode_ACC": main_on}
|
||||
return self.packer.make_can_msg_safety("SCC11", 0, values)
|
||||
|
||||
def test_alka_allowed_for_hyundai_legacy(self):
|
||||
"""Hyundai Legacy should have alka_allowed set to true."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
def test_alka_lkas_on_from_acc_main(self):
|
||||
"""lkas_on should track ACC main state directly."""
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._acc_main_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKAVolkswagenMQB(TestALKABase):
|
||||
"""Test ALKA functionality for Volkswagen MQB."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("vw_mqb")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.volkswagen, 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"ESP_VL_Radgeschw_02": speed * 3.6}
|
||||
return self.packer.make_can_msg_safety("ESP_19", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _tsk_status_msg(self, status):
|
||||
"""TSK_Status: 2=standby/main on, 3=enabled, 0=off."""
|
||||
values = {"TSK_Status": status}
|
||||
return self.packer.make_can_msg_safety("TSK_06", 0, values)
|
||||
|
||||
def test_alka_allowed_for_vw(self):
|
||||
"""VW should have alka_allowed set to true."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
def test_alka_lkas_on_from_acc_main(self):
|
||||
"""lkas_on should follow acc_main_on for VW."""
|
||||
self._rx(self._tsk_status_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._tsk_status_msg(2)) # Standby
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._tsk_status_msg(3)) # Enabled
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKAVolkswagenPQ(TestALKABase):
|
||||
"""Test ALKA functionality for Volkswagen PQ."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("vw_pq")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.volkswagenPq, 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"Geschwindigkeit_neu__Bremse_1_": speed * 3.6}
|
||||
return self.packer.make_can_msg_safety("Bremse_1", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _acc_main_msg(self, main_on):
|
||||
"""Create Motor_5 message (0x480) with ACC main (bit 50 = MO5_GRA_Hauptsch)."""
|
||||
values = {"MO5_GRA_Hauptsch": main_on}
|
||||
return self.packer.make_can_msg_safety("Motor_5", 0, values)
|
||||
|
||||
def test_alka_allowed_for_vw_pq(self):
|
||||
"""VW PQ should have alka_allowed set to true."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
def test_alka_lkas_on_from_acc_main(self):
|
||||
"""lkas_on should follow acc_main_on for VW PQ."""
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._acc_main_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKASubaru(TestALKABase):
|
||||
"""Test ALKA functionality for Subaru (Main only tracking)."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("subaru_global_2017_generated")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.subaru, 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"FR": speed, "FL": speed, "RR": speed, "RL": speed}
|
||||
return self.packer.make_can_msg_safety("Wheel_Speeds", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _cruise_msg(self, main_on):
|
||||
"""CruiseControl message for ACC main."""
|
||||
values = {"Cruise_On": main_on}
|
||||
return self.packer.make_can_msg_safety("CruiseControl", 0, values)
|
||||
|
||||
def test_alka_allowed_for_subaru(self):
|
||||
"""Subaru should have alka_allowed set to true."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
def test_alka_lkas_on_from_acc_main(self):
|
||||
"""lkas_on should track ACC main state directly."""
|
||||
self._rx(self._cruise_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._cruise_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._cruise_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKASubaruPreglobal(TestALKABase):
|
||||
"""Test ALKA functionality for Subaru Preglobal."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("subaru_outback_2015_generated")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.subaruPreglobal, 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"FR": speed, "FL": speed, "RR": speed, "RL": speed}
|
||||
return self.packer.make_can_msg_safety("Wheel_Speeds", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _cruise_msg(self, main_on):
|
||||
"""CruiseControl message for ACC main."""
|
||||
values = {"Cruise_On": main_on}
|
||||
return self.packer.make_can_msg_safety("CruiseControl", 0, values)
|
||||
|
||||
def test_alka_allowed_for_subaru_preglobal(self):
|
||||
"""Subaru Preglobal should have alka_allowed set to true."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
def test_alka_lkas_on_from_acc_main(self):
|
||||
"""lkas_on should follow acc_main_on for Subaru Preglobal."""
|
||||
self._rx(self._cruise_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._cruise_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._cruise_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKAMazda(TestALKABase):
|
||||
"""Test ALKA functionality for Mazda."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("mazda_2017")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.mazda, 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"SPEED": speed * 3.6}
|
||||
return self.packer.make_can_msg_safety("ENGINE_DATA", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _crz_ctrl_msg(self, main_on):
|
||||
"""Create CRZ_CTRL message (0x21C) with ACC main (bit 17 = CRZ_AVAILABLE)."""
|
||||
dat = bytearray(8)
|
||||
if main_on:
|
||||
dat[2] = 0x02
|
||||
return libsafety_py.make_CANPacket(0x21C, 0, bytes(dat))
|
||||
|
||||
def test_alka_allowed_for_mazda(self):
|
||||
"""Mazda should have alka_allowed set to true."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
def test_alka_lkas_on_from_acc_main(self):
|
||||
"""lkas_on should follow acc_main_on for Mazda."""
|
||||
self._rx(self._crz_ctrl_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._crz_ctrl_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._crz_ctrl_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKANissan(TestALKABase):
|
||||
"""Test ALKA functionality for Nissan."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("nissan_leaf_2018_generated")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.nissan, 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"WHEEL_SPEED_RR": speed * 3.6, "WHEEL_SPEED_RL": speed * 3.6}
|
||||
return self.packer.make_can_msg_safety("WHEEL_SPEEDS_REAR", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
self._rx(self._speed_msg(speed))
|
||||
|
||||
def _acc_main_msg(self, main_on):
|
||||
"""Create Leaf ACC main message (CRUISE_THROTTLE 0x239, bit 17)."""
|
||||
dat = bytearray(8)
|
||||
if main_on:
|
||||
dat[2] = 0x02
|
||||
return libsafety_py.make_CANPacket(0x239, 0, bytes(dat))
|
||||
|
||||
def test_alka_allowed_for_nissan(self):
|
||||
"""Nissan should have alka_allowed set to true."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
def test_alka_lkas_on_from_acc_main(self):
|
||||
"""lkas_on should track ACC main for Nissan."""
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._acc_main_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKAFord(TestALKABase):
|
||||
"""Test ALKA functionality for Ford (Main only tracking)."""
|
||||
|
||||
def setUp(self):
|
||||
self.packer = CANPackerSafety("ford_lincoln_base_pt")
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.ford, 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALKA)
|
||||
|
||||
def _speed_msg(self, speed):
|
||||
values = {"VehYaw_W_Actl": 0, "VehSpd_D_Actl": 0}
|
||||
return self.packer.make_can_msg_safety("Yaw_Data_FD1", 0, values)
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
values = {"WhlFl_W_Meas": 10.0 if moving else 0.0,
|
||||
"WhlFr_W_Meas": 10.0 if moving else 0.0,
|
||||
"WhlRl_W_Meas": 10.0 if moving else 0.0,
|
||||
"WhlRr_W_Meas": 10.0 if moving else 0.0}
|
||||
for _ in range(6):
|
||||
self._rx(self.packer.make_can_msg_safety("WheelSpeed_CG1", 0, values))
|
||||
|
||||
def _acc_main_msg(self, main_on):
|
||||
"""EngBrakeData message for ACC main (CcStat)."""
|
||||
values = {"CcStat_D_Actl": 3 if main_on else 0}
|
||||
return self.packer.make_can_msg_safety("EngBrakeData", 0, values)
|
||||
|
||||
def _torque_cmd_msg(self, torque, steer_req=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def test_alka_allowed_for_ford(self):
|
||||
"""Ford should have alka_allowed set to true."""
|
||||
self.assertTrue(self.safety.get_alka_allowed())
|
||||
|
||||
def test_alka_lkas_on_from_acc_main(self):
|
||||
"""lkas_on should track ACC main state directly for Ford."""
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._acc_main_msg(1))
|
||||
self.assertTrue(self.safety.get_lkas_on())
|
||||
|
||||
self._rx(self._acc_main_msg(0))
|
||||
self.assertFalse(self.safety.get_lkas_on())
|
||||
|
||||
|
||||
class TestALKADisabledBrands(unittest.TestCase):
|
||||
"""Test that ALKA is disabled for non-supported brands."""
|
||||
|
||||
TX_MSGS = []
|
||||
|
||||
def test_alka_disabled_for_gm(self):
|
||||
"""GM should have alka_allowed set to false."""
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_safety_hooks(CarParams.SafetyModel.gm, 1)
|
||||
safety.init_tests()
|
||||
self.assertFalse(safety.get_alka_allowed())
|
||||
|
||||
def test_alka_disabled_for_tesla(self):
|
||||
"""Tesla should have alka_allowed set to false."""
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_safety_hooks(CarParams.SafetyModel.tesla, 0)
|
||||
safety.init_tests()
|
||||
self.assertFalse(safety.get_alka_allowed())
|
||||
|
||||
def test_alka_disabled_for_chrysler(self):
|
||||
"""Chrysler should have alka_allowed set to false."""
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_safety_hooks(CarParams.SafetyModel.chrysler, 0)
|
||||
safety.init_tests()
|
||||
self.assertFalse(safety.get_alka_allowed())
|
||||
|
||||
|
||||
class TestALKALatControlAllowed(unittest.TestCase):
|
||||
"""Test lat_control_allowed() logic directly."""
|
||||
|
||||
TX_MSGS = []
|
||||
|
||||
def setUp(self):
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.toyota, 73)
|
||||
self.safety.init_tests()
|
||||
|
||||
def _set_vehicle_moving(self, moving: bool):
|
||||
packer = CANPackerSafety("toyota_nodsu_pt_generated")
|
||||
speed = 10.0 if moving else 0.0
|
||||
for _ in range(6):
|
||||
msg = packer.make_can_msg_safety("WHEEL_SPEEDS", 0, {
|
||||
"WHEEL_SPEED_FR": speed * 3.6,
|
||||
"WHEEL_SPEED_FL": speed * 3.6,
|
||||
"WHEEL_SPEED_RR": speed * 3.6,
|
||||
"WHEEL_SPEED_RL": speed * 3.6,
|
||||
})
|
||||
self.safety.safety_rx_hook(msg)
|
||||
|
||||
def test_controls_allowed_overrides_alka(self):
|
||||
"""controls_allowed should always grant lat control."""
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.DEFAULT)
|
||||
self.assertTrue(self.safety.get_lat_control_allowed())
|
||||
|
||||
def test_alka_all_conditions_required(self):
|
||||
"""All ALKA conditions must be met for lat control when controls_allowed is false."""
|
||||
self.safety.set_controls_allowed(False)
|
||||
|
||||
for alka_flag in [False, True]:
|
||||
for lkas_on in [False, True]:
|
||||
for vehicle_moving in [False, True]:
|
||||
exp = ALTERNATIVE_EXPERIENCE.ALKA if alka_flag else ALTERNATIVE_EXPERIENCE.DEFAULT
|
||||
self.safety.set_alternative_experience(exp)
|
||||
self.safety.set_lkas_on(lkas_on)
|
||||
self._set_vehicle_moving(vehicle_moving)
|
||||
|
||||
expected = alka_flag and lkas_on and vehicle_moving
|
||||
self.assertEqual(expected, self.safety.get_lat_control_allowed(),
|
||||
f"alka_flag={alka_flag}, lkas_on={lkas_on}, vehicle_moving={vehicle_moving}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
38
panda_tici/.gitignore
vendored
Normal file
38
panda_tici/.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
.venv
|
||||
*.tmp
|
||||
*.pyc
|
||||
.*.swp
|
||||
.*.swo
|
||||
*.o
|
||||
*.so
|
||||
*.os
|
||||
*.d
|
||||
*.dump
|
||||
a.out
|
||||
*~
|
||||
.#*
|
||||
dist/
|
||||
build/
|
||||
pandacan.egg-info/
|
||||
obj/
|
||||
examples/output.csv
|
||||
.DS_Store
|
||||
.vscode*
|
||||
nosetests.xml
|
||||
.mypy_cache/
|
||||
.sconsign.dblite
|
||||
uv.lock
|
||||
compile_commands.json
|
||||
|
||||
# CTU info files generated by Cppcheck
|
||||
*.*.ctu-info
|
||||
|
||||
# safety coverage-related files
|
||||
*.gcda
|
||||
*.gcno
|
||||
tests/safety/coverage-out
|
||||
tests/safety/coverage.info
|
||||
|
||||
*.profraw
|
||||
*.profdata
|
||||
mull.yml
|
||||
21
panda_tici/Dockerfile
Normal file
21
panda_tici/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONPATH=/tmp/pythonpath
|
||||
|
||||
# deps install
|
||||
COPY pyproject.toml __init__.py setup.sh /tmp/
|
||||
COPY python/__init__.py /tmp/python/
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends sudo && /tmp/setup.sh
|
||||
|
||||
COPY pyproject.toml __init__.py $PYTHONPATH/panda_tici/
|
||||
COPY python/__init__.py $PYTHONPATH/panda_tici/python/
|
||||
RUN pip3 install --break-system-packages --no-cache-dir $PYTHONPATH/panda_tici/[dev]
|
||||
|
||||
RUN git config --global --add safe.directory $PYTHONPATH/panda_tici
|
||||
|
||||
# for Jenkins
|
||||
COPY README.md panda_tici.tar.* /tmp/
|
||||
RUN mkdir -p /tmp/pythonpath/panda_tici && \
|
||||
tar -xvf /tmp/panda_tici.tar.gz -C /tmp/pythonpath/panda_tici/ || true
|
||||
152
panda_tici/Jenkinsfile
vendored
Normal file
152
panda_tici/Jenkinsfile
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
def docker_run(String step_label, int timeout_mins, String cmd) {
|
||||
timeout(time: timeout_mins, unit: 'MINUTES') {
|
||||
sh script: "docker run --rm --privileged \
|
||||
--env PYTHONWARNINGS=error \
|
||||
--volume /dev/bus/usb:/dev/bus/usb \
|
||||
--volume /var/run/dbus:/var/run/dbus \
|
||||
--workdir /tmp/pythonpath/panda_tici \
|
||||
--net host \
|
||||
${env.DOCKER_IMAGE_TAG} \
|
||||
bash -c 'scons -j8 && ${cmd}'", \
|
||||
label: step_label
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def phone(String ip, String step_label, String cmd) {
|
||||
withCredentials([file(credentialsId: 'id_rsa', variable: 'key_file')]) {
|
||||
def ssh_cmd = """
|
||||
ssh -tt -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' /usr/bin/bash <<'END'
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
source ~/.bash_profile
|
||||
if [ -f /etc/profile ]; then
|
||||
source /etc/profile
|
||||
fi
|
||||
|
||||
export CI=1
|
||||
export TEST_DIR=${env.TEST_DIR}
|
||||
export SOURCE_DIR=${env.SOURCE_DIR}
|
||||
export GIT_BRANCH=${env.GIT_BRANCH}
|
||||
export GIT_COMMIT=${env.GIT_COMMIT}
|
||||
export PYTHONPATH=${env.TEST_DIR}/../
|
||||
export PYTHONWARNINGS=error
|
||||
ln -sf /data/openpilot/opendbc_repo/opendbc /data/opendbc
|
||||
|
||||
cd ${env.TEST_DIR} || true
|
||||
${cmd}
|
||||
exit 0
|
||||
|
||||
END"""
|
||||
|
||||
sh script: ssh_cmd, label: step_label
|
||||
}
|
||||
}
|
||||
|
||||
def phone_steps(String device_type, steps) {
|
||||
lock(resource: "", label: device_type, inversePrecedence: true, variable: 'device_ip', quantity: 1) {
|
||||
timeout(time: 20, unit: 'MINUTES') {
|
||||
phone(device_ip, "git checkout", readFile("tests/setup_device_ci.sh"),)
|
||||
steps.each { item ->
|
||||
phone(device_ip, item[0], item[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pipeline {
|
||||
agent any
|
||||
environment {
|
||||
CI = "1"
|
||||
PYTHONWARNINGS= "error"
|
||||
DOCKER_IMAGE_TAG = "panda:build-${env.GIT_COMMIT}"
|
||||
|
||||
TEST_DIR = "/data/panda_tici"
|
||||
SOURCE_DIR = "/data/panda_tici_source/"
|
||||
}
|
||||
options {
|
||||
timeout(time: 3, unit: 'HOURS')
|
||||
disableConcurrentBuilds(abortPrevious: env.BRANCH_NAME != 'master')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage ('Acquire resource locks') {
|
||||
options {
|
||||
lock(resource: "pandas")
|
||||
}
|
||||
stages {
|
||||
stage('Build Docker Image') {
|
||||
steps {
|
||||
timeout(time: 20, unit: 'MINUTES') {
|
||||
script {
|
||||
sh 'git archive -v -o panda.tar.gz --format=tar.gz HEAD'
|
||||
dockerImage = docker.build("${env.DOCKER_IMAGE_TAG}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('jungle tests') {
|
||||
steps {
|
||||
script {
|
||||
retry (3) {
|
||||
docker_run("reset hardware", 3, "python3 ./tests/hitl/reset_jungles.py")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('parallel tests') {
|
||||
parallel {
|
||||
stage('test cuatro') {
|
||||
agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } }
|
||||
steps {
|
||||
phone_steps("panda-cuatro", [
|
||||
["build", "scons -j4"],
|
||||
["flash", "cd scripts/ && ./reflash_internal_panda.py"],
|
||||
["flash jungle", "cd board/jungle && ./flash.py --all"],
|
||||
["test", "cd tests/hitl && HW_TYPES=10 pytest -n0 --durations=0 2*.py [5-9]*.py"],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
stage('test tres') {
|
||||
agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } }
|
||||
steps {
|
||||
phone_steps("panda-tres", [
|
||||
["build", "scons -j4"],
|
||||
["flash", "cd scripts/ && ./reflash_internal_panda.py"],
|
||||
["flash jungle", "cd board/jungle && ./flash.py --all"],
|
||||
["test", "cd tests/hitl && HW_TYPES=9 pytest -n0 --durations=0 2*.py [5-9]*.py"],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
stage('test dos') {
|
||||
agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } }
|
||||
steps {
|
||||
phone_steps("panda-dos", [
|
||||
["build", "scons -j4"],
|
||||
["flash", "cd scripts/ && ./reflash_internal_panda.py"],
|
||||
["flash jungle", "cd board/jungle && ./flash.py --all"],
|
||||
["test", "cd tests/hitl && HW_TYPES=6 pytest -n0 --durations=0 [2-9]*.py -k 'not test_send_recv'"],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
stage('bootkick tests') {
|
||||
steps {
|
||||
script {
|
||||
docker_run("test", 10, "pytest -n0 ./tests/som/test_bootkick.py")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
panda_tici/LICENSE
Normal file
7
panda_tici/LICENSE
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2016, Comma.ai, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
91
panda_tici/README.md
Normal file
91
panda_tici/README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Welcome to panda
|
||||
|
||||
panda speaks CAN and CAN FD, and it runs on [STM32F413](https://www.st.com/resource/en/reference_manual/rm0430-stm32f413423-advanced-armbased-32bit-mcus-stmicroelectronics.pdf) and [STM32H725](https://www.st.com/resource/en/reference_manual/rm0468-stm32h723733-stm32h725735-and-stm32h730-value-line-advanced-armbased-32bit-mcus-stmicroelectronics.pdf).
|
||||
|
||||
## Directory structure
|
||||
|
||||
```
|
||||
.
|
||||
├── board # Code that runs on the STM32
|
||||
├── drivers # Drivers (not needed for use with Python)
|
||||
├── python # Python userspace library for interfacing with the panda
|
||||
├── tests # Tests for panda
|
||||
├── scripts # Miscellaneous used for panda development and debugging
|
||||
├── examples # Example scripts for using a panda in a car
|
||||
```
|
||||
|
||||
## Safety Model
|
||||
|
||||
panda is compiled with safety firmware provided by [opendbc](https://github.com/commaai/opendbc). See details about the car safety models, safety testing, and code rigor in that repository.
|
||||
|
||||
## Code Rigor
|
||||
|
||||
The panda firmware is written for its use in conjunction with [openpilot](https://github.com/commaai/openpilot). The panda firmware, through its safety model, provides and enforces the
|
||||
[openpilot safety](https://github.com/commaai/openpilot/blob/master/docs/SAFETY.md). Due to its critical function, it's important that the application code rigor within the `board` folder is held to high standards.
|
||||
|
||||
These are the [CI regression tests](https://github.com/commaai/panda_tici/actions) we have in place:
|
||||
* A generic static code analysis is performed by [cppcheck](https://github.com/danmar/cppcheck/).
|
||||
* In addition, [cppcheck](https://github.com/danmar/cppcheck/) has a specific addon to check for [MISRA C:2012](https://misra.org.uk/) violations. See [current coverage](https://github.com/commaai/panda_tici/blob/master/tests/misra/coverage_table).
|
||||
* Compiler options are relatively strict: the flags `-Wall -Wextra -Wstrict-prototypes -Werror` are enforced.
|
||||
* The [safety logic](https://github.com/commaai/panda_tici/tree/master/opendbc/safety) is tested and verified by [unit tests](https://github.com/commaai/panda_tici/tree/master/opendbc/safety/tests) for each supported car variant.
|
||||
to ensure that the behavior remains unchanged.
|
||||
* A hardware-in-the-loop test verifies panda's functionalities on all active panda variants, including:
|
||||
* additional safety model checks
|
||||
* compiling and flashing the bootstub and app code
|
||||
* receiving, sending, and forwarding CAN messages on all buses
|
||||
* CAN loopback and latency tests through USB and SPI
|
||||
|
||||
The above tests are themselves tested by:
|
||||
* a [mutation test](tests/misra/test_mutation.py) on the MISRA coverage
|
||||
|
||||
In addition, we run the [ruff linter](https://github.com/astral-sh/ruff) and [mypy](https://mypy-lang.org/) on panda's Python library.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
git clone https://github.com/commaai/panda_tici.git
|
||||
cd panda
|
||||
|
||||
# setup your environment
|
||||
./setup.sh
|
||||
|
||||
# build fw + run the tests
|
||||
./test.sh
|
||||
```
|
||||
|
||||
See [the Panda class](https://github.com/commaai/panda_tici/blob/master/python/__init__.py) for how to interact with the panda.
|
||||
|
||||
For example, to receive CAN messages:
|
||||
``` python
|
||||
>>> from panda_tici import Panda
|
||||
>>> panda = Panda()
|
||||
>>> panda.can_recv()
|
||||
```
|
||||
And to send one on bus 0:
|
||||
``` python
|
||||
>>> from opendbc.car.structs import CarParams
|
||||
>>> panda.set_safety_mode(CarParams.SafetyModel.allOutput)
|
||||
>>> panda.can_send(0x1aa, b'message', 0)
|
||||
```
|
||||
Note that you may have to setup [udev rules](https://github.com/commaai/panda_tici/tree/master/drivers/linux) for Linux, such as
|
||||
``` bash
|
||||
sudo tee /etc/udev/rules.d/11-panda.rules <<EOF
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="df11", MODE="0666"
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="3801", ATTRS{idProduct}=="ddcc", MODE="0666"
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="3801", ATTRS{idProduct}=="ddee", MODE="0666"
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="bbaa", ATTRS{idProduct}=="ddcc", MODE="0666"
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="bbaa", ATTRS{idProduct}=="ddee", MODE="0666"
|
||||
EOF
|
||||
sudo udevadm control --reload-rules && sudo udevadm trigger
|
||||
```
|
||||
|
||||
The panda jungle uses different udev rules. See [the repo](https://github.com/commaai/panda_jungle#udev-rules) for instructions.
|
||||
|
||||
## Software interface support
|
||||
|
||||
- [Python library](https://github.com/commaai/panda_tici/tree/master/python)
|
||||
- [C++ library](https://github.com/commaai/openpilot/tree/master/selfdrive/pandad)
|
||||
|
||||
## Licensing
|
||||
|
||||
panda software is released under the MIT license unless otherwise specified.
|
||||
213
panda_tici/SConscript
Normal file
213
panda_tici/SConscript
Normal file
@@ -0,0 +1,213 @@
|
||||
import os
|
||||
import opendbc
|
||||
import subprocess
|
||||
|
||||
PREFIX = "arm-none-eabi-"
|
||||
BUILDER = "DEV"
|
||||
|
||||
common_flags = []
|
||||
|
||||
panda_root = Dir('.')
|
||||
|
||||
if os.getenv("RELEASE"):
|
||||
BUILD_TYPE = "RELEASE"
|
||||
cert_fn = os.getenv("CERT")
|
||||
assert cert_fn is not None, 'No certificate file specified. Please set CERT env variable'
|
||||
assert os.path.exists(cert_fn), 'Certificate file not found. Please specify absolute path'
|
||||
else:
|
||||
BUILD_TYPE = "DEBUG"
|
||||
cert_fn = File("./certs/debug").srcnode().relpath
|
||||
common_flags += ["-DALLOW_DEBUG"]
|
||||
|
||||
if os.getenv("DEBUG"):
|
||||
common_flags += ["-DDEBUG"]
|
||||
|
||||
def objcopy(source, target, env, for_signature):
|
||||
return '$OBJCOPY -O binary %s %s' % (source[0], target[0])
|
||||
|
||||
def get_version(builder, build_type):
|
||||
try:
|
||||
git = subprocess.check_output(["git", "rev-parse", "--short=8", "HEAD"], encoding='utf8').strip()
|
||||
except subprocess.CalledProcessError:
|
||||
git = "unknown"
|
||||
return f"{builder}-{git}-{build_type}"
|
||||
|
||||
def get_key_header(name):
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
public_fn = File(f'./certs/{name}.pub').srcnode().get_path()
|
||||
with open(public_fn) as f:
|
||||
rsa = RSA.importKey(f.read())
|
||||
assert(rsa.size_in_bits() == 1024)
|
||||
|
||||
rr = pow(2**1024, 2, rsa.n)
|
||||
n0inv = 2**32 - pow(rsa.n, -1, 2**32)
|
||||
|
||||
r = [
|
||||
f"RSAPublicKey {name}_rsa_key = {{",
|
||||
f" .len = 0x20,",
|
||||
f" .n0inv = {n0inv}U,",
|
||||
f" .n = {to_c_uint32(rsa.n)},",
|
||||
f" .rr = {to_c_uint32(rr)},",
|
||||
f" .exponent = {rsa.e},",
|
||||
f"}};",
|
||||
]
|
||||
return r
|
||||
|
||||
def to_c_uint32(x):
|
||||
nums = []
|
||||
for _ in range(0x20):
|
||||
nums.append(x % (2**32))
|
||||
x //= (2**32)
|
||||
return "{" + 'U,'.join(map(str, nums)) + "U}"
|
||||
|
||||
|
||||
def build_project(project_name, project, extra_flags):
|
||||
linkerscript_fn = File(project["LINKER_SCRIPT"]).srcnode().relpath
|
||||
|
||||
flags = project["PROJECT_FLAGS"] + extra_flags + common_flags + [
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Wstrict-prototypes",
|
||||
"-Werror",
|
||||
"-mlittle-endian",
|
||||
"-mthumb",
|
||||
"-nostdlib",
|
||||
"-fno-builtin",
|
||||
"-std=gnu11",
|
||||
"-fmax-errors=1",
|
||||
f"-T{linkerscript_fn}",
|
||||
]
|
||||
|
||||
includes = [
|
||||
'.',
|
||||
'..',
|
||||
panda_root,
|
||||
f"{panda_root}/board/",
|
||||
opendbc.INCLUDE_PATH,
|
||||
]
|
||||
|
||||
env = Environment(
|
||||
ENV=os.environ,
|
||||
CC=PREFIX + 'gcc',
|
||||
AS=PREFIX + 'gcc',
|
||||
OBJCOPY=PREFIX + 'objcopy',
|
||||
OBJDUMP=PREFIX + 'objdump',
|
||||
CFLAGS=flags,
|
||||
ASFLAGS=flags,
|
||||
LINKFLAGS=flags,
|
||||
CPPPATH=includes,
|
||||
ASCOM="$AS $ASFLAGS -o $TARGET -c $SOURCES",
|
||||
BUILDERS={
|
||||
'Objcopy': Builder(generator=objcopy, suffix='.bin', src_suffix='.elf')
|
||||
},
|
||||
tools=["default", "compilation_db"],
|
||||
)
|
||||
|
||||
startup = env.Object(f"obj/startup_{project_name}", project["STARTUP_FILE"])
|
||||
|
||||
# Bootstub
|
||||
crypto_obj = [
|
||||
env.Object(f"rsa-{project_name}", f"{panda_root}/crypto/rsa.c"),
|
||||
env.Object(f"sha-{project_name}", f"{panda_root}/crypto/sha.c")
|
||||
]
|
||||
bootstub_obj = env.Object(f"bootstub-{project_name}", File(project.get("BOOTSTUB", f"{panda_root}/board/bootstub.c")))
|
||||
bootstub_elf = env.Program(f"obj/bootstub.{project_name}.elf",
|
||||
[startup] + crypto_obj + [bootstub_obj])
|
||||
env.Objcopy(f"obj/bootstub.{project_name}.bin", bootstub_elf)
|
||||
|
||||
# Build main
|
||||
main_obj = env.Object(f"main-{project_name}", project["MAIN"])
|
||||
main_elf = env.Program(f"obj/{project_name}.elf", [startup, main_obj],
|
||||
LINKFLAGS=[f"-Wl,--section-start,.isr_vector={project['APP_START_ADDRESS']}"] + flags)
|
||||
main_bin = env.Objcopy(f"obj/{project_name}.bin", main_elf)
|
||||
|
||||
# Sign main
|
||||
sign_py = File(f"{panda_root}/crypto/sign.py").srcnode().relpath
|
||||
env.Command(f"obj/{project_name}.bin.signed", main_bin, f"SETLEN=1 {sign_py} $SOURCE $TARGET {cert_fn}")
|
||||
|
||||
|
||||
base_project_f4 = {
|
||||
"MAIN": "main.c",
|
||||
"STARTUP_FILE": File("./board/stm32f4/startup_stm32f413xx.s"),
|
||||
"LINKER_SCRIPT": File("./board/stm32f4/stm32f4_flash.ld"),
|
||||
"APP_START_ADDRESS": "0x8004000",
|
||||
"PROJECT_FLAGS": [
|
||||
"-mcpu=cortex-m4",
|
||||
"-mhard-float",
|
||||
"-DSTM32F4",
|
||||
"-DSTM32F413xx",
|
||||
"-Iboard/stm32f4/inc",
|
||||
"-mfpu=fpv4-sp-d16",
|
||||
"-fsingle-precision-constant",
|
||||
"-Os",
|
||||
"-g",
|
||||
],
|
||||
}
|
||||
|
||||
base_project_h7 = {
|
||||
"MAIN": "main.c",
|
||||
"STARTUP_FILE": File("./board/stm32h7/startup_stm32h7x5xx.s"),
|
||||
"LINKER_SCRIPT": File("./board/stm32h7/stm32h7x5_flash.ld"),
|
||||
"APP_START_ADDRESS": "0x8020000",
|
||||
"PROJECT_FLAGS": [
|
||||
"-mcpu=cortex-m7",
|
||||
"-mhard-float",
|
||||
"-DSTM32H7",
|
||||
"-DSTM32H725xx",
|
||||
"-Iboard/stm32h7/inc",
|
||||
"-mfpu=fpv5-d16",
|
||||
"-fsingle-precision-constant",
|
||||
"-Os",
|
||||
"-g",
|
||||
],
|
||||
}
|
||||
|
||||
Export('base_project_f4', 'base_project_h7', 'build_project')
|
||||
|
||||
###### rick - get version.h from panda/ #######
|
||||
# instead of generate them, copy from panda/ so we have the same version
|
||||
import os
|
||||
import shutil
|
||||
SOURCE_DIR = os.path.join("..", "panda", "board", "obj")
|
||||
if os.path.exists(SOURCE_DIR):
|
||||
DEST_DIR = os.path.join("board", "obj")
|
||||
FILES_TO_COPY = ["gitversion.h", "version", "cert.h"]
|
||||
|
||||
# Ensure the destination directory exists.
|
||||
os.makedirs(DEST_DIR, exist_ok=True)
|
||||
|
||||
# Loop through the files and copy them.
|
||||
for filename in FILES_TO_COPY:
|
||||
source_path = os.path.join(SOURCE_DIR, filename)
|
||||
dest_path = os.path.join(DEST_DIR, filename)
|
||||
|
||||
if os.path.exists(source_path):
|
||||
shutil.copy(source_path, dest_path)
|
||||
|
||||
##############################################
|
||||
# if we compile directly, then we fallback to original code
|
||||
else:
|
||||
# Common autogenerated includes
|
||||
with open("board/obj/gitversion.h", "w") as f:
|
||||
version = get_version(BUILDER, BUILD_TYPE)
|
||||
f.write(f'extern const uint8_t gitversion[{len(version)}];\n')
|
||||
f.write(f'const uint8_t gitversion[{len(version)}] = "{version}";\n')
|
||||
|
||||
with open("board/obj/version", "w") as f:
|
||||
f.write(f'{get_version(BUILDER, BUILD_TYPE)}')
|
||||
|
||||
certs = [get_key_header(n) for n in ["debug", "release"]]
|
||||
with open("board/obj/cert.h", "w") as f:
|
||||
for cert in certs:
|
||||
f.write("\n".join(cert) + "\n")
|
||||
|
||||
# panda fw
|
||||
SConscript('board/SConscript')
|
||||
|
||||
# panda jungle fw
|
||||
SConscript('board/jungle/SConscript')
|
||||
|
||||
# test files
|
||||
if GetOption('extras'):
|
||||
SConscript('tests/libpanda/SConscript')
|
||||
19
panda_tici/SConstruct
Normal file
19
panda_tici/SConstruct
Normal file
@@ -0,0 +1,19 @@
|
||||
AddOption('--minimal',
|
||||
action='store_false',
|
||||
dest='extras',
|
||||
default=True,
|
||||
help='the minimum build. no tests, tools, etc.')
|
||||
|
||||
AddOption('--ubsan',
|
||||
action='store_true',
|
||||
help='turn on UBSan')
|
||||
|
||||
env = Environment(
|
||||
COMPILATIONDB_USE_ABSPATH=True,
|
||||
tools=["default", "compilation_db"],
|
||||
)
|
||||
|
||||
env.CompilationDatabase("compile_commands.json")
|
||||
|
||||
# panda fw & test files
|
||||
SConscript('SConscript')
|
||||
10
panda_tici/__init__.py
Normal file
10
panda_tici/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from .python.constants import McuType, BASEDIR, FW_PATH, USBPACKET_MAX_SIZE # noqa: F401
|
||||
from .python.spi import PandaSpiException, PandaProtocolMismatch, STBootloaderSPIHandle # noqa: F401
|
||||
from .python.serial import PandaSerial # noqa: F401
|
||||
from .python.utils import logger # noqa: F401
|
||||
from .python import (Panda, PandaDFU, # noqa: F401
|
||||
pack_can_buffer, unpack_can_buffer, calculate_checksum,
|
||||
DLC_TO_LEN, LEN_TO_DLC, CANPACKET_HEAD_SIZE)
|
||||
|
||||
# panda jungle
|
||||
from .board.jungle import PandaJungle, PandaJungleDFU # noqa: F401
|
||||
20
panda_tici/board/README.md
Normal file
20
panda_tici/board/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## Programming
|
||||
|
||||
```
|
||||
./flash.py # flash application
|
||||
./recover.py # flash bootstub
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
To print out the serial console from the STM32, run `tests/debug_console.py`
|
||||
|
||||
Troubleshooting
|
||||
----
|
||||
|
||||
If your panda will not flash and green LED is on, use `recover.py`.
|
||||
If panda is blinking fast with green LED, use `flash.py`.
|
||||
|
||||
Otherwise if LED is off and panda can't be seen with `lsusb` command, use [panda paw](https://comma.ai/shop/products/panda-paw) to go into DFU mode.
|
||||
|
||||
If your device has an internal panda and none of the above works, try running `../scripts/reflash_internal_panda.py`.
|
||||
18
panda_tici/board/SConscript
Normal file
18
panda_tici/board/SConscript
Normal file
@@ -0,0 +1,18 @@
|
||||
import os
|
||||
import copy
|
||||
|
||||
Import('build_project', 'base_project_f4', 'base_project_h7')
|
||||
|
||||
build_projects = {
|
||||
"panda": base_project_f4,
|
||||
"panda_h7": base_project_h7,
|
||||
}
|
||||
|
||||
for project_name, project in build_projects.items():
|
||||
flags = [
|
||||
"-DPANDA",
|
||||
]
|
||||
if ("ENABLE_SPI" in os.environ or "h7" in project_name):
|
||||
flags.append('-DENABLE_SPI')
|
||||
|
||||
build_project(project_name, project, flags)
|
||||
0
panda_tici/board/__init__.py
Normal file
0
panda_tici/board/__init__.py
Normal file
81
panda_tici/board/boards/board_declarations.h
Normal file
81
panda_tici/board/boards/board_declarations.h
Normal file
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// ******************** Prototypes ********************
|
||||
typedef enum {
|
||||
BOOT_STANDBY,
|
||||
BOOT_BOOTKICK,
|
||||
BOOT_RESET,
|
||||
} BootState;
|
||||
|
||||
typedef void (*board_init)(void);
|
||||
typedef void (*board_init_bootloader)(void);
|
||||
typedef void (*board_enable_can_transceiver)(uint8_t transceiver, bool enabled);
|
||||
typedef void (*board_set_can_mode)(uint8_t mode);
|
||||
typedef bool (*board_check_ignition)(void);
|
||||
typedef uint32_t (*board_read_voltage_mV)(void);
|
||||
typedef uint32_t (*board_read_current_mA)(void);
|
||||
typedef void (*board_set_ir_power)(uint8_t percentage);
|
||||
typedef void (*board_set_fan_enabled)(bool enabled);
|
||||
typedef void (*board_set_siren)(bool enabled);
|
||||
typedef void (*board_set_bootkick)(BootState state);
|
||||
typedef bool (*board_read_som_gpio)(void);
|
||||
typedef void (*board_set_amp_enabled)(bool enabled);
|
||||
|
||||
struct board {
|
||||
harness_configuration *harness_config;
|
||||
GPIO_TypeDef * const led_GPIO[3];
|
||||
const uint8_t led_pin[3];
|
||||
const uint8_t led_pwm_channels[3]; // leave at 0 to disable PWM
|
||||
const bool has_spi;
|
||||
const bool has_canfd;
|
||||
const uint16_t fan_max_rpm;
|
||||
const uint16_t avdd_mV;
|
||||
const bool fan_stall_recovery;
|
||||
const uint8_t fan_enable_cooldown_time;
|
||||
const uint8_t fan_max_pwm;
|
||||
board_init init;
|
||||
board_init_bootloader init_bootloader;
|
||||
board_enable_can_transceiver enable_can_transceiver;
|
||||
board_set_can_mode set_can_mode;
|
||||
board_check_ignition check_ignition;
|
||||
board_read_voltage_mV read_voltage_mV;
|
||||
board_read_current_mA read_current_mA;
|
||||
board_set_ir_power set_ir_power;
|
||||
board_set_fan_enabled set_fan_enabled;
|
||||
board_set_siren set_siren;
|
||||
board_set_bootkick set_bootkick;
|
||||
board_read_som_gpio read_som_gpio;
|
||||
board_set_amp_enabled set_amp_enabled;
|
||||
};
|
||||
|
||||
// ******************* Definitions ********************
|
||||
// These should match the enums in cereal/log.capnp and __init__.py
|
||||
#define HW_TYPE_UNKNOWN 0U
|
||||
//#define HW_TYPE_WHITE_PANDA 1U
|
||||
//#define HW_TYPE_GREY_PANDA 2U
|
||||
//#define HW_TYPE_BLACK_PANDA 3U
|
||||
//#define HW_TYPE_PEDAL 4U
|
||||
//#define HW_TYPE_UNO 5U
|
||||
#define HW_TYPE_DOS 6U
|
||||
#define HW_TYPE_RED_PANDA 7U
|
||||
#define HW_TYPE_RED_PANDA_V2 8U
|
||||
#define HW_TYPE_TRES 9U
|
||||
#define HW_TYPE_CUATRO 10U
|
||||
|
||||
// USB power modes (from cereal.log.health)
|
||||
#define USB_POWER_NONE 0U
|
||||
#define USB_POWER_CLIENT 1U
|
||||
#define USB_POWER_CDP 2U
|
||||
#define USB_POWER_DCP 3U
|
||||
|
||||
// CAN modes
|
||||
#define CAN_MODE_NORMAL 0U
|
||||
#define CAN_MODE_OBD_CAN2 1U
|
||||
|
||||
extern struct board board_dos;
|
||||
extern struct board board_tres;
|
||||
extern struct board board_cuatro;
|
||||
extern struct board board_red;
|
||||
143
panda_tici/board/boards/cuatro.h
Normal file
143
panda_tici/board/boards/cuatro.h
Normal file
@@ -0,0 +1,143 @@
|
||||
#pragma once
|
||||
|
||||
#include "board_declarations.h"
|
||||
|
||||
// ////////////////////////// //
|
||||
// Cuatro (STM32H7) + Harness //
|
||||
// ////////////////////////// //
|
||||
|
||||
static void cuatro_enable_can_transceiver(uint8_t transceiver, bool enabled) {
|
||||
switch (transceiver) {
|
||||
case 1U:
|
||||
set_gpio_output(GPIOB, 7, !enabled);
|
||||
break;
|
||||
case 2U:
|
||||
set_gpio_output(GPIOB, 10, !enabled);
|
||||
break;
|
||||
case 3U:
|
||||
set_gpio_output(GPIOD, 8, !enabled);
|
||||
break;
|
||||
case 4U:
|
||||
set_gpio_output(GPIOB, 11, !enabled);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t cuatro_read_voltage_mV(void) {
|
||||
return adc_get_mV(8) * 11U;
|
||||
}
|
||||
|
||||
static uint32_t cuatro_read_current_mA(void) {
|
||||
return adc_get_mV(3) * 2U;
|
||||
}
|
||||
|
||||
static void cuatro_set_fan_enabled(bool enabled) {
|
||||
set_gpio_output(GPIOD, 3, !enabled);
|
||||
}
|
||||
|
||||
static void cuatro_set_bootkick(BootState state) {
|
||||
set_gpio_output(GPIOA, 0, state != BOOT_BOOTKICK);
|
||||
// TODO: confirm we need this
|
||||
//set_gpio_output(GPIOC, 12, state != BOOT_RESET);
|
||||
}
|
||||
|
||||
static void cuatro_set_amp_enabled(bool enabled){
|
||||
set_gpio_output(GPIOA, 5, enabled);
|
||||
set_gpio_output(GPIOB, 0, enabled);
|
||||
}
|
||||
|
||||
static void cuatro_init(void) {
|
||||
common_init_gpio();
|
||||
|
||||
// open drain
|
||||
set_gpio_output_type(GPIOD, 3, OUTPUT_TYPE_OPEN_DRAIN); // FAN_EN
|
||||
set_gpio_output_type(GPIOC, 12, OUTPUT_TYPE_OPEN_DRAIN); // VBAT_EN
|
||||
|
||||
// Power readout
|
||||
set_gpio_mode(GPIOC, 5, MODE_ANALOG);
|
||||
set_gpio_mode(GPIOA, 6, MODE_ANALOG);
|
||||
|
||||
// CAN transceiver enables
|
||||
set_gpio_pullup(GPIOB, 7, PULL_NONE);
|
||||
set_gpio_mode(GPIOB, 7, MODE_OUTPUT);
|
||||
set_gpio_pullup(GPIOD, 8, PULL_NONE);
|
||||
set_gpio_mode(GPIOD, 8, MODE_OUTPUT);
|
||||
|
||||
// FDCAN3, different pins on this package than the rest of the reds
|
||||
set_gpio_pullup(GPIOD, 12, PULL_NONE);
|
||||
set_gpio_alternate(GPIOD, 12, GPIO_AF5_FDCAN3);
|
||||
set_gpio_pullup(GPIOD, 13, PULL_NONE);
|
||||
set_gpio_alternate(GPIOD, 13, GPIO_AF5_FDCAN3);
|
||||
|
||||
// C2: SOM GPIO used as input (fan control at boot)
|
||||
set_gpio_mode(GPIOC, 2, MODE_INPUT);
|
||||
set_gpio_pullup(GPIOC, 2, PULL_DOWN);
|
||||
|
||||
// SOM bootkick + reset lines
|
||||
cuatro_set_bootkick(BOOT_BOOTKICK);
|
||||
|
||||
// SOM debugging UART
|
||||
gpio_uart7_init();
|
||||
uart_init(&uart_ring_som_debug, 115200);
|
||||
|
||||
// fan setup
|
||||
set_gpio_alternate(GPIOC, 8, GPIO_AF2_TIM3);
|
||||
register_set_bits(&(GPIOC->OTYPER), GPIO_OTYPER_OT8); // open drain
|
||||
|
||||
// Clock source
|
||||
clock_source_init(true);
|
||||
|
||||
// Sound codec
|
||||
cuatro_set_amp_enabled(false);
|
||||
set_gpio_alternate(GPIOA, 2, GPIO_AF8_SAI4); // SAI4_SCK_B
|
||||
set_gpio_alternate(GPIOC, 0, GPIO_AF8_SAI4); // SAI4_FS_B
|
||||
set_gpio_alternate(GPIOD, 11, GPIO_AF10_SAI4); // SAI4_SD_A
|
||||
set_gpio_alternate(GPIOE, 3, GPIO_AF8_SAI4); // SAI4_SD_B
|
||||
set_gpio_alternate(GPIOE, 4, GPIO_AF3_DFSDM1); // DFSDM1_DATIN3
|
||||
set_gpio_alternate(GPIOE, 9, GPIO_AF3_DFSDM1); // DFSDM1_CKOUT
|
||||
set_gpio_alternate(GPIOE, 6, GPIO_AF10_SAI4); // SAI4_MCLK_B
|
||||
sound_init();
|
||||
}
|
||||
|
||||
static harness_configuration cuatro_harness_config = {
|
||||
.has_harness = true,
|
||||
.GPIO_SBU1 = GPIOC,
|
||||
.GPIO_SBU2 = GPIOA,
|
||||
.GPIO_relay_SBU1 = GPIOA,
|
||||
.GPIO_relay_SBU2 = GPIOA,
|
||||
.pin_SBU1 = 4,
|
||||
.pin_SBU2 = 1,
|
||||
.pin_relay_SBU1 = 9,
|
||||
.pin_relay_SBU2 = 3,
|
||||
.adc_channel_SBU1 = 4, // ADC12_INP4
|
||||
.adc_channel_SBU2 = 17 // ADC1_INP17
|
||||
};
|
||||
|
||||
board board_cuatro = {
|
||||
.harness_config = &cuatro_harness_config,
|
||||
.has_spi = true,
|
||||
.has_canfd = true,
|
||||
.fan_max_rpm = 12500U,
|
||||
.fan_max_pwm = 99U, // it can go up to 14k RPM, but 99% -> 100% is very non-linear
|
||||
.avdd_mV = 1800U,
|
||||
.fan_stall_recovery = false,
|
||||
.fan_enable_cooldown_time = 3U,
|
||||
.init = cuatro_init,
|
||||
.init_bootloader = unused_init_bootloader,
|
||||
.enable_can_transceiver = cuatro_enable_can_transceiver,
|
||||
.led_GPIO = {GPIOC, GPIOC, GPIOC},
|
||||
.led_pin = {6, 7, 9},
|
||||
.led_pwm_channels = {1, 2, 4},
|
||||
.set_can_mode = tres_set_can_mode,
|
||||
.check_ignition = red_check_ignition,
|
||||
.read_voltage_mV = cuatro_read_voltage_mV,
|
||||
.read_current_mA = cuatro_read_current_mA,
|
||||
.set_fan_enabled = cuatro_set_fan_enabled,
|
||||
.set_ir_power = unused_set_ir_power,
|
||||
.set_siren = unused_set_siren,
|
||||
.set_bootkick = cuatro_set_bootkick,
|
||||
.read_som_gpio = tres_read_som_gpio,
|
||||
.set_amp_enabled = cuatro_set_amp_enabled
|
||||
};
|
||||
158
panda_tici/board/boards/dos.h
Normal file
158
panda_tici/board/boards/dos.h
Normal file
@@ -0,0 +1,158 @@
|
||||
#pragma once
|
||||
|
||||
#include "board_declarations.h"
|
||||
|
||||
// /////////////////////// //
|
||||
// Dos (STM32F4) + Harness //
|
||||
// /////////////////////// //
|
||||
|
||||
static void dos_enable_can_transceiver(uint8_t transceiver, bool enabled) {
|
||||
switch (transceiver){
|
||||
case 1U:
|
||||
set_gpio_output(GPIOC, 1, !enabled);
|
||||
break;
|
||||
case 2U:
|
||||
set_gpio_output(GPIOC, 13, !enabled);
|
||||
break;
|
||||
case 3U:
|
||||
set_gpio_output(GPIOA, 0, !enabled);
|
||||
break;
|
||||
case 4U:
|
||||
set_gpio_output(GPIOB, 10, !enabled);
|
||||
break;
|
||||
default:
|
||||
print("Invalid CAN transceiver ("); puth(transceiver); print("): enabling failed\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void dos_set_bootkick(BootState state) {
|
||||
set_gpio_output(GPIOC, 4, state != BOOT_BOOTKICK);
|
||||
}
|
||||
|
||||
static void dos_set_can_mode(uint8_t mode) {
|
||||
dos_enable_can_transceiver(2U, false);
|
||||
dos_enable_can_transceiver(4U, false);
|
||||
switch (mode) {
|
||||
case CAN_MODE_NORMAL:
|
||||
case CAN_MODE_OBD_CAN2:
|
||||
if ((bool)(mode == CAN_MODE_NORMAL) != (bool)(harness.status == HARNESS_STATUS_FLIPPED)) {
|
||||
// B12,B13: disable OBD mode
|
||||
set_gpio_mode(GPIOB, 12, MODE_INPUT);
|
||||
set_gpio_mode(GPIOB, 13, MODE_INPUT);
|
||||
|
||||
// B5,B6: normal CAN2 mode
|
||||
set_gpio_alternate(GPIOB, 5, GPIO_AF9_CAN2);
|
||||
set_gpio_alternate(GPIOB, 6, GPIO_AF9_CAN2);
|
||||
dos_enable_can_transceiver(2U, true);
|
||||
} else {
|
||||
// B5,B6: disable normal CAN2 mode
|
||||
set_gpio_mode(GPIOB, 5, MODE_INPUT);
|
||||
set_gpio_mode(GPIOB, 6, MODE_INPUT);
|
||||
|
||||
// B12,B13: OBD mode
|
||||
set_gpio_alternate(GPIOB, 12, GPIO_AF9_CAN2);
|
||||
set_gpio_alternate(GPIOB, 13, GPIO_AF9_CAN2);
|
||||
dos_enable_can_transceiver(4U, true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
print("Tried to set unsupported CAN mode: "); puth(mode); print("\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static bool dos_check_ignition(void){
|
||||
// ignition is checked through harness
|
||||
return harness_check_ignition();
|
||||
}
|
||||
|
||||
static void dos_set_ir_power(uint8_t percentage){
|
||||
pwm_set(TIM4, 2, percentage);
|
||||
}
|
||||
|
||||
static void dos_set_fan_enabled(bool enabled){
|
||||
set_gpio_output(GPIOA, 1, enabled);
|
||||
}
|
||||
|
||||
static void dos_set_siren(bool enabled){
|
||||
set_gpio_output(GPIOC, 12, enabled);
|
||||
}
|
||||
|
||||
static uint32_t dos_read_voltage_mV(void){
|
||||
return adc_get_mV(12) * 11U;
|
||||
}
|
||||
|
||||
static bool dos_read_som_gpio (void){
|
||||
return (get_gpio_input(GPIOC, 2) != 0);
|
||||
}
|
||||
|
||||
static void dos_init(void) {
|
||||
common_init_gpio();
|
||||
|
||||
// A8,A15: normal CAN3 mode
|
||||
set_gpio_alternate(GPIOA, 8, GPIO_AF11_CAN3);
|
||||
set_gpio_alternate(GPIOA, 15, GPIO_AF11_CAN3);
|
||||
|
||||
// C8: FAN PWM aka TIM3_CH3
|
||||
set_gpio_alternate(GPIOC, 8, GPIO_AF2_TIM3);
|
||||
|
||||
// C2: SOM GPIO used as input (fan control at boot)
|
||||
set_gpio_mode(GPIOC, 2, MODE_INPUT);
|
||||
set_gpio_pullup(GPIOC, 2, PULL_DOWN);
|
||||
|
||||
// Initialize IR PWM and set to 0%
|
||||
set_gpio_alternate(GPIOB, 7, GPIO_AF2_TIM4);
|
||||
pwm_init(TIM4, 2);
|
||||
dos_set_ir_power(0U);
|
||||
|
||||
// Bootkick
|
||||
dos_set_bootkick(true);
|
||||
|
||||
// Init clock source (camera strobe) using PWM
|
||||
clock_source_init(false);
|
||||
}
|
||||
|
||||
static harness_configuration dos_harness_config = {
|
||||
.has_harness = true,
|
||||
.GPIO_SBU1 = GPIOC,
|
||||
.GPIO_SBU2 = GPIOC,
|
||||
.GPIO_relay_SBU1 = GPIOC,
|
||||
.GPIO_relay_SBU2 = GPIOC,
|
||||
.pin_SBU1 = 0,
|
||||
.pin_SBU2 = 3,
|
||||
.pin_relay_SBU1 = 10,
|
||||
.pin_relay_SBU2 = 11,
|
||||
.adc_channel_SBU1 = 10,
|
||||
.adc_channel_SBU2 = 13
|
||||
};
|
||||
|
||||
board board_dos = {
|
||||
.harness_config = &dos_harness_config,
|
||||
#ifdef ENABLE_SPI
|
||||
.has_spi = true,
|
||||
#else
|
||||
.has_spi = false,
|
||||
#endif
|
||||
.has_canfd = false,
|
||||
.fan_max_rpm = 6500U,
|
||||
.fan_max_pwm = 100U,
|
||||
.avdd_mV = 3300U,
|
||||
.fan_stall_recovery = true,
|
||||
.fan_enable_cooldown_time = 3U,
|
||||
.init = dos_init,
|
||||
.init_bootloader = unused_init_bootloader,
|
||||
.enable_can_transceiver = dos_enable_can_transceiver,
|
||||
.led_GPIO = {GPIOC, GPIOC, GPIOC},
|
||||
.led_pin = {9, 7, 6},
|
||||
.set_can_mode = dos_set_can_mode,
|
||||
.check_ignition = dos_check_ignition,
|
||||
.read_voltage_mV = dos_read_voltage_mV,
|
||||
.read_current_mA = unused_read_current,
|
||||
.set_fan_enabled = dos_set_fan_enabled,
|
||||
.set_ir_power = dos_set_ir_power,
|
||||
.set_siren = dos_set_siren,
|
||||
.set_bootkick = dos_set_bootkick,
|
||||
.read_som_gpio = dos_read_som_gpio,
|
||||
.set_amp_enabled = unused_set_amp_enabled
|
||||
};
|
||||
144
panda_tici/board/boards/red.h
Normal file
144
panda_tici/board/boards/red.h
Normal file
@@ -0,0 +1,144 @@
|
||||
#pragma once
|
||||
|
||||
#include "board_declarations.h"
|
||||
|
||||
// ///////////////////////////// //
|
||||
// Red Panda (STM32H7) + Harness //
|
||||
// ///////////////////////////// //
|
||||
|
||||
static void red_enable_can_transceiver(uint8_t transceiver, bool enabled) {
|
||||
switch (transceiver) {
|
||||
case 1U:
|
||||
set_gpio_output(GPIOG, 11, !enabled);
|
||||
break;
|
||||
case 2U:
|
||||
set_gpio_output(GPIOB, 3, !enabled);
|
||||
break;
|
||||
case 3U:
|
||||
set_gpio_output(GPIOD, 7, !enabled);
|
||||
break;
|
||||
case 4U:
|
||||
set_gpio_output(GPIOB, 4, !enabled);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void red_set_can_mode(uint8_t mode) {
|
||||
red_enable_can_transceiver(2U, false);
|
||||
red_enable_can_transceiver(4U, false);
|
||||
switch (mode) {
|
||||
case CAN_MODE_NORMAL:
|
||||
case CAN_MODE_OBD_CAN2:
|
||||
if ((bool)(mode == CAN_MODE_NORMAL) != (bool)(harness.status == HARNESS_STATUS_FLIPPED)) {
|
||||
// B12,B13: disable normal mode
|
||||
set_gpio_pullup(GPIOB, 12, PULL_NONE);
|
||||
set_gpio_mode(GPIOB, 12, MODE_ANALOG);
|
||||
|
||||
set_gpio_pullup(GPIOB, 13, PULL_NONE);
|
||||
set_gpio_mode(GPIOB, 13, MODE_ANALOG);
|
||||
|
||||
// B5,B6: FDCAN2 mode
|
||||
set_gpio_pullup(GPIOB, 5, PULL_NONE);
|
||||
set_gpio_alternate(GPIOB, 5, GPIO_AF9_FDCAN2);
|
||||
|
||||
set_gpio_pullup(GPIOB, 6, PULL_NONE);
|
||||
set_gpio_alternate(GPIOB, 6, GPIO_AF9_FDCAN2);
|
||||
red_enable_can_transceiver(2U, true);
|
||||
} else {
|
||||
// B5,B6: disable normal mode
|
||||
set_gpio_pullup(GPIOB, 5, PULL_NONE);
|
||||
set_gpio_mode(GPIOB, 5, MODE_ANALOG);
|
||||
|
||||
set_gpio_pullup(GPIOB, 6, PULL_NONE);
|
||||
set_gpio_mode(GPIOB, 6, MODE_ANALOG);
|
||||
// B12,B13: FDCAN2 mode
|
||||
set_gpio_pullup(GPIOB, 12, PULL_NONE);
|
||||
set_gpio_alternate(GPIOB, 12, GPIO_AF9_FDCAN2);
|
||||
|
||||
set_gpio_pullup(GPIOB, 13, PULL_NONE);
|
||||
set_gpio_alternate(GPIOB, 13, GPIO_AF9_FDCAN2);
|
||||
red_enable_can_transceiver(4U, true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static bool red_check_ignition(void) {
|
||||
// ignition is checked through harness
|
||||
return harness_check_ignition();
|
||||
}
|
||||
|
||||
static uint32_t red_read_voltage_mV(void){
|
||||
return adc_get_mV(2) * 11U; // TODO: is this correct?
|
||||
}
|
||||
|
||||
static void red_init(void) {
|
||||
common_init_gpio();
|
||||
|
||||
// G11,B3,D7,B4: transceiver enable
|
||||
set_gpio_pullup(GPIOG, 11, PULL_NONE);
|
||||
set_gpio_mode(GPIOG, 11, MODE_OUTPUT);
|
||||
|
||||
set_gpio_pullup(GPIOB, 3, PULL_NONE);
|
||||
set_gpio_mode(GPIOB, 3, MODE_OUTPUT);
|
||||
|
||||
set_gpio_pullup(GPIOD, 7, PULL_NONE);
|
||||
set_gpio_mode(GPIOD, 7, MODE_OUTPUT);
|
||||
|
||||
set_gpio_pullup(GPIOB, 4, PULL_NONE);
|
||||
set_gpio_mode(GPIOB, 4, MODE_OUTPUT);
|
||||
|
||||
//B1: 5VOUT_S
|
||||
set_gpio_pullup(GPIOB, 1, PULL_NONE);
|
||||
set_gpio_mode(GPIOB, 1, MODE_ANALOG);
|
||||
|
||||
// B14: usb load switch, enabled by pull resistor on board, obsolete for red panda
|
||||
set_gpio_output_type(GPIOB, 14, OUTPUT_TYPE_OPEN_DRAIN);
|
||||
set_gpio_pullup(GPIOB, 14, PULL_UP);
|
||||
set_gpio_mode(GPIOB, 14, MODE_OUTPUT);
|
||||
set_gpio_output(GPIOB, 14, 1);
|
||||
}
|
||||
|
||||
static harness_configuration red_harness_config = {
|
||||
.has_harness = true,
|
||||
.GPIO_SBU1 = GPIOC,
|
||||
.GPIO_SBU2 = GPIOA,
|
||||
.GPIO_relay_SBU1 = GPIOC,
|
||||
.GPIO_relay_SBU2 = GPIOC,
|
||||
.pin_SBU1 = 4,
|
||||
.pin_SBU2 = 1,
|
||||
.pin_relay_SBU1 = 10,
|
||||
.pin_relay_SBU2 = 11,
|
||||
.adc_channel_SBU1 = 4, //ADC12_INP4
|
||||
.adc_channel_SBU2 = 17 //ADC1_INP17
|
||||
};
|
||||
|
||||
board board_red = {
|
||||
.set_bootkick = unused_set_bootkick,
|
||||
.harness_config = &red_harness_config,
|
||||
.has_spi = false,
|
||||
.has_canfd = true,
|
||||
.fan_max_rpm = 0U,
|
||||
.fan_max_pwm = 100U,
|
||||
.avdd_mV = 3300U,
|
||||
.fan_stall_recovery = false,
|
||||
.fan_enable_cooldown_time = 0U,
|
||||
.init = red_init,
|
||||
.init_bootloader = unused_init_bootloader,
|
||||
.enable_can_transceiver = red_enable_can_transceiver,
|
||||
.led_GPIO = {GPIOE, GPIOE, GPIOE},
|
||||
.led_pin = {4, 3, 2},
|
||||
.set_can_mode = red_set_can_mode,
|
||||
.check_ignition = red_check_ignition,
|
||||
.read_voltage_mV = red_read_voltage_mV,
|
||||
.read_current_mA = unused_read_current,
|
||||
.set_fan_enabled = unused_set_fan_enabled,
|
||||
.set_ir_power = unused_set_ir_power,
|
||||
.set_siren = unused_set_siren,
|
||||
.read_som_gpio = unused_read_som_gpio,
|
||||
.set_amp_enabled = unused_set_amp_enabled
|
||||
};
|
||||
180
panda_tici/board/boards/tres.h
Normal file
180
panda_tici/board/boards/tres.h
Normal file
@@ -0,0 +1,180 @@
|
||||
#pragma once
|
||||
|
||||
#include "board_declarations.h"
|
||||
|
||||
// ///////////////////////////
|
||||
// Tres (STM32H7) + Harness //
|
||||
// ///////////////////////////
|
||||
|
||||
static bool tres_ir_enabled;
|
||||
static bool tres_fan_enabled;
|
||||
static void tres_update_fan_ir_power(void) {
|
||||
set_gpio_output(GPIOD, 3, tres_ir_enabled || tres_fan_enabled);
|
||||
}
|
||||
|
||||
static void tres_set_ir_power(uint8_t percentage){
|
||||
tres_ir_enabled = (percentage > 0U);
|
||||
tres_update_fan_ir_power();
|
||||
pwm_set(TIM3, 4, percentage);
|
||||
}
|
||||
|
||||
static void tres_set_bootkick(BootState state) {
|
||||
set_gpio_output(GPIOA, 0, state != BOOT_BOOTKICK);
|
||||
set_gpio_output(GPIOC, 12, state != BOOT_RESET);
|
||||
}
|
||||
|
||||
static void tres_set_fan_enabled(bool enabled) {
|
||||
// NOTE: fan controller reset doesn't work on a tres if IR is enabled
|
||||
tres_fan_enabled = enabled;
|
||||
tres_update_fan_ir_power();
|
||||
}
|
||||
|
||||
static void tres_enable_can_transceiver(uint8_t transceiver, bool enabled) {
|
||||
static bool can0_enabled = false;
|
||||
static bool can2_enabled = false;
|
||||
|
||||
switch (transceiver) {
|
||||
case 1U:
|
||||
can0_enabled = enabled;
|
||||
break;
|
||||
case 2U:
|
||||
set_gpio_output(GPIOB, 10, !enabled);
|
||||
break;
|
||||
case 3U:
|
||||
can2_enabled = enabled;
|
||||
break;
|
||||
case 4U:
|
||||
set_gpio_output(GPIOB, 11, !enabled);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// CAN0 and 2 are tied, so enable both if either is enabled
|
||||
set_gpio_output(GPIOG, 11, !(can0_enabled || can2_enabled));
|
||||
set_gpio_output(GPIOD, 7, !(can0_enabled || can2_enabled));
|
||||
}
|
||||
|
||||
static void tres_set_can_mode(uint8_t mode) {
|
||||
current_board->enable_can_transceiver(2U, false);
|
||||
current_board->enable_can_transceiver(4U, false);
|
||||
switch (mode) {
|
||||
case CAN_MODE_NORMAL:
|
||||
case CAN_MODE_OBD_CAN2:
|
||||
if ((bool)(mode == CAN_MODE_NORMAL) != (bool)(harness.status == HARNESS_STATUS_FLIPPED)) {
|
||||
// B12,B13: disable normal mode
|
||||
set_gpio_pullup(GPIOB, 12, PULL_NONE);
|
||||
set_gpio_mode(GPIOB, 12, MODE_ANALOG);
|
||||
|
||||
set_gpio_pullup(GPIOB, 13, PULL_NONE);
|
||||
set_gpio_mode(GPIOB, 13, MODE_ANALOG);
|
||||
|
||||
// B5,B6: FDCAN2 mode
|
||||
set_gpio_pullup(GPIOB, 5, PULL_NONE);
|
||||
set_gpio_alternate(GPIOB, 5, GPIO_AF9_FDCAN2);
|
||||
|
||||
set_gpio_pullup(GPIOB, 6, PULL_NONE);
|
||||
set_gpio_alternate(GPIOB, 6, GPIO_AF9_FDCAN2);
|
||||
current_board->enable_can_transceiver(2U, true);
|
||||
} else {
|
||||
// B5,B6: disable normal mode
|
||||
set_gpio_pullup(GPIOB, 5, PULL_NONE);
|
||||
set_gpio_mode(GPIOB, 5, MODE_ANALOG);
|
||||
|
||||
set_gpio_pullup(GPIOB, 6, PULL_NONE);
|
||||
set_gpio_mode(GPIOB, 6, MODE_ANALOG);
|
||||
// B12,B13: FDCAN2 mode
|
||||
set_gpio_pullup(GPIOB, 12, PULL_NONE);
|
||||
set_gpio_alternate(GPIOB, 12, GPIO_AF9_FDCAN2);
|
||||
|
||||
set_gpio_pullup(GPIOB, 13, PULL_NONE);
|
||||
set_gpio_alternate(GPIOB, 13, GPIO_AF9_FDCAN2);
|
||||
current_board->enable_can_transceiver(4U, true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static bool tres_read_som_gpio (void) {
|
||||
return (get_gpio_input(GPIOC, 2) != 0);
|
||||
}
|
||||
|
||||
static void tres_init(void) {
|
||||
// Enable USB 3.3V LDO for USB block
|
||||
register_set_bits(&(PWR->CR3), PWR_CR3_USBREGEN);
|
||||
register_set_bits(&(PWR->CR3), PWR_CR3_USB33DEN);
|
||||
while ((PWR->CR3 & PWR_CR3_USB33RDY) == 0U);
|
||||
|
||||
common_init_gpio();
|
||||
|
||||
// C2: SOM GPIO used as input (fan control at boot)
|
||||
set_gpio_mode(GPIOC, 2, MODE_INPUT);
|
||||
set_gpio_pullup(GPIOC, 2, PULL_DOWN);
|
||||
|
||||
// SOM bootkick + reset lines
|
||||
// WARNING: make sure output state is set before configuring as output
|
||||
tres_set_bootkick(BOOT_BOOTKICK);
|
||||
set_gpio_mode(GPIOC, 12, MODE_OUTPUT);
|
||||
|
||||
// SOM debugging UART
|
||||
gpio_uart7_init();
|
||||
uart_init(&uart_ring_som_debug, 115200);
|
||||
|
||||
// fan setup
|
||||
set_gpio_alternate(GPIOC, 8, GPIO_AF2_TIM3);
|
||||
|
||||
// Initialize IR PWM and set to 0%
|
||||
set_gpio_alternate(GPIOC, 9, GPIO_AF2_TIM3);
|
||||
pwm_init(TIM3, 4);
|
||||
tres_set_ir_power(0U);
|
||||
|
||||
// Fake siren
|
||||
set_gpio_alternate(GPIOC, 10, GPIO_AF4_I2C5);
|
||||
set_gpio_alternate(GPIOC, 11, GPIO_AF4_I2C5);
|
||||
register_set_bits(&(GPIOC->OTYPER), GPIO_OTYPER_OT10 | GPIO_OTYPER_OT11); // open drain
|
||||
|
||||
// Clock source
|
||||
clock_source_init(false);
|
||||
}
|
||||
|
||||
static harness_configuration tres_harness_config = {
|
||||
.has_harness = true,
|
||||
.GPIO_SBU1 = GPIOC,
|
||||
.GPIO_SBU2 = GPIOA,
|
||||
.GPIO_relay_SBU1 = GPIOA,
|
||||
.GPIO_relay_SBU2 = GPIOA,
|
||||
.pin_SBU1 = 4,
|
||||
.pin_SBU2 = 1,
|
||||
.pin_relay_SBU1 = 8,
|
||||
.pin_relay_SBU2 = 3,
|
||||
.adc_channel_SBU1 = 4, // ADC12_INP4
|
||||
.adc_channel_SBU2 = 17 // ADC1_INP17
|
||||
};
|
||||
|
||||
board board_tres = {
|
||||
.harness_config = &tres_harness_config,
|
||||
.has_spi = true,
|
||||
.has_canfd = true,
|
||||
.fan_max_rpm = 6600U,
|
||||
.fan_max_pwm = 100U,
|
||||
.avdd_mV = 1800U,
|
||||
.fan_stall_recovery = false,
|
||||
.fan_enable_cooldown_time = 3U,
|
||||
.init = tres_init,
|
||||
.init_bootloader = unused_init_bootloader,
|
||||
.enable_can_transceiver = tres_enable_can_transceiver,
|
||||
.led_GPIO = {GPIOE, GPIOE, GPIOE},
|
||||
.led_pin = {4, 3, 2},
|
||||
.set_can_mode = tres_set_can_mode,
|
||||
.check_ignition = red_check_ignition,
|
||||
.read_voltage_mV = red_read_voltage_mV,
|
||||
.read_current_mA = unused_read_current,
|
||||
.set_fan_enabled = tres_set_fan_enabled,
|
||||
.set_ir_power = tres_set_ir_power,
|
||||
.set_siren = fake_siren_set,
|
||||
.set_bootkick = tres_set_bootkick,
|
||||
.read_som_gpio = tres_read_som_gpio,
|
||||
.set_amp_enabled = unused_set_amp_enabled
|
||||
};
|
||||
32
panda_tici/board/boards/unused_funcs.h
Normal file
32
panda_tici/board/boards/unused_funcs.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
void unused_init_bootloader(void) {
|
||||
}
|
||||
|
||||
void unused_set_ir_power(uint8_t percentage) {
|
||||
UNUSED(percentage);
|
||||
}
|
||||
|
||||
void unused_set_fan_enabled(bool enabled) {
|
||||
UNUSED(enabled);
|
||||
}
|
||||
|
||||
void unused_set_siren(bool enabled) {
|
||||
UNUSED(enabled);
|
||||
}
|
||||
|
||||
uint32_t unused_read_current(void) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
void unused_set_bootkick(BootState state) {
|
||||
UNUSED(state);
|
||||
}
|
||||
|
||||
bool unused_read_som_gpio(void) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void unused_set_amp_enabled(bool enabled) {
|
||||
UNUSED(enabled);
|
||||
}
|
||||
90
panda_tici/board/bootstub.c
Normal file
90
panda_tici/board/bootstub.c
Normal file
@@ -0,0 +1,90 @@
|
||||
#define BOOTSTUB
|
||||
|
||||
#define VERS_TAG 0x53524556
|
||||
#define MIN_VERSION 2
|
||||
|
||||
// ********************* Includes *********************
|
||||
#include "config.h"
|
||||
|
||||
#include "drivers/led.h"
|
||||
#include "drivers/pwm.h"
|
||||
#include "drivers/usb.h"
|
||||
|
||||
#include "early_init.h"
|
||||
#include "provision.h"
|
||||
|
||||
#include "crypto/rsa.h"
|
||||
#include "crypto/sha.h"
|
||||
|
||||
#include "obj/cert.h"
|
||||
#include "obj/gitversion.h"
|
||||
#include "flasher.h"
|
||||
|
||||
// cppcheck-suppress unusedFunction ; used in headers not included in cppcheck
|
||||
void __initialize_hardware_early(void) {
|
||||
early_initialization();
|
||||
}
|
||||
|
||||
void fail(void) {
|
||||
soft_flasher_start();
|
||||
}
|
||||
|
||||
// know where to sig check
|
||||
extern void *_app_start[];
|
||||
|
||||
// FIXME: sometimes your panda will fail flashing and will quickly blink a single Green LED
|
||||
// BOUNTY: $200 coupon on shop.comma.ai or $100 check.
|
||||
|
||||
int main(void) {
|
||||
// Init interrupt table
|
||||
init_interrupts(true);
|
||||
|
||||
disable_interrupts();
|
||||
clock_init();
|
||||
detect_board_type();
|
||||
|
||||
#ifdef PANDA_JUNGLE
|
||||
current_board->set_panda_power(true);
|
||||
#endif
|
||||
|
||||
if (enter_bootloader_mode == ENTER_SOFTLOADER_MAGIC) {
|
||||
enter_bootloader_mode = 0;
|
||||
soft_flasher_start();
|
||||
}
|
||||
|
||||
// validate length
|
||||
int len = (int)_app_start[0];
|
||||
if ((len < 8) || (len > (0x1000000 - 0x4000 - 4 - RSANUMBYTES))) goto fail;
|
||||
|
||||
// compute SHA hash
|
||||
uint8_t digest[SHA_DIGEST_SIZE];
|
||||
SHA_hash(&_app_start[1], len-4, digest);
|
||||
|
||||
// verify version, last bytes in the signed area
|
||||
uint32_t vers[2] = {0};
|
||||
memcpy(&vers, ((void*)&_app_start[0]) + len - sizeof(vers), sizeof(vers));
|
||||
if (vers[0] != VERS_TAG || vers[1] < MIN_VERSION) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
// verify RSA signature
|
||||
if (RSA_verify(&release_rsa_key, ((void*)&_app_start[0]) + len, RSANUMBYTES, digest, SHA_DIGEST_SIZE)) {
|
||||
goto good;
|
||||
}
|
||||
|
||||
// allow debug if built from source
|
||||
#ifdef ALLOW_DEBUG
|
||||
if (RSA_verify(&debug_rsa_key, ((void*)&_app_start[0]) + len, RSANUMBYTES, digest, SHA_DIGEST_SIZE)) {
|
||||
goto good;
|
||||
}
|
||||
#endif
|
||||
|
||||
// here is a failure
|
||||
fail:
|
||||
fail();
|
||||
return 0;
|
||||
good:
|
||||
// jump to flash
|
||||
((void(*)(void)) _app_start[1])();
|
||||
return 0;
|
||||
}
|
||||
18
panda_tici/board/bootstub_declarations.h
Normal file
18
panda_tici/board/bootstub_declarations.h
Normal file
@@ -0,0 +1,18 @@
|
||||
// ******************** Prototypes ********************
|
||||
void print(const char *a){ UNUSED(a); }
|
||||
void puth(uint8_t i){ UNUSED(i); }
|
||||
void puth2(uint8_t i){ UNUSED(i); }
|
||||
void puth4(uint8_t i){ UNUSED(i); }
|
||||
void hexdump(const void *a, int l){ UNUSED(a); UNUSED(l); }
|
||||
typedef struct board board;
|
||||
typedef struct harness_configuration harness_configuration;
|
||||
void pwm_init(TIM_TypeDef *TIM, uint8_t channel);
|
||||
void pwm_set(TIM_TypeDef *TIM, uint8_t channel, uint8_t percentage);
|
||||
// No UART support in bootloader
|
||||
typedef struct uart_ring {} uart_ring;
|
||||
uart_ring uart_ring_som_debug;
|
||||
void uart_init(uart_ring *q, int baud) { UNUSED(q); UNUSED(baud); }
|
||||
|
||||
// ********************* Globals **********************
|
||||
uint8_t hw_type = 0;
|
||||
board *current_board;
|
||||
5
panda_tici/board/can.h
Normal file
5
panda_tici/board/can.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#define PANDA_CAN_CNT 3U
|
||||
|
||||
#include "opendbc/safety/can.h"
|
||||
122
panda_tici/board/can_comms.h
Normal file
122
panda_tici/board/can_comms.h
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
CAN transactions to and from the host come in the form of
|
||||
a certain number of CANPacket_t. The transaction is split
|
||||
into multiple transfers or chunks.
|
||||
|
||||
* comms_can_read outputs this buffer in chunks of a specified length.
|
||||
chunks are always the given length, except the last one.
|
||||
* comms_can_write reads in this buffer in chunks.
|
||||
* both functions maintain an overflow buffer for a partial CANPacket_t that
|
||||
spans multiple transfers/chunks.
|
||||
* the overflow buffers are reset by a dedicated control transfer handler,
|
||||
which is sent by the host on each start of a connection.
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
uint32_t ptr;
|
||||
uint32_t tail_size;
|
||||
uint8_t data[72];
|
||||
} asm_buffer;
|
||||
|
||||
static asm_buffer can_read_buffer = {.ptr = 0U, .tail_size = 0U};
|
||||
|
||||
int comms_can_read(uint8_t *data, uint32_t max_len) {
|
||||
uint32_t pos = 0U;
|
||||
|
||||
// Send tail of previous message if it is in buffer
|
||||
if (can_read_buffer.ptr > 0U) {
|
||||
uint32_t overflow_len = MIN(max_len - pos, can_read_buffer.ptr);
|
||||
(void)memcpy(&data[pos], can_read_buffer.data, overflow_len);
|
||||
pos += overflow_len;
|
||||
(void)memcpy(can_read_buffer.data, &can_read_buffer.data[overflow_len], can_read_buffer.ptr - overflow_len);
|
||||
can_read_buffer.ptr -= overflow_len;
|
||||
}
|
||||
|
||||
if (can_read_buffer.ptr == 0U) {
|
||||
// Fill rest of buffer with new data
|
||||
CANPacket_t can_packet;
|
||||
while ((pos < max_len) && can_pop(&can_rx_q, &can_packet)) {
|
||||
uint32_t pckt_len = CANPACKET_HEAD_SIZE + dlc_to_len[can_packet.data_len_code];
|
||||
if ((pos + pckt_len) <= max_len) {
|
||||
(void)memcpy(&data[pos], (uint8_t*)&can_packet, pckt_len);
|
||||
pos += pckt_len;
|
||||
} else {
|
||||
(void)memcpy(&data[pos], (uint8_t*)&can_packet, max_len - pos);
|
||||
can_read_buffer.ptr += pckt_len - (max_len - pos);
|
||||
// cppcheck-suppress objectIndex
|
||||
(void)memcpy(can_read_buffer.data, &((uint8_t*)&can_packet)[(max_len - pos)], can_read_buffer.ptr);
|
||||
pos = max_len;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
static asm_buffer can_write_buffer = {.ptr = 0U, .tail_size = 0U};
|
||||
|
||||
// send on CAN
|
||||
void comms_can_write(const uint8_t *data, uint32_t len) {
|
||||
uint32_t pos = 0U;
|
||||
|
||||
// Assembling can message with data from buffer
|
||||
if (can_write_buffer.ptr != 0U) {
|
||||
if (can_write_buffer.tail_size <= (len - pos)) {
|
||||
// we have enough data to complete the buffer
|
||||
CANPacket_t to_push = {0};
|
||||
(void)memcpy(&can_write_buffer.data[can_write_buffer.ptr], &data[pos], can_write_buffer.tail_size);
|
||||
can_write_buffer.ptr += can_write_buffer.tail_size;
|
||||
pos += can_write_buffer.tail_size;
|
||||
|
||||
// send out
|
||||
(void)memcpy((uint8_t*)&to_push, can_write_buffer.data, can_write_buffer.ptr);
|
||||
can_send(&to_push, to_push.bus, false);
|
||||
|
||||
// reset overflow buffer
|
||||
can_write_buffer.ptr = 0U;
|
||||
can_write_buffer.tail_size = 0U;
|
||||
} else {
|
||||
// maybe next time
|
||||
uint32_t data_size = len - pos;
|
||||
(void) memcpy(&can_write_buffer.data[can_write_buffer.ptr], &data[pos], data_size);
|
||||
can_write_buffer.tail_size -= data_size;
|
||||
can_write_buffer.ptr += data_size;
|
||||
pos += data_size;
|
||||
}
|
||||
}
|
||||
|
||||
// rest of the message
|
||||
while (pos < len) {
|
||||
uint32_t pckt_len = CANPACKET_HEAD_SIZE + dlc_to_len[(data[pos] >> 4U)];
|
||||
if ((pos + pckt_len) <= len) {
|
||||
CANPacket_t to_push = {0};
|
||||
(void)memcpy((uint8_t*)&to_push, &data[pos], pckt_len);
|
||||
can_send(&to_push, to_push.bus, false);
|
||||
pos += pckt_len;
|
||||
} else {
|
||||
(void)memcpy(can_write_buffer.data, &data[pos], len - pos);
|
||||
can_write_buffer.ptr = len - pos;
|
||||
can_write_buffer.tail_size = pckt_len - can_write_buffer.ptr;
|
||||
pos += can_write_buffer.ptr;
|
||||
}
|
||||
}
|
||||
|
||||
refresh_can_tx_slots_available();
|
||||
}
|
||||
|
||||
void comms_can_reset(void) {
|
||||
can_write_buffer.ptr = 0U;
|
||||
can_write_buffer.tail_size = 0U;
|
||||
can_read_buffer.ptr = 0U;
|
||||
can_read_buffer.tail_size = 0U;
|
||||
}
|
||||
|
||||
// TODO: make this more general!
|
||||
void refresh_can_tx_slots_available(void) {
|
||||
if (can_tx_check_min_slots_free(MAX_CAN_MSGS_PER_USB_BULK_TRANSFER)) {
|
||||
can_tx_comms_resume_usb();
|
||||
}
|
||||
if (can_tx_check_min_slots_free(MAX_CAN_MSGS_PER_SPI_BULK_TRANSFER)) {
|
||||
can_tx_comms_resume_spi();
|
||||
}
|
||||
}
|
||||
29
panda_tici/board/can_declarations.h
Normal file
29
panda_tici/board/can_declarations.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
// bump this when changing the CAN packet
|
||||
#define CAN_PACKET_VERSION 4
|
||||
|
||||
#define CANPACKET_HEAD_SIZE 6U
|
||||
|
||||
#if !defined(STM32F4)
|
||||
#define CANFD
|
||||
#define CANPACKET_DATA_SIZE_MAX 64U
|
||||
#else
|
||||
#define CANPACKET_DATA_SIZE_MAX 8U
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
unsigned char fd : 1;
|
||||
unsigned char bus : 3;
|
||||
unsigned char data_len_code : 4; // lookup length with dlc_to_len
|
||||
unsigned char rejected : 1;
|
||||
unsigned char returned : 1;
|
||||
unsigned char extended : 1;
|
||||
unsigned int addr : 29;
|
||||
unsigned char checksum;
|
||||
unsigned char data[CANPACKET_DATA_SIZE_MAX];
|
||||
} __attribute__((packed, aligned(4))) CANPacket_t;
|
||||
|
||||
#define GET_BUS(msg) ((msg)->bus)
|
||||
#define GET_LEN(msg) (dlc_to_len[(msg)->data_len_code])
|
||||
#define GET_ADDR(msg) ((msg)->addr)
|
||||
12
panda_tici/board/comms_definitions.h
Normal file
12
panda_tici/board/comms_definitions.h
Normal file
@@ -0,0 +1,12 @@
|
||||
typedef struct {
|
||||
uint8_t request;
|
||||
uint16_t param1;
|
||||
uint16_t param2;
|
||||
uint16_t length;
|
||||
} __attribute__((packed)) ControlPacket_t;
|
||||
|
||||
int comms_control_handler(ControlPacket_t *req, uint8_t *resp);
|
||||
void comms_endpoint2_write(const uint8_t *data, uint32_t len);
|
||||
void comms_can_write(const uint8_t *data, uint32_t len);
|
||||
int comms_can_read(uint8_t *data, uint32_t max_len);
|
||||
void comms_can_reset(void);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user