Skip to content

Commit

Permalink
Merge pull request #647 from baaahs/rework-controllers-manager
Browse files Browse the repository at this point in the history
Redesign controller management
  • Loading branch information
xian authored Dec 6, 2024
2 parents 2a77c24 + bf9ef89 commit 39512ca
Show file tree
Hide file tree
Showing 49 changed files with 1,392 additions and 643 deletions.
240 changes: 239 additions & 1 deletion data/BAAAHS.scene

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion data/config.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"runningShowPath":"BRC 2024.sparkle","runningScenePath":"BRC23.scene","version":0}
{"runningShowPath":"BRC 2024.sparkle","runningScenePath":"BAAAHS.scene","version":0}
3 changes: 3 additions & 0 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ tasks.withType(Test::class) {
useJUnitPlatform {
excludeTags("glsl")
}

// Copy in system properties.
systemProperties = System.getProperties().asIterable().associate { it.key.toString() to it.value }
}

tasks.named<Test>("jvmTest") {
Expand Down
64 changes: 47 additions & 17 deletions shared/src/commonMain/kotlin/baaahs/controller/Controller.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,66 @@ import kotlinx.serialization.Serializable
/** A Controller represents a physical device directly connected to one or more fixtures. */
interface Controller {
val controllerId: ControllerId
val state: ControllerState
val defaultFixtureOptions: FixtureOptions?
val transportType: TransportType
val defaultTransportConfig: TransportConfig?

fun createTransport(
entity: Model.Entity?,
fixtureConfig: FixtureConfig,
transportConfig: TransportConfig?
): Transport

/**
* Retrieves a list of fixture mappings that do not have associated names.
* This could be useful for fixtures that are automatically discovered or
* do not need explicit naming.
*
* @return A list of `FixtureMapping` instances without names.
*/
fun getAnonymousFixtureMappings(): List<FixtureMapping>


/** Called before each frame is rendered. */
fun beforeFrame() {}

/** Called after each frame has been rendered and [baaahs.gl.render.RenderTarget.sendFrame] has been called. */
fun afterFrame() {}

/**
* Creates a [FixtureResolver] that is responsible for constructing transport instances
* necessary to communicate with all the fixtures associated with this controller.
*
* A single fixture resolver will be used to resolve all fixtures associated with this
* controller. That might be useful if, e.g., fixtures are allocated to sequential DMX
* channels.
*
* @return A new instance of `FixtureResolver`.
*/
fun createFixtureResolver(): FixtureResolver

fun beforeFixtureResolution() {}
fun afterFixtureResolution() {}

/**
* Releases any resources associated with this controller
* and performs any necessary cleanup operations.
*
* Called by [ControllersManager] when [ControllerManager.onChange] for this controller returns null.
*/
fun release() {}
}

interface FixtureResolver {
fun createTransport(
entity: Model.Entity?,
fixtureConfig: FixtureConfig,
transportConfig: TransportConfig?
): Transport
}

open class NullController(
override val controllerId: ControllerId,
override val defaultFixtureOptions: FixtureOptions? = null,
override val defaultTransportConfig: TransportConfig? = null
) : Controller {
override val state: ControllerState =
State("Null Controller", "N/A", null)
override val transportType: TransportType
get() = DmxTransportType

@Serializable
class State(
class NullState(
override val title: String,
override val address: String?,
override val onlineSince: Instant?,
Expand All @@ -49,11 +77,13 @@ open class NullController(
override val lastErrorAt: Instant? = null
) : ControllerState()

override fun createTransport(
entity: Model.Entity?,
fixtureConfig: FixtureConfig,
transportConfig: TransportConfig?
): Transport = NullTransport
override fun createFixtureResolver(): FixtureResolver = object : FixtureResolver {
override fun createTransport(
entity: Model.Entity?,
fixtureConfig: FixtureConfig,
transportConfig: TransportConfig?
): Transport = NullTransport
}

override fun getAnonymousFixtureMappings(): List<FixtureMapping> =
emptyList()
Expand Down
103 changes: 88 additions & 15 deletions shared/src/commonMain/kotlin/baaahs/controller/ControllerManager.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,64 @@
package baaahs.controller

import baaahs.fixtures.FixtureMapping
import baaahs.fixtures.FixtureOptions
import baaahs.scene.ControllerConfig
import baaahs.scene.MutableControllerConfig
import baaahs.scene.OpenControllerConfig
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

/** A ControllerManager discovers and registers controllers with its [ControllerListener]. */
interface ControllerManager {
/**
* Instances of `ControllerManager` are responsible for creating controllers
* from configuration, and discovering controllers.
*
* When configuration for this type of controller is found, or changed, or removed,
* [onChange] is called, and should return a [Controller] if it makes sense to.
*
* When a new controller is discovered, the ControllerManager should call [onStateChange].
* Its [onChange] will then immediately be called.
*/
interface ControllerManager<T : Controller, C: ControllerConfig, S: ControllerState> {
val controllerType: String

fun addListener(controllerListener: ControllerListener)
fun removeListener(controllerListener: ControllerListener)
fun onConfigChange(controllerConfigs: Map<ControllerId, OpenControllerConfig<*>>)
fun addStateChangeListener(listener: ControllerStateChangeListener<S>)
fun removeStateChangeListener(listener: ControllerStateChangeListener<S>)
fun onStateChange(controllerId: ControllerId, changeState: (fromState: S?) -> S?)

/**
* Called by the [ControllersManager] when a scene is initially loaded, or when a controller configuration
* has been edited, as well as when the controller's state changes (as notified via
* [ControllerStateChangeListener]).
*
* Changes in the controller configuration and state are automatically synchronized with the client for
* display in the UI.
*
* @param controllerId The unique identifier of the controller.
* @param oldController Previously returned controller, if any.
* @param config Previous configuration of the controller, if any.
* @param state Previous state of the controller, if any.
* @param newConfig New configuration to be applied to the controller.
* @param newState New state to be applied to the controller.
* @return If `oldController` is returned, no action is taken.
*
* If `oldController` is not null and null is returned, any mapped fixtures are released and the old
* controller is disposed of.
*
* If `oldController` is null and null is returned, no action is taken.
*
* If `oldController` is null and a new controller is returned, the controller becomes active and
* any mapped fixtures are bound to it.
*
* If `oldController` is not null and a aifferent new controller is returned, the old controller is
* released, the new controller becomes active, and any mapped fixtures are moved to the new controller.
*/
fun onChange(
controllerId: ControllerId,
oldController: T?,
controllerConfig: Change<C?>,
controllerState: Change<S?>,
fixtureMappings: Change<List<FixtureMapping>>
): T?

fun start()
fun reset() {}
fun stop()
Expand All @@ -33,20 +81,45 @@ interface ControllerManager {
}
}

abstract class BaseControllerManager(
@OptIn(ExperimentalContracts::class)
class Change<T>(val oldValue: T, val newValue: T) {
val changed: Boolean get() = oldValue != newValue
val remainedNull: Boolean get() = oldValue == null && newValue == null
val becameNull: Boolean get() = oldValue != null && newValue == null
val becameNotNull: Boolean get() = oldValue == null && newValue != null
val remainedNotNull: Boolean get() = oldValue != null && newValue != null
}

fun interface ControllerStateChangeListener<S : ControllerState> {
/**
* Invoked by [ControllerManager] when the state of a controller changes.
*
* [ControllerManager.onChange] will immediately be invoked with the affected controller,
* along with its old state, so any state change handling can be performed.
*
* State change is automatically propagated to the client for UI display.
*
* @param controllerId The unique identifier of the controller whose state has changed.
* @param state The new state of the controller.
*/
fun onStateChange(controllerId: ControllerId, changeState: (fromState: S?) -> S?)
}


abstract class BaseControllerManager<T : Controller, C: ControllerConfig, S: ControllerState>(
override val controllerType: String
) : ControllerManager {
private val listeners: MutableList<ControllerListener> = mutableListOf()
) : ControllerManager<T, C, S> {
private val listeners: MutableList<ControllerStateChangeListener<S>> = mutableListOf()

override fun addListener(controllerListener: ControllerListener) {
listeners.add(controllerListener)
override fun addStateChangeListener(listener: ControllerStateChangeListener<S>) {
listeners.add(listener)
}

override fun removeListener(controllerListener: ControllerListener) {
listeners.remove(controllerListener)
override fun removeStateChangeListener(listener: ControllerStateChangeListener<S>) {
listeners.remove(listener)
}

fun notifyListeners(block: ControllerListener.() -> Unit) {
listeners.forEach(block)
override fun onStateChange(controllerId: ControllerId, changeState: (fromState: S?) -> S?) {
listeners.forEach { listener -> listener.onStateChange(controllerId, changeState) }
}
}
18 changes: 18 additions & 0 deletions shared/src/commonMain/kotlin/baaahs/controller/ControllerState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package baaahs.controller

import baaahs.ui.Observable
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable

@Serializable
abstract class ControllerState : Observable() {
abstract val title: String
abstract val address: String?
abstract val onlineSince: Instant?
abstract val firmwareVersion: String?
abstract val lastErrorMessage: String?
abstract val lastErrorAt: Instant?

open fun matches(controllerMatcher: ControllerMatcher): Boolean =
controllerMatcher.matches(title, address)
}
Loading

0 comments on commit 39512ca

Please sign in to comment.