├── LCD.ino ├── README.mediawiki ├── do_crc.ino ├── do_segment.ino ├── fscale.ino ├── habsim.ino ├── output_NEMA.ino ├── testButtons.ino ├── update_alt.ino ├── update_wind.ino └── update_wind_walk.ino /LCD.ino: -------------------------------------------------------------------------------- 1 | void LCDFlight() 2 | { 3 | //Lat & lon 4 | if(CurLat>0&&CurLat<10) lcd.setCursor(2, 0); //eg. 2.345 5 | if(CurLat>10) lcd.setCursor(1, 0); //eg. 23.456 6 | if(CurLat<0&&CurLat>-10) lcd.setCursor(1, 0); //eg. -2.345 7 | if(CurLat<-10) lcd.setCursor(0, 0); //eg. -23.456 8 | lcd.print(CurLat,3); 9 | 10 | if(CurLon>0&&CurLon<10) lcd.setCursor(2, 1); //eg. 2.345 11 | if(CurLon>10) lcd.setCursor(1, 1); //eg. 23.456 12 | if(CurLon<0&&CurLon>-10) lcd.setCursor(1, 1); //eg. -2.345 13 | if(CurLon<-10) lcd.setCursor(0, 1); //eg. -23.456 14 | lcd.print(CurLon,3); 15 | 16 | 17 | //Current Alt 18 | if(CurAlt>10000) 19 | { 20 | lcd.setCursor(8, 0); 21 | } 22 | 23 | if(CurAlt<10000) 24 | { 25 | lcd.setCursor(8, 0); 26 | lcd.print(" "); 27 | lcd.setCursor(9, 0); 28 | } 29 | 30 | if(CurAlt<1000) 31 | { 32 | lcd.setCursor(8, 0); 33 | lcd.print(" "); 34 | lcd.setCursor(10, 0); 35 | } 36 | 37 | if(CurAlt<100) 38 | { 39 | lcd.setCursor(8, 0); 40 | lcd.print(" "); 41 | lcd.setCursor(10, 0); 42 | } 43 | 44 | if(CurAlt<10) 45 | { 46 | lcd.setCursor(8, 0); 47 | lcd.print(" "); 48 | lcd.setCursor(11, 0); 49 | } 50 | 51 | if(CurAlt<1) 52 | { 53 | lcd.setCursor(8, 0); 54 | lcd.print(" "); 55 | lcd.setCursor(12, 0); 56 | } 57 | 58 | lcd.print(CurAlt,0); 59 | 60 | 61 | 62 | // Current Speed ////////////BUG HERE//////////// 63 | 64 | if(CurSpeed>9) 65 | { 66 | lcd.setCursor(14, 0); 67 | } 68 | if(CurSpeed<10) 69 | { 70 | lcd.setCursor(14, 0); 71 | lcd.print(" "); 72 | lcd.setCursor(15, 0); 73 | } 74 | lcd.print(CurSpeed,0); 75 | 76 | 77 | 78 | 79 | 80 | // Current Pressure 81 | 82 | if(CurKPA>100) 83 | { 84 | lcd.setCursor(9, 1); 85 | } 86 | 87 | if(CurKPA<100) 88 | { 89 | lcd.setCursor(9, 1); 90 | lcd.print(" "); 91 | lcd.setCursor(10, 1); 92 | } 93 | 94 | if(CurKPA<10) 95 | { 96 | lcd.setCursor(9, 1); 97 | lcd.print(" "); 98 | lcd.setCursor(11, 1); 99 | } 100 | lcd.print(CurKPA,2); 101 | 102 | 103 | 104 | // Status Icon 105 | 106 | lcd.setCursor(15, 1); 107 | 108 | if(Status==0) lcd.print((char)0); 109 | if(Status==1) lcd.print((char)1); 110 | if(Status==2) lcd.print((char)2); 111 | 112 | 113 | 114 | // graph 115 | float graph_pos = fscale(0,35000,0,9,CurAlt,0); 116 | 117 | 118 | lcd.setCursor(7, 0); 119 | lcd.print((char)3); 120 | lcd.setCursor(7, 1); 121 | lcd.print((char)3); 122 | 123 | 124 | lcd.setCursor(7, 1); 125 | if(int(graph_pos)==0) lcd.print((char)3); 126 | if(int(graph_pos)==1) lcd.print((char)4); 127 | if(int(graph_pos)==2) lcd.print((char)5); 128 | if(int(graph_pos)==3) lcd.print((char)6); 129 | if(int(graph_pos)==4) lcd.print((char)7); 130 | 131 | lcd.setCursor(7, 0); 132 | if(int(graph_pos)==5) lcd.print((char)4); 133 | if(int(graph_pos)==6) lcd.print((char)5); 134 | if(int(graph_pos)==7) lcd.print((char)6); 135 | if(int(graph_pos)==8) lcd.print((char)7); 136 | 137 | } 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /README.mediawiki: -------------------------------------------------------------------------------- 1 | ===What is GPSsim?=== 2 | GPSsim is a project to create a drop in replacement for a physical GPS unit in a [http://www.ukhas.org.uk HAB] payload. It's primary roll is to allow for Hardware-in-the-loop testing of payloads during the development and testing poccess. By replacing the flight hardware GPS module of your payload with a connection to GPSsim you can allow your payload to 'dream' that it's flying. While on the workbench (or freezer!) you can test any number of in flight logic processes, such as altitude (or GPS boundary) Cutdown triggers, Camera control, or telemetry testing. 3 | 4 | ===How do I connect it?=== 5 | The Arduino running the GPSsim firmware is connected to the payload's flight computer in the same way that physical GPS device connects. Connect the RX/TX ((usually) pins 0 and 1) on the GPSsim Arduino to the GPS input to your flight computer (so that the GPS TX pin is connected to the flight computer's GPS RX line, an the RX to TX). In addition to this you need to (probably should) connect the a ground line between the GPSsim Arduino to a ground line on your Flight computer, to ensure good RS232 TTL communication. 6 | 7 | ===How do I get it going?=== 8 | Just like a physical GPS unit, once powered up, GPSsim will start spewing forth NMEA sentences. Currently, the flight simulation also starts on power up, but this could be delayed. 9 | 10 | ===Can I configure it to my needs?=== 11 | Yes, there are lots of #define calls at the top of the code that can be edited to change parameters such as: 12 | * Lat/Lon/Alt/kPa of launch site 13 | * Targeted Ascent and Descent rate 14 | * Terminal Velocity of payload 15 | * GPS HZ 16 | * Burst kPa (Outside pressure at which balloon will burst) 17 | 18 | Throughout the code there are also comments which point out variables that can also be changed to suit your needs, such as the Baud rate of the serial connection and the GPS "time" 19 | 20 | ===How does the simulation work, and how accurate is it?=== 21 | GPSsim, is not a fantastically accurate simulation of the dynamics of a balloon in flight, but it should be fit for purpose as it's described above. If you take the output of a GPSsim run and push it into Google Earth as raw NEMA sentences, it looks pretty similar to to paths of many of the balloon flight data tracks on the [http://ukhas.org.uk UKHAS wiki]. There are few different processes going on in the code, and it's worth briefly discussing each one independently. (It has already been pointed out to me on IRC that the accuracy of simulation is severely compromised due to the absence algorithms during descent that describe the immense magnetic influence of Trees/Powerlines/North Sea/Heathrow/Girls???) 22 | ====Vertical Simulation==== 23 | The Vertical simulation (the balloon's ascent and descent) are possibly the most accurate elements of the whole simulation (although that's not saying much!). The maths for this is all contained with the update_alt() function. The specific part of this which is relatively accurate is the implementation of the [http://en.wikipedia.org/wiki/International_Standard_Atmosphere International Standard Atmosphere Model] which is used in this context to determine when the balloon will burst. Once burst, and descending the payload drops at it's terminal velocity while drag increases to slow it, again as a function of the pressure verses altitude (this time working on the parachute). The drag calculation is not at all accurate, but serves our purpose of realistically slowing the payload. Two other inaccuracies in the Vertical simulation are the instant acceleration to terminal velocity (minus drag) rather than an acceleration of (9.8m/s^2 - drag), and the rather rude clamping of the descent speed so that it can never fall below the targeted descent speed. Neither of these are believed to have a major impact on the usefulness of the simulation as implemented (and saved me a lot of time and headaches!) 24 | ====Horizontal Simulation==== 25 | The Horizontal simulation deals with the movement of the balloon due to the effects of wind. It is almost completely powered by nonsense and has very little baring on reality, other than producing and end result that looks slightly plausible. The function update_wind() takes in as it's parameters, the current Lat and Lon of the balloon, the direction the wind is blowing, and how far the wind has blown the balloon in this 'step' of the simulation. This is then fed into an implementation of the [http://www.movable-type.co.uk/scripts/latlong-vincenty-direct.html Vincenty Direct] equation which updates CurLat and CurLon with the new co-ordinates after this step of the simulation. The direction the wind blows in based on a simple random walk algorithm, changing slightly throughout the flight. It is intended that the balloon will undergo a major change of direction and speed when it transitions the tropopause, but this isn't implemented yet. 26 | ====NMEA output==== 27 | The GPSsim outputs NMEA sentences on the serial port in an exact simulation (on of the more accurate parts of the simulation) of a real GPS unit running in NMEA mode. The main reason I can be confident of the accuracy of the simulation here, is that I didn't write it myself, it's a Arduino port of the existing [http://ukhas.org.uk/code:emulator GPSGen emulator] (specifically the emulate.c file) on the UKHAS wiki. Full credit goes to the author (rocketboy?) of this code for a job well done. Currently the NMEA output renderer runs independently from the simulation, so that you can simulate a 1Hz GPS unit while the simulator ticks over at about 50Hz. However, due to a bug I've found in the Vincenty Direct equation, when presented with changing values for windspeed (or more specifically distance per simulator step) I may have to lockstep the simulator and NMEA renderer to the desired GPS output Hz. 28 | ====LCD output==== 29 | GPSsim supports and makes use of the common two line LCD shields that are common in Arduino world. I've developed it using [http://www.dfrobot.com/index.php?route=product/product&product_id=51 this model], but there are [http://www.ebay.co.uk/sch/i.html?_nkw=arduino+lcd+1602+keypad+shield many clones on ebay] that are built to exactly the the same plan. They should all work. The LCD gives you output on the current Lat/Lon, altitude, outside pressure, and ascent/descent rate. It also has a glyph for the balloon state (Ascent/descent/landed) and an altitude graph. It's quite a busy little screen. The sheild also has buttons, and using the 'select' button you can toggle acceleration of the simulation by about x100. 30 | 31 | ===I still don't get it, why is this useful, why use an Arduino?=== 32 | I come from a background (i.e. my other hobby is...) UAV's (of the Ardupilot kind). In the UAV world we are taught at an early age that hardware-in-the-loop testing is the most important phase of development. By fooling the flight computer into thinking it's really flying (dreaming) you can discover and nail many bugs before they surprise you in the air. (Surprise situations in the UAV world (as in HAB), tend not to be recoverable). You can use a laptop/desktop PC/Mac to emulate the GPS and build your HIL testing rig, but when HAB flight times last at least a couple of hours, do you want to tie up your PC for that long? The Arduino is more than capable of performing this simulation anyway, it seems well suited. There may even be applications for this in the field as part of a flight readiness test, when a laptop isn't available, but having flown yet myself, I wouldn't know .. sigh.. 33 | 34 | ===Further Development=== 35 | I offer this code out to the HAB community, please fork if you wish, or mail me to be added as a developer to the project if you are able to contribute directly. It's going to be a useful tool for me in the development of my HAB flight hardware, and if anyone else finds it at all useful then that's a win for us all. I'm happy to help support the use of this as far as my time allows, both in terms of advise on setting it up, and with code bugs/feature requests (just add them to GitHub) 36 | 37 | You can mail me jim at jimblackhurst dot c o m, or catch me in the [http://webchat.freenode.net/?channels=highaltitude UKHAS irc channel] as Jimthree or Jim3 38 | -------------------------------------------------------------------------------- /do_crc.ino: -------------------------------------------------------------------------------- 1 | // calculate a CRC for the line of input 2 | // tested against http://nmeachecksum.eqth.net/ 3 | 4 | void do_crc(char *pch) 5 | { 6 | unsigned char crc; 7 | 8 | //PString CRC_buf(buf, sizeof(buf)); 9 | 10 | if (*pch != '$') 11 | return; // does not start with '$' - so can't CRC 12 | 13 | pch++; // skip '$' 14 | crc = 0; 15 | 16 | // scan between '$' and '*' (or until CR LF or EOL) 17 | while ((*pch != '*') && (*pch != '\0') && (*pch != '\r') && (*pch != '\n')) 18 | { // checksum calcualtion done over characters between '$' and '*' 19 | crc ^= *pch; 20 | pch++; 21 | } 22 | 23 | // add or re-write checksum 24 | //sprintf(pch,"*%02X\r\n",(unsigned int)crc); 25 | 26 | Serial.println((unsigned int)crc,HEX); 27 | 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /do_segment.ino: -------------------------------------------------------------------------------- 1 | void do_segment(double FromLat, double FromLon, double FromAlt, double ToLat, double ToLon, double ToAlt) 2 | { 3 | double Rate; // ascent(+ve) / decent(-ve) rate meters per second (for segment) 4 | int Duration; // calculated segment duration in seconds 5 | double Elapsed; // floating point duration 6 | double Lat,Lon,Alt; // clacualted for each step 7 | double Course,Speed; // calculated for entire segment 8 | double Distance; // distance between coordinates 9 | double DeltaLat,DeltaLon,DeltaAlt; // Latitude, Longtitude and Altitude differences 10 | double AdjLon; // Adjusted Longtitude difference 11 | int i; // counter 12 | 13 | DeltaAlt = (ToAlt - FromAlt); 14 | DeltaLat = (ToLat - FromLat); 15 | DeltaLon = (ToLon - FromLon); 16 | 17 | 18 | if (DeltaAlt >= 0) 19 | { // ascending 20 | Rate = 200.0; 21 | } 22 | else 23 | { // descending - clculate the geometric mean of the expected velocities at To and From altitude 24 | Rate = -200.0 * sqrt(pow(LOG_BASE,(FromAlt / LOG_POWER)) * pow(LOG_BASE,(ToAlt / LOG_POWER))); 25 | } 26 | 27 | // calcualte time (secs) between co-ordinates 28 | Elapsed = DeltaAlt / Rate; // always positive 29 | Duration = (int)Elapsed; 30 | if ((Elapsed - (float)Duration) >= 0.5) 31 | Duration++; // round duration of segment to nearest integer 32 | 33 | // Calculate Course (degrees relative to north) for entire segment 34 | Course = atan2(DeltaLat,DeltaLon); // result is +PI radians (cw) to -PI radians (ccw) from x (Longtitude) axis 35 | 36 | Course = DEGREES(Course); // convert radians to degrees 37 | if (Course <= 90.0) 38 | Course = 90.0 - Course; 39 | else 40 | Course = 450.0 - Course; // convert to 0 - 360 clockwise from north 41 | 42 | // Calculate Speed (m/sec) for entire segment 43 | AdjLon = cos(RADIANS((FromLat + ToLat) / 2.0)) * DeltaLon; 44 | Distance = (sqrt((DeltaLat * DeltaLat) + (AdjLon * AdjLon)) * 111194.9266); // result in meters 45 | 46 | Speed = Distance / (double)Duration; // meters per second 47 | 48 | // calculate 1 second "step" sizes 49 | DeltaAlt /= (double)Duration; 50 | DeltaLat /= (double)Duration; 51 | DeltaLon /= (double)Duration; 52 | 53 | // now output the NMEA for each step between From and To (but excluding To - which is picked up on next segment) 54 | Lat = FromLat; 55 | Lon = FromLon; 56 | Alt = FromAlt; 57 | 58 | for (i = 0; i < Duration; i++) 59 | { 60 | delay(1000); //simulate GPS at 1hz 61 | Output_NEMA(Now,Lat,Lon,Alt,Course,Speed); 62 | Now++; // 1 second steps 63 | Lat += DeltaLat; 64 | Lon += DeltaLon; 65 | Alt += DeltaAlt; 66 | 67 | 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /fscale.ino: -------------------------------------------------------------------------------- 1 | float fscale( float originalMin, float originalMax, float newBegin, float newEnd, float inputValue, float curve){ 2 | 3 | float OriginalRange = 0; 4 | float NewRange = 0; 5 | float zeroRefCurVal = 0; 6 | float normalizedCurVal = 0; 7 | float rangedValue = 0; 8 | int invFlag = 0; 9 | 10 | 11 | // condition curve parameter 12 | // limit range 13 | 14 | if (curve > 10) curve = 10; 15 | if (curve < -10) curve = -10; 16 | 17 | curve = (curve * -.1) ; // - invert and scale - this seems more intuitive - postive numbers give more weight to high end on output 18 | curve = pow(10, curve); // convert linear scale into lograthimic exponent for other pow function 19 | 20 | /* 21 | println(curve * 100, DEC); // multply by 100 to preserve resolution 22 | println(); 23 | */ 24 | 25 | // Check for out of range inputValues 26 | if (inputValue < originalMin) { 27 | inputValue = originalMin; 28 | } 29 | if (inputValue > originalMax) { 30 | inputValue = originalMax; 31 | } 32 | 33 | // Zero Refference the values 34 | OriginalRange = originalMax - originalMin; 35 | 36 | if (newEnd > newBegin){ 37 | NewRange = newEnd - newBegin; 38 | } 39 | else 40 | { 41 | NewRange = newBegin - newEnd; 42 | invFlag = 1; 43 | } 44 | 45 | zeroRefCurVal = inputValue - originalMin; 46 | normalizedCurVal = zeroRefCurVal / OriginalRange; // normalize to 0 - 1 float 47 | 48 | /* 49 | print(OriginalRange, DEC); 50 | print(" "); 51 | print(NewRange); 52 | print(" "); 53 | println(zeroRefCurVal); 54 | println(); 55 | */ 56 | 57 | // Check for originalMin > originalMax - the math for all other cases i.e. negative numbers seems to work out fine 58 | if (originalMin > originalMax ) { 59 | return 0; 60 | } 61 | 62 | if (invFlag == 0){ 63 | rangedValue = (pow(normalizedCurVal, curve) * NewRange) + newBegin; 64 | 65 | } 66 | else // invert the ranges 67 | { 68 | rangedValue = newBegin - (pow(normalizedCurVal, curve) * NewRange); 69 | } 70 | 71 | return rangedValue; 72 | } 73 | -------------------------------------------------------------------------------- /habsim.ino: -------------------------------------------------------------------------------- 1 | // GPS Generator and Emulator 2 | // Based on the UKHAS GPSgen Code: http://ukhas.org.uk/code:emulator 3 | // Ported to arduino by Jim Blackhurst 4 | 5 | 6 | // todo 7 | // 1. Documentation, Comments, format 8 | // 2. remove unused DEFINEs and Vars 9 | // 3. implement Cutdown 10 | // DONE 4. review descent code to use pressure func 11 | // DONE 5. accurate timing 12 | // 6. simple button control for SIM rate 13 | // 7. bug with rendering '10' m/s descent rate 14 | // 8. rescale glyph graph on descent 15 | // 9. tune max altitude 16 | // 10. fix wind variability 17 | 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | // radians to degrees 25 | #define DEGREES(x) ((x) * 57.295779513082320877) 26 | // degrees to radians 27 | #define RADIANS(x) ((x) / 57.295779513082320877) 28 | 29 | #define LOG_BASE 1.4142135623730950488 30 | #define LOG_POWER 5300.0 31 | 32 | //#define PI 3.141592653589793 33 | 34 | #define WIND_TURBULENCE 10 // chance of the wind changing the bearing by up to 1 degree per cycle of the simulation 35 | 36 | //#define LAUNCH_LAT 52.213389 37 | //#define LAUNCH_LON 0.096834 38 | #define LAUNCH_LAT 52.1658 39 | #define LAUNCH_LON -1.4250 40 | #define LAUNCH_ALT 203 41 | 42 | #define TERMINAL_VELOCITY 40 // meters per second 43 | #define ASCENT_RATE 5 // meters per second 44 | #define DESCENT_RATE 5 // meters per second 45 | 46 | //should take 20secs to do 100m 47 | //currently takes 50sec to do 100m 48 | 49 | //http://www.engineeringtoolbox.com/air-altitude-pressure-d_462.html 50 | //http://en.wikipedia.org/wiki/Atmospheric_pressure 51 | #define LAUNCH_KPA 100 52 | #define BURST_KPA 0.02 53 | #define GPS_HZ 1 54 | 55 | // SIM_HZ - the internal speed of the simulation 56 | // if you are going to change this then you will need to scale simAccel acordingly 57 | // Faster updates result in smaller distances per step being fed into the horizontal speed 58 | // Equation. Due to the ATMEGA's 8bit arcitecture and 4 byte floats, it looses precision 59 | // and Lon stops updating. 20Hz is the reccomneded value. 60 | #define SIM_HZ 10 61 | 62 | 63 | // DEBUG status - output either the intended NMEA sentances, or debug data 64 | // 1 = debug, 0 = NMEA 65 | #define DEBUG 1 66 | 67 | // Define the pins used by the Liquid Crystal Display 68 | LiquidCrystal lcd(8, 9, 4, 5, 6, 7); 69 | 70 | // Status hold the current state of the sim. 71 | // 0 = Ascent, 1 = Descent, 2 = Landed 72 | int Status =0; 73 | 74 | // simAccel - an artifical acceleration factor for speeds. 75 | // This value can be used to maintain a acurate simulation of both the vertical and 76 | // horizontal speeds by compensating for the time it takes to process the calculations 77 | // It can also be used to run the simulation at a fast-forward speed 78 | // With the sim running at 20Hz, a simAccel value of 1.4 will give accurate speeds 79 | float simAccel = 50; 80 | 81 | time_t Now; 82 | char buf[128]; 83 | 84 | float CurLon,CurLat,CurAlt,CurKPA,CurSpeed; 85 | unsigned long update_counter; 86 | unsigned long output_counter; 87 | float ascent_rate_mod = 0; 88 | float tropo_wind_rate_mod = 0; 89 | float strat_wind_rate_mod = 0; 90 | //float lat_wind = 0; 91 | //float lon_wind = 0; 92 | float windBearing = 0.0; 93 | 94 | float windSpeed = 10.0; 95 | 96 | float distancePerStep = 0.0; 97 | float Drag; 98 | 99 | 100 | long speedTest =0; 101 | float msTest =0; 102 | float speedTestResult = 0; 103 | 104 | 105 | byte balloon[8] = {B01110, B11111, B11111, B11111, B01110, B00100, B01110, B01110}; 106 | byte chute[8] = {B11111, B11111, B10001, B10001, B01010, B00100, B01110, B01110}; 107 | byte land[8] = {B00000, B00000, B00000, B11111, B10101, B00100, B01110, B01110}; 108 | byte scale_0[8] = {B00100, B00000, B00100, B00000, B00100, B00000, B00100, B00000}; 109 | byte scale_1[8] = {B00100, B00000, B00100, B00000, B00100, B00000, B11111, B00000}; 110 | byte scale_2[8] = {B00100, B00000, B00100, B00000, B11111, B00000, B00100, B00000}; 111 | byte scale_3[8] = {B00100, B00000, B11111, B00000, B00100, B00000, B00100, B00000}; 112 | byte scale_4[8] = {B11111, B00000, B00100, B00000, B00100, B00000, B00100, B00000}; 113 | 114 | 115 | void setup() 116 | { 117 | // open the serial port at 9600 bps: 118 | Serial.begin(115200); 119 | 120 | // set up the LCD's number of columns and rows: 121 | lcd.begin(16, 2); 122 | lcd.createChar(0, balloon); 123 | lcd.createChar(1, chute); 124 | lcd.createChar(2, land); 125 | lcd.createChar(3, scale_0); 126 | lcd.createChar(4, scale_1); 127 | lcd.createChar(5, scale_2); 128 | lcd.createChar(6, scale_3); 129 | lcd.createChar(7, scale_4); 130 | 131 | long tempBits = 0; // create a long of random bits to use as seed 132 | for (int i=1; i<=32 ; i++) 133 | { 134 | tempBits = ( tempBits | ( analogRead( 0 ) & 1 ) ) << 1; 135 | } 136 | randomSeed(tempBits); // seed 137 | 138 | windBearing = random(359); 139 | 140 | 141 | setTime(14,14,14,1,3,2012); //Hardcode the datetime, this is effectivly the 'launch' time 142 | Now = now(); //assign the current time to the variable 'Now' 143 | 144 | // get first waypoint co-ordinate as launch position 145 | CurLon = LAUNCH_LON; 146 | CurLat = LAUNCH_LAT; 147 | CurAlt = LAUNCH_ALT; 148 | CurKPA = LAUNCH_KPA; 149 | 150 | update_counter = millis(); 151 | output_counter = millis(); 152 | 153 | lcd.clear(); 154 | 155 | speedTest = millis(); 156 | msTest = CurAlt; 157 | } 158 | 159 | 160 | void loop() 161 | { 162 | Now = now(); 163 | float windOffset = random(10); 164 | if (random(WIND_TURBULENCE)==1) windBearing += (windOffset-5)/10; 165 | if (windBearing>359) windBearing = 0; 166 | if (windBearing<0) windBearing = 359; 167 | 168 | if (CurAlt>2000 && CurAlt<2100) windBearing = random(359); 169 | 170 | 171 | 172 | LCDFlight(); 173 | 174 | if (Status != 2) 175 | { 176 | if (millis() > (update_counter + (1000/SIM_HZ))) 177 | { 178 | update_alt(); 179 | 180 | 181 | distancePerStep = (windSpeed/SIM_HZ)*simAccel; 182 | 183 | updateWindWalk(CurLat, CurLon, windBearing, distancePerStep); 184 | 185 | if(DEBUG) Serial.print("[t"); 186 | if(DEBUG) Serial.print(millis()-update_counter); 187 | if(DEBUG) Serial.print(" :VsA "); 188 | if(DEBUG) Serial.print(speedTestResult); 189 | if(DEBUG) Serial.print("] "); 190 | 191 | update_counter = millis(); 192 | 193 | if(DEBUG) Serial.println(); 194 | } 195 | 196 | if (millis() > (output_counter + (1000/GPS_HZ))) 197 | { 198 | if (!DEBUG) Output_NEMA(Now,CurLat,CurLon,CurAlt,windBearing,CurSpeed); 199 | output_counter = millis(); 200 | } 201 | } 202 | 203 | 204 | 205 | 206 | if (millis()>=(speedTest+10000)) 207 | { 208 | speedTestResult = (CurAlt-msTest)/10; 209 | msTest=CurAlt; 210 | speedTest = millis(); 211 | } 212 | } -------------------------------------------------------------------------------- /output_NEMA.ino: -------------------------------------------------------------------------------- 1 | // Speed in Kph 2 | // Course over ground relative to North 3 | // 4 | // In normal operation the GPS emulated sends the following sequence of messages 5 | // $GPGGA, $GPRMC, $GPVTG 6 | // $GPGGA, $GPGSA, $GPRMC, $GPVTG 7 | // $GPGGA, $GPGSV ... $GPGSV, $GPRMC, $GPVTG 8 | 9 | 10 | // Output_NEMA - Output the position in NMEA 11 | // Latitude & Longtitude in degrees, Altitude in meters Speed in meters/sec 12 | 13 | void Output_NEMA(time_t Time, double Lat, double Lon, double Alt, double Course, double Speed) 14 | { 15 | int LatDeg; // latitude - degree part 16 | double LatMin; // latitude - minute part 17 | char LatDir; // latitude - direction N/S 18 | int LonDeg; // longtitude - degree part 19 | double LonMin; // longtitude - minute part 20 | char LonDir; // longtitude - direction E/W 21 | 22 | if (Lat >= 0) 23 | LatDir = 'N'; 24 | else 25 | LatDir = 'S'; 26 | 27 | Lat = fabs(Lat); 28 | LatDeg = (int)(Lat); 29 | LatMin = (Lat - (double)LatDeg) * 60.0; 30 | 31 | if (Lon >= 0) 32 | LonDir = 'E'; 33 | else 34 | LonDir = 'W'; 35 | 36 | Lon = fabs(Lon); 37 | LonDeg = (int)(Lon); 38 | LonMin = (Lon - (double)LonDeg) * 60.0; 39 | 40 | // $GPGGA - 1st in epoc - 5 satellites in view, FixQual = 1, 45m Geoidal separation HDOP = 2.4 41 | 42 | PString GPGGA(buf, sizeof(buf)); 43 | 44 | GPGGA.print("$GPGGA,"); 45 | GPGGA.print(hour(Time)); 46 | GPGGA.print(minute(Time)); 47 | GPGGA.print(second(Time)); 48 | GPGGA.print(","); 49 | GPGGA.print(LatDeg); 50 | if (LatMin < 10) GPGGA.print("0"); 51 | GPGGA.print(LatMin); 52 | GPGGA.print(","); 53 | GPGGA.print(LatDir); 54 | GPGGA.print(","); 55 | GPGGA.print(LonDeg); 56 | if (LonMin < 10) GPGGA.print("0"); 57 | GPGGA.print(LonMin); 58 | GPGGA.print(","); 59 | GPGGA.print(LonDir); 60 | GPGGA.print(",1,05,02.4,"); 61 | GPGGA.print(Alt); 62 | GPGGA.print(",M,45.0,M,,*"); 63 | 64 | Serial.print(buf); // Print Buf 65 | do_crc(buf); // Print CRC 66 | 67 | switch((int)Time % 3) 68 | { // include 'none' or $GPGSA or $GPGSV in 3 second cycle 69 | case 0: 70 | { 71 | break; 72 | } 73 | case -1: 74 | { 75 | // 3D fix - 5 satellites (3,7,18,19 & 22) in view. PDOP = 3.3,HDOP = 2.4, VDOP = 2.3 76 | PString GPGSA(buf, sizeof(buf),"$GPGSA,A,3,03,07,18,19,22,,,,,,,,3.3,2.4,2.3*"); 77 | 78 | Serial.print(buf); // Print Buf 79 | do_crc(buf); // Print CRC 80 | break; 81 | } 82 | case -2: 83 | { 84 | // two lines of GPGSV messages - 1st line of 2, 8 satellites being tracked in total 85 | // 03,07 in view 11,12 being tracked 86 | PString GPGSV1(buf, sizeof(buf),"$GPGSV,2,1,08,03,89,276,30,07,63,181,22,11,,,,12,,,*"); 87 | Serial.print(buf); // Print Buf 88 | do_crc(buf); // Print CRC 89 | // GPGSV 2nd line of 2, 8 satellites being tracked in total 90 | // 18,19,22 in view 27 being tracked 91 | PString GPGSV2(buf, sizeof(buf),"$GPGSV,2.2,08,18,73,111,35,19,33,057,27,22,57,173,37,27,,,*"); 92 | Serial.print(buf); // Print Buf 93 | do_crc(buf); // Print CRC 94 | break; 95 | } 96 | } 97 | 98 | //$GPRMC 99 | //sprintf(buf,"$GPRMC,%02d%02d%02d.000,A,%02d%07.4f,%c,%03d%07.4f,%c,%.2f,%.2f,%02d%02d%02d,,,A*", 100 | // hour(Time),minute(Time),second(Time),LatDeg,LatMin,LatDir,LonDeg,LonMin,LonDir,Speed * 1.943844,Course,day(Time),month(Time) + 1,year(Time) % 100); 101 | 102 | PString GPRMC(buf, sizeof(buf)); 103 | 104 | GPRMC.print("$GPRMC,"); 105 | GPRMC.print(hour(Time)); 106 | GPRMC.print(minute(Time)); 107 | GPRMC.print(second(Time)); 108 | GPRMC.print(",A,"); 109 | GPRMC.print(LatDeg); 110 | if (LatMin < 10) GPRMC.print("0"); 111 | GPRMC.print(LatMin); 112 | GPRMC.print(","); 113 | GPRMC.print(LatDir); 114 | GPRMC.print(","); 115 | GPRMC.print(LonDeg); 116 | if (LonMin < 10) GPRMC.print("0"); 117 | GPRMC.print(LonMin); 118 | GPRMC.print(","); 119 | GPRMC.print(LonDir); 120 | 121 | GPRMC.print(","); 122 | GPRMC.print(Speed * 1.943844); 123 | GPRMC.print(","); 124 | GPRMC.print(Course); 125 | GPRMC.print(","); 126 | GPRMC.print(day(Time)); 127 | GPRMC.print(month(Time) + 1); 128 | GPRMC.print(year(Time) % 100); 129 | GPRMC.print(",,,A*"); 130 | 131 | Serial.print(buf); // Print Buf 132 | do_crc(buf); // Print CRC 133 | 134 | // $GPVTG message last in epoc 135 | //sprintf(buf,"$GPVTG,%.2f,T,,,%.2f,N,%.2f,K,A*",Course,Speed * 1.943844,Speed * 3.6); 136 | 137 | PString GPVTG(buf, sizeof(buf)); 138 | 139 | GPVTG.print("$GPVTG,"); 140 | GPVTG.print(Course); 141 | GPVTG.print(",T,,,"); 142 | GPVTG.print(Speed * 1.943844); 143 | GPVTG.print(",N,"); 144 | GPVTG.print(Speed * 3.6); 145 | GPVTG.print(",K,A*"); 146 | 147 | Serial.print(buf); // Print Buf 148 | do_crc(buf); // Print CRC 149 | 150 | 151 | } 152 | 153 | -------------------------------------------------------------------------------- /testButtons.ino: -------------------------------------------------------------------------------- 1 | /* 2 | void testButtons() 3 | { 4 | int value = analogRead(0); 5 | 6 | if (value>1020) return; 7 | 8 | if (value>700&&value<800) // select 9 | { 10 | return; 11 | } 12 | 13 | if (value>0&&value<100) // right 14 | { 15 | lastMenuScreen++; 16 | LCDMenu(lastMenuScreen); 17 | } 18 | 19 | if (value>400&&value<550) // right 20 | { 21 | lastMenuScreen--; 22 | LCDMenu(lastMenuScreen); 23 | } 24 | } 25 | */ 26 | 27 | 28 | /* 29 | // select 722 30 | // left 480 31 | // right 0 32 | // up 131 33 | // down 307/8 34 | 35 | int adc_key_val[5] ={0, 150, 360, 535, 760 }; 36 | 37 | */ 38 | -------------------------------------------------------------------------------- /update_alt.ino: -------------------------------------------------------------------------------- 1 | void update_alt() 2 | { 3 | 4 | 5 | // If the Current altitude ever falls below the LAUNCH _ALT, then the payload is deemed to have landed 6 | if (CurAlt < LAUNCH_ALT) 7 | { 8 | Status = 2; 9 | return; 10 | } 11 | 12 | // http://artvb.oatmeal.dhs.org/?c=Glider 13 | // update altitude 14 | 15 | if (Status == 0) // Ascent 16 | { 17 | // while Ascending, the speed is simply the ASCENT_RATE 18 | CurSpeed = ASCENT_RATE; 19 | 20 | // We currently use a linear model for asscent, based on the ASCENT_RATE 21 | CurAlt += (CurSpeed*simAccel)/SIM_HZ; 22 | 23 | // As we ascend the atmospheric pressure reduces. Again we are using a linear aproximation 24 | // although there are well published equations for how KpA falls off with altitude. 25 | CurKPA = getPressure(CurAlt); 26 | 27 | // If the outside pressure falls below that of what is defined as BURST_KPA, the balloon is 28 | // deemed to have popped, and the payload starts it's descent 29 | if (CurKPA < BURST_KPA) Status = 1; 30 | } 31 | 32 | if (Status == 1) //Descent 33 | { 34 | // As we descend back though the atmospheric pressure increases. 35 | // Again we are using a linear aproximation which needs to be fixed 36 | CurKPA = getPressure(CurAlt); 37 | 38 | // The payload initaly falls at it's terminal velocity 39 | // (although it should really accelerate towards it's TV at 9.8m/s sq - ignored for simplicity) 40 | // as the KpA increases, so does drag, eventualy slowing 41 | // the payload to it's ideal descent rate. 42 | 43 | 44 | // Drag is a normalised value bsed on the current KPA where the KPA at burst altitude 45 | // results in a vale for Drag ~1 and the KPA at sea level is gives a value ~= 0. 46 | // this is effectivly saying how far removed CurSpeed is from TERMINAL_VELOCITY 47 | 48 | // Note: these are (obviously) not real equations for calculating Drag, they simply simulate the behaviour. 49 | 50 | Drag = 1-fscale(0,100,0,1,CurKPA,0); 51 | CurSpeed = TERMINAL_VELOCITY*Drag; 52 | 53 | // the payload is not able to Fall at speeds slower than it's targeted DESCENT_RATE 54 | if (CurSpeed 1e-12) 65 | { 66 | cos2SigmaM = cos(2*sigma1 + sigma); 67 | sinSigma = sin(sigma); 68 | cosSigma = cos(sigma); 69 | deltaSigma = B*sinSigma*(cos2SigmaM+B/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM) - B/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM))); 70 | sigmaP = sigma; 71 | sigma = s / (b*A) + deltaSigma; 72 | } 73 | 74 | float tmp = sinU1*sinSigma - cosU1*cosSigma*cosAlpha1; 75 | float lat2 = atan2(sinU1*cosSigma + cosU1*sinSigma*cosAlpha1,(1-f)*sqrt(sinAlpha*sinAlpha + tmp*tmp)); 76 | float lambda = atan2(sinSigma*sinAlpha1, cosU1*cosSigma - sinU1*sinSigma*cosAlpha1); 77 | float C = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha)); 78 | float L = lambda - (1-C) * f * sinAlpha * (sigma + C*sinSigma*(cos2SigmaM+C*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM))); 79 | 80 | float lon2 = fmod((radians(lon1)+L+3*PI) , (2*PI)) - PI; // normalise to -180...+180 81 | 82 | float revAz = atan2(sinAlpha, -tmp); // final bearing, if required 83 | 84 | CurLat = degrees(lat2); 85 | CurLon = degrees(lon2); 86 | 87 | // if(DEBUG) Serial.print(" :: WindBearing:",6); 88 | // if(DEBUG) Serial.print(windBearing); 89 | if(DEBUG) Serial.print(":: bearing:"); 90 | if(DEBUG) Serial.print(brng, 2); 91 | if(DEBUG) Serial.print(" :: d: "); 92 | if(DEBUG) Serial.print(distancePerStep,4); 93 | if(DEBUG) Serial.print(" :: CurLat: "); 94 | if(DEBUG) Serial.print(CurLat,8); 95 | if(DEBUG) Serial.print(" :: CurLon: "); 96 | if(DEBUG) Serial.print(CurLon,8); 97 | 98 | } 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | // would not have been possible if it wasn't for: 121 | // http://www.movable-type.co.uk/scripts/latlong.html 122 | // Destination point given distance and bearing from start point 123 | 124 | // useful blending function: 125 | // http://danthompsonsblog.blogspot.com/2009/02/smoothstep-interpolation-with-arduino.html 126 | 127 | 128 | void update_wind() 129 | { 130 | 131 | /** 132 | * Returns the destination point from this point having travelled the given distance (in km) on the 133 | * given initial bearing (bearing may vary before destination is reached) 134 | * 135 | * see http://williams.best.vwh.net/avform.htm#LL 136 | * 137 | * @param {Number} brng: Initial bearing in degrees 138 | * @param {Number} dist: Distance in km 139 | * @returns {LatLon} Destination point 140 | 141 | 142 | //LatLon.prototype.destinationPoint = function(brng, dist) { 143 | dist = typeof(dist)=='number' ? dist : typeof(dist)=='string' && dist.trim()!='' ? +dist : NaN; 144 | dist = dist/this._radius; // convert dist to angular distance in radians 145 | brng = brng.toRad(); // 146 | 147 | 148 | var lat1 = this._lat.toRad(), lon1 = this._lon.toRad(); 149 | 150 | var lat2 = Math.asin( Math.sin(lat1)*Math.cos(dist) + Math.cos(lat1)*Math.sin(dist)*Math.cos(brng) ); 151 | 152 | var lon2 = lon1 + Math.atan2(Math.sin(brng)*Math.sin(dist)*Math.cos(lat1), Math.cos(dist)-Math.sin(lat1)*Math.sin(lat2)); 153 | 154 | 155 | lon2 = (lon2+3*Math.PI) % (2*Math.PI) - Math.PI; // normalise to -180..+180º 156 | 157 | return new LatLon(lat2.toDeg(), lon2.toDeg()); 158 | //} 159 | */ 160 | 161 | /* 162 | int radius = 6371; // mean radius of the earth 163 | int Bearing = 90; // Bearing of travel 164 | int Dist = 1; // km per update 165 | Bearing = radians(Bearing); // convert Bearing to Radians 166 | Dist = Dist/radius; // convert dist to angular distance in radians 167 | 168 | float DestLat = asin(sin(CurLat)*cos(Dist)+cos(CurLat)*sin(Dist)*cos(Bearing)); 169 | float DestLon = CurLon + atan2(sin(Bearing)*sin(Dist)*cos(CurLat),cos(Dist)-sin(CurLat)*sin(DestLat)); 170 | 171 | 172 | DestLon = fmod((DestLon+3*PI),(2*PI)) - PI; // normalise to -180..+180º 173 | 174 | if(DEBUG) Serial.print(":: bearing:"); 175 | if(DEBUG) Serial.print(Bearing); 176 | if(DEBUG) Serial.print(" :: CurLat: "); 177 | if(DEBUG) Serial.print(CurLat,4); 178 | if(DEBUG) Serial.print(" :: DestLat: "); 179 | if(DEBUG) Serial.print(DestLat,4); 180 | if(DEBUG) Serial.print(" :: CurLon: "); 181 | if(DEBUG) Serial.print(CurLon,4); 182 | if(DEBUG) Serial.print(" :: DestLon: "); 183 | if(DEBUG) Serial.print(DestLon,4); 184 | if(DEBUG) Serial.print(" :: "); 185 | 186 | CurLat = DestLat; 187 | CurLon = DestLon; 188 | */ 189 | } 190 | -------------------------------------------------------------------------------- /update_wind_walk.ino: -------------------------------------------------------------------------------- 1 | // A function to update the horizontal posisiton of the balloon 2 | // Designed to overcome some of the difficulties of using more 3 | // demanding mathematcial functions on a 8bit uC 4 | 5 | /* 6 | 7 | After doing a little bit of research, I have decided to make the 8 | wild assumption that a delta of 1m of from a position translates 9 | into 0.000009 units of lon or lat 10 | 11 | 1m @ 0° = 000°00'00.0326?N, 000°00'00.0000?E = 0.000009,0 12 | 1m @ 45° = 000°00'00.0230?N, 000°00'00.0229?E = 0.000006,0.000006 13 | 1m @ 90° = 000°00'00.0000?N, 000°00'00.0323?E = 0.0,0.000009 14 | 10m@ 90° = 000°00'00.0000?N, 000°00'00.3234?E = 0.0,0.000090 15 | */ 16 | 17 | void updateWindWalk(float oldLat, float oldLon, float brng, float s) 18 | { 19 | 20 | float dlat1 = (cos(radians(brng))*s)*0.000009; 21 | float dlon1 = (sin(radians(brng))*s)*0.000009; 22 | 23 | CurLat += dlat1; 24 | CurLon += dlon1; 25 | 26 | if(DEBUG) Serial.print("[dps:"); 27 | if(DEBUG) Serial.print(distancePerStep,2); 28 | if(DEBUG) Serial.print(" :: brng:"); 29 | if(DEBUG) Serial.print(brng, 2); 30 | if(DEBUG) Serial.print(" :: CurLat:"); 31 | if(DEBUG) Serial.print(CurLat,8); 32 | if(DEBUG) Serial.print(" :: CurLon:"); 33 | if(DEBUG) Serial.print(CurLon,8); 34 | if(DEBUG) Serial.print("] "); 35 | } 36 | --------------------------------------------------------------------------------