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 ADDED
File without changes
pykit/autolog.py ADDED
@@ -0,0 +1,329 @@
1
+ import typing
2
+ import inspect
3
+ import gc
4
+ import dataclasses
5
+
6
+ from wpiutil import wpistruct
7
+
8
+ from pykit.logtable import LogTable
9
+ from pykit.logvalue import LogValue
10
+
11
+
12
+ class AutoLogClassOutputManager:
13
+ """
14
+ A manager class for handling automatic logging of dataclass fields.
15
+ """
16
+
17
+ logged_classes = []
18
+
19
+ @classmethod
20
+ def register_class(cls, class_to_register: typing.Any):
21
+ """
22
+ Registers a class for automatic logging.
23
+
24
+ :param class_type: The class type to register.
25
+ """
26
+ cls.logged_classes.append(class_to_register)
27
+
28
+
29
+ class AutoLogInputManager:
30
+ """
31
+ A manager class for handling automatic input loading of dataclass fields.
32
+ """
33
+
34
+ logged_classes = []
35
+
36
+ @classmethod
37
+ def register_class(cls, class_to_register: typing.Any):
38
+ """
39
+ Registers a class for automatic input loading.
40
+
41
+ :param class_type: The class type to register.
42
+ """
43
+ cls.logged_classes.append(class_to_register)
44
+
45
+ @classmethod
46
+ def getInputs(cls) -> typing.List[typing.Any]:
47
+ return cls.logged_classes
48
+
49
+
50
+ class AutoLogOutputManager:
51
+ """
52
+ A manager class for handling automatic logging of output members (fields/methods).
53
+ """
54
+
55
+ # Stores a dictionary where keys are class types and values are lists of
56
+ # dictionaries, each representing a decorated member.
57
+ # Each member dictionary contains:
58
+ # 'name': str (name of the field or method)
59
+ # 'is_method': bool (True if it's a method, False if it's a field)
60
+ # 'log_type': LogValue.LoggableType (the type to log as)
61
+ # 'custom_type': str (optional custom type string)
62
+ logged_members: typing.Dict[
63
+ typing.Type, typing.List[typing.Dict[str, typing.Any]]
64
+ ] = {}
65
+
66
+ root_cache = []
67
+
68
+ @classmethod
69
+ def publish_all(cls, table: LogTable, root_instance=None):
70
+ if root_instance is None:
71
+ if cls.root_cache:
72
+ root_instance = cls.root_cache
73
+ else:
74
+ root_instance = []
75
+ for clS in cls.logged_members:
76
+ for instance in gc.get_referrers(
77
+ clS
78
+ ): # at runtime take all instances that exist of registered classes
79
+ if instance.__class__ == clS:
80
+ root_instance.append(instance)
81
+ cls.root_cache = root_instance
82
+ for instance in root_instance:
83
+ cls.publish(instance, table)
84
+ if (
85
+ hasattr(
86
+ instance, "_do_autolog"
87
+ ) # is the attempted class actually marked for autolog?
88
+ and getattr(instance, "_do_autolog")
89
+ and hasattr(instance, "__dict__")
90
+ and not isinstance(instance, staticmethod)
91
+ ):
92
+ # be recursive, there are sub-members, but only on classes marked for autolog
93
+ cls.publish_all(table, instance.__dict__.values())
94
+
95
+ @classmethod
96
+ def register_member(
97
+ cls,
98
+ class_type: typing.Type,
99
+ member_name: str,
100
+ is_method: bool,
101
+ log_type: LogValue.LoggableType,
102
+ key: str = "",
103
+ custom_type: str = "",
104
+ ):
105
+ """
106
+ Registers a member (field or method) of a class for automatic output logging.
107
+ """
108
+ if class_type not in cls.logged_members:
109
+ cls.logged_members[class_type] = []
110
+ cls.logged_members[class_type].append(
111
+ {
112
+ "name": member_name,
113
+ "is_method": is_method,
114
+ "log_type": log_type,
115
+ "key": key,
116
+ "custom_type": custom_type,
117
+ }
118
+ )
119
+
120
+ @classmethod
121
+ def publish(cls, instance: typing.Any, table: LogTable):
122
+ """
123
+ Publishes the values of all registered members of an instance to a LogTable.
124
+ """
125
+ class_type = type(instance)
126
+ if class_type in cls.logged_members:
127
+ for member_info in cls.logged_members[class_type]:
128
+ member_name = member_info["name"]
129
+ is_method = member_info["is_method"]
130
+ log_type = member_info["log_type"]
131
+ custom_type = member_info["custom_type"]
132
+
133
+ key = member_info["key"] or member_name
134
+
135
+ value = None
136
+ if is_method:
137
+ # Assume methods are getters and take no arguments
138
+ value = getattr(instance, member_name)()
139
+ else:
140
+ value = getattr(instance, member_name)
141
+
142
+ # Put the value into the log table with the specified type
143
+ if hasattr(value, "WPIStruct") or (
144
+ hasattr(value, "__iter__")
145
+ and len(value) > 0
146
+ and hasattr(value[0], "WPIStruct")
147
+ ):
148
+ table.put(key, value)
149
+ else:
150
+ log_value = LogValue(value, custom_type)
151
+ if log_type is not None:
152
+ # Override the inferred log_type if explicitly provided in the decorator
153
+ log_value.log_type = log_type
154
+
155
+ # if table.writeAllowed(
156
+ # full_key, log_value.log_type, log_value.custom_type
157
+ # ):
158
+ table.putValue(key, log_value)
159
+
160
+
161
+ def autolog_output(
162
+ key: str,
163
+ log_type: typing.Optional[LogValue.LoggableType] = None,
164
+ custom_type: str = "",
165
+ ):
166
+ """
167
+ A decorator for methods or fields in a class to automatically log their output.
168
+ """
169
+
170
+ def decorator(member):
171
+ # This part is tricky because Python decorators for methods/fields
172
+ # don't directly give you the class at definition time.
173
+ # We'll store a temporary attribute and process it in a class decorator.
174
+ if inspect.isfunction(member):
175
+ # It's a method
176
+ print(f"[AugoLogOutput] DEBUG: Setting up log for {key}")
177
+ member._autolog_output_info = {
178
+ "is_method": True,
179
+ "log_type": log_type,
180
+ "custom_type": custom_type,
181
+ "key": key,
182
+ }
183
+ else:
184
+ # It's a field (this case is harder to handle directly with a decorator
185
+ # on the field itself, usually done via a class decorator or metaclass)
186
+ # For now, we'll assume it's a method or a property-like descriptor.
187
+ # If it's a simple field, the class decorator approach is more robust.
188
+ # Let's assume for now that direct field decoration will be handled
189
+ # by a class decorator that scans for these attributes.
190
+ # For direct field decoration, we might need a descriptor.
191
+ # For simplicity, let's focus on methods first, or assume a class
192
+ # decorator will pick up field annotations.
193
+ # For now, let's make it work for methods and properties.
194
+ member._autolog_output_info = {
195
+ "is_method": False, # This will be true for properties too
196
+ "log_type": log_type,
197
+ "custom_type": custom_type,
198
+ "key": key,
199
+ }
200
+ return member
201
+
202
+ return decorator
203
+
204
+
205
+ def autologgable_output(cls):
206
+ """
207
+ A class decorator that scans for methods/fields decorated with @autolog_output
208
+ and registers them with AutoLogOutputManager.
209
+ """
210
+ for name in dir(cls):
211
+ member = getattr(cls, name)
212
+ if hasattr(member, "_autolog_output_info"):
213
+ info = member._autolog_output_info
214
+ AutoLogOutputManager.register_member(
215
+ cls,
216
+ name,
217
+ info["is_method"],
218
+ info["log_type"],
219
+ info["key"],
220
+ info["custom_type"],
221
+ )
222
+
223
+ setattr(cls, "_do_autolog", True)
224
+ return cls
225
+
226
+
227
+ def autolog(cls=None, /):
228
+ """
229
+ A class decorator that adds 'toLog' and 'fromLog' methods to a dataclass for automatic logging.
230
+
231
+ The 'toLog' method serializes the dataclass fields to a LogTable.
232
+ The 'fromLog' method deserializes the data from a LogTable into the dataclass fields.
233
+
234
+ This decorator is designed to be used with dataclasses and supports nested dataclasses
235
+ decorated with @autolog.
236
+ """
237
+
238
+ def wrap(clS):
239
+ resolved_hints = typing.get_type_hints(clS)
240
+ field_names = [field.name for field in dataclasses.fields(clS)]
241
+
242
+ def toLog(self, table: LogTable, prefix: str):
243
+ """
244
+ Recursively logs the fields of the dataclass to a LogTable.
245
+
246
+ :param table: The LogTable instance to write to.
247
+ :param prefix: The prefix for the log entries.
248
+ """
249
+ for name in field_names:
250
+ value = getattr(self, name)
251
+ field_prefix = f"{prefix}/{name}"
252
+ if hasattr(value, "toLog"):
253
+ value.toLog(table, field_prefix)
254
+ else:
255
+ table.put(field_prefix, value)
256
+
257
+ def fromLog(self, table: LogTable, prefix: str):
258
+ """
259
+ Recursively reads the fields of the dataclass from a LogTable.
260
+
261
+ :param table: The LogTable instance to read from.
262
+ :param prefix: The prefix for the log entries.
263
+ """
264
+ for name in field_names:
265
+ field_prefix = f"{prefix}/{name}"
266
+
267
+ value = getattr(self, name)
268
+ if hasattr(value, "fromLog"):
269
+ value.fromLog(table, field_prefix)
270
+ else:
271
+ field_type = resolved_hints[name]
272
+ new_value = None
273
+
274
+ origin = typing.get_origin(field_type)
275
+ if origin is list:
276
+ list_type = typing.get_args(field_type)[0]
277
+ if list_type is bool:
278
+ new_value = table.getBooleanArray(field_prefix, value)
279
+ elif list_type is int:
280
+ new_value = table.getIntegerArray(field_prefix, value)
281
+ elif list_type is float:
282
+ new_value = table.getDoubleArray(field_prefix, value)
283
+ elif list_type is str:
284
+ new_value = table.getStringArray(field_prefix, value)
285
+ elif hasattr(list_type, "WPIStruct"):
286
+ new_value = wpistruct.unpackArray(
287
+ list_type, table.getRaw(field_prefix, b"")
288
+ )
289
+ # is it struct?
290
+ else:
291
+ print(
292
+ f"[AutoLog] Failed to read of type {field_type} with value {list_type}"
293
+ )
294
+ else:
295
+ if field_type is bool:
296
+ new_value = table.getBoolean(field_prefix, value)
297
+ elif field_type is int:
298
+ new_value = table.getInteger(field_prefix, value)
299
+ elif field_type is float:
300
+ new_value = table.getDouble(field_prefix, value)
301
+ elif field_type is str:
302
+ new_value = table.getString(field_prefix, value)
303
+ elif hasattr(field_type, "WPIStruct"):
304
+ new_value = wpistruct.unpack(
305
+ field_type, table.getRaw(field_prefix, b"")
306
+ )
307
+ # is it struct?
308
+ else:
309
+ print(f"[AutoLog] Failed to read of type {field_type}")
310
+
311
+ if new_value is not None:
312
+ setattr(self, name, new_value)
313
+
314
+ def registerAutologged(self) -> None:
315
+ print(f"[AutoLog] registering {self.name}")
316
+ AutoLogInputManager.register_class(self)
317
+
318
+ setattr(clS, "toLog", toLog)
319
+ setattr(clS, "fromLog", fromLog)
320
+ # https://docs.python.org/3/library/dataclasses.html#dataclasses.__post_init__
321
+ # https://docs.python.org/3/reference/expressions.html#private-name-mangling
322
+ setattr(cls, f"_{clS.__class__.__name__}__post_init__", registerAutologged)
323
+
324
+ return clS
325
+
326
+ if cls is None:
327
+ return wrap
328
+
329
+ return wrap(cls)
@@ -0,0 +1,110 @@
1
+ from hal import AllianceStationID
2
+ from wpilib import DriverStation
3
+ from wpilib.simulation import DriverStationSim
4
+
5
+ from pykit.logtable import LogTable
6
+ from pykit.logvalue import LogValue
7
+
8
+
9
+ class LoggedDriverStation:
10
+ """A dataclass for holding Driver Station I/O data."""
11
+
12
+ @classmethod
13
+ def saveToTable(cls, table: LogTable):
14
+ """Saves the current Driver Station data to the log table."""
15
+ alliance = DriverStation.getAlliance()
16
+ location = DriverStation.getLocation()
17
+ station = (
18
+ 0
19
+ if location is None or alliance is None
20
+ else (location + (3 if alliance == DriverStation.Alliance.kBlue else 0))
21
+ )
22
+ table.put("AllianceStation", station)
23
+ table.put("EventName", DriverStation.getEventName())
24
+ table.put("GameSpecificMessage", DriverStation.getGameSpecificMessage())
25
+ table.put("MatchNumber", DriverStation.getMatchNumber())
26
+ table.put("ReplayNumber", DriverStation.getReplayNumber())
27
+ table.put("MatchType", DriverStation.getMatchType().value)
28
+ table.put("MatchTime", DriverStation.getMatchTime())
29
+
30
+ table.put("Enabled", DriverStation.isEnabled())
31
+ table.put("Autonomous", DriverStation.isAutonomous())
32
+ table.put("Test", DriverStation.isTest())
33
+ table.put("EmergencyStop", DriverStation.isEStopped())
34
+ table.put("FMSAttached", DriverStation.isFMSAttached())
35
+ table.put("DSAttached", DriverStation.isDSAttached())
36
+
37
+ for i in range(DriverStation.kJoystickPorts):
38
+ joystickTable = table.getSubTable(f"Joystick{i}")
39
+ joystickTable.put("Name", DriverStation.getJoystickName(i).strip())
40
+ joystickTable.put("Type", DriverStation.getJoystickType(i))
41
+ joystickTable.put("Xbox", DriverStation.getJoystickIsXbox(i))
42
+ joystickTable.put("ButtonCount", DriverStation.getStickButtonCount(i))
43
+ joystickTable.put("ButtonValues", DriverStation.getStickButtons(i))
44
+
45
+ povCount = DriverStation.getStickPOVCount(i)
46
+ povValues = []
47
+ for j in range(povCount):
48
+ povValues.append(DriverStation.getStickPOV(i, j))
49
+ joystickTable.putValue(
50
+ "POVs", LogValue.withType(LogValue.LoggableType.IntegerArray, povValues)
51
+ )
52
+
53
+ axisCount = DriverStation.getStickAxisCount(i)
54
+ axisValues = []
55
+ axisTypes = []
56
+ for j in range(axisCount):
57
+ axisValues.append(DriverStation.getStickAxis(i, j))
58
+ axisTypes.append(DriverStation.getJoystickAxisType(i, j))
59
+
60
+ joystickTable.putValue(
61
+ "AxesValues",
62
+ LogValue.withType(LogValue.LoggableType.DoubleArray, axisValues),
63
+ )
64
+ joystickTable.put("AxisTypes", axisTypes)
65
+
66
+ @classmethod
67
+ def loadFromTable(cls, table: LogTable):
68
+ DriverStationSim.setAllianceStationId(
69
+ AllianceStationID(
70
+ table.get("AllianceStation", AllianceStationID.kRed1.value)
71
+ )
72
+ )
73
+ DriverStationSim.setEventName(table.get("EventName", ""))
74
+ DriverStationSim.setGameSpecificMessage(table.get("GameSpecificMessage", ""))
75
+ DriverStationSim.setMatchNumber(table.get("MatchNumber", 0))
76
+ DriverStationSim.setReplayNumber(table.get("ReplayNumber", 0))
77
+ DriverStationSim.setMatchType(
78
+ DriverStation.MatchType(table.get("MatchType", 0))
79
+ )
80
+ DriverStationSim.setMatchTime(table.get("MatchTime", -1.0))
81
+
82
+ DriverStationSim.setEnabled(table.get("Enabled", False))
83
+ DriverStationSim.setAutonomous(table.get("Autonomous", False))
84
+ DriverStationSim.setTest(table.get("Test", False))
85
+ DriverStationSim.setEStop(table.get("EmergencyStop", False))
86
+ DriverStationSim.setFmsAttached(table.get("FMSAttached", False))
87
+ dsAttached = table.get("DSAttached", False)
88
+ DriverStationSim.setDsAttached(dsAttached)
89
+ for i in range(DriverStation.kJoystickPorts):
90
+ joystickTable = table.getSubTable(f"Joystick{i}")
91
+ # print(joystickTable.getDoubleArray("AxesValues", []))
92
+
93
+ buttonValues = joystickTable.get("ButtonValues", 0)
94
+ DriverStationSim.setJoystickButtons(i, buttonValues)
95
+
96
+ povValues = joystickTable.get("POVs", [])
97
+ DriverStationSim.setJoystickPOVCount(i, len(povValues))
98
+ for j, pov in enumerate(povValues):
99
+ DriverStationSim.setJoystickPOV(i, j, pov)
100
+
101
+ axisValues = joystickTable.get("AxesValues", [])
102
+ axisTypes = joystickTable.get("AxisTypes", [])
103
+
104
+ DriverStationSim.setJoystickAxisCount(i, len(axisValues))
105
+ for j, (axis_val, axis_type) in enumerate(zip(axisValues, axisTypes)):
106
+ DriverStationSim.setJoystickAxis(i, j, axis_val)
107
+ DriverStationSim.setJoystickAxisType(i, j, axis_type)
108
+
109
+ if dsAttached:
110
+ DriverStationSim.notifyNewData()
@@ -0,0 +1,14 @@
1
+ from pykit.logtable import LogTable
2
+
3
+
4
+ class LogDataReciever:
5
+ timestampKey: str = "/Timestamp"
6
+
7
+ def start(self):
8
+ pass
9
+
10
+ def end(self):
11
+ pass
12
+
13
+ def putTable(self, table: LogTable):
14
+ pass
pykit/loggedrobot.py ADDED
@@ -0,0 +1,80 @@
1
+ import hal
2
+ from wpilib import (
3
+ DSControlWord,
4
+ IterativeRobotBase,
5
+ RobotController,
6
+ Watchdog,
7
+ )
8
+
9
+ from pykit.logger import Logger
10
+
11
+
12
+ class LoggedRobot(IterativeRobotBase):
13
+ """A robot base class that provides logging and replay functionality."""
14
+
15
+ default_period = 0.02 # seconds
16
+
17
+ def printOverrunMessage(self):
18
+ """Prints a message when the main loop overruns."""
19
+ print("Loop overrun detected!")
20
+
21
+ def __init__(self):
22
+ """
23
+ Constructor for the LoggedRobot.
24
+ Initializes the robot, sets up the logger, and creates I/O objects.
25
+ """
26
+ IterativeRobotBase.__init__(self, LoggedRobot.default_period)
27
+ self.useTiming = True
28
+ self._nextCycleUs = 0
29
+ self._periodUs = int(self.getPeriod() * 1000000)
30
+
31
+ self.notifier = hal.initializeNotifier()[0]
32
+ self.watchdog = Watchdog(LoggedRobot.default_period, self.printOverrunMessage)
33
+ self.word = DSControlWord()
34
+
35
+ def endCompetition(self) -> None:
36
+ """Called at the end of the competition to clean up resources."""
37
+ hal.stopNotifier(self.notifier)
38
+ hal.cleanNotifier(self.notifier)
39
+
40
+ def startCompetition(self) -> None:
41
+ """
42
+ The main loop of the robot.
43
+ Handles timing, logging, and calling the periodic functions.
44
+ """
45
+ self.robotInit()
46
+
47
+ # TODO: handle autolog outputs
48
+
49
+ if self.isSimulation():
50
+ self._simulationInit()
51
+
52
+ self.initEnd = RobotController.getFPGATime()
53
+ Logger.periodicAfterUser(self.initEnd, 0)
54
+ print("Robot startup complete!")
55
+ hal.observeUserProgramStarting()
56
+
57
+ Logger.startReciever()
58
+
59
+ while True:
60
+ if self.useTiming:
61
+ currentTime = RobotController.getFPGATime() # microseconds
62
+ if self._nextCycleUs < currentTime:
63
+ # loop overrun, immediate next cycle
64
+ self._nextCycleUs = currentTime
65
+ else:
66
+ hal.updateNotifierAlarm(self.notifier, int(self._nextCycleUs))
67
+ if hal.waitForNotifierAlarm(self.notifier) == 0:
68
+ break
69
+ self._nextCycleUs += self._periodUs
70
+
71
+ periodicBeforeStart = RobotController.getFPGATime()
72
+ Logger.periodicBeforeUser()
73
+
74
+ userCodeStart = RobotController.getFPGATime()
75
+ self._loopFunc()
76
+ userCodeEnd = RobotController.getFPGATime()
77
+
78
+ Logger.periodicAfterUser(
79
+ userCodeEnd - userCodeStart, userCodeStart - periodicBeforeStart
80
+ )