SimHub plugins

Click for discord discussions
Wotever 12/28/2017
To share a plugin, you must first rename it in project settings,
and also rename the plugin class
after which you will just have to give the plugin dll
and enable it into the settings.
Plugins are the ultimate extensibility point :D, everything in simhub is based on it.
PluginDemo.cs
https://discord.com/channels/299259397060689920/306850703299575809/396037829886738432

7.4.17b1 - SDK now demonstrate how to use AttachDelegate instead of AddProperty/SetPropertyValue

Wotever 10/16/2021 8:47 AM
Attach property is a third property declaration way :D, there are three ways :
- SetPropertyValue : The simplest but the slowest, set the value at each update loop
- Attachproperty : it allows to add a "wrapper" for a property,
  which can be set at each game loop (it was an convoluted variation of the previous one,
  skipping SetPropertyValue costs,
  but keeping the disavantages of requiring computations for each loop.
  I keep this one in use internally for stuffs I need to compute for each loop anyway
  (about 40 properties in grand total I would say)
- AttachDelegate : zero costs if it's not in use

Attachproperty looks like:
public AttachedProperty RGBIntensity = new AttachedProperty();
public void Init(PluginManager pluginManager)
{
    RGBIntensity.Value = initialvalue;
    pluginManager.AttachProperty("RGBIntensity", typeof(SerialDashPlugin), properties.RGBIntensity);
}

void anotherfunc(){
      // This update has no particular CPU costs except computing "newvalue"
       RGBIntensity.Value = newvalue;
}

AttachProperty requires computing a result at each loop;
AttachDelegate is 100% on demand:
// Declare a property available in the property list,
// this gets evaluated "on demand" (when shown or used in formulas)
// '=>' is C# syntax for lambda experssion, in this case without input parameters
// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-operator
this.AttachDelegate("CurrentDateTime", () => DateTime.Now);

Andreas Dahl 02/22/2022 9:58 AM
AddProperty() in the Init scope, and do all data collection,
computation and SetPropertyValue() in the DataUpdate scope

SimHub SDK How-To

Plugins can interact via Properties, Actions and Events,
where Events (e.g. button presses) may launch Actions.
Properties can contain numeric or string variables.
Instead of requiring code to check for changing values e.g. 60 times per second,
one presumably provokes an Event to launch Action code that e.g. changes Properties.

Packaged as .dll files, SimHub plugins are installed by dragging into
the folder containing SimHubWPF.exe and numerous other .dll files.
SimHub presumably sorts plugins from other .dll files by this line in their source:
  using SimHub.Plugins;

SimHub and its plugin SDK are written in C#.
C# and Visual Studio struggles are addressed in this C# Visual Studio How-To.
    - SimHub wants .NET framework 4.8, per Wotever 8.2.0 release 12/16/2022

Click for tips

First tip: starting with a working example plugin

Conventions for plugin C# may differ appreciably from generic .NET C# apps:
  • Not an App;  a DLL launched by SimHub
  • Best created in a SimHub/PluginSdk/ folder;  relative path to SimHub classes and .dll build destination
  • Sadly, Visual Studio 2019 rejects relative path to SimHubWPF.exe for Debug <StartProgram>;
    Visual Studio 2022 fixed that:
      <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
        <StartAction>Program</StartAction>
        <StartProgram>.\SimHubWPF.exe</StartProgram>
      </PropertyGroup>
  • Obvious starting example:  SimHub/PluginSdk/User.PluginSdkDemo/, which has an XAML user interface
  • Also simple, another separate GitHub project:  SimHub/PluginSdk/CalcLngWheelSlip

Properties are listed inside Available Properties.
 

SimHub plugins can add more properties, which can be read in other plugins e.g. ShakeIt.
Similarly, plugins can read properties that are e.g. set in the ShakeIt plugin.
e.g. (Dahl Design Properties reads and sets properties in iRacing.cs).
...
namespace DahlDesign.Plugin.iRacing
{
  public class Data : SectionBase
  {
    public override void Init(PluginManager pluginManager)
    {
...
      InitializeLists();

      #region SimHub Properties
...
      Base.AddProp("SlipLF", 0);
      Base.AddProp("SlipRF", 0);
      Base.AddProp("SlipLR", 0);
      Base.AddProp("SlipRR", 0);
...
      #endregion
    }

    public override void DataUpdate(PluginManager pluginManager, ref GameData data)
    {
...
      double slipLF = Convert.ToDouble(pluginManager.GetPropertyValue("ShakeITMotorsV3Plugin.Export.WheelSlip.FrontLeft"));
      double slipRF = Convert.ToDouble(pluginManager.GetPropertyValue("ShakeITMotorsV3Plugin.Export.WheelSlip.FrontRight"));
      double slipLR = Convert.ToDouble(pluginManager.GetPropertyValue("ShakeITMotorsV3Plugin.Export.WheelSlip.RearLeft"));
      double slipRR = Convert.ToDouble(pluginManager.GetPropertyValue("ShakeITMotorsV3Plugin.Export.WheelSlip.RearRight"));
...

      Base.SetProp("SlipLF", slipLF);
      Base.SetProp("SlipRF", slipRF);
      Base.SetProp("SlipLR", slipLR);
      Base.SetProp("SlipRR", slipRR);
    }
  }
}
By appropriately gluing something like this to MIDI code,
MIDI sliders can be used to set SimHub property values,
while SimHub ShakeIt property values can set MIDI values
for e.g. modifying ShakeIt signals outside SimHub.

Tip #1.5:  Avoiding user interface
@Romainrob 20 Jan 2023:  "remove IWPFSettingsV2 from the class declaration"
public class DataPluginDemo : IPlugin, IDataPlugin//, IWPFSettingsV2

Tip #2:  Initializing properties
Global properties are easily created using NCalc;
Avoiding user interface code, several of my plugins are configured by editing an NCalc script.
NCalc examples are available in \SimHub\NCalcScripts\examples.ini;
edit a copy of MIDIio.ini in that folder.

Plugins can also access Custom Expression properties during Init {}.
SimHub evaluates those custom expressions before [re]launching plugins e.g. after game changes.

Tip #3:  Editing source while debugging
Visual Studio will throw up all sorts of confusing warnings and error messages
which are not actual problems, other than source changes' impacts on that current debugging session.
Regardless, some plugin source code changes are better made while SimHub runs.
Running SimHub standalone, rather than launched via Visual Studio Debug avoids that.

Tip #4:  grep is your friend
Searching for the Visual Studio way to e.g. convert code from WPF to console
mostly yields recommendations to start over.
Instead, grepping for names used in WPF content
will show where/how they are referenced e.g. in the .csproj file.
Rather than sort where in the Visual Studio user interface such stuff is controlled,
employ editor of choice to directly alter project files while Visual Studio is not running.

Tip #5:  removing unused stuff
Visual Studio noted that using System.Windows.Media; was no longer used and prompted its removal
Visual Studio can also remove unused Solution Explorer Dependencies > Assemblies entries,
but only in SDK projects, which SimHub plugins by default are not.

Tip #6:  debugging strings as properties
writing to SimHub's log works, but messages can be missed in the deluge.
Instead, repurpose a delegate property, e.g. string PluginMsg;:
PluginMsg = $"CustomSerial.Write({Traffic[1]})";
CustomSerial.Write(Traffic[1]);
PluginMsg += " sent";
If "sent" is not seen in the PluginMsg property, then the plugin hung or failed in CustomSerial.Write(Traffic[1]);

C# plugin source and discussions
Here is an older and smaller PluginDemo.cs.
It appears to include a trick for Initializing a list of properties that differ only by a number.

OpenAI ChatGPT code for SImHub plugin using DryWetMIDI

// Here is an example of how you might use the DryWetMIDI library
// in a SimHub plugin to send MIDI messages:

using SimHub;
using DryWetMIDI.Devices;
using DryWetMIDI.Interaction;

namespace MySimHubPlugin
{
    public class MySimHubPlugin : Plugin
    {
        private OutputDevice _midiOut;

        public override void Start()
        {
            // Connect to the first available MIDI output device
            _midiOut = OutputDevice.GetByName("My MIDI Device");
        }

        public override void Update()
        {
            // Send a MIDI message to turn on a note
            _midiOut.Send(new NoteOn(Channel.Channel1, 60, 100));
        }
    }
}
/* This plugin connects to the first MIDI output device that is available
 ; and sends a MIDI "note on" message to it on each frame.
 ; The message will turn on a note with a pitch of 60 (middle C)
 ; and a velocity of 100 on channel 1.
 */

MIDIio
15 Jan 2023 plugin experiment
- rename User.PluginSdkDemo to blekenbleu.PluginSdkDemo
- update SimHub from 7.4.23 to 8.2.0, which restored/updated
  > User.PluginSdkDemo
  > User.LedEditorEffect
- User.PluginSdkDemo files:
   DataPluginDemo.cs, DataPluginDemoSettings.cs unchanged
   in User.PluginSdkDemo.csproj
        v4.7.2            became v4.8
- as with v 7.4.23, all replays work, except AC

 StreamDeck SimHub Plugin
https://www.reddit.com/r/simracing/comments/ru20od/streamdeck_simhub_plugin/

Visual Studio project folder rename
https://medium.com/c-sharp-progarmming/safely-rename-a-project-folder-visual-studio-f3c6bd4d0bd6

Rename a code symbol refactoring
https://learn.microsoft.com/en-us/visualstudio/ide/reference/rename?view=vs-2019
https://learn.microsoft.com/en-us/visualstudio/ide/class-designer/refactoring-classes-and-types?view=vs-2019
https://blog.ndepend.com/top-10-visual-studio-refactoring-tips/
17 Jan push to GitHub; Debug output:
Resource not found; ResourceKey='MahApps.Metro.Styles.FlatButtonFocusVisualStyle'
change 'Styles/Accents/' to 'Styles/Themes/' in Properties/DesignTimeResources.xaml
This actually seems to have worked.

"It could be that you don't have an App,
  if so then you must add the resources to every Window of your extension.
  Another trick is to use the DynamicResource instead StaticResource,
  because the resources will maybe are loaded later"
  https://github.com/MahApps/MahApps.Metro/issues/4046
20-21 Jan: eliminating unused resources
SDK project conversion enabled removing unused library references,
but the conversion tool failed to convert <OutputPath> to <OutDir> in .csproj file,
and VS user interface no longer offers changing Debug program in Properties,
although setting in .csproj file is still honored.
22 Jan: renaming MIDIioSettings class to MIDIdrywet
Using VS to rename the MIDIioSettings class
also provoked it to rename the containing MIDIioSettings.cs to MIDIdrywet.cs.
However, MIDIioSettings.cs was under git version control, but VS did not execute a git renaming...
... perhaps because having not previously fiddled with VS Git settings?
Consequently, git mv was done manually from a shell.
After selecting the local repository from its drop-down,
committing the class change from within VS was recognized by GitHub Desktop.

Hacked the MIDI equivalent of "Hello World"
from Melanchall's example C# source for Input and (eventually) Output devices.
Then, glued this MIDIdrywet class into MIDIio.cs...
Added an exception handler for no MIDI input device matching the provided name.
List device names as known to Melanchall.DryWetMidi.Devices...
	INFO	Event received from 'nanoKONTROL2': Control Change [0] (16, 72)
	INFO	ControlNumber = '16'; ControlValue = '72
	INFO	Event received from 'nanoKONTROL2': Control Change [0] (16, 73)
	INFO	ControlNumber = '16'; ControlValue = '73
	INFO	Event received from 'nanoKONTROL2': Control Change [0] (16, 74)
	INFO	ControlNumber = '16'; ControlValue = '74
	INFO	Event received from 'nanoKONTROL2': Control Change [0] (16, 75)
	INFO	ControlNumber = '16'; ControlValue = '75
Need to save and restore Sliders and Knobs in Settings.
23 Jan: Sliders and Knobs saved/restored in Settings
Sadly, SimHub crashes if/when MIDIio and SimHub Midi Controller plugins are both enabled.
Pretending to be surprised, but adding MIDI key events and properties to MIDio
is probably easier than coexistence,
which might require reentrance changes in Melanchall.DryWetMidi and sharing a MIDI device.

Finding available Events was unobvious to me.
  • Clicking New mapping from Controls wants only immediate inputs, which may be unobvious to generate.
  • Clicking New mapping from Events shows available predefined events and actions;
          automagically creating events for non-slider ControlChanges may be easier...
  • Events can probably only be launched from MIDIio class, unless this gets passed...
    24 Jan: Active button Control Change (CC) Settings bitmap for properties and events saved/restored
    Plugin properties seemingly must be set without indexing variables.
    24 Jan: DryWetMIDI output MIDI device
    Basically the same tactic as reading:  try opening the property name,
    then catch exception by logging known devices.
    Trick:  to avoid upsetting VS, copy e.g. MIDIdrywet.cs to a .txt file, edit to change class name, etc. then rename.
  • OUTdrywet.cs was created this way...

  • Serial Ports, for use in Fake8
    11 Mar 2023: Serial Ports
    Any number of issues, largely thanks to:
    • terrible examples;  best of a poor lot:
      public class PortChat
      {
          static bool _continue;
          static SerialPort _serialPort;
      
      Note: use of static for both SerialPort and bool:
      - SerialPort will use a delegate - delegate works with static
      public static void Read()
      {
          while (_continue)
          {
              try
              {
                  string message = _serialPort.ReadLine();
                  Console.WriteLine(message);
              }
              catch (TimeoutException) { }
          }
      }
      ... where Read() runs in another thread, and could not access _continue if not static.
      This example launches Read() by Thread readThread = new Thread(Read);
      ... but can instead by launched by SerialDataReceivedEventHandler Delegate:
          public delegate void SerialDataReceivedEventHandler(object sender, SerialDataReceivedEventArgs e);
      
          mySerialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
          mySerialPort.Open();
      }
      
      private static void DataReceivedHandler(
                              object sender,
                              SerialDataReceivedEventArgs e)
      {
          SerialPort sp = (SerialPort)sender;
          string indata = sp.ReadExisting();
          Console.WriteLine("Data Received:");
          Console.Write(indata);
      }
      
      Notes:
      • that lame DataReceivedHandler() example does nothing interesting with received data,
        e.g. returning it to the writing thread for processing.
      • It requires a new thread for each receive event.  Instead, it could:
        private static void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e) 
        {
            SerialPort sp = (SerialPort)sender;
            while(_continue)                         // set false before PortChat instance thread exits
                PCrcv(PC(), sp.ReadExisting());      // pass current instance to main thread static method() delegate
        }
        
        private delegate void PChatDel(PortChat Instance, string text);
        readonly PChatDel PCrcv = PortChatreceiver; 
        
        private PortChat PC() { return this; }      // callback for current PortChat class instance
        
        PortChatreceiver() fails for asynchronous threads, unless e.g. queues are implemented,
        but for plugin applications, SimHub coordinates property value updates among plugins,
        and serial messages received can always be only in response to messages sent,
        with fixed known unique terminating bytes.
    maintained by blekenbleu