Skip to content

Commit a95ceb5

Browse files
authored
Merge pull request #73 from strykeforce/measurable
Simplify Measurable interface, add MeasurableSubsystem base class
2 parents 5589f03 + 5e53905 commit a95ceb5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+936
-856
lines changed

build.gradle

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ plugins {
99
}
1010

1111
group = "org.strykeforce"
12-
version = "21.1.1"
12+
version = "21.2.0"
1313

1414
sourceCompatibility = JavaVersion.VERSION_11
1515
targetCompatibility = JavaVersion.VERSION_11
@@ -54,6 +54,17 @@ dependencies {
5454
api "org.jetbrains:annotations:20.1.0"
5555
}
5656

57+
compileKotlin {
58+
kotlinOptions {
59+
jvmTarget = "11"
60+
61+
// For creation of default methods in interfaces
62+
freeCompilerArgs += "-Xjvm-default=all"
63+
}
64+
}
65+
66+
67+
5768
java {
5869
withSourcesJar()
5970
withJavadocJar()

src/main/kotlin/org/strykeforce/thirdcoast/telemetry/AbstractInventory.kt renamed to src/main/kotlin/org/strykeforce/telemetry/AbstractInventory.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
1-
package org.strykeforce.thirdcoast.telemetry
1+
package org.strykeforce.telemetry
22

33
import com.squareup.moshi.JsonWriter
44
import okio.BufferedSink
5-
import org.strykeforce.thirdcoast.telemetry.item.Measurable
5+
import org.strykeforce.telemetry.measurable.Measurable
66
import java.io.IOException
77

88
/**
99
* An abstract base class intended to be subclassed by concrete implementations of [Inventory].
1010
*/
11-
abstract class AbstractInventory(items: Collection<Measurable>) : Inventory {
11+
abstract class AbstractInventory(measurableSet: Collection<Measurable>) : Inventory {
1212

13-
protected val items = items.sorted()
13+
protected val measurableList = measurableSet.sorted()
1414

15-
override fun itemForId(id: Int) = items[id]
15+
override fun measurableForId(index: Int) = measurableList[index]
1616

1717
@Throws(IOException::class)
1818
override fun writeInventory(sink: BufferedSink) {
1919
val writer = JsonWriter.of(sink)
2020
writer.indent = " "
2121
writer.beginObject()
2222
.name("items")
23-
.writeItems(items)
23+
.writeMeasurableList(measurableList)
2424
.name("measures")
25-
.writeMeasures(items)
25+
.writeMeasures(measurableList)
2626
.endObject()
2727
}
2828

29-
override fun toString() = "AbstractInventory(items=$items)"
29+
override fun toString() = "AbstractInventory(items=$measurableList)"
3030
}
3131

32-
private fun JsonWriter.writeItems(items: List<Measurable>): JsonWriter {
32+
private fun JsonWriter.writeMeasurableList(items: List<Measurable>): JsonWriter {
3333
beginArray()
3434
items.forEachIndexed { index, item ->
3535
beginObject()

src/main/kotlin/org/strykeforce/thirdcoast/telemetry/Inventory.kt renamed to src/main/kotlin/org/strykeforce/telemetry/Inventory.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
package org.strykeforce.thirdcoast.telemetry
1+
package org.strykeforce.telemetry
22

33
import okio.BufferedSink
4-
import org.strykeforce.thirdcoast.telemetry.item.Measurable
4+
import org.strykeforce.telemetry.measurable.Measurable
55
import java.io.IOException
66

77
/** Represents the inventory of robot hardware and subsystems that can have telemetry streaming. */
@@ -11,10 +11,10 @@ interface Inventory {
1111
* Gets a measurable item by its inventory ID. The inventory ID is an index to a measurable item in inventory and
1212
* should **not** be confused with the device ID of the underlying device the `Measurable` represents.
1313
*
14-
* @param id the inventory ID to look up.
14+
* @param index the inventory ID to look up.
1515
* @return the found Measurable item.
1616
*/
17-
fun itemForId(id: Int): Measurable
17+
fun measurableForId(index: Int): Measurable
1818

1919
/**
2020
* Writes the grapher-format JSON inventory to the supplied sink.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.strykeforce.telemetry
2+
3+
import org.strykeforce.telemetry.measurable.Measurable
4+
5+
/** Default implementation of [Inventory] for a robot. */
6+
class RobotInventory(measurableSet: Collection<Measurable>) : AbstractInventory(measurableSet)

src/main/kotlin/org/strykeforce/thirdcoast/telemetry/TelemetryController.kt renamed to src/main/kotlin/org/strykeforce/telemetry/TelemetryController.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.strykeforce.thirdcoast.telemetry
1+
package org.strykeforce.telemetry
22

33
import mu.KotlinLogging
44
import okio.Buffer
@@ -7,8 +7,8 @@ import org.eclipse.jetty.server.Server
77
import org.eclipse.jetty.server.handler.AbstractHandler
88
import org.eclipse.jetty.server.handler.DefaultHandler
99
import org.eclipse.jetty.server.handler.HandlerList
10-
import org.strykeforce.thirdcoast.telemetry.grapher.ClientHandler
11-
import org.strykeforce.thirdcoast.telemetry.grapher.Subscription
10+
import org.strykeforce.telemetry.grapher.ClientHandler
11+
import org.strykeforce.telemetry.grapher.Subscription
1212
import java.net.DatagramSocket
1313
import java.net.Inet4Address
1414
import java.net.NetworkInterface
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
@file:Suppress("unused")
2+
3+
package org.strykeforce.telemetry
4+
5+
import com.ctre.phoenix.motorcontrol.can.TalonFX
6+
import com.ctre.phoenix.motorcontrol.can.TalonSRX
7+
import mu.KotlinLogging
8+
import org.strykeforce.telemetry.measurable.Measurable
9+
import org.strykeforce.telemetry.measurable.TalonSRXMeasurable
10+
import org.strykeforce.thirdcoast.talon.TalonFXMeasurable
11+
import java.util.*
12+
import java.util.function.Function
13+
14+
private val logger = KotlinLogging.logger {}
15+
16+
/**
17+
* The Telemetry service is the main interface for client applications that are telemetry-enabled. It registers
18+
* [Measurable] instances for data collection and controls the starting and stopping of the service. When active,
19+
* the services listens for incoming control messages via a HTTP REST-like service and sends data over UDP.
20+
*
21+
* The constructor takes a factory function to create a [TelemetryController] instance with a given [Inventory].
22+
* The default [TelemetryController] has a constructor that serves this purpose, for example:
23+
* ```
24+
* TelemetryService ts = new TelemetryService(TelemetryController::new);
25+
* ts.register(talon);
26+
* ts.start();
27+
* ```
28+
*/
29+
class TelemetryService(private val telemetryControllerFactory: Function<Inventory, TelemetryController>) {
30+
31+
// Current implementation passes the `items` list to the inventory as a collection when start is called. The inventory
32+
// sorts and copies this collection into a List, using its index in this list as the inventory id. This should provide
33+
// a stable order of measurable items that assists the Grapher client when saving its configuration.
34+
35+
private val measurableSet = LinkedHashSet<Measurable>()
36+
private var telemetryController: TelemetryController? = null
37+
38+
/**
39+
* Start the Telemetry service and listen for client connections. A new instance of [TelemetryController] is created
40+
* that reflects the current list of [Measurable] items.
41+
*/
42+
fun start() {
43+
if (telemetryController != null) {
44+
logger.info("already started")
45+
return
46+
}
47+
telemetryController =
48+
telemetryControllerFactory.apply(RobotInventory(measurableSet)).also { it.start() }
49+
logger.info("started telemetry controller")
50+
}
51+
52+
/** Stop the Telemetry service. */
53+
fun stop() {
54+
if (telemetryController == null) {
55+
logger.info("already stopped")
56+
return
57+
}
58+
telemetryController?.shutdown()
59+
telemetryController = null
60+
logger.info("stopped")
61+
}
62+
63+
/**
64+
* Un-register all [Measurable] items.
65+
*
66+
* @throws IllegalStateException if TelemetryService is running.
67+
*/
68+
fun clear() {
69+
check(telemetryController == null) { "TelemetryService must be stopped to clear registered items." }
70+
measurableSet.clear()
71+
logger.info("item set was cleared")
72+
}
73+
74+
/**
75+
* Registers an Item for telemetry sending.
76+
*
77+
* @param measurable the [Measurable] to register for data collection
78+
* @throws IllegalStateException if TelemetryService is running.
79+
*/
80+
fun register(measurable: Measurable) {
81+
check(telemetryController == null) { "TelemetryService must be stopped to register an item." }
82+
if (measurableSet.add(measurable)) {
83+
logger.info { "registered item ${measurable.description}" }
84+
return
85+
}
86+
logger.info { "item ${measurable.description} was already registered" }
87+
}
88+
89+
/**
90+
* Register a collection of [Measurable] items for telemetry sending.
91+
*
92+
* @param collection the collection of Items to register for data collection
93+
* @throws IllegalStateException if TelemetryService is running.
94+
*/
95+
fun registerAll(collection: Collection<Measurable>) = collection.forEach(this::register)
96+
97+
/**
98+
* Convenience method to register a `TalonSRX` for telemetry sending.
99+
*
100+
* @param talon the TalonSRX to register for data collection
101+
* @throws IllegalStateException if TelemetryService is running.
102+
*/
103+
fun register(talon: TalonSRX) {
104+
register(TalonSRXMeasurable(talon))
105+
}
106+
107+
/**
108+
* Convenience method to register a `TalonFX` for telemetry sending.
109+
*
110+
* @param talon the TalonSRX to register for data collection
111+
* @throws IllegalStateException if TelemetryService is running.
112+
*/
113+
fun register(talon: TalonFX) {
114+
register(TalonFXMeasurable(talon))
115+
}
116+
117+
/**
118+
* Get an unmodifiable view of the registered items.
119+
*
120+
* @return an unmodifiable Set of Items.
121+
*/
122+
fun getItems(): Set<Measurable> {
123+
return Collections.unmodifiableSet(measurableSet)
124+
}
125+
126+
/**
127+
* Unregister a [Measurable] item. This service must be stopped first.
128+
*
129+
* @throws AssertionError if TelemetryService is running.
130+
*/
131+
fun remove(item: Measurable) {
132+
check(telemetryController == null) { "TelemetryService must be stopped to remove an item." }
133+
if (measurableSet.remove(item)) {
134+
logger.info { "removed $item" }
135+
return
136+
}
137+
throw AssertionError(item.toString())
138+
}
139+
140+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.strykeforce.thirdcoast.telemetry.grapher
1+
package org.strykeforce.telemetry.grapher
22

33
import mu.KotlinLogging
44
import okio.Buffer

src/main/kotlin/org/strykeforce/thirdcoast/telemetry/grapher/Subscription.kt renamed to src/main/kotlin/org/strykeforce/telemetry/grapher/Subscription.kt

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
package org.strykeforce.thirdcoast.telemetry.grapher
1+
package org.strykeforce.telemetry.grapher
22

33
import com.squareup.moshi.JsonClass
44
import com.squareup.moshi.JsonWriter
55
import com.squareup.moshi.Moshi
66
import mu.KotlinLogging
77
import okio.BufferedSink
8-
import org.strykeforce.thirdcoast.telemetry.Inventory
9-
import org.strykeforce.thirdcoast.telemetry.item.Measure
8+
import org.strykeforce.telemetry.Inventory
109
import java.io.IOException
11-
import java.util.*
1210
import java.util.function.DoubleSupplier
1311

1412
private val logger = KotlinLogging.logger {}
@@ -21,11 +19,12 @@ class Subscription(inventory: Inventory, val client: String, requestJson: String
2119

2220
init {
2321
val request = Subscription_RequestJsonJsonAdapter(moshi).fromJson(requestJson)
24-
request?.subscription?.forEach {
25-
val item = inventory.itemForId(it.itemId)
26-
val measure = Measure(it.measurementId, it.measurementId)
27-
measurements += item.measurementFor(measure)
28-
descriptions += "${item.description}: ${measure.description}"
22+
request?.subscription?.forEach { measurement ->
23+
val measurable = inventory.measurableForId(measurement.itemId)
24+
// FIXME: add null check
25+
val measure = measurable.measures.find { it.name == measurement.measurementId }!!
26+
measurements += measure.measurement
27+
descriptions += "${measurable.description}: ${measure.description}"
2928
}
3029
}
3130

@@ -52,9 +51,9 @@ class Subscription(inventory: Inventory, val client: String, requestJson: String
5251
}
5352

5453
@JsonClass(generateAdapter = true)
55-
internal data class Item(val itemId: Int, val measurementId: String)
54+
internal data class MeasurableJson(val itemId: Int, val measurementId: String)
5655

5756
@JsonClass(generateAdapter = true)
58-
internal data class RequestJson(val type:String, val subscription:List<Item>)
57+
internal data class RequestJson(val type: String, val subscription: List<MeasurableJson>)
5958

6059
}

src/main/kotlin/org/strykeforce/thirdcoast/telemetry/item/CanifierItem.kt renamed to src/main/kotlin/org/strykeforce/telemetry/measurable/CanifierMeasurable.kt

Lines changed: 17 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.strykeforce.thirdcoast.telemetry.item
1+
package org.strykeforce.telemetry.measurable
22

33
import com.ctre.phoenix.CANifier
44
import com.ctre.phoenix.CANifier.PWMChannel.*
@@ -20,52 +20,29 @@ internal const val QUAD_POSITION = "QUAD_POSITION"
2020
internal const val QUAD_VELOCITY = "QUAD_VELOCITY"
2121

2222
/** Represents a `CANifier` telemetry-enable `Measurable` item. */
23-
class CanifierItem @JvmOverloads constructor(
23+
class CanifierMeasurable @JvmOverloads constructor(
2424
private val canifier: CANifier,
2525
override val description: String = "CANifier ${canifier.deviceID}"
2626
) : Measurable {
2727

2828
override val deviceId = canifier.deviceID
29-
override val type = "canifier"
3029
override val measures = setOf(
31-
Measure(PWM0_PULSE_WIDTH, "PWM 0 Pulse Width"),
32-
Measure(PWM0_PERIOD, "PWM 0 Period"),
33-
Measure(PWM0_PULSE_WIDTH_POSITION, "PWM 0 Pulse Width Position"),
34-
Measure(PWM1_PULSE_WIDTH, "PWM 1 Pulse Width"),
35-
Measure(PWM1_PERIOD, "PWM 1 Period"),
36-
Measure(PWM1_PULSE_WIDTH_POSITION, "PWM 1 Pulse Width Position"),
37-
Measure(PWM2_PULSE_WIDTH, "PWM 2 Pulse Width"),
38-
Measure(PWM2_PERIOD, "PWM 2 Period"),
39-
Measure(PWM2_PULSE_WIDTH_POSITION, "PWM 2 Pulse Width Position"),
40-
Measure(PWM3_PULSE_WIDTH, "PWM 3 Pulse Width"),
41-
Measure(PWM3_PERIOD, "PWM 3 Period"),
42-
Measure(PWM3_PULSE_WIDTH_POSITION, "PWM 3 Pulse Width Position"),
43-
Measure(QUAD_POSITION, "Quadrature Position"),
44-
Measure(QUAD_VELOCITY, "Quadrature Velocity")
30+
Measure(PWM0_PULSE_WIDTH, "PWM 0 Pulse Width"){ pulseWidthFor(PWMChannel0) },
31+
Measure(PWM0_PERIOD, "PWM 0 Period"){ periodFor(PWMChannel0) },
32+
Measure(PWM0_PULSE_WIDTH_POSITION, "PWM 0 Pulse Width Position"){ pulseWidthPositionFor(PWMChannel0) },
33+
Measure(PWM1_PULSE_WIDTH, "PWM 1 Pulse Width"){ pulseWidthFor(PWMChannel1) },
34+
Measure(PWM1_PERIOD, "PWM 1 Period"){ periodFor(PWMChannel1) },
35+
Measure(PWM1_PULSE_WIDTH_POSITION, "PWM 1 Pulse Width Position"){ pulseWidthPositionFor(PWMChannel1) },
36+
Measure(PWM2_PULSE_WIDTH, "PWM 2 Pulse Width"){ pulseWidthFor(PWMChannel2) },
37+
Measure(PWM2_PERIOD, "PWM 2 Period"){ periodFor(PWMChannel2) },
38+
Measure(PWM2_PULSE_WIDTH_POSITION, "PWM 2 Pulse Width Position"){ pulseWidthPositionFor(PWMChannel2) },
39+
Measure(PWM3_PULSE_WIDTH, "PWM 3 Pulse Width"){ pulseWidthFor(PWMChannel3) },
40+
Measure(PWM3_PERIOD, "PWM 3 Period"){ periodFor(PWMChannel3) },
41+
Measure(PWM3_PULSE_WIDTH_POSITION, "PWM 3 Pulse Width Position"){ pulseWidthPositionFor(PWMChannel3) },
42+
Measure(QUAD_POSITION, "Quadrature Position"){ canifier.quadraturePosition.toDouble() },
43+
Measure(QUAD_VELOCITY, "Quadrature Velocity"){ canifier.quadratureVelocity.toDouble() }
4544
)
4645

47-
override fun measurementFor(measure: Measure): DoubleSupplier {
48-
49-
return when (measure.name) {
50-
PWM0_PULSE_WIDTH -> DoubleSupplier { pulseWidthFor(PWMChannel0) }
51-
PWM0_PERIOD -> DoubleSupplier { periodFor(PWMChannel0) }
52-
PWM0_PULSE_WIDTH_POSITION -> DoubleSupplier { pulseWidthPositionFor(PWMChannel0) }
53-
PWM1_PULSE_WIDTH -> DoubleSupplier { pulseWidthFor(PWMChannel1) }
54-
PWM1_PERIOD -> DoubleSupplier { periodFor(PWMChannel1) }
55-
PWM1_PULSE_WIDTH_POSITION -> DoubleSupplier { pulseWidthPositionFor(PWMChannel1) }
56-
PWM2_PULSE_WIDTH -> DoubleSupplier { pulseWidthFor(PWMChannel2) }
57-
PWM2_PERIOD -> DoubleSupplier { periodFor(PWMChannel2) }
58-
PWM2_PULSE_WIDTH_POSITION -> DoubleSupplier { pulseWidthPositionFor(PWMChannel2) }
59-
PWM3_PULSE_WIDTH -> DoubleSupplier { pulseWidthFor(PWMChannel3) }
60-
PWM3_PERIOD -> DoubleSupplier { periodFor(PWMChannel3) }
61-
PWM3_PULSE_WIDTH_POSITION -> DoubleSupplier { pulseWidthPositionFor(PWMChannel3) }
62-
QUAD_POSITION -> DoubleSupplier { canifier.quadraturePosition.toDouble() }
63-
QUAD_VELOCITY -> DoubleSupplier { canifier.quadratureVelocity.toDouble() }
64-
65-
else -> DoubleSupplier { 2767.0 }
66-
}
67-
}
68-
6946
private fun pulseWidthFor(channel: CANifier.PWMChannel): Double {
7047
val pulseWidthAndPeriod = DoubleArray(2)
7148
canifier.getPWMInput(channel, pulseWidthAndPeriod)
@@ -88,7 +65,7 @@ class CanifierItem @JvmOverloads constructor(
8865
if (this === other) return true
8966
if (javaClass != other?.javaClass) return false
9067

91-
other as CanifierItem
68+
other as CanifierMeasurable
9269

9370
if (deviceId != other.deviceId) return false
9471

0 commit comments

Comments
 (0)