diff --git a/.gitignore b/.gitignore index 0969583..3994162 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,11 @@ build/ bld/ [Bb]in/ [Oo]bj/ +sdBuild/ +# Build app local files +SpyderTallyApp.zip +config.txt # Visual Studo 2015 cache/options directory .vs/ diff --git a/docs/Linux/README.md b/docs/Linux/README.md new file mode 100644 index 0000000..9fe63ea --- /dev/null +++ b/docs/Linux/README.md @@ -0,0 +1,40 @@ +# Building the Linux application + +This document is mostly quick notes to help me remember how to build a disk image for the Raspbery pi. I have high hopes to get this image build process into a script that will run as a Github action when a new release branch is created, but I'm not there yet. + +## Stuff to install on a new Raspian Image +- dotnet ([Scripted Install](https://learn.microsoft.com/en-us/dotnet/core/install/linux-scripted-manual#scripted-install)) +- nginx (apt-get package install) +- git (sort of optional, depending on whether or not you clone the repo) +- From Raspi-Config + - Enable I2C + - Enable SSH + +## Files to copy over from this repo +- nginx.conf (copy to /etc/nginx/nginx.conf) +- SpyderTallies.service (copy to /etc/systemd/system/SpyderTallies.service) + +## Installing the app +- Clone the git repo: `git clone https://github.com/dsmithson/SpyderTallyController.git` +- cd to the '~/git/SpyderTallyController/src/Linux/SpyderTallyControllerLinux' folder (assuming you cloned into the ~/git folder) +- run `mkdir ~/app` to create a place to publish +- run `dotnet publish -p:PublishDir=/home/dsmithson/app -c Release ./SpyderTallyControllerLinux.sln` to build in release and publish to the ~/app directory + +## Installing the app as a service +- Edit SpyderTallies.service as needed (in repo username is dsmithson at the time of this writing) +- `sudo cp SpyderTallies.service /etc/systemd/system/SpyderTallies.service` +- `sudo systemctl daemon-reload` +- (Make sure the app is running with `sudo systemctl status SpyderTallies`) +- `sudo systemctl enable SpyderTallies` + +As needed, run `sudo systemctl [stop|start|restart] SpyderTallies` to manage service. + +Also as needed, run `sudo journalctl -u SpyderTallies -f` to tail the service output log for troubleshooting purposes. + +## Cleaning up before/during building an SD card image after configuring +- Delete SSH keys (if added to edit/push code) +- Make sure your user password is 'spyder' +- Make sure the app starts automatically on reboot before sealing an image +- Delete, or at least clean, the repo cloned (if done) to save space +- DD clone the disk image +- Run [PiShrink](https://github.com/Drewsif/PiShrink) to shrink down the image size diff --git a/docs/Linux/SpyderTallies.service b/docs/Linux/SpyderTallies.service new file mode 100755 index 0000000..e160381 --- /dev/null +++ b/docs/Linux/SpyderTallies.service @@ -0,0 +1,30 @@ +[Unit] +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 + +# 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 + +# ensure the service restarts after crashing +Restart=always +# 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/UI Sandbox.vsdx b/docs/UI Sandbox.vsdx new file mode 100644 index 0000000..f9fa969 Binary files /dev/null and b/docs/UI Sandbox.vsdx differ diff --git a/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerLinux.sln b/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerLinux.sln index 6388671..f3188f6 100644 --- a/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerLinux.sln +++ b/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerLinux.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpyderTallyControllerWebApp", "SpyderTallyControllerWebApp\SpyderTallyControllerWebApp.csproj", "{1E5F6F1B-9473-4A89-A69D-5014519E8002}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpyderTallyControllerWebApp", "SpyderTallyControllerWebApp\SpyderTallyControllerWebApp.csproj", "{1E5F6F1B-9473-4A89-A69D-5014519E8002}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C6C53870-AD2A-4954-9991-5A70E45E3B7D}" ProjectSection(SolutionItems) = preProject @@ -13,13 +13,19 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|ARM32 = Debug|ARM32 Release|Any CPU = Release|Any CPU + Release|ARM32 = Release|ARM32 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {1E5F6F1B-9473-4A89-A69D-5014519E8002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1E5F6F1B-9473-4A89-A69D-5014519E8002}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E5F6F1B-9473-4A89-A69D-5014519E8002}.Debug|ARM32.ActiveCfg = Debug|ARM32 + {1E5F6F1B-9473-4A89-A69D-5014519E8002}.Debug|ARM32.Build.0 = Debug|ARM32 {1E5F6F1B-9473-4A89-A69D-5014519E8002}.Release|Any CPU.ActiveCfg = Release|Any CPU {1E5F6F1B-9473-4A89-A69D-5014519E8002}.Release|Any CPU.Build.0 = Release|Any CPU + {1E5F6F1B-9473-4A89-A69D-5014519E8002}.Release|ARM32.ActiveCfg = Release|ARM32 + {1E5F6F1B-9473-4A89-A69D-5014519E8002}.Release|ARM32.Build.0 = Release|ARM32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Models/ConfigurationRepository.cs b/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Models/ConfigurationRepository.cs index 13632e7..1b17701 100644 --- a/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Models/ConfigurationRepository.cs +++ b/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Models/ConfigurationRepository.cs @@ -52,7 +52,18 @@ public ConfigurationRepository() private string GetFullFilePath(string fileName) { - return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); + string configPath = AppDomain.CurrentDomain.BaseDirectory; + + //Look to see if a path is specified in the app args, in which case look there first for the file + string[] args = Environment.GetCommandLineArgs(); + if(args != null && args.Length > 0) + { + if(Directory.Exists(args[0])) + { + configPath = args[0]; + } + } + return Path.Combine(configPath, fileName); } private T Load(string fileName) @@ -79,7 +90,7 @@ private T Load(string fileName) private void Save(string fileName, T value) { - string configFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); + string configFile = GetFullFilePath(fileName); using var stream = File.Create(configFile); JsonSerializer.Serialize(stream, value, new JsonSerializerOptions() { @@ -93,7 +104,7 @@ private void Save(string fileName, T value) private async Task SaveAsync(string fileName, T value) { - string configFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); + string configFile = GetFullFilePath(fileName); using var stream = File.Create(configFile); await JsonSerializer.SerializeAsync(stream, value, new JsonSerializerOptions() { diff --git a/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Models/DisplayRepository.cs b/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Models/DisplayRepository.cs index 69e77b5..6e05104 100644 --- a/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Models/DisplayRepository.cs +++ b/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Models/DisplayRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Device.Gpio; using System.Device.I2c; using System.Threading; @@ -10,8 +10,11 @@ namespace SpyderTallyControllerWebApp.Models { public class DisplayRepository : IDisplayRepository { + private const int displayWidth = 16; + private const int filledCircleLocation = 0; private const int emptyCircleLocation = 1; + private const int blinkIconLocation = 2; private readonly I2cDevice i2c; private readonly Pcf8574 driver; @@ -19,10 +22,14 @@ public class DisplayRepository : IDisplayRepository private readonly IRelayRepository relayRepository; + private readonly object displayLock = new object(); private DisplayMode displayMode = DisplayMode.Normal; private string manualTextLine1 = ""; private string manualTextLine2 = ""; + private bool isBlinkIconOn; + private Timer blinkTimer; + public DisplayRepository(IRelayRepository relayRepository) { this.relayRepository = relayRepository; @@ -42,34 +49,49 @@ public DisplayRepository(IRelayRepository relayRepository) //Create filled 0 charcater byte[] filledCircle = new byte[] { + 0x00, // 00000 + 0x00, // 00000 0x0E, // 0XXX0 0x1F, // XXXXX 0x1F, // XXXXX 0x1F, // XXXXX - 0x1F, // XXXXX - 0x1F, // XXXXX - 0x1F, // XXXXX 0x0E, // 0XXX0 + 0x00, // 00000 }; lcd.CreateCustomCharacter(filledCircleLocation, filledCircle); byte[] emptyCircle = new byte[] { + 0x00, // 00000 + 0x00, // 00000 0x0E, // 0XXX0 0x11, // X000X 0x11, // X000X 0x11, // X000X - 0x11, // X000X - 0x11, // X000X - 0x11, // X000X 0x0E, // 0XXX0 + 0x00, // 00000 }; lcd.CreateCustomCharacter(emptyCircleLocation, emptyCircle); + byte[] blinkIcon = new byte[] + { + 0x00, // 00000 + 0x00, // 00000 + 0x00, // 00000 + 0x06, // 00XX0 + 0x06, // 00XX0 + 0x00, // 00000 + 0x00, // 00000 + 0x00, // 00000 + }; + lcd.CreateCustomCharacter(blinkIconLocation, blinkIcon); + lcd.Clear(); lcd.DisplayOn = true; UpdateDisplay(); + + blinkTimer = new Timer(OnBlinkTimer, null, TimeSpan.FromSeconds(0.8), TimeSpan.FromSeconds(0.8)); } private void RelayRepository_RelayStatusChanged(object sender, EventArgs e) @@ -95,31 +117,51 @@ public void SetText(string line1, string line2) UpdateDisplay(); } - public void UpdateDisplay() + public void UpdateDisplay(bool blinkCursorOnly = false) { - string text1, text2; - if (displayMode == DisplayMode.Normal) - { - text1 = Dns.GetHostAddresses(Dns.GetHostName()) - .Where(ip => ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork && ip.GetAddressBytes()[0] != 127) - .FirstOrDefault()? - .ToString() ?? ""; - - text2 = String.Join(null, relayRepository.GetRelayStatus().Select(isOn => isOn ? (char)filledCircleLocation : (char)emptyCircleLocation)); - - //Debug - Console.WriteLine(string.Join(Environment.NewLine, Dns.GetHostAddresses(Dns.GetHostName()).ToList())); - } - else + lock (displayLock) { - text1 = manualTextLine1; - text2 = manualTextLine2; - } - lcd.SetCursorPosition(0, 0); - lcd.Write(text1); + if (!blinkCursorOnly) + { + string text1, text2; + if (displayMode == DisplayMode.Normal) + { + text1 = Dns.GetHostAddresses(Dns.GetHostName()) + .Where(ip => ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork && ip.GetAddressBytes()[0] != 127) + .FirstOrDefault()? + .ToString() ?? ""; + + var relays = relayRepository.GetRelayStatus(); + text2 = String.Join(null, relays.Select(isOn => isOn ? (char)filledCircleLocation : (char)emptyCircleLocation)) + .PadLeft((displayWidth + relays.Length) / 2); + } + else + { + text1 = manualTextLine1; + text2 = manualTextLine2; + } + + lcd.SetCursorPosition(0, 0); + lcd.Write(text1); + + lcd.SetCursorPosition(0, 1); + lcd.Write(text2); + } + + //Update blink icon + lcd.SetCursorPosition(displayWidth - 1, 0); + if (isBlinkIconOn) + lcd.Write(new char[] { (char)blinkIconLocation }); + else + lcd.Write(" "); + } + } - lcd.SetCursorPosition(0, 1); - lcd.Write(text2); + private void OnBlinkTimer(object state) + { + //Toggle blink + isBlinkIconOn = !isBlinkIconOn; + UpdateDisplay(true); } } } diff --git a/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Program.cs b/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Program.cs index 48c2052..f316f5f 100644 --- a/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Program.cs +++ b/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Program.cs @@ -2,6 +2,11 @@ using SpyderTallyControllerWebApp.Models; 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 builder = WebApplication.CreateBuilder(args); diff --git a/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Views/Home/Privacy.cshtml b/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Views/Home/Privacy.cshtml index 2479fb7..16c6924 100644 --- a/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Views/Home/Privacy.cshtml +++ b/src/Linux/SpyderTallyControllerLinux/SpyderTallyControllerWebApp/Views/Home/Privacy.cshtml @@ -1,6 +1,10 @@ -@{ +@{ ViewData["Title"] = "Privacy Policy"; }

@ViewData["Title"]

-

Use this page to detail your site's privacy policy.

+

This device assumes no internet connectivity and is designed to work in entirely offline environments. It does not collect or transmit personal or any other kind of information.

+ +

+ Newer updates to this privacy policy, or the software on this device can be found by visiting https://github.com/dsmithson/SpyderTallyController. +

diff --git a/src/Linux/build-sd-card.sh b/src/Linux/build-sd-card.sh new file mode 100644 index 0000000..59f1cfe --- /dev/null +++ b/src/Linux/build-sd-card.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +# Download the latest Raspberry pi lite image +# Instructions from https://geoffhudik.com/tech/2020/04/27/scripting-raspberry-pi-image-builds/ +loop_1_device=/dev/loop2p1 +loop_2_device=/dev/loop2p2 +image_path=./sdBuild/download +mount_path=./sdBuild/mount +netcore_gzip=$image_path/netcore.tar.gz +image_zip=$image_path/osImage.zip +image_iso=$image_path/osImage.img +image_output=$image_path/image.tar.gz +tally_app_zip=./SpyderTallyApp.zip + +# Ensure the app is built before continuing +if [ ! -f "$tally_app_zip" ]; then + echo "SpyderTallyApp.zip not found. Please build the app before continuing." + exit -1 +fi + +# Download the latest .Net Core image +if [ ! -f $image_zip ]; then + mkdir -p $image_path + echo "Downloading .Net core framework" + # curl often gave "error 18 - transfer closed with outstanding read data remaining" + #wget -O $netcore_gzip "https://download.visualstudio.microsoft.com/download/pr/72888385-910d-4ef3-bae2-c08c28e42af0/59be90572fdcc10766f1baf5ac39529a/dotnet-sdk-6.0.101-linux-arm.tar.gz" + wget -O $netcore_gzip "https://download.visualstudio.microsoft.com/download/pr/ff3b2714-0dee-4cf9-94ee-cb9f5ded285f/d6bfe8668428f9eb28acdf6b6f5a81bc/aspnetcore-runtime-6.0.1-linux-arm.tar.gz" + + if [ $? -ne 0 ]; then + echo "Download failed" ; exit -1; + fi +fi + +# Download Raspberry Pi image +if [ ! -f $image_zip ]; then + mkdir -p $image_path + echo "Downloading latest Raspbian lite image" + # curl often gave "error 18 - transfer closed with outstanding read data remaining" + wget -O $image_zip "https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2022-01-28/2022-01-28-raspios-bullseye-armhf-lite.zip" + + if [ $? -ne 0 ]; then + echo "Download failed" ; exit -1; + fi +fi + +echo "Extracting ${image_zip} ISO" +unzip -p $image_zip > $image_iso + +if [ $? -ne 0 ]; then + echo "Unzipping image ${image_zip} failed" ; exit -1; +fi + +# Now we're going to have to extend the image size to make room for the app +# 400Mb added below +echo "Extending image size..." +parted $image_iso print +dd if=/dev/zero bs=1M count=1000 >> $image_iso +parted $image_iso resizepart 2 100% + +# Find partitions on this image and many them available to the +# build server. +losetup --find -P $image_iso + +# Resize partition (take two) +echo "Resizing root partition to add space" +e2fsck -f $loop_2_device +resize2fs $loop_2_device + +echo "Completed extending image size" +parted $image_iso print + +# Mount the boot partition +echo "Mounting boot partition" +mkdir -p $mount_path +mount $loop_1_device $mount_path + +# turn on I2C for front panel display control +echo "Enabling I2C in config.txt" +cp $mount_path/config.txt $image_path/config.txt +echo "dtparam=i2c_arm=on" >> $image_path/config.txt +echo "dtparam=i2c1=on" >> $image_path/config.txt +cp $image_path/config.txt $mount_path/config.txt + +# Add default config files (experimental) +cp -r ../../docs/Linux/appConfig.json $image_path/appConfig.json +cp -r ../../docs/Linux/deviceConfig.json $image_path/deviceConfig.json + +echo "Unmounting boot partition" +umount $mount_path + +# Mount the root partition, and copy any files from filesToAdd +# to the partition. +echo "Mounting root partition" +mount $loop_2_device $mount_path + +# Install .Net Core +echo "Installing .Net Core to ~/dotnet" +local_dotnet_path=/home/pi/dotnet +dotnet_path=$mount_path$local_dotnet_path +mkdir -p $dotnet_path +tar -zxf $netcore_gzip -C $dotnet_path +echo "export PATH=$PATH:\${local_dotnet_path}" >> $mount_path/home/pi/.bashrc +echo "export DOTNET_ROOT=${local_dotnet_path}" >> $mount_path/home/pi/.bashrc + +# Install the app +local_app_path=/home/pi/SpyderTallyApp +app_path=$mount_path$local_app_path +echo "Installing Spyder Tally app to ${local_app_path}" +mkdir -p $app_path +unzip -q $tally_app_zip -d $app_path + +# Add default config files (will be used if not found in /boot) +cp -r ../../docs/Linux/appConfig.json $app_path/appConfig.json +cp -r ../../docs/Linux/deviceConfig.json $app_path/deviceConfig.json +chmod ugo+rw $app_path/*.json + +# Sending app parameter of /boot to the app, which will be where we try to load the config from +echo "Creating Crontab entry to run app on boot" +(crontab -l; echo "@reboot ${local_dotnet_path}/dotnet ${local_app_path}/SpyderTallyControllerWebApp.dll /boot &") | sort -u | crontab - + +echo "Unounting root partition" +umount $mount_path + +# Don't need the loopback device anymore, disconnect it. +losetup -D + +# Zip the output image +echo "Compressing image to ${image_output}" +tar -czf $image_output $image_iso + +echo "Finished building the image!" \ No newline at end of file diff --git a/src/Linux/nginx.conf b/src/Linux/nginx.conf new file mode 100644 index 0000000..55e0b3d --- /dev/null +++ b/src/Linux/nginx.conf @@ -0,0 +1,47 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 768; +} + +http { + # Basic Settings + sendfile on; + tcp_nopush on; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # SSL Settings + 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; + } + } +} + diff --git a/src/WindowsIotCore/SpyderTallyController/SpyderTallyController.csproj b/src/WindowsIotCore/SpyderTallyController/SpyderTallyController.csproj index bb73196..ca072df 100644 --- a/src/WindowsIotCore/SpyderTallyController/SpyderTallyController.csproj +++ b/src/WindowsIotCore/SpyderTallyController/SpyderTallyController.csproj @@ -129,7 +129,7 @@ - 6.2.13 + 6.2.14 4.1.0