From 0e7415296c9b1ceeb5aeb146e83aec7d9eed519a Mon Sep 17 00:00:00 2001 From: fly Date: Mon, 9 Dec 2024 17:12:21 +0100 Subject: [PATCH] Initial commit Signed-off-by: fly --- .gitignore | 2 + WinWingFCU.xml | 477 ++++++++++++++++++++++++++++++++++++++++++++ displayProtocol.xml | 50 +++++ fcuSync.py | 337 +++++++++++++++++++++++++++++++ nix-shell.sh | 2 + 5 files changed, 868 insertions(+) create mode 100644 .gitignore create mode 100644 WinWingFCU.xml create mode 100644 displayProtocol.xml create mode 100755 fcuSync.py create mode 100755 nix-shell.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3819313 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.swp +*.swo diff --git a/WinWingFCU.xml b/WinWingFCU.xml new file mode 100644 index 0000000..4937b92 --- /dev/null +++ b/WinWingFCU.xml @@ -0,0 +1,477 @@ + + + + Winwing WINWING FCU-320 + true + + + = 0 and getprop("/it-autoflight/custom/trk-fpa") == false) or (getprop("/it-autoflight/input/fpa") >= 0 and getprop("/it-autoflight/custom/trk-fpa"))) and (getprop("/it-autoflight/output/vert") == 1 or getprop("/it-autoflight/output/vert") == 5))) { + flags = flags ~ ",plus"; + } + + if (getprop("/controls/switches/annun-test") or getprop("/it-autoflight/output/ap1")) { + flags = flags ~ ",ap1"; + } + if (getprop("/controls/switches/annun-test") or getprop("/it-autoflight/output/ap2")) { + flags = flags ~ ",ap2"; + } + if (getprop("/controls/switches/annun-test") or getprop("/it-autoflight/output/athr")) { + flags = flags ~ ",athr"; + } + if (getprop("/controls/switches/annun-test") or ((getprop("/it-autoflight/output/lat") == 2 or getprop("/it-autoflight/output/loc-arm") == 1) and getprop("/it-autoflight/output/gs-arm") == false and getprop("/it-autoflight/output/vert") != 2 and getprop("/it-autoflight/output/vert") != 6)) { + flags = flags ~ ",loc"; + } + if (getprop("/controls/switches/annun-test") or getprop("/it-autoflight/output/gs-arm") or getprop("/it-autoflight/output/vert") == 2 or getprop("/it-autoflight/output/vert") == 6) { + flags = flags ~ ",appr"; + } + + setprop("/winwing/flags", flags); + return [0x00]; # dummy return so log doesn't get spammed anymore + + }; + + var getButtonBrightnessReport = func(item, value) { + var r = [0x10, 0xBB, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + r[6] = item; + r[7] = int(value); + return r; + }; + var updateWinwingFCUIntegLT = func() { + var value = getprop("/controls/lighting/fcu-panel-norm") * 255; + # debug.dump(getButtonBrightnessReport(0x00, value)); + return getButtonBrightnessReport(0x00, value); + }; + ]]> + + + + + input + 1 + + 1 + 128 + + 176 + + + input + 2 + + 8 + 13 + + 0 + + + output + 2 + + 104 + 1 + + 0 + + + input + 240 + + 8 + 63 + + 0 + + + output + 240 + + 8 + 63 + + 0 + + + + 2 + output + /controls/lighting/fcu-panel-norm + updateWinwingFCUIntegLT + + + 2 + output + /it-autoflight/input/kts-mach + /it-autoflight/input/kts + /it-autoflight/input/mach + /it-autoflight/input/hdg + /it-autoflight/input/alt + /it-autoflight/output/vs-fcu-display + /controls/switches/annun-test + updateWinwingFCUDisplay + + + + button-1 + IAS/Mach + false + + nasal + + + + + button-2 + LOC + false + + nasal + + + + + button-3 + Track/Fligtht Path Angle + false + + nasal + + + + + button-4 + Autopilot 1 + false + + nasal + + + + + button-5 + Autopilot 2 + false + + nasal + + + + + button-6 + Autothrust + false + + nasal + + + + + button-7 + Expedite + false + + nasal + + + + + button-8 + Metric ALT + false + + nasal + + + + + button-9 + Approach + false + + nasal + + + + + button-10 + Speed decrease + false + + nasal + + + + + button-11 + Speed increase + false + + nasal + + + + + button-12 + Speed Push + false + + nasal + + + + + button-13 + Speed Pull + false + + nasal + + + + + button-14 + Heading decrease + false + + nasal + + + + + button-15 + Heading increase + false + + nasal + + + + + button-16 + Heading push + false + + nasal + + + + + button-17 + Heading pull + false + + nasal + + + + + button-18 + Altitude decrease + false + + nasal + + + + + button-19 + Altitude increase + false + + nasal + + + + + button-20 + Altitude Push + false + + nasal + + + + + button-21 + Altitude pull + false + + nasal + + + + + button-22 + Vertical Speed decrease + false + + nasal + + + + + button-23 + Vertical Speed increase + false + + nasal + + + + + button-24 + Vertical Speed push + false + + nasal + + + + + button-25 + Vertical Speed pull + false + + nasal + + + + + button-26 + Altitude mode 100 + false + + property-assign + it-autoflight/config/altitude-dial-mode + 0 + + + + button-27 + Altitude mode 1000 + false + + property-assign + it-autoflight/config/altitude-dial-mode + 1 + + + diff --git a/displayProtocol.xml b/displayProtocol.xml new file mode 100644 index 0000000..bed290f --- /dev/null +++ b/displayProtocol.xml @@ -0,0 +1,50 @@ + + + + + false + , + + d + + + /winwing/spd + string + %s + + + /winwing/hdg + string + %s + + + /winwing/alt + string + %s + + + /winwing/vs + string + %s + + + /controls/lighting/fcu-panel-norm + float + %01.2f + + + /controls/lighting/fcu-digit-norm + float + %01.2f + + + /winwing/flags + string + %s + + + d + + + + diff --git a/fcuSync.py b/fcuSync.py new file mode 100755 index 0000000..34777a4 --- /dev/null +++ b/fcuSync.py @@ -0,0 +1,337 @@ +#! /usr/bin/env python3 + +from dataclasses import dataclass +import time +import os +import sys +import socket + +from enum import Enum + +import hid + +hidraw_path = "" +for device in hid.enumerate(): + if device["vendor_id"] == 0x4098 and device["product_id"] == 0xbb10: + hidraw_path = device["path"] + +if hidraw_path == "": + print("Unable to find FCU device") + sys.exit(1) + +class Leds(Enum): + BACKLIGHT = 0 # 0 .. 255 + SCREEN_BACKLIGHT = 1 # 0 .. 255 + LOC_GREEN = 3 # all on/off + AP1_GREEN = 5 + AP2_GREEN = 7 + ATHR_GREEN = 9 + EXPED_GREEN = 11 + APPR_GREEN = 13 + FLAG_GREEN = 17 # 0 .. 255 + EXPED_YELLOW = 30 # 0 .. 255 + +# A +# --- +# F | G | B +# --- +# E | | C +# --- +# D +# A=0x80, B=0x40, C=0x20, D=0x10, E=0x02, F=0x08, G=0x04 +# Bits are valid for Speed display only, all other share bits in 2 databyte per lcd 7-segment display. +# Use function data_from_string_swapped to recalculate values +representations = { + '0' : 0xfa, + '1' : 0x60, + '2' : 0xd6, + '3' : 0xf4, + '4' : 0x6c, + '5' : 0xbc, + '6' : 0xbe, + '7' : 0xe0, + '8' : 0xfe, + '9' : 0xfc, + 'A' : 0xee, + 'B' : 0xfe, + 'C' : 0x9a, + 'D' : 0x76, + 'E' : 0x9e, + 'F' : 0x8e, + 'G' : 0xbe, + 'H' : 0x6e, + 'I' : 0x60, + 'J' : 0x70, + 'K' : 0x0e, + 'L' : 0x1a, + 'M' : 0xa6, + 'N' : 0x26, + 'O' : 0xfa, + 'P' : 0xce, + 'Q' : 0xec, + 'R' : 0x06, + 'S' : 0xbc, + 'T' : 0x1e, + 'U' : 0x7a, + 'V' : 0x32, + 'W' : 0x58, + 'X' : 0x6e, + 'Y' : 0x7c, + 'Z' : 0xd6, + '-' : 0x04, + '#' : 0x36, + '/' : 0x60, + '\\' : 0xa0, + ' ' : 0x00, +} + +class Byte(Enum): + H0 = 0 + H3 = 1 + A0 = 2 + A1 = 3 + A2 = 4 + A3 = 5 + A4 = 6 + A5 = 7 + V2 = 8 + V3 = 9 + V0 = 10 + V1 = 11 + S1 = 12 + + + +@dataclass +class Flag: + name : str + byte : Byte + mask : int + value : bool = False + + +flags = dict([("spd", Flag('spd-mach_spd', Byte.H3, 0x08, True)), + ("mach", Flag('spd-mach_mach', Byte.H3, 0x04)), + ("hdg", Flag('hdg-trk-lat_hdg', Byte.H0, 0x80)), + ("trk", Flag('hdg-trk-lat_trk', Byte.H0, 0x40)), + ("lat", Flag('hdg-trk-lat_lat', Byte.H0, 0x20, True)), + ("vshdg", Flag('hdg-v/s_hdg', Byte.A5, 0x08, True)), + ("vs", Flag('hdg-v/s_v/s', Byte.A5, 0x04, True)), + ("ftrk", Flag('trk-fpa_trk', Byte.A5, 0x02)), + ("ffpa", Flag('trk-fpa_fpa', Byte.A5, 0x01)), + ("alt", Flag('alt', Byte.A4, 0x10, True)), + ("hdg_managed", Flag('hdg managed', Byte.H0, 0x10)), + ("spd_managed", Flag('spd managed', Byte.H3, 0x02)), + ("alt_managed", Flag('alt_managed', Byte.V1, 0x10)), + ("vs_horz", Flag('v/s plus horizontal', Byte.A0, 0x10, True)), + ("vs_vert", Flag('v/s plus vertical', Byte.V2, 0x10)), + ("lvl", Flag('lvl change', Byte.A2, 0x10, True)), + ("lvl_left", Flag('lvl change left', Byte.A3, 0x10, True)), + ("lvl_right", Flag('lvl change right', Byte.A1, 0x10, True)), + ("fvs", Flag('v/s-fpa_v/s', Byte.V0, 0x40, True)), + ("ffpa2", Flag('v/s-fpa_fpa', Byte.V0, 0x80)), + ("fpa_comma", Flag('fpa_comma', Byte.V3, 0x10)), + ("mach_comma", Flag('mach_comma', Byte.S1, 0x01)), + ]) + + +def swap_nibbles(x): + return ( (x & 0x0F)<<4 | (x & 0xF0)>>4 ) + + +def winwing_fcu_set_led(led, brightness): + data = [0x02, 0x10, 0xbb, 0, 0, 3, 0x49, led.value, brightness, 0,0,0,0,0] + cmd = bytes(data) + send_data_to_hidraw(cmd) + + +def lcd_init(): + data = [0xf0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0] # init packet + cmd = bytes(data) + send_data_to_hidraw(cmd) + + +def data_from_string(num_7segments, string): + + l = num_7segments + d = [0] * (l) + for i in range(min(l, len(string))): + r = representations.get(string.upper()[i]) + if r == None: + r = 0xef + print("ERROR: char '{string.upper()[i]}' not found") + d[l-1-i] = r + return d + + +def data_from_string_swapped(num_7segments, string): # some 7-segemnts have wired mapping, correct ist here + # return array with one byte more than lcd chars + + l = num_7segments + + d = data_from_string(l, string) + d.append(0) + + # fix wired segment mapping special elements such at the bars at the LVL/CH + for i in range(len(d)): + d[i] = swap_nibbles(d[i]) + for i in range(0, len(d) - 1): + d[l-i] = (d[l-i] & 0x0f) | (d[l-1-i] & 0xf0) + d[l-1-i] = d[l-1-i] & 0x0f + + return d + + +def winwing_fcu_lcd_set(speed, heading, alt,vs, new): + s = data_from_string( 3, str(speed)) + h = data_from_string_swapped(3, str(heading)) + a = data_from_string_swapped(5, str(alt)) + v = data_from_string_swapped(4, str(vs)) + + bl = [0] * len(Byte) + for f in flags: + bl[flags[f].byte.value] |= (flags[f].mask * flags[f].value) + + pkg_nr = 1 + data = [0xf0, 0x0, pkg_nr, 0x31, 0x10, 0xbb, 0x0, 0x0, 0x2, 0x1, 0x0, 0x0, 0xff, 0xff, 0x2, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, s[2], s[1], s[0], h[3] | bl[Byte.H3.value], h[2], h[1], h[0] | bl[Byte.H0.value], a[5] | bl[Byte.A5.value], a[4] | bl[Byte.A4.value], a[3] | bl[Byte.A3.value], a[2] | bl[Byte.A2.value], a[1] | bl[Byte.A1.value], a[0] | v[4] | bl[Byte.A0.value], v[3] | bl[Byte.V3.value], v[2] | bl[Byte.V2.value], v[1] | bl[Byte.V1.value], v[0] | bl[Byte.V0.value], 0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0] + cmd = bytes(data) + send_data_to_hidraw(cmd) + + data = [0xf0, 0x0, pkg_nr, 0x11, 0x10, 0xbb, 0x0, 0x0, 0x3, 0x1, 0x0, 0x0, 0xff, 0xff, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0] + cmd = bytes(data) + send_data_to_hidraw(cmd) + +def send_data_to_hidraw(data): + global hidraw_path + try: + # Open the HIDRAW device + hidraw = os.open(hidraw_path, os.O_RDWR | os.O_NONBLOCK) + + # Write data to the HIDRAW device + os.write(hidraw, bytes(data)) + +# print("Data sent successfully to HIDRAW device.") + + # Close the HIDRAW device + os.close(hidraw) + except Exception as e: + print("Error:", e) + + +lcd_init() + +speed = 100 +heading = 360 +alt = 10000 +vs = '----' + + +winwing_fcu_set_led(Leds.BACKLIGHT, 70) +winwing_fcu_set_led(Leds.EXPED_YELLOW, 70) +winwing_fcu_set_led(Leds.SCREEN_BACKLIGHT, 200) +winwing_fcu_lcd_set(speed, heading, alt, vs, 0x0) + +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +s.bind(('', 12000)) +try: + while True: + message, address = s.recvfrom(1024) + print(message) + items = str(message).split(",") + winwing_fcu_lcd_set(items[1], items[2], items[3], items[4], 0x0) + winwing_fcu_set_led(Leds.BACKLIGHT, int(float(items[5]) * 255)) + winwing_fcu_set_led(Leds.EXPED_YELLOW, int(float(items[5]) * 255)) + winwing_fcu_set_led(Leds.SCREEN_BACKLIGHT, int(float(items[6]) * 255)) + + if "spd" in items: + flags["spd"].value = True + else: + flags["spd"].value = False + if "mach" in items: + flags["mach"].value = True + flags["mach_comma"].value = True + else: + flags["mach"].value = False + flags["mach_comma"].value = False + if "spd-mgt" in items: + flags["spd_managed"].value = True + else: + flags["spd_managed"].value = False + + if "lat" in items: + flags["lat"].value = True + flags["hdg_managed"].value = True + else: + flags["lat"].value = False + flags["hdg_managed"].value = False + + if "hdg-vs" in items: + flags["hdg"].value = True + flags["vshdg"].value = True + flags["vs"].value = True + flags["fvs"].value = True + else: + flags["hdg"].value = False + flags["vshdg"].value = False + flags["vs"].value = False + flags["fvs"].value = False + if "trk-fpa" in items: + flags["trk"].value = True + flags["ftrk"].value = True + flags["ffpa"].value = True + flags["ffpa2"].value = True + if items[4] != "----": + flags["fpa_comma"].value = True + else: + flags["fpa_comma"].value = False + else: + flags["trk"].value = False + flags["ftrk"].value = False + flags["ffpa"].value = False + flags["ffpa2"].value = False + flags["fpa_comma"].value = False + + if "plus" in items: + flags["vs_vert"].value = True + else: + flags["vs_vert"].value = False + + if "ap1" in items: + winwing_fcu_set_led(Leds.AP1_GREEN, 1) + else: + winwing_fcu_set_led(Leds.AP1_GREEN, 0) + if "ap2" in items: + winwing_fcu_set_led(Leds.AP2_GREEN, 1) + else: + winwing_fcu_set_led(Leds.AP2_GREEN, 0) + if "athr" in items: + winwing_fcu_set_led(Leds.ATHR_GREEN, 1) + else: + winwing_fcu_set_led(Leds.ATHR_GREEN, 0) + if "loc" in items: + winwing_fcu_set_led(Leds.LOC_GREEN, 1) + else: + winwing_fcu_set_led(Leds.LOC_GREEN, 0) + if "appr" in items: + winwing_fcu_set_led(Leds.APPR_GREEN, 1) + else: + winwing_fcu_set_led(Leds.APPR_GREEN, 0) + + +except KeyboardInterrupt: + s.close() + +#while True: +# buf_in = [None] * 7 +# winwing_fcu_set_led(Leds.AP1_GREEN, 1) +# winwing_fcu_set_led(Leds.AP2_GREEN, 0) +# winwing_fcu_lcd_set(speed, heading, alt, vs, 0x0) +# speed = speed + 1 +# heading = heading + 3 +# if heading > 360: +# heading = 0 +# time.sleep(0.5) +# winwing_fcu_set_led(Leds.AP1_GREEN, 0) +# winwing_fcu_set_led(Leds.AP2_GREEN, 1) +# winwing_fcu_lcd_set(speed, heading, alt, vs, 0xff) +# time.sleep(0.5) diff --git a/nix-shell.sh b/nix-shell.sh new file mode 100755 index 0000000..a6adb30 --- /dev/null +++ b/nix-shell.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +nix-shell -p "python311.withPackages (ps: with ps; [ hidapi ])"