amd-debug-tools 0.2.0__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.
Potentially problematic release.
This version of amd-debug-tools might be problematic. Click here for more details.
- amd_debug/__init__.py +45 -0
- amd_debug/acpi.py +107 -0
- amd_debug/bash/amd-s2idle +89 -0
- amd_debug/battery.py +87 -0
- amd_debug/bios.py +138 -0
- amd_debug/common.py +324 -0
- amd_debug/database.py +331 -0
- amd_debug/failures.py +588 -0
- amd_debug/installer.py +404 -0
- amd_debug/kernel.py +389 -0
- amd_debug/prerequisites.py +1215 -0
- amd_debug/pstate.py +314 -0
- amd_debug/s2idle-hook +72 -0
- amd_debug/s2idle.py +406 -0
- amd_debug/sleep_report.py +453 -0
- amd_debug/templates/html +427 -0
- amd_debug/templates/md +39 -0
- amd_debug/templates/stdout +13 -0
- amd_debug/templates/txt +23 -0
- amd_debug/validator.py +863 -0
- amd_debug/wake.py +111 -0
- amd_debug_tools-0.2.0.dist-info/METADATA +180 -0
- amd_debug_tools-0.2.0.dist-info/RECORD +27 -0
- amd_debug_tools-0.2.0.dist-info/WHEEL +5 -0
- amd_debug_tools-0.2.0.dist-info/entry_points.txt +4 -0
- amd_debug_tools-0.2.0.dist-info/licenses/LICENSE +19 -0
- amd_debug_tools-0.2.0.dist-info/top_level.txt +1 -0
amd_debug/common.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
This module contains common utility functions and classes for various amd-debug-tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.metadata
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import time
|
|
13
|
+
import struct
|
|
14
|
+
import subprocess
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import date, timedelta
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Colors:
|
|
21
|
+
"""Colors for the terminal"""
|
|
22
|
+
|
|
23
|
+
DEBUG = "\033[90m"
|
|
24
|
+
HEADER = "\033[95m"
|
|
25
|
+
OK = "\033[94m"
|
|
26
|
+
WARNING = "\033[32m"
|
|
27
|
+
FAIL = "\033[91m"
|
|
28
|
+
ENDC = "\033[0m"
|
|
29
|
+
UNDERLINE = "\033[4m"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read_file(fn) -> str:
|
|
33
|
+
"""Read a file and return the contents"""
|
|
34
|
+
with open(fn, "r", encoding="utf-8") as r:
|
|
35
|
+
return r.read().strip()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def compare_file(fn, expect) -> bool:
|
|
39
|
+
"""Compare a file to an expected string"""
|
|
40
|
+
return read_file(fn) == expect
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_group_color(group) -> str:
|
|
44
|
+
"""Get the color for a group"""
|
|
45
|
+
if group == "🚦":
|
|
46
|
+
color = Colors.WARNING
|
|
47
|
+
elif group == "🗣️":
|
|
48
|
+
color = Colors.HEADER
|
|
49
|
+
elif group == "💯":
|
|
50
|
+
color = Colors.UNDERLINE
|
|
51
|
+
elif any(mk in group for mk in ["🦟", "🖴"]):
|
|
52
|
+
color = Colors.DEBUG
|
|
53
|
+
elif any(mk in group for mk in ["❌", "👀"]):
|
|
54
|
+
color = Colors.FAIL
|
|
55
|
+
elif any(mk in group for mk in ["✅", "🔋", "🐧", "💻", "○", "💤", "🥱"]):
|
|
56
|
+
color = Colors.OK
|
|
57
|
+
else:
|
|
58
|
+
color = group
|
|
59
|
+
return color
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def print_color(message, group) -> None:
|
|
63
|
+
"""Print a message with a color"""
|
|
64
|
+
prefix = f"{group} "
|
|
65
|
+
suffix = Colors.ENDC
|
|
66
|
+
color = get_group_color(group)
|
|
67
|
+
if color == group:
|
|
68
|
+
prefix = ""
|
|
69
|
+
log_txt = f"{prefix}{message}".strip()
|
|
70
|
+
if any(c in color for c in [Colors.OK, Colors.HEADER, Colors.UNDERLINE]):
|
|
71
|
+
logging.info(log_txt)
|
|
72
|
+
elif color == Colors.WARNING:
|
|
73
|
+
logging.warning(log_txt)
|
|
74
|
+
elif color == Colors.FAIL:
|
|
75
|
+
logging.error(log_txt)
|
|
76
|
+
else:
|
|
77
|
+
logging.debug(log_txt)
|
|
78
|
+
if "TERM" in os.environ and os.environ["TERM"] == "dumb":
|
|
79
|
+
suffix = ""
|
|
80
|
+
color = ""
|
|
81
|
+
print(f"{prefix}{color}{message}{suffix}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def fatal_error(message):
|
|
85
|
+
"""Prints a fatal error message and exits"""
|
|
86
|
+
_configure_log(None)
|
|
87
|
+
print_color(message, "👀")
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def show_log_info():
|
|
92
|
+
"""Show log information"""
|
|
93
|
+
logger = logging.getLogger()
|
|
94
|
+
for handler in logger.handlers:
|
|
95
|
+
if isinstance(handler, logging.FileHandler):
|
|
96
|
+
filename = handler.baseFilename
|
|
97
|
+
if filename != "/dev/null":
|
|
98
|
+
print(f"Debug logs are saved to: {filename}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _configure_log(prefix) -> str:
|
|
102
|
+
"""Configure logging for the tool"""
|
|
103
|
+
if len(logging.root.handlers) > 0:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if prefix:
|
|
107
|
+
user = os.environ.get("SUDO_USER")
|
|
108
|
+
home = os.path.expanduser(f"~{user if user else ''}")
|
|
109
|
+
path = os.environ.get("XDG_DATA_HOME") or os.path.join(
|
|
110
|
+
home, ".local", "share", "amd-debug-tools"
|
|
111
|
+
)
|
|
112
|
+
os.makedirs(path, exist_ok=True)
|
|
113
|
+
log = os.path.join(
|
|
114
|
+
path,
|
|
115
|
+
f"{prefix}-{date.today()}.txt",
|
|
116
|
+
)
|
|
117
|
+
if not os.path.exists(log):
|
|
118
|
+
with open(log, "w", encoding="utf-8") as f:
|
|
119
|
+
f.write("")
|
|
120
|
+
if "SUDO_UID" in os.environ:
|
|
121
|
+
os.chown(path, int(os.environ["SUDO_UID"]), int(os.environ["SUDO_GID"]))
|
|
122
|
+
os.chown(log, int(os.environ["SUDO_UID"]), int(os.environ["SUDO_GID"]))
|
|
123
|
+
level = logging.DEBUG
|
|
124
|
+
else:
|
|
125
|
+
log = "/dev/null"
|
|
126
|
+
level = logging.WARNING
|
|
127
|
+
# for saving a log file for analysis
|
|
128
|
+
logging.basicConfig(
|
|
129
|
+
format="%(asctime)s %(levelname)s:\t%(message)s",
|
|
130
|
+
filename=log,
|
|
131
|
+
level=level,
|
|
132
|
+
)
|
|
133
|
+
return log
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def check_lockdown():
|
|
137
|
+
"""Check if the system is in lockdown"""
|
|
138
|
+
fn = os.path.join("/", "sys", "kernel", "security", "lockdown")
|
|
139
|
+
if not os.path.exists(fn):
|
|
140
|
+
return False
|
|
141
|
+
lockdown = read_file(fn)
|
|
142
|
+
if lockdown.split()[0] != "[none]":
|
|
143
|
+
return lockdown
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def print_temporary_message(msg) -> int:
|
|
148
|
+
"""Print a temporary message to the console"""
|
|
149
|
+
print(msg, end="\r", flush=True)
|
|
150
|
+
return len(msg)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def clear_temporary_message(msg_len) -> None:
|
|
154
|
+
"""Clear a temporary message from the console"""
|
|
155
|
+
print(" " * msg_len, end="\r")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def run_countdown(action, t) -> bool:
|
|
159
|
+
"""Run a countdown timer"""
|
|
160
|
+
msg = ""
|
|
161
|
+
if t < 0:
|
|
162
|
+
return False
|
|
163
|
+
if t == 0:
|
|
164
|
+
return True
|
|
165
|
+
while t > 0:
|
|
166
|
+
msg = f"{action} in {timedelta(seconds=t)}"
|
|
167
|
+
print_temporary_message(msg)
|
|
168
|
+
time.sleep(1)
|
|
169
|
+
t -= 1
|
|
170
|
+
clear_temporary_message(len(msg))
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_distro() -> str:
|
|
175
|
+
"""Get the distribution name"""
|
|
176
|
+
distro = "unknown"
|
|
177
|
+
if os.path.exists("/etc/os-release"):
|
|
178
|
+
with open("/etc/os-release", "r", encoding="utf-8") as f:
|
|
179
|
+
for line in f:
|
|
180
|
+
if line.startswith("ID="):
|
|
181
|
+
return line.split("=")[1].strip().strip('"')
|
|
182
|
+
if os.path.exists("/etc/arch-release"):
|
|
183
|
+
return "arch"
|
|
184
|
+
elif os.path.exists("/etc/fedora-release"):
|
|
185
|
+
return "fedora"
|
|
186
|
+
elif os.path.exists("/etc/debian_version"):
|
|
187
|
+
return "debian"
|
|
188
|
+
|
|
189
|
+
return distro
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_pretty_distro() -> str:
|
|
193
|
+
"""Get the pretty distribution name"""
|
|
194
|
+
distro = "Unknown"
|
|
195
|
+
if os.path.exists("/etc/os-release"):
|
|
196
|
+
with open("/etc/os-release", "r", encoding="utf-8") as f:
|
|
197
|
+
for line in f:
|
|
198
|
+
if line.startswith("PRETTY_NAME="):
|
|
199
|
+
distro = line.split("=")[1].strip().strip('"')
|
|
200
|
+
break
|
|
201
|
+
return distro
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def is_root() -> bool:
|
|
205
|
+
"""Check if the user is root"""
|
|
206
|
+
return os.geteuid() == 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def BIT(num): # pylint: disable=invalid-name
|
|
210
|
+
"""Return a bit shifted value"""
|
|
211
|
+
return 1 << num
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_log_priority(num):
|
|
215
|
+
"""Maps an integer debug level to a priority type"""
|
|
216
|
+
if num:
|
|
217
|
+
try:
|
|
218
|
+
num = int(num)
|
|
219
|
+
except ValueError:
|
|
220
|
+
return num
|
|
221
|
+
if num == 7:
|
|
222
|
+
return "🦟"
|
|
223
|
+
elif num == 4:
|
|
224
|
+
return "🚦"
|
|
225
|
+
elif num <= 3:
|
|
226
|
+
return "❌"
|
|
227
|
+
return "○"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def minimum_kernel(major, minor) -> bool:
|
|
231
|
+
"""Checks if the kernel version is at least major.minor"""
|
|
232
|
+
ver = platform.uname().release.split(".")
|
|
233
|
+
kmajor = int(ver[0])
|
|
234
|
+
kminor = int(ver[1])
|
|
235
|
+
if kmajor > int(major):
|
|
236
|
+
return True
|
|
237
|
+
if kmajor < int(major):
|
|
238
|
+
return False
|
|
239
|
+
return kminor >= int(minor)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def systemd_in_use() -> bool:
|
|
243
|
+
"""Check if systemd is in use"""
|
|
244
|
+
# Check if /proc/1/comm exists and read its contents
|
|
245
|
+
init_daemon = read_file("/proc/1/comm")
|
|
246
|
+
return init_daemon == "systemd"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def get_property_pyudev(properties, key, fallback=""):
|
|
250
|
+
"""Get a property from a udev device"""
|
|
251
|
+
try:
|
|
252
|
+
return properties.get(key, fallback)
|
|
253
|
+
except UnicodeDecodeError:
|
|
254
|
+
return ""
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def read_msr(msr, cpu):
|
|
258
|
+
"""Read a Model-Specific Register (MSR) value from the CPU."""
|
|
259
|
+
p = f"/dev/cpu/{cpu}/msr"
|
|
260
|
+
if not os.path.exists(p) and is_root():
|
|
261
|
+
os.system("modprobe msr")
|
|
262
|
+
try:
|
|
263
|
+
f = os.open(p, os.O_RDONLY)
|
|
264
|
+
except OSError as exc:
|
|
265
|
+
raise PermissionError from exc
|
|
266
|
+
try:
|
|
267
|
+
os.lseek(f, msr, os.SEEK_SET)
|
|
268
|
+
val = struct.unpack("Q", os.read(f, 8))[0]
|
|
269
|
+
except OSError as exc:
|
|
270
|
+
raise PermissionError from exc
|
|
271
|
+
finally:
|
|
272
|
+
os.close(f)
|
|
273
|
+
return val
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def relaunch_sudo() -> None:
|
|
277
|
+
"""Relaunch the script with sudo if not already running as root"""
|
|
278
|
+
if not is_root():
|
|
279
|
+
logging.debug("Relaunching with sudo")
|
|
280
|
+
os.execvp("sudo", ["sudo", "-E"] + sys.argv)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def running_ssh():
|
|
284
|
+
return "SSH_CLIENT" in os.environ or "SSH_TTY" in os.environ
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _git_describe() -> str:
|
|
288
|
+
"""Get the git description of the current commit"""
|
|
289
|
+
try:
|
|
290
|
+
result = subprocess.check_output(
|
|
291
|
+
["git", "log", "-1", '--format=commit %h ("%s")'],
|
|
292
|
+
cwd=os.path.dirname(__file__),
|
|
293
|
+
text=True,
|
|
294
|
+
stderr=subprocess.DEVNULL,
|
|
295
|
+
)
|
|
296
|
+
return result.strip()
|
|
297
|
+
except subprocess.CalledProcessError:
|
|
298
|
+
return None
|
|
299
|
+
except FileNotFoundError:
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def version() -> str:
|
|
304
|
+
"""Get version of the tool"""
|
|
305
|
+
ver = "unknown"
|
|
306
|
+
try:
|
|
307
|
+
ver = importlib.metadata.version("amd-debug-tools")
|
|
308
|
+
except importlib.metadata.PackageNotFoundError:
|
|
309
|
+
pass
|
|
310
|
+
describe = _git_describe()
|
|
311
|
+
if describe:
|
|
312
|
+
ver = f"{ver} [{describe}]"
|
|
313
|
+
return ver
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class AmdTool:
|
|
317
|
+
"""Base class for AMD tools"""
|
|
318
|
+
|
|
319
|
+
def __init__(self, prefix):
|
|
320
|
+
self.log = _configure_log(prefix)
|
|
321
|
+
logging.debug("command: %s (module: %s)", sys.argv, type(self).__name__)
|
|
322
|
+
logging.debug("Version: %s", version())
|
|
323
|
+
if os.uname().sysname != "Linux":
|
|
324
|
+
raise RuntimeError("This tool only runs on Linux")
|
amd_debug/database.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
import sqlite3
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from amd_debug.common import read_file
|
|
9
|
+
|
|
10
|
+
SCHEMA_VERSION = 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def migrate(cur, user_version) -> None:
|
|
14
|
+
"""Migrate sqlite database schema"""
|
|
15
|
+
cur.execute("PRAGMA user_version")
|
|
16
|
+
val = cur.fetchone()[0]
|
|
17
|
+
# Schema 1
|
|
18
|
+
# - add priority column
|
|
19
|
+
if val == 0:
|
|
20
|
+
cur.execute("ALTER TABLE debug ADD COLUMN priority INTEGER")
|
|
21
|
+
# Update schema if necessary
|
|
22
|
+
if val != user_version:
|
|
23
|
+
cur.execute(f"PRAGMA user_version = {user_version}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SleepDatabase:
|
|
27
|
+
"""Database class to store sleep cycle data"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, dbf=None) -> None:
|
|
30
|
+
self.db = None
|
|
31
|
+
self.last_suspend = None
|
|
32
|
+
self.cycle_data_cnt = 0
|
|
33
|
+
self.debug_cnt = 0
|
|
34
|
+
|
|
35
|
+
if not dbf:
|
|
36
|
+
# if we were packaged we would have a directory in /var/lib
|
|
37
|
+
path = os.path.join("/", "var", "lib", "amd-s2idle")
|
|
38
|
+
if not os.path.exists(path):
|
|
39
|
+
path = os.path.join("/", "var", "local", "lib", "amd-s2idle")
|
|
40
|
+
os.makedirs(path, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
dbf = os.path.join(path, "data.db")
|
|
43
|
+
new = not os.path.exists(dbf)
|
|
44
|
+
self.db = sqlite3.connect(dbf)
|
|
45
|
+
cur = self.db.cursor()
|
|
46
|
+
cur.execute(
|
|
47
|
+
"CREATE TABLE IF NOT EXISTS prereq_data ("
|
|
48
|
+
"t0 INTEGER,"
|
|
49
|
+
"id INTEGER,"
|
|
50
|
+
"message TEXT,"
|
|
51
|
+
"symbol TEXT,"
|
|
52
|
+
"PRIMARY KEY(t0, id))"
|
|
53
|
+
)
|
|
54
|
+
cur.execute(
|
|
55
|
+
"CREATE TABLE IF NOT EXISTS debug ("
|
|
56
|
+
"t0 INTEGER,"
|
|
57
|
+
"id INTEGER,"
|
|
58
|
+
"message TEXT,"
|
|
59
|
+
"priority INTEGER,"
|
|
60
|
+
"PRIMARY KEY(t0, id))"
|
|
61
|
+
)
|
|
62
|
+
cur.execute(
|
|
63
|
+
"CREATE TABLE IF NOT EXISTS cycle ("
|
|
64
|
+
"t0 INTEGER PRIMARY KEY,"
|
|
65
|
+
"t1 INTEGER,"
|
|
66
|
+
"requested INTEGER,"
|
|
67
|
+
"gpio TEXT,"
|
|
68
|
+
"wake_irq TEXT,"
|
|
69
|
+
"kernel REAL,"
|
|
70
|
+
"hw REAL)"
|
|
71
|
+
)
|
|
72
|
+
cur.execute(
|
|
73
|
+
"CREATE TABLE IF NOT EXISTS cycle_data ("
|
|
74
|
+
"t0 INTEGER,"
|
|
75
|
+
"id INTEGER,"
|
|
76
|
+
"message TEXT,"
|
|
77
|
+
"symbol TEXT,"
|
|
78
|
+
"PRIMARY KEY(t0, id))"
|
|
79
|
+
)
|
|
80
|
+
cur.execute(
|
|
81
|
+
"CREATE TABLE IF NOT EXISTS battery ("
|
|
82
|
+
"t0 INTEGER PRIMARY KEY,"
|
|
83
|
+
"name TEXT,"
|
|
84
|
+
"b0 INTEGER,"
|
|
85
|
+
"b1 INTEGER,"
|
|
86
|
+
"full INTEGER,"
|
|
87
|
+
"unit TEXT)"
|
|
88
|
+
)
|
|
89
|
+
self.prereq_data_cnt = 0
|
|
90
|
+
|
|
91
|
+
if new:
|
|
92
|
+
cur.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
|
|
93
|
+
else:
|
|
94
|
+
migrate(cur, SCHEMA_VERSION)
|
|
95
|
+
|
|
96
|
+
def __del__(self) -> None:
|
|
97
|
+
if self.db:
|
|
98
|
+
self.db.close()
|
|
99
|
+
|
|
100
|
+
def start_cycle(self, timestamp):
|
|
101
|
+
"""Start a new sleep cycle"""
|
|
102
|
+
self.last_suspend = timestamp
|
|
103
|
+
|
|
104
|
+
# increment the counters so that systemd hooks work
|
|
105
|
+
cur = self.db.cursor()
|
|
106
|
+
cur.execute(
|
|
107
|
+
"SELECT MAX(id) FROM cycle_data WHERE t0=?",
|
|
108
|
+
(int(self.last_suspend.strftime("%Y%m%d%H%M%S")),),
|
|
109
|
+
)
|
|
110
|
+
val = cur.fetchone()[0]
|
|
111
|
+
if val is not None:
|
|
112
|
+
self.cycle_data_cnt = val + 1
|
|
113
|
+
else:
|
|
114
|
+
self.cycle_data_cnt = 0
|
|
115
|
+
cur.execute(
|
|
116
|
+
"SELECT MAX(id) FROM debug WHERE t0=?",
|
|
117
|
+
(int(self.last_suspend.strftime("%Y%m%d%H%M%S")),),
|
|
118
|
+
)
|
|
119
|
+
val = cur.fetchone()[0]
|
|
120
|
+
if val is not None:
|
|
121
|
+
self.debug_cnt = val + 1
|
|
122
|
+
else:
|
|
123
|
+
self.debug_cnt = 0
|
|
124
|
+
|
|
125
|
+
def sync(self) -> None:
|
|
126
|
+
"""Sync the database to disk"""
|
|
127
|
+
self.db.commit()
|
|
128
|
+
|
|
129
|
+
def record_debug(self, message, level=6) -> None:
|
|
130
|
+
"""Helper function to record a message to debug database"""
|
|
131
|
+
assert self.last_suspend
|
|
132
|
+
cur = self.db.cursor()
|
|
133
|
+
cur.execute(
|
|
134
|
+
"INSERT into debug (t0, id, message, priority) VALUES (?, ?, ?, ?)",
|
|
135
|
+
(
|
|
136
|
+
int(self.last_suspend.strftime("%Y%m%d%H%M%S")),
|
|
137
|
+
self.debug_cnt,
|
|
138
|
+
message,
|
|
139
|
+
level,
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
self.debug_cnt += 1
|
|
143
|
+
|
|
144
|
+
def record_debug_file(self, fn):
|
|
145
|
+
"""Helper function to record the entire contents of a file to debug database"""
|
|
146
|
+
try:
|
|
147
|
+
contents = read_file(fn)
|
|
148
|
+
self.record_debug(contents)
|
|
149
|
+
except PermissionError:
|
|
150
|
+
self.record_debug(f"Unable to capture {fn}")
|
|
151
|
+
|
|
152
|
+
def record_battery_energy(self, name, energy, full, unit):
|
|
153
|
+
"""Helper function to record battery energy"""
|
|
154
|
+
cur = self.db.cursor()
|
|
155
|
+
cur.execute(
|
|
156
|
+
"SELECT * FROM battery WHERE t0=?",
|
|
157
|
+
(int(self.last_suspend.strftime("%Y%m%d%H%M%S")),),
|
|
158
|
+
)
|
|
159
|
+
if cur.fetchone():
|
|
160
|
+
cur.execute(
|
|
161
|
+
"UPDATE battery SET b1=? WHERE t0=?",
|
|
162
|
+
(energy, int(self.last_suspend.strftime("%Y%m%d%H%M%S"))),
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
cur.execute(
|
|
166
|
+
"""
|
|
167
|
+
INSERT into battery (t0, name, b0, b1, full, unit)
|
|
168
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
169
|
+
""",
|
|
170
|
+
(
|
|
171
|
+
int(self.last_suspend.strftime("%Y%m%d%H%M%S")),
|
|
172
|
+
name,
|
|
173
|
+
energy,
|
|
174
|
+
None,
|
|
175
|
+
full,
|
|
176
|
+
unit,
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def record_cycle_data(self, message, symbol) -> None:
|
|
181
|
+
"""Helper function to record a message to cycle_data database"""
|
|
182
|
+
assert self.last_suspend
|
|
183
|
+
cur = self.db.cursor()
|
|
184
|
+
cur.execute(
|
|
185
|
+
"""
|
|
186
|
+
INSERT into cycle_data (t0, id, message, symbol)
|
|
187
|
+
VALUES (?, ?, ?, ?)
|
|
188
|
+
""",
|
|
189
|
+
(
|
|
190
|
+
(
|
|
191
|
+
int(self.last_suspend.strftime("%Y%m%d%H%M%S")),
|
|
192
|
+
self.cycle_data_cnt,
|
|
193
|
+
message,
|
|
194
|
+
symbol,
|
|
195
|
+
)
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
self.cycle_data_cnt += 1
|
|
199
|
+
|
|
200
|
+
def record_cycle(
|
|
201
|
+
self,
|
|
202
|
+
requested_duration=0,
|
|
203
|
+
active_gpios="",
|
|
204
|
+
wakeup_irqs="",
|
|
205
|
+
kernel_duration=0,
|
|
206
|
+
hw_sleep_duration=0,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Helper function to record a sleep cycle into the cycle database"""
|
|
209
|
+
assert self.last_suspend
|
|
210
|
+
cur = self.db.cursor()
|
|
211
|
+
cur.execute(
|
|
212
|
+
"""
|
|
213
|
+
REPLACE INTO cycle (t0, t1, requested, gpio, wake_irq, kernel, hw)
|
|
214
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
215
|
+
""",
|
|
216
|
+
(
|
|
217
|
+
int(self.last_suspend.strftime("%Y%m%d%H%M%S")),
|
|
218
|
+
int(datetime.now().strftime("%Y%m%d%H%M%S")),
|
|
219
|
+
requested_duration,
|
|
220
|
+
str(active_gpios) if active_gpios else "",
|
|
221
|
+
str(wakeup_irqs),
|
|
222
|
+
kernel_duration,
|
|
223
|
+
hw_sleep_duration,
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def record_prereq(self, message, symbol) -> None:
|
|
228
|
+
"""Helper function to record a message to prereq_data database"""
|
|
229
|
+
assert self.last_suspend
|
|
230
|
+
cur = self.db.cursor()
|
|
231
|
+
cur.execute(
|
|
232
|
+
"""
|
|
233
|
+
INSERT into prereq_data (t0, id, message, symbol)
|
|
234
|
+
VALUES (?, ?, ?, ?)
|
|
235
|
+
""",
|
|
236
|
+
(
|
|
237
|
+
(
|
|
238
|
+
int(self.last_suspend.strftime("%Y%m%d%H%M%S")),
|
|
239
|
+
self.prereq_data_cnt,
|
|
240
|
+
message,
|
|
241
|
+
symbol,
|
|
242
|
+
)
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
self.prereq_data_cnt += 1
|
|
246
|
+
|
|
247
|
+
def report_prereq(self, t0) -> list:
|
|
248
|
+
"""Helper function to report the prereq_data database"""
|
|
249
|
+
if t0 is None:
|
|
250
|
+
return []
|
|
251
|
+
cur = self.db.cursor()
|
|
252
|
+
cur.execute(
|
|
253
|
+
"SELECT * FROM prereq_data WHERE t0=?",
|
|
254
|
+
(int(t0.strftime("%Y%m%d%H%M%S")),),
|
|
255
|
+
)
|
|
256
|
+
return cur.fetchall()
|
|
257
|
+
|
|
258
|
+
def report_debug(self, t0) -> str:
|
|
259
|
+
"""Helper function to report the debug database"""
|
|
260
|
+
if t0 is None:
|
|
261
|
+
return ""
|
|
262
|
+
cur = self.db.cursor()
|
|
263
|
+
cur.execute(
|
|
264
|
+
"SELECT message, priority FROM debug WHERE t0=?",
|
|
265
|
+
(int(t0.strftime("%Y%m%d%H%M%S")),),
|
|
266
|
+
)
|
|
267
|
+
return cur.fetchall()
|
|
268
|
+
|
|
269
|
+
def report_cycle(self, t0=None) -> list:
|
|
270
|
+
"""Helper function to report a cycle from database"""
|
|
271
|
+
if t0 is None:
|
|
272
|
+
assert self.last_suspend
|
|
273
|
+
t0 = self.last_suspend
|
|
274
|
+
cur = self.db.cursor()
|
|
275
|
+
cur.execute(
|
|
276
|
+
"SELECT * FROM cycle WHERE t0=?",
|
|
277
|
+
(int(t0.strftime("%Y%m%d%H%M%S")),),
|
|
278
|
+
)
|
|
279
|
+
return cur.fetchall()
|
|
280
|
+
|
|
281
|
+
def report_cycle_data(self, t0=None) -> str:
|
|
282
|
+
"""Helper function to report a table matching a timestamp from cycle_data database"""
|
|
283
|
+
if t0 is None:
|
|
284
|
+
t0 = self.last_suspend
|
|
285
|
+
cur = self.db.cursor()
|
|
286
|
+
cur.execute(
|
|
287
|
+
"SELECT message, symbol FROM cycle_data WHERE t0=? ORDER BY symbol",
|
|
288
|
+
(int(t0.strftime("%Y%m%d%H%M%S")),),
|
|
289
|
+
)
|
|
290
|
+
data = ""
|
|
291
|
+
for row in cur.fetchall():
|
|
292
|
+
data += f"{row[1]} {row[0]}\n"
|
|
293
|
+
return data
|
|
294
|
+
|
|
295
|
+
def report_battery(self, t0=None) -> list:
|
|
296
|
+
"""Helper function to report a line from battery database"""
|
|
297
|
+
if t0 is None:
|
|
298
|
+
t0 = self.last_suspend
|
|
299
|
+
cur = self.db.cursor()
|
|
300
|
+
cur.execute(
|
|
301
|
+
"SELECT * FROM battery WHERE t0=?",
|
|
302
|
+
(int(t0.strftime("%Y%m%d%H%M%S")),),
|
|
303
|
+
)
|
|
304
|
+
return cur.fetchall()
|
|
305
|
+
|
|
306
|
+
def get_last_prereq_ts(self) -> int:
|
|
307
|
+
"""Helper function to report the last line from prereq database"""
|
|
308
|
+
cur = self.db.cursor()
|
|
309
|
+
cur.execute("SELECT * FROM prereq_data ORDER BY t0 DESC LIMIT 1")
|
|
310
|
+
result = cur.fetchone()
|
|
311
|
+
return result[0] if result else None
|
|
312
|
+
|
|
313
|
+
def get_last_cycle(self) -> list:
|
|
314
|
+
"""Helper function to report the last line from battery database"""
|
|
315
|
+
cur = self.db.cursor()
|
|
316
|
+
cur.execute("SELECT t0 FROM cycle ORDER BY t0 DESC LIMIT 1")
|
|
317
|
+
return cur.fetchone()
|
|
318
|
+
|
|
319
|
+
def report_summary_dataframe(self, since, until) -> object:
|
|
320
|
+
"""Helper function to report a dataframe from the database"""
|
|
321
|
+
import pandas as pd # pylint: disable=import-outside-toplevel
|
|
322
|
+
|
|
323
|
+
pd.set_option("display.precision", 2)
|
|
324
|
+
return pd.read_sql_query(
|
|
325
|
+
sql="SELECT cycle.t0, cycle.t1, hw, requested, gpio, wake_irq, b0, b1, full FROM cycle LEFT JOIN battery ON cycle.t0 = battery.t0 WHERE cycle.t0 >= ? and cycle.t0 <= ?",
|
|
326
|
+
con=self.db,
|
|
327
|
+
params=(
|
|
328
|
+
int(since.strftime("%Y%m%d%H%M%S")),
|
|
329
|
+
int(until.strftime("%Y%m%d%H%M%S")),
|
|
330
|
+
),
|
|
331
|
+
)
|