Contributing & Development
Extend and improve bbl-shutter-cam.
Overview
This project is open-source and welcomes contributions. For comprehensive developer setup instructions, see CONTRIBUTING.md at the project root.
This guide covers:
- Local development setup
- Code structure and patterns
- Adding features
- Submitting changes
For VSCode setup, tasks, and testing workflows, see CONTRIBUTING.md.
Development Setup
1. Clone Repository
git clone https://github.com/bodybybuddha/bbl-shutter-cam.git
cd bbl-shutter-cam
2. Create Virtual Environment
python3 -m venv venv
source venv/bin/activate
On Windows:
python -m venv venv
venv\Scripts\activate
3. Install Development Dependencies
pip install -e ".[dev]"
This installs the package in editable mode plus dev tools (pytest, pylint, mypy, black).
4. Verify Setup
# Check CLI
bbl-shutter-cam --help
# Run tests
pytest tests/ -v
# Lint code
pylint src/bbl_shutter_cam/
Code Structure
src/bbl_shutter_cam/
├── __init__.py # Package metadata
├── cli.py # Command-line interface
├── discover.py # BLE device discovery & capture orchestration
├── ble.py # Bluetooth Low Energy utilities
├── config.py # Configuration file handling
├── camera.py # Camera capture (rpicam-still wrapper)
├── util.py # Common utilities
└── logging_config.py # Logging setup
Module Responsibilities
cli.py - Command parsing and subcommand routing
main()- Entry point, argument parsing_cmd_scan()- BLE device scanning_cmd_setup()- Interactive device setup_cmd_debug()- Signal discovery_cmd_run()- Photo capture
discover.py - Core business logic
run_profile()- Main event loop, triggers photo capture on signalslearn_notify_uuid()- Interactive Bluetooth setupdebug_signals()- Signal discovery and logging_update_config_with_signals()- Save discovered signals to config
config.py - Configuration management
load_config()- Load user’s main config fileload_profile()- Load specific printer profileget_trigger_events()- Load trigger signals from configget_event_trigger_bytes()- Filter and convert signals to bytesupdate_profile_device_fields()- Save device settings
ble.py - Hardware abstraction
BLEClientclass - Async Bluetooth connection wrapperscan()- Find nearby BLE devicesconnect()- Establish connectionstart_notify()- Subscribe to characteristic updates
camera.py - Camera operations
capture()- Trigger photo using rpicam-still
Architecture Patterns
Asyncio First
All BLE operations are async:
# Good
async def run_profile(profile):
async with BLEClient(mac_address) as client:
await client.start_notify(uuid, callback)
# Avoid
client = BLEClient(mac_address) # Won't work, needs await
Config-Driven, Not Hardcoded
Store configuration in TOML, not code constants:
# Good
trigger_events = get_trigger_events(profile)
for event in trigger_events:
if event.get("capture"):
# Process event
# Avoid
PRESS_BYTES = bytes.fromhex("4000") # Hardcoded!
Profile-Based Isolation
Each printer has its config profile:
[profiles.office-p1s]
device = { mac = "AA:BB:CC:DD:EE:FF", ... }
[profiles.garage-x1c]
device = { mac = "11:22:33:44:55:66", ... }
Common Tasks
Add New CLI Subcommand
- Add argument parser in
cli.py:
# In main(), add subparsers section:
debug_parser = subparsers.add_parser("tune")
debug_parser.add_argument("--profile", required=True)
debug_parser.add_argument("--option", default="value")
debug_parser.set_defaults(func=_cmd_tune)
- Implement handler function:
def _cmd_tune(args):
"""Handle tune subcommand."""
profile = config.load_profile(args.profile)
asyncio.run(discover.tune_profile(profile, args.option))
- Implement logic in
discover.py:
async def tune_profile(profile, option):
"""Interactive camera tuning."""
# Your logic here
pass
Modify BLE Signal Handling
Current signal handling in discover.py run_profile():
trigger_events = get_trigger_events(profile)
trigger_map = {}
for event in trigger_events:
trigger_bytes = bytes.fromhex(event.get("hex", ""))
trigger_map[trigger_bytes] = event
# On notification:
if data in trigger_map:
event = trigger_map[data]
if event.get("capture"):
await camera.capture(profile)
To add a filter (e.g., ignore glitches below 100ms):
# In run_profile():
last_trigger = 0
while running:
async for data in client.iterate_notify():
now = time.time()
if (now - last_trigger) < 0.1: # 100ms debounce
continue
last_trigger = now
if data in trigger_map and trigger_map[data].get("capture"):
await camera.capture(profile)
Add Configuration Option
- Update TOML schema in docs (
docs/user-guide/profiles.md) - Add to config loading (
config.py):
def load_profile(name):
profile = config.load_config()
profile_data = profile[f"profiles.{name}"]
my_option = profile_data.get("my_key", "default_value")
return profile_data
- Use in
discover.py:
async def run_profile(profile):
my_option = profile.get("my_key", "default_value")
# Use option...
Testing
Run All Tests
pytest tests/ -v
Run Specific Test File
pytest tests/test_config.py -v
Check Coverage
pytest tests/ --cov=src/bbl_shutter_cam
Adding New Tests
Create test file in tests/:
# tests/test_myfeature.py
import pytest
from bbl_shutter_cam import myfeature
def test_something():
result = myfeature.my_function()
assert result == expected
@pytest.mark.asyncio
async def test_async_something():
result = await myfeature.my_async_function()
assert result == expected
Run:
pytest tests/test_myfeature.py -v
Code Quality
Linting
pylint src/bbl_shutter_cam/
Fix common issues:
black src/bbl_shutter_cam/ # Auto-format
Type Checking
mypy src/bbl_shutter_cam/ --ignore-missing-imports
Before Committing
# Format code
black src/bbl_shutter_cam/
# Lint
pylint src/bbl_shutter_cam/
# Run tests
pytest tests/ -v
# Type check
mypy src/bbl_shutter_cam/ --ignore-missing-imports
Git Workflow
Create Feature Branch
git checkout -b feature/my-feature
# or
git checkout -b fix/bug-name
Make Changes
git add src/bbl_shutter_cam/my_file.py
git commit -m "feat: add new feature
Detailed description of change.
"
Commit message format:
feat:- New featurefix:- Bug fixdocs:- Documentationtest:- Testsrefactor:- Code refactoring
Push and Create Pull Request
git push origin feature/my-feature
Then open pull request on GitHub.
Release Process
- Update version in
pyproject.toml - Update
CHANGELOG.md(if used) - Create git tag:
git tag -a v1.0.0 -m "Version 1.0.0 release"
git push origin v1.0.0
- Create GitHub Release with release notes
- Publishing the release triggers automated Raspberry Pi builds
- Artifacts are attached to the release (arm64 + armv7)
- Build distribution (optional for PyPI):
pip install build
python -m build
Documentation
When adding features:
- Update code with docstrings:
def my_function(param1: str) -> bool:
"""Short description.
Longer description explaining the function's behavior,
parameters, and return value.
Args:
param1: Description of param1
Returns:
Description of return value
"""
pass
- Update user documentation in
docs/ - Add FAQ entry if commonly needed
Building Standalone Executables
Want to distribute bbl-shutter-cam as a standalone application without requiring Python?
See the Building Executables Guide for detailed instructions on:
- Creating executables for Windows, macOS, and Linux
- Using PyInstaller for cross-platform builds
- Distribution and sharing
Quick start:
# macOS / Linux
./scripts/build.sh
# Windows
scripts\build.bat
Your executable will be in the dist/ folder.
Reporting Issues
Use GitHub Issues to report bugs or suggest features:
- Search existing issues first
- Provide:
- Steps to reproduce
- Expected behavior
- Actual behavior
- Environment (Pi model, OS, camera, etc.)
Questions?
- Check FAQ
- Review Troubleshooting
- Open GitHub discussion or issue
Thank you for contributing!