Getting started with spdgt.telem
spdgt-telem.RmdThis package reads and writes telemetry records — individuals, blueprints, devices, deployments, sensor deployments, captures, events and GPS positions — in the SpeedGoat database.
There is nothing to configure. The first call that needs credentials
starts the login flow, and the API reads your project from your token,
so no function here takes a project_id.
Reading
Every read takes filters through ..., which become
filter[...] on the request:
devices <- telem_read_devices()
elk <- telem_read_individuals(species_id = 4)
dead <- telem_read_individuals(fate = "Dead")
recent <- telem_read_positions(
device_id = 20,
from = "2026-01-01",
to = "2026-06-30"
)The filters people ask for most are named
Rather than remembering filter names, the common ones are arguments:
telem_read_individuals(species_id = 4, sex = "Female", fate = "Dead")
telem_read_devices(state = "Deployed")
telem_read_events(type = "Mortality", from = "2026-01-01", to = "2026-06-30")
telem_read_positions(bounds = c(47.0, -124.5, 47.6, -123.8)) # see belowbounds is the exception: it serialises correctly and the
endpoint returns 500 for every value, including a
whole-world box. The failure is in the API’s query building rather than
in what you pass, so there is nothing to get right on this side. The
argument stays because it will work unchanged once the server side is
fixed. Until then, filter positions by deployment and subset
locally.
Three things are worth knowing about which filters exist, because the API is uneven and guessing wastes time:
- Species is only on individuals. Devices, deployments, captures, events and positions carry no species, so you reach them in two steps — read the individuals for a species, then filter on the ids you get back.
-
Space is only on positions, as
bounds— and that filter is currently broken server-side, as above. Other endpoints have barelatitude/longitude, which test for equality and are no use as a box. -
Time is on every endpoint but under a different
column each — the event date, the capture date, the deployment start.
fromandtomap to whichever one the endpoint means, so you do not have to know its name.
# Every position for one species.
elk <- telem_read_individuals(species_id = 4)
telem_read_positions(individual_id = paste(elk$id, collapse = ","))A filter value takes a small vocabulary beyond plain equality:
| Written as | Means |
|---|---|
"Dead" |
equals |
"!Dead" |
does not equal |
"a,b,c" |
any of these |
"~elk" |
contains |
"?" / "!?"
|
is null / is not null |
">10" / "<10"
|
greater / less than |
There is no “between”: ">a,<b" does not do what it
looks like. That is why periods are from and
to rather than one string.
Reads return everything by default
The API paginates at 100 records. These functions ask for all of them, because a read that silently returns the first page is a correctness bug waiting to happen — on one project’s device list that was 100 of 179 records, with nothing to say the rest existed.
Pass pages when you would rather page deliberately:
telem_read_devices(pages = list(size = 100)) # first 100
telem_read_devices(pages = list(size = 100, number = 2)) # second 100A read with no filters at all asks the API how many records there are
before fetching them, and warns when that is more than
max_records — 50,000 by default. The event tables run to
millions of rows, so the whole of one is rarely what anybody meant.
Filtering, or passing pages explicitly, skips the
check.
Finding out what an endpoint accepts
Rather than guessing filter names, ask the API to describe itself:
# Every endpoint, and the actions each allows.
spdgt.auth::api_discover("telemetry")
# What one endpoint offers: the filters, includes, sorts and pagination
# for reads.
spdgt.auth::api_discover("telemetry", "/events")Discovery is built from the API’s OpenAPI annotations, which have
drifted from the validation rules that actually run — so for the fields
a write accepts, prefer telem_fields() below.
Writing
Creates take a data frame, one row per record, and go in batches:
telem_create_devices(tibble::tibble(
serial_number = c("103633", "103635"),
state = "In-Hand"
))Updates take a single row containing id; deletes take an
id:
telem_update_device(tibble::tibble(id = 20, state = "Deployed"))
telem_delete_device(20)Ordering matters once records reference each other, and a create
returns something less convenient than it looks. Both are covered in
vignette("loading-data").
A create also refuses to run when your session carries no project.
The API would accept such a write and report success, but the row it
makes cannot be read, updated or deleted afterwards by anyone — so the
guard is worth the interruption.
spdgt.auth::auth_switch_project() is the fix it names.
What the API accepts
Every create checks your data before building a request, so a bad
column fails locally with the whole list of problems rather than
returning a 422 partway through a batch. telem_fields() is
the list of writable fields per endpoint, and
telem_validate() runs the same check on its own:
telem_fields("devices")
#> # A tibble: 9 × 9
#> resource field presence na_ok type enum max_len min_val max_val
#> <chr> <chr> <chr> <lgl> <chr> <chr> <int> <dbl> <dbl>
#> 1 devices serial_number required FALSE character NA 50 NA NA
#> 2 devices state present TRUE character telem… 25 NA NA
#> 3 devices blueprint_id optional TRUE integer NA NA NA NA
#> 4 devices price optional TRUE numeric NA NA 0 NA
#> 5 devices purchased_at optional TRUE date NA NA NA NA
#> 6 devices retired_at optional TRUE date NA NA NA NA
#> 7 devices frequency optional TRUE numeric NA NA 0 NA
#> 8 devices notes optional TRUE character NA NA NA NA
#> 9 devices project_id optional TRUE integer NA NA NA NA
telem_validate(data.frame(serial_number = "103633"), "devices")
#> Error in `telem_validate()`:
#> ! `data` is not ready to send to "devices".
#> ✖ Required column state is missing.vignette("loading-data") covers what the columns of that
table mean.
Enumerated fields have a fixed vocabulary, exposed so a value can be checked against the list rather than guessed at:
telem_device_states()
#> [1] "Unknown" "Deployed" "On" "Off"
#> [5] "Paused" "Halted" "Faulty" "Offline"
#> [9] "Low-Battery" "Depleted" "Dropped" "In-Hand"
#> [13] "In-Repair" "At-Warehouse" "At-Manufacturer" "Decommissioned"
#> [17] "Retired"
telem_fates()
#> [1] "Unknown" "Alive" "Sick" "Injured" "Dead"
telem_capture_methods()
#> [1] "Dart" "Net-Gun" "Free-Range" "Culvert" "Snare"
#> [6] "Leg-Hold"What you are allowed to do
The API decides from your role, not from which package you installed:
| Operation | Requires |
|---|---|
| read, create, update | membership of the project |
| delete | the Admin role, and membership of the project |
Without the Admin role a delete returns 403 rather than deleting.
Seeing a request without sending it
Every function takes dry_run, and verbosity
up to 3 for the full exchange:
telem_create_devices(devices, dry_run = TRUE, verbosity = 2)