Compare commits

..

25 Commits

Author SHA1 Message Date
Jason Wen
0bae722215 note 2026-03-12 22:57:47 -04:00
Jason Wen
32bbcfa975 fix 2026-03-12 22:55:55 -04:00
Jason Wen
738b361a25 more 2026-03-12 22:52:54 -04:00
Jason Wen
b61ccf2bee pivot 2026-03-12 22:40:58 -04:00
Jason Wen
c8d543a8fe more 2026-03-12 22:38:21 -04:00
Jason Wen
801c107aa1 update 2026-03-12 22:27:36 -04:00
Jason Wen
7e79d5424f fix 2026-03-12 22:17:30 -04:00
Jason Wen
6dde8560fe another 2026-03-12 22:15:41 -04:00
Jason Wen
2655a2e138 temp disable for now 2026-03-12 22:06:09 -04:00
Jason Wen
06cc8af9bf fix 2026-03-12 22:04:35 -04:00
Jason Wen
c918e5bbdc test 2026-03-12 21:54:32 -04:00
Jason Wen
922ad64d13 sync 2026-03-12 21:46:34 -04:00
Jason Wen
31b3776150 even more 2026-03-12 20:28:28 -04:00
Jason Wen
1e5ad42e7a cool 2026-03-12 01:24:20 -04:00
Jason Wen
58c72df0be more 2026-03-12 01:03:00 -04:00
Jason Wen
b2667cac32 panels 2026-03-12 00:27:41 -04:00
Jason Wen
fd07e80c34 yes 2026-03-11 23:52:41 -04:00
Jason Wen
bf628bd042 rename 2026-03-11 21:59:26 -04:00
Jason Wen
5043f5e7a4 nav 2026-03-11 21:37:56 -04:00
Jason Wen
83a84ba4bc more 2026-03-11 21:27:32 -04:00
Jason Wen
7d225a1e33 more 2026-03-11 20:15:44 -04:00
Jason Wen
450d5a4458 more 2026-03-11 17:20:26 -04:00
Jason Wen
d06a404bd9 6 2026-03-11 15:32:22 -04:00
Jason Wen
27dfd5cbf0 phase 1-5 2026-03-11 15:19:55 -04:00
Jason Wen
2e82908c07 pandad: always prioritize internal panda (#1759)
* pandad: filter out external panda

* fix

* internal panda

* move it even higher

* this

* should be this still

* anoter

* more

* 1 more time

* bruh

* try this out

* revert

* gotta do this after

* filter
2026-03-10 20:30:25 -04:00
94 changed files with 6669 additions and 215 deletions

66
.github/workflows/docs-sp.yaml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: sunnypilot docs
on:
push:
branches:
- docs
paths:
- 'docs_sp/**'
- 'zensical.toml'
pull_request:
paths:
- 'docs_sp/**'
- 'zensical.toml'
concurrency:
group: docs-sp-${{ github.event_name == 'push' && github.ref == 'refs/heads/docs' && github.run_id || github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
build:
name: build sunnypilot docs
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
pip install uv
uv pip install --system zensical
- name: Build docs
run: zensical build
# Push to docs.sunnypilot.ai
- uses: actions/checkout@v6
if: github.ref == 'refs/heads/docs' && github.repository == 'sunnypilot/sunnypilot'
with:
path: sunnypilot-docs
ssh-key: ${{ secrets.OPENPILOT_DOCS_KEY }}
repository: sunnypilot/sunnypilot-docs
- name: Push to GitHub Pages
if: github.ref == 'refs/heads/docs' && github.repository == 'sunnypilot/sunnypilot'
run: |
set -x
source release/identity.sh
cd sunnypilot-docs
git checkout --orphan tmp
git rm -rf .
cp -r ../docs_site_sp/ docs/
touch docs/.nojekyll
echo -n docs.sunnypilot.ai > docs/CNAME
git add -f .
git commit -m "build sunnypilot docs"
git push -f origin tmp:gh-pages

47
.github/workflows/forum-docs.yaml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Sync docs to Discourse
on:
workflow_run:
workflows: ["sunnypilot docs"]
types:
- completed
branches:
- docs
concurrency:
group: forum-docs-sync
cancel-in-progress: true
jobs:
sync:
name: sync docs to Discourse
runs-on: ubuntu-24.04
if: >
github.event.workflow_run.conclusion == 'success' &&
github.repository == 'sunnypilot/sunnypilot'
steps:
- uses: actions/checkout@v6
with:
ref: docs
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
- name: Restore sync cache
uses: actions/cache@v4
with:
path: .discourse_sync_cache
key: discourse-sync-${{ hashFiles('docs_sp/**/*.md') }}
restore-keys: |
discourse-sync-
- name: Sync to Discourse
env:
DISCOURSE_URL: ${{ secrets.DISCOURSE_URL }}
DISCOURSE_API_KEY: ${{ secrets.DISCOURSE_API_KEY }}
DISCOURSE_API_USER: ${{ secrets.DISCOURSE_API_USER }}
DISCOURSE_CATEGORY: ${{ vars.DISCOURSE_DOCS_CATEGORY || 'documentation' }}
DOCS_BASE_URL: https://docs.sunnypilot.ai
run: ruby docs_sp/tools/sync_docs_discourse.rb --verbose

View File

@@ -1,105 +0,0 @@
name: 'Post to Discourse'
description: 'Posts a message to a Discourse topic (existing or new)'
inputs:
discourse-url:
description: 'Discourse instance URL (e.g., https://discourse.example.com)'
required: true
api-key:
description: 'Discourse API key'
required: true
api-username:
description: 'Discourse API username'
required: true
topic-id:
description: 'Discourse topic ID to post to (use this OR category-id + title)'
required: false
category-id:
description: 'Category ID for new topic (required if topic-id not provided)'
required: false
title:
description: 'Title for new topic (required if topic-id not provided)'
required: false
message:
description: 'Message content (markdown supported)'
required: true
outputs:
post-number:
description: 'The post number in the topic'
value: ${{ steps.post.outputs.post_number }}
post-url:
description: 'Direct URL to the post'
value: ${{ steps.post.outputs.post_url }}
topic-id:
description: 'The topic ID (useful when creating a new topic)'
value: ${{ steps.post.outputs.topic_id }}
runs:
using: "composite"
steps:
- name: Post to Discourse
id: post
shell: bash
run: |
# Validate inputs
if [ -z "${{ inputs.topic-id }}" ] && ([ -z "${{ inputs.category-id }}" ] || [ -z "${{ inputs.title }}" ]); then
echo "❌ Error: Must provide either topic-id OR both category-id and title"
exit 1
fi
if [ -n "${{ inputs.topic-id }}" ] && ([ -n "${{ inputs.category-id }}" ] || [ -n "${{ inputs.title }}" ]); then
echo "⚠️ Warning: Both topic-id and category-id/title provided. Will post to existing topic."
fi
# Determine if creating new topic or posting to existing
if [ -n "${{ inputs.topic-id }}" ]; then
echo "📝 Posting to existing topic ID: ${{ inputs.topic-id }}"
# Create JSON payload for posting to existing topic
PAYLOAD=$(jq -n \
--arg content '${{ inputs.message }}' \
--arg topic_id "${{ inputs.topic-id }}" \
'{topic_id: $topic_id, raw: $content}')
else
echo "✨ Creating new topic: ${{ inputs.title }}"
# Create JSON payload for new topic
PAYLOAD=$(jq -n \
--arg content '${{ inputs.message }}' \
--arg title "${{ inputs.title }}" \
--arg category "${{ inputs.category-id }}" \
'{title: $title, category: ($category | tonumber), raw: $content}')
fi
# Post to Discourse
RESPONSE=$(curl -s -w "\n%{http_code}" \
-X POST "${{ inputs.discourse-url }}/posts.json" \
-H "Content-Type: application/json" \
-H "Api-Key: ${{ inputs.api-key }}" \
-H "Api-Username: ${{ inputs.api-username }}" \
-d "$PAYLOAD")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✅ Successfully posted to Discourse!"
POST_NUMBER=$(echo "$BODY" | jq -r '.post_number // "unknown"')
TOPIC_ID=$(echo "$BODY" | jq -r '.topic_id // "${{ inputs.topic-id }}"')
POST_URL="${{ inputs.discourse-url }}/t/${TOPIC_ID}/${POST_NUMBER}"
echo "post_number=${POST_NUMBER}" >> $GITHUB_OUTPUT
echo "post_url=${POST_URL}" >> $GITHUB_OUTPUT
echo "topic_id=${TOPIC_ID}" >> $GITHUB_OUTPUT
echo "Topic ID: ${TOPIC_ID}"
echo "Post number: ${POST_NUMBER}"
echo "URL: ${POST_URL}"
else
echo "❌ Failed to post to Discourse"
echo "HTTP Code: ${HTTP_CODE}"
echo "Response: ${BODY}"
exit 1
fi

View File

@@ -0,0 +1,93 @@
# Discourse Docs Sync — one-way push from docs_sp/ Markdown to Discourse API.
#
# WARNING: This workflow is strictly for Discourse API syncing.
# Do NOT add Zensical build steps, GitHub Pages deployment, or any
# static-site generation to this file. Those belong in docs.yaml.
name: Sync Docs to Discourse
on:
push:
# paths:
# - "docs_sp/**"
# - "zensical.toml"
pull_request:
# paths:
# - "docs_sp/**"
# - "zensical.toml"
workflow_dispatch:
jobs:
smoke-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup environment
run: ./tools/op.sh setup
- name: Smoke test - post one doc to Discourse
env:
DISCOURSE_URL: ${{ secrets.DISCOURSE_URL }}
DISCOURSE_API_KEY: ${{ secrets.DISCOURSE_API_KEY }}
DISCOURSE_API_USER: ${{ secrets.DISCOURSE_API_USER }}
DISCOURSE_CATEGORY_MAP: '{"getting-started": 133}'
run: |
uv run --python 3.12 python -c "
import os, sys
sys.path.insert(0, 'docs_sp/tools')
from pathlib import Path
from converter import convert
from discourse_client import DiscourseClient, DiscourseConfig
DOC_PATH = 'getting-started/what-is-sunnypilot.md'
GITHUB_BRANCH = os.environ.get('GITHUB_REF_NAME', 'master')
raw = (Path('docs_sp') / DOC_PATH).read_text()
body = convert(raw, file_path=DOC_PATH)
# Append sync footer
gh_url = f'https://github.com/sunnypilot/sunnypilot/blob/{GITHUB_BRANCH}/docs_sp/{DOC_PATH}'
body = body.rstrip('\n') + f'\n\n---\n<small>This document is version-controlled. Suggest changes [on GitHub]({gh_url}).</small>\n\n[^docs-sync]: docs-sync-id: {DOC_PATH}\n'
# Extract title from front matter or first heading
title = None
for line in raw.splitlines():
s = line.strip()
if s.startswith('title:'):
title = s[len('title:'):].strip().strip('\"').strip(\"'\")
break
if s.startswith('# '):
title = s[2:].strip()
break
title = f'{title or \"What is sunnypilot?\"} - sunnypilot Docs'
print(f'Title: {title}')
print(f'Body: {len(body)} chars')
config = DiscourseConfig.from_env()
client = DiscourseClient(config)
category_id = config.category_id_for(DOC_PATH)
print(f'Category ID: {category_id}')
existing = client.find_topic_by_sync_id(DOC_PATH)
if existing is not None:
topic_id = existing['id']
post_id = client.first_post_id(topic_id)
if post_id is None:
print(f'ERROR: No first post for topic {topic_id}')
sys.exit(1)
result = client.update_post(post_id, body, edit_reason='CI smoke test')
if result is None:
print('ERROR: Failed to update post')
sys.exit(1)
print(f'Updated: {config.base_url}/t/{topic_id}')
else:
result = client.create_topic(title=title, raw=body, category_id=category_id, tags=['docs-auto-sync'])
if result is None:
print('ERROR: Failed to create topic')
sys.exit(1)
print(f'Created: {config.base_url}/t/{result.get(\"topic_id\", \"?\")}')
print('Smoke test passed!')
"

View File

@@ -1,78 +0,0 @@
name: Debug Discourse Posting
on:
push:
jobs:
test-discourse-post:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Post test message to Discourse
uses: ./.github/workflows/post-to-discourse
with:
discourse-url: ${{ vars.DISCOURSE_URL }}
api-key: ${{ secrets.DISCOURSE_API_KEY }}
api-username: ${{ secrets.DISCOURSE_API_USERNAME }}
topic-id: ${{ vars.DISCOURSE_UPDATES_TOPIC_ID }}
message: |
## 🧪 Test Post from GitHub Actions
**This is a test post to verify Discourse integration**
- **Workflow**: ${{ github.workflow }}
- **Run Number**: #${{ github.run_number }}
- **Branch**: `${{ github.ref_name }}`
- **Commit**: ${{ github.sha }}
- **Actor**: @${{ github.actor }}
- **Timestamp**: ${{ github.event.head_commit.timestamp }}
---
### Fake Build Info (for testing)
- **Version**: 0.9.8-test
- **Build**: #42
- **Branch**: release-test
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
*This is an automated test message. Drive safe! 🚗💨*
- name: Create topic on Discourse
uses: ./.github/workflows/post-to-discourse
with:
discourse-url: ${{ vars.DISCOURSE_URL }}
api-key: ${{ secrets.DISCOURSE_API_KEY }}
api-username: ${{ secrets.DISCOURSE_API_USERNAME }}
#topic-id: ${{ vars.DISCOURSE_UPDATES_TOPIC_ID }}
category-id: 4
title: "This is a test of a new topic instead of a reply"
message: |
## 🧪 Test Post from GitHub Actions
**This is a test post to verify Discourse integration**
- **Workflow**: ${{ github.workflow }}
- **Run Number**: #${{ github.run_number }}
- **Branch**: `${{ github.ref_name }}`
- **Commit**: ${{ github.sha }}
- **Actor**: @${{ github.actor }}
- **Timestamp**: ${{ github.event.head_commit.timestamp }}
---
### Fake Build Info (for testing)
- **Version**: 0.9.8-test
- **Build**: #42
- **Branch**: release-test
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
*This is an automated test message. Drive safe! 🚗💨*
- name: Display results
if: always()
run: |
echo "::notice::Discourse post test completed"
echo "Check your Discourse topic to verify the post appeared correctly"

2
.gitignore vendored
View File

@@ -15,6 +15,8 @@ a.out
.cache/
/docs_site/
/docs_site_sp/
/.discourse_sync_cache/
*.mp4
*.dylib

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f64cd24ccac9a73100f206909639424421db1fce74dc77c386cdeefa9d16df9c
size 1959

BIN
docs_sp/assets/logo.png LFS Normal file

Binary file not shown.

View File

@@ -0,0 +1,27 @@
---
title: Contributing
---
# Contributing to sunnypilot
We welcome contributions from the community! Here's how you can help.
## Ways to Contribute
- **Report bugs** — See [Reporting a Bug](reporting-a-bug.md)
- **Submit code** — Follow the [Workflow](workflow.md) guide
- **Improve documentation** — Submit PRs to the `docs` branch
- **Help others** — Join the [sunnypilot community forum](https://community.sunnypilot.ai)
## Code of Conduct
Be respectful, constructive, and helpful. We're all here to make driving safer and more enjoyable.
## Getting Started
1. Fork the [sunnypilot repository](https://github.com/sunnypilot/sunnypilot)
2. Create a feature branch
3. Make your changes
4. Submit a pull request
See [Workflow](workflow.md) for detailed steps.

View File

@@ -0,0 +1,12 @@
---
title: Community
---
# Community
Get involved with sunnypilot development and connect with other users.
- **[Contributing](contributing.md)** - How to contribute code, documentation, and translations
- **[Workflow](workflow.md)** - Development workflow and branch strategy
- **[Reporting a Bug](reporting-a-bug.md)** - How to file an effective bug report
- **[Community Forum](https://community.sunnypilot.ai)** - Join the discussion

View File

@@ -0,0 +1,69 @@
---
title: Reporting a Bug
---
# Reporting a Bug
Help us improve sunnypilot by reporting issues you encounter.
## Before Reporting
!!! warning "Remove Customizations First"
If you have any custom modifications (forks, patches, config tweaks), **remove them and reproduce the issue on an official sunnypilot branch** before reporting. This rules out your modifications as the cause and helps maintainers focus on real bugs.
1. **Upgrade** to the latest version — the bug may already be fixed
2. **Remove customizations** — reproduce on a stock official branch
3. **Search** [GitHub Issues](https://github.com/sunnypilot/sunnypilot/issues) and the [sunnypilot community forum](https://community.sunnypilot.ai) for known reports
4. **Preserve the route** — upload raw logs via [Comma Connect](https://connect.comma.ai) and keep the route available
## What to Include
### Required Information
- **Dongle ID** — Your comma Dongle ID (found in **Settings****Device** or in Comma Connect)
- **Route ID** — The route ID from Comma Connect for the drive where the issue occurred
- **Device info** — Hardware model (C3, C3X, C4), software version, branch
- **Vehicle info** — Make, model, year
### Bug Report Template
Use the following structure when filing your report:
!!! example "Bug Report Format"
**Title:** One-sentence summary of the issue
**Description:** 1-2 sentences providing additional context about the problem.
**Steps to Reproduce:**
1. Step one
2. Step two
3. Step three
**Expected behavior:** What should have happened
**Actual behavior:** What actually happened
**Related Links:** Route link, log files, screenshots, or references to related issues
## Pre-Submission Checklist
Before submitting, confirm the following:
- [ ] I am running the latest version of sunnypilot
- [ ] I have removed all custom modifications and reproduced the issue on an official branch
- [ ] I have searched existing issues and community channels for duplicates
- [ ] I have preserved the route and uploaded raw logs via Comma Connect
- [ ] I have included my comma Dongle ID
- [ ] I have included the Route ID for the affected drive
## How to Report
1. Go to [GitHub Issues](https://github.com/sunnypilot/sunnypilot/issues/new)
2. Use the bug report template
3. Fill in all requested information using the format above
4. Submit the issue
!!! tip
The more detail you provide, the faster we can diagnose and fix the issue. Incomplete reports without Dongle IDs or route information may be closed.

View File

@@ -0,0 +1,37 @@
---
title: Workflow
---
# Development Workflow
How to contribute code to sunnypilot.
## Prerequisites
- Git installed
- Python 3.12+
- A GitHub account
## Steps
1. **Fork** the repository on GitHub
2. **Clone** your fork locally
3. **Create a branch** from the appropriate base branch
4. **Make changes** and test locally
5. **Push** your branch to your fork
6. **Open a pull request** against the main repository
## Branch Naming
Use descriptive branch names:
- `feat/feature-name` — New features
- `fix/bug-description` — Bug fixes
- `docs/topic` — Documentation changes
## Pull Request Guidelines
- Keep PRs focused on a single change
- Include a clear description of what and why
- Reference any related issues
- Ensure tests pass

View File

@@ -0,0 +1,10 @@
---
title: Connected Services
---
# Connected Services
Cloud and map integrations that extend sunnypilot beyond the vehicle.
- [sunnylink](sunnylink.md) - settings backup/restore, sponsor tiers, GitHub account pairing, and remote device management
- [OSM Maps](osm-maps.md) - OpenStreetMap data for speed limits, road names, and map-based driving features

View File

@@ -0,0 +1,45 @@
---
title: OSM Maps
---
# OSM Maps
## What It Does
Integrates OpenStreetMap (OSM) data to provide speed limit information, road geometry, and other map attributes. This data powers features like Speed Limit Assist and Smart Cruise Control — Map.
## How It Works
1. Select your country (and state, if in the US)
2. Download the map data to your device
3. The map daemon processes the data and makes it available to driving features
4. Periodically check for updates to keep your map data current
## Requirements
!!! info "Requirements"
- Internet connection for initial download and updates
- Storage space on the device for map data
## How to Set Up
**Settings****OSM**
1. Select your **Country**
2. If in the US, select your **State**
3. Tap **Database Update** to download
4. Wait for the download to complete
## Features That Use OSM Data
- **[Speed Limit Assist](../cruise/speed-limit.md)** — Reads speed limits from map data
- **[Smart Cruise Control — Map](../cruise/scc-m.md)** — Uses road geometry for proactive speed adjustment
## Managing Map Data
- **Update:** Tap "Database Update" to check for newer map data
- **Delete:** Tap "Delete Maps" to remove downloaded data and free storage
## Settings Reference
See [OSM Settings](../../settings/osm.md) for all options and status information.

View File

@@ -0,0 +1,67 @@
---
title: sunnylink
---
# sunnylink
## What It Does
sunnylink is sunnypilot's cloud backend integration system. It connects your device to the sunnypilot cloud infrastructure, enabling settings backup and restore, sponsor benefits, remote device management, and optional driving data uploads.
## Key Features
### Settings Backup & Restore
Securely back up your device configuration to the cloud and restore it on the same or a different device.
- **Backup** compresses and encrypts all your sunnypilot settings using AES-256-CBC encryption derived from your device's unique RSA key
- **Restore** downloads and decrypts your backup, applying only recognized sunnypilot parameters
- Progress is displayed in real-time (0-100%) during both operations
- Backups include all sunnypilot-specific parameters (toggles, tuning values, preferences)
!!! note
Backups are device-key encrypted. A backup created on one device can only be restored to the same device or by using the same device keys.
### Sponsor Tiers
sunnylink has a sponsorship system with multiple tiers:
| Tier | Color |
|------|-------|
| Guardian | Gold |
| Benefactor | Green |
| Contributor | Steel Blue |
| Supporter | Purple |
| Free / Novice | Default |
Sponsor status is displayed in the sunnylink settings panel. Higher tiers may unlock additional features (such as the infrastructure uploader).
### GitHub Account Pairing
Link your GitHub account to your device for sponsor verification and identity management. The pairing status is displayed in the sunnylink settings panel as "Paired" or "Not Paired".
### Data Upload (Infrastructure Test)
When enabled, sunnylink can upload driving logs and camera data to sunnypilot's cloud infrastructure. This feature prioritizes crash and boot logs, respects metered connections (skips video on cellular), and uses signed S3 URLs for secure uploads.
!!! warning
The uploader is currently an infrastructure test feature available to high-tier sponsors only. It provides no direct user benefit at this time.
### Remote Device Management
sunnylink maintains a persistent WebSocket connection to the backend, enabling:
- Remote parameter viewing and modification
- Log upload control
- Local port proxying for SSH-like access
- Queued message processing
## How to Configure
**Settings****sunnylink**
sunnylink requires explicit user consent before activation. On first enable, a consent dialog explains what data is collected and how it is used.
## Settings Reference
See [sunnylink Settings](../../settings/sunnylink.md) for all configuration options.

View File

@@ -0,0 +1,53 @@
---
title: Alpha Longitudinal
---
# Alpha Longitudinal
## What It Does
Alpha Longitudinal provides experimental longitudinal (speed and acceleration) control for vehicles that are openpilot-compatible but do not have official sunnypilot longitudinal support. This enables throttle and brake control on cars that would otherwise be limited to lateral (steering) assistance only.
For officially supported vehicles, sunnypilot provides refined longitudinal tuning tailored to each platform. Alpha Longitudinal extends this capability to additional vehicles using a more generalized approach, allowing drivers of these cars to experience full ACC-like functionality through sunnypilot.
## How It Works
Alpha Longitudinal uses the vehicle's existing communication interfaces to send throttle and brake commands, bypassing the need for the vehicle's stock cruise control system. The system:
- Reads vehicle speed, pedal position, and other sensor data through the car's CAN bus
- Calculates desired acceleration based on the driving model's output
- Sends throttle and brake commands directly to the vehicle's powertrain controllers
- Sets `pcmCruise=False`, meaning sunnypilot takes full control of longitudinal behavior instead of relying on the vehicle's built-in cruise control module
Because this operates outside the vehicle manufacturer's intended cruise control pathway, the tuning is less refined than on officially supported platforms.
!!! danger "AEB Is Disabled"
Enabling Alpha Longitudinal **disables your vehicle's Automatic Emergency Braking (AEB)**. This is a significant safety trade-off. Without AEB, the vehicle will not automatically apply emergency braking in imminent collision scenarios. Drive with extra caution and maintain safe following distances at all times.
## Requirements
!!! info "Requirements"
- Vehicle must be openpilot-compatible (listed on the [comma.ai vehicle compatibility list](https://comma.ai/vehicles))
- Vehicle does not have official sunnypilot longitudinal support
- Vehicle must report `CP.alphaLongitudinalAvailable = True` — availability is determined per vehicle in the car interface code
- Feature is experimental and under active development
!!! warning "Development Branches Only"
Alpha Longitudinal is flagged as `DEVELOPMENT_ONLY`. It is **only available on development and staging branches**, not on release branches. This is intentional — the feature is not considered stable enough for general release.
!!! warning "Mutually Exclusive with ICBM"
Alpha Longitudinal and [ICBM](icbm.md) cannot be active at the same time. Enabling Alpha Longitudinal disables ICBM, and vice versa. Alpha Longitudinal provides direct throttle/brake control, while ICBM works through the stock cruise control system — these two approaches are fundamentally incompatible.
!!! warning "Alpha Quality Software"
This feature is **alpha quality**. Expect rough edges, less smooth acceleration and braking behavior, and less refined stop-and-go performance compared to officially supported vehicles. Longitudinal behavior may vary significantly between vehicle models and driving conditions.
## How to Enable
**Settings****Developer****sunnypilot Longitudinal Control (Alpha)**
!!! note
This toggle only appears on development branches and only for vehicles where `alphaLongitudinalAvailable` is `True`.
## Settings Reference
See [Cruise Control Settings](../../settings/cruise/index.md) for configuration details.

View File

@@ -0,0 +1,28 @@
---
title: Custom ACC Increments
---
# Custom ACC Increments
## What It Does
Customize the speed increments when you press the cruise control buttons. Instead of the default step sizes, set your own preferred values for both short and long button presses.
## How It Works
- **Short press:** Changes speed by your configured short press increment (110 km/h or mph)
- **Long press:** Changes speed by your configured long press increment (1, 5, or 10 km/h or mph)
## Requirements
!!! info "Requirements"
- Longitudinal control must be available, **or** [ICBM](icbm.md) must be enabled
- PCM Cruise must not be active (factory cruise control must not override)
## How to Enable
**Settings****Cruise****Custom ACC Increments**
## Settings Reference
See [Cruise Control Settings](../../settings/cruise/index.md) for all increment options.

View File

@@ -0,0 +1,61 @@
---
title: Dynamic Experimental Control
---
# Dynamic Experimental Control (DEC)
## What It Does
DEC automatically switches between openpilot's two longitudinal modes based on real-time road conditions. Instead of manually toggling between modes, the system dynamically selects the most appropriate mode for the current situation.
To understand DEC, it helps to know the two driving modes it switches between:
| Mode | Internal Name | Description |
|------|---------------|-------------|
| **Chill / Standard** | `acc` | The default openpilot driving mode. Follows the lead car and stays in lane at a steady speed. Best suited for highway and open-road driving where stops and complex maneuvers are rare. |
| **Experimental** | `blended` | An enhanced mode that uses the end-to-end (E2E) driving model. Can handle stops at traffic lights and stop signs, navigate turns, and respond to more complex urban scenarios. Designed for city driving. |
!!! note "DEC is a switcher, not a mode"
DEC is not a third driving mode — it is a dynamic switcher that automatically selects between `acc` (Chill/Standard) and `blended` (Experimental) in real time based on road conditions.
## How It Works
DEC uses a confidence-based switching system with specific probability thresholds and hysteresis to prevent rapid mode toggling:
### Detection Signals
| Signal | Threshold | Effect |
|--------|-----------|--------|
| **Lead vehicle probability** | ≥ 0.45 | Favors `acc` mode (standard following) |
| **Slow-down probability** | ≥ 0.3 | Favors `blended` mode (E2E for stops/turns) |
| **Stop sign / traffic light** | Detected by vision model | Triggers switch to `blended` mode |
| **Turn detection** | Upcoming turns | Triggers switch to `blended` mode |
| **Current speed** | Speed-dependent | Lower speeds favor `blended` mode |
### Switching Logic
- DEC uses **Kalman filters** to smooth probability signals and reduce noise
- A **minimum hold time of 10 frames** prevents rapid oscillation between modes
- A **confidence threshold of 0.6** must be met before switching to a new mode
- The system continuously evaluates conditions and transitions seamlessly without driver input
Based on these signals, DEC switches between:
| Mode | When DEC Activates It |
|------|----------|
| **Chill / Standard** (`acc`) | Highway driving with steady speeds, lead vehicle following, clear lanes, and no upcoming stops or complex intersections |
| **Experimental** (`blended`) | City driving with stops, turns, traffic lights, and complex intersections where the vehicle needs to slow down or stop |
## Requirements
!!! info "Requirements"
- Longitudinal control must be available
- Device must be offroad to enable/disable
## How to Enable
**Settings****Cruise****Dynamic Experimental Control**
## Settings Reference
See [Cruise Control Settings](../../settings/cruise/index.md) for configuration details.

View File

@@ -0,0 +1,77 @@
---
title: Intelligent Cruise Button Management
---
# Intelligent Cruise Button Management (ICBM)
## What It Does
ICBM allows sunnypilot to intercept and dynamically manage your vehicle's cruise control button presses. Instead of directly changing the set speed, button presses are routed through sunnypilot's logic, enabling features like Speed Limit Assist and Smart Cruise Control on vehicles that don't natively support sunnypilot longitudinal control.
This is particularly useful for vehicles where sunnypilot cannot directly control the gas and brakes — ICBM gives you many of the same benefits by intelligently managing the cruise buttons.
## When to Use
ICBM is designed specifically for vehicles where sunnypilot cannot directly control the throttle and brakes (i.e., no native longitudinal control). On these vehicles, the stock cruise control system still handles all actual acceleration and deceleration. ICBM bridges the gap by intelligently managing cruise button commands so you can still benefit from sunnypilot's speed planning features.
## How It Works
1. You press the cruise speed button on your steering wheel
2. ICBM intercepts the button press
3. sunnypilot applies its logic (speed limits, map curves, etc.) to determine the appropriate speed change
4. ICBM simulates the corresponding cruise button presses over the CAN bus, sending the adjusted command to the vehicle's stock cruise control system
This happens transparently — from your perspective, the buttons work normally but with smarter behavior. Under the hood, ICBM is communicating with the vehicle's cruise control module by simulating physical button presses on the CAN bus, which is why it works even on vehicles without direct throttle/brake control.
### State Machine
ICBM operates through a 5-state machine:
| State | Description |
|-------|-------------|
| **Inactive** | ICBM is idle — no button simulation is in progress |
| **Pre-Active** | A speed change has been requested; ICBM is preparing to simulate button presses |
| **Increasing** | ICBM is sending "speed up" button presses to reach the target speed |
| **Decreasing** | ICBM is sending "speed down" button presses to reach the target speed |
| **Holding** | Target speed has been reached; ICBM is holding the current setting |
!!! tip "Safety"
ICBM preserves all of your vehicle's stock safety systems. Forward Collision Avoidance (FCA), Automatic Emergency Braking (AEB), and other factory safety features remain fully active and unaffected, since the vehicle's own cruise control system is still performing the actual speed control. ICBM operates purely at the CAN bus button simulation level and does not interact with FCA or AEB message pathways.
## Supported Vehicles
ICBM support varies by vehicle brand. The feature toggle only appears in settings if your vehicle is supported.
| Brand | Notes |
|-------|-------|
| **Hyundai / Kia / Genesis** | Supported on most models with stock cruise control |
| **Honda / Acura** | Supported on compatible models |
| **Chrysler / Dodge / Jeep / RAM** | Supported on compatible models |
| **Mazda** | Supported on compatible models |
!!! info "Not Listed?"
If your brand or model is not listed above and the ICBM toggle does not appear in your settings, your vehicle is not currently supported. Support depends on the vehicle's CAN bus protocol and button command structure.
## Requirements
!!! info "Requirements"
- Your vehicle must support ICBM — not all vehicles are compatible
- If the ICBM toggle does not appear in settings, your vehicle is not supported
- **Mutually exclusive with [Alpha Longitudinal](alpha-longitudinal.md)** — only one can be active at a time
## How to Enable
**Settings****Cruise****Intelligent Cruise Button Management**
## Features Unlocked by ICBM
When ICBM is enabled, the following features become available even on vehicles without native longitudinal control:
- **[Smart Cruise Control — Vision](scc-v.md)** — Vision-based adaptive speed adjustments
- **[Smart Cruise Control — Map](scc-m.md)** — Map-aware speed adjustments
- **[Custom ACC Increments](custom-acc-increments.md)** — Custom button press speed steps
- **[Speed Limit Assist](speed-limit.md)** — Automatic speed limit matching
## Settings Reference
See [Cruise Control Settings](../../settings/cruise/index.md) for all available options.

View File

@@ -0,0 +1,15 @@
---
title: Cruise Control
---
# Cruise Control
Enhancements to adaptive cruise control that give you finer control over speed management, turn handling, and speed limit compliance.
- [Intelligent Cruise Button Management](icbm.md) - intercepts steering wheel button presses for smarter speed adjustments
- [Smart Cruise Control - Vision](scc-v.md) - uses vision predictions to slow for curves
- [Smart Cruise Control - Map](scc-m.md) - uses map data to anticipate speed changes
- [Custom ACC Increments](custom-acc-increments.md) - set custom speed change amounts for short and long button presses
- [Dynamic Experimental Control](dynamic-experimental-control.md) - automatically switches between driving modes based on conditions
- [Speed Limit Assist](speed-limit.md) - adjusts cruise speed to match posted limits
- [Alpha Longitudinal](alpha-longitudinal.md) - experimental longitudinal control for select vehicles

View File

@@ -0,0 +1,30 @@
---
title: Smart Cruise Control - Map
---
# Smart Cruise Control — Map (SCC-M)
## What It Does
SCC-M uses downloaded OpenStreetMap data to anticipate road changes — curves, speed limits, and intersections — and adjusts cruise speed proactively through [ICBM](icbm.md).
## How It Works
1. OSM map data provides information about upcoming road geometry
2. SCC-M calculates appropriate speeds for curves, speed zones, and intersections
3. Speed commands are sent through ICBM to adjust cruise before reaching these road features
## Requirements
!!! info "Requirements"
- [ICBM](icbm.md) must be enabled
- Vehicle must support ICBM
- [OSM Maps](../connected/osm-maps.md) must be configured and downloaded
## How to Enable
**Settings****Cruise****Smart Cruise Control — Map**
## Settings Reference
See [Cruise Control Settings](../../settings/cruise/index.md) for configuration details.

View File

@@ -0,0 +1,28 @@
---
title: Smart Cruise Control - Vision
---
# Smart Cruise Control — Vision (SCC-V)
## What It Does
SCC-V uses the device's camera to detect lead vehicles and make smarter cruise control decisions. It provides vision-based adaptive speed adjustments for vehicles that rely on ICBM for cruise management.
## How It Works
The camera continuously monitors the road ahead. When a lead vehicle is detected, SCC-V adjusts cruise speed commands through [ICBM](icbm.md) to maintain safe following distances and react to speed changes of the vehicle ahead.
## Requirements
!!! info "Requirements"
- [ICBM](icbm.md) must be enabled
- Vehicle must support ICBM
- Camera must have a clear view of the road ahead
## How to Enable
**Settings****Cruise****Smart Cruise Control — Vision**
## Settings Reference
See [Cruise Control Settings](../../settings/cruise/index.md) for configuration details.

View File

@@ -0,0 +1,90 @@
---
title: Speed Limit Assist
---
# Speed Limit Assist
## What It Does
Speed Limit Assist detects the current speed limit and can automatically adjust your cruise speed to match. It offers four operating modes ranging from passive information display to active speed management.
## How It Works
1. sunnypilot reads speed limit data from two sources (see below)
2. A configurable **Speed Limit Policy** determines how the sources are combined when both are available
3. Based on your chosen mode, the system displays, warns, or actively adjusts your set speed
4. An optional offset (fixed or percentage) lets you cruise slightly above or below the limit
## Speed Limit Sources
Speed Limit Assist pulls speed limit data from **two sources**:
| Source | Description |
|--------|-------------|
| **Car State** | Speed limit information provided by the vehicle's built-in sensors (e.g., Traffic Sign Recognition cameras). Availability depends on the vehicle. |
| **Map Data** | Speed limits from downloaded OpenStreetMap data. Requires [OSM Maps](../connected/osm-maps.md) to be configured and downloaded. |
### Speed Limit Policy
When both sources are available and report different values, the **Speed Limit Policy** setting determines which source is used:
| Policy | Behavior |
|--------|----------|
| **Car State Only** | Uses only the vehicle's built-in speed limit data; ignores map data |
| **Map Data Only** | Uses only OSM map speed limit data; ignores car state |
| **Car State Priority** | Uses car state data when available; falls back to map data |
| **Map Data Priority** | Uses map data when available; falls back to car state |
| **Combined** | Uses the highest-confidence value from either source |
## Operating Modes
| Mode | Behavior |
|------|----------|
| **Off** | Speed limit data is not used |
| **Information** | Shows the current speed limit on the driving display |
| **Warning** | Shows the speed limit and alerts you when you're exceeding it |
| **Assist** | Automatically adjusts cruise speed to match the speed limit |
## Speed Offset
You can set an offset so your cruise speed differs from the exact limit:
- **Fixed offset:** Add or subtract a set number of km/h or mph (range: -30 to +30)
- **Percentage offset:** Apply a percentage above or below the limit
## Confirmation Modes
When the detected speed limit changes, you can choose how the system responds:
| Mode | Behavior |
|------|----------|
| **Auto** | The cruise set speed adjusts automatically when a new speed limit is detected — no driver input required |
| **User Confirm** | The system displays the new speed limit and waits for the driver to confirm before adjusting the set speed |
## Driver Notifications
Speed Limit Assist provides visual indicators on the driving HUD:
- The currently detected speed limit is shown on the display
- When a speed limit change is detected, a notification appears showing the new limit
- In Warning and Assist modes, alerts notify you when you are exceeding the posted limit
## Requirements
!!! info "Requirements"
- Longitudinal control must be available, **or** [ICBM](icbm.md) must be enabled
- For map-based limits: [OSM Maps](../connected/osm-maps.md) must be configured and downloaded
## How to Enable
**Settings****Cruise****Speed Limit Assist**
## Vehicle Restrictions
!!! warning "Vehicle Restrictions"
- **Tesla:** Assist mode is disabled on release branches (Information and Warning still work)
- **Rivian:** Assist mode is always disabled
## Settings Reference
See [Speed Limit Settings](../../settings/cruise/speed-limit/index.md) for all configuration options.

View File

@@ -0,0 +1,34 @@
---
title: HUD & Visuals
---
# HUD & Visuals
## What It Does
Customize the heads-up display (HUD) elements and visual overlays shown on the driving screen. Toggle individual elements on or off to create your preferred driving view.
## Available HUD Elements
| Element | Description |
|---------|-------------|
| **Blind Spot Indicator** | Visual warning when a vehicle is in your blind spot |
| **Torque Bar** | Shows current steering torque |
| **Standstill Timer** | Shows how long you've been stopped |
| **Road Name** | Displays the current road name |
| **Green Light Alert** | Alerts when a traffic light turns green |
| **Lead Depart Alert** | Alerts when the car ahead starts moving |
| **True Vehicle Speed** | CAN bus speed (may differ from GPS) |
| **Turn Signals** | Shows turn signal status on the HUD |
| **Rocket Fuel** | Real-time acceleration/deceleration bar on the left side of the screen |
| **Rainbow Mode** | Animated rainbow-gradient overlay on the model driving path |
| **Chevron Info** | Distance, speed, or time gap to lead vehicle |
| **Developer UI** | Debug information for diagnostics |
## How to Configure
**Settings****Visuals**
## Settings Reference
See [Visuals Settings](../../settings/visuals.md) for all toggle options and multi-button selectors.

View File

@@ -0,0 +1,10 @@
---
title: Display & Visuals
---
# Display & Visuals
Customizations for the onroad driving screen, HUD overlays, and visual indicators.
- [Onroad Display](onroad-display.md) - screen brightness, timeout, and layout controls
- [HUD & Visuals](hud-visuals.md) - toggles for visual indicators like blind spot warnings, road names, and traffic light alerts

View File

@@ -0,0 +1,23 @@
---
title: Onroad Display
---
# Onroad Display
## What It Does
Control the screen brightness, timeout behavior, and touch interactivity while driving. These settings help reduce distraction and manage power consumption.
## Key Features
- **Screen brightness control** — Choose from Auto, Auto Dark, Screen Off, or a fixed percentage (5%100%)
- **Brightness delay** — Set how long the screen stays bright before dimming
- **Interactivity timeout** — Make the screen non-interactive after a set period to prevent accidental touches
## How to Configure
**Settings****Display**
## Settings Reference
See [Display Settings](../../settings/display.md) for all configuration options.

27
docs_sp/features/index.md Normal file
View File

@@ -0,0 +1,27 @@
---
title: Features
---
# Features
sunnypilot extends openpilot with a comprehensive set of driving assist features organized into the following categories.
## Cruise Control
Adaptive cruise control enhancements including intelligent button management, vision and map-based smart cruise, custom ACC increments, dynamic experimental control, speed limit assist, and alpha longitudinal control.
## Steering
Lateral control features including the Modular Assistive Driving System (MADS), neural network lateral control (NNLC), automatic lane changes, torque-based steering control, and blinker pause lateral for temporarily pausing steering during turn signal activation.
## Display & Visuals
Onroad display customizations and HUD visual enhancements for real-time driving information.
## Models & AI
Driving model selection and AI-related configuration options.
## Connected Services
Cloud and map integrations including sunnylink (settings backup/restore, sponsor tiers, GitHub account pairing, remote device management) and OpenStreetMap (speed limit data and map-based features).

View File

@@ -0,0 +1,30 @@
---
title: Models & AI
---
# Models & AI
## What It Does
Select and configure the driving model used by sunnypilot. Different models offer different driving characteristics — some may be smoother, others more responsive. Advanced parameters let you fine-tune model behavior.
## Key Features
- **Model Selection** — Choose from available driving models (offroad only)
- **Lane Turn Desire** — Enhance lane positioning during turns
- **LAGD (Live Lateral Delay Compensation)** — Dynamically measures and compensates for steering actuator delay using real-time signal correlation
## Advanced Parameters
Advanced parameters are only visible when **Show Advanced Controls** is enabled in [Developer Settings](../settings/developer.md):
- **Lane Turn Value** — Intensity of lane turn desire (5002000)
- **LAGD Delay** — Fixed steering delay offset in milliseconds when LAGD is disabled (550)
## How to Configure
**Settings****Models**
## Settings Reference
See [Models Settings](../settings/models.md) for all configuration options.

View File

@@ -0,0 +1,59 @@
---
title: Auto Lane Change
---
# Auto Lane Change
## What It Does
Automatically executes lane changes when you activate the turn signal. You can choose between nudge-based confirmation, nudgeless (immediate), or timed delays before the lane change begins.
## How It Works
1. Activate your turn signal in the desired direction
2. The system verifies that clear lane markings are detected by the vision system in the target lane — a lane change will not execute without visible lane lines
3. The driver monitoring system checks that the driver is attentive before proceeding
4. Depending on your setting:
- **Nudge:** Give a light steering nudge to confirm the lane change
- **Nudgeless:** The lane change begins immediately
- **Timed (0.5s3s):** The lane change begins after the configured delay
5. If BSM Delay is enabled and a vehicle is detected in your blind spot, the lane change is delayed by an additional 1 second beyond the configured timer
## Requirements
!!! info "Requirements"
- Lateral control must be active (sunnypilot must be engaged)
- Vehicle must be traveling above a minimum speed threshold
- Clear lane markings must be visible to the vision system
- Driver must be attentive (monitored by the driver monitoring system)
## How to Enable
**Settings****Steering****Customize Lane Change**
## Modes
| Mode | Value | Behavior |
|------|-------|----------|
| **Off** | -1 | Auto lane change is disabled |
| **Nudge** | 0 | Requires a light steering nudge to confirm the lane change |
| **Nudgeless** | 1 | Lane change begins as soon as you signal |
| **0.5 second** | 2 | Lane change begins after 0.5s delay |
| **1 second** | 3 | Lane change begins after 1s delay |
| **2 seconds** | 4 | Lane change begins after 2s delay |
| **3 seconds** | 5 | Lane change begins after 3s delay |
## Blind Spot Monitoring Integration
If your vehicle supports BSM and the BSM Delay option is enabled, the system checks for vehicles in your blind spot before executing the lane change. When a vehicle is detected in the blind spot, the lane change is delayed by an additional **1 second** on top of the configured timer until the adjacent lane is clear.
!!! info "BSM Requirements"
- Vehicle must support Blind Spot Monitoring
- Auto Lane Change Timer must be set beyond Nudge mode
## Settings Reference
See [Lane Change Settings](../../settings/steering/lane-change.md) for all options.
!!! warning "Safety"
Always check your mirrors and blind spots before initiating a lane change. The system assists but does not replace driver awareness.

View File

@@ -0,0 +1,36 @@
---
title: Blinker Pause Lateral Control
---
# Blinker Pause Lateral Control
## What It Does
Temporarily pauses lateral (steering) control when you activate a turn signal. This gives you full manual steering control during the blinker activation, useful for situations where you want to make a manual lane adjustment or turn without the system fighting your steering input.
## How It Works
1. You activate a turn signal (left or right blinker)
2. If your speed is **below** the configured speed threshold, sunnypilot pauses lateral control
3. Lateral control stays paused while the blinker is active
4. After the blinker is deactivated, lateral control re-engages after the configured delay
## Configuration Options
### Speed Threshold
The maximum speed at which Blinker Pause activates. Above this speed, lateral control remains active even with the blinker on (this allows Auto Lane Change to work at highway speeds).
### Re-Engage Delay
How long after the blinker is deactivated before lateral control re-engages. A short delay prevents abrupt steering corrections immediately after turning off the signal.
## How to Configure
**Settings****Steering**
Look for the Blinker Pause Lateral Control toggle and its sub-settings.
## Settings Reference
See [Steering Settings](../../settings/steering/index.md) for all configuration options.

View File

@@ -0,0 +1,13 @@
---
title: Steering
---
# Steering
Lateral control features that govern how sunnypilot handles steering assistance, lane changes, and manual override behavior.
- [Modular Assistive Driving System (MADS)](mads.md) - decouples steering from cruise control for independent lateral assist
- [Neural Network Lateral Control (NNLC)](nnlc.md) - uses a neural network for smoother steering
- [Auto Lane Change](auto-lane-change.md) - automated lane changes triggered by the turn signal
- [Torque Control](torque-control.md) - torque-based steering with tunable parameters
- [Blinker Pause Lateral](blinker-pause.md) - temporarily pauses steering assist when the turn signal is active

View File

@@ -0,0 +1,86 @@
---
title: Modular Assistive Driving System
---
# Modular Assistive Driving System (MADS)
## What It Does
MADS decouples lateral (steering) and longitudinal (speed) controls. In standard openpilot, engaging cruise control activates both steering and speed control together, and disengaging turns both off. With MADS, steering assistance can remain active independently — even when cruise control is off.
This means you can have lane-keeping assistance while controlling the gas and brakes yourself.
## How It Works
With MADS enabled, Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC) can be engaged and disengaged independently. This is the core difference from standard openpilot, where both are always linked together.
- **Steering stays active** even when you cancel cruise control — ALC continues to provide lane centering while you control speed manually
- **Independent engagement** — you can activate ALC without ACC, or both together, giving you flexible control over which assists are active
- You choose what happens to steering when cruise disengages (remain active, pause, or fully disengage)
- You can configure whether the main cruise button also activates steering
### Engagement States
MADS has five internal states:
| State | Description |
|-------|-------------|
| **Disabled** | MADS is off — no steering assistance is provided |
| **Enabled** | Steering assistance is fully engaged and actively providing lane centering |
| **Paused** | Steering assistance is temporarily paused (e.g., due to braking, depending on your Steering Mode on Brake setting) but can resume without re-engaging |
| **Soft Disabling** | The system is transitioning from enabled to disabled due to a safety condition (e.g., driver inattention alert escalation) |
| **Overriding** | The driver is actively steering, temporarily overriding the system's lateral input. Steering assistance resumes when the driver releases the wheel |
### Steering Mode on Brake
This setting controls what happens to steering assistance when the brake pedal is pressed:
| Option | Value | Behavior |
|--------|-------|----------|
| **Remain Active** | 0 | Steering stays fully active while braking |
| **Pause** | 1 | Steering pauses while braking, resumes on release |
| **Disengage** | 2 | Steering fully disengages on brake — must re-engage manually |
## Requirements
!!! info "Requirements"
- Supported on most vehicles with sunnypilot lateral control
## How to Enable
**Settings****Steering** → enable **MADS**
Then configure the sub-settings in **MADS Settings**.
## Key Settings
| Setting | What It Controls |
|---------|-----------------|
| **Main Cruise Allowed** | Whether the cruise on/off button can activate MADS |
| **Unified Engagement Mode** | Whether engaging cruise also engages MADS automatically |
| **Steering Mode on Brake** | What happens to steering when the brake is pressed (Remain Active / Pause / Disengage) |
| **Steering Mode on Disengage** | What happens to steering when cruise disengages |
## Vehicle-Specific Behavior
Some vehicles operate in **limited MADS mode** where certain settings are locked due to platform constraints (e.g., no vehicle bus access):
=== "Tesla (without vehicle bus)"
- Main Cruise Allowed: forced OFF
- Unified Engagement Mode: forced ON
- Steering Mode on Brake: forced to **Disengage**
=== "Rivian"
- Main Cruise Allowed: forced OFF
- Unified Engagement Mode: forced ON
- Steering Mode on Brake: forced to **Disengage**
!!! note "Why these restrictions?"
Vehicles without a full vehicle bus connection (like Tesla without the vehicle bus harness and Rivian) cannot reliably detect certain driver inputs. To maintain safety, MADS defaults to the most conservative behavior on these platforms.
## Settings Reference
See [MADS Settings](../../settings/steering/mads.md) for all configuration options.
!!! warning "Safety"
Always be ready to take full manual control. MADS allows more flexible use of driver assistance, but you remain responsible for safe driving at all times.

View File

@@ -0,0 +1,58 @@
---
title: Neural Network Lateral Control
---
# Neural Network Lateral Control (NNLC)
## What It Does
NNLC replaces the traditional PID or torque-based steering controller with a neural network trained on real driving data. This can provide smoother, more natural-feeling lane keeping that adapts to your vehicle's steering characteristics.
## How It Works
Instead of using fixed mathematical formulas (PID controller), NNLC uses a machine learning model to calculate steering commands. The neural network has been trained on real-world driving data and can handle a wider variety of driving scenarios with smoother output.
!!! note "Evolution from NNFF"
NNLC evolved from **Neural Network FeedForward (NNFF)**, an earlier approach that used a neural network as a feedforward component alongside traditional controllers. NNLC builds on this foundation by giving the neural network full lateral control authority, resulting in improved steering smoothness and adaptability.
!!! info "Vehicle-Specific Training Data"
The neural network models are trained on real driving data collected from specific vehicles. Because each vehicle has unique steering characteristics, a dedicated model must be trained for each supported make/model. Over **70 vehicle models** currently have trained NNLC models available. If no model exists for your vehicle, the NNLC toggle will not appear in settings.
### Model Matching
When NNLC is enabled, the system matches your vehicle to the best available model using a 3-tier fallback:
| Tier | Match Criteria | Confidence | Description |
|------|---------------|------------|-------------|
| **1. Exact** | Fingerprint + firmware version | ≥ 0.99 | Best match — a model trained on your exact vehicle and firmware combination |
| **2. Valid** | Fingerprint only | ≥ 0.9 | Good match — a model trained on your vehicle platform, regardless of firmware version |
| **3. Substitute** | Substitute table lookup | Fallback | A model from a similar vehicle platform is used as a substitute (configured in `substitute.toml`) |
## Requirements
!!! info "Requirements"
- Vehicle must not use angle-based steering (`steerControlType` must not be `angle`)
- A trained NNLC model must be available for your specific vehicle
- Mutually exclusive with [Torque Control](torque-control.md) — only one can be active at a time
- Device must be offroad to enable/disable
## How to Enable
**Settings****Steering****Neural Network Lateral Control**
!!! tip
Not all vehicles have NNLC models available. If the toggle does not appear, your vehicle may use angle-based steering which is not compatible with NNLC, or a trained model may not yet exist for your vehicle.
## NNLC vs. Torque Control
| Feature | NNLC | Torque Control |
|---------|------|---------------|
| **Approach** | Neural network (learned) | Mathematical formula (tuned) |
| **Customization** | Automatic | Manual tuning available |
| **Best for** | Smooth, adaptive steering | Precise, configurable steering |
These two features are mutually exclusive — enabling one automatically disables the other.
## Settings Reference
See [Steering Settings](../../settings/steering/index.md) for configuration details.

View File

@@ -0,0 +1,48 @@
---
title: Torque Control
---
# Torque Control
## What It Does
Torque Control lets you fine-tune how aggressively or gently sunnypilot steers your vehicle. It provides advanced parameters for lateral acceleration, friction compensation, and self-tuning options.
## How It Works
Torque-based lateral control applies steering commands as torque (rotational force) to the steering motor. The key parameters are:
- **Lateral Acceleration Factor** — How strongly the system turns the wheel (higher = more aggressive)
- **Friction** — How much the system compensates for steering resistance
- **Self-Tune** — Automatically adjusts these values based on your driving
## Requirements
!!! info "Requirements"
- Vehicle must not use angle-based steering
- **Enforce Torque Control** must be enabled in [Steering Settings](../../settings/steering/index.md)
- Mutually exclusive with [NNLC](nnlc.md) — only one can be active at a time
- Device must be offroad to change settings
## How to Enable
1. Go to **Settings****Steering**
2. Enable **Enforce Torque Control**
3. Open the **Torque Settings** sub-panel to fine-tune
## Tuning Options
| Option | Description |
|--------|-------------|
| **Self-Tune** | Automatic real-time parameter adjustment |
| **Relaxed Self-Tune** | Less aggressive auto-tuning for smoother feel |
| **Custom Tune** | Manual parameter override |
| **Lat Accel Factor** | 0.015.00 (steering aggressiveness) |
| **Friction** | 0.011.00 (friction compensation) |
!!! tip
Start with Self-Tune enabled. Only switch to Custom Tune if you want precise control over steering feel and are willing to experiment.
## Settings Reference
See [Torque Settings](../../settings/steering/torque.md) for all tuning parameters.

View File

@@ -0,0 +1,10 @@
---
title: Getting Started
---
# Getting Started
New to sunnypilot? Start here to understand what sunnypilot is and whether it works with your vehicle.
- **[What is sunnypilot?](what-is-sunnypilot.md)** - Overview of the system, supported vehicles, and key capabilities
- **[Use sunnypilot in a car](use-sunnypilot-in-a-car.md)** - Requirements and what to expect on your first drive

View File

@@ -0,0 +1,42 @@
---
title: Use sunnypilot in a car
---
# Use sunnypilot in a car
## What You Need
1. **A supported vehicle** — Check the compatibility list for your car's make, model, and year
2. **A comma device** — comma 3X or compatible hardware
3. **A vehicle harness** — Connects the device to your car's systems
4. **An internet connection** — For initial setup and software updates
## Getting Started
1. Mount the device on your windshield
2. Connect the harness to your car
3. Install sunnypilot using one of the setup methods:
- [URL Method](../setup/url-method.md) — Easiest, recommended for most users
- [SSH Method](../setup/ssh-method.md) — For advanced users
## First Drive
After installation:
1. Power on the device
2. Wait for the system to calibrate (drive straight for a few minutes)
3. Enable cruise control as you normally would
4. The system will begin assisting with steering and speed control
!!! tip
Give the system time to calibrate. Steering assistance improves after the first few minutes of straight driving.
## Configuring Settings
sunnypilot offers many configurable options. Access settings through the on-device interface:
- **Cruise Control** — Speed management, gap settings, and cruise behavior
- **Steering** — Lane keeping, MADS, and steering tuning
- **Display & Visuals** — Customize the on-screen display
See the [Settings Reference](../settings/cruise/index.md) for a complete guide to all options.

View File

@@ -0,0 +1,31 @@
---
title: What is sunnypilot?
---
# What is sunnypilot?
sunnypilot is an open-source driver assistance system that enhances your car's existing features. It provides adaptive cruise control, lane centering, and many additional driving assists — all configurable to your preferences.
## Key Features
- **Adaptive Cruise Control** — Automatically adjusts speed to maintain distance from the car ahead
- **Lane Centering** — Keeps your car centered in the lane on highways and well-marked roads
- **Modular Assistive Driving System (MADS)** — Decouple lateral and longitudinal controls for flexible driving
- **Speed Limit Assist** — Automatically adjust speed based on map and sign data
- **Neural Network Lateral Control** — AI-based steering for smoother lane keeping
## How It Works
sunnypilot runs on a compatible device mounted in your car. It connects to your vehicle through a supported harness and uses cameras, radar, and other sensors to assist with driving.
!!! warning "Safety First"
sunnypilot is a **driver assistance** system, not a self-driving system. You must keep your hands on the wheel and eyes on the road at all times. See [Safety Information](../safety/safety.md) for details.
## Supported Vehicles
sunnypilot supports a wide range of vehicles. Check the [openpilot vehicle compatibility list](https://github.com/commaai/openpilot/blob/master/docs/CARS.md) for base compatibility, plus additional sunnypilot-specific enhancements for select brands.
## Next Steps
- [Use sunnypilot in a car](use-sunnypilot-in-a-car.md) — Overview of what you need
- [Read before installing](../setup/read-before-installing.md) — Important pre-installation info

10
docs_sp/how-to/index.md Normal file
View File

@@ -0,0 +1,10 @@
---
title: How-To Guides
---
# How-To Guides
Step-by-step guides for common tasks.
- **[Share a Route](share-a-route.md)** - How to share driving routes with others for debugging or review
- **[Preserve Local File Changes](preserve-local-changes.md)** - Keep your local modifications across software updates

View File

@@ -0,0 +1,49 @@
---
title: Preserve Local Changes
---
# Preserve Local File Changes on Your comma Device
## Overview
By default, the comma device automatically updates sunnypilot, which overwrites any local file modifications you have made. To keep your local changes across reboots, you must disable automatic updates **before** making modifications.
!!! warning "Complete These Steps First"
Local modifications applied **before** completing the steps below will **not** be retained. The automatic update process will overwrite them on the next reboot. Complete all steps first, then make your changes.
## Steps
### 1. Enable Advanced Controls
Navigate to **Settings****Developer****Show Advanced Controls** and toggle it **ON**.
This reveals additional settings options that are hidden by default, including the update toggle.
### 2. Disable Updates
Navigate to **Settings****Software****Disable Updates** and toggle it **ON**.
This prevents the device from automatically pulling and applying updates from the remote branch.
!!! note "Offroad Only"
The Disable Updates toggle can only be changed while the device is **offroad** (not actively driving). If you are in a drive, complete the drive first and return to the home screen before changing this setting.
### 3. Reboot the Device
Navigate to **Settings****Device****Reboot** to restart the device.
The reboot applies the settings change and ensures the update service is fully stopped. A confirmation dialog will appear before the reboot proceeds.
### 4. Verify
After the device restarts:
1. Confirm that your settings from steps 1 and 2 are still active
2. Make your desired local file changes (via SSH or other methods)
3. Reboot again to confirm your changes persist
!!! info "Automatic Updates Are Paused"
With updates disabled, your device will **not** receive new sunnypilot releases automatically. You will need to re-enable updates manually when you want to update to a newer version. To re-enable, reverse step 2: **Settings****Software****Disable Updates****OFF**.
!!! tip "When to Use This"
This is useful for testing custom parameter values, experimental configurations, or developer modifications that you want to persist across device reboots without being overwritten by the update system.

View File

@@ -0,0 +1,58 @@
---
title: How to Share a Route
---
# How to Share a Route
## Overview
Sharing driving routes is essential for debugging issues, getting support, and helping developers improve sunnypilot. Routes are shared through [comma Connect](https://connect.comma.ai), comma's web-based route management tool.
## Step 1: Prepare the Route
Before sharing, ensure the route data is fully uploaded and preserved.
### Upload Raw Logs
1. Go to [connect.comma.ai](https://connect.comma.ai)
2. Select the route you want to share
3. Open the **Files** tab
4. Upload the raw logs for the route
!!! warning "Upload Before Sharing"
Raw logs must be **fully uploaded** before they can be reviewed by others. If the logs are not uploaded, reviewers will not have access to the detailed data needed for debugging.
### Preserve the Route
Routes are automatically deleted after a retention period. To prevent this:
1. Select the route in comma Connect
2. Click **More info**
3. Toggle **Preserved** to **ON**
This ensures the route remains available for as long as you need it.
## Step 2: Choose Sharing Method
### Option A: Public Route (Recommended)
The simplest way to share a single route for support or debugging.
1. Select the route in comma Connect
2. Toggle **Public access** to **ON**
3. Copy the **Route ID** from the route details
4. Share the Route ID in the support channel or forum thread
!!! info "Privacy"
Making a route public means **anyone with the Route ID can access it**. Route data includes GPS coordinates and video footage. For privacy, start and end your recorded drives at public places such as parking lots or gas stations. Avoid starting or ending at your home, workplace, or other private locations.
### Option B: Device Sharing
Grants another user access to **all routes** on your device. Use this when ongoing collaboration is needed.
1. Rename your device with your vehicle's **Year/Make/Model** and your **username** (e.g., "2023 Hyundai Ioniq 6 - jasonwen")
2. Go to device settings in comma Connect
3. Share the device via the other user's email address
!!! tip "When to Use Device Sharing"
Device sharing is best for long-term collaboration with a developer or when multiple routes need to be reviewed. For one-off support requests, prefer Option A (Public Route) to limit access to only the relevant route.

37
docs_sp/index.md Normal file
View File

@@ -0,0 +1,37 @@
---
title: Home
---
# sunnypilot Documentation
Welcome to the official sunnypilot documentation. Find everything you need to get started, configure your system, and get the most out of your driving experience.
## Quick Links
<div class="grid cards" markdown>
- :material-rocket-launch: **[Getting Started](getting-started/index.md)**
Learn what sunnypilot is and how to get set up
- :material-download: **[Installation](setup/index.md)**
Install sunnypilot on your comma device
- :material-car: **[Features](features/index.md)**
Explore all features and how they work
- :material-cog: **[Settings](settings/index.md)**
Complete guide to every configurable option
- :material-shield: **[Safety](safety/index.md)**
Important safety information for all users
- :material-account-group: **[Community](community/index.md)**
Get involved and connect with other users
</div>

View File

@@ -0,0 +1,76 @@
---
title: Branch Definitions
---
# Branch Definitions
Understanding sunnypilot's branching strategy and device compatibility.
!!! tip "Calling All Testers"
Testers, even without software development experience, are encouraged to run **staging** or **dev** branches and report issues. Your feedback is invaluable for improving sunnypilot before each release.
---
## Release Branches
### release-tizi (C3X)
- **Stability:** :material-check-circle: Highly stable
- **Target devices:** Comma 3X (TIZI)
- **Description:** The recommended branch for most Comma 3X users. This branch contains thoroughly tested features and fixes that have passed through staging and dev. Use this for daily driving.
### release-tici (C3)
- **Stability:** Not yet available
- **Target devices:** Comma Three (TICI)
- **Description:** A dedicated release branch for Comma Three is not yet available. C3 users should use `staging-tici` in the meantime.
---
## Pre-Release Branches
### staging (C4 / C3X)
- **Stability:** :material-alert-circle: Generally stable
- **Target devices:** Comma Four (MICI), Comma 3X (TIZI)
- **Description:** Pre-release testing branch. Features here are being validated before promotion to a release branch. Suitable for users who want early access to upcoming features and are willing to report issues.
### staging-tici (C3)
- **Stability:** :material-alert-circle: Generally stable
- **Target devices:** Comma Three (TICI)
- **Description:** Pre-release testing branch for Comma Three. Provides the most stable experience currently available for C3 devices.
---
## Development Branches
### dev (C4 / C3X)
- **Stability:** :material-close-circle: Least stable
- **Target devices:** Comma Four (MICI), Comma 3X (TIZI)
- **Description:** Active development branch with the latest features and fixes. Intended for testers and developers who want to try the newest changes and provide feedback. Expect occasional issues.
### master (C4 / C3X)
- **Stability:** :material-close-circle: Unstable
- **Target devices:** Comma Four (MICI), Comma 3X (TIZI)
- **Description:** The primary development branch where pull requests are merged. Not recommended for daily driving. Use this branch if you are contributing code to sunnypilot.
### master-dev (C4 / C3X)
- **Stability:** :material-close-circle: Unstable
- **Target devices:** Comma Four (MICI), Comma 3X (TIZI)
- **Description:** CI branch used to build prebuilt artifacts for the `dev` branch. Not intended for direct installation.
---
## Which Branch Should I Use?
| Use Case | Recommended Branch |
|----------|--------------------|
| Daily driving (C3X) | `release-tizi` |
| Daily driving (C3) | `staging-tici` |
| Early access to features (C4/C3X) | `staging` |
| Testing and feedback (C4/C3X) | `dev` |
| Contributing code | `master` |

View File

@@ -0,0 +1,46 @@
---
title: Recommended Branches
---
# Recommended Branches
!!! warning
Please only install the branches listed below. All other branches are experimental development branches and are not intended for general use.
See [Branch Definitions](branch-definitions.md) for detailed descriptions of each branch type.
---
## Comma Four (C4 / MICI)
| Branch | Install URL | Stability |
|--------|------------|-----------|
| staging | `staging.sunnypilot.ai` | Generally stable, pre-release testing |
| dev | `dev.sunnypilot.ai` | Least stable, for testers and developers |
---
## Comma 3X (C3X / TIZI)
| Branch | Install URL | Stability |
|--------|------------|-----------|
| release-tizi | `release.sunnypilot.ai` | Highly stable, recommended for most users |
| staging | `staging.sunnypilot.ai` | Generally stable, pre-release testing |
| dev | `dev.sunnypilot.ai` | Least stable, for testers and developers |
---
## Comma Three (C3 / TICI)
| Branch | Install URL | Stability |
|--------|------------|-----------|
| staging-tici | `staging.sunnypilot.ai` | Pre-release testing for C3 |
!!! note
A dedicated release branch for Comma Three (`release-tici`) is not yet available. Use `staging-tici` for the most stable C3 experience.
---
## Changelogs
For detailed changelogs of each release, see the [sunnypilot GitHub Releases](https://github.com/sunnypilot/sunnypilot/releases) page.

View File

@@ -0,0 +1,56 @@
---
title: Driver Responsibility & Level 2 ADAS
---
# Driver Responsibility & Level 2 ADAS
!!! danger "You Are Always Responsible"
sunnypilot is **NOT** a self-driving system. You are legally and morally responsible for the vehicle at all times. No feature in sunnypilot removes or reduces your obligation to drive safely.
## What is Level 2 ADAS
sunnypilot is classified as a **Level 2 Advanced Driver Assistance System (ADAS)** per the SAE J3016 standard for driving automation. The SAE levels define increasing degrees of automation:
| SAE Level | Name | Driver Role |
|-----------|------|-------------|
| 0 | No Automation | Driver performs all tasks |
| 1 | Driver Assistance | System controls steering OR speed, not both |
| **2** | **Partial Automation** | **System controls steering AND speed; driver must supervise** |
| 3 | Conditional Automation | System handles driving in limited scenarios; driver must be ready |
| 45 | High/Full Automation | System handles all driving tasks in defined conditions |
sunnypilot operates at **Level 2** only. It can control steering and speed simultaneously, but the human driver must remain fully engaged at all times.
## What This Means
At Level 2, the system provides assistance — not autonomy. Specifically:
- The system **can** control steering, acceleration, and braking simultaneously
- The system **cannot** handle unexpected situations, edge cases, or complex scenarios reliably
- The **driver** is the fallback for every situation the system cannot handle
- The **driver** bears full legal responsibility for the vehicle's operation
## Driver Obligations
As the operator of a Level 2 ADAS vehicle, you must:
- **Keep your hands on the steering wheel** at all times
- **Keep your eyes on the road** and maintain situational awareness
- **Be ready to intervene immediately** — the system can disengage or behave unexpectedly without warning
- **Follow all traffic laws** — the system does not replace your judgment or legal obligations
- **Never rely on the system** as a substitute for attentive driving
## NHTSA Guidance
The National Highway Traffic Safety Administration (NHTSA) classifies Level 2 systems as requiring **full driver engagement**. NHTSA's position is clear:
- Level 2 systems are **driver support features**, not automated driving systems
- The driver must be able to perform the complete driving task at all times
- Manufacturers and operators share responsibility for safe use of these systems
For more information, see [NHTSA's guidance on automated vehicles](https://www.nhtsa.gov/technology-innovation/automated-vehicles-safety).
## Related Pages
- [Safety Information](safety.md) — General safety guidelines and system limitations
- [Prohibited Modifications](prohibited-modifications.md) — Modifications and uses that are not allowed

11
docs_sp/safety/index.md Normal file
View File

@@ -0,0 +1,11 @@
---
title: Safety
---
# Safety
Read these pages before using sunnypilot. Understanding the safety model and your responsibilities as a driver is essential.
- **[Safety Information](safety.md)** - Core safety principles and system limitations
- **[Driver Responsibility & L2 ADAS](driver-responsibility.md)** - What Level 2 driving assistance means for you
- **[Prohibited Modifications](prohibited-modifications.md)** - Modifications that compromise safety and are not supported

View File

@@ -0,0 +1,61 @@
---
title: Prohibited Modifications
---
# Prohibited Modifications
Certain modifications to sunnypilot are prohibited for safety reasons.
!!! danger "Zero Tolerance Policy"
All official sunnypilot branches strictly adhere to [comma.ai's safety policy](https://github.com/commaai/openpilot/blob/master/docs/SAFETY.md). Any changes against this policy will result in your fork and your device being **banned from both comma.ai and sunnypilot channels**.
## Panda Safety Violations
The following modifications to panda safety logic are strictly prohibited:
- **Preventing longitudinal disengagement on brake** — The system must disengage longitudinal control when the brake pedal is pressed. Overriding this behavior is prohibited.
- **Automatic re-engagement after braking** — Automatically re-engaging longitudinal control upon brake release without explicit driver input is prohibited.
- **Operating with cruise main off** — The system must disengage when cruise control main is in the off state. Bypassing this check is prohibited.
- **Removing steering torque limits** — Modifying or removing the safety-enforced steering torque limits is prohibited.
- **Bypassing vehicle safety interlocks** — Disabling or circumventing any vehicle-level safety interlock is prohibited.
### Panda Safety Limits (Example: Hyundai/Kia/Genesis)
The panda enforces hard acceleration limits that cannot be overridden by software:
| Parameter | Limit |
|-----------|-------|
| **Maximum acceleration** | +2.0 m/s² |
| **Maximum deceleration** | -3.5 m/s² |
These limits are enforced at the hardware level by the panda safety firmware. Any attempt to exceed these limits is blocked regardless of what the software commands.
## Driver Monitoring Violations
- **Reducing or weakening driver monitoring parameters** — Any modification that lowers the sensitivity, delays the response, or otherwise weakens the driver monitoring system is prohibited. This includes increasing allowed distraction time, reducing alert thresholds, or disabling monitoring entirely.
### Driver Monitoring Escalation Timeline
The driver monitoring system follows a strict escalation timeline:
| Phase | Timeout | Description |
|-------|---------|-------------|
| **Active monitoring** | 11 seconds | Time before the first distraction alert when the driver is not looking at the road |
| **Passive monitoring** | 30 seconds | Time before an alert when the driver's face is not detected (e.g., face covered or camera obstructed) |
| **Terminal alerts** | 3 strikes | After 3 terminal-level alerts (driver unresponsive), the system locks out and requires a full restart |
Modifying any of these thresholds to be more permissive is strictly prohibited.
## General Prohibitions
- Disabling or bypassing driver monitoring
- Modifying braking safety thresholds
- Any modification that reduces the driver's ability to take manual control
## Why These Restrictions Exist
These restrictions protect you, your passengers, and other road users. Safety-critical systems have been carefully tuned and tested. Unauthorized modifications can have unpredictable and dangerous consequences.
!!! info "References"
- [comma.ai Safety Policy](https://github.com/commaai/openpilot/blob/master/docs/SAFETY.md)
- [Official sunnypilot Branches](../references/recommended-branches.md)

38
docs_sp/safety/safety.md Normal file
View File

@@ -0,0 +1,38 @@
---
title: Safety Information
---
# Safety Information
!!! danger "Important"
sunnypilot is a **driver assistance** system. It is **NOT** a self-driving system. The driver must maintain attention at all times.
## Driver Responsibilities
- **Keep your hands on the wheel** at all times
- **Keep your eyes on the road** — do not rely on the system to detect all hazards
- **Be ready to take over** immediately at any time
- **Follow all traffic laws** — the system does not replace your judgment
## System Limitations
sunnypilot may not perform well in:
- Heavy rain, snow, or fog
- Construction zones
- Unmarked or poorly marked roads
- Unusual traffic situations
- Night driving with poor lighting
## When to Disengage
Immediately take manual control if:
- The system behaves unexpectedly
- Road conditions change suddenly
- You enter an unfamiliar or complex driving situation
- Emergency vehicles are present
## Prohibited Use
See [Prohibited Modifications](prohibited-modifications.md) for modifications and uses that are not allowed.

View File

@@ -0,0 +1,87 @@
---
title: Cruise Control Settings
---
# Cruise Control Settings
Configure adaptive cruise control behavior, including intelligent button management, smart cruise features, and custom speed increments.
**Location**: `Settings -> Cruise`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Intelligent Cruise Button Management (ICBM) (Alpha)
Allows sunnypilot to dynamically manage cruise speed by intercepting button presses on the steering wheel. When enabled, speed adjustments are handled intelligently to support features like Speed Limit Assist.
!!! note "Vehicle Compatibility"
This toggle only appears if your vehicle supports ICBM. If you do not see it, your vehicle does not support this feature.
---
## Smart Cruise Control - Vision
Uses vision-based path predictions to estimate the appropriate speed to drive through turns ahead. The system monitors predicted lateral acceleration and smoothly decelerates when entering curves, then accelerates back to your set speed when exiting.
!!! note "Availability"
Requires longitudinal control, or ICBM must be enabled.
---
## Smart Cruise Control - Map
Uses map data to estimate the appropriate speed to drive through turns ahead. The system calculates deceleration distance based on your current speed and upcoming waypoint target velocities.
!!! note "Availability"
Requires longitudinal control, or ICBM must be enabled.
---
## Custom ACC Speed Increments
Enables customization of how much the set speed changes with short and long button presses on the steering wheel.
!!! note "Availability"
Requires longitudinal control (or ICBM enabled), and PCM Cruise must not be active.
### Short Press Increment
Once the toggle is enabled, a selector appears below it. Use the **-** and **+** buttons to set the speed change (in km/h or mph) for a short press of the cruise speed button. The range is 1 to 10.
### Long Press Increment
A second selector lets you choose the speed change for a long press:
| Selection | Speed change |
|-----------|-------------|
| **1** | 1 km/h (or mph) |
| **2** | 5 km/h (or mph) |
| **3** | 10 km/h (or mph) |
---
## Enable Dynamic Experimental Control
Automatically switches between standard and experimental driving mode based on driving conditions. When enabled, the system uses real-time signals (speed, turn detection, stop signs, traffic lights) to determine the most appropriate mode.
!!! note "Availability"
Requires longitudinal control. The device must be offroad (parked, ignition off) to change this setting.
---
## Speed Limit
A button that opens the [Speed Limit sub-panel](speed-limit/index.md) where you can configure speed limit mode, offset, and data source.
---
## Related Features
- [Intelligent Cruise Button Management](../../features/cruise/icbm.md)
- [Smart Cruise Control - Vision](../../features/cruise/scc-v.md)
- [Smart Cruise Control - Map](../../features/cruise/scc-m.md)
- [Custom ACC Increments](../../features/cruise/custom-acc-increments.md)
- [Dynamic Experimental Control](../../features/cruise/dynamic-experimental-control.md)

View File

@@ -0,0 +1,66 @@
---
title: Speed Limit Settings
---
# Speed Limit Settings
Configure how sunnypilot responds to detected speed limits from maps, signs, and navigation data.
**Location**: `Settings -> Cruise -> Speed Limit`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Speed Limit
A row of four buttons that controls how sunnypilot uses speed limit information:
| Mode | What it does |
|------|-------------|
| **Off** | Speed limit data is not used |
| **Info** | Displays the current speed limit on the HUD |
| **Warning** | Displays speed limit and alerts you when exceeding it |
| **Assist** | Automatically adjusts cruise speed to match the speed limit (with optional offset) |
!!! note "Availability"
Requires longitudinal control, or ICBM must be enabled.
!!! warning "Vehicle Restrictions"
- **Tesla**: Speed Limit Assist mode is disabled on release branches
- **Rivian**: Speed Limit Assist mode is always disabled
---
## Customize Source
A button that opens the [Speed Limit Source sub-panel](source.md) where you choose which data source provides speed limit information (car, map, or a combination).
---
## Speed Limit Offset
A row of three buttons that controls how the speed offset is calculated:
| Type | What it does |
|------|-------------|
| **None** | No offset - cruise matches the exact speed limit |
| **Fixed** | A fixed value (in km/h or mph) is added to or subtracted from the limit |
| **%** | A percentage is applied to the speed limit |
---
## Speed Limit Value Offset
A slider that sets the offset value applied to the detected speed limit. Use the **-** and **+** buttons to adjust from -30 to +30. This slider only appears when Speed Limit Offset is set to Fixed or %.
- **Positive values**: Cruise faster than the limit (e.g., +5 means 5 over)
- **Negative values**: Cruise slower than the limit (e.g., -5 means 5 under)
---
## Related Features
- [Speed Limit Assist](../../../features/cruise/speed-limit.md)
- [OSM Maps](../../../features/connected/osm-maps.md)

View File

@@ -0,0 +1,37 @@
---
title: Speed Limit Source
---
# Speed Limit Source
**Location**: `Settings -> Cruise -> Speed Limit -> Customize Source`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
Configure which data source sunnypilot uses to determine the current speed limit, and how multiple sources are prioritized.
---
## Speed Limit Source
A row of five buttons lets you choose the source policy:
| Option | What it does |
|--------|-------------|
| **Car Only** | Uses only the speed limit data provided by your vehicle's built-in systems (e.g., traffic sign recognition from the car's cameras) |
| **Map Only** | Uses only OpenStreetMap data for speed limits. Requires a downloaded map database from the [OSM panel](../../osm.md) |
| **Car First** | Checks the car's speed limit data first. Falls back to map data if the car does not report a limit |
| **Map First** | Checks map data first. Falls back to the car's reported limit if no map data is available |
| **Combined** | Uses the higher of the two available values from car and map sources |
!!! tip
**Car First** is a good default for vehicles with traffic sign recognition. If your vehicle does not have sign recognition, use **Map Only** or **Map First** and make sure you have downloaded the map for your region in the [OSM panel](../../osm.md).
---
## Related
- [Speed Limit Settings](index.md) - mode, offset type, and offset value
- [OSM Panel](../../osm.md) - download map data for your region
- [Speed Limit Assist (Feature)](../../../features/cruise/speed-limit.md) - how Speed Limit Assist works

View File

@@ -0,0 +1,136 @@
---
title: Developer Settings
---
# Developer Settings
Advanced settings for developers and power users, including debug tools, connectivity options, and alpha features.
**Location**: `Settings -> Developer`
!!! info "Toggle & Device Availability"
Available and configurable on all devices (comma 3X/3, comma four, sunnylink).
---
## Show Advanced Controls
Reveals additional advanced settings throughout the sunnypilot interface. Enabling this does not change any driving behavior - it only makes hidden controls visible.
Settings gated behind this toggle include:
- [Models](models.md): Adjust Lane Turn Speed, Adjust Software Delay
- [Software](software.md): Disable Updates
- Developer: GitHub Runner Service, copyparty Service, Quickboot Mode
---
## Enable ADB
Enables ADB (Android Debug Bridge) for connecting to the device over USB or network for debugging.
!!! note "Availability"
Can only be changed while the device is offroad.
---
## Enable SSH
Enables SSH access to the device for remote terminal sessions.
---
## SSH Keys
A button that fetches SSH public keys from a GitHub username and installs them on the device. This allows SSH login using your GitHub-associated key pair.
!!! warning
Only add SSH keys from users you trust. Anyone with an installed key has full access to the device.
---
## Joystick Debug Mode
Enables joystick-controlled driving for testing and development purposes.
!!! note "Availability"
Hidden on release branches. Can only be changed while the device is offroad.
---
## Longitudinal Maneuver Mode
Enables a longitudinal debug mode for testing acceleration and braking maneuvers.
!!! note "Availability"
Hidden on release branches. Requires longitudinal control and the device must be offroad.
---
## sunnypilot Longitudinal Control (Alpha)
Enables alpha-quality longitudinal control for vehicles that do not yet have full upstream support. This replaces the car's stock adaptive cruise control with sunnypilot's own gas and brake management.
!!! warning
Enabling this disables AEB (Automatic Emergency Braking). A confirmation dialog appears before activation.
!!! note "Availability"
Hidden on release branches. Only appears if your vehicle supports alpha longitudinal control.
---
## UI Debug Mode
Shows touch indicators, FPS counter, and mouse coordinates on screen. Useful for UI development and troubleshooting display issues.
---
## GitHub Runner Service
Enables the GitHub Actions self-hosted runner service on the device. This allows the device to execute CI/CD jobs from your repository.
!!! note "Availability"
Only appears when **Show Advanced Controls** is enabled. Hidden on release branches.
---
## copyparty Service
Enables a file server on the device for downloading driving routes and logs from a web browser via the device's local IP address.
!!! note "Availability"
Only appears when **Show Advanced Controls** is enabled.
---
## Quickboot Mode
Creates a prebuilt file for accelerated boot, reducing startup time. Requires software updates to be disabled first.
!!! note "Availability"
Only appears when **Show Advanced Controls** is enabled, the device is not on a release or development branch, and **Disable Updates** is enabled in [Software Settings](software.md).
---
## Error Log
A **VIEW** button that opens the sunnypilot crash error log in a viewer. On close, you are offered the option to delete the log.
!!! note "Availability"
Hidden on release branches.
---
## Platform Differences
On **comma four**, the Developer panel includes:
- ADB (circle icon toggle)
- SSH (circle icon toggle)
- SSH Keys
- Joystick Debug Mode
- Longitudinal Maneuver Mode
- Alpha Longitudinal
- UI Debug Mode
The comma four panel does not include: Show Advanced Controls, GitHub Runner Service, copyparty Service, Quickboot Mode, or Error Log.

137
docs_sp/settings/device.md Normal file
View File

@@ -0,0 +1,137 @@
---
title: Device Settings
---
# Device Settings
View device information, manage calibration, configure power behavior, and access system utilities.
**Location**: `Settings -> Device`
!!! info "Toggle & Device Availability"
Available and configurable on all devices (comma 3X/3, comma four, sunnylink).
---
## Device Information
The top of the panel displays your device's **Dongle ID** and **Serial** number. These identifiers are used for pairing and support.
---
## Pair Device
Pairs your comma device with your comma connect account. Once paired, you can view driving routes, manage the device remotely, and access dashcam footage through the comma connect app.
---
## Reset Calibration
Resets the device's calibration data. After reset, the system will re-learn the camera's mounting position during your next drive. The current calibration status is shown in the description text.
---
## Change Language
Opens a language selection dialog where you can choose the display language for the interface.
---
## Enable Always Offroad / Exit Always Offroad
Toggles "Always Offroad" mode. When enabled, the device stays in its offroad (parked) state even when the vehicle is running. This is useful for configuring settings or downloading updates without driving. The button text changes based on the current state.
---
## Wake Up Behavior
A row of two buttons that controls what the device does when it wakes up:
| Option | What it does |
|--------|-------------|
| **Default** | Device boots normally and enters the driving screen |
| **Offroad** | Device boots directly into offroad (settings) mode |
---
## Max Time Offroad
A selector that sets how long the device stays powered on after the engine is turned off before automatically shutting down. Values range from 5 minutes to 30 hours, or **Always On** to prevent auto-shutdown.
---
## Quiet Mode
Toggles quiet mode on or off. When enabled, the device suppresses non-critical sounds. Safety-critical alerts are never silenced.
---
## Driver Camera Preview
Opens a live preview of the driver-facing camera so you can verify its position and angle.
!!! note "Availability"
Disabled while the vehicle is onroad.
---
## Onroad Uploads
Toggles data uploads while driving. When enabled, the device uploads driving segments over a cellular or Wi-Fi connection during your drive instead of waiting until parked.
---
## Training Guide
Opens the sunnypilot training guide, which walks through the system's rules, features, and limitations.
!!! note "Availability"
Disabled while the vehicle is onroad.
---
## Regulatory
Displays FCC regulatory information for the device.
!!! note "Availability"
Disabled while the vehicle is onroad.
---
## Reset Settings
Resets all sunnypilot settings to their defaults. This is a two-step confirmation to prevent accidental resets. After confirmation, the device reboots.
!!! warning
This action cannot be undone. All custom settings will be lost.
!!! note "Availability"
Disabled while the vehicle is onroad.
---
## Reboot / Power Off
**Reboot** restarts the device. **Power Off** shuts the device down completely.
Power Off is hidden while the vehicle is onroad to prevent accidental shutdown during driving.
---
## Platform Differences
On **comma four**, the Device panel has a simplified layout with these items:
- Device ID and Serial (info display)
- Update sunnypilot
- Pair
- Review Training Guide
- Driver Camera Preview
- Terms & Conditions
- Regulatory Info
- Reset Calibration
- Uninstall sunnypilot
- Reboot / Power Off (circle buttons)
The comma four panel does not include: Always Offroad, Wake Up Behavior, Max Time Offroad, Quiet Mode, Onroad Uploads, Reset Settings, or Change Language.

View File

@@ -0,0 +1,52 @@
---
title: Display Settings
---
# Display Settings
Control screen brightness, timing, and UI behavior for the onroad display.
**Location**: `Settings -> Display`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Onroad Brightness
A selector that controls the screen brightness while driving. Use the **-** and **+** buttons to cycle through the options:
| Option | What it does |
|--------|-------------|
| **Auto (Default)** | Adjusts brightness automatically based on ambient light |
| **Auto (Dark)** | Adjusts automatically but biased toward a darker screen |
| **Screen Off** | Turns the screen off completely after the delay period |
| **0% - 100%** | Fixed brightness level in 5% increments |
---
## Onroad Brightness Delay
A selector that sets how long the screen stays at full brightness before the selected brightness mode takes effect. Available delay values range from seconds to minutes.
!!! note "Availability"
This selector is disabled when Onroad Brightness is set to Auto (Default) or Auto (Dark), since those modes manage brightness continuously.
---
## Interactivity Timeout
A selector that controls how long the settings interface stays open before automatically closing and returning to the driving screen. Use the **-** and **+** buttons to choose a timeout:
| Option | Timeout |
|--------|---------|
| **Default** | Standard system timeout |
| **10s - 120s** | Custom timeout in 10-second increments |
| **1m / 2m** | One or two minute timeout |
---
## Related Features
- [Onroad Display](../features/display/onroad-display.md)

View File

@@ -0,0 +1,51 @@
---
title: Firehose Settings
---
# Firehose Settings
Firehose Mode uploads training data from your drives to comma's servers, helping improve autonomous driving models.
**Location**: `Settings -> Firehose`
!!! info "Toggle & Device Availability"
Available and configurable on all devices (comma 3X/3, comma four, sunnylink).
!!! info "First Fork with Official Support"
sunnypilot is the first and currently only major fork to receive official Firehose Mode support.
---
## How It Works
When Firehose Mode is active, the system randomly samples a portion of your drives rather than uploading everything. It queues approximately 10 segments at a time for transmission, whether sunnypilot is actively engaged or not. Both engaged and disengaged driving data contributes to simulator training.
No special driving behavior is needed. Normal driving patterns work fine.
---
## Status Display
The panel shows whether Firehose Mode is currently active or inactive:
- **Active** (green): The device is connected to an unmetered network and uploading data.
- **Inactive** (red): Connect to an unmetered Wi-Fi network to begin uploading.
If available, the panel also displays the number of segments you have contributed to the training dataset.
---
## Requirements
- **Compatible branch**: You must be running one of the following branches:
- `staging`, `dev`, or `master`
- `staging-tici`, `master-tici`
- `release-tizi` or `release-tici`
- **Weekly connectivity**: Connect to Wi-Fi with a reliable USB-C power adapter at minimum once per week.
- **Supported vehicle**: Only vehicles officially supported in upstream openpilot qualify for model training data collection.
---
## Optional: Mobile Upload
Devices with hotspot access or unlimited data plans can upload data while driving, removing the need for weekly Wi-Fi sessions.

28
docs_sp/settings/index.md Normal file
View File

@@ -0,0 +1,28 @@
---
title: Settings
---
# Settings
Complete reference for every configurable option in sunnypilot. The panels below are listed in the same order they appear on your device's settings sidebar.
!!! info "Not all settings appear on every device"
comma four users see a simplified settings screen. Most features still work on comma four - you just configure the hidden ones through sunnylink. A few onroad visual features are not available on comma four at all. See [Platform Differences](platform-differences.md) for the full breakdown.
| Panel | Description |
|-------|-------------|
| [Device](device.md) | Device information, calibration, power, and system controls |
| [Network](network.md) | Wi-Fi connection management |
| [sunnylink](sunnylink.md) | Cloud integration, backup/restore, sponsor status |
| [Toggles](toggles.md) | Core driving toggles (enable sunnypilot, experimental mode, lane departure, etc.) |
| [Software](software.md) | Update management and branch selection |
| [Models](models.md) | Driving model selection, download, and tuning |
| [Steering](steering/index.md) | Lateral control: MADS, lane change, blinker pause, and torque tuning |
| [Cruise](cruise/index.md) | Cruise behavior: ICBM, smart cruise, custom increments, speed limit assist |
| [Visuals](visuals.md) | HUD overlays, indicators, and visual customization |
| [Display](display.md) | Screen brightness, timeout, and interactivity settings |
| [OSM](osm.md) | OpenStreetMap database and map downloads |
| [Trips](trips.md) | Drive statistics and trip history |
| [Vehicle](vehicle/index.md) | Vehicle-specific settings by brand |
| [Firehose](firehose.md) | Training data upload for comma's driving models |
| [Developer](developer.md) | Developer and debug options |

View File

@@ -0,0 +1,83 @@
---
title: Models Settings
---
# Models Settings
Select and manage driving models, and configure model-related parameters.
**Location**: `Settings -> Models`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Current Model
A button labeled **SELECT** that opens a model selection dialog. The currently active model name is displayed. Choose from available driving models, each with different driving characteristics and capabilities.
!!! warning "Offroad Only"
Model selection can only be changed while the vehicle is offroad (parked with ignition off), unless Always Offroad mode is active.
---
## Model Download Progress
When a model is being downloaded, progress bars appear for each component:
- **Driving Model** - the main supercombo neural network
- **Vision Model** - the vision processing model
- **Policy Model** - the decision-making policy model
Each shows a progress bar with download percentage.
---
## Refresh Model List
A button that forces a refresh of the available model list from the server. Use this if you expect a new model to be available but do not see it in the selection dialog.
---
## Clear Model Cache
A button that deletes all downloaded models except the currently active one. The current cache size in MB is displayed. Useful for freeing storage space.
---
## Cancel Download
A button that appears only while a model download is in progress. Cancels the current download.
---
## Use Lane Turn Desires
When enabled, at speeds of 20 mph (32 km/h) or below with the turn signal on, the model plans a turn in the blinker direction at the nearest drivable path. This helps the model make smoother turns at low speeds.
---
## Adjust Lane Turn Speed
A selector that sets the maximum speed at which lane turn desires are active. Default is 19 mph. Use the **-** and **+** buttons to adjust.
!!! note "Availability"
Only appears when **Use Lane Turn Desires** is enabled and **Show Advanced Controls** is enabled in [Developer Settings](developer.md).
---
## Live Learning Steer Delay
Enables real-time learning and adaptive steering response time. The system continuously measures the actual steering delay and adjusts compensation accordingly. When disabled, a fixed response time is used instead.
The description text shows the current live delay value or a breakdown of delay components.
---
## Adjust Software Delay
A selector that sets the fixed software delay value used when Live Learning Steer Delay is disabled. Default is 0.2 seconds. Use the **-** and **+** buttons to adjust.
!!! note "Availability"
Only appears when **Live Learning Steer Delay** is disabled and **Show Advanced Controls** is enabled in [Developer Settings](developer.md).

View File

@@ -0,0 +1,22 @@
---
title: Network Settings
---
# Network Settings
Manage Wi-Fi connections on your comma device.
**Location**: `Settings -> Network`
!!! info "Toggle & Device Availability"
Available and configurable on all devices (comma 3X/3, comma four, sunnylink).
---
## Wi-Fi
The Network panel displays a list of available Wi-Fi networks. Tap a network name to connect, or use the **Scan** button to refresh the list.
- A stable Wi-Fi connection is required for software updates and route uploads.
- For Firehose Mode, connect to Wi-Fi with a reliable USB-C power adapter at least once per week.
- Hotspot and tethered connections also work for data uploads while driving if your data plan supports it.

68
docs_sp/settings/osm.md Normal file
View File

@@ -0,0 +1,68 @@
---
title: OSM Settings
---
# OSM Settings
Download and manage OpenStreetMap (OSM) data used for speed limit lookups, curve detection, and road name display.
**Location**: `Settings -> OSM`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Mapd Version
Displays the version of the map processing daemon currently running on the device.
---
## Downloaded Maps
Shows the current size of downloaded map data. A **DELETE** button removes all downloaded map files to free storage space.
!!! note "Availability"
The delete button is disabled while a map download is in progress.
---
## Downloading Map
A progress indicator that appears only while a map download or database check is active. Shows the download percentage and the file being downloaded.
---
## Database Update
A **CHECK** button that checks for updates to your selected region's OSM database. After checking, it downloads any available updates. The description shows the last check time or current download progress.
!!! note "Availability"
Only appears when a country has been selected. Disabled while a download is in progress.
---
## Country
A **SELECT** button that opens a country selection dialog. Choose the country for which you want to download map data. The currently selected country is displayed.
!!! note "Availability"
Disabled while a download is in progress.
---
## State
A **SELECT** button that opens a US state selection dialog. Narrows the download to a specific state, reducing download size and improving performance.
!!! note "Availability"
Only appears when the selected country is the United States. Disabled while a download is in progress.
---
## Related Features
- [OSM Maps](../features/connected/osm-maps.md)
- [Speed Limit Assist](../features/cruise/speed-limit.md)
- [Smart Cruise Control - Map](../features/cruise/scc-m.md)

View File

@@ -0,0 +1,79 @@
---
title: Platform Differences
---
# Platform Differences
sunnypilot runs on different hardware platforms, each with its own settings interface. This page explains the differences so you know what to expect.
## Platforms
### comma 3X / comma 3 (TIZI/TICI)
The full-featured interface with a sidebar-based settings panel. All settings and onroad visual features documented in this manual are available on these devices.
### comma four (MICI)
A simplified, touch-friendly interface designed for the comma four's compact form factor. Only a subset of settings are exposed through the on-device screen. Most features still work on comma four - you configure the hidden ones through sunnylink.
However, some onroad visual features require UI rendering code that does not exist on comma four. These features cannot be activated on comma four even through sunnylink. Each setting page notes which category it falls into.
### sunnylink (Web / App)
The sunnylink web interface provides access to all configurable settings regardless of which device you use. If a setting is not shown on your comma four's screen, you can configure it through sunnylink - provided the feature is supported on your device.
## Three Levels of Device Availability
Throughout this manual, each setting includes a "Toggle & Device Availability" note using one of these three patterns:
| Level | Badge Text | What It Means |
|-------|-----------|---------------|
| **Full** | "Available and configurable on all devices (comma 3X/3, comma four, sunnylink)." | The setting appears on every device's screen and in sunnylink. |
| **Config-Hidden** | "Supported on all devices. comma four users must use sunnylink to change this setting." | The feature works on comma four, but the toggle is hidden from the device screen. Use sunnylink to configure it. |
| **Unsupported** | "Supported on: comma 3X/3 only. This feature is currently not available on comma four and cannot be forced via sunnylink." | The feature requires onroad UI rendering code that does not exist on comma four. Forcing the param via sunnylink will not make it work. |
## On-Screen Settings Availability
The table below shows which settings panels have on-screen controls on each platform. A :material-close: means the panel is not shown on that device's screen.
| Panel | comma 3X/3 | comma four | sunnylink |
|-------|:----------:|:----------:|:---------:|
| [Device](device.md) | :material-check: | :material-check: (simplified) | :material-check: |
| [Network](network.md) | :material-check: | :material-check: | :material-check: |
| [sunnylink](sunnylink.md) | :material-check: | :material-close: | :material-check: |
| [Toggles](toggles.md) | :material-check: | :material-check: (fewer items) | :material-check: |
| [Software](software.md) | :material-check: | :material-close: | :material-check: |
| [Models](models.md) | :material-check: | :material-close: | :material-check: |
| [Steering](steering/index.md) | :material-check: | :material-close: | :material-check: |
| [Cruise](cruise/index.md) | :material-check: | :material-close: | :material-check: |
| [Visuals](visuals.md) | :material-check: | :material-close: | :material-check: |
| [Display](display.md) | :material-check: | :material-close: | :material-check: |
| [OSM](osm.md) | :material-check: | :material-close: | :material-check: |
| [Trips](trips.md) | :material-check: | :material-close: | :material-check: |
| [Vehicle](vehicle/index.md) | :material-check: | :material-close: | :material-check: |
| [Firehose](firehose.md) | :material-check: | :material-check: (condensed) | :material-check: |
| [Developer](developer.md) | :material-check: | :material-check: | :material-check: |
## Onroad Visual Features Not Available on comma four
The following features from the [Visuals](visuals.md) panel rely on onroad UI rendering code that only exists on comma 3X/3. These cannot be enabled on comma four:
- Enable Standstill Timer
- Display Road Name
- Green Traffic Light Alert (Beta)
- Lead Departure Alert (Beta)
- Speedometer: Always Display True Speed
- Speedometer: Hide from Onroad Screen
- Display Turn Signals (always-on display; lane change alert icons still appear)
- Real-time Acceleration Bar
- Display Metrics Below Chevron
- Developer UI (onroad overlay)
## What This Means for You
If you use a **comma four**, your on-device screen shows a simplified set of controls. Most sunnypilot features still run on your device - you configure the hidden ones through **sunnylink**. The exception is the onroad visual features listed above, which require rendering code that does not exist on comma four.
If you use a **comma 3X or comma 3**, all settings and onroad visual features are directly accessible from the device sidebar.
!!! tip
Throughout this manual, each setting includes a "Toggle & Device Availability" note. Look for these callouts to quickly check whether a feature works on your device and where to configure it.

View File

@@ -0,0 +1,60 @@
---
title: Software Settings
---
# Software Settings
Manage software updates, view release notes, and switch branches.
**Location**: `Settings -> Software`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Current Version
Displays the currently installed version of sunnypilot. The description text shows release notes for the current version.
---
## Download
A button that checks for available updates. If an update is found, the button changes to **DOWNLOAD** to begin downloading. Progress is shown during the download.
!!! note "Availability"
Only available while the device is offroad.
---
## Install Update
A button that appears after an update has been downloaded. Tapping it reboots the device to install the update.
!!! note "Availability"
Only appears when an update has been downloaded and the device is offroad.
---
## Target Branch
A button labeled **SELECT** that opens a branch selection dialog. Choose which sunnypilot branch to track for updates. The currently selected branch is displayed.
!!! tip
For daily driving, stay on the latest **release** branch. Switch to staging or dev branches only if you want to test new features.
---
## Uninstall
A button that uninstalls sunnypilot from the device. Requires confirmation before proceeding.
---
## Disable Updates
Prevents the device from checking for or applying software updates. The device remains on its current version indefinitely. A reboot prompt appears when toggling this setting.
!!! note "Availability"
Only appears when **Show Advanced Controls** is enabled in [Developer Settings](developer.md). Can only be changed while the device is offroad.

View File

@@ -0,0 +1,89 @@
---
title: Steering Settings
---
# Steering Settings
Configure lateral (steering) control behavior including MADS, lane changes, blinker pause, and torque tuning.
**Location**: `Settings -> Steering`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Modular Assistive Driving System (MADS)
A toggle that enables MADS, which decouples lateral (steering) control from longitudinal (speed) control. With MADS on, steering assistance can remain active even when cruise control is not engaged.
Turn this toggle **on** to enable MADS, or **off** to revert to standard engagement behavior.
When enabled, a **Customize MADS** button appears below the toggle. Tap it to open the [MADS sub-panel](mads.md) where you can configure engagement modes and brake pedal behavior.
!!! note "Platform-specific behavior"
Some vehicles (Rivian, certain Tesla configurations) show limited MADS settings. The system displays a note indicating which options are restricted for your vehicle.
---
## Customize Lane Change
A button that opens the [Lane Change sub-panel](lane-change.md) where you can set the auto lane change timer and blind spot monitoring delay.
---
## Pause Lateral Control with Blinker
When you activate your turn signal, this feature temporarily pauses automatic steering so you can take manual control of the wheel. Useful during merges, parking, or any situation where you want to steer by hand while keeping the system otherwise active.
Turn this toggle **on** to enable it.
### Minimum Speed to Pause Lateral Control
Once the blinker pause toggle is enabled, a speed selector appears directly below it. Use the **-** and **+** buttons to set the speed threshold. Lateral control only pauses when your current speed is below this value while the blinker is on. The speed displays in your preferred unit (km/h or mph).
### Post-Blinker Delay
A second selector lets you choose how many seconds the system waits after the blinker turns off before resuming automatic steering. The delay ranges from 0 to 10 seconds.
!!! tip
Set the delay to at least 1-2 seconds to avoid the system resuming steering while you are still mid-maneuver.
---
## Enforce Torque Lateral Control
Forces torque-based lateral control on your vehicle, even if it does not use torque control by default.
When enabled, a **Customize Torque Params** button appears below the toggle. Tap it to open the [Torque sub-panel](torque.md) for advanced tuning.
!!! note "Availability"
This toggle is hidden on vehicles with angle-based steering. Enabling this disables Neural Network Lateral Control (NNLC), and vice versa. The device must be offroad (parked, ignition off) to change this setting.
---
## Neural Network Lateral Control (NNLC)
Enables a neural network model to handle lateral control instead of the traditional controller. NNLC can provide smoother steering in some situations.
!!! note "Availability"
This toggle is hidden on vehicles with angle-based steering. Enabling this disables Enforce Torque Lateral Control, and vice versa. The device must be offroad to change this setting.
---
## Sub-Panels
The Steering section contains these additional configuration pages:
- **[MADS Settings](mads.md)** - configure engagement modes and brake pedal behavior
- **[Lane Change Settings](lane-change.md)** - configure auto lane change timing and blind spot monitoring
- **[Torque Settings](torque.md)** - fine-tune torque lateral control parameters
---
## Related Features
- [Modular Assistive Driving System](../../features/steering/mads.md)
- [Neural Network Lateral Control](../../features/steering/nnlc.md)
- [Auto Lane Change](../../features/steering/auto-lane-change.md)
- [Torque Control](../../features/steering/torque-control.md)

View File

@@ -0,0 +1,43 @@
---
title: Lane Change Settings
---
# Lane Change Settings
Configure automatic lane change behavior and blind spot monitoring integration.
**Location**: `Settings -> Steering -> Customize Lane Change`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Auto Lane Change by Blinker
A selector that controls how lane changes are initiated when you activate the turn signal. Use the **-** and **+** buttons to cycle through the options:
| Option | What happens |
|--------|-------------|
| **Off** | Auto lane change is disabled |
| **Nudge** | Requires a light steering nudge in the desired direction to start the lane change (default) |
| **Nudgeless** | Lane change begins immediately when the turn signal is activated - no nudge needed |
| **0.5 s** | Lane change begins 0.5 seconds after the turn signal is activated |
| **1 s** | Lane change begins 1 second after activation |
| **2 s** | Lane change begins 2 seconds after activation |
| **3 s** | Lane change begins 3 seconds after activation |
---
## Auto Lane Change: Delay with Blind Spot
When enabled, the system waits for the blind spot to clear before executing a lane change. If a vehicle is detected in the blind spot, the lane change is delayed until the path is clear.
!!! note "Availability"
This toggle only appears if your vehicle supports Blind Spot Monitoring (BSM) and Auto Lane Change by Blinker is set to a mode other than Off or Nudge.
---
## Related Features
- [Auto Lane Change](../../features/steering/auto-lane-change.md)

View File

@@ -0,0 +1,62 @@
---
title: MADS Settings
---
# MADS Settings
Fine-tune how the Modular Assistive Driving System (MADS) engages and disengages steering assistance.
**Location**: `Settings -> Steering -> Customize MADS`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
!!! info "Prerequisite"
The main **Modular Assistive Driving System (MADS)** toggle must be enabled in [Steering Settings](index.md) to access this sub-panel.
---
## Toggle with Main Cruise
When enabled, pressing the main cruise button (the on/off button on the steering wheel) can activate MADS steering assistance.
!!! warning "Vehicle Restrictions"
- **Rivian**: Always forced OFF (limited settings mode)
- **Tesla (without vehicle bus)**: Always forced OFF (limited settings mode)
!!! note
For vehicles without a dedicated LFA/LKAS button, disabling this will prevent lateral control engagement.
---
## Unified Engagement Mode (UEM)
When enabled, engaging cruise control also automatically engages MADS lateral control in a single action, rather than requiring separate activation.
Once lateral control is engaged via UEM, it remains engaged until you manually disable it using the MADS button or turn off the car.
!!! warning "Vehicle Restrictions"
- **Rivian**: Always forced ON (limited settings mode)
- **Tesla (without vehicle bus)**: Always forced ON (limited settings mode)
---
## Steering Mode on Brake Pedal
A row of three buttons controls what happens to MADS steering assistance when you press the brake pedal:
| Mode | What happens |
|------|-------------|
| **Remain Active** | Steering assistance continues even after you press the brake |
| **Pause** | Steering assistance pauses but can be quickly resumed |
| **Disengage** | Steering assistance fully disengages when you brake |
!!! warning "Vehicle Restrictions"
- **Rivian**: Always forced to Disengage (limited settings mode)
- **Tesla (without vehicle bus)**: Always forced to Disengage (limited settings mode)
---
## Related Features
- [Modular Assistive Driving System](../../features/steering/mads.md)

View File

@@ -0,0 +1,82 @@
---
title: Torque Settings
---
# Torque Settings
Advanced tuning parameters for torque-based lateral (steering) control. These settings give you fine-grained control over how the steering motor responds.
**Location**: `Settings -> Steering -> Customize Torque Params`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
!!! info "Prerequisite"
**Enforce Torque Lateral Control** must be enabled in [Steering Settings](index.md) to access this sub-panel. Not available on vehicles with angle-based steering.
!!! warning "Offroad Only"
All torque settings can only be changed while the vehicle is offroad (parked with ignition off).
---
## Torque Control Tune Version
A button that opens a version selector. Choose which version of the torque control tuning algorithm to use. Newer versions may offer improved performance but can behave differently than what you are accustomed to.
---
## Self-Tune
Enables real-time self-tuning of torque parameters based on your driving behavior. The system continuously adjusts lateral acceleration factor and friction values for optimal steering feel.
---
## Less Restrict Settings for Self-Tune (Beta)
Uses a more relaxed self-tuning profile. The torque controller is more forgiving when learning values, which may provide a smoother but less responsive steering feel.
!!! note "Availability"
This toggle only appears when **Self-Tune** is enabled.
---
## Enable Custom Tuning
Enables manual override of torque control parameters. When enabled, you can set specific values for lateral acceleration and friction instead of relying on self-tuning. The values you set here override the offline values from the vehicle's tuning data files.
---
## Manual Real-Time Tuning
Forces the torque controller to use your fixed custom values instead of the learned values from Self-Tune. Enabling this overrides Self-Tune values in real time.
!!! note "Availability"
This toggle only appears when **Enable Custom Tuning** is enabled.
---
## Lateral Acceleration Factor
A slider that controls the lateral acceleration gain. Use the **-** and **+** buttons to adjust. The value displays as a decimal (e.g., 1.50 means 1.50x lateral acceleration factor).
Higher values produce more aggressive steering response. Lower values produce softer response.
!!! note "Availability"
This slider only appears when **Enable Custom Tuning** is enabled.
---
## Friction
A slider that controls the friction compensation factor. Use the **-** and **+** buttons to adjust. The value displays as a decimal (e.g., 0.75).
Adjusts how much the system compensates for steering friction in your vehicle.
!!! note "Availability"
This slider only appears when **Enable Custom Tuning** is enabled.
---
## Related Features
- [Torque Control](../../features/steering/torque-control.md)

View File

@@ -0,0 +1,68 @@
---
title: sunnylink Settings
---
# sunnylink Settings
Configure the sunnylink cloud integration for secure backup, restore, sponsorship, and remote configuration.
**Location**: `Settings -> sunnylink`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Enable sunnylink
Master toggle for all sunnylink features. When disabled, no sunnylink requests are made. Your Dongle ID is displayed next to the toggle.
Enabling sunnylink for the first time triggers a consent prompt that must be completed before the feature activates.
!!! note "Availability"
Can only be changed while the device is offroad.
---
## Sponsor Status
A button that opens the sponsor status dialog. Displays your current sponsor tier name and color. Becoming a sponsor grants early access to sunnylink features.
!!! note "Availability"
Disabled when sunnylink is not enabled.
---
## Pair GitHub Account
A button that opens the GitHub pairing dialog. Links your GitHub account to the device for sponsor verification and API access. Displays **Paired** or **Not Paired** as the current status.
!!! note "Availability"
Disabled when sunnylink is not enabled.
---
## Enable sunnylink uploader (infrastructure test)
Uploads driving data to sunnypilot servers. This feature is currently available to highest-tier sponsors only and is used for testing data volume and upload infrastructure.
!!! note "Availability"
Disabled when sunnylink is not enabled.
---
## Backup Settings / Restore Settings
Two side-by-side buttons for managing settings backups through sunnylink:
- **Backup Settings**: Encrypts and uploads all current sunnypilot settings to the cloud. A progress indicator shows the upload status.
- **Restore Settings**: Downloads and applies the most recently backed-up settings. A progress indicator shows the download status.
!!! note "Availability"
Both buttons are disabled while the device is onroad or when sunnylink is not enabled.
---
## Related Features
- [sunnylink](../features/connected/sunnylink.md)

104
docs_sp/settings/toggles.md Normal file
View File

@@ -0,0 +1,104 @@
---
title: Toggles
---
# Toggles
Core driving assist toggles that control fundamental sunnypilot behavior, driving personality, driver monitoring, and data recording.
**Location**: `Settings -> Toggles`
!!! info "Toggle & Device Availability"
Available and configurable on all devices (comma 3X/3, comma four, sunnylink).
---
## Enable sunnypilot
Enables the sunnypilot driving assist system for adaptive cruise control and lane keeping. Your attention is required at all times while the system is active.
!!! warning "Restart Required"
Changing this toggle requires the device to be offroad (parked, ignition off). A restart is needed for the change to take effect.
---
## Experimental Mode
Activates end-to-end longitudinal control with a new driving visualization. This alpha feature uses the neural network to handle gas and braking on all road types, including city streets with stop signs and traffic lights.
!!! note "Availability"
Requires longitudinal control. Not available on vehicles using stock adaptive cruise control.
---
## Disengage on Accelerator Pedal
When enabled, pressing the gas pedal will immediately disengage sunnypilot. When disabled, pressing the gas pedal overrides sunnypilot's speed control without disengaging the system.
---
## Driving Personality
A row of three buttons that controls how closely sunnypilot follows the vehicle ahead and how aggressively it accelerates and brakes:
| Personality | Behavior |
|-------------|----------|
| **Aggressive** | Shorter following distance, quicker acceleration |
| **Standard** | Moderate following distance and response |
| **Relaxed** | Longer following distance, gentler acceleration |
!!! note "Availability"
Requires longitudinal control.
---
## Enable Lane Departure Warnings
Alerts you to steer back into your lane when the vehicle drifts over a detected lane line without the turn signal active. Warnings activate above 31 mph (50 km/h).
---
## Always-On Driver Monitoring
Keeps the driver monitoring camera active even when sunnypilot is not engaged. The system will still watch for distracted or drowsy driving whenever the vehicle is on.
---
## Record and Upload Driver Camera
Uploads footage from the driver-facing camera to help improve the driver monitoring algorithm. When enabled, driver camera data is included in uploaded driving segments.
!!! warning "Restart Required"
Changing this toggle requires the device to be offroad.
---
## Record and Upload Microphone Audio
Records microphone audio while driving. The audio is stored locally and included in dashcam video available through comma connect.
!!! warning "Restart Required"
Changing this toggle requires the device to be offroad.
---
## Use Metric System
Switches the display from miles per hour (mph) to kilometers per hour (km/h) throughout the interface.
---
## Platform Differences
On **comma four**, this panel contains the same toggles with slightly different labels:
| comma 3X/3 Label | comma four Label |
|-------------------|-----------------|
| Enable sunnypilot | enable sunnypilot |
| Driving Personality | driving personality |
| Experimental Mode | experimental mode |
| Use Metric System | use metric units |
| Enable Lane Departure Warnings | lane departure warnings |
| Always-On Driver Monitoring | always-on driver monitor |
| Record and Upload Driver Camera | record & upload driver camera |
| Record and Upload Microphone Audio | record & upload mic audio |

35
docs_sp/settings/trips.md Normal file
View File

@@ -0,0 +1,35 @@
---
title: Trips Settings
---
# Trips Settings
View your cumulative and recent driving statistics.
**Location**: `Settings -> Trips`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## All Time
A stat card showing your lifetime driving totals:
- **Drives** - total number of driving routes recorded
- **Distance** - total distance driven (km or miles, based on your unit preference)
- **Hours** - total hours of driving time
---
## Past Week
A stat card showing your driving activity from the last 7 days:
- **Drives** - number of routes recorded this week
- **Distance** - distance driven this week
- **Hours** - hours driven this week
!!! note
This panel is display-only. There are no interactive controls.

View File

@@ -0,0 +1,27 @@
---
title: Hyundai / Kia / Genesis Settings
---
# Hyundai / Kia / Genesis Vehicle Settings
Settings specific to Hyundai, Kia, and Genesis vehicles. These appear in the Vehicle panel when a supported vehicle is connected or selected.
**Location**: `Settings -> Vehicle`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Custom Longitudinal Tuning
A row of three buttons that selects a longitudinal (speed) control tuning profile:
| Option | Behavior |
|--------|----------|
| **Off** | Default tuning with standard acceleration and braking |
| **Dynamic** | More responsive acceleration and braking for a sportier feel |
| **Predictive** | Smoother, anticipatory speed changes that prioritize comfort |
!!! note "Availability"
Requires sunnypilot Longitudinal Control (Alpha) to be enabled. Can only be changed while the device is offroad. Hidden on vehicles that do not support alpha longitudinal control.

View File

@@ -0,0 +1,42 @@
---
title: Vehicle Settings
---
# Vehicle Settings
View and manage vehicle-specific settings. The content of this panel changes based on your connected or manually selected vehicle.
**Location**: `Settings -> Vehicle`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Vehicle Selection
At the top of the panel, the currently detected or selected vehicle is displayed. The vehicle name is color-coded:
| Color | Meaning |
|-------|---------|
| **Green** | Vehicle was automatically fingerprinted (detected) |
| **Blue** | Vehicle was manually selected |
| **Yellow** | Vehicle could not be fingerprinted |
- **SELECT** button: Opens a vehicle selection dialog where you can manually choose your vehicle make and model.
- **REMOVE** button: Clears the manual selection and returns to automatic fingerprinting.
A legend below the vehicle name explains the color codes.
---
## Brand-Specific Settings
After a vehicle is detected or selected, brand-specific settings appear below the vehicle selector. Only brands with configurable settings show additional items:
- [Toyota / Lexus](toyota.md) - Enforce Factory Longitudinal Control, Stop and Go Hack
- [Hyundai / Kia / Genesis](hyundai.md) - Custom Longitudinal Tuning
- [Subaru](subaru.md) - Stop and Go, Stop and Go for Manual Parking Brake
- [Tesla](tesla.md) - Cooperative Steering
The following brands are supported for vehicle selection but have no brand-specific settings: Honda, GM, Ford, Volkswagen, Nissan, Mazda, Chrysler, PSA, Rivian.

View File

@@ -0,0 +1,33 @@
---
title: Subaru Settings
---
# Subaru Vehicle Settings
Settings specific to Subaru vehicles. These appear in the Vehicle panel when a Subaru vehicle is connected or selected.
**Location**: `Settings -> Vehicle`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Stop and Go (Beta)
Enables automatic resume during stop-and-go traffic for supported Subaru platforms. Without this, you must manually resume after a full stop.
!!! note "Availability"
Can only be changed while the device is offroad. Not available on Global Gen2 or Hybrid platforms.
---
## Stop and Go for Manual Parking Brake (Beta)
Enables stop-and-go for Subaru Global models equipped with a manual handbrake (lever-type parking brake).
!!! warning
Models with an **electric parking brake** should keep this disabled. This setting is designed specifically for vehicles with a manual/lever parking brake.
!!! note "Availability"
Can only be changed while the device is offroad. Not available on Global Gen2 or Hybrid platforms.

View File

@@ -0,0 +1,46 @@
---
title: Tesla Settings
---
# Tesla Vehicle Settings
Settings specific to Tesla vehicles. These appear in the Vehicle panel when a Tesla vehicle is connected or selected.
**Location**: `Settings -> Vehicle`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Cooperative Steering (Beta)
Allows the driver to provide limited steering input while sunnypilot is engaged, rather than requiring a full override or disengage to steer manually. Only works above 23 km/h (14 mph).
!!! warning
May cause steering oscillations below 48 km/h (30 mph) during turns. Disable this feature if you experience oscillations.
!!! note "Availability"
Can only be changed while the device is offroad. Use "Always Offroad" in [Device Settings](../device.md) to change this while the vehicle is running.
---
## MADS Limitations
Tesla vehicles **without a vehicle bus connection** operate in limited MADS mode:
| Setting | Forced Value |
|---------|-------------|
| Toggle with Main Cruise | Off (locked) |
| Unified Engagement Mode | On (locked) |
| Steering Mode on Brake Pedal | Disengage (locked) |
See [MADS Settings](../steering/mads.md) for details.
---
## Speed Limit Assist Restrictions
On **release branches**, Speed Limit Assist mode is disabled for Tesla vehicles. Info and Warning modes remain available.
See [Speed Limit Settings](../cruise/speed-limit/index.md) for details.

View File

@@ -0,0 +1,35 @@
---
title: Toyota / Lexus Settings
---
# Toyota / Lexus Vehicle Settings
Settings specific to Toyota and Lexus vehicles. These appear in the Vehicle panel when a Toyota or Lexus vehicle is connected or selected.
**Location**: `Settings -> Vehicle`
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Enforce Factory Longitudinal Control
When enabled, sunnypilot does not control gas or brakes. The factory Toyota/Lexus adaptive cruise control system handles all speed control. sunnypilot still provides steering assistance.
Enabling this will disable sunnypilot longitudinal control, disable Alpha Longitudinal if it was enabled, and force Stop and Go Hack off. A confirmation dialog appears before activation.
!!! note "Availability"
Cannot be changed while the system is engaged (actively driving).
---
## Stop and Go Hack (Alpha)
Allows some Toyota and Lexus vehicles to automatically resume from a full stop during stop-and-go traffic. Without this, you must press the resume button or tap the gas to resume.
!!! warning "Alpha Feature"
This is an alpha-quality feature. Use at your own risk.
!!! note "Availability"
Requires sunnypilot longitudinal control to be available and enabled. **Enforce Factory Longitudinal Control** must be off. Cannot be changed while engaged.

153
docs_sp/settings/visuals.md Normal file
View File

@@ -0,0 +1,153 @@
---
title: Visuals Settings
---
# Visuals Settings
Configure what information and visual elements appear on the driving screen.
**Location**: `Settings -> Visuals`
!!! info "Toggle & Device Availability"
This panel contains a mix of features. Some work on all devices, while others require onroad UI elements that only exist on comma 3X/3. Each item below includes its own availability note.
---
## Show Blind Spot Warnings
Displays visual warnings on the driving screen when a vehicle is detected in your blind spot. Only available on vehicles equipped with Blind Spot Monitoring (BSM) hardware.
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Steering Arc
Shows the steering arc overlay on the driving screen when lateral (steering) control is active. This arc illustrates the path sunnypilot is steering toward.
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting. Note: On comma four, the steering arc is always displayed and cannot be toggled off.
---
## Enable Tesla Rainbow Mode
Applies a rainbow color effect to the model's planned driving path displayed on screen. This is a cosmetic change only and does not affect driving behavior.
!!! info "Toggle & Device Availability"
Supported on all devices. comma four users must use sunnylink to change this setting.
---
## Enable Standstill Timer
Displays a timer on the HUD when the vehicle is stopped. The timer shows how long you have been at a standstill.
!!! info "Toggle & Device Availability"
Supported on: comma 3X/3 only. This feature is currently not available on comma four and cannot be forced via sunnylink.
---
## Display Road Name
Shows the name of the road you are currently driving on. Requires the OSM map database to be downloaded through the [OSM panel](osm.md).
!!! info "Toggle & Device Availability"
Supported on: comma 3X/3 only. This feature is currently not available on comma four and cannot be forced via sunnylink.
---
## Green Traffic Light Alert (Beta)
Plays a chime and shows an on-screen alert when the traffic light ahead turns green while you are stopped with no lead vehicle in front. Helps you notice the light change.
!!! info "Toggle & Device Availability"
Supported on: comma 3X/3 only. This feature is currently not available on comma four and cannot be forced via sunnylink.
---
## Lead Departure Alert (Beta)
Plays a chime and shows an on-screen alert when you are stopped and the vehicle ahead begins moving. Useful for noticing when traffic starts flowing again.
!!! info "Toggle & Device Availability"
Supported on: comma 3X/3 only. This feature is currently not available on comma four and cannot be forced via sunnylink.
---
## Speedometer: Always Display True Speed
Forces the speedometer to always show the true vehicle speed from wheel speed sensors, rather than the GPS-based speed. Applicable to vehicles where wheel speed and GPS speed differ.
!!! info "Toggle & Device Availability"
Supported on: comma 3X/3 only. This feature is currently not available on comma four and cannot be forced via sunnylink.
---
## Speedometer: Hide from Onroad Screen
Hides the speedometer from the driving screen entirely. When enabled, no speed is displayed while driving.
!!! info "Toggle & Device Availability"
Supported on: comma 3X/3 only. This feature is currently not available on comma four and cannot be forced via sunnylink.
---
## Display Turn Signals
Draws visual turn signal indicators on the HUD when the turn signals are active.
!!! info "Toggle & Device Availability"
Supported on: comma 3X/3 only. This feature is currently not available on comma four and cannot be forced via sunnylink. Note: On comma four, turn signal icons appear during lane change alerts only, but the always-on display does not exist.
---
## Real-time Acceleration Bar
Shows a vertical bar on the left side of the driving screen that indicates real-time acceleration and deceleration. The bar moves up during acceleration and down during braking.
!!! info "Toggle & Device Availability"
Supported on: comma 3X/3 only. This feature is currently not available on comma four and cannot be forced via sunnylink.
---
## Display Metrics Below Chevron
A row of five buttons that controls what information appears below the lead vehicle chevron on the driving screen:
| Option | What it shows |
|--------|--------------|
| **Off** | Nothing displayed below the chevron |
| **Distance** | Distance to the lead vehicle |
| **Speed** | Speed of the lead vehicle |
| **Time** | Time gap to the lead vehicle |
| **All** | Distance, speed, and time together |
!!! note "Availability"
Requires sunnypilot longitudinal control.
!!! info "Toggle & Device Availability"
Supported on: comma 3X/3 only. This feature is currently not available on comma four and cannot be forced via sunnylink.
---
## Developer UI
A row of four buttons that controls the display of real-time developer metrics on the driving screen:
| Option | What it shows |
|--------|--------------|
| **Off** | No developer information displayed |
| **Bottom** | Metrics displayed at the bottom of the screen |
| **Right** | Metrics displayed on the right side |
| **Right & Bottom** | Metrics displayed on both the right side and bottom |
!!! info "Toggle & Device Availability"
Supported on: comma 3X/3 only. This feature is currently not available on comma four and cannot be forced via sunnylink.
---
## Related Features
- [HUD & Visuals](../features/display/hud-visuals.md)

11
docs_sp/setup/index.md Normal file
View File

@@ -0,0 +1,11 @@
---
title: Installation
---
# Installation
Follow these guides to install sunnypilot on your comma device.
- **[Read Before Installing](read-before-installing.md)** - Prerequisites, warnings, and preparation steps
- **[URL Method](url-method.md)** - Install via the built-in URL installer (recommended)
- **[SSH Method](ssh-method.md)** - Install via SSH for advanced users

View File

@@ -0,0 +1,42 @@
---
title: Read Before Installing
---
# Read Before Installing
Before installing sunnypilot, please review the following important information.
## Prerequisites
- A supported comma device (comma 3X or compatible)
- A compatible vehicle harness for your car
- A stable internet connection for the initial setup
- Your vehicle must be in the supported list
## Important Notes
!!! warning
- sunnypilot is a **driver assistance** system — always pay attention to the road
- Ensure your device firmware is up to date before installing
- Installation will replace any existing openpilot installation on your device
## Choosing an Installation Method
| Method | Difficulty | Best For |
|--------|-----------|----------|
| [URL Method](url-method.md) | Easy | Most users |
| [SSH Method](ssh-method.md) | Advanced | Developers and power users |
## After Installation
1. The device will download and install the software
2. A reboot may be required
3. Follow the on-screen setup wizard
4. Take a test drive to calibrate the system
## Getting Help
If you run into issues:
- Visit the [sunnypilot community forum](https://community.sunnypilot.ai)
- Check [Reporting a Bug](../community/reporting-a-bug.md) for how to submit issues

View File

@@ -0,0 +1,35 @@
---
title: SSH Method
---
# SSH Method
The SSH method provides more control over the installation process. Recommended for advanced users.
## Prerequisites
- SSH access to your comma device
- Basic familiarity with the command line
## Steps
1. Connect to your device via SSH
2. Clone the sunnypilot repository:
```bash
cd /data
git clone https://github.com/sunnypilot/sunnypilot.git openpilot
```
3. Check out the desired branch:
```bash
cd openpilot
git checkout <branch-name>
```
4. Reboot the device
See [Branch Definitions](../references/branch-definitions.md) for available branches.
## Troubleshooting
- **SSH connection refused**: Ensure SSH is enabled in device settings
- **Git clone fails**: Check internet connectivity
- **Branch not found**: Verify the branch name in [Recommended Branches](../references/recommended-branches.md)

View File

@@ -0,0 +1,28 @@
---
title: URL Method
---
# URL Method
The URL method is the easiest way to install sunnypilot on your comma device.
## Steps
1. On your comma device, navigate to **Settings** > **Software**
2. Tap **Uninstall** if you have an existing installation
3. Enter the sunnypilot installation URL when prompted
4. Wait for the download and installation to complete
5. The device will reboot automatically
## Recommended Branches
See [Recommended Branches](../references/recommended-branches.md) for which branch to install based on your needs.
!!! tip
Most users should install the latest stable branch for the best experience.
## Troubleshooting
- **Download stalls**: Check your internet connection and try again
- **Device doesn't reboot**: Manually reboot by holding the power button
- **Installation fails**: Try the [SSH Method](ssh-method.md) as an alternative

View File

@@ -0,0 +1,17 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
/* Make the header logo larger */
.md-header__button.md-logo img {
height: 2.4rem;
width: auto;
}
/* Invert the white logo to dark on light mode */
[data-md-color-scheme="default"] .md-header__button.md-logo img {
filter: brightness(0);
}

View File

@@ -0,0 +1,128 @@
---
title: Hyundai Longitudinal Tuning
---
# Hyundai Longitudinal Tuning Implementation
!!! info "Audience"
This is advanced technical documentation intended for developers and contributors. End users do not need to understand these details to use sunnypilot.
## Overview
sunnypilot implements custom longitudinal (speed/acceleration) control tuning for Hyundai, Kia, and Genesis (HKG) vehicles. The goal is to produce smooth, comfortable acceleration and deceleration behavior that matches or exceeds the quality of the vehicle's stock adaptive cruise control system.
This tuning layer sits between the driving model's desired acceleration output and the actual commands sent to the vehicle, shaping the acceleration profile to meet comfort and safety constraints.
## Design Goals
The longitudinal tuning for HKG vehicles targets the following objectives:
- **Comfort**: Minimize abrupt speed changes that cause passenger discomfort
- **Smoothness**: Produce acceleration and deceleration curves that feel natural and predictable
- **Safety**: Maintain appropriate following distances and stopping margins
- **Consistency**: Deliver repeatable behavior across different driving conditions
- **Parity with stock**: Match or exceed the refinement of the factory ACC system
## Standards Reference
The tuning implementation references **ISO 15622:2018** (Intelligent Transport Systems — Adaptive Cruise Control Systems — Performance Requirements and Test Procedures). This standard defines:
- Maximum and minimum acceleration/deceleration rates for ACC systems
- Response time requirements for speed changes
- Following distance behavior and time-gap requirements
- Performance criteria for cut-in and cut-out scenarios
## Key Concepts
### Jerk Limiting
**Jerk** is the rate of change of acceleration (m/s³). High jerk values produce the "jerky" feeling passengers experience during abrupt speed changes.
The tuning system limits jerk to keep acceleration transitions smooth:
| Parameter | Value | Description |
|-----------|-------|-------------|
| **Minimum jerk** | 0.5 m/s³ | Floor value — jerk is never reduced below this to maintain responsiveness |
| **Default jerk limit** | 4.0 m/s³ | Standard upper bound for jerk across most driving scenarios |
- Acceleration onset is ramped gradually rather than applied instantly
- Deceleration transitions are similarly smoothed to avoid sudden braking sensations
- Different jerk limits apply to different driving scenarios (e.g., following vs. stopping)
#### Per-Vehicle Jerk Overrides
Some vehicle models have custom jerk limits tuned to their specific powertrain characteristics:
| Vehicle | Jerk Limit (m/s³) | Reason |
|---------|-------------------|--------|
| **Kia Niro EV** | 3.3 | EV powertrain delivers instant torque; lower jerk limit prevents harsh acceleration onset |
| **Kia Niro PHEV (2022+)** | 5.0 | Hybrid powertrain benefits from slightly higher jerk allowance for smoother transitions between electric and ICE modes |
#### Type-Specific Overrides
Jerk limits and acceleration profiles are also adjusted by vehicle communication type:
| Type | Adjustment |
|------|------------|
| **CAN-FD** | Tuning adapted for CAN-FD protocol timing and message rates |
| **EV** | Lower default jerk limits to account for instant torque delivery |
| **Hybrid** | Adjusted profiles to handle transitions between electric and combustion modes |
### Parabolic Approach
When decelerating to a stop or approaching a slower lead vehicle, the system uses **parabolic deceleration profiles** rather than constant-rate braking. This means:
- Deceleration starts gently and increases progressively
- As the vehicle nears the target speed or stop point, deceleration tapers off
- The result is a smooth, gradual stop rather than an abrupt one
- This mimics how experienced human drivers naturally brake
### Speed-Dependent Tuning
Longitudinal behavior is tuned differently across speed ranges:
| Speed Range | Tuning Focus |
|-------------|--------------|
| Low speed (stop-and-go) | Smooth stop/start transitions, creep management |
| City speeds | Responsive acceleration, comfortable following |
| Highway speeds | Gentle speed adjustments, fuel-efficient cruising |
Parameters such as acceleration limits, jerk bounds, and following distance gains are adjusted based on the current vehicle speed to optimize behavior for each regime.
### Lead Vehicle Response
The system adapts its behavior based on the lead vehicle's actions:
- **Lead accelerating**: Gradual acceleration to maintain gap without aggressive throttle
- **Lead decelerating**: Proportional braking response with jerk limiting
- **Lead cut-in**: Timely but smooth deceleration to establish safe following distance
- **Lead cut-out**: Controlled acceleration to resume set speed
## Panda Safety Limits
The panda enforces hard acceleration limits at the firmware level that cannot be overridden:
| Parameter | Limit |
|-----------|-------|
| **Maximum acceleration** | +2.0 m/s² |
| **Maximum deceleration** | -3.5 m/s² |
All tuning parameters must produce acceleration commands within these bounds. Any command exceeding these limits is clamped by the panda safety layer.
## Technical Parameters
The tuning system uses several categories of parameters:
- **Acceleration bounds**: Maximum and minimum acceleration values at different speeds
- **Jerk limits**: Rate-of-change constraints for both positive and negative acceleration (min: 0.5, default max: 4.0 m/s³)
- **Time constants**: Filtering and smoothing time constants for acceleration commands
- **Following distance gains**: Speed-dependent proportional and derivative gains for gap control
- **Stopping parameters**: Deceleration profiles and creep behavior for stop-and-go
!!! tip "Contributing"
If you are working on longitudinal tuning for HKG vehicles, test changes thoroughly across multiple driving scenarios — highway cruising, city stop-and-go, and lead vehicle cut-in/cut-out — before submitting a pull request.
## Related Pages
- [Cruise Control Settings](../settings/cruise/index.md) — User-facing cruise control configuration
- [Hyundai Vehicle Settings](../settings/vehicle/hyundai.md) — Hyundai-specific settings reference

View File

@@ -0,0 +1,11 @@
---
title: Technical Reference
---
# Technical Reference
In-depth technical documentation, tuning guides, and branch information.
- **[Hyundai Longitudinal Tuning](hyundai-longitudinal-tuning.md)** - Detailed longitudinal control tuning for Hyundai vehicles
- **[Recommended Branches](../references/recommended-branches.md)** - Which branches to use for your setup
- **[Branch Definitions](../references/branch-definitions.md)** - Explanation of branch naming and release channels

View File

@@ -0,0 +1,58 @@
"""SHA-256 content cache for skipping unchanged docs on re-sync.
Stores one .sha256 file per doc in .discourse_sync_cache/. On re-run,
compares the current file hash against the cached hash to determine
whether the doc needs syncing.
"""
from __future__ import annotations
import hashlib
from pathlib import Path
DEFAULT_CACHE_DIR = Path(__file__).resolve().parent.parent.parent / ".discourse_sync_cache"
class ContentCache:
"""File-based SHA-256 content cache."""
def __init__(self, cache_dir: str | Path = DEFAULT_CACHE_DIR) -> None:
self._cache_dir = Path(cache_dir)
@property
def cache_dir(self) -> Path:
return self._cache_dir
@staticmethod
def compute_hash(content: str) -> str:
"""Compute SHA-256 hex digest of content."""
return hashlib.sha256(content.encode("utf-8")).hexdigest()
def _cache_path(self, doc_path: str) -> Path:
"""Map a doc path (e.g. 'features/cruise/icbm.md') to its cache file."""
slug = doc_path.replace("/", "_").replace(".md", "")
return self._cache_dir / f"{slug}.sha256"
def is_changed(self, doc_path: str, content: str) -> bool:
"""Return True if content differs from the cached hash (or no cache exists)."""
content_hash = self.compute_hash(content)
cached = self._cache_path(doc_path)
if not cached.exists():
return True
return cached.read_text().strip() != content_hash
def save(self, doc_path: str, content: str) -> None:
"""Save the SHA-256 hash of content to the cache file."""
content_hash = self.compute_hash(content)
self._cache_dir.mkdir(parents=True, exist_ok=True)
self._cache_path(doc_path).write_text(content_hash + "\n")
def clear(self) -> int:
"""Remove all cached hashes. Returns the number of files removed."""
if not self._cache_dir.exists():
return 0
count = 0
for f in self._cache_dir.glob("*.sha256"):
f.unlink()
count += 1
return count

272
docs_sp/tools/converter.py Normal file
View File

@@ -0,0 +1,272 @@
"""Zensical -> Discourse-compatible Markdown converter.
Converts Zensical/MkDocs Material syntax to Discourse-friendly markdown:
1. Strip YAML front matter
2. Convert admonitions (!!!/???/???+) to Discourse callouts (> [!TYPE])
3. Convert Material tabs (=== "Tab Name") to bold headings + ---
4. Strip grid card HTML (<div class="grid cards" markdown>)
5. Convert Material emoji shortcodes (:material-*:) to Unicode or strip
6. Resolve internal .md links to Discourse search URLs (via docs-sync-id)
7. Clean up excessive blank lines
"""
from __future__ import annotations
import os
import re
import urllib.parse
from pathlib import Path
DISCOURSE_FORUM_URL = os.environ.get(
"DISCOURSE_URL", "https://community.sunnypilot.ai"
).rstrip("/")
ADMONITION_MAP: dict[str, str] = {
"note": "NOTE",
"abstract": "ABSTRACT",
"info": "INFO",
"tip": "TIP",
"success": "SUCCESS",
"question": "QUESTION",
"warning": "WARNING",
"failure": "FAILURE",
"danger": "DANGER",
"bug": "BUG",
"example": "EXAMPLE",
"quote": "QUOTE",
}
EMOJI_MAP: dict[str, str] = {
":material-rocket-launch:": "",
":material-cog:": "",
":material-car:": "",
":material-shield:": "",
":material-download:": "",
":material-check:": "Y",
":material-close:": "N",
":material-alert:": "!",
":material-information:": "i",
":material-help-circle:": "?",
":material-star:": "*",
":material-link:": "",
":material-eye:": "",
":material-map:": "",
":material-wifi:": "",
":material-cellphone:": "",
":material-steering:": "",
":material-speedometer:": "",
}
_ADMONITION_RE = re.compile(r"^(\s*)(!{3}|\?{3}\+?)\s+(\w+)(?:\s+\"([^\"]*)\")?\s*$")
_TAB_RE = re.compile(r'^(\s*)===\s+"([^"]+)"\s*$')
_FRONT_MATTER_RE = re.compile(r"\A---\n.*?\n---\n*", re.DOTALL)
_GRID_CARD_OPEN_RE = re.compile(r'<div\s+class="grid\s+cards"\s*(?:markdown)?\s*>')
_GRID_CARD_CLOSE_RE = re.compile(r"</div>")
_INTERNAL_LINK_RE = re.compile(r"\]\(([^)]+\.md(?:#[^)]*)?)\)")
_EMOJI_SHORTCODE_RE = re.compile(r":material-[\w-]+:")
_EXCESSIVE_BLANKS_RE = re.compile(r"\n{4,}")
def convert(content: str, *, file_path: str) -> str:
"""Convert Zensical markdown to Discourse-compatible markdown.
Args:
content: Raw markdown content.
file_path: Path to the source file relative to docs_sp/
(e.g. "getting-started/what-is-sunnypilot.md").
Used for resolving relative internal links.
Returns:
Converted markdown string.
"""
result = content
result = strip_front_matter(result)
result = convert_admonitions(result)
result = convert_tabs(result)
result = convert_grid_cards(result)
result = convert_emoji_shortcodes(result)
result = resolve_internal_links(result, file_path=file_path)
result = clean_blank_lines(result)
return result.strip() + "\n"
def strip_front_matter(content: str) -> str:
"""Remove YAML front matter (--- ... ---) from the start of content."""
return _FRONT_MATTER_RE.sub("", content)
def convert_admonitions(content: str) -> str:
"""Convert MkDocs admonitions to Discourse/Obsidian callouts.
Input:
!!! warning "Title"
Content line 1
Content line 2
Output:
> [!WARNING] Title
> Content line 1
> Content line 2
"""
lines = content.splitlines(keepends=True)
result: list[str] = []
i = 0
while i < len(lines):
line = lines[i]
m = _ADMONITION_RE.match(line)
if m:
indent = m.group(1)
marker = m.group(2)
ad_type = m.group(3).lower()
title = m.group(4)
callout_type = ADMONITION_MAP.get(ad_type, ad_type.upper())
header = f"{indent}> [!{callout_type}]"
if title:
header += f" {title}"
if marker.startswith("???"):
collapsed = "+" not in marker
if not title:
action = "expand" if collapsed else "collapse"
header += f" *(click to {action})*"
result.append(header + "\n")
i += 1
content_indent = indent + " "
while i < len(lines):
content_line = lines[i]
if content_line.startswith(content_indent):
stripped = content_line[len(content_indent):]
result.append(f"{indent}> {stripped}")
i += 1
elif content_line.strip() == "":
# Check if blank line is internal to the admonition
j = i + 1
while j < len(lines) and lines[j].strip() == "":
j += 1
if j < len(lines) and lines[j].startswith(content_indent):
result.append(f"{indent}>\n")
i += 1
else:
break
else:
break
else:
result.append(line)
i += 1
return "".join(result)
def convert_tabs(content: str) -> str:
"""Convert Material tabs to bold headings with horizontal rules.
Input:
=== "Tab Name"
Content
Output:
**Tab Name**
Content
---
"""
lines = content.splitlines(keepends=True)
result: list[str] = []
i = 0
while i < len(lines):
line = lines[i]
m = _TAB_RE.match(line)
if m:
indent = m.group(1)
tab_name = m.group(2)
result.append(f"{indent}**{tab_name}**\n")
result.append("\n")
i += 1
content_indent = indent + " "
while i < len(lines):
content_line = lines[i]
if content_line.startswith(content_indent) or content_line.strip() == "":
if content_line.strip() == "":
result.append("\n")
else:
stripped = content_line[len(content_indent):]
result.append(f"{indent}{stripped}")
i += 1
else:
break
# Ensure blank line before the horizontal rule
if result and result[-1] != "\n":
result.append("\n")
result.append(f"{indent}---\n")
result.append("\n")
else:
result.append(line)
i += 1
return "".join(result)
def convert_grid_cards(content: str) -> str:
"""Strip grid card HTML wrappers (Discourse doesn't support them)."""
result = _GRID_CARD_OPEN_RE.sub("", content)
result = _GRID_CARD_CLOSE_RE.sub("", result)
return result
def convert_emoji_shortcodes(content: str) -> str:
"""Convert Material emoji shortcodes to plain text equivalents."""
result = content
for shortcode, replacement in EMOJI_MAP.items():
result = result.replace(shortcode, replacement)
# Strip any remaining :material-*: shortcodes not in the map
result = _EMOJI_SHORTCODE_RE.sub("", result)
return result
def resolve_internal_links(content: str, *, file_path: str) -> str:
"""Resolve internal .md links to Discourse search URLs via docs-sync-id.
Converts: [text](../features/icbm.md) ->
[text](https://community.sunnypilot.ai/search?q=%22docs-sync-id%3A+features/icbm.md%22)
Anchors are stripped since Discourse search cannot target sections.
"""
current_dir = str(Path(file_path).parent)
def _replace_link(match: re.Match[str]) -> str:
raw_path = match.group(1)
# Skip external URLs
if raw_path.startswith("http"):
return match.group(0)
# Strip anchor (Discourse search cannot target sections)
if "#" in raw_path:
raw_path = raw_path.rsplit("#", 1)[0]
# Resolve relative path from current file's directory
resolved = os.path.normpath(os.path.join(current_dir, raw_path))
# Build Discourse search URL for the sync ID
query = urllib.parse.quote(f'"docs-sync-id: {resolved}"', safe="")
return f"]({DISCOURSE_FORUM_URL}/search?q={query})"
return _INTERNAL_LINK_RE.sub(_replace_link, content)
def clean_blank_lines(content: str) -> str:
"""Collapse 4+ consecutive blank lines down to 3."""
return _EXCESSIVE_BLANKS_RE.sub("\n\n\n", content)

View File

@@ -0,0 +1,233 @@
"""Minimal Discourse API client using only urllib (zero external deps).
Provides the 4 CRUD operations needed by the docs sync orchestrator:
1. find_topic_by_sync_id(sync_id)
2. create_topic(title, raw, category_id, tags)
3. update_post(post_id, raw, edit_reason)
4. first_post_id(topic_id)
Configuration via environment variables:
DISCOURSE_URL - Base URL (e.g. https://community.sunnypilot.ai)
DISCOURSE_API_KEY - API key with topic create/update permissions
DISCOURSE_API_USER - Username for API requests (default: "system")
DISCOURSE_CATEGORY_MAP - JSON mapping of doc section to Discourse category ID
e.g. '{"getting-started": 115, "features": 116}'
Falls back to parent category 114 for unmapped sections.
"""
from __future__ import annotations
import json
import os
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass, field
from typing import Any
DEFAULT_PARENT_CATEGORY_ID = 114
DEFAULT_CATEGORY_MAP: dict[str, int] = {}
@dataclass(frozen=True)
class DiscourseConfig:
"""Immutable configuration for the Discourse API client."""
base_url: str
api_key: str
api_user: str = "system"
category_mapping: dict[str, int] = field(default_factory=lambda: dict(DEFAULT_CATEGORY_MAP))
def category_id_for(self, doc_path: str) -> int:
"""Return the Discourse category ID for a given doc path.
Looks up the top-level folder (e.g. "getting-started" from
"getting-started/what-is-sunnypilot.md") in category_mapping.
Falls back to the parent Documentation category (114).
"""
section = doc_path.split("/")[0] if "/" in doc_path else doc_path
return self.category_mapping.get(section, DEFAULT_PARENT_CATEGORY_ID)
@classmethod
def from_env(cls) -> DiscourseConfig:
"""Build config from environment variables.
Raises:
ValueError: If required env vars are missing or map is invalid JSON.
"""
base_url = os.environ.get("DISCOURSE_URL", "")
api_key = os.environ.get("DISCOURSE_API_KEY", "")
if not base_url:
raise ValueError("DISCOURSE_URL environment variable is required")
if not api_key:
raise ValueError("DISCOURSE_API_KEY environment variable is required")
category_map_str = os.environ.get("DISCOURSE_CATEGORY_MAP", "")
if category_map_str:
try:
raw_map = json.loads(category_map_str)
except json.JSONDecodeError as e:
raise ValueError(f"DISCOURSE_CATEGORY_MAP must be valid JSON: {e}")
if not isinstance(raw_map, dict):
raise ValueError("DISCOURSE_CATEGORY_MAP must be a JSON object")
category_mapping = {str(k): int(v) for k, v in raw_map.items()}
else:
category_mapping = dict(DEFAULT_CATEGORY_MAP)
return cls(
base_url=base_url.rstrip("/"),
api_key=api_key,
api_user=os.environ.get("DISCOURSE_API_USER", "system"),
category_mapping=category_mapping,
)
class DiscourseClient:
"""Discourse API client for docs sync operations."""
def __init__(self, config: DiscourseConfig) -> None:
self._config = config
@property
def config(self) -> DiscourseConfig:
return self._config
# ----- Public API -----
def find_topic_by_sync_id(self, sync_id: str) -> dict[str, Any] | None:
"""Find an existing topic by its embedded sync ID marker.
Searches for topics containing the visible sync ID text
``docs-sync-id: {sync_id}``. The marker must be visible (not an
HTML comment) so that Discourse indexes it.
Args:
sync_id: The doc path used as sync identifier.
Returns:
Topic dict with at least 'id' key, or None if not found.
"""
query = f'"docs-sync-id: {sync_id}"'
encoded = urllib.parse.urlencode({"q": query})
data = self._get(f"/search.json?{encoded}")
if data is None:
return None
topics = data.get("topics", [])
return topics[0] if topics else None
def create_topic(
self,
title: str,
raw: str,
category_id: int,
tags: list[str] | None = None,
) -> dict[str, Any] | None:
"""Create a new topic in the specified category.
Args:
title: Topic title.
raw: Markdown body content.
category_id: Discourse category ID.
tags: Optional list of tags.
Returns:
Response dict with 'topic_id', 'id' (post ID), etc., or None on failure.
"""
payload: dict[str, Any] = {
"title": title,
"raw": raw,
"category": category_id,
}
if tags:
payload["tags"] = tags
return self._post("/posts.json", payload)
def update_post(
self,
post_id: int,
raw: str,
edit_reason: str = "Documentation sync",
) -> dict[str, Any] | None:
"""Update an existing post's content.
Args:
post_id: The Discourse post ID to update.
raw: New markdown body content.
edit_reason: Reason shown in edit history.
Returns:
Response dict, or None on failure.
"""
payload = {
"post": {
"raw": raw,
"edit_reason": edit_reason,
},
}
return self._put(f"/posts/{post_id}.json", payload)
def first_post_id(self, topic_id: int) -> int | None:
"""Get the first post ID of a topic.
Args:
topic_id: The Discourse topic ID.
Returns:
Post ID of the first post, or None if not found.
"""
data = self._get(f"/t/{topic_id}.json")
if data is None:
return None
posts = data.get("post_stream", {}).get("posts", [])
if not posts:
return None
return posts[0].get("id")
# ----- HTTP helpers -----
def _headers(self) -> dict[str, str]:
return {
"Content-Type": "application/json",
"Api-Key": self._config.api_key,
"Api-Username": self._config.api_user,
"User-Agent": "Mozilla/5.0 (compatible; sunnypilot-docs-sync/1.0)",
}
def _get(self, path: str) -> dict[str, Any] | None:
url = self._config.base_url + path
req = urllib.request.Request(url, headers=self._headers(), method="GET")
return self._send(req)
def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any] | None:
url = self._config.base_url + path
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=self._headers(), method="POST")
return self._send(req)
def _put(self, path: str, payload: dict[str, Any]) -> dict[str, Any] | None:
url = self._config.base_url + path
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=self._headers(), method="PUT")
return self._send(req)
def _send(self, req: urllib.request.Request) -> dict[str, Any] | None:
try:
with urllib.request.urlopen(req) as resp:
body = resp.read().decode("utf-8")
return json.loads(body) if body else {}
except urllib.error.HTTPError as e:
# Log but don't crash — caller decides how to handle None
status = e.code
body = ""
try:
body = e.read().decode("utf-8", errors="replace")[:500]
except Exception:
pass
print(f" Discourse API error: {req.method} {req.full_url} -> {status}: {body}")
return None
except urllib.error.URLError as e:
print(f" Discourse connection error: {req.full_url} -> {e.reason}")
return None

View File

@@ -0,0 +1,88 @@
"""Parse zensical.toml nav structure into a flat list of doc entries.
Reads the nav tree from zensical.toml and produces a flat list of
{title, path, breadcrumb} dicts suitable for the sync orchestrator.
"""
from __future__ import annotations
import tomllib
from dataclasses import dataclass, field
from pathlib import Path
SKIP_FILES = frozenset({"index.md", "README.md"})
@dataclass(frozen=True)
class NavEntry:
"""A single navigable documentation page."""
title: str
path: str
breadcrumb: tuple[str, ...] = field(default_factory=tuple)
def parse(config_path: str | Path) -> list[NavEntry]:
"""Parse zensical.toml and return all nav entries.
Args:
config_path: Path to zensical.toml.
Returns:
Flat list of NavEntry for every page in the nav tree,
excluding index.md, README.md, and external links.
"""
config_path = Path(config_path)
with config_path.open("rb") as f:
config = tomllib.load(f)
nav = config.get("project", {}).get("nav", [])
entries = _flatten_nav(nav)
return [e for e in entries if Path(e.path).name not in SKIP_FILES]
def parse_all(config_path: str | Path) -> list[NavEntry]:
"""Like parse(), but includes index.md and README.md entries."""
config_path = Path(config_path)
with config_path.open("rb") as f:
config = tomllib.load(f)
nav = config.get("project", {}).get("nav", [])
return _flatten_nav(nav)
def _flatten_nav(
items: list[dict[str, str | list] | str],
breadcrumb: tuple[str, ...] = (),
) -> list[NavEntry]:
"""Recursively flatten the nav tree into NavEntry objects."""
result: list[NavEntry] = []
for item in items:
if isinstance(item, dict):
for title, value in item.items():
if isinstance(value, str):
# Skip external links
if value.startswith("http"):
continue
result.append(NavEntry(
title=title,
path=value,
breadcrumb=breadcrumb + (title,),
))
elif isinstance(value, list):
result.extend(
_flatten_nav(value, breadcrumb + (title,))
)
elif isinstance(item, str):
# Bare path without title
if item.startswith("http"):
continue
name = Path(item).stem.replace("-", " ").capitalize()
result.append(NavEntry(
title=name,
path=item,
breadcrumb=breadcrumb + (name,),
))
return result

View File

@@ -0,0 +1,612 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# sync_docs_discourse.rb
#
# Syncs sunnypilot documentation from docs_sp/ to a Discourse forum.
# Reads raw .md files, converts MkDocs Material syntax to Discourse-compatible
# markdown (Obsidian-style callouts), resolves internal links to Discourse URLs,
# and creates or updates topics via the Discourse API.
#
# Usage:
# ruby sync_docs_discourse.rb [--dry-run] [--verbose]
#
# Environment variables (required unless --dry-run):
# DISCOURSE_URL - Base URL of the Discourse instance (e.g. https://forum.sunnypilot.ai)
# DISCOURSE_API_KEY - API key with topic create/update permissions
# DISCOURSE_API_USER - Username for API requests (e.g. system or a bot account)
# DISCOURSE_CATEGORY - Category slug or ID for documentation topics (default: "documentation")
#
# Optional:
# DOCS_BASE_URL - Base URL for the static docs site (default: https://docs.sunnypilot.ai)
require "net/http"
require "uri"
require "json"
require "yaml"
require "digest"
require "fileutils"
require "optparse"
require "toml-rb"
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
DOCS_DIR = File.expand_path("../../", __FILE__) # docs_sp/
MKDOCS_YML = File.expand_path("../../../zensical.toml", __FILE__)
CACHE_DIR = File.expand_path("../../../.discourse_sync_cache", __FILE__)
DOCS_BASE_URL = ENV.fetch("DOCS_BASE_URL", "https://docs.sunnypilot.ai")
DISCOURSE_URL = ENV["DISCOURSE_URL"]
DISCOURSE_API_KEY = ENV["DISCOURSE_API_KEY"]
DISCOURSE_API_USER = ENV.fetch("DISCOURSE_API_USER", "system")
DISCOURSE_CATEGORY = ENV.fetch("DISCOURSE_CATEGORY", "documentation")
# MkDocs admonition type → Obsidian/Discourse callout type
ADMONITION_MAP = {
"note" => "NOTE",
"abstract" => "ABSTRACT",
"info" => "INFO",
"tip" => "TIP",
"success" => "SUCCESS",
"question" => "QUESTION",
"warning" => "WARNING",
"failure" => "FAILURE",
"danger" => "DANGER",
"bug" => "BUG",
"example" => "EXAMPLE",
"quote" => "QUOTE",
}.freeze
# Pages to skip (not meaningful as standalone Discourse topics)
SKIP_FILES = %w[
index.md
README.md
].freeze
# ---------------------------------------------------------------------------
# CLI Options
# ---------------------------------------------------------------------------
options = { dry_run: false, verbose: false }
OptionParser.new do |opts|
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
opts.on("--dry-run", "Show what would be synced without making API calls") { options[:dry_run] = true }
opts.on("--verbose", "Print detailed conversion output") { options[:verbose] = true }
opts.on("-h", "--help", "Show this help") { puts opts; exit }
end.parse!
DRY_RUN = options[:dry_run]
VERBOSE = options[:verbose]
unless DRY_RUN
%w[DISCOURSE_URL DISCOURSE_API_KEY].each do |var|
abort "Error: #{var} environment variable is required" unless ENV[var]
end
end
# ---------------------------------------------------------------------------
# MkDocs → Discourse Markdown Converter
# ---------------------------------------------------------------------------
module MkDocsConverter
module_function
# Main entry point: convert a full MkDocs Material markdown string
# to Discourse-compatible markdown.
def convert(content, file_path:, nav_slug_map: {})
result = content.dup
# 1. Strip YAML front matter
result = strip_front_matter(result)
# 2. Convert admonitions (!!! type "title" / ??? type "title")
result = convert_admonitions(result)
# 3. Convert Material tabs (=== "Tab Name")
result = convert_tabs(result)
# 4. Convert grid cards
result = convert_grid_cards(result)
# 5. Convert Material emoji shortcodes to Unicode or strip
result = convert_emoji_shortcodes(result)
# 6. Resolve internal .md links to docs site URLs
result = resolve_internal_links(result, file_path: file_path)
# 7. Clean up excessive blank lines
result = result.gsub(/\n{4,}/, "\n\n\n")
result.strip + "\n"
end
# Remove YAML front matter (--- ... ---)
def strip_front_matter(content)
content.sub(/\A---\n.*?\n---\n*/m, "")
end
# Convert MkDocs admonitions to Obsidian/Discourse callouts.
#
# Input:
# !!! warning "Title"
# Content line 1
# Content line 2
#
# Output:
# > [!WARNING] Title
# > Content line 1
# > Content line 2
#
# Also handles collapsible (??? / ???+) variants.
def convert_admonitions(content)
lines = content.lines
result = []
i = 0
while i < lines.length
line = lines[i]
# Match admonition opener: !!! type "title" or ??? type "title" or ???+ type "title"
if line =~ /^(\s*)(\!{3}|\?{3}\+?) (\w+)(?: "([^"]*)")?/
indent = $1
marker = $2
ad_type = $3.downcase
title = $4
callout_type = ADMONITION_MAP[ad_type] || ad_type.upcase
# Build the callout header
header = "#{indent}> [!#{callout_type}]"
header += " #{title}" if title && !title.empty?
# For collapsible (???), add a note
if marker.start_with?("???")
collapsed = !marker.include?("+")
header += " *(click to #{collapsed ? 'expand' : 'collapse'})*" if title.nil? || title.empty?
end
result << header + "\n"
i += 1
# Collect indented content lines (4 spaces deeper than the opener)
content_indent = indent + " "
while i < lines.length
content_line = lines[i]
if content_line =~ /^#{Regexp.escape(content_indent)}/
# Indented content line — part of the admonition
stripped = content_line.sub(/^#{Regexp.escape(content_indent)}/, "")
result << "#{indent}> #{stripped}"
i += 1
elsif content_line.strip.empty?
# Blank line: only part of admonition if the next non-blank
# line is still indented at content level
j = i + 1
j += 1 while j < lines.length && lines[j].strip.empty?
if j < lines.length && lines[j] =~ /^#{Regexp.escape(content_indent)}/
result << "#{indent}>\n"
i += 1
else
# Blank line ends the admonition
break
end
else
break
end
end
else
result << line
i += 1
end
end
result.join
end
# Convert Material tabs to Discourse-friendly headings with horizontal rules.
#
# Input:
# === "Tab Name"
# Content
#
# Output:
# **Tab Name**
#
# Content
#
# ---
def convert_tabs(content)
lines = content.lines
result = []
i = 0
while i < lines.length
line = lines[i]
if line =~ /^(\s*)=== "([^"]+)"/
indent = $1
tab_name = $2
result << "#{indent}**#{tab_name}**\n"
result << "\n"
i += 1
# Collect indented content
content_indent = indent + " "
while i < lines.length
content_line = lines[i]
if content_line =~ /^#{Regexp.escape(content_indent)}/ || content_line.strip.empty?
if content_line.strip.empty?
result << "\n"
else
stripped = content_line.sub(/^#{Regexp.escape(content_indent)}/, "")
result << "#{indent}#{stripped}"
end
i += 1
else
break
end
end
result << "#{indent}---\n"
result << "\n"
else
result << line
i += 1
end
end
result.join
end
# Convert grid cards to simple lists (Discourse doesn't support grid cards)
def convert_grid_cards(content)
content
.gsub(/<div class="grid cards" markdown>/, "")
.gsub(/<\/div>/, "")
end
# Convert Material emoji shortcodes to plain text or remove
# e.g., :material-rocket-launch: → 🚀 (or just strip)
EMOJI_MAP = {
":material-rocket-launch:" => "🚀",
":material-cog:" => "⚙️",
":material-car:" => "🚗",
":material-shield:" => "🛡️",
}.freeze
def convert_emoji_shortcodes(content)
result = content.dup
EMOJI_MAP.each do |shortcode, emoji|
result.gsub!(shortcode, emoji)
end
# Strip any remaining :material-*: shortcodes
result.gsub(/:material-[\w-]+:/, "")
end
# Resolve internal links: (../features/icbm.md) → (https://docs.sunnypilot.ai/features/icbm/)
def resolve_internal_links(content, file_path:)
content.gsub(/\]\(([^)]+\.md)\)/) do |match|
relative_path = $1
# Skip external URLs
next match if relative_path.start_with?("http")
# Resolve the relative path from the current file's directory
current_dir = File.dirname(file_path)
resolved = File.expand_path(relative_path, current_dir)
# Make it relative to docs_sp/
docs_relative = resolved.sub(%r{^.*/docs_sp/}, "")
# Convert to URL path: remove .md, add trailing slash
url_path = docs_relative.sub(/\.md$/, "/")
"](#{DOCS_BASE_URL}/#{url_path})"
end
end
end
# ---------------------------------------------------------------------------
# Discourse API Client
# ---------------------------------------------------------------------------
module DiscourseAPI
module_function
def base_uri
URI.parse(DISCOURSE_URL)
end
def headers
{
"Content-Type" => "application/json",
"Api-Key" => DISCOURSE_API_KEY,
"Api-Username" => DISCOURSE_API_USER,
}
end
# Look up category ID by slug
def category_id(slug)
uri = URI.join(DISCOURSE_URL, "/c/#{slug}/show.json")
response = http_get(uri)
return nil unless response.is_a?(Net::HTTPSuccess)
data = JSON.parse(response.body)
data.dig("category", "id")
end
# Search for an existing topic by external_id (stored in the topic's first post)
# We use a convention: embed an HTML comment <!-- docs-sync-id: path/to/file.md -->
def find_topic_by_sync_id(sync_id)
search_query = "<!-- docs-sync-id: #{sync_id} -->"
uri = URI.join(DISCOURSE_URL, "/search.json?q=#{URI.encode_www_form_component(search_query)}")
response = http_get(uri)
return nil unless response.is_a?(Net::HTTPSuccess)
data = JSON.parse(response.body)
topics = data.dig("topics") || []
topics.first
end
# Create a new topic
def create_topic(title:, raw:, category_id:, tags: [])
uri = URI.join(DISCOURSE_URL, "/posts.json")
payload = {
title: title,
raw: raw,
category: category_id,
tags: tags,
}
http_post(uri, payload)
end
# Update an existing topic's first post
def update_post(post_id:, raw:, edit_reason: "Documentation sync")
uri = URI.join(DISCOURSE_URL, "/posts/#{post_id}.json")
payload = {
post: {
raw: raw,
edit_reason: edit_reason,
},
}
http_put(uri, payload)
end
# Get a topic's first post ID
def first_post_id(topic_id)
uri = URI.join(DISCOURSE_URL, "/t/#{topic_id}.json")
response = http_get(uri)
return nil unless response.is_a?(Net::HTTPSuccess)
data = JSON.parse(response.body)
data.dig("post_stream", "posts", 0, "id")
end
# --- HTTP helpers ---
def http_get(uri)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == "https"
request = Net::HTTP::Get.new(uri, headers)
http.request(request)
end
def http_post(uri, payload)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == "https"
request = Net::HTTP::Post.new(uri, headers)
request.body = payload.to_json
http.request(request)
end
def http_put(uri, payload)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == "https"
request = Net::HTTP::Put.new(uri, headers)
request.body = payload.to_json
http.request(request)
end
end
# ---------------------------------------------------------------------------
# Nav Parser — extract title + path from zensical.toml nav
# ---------------------------------------------------------------------------
module NavParser
module_function
# Parse the zensical.toml nav structure into a flat list of { title:, path: }
def parse(config_path)
config = TomlRB.load_file(config_path)
nav = config.dig("project", "nav") || []
flatten_nav(nav)
end
def flatten_nav(items, prefix_parts = [])
result = []
items.each do |item|
case item
when Hash
item.each do |key, value|
case value
when String
# Skip external links
next if value.start_with?("http")
result << { title: key, path: value, breadcrumb: prefix_parts + [key] }
when Array
result.concat(flatten_nav(value, prefix_parts + [key]))
end
end
when String
# Bare path without title (unlikely in our nav)
result << { title: File.basename(item, ".md").tr("-", " ").capitalize, path: item }
end
end
result
end
end
# ---------------------------------------------------------------------------
# Content Cache — skip unchanged files
# ---------------------------------------------------------------------------
module ContentCache
module_function
def cache_path(file_path)
slug = file_path.gsub("/", "_").gsub(".md", "")
File.join(CACHE_DIR, "#{slug}.sha256")
end
def changed?(file_path, content_hash)
cached = cache_path(file_path)
return true unless File.exist?(cached)
File.read(cached).strip != content_hash
end
def save(file_path, content_hash)
FileUtils.mkdir_p(CACHE_DIR)
File.write(cache_path(file_path), content_hash)
end
end
# ---------------------------------------------------------------------------
# Main Sync Logic
# ---------------------------------------------------------------------------
def sync_doc(entry, category_id)
file_path = File.join(DOCS_DIR, entry[:path])
unless File.exist?(file_path)
puts " ⚠ File not found: #{file_path}"
return :skipped
end
raw_content = File.read(file_path, encoding: "utf-8")
content_hash = Digest::SHA256.hexdigest(raw_content)
# Skip unchanged files
unless ContentCache.changed?(entry[:path], content_hash)
puts " ✓ Unchanged: #{entry[:path]}" if VERBOSE
return :unchanged
end
# Convert MkDocs → Discourse markdown
converted = MkDocsConverter.convert(raw_content, file_path: file_path)
# Prepend breadcrumb navigation
if entry[:breadcrumb] && entry[:breadcrumb].length > 1
breadcrumb = entry[:breadcrumb][0..-2].join(" ")
converted = "*#{breadcrumb}*\n\n#{converted}"
end
# Append sync ID comment and docs site link
docs_url = "#{DOCS_BASE_URL}/#{entry[:path].sub(/\.md$/, '/')}"
converted += "\n\n---\n"
converted += "*This documentation is automatically synced from the [sunnypilot docs site](#{docs_url}).*\n"
converted += "<!-- docs-sync-id: #{entry[:path]} -->\n"
title = entry[:title]
if DRY_RUN
puts " → Would sync: \"#{title}\" (#{entry[:path]})"
if VERBOSE
puts " --- Converted content (first 500 chars) ---"
puts " #{converted[0..500].gsub("\n", "\n ")}"
puts " ---"
end
ContentCache.save(entry[:path], content_hash) # Cache even in dry-run for testing
return :would_sync
end
# Check if topic already exists
existing = DiscourseAPI.find_topic_by_sync_id(entry[:path])
if existing
# Update existing topic
post_id = DiscourseAPI.first_post_id(existing["id"])
if post_id
response = DiscourseAPI.update_post(post_id: post_id, raw: converted)
if response.is_a?(Net::HTTPSuccess)
puts " ✓ Updated: \"#{title}\" (topic ##{existing['id']})"
ContentCache.save(entry[:path], content_hash)
return :updated
else
puts " ✗ Failed to update: #{response.code} #{response.body[0..200]}"
return :error
end
else
puts " ✗ Could not find first post for topic ##{existing['id']}"
return :error
end
else
# Create new topic
response = DiscourseAPI.create_topic(
title: "#{title} — sunnypilot Docs",
raw: converted,
category_id: category_id,
tags: ["docs", "auto-sync"]
)
if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
data = JSON.parse(response.body)
puts " ✓ Created: \"#{title}\" (topic ##{data['topic_id']})"
ContentCache.save(entry[:path], content_hash)
return :created
else
puts " ✗ Failed to create: #{response.code} #{response.body[0..200]}"
return :error
end
end
end
# ---------------------------------------------------------------------------
# Entry Point
# ---------------------------------------------------------------------------
def main
puts "sunnypilot Documentation → Discourse Sync"
puts "=" * 50
puts "Mode: #{DRY_RUN ? 'DRY RUN' : 'LIVE'}"
puts "Docs dir: #{DOCS_DIR}"
puts "Discourse: #{DISCOURSE_URL || '(dry-run, no URL)'}"
puts
# Parse nav entries
nav_entries = NavParser.parse(MKDOCS_YML)
puts "Found #{nav_entries.length} nav entries"
# Filter out skipped files
nav_entries.reject! { |e| SKIP_FILES.include?(File.basename(e[:path])) }
puts "After filtering: #{nav_entries.length} pages to sync"
puts
# Resolve category ID
category_id = nil
unless DRY_RUN
category_id = DiscourseAPI.category_id(DISCOURSE_CATEGORY)
abort "Error: Could not find category '#{DISCOURSE_CATEGORY}'" unless category_id
puts "Category: #{DISCOURSE_CATEGORY} (ID: #{category_id})"
puts
end
# Sync each page
stats = { created: 0, updated: 0, unchanged: 0, skipped: 0, error: 0, would_sync: 0 }
nav_entries.each_with_index do |entry, idx|
# Rate limiting: 1 request per second for live mode
sleep(1) if !DRY_RUN && idx > 0
result = sync_doc(entry, category_id)
stats[result] = (stats[result] || 0) + 1
end
# Summary
puts
puts "=" * 50
puts "Sync complete!"
stats.each do |status, count|
next if count == 0
puts " #{status}: #{count}"
end
end
main

View File

@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Tests for the SHA-256 content cache.
Run: python3 docs_sp/tools/test_content_cache.py
"""
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from content_cache import ContentCache
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def make_cache(tmp: str) -> ContentCache:
return ContentCache(cache_dir=Path(tmp) / ".discourse_sync_cache")
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
def test_compute_hash_deterministic():
"""Same content always produces the same hash."""
h1 = ContentCache.compute_hash("hello world")
h2 = ContentCache.compute_hash("hello world")
assert h1 == h2
assert len(h1) == 64 # SHA-256 hex digest length
print(" PASS: compute_hash_deterministic")
def test_compute_hash_differs():
"""Different content produces different hashes."""
h1 = ContentCache.compute_hash("hello world")
h2 = ContentCache.compute_hash("hello world!")
assert h1 != h2
print(" PASS: compute_hash_differs")
def test_is_changed_no_cache():
"""First run (no cache file) should report changed."""
with tempfile.TemporaryDirectory() as tmp:
cache = make_cache(tmp)
assert cache.is_changed("features/icbm.md", "content")
print(" PASS: is_changed_no_cache")
def test_is_changed_after_save():
"""After saving, same content should report unchanged."""
with tempfile.TemporaryDirectory() as tmp:
cache = make_cache(tmp)
cache.save("features/icbm.md", "content v1")
assert not cache.is_changed("features/icbm.md", "content v1")
print(" PASS: is_changed_after_save")
def test_is_changed_after_modification():
"""After saving, different content should report changed."""
with tempfile.TemporaryDirectory() as tmp:
cache = make_cache(tmp)
cache.save("features/icbm.md", "content v1")
assert cache.is_changed("features/icbm.md", "content v2")
print(" PASS: is_changed_after_modification")
def test_separate_paths_independent():
"""Different doc paths have independent caches."""
with tempfile.TemporaryDirectory() as tmp:
cache = make_cache(tmp)
cache.save("features/icbm.md", "content A")
cache.save("safety/safety.md", "content B")
assert not cache.is_changed("features/icbm.md", "content A")
assert not cache.is_changed("safety/safety.md", "content B")
assert cache.is_changed("features/icbm.md", "content B")
assert cache.is_changed("safety/safety.md", "content A")
print(" PASS: separate_paths_independent")
def test_cache_dir_created_on_save():
"""Cache directory is created automatically on first save."""
with tempfile.TemporaryDirectory() as tmp:
cache_dir = Path(tmp) / "nested" / "cache"
cache = ContentCache(cache_dir=cache_dir)
assert not cache_dir.exists()
cache.save("test.md", "content")
assert cache_dir.exists()
assert (cache_dir / "test.sha256").exists()
print(" PASS: cache_dir_created_on_save")
def test_cache_file_naming():
"""Cache files use slug derived from doc path."""
with tempfile.TemporaryDirectory() as tmp:
cache = make_cache(tmp)
cache.save("settings/cruise/speed-limit/source.md", "content")
expected_name = "settings_cruise_speed-limit_source.sha256"
cached_files = list(cache.cache_dir.glob("*.sha256"))
assert len(cached_files) == 1
assert cached_files[0].name == expected_name
print(" PASS: cache_file_naming")
def test_clear_removes_all():
"""clear() removes all .sha256 files and returns count."""
with tempfile.TemporaryDirectory() as tmp:
cache = make_cache(tmp)
cache.save("a.md", "aaa")
cache.save("b.md", "bbb")
cache.save("c.md", "ccc")
removed = cache.clear()
assert removed == 3
assert list(cache.cache_dir.glob("*.sha256")) == []
print(" PASS: clear_removes_all")
def test_clear_empty_cache():
"""clear() on nonexistent cache dir returns 0."""
with tempfile.TemporaryDirectory() as tmp:
cache = make_cache(tmp)
assert cache.clear() == 0
print(" PASS: clear_empty_cache")
def test_overwrite_on_resave():
"""Saving again overwrites the previous hash."""
with tempfile.TemporaryDirectory() as tmp:
cache = make_cache(tmp)
cache.save("doc.md", "version 1")
assert not cache.is_changed("doc.md", "version 1")
assert cache.is_changed("doc.md", "version 2")
cache.save("doc.md", "version 2")
assert cache.is_changed("doc.md", "version 1")
assert not cache.is_changed("doc.md", "version 2")
print(" PASS: overwrite_on_resave")
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
if __name__ == "__main__":
print("Testing content cache:")
tests = [
test_compute_hash_deterministic,
test_compute_hash_differs,
test_is_changed_no_cache,
test_is_changed_after_save,
test_is_changed_after_modification,
test_separate_paths_independent,
test_cache_dir_created_on_save,
test_cache_file_naming,
test_clear_removes_all,
test_clear_empty_cache,
test_overwrite_on_resave,
]
passed = 0
failed = 0
for test in tests:
try:
test()
passed += 1
except AssertionError as e:
print(f" FAIL: {test.__name__}: {e}")
failed += 1
except Exception as e:
print(f" ERROR: {test.__name__}: {e}")
failed += 1
print(f"\n{passed}/{passed + failed} tests passed")
sys.exit(1 if failed > 0 else 0)

View File

@@ -0,0 +1,502 @@
#!/usr/bin/env python3
"""Tests for the MkDocs -> Discourse markdown converter.
Run: python3 docs_sp/tools/test_converter.py
"""
import sys
from pathlib import Path
# Ensure the tools directory is importable
sys.path.insert(0, str(Path(__file__).parent))
from converter import (
clean_blank_lines,
convert,
convert_admonitions,
convert_emoji_shortcodes,
convert_grid_cards,
convert_tabs,
resolve_internal_links,
strip_front_matter,
)
# ---------------------------------------------------------------------------
# 1. Strip YAML Front Matter
# ---------------------------------------------------------------------------
def test_strip_front_matter_basic():
input_text = """---
title: My Page
description: A test page
---
# Hello
"""
expected = """# Hello
"""
result = strip_front_matter(input_text)
assert result == expected, f"FAIL:\n{result!r}\n!=\n{expected!r}"
print(" PASS: strip_front_matter_basic")
def test_strip_front_matter_absent():
input_text = "# Hello\n\nContent here.\n"
result = strip_front_matter(input_text)
assert result == input_text, f"FAIL:\n{result!r}\n!=\n{input_text!r}"
print(" PASS: strip_front_matter_absent")
# ---------------------------------------------------------------------------
# 2. Convert Admonitions
# ---------------------------------------------------------------------------
def test_basic_warning():
input_text = """!!! warning "Important"
sunnypilot is a **driver assistance** system.
Always pay attention.
"""
expected = """> [!WARNING] Important
> sunnypilot is a **driver assistance** system.
> Always pay attention.
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL basic_warning:\n{result!r}\n!=\n{expected!r}"
print(" PASS: basic_warning")
def test_info_no_title():
input_text = """!!! info
Content line 1
Content line 2
"""
expected = """> [!INFO]
> Content line 1
> Content line 2
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL info_no_title:\n{result!r}\n!=\n{expected!r}"
print(" PASS: info_no_title")
def test_info_with_title():
input_text = """!!! info "Requirements"
- Longitudinal control must be available
- ICBM must be enabled
"""
expected = """> [!INFO] Requirements
> - Longitudinal control must be available
> - ICBM must be enabled
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL info_with_title:\n{result!r}\n!=\n{expected!r}"
print(" PASS: info_with_title")
def test_danger():
input_text = """!!! danger "Important"
sunnypilot is a **driver assistance** system. It is **NOT** a self-driving system.
"""
expected = """> [!DANGER] Important
> sunnypilot is a **driver assistance** system. It is **NOT** a self-driving system.
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL danger:\n{result!r}\n!=\n{expected!r}"
print(" PASS: danger")
def test_tip():
input_text = """!!! tip
The more detail you provide, the faster we can diagnose and fix the issue.
"""
expected = """> [!TIP]
> The more detail you provide, the faster we can diagnose and fix the issue.
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL tip:\n{result!r}\n!=\n{expected!r}"
print(" PASS: tip")
def test_multiline_with_blank():
input_text = """!!! warning
Line 1
Line 2 after blank
"""
expected = """> [!WARNING]
> Line 1
>
> Line 2 after blank
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL multiline_with_blank:\n{result!r}\n!=\n{expected!r}"
print(" PASS: multiline_with_blank")
def test_collapsible():
input_text = """??? warning "Click to see"
Hidden content
"""
expected = """> [!WARNING] Click to see
> Hidden content
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL collapsible:\n{result!r}\n!=\n{expected!r}"
print(" PASS: collapsible")
def test_collapsible_open():
input_text = """???+ info "Open by default"
Visible content
"""
expected = """> [!INFO] Open by default
> Visible content
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL collapsible_open:\n{result!r}\n!=\n{expected!r}"
print(" PASS: collapsible_open")
def test_surrounded_by_content():
input_text = """Some text before.
!!! note "Note Title"
Note content here.
Some text after.
"""
expected = """Some text before.
> [!NOTE] Note Title
> Note content here.
Some text after.
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL surrounded:\n{result!r}\n!=\n{expected!r}"
print(" PASS: surrounded_by_content")
def test_multiple_admonitions():
input_text = """!!! info "Requirements"
- Req 1
- Req 2
!!! warning "Vehicle Restrictions"
- Tesla: disabled on release
- Rivian: always disabled
"""
expected = """> [!INFO] Requirements
> - Req 1
> - Req 2
> [!WARNING] Vehicle Restrictions
> - Tesla: disabled on release
> - Rivian: always disabled
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL multiple:\n{result!r}\n!=\n{expected!r}"
print(" PASS: multiple_admonitions")
def test_real_doc_snippet():
"""Test with an actual snippet from docs_sp content."""
input_text = """## Speed Limit Mode
| Property | Value |
|----------|-------|
| **Param** | `SpeedLimitMode` |
| **Type** | Multi-button selector |
!!! info "Requirements"
- Longitudinal control must be available, **or** ICBM must be enabled
!!! warning "Vehicle Restrictions"
- **Tesla:** Speed Limit Assist mode is disabled on release branches
- **Rivian:** Speed Limit Assist mode is always disabled
---
"""
expected = """## Speed Limit Mode
| Property | Value |
|----------|-------|
| **Param** | `SpeedLimitMode` |
| **Type** | Multi-button selector |
> [!INFO] Requirements
> - Longitudinal control must be available, **or** ICBM must be enabled
> [!WARNING] Vehicle Restrictions
> - **Tesla:** Speed Limit Assist mode is disabled on release branches
> - **Rivian:** Speed Limit Assist mode is always disabled
---
"""
result = convert_admonitions(input_text)
assert result == expected, f"FAIL real_doc:\n{result!r}\n!=\n{expected!r}"
print(" PASS: real_doc_snippet")
# ---------------------------------------------------------------------------
# 3. Convert Tabs
# ---------------------------------------------------------------------------
def test_tabs_basic():
input_text = """=== "Tab One"
Content for tab one.
=== "Tab Two"
Content for tab two.
"""
expected = """**Tab One**
Content for tab one.
---
**Tab Two**
Content for tab two.
---
"""
result = convert_tabs(input_text)
assert result == expected, f"FAIL tabs_basic:\n{result!r}\n!=\n{expected!r}"
print(" PASS: tabs_basic")
def test_tabs_multiline():
input_text = """=== "Details"
Line 1
Line 2
Line 3
"""
expected = """**Details**
Line 1
Line 2
Line 3
---
"""
result = convert_tabs(input_text)
assert result == expected, f"FAIL tabs_multiline:\n{result!r}\n!=\n{expected!r}"
print(" PASS: tabs_multiline")
# ---------------------------------------------------------------------------
# 4. Convert Grid Cards
# ---------------------------------------------------------------------------
def test_grid_cards_stripped():
input_text = """<div class="grid cards" markdown>
- **Card 1** - Description
- **Card 2** - Description
</div>
"""
expected = """
- **Card 1** - Description
- **Card 2** - Description
"""
result = convert_grid_cards(input_text)
# Normalize whitespace for comparison
assert result.strip() == expected.strip(), f"FAIL grid_cards:\n{result!r}\n!=\n{expected!r}"
print(" PASS: grid_cards_stripped")
# ---------------------------------------------------------------------------
# 5. Convert Emoji Shortcodes
# ---------------------------------------------------------------------------
def test_emoji_known():
input_text = ":material-check: Supported | :material-close: Not supported"
expected = "Y Supported | N Not supported"
result = convert_emoji_shortcodes(input_text)
assert result == expected, f"FAIL emoji_known:\n{result!r}\n!=\n{expected!r}"
print(" PASS: emoji_known")
def test_emoji_unknown_stripped():
input_text = ":material-unknown-icon: Some text"
expected = " Some text"
result = convert_emoji_shortcodes(input_text)
assert result == expected, f"FAIL emoji_unknown:\n{result!r}\n!=\n{expected!r}"
print(" PASS: emoji_unknown_stripped")
def test_emoji_in_grid_card():
input_text = "- :material-rocket-launch: **[Feature](link.md)**"
expected = "- **[Feature](link.md)**"
result = convert_emoji_shortcodes(input_text)
assert result == expected, f"FAIL emoji_grid:\n{result!r}\n!=\n{expected!r}"
print(" PASS: emoji_in_grid_card")
# ---------------------------------------------------------------------------
# 6. Resolve Internal Links
# ---------------------------------------------------------------------------
def test_internal_link_relative():
input_text = "See [ICBM](../features/cruise/icbm.md) for details."
result = resolve_internal_links(
input_text,
file_path="settings/cruise/speed-limit.md",
)
assert "/search?q=" in result, f"FAIL internal_link missing search URL:\n{result!r}"
assert "docs-sync-id" in result, f"FAIL internal_link missing sync-id:\n{result!r}"
assert "features%2Fcruise%2Ficbm.md" in result or "features/cruise/icbm.md" in result, (
f"FAIL internal_link wrong path:\n{result!r}"
)
print(" PASS: internal_link_relative")
def test_internal_link_with_anchor():
input_text = "See [section](./safety.md#driver-responsibility)."
result = resolve_internal_links(
input_text,
file_path="safety/index.md",
)
# Anchors are stripped — Discourse search cannot target sections
assert "#driver-responsibility" not in result, f"FAIL anchor should be stripped:\n{result!r}"
assert "docs-sync-id" in result, f"FAIL missing sync-id:\n{result!r}"
assert "safety" in result, f"FAIL wrong path:\n{result!r}"
print(" PASS: internal_link_with_anchor")
def test_external_link_untouched():
input_text = "Visit [GitHub](https://github.com/sunnypilot/sunnypilot)."
result = resolve_internal_links(
input_text,
file_path="index.md",
)
assert result == input_text, f"FAIL external_link:\n{result!r}"
print(" PASS: external_link_untouched")
# ---------------------------------------------------------------------------
# 7. Clean Blank Lines
# ---------------------------------------------------------------------------
def test_clean_blank_lines():
input_text = "Line 1\n\n\n\n\nLine 2\n"
expected = "Line 1\n\n\nLine 2\n"
result = clean_blank_lines(input_text)
assert result == expected, f"FAIL clean_blanks:\n{result!r}\n!=\n{expected!r}"
print(" PASS: clean_blank_lines")
def test_clean_blank_lines_no_change():
input_text = "Line 1\n\nLine 2\n"
result = clean_blank_lines(input_text)
assert result == input_text, f"FAIL clean_blanks_noop:\n{result!r}"
print(" PASS: clean_blank_lines_no_change")
# ---------------------------------------------------------------------------
# Integration: full convert()
# ---------------------------------------------------------------------------
def test_full_convert():
input_text = """---
title: Test Doc
---
# Test Document
!!! warning "Important"
Pay attention to the road.
See [safety info](../safety/safety.md) for more.
:material-check: Feature supported
"""
result = convert(
input_text,
file_path="features/index.md",
)
# Front matter stripped
assert "---\ntitle:" not in result, f"FAIL front matter not stripped:\n{result!r}"
# Admonition converted
assert "> [!WARNING] Important" in result, f"FAIL admonition:\n{result!r}"
assert "> Pay attention to the road." in result, f"FAIL admonition content:\n{result!r}"
# Link resolved to Discourse search
assert "/search?q=" in result, f"FAIL link not converted to search:\n{result!r}"
assert "docs-sync-id" in result, f"FAIL link missing sync-id:\n{result!r}"
# Emoji converted
assert ":material-check:" not in result, f"FAIL emoji:\n{result!r}"
assert "Y Feature supported" in result, f"FAIL emoji replacement:\n{result!r}"
print(" PASS: full_convert")
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
if __name__ == "__main__":
print("Testing MkDocs -> Discourse converter:")
tests = [
# 1. Front matter
test_strip_front_matter_basic,
test_strip_front_matter_absent,
# 2. Admonitions
test_basic_warning,
test_info_no_title,
test_info_with_title,
test_danger,
test_tip,
test_multiline_with_blank,
test_collapsible,
test_collapsible_open,
test_surrounded_by_content,
test_multiple_admonitions,
test_real_doc_snippet,
# 3. Tabs
test_tabs_basic,
test_tabs_multiline,
# 4. Grid cards
test_grid_cards_stripped,
# 5. Emoji
test_emoji_known,
test_emoji_unknown_stripped,
test_emoji_in_grid_card,
# 6. Internal links
test_internal_link_relative,
test_internal_link_with_anchor,
test_external_link_untouched,
# 7. Blank lines
test_clean_blank_lines,
test_clean_blank_lines_no_change,
# Integration
test_full_convert,
]
passed = 0
failed = 0
for test in tests:
try:
test()
passed += 1
except AssertionError as e:
print(f" FAIL: {test.__name__}: {e}")
failed += 1
except Exception as e:
print(f" ERROR: {test.__name__}: {e}")
failed += 1
print(f"\n{passed}/{passed + failed} tests passed")
sys.exit(1 if failed > 0 else 0)

View File

@@ -0,0 +1,436 @@
#!/usr/bin/env python3
"""Tests for the Discourse API client (fully mocked, no live requests).
Run: python3 docs_sp/tools/test_discourse_client.py
"""
import io
import json
import sys
import urllib.error
import urllib.request
from pathlib import Path
from unittest.mock import MagicMock, patch
sys.path.insert(0, str(Path(__file__).parent))
from discourse_client import DiscourseClient, DiscourseConfig
TEST_CONFIG = DiscourseConfig(
base_url="https://community.sunnypilot.ai",
api_key="test-api-key-123",
api_user="docs-bot",
category_mapping={"getting-started": 115, "features": 116},
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def mock_response(data: dict, status: int = 200) -> MagicMock:
"""Create a mock urllib response with JSON body."""
body = json.dumps(data).encode("utf-8")
resp = MagicMock()
resp.read.return_value = body
resp.status = status
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
return resp
def mock_http_error(status: int, body: str = "") -> urllib.error.HTTPError:
"""Create a mock HTTPError."""
return urllib.error.HTTPError(
url="https://community.sunnypilot.ai/test",
code=status,
msg=f"HTTP {status}",
hdrs={}, # type: ignore[arg-type]
fp=io.BytesIO(body.encode("utf-8")),
)
# ---------------------------------------------------------------------------
# DiscourseConfig tests
# ---------------------------------------------------------------------------
def test_config_from_env():
env = {
"DISCOURSE_URL": "https://forum.example.com/",
"DISCOURSE_API_KEY": "secret-key",
"DISCOURSE_API_USER": "bot",
"DISCOURSE_CATEGORY_MAP": '{"getting-started": 115, "features": 116}',
}
with patch.dict("os.environ", env, clear=False):
config = DiscourseConfig.from_env()
assert config.base_url == "https://forum.example.com" # trailing slash stripped
assert config.api_key == "secret-key"
assert config.api_user == "bot"
assert config.category_mapping == {"getting-started": 115, "features": 116}
print(" PASS: config_from_env")
def test_config_from_env_defaults():
env = {
"DISCOURSE_URL": "https://forum.example.com",
"DISCOURSE_API_KEY": "key",
}
with patch.dict("os.environ", env, clear=True):
config = DiscourseConfig.from_env()
assert config.api_user == "system"
assert config.category_mapping == {}
print(" PASS: config_from_env_defaults")
def test_config_invalid_category_map_json():
env = {
"DISCOURSE_URL": "https://forum.example.com",
"DISCOURSE_API_KEY": "key",
"DISCOURSE_CATEGORY_MAP": "not-valid-json",
}
with patch.dict("os.environ", env, clear=True):
try:
DiscourseConfig.from_env()
assert False, "Should have raised ValueError"
except ValueError as e:
assert "DISCOURSE_CATEGORY_MAP" in str(e)
print(" PASS: config_invalid_category_map_json")
def test_config_invalid_category_map_type():
env = {
"DISCOURSE_URL": "https://forum.example.com",
"DISCOURSE_API_KEY": "key",
"DISCOURSE_CATEGORY_MAP": "[1, 2, 3]",
}
with patch.dict("os.environ", env, clear=True):
try:
DiscourseConfig.from_env()
assert False, "Should have raised ValueError"
except ValueError as e:
assert "DISCOURSE_CATEGORY_MAP" in str(e)
print(" PASS: config_invalid_category_map_type")
def test_category_id_for_mapped():
assert TEST_CONFIG.category_id_for("getting-started/what-is-sunnypilot.md") == 115
assert TEST_CONFIG.category_id_for("features/icbm.md") == 116
print(" PASS: category_id_for_mapped")
def test_category_id_for_unmapped():
assert TEST_CONFIG.category_id_for("unknown-section/doc.md") == 114
print(" PASS: category_id_for_unmapped")
def test_config_missing_url():
env = {"DISCOURSE_API_KEY": "key"}
with patch.dict("os.environ", env, clear=True):
try:
DiscourseConfig.from_env()
assert False, "Should have raised ValueError"
except ValueError as e:
assert "DISCOURSE_URL" in str(e)
print(" PASS: config_missing_url")
def test_config_missing_api_key():
env = {"DISCOURSE_URL": "https://forum.example.com"}
with patch.dict("os.environ", env, clear=True):
try:
DiscourseConfig.from_env()
assert False, "Should have raised ValueError"
except ValueError as e:
assert "DISCOURSE_API_KEY" in str(e)
print(" PASS: config_missing_api_key")
def test_config_immutable():
try:
TEST_CONFIG.base_url = "https://other.com" # type: ignore[misc]
assert False, "Should have raised"
except AttributeError:
pass
print(" PASS: config_immutable")
# ---------------------------------------------------------------------------
# find_topic_by_sync_id
# ---------------------------------------------------------------------------
@patch("urllib.request.urlopen")
def test_find_topic_found(mock_urlopen: MagicMock):
mock_urlopen.return_value = mock_response({
"topics": [{"id": 101, "title": "ICBM Docs"}],
})
client = DiscourseClient(TEST_CONFIG)
result = client.find_topic_by_sync_id("features/cruise/icbm.md")
assert result is not None
assert result["id"] == 101
call_args = mock_urlopen.call_args[0][0]
assert "/search.json?" in call_args.full_url
assert "docs-sync-id" in call_args.full_url
print(" PASS: find_topic_found")
@patch("urllib.request.urlopen")
def test_find_topic_not_found(mock_urlopen: MagicMock):
mock_urlopen.return_value = mock_response({"topics": []})
client = DiscourseClient(TEST_CONFIG)
result = client.find_topic_by_sync_id("nonexistent.md")
assert result is None
print(" PASS: find_topic_not_found")
@patch("urllib.request.urlopen")
def test_find_topic_api_error(mock_urlopen: MagicMock):
mock_urlopen.side_effect = mock_http_error(500, "Internal Server Error")
client = DiscourseClient(TEST_CONFIG)
result = client.find_topic_by_sync_id("features/icbm.md")
assert result is None
print(" PASS: find_topic_api_error")
# ---------------------------------------------------------------------------
# create_topic
# ---------------------------------------------------------------------------
@patch("urllib.request.urlopen")
def test_create_topic_success(mock_urlopen: MagicMock):
mock_urlopen.return_value = mock_response({
"id": 501,
"topic_id": 201,
"topic_slug": "icbm-sunnypilot-docs",
})
client = DiscourseClient(TEST_CONFIG)
result = client.create_topic(
title="ICBM - sunnypilot Docs",
raw="# ICBM\n\nDoc content here.",
category_id=42,
tags=["docs", "auto-sync"],
)
assert result is not None
assert result["topic_id"] == 201
call_args = mock_urlopen.call_args[0][0]
assert call_args.method == "POST"
assert "/posts.json" in call_args.full_url
sent_payload = json.loads(call_args.data.decode("utf-8"))
assert sent_payload["title"] == "ICBM - sunnypilot Docs"
assert sent_payload["category"] == 42
assert sent_payload["tags"] == ["docs", "auto-sync"]
assert "ICBM" in sent_payload["raw"]
print(" PASS: create_topic_success")
@patch("urllib.request.urlopen")
def test_create_topic_no_tags(mock_urlopen: MagicMock):
mock_urlopen.return_value = mock_response({"id": 502, "topic_id": 202})
client = DiscourseClient(TEST_CONFIG)
client.create_topic(title="Test", raw="body", category_id=1)
sent_payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8"))
assert "tags" not in sent_payload
print(" PASS: create_topic_no_tags")
@patch("urllib.request.urlopen")
def test_create_topic_failure(mock_urlopen: MagicMock):
mock_urlopen.side_effect = mock_http_error(422, '{"errors":["Title too short"]}')
client = DiscourseClient(TEST_CONFIG)
result = client.create_topic(title="X", raw="body", category_id=1)
assert result is None
print(" PASS: create_topic_failure")
# ---------------------------------------------------------------------------
# update_post
# ---------------------------------------------------------------------------
@patch("urllib.request.urlopen")
def test_update_post_success(mock_urlopen: MagicMock):
mock_urlopen.return_value = mock_response({"post": {"id": 501, "version": 2}})
client = DiscourseClient(TEST_CONFIG)
result = client.update_post(post_id=501, raw="Updated content")
assert result is not None
call_args = mock_urlopen.call_args[0][0]
assert call_args.method == "PUT"
assert "/posts/501.json" in call_args.full_url
sent_payload = json.loads(call_args.data.decode("utf-8"))
assert sent_payload["post"]["raw"] == "Updated content"
assert sent_payload["post"]["edit_reason"] == "Documentation sync"
print(" PASS: update_post_success")
@patch("urllib.request.urlopen")
def test_update_post_custom_reason(mock_urlopen: MagicMock):
mock_urlopen.return_value = mock_response({"post": {"id": 501}})
client = DiscourseClient(TEST_CONFIG)
client.update_post(post_id=501, raw="content", edit_reason="Manual fix")
sent_payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8"))
assert sent_payload["post"]["edit_reason"] == "Manual fix"
print(" PASS: update_post_custom_reason")
@patch("urllib.request.urlopen")
def test_update_post_not_found(mock_urlopen: MagicMock):
mock_urlopen.side_effect = mock_http_error(404)
client = DiscourseClient(TEST_CONFIG)
result = client.update_post(post_id=99999, raw="content")
assert result is None
print(" PASS: update_post_not_found")
# ---------------------------------------------------------------------------
# first_post_id
# ---------------------------------------------------------------------------
@patch("urllib.request.urlopen")
def test_first_post_id_found(mock_urlopen: MagicMock):
mock_urlopen.return_value = mock_response({
"post_stream": {
"posts": [
{"id": 501, "post_number": 1},
{"id": 502, "post_number": 2},
],
},
})
client = DiscourseClient(TEST_CONFIG)
result = client.first_post_id(topic_id=201)
assert result == 501
print(" PASS: first_post_id_found")
@patch("urllib.request.urlopen")
def test_first_post_id_empty_stream(mock_urlopen: MagicMock):
mock_urlopen.return_value = mock_response({"post_stream": {"posts": []}})
client = DiscourseClient(TEST_CONFIG)
result = client.first_post_id(topic_id=201)
assert result is None
print(" PASS: first_post_id_empty_stream")
@patch("urllib.request.urlopen")
def test_first_post_id_topic_not_found(mock_urlopen: MagicMock):
mock_urlopen.side_effect = mock_http_error(404)
client = DiscourseClient(TEST_CONFIG)
result = client.first_post_id(topic_id=99999)
assert result is None
print(" PASS: first_post_id_topic_not_found")
# ---------------------------------------------------------------------------
# Headers / auth
# ---------------------------------------------------------------------------
@patch("urllib.request.urlopen")
def test_headers_set_correctly(mock_urlopen: MagicMock):
mock_urlopen.return_value = mock_response({"topics": []})
client = DiscourseClient(TEST_CONFIG)
client.find_topic_by_sync_id("test.md")
req = mock_urlopen.call_args[0][0]
assert req.get_header("Api-key") == "test-api-key-123"
assert req.get_header("Api-username") == "docs-bot"
assert req.get_header("Content-type") == "application/json"
assert "Mozilla" in req.get_header("User-agent")
print(" PASS: headers_set_correctly")
# ---------------------------------------------------------------------------
# Connection error
# ---------------------------------------------------------------------------
@patch("urllib.request.urlopen")
def test_connection_error_returns_none(mock_urlopen: MagicMock):
mock_urlopen.side_effect = urllib.error.URLError("Connection refused")
client = DiscourseClient(TEST_CONFIG)
result = client.find_topic_by_sync_id("test.md")
assert result is None
print(" PASS: connection_error_returns_none")
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
import os # noqa: E402 (needed for test_config_from_env_defaults)
if __name__ == "__main__":
print("Testing Discourse API client:")
tests = [
# Config
test_config_from_env,
test_config_from_env_defaults,
test_config_invalid_category_map_json,
test_config_invalid_category_map_type,
test_config_missing_url,
test_config_missing_api_key,
test_config_immutable,
test_category_id_for_mapped,
test_category_id_for_unmapped,
# find_topic_by_sync_id
test_find_topic_found,
test_find_topic_not_found,
test_find_topic_api_error,
# create_topic
test_create_topic_success,
test_create_topic_no_tags,
test_create_topic_failure,
# update_post
test_update_post_success,
test_update_post_custom_reason,
test_update_post_not_found,
# first_post_id
test_first_post_id_found,
test_first_post_id_empty_stream,
test_first_post_id_topic_not_found,
# Misc
test_headers_set_correctly,
test_connection_error_returns_none,
]
passed = 0
failed = 0
for test in tests:
try:
test()
passed += 1
except AssertionError as e:
print(f" FAIL: {test.__name__}: {e}")
failed += 1
except Exception as e:
print(f" ERROR: {test.__name__}: {e}")
failed += 1
print(f"\n{passed}/{passed + failed} tests passed")
sys.exit(1 if failed > 0 else 0)

View File

@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""Tests for the zensical.toml nav parser.
Run: python3 docs_sp/tools/test_nav_parser.py
"""
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from nav_parser import NavEntry, parse, parse_all, _flatten_nav
ZENSICAL_TOML = Path(__file__).resolve().parent.parent.parent / "zensical.toml"
# ---------------------------------------------------------------------------
# Unit tests: _flatten_nav
# ---------------------------------------------------------------------------
def test_flatten_simple_dict():
nav = [{"Page One": "page-one.md"}]
result = _flatten_nav(nav)
assert len(result) == 1
assert result[0] == NavEntry(title="Page One", path="page-one.md", breadcrumb=("Page One",))
print(" PASS: flatten_simple_dict")
def test_flatten_nested():
nav = [
{"Section": [
{"Child A": "section/a.md"},
{"Child B": "section/b.md"},
]},
]
result = _flatten_nav(nav)
assert len(result) == 2
assert result[0].title == "Child A"
assert result[0].path == "section/a.md"
assert result[0].breadcrumb == ("Section", "Child A")
assert result[1].breadcrumb == ("Section", "Child B")
print(" PASS: flatten_nested")
def test_flatten_deep_nesting():
nav = [
{"L1": [
{"L2": [
{"L3": "deep/page.md"},
]},
]},
]
result = _flatten_nav(nav)
assert len(result) == 1
assert result[0].breadcrumb == ("L1", "L2", "L3")
assert result[0].path == "deep/page.md"
print(" PASS: flatten_deep_nesting")
def test_flatten_skips_external_links():
nav = [
{"Docs": "docs.md"},
{"Forum": "https://community.sunnypilot.ai"},
]
result = _flatten_nav(nav)
assert len(result) == 1
assert result[0].title == "Docs"
print(" PASS: flatten_skips_external_links")
def test_flatten_bare_string():
nav = ["getting-started/index.md"]
result = _flatten_nav(nav)
assert len(result) == 1
assert result[0].title == "Index"
assert result[0].path == "getting-started/index.md"
print(" PASS: flatten_bare_string")
def test_flatten_mixed():
nav = [
{"Home": "index.md"},
{"Features": [
"features/index.md",
{"ICBM": "features/cruise/icbm.md"},
{"Forum": "https://example.com"},
]},
]
result = _flatten_nav(nav)
assert len(result) == 3 # Home, features/index.md (bare), ICBM
titles = [e.title for e in result]
assert "Home" in titles
assert "ICBM" in titles
assert "Forum" not in titles
print(" PASS: flatten_mixed")
# ---------------------------------------------------------------------------
# Unit tests: parse (filters index.md/README.md)
# ---------------------------------------------------------------------------
def test_parse_filters_index():
"""parse() should exclude index.md and README.md entries."""
toml_content = b"""
[project]
nav = [
{"Home" = "index.md"},
{"Guide" = [
"guide/index.md",
{"Setup" = "guide/setup.md"},
]},
]
"""
with tempfile.NamedTemporaryFile(suffix=".toml", delete=False) as f:
f.write(toml_content)
f.flush()
result = parse(f.name)
assert len(result) == 1
assert result[0].title == "Setup"
assert result[0].path == "guide/setup.md"
print(" PASS: parse_filters_index")
def test_parse_all_includes_index():
"""parse_all() should include index.md entries."""
toml_content = b"""
[project]
nav = [
{"Home" = "index.md"},
{"Setup" = "guide/setup.md"},
]
"""
with tempfile.NamedTemporaryFile(suffix=".toml", delete=False) as f:
f.write(toml_content)
f.flush()
result = parse_all(f.name)
assert len(result) == 2
titles = [e.title for e in result]
assert "Home" in titles
assert "Setup" in titles
print(" PASS: parse_all_includes_index")
# ---------------------------------------------------------------------------
# Integration: parse real zensical.toml
# ---------------------------------------------------------------------------
def test_parse_real_zensical():
"""Parse the actual zensical.toml and verify structure."""
if not ZENSICAL_TOML.exists():
print(" SKIP: parse_real_zensical (zensical.toml not found)")
return
all_entries = parse_all(ZENSICAL_TOML)
filtered_entries = parse(ZENSICAL_TOML)
# Sanity checks on totals
assert len(all_entries) > 50, f"Expected 50+ total entries, got {len(all_entries)}"
assert len(filtered_entries) > 40, f"Expected 40+ filtered entries, got {len(filtered_entries)}"
assert len(filtered_entries) < len(all_entries), "Filtering should remove some entries"
# Every filtered entry should NOT be index.md or README.md
for entry in filtered_entries:
assert Path(entry.path).name not in ("index.md", "README.md"), (
f"Filtered list contains {entry.path}"
)
# No external links should be present
for entry in all_entries:
assert not entry.path.startswith("http"), f"External link leaked: {entry.path}"
# Check some known entries exist
paths = {e.path for e in filtered_entries}
assert "getting-started/what-is-sunnypilot.md" in paths, "Missing what-is-sunnypilot"
assert "features/cruise/icbm.md" in paths, "Missing ICBM"
assert "safety/safety.md" in paths, "Missing safety"
# Check breadcrumbs are populated
icbm = next(e for e in filtered_entries if e.path == "features/cruise/icbm.md")
assert len(icbm.breadcrumb) >= 2, f"ICBM breadcrumb too short: {icbm.breadcrumb}"
assert "Features" in icbm.breadcrumb or "Cruise Control" in icbm.breadcrumb
print(f" PASS: parse_real_zensical ({len(all_entries)} total, {len(filtered_entries)} filtered)")
def test_nav_entry_immutable():
"""NavEntry is frozen — attributes cannot be reassigned."""
entry = NavEntry(title="Test", path="test.md", breadcrumb=("Test",))
try:
entry.title = "Modified" # type: ignore[misc]
assert False, "Should have raised FrozenInstanceError"
except AttributeError:
pass
print(" PASS: nav_entry_immutable")
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
if __name__ == "__main__":
print("Testing nav parser:")
tests = [
# Unit
test_flatten_simple_dict,
test_flatten_nested,
test_flatten_deep_nesting,
test_flatten_skips_external_links,
test_flatten_bare_string,
test_flatten_mixed,
test_parse_filters_index,
test_parse_all_includes_index,
test_nav_entry_immutable,
# Integration
test_parse_real_zensical,
]
passed = 0
failed = 0
for test in tests:
try:
test()
passed += 1
except AssertionError as e:
print(f" FAIL: {test.__name__}: {e}")
failed += 1
except Exception as e:
print(f" ERROR: {test.__name__}: {e}")
failed += 1
print(f"\n{passed}/{passed + failed} tests passed")
sys.exit(1 if failed > 0 else 0)

View File

@@ -81,8 +81,7 @@ dependencies = [
[project.optional-dependencies]
docs = [
"Jinja2",
"mkdocs",
"zensical",
]
testing = [

View File

@@ -68,21 +68,20 @@ def flash_panda(panda_serial: str) -> Panda:
return panda
def check_panda_support(panda_serials: list[str]) -> bool:
unsupported = []
def check_panda_support(panda_serials: list[str]) -> list[str]:
spi_serials = set(Panda.spi_list())
for serial in panda_serials:
if serial in spi_serials:
return [serial]
for serial in panda_serials:
panda = Panda(serial)
hw_type = panda.get_type()
is_internal = panda.is_internal()
panda.close()
if hw_type in Panda.SUPPORTED_DEVICES:
return True
if is_internal:
return [serial]
unsupported.append((serial, hw_type))
for serial, hw_type in unsupported:
cloudlog.warning(f"Panda {serial} is not supported (hw_type: {hw_type}), skipping...")
return False
return []
def main() -> None:
@@ -137,8 +136,9 @@ def main() -> None:
# custom flasher for xnor's Rivian Longitudinal Upgrade Kit
flash_rivian_long(panda_serials)
# skip flashing and health check if no supported panda is detected
if not check_panda_support(panda_serials):
# find the internal supported panda (e.g. skip external Black Panda)
panda_serials = check_panda_support(panda_serials)
if len(panda_serials) == 0:
continue
# Flash the first panda

View File

@@ -84,7 +84,11 @@ def flash_rivian_long(panda_serials: list[str]) -> None:
cloudlog.info("Not a Rivian, skipping longitudinal upgrade...")
return
# only check USB connected pandas, internal panda uses SPI and is never an external panda
usb_serials = set(Panda.usb_list())
for serial in panda_serials:
if serial not in usb_serials:
continue
panda = Panda(serial)
# only flash external black pandas (HW_TYPE_BLACK = 0x03)
if panda.get_type() == b'\x03' and not panda.is_internal():

View File

@@ -1,25 +1,8 @@
import os
import subprocess
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal')
replay_env = env.Clone()
replay_env['CCFLAGS'] += ['-Wno-deprecated-declarations']
if arch == "Darwin":
openssl_prefixes = []
for formula in ("openssl@3", "openssl"):
try:
prefix = subprocess.check_output(['brew', '--prefix', formula], encoding='utf8').strip()
if os.path.isdir(prefix):
openssl_prefixes.append(prefix)
except (FileNotFoundError, subprocess.CalledProcessError):
continue
for prefix in openssl_prefixes:
replay_env['CPPPATH'] += [f"{prefix}/include"]
replay_env['LIBPATH'] += [f"{prefix}/lib"]
base_frameworks = []
base_libs = [common, messaging, cereal, visionipc, 'm', 'ssl', 'crypto', 'pthread']

220
zensical.toml Normal file
View File

@@ -0,0 +1,220 @@
[project]
site_name = "sunnypilot docs"
site_url = "https://docs.sunnypilot.ai"
site_description = "sunnypilot Documentation"
repo_name = "sunnypilot/sunnypilot"
repo_url = "https://github.com/sunnypilot/sunnypilot/"
edit_uri = "blob/docs/docs_sp"
docs_dir = "docs_sp"
site_dir = "docs_site_sp"
strict = true
exclude_docs = "README.md"
extra_css = ["stylesheets/style.css"]
nav = [
{ "Home" = "index.md" },
{ "Getting Started" = [
"getting-started/index.md",
{ "What is sunnypilot?" = "getting-started/what-is-sunnypilot.md" },
{ "Use sunnypilot in a car" = "getting-started/use-sunnypilot-in-a-car.md" },
]},
{ "Installation" = [
"setup/index.md",
{ "Read Before Installing" = "setup/read-before-installing.md" },
{ "URL Method" = "setup/url-method.md" },
{ "SSH Method" = "setup/ssh-method.md" },
]},
{ "Features" = [
"features/index.md",
{ "Cruise Control" = [
"features/cruise/index.md",
{ "Intelligent Cruise Button Management" = "features/cruise/icbm.md" },
{ "Smart Cruise Control - Vision" = "features/cruise/scc-v.md" },
{ "Smart Cruise Control - Map" = "features/cruise/scc-m.md" },
{ "Custom ACC Increments" = "features/cruise/custom-acc-increments.md" },
{ "Dynamic Experimental Control" = "features/cruise/dynamic-experimental-control.md" },
{ "Speed Limit Assist" = "features/cruise/speed-limit.md" },
{ "Alpha Longitudinal" = "features/cruise/alpha-longitudinal.md" },
]},
{ "Steering" = [
"features/steering/index.md",
{ "Modular Assistive Driving System" = "features/steering/mads.md" },
{ "Neural Network Lateral Control" = "features/steering/nnlc.md" },
{ "Auto Lane Change" = "features/steering/auto-lane-change.md" },
{ "Torque Control" = "features/steering/torque-control.md" },
{ "Blinker Pause Lateral" = "features/steering/blinker-pause.md" },
]},
{ "Display & Visuals" = [
"features/display/index.md",
{ "Onroad Display" = "features/display/onroad-display.md" },
{ "HUD & Visuals" = "features/display/hud-visuals.md" },
]},
{ "Models & AI" = "features/models.md" },
{ "Connected Services" = [
"features/connected/index.md",
{ "sunnylink" = "features/connected/sunnylink.md" },
{ "OSM Maps" = "features/connected/osm-maps.md" },
]},
]},
{ "Settings" = [
"settings/index.md",
{ "Platform Differences" = "settings/platform-differences.md" },
{ "Device" = "settings/device.md" },
{ "Network" = "settings/network.md" },
{ "sunnylink" = "settings/sunnylink.md" },
{ "Toggles" = "settings/toggles.md" },
{ "Software" = "settings/software.md" },
{ "Models" = "settings/models.md" },
{ "Steering" = [
"settings/steering/index.md",
{ "MADS" = "settings/steering/mads.md" },
{ "Lane Change" = "settings/steering/lane-change.md" },
{ "Torque" = "settings/steering/torque.md" },
]},
{ "Cruise" = [
"settings/cruise/index.md",
{ "Speed Limit" = [
"settings/cruise/speed-limit/index.md",
{ "Speed Limit Source" = "settings/cruise/speed-limit/source.md" },
]},
]},
{ "Visuals" = "settings/visuals.md" },
{ "Display" = "settings/display.md" },
{ "OSM" = "settings/osm.md" },
{ "Trips" = "settings/trips.md" },
{ "Vehicle" = [
"settings/vehicle/index.md",
{ "Tesla" = "settings/vehicle/tesla.md" },
{ "Toyota / Lexus" = "settings/vehicle/toyota.md" },
{ "Hyundai / Kia / Genesis" = "settings/vehicle/hyundai.md" },
{ "Subaru" = "settings/vehicle/subaru.md" },
]},
{ "Firehose" = "settings/firehose.md" },
{ "Developer" = "settings/developer.md" },
]},
{ "How-To Guides" = [
"how-to/index.md",
{ "Share a Route" = "how-to/share-a-route.md" },
{ "Preserve Local File Changes" = "how-to/preserve-local-changes.md" },
]},
{ "Safety" = [
"safety/index.md",
{ "Safety Information" = "safety/safety.md" },
{ "Driver Responsibility & L2 ADAS" = "safety/driver-responsibility.md" },
{ "Prohibited Modifications" = "safety/prohibited-modifications.md" },
]},
{ "Technical Reference" = [
"technical/index.md",
{ "Hyundai Longitudinal Tuning" = "technical/hyundai-longitudinal-tuning.md" },
{ "Recommended Branches" = "references/recommended-branches.md" },
{ "Branch Definitions" = "references/branch-definitions.md" },
]},
{ "Community" = [
"community/index.md",
{ "Contributing" = "community/contributing.md" },
{ "Workflow" = "community/workflow.md" },
{ "Reporting a Bug" = "community/reporting-a-bug.md" },
{ "Community Forum" = "https://community.sunnypilot.ai" },
]},
]
[project.theme]
variant = "modern"
logo = "assets/logo.png"
favicon = "assets/favicon.png"
features = [
"content.code.copy",
"content.tabs.link",
"navigation.tabs",
"navigation.tabs.sticky",
"navigation.indexes",
"navigation.path",
"navigation.instant",
"navigation.instant.progress",
"navigation.tracking",
"navigation.top",
"navigation.sections",
"navigation.expand",
"navigation.footer",
"toc.follow",
"search.suggest",
"search.highlight",
]
[project.theme.font]
text = "Open Sans"
code = "Fira Code"
[[project.theme.palette]]
media = "(prefers-color-scheme)"
toggle.icon = "material/brightness-auto"
toggle.name = "Switch to light mode"
[[project.theme.palette]]
media = "(prefers-color-scheme: light)"
scheme = "default"
primary = "#594AE2"
accent = "#594AE2"
toggle.icon = "material/brightness-7"
toggle.name = "Switch to dark mode"
[[project.theme.palette]]
media = "(prefers-color-scheme: dark)"
scheme = "slate"
primary = "#594AE2"
accent = "#594AE2"
toggle.icon = "material/brightness-4"
toggle.name = "Switch to system preference"
# Markdown extensions
# When specifying any extensions, defaults are overridden, so we must list all needed extensions.
[project.markdown_extensions.admonition]
[project.markdown_extensions.attr_list]
[project.markdown_extensions.def_list]
[project.markdown_extensions.footnotes]
[project.markdown_extensions.md_in_html]
[project.markdown_extensions.toc]
permalink = true
[project.markdown_extensions.pymdownx.details]
[project.markdown_extensions.pymdownx.emoji]
emoji_index = "zensical.extensions.emoji.twemoji"
emoji_generator = "zensical.extensions.emoji.to_svg"
[project.markdown_extensions.pymdownx.highlight]
anchor_linenums = true
line_spans = "__span"
pygments_lang_class = true
[project.markdown_extensions.pymdownx.inlinehilite]
[project.markdown_extensions.pymdownx.magiclink]
normalize_issue_symbols = true
repo_url_shorthand = true
user = "sunnypilot"
repo = "sunnypilot"
[project.markdown_extensions.pymdownx.snippets]
[project.markdown_extensions.pymdownx.superfences]
[project.markdown_extensions.pymdownx.tabbed]
alternate_style = true
[project.markdown_extensions.pymdownx.tasklist]
custom_checkbox = true
clickable_checkbox = true
[[project.extra.social]]
icon = "fontawesome/brands/github"
link = "https://github.com/sunnypilot/sunnypilot"
[[project.extra.social]]
icon = "fontawesome/solid/comments"
link = "https://community.sunnypilot.ai"