[{"element_type":"text","content":"\n        \u003ch2\u003e\u003cstrong\u003eIntroduction\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003eThe flight simulator industry has spawned dozens of custom controllers for those who are looking for a more realistic and entertaining experience. These controllers range from yokes and throttles to communication and GPS systems and their price can rival the costs of actual aviation equipment.\u003c/p\u003e\n\u003cp\u003eThankfully there is a way to create your own controllers using low cost microcontrollers, buttons, encoders and many other input devices. There are even ways to output settings to LEDs, LED segments and displays but this guide does not cover output scenarios.\u003c/p\u003e\n\u003cp\u003eFor this hookup guide I am using a controller I made for myself. The G1000 glass cockpit has a dual-rotary encoder in the lower right corner labeled FMS (Flight Management System) that is a pain to control with a mouse, even more so while the plane is in the air. So I decided to build my own controller to simulate the G1000 corner. My controller includes the FMS encoder knobs and the 6 buttons that tend to be used at the same time.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/gamblor21/G1000-FMS-Controller\" target=\"_blank\"\u003eYou can find the source files and STL files I used on GitHub\u003c/a\u003e.\u003c/p\u003e\n      ","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/000/739/original/fms-real.png?1710123830","metadata":{}},{"element_type":"alert","content":"This is not a full guide on making a CircuitPython HID controller, but a guide on using that CircuitPython controller along with MobiFlight.","metadata":{"class":"element alert-element build-alert alert-info","markdown":"","alert_type":"info"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003e\u003cstrong\u003eThe Controller\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003eFor my FMS controller I found the \u003ca href=\"https://www.digikey.ca/en/products/detail/bourns-inc/PEC11D-4120F-H0015/15926291\" target=\"_blank\"\u003ePEC11D-4120F-H0015\u003c/a\u003e, a dual concentric rotary encoder (meaning it has an inner and outer encoder ring that can turn). This is not the FAA approved version on the actual G1000 but works the same and cheaper. The dual rotary encoder is similar to a single encoder except has two sets of A/B/C pins. The encoder also has a push switch. The push buttons above the encoder are standard push buttons.\u003c/p\u003e\n\u003cp\u003eThe encoder and buttons are wired to a Feather board but almost any board that can run CircuitPython with USB HID enabled will work.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/000/738/original/fms.png?1710118674","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eThere are two steps to setting up the code, creating the HID report descriptor in \u003cem\u003eboot.py \u003c/em\u003eand the python code to read and send reports to the host computer in \u003cem\u003ecode.py\u003c/em\u003e.\u003c/p\u003e\n\u003cp\u003e\u0026nbsp;\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003e\u003cstrong\u003eBOOT.PY\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003eBoot.py is ran once when the microcontroller first powers up (and does not run on resets caused with the reset button or through code). So remember that if you change boot.py you will have to power cycle your controller to have the change take effect.\u003c/p\u003e\n\u003cp\u003eThis file is where the USB HID report descriptor is defined and enabled. The report descriptor tells the host operating system what information to expect from your game controller. This may include status on buttons, joystick movement, throttle settings and more. I wrote a \u003ca href=\"https://adafruit-playground.com/u/Gamblor21/pages/a-beginners-guide-to-writing-usb-hid-report-descriptors-by-a-beginner\" target=\"_blank\"\u003equick guide on how to create your own report descriptor\u003c/a\u003e that can be used to aid in this step.\u003c/p\u003e\n\u003cp\u003eThe FMS controller creates two sets of buttons. The first set of five buttons are for the encoder and the second are for the six push buttons above. This results in a report that is 2 bytes in size. The first byte using 5 bits (with 3 padding) and the second byte using 6 bits (with 2 padding).\u003c/p\u003e\n\u003cp\u003eAfter defining the report descriptor, boot.py creates and enables the descriptor with the USB_HID library. This HID device will later be available to code.py. The descriptor must be created at boot time so it correctly registers with the host operating system as CircuitPython starts up.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"# boot.py\n\nimport usb_hid\n\nSIM_JOYSTICK_REPORT_DESCRIPTOR = bytes((\n    0x05, 0x01,    # UsagePage(Generic Desktop[0x0001])\n    0x09, 0x04,    # UsageId(Joystick[0x0004])\n    0xA1, 0x01,    # Collection(Application)\n    0x85, 0x01,    #     ReportId(1)\n    0x05, 0x09,    #     UsagePage(Button[0x0009])\n    0x19, 0x01,    #     UsageIdMin(Button 1[0x0001])\n    0x29, 0x05,    #//     UsageIdMax(Button 5[0x0005])\n    0x15, 0x00,    #//     LogicalMinimum(0)\n    0x25, 0x01,    #//     LogicalMaximum(1)\n    0x95, 0x05,    #//     ReportCount(5)\n    0x75, 0x01,    #//     ReportSize(1)\n    0x81, 0x02,    #//     Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField)\n    0x95, 0x01,    #//     ReportCount(1)\n    0x75, 0x03,    #//     ReportSize(3)\n    0x81, 0x03,    #//     Input(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField)\n    0x19, 0x06,    #//     UsageIdMin(Button 6[0x0006])\n    0x29, 0x0B,    #//     UsageIdMax(Button 11[0x000B])\n    0x95, 0x06,    #//     ReportCount(6)\n    0x75, 0x01,    #//     ReportSize(1)\n    0x81, 0x02,    #//     Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField)\n    0x95, 0x01,    #//     ReportCount(1)\n    0x75, 0x02,    #//     ReportSize(2)\n    0x81, 0x03,    #//     Input(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField)\n    0xC0,          #// EndCollection()\n))\n\nsim_joystick = usb_hid.Device(\n    report_descriptor=SIM_JOYSTICK_REPORT_DESCRIPTOR,\n    usage_page=0x01,           # Generic Desktop Control\n    usage=0x04,                # Joystick\n    report_ids=(1,),           # Descriptor uses report ID 1.\n    in_report_lengths=(2,),    # This controller sends 2 bytes in its report.\n    out_report_lengths=(0,),   # It does not receive any reports.\n)\n\nusb_hid.enable(\n    (sim_joystick,\n    )\n)","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003e\u003cstrong\u003eCODE.PY\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003eCode.py sets up and monitors the encoder and push buttons on the controller. It then sends that information using HID reports to the host operating system.\u003c/p\u003e\n\u003cp\u003eThere is no set rule on how you send these updates. You can send the updates when a change is detected in the inputs or you can send the updates continuously every few milliseconds. It is important to ensure the program using the controller does have enough time to read the current report and does not miss the change or ignore it thinking it changed too fast.\u003c/p\u003e\n\u003cp\u003eThe CircuitPython program used for my FMS example controller is relatively simple. The first section initializes the encoder and the push buttons.\u003c/p\u003e\n\u003cp\u003eThe remaining code reads the current state of encoder and push buttons and sets the correct bits in the report descriptor if required.\u003c/p\u003e\n\u003cp\u003eFor example if the encoder button is pressed, the 5th bit is toggled on (000\u003cstrong\u003e1\u003c/strong\u003e 0000)\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eif button.value is False:\u003cbr\u003e\u0026nbsp; \u0026nbsp;report[0] |= 0x10\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eEnsure you remember which bits from the HID descriptor map to which physical controller inputs. The button (or other input) names will be used when mapping to MobiFlight.\u003c/p\u003e\n\u003cp\u003eAs in the example above as the encoder button is the 5th bit, it will be Button 5.\u003c/p\u003e\n      \n\n\n\n\n\n\n","metadata":{}},{"element_type":"alert","content":"There is a delay at the start of the program. If code.py starts to send reports before the host operating system is finished recognizing your controller the controller may stop working, to the point you do not see the CircuitPython drive mount and you will have to go into safe-mode to fix the issue.","metadata":{"class":"element alert-element build-alert alert-warning","markdown":"","alert_type":"warning"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n        \u003ch3\u003e\u003cstrong\u003eHow to Send Encoder Information\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003eCompared to buttons, sending encoder data is trickier. There are two potential ways to send the changes of an encoder.\u003c/p\u003e\n\u003cp\u003eJust like with a throttle or joystick you can send a value range (e.g. from -1024 to +1024) in the report description based on the position of the encoder. The problem with this method is if the encoder is turned to hit the maximum or minimum values. It would be like hitting a hard stop where the encoder no longer turns. On the other hand this method ensures if you turn the encoder quickly no position changes are missed.\u003c/p\u003e\n\u003cp\u003eThe other way to send encoder data is to treat turning clockwise and counter-clockwise as button presses, one button for each direction. The host program can interpret the button press as turning the encoder by one. The problem with this method is you must ensure that the \"button\" is pressed long enough for the host program to read it and if the encoder is turned too quickly it is possible a \"button\" press may be missed.\u003c/p\u003e\n\u003cp\u003eIn this example controller the second method is used.\u003c/p\u003e\n      \n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"# code.py\n\nimport usb_hid\nimport time\nimport rotaryio\nimport digitalio\nimport board\n\ntime.sleep(10) # ensure the host OS is ready\nprint(\"Starting\")\n\nin_rot = rotaryio.IncrementalEncoder(board.IO5, board.IO6)\nin_rot.divisor = 2\nout_rot = rotaryio.IncrementalEncoder(board.IO12, board.IO14)\nout_rot.divisor = 2\nbutton = digitalio.DigitalInOut(board.IO18)\nbutton.direction = digitalio.Direction.INPUT\nbutton.pull = digitalio.Pull.UP\n\nbuttons = []\nfor pin in [board.IO33, board.IO38, board.IO1, board.IO3, board.IO7, board.IO10]:\n    b = digitalio.DigitalInOut(pin)\n    b.direction = digitalio.Direction.INPUT\n    b.pull = digitalio.Pull.UP\n    buttons.append(b)\n\ndevice = usb_hid.devices[0]\n\nreport = bytearray(2)\n\nlast_in_position = 0\nlast_out_position = 0\n\nwhile True:\n    report[0] = 0\n    report[1] = 0\n\n    position = in_rot.position\n    if position \u003e last_in_position:\n        report[0] |= 0x01\n        last_in_position = position\n    elif position \u003c last_in_position:\n        report[0] |= 0x02\n        last_in_position = position\n\n    position = out_rot.position\n    if position \u003e last_out_position:\n        report[0] |= 0x04\n        last_out_position = position\n    elif position \u003c last_out_position:\n        report[0] |= 0x08\n        last_out_position = position\n\n    if button.value is False:\n        report[0] |= 0x10\n\n    for i in range(0,len(buttons)):\n        if buttons[i].value is False:\n            report[1] |= (1 \u003c\u003c i)\n\n    device.send_report(report)\n    # ensure the program using the controller has time to realize the encoder \"button\" was pressed\n    time.sleep(0.03)","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n        \u003ch2\u003e\u003cstrong\u003eConnecting to the Sim - Enter MobiFlight\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://www.mobiflight.com/en/index.html\" target=\"_blank\"\u003eMobiFlight\u003c/a\u003e is an open source project to assist in integrating custom controllers with flight simulators. This integration allows you to control items (like the FMS knob) in the simulator that do not have an entry in the bindings menu.\u003c/p\u003e\n\u003cp\u003eThe alternative to using MobiFlight would be writing your own custom plugin for each flight simulator you wish to control with your custom controller. Not a small or easy task.\u003c/p\u003e\n\u003cp\u003eMobiFlight offers two modes. For certain microcontrollers, such as the Raspberry Pi Pico, MobiFlight can upload custom firmware (written in C) that allows you to configure attached pins, buttons, encoders, etc. through a no-code menu system. That mode of operation is not covered in this guide but lots of examples can be found on the MobiFlight site.\u003c/p\u003e\n\u003cp\u003eThe second mode, that we are interested in, is used to map a game controller to in-game items. This allows you to use your controller to affect items that are not exposed in the standard bindings menu in an easy way without writing a custom simulator plugin.\u003c/p\u003e\n      \n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/000/740/original/mobiflight1.png?1710297039","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n        \u003cp\u003eOnce MobiFlight is started go to the \"Input configs\" tab to start mapping your controller buttons to flight sim controls.\u003c/p\u003e\n\u003cp\u003eTo start enter a description of the input you are about to map. In my example I named them after the labels on the actual buttons with In and Out for the encoder inner and outer rings turning left or right. Then click on the 3 dots under edit to do the mapping.\u003c/p\u003e\n\u003cp\u003e\u0026nbsp;\u003c/p\u003e\n      \n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/000/741/original/mobiflight2.png?1710297052","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n        \u003cp\u003eOn the Edit screen you choose which Module you want to map. In our example this is the CircuitPython controller. Device refers to the button, movement axis or other HID object you are mapping.\u003c/p\u003e\n\u003cp\u003eMobiFlight offers a lot of potential actions. For a button it can trigger upon the press, release, a hold of a predefined amount of time and a hold and release.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003eIn this example the Action Type specifies that we are changing a specific flight sim (MS Flight Sim). But MobiFlight also has advanced methods to alter and control internal variables that may affect other controls. The MobiFlight wiki and documentation cover these cases.\u003c/p\u003e\n\u003cp\u003eThe last section of this screen will change depending on the Action Type selected. In this example we want to trigger an action within the simulator. There are thousands of potential actions and the Search box and filters beside will help narrow down the selections.\u003c/p\u003e\n      \n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n        \u003ch2\u003e\u003cstrong\u003eTime to Fly\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003eAfter everything is configured you are ready for takeoff! Ensure your controller is plugged in, your seat belt is fastened and tray table is in the upright position, start the flight simulator, and press \"Run\" on MobiFlight (which will automatically connect to your simulator).\u003c/p\u003e\n\u003cp\u003eFor this example I started up a flight in a Diamond DA40 and a Cessna 172, looked at the G1000 display, pressed the \"FPL\" button started and started to enter my flight plan.\u003c/p\u003e\n\u003cp\u003eThis is just a brief introduction to creating your own flight controls. Both CircuitPython HID devices and MobiFlight offer much more functionality and will allow you to create almost any flight controls that exist or you can imagine.\u003c/p\u003e\n\u003cp\u003eHave fun and safe flights!\u003c/p\u003e\n      \n\n","metadata":{}}]