This commit is contained in:
Alex 2024-10-20 13:23:16 -03:00
commit cc38077527
26 changed files with 2446 additions and 0 deletions

109
Makefile Normal file
View File

@ -0,0 +1,109 @@
# Makefile for installation of echomav_deploy on EchoPilot AI hardware
# This is installation of mavlink-router, cockpit and network setup
.DEFAULT_GOAL := default
SHELL := /bin/bash
SUDO := $(shell test $${EUID} -ne 0 && echo "sudo")
.EXPORT_ALL_VARIABLES:
SYSCFG = /etc/mavlink-router
CONFIG ?= /var/local
LIBSYSTEMD=/lib/systemd/system
SERVICES=mavlink-router.service temperature.service
DRY_RUN=false
LOCAL=/usr/local
LOCAL_SCRIPTS=temperature.sh cockpitScript.sh
.PHONY = enable install see uninstall static default no-static
default:
@$(MAKE) --no-print-directory install
@$(MAKE) --no-print-directory static
explicit-ip:
@$(MAKE) --no-print-directory install
@$(SUDO) ./static-network.sh -i eth0 -a $(ip)
no-static:
@$(MAKE) --no-print-directory install
disable:
@( for c in stop disable ; do $(SUDO) systemctl $${c} $(SERVICES) ; done ; true )
enable:
@( for c in stop disable ; do $(SUDO) systemctl $${c} $(SERVICES) ; done ; true )
@( for s in $(SERVICES) ; do $(SUDO) install -Dm644 $${s%.*}.service $(LIBSYSTEMD)/$${s%.*}.service ; done ; true )
@if [ ! -z "$(SERVICES)" ] ; then $(SUDO) systemctl daemon-reload ; fi
@( for s in $(SERVICES) ; do $(SUDO) systemctl enable $${s%.*} ; done ; true )
@echo ""
@echo "Service is installed. To run now use sudo systemctl start mavlink-router and sudo systemctl start cockpit.socket"
@echo "Inspect output with sudo journalctl -fu mavlink-router"
@echo ""
static:
# set up static ip address on eth0
@$(SUDO) ./static-network.sh -i eth0 -a auto
install:
# install helper apps
# @$(SUDO) rm -r /var/lib/apt/lists/*
@$(SUDO) apt update
@$(SUDO) apt install -y nano
@$(SUDO) apt install -y nload
@$(SUDO) apt install -y htop
@$(SUDO) apt install -y picocom
# install mavlink-router
@rm -rf ~/tmp/mavlink-router-source
@git clone https://github.com/EchoMAV/mavlink-router-src ~/tmp/mavlink-router-source && cd ~/tmp/mavlink-router-source && git submodule update --init --recursive
@$(SUDO) apt -y install git ninja-build pkg-config gcc g++ systemd
@$(SUDO) apt -y install python3-pip
@$(SUDO) pip3 install meson smbus
@cd ~/tmp/mavlink-router-source && meson setup build . && $(SUDO) ninja -C build install
# install the config file
@$(SUDO) mkdir -p $(SYSCFG)
@$(SUDO) cp main.conf $(SYSCFG)/.
# install cockpit
@$(SUDO) ./ensure-cockpit.sh
# set up cockpit files
@$(SUDO) rm -rf /usr/share/cockpit/general/
@$(SUDO) mkdir /usr/share/cockpit/general/
@$(SUDO) cp -rf ui/general/* /usr/share/cockpit/general/
@$(SUDO) cp -rf ui/branding-ubuntu/* /usr/share/cockpit/branding/ubuntu/
@$(SUDO) cp -rf ui/static/* /usr/share/cockpit/static/
@$(SUDO) cp -rf ui/base1/* /usr/share/cockpit/base1/
@[ -d $(LOCAL)/echopilot ] || $(SUDO) mkdir $(LOCAL)/echopilot
@$(SUDO) install -Dm755 version.txt $(LOCAL)/echopilot/.
@for s in $(LOCAL_SCRIPTS) ; do $(SUDO) install -Dm755 $${s} $(LOCAL)/echopilot/$${s} ; done
# stop any running services we care about
@for c in stop disable ; do $(SUDO) systemctl $${c} $(SERVICES) ; done ; true
# install and enable services
@for s in $(SERVICES) ; do $(SUDO) install -Dm644 $${s%.*}.service $(LIBSYSTEMD)/$${s%.*}.service ; done
@if [ ! -z "$(SERVICES)" ] ; then $(SUDO) systemctl daemon-reload ; fi
@for s in $(SERVICES) ; do $(SUDO) systemctl enable $${s%.*} ; done
# set up the system permissions, stop/disable nvgetty etc
@$(SUDO) systemctl stop nvgetty
@$(SUDO) systemctl disable nvgetty
@$(SUDO) usermod -aG dialout $${USER}
@$(SUDO) usermod -aG tty $${USER}
@echo ""
see:
$(SUDO) cat $(SYSCFG)/main.conf
serial:
-@$(SUDO) python3 serial_number.py 0 || true
-@$(SUDO) python3 serial_number.py 1 || true
uninstall:
@$(MAKE) --no-print-directory disable
@( for s in $(SERVICES) ; do $(SUDO) rm $(LIBSYSTEMD)/$${s%.*}.service ; done ; true )
@if [ ! -z "$(SERVICES)" ] ; then $(SUDO) systemctl daemon-reload ; fi

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# EchoMAV EchoPilot AI Deploy
This is the stnadard deployment package used on Jetson modules installed on the EchoPilot AI hardware.
A makefile is included which will install mavlink-router, cockpit and set the device up with a static ip address.
- mavlink-router is an open source tool to route mavlink messages across various types of endpoints. On the EchoPilot AI, it is used to accept serial data from the autopilot and act as a client or server using either UDP or TCP for packets.
- cockpit is web-based graphical user interface, allowing you to manage the system. As configured in this install, a MAVLink-Router configuration page allows you to use a simple to use web user interface to configure mavlink-router. Simply access the system using a web browser at http://IP_ADDRESS.
- A shell script is included which will configure a unique static IP address for the system based on the network adapter's MAC address.
- Other helpful applications are installed, including nano, htop and nload.
## Steps to install
1. Given a freshly flashed image, gain console access via USB, e.g. `picocom /dev/ttyUSB0 -b 115200` on Linux.
2. Ensure that one of the EchoPilot AI's network ports is plugged into a router providing a DHCP address and Internet access.
3. Clone and use `make` to install the software
usage:
```
git clone https://github.com/echomav/echopilot_deploy.git /tmp/echopilot_deploy && cd /tmp/echopilot_deploy && make
# If you do not want to configure a static IP, then use make no-static
```
4. Record and label the device with the static IP address generated by the script, as you will need this to access the device over the network.
5. Using a host computer on the appropriate subnet (e.g., 10.223), access the webUI at http://IP_ADDRESS. https will also work.
6. You may also use ssh for terminal acces, e.g. `ssh echopilot@IP_ADDRESS`
Using the web user interface, you can now configure mavlink-router endpoints. The most common scenario is UDP Client pushing data to the ground control system computer.

49
cockpitScript.sh Normal file
View File

@ -0,0 +1,49 @@
#!/bin/bash
# Only used by cockpit to get some information from the filesystem
function ApnChange
{
echo "Removing existing network manager profile for attcell..."
nmcli con delete 'attcell'
echo "Adding network manager profile for attcell..."
nmcli connection add type gsm ifname cdc-wdm0 con-name "attcell" apn "$1" connection.autoconnect yes
}
while [[ $# -gt 0 ]]; do
key="$1"
shift
shift
case $key in
-a)
ApnChange $1
exit 0
;;
-d)
ls /dev/ | grep video
exit 0
;;
-s)
ls /dev/ | grep ttyTH | sed -e "s/.*/\/dev\/&/"
exit 0
;;
-i)
basename -a /sys/class/net/*
exit 0
;;
-c)
nmcli con show attcell | grep gsm.apn | cut -d ":" -f2 | xargs
exit 0
;;
-v)
cat /usr/local/echopilot/version.txt
exit 0
;;
-u)
hostname -I | awk '{print $1}' | cut -d'.' -f1,2
exit 0
;;
esac
exit 0
done

20
ensure-cockpit.sh Normal file
View File

@ -0,0 +1,20 @@
#!/bin/bash
# usage:
# ensure-cockpit.sh
#
# This script ensures that cockpit it installed and setup
DRY_RUN=false
LOCAL=/usr/local
SUDO=$(test ${EUID} -ne 0 && which sudo)
$SUDO apt-get install -y cockpit
# Change the port to 443/80 and restart
$SUDO sed -i 's/9090/443/g' /lib/systemd/system/cockpit.socket
$SUDO sed -i '/ListenStream=80/d' /lib/systemd/system/cockpit.socket
$SUDO sed -i '/ListenStream=443/a ListenStream=80' /lib/systemd/system/cockpit.socket
$SUDO systemctl daemon-reload
$SUDO systemctl restart cockpit.socket

58
main.conf Normal file
View File

@ -0,0 +1,58 @@
# EchoPilot AI mavlink-router configuration file.
# Note, this file may be auto-generated depending on your configuration.
[General]
# this section only has one instance and therefore no name
# Print traffic statistics to stdout
# Default: <false>
#ReportStats = false
# Logging verbosity (stderr)
# Valid values: <error>, <warning>, <info> or <debug>
# Default: <info>
DebugLogLevel = info
# Enable de-duplication of incoming messages. If a message is received another
# time in the configured time period (in milliseconds), it will be dropped. The
# second message will reset the timer.
# It should be a bit higher, than the latency of the slowest link. More than
# 1000 ms might drop packets unintentionally though.
# Default: 0 (de-duplication disabled)
#DeDuplicationPeriod = 0
## TCP Server Endpoints
# Listen for TCP connections on this port. Set to 0 to disable.
# Default: 5760
TcpServerPort = 5760
##
## UART Endpoint Configurations
## Duplicate sections (with a different name) to create multiple endpoints
##
## EchoPilot UART input. Please ensure the autopilot is configured to output MAVLink2 on SERIAL2 at 500,000 kbps. On most systems this will be /dev/ttyTHS0, but could be /dev/ttyTHS1
## Do not change the name of the endpoint as it may interfere with the web configuration UI
[UartEndpoint alpha]
Device = /dev/ttyTHS0
Baud = 500000
##
## UDP Endpoint Configuration Alpha
##
## send to <IP ADDRESS>:14550. This should be the IP addres of your host computer running ground control software (QGCS or Mission Planner)
## Do not change the name of the endpoint as it may interfere with the web configuration UI
[UdpEndpoint alpha]
Mode = Normal
Address = 10.223.1.10
Port = 14550
##
## TCP Client Endpoint Configuration Alpha
##
## Do not change the name of the endpoint as it may interfere with the web configuration UI
#[TcpEndpoint alpha]
#Address = 10.223.1.10
#Port = 5760

15
mavlink-router.service Normal file
View File

@ -0,0 +1,15 @@
[Unit]
Description=mavlink-router
After=network.target multi-user.target
RequiresMountsFor=/etc /usr
[Service]
ExecStart=/usr/bin/mavlink-routerd
StandardOutput=journal
StandardError=journal
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,4 @@
[Allow users to manage services]
Identity=unix-group:h31
Action=org.freedesktop.systemd1.manage-units
ResultActive=yes

54
scripts/create-update.sh Normal file
View File

@ -0,0 +1,54 @@
#!/bin/bash
# Use this script to create a .mender file to drop into cockpit
dest_dir=""
filename=""
version=""
machine="jetson-nano-devkit"
mkdir -p update-artifacts/
help() {
bn= `basename $0`
echo " Usage: $bn <options> "
echo
echo " options: "
echo " -h display the Help Message"
echo " -n update package name, README should have available names"
echo
echo " Example: $bn create-update.sh -n h31-proxy"
echo
echo
}
create_h31_proxy_update() {
dest_dir="/usr/local/h31/h31proxy/"
filename="bin/h31proxy.net"
version="1.0"
artifactname="$filename-$version"
outputpath="update-artifacts/h31proxy.mender"
./scripts/single-file-artifact-gen \
-n ${artifactname} \
-t ${machine} \
-d ${dest_dir} \
-o ${outputpath} \
${filename}
}
moreoptions=1
while [ "$moreoptions" = 1 -a $# -gt 0 ]; do
case $1 in
-n) shift ; UPDATE_NAME=${1} ;;
-h) help ; exit 3 ;;
*) help ; exit 3 ;;
esac
[ "$moreoptions" = 0 ] && [ $# -gt 1 ] && help && exit 1
[ "$moreoptions" = 1 ] && shift
done
if [ $UPDATE_NAME == "h31proxy" ]
then
create_h31_proxy_update
fi

340
scripts/get-mender.sh Normal file
View File

@ -0,0 +1,340 @@
#!/bin/bash
set -e
CHANNEL="stable"
# Each available component shall be in only one of the lists below
AVAILABLE_COMPONENTS="\
mender-client \
mender-configure \
mender-configure-demo \
mender-configure-timezone \
mender-connect \
mender-monitor \
mender-monitor-demo \
"
DEFAULT_COMPONENTS="\
mender-client \
mender-configure \
mender-connect \
"
DEMO_COMPONENTS="\
mender-configure-demo \
mender-configure-timezone \
"
COMMERCIAL_COMPONENTS="\
mender-monitor \
"
COMMERCIAL_DEMO_COMPONENTS="\
mender-monitor-demo \
"
SELECTED_COMPONENTS="$DEFAULT_COMPONENTS"
DEMO="0"
# URL prefix from where to download commercial compoments
MENDER_COMMERCIAL_DOWNLOAD_URL="https://downloads.customer.mender.io/content/hosted/"
# URL path for the actual components, formatted by version
declare -A COMMERCIAL_COMP_TO_URL_PATH_F=(
[mender-monitor]="mender-monitor/debian/%s/mender-monitor_%s-1_all.deb"
[mender-monitor-demo]="mender-monitor/debian/%s/mender-monitor-demo_%s-1_all.deb"
)
export DEBIAN_FRONTEND=noninteractive
banner (){
echo "
_
_ __ ___ ___ _ __ __| | ___ _ __
| '_ \` _ \ / _ \ '_ \ / _\` |/ _ \ '__|
| | | | | | __/ | | | (_| | __/ |
|_| |_| |_|\___|_| |_|\__,_|\___|_|
Running the Mender installation script.
--
"
}
usage() {
echo "usage: install-mender.sh [options] [component...] [-- [options-for-mender-setup] ]"
echo ""
echo "options: [-h, help] [-c channel] [--demo] [--commercial]"
echo " -h, --help print this help"
echo " -c CHANNEL channel: stable(default)|experimental"
echo " --demo use defaults appropriate for demo"
echo " --commercial install commercial components, requires --jwt-token"
echo " --jwt-token TOKEN Hosted Mender JWT token"
echo ""
echo "If no components are specified, defaults will be installed"
echo ""
echo "Anything after a '--' gets passed directly to 'mender setup' command."
echo ""
echo "Supported components (x = installed by default):"
for c in $AVAILABLE_COMPONENTS; do
if echo "$DEFAULT_COMPONENTS" | egrep -q "(^| )$c( |\$)"; then
echo -n " (x) "
else
echo -n " (-) "
fi
echo "$c"
done
}
is_known_component() {
for known in $AVAILABLE_COMPONENTS; do
if [ "$1" = "$known" ]; then
return 0
fi
done
return 1
}
parse_args() {
local selected_components=""
local args_copy="$@"
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
usage
exit 0
;;
-c)
if [ -n "$2" ]; then
CHANNEL="$2"
shift
else
echo "ERROR: channel requires a non-empty option argument."
echo "Aborting."
exit 1
fi
;;
--demo)
DEMO="1"
SELECTED_COMPONENTS="$SELECTED_COMPONENTS $DEMO_COMPONENTS"
;;
--commercial)
if [[ ! "$args_copy" == *"--jwt-token"* ]]; then
echo "ERROR: commercial requires --jwt-token argument."
echo "Aborting."
exit 1
fi
SELECTED_COMPONENTS="$SELECTED_COMPONENTS $COMMERCIAL_COMPONENTS"
if [[ "$args_copy" == *"--demo"* ]]; then
SELECTED_COMPONENTS="$SELECTED_COMPONENTS $COMMERCIAL_DEMO_COMPONENTS"
fi
;;
--jwt-token)
if [ -n "$2" ]; then
JWT_TOKEN="$2"
shift
else
echo "ERROR: jwt-token requires a non-empty option argument."
echo "Aborting."
exit 1
fi
;;
--)
shift
MENDER_SETUP_ARGS="$@"
break
;;
*)
if is_known_component "$1"; then
if echo "$COMMERCIAL_COMPONENTS" | egrep -q "(^| )$1( |\$)"; then
if [[ ! "$args_copy" == *"--jwt-token"* ]]; then
echo "ERROR: $1 requires --jwt-token argument."
echo "Aborting."
exit 1
fi
fi
selected_components="$selected_components $1 "
else
echo "Unsupported argument: \`$1\`"
echo "Run \`mender-install.sh -h\` for help."
echo "Aborting."
exit 1
fi
;;
esac
shift
done
if [ -n "$selected_components" ]; then
SELECTED_COMPONENTS="$selected_components"
fi
}
print_components() {
echo " Selected components:"
for c in $SELECTED_COMPONENTS; do
printf "\t%s\n" "$c"
done
}
init() {
REPO_URL=https://downloads.mender.io/repos/debian
parse_args "$@"
ARCH=$(dpkg --print-architecture)
echo " Detected architecture:"
printf "\t%s\n" "$ARCH"
echo " Installing from channel:"
printf "\t%s\n" "$CHANNEL"
}
get_deps() {
apt-get update -qq
apt-get install -y -qq --no-install-recommends \
apt-transport-https \
ca-certificates \
curl \
gnupg
}
add_repo() {
curl -fsSL $REPO_URL/gpg | apt-key add -
repo="deb [arch=$ARCH] $REPO_URL $CHANNEL main"
echo "Checking if mender sources already exist in '/etc/apt/sources.list'..."
if grep -F "$repo" /etc/apt/sources.list; then
echo "Removing the old mender debian source list from /etc/apt/sources.list..."
if ! sed -i.bak -e "\,$REPO_URL,d" /etc/apt/sources.list; then
echo "Failed to remove the existing mender debian source from '/etc/apt/sources.list'."
echo "This probably means that there already exists a source in your sources.list."
echo "Please remove it manually before proceeding."
exit 1
fi
fi
echo "$repo" > /etc/apt/sources.list.d/mender.list
}
do_install_open() {
# Filter out commercial components
local selected_components_open=""
for c in $SELECTED_COMPONENTS; do
if ! echo "$COMMERCIAL_COMPONENTS $COMMERCIAL_DEMO_COMPONENTS" | egrep -q "(^| )$c( |\$)"; then
selected_components_open="$selected_components_open $c"
fi
done
# Return if no open source components selected
if [ -z "$selected_components_open" ]; then
return
fi
echo " Installing open source components from APT repository"
apt-get update
apt-get install -y \
-o Dpkg::Options::="--force-confdef" \
-o Dpkg::Options::="--force-confold" \
$selected_components_open
echo " Success! Please run \`mender setup\` to configure the client."
}
do_install_commercial() {
# Filter commercial components
local selected_components_commercial=""
for c in $SELECTED_COMPONENTS; do
if echo "$COMMERCIAL_COMPONENTS $COMMERCIAL_DEMO_COMPONENTS" | egrep -q "(^| )$c( |\$)"; then
selected_components_commercial="$selected_components_commercial $c"
fi
done
# Return if no commercial components selected
if [ -z "$selected_components_commercial" ]; then
return
fi
echo " Installing commercial components from $MENDER_COMMERCIAL_DOWNLOAD_URL"
# Translate Debian "channel" into Mender version
if [ "$CHANNEL" = "experimental" ]; then
version="master"
else
version="latest"
fi
# Download deb packages
for c in $selected_components_commercial; do
url="$MENDER_COMMERCIAL_DOWNLOAD_URL$(printf ${COMMERCIAL_COMP_TO_URL_PATH_F[$c]} $version $version)"
curl -fLsS -H "Authorization: Bearer $JWT_TOKEN" -O "$url" ||
(echo ERROR: Cannot get $c from $url; exit 1)
done
# Install all of them at once and fallback to install missing dependencies
local deb_packages_glob=$(echo $selected_components_commercial | sed -e 's/ /*.deb /g; s/$/*.deb/')
dpkg --install $deb_packages_glob || apt-get -f -y install
# Check individually each package
for c in $selected_components_commercial; do
dpkg --status $c || (echo ERROR: $c could not be installed; exit 1)
done
echo " Success!"
}
do_setup_mender() {
# Return if mender-client was not installed
if [[ ! "$SELECTED_COMPONENTS" == *"mender-client"* ]]; then
return
fi
# Return if no setup options were passed
if [ -z "$MENDER_SETUP_ARGS" ]; then
return
fi
echo " Setting up mender with options: $MENDER_SETUP_ARGS"
mender setup $MENDER_SETUP_ARGS
pidof systemd && systemctl restart mender-client
echo " Success!"
}
do_setup_addons() {
# Setup for mender-connect
if [[ "$SELECTED_COMPONENTS" == *"mender-connect"* ]]; then
if [ "$DEMO" -eq 1 ]; then
echo " Setting up mender-connect with user 'root' and shell 'bash'"
cat > /etc/mender/mender-connect.conf << EOF
{
"User": "root",
"ShellCommand": "/bin/bash"
}
EOF
pidof systemd && systemctl restart mender-connect
echo " Success!"
fi
fi
}
do_install_missing_monitor_dirs () {
if [[ "$SELECTED_COMPONENTS" == *"mender-monitor-demo"* ]]; then
mkdir -p /etc/mender-monitor/monitor.d/enabled || true
mkdir -p /etc/mender-monitor/monitor.d/available || true
fi
}
banner
init "$@"
print_components
get_deps
add_repo
do_install_open
do_install_commercial
do_setup_mender
do_setup_addons
do_install_missing_monitor_dirs
exit 0

View File

@ -0,0 +1,197 @@
#!/bin/sh
set -e
show_help() {
cat << EOF
Simple tool to generate Mender Artifact suitable for single-file Update Module
Usage: $0 [options] file [-- [options-for-mender-artifact] ]
Options: [ -n|artifact-name -t|--device-type -d|--dest-dir --software-name --software-version --software-filesystem -o|--output_path -h|--help ]
--artifact-name - Artifact name
--device-type - Target device type identification (can be given more than once)
--dest-dir - Target destination directory where to deploy the update
--software-name - Name of the key to store the software version: rootfs-image.NAME.version,
instead of rootfs-image.single-file.version
--software-version - Value for the software version, defaults to the name of the artifact
--software-filesystem - If specified, is used instead of rootfs-image
--output-path - Path to output file. Default: file-install-artifact.mender
--help - Show help and exit
file - Single file to bundle in the update
Anything after a '--' gets passed directly to the mender-artifact tool.
EOF
}
show_help_and_exit_error() {
show_help
exit 1
}
check_dependency() {
if ! which "$1" > /dev/null; then
echo "The $1 utility is not found but required to generate Artifacts." 1>&2
return 1
fi
}
if ! check_dependency mender-artifact; then
echo "Please follow the instructions here to install mender-artifact and then try again: https://docs.mender.io/downloads#mender-artifact" 1>&2
exit 1
fi
device_types=""
artifact_name=""
dest_dir=""
output_path="single-file-artifact.mender"
file=""
passthrough_args=""
while [ -n "$1" ]; do
case "$1" in
--device-type | -t)
if [ -z "$2" ]; then
show_help_and_exit_error
fi
device_types="$device_types $1 $2"
shift 2
;;
--artifact-name | -n)
if [ -z "$2" ]; then
show_help_and_exit_error
fi
artifact_name=$2
shift 2
;;
--dest-dir | -d)
if [ -z "$2" ]; then
show_help_and_exit_error
fi
dest_dir=$2
shift 2
;;
--software-name | --software-version | --software-filesystem)
if [ -z "$2" ]; then
show_help_and_exit_error
fi
passthrough_args="$passthrough_args $1 $2"
shift 2
;;
--output-path | -o)
if [ -z "$2" ]; then
show_help_and_exit_error
fi
output_path=$2
shift 2
;;
-h | --help)
show_help
exit 0
;;
--)
shift
passthrough_args="$@"
break
;;
-*)
echo "Error: unsupported option $1"
show_help_and_exit_error
;;
*)
if [ -n "$file" ]; then
echo "File already specified. Unrecognized argument \"$1\""
show_help_and_exit_error
fi
file="$1"
shift
;;
esac
done
if [ -z "${artifact_name}" ]; then
echo "Artifact name not specified. Aborting."
show_help_and_exit_error
fi
if [ -z "${device_types}" ]; then
echo "Device type not specified. Aborting."
show_help_and_exit_error
fi
if [ -z "${dest_dir}" ]; then
echo "Destination dir not specified. Aborting."
show_help_and_exit_error
fi
if [ -z "${file}" ]; then
echo "File not specified. Aborting."
show_help_and_exit_error
fi
# Check dest-dir is an absolute path
case $dest_dir in
/*)
;;
*)
echo "Destination dir must be an absolute path. Aborting"
exit 1
;;
esac
# Create tarball, accepts single file
filename=""
if [ -e "${file}" ]; then
if [ -f "${file}" ]; then
filename=$(basename $file)
else
echo "Error: \"${file}\" is not a regular file. Aborting."
exit 1
fi
else
echo "Error: File \"${file}\" does not exist. Aborting."
exit 1
fi
# Create required files for the Update Module
tmpdir=$(mktemp -d)
dest_dir_file="$tmpdir/dest_dir"
filename_file="$tmpdir/filename"
permissions_file="$tmpdir/permissions"
# Create dest_dir file in plain text
echo "$dest_dir" > $dest_dir_file
# Create single_file file in plain text
echo "$filename" > $filename_file
STAT_HAS_f=1;
stat -f %A "${file}" >/dev/null 2>&1 || STAT_HAS_f=0;
# Create permissions file in plain text
if [ $STAT_HAS_f -eq 1 ]; then
stat -f %A "${file}" > $permissions_file
else
stat -c %a "${file}" > $permissions_file
fi
mender-artifact write module-image \
-T single-file \
$device_types \
-o "$output_path" \
-n "$artifact_name" \
-f "$dest_dir_file" \
-f "$filename_file" \
-f "$permissions_file" \
-f "$file" \
$passthrough_args
rm -rf $tmpdir
echo "Artifact $output_path generated successfully:"
mender-artifact read $output_path
exit 0

30
serial_number.py Normal file
View File

@ -0,0 +1,30 @@
import smbus
import sys
# usage, pass the i2c bus as the first argument, e.g. python3 serial_number 0
i2c_ch = int(sys.argv[1])
# address on the I2C bus
i2c_address = 0x58
# Register address
serial_num = 0x80
# Read serial number register
def read_serial():
# Read the serial register, a 16 byte block
val = bus.read_i2c_block_data(i2c_address, serial_num, 16)
return val
# Initialize I2C (SMBus)
bus = smbus.SMBus(i2c_ch)
try:
# Print out the serial number
print(bytes(read_serial()).hex())
except:
pass

149
static-network.sh Normal file
View File

@ -0,0 +1,149 @@
#!/bin/bash
# EchoMAV, LLC
# This script sets up a static network on the EchoPilot AI using NetworkManager (nmcli)
# usage: static-network.sh -i {interface} -a {ip_addres|auto|dhcp} -g {gateway(optional)}
# If auto is used, the static IP address will be set to 10.223.x.y where x and y are the last two octects of the network interface mac address
# The first two octets cab be changed per IP_PREFIX
# An alias is also added to the interface with the value of BACKDOOR_ADDR
IP_PREFIX="10.223"
BACKDOOR_ADDR="192.168.154.0/24"
sigterm_handler() {
echo "Shutdown signal received."
exit 1
}
## Setup signal trap
trap 'trap " " SIGINT SIGTERM SIGHUP; kill 0; wait; sigterm_handler' SIGINT SIGTERM SIGHUP
SUDO=$(test ${EUID} -ne 0 && which sudo)
function print_help { echo "Usage: ./static-network.sh -i interface_name -a ip_addres|auto -g gateway_address(optional)" >&2 ; }
# process args
while getopts :i:a:g:h: flag;
do
case "${flag}" in
h) print_help
exit 1
;;
i) IFACE=${OPTARG};;
a) IP_INPUT=${OPTARG};;
g) GATEWAY=${OPTARG};;
*) print_help
exit 1
;;
esac
done
# check mandatory arguments
if [ -z $IP_INPUT ]; then
echo 'Missing mandatory -a argument' >&2
print_help
exit 1
fi
if [ -z $IFACE ]; then
echo 'Missing mandatory -i argument' >&2
print_help
exit 1
fi
if [ $IP_INPUT = "auto" ]; then
ifconfig ${IFACE} &> /dev/null
if [ $? -ne 0 ]
then
echo "ERROR: Failed to get information for interface ${IFACE}, does it really exist?"
echo ""
echo "Here is output of ip link show:"
ip link show
exit 1
fi
echo "Determining the auto static IP address for interface ${IFACE}...";
# Get the mac address
MAC_ADDRESS=$(ifconfig ${IFACE} | awk '/ether/ {print $2}')
OCT1DEC=$((0x`ifconfig ${IFACE} | awk '/ether/ {print $2}' | awk '{split($0,a,"[:]"); print a[5]}'`))
OCT2DEC=$((0x`ifconfig ${IFACE} | awk '/ether/ {print $2}' | awk '{split($0,a,"[:]"); print a[6]}'`))
echo "MAC address for ${IFACE} is $MAC_ADDRESS";
if ! [[ $OCT1DEC =~ ^[0-9]{1,3} && $OCT2DEC =~ ^[0-9]{1,3} ]] ; then
echo "Error: Failure calculating the target IP address" >&2; exit 1
fi
HOST="$IP_PREFIX.$OCT1DEC.$OCT2DEC";
NETMASK=16;
echo "Auto-calculated IP is $HOST/$NETMASK";
elif [ $IP_INPUT = "dhcp" ]; then
#if static-iface exists, then mod to dhcp
exist=$(nmcli c show "static-$IFACE" 2>/dev/null)
if [ ! -z "$exist" ] ; then # delete the interface if it exists
$SUDO nmcli con mod "static-$IFACE" ipv4.method auto
$SUDO nmcli con mod "static-$IFACE" ipv4.gateway ""
$SUDO nmcli con mod "static-$IFACE" ipv4.addresses ""
$SUDO nmcli con down "static-$IFACE"
$SUDO nmcli con up "static-$IFACE"
else
echo "Error: connection static-$IFACE is not found. This script is only designed to convert an existing static-$IFACE to DHCP";
fi
echo "Connection static-$IFACE is now set to DHCP";
exit
else
# validate ip address
if [[ ! $IP_INPUT =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\/[0-9]{1,3}$ ]]; then
echo "ERROR! Invalid IP Address, should be x.x.x.x/y where y is the subnet mask" >&2; exit 1
fi
HOST=$(echo ${IP_INPUT} | cut -d/ -f 1);
NETMASK=$(echo ${IP_INPUT} | cut -d/ -f 2);
echo "Configuring ${IFACE} with the provided static IP address ${HOST}/${NETMASK}";
fi
# check if there is a connection called Wired connection 1", if so take it down and delete\
state=$(nmcli -f GENERAL.STATE c show "Wired connection 1" 2>/dev/null)
if [[ "$state" == *activated* ]] ; then # take the interface down
$SUDO nmcli c down "Wired connection 1"
fi
exist=$(nmcli c show "Wired connection 1" 2>/dev/null)
if [ ! -z "$exist" ] ; then # delete the interface if it exists
echo "Removing Wired connection 1..."
$SUDO nmcli c delete "Wired connection 1"
fi
# check if there is already an interface called static-$IFACE, if so take down and delete
state=$(nmcli -f GENERAL.STATE c show "static-$IFACE" 2>/dev/null)
if [[ "$state" == *activated* ]] ; then # take the interface down
$SUDO nmcli c down "static-$IFACE"
fi
exist=$(nmcli c show "static-$IFACE" 2>/dev/null)
if [ ! -z "$exist" ] ; then # delete the interface if it exists
$SUDO nmcli c delete "static-$IFACE"
fi
echo "Creating new connection static-$IFACE..."
$SUDO nmcli c add con-name "static-$IFACE" ifname $IFACE type ethernet ip4 $HOST/$NETMASK
# if gateway was provided, add that info to the connection
if [[ "$GATEWAY" == *.* ]]
then
echo "Defining gateway ${GATEWAY}...";
$SUDO nmcli c mod "static-$IFACE" ifname $IFACE gw4 $GATEWAY
fi
# add backdoor ip address
$SUDO nmcli c mod "static-$IFACE" +ipv4.addresses "$BACKDOOR_ADDR"
# bring up the interface
$SUDO nmcli c up "static-$IFACE"
echo "";
echo "Static Ethernet Configuration Successful! Interface $IFACE is set to $HOST/$NETMASK"
echo ""

19
temperature.service Normal file
View File

@ -0,0 +1,19 @@
[Unit]
Description=Monitor System Temperature
After=systemd-remount-fs.service
RequiresMountsFor=/usr
[Service]
Type=simple
ExecStart=/usr/local/echopilot/temperature.sh
StandardOutput=journal
StandardError=journal
Restart=on-failure
RestartSec=30
StartLimitInterval=300
StartLimitBurst=5
TimeoutStartSec=0
TimeoutStopSec=5
[Install]
WantedBy=multi-user.target

22
temperature.sh Normal file
View File

@ -0,0 +1,22 @@
#!/bin/bash
LOG=/tmp/temperature.csv
PERIOD=10
THERMAL=/sys/devices/virtual/thermal/
n=0
echo "Logging to $LOG, $PERIOD sec period"
echo -n "date/time," >> $LOG
for d in $THERMAL/thermal_zone* ; do
echo -n "zone$n," | tee -a $LOG
n=$(($n + 1))
done
echo "" | tee -a $LOG
while true ; do
echo -n "$(date --iso-8601=seconds)," >> $LOG
for d in $THERMAL/thermal_zone* ; do
x=$(cat $d/temp)
echo -n "$x," | tee -a $LOG
done
echo "" | tee -a $LOG
sleep $PERIOD
done

7
ui/base1/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

2
ui/base1/jquery-3.7.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
body.login-pf {
background: url("bg-plain.jpg") no-repeat 50% 0;
background-size: auto;
background-color: #101010;
}
#badge {
width: 300px;
height: 200px;
background-image: url("logo.png");
background-size: contain;
background-repeat: no-repeat;
}
#brand {
font-size: 18pt;
text-transform: uppercase;
}
#brand:before {
content: "EchoPilot AI";
}
#index-brand {
content: "EchoPilot AI";
}
#index-brand:before {
content: "EchoPilot AI";
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
ui/branding-ubuntu/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

92
ui/general/general.html Normal file
View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<title>General Configuration</title>
<meta charset="utf-8">
<link href="../base1/bootstrap.min.css" type="text/css" rel="stylesheet">
<script src="../base1/cockpit.js"></script>
<script src="../base1/jquery-3.7.1.min.js"></script>
</head>
<body>
<div class="pf-c-page">
<main class="pf-c-page__main" tabindex="-1">
<section class="pf-c-page__main-section pf-m-light">
<div class="row justify-content-start">
<div class="col-lg-12 ms-3"><p class="fw-bold fs-2 mt-3 mb-0">MAVLink Telemetry Configuration</p> </div>
<div class="col-lg-12 ms-3"><p class="mt-0 mb-0">Software Version: <I><span id='version'></span></I>, Configuration File: <I><span id='file_location'></span></I></p> </div>
<div class="col-lg-12 ms-3"><p class="mt-1 me-3">Mavlink-router provides network connectivity to/from the autopilot onboard the EchoPilot AI. The settings below can be used to configure the serial port receiving data from the autopilot as well as endpoints on the network. By default, a TCP server is running on port 5760 of the EchoPilot AI. Additional endpoints can be enabled and configured below.</p></div>
<div class="row justify-content-start">
<div class="col ms-3"><p class="fw-bold fs-5 mt-3 mb-0"><img src="settings_ethernet.svg" class="me-2">FMU Setup</p><p class="fw-light fs-6">Please select the serial device and baud rate used for the input of serial telemetry into the Jetson module. Default baud rate is 500000.</p></div>
</div>
<div class="row justify-content-start">
<div class="col-2 ms-3"><label class="control-label" for="fmuDevice">Linux serial device</label></div>
<div class="col"><select id="fmuDevice"></select></div>
</div>
<div class="row justify-content-start">
<div class="col-2 ms-3 mt-3"><label class="control-label" for="baudrate">Baud Rate (bps)</label></div>
<div class="col mt-3"><select id="baudrate"></select></div>
</div>
<div class="row justify-content-start">
<div class="col ms-3"><p class="fw-bold fs-5 mt-4 mb-0"><img src="lan.svg" class="me-2">Network UDP Telemetry Endpoint</p><p class="fw-light fs-6">Configure the endpoint used for UDP telemetry. Based on the current IP address of the EchoPilot AI, this endpoint should be in the <strong><span id='ipsubnet1'></span></strong> subnet.</p>
<p class="fw-light fs-6">In Normal (client) mode, the EchoPilot AI will send messages to the target endpoint below. When a Ground Control System (GCS) detects these messages, it will start communicating back. Using Normal/Client Mode, the GCS is listening for a connection and the EchoPilot AI initiates the data flow. Most GCS software listens by default on port 14550 for UDP packets.</p>
<p class="fw-light fs-6">In Server mode, the endpoint below is configured to listen for messages from a GCS. Once data is received from a GCS, the EchoPilot AI will begin sending telemetry back to it. In this configuration, the EchoPilot AI does not know about the GCS until the GCS first starts sending packets.
</p>
</div>
<div class="row justify-content-start">
<div class="col-2 ms-3"><label class="control-label" for="udpStatus">Status</label></div>
<div class="col"><select id="udpStatus"></select></div>
</div>
<div class="row justify-content-start">
<div class="col-2 ms-3 mt-3"><label class="control-label" for="udpMode">Mode</label></div>
<div class="col mt-3"><select id="udpMode"></select></div>
</div>
<div class="row justify-content-start">
<div class="col-2 ms-3 mt-3"><label class="control-label" for="losHost">Host</label></div>
<div class="col mt-3"><input id="losHost"></div>
</div>
<div class="row justify-content-start">
<div class="col-2 ms-3 mt-3"><label class="control-label" for="losPort">Port</label></div>
<div class="col mt-3"><input id="losPort"></div>
</div>
<div class="row justify-content-start">
<div class="col ms-3"><p class="fw-bold fs-5 mt-4 mb-0"><img src="lan.svg" class="me-2">Network TCP Telemetry Endpoint</p>
<p class="fw-light fs-6">Configure the endpoint used for TCP client telemetry. Based on the current IP address of the EchoPilot AI, this endpoint should be in the <strong><span id='ipsubnet2'></span></strong> subnet.</p>
<p class="fw-light fs-6">The EchoPilot AI will send messages to the target endpoint by establishing a TCP connection to the endpoint below. When a GCS detects this connection, it will start communicating back on the socket. The GCS does not know about the EchoPilot AI until the client establishes a connection.</p>
</div>
<div class="row justify-content-start">
<div class="col-2 ms-3"><label class="control-label" for="tcpStatus">Status</label></div>
<div class="col"><select id="tcpStatus"></select></div>
</div>
<div class="row justify-content-start">
<div class="col-2 ms-3 mt-3"><label class="control-label" for="tcpHost">Host</label></div>
<div class="col mt-3"><input id="tcpHost"></div>
</div>
<div class="row justify-content-start">
<div class="col-2 ms-3 mt-3"><label class="control-label" for="tcpPort">Port</label></div>
<div class="col mt-3"><input id="tcpPort"></div>
</div>
<div class="row justify-content-start">
<div class="col-2 ms-3 mt-3"><button class="btn-primary" id="save">Save/Update</button></div>
<div class="col mt-3"><span id="result"></span></div>
</div>
<pre id="output"></pre>
</section>
</main>
</div>
<script src="general.js"></script>
</body>
</html>

401
ui/general/general.js Normal file
View File

@ -0,0 +1,401 @@
const scriptLocation = "/usr/local/echopilot/"
const confLocation = "/etc/mavlink-router/"
const version = document.getElementById("version");
const file_location = document.getElementById("file_location");
const losHost = document.getElementById("losHost");
const losPort = document.getElementById("losPort");
const tcpHost = document.getElementById("tcpHost");
const tcpPort = document.getElementById("tcpPort");
const losIface = document.getElementById("losIface");
const backupHost = document.getElementById("backupHost");
const backupPort = document.getElementById("backupPort");
const backupIface = document.getElementById("backupIface");
const fmuDevice = document.getElementById("fmuDevice");
const baudrate = document.getElementById("baudrate");
const fmuId = document.getElementById("fmuId");
const atakHost = document.getElementById("atakHost");
const atakPort = document.getElementById("atakPort");
const atakPeriod = document.getElementById("atakPeriod");
const CONFIG_LENGTH = 11;
// standard Baud rates
const baudRateArray = [ 38400, 57600, 115200, 230400, 460800, 500000, 921600 ];
const udpModeArray = [ "Normal", "Server"];
const udpStatusArray = [ "Enabled", "Disabled"];
const tcpStatusArray = [ "Enabled", "Disabled"];
const atakPeriodArray = [ "Disabled", "1", "3", "5", "10" ];
var isUdpEnabled = "Disabled";
var istcpEnabled = "Disabled";
enabled = true;
// Runs the initPage when the document is loaded
document.onload = InitPage();
// Save file button
document.getElementById("save").addEventListener("click", SaveSettings);
// This attempts to read the conf file, if it exists, then it will parse it and fill out the table
// if it fails then the values are loaded with defaults.
function InitPage() {
cockpit.script(scriptLocation + "cockpitScript.sh -v")
.then((content) => version.innerHTML=content)
.catch(error => Fail(error));
cockpit.script(scriptLocation + "cockpitScript.sh -u")
.then(function(content) {
ipsubnet1.innerHTML=content;
ipsubnet2.innerHTML=content;
})
.catch(error => Fail(error));
file_location.innerHTML = confLocation + "main.conf";
cockpit.file(confLocation + "main.conf")
.read().then((content, tag) => SuccessReadFile(content))
.catch(error => FailureReadFile(error));
}
function getValueByKey(text, sectionName, sectionLength, key){
//function to extract key values from a file with sections
//this should work if the section is commented out or not
var lines = text.split("\n");
var sectionBody = "";
for(let t = 0; t < lines.length; t++){
if (lines[t] === sectionName || lines[t] === "#" + sectionName)
{
for(let n = 0; n < sectionLength; n++){
if (lines[t].startsWith("#"))
{
sectionBody += lines[t+n+1].slice(1) + "\n";
}
else
sectionBody += lines[t+n+1] + "\n";
}
break;
}
}
if (sectionBody === "")
return null;
var regex = new RegExp("^" + key + " =(.*)$", "m");
var match = regex.exec(sectionBody);
if(match)
return match[1].trim();
else
return null;
}
function isSectionEnabled(text, sectionName)
{
var lines = text.split("\n");
for(let t = 0; t < lines.length; t++){
if (lines[t] === sectionName)
{
if (!lines[t].startsWith("#"))
{
return "Enabled";
}
return "Disabled";
break;
}
}
return "Disabled";
}
function setSectionEnabled(text, sectionName, sectionLength, enabled)
{
//if enabled, find the section and remove leading #
//if disabled, find the section and add leading #
//return content
var lines = text.split("\n");
if (enabled)
{
//do action to enable the section
for(let t = 0; t < lines.length; t++){
if (lines[t] === "#" + sectionName)
{
for(let n = 0; n < sectionLength+1; n++){
if (lines[t+n].startsWith("#"))
{
lines[t+n] = lines[t+n].slice(1);
}
}
break;
}
}
}
else
{
//do action to disable the section
for(let t = 0; t < lines.length; t++){
if (lines[t] === sectionName)
{
for(let n = 0; n < sectionLength+1; n++){
lines[t+n] = "#" + lines[t+n];
}
break;
}
}
}
return lines.join("\n");
}
function setValueByKey(text, sectionName, sectionLength, key, value)
{
//find the sectionName in the text, then replace the key value with the one specified
//return the new contents
//skip until we find section, then look forward X lines for the key/value
var lines = text.split("\n");
for(let t = 0; t < lines.length; t++){
if (lines[t] === sectionName)
{
for(let n = 0; n < sectionLength; n++){
if (lines[t+n+1].split('=')[0].trim() == key)
{
lines[t+n+1] = key + " = " + value;
break;
}
}
}
}
return lines.join("\n");
}
function SuccessReadFile(content) {
try{
//FMU Endpoint
var currentfmuDevice = getValueByKey(content, "[UartEndpoint alpha]", 2, "Device");
var currentbaudRate = getValueByKey(content, "[UartEndpoint alpha]", 2, "Baud");
cockpit.script(scriptLocation + "cockpitScript.sh -s")
.then((content) => AddDropDown(fmuDevice, content.split("\n"), currentfmuDevice))
.catch(error => Fail(error));
AddDropDown(baudrate, baudRateArray, currentbaudRate);
//UDP Telemetry
isUdpEnabled = isSectionEnabled(content, "[UdpEndpoint alpha]");
//to do, if disabled, gray out the values
var currentudpMode = getValueByKey(content, "[UdpEndpoint alpha]", 3, "Mode");
losHost.value = getValueByKey(content, "[UdpEndpoint alpha]", 3, "Address");
losPort.value = getValueByKey(content, "[UdpEndpoint alpha]", 3, "Port");
$(udpStatus).change(function() {
if ($(this).val() === null)
return;
if ($(this).val() == "Disabled") {
isUdpEnabled = "Disabled";
$(udpMode).attr("disabled", "disabled");
$(losHost).attr("disabled", "disabled");
$(losPort).attr("disabled", "disabled");
} else {
isUdpEnabled = "Enabled";
$(udpMode).removeAttr("disabled");
$(losHost).removeAttr("disabled");
$(losPort).removeAttr("disabled");
}
}).trigger("change");
AddDropDown(udpMode, udpModeArray, currentudpMode);
AddDropDown(udpStatus, udpStatusArray, isUdpEnabled);
// init state of UDP fields
if (isUdpEnabled === "Disabled")
{
$(udpMode).attr("disabled", "disabled");
$(losHost).attr("disabled", "disabled");
$(losPort).attr("disabled", "disabled");
}
//TCP Telemetry
istcpEnabled = isSectionEnabled(content, "[TcpEndpoint alpha]");
tcpHost.value = getValueByKey(content, "[TcpEndpoint alpha]", 3, "Address");
tcpPort.value = getValueByKey(content, "[TcpEndpoint alpha]", 3, "Port");
$(tcpStatus).change(function() {
if ($(this).val() === null)
return;
if ($(this).val() == "Disabled") {
istcpEnabled = "Disabled";
$(tcpHost).attr("disabled", "disabled");
$(tcpPort).attr("disabled", "disabled");
} else {
istcpEnabled = "Enabled";
$(tcpHost).removeAttr("disabled");
$(tcpPort).removeAttr("disabled");
}
}).trigger("change");
AddDropDown(tcpStatus, tcpStatusArray, istcpEnabled);
// init state of TCP fields
if (istcpEnabled === "Disabled")
{
$(tcpHost).attr("disabled", "disabled");
$(tcpPort).attr("disabled", "disabled");
}
}
catch(e){
FailureReadFile(e);
}
}
function AddPathToDeviceFile(incomingArray){
for(let t = 0; t < incomingArray.length; t++){
incomingArray[t] = "/dev/" + incomingArray[t];
}
return incomingArray;
}
function AddDropDown(box, theArray, defaultValue){
try{
for(let t = 0; t < theArray.length; t++){
var option = document.createElement("option");
option.text = theArray[t];
box.add(option);
if(defaultValue == option.text){
box.value = option.text;
}
}
}
catch(e){
Fail(e)
}
}
function FailureReadFile(error) {
// Display error message
console.log("Error : " + error.message);
output.innerHTML = "Error : " + error.message;
// TODO :: Defaults should go here.
losHost.value = "224.10.10.10";
losPort.value = "14550";
fmuId.value = "1";
atakHost.value = "239.2.3.1";
atakPort.value = "6969";
atakPeriod.value = "5";
}
function SaveSettings() {
//lets do some validation
var ipformat = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
var portformat = /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/;
var errorFlag = false;
var errorText = "";
if (!losHost.value.match(ipformat) && (isUdpEnabled==="Enabled")) {
losHost.focus();
errorText += "Error in the UDP Host Address!<br>";
errorFlag = true;
}
if (!losPort.value.match(portformat) && (isUdpEnabled==="Enabled")) {
losPort.focus();
errorText += "Error in the UDP Port Number! (0-65535 allowed)<br>";
errorFlag = true;
}
if (!tcpHost.value.match(ipformat) && (istcpEnabled==="Enabled")) {
tcpHost.focus();
errorText += "Error in the TCP Host Address, should be x.x.x.x<br>";
errorFlag = true;
}
if (!tcpPort.value.match(portformat) && (istcpEnabled==="Enabled")) {
tcpPort.focus();
errorText += "Error in the TCP Port Number! (0-65535 allowed)<br>";
errorFlag = true;
}
if (errorFlag)
{
result.style.color = "red";
result.innerHTML = errorText;
return;
}
//open the file for writing, and callback function for modification
cockpit.file(confLocation + "main.conf")
.read().then((content, tag) => SuccessReadforSaveFile(content))
.catch(error => FailureReadFile(error));
}
function SuccessReadforSaveFile(content) {
try{
//if udp is disabled, then skip those
if (isUdpEnabled == "Disabled")
{
content = setSectionEnabled(content, "[UdpEndpoint alpha]", 3, false);
}
else
{
content = setSectionEnabled(content, "[UdpEndpoint alpha]", 3, true);
content = setValueByKey(content, "[UdpEndpoint alpha]",3, "Address", losHost.value )
content = setValueByKey(content, "[UdpEndpoint alpha]",3, "Port", losPort.value )
content = setValueByKey(content, "[UdpEndpoint alpha]",3, "Mode", udpMode.value )
}
if (istcpEnabled == "Disabled")
{
content = setSectionEnabled(content, "[TcpEndpoint alpha]", 2, false);
}
else
{
content = setSectionEnabled(content, "[TcpEndpoint alpha]", 2, true);
content = setValueByKey(content, "[TcpEndpoint alpha]",2, "Address", tcpHost.value )
content = setValueByKey(content, "[TcpEndpoint alpha]",2, "Port", tcpPort.value )
}
//at this point we have the contents of the file, we need to replace keys
content = setValueByKey(content, "[UartEndpoint alpha]",2, "Device", fmuDevice.value )
content = setValueByKey(content, "[UartEndpoint alpha]",2, "Baud", baudrate.value )
cockpit.file(confLocation + "main.conf", { superuser : "try" }).replace(content)
.then(Success)
.catch(Fail);
cockpit.spawn(["systemctl", "restart", "mavlink-router"], { superuser : "try" });
}
catch(e){
FailureReadFile(e);
}
}
function Success() {
result.style.color = "green";
result.innerHTML = "Success, mavlink-router restarting...";
setTimeout(() => result.innerHTML = "", 5000);
}
function Fail(error) {
result.style.color = "red";
result.innerHTML = error.message;
}
// Send a 'init' message. This tells integration tests that we are ready to go
cockpit.transport.wait(function() { });

1
ui/general/lan.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#06c" height="36" viewBox="0 -960 960 960" width="36"><path d="M120-80v-280h120v-160h200v-80H320v-280h320v280H520v80h200v160h120v280H520v-280h120v-80H320v80h120v280H120Zm280-600h160v-120H400v120ZM200-160h160v-120H200v120Zm400 0h160v-120H600v120ZM480-680ZM360-280Zm240 0Z"/></svg>

After

Width:  |  Height:  |  Size: 325 B

11
ui/general/manifest.json Normal file
View File

@ -0,0 +1,11 @@
{
"version": 0,
"menu": {
"general": {
"label": "MAVLink Telemtry",
"path": "general.html",
"order": 1
}
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#06c" height="40" viewBox="0 -960 960 960" width="40"><path d="m680-240-56-56 182-184-182-184 56-56 240 240-240 240Zm-400 0L40-480l240-240 56 56-182 184 182 184-56 56Zm40-200q-17 0-28.5-11.5T280-480q0-17 11.5-28.5T320-520q17 0 28.5 11.5T360-480q0 17-11.5 28.5T320-440Zm160 0q-17 0-28.5-11.5T440-480q0-17 11.5-28.5T480-520q17 0 28.5 11.5T520-480q0 17-11.5 28.5T480-440Zm160 0q-17 0-28.5-11.5T600-480q0-17 11.5-28.5T640-520q17 0 28.5 11.5T680-480q0 17-11.5 28.5T640-440Z"/></svg>

After

Width:  |  Height:  |  Size: 523 B

805
ui/static/login.min.html Normal file
View File

@ -0,0 +1,805 @@
<!DOCTYPE html>
<html>
<head>
<title>Loading...</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex">
<meta insert_dynamic_content_here>
<script>
(function(e) {
var t;
try {
t = window.localStorage;
window.localStorage.removeItem("url-root");
} catch (n) {
t = window.sessionStorage;
e.warn(String(n));
}
var o;
var i = window.environment || {};
var r = i.OAuth || null;
if (r) {
if (!r.TokenParam) r.TokenParam = "access_token";
if (!r.ErrorParam) r.ErrorParam = "error_description";
}
var a = /\$\{([^}]+)\}|\$([a-zA-Z0-9_]+)/g;
function s(e) {
var t = Array.prototype.slice.call(arguments, 1);
return e.replace(a, function(e, n, o) {
return t[n || o] || "";
});
}
function l(e) {
if (window.cockpit_po) {
var t = window.cockpit_po[e];
if (t && t[1]) return t[1];
}
return e;
}
function u() {
if (!document.querySelectorAll) return;
var e = document.querySelectorAll("[translate]");
for (var t = 0; t < e.length; t++) e[t].textContent = l(e[t].textContent);
}
var c = l;
var d, f, p, w;
var v = /[?&]?([^=]+)=([^&]*)/g;
var g = null;
function m(e) {
e = e.split("+").join(" ");
var t = {};
var n;
for (;;) {
n = v.exec(e);
if (!n) break;
t[decodeURIComponent(n[1])] = decodeURIComponent(n[2]);
}
return t;
}
function y(e) {
e = e.trim();
if (e[0] == '"') e = e.substr(1, e.length - 2);
return e;
}
if (!e) e = function() {};
if (window.location.pathname.indexOf("/cockpit/") === 0 || window.location.pathname.indexOf("/cockpit+") === 0) document.documentElement.setAttribute("class", "inline");
function h(e) {
return document.getElementById(e);
}
function b(t) {
if (window.console) e.warn("fatal:", t);
h("login-again").style.display = "none";
h("login-wait-validating").style.display = "none";
if (g) {
h("login-again").href = g;
h("login-again").style.display = "block";
}
h("login").style.display = "none";
h("login-details").style.display = "none";
h("login-fatal").style.display = "block";
var n = h("login-fatal-message");
n.textContent = "";
n.appendChild(document.createTextNode(t));
}
function x(e, t) {
var n, o = h(e);
if (o) n = window.getComputedStyle(o, ":before");
if (!n) return;
var i, r = n.content;
if (r && r != "none" && r != "normal") {
i = r.length;
if ((r[0] === '"' || r[0] === "'") && i > 2 && r[i - 1] === r[0]) r = r.substr(1, i - 2);
o.innerHTML = r || t;
} else {
o.removeAttribute("class");
}
}
function k() {
function e(e, t) {
var n;
try {
n = t[e];
} catch (o) {
b(s(c("The web browser configuration prevents Cockpit from running (inaccessible $0)"), e));
throw o;
}
if (n === undefined) {
b(s(c("This web browser is too old to run Cockpit (missing $0)"), e));
return false;
}
return true;
}
return ("MozWebSocket" in window || e("WebSocket", window)) && e("XMLHttpRequest", window) && e("sessionStorage", window) && e("JSON", window) && e("defineProperty", Object) && e("console", window) && e("pushState", window.history) && e("textContent", document);
}
function T(e) {
return e.replace(/^\s+|\s+$/g, "");
}
function S(e) {
var n = document.createElement("a");
var r = document.baseURI;
var a;
if (!r) {
a = document.getElementsByTagName("base");
if (a.length > 0) r = a[0].href; else r = "/";
}
e = e || "/";
n.href = r;
if (n.pathname != "/") {
o = n.pathname.replace(/^\/+|\/+$/g, "");
t.setItem("url-root", o);
if (o && e.indexOf("/" + o) === 0) e = e.replace("/" + o, "") || "/";
}
if (e.indexOf("/=") === 0) {
i.hostname = e.substring(2);
e = "/cockpit+" + e.split("/")[1];
} else if (e.indexOf("/cockpit/") !== 0 && e.indexOf("/cockpit+") !== 0) {
e = "/cockpit";
}
f = e.split("/")[1];
d = "/" + f + "/login";
if (o) d = "/" + o + d;
w = f;
p = d;
}
function O(e, t) {
if (t === undefined) t = h("server-group").style.display === "none";
h("option-group").setAttribute("data-state", t);
if (t) {
h("server-group").style.display = "block";
h("option-caret").setAttribute("class", "caret caret-down");
h("option-caret").setAttribute("className", "caret caret-down");
} else {
h("server-group").style.display = "none";
h("option-caret").setAttribute("class", "caret caret-right");
h("option-caret").setAttribute("className", "caret caret-right");
}
}
function C() {
window.onload = null;
u();
S(window.location.pathname);
var e = i.page.title;
if (!e || f.indexOf("cockpit+=") === 0) e = i.hostname;
document.title = e;
if (f.indexOf("cockpit+=") === 0) {
h("brand").style.display = "none";
h("badge").style.visibility = "hidden";
} else {
x("badge", "");
x("brand", "Cockpit");
}
h("option-group").addEventListener("click", O);
h("server-clear").addEventListener("click", function() {
var e = h("server-field");
e.value = "";
e.focus();
});
if (!k()) return;
var n = t.getItem("authorized-default") || "";
if (n.indexOf("password") !== -1) h("authorized-input").checked = true;
var o = i["os-release"];
if (o) t.setItem("os-release", JSON.stringify(o));
var a = window.sessionStorage.getItem("logout-intent") == "explicit";
if (a) window.sessionStorage.removeItem("logout-intent");
if (r) {
h("login-details").style.display = "none";
h("login").style.display = "none";
if (a) {
E();
h("login-again").textContent = c("Login Again");
b(c("Logout Successful"));
} else {
L();
}
} else if (a) {
q();
} else {
I();
}
}
function I() {
var e = new XMLHttpRequest();
e.open("GET", d, true);
e.onreadystatechange = function() {
if (e.readyState != 4) {
return;
} else if (e.status == 200) {
F(JSON.parse(e.responseText));
} else if (e.status == 401) {
q();
} else if (e.statusText) {
b(decodeURIComponent(e.statusText));
} else if (e.status === 0) {
q();
} else {
b(s(c("$0 error"), e.status));
}
};
e.send();
}
function E() {
var e = window.location.href.split("#", 2);
g = r.URL;
if (r.URL.indexOf("?") > -1) g += "&"; else g += "?";
g += "redirect_uri=" + encodeURIComponent(e[0]);
}
function L() {
var e = document.createElement("a");
if (!r.URL) return b(c("Cockpit authentication is configured incorrectly."));
var t = m(window.location.search);
if (!window.location.search && window.location.hash) t = m(window.location.hash.slice(1));
var n, o, i;
E();
if (t[r.TokenParam]) {
if (window.sessionStorage.getItem("login-wanted")) {
e.href = window.sessionStorage.getItem("login-wanted");
S(e.pathname);
}
n = t[r.TokenParam];
h("login-wait-validating").style.display = "block";
i = new XMLHttpRequest();
i.open("GET", d, true);
i.setRequestHeader("Authorization", "Bearer " + n);
i.onreadystatechange = function() {
if (i.readyState != 4) {
return;
} else if (i.status == 200) {
F(JSON.parse(i.responseText));
} else {
o = $(i.getResponseHeader("WWW-Authenticate"), i.responseText);
if (o) J(o); else b(i.statusText);
}
};
i.send();
} else if (t[r.ErrorParam]) {
b(t[r.ErrorParam]);
} else {
window.sessionStorage.setItem("login-wanted", window.location.href);
window.location = g;
}
}
function A() {
h("error-group").style.display = "none";
h("login-error-message").textContent = "";
}
function R(e, t) {
A();
if (e) {
if (r) {
b(e);
} else {
P(t);
h("login-error-message").textContent = e;
h("error-group").style.display = "block";
}
}
}
function H(e) {
var t = h("server-field").value;
if (!t) {
R(e, false);
} else {
A();
h("login-error-message").textContent = e;
h("error-group").style.display = "block";
O(null, true);
P();
}
}
function N(e) {
var t = h("login-note");
if (e) {
t.style.display = "block";
t.textContent = e;
} else {
t.innerHTML = "&nbsp;";
}
}
function U() {
return i.page.require_host && w.indexOf("cockpit+=") === -1;
}
function z() {
R(null);
var e, n = T(h("login-user-input").value);
if (n === "") {
R(c("User name cannot be empty"));
} else if (U() && h("server-field").value === "") {
R(c("Please specify the host to connect to"));
} else {
e = h("server-field").value;
if (e) {
f = "cockpit+=" + e;
d = p.replace("/" + w + "/", "/" + f + "/");
} else {
f = w;
d = p;
}
h("server-name").textContent = e || i.hostname;
h("login-button").removeEventListener("click", z);
var o = h("authorized-input").checked ? "password" : "";
var r = h("login-password-input").value;
t.setItem("authorized-default", o);
var a = {
Authorization: "Basic " + window.btoa(W(n + ":" + r)),
"X-Authorize": o
};
M("GET", a, false);
}
}
function P(e) {
var t = i.page.connect;
var n = h("option-group").getAttribute("data-state");
h("login-wait-validating").style.display = "none";
h("login").style.visibility = "visible";
h("login").style.display = "block";
h("user-group").style.display = e ? "none" : "block";
h("password-group").style.display = e ? "none" : "block";
h("conversation-group").style.display = e ? "block" : "none";
h("login-button-text").textContent = c("Log In");
h("login-password-input").value = "";
if (U()) {
h("option-group").style.display = "none";
n = true;
} else {
h("option-group").style.display = !t || e ? "none" : "block";
}
if (!t || e) {
h("server-group").style.display = "none";
} else {
h("server-group").style.display = n ? "block" : "none";
}
h("login-button").removeAttribute("disabled");
if (!e) h("login-button").addEventListener("click", z);
}
function q() {
h("server-name").textContent = document.title;
N(c("Log in with your server user account."));
h("login-user-input").addEventListener("keydown", function(e) {
R(null);
if (e.which == 13) h("login-password-input").focus();
}, false);
var e = function(e) {
R(null);
if (e.which == 13) z();
};
h("login-password-input").addEventListener("keydown", e);
h("authorized-input").addEventListener("keydown", e);
P();
h("login-user-input").focus();
}
function J(e) {
var t = e.echo ? "text" : "password";
h("conversation-prompt").textContent = e.prompt;
var n = h("conversation-message");
var o = e.error || e.message;
if (o) {
n.textContent = o;
n.style.display = "block";
} else {
n.style.display = "none";
}
var i = h("conversation-input");
i.value = "";
if (e.default) i.value = e.default;
i.setAttribute("type", t);
i.focus();
R("");
function r() {
h("conversation-input").removeEventListener("keydown", a);
h("login-button").removeEventListener("click", r);
R(null, true);
G(e.id, h("conversation-input").value);
}
function a(e) {
R(null, true);
if (e.which == 13) {
r();
}
}
h("conversation-input").addEventListener("keydown", a);
h("login-button").addEventListener("click", r);
P(true);
}
function W(e) {
return window.unescape(encodeURIComponent(e));
}
function $(t, n) {
var o;
var i;
var r;
var a;
if (!t) return null;
o = t.split(" ");
if (o[0].toLowerCase() !== "x-conversation" && o.length != 3) return null;
a = o[1];
try {
i = window.atob(o[2]);
} catch (s) {
if (window.console) e.error("Invalid prompt data", s);
return null;
}
try {
r = JSON.parse(n);
} catch (s) {
if (window.console) e.log("Got invalid JSON response for prompt data", s);
r = {};
}
r.id = a;
r.prompt = i;
return r;
}
function M(t, n, o) {
h("login-button").setAttribute("disabled", "true");
var i = new XMLHttpRequest();
i.open("GET", d, true);
var r;
var a;
var l;
for (l in n) i.setRequestHeader(l, n[l]);
i.onreadystatechange = function() {
if (i.readyState != 4) {
return;
} else if (i.status == 200) {
var t = JSON.parse(i.responseText);
F(t);
} else if (i.status == 401) {
a = i.getResponseHeader("WWW-Authenticate");
if (a && a.toLowerCase().indexOf("x-conversation") === 0) {
r = $(a, i.responseText);
if (r) J(r); else b(c("Internal Error: Invalid challenge header"));
} else {
if (window.console) e.log(i.statusText);
if (i.statusText.indexOf("authentication-not-supported") > -1) {
var n = T(h("login-user-input").value);
b(s(c("The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available."), n));
} else if (i.statusText.indexOf("terminated") > -1) {
R(c("Authentication Failed: Server closed connection"));
} else if (i.statusText.indexOf("no-host") > -1) {
H(c("Unable to connect to that address"));
} else if (i.statusText.indexOf("unknown-hostkey") > -1) {
H(c("Refusing to connect. Hostkey is unknown"));
} else if (i.statusText.indexOf("unknown-host") > -1) {
H(c("Refusing to connect. Host is unknown"));
} else if (i.statusText.indexOf("invalid-hostkey") > -1) {
H(c("Refusing to connect. Hostkey does not match"));
} else if (o) {
R(c("Authentication failed"));
} else {
R(c("Wrong user name or password"));
}
}
} else if (i.status == 403) {
R(decodeURIComponent(i.statusText) || c("Permission denied"));
} else if (i.statusText) {
b(decodeURIComponent(i.statusText));
} else {
b(s(c("$0 error"), i.status));
}
h("login-button").removeAttribute("disabled");
};
i.send();
}
function G(e, t) {
var n = {
Authorization: "X-Conversation " + e + " " + window.btoa(W(t))
};
M("GET", n, true);
}
function X(e) {
var t = window.setTimeout(function() {
t = null;
window.location.reload(true);
}, 100);
if (e && e != window.location.href) window.location = e;
window.onbeforeunload = function() {
if (t) window.clearTimeout(t);
t = null;
};
}
function _(e) {
var t = "/" + f + "/@localhost/";
if (o) t = "/" + o + t;
var n = t + "shell/index.html";
var i = new XMLHttpRequest();
i.open("GET", t + "manifests.json", true);
i.onreadystatechange = function() {
if (i.readyState != 4) {
return;
} else if (i.status == 200) {
var t = JSON.parse(i.responseText);
var o = t ? t["base1"] : {};
if (!o["version"] || o["version"] < "119.x") {
X(n);
} else X(e);
} else {
X(n);
}
};
i.send();
}
function B(e, t, n) {
var o = 0;
while (o < e.length) {
var i = e.key(o);
if (n && i.indexOf("cockpit") !== 0) e.removeItem(i); else if (i.indexOf(t) === 0) e.removeItem(i); else o++;
}
}
function j(e) {
B(window.sessionStorage, f, true);
t.removeItem("login-data");
B(t, f, false);
var n;
if (e && e["login-data"]) {
n = JSON.stringify(e["login-data"]);
t.setItem(f + "login-data", n);
t.setItem("login-data", n);
}
if (o) t.setItem("url-root", o);
}
function F(e) {
var t = window.sessionStorage.getItem("login-wanted");
var n = h("server-field").value;
var i;
if (n && f != w) {
t = "/=" + n;
if (o) t = "/" + o + t;
}
B(window.sessionStorage, f, false);
j(e);
if (f.indexOf("cockpit+=") === 0) {
_(t);
} else {
X(t);
}
}
window.onload = C;
})(window.console);
//# sourceMappingURL=login.min.js.map
</script>
<style>
#option-group,.btn,.cross,button{cursor:pointer}
.btn,label{font-weight:600}
.btn,img{vertical-align:middle}
html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;font-size:62.5%;-webkit-tap-highlight-color:transparent}
body{margin:0;font-family:"Open Sans",Helvetica,Arial,sans-serif;font-size:12px;line-height:1.66666667;background-color:#fff}
a{background:0 0;color:#0099d3;text-decoration:none}
.btn,.btn:active{background-image:none}
a:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}
a:active,a:hover{outline:0}
a:focus,a:hover{color:#00618a;text-decoration:underline}
img{border:0}
button,input,select,textarea{font-family:inherit;margin:0;font-size:inherit;line-height:inherit}
button,input{line-height:normal}
.btn,.form-control{line-height:1.66666667}
button,select{text-transform:none}
button{-webkit-appearance:button;overflow:visible}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}
p{margin:0 0 10px}
.container{margin-right:auto;margin-left:auto;padding-left:20px;padding-right:20px}
.form-group,.row{margin-right:-20px}
.container:after,.container:before,.row:after,.row:before{content:" ";display:table}
.container:after,.row:after{clear:both}
@media (min-width:768px){.container{width:760px}
}
@media (min-width:992px){.container{width:980px}
}
@media (min-width:1200px){.container{width:1180px}
}
.row{margin-left:-20px}
.col-lg-5,.col-lg-7,.col-md-10,.col-md-2,.col-md-6,.col-sm-1,.col-sm-10,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-5,.col-sm-6,.col-sm-7,.col-xs-12{position:relative;min-height:1px;padding-left:20px;padding-right:20px}
.col-xs-12{float:left;width:100%}
@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-5,.col-sm-6,.col-sm-7{float:left}
.col-sm-12{width:100%}
.col-sm-10{width:83.33333333333334%}
.col-sm-7{width:58.333333333333336%}
.col-sm-6{width:50%}
.col-sm-5{width:41.66666666666667%}
.col-sm-3{width:25%}
.col-sm-2{width:16.666666666666664%}
.col-sm-1{width:8.333333333333332%}
.col-sm-offset-2{margin-left:16.666666666666664%}
.control-label{text-align:right}
}
@media (min-width:992px){.col-md-10,.col-md-2,.col-md-6{float:left}
.col-md-10{width:83.33333333333334%}
.col-md-6{width:50%}
.col-md-2{width:16.666666666666664%}
}
@media (min-width:1200px){.col-lg-5,.col-lg-7{float:left}
.col-lg-7{width:58.333333333333336%}
.col-lg-5{width:41.66666666666667%}
}
label{display:inline-block;margin-bottom:5px}
.form-control{height:26px;color:#333}
.form-control[type=text],.form-control[type=password]{display:block;width:100%;padding:2px 6px;font-size:12px;background-color:#fff;background-image:none;border:1px solid #bababa;border-radius:1px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}
.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}
.form-control:-moz-placeholder{color:#999;font-style:italic}
.form-control::-moz-placeholder{color:#999;font-style:italic;opacity:1}
.form-control:-ms-input-placeholder{color:#999;font-style:italic}
.form-control::-webkit-input-placeholder{color:#999;font-style:italic}
.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}
.control-label{margin-top:0;margin-bottom:0;padding-top:3px}
.form-group{margin-left:-20px}
.form-group:after{clear:both;margin-bottom:15px}
.form-group:after,.form-group:before{content:" ";display:table}
.btn{display:inline-block;margin-bottom:0;text-align:center;border:1px solid transparent;white-space:nowrap;padding:2px 6px;font-size:12px;border-radius:1px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;-webkit-box-shadow:0 2px 3px rgba(0,0,0,.1);box-shadow:0 2px 3px rgba(0,0,0,.1)}
.btn:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}
.btn:focus,.btn:hover{color:#4d5258;text-decoration:none}
.alert-danger,.btn-primary,.login-pf .container .help-block,body{color:#fff}
.btn:active{outline:0;-webkit-box-shadow:inset 0 2px 8px rgba(0,0,0,.2);box-shadow:inset 0 2px 8px rgba(0,0,0,.2)}
.btn-lg{padding:6px 10px;font-size:14px;line-height:1.33;border-radius:1px}
@-ms-viewport{width:device-width}
.btn-primary{background-color:#189ad1;background-image:-webkit-linear-gradient(top,#1cace8 0,#1998cc 100%);background-image:linear-gradient(to bottom,#1cace8 0,#1998cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff1cace8', endColorstr='#ff1998cc', GradientType=0);border-color:#267da1}
.btn-primary:active,.btn-primary:focus,.btn-primary:hover{background-color:#189ad1;background-image:none;border-color:#267da1;color:#fff}
.btn-primary:active{background-image:none}
@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;src:url(cockpit/static/fonts/OpenSans-Regular-webfont.woff) format('woff')}
@font-face{font-family:'Open Sans';font-style:normal;font-weight:700;src:url(cockpit/static/fonts/OpenSans-Bold-webfont.woff) format('woff')}
.form-control:hover{border-color:#7BB2DD}
.login-pf{height:100%}
.login-pf #brand{position:relative;top:-70px}
.login-pf #brand img{display:block;margin:0 auto;max-width:100%}
@media (min-width:768px){.login-pf #brand img{margin:0;text-align:left}
}
.login-pf #badge{display:block;margin:20px auto 70px;position:relative;text-align:center}
.login-pf .container{background-color:#181818;background-color:rgba(255,255,255,.055);clear:right;padding-bottom:40px;padding-top:20px;width:auto}
@media (min-width:768px){.login-pf #badge{float:right;margin-right:64px;margin-top:50px}
.login-pf .container{bottom:13%;padding-left:80px;position:absolute;width:100%}
}
.caret,.server-box,.spinner{position:relative}
.login-pf .container .details p:first-child{border-top:1px solid #474747;padding-top:25px;margin-top:25px}
@media (min-width:768px){.login-pf .container .login-area{border-right:0px solid #474747}
.login-pf .container .details{padding-left:40px}
.login-pf .container .details p:first-child{border-top:0;padding-top:0;margin-top:0}
}
.login-pf .container .details p{margin-bottom:2px}
.login-pf .container .control-label{font-size:13px;font-weight:400;text-align:left}
.login-pf .container .form-group:last-child,.login-pf .container .form-group:last-child .help-block:last-child{margin-bottom:0}
@-webkit-keyframes rotation{from{-webkit-transform:rotate(0)}
to{-webkit-transform:rotate(359deg)}
}
@keyframes rotation{from{transform:rotate(0)}
to{transform:rotate(359deg)}
}
.spinner{-webkit-animation:rotation .6s infinite linear;animation:rotation .6s infinite linear;border-bottom:4px solid rgba(0,0,0,.25);border-left:4px solid rgba(0,0,0,.25);border-right:4px solid rgba(0,0,0,.25);border-radius:100%;border-top:4px solid rgba(0,0,0,.75);height:24px;margin:4px 0 0;width:24px}
.alert{padding:7px 11px;margin-bottom:20px;border:2px solid transparent;border-radius:1px}
.alert-danger{background:0 0;border-color:#c00;font-weight:700}
#option-group{margin-left:-20px;margin-right:-20px}
#server-group:before{clear:both;margin-top:5px}
.cross,.inline .container .help-block{color:#000}
.login-fatal{font-size:130%}
#login-wait-validating div{float:left}
.conversation-prompt{white-space:normal;word-wrap:break-word}
.control-label{white-space:nowrap;font-size:13px}
.spinner{border-color:rgba(255,255,255,.75) rgba(255,255,255,.25) rgba(255,255,255,.25)}
.inline #badge,.inline #brand,.inline #login-details{display:none}
.inline body{background:0 0!important;color:#000}
@media (min-width:768px){.login-button-container{float:right}
}
.caret{display:inline-block;top:4px}
.caret-down{transform:rotate(90deg);-moz-transform:rotate(90deg);-webkit-transform:rotate(90deg);transform-origin:8px 8px;-moz-transform-origin:8px 8px;-webkit-transform-origin:8px 8px}
.cross{position:absolute;right:25px;top:2px;font-weight:700;font-size:14px;opacity:.7}
#option-group:hover svg,.cross:hover{opacity:1}
#option-group div{margin-left:-3px;margin-top:3px;margin-bottom:10px}
#option-group svg{opacity:.7}
#authorized-input{width:13px;height:13px;padding:0;vertical-align:bottom;margin:8px 5px 3px 0}
#login-button{padding:7px}
#login-button .spinner{display:none}
#login-button[disabled]{padding:0;background-color:#333;background-image:none;border-color:#555}
#login-button[disabled] .spinner{display:inline-block}
#login-button[disabled] #login-button-text,.hide-before:before{display:none}
@media (max-width:480px){.login-pf{display:flex;flex-direction:column-reverse;height:auto;position:relative}
.row{display:flex;flex-direction:column}
.login-pf .container{width:100%}
.login-pf #badge{max-width:33vw;margin:3rem auto;height:8rem;padding:0;background-position:50% 100%}
.container>.row>.col-sm-12{order:1;position:absolute;bottom:0;left:0;width:100%;text-align:center}
.login-pf #brand{position:static;font-size:inherit;background-position:50% 50%}
.details{text-align:center}
}
/*# sourceMappingURL=login.min.css.map */ </style>
<link href="cockpit/static/branding.css" type="text/css" rel="stylesheet">
</head>
<body class="login-pf">
<span id="badge">
</span>
<div class="container">
<div class="row">
<div class="col-sm-12">
<div id="brand" class="hide-before">
</div><!--/#brand-->
</div><!--/.col-*-->
<div id="login" class="col-sm-7 col-md-6 col-lg-5 login-area" style="visibility: hidden;">
<div role="form">
<div id="error-group" class="alert alert-danger" hidden>
<span id="login-error-message"></span>
</div>
<div id="conversation-group" class="form-group" hidden>
<div class="col-sm-12 col-md-12">
<div id="conversation-message"></div>
<label id="conversation-prompt" for="conversation-input"></label>
</div>
<div class="col-sm-12 col-md-12">
<input type="password" class="form-control" id="conversation-input">
</div>
</div>
<div id="user-group" class="form-group">
<label for="login-user-input" class="col-sm-2 col-md-2 control-label" translate>User name</label>
<div class="col-sm-10 col-md-10">
<input type="text" class="form-control" id="login-user-input" autocorrect="off" autocapitalize="none" autofocus>
</div>
</div>
<div id="password-group" class="form-group">
<label for="login-password-input" class="col-sm-2 col-md-2 control-label" translate>Password</label>
<div class="col-sm-10 col-md-10">
<input type="password" class="form-control" id="login-password-input">
</div>
<div class="col-sm-2 col-md-2"></div>
<div class="col-sm-10 col-md-10">
<input type="checkbox" class="form-control" id="authorized-input" checked style="display:none;">
<label for="authorized-input" class="control-label" translate style="display:none;">Reuse my password for privileged tasks</label>
</div>
</div>
<div id="option-group" style="display:none;">
<div class="col-sm-5 col-md-5" style="display:none;">
<i id="option-caret" class="caret caret-right" aria-hidden="true">
<svg height="16" width="16" viewBox="0 0 16 16">
<polygon fill="#ffffff" points="4,0 4,14 12,7">
</polygon>
</svg>
</i>
<span translate>Other Options</span>
</div>
</div>
<div id="server-group" class="form-group" hidden style="display:none;">
<label title="Log in to another system. Leave blank to log in to the local system." for="server-field" class="col-sm-2 col-md-2 control-label" translate>Connect to</label>
<div class="col-sm-10 col-md-10 server-box">
<input type="text" class="form-control" id="server-field">
<span class="cross" id="server-clear" aria-hidden="true">&#x274c;</span>
</div>
</div>
<div class="form-group">
<div class="col-md-3 col-sm-3 login-button-container">
<button class="btn btn-primary btn-lg col-xs-12" id="login-button">
<div class="spinner"></div>
<div id="login-button-text" translate>Log In</div>
</button>
</div>
</div>
</div>
</div><!--/.col-*-->
<div class="col-sm-5 col-md-6 col-lg-7 details" id="login-details" style="display:none;">
<p>
<label class="control-label"><span translate>Server</span>: <b id="server-name"></b></label>
</p>
<p id="login-note" class="login-note"></p>
</div><!--/.col-*-->
<div class="col-sm-5 col-md-6 col-lg-7" id="login-wait-validating" hidden>
<div class="col-sm-4">
<span class="help-block" translate>Validating authentication token</span>
</div>
<div class="col-sm-1">
<div class="spinner col-xs-15">
</div>
</div>
</div>
<div class="col-sm-12" id="login-fatal" hidden>
<span id="login-fatal-message"></span>
<a id="login-again" translate hidden>Try Again</a>
</div>
</div><!--/.row-->
</div><!--/.container-->
</body>
</html>

1
version.txt Normal file
View File

@ -0,0 +1 @@
mavlink-router (EchoMAV Fork)