Introduction: Ski Buddy: the Coat That Teaches You to Ski.

With a passion for everything skiing, I always feel bad when I ski by crying children being yelled at by frustrated parents trying to teach them to ski. If you're a skier yourself, I'm sure you've seen many occasions in which incidents like that have occurred. Knowing that childhood memorizes can unintentionally affect our adult lives, I sought out to come up with a tool to help making the process of learning to ski fun for kids at young ages: The Ski Buddy. The Ski Buddy is a coat that helps teach children to ski using lights. Of course, all children love light-up toys, so why not transfer that love to learning? With changeable settings, you can use this coat for a variety of lessons. You can use it to teach concepts such as:

-linking turns

-parallel skiing

-hockey stop

-gradual pizza stopping'

Time with sewing knowledge: 1 hour

Time without sewing knowledge: 2-3 hours

Difficulty/Complexity: Medium (requires sewing knowledge and ability to upload code to an Arduino

Cost: $46.75

Let's get going with the materials!

Step 1: Materials

Step 2: Sewing "The Core"

The "core" of the project is the Adafruit Flora, Accelerometer, and AAA Battery Pack together, as seen in the image above.

**NOTE** Do NOT sew the "core" at the neck-level of the jacket. Doing so does 2 things:

1. Hurts the user's neck/makes the jacket uncomfortable and hard to put on.

2. COMPLETELY messes up the accelerometer readings due to shoulder motion. With this project, we want where the HIPS are facing, not the user's upper body. Why? If the user is looking one way while skiing another, the code will think the user is going the direction they are looking because their shoulders are turned that way. NOT GOOD.

***INSTEAD*** Sew the "core" at the bottom back waist level. IMPORTANT. (Read above)

Honestly, my sewing skills are heavily lacking, but do not fear, for Adafruit Learn is here! Really, the tutorial linked below the most amazing tutorial for how to sew the Flora and accelerometer onto the waist of the jacket, done by Adafruit themselves. For those of you that aren't adept at sewing like me, please use the tutorial and replicate it onto the jacket that you are using.

***NOTE*** Do NOT let the electrical threads intersect or touch/overlap. IT WILL NOT WORK.

Here is the tutorial:

Wiring with Conductive Thread

An example of the finished product of what this should look like on a jacket can be seen above on the second picture.

Step 3: Sewing the LED Sequins

The sewing of the LED sequins is pretty simple in concept. We'll start with the left side. (Left relative to if you were to have the jacket on)

If you want a tutorial for extra help, here's this guide.

1. Get 4 LED Sequins out from the bag.

**As you can see from the pictures above, the LED's are placed in pairs at the elbow area.**

2. Using the picture of the Flora above, sew two threads from the blue-labeled pins.

3. Using the diagram of the LED Sequins/thread above, run the D12 thread through the + pin of the first LED Sequinand tie it off on the other.

4. Using the diagram again, run the GND thread through the - pin of the first LED Sequin and tie it off on the other.

5. The end result should look like the diagram above.

6. Want to see if your sewing job works? Run the code below.


/**
* Created by Max Karpawich 2/6/2016 * Test code for the LED Sequins */ void setup() { pinMode(9,OUTPUT); pinMode(12,OUTPUT);

}

void loop() { digitalWrite(9,HIGH); digitalWrite(12,LOW); delay(500); digitalWrite(9,LOW); digitalWrite(12,HIGH); delay(500); }


If the left side LED pair isn't alternating
I found that the D12 thread would run on top of the VBATT pin of the Flora on occasion. Make sure that the thread isn't touching any other pins besides the one its tied to.

Step 4: The Code

You are either two types of people:

1. Coders who actually want to understand the code.

2. People who don't know a lick of coding and just want the raw source file.

Good news for you, this page covers both!

Please note that this code is under the GPL v3 license.

So for those of you that just want the raw source file, it's on Github:

**But before you click that link, please scroll down and read the code settings.**

Entire File

For those of you who want to understand the code, thanks for appreciating the time I spent writing it! Let's jump right in:

We first need to include these three libraries. Confused what the last two are? That's alright. They appeared in the earlier sewing tutorial that I linked. If you already know how to add Arduino libraries to the IDE, though, here's the direct link to the Github Repositories:

Adafruit Sensor Library

Adafruit LSM303 Library

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include  <Adafruit_LSM303.h>

Next I'll go over the functions that I use in the code:

flash flashes both LED's together.

alternate alternates both LED's.

blink flashes both LED's once.

/**
 * Flash the LED's at a certain interval.
 */
void flash(int milliseconds){
  digitalWrite(9,HIGH);
  digitalWrite(12,HIGH);
  delay(milliseconds);
  digitalWrite(9,LOW);
  digitalWrite(12,LOW);
  delay(milliseconds);
  digitalWrite(9,HIGH);
  digitalWrite(12,HIGH);
  delay(milliseconds);
  digitalWrite(9,LOW);
  digitalWrite(12,LOW);
  delay(milliseconds);
}

/**
 * Alternate the LED's for a certain time.
 */
void alternate(int milliseconds) {
  digitalWrite(9,HIGH);
  digitalWrite(12,LOW);
  delay(milliseconds);
  digitalWrite(9,LOW);
  digitalWrite(12,HIGH);
  delay(milliseconds);
  digitalWrite(12,LOW);
}
/**
 * Blink the LED's for a certain time.
 */
void blink(int milliseconds){
  digitalWrite(9,HIGH);
  digitalWrite(12,HIGH);
  delay(milliseconds);
  digitalWrite(9,LOW);
  digitalWrite(12,LOW);
}

toDegrees converts a pair of x and y magnetic readings into a heading in degrees.

/**
 * Converts a pair of LSM303 magnetic readings into degrees.
 */
int toDegrees(double x, double y) {
  double deg =  (atan2(y,x) * 180)/PI;
  return (int) deg;
}

percent takes two numbers and creates a percent with the first as 100% and the second as the fraction.

/**
 * Takes two numbers and creates a percent.
 */
int percent(double whole, double fract) {
  double ret = fract/(whole/100);
  return (int) ret;
}


***CODE SETTINGS***

turnTime is the amount of time that you estimate it would take the user to turn in milliseconds. For instance, you could lessen the turnTime to help teach linked turns vs. lengthening the turn time to leave room for explanations about ski form in between turns.

slowDown is essentially how much you want the user to slow down at the end of each turn. For instance, setting the value to 2 assumes the user will halve their speed before turning. If you are teaching the user to go into a gradual pizza, you might want to lessen the value to let's say 1.3 so that the user has to reduce their speed by 1/4 before turning.

maxLearnCount is a number of turns that the device uses to learn the course before beginning to tell the user where to ski with the LED's. This is useful if a hill has edges with different lengths and you want to get the average time it takes to cross those different lengths.

skiPercent is the percentage of time you want the device to wait before telling the user to slow down. For instance, turning the percentage down allows for more gradual turns, whereas the opposite usually more abrupt turns.

readyTime is the general amount of time you think it takes to get on and off the lift in milliseconds.

//===============SETTINGS=============


//The amount of time it takes to turn in milliseconds.
//Change for linking turns, time for explanations in-between turns..
int turnTime = 1500;

//The acceleration/slowDown determines how much we want the user to
//slow down before turning.
//Change for slow stopping (pizza), fast stopping (hockey stop)
double slowDown = 2;


//How many turns we want the code to learn for.
//Change for greater accuracy if the hill has slightly off side lengths, etc.
int maxLearnCount = 4;

//The percentage of time it takes to cross the mountain before the code
//begings telling the user to slow down through the LED's.
//Change for gradual stopping vs. instant stopping.
int skiPercent = 75;

//The amount of time it takes to get on and off either the lift or "magic carpet" in milliseconds
int readyTime = 5000;

//==============SETTINGS=============

Now let's initialize the accelerometer:

//Initialize the LSM303 accelerometer.
Adafruit_LSM303 lsm;

Next we will go through the setup code. There are two parts.

1. Initialize the LED's on the jacket.

void setup() {  //Set the two LED ports to outputs.
  pinMode(9,OUTPUT);
  pinMode(12,OUTPUT);
  //Flash them to show they're working.
  flash(100);

2. Load the accelerometer. If it fails, just alternate the LED's to show failure.

//Begin loading the LSM303..
//If it fails to load, flash repeatedly..
  if (!lsm.begin()) {
    while(true){
    flash(300);
    }
  }
  //Finish setup.
}

Now for the different variables.

learnCount is used to keep track of how many turns the device has spent learning, stopping once it reaches the maximum amount of learning turns set above.

lastTurn is used to remember the last time a turn occurred, allowing the measurement of the time of a turn.

lastDir is used to keep the general overall direction of the user.

lastAcc is used to keep the general overall acceleration of the user.

dir is an array that contains the two suggested directions of each turn.

dirTime is the average time it takes for the user to the cross the trail.

currentDir is used to keep track of which direction the user should be going in.

liftMode is used to save battery when going up a lift or "magic carpet."

//Used to start to get a bearing of the trail.
int learnCount = 0; //Used to determine the length of turns.
long lastTurn;//Used to detect turns and for learning the course.
int lastDir = -1; //Used to also detect turns and for learning the course.
int lastAcc = -1; //The two directions of the course.
int dir[2]; //The time it takes to cross the trail.
int dirTime = -1; //The current direction the course is going in.
int currentDir = 0; //Lift mode saves battery by only periodically checking for upward motion.
boolean liftMode = false;

Finally, the loop() code. There are several parts:

1. Read/average data from the accelerometer.

The code takes five readings and averages them out for better accuracy.

There are 3 important variables here:

magAvg is the direction the user is facing in degrees. This is used to help guide the user.

accAvg is the overall x and y acceleration of the user. This is used to detect when the user slows down.

liftAvg is the overall z acceleration of the user. This is used to detect "lift" motion (upwards)

void loop() { //To get precise accuracy, take 5 readings and average them.
  int magRead[5];
  //It should be noted that the accRead array takes in X and Y readings
  //from the accelerometer for general velocity. The liftRead array
  //takes in Z readings to detect if someone is either on a lift
  //or getting on/off. Hopefully clears up confusion!
  int accRead[5];
  int liftRead[5];
  for(int i = 0; i < 5; i++) {
    lsm.read();
    //Convert the magnet data to degrees..
    magRead[i] = toDegrees(lsm.magData.x,lsm.magData.y);
    accRead[i] = abs((lsm.accelData.x + lsm.accelData.y)/2);
    liftRead[i] = lsm.accelData.z;
  }
  //Now average the readings..
  int magAvg = 0;
  int accAvg = 0;
  int liftAvg = 0;
  for(int i = 0; i < 5; i++) {
    magAvg = magAvg + magRead[i];
    accAvg = accAvg + accRead[i];
    liftAvg = liftAvg + liftRead[i];
  }
  magAvg = magAvg/5;
  accAvg = accAvg/5;
  liftAvg = liftAvg/5;

2. Check if the user is skiing down the mountain, has exceeded the amount of maximum learning turns(maxLearnCount)set above, and is skiing downhill.

if(!liftMode) {
    if(learnCount >= maxLearnCount) {
      if(liftAvg < 0) {

3. Knowing what we know above, check first to see if the user has finished a turn based upon how long it took them to cross the mountain in the learning process, hence the dirTime variable. Transfer over to the other turn and delay based upon how long we think time in between turns is, hence the turnTime variable.

long now = millis();
long dif = now - lastTurn;
if(percent(dirTime,dif) >= 100) {
lastTurn = now;
currentDir = 1-currentDir;
//Give them time to turn..
blink(turnTime);

4. If we haven't reached the turning phase yet, check if we should tell the user to slow down after a certain time based upon when the user should start stopping, hence the skiPercent variable set above. If not, then we need to guide the user in which direction they should be going in. Since nobody is perfect, we'll give them 10 degrees of slack on either side.

}else if(percent(dirTime,dif) <= skiPercent) {
	if(magAvg >= dir[currentDir] + 10) {
          //Go Left
          digitalWrite(12,HIGH);
          digitalWrite(9,LOW);
        }else if(magAvg <= dir[currentDir] - 10) {
          //Go Right
          digitalWrite(9,HIGH);
          digitalWrite(12,LOW);
        }else{
          //Stay
          digitalWrite(12,LOW);
          digitalWrite(9,LOW);
        }

5. The only option left as to what is happening is that we have reached the time to tell the user to slow down. We'll do this by alternating the lights back and forth until the user has reached the turning phase.

}else{        //SLOW DOWN..
        alternate(300);
}

6. This else statement is in regard to if the user is going downhill, meaning that user must be going uphill at this point, so we'll put the user into lift mode so that we can save battery and then delay so that the user has time to get onto the lift/magic carpet.

}else{        //Go into lift mode for next loop.
        liftMode = true;
        delay(readyTime);
}

7. This else statement is in regard to if the user is past learning mode, meaning that the user must be in learning mode at this point. This is important because if the user is taking their first learning turn, we don't want to mess up the dirTime variable, because obviously their first trek across the trail will take longer since they have to gain speed. With that, we'll first check to make sure that this is isn't the 1st recording of data, because that would mean that we wouldn't have previous data to build on. Next we average out the direction of the user with their past direction so we can get an accurate reading as to where we want the user to go after learning mode for this particular turn.

}else{      //Time to learn!
        //Make sure this isn't the first time recording data.
        if(lastDir != -1 && lastAcc != -1 && dirTime != -1) {
          lastDir = (magAvg + lastDir)/2;

8. In order to detect turns, we wait until the user begins shedding off speed to the point that we want them to, hence the slowDown variable set above. Then we will assign the direction the user has been going in as 1 of the 2 suggested directions we want the user to go in. (We have a suggested direction for going left across the mountain and another for going right across). We also need to record the amount of time it took to cross the mountain and write that to the dirTime variable.

//Check to see if they slow down by how fast we want them to slow down.
          if(accAvg <= (lastAcc/slowDown)) {
            dir[currentDir] = lastDir;
            learnCount+=1;
            currentDir = 1 - currentDir;
            //Record how long it took to cross the mountain..
            long now = millis();
            //Average that time with how long it took last turn.
            dirTime = (dirTime + (now - lastTurn))/2;
            lastTurn = now;
            //Give them a second to turn..
            delay(turnTime);
          }
          lastAcc = (accAvg + lastAcc)/2;

9. This else statement is in regard to if this is the first recording of data, meaning that it is, so we just assign the current data to their respective variables so that on the next loop we can start averaging out the data.

}else{          //Make recordings for first turn.
          lastDir = magAvg;
          lastAcc = accAvg;
          lastTurn = millis();
        }

10. This else statement is in regard to if the user is in lift mode, meaning that they are. To save battery, we just check for downward motion only once per second and flash the LED's to indicate to the user we are still in lift mode.

}  }else{
    if(liftAvg < 0) {
      liftMode = false;
      delay(readyTime);
    }
    blink(100);
    delay(950);
  }

11. By delaying 50 milliseconds, we don't overload the code and save battery, because even by checking readings every 50 milliseconds, that equates to reading data 20 times per second.

delay(50);}

Step 5: Usage

There are a few steps required to initiate the device properly once you get of the lift/"magic carpet":

1. Turn on the device where you think the user will be after getting off the lift/"magic carpet" after the given amount of time set as the readyTime variable in the code.

2. The LED's will flash once to indicate they are working, and then immediately after, guide the user along the right path that you want them to take, including direction, speed, slowing down, etc.

3. After the maximum amount of learning turns has been reached, let the user go through freely.

4. As long as you calibrated the settings to your needs, the rest should be taken care of. Do remember though that after getting off the lift/"magic carpet", do not delay any further than the set readyTime, or else the device will get messed up.

5. Have fun!

Calibrating Properly

Here are some examples for calibrating for different situations:

linking turns decrease the turnTime to a second or so to allow for turning.

hockey stop increase the skiPercent to something like 90% or 95% for quick stopping.

gradual pizza decrease the skiPercent to something like 50% or 70%.

Some extra tips

- when you turn the device on, if it begins alternating the LED's, it means that it failed to connect to the accelerometer properly, meaning it won't work.

- when skiing, alternating LED's means the user should start slowing down.

- start every time you get off the lift/"magic carpet" roughly where you turned the device on for better accuracy.

- tell the user at the beginning how to respond to the LED's based on what you are teaching them during the lesson. For instance you would tell them that when the LED's alternate, go into a slow pizza, and that when the LED's are off, it means that they are going the right way.

Step 6: Conclusion

The SkiBuddy is a great tool to help children ski, and more importantly, gives them good childhood memories that they can build on to enjoy skiing even more in the future! I had lots of fun building this project, and hope that it can help you in your endeavors to teach children to ski! Here's a video of me testing out the device originally. And yes, those are shorts. If you have ANY questions, comments, or concerns, please leave a comment below and I will get back to you soon!

Improvements List

-button to stop/start device for ease-of-use.

-run electrical thread INSIDE the jacket.

FAQ

(None currently)

Contact me at:

19mkarpawich@gmail.com

Thanks again!

Hack Your Day Contest

Participated in the
Hack Your Day Contest