Skip to content

Commit 91fede5

Browse files
committed
Unify impedance CSV output across streaming and non-streaming paths
Both synapsectl query and synapsectl query --stream now write identical impedance CSVs via a shared synapse.cli.impedance_csv module: Peripheral: <name> Electrode ID,Magnitude (Ohms),Phase (degrees),Status <electrode_id>,<magnitude>,<phase>,<status> Changes: - Add a 'Peripheral: <name>' metadata header line, resolved from the device's peripheral list (preferring the query's peripheral_id, else the broadband recording source). - Fix streaming CSV discrepancies: it previously wrote a different header ('Electrode ID,Magnitude,Phase'), omitted units, and had no Status column. It now matches the non-streaming format. - Streaming now also writes failed measurements with Status=0 (success=1); previously failed measurements were dropped from the CSV entirely.
1 parent 6585182 commit 91fede5

3 files changed

Lines changed: 86 additions & 22 deletions

File tree

synapse/cli/impedance_csv.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Shared helpers for writing impedance-measurement CSV files.
2+
3+
Both the non-streaming (`synapsectl query`) and streaming (`--stream`) paths
4+
emit the same CSV so downstream tooling can parse either identically:
5+
6+
Peripheral: <name>
7+
Electrode ID,Magnitude (Ohms),Phase (degrees),Status
8+
<electrode_id>,<magnitude>,<phase>,<status>
9+
...
10+
11+
`Status` is 1 for a successful measurement and 0 for a failed one.
12+
"""
13+
14+
import csv
15+
16+
from synapse.api.device_pb2 import Peripheral
17+
18+
CSV_COLUMNS = ["Electrode ID", "Magnitude (Ohms)", "Phase (degrees)", "Status"]
19+
20+
STATUS_OK = 1
21+
STATUS_FAILED = 0
22+
23+
# Force LF so the streaming (csv.writer) and non-streaming paths produce
24+
# byte-identical files regardless of platform.
25+
_LINE_TERMINATOR = "\n"
26+
27+
28+
def resolve_peripheral_name(device, impedance_query) -> str:
29+
"""Best-effort name of the peripheral the measurement ran on, for the CSV header.
30+
31+
Prefers the ``peripheral_id`` named in the query (if the proto carries one);
32+
otherwise falls back to the device's broadband (recording) source, then the
33+
first peripheral. Returns "Unknown" if it can't be resolved.
34+
"""
35+
info = device.info() if device is not None else None
36+
if not info or not info.peripherals:
37+
return "Unknown"
38+
39+
# Command-range ids (e.g. 2 = "first broadband source") won't match a
40+
# concrete peripheral_id and fall through to the broadband lookup below.
41+
peripheral_id = getattr(impedance_query, "peripheral_id", 0)
42+
if peripheral_id:
43+
for p in info.peripherals:
44+
if p.peripheral_id == peripheral_id:
45+
return p.name
46+
47+
for p in info.peripherals:
48+
if p.type == Peripheral.kBroadbandSource:
49+
return p.name
50+
51+
return info.peripherals[0].name
52+
53+
54+
def write_header(filename, peripheral_name):
55+
"""Create (truncate) the CSV and write the peripheral line + column header."""
56+
with open(filename, "w", newline="") as f:
57+
f.write(f"Peripheral: {peripheral_name}{_LINE_TERMINATOR}")
58+
csv.writer(f, lineterminator=_LINE_TERMINATOR).writerow(CSV_COLUMNS)
59+
60+
61+
def append_measurements(filename, measurements, status=STATUS_OK):
62+
"""Append measurement rows to an existing CSV created by `write_header`."""
63+
with open(filename, "a", newline="") as f:
64+
writer = csv.writer(f, lineterminator=_LINE_TERMINATOR)
65+
for m in measurements:
66+
writer.writerow([m.electrode_id, m.magnitude, m.phase, status])

synapse/cli/query.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
#!/usr/bin/env python3
22
import asyncio
3-
import csv
43
from threading import Thread
54
import time
65
import sys
76
import synapse as syn
87
from synapse.api.query_pb2 import QueryRequest, StreamQueryRequest
8+
from synapse.cli import impedance_csv
99
from google.protobuf.json_format import Parse
1010

1111
from rich.progress import (
@@ -154,10 +154,9 @@ def handle_impedance_stream(self, request):
154154
failed_measurements = []
155155

156156
# Create a CSV file to read from at the beginning
157+
peripheral_name = impedance_csv.resolve_peripheral_name(self.device, query)
157158
filename = f"impedance_measurements_{time.strftime('%Y%m%d-%H%M%S')}.csv"
158-
with open(filename, "w", newline="") as f:
159-
writer = csv.writer(f)
160-
writer.writerow(["Electrode ID", "Magnitude", "Phase"])
159+
impedance_csv.write_header(filename, peripheral_name)
161160
self.console.print(f"[green] Started saving measurements to {filename}")
162161

163162
progress = Progress(
@@ -217,6 +216,9 @@ def update_progress():
217216
progress.console.log(
218217
f"electrode id (mag, phase): {sample.electrode_id}\t {sample.magnitude},{sample.phase}"
219218
)
219+
self.save_measurement_batch(
220+
filename, failed_batch, status=impedance_csv.STATUS_FAILED
221+
)
220222
measurements_received += len(failed_batch)
221223
progress.update(
222224
task, completed=min(measurements_received, electrode_count)
@@ -269,14 +271,11 @@ def display_impedance_results(self, measurements):
269271
)
270272
self.console.print(table)
271273

272-
def save_measurement_batch(self, filename, measurements):
274+
def save_measurement_batch(
275+
self, filename, measurements, status=impedance_csv.STATUS_OK
276+
):
273277
# Save a batch of measurements as they come in
274-
with open(filename, "a", newline="") as f:
275-
writer = csv.writer(f)
276-
for measurement in measurements:
277-
writer.writerow(
278-
[measurement.electrode_id, measurement.magnitude, measurement.phase]
279-
)
278+
impedance_csv.append_measurements(filename, measurements, status=status)
280279

281280

282281
def load_config_from_file(path_to_config):

synapse/cli/rpc.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from rich.console import Console
1414

1515
from synapse.cli.query import StreamingQueryClient
16+
from synapse.cli import impedance_csv
1617
from synapse.utils.log import log_entry_to_str
1718
from synapse.cli.device_info_display import DeviceInfoDisplay
1819
from synapse.utils.proto import load_device_config
@@ -140,26 +141,24 @@ def load_query_request(path_to_config):
140141
console.print("Running query:")
141142
console.print(query_proto)
142143

143-
result: QueryResponse = syn.Device(args.uri, args.verbose).query(
144-
query_proto
145-
)
144+
device = syn.Device(args.uri, args.verbose)
145+
result: QueryResponse = device.query(query_proto)
146146
if result:
147147
console.print(text_format.MessageToString(result))
148148

149149
if result.HasField("impedance_response"):
150150
measurements = result.impedance_response
151+
peripheral_name = impedance_csv.resolve_peripheral_name(
152+
device, query_proto.impedance_query
153+
)
151154
# Write impedance measurements to a CSV file
152155
timestamp = time.strftime("%Y%m%d-%H%M%S")
153156
filename = f"impedance_measurements_{timestamp}.csv"
154157
try:
155-
with open(filename, "w") as f:
156-
f.write(
157-
"Electrode ID,Magnitude (Ohms),Phase (degrees),Status\n"
158-
)
159-
for measurement in measurements.measurements:
160-
f.write(
161-
f"{measurement.electrode_id},{measurement.magnitude},{measurement.phase},1\n"
162-
)
158+
impedance_csv.write_header(filename, peripheral_name)
159+
impedance_csv.append_measurements(
160+
filename, measurements.measurements
161+
)
163162
console.print(
164163
f"[green]Impedance measurements saved to {filename}[/green]"
165164
)

0 commit comments

Comments
 (0)