Shared-tablet install¶
A step-by-step guide to putting DDB on a Linux tablet that several biologists share, with each biologist logging in under their own OS account. This is the model used at Otago on a wall-mounted Wacom-One tablet for four users; nothing in it is specific to that hardware.
The end state:
- One SQLite database at
/srv/ddb/ddb.sqlite3, read and written by every biologist under their own OS identity. WAL mode + addbgroup give concurrent GUI sessions safe coexistence. - Every biologist has their own conda env and repo clone under their
own
$HOME, but points at the shared DB vialocal_paths.json. - The Brother QL-820NWB label printer and the tablet's USB cameras work for every biologist without per-session reconfiguration.
- A system-level
systemdtimer snapshots the DB hourly, content-dedupes, and pushes changes to Google Drive viarclone. It runs regardless of who is (or isn't) logged in.
The guide is roughly in dependency order — steps 2–6 can be repeated per new user; steps 1 and 7 are once per tablet.
Idempotent scripts
Every provisioning script in scripts/ diffs against the current
state and only makes changes when the target differs from the
source. Safe to re-run after git pull or when adding another
user.
1. Shared database at /srv/ddb/¶
Do this once per tablet, as an admin.
sudo bash scripts/setup_shared_sqlite.sh
What it does:
- Creates
/srv/ddb/and/srv/ddb/env-packs/with mode2775and groupddb(the setgid bit means new files inherit the group automatically). - Creates the
ddbUnix group if it doesn't exist. - Adds an empty
ddb.sqlite3in that directory so a fresh install canalembic upgrade headagainst it.
Then add every biologist to the ddb group:
sudo usermod -aG ddb alice
sudo usermod -aG ddb bob
# ... they need to log out and back in for group membership to apply.
Point DDB at the shared paths — this is the committed template at the repo root:
cp local_paths.template.json local_paths.json
$EDITOR local_paths.json
# → set "active_profile": "shared_tablet"
Migrate the schema against the shared file (once, from any admin
account that has the ddb env installed):
DDB_DATABASE_URL=sqlite:////srv/ddb/ddb.sqlite3 alembic upgrade head
Path resolution inside DDB is env var → active profile → in-repo
default, so local_paths.json is authoritative on the tablet.
Restoring an existing DB
If you're migrating from a pre-existing single-user DDB install,
copy the old .sqlite3 file into /srv/ddb/ddb.sqlite3 before
running alembic upgrade head — otherwise the migration will
happen on an empty DB and you'll lose all vials and audit history.
2. Per-user install¶
Do this once per biologist, from their own account (no sudo).
curl -sSL \
https://raw.githubusercontent.com/zerotonin/drosodb/main/scripts/install_user.sh \
| bash
The one-liner:
- Clones
drosodbto$HOME/PyProject/drosodb. - Creates a
ddbconda env in the user's own Miniforge (or falls back tominiconda3if that's what they have). - Installs the package editable with
pip install -e ".[gui,hardware,dev]". - Drops a launcher at
~/.local/bin/ddb-launcher+ a spec-compliant~/Desktop/ddb.desktopicon.
Speeding up user #2 and beyond
conda-forge downloads through the campus link collapse to ~100 KB/s when two conda processes run at the same time. Build an offline env-pack once — the README's Offline env pack section covers the one-time snapshot workflow — and later biologists' installs finish in seconds instead of tens of minutes.
3. Bluetooth printer access¶
The QL-820NWB pairs once at the OS level, then DDB opens an RFCOMM
channel to it from Python. Both halves need per-user setup: OS-level
group membership, and an app-level .env telling DDB the MAC address.
3a. Bluetooth group membership (admin, once per user)¶
sudo usermod -aG bluetooth alice
sudo usermod -aG bluetooth bob
# → they need to log out and log back in for it to take effect
BlueZ refuses D-Bus + RFCOMM socket access to users outside the
bluetooth group, so this is a hard prerequisite. Without it, DDB's
print calls fail with permission errors even if the .env is
correctly set up.
3b. Pair the printer (once per tablet)¶
The printer only needs to be paired to the tablet once, using any GUI
Bluetooth manager or bluetoothctl. Because the bond is stored in
/var/lib/bluetooth/ (system-wide), all users see the paired printer
after step 3a — they don't each need to re-pair.
If pairing has drifted or the printer refuses to reconnect, the GUI's
Settings → Printer reconnect dialog has a guided flow (reset bond,
restart BT service, re-pair) that works from any biologist's account
provided they're in the bluetooth group.
3c. Printer .env per user¶
DDB's per-user config lives in .env inside each biologist's repo
clone. Two paths are equivalent — pick whichever fits.
Self-service — each biologist runs this from their own account, no sudo needed:
bash ~/PyProject/drosodb/scripts/setup_my_printer.sh
The script:
- Finds the biologist's repo clone (default
$HOME/PyProject/drosodb, falls back to a search). - Writes
.envwith the tablet's printer settings. - Runs a fresh
python -c "from ddb.config import settings; ..."so the biologist can see what pydantic actually loaded. - Checks
bluetoothgroup membership and prints the admin fix if missing.
Admin batch — for provisioning several users in one root shell:
sudo bash scripts/write_user_printer_env.sh alice bob
# → with no args, defaults to the current tablet's user list
Both are idempotent. When something still doesn't work:
sudo bash scripts/diagnose_user_printer.sh
# → per user: clone location, .env content, cold-Python view,
# bluetooth-group status, whether a stale GUI is holding old settings
Restart the GUI after .env changes
pydantic-settings reads .env once at process start. If the
biologist has DDB already open when the .env is written, they
must fully close and reopen the GUI — closing the tab isn't
enough.
4. Cameras¶
DDB uses two USB webcams (front + back) via V4L2 for QR scanning.
Every user needs two things: OS-level access to /dev/video*, and
DDB's per-user role → USB-bus-path mapping in
~/.config/ddb/cameras.json.
4a. OS-level camera access¶
On modern systemd/logind Ubuntu, /dev/video* is granted to the
currently active login session via a uaccess ACL. On a
single-user session this "just works", but shared-tablet setups have
two failure modes:
- Fast user switching can leave the just-switched-to session
marked as
onlinerather thanactive, so no ACL is installed. - Multiple concurrent sessions (e.g. SSH plus a graphical login) only ACL the console one.
The bulletproof fix is a static video group membership — check
whether it's needed first:
# Is the biologist's session properly active?
loginctl user-status <username> | grep -E 'State|Active'
# → "State: active" means uaccess should be working already
If sessions are consistently active, no group change is required.
If any test call fails with Permission denied on /dev/video*:
sudo usermod -aG video alice
sudo usermod -aG video bob
# → log out + back in
4b. Camera role assignments per user¶
Which physical camera is the "front" and which the "back" is stored
in ~/.config/ddb/cameras.json as a role → USB bus_path map:
{"front": "1-6.1", "back": "1-6.2"}
USB bus paths are tied to the physical USB port on the tablet — they
survive reboots and camera swaps, and they're identical for every
user on the same tablet. So the admin's cameras.json can be
copied verbatim into every biologist's home.
# The admin (or any user whose own cameras.json is correct) runs:
sudo bash scripts/write_user_camera_config.sh
# → source: the invoking sudo user's ~/.config/ddb/cameras.json
# → targets: current tablet's user list
Alternate flow — a biologist runs ddb camera assign interactively
in their own terminal to pick which physical device plays each role.
That's the fallback if there's no admin cameras.json to copy from.
No GUI restart needed
The Scan tab re-reads cameras.json every time the user clicks
Start. After the config is written the biologist just clicks
Start; they don't need to close and reopen the GUI.
5. Where DDB actually reads and writes¶
A reference table so you know which file each biologist owns and which is shared. All paths that look project-relative resolve against the repo clone in each user's home.
| Path | Owner | Purpose |
|---|---|---|
/srv/ddb/ddb.sqlite3 |
shared (root:ddb 664) | The one and only DB. Every biologist writes here. |
/srv/ddb/backups/* |
shared (root:ddb 2775) | Hourly snapshot ladder — see §6. |
/srv/ddb/env-packs/*.tar.gz |
shared (root:ddb 2775) | Offline conda env packs for fast onboarding. |
$REPO/local_paths.json |
admin (committed template, gitignored real) | Points DDB at the shared paths on this tablet. |
$HOME/PyProject/drosodb/ |
user | The biologist's git clone + editable install. |
$HOME/PyProject/drosodb/.env |
user | Per-user config — printer MAC, camera defaults, etc. |
$HOME/.config/ddb/cameras.json |
user | Per-user camera role → bus_path map. |
$HOME/miniforge3/envs/ddb/ |
user | The biologist's conda env. |
/etc/ddb/backup.env |
admin | Backup timer's config — retention, rclone remote. |
/etc/ddb/rclone.conf |
admin (root 0644) | rclone remote credentials for offsite push. |
/usr/local/sbin/ddb-backup |
admin (root 0755) | The backup script itself, installed from scripts/. |
/etc/systemd/system/ddb-backup.{timer,service} |
admin | Systemd units triggering the backup. |
6. Off-device backup¶
The full walkthrough is in operations.md. Recap so this guide stands on its own:
6a. Install the system timer¶
sudo bash scripts/setup_system_backup.sh
Installs /usr/local/sbin/ddb-backup, drops the systemd units, and
enables ddb-backup.timer. From now on the backup fires every hour
at :00, independent of any user session.
6b. Enable offsite push to Google Drive¶
Local snapshots survive DB corruption but not a stolen tablet. To also push to Google Drive:
sudo apt install rclone
sudo rclone --config /etc/ddb/rclone.conf config
# → wizard: n (new) → name "gdrive" → drive → blank client_id/secret
# → scope 1 (full) → auto config → open browser, approve
Then wire the remote into the timer:
sudo $EDITOR /etc/ddb/backup.env
# → set:
# DDB_BACKUP_RCLONE_CONFIG=/etc/ddb/rclone.conf
# DDB_BACKUP_RCLONE_REMOTE=gdrive:drosodb-backup
sudo rclone --config /etc/ddb/rclone.conf mkdir gdrive:drosodb-backup
# First fire — pushes the full history (~34 MB) as the seed
sudo systemctl start ddb-backup.service
journalctl -u ddb-backup.service -n 30 --no-pager
6c. Content-addressed dedupe¶
The backup script SHA-256s each new snapshot and compares against
.last-hash (the digest of the last shipped one). If the content
matches, the local candidate is discarded, the rclone push is skipped,
and only ddb.latest.sqlite3's mtime is touched to prove "we
checked, still current".
Practical consequence: on hours when the DB didn't change, zero bytes hit Google Drive. Weekend + overnight traffic is nil; a busy day pushes a handful of ~200 KB deltas.
Retention counts unique states, not hours
DDB_BACKUP_RETAIN_HOURLY (default 168) counts change events,
not clock hours. On a low-turnover DB, the 168-entry ladder can
cover months rather than one week — always tracing real changes,
never idle repeats.
7. Troubleshooting cheat sheet¶
Print button greyed out¶
Run the diagnostic — it names the specific fix per row:
sudo bash scripts/diagnose_user_printer.sh
Most common causes, ranked:
- GUI was open when
.envwas written — pydantic reads.envonce at process start. Close and reopen the GUI. - Biologist not in
bluetoothgroup —sudo usermod -aG bluetooth <user>and log out + back in. .envin the wrong repo (biologist has multiple clones) — the discovery search picks the firstpyproject.tomlnamingddbit finds; check the diagnostic's "found repo" line.
Scan tab shows no cameras / "No camera assigned to role 'back'"¶
~/.config/ddb/cameras.json is missing. Copy from the admin:
sudo bash scripts/write_user_camera_config.sh <user>
Then the biologist clicks Start in the Scan tab again — no GUI restart needed.
unable to open database file in the backup log¶
The ddb-backup.service unit needs ReadWritePaths=/srv/ddb (not
just the subdir), because WAL-mode SQLite touches -wal/-shm
sidecars next to the source DB even for a read-only snapshot. Fixed
in 0cfdf56 — if you see this on an older tablet, git pull and
re-run sudo bash scripts/setup_system_backup.sh.
Backup timer looks like it's firing twice¶
Almost always the old per-user cron still lives alongside the new system timer:
crontab -l | grep -i drosodb
Delete the line (crontab -e) and remove the old script. The
system timer is enough.
Two biologists using the tablet at the same time¶
Supported — SQLite WAL mode lets both GUIs read and write
concurrently. If a write ever gets stuck ("database is locked"), the
busy_timeout (5s) retries automatically. Practical hitting-the-wall
limit is around 3-4 concurrent writers; the tablet form factor caps
this well below that.
8. Not yet supported¶
- Shared-account mode with per-user passwords. The current model assumes each biologist has their own OS account (no login prompt in DDB itself). The plan when a lab wants password-per-user is a Settings toggle that swaps the OS-user resolver for a startup login dialog — filed as a follow-up.
- Automatic printer config propagation. Every user still needs
their own
.envwritten. The natural fix is a shared/etc/ddb/printer.envread on top of the per-user.env— filed as a follow-up. - Camera hot-plug. DDB reads bus paths at Scan-tab startup; if you unplug and replug a camera during a session, restart the GUI to pick up the new device.