diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 371d1ac..de4d4c5 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -15,11 +15,11 @@ jobs: working-directory: src/ steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 7.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -28,11 +28,11 @@ jobs: run: dotnet test --no-build --verbosity normal - name: Publish run: dotnet publish -c Release --no-build --verbosity normal - - # Uploade the published artifacts + + # Upload the published artifacts - name: Upload Publish Artifact - uses: actions/upload-artifact@v2.3.1 + uses: actions/upload-artifact@v4 with: name: SpyderTallyApp - path: src/SpyderTallyControllerWebApp/bin/Release/net7.0/publish/ + path: src/SpyderTallyControllerWebApp/bin/Release/net10.0/publish/ if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..da85bfa --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,81 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + rid: [linux-arm64, linux-arm] + defaults: + run: + working-directory: src/ + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Restore dependencies + run: dotnet restore -r ${{ matrix.rid }} + + - name: Publish self-contained single-file + run: | + dotnet publish SpyderTallyControllerWebApp/SpyderTallyControllerWebApp.csproj \ + -c Release \ + -r ${{ matrix.rid }} \ + --self-contained \ + -p:PublishSingleFile=true \ + -p:IncludeNativeLibrariesForSelfExtract=true \ + -o ../publish/${{ matrix.rid }} + + - name: Package release tarball + working-directory: ${{ github.workspace }} + run: | + STAGE_DIR="spyder-tally-${{ matrix.rid }}" + mkdir -p "$STAGE_DIR/bin" + + # Copy published binary + cp publish/${{ matrix.rid }}/SpyderTallyControllerWebApp "$STAGE_DIR/bin/" + + # Copy config and support files + cp docs/sd_card/install.sh "$STAGE_DIR/" + cp docs/sd_card/nginx.conf "$STAGE_DIR/" + cp docs/sd_card/SpyderTallies.service "$STAGE_DIR/" + cp docs/sd_card/appConfig.json "$STAGE_DIR/" + cp docs/sd_card/deviceConfig.json "$STAGE_DIR/" + + chmod +x "$STAGE_DIR/install.sh" + tar czf "spyder-tally-${{ matrix.rid }}.tar.gz" "$STAGE_DIR" + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: spyder-tally-${{ matrix.rid }} + path: spyder-tally-${{ matrix.rid }}.tar.gz + if-no-files-found: error + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + files: | + spyder-tally-linux-arm64/spyder-tally-linux-arm64.tar.gz + spyder-tally-linux-arm/spyder-tally-linux-arm.tar.gz diff --git a/.gitignore b/.gitignore index 0d55e56..3a132b9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ *.userprefs .vscode/ +.claude/ # Build results [Dd]ebug/ diff --git a/README.md b/README.md index d10ee18..3fc51c6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Hi there! This repository contains source code and information for building a T ## Key Features and Overview -* Works with Spyder 200 / 300 / X20 / X80 hardware +* Works with Spyder 200 / 300 / X20 / X80 / Spyder-S hardware * Works with every major release version of Spyder software * Supports different servers and rules per individual tally * Built-in web server for remote configuration and monitoring @@ -27,12 +27,12 @@ The video below walks through the device hardware and software, providing an ove Building your own Device ================= -All the instructions needed to get a device built and programmed are contained as a blog post on my Knighware.net website, and you can view that post [here](https://www.knighware.net/2021/10/03/building-a-spyder-tally-controller/). The post walks through the hardware needed to build your own device, the wiring for that hardware, and even configuring an SD card image containing the configured tally application. +All the instructions needed to get a device built and programmed are contained as a blog post on my Knighware.net website, and you can view that post [here](https://www.knightware.net/?p=4086). The post walks through the hardware needed to build your own device, the wiring for that hardware, and even configuring an SD card image containing the configured tally application. Some Useful Links ================= -* [Building a Spyder Tally Controller](https://www.knighware.net/2021/10/03/building-a-spyder-tally-controller/) - Blog post on knightware.net containing instructions for building your own device +* [Building a Spyder Tally Controller](https://www.knightware.net/?p=4086) - Blog post on knightware.net containing instructions for building your own device * [YouTube - Building a Spyder Tally Controller](https://youtu.be/8xSgcWn2_8I) - (Updated) Focuses on the updated hardware and software * [YouTube Video - Building a Spyder Tally Controller](https://youtu.be/mBM5LXhSECg) - (Original) Covers older version of Tally controller, but contains a lot of theory and background on Spyder tech that is still very relevant -* [Spyder Client Library](https://www.nuget.org/packages/SpyderClientLibrary/) - this is the library powering communication with Spyder; useful for all kinds of projects and software applications \ No newline at end of file +* [Spyder Client Library](https://www.nuget.org/packages/SpyderClientLibrary/) - this is the library powering communication with Spyder; useful for all kinds of projects and software applications diff --git a/docs/sd_card/SpyderTallies.service b/docs/sd_card/SpyderTallies.service index e160381..7a5756f 100644 --- a/docs/sd_card/SpyderTallies.service +++ b/docs/sd_card/SpyderTallies.service @@ -3,28 +3,19 @@ Description=Spyder Tally application by Derek Smithson (Knightware) After=network.target [Service] -# systemd will run this executable to start the service -# if /usr/bin/dotnet doesn't work, use `which dotnet` to find correct dotnet executable path -WorkingDirectory=/home/dsmithson/app -ExecStart=/home/dsmithson/.dotnet/dotnet /home/dsmithson/app/SpyderTallyControllerWebApp.dll +WorkingDirectory=/opt/spyder-tally +ExecStart=/opt/spyder-tally/SpyderTallyControllerWebApp # to query logs using journalctl, set a logical name here SyslogIdentifier=SpyderTallies -# Use your username to keep things simple. -# If you pick a different user, make sure dotnet and all permissions are set correctly to run the app -# To update permissions, use 'chown yourusername -R /srv/HelloWorld' to take ownership of the folder and files, -# Use 'chmod +x /srv/HelloWorld/HelloWorld' to allow execution of the executable file -User=dsmithson +# The install script will replace __USER__ with the installing user +User=__USER__ # ensure the service restarts after crashing Restart=always -# amount of time to wait before restarting the service +# amount of time to wait before restarting the service RestartSec=5 -# This environment variable is necessary when dotnet isn't loaded for the specified user. -# To figure out this value, run 'env | grep DOTNET_ROOT' when dotnet has been loaded into your shell. -Environment=DOTNET_ROOT=/home/dsmithson/.dotnet - [Install] WantedBy=multi-user.target diff --git a/docs/sd_card/install.sh b/docs/sd_card/install.sh new file mode 100644 index 0000000..291e660 --- /dev/null +++ b/docs/sd_card/install.sh @@ -0,0 +1,137 @@ +#!/bin/bash +set -euo pipefail + +# Spyder Tally Controller - Install Script +# This script installs the Spyder Tally Controller application on a Raspberry Pi. +# It auto-detects architecture (arm64 vs arm32) and installs the correct binary. +# +# Usage: +# tar xzf spyder-tally-linux-arm64.tar.gz # (or linux-arm) +# cd spyder-tally-linux-arm64/ # (or linux-arm) +# sudo ./install.sh + +INSTALL_DIR="/opt/spyder-tally" +SERVICE_NAME="SpyderTallies" + +# --- Preflight checks --- + +if [ "$(id -u)" -ne 0 ]; then + echo "Error: This script must be run as root (use sudo ./install.sh)" + exit 1 +fi + +# Determine the real user who invoked sudo +INSTALL_USER="${SUDO_USER:-$(whoami)}" +if [ "$INSTALL_USER" = "root" ]; then + echo "Error: Please run this script with sudo from a regular user account, not as root directly." + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# --- Architecture detection --- + +ARCH="$(uname -m)" +case "$ARCH" in + aarch64) + EXPECTED_RID="linux-arm64" + ;; + armv7l|armv6l) + EXPECTED_RID="linux-arm" + ;; + *) + echo "Error: Unsupported architecture '$ARCH'. Expected aarch64 (arm64) or armv7l/armv6l (arm32)." + exit 1 + ;; +esac + +# Check that the binary in this package matches the detected architecture +if [ ! -f "$SCRIPT_DIR/bin/SpyderTallyControllerWebApp" ]; then + echo "Error: Binary not found at $SCRIPT_DIR/bin/SpyderTallyControllerWebApp" + echo "" + echo "Detected architecture: $ARCH ($EXPECTED_RID)" + echo "Make sure you downloaded the correct package for your Pi:" + echo " - Raspberry Pi 4/5 (64-bit OS): spyder-tally-linux-arm64.tar.gz" + echo " - Raspberry Pi 3 or older (32-bit OS): spyder-tally-linux-arm.tar.gz" + exit 1 +fi + +echo "=== Spyder Tally Controller Installer ===" +echo "Detected architecture: $ARCH ($EXPECTED_RID)" +echo "Installing as user: $INSTALL_USER" +echo "Install directory: $INSTALL_DIR" +echo "" + +# --- Stop existing service if running --- + +if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then + echo "Stopping existing $SERVICE_NAME service..." + systemctl stop "$SERVICE_NAME" +fi + +# --- Install nginx --- + +echo "Installing nginx..." +apt-get update -qq +apt-get install -y -qq nginx > /dev/null + +# --- Enable I2C --- + +echo "Enabling I2C interface..." +if command -v raspi-config &> /dev/null; then + raspi-config nonint do_i2c 0 +else + echo " Warning: raspi-config not found - skipping I2C setup." + echo " You may need to enable I2C manually if not already enabled." +fi + +# --- Install application --- + +echo "Installing application to $INSTALL_DIR..." +mkdir -p "$INSTALL_DIR" + +# Copy binary +cp "$SCRIPT_DIR/bin/SpyderTallyControllerWebApp" "$INSTALL_DIR/" +chmod +x "$INSTALL_DIR/SpyderTallyControllerWebApp" + +# Copy default config files (only if they don't already exist, to preserve user settings on upgrade) +for config_file in appConfig.json deviceConfig.json; do + if [ ! -f "$INSTALL_DIR/$config_file" ]; then + cp "$SCRIPT_DIR/$config_file" "$INSTALL_DIR/" + echo " Created default $config_file" + else + echo " Keeping existing $config_file (not overwriting)" + fi +done + +# Set ownership so the service user can write config files +chown -R "$INSTALL_USER":"$INSTALL_USER" "$INSTALL_DIR" + +# --- Install nginx config --- + +echo "Installing nginx configuration..." +cp "$SCRIPT_DIR/nginx.conf" /etc/nginx/nginx.conf +nginx -t -q +systemctl restart nginx +systemctl enable nginx + +# --- Install systemd service --- + +echo "Installing systemd service..." +sed "s/__USER__/$INSTALL_USER/g" "$SCRIPT_DIR/SpyderTallies.service" > /etc/systemd/system/SpyderTallies.service +systemctl daemon-reload +systemctl enable "$SERVICE_NAME" +systemctl start "$SERVICE_NAME" + +# --- Done --- + +echo "" +echo "=== Installation complete! ===" +echo "" +echo "The Spyder Tally Controller is now running." +echo " Web UI: http://$(hostname -I | awk '{print $1}')" +echo "" +echo "Useful commands:" +echo " sudo systemctl status $SERVICE_NAME # Check service status" +echo " sudo systemctl restart $SERVICE_NAME # Restart the service" +echo " sudo journalctl -u $SERVICE_NAME -f # Tail the log" diff --git a/docs/sd_card/nginx.conf b/docs/sd_card/nginx.conf index 55e0b3d..4f8839e 100644 --- a/docs/sd_card/nginx.conf +++ b/docs/sd_card/nginx.conf @@ -20,28 +20,25 @@ http { ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE ssl_prefer_server_ciphers on; - # Logging Settings - # access_log /var/log/nginx/access.log; - # error_log /var/log/nginx/error.log; - # Gzip Settings gzip on; - # gzip_vary on; - # gzip_proxied any; - # gzip_comp_level 6; - # gzip_buffers 16 8k; - # gzip_http_version 1.1; - # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; - - server { - listen 80 default_server; - location / { - proxy_pass http://127.0.0.1:5000; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Host $host; - } - } + # WebSocket support (required for SignalR) + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + server { + listen 80 default_server; + location / { + proxy_pass http://127.0.0.1:5000; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + } } - diff --git a/src/SpyderTallyControllerLinux.sln b/src/SpyderTallyControllerLinux.sln index 6388671..59d1307 100644 --- a/src/SpyderTallyControllerLinux.sln +++ b/src/SpyderTallyControllerLinux.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.32014.148 +# Visual Studio Version 18 +VisualStudioVersion = 18.2.11408.102 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpyderTallyControllerWebApp", "SpyderTallyControllerWebApp\SpyderTallyControllerWebApp.csproj", "{1E5F6F1B-9473-4A89-A69D-5014519E8002}" EndProject diff --git a/src/SpyderTallyControllerWebApp/Controllers/ConfigurationController.cs b/src/SpyderTallyControllerWebApp/Controllers/ConfigurationController.cs index b95a09f..49af99d 100644 --- a/src/SpyderTallyControllerWebApp/Controllers/ConfigurationController.cs +++ b/src/SpyderTallyControllerWebApp/Controllers/ConfigurationController.cs @@ -99,5 +99,24 @@ private void ValidateModel(ConfigurationViewModel vm) (TallyMode.ForceOff, "Force Off") }; } + + [HttpGet] + public async Task GetServers() + { + var servers = await spyderRepository.GetServersAsync(); + return Json(servers); + } + + [HttpGet] + public async Task GetSources(string serverIP) + { + if (string.IsNullOrWhiteSpace(serverIP)) + { + return Json(new List()); + } + + var sources = await spyderRepository.GetSourcesAsync(serverIP); + return Json(sources); + } } } diff --git a/src/SpyderTallyControllerWebApp/Hubs/TallyHub.cs b/src/SpyderTallyControllerWebApp/Hubs/TallyHub.cs new file mode 100644 index 0000000..bbe8df1 --- /dev/null +++ b/src/SpyderTallyControllerWebApp/Hubs/TallyHub.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.SignalR; +using SpyderTallyControllerWebApp.Models; + +namespace SpyderTallyControllerWebApp.Hubs +{ + public class TallyHub : Hub + { + public const string TallyStatusUpdatedMessage = "TallyStatusUpdated"; + + private readonly IRelayRepository _relayRepository; + + public TallyHub(IRelayRepository relayRepository) + { + _relayRepository = relayRepository; + } + + public override async Task OnConnectedAsync() + { + // Send current tally status to newly connected clients + var currentStatus = _relayRepository.GetRelayStatus(); + await Clients.Caller.SendAsync(TallyStatusUpdatedMessage, currentStatus); + await base.OnConnectedAsync(); + } + } +} diff --git a/src/SpyderTallyControllerWebApp/Models/DisplayRepository.cs b/src/SpyderTallyControllerWebApp/Models/DisplayRepository.cs index 3954026..21d3503 100644 --- a/src/SpyderTallyControllerWebApp/Models/DisplayRepository.cs +++ b/src/SpyderTallyControllerWebApp/Models/DisplayRepository.cs @@ -43,7 +43,7 @@ public DisplayRepository(IRelayRepository relayRepository) backlightPin: 3, backlightBrightness: 0.1f, readWritePin: 1, - controller: new GpioController(PinNumberingScheme.Logical, driver)); + controller: new GpioController(driver) /* PinNumberingScheme.Logical */); } catch(Exception ex) { diff --git a/src/SpyderTallyControllerWebApp/Models/ISpyderRepository.cs b/src/SpyderTallyControllerWebApp/Models/ISpyderRepository.cs index 294e32c..b7fe768 100644 --- a/src/SpyderTallyControllerWebApp/Models/ISpyderRepository.cs +++ b/src/SpyderTallyControllerWebApp/Models/ISpyderRepository.cs @@ -1,7 +1,12 @@ -namespace SpyderTallyControllerWebApp.Models +using Spyder.Client.Net.DrawingData.Deserializers; +using Spyder.Client.Net.Notifications; + +namespace SpyderTallyControllerWebApp.Models { public interface ISpyderRepository { + event DrawingDataReceivedHandler DrawingDataReceived; + Task> GetServersAsync(); Task> GetSourcesAsync(string serverIP); diff --git a/src/SpyderTallyControllerWebApp/Models/RelayRepository.cs b/src/SpyderTallyControllerWebApp/Models/RelayRepository.cs index d2af692..f43e4e7 100644 --- a/src/SpyderTallyControllerWebApp/Models/RelayRepository.cs +++ b/src/SpyderTallyControllerWebApp/Models/RelayRepository.cs @@ -66,7 +66,8 @@ public void SetRelayStatus(int relayIndex, bool value) if(deviceConfiguration.TallyGpioPinAssignments.TryGetValue(relayIndex, out int pinAssignment)) { //Write hardware - gpioController.Write(pinAssignment, value ? PinValue.High : PinValue.Low); + if(gpioController != null) + gpioController.Write(pinAssignment, value ? PinValue.High : PinValue.Low); //Update internal state and fire event relayStatus[relayIndex] = value; diff --git a/src/SpyderTallyControllerWebApp/Models/SpyderRepository.cs b/src/SpyderTallyControllerWebApp/Models/SpyderRepository.cs index aac23ba..c09a653 100644 --- a/src/SpyderTallyControllerWebApp/Models/SpyderRepository.cs +++ b/src/SpyderTallyControllerWebApp/Models/SpyderRepository.cs @@ -1,39 +1,89 @@ -using Spyder.Client; -using System.Linq; - -namespace SpyderTallyControllerWebApp.Models -{ - public class SpyderRepository : ISpyderRepository - { - private readonly SpyderClientManager spyderManager; - - public SpyderRepository(SpyderClientManager spyderManager, IConfigurationRepository configurationRepository) - { - this.spyderManager = spyderManager; - } - - public async Task> GetServersAsync() - { - var servers = await spyderManager.GetServers(); - if (servers == null) - return new List(); - - return servers - .Select(s => s.ServerIP) - .ToList(); - } - - public async Task> GetSourcesAsync(string serverIP) - { - var server = await spyderManager.GetServerAsync(serverIP); - var sources = await server?.GetSources(); - if (sources == null) - return new List(); - - return sources - .Where(s => !string.IsNullOrWhiteSpace(s?.Name)) - .Select(s => s.Name) - .ToList(); - } - } +using Spyder.Client; +using Spyder.Client.Common; +using Spyder.Client.Net; +using Spyder.Client.Net.DrawingData.Deserializers; +using Spyder.Client.Net.Notifications; +using System.Linq; + +namespace SpyderTallyControllerWebApp.Models +{ + public class SpyderRepository : ISpyderRepository + { + private readonly SpyderServerEventListener serverEventListener; + private readonly HashSet servers = new HashSet(); + private readonly object serversLock = new object(); + + public event DrawingDataReceivedHandler DrawingDataReceived; + + public SpyderRepository(SpyderServerEventListener serverEventListener) + { + this.serverEventListener = serverEventListener; + this.serverEventListener.DrawingDataThrottleInterval = TimeSpan.FromMilliseconds(100); + this.serverEventListener.ServerAnnounceMessageReceived += ServerEventListener_ServerAnnounceMessageReceived; + this.serverEventListener.DrawingDataReceived += ServerEventListener_DrawingDataReceived; + } + + private void ServerEventListener_DrawingDataReceived(object sender, DrawingDataReceivedEventArgs e) + { + DrawingDataReceived?.Invoke(this, e); + } + + private void ServerEventListener_ServerAnnounceMessageReceived(object sender, SpyderServerAnnounceInformation serverInfo) + { + lock (serversLock) + { + servers.Add(serverInfo.Address); // HashSet.Add handles duplicates automatically + } + } + + public Task> GetServersAsync() + { + lock (serversLock) + { + return Task.FromResult(servers.ToList()); + } + } + + public async Task> GetSourcesAsync(string serverIP) + { + var timeout = TimeSpan.FromSeconds(2); + + try + { + var task = GetSourcesInternalAsync(serverIP); + return await task.WaitAsync(timeout); + } + catch (TimeoutException) + { + return []; + } + catch (Exception) + { + return []; + } + } + + private async Task> GetSourcesInternalAsync(string serverIP) + { + var client = new SpyderUdpClient(HardwareType.Spyder300, serverIP); + try + { + if (await client.StartupAsync()) + { + var sources = await client.GetSources(); + return sources?.Select(s => s.Name).ToList() ?? []; + } + return []; + } + finally + { + // Fire and forget shutdown to avoid blocking on cleanup + _ = Task.Run(async () => + { + try { await client.ShutdownAsync(); } + catch { /* ignore cleanup errors */ } + }); + } + } + } } diff --git a/src/SpyderTallyControllerWebApp/Models/SystemHealthRepository.cs b/src/SpyderTallyControllerWebApp/Models/SystemHealthRepository.cs index 23d9698..e671ff4 100644 --- a/src/SpyderTallyControllerWebApp/Models/SystemHealthRepository.cs +++ b/src/SpyderTallyControllerWebApp/Models/SystemHealthRepository.cs @@ -95,15 +95,26 @@ private string SplitAndGetValue(string fullText, string prefix, string delimiter private async Task RunProcessAndGetLine(string processName, string args = null) { - Process process = Process.Start(processName, args); - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.CreateNoWindow = true; - process.Start(); + //Sanity check on process + if (!OperatingSystem.IsLinux()) + return null; + + try + { + Process process = Process.Start(processName, args); + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.CreateNoWindow = true; + process.Start(); - string response = await process.StandardOutput.ReadToEndAsync(); - process.WaitForExit(); - return response; + string response = await process.StandardOutput.ReadToEndAsync(); + process.WaitForExit(); + return response; + } + catch(Exception ex) + { + return $"Error: {ex.Message}"; + } } } } diff --git a/src/SpyderTallyControllerWebApp/Program.cs b/src/SpyderTallyControllerWebApp/Program.cs index f316f5f..8518489 100644 --- a/src/SpyderTallyControllerWebApp/Program.cs +++ b/src/SpyderTallyControllerWebApp/Program.cs @@ -1,20 +1,17 @@ +using Spyder.Client.Net.Notifications; using SpyderTallyControllerWebApp; +using SpyderTallyControllerWebApp.Hubs; using SpyderTallyControllerWebApp.Models; +using SpyderTallyControllerWebApp.Services; -var spyderManager = new Spyder.Client.SpyderClientManager(); -spyderManager.ServerListChanged += async (s, e) => -{ - foreach (var server in (await spyderManager.GetServers())) - server.DrawingDataThrottleInterval = TimeSpan.FromMilliseconds(100); -}; -await spyderManager.StartupAsync(); +var serverEventListener = await SpyderServerEventListener.GetInstanceAsync(); var builder = WebApplication.CreateBuilder(args); //Allow access on outside adapters builder.WebHost.UseUrls("http://*:5000"); -builder.Services.AddSingleton(spyderManager); +builder.Services.AddSingleton(serverEventListener); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -25,6 +22,8 @@ // Add services to the container. builder.Services.AddControllersWithViews(); +builder.Services.AddSignalR(); +builder.Services.AddHostedService(); var app = builder.Build(); @@ -51,4 +50,6 @@ name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); +app.MapHub("/tallyHub"); + app.Run(); diff --git a/src/SpyderTallyControllerWebApp/Properties/launchSettings.json b/src/SpyderTallyControllerWebApp/Properties/launchSettings.json index 0e14b75..3e80763 100644 --- a/src/SpyderTallyControllerWebApp/Properties/launchSettings.json +++ b/src/SpyderTallyControllerWebApp/Properties/launchSettings.json @@ -1,21 +1,13 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:55543", - "sslPort": 44383 - } - }, +{ "profiles": { "SpyderTallyControllerWebApp": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:7253;http://localhost:5253", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7253;http://localhost:5253" }, "IIS Express": { "commandName": "IISExpress", @@ -23,6 +15,24 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "WSL": { + "commandName": "WSL2", + "launchBrowser": true, + "launchUrl": "https://localhost:7253", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "https://localhost:7253;http://localhost:5253" + }, + "distributionName": "" + } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55543", + "sslPort": 44383 } } -} +} \ No newline at end of file diff --git a/src/SpyderTallyControllerWebApp/Services/TallyStatusBroadcaster.cs b/src/SpyderTallyControllerWebApp/Services/TallyStatusBroadcaster.cs new file mode 100644 index 0000000..8d6113d --- /dev/null +++ b/src/SpyderTallyControllerWebApp/Services/TallyStatusBroadcaster.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.SignalR; +using SpyderTallyControllerWebApp.Hubs; +using SpyderTallyControllerWebApp.Models; + +namespace SpyderTallyControllerWebApp.Services +{ + public class TallyStatusBroadcaster : IHostedService + { + private readonly IRelayRepository _relayRepository; + private readonly IHubContext _hubContext; + + public TallyStatusBroadcaster(IRelayRepository relayRepository, IHubContext hubContext) + { + _relayRepository = relayRepository; + _hubContext = hubContext; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _relayRepository.RelayStatusChanged += OnRelayStatusChanged; + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _relayRepository.RelayStatusChanged -= OnRelayStatusChanged; + return Task.CompletedTask; + } + + private async void OnRelayStatusChanged(object sender, EventArgs e) + { + var currentStatus = _relayRepository.GetRelayStatus(); + await _hubContext.Clients.All.SendAsync(TallyHub.TallyStatusUpdatedMessage, currentStatus); + } + } +} diff --git a/src/SpyderTallyControllerWebApp/SpyderTallyControllerWebApp.csproj b/src/SpyderTallyControllerWebApp/SpyderTallyControllerWebApp.csproj index df7aea3..7763230 100644 --- a/src/SpyderTallyControllerWebApp/SpyderTallyControllerWebApp.csproj +++ b/src/SpyderTallyControllerWebApp/SpyderTallyControllerWebApp.csproj @@ -1,10 +1,11 @@ - + - net7.0 + net10.0 disable enable 1.0.1 + 8a5e8aab-906e-4fba-b610-4fc17b1f8b66 @@ -12,10 +13,9 @@ - - - - + + + diff --git a/src/SpyderTallyControllerWebApp/SpyderTallyEngine.cs b/src/SpyderTallyControllerWebApp/SpyderTallyEngine.cs index 6387141..3448e43 100644 --- a/src/SpyderTallyControllerWebApp/SpyderTallyEngine.cs +++ b/src/SpyderTallyControllerWebApp/SpyderTallyEngine.cs @@ -9,7 +9,7 @@ namespace SpyderTallyControllerWebApp public class SpyderTallyEngine { private readonly IConfigurationRepository configurationRepository; - private readonly SpyderClientManager spyderManager; + private readonly ISpyderRepository spyderRepository; private readonly IRelayRepository relayRepository; private readonly TallyDeviceConfiguration deviceConfig; @@ -17,7 +17,7 @@ public class SpyderTallyEngine public SpyderTallyEngine(IConfigurationRepository configurationRepository, IRelayRepository relayRepository, - SpyderClientManager spyderManager) + ISpyderRepository spyderRepository) { this.configurationRepository = configurationRepository; this.appConfig = configurationRepository.GetTallyAppConfiguration(); @@ -26,9 +26,8 @@ public SpyderTallyEngine(IConfigurationRepository configurationRepository, this.relayRepository = relayRepository; - this.spyderManager = spyderManager; - spyderManager.RaiseDrawingDataChanged = true; - spyderManager.DrawingDataReceived += SpyderManager_DrawingDataReceived; + this.spyderRepository = spyderRepository; + spyderRepository.DrawingDataReceived += SpyderRepository_DrawingDataReceived; } private void ConfigurationRepository_TallyAppConfigurationChanged(object sender, TallyAppConfiguration e) @@ -38,7 +37,7 @@ private void ConfigurationRepository_TallyAppConfigurationChanged(object sender, UpdateTallies(null, null); } - private void SpyderManager_DrawingDataReceived(object sender, DrawingDataReceivedEventArgs e) + private void SpyderRepository_DrawingDataReceived(object sender, DrawingDataReceivedEventArgs e) { UpdateTallies(e.ServerIP, e.DrawingData); } diff --git a/src/SpyderTallyControllerWebApp/Views/Configuration/Index.cshtml b/src/SpyderTallyControllerWebApp/Views/Configuration/Index.cshtml index edb46ef..3b9c2c8 100644 --- a/src/SpyderTallyControllerWebApp/Views/Configuration/Index.cshtml +++ b/src/SpyderTallyControllerWebApp/Views/Configuration/Index.cshtml @@ -1,62 +1,338 @@ -@model ConfigurationViewModel -@{ - ViewData["Title"] = "Configuration"; -} - -
- -
- -
-

Tally Configuration

- - - - - - - - - - - @foreach (var tally in Model.TallyConfigurations) - { - - - - - - - - - - } - +@model ConfigurationViewModel +@{ + ViewData["Title"] = "Configuration"; +} + + + + + +
+ +
+

Tally Configuration

+
Relay IndexSpyder ServerSpyder SourceMode
- - @tally.TallyIndex - -
- - -
-
-
- - -
-
-
- - -
-
+ + + + + + + + + + @foreach (var tally in Model.TallyConfigurations) + { + + + + + + + + + + } + + +
Relay IndexSpyder ServerSpyder SourceMode
+ + @tally.TallyIndex + +
+
+ + +
+ +
+
+
+
+ + +
+ + @if (!string.IsNullOrEmpty(tally.SpyderServerIP) && Model.SourcesByServer.TryGetValue(tally.SpyderServerIP, out var sources)) + { + @foreach (var source in sources) + { + + } + } + + +
+
+
+ + +
+
+
+ + + @foreach (var server in Model.AvailableServers) + { + + } + + +
+
+ +
+
+
- +
+

Display Settings

+
+ +
+
-
-
- -
-
- +@section Scripts { + +} diff --git a/src/SpyderTallyControllerWebApp/Views/Home/Index.cshtml b/src/SpyderTallyControllerWebApp/Views/Home/Index.cshtml index 8b869da..e49a011 100644 --- a/src/SpyderTallyControllerWebApp/Views/Home/Index.cshtml +++ b/src/SpyderTallyControllerWebApp/Views/Home/Index.cshtml @@ -7,9 +7,9 @@

Current Status

- @foreach (bool tallyStatus in Model.CurrentTallyStatus) + @for (int i = 0; i < Model.CurrentTallyStatus.Length; i++) { - + }
@@ -24,3 +24,25 @@
Disk: @Model.DiskUsage
HW Model: @Model.Model
+ +@section Scripts { + +} diff --git a/src/SpyderTallyControllerWebApp/Views/Shared/_Layout.cshtml b/src/SpyderTallyControllerWebApp/Views/Shared/_Layout.cshtml index ac3bef8..5419cd4 100644 --- a/src/SpyderTallyControllerWebApp/Views/Shared/_Layout.cshtml +++ b/src/SpyderTallyControllerWebApp/Views/Shared/_Layout.cshtml @@ -4,6 +4,13 @@ @ViewData["Title"] - Spyder Tallies + + @@ -12,7 +19,10 @@