Shaky Lab

Shaky Lab#

This example of %%there ai usage is available as a YouTube video: https://youtu.be/4wqIQ-iWO5A

%load_ext pythonhere
%connect-there
%%there ai
Build Shaky Lab: a pocket motion laboratory for Android.
- show live x, y, and z acceleration values
- include Start Recording, Stop Recording, and Reset buttons
- store recorded samples with timestamps in `samples` variable
- show the number of samples recorded
- portrait mode
- screen should be blocked from rotation
Generating %%there cell with AI... this can take up to 300s.
Generated a %%there cell in 43.5s. Review it, then run it to execute on the connected target.
Hide code cell source
%%there
# Generated locally by %%there ai. Review before running.
from time import time
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.logger import Logger
from kivy.uix.popup import Popup
from kivy.uix.label import Label
from plyer import accelerometer

try:
    _old_event = globals().get("shaky_lab_update_event")
    if _old_event is not None:
        _old_event.cancel()
except Exception:
    Logger.exception("PythonHere: Could not cancel previous Shaky Lab update event")

samples = []
shaky_lab_state = {
    "recording": False,
    "enabled": False,
    "sample_count": 0,
    "last_acceleration": None,
    "last_error": None,
    "orientation_locked": False,
}

def shaky_lab_show_error(message):
    shaky_lab_state["last_error"] = str(message)
    Logger.error("PythonHere: Shaky Lab error: " + str(message))
    try:
        Popup(
            title="Shaky Lab Error",
            content=Label(text=str(message), text_size=(root.width * 0.8, None)),
            size_hint=(0.9, 0.35),
        ).open()
    except Exception:
        Logger.exception("PythonHere: Could not show Shaky Lab error popup")

def shaky_lab_lock_portrait():
    try:
        from jnius import autoclass
        PythonActivity = autoclass("org.kivy.android.PythonActivity")
        ActivityInfo = autoclass("android.content.pm.ActivityInfo")
        activity = PythonActivity.mActivity
        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
        shaky_lab_state["orientation_locked"] = True
    except Exception as exc:
        shaky_lab_state["orientation_locked"] = False
        shaky_lab_state["last_error"] = f"{type(exc).__name__}: {exc}"
        Logger.exception("PythonHere: Could not lock portrait orientation")

def shaky_lab_enable_sensor():
    try:
        accelerometer.enable()
        shaky_lab_state["enabled"] = True
        return True
    except Exception as exc:
        shaky_lab_state["enabled"] = False
        shaky_lab_state["last_error"] = f"{type(exc).__name__}: {exc}"
        Logger.exception("PythonHere: Could not enable accelerometer")
        return False

KV = """
#:import dp kivy.metrics.dp
#:import sp kivy.metrics.sp

BoxLayout:
    orientation: "vertical"
    padding: dp(16)
    spacing: dp(12)

    Label:
        id: title_label
        text: "Shaky Lab"
        font_size: sp(28)
        bold: True
        size_hint_y: None
        height: dp(48)

    Label:
        id: subtitle_label
        text: "Pocket motion laboratory"
        font_size: sp(16)
        size_hint_y: None
        height: dp(32)

    GridLayout:
        cols: 2
        spacing: dp(10)
        size_hint_y: None
        height: dp(180)

        Label:
            text: "X acceleration"
            font_size: sp(18)
        Label:
            id: x_value
            text: "0.000"
            font_size: sp(22)
            bold: True

        Label:
            text: "Y acceleration"
            font_size: sp(18)
        Label:
            id: y_value
            text: "0.000"
            font_size: sp(22)
            bold: True

        Label:
            text: "Z acceleration"
            font_size: sp(18)
        Label:
            id: z_value
            text: "0.000"
            font_size: sp(22)
            bold: True

    Label:
        id: count_label
        text: "Samples recorded: 0"
        font_size: sp(20)
        bold: True
        size_hint_y: None
        height: dp(44)

    Label:
        id: status_label
        text: "Ready. Press Start Recording."
        font_size: sp(16)
        halign: "center"
        valign: "middle"
        text_size: self.size
        size_hint_y: 1

    BoxLayout:
        orientation: "vertical"
        spacing: dp(10)
        size_hint_y: None
        height: dp(186)

        Button:
            id: start_button
            text: "Start Recording"
            font_size: sp(20)
            size_hint_y: None
            height: dp(54)

        Button:
            id: stop_button
            text: "Stop Recording"
            font_size: sp(20)
            size_hint_y: None
            height: dp(54)

        Button:
            id: reset_button
            text: "Reset"
            font_size: sp(20)
            size_hint_y: None
            height: dp(54)
"""

shaky_lab_ui = Builder.load_string(KV)
if shaky_lab_ui is None:
    shaky_lab_show_error("Could not load Shaky Lab interface.")
else:
    def shaky_lab_set_status(text):
        shaky_lab_ui.ids.status_label.text = text

    def shaky_lab_update_count():
        shaky_lab_state["sample_count"] = len(samples)
        shaky_lab_ui.ids.count_label.text = f"Samples recorded: {len(samples)}"

    def shaky_lab_start_recording(instance):
        if not shaky_lab_state.get("enabled"):
            if not shaky_lab_enable_sensor():
                shaky_lab_set_status("Accelerometer is unavailable. See error details.")
                shaky_lab_show_error(shaky_lab_state.get("last_error") or "Accelerometer is unavailable.")
                return
        shaky_lab_state["recording"] = True
        shaky_lab_set_status("Recording motion samples.")

    def shaky_lab_stop_recording(instance):
        shaky_lab_state["recording"] = False
        shaky_lab_set_status("Recording stopped.")

    def shaky_lab_reset(instance):
        samples.clear()
        shaky_lab_state["sample_count"] = 0
        shaky_lab_update_count()
        shaky_lab_set_status("Samples reset. Press Start Recording.")

    def shaky_lab_update(dt):
        try:
            accel = accelerometer.acceleration
            if accel is None:
                shaky_lab_set_status("Waiting for accelerometer data.")
                return

            x, y, z = accel
            x = 0.0 if x is None else float(x)
            y = 0.0 if y is None else float(y)
            z = 0.0 if z is None else float(z)

            shaky_lab_state["last_acceleration"] = (x, y, z)
            shaky_lab_ui.ids.x_value.text = f"{x:.3f}"
            shaky_lab_ui.ids.y_value.text = f"{y:.3f}"
            shaky_lab_ui.ids.z_value.text = f"{z:.3f}"

            if shaky_lab_state.get("recording"):
                samples.append({
                    "timestamp": time(),
                    "x": x,
                    "y": y,
                    "z": z,
                })
                shaky_lab_update_count()
        except Exception as exc:
            shaky_lab_state["recording"] = False
            shaky_lab_state["last_error"] = f"{type(exc).__name__}: {exc}"
            Logger.exception("PythonHere: Could not update Shaky Lab acceleration")
            shaky_lab_set_status("Sensor update failed. Recording stopped.")

    shaky_lab_ui.ids.start_button.bind(on_release=shaky_lab_start_recording)
    shaky_lab_ui.ids.stop_button.bind(on_release=shaky_lab_stop_recording)
    shaky_lab_ui.ids.reset_button.bind(on_release=shaky_lab_reset)

    shaky_lab_lock_portrait()
    if shaky_lab_enable_sensor():
        shaky_lab_set_status("Ready. Press Start Recording.")
    else:
        shaky_lab_set_status("Accelerometer is unavailable on this device.")

    shaky_lab_update_event = Clock.schedule_interval(shaky_lab_update, 0.1)

    root.clear_widgets()
    root.add_widget(shaky_lab_ui)
%%there ai --fix
Rebuild "Shaky Lab" in a Jules Verne science-lab style:
brass, parchment, gears, Victorian instrument styling.

Make x, y, and z three small axis inputs, visibly linked by rods/gears
to one central combiner mechanism, which drives the main total-motion gauge.
The mechanism should clearly suggest that total motion is derived from x/y/z,
not show them as separate disconnected widgets.

Main gauge uses one pointer and these labels only: Calm, Tremor, Shake, Jolt, Surge.

Keep everything connected and fully inside its panels.
Generating %%there cell with AI... this can take up to 300s.
Generated a %%there cell in 143.7s. Review it, then run it to execute on the connected target.
Hide code cell source
%%there
# Generated locally by %%there ai. Review before running.
# AI mode: fix
# Fix: rebuild Shaky Lab as a connected Jules Verne style mechanism with one derived total-motion gauge.
from time import time
from math import sqrt, sin, cos, pi

from kivy.clock import Clock
from kivy.core.text import Label as CoreLabel
from kivy.graphics import Color, Ellipse, Line, Rectangle
from kivy.lang import Builder
from kivy.logger import Logger
from kivy.metrics import dp, sp
from kivy.properties import NumericProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.popup import Popup
from kivy.uix.widget import Widget
from plyer import accelerometer

try:
    _old_event = globals().get("shaky_lab_update_event")
    if _old_event is not None:
        _old_event.cancel()
except Exception:
    Logger.exception("PythonHere: Could not cancel previous Shaky Lab update event")

samples = globals().get("samples")
if not isinstance(samples, list):
    samples = []
globals()["samples"] = samples

shaky_lab_state = {
    "recording": False,
    "enabled": False,
    "sample_count": len(samples),
    "last_acceleration": None,
    "previous_acceleration": None,
    "last_motion": 0.0,
    "last_error": None,
    "orientation_locked": False,
}


def shaky_lab_show_error(message):
    shaky_lab_state["last_error"] = str(message)
    Logger.error("PythonHere: Shaky Lab error: " + str(message))
    try:
        Popup(
            title="Shaky Lab Error",
            content=Label(
                text=str(message),
                text_size=(root.width * 0.8, None),
                halign="center",
                valign="middle",
            ),
            size_hint=(0.9, 0.35),
        ).open()
    except Exception:
        Logger.exception("PythonHere: Could not show Shaky Lab error popup")


def shaky_lab_lock_portrait():
    try:
        from jnius import autoclass

        PythonActivity = autoclass("org.kivy.android.PythonActivity")
        ActivityInfo = autoclass("android.content.pm.ActivityInfo")
        activity = PythonActivity.mActivity
        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
        shaky_lab_state["orientation_locked"] = True
    except Exception as exc:
        shaky_lab_state["orientation_locked"] = False
        shaky_lab_state["last_error"] = f"{type(exc).__name__}: {exc}"
        Logger.exception("PythonHere: Could not lock portrait orientation")


def shaky_lab_enable_sensor():
    try:
        accelerometer.enable()
        shaky_lab_state["enabled"] = True
        return True
    except Exception as exc:
        shaky_lab_state["enabled"] = False
        shaky_lab_state["last_error"] = f"{type(exc).__name__}: {exc}"
        Logger.exception("PythonHere: Could not enable accelerometer")
        return False


def shaky_lab_stop_updates():
    event = globals().get("shaky_lab_update_event")
    if event is not None:
        event.cancel()
    shaky_lab_state["recording"] = False


def _clamp(value, low=0.0, high=1.0):
    return max(low, min(high, value))


class ShakyLabRoot(BoxLayout):
    pass


class MechanismDiagram(Widget):
    axis_x = NumericProperty(0.0)
    axis_y = NumericProperty(0.0)
    axis_z = NumericProperty(0.0)
    total_level = NumericProperty(0.0)
    gear_phase = NumericProperty(0.0)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.bind(pos=self.redraw, size=self.redraw)
        Clock.schedule_once(lambda dt: self.redraw(), 0)

    def set_motion(self, axis_x, axis_y, axis_z, total_level, gear_phase):
        self.axis_x = _clamp(axis_x, -1.0, 1.0)
        self.axis_y = _clamp(axis_y, -1.0, 1.0)
        self.axis_z = _clamp(axis_z, -1.0, 1.0)
        self.total_level = _clamp(total_level, 0.0, 1.0)
        self.gear_phase = gear_phase
        self.redraw()

    def _text(self, text, cx, cy, font_size, color=(0.18, 0.10, 0.04, 1), bold=False):
        label = CoreLabel(
            text=str(text),
            font_size=font_size,
            color=color,
            bold=bold,
        )
        label.refresh()
        tw, th = label.texture.size
        Color(1, 1, 1, 1)
        Rectangle(texture=label.texture, size=(tw, th), pos=(cx - tw / 2, cy - th / 2))

    def _panel(self, x, y, w, h, radius=0):
        Color(0.77, 0.63, 0.39, 1)
        Rectangle(pos=(x, y), size=(w, h))
        Color(0.93, 0.84, 0.62, 1)
        Rectangle(pos=(x + dp(3), y + dp(3)), size=(w - dp(6), h - dp(6)))
        Color(0.40, 0.24, 0.09, 1)
        Line(rectangle=(x, y, w, h), width=dp(2.0))
        Color(0.67, 0.47, 0.20, 1)
        Line(rectangle=(x + dp(6), y + dp(6), w - dp(12), h - dp(12)), width=dp(1.0))

    def _rod(self, x1, y1, x2, y2):
        Color(0.34, 0.20, 0.08, 1)
        Line(points=[x1, y1, x2, y2], width=dp(5.0), cap="round")
        Color(0.83, 0.61, 0.24, 1)
        Line(points=[x1, y1, x2, y2], width=dp(2.2), cap="round")
        Color(0.20, 0.11, 0.04, 1)
        Ellipse(pos=(x1 - dp(4), y1 - dp(4)), size=(dp(8), dp(8)))
        Ellipse(pos=(x2 - dp(4), y2 - dp(4)), size=(dp(8), dp(8)))

    def _gear(self, cx, cy, r, teeth, phase, active=1.0):
        Color(0.35, 0.21, 0.08, 1)
        Ellipse(pos=(cx - r * 1.13, cy - r * 1.13), size=(r * 2.26, r * 2.26))
        Color(0.78, 0.55, 0.20, 1)
        Ellipse(pos=(cx - r, cy - r), size=(r * 2, r * 2))
        Color(0.97, 0.79, 0.34, 1)
        Ellipse(pos=(cx - r * 0.72, cy - r * 0.72), size=(r * 1.44, r * 1.44))
        Color(0.30, 0.18, 0.07, 1)
        Line(circle=(cx, cy, r), width=dp(1.2))
        Line(circle=(cx, cy, r * 0.45), width=dp(1.1))
        for i in range(teeth):
            a = phase + 2 * pi * i / teeth
            x1 = cx + cos(a) * r * 0.93
            y1 = cy + sin(a) * r * 0.93
            x2 = cx + cos(a) * r * 1.25
            y2 = cy + sin(a) * r * 1.25
            Color(0.42, 0.25, 0.08, 0.60 + 0.25 * active)
            Line(points=[x1, y1, x2, y2], width=dp(2.0), cap="round")
        Color(0.18, 0.10, 0.04, 1)
        Ellipse(pos=(cx - r * 0.17, cy - r * 0.17), size=(r * 0.34, r * 0.34))

    def _axis_input(self, name, value, cx, cy, r, rod_x):
        value = _clamp(value, -1.0, 1.0)
        Color(0.42, 0.25, 0.09, 1)
        Ellipse(pos=(cx - r * 1.1, cy - r * 1.1), size=(r * 2.2, r * 2.2))
        Color(0.88, 0.73, 0.43, 1)
        Ellipse(pos=(cx - r, cy - r), size=(r * 2, r * 2))
        Color(0.94, 0.86, 0.65, 1)
        Ellipse(pos=(cx - r * 0.72, cy - r * 0.72), size=(r * 1.44, r * 1.44))
        Color(0.25, 0.14, 0.05, 1)
        Line(circle=(cx, cy, r * 0.78, 205, 335), width=dp(1.2))
        pointer_angle = (-90 + value * 55) * pi / 180.0
        Color(0.45, 0.06, 0.03, 1)
        Line(
            points=[
                cx,
                cy,
                cx + cos(pointer_angle) * r * 0.58,
                cy + sin(pointer_angle) * r * 0.58,
            ],
            width=dp(2.0),
            cap="round",
        )
        Color(0.18, 0.10, 0.04, 1)
        Ellipse(pos=(cx - r * 0.10, cy - r * 0.10), size=(r * 0.20, r * 0.20))
        self._text(name, cx, cy + r * 0.38, max(sp(11), min(sp(15), r * 0.38)), bold=True)

        slot_y = cy - r * 1.30
        slot_w = max(dp(34), rod_x - cx - r * 1.0)
        Color(0.38, 0.23, 0.09, 1)
        Rectangle(pos=(cx + r * 0.52, slot_y - dp(3)), size=(slot_w, dp(6)))
        Color(0.91, 0.68, 0.27, 1)
        piston_x = cx + r * 0.52 + slot_w * (0.5 + value * 0.35)
        Rectangle(pos=(piston_x - dp(4), slot_y - dp(8)), size=(dp(8), dp(16)))
        self._rod(cx + r * 0.96, cy, rod_x, cy)

    def _gauge(self, cx, cy, r, level):
        level = _clamp(level)
        Color(0.36, 0.21, 0.07, 1)
        Ellipse(pos=(cx - r * 1.10, cy - r * 1.10), size=(r * 2.20, r * 2.20))
        Color(0.74, 0.50, 0.18, 1)
        Ellipse(pos=(cx - r, cy - r), size=(r * 2, r * 2))
        Color(0.96, 0.89, 0.69, 1)
        Ellipse(pos=(cx - r * 0.86, cy - r * 0.86), size=(r * 1.72, r * 1.72))
        Color(0.25, 0.14, 0.05, 1)
        Line(circle=(cx, cy, r * 0.80, -30, 210), width=dp(2.0))

        label_data = [
            ("Calm", 210),
            ("Tremor", 150),
            ("Shake", 90),
            ("Jolt", 30),
            ("Surge", -30),
        ]
        label_font = max(sp(8), min(sp(13), r * 0.13))
        for text, angle_deg in label_data:
            a = angle_deg * pi / 180.0
            self._text(
                text,
                cx + cos(a) * r * 0.58,
                cy + sin(a) * r * 0.58,
                label_font,
                color=(0.18, 0.10, 0.04, 1),
                bold=True,
            )

        for i in range(17):
            angle_deg = 210 - i * 15
            a = angle_deg * pi / 180.0
            inner = r * (0.70 if i % 4 else 0.64)
            outer = r * 0.78
            Color(0.30, 0.18, 0.07, 1)
            Line(
                points=[
                    cx + cos(a) * inner,
                    cy + sin(a) * inner,
                    cx + cos(a) * outer,
                    cy + sin(a) * outer,
                ],
                width=dp(1.0 if i % 4 else 1.6),
            )

        pointer_angle = (210 - level * 240) * pi / 180.0
        Color(0.50, 0.05, 0.03, 1)
        Line(
            points=[
                cx,
                cy,
                cx + cos(pointer_angle) * r * 0.68,
                cy + sin(pointer_angle) * r * 0.68,
            ],
            width=dp(3.0),
            cap="round",
        )
        Color(0.20, 0.10, 0.04, 1)
        Ellipse(pos=(cx - r * 0.08, cy - r * 0.08), size=(r * 0.16, r * 0.16))

    def redraw(self, *args):
        self.canvas.clear()
        x, y = self.pos
        w, h = self.size
        if w < dp(220) or h < dp(180):
            return

        pad = dp(10)
        gap = dp(7)
        panel_x = x + pad
        panel_y = y + pad
        panel_w = w - pad * 2
        panel_h = h - pad * 2

        with self.canvas:
            Color(0.36, 0.23, 0.12, 1)
            Rectangle(pos=self.pos, size=self.size)

            self._panel(panel_x, panel_y, panel_w, panel_h)

            inner = dp(11)
            work_x = panel_x + inner
            work_y = panel_y + inner
            work_w = panel_w - inner * 2
            work_h = panel_h - inner * 2

            left_w = max(dp(94), min(dp(135), work_w * 0.30))
            center_w = max(dp(78), min(dp(118), work_w * 0.24))
            gauge_w = work_w - left_w - center_w - gap * 2
            if gauge_w < dp(110):
                left_w = work_w * 0.30
                center_w = work_w * 0.24
                gauge_w = work_w - left_w - center_w - gap * 2

            left_x = work_x
            center_x = left_x + left_w + gap
            gauge_x = center_x + center_w + gap

            self._panel(left_x, work_y, left_w, work_h)
            self._panel(center_x, work_y, center_w, work_h)
            self._panel(gauge_x, work_y, gauge_w, work_h)

            axis_r = max(dp(16), min(dp(24), min(left_w, work_h / 5.7)))
            axis_cx = left_x + left_w * 0.34
            axis_rod_x = left_x + left_w - dp(12)
            axis_ys = [
                work_y + work_h * 0.73,
                work_y + work_h * 0.50,
                work_y + work_h * 0.27,
            ]

            comb_cx = center_x + center_w * 0.50
            comb_cy = work_y + work_h * 0.50
            comb_r = max(dp(26), min(dp(42), min(center_w, work_h) * 0.26))
            pinion_r = max(dp(12), comb_r * 0.38)
            pinion_x = center_x + center_w * 0.22
            pinion_ys = [
                comb_cy + comb_r * 1.35,
                comb_cy,
                comb_cy - comb_r * 1.35,
            ]

            self._axis_input("X", self.axis_x, axis_cx, axis_ys[0], axis_r, axis_rod_x)
            self._axis_input("Y", self.axis_y, axis_cx, axis_ys[1], axis_r, axis_rod_x)
            self._axis_input("Z", self.axis_z, axis_cx, axis_ys[2], axis_r, axis_rod_x)

            for source_y, pinion_y in zip(axis_ys, pinion_ys):
                self._rod(axis_rod_x, source_y, pinion_x, pinion_y)
                self._rod(pinion_x + pinion_r * 0.8, pinion_y, comb_cx - comb_r * 0.62, comb_cy)

            phase = self.gear_phase
            self._gear(pinion_x, pinion_ys[0], pinion_r, 10, -phase * 1.7, abs(self.axis_x))
            self._gear(pinion_x, pinion_ys[1], pinion_r, 10, phase * 1.9, abs(self.axis_y))
            self._gear(pinion_x, pinion_ys[2], pinion_r, 10, -phase * 2.1, abs(self.axis_z))
            self._gear(comb_cx, comb_cy, comb_r, 18, phase, self.total_level)

            gauge_r = max(dp(42), min(gauge_w * 0.42, work_h * 0.36))
            gauge_cx = gauge_x + gauge_w * 0.52
            gauge_cy = work_y + work_h * 0.53
            crank_x = comb_cx + comb_r * 0.78
            crank_y = comb_cy + sin(phase) * comb_r * 0.32
            gauge_link_x = gauge_cx - gauge_r * 0.94
            gauge_link_y = gauge_cy + sin(phase + 1.2) * gauge_r * 0.18
            self._rod(comb_cx + comb_r, comb_cy, crank_x, crank_y)
            self._rod(crank_x, crank_y, gauge_link_x, gauge_link_y)

            Color(0.31, 0.18, 0.07, 1)
            Line(points=[gauge_link_x, gauge_link_y, gauge_cx - gauge_r * 0.72, gauge_cy], width=dp(3.0), cap="round")
            Color(0.84, 0.62, 0.24, 1)
            Line(points=[gauge_link_x, gauge_link_y, gauge_cx - gauge_r * 0.72, gauge_cy], width=dp(1.4), cap="round")

            self._gauge(gauge_cx, gauge_cy, gauge_r, self.total_level)


KV = """
#:import dp kivy.metrics.dp
#:import sp kivy.metrics.sp

ShakyLabRoot:
    orientation: "vertical"
    padding: dp(10)
    spacing: dp(8)
    canvas.before:
        Color:
            rgba: 0.25, 0.16, 0.08, 1
        Rectangle:
            pos: self.pos
            size: self.size

    Label:
        id: title_label
        text: "Shaky Lab"
        font_size: sp(28)
        bold: True
        color: 0.96, 0.83, 0.52, 1
        size_hint_y: None
        height: dp(40)


    MechanismDiagram:
        id: mechanism
        size_hint_y: 1

    Label:
        id: count_label
        text: "Samples recorded: 0"
        font_size: sp(18)
        bold: True
        color: 0.95, 0.82, 0.54, 1
        size_hint_y: None
        height: dp(32)

    Label:
        id: status_label
        text: "Ready. Press Start Recording."
        font_size: sp(15)
        color: 0.98, 0.88, 0.65, 1
        halign: "center"
        valign: "middle"
        text_size: self.size
        size_hint_y: None
        height: dp(44)

    GridLayout:
        cols: 3
        spacing: dp(8)
        size_hint_y: None
        height: dp(58)

        Button:
            id: start_button
            text: "Start Recording"
            font_size: sp(16)

        Button:
            id: stop_button
            text: "Stop Recording"
            font_size: sp(16)

        Button:
            id: reset_button
            text: "Reset"
            font_size: sp(16)
"""

try:
    shaky_lab_ui = Builder.load_string(KV)
    if shaky_lab_ui is None:
        shaky_lab_show_error("Could not load Shaky Lab interface.")
    else:
        def shaky_lab_set_status(text):
            shaky_lab_state["status"] = str(text)
            shaky_lab_ui.ids.status_label.text = str(text)

        def shaky_lab_update_count():
            shaky_lab_state["sample_count"] = len(samples)
            shaky_lab_ui.ids.count_label.text = f"Samples recorded: {len(samples)}"

        def shaky_lab_start_recording(instance):
            if not shaky_lab_state.get("enabled"):
                if not shaky_lab_enable_sensor():
                    shaky_lab_set_status("Accelerometer is unavailable.")
                    shaky_lab_show_error(shaky_lab_state.get("last_error") or "Accelerometer is unavailable.")
                    return
            shaky_lab_state["recording"] = True
            shaky_lab_state["previous_acceleration"] = shaky_lab_state.get("last_acceleration")
            shaky_lab_set_status("Recording connected x, y, and z motion into the main gauge.")

        def shaky_lab_stop_recording(instance):
            shaky_lab_state["recording"] = False
            shaky_lab_set_status("Recording stopped. The mechanism remains live.")

        def shaky_lab_reset(instance):
            samples.clear()
            shaky_lab_state["sample_count"] = 0
            shaky_lab_state["last_motion"] = 0.0
            shaky_lab_update_count()
            shaky_lab_ui.ids.mechanism.set_motion(0.0, 0.0, 0.0, 0.0, shaky_lab_ui.ids.mechanism.gear_phase)
            shaky_lab_set_status("Samples reset. Press Start Recording.")

        def shaky_lab_update(dt):
            try:
                accel = accelerometer.acceleration
                if accel is None:
                    shaky_lab_set_status("Waiting for accelerometer data.")
                    return

                x_val, y_val, z_val = accel
                x_val = 0.0 if x_val is None else float(x_val)
                y_val = 0.0 if y_val is None else float(y_val)
                z_val = 0.0 if z_val is None else float(z_val)

                previous = shaky_lab_state.get("previous_acceleration")
                if previous is None:
                    dx = dy = dz = 0.0
                else:
                    dx = x_val - previous[0]
                    dy = y_val - previous[1]
                    dz = z_val - previous[2]

                motion = sqrt(dx * dx + dy * dy + dz * dz)
                total_level = _clamp(motion / 5.0, 0.0, 1.0)
                axis_x = _clamp(dx / 3.0, -1.0, 1.0)
                axis_y = _clamp(dy / 3.0, -1.0, 1.0)
                axis_z = _clamp(dz / 3.0, -1.0, 1.0)

                shaky_lab_state["last_acceleration"] = (x_val, y_val, z_val)
                shaky_lab_state["previous_acceleration"] = (x_val, y_val, z_val)
                shaky_lab_state["last_motion"] = motion

                mechanism = shaky_lab_ui.ids.mechanism
                new_phase = (mechanism.gear_phase + 0.05 + total_level * 0.45) % (2 * pi)
                mechanism.set_motion(axis_x, axis_y, axis_z, total_level, new_phase)

                if shaky_lab_state.get("recording"):
                    samples.append(
                        {
                            "timestamp": time(),
                            "x": x_val,
                            "y": y_val,
                            "z": z_val,
                            "dx": dx,
                            "dy": dy,
                            "dz": dz,
                            "total_motion": motion,
                        }
                    )
                    shaky_lab_update_count()
            except Exception as exc:
                shaky_lab_state["recording"] = False
                shaky_lab_state["last_error"] = f"{type(exc).__name__}: {exc}"
                Logger.exception("PythonHere: Could not update Shaky Lab acceleration")
                shaky_lab_set_status("Sensor update failed. Recording stopped.")

        shaky_lab_ui.ids.start_button.bind(on_release=shaky_lab_start_recording)
        shaky_lab_ui.ids.stop_button.bind(on_release=shaky_lab_stop_recording)
        shaky_lab_ui.ids.reset_button.bind(on_release=shaky_lab_reset)

        shaky_lab_update_count()
        shaky_lab_lock_portrait()
        if shaky_lab_enable_sensor():
            shaky_lab_set_status("Ready. Press Start Recording.")
        else:
            shaky_lab_set_status("Accelerometer is unavailable on this device.")

        shaky_lab_update_event = Clock.schedule_interval(shaky_lab_update, 0.1)

        root.clear_widgets()
        root.add_widget(shaky_lab_ui)
except Exception as exc:
    shaky_lab_state["last_error"] = f"{type(exc).__name__}: {exc}"
    Logger.exception("PythonHere: Could not rebuild Shaky Lab")
    shaky_lab_show_error(shaky_lab_state["last_error"])
%there -b log
%%there
shaky_lab_update_event.cancel()

def shaky_lab_update(dt):
    try:
        accel = accelerometer.acceleration
        if accel is None:
            shaky_lab_set_status("Waiting for accelerometer data.")
            return

        x_val, y_val, z_val = accel
        x_val = 0.0 if x_val is None else float(x_val)
        y_val = 0.0 if y_val is None else float(y_val)
        z_val = 0.0 if z_val is None else float(z_val)

        previous = shaky_lab_state.get("previous_acceleration")
        if previous is None:
            dx = dy = dz = 0.0
        else:
            dx = x_val - previous[0]
            dy = y_val - previous[1]
            dz = z_val - previous[2]

        motion = sqrt(dx * dx + dy * dy + dz * dz)
        if motion < .2:
            motion = 0
        total_level = _clamp(motion / 5.0, 0.0, 1.0)
        axis_x = _clamp(dx / 3.0, -1.0, 1.0)
        axis_y = _clamp(dy / 3.0, -1.0, 1.0)
        axis_z = _clamp(dz / 3.0, -1.0, 1.0)

        shaky_lab_state["last_acceleration"] = (x_val, y_val, z_val)
        shaky_lab_state["previous_acceleration"] = (x_val, y_val, z_val)
        shaky_lab_state["last_motion"] = motion

        mechanism = shaky_lab_ui.ids.mechanism
        if motion > 0:
            new_phase = (mechanism.gear_phase + 0.05 + total_level * 0.45) % (2 * pi)
        else:
            new_phase = mechanism.gear_phase
        mechanism.set_motion(axis_x, axis_y, axis_z, total_level, new_phase)

        if shaky_lab_state.get("recording"):
            samples.append(
                {
                    "timestamp": time(),
                    "x": x_val,
                    "y": y_val,
                    "z": z_val,
                    "dx": dx,
                    "dy": dy,
                    "dz": dz,
                    "total_motion": motion,
                }
            )
            shaky_lab_update_count()
    except Exception as exc:
        shaky_lab_state["recording"] = False
        shaky_lab_state["last_error"] = f"{type(exc).__name__}: {exc}"
        Logger.exception("PythonHere: Could not update Shaky Lab acceleration")
        shaky_lab_set_status("Sensor update failed. Recording stopped.")

shaky_lab_update_event = Clock.schedule_interval(shaky_lab_update, 0.1)
samples = %there get samples
samples[0]
{'timestamp': 1781377266.3921585,
 'x': 0.9193734526634216,
 'y': -1.2258312702178955,
 'z': 9.710882186889648,
 'dx': 0.06703764200210571,
 'dy': 0.04788398742675781,
 'dz': 0.0766143798828125,
 'total_motion': 0}
import pandas as pd
df = pd.DataFrame(samples)
df["Seconds"] = df["timestamp"] - df["timestamp"].iloc[0]
df.plot(x="Seconds", y=["x", "y", "z", "total_motion"], figsize=(14, 5))
../../_images/11b237f583665e5df081eec74cd91bd7cddf5310ab8a21319074dc787a422940.png