Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ debug_*.py
dist/
build/
*.egg-info/

# Auto-added by Marisol pipeline
.pio/
.gradle/
*.class
local.properties
104 changes: 104 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,130 @@ Thanks, Enjoy!
For the codebase, I built the piDSLM codebase off of a forked a copy of fellow DIYer Martin Manders [MerlinPi project](https://github.com/MisterEmm/MerlinPi). The piDSLM codebase is still in its infancy but it allows the user to take photos/videos, and view them in a gallery. It also allows users to bulk upload the footage to Dropbox. To begin ssh into the Raspberry Pi and run the following command:

```
<<<<<<< Updated upstream

git clone https://github.com/NickEngmann/pidslm.git
cd pidslm
=======
piDSLM/
├── pidslm.py # Main GUI application
├── dropbox_upload.py # Dropbox sync utility with argparse
├── INSTALL.sh # Installation script
├── pidslm.desktop # Auto-start configuration
├── PiDSLR.fzz # 3D enclosure design (Fusion 360)
├── icon/ # UI icons (14 images)
├── requirements.txt # Python dependencies
└── tests/ # Test suite
├── test_example.py # GPIO and I2C hardware tests
├── test_dropbox_filters.py # Dropbox file filtering tests
├── conftest.py # Test configuration with mocks
└── embedded_mocks.py # Hardware simulation mocks
>>>>>>> Stashed changes
```

You're then going to retrieve a Dropbox Access token to enable to Dropbox footage upload feature. To do this go ahead and [go to the Application Developer page on Dropbox](https://www.dropbox.com/developers/apps). Create an application and click the Generate Access Token button to generate your access token.

<<<<<<< Updated upstream
Then replace the dummy access token in Dropbox_upload.py with your new access token.
=======
The project includes a comprehensive test suite with hardware mocking:
>>>>>>> Stashed changes

```

# OAuth2 access token. TODO: login etc.
TOKEN = 'YOUR_ACCESS_TOKEN'
```

<<<<<<< Updated upstream
Finally, run the INSTALL.sh script using the following command
=======
- **RPi.GPIO mocked**: Simulates GPIO pin control
- **I2C/SPI/UART mocked**: Hardware communication simulation
- **guizero mocked**: GUI framework simulation
- **Source loading**: Custom fixture loads and tests actual source files with while-True loops stripped
- **Auto-mocking**: Meta-path finder auto-mocks unknown hardware modules

### Test Coverage

- **Hardware tests** (`test_example.py`): 2 tests covering GPIO pin control and I2C communication
- **Dropbox filter tests** (`test_dropbox_filters.py`): 13 tests covering:
- Dot file filtering (`.hidden_file`, `.gitignore`)
- Temporary file filtering (`.tmp`, `.temp`, `@recycled`, `~backup`)
- Generated file filtering (`.pyc`, `.pyo`)
- Directory filtering (`__pycache__`)
- Edge cases (empty strings, `None` values)
- Normal file acceptance (`.jpg`, `.png`, `.pdf`, `.mp4`)
- Argument parsing (`--count`, `--yes`, `--no` flags)

All 15 tests pass successfully in the mocked environment.
>>>>>>> Stashed changes

```
sudo ./INSTALL.sh
```



<<<<<<< Updated upstream
=======
- **Python 3.12+**
- **Pillow**: Image processing
- **guizero**: GUI framework
- **dropbox**: Dropbox API v2 SDK
- **RPi.GPIO**: Hardware control (production only)
- **raspistill/raspivid**: Camera utilities (production only)

Full dependency list: `requirements.txt` (Pillow, guizero, dropbox, guizero[images])

## Troubleshooting

### Camera not working

1. Check `/boot/config.txt` has `start_x=1` and `gpu_mem=128`
2. Verify camera is connected to CSI port 0
3. Run `vcgencmd get_camera` to check detection

### GPIO button not responding

1. Verify pin 16 wiring (BCM numbering)
2. Check for button bounce issues (currently set to 2500ms)
3. Ensure RPi.GPIO is not being used elsewhere

### Dropbox upload fails

1. Verify TOKEN is set in `dropbox_upload.py`
2. Check internet connection
3. Review Dropbox app permissions

### Gallery not showing photos

1. Ensure photos are saved to `/home/pi/Downloads/`
2. Check file permissions on Downloads directory
3. Verify photo format is `.jpg`

## Design Notes

The piDSLM project is built as a fork of the [MerlinPi project](https://github.com/MisterEmm/MerlinPi) by Martin Manders. Key differences:

- Custom enclosure design (3D printable)
- MHS35-TFT display integration
- Enhanced GUI layout
- Bulk Dropbox upload feature
- GPIO button capture support
- Comprehensive test suite with hardware mocking

## License

This project is built on the MerlinPi foundation. Please respect the original project license and any third-party dependencies.

## Support

If you found this project useful, consider supporting the design work through [PayPal](https://paypal.me/nickengman).

For questions or contributions, please reach out via the [GitHub repository](https://github.com/NickEngmann/piDSLM).

---

*Designed for Raspberry Pi enthusiasts and DIY camera projects.*
>>>>>>> Stashed changes
110 changes: 94 additions & 16 deletions dropbox_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,29 +159,107 @@ def download(dbx, folder, subfolder, name):
return data

def upload(dbx, fullname, folder, subfolder, name, overwrite=False):
"""Upload a file.
Return the request response, or None in case of error.
"""Upload a file to Dropbox.

Args:
dbx: Dropbox client instance
fullname: Full path to the local file to upload
folder: Dropbox folder name
subfolder: Subfolder within Dropbox folder (can be empty)
name: Filename to use in Dropbox
overwrite: Whether to overwrite if file exists

Returns:
dict: Upload result with status information, or error dict on failure

Upload result contains:
- status: 'success' or 'error'
- file_path: Dropbox path of uploaded file
- bytes_uploaded: Number of bytes uploaded
- message: Description of result
"""
path = '/%s/%s/%s' % (folder, subfolder.replace(os.path.sep, '/'), name)
while '//' in path:
path = path.replace('//', '/')

mode = (dropbox.files.WriteMode.overwrite
if overwrite
else dropbox.files.WriteMode.add)
mtime = os.path.getmtime(fullname)
with open(fullname, 'rb') as f:
data = f.read()
with stopwatch('upload %d bytes' % len(data)):
try:
res = dbx.files_upload(
data, path, mode,
client_modified=datetime.datetime(*time.gmtime(mtime)[:6]),
mute=True)
except dropbox.exceptions.ApiError as err:
print('*** API error', err)
return None
print('uploaded as', res.name.encode('utf8'))
return res

try:
# Get file size before upload
file_size = os.path.getsize(fullname)
mtime = os.path.getmtime(fullname)

with open(fullname, 'rb') as f:
data = f.read()

with stopwatch('upload %d bytes' % len(data)):
try:
res = dbx.files_upload(
data, path, mode,
client_modified=datetime.datetime(*time.gmtime(mtime)[:6]),
mute=True)
print('Uploaded %s (%d bytes) -> %s' % (name, file_size, res.name))
return {
'status': 'success',
'file_path': path,
'bytes_uploaded': file_size,
'message': 'File uploaded successfully'
}
except dropbox.exceptions.ApiError as err:
error_msg = str(err)
if 'path' in error_msg and 'not_found' in error_msg:
print('Error: Could not create subfolder, creating...')
# Try creating parent folder first
parent_path = '/'.join(path.split('/')[:-1])
try:
dbx.files_create_folder_v2(parent_path)
print('Created folder:', parent_path)
# Retry upload
res = dbx.files_upload(
data, path, mode,
client_modified=datetime.datetime(*time.gmtime(mtime)[:6]),
mute=True)
print('Uploaded %s (%d bytes) -> %s' % (name, file_size, res.name))
return {
'status': 'success',
'file_path': path,
'bytes_uploaded': file_size,
'message': 'File uploaded successfully after folder creation'
}
except Exception as folder_err:
print('*** Failed to create folder:', folder_err)
return {
'status': 'error',
'file_path': path,
'bytes_uploaded': 0,
'message': 'Failed to create folder: ' + str(folder_err)
}
else:
print('*** API error uploading %s: %s' % (name, error_msg))
return {
'status': 'error',
'file_path': path,
'bytes_uploaded': 0,
'message': 'Upload failed: ' + error_msg
}
except IOError as err:
print('*** IO error reading file %s: %s' % (fullname, err))
return {
'status': 'error',
'file_path': path,
'bytes_uploaded': 0,
'message': 'Failed to read file: ' + str(err)
}
except Exception as err:
print('*** Unexpected error uploading %s: %s' % (fullname, err))
return {
'status': 'error',
'file_path': path,
'bytes_uploaded': 0,
'message': 'Unexpected error: ' + str(err)
}

def yesno(message, default, args):
"""Handy helper function to ask a yes/no question.
Expand Down
Loading