Touch Portal Plug-in Development - My Thoughts¶
Part of my streaming setup is a program called StreamHelper (SH). This is a Delphi application that does a bunch of stuff including playing music and running scripts for automation. The original interface between Touch Portal (TP) and SH was a unidirectional HTTP request based system. The net result was limited capabilities and a need to refer to the source code to keep looking up the request paths and for things like requesting a song, a need to lookup request params and then the information to put in them by searching through the music lists.
I was aware from the moment I started using Touch Portal that they had a plug-in API but for some reason I put off making one and I kept putting it off until a couple of days ago. This post is a bit of a retrospective on my efforts and a few comments about the API and how I think it could be improved.
Let me say from the outset, overall I'm pretty impressed with the API and how easy it makes adding functionality to TP. But having used it, I do have some thoughts on how it could be improved.
So what went well?¶
Adding simple actions... this is a breeze. If all your actions have no data associated with them, it's incredibly straightforward adding them. Receiving them is also a breeze since you only need to look at the command Id in the data you receive from TP.
Adding connectors... like simple actions, adding connectors and handling their bi-directional data exchange is dead easy. Updates are throttled to once every 100ms I believe, but I can't think of many instances where you'll be needing a 100ms update time.
Communicating with TP... the TCP based socket communications is straightforward... simple JSON based messages which are for the most part easy to process.
Implementing dynamic list loading in commands... the example I'll use here is the action I implemented for playing a specific song. The first field is Provider (my stream music collection is organised using Provider/Genre/Album/Song), when the user picks a provider a listChange message is sent to you telling you which option the user selected. At this point, I scan the albums for the selected provider to get a list of Genres which are then sent back to provide the next field with the required options. This works great and makes complex selections a breeze. I'm guessing similar functionality exists in things like the embedded OBS interface for scene/source selection etc. It just makes adding actions a breeze.
What didn't go so well?¶
Implementing events... I think this is down to me not understanding/reading the documentation well enough but thus far, I've only been able to implement events with states that are choices. There is an event you can use that detects changes in plug-in states which can largely remove the need to implement your own, but there are some limitations to the event system. It would be great if we could simply say On <somestate> changed, as the built in methods have conditions like changes to. You can emulate a simple has changed by using does not change to and specifying some value that the state will never hit, but this feels a little clunky.
Sending images for button icons... to be clear, this is not an issue with TP as such. The documentation makes certain statements about the format but it is not totally explicit. The biggest issue I had was encoding the image data which absolutely must be a UTF-8 Base 64 encoded representation of the image with no line breaks.
output := TPNGIMage.CreateBlank(COLOR_RGB, 8, 128, 128);
output.Canvas.StretchDraw(rect(0, 0, 128, 128), png);
outStream := TMemoryStream.create;
outStream.position := 0;
b64enc := TBase64Encoding.create(0);
strStream := TStringStream.create('', TEncoding.UTF8);
fArtData := strStream.DataString;
In this code snippet, png is the source image, and fArtData is a string field on the class that stores album data.
Adding states... overall, this is a simple process but unless I'm much mistaken there is a bug in TP that prevents any of your states being available for selection unless you have at least one dynamic state (i.e. created at run-time within your client as opposed to being created by the plug-in file - the primary difference is run-time states can't be used to create events within the plug-in and they have limited data type options).
Receiving data from actions... I'm really curious about why they elected to use this approach, but this is what an action message from TP to your client looks like when there is data present in the action.
Why is this an issue? Because you cannot make assumptions about the order of the items in the data array (or at least you shouldn't since the documentation doesn't state that the data items will be returned in a particular order - if it stated they are returned in the order in which they are defined in the action/connector then you could simply assume indices and make specific direct requests for their values). This makes using this information a real pain in the ass because you have to loop through all the items matching Ids to get the values you want. In my opinion this would have been a better way of presenting (given you only get the Id and the value)...
That single change would mean processing these actions can be quicker because you're not having to loop through a bunch of items you can immediately get a specific value using it's Id. I don't believe it's possible to having multiple data items with the same Id so, given we only get an Id and value, I can see no reason why this approach wasn't used.
For actions with a single data item, sure you can just retrieve Object.data.value, but if you have multiple data items you are forced to loop through them all.
All that being said, it could be there are design considerations I'm not aware of that mean the current approach is the best one.
Frequent restarts needed... to be fair the TP API documentation does make this clear, but during development you'll need to restart TP alot to ensure all the latest plug-in changes are present. The thing is though, unless I'm much mistaken, you have to uninstall the plug-in, restart TP (easy because they have a menu option on the icon in the system tray for this), re-import the plug-in and then enable it again in the settings page. Pain in the ass, but kind of understandable. What could help is the option to enable some plug-in developer options within TP... Reload plug-in for example
Descriptions on tryInline actions... If you put a description on an action that is tryInline, this will be displayed above the actual action in the visual editor resulting in actions that are twice as high as they need to be. At the same time, the prefix you allocate won't be displayed on these actions, so you're in this kind of dammed if you do, dammed if you don't scenario because these actions will not be directly identified as belonging to your plug-in within the visual script editor. Not a huge deal per sé, but unless you make your actions stand out (mine are a fairly vibrant red), they could get lost in a sea of actions. It's worth pointing out that if you remove the description, any actions you added before removing it will retain it so you have to manually recreate them.
Actions with data that are not tryInline... Thus far, I've not been able to make these work. Sure I can get the dialog to appear with the various labels and data fields, but clicking Add did nothing. There were no errors in the logs, no messages, just nothing. The only option I had was to cancel the dialog.
During a TP restart, the socket seems to be left hanging... What I mean by this is that TP doesn't appear to close the socket when it's going for a restart. You get the closePlugin message but the connection stays open and the socket server stays active, so on my first attempt at handling the closePlugin, I disconnected, started my reconnect timer (with a 1 second interval) and found myself re-connected and paired with TP which then promptly closed and left the socket hanging. This could be handled better... if you know you're restarting, shut off the socket server after sending the closePlugin message, or stop connections pairing, maybe include the fact you're restarting in the response to the pairing message. Just to be clear, this is more an observation than a complaint. I tweaked my handlers a little and closed the socket on receiving the closePlugin message and now it all works fine. The reality is the communications seem really stable and pretty dam performant.
Connector Granularity... Essentially you're limited to integer values in the range 0 to 100, essentially a percentage. It would be nice to have the option of picking a more granular range... e.g. 0 to 1000 which would equate to 0 to 100 in 0.1 increments. Personally I feel the sliders are too small if they occupy a single button (depending on the grid layout you have), but when they are bigger, they need a more granular value for smooth movement.
Now I've actually implemented the interface and I've seen how easy it is, I'm kicking myself for not doing it sooner. Overall the API documentation is pretty good but it does lack clarity in some places.
I was disappointed with the eventing side of things, but I was able to work around it and as I've already stated, it could simply be me not understanding things properly.
It would be great if you could perhaps dynamically configure action data items and I'll illustrate this with an example. One of the actions I implemented allows me to trigger scripts within StreamHelper. The original system used HTTP requests and the data for the script was sent as request parameters, so I might make a request like this:-
Clearly this is a pain in the ass. Now I have an action that includes a data item containing the names of the available scripts, but the data is still packed up as HTTP URL query parameters as illustrated above. It would be great if when I selected the script, I could send TP a list of the data items I needed (this could be as simple as a field name, or you could get more complex and specify different types like you do for regular data items).
Overall though I'm pleased with the functionality I've been able to put together. The uni-directional HTTP interface has been completely replaced with the plug-in and whilst it was a pain in the ass going through all my pages, events and flows sorting it all out, I've been able to streamline some things in the set up and make some things work how they should.
Now I understand how easy it is to create plug-ins for TP, I can safely say I'm going to be making more use of it with my applications... why wouldn't you when it's so easy. I would encourage anyone who creates desktop applications to consider whether TP could be used to make some aspects of your application easier to use. Especially as you can create pages and package them as well.
The Touch Portal gang most definitely deserve some thanks... the API is great and should be more than capable for most applications. The application itself is also pretty good and well worth the money.
Update - Since creating my original interface I have created a generic interface that allows you to implement your own bespoke interfaces by defining handler methods and annotating them with attributes (Available on GitHub at https://github.com/AthenaOfDelphi/basetouchportalinterface, you can read about using the interface here).