OpenSync is an open-source project that provides pilots with an automated digital flight-log. With compatible avionics, such as the Garmin G1000, it also provides warnings for significant flight and/or engine deviations through post-flight e-mails and uploads to SavvyAnalysis.
The Blues Wireless Notecard is the perfect device to provide cellular connectivity to the OpenSync project. It provides plug-and-play simplicity, small size, and an easy Python API.
The Blue Wireless Notehub is the central management for the OpenSync device and routes flight log data to Amazon Web Services (AWS) Simple Queue Service, for processing by AWS Lambda and e-mail delivery via AWS Simple Email Service (SES). This server-less architecture is robust, and for most users will be completely free.
The focus of this guide is on building OpenSync using the Blues Wireless platform. Occasionally, aviation-specific details are included (in italics) to provide interesting context to the non-pilot, but these can be skipped without affecting the understanding of the project.
Choosing a Blues Wireless PlatformBlues Wireless provides many different Notecards and Notecarriers (i.e. boards that hold the Notecard and adapt it to various platforms). For this project, the Notecard WBNA-500 was selected because it provides higher throughput LTE Cat-1 and support for a diversity antenna.
If you’re operating outside of North America you should select a different Notecard. But because the API for all the Notecards are identical, the OpenSync software will work with any model you chose.
Blues provides Notecarriers for Raspberry Pi and Feather compatible devices. OpenSync currently utilizes the Raspberry Pi platform due to an easier development environment, full-breath of Python libraries, and the fact that size and power consumption are not significant constraints.
Aircraft Avionics BackgroundAircraft are fitted with a wide-variety of avionics, ranging from the familiar dials and gauges (called a “six-pack”) to modern “glass cockpits” where screens have replaced the dials and gauges. The Garmin G1000 series of avionics is one of the most popular platforms for both retrofits and new aircraft. A typical G1000 setup includes two screens, the primary flight display (PFD) on the left and the multi-function flight display (MFD) on the right.
Garmin G1000 devices typically log flight data to an SD card in the MFD top-slot. Data is written to the SD card once each second while the MFD is powered on. A new file is created every time the MFD is powered on using a file name in the format of log_YYMMDD_HHMMSS_NEARESTAIRPORT.csv. A wide-variety of parameters are logged by the G1000, and the specifics will depend on the aircraft. OpenSync has been tested to work with logs from a Garmin G1000 Cirrus aircraft, but could easily be expanded to support other aircraft.
Interfacing with the Wifi SD CardThe G1000 writes logs to the SD card once a second, so OpenSync relies upon a WiFi-enabled SD card to allow the logs to be obtained wirelessly. The Toshiba FlashAir WiFi version W-04 is proven to be compatible with Garmin G1000 devices, but a 16GB card is now $320! Sometimes you can find a FlashAir W-04 for sale on e-Bay for less; however, be careful that you get a W-04 version because earlier versions might not work. The ezShare WiFi SD card is a low-cost alternative which generally costs less than $50 when you can find one, but has limited capability on Garmin G1000.
The Toshiba Flash Air has an easy to use and documented API. Unfortunately, the ezShare device was intended to be used in digital cameras via a webpage interface. As such, there isn’t an official ezShare API, but much of the API has been reverse engineered by others. OpenSync utilizes the following APIs:
# Fetch the version information of the ezShare
$ curl http://ezshare.card/client?command=version
# list the files in DIRNAME
$ curl http://ezshare.card/dir?dir=DIRNAME
# download a specific file
$ curl http://ezshare.card/download?file=8CHAR_FILENAME
Because the ezShare device doesn’t have a formal API, the dir command returns a plain HTML page that needs to be parsed before it can be used by OpenSync. Each file in the directory is represented with a single-line within a preformatted HTML block with download links for each file.
$ curl http://ezshare.card/dir?dir=A:
<html xmlns="http://www.w3.org/1999/xhtml">
...
<body>
...
<h1>Directory Index of A:</h1>
<pre>
2021- 4- 5 21:32:58 176KB <a href="http://192.168.4.1/download?file=LOG_21~3.CSV"> log_210406_002903_KXXX.csv</a>
2021- 4- 5 22:23:34 1632KB <a href="http://192.168.4.1/download?file=LOG_21~4.CSV"> log_210406_003502_KXXX.csv</a>
...
Total Entries: 104
Total Size: 155174KB
</pre>
</body>
</html>
The easiest way to parse HTML in Python is using the BeautifulSoup library. Each file available for download is parsed into a list of tuples.
match_pattern = "(\d{4})-( \d{1}|\d{2})-( \d{1}|\d{2})\s+( \d{1}|\d{2}):( \d{1}|\d{2}):( \d{1}|\d{2})\s+(\d+)KB"
result = []
for a in soup.find_all('a'):
if a.previousSibling == None:
continue
metadata_match = re.match(match_pattern,a.previousSibling.string.strip())
fmatch = re.match("http://.+/download\?file=(.+)", a.get('href'))
if metadata_match and fmatch:
created_at = datetime.datetime(
int(metadata_match.group(1)),
int(metadata_match.group(2)),
int(metadata_match.group(3)),
int(metadata_match.group(4)),
int(metadata_match.group(5)),
int(metadata_match.group(6)),
)
size_kb = int(metadata_match.group(7))
result.append((fmatch.group(1), a.string.strip(), created_at, size_kb))
The G1000 writes files into a directory named data_log, so finding a file and downloading it can be accomplished with the following code.
sdcard = ezshare.EzShare(ezshare_url)
files = sdcard.files("A:%5Cdata_log")
for short_fname, fname, created_at, filesize in files:
data = sdcard.download(short_fname)
Dealing with Multi-Function Display (MFD) shutdownIn the aircraft, when the battery is turned off the MFD shuts down immediately. This will cause power to the SD card to be lost, causing the WiFi link between the SD card and the OpenSync to be terminated. Furthermore, the OpenSync is being powered from the 12VDC auxiliary port, so it will also immediately lose power. Because the goal of OpenSync is to provide a post-flight log, it needs to deal with both the loss of connection to the ezShare card and loss of power to the Raspberry Pi.
Once the avionics are shut down and the ezShare WiFi link is dropped, OpenSync can no longer fetch the log file from the CSV card. There are two ways to deal with this: (A) wait until the next power-on to download previous flights log or (B) periodically download the log-file and then at the end of the flight parse the last download. Because we want a post-flight summary to be sent via email, only the latter is a viable approach for OpenSync.
The last 30 seconds of a log are generally uninteresting because the aircraft has landed, taxied to parking, and had the engine shutdown. This means that as long as OpenSync fetches the log file at least every 30 seconds it can produce an useful flight summary based on the last downloaded file. To handle the rare case where a flight log didn't get fully processed, on power-up OpenSync will search for unprocessed files and then download and process them on start-up.
Due to external power being lost on shutdown, a battery backup is required to provide power long enough for OpenSync to parse, summarize, and send the log via Notehub. An easy way to accomplish this is with the HiPi.io UPS HAT. Once the external power is removed, the UPS HAT automatically switches to battery power and allows OpenSync to continue operating for at least 30 minutes.
The status of the external power is read by monitoring GPIO pins and, once external power is lost, the OpenSync will process the file.
# Pin 17 is used to monitor if external power is available
GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.IN)
# Pin 18 is used by the UPS HAT to determine if the Raspberry Pi
# has been shut-down; Pin 18 automatically goes to Hi-Z state on shutdown.
GPIO.setup(18, GPIO.OUT)
GPIO.output(18, GPIO.LOW)
def external_power_available():
return GPIO.input(17)
power_status = external_power_available()
if not power_status:
# External power has been lost
Summarizing the Flight LogsThe flight logs can grow to multiple MBs, which makes it prohibitive to transfer via the cellular network if you are trying to keep overall data usage low. Instead of transferring the entire log, OpenSync summarizes the log file and then sends the summary as a JSON record via Notehub. The log file is processed to produce the following summary:
- max_fuel - the maximum fuel on board during the flight
- min_fuel - the minimum fuel on board during the flight
- fuel_consumed - the estimated fuel used during the flight
- fuel_remaining - the estimated fuel remaining in the aircraft after the flight
- max_cht - the maximum cylinder heat temperature recorded during the flight
- max_egt - the maximum exhaust gas temperature recorded during the flight
- max_tit - the maximum turbo inlet temperature recorded during the flight
- max_oil_temp - the maximum oil temperature recorded during the flight
- max_oil_pressure - the maximum oil pressure recorded during the flight
- max_manifold_pressure - the maximum manifold pressure recorded during the flight
- max_rpm - the maximum engine RPM recorded during the flight
- max_ias - the maximum indicated airspeed recorded during the flight
- max_tas - the maximum true airspeed recorded during the flight
- max_lat_accel - the maximum lateral G-forces recorded during the flight
- max_norm_accel - the maximum vertical G-forces recorded during the flight
- min_bat1_volts - the minimum battery one voltage recorded during the flight
- min_bat2_volts - the minimum battery two voltage recorded during the flight
- max_bat1_amps - the maximum battery one amperage draw recorded during the flight
- hobbs_time - estimated hobbs time, accumulated when engine RPM is greater than zero
- flight_time - estimated flight time, accumulated when indicated airspeed is greater than 35 knots
- origin_pos - the initial latitude/longitude on power up
- destination_pos - the final latitude/longitude on power down
Aviation Note: Hobbs Time vs Flight Time - Pilots keep very accurate logs of their flights because FAA requires that pilots maintain records that demonstrate they have met the requirements for obtaining licenses, ratings, and currency (i.e. the ability to fly legally). Most pilots will record Hobbs Time, which is the amount of time the engine has been running because once the engine starts the pilot is now acting in-command of the aircraft. In addition, when renting an aircraft, pilots are usually charged based on the amount of Hobbs Time elapsed. The problem is that Hobbs Time accumulates when the plane is sitting on the ground or taxiing around the airport, and it's therefore not an accurate measure of how much 'flying' the pilot or aircraft actually did. Flight Time is a measure of how much time the aircraft was engaged in flight. There isn't a strict definition of Flight Time, but on the Cirrus aircraft Flight Time accumulates whenever the aircraft speed is in excess of 35 knots.
Most of these summarized entries are easy to calculate; however, fuel quality and consumption is difficult because the G1000 only logs the fuel quantity in each tank and (surprisingly) does not measure the amount of fuel consumed. The fuel quantity measurements can be very noisy, especially during the final phase of flight (i.e. landing) when the plane may be banking considerably.
Consider the following flight logs:
During the first thirty seconds of logs we can obtain a fairly accurate measure of total fuel by simply adding the left-tank and right-tank quantities together. In this case we estimate 91.5 gallons (the total fuel capacity of an Cirrus SR-22 is 92 gallons)
However, during the last few minutes of logs, the quantity indicators vary considerably due to the aircraft banking in the air and turning on the ground. Based on the logs, it would appear that approximately 29.8 gallons of fuel remain, which leads to a total fuel consumption of 61.74 gallons. However, the fuel totalizer reported approximately 59 gallons consumed.
Aviation Note: About Fuel Indicators - It may come as a surprise to most people that the fuel quantity indicator on an airplane only needs to provide an accurate reading when the tank is empty. Specifically the regulations state "Each fuel quantity indicator must be calibrated to read 'zero' during level flight when the quantity of fuel remaining in the tank is equal to the unusable fuel supply". In modern aircraft, the fuel indicators can be precise and accurate even when not empty, but pilots never solely rely on the fuel quantity indicators. The fuel totalizer is an alternate tool for measuring fuel consumed (and thus fuel remaining). The fuel totalizer provides a very accurate measurement of the fuel consumed by keeping track of exactly how much fuel was delivered to the engine. Prior to flight, pilots will estimate the fuel required using flight-planning tools which consider the winds and aircraft performance. During pre-flight pilots will visually confirm the amount of fuel in each tank to ensure sufficient fuel is available prior to take-off. During flight the fuel quantity is monitored on the fuel-tank indicators and the fuel-totalizer. Because the fuel quantity indicators may not read accurately until empty, the pilot will monitor the fuel totalizer and compare it against planned fuel consumption and the total amount of fuel verified visually. If the G1000 logged the fuel totalizer it would be very easy to accurately estimate the amount of fuel consumed during the flight.Global Cellular with Notecard
The easiest part of OpenSync is sending data via the Notecard to Notehub. This is a testament to the amount of engineering and design effort the Blues Wireless team has put into their devices.
After summarizing the G1000 flight log into a Python dictionary, the "node.add" command is called and magically the summary is sent to Notehub.
from periphery import I2C
import notecard
port = I2C("/dev/i2c-1")
card = notecard.OpenI2C(port, 0, 0)
req = {
"req": "note.add",
"body": flight_summary
}
rsp = nCard.Transaction(req)
Notehub and AWSThe goal of OpenSync is to have a low-cost/no-cost serverless platform. In this area, the Blues Wireless Notehub really shines with its easy management of devices and simple integration with cloud-services like AWS Simple Queue Service (SQS). This tutorial does not attempt to explain every detail of setting up Notehub or AWS services. You can reference these guides to understand how to get things setup.
The first step is establishing an SQS queue to receive the events from Notehub. After creating the SQS queue you can easily route messages via Notehub. I chose to use a simple Access Key-based method of authentication between Notehub and AWS SQS. The route is set up to only pass "data.qo" records to SQS. In other words, only the flight log summaries are passed to SQS. The other Notecard status events that are generated will not be passed to SQS.
The SQS service is then routed to a Lambda function that does a simple conversion of the JSON into somewhat well-formatted HTML suitable for an e-mail report. Then, using SES, an email is sent to the pilot.
By creating a Web Proxy within Notehub, we can push log messages directly to Saavy Analysis. The Web Proxy Route is particularly useful when sending large files, such as the raw G1000 CSV logs.
The full logs can be quite large, but many of the fields in them are not particularly interesting for post-flight analysis of aircraft performance. For example, a 2.5 hour flight resulted in a 4.71MB log-file. The log file size can be reduced to 1.48MB by only keeping fields related to engine performance, battery condition, and fuel.
Aviation Note: Logbooks - Pilots use flight logs for multiple purposes. When analyzing the pilot's performance it is useful to track things like position, speed, pitch, roll, etc. Many pilots are now using iPad-based applications such as Garmin Pilot or ForeFlight to capture these types of flight logs. SavvyAnalysis is primarily geared towards monitoring engine performance so for many users, it makes sense to prune the log file down to the minimum necessary to reduce cellular data usage.
Sending a large file uses the Notecard Web Transaction feature to send a payload from the Notecard, through Notehub, and then transparently onwards to the configured web end point. The Savvy Analysis API is relatively simple, but poses two small challenges when using Notecard: (A) the payload is a content type of multipart/form-data and (B) the log files can be quite large on a long flight (i.e. multiple MBs). An example of invoking the Savvy Analysis API via curl is shown below.
$ curl -X POST https://apps.savvyaviation.com/upload_files_api/AIRCRAFT ID/ \
--form "token=4724aa85-a61b-4d4f-a0ed-xxxxyyyyzzzz" \
--form "file=@log_XXXXXXX.csv"
The first step is creating a Proxy route within Notehub as shown below. This proxy route will allow the Notecard to send HTTP GET/POST requests to the Savvy Analysis URL.
Generating the payload is straightforward using the requests_toolbelt MultipartEncoder.
from requests_toolbelt.multipart.encoder import MultipartEncoder
mp_encoder = MultipartEncoder(
fields={
'token': token,
'file': (fname, data, 'text/plain'),
}
)
payload = mp_encoder.to_string()
This payload is now ready to be sent with a web.post request. Because the payload can be quite large, it needs to be sent in fragments rather than one large request. This requires that your Notecard be upgraded to firmware version 3.2.1 or greater.
Over-the-air Firmware UpdatesIf you haven't used Notecard before, the over-the-air firmware update process is effectively magic. The Blues Wireless team had already made cellular IoT plug-and-play but then they went one step further and made over-the-air firmware updates as simple as clicking a few buttons.
Warning, if the Notecard firmware hasn’t been updated to 3.2.1 a fragmented web.post won't produce an error, it will simply transfer each fragment individually as if they were regular requests. If you get unexpected behavior or errant responses, check that you are running the correct firmware.
Web Post DetailsA fragmented web.post requires three additional parameters on each request.
- total - The total size, in bytes, of the entire payload, across all fragments.
- offset - For a given fragment, the number of bytes to offset from zero when reassembling fragments.
- status - a base64-encoded MD5 sum of the payload fragment. Used by Notehub to validate the payload fragment upon receipt.
It’s important to note that total and offset are the byte positions prior to base 64 encoding. Therefore, sending a large payload via fragmented requests can be accomplished as follows:
fragmented = ( len(payload) > chunk_size )
offset = 0
while offset < len(payload):
req = {
"req": "web.post",
"route": "SavvyAnalysis",
"name": "{aircraft_id}/",
content=mp_encoder.content_type
}
if fragmented:
fragment = payload[offset:offset+chunk_size]
req["total"] = len(payload)
req["payload"] = b2a_base64( fragment, newline=False).decode("ascii")
req["status"] = hashlib.md5( fragment ).hexdigest()
req["offset"] = offset
req["verify"] = True
else:
req["payload"] = b2a_base64( payload, newline=False ).decode("ascii")
offset += chunk_size
rsp = card.Transaction(req)
if offset < len(payload) and rsp.get("result") != 100:
raise RuntimeError(“error in fragmented web.post”)
response = None
if rsp.get("payload"):
response = a2b_base64(rsp['payload']).decode("ascii")
return response
The web.post request requires that the Notecard mode be set to continuous mode. In order to avoid accidentally leaving the card in continuous mode, OpenSync utilizes a custom Python context helper to ensure that the mode is returned to its original value. During flight, the Notecard is kept in the minimal mode since usually the aircraft is flying high enough that cellular connectivity is unreliable.
with notecard_helpers.temporary_mode(card, "continuous", wait_for_connection=True):
# ... make web.post requests …
Finally, to send large payloads via web.post quickly, it is necessary to reduce the Notecard segment delay. By default, a Notecard request is broken into 250 byte chunks (i.e. CARD_REQUEST_SEGMENT_MAX_LEN) and after each chunk a 250ms delay (i.e. CARD_REQUEST_SEGMENT_DELAY_MS) is added to allow the Notecard to process the data.
Base 64 encoding increases the size of a payload by 33%, which means that the maximum throughput for a web.post is approximately (250 Bytes / 1.33) / 250 ms = 752 Bytes Per Second. Therefore, a 8kB (8192 Bytes) fragment would take about 11 seconds to transmit. The web.post throughput can be increased by lowering the CARD_REQUEST_SEGMENT_DELAY_MS. Although no documented limits are provided, the Blues Wireless developers recommend keeping the delay at least 25ms or higher. Similar to the card mode, OpenSync uses a Python context helper to ensure that the CARD_REQUEST_SEGMENT_DELAY_MS is restored to its original value after the web.post has completed.
with temporary_segment_delay(50):
# … make web.post requests …
Using Notehub Environment VariablesOpenSync uses Notehub environment variables to simplify configuration and allow for changes to be made without connecting the Raspberry Pi to a computer. For example, the Savvy Analysis token and aircraft ID can be set for the device via the Notehub web page.
These are then retrieved on startup by OpenSync and merged with the command-line arguments, providing a simple way to set configuration via file, command-line, or Notehub environment variables.
import configargparse
parser = configargparse.ArgParser(
default_config_files=[ "/etc/opensync.conf", "~/.opensync.conf" ]
parser.add_argument(
"--savvy-aviation-aircraft-id"
)
parser.add_argument(
"--savvy-aviation-token"
)
args = vars(parser.parse_args())
# Get environment variables and merge them into kwargs, only
# overriding config-file/command-line if no other value was provided
req = {"req": "env.get"}
rsp = card.Transaction(req)
for env_key, env_val in rsp.get("body", {}).items():
if env_key.startswith("opensync_"):
opensync_arg = env_key[9:]
if kwargs.get(opensync_arg) is None:
logging.info("Setting %s = %s", env_key[9:], env_val)
kwargs[env_key[9:]] = env_val
GPS TrackingAlthough the G1000 logs include very precise GPS coordinates, it is convenient to utilize the Notecard’s built-in GPS to provide additional position tracking. This feature can also be useful for aircraft that do not use Garmin avionics.
The GPS tracking mode is easily turned on with a few calls to the Notecard.
req = {
"req": "card.location.mode",
"mode”: "periodic"
"seconds": 60
}
rsp = card.Transaction(req)
req = {
"req": "card.location.track",
"start": True
}
rsp = card.Transaction(req)
I found that passive GPS antennas didn’t work well enough, so OpenSync uses a Cirocomm active GPS antenna. Remember to switch the toggle on the bottom of the Notecarrier to “Active GPS”.
AssemblingThe fully assembled OpenSync consists of a Raspberry Pi, followed by the Notecarrier-Pi (with Notecard installed), the UPS Hat, the GPS Antenna, and one or two cellular antennas depending on the Notecard being used.
The entire OpenSync is then placed inside a 3D printed case, ready to be installed into the airplane.
It is annoying (and expensive) to fly every time you want to test OpenSync. The easiest way to test at home is to grab a bunch of log files and place the ezShare adapter into your computer. Using the tools/replay.py script, you can simulate a new log file being written one-second per-line to the SD card.
Putting it All TogetherIf you’re looking to assemble this yourself, you can complete the following steps to get up and running.
- Assemble the Raspberry Pi, Notecarrier with Notecard, and UPS Hat.
- Download the Raspberry Pi Imager from https://www.raspberrypi.com/software/.
- Select Raspberry Pi OS (other) and then choose Raspberry Pi OS Lite (32-bit).
- Press Ctrl-Shift-X to display advanced options. Set hostname to opensync and Enable SSH.
- Choose the Storage for your desired SD card and press Write.
- Assemble the Notecarrier (with Notecard) on top of the Raspberry Pi, then add the UPS-HAT on top of the Notecarrier.
- Connect the Raspberry Pi device Ethernet to your network and apply power to the device.
- Follow the directions in README.md.
Comments