class

Kozai::Api

Inherits Reference < Object

The HTTP JSON API, and the server that also hands out the web interface.

Every answer carries the age of the elements behind it

This is the one thing the API does that a naive one would not. SGP4's accuracy decays with time from the element set epoch — a few hundred metres after a day, kilometres after a week, and eventually nonsense — but the numbers coming out never look any less confident. A client that cannot see the age has no way to tell a good prediction from a useless one, so every response that depends on a satellite reports elements_age_seconds and a stale flag.

Routing

Hand-rolled, splitting the path on slashes. Regular expressions would pull PCRE2 into a binary that otherwise has no C dependencies beyond the runtime, and for a dozen fixed routes they would not earn their place anyway.

Constants

Log = ::Log.for("kozai.api")
STALE_AFTER = 3.days

Age beyond which element sets are reported as stale.

NOTE: three days is a judgement, not a threshold with physics behind it. Below a day the error is normally under a kilometre; by a week a low satellite can be tens of seconds out in time of pass, which is enough to miss it. Three days is where a warning starts being more useful than annoying.

Constructors

new(catalog : Catalog::Store, observer : Observer, config : Config = Config.new, rotator : Rotator::Tracker | Nil = nil, radio : Radio::Tuner | Nil = nil, transponders : Transponders::Table = Transponders::Table.new, settings : Settings::Store | Nil = nil, base_config : Config | Nil = nil)
Source

Class methods

secrets_equal?(expected : String, offered : String) : Bool

Compares two secrets without leaking where they first differ.

NOTE: written out rather than taken from the standard library, for the same reason the asset ETag is a hand-rolled FNV-1a: crypto/subtle is not on the list of requires this project allows itself, and CI greps for that. Eight lines is a smaller price than a C library.

The lengths are compared first and separately, which does leak the length of the secret. That is deliberate and harmless — an attacker who knows how many characters to guess is no closer to knowing which.

Source

Instance methods

catalog

The satellite catalogue being served.

Source
config

Configuration in force.

Source
handle(context : HTTP::Server::Context) : Nil

Routes one request.

Source
listen(host : String = "127.0.0.1", port : Int32 = 8080) : Nil

Binds and serves until interrupted.

Source
manual=(manual : Bool)

Whether an operator has taken the antenna by hand.

NOTE: set by POST /api/rotator/track and cleared by stop, and the scheduler keeps its hands off while it is set. Without it the daemon would take the antenna back at the next slot boundary — in the middle of whatever the operator had deliberately pointed it at.

Source
manual?

Whether an operator has taken the antenna by hand.

NOTE: set by POST /api/rotator/track and cleared by stop, and the scheduler keeps its hands off while it is set. Without it the daemon would take the antenna back at the next slot boundary — in the middle of whatever the operator had deliberately pointed it at.

Source
observer

The station passes are computed for.

NOTE: replaceable, because the settings page can move the station. The loops that use it read it from here on every pass rather than capturing it at start, or a station that corrected its coordinates would keep pointing at the old ones until it was restarted.

Source
on_settings_change

Called when the settings change, to rebuild whatever the daemon owns — the rotator and receiver connections, which the API has no business constructing itself.

Source
on_settings_change=(on_settings_change : Proc(Config, Nil) | Nil)

Called when the settings change, to rebuild whatever the daemon owns — the rotator and receiver connections, which the API has no business constructing itself.

Source
on_settings_validate

Called with a candidate configuration before it is written anywhere.

NOTE: this is the difference between a settings page and a way to brick a station. A field can be individually valid and collectively fatal — a transponder path is a perfectly good string and a missing file — and writing it first meant the daemon then refused to start, leaving the operator to delete a file from a volume, which is exactly what somebody holding a phone cannot do. Anything that raises here is refused before it reaches the disk.

Source
on_settings_validate=(on_settings_validate : Proc(Config, Nil) | Nil)

Called with a candidate configuration before it is written anywhere.

NOTE: this is the difference between a settings page and a way to brick a station. A field can be individually valid and collectively fatal — a transponder path is a perfectly good string and a missing file — and writing it first meant the daemon then refused to start, leaving the operator to delete a file from a volume, which is exactly what somebody holding a phone cannot do. Anything that raises here is refused before it reaches the disk.

Source
plan

What the scheduler intends to work, if it is running.

Source
plan=(plan : Schedule::Plan | Nil)

What the scheduler intends to work, if it is running.

Source
radio

Radio control, if enabled.

Source
reconfigure(config : Config) : Nil

Takes a changed configuration into use without a restart.

NOTE: everything derived from the configuration has to be rebuilt here, and the list is the interesting part. The observer, obviously. The watchlist, because it is parsed from the configuration and cached against the catalogue's load time — which has not changed, so it would otherwise keep the old answer. The schedule, because a plan built for one place is wrong in another. And the rotator and receiver, through the callback, because a port or a host may now be different.

Missing any one of these gives a station that says it moved and behaves as though it had not, which is worse than requiring a restart. Rebuilt from the configuration as it stood before the settings page, plus whatever the page holds now. Not by writing over the live one: applying a store can only set values, so a field the operator cleared would keep its old value until the daemon was restarted — and "no restart" is the entire point of the page.

Source
reconfigure

Takes a changed configuration into use without a restart.

NOTE: everything derived from the configuration has to be rebuilt here, and the list is the interesting part. The observer, obviously. The watchlist, because it is parsed from the configuration and cached against the catalogue's load time — which has not changed, so it would otherwise keep the old answer. The schedule, because a plan built for one place is wrong in another. And the rotator and receiver, through the callback, because a port or a host may now be different.

Missing any one of these gives a station that says it moved and behaves as though it had not, which is worse than requiring a restart. Rebuilt from the configuration as it stood before the settings page, plus whatever the page holds now. Not by writing over the live one: applying a store can only set values, so a field the operator cleared would keep its old value until the daemon was restarted — and "no restart" is the entire point of the page.

Source
retarget_hardware(rotator : Rotator::Tracker | Nil, radio : Radio::Tuner | Nil, transponders : Transponders::Table) : Nil

Replaces the hardware control after a settings change.

Source
rotator

Rotator control, if enabled.

Source
searchable

The satellites a catalogue-wide search should cover.

The watchlist when there is one and it matched something, the whole catalogue otherwise. A list that matched nothing falls back rather than returning an empty station: the entries are reported as unknown, and an operator staring at a blank pass table learns less than one staring at every pass plus a line saying which of their satellites was not found.

Source
selection

The configured satellites, matched against whatever is loaded now.

NOTE: memoised against the catalogue's load time rather than computed per request. Resolving a name fragment is a scan of the catalogue, and on a full one that is tens of thousands of string comparisons for an answer that only changes when the elements are reloaded.

Source
server

Builds the HTTP server.

Source
settings

Settings written from the interface, if any.

Source
started_at

When the daemon started, for the uptime report.

Source
tracked

Catalog number of the satellite being tracked, if any.

Source
tracked=(tracked : Int32 | Nil)

Catalog number of the satellite being tracked, if any.

Source
transponders

What frequency to use for which satellite.

Source
tune(satellite : SGP4::Satellite, id : String | Nil = nil) : Transponders::Transponder | Nil

Points the receiver at satellite, using id if one was asked for and the satellite's default transponder otherwise.

Returns the transponder tuned, or nil if there is no radio or nothing is known about that satellite. Raises ArgumentError when a transponder was named and either does not exist or belongs to a different satellite — silently tuning something else would be worse than refusing.

Source