mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-08 23:04:19 +08:00
Compare commits
25 Commits
cabana-fix
...
docs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bae722215 | ||
|
|
32bbcfa975 | ||
|
|
738b361a25 | ||
|
|
b61ccf2bee | ||
|
|
c8d543a8fe | ||
|
|
801c107aa1 | ||
|
|
7e79d5424f | ||
|
|
6dde8560fe | ||
|
|
2655a2e138 | ||
|
|
06cc8af9bf | ||
|
|
c918e5bbdc | ||
|
|
922ad64d13 | ||
|
|
31b3776150 | ||
|
|
1e5ad42e7a | ||
|
|
58c72df0be | ||
|
|
b2667cac32 | ||
|
|
fd07e80c34 | ||
|
|
bf628bd042 | ||
|
|
5043f5e7a4 | ||
|
|
83a84ba4bc | ||
|
|
7d225a1e33 | ||
|
|
450d5a4458 | ||
|
|
d06a404bd9 | ||
|
|
27dfd5cbf0 | ||
|
|
2e82908c07 |
66
.github/workflows/docs-sp.yaml
vendored
Normal file
66
.github/workflows/docs-sp.yaml
vendored
Normal 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
47
.github/workflows/forum-docs.yaml
vendored
Normal 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
|
||||
105
.github/workflows/post-to-discourse/action.yml
vendored
105
.github/workflows/post-to-discourse/action.yml
vendored
@@ -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
|
||||
93
.github/workflows/sync-docs-discourse.yml
vendored
Normal file
93
.github/workflows/sync-docs-discourse.yml
vendored
Normal 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!')
|
||||
"
|
||||
78
.github/workflows/test-discourse.yaml.yml
vendored
78
.github/workflows/test-discourse.yaml.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -15,6 +15,8 @@ a.out
|
||||
.cache/
|
||||
|
||||
/docs_site/
|
||||
/docs_site_sp/
|
||||
/.discourse_sync_cache/
|
||||
|
||||
*.mp4
|
||||
*.dylib
|
||||
|
||||
3
docs_sp/assets/favicon.png
Normal file
3
docs_sp/assets/favicon.png
Normal 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
BIN
docs_sp/assets/logo.png
LFS
Normal file
Binary file not shown.
27
docs_sp/community/contributing.md
Normal file
27
docs_sp/community/contributing.md
Normal 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.
|
||||
12
docs_sp/community/index.md
Normal file
12
docs_sp/community/index.md
Normal 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
|
||||
69
docs_sp/community/reporting-a-bug.md
Normal file
69
docs_sp/community/reporting-a-bug.md
Normal 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.
|
||||
37
docs_sp/community/workflow.md
Normal file
37
docs_sp/community/workflow.md
Normal 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
|
||||
10
docs_sp/features/connected/index.md
Normal file
10
docs_sp/features/connected/index.md
Normal 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
|
||||
45
docs_sp/features/connected/osm-maps.md
Normal file
45
docs_sp/features/connected/osm-maps.md
Normal 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.
|
||||
67
docs_sp/features/connected/sunnylink.md
Normal file
67
docs_sp/features/connected/sunnylink.md
Normal 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.
|
||||
53
docs_sp/features/cruise/alpha-longitudinal.md
Normal file
53
docs_sp/features/cruise/alpha-longitudinal.md
Normal 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.
|
||||
28
docs_sp/features/cruise/custom-acc-increments.md
Normal file
28
docs_sp/features/cruise/custom-acc-increments.md
Normal 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 (1–10 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.
|
||||
61
docs_sp/features/cruise/dynamic-experimental-control.md
Normal file
61
docs_sp/features/cruise/dynamic-experimental-control.md
Normal 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.
|
||||
77
docs_sp/features/cruise/icbm.md
Normal file
77
docs_sp/features/cruise/icbm.md
Normal 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.
|
||||
15
docs_sp/features/cruise/index.md
Normal file
15
docs_sp/features/cruise/index.md
Normal 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
|
||||
30
docs_sp/features/cruise/scc-m.md
Normal file
30
docs_sp/features/cruise/scc-m.md
Normal 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.
|
||||
28
docs_sp/features/cruise/scc-v.md
Normal file
28
docs_sp/features/cruise/scc-v.md
Normal 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.
|
||||
90
docs_sp/features/cruise/speed-limit.md
Normal file
90
docs_sp/features/cruise/speed-limit.md
Normal 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.
|
||||
34
docs_sp/features/display/hud-visuals.md
Normal file
34
docs_sp/features/display/hud-visuals.md
Normal 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.
|
||||
10
docs_sp/features/display/index.md
Normal file
10
docs_sp/features/display/index.md
Normal 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
|
||||
23
docs_sp/features/display/onroad-display.md
Normal file
23
docs_sp/features/display/onroad-display.md
Normal 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
27
docs_sp/features/index.md
Normal 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).
|
||||
30
docs_sp/features/models.md
Normal file
30
docs_sp/features/models.md
Normal 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 (500–2000)
|
||||
- **LAGD Delay** — Fixed steering delay offset in milliseconds when LAGD is disabled (5–50)
|
||||
|
||||
## How to Configure
|
||||
|
||||
**Settings** → **Models**
|
||||
|
||||
## Settings Reference
|
||||
|
||||
See [Models Settings](../settings/models.md) for all configuration options.
|
||||
59
docs_sp/features/steering/auto-lane-change.md
Normal file
59
docs_sp/features/steering/auto-lane-change.md
Normal 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.5s–3s):** 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.
|
||||
36
docs_sp/features/steering/blinker-pause.md
Normal file
36
docs_sp/features/steering/blinker-pause.md
Normal 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.
|
||||
13
docs_sp/features/steering/index.md
Normal file
13
docs_sp/features/steering/index.md
Normal 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
|
||||
86
docs_sp/features/steering/mads.md
Normal file
86
docs_sp/features/steering/mads.md
Normal 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.
|
||||
58
docs_sp/features/steering/nnlc.md
Normal file
58
docs_sp/features/steering/nnlc.md
Normal 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.
|
||||
48
docs_sp/features/steering/torque-control.md
Normal file
48
docs_sp/features/steering/torque-control.md
Normal 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.01–5.00 (steering aggressiveness) |
|
||||
| **Friction** | 0.01–1.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.
|
||||
10
docs_sp/getting-started/index.md
Normal file
10
docs_sp/getting-started/index.md
Normal 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
|
||||
42
docs_sp/getting-started/use-sunnypilot-in-a-car.md
Normal file
42
docs_sp/getting-started/use-sunnypilot-in-a-car.md
Normal 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.
|
||||
31
docs_sp/getting-started/what-is-sunnypilot.md
Normal file
31
docs_sp/getting-started/what-is-sunnypilot.md
Normal 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
10
docs_sp/how-to/index.md
Normal 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
|
||||
49
docs_sp/how-to/preserve-local-changes.md
Normal file
49
docs_sp/how-to/preserve-local-changes.md
Normal 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.
|
||||
58
docs_sp/how-to/share-a-route.md
Normal file
58
docs_sp/how-to/share-a-route.md
Normal 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
37
docs_sp/index.md
Normal 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>
|
||||
76
docs_sp/references/branch-definitions.md
Normal file
76
docs_sp/references/branch-definitions.md
Normal 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` |
|
||||
46
docs_sp/references/recommended-branches.md
Normal file
46
docs_sp/references/recommended-branches.md
Normal 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.
|
||||
56
docs_sp/safety/driver-responsibility.md
Normal file
56
docs_sp/safety/driver-responsibility.md
Normal 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 |
|
||||
| 4–5 | 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
11
docs_sp/safety/index.md
Normal 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
|
||||
61
docs_sp/safety/prohibited-modifications.md
Normal file
61
docs_sp/safety/prohibited-modifications.md
Normal 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
38
docs_sp/safety/safety.md
Normal 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.
|
||||
87
docs_sp/settings/cruise/index.md
Normal file
87
docs_sp/settings/cruise/index.md
Normal 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)
|
||||
66
docs_sp/settings/cruise/speed-limit/index.md
Normal file
66
docs_sp/settings/cruise/speed-limit/index.md
Normal 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)
|
||||
37
docs_sp/settings/cruise/speed-limit/source.md
Normal file
37
docs_sp/settings/cruise/speed-limit/source.md
Normal 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
|
||||
136
docs_sp/settings/developer.md
Normal file
136
docs_sp/settings/developer.md
Normal 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
137
docs_sp/settings/device.md
Normal 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.
|
||||
52
docs_sp/settings/display.md
Normal file
52
docs_sp/settings/display.md
Normal 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)
|
||||
51
docs_sp/settings/firehose.md
Normal file
51
docs_sp/settings/firehose.md
Normal 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
28
docs_sp/settings/index.md
Normal 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 |
|
||||
83
docs_sp/settings/models.md
Normal file
83
docs_sp/settings/models.md
Normal 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).
|
||||
22
docs_sp/settings/network.md
Normal file
22
docs_sp/settings/network.md
Normal 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
68
docs_sp/settings/osm.md
Normal 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)
|
||||
79
docs_sp/settings/platform-differences.md
Normal file
79
docs_sp/settings/platform-differences.md
Normal 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.
|
||||
60
docs_sp/settings/software.md
Normal file
60
docs_sp/settings/software.md
Normal 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.
|
||||
89
docs_sp/settings/steering/index.md
Normal file
89
docs_sp/settings/steering/index.md
Normal 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)
|
||||
43
docs_sp/settings/steering/lane-change.md
Normal file
43
docs_sp/settings/steering/lane-change.md
Normal 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)
|
||||
62
docs_sp/settings/steering/mads.md
Normal file
62
docs_sp/settings/steering/mads.md
Normal 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)
|
||||
82
docs_sp/settings/steering/torque.md
Normal file
82
docs_sp/settings/steering/torque.md
Normal 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)
|
||||
68
docs_sp/settings/sunnylink.md
Normal file
68
docs_sp/settings/sunnylink.md
Normal 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
104
docs_sp/settings/toggles.md
Normal 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
35
docs_sp/settings/trips.md
Normal 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.
|
||||
27
docs_sp/settings/vehicle/hyundai.md
Normal file
27
docs_sp/settings/vehicle/hyundai.md
Normal 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.
|
||||
42
docs_sp/settings/vehicle/index.md
Normal file
42
docs_sp/settings/vehicle/index.md
Normal 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.
|
||||
33
docs_sp/settings/vehicle/subaru.md
Normal file
33
docs_sp/settings/vehicle/subaru.md
Normal 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.
|
||||
46
docs_sp/settings/vehicle/tesla.md
Normal file
46
docs_sp/settings/vehicle/tesla.md
Normal 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.
|
||||
35
docs_sp/settings/vehicle/toyota.md
Normal file
35
docs_sp/settings/vehicle/toyota.md
Normal 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
153
docs_sp/settings/visuals.md
Normal 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
11
docs_sp/setup/index.md
Normal 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
|
||||
42
docs_sp/setup/read-before-installing.md
Normal file
42
docs_sp/setup/read-before-installing.md
Normal 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
|
||||
35
docs_sp/setup/ssh-method.md
Normal file
35
docs_sp/setup/ssh-method.md
Normal 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)
|
||||
28
docs_sp/setup/url-method.md
Normal file
28
docs_sp/setup/url-method.md
Normal 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
|
||||
17
docs_sp/stylesheets/style.css
Normal file
17
docs_sp/stylesheets/style.css
Normal 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);
|
||||
}
|
||||
128
docs_sp/technical/hyundai-longitudinal-tuning.md
Normal file
128
docs_sp/technical/hyundai-longitudinal-tuning.md
Normal 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
|
||||
11
docs_sp/technical/index.md
Normal file
11
docs_sp/technical/index.md
Normal 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
|
||||
58
docs_sp/tools/content_cache.py
Normal file
58
docs_sp/tools/content_cache.py
Normal 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
272
docs_sp/tools/converter.py
Normal 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)
|
||||
233
docs_sp/tools/discourse_client.py
Normal file
233
docs_sp/tools/discourse_client.py
Normal 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
|
||||
88
docs_sp/tools/nav_parser.py
Normal file
88
docs_sp/tools/nav_parser.py
Normal 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
|
||||
612
docs_sp/tools/sync_docs_discourse.rb
Normal file
612
docs_sp/tools/sync_docs_discourse.rb
Normal 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
|
||||
183
docs_sp/tools/test_content_cache.py
Normal file
183
docs_sp/tools/test_content_cache.py
Normal 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)
|
||||
502
docs_sp/tools/test_converter.py
Normal file
502
docs_sp/tools/test_converter.py
Normal 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)
|
||||
436
docs_sp/tools/test_discourse_client.py
Normal file
436
docs_sp/tools/test_discourse_client.py
Normal 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)
|
||||
238
docs_sp/tools/test_nav_parser.py
Normal file
238
docs_sp/tools/test_nav_parser.py
Normal 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)
|
||||
@@ -81,8 +81,7 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
docs = [
|
||||
"Jinja2",
|
||||
"mkdocs",
|
||||
"zensical",
|
||||
]
|
||||
|
||||
testing = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
220
zensical.toml
Normal file
220
zensical.toml
Normal 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"
|
||||
Reference in New Issue
Block a user