|
|
|
""" Sartorius Logger
|
|
|
|
|
|
|
|
Make time series measurements with a Sartorius sartoriusb.
|
|
|
|
"""
|
|
|
|
|
|
|
|
__version__ = "0.0.1"
|
|
|
|
|
|
|
|
import pandas
|
|
|
|
import sartoriusb
|
|
|
|
import time
|
|
|
|
import sys
|
|
|
|
|
|
|
|
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
|
|
|
|
sys.stdout.flush()
|
|
|
|
print(f"measurement {total} of {total}")
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
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)
|