Raspberry Pi RC Transmitter Phase 2 – Adding Centering, End Points and Expo

Advertisements

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 threading
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)

pi.wave_tx_stop() # Start with a clean slate.

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()
    
# Get ready to print
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][0] = 'a'
                ppm_joystick_values[i] = joystick.get_axis(channel_assignment[i][1])
            
            if channel_assignment[i][0] = 'b'
                ppm_joystick_values[i] = joystick.get_button(channel_assignment[i][1])
              
            if channel_assignment[i][0] = 'h'
                ppm_joystick_values[i] = joystick.get_hat(channel_assignment[i][1])

        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 ()