You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
6.2 KiB
214 lines
6.2 KiB
""" Sartorius Logger |
|
|
|
Make time series measurements with a Sartorius sartoriusb. |
|
""" |
|
|
|
__version__ = "0.0.1" |
|
|
|
import pandas |
|
import sartoriusb |
|
import time |
|
|
|
from collections import namedtuple |
|
from datetime import datetime |
|
from tqdm import tqdm |
|
|
|
from .datalogger import DataLogger, NullLogger |
|
from .parsers import parse_cli_arguments, parse_gui_arguments |
|
|
|
try: |
|
from gooey import Gooey |
|
except ImportError: |
|
|
|
def Gooey(*args, **kargs): |
|
msg = "The graphilcal user interface must be installed separately" |
|
raise NotImplementedError(msg) |
|
|
|
|
|
SCALE_INFO_LABELS = { |
|
sartoriusb.CMD_INFO_TYPE: "Scale Model", |
|
sartoriusb.CMD_INFO_SNR: "Scale Serial Number", |
|
sartoriusb.CMD_INFO_VERSION_SCALE: "Software Version of Scale", |
|
sartoriusb.CMD_INFO_VERSION_CONTROL_UNIT: "Software Version of Control Unit", # noqa: E501 |
|
} |
|
|
|
MEASUREMENT_KEYS = [ |
|
"nr", |
|
"time", |
|
"mode", |
|
"value", |
|
"unit", |
|
"stable", |
|
"message", |
|
] |
|
|
|
|
|
Result = namedtuple("Result", ["info", "scale", "data", "log_file"]) |
|
|
|
|
|
def get_scale_info(conn): |
|
""" returns the available scale information """ |
|
data = {} |
|
|
|
for command, label in SCALE_INFO_LABELS.items(): |
|
raw_data_lines = conn.get(command) |
|
if raw_data_lines: |
|
raw_data = raw_data_lines[0] |
|
raw_data = raw_data.strip() |
|
parts = raw_data.split(maxsplit=1) |
|
info = parts[1] if len(parts) > 1 else "" |
|
else: |
|
# propably a timeout of the serial connection |
|
info = "" |
|
data[label] = info |
|
|
|
return data |
|
|
|
|
|
def _measure_and_log(nr, conn, logger): |
|
""" performs and logs one measurement |
|
|
|
:params nr: number of measurement |
|
:params conn: connection to the scale |
|
:params log: data logger instance |
|
:returns: dict for measurement point |
|
""" |
|
measurement = conn.measure() |
|
data_list = [nr, datetime.now()] + list(measurement) |
|
logger.add_list(data_list) |
|
|
|
data_dict = dict(zip(MEASUREMENT_KEYS, data_list)) |
|
|
|
# for the pandas data frame the value should be transformed into a float |
|
try: |
|
data_dict["value"] = float(data_dict["value"]) |
|
except (ValueError, TypeError): |
|
pass |
|
|
|
return data_dict |
|
|
|
|
|
def no_progress_bar(iterator): |
|
"""" a stub function for not displaying a progress bar """ |
|
return iterator |
|
|
|
|
|
def gooey_progress_factory(settings): |
|
"""" progress information, tailored to Gooey """ |
|
total = settings.measurements |
|
print( |
|
( |
|
f"measuring every " |
|
f"{settings.interval.value}{settings.interval.unit} " |
|
f"for {settings.duration.value}{settings.duration.unit}" |
|
) |
|
) |
|
|
|
def gui_progress(iterator): |
|
for i in iterator: |
|
print(f"measurement {i} of {total}") |
|
yield i |
|
print(f"measurement {total} of {total}") |
|
|
|
return gui_progress |
|
|
|
|
|
def _get_log_file_path(settings): |
|
""" constructs the path to the log file """ |
|
|
|
now = datetime.now() |
|
log_file_name = now.strftime("%Y-%m-%d %H-%M-%S") + ".txt" |
|
return settings.directory / log_file_name |
|
|
|
|
|
def _log_measurement_info(logger, settings): |
|
""" logs all measurement info """ |
|
measurement_info = { |
|
"Measurements": settings.measurements, |
|
"Duration": f"{settings.duration.value}{settings.duration.unit}", |
|
"Interval": f"{settings.interval.value}{settings.interval.unit}", |
|
"Com-Port": settings.port, |
|
} |
|
logger.add_section("Measurement Settings", measurement_info.items()) |
|
return measurement_info |
|
|
|
|
|
def _log_scale_info(logger, conn): |
|
""" logs common scale info """ |
|
scale_info = get_scale_info(conn) |
|
logger.add_section("Scale Info", scale_info.items()) |
|
return scale_info |
|
|
|
|
|
def measure_series(settings, progress_bar=no_progress_bar, data_logger=None): |
|
""" serial measurements |
|
|
|
will return the data as pandas data frames in a "Result" named tuple |
|
|
|
:params settings: parser.Settings named tuple |
|
:params progress_bar: progress bar function to use |
|
:params data_logger: class of the data logger to use |
|
:returns: named tuple "Result" |
|
""" |
|
data_logger = data_logger or NullLogger() |
|
|
|
data_collection = [] |
|
|
|
with data_logger as logger: |
|
measurement_info = _log_measurement_info(logger, settings) |
|
|
|
with sartoriusb.SartoriusUsb(settings.port) as conn: |
|
scale_info = _log_scale_info(logger, conn) |
|
|
|
# add column headers |
|
headers = [item.capitalize() for item in MEASUREMENT_KEYS] |
|
logger.add_section( |
|
"Measured Data", [headers], append_empty_line=False |
|
) |
|
|
|
for i in progress_bar(range(1, settings.measurements)): |
|
data = _measure_and_log(i, conn, logger) |
|
data_collection.append(data) |
|
time.sleep(settings.interval.seconds) |
|
|
|
data = _measure_and_log(settings.measurements, conn, logger) |
|
data_collection.append(data) |
|
|
|
data_df = pandas.DataFrame(data_collection).set_index("time") |
|
info_df = pandas.DataFrame(measurement_info.items()).set_index(0) |
|
scale_df = pandas.DataFrame(scale_info.items()).set_index(0) |
|
|
|
return Result(info_df, scale_df, data_df, data_logger.path) |
|
|
|
|
|
def export_as_excel(measurement_result): |
|
""" saves the collected data as an Excel file """ |
|
excel_path = measurement_result.log_file.with_suffix(".xlsx") |
|
with pandas.ExcelWriter(excel_path) as writer: |
|
measurement_result.data.to_excel(writer, sheet_name="Measurements") |
|
measurement_result.info.to_excel(writer, sheet_name="Settings") |
|
measurement_result.scale.to_excel(writer, sheet_name="Scale") |
|
|
|
|
|
def cli(): |
|
settings = parse_cli_arguments() |
|
log_file_path = _get_log_file_path(settings) |
|
result = measure_series( |
|
settings, progress_bar=tqdm, data_logger=DataLogger(log_file_path) |
|
) |
|
export_as_excel(result) |
|
|
|
|
|
@Gooey( |
|
program_name="SartoriusLogger", |
|
progress_regex=r"^measurement (?P<current>\d+) of (?P<total>\d+)", |
|
progress_expr="current / total * 100", |
|
) |
|
def gui(): |
|
settings = parse_gui_arguments() |
|
log_file_path = _get_log_file_path(settings) |
|
gpb = gooey_progress_factory(settings) |
|
result = measure_series( |
|
settings, progress_bar=gpb, data_logger=DataLogger(log_file_path) |
|
) |
|
export_as_excel(result)
|
|
|