├── .gitignore ├── gs ├── FPVue.key ├── gs-init.service ├── gs.service ├── custom-bonnet.conf ├── rk3566-hdmi-max-resolution-4k.dts ├── uninstall.sh ├── rk3566-dwc3-otg-role-switch.dts ├── custom-sample.conf ├── 98-rename.rules ├── rk3566-ina226-overlay.dts ├── 99-GS.rules ├── pixelpilot.yaml ├── pixelpilot_osd_osmon.json ├── install.sh ├── fan.sh ├── channel-scan.sh ├── otg-gadget.sh ├── gs.sh ├── wfb.sh ├── button-kbd.py ├── gs-init.sh ├── stream.sh ├── button.sh ├── pixelpilot_osd_simple.json ├── pixelpilot_msposd.json ├── oled.py ├── gs.conf ├── gs-applyconf.sh ├── pixelpilot_osd.json └── gsmenu.sh ├── pics └── OpenIPC.png ├── .github └── workflows │ └── autobuild.yml ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | rootfs/ 2 | *.img 3 | *.xz 4 | *.7z -------------------------------------------------------------------------------- /gs/FPVue.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhouruixi/SBC-GS/HEAD/gs/FPVue.key -------------------------------------------------------------------------------- /pics/OpenIPC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhouruixi/SBC-GS/HEAD/pics/OpenIPC.png -------------------------------------------------------------------------------- /gs/gs-init.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=OpenIPC GS init 3 | After=getty@tty1.service network.target 4 | 5 | [Service] 6 | ExecStart=/gs/gs-init.sh 7 | Type=oneshot 8 | RemainAfterExit=true 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /gs/gs.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Single Board Computer Ground Station Main Service 3 | After=network.target gs-init.service 4 | 5 | [Service] 6 | ExecStart=/gs/gs.sh 7 | Type=oneshot 8 | RemainAfterExit=true 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /gs/custom-bonnet.conf: -------------------------------------------------------------------------------- 1 | #################################### 2 | # custom config for OpenIPC bonnet # 3 | #################################### 4 | 5 | ## Button GPIO Configuration 6 | btn_pin_layout='bonnet' 7 | 8 | ## System Configuration 9 | # Enable GPIO functions 10 | dtbo_enable_list='pwm14-m0 i2c3-m0 i2c4-m0 i2c5-m0' 11 | # I2C port used by oled and ina226 12 | oled_i2c_port='3' 13 | # Use ina2xx kernel driver or not. 14 | ina226_kernel_driver='yes' 15 | -------------------------------------------------------------------------------- /gs/rk3566-hdmi-max-resolution-4k.dts: -------------------------------------------------------------------------------- 1 | /dts-v1/; 2 | 3 | / { 4 | metadata { 5 | title = "Set maximum monitor resolution to 3840x2160"; 6 | category = "misc"; 7 | description = "Set maximum monitor resolution to 4K."; 8 | compatible = "radxa,rock-3a", "radxa,rock-3b", "radxa,rock-3c", "radxa,zero3", "radxa,e25"; 9 | }; 10 | 11 | fragment@0 { 12 | target-path = "/hdmi@fe0a0000"; 13 | __overlay__ { 14 | preset_max_hdisplay = <3840>; 15 | preset_max_vdisplay = <2160>; 16 | }; 17 | }; 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /gs/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | install_dir='/gs' 4 | script_dir=$(dirname $(readlink -f $0)) 5 | cd $script_dir 6 | 7 | echo "Delete gs.service and enable wifibroadcast service" 8 | systemctl disable gs 9 | rm /etc/systemd/system/gs.service 10 | systemctl daemon-reload 11 | systemctl enable wifibroadcast.service wifibroadcast@gs.service 12 | 13 | echo "Remove udev rules" 14 | rm /etc/udev/rules.d/* 15 | 16 | echo "Remove files" 17 | rm /config/gs.conf 18 | rm /etc/NetworkManager/system-connections/* 19 | rm /etc/network/interfaces.d/wfb* 20 | rm -rf /gs 21 | 22 | echo "uninstall done, need reboot!" 23 | -------------------------------------------------------------------------------- /gs/rk3566-dwc3-otg-role-switch.dts: -------------------------------------------------------------------------------- 1 | /dts-v1/; 2 | 3 | / { 4 | 5 | metadata { 6 | title = "Set OTG to host mode and switchable"; 7 | compatible = "radxa,rock-3a", "radxa,rock-3b", "radxa,rock-3c", "radxa,zero3", "radxa,e25"; 8 | category = "misc"; 9 | exclusive = "usbdrd_dwc3-dr_mode"; 10 | description = "Set OTG to host mode and switchable"; 11 | }; 12 | 13 | fragment@0 { 14 | target = <0xffffffff>; 15 | 16 | __overlay__ { 17 | status = "okay"; 18 | dr_mode = "otg"; 19 | usb-role-switch; 20 | role-switch-default-mode = "host"; 21 | }; 22 | }; 23 | 24 | __fixups__ { 25 | usbdrd_dwc3 = "/fragment@0:target:0"; 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /gs/custom-sample.conf: -------------------------------------------------------------------------------- 1 | ## Button Configuration 2 | # Quick Button 3 | btn_q1_pin='31' 4 | btn_q2_pin='35' 5 | btn_q3_pin='37' 6 | # Custom Button 7 | btn_cu_pin='13' 8 | btn_cd_pin='15' 9 | btn_cl_pin='12' 10 | btn_cr_pin='11' 11 | btn_cm_pin='' 12 | 13 | ## LED Configuration 14 | # Red LED (recommend for record) 15 | red_led_pin='36' 16 | red_led_drive='open-drain' 17 | # Green LED (recommend for power) 18 | green_led_pin='38' 19 | green_led_drive='open-drain' 20 | # Blue LED 21 | blue_led_pin='40' 22 | blue_led_drive='open-drain' 23 | 24 | ## System Configuration 25 | # Enable GPIO functions 26 | dtbo_enable_list='uart3-m0 uart5-m1 pwm8-m0 pwm9-m0 pwm14-m0 i2c4-m0 i2c5-m0' 27 | max_resolution_4k='yes' 28 | -------------------------------------------------------------------------------- /gs/98-rename.rules: -------------------------------------------------------------------------------- 1 | # use legacy wlan name for compatibility with RubyFpv 2 | ACTION=="add", SUBSYSTEM=="net", KERNEL=="wl*", NAME="wlan0" 3 | # force name radxa zero 3w onboard wifi to wifi0 4 | ACTION=="add", SUBSYSTEM=="net", KERNEL=="wl*", ENV{ID_NET_DRIVER}=="aicwf_sdio", NAME="wifi0" 5 | # SUBSYSTEM=="net", KERNEL=="wl*", ACTION=="add", DRIVERS=="aicwf_sdio", NAME="wifi0" 6 | # rename gadget usb net to radxa0 7 | ACTION=="add", SUBSYSTEM=="net", KERNELS=="gadget", NAME="radxa0" 8 | # rename usb tethering ncm nic to usb0 9 | ACTION=="add", SUBSYSTEM=="net", ENV{ID_USB_DRIVER}=="cdc_ncm", NAME="usb0" 10 | # usb tethering rndis nic default name is usb0 11 | # ACTION=="add", SUBSYSTEM=="net", ENV{ID_USB_DRIVER}=="rndis_host", NAME="usb0" 12 | # rename usb wired nic to eth1 13 | ACTION=="add", SUBSYSTEM=="net", KERNEL=="eth*", ENV{ID_BUS}=="usb", NAME="eth1" 14 | -------------------------------------------------------------------------------- /gs/rk3566-ina226-overlay.dts: -------------------------------------------------------------------------------- 1 | /dts-v1/; 2 | // Thanks seriyps. https://seriyps.com/blog/2025/09/25/pair-ina226-and-raspberry-pi-ina2xx-kernel-driver/ 3 | / { 4 | metadata { 5 | title = "Add INA226 sensor"; 6 | compatible = "radxa,rock-3a", "radxa,rock-3b", "radxa,rock-3c", "radxa,zero3", "radxa,e25"; 7 | category = "misc"; 8 | description = "Add INA226 sensor"; 9 | }; 10 | 11 | fragment@0 { 12 | // i2c0 = "/i2c@fdd40000" 13 | // i2c1 = "/i2c@fe5a0000" 14 | // i2c2 = "/i2c@fe5b0000" 15 | // i2c3 = "/i2c@fe5c0000" 16 | // i2c4 = "/i2c@fe5d0000" 17 | // i2c5 = "/i2c@fe5e0000" 18 | target-path = "/i2c@fe5c0000"; // Target the I2C bus that corresponds to i2c3 19 | __overlay__ { 20 | #address-cells = <1>; 21 | #size-cells = <0>; 22 | ina226@40 { 23 | compatible = "ti,ina226"; 24 | reg = <0x40>; // I2C address of the INA226 from i2cdetect 25 | shunt-resistor = <100000>; // Shunt resistor value in micro-ohms (0.1 ohm) 26 | }; 27 | }; 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /gs/99-GS.rules: -------------------------------------------------------------------------------- 1 | ## wifi hotplug 2 | # Exclude wifi0 by name 3 | SUBSYSTEM=="net", KERNEL=="wl*", ACTION=="add", ENV{ID_NET_NAME}!="wifi0", RUN+="/gs/wfb.sh $name" 4 | # Exclude wlan by MAC 5 | # SUBSYSTEM=="net", KERNEL=="wl*", ACTION=="add", ATTR{address}!="98:03:cf:*:*:*", RUN+="/gs/wfb.sh $name" 6 | # Trigger only for USB WiFi 7 | # SUBSYSTEM=="net", KERNEL=="wl*", ACTION=="add", SUBSYSTEMS=="usb", RUN+="/gs/wfb.sh $name" 8 | SUBSYSTEM=="net", KERNEL=="wl*", ACTION=="remove", RUN+="/gs/wfb.sh" 9 | # 10 | # KERNEL== must be wl*, not wlx*, otherwise the network card cannot be matched. Although udevadm test shows that the script will be executed, it will not actually be executed. 11 | 12 | ## keyboard 13 | ACTION=="add", SUBSYSTEM=="input", KERNEL=="event*", ENV{ID_INPUT_KEYBOARD}=="1", RUN+="/usr/bin/systemd-run /gs/button-kbd.py $devnode" 14 | 15 | ## External disk 16 | ACTION=="add", SUBSYSTEM=="block", KERNEL=="sda1", RUN+="/gs/button.sh mount_extdisk $devnode" 17 | 18 | ## MicroSD on emmc board 19 | # ACTION=="add", SUBSYSTEM=="block", KERNEL=="mmcblk1p1", RUN+="/gs/button.sh mount_extdisk /dev/%k" 20 | -------------------------------------------------------------------------------- /gs/pixelpilot.yaml: -------------------------------------------------------------------------------- 1 | gsmenu: 2 | enabled: true 3 | gpio: 4 | # RuncamVRX 5 | # left: 13 6 | # right: 38 7 | # up: 16 8 | # down: 18 9 | # center: 11 10 | # rec: 32 11 | # RubyFpv 12 | left: 13 13 | right: 11 14 | up: 16 15 | down: 18 16 | rec: 32 17 | 18 | # enables collection of `os_mon.*` OSD facts 19 | os_sensors: 20 | cpu: true # Monitor CPU load percentage 21 | power: auto 22 | # Monitor power supply voltage and current (advanced: requires INA226 sensor to be installed) 23 | # It can be several, but normally you just install one to monitor battery 24 | # - type: ina226 # currently the only type supported 25 | # hwmon_id: hwmon3 # /sys/class/hwmon/{hwmon_id}/name should be ina226 26 | temperature: 27 | # Monitor Radxa chip temperatures, see /sys/class/thermal/thermal_zone*/ 28 | - thermal_zone: thermal_zone0 # CPU on Radxa 29 | - thermal_zone: thermal_zone1 # GPU on Radxa 30 | 31 | # OS sensors can be auto-discovered 32 | # os_sensors: 33 | # cpu: auto 34 | # power: auto 35 | # temperature: auto 36 | -------------------------------------------------------------------------------- /.github/workflows/autobuild.yml: -------------------------------------------------------------------------------- 1 | name: AutoBuild 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | # push: 8 | # branches: 9 | # - main 10 | # pull_request: 11 | # branches: 12 | # - main 13 | 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-22.04-arm 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Build 24 | run: | 25 | echo "cd code dir ${{ github.workspace }}" 26 | cd ${{ github.workspace }}/build 27 | sudo ./release.sh ${GITHUB_SHA} ${GITHUB_REF} 28 | 29 | - name: Upload test Image 30 | if: github.event_name != 'release' 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: build-artifacts 34 | path: ${{ github.workspace }}/build/*.xz 35 | 36 | - name: Upload Release Image 37 | if: github.event_name == 'release' 38 | uses: softprops/action-gh-release@v2 39 | with: 40 | tag_name: ${{env.TAG_NAME}} 41 | token: ${{ secrets.RELEASE_TOKEN }} 42 | files: ${{ github.workspace }}/build/*.xz 43 | -------------------------------------------------------------------------------- /gs/pixelpilot_osd_osmon.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "IconTplTextWidget", 5 | "name": "CPU load widget", 6 | "x": 10, 7 | "y": 30, 8 | "icon_path": "memory.png", 9 | "template": "CPU: %u IO: %u", 10 | "facts": [ 11 | {"name": "os_mon.cpu.load_total"}, 12 | {"name": "os_mon.cpu.load_iowait"} 13 | ] 14 | }, 15 | { 16 | "type": "IconTplTextWidget", 17 | "name": "Thermal sensors", 18 | "x": 10, 19 | "y": 60, 20 | "icon_path": "device_thermostat.png", 21 | "template": "CPU: %.0f⁰C, GPU: %.0f⁰C", 22 | "facts": [ 23 | {"name": "os_mon.temperature", 24 | "tags": {"name": "soc-thermal"}, 25 | "convert": "x / 1000"}, 26 | {"name": "os_mon.temperature", 27 | "tags": {"name": "gpu-thermal"}, 28 | "convert": "x / 1000"} 29 | ] 30 | }, 31 | { 32 | "type": "IconTplTextWidget", 33 | "name": "Battery usage widget", 34 | "__comment": "Make sure you have only one power sensor! Otherwise filter them by tags", 35 | "x": 10, 36 | "y": 90, 37 | "icon_path": "battery_android_frame_full.png", 38 | "template": "Power: %.2fV, %.2fA, %.2fW", 39 | "facts": [ 40 | {"name": "os_mon.power.voltage", 41 | "convert": "x / 1000"}, 42 | {"name": "os_mon.power.current", 43 | "convert": "x / 1000"}, 44 | {"name": "os_mon.power.power", 45 | "convert": "x / 1000000"} 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /gs/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | # Start install gs 6 | echo -e "\033[31mStart install gs\033[0m" 7 | 8 | install_dir='/gs' 9 | script_dir=$(dirname $(readlink -f $0)) 10 | cd $script_dir 11 | 12 | if [ "$(id -u)" -ne 0 ]; then 13 | echo -e "This script must be run as root.\033[0m" 14 | exit 1 15 | fi 16 | [ -d ${install_dir} ] || mkdir -p ${install_dir} 17 | [ "$(ls -A /etc/NetworkManager/system-connections/)" ] && rm /etc/NetworkManager/system-connections/* 18 | systemctl disable wifibroadcast.service wifibroadcast@gs.service 19 | chmod +x gs.sh wfb.sh stream.sh fan.sh button.sh button-kbd.py gs-init.sh channel-scan.sh otg-gadget.sh oled.py 20 | cp gs.sh wfb.sh stream.sh fan.sh button.sh button-kbd.py gs-applyconf.sh gs-init.sh channel-scan.sh otg-gadget.sh oled.py rk3566-dwc3-otg-role-switch.dts rk3566-hdmi-max-resolution-4k.dts rk3566-ina226-overlay.dts ${install_dir}/ 21 | cp gs.conf custom-sample.conf custom-bonnet.conf /config/ 22 | [ -d /etc/pixelpilot ] || mkdir -p /etc/pixelpilot 23 | cp pixelpilot_osd.json pixelpilot_osd_simple.json pixelpilot_msposd.json pixelpilot_osd_osmon.json /etc/pixelpilot/ 24 | cp gs.service gs-init.service /etc/systemd/system/ 25 | cp 99-GS.rules 98-rename.rules /etc/udev/rules.d/ 26 | cp ../pics/OpenIPC.png ${install_dir}/wallpaper.png 27 | systemctl enable gs-init.service 28 | systemctl enable gs.service 29 | cp FPVue.key /config/gs.key 30 | [ $(readlink -f /etc/gs.key) == "/config/gs.key" ] || ( [ -f /etc/gs.key ] && rm /etc/gs.key; ln -s /config/gs.key /etc/gs.key ) 31 | [ $(readlink -f /etc/gs.conf) == "/config/gs.conf" ] || ( [ -f /etc/gs.conf ] && rm /etc/gs.conf; ln -s /config/gs.conf /etc/gs.conf ) 32 | 33 | echo -e "\033[31m GS installation Complete, Configuration file is /config/gs.conf\033[0m" 34 | 35 | 36 | # Start install webui 37 | echo -e "\033[31mStart install WebUI\033[0m" 38 | webui_install_dir=${install_dir}/webui 39 | 40 | git clone --depth=1 https://github.com/zhouruixi/SBC-GS-WebUI.git $webui_install_dir 41 | 42 | apt -y install python3-venv python3-dev python3-pil 43 | 44 | pushd ${install_dir} 45 | python3 -m venv venv 46 | 47 | source venv/bin/activate 48 | pip install -r ${webui_install_dir}/requirements.txt 49 | # i2c oled and ina226 50 | pip install luma.oled psutil python-dotenv smbus2 51 | 52 | cat > /etc/systemd/system/webui.service << EOF 53 | [Unit] 54 | Description=SBC GS CC Edition WebUI 55 | After=network.target 56 | 57 | [Service] 58 | ExecStart=${install_dir}/venv/bin/python ${webui_install_dir}/webui.py 59 | WorkingDirectory=${webui_install_dir} 60 | Environment=PATH=${install_dir}/venv/bin:$PATH 61 | Environment=VIRTUAL_ENV=${install_dir}/venv 62 | Restart=on-failure 63 | RestartSec=5 64 | StartLimitIntervalSec=120 65 | StartLimitBurst=5 66 | 67 | [Install] 68 | WantedBy=multi-user.target 69 | EOF 70 | 71 | deactivate 72 | echo -e "\033[31m WebUI installation Complete, Configuration file is /config/gs.conf\033[0m" 73 | 74 | 75 | sync 76 | echo -e "\033[31mRestart required to take effect!\033[0m" 77 | -------------------------------------------------------------------------------- /gs/fan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source /etc/gs.conf 4 | 5 | pwmchip_path="/sys/class/pwm/pwmchip${fan_pwm_chip}" 6 | if [ ! -d $pwmchip_path ]; then 7 | echo "Need enale pwmchip${fan_pwm_chip} channel $fan_pwm_channel in rsetup!" 8 | elif [ ! -d ${pwmchip_path}/pwm${fan_pwm_channel} ]; then 9 | echo "export channel $fan_pwm_channel to pwmchip${fan_pwm_chip}" 10 | echo $fan_pwm_channel > ${pwmchip_path}/export 11 | echo "Using pwmchip${fan_pwm_chip} channel $fan_pwm_channel for fan" 12 | else 13 | echo "Using pwmchip${fan_pwm_chip} channel $fan_pwm_channel for fan" 14 | fi 15 | 16 | cd ${pwmchip_path}/pwm${fan_pwm_channel} 17 | period=$((1000000000 / $fan_pwm_frequency)) 18 | one_percent_period=$(($period / 100)) 19 | echo $period > period 20 | echo $(($period / 5)) > duty_cycle 21 | echo 1 > enable 22 | # if direct connect pwm pin to fan, need set polarity to normal 23 | echo $fan_pwm_polarity > polarity 24 | sleep 10 25 | 26 | while true; do 27 | temp_cpu=$(cat /sys/class/thermal/thermal_zone0/temp) 28 | temp_max=${temp_cpu:0:-3} 29 | echo "CPU temperature: ${temp_max}°" 30 | if [[ "$monitor_8812eu_temperature" == "yes" && -d /proc/net/rtl88x2eu && $(ls /proc/net/rtl88x2eu | wc -l) -gt 10 ]]; then 31 | for temp_file in /proc/net/rtl88x2eu/*/thermal_state; do 32 | temp_eu_info=$(head -n 1 $temp_file) 33 | temp_eu=$((${temp_eu_info##* } + ${rtl8812eu_temperature_offset})) 34 | echo "RTL8812EU temp: ${temp_eu}°" 35 | [ $temp_eu -gt $temp_max ] && temp_max=$temp_eu 36 | done 37 | fi 38 | echo "Max temperature: ${temp_max}°" 39 | target_temp_min=$(($fan_target_temperature - $fan_target_temperature_deviation)) 40 | target_temp_max=$(($fan_target_temperature + $fan_target_temperature_deviation)) 41 | duty_cycle_now=$(cat ${pwmchip_path}/pwm${fan_pwm_channel}/duty_cycle) 42 | 43 | if [ $temp_max -gt $fan_overheat_temperature ];then 44 | echo "CATION: System is overheat! fan speed up to 100%!" 45 | echo $period > ${pwmchip_path}/pwm${fan_pwm_channel}/duty_cycle 46 | echo "System overheat!" > /run/pixelpilot.msg 47 | elif [ $temp_max -gt $target_temp_max ]; then 48 | if [ $duty_cycle_now -lt $(($fan_pwm_max_duty_cycle * $one_percent_period)) ]; then 49 | echo "$temp_max is greater than ${target_temp_max}, fan speed up ${fan_pwm_step_duty_cycle}%" 50 | echo $(($duty_cycle_now + $fan_pwm_step_duty_cycle * $one_percent_period)) > ${pwmchip_path}/pwm${fan_pwm_channel}/duty_cycle 51 | else 52 | echo "$temp_max is greater than ${target_temp_max}, but max fan speed limited to ${fan_pwm_max_duty_cycle}%" 53 | fi 54 | elif [ $temp_max -lt $target_temp_min ]; then 55 | if [ $duty_cycle_now -gt $(($fan_pwm_min_duty_cycle * $one_percent_period)) ];then 56 | echo "$temp_max is less than ${target_temp_min}, fan speed down ${fan_pwm_step_duty_cycle}%" 57 | echo $(($duty_cycle_now - $fan_pwm_step_duty_cycle * $one_percent_period)) > ${pwmchip_path}/pwm${fan_pwm_channel}/duty_cycle 58 | else 59 | echo "$temp_max is less than ${target_temp_min}, but min fan speed limited to ${fan_pwm_min_duty_cycle}%" 60 | fi 61 | else 62 | echo "$temp_max is between $target_temp_min and ${target_temp_max}, Keep speed" 63 | fi 64 | echo "----------------------${date}--------------------" 65 | sleep $temperature_monitor_cycle 66 | done 67 | -------------------------------------------------------------------------------- /gs/channel-scan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | echo "start channel scan" > /run/pixelpilot.msg 5 | source /etc/gs.conf 6 | [[ "$wfb_bandwidth" == "40" ]] && wfb_bandwidth="40+" 7 | 8 | if [ -z "$1" ]; then 9 | wfb_nics=$(echo /sys/class/net/wl* | sed -r -e "s^/sys/class/net/^^g" -e "s/wifi0\s{0,1}//" -e "s/wl\*//") 10 | [ -n "$wfb_integrated_wnic" ] && wfb_nics="$wfb_integrated_wnic $wfb_nics" 11 | if [ -n "$wfb_nics" ]; then 12 | iface_name=${wfb_nics##* } 13 | else 14 | echo "No USB WiFi Found!" > /run/pixelpilot.msg 15 | exit 0 16 | fi 17 | else 18 | iface_name="$1" 19 | echo "use specified WINC ${iface_name} for scanning" 20 | if [ ! -d /sys/class/net/${iface_name} ]; then 21 | echo "no WINC $iface_name found" > /run/pixelpilot.msg 22 | exit 1 23 | fi 24 | fi 25 | 26 | # make sure WNIC in monitor mode 27 | if iw $iface_name info | grep -q monitor; then 28 | echo "$iface_name is in monitor mode, start scanning" 29 | else 30 | echo "$iface_name is not in monitor mode, set to monitor mode" 31 | ip link set $iface_name down 32 | iw dev $iface_name set monitor otherbss 33 | ip link set $iface_name up 34 | fi 35 | 36 | # get WINC phy name 37 | iface_phy=$(basename $(readlink /sys/class/net/${iface_name}/phy80211)) 38 | 39 | # RTL8812AU BUG: 40 | #* Tx set to channel 100, Rx set to channel 132 can receive data 41 | # + TX: iw dev wlxc8fe0f41d393 set channel 100 HT20 42 | # + RX: iw dev wlx08107b91b856 set channel 132 HT20 43 | #* Tx set to channel 104, Rx set to channel 136 can receive data 44 | # + TX: iw dev wlxc8fe0f41d393 set channel 104 HT20 45 | # + RX: iw dev wlx08107b91b856 set channel 136 HT20 46 | 47 | channel_available=$(iw phy $iface_phy info | grep -oP "\s*\*\s5.*\[\K\d+(?=\].*dBm)") 48 | [ $wfb_channel -gt 104 ] && channel_available=$(echo "$channel_available" | sort -nr) 49 | for channel in $channel_available; do 50 | iw dev $iface_name set channel $channel HT${wfb_bandwidth} > /dev/null 2>&1 || continue 51 | iface_start_bytes=$(grep -oP "${iface_name}:\s+\d+\s+\K\d+" /proc/net/dev) 52 | sleep 0.1 53 | iface_stop_bytes=$(grep -oP "${iface_name}:\s+\d+\s+\K\d+" /proc/net/dev) 54 | iface_receive_bytes=$(( ${iface_stop_bytes} - ${iface_start_bytes} )) 55 | echo "channel $channel bytes in 0.1s is: $iface_receive_bytes" 56 | if [ $iface_receive_bytes -ge 50 ]; then 57 | if timeout 0.2s tcpdump -i $iface_name -e 'type mgt or type data' -c 5 -s 68 -l 2>/dev/null | grep -q 'SA:57:42'; then 58 | # Incompatible with wfb cluster and aggregation mode 59 | # need judge by wfb cli api 60 | udp_start_bytes=$(grep -oP "^Udp: \K\d+" /proc/net/snmp) 61 | sleep 0.2 62 | udp_stop_bytes=$(grep -oP "^Udp: \K\d+" /proc/net/snmp) 63 | udp_receive_bytes=$(($udp_stop_bytes - $udp_start_bytes)) 64 | if [ $udp_receive_bytes -ge 30 ]; then 65 | echo "wfb channel found: channel $channel " 66 | echo "found channel: $channel" > /run/pixelpilot.msg 67 | channel_wfb_used=$channel 68 | break 69 | else 70 | echo "channel $channel is wfb but key may not matched" 71 | channel_wfb_used=$channel 72 | fi 73 | else 74 | echo " * channel $channel have traffic but not wfb" 75 | fi 76 | fi 77 | done 78 | 79 | if [ -n "${channel_wfb_used}" ]; then 80 | sed -i "s/wfb_channel='[0-9]\+'/wfb_channel='${channel_wfb_used}'/" $(readlink -f /etc/gs.conf) 81 | for nic in $wfb_nics; do 82 | iw dev $nic set channel $channel_wfb_used HT${wfb_bandwidth} 83 | done 84 | else 85 | echo "no channel found" > /run/pixelpilot.msg 86 | fi 87 | -------------------------------------------------------------------------------- /gs/otg-gadget.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 切换OTG端口为device模式 4 | if [ "$(cat /sys/kernel/debug/usb/fcc00000.dwc3/mode)" == "host" ]; then 5 | echo device > /sys/kernel/debug/usb/fcc00000.dwc3/mode || exit 1 6 | sleep 0.5 7 | fi 8 | 9 | # 挂载ConfigFS 10 | grep -q "configfs /sys/kernel/config" /proc/mounts || mount -t configfs none /sys/kernel/config 11 | 12 | # 加载 libcomposite 模块 13 | [ -d /sys/kernel/config/usb_gadget ] || modprobe libcomposite 14 | 15 | # 若存在 gadget g1 则先删除 16 | if [ -d /sys/kernel/config/usb_gadget/g1 ]; then 17 | echo ">>>>>>>>>>>>g1 is exist! delete it!" 18 | # stop adb 19 | # [ -e /var/run/adbd.pid ] && start-stop-daemon --stop --oknodo --pidfile /var/run/adbd.pid --remove-pidfile --retry 5 20 | # [ -e /dev/usb-ffs/adb ] && umount /dev/usb-ffs/adb 21 | # [ -e /dev/usb-ffs ] && rmdir /dev/usb-ffs/adb /dev/usb-ffs 22 | systemctl stop serial-getty@ttyGS0.service 23 | cd /sys/kernel/config/usb_gadget/g1 24 | echo '' > UDC 25 | #remove all links 26 | find . -type l -exec rm -v {} \; 27 | find configs -name 'strings' -exec rmdir -v {}/0x409 \; 28 | ls -d configs/* | xargs rmdir -v 29 | ls -d strings/* | xargs rmdir -v 30 | ls -d functions/* | xargs rmdir -v 31 | cd .. 32 | rmdir -v g1 33 | modprobe -r libcomposite > /dev/null 2>&1 34 | echo ">>>>>>>>>>>>>Delete success, Run script again to create the gadget!!" 35 | exit 0 36 | fi 37 | 38 | # 定义一些变量 39 | HOST_MAC="48:6f:73:74:50:43" 40 | DEVICE_MAC="42:61:64:55:53:42" 41 | MASS_FILE=/dev/mmcblk0p4 42 | [ -b /dev/mmcblk1p4 ] && MASS_FILE=/dev/mmcblk1p4 43 | # MASS_FILE=/root/usbdisk.img 44 | # 若指定块设备不存在则创建一个测试用镜像 45 | # if [ ! -e $MASS_FILE ]; then 46 | # echo "Create $MASS_FILE format with vfat for Mass Storage......" 47 | # dd if=/dev/zero of=$MASS_FILE bs=1M count=8 48 | # mkfs.vfat $MASS_FILE 49 | # fi 50 | 51 | ###############开始创建gadget################ 52 | echo ">>>>>>>>>>>>>Starting create gadget......" 53 | cd /sys/kernel/config/usb_gadget/ 54 | mkdir g1 55 | cd g1 56 | echo "0x1d6b" > "idVendor" # Linux Foundation 57 | echo "0x0104" > "idProduct" # Multifunction Composite Gadget 58 | echo "0x0100" > "bcdDevice" # v1.0.0 59 | echo "0x0200" > "bcdUSB" # USB 2.0 60 | echo "0xEF" > "bDeviceClass" 61 | echo "0x02" > "bDeviceSubClass" 62 | echo "0x01" > "bDeviceProtocol" 63 | 64 | mkdir -p strings/0x409 65 | cat /proc/device-tree/serial-number > strings/0x409/serialnumber 66 | uname -r > strings/0x409/manufacturer 67 | hostname -s > strings/0x409/product 68 | 69 | # 创建config1 70 | echo ">>>>>>>>>>>Create config1: ACM + Mass_Storage + NCM ......" 71 | mkdir -p configs/c.1/strings/0x409 72 | echo "0x80" > configs/c.1/bmAttributes 73 | echo 250 > configs/c.1/MaxPower 74 | echo "config1: ACM + Mass_Storage + NCM" > configs/c.1/strings/0x409/configuration 75 | 76 | # 创建NCM function 77 | echo ">>>>>>>>Create RNDIS ......" 78 | mkdir -p functions/ncm.usb0 79 | echo "$HOST_MAC" > functions/ncm.usb0/host_addr 80 | echo "$DEVICE_MAC" > functions/ncm.usb0/dev_addr 81 | 82 | # 创建串口function 83 | echo ">>>>>>>>Create serial ......" 84 | mkdir -p functions/acm.gs0 85 | 86 | # 创建mass_storage function 87 | echo ">>>>>>>>Create Mass_Storage ......" 88 | mkdir -p functions/mass_storage.usb0 89 | echo 1 > functions/mass_storage.usb0/stall 90 | echo 1 > functions/mass_storage.usb0/lun.0/ro 91 | echo 1 > functions/mass_storage.usb0/lun.0/removable 92 | echo $MASS_FILE > functions/mass_storage.usb0/lun.0/file 93 | 94 | # 创建adb function 95 | # mkdir -p functions/ffs.adb 96 | # mkdir -p /dev/usb-ffs/adb 97 | # mount -o uid=2000,gid=2000 -t functionfs adb /dev/usb-ffs/adb 98 | # export service_adb_tcp_port=5555 99 | # start-stop-daemon --start --oknodo --make-pidfile --pidfile /var/run/adbd.pid --startas /usr/bin/adbd --background 100 | # sleep 2 101 | 102 | 103 | # 将对应function软连到c.1下面, 即启用该function,MAX 3 functions 104 | ln -s functions/ncm.usb0 configs/c.1/ 105 | ln -s functions/acm.gs0 configs/c.1/ 106 | ln -s functions/mass_storage.usb0 configs/c.1/ 107 | # ln -s functions/ffs.adb configs/c.1/ 108 | 109 | # 将gadget绑定到USB接 110 | echo ">>>>>>>>>>>Bind config1 to $(ls /sys/class/udc)" 111 | ls /sys/class/udc > UDC 112 | echo "All is done, Run script again to delete the gadget!" 113 | 114 | # 启动串口控制台 115 | sleep 0.5 116 | systemctl start serial-getty@ttyGS0.service 117 | -------------------------------------------------------------------------------- /gs/gs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | # load config 7 | source /etc/gs.conf 8 | [ "$gs_enable" == 'no' ] && exit 0 9 | 10 | # check and apply configuration in gs.conf 11 | source /gs/gs-applyconf.sh 12 | 13 | # RTC 14 | if [ "$use_external_rtc" == "yes" ]; then 15 | if [ -c /dev/i2c-4 ]; then 16 | modprobe i2c-dev 17 | echo ds3231 0x68 > /sys/class/i2c-adapter/i2c-4/new_device 18 | ( sleep 1 && [ -c /dev/rtc1 ] && hwclock -s -f /dev/rtc1 || echo "no ds3231 found" ) & 19 | else 20 | echo "i2c-4 is not enabled" 21 | fi 22 | fi 23 | 24 | # GPS 25 | [ "$use_gps" == "yes" ] && systemctl start chrony gpsd & 26 | 27 | # If set otg mode to device, use gadget acm, ncm, mass on boot 28 | if [ "$otg_mode" == "device" ]; then 29 | /gs/otg-gadget.sh & 30 | fi 31 | 32 | # pwm fan service 33 | [ "$fan_service_enable" == "yes" ] && ( echo "start fan service"; systemd-run --unit=fan /gs/fan.sh ) 34 | 35 | # ttyd 36 | if [ "$ttyd_enable" == "yes" ]; then 37 | [ "$(systemctl is-enabled ttyd)" == "enabled" ] || systemctl enable --now ttyd 38 | else 39 | [ "$(systemctl is-enabled ttyd)" == "enabled" ] && systemctl disable --now ttyd 40 | fi 41 | 42 | # fsck and mount record parittion if not auto mounted 43 | if ! grep -q $rec_dir /proc/mounts; then 44 | rec_dev=/dev/$(lsblk -no PKNAME $(findmnt -n -o SOURCE /))p4 45 | fsck.exfat -a $rec_dev 46 | mount $rec_dev $rec_dir 47 | fi 48 | 49 | # If video_on_boot=yes, video playback will be automatically started 50 | if [ "$video_on_boot" == "yes" ]; then 51 | # Start RubyFpv 52 | if [ "$fpv_firmware_type" == "rubyfpv" ]; then 53 | # Load wifi drivers 54 | [ -d "/sys/module/8812eu" ] || modprobe 8812eu rtw_tx_pwr_by_rate=0 rtw_tx_pwr_lmt_enable=0 55 | [ -d "/sys/module/88XXau_wfb" ] || modprobe 88XXau_wfb rtw_tx_pwr_idx_override=1 56 | # bind mount Vides dir to ruby 57 | [ -d "/home/radxa/ruby/media" ] || mkdir -p /home/radxa/ruby/media 58 | mount --bind $rec_dir /home/radxa/ruby/media 59 | # Use button gpio settings in gs.conf 60 | button_gpio="$btn_cr_pin $btn_cl_pin $btn_cu_pin $btn_cd_pin $btn_q1_pin $btn_q2_pin $btn_q3_pin" 61 | [ ! -e /config/gpio.txt ] && touch /config/gpio.txt 62 | [ "$button_gpio" == "$(< /config/gpio.txt)" ] || echo "$button_gpio" > /config/gpio.txt 63 | # start rubyfpv 64 | systemd-run --unit=rubyfpv \ 65 | --property=TTYPath=/dev/tty1 \ 66 | --property=StandardInput=file:/dev/tty1 \ 67 | --property=StandardOutput=file:/dev/tty1 \ 68 | --property=StandardError=file:/dev/tty1 \ 69 | --property=Type=forking \ 70 | --property=WorkingDirectory=/home/radxa/ruby \ 71 | /home/radxa/ruby/ruby_start 72 | else 73 | # add route to 224.0.0.1 74 | ip ro add 224.0.0.0/4 dev br0 75 | 76 | # Start wfb 77 | if [ "$wfb_mode" == "standalone" ]; then 78 | echo "start wfb in standalone mode" 79 | # Bind mount the wifibroadcast configuration file 80 | touch /tmp/wifibroadcast.cfg /tmp/wifibroadcast.default 81 | mount --bind /tmp/wifibroadcast.cfg /etc/wifibroadcast.cfg 82 | mount --bind /tmp/wifibroadcast.default /etc/default/wifibroadcast 83 | /gs/wfb.sh & 84 | elif [ "$wfb_mode" == "cluster" ]; then 85 | echo "start wfb in cluster mode" 86 | systemctl start wfb-cluster-manager@gs.service & 87 | /gs/wfb.sh & 88 | elif [ "$wfb_mode" == "aggregator" ]; then 89 | echo "start wfb in aggregator mode" 90 | wfb_rx -a 10000 -K $wfb_key -i $wfb_link_id -c $wfb_outgoing_ip -u $wfb_outgoing_port_video 2>&1 > /dev/null & 91 | wfb_rx -a 10001 -K $wfb_key -i $wfb_link_id -c $wfb_outgoing_ip -u $wfb_outgoing_port_mavlink 2>&1 > /dev/null & 92 | if [[ "$wfb_integrated_wnic" == "wifi0" && -d /sys/class/net/wifi0 ]]; then 93 | /gs/wfb.sh wifi0 & 94 | fi 95 | fi 96 | 97 | # start stream service 98 | echo "start stream service" 99 | systemd-run --unit=stream /gs/stream.sh 100 | 101 | # start button service 102 | echo "start button service" 103 | systemd-run --unit=button /gs/button.sh 104 | 105 | # start alink service 106 | [ "$alink_enable" == "yes" ] && systemd-run --unit=alink /usr/local/bin/alink --config /etc/alink.conf 107 | 108 | # copy video stream to local 109 | [[ "$wfb_outgoing_ip" != "224.0.0.1" && "$wfb_outgoing_ip" != "127.0.0.1" ]] && \ 110 | iptables -t mangle -A OUTPUT -d $wfb_outgoing_ip -p udp --dport $wfb_outgoing_port_video -j TEE --gateway ${br0_fixed_ip%/*} 111 | 112 | # start wfb rtsp service 113 | [ "$wfb_rtsp_server_enable" == "yes" ] && systemctl start rtsp@$video_codec 114 | fi 115 | fi 116 | 117 | # start oled 118 | [ "$oled_enable" == "yes" ] && systemd-run --unit=oled \ 119 | --setenv=VIRTUAL_ENV=/gs/venv \ 120 | --setenv=PATH="/gs/venv/bin:$PATH" \ 121 | /gs/venv/bin/python /gs/oled.py 122 | 123 | # start webui 124 | [ "$webui_enable" == "yes" ] && systemctl start webui 125 | 126 | # system boot complete, turn red record LED off 127 | gpioset -D $red_led_drive $(gpiofind PIN_${red_led_pin})=0 128 | echo "gs service start completed" 129 | 130 | exit 0 131 | -------------------------------------------------------------------------------- /gs/wfb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | # Only run script after gs service started 7 | [ -h "/run/systemd/units/invocation:gs.service" ] || exit 0 8 | 9 | source /etc/gs.conf 10 | # Not run script when using rubyfpv 11 | [ "$fpv_firmware_type" == "rubyfpv" ] && exit 0 12 | 13 | wfb_nics=$(echo /sys/class/net/wl* | sed -r -e "s^/sys/class/net/^^g" -e "s/wifi0\s{0,1}//" -e "s/wl\*//") 14 | [ -n "$wfb_integrated_wnic" ] && wfb_nics="$wfb_integrated_wnic $wfb_nics" 15 | [ -z "$wfb_nics" ] && exit 0 16 | 17 | monitor_wnic() { 18 | # Unmanage USB WiFi from NetworkManager 19 | # [ -f /etc/network/interfaces.d/wfb-$1 ] || echo -e "allow-hotplug $1\niface $1 inet manual" > /etc/network/interfaces.d/wfb-$1 20 | # if ! nmcli device show $1 | grep -q '(unmanaged)'; then 21 | # nmcli device set $1 managed no 22 | # sleep 1 23 | # fi 24 | [[ "$wfb_bandwidth" == "40" ]] && wfb_bandwidth="40+" 25 | 26 | ip link set $1 down 27 | iw dev $1 set monitor otherbss 28 | iw reg set $wfb_region 29 | ip link set $1 up 30 | iw dev $1 set channel $wfb_channel HT$wfb_bandwidth 31 | } 32 | 33 | set_txpower() { 34 | local driver_name=$(basename $(readlink /sys/class/net/${1}/device/driver)) 35 | case "$driver_name" in 36 | "rtl88xxau_wfb") 37 | iw dev $1 set txpower fixed -${2} 38 | ;; 39 | "8812eu" | "rtl88x2cu" | "8733bu") 40 | iw dev $1 set txpower fixed $2 41 | ;; 42 | *) 43 | echo "not set txpower for $1" 44 | ;; 45 | esac 46 | } 47 | 48 | if [ "$fpv_firmware_type" == "ap" ]; then 49 | wpa_passphrase "$ap_wifi_ssid" "$ap_wifi_password" > /run/wpa_supplicant.conf 50 | wpa_supplicant -B -i ${wfb_nics%% *} -c /run/wpa_supplicant.conf 51 | ip addr add "192.168.0.10/24" dev ${wfb_nics%% *} 52 | # dhclient ${wfb_nics%% *} 53 | exit 0 54 | elif [ "$wfb_mode" == "cluster" ]; then 55 | # stop local_node.service if exist 56 | [ -h "/run/systemd/units/invocation:local_node.service" ] && systemctl stop local_node.service 57 | # set all wnic to monitor 58 | for wnic in $wfb_nics; do 59 | monitor_wnic $wnic 60 | done 61 | # run wfb local_node 62 | systemd-run --unit=local_node.service bash -c " 63 | # gs_video 64 | wfb_rx -f -c 127.0.0.1 -u 10000 -p $wfb_stream_id_video -i 7669206 -R 2097152 $wfb_nics & 65 | # gs_mavlink 66 | wfb_rx -f -c 127.0.0.1 -u 10001 -p $wfb_stream_id_mavlink -i 7669206 -R 2097152 $wfb_nics & 67 | wfb_tx -I 11001 -R 2097152 $wfb_nics & 68 | # gs_tunnel 69 | wfb_rx -f -c 127.0.0.1 -u 10002 -p $wfb_stream_id_tunnel -i 7669206 -R 2097152 $wfb_nics & 70 | wfb_tx -I 11002 -R 2097152 $wfb_nics & 71 | wait 72 | " 73 | elif [ "$wfb_mode" == "standalone" ]; then 74 | if [ "$wfb_outgoing_video" == "socket" ]; then 75 | gs_video_peer="connect_unix://@/run/wfb_video.sock" 76 | else 77 | gs_video_peer="connect://${wfb_outgoing_ip}:${wfb_outgoing_port_video}" 78 | fi 79 | # Modify /etc/wifibroadcast.cfg according to gs.conf 80 | cat > /etc/wifibroadcast.cfg << EOF 81 | [common] 82 | wifi_channel = ${wfb_channel} 83 | wifi_region = '${wfb_region}' 84 | log_interval = ${wfb_log_interval} 85 | 86 | [gs_mavlink] 87 | peer = 'connect://${wfb_outgoing_ip}:${wfb_outgoing_port_mavlink}' 88 | bandwidth = 20 89 | 90 | [gs_video] 91 | peer = '${gs_video_peer}' 92 | 93 | [base] 94 | bandwidth = ${wfb_bandwidth} 95 | 96 | [gs_tunnel] 97 | bandwidth = 20 98 | 99 | EOF 100 | # Direct use wfb_rx for msposd_gs 101 | if [[ "$osd_type" == "msposd_gs" && "$msposd_gs_method" == "wfbrx" ]]; then 102 | cat >> /etc/wifibroadcast.cfg << EOF 103 | [gs] 104 | streams = [{'name': 'video', 'stream_rx': 0x00, 'stream_tx': None, 'service_type': 'udp_direct_rx', 'profiles': ['base', 'gs_base', 'video', 'gs_video']}, 105 | {'name': 'mavlink', 'stream_rx': 0x10, 'stream_tx': 0x90, 'service_type': 'mavlink', 'profiles': ['base', 'gs_base', 'mavlink', 'gs_mavlink']}, 106 | {'name': 'tunnel', 'stream_rx': 0x20, 'stream_tx': 0xa0, 'service_type': 'tunnel', 'profiles': ['base', 'gs_base', 'tunnel', 'gs_tunnel']}, 107 | {'name': 'msp', 'stream_rx': 0x11, 'stream_tx': 0x91, 'service_type': 'udp_proxy', 'profiles': ['base', 'gs_base', 'gs_msp']} 108 | ] 109 | [gs_msp] 110 | peer = 'connect://127.0.0.1:${msposd_gs_port}' # outgoing connection 111 | frame_type = 'data' # Use data or rts frames 112 | fec_k = 1 # FEC K (For tx side. Rx will get FEC settings from session packet) 113 | fec_n = 2 # FEC N (For tx side. Rx will get FEC settings from session packet) 114 | fec_timeout = 0 # [ms], 0 to disable. If no new packets during timeout, emit one empty packet if FEC block is open 115 | fec_delay = 0 # [us], 0 to disable. Issue FEC packets with delay between them. 116 | EOF 117 | fi 118 | # grep -q "WFB_NICS=\"${wfb_nics}\"" /etc/default/wifibroadcast || echo "WFB_NICS=\"${wfb_nics}\"" > /tmp/wifibroadcast.default 119 | echo "WFB_NICS=\"${wfb_nics}\"" > /etc/default/wifibroadcast 120 | systemctl restart wifibroadcast@gs 121 | elif [[ "$wfb_mode" == "aggregator" && -n $1 ]]; then 122 | monitor_wnic $1 123 | systemd-run /usr/bin/wfb_rx -f -p $wfb_stream_id_video -c 127.0.0.1 -u 10000 -i $wfb_link_id $1 124 | systemd-run /usr/bin/wfb_rx -f -p $wfb_stream_id_mavlink -c 127.0.0.1 -u 10001 -i $wfb_link_id $1 125 | else 126 | exit 0 127 | fi 128 | 129 | # set tx power for each WNIC 130 | if [ -n "$wfb_txpower" ]; then 131 | for wnic in $wfb_nics; do 132 | # wait 20s for wnic up to monitor mode 133 | sleep 20 && set_txpower $wnic $wfb_txpower & 134 | done 135 | fi 136 | -------------------------------------------------------------------------------- /gs/button-kbd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import time 6 | import subprocess 7 | from evdev import InputDevice, categorize, ecodes 8 | from dotenv import dotenv_values 9 | 10 | # Only connect keyboard after gs service is started will use kbd as buttons 11 | if not os.path.lexists("/run/systemd/units/invocation:gs.service"): 12 | print("gs service is not started, use keyboard as normal") 13 | sys.exit(1) 14 | 15 | # Do not use keyboard as button when RubyFpv is running 16 | if os.path.lexists("/run/systemd/units/invocation:rubyfpv.service"): 17 | print("RubyFpv is running, use keyboard as normal") 18 | sys.exit(1) 19 | 20 | # Check number of command line arguments 21 | if len(sys.argv) != 2: 22 | print("Usage: python script.py /path/to/kbd_device") 23 | sys.exit(1) 24 | 25 | # Get keyboard device path from the parameters 26 | kbd_device_path = sys.argv[1] 27 | try: 28 | kbd_device = InputDevice(kbd_device_path) 29 | except FileNotFoundError: 30 | print(f"Device not found: {kbd_device_path}") 31 | sys.exit(1) 32 | 33 | # Get gs conf 34 | gs_conf = dotenv_values("/etc/gs.conf") 35 | 36 | # Long press time threshold 37 | LONG_PRESS_THRESHOLD = 1.0 38 | # Record the timestamp of the key pressed 39 | key_press_time = {} 40 | # button script location 41 | button_script="/gs/button.sh" 42 | # Buttons mapped to keys 43 | BTN_Q1 = ecodes.KEY_1 44 | BTN_Q2 = ecodes.KEY_2 45 | BTN_Q3 = ecodes.KEY_3 46 | BTN_CU = ecodes.KEY_UP 47 | BTN_CD = ecodes.KEY_DOWN 48 | BTN_CL = ecodes.KEY_LEFT 49 | BTN_CR = ecodes.KEY_RIGHT 50 | KEY_QUIT = ecodes.KEY_Q 51 | KEY_SHUTDOWN = ecodes.KEY_S 52 | KEY_REBOOT = ecodes.KEY_R 53 | 54 | get_button_conf_command = "grep '^BTN.*press=' /etc/gs.conf" 55 | try: 56 | button_function_conf = subprocess.check_output(get_button_conf_command, shell=True, text=True) 57 | except subprocess.CalledProcessError: 58 | print("No matching lines found or error in grep command.") 59 | button_function_conf = "" 60 | 61 | if button_function_conf: 62 | try: 63 | exec(button_function_conf) 64 | except Exception as e: 65 | print(f"Error executing code: {e}") 66 | 67 | try: 68 | kbd_device.grab() 69 | print(f"Monitoring keyboard events on {kbd_device.path}...") 70 | for event in kbd_device.read_loop(): 71 | if event.type == ecodes.EV_KEY: 72 | key_event = categorize(event) 73 | key_code = key_event.scancode 74 | key_state = key_event.keystate 75 | 76 | if key_state == 1: 77 | key_press_time[key_code] = time.time() 78 | print(f"Key {key_event.keycode} pressed.") 79 | elif key_state == 0: 80 | press_duration = time.time() - key_press_time.get(key_code, 0) 81 | if press_duration >= LONG_PRESS_THRESHOLD: 82 | # long press 83 | print(f"Key {key_event.keycode} long pressed for {press_duration:.2f} seconds.") 84 | if key_code == KEY_QUIT: 85 | print("KEY_Q long press detected, quit now. ") 86 | break 87 | elif key_code == BTN_Q1: 88 | os.system(f"{button_script} {gs_conf['btn_q1_long_press']}") 89 | elif key_code == BTN_Q2: 90 | os.system(f"{button_script} {gs_conf['btn_q2_long_press']}") 91 | elif key_code == BTN_Q3: 92 | os.system(f"{button_script} {gs_conf['btn_q3_long_press']}") 93 | elif key_code == BTN_CU: 94 | os.system(f"{button_script} {gs_conf['btn_cu_long_press']}") 95 | elif key_code == BTN_CD: 96 | os.system(f"{button_script} {gs_conf['btn_cd_long_press']}") 97 | elif key_code == BTN_CL: 98 | os.system(f"{button_script} {gs_conf['btn_cl_long_press']}") 99 | elif key_code == BTN_CR: 100 | os.system(f"{button_script} {gs_conf['btn_cr_long_press']}") 101 | elif key_code == KEY_SHUTDOWN: 102 | os.system(f"{button_script} shutdown_gs") 103 | elif key_code == KEY_REBOOT: 104 | os.system(f"{button_script} reboot_gs") 105 | else: 106 | # single press 107 | print(f"Key {key_event.keycode} short pressed for {press_duration:.2f} seconds.") 108 | if key_code == BTN_Q1: 109 | os.system(f"{button_script} {gs_conf['btn_q1_single_press']}") 110 | elif key_code == BTN_Q2: 111 | os.system(f"{button_script} {gs_conf['btn_q2_single_press']}") 112 | elif key_code == BTN_Q3: 113 | os.system(f"{button_script} {gs_conf['btn_q3_single_press']}") 114 | elif key_code == BTN_CU: 115 | os.system(f"{button_script} {gs_conf['btn_cu_single_press']}") 116 | elif key_code == BTN_CD: 117 | os.system(f"{button_script} {gs_conf['btn_cd_single_press']}") 118 | elif key_code == BTN_CL: 119 | os.system(f"{button_script} {gs_conf['btn_cl_single_press']}") 120 | elif key_code == BTN_CR: 121 | os.system(f"{button_script} {gs_conf['btn_cr_single_press']}") 122 | 123 | # Clean after releasing the key 124 | key_press_time.pop(key_code, None) 125 | finally: 126 | kbd_device.ungrab() 127 | print("quit button-kbd") 128 | 129 | -------------------------------------------------------------------------------- /gs/gs-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | source /etc/gs.conf 6 | [ "$gs_enable" == 'no' ] && exit 0 7 | 8 | sleep 12 9 | setfont /usr/share/consolefonts/CyrAsia-TerminusBold32x16.psf.gz 10 | cat | tee /dev/ttyFIQ0 /dev/tty1 << EOF 11 | 12 | ############################### Welcome to SBC Ground Station ################################## 13 | # # 14 | # WARING: Thist is init startup, may take few minuts, system will auto restart when init done. # 15 | # WARING: Do not turn off the power during the initialization process. # 16 | # # 17 | ################################################################################################ 18 | 19 | EOF 20 | 21 | BOARD=$(cat /etc/hostname) 22 | 23 | # Expand overlay partition size and create vides partition(exfat) 24 | [ -d $rec_dir ] || mkdir -p $rec_dir 25 | os_dev=$(blkid | grep rootfs | grep -oP "/dev/.+(?=p\d+)") || true 26 | if [ ! -b ${os_dev}p5 ]; then 27 | sgdisk -ge $os_dev 28 | overlay_partition_start=$(parted -m $os_dev unit MiB print | tail -n 1 | cut -d: -f3 | tr -d 'MiB') 29 | overlay_partition_end=$(( $overlay_partition_start + $rootfs_reserved_space )) 30 | cat << EOF | parted ---pretend-input-tty $os_dev > /dev/null 2>&1 31 | resizepart 4 ${overlay_partition_end}MiB 32 | yes 33 | EOF 34 | resize2fs ${os_dev}p4 35 | videos_partition_start=$(parted -m $os_dev unit MiB print | tail -n 1 | cut -d: -f3 | tr -d 'MiB') 36 | parted -s $os_dev mkpart videos fat32 ${videos_partition_start}MiB 100% 37 | mkfs.exfat -L videos ${os_dev}p5 38 | fi 39 | 40 | # mount overlay lower to rw on init boot 41 | mount -o remount,rw /media/root-ro 42 | 43 | # mount /dev/disk/by-partlabel/videos $rec_dir 44 | if ! grep -Pq "^/dev/[^\s]*\s*${rec_dir}\s*exfat\s*defaults\,nofail\s*0\s*0" /media/root-ro/etc/fstab; then 45 | echo -e "${os_dev}p5 ${rec_dir} exfat defaults,nofail 0 0" >> /media/root-ro/etc/fstab 46 | fi 47 | 48 | # Enable dtbo 49 | # set max resolution to 4k, disabled by default 50 | dtc -I dts -O dtb -o /media/root-ro/boot/dtbo/rk3566-hdmi-max-resolution-4k.dtbo.disabled /gs/rk3566-hdmi-max-resolution-4k.dts 51 | # enbale USB OTG role switch 52 | dtc -I dts -O dtb -o /media/root-ro/boot/dtbo/rk3566-dwc3-otg-role-switch.dtbo /gs/rk3566-dwc3-otg-role-switch.dts 53 | # INA226 device, disabled by default 54 | dtc -I dts -O dtb -o /media/root-ro/boot/dtbo/rk3566-ina226-overlay.dtbo.disabled /gs/rk3566-ina226-overlay.dts 55 | 56 | 57 | # Add br0 network configuration 58 | [ -f /etc/systemd/network/br0.netdev ] || cat > /etc/systemd/network/br0.netdev << EOF 59 | [NetDev] 60 | Name=br0 61 | Kind=bridge 62 | EOF 63 | 64 | [ -f /etc/systemd/network/br0.network ] || cat > /etc/systemd/network/br0.network << EOF 65 | [Match] 66 | Name=br0 67 | 68 | [Network] 69 | Address=${br0_fixed_ip} 70 | Address=${br0_fixed_ip2} 71 | DHCP=yes 72 | EOF 73 | 74 | [ -f /etc/systemd/network/eth0.network ] || cat > /etc/systemd/network/eth0.network << EOF 75 | [Match] 76 | Name=eth0 77 | 78 | [Network] 79 | Bridge=br0 80 | EOF 81 | 82 | [ -f /etc/systemd/network/eth1.network ] || cat > /etc/systemd/network/eth1.network << EOF 83 | [Match] 84 | Name=eth1 85 | 86 | [Network] 87 | Bridge=br0 88 | EOF 89 | 90 | [ -f /etc/systemd/network/usb0.network ] || cat > /etc/systemd/network/usb0.network << EOF 91 | [Match] 92 | Name=usb0 93 | 94 | [Network] 95 | Bridge=br0 96 | EOF 97 | 98 | [ -f /etc/systemd/network/dummy0.netdev ] || cat > /etc/systemd/network/dummy0.netdev << EOF 99 | [NetDev] 100 | Name=dummy0 101 | Kind=dummy 102 | EOF 103 | 104 | [ -f /etc/systemd/network/dummy0.network ] || cat > /etc/systemd/network/dummy0.network << EOF 105 | [Match] 106 | Name=dummy0 107 | 108 | [Network] 109 | Bridge=br0 110 | EOF 111 | 112 | # Add radxa0 usb gadget network configuration 113 | echo "start configure radxa0 usb gadget network" 114 | gadget_net_fixed_ip_addr=${gadget_net_fixed_ip%/*} 115 | gadget_net_fixed_ip_sub=${gadget_net_fixed_ip%.*} 116 | if [ ! -f /etc/network/interfaces.d/radxa0 ]; then 117 | cat > /etc/network/interfaces.d/radxa0 << EOF 118 | auto radxa0 119 | allow-hotplug radxa0 120 | iface radxa0 inet static 121 | address $gadget_net_fixed_ip 122 | # post-up mount -o remount,ro /Videos && link mass 123 | # post-down remove mass && mount -o remount,rw /Videos 124 | up /usr/sbin/dnsmasq --conf-file=/dev/null --no-hosts --bind-interfaces --except-interface=lo --clear-on-reload --strict-order --listen-address=${gadget_net_fixed_ip_addr} --dhcp-range=${gadget_net_fixed_ip_sub}.21,${gadget_net_fixed_ip_sub}.199,12h --dhcp-lease-max=5 --pid-file=/run/dnsmasq-radxa0.pid --dhcp-option=3 --dhcp-option=6 125 | EOF 126 | fi 127 | 128 | # Add samba configuration 129 | grep -q "\[config\]" /etc/samba/smb.conf || cat >> /etc/samba/smb.conf << EOF 130 | [Videos] 131 | path = /Videos 132 | writable = yes 133 | browseable = yes 134 | create mode = 0777 135 | directory mode = 0777 136 | guest ok = yes 137 | force user = root 138 | 139 | [config] 140 | path = /config 141 | writable = yes 142 | browseable = yes 143 | create mode = 0777 144 | directory mode = 0777 145 | guest ok = yes 146 | force user = root 147 | EOF 148 | 149 | # wfb default configuration 150 | cat > /etc/wifibroadcast.cfg << EOF 151 | [common] 152 | wifi_channel = '${wfb_channel}' 153 | wifi_region = '${wfb_region}' 154 | 155 | [gs_mavlink] 156 | peer = 'connect://${wfb_outgoing_ip}:${wfb_outgoing_port_mavlink}' 157 | 158 | [gs_video] 159 | peer = 'connect://${wfb_outgoing_ip}:${wfb_outgoing_port_video}' 160 | 161 | [cluster] 162 | 163 | nodes = { 164 | '127.0.0.1': { 'wlans': ['wlan'], 'wifi_txpower': None, 'server_address': '127.0.0.1' }, 165 | # Remote cards: 166 | #'192.168.1.123' : { 'wlans': ['wlan0', 'wlan1'], 'wifi_txpower': 'off'}, # rx-only node 167 | #'192.168.1.155' : { 'wlans': ['wlan0', 'wlan1']}, # rx/tx node 168 | } 169 | 170 | server_address = '${br0_fixed_ip%/*}' 171 | 172 | EOF 173 | 174 | # gpsd and chrony configuration 175 | cat > /etc/default/gpsd << EOF 176 | stty -F /dev/${gps_uart} ${gps_uart_baudrate} 177 | START_DAEMON="true" 178 | DEVICES="/dev/${gps_uart}" 179 | GPSD_OPTIONS="-n -b -G -r" 180 | USBAUTO="true" 181 | EOF 182 | 183 | systemctl disable gs-init.service 184 | 185 | # check and apply configuration in gs.conf 186 | source /gs/gs-applyconf.sh 187 | 188 | while [ -f /config/before.txt ]; do sleep 2; done 189 | sync 190 | reboot 191 | -------------------------------------------------------------------------------- /gs/stream.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | source /etc/gs.conf 7 | export DISPLAY=:0 8 | cd $rec_dir 9 | 10 | pixelpilot_conf=/etc/pixelpilot.yaml 11 | video_record="0" 12 | video_play_cmd="" 13 | video_rec_cmd="" 14 | 15 | get_screen_mode() { 16 | case "$screen_mode" in 17 | "max-fps") 18 | screen_mode=$(pixelpilot --screen-mode-list | sort -t @ -k 2,2nr -k 1,1nr | head -n 1) 19 | echo "use max-fps screen_mode: $screen_mode" 20 | ;; 21 | "max-res") 22 | screen_mode=$(pixelpilot --screen-mode-list | sort -t @ -k 1,1nr -k 2,2nr | head -n 1) 23 | echo "use max-resolution screen_mode: $screen_mode" 24 | ;; 25 | *"x"*"@"*) 26 | screen_mode=${screen_mode%D} 27 | echo "use screen_mode in gs.conf: $screen_mode" 28 | ;; 29 | *) 30 | echo "auto screen_mode" 31 | screen_mode="" 32 | ;; 33 | esac 34 | } 35 | 36 | # Check and apply button gpio pins for pixelpilot gsmenu 37 | apply_gsmenu_gpio () { 38 | local pin_num=$(yq ".gsmenu.gpio.$2" $pixelpilot_conf) 39 | 40 | if [ -z "${!1}" ]; then 41 | yq -i "del(.gsmenu.gpio.$2)" $pixelpilot_conf 42 | elif [ "${!1}" != "$pin_num" ]; then 43 | yq -i ".gsmenu.gpio.$2 = ${!1}" $pixelpilot_conf 44 | fi 45 | } 46 | check_button_gpio() { 47 | apply_gsmenu_gpio btn_cu_pin up 48 | apply_gsmenu_gpio btn_cd_pin down 49 | apply_gsmenu_gpio btn_cl_pin left 50 | apply_gsmenu_gpio btn_cr_pin right 51 | apply_gsmenu_gpio btn_cm_pin center 52 | apply_gsmenu_gpio btn_q1_pin rec 53 | } 54 | 55 | # Auto select osd config file based on osd_type if osd_config_file is not set 56 | if [ -z "$osd_config_file" ]; then 57 | case "$osd_type" in 58 | "msposd_air") 59 | osd_config_file="/etc/pixelpilot/pixelpilot_osd_simple.json" 60 | ;; 61 | "msposd_gs") 62 | osd_config_file="/etc/pixelpilot/pixelpilot_msposd.json" 63 | ;; 64 | *) 65 | osd_config_file="/etc/pixelpilot/pixelpilot_osd.json" 66 | ;; 67 | esac 68 | 69 | # append os mon widget to osd config file 70 | if [ "$osd_widgets_osmon" == "yes" ]; then 71 | jq '.widgets += (input.widgets)' "$osd_config_file" /etc/pixelpilot/pixelpilot_osd_osmon.json > /run/pixelpilot_osd.json 72 | osd_config_file="/run/pixelpilot_osd.json" 73 | fi 74 | fi 75 | 76 | GPIO_RED_LED=$(gpiofind PIN_${red_led_pin}) 77 | 78 | gencmd(){ 79 | if [ "$video_player" == "pixelpilot" ]; then 80 | get_screen_mode 81 | check_button_gpio 82 | video_play_cmd="pixelpilot --codec $video_codec --dvr-framerate $rec_fps --dvr-template ${rec_dir}/record_%Y-%m-%d_%H-%M-%S.mp4 --dvr-sequenced-files" 83 | [ -n "$screen_mode" ] && video_play_cmd="$video_play_cmd --screen-mode $screen_mode" 84 | [ "$osd_enable" == "no" ] || video_play_cmd="$video_play_cmd --osd --osd-config $osd_config_file --osd-custom-message --osd-refresh $((1000 / ${osd_fps}))" 85 | [ "$record_on" == "arm" ] && video_play_cmd="$video_play_cmd --mavlink-dvr-on-arm" 86 | [ "$dvr_fmp4" == "yes" ] && video_play_cmd="$video_play_cmd --dvr-fmp4" 87 | [ "$disable_vsync" == "yes" ] && video_play_cmd="$video_play_cmd --disable-vsync" 88 | [ "$gsmenu_enable" == "yes" ] && video_play_cmd="$video_play_cmd --config /etc/pixelpilot.yaml" 89 | [ "$wfb_outgoing_video" == "socket" ] && video_play_cmd="$video_play_cmd --socket /run/wfb_video.sock" 90 | video_rec_cmd="$video_play_cmd --dvr-start" 91 | elif [ "$video_player" == "gstreamer" ]; then 92 | # current_date=$(date +'%m-%d-%Y_%H-%M-%S') 93 | # gencmd record_${current_date}.ts 94 | rec_index=$(ls -1 $rec_dir | grep -oP "^\d+(?=\.mkv)" | tail -n 1) 95 | if [ -z $rec_index ]; then 96 | rec_index="1000" 97 | else 98 | rec_index=$(($rec_index + 1)) 99 | fi 100 | video_play_cmd="gst-launch-1.0 -e udpsrc port=5600 caps='application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H${video_codec:1:4}' ! rtp${video_codec}depay ! ${video_codec}parse ! mppvideodec ! kmssink" 101 | video_rec_cmd="gst-launch-1.0 -e udpsrc port=5600 caps='application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H${video_codec:1:4}' ! rtp${video_codec}depay ! ${video_codec}parse ! tee name=t ! mppvideodec ! kmssink t. ! queue ! matroskamux ! filesink location=${rec_index}" 102 | else 103 | echo "wrong video player, only support pixelpilot and gstreamer" 104 | fi 105 | } 106 | 107 | check_record_freespace() { 108 | local rec_dir_freespace=$(df $rec_dir | tail -n 1 | awk '{print $4}') 109 | local rec_dir_freespace_MB=$((${rec_dir_freespace} / 1024)) 110 | if [ $rec_dir_freespace_MB -lt $rec_dir_freespace_min ]; then 111 | echo "insufficient" 112 | else 113 | echo "sufficient" 114 | fi 115 | } 116 | 117 | gencmd 118 | # wait monitor connected 119 | while true; do 120 | monitor_status=$(cat /sys/class/drm/card0-HDMI-A-1/status) 121 | [ "$monitor_status" == "connected" ] && break 122 | sleep 1 123 | done 124 | if [[ "$record_on" == "boot" && "$(check_record_freespace)" == "sufficient" ]]; then 125 | bash -c "$video_rec_cmd" & 126 | pid_player=$! 127 | video_record="1" 128 | else 129 | bash -c "$video_play_cmd" & 130 | pid_player=$! 131 | fi 132 | 133 | # start OSD 134 | ( 135 | if [ "$osd_enable" == "yes" ]; then 136 | if [[ "$video_player" == "pixelpilot" && "$osd_type" == "msposd_gs" ]]; then 137 | # start msposd_rockchip after /dev/shm/msposd is created and wfb tunnel is up 138 | while [[ ! -e /dev/shm/msposd || ! -d /sys/class/net/gs-wfb ]]; do sleep 1; done 139 | if [ "$msposd_gs_record" == "yes" ]; then 140 | msposd --master 0.0.0.0:$msposd_gs_port --osd -r $msposd_gs_fps --ahi $msposd_gs_ahi --subtitle $rec_dir 141 | else 142 | msposd --master 0.0.0.0:$msposd_gs_port --osd -r $msposd_gs_fps --ahi $msposd_gs_ahi 143 | fi 144 | elif [ "$video_player" == "gstreamer" ]; then 145 | wfb-ng-osd -p 14550 146 | fi 147 | fi 148 | ) & 149 | 150 | # show wallpaper 151 | ( sleep 10 && fbi -d /dev/fb0 -a -fitwidth -T 1 --noverbose /gs/wallpaper.png ) & 152 | 153 | # Monitor button for start/stop reocrd 154 | [ -p /run/record_button.fifo ] || mkfifo /run/record_button.fifo 155 | while read record_button_action < /run/record_button.fifo; do 156 | [ "$record_button_action" == "single" ] || continue 157 | if [ "$video_record" == "0" ]; then 158 | if [ "$(check_record_freespace)" == "insufficient" ]; then 159 | echo "No enough record space!" > /run/pixelpilot.msg 160 | continue 161 | fi 162 | if [ "$video_player" == "pixelpilot" ]; then 163 | kill -SIGUSR1 $pid_player 164 | else 165 | kill -15 $pid_player 166 | sleep 0.2 167 | gencmd 168 | bash -c "$video_rec_cmd" & 169 | pid_player=$! 170 | fi 171 | echo "record start!" > /run/pixelpilot.msg 172 | video_record='1' 173 | ( 174 | while true; do 175 | # Blink red record LED 176 | gpioset -D $red_led_drive -m time -s 1 ${GPIO_RED_LED}=1 177 | gpioset -D $red_led_drive -m time -s 1 ${GPIO_RED_LED}=0 178 | done 179 | ) & 180 | pid_led=$! 181 | else 182 | # turn off record LED 183 | [ -z $pid_led ] || kill $pid_led 184 | sleep 1.2 && gpioset -D $red_led_drive ${GPIO_RED_LED}=0 & 185 | if [ "$video_player" == "pixelpilot" ]; then 186 | kill -SIGUSR1 $pid_player 187 | else 188 | kill -15 $pid_player 189 | sleep 0.2 190 | bash -c "$video_play_cmd" & 191 | pid_player=$! 192 | fi 193 | sync 194 | echo "record stop!" > /run/pixelpilot.msg 195 | video_record='0' 196 | fi 197 | sleep 3 198 | done 199 | -------------------------------------------------------------------------------- /gs/button.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | source /etc/gs.conf 5 | 6 | # Exit if gs service is not enable 7 | [ -e /etc/systemd/system/multi-user.target.wants/gs.service ] || exit 0 8 | # Exit if gs is not enable 9 | [ "$gs_enable" == 'no' ] && exit 0 10 | # Passing record button state from button.sh to stream.sh 11 | [ -p /run/record_button.fifo ] || mkfifo /run/record_button.fifo 12 | 13 | # change wifi mode between station and hotspot 14 | function change_wifi_mode() { 15 | if [ ! -d /sys/class/net/wifi0 ]; then 16 | echo "WARING: no wifi0 found, can't switch wifi mode." 17 | exit 0 18 | elif [ "$wfb_integrated_wnic" == "wifi0" ]; then 19 | echo "WARING: wifi0 used by wfb, can't switch wifi mode." 20 | exit 0 21 | fi 22 | wifi0_connected_connection=$(nmcli device status | grep '^wifi0.*connected' | tr -s ' ' | cut -d ' ' -f 4) 23 | case "$wifi0_connected_connection" in 24 | "hotspot") 25 | echo "Prepare connect to ${wifi_ssid}!" > /run/pixelpilot.msg 26 | if nmcli connection up wifi0 > /dev/null 2>&1; then 27 | echo "WiFi connected to ${wifi_ssid}!" > /run/pixelpilot.msg 28 | else 29 | nmcli connection up hotspot 30 | echo "Failed connect to ${wifi_ssid}, fallback to hotspot mode!" > /run/pixelpilot.msg 31 | fi 32 | sleep 3 33 | ;; 34 | "wifi0"|"--") 35 | echo "Prepare change to hotspot mode!" > /run/pixelpilot.msg 36 | if nmcli connection up hotspot > /dev/null 2>&1; then 37 | echo "WiFi changed to hotspot mode!" > /run/pixelpilot.msg 38 | else 39 | echo "Failed change to hotspot mode!" > /run/pixelpilot.msg 40 | fi 41 | sleep 3 42 | ;; 43 | *) 44 | echo "connection is unknow" 45 | ;; 46 | esac 47 | } 48 | 49 | # change usb otg mode between host and device 50 | function change_otg_mode() { 51 | local otg_mode_LED_PIN_info=$(gpiofind PIN_${!otg_mode_led_pin}) 52 | local otg_mode_file="/sys/kernel/debug/usb/fcc00000.dwc3/mode" 53 | local otg_mode=$(cat $otg_mode_file) 54 | if [ "$otg_mode" == "host" ]; then 55 | echo device > $otg_mode_file 56 | echo "change otg mode to device!" > /run/pixelpilot.msg 57 | sleep 0.2 58 | if [ -d /sys/kernel/config/usb_gadget/g1 ]; then 59 | ls /sys/class/udc > /sys/kernel/config/usb_gadget/g1/UDC 60 | else 61 | /gs/otg-gadget.sh & 62 | fi 63 | # [ "$(ip link ls radxa0 | grep -oP '(?<=state ).+(?=mode)')" == "DOWN" ] && ifup radxa0 64 | ( 65 | while true; do 66 | # Blink green power LED 67 | gpioset -D ${!otg_mode_led_drive} -m time -s 1 $otg_mode_LED_PIN_info=1 68 | gpioset -D ${!otg_mode_led_drive} -m time -s 1 $otg_mode_LED_PIN_info=0 69 | done 70 | ) & 71 | local pid_led=$! 72 | elif [ "$otg_mode" == "device" ]; then 73 | echo host > $otg_mode_file 74 | echo "change otg mode to host!" > /run/pixelpilot.msg 75 | [ -z "$pid_led" ] || kill $pid_led 76 | sleep 1.2 77 | gpioset -D ${!otg_mode_led_drive} -m time -s 1 $otg_mode_LED_PIN_info=1 78 | else 79 | echo "otg mode is unkonw" 80 | fi 81 | 82 | } 83 | 84 | # scan wfb wifi channel 85 | function scan_wfb_channel() { 86 | /gs/channel-scan.sh 87 | } 88 | 89 | # start or stop recording 90 | function toggle_record() { 91 | echo "single" > /run/record_button.fifo 92 | } 93 | 94 | # start or stop stream 95 | function toggle_stream() { 96 | if [ -h "/run/systemd/units/invocation:stream.service" ]; then 97 | echo "Stop stream service" > /run/pixelpilot.msg 98 | sleep 1 99 | systemctl stop stream.service 100 | else 101 | systemd-run --unit=stream /gs/stream.sh 102 | fi 103 | } 104 | 105 | # cleanup record files 106 | function cleanup_record_files() { 107 | # first long press cleanup record files until have enough space 108 | # secord long press in 60s will remove all record files 109 | record_file_list=$(find $rec_dir -maxdepth 1 -type f \( -name '*.mp4' -o -name '*.mkv' \)) 110 | if [ -n "$record_file_list" ];then 111 | if [ ! -f /tmp/cleanup_record ]; then 112 | for record_file in $record_file_list; do 113 | [ "$(check_record_freespace)" == "sufficient" ] && break 114 | rm $record_file 115 | done 116 | echo "cleanup record done!" > /run/pixelpilot.msg 117 | ( 118 | touch /tmp/cleanup_record 119 | sleep 60 120 | [ -f /tmp/cleanup_record ] && rm /tmp/cleanup_record 121 | ) & 122 | else 123 | for record_file in $record_file_list; do 124 | rm $record_file 125 | done 126 | [ -f /tmp/cleanup_record ] && rm /tmp/cleanup_record 127 | echo "All record file deleted!" > /run/pixelpilot.msg 128 | fi 129 | else 130 | echo "no record file found!" > /run/pixelpilot.msg 131 | fi 132 | } 133 | 134 | # check and apply configuration in gs.conf 135 | function apply_conf() { 136 | ( 137 | echo "apply gs.conf!" > /run/pixelpilot.msg 138 | /gs/gs-applyconf.sh 139 | ) & 140 | } 141 | 142 | # shutdown Ground Station 143 | function shutdown_gs() { 144 | echo "Ground Station going to shutdown in 2 seconds!" > /run/pixelpilot.msg 145 | ( sleep 2 && poweroff) & 146 | } 147 | 148 | # reboot Ground Station 149 | function reboot_gs() { 150 | echo "Ground Station going to reboot in 2 seconds!" > /run/pixelpilot.msg 151 | ( sleep 2 && reboot) & 152 | } 153 | 154 | # mount extdisk first partition to rec_dir 155 | function mount_extdisk() { 156 | local root_dev=$(findmnt -n -o SOURCE /) 157 | # skip if boot from MicroSD 158 | [[ "$root_dev" == "/dev/mmcblk1p"? && "${arg_2}" == "/dev/mmcblk1p1" ]] && exit 0 159 | # mount in root mnt namespace. Udev mount will use systemd-udevd mnt namespace by default. 160 | if nsenter --mount=/proc/1/ns/mnt mount ${arg_2} ${rec_dir} > /dev/null 2>&1; then 161 | partitioninfo=$(df -hT ${rec_dir} | tail -n 1 | tr -s ' ') 162 | echo "$partitioninfo" > /run/pixelpilot.msg 163 | else 164 | echo "mount extdisk failed, check fstype and file system" > /run/pixelpilot.msg 165 | fi 166 | } 167 | 168 | # unmount extdisk video partition 169 | function ummount_extdisk() { 170 | if grep -Eq "^/dev/sda1 /${rec_dir}|^/dev/mmcblk1p1 /${rec_dir}" /proc/mounts; then 171 | if umount -lf "$rec_dir" > /dev/null 2>&1; then 172 | echo "umount $rec_dir success" > /run/pixelpilot.msg 173 | else 174 | echo "umount $rec_dir failed, check record status" > /run/pixelpilot.msg 175 | fi 176 | else 177 | echo "extdisk already umounted" > /run/pixelpilot.msg 178 | fi 179 | } 180 | 181 | # Add more custom functions above 182 | 183 | # Pass function name to script to execute the function 184 | if [ -n "$1" ] && declare -f $1 > /dev/null; then 185 | [ -n "$2" ] && arg_2=$2 186 | $1 187 | exit 0 188 | else 189 | echo "function $1 not found" 190 | fi 191 | 192 | function button_action() { 193 | local gpio_info=$(gpiofind PIN_${1}) 194 | while gpiomon -r -s -n 1 -B pull-down ${gpio_info}; do 195 | sleep 0.05 196 | [ "$(gpioget ${gpio_info})" == "1" ] || continue 197 | local button_press_uptime=$(cut -d ' ' -f 1 /proc/uptime | tr -d .) 198 | gpiomon -f -s -n 1 -B pull-down ${gpio_info} 199 | local button_release_uptime=$(cut -d ' ' -f 1 /proc/uptime | tr -d .) 200 | local button_pressed_time=$((${button_release_uptime} - ${button_press_uptime})) 201 | if [ $button_pressed_time -lt 200 ]; then 202 | echo "single" 203 | elif [ $button_pressed_time -ge 200 ]; then 204 | echo "long" 205 | fi 206 | break 207 | done 208 | } 209 | 210 | function execute_button_function() { 211 | local gpio_pin="${1}_pin" 212 | [ -z "${!gpio_pin}" ] && exit 0 213 | local single_press_function="${1}_single_press" 214 | local long_press_function="${1}_long_press" 215 | [ -z "${!single_press_function}" ] && [ -z "${!long_press_function}" ] && exit 0 216 | while true; do 217 | local action=$(button_action ${!gpio_pin}) 218 | case $action in 219 | single) 220 | ${!single_press_function} 221 | ;; 222 | long) 223 | ${!long_press_function} 224 | ;; 225 | *) 226 | echo "unknow button action" 227 | esac 228 | 229 | done 230 | } 231 | 232 | # up/down/left/right/q1 is used by PixelPilot_rk 233 | if [ "$video_player" != "pixelpilot" ]; then 234 | execute_button_function btn_cu & 235 | execute_button_function btn_cd & 236 | execute_button_function btn_cl & 237 | execute_button_function btn_cr & 238 | execute_button_function btn_cm & 239 | execute_button_function btn_q1 & 240 | fi 241 | execute_button_function btn_q2 & 242 | execute_button_function btn_q3 & 243 | 244 | wait 245 | -------------------------------------------------------------------------------- /gs/pixelpilot_osd_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "format": "0.0.2", 3 | "assets_dir": "/usr/local/share/pixelpilot/", 4 | "widgets": [ 5 | { 6 | "type": "IconSelectorWidget", 7 | "name": "RSSI of antenna 1", 8 | "x": -366, 9 | "y": 0, 10 | "facts": [ 11 | { 12 | "name": "wfbcli.rx.ant_stats.rssi_avg", 13 | "tags": { 14 | "ant_id": "0", 15 | "id": "video rx" 16 | } 17 | } 18 | ], 19 | "ranges_and_icons": [ 20 | { 21 | "range": [-59, 1], 22 | "icon_path": "signal1.png" 23 | }, 24 | { 25 | "range": [-69, -60], 26 | "icon_path": "signal2.png" 27 | }, 28 | { 29 | "range": [-79, -70], 30 | "icon_path": "signal3.png" 31 | }, 32 | { 33 | "range": [-89, -80], 34 | "icon_path": "signal5.png" 35 | }, 36 | { 37 | "range": [-130, -90], 38 | "icon_path": "signal4.png" 39 | } 40 | ] 41 | }, 42 | { 43 | "type": "IconSelectorWidget", 44 | "name": "RSSI of antenna 2", 45 | "x": -462, 46 | "y": 0, 47 | "facts": [ 48 | { 49 | "name": "wfbcli.rx.ant_stats.rssi_avg", 50 | "tags": { 51 | "ant_id": "1", 52 | "id": "video rx" 53 | } 54 | } 55 | ], 56 | "ranges_and_icons": [ 57 | { 58 | "range": [-59, 1], 59 | "icon_path": "signal1.png" 60 | }, 61 | { 62 | "range": [-69, -60], 63 | "icon_path": "signal2.png" 64 | }, 65 | { 66 | "range": [-79, -70], 67 | "icon_path": "signal3.png" 68 | }, 69 | { 70 | "range": [-89, -80], 71 | "icon_path": "signal5.png" 72 | }, 73 | { 74 | "range": [-130, -90], 75 | "icon_path": "signal4.png" 76 | } 77 | ], 78 | "calculation": "wfbcli_rx_ant_stats_rssi_avg_ant_id_1_id_video_rx" 79 | }, 80 | { 81 | "type": "IconSelectorWidget", 82 | "name": "RSSI of antenna 3", 83 | "x": -558, 84 | "y": 0, 85 | "facts": [ 86 | { 87 | "name": "wfbcli.rx.ant_stats.rssi_avg", 88 | "tags": { 89 | "ant_id": "256", 90 | "id": "video rx" 91 | } 92 | } 93 | ], 94 | "ranges_and_icons": [ 95 | { 96 | "range": [-59, 1], 97 | "icon_path": "signal1.png" 98 | }, 99 | { 100 | "range": [-69, -60], 101 | "icon_path": "signal2.png" 102 | }, 103 | { 104 | "range": [-79, -70], 105 | "icon_path": "signal3.png" 106 | }, 107 | { 108 | "range": [-89, -80], 109 | "icon_path": "signal5.png" 110 | }, 111 | { 112 | "range": [-130, -90], 113 | "icon_path": "signal4.png" 114 | } 115 | ] 116 | }, 117 | { 118 | "type": "IconSelectorWidget", 119 | "name": "RSSI of antenna 4", 120 | "x": -654, 121 | "y": 0, 122 | "facts": [ 123 | { 124 | "name": "wfbcli.rx.ant_stats.rssi_avg", 125 | "tags": { 126 | "ant_id": "257", 127 | "id": "video rx" 128 | } 129 | } 130 | ], 131 | "ranges_and_icons": [ 132 | { 133 | "range": [-59, 1], 134 | "icon_path": "signal1.png" 135 | }, 136 | { 137 | "range": [-69, -60], 138 | "icon_path": "signal2.png" 139 | }, 140 | { 141 | "range": [-79, -70], 142 | "icon_path": "signal3.png" 143 | }, 144 | { 145 | "range": [-89, -80], 146 | "icon_path": "signal5.png" 147 | }, 148 | { 149 | "range": [-130, -90], 150 | "icon_path": "signal4.png" 151 | } 152 | ] 153 | }, 154 | { 155 | "type": "IconSelectorWidget", 156 | "name": "RSSI of antenna 5", 157 | "x": -750, 158 | "y": 0, 159 | "facts": [ 160 | { 161 | "name": "wfbcli.rx.ant_stats.rssi_avg", 162 | "tags": { 163 | "ant_id": "512", 164 | "id": "video rx" 165 | } 166 | } 167 | ], 168 | "ranges_and_icons": [ 169 | { 170 | "range": [-59, 1], 171 | "icon_path": "signal1.png" 172 | }, 173 | { 174 | "range": [-69, -60], 175 | "icon_path": "signal2.png" 176 | }, 177 | { 178 | "range": [-79, -70], 179 | "icon_path": "signal3.png" 180 | }, 181 | { 182 | "range": [-89, -80], 183 | "icon_path": "signal5.png" 184 | }, 185 | { 186 | "range": [-130, -90], 187 | "icon_path": "signal4.png" 188 | } 189 | ] 190 | }, 191 | { 192 | "type": "IconSelectorWidget", 193 | "name": "RSSI of antenna 6", 194 | "x": -846, 195 | "y": 0, 196 | "facts": [ 197 | { 198 | "name": "wfbcli.rx.ant_stats.rssi_avg", 199 | "tags": { 200 | "ant_id": "513", 201 | "id": "video rx" 202 | } 203 | } 204 | ], 205 | "ranges_and_icons": [ 206 | { 207 | "range": [-59, 1], 208 | "icon_path": "signal1.png" 209 | }, 210 | { 211 | "range": [-69, -60], 212 | "icon_path": "signal2.png" 213 | }, 214 | { 215 | "range": [-79, -70], 216 | "icon_path": "signal3.png" 217 | }, 218 | { 219 | "range": [-89, -80], 220 | "icon_path": "signal5.png" 221 | }, 222 | { 223 | "range": [-130, -90], 224 | "icon_path": "signal4.png" 225 | } 226 | ] 227 | }, 228 | { 229 | "name": "Metrics background", 230 | "type": "BoxWidget", 231 | "x": -270, 232 | "y": 0, 233 | "width": 270, 234 | "height": 150, 235 | "color": { 236 | "r": 0.0, 237 | "g": 0.0, 238 | "b": 0.0, 239 | "alpha": 0.4 240 | }, 241 | "facts": [] 242 | }, 243 | { 244 | "name": "Video FPS and resolution", 245 | "type": "VideoWidget", 246 | "x": -250, 247 | "y": 26, 248 | "icon_path": "framerate.png", 249 | "template": "%u fps | %ux%u", 250 | "per_second_window_s": 2, 251 | "per_second_bucket_ms": 200, 252 | "facts": [ 253 | { 254 | "__comment": "Will be converted to per-second", 255 | "name": "video.displayed_frame" 256 | }, 257 | { 258 | "name": "video.width" 259 | }, 260 | { 261 | "name": "video.height" 262 | } 263 | ] 264 | }, 265 | { 266 | "name": "Video link throughput", 267 | "type": "VideoBitrateWidget", 268 | "x": -250, 269 | "y": 56, 270 | "icon_path": "network.png", 271 | "template": "%f Mbps", 272 | "per_second_window_s": 2, 273 | "per_second_bucket_ms": 100, 274 | "facts": [ 275 | { 276 | "__comment": "Should be sum per-second, scaled to Megs", 277 | "name": "gstreamer.received_bytes" 278 | } 279 | ] 280 | }, 281 | { 282 | "name": "WFB radio", 283 | "type": "IconTplTextWidget", 284 | "x": -250, 285 | "y": 86, 286 | "icon_path": "wfb.png", 287 | "template": "RSSI %d ", 288 | "facts": [ 289 | { 290 | "name": "mavlink.radio_status.rssi", 291 | "tags": { 292 | "sysid": "3", 293 | "compid": "68" 294 | } 295 | } 296 | ] 297 | }, 298 | { 299 | "name": "WFB radio SNR", 300 | "type": "TplTextWidget", 301 | "x": -110, 302 | "y": 86, 303 | "template": "| SNR %d", 304 | "facts": [ 305 | { 306 | "name": "wfbcli.rx.ant_stats.snr_min", 307 | "tags": { 308 | "ant_id": "0", 309 | "id": "video rx" 310 | } 311 | } 312 | ] 313 | }, 314 | { 315 | "name": "WFB radio package lost", 316 | "type": "IconTplTextWidget", 317 | "x": -250, 318 | "y": 116, 319 | "icon_path": "wfb.png", 320 | "template": "LOST %u ", 321 | "facts": [ 322 | { 323 | "name": "mavlink.radio_status.rxerrors", 324 | "tags": { 325 | "sysid": "3", 326 | "compid": "68" 327 | } 328 | } 329 | ] 330 | }, 331 | { 332 | "name": "WFB radio package fec", 333 | "type": "TplTextWidget", 334 | "x": -110, 335 | "y": 116, 336 | "template": "| FEC %u", 337 | "facts": [ 338 | { 339 | "name": "mavlink.radio_status.fixed", 340 | "tags": { 341 | "sysid": "3", 342 | "compid": "68" 343 | } 344 | } 345 | ] 346 | }, 347 | { 348 | "name": "DVR status", 349 | "type": "DvrStatusWidget", 350 | "x": -250, 351 | "y": 146, 352 | "icon_path": "sdcard-white.png", 353 | "text": "Recording", 354 | "facts": [ 355 | { 356 | "name": "dvr.recording" 357 | } 358 | ] 359 | }, 360 | { 361 | "name": "Custom fading message", 362 | "type": "PopupWidget", 363 | "x": 400, 364 | "y": 50, 365 | "timeout_ms": 10000, 366 | "facts": [ 367 | { 368 | "name": "osd.custom_message" 369 | } 370 | ] 371 | } 372 | ] 373 | } 374 | -------------------------------------------------------------------------------- /gs/pixelpilot_msposd.json: -------------------------------------------------------------------------------- 1 | { 2 | "format": "0.0.1", 3 | "assets_dir": "/usr/local/share/pixelpilot/", 4 | "widgets": [ 5 | { 6 | "type": "IconSelectorWidget", 7 | "name": "RSSI of antenna 1", 8 | "x": -366, 9 | "y": 0, 10 | "facts": [ 11 | { 12 | "name": "wfbcli.rx.ant_stats.rssi_avg", 13 | "tags": { 14 | "ant_id": "0", 15 | "id": "video rx" 16 | } 17 | } 18 | ], 19 | "ranges_and_icons": [ 20 | { 21 | "range": [-59, 1], 22 | "icon_path": "signal1.png" 23 | }, 24 | { 25 | "range": [-69, -60], 26 | "icon_path": "signal2.png" 27 | }, 28 | { 29 | "range": [-79, -70], 30 | "icon_path": "signal3.png" 31 | }, 32 | { 33 | "range": [-89, -80], 34 | "icon_path": "signal5.png" 35 | }, 36 | { 37 | "range": [-130, -90], 38 | "icon_path": "signal4.png" 39 | } 40 | ] 41 | }, 42 | { 43 | "type": "IconSelectorWidget", 44 | "name": "RSSI of antenna 2", 45 | "x": -462, 46 | "y": 0, 47 | "facts": [ 48 | { 49 | "name": "wfbcli.rx.ant_stats.rssi_avg", 50 | "tags": { 51 | "ant_id": "1", 52 | "id": "video rx" 53 | } 54 | } 55 | ], 56 | "ranges_and_icons": [ 57 | { 58 | "range": [-59, 1], 59 | "icon_path": "signal1.png" 60 | }, 61 | { 62 | "range": [-69, -60], 63 | "icon_path": "signal2.png" 64 | }, 65 | { 66 | "range": [-79, -70], 67 | "icon_path": "signal3.png" 68 | }, 69 | { 70 | "range": [-89, -80], 71 | "icon_path": "signal5.png" 72 | }, 73 | { 74 | "range": [-130, -90], 75 | "icon_path": "signal4.png" 76 | } 77 | ], 78 | "calculation": "wfbcli_rx_ant_stats_rssi_avg_ant_id_1_id_video_rx" 79 | }, 80 | { 81 | "type": "IconSelectorWidget", 82 | "name": "RSSI of antenna 3", 83 | "x": -558, 84 | "y": 0, 85 | "facts": [ 86 | { 87 | "name": "wfbcli.rx.ant_stats.rssi_avg", 88 | "tags": { 89 | "ant_id": "256", 90 | "id": "video rx" 91 | } 92 | } 93 | ], 94 | "ranges_and_icons": [ 95 | { 96 | "range": [-59, 1], 97 | "icon_path": "signal1.png" 98 | }, 99 | { 100 | "range": [-69, -60], 101 | "icon_path": "signal2.png" 102 | }, 103 | { 104 | "range": [-79, -70], 105 | "icon_path": "signal3.png" 106 | }, 107 | { 108 | "range": [-89, -80], 109 | "icon_path": "signal5.png" 110 | }, 111 | { 112 | "range": [-130, -90], 113 | "icon_path": "signal4.png" 114 | } 115 | ] 116 | }, 117 | { 118 | "type": "IconSelectorWidget", 119 | "name": "RSSI of antenna 4", 120 | "x": -654, 121 | "y": 0, 122 | "facts": [ 123 | { 124 | "name": "wfbcli.rx.ant_stats.rssi_avg", 125 | "tags": { 126 | "ant_id": "257", 127 | "id": "video rx" 128 | } 129 | } 130 | ], 131 | "ranges_and_icons": [ 132 | { 133 | "range": [-59, 1], 134 | "icon_path": "signal1.png" 135 | }, 136 | { 137 | "range": [-69, -60], 138 | "icon_path": "signal2.png" 139 | }, 140 | { 141 | "range": [-79, -70], 142 | "icon_path": "signal3.png" 143 | }, 144 | { 145 | "range": [-89, -80], 146 | "icon_path": "signal5.png" 147 | }, 148 | { 149 | "range": [-130, -90], 150 | "icon_path": "signal4.png" 151 | } 152 | ] 153 | }, 154 | { 155 | "type": "IconSelectorWidget", 156 | "name": "RSSI of antenna 5", 157 | "x": -750, 158 | "y": 0, 159 | "facts": [ 160 | { 161 | "name": "wfbcli.rx.ant_stats.rssi_avg", 162 | "tags": { 163 | "ant_id": "512", 164 | "id": "video rx" 165 | } 166 | } 167 | ], 168 | "ranges_and_icons": [ 169 | { 170 | "range": [-59, 1], 171 | "icon_path": "signal1.png" 172 | }, 173 | { 174 | "range": [-69, -60], 175 | "icon_path": "signal2.png" 176 | }, 177 | { 178 | "range": [-79, -70], 179 | "icon_path": "signal3.png" 180 | }, 181 | { 182 | "range": [-89, -80], 183 | "icon_path": "signal5.png" 184 | }, 185 | { 186 | "range": [-130, -90], 187 | "icon_path": "signal4.png" 188 | } 189 | ] 190 | }, 191 | { 192 | "type": "IconSelectorWidget", 193 | "name": "RSSI of antenna 6", 194 | "x": -846, 195 | "y": 0, 196 | "facts": [ 197 | { 198 | "name": "wfbcli.rx.ant_stats.rssi_avg", 199 | "tags": { 200 | "ant_id": "513", 201 | "id": "video rx" 202 | } 203 | } 204 | ], 205 | "ranges_and_icons": [ 206 | { 207 | "range": [-59, 1], 208 | "icon_path": "signal1.png" 209 | }, 210 | { 211 | "range": [-69, -60], 212 | "icon_path": "signal2.png" 213 | }, 214 | { 215 | "range": [-79, -70], 216 | "icon_path": "signal3.png" 217 | }, 218 | { 219 | "range": [-89, -80], 220 | "icon_path": "signal5.png" 221 | }, 222 | { 223 | "range": [-130, -90], 224 | "icon_path": "signal4.png" 225 | } 226 | ] 227 | }, 228 | { 229 | "name": "Metrics background", 230 | "type": "BoxWidget", 231 | "x": -270, 232 | "y": 0, 233 | "width": 270, 234 | "height": 150, 235 | "color": { 236 | "r": 0.0, 237 | "g": 0.0, 238 | "b": 0.0, 239 | "alpha": 0.4 240 | }, 241 | "facts": [] 242 | }, 243 | { 244 | "name": "Video FPS and resolution", 245 | "type": "VideoWidget", 246 | "x": -250, 247 | "y": 26, 248 | "icon_path": "framerate.png", 249 | "template": "%u fps | %ux%u", 250 | "per_second_window_s": 2, 251 | "per_second_bucket_ms": 200, 252 | "facts": [ 253 | { 254 | "__comment": "Will be converted to per-second", 255 | "name": "video.displayed_frame" 256 | }, 257 | { 258 | "name": "video.width" 259 | }, 260 | { 261 | "name": "video.height" 262 | } 263 | ] 264 | }, 265 | { 266 | "name": "Video link throughput", 267 | "type": "VideoBitrateWidget", 268 | "x": -250, 269 | "y": 56, 270 | "icon_path": "network.png", 271 | "template": "%f Mbps", 272 | "per_second_window_s": 2, 273 | "per_second_bucket_ms": 100, 274 | "facts": [ 275 | { 276 | "__comment": "Should be sum per-second, scaled to Megs", 277 | "name": "gstreamer.received_bytes" 278 | } 279 | ] 280 | }, 281 | { 282 | "name": "WFB radio", 283 | "type": "IconTplTextWidget", 284 | "x": -250, 285 | "y": 86, 286 | "icon_path": "wfb.png", 287 | "template": "RSSI %d ", 288 | "facts": [ 289 | { 290 | "name": "mavlink.radio_status.rssi", 291 | "tags": { 292 | "sysid": "3", 293 | "compid": "68" 294 | } 295 | } 296 | ] 297 | }, 298 | { 299 | "name": "WFB radio SNR", 300 | "type": "TplTextWidget", 301 | "x": -110, 302 | "y": 86, 303 | "template": "| SNR %d", 304 | "facts": [ 305 | { 306 | "name": "wfbcli.rx.ant_stats.snr_min", 307 | "tags": { 308 | "ant_id": "0", 309 | "id": "video rx" 310 | } 311 | } 312 | ] 313 | }, 314 | { 315 | "name": "WFB radio package lost", 316 | "type": "IconTplTextWidget", 317 | "x": -250, 318 | "y": 116, 319 | "icon_path": "wfb.png", 320 | "template": "LOST %u ", 321 | "facts": [ 322 | { 323 | "name": "mavlink.radio_status.rxerrors", 324 | "tags": { 325 | "sysid": "3", 326 | "compid": "68" 327 | } 328 | } 329 | ] 330 | }, 331 | { 332 | "name": "WFB radio package fec", 333 | "type": "TplTextWidget", 334 | "x": -110, 335 | "y": 116, 336 | "template": "| FEC %u", 337 | "facts": [ 338 | { 339 | "name": "mavlink.radio_status.fixed", 340 | "tags": { 341 | "sysid": "3", 342 | "compid": "68" 343 | } 344 | } 345 | ] 346 | }, 347 | { 348 | "name": "DVR status", 349 | "type": "DvrStatusWidget", 350 | "x": -250, 351 | "y": 146, 352 | "icon_path": "sdcard-white.png", 353 | "text": "Recording", 354 | "facts": [ 355 | { 356 | "name": "dvr.recording" 357 | } 358 | ] 359 | }, 360 | { 361 | "name": "Custom fading message", 362 | "type": "PopupWidget", 363 | "x": 400, 364 | "y": 50, 365 | "timeout_ms": 10000, 366 | "facts": [ 367 | { 368 | "name": "osd.custom_message" 369 | } 370 | ] 371 | }, 372 | { 373 | "name": "msposd", 374 | "type": "ExternalSurfaceWidget", 375 | "x": 0, 376 | "y": 0, 377 | "width": 0, 378 | "height": 0, 379 | "facts": [] 380 | } 381 | ] 382 | } 383 | -------------------------------------------------------------------------------- /gs/oled.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | 3 | import time 4 | import psutil 5 | import glob 6 | import os 7 | import subprocess 8 | import re 9 | from luma.core.interface.serial import i2c 10 | from luma.oled.device import ssd1306 11 | from luma.core.render import canvas 12 | from PIL import ImageFont 13 | from dotenv import dotenv_values 14 | from smbus2 import SMBus, i2c_msg 15 | from pathlib import Path 16 | # import socket 17 | 18 | # ========================================================== 19 | # system 20 | # ========================================================== 21 | gs_conf = dotenv_values("/etc/gs.conf") 22 | oled_refresh_interval = int(gs_conf['oled_refresh_interval']) 23 | oled_port = int(gs_conf['oled_i2c_port']) 24 | oled_address = gs_conf['oled_i2c_address'] 25 | # init oled screen 26 | serial = i2c(port=oled_port, address=oled_address) 27 | device = ssd1306(serial, rotate=0) 28 | # device = ssd1306(serial, width=128, height=64) 29 | # print(device.width, device.height) 30 | 31 | # Define font sizes 32 | font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 8) 33 | font_medium = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 10) 34 | font_large = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 12) 35 | # font = ImageFont.load_default() 36 | 37 | def get_wifi_info(): 38 | channel, frequency, width = "", "", "" 39 | matches = glob.glob('/sys/class/net/wl*') 40 | if matches: 41 | interface = os.path.basename(matches[0]) 42 | try: 43 | # run iw command 44 | result = subprocess.run(['iw', interface, 'info'], capture_output=True, text=True, check=True) 45 | output = result.stdout 46 | 47 | # get channel, freq and bandwidth 48 | match = re.search(r'channel\s+(\d+)\s+\((\d+)\s+MHz\),\s+width:\s+([\d]+),?', output) 49 | if match: 50 | channel = match.group(1) 51 | frequency = match.group(2) 52 | width = match.group(3).strip() 53 | else: 54 | print("No matching information found.") 55 | except subprocess.CalledProcessError as e: 56 | print(f"Command execution failed: {e}") 57 | else: 58 | print("No wifi card found.") 59 | return f"CH:{channel}/{frequency}/{width}MHz" 60 | 61 | def get_ip_addresses(): 62 | ip_list = [] 63 | for iface, addrs in psutil.net_if_addrs().items(): 64 | for addr in addrs: 65 | if addr.family == 2: 66 | ip_list.append((iface, addr.address)) 67 | return ip_list 68 | 69 | def get_cpu(): 70 | cpu_usage = psutil.cpu_percent(interval=1) 71 | try: 72 | with open("/sys/class/thermal/thermal_zone0/temp", "r") as f: 73 | temp = round(int(f.read()) / 1000, 1) 74 | except: 75 | temp = "N/A" 76 | 77 | return f"CPU:{cpu_usage}% {temp}°C" 78 | 79 | def get_mem(): 80 | mem = psutil.virtual_memory() 81 | mem_str="Mem:{}/{}M {}%".format( 82 | int(mem.used/(1024*1024)), 83 | int(mem.total/(1024*1024)), 84 | mem.percent) 85 | return mem_str 86 | 87 | def get_root_disk(): 88 | disk = psutil.disk_usage('/') 89 | disk_str="Disk:{}/{}G {}%".format( 90 | int(disk.used/(1024*1024*1024)), 91 | int(disk.total/(1024*1024*1024)), 92 | disk.percent) 93 | return disk_str 94 | 95 | def get_rec_disk(): 96 | disk = psutil.disk_usage('/Videos') 97 | disk_str="REC:{}/{}G {}%".format( 98 | int(disk.used/(1024*1024*1024)), 99 | int(disk.total/(1024*1024*1024)), 100 | disk.percent) 101 | return disk_str 102 | 103 | # ========================================================== 104 | # ina226 105 | # ========================================================== 106 | # I2C bus number 107 | I2C_DEVICE = oled_port 108 | 109 | # INA226 I2C device address 110 | INA226_ADDR = int(gs_conf['ina226_i2c_address'], 16) 111 | # Global flag, ina226 available or not 112 | ina226_available = False 113 | 114 | # Register addresses 115 | REG_CONFIG = 0x00 116 | REG_SHUNTV = 0x01 # Shunt voltage 117 | REG_BUSV = 0x02 # Bus voltage 118 | REG_POWER = 0x03 # Power 119 | REG_CURRENT = 0x04 # Current 120 | REG_CALIB = 0x05 # Calibration register 121 | 122 | # Configuration parameters 123 | SHUNT_RESISTOR = 0.1 # Shunt resistor value (unit: Ω) 124 | CURRENT_LSB = 0.001 # Current resolution (unit: A) 125 | 126 | # Initialize I2C bus 127 | bus = SMBus(I2C_DEVICE) 128 | 129 | def detect_ina226(): 130 | """Check whether INA226 exists on the I2C bus""" 131 | global ina226_available 132 | if gs_conf['ina226_kernel_driver'] == 'no': 133 | try: 134 | msg = i2c_msg.write(INA226_ADDR, []) 135 | bus.i2c_rdwr(msg) 136 | ina226_available = True 137 | print("INA226 detected on I2C bus.") 138 | return True 139 | except Exception: 140 | ina226_available = False 141 | print("INA226 not detected, skip power monitoring.") 142 | return False 143 | elif gs_conf['ina226_kernel_driver'] == 'yes': 144 | hwmon_path = None 145 | for name_file in Path("/sys/class/hwmon").glob("hwmon*/name"): 146 | try: 147 | if name_file.read_text().strip() == "ina226": 148 | hwmon_path = name_file.parent 149 | break 150 | except Exception: 151 | continue 152 | if hwmon_path: 153 | ina226_available = hwmon_path 154 | print(f"INA226 kernel device detected {hwmon_path}.") 155 | return True 156 | else: 157 | ina226_available = False 158 | print("INA226 kernel device not detected, skip power monitoring.") 159 | return False 160 | 161 | def init_ina226(): 162 | global ina226_available 163 | if not ina226_available: 164 | return 165 | elif ina226_available is True: 166 | try: 167 | # Calculate calibration value: CAL = 0.00512 / (CURRENT_LSB * SHUNT_RESISTOR) 168 | cal_value = int(0.00512 / (CURRENT_LSB * SHUNT_RESISTOR)) 169 | 170 | # Configure register: continuous mode, sampling rate, etc. 171 | # 0x4127 = average 16 samples, conversion time 1ms 172 | config = 0x4127 173 | bus.write_i2c_block_data(INA226_ADDR, REG_CONFIG, [(config >> 8) & 0xFF, config & 0xFF]) 174 | bus.write_i2c_block_data(INA226_ADDR, REG_CALIB, [(cal_value >> 8) & 0xFF, cal_value & 0xFF]) 175 | except Exception as e: 176 | print("INA226 init failed:", e) 177 | ina226_available = False 178 | else: 179 | print("INA226 is already inited by kernel driver.") 180 | 181 | def read_sensor(): 182 | if not ina226_available: 183 | return 0.0, 0.0, 0.0 184 | elif ina226_available is True: 185 | try: 186 | # Read bus voltage (unit: V) 187 | bus_voltage_raw = bus.read_i2c_block_data(INA226_ADDR, REG_BUSV, 2) 188 | bus_voltage = (bus_voltage_raw[0] << 8 | bus_voltage_raw[1]) * 0.00125 # LSB=1.25mV 189 | 190 | # Read current (unit: A) 191 | current_raw = bus.read_i2c_block_data(INA226_ADDR, REG_CURRENT, 2) 192 | current = (current_raw[0] << 8 | current_raw[1]) * CURRENT_LSB 193 | 194 | # Read power (unit: W) 195 | power_raw = bus.read_i2c_block_data(INA226_ADDR, REG_POWER, 2) 196 | power = (power_raw[0] << 8 | power_raw[1]) * CURRENT_LSB * 25 # LSB=25*CURRENT_LSB 197 | 198 | return bus_voltage, current, power 199 | except Exception as e: 200 | print("INA226 read error:", e) 201 | return 0.0, 0.0, 0.0 202 | else: 203 | hwmon_path = ina226_available 204 | def read_value(file_path): 205 | try: 206 | with open(file_path, "r") as f: 207 | return int(f.read().strip()) 208 | except Exception as e: 209 | print(f"Error reading {file_path}: {e}") 210 | return 0.0 211 | 212 | in1_input = read_value(os.path.join(hwmon_path, "in1_input")) 213 | curr1_input = read_value(os.path.join(hwmon_path, "curr1_input")) 214 | power1_input = read_value(os.path.join(hwmon_path, "power1_input")) 215 | return in1_input/1000, curr1_input/1000, power1_input/1000000 216 | 217 | # ========================================================== 218 | # display 219 | # ========================================================== 220 | def display_system_info(device, delay): 221 | if detect_ina226(): 222 | init_ina226() 223 | while True: 224 | ip_entries = get_ip_addresses() 225 | if not ip_entries: 226 | ip_entries = [("None", "No IP")] 227 | for iface, ip in ip_entries: 228 | with canvas(device) as draw: 229 | # Display all white 230 | # draw.rectangle(device.bounding_box, outline="white", fill="white") 231 | draw.text((0, 0), f"{iface}:{ip}", font=font_medium, fill=255) 232 | draw.text((0, 10), get_cpu(), font=font_medium, fill=255) 233 | draw.text((0, 20), get_mem(), font=font_medium, fill=255) 234 | if ina226_available: 235 | voltage, current, power = read_sensor() 236 | draw.text((0, 30), f"DC:{voltage:.1f}V|{current:.1f}A|{power:.1f}W", font=font_medium, fill=255) 237 | else: 238 | draw.text((0, 30), get_root_disk(), font=font_medium, fill=255) 239 | draw.text((0, 40), get_wifi_info(), font=font_large, fill=255) 240 | draw.text((0, 52), get_rec_disk(), font=font_large, fill=255) 241 | 242 | time.sleep(delay) 243 | 244 | # ========================================================== 245 | # main function 246 | # ========================================================== 247 | if __name__ == "__main__": 248 | try: 249 | display_system_info(device, oled_refresh_interval) 250 | except KeyboardInterrupt: 251 | device.clear() 252 | -------------------------------------------------------------------------------- /gs/gs.conf: -------------------------------------------------------------------------------- 1 | ## Network Configuration 2 | # WiFi device integrated by radxa zero 3W is named wifi0 in OS. 3 | # wifi_mode can be station or hotspot 4 | wifi_mode='hotspot' 5 | # If SSID and Password is not configured, system will automatically connect to an open WiFi named OpenIPC. 6 | wifi_ssid='OpenIPC' 7 | wifi_encryption='wpa-psk' 8 | wifi_password='' 9 | hotspot_ssid='SBC-GS' 10 | hotspot_password='12345678' 11 | hotspot_ip='192.168.4.1/24' 12 | # Ethernet device integrated by radxa zero 3E is named eth0 in OS. 13 | # External USB tethering NIC named usb0 14 | # Bridged eth0 and usb0 to br0 15 | br0_fixed_ip='192.168.1.20/24' 16 | br0_fixed_ip2='10.0.36.254/24' 17 | # Radxa Gadget USB NIC named radxa0 18 | gadget_net_fixed_ip='192.168.2.20/24' 19 | 20 | ## Video Configuration 21 | # If you want to boot into the terminal, set video_on_boot to no. 22 | video_on_boot='yes' 23 | # CAUTION: Invalid screen mode settings may cause pixelpilot fail to start. 24 | # keep screen_mode empty for auto detect, recommended set manually only when preferred screen mode is not the best mode 25 | # screen_mode support x@(e.g. 1920x1080@60), max-fps, max-res and empty. 26 | screen_mode='' 27 | # video_player can be pixelpilot or gstreamer. 28 | video_player='pixelpilot' 29 | video_codec='h265' 30 | # use fmp4(fragmented mp4) container format for DVR or not 31 | dvr_fmp4='yes' 32 | # pixelpilot enables vsync by default, set yes to disable vsync. 33 | disable_vsync='no' 34 | # enable pixelpilot gsmenu or not. gamenu is disabled by pixelpilot default 35 | gsmenu_enable='yes' 36 | # pixelpilot support multi osd types, gstreamer only support wfb-ng-osd 37 | osd_enable='yes' 38 | # following OSD options is only for pixelpilot. 39 | # pixelpilot osd fps 40 | osd_fps='20' 41 | # osd_type can be mavlink, msposd_air, msposd_gs 42 | osd_type='msposd_air' 43 | # enable or disable OS monitoring (power, CPU, temperature) widgets 44 | osd_widgets_osmon='yes' 45 | # msposd_gs_ options only valid when osd_type set to msposd_gs 46 | # enable or disable msposd OSD/SRT Recoding 47 | msposd_gs_record='yes' 48 | # msposd_gs_method can be tunnel or wfbrx 49 | msposd_gs_method='tunnel' 50 | msposd_gs_port='14551' 51 | msposd_gs_fps='20' 52 | msposd_gs_ahi='0' 53 | # osd_config_file must be the absolute path of pixelpilot osd json config file. e.g. `/config/pixelpilot_osd_custom.json`. 54 | # If osd_config_file is not set, will auto select as follows 55 | # * mavlink => /etc/pixelpilot/pixelpilot_osd.json 56 | # * msposd_air => /etc/pixelpilot/pixelpilot_osd_simple.json 57 | # * msposd_gs => /etc/pixelpilot/pixelpilot_msposd.json 58 | osd_config_file='' 59 | 60 | ## Record Configuration 61 | # Folder for saving video files. 62 | rec_dir='/Videos' 63 | # Minimum remaining space(MB) of record partition 64 | rec_dir_freespace_min='1000' 65 | # CAUTION: Must be the same value setting in IPC. 66 | rec_fps='60' 67 | # Auto recording Configuration 68 | # * keep blank will recording manually 69 | # * set to boot will auto start recording when system startup 70 | # * set to arm will start recording when FC armed(only working with pixelpilot) 71 | record_on='' 72 | 73 | ## Wifibroadcast Configuration 74 | # Use integrated WiFi card for wfb, hotplug script only working with USB WiFi. 75 | # If you want use wifi0 (sdio Wifi integrated by Radxa zero 3W) for wfb, set wfb_integrated_wnic='wifi0' 76 | wfb_integrated_wnic='' 77 | # wfb_mode can be standalone, cluster, aggregator. 78 | # standalone mode: Offical mode supported by wfb-ng. start wifibroadcast@gs.service on boot. Auto generate /etc/wifibroadcast.cfg /etc/default/wifibroadcast 79 | # and restart wifibroadcast@gs.services when USB WiFi plug and unplug by udev rules. 80 | # cluster mode: Offical mode supported by wfb-ng. start wfb-cluster-manager@gs.service on boot. 81 | # start wfb_tx forwarder for each USB WiFi. Auto generate /etc/wifibroadcast.cfg 82 | # and restart wfb-cluster-manager@gs.services when USB WiFi plug and unplug by udev rules. 83 | # You can get stream from USB WiFi or external devices like an OpenWrt router via Ethernet. 84 | # aggregator mode: Unoffical mode. wfb_rx aggregator running on boot and start wfb_tx forwarder for each USB WiFi. 85 | # You can get stream from USB WiFi or external devices like an OpenWrt router via Ethernet. 86 | # Hot-plugging of USB WiFi or Ethernet will not interrupting data stream. 87 | # Rx only and not support wfb_tunnel. 88 | # WARING: cluster and aggregator mode may introduce additional delays(~1ms?) 89 | wfb_mode='standalone' 90 | wfb_key='/etc/gs.key' 91 | # Enable or disable wfb_rtsp_server, rtsp://x.x.x.x:8554/wfb 92 | wfb_rtsp_server_enable='no' 93 | # Default interval for statistics reporting in ms. 94 | wfb_log_interval='1000' 95 | 96 | # WiFi Monitor Configuration 97 | wfb_channel='161' 98 | wfb_region='00' 99 | wfb_bandwidth='20' 100 | # Set tx power in mbm(0~3000) 101 | wfb_txpower='' 102 | 103 | # wfb general Configuration 104 | wfb_link_id='7669206' 105 | wfb_stream_id_video='0' 106 | wfb_stream_id_mavlink='16' 107 | wfb_stream_id_tunnel='32' 108 | # Output video stream to ip or socket 109 | wfb_outgoing_video='ip' 110 | # use 224.0.0.1 for multicast and USB tethering push stream support 111 | # use 127.0.0.1 for localhost 112 | wfb_outgoing_ip='224.0.0.1' 113 | wfb_outgoing_port_video='5600' 114 | wfb_outgoing_port_mavlink='14550' 115 | 116 | ## APFPV Configuration 117 | ap_wifi_ssid='OpenIPC' 118 | ap_wifi_password='12345678' 119 | ap_wifi_ip='192.168.0.10/24' 120 | 121 | ## GPIO Configuration 122 | # Button layout support custom, rubyfpv, bonnet and runcam, 123 | # gpio pin will set automatically unless use custom layout 124 | btn_pin_layout='custom' 125 | # Quick Button 126 | btn_q1_pin='32' 127 | btn_q2_pin='38' 128 | btn_q3_pin='40' 129 | # Custom Button 130 | btn_cu_pin='16' 131 | btn_cd_pin='18' 132 | btn_cl_pin='13' 133 | btn_cr_pin='11' 134 | btn_cm_pin='' 135 | # Red LED (recommend for record) 136 | red_led_pin='22' 137 | # LED GPIO drive mode can be push-pull or open-drain. 138 | # If LED direct connected to GPIO, should use `push-pull` drive mode 139 | # If LED is connected to a pull-up resistor, should use `open-drain` mode 140 | red_led_drive='push-pull' 141 | # Green LED (recommend for power) 142 | green_led_pin='15' 143 | green_led_drive='push-pull' 144 | # Blue LED 145 | blue_led_pin='12' 146 | blue_led_drive='push-pull' 147 | 148 | ## Button Function Configuration 149 | # Supported Functions 150 | # * toggle_record 151 | # * toggle_stream 152 | # * cleanup_record_files 153 | # * change_wifi_mode 154 | # * change_otg_mode 155 | # * scan_wfb_channel 156 | # * apply_conf 157 | # * shutdown_gs 158 | # * reboot_gs 159 | # Quick Button Function 160 | btn_q1_single_press='toggle_record' 161 | btn_q1_long_press='cleanup_record_files' 162 | btn_q2_single_press='scan_wfb_channel' 163 | btn_q2_long_press='change_otg_mode' 164 | btn_q3_single_press='' 165 | btn_q3_long_press='change_wifi_mode' 166 | # Custom Button Function 167 | btn_cu_single_press='' 168 | btn_cu_long_press='' 169 | btn_cd_single_press='' 170 | btn_cd_long_press='' 171 | btn_cl_single_press='' 172 | btn_cl_long_press='' 173 | btn_cr_single_press='' 174 | btn_cr_long_press='' 175 | btn_cm_single_press='' 176 | btn_cm_long_press='' 177 | 178 | ## System Configuration 179 | # Setting to no will interrupt gs startup for debugging. 180 | gs_enable='yes' 181 | # Support wfb, ap and rubyfpv 182 | fpv_firmware_type='wfb' 183 | # Enable or disable SBC GS CC Edition WebUI 184 | webui_enable='yes' 185 | # Enable or disable I2C OLED screen, SSD1306 128x64 oled is recommend 186 | oled_enable='yes' 187 | # OLED refresh interval in seconds 188 | oled_refresh_interval=2 189 | # I2C port used by oled and ina226 190 | oled_i2c_port='5' 191 | # oled screen i2c address 192 | oled_i2c_address='0x3C' 193 | # Use ina2xx kernel driver or not. Edit dts file to config I2C bus and address, then run 194 | # dtc -I dts -O dtb -o /boot/dtbo/rk3566-ina226.dtb* /gs/rk3566-ina226-overlay.dts 195 | ina226_kernel_driver='no' 196 | # ina226 i2c address, default 0x40 on OpenIPC bonnet. Not for ina2xx kernel driver 197 | ina226_i2c_address='0x40' 198 | # Rootfs reserved space in MB. 199 | rootfs_reserved_space='1500' 200 | # disable radxa zero 3W integrated WiFi 201 | disable_integrated_wifi='no' 202 | # use external antenna or not for integrated WiFi on radxa zero 3w 203 | enable_external_antenna='no' 204 | # Set max resolution to 3820x2160(default max resolution is 1920x1080) 205 | # CAUTION: Not compat with some monitors which may result in no display at all 206 | max_resolution_4k='no' 207 | # Enable GPIO functions using dtb overlay 208 | # Leave blank to set manually 209 | dtbo_enable_list='pwm14-m0 uart3-m0 i2c4-m0 i2c5-m0' 210 | # OTG mode can be host or device 211 | otg_mode='device' 212 | # Use external DS3231 RTC 213 | use_external_rtc='no' 214 | # Append to kernel cmdline, e.g. console=ttyS3,115200n8 215 | append_kernel_cmdline='' 216 | # set screen mode for system wide 217 | # Tips: set system_wide_screen_mode to yes and add "D" at the end of screen_mode 218 | # can force output when monitor is not connected or not recognized. e.g. 1920x1080@60D 219 | # Waring: wrong screen mode will cause the screen to be completely blank 220 | system_wide_screen_mode='no' 221 | # enable gps will auto use gps time 222 | use_gps='no' 223 | # uart need enable in dtbo, ttyS3 is enabled by GS default 224 | gps_uart='ttyS3' 225 | gps_uart_baudrate='38400' 226 | # Enable or Disable web tty. Default port is 81 227 | ttyd_enable='yes' 228 | # Enable or Disable Adaptive Link 229 | alink_enable='no' 230 | 231 | ## Cooling Configuration 232 | fan_service_enable='yes' 233 | # Must enable the following pwm chip and channel in dtbo 234 | fan_pwm_chip='14' 235 | fan_pwm_channel='0' 236 | fan_pwm_frequency='25000' 237 | # If fan direct connected to pwm pin, should set polarity to normal 238 | # If fan connected to a pull-up resistor, should set polarity to inversed 239 | fan_pwm_polarity='normal' 240 | fan_pwm_min_duty_cycle='0' 241 | fan_pwm_max_duty_cycle='80' 242 | fan_pwm_step_duty_cycle='5' 243 | fan_target_temperature='50' 244 | fan_target_temperature_deviation='3' 245 | fan_overheat_temperature='70' 246 | monitor_8812eu_temperature='yes' 247 | rtl8812eu_temperature_offset='-1' 248 | temperature_monitor_cycle='10' 249 | 250 | ## LED Configuration 251 | # otg mode led 252 | otg_mode_led_pin='green_led_pin' 253 | otg_mode_led_drive='green_led_drive' 254 | -------------------------------------------------------------------------------- /gs/gs-applyconf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | # merge custom.conf to gs.conf 7 | if [ -f /config/custom.conf ]; then 8 | grep -E '^\s*[^#]' /config/custom.conf | while IFS='=' read -r ckey cvalue; do 9 | sed -i "s/^${ckey}=.*/${ckey}=${cvalue}/" $(readlink -f /etc/gs.conf) 10 | done 11 | mv /config/custom.conf /config/custom-merged.conf 12 | source /etc/gs.conf 13 | fi 14 | 15 | # apply button gpio layout 16 | if [ "$btn_pin_layout" != "custom" ]; then 17 | button_layout_bonnet="16,18,13,12,11,22,," 18 | button_layout_rubyfpv="16,18,13,11,,32,38,40" 19 | button_layout_runcam="16,18,13,38,11,32,," 20 | button_layout_conf=button_layout_${btn_pin_layout} 21 | button_pins_conf="${btn_cu_pin},${btn_cd_pin},${btn_cl_pin},${btn_cr_pin},${btn_cm_pin},${btn_q1_pin},${btn_q2_pin},${btn_q3_pin}" 22 | 23 | if [ "$button_pins_conf" != "${!button_layout_conf}" ]; then 24 | IFS=',' read -r -a button_pins_arr <<< "${!button_layout_conf}," 25 | sed -i \ 26 | -e "s/btn_cu_pin=.*/btn_cu_pin='${button_pins_arr[0]}'/" \ 27 | -e "s/btn_cd_pin=.*/btn_cd_pin='${button_pins_arr[1]}'/" \ 28 | -e "s/btn_cl_pin=.*/btn_cl_pin='${button_pins_arr[2]}'/" \ 29 | -e "s/btn_cr_pin=.*/btn_cr_pin='${button_pins_arr[3]}'/" \ 30 | -e "s/btn_cm_pin=.*/btn_cm_pin='${button_pins_arr[4]}'/" \ 31 | -e "s/btn_q1_pin=.*/btn_q1_pin='${button_pins_arr[5]}'/" \ 32 | -e "s/btn_q2_pin=.*/btn_q2_pin='${button_pins_arr[6]}'/" \ 33 | -e "s/btn_q3_pin=.*/btn_q3_pin='${button_pins_arr[7]}'/" \ 34 | $(readlink -f /etc/gs.conf) 35 | source /etc/gs.conf 36 | fi 37 | fi 38 | 39 | # load gs.conf if not loded 40 | [ -z "${wifi_mode+defined}" ] && source /etc/gs.conf 41 | 42 | need_reboot=0 43 | need_restart_services="" 44 | cmds_lower_rootfs_rw=() 45 | 46 | ## kernel cmdline configuration 47 | [ -f /etc/kernel/cmdline.bak ] || cp /etc/kernel/cmdline /etc/kernel/cmdline.bak 48 | kernel_cmdline_now=$(< /etc/kernel/cmdline) 49 | kernel_cmdline_base=$(< /etc/kernel/cmdline.bak) 50 | kernel_cmdline_config=$kernel_cmdline_base 51 | # append kernel cmdline 52 | [ -n "$append_kernel_cmdline" ] && kernel_cmdline_config="$kernel_cmdline_config $append_kernel_cmdline" 53 | # system wide screen mode 54 | if [ "$system_wide_screen_mode" == "yes" ]; then 55 | [ -n "$screen_mode" ] && kernel_cmdline_config="$kernel_cmdline_config video=HDMI-A-1:${screen_mode}" 56 | fi 57 | # update kernel cmdline 58 | if [ "$kernel_cmdline_config" != "$kernel_cmdline_now" ]; then 59 | cmds_lower_rootfs_rw+=("[ -f /etc/kernel/cmdline.bak ] || cp /etc/kernel/cmdline /etc/kernel/cmdline.bak") 60 | cmds_lower_rootfs_rw+=("echo '$kernel_cmdline_config' > /media/root-ro/etc/kernel/cmdline") 61 | fi 62 | 63 | ## dtbo configuration 64 | # set max resolution to 4k 65 | if [[ "$max_resolution_4k" == "yes" && -f /boot/dtbo/rk3566-hdmi-max-resolution-4k.dtbo.disabled ]]; then 66 | cmds_lower_rootfs_rw+=("mv /boot/dtbo/rk3566-hdmi-max-resolution-4k.dtbo.disabled /boot/dtbo/rk3566-hdmi-max-resolution-4k.dtbo") 67 | elif [[ "$max_resolution_4k" == "no" && -f /boot/dtbo/rk3566-hdmi-max-resolution-4k.dtbo ]]; then 68 | cmds_lower_rootfs_rw+=("mv /boot/dtbo/rk3566-hdmi-max-resolution-4k.dtbo /boot/dtbo/rk3566-hdmi-max-resolution-4k.dtbo.disabled") 69 | fi 70 | # disable integrated wifi of radxa zero 3W 71 | if [[ "$disable_integrated_wifi" == "yes" && -f /boot/dtbo/radxa-zero3-disabled-wireless.dtbo.disabled ]]; then 72 | cmds_lower_rootfs_rw+=("mv /boot/dtbo/radxa-zero3-disabled-wireless.dtbo.disabled /boot/dtbo/radxa-zero3-disabled-wireless.dtbo") 73 | elif [[ "$disable_integrated_wifi" == "no" && -f /boot/dtbo/radxa-zero3-disabled-wireless.dtbo ]]; then 74 | cmds_lower_rootfs_rw+=("mv /boot/dtbo/radxa-zero3-disabled-wireless.dtbo /boot/dtbo/radxa-zero3-disabled-wireless.dtbo.disabled") 75 | 76 | fi 77 | # enable external antenna of radxa zero 3W 78 | if [[ "$enable_external_antenna" == "yes" && -f /boot/dtbo/radxa-zero3-external-antenna.dtbo.disabled && -d /sys/class/net/wifi0 ]]; then 79 | cmds_lower_rootfs_rw+=("mv /boot/dtbo/radxa-zero3-external-antenna.dtbo.disabled /boot/dtbo/radxa-zero3-external-antenna.dtbo") 80 | elif [[ "$enable_external_antenna" == "no" && -f /boot/dtbo/radxa-zero3-external-antenna.dtbo && -d /sys/class/net/wifi0 ]] ; then 81 | cmds_lower_rootfs_rw+=("mv /boot/dtbo/radxa-zero3-external-antenna.dtbo /boot/dtbo/radxa-zero3-external-antenna.dtbo.disabled") 82 | fi 83 | # INA226 device 84 | if [[ "$ina226_kernel_driver" == "yes" && -f /boot/dtbo/rk3566-ina226-overlay.dtbo.disabled ]]; then 85 | cmds_lower_rootfs_rw+=("mv /boot/dtbo/rk3566-ina226-overlay.dtbo.disabled /boot/dtbo/rk3566-ina226-overlay.dtbo") 86 | elif [[ "$ina226_kernel_driver" == "no" && -f /boot/dtbo/rk3566-ina226-overlay.dtbo ]]; then 87 | cmds_lower_rootfs_rw+=("mv /boot/dtbo/rk3566-ina226-overlay.dtbo /boot/dtbo/rk3566-ina226-overlay.dtbo.disabled") 88 | fi 89 | # dtbo enable or disable 90 | if [ -n "$dtbo_enable_list" ]; then 91 | dtbo_enable_array=$(echo $dtbo_enable_list | tr -s ' ' | tr ' ' '\n' | sort) 92 | dtbo_enabled_array=$(ls /boot/dtbo/rk3568-*.dtbo 2>/dev/null | sed -e "s^/boot/dtbo/rk3568-^^g" -e "s/.dtbo//g" | sort) 93 | dtbo_need_enable=$(comm -23 <(echo "$dtbo_enable_array") <(echo "$dtbo_enabled_array")) 94 | dtbo_need_disable=$(comm -13 <(echo "$dtbo_enable_array") <(echo "$dtbo_enabled_array")) 95 | # enable dtbo 96 | if [ -n "$dtbo_need_enable" ]; then 97 | for dtboe in $dtbo_need_enable; do 98 | if [ -f /boot/dtbo/rk3568-${dtboe}.dtbo.disabled ]; then 99 | cmds_lower_rootfs_rw+=("mv /boot/dtbo/rk3568-${dtboe}.dtbo.disabled /boot/dtbo/rk3568-${dtboe}.dtbo") 100 | fi 101 | done 102 | fi 103 | # disable dtbo 104 | if [ -n "$dtbo_need_disable" ]; then 105 | for dtbod in $dtbo_need_disable; do 106 | cmds_lower_rootfs_rw+=("mv /boot/dtbo/rk3568-${dtbod}.dtbo /boot/dtbo/rk3568-${dtbod}.dtbo.disabled") 107 | done 108 | fi 109 | fi 110 | 111 | ## Update rec_dir in fstab 112 | [ -d $rec_dir ] || mkdir -p $rec_dir 113 | if ! grep -Pq "^/dev/[^\s]*\s*${rec_dir}\s*exfat\s*defaults\,nofail\s*0\s*0" /media/root-ro/etc/fstab; then 114 | echo "[info]: Update rec_dir in fstab and need reboot" 115 | mount -o remount,rw /media/root-ro 116 | sed -i "s#^\(/dev/[^\s]*\s*\)[^\s]*#\1${rec_dir}#" /media/root-ro/etc/fstab 117 | need_reboot=1 118 | fi 119 | 120 | ## GPS configuration 121 | if [[ $(grep -q "stty -F /dev/${gps_uart} ${gps_uart_baudrate}" /etc/default/gpsd) && $(grep -q "DEVICES=\"/dev/${gps_uart}\"" /etc/default/gpsd) ]]; then 122 | echo "GPS configuration not changed!" 123 | else 124 | cat > /etc/default/gpsd << EOF 125 | stty -F /dev/${gps_uart} ${gps_uart_baudrate} 126 | START_DAEMON="true" 127 | DEVICES="/dev/${gps_uart}" 128 | GPSD_OPTIONS="-n -b -G -r" 129 | USBAUTO="true" 130 | EOF 131 | fi 132 | 133 | ## Network configuration 134 | # br0 configuration 135 | if [[ -f /etc/systemd/network/br0.network && -n "$br0_fixed_ip" && -n "$br0_fixed_ip2" ]]; then 136 | br0_fixed_ip_OS=$(grep -m 1 -oP '(?<=Address=).*' /etc/systemd/network/br0.network) 137 | br0_fixed_ip_OS2=$(tac /etc/systemd/network/br0.network | grep -m 1 -oP '(?<=Address=).*') 138 | [ "${br0_fixed_ip_OS}" == "${br0_fixed_ip}" ] || sed -i "s^${br0_fixed_ip_OS}^${br0_fixed_ip}^" /etc/systemd/network/br0.network 139 | [ "${br0_fixed_ip_OS2}" == "${br0_fixed_ip2}" ] || sed -i "s^${br0_fixed_ip_OS2}^${br0_fixed_ip2}^" /etc/systemd/network/br0.network 140 | need_restart_services="$need_restart_services systemd-networkd" 141 | fi 142 | echo "br0 configure done" 143 | 144 | # wifi0 configuration 145 | if [ -z "$wfb_integrated_wnic" ]; then 146 | # managed wifi0 by NetworkManager 147 | [ -f /etc/network/interfaces.d/wfb-wifi0 ] && rm /etc/network/interfaces.d/wfb-wifi0 148 | nmcli device | grep -q "^wifi0.*unmanaged.*" && nmcli device set wifi0 managed yes 149 | 150 | # wifi0 station mode configuration 151 | echo "start configure wifi0 station mode" 152 | # If no connection named radxa, create one to automatically connect to the unencrypted WiFi named OpenIPC. 153 | [ -f /etc/NetworkManager/system-connections/wifi0.nmconnection ] || nmcli con add type wifi ifname wifi0 con-name wifi0 ssid OpenIPC autoconnect no 154 | # If the WiFi configuration in gs.conf is not empty and changes, modify the WiFi connection information according to the configuration file 155 | if [[ -f /etc/NetworkManager/system-connections/wifi0.nmconnection && -n $wifi_ssid && -n $wifi_encryption && -n $wifi_password ]]; then 156 | WIFI_SSID_OS=$(nmcli -g 802-11-wireless.ssid connection show wifi0) 157 | WIFI_Encryption_OS=$(nmcli -g 802-11-wireless-security.key-mgmt connection show wifi0) 158 | WIFI_Password_OS=$(nmcli -s -g 802-11-wireless-security.psk connection show wifi0) 159 | [[ "$WIFI_SSID_OS" == "$wifi_ssid" && "$WIFI_Encryption_OS" == "$wifi_encryption" && "$WIFI_Password_OS" == "$wifi_password" ]] || nmcli con modify wifi0 ssid "${wifi_ssid}" wifi-sec.key-mgmt ${wifi_encryption} wifi-sec.psk "${wifi_password}" 160 | nmcli con down wifi0 && nmcli con up wifi0 161 | fi 162 | echo "wifi0 station mode configure done" 163 | 164 | # wifi0 hotspot mode configuration 165 | echo "start configure wifi0 hotspot mode" 166 | if [[ -f /etc/NetworkManager/system-connections/hotspot.nmconnection && -n $hotspot_ssid && -n $hotspot_password && -n $hotspot_ip ]];then 167 | Hotspot_SSID_OS=$(nmcli -g 802-11-wireless.ssid connection show hotspot) 168 | Hotspot_Password_OS=$(nmcli -s -g 802-11-wireless-security.psk connection show hotspot) 169 | Hotspot_ip_OS=$(nmcli -g ipv4.addresses con show hotspot) 170 | [[ "$Hotspot_SSID_OS" == "$hotspot_ssid" && "$Hotspot_Password_OS" == "$hotspot_password" ]] || nmcli connection modify hotspot ssid "$hotspot_ssid" wifi-sec.psk "$hotspot_password" 171 | [[ "$Hotspot_ip_OS" == $hotspot_ip ]] || nmcli connection modify hotspot ipv4.method shared ipv4.addresses $hotspot_ip 172 | elif [[ -d /sys/class/net/wifi0 && -n $hotspot_ssid && -n $hotspot_password && -n $hotspot_ip ]]; then 173 | nmcli dev wifi hotspot con-name hotspot ifname wifi0 ssid "$hotspot_ssid" password "$hotspot_password" 174 | nmcli connection modify hotspot ipv4.method shared ipv4.addresses $hotspot_ip autoconnect yes 175 | else 176 | echo "no wifi0 or hotspot setting is blank" 177 | fi 178 | 179 | # Fallback to hotspot mode if wifi connect failed 180 | [[ -d /sys/class/net/wifi0 && "$wifi_mode" == "station" ]] && ( sleep 15; nmcli connection up wifi0 || nmcli con up hotspot ) & 181 | fi 182 | 183 | # radxa0 dnsmasq configuration 184 | if [[ -f /etc/network/interfaces.d/radxa0 && -n "$gadget_net_fixed_ip" ]]; then 185 | # Check whether the configuration in gs.conf is consistent with radxa0. If not, update it. 186 | radxa0_fixed_ip_OS=$(grep -oP "(?<=address\s).*" /etc/network/interfaces.d/radxa0) 187 | [ "$radxa0_fixed_ip_OS" == "${gadget_net_fixed_ip}" ] || sed -i "s^${radxa0_fixed_ip_OS}^${gadget_net_fixed_ip}^" /etc/network/interfaces.d/radxa0 188 | grep -q "${gadget_net_fixed_ip_addr}" /etc/network/interfaces.d/radxa0 || sed -i "s/--listen-address=.*,12h/--listen-address=${gadget_net_fixed_ip_addr} --dhcp-range=${gadget_net_fixed_ip_sub}.11,${gadget_net_fixed_ip_sub}.20,12h/" /etc/network/interfaces.d/radxa0 189 | fi 190 | echo "radxa0 usb gadget network configure done" 191 | 192 | # Update rec_dir in smb.conf 193 | grep -q "$rec_dir" /etc/samba/smb.conf || ( sed -i "/\[Videos\]/{n;s|.*| ${rec_dir}|;}" /etc/samba/smb.conf && need_restart_services="$need_restart_services smbd nmbd") 194 | 195 | # some configuration need reboot to take effect 196 | if [ "${#cmds_lower_rootfs_rw[@]}" -ne 0 ]; then 197 | mount -o remount,rw /media/root-ro 198 | for cmd in "${cmds_lower_rootfs_rw[@]}"; do 199 | echo "[info]: Running in lower rootfs rw: $cmd" 200 | chroot /media/root-ro /bin/bash -c "$cmd" 201 | done 202 | chroot /media/root-ro /usr/sbin/u-boot-update 203 | reboot 204 | fi 205 | [ "$need_reboot" == "1" ] && reboot 206 | [ -n "$need_restart_services" ] && systemctl restart $need_restart_services 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Main Features 2 | ------------- 3 | 4 | * __Centralize configuration file in fat32 partition.__ Use a single configuration file to configure almost everything you need. The configuration file path is `/config/gs.conf` and linked to `/etc/gs.conf`. The partition mount to /config is fat32 format, configuration file can be accessed and edited directly on Windows PC. See configuration part for detial. 5 | * __Default wfb key file in fat32 partition.__ `/config/gs.key` and linked to `/etc/gs.key`. 6 | * __Button support.__ There are 8 buttons in plan, they are `up, down, left, right, middle and quick 1, 2, 3` buttons. 7 | * __Keyboard support.__ Map keyboard key UP, DOWN, LEFT, RIGHT, 1, 2, 3 to button up, down, left, right, quick 1, 2, 3. 8 | * __LED support.__ Currently support `video record LED (Red)` and `power LED (Green)`. The green and red LED turn on after the SBC is powered on. Red LED turn off after the system is startup completed. The red LED will blink when video record is turn on. The green LED will blink when OTG mode is switched to device. 9 | * __Multiple types of network support.__ Support `WiFi`, `Ethernet` and `USB Net`, See Network Configuration for details. 10 | * __Optional video player.__ Support pixelpilot, fpvue, gstreamer as Video player. Gstreamer not support OSD. 11 | * __Multiple USB OTG gadget functions.__ Support `adbd` and `CDC NCM`, `CDC ACM` and `MASS` supported in plan. Use a data cable to connect the computer and the SBC OTG port, adb device and an additional NCM network card will appear on the computer. NCM can auto get ip address with DHCP. 12 | * __Temperature monitoring and active cooling.__ Support monitoring `RK3566` and `RTL8812EU` temperature and automatically adjust PWM fan speed according to temperature. 13 | * __Multiple WiFi card drivers.__ Currently supports `RTL8812AU, RTL8812EU, RTL8812BU, RTL8812CU, RTL8814AU, RTL8731BU`. 14 | * __USB WiFi card hot plug.__ Support multiple USB WiFi cards with Hotplugging. 15 | * __Three wifibroadcast working modes.__ `standalone` mode is the native mode with the best compatibility, but hot-plugging USB WiFi card will briefly interrupted the stream. `aggregator` mode will run wfb_rx aggregator on boot, and run wfb_rx forwarder for each USB WiFi card, hot-plugging USB WiFi card will not interrupt the stram and can receive streams from other external devices through the network like an openwrt router, but may add a little delay (<1ms?). `cluster` mode. 16 | * __Share config and videos with smb.__ Anonymous access with root permissions is enabled by default, which allows you to easily modify configurations, obtain and delete record files. Enter \\192.168.x.x (SBC IP address) in the Windows Explorer address bar to access. 17 | * __Auto extend root partition and rootfs.__ The root partition and rootfs will automatically `expand to the size specified in gs.conf->rootfs_reserved_space` on initial startup. 18 | * __exfat partition for video recordings.__ Automatically create an `exfat partition` using all remaining space during initial startup. The partition will be `mounted to /home/radxa/Videos` for storing video recordings, can get the record files through smb or insert the TF card into the computer. 19 | * __Sequentially increasing video file names.__ Gstreamer record video files name sequentially starting from 1000.mkv, e.g. `1000.mkv, 1001.mkv`. PixelPilot record file name use template `xxxx_record_%Y-%m-%d_%H-%M-%S.mp4`, e.g. `0000_record_2025-02-15_17-26-25.mp4, 0001_record_2025-02-15_17-26-35.mp4`. CAUTION: Radxa zero3 have no RTC battery, datetime may incorrect without internet, gps or external RTC. 20 | * __send stream over USB tethering and Ethernet.__ Video and telemetry stream can send to other device over USB tethering or Ethernet, witch can be played with Android QGroundControl,PixelPilot etc. Notice: share stream using multicast by default, not working with windows QGroundControl. 21 | * __Forward SBC port to IPC over wfb tun.__ Forward SBC port 2222/8080 to IPC port 22/80 over wfb tun. 22 | * __Web terminal.__ Use [ttyd](https://github.com/tsl0922/ttyd) for web terminal. Default port is 81. 23 | * __WFB channel scan.__ 24 | * __Record to external disk.__ Save record files to the first partition of the external disk when plug in, support vfat, exfat, ext4 formats. Recommended to umount or shutdown SBC before unplugging the disk. **WARING:** Do not unplugging the disk when recoding, may corrupt the file system. 25 | * __Version in /etc/gs-release.__ 26 | * __Auto build with github action.__ 27 | 28 | 29 | Configuration [ [gs.conf](https://github.com/zhouruixi/SBC-GS/blob/main/gs/gs.conf) for details ] 30 | ------------------------------------- 31 | 32 | ### 1. Button Configuration 33 | There are some built-in functions that can bind to button behaviors. 34 | * Buttons 35 | + Q1, Q2, Q3, CU, CD, CL, CR. (PIN configured in the GPIO section.) 36 | * Button behaviors 37 | + single press 38 | + long press (Pressing for more than 2 seconds) 39 | * Button functions 40 | + __change_wifi_mode:__ change wifi mode between station and hotspot.(Radxa zero 3W) 41 | + __change_otg_mode:__ change usb otg port between host and device. 42 | + __scan_wfb_channel:__ search wifi channel used by drone. 43 | + __toggle_record:__ start or stop record. 44 | + __toggle_stream:__ start or stop stream service. 45 | + __cleanup_record_files:__ cleanup record files in order of file names until remaining space(MB) of record partition is large than `rec_dir_freespace_min` settings. 46 | **CAUTION:** Second long press in 60 seconds after first long press will delete all record files. 47 | + __apply_conf:__ check and apply configuration in gs.conf. 48 | + __shutdown_gs:__ shutdown ground station. 49 | + __reboot_gs:__ reboot ground station. 50 | + __mount_extdisk:__ mount external disk first partition to record directory. **CAUTION:** This function will auto execute by udev rules when external disk is plug in. Do not use for other purposes. 51 | + __ummount_extdisk:__ umount external disk. 52 | * Default button behavior function 53 | + Q1 54 | - single press: toggle_record 55 | - long press: cleanup_record_files 56 | - second long press: delete all record files(second long press in 60 seconds after first long press). 57 | + Q2 58 | - single press: scan_wfb_channel 59 | - long press: change_otg_mode 60 | + Q3 61 | - single press: null 62 | - long press: change_wifi_mode 63 | + KEY_Q 64 | - single press: null 65 | - long press: Stop monitoring and use the keyboard as normal. Reseat the keyboard to using as buttons. 66 | + KEY_S 67 | - single press: null 68 | - long press: shutdown_gs 69 | + KEY_R 70 | - single press: null 71 | - long press: reboot_gs 72 | 73 | ### 2. GPIO Configuration 74 | Default buttons and LEDs PIN number. 75 | * Quick button PIN 76 | + btn_q1_pin='32' 77 | + btn_q2_pin='38' 78 | + btn_q3_pin='40' 79 | * Custom button PIN 80 | + btn_cu_pin='16' 81 | + btn_cd_pin='18' 82 | + btn_cl_pin='13' 83 | + btn_cr_pin='11' 84 | * LED PIN 85 | + red_led_pin='22' 86 | + green_led_pin='15' 87 | + blue_led_pin='12' 88 | 89 | ### 3. Network Configuration 90 | * __Onboard WiFi:__ `wifi0` 91 | + `station mode:` Default connect to an open WiFi named `OpenIPC` if not configured. 92 | + `hotspot mode:` Default SSID is `SBC-GS` with password `12345678`, IP is `192.168.4.1/24` 93 | * __Bridge:__ `br0` Default `DHCP client` with static IP `192.168.1.20/24, 10.0.36.254/24` 94 | * __Onboard Ethernet:__ `eth0` Default slave of br0. 95 | * __USB Ethernet:__ `eth1` Default slave of br0. 96 | * __USB tethering:__ `usb0` Default slave of br0. 97 | * __USB gadget ncm:__ `radxa0` Default `DHCP server` with static IP `192.168.2.20/24` 98 | 99 | ### 4. Video Configuration 100 | * __video_on_boot:__ used to control showing video or terminal console after startup. Default is `yes`. Set it to `no` will boot into the terminal and only recommended for development and debugging. 101 | * __screen_mode:__ used to set the screen resolution and refresh rate. Support x@(e.g. 1920x1080@60), max-fps, max-res and empty. Default is empty and will auto detect by pixelpilot or SBC. Recommended set it manually only when preferred screen mode is not the best. __CAUTION:__ Resolution is limited to 1920x1080 by radxa, can changed by setting `max_resolution_4k` to `yes` in `System Configuration` section. 102 | * __video_player:__ `pixelpilot` or `gstreamer`. 103 | * __video_codec:__ `h265` or `h264`. 104 | * __osd_enable:__ Enable or disable OSD. pixelpilot video player support mulit OSD types. gstreamer video player only support wfb-ng-osd. 105 | * __osd_fps:__ Pixelpilot OSD refresh rate. 106 | * __osd_type:__ `mavlink`(Native OSD provided by pixelpilot), `msposd_air`(msposd air side rendering), `msposd_gs`(msposd ground side rendering) 107 | * __msposd_gs_method:__ msposd message transmission method. Can be `tunnel`(over wfb tunnel) or `wfbrx`(over wfb tx rx pair) 108 | * __msposd_gs_port:__ The port that msposd listens on and uses to obtain data. 109 | * __msposd_gs_fps:__ Max MSP Display refresh rate. 110 | * __msposd_gs_ahi:__ Graphic AHI mode. 111 | * __osd_config_file:__ pixelpilot's osd config file. Default is blank and auto select according to `osd_type`. Can manually set the configuration file e.g. `/config/pixelpilot_osd_custom.json`. 112 | + mavlink => /etc/pixelpilot/pixelpilot_osd.json 113 | + msposd_air => /etc/pixelpilot/pixelpilot_osd_simple.json 114 | + msposd_gs => /etc/pixelpilot/pixelpilot_msposd.json 115 | 116 | ### 5. Record Configuration 117 | * `rec_dir`: the record storage location. Default is `/home/radxa/Videos`. 118 | * `rec_dir_freespace_min`: the minimum remaining space before recording. When press the record button, if remaining space is lower than this value, it will prompt that there is insufficient space and the recording will not start. Default is `1000`MB. 119 | * `rec_fps`: Record video fps and must same video fps set on drone. Default is `60`. 120 | * __CAUTION:__ OSD will not be recorded. 121 | 122 | ### 6. Wifibroadcast Configuration 123 | 124 | ### 7. System Configuration 125 | #### Recommended GPIO Functions 126 | | Purpose#1 | Recommended Function#1 | Pin#1 | Pin#2 | Recommended Function#2 | Purpose#2 | 127 | | ---------------: | -------------------------: | ----: | ----- | ------------------------- | ---------------- | 128 | | +3.3V | +3.3V | 1 | 2 | +5.0V | +5.0V | 129 | | telemetry | UART3_RX_M0 | 3 | 4 | +5.0V | +5.0V | 130 | | telemetry | UART3_TX_M0 | 5 | 6 | GND | GND | 131 | | PWM_FAN | PWM14_M0 | 7 | 8 | UART2_TX_M0 | DEBUG | 132 | | GND | GND | 9 | 10 | UART2_RX_M0 | DEBUG | 133 | | BTN_R | GPIO3_A1 | 11 | 12 | GPIO3_A3 | BTN_L | 134 | | BTN_U | GPIO3_A2 | 13 | 14 | GND | GND | 135 | | BTN_D | GPIO3_B0 | 15 | 16 | PWM8_M0 / UART4_RX_M1 | AAT_SERVO | 136 | | +3.3V | +3.3V | 17 | 18 | PWM9_M0 / UART4_TX_M1 | AAT_SERVO | 137 | | SPI_SCREEN | SPI3_MOSI_M1 / PWM15_IR_M1 | 19 | 20 | GND | GND | 138 | | SPI_SCREEN | SPI3_MISO_M1 / UART9_TX_M1 | 21 | 22 | GPIO3_C1 | SPI_SCREEN | 139 | | SPI_SCREEN | SPI3_CLK_M1 / PWM14_M1 | 23 | 24 | SPI3_CS0_M1 / UART9_RX_M1 | SPI_SCREEN | 140 | | GND | GND | 25 | 26 | NC | NC | 141 | | COMPASS / USB D+ | I2C4_SDA_M0 / USB D+ | 27 | 28 | I2C4_SCL_M0 / USB D- | COMPASS / USB D- | 142 | | SPI_SCREEN | GPIO3_B3 | 29 | 30 | GND | GND | 143 | | BTN_Q1 | GPIO3_B4 | 31 | 32 | UART5_TX_M1 | GPS | 144 | | GPS | UART5_RX_M1 | 33 | 34 | GND | GND | 145 | | BTN_Q2 | GPIO3_A4 | 35 | 36 | GPIO3_A7 | RECORD_RED_LED | 146 | | BTN_Q3 | GPIO1_A4 | 37 | 38 | GPIO3_A6 | PWR_GREEN_LED | 147 | | GND | GND | 39 | 40 | GPIO3_A5 | RC_BLUE_LED | 148 | 149 | ### 8. Cooling Configuration 150 | 151 | 152 | Files and Services 153 | ------------------ 154 | 155 | * __build files:__ script files for build images 156 | * __workflows files:__ Auto build images using github action 157 | * __gs files:__ 158 | 1. Configuration file `/config/gs.conf` 159 | 2. wfb key file `/config/gs.key` 160 | 3. script files `/home/radxa/gs/[button.sh, channel-scan.sh, fan.sh, gs-init.sh, gs.sh, stream.sh, wfb.sh]` 161 | 4. udev rules in `/etc/udev/rules.d` 162 | * __Services:__ 163 | 1. `gs`.service 164 | 2. `stream`.service (temporary unit) 165 | 3. `button`.service (temporary unit) 166 | 4. `fan`.service (temporary unit) 167 | 5. unnamed temporary services started for each USB WiFi card in wfb aggregator mode 168 | ```bash 169 | GS Directory Tree 170 | / 171 | ├── boot 172 | │   └── dtbo 173 | │   ├── rk3566-dwc3-otg-role-switch.dtbo 174 | │   └── rk3566-hdmi-max-resolution-4k.dtbo.disabled 175 | ├── config 176 | │   ├── custom-sample.conf 177 | │   ├── gs.conf 178 | │   └── gs.key 179 | ├── etc 180 | │   ├── alink.conf -> /config/alink.conf 181 | │   ├── default 182 | │   │   └── wifibroadcast -> /tmp/wifibroadcast 183 | │   ├── gs.conf -> /config/gs.conf 184 | │   ├── gs.key -> /config/gs.key 185 | │   ├── gs-release 186 | │   ├── iptables 187 | │   │   └── rules.v4 188 | │   ├── network 189 | │   │   └── interfaces.d 190 | │   │   └── radxa0 191 | │   ├── NetworkManager 192 | │   │   ├── conf.d 193 | │   │   │   └── 00-gs-unmanaged.conf 194 | │   │   └── system-connections 195 | │   │   ├── hotspot.nmconnection 196 | │   │   └── wifi0.nmconnection 197 | │   ├── pixelpilot 198 | │   │   ├── pixelpilot_msposd.json 199 | │   │   ├── pixelpilot_osd.json 200 | │   │   └── pixelpilot_osd_simple.json 201 | │   ├── samba 202 | │   │   └── smb.conf 203 | │   ├── systemd 204 | │   │   ├── network 205 | │   │   │   ├── br0.netdev 206 | │   │   │   ├── br0.network 207 | │   │   │   ├── dummy0.netdev 208 | │   │   │   ├── dummy0.network 209 | │   │   │   ├── eth0.network 210 | │   │   │   ├── eth1.network 211 | │   │   │   └── usb0.network 212 | │   │   └── system 213 | │   │   ├── gs-init.service 214 | │   │   ├── gs.service 215 | │   │   ├── multi-user.target.wants 216 | │   │   │   ├── gs-init.service -> /etc/systemd/system/gs-init.service 217 | │   │   │   └── gs.service -> /etc/systemd/system/gs.service 218 | │   │   ├── ttyd.service 219 | │   │   └── webui.service 220 | │   ├── udev 221 | │   │   └── rules.d 222 | │   │   ├── 98-rename.rules 223 | │   │   └── 99-GS.rules 224 | │   └── wifibroadcast.cfg -> /tmp/wifibroadcast.cfg 225 | ├── gs 226 | │   ├── button-kbd.py 227 | │   ├── button.sh 228 | │   ├── channel-scan.sh 229 | │   ├── fan.sh 230 | │   ├── gs-applyconf.sh 231 | │   ├── gs-init.sh 232 | │   ├── gs.sh 233 | │   ├── rk3566-dwc3-otg-role-switch.dts 234 | │   ├── rk3566-hdmi-max-resolution-4k.dts 235 | │   ├── stream.sh 236 | │   ├── webui 237 | │   │   ├── plotter.py 238 | │   │   ├── requirements.txt 239 | │   │   ├── settings_webui.yaml 240 | │   │   ├── static 241 | │   │   │   ├── css 242 | │   │   │   │   └── bootstrap.min.css 243 | │   │   │   └── js 244 | │   │   │   ├── bootstrap.bundle.min.js 245 | │   │   │   ├── jquery-3.6.0.min.js 246 | │   │   │   └── webui.js 247 | │   │   ├── templates 248 | │   │   │   ├── filemanager.html 249 | │   │   │   ├── index.html 250 | │   │   │   ├── plotter.html 251 | │   │   │   └── viewer.html 252 | │   │   ├── venv -> python-venv 253 | │   │   └── webui.py 254 | │   └── wfb.sh 255 | ├── tmp 256 | │   ├── wifibroadcast.cfg 257 | │   └── wifibroadcast.default 258 | ├── usr 259 | │   └── local 260 | │   └── bin 261 | │   ├── alink 262 | │   ├── msposd 263 | │   ├── pixelpilot 264 | │   ├── ttyd 265 | │   └── wfb-ng-osd 266 | └── Videos 267 | 268 | ``` 269 | 270 | 271 | Hardware 272 | -------- 273 | 274 | __Designed for and tested on Radxa Zero 3W/3E only.__ 275 | * __Buttons:__ All buttons must connect to 3.3V. 276 | * __LEDs:__ 277 | 1. GPIO work in `Push-Pull` mode. `GPIO->Resistor->LED->GND` 278 | 2. GPIO work in `Open-Drain` mode. 279 | ``` 280 | 3V3->Resistor--->LED->GND 281 | │   282 | GPIO 283 | ``` 284 | 285 | 286 | Troubleshooting 287 | --------------- 288 | 289 | 1. Access to SBC console. Default username/password is radxa/radxa and root/root 290 | * ssh over network with wireless(hotspot/station), ethernet or usb gadget ncm. 291 | * [serial console](https://docs.radxa.com/en/zero/zero3/low-level-dev/serial) with usb uart. 292 | * terminal console with keyboard. 293 | * adb with usb otg. 294 | 2. Check gs service logs 295 | * `systemctl status gs` 296 | * `journalctl -u gs` 297 | 298 | 299 | TODO 300 | ---- 301 | 302 | * Change the built-in LED to a normal GPIO LED 303 | * Screenshots 304 | * Record video playback 305 | 306 | 307 | Known issues 308 | ------------ 309 | 310 | * When WiFi uses 192.168.1.0/24 network segment, SBC cannot be accessed via WiFi. Temporary solution: Modify br0 default network configuration, e.g. modify `br0_fixed_ip` to `192.168.3.20/24`. 311 | -------------------------------------------------------------------------------- /gs/pixelpilot_osd.json: -------------------------------------------------------------------------------- 1 | { 2 | "format": "0.0.1", 3 | "assets_dir": "/usr/local/share/pixelpilot/", 4 | "widgets": [ 5 | { 6 | "name": "WFB RSSI chart", 7 | "type": "BarChartWidget", 8 | "x": -320, 9 | "y": 300, 10 | "width": 310, 11 | "height": 80, 12 | "__comment": "stats_kind can be min, max, sum, count, avg", 13 | "stats_kind": "avg", 14 | "window_s": 10, 15 | "num_buckets": 10, 16 | "facts": [ 17 | { 18 | "name": "mavlink.radio_status.rssi", 19 | "tags": { 20 | "sysid": "3", 21 | "compid": "68" 22 | } 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "Video link throughput chart", 28 | "type": "BarChartWidget", 29 | "x": -320, 30 | "y": 200, 31 | "width": 310, 32 | "height": 80, 33 | "__comment": "stats_kind can be min, max, sum, count, avg", 34 | "stats_kind": "sum", 35 | "window_s": 10, 36 | "num_buckets": 10, 37 | "facts": [ 38 | { 39 | "name": "gstreamer.received_bytes" 40 | } 41 | ] 42 | }, 43 | { 44 | "type": "IconSelectorWidget", 45 | "name": "RSSI of antenna 1", 46 | "x": -366, 47 | "y": 0, 48 | "facts": [ 49 | { 50 | "name": "wfbcli.rx.ant_stats.rssi_avg", 51 | "tags": { 52 | "ant_id": "0", 53 | "id": "video rx" 54 | } 55 | } 56 | ], 57 | "ranges_and_icons": [ 58 | { 59 | "range": [-59, 1], 60 | "icon_path": "signal1.png" 61 | }, 62 | { 63 | "range": [-69, -60], 64 | "icon_path": "signal2.png" 65 | }, 66 | { 67 | "range": [-79, -70], 68 | "icon_path": "signal3.png" 69 | }, 70 | { 71 | "range": [-89, -80], 72 | "icon_path": "signal5.png" 73 | }, 74 | { 75 | "range": [-130, -90], 76 | "icon_path": "signal4.png" 77 | } 78 | ] 79 | }, 80 | { 81 | "type": "IconSelectorWidget", 82 | "name": "RSSI of antenna 2", 83 | "x": -462, 84 | "y": 0, 85 | "facts": [ 86 | { 87 | "name": "wfbcli.rx.ant_stats.rssi_avg", 88 | "tags": { 89 | "ant_id": "1", 90 | "id": "video rx" 91 | } 92 | } 93 | ], 94 | "ranges_and_icons": [ 95 | { 96 | "range": [-59, 1], 97 | "icon_path": "signal1.png" 98 | }, 99 | { 100 | "range": [-69, -60], 101 | "icon_path": "signal2.png" 102 | }, 103 | { 104 | "range": [-79, -70], 105 | "icon_path": "signal3.png" 106 | }, 107 | { 108 | "range": [-89, -80], 109 | "icon_path": "signal5.png" 110 | }, 111 | { 112 | "range": [-130, -90], 113 | "icon_path": "signal4.png" 114 | } 115 | ], 116 | "calculation": "wfbcli_rx_ant_stats_rssi_avg_ant_id_1_id_video_rx" 117 | }, 118 | { 119 | "type": "IconSelectorWidget", 120 | "name": "RSSI of antenna 3", 121 | "x": -558, 122 | "y": 0, 123 | "facts": [ 124 | { 125 | "name": "wfbcli.rx.ant_stats.rssi_avg", 126 | "tags": { 127 | "ant_id": "256", 128 | "id": "video rx" 129 | } 130 | } 131 | ], 132 | "ranges_and_icons": [ 133 | { 134 | "range": [-59, 1], 135 | "icon_path": "signal1.png" 136 | }, 137 | { 138 | "range": [-69, -60], 139 | "icon_path": "signal2.png" 140 | }, 141 | { 142 | "range": [-79, -70], 143 | "icon_path": "signal3.png" 144 | }, 145 | { 146 | "range": [-89, -80], 147 | "icon_path": "signal5.png" 148 | }, 149 | { 150 | "range": [-130, -90], 151 | "icon_path": "signal4.png" 152 | } 153 | ] 154 | }, 155 | { 156 | "type": "IconSelectorWidget", 157 | "name": "RSSI of antenna 4", 158 | "x": -654, 159 | "y": 0, 160 | "facts": [ 161 | { 162 | "name": "wfbcli.rx.ant_stats.rssi_avg", 163 | "tags": { 164 | "ant_id": "257", 165 | "id": "video rx" 166 | } 167 | } 168 | ], 169 | "ranges_and_icons": [ 170 | { 171 | "range": [-59, 1], 172 | "icon_path": "signal1.png" 173 | }, 174 | { 175 | "range": [-69, -60], 176 | "icon_path": "signal2.png" 177 | }, 178 | { 179 | "range": [-79, -70], 180 | "icon_path": "signal3.png" 181 | }, 182 | { 183 | "range": [-89, -80], 184 | "icon_path": "signal5.png" 185 | }, 186 | { 187 | "range": [-130, -90], 188 | "icon_path": "signal4.png" 189 | } 190 | ] 191 | }, 192 | { 193 | "type": "IconSelectorWidget", 194 | "name": "RSSI of antenna 5", 195 | "x": -750, 196 | "y": 0, 197 | "facts": [ 198 | { 199 | "name": "wfbcli.rx.ant_stats.rssi_avg", 200 | "tags": { 201 | "ant_id": "512", 202 | "id": "video rx" 203 | } 204 | } 205 | ], 206 | "ranges_and_icons": [ 207 | { 208 | "range": [-59, 1], 209 | "icon_path": "signal1.png" 210 | }, 211 | { 212 | "range": [-69, -60], 213 | "icon_path": "signal2.png" 214 | }, 215 | { 216 | "range": [-79, -70], 217 | "icon_path": "signal3.png" 218 | }, 219 | { 220 | "range": [-89, -80], 221 | "icon_path": "signal5.png" 222 | }, 223 | { 224 | "range": [-130, -90], 225 | "icon_path": "signal4.png" 226 | } 227 | ] 228 | }, 229 | { 230 | "type": "IconSelectorWidget", 231 | "name": "RSSI of antenna 6", 232 | "x": -846, 233 | "y": 0, 234 | "facts": [ 235 | { 236 | "name": "wfbcli.rx.ant_stats.rssi_avg", 237 | "tags": { 238 | "ant_id": "513", 239 | "id": "video rx" 240 | } 241 | } 242 | ], 243 | "ranges_and_icons": [ 244 | { 245 | "range": [-59, 1], 246 | "icon_path": "signal1.png" 247 | }, 248 | { 249 | "range": [-69, -60], 250 | "icon_path": "signal2.png" 251 | }, 252 | { 253 | "range": [-79, -70], 254 | "icon_path": "signal3.png" 255 | }, 256 | { 257 | "range": [-89, -80], 258 | "icon_path": "signal5.png" 259 | }, 260 | { 261 | "range": [-130, -90], 262 | "icon_path": "signal4.png" 263 | } 264 | ] 265 | }, 266 | { 267 | "name": "Metrics background", 268 | "type": "BoxWidget", 269 | "x": -270, 270 | "y": 0, 271 | "width": 270, 272 | "height": 150, 273 | "color": { 274 | "r": 0.0, 275 | "g": 0.0, 276 | "b": 0.0, 277 | "alpha": 0.4 278 | }, 279 | "facts": [] 280 | }, 281 | { 282 | "name": "Video FPS and resolution", 283 | "type": "VideoWidget", 284 | "x": -250, 285 | "y": 26, 286 | "icon_path": "framerate.png", 287 | "template": "%u fps | %ux%u", 288 | "per_second_window_s": 2, 289 | "per_second_bucket_ms": 200, 290 | "facts": [ 291 | { 292 | "__comment": "Will be converted to per-second", 293 | "name": "video.displayed_frame" 294 | }, 295 | { 296 | "name": "video.width" 297 | }, 298 | { 299 | "name": "video.height" 300 | } 301 | ] 302 | }, 303 | { 304 | "name": "Video link throughput", 305 | "type": "VideoBitrateWidget", 306 | "x": -250, 307 | "y": 56, 308 | "icon_path": "network.png", 309 | "template": "%f Mbps", 310 | "per_second_window_s": 2, 311 | "per_second_bucket_ms": 100, 312 | "facts": [ 313 | { 314 | "__comment": "Should be sum per-second, scaled to Megs", 315 | "name": "gstreamer.received_bytes" 316 | } 317 | ] 318 | }, 319 | { 320 | "name": "WFB radio", 321 | "type": "IconTplTextWidget", 322 | "x": -250, 323 | "y": 86, 324 | "icon_path": "wfb.png", 325 | "template": "RSSI %d ", 326 | "facts": [ 327 | { 328 | "name": "mavlink.radio_status.rssi", 329 | "tags": { 330 | "sysid": "3", 331 | "compid": "68" 332 | } 333 | } 334 | ] 335 | }, 336 | { 337 | "name": "WFB radio SNR", 338 | "type": "TplTextWidget", 339 | "x": -110, 340 | "y": 86, 341 | "template": "| SNR %d", 342 | "facts": [ 343 | { 344 | "name": "wfbcli.rx.ant_stats.snr_min", 345 | "tags": { 346 | "ant_id": "0", 347 | "id": "video rx" 348 | } 349 | } 350 | ] 351 | }, 352 | { 353 | "name": "WFB radio package lost", 354 | "type": "IconTplTextWidget", 355 | "x": -250, 356 | "y": 116, 357 | "icon_path": "wfb.png", 358 | "template": "LOST %u ", 359 | "facts": [ 360 | { 361 | "name": "mavlink.radio_status.rxerrors", 362 | "tags": { 363 | "sysid": "3", 364 | "compid": "68" 365 | } 366 | } 367 | ] 368 | }, 369 | { 370 | "name": "WFB radio package fec", 371 | "type": "TplTextWidget", 372 | "x": -110, 373 | "y": 116, 374 | "template": "| FEC %u", 375 | "facts": [ 376 | { 377 | "name": "mavlink.radio_status.fixed", 378 | "tags": { 379 | "sysid": "3", 380 | "compid": "68" 381 | } 382 | } 383 | ] 384 | }, 385 | { 386 | "name": "DVR status", 387 | "type": "DvrStatusWidget", 388 | "x": -250, 389 | "y": 146, 390 | "icon_path": "sdcard-white.png", 391 | "text": "Recording", 392 | "facts": [ 393 | { 394 | "name": "dvr.recording" 395 | } 396 | ] 397 | }, 398 | { 399 | "name": "Altitude", 400 | "type": "TplTextWidget", 401 | "x": 10, 402 | "y": 20, 403 | "template": "Alt:%fM", 404 | "facts": [ 405 | { 406 | "name": "mavlink.vfr_hud.alt" 407 | } 408 | ] 409 | }, 410 | { 411 | "name": "Speed", 412 | "type": "TplTextWidget", 413 | "x": 10, 414 | "y": 50, 415 | "template": "Spd:%fkm/h", 416 | "facts": [ 417 | { 418 | "name": "mavlink.vfr_hud.groundspeed" 419 | } 420 | ] 421 | }, 422 | { 423 | "name": "Vertical speed", 424 | "type": "TplTextWidget", 425 | "x": 10, 426 | "y": 80, 427 | "template": "VSpd:%fm/h", 428 | "facts": [ 429 | { 430 | "name": "mavlink.vfr_hud.climb" 431 | } 432 | ] 433 | }, 434 | 435 | 436 | 437 | { 438 | "name": "Satellite number", 439 | "type": "TplTextWidget", 440 | "x": 10, 441 | "y": 110, 442 | "template": "Sats:%u", 443 | "facts": [ 444 | { 445 | "name": "mavlink.gps_raw.satellites_visible" 446 | } 447 | ] 448 | }, 449 | { 450 | "name": "GPS coordinates", 451 | "type": "GPSWidget", 452 | "x": 10, 453 | "y": 140, 454 | "facts": [ 455 | { 456 | "name": "mavlink.gps_raw.fix_type" 457 | }, 458 | { 459 | "name": "mavlink.gps_raw.lat" 460 | }, 461 | { 462 | "name": "mavlink.gps_raw.lon" 463 | } 464 | ] 465 | }, 466 | 467 | { 468 | "name": "Pitch", 469 | "type": "TplTextWidget", 470 | "x": 10, 471 | "y": 170, 472 | "template": "Pitch:%f", 473 | "facts": [ 474 | { 475 | "name": "mavlink.attitude.pitch" 476 | } 477 | ] 478 | }, 479 | { 480 | "name": "Roll", 481 | "type": "TplTextWidget", 482 | "x": 10, 483 | "y": 200, 484 | "template": "Roll:%f", 485 | "facts": [ 486 | { 487 | "name": "mavlink.attitude.roll" 488 | } 489 | ] 490 | }, 491 | { 492 | "name": "Battery voltage", 493 | "type": "TplTextWidget", 494 | "x": 10, 495 | "y": 230, 496 | "template": "BAT:%umV", 497 | "facts": [ 498 | { 499 | "name": "mavlink.sys_status.voltage_battery" 500 | } 501 | ] 502 | }, 503 | { 504 | "name": "Battery consumed", 505 | "type": "TplTextWidget", 506 | "x": 10, 507 | "y": 260, 508 | "template": "CONS:%imAh", 509 | "facts": [ 510 | { 511 | "name": "mavlink.battery_status.current_consumed" 512 | } 513 | ] 514 | }, 515 | { 516 | "name": "Battery current", 517 | "type": "TplTextWidget", 518 | "x": 10, 519 | "y": 290, 520 | "template": "CUR:%i/100 A", 521 | "facts": [ 522 | { 523 | "name": "mavlink.sys_status.current_battery" 524 | } 525 | ] 526 | }, 527 | { 528 | "name": "Custom fading message", 529 | "type": "PopupWidget", 530 | "x": 400, 531 | "y": 50, 532 | "timeout_ms": 10000, 533 | "facts": [ 534 | { 535 | "name": "osd.custom_message" 536 | } 537 | ] 538 | }, 539 | { 540 | "name": "Dump raw facts to the scren (remove `--` from `type` to enable)", 541 | "type": "---DebugWidget", 542 | "x": 10, 543 | "y": -100, 544 | "facts": [ 545 | { 546 | "name": "mavlink.heartbeet.base_mode.armed" 547 | }, 548 | { 549 | "name": "mavlink.radio_status.rssi", 550 | "tags": { 551 | "sysid": "3", 552 | "compid": "68" 553 | } 554 | }, 555 | { 556 | "name": "mavlink.gps_raw.lat" 557 | }, 558 | { 559 | "name": "mavlink.gps_raw.lon" 560 | }, 561 | { 562 | "name": "mavlink.gps_raw.fix_type" 563 | }, 564 | { 565 | "name": "mavlink.global_position_int.lat" 566 | }, 567 | { 568 | "name": "mavlink.global_position_int.lon" 569 | } 570 | ] 571 | }, 572 | { 573 | "name": "Distance", 574 | "type": "DistanceWidget", 575 | "x": 10, 576 | "y": 230, 577 | "template": "Pitch:%f", 578 | "facts": [ 579 | { 580 | "__comment": "Assuming arm is on channel 5", 581 | "name": "mavlink.rc_channels_raw.chan5" 582 | }, 583 | { 584 | "__comment": "Arms state from heartbeat", 585 | "name": "mavlink.heartbeet.base_mode.armed" 586 | }, 587 | { 588 | "__comment": "Lat", 589 | "name": "mavlink.gps_raw.lat" 590 | }, 591 | { 592 | "__comment": "Lon", 593 | "name": "mavlink.gps_raw.lon" 594 | } 595 | ] 596 | } 597 | ] 598 | } 599 | -------------------------------------------------------------------------------- /gs/gsmenu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o pipefail 3 | source /etc/gs.conf 4 | # Configuration 5 | REMOTE_IP="10.5.0.10" 6 | SSH_PASS="12345" 7 | CACHE_DIR="/tmp/gsmenu_cache" 8 | CACHE_TTL=10 # seconds 9 | MAJESTIC_YAML="/etc/majestic.yaml" 10 | WFB_YAML="/etc/wfb.yaml" 11 | ALINK_CONF="/etc/alink.conf" 12 | TXPROFILES_CONF="/etc/txprofiles.conf" 13 | PRESET_DIR="/etc/presets" 14 | 15 | # SSH command setup 16 | SSH="timeout -k 1 11 sshpass -p $SSH_PASS ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -o ControlMaster=auto -o ControlPath=/run/ssh_control:%h:%p:%r -o ControlPersist=15s -o ServerAliveInterval=3 -o ServerAliveCountMax=2 root@$REMOTE_IP" 17 | SCP="timeout -k 1 11 sshpass -p $SSH_PASS scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -o ControlMaster=auto -o ControlPath=/run/ssh_control:%h:%p:%r -o ControlPersist=15s -o ServerAliveInterval=3 -o ServerAliveCountMax=2" 18 | 19 | # Create cache directory if it doesn't exist 20 | mkdir -p "$CACHE_DIR" 21 | 22 | # Function to refresh cached files 23 | refresh_cache() { 24 | local current_time=$(date +%s) 25 | local last_refresh=$((current_time - CACHE_TTL)) 26 | 27 | # Check if we need to refresh 28 | if [[ ! -f "$CACHE_DIR/last_refresh" ]] || [[ $(cat "$CACHE_DIR/last_refresh") -lt $last_refresh ]]; then 29 | # Copy the YAML configuration files 30 | $SCP root@$REMOTE_IP:$MAJESTIC_YAML root@$REMOTE_IP:$WFB_YAML root@$REMOTE_IP:$ALINK_CONF root@$REMOTE_IP:$TXPROFILES_CONF $CACHE_DIR 2>/dev/null 31 | 32 | # Update refresh timestamp 33 | echo "$current_time" > "$CACHE_DIR/last_refresh" 34 | fi 35 | } 36 | 37 | # Function to get value from majestic.yaml using yaml-cli 38 | get_majestic_value() { 39 | local key="$1" 40 | yaml-cli -i "$CACHE_DIR/majestic.yaml" -g "$key" 2>/dev/null 41 | } 42 | 43 | # Function to get value from wfb.yaml using yaml-cli 44 | get_wfb_value() { 45 | local key="$1" 46 | yaml-cli -i "$CACHE_DIR/wfb.yaml" -g "$key" 2>/dev/null 47 | } 48 | 49 | # Function to get value from alink.conf 50 | get_alink_value() { 51 | local key="$1" 52 | grep $key= "$CACHE_DIR/alink.conf" | cut -d "=" -f 2 2>/dev/null 53 | } 54 | 55 | # Refresh cache for get 56 | case "$@" in 57 | "get air"*) 58 | [ "$3" != "presets" ] && refresh_cache 59 | ;; 60 | esac 61 | 62 | case "$@" in 63 | "values air presets preset") 64 | if [ -d $PRESET_DIR ]; then 65 | for dir in $PRESET_DIR/presets/*; do 66 | echo $(basename $dir) 67 | done 68 | fi 69 | ;; 70 | "values air wfbng mcs_index") 71 | echo -n 0 10 72 | ;; 73 | "values air wfbng fec_k") 74 | echo -n 0 15 75 | ;; 76 | "values air wfbng fec_n") 77 | echo -n 0 15 78 | ;; 79 | "values air wfbng mlink") 80 | echo -n -e "1500\n1600\n1700\n1800\n1900\n2000\n2100\n2200\n2300\n2400\n2500\n2600\n2700\n2800\n2900\n3000\n3100\n3200\n3300\n3400\n3500\n3600\n3700\n3800\n3900\n4000" 81 | ;; 82 | "values air camera contrast") 83 | echo -n 0 100 84 | ;; 85 | "values air camera hue") 86 | echo -n 0 100 87 | ;; 88 | "values air camera saturation") 89 | echo -n 0 100 90 | ;; 91 | "values air camera luminace") 92 | echo -n 0 100 93 | ;; 94 | "values air camera gopsize") 95 | echo -n 0 10 96 | ;; 97 | "values air camera rec_split") 98 | echo -n 0 60 99 | ;; 100 | "values air camera rec_maxusage") 101 | echo -n 0 100 102 | ;; 103 | "values air camera exposure") 104 | echo -n 5 50 105 | ;; 106 | "values air camera noiselevel") 107 | echo -n 0 1 108 | ;; 109 | "values air telemetry osd_fps") 110 | echo -n 0 60 111 | ;; 112 | "values air wfbng power") 113 | echo -n -e "1\n20\n25\n30\n35\n40\n45\n50\n55\n58" 114 | ;; 115 | "values air wfbng air_channel") 116 | iw list | grep MHz | grep -v disabled | grep \* | tr -d '[]' | awk '{print $4 " (" $2 " " $3 ")"}' | grep '^[1-9]' | sort -n | uniq 117 | ;; 118 | "values air wfbng width") 119 | echo -n -e "20\n40" 120 | ;; 121 | "values air alink power_level_0_to_4") 122 | echo -n -e "0\n1\n2\n3\n4" 123 | ;; 124 | "values air alink fallback_ms") 125 | echo -n 1 2000 126 | ;; 127 | "values air alink hold_fallback_mode_s") 128 | echo -n 1 10 129 | ;; 130 | "values air alink min_between_changes_ms") 131 | echo -n 1 10000 132 | ;; 133 | "values air alink hold_modes_down_s") 134 | echo -n 1 10 135 | ;; 136 | "values air alink hysteresis_percent") 137 | echo -n 0 100 138 | ;; 139 | "values air alink hysteresis_percent_down") 140 | echo -n 0 100 141 | ;; 142 | "values air alink exp_smoothing_factor") 143 | echo -n 0 1.6 144 | ;; 145 | "values air alink exp_smoothing_factor_down") 146 | echo -n 0 1.6 147 | ;; 148 | "values air alink check_xtx_period_ms") 149 | echo -n 1 5000 150 | ;; 151 | "values air alink request_keyframe_interval_ms") 152 | echo -n 1 5000 153 | ;; 154 | "values air alink osd_level") 155 | echo -n -e "0\n1\n2\n3\n4\n5\n6" 156 | ;; 157 | "values air alink multiply_font_size_by") 158 | echo -n 0 1.5 159 | ;; 160 | "values air camera size") 161 | echo -n -e "1280x720\n1456x816\n1920x1080\n1440x1080\n1920x1440\n2104x1184\n2208x1248\n2240x1264\n2312x1304\n2436x1828\n2512x1416\n2560x1440\n2560x1920\n2720x1528\n2944x1656\n3200x1800\n3840x2160" 162 | ;; 163 | "values air camera fps") 164 | echo -n -e "60\n90\n120" 165 | ;; 166 | "values air camera bitrate") 167 | echo -n -e "1024\n2048\n3072\n4096\n5120\n6144\n7168\n8192\n9216\n10240\n11264\n12288\n13312\n14336\n15360\n16384\n17408\n18432\n19456\n20480\n21504\n22528\n23552\n24576\n25600\n26624\n27648\n28672\n29692\n30720" 168 | ;; 169 | "values air camera codec") 170 | echo -n -e "h264\nh265" 171 | ;; 172 | "values air camera rc_mode") 173 | echo -n -e "vbr\navbr\ncbr" 174 | ;; 175 | "values air camera antiflicker") 176 | echo -n -e "disabled\n50\n60" 177 | ;; 178 | "values air camera sensor_file") 179 | echo -n -e "imx307\nimx335\nimx335_fpv\nimx415_fpv\nimx415_fpv\nimx415_milos10\nimx415_milos15\nimx335_milos12tweak\nimx335_greg15\nimx335_spike5\ngregspike05" 180 | ;; 181 | "values air telemetry serial") 182 | echo -n -e "ttyS0\nttyS1\nttyS2\nttyS3" 183 | ;; 184 | "values air telemetry router") 185 | echo -n -e "mavfwd\nmsposd" 186 | ;; 187 | 188 | "get air presets info"*) 189 | if [ "$5" == "" ] ; then 190 | echo "" 191 | else 192 | echo "Name: $(yaml-cli -i $PRESET_DIR/presets/$5/preset-config.yaml -g .name)" 193 | echo "Author: $(yaml-cli -i $PRESET_DIR/presets/$5/preset-config.yaml -g .author)" 194 | echo "Description: $(yaml-cli -i $PRESET_DIR/presets/$5/preset-config.yaml -g .description)" 195 | echo "Category: $(yaml-cli -i $PRESET_DIR/presets/$5/preset-config.yaml -g .category)" 196 | echo "Sensor: $(yaml-cli -i $PRESET_DIR/presets/$5/preset-config.yaml -g .sensor)" 197 | echo "Status: $(yaml-cli -i $PRESET_DIR/presets/$5/preset-config.yaml -g .status)" 198 | echo "Tags: $(yaml-cli -i $PRESET_DIR/presets/$5/preset-config.yaml -g .tags)" 199 | fi 200 | ;; 201 | "get air presets update") 202 | mkdir -p $PRESET_DIR 203 | if [ -d "$PRESET_DIR/.git" ]; then 204 | # If it's already a git repo, force reset and pull 205 | cd $PRESET_DIR 206 | git fetch --all 207 | git reset --hard origin/master 208 | git pull origin master --force 209 | else 210 | # If not a git repo yet, clone fresh 211 | git clone https://github.com/OpenIPC/fpv-presets.git $PRESET_DIR 212 | fi 213 | ;; 214 | "get air camera mirror") 215 | [ "$(get_majestic_value '.image.mirror')" = "true" ] && echo 1 || echo 0 216 | ;; 217 | "get air camera flip") 218 | [ "$(get_majestic_value '.image.flip')" = "true" ] && echo 1 || echo 0 219 | ;; 220 | "get air camera contrast") 221 | get_majestic_value '.image.contrast' 222 | ;; 223 | "get air camera hue") 224 | get_majestic_value '.image.hue' 225 | ;; 226 | "get air camera saturation") 227 | get_majestic_value '.image.saturation' 228 | ;; 229 | "get air camera luminace") 230 | get_majestic_value '.image.luminance' 231 | ;; 232 | "get air camera size") 233 | get_majestic_value '.video0.size' 234 | ;; 235 | "get air camera fps") 236 | get_majestic_value '.video0.fps' 237 | ;; 238 | "get air camera bitrate") 239 | get_majestic_value '.video0.bitrate' 240 | ;; 241 | "get air camera codec") 242 | get_majestic_value '.video0.codec' 243 | ;; 244 | "get air camera gopsize") 245 | get_majestic_value '.video0.gopSize' 246 | ;; 247 | "get air camera rc_mode") 248 | get_majestic_value '.video0.rcMode' 249 | ;; 250 | "get air camera rec_enable") 251 | [ "$(get_majestic_value '.records.enabled')" = "true" ] && echo 1 || echo 0 252 | ;; 253 | "get air camera rec_split") 254 | get_majestic_value '.records.split' 255 | ;; 256 | "get air camera rec_maxusage") 257 | get_majestic_value '.records.maxUsage' 258 | ;; 259 | "get air camera exposure") 260 | get_majestic_value '.isp.exposure' 261 | ;; 262 | "get air camera antiflicker") 263 | get_majestic_value '.isp.antiFlicker' 264 | ;; 265 | "get air camera sensor_file") 266 | basename -s .bin $(basename $(get_majestic_value '.isp.sensorConfig')) 267 | ;; 268 | "get air camera fpv_enable") 269 | get_majestic_value '.fpv.enabled' | grep -q true && echo 1 || echo 0 270 | ;; 271 | "get air camera noiselevel") 272 | get_majestic_value '.fpv.noiseLevel' 273 | ;; 274 | 275 | "set air presets "*) 276 | PRESET="$PRESET_DIR/presets/$4/preset-config.yaml" 277 | 278 | # Create a temporary script file 279 | REMOTE_SCRIPT=$(mktemp) 280 | echo "#!/bin/sh" > "$REMOTE_SCRIPT" 281 | echo "# Auto-generated configuration script" >> "$REMOTE_SCRIPT" 282 | echo "echo 'Applying configuration...'" >> "$REMOTE_SCRIPT" 283 | 284 | # Process config files 285 | FILES=$(yq e '.files | keys | .[]' "$PRESET") 286 | 287 | for FILE in $FILES; do 288 | # Generate CLI commands for each key-value pair 289 | while IFS= read -r LINE; do 290 | KEY="${LINE%% *}" # Get everything before first space 291 | VALUE="${LINE#* }" # Get everything after first space 292 | 293 | # Escape single quotes in values for bash 294 | VALUE=${VALUE//\'/\'\\\'\'} 295 | 296 | echo "echo \"Setting $KEY to $VALUE in $FILE\"" >> "$REMOTE_SCRIPT" 297 | echo "cli -i '/etc/$FILE' -s '$KEY' '$VALUE'" >> "$REMOTE_SCRIPT" 298 | done < <(yq e ".files[\"$FILE\"] | to_entries | .[] | \".\" + .key + \" \" + .value" "$PRESET") 299 | done 300 | 301 | # Add additional file copies 302 | yq e '.additional_files // [] | .[]' "$PRESET" | while read -r ADDITIONAL_FILE; do 303 | LOCAL_FILE="$PRESET_DIR/presets/$4/$ADDITIONAL_FILE" 304 | if [ -f "$LOCAL_FILE" ]; then 305 | # Transfer the file first 306 | $SCP "$LOCAL_FILE" "root@$REMOTE_IP:/etc/" 307 | echo "echo 'Copied additional file: $ADDITIONAL_FILE'" 308 | else 309 | echo "echo 'Warning: Additional file not found: $ADDITIONAL_FILE'" 310 | fi 311 | done 312 | 313 | # Add service restart commands 314 | echo "echo 'Restarting services...'" >> "$REMOTE_SCRIPT" 315 | echo "(wifibroadcast stop; wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" >> "$REMOTE_SCRIPT" 316 | echo "killall -1 majestic" >> "$REMOTE_SCRIPT" 317 | echo "echo 'Configuration applied successfully'" >> "$REMOTE_SCRIPT" 318 | 319 | # Transfer and execute the script 320 | $SCP "$REMOTE_SCRIPT" "root@$REMOTE_IP:/tmp/apply_config.sh" 321 | $SSH "sh /tmp/apply_config.sh" 322 | 323 | # Cleanup 324 | rm "$REMOTE_SCRIPT" 325 | ;; 326 | "set air camera mirror"*) 327 | if [ "$5" = "on" ] 328 | then 329 | $SSH 'cli -s .image.mirror true && killall -1 majestic' 330 | else 331 | $SSH 'cli -s .image.mirror false && killall -1 majestic' 332 | fi 333 | ;; 334 | "set air camera flip"*) 335 | if [ "$5" = "on" ] 336 | then 337 | $SSH 'cli -s .image.flip true && killall -1 majestic' 338 | else 339 | $SSH 'cli -s .image.flip false && killall -1 majestic' 340 | fi 341 | ;; 342 | "set air camera contrast"*) 343 | $SSH "cli -s .image.contrast $5 && killall -1 majestic" 344 | ;; 345 | "set air camera hue"*) 346 | $SSH "cli -s .image.hue $5 && killall -1 majestic" 347 | ;; 348 | "set air camera saturation"*) 349 | $SSH "cli -s .image.saturation $5 && killall -1 majestic" 350 | ;; 351 | "set air camera luminace"*) 352 | $SSH "cli -s .image.luminance $5 && killall -1 majestic" 353 | ;; 354 | "set air camera size"*) 355 | $SSH "cli -s .video0.size $5 && killall -1 majestic" 356 | ;; 357 | "set air camera fps"*) 358 | $SSH "cli -s .video0.fps $5 && killall -1 majestic" 359 | ;; 360 | "set air camera bitrate"*) 361 | $SSH "cli -s .video0.bitrate $5 && killall -1 majestic" 362 | ;; 363 | "set air camera codec"*) 364 | $SSH "cli -s .video0.codec $5 && killall -1 majestic" 365 | ;; 366 | "set air camera gopsize"*) 367 | $SSH "cli -s .video0.gopSize $5 && killall -1 majestic" 368 | ;; 369 | "set air camera rc_mode"*) 370 | $SSH "cli -s .video0.rcMode $5 && killall -1 majestic" 371 | ;; 372 | "set air camera rec_enable"*) 373 | if [ "$5" = "on" ] 374 | then 375 | $SSH 'cli -s .records.enable true && killall -1 majestic' 376 | else 377 | $SSH 'cli -s .records.enable false && killall -1 majestic' 378 | fi 379 | ;; 380 | "set air camera rec_split"*) 381 | $SSH "cli -s .records.split $5 && killall -1 majestic" 382 | ;; 383 | "set air camera rec_maxusage"*) 384 | $SSH "cli -s .records.maxUsage $5 && killall -1 majestic" 385 | ;; 386 | "set air camera exposure"*) 387 | $SSH "cli -s .isp.exposure $5 && killall -1 majestic" 388 | ;; 389 | "set air camera antiflicker"*) 390 | $SSH "cli -s .isp.antiFlicker $5 && killall -1 majestic" 391 | ;; 392 | "set air camera sensor_file"*) 393 | $SSH "cli -s .isp.sensorConfig /etc/sensors/${5}.bin && killall -1 majestic" 394 | ;; 395 | "set air camera fpv_enable"*) 396 | if [ "$5" = "on" ] 397 | then 398 | $SSH 'cli -s .fpv.enabled true && killall -1 majestic' 399 | else 400 | $SSH 'cli -s .fpv.enabled false && killall -1 majestic' 401 | fi 402 | ;; 403 | "set air camera noiselevel"*) 404 | $SSH "cli -s .fpv.noiseLevel $5 && killall -1 majestic" 405 | ;; 406 | 407 | "get air telemetry serial") 408 | $SSH wifibroadcast cli -g .telemetry.serial 409 | ;; 410 | "get air telemetry router") 411 | $SSH wifibroadcast cli -g .telemetry.router 412 | ;; 413 | "get air telemetry osd_fps") 414 | $SSH wifibroadcast cli -g .telemetry.osd_fps 415 | ;; 416 | "get air telemetry gs_rendering") 417 | $SSH 'grep "\-z \"\$size\"" /usr/bin/wifibroadcast' | grep -q size && echo 0 || echo 1 418 | ;; 419 | 420 | "set air telemetry serial"*) 421 | if [ "$5" = "ttyS0" ] 422 | then 423 | $SSH "sed -i 's/^console::respawn:\/sbin\/getty -L console 0 vt100/#console::respawn:\/sbin\/getty -L console 0 vt100/' /etc/inittab ; kill -HUP 1" 424 | else 425 | $SSH "sed -i 's/^#console::respawn:\/sbin\/getty -L console 0 vt100/console::respawn:\/sbin\/getty -L console 0 vt100/' /etc/inittab ; kill -HUP 1" 426 | fi 427 | $SSH wifibroadcast cli -s .telemetry.serial $5 428 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 429 | ;; 430 | "set air telemetry router"*) 431 | $SSH wifibroadcast cli -s .telemetry.router $5 432 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 433 | ;; 434 | "set air telemetry osd_fps"*) 435 | $SSH wifibroadcast cli -s .telemetry.osd_fps $5 436 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 437 | ;; 438 | "set air telemetry gs_rendering"*) 439 | if [ "$5" = "on" ] 440 | then 441 | $SSH 'sed -i "s/-o 127\.0\.0\.1:\"\$port_tx\" -z \"\$size\"/-o 10\.5\.0\.1:\"\$port_tx\"/" /usr/bin/wifibroadcast' 442 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 443 | else 444 | $SSH 'sed -i "s/-o 10\.5\.0\.1:\"\$port_tx\"/-o 127\.0\.0\.1:\"\$port_tx\" -z \"\$size\"/" /usr/bin/wifibroadcast' 445 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 446 | fi 447 | ;; 448 | 449 | "get air wfbng power") 450 | get_wfb_value '.wireless.txpower' 451 | ;; 452 | "get air wfbng air_channel") 453 | channel=$(get_wfb_value '.wireless.channel' | tr -d '\n') 454 | iw list | grep "\[$channel\]" | tr -d '[]' | awk '{print $4 " (" $2 " " $3 ")"}' | sort -n | uniq | tr -d '\n' 455 | ;; 456 | "get air wfbng width") 457 | get_wfb_value '.wireless.width' 458 | ;; 459 | "get air wfbng mcs_index") 460 | get_wfb_value '.broadcast.mcs_index' 461 | ;; 462 | "get air wfbng stbc") 463 | get_wfb_value '.broadcast.stbc' 464 | ;; 465 | "get air wfbng ldpc") 466 | get_wfb_value '.broadcast.ldpc' 467 | ;; 468 | "get air wfbng fec_k") 469 | get_wfb_value '.broadcast.fec_k' 470 | ;; 471 | "get air wfbng fec_n") 472 | get_wfb_value '.broadcast.fec_n' 473 | ;; 474 | "get air wfbng mlink") 475 | get_wfb_value '.wireless.mlink' 476 | ;; 477 | "get air wfbng adaptivelink") 478 | $SSH grep ^alink_drone /etc/rc.local | grep -q 'alink_drone' && echo 1 || echo 0 479 | ;; 480 | 481 | "set air wfbng power"*) 482 | $SSH wifibroadcast cli -s .wireless.txpower $5 483 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 484 | ;; 485 | "set air wfbng air_channel"*) 486 | channel=$(echo $5 | awk '{print $1}') 487 | $SSH wifibroadcast cli -s .wireless.channel $channel 488 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 489 | sed -i "s/^wifi_channel =.*/wifi_channel = $channel/" /etc/wifibroadcast.cfg 490 | systemctl restart wifibroadcast.service 491 | ;; 492 | "set air wfbng width"*) 493 | $SSH wifibroadcast cli -s .wireless.width $5 494 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 495 | ;; 496 | "set air wfbng mcs_index"*) 497 | $SSH wifibroadcast cli -s .broadcast.mcs_index $5 498 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 499 | ;; 500 | "set air wfbng stbc"*) 501 | if [ "$5" = "on" ] 502 | then 503 | $SSH wifibroadcast cli -s .broadcast.stbc 1 504 | else 505 | $SSH wifibroadcast cli -s .broadcast.stbc 0 506 | fi 507 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 508 | ;; 509 | "set air wfbng ldpc"*) 510 | if [ "$5" = "on" ] 511 | then 512 | $SSH wifibroadcast cli -s .broadcast.ldpc 1 513 | else 514 | $SSH wifibroadcast cli -s .broadcast.ldpc 0 515 | 516 | fi 517 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 518 | ;; 519 | "set air wfbng fec_k"*) 520 | $SSH wifibroadcast cli -s .broadcast.fec_k $5 521 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 522 | ;; 523 | "set air wfbng fec_n"*) 524 | $SSH wifibroadcast cli -s .broadcast.fec_n $5 525 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 526 | ;; 527 | "set air wfbng mlink"*) 528 | $SSH wifibroadcast cli -s .wireless.mlink $5 529 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 530 | ;; 531 | "set air wfbng adaptivelink"*) 532 | if [ "$5" = "on" ] 533 | then 534 | $SSH 'sed -i "/alink_drone &/d" /etc/rc.local && sed -i -e "\$i alink_drone &" /etc/rc.local && cli -s .video0.qpDelta -12 && killall -1 majestic && (nohup alink_drone >/dev/null 2>&1 &)' 535 | else 536 | $SSH 'killall -q -9 alink_drone; sed -i "/alink_drone &/d" /etc/rc.local ; cli -d .video0.qpDelta && killall -1 majestic' 537 | fi 538 | ;; 539 | 540 | "get air alink"*) 541 | get_alink_value $4 542 | ;; 543 | 544 | "set air alink"*) 545 | if [ "$5" = "off" ] 546 | then 547 | $SSH 'sed -i "s/'$4'=.*/'$4'=0/" /etc/alink.conf; killall -9 alink_drone ; alink_drone &' 548 | elif [ "$5" = "on" ] 549 | then 550 | $SSH 'sed -i "s/'$4'=.*/'$4'=1/" /etc/alink.conf; killall -9 alink_drone ; alink_drone &' 551 | elif [ "$4" = "txprofiles" ] 552 | then 553 | $SCP $CACHE_DIR/txprofiles.conf root@$REMOTE_IP:$TXPROFILES_CONF 554 | $SSH 'killall -9 alink_drone ; alink_drone &' 555 | else 556 | $SSH 'sed -i "s/'$4'=.*/'$4'='$5'/" /etc/alink.conf; killall -9 alink_drone ; alink_drone &' 557 | fi 558 | ;; 559 | 560 | "values gs wfbng gs_channel") 561 | iw list | grep MHz | grep -v disabled | grep \* | tr -d '[]' | awk '{print $4 " (" $2 " " $3 ")"}' | grep '^[1-9]' | sort -n | uniq | sed -z '$ s/\n$//' 562 | ;; 563 | "values gs wfbng bandwidth") 564 | echo -n -e "20\n40" 565 | ;; 566 | "values gs wfbng txpower") 567 | echo -n -e "1\n100" 568 | ;; 569 | "values gs system resolution") 570 | drm_info -j /dev/dri/card0 2>/dev/null | jq -r '."/dev/dri/card0".connectors[1].modes[] | select(.name | contains("i") | not) | .name + "@" + (.vrefresh|tostring)' | sort | uniq | sed -z '$ s/\n$//' 571 | ;; 572 | "values gs system rec_fps") 573 | echo -n -e "60\n90\n120" 574 | ;; 575 | 576 | "get gs system gs_rendering") 577 | [ "$osd_type" = "msposd_gs" ] && echo 1 || echo 0 578 | ;; 579 | "get gs system resolution") 580 | drm_info -j /dev/dri/card0 2>/dev/null | jq -r '."/dev/dri/card0".crtcs[0].mode| .name + "@" + (.vrefresh|tostring)' 581 | ;; 582 | "get gs system rec_fps") 583 | echo -n $rec_fps 584 | ;; 585 | "set gs system gs_rendering"*) 586 | if [ "$5" = "off" ] 587 | then 588 | sed -i "s/^osd_type=.*/osd_type='msposd_air'/" "$(readlink -f /etc/gs.conf)" 589 | killall -q msposd 590 | else 591 | sed -i "s/^osd_type=.*/osd_type='msposd_gs'/" "$(readlink -f /etc/gs.conf)" 592 | if [ -e /dev/shm/msposd ]; then 593 | if [ "$msposd_gs_record" == "yes" ]; then 594 | msposd --master 0.0.0.0:$msposd_gs_port --osd -r $msposd_gs_fps --ahi $msposd_gs_ahi --subtitle $rec_dir & 595 | else 596 | msposd --master 0.0.0.0:$msposd_gs_port --osd -r $msposd_gs_fps --ahi $msposd_gs_ahi & 597 | fi 598 | else 599 | systemctl restart stream 600 | fi 601 | fi 602 | ;; 603 | "set gs system resolution"*) 604 | sed -i "s/^screen_mode=.*/screen_mode='$5'/" "$(readlink -f /etc/gs.conf)" 605 | ;; 606 | "set gs system rec_fps"*) 607 | sed -i "s/^rec_fps=.*/rec_fps='$5'/" "$(readlink -f /etc/gs.conf)" 608 | ;; 609 | "set gs system rec_enabled"*) 610 | if [ "$5" = "off" ] 611 | then 612 | : #noop 613 | else 614 | : #noop 615 | fi 616 | ;; 617 | "get gs wifi hotspot") 618 | nmcli connection show --active | grep -q "hotspot" && echo 1 || echo 0 619 | ;; 620 | "get gs wifi wlan") 621 | connection=$(nmcli -t connection show --active | grep wifi0 | cut -d : -f1) 622 | [ -z "${connection}" ] && echo 0 || echo 1 623 | ;; 624 | "get gs wifi ssid") 625 | if [ -d /sys/class/net/wifi0 ]; then 626 | nmcli -t connection show --active | grep wifi0 | cut -d : -f1 627 | else 628 | echo -n "" 629 | fi 630 | ;; 631 | "get gs wifi password") 632 | if [ -d /sys/class/net/wifi0 ]; then 633 | connection=$(nmcli -t connection show --active | grep wifi0 | cut -d : -f1) 634 | nmcli -t connection show $connection --show-secrets | grep 802-11-wireless-security.psk: | cut -d : -f2 635 | else 636 | echo -n "" 637 | fi 638 | ;; 639 | "get gs wifi IP") 640 | WIFI_DEV=$(nmcli -t connection show --active | grep wifi0 | cut -d : -f4) 641 | ip -4 addr show "$WIFI_DEV" | grep -oP '(?<=inet\s)\d+(\.\d+){3}' 642 | ;; 643 | "set gs wifi wlan"*) 644 | [ ! -d /sys/class/net/wifi0 ] && exit 0 # we have no wifi 645 | if [ "$5" = "on" ] 646 | then 647 | # Check if connection already exists 648 | if nmcli connection show | grep -q "$6"; then 649 | echo "$6 connection exists. Starting it..." 650 | nmcli con up "$6" 651 | else 652 | echo "Creating new "$6" connection..." 653 | nmcli device wifi connect "$6" password "$7" ifname wifi 654 | echo "Starting Wlan..." 655 | nmcli con up "$6" 656 | fi 657 | else 658 | nmcli con down "$6" 659 | fi 660 | ;; 661 | "set gs wifi hotspot"*) 662 | [ ! -d /sys/class/net/wifi0 ] && exit 0 # we have no wifi 663 | if [ "$5" = "on" ] 664 | then 665 | # Check if connection already exists 666 | if nmcli connection show | grep -q "hotspot"; then 667 | echo "Hotspot connection exists. Starting it..." 668 | nmcli con up hotspot 669 | # else 670 | # echo "Creating new Hotspot connection..." 671 | # nmcli con add type wifi ifname wifi0 con-name Hotspot autoconnect no ssid "OpenIPC GS" 672 | # nmcli con modify Hotspot 802-11-wireless.mode ap 802-11-wireless.band bg ipv4.method shared 673 | # nmcli con modify Hotspot wifi-sec.key-mgmt wpa-psk 674 | # nmcli con modify Hotspot wifi-sec.psk "openipcgs" 675 | # nmcli con modify Hotspot ipv4.addresses 192.168.4.1/24 676 | # echo "Starting Hotspot..." 677 | # nmcli con up Hotspot 678 | fi 679 | else 680 | nmcli con down hotspot 681 | fi 682 | ;; 683 | 684 | "get gs wfbng adaptivelink") 685 | [ "$alink_enable" == "yes" ] && echo 1 || echo 0 686 | ;; 687 | "get gs wfbng gs_channel") 688 | channel=$wfb_channel 689 | iw list | grep "\[$channel\]" | tr -d '[]' | awk '{print $4 " (" $2 " " $3 ")"}' | sort -n | uniq 690 | ;; 691 | "get gs wfbng bandwidth") 692 | echo -n $wfb_bandwidth 693 | ;; 694 | "get gs wfbng txpower") 695 | wifi_txpower=$(grep ^wifi_txpower /etc/wifibroadcast.cfg) 696 | [ -z "$wifi_txpower" ] && echo "50" && exit 0 697 | read first_card first_card_power < <( 698 | echo "$wifi_txpower" | cut -d = -f 2 | jq -r '"\(to_entries[0].key) \(to_entries[0].value)"' 699 | ) 700 | first_card_type=$(udevadm info /sys/class/net/${first_card}/ | grep -E 'ID_NET_DRIVER=(rtl88xxau_wfb|rtl88x2eu)'| cut -d = -f2) 701 | case "$first_card_type" in 702 | "rtl88xxau_wfb") 703 | min_phy_txpower=-1000 704 | max_phy_txpower=-3000 705 | ;; 706 | 707 | "rtl88x2eu") 708 | min_phy_txpower=1000 709 | max_phy_txpower=2900 710 | ;; 711 | esac 712 | range=$((max_phy_txpower - min_phy_txpower)) 713 | position=$((first_card_power - min_phy_txpower)) 714 | percentage=$(( (position * 100) / range )) 715 | echo $percentage 716 | ;; 717 | 718 | "set gs wfbng adaptivelink"*) 719 | if [ "$5" = "on" ] 720 | then 721 | sed -i "s/^alink_enable=.*/alink_enable='yes'/" "$(readlink -f /etc/gs.conf)" 722 | systemd-run --unit=alink /usr/local/bin/alink --config /etc/alink.conf 723 | else 724 | sed -i "s/^alink_enable=.*/alink_enable='no'/" "$(readlink -f /etc/gs.conf)" 725 | systemctl stop alink.service 726 | fi 727 | ;; 728 | "set gs wfbng gs_channel"*) 729 | channel=$(echo $5 | awk '{print $1}') 730 | if [ "$GSMENU_VTX_DETECTED" -eq "1" ]; then 731 | $SSH wifibroadcast cli -s .wireless.channel $channel 732 | $SSH "(wifibroadcast stop ;wifibroadcast stop; sleep 1; wifibroadcast start) >/dev/null 2>&1 &" 733 | fi 734 | sed -i "s/^wfb_channel=.*/wfb_channel='$channel'/" "$(readlink -f /etc/gs.conf)" 735 | /gs/wfb.sh 736 | ;; 737 | "set gs wfbng bandwidth"*) 738 | sed -i "s/^wfb_bandwidth=.*/wfb_bandwidth='$5'/" "$(readlink -f /etc/gs.conf)" 739 | /gs/wfb.sh 740 | ;; 741 | "set gs wfbng txpower"*) 742 | source /etc/default/wifibroadcast 743 | wifi_txpower="" 744 | for nic in $WFB_NICS 745 | do 746 | card_type=$(udevadm info /sys/class/net/${nic}/ | grep -E 'ID_NET_DRIVER=(rtl88xxau_wfb|rtl88x2eu)'| cut -d = -f2) 747 | case "$card_type" in 748 | "rtl88xxau_wfb") 749 | min_phy_txpower=-1000 750 | max_phy_txpower=-3000 751 | ;; 752 | 753 | "rtl88x2eu") 754 | min_phy_txpower=1000 755 | max_phy_txpower=2900 756 | ;; 757 | esac 758 | range=$((max_phy_txpower - min_phy_txpower)) 759 | percentage=$5 760 | power_value=$(( min_phy_txpower + (percentage * range) / 100 )) 761 | [ ! -z "$wifi_txpower" ] && wifi_txpower=$wifi_txpower, 762 | wifi_txpower=$wifi_txpower" \"$nic\": $power_value" 763 | done 764 | if ! grep -A 20 "\[common\]" /etc/wifibroadcast.cfg | grep -q "^wifi_txpower = "; then 765 | sed -i "/^\[common\]/a\wifi_txpower = {$wifi_txpower}" /etc/wifibroadcast.cfg 766 | else 767 | sed -i "s/^wifi_txpower = .*/wifi_txpower = {$wifi_txpower}/" /etc/wifibroadcast.cfg 768 | fi 769 | systemctl restart wifibroadcast.service 770 | ;; 771 | "get gs main Channel") 772 | gsmenu.sh get gs wfbng gs_channel 773 | ;; 774 | "get gs main HDMI-OUT") 775 | gsmenu.sh get gs system resolution 776 | ;; 777 | "get gs main Version") 778 | source /etc/gs-release 779 | echo -n "$VERSION" 780 | ;; 781 | "get gs main Disk") 782 | read -r size avail pcent <<< $(df -h / | awk 'NR==2 {print $2, $4, $5}') 783 | echo -e "\n Size: $size\n Available: $avail\n Pct: $pcent\c" 784 | ;; 785 | "get gs main WFB_NICS") 786 | grep ^WFB_NICS /etc/default/wifibroadcast | cut -d \" -f 2 787 | ;; 788 | "search channel") 789 | /gs/channel-scan.sh >/dev/null 2>&1 & 790 | ;; 791 | 792 | "button air actions Reboot") 793 | $SSH 'reboot &' 794 | ;; 795 | 796 | "button gs actions Reboot") 797 | reboot 798 | ;; 799 | 800 | *) 801 | echo "Unknown $@" 802 | exit 1 803 | ;; 804 | esac 805 | 806 | case $? in 807 | 0) ;; 808 | 1) exit 0 ;; 809 | *) exit $? ;; 810 | esac 811 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------