API Reference
mirror-api
The shared contract in :mirror-api that both the app and every plugin compile against. Package org.speculum.
MirrorModule
org.speculum.core.MirrorModule — base class every module extends. Modeled on MagicMirror's Module.register. Lifecycle: start() → (loop refresh()) → Content() rendered → stop().
abstract class MirrorModule(
val config: ModuleConfig
) : NotificationListener {
val name: String get() = config.module
val region: Region get() = config.regionEnum
open val refreshIntervalMs: Long get() = config.refreshIntervalMs
open fun start(scope: CoroutineScope) {} // once, at boot
open suspend fun refresh() {} // every refreshIntervalMs
open fun stop() {} // at shutdown
override fun onNotification(notification: Notification) {} // bus events
@Composable
abstract fun Content() // the UI — required
}
| Member | Notes |
|---|---|
start(scope) | Called once. Launch coroutines in scope (auto-cancelled on shutdown) — clocks, animations, one-off setup. |
refresh() | Engine calls it on a loop every refreshIntervalMs (first call immediate). Put data fetching here. Return 0 from refreshIntervalMs to skip the loop. |
stop() | Release resources. |
onNotification(n) | Receive bus events (e.g. "ALL_MODULES_STARTED"). |
Content() | Required. The composable UI. Hold state in mutableStateOf(...); assigning recomposes. |
Only Content() is required; override the rest as needed.
ModuleConfig
org.speculum.config.ModuleConfig — one entry in the modules array, equivalent to one object in MagicMirror's config.js. config is a free-form string map so each module reads its own options.
@Serializable
data class ModuleConfig(
val module: String,
val position: String = "top_left",
@SerialName("refreshInterval") val refreshIntervalMs: Long = 60_000,
val config: Map<String, String> = emptyMap()
) {
val regionEnum: Region
get() = runCatching { Region.valueOf(position.uppercase()) }
.getOrDefault(Region.TOP_LEFT)
fun string(key: String, default: String = ""): String
fun int(key: String, default: Int = 0): Int
fun bool(key: String, default: Boolean = false): Boolean
}
| Getter | Behavior |
|---|---|
string(key, default) | Raw map value or default. |
int(key, default) | toIntOrNull() or default. |
bool(key, default) | toBooleanStrictOrNull() or default. |
refreshIntervalMs | The entry's refreshInterval (JSON key), in milliseconds. |
MirrorConfig
Top-level config, equivalent to MagicMirror's config.js.
@Serializable
data class MirrorConfig(
val language: String = "en",
val timeFormat: Int = 24,
val units: String = "metric",
val modules: List<ModuleConfig> = emptyList()
)
ModuleFactory
org.speculum.core.ModuleFactory — the SPI for pluggable JAR modules. Implement it and declare the FQN in META-INF/services/org.speculum.core.ModuleFactory.
interface ModuleFactory {
val name: String // config `module` name
fun create(config: ModuleConfig): MirrorModule // build an instance
fun defaultConfig(): ModuleConfig? = null // auto-added placement
fun settingsSchema(): List<SettingSpec> = emptyList() // admin-console controls
val order: Int get() = 0 // stacking within a region
}
| Member | Notes |
|---|---|
name | The config module name this factory handles (e.g. "example"). |
create(config) | Build a module instance for the given config entry. |
defaultConfig() | Optional default placement, auto-added when no entry already names this module. Return null to require an explicit entry. |
settingsSchema() | Declares the config options this module understands, so the admin console renders labelled, typed controls instead of raw key/value rows. See SettingSpec. Empty by default, which keeps the raw rows. |
order | Stacking order within a region (lower = higher/earlier). Keeps layout deterministic when JARs load in unspecified order. |
class FooModuleFactory : ModuleFactory {
override val name = "foo"
override fun create(config: ModuleConfig) = FooModule(config)
override fun defaultConfig() = ModuleConfig("foo", "top_center", 3000, mapOf("k" to "v"))
override fun settingsSchema() = listOf(
SettingSpec("k", "Label", help = "What this option does."),
)
}
SettingSpec
org.speculum.config.SettingSpec — one declared entry of a module's config map. The admin console has no built-in knowledge of any module: it renders whatever settingsSchema() returns, served through GET /api/modules. Keys saved in a config but absent from the schema still render as raw key/value rows, so a schema may describe only part of a module's options.
@Serializable
data class SettingSpec(
val key: String, // the config map key
val label: String, // shown next to the control
val type: SettingType = SettingType.STRING,
val default: String = "", // what the module falls back to
val options: List<String> = emptyList(), // ENUM values
val min: Int? = null, // INT bounds
val max: Int? = null,
val step: Int? = null,
val help: String = "", // one sentence under the control
val placeholder: String = "",
val editor: String = "", // editor name for CUSTOM
val preview: List<String> = emptyList(), // "effective value" templates
val advanced: Boolean = false, // fold behind "show advanced"
)
enum class SettingType { STRING, INT, BOOL, ENUM, TEXT, URL, IP, CUSTOM }
| Type | Control |
|---|---|
string | Single-line text field. |
int | Number field, bounded by min/max and stepped by step. |
bool | Checkbox storing "true"/"false". |
enum | Dropdown over options. A saved value no longer offered stays selectable. |
text | Textarea for multi-line values. |
url | Full-width text field with URL validation. |
ip | Dropdown of the host addresses the server detects, plus auto-detect. |
custom | Purpose-built editor looked up by editor; falls back to a textarea when the console has no editor of that name. |
preview renders an "effective value" hint below the control. Each template interpolates {key} from the module's current config, and {key|fallback} substitutes fallback when that key is blank. The first template whose plain {key} references all resolve is shown, so a module can list a specific value first and a derived one after it:
SettingSpec(
key = "url", label = "URL override", type = SettingType.URL,
help = "Leave blank to build the address from the IP and port.",
preview = listOf("{url}", "http://{ip|<auto LAN IP>}:{port|8080}"),
)
Region
org.speculum.core.Region — screen regions; the layout engine places each module in the matching region of the fullscreen dashboard.
enum class Region {
TOP_LEFT, TOP_CENTER, TOP_RIGHT,
UPPER_THIRD, MIDDLE_CENTER, LOWER_THIRD,
BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT,
BOTTOM_BAR, FULLSCREEN_ABOVE, FULLSCREEN_BELOW
}
Notification & NotificationListener
org.speculum.core — inter-module messaging, equivalent to MagicMirror's sendNotification / notificationReceived bus.
data class Notification(
val name: String,
val payload: Any? = null,
val sender: String? = null
)
interface NotificationListener {
fun onNotification(notification: Notification)
}
The engine broadcasts events (e.g. ALL_MODULES_STARTED). Broadcasting from a module needs an engine reference — today the engine is the broadcaster.
MirrorColors
org.speculum.ui.MirrorColors — the three brightness levels on black.
object MirrorColors {
val Bright = Color(0xFFFFFFFF) // headlines, values
val Normal = Color(0xFF999999) // labels
val Dimmed = Color(0xFF666666) // secondary / headers
}
Canvas rather than emoji. Plain geometric text glyphs (• ↑ ≈) render monochrome and are fine.Config-server REST API
Auth is Authorization: Bearer <token> (HMAC-signed, 12 h TTL) obtained from /api/login.
| Method | Path | Auth | Body / Result |
|---|---|---|---|
| POST | /api/login | none | {password} → {token} (12 h) |
| POST | /api/password | bearer | {currentPassword, newPassword} → {message} |
| GET | /api/modules | bearer | discovered modules + default configs + settings schemas |
| GET | /api/config | bearer | effective MirrorConfig |
| PUT | /api/config | bearer | save MirrorConfig → writes config.json |
POST /api/password → 401 if currentPassword wrong, 400 if newPassword < 4 chars. Changing the password does not revoke existing tokens — they expire on their own TTL.
Use mirror-api as a dependency
For a module developed in its own repo, depend on the published API instead of project(":mirror-api"). It's released to GitHub Packages as org.speculum:mirror-api on every v* tag (version = tag without the v; latest published 1.4.0). The package is public, but GitHub's Maven registry still requires a token with read:packages.
repositories {
maven {
url = uri("https://maven.pkg.github.com/pierrejochem/SpeculumSmartMirror")
credentials {
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.token").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
}
dependencies { compileOnly("org.speculum:mirror-api:1.4.0") }
Put gpr.user / gpr.token in ~/.gradle/gradle.properties, or export GITHUB_ACTOR / GITHUB_TOKEN.