119 lines
3.3 KiB
Python
119 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
DEFAULT_CHANNELS = (
|
|
"turn",
|
|
"pulse",
|
|
"gp0_falling",
|
|
"gp1_falling",
|
|
)
|
|
|
|
|
|
def read_events(path: Path) -> tuple[list[str], list[list[float]]]:
|
|
with path.open(newline="") as file:
|
|
reader = csv.DictReader(file)
|
|
fieldnames = set(reader.fieldnames or ())
|
|
if "time_us" not in fieldnames:
|
|
raise ValueError("CSV is missing column: time_us")
|
|
|
|
channels = [channel for channel in DEFAULT_CHANNELS if channel in fieldnames]
|
|
if not channels:
|
|
raise ValueError("CSV has no plottable event columns")
|
|
|
|
events = [[] for _ in channels]
|
|
|
|
for row in reader:
|
|
time_s = int(row["time_us"]) / 1_000_000
|
|
for index, channel in enumerate(channels):
|
|
if int(row[channel]):
|
|
events[index].append(time_s)
|
|
|
|
return channels, events
|
|
|
|
|
|
def iter_csv_paths(path: Path) -> list[Path]:
|
|
if path.is_dir():
|
|
return sorted(path.rglob("*.csv"))
|
|
return [path]
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Plot ESP32 event CSV files.")
|
|
parser.add_argument("csv_path", type=Path, help="CSV file or directory of CSV files.")
|
|
parser.add_argument("-o", "--output", type=Path, help="Optional image output path.")
|
|
return parser.parse_args()
|
|
|
|
|
|
def plot_events(csv_path: Path, output: Path | None) -> bool:
|
|
try:
|
|
channels, events = read_events(csv_path)
|
|
except (OSError, ValueError) as exc:
|
|
print(f"Could not read {csv_path}: {exc}", file=sys.stderr)
|
|
return False
|
|
|
|
fig, ax = plt.subplots(figsize=(12, 5))
|
|
ax.eventplot(events, orientation="horizontal", lineoffsets=range(len(channels)))
|
|
ax.set_yticks(range(len(channels)), channels)
|
|
ax.set_xlabel("time (s)")
|
|
ax.set_title(str(csv_path))
|
|
ax.grid(axis="x", alpha=0.25)
|
|
fig.tight_layout()
|
|
|
|
if output is not None:
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
fig.savefig(output, dpi=160)
|
|
print(f"Wrote {output}")
|
|
plt.close(fig)
|
|
else:
|
|
plt.show()
|
|
plt.close(fig)
|
|
|
|
return True
|
|
|
|
|
|
def output_path_for(csv_path: Path, output: Path | None, multiple: bool) -> Path | None:
|
|
if output is None:
|
|
return None
|
|
if not multiple:
|
|
return output
|
|
return output / f"{csv_path.stem}.png"
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
|
|
if not args.csv_path.exists():
|
|
print(f"Path does not exist: {args.csv_path}", file=sys.stderr)
|
|
return 1
|
|
|
|
csv_paths = iter_csv_paths(args.csv_path)
|
|
if not csv_paths:
|
|
print(f"No CSV files found under {args.csv_path}", file=sys.stderr)
|
|
return 1
|
|
|
|
multiple = len(csv_paths) > 1
|
|
if multiple and args.output is not None and args.output.suffix:
|
|
print("When plotting a directory, --output must be a directory.", file=sys.stderr)
|
|
return 1
|
|
|
|
failures = 0
|
|
for csv_path in csv_paths:
|
|
output = output_path_for(csv_path, args.output, multiple)
|
|
if multiple and output is None:
|
|
print(f"Plotting {csv_path}")
|
|
if not plot_events(csv_path, output):
|
|
failures += 1
|
|
|
|
return 1 if failures else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|