# Dashcam Transfer > ⚡ Dit project is **vibecoded** — gebouwd, gedebugd en gehard in een AI-agent sessie. Automatisch dashcam-opnames (MP4) overzetten van een SD-kaart naar een USB-SSD, met organisatie per datum en camera-positie (front/back). Draait op de **nextcloud** server. ## Architectuur ``` SD-kaart insteken → udev detecteert vendor:product (14cd:1212 of 05e3:0751) → systemd-run start /usr/local/bin/dashcam-transfer-mount %k → skip als de transfer-service al draait (flaky lezer = geen 2e mount) → wacht tot /dev/sdX1 verschijnt (max 60s) → mount apparaat dynamisch op /mnt/sdcard (uid=1000, exfat) → read-only fsck.exfat health check (logt, blokkeert niet) → systemctl start dashcam-transfer.service → /usr/local/bin/dashcam-transfer 1. Check mounts (/mnt/sdcard, /mnt/ultratouch) 2. Scan /mnt/sdcard/Novatek/Movie/ op *.MP4 3. Leesbaarheidscheck bron (voorkomt ruimte wissen bij defecte kaart) 4. Check vrije ruimte — verwijder oudste datum-mappen indien nodig 5. Copy + verify + delete per bestand, met retry 6. Early-abort bij aanhoudende I/O-fouten (SD-kaart defect) 7. Stuur Matrix notificatie via sherpa-bot SD-kaart uittrekken → udev REMOVE trigger (zelfde vendor:product) → systemd-run start /usr/local/bin/dashcam-transfer-cleanup (%k) → stale mount? (device-node weg of source = verwijderd device) → stop dashcam-transfer.service → umount /mnt/sdcard (fallback: umount -l) ``` De triggers worden gedefinieerd in `udev/99-dashcam-trigger.rules`. Udev zelf voert de scripts niet uit maar start ze via `systemd-run` — in udev-context ontbreken mount/umount-rechten op Fedora met SELinux in enforcing mode. ## Robuustheid Het script is ontworpen tegen een bekende productie-fout (defecte/flakke SD-kaartlezer die midden in de transfer offline klapt, `I/O error`, `device offline`): - **Echte fout zichtbaar** — kopieerfouten worden als `OSError` met reden gelogd (i.p.v. alleen "exit status 1"); de eerdere `cp`-stderr ging verloren. - **Pre-flight leesbaarheidscheck** — voordat oude datum-mappen op de SSD worden verwijderd, wordt eerst een bestand van de kaart gelezen. Een onleesbare kaart resulteert in een duidelijke Matrix-"GESTOPT" en **niets** wordt gewist. - **Retry per bestand** — 3 pogingen met 2s pauze; vangt transiënte USB-glitches. - **Early-abort** — bij 5 opeenvolgende fouten stopt het script en stuurt het een Matrix-bericht met de laatste fout. Bronbestanden worden nooit verwijderd zonder geverifieerde kopie. - **Geen crash mid-loop** — alle bestandssysteem-calls zijn afgeschermd. - **fsck.exfat** — na mount draait het mount-script een read-only health check; een dirty/defecte kaart wordt daarmee zichtbaar in syslog. - **Geen dubbele runs** — het mount-script skipt als de transfer al actief is. - **Service-user = data-eigenaar (uid 1000)** — de exfat-mounts draaien als `uid=1000,gid=1000`, dus álle bestanden op de SSD zijn eigendom van uid 1000 (`eddy`). De unit draait als `User=eddy`; dat is noodzakelijk want copy2 doet `chmod`/`utime` op het doel, en dat mag alleen de eigenaar. Dit was eerder fout opgebouwd (eerst `root` — bad practice; daarna `aiaiaiaiagent` — leidde tot EPERM op élke kopie). Wie de unit-user aanpast: alleen naar een uid dat eigenaar is van de mount, anders breekt elke transfer met `Operation not permitted`. - **Stale-mount opruiming** — als de kaartlezer wordt uitgetrokken (of de kaart offline klapt) ruimt een udev REMOVE-trigger de mount op: de service wordt gestopt en `/mnt/sdcard` netjes ontkoppeld (lazy als dat mislukt). Zo blijft er nooit een dood device in de `mount`-output hangen. ## Hardware | Component | Mount | Device | Doel | |---|---|---|---| | USB-SSD (4TB, Seagate Ultra Touch) | `/mnt/ultratouch` | `/dev/disk/by-uuid/3027-8ECA` | Opslag | | SD-kaart (via USB reader) | `/mnt/sdcard` | dynamisch via udev `%k` | Bron | SSD wordt gemount via systemd `.automount` unit (permanent aangesloten). SD-kaart wordt gemount door `dashcam-transfer-mount.sh` na udev trigger, met het kernel-device (b.v. `sde1`) als argument — zo werkt het ongeacht het filesystem UUID. ## Bestandsstructuur ### SD-kaart (bron) ``` /mnt/sdcard/Novatek/Movie/ ├── 20260622050346_0000001F.MP4 ← front camera ├── 20260622050346_0000002B.MP4 ← back camera └── ... ``` Bestandsnaam: `YYYYMMDDHHMMSS_.MP4` - `F` = front, `B` = back - ~381 MB per clip (~5 minuten) ### SSD (doel) ``` /mnt/ultratouch/dashcam/ └── YYYYMMDD/ ├── front/ │ └── ...F.MP4 └── back/ └── ...B.MP4 ``` ## Operations-handleiding ### Matrix-berichten Na elke transfer stuurt het script een notificatie naar de beheerder. Er zijn drie basissituaties: | Situatie | Bericht | Actie | |---|---|---| | **Geslaagd** | Succesmelding met aantal bestanden en totaal in GB | Geen actie nodig | | **Voltooid met fouten** | Waarschuwing met aantal bestanden, GB en aantal fouten | De betreffende bestanden blijven op de kaart staan en worden de volgende keer opnieuw geprobeerd. Blijft dit terugkomen → kaart/lezer controleren | | **Gestopt (abort)** | Afbreekmelding met reden en aantal al overgezette bestanden | Zie onderstaande oorzaken | Bij een abort zijn er drie vaste redenen: - **SD-kaart onleesbaar** — de lezer of kaart gaf een I/O-fout, of de kaart werd niet netjes afgemeld (dirty). Geen enkel bronbestand is verwijderd. Controleer de kaart met `fsck.exfat` en probeer een andere lezer/poort. - **Te veel opeenvolgende fouten** — 5 fouten achter elkaar tijdens het kopiëren. Ook hier zijn geen bronbestanden verwijderd; de melding bevat de laatste fout. - **Onvoldoende vrije ruimte** — de SSD had ook na het automatisch verwijderen van oudste datum-mappen te weinig ruimte. Let op: een abort betekent **niet** dat data verloren is. Het script verwijdert een bronbestand alleen nadat een kopie is geverifieerd (zelfde byte-grootte). ### Logging Alle transfer-activiteit wordt geschreven naar `/tmp/dashcam-transfer.log` op nextcloud, in het formaat `[YYYY-MM-DD HH:MM:SS] LEVEL bericht`. De mount- en cleanup-scripts loggen via `logger` naar syslog met tag `dashcam-transfer`. Handige greppatronen voor diagnose: ```bash # Laatste transfer-run tail -50 /tmp/dashcam-transfer.log # Mount/cleanup triggers + fsck-uitvoer (syslog) journalctl -t dashcam-transfer --since today grep dashcam-transfer /var/log/messages # I/O- en USB-fouten van de lezer dmesg | grep -Ei "usb|sd[a-z]|device offline|I/O error" ``` ### Troubleshooting | Symptoom | Waarschijnlijke oorzaak | Actie | |---|---|---| | `GESTOPT: SD-kaart onleesbaar` | Lezer/kaart defect of niet netjes afgemeld | `fsck.exfat -n` draaien, lezer/poort wisselen, kaart opnieuw insteken | | `device offline` / `DID_ERROR` in dmesg | Flakke USB-verbinding, kaart klapt weg | Lezer vervangen, USB-poort wisselen; het script herstelt automatisch bij een nieuwe udev-trigger | | `Geen MP4 bestanden gevonden` in log | Kaart leeg (transfer geslaagd) of verkeerde mapstructuur | Niks mis; controleer op de SSD of bestanden er zijn | | Transfer start niet na insteken | Service draait al (is-active gate) of device verscheen niet binnen 60s | Logs checken; als het device nooit komt, lezer opnieuw insteken | | Stale mount in `mount`-output | REMOVE-trigger miste (lezer trok in één keer beide devices weg) | Cleanup-script handmatig draaien (zie herstel) | | `Matrix notificatie mislukt` in log | sherpa-token/room-file onleesbaar voor service-user of Matrix-homeserver onbereikbaar | `~/whatsed-new/secrets/sherpa-token` + `eddy-report-room` controleren (mode 640, group eddy; service draait als uid 1000), `https://whatsed.eddydevink.nl` checken | | Abort: onvoldoende vrije ruimte | SSD vol, opschonen hielp niet | Oude datum-mappen handmatig opschonen of SSD uitbreiden | ### Herstelprocedures ```bash # Handmatige transfer (zonder udev-trigger) /usr/local/bin/dashcam-transfer # Alleen de eerste N bestanden (testen) /usr/local/bin/dashcam-transfer --limit 10 # Read-only health check van de kaart (niet-mounted apparaat nodig) fsck.exfat -n /dev/sde1 # Stale mount opruimen (als de udev-trigger het niet deed) /usr/local/bin/dashcam-transfer-cleanup # of handmatig: umount -l /mnt/sdcard # Transfer-service handmatig starten systemctl start dashcam-transfer ``` ## Configuratiereferentie ### `dashcam-transfer.py` | Constante | Waarde | Doel | |---|---|---| | `SD_CARD` | `/mnt/sdcard` | Mountpoint SD-kaart | | `SOURCE_DIR` | `/mnt/sdcard/Novatek/Movie` | Locatie MP4-bestanden op de kaart | | `SSD` | `/mnt/ultratouch` | Mountpoint USB-SSD | | `TARGET_BASE` | `/mnt/ultratouch/dashcam` | Doelmap voor overgezette opnames | | `LOG_FILE` | `/tmp/dashcam-transfer.log` | Logbestand | | `LOCK_FILE` | `/tmp/dashcam-transfer.lock` | Lockbestand tegen dubbele runs | | `CAMERA_MAP` | `F→front`, `B→back` | Vertaling van naam-suffix naar submap | | `FILE_PATTERN` | `^\d{8}\d{6}_\d+[FB]\.MP4$` | Herkent geldige dashcam-bestandsnamen | | `MAX_RETRIES` | `3` | Kopieerpogingen per bestand | | `RETRY_DELAY` | `2` | Pauze tussen pogingen (seconden) | | `MAX_CONSECUTIVE_ERRORS` | `5` | Aantal opeenvolgende fouten vóór abort | | `MATRIX_API` | `https://whatsed.eddydevink.nl/_matrix/client/v3` | Matrix homeserver client API | | `MATRIX_TOKEN_FILE` | `/home/aiaiaiaiagent/whatsed-new/secrets/sherpa-token` | Compat-token van `@sherpa:whatsed.eddydevink.nl` | | `MATRIX_ROOM_FILE` | `/home/aiaiaiaiagent/whatsed-new/secrets/eddy-report-room` | DM sherpa → Eddy (Systeemrapportage) | ### `dashcam-transfer-mount.sh` | Variabele | Waarde | Doel | |---|---|---| | `MAX_WAIT` | `60` | Maximaal wachten (s) op het device na trigger | | `DASHCAM_MAX_WAIT` | — | Omgevingsvariabele om bovenstaande te overschrijven | ### Systeemconfiguratie | Onderdeel | Locatie | |---|---| | udev-rules | `/etc/udev/rules.d/99-dashcam-trigger.rules` | | Mount-script | `/usr/local/bin/dashcam-transfer-mount` | | Cleanup-script | `/usr/local/bin/dashcam-transfer-cleanup` | | Transfer-service | `/etc/systemd/system/dashcam-transfer.service` | | SD-kaart UUID | `00D3-7715` | | SSD UUID | `3027-8ECA` | ## Matrix notificatie De notificatie wordt verstuurd als `@sherpa:whatsed.eddydevink.nl` naar de bestaande DM "Systeemrapportage" met `@eddy:whatsed.eddydevink.nl` (zelfde kanaal als watchdog-rapportage, Home Assistant en Uptime Kuma): | Config | Waarde | |---|---| | API | `https://whatsed.eddydevink.nl/_matrix/client/v3` | | Bot-account | `@sherpa:whatsed.eddydevink.nl` (compat-token `mct_…`) | | Tokenbestand | `/home/aiaiaiaiagent/whatsed-new/secrets/sherpa-token` (mode 640, group eddy — leesbaar door service-user eddy) | | Roombestand | `/home/aiaiaiaiagent/whatsed-new/secrets/eddy-report-room` (mode 640, group eddy) | | Bericht | `PUT /rooms//send/m.room.message/…` met `m.text`-event | Token en room-ID blijven buiten Git; de bestanden worden bij elke notificatie gelezen. Een mislukte notificatie wordt alleen gelogd en breekt de transfer niet af. Verloopt het token → nieuw compat-token uitgeven met `mas-cli manage issue-compatibility-token sherpa` en het bestand bijwerken (zie [[Homelab/Software Infrastructuur/Matrix Bots]]). ## Repo-structuur ``` dashcam-transfer/ ├── dashcam-transfer.py Hoofdscript: transfer + Matrix-notificatie ├── dashcam-transfer-mount.sh Udev add-trigger: wachten, mount, fsck, service start ├── dashcam-transfer-cleanup.sh Udev remove-trigger: stale mount opruimen ├── udev/ │ └── 99-dashcam-trigger.rules Bron van de geïnstalleerde udev-rules ├── tests/ │ ├── test_dashcam_transfer.py 20 unittest-testen (Python) │ ├── test_mount_script.sh 7 functionele checks (bash) │ └── test_cleanup_script.sh 12 functionele checks (bash) └── README.md Deze documentatie ``` ## Service & triggers ### Udev rule (`/etc/udev/rules.d/99-dashcam-trigger.rules`) ```udev ACTION=="add", SUBSYSTEM=="block", KERNEL=="sd*[0-9]", ENV{ID_VENDOR_ID}=="14cd", ENV{ID_MODEL_ID}=="1212", RUN+="/usr/bin/systemd-run --no-block --collect /usr/local/bin/dashcam-transfer-mount %k" ACTION=="add", SUBSYSTEM=="block", KERNEL=="sd*[0-9]", ENV{ID_VENDOR_ID}=="05e3", ENV{ID_MODEL_ID}=="0751", RUN+="/usr/bin/systemd-run --no-block --collect /usr/local/bin/dashcam-transfer-mount %k" ACTION=="remove", SUBSYSTEM=="block", KERNEL=="sd*[0-9]", ENV{ID_VENDOR_ID}=="14cd", ENV{ID_MODEL_ID}=="1212", RUN+="/usr/bin/systemd-run --no-block --collect /usr/local/bin/dashcam-transfer-cleanup %k" ACTION=="remove", SUBSYSTEM=="block", KERNEL=="sd*[0-9]", ENV{ID_VENDOR_ID}=="05e3", ENV{ID_MODEL_ID}=="0751", RUN+="/usr/bin/systemd-run --no-block --collect /usr/local/bin/dashcam-transfer-cleanup %k" ``` `%k` geeft het kernel device (b.v. `sde1`) door aan het mount- en cleanup-script, zodat de kaart dynamisch gemount wordt ongeacht het filesystem UUID. Na wijziging: `sudo udevadm control --reload-rules`. ### Mount script (`/usr/local/bin/dashcam-transfer-mount.sh`) Wacht tot het device beschikbaar is, unmount eventuele stale mounts, mount de SD-kaart en start de service. Sluit vroegtijdig af als de service al actief is (een flakke lezer mag nooit een draaiende transfer onderbreken). ### Cleanup script (`/usr/local/bin/dashcam-transfer-cleanup.sh`) Draait op udev REMOVE-trigger. Checkt of `/mnt/sdcard` een stale mount is (device-node weg of gemount op een verwijderd device) en ruimt hem dan op: service stoppen, `umount` (fallback `umount -l`). Bij een gezonde mount (ander device) doet het niets. ### Systemd service (`/etc/systemd/system/dashcam-transfer.service`) ```ini [Unit] Description=Dashcam SD card transfer After=mnt-sdcard.mount [Service] Type=oneshot # /tmp-log en -lock erven de identiteit van de eerste aanmaker; bij een # service-user-wissel zou de import-time logging crashen (PermissionError, # 2026-10-15). ExecStartPre (als root) maakt ze elke start schrijfbaar voor # élke user, zodat het script nooit aan identiteit-artefacten breekt. ExecStartPre=/usr/bin/touch /tmp/dashcam-transfer.log /tmp/dashcam-transfer.lock ExecStartPre=/usr/bin/chmod 666 /tmp/dashcam-transfer.log /tmp/dashcam-transfer.lock ExecStart=/usr/local/bin/dashcam-transfer # uid 1000 (eddy): eigenaar van de exfat-SSD (mount uid=1000) — copy2 heeft # eigenaarschap nodig voor chmod/utime. Niet root, niet een andere user. User=eddy ``` ### Logging Alle activiteit wordt gelogd naar `/tmp/dashcam-transfer.log` op nextcloud; de trigger-scripts loggen naar syslog met tag `dashcam-transfer`. ## Testdocumentatie ### Overzicht | Suite | Commando | Aantal | Dekking | |---|---|---|---| | Python unit | `python3 -m unittest discover -s tests` | 22 | Naam-parsing, directory-scans, kopie/retry, bestandsvergelijking, ruimte-opschoning, Matrix-berichten | | Mount-script | `./tests/test_mount_script.sh` | 7 | Skip bij actieve service, mount + service start, fsck-uitvoering, logging | | Cleanup-script | `./tests/test_cleanup_script.sh` | 12 | Stale-detectie (device weg / naam-match), service stop, umount + lazy-fallback, no-op bij gezonde mount | De Python-testen draaien tegen een gefakete omgeving (temp-mappen, gemonteerde modules) en raken geen echte bestanden of de API aan. De bash-testen plaatsen een set fake-commando's (`findmnt`, `systemctl`, `umount`, `logger`) op de `PATH` en verificeren met markerbestanden of het script het juiste gedrag vertoont. Een nieuw scenario toevoegen betekent: een nieuwe fake-commando/marker-regel in de test en een `check`-assertie voor het verwachte gedrag. ## Handmatig gebruik ```bash # Alle bestanden overzetten /usr/local/bin/dashcam-transfer # Beperkt tot eerste N bestanden (testen) /usr/local/bin/dashcam-transfer --limit 10 ``` ## Ontwikkeling Lokale repo: `~/src/dashcam-transfer/` op Mac Mini. Tests: `python3 -m unittest discover -s tests`, `./tests/test_mount_script.sh` en `./tests/test_cleanup_script.sh`. Deployen naar nextcloud: ```bash scp dashcam-transfer.py aiaiaiaiagent@nextcloud:/tmp/ ssh aiaiaiaiagent@nextcloud 'sudo -A cp /tmp/dashcam-transfer.py /usr/local/bin/dashcam-transfer' scp dashcam-transfer-mount.sh aiaiaiaiagent@nextcloud:/tmp/ ssh aiaiaiaiagent@nextcloud 'sudo -A cp /tmp/dashcam-transfer-mount.sh /usr/local/bin/dashcam-transfer-mount' scp dashcam-transfer-cleanup.sh aiaiaiaiagent@nextcloud:/tmp/ ssh aiaiaiaiagent@nextcloud 'sudo -A cp /tmp/dashcam-transfer-cleanup.sh /usr/local/bin/dashcam-transfer-cleanup' scp udev/99-dashcam-trigger.rules aiaiaiaiagent@nextcloud:/tmp/ ssh aiaiaiaiagent@nextcloud 'sudo -A cp /tmp/99-dashcam-trigger.rules /etc/udev/rules.d/ && sudo -A udevadm control --reload-rules' ``` Controleer na deploy altijd met `sudo sha256sum` dat de hash op de server gelijk is aan de lokale `shasum -a 256`.