Exploring the new Arduino/Genuino 101

Gyrating Helicopters

Next, I'll show how you can initialize and read from the 101's onboard gyroscope. Take a look at Listing 2. It might look complicated, but it is really the two libraries brought in on lines 1 and 2 that do the heavy lifting. CurieImu.h contains the Arduino 101's specific libraries and Madgwick.h contains, as explained above, the functions that help turn the gyroscope's input into usable data.

Listing 2

gyro.ino

01 #include "CurieImu.h"
02 #include "MadgwickAHRS.h"
03
04 Madgwick filter;
05
06 int gx, gy, gz;
07 int yaw;
08 int pitch;
09 int roll;
10 int factor = 800;
11
12 void setup() {
13   Serial.begin(9600);
14
15   CurieImu.initialize();
16
17   if (!CurieImu.testConnection()) {
18     Serial.println("CurieImu connection failed");
19   }
20
21   Serial.print("Starting Gyroscope calibration...");
22   CurieImu.autoCalibrateGyroOffset();
23   Serial.println(" Done");
24
25   Serial.println("Enabling Gyroscope offset compensation");
26   CurieImu.setGyroOffsetEnabled(true);
27 }
28
29 void loop() {
30   gx = CurieImu.getRotationX();
31   gy = CurieImu.getRotationY();
32   gz = CurieImu.getRotationZ();
33
34   filter.updateIMU(gx/factor, gy/factor, gz/factor, 0.0, 0.0, 0.0);
35
36   yaw = int(filter.getYaw()*100);
37   roll = int(filter.getRoll()*100);
38   pitch = int(filter.getPitch()*100);
39
40   Serial.print(yaw); Serial.print(" ");
41   Serial.print(roll);Serial.print(" ");
42   Serial.println(pitch);
43   delay(10);
44 }

Note that line 4 creates a Madgwick object called filter that is used on line 34 to convert the raw data incoming from the gyroscope. Lines 6 to 10 initialize some variables: gx, gy, gz will contain the data culled from the gyroscope, which will be processed and ultimately passed on into the yaw, pitch, and roll variables (see the "Yaw, Pitch, and Roll" box for details). Finally, the factor variable is used to dampen the gyroscope's sensitivity.

Yaw, Pitch, and Roll

Yaw, pitch, and roll (Figure 4) are the proper names of the axes around which a 3D object can rotate. If you think of a road as a flat 2D surface, a car only has yaw, meaning it can turn only left or right. An airplane, however, can move on three axes. It can turn left and right, rotating on its y-axis (yaw). It can dive or climb by dipping or rising its nose, rotating on its x-axis (pitch), and it can rotate along its z-axis, dipping one wing while raising the other (roll).

Figure 4: The three types of rotation on a 3D body. (CC BY-SA 3.0) [8]

This sketch's setup() first opens a Serial connection back to the computer on line 13. You're going to read the gyroscope's output over that channel later. After that, a connection is opened to the gyroscope on lines 17 to  19, and the gyroscope is calibrated on line  22. While this is happening, the 101 should be resting on a flat surface and pointing in the direction you rule as "front," because that is what the 101 is going to consider the starting position.

The gyroscope starts measuring as soon as its offset is enabled on line 26. Down in the loop() section, the first step is to read in the relative rotation of the three axes (lines 30 to  32). Note how the names of the CurieImu library are quite self-explanatory.

Next, the data is converted to something usable with the Madgwick functions (lines 34 to  38). Unless you want to get into the math and physics of how you filter inertial and inertial/magnetic sensor arrays [9], you'll just have to trust the Madgwick library on this one.

The last three parameters of the updateIMU() function, the 0.0s, would usually contain data incoming from the accelerometer. As you are only going to use the rotation data, you don't need them.

Then, the yaw, pitch, and roll data is pushed through the serial channel to the computer (lines 40 to 42). Each rotation value is separated with a space, because that's the easiest way to process the line when you read it from Python on the computer. Finally, the script pauses a bit at the end (line 43).

You can run this code without anything else. Plug in your 101, choose it from the Tools  | Board: drop-down menu, then upload the sketch to your Arduino (you can see the progress in the console at the bottom of the Arduino IDE) and wait about five seconds for the 101 to restart with the new sketch. Next, click on Tools | Serial Monitor (Figure 5). Pick up your Arduino and move it around. You should see the numbers change depending on the board's rotation.

Figure 5: Wave your 101 around and watch the rotation values change.

3D Pandas

Now, I'll get down to the task of showing the effects of rotating the 101 in a more visually appealing way.

As a Raspberry Pi user, you are probably familiar with Python. Although you can find several 3D engines, Panda3D is probably one of the most powerful ones for Python. Besides, it makes rendering and moving a 3D object around very simple (see the "From Blender to Panda" box for more info).

From Blender to Panda

You can of course write code that will render an object in Panda3D programmatically, but writing a program that generates anything more complex than a cube is a pain. Usually, you would create an object in a visual 3D editing application, such as Blender [10], save it in some standardized format, and then convert it to the EGG format Panda3D understands best. That is how the helicopter in this article was created.

You can download the original model online  [11]. The archive file comes with the mesh in several formats, one of which is for Blender. Open the file in Blender, delete all unnecessary elements (cameras, lights, floor, etc.) and make the chopper green, because … reasons.

Since Panda3D doesn't support importing directly from Blender, the documentation recommends saving the mesh as a DirectX object. Go to File | User Preferences and, in the dialog that opens, click the Import-Export button to narrow things down. Scroll down until you see the Import-Export DirectX X Format option. Click in the checkbox on the far right. If you're going to do this often, you might want to click on Save User Settings. Either way, when you're done, close the window.

Now you can save the mesh in the X format by choosing the File | Export | DirectX (.x) option and a name and place to put your file.

Panda3D comes with several handy tools to convert from format to format. The one you need to convert to the Panda3D EGG format is called (unsurprisingly) x2egg. So, using a terminal, cd into the directory were you saved your model, and type:

$ x2egg <modelname>.x > <newmodelname>.egg

The truth is, both .x and .egg files are plain text files, containing very similar information – namely, a list of three-number series that represent vertexes and instructions on how to apply textures, only in a slightly different order. All x2egg does is reformat the data for the egg.

Once you have your .egg file, you can import it into your program as shown in Listing 3.

Listing 3

copter.py

01 from direct.showbase.ShowBase import ShowBase
02 from direct.task import Task
03 from panda3d.core import *
04
05 import serial
06
07 class MyWindow(ShowBase):
08     def __init__(self):
09         self.ser = serial. Serial('/dev/ttyACM0', 9600)
10
11         ShowBase.__init__(self)
12
13         base.disableMouse()
14
15         alight = AmbientLight('alight')
16         alight.setColor(Vec4(0.4, 0.4, 0.4, 1))
17         alightNP = render.attachNewNode(alight)
18         render.setLight(alightNP)
19
20         plight = PointLight('plight')
21         plight.setColor(VBase4(1, 1, 1, 1))
22         plNP = render.attachNewNode(plight)
23         plNP.setPos(10, 10, 0)
24         render.setLight(plNP)
25
26         self.copter = self.loader.loadModel("0000_MyModels/helicopter.egg")
27         self.copter.setScale(1, 1, 1)
28         self.copter.setPos(0, 20, 0)
29         self.copter.setHpr(0, 0, 0);
30         self.copter.reparentTo(self.render)
31
32         self.taskMgr.add(self.rotate_copter, "Rotate Copter")
33
34     def get_YPR(self):
35         self.Hpr=[int(i) for i in self.ser.readline().split()]
36
37     def rotate_copter(self, task):
38         self.get_YPR()
39         self.copter.setHpr(self.Hpr[0],self.Hpr[1],self.Hpr[2]);
40         return Task.cont
41
42 if __name__ == '__main__':
43     win = MyWindow()
44     win.run()

As a proof of how easy Panda3D is, take a look at Listing 3. It is grand total 44 lines long, including empty lines, and it is all you need to read in the data from the Arduino and jiggle a 3D model of a helicopter around, depending on the rotation of the gyroscope (Figure 6).

Figure 6: Wave your 101 around and watch the chopper bank and spin!

To start, on lines 1 through 3, you bring in some Panda3D-specific modules. Because you have to show your 3D objects somewhere (e.g., a window), Panda3D provides a class that builds a window. The ShowBase module provides a framework for that. When ShowBase.__init__(self) is called on line 11 (i.e., when a ShowBase object is initialized), Panda3D automatically creates a window for your 3D objects as part of that process.

Next up, the serial module is imported on line 5. This is a Python module you may have to install from your distro's repository, but it will let you communicate easily with the Arduino 101 over the serial channel. On line 9, I create the serial object ser (note that you may have to modify this so it matches the port your 101 is assigned – look in your /dev directory), and this will be used later to read the input on line 35. The same line splits the input (which comes in as a string of characters, the data itself separated by spaces) into integers and dumps it into the object's Hpr attribute.

But I don't want to get too far ahead.

To create your 3D application/window, you can have the class inherit ShowBase (line  7) and then initialize the Panda3D window from within the initialization method of the child class (line 11).

The next thing to do is disable the mouse control in your window (line 13). With mouse control enabled (which is the default), you can use the mouse buttons and drag to zoom, rotate, and pan your 3D scene. Because you will be using the Arduino 101 to move things around, however, you don't want the mouse interfering, so you can zap it.

Next, you get to create two 3D objects: in this case, two lights. The first, alight, is an ambient light. An ambient light is like the light that bathes real life on a cloudy day. It doesn't seem to come from any particular point (so you don't have to indicate where it is located in the scene). In 3D, it is used to diffuse the shadows projected by other lights that otherwise would be unrealistically sharp. Line 16 sets the color of the light. The four parameters are the light's red, green, blue, and alpha components, so (0.4, 0.4, 0.4, 1) is a muted gray.

Line 17 attaches the light to the scene. Panda3D treats objects in a scene as children of the scene, which can also have their own children. In this case, alight is a child of the scene, so it illuminates all the objects contained within the scene. If it were the child of another object, it would illuminate only that object. Finally, line 18 tells the light to switch itself on by rendering it into the scene.

The block of code from lines 20 to 24 is very much the same, except it describes a point light, which is like a naked light bulb. One difference is that, for point lights, you can set the location (line 23).

At last, I come to the object you want to rotate. As explained in the "From Blender to Panda" box, you can create an object in a 3D editor, such as Blender, convert it to Panda3D's EGG format, and import it into the scene (as on line 26).

Line 27 sets the object's scale. Currently, it's the same size as when it was created. However, you can not only increment the scale of an object across the board but also stretch it or shrink it along any one of its axes. For reference, the x-axis in Panda3D is width (i.e., the axis running from left to right of the window); y is depth, going into the window; and z is height, from bottom to top of the window.

Line 28 sets the object's location in 3D space, in which (0, 0, 0) would be where the viewer is standing. Setting the helicopter's position at (0, 20, 0) places it in front of the viewer, a short distance away.

Line 29 sets the object's rotation. In Panda3D, yaw is referred to as head; hence, setHpr() sets the head/yaw, pitch, and roll in that order.

The last thing you do with the helicopter is reparent it to the scene to display it – that is, attach it as a node to the scene. This, as with the lights, renders the object.

Because Panda3D was designed with games in mind, it comes with a useful feature called tasks. Tasks are operations that you can execute once each frame. All you have to do is add a task to the task manager, and every frame it gets will run until you tell it to stop. You don't have to put it in a loop or anything.

Line 32 shows how this is done: Use the add module to the taskMgr with the name of the function you want to repeat (rotate_copter) and a human-readable name for the task ("Rotate Copter"). The task itself (lines 37 to  40) inherits from the Panda3D task object and, in this case, reads in the rotation values for the Arduino 101 (lines 38 and then 35) and applies them to the helicopter's head, pitch, and roll (line 39). By returning Task.cont (line  40), a Panda3D construct, you ensure the task is executed in the next frame.

You can download all the code for the programs in this article [12] and see the program in action in a video [13].

Buy this article as PDF

Express-Checkout as PDF

Pages: 8

Price $2.95
(incl. VAT)

Buy Raspberry Pi Geek

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content