Agent Skills: QuackBot Duckoid

Mechanic wobbling duckoid robot that quacks and generates nonstandard musical scale compositions. Maximally cost-efficient design (~$68 BOM).

UncategorizedID: plurigrid/asi/quackbot-duckoid

Install this agent skill to your local

pnpm dlx add-skill https://github.com/plurigrid/asi/tree/HEAD/plugins/asi/skills/quackbot-duckoid

Skill Files

Browse the full folder contents for quackbot-duckoid.

Download Skill

Loading file tree…

plugins/asi/skills/quackbot-duckoid/SKILL.md

Skill Metadata

Name
quackbot-duckoid
Description
Mechanic wobbling duckoid robot that quacks and generates nonstandard musical scale compositions. Maximally cost-efficient design (~$68 BOM).

QuackBot Duckoid

Trit: +1 (PLUS - generative/constructive) Color: #1FC4E0 (Cyan) URI: skill://quackbot-duckoid#1FC4E0

Overview

A mechanic wobbling duckoid robot that:

  • πŸ¦† Quacks with piezo-synthesized duck calls
  • 🎡 Composes nonstandard musical scales (Bohlen-Pierce, just intonation, xenharmonic)
  • πŸŒ€ Wobbles via 2-DOF gimbal base with IMU feedback
  • πŸ’° Costs ~$68 total BOM

Physical Design

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”
                   β•±  O  O  β•²     ← LED eyes ($0.50)
                  β”‚  ══════  β”‚    ← Servo beak ($2)
                  β”‚   ))))   β”‚    ← Piezo speaker ($1)
                   β•²________β•±
                       β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
             β•±                   β•²
            β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
            β”‚    β”‚ ESP32-S3β”‚     β”‚   ← MCU ($8)
    Wing ──▢│    β”‚  +IMU   β”‚     │◀── Wing
   ($2ea)   β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚   ($2ea)
            β”‚     β”Œβ”€β”€β”€β”€β”€β”€β”€β”      β”‚
            β”‚     β”‚ LiPo  β”‚      β”‚   ← Battery ($12)
             β•²    β””β”€β”€β”€β”€β”€β”€β”€β”˜     β•±
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
              ╔════════╧════════╗
              β•‘   WOBBLE BASE   β•‘
              β•‘  β”Œβ”€β”€β”€β”   β”Œβ”€β”€β”€β”  β•‘   ← 2x MG996R ($10)
              β•‘  β”‚ S │───│ S β”‚  β•‘
              β•‘  β””β”€β”€β”€β”˜   β””β”€β”€β”€β”˜  β•‘
              β•šβ•β•β•β•β•β•β•β•β•€β•β•β•β•β•β•β•β•β•
                  ◯────┴────◯      ← Rubber feet ($2)

Bill of Materials (BOM)

| Component | Qty | Unit Cost | Total | |-----------|-----|-----------|-------| | Head | | | $15 | | SG90 Micro Servo (beak) | 1 | $2 | $2 | | Piezo Buzzer/Speaker | 1 | $1 | $1 | | 5mm LED (eyes) | 2 | $0.25 | $0.50 | | MPU6050 IMU | 1 | $3 | $3 | | 3D Print (head shell) | 1 | $8.50 | $8.50 | | Body | | | $25 | | ESP32-S3 DevKit | 1 | $8 | $8 | | PAM8403 Amplifier | 1 | $1 | $1 | | 2S LiPo 1000mAh | 1 | $12 | $12 | | PCA9685 PWM Driver | 1 | $4 | $4 | | Wobble Base | | | $20 | | MG996R Servo | 2 | $5 | $10 | | 3D Print (gimbal) | 1 | $5 | $5 | | Rubber Feet | 2 | $1 | $2 | | Brass Counterweight | 1 | $3 | $3 | | Wings | | | $8 | | SG90 Micro Servo | 2 | $2 | $4 | | 3D Print (wings) | 2 | $2 | $4 | | | | | | | TOTAL | | | $68 |

Nonstandard Musical Scales

QuackBot composes using xenharmonic scales that are not 12-TET:

Bohlen-Pierce Scale (13 steps per tritave)

Ratio:  1   25/21  9/7   7/5   5/3   9/5   15/7  7/3   25/9  3    ...
Step:   0    1      2     3     4     5     6     7     8    9    ...
Cents:  0   133.2  266.9 400.1 533.8 666.9 800.1 933.1 1066.9 1200

Just Intonation (Ptolemaic)

JUST_RATIOS = {
    'C': 1/1,      # Unison
    'D': 9/8,      # Major second
    'E': 5/4,      # Major third
    'F': 4/3,      # Perfect fourth
    'G': 3/2,      # Perfect fifth
    'A': 5/3,      # Major sixth
    'B': 15/8,     # Major seventh
}

Wendy Carlos Alpha/Beta/Gamma

CARLOS_ALPHA = 78.0   # cents per step (15.385 steps/octave)
CARLOS_BETA = 63.8    # cents per step (18.809 steps/octave)
CARLOS_GAMMA = 35.1   # cents per step (34.188 steps/octave)

Quack-Native Scale (custom)

A duck-optimized scale based on duck vocalization formants:

QUACK_SCALE = [
    1.0,      # Root quack
    1.059,    # Minor second quack
    1.189,    # Minor third quack  
    1.335,    # Fourth quack
    1.498,    # Fifth quack
    1.682,    # Sixth quack
    1.888,    # Seventh quack
]
# Formant frequencies: F1=1200Hz, F2=2400Hz, F3=3600Hz

Wobble Dynamics

The 2-DOF gimbal creates a chaotic wobble pattern using coupled oscillators:

import numpy as np

class WobbleDynamics:
    """Kuramoto-style coupled oscillator for duck wobble."""
    
    def __init__(self, natural_freq=2.0, coupling=0.5):
        self.omega = natural_freq  # rad/s
        self.K = coupling
        self.theta = [0.0, np.pi/4]  # pitch, roll phases
        
    def step(self, dt: float, imu_feedback: tuple) -> tuple:
        """Compute next wobble angles."""
        pitch_accel, roll_accel = imu_feedback
        
        # Kuramoto coupling
        phase_diff = self.theta[1] - self.theta[0]
        
        # Update phases with coupling and feedback
        self.theta[0] += dt * (
            self.omega + 
            self.K * np.sin(phase_diff) + 
            0.1 * pitch_accel
        )
        self.theta[1] += dt * (
            self.omega * 1.1 +  # Slight detuning
            self.K * np.sin(-phase_diff) + 
            0.1 * roll_accel
        )
        
        # Convert to servo angles (Β±30Β°)
        pitch_angle = 30 * np.sin(self.theta[0])
        roll_angle = 30 * np.sin(self.theta[1])
        
        return pitch_angle, roll_angle


class QuackSynthesizer:
    """Duck vocalization synthesizer."""
    
    FORMANTS = [1200, 2400, 3600]  # Hz
    
    def quack(self, duration_ms: int = 200, pitch_shift: float = 1.0):
        """Generate a quack waveform."""
        sr = 16000
        t = np.linspace(0, duration_ms/1000, int(sr * duration_ms/1000))
        
        # Fundamental with pitch shift
        f0 = 220 * pitch_shift
        
        # Sum formants
        signal = np.zeros_like(t)
        for i, formant in enumerate(self.FORMANTS):
            amp = 1.0 / (i + 1)  # Decreasing amplitude
            signal += amp * np.sin(2 * np.pi * formant * pitch_shift * t)
        
        # Apply envelope (sharp attack, quick decay)
        envelope = np.exp(-t * 20) * (1 - np.exp(-t * 100))
        
        return (signal * envelope * 32767).astype(np.int16)
    
    def compose_nonstandard(self, scale: list, pattern: list) -> list:
        """Compose using nonstandard scale."""
        quacks = []
        for note_idx, duration in pattern:
            pitch = scale[note_idx % len(scale)]
            quacks.append(self.quack(duration, pitch))
        return quacks

MJCF Model

<mujoco model="quackbot">
  <compiler angle="radian" meshdir="meshes"/>
  
  <default>
    <joint damping="0.1" armature="0.01"/>
    <geom friction="0.8 0.02 0.01"/>
  </default>
  
  <asset>
    <mesh name="body" file="duck_body.stl"/>
    <mesh name="head" file="duck_head.stl"/>
    <mesh name="beak" file="duck_beak.stl"/>
    <mesh name="wing" file="duck_wing.stl"/>
  </asset>
  
  <worldbody>
    <!-- Wobble Base -->
    <body name="base" pos="0 0 0.05">
      <geom type="cylinder" size="0.08 0.02" rgba="0.2 0.2 0.2 1"/>
      
      <!-- Pitch Joint -->
      <body name="pitch_link" pos="0 0 0.03">
        <joint name="pitch" type="hinge" axis="0 1 0" range="-0.5 0.5"/>
        
        <!-- Roll Joint -->
        <body name="roll_link" pos="0 0 0.02">
          <joint name="roll" type="hinge" axis="1 0 0" range="-0.5 0.5"/>
          
          <!-- Duck Body -->
          <body name="body" pos="0 0 0.08">
            <geom type="mesh" mesh="body" rgba="1 0.9 0 1"/>
            <inertial pos="0 0 0" mass="0.3" diaginertia="0.001 0.001 0.001"/>
            
            <!-- Head -->
            <body name="head" pos="0 0.06 0.05">
              <geom type="mesh" mesh="head" rgba="1 0.9 0 1"/>
              
              <!-- Beak -->
              <body name="beak" pos="0 0.03 0">
                <joint name="beak" type="hinge" axis="1 0 0" range="0 0.3"/>
                <geom type="mesh" mesh="beak" rgba="1 0.5 0 1"/>
              </body>
            </body>
            
            <!-- Left Wing -->
            <body name="wing_l" pos="-0.06 0 0.02">
              <joint name="wing_l" type="hinge" axis="0 1 0" range="-0.5 0.5"/>
              <geom type="mesh" mesh="wing" rgba="1 0.9 0 1"/>
            </body>
            
            <!-- Right Wing -->
            <body name="wing_r" pos="0.06 0 0.02">
              <joint name="wing_r" type="hinge" axis="0 1 0" range="-0.5 0.5"/>
              <geom type="mesh" mesh="wing" rgba="1 0.9 0 1" euler="0 0 3.14"/>
            </body>
          </body>
        </body>
      </body>
    </body>
  </worldbody>
  
  <actuator>
    <position name="pitch_servo" joint="pitch" kp="50"/>
    <position name="roll_servo" joint="roll" kp="50"/>
    <position name="beak_servo" joint="beak" kp="20"/>
    <position name="wing_l_servo" joint="wing_l" kp="10"/>
    <position name="wing_r_servo" joint="wing_r" kp="10"/>
  </actuator>
</mujoco>

ESP32 Firmware Skeleton

#include <ESP32Servo.h>
#include <driver/i2s.h>
#include <MPU6050.h>

// Servos
Servo pitchServo, rollServo, beakServo, wingLServo, wingRServo;

// IMU
MPU6050 imu;

// Wobble state
float theta[2] = {0, M_PI/4};
const float omega = 2.0;
const float K = 0.5;

void setup() {
    // Attach servos
    pitchServo.attach(13);
    rollServo.attach(12);
    beakServo.attach(14);
    wingLServo.attach(27);
    wingRServo.attach(26);
    
    // Init IMU
    Wire.begin();
    imu.initialize();
    
    // Init I2S for audio
    i2s_config_t i2s_config = {
        .mode = I2S_MODE_MASTER | I2S_MODE_TX,
        .sample_rate = 16000,
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
        .communication_format = I2S_COMM_FORMAT_I2S,
        .dma_buf_count = 8,
        .dma_buf_len = 64,
    };
    i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
}

void loop() {
    // Read IMU
    int16_t ax, ay, az, gx, gy, gz;
    imu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
    
    // Wobble dynamics (Kuramoto)
    float dt = 0.02;
    float phase_diff = theta[1] - theta[0];
    theta[0] += dt * (omega + K * sin(phase_diff) + 0.0001 * ax);
    theta[1] += dt * (omega * 1.1 + K * sin(-phase_diff) + 0.0001 * ay);
    
    // Servo angles
    int pitch = 90 + 30 * sin(theta[0]);
    int roll = 90 + 30 * sin(theta[1]);
    
    pitchServo.write(pitch);
    rollServo.write(roll);
    
    // Occasional quack
    if (random(100) < 2) {
        quack();
    }
    
    // Wing flap (sync with wobble)
    wingLServo.write(90 + 20 * sin(theta[0] * 2));
    wingRServo.write(90 - 20 * sin(theta[0] * 2));
    
    delay(20);
}

void quack() {
    // Generate quack waveform
    int16_t buffer[800];
    for (int i = 0; i < 800; i++) {
        float t = i / 16000.0;
        float env = exp(-t * 20) * (1 - exp(-t * 100));
        float sig = sin(2 * M_PI * 1200 * t) + 
                    0.5 * sin(2 * M_PI * 2400 * t) +
                    0.25 * sin(2 * M_PI * 3600 * t);
        buffer[i] = (int16_t)(sig * env * 16000);
    }
    
    // Beak animation
    beakServo.write(120);
    
    size_t bytes_written;
    i2s_write(I2S_NUM_0, buffer, sizeof(buffer), &bytes_written, portMAX_DELAY);
    
    beakServo.write(90);
}

GF(3) Triads

quackbot-duckoid (+1) βŠ— wobble-dynamics (0) βŠ— nonstandard-scales (+1) = needs -1
quackbot-duckoid (+1) βŠ— topos-of-music (-1) βŠ— wobble-dynamics (0) = 0 βœ“
quackbot-duckoid (+1) βŠ— ksim-rl (-1) βŠ— mujoco-scenes (0) = 0 βœ“

Related Skills

  • topos-of-music (-1): Mazzola's mathematical music theory
  • wobble-dynamics (0): Kuramoto oscillators for motion
  • nonstandard-scales (+1): Xenharmonic scale generation
  • ksim-rl (-1): RL training for behaviors
  • catsharp-sonification (0): GF(3) color β†’ sound mapping

Cost Optimization Strategies

  1. Use SG90 over MG996R where torque permits (saves $6)
  2. ESP32-C3 instead of S3 if BLE sufficient (saves $3)
  3. Passive piezo instead of active speaker (saves $0.50)
  4. Shared 3D print batch for multiple units (saves 30%)

Minimum viable BOM: $52

References

@article{bohlen1978,
  title={13 Tonstufen in der Duodezime},
  author={Bohlen, Heinz},
  journal={Acustica},
  year={1978}
}

@book{carlos1987,
  title={Tuning: At the Crossroads},
  author={Carlos, Wendy},
  year={1987}
}

@article{kuramoto1975,
  title={Self-entrainment of a population of coupled oscillators},
  author={Kuramoto, Yoshiki},
  journal={Lecture Notes in Physics},
  year={1975}
}