25 Commits

Author SHA1 Message Date
jwetzell ceb18307f6 Update min and target android version 2023-11-25 23:31:47 -06:00
jwetzell 2f5ce281d3 cleanup logging 2023-11-25 23:26:07 -06:00
jwetzell 7b6ed09ced rework TCPClient yet again to try to nail down this SLIP framing problem 2023-11-25 22:46:06 -06:00
jwetzell 486af45d53 Make SlipFrame constants public 2023-11-25 22:45:10 -06:00
jwetzell e5d09538ab add in status/data and connection event 2022-11-08 15:41:27 -06:00
jwetzell 1a0ccc8763 Update publish.yml 2022-11-05 15:03:51 -05:00
jwetzell fce3eab8d7 bump version number 2022-11-05 14:58:58 -05:00
jwetzell 2176ca4488 TCP Work, Log cleanup, Message parsing 2022-11-05 13:50:27 -05:00
jwetzell 57ca80df77 Fix QLab Message parsing for reply formats 2022-11-05 13:23:14 -05:00
jwetzell 974d14156f Update dependencies 2022-10-11 18:00:32 -05:00
jwetzell 92a44d95d4 Create LICENSE 2022-07-25 10:06:15 -05:00
jwetzell 498d2a41f9 Update README.md 2022-07-23 13:56:56 -05:00
jwetzell 730e7bc554 Update dependencies 2022-07-23 13:10:25 -05:00
jwetzell 1eb8e0c42b Cue properties clean up 2022-07-23 09:57:47 -05:00
jwetzell 69b8210362 switch to simpletcp library 2022-07-23 09:57:33 -05:00
jwetzell 1deca3e7f3 Change Log Levels 2022-07-23 09:57:06 -05:00
jwetzell ff498f83d0 * SlipFrame.cs:
* TCPClient.cs: Split SLIP frame stuff to separate class

* QControlKit.csproj: Bump version number
2022-02-17 17:45:59 -06:00
jwetzell 19794f40a7 * App.xaml:
* App.xaml.cs:
* MainPage.xaml:
* Info.plist:
* MainPage.xaml.cs:
* AppDelegate.cs:
* CueListPage.xaml.cs:
* QControlKitXamDemo.csproj:
* QCueViewModel.cs:
* QServerViewModel.cs:
* QBrowserViewModel.cs:
* QWorkspaceViewModel.cs:
* QControlKitXamDemo.iOS.csproj:
* QControlKitXamDemo.Android.csproj:
* materialdesignicons-webfont.ttf:
* materialdesignicons-webfont.ttf: Update demo to look a bit closer to
  real

* QClient.cs:
* QBrowser.cs: Change log levels

* QControlKit.csproj: Update Package Version

* QCue.cs: decent fix for bool property fetching

* QMessage.cs: add ToString()

* QServer.cs: Add Logging

* TCPClient.cs: Big Restructure using SuperSimpleTcp and SlipDecode
  GAH!

* QWorkspace.cs: Add ability to address active cues
2022-02-17 17:27:05 -06:00
jwetzell 6057105f44 * QCue.cs:
* QWorkspace.cs:
* QControlKitXamDemo.csproj:
* QControlKitXamDemo.iOS.csproj:
* QControlKitXamDemo.Android.csproj: Update Dependencies

* QTextCue.cs: Add QTextCue Class
2022-02-03 13:45:19 -06:00
jwetzell 66ae48cf33 * QMessage.cs:
* QWorkspace.cs:
* QEventHandlers.cs: Cleanup

* QControlKit.csproj:
* QControlKitXamDemo.csproj:
* QControlKitXamDemo.iOS.csproj:
* QControlKitXamDemo.Android.csproj: Update packages
2021-12-28 10:18:58 -06:00
jwetzell 9601d59d81 * QBrowser.cs: Change log levels
* QClient.cs: Change to Queue for TCP sending messages

* QControlKit.csproj:
* Info.plist:
* MainPage.xaml.cs:
* QControlKitXamDemo.csproj:
* QControlKitXamDemo.iOS.csproj:
* QControlKitXamDemo.Android.csproj: Updates

* TCPClient.cs: Start work on send queue for TCP
2021-11-18 00:42:31 -06:00
jwetzell 406bcc9e7f Update publish.yml 2021-09-05 11:32:25 -05:00
jwetzell 015913b2a1 Update version number 2021-09-05 11:28:43 -05:00
jwetzell 6f20d7ba19 * QControlKit.csproj: Update Packages
* QBrowser.cs: Fix zeroconf server loss detection
2021-09-04 10:41:49 -05:00
jwetzell 6dd0f766e8 Add Workspace Event Handlers 2021-07-28 09:07:55 -05:00
35 changed files with 1354 additions and 600 deletions
+4 -4
View File
@@ -1,8 +1,8 @@
name: Publish to NuGet name: Publish to NuGet
on: on:
release: push:
types: branches:
- created # When a release is made - master # Default release branch
jobs: jobs:
publish: publish:
name: build, pack & publish name: build, pack & publish
@@ -18,7 +18,7 @@ jobs:
- name: Publish NuGet - name: Publish NuGet
# You may pin to the exact commit or the version. # You may pin to the exact commit or the version.
# uses: brandedoutcast/publish-nuget@c12b8546b67672ee38ac87bea491ac94a587f7cc # uses: brandedoutcast/publish-nuget@c12b8546b67672ee38ac87bea491ac94a587f7cc
uses: brandedoutcast/publish-nuget@v2.5.5 uses: alirezanet/publish-nuget@v3.0.4
with: with:
# Filepath of the project to be packaged, relative to root of repository # Filepath of the project to be packaged, relative to root of repository
PROJECT_FILE_PATH: QControlKit/QControlKit.csproj PROJECT_FILE_PATH: QControlKit/QControlKit.csproj
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Joel Wetzell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+11
View File
@@ -289,6 +289,17 @@
public const string Error = "error"; public const string Error = "error";
} }
public static class QConnectionRole
{
public const string View = "view";
public const string ViewEdit = "view|edit";
public const string ViewEditControl = "view|edit|control";
public const string ViewControl = "view|control";
public const string EditControl = "edit|control";
public const string Edit = "edit";
public const string Control = "control";
}
public static class QColorName public static class QColorName
{ {
public const string Red = "red"; public const string Red = "red";
+69
View File
@@ -0,0 +1,69 @@
using System.Collections.Generic;
using QControlKit.Constants;
namespace QControlKit.Cue
{
public class QTextCue : QCue
{
public int fixedWidth
{
get
{
return (int)propertyForKey(QOSCKey.FixedWidth);
}
set
{
if(fixedWidth > 0)
{
setProperty(value, QOSCKey.FixedWidth);
}
}
}
public string text
{
get
{
return propertyForKey(QOSCKey.Text).ToString();
}
set
{
setProperty(value, QOSCKey.Text);
}
}
public string liveText
{
get
{
return propertyForKey(QOSCKey.LiveText).ToString();
}
set
{
setProperty(value, QOSCKey.LiveText);
}
}
//TODO:
/* https://qlab.app/docs/v4/scripting/osc-dictionary-v4/#text-cue-methods
* /cue/{cue_number}/text/format {json_string}
* /cue/{cue_number}/text/format/alignment {alignment}
* /cue/{cue_number}/text/format/fontFamily
* /cue/{cue_number}/text/format/fontStyle
* /cue/{cue_number}/text/format/fontFamilyAndStyle {family} {style}
* /cue/{cue_number}/text/format/fontName {name}
* /cue/{cue_number}/text/format/fontSize {number}
* /cue/{cue_number}/text/format/lineSpacing {number}
* /cue/{cue_number}/text/format/color {red} {green} {blue} {alpha}
* /cue/{cue_number}/text/format/backgroundColor {red} {green} {blue} {alpha}
* /cue/{cue_number}/text/format/strikethroughColor {red} {green} {blue} {alpha}
* /cue/{cue_number}/text/format/underlineColor {red} {green} {blue} {alpha}
* /cue/{cue_number}/text/format/strikethroughStyle {style}
* /cue/{cue_number}/text/format/underlineStyle {style}
* /cue/{cue_number}/text/outputSize
* /cue/{cue_number}/liveText/outputSize
*/
}
}
+3 -1
View File
@@ -48,7 +48,8 @@ namespace QControlKit.Events
public class QWorkspaceConnectedArgs : EventArgs public class QWorkspaceConnectedArgs : EventArgs
{ {
public string status { get; set; }
public string data { get; set; }
} }
public class QWorkspaceDisconnectedArgs : EventArgs public class QWorkspaceDisconnectedArgs : EventArgs
@@ -59,6 +60,7 @@ namespace QControlKit.Events
public class QWorkspaceConnectionErrorArgs : EventArgs public class QWorkspaceConnectionErrorArgs : EventArgs
{ {
public string status { get; set; } public string status { get; set; }
public string data { get; set; }
} }
public class QQLabPreferencesUpdatedArgs : EventArgs public class QQLabPreferencesUpdatedArgs : EventArgs
{ {
+3 -2
View File
@@ -1,4 +1,6 @@
namespace QControlKit.Events using System;
namespace QControlKit.Events
{ {
public delegate void QServerFoundHandler(object source, QServerFoundArgs args); public delegate void QServerFoundHandler(object source, QServerFoundArgs args);
public delegate void QServerLostHandler(object source, QServerLostArgs args); public delegate void QServerLostHandler(object source, QServerLostArgs args);
@@ -16,7 +18,6 @@
public delegate void QWorkspaceConnectionErrorHandler(object source, QWorkspaceConnectionErrorArgs args); public delegate void QWorkspaceConnectionErrorHandler(object source, QWorkspaceConnectionErrorArgs args);
public delegate void QQLabPreferencesUpdatedHandler(object source, QQLabPreferencesUpdatedArgs args); public delegate void QQLabPreferencesUpdatedHandler(object source, QQLabPreferencesUpdatedArgs args);
public delegate void QCueUpdatedHandler(object source, QCueUpdatedArgs args); public delegate void QCueUpdatedHandler(object source, QCueUpdatedArgs args);
public delegate void QCuePropertiesUpdatedHandler(object source, QCuePropertiesUpdatedArgs args); public delegate void QCuePropertiesUpdatedHandler(object source, QCuePropertiesUpdatedArgs args);
public delegate void QCueListsUpdatesHandler(object source, QCueListsUpdatedArgs args); public delegate void QCueListsUpdatesHandler(object source, QCueListsUpdatedArgs args);
+13 -12
View File
@@ -13,6 +13,8 @@ namespace QControlKit
{ {
public class QBrowser public class QBrowser
{ {
private ILogger _log = Log.Logger.ForContext<QBrowser>();
public ObservableCollection<QServer> servers = new ObservableCollection<QServer>(); public ObservableCollection<QServer> servers = new ObservableCollection<QServer>();
public event QServerFoundHandler ServerFound; public event QServerFoundHandler ServerFound;
@@ -26,11 +28,14 @@ namespace QControlKit
public async void ProbeForQLabInstances() public async void ProbeForQLabInstances()
{ {
_log.Debug("probing for instances");
IReadOnlyList<IZeroconfHost> results = await IReadOnlyList<IZeroconfHost> results = await
ZeroconfResolver.ResolveAsync(QBonjour.TCPService,TimeSpan.FromSeconds(3)); ZeroconfResolver.ResolveAsync(QBonjour.TCPService,TimeSpan.FromSeconds(3));
foreach (var zeroconfHost in results) foreach (var zeroconfHost in results)
{ {
_log.Debug($"found host {zeroconfHost.IPAddress}:{zeroconfHost.DisplayName}");
foreach (var service in zeroconfHost.Services) foreach (var service in zeroconfHost.Services)
{ {
if (service.Key.Equals(QBonjour.TCPService)) if (service.Key.Equals(QBonjour.TCPService))
@@ -40,7 +45,7 @@ namespace QControlKit
if (server == null) if (server == null)
{ {
Log.Information($"[qbrowser] Found {zeroconfHost.DisplayName} : {zeroconfHost.IPAddress} : {service.Value.Port}"); _log.Information($"Found {zeroconfHost.DisplayName} : {zeroconfHost.IPAddress} : {service.Value.Port}");
QServer serverToAdd = new QServer(zeroconfHost.IPAddress, service.Value.Port); QServer serverToAdd = new QServer(zeroconfHost.IPAddress, service.Value.Port);
serverToAdd.name = zeroconfHost.DisplayName; serverToAdd.name = zeroconfHost.DisplayName;
serverToAdd.zeroconfHost = zeroconfHost; serverToAdd.zeroconfHost = zeroconfHost;
@@ -62,19 +67,14 @@ namespace QControlKit
foreach(var server in qServers) foreach(var server in qServers)
{ {
bool serverToBeRemoved = true; IZeroconfHost found = results.ToList().Find(x => x.IPAddress.Equals(server.host));
foreach(var zeroconfHost in results){
QServer existingServer = serverForAddress(zeroconfHost.IPAddress); if (found == null)
if(server != null)
{
serverToBeRemoved = false;
}
}
if (serverToBeRemoved)
{ {
_log.Information($"Lost {server.name} : {server.host} : {server.port} disconnecting");
server.disconnect(); server.disconnect();
_log.Verbose($"after server disconnect()");
servers.Remove(server); servers.Remove(server);
Log.Information($"[qbrowser] Lost {server.name} : {server.host} : {server.port}");
OnServerLost(server); OnServerLost(server);
} }
} }
@@ -123,7 +123,8 @@ namespace QControlKit
public void Close() public void Close()
{ {
foreach(var server in servers) _log.Information($"Close requested");
foreach (var server in servers)
{ {
server.disconnect(); server.disconnect();
} }
+31 -32
View File
@@ -12,6 +12,8 @@ namespace QControlKit
{ {
public class QClient public class QClient
{ {
private ILogger _log = Log.Logger.ForContext<QClient>();
TCPClient tcpClient; TCPClient tcpClient;
public event QWorkspacesUpdatedHandler WorkspacesUpdated; public event QWorkspacesUpdatedHandler WorkspacesUpdated;
@@ -35,7 +37,7 @@ namespace QControlKit
public QClient(string host, int port) public QClient(string host, int port)
{ {
tcpClient = new TCPClient(host, port); tcpClient = new TCPClient(host, port);
Log.Debug($"[client] setup connection to: <{host}:{port}>"); _log.Debug($"setup connection to: <{host}:{port}>");
tcpClient.MessageReceived += ProcessMessage; tcpClient.MessageReceived += ProcessMessage;
} }
@@ -52,7 +54,7 @@ namespace QControlKit
public void disconnect() public void disconnect()
{ {
Log.Debug($"[client] disconnecting from {tcpClient.Address}"); _log.Information($"disconnecting from {tcpClient.Address}");
tcpClient.Close(); tcpClient.Close();
} }
@@ -60,7 +62,6 @@ namespace QControlKit
public void sendMessage(string address, params object[] args) public void sendMessage(string address, params object[] args)
{ {
tcpClient.Send(new OscMessage(address, args)); tcpClient.Send(new OscMessage(address, args));
Log.Debug($"[client] send message {address} : {args}");
} }
private void ProcessMessage(object source, MessageEventArgs args) private void ProcessMessage(object source, MessageEventArgs args)
@@ -70,7 +71,7 @@ namespace QControlKit
if (message.IsReply) if (message.IsReply)
{ {
JToken data = message.response; JToken data = message.data;
//special case, want to update cue properties //special case, want to update cue properties
if (message.IsReplyFromCue) if (message.IsReplyFromCue)
@@ -84,26 +85,25 @@ namespace QControlKit
string property = message.AddressParts.Last(); string property = message.AddressParts.Last();
if (property == null) if (property == null)
return; return;
if (property == QOSCKey.PlaybackPositionId)
{
OnCueListChangedPlaybackPosition(message.cueID, data.ToString());
return;
}
//create object manually since single value replies don't have dictionaries //create object manually since single value replies don't have dictionaries
JObject properties = new JObject(); JObject properties = new JObject();
properties.Add(property, data); properties.Add(property, data);
OnCueUpdated(message.cueID, properties); OnCueUpdated(message.cueID, properties);
if (property == QOSCKey.PlaybackPositionId)
{
OnCueListChangedPlaybackPosition(message.cueID, data.ToString());
}
} }
else else
{ {
Log.Warning($"[client] unhandled reply from cue: Type: {data.Type} value: {message.response}"); _log.Error($"unhandled reply from cue: Type: {data.Type} value: {message.data}");
} }
} }
else if (message.IsReplyFromCueLists) else if (message.IsReplyFromCueLists)
{ {
OnCueListsUpdated(message.response); OnCueListsUpdated(message.data);
} }
else if (message.IsWorkspacesInfo) else if (message.IsWorkspacesInfo)
{ {
@@ -111,14 +111,14 @@ namespace QControlKit
} }
else if (message.IsConnect) else if (message.IsConnect)
{ {
if (message.response.ToString() == "ok") if (message.status.ToString().Equals("ok"))
OnWorkspaceConnected(); OnWorkspaceConnected(message.status.ToString(),message.data.ToString());
else else
OnWorkspaceConnectionError(message.response.ToString()); OnWorkspaceConnectionError(message.status.ToString(), message.data.ToString());
} }
else else
{ {
Log.Warning($"[client] unhandled reply message: {message.address}"); _log.Error($"unhandled reply message: {message.address}");
} }
} }
else if(message.IsUpdate) { else if(message.IsUpdate) {
@@ -150,7 +150,6 @@ namespace QControlKit
else if ( message.IsPreferencesUpdate) else if ( message.IsPreferencesUpdate)
{ {
//need to do checks for 4.2 or newer //need to do checks for 4.2 or newer
string key = message.AddressParts.Last(); string key = message.AddressParts.Last();
if (key == null) if (key == null)
return; return;
@@ -162,76 +161,76 @@ namespace QControlKit
} }
else else
{ {
Log.Warning($"[client] unhandled update message: {message.address}"); _log.Error($"unhandled update message: {message.address}");
} }
} }
else else
{ {
Log.Warning($"[client] unhandled message: {message.address}"); _log.Error($"unhandled message: {message.address}");
} }
} }
protected virtual void OnCueUpdated(string cueID, JToken properties) protected virtual void OnCueUpdated(string cueID, JToken properties)
{ {
Log.Debug($"[client] cue updated: {cueID}"); _log.Debug($"cue updated: {cueID}");
CueUpdated?.Invoke(this, new QCueUpdatedArgs { cueID = cueID, data = properties }); CueUpdated?.Invoke(this, new QCueUpdatedArgs { cueID = cueID, data = properties });
} }
protected virtual void OnCueNeedsUpdated(string cueID) protected virtual void OnCueNeedsUpdated(string cueID)
{ {
Log.Debug($"[client] cue needs updated: {cueID}"); _log.Debug($"cue needs updated: {cueID}");
CueNeedsUpdated?.Invoke(this, new QCueNeedsUpdatedArgs { cueID = cueID }); CueNeedsUpdated?.Invoke(this, new QCueNeedsUpdatedArgs { cueID = cueID });
} }
protected virtual void OnCueListsUpdated(JToken response) protected virtual void OnCueListsUpdated(JToken response)
{ {
Log.Debug($"[client] Cue Lists Updated"); _log.Debug("Cue Lists Updated");
CueListsUpdated?.Invoke(this, new QCueListsUpdatedArgs { data = response }); CueListsUpdated?.Invoke(this, new QCueListsUpdatedArgs { data = response });
} }
protected virtual void OnCueListChangedPlaybackPosition(string cueListID, string cueID) protected virtual void OnCueListChangedPlaybackPosition(string cueListID, string cueID)
{ {
Log.Debug($"[client] CueList <{cueListID}> Playback Position Changed to <{cueID}>"); _log.Debug($"CueList <{cueListID}> Playback Position Changed to <{cueID}>");
CueListChangedPlaybackPosition?.Invoke(this, new QCueListChangedPlaybackPositionArgs { cueListID = cueListID, cueID = cueID }); CueListChangedPlaybackPosition?.Invoke(this, new QCueListChangedPlaybackPositionArgs { cueListID = cueListID, cueID = cueID });
} }
protected virtual void OnWorkspaceUpdated() protected virtual void OnWorkspaceUpdated()
{ {
//Log.Debug($"[client] Workspace Updated"); _log.Debug($"Workspace Updated");
WorkspaceUpdated?.Invoke(this, new QWorkspaceUpdatedArgs()); WorkspaceUpdated?.Invoke(this, new QWorkspaceUpdatedArgs());
} }
protected virtual void OnWorkspaceSettingsUpdated(string settingsType) protected virtual void OnWorkspaceSettingsUpdated(string settingsType)
{ {
Log.Debug($"[client] Workspace Settings Updated"); _log.Debug($"Workspace Settings Updated");
WorkspaceSettingsUpdated?.Invoke(this, new QWorkspaceSettingsUpdatedArgs { settingsType = settingsType }); WorkspaceSettingsUpdated?.Invoke(this, new QWorkspaceSettingsUpdatedArgs { settingsType = settingsType });
} }
protected virtual void OnWorkspaceLightDashboardUpdated() protected virtual void OnWorkspaceLightDashboardUpdated()
{ {
Log.Debug($"[client] Workspace Light Dashboard Updated"); _log.Debug("Workspace Light Dashboard Updated");
WorkspaceLightDashboardUpdated?.Invoke(this, new QWorkspaceLightDashboardUpdatedArgs()); WorkspaceLightDashboardUpdated?.Invoke(this, new QWorkspaceLightDashboardUpdatedArgs());
} }
protected virtual void OnQLabPreferencesUpdated(string key) protected virtual void OnQLabPreferencesUpdated(string key)
{ {
Log.Debug($"[client] QLab Preferences Updated"); _log.Debug("QLab Preferences Updated");
QLabPreferencesUpdated?.Invoke(this, new QQLabPreferencesUpdatedArgs { key = key }); QLabPreferencesUpdated?.Invoke(this, new QQLabPreferencesUpdatedArgs { key = key });
} }
protected virtual void OnWorkspaceDisconnected() protected virtual void OnWorkspaceDisconnected()
{ {
Log.Debug($"[client] Workspace Disconnected"); _log.Debug("Workspace Disconnected");
WorkspaceDisconnected?.Invoke(this, new QWorkspaceDisconnectedArgs()); WorkspaceDisconnected?.Invoke(this, new QWorkspaceDisconnectedArgs());
} }
protected virtual void OnWorkspaceConnected() protected virtual void OnWorkspaceConnected(string status, string data)
{ {
WorkspaceConnected?.Invoke(this, new QWorkspaceConnectedArgs()); WorkspaceConnected?.Invoke(this, new QWorkspaceConnectedArgs { status = status, data = data });
} }
protected virtual void OnWorkspaceConnectionError(string status) protected virtual void OnWorkspaceConnectionError(string status, string data)
{ {
WorkspaceConnectionError?.Invoke(this, new QWorkspaceConnectionErrorArgs { status = status }); WorkspaceConnectionError?.Invoke(this, new QWorkspaceConnectionErrorArgs { status = status, data = data });
} }
protected virtual void OnWorkspacesUpdated(QMessage message) protected virtual void OnWorkspacesUpdated(QMessage message)
@@ -239,7 +238,7 @@ namespace QControlKit
if(WorkspacesUpdated != null) if(WorkspacesUpdated != null)
{ {
List<QWorkspaceInfo> workspaces = new List<QWorkspaceInfo>(); List<QWorkspaceInfo> workspaces = new List<QWorkspaceInfo>();
foreach (var item in message.response) foreach (var item in message.data)
{ {
QWorkspaceInfo workspacefound = JsonConvert.DeserializeObject<QWorkspaceInfo>(item.ToString()); QWorkspaceInfo workspacefound = JsonConvert.DeserializeObject<QWorkspaceInfo>(item.ToString());
workspaces.Add(workspacefound); workspaces.Add(workspacefound);
+12 -4
View File
@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild> <GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<Authors>Joel Wetzell</Authors> <Authors>Joel Wetzell</Authors>
<Version>1.0.0-devel</Version> <Version>1.0.0-devel</Version>
<PackOnBuild>true</PackOnBuild> <PackOnBuild>true</PackOnBuild>
<PackageVersion>0.0.19</PackageVersion> <PackageVersion>0.0.33</PackageVersion>
<PackageId>QControlKit</PackageId> <PackageId>QControlKit</PackageId>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<Summary>Unofficial C# port of Figure53's QLabKit.objc</Summary> <Summary>Unofficial C# port of Figure53's QLabKit.objc</Summary>
@@ -15,15 +15,23 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog" Version="2.10.0" /> <PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="Zeroconf" Version="3.4.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="TcpSharp" Version="1.2.0" />
<PackageReference Include="Zeroconf" Version="3.5.11" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Constants\" /> <Folder Include="Constants\" />
<Folder Include="Cue\" /> <Folder Include="Cue\" />
<Folder Include="SharpOSC\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Cue\" /> <None Remove="Cue\" />
<None Remove="SharpOSC" />
<None Remove="Zeroconf" />
<None Remove="SharpOSC\" />
<None Remove="SimpleTCP" />
<None Remove="SuperSimpleTcp" />
<None Remove="WatsonTcp" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+142 -84
View File
@@ -12,12 +12,14 @@ namespace QControlKit
{ {
public class QCue : IComparable public class QCue : IComparable
{ {
private ILogger _log = Log.Logger.ForContext<QCue>();
public QWorkspace workspace; public QWorkspace workspace;
public Dictionary<string, object> cueData; public Dictionary<string, object> cueData;
public List<QCue> childCues; public List<QCue> childCues;
public Dictionary<string, QCue> childCuesUIDMap; public Dictionary<string, QCue> childCuesUIDMap;
public int sortIndex; public int sortIndex;
public int nestLevel; public int nestLevel;
@@ -43,31 +45,6 @@ namespace QControlKit
{ {
init(); init();
this.workspace = workspace; this.workspace = workspace;
//this breaks things???
//it definitely is in the original but somehow the recursion is not working as I expect..
//but cue population still works without this as child cues are populated later
/*JToken children = dict[QOSCKey.Cues];
if(children != null && children.Type == JTokenType.Array)
{
Log.Debug($"[cue] new cue being created with {children.Count()} childCues");
foreach (var aChildDict in children)
{
string uid = (string)aChildDict[QOSCKey.UID];
//Log.Debug($"[cue] childDict with uid: {uid} being processed");
if (uid == null)
continue;
string name = dict[QOSCKey.UID].ToString();
Log.Debug($"[cue] calling new cue from {name}");
//QCue child = new QCue(aChildDict, workspace);
*//*childCues.Add(child);
childCuesUIDMap.Add(uid, child);*//*
}
}*/
updatePropertiesWithDictionary(dict); updatePropertiesWithDictionary(dict);
} }
@@ -76,30 +53,6 @@ namespace QControlKit
init(); init();
this.workspace = workspace; this.workspace = workspace;
this.nestLevel = nestLevel + 1; this.nestLevel = nestLevel + 1;
//this breaks things???
//it definitely is in the original but somehow the recursion is not working as I expect..
//but cue population still works without this as child cues are populated later
/*JToken children = dict[QOSCKey.Cues];
if(children != null && children.Type == JTokenType.Array)
{
Log.Debug($"[cue] new cue being created with {children.Count()} childCues");
foreach (var aChildDict in children)
{
string uid = (string)aChildDict[QOSCKey.UID];
//Log.Debug($"[cue] childDict with uid: {uid} being processed");
if (uid == null)
continue;
string name = dict[QOSCKey.UID].ToString();
Log.Debug($"[cue] calling new cue from {name}");
//QCue child = new QCue(aChildDict, workspace);
*//*childCues.Add(child);
childCuesUIDMap.Add(uid, child);*//*
}
}*/
updatePropertiesWithDictionary(dict); updatePropertiesWithDictionary(dict);
} }
@@ -210,7 +163,6 @@ namespace QControlKit
QCue cue = childCuesUIDMap[aUid]; QCue cue = childCuesUIDMap[aUid];
//Log.Debug($"Removing Child Cue From {listName} : {cue.uid} + {cue.listName}");
childCues.Remove(cue); childCues.Remove(cue);
childCuesUIDMap.Remove(aUid); childCuesUIDMap.Remove(aUid);
} }
@@ -244,7 +196,14 @@ namespace QControlKit
{ {
get get
{ {
return propertyForKey(QOSCKey.Parent).ToString(); if(propertyForKey(QOSCKey.Parent) != null)
{
return propertyForKey(QOSCKey.Parent).ToString();
}
else
{
return "";
}
} }
} }
public string playbackPositionID { public string playbackPositionID {
@@ -301,7 +260,14 @@ namespace QControlKit
{ {
get get
{ {
return propertyForKey(QOSCKey.ListName).ToString(); if(propertyForKey(QOSCKey.ListName) == null)
{
return "";
}
else
{
return propertyForKey(QOSCKey.ListName).ToString();
}
} }
set set
{ {
@@ -353,46 +319,39 @@ namespace QControlKit
if (workspace.connectedToQLab3 || workspace.isOlderThanVersion("4.2.0")) if (workspace.connectedToQLab3 || workspace.isOlderThanVersion("4.2.0"))
return false; return false;
bool overridden = (bool)propertyForKey(QOSCKey.IsOverridden);
if (overridden)
return true;
foreach (var cue in cues) foreach (var cue in cues)
{ {
if (cue.IsOverridden) if (cue.IsOverridden)
return true; return true;
} }
return false;
return getBoolForKey(QOSCKey.IsOverridden);
} }
} }
public bool IsBroken public bool IsBroken
{ {
get get
{ {
if ((bool)propertyForKey(QOSCKey.IsBroken)) foreach (var cue in cues)
return true;
foreach(var cue in cues)
{ {
if (cue.IsBroken) if (cue.IsBroken)
return true; return true;
} }
return false;
return getBoolForKey(QOSCKey.IsBroken);
} }
} }
public bool IsRunning public bool IsRunning
{ {
get get
{ {
bool running = (bool)propertyForKey(QOSCKey.IsRunning); foreach (var cue in cues)
if (running)
return true;
foreach(var cue in cues)
{ {
if (cue.IsRunning) if (cue.IsRunning)
return true; return true;
} }
return false;
return getBoolForKey(QOSCKey.IsRunning);
} }
} }
public bool IsTailingOut public bool IsTailingOut
@@ -402,16 +361,13 @@ namespace QControlKit
if (workspace.connectedToQLab3) if (workspace.connectedToQLab3)
return false; return false;
bool tailingOut = (bool)propertyForKey(QOSCKey.IsTailingOut); foreach (var cue in cues)
if (tailingOut)
return true;
foreach(var cue in cues)
{ {
if (cue.IsTailingOut) if (cue.IsTailingOut)
return true; return true;
} }
return false;
return getBoolForKey(QOSCKey.IsTailingOut);
} }
} }
public bool IsPanicking public bool IsPanicking
@@ -421,16 +377,13 @@ namespace QControlKit
if (workspace.connectedToQLab3) if (workspace.connectedToQLab3)
return false; return false;
bool panicking = (bool)propertyForKey(QOSCKey.IsPanicking);
if (panicking)
return true;
foreach (var cue in cues) foreach (var cue in cues)
{ {
if (cue.IsPanicking) if (cue.IsPanicking)
return true; return true;
} }
return false;
return getBoolForKey(QOSCKey.IsPanicking);
} }
} }
public bool IsGroup public bool IsGroup
@@ -571,6 +524,62 @@ namespace QControlKit
} }
} }
public float actionElapsed
{
get
{
object secs = propertyForKey(QOSCKey.ActionElapsed);
if (secs != null)
{
return float.Parse(secs.ToString());
}
else
{
return 0F;
}
}
}
public QTriggerAction secondTriggerAction
{
get
{
return (QTriggerAction)propertyForKey(QOSCKey.SecondTriggerAction);
}
set
{
setProperty(value, QOSCKey.SecondTriggerAction);
}
}
public bool secondTriggerOnRelease
{
get
{
var val = (int)propertyForKey(QOSCKey.SecondTriggerOnRelease);
if (val == 0)
{
return true;
}
else
{
return false;
}
}
set
{
if (value)
{
setProperty(0, QOSCKey.SecondTriggerOnRelease);
}
else
{
setProperty(1, QOSCKey.SecondTriggerOnRelease);
}
}
}
//TODO add quaternion property and setter //TODO add quaternion property and setter
public Size surfaceSize() { throw new NotImplementedException(); } public Size surfaceSize() { throw new NotImplementedException(); }
public Size cueSize() { throw new NotImplementedException(); } public Size cueSize() { throw new NotImplementedException(); }
@@ -638,14 +647,12 @@ namespace QControlKit
JToken value = obj.Value; JToken value = obj.Value;
if (obj.Key.Equals(QOSCKey.Cues)) if (obj.Key.Equals(QOSCKey.Cues))
{ {
//Log.Debug($"Cues OSC Key found in update message...updating child cues");
if (value.Type != JTokenType.Array) if (value.Type != JTokenType.Array)
continue; continue;
updateChildCuesWithPropertiesArray(value, false); updateChildCuesWithPropertiesArray(value, false);
} }
else if(obj.Key.Equals(QOSCKey.Children) && IsGroup) else if(obj.Key.Equals(QOSCKey.Children) && IsGroup)
{ {
//Log.Debug($"Children OSC Key found in update message for {uid} ...updating child and removing deleted ones.");
if (value.Type != JTokenType.Array) if (value.Type != JTokenType.Array)
continue; continue;
updateChildCuesWithPropertiesArray(value, true); updateChildCuesWithPropertiesArray(value, true);
@@ -659,7 +666,6 @@ namespace QControlKit
{ {
cueUpdated = true; cueUpdated = true;
propertiesUpdated.Add(obj.Key); propertiesUpdated.Add(obj.Key);
//Log.Debug($"[cue] cue property {obj.Key} updated with {obj.Value}");
} }
} }
} }
@@ -788,6 +794,32 @@ namespace QControlKit
} }
return null; return null;
} }
public bool getBoolForKey(string key)
{
if (propertyForKey(key) != null)
{
object prop = propertyForKey(key);
//_log.Warning($"{listName} IsBroken: {propertyForKey(QOSCKey.IsBroken).ToString()} {prop.GetType().ToString()}");
if(prop.GetType() == typeof(Boolean)){
return (bool)prop;
}
else if (prop.GetType() == typeof(JValue))
{
JValue value = (JValue)prop;
return (bool)value.Value;
}
else
{
return false;
}
}
else
{
return false;
}
}
public object propertyForKey(string key) public object propertyForKey(string key)
{ {
//TODO: implerment special key checks color, surfaceName, patchDescription, //TODO: implerment special key checks color, surfaceName, patchDescription,
@@ -858,7 +890,7 @@ namespace QControlKit
} }
else if (key.Equals(QOSCKey.Children)) else if (key.Equals(QOSCKey.Children))
{ {
Log.Debug($"Children Cues property updated for: {this.displayName} : {this.uid}"); _log.Debug($"Children Cues property updated for: {this.displayName} : {this.uid}");
} }
else if(key.Equals(QOSCKey.PlaybackPositionId)) else if(key.Equals(QOSCKey.PlaybackPositionId))
{ {
@@ -936,6 +968,20 @@ namespace QControlKit
public void togglePause() { workspace.togglePauseCue(this); } public void togglePause() { workspace.togglePauseCue(this); }
public void preview() { workspace.previewCue(this); } public void preview() { workspace.previewCue(this); }
public void panic() { workspace.panicCue(this); } public void panic() { workspace.panicCue(this); }
public void playheadNext()
{
if (this.type == QCueType.CueList)
{
workspace.sendMessageForCue(this, "playhead/next");
}
}
public void playheadPrevious()
{
if (this.type == QCueType.CueList)
{
workspace.sendMessageForCue(this, "playhead/previous");
}
}
#endregion #endregion
@@ -943,7 +989,19 @@ namespace QControlKit
#region Event Handling #region Event Handling
void OnCuePropertiesUpdated(List<string> properties) void OnCuePropertiesUpdated(List<string> properties)
{ {
Log.Debug($"[cue] <{nonEmptyName}> properties have been updated."); _log.Verbose("OnCuePropertiesUpdated");
if(parentID != null && parentID != "")
{
QCue parentCue = workspace.cueWithID(parentID);
if(parentCue != null)
{
workspace.fetchBasicPropertiesForCue(workspace.cueWithID(parentID));
}
else
{
_log.Error($"parent cue with id: {parentID} can't be found this could be a problem");
}
}
CuePropertiesUpdated?.Invoke(this, new QCuePropertiesUpdatedArgs { properties = properties }); CuePropertiesUpdated?.Invoke(this, new QCuePropertiesUpdatedArgs { properties = properties });
} }
#endregion #endregion
@@ -959,7 +1017,7 @@ namespace QControlKit
{ {
string indent = new string(' ', level*2); string indent = new string(' ', level*2);
Log.Information($"{indent}\u00b7{displayName} - {uid}"); _log.Information($"{indent}\u00b7{displayName} - {uid}");
if (IsGroup) if (IsGroup)
{ {
level++; level++;
+67 -4
View File
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using SharpOSC; using SharpOSC;
@@ -28,7 +29,32 @@ namespace QControlKit
public bool IsReply { get { return OSCMessage.Address.StartsWith("/reply"); } } public bool IsReply { get { return OSCMessage.Address.StartsWith("/reply"); } }
// /reply/cue_id/1/action // /reply/cue_id/1/action
public bool IsReplyFromCue { get{ return OSCMessage.Address.StartsWith("/reply/cue_id"); } } public bool IsReplyFromCue {
get {
if (!IsReply)
{
return false;
}
//return OSCMessage.Address.StartsWith("/reply/cue_id");
if (AddressParts.Length > 5)
{
// /reply/workspace/*/cue_id/*/*
if (AddressParts[1].Equals("workspace") && AddressParts[3].Equals("cue_id")){
return true;
}
}
if(AddressParts.Length > 2)
{
// /reply/cue_id/*/*
if (AddressParts[1].Equals("cue_id"))
{
return true;
}
}
return false;
}
}
public bool IsReplyFromCueLists { get { return IsReply && OSCMessage.Address.EndsWith("cueLists"); } } public bool IsReplyFromCueLists { get { return IsReply && OSCMessage.Address.EndsWith("cueLists"); } }
public bool IsUpdate { get{ return OSCMessage.Address.StartsWith("/update"); } } public bool IsUpdate { get{ return OSCMessage.Address.StartsWith("/update"); } }
@@ -103,6 +129,7 @@ namespace QControlKit
return (parts.Length == 4 && parts[3].Equals("connect")); return (parts.Length == 4 && parts[3].Equals("connect"));
} }
} }
#endregion #endregion
//host method //host method
@@ -114,12 +141,25 @@ namespace QControlKit
public string[] AddressParts { get { return address.Split(new char[] { '/' },StringSplitOptions.RemoveEmptyEntries); } } public string[] AddressParts { get { return address.Split(new char[] { '/' },StringSplitOptions.RemoveEmptyEntries); } }
public JToken response public JToken data
{ {
get get
{ {
JObject responseObj = JObject.Parse((string)arguments[0]); JObject dataObj = JObject.Parse((string)arguments[0]);
return responseObj["data"]; return dataObj["data"];
}
}
public JToken status
{
get
{
if (arguments != null && arguments.Count > 0)
{
JObject statusObj = JObject.Parse((string)arguments[0]);
return statusObj["status"];
}
return null;
} }
} }
@@ -135,6 +175,13 @@ namespace QControlKit
//TODO: check string cast //TODO: check string cast
return arguments.Count > 0 ? (string)arguments[0] : null; return arguments.Count > 0 ? (string)arguments[0] : null;
} else if (IsReplyFromCue) { } else if (IsReplyFromCue) {
// /reply/workspace/*/cue_id/*/*
if (AddressParts.Length > 5)
{
return AddressParts[4];
}
// /reply/cue_id/*/*
return AddressParts[2]; return AddressParts[2];
} else { } else {
return null; return null;
@@ -142,5 +189,21 @@ namespace QControlKit
} }
} }
override
public string ToString()
{
string stringified = "";
stringified += $"\nAddress: {this.address}\nArguments: \n";
for(int i = 0; i<this.arguments.Count; i++)
{
object argument = this.arguments[i];
stringified += $"\targ {i + 1}: {argument.ToString()}\n";
}
return stringified;
}
} }
} }
+11 -5
View File
@@ -10,11 +10,13 @@ namespace QControlKit
{ {
public class QServer public class QServer
{ {
private ILogger _log = Log.Logger.ForContext<QServer>();
public event QServerUpdatedHandler ServerUpdated; public event QServerUpdatedHandler ServerUpdated;
public event QServerWorkspaceAddedHandler WorkspaceAdded; public event QServerWorkspaceAddedHandler WorkspaceAdded;
public event QServerWorkspaceRemovedHandler WorkspaceRemoved; public event QServerWorkspaceRemovedHandler WorkspaceRemoved;
private QClient client; private QClient _client;
public string host { get; set; } public string host { get; set; }
public int port { get; set; } public int port { get; set; }
@@ -28,18 +30,21 @@ namespace QControlKit
this.host = host; this.host = host;
this.port = port; this.port = port;
client = new QClient(host, port); _client = new QClient(host, port);
client.WorkspacesUpdated += OnServerWorkspacesUpdated; client.WorkspacesUpdated += OnServerWorkspacesUpdated;
if (!client.connect()) if (!client.connect())
{ {
Log.Error($"[server] unable to connect to QLab Server: {host}:{port}"); _log.Error($"unable to connect to QLab Server: {host}:{port}");
} }
} }
public string description { get { return $"{name} - {host} - {port}"; } } public string description { get { return $"{name} - {host} - {port}"; } }
public QClient client {
get { return _client; }
}
public void refreshWorkspaces() public void refreshWorkspaces()
{ {
client.sendMessage("/workspaces"); client.sendMessage("/workspaces");
@@ -111,11 +116,12 @@ namespace QControlKit
public void disconnect() public void disconnect()
{ {
_log.Information($"disconnect requested for server <{name}>");
foreach (var workspace in workspaces) foreach (var workspace in workspaces)
{ {
if (workspace.connected) if (workspace.connected)
{ {
Log.Debug($"[server] Closing workspace <{workspace.name}> still connected to {name}"); _log.Debug($"Closing workspace <{workspace.name}> still connected to {name}");
workspace.disconnect(); workspace.disconnect();
} }
} }
+77 -69
View File
@@ -3,8 +3,6 @@ using Serilog;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Timers;
using QControlKit.Events; using QControlKit.Events;
using QControlKit.Constants; using QControlKit.Constants;
@@ -12,6 +10,7 @@ namespace QControlKit
{ {
public class QWorkspace: IEquatable<QWorkspace> public class QWorkspace: IEquatable<QWorkspace>
{ {
private ILogger _log = Log.Logger.ForContext<QWorkspace>();
private QServer server; private QServer server;
private QClient client; private QClient client;
@@ -24,9 +23,6 @@ namespace QControlKit
private QCue root; private QCue root;
private Timer heartbeatTimer;
private int heartbeatAttempts;
public bool defaultSendUpdatesOSC; public bool defaultSendUpdatesOSC;
public string version; public string version;
@@ -35,6 +31,8 @@ namespace QControlKit
public event QWorkspaceUpdatedHandler WorkspaceUpdated; public event QWorkspaceUpdatedHandler WorkspaceUpdated;
public event QCueListChangedPlaybackPositionHandler CueListChangedPlaybackPosition; public event QCueListChangedPlaybackPositionHandler CueListChangedPlaybackPosition;
public event QWorkspaceConnectionErrorHandler WorkspaceConnectionError; public event QWorkspaceConnectionErrorHandler WorkspaceConnectionError;
public event QWorkspaceDisconnectedHandler WorkspaceDisconnected;
public event QWorkspaceConnectedHandler WorkspaceConnected;
private void Init() private void Init()
{ {
@@ -67,7 +65,7 @@ namespace QControlKit
updateWithWorkspaceInfo(workspaceInfo); updateWithWorkspaceInfo(workspaceInfo);
client = new QClient(server.host, server.port); client = server.client;
client.WorkspaceConnected += OnWorkspaceConnected; client.WorkspaceConnected += OnWorkspaceConnected;
client.WorkspaceConnectionError += OnWorkspaceConnectionError; client.WorkspaceConnectionError += OnWorkspaceConnectionError;
@@ -78,7 +76,7 @@ namespace QControlKit
client.CueUpdated += OnCueUpdated; client.CueUpdated += OnCueUpdated;
this.server = server; this.server = server;
Log.Debug($"[workspace] <{name}> on <{server.name}> initialized."); _log.Debug($"<{name}> on <{server.name}> initialized.");
} }
//updateWithDictionary //updateWithDictionary
@@ -162,24 +160,26 @@ namespace QControlKit
public string[] versionParts { get { return version.Split('.'); } } public string[] versionParts { get { return version.Split('.'); } }
public bool connectedToQLab3 { get { return versionParts[0] == "3"; } } public bool connectedToQLab3 { get { return versionParts[0] == "3"; } }
public bool connectedToQLab4 { get { return versionParts[0] == "4"; } }
public bool connectedToQLab5 { get { return versionParts[0] == "5"; } }
#region Connection/reconnection #region Connection/reconnection
public void connect(string passcode = null) public void connect(string passcode = null)
{ {
Log.Information($"[workspace] connecting to <{name}> @ {this.server.host}:{this.server.port}"); _log.Information($"connecting to <{name}> @ {this.server.host}:{this.server.port}");
if(hasPasscode && passcode == null) if(hasPasscode && passcode == null)
{ {
Log.Error($"[workspace] *** workspace <{name}> requires a passcode but none was supplied."); _log.Error($"*** workspace <{name}> requires a passcode but none was supplied.");
OnWorkspaceConnectionError(this, new QWorkspaceConnectionErrorArgs { status = "badpass" }); OnWorkspaceConnectionError(this, new QWorkspaceConnectionErrorArgs { status = QConnectionStatus.BadPass, data = "badpass" });
return; return;
} }
if (!client.connect()) if (!client.connect())
{ {
Log.Error($"[workspace] *** couldn't connect to server client is not connected."); _log.Error($"*** couldn't connect to server client is not connected.");
OnWorkspaceConnectionError(this, new QWorkspaceConnectionErrorArgs { status = "unreachable" }); OnWorkspaceConnectionError(this, new QWorkspaceConnectionErrorArgs { status = QConnectionStatus.Error, data = "error" });
return; return;
} }
@@ -213,22 +213,18 @@ namespace QControlKit
public void disconnect() public void disconnect()
{ {
Log.Information($"[workspace] disconnecting from <{name}>"); _log.Information($"disconnecting from <{name}>");
if (!connected) if (!connected)
return; return;
if (heartbeatTimer != null)
stopHeartbeat();
stopReceivingUpdates(); stopReceivingUpdates();
disconnectFromWorkspace(); disconnectFromWorkspace();
connected = false;
client.disconnect(); client.disconnect();
connected = false;
} }
public void temporarilyDisconnect() public void temporarilyDisconnect()
{ {
//TODO //TODO what does this mean?
} }
#endregion #endregion
@@ -248,6 +244,7 @@ namespace QControlKit
#region Workspace Methods #region Workspace Methods
public void disconnectFromWorkspace() { client.sendMessage($"{workspacePrefix}/disconnect"); } public void disconnectFromWorkspace() { client.sendMessage($"{workspacePrefix}/disconnect"); }
public void startReceivingUpdates() { client.sendMessage($"{workspacePrefix}/updates", 1); } public void startReceivingUpdates() { client.sendMessage($"{workspacePrefix}/updates", 1); }
public void getPropertyForActive(string prop) { client.sendMessage($"{workspacePrefix}/cue/active/{prop}"); }
public void stopReceivingUpdates() { client.sendMessage($"{workspacePrefix}/updates", 0); } public void stopReceivingUpdates() { client.sendMessage($"{workspacePrefix}/updates", 0); }
public void enableAlwaysReply() { client.sendMessage($"{workspacePrefix}/alwaysReply", 1); } public void enableAlwaysReply() { client.sendMessage($"{workspacePrefix}/alwaysReply", 1); }
public void disableAlwaysReply() { client.sendMessage($"{workspacePrefix}/alwaysReply", 0); } public void disableAlwaysReply() { client.sendMessage($"{workspacePrefix}/alwaysReply", 0); }
@@ -255,6 +252,7 @@ namespace QControlKit
public void fetchCueLists() { client.sendMessage($"{workspacePrefix}/cueLists"); } public void fetchCueLists() { client.sendMessage($"{workspacePrefix}/cueLists"); }
public void fetchPlaybackPositionForCue(QCue cue) { client.sendMessage(addressForCue(cue, QOSCKey.PlaybackPositionId)); } //EventHandler for this? can I use the CueListPlaybackPosition one? public void fetchPlaybackPositionForCue(QCue cue) { client.sendMessage(addressForCue(cue, QOSCKey.PlaybackPositionId)); } //EventHandler for this? can I use the CueListPlaybackPosition one?
public void go() { client.sendMessage($"{workspacePrefix}/go"); } public void go() { client.sendMessage($"{workspacePrefix}/go"); }
public void go(string cueNumber) { client.sendMessage($"{workspacePrefix}/go/{cueNumber}"); }
public void save() { client.sendMessage($"{workspacePrefix}/save"); } public void save() { client.sendMessage($"{workspacePrefix}/save"); }
public void undo() { client.sendMessage($"{workspacePrefix}/undo"); } public void undo() { client.sendMessage($"{workspacePrefix}/undo"); }
public void redo() { client.sendMessage($"{workspacePrefix}/redo"); } public void redo() { client.sendMessage($"{workspacePrefix}/redo"); }
@@ -266,39 +264,7 @@ namespace QControlKit
#endregion #endregion
#region Heartbeat #region Heartbeat
//TODO //TODO? Do I even need this if I only allow TCP and just watch for a change in the state of the TCP connection
public void startHeartbeat()
{
clearHeartbeatTimeout();
sendHeartbeat();
}
public void stopHeartbeat()
{
clearHeartbeatTimeout();
heartbeatAttempts = -1;
}
public void clearHeartbeatTimeout()
{
heartbeatTimer.Stop();
heartbeatTimer = null;
heartbeatAttempts = 0;
//TODO
}
public void sendHeartbeat()
{
client.sendMessage("/thump");
//TODO
}
public void heartbeatTimeout(object sender, ElapsedEventArgs e)
{
//TODO
}
#endregion #endregion
@@ -354,18 +320,43 @@ namespace QControlKit
public void fetchDefaultPropertiesForCue(QCue cue) public void fetchDefaultPropertiesForCue(QCue cue)
{ {
List<string> keys = new List<string> { QOSCKey.UID, QOSCKey.Number, QOSCKey.Name, List<string> keys = new List<string> {
QOSCKey.ListName, QOSCKey.Type, QOSCKey.ColorName, QOSCKey.UID,
QOSCKey.Flagged, QOSCKey.Armed, QOSCKey.Notes }; QOSCKey.Flagged,
QOSCKey.Armed,
QOSCKey.IsRunning,
QOSCKey.IsBroken,
QOSCKey.IsLoaded,
QOSCKey.Parent,
};
fetchPropertiesForCue(cue, keys.ToArray(), false); fetchPropertiesForCue(cue, keys.ToArray(), false);
} }
public void fetchBasicPropertiesForCue(QCue cue) public void fetchBasicPropertiesForCue(QCue cue)
{ {
List<string> keys = new List<string> { QOSCKey.Name, QOSCKey.ListName, QOSCKey.Number, QOSCKey.FileTarget, QOSCKey.CueTargetNumber, List<string> keys = new List<string> {
QOSCKey.HasFileTargets, QOSCKey.HasCueTargets, QOSCKey.Armed, QOSCKey.ColorName, QOSCKey.Name,
QOSCKey.ContinueMode, QOSCKey.Flagged, QOSCKey.PreWait, QOSCKey.PostWait, QOSCKey.ListName,
QOSCKey.Duration, QOSCKey.AllowsEditingDuration, QOSCKey.Notes }; QOSCKey.Number,
QOSCKey.FileTarget,
QOSCKey.CueTargetNumber,
QOSCKey.HasFileTargets,
QOSCKey.HasCueTargets,
QOSCKey.Armed,
QOSCKey.ColorName,
QOSCKey.ContinueMode,
QOSCKey.Flagged,
QOSCKey.PreWait,
QOSCKey.PostWait,
QOSCKey.Duration,
QOSCKey.AllowsEditingDuration,
QOSCKey.IsBroken,
QOSCKey.IsRunning,
QOSCKey.IsLoaded,
QOSCKey.Notes
};
if (cue.IsGroup) if (cue.IsGroup)
keys.Add(QOSCKey.Children); keys.Add(QOSCKey.Children);
fetchPropertiesForCue(cue, keys.ToArray(), false); fetchPropertiesForCue(cue, keys.ToArray(), false);
@@ -393,6 +384,11 @@ namespace QControlKit
client.sendMessage(address, args); client.sendMessage(address, args);
} }
public void sendMessageForCue(QCue cue, string action)
{
client.sendMessage(addressForCue(cue, action));
}
private string addressForCue(QCue cue, string action) private string addressForCue(QCue cue, string action)
{ {
return $"{workspacePrefix}/cue_id/{cue.propertyForKey(QOSCKey.UID)}/{action}"; return $"{workspacePrefix}/cue_id/{cue.propertyForKey(QOSCKey.UID)}/{action}";
@@ -424,26 +420,28 @@ namespace QControlKit
{ {
//clear passcode if there was one set in the connect() method //clear passcode if there was one set in the connect() method
this.passcode = null; this.passcode = null;
Log.Error($"[workspace] *** Password for workspace {name} was incorrect!"); _log.Error($"*** Password for workspace <{name}> was incorrect!");
} }
else else
{ {
Log.Error($"[workspace] *** Unable to connect to workspace: <{name}> on server: <{server.name}>"); _log.Error($"*** Unable to connect to workspace: <{name}> on server: <{server.name}>");
} }
WorkspaceConnectionError?.Invoke(this, new QWorkspaceConnectionErrorArgs { status = args.status }); WorkspaceConnectionError?.Invoke(this, args);
} }
private void OnWorkspaceConnected(object source, QWorkspaceConnectedArgs args) private void OnWorkspaceConnected(object source, QWorkspaceConnectedArgs args)
{ {
Log.Information($"[workspace] Connection to <{name}> successful, finishing things up."); _log.Information($"Connection to <{name}> successful, finishing things up.");
WorkspaceConnected?.Invoke(this, args);
finishConnection(); finishConnection();
} }
private void OnWorkspaceDisconnected(object source, QWorkspaceDisconnectedArgs args) private void OnWorkspaceDisconnected(object source, QWorkspaceDisconnectedArgs args)
{ {
//this might not be called with TCP? //this might not be called with TCP?
Log.Warning($"[workspace] *** Workspace has indicated it is disconnecting"); _log.Warning($"*** Workspace has indicated it is disconnecting");
WorkspaceDisconnected?.Invoke(this, new QWorkspaceDisconnectedArgs());
} }
private void OnCueListsUpdated(object source, QCueListsUpdatedArgs args) private void OnCueListsUpdated(object source, QCueListsUpdatedArgs args)
@@ -511,7 +509,7 @@ namespace QControlKit
//add Event handled? use CueUpdated one? //add Event handled? use CueUpdated one?
} }
Log.Debug($"[workspace] cueLists finished processing. root updated? {rootCueUpdated}"); _log.Debug($"cueLists finished processing. root updated? {rootCueUpdated}");
OnWorkspaceUpdated(); OnWorkspaceUpdated();
@@ -529,7 +527,7 @@ namespace QControlKit
cueList.setProperty(args.cueID, QOSCKey.PlaybackPositionId, false); cueList.setProperty(args.cueID, QOSCKey.PlaybackPositionId, false);
} }
Log.Debug($"[workspace] cue list <{args.cueListID}> playback position changed to <{args.cueID}>"); _log.Information($"cue list <{args.cueListID}> playback position changed to <{args.cueID}>");
CueListChangedPlaybackPosition?.Invoke(this, new QCueListChangedPlaybackPositionArgs { cueListID = args.cueListID, cueID = args.cueID }); CueListChangedPlaybackPosition?.Invoke(this, new QCueListChangedPlaybackPositionArgs { cueListID = args.cueListID, cueID = args.cueID });
} }
@@ -543,7 +541,7 @@ namespace QControlKit
{ {
if (args.cueID.Equals(QIdentifiers.RootCueUpdate)) if (args.cueID.Equals(QIdentifiers.RootCueUpdate))
{ {
Log.Debug("[workspace] root cue update requested, updating all cue lists"); _log.Debug("root cue update requested, updating all cue lists");
foreach (var cuelist in this.cueLists) foreach (var cuelist in this.cueLists)
{ {
if (!cuelist.uid.Equals(QIdentifiers.ActiveCues)) if (!cuelist.uid.Equals(QIdentifiers.ActiveCues))
@@ -565,10 +563,20 @@ namespace QControlKit
private void OnCueUpdated(object source, QCueUpdatedArgs args) private void OnCueUpdated(object source, QCueUpdatedArgs args)
{ {
_log.Verbose($"OnCueUpdated: {args.cueID}");
QCue cue = cueWithID(args.cueID); QCue cue = cueWithID(args.cueID);
if (cue == null || cue.ignoreUpdates)
if(cue == null)
{
_log.Error($"Could not find cue to update, this is likely a problem.");
return; return;
}
if (cue.ignoreUpdates)
{
return;
}
cue.updatePropertiesWithDictionary(args.data); cue.updatePropertiesWithDictionary(args.data);
} }
@@ -583,7 +591,7 @@ namespace QControlKit
#region Printing #region Printing
public void Print() public void Print()
{ {
Log.Information($"[workspace] {name}"); _log.Information($"{name}");
foreach (var cueList in root.cues) foreach (var cueList in root.cues)
{ {
cueList.Print(); cueList.Print();
-2
View File
@@ -26,8 +26,6 @@ namespace SharpOSC
private static OscMessage parseMessage(byte[] msg) private static OscMessage parseMessage(byte[] msg)
{ {
int index = 0; int index = 0;
//Log.Debug("Raw ASCII DATA: " + System.Text.Encoding.ASCII.GetString(msg));
//Log.Debug("Raw UTF-8 DATA: " + System.Text.Encoding.UTF8.GetString(msg));
string address = null; string address = null;
char[] types = new char[0]; char[] types = new char[0];
List<object> arguments = new List<object>(); List<object> arguments = new List<object>();
+61
View File
@@ -0,0 +1,61 @@
using System.Collections.Generic;
namespace SharpOSC
{
public static class SlipFrame
{
public static readonly byte END = 0xc0;
public static readonly byte ESC = 0xdb;
public static readonly byte ESC_END = 0xDC;
public static readonly byte ESC_ESC = 0xDD;
public static List<byte[]> Decode(byte[] data)
{
List<byte[]> messages = new List<byte[]>();
List<byte> buffer = new List<byte>();
for (int i = 0; i < data.Length; i++)
{
if (data[i] == END && buffer.Count > 0)
{
messages.Add(buffer.ToArray());
buffer.Clear();
}
else if (data[i] != END)
{
buffer.Add(data[i]);
}
}
return messages;
}
public static byte[] Encode(byte[] data)
{
List<byte> slipData = new List<byte>();
byte[] esc_end = { ESC, ESC_END };
byte[] esc_esc = { ESC, ESC_ESC };
byte[] end = { END };
int length = data.Length;
for (int i = 0; i < length; i++)
{
if (data[i] == END)
{
slipData.AddRange(esc_end);
}
else if (data[i] == ESC)
{
slipData.AddRange(esc_esc);
}
else
{
slipData.Add(data[i]);
}
}
slipData.AddRange(end);
return slipData.ToArray();
}
}
}
+81 -100
View File
@@ -2,9 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Net.Sockets;
using System.Threading;
using Serilog; using Serilog;
using TcpSharp.Events;
namespace SharpOSC namespace SharpOSC
{ {
@@ -19,6 +18,8 @@ namespace SharpOSC
public class TCPClient public class TCPClient
{ {
private ILogger _log = Log.Logger.ForContext<TCPClient>();
public int Port public int Port
{ {
get { return _port; } get { return _port; }
@@ -31,15 +32,12 @@ namespace SharpOSC
} }
public delegate void MessageReceivedHandler(object source, MessageEventArgs args); public delegate void MessageReceivedHandler(object source, MessageEventArgs args);
public event MessageReceivedHandler MessageReceived; public event MessageReceivedHandler MessageReceived;
private Thread receivingThread;
string _address; string _address;
TcpClient client;
byte END = 0xc0; TcpSharp.TcpSharpSocketClient tcpClient;
byte ESC = 0xdb;
byte ESC_END = 0xDC;
byte ESC_ESC = 0xDD;
private List<byte> frameStream = new List<byte>();
public TCPClient(string address, int port) public TCPClient(string address, int port)
{ {
@@ -49,27 +47,82 @@ namespace SharpOSC
public bool Connect() public bool Connect()
{ {
try try
{ {
client = new TcpClient(Address, Port); tcpClient = new TcpSharp.TcpSharpSocketClient(Address, Port);
receivingThread = new Thread(ReceiveLoop); tcpClient.OnConnected += ClientConnected;
receivingThread.Start(); tcpClient.OnDataReceived += DataReceived;
Log.Debug($"[tcpclient] connected to <{Address}:{Port}>"); tcpClient.OnDisconnected += ClientDisconneted;
tcpClient.OnError += ClientError;
tcpClient.Connect();
return true; return true;
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e.Message); _log.Error(e.Message);
return false; return false;
} }
}
private void ClientError(object sender, OnClientErrorEventArgs e)
{
_log.Error("client error");
_log.Error(e.Exception.ToString());
}
private void ClientDisconneted(object sender, OnClientDisconnectedEventArgs e)
{
_log.Information($"client disconnected: {e.Reason}");
Close();
}
private void DataReceived(object sender, OnClientDataReceivedEventArgs e)
{
frameStream.AddRange(e.Data);
int i = 0;
int frameEnd = frameStream.FindIndex(1, frameByte=>frameByte.Equals(SlipFrame.END));
while(frameEnd > 0)
{
List<byte> frame = frameStream.GetRange(0, frameEnd+1);
frameStream.RemoveRange(0, frameEnd + 1);
List<byte[]> messages = SlipFrame.Decode(frame.ToArray());
foreach (var message in messages)
{
try
{
OscPacket packet = OscPacket.GetPacket(message);
OscMessage responseMessage = (OscMessage)packet;
if (packet == null)
{
_log.Error("packet is null");
}
if (responseMessage == null)
{
_log.Error("responeMessage is null");
}
OnMessageReceived(responseMessage);
}
catch (Exception ex)
{
_log.Error($"Exception parsing OSC message: {ex.ToString()}");
}
}
frameEnd = frameStream.FindIndex(0, frameByte => frameByte.Equals(SlipFrame.END));
}
}
private void ClientConnected(object sender, OnClientConnectedEventArgs e)
{
_log.Debug($"connected to <{Address}:{Port}>");
} }
public void Send(byte[] message) public void Send(byte[] message)
{ {
byte[] slipData = SlipEncode(message); byte[] slipData = SlipFrame.Encode(message);
NetworkStream netStream = client.GetStream(); tcpClient.SendBytes(slipData.ToArray());
netStream.Write(slipData.ToArray(), 0, slipData.ToArray().Length);
} }
public void Send(OscPacket packet) public void Send(OscPacket packet)
@@ -82,96 +135,24 @@ namespace SharpOSC
{ {
get get
{ {
if (client == null) if (tcpClient == null)
return false; return false;
else else
return client.Connected; return tcpClient.Connected;
} }
} }
public void ReceiveLoop()
{
while (client.Connected)
{
Receive();
}
//Log.Debug("[tcpclient] - ReceiveLoop has exited");
}
public void Receive()
{
Random random = new Random();
int num = random.Next(1000);
try
{
NetworkStream netStream = client.GetStream();
netStream.ReadTimeout = 250;
List<byte> responseData = new List<byte>();
if (netStream.CanRead)
{
//var watch = System.Diagnostics.Stopwatch.StartNew();
byte[] buffer = new byte[256];
int bytesRead = 0;
int reads = 0;
do
{
bytesRead = netStream.Read(buffer, 0, buffer.Length);
responseData.AddRange(buffer);
reads += 1;
Thread.Sleep(1);
//Log.Debug("Thread " + num + ": Bytes read: " + bytesRead + " - " + Encoding.UTF8.GetString(buffer));
} while (netStream.DataAvailable);
//Console.WriteLine("Raw TCP In: " + System.Text.Encoding.UTF8.GetString(responseData.ToArray()));
OscMessage response = (OscMessage)OscPacket.GetPacket(responseData.Skip(1).ToArray());
//watch.Stop();
//Console.WriteLine($"TCPCLient - message receive took {watch.ElapsedMilliseconds}ms and {reads} reads");
OnMessageReceived(response);
}
} catch(Exception e)
{
//Console.WriteLine("TCPSENDER - Receive Exception: " + e.ToString());
}
}
public byte[] SlipEncode(byte[] data)
{
List<byte> slipData = new List<byte>();
byte[] esc_end = { ESC, ESC_END };
byte[] esc_esc = { ESC, ESC_ESC };
byte[] end = { END };
int length = data.Length;
for (int i = 0; i < length; i++)
{
if (data[i] == END)
{
slipData.AddRange(esc_end);
}
else if (data[i] == ESC)
{
slipData.AddRange(esc_esc);
}
else
{
slipData.Add(data[i]);
}
}
slipData.AddRange(end);
return slipData.ToArray();
}
public void Close() public void Close()
{ {
if (client != null) if (tcpClient != null)
{ {
if (client.Connected) tcpClient.OnConnected -= ClientConnected;
tcpClient.OnDataReceived -= DataReceived;
tcpClient.OnDisconnected -= ClientDisconneted;
if (tcpClient.Connected)
{ {
Log.Debug($"[tcpClient] closing connection to {Address}"); _log.Debug($"closing connection to {Address}");
client.GetStream().Close(); tcpClient.Disconnect();
client.Close();
} }
} }
} }
@@ -181,4 +162,4 @@ namespace SharpOSC
MessageReceived?.Invoke(this, new MessageEventArgs() { Message = msg }); MessageReceived?.Invoke(this, new MessageEventArgs() { Message = msg });
} }
} }
} }
-2
View File
@@ -100,7 +100,6 @@ namespace SharpOSC
Log.Error(e.ToString()); Log.Error(e.ToString());
// If there is an error reading the packet, null is sent to the callback // If there is an error reading the packet, null is sent to the callback
} }
//Log.Debug("Raw UDP In: " + System.Text.Encoding.ASCII.GetString(bytes));
OscPacketCallback(packet); OscPacketCallback(packet);
} }
else else
@@ -151,7 +150,6 @@ namespace SharpOSC
{ {
byte[] bytes = queue.Dequeue(); byte[] bytes = queue.Dequeue();
var packet = OscPacket.GetPacket(bytes); var packet = OscPacket.GetPacket(bytes);
//Log.Debug("Raw UDP In: " + System.Text.Encoding.ASCII.GetString(bytes));
return packet; return packet;
} }
else else
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.jwetzell.qcontrolkitxamdemo"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.jwetzell.qcontrolkitxamdemo">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:label="QControlKitXamDemo.Android"></application> <application android:label="QControlKitXamDemo.Android"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest> </manifest>
@@ -1,110 +1,113 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{4F7F13C7-85EF-431F-AB03-B2494F0F4C7F}</ProjectGuid> <ProjectGuid>{4F7F13C7-85EF-431F-AB03-B2494F0F4C7F}</ProjectGuid>
<ProjectTypeGuids>{EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> <ProjectTypeGuids>{EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TemplateGuid>{c9e5eea5-ca05-42a1-839b-61506e0a37df}</TemplateGuid> <TemplateGuid>{c9e5eea5-ca05-42a1-839b-61506e0a37df}</TemplateGuid>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<RootNamespace>QControlKitXamDemo.Droid</RootNamespace> <RootNamespace>QControlKitXamDemo.Droid</RootNamespace>
<AssemblyName>QControlKitXamDemo.Android</AssemblyName> <AssemblyName>QControlKitXamDemo.Android</AssemblyName>
<Deterministic>True</Deterministic> <Deterministic>True</Deterministic>
<AndroidApplication>True</AndroidApplication> <AndroidApplication>True</AndroidApplication>
<AndroidResgenFile>Resources\Resource.designer.cs</AndroidResgenFile> <AndroidResgenFile>Resources\Resource.designer.cs</AndroidResgenFile>
<AndroidResgenClass>Resource</AndroidResgenClass> <AndroidResgenClass>Resource</AndroidResgenClass>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest> <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix> <MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix> <MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion> <TargetFrameworkVersion>v13.0</TargetFrameworkVersion>
<AndroidEnableSGenConcurrent>true</AndroidEnableSGenConcurrent> <AndroidEnableSGenConcurrent>true</AndroidEnableSGenConcurrent>
<AndroidUseAapt2>true</AndroidUseAapt2> <AndroidUseAapt2>true</AndroidUseAapt2>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType> <AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<NuGetPackageImportStamp> <NuGetPackageImportStamp>
</NuGetPackageImportStamp> </NuGetPackageImportStamp>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType> <DebugType>portable</DebugType>
<Optimize>false</Optimize> <Optimize>false</Optimize>
<OutputPath>bin\Debug</OutputPath> <OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG;</DefineConstants> <DefineConstants>DEBUG;</DefineConstants>
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<AndroidLinkMode>None</AndroidLinkMode> <AndroidLinkMode>None</AndroidLinkMode>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType> <DebugType>portable</DebugType>
<Optimize>true</Optimize> <Optimize>true</Optimize>
<OutputPath>bin\Release</OutputPath> <OutputPath>bin\Release</OutputPath>
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<AndroidManagedSymbols>true</AndroidManagedSymbols> <AndroidManagedSymbols>true</AndroidManagedSymbols>
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime> <AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Mono.Android" /> <Reference Include="Mono.Android" />
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Numerics" /> <Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors" /> <Reference Include="System.Numerics.Vectors" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Sinks.Xamarin"> <PackageReference Include="Serilog.Sinks.Xamarin">
<Version>0.2.0.64</Version> <Version>1.0.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2083" /> <PackageReference Include="Xamarin.Forms" Version="5.0.0.2622" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" /> <PackageReference Include="Xamarin.Essentials" Version="1.8.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="MainActivity.cs" /> <Compile Include="MainActivity.cs" />
<Compile Include="Resources\Resource.designer.cs" /> <Compile Include="Resources\Resource.designer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Resources\AboutResources.txt" /> <None Include="Resources\AboutResources.txt" />
<None Include="Assets\AboutAssets.txt" /> <None Include="Assets\AboutAssets.txt" />
<None Include="Properties\AndroidManifest.xml" /> <None Include="Properties\AndroidManifest.xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidResource Include="Resources\layout\Tabbar.xml" /> <AndroidResource Include="Resources\layout\Tabbar.xml" />
<AndroidResource Include="Resources\layout\Toolbar.xml" /> <AndroidResource Include="Resources\layout\Toolbar.xml" />
<AndroidResource Include="Resources\values\styles.xml" /> <AndroidResource Include="Resources\values\styles.xml" />
<AndroidResource Include="Resources\values\colors.xml" /> <AndroidResource Include="Resources\values\colors.xml" />
<AndroidResource Include="Resources\mipmap-anydpi-v26\icon.xml" /> <AndroidResource Include="Resources\mipmap-anydpi-v26\icon.xml" />
<AndroidResource Include="Resources\mipmap-anydpi-v26\icon_round.xml" /> <AndroidResource Include="Resources\mipmap-anydpi-v26\icon_round.xml" />
<AndroidResource Include="Resources\mipmap-hdpi\icon.png" /> <AndroidResource Include="Resources\mipmap-hdpi\icon.png" />
<AndroidResource Include="Resources\mipmap-hdpi\launcher_foreground.png" /> <AndroidResource Include="Resources\mipmap-hdpi\launcher_foreground.png" />
<AndroidResource Include="Resources\mipmap-mdpi\icon.png" /> <AndroidResource Include="Resources\mipmap-mdpi\icon.png" />
<AndroidResource Include="Resources\mipmap-mdpi\launcher_foreground.png" /> <AndroidResource Include="Resources\mipmap-mdpi\launcher_foreground.png" />
<AndroidResource Include="Resources\mipmap-xhdpi\icon.png" /> <AndroidResource Include="Resources\mipmap-xhdpi\icon.png" />
<AndroidResource Include="Resources\mipmap-xhdpi\launcher_foreground.png" /> <AndroidResource Include="Resources\mipmap-xhdpi\launcher_foreground.png" />
<AndroidResource Include="Resources\mipmap-xxhdpi\icon.png" /> <AndroidResource Include="Resources\mipmap-xxhdpi\icon.png" />
<AndroidResource Include="Resources\mipmap-xxhdpi\launcher_foreground.png" /> <AndroidResource Include="Resources\mipmap-xxhdpi\launcher_foreground.png" />
<AndroidResource Include="Resources\mipmap-xxxhdpi\icon.png" /> <AndroidResource Include="Resources\mipmap-xxxhdpi\icon.png" />
<AndroidResource Include="Resources\mipmap-xxxhdpi\launcher_foreground.png" /> <AndroidResource Include="Resources\mipmap-xxxhdpi\launcher_foreground.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Resources\drawable\" /> <Folder Include="Resources\drawable\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\QControlKit\QControlKit.csproj"> <ProjectReference Include="..\..\QControlKit\QControlKit.csproj">
<Project>{51cd2579-79c9-49a9-b68d-93cbe878baa1}</Project> <Project>{51cd2579-79c9-49a9-b68d-93cbe878baa1}</Project>
<Name>QControlKit</Name> <Name>QControlKit</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\QControlKitXamDemo\QControlKitXamDemo.csproj"> <ProjectReference Include="..\QControlKitXamDemo\QControlKitXamDemo.csproj">
<Project>{20D2D0D8-A518-42DC-BAB8-CF098CDF03C4}</Project> <Project>{20D2D0D8-A518-42DC-BAB8-CF098CDF03C4}</Project>
<Name>QControlKitXamDemo</Name> <Name>QControlKitXamDemo</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> <ItemGroup>
<ProjectExtensions> <AndroidAsset Include="Assets\materialdesignicons-webfont.ttf" />
<VisualStudio> </ItemGroup>
<UserProperties XamarinHotReloadDebuggerTimeoutExceptionQControlKitXamDemoAndroidHideInfoBar="True" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</VisualStudio> <ProjectExtensions>
</ProjectExtensions> <VisualStudio>
<UserProperties XamarinHotReloadDebuggerTimeoutExceptionQControlKitXamDemoAndroidHideInfoBar="True" />
</VisualStudio>
</ProjectExtensions>
</Project> </Project>
@@ -5,6 +5,8 @@ using System.Linq;
using Foundation; using Foundation;
using UIKit; using UIKit;
using Serilog; using Serilog;
using Serilog.Events;
namespace QControlKitXamDemo.iOS namespace QControlKitXamDemo.iOS
{ {
// The UIApplicationDelegate for the application. This class is responsible for launching the // The UIApplicationDelegate for the application. This class is responsible for launching the
@@ -23,7 +25,7 @@ namespace QControlKitXamDemo.iOS
public override bool FinishedLaunching(UIApplication app, NSDictionary options) public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{ {
global::Xamarin.Forms.Forms.Init(); global::Xamarin.Forms.Forms.Init();
Log.Logger = new LoggerConfiguration().WriteTo.NSLog().MinimumLevel.Debug().CreateLogger(); Log.Logger = new LoggerConfiguration().WriteTo.NSLog(outputTemplate: "[{Level}] ({SourceContext}) {Message}{NewLine}{Exception}").MinimumLevel.Verbose().CreateLogger();
LoadApplication(new App()); LoadApplication(new App());
return base.FinishedLaunching(app, options); return base.FinishedLaunching(app, options);
@@ -2,37 +2,47 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>UIDeviceFamily</key> <key>UIDeviceFamily</key>
<array> <array>
<integer>1</integer> <integer>1</integer>
<integer>2</integer> <integer>2</integer>
</array> </array>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>8.0</string> <string>8.0</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>QControlKitXamDemo</string> <string>QControlKitXamDemo</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.companyname.QControlKitXamDemo</string> <string>com.companyname.QControlKitXamDemo</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>QControlKitXamDemo</string> <string>QControlKitXamDemo</string>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string> <string>Assets.xcassets/AppIcon.appiconset</string>
<key>NSBonjourServices</key>
<array>
<string>_qlab._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Scan network for QLab instances</string>
<key>UIAppFonts</key>
<array>
<string>materialdesignicons-webfont.ttf</string>
</array>
</dict> </dict>
</plist> </plist>
@@ -51,6 +51,7 @@
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements> <CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
<MtouchLink>None</MtouchLink> <MtouchLink>None</MtouchLink>
<MtouchInterpreter>-all</MtouchInterpreter> <MtouchInterpreter>-all</MtouchInterpreter>
<CodesignProvision>Wildcard</CodesignProvision>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhone' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhone' ">
<DebugType>none</DebugType> <DebugType>none</DebugType>
@@ -125,13 +126,16 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog"> <PackageReference Include="Serilog">
<Version>2.10.0</Version> <Version>2.12.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Serilog.Sinks.Xamarin"> <PackageReference Include="Serilog.Sinks.Xamarin">
<Version>0.2.0.64</Version> <Version>1.0.0</Version>
</PackageReference>
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2515" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
<PackageReference Include="SuperSimpleTcp">
<Version>3.0.0.2</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2083" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
<ItemGroup> <ItemGroup>
@@ -140,4 +144,7 @@
<Name>QControlKitXamDemo</Name> <Name>QControlKitXamDemo</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\materialdesignicons-webfont.ttf" />
</ItemGroup>
</Project> </Project>
@@ -6,6 +6,52 @@
mc:Ignorable="d" mc:Ignorable="d"
x:Class="QControlKitXamDemo.App"> x:Class="QControlKitXamDemo.App">
<Application.Resources> <Application.Resources>
<OnPlatform x:Key="MaterialFontFamily" x:TypeArguments="x:String">
<On Platform="iOS">Material Design Icons</On>
<On Platform="Android">materialdesignicons-webfont.ttf#Material Design Icons</On>
</OnPlatform>
<Style TargetType="Label"
Class="PrimaryText"
ApplyToDerivedTypes="true">
<Setter Property="TextColor"
Value="{DynamicResource PrimaryTextColor}" />
</Style>
<Style TargetType="Button"
Class="PrimaryButton"
ApplyToDerivedTypes="true">
<Setter Property="TextColor"
Value="{DynamicResource PrimaryTextColor}" />
</Style>
<x:Double x:Key="SmallPadding">4</x:Double>
<x:Double x:Key="MediumPadding">8</x:Double>
<x:Double x:Key="StandardPadding">16</x:Double>
<Thickness x:Key="SmallTopThickness" Top="{StaticResource SmallPadding}" />
<Thickness x:Key="MediumTopThickness" Top="{StaticResource MediumPadding}" />
<Thickness x:Key="StandardTopThickness" Top="{StaticResource StandardPadding}" />
<Thickness x:Key="SmallThickness"
Bottom="{StaticResource SmallPadding}"
Left="{StaticResource SmallPadding}"
Right="{StaticResource SmallPadding}"
Top="{StaticResource SmallPadding}" />
<Thickness x:Key="MediumThickness"
Bottom="{StaticResource MediumPadding}"
Left="{StaticResource MediumPadding}"
Right="{StaticResource MediumPadding}"
Top="{StaticResource MediumPadding}" />
<Thickness x:Key="StandardThickness"
Bottom="{StaticResource StandardPadding}"
Left="{StaticResource StandardPadding}"
Right="{StaticResource StandardPadding}"
Top="{StaticResource StandardPadding}" />
</Application.Resources> </Application.Resources>
</Application> </Application>
@@ -9,6 +9,8 @@ namespace QControlKitXamDemo
InitializeComponent(); InitializeComponent();
MainPage = new NavigationPage(new MainPage()); MainPage = new NavigationPage(new MainPage());
Current.RequestedThemeChanged += Current_RequestedThemeChanged;
SetAppResources();
} }
protected override void OnStart() protected override void OnStart()
@@ -22,5 +24,80 @@ namespace QControlKitXamDemo
protected override void OnResume() protected override void OnResume()
{ {
} }
private void Current_RequestedThemeChanged(object sender, AppThemeChangedEventArgs e)
{
SetAppResources();
}
private void SetAppResources()
{
switch (Current.RequestedTheme)
{
case OSAppTheme.Light:
SetLightResources();
break;
case OSAppTheme.Dark:
SetDarkResources();
break;
case OSAppTheme.Unspecified:
//Default to Light
SetLightResources();
break;
default:
SetLightResources();
break;
}
}
private void SetDarkResources()
{
Resources["NavigationBarColor"] = Color.FromHex("#3f6dab");
Resources["PageBackgroundColor"] = Color.FromHex("#525252");
Resources["PrimaryColor"] = Color.WhiteSmoke;
Resources["SecondaryColor"] = Color.White;
Resources["PrimaryTextColor"] = Color.LightGray;
Resources["SecondaryTextColor"] = Color.White;
Resources["TertiaryTextColor"] = Color.Gray;
Resources["TransparentColor"] = Color.Transparent;
Resources["IconTextColor"] = Color.WhiteSmoke;
Resources["ListViewBackgroundColor"] = Color.FromHex("#363636");
Resources["ServerCellBackgroundColor"] = Color.FromHex("#363636");
Resources["WorkspaceCellBackgroundColor"] = Color.FromHex("#737373");
Resources["SelectedCueCellBackgroundColor"] = Color.FromHex("#737373");
}
private void SetLightResources()
{
Resources["NavigationBarColor"] = Color.FromHex("#3089ff");
Resources["PageBackgroundColor"] = Color.LightGray;
Resources["PrimaryColor"] = Color.WhiteSmoke;
Resources["SecondaryColor"] = Color.White;
Resources["PrimaryTextColor"] = Color.Black;
Resources["SecondaryTextColor"] = Color.White;
Resources["TertiaryTextColor"] = Color.Gray;
Resources["TransparentColor"] = Color.Transparent;
Resources["IconTextColor"] = Color.Black;
Resources["ListViewBackgroundColor"] = Color.White;
Resources["ServerCellBackgroundColor"] = Color.White;
Resources["WorkspaceCellBackgroundColor"] = Color.FromHex("#D8D8D8");
Resources["SelectedCueCellBackgroundColor"] = Color.FromHex("#D8D8D8");
}
} }
} }
@@ -7,6 +7,7 @@ using Xamarin.Forms;
using QControlKit.Events; using QControlKit.Events;
using QControlKit.Constants; using QControlKit.Constants;
using Serilog;
namespace QControlKitXamDemo namespace QControlKitXamDemo
{ {
@@ -20,25 +21,44 @@ namespace QControlKitXamDemo
connectedWorkspace = workspace; connectedWorkspace = workspace;
connectedWorkspace.WorkspaceUpdated += Workspace_WorkspaceUpdated; connectedWorkspace.WorkspaceUpdated += Workspace_WorkspaceUpdated;
connectedWorkspace.WorkspaceConnectionError += ConnectedWorkspace_WorkspaceConnectionError; connectedWorkspace.WorkspaceConnectionError += ConnectedWorkspace_WorkspaceConnectionError;
connectedWorkspace.WorkspaceConnected += ConnectedWorkspace_WorkspaceConnected; ;
connectedWorkspace.connect(passcode); connectedWorkspace.connect(passcode);
connectedWorkspace.defaultSendUpdatesOSC = true; connectedWorkspace.defaultSendUpdatesOSC = true;
connectedWorkspace.CueListChangedPlaybackPosition += ConnectedWorkspace_CueListChangedPlaybackPosition; connectedWorkspace.CueListChangedPlaybackPosition += ConnectedWorkspace_CueListChangedPlaybackPosition;
} }
protected override void OnDisappearing() protected override void OnDisappearing()
{ {
base.OnDisappearing(); base.OnDisappearing();
connectedWorkspace.disconnect(); connectedWorkspace.disconnect();
connectedWorkspace.WorkspaceUpdated -= Workspace_WorkspaceUpdated; connectedWorkspace.WorkspaceUpdated -= Workspace_WorkspaceUpdated;
connectedWorkspace.WorkspaceConnectionError -= ConnectedWorkspace_WorkspaceConnectionError; connectedWorkspace.WorkspaceConnectionError -= ConnectedWorkspace_WorkspaceConnectionError;
connectedWorkspace.WorkspaceConnected -= ConnectedWorkspace_WorkspaceConnected;
connectedWorkspace.CueListChangedPlaybackPosition -= ConnectedWorkspace_CueListChangedPlaybackPosition; connectedWorkspace.CueListChangedPlaybackPosition -= ConnectedWorkspace_CueListChangedPlaybackPosition;
} }
private async void ConnectedWorkspace_WorkspaceConnected(object source, QWorkspaceConnectedArgs args)
{
if (args.status.Equals("ok") && connectedWorkspace.connectedToQLab5)
{
if (!args.data.Contains(QConnectionRole.View))
{
string passcode = await DisplayPromptAsync("PIN does not have view permissions.", "Try Again", maxLength: 4, keyboard: Keyboard.Numeric);
if (passcode != null)
{
connectedWorkspace.connect(passcode);
}
}
}
}
private async void ConnectedWorkspace_WorkspaceConnectionError(object source, QWorkspaceConnectionErrorArgs args) private async void ConnectedWorkspace_WorkspaceConnectionError(object source, QWorkspaceConnectionErrorArgs args)
{ {
if (args.status.Equals("badpass")) if (args.status.Equals("badpass"))
{ {
string passcode = await DisplayPromptAsync("Passcode was incorrect.", "Try Again", maxLength: 4, keyboard: Keyboard.Numeric); string passcode = await DisplayPromptAsync("PIN is incorrect.", "Try Again", maxLength: 4, keyboard: Keyboard.Numeric);
if (passcode != null) if (passcode != null)
{ {
connectedWorkspace.connect(passcode); connectedWorkspace.connect(passcode);
@@ -53,7 +73,7 @@ namespace QControlKitXamDemo
QCue selectedCue = connectedWorkspace.cueWithID(args.cueID); QCue selectedCue = connectedWorkspace.cueWithID(args.cueID);
if(selectedCue != null) //TODO: I think this being null is the result of a race condition? if(selectedCue != null) //TODO: I think this being null is the result of a race condition?
{ {
connectedWorkspace.fetchDefaultPropertiesForCue(selectedCue); connectedWorkspace.fetchBasicPropertiesForCue(selectedCue);
selectedCueGrid.BindingContext = new QCueViewModel(selectedCue, false); selectedCueGrid.BindingContext = new QCueViewModel(selectedCue, false);
if (cueGridDict.ContainsKey(args.cueID)) if (cueGridDict.ContainsKey(args.cueID))
{ {
@@ -64,6 +84,18 @@ namespace QControlKitXamDemo
}); });
} }
void updateCuePropertes(QCue cue)
{
cue.workspace.fetchDefaultPropertiesForCue(cue);
if (cue.cues.Count > 0)
{
foreach (QCue childCue in cue.cues)
{
childCue.workspace.fetchDefaultPropertiesForCue(childCue);
}
}
}
void Workspace_WorkspaceUpdated(object source, QWorkspaceUpdatedArgs args) void Workspace_WorkspaceUpdated(object source, QWorkspaceUpdatedArgs args)
{ {
if(connectedWorkspace.cueLists.Count > 0) if(connectedWorkspace.cueLists.Count > 0)
@@ -71,6 +103,7 @@ namespace QControlKitXamDemo
List<Task> cueAddTasks = new List<Task>(); List<Task> cueAddTasks = new List<Task>();
foreach(var aCue in connectedWorkspace.cueLists) foreach(var aCue in connectedWorkspace.cueLists)
{ {
updateCuePropertes(aCue);
Grid cueGrid = cueToGrid(aCue); Grid cueGrid = cueToGrid(aCue);
cueGridDict.Add(aCue.uid, cueGrid); cueGridDict.Add(aCue.uid, cueGrid);
MainThread.InvokeOnMainThreadAsync(() => MainThread.InvokeOnMainThreadAsync(() =>
@@ -3,14 +3,121 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Class="QControlKitXamDemo.MainPage"> xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
mc:Ignorable="d"
x:Class="QControlKitXamDemo.MainPage"
ios:Page.UseSafeArea="true"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<ContentPage.Content>
<ListView x:Name="serverListView" GroupDisplayBinding="{Binding name}" IsGroupingEnabled="True"> <StackLayout>
<ListView.ItemTemplate> <Frame BackgroundColor="{DynamicResource PageBackgroundColor}"
<DataTemplate> CornerRadius="0"
<TextCell Text="{Binding name}"/> Margin="0"
</DataTemplate> Padding="0"
</ListView.ItemTemplate> HasShadow="False">
</ListView> <StackLayout Spacing="0" BackgroundColor="Transparent">
<Label Margin="0,10,0,10"
</ContentPage> Padding="0"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
HorizontalTextAlignment="Center"
FontSize="Title"
Text="Discovered QLab Instances" />
<!--Need to try and remove the group spacing caused by ios:GroupHeaderStyle-->
<ListView x:Name="serverListView"
ios:ListView.GroupHeaderStyle="Grouped"
GroupDisplayBinding="{Binding name}"
IsGroupingEnabled="True"
d:IsGroupingEnabled="False"
BackgroundColor="Transparent"
SeparatorVisibility="None"
ItemsSource="{Binding ServersGrouped}"
Margin="0">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<Frame BackgroundColor="{DynamicResource ServerCellBackgroundColor}"
Margin="0,0,0,0"
Padding="10,0,10,0"
CornerRadius="0">
<Grid BackgroundColor="Transparent" VerticalOptions="FillAndExpand"
RowSpacing="0"
ColumnSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Text="{Binding GroupName}"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
HorizontalTextAlignment="Start"
StyleClass="PrimaryText"/>
<Label Grid.Column="1"
Text="{Binding version}"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
HorizontalTextAlignment="End"
StyleClass="PrimaryText"/>
</Grid>
</Frame>
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Frame BackgroundColor="{DynamicResource WorkspaceCellBackgroundColor}"
BorderColor="Black"
Margin="5,0,5,0"
Padding="10,0,10,0"
CornerRadius="0"
HasShadow="False">
<Grid Margin="0"
BackgroundColor="Transparent"
RowSpacing="0"
ColumnSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Text="{Binding Name}"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
HorizontalTextAlignment="Start"
StyleClass="PrimaryText"/>
<Label Grid.Column="1" Text="&#xf1184;"
FontFamily="{StaticResource MaterialFontFamily}"
FontSize="Large"
IsVisible="{Binding HasPasscode}"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
HorizontalTextAlignment="End"
StyleClass="PrimaryText"/>
</Grid>
</Frame>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</Frame>
</StackLayout>
</ContentPage.Content>
</ContentPage>
@@ -2,146 +2,71 @@
using System.ComponentModel; using System.ComponentModel;
using Xamarin.Forms; using Xamarin.Forms;
using QControlKit; using QControlKit;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using QControlKit.Events;
using System.Threading.Tasks;
using Serilog; using Serilog;
using QControlKitXamDemo.ViewModels;
using Acr.UserDialogs;
namespace QControlKitXamDemo namespace QControlKitXamDemo
{ {
// Learn more about making custom code visible in the Xamarin.Forms previewer
// by visiting https://aka.ms/xamarinforms-previewer
[DesignTimeVisible(false)]
public partial class MainPage : ContentPage public partial class MainPage : ContentPage
{ {
public ObservableCollection<ServerGroup> servers = new ObservableCollection<ServerGroup>(); private QBrowserViewModel qBrowserViewModel;
public MainPage() public MainPage()
{ {
InitializeComponent(); InitializeComponent();
serverListView.ItemsSource = servers;
QBrowser browser = new QBrowser();
browser.ServerLost += Browser_ServerLost;
browser.ServerFound += Browser_ServerFound;
serverListView.ItemSelected += ServerListView_ItemSelected;
Device.StartTimer(TimeSpan.FromSeconds(5), () => qBrowserViewModel = new QBrowserViewModel(new QBrowser());
{ serverListView.BindingContext = qBrowserViewModel;
Task.Run(async () => serverListView.ItemSelected += QWorkspaceSelected;
{
browser.ProbeForQLabInstances();
});
return true;
});
} }
private void Browser_ServerFound(object source, QServerFoundArgs args) async void QWorkspaceSelected(object sender, SelectedItemChangedEventArgs e)
{ {
List<ServerGroup> serverGroups = servers.ToList(); if (e.SelectedItem == null)
bool serverFound = false;
foreach (var serverGroup in serverGroups)
{ {
return;
if (serverGroup.host == args.server.host)
{
serverFound = true;
}
} }
if (!serverFound) qBrowserViewModel.autoUpdate = false;
{ QWorkspace selectedWorkspace = (e.SelectedItem as QWorkspaceViewModel).workspace;
Device.BeginInvokeOnMainThread(() => Log.Debug($"workspace: {selectedWorkspace.nameWithoutPathExtension} has been selected");
{ ((ListView)sender).SelectedItem = null;
ServerGroup serverGroup = new ServerGroup(args.server.name);
args.server.WorkspaceAdded += Server_WorkspaceAdded;
args.server.WorkspaceRemoved += Server_WorkspaceRemoved;
serverGroup.host = args.server.host;
servers.Add(serverGroup);
});
}
}
private void Server_WorkspaceRemoved(object source, QServerWorkspaceChangedArgs args) if (selectedWorkspace.hasPasscode)
{
foreach (var server in servers)
{ {
if (server.host == args.server.host) UserDialogs.Instance.Prompt(new PromptConfig
{ {
Device.BeginInvokeOnMainThread(() => InputType = InputType.Number,
MaxLength = 4,
Title = "Workspace Requires Passcode",
OkText = "Connect",
IsCancellable = true,
OnTextChanged = args =>
{ {
server.Remove(args.workspace); args.IsValid = args.Value != null && !args.Value.Equals("") && args.Value.Length == 4;
}); },
} OnAction = async (resp) =>
}
}
private void Server_WorkspaceAdded(object source, QServerWorkspaceChangedArgs args)
{
foreach(var server in servers)
{
if(server.host == args.server.host)
{
Device.BeginInvokeOnMainThread(() =>
{ {
server.Add(args.workspace); if (resp.Ok)
}); {
} await Navigation.PushAsync(new CueListPage(selectedWorkspace, resp.Value));
} }
}
private void Browser_ServerLost(object source, QServerLostArgs args)
{
List<ServerGroup> serverGroups = servers.ToList();
foreach(var serverGroup in serverGroups)
{
if(serverGroup.host == args.server.host)
{
Device.BeginInvokeOnMainThread(() =>
{
servers.Remove(serverGroup);
});
}
}
}
async void ServerListView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
if(e.SelectedItem != null)
{
QWorkspace selectedWorkspace = e.SelectedItem as QWorkspace;
Log.Debug($"[demo] Workspace <{selectedWorkspace.nameWithoutPathExtension}> has been selected");
if (selectedWorkspace.hasPasscode)
{
string passcode = await DisplayPromptAsync("Workspace has passcode", "Enter Passcode", maxLength: 4, keyboard: Keyboard.Numeric);
if (passcode != null)
{
await Navigation.PushAsync(new CueListPage(selectedWorkspace, passcode));
} }
}
else
{
await Navigation.PushAsync(new CueListPage(selectedWorkspace));
} });
serverListView.SelectedItem = null; }
else
{
await Navigation.PushAsync(new CueListPage(selectedWorkspace));
} }
} }
}
public class ServerGroup : ObservableCollection<QWorkspace>{
public string name { get; set; } protected override void OnAppearing()
public string host { get; set; }
public ServerGroup(string name)
{ {
this.name = name; base.OnAppearing();
qBrowserViewModel.autoUpdate = true;
} }
} }
} }
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
@@ -11,9 +11,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog" Version="2.10.0" /> <PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2083" /> <PackageReference Include="Xamarin.Forms" Version="5.0.0.2622" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" /> <PackageReference Include="Xamarin.Essentials" Version="1.8.0" />
<PackageReference Include="Acr.UserDialogs" Version="7.2.0.564" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -28,4 +29,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\QControlKit\QControlKit.csproj" /> <ProjectReference Include="..\..\QControlKit\QControlKit.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="Acr.UserDialogs" />
</ItemGroup>
</Project> </Project>
@@ -0,0 +1,98 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xamarin.Forms;
using QControlKit;
using QControlKit.Events;
using System;
using System.Threading.Tasks;
using Serilog;
namespace QControlKitXamDemo.ViewModels
{
public class QBrowserViewModel : INotifyPropertyChanged
{
private ILogger _log = Log.Logger.ForContext<QBrowserViewModel>();
QBrowser browser;
public event PropertyChangedEventHandler PropertyChanged;
public bool autoUpdate = false;
public ObservableCollection<QServerViewModel> ServersGrouped { get; set; }
bool connected = false;
public QBrowserViewModel(QBrowser browser)
{
this.browser = browser;
ServersGrouped = new ObservableCollection<QServerViewModel>();
this.browser.ServerFound += OnServerFound;
this.browser.ServerLost += OnServerLost;
Device.StartTimer(TimeSpan.FromSeconds(4), () =>
{
Task.Run(() =>
{
if (autoUpdate)
{
_log.Verbose($"Auto Update is enabled running probe");
Device.BeginInvokeOnMainThread(() =>
{
_log.Debug("QBrowser probe triggered");
this.browser.ProbeForQLabInstances();
});
}
});
return true;
});
}
private void OnServerFound(object source, QServerFoundArgs args)
{
Device.BeginInvokeOnMainThread(() =>
{
_log.Debug($"adding server: {args.server.description}");
ServersGrouped.Add(new QServerViewModel(args.server));
});
}
private void OnServerLost(object source, QServerLostArgs args)
{
QServerViewModel serverToRemove = null;
foreach (QServerViewModel serverGroup in ServersGrouped)
{
if (serverGroup.host.Equals(args.server.host))
{
serverToRemove = serverGroup;
break;
}
}
if (serverToRemove != null)
{
Device.BeginInvokeOnMainThread(() =>
{
_log.Debug($"removing server: {args.server.description}");
ServersGrouped.Remove(serverToRemove);
});
}
}
void OnPropertyChanged([CallerMemberName] string name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public void InitiateScan()
{
if (!autoUpdate)
{
_log.Debug("Manual Scan Initiated");
browser.ProbeForQLabInstances();
}
}
}
}
@@ -5,11 +5,14 @@ using Xamarin.Forms;
using Serilog; using Serilog;
using QControlKit.Events; using QControlKit.Events;
using QControlKit.Constants; using QControlKit.Constants;
using System;
namespace QControlKitXamDemo.ViewModels namespace QControlKitXamDemo.ViewModels
{ {
public class QCueViewModel : INotifyPropertyChanged public class QCueViewModel : INotifyPropertyChanged
{ {
private ILogger _log = Log.Logger.ForContext<QCueViewModel>();
QCue cue; QCue cue;
bool isSelected = false; bool isSelected = false;
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
@@ -17,22 +20,32 @@ namespace QControlKitXamDemo.ViewModels
public QCueViewModel(QCue cue, bool checkPlayback) public QCueViewModel(QCue cue, bool checkPlayback)
{ {
this.cue = cue; this.cue = cue;
if(checkPlayback) if (checkPlayback)
{
this.cue.workspace.CueListChangedPlaybackPosition += Workspace_CueListChangedPlaybackPosition; this.cue.workspace.CueListChangedPlaybackPosition += Workspace_CueListChangedPlaybackPosition;
}
this.cue.CuePropertiesUpdated += OnCuePropertiesUpdated; this.cue.CuePropertiesUpdated += OnCuePropertiesUpdated;
} }
private void OnCuePropertiesUpdated(object source, QCuePropertiesUpdatedArgs args) private void OnCuePropertiesUpdated(object source, QCuePropertiesUpdatedArgs args)
{ {
foreach(var property in args.properties) _log.Debug($"properties updated from cue");
foreach (var property in args.properties)
{ {
Log.Debug($"[cueviewmodel] property <{property}> has been updated."); _log.Debug($"property <{property}> has been updated for cue {name}.");
if (property.Equals(QOSCKey.Name) || property.Equals(QOSCKey.ListName)) if (property.Equals(QOSCKey.Name) || property.Equals(QOSCKey.ListName))
{ {
OnPropertyChanged("name"); OnPropertyChanged("name");
}else if (property.Equals(QOSCKey.ColorName)) }else if (property.Equals(QOSCKey.ColorName))
{ {
OnPropertyChanged("color"); OnPropertyChanged("color");
}else if (property.Equals(QOSCKey.ActionElapsed))
{
OnPropertyChanged("actionElapsed");
}else if (property.Equals(QOSCKey.IsBroken) || property.Equals(QOSCKey.IsRunning))
{
OnPropertyChanged("status");
} }
} }
} }
@@ -84,6 +97,34 @@ namespace QControlKitXamDemo.ViewModels
} }
} }
public string actionElapsed
{
get
{
TimeSpan time = TimeSpan.FromSeconds(cue.actionElapsed);
return time.ToString(@"mm\:ss\:ff");
}
}
public string status
{
get
{
if (cue.IsRunning)
{
return "running";
}else if (cue.IsBroken)
{
return "broken";
}
else
{
return " ";
}
}
}
public bool IsSelected { public bool IsSelected {
get get
{ {
@@ -0,0 +1,90 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xamarin.Forms;
using QControlKit;
using QControlKit.Events;
using Serilog;
namespace QControlKitXamDemo.ViewModels
{
public class QServerViewModel : ObservableCollection<QWorkspaceViewModel>, INotifyPropertyChanged
{
private ILogger _log = Log.Logger.ForContext<QServerViewModel>();
QServer server;
public new event PropertyChangedEventHandler PropertyChanged;
public string name
{
get => server.name;
set => server.name = value;
}
public string host => server.host;
public string version => server.version == null ? "" : $"v{server.version}";
public string GroupName => $"{name} ({host})";
public QServerViewModel(QServer server)
{
this.server = server;
name = this.server.name;
foreach (QWorkspace workspace in this.server.workspaces)
{
Device.BeginInvokeOnMainThread(() =>
{
Add(new QWorkspaceViewModel(workspace));
});
}
this.server.WorkspaceAdded += Server_WorkspaceAdded;
this.server.WorkspaceRemoved += Server_WorkspaceRemoved;
this.server.ServerUpdated += OnServerUpdated;
}
private void Server_WorkspaceRemoved(object source, QServerWorkspaceChangedArgs args)
{
QWorkspaceViewModel workspaceToRemove = null;
foreach (QWorkspaceViewModel workspaceViewModel in this)
{
if (workspaceViewModel.workspace.uniqueID == args.workspace.uniqueID)
{
workspaceToRemove = workspaceViewModel;
break;
}
}
if (workspaceToRemove != null)
{
Device.BeginInvokeOnMainThread(() =>
{
Remove(workspaceToRemove);
});
}
}
private void Server_WorkspaceAdded(object source, QServerWorkspaceChangedArgs args)
{
QWorkspaceViewModel workspaceViewModel = new QWorkspaceViewModel(args.workspace);
Device.BeginInvokeOnMainThread(() =>
{
Add(workspaceViewModel);
});
}
private void OnServerUpdated(object source, QServerUpdatedArgs args)
{
OnPropertyChanged("GroupName");
OnPropertyChanged("version");
}
void OnPropertyChanged([CallerMemberName] string name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
@@ -0,0 +1,17 @@
using QControlKit;
namespace QControlKitXamDemo.ViewModels
{
public class QWorkspaceViewModel
{
public QWorkspace workspace;
public QWorkspaceViewModel(QWorkspace workspace)
{
this.workspace = workspace;
}
public string Name => workspace.name;
public bool HasPasscode => workspace.hasPasscode;
}
}
+9
View File
@@ -2,6 +2,15 @@
This is an unofficial .NET library port of Figure53's [QLabKit](https://github.com/Figure53/QLabKit.objc). This is an unofficial .NET library port of Figure53's [QLabKit](https://github.com/Figure53/QLabKit.objc).
## Components
- **QBrowser**: Searches for an maintains a list of QLab "instances" using bonjour
- **QClient**: Wrapper over underlying TCP/UDP connection as well as providing event handlers for common QLab replies (i.e. cue needs updated, workspace need updated, etc.)
- **QCue**: The most basic object in QLab, contains shared properties and methods for a cue in a cuelist (cuelists are also a cue) is extended for other cue types (Text, Midi, etc.)
- **QMessage**: Wrapper over underlying OSC Message, has methods to quickly determine the message type and parts (Workspace info, address, arguments, etc.)
- **QServer**: This class represents a computer that is running QLab, it has host information (IP, Port) as well as discovered workspaces on that computer.
- **QWorkspace**: This class represents a workspace on a QServer, it contains methods and properties to interact with a workspace as well as access the underlying QCues and QClient that is facilitating communication
## Todo: give a basic example of the workflow ## Todo: give a basic example of the workflow