Thursday, June 16, 2016

Using Two Party Balloons for the Payload

Using Two Party Balloons for the payload


So, you can get a bit more free lift out of the system, and perhaps a bit more altitude, by using two party balloons in tandem to fly the payload.  In my most recent flight, my payload was 18g, and I wanted 5g of free lift.  Using the float calculator (assuming helium), I came up with these numbers:

Scenario Float Altitude
(1) 36" mylar, 18g payload, 5g free liftProjected: 7630
(1) 36" mylar, 9g payload, 2.5g free lift (each doing 1/2 the work)Projected: 8860
(1) 36" mylar, 9g payload, 2.5g free lift (each doing 1/2 the work)Flight 1 Actual: 9156

The third row is an edit following the first flight, confirming the expectation that we would have a much nicer altitude with two balloons.

How to attach them

Consulting with Dave/VE3KCL, who is also flying dual balloons, he said he was using a Tyvek tape. I don't think I found exactly what he's using.  I found this:



I sealed the upper balloon with the Tyvek tape.  Note the attempt to keep the stem and valve section very flat.  I then found the center line of the lower balloon, and put a strip of Tyvek tape along the seam edge facing me.  I tried to keep the tape on the seam, and not extending down onto the balloon proper.  I then pressed the upper balloon into the tape.  The picture below shows the strip of tape attached to the lower balloon, and the upper balloon attached to the sticky side, facing me.  There is a little bit of the tape from the lower balloon extending past either side of the upper balloon, providing a sticky surface on each end.


I then placed a second strip of tape on the lower balloon closest to me, again trying not to hit the inflatable section of the lower balloon, and capturing the upper balloon. The front and back strips are stuck to each other on the end, and via the upper balloon in the middle.


Secondary connection


Dave also makes a secondary connection between the two balloons.  The runs a loose cord down from the upper to the attachment point at the bottom of the lower balloon.  I didn't do that for fear that the string rubbing on the lower balloon might cause problems.

Caveat


Note, in retrospect I would not have used two different color balloons.  They probably have different thermal characteristics.

Sunday, June 12, 2016

Antenna length thoughts

Antenna length for the WSPR tracker


So, there are a few factors that impact the ideal length of the antenna for the WSPR tracker.  The classic wavelength formula is:
wevelength = 984 feet / Frequency in MHZ
This is adjusted by the velocity factor of the wire, which is also impacted by whether or not the wire is insulated.  I found several resources discussing wire length.  Conventional wisdom seems to be to adjust by between 4-5%.  W7NAT likes 5%.  This would be for ground-mounted antennas.

However, we're using very thin wire,  and we're in free space.  I'm going to try a full 1/4 wave with no adjustment.

Antenna scenario Element length (inches)
20m 1/4 wave dipole for 14.0971.  No Adjustment.209.4
20m 1/4 wave dipole for 14.0971.  4% adjustment for insulation.201.0
20m 1/4 wave dipole for 14.0971.  5% adjustment for insulation.198.9

Thursday, June 9, 2016

Buglets

Buglets

I spent a fair amount of time writing code when it wasn't convenient to test it right away.  As a consequence, I had a pile of stuff to test the other night, and ran into a gazillion little buglets. Everything, so far, has been pretty easy to fix.

Watchdog Timer & Supercap charging

I implemented a watchdog timer, and sprinkled calls to refresh it throughout my code. Later, I implemented a routine at initialization to assure that the Supercap is charged up beyond a certain voltage before allowing the code to proceed.  The first thing we do is turn on the GPS, and I envisioned a situation where the GPS would brown out, repeatedly, as we're trying to charge the supercap and get going.  Well, I added a routine that was basically like this:
volts = getvolts();
while (volts < 3500) {      // Keep charging if less than 3500 microvolts
   deepsleep(5);        // Hibernate for 5 seconds
   volts = getvolts();
}
Well, the code worked a little too well.  I forgot to add a line to refresh the watchdog timer in the middle of that.  My timer pops after about 14 seconds, if it isn't refreshed. The entire board was rebooting during the charge-up phase.  It was a simple matter to call my "WATCHDOG_REFRESH()" macro to the middle of the loop.


GPS Reset

A few people running Pico trackers have had problems with the GPS freezing up, and basically not working for an entire day, until the sun went down, and the board rebooted overnight.  I added counter to keep track, and reset the GPS if it takes "too long" to get a lock.   

While I was testing something else, I left the tracker running on my desk.  I turned later, to find that I had exceeded my "too long" threshold, and the tracker had reset. Unfortunately, I neglected to reset my counter in the GPS Reset routine, so it kept getting called over and over, once the time was exceeded.  It was a simple matter to zero the counter after the reset.


Geofencing default

In testing my geofencing code, I discovered that the default if a coordinate was not in one of the polygons was "not allowed".  I found a #define in the code I had borrowed, and changed the default in my test environment.  Unfortunately, I forgot to port the one-line "#define" line into my Eclipse environment.  So, the geofencing in the code "worked", because it got back a "not allowed", and didn't transmit.


Revenge of geofencing

Well, I thought I fixed it, but I didn't.  I ran into another issue.  The geofence routine returns "true" if you are allowed to transmit in the coordinates passed to it.  I was setting a variable called "fenced" to the value of the geofence subroutine.  I basically had the value backwards.  I had to reverse the returned value from the call to make it behave as expected.  


Oversleeping

My deepsleep() routine always seemed to sleep about 1 second too long.  I didn't worry about it, since I was waking up the GPS 30-45 seconds before transmit time, and the one second didn't matter.Well, when I added the watchdog timer, I had to modify deepsleep() to sleep in 5 second chunks, and wake up to reset the watchdog.

Well, long story short, it turns out that the HAL routine to sleep is 0 based.  So, to sleep for 1 tick, you call it with a "0".  So, when I called it with "5", it actually slept for 6 seconds.  

In the original code, this wasn't a big deal.  No matter what length of time I intended to sleep, I just slept one extra second.  However, when I broke my sleep into 5 second intervals (which were really 6), the problem multiplied. 

Another easy fix.  Just subtract 1 from the sleep time, and voila, it worked much better

WSPR transmit DT


During testing, I noticed the DT column showed that my WSPR packets were about 2.4 seconds off my laptop.  I was pretty confident that I was syncing to GPS right in the tracker.  Upon conversations with Alan/W7QO, I was reminded that the WSPR protocol calls for the transmission to start at 1 second after the even numbered minute, not at 0 seconds.  That accounted for one second right there.

I'm still chasing the other 1.4 seconds of error.  It could well be my work laptop is not well synchronized to GMT.

Wednesday, May 25, 2016

GPS resolution planning & Floating Point Avoidance

Avoiding Floats

So, the ARM Cortex M3 processor does not include an FPU.  Any floating point operations would require emulation libraries to be linked in and used.  This increases the size of the code and reduces performance.  Thus, I'm trying to avoid floating point math in the tracker.

GPS Strings

The GPS strings include latitude and longitude which are presumed to be floating point.
$GPGGA,204403.00,4226.59508,N,07628.88487,W,1,06,2.83,283.3,M,-34.5,M,,*66
These numbers need to be converted to decimal degrees for use in grid square math, and also for the math used by the geofencing code.  They have a fair amount of precision, but the question is "How much do we need?"

Grid Square Precision

A 6-character grid square has a precision of about 3x4 miles.   Each degree of latitude and longitude is about 69 miles.  So, if we used 1/10th of a degree resolution, we would have a resolution of about 7 miles.  That's not sufficient.  However, if we use 1/100th of a degree, we would then have a resolution of about 0.69 miles.  That's more than adequate for a 6 character grid square.

Geofencing precision

The generally available geofencing polygons are composed of relatively few points enclosing the countries on the map.  They're not drawn with a great deal of precision near the borders.  A resolution of 0.69 miles should be sufficient to fence off areas in which transmission is prohibited.   So, 1/100th of a degree would work fine here, as well.

Assuring 1/100 degree is safe for int32_t

The latitude and longitude numbers, when in decimal degree will range from -180.00 to 180.00.  By shifting the decimal point two places, we can represent these as integer values from -18000 to 18000.

Grid square calculations begin with the latitude and longitude and do divisions and modulo operations to build the grid square representation. Those won't present any difficulties for 32 bit integer math.

The Geofencing logic is more complicated.  The heart of the "PointInPoly" routine, used by the geofencing logic, looks like this:

if ( ((verty[i]>testy) != (verty[j]>testy)) &&
    (testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) )
         c = !c;
I want to remove the division.  There's a risk that the denominator may be negative.  If it is, it will switch the case of the test from "Less Than" to "Greater Than".  Modifying the code as I implement this algebra yields this:
if  ((verty[i]>testy) != (verty[j]>testy)) {
   denominator =  verty[j]-verty[i];
   if (denominator > 0) {
      if ( (testx - vertx[i]) * denominator < (testy-verty[i]) ) {
         c = !c;
      }
   } else if ( (testx - vertx[i]) * denominator > (testy-verty[i]) ) {
      c = !c;
   }
}
Denominator worst case:  18000 - -18000 == 36000

So, the worst case math is this:
(testx - vertx[i]) * denominator
IE:  
(( 18000 --18000) * 36000) == 36000 * 36000 = 1,296,000,000
The max value of a signed 32 bit integer is:
2^^31 == 2,147,483,647

So, the worst case scanario in the PointInPoly routine, assuming we use integers from -18,000 to 18,000, fits comfortably inside a 32 bit signed number.

Conclusion

I'll be using Latitude and Longitude to the 1/100th of a degree. I'll represent the values as signed 32 bit integers by multiplying the values by 100 and rounding.  By doing so, I can completely avoid using floating point math and/or 64 bit numbers.  This should keep the tracker code lean and mean.

Sunday, May 15, 2016

Borrowed routines for SI5351 and WSPR encoding

Borrowed routines

The interwebz are a wonderful place.  People publish their software for others to use.  I took advantage of this to get a driver for the Si5351 chip, and also to find code to encode my WSPR strings.

SI5351 library

I ported code written by Jason Milldrum & Dana H. Myers.  This code was written for Arduino. Fortunately, the Arduino specifics were isolated to a few functions.  

si5351_read
si5351_write
si5351_write_bulk

One little issue that I ran into was that the I2C bus address in arduino is automatically shifted by the Arduino libraries, but it is not by the HAL libraries for STM.  So, where these routines used "addr", I basically had to use "addr <<1".

I had some initial issues with timing in I2C.  Things would work fine if I had lots of debugging turned on, but would fail if I removed debugging.  Then, I found a sparsely documented HAL_I2C_IsDeviceReady() routine.  By adding this call before all of my I2C reads and writes, the problems cleared up.  At some point, I'll investigate this more thoroughly to make sure that I understand exactly what the issue was.

Below is a before/after sample of one of the changes to get a feel for the changes required:

Vanilla:
uint8_t Si5351::si5351_read(uint8_t addr)
{
uint8_t reg_val;
Wire.beginTransmission(SI5351_BUS_BASE_ADDR);
Wire.write(addr);
Wire.endTransmission();
Wire.requestFrom(SI5351_BUS_BASE_ADDR, 1, false);
while(Wire.available())
{
reg_val = Wire.read();
}
return reg_val;
}
My Version:
uint8_t Si5351::si5351_read(uint8_t addr)
{
uint8_t buffer[] = {addr};
uint8_t value;

    if (HAL_I2C_IsDeviceReady(&Globals.I2cHandle, (uint16_t)SI5351_BUS_BASE_ADDR<<1, 2, 
I2CTIMEOUT) != HAL_OK) {
    Error_Handler();
    }

while(HAL_I2C_Master_Transmit(&Globals.I2cHandle, (uint16_t)SI5351_BUS_BASE_ADDR<<1, (uint8_t*)&buffer, sizeof(buffer), I2CTIMEOUT)!= HAL_OK)
{
/* Error_Handler() function is called when Timeout error occurs.
      When Acknowledge failure occurs (Slave don't acknowledge it's address)
      Master restarts communication */
   if (HAL_I2C_GetError(&Globals.I2cHandle) != HAL_I2C_ERROR_AF)
   {
    Error_Handler();
   }
}

    if (HAL_I2C_IsDeviceReady(&Globals.I2cHandle, (uint16_t)SI5351_BUS_BASE_ADDR<<1, 2, I2CTIMEOUT) != HAL_OK) {
    Error_Handler();
    }

while(HAL_I2C_Master_Receive(&Globals.I2cHandle, (uint16_t)SI5351_BUS_BASE_ADDR<<1, (uint8_t *)&value, 1, I2CTIMEOUT) != HAL_OK)
{
/* Error_Handler() function is called when Timeout error occurs.
          When Acknowledge failure occurs (Slave don't acknowledge it's address)
          Master restarts communication */
if (HAL_I2C_GetError(&Globals.I2cHandle) != HAL_I2C_ERROR_AF)
{
Error_Handler();
}
  }

  return value;
}

WSPR Encoding routine

I poked around and found a few different options for WSPR encoding.  I found some truly wretched code in a few Arduino samples I found.  I'm always nervous about code with malloc() and free() calls.  One particularly good looking implementation, however, includes support for JT65, JT9, JT4 and WSPR can be found at this link right here.

Ultimately, though, being the contrarian that I am, I found an even leaner and meaner encoding routine written by Mark VandeWettering called "genwspr".

The code is VERY small and lean, with no malloc() or free() calls, which made me happy.  I can't say that I understand it one tiny bit, but it creates a tone array of values to use for making the WSPR signal.  It was trivial to just pick up his code and port it into my project. 

I was able to take the output of his routine and compare it to output from the "wsprcode.exe" program documented in the WSPR_2.0 User manual, and found it matched exactly.  Good 'nuff for me!




Wisp1 Telemetry Revisited

Wisp1 Telemetry Revisited

So, in my previous blog post about telemetry, I outlined a WSPR telemetry scheme to maximize the amount of data packed into the few bits available.  Alan Adamson (W7QO) and I were both thinking of using it.  However, after some initial implementation, Alan realized it required a hefty bit of coding.  He counter-proposed a more "positional" concept.  We went back and forth for a day or two, and came up with the scheme below.  It's agreeable to both of us, so we each intend to use it on our trackers.  Since he did the heavy lifting with most of the design, I'm writing it up for the interwebz to enjoy.

First Packet

     KD2EAT FN12 37

The fields include Callsign, Grid locator, and a power level.  There are 19 discrete values permitted in in the power level field.

The first modification is to re-purpose the power level field in the WSPR packet as an "Altitude" indicator. Each of the 19 discrete values will represent 1000 meters of altitude.   The altitude mapping ends up looking like this:

DBMEncoded Altitude Meters
00
31,000
72,000
103,000
134,000
175,000
206,000
237,000
278,000
309,000
3310,000
3711,000
4012,000
4313,000
4714,000
5015,000
5316,000
5717,000
6018,000

So, the packet above indicates that the balloon is flying at, at least, 11,000 meters.  This provides altitude data in one packet with no additional modifications to the protocol, if desired.  It provides for altitudes from 0-18,000 meters (59,055 feet).

Second Packet

This is where the rubber meets the road.  We are encoding a lot of data in the callsign field, as well as the power field.  However, the grid square field is NOT used to carry telemetry data.  This means that, though the callsign will be bogus, the telemetry data will appear in the same grid square as the primary packet.  It is assumed that the same data will be used to generate BOTH the primary and secondary packet, since the altitude encoding in the primary packet is refined by the telemetry packet.

We'll illustrate the encoding scheme by decoding an example.

     QK1SKN FN12 33

The callsign consists of 6 positions with potential values as follows:

Position Possible Values Use in Scheme (number of values) Number of Used Values /
Number of Possible Values
Callsign 1 Q,0 Telemetry Channel (2) 2 / 2
Callsign 2 0-9,A-Z Battery volts (12), Altitude_fine(3) 36 / 36
Callsign 3 0-9Telemetry Channel (10) 10 / 10
Callsign 4 A-Z Grid Square 5th char (A-X) (24) 24 / 26
Callsign 5 A-Z Grid Square 6th char (A-X) (24) 24 / 26
Callsign 6 A-Z, space Temp (9), Altitude_super_fine(3) 27 / 27
Gridsquare 1 A-RSame as Packet 1 n/a
Gridsquare 2 A-R Same as Packet 1 n/a
Gridsquare 30-9 Same as Packet 1 n/a
Gridsquare 4 0-9 Same as Packet 1 n/a
DBM 0-18 Solar_volts(6), Sats(3) 18 / 19

Callsign Positions 1 & 3: Telemetry "Flight" or "Channel" number

Positions 1 & 3 allow for 2*10 = 20 possible values.  This allows for up to 20 pico flights to be in operation simultaneously without telemetry confusion, provided everyone cooperates and uses a unique pair of characters in those two positions.  In the example above, the two telemetry channel bytes are "Q" and "1". We can interpret a "Q" in position 1 as the 10's value, so this is "Flight Number 11" or "Channel 11".

Altitude encoding

Altitude is encoded across both the first and second WSPR packet.  The first WSPR packet gives us the altitude with 1 km of granularity, from 0-18,000 meters, as described above in the first packet.

The second packet, gives us two more levels of granularity:

Altitude_fine:  0, 333, 666.
Altitude_super_fine: 0, 111, 222

Balloon altitude is thus calculated by adding the three values together.  As we expand the telemetry below, we'll see in this example that:

Altitude_fine = 2, so we add 666 to the altitude.
Altitude_super_fine = 1, so we add another 111 to the altitude.

Actual Altitude = 11,000 (first packet) + 666 (second packet) + 111 (second packet) = 11,777 meters.

Callsign Position 2: Battery volts, and "fine" altitude

We encode volts and altitude fine as:

Volts Encoded Volts
3.0 or lower0
3.21
3.42
3.63
3.84
4.05
4.26
4.47
4.68
4.89
5.010
5.2 or higher11

Altitude Fine Encoded Altitude Fine
00
3331
6662

Having the two encoded values, we calculate the value for this "character" as follows:

value = (EncodedVolts * 3) + EncodedAltitudeFine.

Conversely, to get the Encoded values, you do the following:

EncodedVolts = value / 3
EncodedAltitudeFine = value % 3

The value of the second column is a letter 'A' - 'Z', or number '0' - '9'.

Encoded Value Letter
00
11
......
99
A10
B11
......
K20
......
Z36


In our example, we have the letter "K" in position 2, which represents "20".

EncodedVolts = 20 / 3 = 6.  So, our Volts = 4.2v.

EncodedAltitudeFine = 20 % 3 = 2.  So, our AltitudeFine is 666.  Added to our 11,000 meters from the first packet, our altitude is at least 11,666 feet.

Callsign Position 4&5: Grid Square 5&6.

This is straightforward.  We simply take these two characters, and append them to the grid square. 

     QK1SKN FN12 33

"SK" is added to our grid square "FN12".  By convention, the last two characters of the grid square are in lower case, so our 6-character grid square is "FN12sk".  

Callsign Position 6: Temperature and Altitude "super fine"

This position is permitted the characters 'A' - 'Z', or a space, making 27 total values.  We reserve 9 values for Temperature, and 3 values for "super fine" altitude, as follows.

Temperature (c) Encoded Temperature
-35 or less 0
-301
-252
-203
-154
-105
-56
07
5 or more8

Altitude Super Fine Encoded Altitude Super Fine
00
1111
2222

Having the two encoded values, we calculate the value for this "character" as follows:

value = (EncodedTemperature * 3) + EncodedAltitudeSuperFine.

Conversely, to get the Encoded values, you do the following:

EncodedTemperature = value / 3
EncodedAltitudeSuperFine = value % 3

The value of the second column is a letter 'A' - 'Z', or a space.
Encoded Value Letter
A0
B1
......
N13
......
Z25
(space)26


In our example, we have the letter "N" in position 6, which represents "13".

EncodedTemperature = 13 / 3 = 4.  So, our Temperature = -15c.

EncodedAltitudeSuperFine = 13 % 3 = 1.  So, our AltitudeSuperFine is 111.  Added to our 11,666 meters from above, our altitude is at least 11,777 feet.


DBM value: Solar Volts, Number of Satellites

The DBM field has 19 potential values, which we consider 0..18 (as in the Altitude table in the first packet above).  We use 18 of these 19 values.

We encode 6 values for Solar Voltage, and 3 values for Number of Satellites as follows:
Solar Volts Encoded Solar Volts
0.2 or lower0
0.41
0.62
0.83
1.04
1.2 or greater5

Number of Satellites Encoded Number of Satellites
0 or no fix0
4 - 71
8 or more2

Having the two encoded values, we calculate the value for this field as follows:

value = (EncodedSolarVolts * 3) + EncodedSatellites.

Conversely, to get the Encoded values, you do the following:

EncodedSolarVolts= value / 3
EncodedSatellites = value % 3

We follow the same DBM to Encoded Value scheme as the Altitude does in packet 1.

In our example, we have a DBM of 33, which represents a "value" of 10.

EncodedSolarVolts = 10 / 3 = 3.  Solar Volts = 0.8v.

Encoded Satellites = 10 % 3 = 1.  Satellites = 1.  4-7 satellites.


Sunday, April 17, 2016

Blinky lights

Wisp1 LED uses and meanings

The Wisp1 has three LEDs, Green, Red and Blue.   Their blinking rate will convey status.

  • Green is the overall "system" LED.  
  • Red is for GPS status.  
  • Blue is for the Si5351 transmitter status.  

My intention is to have the LEDs active for the first 30 minutes after the tracker is powered on.  After that, they will be disabled to conserve power.

LED Color Blink Rate Meaning
GreenOff Tracker powered off, or STOP mode
Green3 hz RTC problem - Switched to LSI
Green1 hz Tracker up and operating normally
GreenOn System Error - Tracker abnormally stopped
RedOff GPS Off
Red3 hz GPS Seeking Time
Red1 hz GPS Seeking Lock
RedOn GPS Locked
BlueOff Si5351 powered off.  Not Transmitting
Blue1.4648 hz Transmitting
BlueOn Si5351 Error