Fix dance-loop tremor (set_target) and raspotify restart lockup (#1130)

#3
Files changed (1) hide show
  1. spotify_dancer/main.py +31 -3
spotify_dancer/main.py CHANGED
@@ -432,6 +432,23 @@ def check_and_fix_alsa_config():
432
  logger.info(f"Detected: speaker=card {speaker_card}, loopback=card {loopback_card}")
433
 
434
  config = generate_alsa_config(speaker_card, loopback_card)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  try:
436
  with open('/tmp/asound.conf', 'w') as f:
437
  f.write(config)
@@ -499,6 +516,12 @@ class SpotifyDancer(ReachyMiniApp):
499
  self.latest_features = self.analyzer.latest_features
500
  movement = self.controller.update(self.latest_features, dt)
501
 
 
 
 
 
 
 
502
  try:
503
  head_pose = create_head_pose(
504
  z=movement['head_z'],
@@ -506,12 +529,17 @@ class SpotifyDancer(ReachyMiniApp):
506
  mm=True,
507
  degrees=True
508
  )
509
- reachy_mini.goto_target(
 
 
 
 
 
 
 
510
  head=head_pose,
511
  antennas=[movement['antenna_left'], movement['antenna_right']],
512
  body_yaw=np.deg2rad(movement['body_yaw']),
513
- duration=0.1,
514
- method="minjerk"
515
  )
516
  except Exception as e:
517
  logger.debug(f"Movement error: {e}")
 
432
  logger.info(f"Detected: speaker=card {speaker_card}, loopback=card {loopback_card}")
433
 
434
  config = generate_alsa_config(speaker_card, loopback_card)
435
+
436
+ # Idempotency guard: only rewrite /etc/asound.conf and restart raspotify when
437
+ # the config actually changes. Restarting raspotify while the audio analyzer
438
+ # holds the ALSA loopback capture (loopback_in) wedges ALSA in an EINVAL state
439
+ # ("Slave PCM not usable") that survives until a reboot. The aplay probe above
440
+ # can fail transiently for that exact reason while the on-disk config is
441
+ # already correct, so a byte-identical config must not trigger a restart.
442
+ try:
443
+ with open('/etc/asound.conf', 'r') as f:
444
+ existing_config = f.read()
445
+ except OSError:
446
+ existing_config = None
447
+
448
+ if existing_config == config:
449
+ logger.info("ALSA config already up to date, skipping rewrite and restart")
450
+ return True
451
+
452
  try:
453
  with open('/tmp/asound.conf', 'w') as f:
454
  f.write(config)
 
516
  self.latest_features = self.analyzer.latest_features
517
  movement = self.controller.update(self.latest_features, dt)
518
 
519
+ # Deadzone: when there is effectively no audio, hold a neutral
520
+ # pose instead of reacting to numeric noise. Without this the
521
+ # robot twitches continuously even when nothing is playing.
522
+ if self.latest_features.rms < 0.02:
523
+ movement = {key: 0.0 for key in movement}
524
+
525
  try:
526
  head_pose = create_head_pose(
527
  z=movement['head_z'],
 
529
  mm=True,
530
  degrees=True
531
  )
532
+ # Use set_target (not goto_target) inside this ~20 Hz control
533
+ # loop. goto_target starts a fresh min-jerk profile from zero
534
+ # velocity on every call and blocks until it completes; calling
535
+ # it every 50 ms makes the profile restart 20x/s, producing
536
+ # visible head/antenna tremor independent of the music.
537
+ # AGENTS.md: set_target for loops >=10 Hz, goto_target only for
538
+ # one-shot gestures >=0.5 s.
539
+ reachy_mini.set_target(
540
  head=head_pose,
541
  antennas=[movement['antenna_left'], movement['antenna_right']],
542
  body_yaw=np.deg2rad(movement['body_yaw']),
 
 
543
  )
544
  except Exception as e:
545
  logger.debug(f"Movement error: {e}")