diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 5c2ac22..74b58bb 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -13,18 +13,14 @@ on: jobs: build: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest, windows-latest] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/Tests/skullOS.Core.Tests/skullOS.Core.Tests.csproj b/CoreTests/CoreTests.csproj similarity index 63% rename from Tests/skullOS.Core.Tests/skullOS.Core.Tests.csproj rename to CoreTests/CoreTests.csproj index 9aad537..8556725 100644 --- a/Tests/skullOS.Core.Tests/skullOS.Core.Tests.csproj +++ b/CoreTests/CoreTests.csproj @@ -1,21 +1,22 @@ - net7.0 + net8.0 enable enable false + true - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -25,4 +26,14 @@ + + + + + + + PreserveNewest + + + diff --git a/CoreTests/FileManagerTests.cs b/CoreTests/FileManagerTests.cs new file mode 100644 index 0000000..1d0ea89 --- /dev/null +++ b/CoreTests/FileManagerTests.cs @@ -0,0 +1,7 @@ +namespace CoreTests +{ + public class FileManagerTests + { + + } +} diff --git a/Tests/skullOS.Core.Tests/Usings.cs b/CoreTests/GlobalUsings.cs similarity index 100% rename from Tests/skullOS.Core.Tests/Usings.cs rename to CoreTests/GlobalUsings.cs diff --git a/CoreTests/LoggerTests.cs b/CoreTests/LoggerTests.cs new file mode 100644 index 0000000..fa084c4 --- /dev/null +++ b/CoreTests/LoggerTests.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoreTests +{ + internal class LoggerTests + { + } +} diff --git a/CoreTests/SettingsTests.cs b/CoreTests/SettingsTests.cs new file mode 100644 index 0000000..6fca46e --- /dev/null +++ b/CoreTests/SettingsTests.cs @@ -0,0 +1,17 @@ +using skullOS.Core; + +namespace CoreTests +{ + public class SettingsTests + { + [Fact] + public void TestSettingsLoader() + { + var result = SettingsLoader.LoadConfig(@"TestData/TestSettings.txt"); + Assert.NotNull(result); + Assert.Single(result); + result.TryGetValue("Worked", out string response); + Assert.Equal("True", response); + } + } +} diff --git a/CoreTests/TestData/TestSettings.txt b/CoreTests/TestData/TestSettings.txt new file mode 100644 index 0000000..e360a09 --- /dev/null +++ b/CoreTests/TestData/TestSettings.txt @@ -0,0 +1 @@ +Worked=True \ No newline at end of file diff --git a/ModuleTests/AdventureTest.cs b/ModuleTests/AdventureTest.cs new file mode 100644 index 0000000..ec6f0d2 --- /dev/null +++ b/ModuleTests/AdventureTest.cs @@ -0,0 +1,51 @@ +using Moq; +using skullOS.Core; +using skullOS.HardwareServices.Interfaces; +using skullOS.Modules; +using skullOS.Modules.Exceptions; + +namespace ModuleTests +{ + public class AdventureTest + { + Mock cameraMock; + Adventure sut; + public AdventureTest() + { + FileManager.CreateSkullDirectory(false, true); + + cameraMock = new(); + cameraMock.Setup(camera => camera.TakePictureAsync(It.IsAny())).Returns(Task.FromResult("Pass")); + sut = new(cameraMock.Object); + } + + [Fact] + public void CanCreateAdventureModule() + { + Assert.NotNull(sut); + } + [Fact] + public void OnEnableThrowsException() + { + Assert.Throws(() => sut.OnEnable(It.IsAny())); + } + [Fact] + public void OnActionThrowsException() + { + Assert.Throws(() => sut.OnAction(It.IsAny(), It.IsAny())); + } + [Fact] + public void NameReturnsCorrect() + { + Assert.Equal("Adventure", sut.ToString()); + } + + //Timelapse settings should probably be moved to read from file? + [Fact] + public void TimerHasCorrectDuration() + { + var timer = sut.GetTimelapseController(); + Assert.Equal(30000, timer.Interval); + } + } +} diff --git a/ModuleTests/BuzzerTest.cs b/ModuleTests/BuzzerTest.cs new file mode 100644 index 0000000..4347c8c --- /dev/null +++ b/ModuleTests/BuzzerTest.cs @@ -0,0 +1,54 @@ +using Moq; +using skullOS.HardwareServices.Interfaces; +using skullOS.Modules; +using skullOS.Modules.Exceptions; +using skullOS.Modules.Interfaces; + +namespace ModuleTests +{ + public class BuzzerTest + { + Buzzer sut; + Mock buzzerPlayer; + + public BuzzerTest() + { + Mock buzzerHardware = new(); + buzzerPlayer = new(); + buzzerPlayer.Setup(x => x.Play(It.IsAny>(), It.IsAny())).Verifiable(); + + sut = new Buzzer(buzzerHardware.Object, 13, buzzerPlayer.Object); + } + + [Fact] + public void CanCreateBuzzer() + { + Assert.NotNull(sut); + } + + [Fact] + public void CanPlayTune() + { + sut.PlayTune(BuzzerLibrary.Tunes.AlphabetSong); + + Mock.Verify([buzzerPlayer]); + } + + [Fact] + public void NameReturnsCorrect() + { + Assert.Equal("Buzzer", sut.ToString()); + } + + [Fact] + public void OnEnableThrowsException() + { + Assert.Throws(() => sut.OnEnable(It.IsAny())); + } + [Fact] + public void OnActionThrowsException() + { + Assert.Throws(() => sut.OnAction(It.IsAny(), It.IsAny())); + } + } +} diff --git a/ModuleTests/CameraTest.cs b/ModuleTests/CameraTest.cs new file mode 100644 index 0000000..d172138 --- /dev/null +++ b/ModuleTests/CameraTest.cs @@ -0,0 +1,44 @@ +using Moq; +using skullOS.HardwareServices.Interfaces; +using skullOS.Modules; + +namespace ModuleTests +{ + public class CameraTest + { + Mock camMock = new(); + Mock micMock = new(); + Mock buzzerMock = new(); + Mock speakerMock = new(); + Mock LedMock = new(); + + [Fact] + public void CanCreate() + { + Camera sut = new Camera(camMock.Object, micMock.Object, speakerMock.Object, LedMock.Object, buzzerMock.Object, @"TestData/CameraSettings.txt"); + Assert.NotNull(sut); + } + + [Fact] + public async void CanTakePicture() + { + camMock.Setup(camera => camera.TakePictureAsync(It.IsAny())).Returns(Task.FromResult("Pass")).Verifiable(); + + Camera sut = new Camera(camMock.Object, micMock.Object, speakerMock.Object, LedMock.Object, buzzerMock.Object, @"TestData/CameraSettings.txt"); + + await sut.TakePicture(); + Mock.Verify(camMock); + } + [Fact] + public async void CanRecordVideo() + { + camMock.Setup(camera => + camera.RecordShortVideoAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult("Pass")).Verifiable(); + + Camera sut = new Camera(camMock.Object, micMock.Object, speakerMock.Object, LedMock.Object, buzzerMock.Object, @"TestData/CameraSettings.txt"); + + await sut.RecordShortVideo(); + Mock.Verify(camMock); + } + } +} diff --git a/ModuleTests/GlobalUsings.cs b/ModuleTests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/ModuleTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/ModuleTests/ModuleTests.csproj b/ModuleTests/ModuleTests.csproj new file mode 100644 index 0000000..f07c1b4 --- /dev/null +++ b/ModuleTests/ModuleTests.csproj @@ -0,0 +1,43 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/ModuleTests/PropTest.cs b/ModuleTests/PropTest.cs new file mode 100644 index 0000000..cf983d1 --- /dev/null +++ b/ModuleTests/PropTest.cs @@ -0,0 +1,45 @@ +using Moq; +using skullOS.HardwareServices.Interfaces; +using skullOS.Modules; +using skullOS.Modules.Exceptions; + +namespace ModuleTests +{ + public class PropTest + { + Prop sut; + string pathToTestData = @"TestData/PropSettings.txt"; + + public PropTest() + { + Mock speakerMock = new(); + speakerMock.Setup(speaker => speaker.PlayAudio(It.IsAny())).Returns(Task.CompletedTask); + Mock ledMock = new(); + + sut = new Prop(speakerMock.Object, ledMock.Object, pathToTestData); + } + + [Fact] + public void CanCreateProp() + { + Assert.NotNull(sut); + } + + [Fact] + public void NameReturnsCorrect() + { + Assert.Equal("Prop", sut.ToString()); + } + + [Fact] + public void OnEnableThrowsException() + { + Assert.Throws(() => sut.OnEnable(It.IsAny())); + } + [Fact] + public void OnActionThrowsException() + { + Assert.Throws(() => sut.OnAction(It.IsAny(), It.IsAny())); + } + } +} diff --git a/ModuleTests/SupportTest.cs b/ModuleTests/SupportTest.cs new file mode 100644 index 0000000..024d848 --- /dev/null +++ b/ModuleTests/SupportTest.cs @@ -0,0 +1,44 @@ +using Moq; +using skullOS.HardwareServices.Interfaces; +using skullOS.Modules; +using skullOS.Modules.Exceptions; +using skullOS.Tests; +using System.Device.Gpio; + +namespace ModuleTests +{ + public class SupportTest + { + Support sut; + public SupportTest() + { + Mock speakerMock = new(); + speakerMock.Setup(speaker => speaker.PlayAudio(It.IsAny())).Returns(Task.CompletedTask); + Mock gpioMock = new(); + var ctrlr = new GpioController(PinNumberingScheme.Logical, gpioMock.Object); + + sut = new Support(ctrlr, speakerMock.Object, 4); + } + + [Fact] + public void CanCreateSupportModule() + { + Assert.NotNull(sut); + } + [Fact] + public void SupportOnEnableThrowsException() + { + Assert.Throws(() => sut.OnEnable(It.IsAny())); + } + [Fact] + public void SupportOnActionThrowsException() + { + Assert.Throws(() => sut.OnAction(It.IsAny(), It.IsAny())); + } + [Fact] + public void SupportReturnsCorrectName() + { + Assert.Equal("Support", sut.ToString()); + } + } +} diff --git a/ModuleTests/TestData/CameraSettings.txt b/ModuleTests/TestData/CameraSettings.txt new file mode 100644 index 0000000..2cb243c --- /dev/null +++ b/ModuleTests/TestData/CameraSettings.txt @@ -0,0 +1,4 @@ +UseMic=False +UseBuzzer=False +CameraLight=0 +UseSpeaker=False \ No newline at end of file diff --git a/ModuleTests/TestData/PropSettings.txt b/ModuleTests/TestData/PropSettings.txt new file mode 100644 index 0000000..10600e6 --- /dev/null +++ b/ModuleTests/TestData/PropSettings.txt @@ -0,0 +1,3 @@ +Sounds=False +Lights=False +Servos=False \ No newline at end of file diff --git a/ServoSkull.sln b/ServoSkull.sln index 5266c87..995ed2c 100644 --- a/ServoSkull.sln +++ b/ServoSkull.sln @@ -36,6 +36,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "skullOS.HardwareServices", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServoSkullDemo", "MiscFiles\ServoSkullDemo\ServoSkullDemo.csproj", "{0B0F89D7-32B6-4E20-B676-44F70E7B5352}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModuleTests", "ModuleTests\ModuleTests.csproj", "{356018AA-0F2A-48D2-B263-62D52906053F}" + ProjectSection(ProjectDependencies) = postProject + {3CBE1B63-957B-43B6-939D-070EAC517C7C} = {3CBE1B63-957B-43B6-939D-070EAC517C7C} + {464B162B-62FC-49CC-A6AB-72DBE959DD8C} = {464B162B-62FC-49CC-A6AB-72DBE959DD8C} + {DE800E5A-E68B-4BCA-AC99-4BC9DB44FD56} = {DE800E5A-E68B-4BCA-AC99-4BC9DB44FD56} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreTests", "CoreTests\CoreTests.csproj", "{B5CE836D-7EEE-4D59-81AF-A29399CF6BC6}" + ProjectSection(ProjectDependencies) = postProject + {464B162B-62FC-49CC-A6AB-72DBE959DD8C} = {464B162B-62FC-49CC-A6AB-72DBE959DD8C} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "skullOS.Tests", "skullOS.Tests\skullOS.Tests.csproj", "{7B1AA4E5-C354-49D8-A463-A804E9B00A68}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -92,12 +106,39 @@ Global {0B0F89D7-32B6-4E20-B676-44F70E7B5352}.Release|Any CPU.Build.0 = Release|Any CPU {0B0F89D7-32B6-4E20-B676-44F70E7B5352}.Release|ARM64.ActiveCfg = Release|Any CPU {0B0F89D7-32B6-4E20-B676-44F70E7B5352}.Release|ARM64.Build.0 = Release|Any CPU + {356018AA-0F2A-48D2-B263-62D52906053F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {356018AA-0F2A-48D2-B263-62D52906053F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {356018AA-0F2A-48D2-B263-62D52906053F}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {356018AA-0F2A-48D2-B263-62D52906053F}.Debug|ARM64.Build.0 = Debug|Any CPU + {356018AA-0F2A-48D2-B263-62D52906053F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {356018AA-0F2A-48D2-B263-62D52906053F}.Release|Any CPU.Build.0 = Release|Any CPU + {356018AA-0F2A-48D2-B263-62D52906053F}.Release|ARM64.ActiveCfg = Release|Any CPU + {356018AA-0F2A-48D2-B263-62D52906053F}.Release|ARM64.Build.0 = Release|Any CPU + {B5CE836D-7EEE-4D59-81AF-A29399CF6BC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5CE836D-7EEE-4D59-81AF-A29399CF6BC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5CE836D-7EEE-4D59-81AF-A29399CF6BC6}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {B5CE836D-7EEE-4D59-81AF-A29399CF6BC6}.Debug|ARM64.Build.0 = Debug|Any CPU + {B5CE836D-7EEE-4D59-81AF-A29399CF6BC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5CE836D-7EEE-4D59-81AF-A29399CF6BC6}.Release|Any CPU.Build.0 = Release|Any CPU + {B5CE836D-7EEE-4D59-81AF-A29399CF6BC6}.Release|ARM64.ActiveCfg = Release|Any CPU + {B5CE836D-7EEE-4D59-81AF-A29399CF6BC6}.Release|ARM64.Build.0 = Release|Any CPU + {7B1AA4E5-C354-49D8-A463-A804E9B00A68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B1AA4E5-C354-49D8-A463-A804E9B00A68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B1AA4E5-C354-49D8-A463-A804E9B00A68}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {7B1AA4E5-C354-49D8-A463-A804E9B00A68}.Debug|ARM64.Build.0 = Debug|Any CPU + {7B1AA4E5-C354-49D8-A463-A804E9B00A68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B1AA4E5-C354-49D8-A463-A804E9B00A68}.Release|Any CPU.Build.0 = Release|Any CPU + {7B1AA4E5-C354-49D8-A463-A804E9B00A68}.Release|ARM64.ActiveCfg = Release|Any CPU + {7B1AA4E5-C354-49D8-A463-A804E9B00A68}.Release|ARM64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {0B0F89D7-32B6-4E20-B676-44F70E7B5352} = {EE38A10C-F0BB-40B5-B1E6-8CB16C6E452C} + {356018AA-0F2A-48D2-B263-62D52906053F} = {B50B951E-DF9B-49DD-9CAC-ED8B01C6B1E7} + {B5CE836D-7EEE-4D59-81AF-A29399CF6BC6} = {B50B951E-DF9B-49DD-9CAC-ED8B01C6B1E7} + {7B1AA4E5-C354-49D8-A463-A804E9B00A68} = {B50B951E-DF9B-49DD-9CAC-ED8B01C6B1E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FA7BE016-5699-47F3-A654-08A907CE1F11} diff --git a/Tests/skullOS.Core.Tests/ControllerTests.cs b/Tests/skullOS.Core.Tests/ControllerTests.cs deleted file mode 100644 index 0827d54..0000000 --- a/Tests/skullOS.Core.Tests/ControllerTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace skullOS.Core.Tests -{ - public class ControllerTests - { - [Fact] - public void TestConstructor() - { - Assert.True(true); - } - } -} diff --git a/Tests/skullOS.Tests/ModulesTests.cs b/Tests/skullOS.Tests/ModulesTests.cs deleted file mode 100644 index 57123b4..0000000 --- a/Tests/skullOS.Tests/ModulesTests.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace skullOS.Tests -{ - public class ModulesTests - { - - - } -} diff --git a/Tests/skullOS.Tests/ProgramTests.cs b/Tests/skullOS.Tests/ProgramTests.cs deleted file mode 100644 index b44df7f..0000000 --- a/Tests/skullOS.Tests/ProgramTests.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace skullOS.Tests -{ - //Main & Run are not tested - public class ProgramTests - { - - - //[Fact] - //public void TestSetupModules() - //{ - // var mockController = new MockGpioController(); - // var modulesLoaded = CreateTestModules(); - // Assert.True(Program.SetupModules(modulesLoaded, mockController)); - //} - - - } -} diff --git a/Tests/skullOS.Tests/TestData/Modules.txt b/Tests/skullOS.Tests/TestData/Modules.txt deleted file mode 100644 index 83df793..0000000 --- a/Tests/skullOS.Tests/TestData/Modules.txt +++ /dev/null @@ -1,2 +0,0 @@ -Input = True -Camera = True \ No newline at end of file diff --git a/Tests/skullOS.Tests/Usings.cs b/Tests/skullOS.Tests/Usings.cs deleted file mode 100644 index c802f44..0000000 --- a/Tests/skullOS.Tests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; diff --git a/Tests/skullOS.Tests/skullOS.Tests.csproj b/Tests/skullOS.Tests/skullOS.Tests.csproj deleted file mode 100644 index b487c87..0000000 --- a/Tests/skullOS.Tests/skullOS.Tests.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - net7.0 - enable - enable - - false - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - Always - - - - diff --git a/skullOS.Core/FileManager.cs b/skullOS.Core/FileManager.cs index d3672d2..8532aef 100644 --- a/skullOS.Core/FileManager.cs +++ b/skullOS.Core/FileManager.cs @@ -6,40 +6,64 @@ public static class FileManager { private static string rootDirectoryPath = string.Empty; - public static void CreateSkullDirectory(bool usePersonalDir = true) + public static void CreateSkullDirectory(bool usePersonalDir = true, bool isTest = false) { - DirectoryInfo? rootDirectory = null; - string pathToDir; - if (usePersonalDir) + if (!isTest) { - //pathToDir = Environment.GetFolderPath(Environment.SpecialFolder.Personal); - pathToDir = Environment.GetEnvironmentVariable("HOME"); - Console.WriteLine("Path to personal dir is " + pathToDir); + DirectoryInfo? rootDirectory = null; + string pathToDir; + if (usePersonalDir) + { + //pathToDir = Environment.GetFolderPath(Environment.SpecialFolder.Personal); + pathToDir = Environment.GetEnvironmentVariable("HOME"); + Console.WriteLine("Path to personal dir is " + pathToDir); + } + else + { + pathToDir = "/media"; + } + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + rootDirectory = Directory.CreateDirectory(pathToDir + @"/skullOS", + unixCreateMode: UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute); + } + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + if (rootDirectory == null) + { + throw new Exception("Root directory not defined!"); + } + rootDirectoryPath = rootDirectory.FullName; } else - { - pathToDir = "/media"; - } - try { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - rootDirectory = Directory.CreateDirectory(pathToDir + @"/skullOS", - unixCreateMode: UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | - UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | - UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute); + try + { + DirectoryInfo rootDirectory = Directory.CreateDirectory("/skullOS-TestData", unixCreateMode: + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute); + rootDirectoryPath = rootDirectory.FullName; + } + catch (Exception) + { + //When running on Github runner, doesn't seem to like creating a directory + rootDirectoryPath = Directory.GetCurrentDirectory(); + } + } + } - catch (Exception e) - { - Console.WriteLine(e.Message); - } - if (rootDirectory == null) - { - throw new Exception("Root directory not defined!"); - } - rootDirectoryPath = rootDirectory.FullName; - Console.WriteLine("Root directory is: " + rootDirectoryPath); + } public static string GetSkullDirectory() @@ -57,5 +81,10 @@ public static void CreateSubDirectory(string directoryName) UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute); } } + + public static void DeleteDirectory() + { + Directory.Delete(rootDirectoryPath, true); + } } } \ No newline at end of file diff --git a/skullOS.Core/Interfaces/ISettingsLoader.cs b/skullOS.Core/Interfaces/ISettingsLoader.cs new file mode 100644 index 0000000..e727436 --- /dev/null +++ b/skullOS.Core/Interfaces/ISettingsLoader.cs @@ -0,0 +1,6 @@ +namespace skullOS.Core.Interfaces +{ + public interface ISettingsLoader + { + } +} \ No newline at end of file diff --git a/skullOS.Core/Interfaces/ISkullLogger.cs b/skullOS.Core/Interfaces/ISkullLogger.cs new file mode 100644 index 0000000..c677ecf --- /dev/null +++ b/skullOS.Core/Interfaces/ISkullLogger.cs @@ -0,0 +1,7 @@ +namespace skullOS.Core.Interfaces +{ + public interface ISkullLogger + { + void LogMessage(string message); + } +} \ No newline at end of file diff --git a/skullOS.Core/SkullLogger.cs b/skullOS.Core/SkullLogger.cs index 2b03226..902201d 100644 --- a/skullOS.Core/SkullLogger.cs +++ b/skullOS.Core/SkullLogger.cs @@ -1,6 +1,8 @@ -namespace skullOS.Core +using skullOS.Core.Interfaces; + +namespace skullOS.Core { - public class SkullLogger + public class SkullLogger : ISkullLogger { string filepath; public SkullLogger() diff --git a/skullOS.HardwareServices/BuzzerService.cs b/skullOS.HardwareServices/BuzzerService.cs index b76ae94..ef92b5e 100644 --- a/skullOS.HardwareServices/BuzzerService.cs +++ b/skullOS.HardwareServices/BuzzerService.cs @@ -10,5 +10,10 @@ public BuzzerService(int pinNumber) { Buzzer = new Buzzer(pinNumber); } + + public void SetBuzzer(int pinNumber) + { + Buzzer = new Buzzer(pinNumber); + } } } diff --git a/skullOS.HardwareServices/Interfaces/IBuzzerService.cs b/skullOS.HardwareServices/Interfaces/IBuzzerService.cs index eefdaf4..30a1b20 100644 --- a/skullOS.HardwareServices/Interfaces/IBuzzerService.cs +++ b/skullOS.HardwareServices/Interfaces/IBuzzerService.cs @@ -1,6 +1,11 @@ -namespace skullOS.HardwareServices.Interfaces +using Iot.Device.Buzzer; + +namespace skullOS.HardwareServices.Interfaces { public interface IBuzzerService { + Buzzer Buzzer { get; } + + void SetBuzzer(int pinNumber); } } \ No newline at end of file diff --git a/skullOS.HardwareServices/Interfaces/ILedService.cs b/skullOS.HardwareServices/Interfaces/ILedService.cs index 9aa3781..0f637b4 100644 --- a/skullOS.HardwareServices/Interfaces/ILedService.cs +++ b/skullOS.HardwareServices/Interfaces/ILedService.cs @@ -6,5 +6,6 @@ public interface ILedService void TurnOff(string Pin); void TurnOn(string Pin); Dictionary GetLeds(); + void SetLeds(Dictionary ledsToControl); } } \ No newline at end of file diff --git a/skullOS.HardwareServices/LedService.cs b/skullOS.HardwareServices/LedService.cs index b250799..c0e5ad4 100644 --- a/skullOS.HardwareServices/LedService.cs +++ b/skullOS.HardwareServices/LedService.cs @@ -45,5 +45,10 @@ public Dictionary GetLeds() { return LEDs; } + + public void SetLeds(Dictionary ledsToControl) + { + LEDs = ledsToControl; + } } } diff --git a/skullOS.Modules/Adventure.cs b/skullOS.Modules/Adventure.cs index e463cb0..d1d5025 100644 --- a/skullOS.Modules/Adventure.cs +++ b/skullOS.Modules/Adventure.cs @@ -14,12 +14,19 @@ public class Adventure : Module, IAdventure double interval = 30000; ICameraService cameraService; - public Adventure() + public Adventure(ICameraService camService = null) { - var now = DateTime.Now.ToString("M"); + string now = DateTime.Now.ToString("M"); FileManager.CreateSubDirectory("Timelapse - " + now); directory = FileManager.GetSkullDirectory() + @"/Timelapse - " + now + @"/"; - cameraService = new CameraService(); + if (camService == null) + { + cameraService = new CameraService(); + } + else + { + cameraService = camService; + } takePicture = new Timer(interval); @@ -33,6 +40,11 @@ private void TakePicture_Elapsed(object? sender, System.Timers.ElapsedEventArgs cameraService.TakePictureAsync(directory); } + public Timer GetTimelapseController() + { + return takePicture; + } + public override void OnAction(object? sender, EventArgs e) { throw new OnActionException("Adventure does not support OnAction"); diff --git a/skullOS.Modules/Buzzer.cs b/skullOS.Modules/Buzzer.cs index a7e845e..8157236 100644 --- a/skullOS.Modules/Buzzer.cs +++ b/skullOS.Modules/Buzzer.cs @@ -1,4 +1,5 @@ using skullOS.HardwareServices; +using skullOS.HardwareServices.Interfaces; using skullOS.Modules.Exceptions; using skullOS.Modules.Interfaces; using static skullOS.Modules.BuzzerLibrary; @@ -10,12 +11,27 @@ namespace skullOS.Modules { public class Buzzer : Module, IBuzzerModule { - BuzzerService PwmBuzzer; - MelodyPlayer Player; - public Buzzer() + IBuzzerService PwmBuzzer; + IMelodyPlayer Player; + public Buzzer(IBuzzerService buzzerService = null, int pwmPin = 13, IMelodyPlayer testPlayer = null) { - PwmBuzzer = new BuzzerService(13); - Player = new MelodyPlayer(PwmBuzzer.Buzzer); + if (buzzerService == null) + { + PwmBuzzer = new BuzzerService(pwmPin); + } + else + { + PwmBuzzer = buzzerService; + } + + if (testPlayer == null) + { + Player = new MelodyPlayer(PwmBuzzer.Buzzer); + } + else + { + Player = testPlayer; + } } public void PlayTune(Tunes tuneToPlay) @@ -52,7 +68,7 @@ public override string ToString() /// 3rd party class for playing media via a buzzer /// Sourced from teh iot samples /// - internal class MelodyPlayer + public class MelodyPlayer : IMelodyPlayer { private readonly DeviceBuzzer _buzzer; private int _wholeNoteDurationInMilliseconds; @@ -61,10 +77,13 @@ internal class MelodyPlayer /// Create MelodyPlayer. /// /// Buzzer instance to be played on. - public MelodyPlayer(DeviceBuzzer buzzer) => _buzzer = buzzer; + public MelodyPlayer(DeviceBuzzer buzzer) + { + _buzzer = buzzer; + } /// - /// Play melody elements sequecne. + /// Play melody elements sequence. /// /// Sequence of pauses and notes elements to be played. /// Tempo of melody playing. @@ -79,6 +98,21 @@ public void Play(IList sequence, int tempo, int tonesToTranspose } } + /// + /// Play melody elements sequence. 0 set transposing for testing + /// + /// Sequence of pauses and notes elements to be played. + /// Tempo of melody playing. + public void Play(IList sequence, int tempo) + { + _wholeNoteDurationInMilliseconds = GetWholeNoteDurationInMilliseconds(tempo); + sequence = TransposeSequence(sequence, 0); + foreach (var element in sequence) + { + PlayElement(element); + } + } + private static IList TransposeSequence(IList sequence, int tonesToTranspose) { if (tonesToTranspose == 0) diff --git a/skullOS.Modules/Camera.cs b/skullOS.Modules/Camera.cs index 95ce260..6f08339 100644 --- a/skullOS.Modules/Camera.cs +++ b/skullOS.Modules/Camera.cs @@ -1,5 +1,6 @@ using skullOS.Core; using skullOS.HardwareServices; +using skullOS.HardwareServices.Interfaces; using skullOS.Modules.Interfaces; namespace skullOS.Modules @@ -13,30 +14,51 @@ public enum CameraMode public class Camera : Module, ICameraModule { - public CameraService CameraService; - public MicrophoneService? MicrophoneService = null; - public SpeakerService? SpeakerService = null; - public LedService? LedService = null; + public ICameraService CameraService; + public IMicrophoneService? MicrophoneService = null; + public ISpeakerService? SpeakerService = null; + public ILedService? LedService = null; + public IBuzzerService? BuzzerService = null; + + public CameraMode CameraMode = CameraMode.Image; - public BuzzerService? BuzzerService = null; bool useMic = false; bool useSpeaker = false; bool useBuzzer = false; bool isActive = false; - public Camera() + public Camera(ICameraService camService = null, IMicrophoneService micService = null, + ISpeakerService spkService = null, ILedService ledService = null, IBuzzerService buzService = null, + string configFile = @"Data/CameraSettings.txt") { FileManager.CreateSubDirectory("Captures"); - var cameraSettings = SettingsLoader.LoadConfig(@"Data/CameraSettings.txt"); - CameraService = new CameraService(); + var cameraSettings = SettingsLoader.LoadConfig(configFile); + + if (camService == null) + { + CameraService = new CameraService(); + } + else + { + CameraService = camService; + } + if (cameraSettings.ContainsKey("UseMic")) { if (cameraSettings.TryGetValue("UseMic", out string shouldUseMic)) { if (bool.Parse(shouldUseMic)) { - MicrophoneService = new MicrophoneService(); + //Might this be null coalescable? + if (micService == null) + { + MicrophoneService = new MicrophoneService(); + } + else + { + MicrophoneService = micService; + } useMic = true; } else @@ -53,7 +75,17 @@ public Camera() { { "CameraLight", int.Parse(lightPin) } }; - LedService = new LedService(pins); + + if (ledService == null) + { + LedService = new LedService(pins); + } + else + { + LedService = ledService; + LedService.SetLeds(pins); + } + } } if (cameraSettings.ContainsKey("UseBuzzer")) @@ -62,12 +94,21 @@ public Camera() { if (bool.Parse(shouldUseBuzzer)) { - BuzzerService = new BuzzerService(13); + //This should be reading from the file! + if (buzService == null) + { + BuzzerService = new BuzzerService(13); + } + else + { + BuzzerService = buzService; + BuzzerService.SetBuzzer(13); + } useBuzzer = true; } else { - //No Mic desired + //No buzzer desired } } } @@ -77,12 +118,19 @@ public Camera() { if (bool.Parse(shouldUseSpeaker)) { - SpeakerService = new SpeakerService(); + if (spkService == null) + { + SpeakerService = new SpeakerService(); + } + else + { + SpeakerService = spkService; + } useSpeaker = true; } else { - //No Mic desired + //No Speaker desired } } } @@ -93,7 +141,7 @@ public async Task TakePicture() if (!isActive) { isActive = true; - if (LedService != null && LedService.LEDs.ContainsKey("CameraLight")) + if (LedService != null && LedService.GetLeds().ContainsKey("CameraLight")) { LedService.BlinkLight("CameraLight"); } @@ -117,7 +165,7 @@ public async Task RecordShortVideo() if (!isActive) { isActive = true; - if (LedService != null && LedService.LEDs.ContainsKey("CameraLight")) + if (LedService != null && LedService.GetLeds().ContainsKey("CameraLight")) { LedService.TurnOn("CameraLight"); } @@ -131,7 +179,7 @@ public async Task RecordShortVideo() } string result = await CameraService.RecordShortVideoAsync($"{FileManager.GetSkullDirectory()}/Captures/", false); LogMessage(result); - if (LedService != null && LedService.LEDs.ContainsKey("CameraLight")) + if (LedService != null && LedService.GetLeds().ContainsKey("CameraLight")) { LedService.TurnOff("CameraLight"); } diff --git a/skullOS.Modules/Interfaces/IMelodyPlayer.cs b/skullOS.Modules/Interfaces/IMelodyPlayer.cs new file mode 100644 index 0000000..ef94c42 --- /dev/null +++ b/skullOS.Modules/Interfaces/IMelodyPlayer.cs @@ -0,0 +1,9 @@ + +namespace skullOS.Modules.Interfaces +{ + public interface IMelodyPlayer + { + void Play(IList sequence, int tempo); + void Play(IList sequence, int tempo, int tonesToTranspose = 0); + } +} \ No newline at end of file diff --git a/skullOS.Modules/Interfaces/ISupport.cs b/skullOS.Modules/Interfaces/ISupportModule.cs similarity index 59% rename from skullOS.Modules/Interfaces/ISupport.cs rename to skullOS.Modules/Interfaces/ISupportModule.cs index cc62e00..892df56 100644 --- a/skullOS.Modules/Interfaces/ISupport.cs +++ b/skullOS.Modules/Interfaces/ISupportModule.cs @@ -1,6 +1,6 @@ namespace skullOS.Modules.Interfaces { - internal interface ISupport + internal interface ISupportModule { } } \ No newline at end of file diff --git a/skullOS.Modules/Prop.cs b/skullOS.Modules/Prop.cs index 3fe5481..7b92b1d 100644 --- a/skullOS.Modules/Prop.cs +++ b/skullOS.Modules/Prop.cs @@ -10,8 +10,8 @@ namespace skullOS.Modules { public class Prop : Module, IPropModule { - public ISpeakerService SpeakerService { get; set; } - public ILedService LedService { get; set; } + ISpeakerService SpeakerService; + ILedService LedService; static Timer? PlayIdleSound; double interval = 30000; @@ -22,23 +22,31 @@ public class Prop : Module, IPropModule ServoMotor rightFlap; Dictionary propSettings; -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public Prop() -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public Prop(ISpeakerService speaker = null, ILedService leds = null, string pathToSettings = @"Data/PropSettings.txt") { - propSettings = SettingsLoader.LoadConfig(@"Data/PropSettings.txt"); + propSettings = SettingsLoader.LoadConfig(pathToSettings); propSettings.TryGetValue("Sounds", out string soundsState); bool useSounds = bool.Parse(soundsState); if (propSettings.ContainsKey("Sounds") && useSounds) { - SpeakerService = new SpeakerService(); + if (speaker == null) + { + SpeakerService = new SpeakerService(); + } + else + { + SpeakerService = speaker; + } + SpeakerService.PlayAudio(@"Resources/computer-startup-music.mp3"); //This one won't await :( - sounds = Directory.GetFiles(@"Resources/Haro/Idles"); + sounds = Directory.GetFiles(@"Resources/Haro/Idles"); //This shouldn't be hardcoded like this at all >:( numberOfIdles = sounds.Length; - PlayIdleSound = new Timer(interval); - PlayIdleSound.AutoReset = true; + PlayIdleSound = new Timer(interval) + { + AutoReset = true + }; PlayIdleSound.Elapsed += PlayIdleSound_Elapsed; PlayIdleSound.Start(); } @@ -47,13 +55,23 @@ public Prop() bool useLights = bool.Parse(lightsState); if (propSettings.ContainsKey("Lights") && useLights) { - //Left and right eye, these are next to each other so it should be easy to tell - Dictionary pins = new Dictionary - { + //LEDs should be read from the file as well + Dictionary pins = new() + { { "LeftEye", 26 }, {"RightEye", 26 } }; - LedService = new LedService(pins); + + if (leds == null) + { + LedService = new LedService(pins); + } + else + { + LedService = leds; + LedService.SetLeds(pins); + } + foreach (var item in LedService.GetLeds()) { string pin = item.Key; @@ -65,10 +83,10 @@ public Prop() bool useServos = bool.Parse(servosState); if (propSettings.ContainsKey("Servos") && useServos) { - SoftwarePwmChannel leftPWM = new SoftwarePwmChannel(5, 50); + SoftwarePwmChannel leftPWM = new(5, 50); leftFlap = new ServoMotor(leftPWM); leftFlap.Start(); - SoftwarePwmChannel rightPWM = new SoftwarePwmChannel(6, 50); + SoftwarePwmChannel rightPWM = new(6, 50); rightFlap = new ServoMotor(rightPWM); rightFlap.Start(); } diff --git a/skullOS.Modules/Support.cs b/skullOS.Modules/Support.cs index 1fbf99a..b58d2c0 100644 --- a/skullOS.Modules/Support.cs +++ b/skullOS.Modules/Support.cs @@ -8,24 +8,40 @@ namespace skullOS.Modules { - public class Support : Module, ISupport + public class Support : Module, ISupportModule { GpioController controller; ISpeakerService speakerService; static Timer? LowBatteryAlert; - double interval = 60000; - int pin = 4; + readonly double interval = 60000; + int pin; //Defaults to Pimoroni's Lipo shim, - public Support() + public Support(GpioController gpioController = null, ISpeakerService speaker = null, int signalPin = 4) { + pin = signalPin; LowBatteryAlert = new Timer(interval) { AutoReset = true, }; LowBatteryAlert.Elapsed += LowBatteryAlert_Elapsed; - speakerService = new SpeakerService(); - controller = new GpioController(); + if (speaker == null) + { + speakerService = new SpeakerService(); + } + else + { + speakerService = speaker; + } + + if (gpioController == null) + { + controller = new GpioController(); + } + else + { + controller = gpioController; + } controller.OpenPin(pin); controller.RegisterCallbackForPinValueChangedEvent(pin, PinEventTypes.Rising, OnLowBattery); } diff --git a/skullOS.ModulesTests/CameraTests.cs b/skullOS.ModulesTests/CameraTests.cs new file mode 100644 index 0000000..df13580 --- /dev/null +++ b/skullOS.ModulesTests/CameraTests.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace skullOS.ModulesTests +{ + internal class CameraTests + { + } +} diff --git a/skullOS.ModulesTests/skullOS.ModulesTests.csproj b/skullOS.ModulesTests/skullOS.ModulesTests.csproj new file mode 100644 index 0000000..7c67ac0 --- /dev/null +++ b/skullOS.ModulesTests/skullOS.ModulesTests.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + diff --git a/skullOS.Tests/MockableGpioDriver.cs b/skullOS.Tests/MockableGpioDriver.cs new file mode 100644 index 0000000..4f7952c --- /dev/null +++ b/skullOS.Tests/MockableGpioDriver.cs @@ -0,0 +1,107 @@ +using System.Device.Gpio; + +namespace skullOS.Tests +{ + /// + /// A wrapper class that exposes the internal protected methods, so that they can be mocked. + /// Note: To provide an expectation for this mock, be sure to set the Callbase property to true and then + /// set expectations on the Ex methods. Only loose behavior works. + /// + /// Taken from https://github.com/dotnet/iot/blob/main/src/System.Device.Gpio.Tests/MockableGpioDriver.cs + public abstract class MockableGpioDriver : GpioDriver + { + private PinChangeEventHandler? _event; + + protected override int PinCount + { + get + { + return 28; + } + } + + public void FireEventHandler(int forPin, PinEventTypes eventTypes) + { + _event?.Invoke(this, new PinValueChangedEventArgs(eventTypes, forPin)); + } + + public abstract int ConvertPinNumberToLogicalNumberingSchemeEx(int pinNumber); + + protected override int ConvertPinNumberToLogicalNumberingScheme(int pinNumber) + { + return ConvertPinNumberToLogicalNumberingSchemeEx(pinNumber); + } + + public abstract void OpenPinEx(int pinNumber); + + protected override void OpenPin(int pinNumber) + { + OpenPinEx(pinNumber); + } + + public abstract void ClosePinEx(int pinNumber); + + protected override void ClosePin(int pinNumber) + { + ClosePinEx(pinNumber); + } + + public abstract void SetPinModeEx(int pinNumber, PinMode mode); + + protected override void SetPinMode(int pinNumber, PinMode mode) + { + SetPinModeEx(pinNumber, mode); + } + + public abstract PinMode GetPinModeEx(int pinNumber); + + protected override PinMode GetPinMode(int pinNumber) + { + return GetPinModeEx(pinNumber); + } + + public abstract bool IsPinModeSupportedEx(int pinNumber, PinMode mode); + + protected override bool IsPinModeSupported(int pinNumber, PinMode mode) + { + return IsPinModeSupportedEx(pinNumber, mode); + } + + public abstract PinValue ReadEx(int pinNumber); + + protected override PinValue Read(int pinNumber) + { + return ReadEx(pinNumber); + } + + public abstract void WriteEx(int pinNumber, PinValue value); + + protected override void Write(int pinNumber, PinValue value) + { + WriteEx(pinNumber, value); + } + + public abstract WaitForEventResult WaitForEventEx(int pinNumber, PinEventTypes eventTypes, CancellationToken cancellationToken); + + protected override WaitForEventResult WaitForEvent(int pinNumber, PinEventTypes eventTypes, CancellationToken cancellationToken) + { + return WaitForEventEx(pinNumber, eventTypes, cancellationToken); + } + + public abstract void AddCallbackForPinValueChangedEventEx(int pinNumber, PinEventTypes eventTypes, PinChangeEventHandler callback); + + protected override void AddCallbackForPinValueChangedEvent(int pinNumber, PinEventTypes eventTypes, PinChangeEventHandler callback) + { + _event = callback; + AddCallbackForPinValueChangedEventEx(pinNumber, eventTypes, callback); + } + + public abstract void RemoveCallbackForPinValueChangedEventEx(int pinNumber, PinChangeEventHandler callback); + + protected override void RemoveCallbackForPinValueChangedEvent(int pinNumber, PinChangeEventHandler callback) + { + RemoveCallbackForPinValueChangedEventEx(pinNumber, callback); + _event = null!; + } + } +} diff --git a/skullOS.Tests/skullOS.Tests.csproj b/skullOS.Tests/skullOS.Tests.csproj new file mode 100644 index 0000000..e3c6ea1 --- /dev/null +++ b/skullOS.Tests/skullOS.Tests.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + +