Author Topic: Bike computer for use on trainer  (Read 5711 times)

postuma

  • Jr. Member
  • **
  • Posts: 64
Bike computer for use on trainer
« on: January 17, 2016, 05:47:43 pm »
I decided I needed a better bike computer for those winter months - I'd like to know what kind of power I'm cranking, and calculate calories burned based on more than speed or time on bike. Every trainer is different, and provides a different degree of resistance at different speeds ..

Fortunately, I found a power formula for my trainer on the Kurt Kinetic web site, though it seems that they've since taken these down - I guess they have newer products to sell. And while I know these numbers are only approximate and not nearly as accurate as a proper power meter, they're enough for me.

The code is a little unusual in places. For example, I could not use standard string handling functions because of the memory overhead involved, both in terms of total available and dynamic memory. Other compromises have been made. But it works.

Code: [Select]
//new IRQ version as polling code missed many wheel rotations even w 10 microsecond delay
//
//this code uses a custom truncated version of GLCDFONT.C to conserve memory on the Digispark Pro (max 14,844 bytes of program memory)
//display is in S.I. units, and maxes at 99.9 km/h. If I ever go faster than that on my trainer, I will update the code accordingly
//
//hardware:  Arduino 1.77" TFT LCD Display 160x128, two reed switches (coulda used Hall effect sensors, and I might at a later date),
//two 10K pull-down resistors


#include <TFT.h>                              //LCD TFT screen library
#include <SPI.h>
#include <TinyPinChange.h>                    //pin management for interrupts

#define pedal_sensor  2                       //digital pin 2 for pedal sensor
#define wheel_sensor  3                       //pin 3 for wheel sensor

uint8_t virtPort0;
uint8_t virtPort1;

#define cs   9                                //pin definitions for TFT screen
#define dc   6
#define rst  7

TFT TFTscreen = TFT(cs, dc, rst);



//define bike, trainer, and personal parameters ** commented out here because of memory overflow problems
//but ideally will define here so they can be adjusted for other bikes, trainers, and athletes
//should not be a problem when porting this code to the Oak

//uint16_t wheelCirc = 1934;                    //circumference of wheel with tire fully inflated, in mm
//float trainerVar1 = 0.02336;                  //calibration variables for trainer (from Kurt's Kinetic PC, formula converted to metric)
//float trainerVar2 = 3.082E-7;
//float trainerVar3 = 3.071E-9;
//byte GME = 23;                                //estimated Gross Metabolic Efficiency, in percent



//initialize sensors, timers

uint32_t i;
char a1[6] = { 0,0,0,0,0,0 };                 //two character arrays for displaying to TFT screen; string handling i.e. dtostrf or String()
char a2[6] = { 1,1,1,1,1,0 };                 //methods not used, as add > 2kb and > 3.5 kb to compiled code, respectively
uint16_t delayInterval = 300;                 //timing delay between code loops (with execution time of about 200 msec/loop - 2 updates/second
boolean zeroMainDisplayFlag = 0;


volatile uint16_t pedalCount = 0;             //pedal counters; rolls over at 65,535 pedal revolutions - at a cadence of 60/min, after 18 hrs
volatile uint32_t pedalTime = 0;              //time pedal sensor was activated, from millis()
uint16_t lastPedalCount = 0;
uint32_t lastPedalTime = 0;
uint16_t pedalLoopTime = 0;                   //time between pedal sensor activations; if > 10,000 msec, stop recording as wheel is paused
byte pedalLoops = 0;                          //number of pedal revolutions since last time through main loop

volatile uint32_t wheelCount = 0;             //wheel counters
volatile uint32_t wheelTime = 0;
uint32_t lastWheelCount = 0;
uint32_t lastWheelTime = 0;
uint16_t wheelLoopTime = 2000;                //first loop through wheel sensor code, before any activity, ensures BikeTime clock does not increment
byte wheelLoops = 0;


uint32_t codeLoopTime = 0;                    //time elapsed since last loop through code
uint32_t lastCodeLoopTime = 0;                //stores prev time through loop
uint32_t bikeTime = 0;                        //actual cycling time
byte currTime[] = { 0,0,0 };                  //currTime used to change minutes, hours displayed only when changed
uint32_t distance = 0;
uint16_t distanceInc = 0;
byte prevKm = 0;
uint16_t spd = 0;
uint16_t spdMean;
uint16_t spdMax = 0;
uint16_t cad;
uint16_t cadMean;
uint16_t cadMax = 0;
uint16_t power = 0;
uint16_t powerMean = 0;
uint32_t powerSum = 0;
uint16_t powerMax = 0;
float work = 0.0;
uint16_t prevWork = 0;



////* SETUP - use Interrupts to trigger pedal and wheel sensor code when magnetic switches activated on digital pins 2 and 3

void setup() {
  TinyPinChange_Init();
  virtPort0 = TinyPinChange_RegisterIsr(pedal_sensor, incPedal);
  virtPort1 = TinyPinChange_RegisterIsr(wheel_sensor, incWheel);
  TinyPinChange_EnablePin(pedal_sensor);
  TinyPinChange_EnablePin(wheel_sensor);

//  float workConvVar = 41.84 * GME;            //conversion variable for caloric calculations; restore on system with higher dynamic memory

  TFTscreen.begin();                          //initialize LCD TFT display screen
  TFTscreen.background(0,0,0);                //clear the screen with a black background
  TFTscreen.stroke(255,255,255);              //characters white
  TFTscreen.setTextSize(2);
  TFTscreen.text("0",0,0);
  TFTscreen.text(":",9,0);
  TFTscreen.text("00",18,0);
  TFTscreen.text(":",39,0);
  TFTscreen.text("00",48,0);
  TFTscreen.text(" 0",83,0);
  TFTscreen.text("000",111,0);

  TFTscreen.text(".",32,26);
  TFTscreen.text("k",62,26);
  TFTscreen.text("/",71,26);
  TFTscreen.text("h",79,26);
  TFTscreen.text("cad",55,52);
  TFTscreen.text("watt",55,78);
  TFTscreen.text("calories",55,104);
 
  zeroMainDisplay();
  TFTscreen.text("0",36,70);
  TFTscreen.text("0",36,96);

  TFTscreen.setTextSize(1);
  TFTscreen.text(".",105,7);
  TFTscreen.text("km",149,7);

  TFTscreen.text("0.00 MEAN",107,24);
  TFTscreen.text("0.00 MAX",107,34);
  TFTscreen.text("0.0 MEAN",113,50);
  TFTscreen.text("0.0 MAX",113,60);
  TFTscreen.text("0 MEAN",125,76);
  TFTscreen.text("0 MAX",125,86);
 
  TFTscreen.stroke(150, 150, 255);            //blue
  TFTscreen.text("1 SETUP  2 STOP  3 SUMMARY",0,121);     //not yet implemented. Waiting for the extra code space on the Oak
}


void loop() {

  i = millis();
  codeLoopTime = i - lastCodeLoopTime;
  lastCodeLoopTime = i;

 
////* pedal code. Cadence updates if the wheel has turned in the last second i.e. bike is still in motion

  pedalLoops = pedalCount - lastPedalCount;
  if (pedalLoops > 0) {                       //check for activity on pedal sensor
    pedalLoopTime = pedalTime - lastPedalTime;                          //check time since last loop through pedal sensor code
    pedalLoopTime = (pedalLoopTime < 10000) ? pedalLoopTime : 10000;    //and set to 10 sec if more than 10 sec has passed - assume wheel has stopped rotation     
    lastPedalCount = pedalCount;                                        //update counter with current pedal count
    lastPedalTime = pedalTime;                //and store current time
   
    cad = (float)600000 / pedalLoopTime;      //1000 msec/sec * 60 sec/min * 10 for decimal point (cadence in revs/minute with one decimal, for Max)
   
    TFTscreen.setTextSize(3);                 //update cadence display
    i = cad / 10;                             //remove decimal point
//    i = codeLoopTime;                       //DEBUG: display time taken by code loops instead of cadence
    a1[2] = (i % 10)+'0';
    i /= 10;
    if (!i) {
      a1[1] = ' ';
      a1[0] = ' ';
    }
    else {
      a1[1] = (i % 10)+'0';
      i /= 10;
      a1[0] = (i) ? (i%10)+'0' : ' ';
    }
    a1[3] = 0;
    wrtVal(0,44);
  }

  TFTscreen.setTextSize(1);
  i = (float)pedalCount * 600000 / bikeTime;   //revs / bikeTime in millisecs * 60000 msec/min
  if (cadMean != i) {
    cadMean = i;
    a1[4] = (i % 10)+'0';
    a1[3] = '.';
    i /= 10;
    a1[2] = (i % 10)+'0';
    i /= 10;
    if (!i) {
      a1[1] = ' ';
      a1[0] = ' ';
    }
    else {
      a1[1] = (i % 10)+'0';
      i /= 10;
      a1[0] = (i) ? (i%10)+'0' : ' ';
    }
    wrtVal(101,50);
  }
 
  if (cad > cadMax) {
    i = cad;
    cadMax = i;
    a1[4] = (i % 10)+'0';
    a1[3] = '.';
    i /= 10;
    a1[2] = (i % 10)+'0';
    i /= 10;
    if (!i) {
      a1[1] = ' ';
      a1[0] = ' ';
    }
    else {
      a1[1] = (i % 10)+'0';
      i /= 10;
      a1[0] = (i) ? (i%10)+'0' : ' ';
    }
    wrtVal(101,60);
  }



////* wheel rotation handling. Bike timer increments if wheel has turned in the last second. Update those elements depending on wheel rotation i.e. distance, speed, power, calories

  wheelLoops = wheelCount - lastWheelCount;
  if (wheelLoops > 0) {
    wheelLoopTime = wheelTime - lastWheelTime;                        //check time since last loop through wheel sensor code
    wheelLoopTime = (wheelLoopTime < 2000) ? wheelLoopTime : 2000;    //and set to 2 sec if more than 2 sec has passed - assume wheel has stopped rotation     
    lastWheelTime = wheelTime;
    lastWheelCount = wheelCount;

//    distanceInc = wheelCirc * wheelLoops;   //restore trainer variables if/when porting code to processor with more dynamic memory
    distanceInc = 1934 * wheelLoops;
    distance += distanceInc;
    bikeTime += codeLoopTime;

    updateTime();
    updateMainDisplay();
    zeroMainDisplayFlag = 1;
  }

  else if (wheelLoopTime < 2000) {                //if wheel spinning has not been detected for under 2 sec, presumed to be spinning slowly
    wheelLoopTime += codeLoopTime;                //increment wheelLoopTime by codeLoopTime
    bikeTime += codeLoopTime;
    updateTime();
    updateMainDisplay();
  }

  else if (zeroMainDisplayFlag == 1) {
    zeroMainDisplay();                            //if no wheel rotation more than 1 second since last sensed, zero main display - specifically speed, cadence, and power
  }
 
  delay(delayInterval);
}



////* bike time display

void updateTime() {

//update time elapsed, but only if value for hours, minutes, or seconds has changed - fewer computations, less screen flicker

  TFTscreen.setTextSize(2);
 
  i = bikeTime/1000;                          //convert to seconds
  i = i % 60;
  if ((i > currTime[2]) || ((i == 0) && (currTime[2] == 59))) {
    a1[0] = (i/10)+'0';
    a1[1] = (i%10)+'0';
    a1[2] = 0;
    wrtVal(48,0);
    currTime[2] = (currTime[2] == 59) ? 0 : i;
  }
 
  i = bikeTime/60000;                         //calculate minutes
  i = i % 60;
  if ((i > currTime[1]) || ((i == 0) && (currTime[1] == 59))) {
    a1[0] = (i/10)+'0';
    a1[1] = (i%10)+'0';
    a1[2] = 0;
    wrtVal(18,0);
    currTime[1] = (currTime[1] == 59) ? 0 : i;
  }
 
  i = bikeTime/3600000;                       //hours
  if (i > currTime[0]) {
    a1[0] = i+'0';
    a1[1] = 0;
    wrtVal(0,0);
    currTime[0] = i;
  }
}



////* update Main Display: speed, power, calories

void updateMainDisplay() {
  TFTscreen.setTextSize(2);

  //distance
 
  i = distance /1000;
  a1[2] = (i % 10)+'0';                       //this code structure uses fewer bytes than for loop
  i = i/10;
  a1[1] = (i %10)+'0';
  i = i/10;
  a1[0] = (i %10)+'0';
  a1[3] = 0;
  wrtVal(111,0);

  i = i/10;
  if (prevKm != i) {
    prevKm = i;
    a1[1] = (i %10)+'0';
    i = i/10;
    a1[0] = (i) ? i+'0' : '  '; 
    a1[2] = 0;
    wrtVal(83,0);
  }
 

  //speed, power - update if speed has changed

  i = distanceInc * 360.0 / wheelLoopTime;    //distanceInc in mm / 10,000 for speed in decametres * 3,600,000 for msec/hr
  if (spd != i) {
    spd = i;
    TFTscreen.setTextSize(3);
    i /= 10;                                  //convert to hectometres/hr
    a1[0] = (i % 10)+'0';
    a1[1] = 0;
    wrtVal(43,18);
    i /= 10;   
    a1[1] = (i % 10)+'0'; 
    i /= 10;
    a1[0] = (i) ? i+'0' : '  '; 
    a1[2] = 0;
    wrtVal(0,18);

    //power calcs - watts. Using straight multiplication since Pow() compiles larger and slower
 
//    i = trainerVar1*spd + trainerVar2*spd*spd + trainerVar3*spd*spd*spd;     //restore trainer variables in initializing code if porting program to a processor with more dynamic memory
    i = 0.02336*spd + 3.082E-7*spd*spd + 3.071E-9*spd*spd*spd;
    power = i;
 
    TFTscreen.setTextSize(3);
    a1[2] = (i % 10)+'0';
    i /= 10;
    if (!i) {
      a1[1] = ' ';
      a1[0] = ' ';
    }
    else {
      a1[1] = (i % 10)+'0';
      i /= 10;
      a1[0] = (i) ? (i%10)+'0' : ' ';
    }
    a1[3] = 0;
    wrtVal(0,70);
   
    TFTscreen.setTextSize(1);
    if (power > powerMax) {
      i = power;
      powerMax = i;
      a1[2] = (i % 10)+'0';
      i /= 10;
      if (!i) {
        a1[1] = ' ';
        a1[0] = ' ';
      }
      else {
        a1[1] = (i % 10)+'0';
        i /= 10;
        a1[0] = (i) ? (i%10)+'0' : ' ';
      }
      a1[3] = 0;
      wrtVal(113,86);
    }
  }
 

  //mean and maximum speed

  TFTscreen.setTextSize(1);
  i = distance * 360.0 / bikeTime;
  if (spdMean != i) {
    spdMean = i;                              //mean speed in decametres/hr
    a1[4] = (i % 10)+'0';
    i /= 10;
    a1[3] = (i % 10)+'0';
    a1[2] = '.';
    i /= 10;
    a1[1] = (i % 10)+'0';
    i /= 10;
    a1[0] = (i) ? (i%10)+'0' : ' ';
    wrtVal(101,24);
  }
 
  if (spd > spdMax) {
    i = spd;
    spdMax = i;                               //update SpdMax to new value
    a1[4] = (i % 10)+'0';
    i /= 10;
    a1[3] = (i % 10)+'0';
    a1[2] = '.';
    i /= 10;
    a1[1] = (i % 10)+'0';
    i /= 10;
    a1[0] = (i) ? (i%10)+'0' : ' ';
    wrtVal(101,34);
  }


  //mean power
 
  powerSum += (float)power * codeLoopTime;    //calculate power consumed in the time elapsed, in Joules
  i = powerSum / bikeTime;                    //and calc average watts over time on bike
  if (i != powerMean) {
    powerMean = i;
    a1[2] = (i % 10)+'0';
    i /= 10;
    if (!i) {
      a1[1] = ' ';
      a1[0] = ' ';
    }
    else {
      a1[1] = (i % 10)+'0';
      i /= 10;
      a1[0] = (i) ? (i%10)+'0' : ' ';
    }
    a1[3] = 0;
    wrtVal(113,76);
  }

 
  //calorie count: cal = work * codeLoopTime /1000 (in seconds) / 4.184 cal/joule / (GME/100) for GME percent

//  work += (float)power * codeLoopTime / workConvVar;
  work += (float)power * codeLoopTime / 962.3;
  i = work / 1000;                            //convert kCal ("calories")
  if (i != prevWork) {
    prevWork = i;
    TFTscreen.setTextSize(3);
    a1[2] = (i % 10)+'0';     
    i /= 10;
    if (!i) {
      a1[1] = ' ';
      a1[0] = ' ';
    }
    else {
      a1[1] = (i % 10)+'0';
      i /= 10;
      a1[0] = (i) ? (i%10)+'0' : ' ';
    }
    a1[3] = 0;
    wrtVal(0,96);
  }
}



////* Interrupt Service Routines for pedal and wheel sensors

void incPedal() {
  if(TinyPinChange_RisingEdge(virtPort0, pedal_sensor)) {
    uint32_t j = millis();
    if (j - pedalTime > 100) {                //if at least 10 msec have passed since ISR last triggered, i.e. past possible switch bounce
      pedalTime = j;                          //store current pedalTime
      pedalCount++;                           //and increment pedal counter
    }
  }
}
void incWheel() {
  if(TinyPinChange_RisingEdge(virtPort1, wheel_sensor)) {
    uint32_t j = millis();
    if (j - wheelTime > 10) {                 //debounce
      wheelTime = j;                          //etc.
      wheelCount++;
    }
  }
}




////* zero the main display: speed, cadence, watts

void zeroMainDisplay() {
  TFTscreen.setTextSize(3);
  zeroMainDisplayFlag = 0;

  a1[0] = ' ';                                //zero speed
  a1[1] = '0';
  a1[2] = 0;
  wrtVal(0,18);
  a1[0] = '0';
  a1[1] = 0;
  wrtVal(43,18);
  a1[0] = ' ';                                //and zero cadence and watts
  a1[1] = ' ';
  a1[2] = '0';
  a1[3] = 0;
  wrtVal(0,44);
  wrtVal(0,70);
}



////* blank out previous characters by writing an extended ASCII code 219 character (block, or █) in black over the previous. To conserve program space,
//    I have truncated GLCDFONT.C at 'z', or character 123, and remapped ASCII character 219 to character code 1

void wrtVal(byte x, byte y) {                   //code works to print up to 5 spaces truncated by 0
  byte v=0;
  while (a1[v]) {
    a2[v] = 1;                                  //print full character-sized rectangles (char code 1 in custom font)
    v++;
  }
  a2[v] = 0;
  TFTscreen.stroke(0,0,0);                      //in black, to blank out the previous character
  TFTscreen.text(a2,x,y);
  TFTscreen.stroke(255,255,255);                //then print new text in white
  TFTscreen.text(a1,x,y);
}


« Last Edit: February 05, 2016, 01:09:46 pm by postuma »

postuma

  • Jr. Member
  • **
  • Posts: 64
Re: Bike computer for use on trainer
« Reply #1 on: February 05, 2016, 01:15:54 pm »
Have updated the code above - fixed a few bugs, mostly related to wheel rotations being missed while code was executing, by using ISRs to capture wheel and pedal rotations. And then the bugs that came from implementing the new code ...

I've also updated the math for power calculations using formula derived from the www.powercurvesensor.com/cycling-trainer-power-curves/ site, as these are more accurate for my trainer than the values from Kurt Kinetic.

Happily, my trainer computer comes within 0.02% of values derived from a known-good bike computer!



postuma

  • Jr. Member
  • **
  • Posts: 64
Re: Bike computer for use on trainer
« Reply #2 on: February 05, 2016, 03:58:55 pm »
and below, the truncated/modified GLCDFONT.C. Numbers and letters are in their original positions - for ease of coding. Truncating the font after 'z' saves 660 bytes compiled. Additionally, I have remapped extended ASCII code 219 character (block, or █) to position 1 in the font. This allows me to blank out any previous character in a given position, using the same code used to write new text, saving even more on compile size.

Code: [Select]
#ifndef ARDUINO_ARCH_SAM
#include <avr/io.h>
#endif
#include <avr/pgmspace.h>
 
#ifndef FONT5X7_H
#define FONT5X7_H

// standard ascii 5x7 font

static const unsigned char  font[] PROGMEM = {
        0x00, 0x00, 0x00, 0x00, 0x00,   
0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x3E, 0x6B, 0x4F, 0x6B, 0x3E,
0x1C, 0x3E, 0x7C, 0x3E, 0x1C,
0x18, 0x3C, 0x7E, 0x3C, 0x18,
0x1C, 0x57, 0x7D, 0x57, 0x1C,
0x1C, 0x5E, 0x7F, 0x5E, 0x1C,
0x00, 0x18, 0x3C, 0x18, 0x00,
0xFF, 0xE7, 0xC3, 0xE7, 0xFF,
0x00, 0x18, 0x24, 0x18, 0x00,
0xFF, 0xE7, 0xDB, 0xE7, 0xFF,
0x30, 0x48, 0x3A, 0x06, 0x0E,
0x26, 0x29, 0x79, 0x29, 0x26,
0x40, 0x7F, 0x05, 0x05, 0x07,
0x40, 0x7F, 0x05, 0x25, 0x3F,
0x5A, 0x3C, 0xE7, 0x3C, 0x5A,
0x7F, 0x3E, 0x1C, 0x1C, 0x08,
0x08, 0x1C, 0x1C, 0x3E, 0x7F,
0x14, 0x22, 0x7F, 0x22, 0x14,
0x5F, 0x5F, 0x00, 0x5F, 0x5F,
0x06, 0x09, 0x7F, 0x01, 0x7F,
0x00, 0x66, 0x89, 0x95, 0x6A,
0x60, 0x60, 0x60, 0x60, 0x60,
0x94, 0xA2, 0xFF, 0xA2, 0x94,
0x08, 0x04, 0x7E, 0x04, 0x08,
0x10, 0x20, 0x7E, 0x20, 0x10,
0x08, 0x08, 0x2A, 0x1C, 0x08,
0x08, 0x1C, 0x2A, 0x08, 0x08,
0x1E, 0x10, 0x10, 0x10, 0x10,
0x0C, 0x1E, 0x0C, 0x1E, 0x0C,
0x30, 0x38, 0x3E, 0x38, 0x30,
0x06, 0x0E, 0x3E, 0x0E, 0x06,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x5F, 0x00, 0x00,
0x00, 0x07, 0x00, 0x07, 0x00,
0x14, 0x7F, 0x14, 0x7F, 0x14,
0x24, 0x2A, 0x7F, 0x2A, 0x12,
0x23, 0x13, 0x08, 0x64, 0x62,
0x36, 0x49, 0x56, 0x20, 0x50,
0x00, 0x08, 0x07, 0x03, 0x00,
0x00, 0x1C, 0x22, 0x41, 0x00,
0x00, 0x41, 0x22, 0x1C, 0x00,
0x2A, 0x1C, 0x7F, 0x1C, 0x2A,
0x08, 0x08, 0x3E, 0x08, 0x08,
0x00, 0x80, 0x70, 0x30, 0x00,
0x08, 0x08, 0x08, 0x08, 0x08,
0x00, 0x00, 0x60, 0x60, 0x00,
0x20, 0x10, 0x08, 0x04, 0x02,
0x3E, 0x51, 0x49, 0x45, 0x3E,
0x00, 0x42, 0x7F, 0x40, 0x00,
0x72, 0x49, 0x49, 0x49, 0x46,
0x21, 0x41, 0x49, 0x4D, 0x33,
0x18, 0x14, 0x12, 0x7F, 0x10,
0x27, 0x45, 0x45, 0x45, 0x39,
0x3C, 0x4A, 0x49, 0x49, 0x31,
0x41, 0x21, 0x11, 0x09, 0x07,
0x36, 0x49, 0x49, 0x49, 0x36,
0x46, 0x49, 0x49, 0x29, 0x1E,
0x00, 0x00, 0x14, 0x00, 0x00,
0x00, 0x40, 0x34, 0x00, 0x00,
0x00, 0x08, 0x14, 0x22, 0x41,
0x14, 0x14, 0x14, 0x14, 0x14,
0x00, 0x41, 0x22, 0x14, 0x08,
0x02, 0x01, 0x59, 0x09, 0x06,
0x3E, 0x41, 0x5D, 0x59, 0x4E,
0x7C, 0x12, 0x11, 0x12, 0x7C,
0x7F, 0x49, 0x49, 0x49, 0x36,
0x3E, 0x41, 0x41, 0x41, 0x22,
0x7F, 0x41, 0x41, 0x41, 0x3E,
0x7F, 0x49, 0x49, 0x49, 0x41,
0x7F, 0x09, 0x09, 0x09, 0x01,
0x3E, 0x41, 0x41, 0x51, 0x73,
0x7F, 0x08, 0x08, 0x08, 0x7F,
0x00, 0x41, 0x7F, 0x41, 0x00,
0x20, 0x40, 0x41, 0x3F, 0x01,
0x7F, 0x08, 0x14, 0x22, 0x41,
0x7F, 0x40, 0x40, 0x40, 0x40,
0x7F, 0x02, 0x1C, 0x02, 0x7F,
0x7F, 0x04, 0x08, 0x10, 0x7F,
0x3E, 0x41, 0x41, 0x41, 0x3E,
0x7F, 0x09, 0x09, 0x09, 0x06,
0x3E, 0x41, 0x51, 0x21, 0x5E,
0x7F, 0x09, 0x19, 0x29, 0x46,
0x26, 0x49, 0x49, 0x49, 0x32,
0x03, 0x01, 0x7F, 0x01, 0x03,
0x3F, 0x40, 0x40, 0x40, 0x3F,
0x1F, 0x20, 0x40, 0x20, 0x1F,
0x3F, 0x40, 0x38, 0x40, 0x3F,
0x63, 0x14, 0x08, 0x14, 0x63,
0x03, 0x04, 0x78, 0x04, 0x03,
0x61, 0x59, 0x49, 0x4D, 0x43,
0x00, 0x7F, 0x41, 0x41, 0x41,
0x02, 0x04, 0x08, 0x10, 0x20,
0x00, 0x41, 0x41, 0x41, 0x7F,
0x04, 0x02, 0x01, 0x02, 0x04,
0x40, 0x40, 0x40, 0x40, 0x40,
0x00, 0x03, 0x07, 0x08, 0x00,
0x20, 0x54, 0x54, 0x78, 0x40,
0x7F, 0x28, 0x44, 0x44, 0x38,
0x38, 0x44, 0x44, 0x44, 0x28,
0x38, 0x44, 0x44, 0x28, 0x7F,
0x38, 0x54, 0x54, 0x54, 0x18,
0x00, 0x08, 0x7E, 0x09, 0x02,
0x18, 0xA4, 0xA4, 0x9C, 0x78,
0x7F, 0x08, 0x04, 0x04, 0x78,
0x00, 0x44, 0x7D, 0x40, 0x00,
0x20, 0x40, 0x40, 0x3D, 0x00,
0x7F, 0x10, 0x28, 0x44, 0x00,
0x00, 0x41, 0x7F, 0x40, 0x00,
0x7C, 0x04, 0x78, 0x04, 0x78,
0x7C, 0x08, 0x04, 0x04, 0x78,
0x38, 0x44, 0x44, 0x44, 0x38,
0xFC, 0x18, 0x24, 0x24, 0x18,
0x18, 0x24, 0x24, 0x18, 0xFC,
0x7C, 0x08, 0x04, 0x04, 0x08,
0x48, 0x54, 0x54, 0x54, 0x24,
0x04, 0x04, 0x3F, 0x44, 0x24,
0x3C, 0x40, 0x40, 0x20, 0x7C,
0x1C, 0x20, 0x40, 0x20, 0x1C,
0x3C, 0x40, 0x30, 0x40, 0x3C,
0x44, 0x28, 0x10, 0x28, 0x44,
0x4C, 0x90, 0x90, 0x90, 0x7C,
0x44, 0x64, 0x54, 0x4C, 0x44,
};
#endif