class Index: config = None storage = None def __init__(self): pass def register(self): blueprint = Blueprint("index", __name__, template_folder="templates") blueprint.add_url_rule("/", "default", self.render_default) blueprint.add_url_rule("/data", "data", self.render_data) blueprint.add_url_rule("/graph", "graph", self.render_graph) blueprint.add_url_rule("/graph.json", "graph_data", self.render_graph_data) blueprint.add_url_rule("/ble", "ble", self.render_ble) blueprint.add_url_rule("/serial", "serial", self.render_serial) blueprint.context_processor(self.fill) return blueprint def init(self): self.config = Config() self.storage = Storage() def fill(self): variables = { "rd_user_version": version, "format": Format(), "url_for": self.url_for, "version": self.config.read("version", "UM34C"), "port": self.config.read("port", ""), "rate": str(self.config.read("rate", 1.0)), "name": self.config.read("name", arrow.now().format("YYYY-MM-DD")), "ble_address": self.config.read("ble_address"), } status = self.storage.fetch_status() variables["status"] = status.title() variables["connect_disabled"] = status != "disconnected" variables[ "connect_button"] = "Connect" if status == "disconnected" else "Disconnect" return variables def render_default(self): self.init() self.storage.clear_log() log = self.storage.fetch_log() return render_template("default.html", log=log, page="default") def render_data(self): self.init() names, selected = self.prepare_selection() name = self.storage.translate_selected_name(selected) if request.args.get("export") == "": string = io.StringIO() writer = csv.writer(string) format = Format() names = [] for field in format.export_fields: names.append(format.field_name(field)) writer.writerow(names) for item in self.storage.fetch_measurements(name): values = [] for field in format.export_fields: if field == "time": values.append(format.time(item)) else: values.append(item[field]) writer.writerow(values) output = make_response(string.getvalue()) output.headers[ "Content-Disposition"] = "attachment; filename=" + name + ".csv" output.headers["Content-type"] = "text/csv" return output elif request.args.get("destroy") == "": self.storage.destroy_measurements(name) flash("Measurements with session name '" + name + "' were deleted", "danger") return redirect(request.path) page = request.args.get("page", 1, int) limit = 100 offset = limit * (page - 1) count = self.storage.fetch_measurements_count(name) pages = self.prepare_pages(name, page, limit, count) measurements = self.storage.fetch_measurements(name, limit, offset) return render_template( "data.html", names=names, selected=selected, measurements=measurements, page="data", pages=pages, ) def prepare_pages(self, name, page, limit, count, blocks=10): first_page = 1 related = 3 last_page = int(ceil(count / limit)) steps = set( range(max((first_page, page - related)), min((last_page, page + related)) + 1)) quotient = (last_page - 1) / blocks if len(steps) > 1: for index in range(0, blocks): steps.add(round(quotient * index) + first_page) steps.add(last_page) steps = sorted(steps) pages = [] for number in steps: pages.append({ "number": number, "link": url_for("index.data", page=number, name=name), "current": number == page, }) return pages def render_graph(self): self.init() names, selected = self.prepare_selection() last_measurement = None if selected == "": last_measurement = self.storage.fetch_last_measurement() return render_template("graph.html", names=names, selected=selected, item=last_measurement, left_axis="voltage", right_axis="current", page="graph") def render_graph_data(self): self.init() selected = request.args.get("name") name = self.storage.translate_selected_name(selected) left_axis = request.args.get("left_axis") right_axis = request.args.get("right_axis") format = Format() data = [] for item in self.storage.fetch_measurements(name): if left_axis in item: data.append({ "date": format.timestamp(item), "left": item[left_axis], "right": item[right_axis], }) return jsonify(data) def prepare_selection(self): names = self.storage.fetch_measurement_names() selected = request.args.get("name") if not selected: selected = "" return names, selected def fill_config_from_parameters(self): value = request.args.get("version") if value is not None: self.config.write("version", value) value = request.args.get("name") if value is not None: self.config.write("name", value) value = request.args.get("rate") if value is not None: self.config.write("rate", float(value)) def render_ble(self): self.init() self.fill_config_from_parameters() return render_template("ble.html") def render_serial(self): self.init() self.fill_config_from_parameters() return render_template("serial.html") def url_for(self, endpoint, **values): if endpoint == "static": filename = values.get("filename", None) if filename: file_path = static_path + "/" + filename values["v"] = int(os.stat(file_path).st_mtime) return url_for(endpoint, **values)
class Index: config = None storage = None import_in_progress = False def register(self): blueprint = Blueprint("index", __name__, template_folder="templates") blueprint.add_url_rule("/", "default", self.render_default) blueprint.add_url_rule("/data", "data", self.render_data) blueprint.add_url_rule("/graph", "graph", self.render_graph) blueprint.add_url_rule("/graph.json", "graph_data", self.render_graph_data) blueprint.add_url_rule("/setup", "setup", self.render_setup, methods=["GET", "POST"]) blueprint.add_url_rule("/ble", "ble", self.render_ble) blueprint.add_url_rule("/serial", "serial", self.render_serial) blueprint.add_url_rule("/tc66c-import", "tc66c_import", self.render_tc66c_import, methods=["GET", "POST"]) blueprint.context_processor(self.fill) return blueprint def init(self): self.config = Config() self.storage = Storage() def fill(self): variables = { "rd_user_version": version, "url_for": self.url_for, "version": self.config.read("version", "UM34C"), "port": self.config.read("port", ""), "rate": str(self.config.read("rate", 1.0)), "name": self.config.read("name", pendulum.now().format("YYYY-MM-DD")), "ble_address": self.config.read("ble_address"), "format_datetime": self.format_date, } status = self.storage.fetch_status() variables["status"] = status.title() variables["connect_disabled"] = status != "disconnected" variables["connect_button"] = "Connect" if status == "disconnected" else "Disconnect" setup = self.config.read("setup") variables["theme"] = setup["theme"] if setup and "theme" in setup else "light" variables["embedded"] = current_app.config["embedded"] return variables def render_default(self): self.init() self.storage.clear_log() log = self.storage.fetch_log() return render_template("default.html", log=log, page="default") def render_data(self): self.init() sessions, selected = self.prepare_selection() session = self.storage.get_selected_session(selected) format = None measurements = [] pages = [] if session: format = Format(session["version"]) if request.args.get("export") == "": string = io.StringIO() writer = csv.writer(string) names = [] for field in format.export_fields: names.append(format.field_name(field)) writer.writerow(names) run_time_offset = None for item in self.storage.fetch_measurements(session["id"]): if run_time_offset is None and item["resistance"] < 9999.9: run_time_offset = item["timestamp"] rune_time = 0 if run_time_offset is not None: rune_time = round(item["timestamp"] - run_time_offset) values = [] for field in format.export_fields: if field == "time": values.append(format.time(item)) elif field == "run_time": remaining = rune_time hours = floor(remaining / 3600) remaining -= hours * 3600 minutes = floor(remaining / 60) remaining -= minutes * 60 seconds = remaining parts = [ hours, minutes, seconds, ] for index, value in enumerate(parts): parts[index] = str(value).zfill(2) values.append(":".join(parts)) elif field == "run_time_seconds": values.append(rune_time) else: values.append(item[field]) writer.writerow(values) output = make_response(string.getvalue()) output.headers["Content-Disposition"] = "attachment; filename=" + session["name"] + ".csv" output.headers["Content-type"] = "text/csv" return output elif request.args.get("destroy") == "": if selected == "": flash("Please select session first", "info") return redirect(request.path) self.storage.destroy_measurements(session["id"]) flash("Measurements with session name '" + session["name"] + "' were deleted", "danger") return redirect(request.path) page = request.args.get("page", 1, int) limit = 100 offset = limit * (page - 1) count = self.storage.fetch_measurements_count(session["id"]) pages = self.prepare_pages(session["id"], page, limit, count) measurements = self.storage.fetch_measurements(session["id"], limit, offset) return render_template( "data.html", format=format, sessions=sessions, selected=selected, measurements=measurements, pages=pages, page="data", ) def prepare_pages(self, name, page, limit, count, blocks=10): first_page = 1 related = 3 last_page = int(ceil(count / limit)) steps = set(range(max((first_page, page - related)), min((last_page, page + related)) + 1)) quotient = (last_page - 1) / blocks if len(steps) > 1: for index in range(0, blocks): steps.add(round(quotient * index) + first_page) steps.add(last_page) steps = sorted(steps) pages = [] for number in steps: pages.append({ "number": number, "link": url_for("index.data", page=number, name=name), "current": number == page, }) return pages def render_graph(self): self.init() sessions, selected = self.prepare_selection() session = self.storage.get_selected_session(selected) format = Format(session["version"] if session else None) last_measurement = None if selected == "": last_measurement = self.storage.fetch_last_measurement() return render_template( "graph.html", format=format, sessions=sessions, selected=selected, item=last_measurement, left_axis="voltage", right_axis="current", colors=self.config.read("colors", "colorful"), page="graph" ) def render_graph_data(self): self.init() selected = request.args.get("session") session = self.storage.get_selected_session(selected) left_axis = request.args.get("left_axis") right_axis = request.args.get("right_axis") colors = request.args.get("colors") if self.config.read("colors") != colors: self.config.write("colors", colors, flush=True) format = Format(session["version"] if session else None) data = [] if session: for item in self.storage.fetch_measurements(session["id"]): if left_axis in item: data.append({ "date": format.timestamp(item), "left": item[left_axis], "right": item[right_axis], }) return jsonify(data) def prepare_selection(self): sessions = self.storage.fetch_sessions() selected = request.args.get("session") try: selected = int(selected) except (ValueError, TypeError): selected = None if selected is None: selected = "" return sessions, selected def fill_config_from_parameters(self): value = request.args.get("version") if value is not None: self.config.write("version", value) value = request.args.get("rate") if value is not None: self.config.write("rate", float(value)) def render_setup(self): self.init() setup = self.config.read("setup") if not isinstance(setup, dict): setup = {} if "do" in request.form: setup["theme"] = request.form["theme"] self.config.write("setup", setup) flash("Settings were successfully saved", "success") return redirect(url_for("index.setup")) return render_template( "setup.html", setup=setup, page="setup" ) def render_ble(self): self.init() self.fill_config_from_parameters() return render_template( "ble.html" ) def render_serial(self): self.init() self.fill_config_from_parameters() return render_template( "serial.html" ) def render_tc66c_import(self): self.init() self.fill_config_from_parameters() messages = [] if self.config.read("version") not in ["TC66C-USB"]: messages.append("Available only for TC66C USB") elif self.storage.fetch_status() != "disconnected": messages.append("Disconnect first") session_name = "My recording" period = 1 calculate = False if "do" in request.form: if len(messages) == 0: session_name = request.form.get("session_name") if not session_name: messages.append("Please provide session name") try: period = int(request.form.get("period")) if period < 1 or period > 60: raise ValueError except ValueError: messages.append("Period has invalid value, please enter number between 1 and 60") calculate = request.form.get("calculate") is not None if len(messages) == 0: messages.extend(self.do_tc66c_import(session_name, period, calculate)) if len(messages) == 0: return redirect(url_for("index.graph")) return render_template( "tc66c-import.html", messages=messages, session_name=session_name, period=period, calculate=calculate, page="data" ) def do_tc66c_import(self, name, period=1, calculate=False): messages = [] if self.import_in_progress: return "Import is already running" self.import_in_progress = True serial_timeout = int(self.config.read("serial_timeout", 10)) interface = TcSerialInterface(self.config.read("port"), serial_timeout) self.storage.fetch_status() try: interface.connect() begin = time() previous_timestamp = None accumulated_current = 0 accumulated_power = 0 session_id = None for index, record in enumerate(interface.read_records()): timestamp = begin + (index * period) if session_id is None: session_id = self.storage.create_session(name, "TC66C recording") data = { "timestamp": timestamp, "voltage": round(record["voltage"] * 10000) / 10000, "current": round(record["current"] * 100000) / 100000, "power": 0, "temperature": 0, "data_plus": 0, "data_minus": 0, "mode_id": 0, "mode_name": None, "accumulated_current": round(accumulated_current), "accumulated_power": round(accumulated_power), "accumulated_time": 0, "resistance": 0, "session_id": session_id, } if calculate: data["power"] = round(record["voltage"] * record["current"] * 1000) / 1000 if record["current"] <= 0 and record["current"] >= 0: data["resistance"] = 9999.9 else: data["resistance"] = round(record["voltage"] / record["current"] * 10) / 10 if data["resistance"] > 9999.9: data["resistance"] = 9999.9 if previous_timestamp is not None: delta = (timestamp - previous_timestamp) / 3600 accumulated_current += (data["current"] * 1000) * delta accumulated_power += (data["power"] * 1000) * delta self.storage.store_measurement(data) previous_timestamp = timestamp except (KeyboardInterrupt, SystemExit): raise except: message = "Failed to connect:" exception = traceback.format_exc() self.storage.log(exception) message += "\n%s" % exception messages.append(message) finally: interface.disconnect() self.import_in_progress = False return messages def url_for(self, endpoint, **values): if endpoint == "static": filename = values.get("filename", None) if filename: file_path = static_path + "/" + filename values["v"] = int(os.stat(file_path).st_mtime) return url_for(endpoint, **values) def format_date(self, timestamp): date = pendulum.from_timestamp(timestamp) return date.in_timezone("local").format("YYYY-MM-DD HH:mm:ss")