QMK Toolbox

Missing Linux Version

QMK Toolbox is a program that allows one to easily re-program QMK-Firmware based keyboards. The program is available as a WinForms application, and a Swift-based macOs version is also available. QMK delegates all the hard work of erasing and programming the flash to other executables.

Consequently, I decided to create a new version of the program capable of running on Linux, as this is my OS of choice for devops work.

Enter Avalonia

Avalonia is a cross-platform framework based on WPF, a presentation library for Windows desktops. I did work with WPF years ago, thus the barrier of entry was low for me.

Main Window code

Below is the code for the main Window – this is just to give readers an idea of what this looks like.

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:QMK_Toolbox.ViewModels"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:r="clr-namespace:QMK_Toolbox.Properties" 
        mc:Ignorable="d" 
        d:DesignWidth="800" 
        d:DesignHeight="450"
        x:Class="QMK_Toolbox.Views.MainWindow"
        Initialized="OnInitialized"
        Width="800"
        DragDrop.AllowDrop="True"
        Height="680"
        FontSize="10"
        MinWidth="800"
        MinHeight="680"
        Title="{x:Static r:Resources.ApplicationTitle}"
        Background="#f0f0f0"
        Icon="/Resources/qmk.ico">

    <Design.DataContext>
        <vm:MainWindowViewModel />
    </Design.DataContext>

    <Grid HorizontalAlignment="Stretch" 
          ShowGridLines="False" 
          VerticalAlignment="Stretch" 
          RowDefinitions="22,20,30,30,*,30,5"
          ColumnDefinitions="10,*,200,10">
        <Grid.Resources>
   
        </Grid.Resources>
        <Menu Grid.Row="0" 
              Grid.Column="0"
              Grid.ColumnSpan="2"
              Margin="0,3,0,3">
            <MenuItem Header="{x:Static r:Resources.Menu_File}">
                <MenuItem Header="{x:Static r:Resources.Menu_FileOpen}" Command="{Binding OpenFileCommand}"/>
                <Separator/>
                <MenuItem Header="{x:Static r:Resources.Menu_FileExit}" Command="{Binding CloseCommand}"/>
            </MenuItem>
            <MenuItem Header="{x:Static r:Resources.Menu_Tools}">
                <MenuItem Header="Flash" Command="{Binding FlashCommand}" />
                <MenuItem Header="EEPROM" Command="{Binding ClearEepromCommand}" />
                <MenuItem Header="Exit DFU" Command="{Binding ExitDfuCommand}"/>
                <Separator/>
                <MenuItem Header="{x:Static r:Resources.Menu_ToolsFlash}" Command="{Binding IsAutoFlash }" >
                    <MenuItem.Icon>
                        <CheckBox IsChecked="{Binding IsAutoFlash}" BorderThickness="0">
                        </CheckBox>
                    </MenuItem.Icon>
                </MenuItem>
                <MenuItem Header="{x:Static r:Resources.Menu_ToolShowAll}"/>
                <Separator/>
                <MenuItem Header="{x:Static r:Resources.Menu_ToolsKeyTester}" IsEnabled="False"/>
            </MenuItem>
            <MenuItem Header="{x:Static r:Resources.Menu_Help}">
                <MenuItem Header="{x:Static r:Resources.Menu_HelpCheckNew}" IsEnabled="False"/>
                <Separator/>
                <MenuItem Header="{x:Static r:Resources.Menu_HelpAbout}"/>
            </MenuItem>
        </Menu>
        <Label
            FontSize="10"
            Grid.Row="1"
            Grid.Column="1"
            Padding="0,0,0,0"
            VerticalAlignment="Top"
            Margin="0,8,0,0"
            HorizontalAlignment="Left"
            Content="{x:Static r:Resources.Label_LocalFile}" />
        <Label
            FontSize="10"
            Grid.Row="1"
            Grid.Column="2"
            Padding="0,0,0,0"
            VerticalAlignment="Top"
            HorizontalAlignment="Right"
            Margin="0,8,42,0"
            Content="{x:Static r:Resources.Label_MCU}" />
        <Border x:Name="border"
            BorderThickness="1"
            BorderBrush="Black"
            Grid.Row="2"
            Grid.Column="1"
            VerticalAlignment="Center"
            HorizontalAlignment ="Stretch">
            <TextBlock
                x:Name="HexFile"
                TextWrapping="Wrap"
                Padding="2,3,0,0"
                Height="19"
                Background="White"
                VerticalAlignment="Center"
                HorizontalAlignment="Stretch"
                Margin="0,0,0,0"
                FontSize="10"
                Text="{Binding HexFile}" />
        </Border>
        <StackPanel
            Orientation="Horizontal"
            Grid.Row="2"
            Grid.Column="2"
            HorizontalAlignment="Right">
            <Button
                Command="{Binding OpenFileCommand}"
                Margin="0,0,10,0"
                Padding ="2,4,0,0"
                VerticalAlignment="Center"
                HorizontalAlignment="Right"
                Height="22"
                Width="45"
                FontSize="10"
                Content="{x:Static r:Resources.Button_Open}" />
            <ComboBox
                x:Name="McuCombo"
                Padding ="4,2,0,0"
                Items="{Binding Mcus}"
                VerticalAlignment="Center"
                HorizontalAlignment="Right"
                BorderBrush="DarkGray"
                FontSize="10"
                Width="120"
                Height="22"
                SelectedIndex="{Binding SelectedMcuIndex}" />
        </StackPanel>
        <StackPanel
            Orientation="Horizontal"
            Grid.Row="3"
            Grid.Column="1"
            Grid.ColumnSpan="2"
            HorizontalAlignment="Right">
            <Label
                VerticalAlignment="Center"
                HorizontalAlignment="Left"
                Height="20"
                Margin="0,2,0,0"
                FontSize="11"
                Content="{x:Static r:Resources.Label_AutoFlash}" />
            <CheckBox
                x:Name="CheckBox"
                Margin="0,0,5,0"
                Padding="0,0,0,0"
                FontSize="8"
                Height="22"
                IsChecked="{Binding IsAutoFlash}"
                VerticalAlignment="Center"
                HorizontalAlignment="Left" >
            </CheckBox>
            <Button
                Command="{Binding FlashCommand}"
                VerticalAlignment="Center"
                HorizontalAlignment="Left"
                HorizontalContentAlignment="Center"
                Padding ="3,4,0,0"
                Height="22"
                Width="55"
                FontSize="10"
                Margin="0,0,5,0"
                Content="{x:Static r:Resources.Button_Flash}" />
            <Button
                Command="{Binding ClearEEPromCommand}"
                VerticalAlignment="Center"
                HorizontalAlignment="Left"
                Padding ="0,4,0,0"
                HorizontalContentAlignment="Center"
                Height="22"
                Width="110"
                FontSize="10"
                Margin="0,0,5,0"
                Content="{x:Static r:Resources.Button_Clear}" />
            <Button
                Command="{Binding ExitDfuCommand}"
                VerticalAlignment="Center"
                Padding ="0,4,0,0"
                HorizontalAlignment="Left"
                HorizontalContentAlignment="Center"
                Height="22"
                Width="70"
                FontSize="10"
                Content="{x:Static r:Resources.Button_ExitDFU}" />
        </StackPanel>
        <Border 
            Margin="0,0,0,0"
            BorderThickness="0"
            BorderBrush="DarkGray"
            Grid.ColumnSpan="2"
            Grid.Row="4"
            Grid.Column="1">
            <TextBlock
                Padding="0,0,0,0"
                FontFamily="Courier New"
                Background="#1e1e1e"
                Foreground="DarkGray"
                FontSize="13"
                ScrollViewer.VerticalScrollBarVisibility ="Auto"
                Margin="0,5,0,6"
                VerticalAlignment="Stretch"
                HorizontalAlignment="Stretch"
                Text="{Binding Log}" />
        </Border>
        <ComboBox x:Name="HidDeviceCombo"
              Background="LightGray"
              Margin="0,0,0,0"
              Grid.Row="5"
              Grid.Column="1"
              VerticalAlignment="Top"
              HorizontalAlignment="Stretch"
              FontSize="10"
              Height="22"
              BorderBrush="DarkGray"
              Grid.ColumnSpan="2"
              SelectedIndex="0">
            <ComboBoxItem Content="{x:Static r:Resources.NoHID}"/>
        </ComboBox>
    </Grid>
</Window>

One can find the full code in my github repo.

Missing Features in the Linux code

While the main UI is working, including the auto-flash functionality, the following features are missing:

  • HID console logging
  • Keyboard tester window
  • About Box
  • Main program does not install tools to program/flash MCU.
  • Program does not implement Show all devices (bootloaders connections/disconnections works.

Challenges

I re-wrote the USB detection code completely, Windows handles this with WMI events. Fortunately, LibDotNetUsb came to the rescue, implementing a cross-platform way of interfacing to the systems USB driver. I leaned quite a bit about USB with this project.

I re-jigged the UI to use Avalonia/MVVM Pattern

The FileOpenDialog was not working on Linux at first due to an async/await issue.

The UI is now localizable, with strings in Resources.

Conclusion

This was a fun litte project over the holidays which tought me lots about doing cross platform development in C# for desktop apps.

Leave a Reply