Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
76c69d8
Web: Fix CAN category visibility and update backend board metadata
garvit000 Jan 17, 2026
a0cb13a
Use standard BoardMetadata class and revert cache changes.
garvit000 Jan 18, 2026
18f31d4
builder: fix linting error
shiv-tyagi Jan 19, 2026
33e8711
web: allow downloads for all terminal states
shiv-tyagi Jan 20, 2026
fd5913a
web: add red color for TIMED_OUT state in UI
lakhmanisahil Jan 20, 2026
1efff46
builder: add timeout handling using CBS_BUILD_TIMEOUT_SEC
lakhmanisahil Jan 20, 2026
d5bdcd5
build_manager: track time_started and handle timeout state
lakhmanisahil Jan 20, 2026
c09a915
docker-compose: add CBS_BUILD_TIMEOUT_SEC env var
lakhmanisahil Jan 20, 2026
d632ff9
Fix rate limit exception handling in /generate endpoint
imDarshanGK Jan 11, 2026
cb84cb9
metadata_manager: add method to get feature defaults from firmware se…
shiv-tyagi Jan 18, 2026
1a83d35
web: migrate web backend to fastapi
shiv-tyagi Jan 18, 2026
3d7637b
docker-compose: update default port number and CBS_REMOTES_RELOAD_TO…
shiv-tyagi Jan 18, 2026
98e29e6
README: fix commands to run the new web service
shiv-tyagi Jan 18, 2026
2e63195
build_manager: fix linting issues
shiv-tyagi Jan 31, 2026
e946217
builder: fix linting issues
shiv-tyagi Jan 31, 2026
69f7b31
.flake8: add flake8 config
shiv-tyagi Jan 31, 2026
74470e3
.github: add workflow to run flake8
shiv-tyagi Jan 31, 2026
ed51d6d
Add missing TIMED_OUT literal to schema
rob-clarke Feb 3, 2026
8587dd5
web: remove viewlog route, use root with build_id param instead
shiv-tyagi Feb 4, 2026
92e8c75
build_manager: store version id in build metadata
shiv-tyagi Feb 6, 2026
9689878
web: add functionality to copy existing build configuration in add-bu…
shiv-tyagi Feb 6, 2026
2babfbb
web:add donate button to index page
lakhmanisahil Feb 4, 2026
fc5db88
web: upgrade uvicorn to 0.40.0
shiv-tyagi Feb 9, 2026
89f4d61
web: correct paypal donation link
shiv-tyagi Feb 9, 2026
3d45a48
Web: Fix CAN category visibility and update backend board metadata
garvit000 Jan 17, 2026
d029179
refactor board metadata and backend feature filtering
garvit000 Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[flake8]
max-line-length = 127
max-complexity = 10
exclude =
.git,
__pycache__,
.pytest_cache,
*.pyc,
.venv,
venv,
env,
base/,
redis_data/,
.vagrant/
27 changes: 27 additions & 0 deletions .github/workflows/linting.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Linting

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
flake8:
runs-on: ubuntu-24.04

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Install flake8
run: pip install flake8

- name: Run flake8
run: |
flake8 . --count --show-source --statistics
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,24 +105,28 @@ To run the ArduPilot Custom Firmware Builder locally without Docker, ensure you
```

5. **Execute the Application:**
- For a development environment, run:
- For a development environment with auto-reload, run:
```bash
./web/app.py
python3 web/main.py
```
To change the port, use the `--port` argument:
```bash
python3 web/main.py --port 9000
```
- For a production environment, use:
```bash
gunicorn web.wsgi:application
uvicorn web.main:app --host 0.0.0.0 --port 8080
```

During the coding and testing phases, use the development environment to easily debug and make changes. When deploying the app for end users, use the production environment to ensure better performance, scalability, and security.
During the coding and testing phases, use the development environment to easily debug and make changes with auto-reload enabled. When deploying the app for end users, use the production environment to ensure better performance, scalability, and security.

The application will automatically set up the required base directory at `./base` upon first execution. You may customize this path by using the `--basedir` option with the above commands or by setting the `CBS_BASEDIR` environment variable.
The application will automatically set up the required base directory at `./base` upon first execution. You may customize this path by setting the `CBS_BASEDIR` environment variable.

6. **Access the Web Interface:**

Once the application is running, you can access the interface in your web browser at http://localhost:5000 if running directly using app.py (development environment), or at http://localhost:8000 if using Gunicorn (production environment).
Once the application is running, you can access the interface in your web browser at http://localhost:8080.

To change the default port when running with app.py, modify the `app.run()` call in web/app.py file by passing `port=<expected-port>` as an argument. For Gunicorn, refer to the [commonly used arguments](https://docs.gunicorn.org/en/latest/run.html#commonly-used-arguments) section of the Gunicorn documentation to specify a different port.
The default port is 8080, or the value of the `WEB_PORT` environment variable if set. You can override this by passing the `--port` argument when running the application directly (e.g., `python3 web/main.py --port 9000`) or when using uvicorn (e.g., `uvicorn web.main:app --port 5000`). Refer to the [uvicorn documentation](https://www.uvicorn.org/) for additional configuration options.

## Directory Structure
The default directory structure is established as follows:
Expand Down
28 changes: 28 additions & 0 deletions build_manager/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class BuildState(Enum):
SUCCESS = 2
FAILURE = 3
ERROR = 4
TIMED_OUT = 5


class BuildProgress:
Expand Down Expand Up @@ -44,6 +45,7 @@ def to_dict(self) -> dict:
class BuildInfo:
def __init__(self,
vehicle_id: str,
version_id: str,
remote_info: RemoteInfo,
git_hash: str,
board: str,
Expand All @@ -55,13 +57,15 @@ def __init__(self,

Parameters:
vehicle_id (str): The vehicle ID associated with the build.
version_id (str): The version ID associated with the build.
remote_info (RemoteInfo): The remote repository containing the
source commit to build on.
git_hash (str): The git commit hash to build on.
board (str): Board to build for.
selected_features (set): Set of features selected for the build.
"""
self.vehicle_id = vehicle_id
self.version_id = version_id
self.remote_info = remote_info
self.git_hash = git_hash
self.board = board
Expand All @@ -71,16 +75,19 @@ def __init__(self,
percent=0
)
self.time_created = time.time()
self.time_started = None # when build state becomes RUNNING

def to_dict(self) -> dict:
return {
'vehicle_id': self.vehicle_id,
'version_id': self.version_id,
'remote_info': self.remote_info.to_dict(),
'git_hash': self.git_hash,
'board': self.board,
'selected_features': list(self.selected_features),
'progress': self.progress.to_dict(),
'time_created': self.time_created,
'time_started': getattr(self, 'time_started', None),
}


Expand Down Expand Up @@ -353,6 +360,27 @@ def __update_build_info(self,
keepttl=True
)

def update_build_time_started(self,
build_id: str,
time_started: float) -> None:
"""
Update the build's time_started timestamp.

Parameters:
build_id (str): The ID of the build to update.
time_started (float): The timestamp when the build started running.
"""
build_info = self.get_build_info(build_id=build_id)

if build_info is None:
raise ValueError(f"Build with id {build_id} not found.")

build_info.time_started = time_started
self.__update_build_info(
build_id=build_id,
build_info=build_info
)

def update_build_progress_percent(self,
build_id: str,
percent: int) -> None:
Expand Down
31 changes: 31 additions & 0 deletions build_manager/progress_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
BuildManager as bm,
BuildState
)
import time

CBS_BUILD_TIMEOUT_SEC = int(os.getenv('CBS_BUILD_TIMEOUT_SEC', 900))


class BuildProgressUpdater:
Expand Down Expand Up @@ -157,6 +160,28 @@ def __refresh_running_build_state(self, build_id: str) -> BuildState:
raise RuntimeError(
"This method should only be called for running builds."
)
# Set time_started if not already set
if build_info.time_started is None:
start_time = time.time()
bm.get_singleton().update_build_time_started(
build_id=build_id,
time_started=start_time
)
self.logger.info(
f"Build {build_id} started running at {start_time}"
)
build_info.time_started = start_time

# Check for timeout
elapsed = time.time() - build_info.time_started
if elapsed > CBS_BUILD_TIMEOUT_SEC:
self.logger.warning(
f"Build {build_id} timed out after {elapsed:.0f} seconds"
)
build_info.error_message = (
f"Build exceeded {CBS_BUILD_TIMEOUT_SEC // 60} minute timeout"
)
return BuildState.TIMED_OUT

# Builder ships the archive post completion
# This is irrespective of SUCCESS or FAILURE
Expand Down Expand Up @@ -213,6 +238,9 @@ def __update_build_percent(self, build_id: str) -> None:
elif current_state == BuildState.ERROR:
# Keep existing percentage
pass
elif current_state == BuildState.TIMED_OUT:
# Keep existing percentage
pass
else:
raise Exception("Unhandled BuildState.")

Expand Down Expand Up @@ -259,6 +287,9 @@ def __update_build_state(self, build_id: str) -> None:
elif current_state == BuildState.ERROR:
# ERROR is a conclusive state
pass
elif current_state == BuildState.TIMED_OUT:
# TIMED_OUT is a conclusive state
pass
else:
raise Exception("Unhandled BuildState.")

Expand Down
111 changes: 64 additions & 47 deletions builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
)
from pathlib import Path

CBS_BUILD_TIMEOUT_SEC = int(os.getenv('CBS_BUILD_TIMEOUT_SEC', 900))


class Builder:
"""
Expand Down Expand Up @@ -366,7 +368,9 @@ def __build(self, build_id: str) -> None:
logpath = bm.get_singleton().get_build_log_path(build_id)
with open(logpath, "a") as build_log:
# Get vehicle object
vehicle = vehm.get_singleton().get_vehicle_by_id(build_info.vehicle_id)
vehicle = vehm.get_singleton().get_vehicle_by_id(
build_info.vehicle_id
)

# Log initial configuration
build_log.write(
Expand All @@ -375,52 +379,65 @@ def __build(self, build_id: str) -> None:
)
build_log.flush()

# Run the build steps
self.logger.info("Running waf configure")
build_log.write("Running waf configure\n")
build_log.flush()
subprocess.run(
[
"python3",
"./waf",
"configure",
"--board",
build_info.board,
"--out",
self.__get_path_to_build_dir(build_id),
"--extra-hwdef",
self.__get_path_to_extra_hwdef(build_id),
],
cwd=self.__get_path_to_build_src(build_id),
stdout=build_log,
stderr=build_log,
shell=False,
)

self.logger.info("Running clean")
build_log.write("Running clean\n")
build_log.flush()
subprocess.run(
["python3", "./waf", "clean"],
cwd=self.__get_path_to_build_src(build_id),
stdout=build_log,
stderr=build_log,
shell=False,
)

self.logger.info("Running build")
build_log.write("Running build\n")
build_log.flush()
build_command = vehicle.waf_build_command
subprocess.run(
["python3", "./waf", build_command],
cwd=self.__get_path_to_build_src(build_id),
stdout=build_log,
stderr=build_log,
shell=False,
)
build_log.write("done build\n")
build_log.flush()
try:
# Run the build steps
self.logger.info("Running waf configure")
build_log.write("Running waf configure\n")
build_log.flush()
subprocess.run(
[
"python3",
"./waf",
"configure",
"--board",
build_info.board,
"--out",
self.__get_path_to_build_dir(build_id),
"--extra-hwdef",
self.__get_path_to_extra_hwdef(build_id),
],
cwd=self.__get_path_to_build_src(build_id),
stdout=build_log,
stderr=build_log,
shell=False,
timeout=CBS_BUILD_TIMEOUT_SEC,
)

self.logger.info("Running clean")
build_log.write("Running clean\n")
build_log.flush()
subprocess.run(
["python3", "./waf", "clean"],
cwd=self.__get_path_to_build_src(build_id),
stdout=build_log,
stderr=build_log,
shell=False,
timeout=CBS_BUILD_TIMEOUT_SEC,
)

self.logger.info("Running build")
build_log.write("Running build\n")
build_log.flush()
build_command = vehicle.waf_build_command
subprocess.run(
["python3", "./waf", build_command],
cwd=self.__get_path_to_build_src(build_id),
stdout=build_log,
stderr=build_log,
shell=False,
timeout=CBS_BUILD_TIMEOUT_SEC,
)
build_log.write("done build\n")
build_log.flush()
except subprocess.TimeoutExpired:
self.logger.error(
f"Build {build_id} timed out after "
f"{CBS_BUILD_TIMEOUT_SEC} seconds."
)
build_log.write(
f"Build timed out after {CBS_BUILD_TIMEOUT_SEC} seconds.\n"
)
build_log.flush()

def shutdown(self) -> None:
"""
Expand Down
6 changes: 4 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ services:
CBS_LOG_LEVEL: ${CBS_LOG_LEVEL:-INFO}
CBS_ENABLE_INBUILT_BUILDER: 0
CBS_GITHUB_ACCESS_TOKEN: ${CBS_GITHUB_ACCESS_TOKEN}
CBS_REMOTES_RELOAD_TOKEN: ${CBS_REMOTES_RELOAD_TOKEN}
PYTHONPATH: /app
GUNICORN_CMD_ARGS: --bind=0.0.0.0:80 --timeout=300
CBS_BUILD_TIMEOUT_SEC: ${CBS_BUILD_TIMEOUT_SEC:-900}
volumes:
- ./base:/base:rw
depends_on:
- redis
ports:
- "127.0.0.1:${WEB_PORT:-8080}:80"
- "127.0.0.1:${WEB_PORT:-8080}:8080"

builder:
build:
Expand All @@ -40,6 +41,7 @@ services:
CBS_BASEDIR: /base
CBS_LOG_LEVEL: ${CBS_LOG_LEVEL:-INFO}
PYTHONPATH: /app
CBS_BUILD_TIMEOUT_SEC: ${CBS_BUILD_TIMEOUT_SEC:-900}
volumes:
- ./base:/base:rw
depends_on:
Expand Down
Loading