module

Kozai::Settings

What the web interface is allowed to change, and where it is kept.

NOTE: this exists for the first five minutes of a station's life.

Everything here can be set in kozai.conf or through the environment, and for a station somebody administers over ssh that is the better way. The case this serves is the other one, which is also the one the project claims: a single-board computer in a shed and an operator holding a phone. Without a settings page the first run shows the Gulf of Guinea and a banner telling you to edit a file — and editing a file over ssh from a phone is the worst minute in the product.

One schema, two consumers

The list below drives the API and the interface. A field added here appears in both, with its label, its help text and its bounds, and cannot appear in one and not the other.

Precedence, and why nothing is silent about it

The order is: configuration file, then environment, then this, then command line flags. So a value set here beats docker-compose.yml, which is what makes the page useful on a station configured entirely through environment variables — and is also a way to leave somebody staring at a compose file wondering why it does nothing.

So it is never silent. Store#shadowing lists every setting here that covers an environment variable, the daemon says so on the way up, and the API reports the origin of every field so the interface can show "set here, overriding $KOZAI_STATION_LATITUDE" rather than pretending the environment was never involved.

[server] is deliberately absent. Changing the host or port of the server you are talking to, from the thing you are talking to it with, cannot end well; and a token editable by anyone who has not got the token is not a token.

Constants

FIELDS = [Field.new("station", "name", Kind::Text, "Station name", "Shown in the interface. Optional."), Field.new("station", "latitude", Kind::Number, "Latitude", "Degrees north, as a GPS or a map gives it.", -90.0, 90.0), Field.new("station", "longitude", Kind::Number, "Longitude", "Degrees east. Both -180..180 and 0..360 are accepted.", -180.0, 360.0), Field.new("station", "altitude", Kind::Number, "Altitude", "Height above the ellipsoid, in METRES — not kilometres.", -500.0, 9000.0), Field.new("prediction", "minimum_elevation", Kind::Number, "Minimum elevation", "Passes peaking below this are not reported.", -90.0, 89.0), Field.new("prediction", "horizon_hours", Kind::Number, "Look ahead", "How many hours of passes to predict.", 0.0, 8760.0), Field.new("prediction", "twilight", Kind::Number, "Twilight", "Sun elevation below which the sky counts as dark. -6 is civil.", -90.0, 90.0), Field.new("prediction", "refraction", Kind::Flag, "Apply refraction", "Report apparent elevations instead of geometric ones."), Field.new("prediction", "satellites", Kind::List, "Satellites worked here", "Names or catalogue numbers, comma separated. Empty means the whole catalogue, which is slow on a large one. A number after a colon is a scheduling priority."), Field.new("schedule", "enabled", Kind::Flag, "Work passes automatically", "Track and tune with nobody at the keyboard."), Field.new("schedule", "lookahead_hours", Kind::Number, "Plan ahead", "How far ahead the schedule is built.", 0.25, 168.0), Field.new("schedule", "minimum_elevation", Kind::Number, "Worth moving for", "Elevation a pass must reach to be scheduled.", -90.0, 89.0), Field.new("schedule", "minimum_duration_seconds", Kind::Number, "Shortest pass", "Below this, swinging the antenna costs more than it returns.", 0.0, 3600.0), Field.new("schedule", "park_at_los", Kind::Flag, "Park after a pass", "Return the antenna to its park position at loss of signal."), Field.new("rotator", "enabled", Kind::Flag, "Rotator", "Point an antenna through hamlib's rotctld."), Field.new("rotator", "host", Kind::Text, "Rotator host", "Where rotctld is listening."), Field.new("rotator", "port", Kind::Number, "Rotator port", "rotctld's port. 4533 is its default.", 1.0, 65535.0), Field.new("rotator", "protocol", Kind::Text, "Rotator protocol", "rotctld, or easycomm to speak Easycomm II directly."), Field.new("rotator", "azimuth_max", Kind::Number, "Azimuth limit", "Above 360 says the rotator has overlap past north, which saves most of a pass that crosses it.", 0.0, 720.0), Field.new("rotator", "deadband", Kind::Number, "Deadband", "Movements smaller than this are not commanded.", 0.0, 45.0), Field.new("radio", "enabled", Kind::Flag, "Receiver", "Tune a receiver through hamlib's rigctld."), Field.new("radio", "host", Kind::Text, "Receiver host", "Where rigctld is listening."), Field.new("radio", "port", Kind::Number, "Receiver port", "rigctld's port. 4532 is its default.", 1.0, 65535.0), Field.new("radio", "transponders", Kind::Text, "Transponder file", "Path to your own transponder table. The compiled-in one is used for anything it does not mention."), Field.new("radio", "downlink_hz", Kind::Number, "Fallback downlink", "Hertz, used only for satellites the transponder table does not cover.", 0.0, 3.0e11), Field.new("radio", "step_hz", Kind::Number, "Retune step", "Smallest retune worth sending, in hertz.", 0.0, 1.0e6)]

Every setting the interface may change.

The bounds repeat the ones in Config, which is duplication with a purpose: Config rejects a bad value at load, and these let the interface refuse it before anybody submits it. Both must agree, and the spec checks that they do.

Log = ::Log.for("kozai.settings")

Class methods

field(section : String, key : String) : Field | Nil

The field for section.key, or nil if it is not editable.

Source
field(qualified : String) : Field | Nil

The field for section.key, or nil if it is not editable.

Source
normalise(field : Field, raw : String) : String

Checks a value against its field and returns it in the form the configuration file should hold.

Raises Config::ValueError naming the field. The interface checks first, so reaching this with a bad value means somebody is talking to the API directly — which is allowed, and is exactly when a clear refusal matters.

Source

Nested types