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/__init__.py +0 -0
- pykit/autolog.py +329 -0
- pykit/inputs/loggableds.py +110 -0
- pykit/logdatareciever.py +14 -0
- pykit/loggedrobot.py +80 -0
- pykit/logger.py +207 -0
- pykit/logreplaysource.py +14 -0
- pykit/logtable.py +248 -0
- pykit/logvalue.py +133 -0
- pykit/networktables/loggeddashboardchooser.py +55 -0
- pykit/networktables/loggednetworkinput.py +14 -0
- pykit/networktables/nt4Publisher.py +68 -0
- pykit/watch/cli_replaywatch.py +102 -0
- pykit/wpilog/wpilogconstants.py +2 -0
- pykit/wpilog/wpilogreader.py +175 -0
- pykit/wpilog/wpilogwriter.py +279 -0
- robotpy_pykit-0.1.3b1.dist-info/METADATA +31 -0
- robotpy_pykit-0.1.3b1.dist-info/RECORD +22 -0
- robotpy_pykit-0.1.3b1.dist-info/WHEEL +5 -0
- robotpy_pykit-0.1.3b1.dist-info/entry_points.txt +2 -0
- robotpy_pykit-0.1.3b1.dist-info/licenses/LICENSE +26 -0
- robotpy_pykit-0.1.3b1.dist-info/top_level.txt +1 -0
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))
|
pykit/logreplaysource.py
ADDED
|
@@ -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)
|