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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from ntcore import (
|
|
2
|
+
GenericPublisher,
|
|
3
|
+
IntegerPublisher,
|
|
4
|
+
NetworkTable,
|
|
5
|
+
NetworkTableInstance,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from pykit.logdatareciever import LogDataReciever
|
|
9
|
+
from pykit.logtable import LogTable
|
|
10
|
+
from pykit.logvalue import LogValue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NT4Publisher(LogDataReciever):
|
|
14
|
+
pykitTable: NetworkTable
|
|
15
|
+
lastTable: LogTable = LogTable(0)
|
|
16
|
+
|
|
17
|
+
timestampPublisher: IntegerPublisher
|
|
18
|
+
publishers: dict[str, GenericPublisher] = {}
|
|
19
|
+
|
|
20
|
+
def __init__(self, actLikeAKit: bool = False):
|
|
21
|
+
self.pykitTable = NetworkTableInstance.getDefault().getTable(
|
|
22
|
+
"/AdvantageKit" if actLikeAKit else "/PyKit"
|
|
23
|
+
)
|
|
24
|
+
self.timestampPublisher = self.pykitTable.getIntegerTopic(
|
|
25
|
+
self.timestampKey[1:]
|
|
26
|
+
).publish()
|
|
27
|
+
|
|
28
|
+
def putTable(self, table: LogTable):
|
|
29
|
+
self.timestampPublisher.set(table.getTimestamp(), table.getTimestamp())
|
|
30
|
+
|
|
31
|
+
newMap = table.getAll(False)
|
|
32
|
+
oldMap = self.lastTable.getAll(False)
|
|
33
|
+
|
|
34
|
+
for key, newValue in newMap.items():
|
|
35
|
+
if newValue == oldMap.get(key):
|
|
36
|
+
continue
|
|
37
|
+
key = key[1:]
|
|
38
|
+
publisher = self.publishers.get(key)
|
|
39
|
+
if publisher is None:
|
|
40
|
+
publisher = self.pykitTable.getTopic(key).genericPublish(
|
|
41
|
+
newValue.getNT4Type()
|
|
42
|
+
)
|
|
43
|
+
self.publishers[key] = publisher
|
|
44
|
+
|
|
45
|
+
match newValue.log_type:
|
|
46
|
+
case LogValue.LoggableType.Raw:
|
|
47
|
+
publisher.setRaw(newValue.value, table.getTimestamp())
|
|
48
|
+
|
|
49
|
+
case LogValue.LoggableType.Boolean:
|
|
50
|
+
publisher.setBoolean(newValue.value, table.getTimestamp())
|
|
51
|
+
case LogValue.LoggableType.Integer:
|
|
52
|
+
publisher.setInteger(newValue.value, table.getTimestamp())
|
|
53
|
+
case LogValue.LoggableType.Float:
|
|
54
|
+
publisher.setFloat(newValue.value, table.getTimestamp())
|
|
55
|
+
case LogValue.LoggableType.Double:
|
|
56
|
+
publisher.setDouble(newValue.value, table.getTimestamp())
|
|
57
|
+
case LogValue.LoggableType.String:
|
|
58
|
+
publisher.setString(newValue.value, table.getTimestamp())
|
|
59
|
+
case LogValue.LoggableType.BooleanArray:
|
|
60
|
+
publisher.setBooleanArray(newValue.value, table.getTimestamp())
|
|
61
|
+
case LogValue.LoggableType.IntegerArray:
|
|
62
|
+
publisher.setIntegerArray(newValue.value, table.getTimestamp())
|
|
63
|
+
case LogValue.LoggableType.FloatArray:
|
|
64
|
+
publisher.setFloatArray(newValue.value, table.getTimestamp())
|
|
65
|
+
case LogValue.LoggableType.DoubleArray:
|
|
66
|
+
publisher.setDoubleArray(newValue.value, table.getTimestamp())
|
|
67
|
+
case LogValue.LoggableType.StringArray:
|
|
68
|
+
publisher.setStringArray(newValue.value, table.getTimestamp())
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import argparse
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import logging
|
|
5
|
+
import pathlib
|
|
6
|
+
from tempfile import gettempdir
|
|
7
|
+
import time
|
|
8
|
+
import typing
|
|
9
|
+
from watchdog.events import FileSystemEventHandler
|
|
10
|
+
from watchdog.observers import Observer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
from pykit.loggedrobot import LoggedRobot
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("pyfrc.sim")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
entry_points = importlib.metadata.entry_points
|
|
20
|
+
|
|
21
|
+
AKIT_FILENAME = "akit-log-path.txt"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PyKitReplayWatch:
|
|
25
|
+
"""
|
|
26
|
+
Runs the robot in simulation and replay watch
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
do_update: bool = False
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def doUpdate(cls) -> bool:
|
|
33
|
+
return cls.do_update
|
|
34
|
+
|
|
35
|
+
def __init__(self, parser: argparse.ArgumentParser):
|
|
36
|
+
self.simexts = {}
|
|
37
|
+
|
|
38
|
+
for entry_point in entry_points(group="robotpy_sim.2026"):
|
|
39
|
+
try:
|
|
40
|
+
sim_ext_module = entry_point.load()
|
|
41
|
+
except ImportError:
|
|
42
|
+
print(f"WARNING: Error detected in {entry_point}")
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
self.simexts[entry_point.name] = sim_ext_module
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
cmd_help = importlib.metadata.metadata(entry_point.dist.name)["summary"]
|
|
49
|
+
except AttributeError:
|
|
50
|
+
cmd_help = "Load specified simulation extension"
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
f"--{entry_point.name}",
|
|
53
|
+
default=False,
|
|
54
|
+
action="store_true",
|
|
55
|
+
help=cmd_help,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def run(
|
|
59
|
+
self,
|
|
60
|
+
options: argparse.Namespace, # pylint: disable=unused-argument
|
|
61
|
+
project_path: pathlib.Path, # pylint: disable=unused-argument
|
|
62
|
+
robot_class: typing.Type[LoggedRobot], # pylint: disable=unused-argument
|
|
63
|
+
):
|
|
64
|
+
|
|
65
|
+
PyKitReplayWatch.do_update = False
|
|
66
|
+
|
|
67
|
+
class UpdateHandler(FileSystemEventHandler):
|
|
68
|
+
def on_modified(self, event):
|
|
69
|
+
if not event.is_directory and event.src_path.endswith(".py"):
|
|
70
|
+
print("[PyKit] Modification detected!")
|
|
71
|
+
PyKitReplayWatch.do_update = True
|
|
72
|
+
|
|
73
|
+
file_handler = UpdateHandler()
|
|
74
|
+
self.observer = Observer()
|
|
75
|
+
self.observer.schedule(file_handler, ".", recursive=True)
|
|
76
|
+
|
|
77
|
+
self.observer.start()
|
|
78
|
+
|
|
79
|
+
if "LOG_PATH" not in os.environ:
|
|
80
|
+
# see if we can pull from ascope's actively loaded log
|
|
81
|
+
readPath = os.path.join(gettempdir(), AKIT_FILENAME)
|
|
82
|
+
if not os.path.exists(readPath):
|
|
83
|
+
print("[PyKit] Cannot load log to replay!")
|
|
84
|
+
return
|
|
85
|
+
with open(
|
|
86
|
+
os.path.join(gettempdir(), AKIT_FILENAME), "r", encoding="utf-8"
|
|
87
|
+
) as f:
|
|
88
|
+
readfilepath = f.readline()
|
|
89
|
+
os.environ["LOG_PATH"] = readfilepath
|
|
90
|
+
print(f"[PyKit] Logging from {readfilepath}")
|
|
91
|
+
else:
|
|
92
|
+
logpath = os.environ["LOG_PATH"]
|
|
93
|
+
print(f"[PyKit] Logging from {logpath}")
|
|
94
|
+
|
|
95
|
+
while True:
|
|
96
|
+
PyKitReplayWatch.do_update = False
|
|
97
|
+
print("[PyKit] Running replay...")
|
|
98
|
+
# this is hacky, a real solution is needed for resetting environment
|
|
99
|
+
os.system("python -m robotpy sim --nogui")
|
|
100
|
+
print("[PyKit] replay finished...")
|
|
101
|
+
while not PyKitReplayWatch.doUpdate():
|
|
102
|
+
time.sleep(1)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from typing import Any, Iterator
|
|
2
|
+
|
|
3
|
+
from wpiutil.log import DataLogReader
|
|
4
|
+
|
|
5
|
+
from pykit.logreplaysource import LogReplaySource
|
|
6
|
+
from pykit.logtable import LogTable
|
|
7
|
+
from pykit.logvalue import LogValue
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def safeNext(val: Iterator[Any]):
|
|
11
|
+
try:
|
|
12
|
+
return next(val)
|
|
13
|
+
except StopIteration:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WPILOGReader(LogReplaySource):
|
|
18
|
+
"""Reads a .wpilog file and provides the data as a replay source."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, filename: str) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Constructor for WPILOGReader.
|
|
23
|
+
|
|
24
|
+
:param filename: The path to the .wpilog file.
|
|
25
|
+
"""
|
|
26
|
+
self.filename = filename
|
|
27
|
+
|
|
28
|
+
def start(self):
|
|
29
|
+
self.reader = DataLogReader(self.filename)
|
|
30
|
+
self.isValid = (
|
|
31
|
+
self.reader.isValid()
|
|
32
|
+
# and self.reader.getExtraHeader() == wpilogconstants.extraHeader
|
|
33
|
+
)
|
|
34
|
+
print(self.reader.isValid())
|
|
35
|
+
print(self.reader.getExtraHeader())
|
|
36
|
+
self.records = iter([])
|
|
37
|
+
|
|
38
|
+
if self.isValid:
|
|
39
|
+
# Create a new iterator for the initial entry scan
|
|
40
|
+
self.records = iter(self.reader)
|
|
41
|
+
self.entryIds: dict[int, str] = {}
|
|
42
|
+
self.entryTypes: dict[int, LogValue.LoggableType] = {}
|
|
43
|
+
self.timestamp = None
|
|
44
|
+
self.entryCustomTypes = {}
|
|
45
|
+
|
|
46
|
+
else:
|
|
47
|
+
print("[WPILogReader] not valid")
|
|
48
|
+
|
|
49
|
+
def updateTable(self, table: LogTable) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Updates a LogTable with the next record from the log file.
|
|
52
|
+
|
|
53
|
+
:param table: The LogTable to update.
|
|
54
|
+
:return: True if the table was updated, False if the end of the log was reached.
|
|
55
|
+
"""
|
|
56
|
+
if not self.isValid:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
if self.timestamp is not None:
|
|
60
|
+
table.setTimestamp(self.timestamp)
|
|
61
|
+
|
|
62
|
+
keepLogging = False
|
|
63
|
+
while (record := safeNext(self.records)) is not None:
|
|
64
|
+
if record.isControl():
|
|
65
|
+
if record.isStart():
|
|
66
|
+
startData = record.getStartData()
|
|
67
|
+
self.entryIds[startData.entry] = startData.name
|
|
68
|
+
typeStr = startData.type
|
|
69
|
+
self.entryTypes[startData.entry] = (
|
|
70
|
+
LogValue.LoggableType.fromWPILOGType(typeStr)
|
|
71
|
+
)
|
|
72
|
+
if typeStr.startswith("struct:") or typeStr == "structschema":
|
|
73
|
+
self.entryCustomTypes[startData.entry] = typeStr
|
|
74
|
+
else:
|
|
75
|
+
entry = self.entryIds.get(record.getEntry())
|
|
76
|
+
if entry is not None:
|
|
77
|
+
if entry == self.timestampKey:
|
|
78
|
+
firsttimestamp = self.timestamp is None
|
|
79
|
+
self.timestamp = record.getInteger()
|
|
80
|
+
if firsttimestamp:
|
|
81
|
+
table.setTimestamp(self.timestamp)
|
|
82
|
+
else:
|
|
83
|
+
keepLogging = True # we still have a timestamp, just need to wait until next iter
|
|
84
|
+
break
|
|
85
|
+
elif (
|
|
86
|
+
self.timestamp is not None
|
|
87
|
+
and record.getTimestamp() == self.timestamp
|
|
88
|
+
):
|
|
89
|
+
entry = entry[1:]
|
|
90
|
+
if entry.startswith("ReplayOutputs"):
|
|
91
|
+
continue
|
|
92
|
+
customType = self.entryCustomTypes.get(record.getEntry())
|
|
93
|
+
entryType = self.entryTypes.get(record.getEntry())
|
|
94
|
+
if customType is None:
|
|
95
|
+
customType = ""
|
|
96
|
+
match entryType:
|
|
97
|
+
case LogValue.LoggableType.Raw:
|
|
98
|
+
table.putValue(
|
|
99
|
+
entry,
|
|
100
|
+
LogValue.withType(
|
|
101
|
+
entryType, record.getRaw(), customType
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
case LogValue.LoggableType.Boolean:
|
|
105
|
+
table.putValue(
|
|
106
|
+
entry,
|
|
107
|
+
LogValue.withType(
|
|
108
|
+
entryType, record.getBoolean(), customType
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
case LogValue.LoggableType.Integer:
|
|
112
|
+
table.putValue(
|
|
113
|
+
entry,
|
|
114
|
+
LogValue.withType(
|
|
115
|
+
entryType, record.getInteger(), customType
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
case LogValue.LoggableType.Float:
|
|
119
|
+
table.putValue(
|
|
120
|
+
entry,
|
|
121
|
+
LogValue.withType(
|
|
122
|
+
entryType, record.getFloat(), customType
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
case LogValue.LoggableType.Double:
|
|
126
|
+
table.putValue(
|
|
127
|
+
entry,
|
|
128
|
+
LogValue.withType(
|
|
129
|
+
entryType, record.getDouble(), customType
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
case LogValue.LoggableType.String:
|
|
133
|
+
table.putValue(
|
|
134
|
+
entry,
|
|
135
|
+
LogValue.withType(
|
|
136
|
+
entryType, record.getString(), customType
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
case LogValue.LoggableType.BooleanArray:
|
|
140
|
+
table.putValue(
|
|
141
|
+
entry,
|
|
142
|
+
LogValue.withType(
|
|
143
|
+
entryType, record.getBooleanArray(), customType
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
case LogValue.LoggableType.IntegerArray:
|
|
147
|
+
table.putValue(
|
|
148
|
+
entry,
|
|
149
|
+
LogValue.withType(
|
|
150
|
+
entryType, record.getIntegerArray(), customType
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
case LogValue.LoggableType.FloatArray:
|
|
154
|
+
table.putValue(
|
|
155
|
+
entry,
|
|
156
|
+
LogValue.withType(
|
|
157
|
+
entryType, record.getFloatArray(), customType
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
case LogValue.LoggableType.DoubleArray:
|
|
161
|
+
table.putValue(
|
|
162
|
+
entry,
|
|
163
|
+
LogValue.withType(
|
|
164
|
+
entryType, record.getDoubleArray(), customType
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
case LogValue.LoggableType.StringArray:
|
|
168
|
+
table.putValue(
|
|
169
|
+
entry,
|
|
170
|
+
LogValue.withType(
|
|
171
|
+
entryType, record.getStringArray(), customType
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return keepLogging
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import random
|
|
3
|
+
import datetime
|
|
4
|
+
from tempfile import gettempdir
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from hal import MatchType
|
|
9
|
+
from wpilib import DataLogManager, RobotBase, RobotController
|
|
10
|
+
from pykit.logdatareciever import LogDataReciever
|
|
11
|
+
from pykit.logger import Logger
|
|
12
|
+
from pykit.logtable import LogTable
|
|
13
|
+
from pykit.logvalue import LogValue
|
|
14
|
+
from pykit.wpilog import wpilogconstants
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from wpiutil.log import DataLog
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ASCOPE_FILENAME = "ascope-log-path.txt"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WPILOGWriter(LogDataReciever):
|
|
24
|
+
"""Writes a LogTable to a .wpilog file."""
|
|
25
|
+
|
|
26
|
+
log: "DataLog"
|
|
27
|
+
defaultPathRio: str = "/U/logs"
|
|
28
|
+
defaultPathSim: str = "pyLogs"
|
|
29
|
+
|
|
30
|
+
folder: str
|
|
31
|
+
filename: str
|
|
32
|
+
randomIdentifier: str
|
|
33
|
+
dsAttachedTime: int = 0
|
|
34
|
+
autoRename: bool
|
|
35
|
+
logDate: datetime.datetime | None
|
|
36
|
+
logMatchText: str
|
|
37
|
+
|
|
38
|
+
isOpen: bool = False
|
|
39
|
+
lastTable: LogTable
|
|
40
|
+
timestampId: int
|
|
41
|
+
entryIds: dict[str, int]
|
|
42
|
+
entryTypes: dict[str, LogValue.LoggableType]
|
|
43
|
+
entryUnits: dict[str, str]
|
|
44
|
+
|
|
45
|
+
def __init__(self, filename: str | None = None):
|
|
46
|
+
path = self.defaultPathSim if RobotBase.isSimulation() else self.defaultPathRio
|
|
47
|
+
|
|
48
|
+
self.randomIdentifier = f"{random.randint(0, 0xFFFF):04X}"
|
|
49
|
+
|
|
50
|
+
self.folder = os.path.abspath(
|
|
51
|
+
os.path.dirname(filename) if filename is not None else path
|
|
52
|
+
)
|
|
53
|
+
self.filename = (
|
|
54
|
+
os.path.basename(filename)
|
|
55
|
+
if filename is not None
|
|
56
|
+
else f"pykit_{self.randomIdentifier}.wpilog"
|
|
57
|
+
)
|
|
58
|
+
self.autoRename = False
|
|
59
|
+
|
|
60
|
+
def start(self):
|
|
61
|
+
# create folder if necessary
|
|
62
|
+
if not os.path.exists(self.folder):
|
|
63
|
+
os.makedirs(self.folder)
|
|
64
|
+
|
|
65
|
+
# delete log if it exists
|
|
66
|
+
|
|
67
|
+
# create a new log
|
|
68
|
+
fullPath = os.path.join(self.folder, self.filename)
|
|
69
|
+
print(f"[WPILogWriter] Creating WPILOG file at {fullPath}")
|
|
70
|
+
# DataLogManager.stop() # ensure its fully stopped
|
|
71
|
+
if os.path.exists(fullPath):
|
|
72
|
+
print("[WPILogWriter] File exists, overwriting")
|
|
73
|
+
os.remove(fullPath)
|
|
74
|
+
DataLogManager.stop()
|
|
75
|
+
DataLogManager.start(self.folder, self.filename)
|
|
76
|
+
print(DataLogManager.getLogDir())
|
|
77
|
+
DataLogManager.logNetworkTables(False)
|
|
78
|
+
self.log = DataLogManager.getLog()
|
|
79
|
+
# self.log = DataLogWriter(fullPath, wpilogconstants.extraHeader)
|
|
80
|
+
|
|
81
|
+
self.isOpen = True
|
|
82
|
+
self.timestampId = self.log.start(
|
|
83
|
+
self.timestampKey,
|
|
84
|
+
LogValue.LoggableType.Integer.getWPILOGType(),
|
|
85
|
+
wpilogconstants.entryMetadata,
|
|
86
|
+
0,
|
|
87
|
+
)
|
|
88
|
+
self.lastTable = LogTable(0)
|
|
89
|
+
|
|
90
|
+
self.entryIds: dict[str, int] = {}
|
|
91
|
+
self.entryTypes: dict[str, LogValue.LoggableType] = {}
|
|
92
|
+
self.entryUnits: dict[str, str] = {}
|
|
93
|
+
self.logDate = None
|
|
94
|
+
self.logMatchText = f"pykit_{self.randomIdentifier}"
|
|
95
|
+
|
|
96
|
+
def end(self):
|
|
97
|
+
print("[WPILogWriter] Shutting down")
|
|
98
|
+
self.log.flush()
|
|
99
|
+
self.log.stop()
|
|
100
|
+
|
|
101
|
+
if RobotBase.isSimulation() and Logger.isReplay():
|
|
102
|
+
# open ascope
|
|
103
|
+
fullpath = os.path.join(gettempdir(), ASCOPE_FILENAME)
|
|
104
|
+
if not os.path.exists(gettempdir()):
|
|
105
|
+
return
|
|
106
|
+
fullLogPath = os.path.abspath(os.path.join(self.folder, self.filename))
|
|
107
|
+
print(f"Sending {fullLogPath} to AScope")
|
|
108
|
+
with open(fullpath, "w", encoding="utf-8") as f:
|
|
109
|
+
f.write(fullLogPath)
|
|
110
|
+
|
|
111
|
+
# DataLogManager.stop()
|
|
112
|
+
|
|
113
|
+
def putTable(self, table: LogTable):
|
|
114
|
+
if not self.isOpen:
|
|
115
|
+
return
|
|
116
|
+
if self.autoRename:
|
|
117
|
+
# rename log if necessary
|
|
118
|
+
if self.logDate is None:
|
|
119
|
+
if (
|
|
120
|
+
table.get("DriverStation/DSAttached", False)
|
|
121
|
+
and table.get("SystemStats/SystemTimeValid", False)
|
|
122
|
+
) or RobotBase.isSimulation():
|
|
123
|
+
if self.dsAttachedTime == 0:
|
|
124
|
+
self.dsAttachedTime = RobotController.getFPGATime() / 1e6
|
|
125
|
+
elif (
|
|
126
|
+
RobotController.getFPGATime() / 1e6 - self.dsAttachedTime
|
|
127
|
+
) > 5 or RobotBase.isSimulation():
|
|
128
|
+
self.logDate = datetime.datetime.now()
|
|
129
|
+
else:
|
|
130
|
+
self.dsAttachedTime = 0
|
|
131
|
+
|
|
132
|
+
matchType: MatchType
|
|
133
|
+
match table.get("DriverStation/MatchType", 0):
|
|
134
|
+
case 1:
|
|
135
|
+
matchType = MatchType.practice
|
|
136
|
+
case 2:
|
|
137
|
+
matchType = MatchType.qualification
|
|
138
|
+
case 3:
|
|
139
|
+
matchType = MatchType.elimination
|
|
140
|
+
case _:
|
|
141
|
+
matchType = MatchType.none
|
|
142
|
+
|
|
143
|
+
if self.logMatchText == "" and matchType != MatchType.none:
|
|
144
|
+
match matchType:
|
|
145
|
+
case MatchType.practice:
|
|
146
|
+
self.logMatchText = "p"
|
|
147
|
+
case MatchType.qualification:
|
|
148
|
+
self.logMatchText = "q"
|
|
149
|
+
case MatchType.elimination:
|
|
150
|
+
self.logMatchText = "e"
|
|
151
|
+
case _:
|
|
152
|
+
self.logMatchText = "u"
|
|
153
|
+
self.logMatchText += str(table.get("DriverStation/MatchNumber", 0))
|
|
154
|
+
|
|
155
|
+
# update filename
|
|
156
|
+
filename = "pykit_"
|
|
157
|
+
if self.logDate is not None:
|
|
158
|
+
filename += self.logDate.strftime("%Y%m%d_%H%M%S")
|
|
159
|
+
else:
|
|
160
|
+
filename += self.randomIdentifier
|
|
161
|
+
eventName = (
|
|
162
|
+
table.get("DriverStation/EventName", "").lower().replace(" ", "_")
|
|
163
|
+
)
|
|
164
|
+
if eventName != "":
|
|
165
|
+
filename += f"_{eventName}"
|
|
166
|
+
if self.logMatchText != "":
|
|
167
|
+
filename += f"_{self.logMatchText}"
|
|
168
|
+
filename += ".wpilog"
|
|
169
|
+
if self.filename != filename:
|
|
170
|
+
print(f"[WPILogWriter] Renaming log to {filename}")
|
|
171
|
+
# DataLogManager.stop()
|
|
172
|
+
# self.log.stop()
|
|
173
|
+
self.log.stop()
|
|
174
|
+
fullPath = os.path.join(self.folder, self.filename)
|
|
175
|
+
if os.path.exists(fullPath):
|
|
176
|
+
print(f"[WPILogWriter] Old file removed ({self.filename})")
|
|
177
|
+
os.remove(fullPath)
|
|
178
|
+
|
|
179
|
+
# DataLogManager.logNetworkTables(False)
|
|
180
|
+
DataLogManager.stop()
|
|
181
|
+
DataLogManager.start(self.folder, filename)
|
|
182
|
+
self.log = DataLogManager.getLog()
|
|
183
|
+
# self.log = DataLogWriter(fullPath)
|
|
184
|
+
# self.log._startFile()
|
|
185
|
+
self.timestampId = self.log.start(
|
|
186
|
+
"/Timestamp",
|
|
187
|
+
LogValue.LoggableType.Integer.getWPILOGType(),
|
|
188
|
+
wpilogconstants.entryMetadata,
|
|
189
|
+
0,
|
|
190
|
+
)
|
|
191
|
+
self.filename = filename
|
|
192
|
+
|
|
193
|
+
# write timestamp
|
|
194
|
+
self.log.appendInteger(
|
|
195
|
+
self.timestampId, table.getTimestamp(), table.getTimestamp()
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# get new and old data
|
|
199
|
+
newMap = table.getAll()
|
|
200
|
+
oldMap = self.lastTable.getAll()
|
|
201
|
+
|
|
202
|
+
# encode fields
|
|
203
|
+
for key, newValue in newMap.items():
|
|
204
|
+
fieldType = newValue.log_type
|
|
205
|
+
appendData = False
|
|
206
|
+
|
|
207
|
+
if key not in self.entryIds: # new field
|
|
208
|
+
entryId = self.log.start(
|
|
209
|
+
key,
|
|
210
|
+
newValue.getWPILOGType(),
|
|
211
|
+
wpilogconstants.entryMetadata,
|
|
212
|
+
table.getTimestamp(),
|
|
213
|
+
)
|
|
214
|
+
self.entryIds[key] = entryId
|
|
215
|
+
self.entryTypes[key] = newValue.log_type
|
|
216
|
+
self.entryUnits[key] = ""
|
|
217
|
+
|
|
218
|
+
appendData = True
|
|
219
|
+
elif newValue != oldMap.get(key): # existing field changed
|
|
220
|
+
appendData = True
|
|
221
|
+
|
|
222
|
+
# check if type changed
|
|
223
|
+
elif newValue.log_type != self.entryTypes[key]:
|
|
224
|
+
print(
|
|
225
|
+
f"[WPILOGWriter] Type of {key} changed from "
|
|
226
|
+
f"{self.entryTypes[key]} to {newValue.log_type}, skipping log"
|
|
227
|
+
)
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
if appendData:
|
|
231
|
+
entryId = self.entryIds[key]
|
|
232
|
+
match fieldType:
|
|
233
|
+
case LogValue.LoggableType.Raw:
|
|
234
|
+
self.log.appendRaw(
|
|
235
|
+
entryId, newValue.value, table.getTimestamp()
|
|
236
|
+
)
|
|
237
|
+
case LogValue.LoggableType.Boolean:
|
|
238
|
+
self.log.appendBoolean(
|
|
239
|
+
entryId, newValue.value, table.getTimestamp()
|
|
240
|
+
)
|
|
241
|
+
case LogValue.LoggableType.Integer:
|
|
242
|
+
self.log.appendInteger(
|
|
243
|
+
entryId, newValue.value, table.getTimestamp()
|
|
244
|
+
)
|
|
245
|
+
case LogValue.LoggableType.Float:
|
|
246
|
+
self.log.appendFloat(
|
|
247
|
+
entryId, newValue.value, table.getTimestamp()
|
|
248
|
+
)
|
|
249
|
+
case LogValue.LoggableType.Double:
|
|
250
|
+
self.log.appendDouble(
|
|
251
|
+
entryId, newValue.value, table.getTimestamp()
|
|
252
|
+
)
|
|
253
|
+
case LogValue.LoggableType.String:
|
|
254
|
+
self.log.appendString(
|
|
255
|
+
entryId, newValue.value, table.getTimestamp()
|
|
256
|
+
)
|
|
257
|
+
case LogValue.LoggableType.BooleanArray:
|
|
258
|
+
self.log.appendBooleanArray(
|
|
259
|
+
entryId, newValue.value, table.getTimestamp()
|
|
260
|
+
)
|
|
261
|
+
case LogValue.LoggableType.IntegerArray:
|
|
262
|
+
self.log.appendIntegerArray(
|
|
263
|
+
entryId, newValue.value, table.getTimestamp()
|
|
264
|
+
)
|
|
265
|
+
case LogValue.LoggableType.FloatArray:
|
|
266
|
+
self.log.appendFloatArray(
|
|
267
|
+
entryId, newValue.value, table.getTimestamp()
|
|
268
|
+
)
|
|
269
|
+
case LogValue.LoggableType.DoubleArray:
|
|
270
|
+
self.log.appendDoubleArray(
|
|
271
|
+
entryId, newValue.value, table.getTimestamp()
|
|
272
|
+
)
|
|
273
|
+
case LogValue.LoggableType.StringArray:
|
|
274
|
+
self.log.appendStringArray(
|
|
275
|
+
entryId, newValue.value, table.getTimestamp()
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
self.log.flush()
|
|
279
|
+
self.lastTable = table
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robotpy-pykit
|
|
3
|
+
Version: 0.1.3b1
|
|
4
|
+
Summary: A logging, telemetry, and replay framework for FRC robots running python
|
|
5
|
+
Author-email: Luke Maxwell <luke@whsrobotics.org>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/1757WestwoodRobotics/PyKit
|
|
8
|
+
Keywords: pykit,frc,robotics,logging,telemetry,replay
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: pyntcore>=2025.3.2.2
|
|
13
|
+
Requires-Dist: robotpy-wpimath>=2025.3.2.2
|
|
14
|
+
Requires-Dist: robotpy-wpiutil>=2025.3.2.2
|
|
15
|
+
Requires-Dist: wpilib>=2025.3.2.2
|
|
16
|
+
Requires-Dist: robotpy-hal>=2025.3.2.2
|
|
17
|
+
Requires-Dist: pyfrc>=2025.1.0; platform_machine == "x86_64"
|
|
18
|
+
Requires-Dist: watchdog>=6.0.0; platform_machine == "x86_64"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# PyKit
|
|
22
|
+
|
|
23
|
+
PyKit is a Pure-Python logging, telementary, and replay framework developed by [Team 1757](https://whsrobotics.org) for use on Python robots. PyKit enables replaying log files to re-simulate robot code based on real data.
|
|
24
|
+
|
|
25
|
+
See also
|
|
26
|
+
- [AdvantageKit](https://github.com/Mechanical-Advantage/AdvantageKit/) the inspiration project, in Java
|
|
27
|
+
- [WPILib Data Logging](https://docs.wpilib.org/en/stable/docs/software/telemetry/datalog.html) a simpler logging system included in WPILib
|
|
28
|
+
|
|
29
|
+
Documentation and example projects coming soon!
|
|
30
|
+
|
|
31
|
+
Feedback, feature requests, and bug reports are welcome on the [issues](https://github.com/1757WestwoodRobotics/PyKit/issues) page. For non-public inquiries, please send a message to contact@whsrobotics.org.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
pykit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pykit/autolog.py,sha256=y98ei8QXh0Ig5F5UxgKRglH17J3kleqkl6B1PVPCE00,12413
|
|
3
|
+
pykit/logdatareciever.py,sha256=n0Xbx93gVzAhVU7lTjXe8LjHc4jeeEgGdQJi3BJw0ME,221
|
|
4
|
+
pykit/loggedrobot.py,sha256=MYIoXgEi7LhiGqfgh4KRvSO4SdpmWG7SIl_cA4VIjmM,2609
|
|
5
|
+
pykit/logger.py,sha256=Qgs5rEIVG80dQI1tOc-KLm5iwQ41lPv4_4BYzD1dyu4,7367
|
|
6
|
+
pykit/logreplaysource.py,sha256=w-HqCuUqjv77FXyRmsJg1K9FbKd_KNvICJ_ayXECMXI,349
|
|
7
|
+
pykit/logtable.py,sha256=SHFkRo1bJ790gw1GdhBqfR5j0qbuFmP4uFtJRZKCoRg,9448
|
|
8
|
+
pykit/logvalue.py,sha256=ic5N3oxI5jmD75fOcyNTsErYp3JDhAI_DdqN_1E6sPs,4429
|
|
9
|
+
pykit/inputs/loggableds.py,sha256=KsOtSOkvShdw24Scxaq1nx7MCjltz7-0tuM5xXABxQY,5011
|
|
10
|
+
pykit/networktables/loggeddashboardchooser.py,sha256=GW3dH0PS1BJJ8K5UXhp7q9Yk9DtHo1YGq2tbjByRrS4,1668
|
|
11
|
+
pykit/networktables/loggednetworkinput.py,sha256=t5uH8B8cLq_9NtdbYQVa8OO0alhbJxUMwd8i0sVmwho,272
|
|
12
|
+
pykit/networktables/nt4Publisher.py,sha256=D9DSoWU9Nrwp7xpFvx7yltBSblX4DhIjI29tkUEBzUk,2860
|
|
13
|
+
pykit/watch/cli_replaywatch.py,sha256=hpiIndevBZsY1S9aDx4TAFxb7PK3mxcsNyea7FbF8g4,3244
|
|
14
|
+
pykit/wpilog/wpilogconstants.py,sha256=BdhugV6UC4ShP5s7lG8CO95Txc5qlVmxTOxyjyhpmfo,60
|
|
15
|
+
pykit/wpilog/wpilogreader.py,sha256=6wtO79aBCoP8M7kzuABrkX0eX6kN8I0zWC1q7E72dQY,7646
|
|
16
|
+
pykit/wpilog/wpilogwriter.py,sha256=WaRQMu8ob2sqrGPmUdLqb7C6pA9NswkvNaJm4x7DdWo,10772
|
|
17
|
+
robotpy_pykit-0.1.3b1.dist-info/licenses/LICENSE,sha256=QEEUUO4dKcmhX9pW9BP4WMEMWqY-jhnmwdPi-Oo-S9w,1554
|
|
18
|
+
robotpy_pykit-0.1.3b1.dist-info/METADATA,sha256=Sp1K_xWrdOOlhTFLrYzwpk_PUHR8mwKL8eHAryB1Tu4,1517
|
|
19
|
+
robotpy_pykit-0.1.3b1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
+
robotpy_pykit-0.1.3b1.dist-info/entry_points.txt,sha256=fLvOR7Epp2Wx9epr_YMtEbQoSMiHfxozV5QsiraA_Os,63
|
|
21
|
+
robotpy_pykit-0.1.3b1.dist-info/top_level.txt,sha256=tJQlZyFURJ0FEmLtjr4t5CR3lrZKIFEgdw_xxabFeeY,6
|
|
22
|
+
robotpy_pykit-0.1.3b1.dist-info/RECORD,,
|