JetBot Teleop w/Flight Yoke issue >> How to implement observer pattern?

I have built a JetBot that is functioning 100% correctly in the TeleOp mode with a gamepad.
I want to change the HID from gamepad ‘Tank Drive’ to a Flight Yoke.

To do this, I have taken the Y-axis (pitch) from the yoke as throttle input, and take the X-axis (roll) as the steering input and apply some math to get the left drive motor and right drive motor control values. The math in the code produces the expected result, and is not the center of my concern.

The issue is in getting the signal back out to the motors. Currently, it only takes the inputs at the moment the code was instantiated and sets the motor outputs one time. I believe the observe function is the answer (yes, no?), but have not been able to implement the Observe pattern to keep the values constantly updating. I have tried many things, but cannot get it to function at all…

Code below in three blocks.
This is the cleanest version I have.
Please help me rewrite the code in block 3 for full function…

Second issue (minor) is that Code block 2 cannot be inserted into Code block 1. It errors out. How can this be addressed? Is there something like a pause or delay command that would work here?

#### Forum question << code block 1 >> ####
# initialize environment
import ipywidgets.widgets as widgets
from jetbot import Robot
import traitlets
from IPython.display import display
from traitlets import HasTraits, Float, Int, Unicode

# create Fader class
class Fader (HasTraits): value = Float() 

# "create" a Robot    
robot = Robot()

# create a gamepad controller display    
controller = widgets.Controller(index=0)  # replace with index of your controller
display(controller)

# create four sliders with range [-1.0, 1.0]
#####  These sliders move with the input, because they represent the output of the controller ####
throttle_slider = widgets.FloatSlider(description='throttle', min=-1.0, max=1.0, step=0.01, orientation='vertical')
steering_slider = widgets.FloatSlider(description='steering', min=-1.0, max=1.0, step=0.01, orientation='horizontal')

#### These sliders are get set ONE TIME when code block three is run.  This is the problem I am trying to solve. ####
left_motor_slider = widgets.FloatSlider(description='left motor', min=-1.0, max=1.0, step=0.01, orientation='vertical')
right_motor_slider = widgets.FloatSlider(description='right motor', min=-1.0, max=1.0, step=0.01, orientation='vertical')

slider_container = widgets.HBox([throttle_slider, steering_slider, left_motor_slider, right_motor_slider])
display(slider_container)
##### Forum question << end of code block 1 >> ####

##### Forum question << code block 2 >> ####
# wait for displays to render before running this block
# connect gamepad to slider display
left_link = traitlets.dlink((controller.axes[1], 'value'), (throttle_slider, 'value'), transform=lambda x: -x) # makes slider right side up
right_link = traitlets.dlink((controller.axes[2], 'value'), (steering_slider, 'value'))
##### Forum question << end of code block 2 >> ####


#### Forum question << codeblock 3 >> ####
#Run this block to calculate left and right motor speed outputs from throttle and direction inputs
#Run this block multiple times with different joystick positions (faders 1 and 2) to see the calculations working as designed in the left motor and right motor sliders above.

# translates throttle, steering inputs to left-right motor outputs
leftfun  = lambda a,b: -a+b
rightfun = lambda a,b: -a-b
yvar = rightfun(controller.axes[1].value,controller.axes[2].value)
xvar = leftfun(controller.axes[1].value,controller.axes[2].value)
print ((xvar), "x var")  # Print to see if values are being summed properly
print ((yvar), "y var")

#limits values between b (1) and c (-1)
posfun  = lambda a,b,c: (b if a>b else (c if a < c else a ))
leftspeedVar  = posfun(xvar,1.0,-1.0)
rightspeedVar = posfun(yvar,1.0,-1.0)
print ((leftspeedVar), "left speed var")  #Print to see if values are being limited properly
print ((rightspeedVar), "right speed var")

# Convert (to Fader Class with 'HasTraits') formatted for dlink function to work
fader_Left = Fader(value= leftspeedVar)
fader_Right = Fader(value= rightspeedVar)
print ((fader_Left.value),"fader left")  #Print to see if fader values are being set properly
print ((fader_Right.value),"fader right")

#send left and right drive motor values to fader widget
left_fun_link = traitlets.dlink((fader_Left, 'value'), (left_motor_slider, 'value'))
right_fun_link = traitlets.dlink((fader_Right, 'value'), ( right_motor_slider, 'value'))
Forum question << end of code block 3 >>

Hi dbreggin,

Thanks for reaching out!

If the intent of this code block is to convert something like (steering, throttle) → (left value, right value). Perhaps you could attach a callback to both controller axes which does this.

def on_gamepad(change):
    left_motor_slider = leftfun(controller.axes[1].value, controller.axes[2].value)
    right_motor_slider = rightfun(controller.axes[1].value, controller.axes[2].value)

controller.axes[1].observe(on_gamepad, names='value')
controller.axes[2].observe(on_gamepad, names='value')

By calling observe on either axis, the on_gamepad function will be called for a controller change on either axis. It will then look up the controller axis values, transform them to the desired values, and send them to the motor.

I haven’t tested this, so I’m not 100% it will work as intended, but may be worth a shot.

Please let me know if this helps, or you have any questions.

Best,
John

1 Like

I’ll give it a try and see what happens.

Jaybdub, you had the right answer.

The rest of my code was not in sequential order, so it would not run recursively.
Once I fixed that, your solution worked perfectly!

Thank you so much!!!