コード例 #1
0
ファイル: backend.py プロジェクト: kolinger/rd-usb
class Daemon:
    running = None
    thread = None
    storage = None
    config = None
    interface = None
    buffer = None
    buffer_expiration = None

    def __init__(self, backend, on_receive, on_receive_interval):
        self.backed = backend
        self.on_receive = on_receive
        self.on_receive_interval = on_receive_interval
        self.storage = Storage()
        if self.storage.fetch_status() != "disconnected":
            self.storage.update_status("disconnected")
        self.loop = asyncio.new_event_loop()
        self.converter = Converter()

    def start(self):
        self.running = True
        if self.thread is None:
            self.thread = Thread(target=self.run)
        if not self.thread.is_alive():
            self.thread.start()

    def stop(self):
        self.log("Disconnecting")
        self.running = False
        if self.interface:
            self.interface.disconnect()
        while self.thread and self.thread.is_alive():
            sleep(0.1)
        self.emit("disconnected")
        self.thread = None

    def run(self):
        self.storage = Storage()
        self.config = Config()

        self.interface = Wrapper()

        try:
            self.log("Connecting")
            self.retry(self.interface.connect)
            self.emit("connected")
            self.log("Connected")

            name = self.config.read("name")
            interval = float(self.config.read("rate"))
            version = self.config.read("version")
            session_id = self.storage.create_session(name, version)
            while self.running:
                begin = timer()
                data = self.retry(self.interface.read)

                if isinstance(data, str):
                    if data in ["disconnected", "connected"]:
                        self.disconnect()
                        return
                    raise Exception(data)
                else:
                    self.log(json.dumps(data))
                    if data:
                        data["session_id"] = session_id
                        self.update(data, version)
                    self.storage.store_measurement(data)

                measurement_runtime = timer() - begin
                sleep_time = interval - measurement_runtime
                if sleep_time > 0:
                    sleep(sleep_time)

        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            logging.exception(sys.exc_info()[0])
            self.emit("log", traceback.format_exc())
            self.emit("log-error")
        finally:
            self.disconnect()

    def disconnect(self):
        self.interface.disconnect()
        self.emit("disconnected")
        self.log("Disconnected")
        self.thread = None

    def update(self, data, version):
        format = Format(version)

        table = []
        for name in format.table_fields:
            callback = getattr(format, name)
            table.append(callback(data))

        graph = {}
        for name in format.graph_fields:
            if name == "timestamp":
                callback = getattr(format, name)
                value = callback(data)
            else:
                value = data[name]
            graph[name] = value

        graph = self.converter.convert(graph)

        if self.on_receive:
            if not self.buffer:
                self.buffer = []

            data["timestamp"] = int(data["timestamp"])
            self.buffer.append(data)

            execute = True
            if self.on_receive_interval:
                execute = False
                if not self.buffer_expiration or self.buffer_expiration <= time(
                ):
                    execute = True
                    self.buffer_expiration = time() + self.on_receive_interval

            if execute:
                payload = json.dumps(self.buffer)
                self.buffer = None
                payload_file = os.path.join(
                    os.getcwd(), "on-receive-payload-%s.json") % time()
                with open(payload_file, "w") as file:
                    file.write(payload)
                command = self.on_receive + " \"" + payload_file + "\""
                subprocess.Popen(command, shell=True, env={})

        self.emit("update", json.dumps({
            "table": table,
            "graph": graph,
        }))

    def retry(self, callback):
        timeout = time() + 60
        count = 10
        reconnect = False
        while True:
            try:
                if reconnect:
                    self.interface.disconnect()
                    self.interface.connect()
                    # noinspection PyUnusedLocal
                    reconnect = False

                return callback()
            except (KeyboardInterrupt, SystemExit):
                raise
            except:
                count -= 1
                logging.exception(sys.exc_info()[0])
                if timeout <= time() or count <= 0:
                    raise
                else:
                    self.log("operation failed, retrying")
                    self.emit("log", traceback.format_exc())
                    reconnect = True

    def emit(self, event, data=None):
        if event == "log":
            self.storage.log(data)
        elif event in [
                "connecting", "connected", "disconnecting", "disconnected"
        ]:
            self.storage.update_status(event)
        self.backed.emit(event, data)

    def log(self, message):
        prefix = pendulum.now().format("YYYY-MM-DD HH:mm:ss") + " - "
        self.emit("log", prefix + message + "\n")
コード例 #2
0
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")