- Python 74.1%
- Shell 25.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| docs/superpowers | ||
| tests | ||
| udev | ||
| .gitignore | ||
| dashcam-transfer-cleanup.sh | ||
| dashcam-transfer-mount.sh | ||
| dashcam-transfer.py | ||
| README.md | ||
| restructure.py | ||
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 WhatsApp notificatie via wwebjs-api
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 geziene productie-fout (defecte/flakke
SD-kaartlezer die midden in de transfer offline klapt, I/O error, device offline):
- Echte fout zichtbaar — kopieerfouten worden als
OSErrormet reden gelogd (i.p.v. alleen "exit status 1"); de eerderecp-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 WhatsApp-"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 WhatsApp 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 skip als de transfer al actief is.
- 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/sdcardnetjes ontkoppeld (lazy als dat mislukt). Zo blijft er nooit een dood device in demount-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_<sequentie><kamera>.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
WhatsApp 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.exfaten 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:
# 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]|dided|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) |
WhatsApp notificatie mislukt in log |
wwebjs-api container down | docker compose in /home/aiaiaiaiagent/wwebjs-api/ controleren |
| Abort: onvoldoende vrije ruimte | SSD vol, opschonen hielp niet | Oude datum-mappen handmatig opschonen of SSD uitbreiden |
Herstelprocedures
# 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 |
WHATSAPP_API |
http://localhost:3001/client/sendMessage/EdGpt |
wwebjs-api endpoint (localhost = nextcloud zelf) |
WHATSAPP_CHAT |
31620248011@c.us |
Ontvanger van notificaties |
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 |
WhatsApp notificatie
De API (wwebjs-api) draait als Docker container op nextcloud zelf:
| Config | Waarde |
|---|---|
| API endpoint | http://localhost:3001/client/sendMessage/EdGpt |
| Sessie | EdGpt |
| Ontvanger | 31620248011@c.us (privé chat Eddy) |
De container: docker-compose.yml in /home/aiaiaiaiagent/wwebjs-api/.
De notificatie zelf is een JSON-payload naar het endpoint; een mislukte notificatie
wordt alleen gelogd en breekt de transfer niet af.
Repo-structuur
dashcam-transfer/
├── dashcam-transfer.py Hoofdscript: transfer + WhatsApp-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)
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)
[Unit]
Description=Dashcam SD card transfer
After=mnt-sdcard.mount
[Service]
Type=oneshot
ExecStart=/usr/local/bin/dashcam-transfer
User=root
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 |
20 | Naam-parsing, directory-scans, kopie/retry, bestandsvergelijking, ruimte-opschoning, WhatsApp-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
# 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:
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.