PWM Slider icon PWM Slider — Slider to set EZ-B digital PWM for motor speed or LED brightness; shows duty-cycle, stop button, and board/port selection. Try it →
Asked
1 Participant
Smoothing jaw via WebSocket Client

Smoothing Jaw Via Websocket Client

I’ve got an EZ-InMoov Robot Head wired to an EZ-B over WiFi, with the jaw servo on D5 and neck pan/tilt on D0/D1. I’m feeding lip-sync amplitude from a local WebSocket server (ws://192.168.1.50:8080) into the WebSocket Client skill. The Response Variable is $ws_amp (0-100 text), and the Response Script maps that to a jaw angle. I also added a Slide Script named "Lip Smoothing" that writes into $lip_smooth (range 1-10) so I can tweak the easing live, and the Bonus Button snaps the jaw to neutral for testing.

The jaw moves, but it chatters like crazy and sometimes lags behind the audio. Debug shows the WebSocket pushing ~40-60 messages/sec, so the Response Script gets hammered. I tried ServoSpeed(D5, 2) and even Sleep(30) inside the Response Script, but that either doesn’t help or starves the socket and I start missing updates. Turning Debug off reduced CPU, but the jitter remains. I also tried casting to ints and clamping the range, plus a naïve blend toward target:

# WebSocket Client Response Script
$amp = ToInt($ws_amp)
$target = 40 + (($amp / 100) * 45)
$current = GetServo(D5)
if ($lip_smooth < 1)
  $lip_smooth = 1
endif
$next = $current + (($target - $current) / $lip_smooth)
Servo(D5, $next)

Neck servos on D0/D1 are fine because they’re not being spammed like the jaw. I’m thinking the right pattern is: let WebSocket Client only update a global target variable, and run a separate looping script at, say, 25-30 Hz that eases the servo toward that target using $lip_smooth from the Slide Script-but I’m unsure of the cleanest ARC way to wire that without blocking or racing the Response Script, so what’s the recommended approach to handle highfrequency WebSocket Client updates for a smooth D5 jaw on an EZInMoov using a Slide Script for smoothing?



Related Hardware (view all EZB hardware)
EZ-Inmoov Robot Head by EZ-Robot & Gael Langevin
EZ-InMoov Robot Head with Synthiam ARCx: face tracking, speech recognition, Talk Servo jaw motion and AI conversation
Wi-Fi
Servos 8
Camera
Audio
UART ✕
I2C
ADC 2
Digital 8

Related Robot Skills (view all robot skills)
Slide Script by Synthiam
Adjust a numeric variable via slider (0-1000) with customizable min/max, center button and scripts triggered on value change or bonus button press.
Websocket Client by Synthiam
WebSocket client for ARC: open/send messages, store server responses in a variable, run a response script, and track connection status.

ARC Pro

Upgrade to ARC Pro

Unleash your creativity with the power of easy robot programming using Synthiam ARC Pro

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#1  

Yes - the recommended ARC pattern is exactly what you described:

  • WebSocket Client should only store the latest target amplitude
  • A separate loop script should drive the jaw at a fixed rate
  • Don’t move the servo directly inside the WebSocket Response Script

That is the clean, proven way to handle high-frequency input in ARC. If you try to animate the servo inside the WebSocket callback, you’ll get exactly what you’re seeing: chatter, lag, and dropped responsiveness.

Why your current approach jitters

Your WebSocket is sending 40-60 updates/sec, which means the Response Script is being fired constantly. If that script:

  • reads servo position,
  • computes easing,
  • moves the servo,
  • and especially if it sleeps,

then you’re mixing network reception with motion control. That causes timing conflicts and backlog.

Better architecture in ARC

1) WebSocket Client Response Script

Make it do only this:

  • parse the amplitude
  • save it to a global variable like $jaw_target

Example:

var amp = parseInt(getVar("$ws_amp", "0"));
if (isNaN(amp)) amp = 0;

setVar("$jaw_target", amp);

If you want to clamp it:

var amp = parseInt(getVar("$ws_amp", "0"));
if (isNaN(amp)) amp = 0;

if (amp < 0) amp = 0;
if (amp > 100) amp = 100;

setVar("$jaw_target", amp);

2) Separate jaw loop script

Run a loop script from a dedicated skill button, init script, or startup script. This script should:

  • read $jaw_target
  • read $lip_smooth
  • smooth toward the target
  • update D5 at a fixed rate like 25-30 Hz
  • avoid blocking the WebSocket

Example JavaScript:

var jawPort = d5;
var minJaw = 40;
var maxJaw = 85;

while (true) {
  var amp = parseInt(getVar("$jaw_target", "0"));
  var smooth = parseInt(getVar("$lip_smooth", "5"));

  if (isNaN(amp)) amp = 0;
  if (isNaN(smooth) || smooth < 1) smooth = 1;

  if (amp < 0) amp = 0;
  if (amp > 100) amp = 100;

  var target = minJaw + ((amp / 100) * (maxJaw - minJaw));
  var current = Servo.getPosition(jawPort);

  if (current < 1) current = target;

  var next = current + ((target - current) / smooth);
  Servo.setPosition(jawPort, Math.round(next));

  sleep(33); // ~30 Hz
}

Important note

Your example uses GetServo(D5) and Servo(D5, next) - that’s not the preferred ARC approach. In ARC JavaScript, use the official API:

  • Servo.getPosition(d5)
  • Servo.setPosition(d5, value)

Also, don’t use Sleep(30) in a way that blocks the WebSocket response path. Use the separate loop instead.

Even better: add a deadband

To reduce chatter near small movements, only move the jaw if the difference is significant enough:

var jawPort = d5;
var minJaw = 40;
var maxJaw = 85;
var deadband = 1;

while (true) {
  var amp = parseInt(getVar("$jaw_target", "0"));
  var smooth = parseInt(getVar("$lip_smooth", "5"));

  if (isNaN(amp)) amp = 0;
  if (isNaN(smooth) || smooth < 1) smooth = 1;

  if (amp < 0) amp = 0;
  if (amp > 100) amp = 100;

  var target = minJaw + ((amp / 100) * (maxJaw - minJaw));
  var current = Servo.getPosition(jawPort);

  if (current < 1) current = target;

  var next = current + ((target - current) / smooth);

  if (Math.abs(next - current) >= deadband) {
    Servo.setPosition(jawPort, Math.round(next));
  }

  sleep(33);
}

WebSocket Client skill

  • Response Variable: $ws_amp
  • Response Script: update $jaw_target only

Slide Script skill

  • Variable: $lip_smooth
  • Range: 1 to 10
  • Bonus button: set jaw to neutral or set $jaw_target to 0

Separate script skill

Use a script skill to continuously animate the jaw.

If you want it even cleaner

You can also have the WebSocket script write a latest target only, and let the loop script do all servo math. That keeps the WebSocket handler extremely lightweight.

On your neck servos

You’re right that D0/D1 are fine because they aren’t being hit at the same rate. But if you later want head movement to also follow streaming data, use the same pattern: latest target in one script, motion update in another.

Best practice summary

  • Do not move servos directly in the WebSocket response
  • Do not sleep inside the response script
  • Store target values only
  • Use a timed loop script to interpolate toward the target
  • Use Slide Script for live smoothing control
  • Add a deadband to stop micro-jitter

One more question

What do you want the jaw behavior to feel like overall:

  • robotic and snappy
  • natural speech-like movement
  • or very smooth/cinematic

That changes the best update rate and smoothing math.

If you want, I can write you a complete ARC JavaScript solution using:

  • WebSocket Client response script
  • Slide Script integration
  • jaw loop script
  • neutral reset button
I am a customer support AI and still learning. Please verify important answers in our support documentation. For advanced AI features, use the AI Script Agent built into ARC.