In this post we show how to create a Digital Spirit Level using a Raspberry Pi and python.
The code moves that bubbles on the display in relation to the angle read from the IMU.
Parts used in this project;
Any IMU or TFT can be used, however the code would need to be updated to accommodate the different devices. It is best to use a 480×320 TFT as the images are scaled to fit this resolution.
This guide assumes that some basic understanding of an IMU(Accelerometer and Gyroscope) is already known. And you have one already working with your Raspberry Pi.
If you don’t, we do have some guides which covers this.
We have used our existing python code to read the values from the IMU, however we have removed the code related to the magnetometer as it isn’t needed for this project.
Git repository here
The code can be pulled down to your Raspberry Pi with;
Placement of IMU
The IMU can be attached anywhere, however it is best to place it in the same orientation as shown below. If you do change the orientation, you will need to update the code accordingly.
Pygame is used to display the images to the TFT, so the first task that needs to be done is to install pygame.
Now we can update our IMU code and tell it to use pygame to display images.
The next pieces of code can be added anywhere before the main loop.
Import the pygame framework into the code;
We then set our display mode to get a surface(screen) which we will draw to. And we also get the dimensions of the surface which will be used to tell pygame what area needs to be updated.
screen = pygame.display.set_mode ( ( 480,320 )) #Set display mode screen_rect = screen.get_rect() #Get the rectangular area of the screen Surface
We now can initialise pygame. However, before we do this we need to specify where we want the output displayed. This can be done by setting the SDL_FBDEV environment variable.
os.putenv('SDL_FBDEV', '/dev/fb1') #Set the display to fb1, which is the small TFT pygame.display.init() #Initialise pygame.
We then can load the images.
#Load images and convert to the correct format. Alpha is needed for PNGs to preserve alpha channel img_background = pygame.image.load ( "spirit-level-background.jpg" ).convert() img_Hbubble = pygame.image.load ( "spirit-level-Hbubble.png" ).convert_alpha() img_Vbubble = pygame.image.load ( "spirit-level-Vbubble.png" ).convert_alpha() img_Cbubble = pygame.image.load ( "spirit-level-Cbubble.png" ).convert_alpha() img_overlay = pygame.image.load ( "spirit-level-overlay.png" ).convert_alpha() #These images are used when only displaying the horizontal bar img_background_H_only = pygame.image.load ( "spirit-level-H-only-background.jpg" ).convert() img_Hbubble_H_only = pygame.image.load ( "spirit-level-H-only-bubble.png" ).convert_alpha() img_overlay_H_only = pygame.image.load ( "spirit-level-H-only-overlay.png" ).convert_alpha()
The mouse cursor is also disabled
pygame.mouse.set_visible(False) #Disable the mouse cursor
Show the images on the TFT
Showing the images on the screen can be done by blitting the images to the surface. Order is important as we want the background to actually be in the background.
The second values in the functions below are the X and Y coordinates of where we blit the images on the surface. Changing the values of hBposition, vBposition cBposition is how we move the bubbles.
screen.blit(img_background, screen_rect) screen.blit(img_Hbubble, hBposition) screen.blit(img_Vbubble, vBposition) screen.blit(img_Cbubble, cBposition) screen.blit(img_overlay, screen_rect)
And finally, we flip the surface so the TFT is updated
Moving the Bubbles!
We can easily move the bubbles using the angles returned from the IMU.
This can be done by rescaling the values from an angle, which are then used to move the bubbles on the display.
Lets look at the X angle which is represented on the display as the bottom horizontal bar in the image below.
The array hSlideLimit stores the left limit and right limit of where the bubble can move. These correspond to the X coordinates of the display. The horizontal bubble can only move between 62 and 252.
The X axis values from -90 to +90 from the IMU will be represented by the bottom bubble.
This means that we need to convert the range of;
-90 to +90 (0 is the center)
61 to 252 (95 is the center)
This can be done with the below function.
hSlideLimit = [62,252] angleLimit = 90 def scaleH(value): min = -angleLimit max = angleLimit minScale = hSlideLimit maxScale = hSlideLimit scaled = minScale + (value - min)/(max-min) * (maxScale - minScale) return scaled
min and max is the range we will accept from the IMU, which is between -90 and +90
micScale and maxScale are the values we want to convert them to.
The function then returns the new scaled value.
Here is the code we use to call the above function;
newHvalue = int(scaleH(CFangleX)) hBposition = (newHvalue,244) screen.blit(img_Hbubble, hBposition)
scaleH(). And we store the returned value in newHvalue.
We also type cast it as int as a floating point value will be returned and we need a whole number to plot to the display.
hBposition is updated with the new horizontal value. 244 never changes for this bubble as this is the Y position of the bubble on the display.
The bubble is then blitted to the display with the new coordinates.
Two different display modes
There are two different display modes.
If the angle of Y is larger than 70 or smaller than -70, we only display the large horizontal bar. Otherwise we show all three.
if CFangleY > 70 or CFangleY < -70:
Low Pass filter
We use a low pass filter to reduce noise. This will stop the bubbles from bouncing around, however it will add a little delay in the movement. This delay also makes the bubbles look more realistic when they move. The lower the low pass filter factor (LPF_FACT0R) the less noise and the larger the delay.
The filter gets applied to the raw values of the accelerometer and should be applied right after reading the values from the accelerometer.
#Apply low pass filter to reduce noise ACC_LPF_FACTOR = 0.05 ACCx = ACCx * ACC_LPF_FACTOR + oldXAccRawValue*(1 - ACC_LPF_FACTOR); ACCy = ACCy * ACC_LPF_FACTOR + oldYAccRawValue*(1 - ACC_LPF_FACTOR); ACCz = ACCz * ACC_LPF_FACTOR + oldZAccRawValue*(1 - ACC_LPF_FACTOR); oldXAccRawValue = ACCx; oldYAccRawValue = ACCy; oldZAccRawValue = ACCz;