Skip to main content

Developer Guide

This page explains how Validrive is built: the stack, main modules, data flow, and the key functions you’ll touch when extending or maintaining the app.

1. Tech stack

  • Language: Python 3.x
  • UI framework: PySide6 (Qt)
  • Automation: Selenium WebDriver (Chrome)
  • Data handling: pandas, NumPy, csv (Sniffer)
  • Styling: Qt Palette + QSS
  • Packaging: PyInstaller
  • Target OS: Windows

2. Bundled assets

A Validrive folder contains the following files:

  • validrive.py → main Python source code
  • chromedriver.exe → Chrome driver for Selenium (if you bundle driver)
  • style.qss, theme_dark.qss, theme_light.qss → stylesheets
  • validrive_splash.png → splash image
  • logo_valid.ico → application icon
  • logo.png → toolbar/menu logo
  • settings.png, sett_black.png, mail.png, mail-black.png, arrow.png → icons

Resource loading

All external assets (icons, stylesheets, drivers) are accessed through the resource_path() helper:

def resource_path(relative_path: str) -> str:
"""Return absolute path to resource, works for dev and PyInstaller."""
try:
base_path = sys._MEIPASS # PyInstaller temp folder
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
  • In dev mode: loads from the current folder.
  • In packaged mode: loads from the temp directory where PyInstaller unpacks assets.

This way u don’t need to worry about paths. Just place the asset in the project root and reference it like:

self.setWindowIcon(QIcon(resource_path("logo_valid.ico")))

3. High-level architecturee

LayerMain componentsResponsibilities
UI (PySide6)MainWindow, FilterPanel, CorrectEventDialog, SplitDialog, MergeDialog, force_dark_palette + QSSLayout, filters, dialogs, theming, preview (table/image)
Data (pandas)df, df_backup, helpers: ensure_validrive_copy, read_csv_safely, normalize_numeric_columns, save_df, parse_user_numberLoad/save files, normalize values, maintain working copy, undo
Automation (Selenium)SeleniumController, helpers: pick_ddv_url_from_df, init_driver, create_list, first_search_capsule, search_capsule, play_video, replay_video, handle_optional_large_list_dialogChoose Daredeevil env, launch Chrome, create/search capsule lists, video control
Integration (signals/slots)UI → Selenium: sig_selenium_connect, sig_selenium_go_to, sig_selenium_replay, sig_selenium_shutdown / Selenium → UI: connected, navigated, replayed, errorAsync link between UI and browser, keep interface responsive
Tree view

Validrive
├─ UI (PySide6)
│ ├─ MainWindow — layout, actions, preview
│ ├─ FilterPanel — filters + results list
│ ├─ CorrectEventDialog / SplitDialog / MergeDialog — edits
│ └─ force_dark_palette + QSS — theming
├─ Data (pandas)
│ ├─ df, df_backup — working/undo data
│ ├─ ensure_validrive_copy, read_csv_safely
│ ├─ normalize_numeric_columns, parse_user_number, save_df
│ └─ resolve_image_path — image handling
├─ Automation (Selenium)
│ ├─ SeleniumController — browser automation
│ ├─ pick_ddv_url_from_df — env selection
│ ├─ init_driver, create_list, handle_optional_large_list_dialog
│ ├─ first_search_capsule, search_capsule
│ └─ play_video, replay_video
└─ Integration (signals/slots)
├─ UI → Selenium: sig_selenium_connect, sig_selenium_go_to, sig_selenium_replay, sig_selenium_shutdown
└─ Selenium → UI: connected, navigated, replayed, error

4. Data model

Validrive keeps all event data in a global pandas.DataFrame (df).
This working dataset is created by upload_excel() and persisted to disk on every change.

Required columns

  • caps → raw capsule name string
  • event_type → event type label (Lane_change, Lane_keep etc.)
  • start_time, end_time → numeric (floats; seconds)

Automatically added / derived columns

  • caps_number → extracted YYYYMMDD_HHMMSS from caps
  • durationend_time - start_time (computed if missing)
  • status → defaults to "invalidated" if missing
  • corrected_event → always present; empty entries set to NaN
  • event_id → auto-assigned 1..N if missing

Normalization utilities

  • normalize_numeric_columns(df, ["start_time", "end_time", "duration"])
    → coerces user input to floats via _clean_to_float()
  • read_csv_safely()
    → detects delimiter with csv.Sniffer, retries encoding (utf-8 → latin-1)

Persistence

  • ensure_validrive_copy(src_path)
    → creates a working copy file (_ValidriveCopy.xlsx or .csv) alongside the original
  • save_df(df, path)
    → writes back to the working copy after each action

Undo

  • _save_backup() → snapshots df_backup = df.copy(deep=True)
  • undo_last_change() → restores from df_backup, disables undo, saves immediately

5. Daredeevil target selection

Validrive can connect to two Daredeevil environments:

  • Classic DaredeevilDDV_URL_CLASSIC
  • SW500 DaredeevilDDV_URL_SW500

The target is chosen automatically based on the event data.

Key functions

  • is_sdv_caps_name(name)
    → returns True if "sdv" appears in the capsule name (case-insensitive).

  • pick_ddv_url_from_df(df)
    → inspects the first row’s caps (or caps_number if present)
    → decides whether to connect to Classic or SW500 Daredeevil.

6. Selenium layer

All browser automation is handled by the SeleniumController, which runs inside a background QThread. This keeps the Qt UI responsive while Chrome is being controlled. Communication between the UI and Selenium is done with Qt signals and slots.

Controller class

The SeleniumController(QObject) is the central piece of the automation layer.
It exposes four signals connected, navigated, replayed, and error which inform the UI of progress or failures.
On the other side, the main slots are init_and_create_list(df), go_to_event(caps_number, start_time), replay(caps_number, start_time), and shutdown().
The controller itself is created in MainWindow._ensure_selenium_thread() and then moved into its own thread.

Driver bootstrap

When init_driver(base_url) is called, the controller resolves the packaged chromedriver.exe using resource_path, launches Chrome with the --start-maximized flag, attaches a WebDriverWait(driver, 15), and navigates to either the Classic or SW500 Daredeevil site.

The next step is handled by create_list(df): it inspects the first row of the DataFrame to extract capsule information, fills in the "Add a capsule list" form via XPath selectors, submits the form, and accepts the optional “large-list” confirmation dialog if it appears.

Note: the entire workflow depends on Daredeevil’s DOM. If selectors change, create_list and the navigation helpers are the first places to update.

Event navigation is split into two cases.
The first time an event is visited, first_search_capsule(caps_number) opens the search panel, selects the capsule, and then play_video(start_time) jumps to the exact timestamp.
Subsequent visits reuse the context: if the capsule is unchanged, replay_video is used directly; if the capsule differs, the controller falls back to search_capsule followed by play_video.

Internally, the last capsule ID is tracked in _last_caps to decide between replay and fresh navigation.

Summary of flow

  1. Initialize driver → open Daredeevil (init_driver).
  2. Create capsule list based on DataFrame (create_list).
  3. Navigate to first event: first_search_capsuleplay_video.
  4. Next events: replay if same capsule, or search_capsuleplay_video if different.
  5. Emit signals back to the UI at each stage (connected, navigated, replayed).

7. UI: MainWindow

The MainWindow class is the central widget of Validrive.
It brings together the filter panel, the event preview, the action buttons, and the connection controls into one consistent interface.

Layout

The window is split into two main areas:

  • Left panel: the FilterPanel, where the user can narrow down events and navigate quickly.
  • Right panel: a vertical splitter that contains:
    • A preview area, showing either the first/last rows of the event file or images if available.
    • An event info group, which displays details of the currently selected event (capsule, type, duration, status).
    • An actions group, with buttons for validation, correction, splitting, merging, replay, and reporting.

At the top sits the toolbar with connection controls and settings.

Toolbar

The toolbar provides quick access to high-level actions:

  • Connect / Disconnect Browser: start or stop the Selenium controller.
  • Settings menu: a QToolButton that toggles window pinning, switches theme (dark/light), and opens the About dialog.

Event handling

Each action button is directly tied to a DataFrame update:

  • Validating an event sets status="validated".
  • Invalidating sets status="invalidated".
  • Correction opens a dialog to adjust type or times.
  • Split and Merge create new rows or collapse ranges, always generating new event_ids.
  • Replay triggers a navigation back to Daredeevil.
  • Report opens a summary dialog with counts per status.

After every change, save_df is called so the working copy stays in sync.

State management

The MainWindow keeps the interface consistent through:

  • refresh_preview(): updates the preview table with the current filter context.
  • update_interface(): refreshes the event info panel and colors the status indicator.
  • Undo support: the last DataFrame state is preserved, and undo_last_change() reverts it.

Theming

Dark mode is applied by default using force_dark_palette(app) and style.qss.
If the user switches to light mode from the settings menu, alternate stylesheets (theme_light.qss) and icons are loaded dynamically.

8. UI: FilterPanel

The FilterPanel sits on the left side of the main window.
Its role is to let users quickly narrow down the event list and jump to specific rows in the DataFrame.

Filters

There are two kinds of filters:

  • Quick filters: ready-made options for the most common cases.
    These include duration greater/less than a threshold, filtering by event type, by capsule name, or by validation status (validated / corrected / invalidated).

  • Personalized filter: a more advanced input where the user chooses any column from the DataFrame, selects a comparison operator, and provides a value.
    The system infers the operator choices based on the column’s data type, so numbers, text, and categories are all handled safely.

All filters are combined, and the results are shown in the list below.

Results list

The filtered events appear in a list widget.
Each row is displayed in a readable format:

Event #42 | Duration: 3.50s | Type: lane_change | Caps: 20240115_093055

From here, the user can:

  • Double-click an item to navigate directly to it in Daredeevil.
  • Use “Next” and “Previous” buttons to cycle through the filtered results.

Internal logic

Behind the scenes, the filter input is read and parsed by _read_filters().

Conditions are applied with pandas masking, and numeric parsing uses parse_user_number() to handle both integers and floats without errors.

Column data types are checked with _infer_typeset() so the available operators make sense for each field.

The panel emits a goToIndex signal whenever a new row is selected, which the MainWindow listens to in order to trigger navigation or replay.

9. Dialogs

Validrive defines three main dialogs, all implemented with PySide6 QDialog.
Each dialog modifies the working DataFrame (df) and saves changes immediately through save_df().

CorrectEventDialog

Used to correct a single event.
The user can pick or add an event type, adjust the start and end times and optionally write a custom value into a chosen column.

Two actions are available: Correct, which marks the event as corrected, and Correct & Validate, which applies the correction and directly sets the status to validated.

SplitDialog

Allows splitting one event into multiple smaller ones.
The user first chooses how many splits are needed, then defines start and end times and event types for each part.

The resulting rows are created with the status set to corrected, unless the user chooses Split & Validate, in which case they are directly marked as validated.

MergeDialog

Merges a range of consecutive events.
The user specifies the first and last event IDs to merge. A new row is created with a fresh event_id, and the duration spans from the start of the first event to the end of the last.
The merged event replaces the originals and follows the same persistence and undo logic as other operations.

10. Signals & threading

Validrive separates browser automation from the UI using Qt’s signal/slot system.
All Selenium actions run inside a background QThread, keeping the interface responsive while long operations are in progress.

The main signals defined are:

  • sig_selenium_connect — request to start a browser and create the capsule list
  • sig_selenium_go_to — request navigation to a capsule at a given time
  • sig_selenium_replay — request replay of the current capsule
  • sig_selenium_shutdown — request to close the browser and stop the thread

On the automation side, the SeleniumController emits back signals to update the UI:

  • connected(bool, str) — tells if the browser connection and list creation succeeded
  • navigated(bool, str) — informs when navigation to a capsule finished
  • replayed(bool, str) — informs when replay is done
  • error(str) — reports failures with a human-readable message

The thread itself is started in MainWindow._ensure_selenium_thread().
This setup allows heavy Selenium work (page loads, waits, DOM interactions) to happen in the background, while the PySide6 UI remains smooth and interactive.

11. Coding conventions

Validrive follows simple conventions to keep code readable and consistent:

  • PEP8 style with 4-space indentation.
  • Classes: CamelCase (e.g., MainWindow, FilterPanel).
  • Functions & methods: snake_case (e.g., refresh_preview, create_list).
  • Constants: ALL_CAPS (e.g., DDV_URL_CLASSIC).
  • UI widgets: always stored as self.<name> so they can be updated later.
  • Signals/slots: prefix with sig_ for signals, on_ for handlers if needed.

Example

class MainWindow(QMainWindow):
def refresh_preview(self):
self.table_preview.clear()
...

12. Logging & debug

Validrive uses lightweight logging based on plain print() calls with prefixes.
This avoids extra dependencies and keeps PyInstaller bundles simple.

  • [INFO] → normal operations
  • [DEBUG] → internal state, extracted values, decisions
  • [ERROR] → failures or exceptions

Examplee

print("[INFO] 'Add a capsule list' button clicked successfully.")
print(f"[DEBUG] Extracted parameters: logTarget={logTarget}, vehicleProject={vehicleProject}")
print(f"[ERROR] Dropdown '{formcontrolname}' selection failed: {e}")

This convention makes it easy to grep logs when users report issues. If needed in the future, these print() statements can be swapped for the Python logging module without changing function calls, just wrap them in a custom logger.