commit #2
This commit is contained in:
parent
c4b134bb80
commit
ea15c19e70
26 changed files with 653 additions and 215 deletions
|
@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.serialization")
|
||||
application
|
||||
}
|
||||
|
||||
|
@ -23,6 +24,11 @@ dependencies {
|
|||
implementation(KotlinX.collections.immutable)
|
||||
implementation(KotlinX.serialization.json)
|
||||
implementation(KotlinX.datetime)
|
||||
implementation(Ktor.server.core)
|
||||
implementation(Ktor.server.websockets)
|
||||
implementation(Ktor.server.cio)
|
||||
implementation(Ktor.server.contentNegotiation)
|
||||
implementation(Ktor.plugins.serialization.kotlinx.json)
|
||||
implementation("org.slf4j:slf4j-simple:_")
|
||||
implementation("com.fazecast:jSerialComm:_")
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:_")
|
||||
|
|
|
@ -1,50 +1,23 @@
|
|||
package de.moritzruth.lampenfieber
|
||||
|
||||
import de.moritzruth.theaterdsl.StepTrigger
|
||||
import de.moritzruth.theaterdsl.createAct
|
||||
import de.moritzruth.lampenfieber.device.StairvilleTriLedBar
|
||||
import de.moritzruth.theaterdsl.dmx.DmxAddress
|
||||
import de.moritzruth.theaterdsl.dmx.EnttecOpenDmxUsb
|
||||
import de.moritzruth.theaterdsl.runShow
|
||||
import de.moritzruth.theaterdsl.value.Color
|
||||
import de.moritzruth.theaterdsl.value.degrees
|
||||
import de.moritzruth.theaterdsl.value.percent
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import de.moritzruth.theaterdsl.show.createShow
|
||||
import de.moritzruth.theaterdsl.show.runShow
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
val bar = FloorBarDevice(DmxAddress(15u))
|
||||
val bar = StairvilleTriLedBar(DmxAddress(15u))
|
||||
|
||||
val firstAct = createAct("Erster Akt") {
|
||||
val show = createShow {
|
||||
act("Erster Akt") {
|
||||
scene("Prolog") {
|
||||
step {
|
||||
trigger = StepTrigger.MusicStart("1. Ouvertüre: Dissonanzen in Schwarz und Weiß", 2.minutes)
|
||||
|
||||
actors {
|
||||
+ "Sophie / von links"
|
||||
}
|
||||
|
||||
bar.color.static(Color(100.degrees, 100.percent, 100.percent))
|
||||
bar.brightness.switch(100.percent, 0.percent, 10.seconds)
|
||||
// bar.color.fade(Color(100.degrees, 100.percent, 100.percent), 2.seconds)
|
||||
}
|
||||
|
||||
step {
|
||||
trigger = StepTrigger.MusicEnd
|
||||
|
||||
actors {
|
||||
- "Sophie"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun main() {
|
||||
EnttecOpenDmxUsb.start()
|
||||
runShow(
|
||||
devices = setOf(
|
||||
bar
|
||||
),
|
||||
acts = listOf(
|
||||
firstAct
|
||||
)
|
||||
)
|
||||
runShow(show, persistentSetOf(bar))
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package de.moritzruth.lampenfieber.device
|
||||
|
||||
import de.moritzruth.theaterdsl.device.Device
|
||||
import de.moritzruth.theaterdsl.device.PercentageDV
|
||||
import de.moritzruth.theaterdsl.dmx.DmxAddress
|
||||
import de.moritzruth.theaterdsl.dmx.DmxDataWriter
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
class FuturelightDmh160(override val firstChannel: DmxAddress) : Device {
|
||||
override val numberOfChannels: UInt = 16u
|
||||
|
||||
override fun writeDmxData(writer: DmxDataWriter) {
|
||||
writer.writePercentage(brightness.getCurrentValue())
|
||||
}
|
||||
|
||||
val pan = AngleDV()
|
||||
val brightness = PercentageDV()
|
||||
|
||||
override val dvs = persistentSetOf(brightness)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package de.moritzruth.lampenfieber.device
|
||||
|
||||
import de.moritzruth.theaterdsl.device.Device
|
||||
import de.moritzruth.theaterdsl.device.PercentageDV
|
||||
import de.moritzruth.theaterdsl.dmx.DmxAddress
|
||||
import de.moritzruth.theaterdsl.dmx.DmxDataWriter
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
class SimpleDimmer(channel: DmxAddress) : Device {
|
||||
override val firstChannel = channel
|
||||
override val numberOfChannels: UInt = 1u
|
||||
|
||||
override fun writeDmxData(writer: DmxDataWriter) {
|
||||
writer.writePercentage(brightness.getCurrentValue())
|
||||
}
|
||||
|
||||
val brightness = PercentageDV()
|
||||
|
||||
override val dvs = persistentSetOf(brightness)
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
package de.moritzruth.lampenfieber
|
||||
package de.moritzruth.lampenfieber.device
|
||||
|
||||
import de.moritzruth.theaterdsl.*
|
||||
import de.moritzruth.theaterdsl.device.ColorDV
|
||||
import de.moritzruth.theaterdsl.device.Device
|
||||
import de.moritzruth.theaterdsl.device.PercentageDV
|
||||
import de.moritzruth.theaterdsl.dmx.DmxAddress
|
||||
import de.moritzruth.theaterdsl.dmx.DmxDataWriter
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
class FloorBarDevice(override val firstChannel: DmxAddress): Device {
|
||||
class StairvilleTriLedBar(override val firstChannel: DmxAddress) : Device {
|
||||
override val numberOfChannels: UInt = 5u
|
||||
|
||||
override fun writeDmxData(writer: DmxDataWriter) {
|
||||
|
@ -19,4 +22,10 @@ class FloorBarDevice(override val firstChannel: DmxAddress): Device {
|
|||
val color = ColorDV()
|
||||
val brightness = PercentageDV()
|
||||
val strobeSpeed = PercentageDV()
|
||||
|
||||
override val dvs = persistentSetOf(
|
||||
color,
|
||||
brightness,
|
||||
strobeSpeed
|
||||
)
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package de.moritzruth.theaterdsl
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
class Act(val name: String, val scenes: ImmutableList<Scene>)
|
|
@ -1,59 +0,0 @@
|
|||
package de.moritzruth.theaterdsl
|
||||
|
||||
import de.moritzruth.theaterdsl.dmx.EnttecOpenDmxUsb
|
||||
import de.moritzruth.theaterdsl.dmx.PerDeviceDmxDataWriter
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.math.roundToLong
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlin.time.Duration
|
||||
|
||||
suspend fun runShow(devices: Set<Device>, acts: List<Act>) = coroutineScope {
|
||||
launch {
|
||||
while(isActive) {
|
||||
val took = measureTimeMillis {
|
||||
val data = UByteArray(512)
|
||||
val writer = PerDeviceDmxDataWriter(data)
|
||||
|
||||
for (device in devices) {
|
||||
writer.reset(0, device.firstChannel, device.numberOfChannels)
|
||||
device.writeDmxData(writer)
|
||||
}
|
||||
|
||||
EnttecOpenDmxUsb.data = data
|
||||
}
|
||||
|
||||
delay(((1000.0 / 30) - took).roundToLong())
|
||||
}
|
||||
}
|
||||
|
||||
var lastStepJob: Job? = null
|
||||
|
||||
for (act in acts) {
|
||||
println("ACT: ${act.name}\n")
|
||||
|
||||
for (scene in act.scenes) {
|
||||
println("SCENE: ${scene.name}")
|
||||
|
||||
for (step in scene.steps) {
|
||||
println("NEXT STEP: ${step.trigger.format()}")
|
||||
val input = readln()
|
||||
if (input === "q") exitProcess(0)
|
||||
|
||||
lastStepJob?.cancelAndJoin()
|
||||
lastStepJob = launch(SupervisorJob()) {
|
||||
val context = object : StepRunContext, CoroutineScope by this {
|
||||
|
||||
}
|
||||
|
||||
with(step) {
|
||||
runner?.let { context.it() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println("\nEND OF THE SHOW")
|
||||
delay(Duration.INFINITE)
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package de.moritzruth.theaterdsl
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
class Scene(val name: String, val steps: ImmutableList<Step>)
|
|
@ -1,42 +0,0 @@
|
|||
package de.moritzruth.theaterdsl
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
enum class PropPosition {
|
||||
PROSCENIUM_LEFT,
|
||||
PROSCENIUM_CENTER,
|
||||
PROSCENIUM_RIGHT,
|
||||
LEFT,
|
||||
CENTER,
|
||||
RIGHT,
|
||||
BACKDROP
|
||||
}
|
||||
|
||||
@TheaterDslMarker
|
||||
interface StepRunContext: CoroutineScope
|
||||
|
||||
typealias StepRunner = (StepRunContext.() -> Unit)
|
||||
|
||||
/**
|
||||
* A string which has optional additional details separated by " / "
|
||||
*/
|
||||
@JvmInline
|
||||
value class StringWithDetails(val value: String) {
|
||||
companion object {
|
||||
const val DELIMITER = " / "
|
||||
}
|
||||
|
||||
val main get() = value.split(DELIMITER)[0]
|
||||
val details: String? get() = value.split(DELIMITER, limit = 2).takeIf { it.size == 2 }?.let { it[1] }
|
||||
val hasDetails get() = details !== null
|
||||
}
|
||||
|
||||
class Step(
|
||||
val trigger: StepTrigger,
|
||||
val actorEntrances: ImmutableSet<StringWithDetails>,
|
||||
val actorExits: ImmutableSet<StringWithDetails>,
|
||||
val actorsOnStage: ImmutableList<String>,
|
||||
val runner: StepRunner?
|
||||
)
|
|
@ -1,37 +0,0 @@
|
|||
package de.moritzruth.theaterdsl
|
||||
|
||||
import kotlin.time.Duration
|
||||
|
||||
sealed interface StepTrigger {
|
||||
fun format(): String
|
||||
|
||||
data class MusicStart(val title: String, val duration: Duration): StepTrigger {
|
||||
override fun format() = "music start: $title"
|
||||
}
|
||||
|
||||
object MusicEnd: StepTrigger {
|
||||
override fun format() = "music end"
|
||||
}
|
||||
|
||||
data class Curtain(val state: State, val whileMoving: Boolean): StepTrigger {
|
||||
enum class State {
|
||||
OPEN,
|
||||
CLOSED
|
||||
}
|
||||
|
||||
override fun format() = "curtain: $state${if (whileMoving) "(while moving)" else ""}"
|
||||
}
|
||||
|
||||
data class Light(val state: State, val whileFading: Boolean): StepTrigger {
|
||||
enum class State {
|
||||
ON,
|
||||
OFF
|
||||
}
|
||||
|
||||
override fun format() = "light: $state${if (whileFading) "(while fading)" else ""}"
|
||||
}
|
||||
|
||||
data class Custom(val text: String): StepTrigger {
|
||||
override fun format() = "custom: $text"
|
||||
}
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
package de.moritzruth.theaterdsl
|
||||
package de.moritzruth.theaterdsl.device
|
||||
|
||||
import de.moritzruth.theaterdsl.dmx.DmxAddress
|
||||
import de.moritzruth.theaterdsl.dmx.DmxDataWriter
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
interface Device {
|
||||
val dvs: ImmutableSet<DynamicValue<*>>
|
||||
|
||||
val firstChannel: DmxAddress
|
||||
val numberOfChannels: UInt
|
||||
fun writeDmxData(writer: DmxDataWriter)
|
|
@ -1,4 +1,4 @@
|
|||
package de.moritzruth.theaterdsl
|
||||
package de.moritzruth.theaterdsl.device
|
||||
|
||||
import de.moritzruth.theaterdsl.value.*
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
@ -13,15 +13,16 @@ import kotlin.time.ExperimentalTime
|
|||
import kotlin.time.TimeSource
|
||||
|
||||
interface DynamicValue<T> {
|
||||
fun reset()
|
||||
fun getCurrentValue(): T
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
class PercentageDV(initialStaticValue: Percentage = 0.percent): DynamicValue<Percentage> {
|
||||
class PercentageDV(private val initialStaticValue: Percentage = 0.percent) : DynamicValue<Percentage> {
|
||||
private sealed interface State {
|
||||
data class Static(val value: Percentage): State
|
||||
data class Static(val value: Percentage) : State
|
||||
|
||||
data class Fade(val start: Percentage, val end: Percentage, val duration: Duration): State {
|
||||
data class Fade(val start: Percentage, val end: Percentage, val duration: Duration) : State {
|
||||
val delta = end.value - start.value
|
||||
}
|
||||
|
||||
|
@ -38,7 +39,7 @@ class PercentageDV(initialStaticValue: Percentage = 0.percent): DynamicValue<Per
|
|||
}
|
||||
}
|
||||
|
||||
data class Step(val steps: ImmutableList<Percentage>, val interval: Duration, val startIndex: Int): State
|
||||
data class Step(val steps: ImmutableList<Percentage>, val interval: Duration, val startIndex: Int) : State
|
||||
}
|
||||
|
||||
private var stateChangeMark = TimeSource.Monotonic.markNow()
|
||||
|
@ -48,6 +49,10 @@ class PercentageDV(initialStaticValue: Percentage = 0.percent): DynamicValue<Per
|
|||
stateChangeMark = TimeSource.Monotonic.markNow()
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
state = State.Static(initialStaticValue)
|
||||
}
|
||||
|
||||
override fun getCurrentValue(): Percentage {
|
||||
val elapsedTime = stateChangeMark.elapsedNow()
|
||||
|
||||
|
@ -97,10 +102,10 @@ class PercentageDV(initialStaticValue: Percentage = 0.percent): DynamicValue<Per
|
|||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
class ColorDV(initialStaticValue: Color = Color.WHITE): DynamicValue<Color> {
|
||||
class ColorDV(private val initialStaticValue: Color = Color.WHITE) : DynamicValue<Color> {
|
||||
private sealed interface State {
|
||||
data class Static(val value: Color): State
|
||||
data class Fade(val start: Color, val end: Color, val duration: Duration): State {
|
||||
data class Static(val value: Color) : State
|
||||
data class Fade(val start: Color, val end: Color, val duration: Duration) : State {
|
||||
val deltaHue = ((end.hue.degree - start.hue.degree).mod(360f) + 540f).mod(360f) - 180f
|
||||
val deltaSaturation = end.saturation.value - start.saturation.value
|
||||
val deltaBrightness = end.brightness.value - start.brightness.value
|
||||
|
@ -114,10 +119,14 @@ class ColorDV(initialStaticValue: Color = Color.WHITE): DynamicValue<Color> {
|
|||
stateChangeMark = TimeSource.Monotonic.markNow()
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
state = State.Static(initialStaticValue)
|
||||
}
|
||||
|
||||
override fun getCurrentValue(): Color {
|
||||
val elapsedTime = stateChangeMark.elapsedNow()
|
||||
|
||||
return when(val s = state) {
|
||||
return when (val s = state) {
|
||||
is State.Static -> s.value
|
||||
is State.Fade -> {
|
||||
val progress = min(elapsedTime / s.duration, 1.0)
|
|
@ -2,11 +2,14 @@ package de.moritzruth.theaterdsl.dmx
|
|||
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
import com.fazecast.jSerialComm.SerialPortIOException
|
||||
import io.github.oshai.KotlinLogging
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
object EnttecOpenDmxUsb {
|
||||
private const val PORT_NAME = "FT232R USB UART"
|
||||
|
||||
private val logger = KotlinLogging.logger("EnttecOpenDmxUsb")
|
||||
|
||||
var data = UByteArray(512)
|
||||
set(value) {
|
||||
require(value.size <= 512) { "array length <= 512" }
|
||||
|
@ -78,11 +81,11 @@ object EnttecOpenDmxUsb {
|
|||
port.numStopBits = SerialPort.TWO_STOP_BITS
|
||||
port.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED)
|
||||
|
||||
println("DMX interface connected")
|
||||
logger.info("DMX interface connected")
|
||||
return true
|
||||
} else {
|
||||
if (port.lastErrorCode == 13) System.err.println("The serial port could not be opened because of insufficient permissions (13).")
|
||||
else System.err.println("The serial port could not be opened (${port.lastErrorCode}).")
|
||||
if (port.lastErrorCode == 13) logger.error("The serial port could not be opened because of insufficient permissions (13).")
|
||||
else logger.error("The serial port could not be opened (${port.lastErrorCode}).")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
10
src/main/kotlin/de/moritzruth/theaterdsl/show/Act.kt
Normal file
10
src/main/kotlin/de/moritzruth/theaterdsl/show/Act.kt
Normal file
|
@ -0,0 +1,10 @@
|
|||
package de.moritzruth.theaterdsl.show
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Act(val name: String, val scenes: List<Scene>)
|
||||
|
||||
operator fun List<Act>.get(position: ShowPosition) =
|
||||
if (position == ShowPosition.START) Step.START
|
||||
else this[position.act].scenes[position.scene].steps[position.step]
|
|
@ -1,12 +1,20 @@
|
|||
package de.moritzruth.theaterdsl
|
||||
package de.moritzruth.theaterdsl.show
|
||||
|
||||
import de.moritzruth.theaterdsl.runOptionalSanityChecks
|
||||
import de.moritzruth.theaterdsl.util.StringWithDetails
|
||||
import io.github.oshai.KotlinLogging
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@DslMarker
|
||||
annotation class TheaterDslMarker
|
||||
|
||||
@TheaterDslMarker
|
||||
interface ShowBuilderContext {
|
||||
fun act(name: String, build: ActBuilderContext.() -> Unit)
|
||||
}
|
||||
|
||||
@TheaterDslMarker
|
||||
interface ActBuilderContext {
|
||||
fun scene(name: String, build: SceneBuilderContext.() -> Unit)
|
||||
|
@ -19,16 +27,19 @@ interface SceneBuilderContext {
|
|||
|
||||
@TheaterDslMarker
|
||||
interface StepDataBuilderContext {
|
||||
var trigger: StepTrigger
|
||||
var trigger: StepCue
|
||||
val props: PropsBuilderMap
|
||||
|
||||
fun actors(build: ActorsBuildContext.() -> Unit)
|
||||
fun onRun(block: StepRunner)
|
||||
}
|
||||
|
||||
class PropsBuilderMap(private val backingMap: MutableMap<PropPosition, StringWithDetails>) {
|
||||
operator fun set(position: PropPosition, prop: StringWithDetails) = backingMap.set(position, prop)
|
||||
operator fun set(position: PropPosition, prop: String) = backingMap.set(position, StringWithDetails(prop))
|
||||
class PropsBuilderMap(private val backingMap: MutableMap<PropPosition, StringWithDetails?>) {
|
||||
operator fun set(position: PropPosition, prop: String?) = backingMap.set(position, prop?.let { StringWithDetails(it) })
|
||||
|
||||
operator fun invoke(block: (PropsBuilderMap) -> Unit) {
|
||||
block(this)
|
||||
}
|
||||
}
|
||||
|
||||
@TheaterDslMarker
|
||||
|
@ -54,9 +65,23 @@ class ActorsBuildContext(
|
|||
}
|
||||
|
||||
@TheaterDslMarker
|
||||
fun createAct(name: String, build: ActBuilderContext.() -> Unit): Act {
|
||||
fun createShow(build: ShowBuilderContext.() -> Unit): Show {
|
||||
val acts = mutableListOf<Act>()
|
||||
|
||||
object : ShowBuilderContext {
|
||||
override fun act(name: String, build: ActBuilderContext.() -> Unit) {
|
||||
acts.add(buildAct(acts.size, name, build))
|
||||
}
|
||||
}.build()
|
||||
|
||||
return Show(acts.toList())
|
||||
}
|
||||
|
||||
@TheaterDslMarker
|
||||
private fun buildAct(actIndex: Int, name: String, build: ActBuilderContext.() -> Unit): Act {
|
||||
val scenes = mutableListOf<Scene>()
|
||||
val actorsOnStage = mutableListOf<String>()
|
||||
val props = PropPosition.values().associateWith<PropPosition, StringWithDetails?> { null }.toMutableMap()
|
||||
|
||||
object : ActBuilderContext {
|
||||
override fun scene(name: String, build: SceneBuilderContext.() -> Unit) {
|
||||
|
@ -64,20 +89,20 @@ fun createAct(name: String, build: ActBuilderContext.() -> Unit): Act {
|
|||
|
||||
object : SceneBuilderContext {
|
||||
override fun step(build: StepDataBuilderContext.() -> Unit) {
|
||||
var nullableTrigger: StepTrigger? = null
|
||||
val propsMap = mutableMapOf<PropPosition, StringWithDetails>()
|
||||
var nullableTrigger: StepCue? = null
|
||||
val changedProps = mutableMapOf<PropPosition, StringWithDetails?>()
|
||||
val actorEntrances = mutableSetOf<StringWithDetails>()
|
||||
val actorExits = mutableSetOf<StringWithDetails>()
|
||||
var runner: StepRunner? = null
|
||||
|
||||
object : StepDataBuilderContext {
|
||||
override var trigger: StepTrigger
|
||||
override var trigger: StepCue
|
||||
get() = nullableTrigger ?: throw IllegalStateException("trigger was not set yet")
|
||||
set(value) {
|
||||
nullableTrigger = value
|
||||
}
|
||||
|
||||
override val props = PropsBuilderMap(propsMap)
|
||||
override val props = PropsBuilderMap(changedProps)
|
||||
|
||||
override fun actors(build: ActorsBuildContext.() -> Unit) {
|
||||
ActorsBuildContext(actorEntrances, actorExits).build()
|
||||
|
@ -122,12 +147,17 @@ fun createAct(name: String, build: ActBuilderContext.() -> Unit): Act {
|
|||
actorsOnStage.removeAll(actorExitsNames)
|
||||
actorsOnStage.addAll(actorEntrancesNames)
|
||||
|
||||
changedProps.forEach { (k, v) -> props[k] = v }
|
||||
|
||||
steps.add(
|
||||
Step(
|
||||
ShowPosition(actIndex, scenes.size, steps.size),
|
||||
trigger,
|
||||
actorEntrances.toImmutableSet(),
|
||||
actorExits.toImmutableSet(),
|
||||
actorsOnStage.toImmutableList(),
|
||||
props.toImmutableMap(),
|
||||
changedProps.isNotEmpty(),
|
||||
runner
|
||||
)
|
||||
)
|
230
src/main/kotlin/de/moritzruth/theaterdsl/show/Runner.kt
Normal file
230
src/main/kotlin/de/moritzruth/theaterdsl/show/Runner.kt
Normal file
|
@ -0,0 +1,230 @@
|
|||
package de.moritzruth.theaterdsl.show
|
||||
|
||||
import de.moritzruth.theaterdsl.device.Device
|
||||
import de.moritzruth.theaterdsl.device.DynamicValue
|
||||
import de.moritzruth.theaterdsl.dmx.EnttecOpenDmxUsb
|
||||
import de.moritzruth.theaterdsl.dmx.PerDeviceDmxDataWriter
|
||||
import de.moritzruth.theaterdsl.util.InstantAsEpochMillisecondsSerializer
|
||||
import de.moritzruth.theaterdsl.util.mapState
|
||||
import io.github.oshai.KLogger
|
||||
import io.github.oshai.KotlinLogging
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.cio.*
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.math.roundToLong
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
class ShowContext(
|
||||
val devices: ImmutableSet<Device>,
|
||||
val show: Show,
|
||||
val logger: KLogger,
|
||||
val stateFlow: MutableStateFlow<ShowState>,
|
||||
val outputDataFreeze: AtomicInteger
|
||||
) {
|
||||
val stepFlow = stateFlow.mapState {
|
||||
if (it.position == ShowPosition.START) Step.START
|
||||
else show.acts[it.position.act].scenes[it.position.scene].steps[it.position.step]
|
||||
}
|
||||
|
||||
fun goTo(position: ShowPosition) {
|
||||
val step = show.acts[position]
|
||||
|
||||
stateFlow.update {
|
||||
when {
|
||||
step.cue is StepCue.MusicStart -> {
|
||||
it.copy(
|
||||
position = position,
|
||||
activeMusic = ActiveShowMusic(
|
||||
title = step.cue.title,
|
||||
duration = step.cue.duration
|
||||
),
|
||||
musicStartTime = Clock.System.now()
|
||||
)
|
||||
}
|
||||
|
||||
step.cue is StepCue.MusicEnd ||
|
||||
step.position == ShowPosition.START -> it.copy(position = position, activeMusic = null)
|
||||
|
||||
else -> it.copy(position = position)
|
||||
}
|
||||
}
|
||||
|
||||
if (step.cue is StepCue.MusicStart) {
|
||||
logger.info("Music started: ${step.cue.title} (${step.cue.duration.inWholeSeconds}s)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun runShow(show: Show, devices: ImmutableSet<Device>) = coroutineScope {
|
||||
val logger = KotlinLogging.logger("runShow")
|
||||
|
||||
val stateFlow = MutableStateFlow(
|
||||
ShowState(
|
||||
ShowPosition.START,
|
||||
"",
|
||||
null,
|
||||
Instant.DISTANT_PAST
|
||||
)
|
||||
)
|
||||
|
||||
val context = ShowContext(
|
||||
devices,
|
||||
show,
|
||||
logger,
|
||||
stateFlow,
|
||||
AtomicInteger(0)
|
||||
)
|
||||
|
||||
startDataWriting(context)
|
||||
startStepRunning(context)
|
||||
startWebsocketServer(context)
|
||||
|
||||
launch {
|
||||
stateFlow.collect {
|
||||
logger.info("ShowState changed: $it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun CoroutineScope.startDataWriting(context: ShowContext) = launch {
|
||||
while (isActive) {
|
||||
val took = measureTimeMillis {
|
||||
val data = UByteArray(512)
|
||||
val writer = PerDeviceDmxDataWriter(data)
|
||||
|
||||
for (device in context.devices) {
|
||||
writer.reset(0, device.firstChannel, device.numberOfChannels)
|
||||
device.writeDmxData(writer)
|
||||
}
|
||||
|
||||
if (context.outputDataFreeze.get() == 0) EnttecOpenDmxUsb.data = data
|
||||
}
|
||||
|
||||
delay(((1000.0 / 30) - took).roundToLong())
|
||||
}
|
||||
}
|
||||
|
||||
fun CoroutineScope.startStepRunning(context: ShowContext) = launch {
|
||||
var lastPosition = ShowPosition.START
|
||||
var lastStepJob: Job? = null
|
||||
|
||||
context.stateFlow.map { it.position }.distinctUntilChanged().collect { position ->
|
||||
context.outputDataFreeze.incrementAndGet()
|
||||
|
||||
if (position < lastPosition) {
|
||||
context.devices.forEach { it.dvs.forEach(DynamicValue<*>::reset) }
|
||||
lastPosition = ShowPosition.START
|
||||
}
|
||||
|
||||
while (lastPosition != position) {
|
||||
lastPosition = context.show.getNextValidPosition(lastPosition) ?: break
|
||||
|
||||
val step = context.show.acts[lastPosition]
|
||||
lastStepJob?.cancelAndJoin()
|
||||
lastStepJob = launch(SupervisorJob(currentCoroutineContext().job)) {
|
||||
val runContext = object : StepRunContext, CoroutineScope by this {}
|
||||
|
||||
step.runner?.let { runContext.it() }
|
||||
}
|
||||
}
|
||||
|
||||
context.outputDataFreeze.decrementAndGet()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class StateUpdateMessage(
|
||||
@Serializable(with = InstantAsEpochMillisecondsSerializer::class)
|
||||
val timestamp: Instant,
|
||||
val state: ShowState
|
||||
)
|
||||
|
||||
private fun CoroutineScope.startWebsocketServer(context: ShowContext) = launch(Dispatchers.IO) {
|
||||
val updateMessageJsonFlow = context.stateFlow.map { state ->
|
||||
Json.encodeToString(
|
||||
StateUpdateMessage(
|
||||
Clock.System.now(),
|
||||
state
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val showJson = Json.encodeToString(context.show)
|
||||
|
||||
embeddedServer(CIO, port = 8000) {
|
||||
install(WebSockets) {
|
||||
pingPeriod = 10.seconds.toJavaDuration()
|
||||
timeout = 15.seconds.toJavaDuration()
|
||||
maxFrameSize = Long.MAX_VALUE
|
||||
}
|
||||
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
|
||||
routing {
|
||||
route("/api") {
|
||||
post("/go") {
|
||||
val position = call.receive<ShowPosition>()
|
||||
context.goTo(position)
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
|
||||
post("/message") {
|
||||
val message = call.receiveText()
|
||||
context.stateFlow.update { it.copy(message = message) }
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
|
||||
get("/show") {
|
||||
call.respond(context.show.acts.toMutableList())
|
||||
}
|
||||
|
||||
get("/state") {
|
||||
call.respond(context.stateFlow.value)
|
||||
}
|
||||
|
||||
val clientsCount = AtomicInteger(0)
|
||||
|
||||
webSocket("/ws") {
|
||||
var count = clientsCount.incrementAndGet()
|
||||
context.logger.info("[${call.request.local.remoteHost}] connected ($count)")
|
||||
|
||||
val sendJob = launch {
|
||||
send(Frame.Text(showJson))
|
||||
updateMessageJsonFlow.collect { send(Frame.Text(it)) }
|
||||
}
|
||||
|
||||
try {
|
||||
for (frame in incoming) {
|
||||
context.logger.debug { "[${call.request.local.remoteHost}] frame: $frame" }
|
||||
}
|
||||
} finally {
|
||||
sendJob.cancel()
|
||||
count = clientsCount.decrementAndGet()
|
||||
context.logger.info("[${call.request.local.remoteHost}] disconnected ($count)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start(wait = true)
|
||||
}
|
6
src/main/kotlin/de/moritzruth/theaterdsl/show/Scene.kt
Normal file
6
src/main/kotlin/de/moritzruth/theaterdsl/show/Scene.kt
Normal file
|
@ -0,0 +1,6 @@
|
|||
package de.moritzruth.theaterdsl.show
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class Scene(val name: String, val steps: List<Step>)
|
65
src/main/kotlin/de/moritzruth/theaterdsl/show/Show.kt
Normal file
65
src/main/kotlin/de/moritzruth/theaterdsl/show/Show.kt
Normal file
|
@ -0,0 +1,65 @@
|
|||
package de.moritzruth.theaterdsl.show
|
||||
|
||||
import de.moritzruth.theaterdsl.util.DurationAsMillisecondsSerializer
|
||||
import de.moritzruth.theaterdsl.util.InstantAsEpochMillisecondsSerializer
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Serializable
|
||||
data class Show(val acts: List<Act>) {
|
||||
fun getNextValidPosition(position: ShowPosition): ShowPosition? {
|
||||
if (position == ShowPosition.START) return ShowPosition.FIRST
|
||||
|
||||
var nextStep = position.step + 1
|
||||
var nextScene = position.scene
|
||||
var nextAct = position.act
|
||||
|
||||
if (nextStep >= acts[position.act].scenes[position.scene].steps.size) {
|
||||
nextStep = 0
|
||||
nextScene++
|
||||
|
||||
if (nextScene >= acts[position.act].scenes.size) {
|
||||
nextScene = 0
|
||||
nextAct++
|
||||
|
||||
if (nextAct >= acts.size) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ShowPosition(nextAct, nextScene, nextStep)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ShowPosition(val act: Int, val scene: Int, val step: Int) : Comparable<ShowPosition> {
|
||||
companion object {
|
||||
val START = ShowPosition(-1, 0, 0)
|
||||
val FIRST = ShowPosition(0, 0, 0)
|
||||
}
|
||||
|
||||
override fun compareTo(other: ShowPosition): Int {
|
||||
return if (act == other.act) {
|
||||
if (scene == other.scene) step.compareTo(other.step)
|
||||
else scene.compareTo(other.scene)
|
||||
} else act.compareTo(other.act)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ShowState(
|
||||
val position: ShowPosition,
|
||||
val message: String,
|
||||
val activeMusic: ActiveShowMusic?,
|
||||
@Serializable(with = InstantAsEpochMillisecondsSerializer::class)
|
||||
val musicStartTime: Instant
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ActiveShowMusic(
|
||||
val title: String,
|
||||
@Serializable(with = DurationAsMillisecondsSerializer::class)
|
||||
val duration: Duration
|
||||
)
|
48
src/main/kotlin/de/moritzruth/theaterdsl/show/Step.kt
Normal file
48
src/main/kotlin/de/moritzruth/theaterdsl/show/Step.kt
Normal file
|
@ -0,0 +1,48 @@
|
|||
package de.moritzruth.theaterdsl.show
|
||||
|
||||
import de.moritzruth.theaterdsl.util.StringWithDetails
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
|
||||
@Serializable
|
||||
enum class PropPosition {
|
||||
PROSCENIUM_LEFT,
|
||||
PROSCENIUM_CENTER,
|
||||
PROSCENIUM_RIGHT,
|
||||
LEFT,
|
||||
CENTER,
|
||||
RIGHT,
|
||||
BACKDROP
|
||||
}
|
||||
|
||||
@TheaterDslMarker
|
||||
interface StepRunContext: CoroutineScope
|
||||
|
||||
typealias StepRunner = (StepRunContext.() -> Unit)
|
||||
|
||||
@Serializable
|
||||
class Step(
|
||||
val position: ShowPosition,
|
||||
val cue: StepCue,
|
||||
val actorEntrances: Set<StringWithDetails>,
|
||||
val actorExits: Set<StringWithDetails>,
|
||||
val actorsOnStage: List<String>,
|
||||
val props: Map<PropPosition, StringWithDetails?>,
|
||||
val hasChangedProps: Boolean,
|
||||
@Transient
|
||||
val runner: StepRunner? = null
|
||||
) {
|
||||
companion object {
|
||||
val START = Step(
|
||||
ShowPosition.START,
|
||||
StepCue.Custom("Start"),
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
emptyList(),
|
||||
emptyMap(),
|
||||
false,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
63
src/main/kotlin/de/moritzruth/theaterdsl/show/StepCue.kt
Normal file
63
src/main/kotlin/de/moritzruth/theaterdsl/show/StepCue.kt
Normal file
|
@ -0,0 +1,63 @@
|
|||
package de.moritzruth.theaterdsl.show
|
||||
|
||||
import de.moritzruth.theaterdsl.util.DurationAsMillisecondsSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Serializable
|
||||
sealed interface StepCue {
|
||||
fun format(): String
|
||||
|
||||
@Serializable
|
||||
@SerialName("TEXT")
|
||||
data class Text(val speaker: String, val text: String, val clarification: String? = null) : StepCue {
|
||||
override fun format() = "text: $speaker: $text"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("MUSIC_START")
|
||||
data class MusicStart(val title: String, val duration: @Serializable(with = DurationAsMillisecondsSerializer::class) Duration) : StepCue {
|
||||
override fun format() = "music start: $title"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("MUSIC_END")
|
||||
object MusicEnd : StepCue {
|
||||
override fun format() = "music end"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("CURTAIN")
|
||||
data class Curtain(val state: State, val whileMoving: Boolean) : StepCue {
|
||||
enum class State {
|
||||
@SerialName("open")
|
||||
OPEN,
|
||||
|
||||
@SerialName("closed")
|
||||
CLOSED
|
||||
}
|
||||
|
||||
override fun format() = "curtain: $state${if (whileMoving) "(while moving)" else ""}"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("LIGHT")
|
||||
data class Light(val state: State, val whileFading: Boolean) : StepCue {
|
||||
enum class State {
|
||||
@SerialName("on")
|
||||
ON,
|
||||
|
||||
@SerialName("off")
|
||||
OFF
|
||||
}
|
||||
|
||||
override fun format() = "light: $state${if (whileFading) "(while fading)" else ""}"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("CUSTOM")
|
||||
data class Custom(val text: String) : StepCue {
|
||||
override fun format() = "custom: $text"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package de.moritzruth.theaterdsl.util
|
||||
|
||||
import kotlinx.coroutines.InternalCoroutinesApi
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
// from https://github.com/Kotlin/kotlinx.coroutines/issues/2631#issuecomment-870565860
|
||||
|
||||
class DerivedStateFlow<T>(
|
||||
private val getValue: () -> T,
|
||||
private val flow: Flow<T>
|
||||
) : StateFlow<T> {
|
||||
override val replayCache: List<T>
|
||||
get() = listOf(value)
|
||||
|
||||
override val value: T
|
||||
get() = getValue()
|
||||
|
||||
@InternalCoroutinesApi
|
||||
override suspend fun collect(collector: FlowCollector<T>): Nothing {
|
||||
coroutineScope { flow.distinctUntilChanged().stateIn(this).collect(collector) }
|
||||
}
|
||||
}
|
||||
|
||||
fun <T1, R> StateFlow<T1>.mapState(transform: (a: T1) -> R): StateFlow<R> {
|
||||
return DerivedStateFlow(
|
||||
getValue = { transform(this.value) },
|
||||
flow = this.map { a -> transform(a) }
|
||||
)
|
||||
}
|
||||
|
||||
fun <T1, T2, R> combineStates(flow: StateFlow<T1>, flow2: StateFlow<T2>, transform: (a: T1, b: T2) -> R): StateFlow<R> {
|
||||
return DerivedStateFlow(
|
||||
getValue = { transform(flow.value, flow2.value) },
|
||||
flow = combine(flow, flow2) { a, b -> transform(a, b) }
|
||||
)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package de.moritzruth.theaterdsl.util
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.serializer
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
object DurationAsMillisecondsSerializer : KSerializer<Duration> {
|
||||
override val descriptor: SerialDescriptor = serializer<Duration>().descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): Duration =
|
||||
decoder.decodeLong().milliseconds
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Duration) {
|
||||
encoder.encodeLong(value.inWholeMilliseconds)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package de.moritzruth.theaterdsl.util
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.serializer
|
||||
|
||||
object InstantAsEpochMillisecondsSerializer : KSerializer<Instant> {
|
||||
override val descriptor: SerialDescriptor = serializer<Instant>().descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): Instant =
|
||||
Instant.fromEpochMilliseconds(decoder.decodeLong())
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Instant) {
|
||||
encoder.encodeLong(value.toEpochMilliseconds())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package de.moritzruth.theaterdsl.util
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* A string which has optional additional details separated by " / "
|
||||
*/
|
||||
@JvmInline
|
||||
@Serializable
|
||||
value class StringWithDetails(val value: String) {
|
||||
companion object {
|
||||
const val DELIMITER = " / "
|
||||
}
|
||||
|
||||
val main get() = value.split(DELIMITER)[0]
|
||||
val details: String? get() = value.split(DELIMITER, limit = 2).takeIf { it.size == 2 }?.let { it[1] }
|
||||
val hasDetails get() = details !== null
|
||||
}
|
1
src/main/resources/simplelogger.properties
Normal file
1
src/main/resources/simplelogger.properties
Normal file
|
@ -0,0 +1 @@
|
|||
org.slf4j.simpleLogger.defaultLogLevel=debug
|
|
@ -12,13 +12,9 @@ version.com.fazecast..jSerialComm=2.9.3
|
|||
version.io.github.oshai..kotlin-logging-jvm=4.0.0-beta-28
|
||||
|
||||
version.kotlin=1.8.20
|
||||
|
||||
version.kotlinx.collections.immutable=0.3.5
|
||||
|
||||
version.kotlinx.coroutines=1.7.1
|
||||
|
||||
version.kotlinx.datetime=0.4.0
|
||||
|
||||
version.kotlinx.serialization=1.5.1
|
||||
|
||||
version.ktor=2.3.0
|
||||
version.org.slf4j..slf4j-simple=2.0.7
|
||||
|
|
Loading…
Add table
Reference in a new issue