StarPilot

This commit is contained in:
firestar5683
2026-03-12 01:49:47 -05:00
parent 0e9ef526f7
commit d0e1db6766
2171 changed files with 590688 additions and 247754 deletions
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
derive_feasible_params.py
Dynamically parses the OpenPilot/StarPilot codebase to cross-reference logically
registered Param keys with UI string literals. This ensures that no hidden or
dynamically-instantiated UI toggles are missed, outputting a highly accurate "Golden List"
of parameters that can be safely modified by The Pond or other configuration interfaces.
"""
import os
import re
def get_repo_root() -> str:
# Resolves to the root of the StarPilot repository based on this script's location
return os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))
# Constants
REPO_ROOT = get_repo_root()
PARAMS_CC_PATH = os.path.join(REPO_ROOT, 'common/params.cc')
UI_DIRECTORIES = [
os.path.join(REPO_ROOT, 'selfdrive/ui'),
os.path.join(REPO_ROOT, 'frogpilot/ui')
]
# A curated list of parameters that are known to be strictly readable state metadata
# rather than user-toggled configurations.
KNOWN_READ_ONLY = {
"ApiCache_Device", "ApiCache_DriveStats", "ApiCache_NavDestinations",
"CarMake", "CarModel", "CarModelName", "CarParamsPersistent", "CarVin",
"ClusterOffset", "Compass", "DeveloperSidebarMetric1", "DeveloperSidebarMetric2",
"DeveloperSidebarMetric3", "DeveloperSidebarMetric4", "DeveloperSidebarMetric5",
"DeveloperSidebarMetric6", "DeveloperSidebarMetric7", "DongleId",
"FrogPilotCarParamsPersistent", "FrogPilotDrives", "FrogPilotKilometers",
"FrogPilotMinutes", "GitBranch", "GitCommit", "GitCommitDate", "GitDiff",
"GitRemote", "GithubSshKeys", "GithubUsername", "HardwareSerial", "IMEI",
"InstallDate", "IsRhdDetected", "KonikMinutes", "LastGPSPosition",
"LastMapsUpdate", "LastUpdateTime", "ModelDrivesAndScores", "ModelReleasedDates",
"ModelVersions", "PrimeType", "TermsVersion", "TrainingVersion", "Version",
"openpilotMinutes", "CompletedTrainingVersion"
}
def extract_registered_keys(params_path: str) -> set:
"""Extracts all legally registered parameter keys from common/params.cc"""
registered_keys = set()
try:
with open(params_path, 'r', encoding='utf-8') as f:
content = f.read()
# Isolate the keys `unordered_map` block
keys_block_match = re.search(r'unordered_map<std::string, uint32_t> keys = \{(.*?)\};', content, re.DOTALL)
if not keys_block_match:
print("Error: Could not locate 'keys' map in params.cc")
return registered_keys
# Extract {"KeyName", FLAG} entries
for match in re.finditer(r'\{"([A-Za-z0-9_]+)",\s*([^}]+)\}', keys_block_match.group(1)):
key, flag = match.group(1), match.group(2)
# Remove keys that are strictly internal ephemeral states
if 'CLEAR_ON_MANAGER_START' not in flag:
registered_keys.add(key)
except FileNotFoundError:
print(f"Error: Could not find params source file at {params_path}")
return registered_keys
def extract_ui_string_literals(ui_dirs: list) -> set:
"""Recursively walks UI directories to extract every string literal."""
ui_strings = set()
valid_extensions = ('.cc', '.h', '.cpp', '.hpp', '.qml')
for directory in ui_dirs:
if not os.path.exists(directory):
print(f"Warning: UI directory not found {directory}")
continue
for root, _, files in os.walk(directory):
for file in files:
if file.endswith(valid_extensions):
filepath = os.path.join(root, file)
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
# Extract all "StringLiterals" block
matches = re.findall(r'\"([A-Za-z0-9_]+)\"', f.read())
ui_strings.update(matches)
return ui_strings
def main():
print(f"Starting parameter derivation inside {REPO_ROOT}...")
# 1. Fetch
registered_keys = extract_registered_keys(PARAMS_CC_PATH)
ui_strings = extract_ui_string_literals(UI_DIRECTORIES)
# 2. Intersect
feasible_keys = registered_keys.intersection(ui_strings)
# 3. Filter Read-Only
editable_keys = feasible_keys - KNOWN_READ_ONLY
# 4. Export
output_path = os.path.join(os.path.dirname(__file__), 'feasibleparams.txt')
try:
with open(output_path, 'w', encoding='utf-8') as f:
f.write("Dynamically Derived Feasible Param Candidates (The Golden List)\n")
f.write("===============================================================\n\n")
f.write(f"Total globally registered C++ keys: {len(registered_keys)}\n")
f.write(f"Total explicit UI string references: {len(feasible_keys)}\n")
f.write(f"Total Editable/Toggleable targets: {len(editable_keys)}\n\n")
for key in sorted(list(editable_keys)):
f.write(f"{key}\n")
print(f"Successfully derived {len(editable_keys)} highly feasible parameter targets.")
print(f"Report exported to: {output_path}")
except Exception as e:
print(f"Error writing to output file: {e}")
if __name__ == '__main__':
main()
+337
View File
@@ -0,0 +1,337 @@
Dynamically Derived Feasible Param Candidates (The Golden List)
===============================================================
Total globally registered C++ keys: 417
Total explicit UI string references: 369
Total Editable/Toggleable targets: 331
AMapKey1
AMapKey2
AccelerationPath
AccelerationProfile
AdjacentLeadsUI
AdjacentPath
AdjacentPathMetrics
AdvancedCustomUI
AdvancedLateralTune
AdvancedLongitudinalTune
AggressiveFollow
AggressiveFollowHigh
AggressiveJerkAcceleration
AggressiveJerkDanger
AggressiveJerkDeceleration
AggressiveJerkSpeed
AggressiveJerkSpeedDecrease
AggressivePersonalityProfile
AlertVolumeControl
AlwaysOnLateral
AlwaysOnLateralLKAS
AlwaysOnLateralMain
AutomaticUpdates
AutomaticallyDownloadModels
AvailableModelNames
AvailableModelSeries
AvailableModels
BigMap
BlacklistedModels
BlindSpotMetrics
BlindSpotPath
BootLogo
BorderMetrics
CECurves
CECurvesLead
CELead
CEModelStopTime
CENavigation
CENavigationIntersections
CENavigationLead
CENavigationTurns
CESignalLaneDetection
CESignalSpeed
CESlowerLead
CESpeed
CESpeedLead
CEStatus
CEStoppedLead
CalibratedLateralAcceleration
CalibrationParams
CalibrationProgress
CameraView
CommunityFavorites
ConditionalExperimental
CurvatureData
CurveSpeedController
CustomAlerts
CustomColors
CustomCruise
CustomCruiseLong
CustomDistanceIcons
CustomIcons
CustomPersonalities
CustomSignals
CustomSounds
CustomUI
DebugMode
DecelerationProfile
DeveloperMetrics
DeveloperSidebar
DeveloperUI
DeveloperWidgets
DeviceManagement
DeviceShutdown
DisableOnroadUploads
DisableOpenpilotLongitudinal
DiscordUsername
DisengageOnAccelerator
DisengageVolume
DistanceButtonControl
DoToggleReset
DoToggleResetStock
DownloadableBootLogos
DownloadableColors
DownloadableDistanceIcons
DownloadableIcons
DownloadableSignals
DownloadableSounds
DownloadableWheels
DriverCamera
DynamicPathWidth
DynamicPedalsOnUI
EVTuning
EngageVolume
ExperimentalLongitudinalEnabled
ExperimentalMode
ExperimentalModeConfirmed
FPSCounter
Fahrenheit
FavoriteDestinations
ForceAutoTune
ForceAutoTuneOff
ForceFingerprint
ForceMPHDashboard
ForceStops
ForceTorqueController
FrogPilotStats
FrogsGoMoosTweak
FullMap
GMPedalLongitudinal
GoatScream
GreenLightAlert
GsmApn
GsmMetered
GsmRoaming
HasAcceptedTerms
HideAlerts
HideLeadMarker
HideMap
HideMapIcon
HideMaxSpeed
HideSpeed
HideSpeedLimit
HigherBitrate
HolidayThemes
HumanAcceleration
HumanFollowing
IncreaseThermalLimits
IncreasedStoppedDistance
IsDriverViewEnabled
IsLdwEnabled
IsMetric
LKASButtonControl
LaneChangeTime
LaneChanges
LaneDetectionWidth
LaneLinesWidth
LanguageSetting
LateralTune
LeadDepartingAlert
LeadDetectionThreshold
LeadInfo
LiveDelay
LiveParameters
LiveTorqueParameters
LockDoors
LockDoorsTimer
LongDistanceButtonControl
LongPitch
LongitudinalActuatorDelay
LongitudinalActuatorDelayStock
LongitudinalPersonality
LongitudinalTune
LoudBlindspotAlert
LowVoltageShutdown
MapAcceleration
MapDeceleration
MapGears
MapStyle
MapboxPublicKey
MapboxSecretKey
MapsSelected
MaxDesiredAcceleration
MinimumLaneChangeSpeed
Model
ModelRandomizer
ModelUI
ModelVersion
NNFF
NNFFLite
NavPastDestinations
NavSettingLeftSide
NavSettingTime24h
NavigationUI
NewLongAPI
NoLogging
NoUploads
NudgelessLaneChange
NumericalTemp
OSMDownloadLocations
Offset1
Offset2
Offset3
Offset4
Offset5
Offset6
Offset7
OneLaneChange
OnroadDistanceButton
OpenpilotEnabledToggle
OverpassRequests
PathEdgeWidth
PathWidth
PauseAOLOnBrake
PauseLateralOnSignal
PauseLateralSpeed
PedalsOnUI
PersonalizeOpenpilot
PreferredSchedule
PromptDistractedVolume
PromptVolume
QOLLateral
QOLLongitudinal
QOLVisuals
RadarTracksUI
RainbowPath
RandomEvents
RandomThemes
RecordFront
RecordFrontLock
RecoveryPower
RedPanda
RefuseVolume
RelaxedFollow
RelaxedFollowHigh
RelaxedJerkAcceleration
RelaxedJerkDanger
RelaxedJerkDeceleration
RelaxedJerkSpeed
RelaxedJerkSpeedDecrease
RelaxedPersonalityProfile
RemoteStartBootsComma
ReverseCruise
RoadEdgesWidth
RoadNameUI
RotatingWheel
SLCConfirmation
SLCConfirmationHigher
SLCConfirmationLower
SLCFallback
SLCLookaheadHigher
SLCLookaheadLower
SLCMapboxFiller
SLCOverride
SNGHack
ScreenBrightness
ScreenBrightnessOnroad
ScreenManagement
ScreenRecorder
ScreenTimeout
ScreenTimeoutOnroad
SearchInput
SetSpeedLimit
SetSpeedOffset
ShowCEMStatus
ShowCPU
ShowCSCStatus
ShowGPU
ShowIP
ShowMemoryUsage
ShowSLCOffset
ShowSpeedLimits
ShowSteering
ShowStoppingPoint
ShowStoppingPointMetrics
ShowStorageLeft
ShowStorageUsed
ShownToggleDescriptions
Sidebar
SignalMetrics
SpeedLimitChangedAlert
SpeedLimitController
SpeedLimitFiller
SpeedLimitSources
SshEnabled
StandardFollow
StandardFollowHigh
StandardJerkAcceleration
StandardJerkDanger
StandardJerkDeceleration
StandardJerkSpeed
StandardJerkSpeedDecrease
StandardPersonalityProfile
StandbyMode
StartAccel
StartAccelStock
StartupMessageBottom
StartupMessageTop
StaticPedalsOnUI
SteerDelay
SteerDelayStock
SteerFriction
SteerFrictionStock
SteerKP
SteerKPStock
SteerLatAccel
SteerLatAccelStock
SteerOffset
SteerOffsetStock
SteerRatio
SteerRatioStock
StopAccel
StopAccelStock
StopDistance
StoppedTimer
StoppingDecelRate
StoppingDecelRateStock
TacoTune
TacoTuneHacks
TetheringEnabled
ToyotaDoors
TrafficFollow
TrafficJerkAcceleration
TrafficJerkDanger
TrafficJerkDeceleration
TrafficJerkSpeed
TrafficJerkSpeedDecrease
TrafficPersonalityProfile
TrailerLoad
TruckTuning
TuningLevel
TuningLevelConfirmed
TurnDesires
UnlimitedLength
UnlockDoors
UpdaterAvailableBranches
UseKonikServer
UseSI
UseVienna
UserFavorites
VEgoStarting
VEgoStartingStock
VEgoStopping
VEgoStoppingStock
VeryLongDistanceButtonControl
VoltSNG
WarningImmediateVolume
WarningSoftVolume
WheelIcon
WheelSpeed
+550
View File
@@ -0,0 +1,550 @@
import os
import re
import sys
import json
import ast
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))
CATEGORIES = [
{"file": "lateral_settings.cc", "name": "Lateral (Steering)", "icon": "bi-arrows-move"},
{"file": "longitudinal_settings.cc", "name": "Longitudinal (Speed & Following)", "icon": "bi-speedometer2"},
{"file": "visual_settings.cc", "name": "Visual (Display & UI)", "icon": "bi-eye"},
{"file": "sounds_settings.cc", "name": "Sounds & Alerts", "icon": "bi-volume-up"},
{"file": "vehicle_settings.cc", "name": "Vehicle", "icon": "bi-car-front"},
{"file": "device_settings.cc", "name": "Device & Data", "icon": "bi-hdd"},
{"file": "model_settings.cc", "name": "Model & Customization", "icon": "bi-cpu"},
]
DROPDOWN_MAPPING = {
"SelectModel": {
"key": "DrivingModel",
"options_endpoint": "/api/models/installed"
}
}
# Custom controls implemented outside the tuple vectors in Qt settings panels.
# Inject these so regenerated pond layouts retain equivalent functionality.
INJECTED_SECTION_PARAMS = {
"Vehicle": [
{
"key": "CarMake",
"label": "Car Make",
"description": "Select your car make.",
"data_type": "string",
"ui_type": "dropdown",
"options_endpoint": "/api/fingerprints/makes",
},
{
"key": "CarModel",
"label": "Car Model (Fingerprint)",
"description": "Choose the fingerprint platform to use when automatic detection is disabled.",
"data_type": "string",
"ui_type": "dropdown",
"options_endpoint": "/api/fingerprints/models?make={CarMake}",
},
{
"key": "ForceFingerprint",
"label": "Disable Automatic Fingerprint Detection",
"description": "Force the selected fingerprint and prevent it from changing automatically.",
"data_type": "bool",
"ui_type": "toggle",
},
],
}
# Keys explicitly hidden from The Pond's generic settings UI.
HIDDEN_KEYS = {
"FrogsGoMoosTweak",
"LockDoorsTimer",
"NewLongAPI",
"ToyotaDoors",
}
# Keys that are boolean toggles despite ambiguous defaults in frogpilot_variables.py.
FORCE_BOOL_KEYS = {"EVTuning"}
DEVELOPER_SIDEBAR_METRIC_KEYS = {
"DeveloperSidebarMetric1",
"DeveloperSidebarMetric2",
"DeveloperSidebarMetric3",
"DeveloperSidebarMetric4",
"DeveloperSidebarMetric5",
"DeveloperSidebarMetric6",
"DeveloperSidebarMetric7",
}
DEVELOPER_SIDEBAR_METRIC_OPTIONS = [
{"value": 0, "label": "None"},
{"value": 1, "label": "Acceleration: Current"},
{"value": 2, "label": "Acceleration: Max"},
{"value": 3, "label": "Auto Tune: Actuator Delay"},
{"value": 4, "label": "Auto Tune: Friction"},
{"value": 5, "label": "Auto Tune: Lateral Acceleration"},
{"value": 6, "label": "Auto Tune: Steer Ratio"},
{"value": 7, "label": "Auto Tune: Stiffness Factor"},
{"value": 8, "label": "Engagement %: Lateral"},
{"value": 9, "label": "Engagement %: Longitudinal"},
{"value": 10, "label": "Lateral Control: Steering Angle"},
{"value": 11, "label": "Lateral Control: Torque % Used"},
{"value": 12, "label": "Longitudinal Control: Actuator Acceleration Output"},
{"value": 13, "label": "Longitudinal MPC Jerk: Acceleration"},
{"value": 14, "label": "Longitudinal MPC Jerk: Danger Zone"},
{"value": 15, "label": "Longitudinal MPC Jerk: Speed Control"},
{"value": 16, "label": "Driving Model: Current"},
]
PARENT_KEYS_MAPPING = {
"device_settings.cc": {
"deviceManagementKeys": "DeviceManagement",
"screenKeys": "ScreenManagement"
},
"lateral_settings.cc": {
"advancedLateralTuneKeys": "AdvancedLateralTune",
"aolKeys": "AlwaysOnLateral",
"laneChangeKeys": "LaneChanges",
"lateralTuneKeys": "LateralTune",
"qolKeys": "QOLLateral"
},
"longitudinal_settings.cc": {
"advancedLongitudinalTuneKeys": "AdvancedLongitudinalTune",
"aggressivePersonalityKeys": "AggressivePersonalityProfile",
"conditionalExperimentalKeys": "ConditionalExperimental",
"curveSpeedKeys": "CurveSpeedController",
"customDrivingPersonalityKeys": "CustomPersonalities",
"longitudinalTuneKeys": "LongitudinalTune",
"qolKeys": "QOLLongitudinal",
"relaxedPersonalityKeys": "RelaxedPersonalityProfile",
"speedLimitControllerKeys": "SpeedLimitController",
"speedLimitControllerOffsetsKeys": "SpeedLimitController",
"speedLimitControllerQOLKeys": "SpeedLimitController",
"speedLimitControllerVisualKeys": "SpeedLimitController",
"standardPersonalityKeys": "StandardPersonalityProfile",
"trafficPersonalityKeys": "TrafficPersonalityProfile"
},
"sounds_settings.cc": {
"alertVolumeControlKeys": "AlertVolumeControl",
"customAlertsKeys": "CustomAlerts"
},
"theme_settings.cc": {
"customThemeKeys": "CustomTheme"
},
"visual_settings.cc": {
"advancedCustomOnroadUIKeys": "AdvancedCustomUI",
"customOnroadUIKeys": "CustomUI",
"developerMetricKeys": "DeveloperMetrics",
"developerSidebarKeys": "DeveloperSidebar",
"developerUIKeys": "DeveloperUI",
"developerWidgetKeys": "DeveloperWidgets",
"modelUIKeys": "ModelUI",
"navigationUIKeys": "NavigationUI",
"qualityOfLifeKeys": "QOLVisuals"
},
"vehicle_settings.cc": {}
}
ALL_PARENT_KEYS = set()
for cmap in PARENT_KEYS_MAPPING.values():
for parent in cmap.values():
ALL_PARENT_KEYS.add(parent)
def get_variables_data():
filepath = os.path.join(REPO_ROOT, "frogpilot/common/frogpilot_variables.py")
excluded = set()
defaults = {}
if not os.path.exists(filepath):
return excluded, defaults
with open(filepath, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
def parse_params_list(value_node):
try:
if isinstance(value_node, ast.List):
for elt in value_node.elts:
if isinstance(elt, ast.Tuple) and len(elt.elts) >= 2:
key_node = elt.elts[0]
val_node = elt.elts[1]
if isinstance(key_node, ast.Constant):
key = key_node.value
if isinstance(val_node, ast.Constant):
val = val_node.value
if isinstance(val, (str, bytes)):
v = val.decode('utf-8') if isinstance(val, bytes) else str(val)
if v in ("0", "1"):
defaults[key] = "bool"
elif "." in v and v.replace(".", "", 1).isdigit():
defaults[key] = "float"
elif v.isdigit():
defaults[key] = "int"
else:
defaults[key] = "string"
else:
defaults[key] = "unknown"
elif isinstance(val_node, ast.Call) and isinstance(val_node.func, ast.Name) and val_node.func.id == "str":
# str(<numeric expression>) is used for several numeric defaults.
defaults[key] = "float"
else:
defaults[key] = "unknown"
except:
pass
for node in tree.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if getattr(target, 'id', '') == 'EXCLUDED_KEYS':
try:
excluded = ast.literal_eval(node.value)
except:
pass
elif getattr(target, 'id', '') in ('frogpilot_default_params', 'misc_tuning_levels'):
parse_params_list(node.value)
elif isinstance(node, ast.AnnAssign):
if getattr(node.target, 'id', '') in ('frogpilot_default_params', 'misc_tuning_levels'):
parse_params_list(node.value)
return excluded, defaults
EXCLUDED_KEYS, DEFAULT_TYPES = get_variables_data()
def get_editable_keys():
filepath = os.path.join(REPO_ROOT, "frogpilot/common/frogpilot_variables.py")
editable = set()
if not os.path.exists(filepath):
return editable
with open(filepath, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
for node in tree.body:
value_node = None
if isinstance(node, ast.Assign):
for target in node.targets:
if getattr(target, 'id', '') == 'frogpilot_default_params':
value_node = node.value
break
elif isinstance(node, ast.AnnAssign):
if getattr(node.target, 'id', '') == 'frogpilot_default_params':
value_node = node.value
if isinstance(value_node, ast.List):
for elt in value_node.elts:
if isinstance(elt, ast.Tuple) and elt.elts and isinstance(elt.elts[0], ast.Constant):
editable.add(elt.elts[0].value)
return editable
EDITABLE_KEYS = get_editable_keys()
def parse_params_keys_h():
filepath = os.path.join(REPO_ROOT, "common/params_keys.h")
keys = set()
types = {}
if not os.path.exists(filepath):
return keys, types
type_map = {
"BOOL": "bool",
"INT": "int",
"FLOAT": "float",
"STRING": "string",
"JSON": "string",
"BYTES": "string",
}
pattern = re.compile(r'\{"([A-Za-z0-9_]+)",\s*\{[^,]+,\s*([A-Z]+)')
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
match = pattern.search(line)
if not match:
continue
key, ptype = match.groups()
keys.add(key)
types[key] = type_map.get(ptype, "unknown")
return keys, types
if not DEFAULT_TYPES or not EDITABLE_KEYS:
parsed_keys, parsed_types = parse_params_keys_h()
if not EDITABLE_KEYS:
EDITABLE_KEYS = parsed_keys
for key, value in parsed_types.items():
DEFAULT_TYPES.setdefault(key, value)
def get_param_type(key):
return DEFAULT_TYPES.get(key, "unknown")
def extract_bracket_block(text, start_idx):
if text[start_idx] != '{': return ""
depth = 0
in_str = False
escape = False
for i in range(start_idx, len(text)):
char = text[i]
if escape:
escape = False
continue
if char == '\\':
escape = True
continue
if char == '"':
in_str = not in_str
continue
if not in_str:
if char == '{': depth += 1
elif char == '}':
depth -= 1
if depth == 0:
return text[start_idx:i+1]
return ""
def parse_cpp_file(filename):
filepath = os.path.join(REPO_ROOT, "frogpilot/ui/qt/offroad", filename)
if not os.path.exists(filepath): return []
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
vector_match = re.search(
r'(?:const\s+)?std::vector<\s*std::tuple<QString,\s*QString,\s*QString,\s*QString>\s*>\s*\w+\s*\{',
content,
re.DOTALL,
)
if not vector_match: return []
start_idx = vector_match.end() - 1
vector_content = extract_bracket_block(content, start_idx)
local_parent_map = PARENT_KEYS_MAPPING.get(filename, {})
child_to_parent = {}
child_to_qsets = {}
header_filename = filename.replace(".cc", ".h")
header_filepath = os.path.join(REPO_ROOT, "frogpilot/ui/qt/offroad", header_filename)
full_source = content
if os.path.exists(header_filepath):
with open(header_filepath, 'r', encoding='utf-8') as fh:
full_source += "\n" + fh.read()
for qset_match in re.finditer(r'QSet<QString>\s+(\w+)\s*(?:=\s*)?\{([^}]+)\};', full_source):
qset_name = qset_match.group(1)
if qset_name in local_parent_map:
parent_key = local_parent_map[qset_name]
children_str = qset_match.group(2)
children = [c.strip().strip('"') for c in children_str.split(',') if c.strip()]
for child in children:
child_to_parent[child] = parent_key
child_to_qsets.setdefault(child, []).append(qset_name)
items = []
idx = 0
while True:
idx = vector_content.find('{"', idx)
if idx == -1: break
block = extract_bracket_block(vector_content, idx)
if not block:
idx += 1
continue
row_match = re.search(r'\{"([A-Za-z0-9_]+)"\s*,\s*(.*?)\s*\}$', block, re.DOTALL)
if not row_match:
idx += len(block)
continue
key = row_match.group(1)
rest = row_match.group(2)
idx += len(block)
if key in HIDDEN_KEYS or key in EXCLUDED_KEYS or key.startswith("IgnoreMe"):
continue
strings = re.findall(r'tr\("((?:[^"\\]|\\.)+)"\)|"((?:[^"\\]|\\.)+)"', rest)
valid_strings = [s[0] or s[1] for s in strings if s[0] or s[1]]
if not valid_strings: continue
title = valid_strings[0]
desc = valid_strings[1] if len(valid_strings) > 1 else ""
options_endpoint = None
dropdown_options = None
if key in DEVELOPER_SIDEBAR_METRIC_KEYS:
if key not in EDITABLE_KEYS:
continue
widget_type = "dropdown"
data_type = "int"
dropdown_options = DEVELOPER_SIDEBAR_METRIC_OPTIONS
elif key in DROPDOWN_MAPPING:
m = DROPDOWN_MAPPING[key]
key = m["key"]
widget_type = "dropdown"
options_endpoint = m["options_endpoint"]
data_type = "string"
else:
if key not in EDITABLE_KEYS:
continue
data_type = get_param_type(key)
if data_type == "unknown": continue
widget_type = "toggle"
min_val, max_val, step = None, None, None
for i in range(1, 10):
placeholder = f"%{i}"
if placeholder in desc and len(valid_strings) > i + 1:
desc = desc.replace(placeholder, valid_strings[i + 1])
desc = re.sub(r'<br\s*/?>', '\n', desc, flags=re.IGNORECASE)
desc = re.sub(r'<[^>]+>', '', desc)
desc = desc.replace('\\"', '"').strip()
title = re.sub(r'\s*\(\s*Default:\s*%\d\s*\)', '', title)
title = re.sub(r'%\d', '', title).strip()
desc = re.sub(r'\s*\(\s*Default:\s*%\d\s*\)', '', desc)
desc = re.sub(r'%\d', '', desc).strip()
if widget_type == "toggle":
snippet_match = None
# Let's match the original's regex for finding the Toggle = assignment line
search_patterns = [r'param\s*==\s*"' + key + r'"']
for qset_name in child_to_qsets.get(key, []):
search_patterns.append(r'(?:' + qset_name + r'\.contains\(param\))')
for pattern in search_patterns:
match = re.search(pattern + r'.*?[a-zA-Z]+Toggle\s*=\s*(.*?);', content, re.DOTALL)
if match:
snippet_match = match
break
if snippet_match:
assignment = snippet_match.group(1)
if "FrogPilotParamValueControl" in assignment or "FrogPilotParamValueButtonControl" in assignment:
widget_type = "numeric"
if data_type in ("string", "bool", "unknown"):
data_type = "float"
if "alertVolumeControlKeys" in child_to_qsets.get(key, []):
if key in ["WarningImmediateVolume", "WarningSoftVolume"]:
min_val, max_val, step = "25", "101", "1"
else:
min_val, max_val, step = "0", "101", "1"
else:
args_match = re.search(r'Control[^(]*\(([^;]+)\)', assignment)
if args_match:
args_str = args_match.group(1)
num_match = re.search(r'icon\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,(?:[^,]*,){2}\s*([-\d.]+)', args_str)
if num_match:
min_val, max_val, step = num_match.group(1), num_match.group(2), num_match.group(3)
else:
num_match = re.search(r'icon\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)', args_str)
if num_match:
min_val, max_val = num_match.group(1), num_match.group(2)
step_match = re.search(r'(?:std::map<float,\s*QString>\(\)|[a-zA-Z0-9_]+Labels)\s*,\s*([-\d.]+)', args_str)
if step_match:
step = step_match.group(1)
# CESpeed is rendered in Qt with a dual numeric control (CESpeed + CESpeedLead),
# so the generic assignment matcher cannot infer it reliably.
if key == "CESpeed":
widget_type = "numeric"
data_type = "int"
min_val, max_val, step = "0", "99", "1"
if key in FORCE_BOOL_KEYS:
data_type = "bool"
precision = None
precision_match = re.search(r"QString::number\([^,]+,\s*'f'\s*,\s*(\d+)\)", rest)
if precision_match:
precision = int(precision_match.group(1))
if data_type == "float" and step and float(step).is_integer():
data_type = "int"
# Generic pond UI can't faithfully represent non-boolean button/multi-option controls.
if widget_type == "toggle" and data_type != "bool":
continue
s = {
"key": key,
"label": title,
"description": desc,
"data_type": data_type,
"ui_type": widget_type
}
if widget_type == "numeric":
if min_val is not None: s["min"] = float(min_val)
if max_val is not None: s["max"] = float(max_val)
if step is not None: s["step"] = float(step)
if precision is not None: s["precision"] = precision
elif widget_type == "dropdown":
if options_endpoint: s["options_endpoint"] = options_endpoint
if dropdown_options: s["options"] = dropdown_options
if key in child_to_parent: s["parent_key"] = child_to_parent[key]
if key in ALL_PARENT_KEYS: s["is_parent_toggle"] = True
if key == "CELead":
s["is_parent_toggle"] = True
items.append(s)
# Mirror CELead's split sub-toggles from FrogPilotButtonToggleControl.
if key == "CELead":
items.extend([
{
"key": "CESlowerLead",
"label": "Slower Lead",
"description": "Switch to \"Experimental Mode\" when a slower lead vehicle is detected ahead.",
"data_type": "bool",
"ui_type": "toggle",
"parent_key": "CELead",
},
{
"key": "CEStoppedLead",
"label": "Stopped Lead",
"description": "Switch to \"Experimental Mode\" when a stopped lead vehicle is detected ahead.",
"data_type": "bool",
"ui_type": "toggle",
"parent_key": "CELead",
},
])
# Mirror CESpeed's dual slider (with-lead variant) from Qt.
if key == "CESpeed":
items.append({
"key": "CESpeedLead",
"label": "Below (With Lead)",
"description": "Switch to \"Experimental Mode\" when driving below this speed with a lead.",
"data_type": "int",
"ui_type": "numeric",
"min": 0.0,
"max": 99.0,
"step": 1.0,
"parent_key": "ConditionalExperimental",
})
return items
def main():
layout = []
for cat in CATEGORIES:
items = parse_cpp_file(cat["file"])
injected = INJECTED_SECTION_PARAMS.get(cat["name"], [])
if injected:
existing_keys = {item["key"] for item in items}
items = [dict(item) for item in injected if item["key"] not in existing_keys] + items
if items:
layout.append({
"name": cat["name"],
"icon": cat["icon"],
"params": items
})
output_path = os.path.join(REPO_ROOT, "frogpilot/system/the_pond/assets/components/tools/device_settings_layout.json")
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(layout, f, indent=2)
if __name__ == '__main__':
main()
File diff suppressed because it is too large Load Diff
+6
View File
@@ -15,6 +15,12 @@ if ! command -v "uv" > /dev/null 2>&1; then
PATH="$UV_BIN:$PATH"
fi
# Homebrew llvm can break extension builds on macOS by masking system SDK headers.
if [[ "$(uname)" == 'Darwin' ]]; then
export CC=/usr/bin/clang
export CXX=/usr/bin/clang++
fi
echo "updating uv..."
# ok to fail, can also fail due to installing with brew
uv self update || true
+26
View File
@@ -0,0 +1,26 @@
FROM ghcr.io/commaai/openpilot-base-aarch64:latest
ENV PYTHONUNBUFFERED=1
ENV UV_LINK_MODE=copy
WORKDIR /work
# Keep this image minimal; dependency resolution is done by uv inside the workspace.
# rsync/ssh-client are needed for optional sysroot sync from a comma device.
RUN apt-get update && apt-get install -y --no-install-recommends \
rsync \
openssh-client \
e2fsprogs \
android-sdk-libsparse-utils \
qemu-user-static \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
qtbase5-private-dev \
python3-pip \
xz-utils \
git \
&& rm -rf /var/lib/apt/lists/*
RUN /usr/bin/python3 -m pip install --no-cache-dir --break-system-packages uv
# Fix for "dubious ownership" git errors when mounting folders on Linux hosts.
RUN git config --global --add safe.directory /work
@@ -0,0 +1,290 @@
#!/usr/bin/env python3
import argparse
import json
import lzma
import os
import shutil
import subprocess
import sys
import tempfile
import urllib.request
from pathlib import Path
REQUIRED_DIRS = [
("usr/local/lib", "/usr/local/lib"),
("usr/local/include", "/usr/local/include"),
("lib/aarch64-linux-gnu", "/lib/aarch64-linux-gnu"),
("usr/lib/aarch64-linux-gnu", "/usr/lib/aarch64-linux-gnu"),
("usr/include", "/usr/include"),
]
OPTIONAL_DIRS = [
("usr/lib/qt5/bin", "/usr/lib/qt5/bin"),
]
VENDOR_CANDIDATES = ["/system/vendor/lib64", "/vendor/lib64"]
OPTIONAL_INCLUDE_DIRS: list[str] = []
DEBUGFS_NOT_FOUND_MARKERS = (
"file not found by ext2_lookup",
"while trying to resolve filename",
"couldn't allocate",
"no such file",
"not found",
)
ANDROID_SPARSE_MAGIC = 0xED26FF3A
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Extract TICI sysroot dirs from AGNOS system image")
p.add_argument("--manifest", default="system/hardware/tici/agnos.json", help="Path to AGNOS manifest JSON")
p.add_argument("--output-dir", required=True, help="Destination sysroot directory")
p.add_argument("--cache-dir", default=".cache/agnos", help="Cache directory for downloaded images")
p.add_argument("--url", default=None, help="Override AGNOS system image URL")
p.add_argument("--force-download", action="store_true", help="Redownload system image even if cached")
return p.parse_args()
def get_system_url(manifest_path: Path) -> str:
data = json.loads(manifest_path.read_text())
for entry in data:
if entry.get("name") != "system":
continue
if isinstance(entry.get("url"), str):
return entry["url"]
alt = entry.get("alt") if isinstance(entry, dict) else None
if isinstance(alt, dict) and isinstance(alt.get("url"), str):
return alt["url"]
raise RuntimeError(f"No system entry in manifest: {manifest_path}")
def download(url: str, dst: Path) -> None:
dst.parent.mkdir(parents=True, exist_ok=True)
tmp = dst.with_suffix(dst.suffix + ".part")
print(f"Downloading {url} -> {dst}", flush=True)
with urllib.request.urlopen(url) as src, open(tmp, "wb") as out:
shutil.copyfileobj(src, out, length=1024 * 1024)
tmp.replace(dst)
def download_and_prepare_image(url: str, cache_dir: Path, force_download: bool) -> Path:
cache_dir.mkdir(parents=True, exist_ok=True)
compressed = cache_dir / "agnos_system.img.xz"
raw_image = cache_dir / "agnos_system.img"
ext4_image = cache_dir / "agnos_system.ext4.img"
if force_download:
compressed.unlink(missing_ok=True)
raw_image.unlink(missing_ok=True)
ext4_image.unlink(missing_ok=True)
# Prefer an already-cached raw image to avoid unnecessary multi-GB downloads.
if raw_image.exists():
return raw_image
if url.endswith(".xz"):
if not compressed.exists():
download(url, compressed)
if not raw_image.exists():
print(f"Decompressing {compressed} -> {raw_image}", flush=True)
with lzma.open(compressed, "rb") as f_in, open(raw_image, "wb") as f_out:
shutil.copyfileobj(f_in, f_out, length=1024 * 1024)
else:
if not raw_image.exists():
download(url, raw_image)
return raw_image
def is_android_sparse(image_path: Path) -> bool:
with open(image_path, "rb") as f:
header = f.read(4)
if len(header) != 4:
return False
magic = int.from_bytes(header, "little")
return magic == ANDROID_SPARSE_MAGIC
def ensure_debugfs_readable_image(image_path: Path, cache_dir: Path) -> Path:
if not is_android_sparse(image_path):
return image_path
if shutil.which("simg2img") is None:
raise RuntimeError(
f"{image_path} is an Android sparse image, but simg2img is not installed."
)
converted = cache_dir / "agnos_system.ext4.img"
needs_convert = not converted.exists()
if converted.exists():
needs_convert = os.path.getmtime(converted) < os.path.getmtime(image_path)
if needs_convert:
print(f"Converting sparse image {image_path} -> {converted}", flush=True)
proc = subprocess.run(["simg2img", str(image_path), str(converted)], capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(f"simg2img failed:\n{proc.stdout}\n{proc.stderr}")
return converted
def _has_content(path: Path) -> bool:
if not path.exists():
return False
if path.is_file():
return path.stat().st_size > 0
return any(path.iterdir())
def _pick_extracted_root(tmp_path: Path, src_path: str) -> Path | None:
rel = Path(src_path.lstrip("/"))
candidates = [
tmp_path / rel,
tmp_path / rel.name,
tmp_path,
]
for c in candidates:
if _has_content(c):
return c
return None
def run_debugfs(image_path: Path, src_path: str, dst_path: Path) -> None:
attempts: list[str] = []
for candidate_src in (src_path, src_path.lstrip("/")):
if not candidate_src:
continue
with tempfile.TemporaryDirectory(prefix="sysroot_extract_") as tmp:
tmp_path = Path(tmp)
cmd = ["debugfs", "-R", f"rdump {candidate_src} {tmp_path}", str(image_path)]
proc = subprocess.run(cmd, capture_output=True, text=True)
out = f"{proc.stdout}\n{proc.stderr}".lower()
attempts.append(f"{' '.join(cmd)}\n{proc.stdout}\n{proc.stderr}")
if proc.returncode != 0:
continue
if any(marker in out for marker in DEBUGFS_NOT_FOUND_MARKERS):
continue
source_to_copy = _pick_extracted_root(tmp_path, candidate_src)
if source_to_copy is None:
continue
if dst_path.exists():
shutil.rmtree(dst_path)
dst_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(source_to_copy, dst_path, symlinks=True, dirs_exist_ok=True)
return
joined_attempts = "\n---\n".join(attempts)
raise RuntimeError(f"debugfs could not extract '{src_path}'. Attempts:\n{joined_attempts}")
def is_non_empty_dir(path: Path) -> bool:
return path.is_dir() and any(path.iterdir())
def populate_optional_host_includes(output_dir: Path) -> None:
dst_root = output_dir / "usr/include"
if not dst_root.exists():
return
include_roots = [
Path("/usr/include/aarch64-linux-gnu"),
Path("/usr/include"),
Path("/usr/include/x86_64-linux-gnu"),
]
for include_dir in OPTIONAL_INCLUDE_DIRS:
dst = dst_root / include_dir
if dst.exists():
continue
for root in include_roots:
src = root / include_dir
if not src.is_dir():
continue
shutil.copytree(src, dst, symlinks=True)
break
openssl_dst = dst_root / "openssl"
for src_dir in (
Path("/usr/include/aarch64-linux-gnu/openssl"),
Path("/usr/include/x86_64-linux-gnu/openssl"),
):
if not src_dir.is_dir():
continue
openssl_dst.mkdir(parents=True, exist_ok=True)
shutil.copytree(src_dir, openssl_dst, symlinks=True, dirs_exist_ok=True)
break
def main() -> int:
args = parse_args()
if shutil.which("debugfs") is None:
raise RuntimeError("debugfs is required (install e2fsprogs)")
manifest_path = Path(args.manifest).resolve()
output_dir = Path(args.output_dir).resolve()
cache_dir = Path(args.cache_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
url = args.url or get_system_url(manifest_path)
image_path = download_and_prepare_image(url, cache_dir, args.force_download)
image_path = ensure_debugfs_readable_image(image_path, cache_dir)
for rel_dst, src_path in REQUIRED_DIRS:
dst = output_dir / rel_dst
print(f"Extracting {src_path} -> {dst}", flush=True)
run_debugfs(image_path, src_path, dst)
for rel_dst, src_path in OPTIONAL_DIRS:
dst = output_dir / rel_dst
try:
print(f"Extracting {src_path} -> {dst}", flush=True)
run_debugfs(image_path, src_path, dst)
except RuntimeError:
print(f"WARN: optional path not found in AGNOS image: {src_path}", flush=True)
vendor_ok = False
vendor_dst = output_dir / "system/vendor/lib64"
for src_path in VENDOR_CANDIDATES:
try:
print(f"Extracting {src_path} -> {vendor_dst}", flush=True)
run_debugfs(image_path, src_path, vendor_dst)
vendor_ok = True
break
except RuntimeError:
continue
if not vendor_ok:
print(
"WARN: vendor libs not found in AGNOS image at /system/vendor/lib64 or /vendor/lib64; "
"falling back to usr/lib/aarch64-linux-gnu",
flush=True,
)
if vendor_dst.is_symlink() or vendor_dst.is_file():
vendor_dst.unlink()
elif vendor_dst.exists():
shutil.rmtree(vendor_dst)
vendor_dst.parent.mkdir(parents=True, exist_ok=True)
os.symlink("../../usr/lib/aarch64-linux-gnu", vendor_dst, target_is_directory=True)
populate_optional_host_includes(output_dir)
missing = []
for rel in ("usr/local/lib", "usr/local/include", "lib/aarch64-linux-gnu", "usr/lib/aarch64-linux-gnu", "usr/include", "system/vendor/lib64"):
if not is_non_empty_dir(output_dir / rel):
missing.append(rel)
if missing:
raise RuntimeError(f"Extracted sysroot is incomplete, missing content in: {', '.join(missing)}")
print(f"Sysroot ready: {output_dir}", flush=True)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except KeyboardInterrupt:
raise
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr)
raise SystemExit(1)