diff --git a/.gitmodules b/.gitmodules index a89fd995..13a52a3c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "linux-mpu9150"] - path = linux-mpu9150 - url = git://github.com/cyoung/linux-mpu9150 [submodule "dump1090"] path = dump1090 url = https://github.com/AvSquirrel/dump1090 diff --git a/Makefile b/Makefile index 0cbb5ee1..828cbf87 100644 --- a/Makefile +++ b/Makefile @@ -9,12 +9,11 @@ endif all: make xdump978 make xdump1090 - make xlinux-mpu9150 make xgen_gdl90 xgen_gdl90: - go get -t -d -v ./main ./test ./linux-mpu9150/mpu ./godump978 ./mpu ./uatparse - go build $(BUILDINFO) -p 4 main/gen_gdl90.go main/traffic.go main/ry83Xai.go main/network.go main/managementinterface.go main/sdr.go main/ping.go main/uibroadcast.go main/monotonic.go main/datalog.go main/equations.go + go get -t -d -v ./main ./test ./godump978 ./uatparse ./sensors + go build $(BUILDINFO) -p 4 main/gen_gdl90.go main/traffic.go main/gps.go main/network.go main/managementinterface.go main/sdr.go main/ping.go main/uibroadcast.go main/monotonic.go main/datalog.go main/equations.go main/sensors.go xdump1090: git submodule update --init @@ -24,10 +23,6 @@ xdump978: cd dump978 && make lib sudo cp -f ./libdump978.so /usr/lib/libdump978.so -xlinux-mpu9150: - git submodule update --init - cd linux-mpu9150 && make -f Makefile-native-shared - .PHONY: test test: make -C test @@ -53,4 +48,3 @@ clean: rm -f gen_gdl90 libdump978.so cd dump1090 && make clean cd dump978 && make clean - rm -f linux-mpu9150/*.o linux-mpu9150/*.so diff --git a/README.md b/README.md index b69a4d35..55381e4e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ Raspberry Pi 2 with the Edimax EW-7811Un Wi-Fi dongle is supported but not recom Tested and works well with most common R820T and R820T2 RTL-SDR devices. +Tested with and preliminary support added for [uAvionix pingEFB dual-link ADS-B receiver](http://www.uavionix.com/products/pingefb/). + + Apps with stratux recognition/support: * Seattle Avionics FlyQ EFB 2.1.1+. * AvNav EFB 2.0.0+. @@ -33,3 +36,10 @@ Questions? [See the FAQ](https://github.com/cyoung/stratux/wiki/FAQ) http://stratux.me/ https://www.reddit.com/r/stratux + +Jet tests (high gain antennas): + +* Dassault Falcon 20 +* Embraer ERJ 145 +* Cessna Citation 501 +* Lear 35 diff --git a/__root__stratux-pre-start.sh b/__root__stratux-pre-start.sh index cda14e54..4e250df8 100755 --- a/__root__stratux-pre-start.sh +++ b/__root__stratux-pre-start.sh @@ -15,3 +15,39 @@ if [ -e /root/update*stratux*v*.sh ] ; then fi fi +##### Script for setting up new file structure for hostapd settings +##### Look for hostapd.user and if found do nothing. +##### If not assume because of previous version and convert to new file structure +DAEMON_USER_PREF=/etc/hostapd/hostapd.user +if [ ! -f $DAEMON_USER_PREF ]; then + DAEMON_CONF=/etc/hostapd/hostapd.conf + DAEMON_CONF_EDIMAX=/etc/hostapd/hostapd-edimax.conf + HOSTAPD_VALUES=('ssid=' 'channel=' 'auth_algs=' 'wpa=' 'wpa_passphrase=' 'wpa_key_mgmt=' 'wpa_pairwise=' 'rsn_pairwise=') + HOSTAPD_VALUES_RM=('#auth_algs=' '#wpa=' '#wpa_passphrase=' '#wpa_key_mgmt=' '#wpa_pairwise=' '#rsn_pairwise=') + + for i in "${HOSTAPD_VALUES[@]}" + do + if grep -q "^$i" $DAEMON_CONF + then + grep "^$i" $DAEMON_CONF >> $DAEMON_USER_PREF + sed -i '/^'"$i"'/d' $DAEMON_CONF + sed -i '/^'"$i"'/d' $DAEMON_CONF_EDIMAX + fi + done + for i in "${HOSTAPD_VALUES_RM[@]}" + do + if grep -q "^$i" $DAEMON_CONF + then + sed -i '/^'"$i"'/d' $DAEMON_CONF + sed -i '/^'"$i"'/d' $DAEMON_CONF_EDIMAX + fi + done + sleep 1 #make sure there is time to get the file written before checking for it again + # If once the code above runs and there is still no hostapd.user file then something is wrong and we will just create the file with basic settings. + # Any more then this they somebody was messing with things and its not our fault things are this bad + if [ ! -f $DAEMON_USER_PREF ]; then + echo "ssid=stratux" >> $DAEMON_USER_PREF + echo "channel=1" >> $DAEMON_USER_PREF + fi +fi +##### End hostapd settings structure script diff --git a/image/config.txt b/image/config.txt index 937f43be..b1247655 100644 --- a/image/config.txt +++ b/image/config.txt @@ -8,3 +8,7 @@ dtparam=i2c_arm_baudrate=400000 # move RPi3 Bluetooth off of hardware UART to free up connection for GPS dtoverlay=pi3-miniuart-bt + +# disable default (mmc0) behavior on the ACT LED. +dtparam=act_led_trigger=none +dtparam=act_led_activelow=off diff --git a/image/dhcpd.conf b/image/dhcpd.conf index 83939f18..fc267f2a 100644 --- a/image/dhcpd.conf +++ b/image/dhcpd.conf @@ -110,6 +110,7 @@ log-facility local7; subnet 192.168.10.0 netmask 255.255.255.0 { range 192.168.10.10 192.168.10.50; option broadcast-address 192.168.10.255; + option routers 192.168.10.1; default-lease-time 12000; max-lease-time 12000; option domain-name "stratux.local"; diff --git a/image/fancontrol.py b/image/fancontrol.py old mode 100644 new mode 100755 index 9b11be04..6366719f --- a/image/fancontrol.py +++ b/image/fancontrol.py @@ -3,32 +3,43 @@ # # This script throttles a fan based on CPU temperature. # -# It expects a fan that's externally powered, and uses GPIO pin 11 for control. +# It expects a fan that's externally powered, and uses GPIO pin 12 for control. import RPi.GPIO as GPIO import time import os -# Return CPU temperature as float -def getCPUtemp(): - cTemp = os.popen('vcgencmd measure_temp').readline() - return float(cTemp.replace("temp=","").replace("'C\n","")) +from daemon import runner -GPIO.setmode(GPIO.BOARD) -GPIO.setup(11,GPIO.OUT) -GPIO.setwarnings(False) -p=GPIO.PWM(11,1000) -PWM = 50 +class FanControl(): + # Return CPU temperature as float + def getCPUtemp(self): + cTemp = os.popen('vcgencmd measure_temp').readline() + return float(cTemp.replace("temp=","").replace("'C\n","")) -while True: + def __init__(self): + self.stdin_path = '/dev/null' + self.stdout_path = '/var/log/fancontrol.log' + self.stderr_path = '/var/log/fancontrol.log' + self.pidfile_path = '/var/run/fancontrol.pid' + self.pidfile_timeout = 5 + def run(self): + GPIO.setmode(GPIO.BOARD) + GPIO.setup(12, GPIO.OUT) + GPIO.setwarnings(False) + p=GPIO.PWM(12, 1000) + PWM = 50 + while True: + CPU_temp = self.getCPUtemp() + if CPU_temp > 40.5: + PWM = min(max(PWM + 1, 0), 100) + p.start(PWM) + elif CPU_temp < 39.5: + PWM = min(max(PWM - 1, 0), 100) + p.start(PWM) + time.sleep(5) + GPIO.cleanup() - CPU_temp = getCPUtemp() - if CPU_temp > 40.5: - PWM = min(max(PWM + 1, 0), 100) - p.start(PWM) - elif CPU_temp < 39.5: - PWM = min(max(PWM - 1, 0), 100) - p.start(PWM) - time.sleep(5) - -GPIO.cleanup() +fancontrol = FanControl() +daemon_runner = runner.DaemonRunner(fancontrol) +daemon_runner.do_action() diff --git a/image/hostapd-edimax.conf b/image/hostapd-edimax.conf index f1e7552f..19f599a0 100644 --- a/image/hostapd-edimax.conf +++ b/image/hostapd-edimax.conf @@ -1,8 +1,6 @@ interface=wlan0 driver=rtl871xdrv -ssid=stratux hw_mode=g -channel=1 wme_enabled=1 ieee80211n=1 ignore_broadcast_ssid=0 diff --git a/image/hostapd.conf b/image/hostapd.conf index ca3c19f0..10909401 100644 --- a/image/hostapd.conf +++ b/image/hostapd.conf @@ -1,7 +1,5 @@ interface=wlan0 -ssid=stratux hw_mode=g -channel=1 wmm_enabled=1 ieee80211n=1 ignore_broadcast_ssid=0 diff --git a/image/hostapd_manager.sh b/image/hostapd_manager.sh index 13963310..2901bcec 100644 --- a/image/hostapd_manager.sh +++ b/image/hostapd_manager.sh @@ -69,7 +69,7 @@ fi #If an option should be followed by an argument, it should be followed by a ":". #Notice there is no ":" after "oqh". The leading ":" suppresses error messages from #getopts. This is required to get my unrecognized option code to work. -options=':s:c:e:oqh' +options=':s:c:eoqh' while getopts $options option; do case $option in s) #set option "s" @@ -90,7 +90,7 @@ while getopts $options option; do OPT_C=$OPTARG echo "$parm Channel option -c used: $OPT_C" if [[ "$OPT_C" =~ ^[0-9]+$ ]] && [ "$OPT_C" -ge 1 -a "$OPT_C" -le 13 ]; then - echo "${GREEN} Channel will now be set to ${BOLD}${UNDR}$OPT_C.${WHITE}${NORMAL}" + echo "${GREEN} Channel will now be set to ${BOLD}${UNDR}$OPT_C${WHITE}${NORMAL}." else echo "${BOLD}${RED}$err Channel is not within acceptable values, exiting...${WHITE}${NORMAL}" exit 1 @@ -99,8 +99,8 @@ while getopts $options option; do ;; e) #set option "e" if [[ -z "${OPTARG}" || "${OPTARG}" == *[[:space:]]* || "${OPTARG}" == -* ]]; then - echo "${BOLD}${RED}$err Encryption option(-e) used without passphrase, exiting...${WHITE}${NORMAL}" - exit 1 + echo "${BOLD}${RED}$err Encryption option(-e) used without passphrase, Passphrase will be set to ${BOLD}$defaultPass${NORMAL}...${WHITE}${NORMAL}" + OPT_E=$defaultPass else OPT_E=$OPTARG echo "$parm Encryption option -e used:" @@ -126,7 +126,7 @@ while getopts $options option; do HELP ;; \?) # invalid option - echo "${BOLD}${RED}$err Invalid option -$OPTARG" >&2 + echo "${BOLD}${RED}$err Invalid option -$OPTARG ${WHITE}${NORMAL}" >&2 exit 1 ;; :) # Missing Arg @@ -161,7 +161,7 @@ echo "${BOLD}No errors found. Continuning...${NORMAL}" echo "" # files to edit -HOSTAPD=('/etc/hostapd/hostapd.conf' '/etc/hostapd/hostapd-edimax.conf') +HOSTAPD=('/etc/hostapd/hostapd.user') #### #### File modification loop @@ -254,9 +254,31 @@ do echo "" fi done -echo "${YELLOW}$att Don't forget to reboot... $att ${WHITE}" +echo "${RED}${BOLD} $att At this time the script will restart your WiFi services.${WHITE}${NORMAL}" +echo "If you are connected to Stratux through the ${BOLD}192.168.10.1${NORMAL} interface then you will be disconnected" +echo "Please wait 1 min and look for the new SSID on your wireless device." +sleep 3 +echo "${YELLOW}$att Restarting Stratux WiFi Services... $att ${WHITE}" +echo "Killing hostapd..." +/usr/bin/killall -9 hostapd hostapd-edimax +echo "Killed..." +echo "" +echo "Killing DHCP Server..." +echo "" +/usr/sbin/service isc-dhcp-server stop +sleep 0.5 +echo "Killed..." +echo "" +echo "ifdown wlan0..." +ifdown wlan0 +sleep 0.5 +echo "ifup wlan0..." +echo "Calling Stratux WiFI Start Script(stratux-wifi.sh)..." +ifup wlan0 +sleep 0.5 echo "" echo "" +echo "All systems should be up and running and you should see your new SSID!" ### End main loop ### diff --git a/image/interfaces b/image/interfaces index 96de7bf1..17dcaf75 100644 --- a/image/interfaces +++ b/image/interfaces @@ -4,33 +4,52 @@ iface lo inet loopback iface eth0 inet dhcp allow-hotplug wlan0 -#iface wlan0 inet manual -#wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf -#iface default inet dhcp - iface wlan0 inet static address 192.168.10.1 netmask 255.255.255.0 post-up /usr/sbin/stratux-wifi.sh +##################################################################### +## Custom settings not for novice users!!!!!! +## Modify at your own risk!!!!!!!!!!!!!!!!!!! ## ## Second Wifi Dongle for local work and internet access -## wifi must be open for these settings to work +## This template is for adding a second wifi dongle to your PI for internet access while debugging +## Modify /etc/wpa_supplicant/wpa_supplicant.conf with your settings also( see below ) ## -## uncomment the next two lines to activate the service as well as modify the settings and comments below -#allow-hotplug wlan1 -#iface wlan1 inet static +## Uncomment the following lines as needed. -# The SSID you want to connect to -# uncomment the next line and modify if necessary -# wireless-essid 6719 -# The address you want to use on your network -# uncomment the next line and modify if necessary -# address 192.168.1.50 -# The address of your netmask -# uncomment the next line and modify if necessary -# netmask 255.255.255.0 -# The gateway of your router that you are connecting to -# uncomment the next line and modify -# gateway 192.168.1.1 +# allow-hotplug wlan1 +# iface wlan1 inet manual +# wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf +# iface name_of_WPA_Config inet dhcp +# iface name_of_other_WPA_Config inet dhcp + + +## End of interfaces + + +#contents of wpa_supplicant.conf +############################################################# +# ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev +# update_config=1 + +# network={ +# ssid="SSID_1" +# id_str="name_of_WPA_Config" +# scan_ssid=1 +# key_mgmt=WPA-PSK +# psk="mypassword" +# priority=1 +# } + +# network={ +# ssid="SSID_2" +# id_str="name_of_other_WPA_Config" +# scan_ssid=1 +# key_mgmt=WPA-PSK +# psk="mypassword" +# priority=3 +# } +############################################################# diff --git a/image/mkimg.sh b/image/mkimg.sh index 9ab5ae69..8b542226 100755 --- a/image/mkimg.sh +++ b/image/mkimg.sh @@ -54,6 +54,10 @@ cp -f interfaces mnt/etc/network/interfaces cp stratux-wifi.sh mnt/usr/sbin/ chmod 755 mnt/usr/sbin/stratux-wifi.sh +#SDR Serial Script +cp -f sdr-tool.sh mnt/usr/sbin/sdr-tool.sh +chmod 755 mnt/usr/sbin/sdr-tool.sh + #ping udev cp -f 99-uavionix.rules mnt/etc/udev/rules.d @@ -72,7 +76,6 @@ cp -f 10-stratux.rules mnt/etc/udev/rules.d #stratux files cp -f ../libdump978.so mnt/usr/lib/libdump978.so -cp -f ../linux-mpu9150/libimu.so mnt/usr/lib/libimu.so #go1.5.1 setup cp -rf /root/go mnt/root/ @@ -126,6 +129,18 @@ sed -i /etc/default/keyboard -e "/^XKBLAYOUT/s/\".*\"/\"us\"/" cp -f config.txt mnt/boot/ #external OLED screen -#apt-get install -y libjpeg-dev i2c-tools python-smbus python-pip python-dev python-pil screen -#git clone https://github.com/rm-hull/ssd1306 -#cd ssd1306 && python setup.py install +apt-get install -y libjpeg-dev i2c-tools python-smbus python-pip python-dev python-pil python-daemon screen +#for fancontrol.py: +pip install wiringpi +cd /root +git clone https://github.com/rm-hull/ssd1306 +cd ssd1306 && python setup.py install +cp /root/stratux/test/screen/screen.py /usr/bin/stratux-screen.py +mkdir -p /etc/stratux-screen/ +cp -f /root/stratux/test/screen/stratux-logo-64x64.bmp /etc/stratux-screen/stratux-logo-64x64.bmp +cp -f /root/stratux/test/screen/CnC_Red_Alert.ttf /etc/stratux-screen/CnC_Red_Alert.ttf + +#startup scripts +cp -f ../__lib__systemd__system__stratux.service mnt/lib/systemd/system/stratux.service +cp -f ../__root__stratux-pre-start.sh mnt/root/stratux-pre-start.sh +cp -f rc.local mnt/etc/rc.local diff --git a/image/rc.local b/image/rc.local new file mode 100755 index 00000000..6161d6c5 --- /dev/null +++ b/image/rc.local @@ -0,0 +1,25 @@ +#!/bin/sh -e +# +# rc.local +# +# This script is executed at the end of each multiuser runlevel. +# Make sure that the script will "exit 0" on success or any other +# value on error. +# +# In order to enable or disable this script just change the execution +# bits. +# +# By default this script does nothing. + +# Print the IP address +_IP=$(hostname -I) || true +if [ "$_IP" ]; then + printf "My IP address is %s\n" "$_IP" +fi + + +/usr/bin/fancontrol.py start +/usr/bin/stratux-screen.py start + + +exit 0 diff --git a/image/sdr-tool.sh b/image/sdr-tool.sh new file mode 100644 index 00000000..3609a664 --- /dev/null +++ b/image/sdr-tool.sh @@ -0,0 +1,295 @@ +#!/bin/bash + +###################################################################### +# STRATUX SDR MANAGER # +###################################################################### + +#Set Script Name variable +SCRIPT=`basename ${BASH_SOURCE[0]}` + +# rtl_eeprom -d 0 -s :: +#Initialize variables to default values. +SERVICE=stratux.service +WhichSDR=1090 +FallBack=true +PPMValue=0 + +parm="*" +err="####" +att="+++" + +#Set fonts for Help. +BOLD=$(tput bold) +STOT=$(tput smso) +DIM=$(tput dim) +UNDR=$(tput smul) +REV=$(tput rev) +RED=$(tput setaf 1) +GREEN=$(tput setaf 2) +YELLOW=$(tput setaf 3) +MAGENTA=$(tput setaf 5) +WHITE=$(tput setaf 7) +NORM=$(tput sgr0) +NORMAL=$(tput sgr0) + +#This is the Title +function HEAD { + clear + echo "######################################################################" + echo "# STRATUX SDR SERIAL TOOL #" + echo "######################################################################" + echo " " +} + +function STOPSTRATUX { + HEAD + echo "Give me a few seconds to check if STRATUX is running..." + # The service we want to check (according to systemctl) + if [ "`systemctl is-active $SERVICE`" = "active" ] + then + echo "$SERVICE is currently running" + echo "Stopping..." + SDRs=`systemctl stop stratux.service` + fi + sleep 3 +} + +#Function to set the serial function +function SETSDRSERIAL { + HEAD + echo "# Setting ${WhichSDR}mhz SDR Serial Data #" + #Build this string + # rtl_eeprom -d 0 -s :: + echo " SETTING SERIAL: " + echo " rtl_eeprom -d 0 -s stx:${WhichSDR}:${PPMValue} " + echo " " + echo "${REV}Answer 'y' to the qustion: 'Write new configuration to device [y/n]?'${NORM}" + echo " " + SDRs=`rtl_eeprom -d 0 -s stx:${WhichSDR}:${PPMValue}` + sleep 2 + echo " " + echo "Do you have another SDR to program?" + echo " 'Yes' will shutdown your STRATUX and allow you to swap SDRs." + echo " 'No' will reboot your STRATUX and return your STRATUX to normal operation." + echo " 'exit' will exit the script and return you to your shell prompt" + choices=( 'Yes' 'No' 'exit' ) + # Present the choices. + # The user chooses by entering the *number* before the desired choice. + select choice in "${choices[@]}"; do + + # If an invalid number was chosen, $choice will be empty. + # Report an error and prompt again. + [[ -n $choice ]] || { echo "Invalid choice." >&2; continue; } + + case $choice in + 'Yes') + echo "Shutting down..." + SDRs=`shutdown -h now` + ;; + 'No') + echo "Rebooting..." + SDRs=`reboot` + ;; + exit) + echo "Exiting. " + exit 0 + esac + break + done +} + + +function SDRInfo { + HEAD + echo "# Building ${WhichSDR}mhz SDR Serial #" + echo " " + echo "Do you have a PPM value to enter?" + echo "If not, its ok... Just choose 'No'" + choices=( 'Yes' 'No' 'exit' ) + # Present the choices. + # The user chooses by entering the *number* before the desired choice. + select choice in "${choices[@]}"; do + + # If an invalid number was chosen, $choice will be empty. + # Report an error and prompt again. + [[ -n $choice ]] || { echo "Invalid choice." >&2; continue; } + + case $choice in + 'Yes') + echo "Please enter your PPM value for your 978mhz SDR:" + read PPMValue + ;; + 'No') + echo " " + ;; + exit) + echo "Exiting. " + exit 0 + esac + break + done + SETSDRSERIAL +} + +function PICKFALLBACK { + HEAD + echo "# Gathering ${WhichSDR}mhz SDR Serial #" + echo " " + echo "${RED}${BOLD}IMPORTANT INFORMATION: READ CAREFULLY${NORM}" + echo "${BOLD}DO you want to set the 1090mhz SDR to Fall Back to 978mhz in the event of the 978mhx SDR failing inflight?${NORM}" + echo "If no serials are set on any of the attached SDRs then STRATUX will assign 978mhz to the first SDR found and 1090mhz to the remaining SDR. This is a safety featre of STRATUX to always allow users to always have access to WEATHER and METAR data in the event of one SDR failing in flight. " + echo " " + echo "When a user assigns a frequency to an SDR, via setting serials, STRATUX will always assign that frequency. NO MATTER WHAT." + echo "This could cause issues if an SDR fails in flight. If the 978mhz SDR fails in flight and the other SDR is assigned the 1090 serial this SDR will never be set to 978mhz and the user will not have access to WEATHER and METAR data" + echo " " + echo "Choosing the Fall Back mode will allow the remaining SDR to be assigned to 978mhz while keeping the PPM value, allowing the user to continue to receive WEATHER and METAR data." + echo "Fall Back mode is reccomended!" + + choices=( 'FallBack' '1090mhz' 'exit' ) + # Present the choices. + # The user chooses by entering the *number* before the desired choice. + select choice in "${choices[@]}"; do + + # If an invalid number was chosen, $choice will be empty. + # Report an error and prompt again. + [[ -n $choice ]] || { echo "Invalid choice." >&2; continue; } + + case $choice in + 'FallBack') + WhichSDR=0 + ;; + '1090mhz') + echo " " + ;; + exit) + echo "Exiting. " + exit 0 + esac + break + done + +} + +function PICKFREQ { + HEAD + echo "# Selecting Radio to set Serial #" + echo " " + echo "${BOLD}Which SDR are you setting up?${NORM}" + echo "${DIM}If you have tuned antennas make sure you have the correct SDR and antenna combination hooked up at this time and remember which antenna connection is for which antenna.${NORM}" + choices=( '978mhz' '1090mhz' 'exit' ) + # Present the choices. + # The user chooses by entering the *number* before the desired choice. + select choice in "${choices[@]}"; do + + # If an invalid number was chosen, $choice will be empty. + # Report an error and prompt again. + [[ -n $choice ]] || { echo "Invalid choice." >&2; continue; } + + case $choice in + '978mhz') + WhichSDR=978 + SDRInfo + ;; + '1090mhz') + PICKFALLBACK + SDRInfo + ;; + exit) + echo "Exiting. " + exit 0 + esac + break + done +} + +function MAINMENU { + HEAD + echo "Loading SDR info..." + sleep 2 + HEAD + echo "-----------------------------------------------------------" + SDRs=`rtl_eeprom` + echo "-----------------------------------------------------------" + echo " " + echo "${BOLD}${RED}Read the lines above.${NORM}" + echo "${BOLD}How many SDRs were found?${NORM}" + + # Define the choices to present to the user, which will be + # presented line by line, prefixed by a sequential number + # (E.g., '1) copy', ...) + choices=( 'Only 1' '2 or more' 'exit' ) + # Present the choices. + # The user chooses by entering the *number* before the desired choice. + select choice in "${choices[@]}"; do + + # If an invalid number was chosen, $choice will be empty. + # Report an error and prompt again. + [[ -n $choice ]] || { echo "Invalid choice." >&2; continue; } + + case $choice in + 'Only 1') + PICKFREQ + ;; + '2 or more') + echo "#####################################################################################" + echo "# ${RED}Too Many SDRs Plugged in. Unplug all SDRs except one and try again!!${NORM} #" + echo "#####################################################################################" + exit 0 + ;; + exit) + echo "Exiting. " + exit 0 + esac + # Getting here means that a valid choice was made, + # so break out of the select statement and continue below, + # if desired. + # Note that without an explicit break (or exit) statement, + # bash will continue to prompt. + break + done +} + +function START { + echo "Help documentation for ${BOLD}${SCRIPT}.${NORM}" + echo " " + echo "This script will help you in setting your SDR serials. Please read carefully before continuing. There are many options in settings the SDR serials. Programming the SDR serials does 2 things. " + echo " " + echo "${BOLD}First:${NORM}" + echo "Setting the serials will tell your STRATUX which SDR is attached to which tuned antenna." + echo " " + echo "${BOLD}Second:${NORM}" + echo "Setting the PPM value will enhance the reception of your SDR by correcting the Frequency Error in each SDR. Each PPM value is unique to each SDR. For more info on this please refer to the Settings page in the WebUI and click on the Help in the top right." + echo " " + echo "Steps we will take:" + echo "1) Make sure you have ${BOLD}${REV}ONLY ONE${NORM} SDR plugged in at a time. Plugging in one SDR at a time will ensure they are not mixed up." + echo "2) Select which SDR we are setting the serial for." + echo "3) Add a PPM value. If you do not know or do not want to set this value this will be set to 0. " + echo "4) Write the serial to the SDR." + echo " " + echo "If you are ready to begin choose ${BOLD}Continue${NORM} to begin." + echo " Continuing will stop the STRATUX service to release the SDRs for setting the serials" + + choices=( 'Continue' 'Exit' ) + # Present the choices. + # The user chooses by entering the *number* before the desired choice. + select choice in "${choices[@]}"; do + + # If an invalid number was chosen, $choice will be empty. + # Report an error and prompt again. + [[ -n $choice ]] || { echo "Invalid choice." >&2; continue; } + + case $choice in + 'Continue') + STOPSTRATUX + MAINMENU + ;; + exit) + echo "Exiting. " + exit 0 + esac + break + done +} + +HEAD +START diff --git a/image/stratux-wifi.sh b/image/stratux-wifi.sh index 55e6a87a..b928afe5 100755 --- a/image/stratux-wifi.sh +++ b/image/stratux-wifi.sh @@ -1,29 +1,36 @@ #!/bin/bash - # Preliminaries. Kill off old services. /usr/bin/killall -9 hostapd hostapd-edimax /usr/sbin/service isc-dhcp-server stop +#Assume PI3 settings +DAEMON_CONF=/etc/hostapd/hostapd.conf +DAEMON_SBIN=/usr/sbin/hostapd + +#User settings for hostapd.conf and hostapd-edimax.conf +DAEMON_USER_PREF=/etc/hostapd/hostapd.user + +# Temporary hostapd.conf built by combining +# non-editable /etc/hostapd/hostapd.conf or hostapd-edimax.conf +# and the user configurable /etc/hostapd/hostapd.conf +DAEMON_TMP=/tmp/hostapd.conf # Detect RPi version. # Per http://elinux.org/RPi_HardwareHistory - -DAEMON_CONF=/etc/hostapd/hostapd.conf -DAEMON_SBIN=/usr/sbin/hostapd EW7811Un=$(lsusb | grep EW-7811Un) RPI_REV=`cat /proc/cpuinfo | grep 'Revision' | awk '{print $3}' | sed 's/^1000//'` if [ "$RPI_REV" = "a01041" ] || [ "$RPI_REV" = "a21041" ] || [ "$RPI_REV" = "900092" ] || [ "$RPI_REV" = "900093" ] && [ "$EW7811Un" != '' ]; then # This is a RPi2B or RPi0 with Edimax USB Wifi dongle. DAEMON_CONF=/etc/hostapd/hostapd-edimax.conf DAEMON_SBIN=/usr/sbin/hostapd-edimax -else - DAEMON_CONF=/etc/hostapd/hostapd.conf fi +#Make a new hostapd or hostapd-edimax conf file based on logic above +cat ${DAEMON_USER_PREF} ${DAEMON_CONF} > ${DAEMON_TMP} -${DAEMON_SBIN} -B ${DAEMON_CONF} +${DAEMON_SBIN} -B ${DAEMON_TMP} -sleep 5 +sleep 3 /usr/sbin/service isc-dhcp-server start diff --git a/image/stxAliases.txt b/image/stxAliases.txt index d97ccc2f..323e7e27 100644 --- a/image/stxAliases.txt +++ b/image/stxAliases.txt @@ -53,6 +53,7 @@ echo "kalChan1 Use the Chan from above to get ppm pf SDR1. Exam echo "setSerial0 Set the PPM error to SDR0. Value from kalChan0. Example: setSerial0 -45" echo "setSerial1 Set the PPM error to SDR1. Value from kalChan1. Example: setSerial1 23" echo "hostapd_manager.sh Sets the Change SSID, Channel, or Encryption for your Stratux" +echo "sdr-tool.sh Tool to walk you though setting your SDRs Serials" echo "raspi-config Open Raspberry Pi settings to expand filesystem" } diff --git a/linux-mpu9150 b/linux-mpu9150 deleted file mode 160000 index 56658ffe..00000000 --- a/linux-mpu9150 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 56658ffe83c23fde04fccc8e747bf9b7c1e184ea diff --git a/main/datalog.go b/main/datalog.go index eda48cd5..621400fa 100644 --- a/main/datalog.go +++ b/main/datalog.go @@ -474,6 +474,7 @@ func dataLog() { makeTable(msg{}, "messages", db) makeTable(esmsg{}, "es_messages", db) makeTable(Dump1090TermMessage{}, "dump1090_terminal", db) + makeTable(gpsPerfStats{}, "gps_attitude", db) makeTable(StratuxStartup{}, "startup", db) } @@ -567,6 +568,12 @@ func logESMsg(m esmsg) { } } +func logGPSAttitude(gpsPerf gpsPerfStats) { + if globalSettings.ReplayLog && isDataLogReady() { + dataLogChan <- DataLogRow{tbl: "gps_attitude", data: gpsPerf} + } +} + func logDump1090TermMessage(m Dump1090TermMessage) { if globalSettings.DEBUG && globalSettings.ReplayLog && isDataLogReady() { dataLogChan <- DataLogRow{tbl: "dump1090_terminal", data: m} diff --git a/main/equations.go b/main/equations.go index fecc9b04..edd86d97 100644 --- a/main/equations.go +++ b/main/equations.go @@ -249,12 +249,12 @@ func degreesHdg(angle float64) float64 { return angle * 180.0 / math.Pi } -// roundToInt cheaply rounds a float64 to an int, rather than truncating -func roundToInt(in float64) (out int) { +// roundToInt16 cheaply rounds a float64 to an int16, rather than truncating +func roundToInt16(in float64) (out int16) { if in >= 0 { - out = int(in + 0.5) + out = int16(in + 0.5) } else { - out = int(in - 0.5) + out = int16(in - 0.5) } return } diff --git a/main/gen_gdl90.go b/main/gen_gdl90.go index 9296cabd..c8e5fb16 100644 --- a/main/gen_gdl90.go +++ b/main/gen_gdl90.go @@ -35,7 +35,8 @@ import ( "github.com/ricochet2200/go-disk-usage/du" ) -// http://www.faa.gov/nextgen/programs/adsb/wsa/media/GDL90_Public_ICD_RevA.PDF +// https://www.faa.gov/nextgen/programs/adsb/Archival/ +// https://www.faa.gov/nextgen/programs/adsb/Archival/media/GDL90_Public_ICD_RevA.PDF var debugLogf string // Set according to OS config. var dataLogFilef string // Set according to OS config. @@ -75,6 +76,15 @@ const ( LON_LAT_RESOLUTION = float32(180.0 / 8388608.0) TRACK_RESOLUTION = float32(360.0 / 256.0) + + GPS_TYPE_NMEA = 0x01 + GPS_TYPE_UBX = 0x02 + GPS_TYPE_SIRF = 0x03 + GPS_TYPE_MEDIATEK = 0x04 + GPS_TYPE_FLARM = 0x05 + GPS_TYPE_GARMIN = 0x06 + // other GPS types to be defined as needed + ) var usage *du.DiskUsage @@ -100,8 +110,6 @@ type ReadCloser interface { io.Closer } -var developerMode bool - type msg struct { MessageClass uint TimeReceived time.Time @@ -163,8 +171,8 @@ func prepareMessage(data []byte) []byte { // Compute CRC before modifying the message. crc := crcCompute(data) // Add the two CRC16 bytes before replacing control characters. - data = append(data, byte(crc & 0xFF)) - data = append(data, byte((crc >> 8) & 0xFF)) + data = append(data, byte(crc&0xFF)) + data = append(data, byte(crc>>8)) tmp := []byte{0x7E} // Flag start. @@ -196,10 +204,18 @@ func makeLatLng(v float32) []byte { return ret } +func isDetectedOwnshipValid() bool { + return stratuxClock.Since(OwnshipTrafficInfo.Last_seen) < 10*time.Second +} + func makeOwnshipReport() bool { - if !isGPSValid() { + gpsValid := isGPSValid() + selfOwnshipValid := isDetectedOwnshipValid() + if !gpsValid && !selfOwnshipValid { return false } + curOwnship := OwnshipTrafficInfo + msg := make([]byte, 28) // See p.16. msg[0] = 0x0A // Message type "Ownship". @@ -218,15 +234,26 @@ func makeOwnshipReport() bool { msg[4] = code[2] // Mode S address. } - tmp := makeLatLng(mySituation.Lat) - msg[5] = tmp[0] // Latitude. - msg[6] = tmp[1] // Latitude. - msg[7] = tmp[2] // Latitude. - - tmp = makeLatLng(mySituation.Lng) - msg[8] = tmp[0] // Longitude. - msg[9] = tmp[1] // Longitude. - msg[10] = tmp[2] // Longitude. + var tmp []byte + if selfOwnshipValid { + tmp = makeLatLng(curOwnship.Lat) + msg[5] = tmp[0] // Latitude. + msg[6] = tmp[1] // Latitude. + msg[7] = tmp[2] // Latitude. + tmp = makeLatLng(curOwnship.Lng) + msg[8] = tmp[0] // Longitude. + msg[9] = tmp[1] // Longitude. + msg[10] = tmp[2] // Longitude. + } else { + tmp = makeLatLng(mySituation.Lat) + msg[5] = tmp[0] // Latitude. + msg[6] = tmp[1] // Latitude. + msg[7] = tmp[2] // Latitude. + tmp = makeLatLng(mySituation.Lng) + msg[8] = tmp[0] // Longitude. + msg[9] = tmp[1] // Longitude. + msg[10] = tmp[2] // Longitude. + } // This is **PRESSURE ALTITUDE** //FIXME: Temporarily removing "invalid altitude" when pressure altitude not available - using GPS altitude instead. @@ -235,25 +262,30 @@ func makeOwnshipReport() bool { var alt uint16 var altf float64 - if isTempPressValid() { + if selfOwnshipValid { + altf = float64(curOwnship.Alt) + } else if isTempPressValid() { altf = float64(mySituation.Pressure_alt) } else { altf = float64(mySituation.Alt) //FIXME: Pass GPS altitude if PA not available. **WORKAROUND FOR FF** } + altf = (altf + 1000) / 25 alt = uint16(altf) & 0xFFF // Should fit in 12 bits. msg[11] = byte((alt & 0xFF0) >> 4) // Altitude. msg[12] = byte((alt & 0x00F) << 4) - if isGPSGroundTrackValid() { + if selfOwnshipValid || isGPSGroundTrackValid() { msg[12] = msg[12] | 0x09 // "Airborne" + "True Track" } - msg[13] = byte(0x80 | (mySituation.NACp & 0x0F)) //Set NIC = 8 and use NACp from ry83xai.go. + msg[13] = byte(0x80 | (mySituation.NACp & 0x0F)) //Set NIC = 8 and use NACp from gps.go. gdSpeed := uint16(0) // 1kt resolution. - if isGPSGroundTrackValid() { + if selfOwnshipValid && curOwnship.Speed_valid { + gdSpeed = curOwnship.Speed + } else if isGPSGroundTrackValid() { gdSpeed = uint16(mySituation.GroundSpeed + 0.5) } @@ -269,7 +301,9 @@ func makeOwnshipReport() bool { // Track is degrees true, set from GPS true course. groundTrack := float32(0) - if isGPSGroundTrackValid() { + if selfOwnshipValid { + groundTrack = float32(curOwnship.Track) + } else if isGPSGroundTrackValid() { groundTrack = mySituation.TrueCourse } @@ -290,6 +324,17 @@ func makeOwnshipReport() bool { msg[18] = 0x01 // "Light (ICAO) < 15,500 lbs" + if selfOwnshipValid { + // Limit tail number to 7 characters. + tail := curOwnship.Tail + if len(tail) > 7 { + tail = tail[:7] + } + // Copy tail number into message. + for i := 0; i < len(tail); i++ { + msg[19+i] = tail[i] + } + } // Create callsign "Stratux". msg[19] = 0x53 msg[20] = 0x74 @@ -423,18 +468,14 @@ func makeStratuxStatus() []byte { } // Valid/Enabled: AHRS Enabled portion. - if globalSettings.AHRS_Enabled { - msg[12] = 1 << 0 - } + // msg[12] = 1 << 0 // Valid/Enabled: last bit unused. // Connected hardware: number of radios. msg[15] = msg[15] | (byte(globalStatus.Devices) & 0x3) - // Connected hardware: RY83XAI. - if globalStatus.RY83XAI_connected { - msg[15] = msg[15] | (1 << 2) - } + // Connected hardware. + // RY835AI: msg[15] = msg[15] | (1 << 2) // Number of GPS satellites locked. msg[16] = byte(globalStatus.GPS_satellites_locked) @@ -442,24 +483,12 @@ func makeStratuxStatus() []byte { // Number of satellites tracked msg[17] = byte(globalStatus.GPS_satellites_tracked) - // Summarize number of UAT and 1090ES traffic targets for reports that follow. - var uat_traffic_targets uint16 - var es_traffic_targets uint16 - for _, traf := range traffic { - switch traf.Last_source { - case TRAFFIC_SOURCE_1090ES: - es_traffic_targets++ - case TRAFFIC_SOURCE_UAT: - uat_traffic_targets++ - } - } - // Number of UAT traffic targets. - msg[18] = byte((uat_traffic_targets & 0xFF00) >> 8) - msg[19] = byte(uat_traffic_targets & 0xFF) + msg[18] = byte((globalStatus.UAT_traffic_targets_tracking & 0xFF00) >> 8) + msg[19] = byte(globalStatus.UAT_traffic_targets_tracking & 0xFF) // Number of 1090ES traffic targets. - msg[20] = byte((es_traffic_targets & 0xFF00) >> 8) - msg[21] = byte(es_traffic_targets & 0xFF) + msg[20] = byte((globalStatus.ES_traffic_targets_tracking & 0xFF00) >> 8) + msg[21] = byte(globalStatus.ES_traffic_targets_tracking & 0xFF) // Number of UAT messages per minute. msg[22] = byte((globalStatus.UAT_messages_last_minute & 0xFF00) >> 8) @@ -570,6 +599,9 @@ func heartBeatSender() { for { select { case <-timer.C: + // Turn on green ACT LED on the Pi. + ioutil.WriteFile("/sys/class/leds/led0/brightness", []byte("1\n"), 0644) + sendGDL90(makeHeartbeat(), false) sendGDL90(makeStratuxHeartbeat(), false) sendGDL90(makeStratuxStatus(), false) @@ -759,7 +791,7 @@ type WeatherMessage struct { } // Send update to connected websockets. -func registerADSBTextMessageReceived(msg string) { +func registerADSBTextMessageReceived(msg string, uatMsg *uatparse.UATMsg) { x := strings.Split(msg, " ") if len(x) < 5 { return @@ -767,16 +799,49 @@ func registerADSBTextMessageReceived(msg string) { var wm WeatherMessage + if (x[0] == "METAR") || (x[0] == "SPECI") { + globalStatus.UAT_METAR_total++ + } + if (x[0] == "TAF") || (x[0] == "TAF.AMD") { + globalStatus.UAT_TAF_total++ + } + if x[0] == "WINDS" { + globalStatus.UAT_TAF_total++ + } + if x[0] == "PIREP" { + globalStatus.UAT_PIREP_total++ + } wm.Type = x[0] wm.Location = x[1] wm.Time = x[2] wm.Data = strings.Join(x[3:], " ") wm.LocaltimeReceived = stratuxClock.Time - wmJSON, _ := json.Marshal(&wm) - // Send to weatherUpdate channel for any connected clients. - weatherUpdate.Send(wmJSON) + weatherUpdate.SendJSON(wm) +} + +func UpdateUATStats(ProductID uint32) { + switch ProductID { + case 0, 20: + globalStatus.UAT_METAR_total++ + case 1, 21: + globalStatus.UAT_TAF_total++ + case 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 81, 82, 83: + globalStatus.UAT_NEXRAD_total++ + // AIRMET and SIGMETS + case 2, 3, 4, 6, 11, 12, 22, 23, 24, 26, 254: + globalStatus.UAT_SIGMET_total++ + case 5, 25: + globalStatus.UAT_PIREP_total++ + case 8: + globalStatus.UAT_NOTAM_total++ + case 413: + // Do nothing in the case since text is recorded elsewhere + return + default: + globalStatus.UAT_OTHER_total++ + } } func parseInput(buf string) ([]byte, uint16) { @@ -865,11 +930,13 @@ func parseInput(buf string) ([]byte, uint16) { // Get all of the "product ids". for _, f := range uatMsg.Frames { thisMsg.Products = append(thisMsg.Products, f.Product_id) + UpdateUATStats(f.Product_id) + weatherRawUpdate.SendJSON(f) } // Get all of the text reports. textReports, _ := uatMsg.GetTextReports() for _, r := range textReports { - registerADSBTextMessageReceived(r) + registerADSBTextMessageReceived(r, uatMsg) } thisMsg.uatMsg = uatMsg } @@ -959,13 +1026,14 @@ type settings struct { Ping_Enabled bool GPS_Enabled bool NetworkOutputs []networkConnection - AHRS_Enabled bool + SerialOutputs map[string]serialConnection DisplayTrafficSource bool DEBUG bool ReplayLog bool PPM int OwnshipModeS string WatchList string + DeveloperMode bool } type status struct { @@ -979,6 +1047,8 @@ type status struct { UAT_messages_max uint ES_messages_last_minute uint ES_messages_max uint + UAT_traffic_targets_tracking uint16 + ES_traffic_targets_tracking uint16 Ping_connected bool GPS_satellites_locked uint16 GPS_satellites_seen uint16 @@ -986,7 +1056,7 @@ type status struct { GPS_position_accuracy float32 GPS_connected bool GPS_solution string - RY83XAI_connected bool + GPS_detected_type uint Uptime int64 UptimeClock time.Time CPUTemp float32 @@ -998,7 +1068,17 @@ type status struct { NetworkDataMessagesSentNonqueueableLastSec uint64 NetworkDataBytesSentLastSec uint64 NetworkDataBytesSentNonqueueableLastSec uint64 - Errors []string + UAT_METAR_total uint32 + UAT_TAF_total uint32 + UAT_NEXRAD_total uint32 + UAT_SIGMET_total uint32 + UAT_PIREP_total uint32 + UAT_NOTAM_total uint32 + UAT_OTHER_total uint32 + PressureSensorConnected bool + IMUConnected bool + + Errors []string } var globalSettings settings @@ -1013,11 +1093,11 @@ func defaultSettings() { {Conn: nil, Ip: "", Port: 4000, Capability: NETWORK_GDL90_STANDARD | NETWORK_AHRS_GDL90}, // {Conn: nil, Ip: "", Port: 49002, Capability: NETWORK_AHRS_FFSIM}, } - globalSettings.AHRS_Enabled = false globalSettings.DEBUG = false globalSettings.DisplayTrafficSource = false globalSettings.ReplayLog = false //TODO: 'true' for debug builds. globalSettings.OwnshipModeS = "F00000" + globalSettings.DeveloperMode = false } func readSettings() { @@ -1086,6 +1166,21 @@ func openReplay(fn string, compressed bool) (WriteCloser, error) { return ret, err } +/* + fsWriteTest(). + Makes a temporary file in 'dir', checks for error. Deletes the file. +*/ + +func fsWriteTest(dir string) error { + fn := dir + "/.write_test" + err := ioutil.WriteFile(fn, []byte("test\n"), 0644) + if err != nil { + return err + } + err = os.Remove(fn) + return err +} + func printStats() { statTimer := time.NewTicker(30 * time.Second) diskUsageWarning := false @@ -1194,6 +1289,8 @@ func gracefulShutdown() { //TODO: Any other graceful shutdown functions. + // Turn off green ACT LED on the Pi. + ioutil.WriteFile("/sys/class/leds/led0/brightness", []byte("0\n"), 0644) os.Exit(1) } @@ -1210,6 +1307,12 @@ func main() { stratuxClock = NewMonotonic() // Start our "stratux clock". + // Set up mySituation, do it here so logging JSON doesn't panic + mySituation.mu_GPS = &sync.Mutex{} + mySituation.mu_GPSPerf = &sync.Mutex{} + mySituation.mu_Attitude = &sync.Mutex{} + mySituation.mu_Pressure = &sync.Mutex{} + // Set up status. globalStatus.Version = stratuxVersion globalStatus.Build = stratuxBuild @@ -1250,7 +1353,6 @@ func main() { // replayESFilename := flag.String("eslog", "none", "ES Log filename") replayUATFilename := flag.String("uatlog", "none", "UAT Log filename") - develFlag := flag.Bool("developer", false, "Developer mode") replayFlag := flag.Bool("replay", false, "Replay file flag") replaySpeed := flag.Int("speed", 1, "Replay speed multiplier") stdinFlag := flag.Bool("uatin", false, "Process UAT messages piped to stdin") @@ -1260,11 +1362,6 @@ func main() { timeStarted = time.Now() runtime.GOMAXPROCS(runtime.NumCPU()) // redundant with Go v1.5+ compiler - if *develFlag == true { - log.Printf("Developer mode flag true!\n") - developerMode = true - } - // Duplicate log.* output to debugLog. fp, err := os.OpenFile(debugLogf, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { @@ -1299,10 +1396,27 @@ func main() { globalSettings.ReplayLog = true } + if globalSettings.DeveloperMode == true { + log.Printf("Developer mode set\n") + } + //FIXME: Only do this if data logging is enabled. initDataLog() - initRY83XAI() + // Start the AHRS sensor monitoring. + initI2CSensors() + + // Start the GPS external sensor monitoring. + initGPS() + + // Start appropriate AHRS calc, depending on whether or not we have an IMU connected + if globalStatus.IMUConnected { + log.Println("AHRS Info: IMU connected - starting sensorAttitudeSender") + go sensorAttitudeSender() + } else { + log.Println("AHRS Info: IMU not connected - starting gpsAttitudeSender") + go gpsAttitudeSender() + } // Start the heartbeat message loop in the background, once per second. go heartBeatSender() diff --git a/main/ry83Xai.go b/main/gps.go similarity index 64% rename from main/ry83Xai.go rename to main/gps.go index d40a3a02..c52e4952 100644 --- a/main/ry83Xai.go +++ b/main/gps.go @@ -4,15 +4,13 @@ that can be found in the LICENSE file, herein included as part of this header. - ry83Xai.go: GPS functions, GPS init, AHRS status messages, other external sensor monitoring. + gps.go: GPS functions, GPS init, AHRS status messages, other external sensor monitoring. */ package main import ( - "errors" "fmt" - "github.com/westphae/goflying/ahrs" "log" "math" "strconv" @@ -22,16 +20,10 @@ import ( "bufio" - "github.com/kidoman/embd" - _ "github.com/kidoman/embd/host/all" - // "github.com/kidoman/embd/sensor/bmp180" "github.com/tarm/serial" "os" "os/exec" - - "../mpu" - "github.com/westphae/goflying/ahrsweb" ) const ( @@ -41,10 +33,6 @@ const ( SAT_TYPE_GALILEO = 3 // GAxxx; NMEA IDs unknown SAT_TYPE_BEIDOU = 4 // GBxxx; NMEA IDs 201-235 SAT_TYPE_SBAS = 10 // NMEA IDs 33-54 - MPURETRYNUM = 5 // Number of times to retry connecting to MPU - DEG = math.Pi/180.0 // Conversion from degrees to radians - KTSPERFPS = 3600 / 6076.12 - VSIDECAYTIME = 5.0 // Decay time for measuring rate of climb in sec; slightly faster than typical VSI ) type SatelliteInfo struct { @@ -61,8 +49,8 @@ type SatelliteInfo struct { } type SituationData struct { - mu_GPS *sync.Mutex - + mu_GPS *sync.Mutex + mu_GPSPerf *sync.Mutex // From GPS. LastFixSinceMidnightUTC float32 Lat float32 @@ -80,38 +68,57 @@ type SituationData struct { GPSVertVel float32 // GPS vertical velocity, feet per second LastFixLocalTime time.Time TrueCourse float32 - GroundSpeed float32 + GPSTurnRate float64 // calculated GPS rate of turn, degrees per second + GroundSpeed float64 LastGroundTrackTime time.Time GPSTime time.Time LastGPSTimeTime time.Time // stratuxClock time since last GPS time received. LastValidNMEAMessageTime time.Time // time valid NMEA message last seen LastValidNMEAMessage string // last NMEA message processed. - mu_Attitude *sync.Mutex - - // From BMPX80 pressure sensor. + // From pressure sensor. + mu_Pressure *sync.Mutex Temp float64 Pressure_alt float64 RateOfClimb float64 LastTempPressTime time.Time - BMPExists bool - // From MPU6050 or MPU9250 accel/gyro. + // From AHRS source. + mu_Attitude *sync.Mutex Pitch float64 Roll float64 Gyro_heading float64 - Mag_heading float64 - SlipSkid float64 - RateOfTurn float64 - GLoad float64 - LastMagTime time.Time + Mag_heading float64 + SlipSkid float64 + RateOfTurn float64 + GLoad float64 LastAttitudeTime time.Time } +/* +myGPSPerfStats used to track short-term position / velocity trends, used to feed dynamic AHRS model. Use floats for better resolution of calculated data. +*/ +type gpsPerfStats struct { + stratuxTime uint64 // time since Stratux start, msec + nmeaTime float32 // timestamp from NMEA message + msgType string // NMEA message type + gsf float32 // knots + coursef float32 // true course [degrees] + alt float32 // gps altitude, ft msl + vv float32 // vertical velocity, ft/sec + gpsTurnRate float64 // calculated turn rate, deg/sec. Right turn is positive. + gpsPitch float64 // estimated pitch angle, deg. Calculated from gps ground speed and VV. Equal to flight path angle. + gpsRoll float64 // estimated roll angle from turn rate and groundspeed, deg. Assumes airplane in coordinated turns. + gpsLoadFactor float64 // estimated load factor from turn rate and groundspeed, "gee". Assumes airplane in coordinated turns. +} + +var gpsPerf gpsPerfStats +var myGPSPerfStats []gpsPerfStats + var serialConfig *serial.Config var serialPort *serial.Port -var readyToInitGPS bool // TO-DO: replace with channel control to terminate goroutine when complete +var readyToInitGPS bool //TODO: replace with channel control to terminate goroutine when complete var satelliteMutex *sync.Mutex var Satellites map[string]SatelliteInfo @@ -132,6 +139,13 @@ p.109 CFG-NAV5 (0x06 0x24) Poll Navigation Engine Settings */ +/* + chksumUBX() + returns the two-byte Fletcher algorithm checksum of byte array msg. + This is used in configuration messages for the u-blox GPS. See p. 97 of the + u-blox M8 Receiver Description. +*/ + func chksumUBX(msg []byte) []byte { ret := make([]byte, 2) for i := 0; i < len(msg); i++ { @@ -141,7 +155,12 @@ func chksumUBX(msg []byte) []byte { return ret } -// p.62 +/* + makeUBXCFG() + creates a UBX-formatted package consisting of two sync characters, + class, ID, payload length in bytes (2-byte little endian), payload, and checksum. + See p. 95 of the u-blox M8 Receiver Description. +*/ func makeUBXCFG(class, id byte, msglen uint16, msg []byte) []byte { ret := make([]byte, 6) ret[0] = 0xB5 @@ -169,6 +188,7 @@ func initGPSSerial() bool { var device string baudrate := int(9600) isSirfIV := bool(false) + globalStatus.GPS_detected_type = 0 // reset detected type on each initialization if _, err := os.Stat("/dev/ublox8"); err == nil { // u-blox 8 (RY83xAI over USB). device = "/dev/ublox8" @@ -191,54 +211,6 @@ func initGPSSerial() bool { log.Printf("Using %s for GPS\n", device) } - /* Developer option -- uncomment to allow "hot" configuration of GPS (assuming 38.4 kpbs on warm start) - serialConfig = &serial.Config{Name: device, Baud: 38400} - p, err := serial.OpenPort(serialConfig) - if err != nil { - log.Printf("serial port err: %s\n", err.Error()) - return false - } else { // reset port to 9600 baud for configuration - cfg1 := make([]byte, 20) - cfg1[0] = 0x01 // portID. - cfg1[1] = 0x00 // res0. - cfg1[2] = 0x00 // res1. - cfg1[3] = 0x00 // res1. - - // [ 7 ] [ 6 ] [ 5 ] [ 4 ] - // 0000 0000 0000 0000 1000 0000 1100 0000 - // UART mode. 0 stop bits, no parity, 8 data bits. Little endian order. - cfg1[4] = 0xC0 - cfg1[5] = 0x08 - cfg1[6] = 0x00 - cfg1[7] = 0x00 - - // Baud rate. Little endian order. - bdrt1 := uint32(9600) - cfg1[11] = byte((bdrt1 >> 24) & 0xFF) - cfg1[10] = byte((bdrt1 >> 16) & 0xFF) - cfg1[9] = byte((bdrt1 >> 8) & 0xFF) - cfg1[8] = byte(bdrt1 & 0xFF) - - // inProtoMask. NMEA and UBX. Little endian. - cfg1[12] = 0x03 - cfg1[13] = 0x00 - - // outProtoMask. NMEA. Little endian. - cfg1[14] = 0x02 - cfg1[15] = 0x00 - - cfg1[16] = 0x00 // flags. - cfg1[17] = 0x00 // flags. - - cfg1[18] = 0x00 //pad. - cfg1[19] = 0x00 //pad. - - p.Write(makeUBXCFG(0x06, 0x00, 20, cfg1)) - p.Close() - } - - -- End developer option */ - // Open port at default baud for config. serialConfig = &serial.Config{Name: device, Baud: baudrate} p, err := serial.OpenPort(serialConfig) @@ -443,13 +415,369 @@ func validateNMEAChecksum(s string) (string, bool) { // changes while on the ground and "movement" is really only changes in GPS fix as it settles down. //TODO: Some more robust checking above current and last speed. //TODO: Dynamic adjust for gain based on groundspeed -//TODO westphae: Do I need to do anything here? func setTrueCourse(groundSpeed uint16, trueCourse float64) { - if myMPU != nil && globalStatus.RY83XAI_connected && globalSettings.AHRS_Enabled { - if mySituation.GroundSpeed >= 7 && groundSpeed >= 7 { - myMPU.ResetHeading(trueCourse, 0.10) + if mySituation.GroundSpeed >= 7 && groundSpeed >= 7 { + // This was previously used to filter small ground speed spikes caused by GPS position drift. + // It was passed to the previous AHRS heading calculator. Currently unused, maybe in the future it will be. + _ = trueCourse + _ = groundSpeed + } +} + +/* +calcGPSAttitude estimates turn rate, pitch, and roll based on recent GPS groundspeed, track, and altitude / vertical speed. + +Method uses stored performance statistics from myGPSPerfStats[]. Ideally, calculation is based on most recent 1.5 seconds of data, +assuming 10 Hz sampling frequency. Lower frequency sample rates will increase calculation window for smoother response, at the +cost of slightly increased lag. + +(c) 2016 AvSquirrel (https://github.com/AvSquirrel) . All rights reserved. +Distributable under the terms of the "BSD-New" License that can be found in +the LICENSE file, herein included as part of this header. +*/ + +func calcGPSAttitude() bool { + // check slice length. Return error if empty set or set zero values + mySituation.mu_GPSPerf.Lock() + defer mySituation.mu_GPSPerf.Unlock() + length := len(myGPSPerfStats) + index := length - 1 + + if length == 0 { + log.Printf("GPS attitude: No data received yet. Not calculating attitude.\n") + return false + } else if length == 1 { + //log.Printf("myGPSPerfStats has one data point. Setting statistics to zero.\n") + myGPSPerfStats[index].gpsTurnRate = 0 + myGPSPerfStats[index].gpsPitch = 0 + myGPSPerfStats[index].gpsRoll = 0 + return false + } + + // check if GPS data was put in the structure more than three seconds ago -- this shouldn't happen unless something is wrong. + if (stratuxClock.Milliseconds - myGPSPerfStats[index].stratuxTime) > 3000 { + myGPSPerfStats[index].gpsTurnRate = 0 + myGPSPerfStats[index].gpsPitch = 0 + myGPSPerfStats[index].gpsRoll = 0 + log.Printf("GPS attitude: GPS data is more than three seconds old. Setting attitude to zero.\n") + return false + } + + // check time interval between samples + t1 := myGPSPerfStats[index].nmeaTime + t0 := myGPSPerfStats[index-1].nmeaTime + dt := t1 - t0 + + // first time error case: index is more than three seconds ahead of index-1 + if dt > 3 { + log.Printf("GPS attitude: Can't calculate GPS attitude. Reference data is old. dt = %v\n", dt) + return false + } + + // second case: index is behind index-1. This could be result of day rollover. If time is within n seconds of UTC, + // we rebase to the previous day, and will re-rebase the entire slice forward to the current day once all values roll over. + //TODO: Validate by testing at 0000Z + if dt < 0 { + log.Printf("GPS attitude: Current GPS time (%.2f) is older than last GPS time (%.2f). Checking for 0000Z rollover.\n", t1, t0) + if myGPSPerfStats[index-1].nmeaTime > 86300 && myGPSPerfStats[index].nmeaTime < 100 { // be generous with the time window at rollover + myGPSPerfStats[index].nmeaTime += 86400 + } else { + // time decreased, but not due to a recent rollover. Something odd is going on. + log.Printf("GPS attitude: Time isn't near 0000Z. Unknown reason for offset. Can't calculate GPS attitude.\n") + return false + } + + // check time array to see if all timestamps are > 86401 seconds since midnight + var tempTime []float64 + tempTime = make([]float64, length, length) + for i := 0; i < length; i++ { + tempTime[i] = float64(myGPSPerfStats[i].nmeaTime) + } + minTime, _ := arrayMin(tempTime) + if minTime > 86401.0 { + log.Printf("GPS attitude: Rebasing GPS time since midnight to current day.\n") + for i := 0; i < length; i++ { + myGPSPerfStats[i].nmeaTime -= 86400 + } + } + + // Verify adjustment + dt = myGPSPerfStats[index].nmeaTime - myGPSPerfStats[index-1].nmeaTime + log.Printf("GPS attitude: New dt = %f\n", dt) + if dt > 3 { + log.Printf("GPS attitude: Can't calculate GPS attitude. Reference data is old. dt = %v\n", dt) + return false + } else if dt < 0 { + log.Printf("GPS attitude: Something went wrong rebasing the time.\n") + return false + } + + } + + // If all of the bounds checks pass, begin processing the GPS data. + + // local variables + var headingAvg, dh, v_x, v_z, a_c, omega, slope, intercept, dt_avg float64 + var tempHdg, tempHdgUnwrapped, tempHdgTime, tempSpeed, tempVV, tempSpeedTime, tempRegWeights []float64 // temporary arrays for regression calculation + var valid bool + var lengthHeading, lengthSpeed int + + center := float64(myGPSPerfStats[index].nmeaTime) // current time for calculating regression weights + halfwidth := float64(1.4) // width of regression evaluation window. Default of 1.4 seconds for 5 Hz sampling; can increase to 3.5 sec @ 1 Hz + + // frequency detection + tempSpeedTime = make([]float64, 0) + for i := 1; i < length; i++ { + dt = myGPSPerfStats[i].nmeaTime - myGPSPerfStats[i-1].nmeaTime + if dt > 0.05 { // avoid double counting messages with same / similar timestamps + tempSpeedTime = append(tempSpeedTime, float64(dt)) } } + //log.Printf("Delta time array is %v.\n",tempSpeedTime) + dt_avg, valid = mean(tempSpeedTime) + if valid && dt_avg > 0 { + if globalSettings.DEBUG { + log.Printf("GPS attitude: Average delta time is %.2f s (%.1f Hz)\n", dt_avg, 1/dt_avg) + } + halfwidth = 7 * dt_avg + } else { + if globalSettings.DEBUG { + log.Printf("GPS attitude: Couldn't determine sample rate\n") + } + halfwidth = 3.5 + } + + if halfwidth > 3.5 { + halfwidth = 3.5 // limit calculation window to 3.5 seconds of data for 1 Hz or slower samples + } + + if globalStatus.GPS_detected_type == GPS_TYPE_UBX { // UBX reports vertical speed, so we can just walk through all of the PUBX messages in order + // Speed and VV. Use all values in myGPSPerfStats; perform regression. + tempSpeedTime = make([]float64, length, length) // all are length of original slice + tempSpeed = make([]float64, length, length) + tempVV = make([]float64, length, length) + tempRegWeights = make([]float64, length, length) + + for i := 0; i < length; i++ { + tempSpeed[i] = float64(myGPSPerfStats[i].gsf) + tempVV[i] = float64(myGPSPerfStats[i].vv) + tempSpeedTime[i] = float64(myGPSPerfStats[i].nmeaTime) + tempRegWeights[i] = triCubeWeight(center, halfwidth, tempSpeedTime[i]) + } + + // Groundspeed regression estimate. + slope, intercept, valid = linRegWeighted(tempSpeedTime, tempSpeed, tempRegWeights) + if !valid { + log.Printf("GPS attitude: Error calculating speed regression from UBX position messages") + return false + } else { + v_x = (slope*float64(myGPSPerfStats[index].nmeaTime) + intercept) * 1.687810 // units are knots, converted to feet/sec + } + + // Vertical speed regression estimate. + slope, intercept, valid = linRegWeighted(tempSpeedTime, tempVV, tempRegWeights) + if !valid { + log.Printf("GPS attitude: Error calculating vertical speed regression from UBX position messages") + return false + } else { + v_z = (slope*float64(myGPSPerfStats[index].nmeaTime) + intercept) // units are feet per sec; no conversion needed + } + + } else { // If we need to parse standard NMEA messages, determine if it's RMC or GGA, then fill the temporary slices accordingly. Need to pull from multiple message types since GGA doesn't do course or speed; VTG / RMC don't do altitude, etc. Grrr. + + //v_x = float64(myGPSPerfStats[index].gsf * 1.687810) + //v_z = 0 + + // first, parse groundspeed from RMC messages. + tempSpeedTime = make([]float64, 0) + tempSpeed = make([]float64, 0) + tempRegWeights = make([]float64, 0) + + for i := 0; i < length; i++ { + if myGPSPerfStats[i].msgType == "GPRMC" || myGPSPerfStats[i].msgType == "GNRMC" { + tempSpeed = append(tempSpeed, float64(myGPSPerfStats[i].gsf)) + tempSpeedTime = append(tempSpeedTime, float64(myGPSPerfStats[i].nmeaTime)) + tempRegWeights = append(tempRegWeights, triCubeWeight(center, halfwidth, float64(myGPSPerfStats[i].nmeaTime))) + } + } + lengthSpeed = len(tempSpeed) + if lengthSpeed == 0 { + log.Printf("GPS Attitude: No groundspeed data could be parsed from NMEA RMC messages\n") + return false + } else if lengthSpeed == 1 { + v_x = tempSpeed[0] * 1.687810 + } else { + slope, intercept, valid = linRegWeighted(tempSpeedTime, tempSpeed, tempRegWeights) + if !valid { + log.Printf("GPS attitude: Error calculating speed regression from NMEA RMC position messages") + return false + } else { + v_x = (slope*float64(myGPSPerfStats[index].nmeaTime) + intercept) * 1.687810 // units are knots, converted to feet/sec + //log.Printf("Avg speed %f calculated from %d RMC messages\n", v_x, lengthSpeed) // DEBUG + } + } + + // next, calculate vertical velocity from GGA altitude data. + tempSpeedTime = make([]float64, 0) + tempVV = make([]float64, 0) + tempRegWeights = make([]float64, 0) + + for i := 0; i < length; i++ { + if myGPSPerfStats[i].msgType == "GPGGA" || myGPSPerfStats[i].msgType == "GNGGA" { + tempVV = append(tempVV, float64(myGPSPerfStats[i].alt)) + tempSpeedTime = append(tempSpeedTime, float64(myGPSPerfStats[i].nmeaTime)) + tempRegWeights = append(tempRegWeights, triCubeWeight(center, halfwidth, float64(myGPSPerfStats[i].nmeaTime))) + } + } + lengthSpeed = len(tempVV) + if lengthSpeed < 2 { + log.Printf("GPS Attitude: Not enough points to calculate vertical speed from NMEA GGA messages\n") + return false + } else { + slope, _, valid = linRegWeighted(tempSpeedTime, tempVV, tempRegWeights) + if !valid { + log.Printf("GPS attitude: Error calculating vertical speed regression from NMEA GGA messages") + return false + } else { + v_z = slope // units are feet/sec + //log.Printf("Avg VV %f calculated from %d GGA messages\n", v_z, lengthSpeed) // DEBUG + } + } + + } + + // If we're going too slow for processNMEALine() to give us valid heading data, there's no sense in trying to parse it. + // However, we need to return a valid level attitude so we don't get the "red X of death" on our AHRS display. + // This will also eliminate most of the nuisance error message from the turn rate calculation. + if v_x < 6 { // ~3.55 knots + + myGPSPerfStats[index].gpsPitch = 0 + myGPSPerfStats[index].gpsRoll = 0 + myGPSPerfStats[index].gpsTurnRate = 0 + myGPSPerfStats[index].gpsLoadFactor = 1.0 + mySituation.GPSTurnRate = 0 + + // Output format:GPSAtttiude,seconds,nmeaTime,msg_type,GS,Course,Alt,VV,filtered_GS,filtered_course,turn rate,filtered_vv,pitch, roll,load_factor + buf := fmt.Sprintf("GPSAttitude,%.1f,%.2f,%s,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f\n", float64(stratuxClock.Milliseconds)/1000, myGPSPerfStats[index].nmeaTime, myGPSPerfStats[index].msgType, myGPSPerfStats[index].gsf, myGPSPerfStats[index].coursef, myGPSPerfStats[index].alt, myGPSPerfStats[index].vv, v_x/1.687810, headingAvg, myGPSPerfStats[index].gpsTurnRate, v_z, myGPSPerfStats[index].gpsPitch, myGPSPerfStats[index].gpsRoll, myGPSPerfStats[index].gpsLoadFactor) + if globalSettings.DEBUG { + log.Printf("%s", buf) // FIXME. Send to sqlite log or other file? + } + logGPSAttitude(myGPSPerfStats[index]) + //replayLog(buf, MSGCLASS_AHRS) + + return true + } + + // Heading. Same method used for UBX and generic. + // First, walk through the PerfStats and parse only valid heading data. + //log.Printf("Raw heading data:") + for i := 0; i < length; i++ { + //log.Printf("%.1f,",myGPSPerfStats[i].coursef) + if myGPSPerfStats[i].coursef >= 0 { // negative values are used to flag invalid / unavailable course + tempHdg = append(tempHdg, float64(myGPSPerfStats[i].coursef)) + tempHdgTime = append(tempHdgTime, float64(myGPSPerfStats[i].nmeaTime)) + } + } + //log.Printf("\n") + //log.Printf("tempHdg: %v\n", tempHdg) + + // Next, unwrap the heading so we don't mess up the regression by fitting a line across the 0/360 deg discontinutiy + lengthHeading = len(tempHdg) + tempHdgUnwrapped = make([]float64, lengthHeading, lengthHeading) + tempRegWeights = make([]float64, lengthHeading, lengthHeading) + + if lengthHeading > 1 { + tempHdgUnwrapped[0] = tempHdg[0] + tempRegWeights[0] = triCubeWeight(center, halfwidth, tempHdgTime[0]) + for i := 1; i < lengthHeading; i++ { + tempRegWeights[i] = triCubeWeight(center, halfwidth, tempHdgTime[i]) + if math.Abs(tempHdg[i]-tempHdg[i-1]) < 180 { // case 1: if angle change is less than 180 degrees, use the same reference system + tempHdgUnwrapped[i] = tempHdgUnwrapped[i-1] + tempHdg[i] - tempHdg[i-1] + } else if tempHdg[i] > tempHdg[i-1] { // case 2: heading has wrapped around from NE to NW. Subtract 360 to keep consistent with previous data. + tempHdgUnwrapped[i] = tempHdgUnwrapped[i-1] + tempHdg[i] - tempHdg[i-1] - 360 + } else { // case 3: heading has wrapped around from NW to NE. Add 360 to keep consistent with previous data. + tempHdgUnwrapped[i] = tempHdgUnwrapped[i-1] + tempHdg[i] - tempHdg[i-1] + 360 + } + } + } else { // + if globalSettings.DEBUG { + log.Printf("GPS attitude: Can't calculate turn rate with less than two points.\n") + } + return false + } + + // Finally, calculate turn rate as the slope of the weighted linear regression of unwrapped heading. + slope, intercept, valid = linRegWeighted(tempHdgTime, tempHdgUnwrapped, tempRegWeights) + + if !valid { + log.Printf("GPS attitude: Regression error calculating turn rate") + return false + } else { + headingAvg = slope*float64(myGPSPerfStats[index].nmeaTime) + intercept + dh = slope // units are deg per sec; no conversion needed here + //log.Printf("Calculated heading and turn rate: %.3f degrees, %.3f deg/sec\n",headingAvg,dh) + } + + myGPSPerfStats[index].gpsTurnRate = dh + mySituation.GPSTurnRate = dh + + // pitch angle -- or to be more pedantic, glide / climb angle, since we're just looking a rise-over-run. + // roll angle, based on turn rate and ground speed. Only valid for coordinated flight. Differences between airspeed and groundspeed will trip this up. + if v_x > 20 { // reduce nuisance 'bounce' at low speeds. 20 ft/sec = 11.9 knots. + myGPSPerfStats[index].gpsPitch = math.Atan2(v_z, v_x) * 180.0 / math.Pi + + /* + Governing equations for roll calculations + + Physics tells us that + a_z = g (in steady-state flight -- climbing, descending, or level -- this is gravity. 9.81 m/s^2 or 32.2 ft/s^2) + a_c = v^2/r (centripetal acceleration) + + We don't know r. However, we do know the tangential velocity (v) and angular velocity (omega). Express omega in radians per unit time, and + + v = omega*r + + By substituting and rearranging terms: + + a_c = v^2 / (v / omega) + a_c = v*omega + + Free body diagram time! + + /| + a_r / | a_z + /__| + X a_c + \_________________ [For the purpose of this comment, " X" is an airplane in a 20 degree bank. Use your imagination, mkay?) + + Resultant acceleration a_r is what the wings feel; a_r/a_z = load factor. Anyway, trig out the bank angle: + + bank angle = atan(a_c/a_z) + = atan(v*omega/g) + + wing loading = sqrt(a_c^2 + a_z^2) / g + + */ + + g := 32.174 // ft-s^-2 + omega = radians(myGPSPerfStats[index].gpsTurnRate) // need radians/sec + a_c = v_x * omega + myGPSPerfStats[index].gpsRoll = math.Atan2(a_c, g) * 180 / math.Pi // output is degrees + myGPSPerfStats[index].gpsLoadFactor = math.Sqrt(a_c*a_c+g*g) / g + } else { + myGPSPerfStats[index].gpsPitch = 0 + myGPSPerfStats[index].gpsRoll = 0 + myGPSPerfStats[index].gpsLoadFactor = 1 + } + + // Output format:GPSAtttiude,seconds,nmeaTime,msg_type,GS,Course,Alt,VV,filtered_GS,filtered_course,turn rate,filtered_vv,pitch, roll,load_factor + buf := fmt.Sprintf("GPSAttitude,%.1f,%.2f,%s,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f,%0.3f\n", float64(stratuxClock.Milliseconds)/1000, myGPSPerfStats[index].nmeaTime, myGPSPerfStats[index].msgType, myGPSPerfStats[index].gsf, myGPSPerfStats[index].coursef, myGPSPerfStats[index].alt, myGPSPerfStats[index].vv, v_x/1.687810, headingAvg, myGPSPerfStats[index].gpsTurnRate, v_z, myGPSPerfStats[index].gpsPitch, myGPSPerfStats[index].gpsRoll, myGPSPerfStats[index].gpsLoadFactor) + if globalSettings.DEBUG { + log.Printf("%s", buf) // FIXME. Send to sqlite log or other file? + } + logGPSAttitude(myGPSPerfStats[index]) + //replayLog(buf, MSGCLASS_AHRS) + return true } func calculateNACp(accuracy float32) uint8 { @@ -472,6 +800,15 @@ func calculateNACp(accuracy float32) uint8 { return ret } +/* + registerSituationUpdate(). + Called whenever there is a change in mySituation. +*/ +func registerSituationUpdate() { + logSituation() + situationUpdate.SendJSON(mySituation) +} + /* processNMEALine parses NMEA-0183 formatted strings against several message types. @@ -488,11 +825,17 @@ func processNMEALine(l string) (sentenceUsed bool) { defer func() { if sentenceUsed || globalSettings.DEBUG { - logSituation() + registerSituationUpdate() } mySituation.mu_GPS.Unlock() }() + // Local variables for GPS attitude estimation + thisGpsPerf := gpsPerf // write to myGPSPerfStats at end of function IFF + thisGpsPerf.coursef = -999.9 // default value of -999.9 indicates invalid heading to regression calculation + thisGpsPerf.stratuxTime = stratuxClock.Milliseconds // used for gross indexing + updateGPSPerf := false // change to true when position or vector info is read + l_valid, validNMEAcs := validateNMEAChecksum(l) if !validNMEAcs { log.Printf("GPS error. Invalid NMEA string: %s\n", l_valid) // remove log message once validation complete @@ -509,6 +852,15 @@ func processNMEALine(l string) (sentenceUsed bool) { return false } + // set the global GPS type to UBX as soon as we see our first (valid length) + // PUBX,01 position message, even if we don't have a fix + if globalStatus.GPS_detected_type != GPS_TYPE_UBX { + globalStatus.GPS_detected_type = GPS_TYPE_UBX + log.Printf("GPS detected: u-blox NMEA position message seen.\n") + } + + thisGpsPerf.msgType = x[0] + x[1] + tmpSituation := mySituation // If we decide to not use the data in this message, then don't make incomplete changes in mySituation. // Do the accuracy / quality fields first to prevent invalid position etc. from being sent downstream @@ -557,6 +909,7 @@ func processNMEALine(l string) (sentenceUsed bool) { } tmpSituation.LastFixSinceMidnightUTC = float32(3600*hr+60*min) + float32(sec) + thisGpsPerf.nmeaTime = tmpSituation.LastFixSinceMidnightUTC // field 3-4 = lat if len(x[3]) < 10 { @@ -595,9 +948,20 @@ func processNMEALine(l string) (sentenceUsed bool) { if err1 != nil { return false } - alt := float32(hae*3.28084) - tmpSituation.GeoidSep // convert to feet and offset by geoid separation - tmpSituation.HeightAboveEllipsoid = float32(hae * 3.28084) // feet - tmpSituation.Alt = alt + + // the next 'if' statement is a workaround for a ubx7 firmware bug: + // PUBX,00 reports HAE with a floor of zero (i.e. negative altitudes are set to zero). This causes GPS altitude to never read lower than -GeoidSep, + // placing minimum GPS altitude at about +80' MSL over most of North America. + // + // This does not affect GGA messages, so we can just rely on GGA to set altitude in these cases. It's a slower (1 Hz vs 5 Hz / 10 Hz), less precise + // (0.1 vs 0.001 mm resolution) report, but in practice the accuracy never gets anywhere near this resolution, so this should be an acceptable tradeoff + + if hae != 0 { + alt := float32(hae*3.28084) - tmpSituation.GeoidSep // convert to feet and offset by geoid separation + tmpSituation.HeightAboveEllipsoid = float32(hae * 3.28084) // feet + tmpSituation.Alt = alt + thisGpsPerf.alt = alt + } tmpSituation.LastFixLocalTime = stratuxClock.Time @@ -607,7 +971,8 @@ func processNMEALine(l string) (sentenceUsed bool) { return false } groundspeed = groundspeed * 0.540003 // convert to knots - tmpSituation.GroundSpeed = float32(groundspeed) + tmpSituation.GroundSpeed = groundspeed + thisGpsPerf.gsf = float32(groundspeed) // field 12 = track, deg trueCourse := float32(0.0) @@ -615,13 +980,15 @@ func processNMEALine(l string) (sentenceUsed bool) { if err != nil { return false } - if groundspeed > 3 { // TO-DO: use average groundspeed over last n seconds to avoid random "jumps" + if groundspeed > 3 { //TODO: use average groundspeed over last n seconds to avoid random "jumps" trueCourse = float32(tc) setTrueCourse(uint16(groundspeed), tc) tmpSituation.TrueCourse = trueCourse + thisGpsPerf.coursef = float32(tc) } else { + thisGpsPerf.coursef = -999.9 // regression will skip negative values // Negligible movement. Don't update course, but do use the slow speed. - // TO-DO: use average course over last n seconds? + //TODO: use average course over last n seconds? } tmpSituation.LastGroundTrackTime = stratuxClock.Time @@ -631,6 +998,7 @@ func processNMEALine(l string) (sentenceUsed bool) { return false } tmpSituation.GPSVertVel = float32(vv * -3.28084) // convert to ft/sec and positive = up + thisGpsPerf.vv = tmpSituation.GPSVertVel // field 14 = age of diff corrections @@ -643,6 +1011,19 @@ func processNMEALine(l string) (sentenceUsed bool) { // We've made it this far, so that means we've processed "everything" and can now make the change to mySituation. mySituation = tmpSituation + updateGPSPerf = true + if updateGPSPerf { + mySituation.mu_GPSPerf.Lock() + myGPSPerfStats = append(myGPSPerfStats, thisGpsPerf) + lenGPSPerfStats := len(myGPSPerfStats) + // log.Printf("GPSPerf array has %n elements. Contents are: %v\n",lenGPSPerfStats,myGPSPerfStats) + if lenGPSPerfStats > 299 { //30 seconds @ 10 Hz for UBX, 30 seconds @ 5 Hz for MTK or SIRF with 2x messages per 200 ms) + myGPSPerfStats = myGPSPerfStats[(lenGPSPerfStats - 299):] // remove the first n entries if more than 300 in the slice + } + + mySituation.mu_GPSPerf.Unlock() + } + return true } else if x[1] == "03" { // satellite status message. Only the first 20 satellites will be reported in this message for UBX firmware older than v3.0. Order seems to be GPS, then SBAS, then GLONASS. @@ -706,7 +1087,7 @@ func processNMEALine(l string) (sentenceUsed bool) { svType = SAT_TYPE_SBAS svStr = fmt.Sprintf("S%d", sv) sv -= 87 // subtract 87 to convert to NMEA from PRN. - } else { // TO-DO: Galileo + } else { //TODO: Galileo svType = SAT_TYPE_UNKNOWN svStr = fmt.Sprintf("U%d", sv) } @@ -793,9 +1174,7 @@ func processNMEALine(l string) (sentenceUsed bool) { log.Printf("GPS week # %v out of scope; not setting time and date\n", utcWeek) } return false - } /* else { - log.Printf("GPS week # %v valid; evaluate time and date\n", utcWeek) //debug option - } */ + } // field 2 is UTC time if len(x[2]) < 7 { @@ -818,6 +1197,7 @@ func processNMEALine(l string) (sentenceUsed bool) { // We only update ANY of the times if all of the time parsing is complete. mySituation.LastGPSTimeTime = stratuxClock.Time mySituation.GPSTime = gpsTime + stratuxClock.SetRealTimeReference(gpsTime) mySituation.LastFixSinceMidnightUTC = float32(3600*hr+60*min) + float32(sec) // log.Printf("GPS time is: %s\n", gpsTime) //debug if time.Since(gpsTime) > 3*time.Second || time.Since(gpsTime) < -3*time.Second { @@ -846,20 +1226,20 @@ func processNMEALine(l string) (sentenceUsed bool) { if err != nil { return false } - tmpSituation.GroundSpeed = float32(groundspeed) + tmpSituation.GroundSpeed = groundspeed trueCourse := float32(0) tc, err := strconv.ParseFloat(x[1], 32) if err != nil { return false } - if groundspeed > 3 { // TO-DO: use average groundspeed over last n seconds to avoid random "jumps" + if groundspeed > 3 { //TODO: use average groundspeed over last n seconds to avoid random "jumps" trueCourse = float32(tc) setTrueCourse(uint16(groundspeed), tc) tmpSituation.TrueCourse = trueCourse } else { // Negligible movement. Don't update course, but do use the slow speed. - // TO-DO: use average course over last n seconds? + //TODO: use average course over last n seconds? } tmpSituation.LastGroundTrackTime = stratuxClock.Time @@ -874,6 +1254,11 @@ func processNMEALine(l string) (sentenceUsed bool) { return false } + // use RMC / GGA message detection to sense "NMEA" type. + if globalStatus.GPS_detected_type == 0 { + globalStatus.GPS_detected_type = GPS_TYPE_NMEA + } + // Quality indicator. q, err1 := strconv.Atoi(x[6]) if err1 != nil { @@ -893,6 +1278,9 @@ func processNMEALine(l string) (sentenceUsed bool) { } tmpSituation.LastFixSinceMidnightUTC = float32(3600*hr+60*min) + float32(sec) + if globalStatus.GPS_detected_type != GPS_TYPE_UBX { + thisGpsPerf.nmeaTime = tmpSituation.LastFixSinceMidnightUTC + } // Latitude. if len(x[2]) < 4 { @@ -931,6 +1319,9 @@ func processNMEALine(l string) (sentenceUsed bool) { return false } tmpSituation.Alt = float32(alt * 3.28084) // Convert to feet. + if globalStatus.GPS_detected_type != GPS_TYPE_UBX { + thisGpsPerf.alt = float32(tmpSituation.Alt) + } // Geoid separation (Sep = HAE - MSL) // (needed for proper MSL offset on PUBX,00 altitudes) @@ -945,15 +1336,32 @@ func processNMEALine(l string) (sentenceUsed bool) { // Timestamp. tmpSituation.LastFixLocalTime = stratuxClock.Time + if globalStatus.GPS_detected_type != GPS_TYPE_UBX { + updateGPSPerf = true + thisGpsPerf.msgType = x[0] + } + // We've made it this far, so that means we've processed "everything" and can now make the change to mySituation. mySituation = tmpSituation + + if updateGPSPerf { + mySituation.mu_GPSPerf.Lock() + myGPSPerfStats = append(myGPSPerfStats, thisGpsPerf) + lenGPSPerfStats := len(myGPSPerfStats) + // log.Printf("GPSPerf array has %n elements. Contents are: %v\n",lenGPSPerfStats,myGPSPerfStats) + if lenGPSPerfStats > 299 { //30 seconds @ 10 Hz for UBX, 30 seconds @ 5 Hz for MTK or SIRF with 2x messages per 200 ms) + myGPSPerfStats = myGPSPerfStats[(lenGPSPerfStats - 299):] // remove the first n entries if more than 300 in the slice + } + mySituation.mu_GPSPerf.Unlock() + } + return true - } else if (x[0] == "GNRMC") || (x[0] == "GPRMC") { // Recommended Minimum data. FIXME: Is this needed anymore? + } else if (x[0] == "GNRMC") || (x[0] == "GPRMC") { // Recommended Minimum data. tmpSituation := mySituation // If we decide to not use the data in this message, then don't make incomplete changes in mySituation. //$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A - /* check RY83XAI man for NMEA version, if >2.2, add mode field + /* check RY835 man for NMEA version, if >2.2, add mode field Where: RMC Recommended Minimum sentence C 123519 Fix taken at 12:35:19 UTC @@ -971,6 +1379,11 @@ func processNMEALine(l string) (sentenceUsed bool) { return false } + // use RMC / GGA message detection to sense "NMEA" type. + if globalStatus.GPS_detected_type == 0 { + globalStatus.GPS_detected_type = GPS_TYPE_NMEA + } + if x[2] != "A" { // invalid fix tmpSituation.Quality = 0 // Just a note. return false @@ -987,14 +1400,18 @@ func processNMEALine(l string) (sentenceUsed bool) { return false } tmpSituation.LastFixSinceMidnightUTC = float32(3600*hr+60*min) + float32(sec) + if globalStatus.GPS_detected_type != GPS_TYPE_UBX { + thisGpsPerf.nmeaTime = tmpSituation.LastFixSinceMidnightUTC + } if len(x[9]) == 6 { // Date of Fix, i.e 191115 = 19 November 2015 UTC field 9 gpsTimeStr := fmt.Sprintf("%s %02d:%02d:%06.3f", x[9], hr, min, sec) gpsTime, err := time.Parse("020106 15:04:05.000", gpsTimeStr) - if err == nil { + if err == nil && gpsTime.After(time.Date(2016, time.January, 0, 0, 0, 0, 0, time.UTC)) { // Ignore dates before 2016-JAN-01. tmpSituation.LastGPSTimeTime = stratuxClock.Time tmpSituation.GPSTime = gpsTime + stratuxClock.SetRealTimeReference(gpsTime) if time.Since(gpsTime) > 3*time.Second || time.Since(gpsTime) < -3*time.Second { setStr := gpsTime.Format("20060102 15:04:05.000") + " UTC" log.Printf("setting system time to: '%s'\n", setStr) @@ -1041,27 +1458,51 @@ func processNMEALine(l string) (sentenceUsed bool) { if err != nil { return false } - tmpSituation.GroundSpeed = float32(groundspeed) + tmpSituation.GroundSpeed = groundspeed + if globalStatus.GPS_detected_type != GPS_TYPE_UBX { + thisGpsPerf.gsf = float32(groundspeed) + } // ground track "True" (field 8) trueCourse := float32(0) tc, err := strconv.ParseFloat(x[8], 32) - if err != nil { + if err != nil && groundspeed > 3 { // some receivers return null COG at low speeds. Need to ignore this condition. return false } - if groundspeed > 3 { // TO-DO: use average groundspeed over last n seconds to avoid random "jumps" + if groundspeed > 3 { //TODO: use average groundspeed over last n seconds to avoid random "jumps" trueCourse = float32(tc) setTrueCourse(uint16(groundspeed), tc) tmpSituation.TrueCourse = trueCourse + if globalStatus.GPS_detected_type != GPS_TYPE_UBX { + thisGpsPerf.coursef = float32(tc) + } } else { + if globalStatus.GPS_detected_type != GPS_TYPE_UBX { + thisGpsPerf.coursef = -999.9 + } // Negligible movement. Don't update course, but do use the slow speed. - // TO-DO: use average course over last n seconds? + //TODO: use average course over last n seconds? + } + if globalStatus.GPS_detected_type != GPS_TYPE_UBX { + updateGPSPerf = true + thisGpsPerf.msgType = x[0] } - tmpSituation.LastGroundTrackTime = stratuxClock.Time // We've made it this far, so that means we've processed "everything" and can now make the change to mySituation. mySituation = tmpSituation + + if updateGPSPerf { + mySituation.mu_GPSPerf.Lock() + myGPSPerfStats = append(myGPSPerfStats, thisGpsPerf) + lenGPSPerfStats := len(myGPSPerfStats) + // log.Printf("GPSPerf array has %n elements. Contents are: %v\n",lenGPSPerfStats,myGPSPerfStats) + if lenGPSPerfStats > 299 { //30 seconds @ 10 Hz for UBX, 30 seconds @ 5 Hz for MTK or SIRF with 2x messages per 200 ms) + myGPSPerfStats = myGPSPerfStats[(lenGPSPerfStats - 299):] // remove the first n entries if more than 300 in the slice + } + mySituation.mu_GPSPerf.Unlock() + } + setDataLogTimeWithGPS(mySituation) return true @@ -1113,7 +1554,7 @@ func processNMEALine(l string) (sentenceUsed bool) { svType = SAT_TYPE_GLONASS svStr = fmt.Sprintf("R%d", sv-64) // subtract 64 to convert from NMEA to PRN. svGLONASS = true - } else { // TO-DO: Galileo + } else { //TODO: Galileo svType = SAT_TYPE_UNKNOWN svStr = fmt.Sprintf("U%d", sv) } @@ -1151,7 +1592,6 @@ func processNMEALine(l string) (sentenceUsed bool) { tmpSituation.Satellites++ } } - //log.Printf("There are %d satellites in solution from this GSA message\n", sat) // TESTING - DEBUG // field 16: HDOP // Accuracy estimate @@ -1238,7 +1678,7 @@ func processNMEALine(l string) (sentenceUsed bool) { } else if sv < 97 { // GLONASS svType = SAT_TYPE_GLONASS svStr = fmt.Sprintf("R%d", sv-64) // subtract 64 to convert from NMEA to PRN. - } else { // TO-DO: Galileo + } else { //TODO: Galileo svType = SAT_TYPE_UNKNOWN svStr = fmt.Sprintf("U%d", sv) } @@ -1316,13 +1756,13 @@ func processNMEALine(l string) (sentenceUsed bool) { return true } - // if we've gotten this far, the message isn't one that we want to parse + // If we've gotten this far, the message isn't one that we can use. return false } func gpsSerialReader() { defer serialPort.Close() - readyToInitGPS = false // TO-DO: replace with channel control to terminate goroutine when complete + readyToInitGPS = false //TODO: replace with channel control to terminate goroutine when complete i := 0 //debug monitor scanner := bufio.NewScanner(serialPort) @@ -1348,115 +1788,10 @@ func gpsSerialReader() { log.Printf("Exiting gpsSerialReader() after i=%d loops\n", i) // debug monitor } globalStatus.GPS_connected = false - readyToInitGPS = true // TO-DO: replace with channel control to terminate goroutine when complete + readyToInitGPS = true //TODO: replace with channel control to terminate goroutine when complete return } -var i2cbus embd.I2CBus -var myBMPX80 mpu.BMP -var myMPU mpu.MPU - -func readBMP() (float64, float64, error) { // ÂșCelsius, feet - temp, err := myBMPX80.Temperature() - if err != nil { - return 0.0, 0.0, fmt.Errorf("AHRS Error: Couldn't read temp from BMP: %s", err) - } - press, err := myBMPX80.Pressure() - if err != nil { - return temp, 0.0, fmt.Errorf("AHRS Error: Couldn't read temp from BMP: %s", err) - } - altitude := CalcAltitude(press) - return temp, altitude, nil -} - -func initBMP() error { - bmp, err := mpu.NewBMP280(&i2cbus, 100 * time.Millisecond) - if err == nil { - myBMPX80 = bmp - mySituation.BMPExists = true - log.Println("AHRS: Successfully initialized BMP280") - return nil - } - - // TODO westphae: make bmp180 to fit bmp interface - //for i := 0; i < MPURETRYNUM; i++ { - // myBMPX80 = bmp180.New(i2cbus) - // _, err := myBMPX80.Temperature() // Test to see if it works, since bmp180.New doesn't return err - // if err != nil { - // time.Sleep(250 * time.Millisecond) - // } else { - // mySituation.BMPExists = true - // log.Println("AHRS: Successfully initialized BMP180") - // return nil - // } - //} - - mySituation.BMPExists = false - log.Println("AHRS Error: couldn't initialize BMP280 or BMP180") - return errors.New("AHRS Error: couldn't initialize BMP280 or BMP180") -} - -func initMPU() error { - var err error - - for i:=0; i < MPURETRYNUM; i++ { - myMPU, err = mpu.NewMPU9250() - if err != nil { - time.Sleep(100 * time.Millisecond) - } else { - log.Println("AHRS: Successfully initialized MPU9250") - time.Sleep(time.Second) - myMPU.Calibrate(1, 5) - return nil - } - } - - for i := 0; i < MPURETRYNUM; i++ { - myMPU, err = mpu.NewMPU6050() - if err != nil { - time.Sleep(100 * time.Millisecond) - } else { - log.Println("AHRS: Successfully initialized MPU6050") - return nil - } - } - - log.Println("AHRS Error: couldn't initialize MPU9250 or MPU6050") - return errors.New("AHRS Error: couldn't initialize MPU9250 or MPU6050") -} - -func initI2C() error { - i2cbus = embd.NewI2CBus(1) //TODO: error checking. - return nil -} - -// Unused at the moment. 5 second update, since read functions in bmp180 are slow. -// A bit slow for a useful rate of climb indication; BMP280 may be faster -func tempAndPressureReader() { - // Initialize some variables for rate of climb calc - _, altLast, _ := readBMP() // Set up some variables for rate of climb calc - dt := 5 - u := VSIDECAYTIME /(VSIDECAYTIME +float64(dt)) - timer := time.NewTicker(time.Duration(dt) * time.Second) - for globalStatus.RY83XAI_connected && globalSettings.AHRS_Enabled { - <-timer.C - // Read temperature and pressure altitude. - temp, alt, err := readBMP() - // Process. - if err != nil { - log.Printf("readBMP(): %s\n", err) - mySituation.BMPExists = false - } else { - mySituation.Temp = temp - mySituation.Pressure_alt = alt - // Assuming timer is reasonably accurate, use a regular ewma - mySituation.RateOfClimb = u*mySituation.RateOfClimb + (1-u)*(alt-altLast)/(float64(dt)/60) - mySituation.LastTempPressTime = stratuxClock.Time - altLast = alt - } - } -} - func makeFFAHRSSimReport() { s := fmt.Sprintf("XATTStratux,%f,%f,%f", mySituation.Gyro_heading, mySituation.Pitch, mySituation.Roll) @@ -1477,23 +1812,21 @@ func makeAHRSGDL90Report() { slip_skid := int16(0x7FFF) yaw_rate := int16(0x7FFF) g := int16(0x7FFF) - airspeed := int16(0x7FFF) // Can add this once we can read airspeed + airspeed := int16(0x7FFF) // Can add this once we can read airspeed palt := uint16(0xFFFF) vs := int16(0x7FFF) if isAHRSValid() { - pitch = int16(roundToInt(mySituation.Pitch * 10)) - roll = int16(roundToInt(mySituation.Roll * 10)) - hdg = int16(roundToInt(mySituation.Gyro_heading * 10)) - slip_skid = int16(roundToInt(mySituation.SlipSkid * 10)) - yaw_rate = int16(roundToInt(mySituation.RateOfTurn * 10)) - g = int16(roundToInt(mySituation.GLoad * 10)) - } - if isMagValid() { - hdg = int16(roundToInt(mySituation.Mag_heading * 10)) + pitch = roundToInt16(mySituation.Pitch * 10) + roll = roundToInt16(mySituation.Roll * 10) + hdg = roundToInt16(mySituation.Gyro_heading * 10) + slip_skid = roundToInt16(mySituation.SlipSkid * 10) + yaw_rate = roundToInt16(mySituation.RateOfTurn * 10) + g = roundToInt16(mySituation.GLoad * 10) + hdg = roundToInt16(mySituation.Mag_heading * 10) } if isTempPressValid() { - palt = uint16(roundToInt(mySituation.Pressure_alt + 5000)) - vs = int16(roundToInt(mySituation.RateOfClimb)) + palt = uint16(mySituation.Pressure_alt + 5000.5) + vs = roundToInt16(mySituation.RateOfClimb) } // Roll. @@ -1512,7 +1845,7 @@ func makeAHRSGDL90Report() { msg[10] = byte((slip_skid >> 8) & 0xFF) msg[11] = byte(slip_skid & 0xFF) - // Turn rate. + // Yaw rate. msg[12] = byte((yaw_rate >> 8) & 0xFF) msg[13] = byte(yaw_rate & 0xFF) @@ -1539,129 +1872,33 @@ func makeAHRSGDL90Report() { sendMsg(prepareMessage(msg), NETWORK_AHRS_GDL90, false) } -func attitudeReaderSender() { - var ( - roll, pitch, heading float64 - t time.Time - s ahrs.AHRSProvider - m *ahrs.Measurement - bx, by, bz, ax, ay, az, mx, my, mz float64 - mpuError, magError error - headingMag, slipSkid, turnRate, gLoad float64 - err_headingMag, err_slipSkid, err_turnRate, err_gLoad error - ) - m = ahrs.NewMeasurement() - - //TODO westphae: remove this logging when finished testing, or make it optional in settings - logger := ahrs.NewSensorLogger(fmt.Sprintf("/var/log/sensors_%s.csv", time.Now().Format("20060102_150405")), - "T", "TS", "A1", "A2", "A3", "H1", "H2", "H3", "M1", "M2", "M3", "TW", "W1", "W2", "W3", "TA", "Alt", - "pitch", "roll", "heading", "mag_heading", "slip_skid", "turn_rate", "g_load", "T_Attitude") - defer logger.Close() - - ahrswebListener, err := ahrsweb.NewKalmanListener() - if err != nil { - log.Printf("AHRS error starting ahrswebListener: %s\n", err.Error()) - } - - // Need a 10Hz sampling freq +func gpsAttitudeSender() { timer := time.NewTicker(100 * time.Millisecond) // ~10Hz update. - for globalStatus.RY83XAI_connected && globalSettings.AHRS_Enabled { + for { <-timer.C - t = stratuxClock.Time - m.T = float64(t.UnixNano()/1000)/1e6 + myGPSPerfStats = make([]gpsPerfStats, 0) // reinitialize statistics on disconnect / reconnect + for globalSettings.GPS_Enabled && globalStatus.GPS_connected { + <-timer.C - // Take sensor readings - m.UValid = false //TODO westphae: set m.U1, m.U2, m.U3 here once we have an airspeed sensor - - _, bx, by, bz, ax, ay, az, mx, my, mz, mpuError, magError = myMPU.ReadRaw() - //m.B1, m.B2, m.B3 = +by, -bx, +bz // This is how the RY83XAI is wired up - //m.A1, m.A2, m.A3 = -ay, +ax, -az // This is how the RY83XAI is wired up - m.B1, m.B2, m.B3 = -bx, +by, -bz // This is how the OpenFlightBox board is wired up - m.A1, m.A2, m.A3 = -ay, +ax, +az // This is how the OpenFlightBox board is wired up - m.M1, m.M2, m.M3 = +mx, +my, +mz - m.SValid = mpuError == nil - m.MValid = magError == nil - if mpuError != nil { - log.Printf("AHRS Gyro/Accel Error, not using for this run: %s\n", mpuError.Error()) - } - if magError != nil { - log.Printf("AHRS Magnetometer Error, not using for this run: %s\n", magError.Error()) - } - m.MValid = false //TODO westphae: for now - - m.WValid = t.Sub(mySituation.LastGroundTrackTime) < 500 * time.Millisecond - if m.WValid { - m.W1 = float64(mySituation.GroundSpeed) * math.Sin(float64(mySituation.TrueCourse) * DEG) - m.W2 = float64(mySituation.GroundSpeed) * math.Cos(float64(mySituation.TrueCourse) * DEG) - m.W3 = float64(mySituation.GPSVertVel * KTSPERFPS) //TODO westphae: Use BMP here instead of GPS - } - - // Run the AHRS calcs - if s == nil { // s is nil if we should (re-)initialize the Kalman state - s = ahrs.InitializeSimple(m, "") - } - s.Compute(m) - - // Debugging server: - if ahrswebListener != nil { - ahrswebListener.Send(s.GetState(), m) - } - - // If we have valid AHRS info, then send - if s.Valid() { - mySituation.mu_Attitude.Lock() - - roll, pitch, heading = s.CalcRollPitchHeading() - mySituation.Roll = roll / ahrs.Deg - mySituation.Pitch = pitch / ahrs.Deg - mySituation.Gyro_heading = heading / ahrs.Deg - - if headingMag, err_headingMag = myMPU.MagHeading(); err_headingMag != nil { - log.Printf("AHRS MPU Error: %s\n", err_headingMag.Error()) + if mySituation.Quality == 0 || !calcGPSAttitude() { + if globalSettings.DEBUG { + log.Printf("Couldn't calculate GPS-based attitude statistics\n") + } } else { - mySituation.Mag_heading = headingMag - mySituation.LastMagTime = t + mySituation.mu_GPSPerf.Lock() + index := len(myGPSPerfStats) - 1 + if index > 1 { + mySituation.Pitch = myGPSPerfStats[index].gpsPitch + mySituation.Roll = myGPSPerfStats[index].gpsRoll + mySituation.Gyro_heading = float64(mySituation.TrueCourse) + mySituation.LastAttitudeTime = stratuxClock.Time + + makeAHRSGDL90Report() + } + mySituation.mu_GPSPerf.Unlock() } - - if slipSkid, err_slipSkid = myMPU.SlipSkid(); err_slipSkid != nil { - log.Printf("AHRS MPU Error: %s\n", err_slipSkid.Error()) - } else { - mySituation.SlipSkid = slipSkid - } - - if turnRate, err_turnRate = myMPU.RateOfTurn(); err_turnRate != nil { - log.Printf("AHRS MPU Error: %s\n", err_turnRate.Error()) - } else { - mySituation.RateOfTurn = turnRate - } - - if gLoad, err_gLoad = myMPU.GLoad(); err_gLoad != nil { - log.Printf("AHRS MPU Error: %s\n", err_gLoad.Error()) - } else { - mySituation.GLoad = gLoad - } - - mySituation.LastAttitudeTime = t - - // makeFFAHRSSimReport() // simultaneous use of GDL90 and FFSIM not supported in FF 7.5.1 or later. Function definition will be kept for AHRS debugging and future workarounds. - mySituation.mu_Attitude.Unlock() - } else { - s = nil - mySituation.LastAttitudeTime = time.Time{} } - - makeAHRSGDL90Report() // Send whether or not valid - the function will invalidate the values as appropriate - - logger.Log( - float64(t.UnixNano() / 1000) / 1e6, - m.T, m.A1, m.A2, m.A3, m.B1, m.B2, m.B3, m.M1, m.M2, m.M3, - float64(mySituation.LastGroundTrackTime.UnixNano() / 1000) / 1e6, m.W1, m.W2, m.W3, - float64(mySituation.LastTempPressTime.UnixNano() / 1000) / 1e6, mySituation.Pressure_alt, - pitch / ahrs.Deg, roll / ahrs.Deg, heading / ahrs.Deg, headingMag, slipSkid, turnRate, gLoad, - float64(mySituation.LastAttitudeTime.UnixNano() / 1000) / 1e6) } - globalStatus.RY83XAI_connected = false - ahrswebListener.Close() } /* @@ -1690,8 +1927,7 @@ func updateConstellation() { // do any other calculations needed for this satellite } } - //log.Printf("Satellite counts: %d tracking channels, %d with >0 dB-Hz signal\n", tracked, seen) // DEBUG - REMOVE - //log.Printf("Satellite struct: %v\n", Satellites) // DEBUG - REMOVE + mySituation.Satellites = uint16(sats) mySituation.SatellitesTracked = uint16(tracked) mySituation.SatellitesSeen = uint16(seen) @@ -1735,60 +1971,24 @@ func isTempPressValid() bool { return stratuxClock.Since(mySituation.LastTempPressTime) < 15*time.Second } -func isMagValid() bool { - return stratuxClock.Since(mySituation.LastMagTime) < 1*time.Second -} - -func initAHRS() error { - if err := initI2C(); err != nil { // I2C bus. - log.Println("AHRS Error: Couldn't initialize i2c bus") - return err - } - if err := initBMP(); err != nil { // I2C temperature and pressure altitude. - log.Println("AHRS Warning: No BMPX80") - } - if err := initMPU(); err != nil { // I2C accel/gyro. - log.Println("AHRS Error: Couldn't init MPU, closing i2c bus") - i2cbus.Close() - myBMPX80.Close() - return err - } - globalStatus.RY83XAI_connected = true - time.Sleep(100 * time.Millisecond) - go attitudeReaderSender() - go tempAndPressureReader() - - return nil -} - -func pollRY83XAI() { +func pollGPS() { readyToInitGPS = true //TODO: Implement more robust method (channel control) to kill zombie serial readers timer := time.NewTicker(4 * time.Second) for { <-timer.C // GPS enabled, was not connected previously? - if globalSettings.GPS_Enabled && !globalStatus.GPS_connected && readyToInitGPS { //TO-DO: Implement more robust method (channel control) to kill zombie serial readers + if globalSettings.GPS_Enabled && !globalStatus.GPS_connected && readyToInitGPS { //TODO: Implement more robust method (channel control) to kill zombie serial readers globalStatus.GPS_connected = initGPSSerial() if globalStatus.GPS_connected { go gpsSerialReader() } } - // RY83XAI I2C enabled, was not connected previously? - if globalSettings.AHRS_Enabled && !globalStatus.RY83XAI_connected { - err := initAHRS() - if err != nil { - log.Printf("initAHRS(): %s\ndisabling AHRS sensors.\n", err.Error()) - globalStatus.RY83XAI_connected = false - } - } } } -func initRY83XAI() { - mySituation.mu_GPS = &sync.Mutex{} - mySituation.mu_Attitude = &sync.Mutex{} +func initGPS() { satelliteMutex = &sync.Mutex{} Satellites = make(map[string]SatelliteInfo) - go pollRY83XAI() + go pollGPS() } diff --git a/main/managementinterface.go b/main/managementinterface.go index b0b654cf..fc03104b 100644 --- a/main/managementinterface.go +++ b/main/managementinterface.go @@ -35,6 +35,31 @@ type SettingMessage struct { // Weather updates channel. var weatherUpdate *uibroadcaster var trafficUpdate *uibroadcaster +var gdl90Update *uibroadcaster + +func handleGDL90WS(conn *websocket.Conn) { + // Subscribe the socket to receive updates. + gdl90Update.AddSocket(conn) + + // Connection closes when function returns. Since uibroadcast is writing and we don't need to read anything (for now), just keep it busy. + for { + buf := make([]byte, 1024) + _, err := conn.Read(buf) + if err != nil { + break + } + if buf[0] != 0 { // Dummy. + continue + } + time.Sleep(1 * time.Second) + } +} + +// Situation updates channel. +var situationUpdate *uibroadcaster + +// Raw weather (UATFrame packet stream) update channel. +var weatherRawUpdate *uibroadcaster /* The /weather websocket starts off by sending the current buffer of weather messages, then sends updates as they are received. @@ -57,6 +82,36 @@ func handleWeatherWS(conn *websocket.Conn) { } } +func handleJsonIo(conn *websocket.Conn) { + trafficMutex.Lock() + for _, traf := range traffic { + if !traf.Position_valid { // Don't send unless a valid position exists. + continue + } + trafficJSON, _ := json.Marshal(&traf) + conn.Write(trafficJSON) + } + // Subscribe the socket to receive updates. + trafficUpdate.AddSocket(conn) + weatherRawUpdate.AddSocket(conn) + situationUpdate.AddSocket(conn) + + trafficMutex.Unlock() + + // Connection closes when function returns. Since uibroadcast is writing and we don't need to read anything (for now), just keep it busy. + for { + buf := make([]byte, 1024) + _, err := conn.Read(buf) + if err != nil { + break + } + if buf[0] != 0 { // Dummy. + continue + } + time.Sleep(1 * time.Second) + } +} + // Works just as weather updates do. func handleTrafficWS(conn *websocket.Conn) { @@ -104,7 +159,6 @@ func handleStatusWS(conn *websocket.Conn) { */ // Send status. - <-timer.C update, _ := json.Marshal(&globalStatus) _, err := conn.Write(update) @@ -112,19 +166,20 @@ func handleStatusWS(conn *websocket.Conn) { // log.Printf("Web client disconnected.\n") break } + <-timer.C } } func handleSituationWS(conn *websocket.Conn) { timer := time.NewTicker(100 * time.Millisecond) for { - <-timer.C situationJSON, _ := json.Marshal(&mySituation) _, err := conn.Write(situationJSON) if err != nil { break } + <-timer.C } @@ -219,8 +274,6 @@ func handleSettingsSetRequest(w http.ResponseWriter, r *http.Request) { globalSettings.Ping_Enabled = val.(bool) case "GPS_Enabled": globalSettings.GPS_Enabled = val.(bool) - case "AHRS_Enabled": - globalSettings.AHRS_Enabled = val.(bool) case "DEBUG": globalSettings.DEBUG = val.(bool) case "DisplayTrafficSource": @@ -232,6 +285,22 @@ func handleSettingsSetRequest(w http.ResponseWriter, r *http.Request) { } case "PPM": globalSettings.PPM = int(val.(float64)) + case "Baud": + if serialOut, ok := globalSettings.SerialOutputs["/dev/serialout0"]; ok { //FIXME: Only one device for now. + newBaud := int(val.(float64)) + if newBaud == serialOut.Baud { // Same baud rate. No change. + continue + } + log.Printf("changing /dev/serialout0 baud rate from %d to %d.\n", serialOut.Baud, newBaud) + serialOut.Baud = newBaud + // Close the port if it is open. + if serialOut.serialPort != nil { + log.Printf("closing /dev/serialout0 for baud rate change.\n") + serialOut.serialPort.Close() + serialOut.serialPort = nil + } + globalSettings.SerialOutputs["/dev/serialout0"] = serialOut + } case "WatchList": globalSettings.WatchList = val.(string) case "OwnshipModeS": @@ -274,6 +343,17 @@ func doReboot() { syscall.Reboot(syscall.LINUX_REBOOT_CMD_RESTART) } +func handleDevelModeToggle(w http.ResponseWriter, r *http.Request) { + log.Printf("handleDevelModeToggle called!!!\n") + globalSettings.DeveloperMode = true + saveSettings() +} + +func handleRestartRequest(w http.ResponseWriter, r *http.Request) { + log.Printf("handleRestartRequest called\n") + go doRestartApp() +} + func handleRebootRequest(w http.ResponseWriter, r *http.Request) { setNoCache(w) setJSONHeaders(w) @@ -282,6 +362,17 @@ func handleRebootRequest(w http.ResponseWriter, r *http.Request) { go delayReboot() } +func doRestartApp() { + time.Sleep(1) + syscall.Sync() + out, err := exec.Command("/bin/systemctl", "restart", "stratux").Output() + if err != nil { + log.Printf("restart error: %s\n%s", err.Error(), out) + } else { + log.Printf("restart: %s\n", out) + } +} + // AJAX call - /getClients. Responds with all connected clients. func handleClientsGetRequest(w http.ResponseWriter, r *http.Request) { setNoCache(w) @@ -307,7 +398,7 @@ func handleUpdatePostRequest(w http.ResponseWriter, r *http.Request) { } defer file.Close() // Special hardware builds. Don't allow an update unless the filename contains the hardware build name. - if (len(globalStatus.HardwareBuild) > 0) && !strings.Contains(handler.Filename, globalStatus.HardwareBuild) { + if (len(globalStatus.HardwareBuild) > 0) && !strings.Contains(strings.ToLower(handler.Filename), strings.ToLower(globalStatus.HardwareBuild)) { w.WriteHeader(404) return } @@ -444,11 +535,20 @@ func viewLogs(w http.ResponseWriter, r *http.Request) { func managementInterface() { weatherUpdate = NewUIBroadcaster() trafficUpdate = NewUIBroadcaster() + situationUpdate = NewUIBroadcaster() + weatherRawUpdate = NewUIBroadcaster() + gdl90Update = NewUIBroadcaster() http.HandleFunc("/", defaultServer) http.Handle("/logs/", http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log")))) http.HandleFunc("/view_logs/", viewLogs) + http.HandleFunc("/gdl90", + func(w http.ResponseWriter, req *http.Request) { + s := websocket.Server{ + Handler: websocket.Handler(handleGDL90WS)} + s.ServeHTTP(w, req) + }) http.HandleFunc("/status", func(w http.ResponseWriter, req *http.Request) { s := websocket.Server{ @@ -474,17 +574,26 @@ func managementInterface() { s.ServeHTTP(w, req) }) + http.HandleFunc("/jsonio", + func(w http.ResponseWriter, req *http.Request) { + s := websocket.Server{ + Handler: websocket.Handler(handleJsonIo)} + s.ServeHTTP(w, req) + }) + http.HandleFunc("/getStatus", handleStatusRequest) http.HandleFunc("/getSituation", handleSituationRequest) http.HandleFunc("/getTowers", handleTowersRequest) http.HandleFunc("/getSatellites", handleSatellitesRequest) http.HandleFunc("/getSettings", handleSettingsGetRequest) http.HandleFunc("/setSettings", handleSettingsSetRequest) + http.HandleFunc("/restart", handleRestartRequest) http.HandleFunc("/shutdown", handleShutdownRequest) http.HandleFunc("/reboot", handleRebootRequest) http.HandleFunc("/getClients", handleClientsGetRequest) http.HandleFunc("/updateUpload", handleUpdatePostRequest) http.HandleFunc("/roPartitionRebuild", handleroPartitionRebuild) + http.HandleFunc("/develmodetoggle", handleDevelModeToggle) err := http.ListenAndServe(managementAddr, nil) diff --git a/main/monotonic.go b/main/monotonic.go index c0e27330..c2aca2cc 100644 --- a/main/monotonic.go +++ b/main/monotonic.go @@ -21,6 +21,8 @@ type monotonic struct { Milliseconds uint64 Time time.Time ticker *time.Ticker + realTimeSet bool + RealTime time.Time } func (m *monotonic) Watcher() { @@ -28,6 +30,9 @@ func (m *monotonic) Watcher() { <-m.ticker.C m.Milliseconds += 10 m.Time = m.Time.Add(10 * time.Millisecond) + if m.realTimeSet { + m.RealTime = m.RealTime.Add(10 * time.Millisecond) + } } } @@ -43,6 +48,17 @@ func (m *monotonic) Unix() int64 { return int64(m.Since(time.Time{}).Seconds()) } +func (m *monotonic) HasRealTimeReference() bool { + return m.realTimeSet +} + +func (m *monotonic) SetRealTimeReference(t time.Time) { + if !m.realTimeSet { // Only allow the real clock to be set once. + m.RealTime = t + m.realTimeSet = true + } +} + func NewMonotonic() *monotonic { t := &monotonic{Milliseconds: 0, Time: time.Time{}, ticker: time.NewTicker(10 * time.Millisecond)} go t.Watcher() diff --git a/main/network.go b/main/network.go index 55af4b5e..b0c1683c 100644 --- a/main/network.go +++ b/main/network.go @@ -11,6 +11,8 @@ package main import ( "errors" + "fmt" + "github.com/tarm/serial" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "io/ioutil" @@ -50,6 +52,12 @@ type networkConnection struct { FFCrippled bool } +type serialConnection struct { + DeviceString string + Baud int + serialPort *serial.Port +} + var messageQueue chan networkMessage var outSockets map[string]networkConnection var dhcpLeases map[string]string @@ -64,10 +72,26 @@ const ( NETWORK_AHRS_FFSIM = 2 NETWORK_AHRS_GDL90 = 4 dhcp_lease_file = "/var/lib/dhcp/dhcpd.leases" + dhcp_lease_dir = "/var/lib/dhcp" + extra_hosts_file = "/etc/stratux-static-hosts.conf" ) +var dhcpLeaseFileWarning bool +var dhcpLeaseDirectoryLastTest time.Time // Last time fsWriteTest() was run on the DHCP lease directory. + // Read the "dhcpd.leases" file and parse out IP/hostname. func getDHCPLeases() (map[string]string, error) { + // Do a write test. Even if we are able to read the file, it may be out of date because there's a fs write issue. + // Only perform the test once every 5 minutes to minimize writes. + if !dhcpLeaseFileWarning && (stratuxClock.Since(dhcpLeaseDirectoryLastTest) >= 5*time.Minute) { + err := fsWriteTest(dhcp_lease_dir) + if err != nil { + err_p := fmt.Errorf("Write error on '%s', your EFB may have issues receiving weather and traffic.", dhcp_lease_dir) + addSystemError(err_p) + dhcpLeaseFileWarning = true + } + dhcpLeaseDirectoryLastTest = stratuxClock.Time + } dat, err := ioutil.ReadFile(dhcp_lease_file) ret := make(map[string]string) if err != nil { @@ -90,14 +114,33 @@ func getDHCPLeases() (map[string]string, error) { ret[block_ip] = "" } } + + // Added the ability to have static IP hosts stored in /etc/stratux-static-hosts.conf + + dat2, err := ioutil.ReadFile(extra_hosts_file) + if err != nil { + return ret, nil + } + + iplines := strings.Split(string(dat2), "\n") + block_ip2 := "" + for _, ipline := range iplines { + spacedip := strings.Split(ipline, " ") + if len(spacedip) == 2 { + // The ip is in block_ip2 + block_ip2 = spacedip[0] + // the hostname is here + ret[block_ip2] = spacedip[1] + } + } + return ret, nil } func isSleeping(k string) bool { ipAndPort := strings.Split(k, ":") - lastPing, ok := pingResponse[ipAndPort[0]] // No ping response. Assume disconnected/sleeping device. - if !ok || stratuxClock.Since(lastPing) > (10*time.Second) { + if lastPing, ok := pingResponse[ipAndPort[0]]; !ok || stratuxClock.Since(lastPing) > (10*time.Second) { return true } if stratuxClock.Since(outSockets[k].LastUnreachable) < (5 * time.Second) { @@ -113,6 +156,12 @@ func isThrottled(k string) bool { } func sendToAllConnectedClients(msg networkMessage) { + if (msg.msgType & NETWORK_GDL90_STANDARD) != 0 { + // It's a GDL90 message. Send to serial output channel (which may or may not cause something to happen). + serialOutputChan <- msg.msg + networkGDL90Chan <- msg.msg + } + netMutex.Lock() defer netMutex.Unlock() for k, netconn := range outSockets { @@ -135,11 +184,7 @@ func sendToAllConnectedClients(msg networkMessage) { if sleepFlag { continue } - _, err := netconn.Conn.Write(msg.msg) // Write immediately. - if err != nil { - //TODO: Maybe we should drop the client? Retry first? - log.Printf("GDL Message error: %s\n", err.Error()) - } + netconn.Conn.Write(msg.msg) // Write immediately. totalNetworkMessagesSent++ globalStatus.NetworkDataMessagesSent++ globalStatus.NetworkDataMessagesSentNonqueueable++ @@ -163,6 +208,73 @@ func sendToAllConnectedClients(msg networkMessage) { } } +var serialOutputChan chan []byte +var networkGDL90Chan chan []byte + +func networkOutWatcher() { + for { + ch := <-networkGDL90Chan + gdl90Update.SendJSON(ch) + } +} + +// Monitor serial output channel, send to serial port. +func serialOutWatcher() { + // Check every 30 seconds for a serial output device. + serialTicker := time.NewTicker(30 * time.Second) + + serialDev := "/dev/serialout0" //FIXME: This is temporary. Only one serial output device for now. + + for { + select { + case <-serialTicker.C: + if _, err := os.Stat(serialDev); !os.IsNotExist(err) { // Check if the device file exists. + var thisSerialConn serialConnection + // Check if we need to start handling a new device. + if val, ok := globalSettings.SerialOutputs[serialDev]; !ok { + newSerialOut := serialConnection{DeviceString: serialDev, Baud: 38400} + log.Printf("detected new serial output, setting up now: %s. Default baudrate 38400.\n", serialDev) + if globalSettings.SerialOutputs == nil { + globalSettings.SerialOutputs = make(map[string]serialConnection) + } + globalSettings.SerialOutputs[serialDev] = newSerialOut + saveSettings() + thisSerialConn = newSerialOut + } else { + thisSerialConn = val + } + // Check if we need to open the connection now. + if thisSerialConn.serialPort == nil { + cfg := &serial.Config{Name: thisSerialConn.DeviceString, Baud: thisSerialConn.Baud} + p, err := serial.OpenPort(cfg) + if err != nil { + log.Printf("serialout port (%s) err: %s\n", thisSerialConn.DeviceString, err.Error()) + break // We'll attempt again in 30 seconds. + } else { + log.Printf("opened serialout: Name: %s, Baud: %d\n", thisSerialConn.DeviceString, thisSerialConn.Baud) + } + // Save the serial port connection. + thisSerialConn.serialPort = p + globalSettings.SerialOutputs[serialDev] = thisSerialConn + } + } + + case b := <-serialOutputChan: + if val, ok := globalSettings.SerialOutputs[serialDev]; ok { + if val.serialPort != nil { + _, err := val.serialPort.Write(b) + if err != nil { // Encountered an error in writing to the serial port. Close it and set Serial_out_enabled. + log.Printf("serialout (%s) port err: %s. Closing port.\n", val.DeviceString, err.Error()) + val.serialPort.Close() + val.serialPort = nil + globalSettings.SerialOutputs[serialDev] = val + } + } + } + } + } +} + // Returns the number of DHCP leases and prints queue lengths. func getNetworkStats() { @@ -461,7 +573,7 @@ func networkStatsCounter() { /* ffMonitor(). Watches for "i-want-to-play-ffm-udp", "i-can-play-ffm-udp", and "i-cannot-play-ffm-udp" UDP messages broadcasted on - port 50113. Tags the client, issues a warning, and disables AHRS. + port 50113. Tags the client, issues a warning, and disables AHRS GDL90 output. */ @@ -500,8 +612,7 @@ func ffMonitor() { } if strings.HasPrefix(s, "i-want-to-play-ffm-udp") || strings.HasPrefix(s, "i-can-play-ffm-udp") || strings.HasPrefix(s, "i-cannot-play-ffm-udp") { p.FFCrippled = true - //FIXME: AHRS doesn't need to be disabled globally, just messages need to be filtered. - globalSettings.AHRS_Enabled = false + //FIXME: AHRS output doesn't need to be disabled globally, just on the ForeFlight client IPs. if !ff_warned { e := errors.New("Stratux is not supported by your EFB app. Your EFB app is known to regularly make changes that cause compatibility issues with Stratux. See the README for a list of apps that officially support Stratux.") addSystemError(e) @@ -515,6 +626,8 @@ func ffMonitor() { func initNetwork() { messageQueue = make(chan networkMessage, 1024) // Buffered channel, 1024 messages. + serialOutputChan = make(chan []byte, 1024) // Buffered channel, 1024 GDL90 messages. + networkGDL90Chan = make(chan []byte, 1024) outSockets = make(map[string]networkConnection) pingResponse = make(map[string]time.Time) netMutex = &sync.Mutex{} @@ -523,4 +636,6 @@ func initNetwork() { go messageQueueSender() go sleepMonitor() go networkStatsCounter() + go serialOutWatcher() + go networkOutWatcher() } diff --git a/main/ping.go b/main/ping.go index 2493a45e..b491b9d7 100644 --- a/main/ping.go +++ b/main/ping.go @@ -11,7 +11,7 @@ package main import ( "bufio" - "fmt" + //"fmt" "log" "os" "strings" @@ -148,8 +148,8 @@ func pingSerialReader() { s := scanner.Text() // Trimspace removes newlines as well as whitespace s = strings.TrimSpace(s) - logString := fmt.Sprintf("Ping received: %s", s) - log.Println(logString) + //logString := fmt.Sprintf("Ping received: %s", s) + //log.Println(logString) if s[0] == '*' { // 1090ES report // Ping appends a signal strength at the end of the message diff --git a/main/sensors.go b/main/sensors.go new file mode 100644 index 00000000..ab6de9fd --- /dev/null +++ b/main/sensors.go @@ -0,0 +1,278 @@ +package main + +import ( + "fmt" + "log" + "math" + "time" + + "../sensors" + + "github.com/kidoman/embd" + _ "github.com/kidoman/embd/host/all" + // "github.com/kidoman/embd/sensor/bmp180" + "github.com/westphae/goflying/ahrs" + "github.com/westphae/goflying/ahrsweb" +) + +var ( + i2cbus embd.I2CBus + myPressureReader sensors.PressureReader + myIMUReader sensors.IMUReader +) + +func initI2CSensors() { + i2cbus = embd.NewI2CBus(1) + + globalStatus.PressureSensorConnected = initPressureSensor() // I2C temperature and pressure altitude. + log.Printf("AHRS Info: pressure sensor connected: %t\n", globalStatus.PressureSensorConnected) + globalStatus.IMUConnected = initIMU() // I2C accel/gyro/mag. + log.Printf("AHRS Info: IMU connected: %t\n", globalStatus.IMUConnected) + + if !(globalStatus.PressureSensorConnected || globalStatus.IMUConnected) { + i2cbus.Close() + log.Println("AHRS Info: I2C bus closed") + } + + if globalStatus.PressureSensorConnected { + go tempAndPressureSender() + log.Println("AHRS Info: monitoring pressure sensor") + } +} + +func initPressureSensor() (ok bool) { + bmp, err := sensors.NewBMP280(&i2cbus, 100*time.Millisecond) + if err == nil { + myPressureReader = bmp + ok = true + log.Println("AHRS Info: Successfully initialized BMP280") + return + } + + // TODO westphae: make bmp180.go to fit bmp interface + //for i := 0; i < 5; i++ { + // myBMPX80 = bmp180.New(i2cbus) + // _, err := myBMPX80.Temperature() // Test to see if it works, since bmp180.New doesn't return err + // if err != nil { + // time.Sleep(250 * time.Millisecond) + // } else { + // globalStatus.PressureSensorConnected = true + // log.Println("AHRS Info: Successfully initialized BMP180") + // return nil + // } + //} + + log.Println("AHRS Info: couldn't initialize BMP280 or BMP180") + return +} + +func initIMU() (ok bool) { + var err error + + for i := 0; i < 5; i++ { + log.Printf("AHRS Info: attempt %d to connect to MPU9250\n", i) + myIMUReader, err = sensors.NewMPU9250() + if err != nil { + log.Printf("AHRS Info: attempt %d failed to connect to MPU9250\n", i) + time.Sleep(100 * time.Millisecond) + } else { + time.Sleep(time.Second) + log.Println("AHRS Info: Successfully connected MPU9250, running calibration") + myIMUReader.Calibrate(1, 5) + log.Println("AHRS Info: Successfully initialized MPU9250") + return true + } + } + + //for i := 0; i < 5; i++ { + // myIMUReader, err = sensors.NewMPU6050() + // if err != nil { + // log.Printf("AHRS Info: attempt %d failed to connect to MPU6050\n", i) + // time.Sleep(100 * time.Millisecond) + // } else { + // ok = true + // log.Println("AHRS Info: Successfully initialized MPU6050") + // return + // } + //} + + log.Println("AHRS Error: couldn't initialize MPU9250 or MPU6050") + return +} + +func tempAndPressureSender() { + var ( + temp float64 + press float64 + altLast float64 + altitude float64 + err error + dt float64 = 0.1 + ) + + // Initialize variables for rate of climb calc + u := 5 / (5 + float64(dt)) // Use 5 sec decay time for rate of climb, slightly faster than typical VSI + if press, err = myPressureReader.Pressure(); err != nil { + log.Printf("AHRS Error: Couldn't read temp from sensor: %s", err) + } + altLast = CalcAltitude(press) + + timer := time.NewTicker(time.Duration(1000*dt) * time.Millisecond) + for globalStatus.PressureSensorConnected { + <-timer.C + + // Read temperature and pressure altitude. + temp, err = myPressureReader.Temperature() + if err != nil { + log.Printf("AHRS Error: Couldn't read temperature from sensor: %s", err) + } + press, err = myPressureReader.Pressure() + if err != nil { + log.Printf("AHRS Error: Couldn't read pressure from sensor: %s", err) + continue + } + + // Update the Situation data. + mySituation.mu_Pressure.Lock() + mySituation.LastTempPressTime = stratuxClock.Time + mySituation.Temp = temp + altitude = CalcAltitude(press) + mySituation.Pressure_alt = altitude + // Assuming timer is reasonably accurate, use a regular ewma + mySituation.RateOfClimb = u*mySituation.RateOfClimb + (1-u)*(altitude-altLast)/(float64(dt)/60) + mySituation.mu_Pressure.Unlock() + altLast = altitude + } +} + +func sensorAttitudeSender() { + log.Println("AHRS Info: Setting up sensorAttitudeSender") + var ( + roll, pitch, heading float64 + t time.Time + s ahrs.AHRSProvider + m *ahrs.Measurement + bx, by, bz, ax, ay, az, mx, my, mz float64 + mpuError, magError error + headingMag, slipSkid, turnRate, gLoad float64 + errHeadingMag, errSlipSkid, errTurnRate, errGLoad error + ) + m = ahrs.NewMeasurement() + + //TODO westphae: remove this logging when finished testing, or make it optional in settings + logger := ahrs.NewSensorLogger(fmt.Sprintf("/var/log/sensors_%s.csv", time.Now().Format("20060102_150405")), + "T", "TS", "A1", "A2", "A3", "H1", "H2", "H3", "M1", "M2", "M3", "TW", "W1", "W2", "W3", "TA", "Alt", + "pitch", "roll", "heading", "mag_heading", "slip_skid", "turn_rate", "g_load", "T_Attitude") + defer logger.Close() + + ahrswebListener, err := ahrsweb.NewKalmanListener() + if err != nil { + log.Printf("AHRS Error: couldn't start ahrswebListener: %s\n", err.Error()) + } + + // Need a 10Hz sampling freq + timer := time.NewTicker(100 * time.Millisecond) // ~10Hz update. + for globalStatus.IMUConnected { + <-timer.C + t = stratuxClock.Time + m.T = float64(t.UnixNano()/1000) / 1e6 + + _, bx, by, bz, ax, ay, az, mx, my, mz, mpuError, magError = myIMUReader.ReadRaw() + //TODO westphae: allow user configuration of this mapping from a file, plus UI modification + //m.B1, m.B2, m.B3 = +by, -bx, +bz // This is how the RY83XAI is wired up + //m.A1, m.A2, m.A3 = -ay, +ax, -az // This is how the RY83XAI is wired up + m.B1, m.B2, m.B3 = -bx, +by, -bz // This is how the OpenFlightBox board is wired up + m.A1, m.A2, m.A3 = -ay, +ax, +az // This is how the OpenFlightBox board is wired up + m.M1, m.M2, m.M3 = +mx, +my, +mz + m.SValid = mpuError == nil + m.MValid = magError == nil + if mpuError != nil { + log.Printf("AHRS Gyro/Accel Error, not using for this run: %s\n", mpuError.Error()) + //TODO westphae: disconnect? + } + if magError != nil { + log.Printf("AHRS Magnetometer Error, not using for this run: %s\n", magError.Error()) + m.MValid = false + } + + m.WValid = t.Sub(mySituation.LastGroundTrackTime) < 500*time.Millisecond + if m.WValid { + m.W1 = mySituation.GroundSpeed * math.Sin(float64(mySituation.TrueCourse)*ahrs.Deg) + m.W2 = mySituation.GroundSpeed * math.Cos(float64(mySituation.TrueCourse)*ahrs.Deg) + if globalStatus.PressureSensorConnected { + m.W3 = mySituation.RateOfClimb * 3600 / 6076.12 + } else { + m.W3 = float64(mySituation.GPSVertVel) * 3600 / 6076.12 + } + } + + // Run the AHRS calcs + if s == nil { // s is nil if we should (re-)initialize the Kalman state + log.Println("AHRS Info: initializing new simple AHRS") + s = ahrs.InitializeSimple(m, "") + } + s.Compute(m) + + // Debugging server: + if ahrswebListener != nil { + ahrswebListener.Send(s.GetState(), m) + } + + // If we have valid AHRS info, then send + if s.Valid() { + mySituation.mu_Attitude.Lock() + + roll, pitch, heading = s.CalcRollPitchHeading() + mySituation.Roll = roll / ahrs.Deg + mySituation.Pitch = pitch / ahrs.Deg + mySituation.Gyro_heading = heading / ahrs.Deg + + if headingMag, errHeadingMag = myIMUReader.MagHeading(); errHeadingMag != nil { + log.Printf("AHRS MPU Error: %s\n", errHeadingMag.Error()) + } else { + mySituation.Mag_heading = headingMag + } + + if slipSkid, errSlipSkid = myIMUReader.SlipSkid(); errSlipSkid != nil { + log.Printf("AHRS MPU Error: %s\n", errSlipSkid.Error()) + } else { + mySituation.SlipSkid = slipSkid + } + + if turnRate, errTurnRate = myIMUReader.RateOfTurn(); errTurnRate != nil { + log.Printf("AHRS MPU Error: %s\n", errTurnRate.Error()) + } else { + mySituation.RateOfTurn = turnRate + } + + if gLoad, errGLoad = myIMUReader.GLoad(); errGLoad != nil { + log.Printf("AHRS MPU Error: %s\n", errGLoad.Error()) + } else { + mySituation.GLoad = gLoad + } + + mySituation.LastAttitudeTime = t + mySituation.mu_Attitude.Unlock() + + // makeFFAHRSSimReport() // simultaneous use of GDL90 and FFSIM not supported in FF 7.5.1 or later. Function definition will be kept for AHRS debugging and future workarounds. + } else { + s = nil + mySituation.LastAttitudeTime = time.Time{} + } + + makeAHRSGDL90Report() // Send whether or not valid - the function will invalidate the values as appropriate + + logger.Log( + float64(t.UnixNano()/1000)/1e6, + m.T, m.A1, m.A2, m.A3, m.B1, m.B2, m.B3, m.M1, m.M2, m.M3, + float64(mySituation.LastGroundTrackTime.UnixNano()/1000)/1e6, m.W1, m.W2, m.W3, + float64(mySituation.LastTempPressTime.UnixNano()/1000)/1e6, mySituation.Pressure_alt, + pitch/ahrs.Deg, roll/ahrs.Deg, heading/ahrs.Deg, headingMag, slipSkid, turnRate, gLoad, + float64(mySituation.LastAttitudeTime.UnixNano()/1000)/1e6) + } + log.Println("AHRS Info: Exited sensorAttitudeSender loop") + globalStatus.IMUConnected = false + ahrswebListener.Close() + myPressureReader.Close() + myIMUReader.Close() +} diff --git a/main/traffic.go b/main/traffic.go index 78e8f512..1f276396 100644 --- a/main/traffic.go +++ b/main/traffic.go @@ -84,7 +84,7 @@ type TrafficInfo struct { TargetType uint8 // types decribed in const above SignalLevel float64 // Signal level, dB RSSI. Squawk int // Squawk code - Position_valid bool // set when position report received. Unset after n seconds? (To-do) + Position_valid bool //TODO: set when position report received. Unset after n seconds? Lat float32 // decimal degrees, north positive Lng float32 // decimal degrees, east positive Alt int32 // Pressure altitude, feet @@ -109,7 +109,7 @@ type TrafficInfo struct { Last_GnssDiffAlt int32 // Altitude at last GnssDiffFromBaroAlt update. Last_speed time.Time // Time of last velocity and track update (stratuxClock). Last_source uint8 // Last frequency on which this target was received. - ExtrapolatedPosition bool // TO-DO: True if Stratux is "coasting" the target from last known position. + ExtrapolatedPosition bool //TODO: True if Stratux is "coasting" the target from last known position. Bearing float64 // Bearing in degrees true to traffic from ownship, if it can be calculated. Distance float64 // Distance to traffic from ownship, if it can be calculated. //FIXME: Some indicator that Bearing and Distance are valid, since they aren't always available. @@ -165,13 +165,26 @@ func sendTrafficUpdates() { trafficMutex.Lock() defer trafficMutex.Unlock() cleanupOldEntries() - var msg []byte + + // Summarize number of UAT and 1090ES traffic targets for reports that follow. + globalStatus.UAT_traffic_targets_tracking = 0 + globalStatus.ES_traffic_targets_tracking = 0 + for _, traf := range traffic { + switch traf.Last_source { + case TRAFFIC_SOURCE_1090ES: + globalStatus.ES_traffic_targets_tracking++ + case TRAFFIC_SOURCE_UAT: + globalStatus.UAT_traffic_targets_tracking++ + } + } + + msgs := make([][]byte, 1) if globalSettings.DEBUG && (stratuxClock.Time.Second()%15) == 0 { log.Printf("List of all aircraft being tracked:\n") log.Printf("==================================================================\n") } code, _ := strconv.ParseInt(globalSettings.OwnshipModeS, 16, 32) - for icao, ti := range traffic { // TO-DO: Limit number of aircraft in traffic message. ForeFlight 7.5 chokes at ~1000-2000 messages depending on iDevice RAM. Practical limit likely around ~500 aircraft without filtering. + for icao, ti := range traffic { // ForeFlight 7.5 chokes at ~1000-2000 messages depending on iDevice RAM. Practical limit likely around ~500 aircraft without filtering. if isGPSValid() { // func distRect(lat1, lon1, lat2, lon2 float64) (dist, bearing, distN, distE float64) { dist, bearing := distance(float64(mySituation.Lat), float64(mySituation.Lng), float64(ti.Lat), float64(ti.Lng)) @@ -194,23 +207,35 @@ func sendTrafficUpdates() { traffic[icao] = ti // write the updated ti back to the map //log.Printf("Traffic age of %X is %f seconds\n",icao,ti.Age) if ti.Age > 2 { // if nothing polls an inactive ti, it won't push to the webUI, and its Age won't update. - tiJSON, _ := json.Marshal(&ti) - trafficUpdate.Send(tiJSON) + trafficUpdate.SendJSON(ti) } - if ti.Position_valid && ti.Age < 6 { // ... but don't pass stale data to the EFB. TO-DO: Coast old traffic? Need to determine how FF, WingX, etc deal with stale targets. + if ti.Position_valid && ti.Age < 6 { // ... but don't pass stale data to the EFB. + //TODO: Coast old traffic? Need to determine how FF, WingX, etc deal with stale targets. logTraffic(ti) // only add to the SQLite log if it's not stale - if ti.Icao_addr == uint32(code) { // - log.Printf("Ownship target detected for code %X\n", code) // DEBUG - REMOVE + if ti.Icao_addr == uint32(code) { + if globalSettings.DEBUG { + log.Printf("Ownship target detected for code %X\n", code) + } OwnshipTrafficInfo = ti } else { - msg = append(msg, makeTrafficReportMsg(ti)...) + cur_n := len(msgs) - 1 + if len(msgs[cur_n]) >= 35 { + // Batch messages into packets with at most 35 traffic reports + // to keep each packet under 1KB. + cur_n++ + msgs = append(msgs, make([]byte, 0)) + } + msgs[cur_n] = append(msgs[cur_n], makeTrafficReportMsg(ti)...) } } } - if len(msg) > 0 { - sendGDL90(msg, false) + for i := 0; i < len(msgs); i++ { + msg := msgs[i] + if len(msg) > 0 { + sendGDL90(msg, false) + } } } @@ -222,8 +247,7 @@ func registerTrafficUpdate(ti TrafficInfo) { return } */ // Send all traffic to the websocket and let JS sort it out. This will provide user indication of why they see 1000 ES messages and no traffic. - tiJSON, _ := json.Marshal(&ti) - trafficUpdate.Send(tiJSON) + trafficUpdate.SendJSON(ti) } func makeTrafficReportMsg(ti TrafficInfo) []byte { @@ -677,7 +701,7 @@ func esListen() { } // generate human readable summary of message types for debug - // TO-DO: Use for ES message statistics? + //TODO: Use for ES message statistics? /* var s1 string if newTi.DF == 17 { @@ -1061,7 +1085,7 @@ func icao2reg(icao_addr uint32) (string, bool) { } else if (icao_addr >= 0xC00001) && (icao_addr <= 0xC3FFFF) { nation = "CA" } else { - // future national decoding is TO-DO + //TODO: future national decoding. return "NON-NA", false } diff --git a/main/uibroadcast.go b/main/uibroadcast.go index 500e2afa..b7d45ab1 100644 --- a/main/uibroadcast.go +++ b/main/uibroadcast.go @@ -11,6 +11,7 @@ package main import ( + "encoding/json" "golang.org/x/net/websocket" "sync" "time" @@ -36,6 +37,11 @@ func (u *uibroadcaster) Send(msg []byte) { u.messages <- msg } +func (u *uibroadcaster) SendJSON(i interface{}) { + j, _ := json.Marshal(&i) + u.Send(j) +} + func (u *uibroadcaster) AddSocket(sock *websocket.Conn) { u.sockets_mu.Lock() u.sockets = append(u.sockets, sock) diff --git a/mpu/bmp.go b/mpu/bmp.go deleted file mode 100644 index 3c235f59..00000000 --- a/mpu/bmp.go +++ /dev/null @@ -1,7 +0,0 @@ -package mpu - -type BMP interface { - Temperature() (float64, error) - Pressure() (float64, error) - Close() -} diff --git a/mpu/bmp280.go b/mpu/bmp280.go deleted file mode 100644 index a37ff96b..00000000 --- a/mpu/bmp280.go +++ /dev/null @@ -1,74 +0,0 @@ -package mpu - -import ( - "github.com/westphae/goflying/bmp280" - "github.com/kidoman/embd" - "log" - "time" - "errors" -) - -const ( - BMP280PowerMode = bmp280.NormalMode - BMP280Standby = bmp280.StandbyTime63ms - BMP280FilterCoeff = bmp280.FilterCoeff16 - BMP280TempRes = bmp280.Oversamp16x - BMP280PressRes = bmp280.Oversamp16x -) - -type BMP280 struct { - bmp *bmp280.BMP280 - bmpdata *bmp280.BMPData - running bool -} - -var bmperr = errors.New("BMP280 Error: BMP280 is not running") - -func NewBMP280(i2cbus *embd.I2CBus, freq time.Duration) (*BMP280, error) { - var ( - bmp *bmp280.BMP280 - errbmp error - ) - - bmp, errbmp = bmp280.NewBMP280(i2cbus, bmp280.Address1, - BMP280PowerMode, BMP280Standby, BMP280FilterCoeff, BMP280TempRes, BMP280PressRes) - if errbmp != nil { // Maybe the BMP280 isn't at Address1, try Address2 - bmp, errbmp = bmp280.NewBMP280(i2cbus, bmp280.Address2, - BMP280PowerMode, BMP280Standby, BMP280FilterCoeff, BMP280TempRes, BMP280PressRes) - } - if errbmp != nil { - log.Println("AHRS Error: couldn't initialize BMP280") - return nil, errbmp - } - - newbmp := BMP280{bmp: bmp, bmpdata: new(bmp280.BMPData)} - go newbmp.run() - - return &newbmp, nil -} - -func (bmp *BMP280) run() { - bmp.running = true - for bmp.running { - bmp.bmpdata = <-bmp.bmp.C - } -} - -func (bmp *BMP280) Temperature() (float64, error) { - if !bmp.running { - return 0, bmperr - } - return bmp.bmpdata.Temperature, nil -} - -func (bmp *BMP280) Pressure() (float64, error) { - if !bmp.running { - return 0, bmperr - } - return bmp.bmpdata.Pressure, nil -} - -func (bmp *BMP280) Close() { - bmp.running = false - bmp.bmp.Close() -} diff --git a/mpu/mpu.go b/mpu/mpu.go deleted file mode 100644 index 90c3dd6c..00000000 --- a/mpu/mpu.go +++ /dev/null @@ -1,12 +0,0 @@ -package mpu - -type MPU interface { - Close() - ResetHeading(float64, float64) - MagHeading() (float64, error) - SlipSkid() (float64, error) - RateOfTurn() (float64, error) - GLoad() (float64, error) - ReadRaw() (int64, float64, float64, float64, float64, float64, float64, float64, float64, float64, error, error) - Calibrate(int, int) error -} diff --git a/mpu/mpu6050.go b/mpu/mpu6050.go deleted file mode 100644 index 1da7152b..00000000 --- a/mpu/mpu6050.go +++ /dev/null @@ -1,197 +0,0 @@ -// Package mpu6050 allows interfacing with InvenSense mpu6050 barometric pressure sensor. This sensor -// has the ability to provided compensated temperature and pressure readings. -package mpu - -import ( - "../linux-mpu9150/mpu" - "log" - "math" - "time" - "errors" -) - -//https://www.olimex.com/Products/Modules/Sensors/MOD-MPU6050/resources/RM-MPU-60xxA_rev_4.pdf -const ( - pollDelay = 98 * time.Millisecond // ~10Hz -) - -// MPU6050 represents a InvenSense MPU6050 sensor. -type MPU6050 struct { - poll time.Duration - - started bool - - pitch float64 - roll float64 - - // Calibration variables. - calibrated bool - pitch_history []float64 - roll_history []float64 - pitch_resting float64 - roll_resting float64 - - // For tracking heading (mixing GPS track and the gyro output). - heading float64 // Current heading. - gps_track float64 // Last reading directly from the gyro for comparison with current heading. - gps_track_valid bool - heading_correction float64 - - quit chan struct{} -} - -// New returns a handle to a MPU6050 sensor. -func NewMPU6050() (*MPU6050, error) { - n := &MPU6050{poll: pollDelay} - if err := n.startUp(); err != nil { - return nil, err - } - return n, nil -} - -func (d *MPU6050) startUp() error { - mpu_sample_rate := 10 // 10 Hz read rate of hardware IMU - yaw_mix_factor := 0 // must be zero if no magnetometer - err := mpu.InitMPU(mpu_sample_rate, yaw_mix_factor) - if err != 0 { - return errors.New("MPU6050 Error: couldn't start MPU") - } - - d.pitch_history = make([]float64, 0) - d.roll_history = make([]float64, 0) - - d.started = true - d.run() - - return nil -} - -/* - -func (d *MPU6050) calibrate() { - //TODO: Error checking to make sure that the histories are extensive enough to be significant. - //TODO: Error checking to do continuous calibrations. - pitch_adjust := float64(0) - for _, v := range d.pitch_history { - pitch_adjust = pitch_adjust + v - } - pitch_adjust = pitch_adjust / float64(len(d.pitch_history)) - d.pitch_resting = pitch_adjust - - roll_adjust := float64(0) - for _, v := range d.roll_history { - roll_adjust = roll_adjust + v - } - roll_adjust = roll_adjust / float64(len(d.roll_history)) - d.roll_resting = roll_adjust - log.Printf("calibrate: pitch %f, roll %f\n", pitch_adjust, roll_adjust) - d.calibrated = true -} - -*/ - -func normalizeHeading(h float64) float64 { - for h < float64(0.0) { - h = h + float64(360.0) - } - for h >= float64(360.0) { - h = h - float64(360.0) - } - return h -} - -func (d *MPU6050) getMPUData() { - pr, rr, hr, err := mpu.ReadMPU() - - // Convert from radians to degrees. - pitch := float64(pr) * (float64(180.0) / math.Pi) - roll := float64(-rr) * (float64(180.0) / math.Pi) - heading := float64(hr) * (float64(180.0) / math.Pi) - if heading < float64(0.0) { - heading = float64(360.0) + heading - } - - if err == nil { - d.pitch = pitch - d.roll = roll - - // Heading is raw value off the IMU. Without mag compass fusion, need to apply correction bias. - // Amount of correction is set by ResetHeading() -- doesn't necessarily have to be based off GPS. - d.heading = normalizeHeading((heading - d.heading_correction)) - - } else { - // log.Printf("mpu6050.calculatePitchAndRoll(): mpu.ReadMPU() err: %s\n", err.Error()) - } -} - -func (d *MPU6050) run() { - time.Sleep(d.poll) - go func() { - d.quit = make(chan struct{}) - timer := time.NewTicker(d.poll) - // calibrateTimer := time.NewTicker(1 * time.Minute) - for { - select { - case <-timer.C: - d.getMPUData() - // case <-calibrateTimer.C: - // d.calibrate() - // calibrateTimer.Stop() - case <-d.quit: - mpu.CloseMPU() - return - } - } - }() - return -} - -// Set heading from a known value (usually GPS true heading). -func (d *MPU6050) ResetHeading(newHeading float64, gain float64) { - if gain < 0.001 { // sanitize our inputs! - gain = 0.001 - } else if gain > 1 { - gain = 1 - } - - old_hdg := d.heading // only used for debug log report - //newHeading = float64(30*time.Now().Minute()) // demo input for testing - newHeading = normalizeHeading(newHeading) // sanitize the inputs - - // By applying gain factor, this becomes a 1st order function that slowly converges on solution. - // Time constant is poll rate over gain. With gain of 0.1, convergence to +/-2 deg on a 180 correction difference is about 4 sec; 0.01 converges in 45 sec. - - hdg_corr_bias := float64(d.heading - newHeading) // desired adjustment to heading_correction - if hdg_corr_bias > 180 { - hdg_corr_bias = hdg_corr_bias - 360 - } else if hdg_corr_bias < -180 { - hdg_corr_bias = hdg_corr_bias + 360 - } - hdg_corr_bias = hdg_corr_bias * gain - d.heading_correction = normalizeHeading(d.heading_correction + hdg_corr_bias) - log.Printf("Adjusted heading. Old: %f Desired: %f Adjustment: %f New: %f\n", old_hdg, newHeading, hdg_corr_bias, d.heading-hdg_corr_bias) -} - -// Close. -func (d *MPU6050) Close() { - if d.quit != nil { - d.quit <- struct{}{} - } -} - -func (d *MPU6050) MagHeading() (float64, error) { return 0, nil } -func (d *MPU6050) SlipSkid() (float64, error) { return 0, nil } -func (d *MPU6050) RateOfTurn() (float64, error) { return 0, nil } -func (d *MPU6050) GLoad() (float64, error) { return 0, nil } - -func (d *MPU6050) ReadRaw() (int64, float64, float64, float64, float64, float64, float64, float64, float64, float64, error, error) { - return 0, // Ts, time of last sensor reading - 0.0, 0.0, 0.0, // Gyro x, y, z - 0.0, 0.0, 0.0, // Accel x, y, z - 0.0, 0.0, 0.0, // Mag x, y, z - errors.New("Error: ReadRaw() not implemented yet for MPU6050"), - errors.New("Error: MPU6050 magnetometer isn't working on RY835AI chip") -} -func (d *MPU6050) Calibrate(dur int, retries int) error { - return nil //TODO westphae: for now, maybe we'll get lucky; but eventually we should calibrate -} \ No newline at end of file diff --git a/notes/app-vendor-integration.md b/notes/app-vendor-integration.md index a0a4ce9e..2f49f78e 100644 --- a/notes/app-vendor-integration.md +++ b/notes/app-vendor-integration.md @@ -103,7 +103,6 @@ Stratux makes available a webserver to retrieve statistics which may be useful t "GPS_satellites_locked": 0, // Number of GPS satellites used in last GPS lock. "GPS_connected": true, // GPS unit connected and functioning. "GPS_solution": "", // "DGPS (WAAS)", "3D GPS", "N/A", or "" when GPS not connected/enabled. - "RY835AI_connected": false, // GPS/AHRS unit - use only for debugging (this will be removed). "Uptime": 227068, // Device uptime (in milliseconds). "CPUTemp": 42.236 // CPU temperature (in ÂșC). } @@ -131,7 +130,6 @@ Stratux makes available a webserver to retrieve statistics which may be useful t "Capability": 2 } ], - "AHRS_Enabled": false, "DEBUG": false, "ReplayLog": true, "PPM": 0, diff --git a/selfupdate/makeupdate.sh b/selfupdate/makeupdate.sh index d6a2d174..2f4777aa 100755 --- a/selfupdate/makeupdate.sh +++ b/selfupdate/makeupdate.sh @@ -23,16 +23,19 @@ cp __root__stratux-pre-start.sh work/bin/ cp dump1090/dump1090 work/bin/ cp -r web work/bin/ cp image/hostapd.conf work/bin/ +cp image/hostapd-edimax.conf work/bin/ cp image/config.txt work/bin/ cp image/rtl-sdr-blacklist.conf work/bin/ cp image/bashrc.txt work/bin/ cp image/modules.txt work/bin/ cp image/stxAliases.txt work/bin/ cp image/hostapd_manager.sh work/bin/ +cp image/sdr-tool.sh work/bin/ cp image/10-stratux.rules work/bin/ cp image/99-uavionix.rules work/bin/ cp image/motd work/bin/ cp image/fancontrol.py work/bin/ +cp image/stratux-wifi.sh work/bin/ #TODO: librtlsdr. cd work/ diff --git a/selfupdate/update_footer.sh b/selfupdate/update_footer.sh index bd089b64..4d1f36c9 100755 --- a/selfupdate/update_footer.sh +++ b/selfupdate/update_footer.sh @@ -23,9 +23,15 @@ ln -fs /lib/systemd/system/stratux.service /etc/systemd/system/multi-user.target cp -f hostapd.conf /etc/hostapd/hostapd.conf cp -f hostapd-edimax.conf /etc/hostapd/hostapd-edimax.conf +#WiFi Hostapd ver test and hostapd.conf builder script +cp -f stratux-wifi.sh /usr/sbin/ + #WiFi Config Manager cp -f hostapd_manager.sh /usr/sbin/ +#SDR Serial Script +cp -f sdr-tool.sh /usr/sbin/ + #boot config cp -f config.txt /boot/config.txt diff --git a/sensors/bmp280.go b/sensors/bmp280.go new file mode 100644 index 00000000..b2df389b --- /dev/null +++ b/sensors/bmp280.go @@ -0,0 +1,79 @@ +// Package sensors provides a stratux interface to sensors used for AHRS calculations. +package sensors + +import ( + "errors" + "time" + + "github.com/kidoman/embd" + "github.com/westphae/goflying/bmp280" +) + +const ( + bmp280PowerMode = bmp280.NormalMode + bmp280Standby = bmp280.StandbyTime63ms + bmp280FilterCoeff = bmp280.FilterCoeff16 + bmp280TempRes = bmp280.Oversamp16x + bmp280PressRes = bmp280.Oversamp16x +) + +// BMP280 represents a BMP280 sensor and implements the PressureSensor interface. +type BMP280 struct { + sensor *bmp280.BMP280 + data *bmp280.BMPData + running bool +} + +var errBMP = errors.New("BMP280 Error: BMP280 is not running") + +// NewBMP280 looks for a BMP280 connected on the I2C bus having one of the valid addresses and begins reading it. +func NewBMP280(i2cbus *embd.I2CBus, freq time.Duration) (*BMP280, error) { + var ( + bmp *bmp280.BMP280 + errbmp error + ) + + bmp, errbmp = bmp280.NewBMP280(i2cbus, bmp280.Address1, + bmp280PowerMode, bmp280Standby, bmp280FilterCoeff, bmp280TempRes, bmp280PressRes) + if errbmp != nil { // Maybe the BMP280 isn't at Address1, try Address2 + bmp, errbmp = bmp280.NewBMP280(i2cbus, bmp280.Address2, + bmp280PowerMode, bmp280Standby, bmp280FilterCoeff, bmp280TempRes, bmp280PressRes) + } + if errbmp != nil { + return nil, errbmp + } + + newbmp := BMP280{sensor: bmp, data: new(bmp280.BMPData)} + go newbmp.run() + + return &newbmp, nil +} + +func (bmp *BMP280) run() { + bmp.running = true + for bmp.running { + bmp.data = <-bmp.sensor.C + } +} + +// Temperature returns the current temperature in degrees C measured by the BMP280 +func (bmp *BMP280) Temperature() (float64, error) { + if !bmp.running { + return 0, errBMP + } + return bmp.data.Temperature, nil +} + +// Pressure returns the current pressure in mbar measured by the BMP280 +func (bmp *BMP280) Pressure() (float64, error) { + if !bmp.running { + return 0, errBMP + } + return bmp.data.Pressure, nil +} + +// Close stops the measurements of the BMP280 +func (bmp *BMP280) Close() { + bmp.running = false + bmp.sensor.Close() +} diff --git a/sensors/imu.go b/sensors/imu.go new file mode 100644 index 00000000..6e4b6e16 --- /dev/null +++ b/sensors/imu.go @@ -0,0 +1,15 @@ +// Package sensors provides a stratux interface to sensors used for AHRS calculations. +package sensors + +// IMUReader provides an interface to various Inertial Measurement Unit sensors, +// such as the InvenSense MPU9150 or MPU9250. +type IMUReader interface { + // ReadRaw returns the time, Gyro X-Y-Z, Accel X-Y-Z, Mag X-Y-Z, error reading Gyro/Accel, and error reading Mag. + ReadRaw() (T int64, G1, G2, G3, A1, A2, A3, M1, M2, M3 float64, GAError, MagError error) + Calibrate(duration, retries int) error // Calibrate kicks off a calibration for specified duration (s) and retries. + Close() // Close stops reading the MPU. + MagHeading() (hdg float64, MagError error) // MagHeading returns the magnetic heading in degrees. + SlipSkid() (slipSkid float64, err error) // SlipSkid returns the slip/skid angle in degrees. + RateOfTurn() (turnRate float64, err error) // RateOfTurn returns the turn rate in degrees per second. + GLoad() (gLoad float64, err error) // GLoad returns the current G load, in G's. +} diff --git a/mpu/mpu9250.go b/sensors/mpu9250.go similarity index 57% rename from mpu/mpu9250.go rename to sensors/mpu9250.go index 802ed936..4a48a483 100644 --- a/mpu/mpu9250.go +++ b/sensors/mpu9250.go @@ -1,21 +1,24 @@ -// Package MPU9250 provides a stratux interface to the MPU9250 IMU -package mpu +// Package sensors provides a stratux interface to sensors used for AHRS calculations. +package sensors import ( "errors" - "github.com/westphae/goflying/mpu9250" - "log" "math" "time" + + "github.com/westphae/goflying/mpu9250" + "log" ) const ( - DECAY = 0.8 - GYRORANGE = 250 - ACCELRANGE = 4 - UPDATEFREQ = 1000 + decay = 0.8 // decay is the decay constant used for exponential smoothing of sensor values. + gyroRange = 250 // gyroRange is the default range to use for the Gyro. + accelRange = 4 // accelRange is the default range to use for the Accel. + updateFreq = 1000 // updateFreq is the rate at which to update the sensor values. ) +// MPU9250 represents an InvenSense MPU9250 attached to the I2C bus and satisfies +// the IMUReader interface. type MPU9250 struct { mpu *mpu9250.MPU9250 pitch, roll, heading float64 @@ -29,6 +32,8 @@ type MPU9250 struct { quit chan struct{} } +// NewMPU9250 returns an instance of the MPU9250 IMUReader, connected to an +// MPU9250 attached on the I2C bus with either valid address. func NewMPU9250() (*MPU9250, error) { var ( m MPU9250 @@ -36,13 +41,14 @@ func NewMPU9250() (*MPU9250, error) { err error ) - mpu, err = mpu9250.NewMPU9250(GYRORANGE, ACCELRANGE, UPDATEFREQ, true, false) + log.Println("AHRS Info: Making new MPU9250") + mpu, err = mpu9250.NewMPU9250(gyroRange, accelRange, updateFreq, true, false) if err != nil { - log.Println("AHRS Error: couldn't initialize MPU9250") return nil, err } // Set Gyro (Accel) LPFs to 20 (21) Hz to filter out prop/glareshield vibrations above 1200 (1260) RPM + log.Println("AHRS Info: Setting MPU9250 LPF") mpu.SetGyroLPF(21) mpu.SetAccelLPF(21) @@ -50,6 +56,7 @@ func NewMPU9250() (*MPU9250, error) { m.valid = true time.Sleep(100 * time.Millisecond) + log.Println("AHRS Info: monitoring IMU") m.run() return &m, nil @@ -74,8 +81,8 @@ func (m *MPU9250) run() { } if data.MagError == nil && data.NM > 0 { - hM := math.Atan2(-data.M2, data.M1)*180/math.Pi - if hM - m.headingMag < -180 { + hM := math.Atan2(-data.M2, data.M1) * 180 / math.Pi + if hM-m.headingMag < -180 { hM += 360 } smooth(&m.headingMag, hM) @@ -95,45 +102,42 @@ func (m *MPU9250) run() { } func smooth(val *float64, new float64) { - *val = DECAY**val + (1-DECAY)*new -} - -func (m *MPU9250) ResetHeading(newHeading float64, gain float64) { - m.heading = newHeading + *val = decay**val + (1-decay)*new } +// MagHeading returns the magnetic heading in degrees. func (m *MPU9250) MagHeading() (float64, error) { if m.valid { return m.headingMag, nil - } else { - return 0, errors.New("MPU error: data not available") } + return 0, errors.New("MPU error: data not available") } +// SlipSkid returns the slip/skid angle in degrees. func (m *MPU9250) SlipSkid() (float64, error) { if m.valid { return m.slipSkid, nil - } else { - return 0, errors.New("MPU error: data not available") } + return 0, errors.New("MPU error: data not available") } +// RateOfTurn returns the turn rate in degrees per second. func (m *MPU9250) RateOfTurn() (float64, error) { if m.valid { return m.turnRate, nil - } else { - return 0, errors.New("MPU error: data not available") } + return 0, errors.New("MPU error: data not available") } +// GLoad returns the current G load, in G's. func (m *MPU9250) GLoad() (float64, error) { if m.valid { return m.gLoad, nil - } else { - return 0, errors.New("MPU error: data not available") } + return 0, errors.New("MPU error: data not available") } +// ReadRaw returns the time, Gyro X-Y-Z, Accel X-Y-Z, Mag X-Y-Z, error reading Gyro/Accel, and error reading Mag. func (m *MPU9250) ReadRaw() (T int64, G1, G2, G3, A1, A2, A3, M1, M2, M3 float64, GAError, MAGError error) { data := <-m.mpu.C T = data.T.UnixNano() @@ -151,18 +155,23 @@ func (m *MPU9250) ReadRaw() (T int64, G1, G2, G3, A1, A2, A3, M1, M2, M3 float64 return } +// Calibrate kicks off a calibration for specified duration (s) and retries. func (m *MPU9250) Calibrate(dur, retries int) (err error) { - for i:=0; i= 0xC00001) && (icao_addr <= 0xC3FFFF) { nation = "CA" } else { - // future national decoding is TO-DO + //TODO: future national decoding. return "NON-NA", false } diff --git a/test/screen/screen.py b/test/screen/screen.py index 8acc3307..148cd92a 100755 --- a/test/screen/screen.py +++ b/test/screen/screen.py @@ -9,69 +9,80 @@ import urllib2 import json import time -font2 = ImageFont.truetype('/root/stratux/test/screen/CnC_Red_Alert.ttf', 12) -oled = ssd1306(port=1, address=0x3C) +from daemon import runner -with canvas(oled) as draw: - logo = Image.open('/root/stratux/test/screen/logo.bmp') - draw.bitmap((32, 0), logo, fill=1) +class StratuxScreen(): + def __init__(self): + self.stdin_path = '/dev/null' + self.stdout_path = '/var/log/stratux-screen.log' + self.stderr_path = '/var/log/stratux-screen.log' + self.pidfile_path = '/var/run/stratux-screen.pid' + self.pidfile_timeout = 5 + def run(self): + font2 = ImageFont.truetype('/etc/stratux-screen/CnC_Red_Alert.ttf', 12) + oled = ssd1306(port=1, address=0x3C) -time.sleep(10) + with canvas(oled) as draw: + logo = Image.open('/etc/stratux-screen/stratux-logo-64x64.bmp') + draw.bitmap((32, 0), logo, fill=1) -n = 0 + time.sleep(10) + n = 0 -while 1: - time.sleep(1) - response = urllib2.urlopen('http://localhost/getStatus') - getStatusHTML = response.read() - getStatusData = json.loads(getStatusHTML) - CPUTemp = getStatusData["CPUTemp"] - uat_current = getStatusData["UAT_messages_last_minute"] - uat_max = getStatusData["UAT_messages_max"] - es_current = getStatusData["ES_messages_last_minute"] - es_max = getStatusData["ES_messages_max"] + while 1: + time.sleep(1) + response = urllib2.urlopen('http://localhost/getStatus') + getStatusHTML = response.read() + getStatusData = json.loads(getStatusHTML) + CPUTemp = getStatusData["CPUTemp"] + uat_current = getStatusData["UAT_messages_last_minute"] + uat_max = getStatusData["UAT_messages_max"] + es_current = getStatusData["ES_messages_last_minute"] + es_max = getStatusData["ES_messages_max"] - response = urllib2.urlopen('http://localhost/getTowers') - getTowersHTML = response.read() - getTowersData = json.loads(getTowersHTML) - NumTowers = len(getTowersData) - - - - - with canvas(oled) as draw: - pad = 2 # Two pixels on the left and right. - text_margin = 25 - # UAT status. - draw.text((50, 0), "UAT", font=font2, fill=255) - # "Status bar", 2 pixels high. - status_bar_width_max = oled.width - (2 * pad) - (2 * text_margin) - status_bar_width = 0 - if uat_max > 0: - status_bar_width = int((float(uat_current) / uat_max) * status_bar_width_max) - draw.rectangle((pad + text_margin, 14, pad + text_margin + status_bar_width, 20), outline=255, fill=255) # Top left, bottom right. - # Draw the current (left) and max (right) numbers. - draw.text((pad, 14), str(uat_current), font=font2, fill=255) - draw.text(((2*pad) + text_margin + status_bar_width_max, 14), str(uat_max), font=font2, fill=255) - # ES status. - draw.text((44, 24), "1090ES", font=font2, fill=255) - status_bar_width = 0 - if es_max > 0: - status_bar_width = int((float(es_current) / es_max) * status_bar_width_max) - draw.rectangle((pad + text_margin, 34, pad + text_margin + status_bar_width, 40), outline=255, fill=255) # Top left, bottom right. - # Draw the current (left) and max (right) numbers. - draw.text((pad, 34), str(es_current), font=font2, fill=255) - draw.text(((2*pad) + text_margin + status_bar_width_max, 34), str(es_max), font=font2, fill=255) - # Other stats. - seq = (n / 5) % 2 - t = "" - if seq == 0: - t = "CPU: %0.1fC, Towers: %d" % (CPUTemp, NumTowers) - if seq == 1: - t = "GPS Sat: %d/%d/%d" % (getStatusData["GPS_satellites_locked"], getStatusData["GPS_satellites_seen"], getStatusData["GPS_satellites_tracked"]) - if getStatusData["GPS_solution"] == "GPS + SBAS (WAAS / EGNOS)": - t = t + " (WAAS)" - print t - draw.text((pad, 45), t, font=font2, fill=255) - - n = n+1 \ No newline at end of file + response = urllib2.urlopen('http://localhost/getTowers') + getTowersHTML = response.read() + getTowersData = json.loads(getTowersHTML) + NumTowers = len(getTowersData) + + with canvas(oled) as draw: + pad = 2 # Two pixels on the left and right. + text_margin = 25 + # UAT status. + draw.text((50, 0), "UAT", font=font2, fill=255) + # "Status bar", 2 pixels high. + status_bar_width_max = oled.width - (2 * pad) - (2 * text_margin) + status_bar_width = 0 + if uat_max > 0: + status_bar_width = int((float(uat_current) / uat_max) * status_bar_width_max) + draw.rectangle((pad + text_margin, 14, pad + text_margin + status_bar_width, 20), outline=255, fill=255) # Top left, bottom right. + # Draw the current (left) and max (right) numbers. + draw.text((pad, 14), str(uat_current), font=font2, fill=255) + draw.text(((2*pad) + text_margin + status_bar_width_max, 14), str(uat_max), font=font2, fill=255) + # ES status. + draw.text((44, 24), "1090ES", font=font2, fill=255) + status_bar_width = 0 + if es_max > 0: + status_bar_width = int((float(es_current) / es_max) * status_bar_width_max) + draw.rectangle((pad + text_margin, 34, pad + text_margin + status_bar_width, 40), outline=255, fill=255) # Top left, bottom right. + # Draw the current (left) and max (right) numbers. + draw.text((pad, 34), str(es_current), font=font2, fill=255) + draw.text(((2*pad) + text_margin + status_bar_width_max, 34), str(es_max), font=font2, fill=255) + # Other stats. + seq = (n / 5) % 2 + t = "" + if seq == 0: + t = "CPU: %0.1fC, Towers: %d" % (CPUTemp, NumTowers) + if seq == 1: + t = "GPS Sat: %d/%d/%d" % (getStatusData["GPS_satellites_locked"], getStatusData["GPS_satellites_seen"], getStatusData["GPS_satellites_tracked"]) + if getStatusData["GPS_solution"] == "GPS + SBAS (WAAS / EGNOS)": + t = t + " (WAAS)" + #print t + draw.text((pad, 45), t, font=font2, fill=255) + + n = n+1 + + +stratuxscreen = StratuxScreen() +daemon_runner = runner.DaemonRunner(stratuxscreen) +daemon_runner.do_action() diff --git a/test/screen/logo.bmp b/test/screen/stratux-logo-64x64.bmp similarity index 100% rename from test/screen/logo.bmp rename to test/screen/stratux-logo-64x64.bmp diff --git a/test/sensortest.go b/test/sensortest.go deleted file mode 100644 index c66e1b82..00000000 --- a/test/sensortest.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "../mpu" - "fmt" - "net" - "time" -) - -var attSensor mpu.MPU - -func readMPU6050() (float64, float64) { - pitch, _ := attSensor.Pitch() - roll, _ := attSensor.Roll() - - return pitch, roll -} - -func initMPU6050() { - attSensor = mpu.NewMPU6050() -} - -func main() { - initMPU6050() - addr, err := net.ResolveUDPAddr("udp", "192.168.1.255:49002") - if err != nil { - panic(err) - } - outConn, err := net.DialUDP("udp", nil, addr) - for { - pitch, roll := readMPU6050() - heading, _ := attSensor.Heading() - s := fmt.Sprintf("XATTMy Sim,%f,%f,%f", heading, pitch, roll) - fmt.Printf("%f, %f\n", pitch, roll) - outConn.Write([]byte(s)) - time.Sleep(50 * time.Millisecond) - } -} diff --git a/uatparse/nexrad.go b/uatparse/nexrad.go new file mode 100644 index 00000000..453955aa --- /dev/null +++ b/uatparse/nexrad.go @@ -0,0 +1,147 @@ +package uatparse + +import () + +const ( + BLOCK_WIDTH = float64(48.0 / 60.0) + WIDE_BLOCK_WIDTH = float64(96.0 / 60.0) + BLOCK_HEIGHT = float64(4.0 / 60.0) + BLOCK_THRESHOLD = 405000 + BLOCKS_PER_RING = 450 +) + +type NEXRADBlock struct { + Radar_Type uint32 + Scale int + LatNorth float64 + LonWest float64 + Height float64 + Width float64 + Intensity []uint16 // Really only 4-bit values, but using this as a hack for the JSON encoding. +} + +func block_location(block_num int, ns_flag bool, scale_factor int) (float64, float64, float64, float64) { + var realScale float64 + if scale_factor == 1 { + realScale = float64(5.0) + } else if scale_factor == 2 { + realScale = float64(9.0) + } else { + realScale = float64(1.0) + } + + if block_num >= BLOCK_THRESHOLD { + block_num = block_num & ^1 + } + + raw_lat := float64(BLOCK_HEIGHT * float64(int(float64(block_num)/float64(BLOCKS_PER_RING)))) + raw_lon := float64(block_num%BLOCKS_PER_RING) * BLOCK_WIDTH + + var lonSize float64 + if block_num >= BLOCK_THRESHOLD { + lonSize = WIDE_BLOCK_WIDTH * realScale + } else { + lonSize = BLOCK_WIDTH * realScale + } + + latSize := BLOCK_HEIGHT * realScale + + if ns_flag { // Southern hemisphere. + raw_lat = 0 - raw_lat + } else { + raw_lat = raw_lat + BLOCK_HEIGHT + } + + if raw_lon > 180.0 { + raw_lon = raw_lon - 360.0 + } + + return raw_lat, raw_lon, latSize, lonSize + +} + +func (f *UATFrame) decodeNexradFrame() { + if len(f.FISB_data) < 4 { // Short read. + return + } + + rle_flag := (uint32(f.FISB_data[0]) & 0x80) != 0 + ns_flag := (uint32(f.FISB_data[0]) & 0x40) != 0 + block_num := ((int(f.FISB_data[0]) & 0x0f) << 16) | (int(f.FISB_data[1]) << 8) | (int(f.FISB_data[2])) + scale_factor := (int(f.FISB_data[0]) & 0x30) >> 4 + + if rle_flag { // Single bin, RLE encoded. + lat, lon, h, w := block_location(block_num, ns_flag, scale_factor) + var tmp NEXRADBlock + tmp.Radar_Type = f.Product_id + tmp.Scale = scale_factor + tmp.LatNorth = lat + tmp.LonWest = lon + tmp.Height = h + tmp.Width = w + tmp.Intensity = make([]uint16, 0) + + intensityData := f.FISB_data[3:] + for _, v := range intensityData { + intensity := uint16(v) & 0x7 + runlength := (uint16(v) >> 3) + 1 + for runlength > 0 { + tmp.Intensity = append(tmp.Intensity, intensity) + runlength-- + } + } + f.NEXRAD = []NEXRADBlock{tmp} + } else { + var row_start int + var row_size int + if block_num >= 405000 { + row_start = block_num - ((block_num - 405000) % 225) + row_size = 225 + } else { + row_start = block_num - (block_num % 450) + row_size = 450 + } + + row_offset := block_num - row_start + + L := int(f.FISB_data[3] & 15) + + if len(f.FISB_data) < L+3 { // Short read. + return + } + + for i := 0; i < L; i++ { + var bb int + if i == 0 { + bb = (int(f.FISB_data[3]) & 0xF0) | 0x08 + } else { + bb = int(f.FISB_data[i+3]) + } + + for j := 0; j < 8; j++ { + if bb&(1<>/var/www/stratux.appcache echo "# Stratux build: " ${stratuxBuild} >>/var/www/stratux.appcache \ No newline at end of file diff --git a/web/css/main.css b/web/css/main.css old mode 100755 new mode 100644 index b30a1f8a..5ab603b2 --- a/web/css/main.css +++ b/web/css/main.css @@ -5,22 +5,22 @@ .modal { } .vertical-alignment-helper { - display:table; - height: 100%; - width: 75%; + display:table; + height: 100%; + width: 75%; } .vertical-align-center { - /* To center vertically */ - display: table-cell; - vertical-align: middle; + /* To center vertically */ + display: table-cell; + vertical-align: middle; } .modal-content { - /* Bootstrap sets the size of the modal in the modal-dialog class, we need to inherit it */ - width:inherit; - height:inherit; - /* To center horizontally */ - margin: 0 auto; + /* Bootstrap sets the size of the modal in the modal-dialog class, we need to inherit it */ + width:inherit; + height:inherit; + /* To center horizontally */ + margin: 0 auto; } .traffic-page {} @@ -131,22 +131,44 @@ content: "\f1d9"; } +.report_TAF { + border-radius: 5px; + background-color: cornsilk; + color: black; +} + +.report_PIREP { + border-radius: 5px; + background-color: gainsboro; + color: black; +} + +.report_WINDS { + border-radius: 5px; + background-color: lavender; + color: black; +} + .flight_condition_VFR { + border-radius: 5px; background-color: forestgreen; color: white; } .flight_condition_MVFR { + border-radius: 5px; background-color: blue; color: white; } .flight_condition_IFR { + border-radius: 5px; background-color: crimson; color: white; } .flight_condition_LIFR { + border-radius: 5px; background-color: darkorchid; color: white; } @@ -237,7 +259,7 @@ /* *************************************************************************** -everything below this comment represents tweeks to the mobile-angular-uis CSS +everything below this comment represents tweaks to the mobile-angular-uis CSS *************************************************************************** */ @font-face { @@ -396,6 +418,9 @@ pre { border-radius: 0px; } +body { + font-weight: 400; +} /* change right sidebar behavior to always push */ diff --git a/web/index.html b/web/index.html index 796d7322..48594d4e 100755 --- a/web/index.html +++ b/web/index.html @@ -64,6 +64,7 @@ + @@ -83,6 +84,9 @@ Towers Logs Settings + diff --git a/web/js/main.js b/web/js/main.js old mode 100755 new mode 100644 index a92eae92..fdfbbb02 --- a/web/js/main.js +++ b/web/js/main.js @@ -9,9 +9,12 @@ var URL_SATELLITES_GET = "http://" + URL_HOST_BASE + "/getSatellites" var URL_STATUS_WS = "ws://" + URL_HOST_BASE + "/status" var URL_TRAFFIC_WS = "ws://" + URL_HOST_BASE + "/traffic"; var URL_WEATHER_WS = "ws://" + URL_HOST_BASE + "/weather"; +var URL_DEVELOPER_GET = "ws://" + URL_HOST_BASE + "/developer"; var URL_UPDATE_UPLOAD = "http://" + URL_HOST_BASE + "/updateUpload"; var URL_REBOOT = "http://" + URL_HOST_BASE + "/reboot"; var URL_SHUTDOWN = "http://" + URL_HOST_BASE + "/shutdown"; +var URL_RESTARTAPP = "http://" + URL_HOST_BASE + "/restart"; +var URL_DEV_TOGGLE_GET = "http://" + URL_HOST_BASE + "/develmodetoggle"; // define the module with dependency on mobile-angular-ui //var app = angular.module('stratux', ['ngRoute', 'mobile-angular-ui', 'mobile-angular-ui.gestures', 'appControllers']); @@ -62,6 +65,12 @@ app.config(function ($stateProvider, $urlRouterProvider) { templateUrl: 'plates/settings.html', controller: 'SettingsCtrl', reloadOnSearch: false + }) + .state('developer', { + url: '/developer', + templateUrl: 'plates/developer.html', + controller: 'DeveloperCtrl', + reloadOnSearch: false }); $urlRouterProvider.otherwise('/'); }); @@ -72,6 +81,13 @@ app.run(function ($transform) { }); // For this app we have a MainController for whatever and individual controllers for each page -app.controller('MainCtrl', function ($rootScope, $scope) { +app.controller('MainCtrl', function ($scope, $http) { // any logic global logic + $http.get(URL_SETTINGS_GET) + .then(function(response) { + settings = angular.fromJson(response.data); + $scope.DeveloperMode = settings.DeveloperMode; + }, function(response) { + //Second function handles error + }); }); \ No newline at end of file diff --git a/web/maui/css/mobile-angular-ui-base.css b/web/maui/css/mobile-angular-ui-base.css old mode 100755 new mode 100644 index 35c2f579..67a8ffcf --- a/web/maui/css/mobile-angular-ui-base.css +++ b/web/maui/css/mobile-angular-ui-base.css @@ -6373,50 +6373,62 @@ a.label:focus { .label-default { background-color: #777777; + border-radius: 5px; } .label-default[href]:focus { background-color: #5e5e5e; + border-radius: 5px; } .label-primary { background-color: #007aff; + border-radius: 5px; } .label-primary[href]:focus { background-color: #0062cc; + border-radius: 5px; } .label-success { background-color: #4cd964; + border-radius: 5px; } .label-success[href]:focus { background-color: #2ac845; + border-radius: 5px; } .label-info { background-color: #34aadc; + border-radius: 5px; } .label-info[href]:focus { background-color: #218ebd; + border-radius: 5px; } .label-warning { background-color: #ffcc00; + border-radius: 5px; } .label-warning[href]:focus { background-color: #cca300; + border-radius: 5px; } .label-danger { background-color: #ff3b30; + border-radius: 5px; } .label-danger[href]:focus { background-color: #fc0d00; + border-radius: 5px; } .badge { diff --git a/web/maui/css/mobile-angular-ui-hover.css b/web/maui/css/mobile-angular-ui-hover.css index 61bc5c4d..38613826 100755 --- a/web/maui/css/mobile-angular-ui-hover.css +++ b/web/maui/css/mobile-angular-ui-hover.css @@ -100,6 +100,10 @@ a.bg-danger:hover { text-decoration: none; } +.btn-hidden:hover { + color: #000000; +} + .btn-default:hover { color: #333333; background-color: #e6e6e6; diff --git a/web/plates/developer-help.html b/web/plates/developer-help.html new file mode 100644 index 00000000..333c1e30 --- /dev/null +++ b/web/plates/developer-help.html @@ -0,0 +1,4 @@ +
+

The Developer page provides basic access to developer options

+

+
\ No newline at end of file diff --git a/web/plates/developer.html b/web/plates/developer.html new file mode 100644 index 00000000..87d3396d --- /dev/null +++ b/web/plates/developer.html @@ -0,0 +1,21 @@ +
+
+
+ Developer Mode Items +
+
+
+
+
Restart Stratux application
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/web/plates/js/developer.js b/web/plates/js/developer.js new file mode 100644 index 00000000..ee815fa4 --- /dev/null +++ b/web/plates/js/developer.js @@ -0,0 +1,18 @@ +angular.module('appControllers').controller('DeveloperCtrl', DeveloperCtrl); // get the main module contollers set +DeveloperCtrl.$inject = ['$rootScope', '$scope', '$state', '$http', '$interval']; // Inject my dependencies + +// create our controller function with all necessary logic +function DeveloperCtrl($rootScope, $scope, $state, $http, $interval) { + $scope.$parent.helppage = 'plates/developer-help.html'; + + $scope.postRestart = function () { + $http.post(URL_RESTARTAPP). + then(function (response) { + // do nothing + // $scope.$apply(); + }, function (response) { + // do nothing + }); + }; +}; + diff --git a/web/plates/js/settings.js b/web/plates/js/settings.js index fd158dad..135f6a09 100755 --- a/web/plates/js/settings.js +++ b/web/plates/js/settings.js @@ -6,7 +6,7 @@ function SettingsCtrl($rootScope, $scope, $state, $location, $window, $http) { $scope.$parent.helppage = 'plates/settings-help.html'; - var toggles = ['UAT_Enabled', 'ES_Enabled', 'Ping_Enabled', 'GPS_Enabled', 'AHRS_Enabled', 'DisplayTrafficSource', 'DEBUG', 'ReplayLog']; + var toggles = ['UAT_Enabled', 'ES_Enabled', 'Ping_Enabled', 'GPS_Enabled', 'DisplayTrafficSource', 'DEBUG', 'ReplayLog']; var settings = {}; for (i = 0; i < toggles.length; i++) { settings[toggles[i]] = undefined; @@ -17,17 +17,22 @@ function SettingsCtrl($rootScope, $scope, $state, $location, $window, $http) { settings = angular.fromJson(data); // consider using angular.extend() $scope.rawSettings = angular.toJson(data, true); + $scope.visible_serialout = false; + if ((settings.SerialOutputs !== undefined) && (settings.SerialOutputs !== null) && (settings.SerialOutputs['/dev/serialout0'] !== undefined)) { + $scope.Baud = settings.SerialOutputs['/dev/serialout0'].Baud; + $scope.visible_serialout = true; + } $scope.UAT_Enabled = settings.UAT_Enabled; $scope.ES_Enabled = settings.ES_Enabled; $scope.Ping_Enabled = settings.Ping_Enabled; $scope.GPS_Enabled = settings.GPS_Enabled; - $scope.AHRS_Enabled = settings.AHRS_Enabled; $scope.DisplayTrafficSource = settings.DisplayTrafficSource; $scope.DEBUG = settings.DEBUG; $scope.ReplayLog = settings.ReplayLog; $scope.PPM = settings.PPM; $scope.WatchList = settings.WatchList; $scope.OwnshipModeS = settings.OwnshipModeS; + $scope.DeveloperMode = settings.DeveloperMode; } function getSettings() { @@ -91,6 +96,18 @@ function SettingsCtrl($rootScope, $scope, $state, $location, $window, $http) { } }; + $scope.updateBaud = function () { + settings["Baud"] = 0 + if (($scope.Baud !== undefined) && ($scope.Baud !== null) && ($scope.Baud !== settings["Baud"])) { + settings["Baud"] = parseInt($scope.Baud); + newsettings = { + "Baud": settings["Baud"] + }; + // console.log(angular.toJson(newsettings)); + setSettings(angular.toJson(newsettings)); + } + }; + $scope.updatewatchlist = function () { if ($scope.WatchList !== settings["WatchList"]) { settings["WatchList"] = ""; @@ -180,4 +197,4 @@ function SettingsCtrl($rootScope, $scope, $state, $location, $window, $http) { }); }; -}; +}; diff --git a/web/plates/js/status.js b/web/plates/js/status.js old mode 100755 new mode 100644 index 1a4344c2..a8e0aacc --- a/web/plates/js/status.js +++ b/web/plates/js/status.js @@ -54,9 +54,14 @@ function StatusCtrl($rootScope, $scope, $state, $http, $interval) { $scope.GPS_satellites_tracked = status.GPS_satellites_tracked; $scope.GPS_satellites_seen = status.GPS_satellites_seen; $scope.GPS_solution = status.GPS_solution; - $scope.GPS_position_accuracy = String(status.GPS_solution ? ", " + status.GPS_position_accuracy.toFixed(1) : ""); - $scope.RY835AI_connected = status.RY835AI_connected; - + $scope.GPS_position_accuracy = String(status.GPS_solution ? ", " + status.GPS_position_accuracy.toFixed(1) + " m" : " "); + $scope.UAT_METAR_total = status.UAT_METAR_total; + $scope.UAT_TAF_total = status.UAT_TAF_total; + $scope.UAT_NEXRAD_total = status.UAT_NEXRAD_total; + $scope.UAT_SIGMET_total = status.UAT_SIGMET_total; + $scope.UAT_PIREP_total = status.UAT_PIREP_total; + $scope.UAT_NOTAM_total = status.UAT_NOTAM_total; + $scope.UAT_OTHER_total = status.UAT_OTHER_total; // Errors array. if (status.Errors.length > 0) { $scope.visible_errors = true; @@ -95,6 +100,7 @@ function StatusCtrl($rootScope, $scope, $state, $http, $interval) { $http.get(URL_SETTINGS_GET). then(function (response) { settings = angular.fromJson(response.data); + $scope.DeveloperMode = settings.DeveloperMode; $scope.visible_uat = settings.UAT_Enabled; $scope.visible_es = settings.ES_Enabled; $scope.visible_ping = settings.Ping_Enabled; @@ -103,7 +109,6 @@ function StatusCtrl($rootScope, $scope, $state, $http, $interval) { $scope.visible_es = true; } $scope.visible_gps = settings.GPS_Enabled; - $scope.visible_ahrs = settings.AHRS_Enabled; }, function (response) { // nop }); @@ -133,7 +138,16 @@ function StatusCtrl($rootScope, $scope, $state, $http, $interval) { getTowers(); }, (5 * 1000), 0, false); - + var clicks = 0; + var clickSeconds = 0; + var DeveloperModeClick = 0; + + var clickInterval = $interval(function () { + if ((clickSeconds >= 3)) + clicks=0; + clickSeconds++; + }, 1000); + $state.get('home').onEnter = function () { // everything gets handled correctly by the controller }; @@ -144,7 +158,26 @@ function StatusCtrl($rootScope, $scope, $state, $http, $interval) { } $interval.cancel(updateTowers); }; - + + $scope.VersionClick = function() { + if (clicks==0) + { + clickSeconds = 0; + } + ++clicks; + if ((clicks > 7) && (clickSeconds < 3)) + { + clicks=0; + clickSeconds=0; + DeveloperModeClick = 1; + $http.get(URL_DEV_TOGGLE_GET); + location.reload(); + } + } + + $scope.GetDeveloperModeClick = function() { + return DeveloperModeClick; + } // Status Controller tasks setHardwareVisibility(); connect($scope); // connect - opens a socket and listens for messages diff --git a/web/plates/js/weather.js b/web/plates/js/weather.js old mode 100755 new mode 100644 index 1ca6e220..bb02a419 --- a/web/plates/js/weather.js +++ b/web/plates/js/weather.js @@ -140,7 +140,17 @@ function WeatherCtrl($rootScope, $scope, $state, $http, $interval) { var dNow = new Date(); var dThen = parseShortDatetime(obj.Time); data_item.age = dThen.getTime(); - data_item.time = deltaTimeString(dNow - dThen) + " old"; + var diff_ms = Math.abs(dThen - dNow); + + // If time is more than two days away, don't attempt to display data age. + if (diff_ms > (1000*60*60*24*2)) { + data_item.time = "?"; + } else if (dThen > dNow) { + data_item.time = deltaTimeString(dThen - dNow) + " from now"; + } else { + data_item.time = deltaTimeString(dNow - dThen) + " old"; + } + // data_item.received = utcTimeString(obj.LocaltimeReceived); data_item.data = obj.Data; } diff --git a/web/plates/settings.html b/web/plates/settings.html index 44817a0c..52d5dc77 100755 --- a/web/plates/settings.html +++ b/web/plates/settings.html @@ -28,12 +28,6 @@ -
- -
- -
-
@@ -92,6 +86,13 @@ +
+ +
+ + +
+
@@ -127,6 +128,19 @@ + +
+
+
+
Developer Options
+
+
+

Coming soon

+
+
+
+
+