SSC (Spore System Configuration) describes what combinations of characteristics a device can legally have. A knowledge base is a directory of these declarations, compiled by kb-emit into a binary the solver in engine.c loads and evaluates - unmodified on a PC, in a browser, or on a microcontroller.
Every characteristic value is either committed (a user pick, a policy default, or a sensor reading) or derived (a domain narrowed by constraints, recomputed fresh on every solve). The engine never mixes the two: a derived value is recomputed from scratch each time, so it can never drift out of sync with the constraints that produced it.
A characteristic is one of three types:
A characteristic block declares one named, typed slot. SINGLE and MULTI list their legal values with values; RANGE declares its legal interval with intervals. A MULTI is the same as SINGLE with multiValue added.
characteristic MODE { names EN "Mode" textLength 8 values 'ECO' names EN "Eco", 'COMFORT' names EN "Comfort", 'BOOST' names EN "Boost" } characteristic FEATURES { names EN "Optional Features" textLength 10 multiValue values 'AUTOTUNE' names EN "Auto-tune on startup", 'DEADBAND' names EN "Output deadband" } characteristic SETPOINT { names EN "Setpoint" numericLength 4 decimalPlaces 1 unit "C" intervals 0 - 400 }
decimalPlaces/unit on a RANGE are display-only - a UI divides the wire value by 10**decimalPlaces and labels it; the engine itself always solves over the plain integer.
A class groups characteristics into the shape one kind of object has. Each usage inside characteristics can carry modifiers:
required - the config isn't complete until this has a valuedefaultValues [...] - a policy default, committed but replaceable by a later user picknoinput - solver output only; never accepted as a user commit (drive it from a procedure instead)observed - a sensor reading; written with a distinct API (set_cstic_observed, not set_cstic) that skips the normal admission gateclass Thermostat { characteristics TEMP observed, MODE defaultValues ['ECO'], FAN noinput }
An object is a persistent, named instance of a class - the thing constraints, procedures and preconditions actually attach to.
object thermo is_a Thermostat;
A constraint binds one or more ?alias to a class in objects:, optionally gates on a condition:, and narrows what's legal in restrictions: - evaluated fresh on every solve, for every matching combination of objects.
constraint fan_high_CS { objects: ?t is_a Thermostat condition: ?t.MODE = 'BOOST' restrictions: ?t.FAN in ['HIGH'] }
objects: can bind more than one ?alias - the constraint is then evaluated for every matching combination across both classes. specified/not specified tests whether a characteristic (typically an observed sensor) has reported a value at all yet:
constraint hot_hvac_CS { objects: ?h is_a House, ?r is_a Room condition: ?r.TEMP specified and ?r.TEMP > 28 and ?h.HOUSE_MODE = 'AWAY' restrictions: ?r.HVAC in ['FROST'] }
A restriction target can also be another characteristic, not just a literal list - useful for keeping two RANGE fields consistent:
constraint output_bounds_CS { objects: ?p is_a PidLoop restrictions: ?p.OUTPUT_MIN <= ?p.OUTPUT_MAX }
in [...] restricts a SINGLE/MULTI's own domain (what's still allowed) - a plain = is not valid here. includes, only meaningful on a MULTI, tests what's actually chosen right now instead of the domain.
A precondition is a single-object, single-target visibility gate: true makes the target characteristic available, false makes it invisible. Unlike a constraint it has no explicit ?alias - $SELF refers to whichever object the precondition was expanded for.
precondition ki_visible_PRE for PidLoop on KI { condition: $SELF.CONTROLLER_TYPE = 'PI' or $SELF.CONTROLLER_TYPE = 'PID' }
A precondition only ever gates one target on $SELF. For a multi-object, if-guardable version - one alias setting another alias's characteristic invisible or read-only - use a property restriction directly inside a constraint's restrictions: instead:
constraint away_lights_CS { objects: ?h is_a House, ?r is_a Room restrictions: ?r.no_input LIGHTS if ?h.HOUSE_MODE = 'AWAY' }
.invisible, .required, and .no_input are all supported this way; each is re-evaluated fresh on every solve, so a property can appear and disappear as the configuration changes, not just accumulate.
A procedure runs imperative, single-object logic against $SELF - plain arithmetic assignment, or $set/$set_default/$del_default calls. It re-runs, and its target recomputes, on every solve that touches anything it reads.
procedure error_PROC for PidLoop { $SELF.ERROR = $SELF.SETPOINT - $SELF.PROCESS_VALUE } procedure output_PROC for PidLoop { $SELF.OUTPUT = ($SELF.KP * $SELF.ERROR) / 100 }
Supported arithmetic today: + - * / only, evaluated over plain integers. The grammar also parses ** and a set of math functions (abs, round, sqrt, sin/cos/tan and friends) for forward compatibility, but the compiler doesn't emit an opcode for either yet - either one in an expression is skipped with a warning, not silently wrong. A characteristic that's still unset reads as 0 in an expression, same as a plain assignment.
A table maps one or more primary (key) columns to the rest, either inline or from a companion tab-separated .vtable file. Referencing it from a constraint's restrictions: narrows every mapped column both ways - picking a dependent column value narrows the primary column back too, not just primary → dependent.
table POWER_TABLE { characteristics MODE primary, FAN primary, POWER_W file 'power_table.vtable' } constraint power_table_CS { objects: ?t is_a Thermostat restrictions: table POWER_TABLE(MODE = ?t.MODE, FAN = ?t.FAN, POWER_W = ?t.POWER_W) }