robotpy-pykit 0.1.3b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pykit/logger.py ADDED
@@ -0,0 +1,207 @@
1
+ from typing import Any, Optional
2
+
3
+ from wpilib import RobotController
4
+ from pykit.autolog import AutoLogInputManager, AutoLogOutputManager
5
+ from pykit.inputs.loggableds import LoggedDriverStation
6
+ from pykit.logdatareciever import LogDataReciever
7
+ from pykit.logreplaysource import LogReplaySource
8
+ from pykit.logtable import LogTable
9
+ from pykit.networktables.loggednetworkinput import LoggedNetworkInput
10
+
11
+
12
+ class Logger:
13
+ """Manages the logging and replay of data."""
14
+
15
+ replaySource: Optional[LogReplaySource] = None
16
+ running: bool = False
17
+ cycleCount: int = 0
18
+ entry: LogTable = LogTable(0)
19
+ outputTable: LogTable = LogTable(0)
20
+ metadata: dict[str, str] = {}
21
+ checkConsole: bool = True
22
+
23
+ dataRecievers: list[LogDataReciever] = []
24
+ dashboardInputs: list[LoggedNetworkInput] = []
25
+
26
+ @classmethod
27
+ def setReplaySource(cls, replaySource: LogReplaySource):
28
+ """Sets the replay source for the logger."""
29
+ cls.replaySource = replaySource
30
+
31
+ @classmethod
32
+ def isReplay(cls) -> bool:
33
+ """Returns True if the logger is in replay mode."""
34
+ return cls.replaySource is not None
35
+
36
+ @classmethod
37
+ def recordOutput(cls, key: str, value: Any):
38
+ """
39
+ Records an output value to the log table.
40
+ This is only active when not in replay mode.
41
+ """
42
+ if cls.running:
43
+ cls.outputTable.put(key, value)
44
+
45
+ @classmethod
46
+ def recordMetadata(cls, key: str, value: str):
47
+ """
48
+ Records metadata information.
49
+ This is only active when not in replay mode.
50
+ """
51
+ if not cls.isReplay():
52
+ cls.metadata[key] = value
53
+
54
+ @classmethod
55
+ def processInputs(cls, prefix: str, inputs):
56
+ """
57
+ Processes an I/O object, either by logging its state or by updating it from the log.
58
+
59
+ In normal mode, it calls 'toLog' on the inputs object to record its state.
60
+ In replay mode, it calls 'fromLog' on the inputs object to update its state from the log.
61
+ """
62
+ if cls.running:
63
+ if cls.isReplay():
64
+ inputs.fromLog(cls.entry, prefix)
65
+ else:
66
+ inputs.toLog(cls.entry, prefix)
67
+
68
+ @classmethod
69
+ def addDataReciever(cls, reciever: LogDataReciever):
70
+ cls.dataRecievers.append(reciever)
71
+
72
+ @classmethod
73
+ def registerDashboardInput(cls, dashboardInput: LoggedNetworkInput):
74
+ cls.dashboardInputs.append(dashboardInput)
75
+
76
+ @classmethod
77
+ def start(cls):
78
+ if not cls.running:
79
+ cls.running = True
80
+ cls.cycleCount = 0
81
+ print("Logger started")
82
+
83
+ if cls.isReplay():
84
+ cls.replaySource.start()
85
+
86
+ if not cls.isReplay():
87
+ print("Logger in normal logging mode")
88
+ cls.outputTable = cls.entry.getSubTable("RealOutputs")
89
+ else:
90
+ print("Logger in replay mode")
91
+ cls.outputTable = cls.entry.getSubTable("ReplayOutputs")
92
+
93
+ metadataTable = cls.entry.getSubTable(
94
+ "ReplayMetadata" if cls.isReplay() else "RealMetadata"
95
+ )
96
+
97
+ for key, value in cls.metadata.items():
98
+ metadataTable.put(key, value)
99
+
100
+ RobotController.setTimeSource(cls.getTimestamp)
101
+ cls.periodicBeforeUser()
102
+
103
+ @classmethod
104
+ def startReciever(cls):
105
+ for reciever in cls.dataRecievers:
106
+ reciever.start()
107
+
108
+ @classmethod
109
+ def end(cls):
110
+ if cls.running:
111
+ cls.running = False
112
+ print("Logger ended")
113
+
114
+ if cls.isReplay():
115
+ cls.replaySource.end()
116
+
117
+ RobotController.setTimeSource(RobotController.getFPGATime)
118
+ for reciever in cls.dataRecievers:
119
+ reciever.end()
120
+
121
+ @classmethod
122
+ def getTimestamp(cls) -> int:
123
+ """Returns the current timestamp for logging."""
124
+ if cls.isReplay():
125
+ return cls.entry.getTimestamp()
126
+ return RobotController.getFPGATime()
127
+
128
+ @classmethod
129
+ def periodicBeforeUser(cls):
130
+ """Called periodically before user code to update the log table."""
131
+ cls.cycleCount += 1
132
+ if cls.running:
133
+ entryUpdateStart = RobotController.getFPGATime()
134
+ if not cls.isReplay():
135
+ cls.entry.setTimestamp(RobotController.getFPGATime())
136
+ else:
137
+ if not cls.replaySource.updateTable(cls.entry):
138
+ print("End of replay reached")
139
+ cls.end()
140
+ raise SystemExit(0)
141
+
142
+ dsStart = RobotController.getFPGATime()
143
+ if cls.isReplay():
144
+ LoggedDriverStation.loadFromTable(
145
+ cls.entry.getSubTable("DriverStation")
146
+ )
147
+ dashboardInputStart = RobotController.getFPGATime()
148
+
149
+ for dashInput in cls.dashboardInputs:
150
+ dashInput.periodic()
151
+
152
+ dashboardInputEnd = RobotController.getFPGATime()
153
+
154
+ cls.recordOutput(
155
+ "Logger/EntryUpdateMS", (dsStart - entryUpdateStart) / 1000.0
156
+ )
157
+ if cls.isReplay():
158
+ cls.recordOutput(
159
+ "Logger/DriverStationMS", (dashboardInputStart - dsStart) / 1000.0
160
+ )
161
+ cls.recordOutput(
162
+ "Logger/DashboardInputsMS",
163
+ (dashboardInputEnd - dashboardInputStart) / 1000.0,
164
+ )
165
+
166
+ @classmethod
167
+ def periodicAfterUser(cls, userCodeLength: int, periodicBeforeLength: int):
168
+ """Called periodically after user code to finalize the log table."""
169
+ if cls.running:
170
+ dsStart = RobotController.getFPGATime()
171
+ if not cls.isReplay():
172
+ LoggedDriverStation.saveToTable(cls.entry.getSubTable("DriverStation"))
173
+ autoLogStart = RobotController.getFPGATime()
174
+ # TODO: AutoLogOutput periodic check and update
175
+ AutoLogOutputManager.publish_all(cls.outputTable)
176
+ radioLogStart = RobotController.getFPGATime()
177
+ # TODO: RadioLogger
178
+ radioLogEnd = RobotController.getFPGATime()
179
+ if not cls.isReplay():
180
+ cls.recordOutput(
181
+ "Logger/DriverStationMS", (autoLogStart - dsStart) / 1000.0
182
+ )
183
+ for logged_input in AutoLogInputManager.getInputs():
184
+ logged_input.toLog(
185
+ cls.entry.getSubTable("/"),
186
+ "/" + logged_input.__class__.__name__,
187
+ )
188
+
189
+ cls.recordOutput(
190
+ "Logger/AutoLogOutputMS", (radioLogStart - autoLogStart) / 1000.0
191
+ )
192
+ cls.recordOutput(
193
+ "Logger/RadioLoggerMS", (radioLogEnd - radioLogStart) / 1000.0
194
+ )
195
+ cls.recordOutput("LoggedRobot/UserCodeMS", userCodeLength / 1000.0)
196
+ periodicAfterLength = radioLogEnd - dsStart
197
+ cls.recordOutput(
198
+ "LoggedRobot/LogPeriodicMS",
199
+ (periodicBeforeLength + periodicAfterLength) / 1000.0,
200
+ )
201
+ cls.recordOutput(
202
+ "LoggedRobot/FullCycleMS",
203
+ (periodicBeforeLength + userCodeLength + periodicAfterLength) / 1000.0,
204
+ )
205
+
206
+ for reciever in cls.dataRecievers:
207
+ reciever.putTable(LogTable.clone(cls.entry))
@@ -0,0 +1,14 @@
1
+ from pykit.logtable import LogTable
2
+
3
+
4
+ class LogReplaySource:
5
+ timestampKey: str = "/Timestamp"
6
+
7
+ def start(self):
8
+ raise NotImplementedError("must be implemented by a subclass")
9
+
10
+ def end(self):
11
+ pass
12
+
13
+ def updateTable(self, _table: LogTable) -> bool:
14
+ raise NotImplementedError("must be implemented by a subclass")
pykit/logtable.py ADDED
@@ -0,0 +1,248 @@
1
+ from typing import Any, Set
2
+
3
+ from wpiutil import wpistruct
4
+ from pykit.logvalue import LogValue
5
+
6
+
7
+ class LogTable:
8
+ """A table of loggable values for a single timestamp."""
9
+
10
+ prefix: str
11
+ depth: int
12
+ _timestamp: int
13
+ data: dict[str, LogValue]
14
+
15
+ def __init__(self, timestamp: int, prefix="/") -> None:
16
+ """
17
+ Constructor for the LogTable.
18
+
19
+ :param timestamp: The timestamp for the log entries in this table.
20
+ :param prefix: The prefix for the log entries.
21
+ """
22
+ self._timestamp = timestamp
23
+ self.prefix = prefix
24
+ self.depth = 0
25
+ self.data: dict[str, LogValue] = {}
26
+
27
+ @staticmethod
28
+ def clone(source: "LogTable"):
29
+ data: dict[str, LogValue] = {}
30
+ for item, value in source.data.items():
31
+ data[item] = value
32
+
33
+ newTable = LogTable(source._timestamp, source.prefix)
34
+ newTable.data = data
35
+ return newTable
36
+
37
+ def getTimestamp(self) -> int:
38
+ """Returns the timestamp of the log table."""
39
+ return self._timestamp
40
+
41
+ def setTimestamp(self, timestamp: int) -> None:
42
+ """Sets the timestamp of the log table."""
43
+ self._timestamp = timestamp
44
+
45
+ def writeAllowed(
46
+ self,
47
+ key: str,
48
+ logType: LogValue.LoggableType,
49
+ customType: str,
50
+ ) -> bool:
51
+ """
52
+ Checks if a write operation is allowed for a given key and type.
53
+ Prevents changing the type of a log entry.
54
+ """
55
+ if (currentVal := self.data.get(self.prefix + key)) is None:
56
+ return True
57
+ if currentVal.log_type != logType:
58
+ print(
59
+ f"Failed to write {key}:\nAttempted {logType} but type is {currentVal.log_type}"
60
+ )
61
+ return False
62
+ if customType != currentVal.custom_type:
63
+ print(
64
+ f"Failed to write {key}:\nAttempted {customType} but type is {currentVal.custom_type}"
65
+ )
66
+ return False
67
+ return True
68
+
69
+ def addStructSchemaNest(self, structname: str, schema: str):
70
+ typeString = structname
71
+ key = "/.schema/" + typeString
72
+ if key in self.data.keys():
73
+ return
74
+
75
+ self.data[key] = LogValue(schema.encode(), "structschema")
76
+
77
+ def addStructSchema(self, struct: Any, seen: Set[str]):
78
+ typeString = "struct:" + wpistruct.getTypeName(struct.__class__)
79
+ key = "/.schema/" + typeString
80
+ if key in self.data.keys():
81
+ return
82
+ seen.add(typeString)
83
+ schema = wpistruct.getSchema(struct.__class__)
84
+ self.data[key] = LogValue(schema.encode(), "structschema")
85
+
86
+ wpistruct.forEachNested(struct.__class__, self.addStructSchemaNest)
87
+ seen.remove(typeString)
88
+
89
+ def put(self, key: str, value: Any, typeStr: str = ""):
90
+ """
91
+ Puts a value into the log table.
92
+ The value is wrapped in a LogValue object.
93
+ """
94
+ if hasattr(value, "WPIStruct"):
95
+ # its a struct!
96
+ self.addStructSchema(value, set())
97
+ log_value = LogValue(
98
+ wpistruct.pack(value),
99
+ "struct:" + wpistruct.getTypeName(value.__class__),
100
+ )
101
+ elif (
102
+ hasattr(value, "__iter__")
103
+ and len(value) > 0
104
+ and hasattr(value[0], "WPIStruct")
105
+ ):
106
+ # structured array
107
+ self.addStructSchema(value[0], set())
108
+ log_value = LogValue(
109
+ wpistruct.packArray(value),
110
+ "struct:" + wpistruct.getTypeName(value[0].__class__) + "[]",
111
+ )
112
+ else:
113
+ log_value = LogValue(value, typeStr)
114
+ self.putValue(key, log_value)
115
+
116
+ def putValue(self, key: str, log_value: LogValue):
117
+ if isinstance(log_value.value, list) and len(log_value.value) == 0:
118
+ # empty array is a weird case in dynamic type python, just force the type to match
119
+ currentVal = self.data.get(self.prefix + key)
120
+ if currentVal is not None:
121
+ log_value.log_type = currentVal.log_type
122
+ log_value.custom_type = currentVal.custom_type
123
+ if currentVal.custom_type.startswith("struct"):
124
+ # struct logging is raw, empty array means we need a empty bytes buffer
125
+ log_value.value = b""
126
+ else:
127
+ # in the interest of not type mismatch, don't log
128
+ return
129
+ if self.writeAllowed(key, log_value.log_type, log_value.custom_type):
130
+ self.data[self.prefix + key] = log_value
131
+ else:
132
+ print(f"Failed to insert {log_value.value}")
133
+
134
+ def get(self, key: str, defaultValue: Any) -> Any:
135
+ """Gets a value from the log table."""
136
+ if (log_value := self.data.get(self.prefix + key)) is not None:
137
+ return log_value.value
138
+ return defaultValue
139
+
140
+ def getRaw(self, key: str, defaultValue: bytes) -> bytes:
141
+ """Gets a raw value from the log table."""
142
+ if (
143
+ log_value := self.data.get(self.prefix + key)
144
+ ) is not None and log_value.log_type == LogValue.LoggableType.Raw:
145
+ return log_value.value
146
+ return defaultValue
147
+
148
+ def getBoolean(self, key: str, defaultValue: bool) -> bool:
149
+ """Gets a boolean value from the log table."""
150
+ if (
151
+ log_value := self.data.get(self.prefix + key)
152
+ ) is not None and log_value.log_type == LogValue.LoggableType.Boolean:
153
+ return log_value.value
154
+ return defaultValue
155
+
156
+ def getInteger(self, key: str, defaultValue: int) -> int:
157
+ """Gets an integer value from the log table."""
158
+ if (
159
+ log_value := self.data.get(self.prefix + key)
160
+ ) is not None and log_value.log_type == LogValue.LoggableType.Integer:
161
+ return log_value.value
162
+ return defaultValue
163
+
164
+ def getFloat(self, key: str, defaultValue: float) -> float:
165
+ """Gets a float value from the log table."""
166
+ if (
167
+ log_value := self.data.get(self.prefix + key)
168
+ ) is not None and log_value.log_type == LogValue.LoggableType.Float:
169
+ return log_value.value
170
+ return defaultValue
171
+
172
+ def getDouble(self, key: str, defaultValue: float) -> float:
173
+ """Gets a double value from the log table."""
174
+ if (
175
+ log_value := self.data.get(self.prefix + key)
176
+ ) is not None and log_value.log_type == LogValue.LoggableType.Double:
177
+ return log_value.value
178
+ return defaultValue
179
+
180
+ def getString(self, key: str, defaultValue: str) -> str:
181
+ """Gets a string value from the log table."""
182
+ if (
183
+ log_value := self.data.get(self.prefix + key)
184
+ ) is not None and log_value.log_type == LogValue.LoggableType.String:
185
+ return log_value.value
186
+ return defaultValue
187
+
188
+ def getBooleanArray(self, key: str, defaultValue: list[bool]) -> list[bool]:
189
+ """Gets a boolean array value from the log table."""
190
+ if (
191
+ log_value := self.data.get(self.prefix + key)
192
+ ) is not None and log_value.log_type == LogValue.LoggableType.BooleanArray:
193
+ return log_value.value
194
+ return defaultValue
195
+
196
+ def getIntegerArray(self, key: str, defaultValue: list[int]) -> list[int]:
197
+ """Gets an integer array value from the log table."""
198
+ if (
199
+ log_value := self.data.get(self.prefix + key)
200
+ ) is not None and log_value.log_type == LogValue.LoggableType.IntegerArray:
201
+ return log_value.value
202
+ return defaultValue
203
+
204
+ def getFloatArray(self, key: str, defaultValue: list[float]) -> list[float]:
205
+ """Gets a float array value from the log table."""
206
+ if (
207
+ log_value := self.data.get(self.prefix + key)
208
+ ) is not None and log_value.log_type == LogValue.LoggableType.FloatArray:
209
+ return log_value.value
210
+ return defaultValue
211
+
212
+ def getDoubleArray(self, key: str, defaultValue: list[float]) -> list[float]:
213
+ """Gets a double array value from the log table."""
214
+ if (
215
+ log_value := self.data.get(self.prefix + key)
216
+ ) is not None and log_value.log_type == LogValue.LoggableType.DoubleArray:
217
+ return log_value.value
218
+ return defaultValue
219
+
220
+ def getStringArray(self, key: str, defaultValue: list[str]) -> list[str]:
221
+ """Gets a string array value from the log table."""
222
+ if (
223
+ log_value := self.data.get(self.prefix + key)
224
+ ) is not None and log_value.log_type == LogValue.LoggableType.StringArray:
225
+ return log_value.value
226
+ return defaultValue
227
+
228
+ def getAll(self, subtableOnly: bool = False) -> dict[str, LogValue]:
229
+ """Returns all log values in the table."""
230
+ if not subtableOnly:
231
+ return self.data
232
+ return {
233
+ key: value
234
+ for key, value in self.data.items()
235
+ if key.startswith(self.prefix)
236
+ }
237
+
238
+ def getSubTable(self, subtablePrefix: str) -> "LogTable":
239
+ """
240
+ Returns a subtable containing only entries with the given prefix.
241
+
242
+ :param subtablePrefix: The prefix to filter entries by.
243
+ :return: A new LogTable containing only the filtered entries.
244
+ """
245
+ subtable = LogTable(self.getTimestamp(), self.prefix + subtablePrefix + "/")
246
+ subtable.data = self.data
247
+ subtable.depth = self.depth + 1
248
+ return subtable
pykit/logvalue.py ADDED
@@ -0,0 +1,133 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum, auto
3
+ from typing import Any
4
+
5
+
6
+ @dataclass
7
+ class LogValue:
8
+ """Represents a value in the log table, with its type and custom type string."""
9
+
10
+ log_type: "LogValue.LoggableType"
11
+ custom_type: str
12
+ value: Any
13
+
14
+ def __init__(self, value: Any, typeStr: str = "") -> None:
15
+ """
16
+ Constructor for LogValue.
17
+ Infers the loggable type from the value.
18
+ """
19
+ self.value = value
20
+ self.custom_type = typeStr
21
+ if isinstance(value, bool):
22
+ self.log_type = LogValue.LoggableType.Boolean
23
+ elif isinstance(value, int):
24
+ self.log_type = LogValue.LoggableType.Integer
25
+ elif isinstance(value, float):
26
+ self.log_type = LogValue.LoggableType.Double
27
+ elif isinstance(value, str):
28
+ self.log_type = LogValue.LoggableType.String
29
+ elif isinstance(value, bytes):
30
+ self.log_type = LogValue.LoggableType.Raw
31
+ elif isinstance(value, list):
32
+ if len(value) == 0:
33
+ self.log_type = LogValue.LoggableType.IntegerArray
34
+ elif all(isinstance(x, bool) for x in value):
35
+ self.log_type = LogValue.LoggableType.BooleanArray
36
+ elif all(isinstance(x, int) for x in value):
37
+ self.log_type = LogValue.LoggableType.IntegerArray
38
+ elif all(isinstance(x, float) for x in value):
39
+ self.log_type = LogValue.LoggableType.DoubleArray
40
+ elif all(isinstance(x, str) for x in value):
41
+ self.log_type = LogValue.LoggableType.StringArray
42
+ else:
43
+ raise TypeError("Unsupported list type for LogValue")
44
+ else:
45
+ raise TypeError(f"Unsupported type for LogValue: {type(value)}")
46
+
47
+ @staticmethod
48
+ def withType(
49
+ log_type: "LogValue.LoggableType", data: Any, typeStr: str = ""
50
+ ) -> "LogValue":
51
+ val = LogValue(1, typeStr)
52
+ val.log_type = log_type
53
+ val.value = data
54
+ return val
55
+
56
+ def getWPILOGType(self):
57
+ if self.custom_type != "":
58
+ return self.custom_type
59
+ return self.log_type.getWPILOGType()
60
+
61
+ def getNT4Type(self):
62
+ if self.custom_type != "":
63
+ return self.custom_type
64
+ return self.log_type.getNT4Type()
65
+
66
+ class LoggableType(Enum):
67
+ """Enum for the different types of loggable values."""
68
+
69
+ Raw = auto()
70
+ Boolean = auto()
71
+ Integer = auto()
72
+ Float = auto()
73
+ Double = auto()
74
+ String = auto()
75
+ BooleanArray = auto()
76
+ IntegerArray = auto()
77
+ FloatArray = auto()
78
+ DoubleArray = auto()
79
+ StringArray = auto()
80
+
81
+ wpilogTypes = [
82
+ "raw",
83
+ "boolean",
84
+ "int64",
85
+ "float",
86
+ "double",
87
+ "string",
88
+ "boolean[]",
89
+ "int64[]",
90
+ "float[]",
91
+ "double[]",
92
+ "string[]",
93
+ ]
94
+
95
+ nt4Types = [
96
+ "raw",
97
+ "boolean",
98
+ "int",
99
+ "float",
100
+ "double",
101
+ "string",
102
+ "boolean[]",
103
+ "int[]",
104
+ "float[]",
105
+ "double[]",
106
+ "string[]",
107
+ ]
108
+
109
+ def getWPILOGType(self) -> str:
110
+ """Returns the WPILOG type string for this type."""
111
+ return LogValue.LoggableType.wpilogTypes.value[self.value - 1]
112
+
113
+ def getNT4Type(self) -> str:
114
+ """Returns the NT4 type string for this type."""
115
+ return LogValue.LoggableType.nt4Types.value[self.value - 1]
116
+
117
+ @staticmethod
118
+ def fromWPILOGType(typeStr: str) -> "LogValue.LoggableType":
119
+ """Returns a LoggableType from a WPILOG type string."""
120
+ if typeStr in LogValue.LoggableType.wpilogTypes.value:
121
+ return LogValue.LoggableType(
122
+ LogValue.LoggableType.wpilogTypes.value.index(typeStr) + 1
123
+ )
124
+ return LogValue.LoggableType.Raw
125
+
126
+ @staticmethod
127
+ def fromNT4Type(typeStr: str) -> "LogValue.LoggableType":
128
+ """Returns a LoggableType from an NT4 type string."""
129
+ if typeStr in LogValue.LoggableType.nt4Types.value:
130
+ return LogValue.LoggableType(
131
+ LogValue.LoggableType.nt4Types.value.index(typeStr) + 1
132
+ )
133
+ return LogValue.LoggableType.Raw
@@ -0,0 +1,55 @@
1
+ from typing import Optional, Generic, TypeVar
2
+
3
+ from wpilib import SendableChooser, SmartDashboard
4
+ from pykit.logger import Logger
5
+ from pykit.logtable import LogTable
6
+ from pykit.networktables.loggednetworkinput import LoggedNetworkInput
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ class LoggedDashboardChooserInputs:
12
+ def __init__(self) -> None:
13
+ pass
14
+
15
+
16
+ class LoggedDashboardChooser(LoggedNetworkInput, Generic[T]):
17
+ key: str
18
+ selectedValue: str = ""
19
+
20
+ sendableChooser: SendableChooser = SendableChooser()
21
+
22
+ options: dict[str, T] = {}
23
+
24
+ def __init__(self, key: str) -> None:
25
+ self.key = key
26
+ SmartDashboard.putData(key, self.sendableChooser)
27
+ self.periodic()
28
+
29
+ Logger.registerDashboardInput(self)
30
+
31
+ def addOption(self, key: str, value: T):
32
+ self.sendableChooser.addOption(key, key)
33
+ self.options[key] = value
34
+
35
+ def setDefaultOption(self, key: str, value: T):
36
+ self.sendableChooser.setDefaultOption(key, key)
37
+ self.options[key] = value
38
+
39
+ def getSelected(self) -> Optional[T]:
40
+ assert self.selectedValue is not None
41
+ return self.options.get(self.selectedValue)
42
+
43
+ def periodic(self):
44
+ if not Logger.isReplay():
45
+ self.selectedValue = self.sendableChooser.getSelected()
46
+ if self.selectedValue is None:
47
+ self.selectedValue = ""
48
+
49
+ Logger.processInputs(self.prefix + "/SmartDashboard", self)
50
+
51
+ def toLog(self, table: LogTable, prefix: str):
52
+ table.put(f"{prefix}/{self.key}", self.selectedValue)
53
+
54
+ def fromLog(self, table: LogTable, prefix: str):
55
+ self.selectedValue = table.get(f"{prefix}/{self.key}", self.selectedValue)
@@ -0,0 +1,14 @@
1
+ class LoggedNetworkInput:
2
+ prefix: str = "NetworkInputs"
3
+
4
+ def __init__(self) -> None:
5
+ pass
6
+
7
+ def periodic(self):
8
+ pass
9
+
10
+ @staticmethod
11
+ def removeSlash(key: str):
12
+ if key.startswith("/"):
13
+ return key[1:]
14
+ return key