eggStation - an eggTimer ground station software build thread.

The Rocketry Forum

Help Support The Rocketry Forum:

This site may earn a commission from merchant affiliate links, including eBay, Amazon, and others.

Charles_McG

Ciderwright
Joined
Sep 12, 2013
Messages
3,787
Reaction score
2,562
Location
SE Wisconsin
First up, some caveats:
a) This isn't an eggTimer Rocketry project. I'm doing on my own, for fun. I did check with Cris about using the egg prefix, though.
b) I'm as much a hobbyist coder as I am a hobbyist rocketeer. YMMV.
c) I'm a biologist and a chemist. And an accidental data scientist. This project has another purpose - to refresh my coding skills for the day job.

So, my goal with this project is to have a live display of data streaming from my 1/5th scale staged sounding rockets. eggTimer released a telemetry module last fall that uses the 900MHz Hope module (like the eggFinders) to stream data back from an eggTimer flight computer. I'm partial to the Proton for the upper stage of my rockets, and the Proton has a pretty rich data set it can send back through the telemetry module. The details are in the eggTimer docs.

BUT - I only have one LCD Receiver. If you're flying with radioed telemetry AND GPS tracking, each needs it's own receiver. Cris has taken pains to make the telemetry packet distinct from NMEA sentences - but they aren't meant to intermingle on one channel. I have 2 USB dongle receivers, and my LCD receiver has the HC06 bluetooth module add-on. So with a laptop with a couple of USB ports, I should be able to make a 'receiver' of my own.

I'm building this project in National Instruments Labview. Partly because I have it. Partly because I know it will make reading the serial data pretty easy. Partly because we have some short term needs in the labs at work that I want to see if I can contribute to. I've coded in Labview before - but it's been a decade or so. Labview has grown in the meantime. For those of you who aren't familiar with Labview, it's coded graphically. The program will look like a wiring diagram. Functions (VIs - virtual instruments) will look like discrete components. Information flows from sources (controls) to sinks (indicators). A function/VI won't execute until all its inputs are available to it. Visually, this 'data flow' programming generally flows from left to right, and top to bottom. But that's a generalization. Structurally, it's like C without pointers. At least it used to be. In fact, the internal written version of the diagrams is called 'G'. Like C, Labview is [mostly] a pretty low level language. It's not event driven. It's not object oriented. Its version of variable scope is pretty limited, and it doesn't have inheritance. New features in the past 10 years push the edges of what I just said - like references (pointers), type definitions, event cases, queues and notifiers. I want to test those out, so this project is [slightly] more complicated than it needs to be, so I can explore the uses of those new tools. Oh, one big difference from C (C wasn't my first programming language - but it's one I worked with a lot. Adapting KA9Q NOS to run on an Atari ST. Nearly back when computers were made of cast iron.) - loops that are not wired together so an order is explicit run in parallel. It's my understanding that they may not be completely multithreaded unless you dig into the options to take more direct control - but for most purposes, they behave like separate threads. Second 'Oh', Labview is used to program LEGO Mindstorm.

Without further ado, let's get started. I opened up Labview 2015 (old, I know, but I had an old license not being used elsewhere.) and started a project based on the 'Continuous Measurement and Logging' template. A three data streams I'm aiming for seem continuous to me, and a quick look showed the VI was using everything I wanted to learn about. Looking at Main.vi...

Every VI has a Front Panel:
1678578833329.png

So, stop and start buttons, some status and graphing indicators. Alright.
And a Block Diagram
1678579195508.png

Let's see... some initialization on the left, starting with User Event creation, then a message queue, some clusters (structures in C), a data queue and notifier in the lower left. Go read up about those. Hmmm 'syncronization' tools. A queue is a FIFO stack. A notifier always gives the most recent element pushed. They are 'global' in that they can be called by name from where ever, without explicit wires running all over. Down the middle are 5 parallel While Loops, two having been shrunk down to VIs - maybe to save space, maybe to make development easier. Then some tear down of objects and closing out before finally exiting. (PS - IRL this build has played out over about a week and there was good bit of research, reading, iteration, trial and error, and re-work involved. I'm going to spare you most of that. Also, from this point out, I'm going to use screen shots from my actual build, rather than the template. I just wanted you all to see the equivalent of the body tubes and balsa sheets.

Queue driven state machines (QDSM), I am already familiar with, so let's go check out how Labview is doing user events.
1678580057778.png
So this is a While loop with a new, special Case structure in it to handle user events. Each Case (like select case) holds the Front Panel Control (it has to be -somewhere- in the block diagram) - but the controls aren't wired to anything. Each case sends a message to the UI QDSM. Here are all the cases. You can see I added more for all the stop/start buttons I added to the front panel.
1678580390037.png

Right clicking on the case frame brings up a menu that has things like 'add', 'duplicate', 'edit'. From the edit, you can get to the object triggering events:
1678580536127.png

Not bad, not bad. The old way to do this was to have the 'idle' state in your UI QDSM check for changes in controls since the prior iteration, and throw messages on the queue based on those changes. These new user events and event case structure break the data flow paradigm - but are supposed to be more responsive to user interaction.

I think that's enough for the first post. Time to go get some brandy.
 
Last edited:
Apple brandy in hand...

I want to look at those clusters being 'declared' before the loops next.
1678582610859.png

And I had to make some of my own to define the types of data I was going to be passing around.
1678582653872.png

It turns out these are type definitions, not regular clusters or VI. When you open them, they look like a VI with controls, but no block diagram.
Config holds two clusters, one with the VISA COM# references and parameters. The other is for logging info. (Spoiler Alert, I haven't gotten to logging yet.) Type defs can be set to automagically update all their instances. That should make them useful.
1678582940813.png
UI Refs holds a big cluster of pointers to the UI controls. This allows some nifty tricks later on.
1678583179921.png

I parse the info in the telemetry string out into a cluster, so here's that definition.
1678583479923.png
Channel State is an array of strings, and Flight State is an enumerated unsigned integer:
1678583683605.png
Everything except the timestamp is from eggCris' documentation.

And the GPS data cluster type def.
1678583803701.png

Now let's look at the eggStation Front Panel:
1678584134014.png

Telemetry is on the left. Dials, boxes, sliders. Flight state is both text, and a series of buttons. The bottons have different colors and text for the off/on states and this screen shot is after I tried simulating a flight by sucking on the avbay.

GPS is on the right, with boxes across the bottom for the booster and sustainer data values, and a largish web-browser object displaying a static google map, with some related controls above that, and force reset button below.

Along the bottom edge are separate start and stop buttons for the 3 devices I'm reading, a button to open the settings dialog VI, one to clear the telemetry display and recenter the map on the current sustainer GPS, and the exit app button.

Searching the labview community forums turned up a NMEA parsing library of VIs, and a tutorial on how to insert maps with the google static map API. Both were very handy. I set up a Maps API key through my google account in order to use the API.
 
Do you know if the runtime-only download requires a paid license?

The RunTime Engine is free. Like JRE. BUT... 'Application Builder', which takes a project like the one I've built and makes an .EXE, requires an extra license on top of the normal Labview Development system. I've been running in 'interpreted' mode to date.

If you, or others, are interested, once this is functioning reasonably well, I can move it to a machine with application builder and make the EXE that can be shared. I'll move the Maps API key to the Config typedef - which stores its info in an XML between sessions. That way I can strip it out before sharing.
 
The RunTime Engine is free. Like JRE. BUT... 'Application Builder', which takes a project like the one I've built and makes an .EXE, requires an extra license on top of the normal Labview Development system. I've been running in 'interpreted' mode to date.

If you, or others, are interested, once this is functioning reasonably well, I can move it to a machine with application builder and make the EXE that can be shared. I'll move the Maps API key to the Config typedef - which stores its info in an XML between sessions. That way I can strip it out before sharing.

This looks amazing, I'd be super interested in checking this out. If you are open to sharing the Labview source files, I'd love to see those as well. I've got a license for Labview development kit through my work.

cheers - mark
 
This looks amazing, I'd be super interested in checking this out. If you are open to sharing the Labview source files, I'd love to see those as well. I've got a license for Labview development kit through my work.

cheers - mark
I will at some point. I've still got logging to do, and the UI QDSM is a bit of a mess at the moment. It works - but probably leaves resources hanging when it exits.
Also, Cris will get it first. :)
 
Speaking of the UI QDSM, let's look at that before digging into the meat of acquisition.

The UI QDSM is a While loop with a VI that pops a message off the queue stack and feeds it to a Case structure.
1678645392926.png

The 'state' is actually that blue enumerated integer. You can see it getting set to 'idle' during the initialization, here. The Initialization case pushes setting up the button pointers and spreading the settings around, along with a status message onto the queue.

The broadcast setting case is interesting. It splits out the subclusters of the Config cluster, and sends them as data with an 'Update Settings' message to the acquisition and logging QDSMs.
1678645818727.png

The 'Init UI Refs taught me a lot.

This is messy, because I added a lot of buttons to the front panel. Some for controlling acquisition, some for indicating flight computer status. The basic idea is that it grabs a reference/pointer to the controls I've set up and stuffs that into a cluster. That's the 'Init' part. But then it does something sneaky. It also stacks those references into arrays and sends them to a VI that iterates through the array and enables/disables the button as commanded (a little green T/F boolean constant. I adapted that VI to set the -value- of the flight state buttons, rather than the enabled state.

1678646144168.png

And the Set Multiple Values VI:
1678646554509.png

Just so you know, if you pass an array into a ForNext loop in Labview, it will automatically do a For Each on it.

That's a sneaky trick, when you're used to updating indicators only by passed values (wires). it also means I have a way to move data from the display loop up to the UI loop when I need to, without wires that would force otherwise separate threads to synchronize.

The Start/Stop data acquisition loops are all similar. They enable/disable buttons, and send 'start' messages to their respective acquisition QDSM. They also change the state of the QDSM. That's mostly used in making sure the program closes out in a tidy fashion - making sure resources are stopped and released. I've complicated the possible number of states with 3 independent data loops. I'm still thinking about how I want to tidy that up.

1678647359495.png

The 'Reset Displays' case uses some of these learned tricks to 'home' the display. It resets the FC state buttons, sends a fresh/blank telemetry data frame to the telemetry display loop via the telemetry notifier, and copies the sustainer GPS lat/lon up to the map center lat/lon before pushing a 'Refresh Map message onto the UI queue.

1678647561477.png


The Refresh Map case is the last one I'm going to highlight.
1678647724326.png
It gets the values from the map controls, passes them to a VI that formats them into a URL, then adds the API key, and the &markers data (if any) that were passed as data in the message. That's the bit along the bottom - it takes the message data variant and casts it into a string. Then it calls an ActiveX web control. And -that's- how I get markers onto the map.

Here's the whole UI QDSM case list.
1678648363997.png

Now I have to go clean up the cider filtration that's been running in the background.
 

Attachments

  • 1678646593912.png
    1678646593912.png
    34.3 KB · Views: 1
Last edited:
I think Labview code is at least pretty to look at. I also think it lends itself to a 'build thread' much more than text based line-flow coding would be. Do you all agree? Anyone inspired to go install the free 'community edition'?

Can you follow it conceptually? Or is it just gibberish?
 
Let's look at how I actually read data in and crunch it.

There are 3 VIs doing the data acquisition - they differ by how they process the data read. I re-worked them to make the serial comms part as re-used as possible. One is for telemetry, two are for GPS.

1678661761173.png

I tried to simply copy and paste the GPS Acq VI and feed it the enumerated int for COM channel - but it didn't work. I ended up copying the GPS on disk, changing the name and putting it back in. My research this afternoon suggests that I should be able to set the 'execution' options of the VI to 're-entrant', and run clones like I originally thought. It might serialize the execution if I do that - and there are ways around that, too. Making an extra copy on disk (and memory) feels a bit like brute force and ignorance - but it does, in fact, work.

The template I started with feeds the Acq loop with both a Queue and a Notifier carrying the same data type. For now, I've kept that. The difference is that the queue is a stack - every data element pushed onto it stays until it's popped off. A notifier only has one element - each push displaces any prior. The comments say that watching the notifier means you can miss data that's updated before the receiving loop can pop it. That's 'ok' for the data visualization, but not reliable enough for logging. The queue doesn't lose anything.

You'll note that I'm careful to use 'push' and 'pop' with stacks. I taught myself Forth in high school. And HP 28s and 48s were -the- calculators of choice at the college I went to.

So inside the VI:
Its Front Panel is just controls and indicators for wiring terminals to. That's true all through this, so that's the last one you're getting.
1678662600281.png

The Block Diagram is where it's at.
I'll start with the 'Update Settings' Case that the UI QDSM messaged during startup.
1678662736142.png

So the VI is a case structure tied to the error handler with an internal while loop, popping messages off the queue and sending them to another case structure. Update settings takes the config typedef to prime the hardware refs, and then replaces the old (or blank) settings with the values sent in the message data (the data variant cast into the HW ref cluster this time). The error output of the variant to data type selects with to pass forward. For those of you who are new to labview, the little squares with arrows on each side of the While loop are shift registers. The one on the right hand side passes data to the matching shift register on the left hand side OF THE NEXT ITERATION. Very handy. This QDSM also has a state variable used to control or skip actions when needed.

The template Acq Loop steps though several messages both leading to 'Acquire' and leaving it. I kept them, because it guided me to some better practices. I tend to be sloppy about initialization and releasing resources on close, otherwise.

I put the initialization of the COM port in 'Start'. There were two placeholder VIs, but I only need one for a VISA COM resource, so I used the one with the wrench in the icon.
1678663460462.png
Note: if the state is already 'acquiring' then the init is skipped. The message must have been received by mistake. THAT'S what state variables are for. And you can see that after the init, the VI sends itself the message 'Acquire'.

1678663575240.png

I use the Channel enum to select which VISA reference gets sent to the VISA config and clear buffer VIs. - and then is updated with the resulting ref/pointer. I'll point out that the little green T boolean constant is setting the use of Termination Character true. Also, the serial port values for the eggReceivers (LCD w/Bluetooth or USB Dongle) -happen- to be the VISA defaults. But I make them configurable anyway.

'Acquire' gets data, and sends itself another 'Acquire message.
1678663907932.png

1678663971974.png

HERE'S THE MEAT. The structure that looks like old sprocket driven film is a 'sequence'. It forces execution order. So I wait until the next 275 millisecond clock multiple. Memo to me: change that back to simple 275ms wait to avoid accident syncing with other loops. Then read the number of bytes waiting in the buffer. Given that the Telemetry module transmits every 2000 ms, it's pretty easy to get a packet. Using the termination character means that I only read -1- packet. ('Give me a packet, Vasily'). This makes parsing the packet easy - but causes other complications when we get to GPS. Cris isn't using a LF/CR, so far as I can tell, as Putty doesn't write each packet on a new line. If I turn off using the termination character, I can end up reading partial packets before and after a clean one. So, I'm making my life easier by using timing and termination characters to frame the data for me. I might miss a packet - but the nature of the radio link guarantees that anyway. If the read string is at least 20 character long, I send it off to get processed, and the resulting data element is pushed to both the queue and the notifier.

Inside the parser. I left the contextual help up for your interest.

1678664836169.png

The parser takes the read string and sends it into a While loop set to execute until offset past the scanned string is an error, or equal to the string length - I hit an edge case. The token scanner uses '>' as the delimiter - there's one at the end of every token. The delimiter isn't included in the token string. The first character of the token is peeled off and sent to the case structure. The rest of the token is sent into the structure for processing. The processed result is inserted into a blank data cluster. The offset past token is sent around a shift register to be used as the starting point of the scan the next iteration around. The 'default' case does nothing - it catches broken tokens where a delimiter was lost in transmission. Several 'triggers' (what Cris calls them) convert to double floats - like this one. If there's math to convert acceleration to Gs, or shift a decimal place, i do it. After I parse the data packet, I add a timestamp and hand it off. If the string contained more than one packet, it would just overwrite the first values with the next.

The Channel state is a little more interesting, so I'll show it.
1678665472365.png
It iterates through the 6 channels with a For Next loop. 'i' is the iteration counter in all types of loops. If the character is '-', it sets the text to 'Disabled', otherwise, if the character is a numeral, it sets 'Fired', and if not, it sets 'Enabled'. In Labview, if you pass a wire out through a For Next loop, it automatically concatenates them into an array.

I'm nearly out of photos for this post.

1678665832861.png
The Stop message releases the COM resource and sets the state back to idle. But it doesn't stop the main While loop. So it can be re-Started. The Exit message releases the Acquistion message queue and stops the VI for further loops.

At this point, I'll take the time to point out a subtle detail of loops in Labview that's used to great effect in this app. (And that I break, on purpose, in the GPS Data Display loop.) The keen eyed Labview coder will note that nothing is limiting the speed of execution of the loops I've shown so far. Back in the day, if you wanted to keep a loop from running as fast as possible (and consuming all the PC computrons as a result), you'd throw a timer in the loop to force it to pause so many milliseconds each iteration. QDSMs, or any loop based on the output of a queue 'pop' (so the data queue and notifiers) are tricksy, my precious. Remember that I said that a VI, or operator, or structure in Labview only executes when all of its inputs are available to it. The VIs that pop messages or queue/notifier elements will sit and wait until a message or element actually appears. Then it passes the message/element along to (usually) a case structure to handle. So by placing that pop first inside the loop, the loop actually waits for the message to appear before anything happens. Which means a loop waiting for a queue does that - it waits, taking up little CPU time. I think that's awesomely sneaky.
 
Last edited:
Apple brandy in hand...

I want to look at those clusters being 'declared' before the loops next.
View attachment 567980

And I had to make some of my own to define the types of data I was going to be passing around.
View attachment 567981

It turns out these are type definitions, not regular clusters or VI. When you open them, they look like a VI with controls, but no block diagram.
Config holds two clusters, one with the VISA COM# references and parameters. The other is for logging info. (Spoiler Alert, I haven't gotten to logging yet.) Type defs can be set to automagically update all their instances. That should make them useful.
View attachment 567982
UI Refs holds a big cluster of pointers to the UI controls. This allows some nifty tricks later on.
View attachment 567983

I parse the info in the telemetry string out into a cluster, so here's that definition.
View attachment 567984
Channel State is an array of strings, and Flight State is an enumerated unsigned integer:
View attachment 567985
Everything except the timestamp is from eggCris' documentation.

And the GPS data cluster type def.
View attachment 567986

Now let's look at the eggStation Front Panel:
View attachment 567987

Telemetry is on the left. Dials, boxes, sliders. Flight state is both text, and a series of buttons. The bottons have different colors and text for the off/on states and this screen shot is after I tried simulating a flight by sucking on the avbay.

GPS is on the right, with boxes across the bottom for the booster and sustainer data values, and a largish web-browser object displaying a static google map, with some related controls above that, and force reset button below.

Along the bottom edge are separate start and stop buttons for the 3 devices I'm reading, a button to open the settings dialog VI, one to clear the telemetry display and recenter the map on the current sustainer GPS, and the exit app button.

Searching the labview community forums turned up a NMEA parsing library of VIs, and a tutorial on how to insert maps with the google static map API. Both were very handy. I set up a Maps API key through my google account in order to use the API.
Thanks so much for sharing what you are working on. No doubt a lot of folks flying Eggtimer electronics are going to be closely following your efforts!
 
I’m definitely interested. I use a Mac, though. Is that a problem?

This is a very different coding paradigm from what I’m used to. Never done graphical coding before.

And to be clear, this setup requires the Proton or Quasar with the Telemetry add-on and in addition TWO USB receivers (one for GPS, one for telemetry)? Is that correct?
 
I’m definitely interested. I use a Mac, though. Is that a problem?

This is a very different coding paradigm from what I’m used to. Never done graphical coding before.

And to be clear, this setup requires the Proton or Quasar with the Telemetry add-on and in addition TWO USB receivers (one for GPS, one for telemetry)? Is that correct?
Labview, including the community edition, has versions for Windows, Mac, and Linux. I just checked. That should work for the VIs, which I'll release eventually. Compiled runtimes likely would need to be done on each platform.

Hmmm- the mapping part might need to be rebuilt for non-windows. I expect that the VISA drivers would normalize serial comms across platforms, but have never tried it.

This dashboard is set up to read a telemetry module and two eggFinders. So I'm using 3 serial ports - 2 USB (the dongle Rx) and one bluetooth via an LCD Rx. I could certainly use 3 USB, but chasing the sustainer eggFinder through the LCD receiver means I've got a field portable receiver AND the laptop readout. When you put the bluetooth module in the LCD receiver, it echoes the data stream from the Hope RF module. My app can't tell the difference.

The Telemetry module can be used on a number of eggTimer altimeters. They vary in what 'triggers' get sent, but Quarks >= vD3, IONs, Quantums, and Protons all send something. My app would display the tokens it receives and ignore the missing ones. Hmmm... memo to me, I'm using a 20 byte lower limit to filter out broken GPS packets. That should be lower to catch the minimum Quark telemetry packet.

http://eggtimerrocketry.com/wp-content/uploads/2021/05/Eggtimer-Telemetry-Data-Format.pdf
 
I just read the Quasar manual. (I don't have one - I use a separation charge on my staged sounding rockets - so I need more than 1 airstart channel) @cerving will need to confirm, but I don't think a Quasar sends telemetry in a way that can be read by this app. The Telemetry module uses a dedicated receiver, and doesn't intermingle telemetry and GPS in one data stream. The Quasar appears to be tacking some telemetry data onto the end of the GPS data, like the TRS does. The LCD Rx in Finder mode can handle that. you don't have to switch back and forth between Finder and Telemetry mode on two different frequencies - or just have to receivers.
 
I just read the Quasar manual. (I don't have one - I use a separation charge on my staged sounding rockets - so I need more than 1 airstart channel) @cerving will need to confirm, but I don't think a Quasar sends telemetry in a way that can be read by this app. The Telemetry module uses a dedicated receiver, and doesn't intermingle telemetry and GPS in one data stream. The Quasar appears to be tacking some telemetry data onto the end of the GPS data, like the TRS does. The LCD Rx in Finder mode can handle that. you don't have to switch back and forth between Finder and Telemetry mode on two different frequencies - or just have to receivers.
For compatibility, the Quasar sends out data in the older TRS format, however ETM-formatted data similar to the Quantum is planned for this year. The biggest hangup is the LCD receiver... there's not much firmware space available, so I'm going to have to get a bit creative about how the screens work with the Quasar.
 
I've spent some time working on my GPS Acq loop before posting it. It functioned, but when both GPS channels were acquiring, it tended to get buffer overflows. I wanted to figure that out. I think I've got it fixed, but won't be able to check until tonight, when I'll have the LCD receiver handy.

So here's the new and improved GPS Acquire VI.
1678801999205.png

It does a short wait (should be enough for 2 NMEA messages) and then reads the buffer. Recall, I'm using the termination character to help frame the received data. That means I'm reading one line at a time, and after a possible partial first line, I should be starting with the $Talker code and getting a whole message. I pass the first 6 characters off to a case statement, and if it's the $GPGGA message, I parse it, and if it was a good packet, I translate the values into read world units (decimal degrees and MSL in feet), cluster it up and push it to the queue and notifier.

AND - if it was a $GPGGA message, whether the parsing worked or not, I flush the rest of the buffer.

That was core of the problem I was running into. I was only reading 1 NMEA message at a time, and with 2 GPS channels reading, the loop was lagging behind the incoming data, and it was piling up. The telemetry channel doesn't seem to interfere, so I'm suspicious about the bluetooth serial port. I wonder if it's a resource hog. Anyway, now, once I have the $GPGGA message in hand, I flush the rest of the buffer, since I don't parse any of the other messages.

The pattern that the eggFinder is sending is 4 groups of GGA, GSA, RMC messages, followed by a 5th group of GGA, GSA, GSV, GSC, GSC, RMC. Each group is sent at a 1 second interval, so the whole pattern cycle is 5 seconds long - which you can also hear in the LCD receiver beeps.

Right now, I know of one untrapped bug. I have seen an eggfinder send out a dud time value in the GPGGA message. I haven't duplicated the conditions - it's either a certain power off period since the last fix, or so many power cycles without a fix. For now, I wait until the eggFinder has a fix (white LED, or watch with Putty) before I start GPS Acquisition with my app.
 
Are you verifying the checksum on the $GPGGA sentence? That should prevent most bad data from coming through.
 
Are you verifying the checksum on the $GPGGA sentence? That should prevent most bad data from coming through.
I just checked, and the NMEA parser I'm using (which can handle both the GPGGA and the GPRMC messages) is verifying the checksum and sending the result to the error cluster. I'm picking the Status out of the error cluster and sending that to the case structure - if there's no error, I do some further math on the output of the parser and push the results to the queue and notifier.

The bad time condition I mentioned gets through the checksum test. The parser doesn't seem to like a perfect 0 (or maybe a null) in the UTC time.
 
Yep, that's it. When the eggFinder wakes up, it transmits otherwise valid messages with empty internal tokens.
$PSRF150,1*3E
$GPGGA,,,,,,0,00,,,M,0.0,M,,0000*48
$GPGSA,A,1,,,,,,,,,,,,,,,*1E
$GPRMC,,V,,,,,,,,,,N*53

The first thing it gets, before a fix, is the time.
$GPGGA,150854.738,,,,,0,00,,,M,0.0,M,,0000*57
$GPGSA,A,1,,,,,,,,,,,,,,,*1E
$GPRMC,150854.738,V,,,,,,,151111,,,N*48

The parser I found in the Labview communities will return a data element with a GPS Quality of 'invalid' on the latter, but chokes on the empty time string of the former.

Fixed.
 
Last edited:
The empty data in the $GP sentences means that there's no valid data yet. Is that a fatal condition in the parser, or does it just return an error and/or garbage until there's valid data?
 
Now, displaying the data. Here's the telemetry display loop.
1678815605079.png

I told you that I was breaking the 'let queue updates drive the loop rate' paradigm of the rest of the loops. First thing in the loop, I'm popping off a data element from the telemetry notifier. BUT, I've given it a 500ms timeout. If the pop times out, the True case does nothing, but if a data element IS returned, then the values are sent to the front panel indicators. Flight State is also chased through a case statement to turn on the appropriate display button. AND, the timestamp of the data element is subtracted from the current timestand and sent to the Telemetry Silence indicator. That's so I have a time since last packet display. And that's why I force the Notifier Pop to time out. Otherwise the display would only update when there's a packet, and the time since last packet would always essentially be zero.

The 'timed out' == True case passes the prior timestamp forward to the time difference calc and the shift register.

This display is pretty simple. The only other kinda sneaky thing I do is to wire the reported Apogee to a second needle on the altitude dial.
 
The empty data in the $GP sentences means that there's no valid data yet. Is that a fatal condition in the parser, or does it just return an error and/or garbage until there's valid data?
It was fatal for the timestamp. It 'worked' for everything else in that it processed it and returned an almost empty data element with the quality enum set to 'invalid'.

I fixed it by trapping that particular case. If the string is empty, it replaces it with a valid, but 0 filled, string. Now it doesn't give a fatal error.
1678816391485.png
 
The GPS data display is slightly more complicated. I pop off data from both the sustainer and booster notifiers, and update the respective indicators if the pop didn't time out.
1678817583717.png
I also build the marker string for the map API (or an empty string if there was no data, and if -either- GPS had data, I concatenate the marker strings and send it a data in a Refresh Map message to the UI queue. The marker strings are passed to shift registers, so if a GPS packet isn't receiver, the prior marker is used. I've still seen the markers disappear and reappear from the map - haven't figured out why yet.

And that's the app to date. It does work - there are pictures over in the 'what I did today rocket-wise' thread.

I want to clean up the states of the UI QDSM to make sure the program closes cleanly. And then I want to log the data to CSV files.
 
I did some clean up today. Well, it doesn't make the block diagrams prettier, but it mops up some stray odd behavior. I switched the GPS data element update to checking for a GPS-Fix - the GPGGA message can have no errors, but the data be invalid.

And I fixed possible interactions of the control buttons by making an array of booleans (1 for telemetry, one for booster, one for sustainer) and doing a logical OR on them to determine whether or not to enable or disable the settings and exit buttons.

Here's the example from the 'Start Sustainer' UP loop message case.
1678922735939.png
At the bottom, I set array element #2 to TRUE, then OR the whole array, NOT the result, and send it, and the control pointers, the to 'make it so' VI.

I also worked my way througn an analysis of the UI QDSM messages and states. The states are mostly used to make sure both acquisition and logging are stopped before exiting if someone closes the panel, rather than using the exit button. I'll keep thing about whether I want to invest the energy in being that tidy.

I also got an EXE built today. Whoops, and one more thing, I moved the Maps API Key to the configuration typedef and XML. So I can strip it out before releasing it.

And Cris Erving has a copy for testing.

Time for popcorn and whiskey.
 
Last edited:
So, more progress. I decided to try and simplify the logging by staying with just one logging VI - rather than three, like acquisition. With the acquisition, I've got three serial channels that are streaming in data whether I'm ready or not. I wanted the reading to be as asynchronous as possible. But the data logging only happens when there's a parsed packet in hand. It doesn't matter if it's time sync'd.

So far, I've got both GPS channels logging. I wrote it first, just logging the $GPGGA messages to a txt file for each of the eggFinders. Then I got more ambitious.

So, the logging loop has a QDSM with some messages and really simple states - 'idle' and 'logging'. I expanded the states into an array, and then mirrored it with an array of booleans. I did this so the 'logging stopped' message would only get sent back to the UI loop when all three channels were idled.

1679105139497.png1679105192287.png

The logging loop has three sets of Start, Log, and Stop cases - one for each channel.
The interaction with the UI state machine probably isn't perfect - but it's working. The edge case of closing the front panel with the windows 'X' rather than stopping the acq/log and exiting the app when the Exit button is enabled is probably still messy. I still don't care that much.

I ran into a bug that drove me bonkers all evening. I found an unusable workaround and two actual fixes. An earlier error message that I was getting when stopping acq/log should have clued me in - but it didn't.

The 'Log' state of the QDSM - as coded in the original template from NI that I'm using has a subtle flaw.

Here's the message case:
1679105602980.png

And here's the logging VI.
1679105667763.png

The kicker is that the data queue pop doesn't have a timeout. If acquisition stops before logging - and logging has been keeping up with the queue - then this case just hangs. Whoops. So the first thing I did was to put a timeout here. That works - but does force slow, continuing execution - rather than being message driven.

I realized the original way would work if I always made sure to stop logging before the data. So I changed the UI loop to only send a stop message to the logging, and I have the logging send a stop message to the appropriate acq loop.

1679105999921.png

I had tried it the other way - with the timeout and sending a 'stop logging' message from the acq loops - but I think this is cleaner.

Now for the meaty upgrade. The 'Start Logging' message will give it away:
1679106169857.png

That right - a KML header. The 'Stop Logging' message closes it out.

1679106240148.png

The earlier warning message that should have clued me into a basic design issue was that I had added code to write out any pending data elements that were left when the queue is flushed. A format string was failing - because there were never any pending data packets. The logging loops runs way fast enough to keep pace with the acq loops. So I got rid of that purge - but it took me a long while that the QDSM was getting hung up on lack of data when the acq loop stopped before the logging loop.

Here's proof they work in Google Earth - though the altitude was a bit high at the time.
1679106405874.png

That just leaves telemetry logging.
 
Last edited:
Back
Top