Raspberry Pi RC Transmitter Phase 2 – Adding Centering, End Points and Expo Now that I have a very basic transmitter I can now make it more useful. With the PPM output fixed at 1.5mS centering and end points at 1.0mS and 2mS I would have to be really lucky if it works with my robots. I really need to be able to adjust the end points so I can adjust the throws on servos and max throttle levels on motors. It is also important to adjust the centering to be able to trim the channels. While I am at it I will also add channel reversing and expo.

To make all these values easy to edit they are all added as constants. The math to adjust the channel output is done on the PyGame Joystick values so are based on values from -1 to 1. This is easier to work on compared to the 1mS to 2mS values of the PPM function. Because negative values need to be handled differently than positive values to calculate the range of movement and expo an if function is used to determine how to handle the joystick value.

To also make things easier to setup in the future I have also added channel mapping as a set of nested lists at the top of the code. Each 2 value list includes a value to determine if the channel is a joystick axis, button or hat as well as the number of that axis, button or hat. The program will look up this list when reading the joystick values to assign the correct axis to the channel.

import pygame
import signal
import pigpio
import ppm

ppm_output = []

ppm_output_pin = 18									#Output pin for transmitter module

reverse = [1, 1, 1, 1, 1, 1, 1, 1]					#set to -1 if each channel is reversed

center = [0, 0, 0, 0, 0, 0, 0, 0]	                #center value for each channel

lower_end_point = [-1, -1, -1, -1, -1, -1, -1, -1]	#lower end point

upper_end_point = [1, 1, 1, 1, 1, 1, 1, 1]			#upper end point

expo = [1, 1, 1, 1, 1, 1, 1, 1]						#expo value. should be between 0.5 and 1 for negative expo and 1 to 2 for positive expo

#Channel assignment nested list. There are 8 pairs of values corresponding to each of the 8 channels.
#The first value is either an "a" for an axis, "b" for a button or a "h" for a hat. The second value corresponds to which axis, button or hat is selected.
channel_assignment = [[a, 3], [a, 4], [a, 1], [a, 0], [a, 5], [a, 2], [b, 0], [b, 2]]

pi = pigpio.pi()
pi.set_mode(ppm_output_pin, pigpio.OUTPUT)

# Define some colors
BLACK    = (   0,   0,   0)
WHITE    = ( 255, 255, 255)

if not pi.connected:
exit(0)

ppm = ppm.X(pi, ppm_output_pin, frame_ms=20)

# This is a simple class that will help us print to the screen
# It has nothing to do with the joysticks, just outputting the
# information.
class TextPrint:
def __init__(self):
self.reset()
self.font = pygame.font.Font(None, 20)

def print(self, screen, textString):
textBitmap = self.font.render(textString, True, BLACK)
screen.blit(textBitmap, [self.x, self.y])
self.y += self.line_height

def reset(self):
self.x = 10
self.y = 10
self.line_height = 15

def indent(self):
self.x += 10

def unindent(self):
self.x -= 10

def second_column(self):
self.x = 510
self.y = 10
self.line_height = 15

pygame.init()

# Set the width and height of the screen [width,height]
size = [1000, 700]
screen = pygame.display.set_mode(size)

pygame.display.set_caption("RC Controller")

#Loop until the user clicks the close button.
done = False

# Used to manage how fast the screen updates
clock = pygame.time.Clock()

# Initialize the joysticks
pygame.joystick.init()

textPrint = TextPrint()

# -------- Main Program Loop -----------
while done==False:

ppm_joystick_values = [0, 0, 0, 0, 0, 0, 0, 0]		#Raw values for the PyGame Joystick function ranging from -1 to 1
ppm_chanels_us = [0, 0, 0, 0, 0, 0, 0, 0]			#Processed Joystick values converted to micro-second value to be outputted to the PPM function
scaled_channel_value = [0, 0, 0, 0, 0, 0, 0, 0]		#A Joystick value that has been scalled according to the reversing, centering, end-point and expo values

# EVENT PROCESSING STEP
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT: # If user clicked close
done=True # Flag that we are done so we exit this loop

# Possible joystick actions: JOYAXISMOTION JOYBALLMOTION JOYBUTTONDOWN JOYBUTTONUP JOYHATMOTION
if event.type == pygame.JOYBUTTONDOWN:
print("Joystick button pressed.")
if event.type == pygame.JOYBUTTONUP:
print("Joystick button released.")

# DRAWING STEP
# First, clear the screen to white. Don't put other drawing commands
# above this, or they will be erased with this command.
screen.fill(WHITE)
textPrint.reset()

# Get count of joysticks
joystick_count = pygame.joystick.get_count()

textPrint.print(screen, "Number of joysticks: {}".format(joystick_count) )
textPrint.indent()

# For each joystick:
for i in range(joystick_count):
joystick = pygame.joystick.Joystick(i)
joystick.init()

textPrint.print(screen, "Joystick {}".format(i) )
textPrint.indent()

# Get the name from the OS for the controller/joystick
name = joystick.get_name()
textPrint.print(screen, "Joystick name: {}".format(name) )

# Usually axis run in pairs, up/down for one, and left/right for
# the other.
axes = joystick.get_numaxes()
textPrint.print(screen, "Number of axes: {}".format(axes) )
textPrint.indent()

for i in range( axes ):
axis = joystick.get_axis( i )
textPrint.print(screen, "Axis {} value: {:>6.3f}".format(i, axis) )
textPrint.unindent()

buttons = joystick.get_numbuttons()
textPrint.print(screen, "Number of buttons: {}".format(buttons) )
textPrint.indent()

for i in range( buttons ):
button = joystick.get_button( i )
textPrint.print(screen, "Button {:>2} value: {}".format(i,button) )
textPrint.unindent()

# Hat switch. All or nothing for direction, not like joysticks.
# Value comes back in an array.
hats = joystick.get_numhats()
textPrint.print(screen, "Number of hats: {}".format(hats) )
textPrint.indent()

for i in range( hats ):
hat = joystick.get_hat( i )
textPrint.print(screen, "Hat {} value: {}".format(i, str(hat)) )
textPrint.unindent()

textPrint.unindent()

#Read Joystick values based on the channel mapping
for i in range(ppm_joystick_values)
if channel_assignment[i] = 'a'
ppm_joystick_values[i] = joystick.get_axis(channel_assignment[i])

if channel_assignment[i] = 'b'
ppm_joystick_values[i] = joystick.get_button(channel_assignment[i])

if channel_assignment[i] = 'h'
ppm_joystick_values[i] = joystick.get_hat(channel_assignment[i])

textPrint.second_column()
textPrint.print(screen, "PPM values")
textPrint.indent()
for i in range(8):
textPrint.print(screen, "Channel {}: {}".format(i+1, ppm_joystick_values[i]))
textPrint.unindent()

textPrint.print(screen, "PPM channels in uS")
textPrint.indent()

for i in range(8):
ppm_joystick_values[i] = ppm_joystick_values[i] * reverse[i]                    #reverse the channel if needed

if ppm_joystick_values[i] < 0:
scaled_channel_value[i] = -abs(ppm_joystick_values[i]**expo[i]) * abs(lower_end_point[i] - center[i]) + center[i]    #Processes the negative joystick values

if ppm_joystick_values[i] > 0:
scaled_channel_values[i] = ppm_joystick_value[i]**expo[i] * (upper_end_point[i] - center[i]) + center[i]			       #Processes the positive joystick values

ppm_chanels_us[i] = int(round(1100 + (500 * scaled_channel_value[i])))
textPrint.print(screen, "Chanel {} in uS: {}".format(i+1, ppm_chanels_us[i]))
textPrint.unindent()

ppm.update_channels(ppm_chanels_us)

# ALL CODE TO DRAW SHOULD GO ABOVE THIS COMMENT

# Go ahead and update the screen with what we've drawn.
pygame.display.flip()

# Limit to 20 frames per second
clock.tick(20)

# Close the window and quit.
# If you forget this line, the program will 'hang'
# on exit if running from IDLE.
pygame.quit ()