Scripted Snapper · complete user-facing reference for the embedded JavaScript scripting engine (v1)
A script is plain JavaScript (ES2022, single file, strict mode, top-level
await supported) that drives your camera over local Wi-Fi entirely
on-device - no network, no accounts, no telemetry. Anything the app UI can do, a
script can do, plus environment data the UI does not expose directly (sun/moon
geometry, shadows, moon phase).
Scripts are sandboxed and security-hardened: no filesystem, no
network, no host environment access. Scripts can only touch the documented
namespaces in this reference - everything else is unavailable, not restricted.
There is no Math.random (use rng), no host timers
(use time.*), and no wall-clock reads outside time.*.
A script run proceeds: saved → compiled → validated → estimated → running → finished | stopped (user) | killed (limit) | error. A running script is the session: session state, the trial cap, keep-awake, and the disconnect alarm all apply exactly as for the built-in intervalometer. A script run cannot be paused manually (run/stop only); the only automatic pause is the camera-loss pause policy state, where the in-flight camera call simply waits.
Pressing Stop requests a clean stop: the script's next
step observes it and receives a ScriptStopped error. Stopping is
safe - counters are preserved and any in-flight shot command is released.
Scripts have no shot-count or duration bounds by design: a script runs until it returns, you stop it, the trial cap is reached, or a session failure occurs. Multi-day time-lapses are a first-class use case. The editor's pre-run estimate runs a dry-run simulation and reports the full-run step count - it also sizes the internal step budget for you when you save (below).
Every call into the documented API counts as two steps
(entry and completion); long-running host calls (shots, bulb holds, location
acquisition) additionally count a step on each internal progress tick. The only
free call is location.get() (a pure cache read). The engine counts
steps and execution time so a buggy script can never freeze the app:
| Limit | Default | Hard maximum | Where configured |
|---|---|---|---|
| CPU time (cumulative busy) | internal 24 h backstop | none user-facing | not user-configurable |
| Steps per run | 10 000 | none | set automatically by the editor when you save (from the step estimate) |
| Run duration (wall-clock) | - | none | not limitable |
If a script loops without calling the API for more than 500 ms of
uninterrupted script execution, or exhausts its step budget or memory, it
terminates with a readable ScriptLimitExceeded error in the script
console - the session is not corrupted (counters preserved, shot released). The
500 ms rule applies only to script CPU between steps - never to a
legitimate in-flight camera call. The step budget grows with your script
automatically: the editor sizes it from the step estimate every time you save,
so long multi-day loops just work.
Per-call deadlines bound long individual calls (a generous maximum per call type, enforced with a readable result instead of a freeze):
| Call | Deadline | Result on deadline |
|---|---|---|
camera.focus() | fixed 10 s | status: timeout |
camera.takePicture() / takePictureAF() / burst(n) |
camera confirmation window | status: timeout (the shot may still have fired) |
camera.bulbShot(seconds) | declared duration + 30 s | status: timeout |
location.refresh() | 10 s quick refresh; 5 min for the first fix after a cold start | throws LocationUnavailable |
A camera call blocked in the camera-loss wait state (below) is exempt from the watchdog and deadlines - the session policy, not the CPU limiter, governs that wait.
If the Wi-Fi link to the camera is lost mid-run, the session follows your camera-loss policy (the same values as the intervalometer: pause, continue, abort) and auto-reconnects:
paused) -
and proceeds automatically on reconnect.takePicture, takePictureAF, burst,
the set* exposure calls, focus, the bulb calls)
return records with a cameraDisconnected status, while the
three read calls (getCapabilities, getExposure,
getFocus) throw a catchable
CameraDisconnected error instead.The trial cap (50 shots in trial mode) is enforced by the
app, not the script: once the cap is spent, camera.takePicture(),
burst() and bulbShot() return
trialLimit records, and the session force-stops the script (its
next step receives ScriptStopped). No script logic can continue
past the cap. session.trialCapRemaining lets well-behaved scripts
plan ahead.
camera.*,
sky.*, location.*, time.*,
exposure.*, log.* and rng function
returns a Promise - await every one, including
read-only queries (sky.hour, time.now,
camera.getExposure, location.get, …). Reading a
property on the Promise instead of the awaited value never throws - it is
undefined, and loop conditions built on it silently never hold.
Only the bare session.* getters and the
constants / sky.HOURS namespaces are plain
synchronous values.shutter in seconds (sub-second as an ordinary numeric
expression, e.g. 1/250; ≥ 1 s as 2,
30), aperture as f-value (2.8,
8, 22), iso as a number or
'auto' where supported. No value objects, no index forms.TypeError. Wrong value
types (non-numeric shutter, malformed settings object, unknown enum value,
unknown option key, negative sleep) throw a TypeError whose
message states what was expected. Valid camera-level problems never throw -
they come back as statuses.rng() is seeded per run and
the seed is recorded in the run record - the same seed reproduces the same
shot sequence.| Namespace | Description |
|---|---|
| camera | Exposure control (shutter, aperture, ISO), focus control, shots, bursts, and bulb exposures. |
| session | Read-only session state: status, photo count, shots remaining, running / time remaining, trial cap remaining. |
| exposure | Light-amount math: scene EV for a setting and relative light ratios between settings. |
| sky | Offline sun & moon tracker: named hours, live geometry, moon state, and custom altitude/azimuth window queries. |
| location | Device GPS (no network): the cached session position and explicit refresh. |
| time | Device wall time, suspension (sleep), and time-until math. |
| log | Timestamped script console output, persisted with the run record. |
| rng | Deterministic pseudo-random numbers (seeded per run). |
| constants | Frozen objects of named string constants for every status / mode / phase enum. |
{ ms, s, min, h }: ms is the
exact integer; s, min, h are the same
duration in seconds/minutes/hours (fractions). Returned by
session.runningTime, session.timeRemaining and
time.until() - pick the unit you want.
ExposureSettings - flat { shutter, aperture, iso }
numbers (extra keys ignored, so camera.getExposure() is accepted
wherever an ExposureSettings is expected).
LocationFix - frozen { lat, lon, accuracy, timestamp }:
decimal degrees, accuracy in meters, fix time as ISO datetime.Camera control over the camera's built-in web server on your local Wi-Fi.
All calls are async and are steps. All exposure values are flat numbers
(see conventions). Camera-level problems
come back as statuses in the result record, except the three read
calls, which throw CameraDisconnected when the link is still lost
(see Camera loss).
| Function | Description |
|---|---|
| getCapabilities() | Snapshot of shooting modes, value ladders, and features. |
| getExposure() | Current exposure settings, shooting mode, and settable flags. |
| setShutter(value) | Set the shutter speed. |
| setAperture(value) | Set the aperture (f-stop). |
| setIso(value) | Set the ISO (or 'auto'). |
| setExposure(settings) | Set several exposure parameters in one call. |
| getFocus() | Current focus state (AF/MF and AF method). |
| focus() | Autofocus pass - no shot (half-press). |
| takePicture() | Trigger one shot with the current focus. |
| takePictureAF() | Autofocus + shot in one call. |
| burst(n) | n shots at the camera's own continuous rate. |
| bulbShot(seconds) | One-shot bulb exposure of a fixed duration. |
| bulbStart() / bulbStop() | Manual bulb open/close. |
Description: One-shot snapshot of what the connected camera supports: available shooting modes, the exact value ladders per mode, and feature flags. Any value you set later is snapped to the nearest ladder step.
Returns: frozen { modes: string[], perMode: Record<string, {
shutter: number[], aperture: number[], iso: Array<number | 'auto'> }>,
features: string[] } - shutters in seconds (sub-second as fractions, e.g.
0.008 = 1/125); features is a subset of
['bulb', 'af', 'burst'] - check it before relying on a feature.
Errors: throws CameraDisconnected when
the link is still lost (see Camera loss);
otherwise blocks while the session waits for the camera.
const caps = await camera.getCapabilities();
// caps.perMode.M.shutter → [30, 15, 8, 4, 2, 1, 0.5, …, 0.008]
// caps.features → ['bulb', 'af', 'burst']
Description: Reads the camera's current exposure settings, shooting mode,
and which parameters are settable in that mode. settable is live - it changes
automatically if the user turns the mode dial on the body.
Returns: frozen { shutter, aperture, iso, mode, settable } -
flat numbers (iso may be 'auto'; a field the camera does
not report is absent), mode a constants.MODE.* value,
settable = { shutter: boolean, aperture: boolean, iso: boolean }.
Errors: throws CameraDisconnected when
the link is still lost.
const ex = await camera.getExposure(); // { shutter: 1/4, aperture: 8, iso: 100, mode: 'manual', … }
const ev = await exposure.lightAmount(ex); // extra keys ignored - getExposure() works as ExposureSettings
Description: Sets one exposure parameter on the camera, honouring the current shooting mode. The value is snapped to the nearest supported ladder step. None of these accepts "bulb" - use camera.bulbShot(seconds).
Parameters:
setShutter: seconds > 0 (sub-second as an ordinary numeric expression,
1/250; ≥ 1 s as 2, 30).
setAperture: f-value > 0 (2.8, 8, 22).
setIso: number > 0, or 'auto' where the camera supports it.
Returns: frozen { status, applied } -
status = constants.EXPOSURE.ok | notSettable | busy | error
(plus cameraDisconnected when the link is still lost).
notSettable means the parameter is not settable in the current
shooting mode - a status, not an error. applied =
{ shutter } / { aperture } / { iso } with
the snapped value actually applied, or null when nothing was
applied.
Errors: throws TypeError for an
invalid value (non-numeric, ≤ 0, non-finite; setIso also rejects
anything other than a number or 'auto'). Camera-level outcomes are
in the record, never thrown.
const r = await camera.setAperture(8);
if (r.status === constants.EXPOSURE.notSettable) log.warn('aperture locked by mode');
Description: Sets several exposure parameters in one call, applied
per parameter - a notSettable parameter does not block the others.
Parameters: settings - a flat object; any of
shutter, aperture, iso may be present; only the
fields given are touched. Field values follow the same rules as the individual
setters.
Returns: frozen { status, applied } -
applied maps each field that applied to its snapped value (empty
or null when nothing applied); status is the
least-good outcome across the given fields
(ok | notSettable | busy | error, plus
cameraDisconnected when the link is still lost).
Errors: throws TypeError if
settings is not a flat object or a field value is invalid.
await camera.setExposure({ aperture: 8, iso: 100, shutter: 1 / 4 });
Description: Reads the camera's current focus state.
Returns: frozen { mode, method } -
mode = constants.FOCUS_MODE.af | mf (the camera's AF/MF
switch), method = the camera's AF method as a raw string
(camera-specific, e.g. single-shot / continuous AF).
Errors: throws CameraDisconnected when
the link is still lost.
if ((await camera.getFocus()).mode === constants.FOCUS_MODE.af) { /* AF available */ }
Description: Runs a standalone autofocus pass - no shot is
taken (the camera's half-press / AF-only path). The focus stays where it
landed: subsequent takePicture() calls reuse it without refocusing.
This is how a script "locks" focus before a long run (time-lapse / astro pattern).
Returns: frozen { status } -
status = constants.FOCUS.ok | notSettable | busy | timeout |
cameraDisconnected.
Statuses: notSettable - camera in MF mode or no AF
support; timeout - AF did not converge within the fixed 10 s
per-call deadline. No shot is taken on any non-ok status.
const f = await camera.focus(); // focus once …
if (f.status === constants.FOCUS.ok) {
while ((await time.until(goldenEnd)).ms > 0) {
await camera.takePicture(); // … then shoot with focus locked
await time.sleepS(30);
}
}
Description: Triggers one shot with the camera's current
focus - it never re-triggers autofocus (focus is a separate action:
focus() / takePictureAF()) and suspends until the camera
responds. The shot counts (session counter, trial cap) only on the camera's
confirmation.
Returns: frozen { status, shotNumber, takenAt } -
status = constants.SHOT.ok | busy | timeout |
cameraDisconnected | trialLimit; on ok,
shotNumber = the session shot count and takenAt = ISO
datetime; otherwise both are null.
Statuses: busy - a bulb exposure is open or another shot
in flight; timeout - the camera did not confirm in time (the shot
may still have fired); cameraDisconnected - link lost and not
recovered (see Camera loss);
trialLimit - cap spent: the shot is not taken and the
session force-stops the script.
const r = await camera.takePicture();
if (r.status === constants.SHOT.ok) log.info('shot ' + r.shotNumber);
Description: Autofocus and release in a single call -
the camera's native AF+release path (one step, not two). Use for single shots
where the subject may have moved; for multi-shot runs prefer focus()
once + a takePicture() loop (no refocus per frame).
Returns: frozen { status, af, shotNumber, takenAt } -
af = constants.FOCUS.* (the AF part),
status = constants.SHOT.* (the shot part). When
af is not ok, the shot does not happen and
shotNumber / takenAt are null.
af is null when no AF attempt ran at all
(trial cap spent, a bulb open, or the link lost).
const r = await camera.takePictureAF(); // single portrait shot - subject may have moved
Description: Takes n consecutive shots at the camera's own
continuous rate in one call.
Parameters: n - integer ≥ 1
(anything else is a TypeError).
Returns: frozen array of exactly n
takePicture-shaped records [{ status, shotNumber, takenAt }, …];
the same shot statuses apply per shot. Trial cap: if the cap
is reached mid-burst, only as many shots as remain are fired and the tail of
the array comes back as trialLimit records (the length stays
n). If a bulb is open or the link is lost, every record is
busy / cameraDisconnected respectively.
const results = await camera.burst(3); // one record per frame
Description: One-shot bulb exposure - the recommended bulb pattern: the engine starts the shutter, holds it for the duration, and stops it - one long call; the script suspends for the whole exposure. Can exceed the camera's maximum shutter time (e.g. 45 s on a 30 s-max body).
Parameters: seconds - number > 0, the exposure
duration in seconds (10 = 10 s).
Returns: frozen { status, shotNumber, takenAt, exposure }
- status = constants.SHOT.ok | busy | timeout |
cameraDisconnected | trialLimit plus notSettable (camera
without bulb support); exposure = the actual duration in seconds
(measured; may slightly exceed the request) or null on failure;
shotNumber / takenAt as in takePicture().
Statuses: busy - a bulb is already open;
timeout - the closing command did not land within the per-call
deadline (declared duration + 30 s); trialLimit as above.
await camera.bulbShot(45); // a 45 s astro exposure (see examples below)
Description: Manual bulb control for scripts that must intervene
during the exposure. bulbStart() opens the shutter;
bulbStop() closes it.
Returns: bulbStart() → frozen
{ status }; bulbStop() → frozen
{ status, exposure } with exposure = the actual open
duration in seconds (camera-reported, or null when unavailable).
Statuses: constants.EXPOSURE.* plus cameraDisconnected.
Statuses: busy - bulbStart() when a bulb is
already open, or bulbStop() when no bulb is open;
notSettable on cameras without bulb support. Calling
takePicture() / bulbShot() while a bulb is open
returns busy.
await camera.bulbStart();
while ((await sky.geometry()).sunAlt < -18) await time.sleepS(5);
await camera.bulbStop(); // exposure = the actual open duration in seconds
Read-only session-state variables. Plain values - reading them is not a host call, costs no step, and each is always current when read.
| Variable | Type | Meaning |
|---|---|---|
session.status | constants.SESSION.* |
scheduled | idle | running | paused | finished | error;
paused = the camera-loss pause state;
scheduled = waiting for a scheduled/astronomical start. |
session.photoCount | number | Shots confirmed this run. |
session.shotsRemaining | number | null | Remaining until this run's effective shot cap
(min of stop-condition N, time-limit-derived count, trial cap);
null only when no cap exists (purchased + unbounded run). |
session.runningTime | Span | Elapsed this run. |
session.timeRemaining | Span | null | Until this run's stop condition. |
session.trialCapRemaining | number | null | Shots left on the 50-shot trial cap; null when purchased. |
while (session.status === constants.SESSION.running) { /* … */ }
Light-amount math over flat ExposureSettings. Pure computation.
Description: Computes the scene light / EV for an exposure setting.
Formula: EV = log2(N²/t) − log2(ISO/100) - the ISO-100 exposure value
corrected to scene light. Comparing EV across settings is only meaningful at
constant ISO; for relative comparisons (different ISO) use
lightRatio.
Parameters: settings - ExposureSettings (flat numbers;
extra keys ignored, so camera.getExposure() is accepted directly).
Pass a complete, valid setting.
Returns: number - scene EV, 1/3-EV precision. Lower = less light.
Errors: throws TypeError if the argument is not a
settings object (missing or invalid components then simply yield
NaN, not a throw).
const ev = await exposure.lightAmount(await camera.getExposure());
if (ev < 10) await camera.setExposure({ shutter: 1 }); // admit more light as it falls
Description: Relative light comparison between two settings - covers
cases (different ISO) that lightAmount cannot compare directly.
Parameters: a, b - complete, valid
ExposureSettings (all three fields, positive numbers).
Returns: number - the 2n light factor between the two
settings (2 = one stop more light in b than in
a).
Errors: throws TypeError for an incomplete or
invalid settings object.
await exposure.lightRatio({ shutter: 1/4, aperture: 8, iso: 100 },
{ shutter: 1, aperture: 8, iso: 100 }) // → 2 (one stop more light)
Offline sun & moon tracker - computed on-device from location + time, no
network. Every call uses the session position by default (the GPS
fix acquired at session start, location.get()) and accepts
an optional coords ({ lat, lon }) override - how a script plans a
shoot at another known location. Dates default to today; date
accepts 'today', 'tomorrow', or an explicit ISO date.
All computations are pure and re-run on every call (the "today" / "now"
defaults resolve at call time, so a run that spans midnight or days sees the
values advance). All datetimes are ISO strings. Unknown option keys in the
options object are a TypeError.
| Member | Description |
|---|---|
sky.HOURS | Frozen enum object - the named hours/points vocabulary (see table below). |
| hour(h, opts?) | The daily occurrence of a named hour on a date. |
| next(h, opts?) | Next occurrence of a non-daily event (full/new moon). |
| all(opts?) | All named hours for a date in one object. |
| geometry(opts?) | Live sun/moon altitudes, azimuths, shadow length & direction. |
| moon(opts?) | Moon phase, age, illumination, usable-moonlight flag. |
| when(cond, opts?) | Time ranges on a day inside an altitude/azimuth band. |
sky.HOURS members:
sunrise, sunset, solarNoon, goldenStart, goldenEnd, blueStart, blueEnd,
civilDawn, civilDusk, nauticalDawn, nauticalDusk, astronomicalDawn,
astronomicalDusk, astroDarknessStart, astroDarknessEnd, moonrise, moonset,
moonTransit, fullMoon, newMoon.
Conventions: golden hour = sun 0° → −4°;
blue hour = sun −4° → −6°;
astro darkness = sun below −18° and moon below the
horizon (the usable Milky-Way window). Full/new moon are non-daily events - use
sky.next().
Description: The occurrence of a daily named hour/point (sunrise, golden/blue hours, twilights, astro darkness, moon events) on a given date.
Parameters: h - a sky.HOURS member
(strictly validated - an unknown value is a TypeError listing the
allowed values). coords? - { lat, lon }, default the
session position. date? - 'today' | 'tomorrow' or an
explicit ISO date, default today. For midnight-crossing events (astro darkness),
date is the day on which the night starts.
Returns: ISO datetime string, or null when the event
does not occur on that date (polar day/night).
Errors: LocationUnavailable (no GPS fix - never a silent
fallback position); TypeError for an invalid h.
const end = await sky.hour(sky.HOURS.goldenEnd);
Description: The next occurrence of a non-daily event after a given time - the only correct form for those events (they have no per-day meaning).
Parameters: h - sky.HOURS.fullMoon |
sky.HOURS.newMoon. coords? as in sky.hour.
after? - ISO datetime, default now.
Returns: ISO datetime string.
Errors: LocationUnavailable; TypeError for an unknown h.
const full = await sky.next(sky.HOURS.fullMoon);
Description: All named hours/points for a date in one object - the
bulk form of sky.hour.
Returns: frozen object with one key per sky.HOURS
member; each value is an ISO datetime or null (event not on that date).
Errors: LocationUnavailable.
const t = await sky.all(); // t.sunrise, t.astroDarknessStart, …
Description: Live solar/lunar geometry at a given time and location - not just named hours.
Parameters: coords? as in sky.hour;
time? - ISO datetime, default now.
Returns: frozen { sunAlt, sunAz, moonAlt, moonAz,
shadowRatio, shadowDir } - altitudes, azimuths and shadow direction in
degrees; shadowRatio = 1 : x (shadow length per unit of
object height).
Errors: LocationUnavailable.
if ((await sky.geometry()).sunAlt < -18) { /* sun below -18° */ }
Description: Moon state at a given time and location.
Parameters: coords? as in sky.hour;
time? - ISO datetime, default now.
Returns: frozen { phase, moonAgeDays, illuminationPct,
usableMoonlight } - phase = constants.MOON.*,
illuminationPct = number 0-100, usableMoonlight =
boolean (moon above the horizon and illumination ≥ 50%).
Errors: LocationUnavailable.
(await sky.moon()).usableMoonlight
Description: Custom-interval query - the time ranges on a given day
where the sun is inside an altitude/azimuth band (e.g. "when is the sun behind
the mountains (az 90-135 deg)?"). The workhorse for location planning.
Single-day; for events that straddle midnight, date is the day on
which the night starts and the range spans into the next day.
Parameters: cond -
{ altitudeBetween?: [lo, hi], azimuthBetween?: [lo, hi] } in degrees
(at least one field required). coords?, date? as in
sky.hour.
Returns: array of { start, end } ISO datetime ranges
for that day (possibly empty, possibly several windows).
Errors: LocationUnavailable; TypeError
for a missing/invalid cond.
const late = await sky.when({ altitudeBetween: [0, 5] }, { date: 'tomorrow' }); // late-golden window
const east = await sky.when({ azimuthBetween: [90, 135] }); // sun over the eastern ridge
Device GPS - no network. A fix is acquired at session start
and cached as the session position for the whole run (static, no
periodic refresh). A missing fix does not block the session - it degrades sky
features: location.get() returns null and
sky.* calls without an explicit coords throw
LocationUnavailable (never a silent fallback position).
Description: Returns the cached session position -
the fix every sky.* call is computed with. Pure cache read: no
acquisition, the only API call that costs no step.
Returns: frozen LocationFix { lat, lon, accuracy, timestamp }
(decimal degrees, meters, ISO datetime), or null when no fix exists.
Errors: none (null, not a throw).
const fix = await location.get();
if (fix && fix.accuracy > 200) log.warn('coarse fix');
Description: User-initiated re-acquisition for when a new fix is
really needed (setup moved, coarse initial fix): acquires a fresh fix,
updates the session position (all subsequent sky.*
calls use it), and returns it.
Returns: frozen LocationFix (the fresh fix - now the session position).
Errors: throws LocationUnavailable (permission denied /
no fix within the deadline). Acquisition deadline: 10 s quick refresh when a
fix already exists, 5 min for the first fix after a cold start /
permission grant.
const fix = await location.refresh(); // e.g. after moving the tripod
log.info('now at ' + fix.lat + ', ' + fix.lon + ' ±' + fix.accuracy + ' m');
The only way to read time and the only way to wait - there are no host timers.
Description: Device wall time.
Returns: number - ms epoch (ISO-convertible).
log.info('done at ' + (await time.now()));
Description: Suspends the script for a duration (step suspension - the UI stays responsive). The only built-in way to wait.
Parameters: the duration in milliseconds / seconds / minutes /
hours; must be a finite number ≥ 0 (fractions are OK for
sleepS / sleepH, e.g. time.sleepS(0.5)).
time.sleepS(1) ≡ time.sleep(1000).
Returns: none - the script resumes when the duration has elapsed.
Errors: throws ScriptStopped when a user stop is
observed at the step; TypeError for a negative or non-finite
duration.
await time.sleepMin(1);
Description: Computes the time remaining until an ISO datetime.
Read-only - it does NOT suspend (only time.sleep*
suspends).
Parameters: when - ISO datetime string.
Returns: Span { ms, s, min, h } (≤ 0 once when
has passed) - as with every API call this returns a Promise:
await time.until(when).
Errors: none thrown - an unparseable when resolves to a
zero span (loop guards built on .ms > 0 simply end the loop
instead of crashing an unattended run).
while ((await time.until(goldenEnd)).ms > 0) { /* loop until golden end */ }
Description: Writes a timestamped line to the script console (editor output panel); lines are persisted per run with the run record.
Parameters: msg - string (any value is coerced to a
string).
Returns: none.
log.info('shot ' + r.shotNumber);
Description: Deterministic pseudo-random number generator - the only
randomness available (Math.random is removed). The generator is
seeded per run and the seed is recorded in the run record, so the same seed
reproduces the same shot sequence.
Parameters: none - draw the next value; or seed - a
finite number that re-seeds the run's generator (all later
rng() draws continue from the new seed).
Returns: number in [0, 1).
Errors: TypeError for a non-finite seed.
const jitter = (await rng()) * 500; // 0-500 ms random jitter
await time.sleep(jitter);
Frozen objects of string constants (string value = member name), one per
closed vocabulary. Scripts compare against these named values instead of raw
strings. sky.HOURS stays on sky with the same convention.
| Object | Members | Compared against |
|---|---|---|
constants.SHOT | ok, busy, timeout, cameraDisconnected, trialLimit |
*.status from takePicture / burst / bulb calls |
constants.EXPOSURE | ok, busy, notSettable, error |
*.status from the set* exposure calls and the bulb open/close calls |
constants.FOCUS | ok, notSettable, busy, timeout, cameraDisconnected |
*.status from camera.focus() and the af field of takePictureAF() |
constants.FOCUS_MODE | af, mf |
camera.getFocus().mode |
constants.MODE | manual, aperturePriority, shutterPriority, program, auto |
camera.getExposure().mode |
constants.MOON | new, waxingCrescent, firstQuarter, waxingGibbous, full, waningGibbous, lastQuarter, waningCrescent |
sky.moon().phase |
constants.SESSION | scheduled, idle, running, paused, finished, error |
session.status |
Camera calls can also report cameraDisconnected outside these
lists (see Camera loss) - compare raw
strings for that value or simply handle "not ok".
if (r.status === constants.SHOT.ok) { /* shot confirmed */ }
Errors are thrown, catchable JS errors. Statuses are values returned inside the result record - inspect them, do not expect a throw.
| Error / status | Meaning | Notes |
|---|---|---|
ScriptStopped | User stop requested (or the session force-stopped the run, e.g. on the trial cap); clean exit path. | Catchable - a well-behaved script unwinds gracefully. |
ScriptLimitExceeded | Step/CPU/memory budget exhausted. | Catchable (once); further budget consumption kills the run. Counters are preserved. |
LocationUnavailable | No GPS fix (permission denied / cold start) or acquisition timed out. | Catchable. Never a silent fallback position - pass coords to sky.* to plan without a fix. |
CameraDisconnected | Wi-Fi link to the camera lost and not recovered. | Catchable. Thrown by the camera read calls (getCapabilities, getExposure, getFocus); shot and set calls instead return a cameraDisconnected status. The session follows your camera-loss policy (pause / continue / abort) and auto-reconnects. |
TypeError | Invalid argument (non-numeric value, malformed settings, unknown enum member, unknown option, negative sleep, non-finite seed). | The message states what was expected. This is an input mistake, not a camera condition. |
SHOT.busy / timeout | Camera did not accept the shot in time. | Statuses - scripts implement retry logic. |
EXPOSURE.notSettable | Parameter not settable in the camera's current shooting mode. | Status, not an error - check (await camera.getExposure()).settable first, or handle the status and adapt. |
FOCUS.notSettable / timeout | MF mode or no AF support / AF did not converge within 10 s. | Statuses - no shot is taken; scripts can retry or continue with the last focus. |
SHOT.trialLimit | Trial cap (50 shots) reached. | Status - do not rely on the script: the session engine force-stops the script on the first trialLimit. |
*.cameraDisconnected | Wi-Fi link to the camera lost and not recovered. | Status on shot/set calls - the session follows your camera-loss policy; on a paused policy an in-flight call simply waits for the reconnect instead. |
// Every host-API call is async - await them all, including read-only queries.
log.info('Golden hour time-lapse');
const goldenEnd = await sky.hour(sky.HOURS.goldenEnd);
if (goldenEnd !== null) {
await camera.setExposure({ aperture: 8, iso: 100, shutter: 1 / 4 });
// the trial cap is enforced by the app - no script-side bookkeeping
while ((await time.until(goldenEnd)).ms > 0) {
const r = await camera.takePicture();
if (r.status === constants.SHOT.ok) log.info('shot ' + r.shotNumber);
await time.sleepS(30); // every 30 s
// slow down (lengthen) the shutter as light falls - lower scene EV means
// less light, so admit more: 1/4 s → 1 s is +2 stops (at fixed ISO)
const ev = await exposure.lightAmount(await camera.getExposure());
if (ev < 10) await camera.setExposure({ shutter: 1 });
}
} else {
log.warn('No golden-hour end today - nothing to do');
}
log.info('done at ' + (await time.now()));
// Overnight / multi-day loop: runs until stopped. For runs of several days
// the step budget grows to match automatically when you save (see the step estimate).
while (session.status === constants.SESSION.running) {
const g = await sky.geometry();
// "astro dark": sun below -18° AND moon below the horizon
if (g.sunAlt < -18 && g.moonAlt < 0) {
await camera.setExposure({ aperture: 2.8, iso: 3200 });
await camera.bulbShot(45); // one-shot 45 s bulb - the call IS the long exposure
}
await time.sleepMin(1);
}
// Lock focus once, then shoot every 30 s for 48 h. Night frames (10 s, wide,
// high ISO) and day frames (1/500 s, low ISO) switch automatically on the sun's
// altitude; a lost location fix keeps the last known mode instead of stopping.
const end = (await time.now()) + 2 * 24 * 3600 * 1000;
await camera.focus();
let night = true;
while ((await time.now()) < end) {
try {
night = (await sky.geometry()).sunAlt < -6;
} catch {} // no fix right now - keep the last known mode
await camera.setExposure(night
? { shutter: 10, aperture: 2.8, iso: 3200 }
: { shutter: 1 / 500, aperture: 8, iso: 100 });
const r = await camera.takePicture();
if (r.status !== constants.SHOT.ok) log.warn('shot failed: ' + r.status);
await time.sleepS(30);
}
log.info('sky time-lapse done: ' + session.photoCount + ' frames');