Skip to content
Merged
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
12 changes: 6 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ LOG_LEVEL=info
CAMERA_BACKEND=picamera2

# Frontend Configuration
# Set HOST_IP to the Pi's local IP for network access (e.g. 10.0.0.133).
# Leave unset or use localhost for local dev — docker-compose.yml defaults to localhost.
# Set HOST_IP to the Pi's hostname or IP for network access (e.g. digitool.local or 10.0.0.133).
# Leave unset or use localhost for local dev.
HOST_IP=localhost
PUBLIC_API_BASE=http://${HOST_IP}:8000
# Add the Pi's IP here so the backend accepts requests from network devices.
# For local dev, the defaults (localhost:3000 and :5173) are used automatically.
CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"]
# Browser-side API calls are routed through Nginx (/api → backend:8000)
PUBLIC_API_BASE=http://${HOST_IP}/api
# Add origins for both the port-80 Nginx URL and the direct port-3000 fallback.
CORS_ORIGINS=["http://localhost","http://localhost:5173","http://localhost:3000"]
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ _logs/
temp/

# Working files
docs/RP-008156-DS-2-picamera2-manual.txt
docs/*.txt

# External tools
fisqua/
fisqua/

# Symlinks
dtk-sym
logs-sym
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project uses pre-1.0 semantic versioning.

## [0.0.0-pre.2] - 2026-05-31

### Added

- **DSLR camera support via gPhoto2** — Canon EOS and compatible USB cameras now work alongside picamera2. Backend auto-selects via `CAMERA_BACKEND=gphoto2` env var.
- **RAW (CR2) capture** — Selecting RAW or RAW+JPEG format now correctly saves `.cr2` files. Previously the camera silently fell back to JPEG on every capture.
- **Orientation / rotation control** — Per-camera post-capture rotation (0°, 90°, 180°, 270° CW) for scanning manuscripts in portrait orientation. JPEG captures are pixel-rotated via Pillow; CR2 files receive an EXIF Orientation tag via piexif so RAW converters auto-rotate.
- **Live preview rotation** — The camera feed in the live-preview viewport reflects the selected rotation in real time so operators can frame shots correctly before capturing.
- **Floating rotation buttons on each camera feed** — Quick-access ↺ / ↻ buttons float over each camera feed (bottom-centre). Clicking them updates both the live preview rotation and the capture rotation without opening the sidebar. State stays in sync with the sidebar orientation control.
- **Nginx reverse proxy** — All traffic now routes through Nginx on port 80 (`/api/*` → backend, `/*` → frontend). Eliminates hard-coded port references in the browser.
- **Network discoverability** — Raspberry Pi is accessible on the local network by hostname (`digitool.local`) and by IP. `HOST_IP` and CORS origins are configured dynamically at startup.
- **Documentation service** — Quarto-based docs rendered and served via Docker; includes device-setup guides for CM4 and Pi 5 + IMX519.
- **SD card distribution guide** — New developer documentation for distributing pre-imaged SD cards.
- **gPhoto2 device-setup guide** — Step-by-step instructions for connecting Canon DSLR cameras.

### Fixed

- **CR2 thumbnail in record viewer** — The modal image viewer (`img-viewer-frame`) now shows a renderable JPEG instead of sending the `.cr2` file directly to the browser (which browsers cannot display). The `_preview.jpg` sidecar is served when available.
- **`image_format` reverse mapping** — Camera configuration now correctly returns human-readable values (`JPEG`, `RAW`, `RAW+JPEG`) instead of internal PTP strings.
- **Capture path mismatch** — `GPhoto2Backend.capture_image()` previously returned the originally requested `.jpg` path even when the camera had saved a `.cr2` file. It now returns the actual saved path.
- **imageformat reset on every capture** — `apply_dslr_config()` was overwriting the camera's format setting to JPEG on every shot when no explicit format was configured. It now skips the PTP write when `image_format` is `None`, leaving the camera's own setting intact.
- **Record `format` field hardcoded to `"jpg"`** — The format stored in the database now reflects the actual file extension (e.g. `"cr2"`).

### Notes

- Project remains pre-alpha pending replication and validation on additional machines.
- Default capture orientation is 90° (portrait) to match typical book/manuscript scanning setups.
- piexif must be present in the pixi environment (`pixi run python -c "import piexif"`) for CR2 EXIF rotation to apply.

## [0.0.0-pre.1]

### Added

- Initial pre-alpha release baseline.

[0.0.0-pre.1]: https://github.com/UCSB-AMPLab/digitization-toolkit/releases/tag/pre-alpha
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ An open-source, modular digitization toolkit designed for low-cost, high-quality
> Project currently in development. Kick-off: September 2025
> Alpha prototype planned for deployment at SBMAL in June 2026.

## Release History

See [CHANGELOG.md](CHANGELOG.md) for version history and release notes.

***

## Quick Start
Expand Down
24 changes: 24 additions & 0 deletions docker-compose.pi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,27 @@ services:
# Override portable folders with appliance paths
- /var/lib/dtk:/var/lib/dtk
- /var/log/dtk:/var/log/dtk

frontend:
environment:
# Route browser-side API calls through Nginx (same-origin, fixes Chrome PNA policy)
# http://digitool.local/api/* → Nginx strips /api → backend:8000/*
- PUBLIC_API_BASE=http://${HOST_IP:-localhost}/api
# ORIGIN must match the URL users open (port 80 via Nginx, no explicit port)
- ORIGIN=http://${HOST_IP:-localhost}

# ── Nginx reverse proxy ────────────────────────────────────────────────────
# Puts frontend (:3000) and backend (:8000) behind a single origin (port 80).
# This eliminates the cross-port request that Chrome's Private Network Access
# (PNA) policy blocks when the client is on a .local HTTP address.
# Access the app at: http://digitool.local (instead of :3000)
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- frontend
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ services:
- NODE_ENV=production
# No depends_on db — the frontend is a Node.js API proxy and never connects
# to Postgres directly. Removing this lets db and frontend start in parallel.
# No depends_on db — the frontend is a Node.js API proxy and never connects
# to Postgres directly. Removing this lets db and frontend start in parallel.

docs:
build:
context: ./docs
dockerfile: Dockerfile
ports:
- "8080:80"

volumes:
postgres_data:
4 changes: 4 additions & 0 deletions docs/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Exclude pre-rendered output and cache from build context.
# The Dockerfile re-renders docs fresh with `quarto render`.
.quarto/
**/*.quarto_ipynb
8 changes: 8 additions & 0 deletions docs/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM ghcr.io/quarto-dev/quarto:1.9.37 AS build
COPY . /site
WORKDIR /site
RUN quarto render --output-dir /site

FROM nginx:alpine
COPY site/ /usr/share/nginx/html
EXPOSE 80
7 changes: 7 additions & 0 deletions docs/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ website:
contents:
- href: users/external-storage.qmd
text: "External Storage"
- section: "Cameras Setup"
contents:
- href: users/setting-up-gphoto2-cams.qmd
- section: "Developer Guides"
contents:
- section: "Hardware Setup Guides"
Expand All @@ -35,6 +38,10 @@ website:
text: "Device Setup: Raspberry Pi CM4 with Cameras"
- href: developers/device_setup_pi5_imx519.qmd
text: "Device Setup: Raspberry Pi 5 with IMX519 Camera"
- section: "Software Setup Guides"
contents:
- href: developers/sd_card_distribution.qmd
text: "SD Card Distribution"
page-footer:
center: "Documentation site built: {{< meta date >}}"

Expand Down
Binary file added docs/_static/imgs/cannon-EOS-RebelT7-Mode.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/_static/imgs/cannon-EOS-RebelT7-flash.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/_static/imgs/cannon-EOS-RebelT7-lensMF.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/_static/imgs/cannon-EOS-RebelT7-usb4cam.JPG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/_static/imgs/cannon_EOS-RebelT7-usb2RPi.JPG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/developers/device_setup_CM4.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
title: "Raspberry Pi Compute Module 4 Dual Camera Setup"
date-modified: last-modified
format: html
lightbox: true
---

***
Expand Down
1 change: 1 addition & 0 deletions docs/developers/device_setup_pi5_imx519.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
title: "Raspberry Pi 5 IMX519 Camera Setup"
date-modified: last-modified
format: html
lightbox: true
---

***
Expand Down
156 changes: 156 additions & 0 deletions docs/developers/sd_card_distribution.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
title: "SD Card Distribution"
date-modified: last-modified
format: html
---

***

This guide explains how to prepare and ship a Raspberry Pi SD card image with the Digitization Toolkit pre-installed, so the recipient can plug it in and access the application from any device on the same network — without any manual IP configuration.

---

## How It Works

The Raspberry Pi broadcasts its hostname over the local network using **mDNS** (via the Avahi daemon, pre-installed on Raspberry Pi OS). This means any device on the same network can reach the Pi at `http://<hostname>.local` without knowing its IP address.

The application is configured to use this hostname instead of a hardcoded IP, so the image works on any network regardless of what IP the router assigns.

---

## Preparing the Master Image

### 1. Set a meaningful hostname

Choose a hostname that identifies the device. The current convention is `digitool`:

```bash
sudo hostnamectl set-hostname digitool
```

Verify:

```bash
hostname
# digitool
```

The Pi will be reachable at `http://digitool.local` on the destination network.

### 2. Confirm Avahi is running

Avahi (mDNS) is pre-installed on Raspberry Pi OS. Confirm it is active:

```bash
systemctl is-enabled avahi-daemon
systemctl is-active avahi-daemon
```

Both should return `enabled` / `active`. If not:

```bash
sudo systemctl enable --now avahi-daemon
```

### 3. Update `.env` to use the hostname

In `/home/pi/dtk/.env`, set `HOST_IP` to the `.local` hostname:

```bash
HOST_IP=digitool.local
CORS_ORIGINS=["http://digitool.local:5173","http://digitool.local:3000","http://localhost:5173","http://localhost:3000"]
```

`PUBLIC_API_BASE` resolves automatically from `HOST_IP`:

```bash
PUBLIC_API_BASE=http://${HOST_IP}:8000
```

### 4. Rebuild and verify the frontend container

The `PUBLIC_API_BASE` variable is baked into the compiled frontend at build time. After editing `.env`, rebuild:

```bash
cd /home/pi/dtk
docker compose -f docker-compose.yml -f docker-compose.pi.yml up -d --build --force-recreate
```

Open `http://digitool.local:3000` from another device on the same network to confirm it loads.

### 5. Image the SD card

Once the system is working correctly, shut down the Pi cleanly and image the SD card from another machine:

**macOS / Linux:**
```bash
# Identify the SD card device (e.g. /dev/disk4 or /dev/sdb)
diskutil list # macOS
lsblk # Linux

# Create compressed image
sudo dd if=/dev/sdX bs=4M status=progress | gzip > dtk-$(date +%Y%m%d).img.gz
```

**Windows:** Use [Raspberry Pi Imager](https://www.raspberrypi.com/software/) or [Win32DiskImager](https://win32diskimager.org/) to read the card to a `.img` file.

::: {.callout-tip}
## Shrink before imaging
Tools like [PiShrink](https://github.com/Drewsif/PiShrink) can reduce the image size significantly before compression by trimming unused filesystem space.
:::

---

## Flashing and Delivering the Image

Flash the image onto a new SD card:

```bash
# Linux / macOS
gzip -dc dtk-YYYYMMDD.img.gz | sudo dd of=/dev/sdX bs=4M status=progress
```

Or use Raspberry Pi Imager → *Use custom image* → select the `.img.gz` file.

---

## What the Recipient Needs to Do

Nothing, in most cases. They:

1. Insert the SD card and power on the Pi.
2. Connect the Pi to their local network (Ethernet or Wi-Fi configured before imaging).
3. Open `http://digitool.local:3000` from any browser on the same network.

::: {.callout-note}
## Wi-Fi pre-configuration
If the destination uses Wi-Fi (not Ethernet), configure the network credentials on the SD card **before** shipping. The easiest way is via Raspberry Pi Imager's *Advanced options* when flashing, or by placing a `wpa_supplicant.conf` in the `/boot` partition after flashing.
:::

---

## Troubleshooting

**`digitool.local` doesn't resolve**

- Windows requires [Bonjour](https://support.apple.com/kb/DL999) (installed automatically with iTunes or iCloud). Windows 10 (1803+) supports mDNS natively.
- Try pinging: `ping digitool.local`
- As a fallback, find the IP via the router's DHCP client list and use it directly.

**CORS errors in the browser**

The frontend origin must match `CORS_ORIGINS` in `.env`. If you changed the hostname or port, update that list and restart the backend:

```bash
cd /home/pi/dtk/backend && pixi run dev
```

**Two Pis on the same network**

mDNS hostnames must be unique per network. Change the hostname on one of them:

```bash
sudo hostnamectl set-hostname digitool-2
```

And update `HOST_IP` and `CORS_ORIGINS` in `.env` accordingly, then rebuild the frontend container.
Loading
Loading