This project showcases a full-sized air hockey robot controlled by a MinnowBoard Max running Windows 10 IoT Core. The goal of the this project was to prove out the real time properties of the IoT Core OS. We chose the air hockey table due to the requirements involved in tracking a moving object across a plane and responding appropriately. The robot uses a color blob tracking camera to detect the location of the puck on a table surface, and uses stepper motors to move a striker mallet to intercept the puck. Additional sensors help detect goals and the XY mallet position. All of these devices hook into a MinnowBoard Max, which is responsible for taking in puck position data and calculating motor movements in response.
The code for this project can be found here: https://github.com/Microsoft/Windows-iotcore-samples/tree/develop/Demos/AirHockeyRobot
The aluminum frame serves as a foundation for the rest of the robot, and provide rigid mounting points for all the additional hardware. It’s built using using using 8020 T-slotted aluminum extrusion, which makes it relativity quick and easy to assemble. A total of eleven 2x2” 2020 profile aluminum beams are used in the construction:
- 4x 35” 2020 beams - These function as legs for the frame. They stand on the ground to hold up the weight of the frame
- 2x 64” 2020 beams - These beams rise vertically to create a overhead mounting point for the puck tracking camera (detailed in the Electrical Assembly section)
- 2x 96” 2020 beams - These are the long horizontal beams the run the length of the table
- 3x 54” 2020 beams - These beams serve to link the frame across the short axis of the table
Note that the beam lengths here worked well for our specific air hockey table. If you’re building this yourself, these lengths will need to be adjusted for your specific air hockey table.
Start by joining together two 35” leg beams with one 54” crossbeam to form an H-shaped assembly (red). You’ll create two of these, one for each end of the table. The beams are bolted to each other using two L-brackets and 16 fasteners as shown in the last image.
Next, take the two 96” horizontal beams, and tentatively install two corner brackets into the center of each beam (magenta). These will be used in the next step when assembling the overhead beams. Now stand up both H assemblies and attach the two 96” horizontal beams (green). Join the beams to the frame legs using L-bracket configuration shown in the last image.
Now, attach two 64” beams (yellow) vertically to the previously installed corner brackets (magenta). The tops of the beams should be approximately 4ft above the surface of the table. Finally, attach the top 54” crossbeam (blue) with corner bracket as shown.
Assembly of the robot starts with the Y-axis. This section of the assembly requires a large number of 3D printed and laser cut parts, so make sure to have the parts printed/laser cut and ready to go.
Assembling the Y-axis motor mountsStart by mounting the stepper motor to the larger of the two Y Axis Mounting Plates using four M5 screws, nuts, and spacers. Next, attach a pulley to the motor shaft. Apply threadlocker compound to the set screw and tighten. The T-shaped slot in the mounting plate allows insertion of a hex wrench to tighten the set screw. Use 1/4” nylon spacers (2x 1” long and 1x 1/2” long) and 1/4” screws to attach bottom mounting plate and loosely affix entire assembly to the aluminum rails. Notice that the bevelled edges of the mounting plates point towards the same direction that the drive belt will go. Don’t tighten the assembly to the rails too much for now, as we’ll need to move the assembly to tension the drive belt later on. Repeat the assembly for the other side of the table.
The Y-axis guide rails allows the mallet carriage assembly to move along the Y-axis of the table. Take a 12MM steel shaft and slide a linear bearing into the center of the shaft (This is important!
The linear bearing will be used in a later step). Next, slide a Y Axis Rail Bumper, spring, and Shaft Support onto both ends of the shaft. Use a rubber band to tie the bumpers to the Shaft Support. The bumpers and springs ensure that the carriage doesn’t ram into the end of the rail at high speed. They were very helpful in preventing damage during development of the robot game algorithm. Attach both Shaft Supports to the aluminum beams with four T-nuts each. Repeat assembly for the second half of the table.
Start by measuring the distance between the two 96” aluminum beams. Take note of this spacing distance, as we’ll use it again in a later step. Use a Dremel tool or saw to cut down the 1/4” D-shaft to the measured length. This shaft will link the two Y-axis motors on opposite sides of the table, and ensure that they rotate together. Now slide two bearings, shaft collars, and Shaft Guide Plates into the middle of the shaft. Notice that the flange on the bearing should be the side on which you install the collars.
Next, grab 2 bearings, 1 pulley, and the Y-axis Pulley Plates. Use 1/4” screws, T-nuts, and nylon spacers (2x 1” long and 1x 1/2” long) to construct the pulley assembly shown. Make sure to apply threadlocker to the pulley set screw and tighten. Repeat this assembly for both ends of the shaft.
Once both sides have been assembled, you can tighten the pulley mounts to the aluminum frame. Finally, install the 48” 1010 aluminum beam parallel with the shaft. Use the 1010 inside corner brackets to affix the Shaft Guide Plates to the 1010 aluminum beam. Similarly, use corner brackets to affix the ends of the 1010 aluminum beam to the frame.
Cut two 8 ft segments of timing belt, and make sure both are identical in length. Loop each segment around the motor and pulley on each end of the Y-axis assembly. Take a Y Carriage Belt Plate and a Y Carriage Belt Plate Back, and sandwich the cut ends of the timing belt between them. Tighten the two plates together using 6x 6-32 screws and nuts. Repeat for the other timing belt. The belt should still be loose at this point, since the Y-axis motor mounts were previously left untightened. We’ll tighten the belts later.
Take the 3D printed X-axis carriage and attach two linear bearings to each side with four M5 screws each. You’ll need to use hex screws and wrenches here, since a screwdriver cannot effectively reach into the carriage.
Take the beam spacing distance you noted earlier when installing the Y-axis pulley mount and subtract 5.7 inches from this measurement. This is the length your carbon shafts should be cut down to. Use a dremel tool to cut the shafts to the correct length, and be careful to avoid inhaling the carbon dust generated from cutting. Slide both shafts into the X-axis carriage assembly. Next, slide a X-Axis Rail Bumper and spring onto each end of the carbon shafts. Take the 3D printed Y Carriage Motor Mountand Y Carriage Pulley Mount, and slide them over the ends of the carbon shafts.
We now have the full X-axis assembly, and can install this onto the Y-axis rails. Take the assembly, and screw the Y Carriage Motor Mount and Y Carriage Pulley Mountinto the linear bearings previously installed on the Y-axis rails with 4x M5 screws each. When this is complete, the X-axis carriage should be able to slide freely along both axis. Now take 4x 1/4” 3.5” bolts, 4x 1/4” nuts, and 4x 1/4” 1” nylon spacers. Use these to link the Y Carriage Motor Mount and Y Carriage Pulley Mount with the respective Y Carriage Belt Plates.
We can now tension the Y-axis timing belt. The Y Carriage Pulley Mounts should already be tightened to the end of the aluminum frame. However, the Y Carriage Motor Mounts should only be loosely screwed to the frame. Make sure the X-axis assembly is completely perpendicular to the Y-axis rails, then pull the Y Carriage Motor Mounts along the aluminum beam to tighten the belts. Once the belt is sufficiently tight, tighten the motor mount into the frame. Repeat for the second motor mount. Test the assembly by making sure the Y-carriage slides smoothly along the rails, and make adjustments as necessary.
We’re almost done now, and can finish the assembly of the X-axis. Take the Y Carriage Pulley Plates, 2 bearings, 2 shaft collars, 1 3” shaft, and 1 pulley. Assemble the pulley as shown below. 8-32 screws and 1/2” standoffs are used in the four corners.
Take the X-axis stepper motor, and attach tighten a pulley to the shaft using thread lock compound. Use 4x M5 screws and nuts to screw it into the Y Carriage Motor Mount. Use spacers/washers/nuts to align the motor pulley to the same height as the pulley on the opposite side. Run a timing belt through the pulleys, and cut it down the length so the ends meet in the middle at the X Axis Carriage. Pull the belt taut and clamp it down with the X Axis Carriage Belt Plate.
Finally, take the 3D printed Mallet and 4x 4” long 1/4” screws. Insert the bolts through the carriage and into the mallet. Use nuts to tighten the bolts to the carriage. Note that mallet itself here isn’t screwed into anything. It simply rests on the surface of the table with bolts keeping it in position along the XY plane.
The main components of the electrical assembly include:
- MinnowBoard Max
- TouchScreen HDMI Monitor
- CMUCam Pixy 5 Camera
- Stepper motors and controllers
- Limit Switches
- Emergency stop button
- 48V 7.3A Switching Power Supply - Provides power to the motor controllers
- IR Beam Break sensors for detecting goals
At a high level, here’s how the various components are hooked up.
Notes:
- Notice that the Y1 and Y2 Steppers have their motor phases flipped so they turn in opposite directions. When installed on opposite sides of the table, this ensures they work in tandem to move mallet.
- Make sure to set the selector switches on the motor controller correctly: For the X axis controller: For both Y axis controllers:
- Due to the noise from the motor controllers, try to physically route wires away from them. Use shielded wires to minimize interference.
- For the limit switches, the 100Ω resistors combined with the 100nF capacitors form a low-pass network which helps filter high-frequency noise from the motors. Internal pull-up resistors bias the line high until the switches are triggered.
To avoid a mess of wires and parts, we mounted the components to the table frame where possible with laser-cut acrylic or 3D printed parts.
Additionally, wires were organized with a combination of zip-ties, cable spiral wrap, and binder clips. Soldered connections were kept clean and insulated with heatshrink tubing.
Electrical Assembly: MinnowBoard and Motor ControllersInstalling the emergency stop buttonStart by installing the emergency stop button. This button gates power to the motor power supply, and makes it easy to shut off power in case something goes wrong. We drilled a 7/8” inch hole into the top of a plastic project enclosure, allowing the button to be screwed onto the box. Two holes were then drilled into the sides to let the the power cable pass through. Additional 1/4” holes were drilled into the bottom for mounting to the aluminium frame.
To make it easier to organize all the wires coming off of the MinnowBoard, we designed the laser cut MinnowBoard Mount Plate for securing the MinnowBoard along with 3 perfbords. These perfboards provide a location to solder the various sensor connections. The plate is affixed to the aluminum frame with a few spacers and 1/4 screws. Laser cut acrylic bars near the perfboards are used to help clamp the wires in place and prevent them from wiggling loose. A few #6 standoffs are used to secure the MinnowBoard to the mounting plate.
The motor power supply is affixed to the frame right underneath the MinnowBoard. Use the PSU Mount Plate to affix the power supply to the frame. From here, use crimped wires to connect up the three separate motor controllers with power. This particular power supply has a voltage adjust potentiometer, which we used adjust the output to 45V. The provide a little bit of headroom for the capacitors and motor controllers are rated for 50V.
There are a total of 3 motor controllers in the system, 1 for the X-axis motors and 2 for the Y-axis motors. You’ll use the laser cut Motor Controller Mount Plate to fasten the controllers.
X-Axis controllerThe X-Axis motor controller is mounted underneath the table frame. A 1000uF bypass capacitor was soldered directly to the DC input to smooth out the power. A cable carrier is used to carry the 4 stepper motor wires along with the connections the X1 and X2 limit switches. The cable carrier we used did not come with end mounting hardware, so we ended up 3D printing a few of our own (See the 3D print files Cable Carrier End Mounts). Cable Carrier Mount Plates were then used to attach the cable carrier to the frame. Make sure to set the selector switches on the motor controller for 2.5A motor current and 400 pulses/rev.
The two Y-axis controllers were mounted above frame directly adjacent to motors. The selector switches for the Y-axis controllers are set to the same settings as the X-axis controller.
The Pixy should be mounted over the top of the play surface to provide it with a full view of the table. Use the Pixy Cam Mount Plate and a corner bracket to affix the camera. We found that a height of 4ft above the table surface worked well for the Pixy. Additionally, make sure the Air Hockey table should be located in a well lighted area to provide the Pixy with adequate and uniform lighting. A ribbon cable with a 2x5 IDC connector is used to connect the Pixy and MinnowBoard.
Reed switches located at the ends of the linear rails trigger when the carriage reaches the end of its travel. These switches are used to reset the carriage position at power-on, and also notify the MinnowBoard if the carriage reaches the end of the rail. 3D printed enclosures (Red) are used to help mount the fragile glass switches.
X-Axis switchesBoth switches for the X-Axis are mounted in the same red enclosure (Limit Switch Mount X) underneath the motor. Use a Limit Switch Mount Plate X to attach the 3D printed enclosure to the Y-axis motor carriage. The triggering magnets are inserted into a Magnet Belt Holder then taped to opposite ends of the timing belt (black rectangles shown in the last image).
The Y-Axis switches are in separate 3D printed enclosures (Limit Switch Mount Y) on the far ends of each rail. Laser cut the Limit Switch Mount Plate Ypieces, and use them to mount the 3D printed enclosures to the frame. Round magnets attached to the carriage bolts trigger the switch when nearby.
The goal sensors are mounted inside of the Goal Sensor Mounts. These blocks have holes in the side to allow the IR beam to pass, and are installed such that a puck passing between them blocks the beam.
Take the monitor mounting bracket, and use a 1/4” drill to open up both screw holes. Use 1/4” screws and T-nuts to secure the monitor mount to a vertical beam. Use an HDMI cable to connect the monitor to the MinnowBoard, and plug in the touchscreen USB cable as well.
With the frame complete and all hardware mounted, you finish the assembly by capping the ends of the ends of the aluminum beams with endcaps.
Software App: App DeploymentIf you aren’t already familiar with app deployment, follow the instructions here for building and deploying the app. Since we’re building for the MinnowBoard Max, make sure to select x86
for the architecture.
You can see that on the home screen, there are are four available options:
- Start Game - Starts a game of air hockey with the robot. The robot will first reset the motor positions, and then start playing with the user. The software will keep score indefinitely until the user exits this mode.
- Diagnostics - Shows you what the camera is seeing along with the trajectory prediction algorithm. The green circle represents the puck and lines are displayed indicating where the puck is predicted to go. The red circle represents where the robot will try to move the mallet to counter the puck.
- Test - This is a useful mode for playing with game algorithms. You can use a mouse to move a simulated puck, and see how the robot plans to respond.
- Mirror - This is another useful diagnostic mode. The robot simply notes the position of the puck on the table, and physically moves the mallet to mirror the position of the puck on the table. This way you can easily test the XY positioning of the mallet by moving a puck around the table.
The PixyCam class, contained in the AirHockeyHelper project, has been adapted from C++ open source code. This class contains methods for interfacing with the PIXY Camera, a fast vision sensor. The SPI APIs are used by this class to interface with the hardware/camera. The PixyCam object is initialized as follows:
public async Task Initialize()
{
var spiAqs = SpiDevice.GetDeviceSelector();
var devicesInfo = await DeviceInformation.FindAllAsync(spiAqs);
var settings = new SpiConnectionSettings(0);
settings.ClockFrequency = 1000000;
settings.DataBitLength = 8;
settings.SharingMode = SpiSharingMode.Shared;
Device = await SpiDevice.FromIdAsync(devicesInfo[0].Id, settings);
}
The clock frequency is initialized to 1 mbits/sec and the data bit length is set to 8. This class contains methods which support the reading in of blocks of data (e.g. bytes, DWORD, etc). The block-read rate is determined in the game class, which determines the robot’s response time.
private ObjectBlock getBlock()
{
byte[] readData = getBytes(10);
if (readData != null)
{
ObjectBlock block = new ObjectBlock();
block.Signature = BitConverter.ToUInt16(readData, 0);
block.X = BitConverter.ToUInt16(readData, 2);
block.Y = BitConverter.ToUInt16(readData, 4);
block.Width = BitConverter.ToUInt16(readData, 6);
block.Height = BitConverter.ToUInt16(readData, 8);
return block;
}
return null;
}
Before you get any data from the Pixy camera, you will need to teach the Pixy camera what the puck looks like. After it learns what the puck looks like, it will be able to send your app the coordinates of the puck within a block. The block contains the signature, XY coordinates, the width, and the height. The signature is only important if you taught the Pixy camera more than one object, or signature. It is a number that refers to which signature the rest of the data pertains to. The rest of the parameters are self-explanatory.
AIHelper.csThe AIHelper class contains the “brains” of the robot. This class contains functions for determining where the mallet should move in response to where the puck is.
public Point calculateMalletTarget(Point currentPuckPosition, long currentTime)
{
puckPosition = currentPuckPosition;
.
.
.
// Puck is behind the mallet
if (distanceFromMallet.X < 0)
{
// Defensive position in front of goal
malletTargetOffset = defenseOffset;
// Don't interrupt this move until we get to the destination
DoNotInterrupt = true;
}
// Puck is moving away
else if (prevCenterOfMass.X > centerOfMass.X)
{
// Defensive position in front of goal
malletTargetOffset = defenseOffset;
}
.
.
.
// Puck is moving towards mallet
else if (prevCenterOfMass.X < centerOfMass.X)
{
trajectoryPoint = CoordinateHelper.CalculateTrajectoryPoint(prevCenterOfMass, centerOfMass, currentMalletPosition.X);
if (trajectoryPoint == CoordinateHelper.INVALID_POINT)
{
malletTargetOffset = CoordinateHelper.INVALID_POINT;
}
// We can block the puck from our current location (no Y movement)
else if (trajectoryPoint.X == currentMalletPosition.X)
{
.
.
.
// We have a valid target destination for the mallet
if (malletTargetOffset != CoordinateHelper.INVALID_POINT)
{
// Constrain offset so that mallet doesn't move too far
malletTargetOffset.X = Helper.Constrain(malletTargetOffset.X, 0, Config.MAX_MALLET_OFFSET_X);
malletTargetOffset.Y = Helper.Constrain(malletTargetOffset.Y, 0, Config.MAX_MALLET_OFFSET_Y);
}
.
.
.
return malletTargetOffset;
}
The function calculateMalletTarget() is called each time the robot receives new information about the puck’s position. This function calculates the trajectory based on the puck’s current position and its old position and attempts to either block or hit the puck. The coordinates from the Pixy camera are jittery, so the algorithm attempts to smooth out its location by calculating the center of mass of the puck.
Config.csThis file defines the default values for mallet position, air hockey table mapping and motor speeds and acceleration. The values here are values that worked for us. You will need to tweak these until your motors are able to run smoothly. The MAX_MALLET_OFFSET values are the number of steps for the mallet to go from one end of the track to the other.
public struct Config
{
public static float
MOTOR_X_MAX_SPEED = 100000,
MOTOR_Y_MAX_SPEED = 70000,
MOTOR_X_ACCELERATION = 1000000,
MOTOR_Y_ACCELERATION = 600000;
// Default values, not const because Calibration can adjust values
public static int MAX_MALLET_OFFSET_X = 2681;
public static int MAX_MALLET_OFFSET_Y = 2462;
public const long TABLE_HEIGHT = 766;
public const long TABLE_MID_X_COORDINATE = 950;
public const long TABLE_GOAL_Y_TOP = TABLE_HEIGHT / 2 - 150;
public const long TABLE_GOAL_Y_BOTTOM = TABLE_HEIGHT / 2 + 150;
}
Global.csThis class contains a static Stopwatch object which is used to keep track of time throughout the app.
Helper.csThe Helper class contains methods for solving a system of linear equations, constraining a value between an upper and lower limit, and getting the distance between two points.
public static double[] ComputeCoefficents(double[,] X, double[] Y)
{
// Used for calibration
int I, J, K, K1, N;
N = Y.Length;
for (K = 0; K < N; K++)
{
K1 = K + 1;
for (I = K; I < N; I++)
{
if (X[I, K] != 0)
{
for (J = K1; J < N; J++)
{
X[I, J] /= X[I, K];
}
Y[I] /= X[I, K];
}
}
for (I = K1; I < N; I++)
{
if (X[I, K] != 0)
{
for (J = K1; J < N; J++)
{
X[I, J] -= X[K, J];
}
Y[I] -= Y[K];
}
}
}
for (I = N - 2; I >= 0; I--)
{
for (J = N - 1; J >= I + 1; J--)
{
Y[I] -= X[I, J] * Y[J];
}
}
return Y;
}
The ComputeCoefficents() function is used to calculate the coefficients for the quadratic equations that are used to model the curvature of the lens. The pre-calibrated values are fed into this function, which then return the coefficients for the quadratic equation. After we have the equation, we are able to translate the XY-coordinates from the camera, to the XY-coordinates on the screen.
CoordinateHelper.csThe CoordinateHelper class contains functions to translate camera coordinates to screen coordinates and calculate puck trajectory points.
Initialization involves setting the width and height of the table on the screen.
public static void Initialize(double virtualWidth, double virtualHeight)
{
VirtualWidth = virtualWidth;
VirtualHeight = virtualHeight;
setDefaultCalibration();
}
Good calibration is required for the accuracy of our system.
private static void setDefaultCalibration()
{
// Set pre-calibrated points
topLeft = new Point(17, 20);
topRight = new Point(299, 30);
topCenter = new Point(160, 12);
bottomLeft = new Point(12, 170);
bottomRight = new Point(293, 182);
bottomCenter = new Point(155, 190);
center = new Point(156, 100);
centerLeft = new Point(9, 96);
centerRight = new Point(303, 107);
// Calculate coefficients for the quadratic equations to help account for curvature distortion in lens
topQuadraticCoeff = Helper.ComputeCoefficents(
new double[,] {
{ Math.Pow(topLeft.X, 2), topLeft.X, 1 },
{ Math.Pow(topCenter.X, 2), topCenter.X, 1 },
{ Math.Pow(topRight.X, 2), topRight.X, 1 }
},
new double[] { topLeft.Y, topCenter.Y, topRight.Y }
);
.
.
.
}
public static Point TranslatePoint(double x, double y)
{
if (topQuadraticCoeff.Length < 1 || bottomQuadraticCoeff.Length < 1)
{
return new Point(-1, -1);
}
Point translatedPoint = new Point();
// Calculate the horizontal and vertical lines of the table
double centerY = centerHorizontalQuadraticCoeff[0] * Math.Pow(x, 2) + centerHorizontalQuadraticCoeff[1] * x + centerHorizontalQuadraticCoeff[2];
double centerX = centerVerticalQuadraticCoeff[0] * Math.Pow(y, 2) + centerVerticalQuadraticCoeff[1] * y + centerVerticalQuadraticCoeff[2];
if (y < centerY)
{
double topY = topQuadraticCoeff[0] * Math.Pow(x, 2) + topQuadraticCoeff[1] * x + topQuadraticCoeff[2];
double ratioY = (y - topY) / (centerY - topY);
translatedPoint.Y = VirtualHeight / 2 * ratioY;
}
.
.
.
return translatedPoint;
}
In order to calibrate the table, we take the raw camera coordinates of the puck at different points on the table:
- Top left, right, and center
- Bottom left, right, and center
- Middle left, right, and center.
Because of the lens on the Pixy cam, the lines that connect these points will not be completely straight. To adjust for this, we calculate the equations of the quadratic curves that run through the points, as shown in the diagram below. After we get these equations, the TranslatePoint() function uses them to figure out where the untranslated point is relative to any two curves and then translates the point to the screen.
The StepperLib class contains the methods needed to accelerate the motors to faster speeds. These methods need to be implemented by you, but I would recommend taking a look at the AccelStepper library for Arduino (http://www.airspayce.com/mikem/arduino/AccelStepper/) for help on how to implement this class.
MotorHelper.csThe MotorHelper class contains functions used to translate the step offsets of the X and Y motors to coordinates on the screen.
// Used to get current mallet and target positions
public static Point GetCoordinatesFromOffset(Point offset)
{
double x = GetCoordinateXFromOffsetY((long)offset.Y);
double y = GetCoordinateYFromOffsetX((long)offset.X);
return new Point(x, y);
}
Robot.csThe Robot class contains methods for interfacing with the sensors and coordinating the control of the X and Y motors.
There are two variables that keep track of the stepper motors responsible for moving the mallet in the X and Y axes.
public AccelStepper StepperX, StepperY;
The mallet carriage is supported on a guide rail. Limit switches connected to GPIO are used to detect the ends of the guide rails.
private const int LIMIT_SWITCH_PIN_X1 = 0;
private const int LIMIT_SWITCH_PIN_X2 = 1;
private const int LIMIT_SWITCH_PIN_Y1 = 2;
private const int LIMIT_SWITCH_PIN_Y2 = 3;
// Y sensors go low when triggered, X sensors go high when triggered
private GpioPin LimitSwitchPin_X1; // Right
private GpioPin LimitSwitchPin_X2; // Left
private GpioPin LimitSwitchPin_Y1; // Bottom
private GpioPin LimitSwitchPin_Y2; // Top
Two event handlers are attached to the GPIO interrupts for the GPIO pins that are connected to the beam break sensors that serve as our goals.
public event EventHandler<EventArgs> RobotGoalSensorTriggered;
public event EventHandler<EventArgs> HumanGoalSensorTriggered;
MoveStraightToOffset() is an important function for coordinating the X and Y motors to move the mallet in a straight line. If you don’t use this function, each motor will move at different speeds, resulting in a curve.
public void MoveStraightToOffset(Point offset)
{
ResetMotorParameters();
float diffX = (float)Math.Abs(StepperX.CurrentPosition() - offset.X);
float diffY = (float)Math.Abs(StepperY.CurrentPosition() - offset.Y);
if (diffX > 0 && diffY > 0)
{
float newAccelY = Config.MOTOR_Y_ACCELERATION;
float newAccelX = Config.MOTOR_X_ACCELERATION;
long minMaxSpeed = (long)Math.Min(Config.MOTOR_X_MAX_SPEED, Config.MOTOR_Y_MAX_SPEED);
StepperX.SetMaxSpeed(minMaxSpeed);
StepperY.SetMaxSpeed(minMaxSpeed);
// We need to move more in the X direction than Y, so Y accel will be slower than X accel
if (diffX > diffY)
{
if (Config.MOTOR_Y_ACCELERATION < Config.MOTOR_X_ACCELERATION)
{
newAccelY = diffY / diffX * Config.MOTOR_X_ACCELERATION;
if (newAccelY > Config.MOTOR_Y_ACCELERATION)
{
newAccelX = Config.MOTOR_X_ACCELERATION * (Config.MOTOR_Y_ACCELERATION / newAccelY);
newAccelY = Config.MOTOR_Y_ACCELERATION;
}
}
else
{
newAccelY = diffY / diffX * Config.MOTOR_X_ACCELERATION;
newAccelX = Config.MOTOR_X_ACCELERATION;
}
}
// We need to move more in the Y direction, so X accel will be slower than Y accel
else
{
if (Config.MOTOR_Y_ACCELERATION < Config.MOTOR_X_ACCELERATION)
{
newAccelX = diffX / diffY * Config.MOTOR_Y_ACCELERATION;
newAccelY = Config.MOTOR_Y_ACCELERATION;
}
else
{
newAccelX = diffX / diffY * Config.MOTOR_Y_ACCELERATION;
if (newAccelX > Config.MOTOR_X_ACCELERATION)
{
newAccelY = Config.MOTOR_Y_ACCELERATION * (Config.MOTOR_X_ACCELERATION / newAccelX);
newAccelX = Config.MOTOR_X_ACCELERATION;
}
}
}
StepperX.SetAcceleration(newAccelX);
StepperY.SetAcceleration(newAccelY);
}
MoveToOffset(offset);
}
MainPage.xaml.csThis file contains the code-behind for the home page of the app.
Game.xaml.csThis file contains the user interface for the human player and governs the robot’s actions based on the mode selected in the home page
In addition to drawing the UI, this class contains runDecisionThread() which provides the decision-making logic of the game. AI Helper methods are used by the robot to determine the mallet’s target.
Reading in the camera data and decision-making routines are run in a high priority thread separate from the UI thread.
private void runDecisionThread(Point puckPosition)
{
ThreadPool.RunAsync((s) =>
{
Point malletOffset;
if (gameMode == GameMode.Mirror || mirrorMode)
{
// Mirror the puck
double yOffset = MotorHelper.GetOffsetYFromCoordinateX(virtualWidth - puckPosition.X);
double xOffset = MotorHelper.GetOffsetXFromCoordinateY(puckPosition.Y);
if (Math.Abs(xOffset - robot.StepperX.CurrentPosition()) < 50)
{
xOffset = robot.StepperX.CurrentPosition();
}
if (Math.Abs(yOffset - robot.StepperY.CurrentPosition()) < 50)
{
yOffset = robot.StepperY.CurrentPosition();
}
malletOffset = new Point(xOffset, yOffset);
}
else
{
// Figure out where the mallet should move
malletOffset = robot.AI.calculateMalletTargetV3(puckPosition, Global.Stopwatch.ElapsedMilliseconds);
}
if (malletOffset != CoordinateHelper.INVALID_POINT)
{
// Set the destination for stepper motors
if (!robot.AI.DoNotInterrupt)
{
switch (robot.AI.Move)
{
case MoveType.Fast:
robot.MoveFastToOffset(malletOffset);
break;
case MoveType.Straight:
robot.MoveStraightToOffset(malletOffset);
break;
}
}
}
});
}
Summary and LearningsOverall, building the air hockey robot was a challenging but rewarding project. Along the way, we encountered difficulties which limited the effectiveness of the robot as a whole. Below are some findings and recommendations for improving future iterations of the air hockey robot.
Mechanical ImprovementsDuring development of the robot, we found that the high movement speeds and rapid accelerations caused a number of problems for our robot reliability.
- Use more powerful motors. The relatively sluggish performance meant that the robot could not competitively play against a human. We recommend using larger stepper motors with bigger pulleys (or step-up gears) to enhance the linear travel speed. You may also want to beef up the power supply as well. Alternatively, we considered using powerful DC motors with encoders for position feedback. The motors found in power drills would work well here.
- Motor shaft wear. We noticed significant wear and tear on motor shafts after a while. Vibration from the robots movements would loosen the pulley set-screws and cause damage to the shafts. Use thread locker compound or jam screws to ensure things don’t come loose. Even better, consider using motors with larger diameter shafts.
- Wear and tear on the X-axis carbon shafts. The linear bearings ended up cutting grooves in the carbon shaft after repeated cycling. If you followed the previous recommendation to install stronger motors, you may be able to get away with replacing the carbon shafts with heavier steel shafts. The steel would be much more resistant to wear.
- Use a denser mallet The 3D printed mallet wasn’t very effective at hitting the puck due to its lightness. The 3D printer we used to printed the mallet with an internal criss-cross pattern. This resulted in a somewhat hollow mallet that absorbed a lot of the impact energy from the puck. Printing the mallet more densely would help the robot hit harder. Furthermore, designing the mallet so that it can screw tightly into the 1/4” bolts would also improve rebound energy.
- Redesign the X-axis carriage. The currently X-axis carriage requires that tools be inserted at odd angles for assembly, and makes it difficult to tension the X-axis timing belt. Redesigning the carriage with assembly in mind would make it easier to put together.
There are a number of electrical improvements which would help increase reliability and improve robot performance.
- Motor controller noise. We found that the motor controllers tended to couple a large amount of noise onto adjacent circuits. This affected the reliability of the limit switches and the SPI Pixy connection. The wires connecting the Pixy and limit switches were approximately 6ft long, and frequently picked up spurious signals from the controllers. Shortening these wires and adding shielding would help suppress these issues. Additionally, repositioning the motor controllers to be physically distant from the signal wires would also likely help the situation.
- Improve table lighting conditions. The Pixy camera we used was convenient in that the object detection processing happens on-board the camera. However, the lighting we used for the table varied greatly in intensity. We found that glare on the table from surrounding lights caused a dead-zone for the camera detection. As a result, our robot struggled to respond to fast-moving pucks when there was too much glare on the table. Improving lighting uniformity would have helped. Alternatively, choosing a camera with a greater dynamic range would also help the robot see past difficult lighting.
- Sluggish stop button. When the emergency stop button is pressed, ideally the robot would stop moving immediately. However, the installed emergency stop button ended up with a latency of about 1-2 seconds. This was due to the large capacitors on the power supply which ended up holding power even after power was cut. For quicker shut-off times, consider wiring the emergency switch to break the power after the capacitors
- Vibration loosening. Vibration from the robot movements caused wires to wiggle loose and created a number of difficult to troubleshoot issues. Using soldered connections greatly improved the reliability of the system.
Windows 10 IoT Core is not a real-time operating system, and as such generating precise motor timing pulses raised a number of challenges. Nonetheless, we found that with careful programming practices we could prevent costly system context switches that would impact performance. In the end we were able to achieve good timing performance on the MinnowBoard Max by taking into account the following:
- UI updates impact performance. Updating the UI typically causes a system context switch over to the UI thread. This switch has a huge impact on system timing and would cause our stepper motors to jam from the output jitter. As a result, never update the UI when running timing critical operations.
- Use loops and not timer interrupts. In order to smoothly run the motors, we needed to send timed pulses out over GPIO to the motor controllers. These pulses had to be precisely timed and spaced in order to quickly accelerate and move the motors. Too much jitter in the output would cause the motors to jam when they missed a step. We originally used timer interrupts to generate the GPIO pulses, but found that the limited resolution and thread context switch caused up to 20ms of jitter. Using tight loops with high resolution diagnostics timers produced much better results (See implementation in Game.xaml.cs).
- Turn on code optimization. Turning on code optimization in the project helped increase the maximum output rate, and helped the motors run more smoothly with less jitter.
Comments