| Title: | Access 'ERDA' Data Archive via SFTP and HTTP |
|---|---|
| Description: | Utilities for listing folders and reading image EXIF metadata from the Electronic Research Data Archive (ERDA) at Aarhus University. Supports both SFTP access via SSH ControlMaster and public anonymous HTTP URLs. |
| Authors: | Lars Dalby [aut, cre, cph] |
| Maintainer: | Lars Dalby <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.14.1 |
| Built: | 2026-10-07 09:04:39 UTC |
| Source: | https://gitlab.au.dk/ecos/tools/r-pkgs/erdatools |
Starts the plumber REST API bundled with erdatools. The API provides endpoints for ERDA directory indexing, sourceimages pipelines, and deployment management. Opens a Swagger UI in the browser by default.
et_api_run(port = 8000, host = "127.0.0.1", ...)et_api_run(port = 8000, host = "127.0.0.1", ...)
port |
Port to listen on. |
host |
Host to bind to. Use |
... |
Additional arguments passed to |
Invisibly returns the plumber router (after the server stops).
Transforms a filtered ERDA index (files only) into the shape needed to write
CamAlien on-board images into the camalien database data2 model. Every
image attribute is parsed from the filename via et_parse_camalien_filename()
— no EXIF reads are performed. One row is produced per image file.
et_build_camalien_images(index, sessionid)et_build_camalien_images(index, sessionid)
index |
Data frame as returned by |
sessionid |
Character UUID of the |
Filename tokens map to columns as follows: GT -> timestamp, CT ->
computertime, lon/lat -> point position, expt -> exposuretime,
gain, alt -> altitude, vel -> velocity, tag. CT is the onboard
computer clock and GT the GPS-derived time; the backend treats the GPS time
as the authoritative capture timestamp and keeps the computer clock in
computertime (verified against the live camalien DB). The timestamps are
UTC wall-clock in ISO 8601 basic format (e.g. 20250403T125240.225632) and
are stamped with a trailing Z so the stored instant is independent of the
database session timezone. Tokens absent from a filename become NA. All
rows are stamped status = 1 (the initial data2.imagedata_status, named
pending); the backend PlantNet pipeline advances the status later.
Duplicate filenames in the index (the same file listed in more than one
directory, e.g. a folder copied into a sibling on ERDA) are dropped with a
warning, keeping the first occurrence. data2.imagefile.filename is globally
unique, so a repeated filename is the same image; writing both would insert
an orphaned data2.imagedata row whose imagefile insert is silently
discarded by ON CONFLICT DO NOTHING.
Data frame with one row per image and columns filename, path,
type, sessionid, lon, lat, timestamp, status, tag,
computertime, exposuretime, gain, altitude, velocity.
## Not run: idx <- et_index_filter("erda_index.parquet", extensions = c("jpg", "tiff")) imgs <- et_build_camalien_images(idx, sessionid = "3f2c8b1e-...-...") ## End(Not run)## Not run: idx <- et_index_filter("erda_index.parquet", extensions = c("jpg", "tiff")) imgs <- et_build_camalien_images(idx, sessionid = "3f2c8b1e-...-...") ## End(Not run)
Produces up to two data.classifications rows per edge detection: an order
classification (from the order_* fields) and a species classification (from
the species_* fields). Detections are matched to their database UUIDs by
joining on key, so parsed_detect and detection_ids must come from the
same session (the edge key is unique per session only).
et_build_classifications( parsed_detect, detection_ids, order_algorithm = "mcc24", species_algorithm = "uk-denmark-moths" )et_build_classifications( parsed_detect, detection_ids, order_algorithm = "mcc24", species_algorithm = "uk-denmark-moths" )
parsed_detect |
Tibble as returned by |
detection_ids |
Data frame carrying detection UUIDs, as returned by
|
order_algorithm |
Character. Algorithm slug stamped on order
classifications. Default |
species_algorithm |
Character. Algorithm slug stamped on species
classifications. Default |
Rows whose label is NA/empty are skipped. Following the database's own
backfill (migration 2044), the single edge aboveTH flag is the order
threshold, so the order row carries it as abovethreshold and the species
row's abovethreshold is NA (SQL NULL).
A data frame with columns detectionid, taxonlevel, label,
labelid, score, abovethreshold, algorithm.
## Not run: parsed <- et_parse_edge_detections("results/detect/20250508.csv") det_ids <- et_db_write_detections(con, et_build_detections(parsed, si_ids)) classifications <- et_build_classifications(parsed, det_ids) ## End(Not run)## Not run: parsed <- et_parse_edge_detections("results/detect/20250508.csv") det_ids <- et_db_write_detections(con, et_build_detections(parsed, si_ids)) classifications <- et_build_classifications(parsed, det_ids) ## End(Not run)
Reads a CSV file of deployment records and returns a tibble ready for
et_db_write_deployments(). Validates required columns, normalizes values,
and optionally resolves partner names to IDs.
et_build_deployments(csv_path, projectid, partner_map = NULL)et_build_deployments(csv_path, projectid, partner_map = NULL)
csv_path |
Character. Path to the CSV file. |
projectid |
Character. Project ID (e.g. |
partner_map |
Named character vector mapping partner names to IDs
(e.g. |
The Partner column must match values in data.partners.name exactly.
Required CSV columns: Code, Partner, Location name, Year,
Latitude, Longitude.
Optional CSV columns (filled with NA if absent): Elevation (m),
Solar or grid power, Trap serial number, Trap type, Comments.
A tibble::tibble() with columns year, code, locationname,
elevation, latitude, longitude, power, trapserialno,
trapstatus, traptype, comment, projectid, and either partnerid
(when partner_map is supplied) or partner (when it is not).
## Not run: # With partner resolution partners <- DBI::dbGetQuery(con, "SELECT id, name FROM data.partners") pmap <- setNames(partners$id, partners$name) df <- et_build_deployments("deployments.csv", "ias", partner_map = pmap) # Without (caller resolves later) df <- et_build_deployments("deployments.csv", "ias") ## End(Not run)## Not run: # With partner resolution partners <- DBI::dbGetQuery(con, "SELECT id, name FROM data.partners") pmap <- setNames(partners$id, partners$name) df <- et_build_deployments("deployments.csv", "ias", partner_map = pmap) # Without (caller resolves later) df <- et_build_deployments("deployments.csv", "ias") ## End(Not run)
Joins parsed track-detail rows (which map detection key to a track
edge_id) to detection UUIDs (by key) and track UUIDs (by edge_id),
producing data.detection_tracks rows. Detections whose key is absent from
the track-detail file, or whose track has no UUID, produce no row.
et_build_detection_tracks( trs_parsed, detection_ids, track_ids, algorithm = "tracy", algorithmversion = "luke" )et_build_detection_tracks( trs_parsed, detection_ids, track_ids, algorithm = "tracy", algorithmversion = "luke" )
trs_parsed |
Tibble as returned by |
detection_ids |
Data frame carrying detection UUIDs, as returned by
|
track_ids |
Data frame carrying track UUIDs, as returned by
|
algorithm |
Character. Tracker algorithm name. Must be non-empty.
Default |
algorithmversion |
Character. Tracker algorithm version. Must be
non-empty. Default |
algorithm and algorithmversion are denormalised onto every junction row
and must equal the referenced track's identity — the composite foreign
key (trackid, algorithm, algorithmversion) -> data.tracks fails otherwise.
Only tracks whose identity matches these arguments are considered when
resolving trackid.
A data frame with columns detectionid, trackid, algorithm,
algorithmversion.
## Not run: trs <- et_parse_edge_track_detections("results/tracks/20250508TRS.csv") track_ids <- et_db_get_tracks(con, sessionid) links <- et_build_detection_tracks(trs, det_ids, track_ids) ## End(Not run)## Not run: trs <- et_parse_edge_track_detections("results/tracks/20250508TRS.csv") track_ids <- et_db_get_tracks(con, sessionid) links <- et_build_detection_tracks(trs, det_ids, track_ids) ## End(Not run)
Transforms the output of et_parse_edge_detections() into the
data.detections schema. Each edge detection becomes one row: the source
image is resolved by joining the detection filename (as a basename) to
sourceimage_ids, and the bounding box is rounded to integer pixels. The
contour, cropurl, and contour_geom columns are left NULL (the edge
pipeline provides no polygon). key and detect_id are carried through for
the downstream classifications and detection-track joins.
et_build_detections(parsed_detect, sourceimage_ids)et_build_detections(parsed_detect, sourceimage_ids)
parsed_detect |
Tibble as returned by |
sourceimage_ids |
Data frame with columns |
Detections whose filename has no matching source image are dropped (a warning reports the count).
A data frame with columns sourceimageid, bbox_x1, bbox_y1,
bbox_x2, bbox_y2 (integer), plus key and detect_id for downstream
joins.
## Not run: parsed <- et_parse_edge_detections("results/detect/20250508.csv") si_ids <- et_db_get_sourceimages(con, "MBO50#2025") detections <- et_build_detections(parsed, si_ids) ## End(Not run)## Not run: parsed <- et_parse_edge_detections("results/detect/20250508.csv") si_ids <- et_db_get_sourceimages(con, "MBO50#2025") detections <- et_build_detections(parsed, si_ids) ## End(Not run)
Aggregates a grouped data frame (from et_group_motion_images() or
et_group_snapshot_images()) into one row per unique session_key,
matching the data.sessions database schema.
et_build_sessions(grouped, deploymentid, type = "motion")et_build_sessions(grouped, deploymentid, type = "motion")
grouped |
Data frame as returned by |
deploymentid |
Character. Deployment ID (e.g. |
type |
Character. Session type: |
Data frame with columns: session_key (character, retained for
joining), date (Date), deploymentid (character), type (character),
starttime (POSIXct), endtime (POSIXct). One row per unique
session_key.
## Not run: grouped <- et_group_motion_images(idx) sessions <- et_build_sessions(grouped, deploymentid = "IT1#2023", type = "motion") ## End(Not run)## Not run: grouped <- et_group_motion_images(idx) sessions <- et_build_sessions(grouped, deploymentid = "IT1#2023", type = "motion") ## End(Not run)
Transforms a filtered ERDA index (files only) into the data.sourceimages
schema used by the AMI database. Constructs public URLs from the full
dir/name path in the index and extracts timestamps from AMI or CamAlien
filenames.
et_build_sourceimages( index, sharelink, http_base = "https://anon.erda.au.dk/share_redirect", deploymentid, type = "snapshot" )et_build_sourceimages( index, sharelink, http_base = "https://anon.erda.au.dk/share_redirect", deploymentid, type = "snapshot" )
index |
Data frame as returned by |
sharelink |
ERDA sharelink token. |
http_base |
Base URL for the ERDA share-redirect endpoint. |
deploymentid |
Character. Deployment ID for all rows (e.g. |
type |
Character or |
The index_ts column records the time this function is called (i.e. the
build timestamp), not the time the index was originally created.
Data frame with columns: index_ts (character, build timestamp),
filename, deploymentid, url, timestamp, issnapshot, processed,
queued.
## Not run: idx <- et_index_filter("erda_index.parquet", extensions = c("jpg", "jpeg")) si <- et_build_sourceimages( idx, sharelink = Sys.getenv("ERDA_SHARELINK"), http_base = "https://anon.erda.au.dk/share_redirect", deploymentid = "IT1#2023", type = "snapshot" ) ## End(Not run)## Not run: idx <- et_index_filter("erda_index.parquet", extensions = c("jpg", "jpeg")) si <- et_build_sourceimages( idx, sharelink = Sys.getenv("ERDA_SHARELINK"), http_base = "https://anon.erda.au.dk/share_redirect", deploymentid = "IT1#2023", type = "snapshot" ) ## End(Not run)
Transforms the output of et_parse_edge_tracks() into the data.tracks
database schema by mapping id to edge_id and adding sessionid,
algorithm, and algorithmversion columns.
et_build_tracks( parsed_tracks, sessionid, algorithm = "tracy", algorithmversion = "luke" )et_build_tracks( parsed_tracks, sessionid, algorithm = "tracy", algorithmversion = "luke" )
parsed_tracks |
Tibble as returned by |
sessionid |
Character. Session UUID to associate with all tracks. |
algorithm |
Character. Tracker algorithm name stamped on every track.
Must be a non-empty string. Default |
algorithmversion |
Character. Tracker algorithm version. Must be a
non-empty string. Default |
algorithm and algorithmversion are part of the data.tracks uniqueness
key (sessionid, edge_id, algorithm, algorithmversion), which is
NULLS DISTINCT (migration 2048). A NULL/empty identity would defeat the
upsert de-dupe and insert duplicate tracks on re-ingest, so both are required
to be non-empty.
The tracker identity is named tracy luke: tracy is the tracker, and the
version luke marks the on-edge tracker that predates kylo.
A data frame with columns matching the data.tracks schema:
sessionid, edge_id, algorithm, algorithmversion.
## Not run: parsed <- et_parse_edge_tracks("results/tracks/20250508TR.csv") tracks <- et_build_tracks(parsed, sessionid = "abc-123") ## End(Not run)## Not run: parsed <- et_parse_edge_tracks("results/tracks/20250508TR.csv") tracks <- et_build_tracks(parsed, sessionid = "abc-123") ## End(Not run)
Wrapper around DBI::dbConnect() using RPostgres::Postgres(), parameterised
by the CAMALIEN_* environment variables. This is intentionally separate
from et_db_connect() (which targets the AMI database) so CamAlien writers can
never accidentally connect to the wrong database — always call this function
explicitly in CamAlien code paths (including inside mirai job bodies).
et_camalien_db_connect( host = Sys.getenv("CAMALIEN_HOST"), port = 5432L, dbname = Sys.getenv("CAMALIEN_DB", unset = "camalien"), user = Sys.getenv("CAMALIEN_USER"), password = Sys.getenv("CAMALIEN_PASSWORD"), sslmode = "verify-full", sslrootcert = Sys.getenv("CAMALIEN_SSLROOTCERT") )et_camalien_db_connect( host = Sys.getenv("CAMALIEN_HOST"), port = 5432L, dbname = Sys.getenv("CAMALIEN_DB", unset = "camalien"), user = Sys.getenv("CAMALIEN_USER"), password = Sys.getenv("CAMALIEN_PASSWORD"), sslmode = "verify-full", sslrootcert = Sys.getenv("CAMALIEN_SSLROOTCERT") )
host |
Database hostname. Defaults to |
port |
Database port. Defaults to |
dbname |
Database name. Defaults to |
user |
Database user. Defaults to |
password |
Database password. Defaults to |
sslmode |
SSL mode for the connection. Defaults to |
sslrootcert |
Path to the root CA certificate. Defaults to the
|
Requires the DBI and RPostgres packages at runtime.
A DBI connection object.
## Not run: con <- et_camalien_db_connect() on.exit(DBI::dbDisconnect(con)) ## End(Not run)## Not run: con <- et_camalien_db_connect() on.exit(DBI::dbDisconnect(con)) ## End(Not run)
Given built CamAlien image rows, returns only those whose filename is not
already in data2.imagefile. data2.imagefile.filename is globally unique
(uq_v2_imagefile_filename), so it is the idempotency key — no session or
partner scoping is required.
et_camalien_db_diff_images(dbcon, df, batch_size = 5000L)et_camalien_db_diff_images(dbcon, df, batch_size = 5000L)
dbcon |
A DBI connection (from |
df |
Data frame with at least a |
batch_size |
Integer. Filenames per lookup query. Default |
The subset of df whose filenames are not yet in the database.
## Not run: new <- et_camalien_db_diff_images(con, imgs) ## End(Not run)## Not run: new <- et_camalien_db_diff_images(con, imgs) ## End(Not run)
Looks up a data.partner.id (UUID) in the camalien database, scoped to a
project via projectSlug. camalien-db moved partner uniqueness to
(storagePath, projectSlug), so the lookup is always project-scoped. This is
a pure lookup — it never creates partners and errors if none matches.
et_camalien_db_get_partner( dbcon, partner, project_slug = "onestop", by = c("name", "partnerkey", "storagepath") )et_camalien_db_get_partner( dbcon, partner, project_slug = "onestop", by = c("name", "partnerkey", "storagepath") )
dbcon |
A DBI connection (from |
partner |
Character. The partner identifier to match. |
project_slug |
Character. Project slug to scope by. Defaults to
|
by |
Character. Which column |
Character partner UUID.
## Not run: con <- et_camalien_db_connect() pid <- et_camalien_db_get_partner(con, "Aarhus University") ## End(Not run)## Not run: con <- et_camalien_db_connect() pid <- et_camalien_db_get_partner(con, "Aarhus University") ## End(Not run)
Resolves a data2.session.id (UUID) by (partnerid, name, year). This is a
pure lookup — sessions are created by the backend, so it errors if no session
matches, and also errors if more than one matches (there is no unique
constraint on the triple; pass a session UUID directly in that case).
et_camalien_db_get_session(dbcon, partnerid, name, year)et_camalien_db_get_session(dbcon, partnerid, name, year)
dbcon |
A DBI connection (from |
partnerid |
Character partner UUID (from |
name |
Character session name. |
year |
Integer session year. |
Character session UUID.
## Not run: sid <- et_camalien_db_get_session(con, pid, "denmark", 2025L) ## End(Not run)## Not run: sid <- et_camalien_db_get_session(con, pid, "denmark", 2025L) ## End(Not run)
Writes new CamAlien on-board image records into the data2 model in a single
transaction, in FK-safe order: data2.imagedata (with a client-generated
UUID id — the column has no DB default — and a PostGIS point built from
lon/lat), then data2.additionalImagedata, then data2.imagefile. The
data2.capturepoint row is created automatically by the
trg_insert_capturepoint trigger and is never written here.
et_camalien_db_write_images(dbcon, df, batch_size = 250L)et_camalien_db_write_images(dbcon, df, batch_size = 250L)
dbcon |
A DBI connection (from |
df |
Data frame from |
batch_size |
Integer. Rows per INSERT statement. Default |
data2.imagefile uses ON CONFLICT (filename) DO NOTHING as a safety net;
call et_camalien_db_diff_images() first so only genuinely new images are
written. When lon/lat are NA the point is inserted as SQL NULL.
Duplicate filenames within df are an error: data2.imagefile.filename is
globally unique, so the second copy's imagefile insert would be silently
dropped by ON CONFLICT DO NOTHING, leaving its data2.imagedata row
orphaned. et_build_camalien_images() deduplicates the index up front.
Requires the DBI and uuid packages at runtime.
The number of images written (invisibly).
## Not run: con <- et_camalien_db_connect() on.exit(DBI::dbDisconnect(con)) et_camalien_db_write_images(con, et_camalien_db_diff_images(con, imgs)) ## End(Not run)## Not run: con <- et_camalien_db_connect() on.exit(DBI::dbDisconnect(con)) et_camalien_db_write_images(con, et_camalien_db_diff_images(con, imgs)) ## End(Not run)
After filtering stray images with et_filter_stray_sourceimages() or
et_filter_stray_images(), this function examines the removed images to
determine which years they belong to and whether matching deployments exist
in the database. Emits informative cli messages and returns a summary data
frame.
et_check_stray_deployments(removed, deployment_id, dbcon)et_check_stray_deployments(removed, deployment_id, dbcon)
removed |
Data frame of removed images from
|
deployment_id |
Character. The current deployment ID (e.g.
|
dbcon |
A DBI connection object. |
The lookup uses the projectid, partnerid, and code from the current
deployment, substituting each stray year.
Data frame with columns:
Integer. Year extracted from stray image timestamps.
Integer. Number of stray images in that year.
Character. The deployment ID if it exists, or
NA_character_.
Logical. Whether a deployment was found.
Returns a zero-row data frame if removed is empty.
## Not run: stray <- et_filter_stray_sourceimages(si, year = 2023) if (nrow(stray$removed) > 0L) { info <- et_check_stray_deployments(stray$removed, "IT1#2023", con) } ## End(Not run)## Not run: stray <- et_filter_stray_sourceimages(si, year = 2023) if (nrow(stray$removed) > 0L) { info <- et_check_stray_deployments(stray$removed, "IT1#2023", con) } ## End(Not run)
Batch UPDATE that assigns sessionid to sourceimages based on temporal
overlap with session (starttime, endtime) ranges. Motion sourceimages are
matched to motion sessions, and snapshot sourceimages to snapshot sessions.
Only sourceimages with sessionid IS NULL are updated, making this function
idempotent.
et_db_assign_sessions(dbcon, deploymentid)et_db_assign_sessions(dbcon, deploymentid)
dbcon |
A DBI connection object (from |
deploymentid |
Character. Deployment ID to scope the update. |
The number of rows updated (invisibly).
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) et_db_assign_sessions(con, "IT1#2023") ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) et_db_assign_sessions(con, "IT1#2023") ## End(Not run)
Wrapper around DBI::dbConnect() using RPostgres::Postgres(). Connection
parameters default to the environment variables used by the existing
ami-indexer container (AMI_HOST, AMI_USER, AMI_PASSWORD, AMI_DB).
et_db_connect( host = Sys.getenv("AMI_HOST"), port = 5432L, dbname = Sys.getenv("AMI_DB", unset = "ami_ias"), user = Sys.getenv("AMI_USER"), password = Sys.getenv("AMI_PASSWORD"), sslmode = "verify-full", sslrootcert = Sys.getenv("AMI_SSLROOTCERT") )et_db_connect( host = Sys.getenv("AMI_HOST"), port = 5432L, dbname = Sys.getenv("AMI_DB", unset = "ami_ias"), user = Sys.getenv("AMI_USER"), password = Sys.getenv("AMI_PASSWORD"), sslmode = "verify-full", sslrootcert = Sys.getenv("AMI_SSLROOTCERT") )
host |
Database hostname. Defaults to |
port |
Database port. Defaults to |
dbname |
Database name. Defaults to |
user |
Database user. Defaults to |
password |
Database password. Defaults to |
sslmode |
SSL mode for the connection. Defaults to |
sslrootcert |
Path to the root CA certificate ( |
Requires the DBI and RPostgres packages at runtime.
A DBI connection object.
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) ## End(Not run)
Looks up a deployment's metadata and its associated project and partner folders to construct the canonical ERDA directory path. The path structure depends on the deployment's trap type:
et_db_deployment_path(dbcon, deployment_id)et_db_deployment_path(dbcon, deployment_id)
dbcon |
A DBI connection object. |
deployment_id |
Character. The deployment ID (primary key). |
standalone/lepisense/mothbox: /project_folder/year/partner_folder/code/
iot: /project_folder/code/ (no year or partner level; trap codes are unique within a project and images are organised in date-based subdirectories)
For standalone traps, a partner folder of "root" (or an unknown partner)
is a sentinel meaning the deployment sits directly under the year, so the
partner level is omitted (e.g. /vibes/2026/VIB01/).
Character string with the remote path, e.g. "/ias/2023/italy/IT1/"
for standalone or "/ias/BIO25/" for IoT.
## Not run: con <- et_db_connect() path <- et_db_deployment_path(con, "IT1#2023") # "/ias/2023/italy/IT1/" ## End(Not run)## Not run: con <- et_db_connect() path <- et_db_deployment_path(con, "IT1#2023") # "/ias/2023/italy/IT1/" ## End(Not run)
Queries existing (code, year, projectid) triples from data.deployments
and returns only the rows in df that do not yet exist.
et_db_diff_deployments(dbcon, df)et_db_diff_deployments(dbcon, df)
dbcon |
A DBI connection object. |
df |
Data frame with at least |
A subset of df containing only new deployments. All columns are
preserved.
## Not run: new <- et_db_diff_deployments(con, df) et_db_write_deployments(con, new) ## End(Not run)## Not run: new <- et_db_diff_deployments(con, df) et_db_write_deployments(con, new) ## End(Not run)
Compares a sourceimages data frame against data.sourceimages in the
database and returns only the rows whose filename is not yet present.
Queries are batched to stay within PostgreSQL's parameter limit.
et_db_diff_sourceimages(dbcon, df, batch_size = 5000L, n_workers = 1L)et_db_diff_sourceimages(dbcon, df, batch_size = 5000L, n_workers = 1L)
dbcon |
A DBI connection object (from |
df |
Data frame with at least a |
batch_size |
Integer. Number of filenames per query. Default |
n_workers |
Integer. Number of parallel mirai workers. Default |
A subset of df containing only rows not already in the database.
All original columns are preserved.
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) si <- et_build_sourceimages(idx, sharelink, http_base = http_base, deploymentid = 1L) new <- et_db_diff_sourceimages(con, si) et_db_write_sourceimages(con, new) ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) si <- et_build_sourceimages(idx, sharelink, http_base = http_base, deploymentid = 1L) new <- et_db_diff_sourceimages(con, si) et_db_write_sourceimages(con, new) ## End(Not run)
Queries data.deployments by primary key and returns the deployment's
year, code, partner ID, and project ID.
et_db_get_deployment(dbcon, deployment_id)et_db_get_deployment(dbcon, deployment_id)
dbcon |
A DBI connection object. |
deployment_id |
Character. The deployment ID (primary key). |
A named list with elements year (integer), code (character),
partnerid (character), projectid (character), and traptype
(character, e.g. "standalone", "iot", "lepisense", or "mothbox").
Aborts with an error if the deployment is not found.
## Not run: con <- et_db_connect() dep <- et_db_get_deployment(con, "IT1#2023") dep$year # 2023 dep$code # "IT1" ## End(Not run)## Not run: con <- et_db_connect() dep <- et_db_get_deployment(con, "IT1#2023") dep$year # 2023 dep$code # "IT1" ## End(Not run)
Queries data.deployments for a deployment matching the given project,
partner, year, and code. The deployment ID is a character string
(e.g. "IT1#2023").
et_db_get_deployment_id(dbcon, projectid, partnerid, year, code)et_db_get_deployment_id(dbcon, projectid, partnerid, year, code)
dbcon |
A DBI connection object. |
projectid |
Character. Project identifier (e.g. |
partnerid |
Character. Partner ID (e.g. |
year |
Integer. Deployment year. |
code |
Character. Deployment code (e.g. |
Character deployment ID (e.g. "IT1#2023"), or NA_character_
if not found.
## Not run: con <- et_db_connect() dep_id <- et_db_get_deployment_id(con, "ias", "8", 2023L, "IT1") ## End(Not run)## Not run: con <- et_db_connect() dep_id <- et_db_get_deployment_id(con, "ias", "8", 2023L, "IT1") ## End(Not run)
Queries data.partners for the folder associated with a partner ID.
et_db_get_partner_folder(dbcon, partnerid)et_db_get_partner_folder(dbcon, partnerid)
dbcon |
A DBI connection object. |
partnerid |
Integer. Partner ID. |
Character string with the partner folder, or NA_character_ if not found.
## Not run: con <- et_db_connect() folder <- et_db_get_partner_folder(con, 1L) ## End(Not run)## Not run: con <- et_db_connect() folder <- et_db_get_partner_folder(con, 1L) ## End(Not run)
Queries data.projects for the folder associated with a project ID.
et_db_get_project_folder(dbcon, projectid)et_db_get_project_folder(dbcon, projectid)
dbcon |
A DBI connection object. |
projectid |
Character. Project ID. |
Character string with the project folder, or NA_character_ if
not found.
## Not run: con <- et_db_connect() folder <- et_db_get_project_folder(con, "ias") ## End(Not run)## Not run: con <- et_db_connect() folder <- et_db_get_project_folder(con, "ias") ## End(Not run)
Queries data.sessions for all sessions belonging to a deployment. Used
to retrieve sessions that were created by the database trigger (migration
2034) after sourceimages were inserted.
et_db_get_sessions(dbcon, deploymentid)et_db_get_sessions(dbcon, deploymentid)
dbcon |
A DBI connection object (from |
deploymentid |
Character. Deployment ID (e.g. |
Data frame with columns: id (character, UUID), date (Date),
type (character), starttime (POSIXct), endtime (POSIXct).
Zero rows if no sessions exist.
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) sessions <- et_db_get_sessions(con, "IT1#2023") ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) sessions <- et_db_get_sessions(con, "IT1#2023") ## End(Not run)
Queries data.sourceimages for all images belonging to a deployment,
returning the filename (basename) and UUID of each. Used to resolve edge
detection filenames to source-image UUIDs when writing data.detections.
et_db_get_sourceimages(dbcon, deploymentid)et_db_get_sourceimages(dbcon, deploymentid)
dbcon |
A DBI connection object (from |
deploymentid |
Character. Deployment ID (e.g. |
Data frame with columns: id (character, UUID) and filename
(character, basename). Zero rows if no source images exist.
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) si_ids <- et_db_get_sourceimages(con, "MBO50#2025") ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) si_ids <- et_db_get_sourceimages(con, "MBO50#2025") ## End(Not run)
Queries data.tracks for all tracks belonging to a session, returning the
track UUID keyed by (edge_id, algorithm, algorithmversion). Used to resolve
edge track IDs to track UUIDs when writing the data.detection_tracks
junction (the writers et_db_write_tracks() return only a row count).
et_db_get_tracks(dbcon, sessionid)et_db_get_tracks(dbcon, sessionid)
dbcon |
A DBI connection object (from |
sessionid |
Character. Session UUID. |
Data frame with columns: id (character, UUID), edge_id
(integer), algorithm (character), algorithmversion (character). Zero
rows if no tracks exist.
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) track_ids <- et_db_get_tracks(con, "abc-123") ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) track_ids <- et_db_get_tracks(con, "abc-123") ## End(Not run)
Queries data.deployments for all deployments matching an optional
project and/or year filter. Useful for batch operations such as running
the deploy pipeline over every deployment in a given project-year.
et_db_list_deployments(dbcon, projectid = NULL, year = NULL, traptype = NULL)et_db_list_deployments(dbcon, projectid = NULL, year = NULL, traptype = NULL)
dbcon |
A DBI connection object. |
projectid |
Character or |
year |
Integer or |
traptype |
Character or |
Character vector of deployment IDs ordered by id. Returns
character(0) when no deployments match.
## Not run: con <- et_db_connect() ids <- et_db_list_deployments(con, projectid = "ias", year = 2025L) ## End(Not run)## Not run: con <- et_db_connect() ids <- et_db_list_deployments(con, projectid = "ias", year = 2025L) ## End(Not run)
Performs batch INSERT into data.classifications. On conflict with
uq_classifications_detectionid_taxonlevel_algorithm
(detectionid, taxonlevel, algorithm) the row is updated (DO UPDATE SET
label, labelid, score, abovethreshold), so re-ingest refreshes
scores. embeddingpath is left NULL (the edge pipeline provides none).
et_db_write_classifications(dbcon, df, batch_size = 250L)et_db_write_classifications(dbcon, df, batch_size = 250L)
dbcon |
A DBI connection object (from |
df |
Data frame with columns: |
batch_size |
Integer. Number of rows per INSERT statement. Default |
Total number of rows affected (invisibly).
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) cls <- et_build_classifications(parsed, det_ids) et_db_write_classifications(con, cls) ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) cls <- et_build_classifications(parsed, det_ids) et_db_write_classifications(con, cls) ## End(Not run)
Performs batch INSERT into data.deployments. Each deployment receives a
UUID generated by uuid_generate_v4() in PostgreSQL. Geographic coordinates
are converted to a PostGIS point via
ST_SetSRID(ST_MakePoint(lon, lat), 4326).
et_db_write_deployments(dbcon, df, batch_size = 250L)et_db_write_deployments(dbcon, df, batch_size = 250L)
dbcon |
A DBI connection object (from |
df |
Data frame with columns: |
batch_size |
Integer. Rows per INSERT statement (default 250). |
Use et_db_diff_deployments() beforehand to avoid inserting duplicates.
Total number of rows inserted (invisibly).
## Not run: df <- et_build_deployments("deployments.csv", "ias", partner_map = pmap) new <- et_db_diff_deployments(con, df) et_db_write_deployments(con, new) ## End(Not run)## Not run: df <- et_build_deployments("deployments.csv", "ias", partner_map = pmap) new <- et_db_diff_deployments(con, df) et_db_write_deployments(con, new) ## End(Not run)
Performs batch INSERT into data.detection_tracks. On conflict with the
primary key (detectionid, trackid) the row is skipped (DO NOTHING), making
re-ingest idempotent. The algorithm/algorithmversion on each row must
equal the referenced track's identity or the composite foreign key to
data.tracks fails.
et_db_write_detection_tracks(dbcon, df, batch_size = 250L)et_db_write_detection_tracks(dbcon, df, batch_size = 250L)
dbcon |
A DBI connection object (from |
df |
Data frame with columns: |
batch_size |
Integer. Number of rows per INSERT statement. Default |
Total number of rows affected (invisibly).
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) links <- et_build_detection_tracks(trs, det_ids, track_ids) et_db_write_detection_tracks(con, links) ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) links <- et_build_detection_tracks(trs, det_ids, track_ids) et_db_write_detection_tracks(con, links) ## End(Not run)
Performs batch INSERT into data.detections. Each detection receives a UUID
generated by uuid_generate_v4() in PostgreSQL (the id column has no
default). On conflict with uq_detections_sourceimage_bbox
(sourceimageid, bbox_x1, bbox_y1, bbox_x2, bbox_y2) the row is skipped
(DO NOTHING), making re-ingest idempotent. After writing, the detection UUIDs
are read back and matched onto the input rows by
(sourceimageid, bbox_x1, bbox_y1, bbox_x2, bbox_y2), mirroring
et_db_write_sessions().
et_db_write_detections(dbcon, df, batch_size = 250L)et_db_write_detections(dbcon, df, batch_size = 250L)
dbcon |
A DBI connection object (from |
df |
Data frame with columns: |
batch_size |
Integer. Number of rows per INSERT statement. Default |
contour, cropurl, and contour_geom are left NULL (the edge pipeline
provides no polygon); area_px is a generated column and is never written.
The input data frame with an id column (character UUID) populated
from the database. Carries an n_written attribute: the number of rows
actually inserted (rows skipped by the DO NOTHING conflict clause are not
counted).
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) det <- et_build_detections(parsed, si_ids) det_ids <- et_db_write_detections(con, det) ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) det <- et_build_detections(parsed, si_ids) det_ids <- et_db_write_detections(con, det) ## End(Not run)
Performs batch INSERT into data.sessions with ON CONFLICT upsert on
(date, deploymentid, type). Returns the input data frame augmented with
id (UUID) from the database.
et_db_write_sessions(dbcon, df, batch_size = 250L, n_workers = 1L)et_db_write_sessions(dbcon, df, batch_size = 250L, n_workers = 1L)
dbcon |
A DBI connection object (from |
df |
Data frame with columns: |
batch_size |
Integer. Number of rows per INSERT statement. Default |
n_workers |
Integer. Number of parallel mirai workers. Default |
The input data frame with an id column (character UUID) populated
from the database.
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) sessions <- et_build_sessions(grouped, "IT1#2023", type = "motion") sessions_with_ids <- et_db_write_sessions(con, sessions) ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) sessions <- et_build_sessions(grouped, "IT1#2023", type = "motion") sessions_with_ids <- et_db_write_sessions(con, sessions) ## End(Not run)
Performs batch INSERT into data.sourceimages with ON CONFLICT upsert on
(filename, deploymentid). Existing rows are updated with the new values.
Uses parameterized queries via DBI::dbExecute().
et_db_write_sourceimages(dbcon, df, batch_size = 250L, n_workers = 1L)et_db_write_sourceimages(dbcon, df, batch_size = 250L, n_workers = 1L)
dbcon |
A DBI connection object (from |
df |
Data frame with columns: |
batch_size |
Integer. Number of rows per INSERT statement. Default |
n_workers |
Integer. Number of parallel mirai workers. Default |
The total number of rows affected (invisibly).
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) et_db_write_sourceimages(con, sourceimages_df) et_db_write_sourceimages(con, sourceimages_df, n_workers = 4L) ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) et_db_write_sourceimages(con, sourceimages_df) et_db_write_sourceimages(con, sourceimages_df, n_workers = 4L) ## End(Not run)
Performs batch upsert into data.tracking_tasks, the session-level tracking
work queue. Used by the IoT path to record that a session's tracks were
produced on the edge device: each row is written with status = "done" and
the tracker algorithm/algorithmversion, which mark the edge provenance.
The unique key is (sessionid, algorithm, algorithmversion)
(constraint uq_tracking_tasks_session_algorithm), so on conflict the
existing row's status is updated (DO UPDATE), making re-ingest idempotent.
Every row needs a workerid — the tracking_tasks CHECK requires a non-NULL
workerid for any non-pending status. Edge-tracked sessions have no
server-side worker, so callers pass a fixed virtual edge-worker id (see
.et_edge_workerid in erda_edge.R); workerid has no foreign key.
et_db_write_tracking_tasks(dbcon, df, batch_size = 250L)et_db_write_tracking_tasks(dbcon, df, batch_size = 250L)
dbcon |
A DBI connection object (from |
df |
Data frame with columns: |
batch_size |
Integer. Number of rows per INSERT statement. Default |
Total number of rows affected (invisibly).
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) tasks <- data.frame( sessionid = "abc-123", status = "done", algorithm = "tracy", algorithmversion = "luke", workerid = "f2491a34-c18a-5188-984b-09092a9aaa55" ) et_db_write_tracking_tasks(con, tasks) ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) tasks <- data.frame( sessionid = "abc-123", status = "done", algorithm = "tracy", algorithmversion = "luke", workerid = "f2491a34-c18a-5188-984b-09092a9aaa55" ) et_db_write_tracking_tasks(con, tasks) ## End(Not run)
Performs batch INSERT into data.tracks. On conflict with the
uq_tracks_session_edge_algorithm constraint
(sessionid, edge_id, algorithm, algorithmversion) the row is skipped
(DO NOTHING), making re-ingest idempotent. Typically used to write
edge-processed tracking data from IoT AMI traps.
et_db_write_tracks(dbcon, df, batch_size = 250L)et_db_write_tracks(dbcon, df, batch_size = 250L)
dbcon |
A DBI connection object (from |
df |
Data frame with columns: |
batch_size |
Integer. Number of rows per INSERT statement. Default |
Total number of rows affected (invisibly).
## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) parsed <- et_parse_edge_tracks("results/tracks/20250508TR.csv") tracks <- et_build_tracks(parsed, sessionid = "abc-123") et_db_write_tracks(con, tracks) ## End(Not run)## Not run: con <- et_db_connect() on.exit(DBI::dbDisconnect(con)) parsed <- et_parse_edge_tracks("results/tracks/20250508TR.csv") tracks <- et_build_tracks(parsed, sessionid = "abc-123") et_db_write_tracks(con, tracks) ## End(Not run)
Returns the vector of ExifTool tag names passed to exifr::read_exif().
When the environment variable EXIF_TAGS is set (comma-separated tag names),
it is used instead of the built-in defaults. "FileName" is always prepended
so output parquet files retain their original name.
et_exif_tags()et_exif_tags()
If EXIF_TAGS is set but produces no valid tag names after splitting and
trimming (e.g. EXIF_TAGS=" , , "), a warning is emitted and the
built-in defaults are used.
Default tags (when EXIF_TAGS is unset):
"FileName", "Model", "CameraSerialNumber", "XResolution",
"YResolution", "ImageWidth", "ImageHeight", "UserComment",
"Duration", "VideoFrameRate".
Character vector of tag names, always starting with "FileName".
Keeps filenames ending in .jpg, .jpeg, .tif, .tiff,
.mp4, .mov, or .avi (all case-insensitive).
et_filter_media_files(filenames)et_filter_media_files(filenames)
filenames |
Character vector of file paths or names. |
Subset of filenames matching a supported extension.
Removes rows from a grouped image data frame where the timestamp year
does not match the deployment year exactly.
et_filter_stray_images(grouped, year, log_path = NULL)et_filter_stray_images(grouped, year, log_path = NULL)
grouped |
Data frame as returned by |
year |
Integer. The deployment year. |
log_path |
Character or |
Optionally writes the ERDA paths of removed images to a log file.
A named list:
Data frame of rows whose timestamps are within range.
Data frame of rows that were filtered out.
## Not run: grouped <- et_group_motion_images(idx) result <- et_filter_stray_images(grouped, year = 2025) grouped <- result$kept ## End(Not run)## Not run: grouped <- et_group_motion_images(idx) result <- et_filter_stray_images(grouped, year = 2025) grouped <- result$kept ## End(Not run)
Removes rows from a sourceimages data frame where the timestamp year
does not match the deployment year exactly. This is the
sourceimages-schema equivalent of et_filter_stray_images(), operating on
character timestamps and the filename column instead of grouped data
frames.
et_filter_stray_sourceimages(sourceimages, year, log_path = NULL)et_filter_stray_sourceimages(sourceimages, year, log_path = NULL)
sourceimages |
Data frame as returned by |
year |
Integer. The deployment year. |
log_path |
Character or |
A named list:
Data frame of rows whose timestamps are within range.
Data frame of rows that were filtered out.
## Not run: si <- et_build_sourceimages(idx, sharelink = "TOKEN", deploymentid = "IT1#2023", type = NULL) result <- et_filter_stray_sourceimages(si, year = 2023) si <- result$kept ## End(Not run)## Not run: si <- et_build_sourceimages(idx, sharelink = "TOKEN", deploymentid = "IT1#2023", type = NULL) result <- et_filter_stray_sourceimages(si, year = 2023) si <- result$kept ## End(Not run)
Takes an ERDA index data frame (as returned by et_index_filter()) and
groups motion images into sessions using noon-to-noon grouping.
et_group_motion_images(index)et_group_motion_images(index)
index |
Data frame with columns |
A session spans from noon UTC on one day to noon UTC on the next. The
session_key column is the calendar date of the noon boundary, so an
image at 22:00 on Aug 31 and another at 06:00 on Sep 1 both receive
session_key = "2023_08_31". An image at 13:00 on Sep 1 starts a
new session with key "2023_09_01".
Only motion images (as identified by et_parse_ami_filename()) are retained.
The filtered data frame with three added columns:
Character. Session date as "YYYY_MM_DD".
POSIXct (UTC). Parsed from the AMI filename.
Character. Trigger counter from the filename.
## Not run: idx <- et_index_filter("motion_index.parquet", extensions = c("jpg", "jpeg")) grouped <- et_group_motion_images(idx) table(grouped$session_key) ## End(Not run)## Not run: idx <- et_index_filter("motion_index.parquet", extensions = c("jpg", "jpeg")) grouped <- et_group_motion_images(idx) table(grouped$session_key) ## End(Not run)
Takes an ERDA index data frame (as returned by et_index_filter()) and
groups snapshot images into sessions using noon-to-noon grouping.
et_group_snapshot_images(index)et_group_snapshot_images(index)
index |
Data frame with columns |
A session spans from noon UTC on one day to noon UTC on the next. The
session_key column is the calendar date of the noon boundary, so an
image at 22:00 on Aug 31 and another at 06:00 on Sep 1 both receive
session_key = "2023_08_31". An image at 13:00 on Sep 1 starts a
new session with key "2023_09_01".
Only snapshot images (as identified by et_parse_ami_filename()) are
retained.
The filtered data frame with two added columns:
Character. Session date as "YYYY_MM_DD".
POSIXct (UTC). Parsed from the AMI filename.
## Not run: idx <- et_index_filter("snapshot_index.parquet", extensions = c("jpg", "jpeg")) grouped <- et_group_snapshot_images(idx) table(grouped$session_key) ## End(Not run)## Not run: idx <- et_index_filter("snapshot_index.parquet", extensions = c("jpg", "jpeg")) grouped <- et_group_snapshot_images(idx) table(grouped$session_key) ## End(Not run)
Builds the HTTPS URL used to access a single image file through ERDA's anonymous share-redirect endpoint. This is a pure string operation and requires no network connection. Slashes between path components are normalised automatically.
et_img_url( sharelink, remote_dir, country, folder, filename, http_base = "https://anon.erda.au.dk/share_redirect" )et_img_url( sharelink, remote_dir, country, folder, filename, http_base = "https://anon.erda.au.dk/share_redirect" )
sharelink |
ERDA sharelink token. |
remote_dir |
Remote root directory, i.e. the path relative to the
sharelink root (e.g. |
country |
Country subdirectory name (e.g. |
folder |
Dated subfolder name (e.g. |
filename |
Image file name (e.g. |
http_base |
Base URL of the ERDA share-redirect endpoint. Defaults to
|
A length-1 character vector containing the full URL.
et_img_url( sharelink = "TOKEN", remote_dir = "/storage/2025/", country = "portugal", folder = "2025_07_10-11_10", filename = "img.jpg" )et_img_url( sharelink = "TOKEN", remote_dir = "/storage/2025/", country = "portugal", folder = "2025_07_10-11_10", filename = "img.jpg" )
Traverses the directory tree rooted at remote_path using BFS, calling
ls -la on each directory via et_sftp_batch(). Results are stored in a
parquet file at index_path with columns dir, name, is_dir,
size (integer, file size in bytes; NA for directories), and
mtime (character, last-modified timestamp from ls -la, e.g.
"Jul 10 11:10" or "Jan 5 2024").
et_index_dir( conn, remote_path, index_path, checkpoint_every = 25L, n_workers = 1L )et_index_dir( conn, remote_path, index_path, checkpoint_every = 25L, n_workers = 1L )
conn |
Connection object returned by |
remote_path |
Remote root path to index. |
index_path |
Local path for the output parquet file. |
checkpoint_every |
Integer. Rewrite the parquet every this many
directories. Default |
n_workers |
Integer. Number of parallel mirai workers for wave-based
BFS. Default |
On a fresh run, remote_path must exist: a missing root aborts with an
error instead of producing an empty index.
Resume: if index_path already exists, the index is loaded and any
directory already present in unique(index$dir) is skipped. Previously
indexed directories are used to reconstruct the BFS queue without additional
SFTP calls, so resuming after an interrupted run is fast.
Checkpointing: the parquet is rewritten every checkpoint_every
directories. At most checkpoint_every directories of work can be lost if
the connection drops between checkpoints.
The full index data frame (invisibly).
## Not run: conn <- et_sftp_connect(Sys.getenv("ERDA_SHARELINK")) on.exit(conn$disconnect(), add = TRUE, after = FALSE) idx <- et_index_dir(conn, "/storage/2025", "./erda_index.parquet") idx <- et_index_dir(conn, "/storage/2025", "./erda_index.parquet", n_workers = 4L) ## End(Not run)## Not run: conn <- et_sftp_connect(Sys.getenv("ERDA_SHARELINK")) on.exit(conn$disconnect(), add = TRUE, after = FALSE) idx <- et_index_dir(conn, "/storage/2025", "./erda_index.parquet") idx <- et_index_dir(conn, "/storage/2025", "./erda_index.parquet", n_workers = 4L) ## End(Not run)
Removes directory rows from the index and optionally filters by file extension. Returns a filtered data frame with the same schema as the input.
et_index_filter(index, extensions = NULL)et_index_filter(index, extensions = NULL)
index |
A data frame as returned by |
extensions |
Character vector of file extensions to keep, e.g.
|
Data frame with the same columns as the input index (dir, name,
is_dir, size, mtime), filtered to files only.
## Not run: # From a parquet file filtered <- et_index_filter("erda_index.parquet", extensions = c("jpg", "jpeg")) # From an in-memory data frame idx <- et_index_dir(conn, "/storage/2025", "erda_index.parquet") filtered <- et_index_filter(idx, extensions = "jpg") # All files (no extension filter) all_files <- et_index_filter("erda_index.parquet") ## End(Not run)## Not run: # From a parquet file filtered <- et_index_filter("erda_index.parquet", extensions = c("jpg", "jpeg")) # From an in-memory data frame idx <- et_index_dir(conn, "/storage/2025", "erda_index.parquet") filtered <- et_index_filter(idx, extensions = "jpg") # All files (no extension filter) all_files <- et_index_filter("erda_index.parquet") ## End(Not run)
Re-runs ls -la on every directory in the existing index, diffs the
listings against the stored entries, and returns a named list describing
the changes. Newly discovered subdirectories are recursively indexed via
et_index_dir().
et_index_update(conn, remote_path, index_path, update = TRUE)et_index_update(conn, remote_path, index_path, update = TRUE)
conn |
Connection object returned by |
remote_path |
Remote root path (used when recursively indexing new subdirectories). |
index_path |
Local path to the existing parquet index file. |
update |
Logical. If |
Named list list(added = <data.frame>, removed = <data.frame>)
(invisibly). Each data frame has columns dir, name, is_dir,
size (integer, bytes), mtime (character timestamp).
## Not run: conn <- et_sftp_connect(Sys.getenv("ERDA_SHARELINK")) on.exit(conn$disconnect(), add = TRUE, after = FALSE) delta <- et_index_update(conn, "/storage/2025", "./erda_index.parquet") ## End(Not run)## Not run: conn <- et_sftp_connect(Sys.getenv("ERDA_SHARELINK")) on.exit(conn$disconnect(), add = TRUE, after = FALSE) delta <- et_index_update(conn, "/storage/2025", "./erda_index.parquet") ## End(Not run)
Creates a job in the in-memory job store with status "queued".
et_job_create(type, params = list())et_job_create(type, params = list())
type |
Character. Job type (e.g. |
params |
Named list of job parameters. |
Character string: the job ID.
id <- et_job_create("index_full", list(remote_path = "/storage/2025")) et_job_get(id)id <- et_job_create("index_full", list(remote_path = "/storage/2025")) et_job_get(id)
If the job has a background process handle, its status is synced from the process state before returning.
et_job_get(id)et_job_get(id)
id |
Character. Job ID. |
Named list with job details, or NULL if not found.
id <- et_job_create("test", list()) et_job_get(id)id <- et_job_create("test", list()) et_job_get(id)
Syncs status from background process handles before returning.
et_job_list()et_job_list()
A data frame with columns: id, type, status, stage,
created_at, updated_at, last_progress_at. stage and
last_progress_at are NA for jobs that have not reported progress.
Returns an empty data frame if no jobs exist.
et_job_list()et_job_list()
Background workers cannot write to the in-memory job store, so they report
progress by writing a small JSON file that et_job_get() reads. This
returns the path for a given job ID, under a per-process directory in
base::tempdir(). Pass it to the worker and call et_progress_start()
there.
et_job_progress_file(id)et_job_progress_file(id)
id |
Character. Job ID. |
Character string: absolute path of the progress file. The file itself only exists once the worker has reported progress.
et_job_progress_file("index_full_20260101_120000_abc123")et_job_progress_file("index_full_20260101_120000_abc123")
Stores a mirai::mirai() handle on the job and sets the status to
"running". The handle is polled by et_job_get() and et_job_list()
to derive the current status.
et_job_set_process(id, process)et_job_set_process(id, process)
id |
Character. Job ID. |
process |
A |
The updated job list (invisibly).
Update a job's status
et_job_update(id, status, result = NULL, error = NULL)et_job_update(id, status, result = NULL, error = NULL)
id |
Character. Job ID. |
status |
Character. New status: |
result |
Optional result object to attach. |
error |
Optional error message to attach. |
The updated job list (invisibly).
id <- et_job_create("test", list()) et_job_update(id, "running") et_job_update(id, "completed", result = list(rows = 100))id <- et_job_create("test", list()) et_job_update(id, "running") et_job_update(id, "completed", result = list(rows = 100))
Given a character vector of folder names and a local parquet directory,
returns a named logical vector indicating which folders already have a
corresponding .parquet file. This is a pure function: it performs no
network I/O.
et_parquet_folders_done(folders, parquet_dir)et_parquet_folders_done(folders, parquet_dir)
folders |
Character vector of folder names. If full paths are supplied,
|
parquet_dir |
Path to the local parquet directory. |
Named logical vector (names equal to folders). TRUE means a
.parquet file already exists for that folder.
tmp <- tempdir() writeLines("", fs::path(tmp, "2025_07_10-11_10.parquet")) et_parquet_folders_done(c("2025_07_10-11_10", "2025_07_11-08_00"), tmp)tmp <- tempdir() writeLines("", fs::path(tmp, "2025_07_10-11_10.parquet")) et_parquet_folders_done(c("2025_07_10-11_10", "2025_07_11-08_00"), tmp)
AMI traps produce five filename formats:
et_parse_ami_filename(filenames)et_parse_ami_filename(filenames)
filenames |
Character vector of filenames (with or without extension). |
Motion (standalone): <YYYYMMDDHHmmss>-<sequence>-<trigger>.ext
(e.g. 20230831124747-00-01.jpg)
Motion (IoT): <YYYYMMDDHHmmss>.ext
(e.g. 20250508104842.jpg) — bare 14-digit timestamp, no sequence or
trigger counter
Snapshot (with camera id): <camera_id>-<YYYYMMDDHHmmss>-<type>.ext
(e.g. 01-20230831225959-snapshot.jpg)
Snapshot (no camera id): <YYYYMMDDHHmmss>-<type>.ext
(e.g. 20250527110000-snapshot.jpg)
Mothbox: <trap_name>_<YYYY>_<MM>_<DD>__<HH>_<MM>_<SS>_<HDR>.ext
(e.g. wryNinox_2026_05_29__23_02_29_HDR0.jpg) — scheduled captures
with underscore-delimited date/time separated by double underscore
This function extracts the trigger counter (or camera ID for snapshots),
timestamp, and image type from each filename. All formats are detected
automatically. Filenames that match none of the patterns produce NA
values for all columns.
A data frame with columns trigger (character: trigger counter for
motion images, camera ID for snapshot images that include one, or NA),
timestamp (character, YYYY-MM-DD HH:MM:SS), type (character,
typically "snapshot" or "motion"), and sequence (character, motion
sequence number or NA for snapshots). One row per filename.
et_parse_ami_filename(c( "01-20230831225959-snapshot.jpg", "20250527110000-snapshot.jpg", "20230831124747-00-01.jpg", "20250508104842.jpg", "wryNinox_2026_05_29__23_02_29_HDR0.jpg" ))et_parse_ami_filename(c( "01-20230831225959-snapshot.jpg", "20250527110000-snapshot.jpg", "20230831124747-00-01.jpg", "20250508104842.jpg", "wryNinox_2026_05_29__23_02_29_HDR0.jpg" ))
CamAlien filenames encode metadata as underscore-delimited key-value pairs
(e.g. CT_1234_GT_5678_lon_12.34_lat_56.78_alt_120.5_gain_1.4.jpg). This
function strips the file extension and splits on _, treating odd-positioned
tokens as keys and even-positioned tokens as values.
et_parse_camalien_filename(filenames)et_parse_camalien_filename(filenames)
filenames |
Character vector of filenames (with or without extension). |
If the number of tokens is odd, the last value is padded with NA_character_
so that key-value pairing still works.
Known numeric columns (alt, vel, expt, gain) are coerced to
double. Coordinates (lon, lat) and timestamps (CT, GT) are kept as
character to preserve their original string representation. All other
columns are character.
Note: Values containing underscores will cause incorrect key-value
pairing, since _ is also the key-value and pair delimiter.
A tibble::tibble() with one row per filename and one column per
key found in the filenames. Columns not present in a given filename are
filled with NA. Column types: alt, vel, expt, gain are
double; all others are character.
et_parse_camalien_filename(c( "CT_1234_GT_5678_lon_12.34_lat_56.78_alt_120.5_vel_0.0_expt_0.01_gain_1.4.jpg", "CT_9999_GT_0000_lon_8.10_lat_55.40_alt_80.0_vel_2.3_expt_0.02_gain_2.0.jpg" ))et_parse_camalien_filename(c( "CT_1234_GT_5678_lon_12.34_lat_56.78_alt_120.5_vel_0.0_expt_0.01_gain_1.4.jpg", "CT_9999_GT_0000_lon_8.10_lat_55.40_alt_80.0_vel_2.3_expt_0.02_gain_2.0.jpg" ))
Reads a detection CSV produced by IoT AMI trap edge processing
(e.g. results/detect/YYYYMMDD.csv). Parses timestamps and coerces
column types.
et_parse_edge_detections(path)et_parse_edge_detections(path)
path |
Character. Path to the detection CSV file. |
A tibble::tibble() with columns:
Integer. Capture year.
Character. Trap identifier.
POSIXct (UTC). Capture time.
Double. Object detection confidence.
Integer. Detection ID within image.
Double. Bounding box coordinates.
Character. Source image filename.
Character. Taxonomic order label.
Integer. Order ID.
Double. Order classification confidence.
Logical. Whether confidence exceeds threshold.
Integer. Detection key (links to track details).
Character. Species label.
Integer. Species ID.
Double. Species classification confidence.
## Not run: detections <- et_parse_edge_detections("results/detect/20250508.csv") ## End(Not run)## Not run: detections <- et_parse_edge_detections("results/detect/20250508.csv") ## End(Not run)
Reads a track detail CSV produced by IoT AMI trap edge processing
(e.g. results/tracks/YYYYMMDDTRS.csv). One row per detection that belongs
to a track; supplies the key -> track edge_id map used to build the
data.detection_tracks junction.
et_parse_edge_track_detections(path)et_parse_edge_track_detections(path)
path |
Character. Path to the track detail CSV file. |
A tibble::tibble() with columns:
Integer. Track ID from the edge processor (joins to
data.tracks.edge_id).
Integer. Detection key (joins to the detection CSV key).
POSIXct (UTC). Detection time.
Double. Species classification confidence (percent).
Logical. Whether confidence exceeds the threshold.
Character. Taxonomic order label.
Character. Species-level label.
Integer. Bounding-box centre (pixels).
Integer. Top-left corner of the bounding box (pixels).
Integer. Bounding-box dimensions (pixels).
Integer. Bottom-right corner (x1 + width, y1 + height).
Character. Source image filename (already a basename).
## Not run: trs <- et_parse_edge_track_detections("results/tracks/20250508TRS.csv") ## End(Not run)## Not run: trs <- et_parse_edge_track_detections("results/tracks/20250508TRS.csv") ## End(Not run)
Reads a track summary CSV produced by IoT AMI trap edge processing
(e.g. results/tracks/YYYYMMDDTR.csv). Parses timestamps and coerces
column types.
et_parse_edge_tracks(path)et_parse_edge_tracks(path)
path |
Character. Path to the track summary CSV file. |
A tibble::tibble() with columns:
Integer. Track ID from edge processor.
POSIXct (UTC). Start of track.
POSIXct (UTC). End of track.
Integer. Duration in seconds.
Character. Species label (majority vote).
Integer. Number of frames in track.
Double. Classification consistency in percent.
Double. Mean bounding box area in pixels squared.
Double. Total pixel displacement.
## Not run: tracks <- et_parse_edge_tracks("results/tracks/20250508TR.csv") ## End(Not run)## Not run: tracks <- et_parse_edge_tracks("results/tracks/20250508TR.csv") ## End(Not run)
Finds detection CSVs (results/detect/YYYYMMDD.csv) and track-detail CSVs
(results/tracks/YYYYMMDDTRS.csv) in an ERDA index, downloads each via SFTP,
and writes data.detections, data.classifications, and the
data.detection_tracks junction. Source images, sessions, and tracks must
already exist in the database (written upstream); this function fetches their
UUIDs via et_db_get_sourceimages() and et_db_get_tracks().
et_process_edge_detections( conn, dbcon, idx_raw, sessions, deploymentid, algorithm = "tracy", algorithmversion = "luke", order_algorithm = "mcc24", species_algorithm = "uk-denmark-moths" )et_process_edge_detections( conn, dbcon, idx_raw, sessions, deploymentid, algorithm = "tracy", algorithmversion = "luke", order_algorithm = "mcc24", species_algorithm = "uk-denmark-moths" )
conn |
SFTP connection object from |
dbcon |
A DBI connection object. |
idx_raw |
Data frame. Raw ERDA index (as returned by |
sessions |
Data frame. Sessions with |
deploymentid |
Character. Deployment ID, used to fetch source-image UUIDs. |
algorithm |
Character. Tracker algorithm name for the junction rows.
Must match the tracks written upstream. Default |
algorithmversion |
Character. Tracker algorithm version for the junction
rows. Must match the tracks written upstream. Default |
order_algorithm |
Character. Algorithm slug for order classifications.
Default |
species_algorithm |
Character. Algorithm slug for species
classifications. Default |
Writes happen in foreign-key-safe order: detections, then classifications, then the detection-track junction. Each detection CSV is matched to its track-detail CSV and motion session by the session date encoded in the filename.
A named list with integer counts: detections, classifications,
and detection_tracks (rows written).
Finds track summary CSVs in an ERDA index, downloads each via SFTP,
parses them with et_parse_edge_tracks(), maps tracks to motion sessions
using the noon-to-noon date boundary, and writes to the database via
et_db_write_tracks().
et_process_edge_tracks( conn, dbcon, idx_raw, sessions, algorithm = "tracy", algorithmversion = "luke" )et_process_edge_tracks( conn, dbcon, idx_raw, sessions, algorithm = "tracy", algorithmversion = "luke" )
conn |
SFTP connection object from |
dbcon |
A DBI connection object. |
idx_raw |
Data frame. Raw ERDA index (as returned by |
sessions |
Data frame. Sessions with |
algorithm |
Character. Tracker algorithm name stamped on every track.
Must be non-empty. Default |
algorithmversion |
Character. Tracker algorithm version. Must be
non-empty. Default |
Integer. Total number of track rows written.
et_progress_start() points the current process at a job's progress file
(from et_job_progress_file()); call it at the top of a mirai::mirai()
job body. et_progress() then records the job's current stage and counters
in that file, where et_job_get() picks them up and /index/status/{id}
reports them as stage, progress and last_progress_at.
et_progress_stop() detaches the reporter again.
et_progress_start(path) et_progress_stop() et_progress(stage, ..., interval = 2) et_progress_read(path)et_progress_start(path) et_progress_stop() et_progress(stage, ..., interval = 2) et_progress_read(path)
path |
Character. Progress file path, or |
stage |
Character. Short name of the current stage, e.g. |
... |
Named counters describing progress within the stage, e.g.
|
interval |
Numeric. Minimum seconds between writes within one stage. |
Long-running functions (et_index_dir(), et_index_update(), the edge
processors and the database writers) call et_progress() themselves, so a
job only needs to mark the stages between them. Outside a reporting job
(no et_progress_start()), all calls are no-ops.
Writes are throttled to one every interval seconds unless the stage
changes, and each write is atomic (temp file + rename), so a reader never
sees a partial file.
et_progress_start(), et_progress_stop() and et_progress():
NULL, invisibly. et_progress_read(): a list with stage, progress
(named list of counters, or NULL), last_progress_at (POSIXct) and
pid (the worker's process ID), or NULL if the file does not exist or
cannot be parsed.
f <- tempfile(fileext = ".json") et_progress_start(f) et_progress("index", dirs_processed = 10L, files = 250L) et_progress_read(f) et_progress_stop()f <- tempfile(fileext = ".json") et_progress_start(f) et_progress("index", dirs_processed = 10L, files = 250L) et_progress_read(f) et_progress_stop()
Executes one SFTP batch session over the existing ControlMaster connection
returned by et_sftp_connect(), returning standard output as a character
vector.
et_sftp_batch(conn, commands, stdout = TRUE, stderr = FALSE, timeout = 0)et_sftp_batch(conn, commands, stdout = TRUE, stderr = FALSE, timeout = 0)
conn |
Connection object returned by |
commands |
Character vector of SFTP commands, passed to the session
via |
stdout |
Passed to |
stderr |
Passed to |
timeout |
Numeric. Seconds after which the session is killed, passed
to |
Character vector of stdout lines (when stdout = TRUE), or the
integer exit code (when stdout = FALSE). With stdout = TRUE, a
non-zero exit status is attached as the "status" attribute.
## Not run: conn <- et_sftp_connect(Sys.getenv("ERDA_SHARELINK")) on.exit(conn$disconnect(), add = TRUE, after = FALSE) lines <- et_sftp_batch(conn, paste0("ls -1 /storage/2025/")) ## End(Not run)## Not run: conn <- et_sftp_connect(Sys.getenv("ERDA_SHARELINK")) on.exit(conn$disconnect(), add = TRUE, after = FALSE) lines <- et_sftp_batch(conn, paste0("ls -1 /storage/2025/")) ## End(Not run)
Establishes a persistent SSH connection to ERDA using an SSH ControlMaster
socket. Authentication uses SSH_ASKPASS — no sshpass is required. The
function waits up to 5 seconds for the socket to appear and calls
cli::cli_abort() on timeout.
et_sftp_connect( sharelink, host = "io.erda.au.dk", port = "2222", tmp_root = fs::path(tempdir(), "sftp_imgs") )et_sftp_connect( sharelink, host = "io.erda.au.dk", port = "2222", tmp_root = fs::path(tempdir(), "sftp_imgs") )
sharelink |
ERDA sharelink token, used as both the SSH user name and password. |
host |
ERDA SFTP hostname. Defaults to |
port |
ERDA SFTP port. Defaults to |
tmp_root |
Local directory used for the SSH control socket and
temporary downloads. Created if it does not exist. Defaults to a
subdirectory of |
The returned connection object contains a $disconnect() closure that
kills any tracked child SFTP processes, closes the ControlMaster, and
removes the temporary directory. Register it in the calling frame with:
conn <- et_sftp_connect(sharelink, host, port) on.exit(conn$disconnect(), add = TRUE, after = FALSE)
A named list (connection object) with elements:
ctl_optsCharacter vector of SSH -o flags for multiplexing.
remote_targetString "<sharelink>@<host>".
tmp_rootPath to the temporary directory.
portThe SFTP port string.
add_child_pidfunction(pid) — register a background SFTP PID
for cleanup on disconnect.
remove_child_pidsfunction(pids) — deregister finished PIDs.
disconnectParameterless function — kill tracked children,
close the ControlMaster, and remove tmp_root.
## Not run: conn <- et_sftp_connect( sharelink = Sys.getenv("ERDA_SHARELINK"), host = Sys.getenv("ERDA_SFTP_HOST", unset = "io.erda.au.dk"), port = Sys.getenv("ERDA_SFTP_PORT", unset = "2222") ) on.exit(conn$disconnect(), add = TRUE, after = FALSE) ## End(Not run)## Not run: conn <- et_sftp_connect( sharelink = Sys.getenv("ERDA_SHARELINK"), host = Sys.getenv("ERDA_SFTP_HOST", unset = "io.erda.au.dk"), port = Sys.getenv("ERDA_SFTP_PORT", unset = "2222") ) on.exit(conn$disconnect(), add = TRUE, after = FALSE) ## End(Not run)
Calls et_sftp_list_folders() and filters results to entries whose
base::basename() matches the YYYY_MM_DD-HH_MM pattern used by CamAlien
onboard systems.
et_sftp_list_dated_subfolders(conn, country_path)et_sftp_list_dated_subfolders(conn, country_path)
conn |
Connection object returned by |
country_path |
Remote SFTP path to the country folder. |
Character vector of matching folder names (or full paths, depending on what the SFTP server returns).
## Not run: conn <- et_sftp_connect(Sys.getenv("ERDA_SHARELINK")) on.exit(conn$disconnect(), add = TRUE, after = FALSE) subfolders <- et_sftp_list_dated_subfolders(conn, "/storage/2025/portugal") ## End(Not run)## Not run: conn <- et_sftp_connect(Sys.getenv("ERDA_SHARELINK")) on.exit(conn$disconnect(), add = TRUE, after = FALSE) subfolders <- et_sftp_list_dated_subfolders(conn, "/storage/2025/portugal") ## End(Not run)
Runs ls -1 <remote_path> over the SFTP connection and returns the
non-blank, non-prompt lines.
et_sftp_list_folders(conn, remote_path)et_sftp_list_folders(conn, remote_path)
conn |
Connection object returned by |
remote_path |
Remote SFTP path to list. |
Character vector of folder/file names returned by the server.
## Not run: conn <- et_sftp_connect(Sys.getenv("ERDA_SHARELINK")) on.exit(conn$disconnect(), add = TRUE, after = FALSE) folders <- et_sftp_list_folders(conn, "/storage/2025/portugal") ## End(Not run)## Not run: conn <- et_sftp_connect(Sys.getenv("ERDA_SHARELINK")) on.exit(conn$disconnect(), add = TRUE, after = FALSE) folders <- et_sftp_list_folders(conn, "/storage/2025/portugal") ## End(Not run)
Each element of user_comment is expected to be a JSON string of the form:
{"gain": "1.4", "detections": ["1359658 Morus alba L. 9.97668 8.829123", ...]}
Each detection string has the layout:
<plantnet_id> <species tokens...> <probability> <score>
parse_detections(user_comment)parse_detections(user_comment)
user_comment |
Character vector of UserComment EXIF values (one per image row). |
The function is designed to be used with tidyr::unnest() on a list column.
NA, empty, or malformed entries yield a zero-row data frame.
A list of tibble::tibble()s, one per element of user_comment,
each with columns plantnet_id (integer), species (character),
probability (double), score (double).
Strips sftp> prompt lines, blank lines, and ./.. entries from the
raw character vector returned by et_sftp_batch() when running
ls -la <path>. Parses the permission string to determine entry type.
parse_sftp_ls_la(raw_lines)parse_sftp_ls_la(raw_lines)
raw_lines |
Character vector of raw SFTP stdout lines. |
Each line must have at least 9 whitespace-separated fields:
<perms> <links> <owner> <group> <size> <month> <day> <time> <name...>.
Lines with fewer fields are silently dropped.
For symlinks (lrwxrwxrwx), the -> target suffix is stripped from the
name and the entry is treated as a regular file (is_dir = FALSE).
Data frame with columns name (character), is_dir (logical),
size (integer – file size in bytes, NA for directories),
mtime (character – last-modified timestamp from ls -la, fields 6-8
joined with a space, e.g. "Jul 10 11:10" or "Jan 5 2024").