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.
Show 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.
Show 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))