diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..9cff1bf --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "0.30.3", + "commands": [ + "dotnet-csharpier" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 29bfcb9..2456c9e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,74 +1,221 @@ +max_line_length = 120 +# EditorConfig is awesome:http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +charset = utf-8-bom + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +### Dotnet code style settings ### [*.{cs,vb}] -#### Naming styles #### +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true + +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning + +# Field preferences +dotnet_style_readonly_field = true:warning + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:warning + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +### CSharp code style settings ### +[*.cs] -# Naming rules +#### C# Coding Conventions #### + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = true:silent + +# Prefer method-like constructs to have a block body, except for lambdas +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none +csharp_style_expression_bodied_local_functions = false:none +csharp_style_expression_bodied_lambdas = true:none + + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:warning + +# Modifier preferences +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent + +# Code-block preferences +csharp_prefer_braces = when_multiline:warning +csharp_prefer_simple_using_statement = true:warning + +# Expression-level preferences +csharp_style_unused_value_assignment_preference = discard_variable:warning +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent -dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = no_change +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +### Visual Basic code style settings ### +[*.vb] + +# Expression-level preferences +visual_basic_style_unused_value_assignment_preference = unused_local_variable:warning + + +### Configuration for IDE code style by diagnostic IDs ### +[*.{cs,vb}] -dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.types_should_be_pascal_case.symbols = types -dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case +# IDE2000: Allow multiple blank lines +dotnet_style_allow_multiple_blank_lines_experimental = false -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case +# IDE2001: Embedded statements must be on their own line +csharp_style_allow_embedded_statements_on_same_line_experimental = false -# Symbol specifications +# IDE2002: Consecutive braces must not have blank line between them +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +# IDE2003: Blank line required between block and subsequent statement +dotnet_style_allow_statement_immediately_after_block_experimental = false -dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = +# IDE2004: Blank line not allowed after constructor initializer colon +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +### Configuration for .Net analyzers executed on this repo ### +[*.{cs,vb}] -# Naming styles +# Default analyzed API surface = 'all' (public APIs + non-public APIs) +dotnet_code_quality.api_surface = all -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case +# Restrict the analyzed API surface for certain analyzers to 'public' (public APIs only). +# CA1043: Use integral or string argument for indexers +dotnet_code_quality.CA1043.api_surface = public +# CA1707: Identifiers should not contain underscores +dotnet_code_quality.CA1707.api_surface = public +# CA1720: Identifiers should not contain type names +dotnet_code_quality.CA1720.api_surface = public -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case +# Exclude single letter type parameter names +# CA1715: Identifiers should have correct prefix +dotnet_code_quality.CA1715.exclude_single_letter_type_parameters = true -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -indent_size = 4 -end_of_line = crlf -[*.cs] -csharp_indent_labels = flush_left -csharp_using_directive_placement = outside_namespace:silent -csharp_prefer_simple_using_statement = false:suggestion -csharp_prefer_braces = true:silent -csharp_style_namespace_declarations = block_scoped:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = false:silent -csharp_style_prefer_primary_constructors = true:suggestion -csharp_style_expression_bodied_methods = true:silent -csharp_style_expression_bodied_constructors = true:silent -csharp_style_expression_bodied_operators = true:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent - -# ADDED +# CA1305: Pass IFormatProvider - https://github.com/dotnet/roslyn-analyzers/issues/6379 +dotnet_diagnostic.CA1305.severity = suggestion -[*.cs] -# Disable 'Use primary constructor' -dotnet_diagnostic.IDE0290.severity = none \ No newline at end of file +# CA1851: Possible multiple enumerations of 'IEnumerable' collection - https://github.com/dotnet/roslyn-analyzers/issues/6379 +dotnet_diagnostic.CA1851.severity = suggestion + +### Configuration for PublicAPI analyzers executed on this repo ### +[*.{cs,vb}] + +# Analyzers bail-out if the PublicAPI.*.txt file is not found +dotnet_public_api_analyzer.require_api_files = true \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..40e5256 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +############################################################################### +# Set default behaviour to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behaviour for command prompt diff. +############################################################################### +*.cs diff=csharp \ No newline at end of file diff --git a/.github/actions/check/action.yml b/.github/actions/check/action.yml new file mode 100644 index 0000000..dcd3595 --- /dev/null +++ b/.github/actions/check/action.yml @@ -0,0 +1,24 @@ +name: Run static checks +description: Runs formatting and style checks + +runs: + using: composite + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + + - name: Restore dependencies + shell: bash + run: | + dotnet restore + dotnet tool restore + + - name: Check formatting + shell: bash + run: dotnet csharpier --check . + + - name: Check style + shell: bash + run: | + dotnet format style backend24.sln --verify-no-changes --verbosity diagnostic + dotnet format analyzers backend24.sln --verify-no-changes --verbosity diagnostic diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..3eaf251 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,14 @@ +name: Run checks + +on: [push, pull_request] + +jobs: + call-skip: + uses: ./.github/workflows/skip.yml + secrets: inherit + + call-test: + needs: call-skip + if: ${{needs.call-skip.outputs.should_skip != 'true'}} + uses: ./.github/workflows/test.yml + secrets: inherit diff --git a/.github/workflows/skip.yml b/.github/workflows/skip.yml new file mode 100644 index 0000000..b251540 --- /dev/null +++ b/.github/workflows/skip.yml @@ -0,0 +1,22 @@ +name: Skip unnecessary workflow runs + +on: + workflow_call: + outputs: + should_skip: + value: ${{jobs.skip.outputs.should_skip}} + +jobs: + skip: + runs-on: ubuntu-latest + outputs: + should_skip: ${{steps.skip_check.outputs.should_skip}} + + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@v5 + with: + paths: '["metroidvania/Assets/Scripts/**", "metroidvania/Assets/Tests/**"]' + cancel_others: "true" + skip_after_successful_duplicate: "true" + concurrent_skipping: "same_content_newer" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..fad23af --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,13 @@ +name: Run checks and tests + +on: workflow_call + +jobs: + check: + name: Run style checks + runs-on: ubuntu-latest + # Skip checks for PR merges + if: (github.event_name == 'push' && contains(toJSON(github.event.head_commit.message), 'Merge pull request ') == false) + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/check diff --git a/Controllers/ServerEventsController.cs b/Controllers/ServerEventsController.cs index eb0ba45..8398578 100644 --- a/Controllers/ServerEventsController.cs +++ b/Controllers/ServerEventsController.cs @@ -1,70 +1,80 @@ using backend24.Extensions; - using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; namespace backend24.Controllers { - /// - /// Controller for handling server-sent events. - /// - [ApiController] - [Route("api/[action]")] - [EnableCors] - public class ServerEventsController : ControllerBase - { - // Logger provided by DI, used for printing information to all logging providers at once - private readonly ILogger _logger; - // List of registered event finalizers, which provide ready-to-send events - private readonly IEnumerable _eventFinalizers; + /// + /// Controller for handling server-sent events. + /// + [ApiController] + [Route("api/[action]")] + [EnableCors] + public class ServerEventsController : ControllerBase + { + // Logger provided by DI, used for printing information to all logging providers at once + private readonly ILogger _logger; + + // List of registered event finalizers, which provide ready-to-send events + private readonly IEnumerable _eventFinalizers; - /// - /// Create a new instance of ServerEventsController - /// - /// - /// - public ServerEventsController(ILogger logger, IEnumerable eventFinalizers) { - _logger = logger; - _eventFinalizers = eventFinalizers; - _logger.LogInformation("DI provided {evtFinalizerCount} event finalizers.", _eventFinalizers.Count().ToString()); - } + /// + /// Create a new instance of ServerEventsController + /// + /// + /// + public ServerEventsController( + ILogger logger, + IEnumerable eventFinalizers + ) + { + _logger = logger; + _eventFinalizers = eventFinalizers; + _logger.LogInformation( + "DI provided {evtFinalizerCount} event finalizers.", + _eventFinalizers.Count().ToString() + ); + } - /// - /// Provide a GET endpoint for connecting to the SSE channel - /// - /// Good question - [HttpGet()] - public async Task SSE() { - // Set the response headers; this tells the client we're initiating SSE - Response.Headers.ContentType = "text/event-stream"; - Response.Headers.CacheControl = "no-cache"; - Response.Headers.Connection = "keep-alive"; + /// + /// Provide a GET endpoint for connecting to the SSE channel + /// + /// Good question + [HttpGet()] + public async Task SSE() + { + // Set the response headers; this tells the client we're initiating SSE + Response.Headers.ContentType = "text/event-stream"; + Response.Headers.CacheControl = "no-cache"; + Response.Headers.Connection = "keep-alive"; foreach (var eventFinalizer in _eventFinalizers) { - // Subscribe to finalizers - eventFinalizer.OnDataProvided += async payload => { - // Leaving these here just in case... - //_logger.LogInformation("Sending event provided by {evtFinalizerType}.", eventFinalizer.GetType().Name); - //_logger.LogDebug("Tag: {tag}\nContent: {content}", payload.Data.tag, payload.Data.content); + // Subscribe to finalizers + eventFinalizer.OnDataProvided += async payload => + { + // Leaving these here just in case... + //_logger.LogInformation("Sending event provided by {evtFinalizerType}.", eventFinalizer.GetType().Name); + //_logger.LogDebug("Tag: {tag}\nContent: {content}", payload.Data.tag, payload.Data.content); - // Send the tagged event in a properly formatted way - await Response.WriteAsync($"event: {payload.Data.tag}\n"); - await Response.WriteAsync($"data: "); - // Convert the content to JSON - await Response.WriteJSONAsync(payload.Data.content); - await Response.WriteAsync("@"); - await Response.WriteJSONAsync(payload.DataStamp); - await Response.WriteAsync("\n\n"); - await Response.Body.FlushAsync(); - }; + // Send the tagged event in a properly formatted way + await Response.WriteAsync($"event: {payload.Data.tag}\n"); + await Response.WriteAsync($"data: "); + // Convert the content to JSON + await Response.WriteJSONAsync(payload.Data.content); + await Response.WriteAsync("@"); + await Response.WriteJSONAsync(payload.DataStamp); + await Response.WriteAsync("\n\n"); + await Response.Body.FlushAsync(); + }; } - // Keep the server alive - // This feels very dodgy - while (true) { - await Task.Delay(1000); - } - } - } + // Keep the server alive + // This feels very dodgy + while (true) + { + await Task.Delay(1000); + } + } + } } diff --git a/Extensions/ASPExtensions.cs b/Extensions/ASPExtensions.cs index fd6fd6a..45fe9cd 100644 --- a/Extensions/ASPExtensions.cs +++ b/Extensions/ASPExtensions.cs @@ -2,35 +2,57 @@ namespace backend24.Extensions { - public static class ASPExtensions - { - /// - /// Add a finalizer service to an IServiceCollection. - /// - /// Type of the finalizer to be added - /// - /// - public static IServiceCollection AddFinalizer(this IServiceCollection services) where TFinalizer : class, IFinalizedProvider { - // Two singletons are added to expose the same object as both a TFinalizer, whatever it might be, and an IFinalizedProvider - // At least I think that's the reason... - return services.AddSingleton().AddSingleton(provider => (IFinalizedProvider)provider.GetRequiredService()); - } + public static class ASPExtensions + { + /// + /// Add a finalizer service to an IServiceCollection. + /// + /// Type of the finalizer to be added + /// + /// + public static IServiceCollection AddFinalizer(this IServiceCollection services) + where TFinalizer : class, IFinalizedProvider + { + // Two singletons are added to expose the same object as both a TFinalizer, whatever it might be, and an IFinalizedProvider + // At least I think that's the reason... + return services + .AddSingleton() + .AddSingleton(provider => + (IFinalizedProvider)provider.GetRequiredService() + ); + } - /// - /// Write an object encoded as JSON to an HttpResponse - /// - /// - /// The object to be encoded and written - /// - /// - public static Task WriteJSONAsync(this HttpResponse response, object value, CancellationToken cancellationToken = default) { - JsonSerializerOptions opts = new JsonSerializerOptions { NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals }; + /// + /// Write an object encoded as JSON to an HttpResponse + /// + /// + /// The object to be encoded and written + /// + /// + public static Task WriteJSONAsync( + this HttpResponse response, + object value, + CancellationToken cancellationToken = default + ) + { + JsonSerializerOptions opts = new JsonSerializerOptions + { + NumberHandling = System + .Text + .Json + .Serialization + .JsonNumberHandling + .AllowNamedFloatingPointLiterals, + }; - // WriteAsync does this, so it's done here too - ArgumentNullException.ThrowIfNull(response); - ArgumentNullException.ThrowIfNull(value); + // WriteAsync does this, so it's done here too + ArgumentNullException.ThrowIfNull(response); + ArgumentNullException.ThrowIfNull(value); - return response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(value, opts), cancellationToken); - } - } + return response.WriteAsync( + System.Text.Json.JsonSerializer.Serialize(value, opts), + cancellationToken + ); + } + } } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..15fdf71 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY: install installhooks installdotnet + +install: installhooks installdotnet + +installhooks: + cp githooks/* .git/hooks + chmod +x .git/hooks/* + +installdotnet: + dotnet tool restore \ No newline at end of file diff --git a/Models/Acceleration.cs b/Models/Acceleration.cs index 0b03fca..64af23b 100644 --- a/Models/Acceleration.cs +++ b/Models/Acceleration.cs @@ -1,9 +1,9 @@ namespace backend24.Models { - public readonly struct Acceleration - { - public float X { init; get; } - public float Y { init; get; } - public float Z { init; get; } - } + public readonly struct Acceleration + { + public float X { init; get; } + public float Y { init; get; } + public float Z { init; get; } + } } diff --git a/Models/DataStamp.cs b/Models/DataStamp.cs index a51653f..cd3ae7c 100644 --- a/Models/DataStamp.cs +++ b/Models/DataStamp.cs @@ -1,18 +1,19 @@ namespace backend24.Models { - /// - /// Reference data which is attached to all data. - /// - public readonly struct DataStamp - { - /// - /// Milliseconds since the Arduino was initialized, registered - /// when the data was sent. - /// - public long Timestamp { get; init; } - /// - /// Coordinates registered by the GPS when the data was sent - /// - public GPSCoords Coordinates { get; init; } - } + /// + /// Reference data which is attached to all data. + /// + public readonly struct DataStamp + { + /// + /// Milliseconds since the Arduino was initialized, registered + /// when the data was sent. + /// + public long Timestamp { get; init; } + + /// + /// Coordinates registered by the GPS when the data was sent + /// + public GPSCoords Coordinates { get; init; } + } } diff --git a/Models/EventData.cs b/Models/EventData.cs index 8f15b06..313b2d3 100644 --- a/Models/EventData.cs +++ b/Models/EventData.cs @@ -5,8 +5,8 @@ /// object /// /// - public readonly struct EventData - { + public readonly struct EventData + { public DataStamp DataStamp { get; init; } public T Data { get; init; } } diff --git a/Models/GPSCoords.cs b/Models/GPSCoords.cs index 06b5136..b30ae1e 100644 --- a/Models/GPSCoords.cs +++ b/Models/GPSCoords.cs @@ -2,26 +2,28 @@ namespace backend24.Models { - /// - /// Represents GPS coordinates - latitude, longitude and altitude. - /// - public readonly struct GPSCoords - { - /// - /// Vertical angle, measured with respect to the equator. - /// -90º is the south pole, 0º is the equator and 90º is the north pole. - /// - [Range(-90,90)] + /// + /// Represents GPS coordinates - latitude, longitude and altitude. + /// + public readonly struct GPSCoords + { + /// + /// Vertical angle, measured with respect to the equator. + /// -90º is the south pole, 0º is the equator and 90º is the north pole. + /// + [Range(-90, 90)] public float Latitude { get; init; } - /// - /// Horizontal angle, measured with respect to the Greenwich semi-meridian. - /// 0º is the Greenwich semi-meridian, 180º and -180º - /// - [Range(-180, 180)] - public float Longitude { get; init; } - /// - /// Meters above average sea level. - /// - public float Altitude { get; init; } - } + + /// + /// Horizontal angle, measured with respect to the Greenwich semi-meridian. + /// 0º is the Greenwich semi-meridian, 180º and -180º + /// + [Range(-180, 180)] + public float Longitude { get; init; } + + /// + /// Meters above average sea level. + /// + public float Altitude { get; init; } + } } diff --git a/Program.cs b/Program.cs index 0683061..9958138 100644 --- a/Program.cs +++ b/Program.cs @@ -18,54 +18,86 @@ public static void Main(string[] args) { builder.Logging.ClearProviders(); builder.Logging.AddConsole(); builder.Logging.AddFile("Logs/log.txt"); - // Get the name of the serial port where data is arriving - Console.WriteLine("Enter the name of the serial port where the APC220 module is connected.\nAvailable ports are:"); - Console.Write(string.Concat(SerialPort.GetPortNames().Select(x => "\t" + x + "\n")) + "> "); - string serialPortName = Console.ReadLine()!; - // Add services to the container. - // Register internal services, using keyed services - builder.Services - .AddKeyedSingleton>, SerialProvider>(ServiceKeys.SerialProvider, - (serviceProvider, _) => ActivatorUtilities.CreateInstance(serviceProvider, serialPortName, 19200, Parity.None)) - .AddKeyedSingleton, PressureExtractor>(ServiceKeys.PressureExtractor) - .AddFinalizer() - .AddKeyedSingleton, TemperatureExtractor>(ServiceKeys.TemperatureExtractor) - .AddFinalizer() - .AddKeyedSingleton, AltitudeExtractor>(ServiceKeys.AltitudeExtractor) - .AddFinalizer() - .AddKeyedSingleton, AltitudeGPSExtractor>(ServiceKeys.AltitudeGPSExtractor) - .AddFinalizer() - .AddKeyedSingleton, AltitudeDeltaProcessor>(ServiceKeys.AltitudeDeltaProcessor) - .AddFinalizer() - .AddKeyedSingleton, VelocityProcessor>(ServiceKeys.VelocityProcessor) - .AddFinalizer() - ; + // Get the name of the serial port where data is arriving + Console.WriteLine( + "Enter the name of the serial port where the APC220 module is connected.\nAvailable ports are:" + ); + Console.Write( + string.Concat(SerialPort.GetPortNames().Select(x => "\t" + x + "\n")) + "> " + ); + string serialPortName = Console.ReadLine()!; - // This will register all classes annotated with ApiController - builder.Services.AddControllers(); - // Set up Swagger/OpenAPI (learn more at https://aka.ms/aspnetcore/swashbuckle) - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); + // Add services to the container. + // Register internal services, using keyed services + builder + .Services.AddKeyedSingleton< + IDataProvider>, + SerialProvider + >( + ServiceKeys.SerialProvider, + (serviceProvider, _) => + ActivatorUtilities.CreateInstance( + serviceProvider, + serialPortName, + 19200, + Parity.None + ) + ) + .AddKeyedSingleton, PressureExtractor>( + ServiceKeys.PressureExtractor + ) + .AddFinalizer() + .AddKeyedSingleton, TemperatureExtractor>( + ServiceKeys.TemperatureExtractor + ) + .AddFinalizer() + .AddKeyedSingleton, AltitudeExtractor>( + ServiceKeys.AltitudeExtractor + ) + .AddFinalizer() + .AddKeyedSingleton, AltitudeGPSExtractor>( + ServiceKeys.AltitudeGPSExtractor + ) + .AddFinalizer() + .AddKeyedSingleton, AltitudeDeltaProcessor>( + ServiceKeys.AltitudeDeltaProcessor + ) + .AddFinalizer() + .AddKeyedSingleton, VelocityProcessor>( + ServiceKeys.VelocityProcessor + ) + .AddFinalizer(); - builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod())); + // This will register all classes annotated with ApiController + builder.Services.AddControllers(); + // Set up Swagger/OpenAPI (learn more at https://aka.ms/aspnetcore/swashbuckle) + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); - // Build an app from the configuration. - var app = builder.Build(); + builder.Services.AddCors(options => + options.AddDefaultPolicy(policy => + policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod() + ) + ); - // Configure the HTTP request pipeline. - if(app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); - } + // Build an app from the configuration. + var app = builder.Build(); - app.UseHttpsRedirection(); - app.UseCors(); - app.UseAuthorization(); // TODO: Research this - is it necessary? - app.MapControllers(); + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } - // Start the app. - app.Run(); - } - } -} \ No newline at end of file + app.UseHttpsRedirection(); + app.UseCors(); + app.UseAuthorization(); // TODO: Research this - is it necessary? + app.MapControllers(); + + // Start the app. + app.Run(); + } + } +} diff --git a/README.md b/README.md index 2a78475..44a8189 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,44 @@ -## Project structure -- Controllers: classes derived from ``ControlerBase``, implementing API endpoints - - ``ServerEventsController``: responsible for opening an SSE channel and sending tagged data to the client. Provides the following endpoint for subscribing to SSE: ``api/sse``. All data sent will be collected from objects marked with ``EventFinalizerAttribute``, which will be automatically registered at startup. +## Developing + +After cloning the repository, please run the following command: + +```bash +make install +``` + +This will install all the necessary tools and Git hooks to enforce proper style. + +## Project structure + +- Controllers: classes derived from `ControlerBase`, implementing API endpoints + - `ServerEventsController`: responsible for opening an SSE channel and sending tagged data to the client. Provides the following endpoint for subscribing to SSE: `api/sse`. All data sent will be collected from objects marked with `EventFinalizerAttribute`, which will be automatically registered at startup. - Models: classes defining data models for the app to work with - - ``GPSCoords``: represents GPS coordinates (latitude, longitude, altitude) - - ``DataStamp``: general metadata attached to all data sent, e.g., timestamp and coordinates - - ``EventData``: wrapper for data flowing through the app, packs the main data with a ``DataStamp`` object + - `GPSCoords`: represents GPS coordinates (latitude, longitude, altitude) + - `DataStamp`: general metadata attached to all data sent, e.g., timestamp and coordinates + - `EventData`: wrapper for data flowing through the app, packs the main data with a `DataStamp` object - Services: Modular classes implementing internal functionality - - ``DataProviders``: classes responsible for emitting data. All data providers must implement ``IDataProvider``. A consumer class can subscribe to a provider's ``OnDataProvided`` to be notified whenever new data is available. - - ``RandomProvider``: provides random floats on a configurable interval. - - ``SerialProvider``: provides string arrays from a serial port. - - ``DataProcessors``: classes responsible for transforming data. Notably, every processor both subscribes to a provider to get new data to transform, and is itself a provider, emiting a new event after it has transformed the data. - - ``DataExtractors``: classes responsible for extracting a specific piece of data from ``SerialProvider`` data. - - ``EventFinalizers``: classes responsible for finalizing an event, i.e., collecting the necessary data and tagging it properly. Finalizers must be ``IDataProviders`` (though they'll usually be processors) marked with ``EventFinalizerAttribute``. Each finalizer is responsible for only one tagged events. The following tags are (or will be) provided, along with their respective finalizers. - - ``primary/pressure`` by ``PressureFinalizer`` - - ``primary/temperature`` by ``TemperatureFinalizer`` - - ``primary/altitude`` by ``AltitudeFinalizer`` - - ``secondary/raw`` - - ``seconddary/ndvi`` - - ``general/acceleration`` - - ``general/position`` - - ``general/raw`` + - `DataProviders`: classes responsible for emitting data. All data providers must implement `IDataProvider`. A consumer class can subscribe to a provider's `OnDataProvided` to be notified whenever new data is available. + - `RandomProvider`: provides random floats on a configurable interval. + - `SerialProvider`: provides string arrays from a serial port. + - `DataProcessors`: classes responsible for transforming data. Notably, every processor both subscribes to a provider to get new data to transform, and is itself a provider, emiting a new event after it has transformed the data. + - `DataExtractors`: classes responsible for extracting a specific piece of data from `SerialProvider` data. + - `EventFinalizers`: classes responsible for finalizing an event, i.e., collecting the necessary data and tagging it properly. Finalizers must be `IDataProviders` (though they'll usually be processors) marked with `EventFinalizerAttribute`. Each finalizer is responsible for only one tagged events. The following tags are (or will be) provided, along with their respective finalizers. + - `primary/pressure` by `PressureFinalizer` + - `primary/temperature` by `TemperatureFinalizer` + - `primary/altitude` by `AltitudeFinalizer` + - `secondary/raw` + - `seconddary/ndvi` + - `general/acceleration` + - `general/position` + - `general/raw` ## Data flow -- All data is encapsulated in ``EventData`` objects, which pack the actual data together with a ``DataStamp``, which in turn contains mandatory information (like timestamp and GPS coordinates) -- ``IDataProvider``s emit an event when they get data -- ``IDataProcessor``s subscribe to providers, process their data and send a new event + +- All data is encapsulated in `EventData` objects, which pack the actual data together with a `DataStamp`, which in turn contains mandatory information (like timestamp and GPS coordinates) +- `IDataProvider`s emit an event when they get data +- `IDataProcessor`s subscribe to providers, process their data and send a new event - The main controller finds and subscribes to finalizers and communicates their data to the client through Server Sent Events (SSE) ## Notes and useful things -Most browsers have a limit of 6 connections per domain. Since each SSE endpoint represents a connection that stays open indeterminatly, we have to be very careful when subscribing to SSEs. However, response bodies consist of ``data`` tags and ``event`` tags, so we can have a single endpoint which sends all the data. Thus the endpoints specified above become internal spearations which all write to the same endpoint. \ No newline at end of file + +Most browsers have a limit of 6 connections per domain. Since each SSE endpoint represents a connection that stays open indeterminatly, we have to be very careful when subscribing to SSEs. However, response bodies consist of `data` tags and `event` tags, so we can have a single endpoint which sends all the data. Thus the endpoints specified above become internal spearations which all write to the same endpoint. diff --git a/Services/DataProcessors/AltitudeDeltaProcessor.cs b/Services/DataProcessors/AltitudeDeltaProcessor.cs index 8568fb9..5d4767f 100644 --- a/Services/DataProcessors/AltitudeDeltaProcessor.cs +++ b/Services/DataProcessors/AltitudeDeltaProcessor.cs @@ -3,15 +3,16 @@ namespace backend24.Services.DataProcessors { - public class AltitudeDeltaProcessor : DataProcessorBase - { - public AltitudeDeltaProcessor([FromKeyedServices(ServiceKeys.AltitudeExtractor)]IDataProvider provider) : base(provider) { - } + public class AltitudeDeltaProcessor : DataProcessorBase + { + public AltitudeDeltaProcessor( + [FromKeyedServices(ServiceKeys.AltitudeExtractor)] IDataProvider provider + ) + : base(provider) { } - protected override EventData Process(EventData data) { - return data with { - Data = data.Data - data.DataStamp.Coordinates.Altitude - }; - } - } + protected override EventData Process(EventData data) + { + return data with { Data = data.Data - data.DataStamp.Coordinates.Altitude }; + } + } } diff --git a/Services/DataProcessors/DataExtractors/AltitudeExtractor.cs b/Services/DataProcessors/DataExtractors/AltitudeExtractor.cs index fab1ece..486fc55 100644 --- a/Services/DataProcessors/DataExtractors/AltitudeExtractor.cs +++ b/Services/DataProcessors/DataExtractors/AltitudeExtractor.cs @@ -2,31 +2,41 @@ namespace backend24.Services.DataProcessors.DataExtractors { - /// - /// Computes altitude from pressure and temperature data - /// - public class AltitudeExtractor : DataExtractorBase - { - public AltitudeExtractor([FromKeyedServices(ServiceKeys.SerialProvider)] IDataProvider> provider) : base(provider) { - _sourceIndexes = [SerialProvider.DataLabel.Pressure, SerialProvider.DataLabel.Temperature]; - } + /// + /// Computes altitude from pressure and temperature data + /// + public class AltitudeExtractor : DataExtractorBase + { + public AltitudeExtractor( + [FromKeyedServices(ServiceKeys.SerialProvider)] + IDataProvider> provider + ) + : base(provider) + { + _sourceIndexes = + [ + SerialProvider.DataLabel.Pressure, + SerialProvider.DataLabel.Temperature, + ]; + } - protected override float Convert(IEnumerable data) { - float pressure = float.Parse(data.First()); - // Add 273.15 to convert from Celsius to kelvin - float temperature = float.Parse(data.Last()) + 273.15f; + protected override float Convert(IEnumerable data) + { + float pressure = float.Parse(data.First()); + // Add 273.15 to convert from Celsius to kelvin + float temperature = float.Parse(data.Last()) + 273.15f; - // Calculate the altitude from pressure and temperature. Based on the first formula from - // https://physics.stackexchange.com/questions/333475/how-to-calculate-altitude-from-current-temperature-and-pressure - // Physical constants are declared as variables in favour of readability and future changes to values. - // Their values are obtained from https://en.wikipedia.org/wiki/Barometric_formula. For layer-varying values, - // b=0 is used, as it appears to range between 0 and 11000 meters, so our maximum altitude of 1000 meters sits - // comfortably under the threshold. - float pressureRef = 101325f; - // This represents the exponent (g0 * M)/(R * L) - float exp = 5.2558f; - float lapseRate = 0.0065f; - return temperature * (MathF.Pow(pressureRef / pressure, 1 / exp) - 1) / lapseRate; - } - } + // Calculate the altitude from pressure and temperature. Based on the first formula from + // https://physics.stackexchange.com/questions/333475/how-to-calculate-altitude-from-current-temperature-and-pressure + // Physical constants are declared as variables in favour of readability and future changes to values. + // Their values are obtained from https://en.wikipedia.org/wiki/Barometric_formula. For layer-varying values, + // b=0 is used, as it appears to range between 0 and 11000 meters, so our maximum altitude of 1000 meters sits + // comfortably under the threshold. + float pressureRef = 101325f; + // This represents the exponent (g0 * M)/(R * L) + float exp = 5.2558f; + float lapseRate = 0.0065f; + return temperature * (MathF.Pow(pressureRef / pressure, 1 / exp) - 1) / lapseRate; + } + } } diff --git a/Services/DataProcessors/DataExtractors/AltitudeGPSExtractor.cs b/Services/DataProcessors/DataExtractors/AltitudeGPSExtractor.cs index b29d3da..7fd283b 100644 --- a/Services/DataProcessors/DataExtractors/AltitudeGPSExtractor.cs +++ b/Services/DataProcessors/DataExtractors/AltitudeGPSExtractor.cs @@ -1,18 +1,24 @@ - -using backend24.Services.DataProviders; +using backend24.Services.DataProviders; namespace backend24.Services.DataProcessors.DataExtractors { - public class AltitudeGPSExtractor : DataExtractorBase - { - public AltitudeGPSExtractor([FromKeyedServices(ServiceKeys.SerialProvider)] IDataProvider> provider) : base(provider) { - _sourceIndexes = [SerialProvider.DataLabel.Altitude]; - } + public class AltitudeGPSExtractor : DataExtractorBase + { + public AltitudeGPSExtractor( + [FromKeyedServices(ServiceKeys.SerialProvider)] + IDataProvider> provider + ) + : base(provider) + { + _sourceIndexes = [SerialProvider.DataLabel.Altitude]; + } - protected override float Convert(IEnumerable data) { - float altitude = 0; - if(data.First() != "nan") altitude = float.Parse(data.First()); - return altitude; - } - } + protected override float Convert(IEnumerable data) + { + float altitude = 0; + if (data.First() != "nan") + altitude = float.Parse(data.First()); + return altitude; + } + } } diff --git a/Services/DataProcessors/DataExtractors/DataExtractorBase.cs b/Services/DataProcessors/DataExtractors/DataExtractorBase.cs index b7d8b90..163ce3c 100644 --- a/Services/DataProcessors/DataExtractors/DataExtractorBase.cs +++ b/Services/DataProcessors/DataExtractors/DataExtractorBase.cs @@ -3,32 +3,41 @@ namespace backend24.Services.DataProcessors.DataExtractors { - /// - /// Base class for data extractors, i.e., classes which extract individual pieces of data from a SerialProvider - /// - /// - public abstract class DataExtractorBase : DataProcessorBase, T> - { - /// - /// Indexes of the required data pieces in the array provided by SerialProvider - /// - protected SerialProvider.DataLabel[] _sourceIndexes = []; + /// + /// Base class for data extractors, i.e., classes which extract individual pieces of data from a SerialProvider + /// + /// + public abstract class DataExtractorBase + : DataProcessorBase, T> + { + /// + /// Indexes of the required data pieces in the array provided by SerialProvider + /// + protected SerialProvider.DataLabel[] _sourceIndexes = []; - protected DataExtractorBase([FromKeyedServices(ServiceKeys.SerialProvider)]IDataProvider> provider) : base(provider) { - } + protected DataExtractorBase( + [FromKeyedServices(ServiceKeys.SerialProvider)] + IDataProvider> provider + ) + : base(provider) { } - /// - /// Convert the data piece into the proper format - /// - /// Data piece as a string - /// Data piece as - protected abstract T Convert(IEnumerable data); - protected override EventData Process(EventData> data) { - return new EventData() { - DataStamp = data.DataStamp, - // Select a subset of the data pieces - Data = Convert(_sourceIndexes.Select(x => data.Data[x])) - }; - } - } + /// + /// Convert the data piece into the proper format + /// + /// Data piece as a string + /// Data piece as + protected abstract T Convert(IEnumerable data); + + protected override EventData Process( + EventData> data + ) + { + return new EventData() + { + DataStamp = data.DataStamp, + // Select a subset of the data pieces + Data = Convert(_sourceIndexes.Select(x => data.Data[x])), + }; + } + } } diff --git a/Services/DataProcessors/DataExtractors/PressureExtractor.cs b/Services/DataProcessors/DataExtractors/PressureExtractor.cs index 33bc96e..36bed83 100644 --- a/Services/DataProcessors/DataExtractors/PressureExtractor.cs +++ b/Services/DataProcessors/DataExtractors/PressureExtractor.cs @@ -2,16 +2,21 @@ namespace backend24.Services.DataProcessors.DataExtractors { - /// - /// Extracts pressure data from SerialProvider data. - /// - public class PressureExtractor : DataExtractorBase - { - public PressureExtractor([FromKeyedServices(ServiceKeys.SerialProvider)] IDataProvider> provider) : base(provider) { - // Extract pressure data - _sourceIndexes = [SerialProvider.DataLabel.Pressure]; - } + /// + /// Extracts pressure data from SerialProvider data. + /// + public class PressureExtractor : DataExtractorBase + { + public PressureExtractor( + [FromKeyedServices(ServiceKeys.SerialProvider)] + IDataProvider> provider + ) + : base(provider) + { + // Extract pressure data + _sourceIndexes = [SerialProvider.DataLabel.Pressure]; + } - protected override float Convert(IEnumerable data) => float.Parse(data.First()); - } + protected override float Convert(IEnumerable data) => float.Parse(data.First()); + } } diff --git a/Services/DataProcessors/DataExtractors/TemperatureExtractor.cs b/Services/DataProcessors/DataExtractors/TemperatureExtractor.cs index ebe3fad..2353aa1 100644 --- a/Services/DataProcessors/DataExtractors/TemperatureExtractor.cs +++ b/Services/DataProcessors/DataExtractors/TemperatureExtractor.cs @@ -2,16 +2,21 @@ namespace backend24.Services.DataProcessors.DataExtractors { - /// - /// Extracts temperature data from SerialProvider data. - /// - public class TemperatureExtractor : DataExtractorBase - { - public TemperatureExtractor([FromKeyedServices(ServiceKeys.SerialProvider)] IDataProvider> provider) : base(provider) { - // Extract temperature data - _sourceIndexes = [SerialProvider.DataLabel.Temperature]; - } + /// + /// Extracts temperature data from SerialProvider data. + /// + public class TemperatureExtractor : DataExtractorBase + { + public TemperatureExtractor( + [FromKeyedServices(ServiceKeys.SerialProvider)] + IDataProvider> provider + ) + : base(provider) + { + // Extract temperature data + _sourceIndexes = [SerialProvider.DataLabel.Temperature]; + } - protected override float Convert(IEnumerable data) => float.Parse(data.First()); - } + protected override float Convert(IEnumerable data) => float.Parse(data.First()); + } } diff --git a/Services/DataProcessors/DataProcessorBase.cs b/Services/DataProcessors/DataProcessorBase.cs index 9b70be5..d58c209 100644 --- a/Services/DataProcessors/DataProcessorBase.cs +++ b/Services/DataProcessors/DataProcessorBase.cs @@ -3,33 +3,36 @@ namespace backend24.Services.DataProcessors { - /// - /// Abstract base class for a data processor. - /// Subscribes to an IDataProvider, transforms the T1 it provides into a T2, and re-sends the event - /// - /// The data type received by the processor - /// The data type emitted by the processor - public abstract class DataProcessorBase : IDataProvider - { - public event Action>? OnDataProvided; + /// + /// Abstract base class for a data processor. + /// Subscribes to an IDataProvider, transforms the T1 it provides into a T2, and re-sends the event + /// + /// The data type received by the processor + /// The data type emitted by the processor + public abstract class DataProcessorBase : IDataProvider + { + public event Action>? OnDataProvided; - /// - /// Transform data from into - /// - /// The data to be processed - /// The processed data - protected abstract EventData Process(EventData data); - /// - /// Re-sends the data that is received, after processing it. - /// - /// The data to be processed and re-sent - void BubbleUp(EventData data) { - OnDataProvided?.Invoke(Process(data)); - } + /// + /// Transform data from into + /// + /// The data to be processed + /// The processed data + protected abstract EventData Process(EventData data); - public DataProcessorBase(IDataProvider provider) { - // Subscribe to a provider - provider.OnDataProvided += BubbleUp; - } - } + /// + /// Re-sends the data that is received, after processing it. + /// + /// The data to be processed and re-sent + void BubbleUp(EventData data) + { + OnDataProvided?.Invoke(Process(data)); + } + + public DataProcessorBase(IDataProvider provider) + { + // Subscribe to a provider + provider.OnDataProvided += BubbleUp; + } + } } diff --git a/Services/DataProcessors/VelocityProcessor.cs b/Services/DataProcessors/VelocityProcessor.cs index 099800b..57b6eb6 100644 --- a/Services/DataProcessors/VelocityProcessor.cs +++ b/Services/DataProcessors/VelocityProcessor.cs @@ -3,21 +3,24 @@ namespace backend24.Services.DataProcessors { - public class VelocityProcessor : DataProcessorBase { - float _last = float.NaN; + public class VelocityProcessor : DataProcessorBase + { + float _last = float.NaN; - public VelocityProcessor([FromKeyedServices(ServiceKeys.AltitudeExtractor)] IDataProvider provider) : base(provider) { - } + public VelocityProcessor( + [FromKeyedServices(ServiceKeys.AltitudeExtractor)] IDataProvider provider + ) + : base(provider) { } - protected override EventData Process(EventData data) { - float vel = 0; - if(!float.IsNaN(_last)) { - vel = data.Data - _last; - } - _last = data.Data; - return data with { - Data = vel - }; - } - } + protected override EventData Process(EventData data) + { + float vel = 0; + if (!float.IsNaN(_last)) + { + vel = data.Data - _last; + } + _last = data.Data; + return data with { Data = vel }; + } + } } diff --git a/Services/DataProviders/IDataProvider.cs b/Services/DataProviders/IDataProvider.cs index f2e9cec..20567e3 100644 --- a/Services/DataProviders/IDataProvider.cs +++ b/Services/DataProviders/IDataProvider.cs @@ -2,16 +2,16 @@ namespace backend24.Services.DataProviders { - /// - /// Contract for a data provider. - /// - /// The type of the data provided - public interface IDataProvider - { - /// - /// Event triggered whenever the provider has new data. - /// Subscribe to this event to act on the new data. - /// - public event Action>? OnDataProvided; - } + /// + /// Contract for a data provider. + /// + /// The type of the data provided + public interface IDataProvider + { + /// + /// Event triggered whenever the provider has new data. + /// Subscribe to this event to act on the new data. + /// + public event Action>? OnDataProvided; + } } diff --git a/Services/DataProviders/SerialProvider.cs b/Services/DataProviders/SerialProvider.cs index f63569e..9042a6a 100644 --- a/Services/DataProviders/SerialProvider.cs +++ b/Services/DataProviders/SerialProvider.cs @@ -1,15 +1,16 @@ -using backend24.Models; - -using System; +using System; using System.IO.Ports; using System.Text.RegularExpressions; +using backend24.Models; namespace backend24.Services.DataProviders { /// /// Provides data read from a serial port. /// - public sealed class SerialProvider : IDataProvider>, IDisposable + public sealed class SerialProvider + : IDataProvider>, + IDisposable { /// /// Represent the index of each data piece in the list provided by a SerialProvider. @@ -27,7 +28,7 @@ public enum DataLabel AccelerationZ, Latitude, Longitude, - Altitude + Altitude, } public event Action>>? OnDataProvided; @@ -38,8 +39,8 @@ public enum DataLabel private readonly Dictionary _schema; private string _buffer = ""; - - private readonly System.Timers.Timer _timer; + + private readonly System.Timers.Timer _timer; /// /// Create a new instance of SerialProvider @@ -48,20 +49,26 @@ public enum DataLabel /// Baud rate, in bps, of the serial port /// Parity of the serial port /// - public SerialProvider(string portName, int baudRate, Parity parity, ILogger logger) + public SerialProvider( + string portName, + int baudRate, + Parity parity, + ILogger logger + ) { _logger = logger; // Initialize schema with invalid values - _schema = new Dictionary { - {DataLabel.Timestamp, -1 }, - {DataLabel.Pressure, -1}, - {DataLabel.Temperature, -1}, - {DataLabel.AccelerationX, -1}, - {DataLabel.AccelerationY, -1}, - {DataLabel.AccelerationZ, -1}, - {DataLabel.Latitude, -1}, - {DataLabel.Longitude, -1}, - {DataLabel.Altitude, -1}, + _schema = new Dictionary + { + { DataLabel.Timestamp, -1 }, + { DataLabel.Pressure, -1 }, + { DataLabel.Temperature, -1 }, + { DataLabel.AccelerationX, -1 }, + { DataLabel.AccelerationY, -1 }, + { DataLabel.AccelerationZ, -1 }, + { DataLabel.Latitude, -1 }, + { DataLabel.Longitude, -1 }, + { DataLabel.Altitude, -1 }, }; // Note that more options are available for configuring a SerialPort, // namely data bits, stop bits and handshake. I have no idea what those @@ -73,40 +80,40 @@ public SerialProvider(string portName, int baudRate, Parity parity, ILogger + + /// /// Handle data being received on the serial port /// /// The SerialPort object which raised the event /// private void HandleDataReceived(object? sender, System.Timers.ElapsedEventArgs e) { - _logger.LogInformation("Receiving..."); + _logger.LogInformation("Receiving..."); _buffer += _serialPort.ReadExisting(); _buffer = _buffer.TrimStart(); - while(_buffer.Contains('\n')){ + while (_buffer.Contains('\n')) + { int idx = _buffer.IndexOf('\n'); - string line = _buffer[..idx]; + string line = _buffer[..idx]; _buffer = _buffer.Remove(0, line.Length); HandleLineReceived(line); AppendToFile(line); } - } + } private void HandleLineReceived(string line) { - _logger.LogInformation("LINE: {line}", line);; + _logger.LogInformation("LINE: {line}", line); + ; // Check for schema message if (line.StartsWith("schema", StringComparison.CurrentCultureIgnoreCase)) { @@ -115,7 +122,7 @@ private void HandleLineReceived(string line) } // Start processing data only after a schema has arrived - //_logger.LogInformation("Should"); + //_logger.LogInformation("Should"); if (!_schema.ContainsValue(-1)) { if (line.Trim() != "") @@ -152,12 +159,17 @@ private void ParseSchema(string schema) "latitude" => DataLabel.Latitude, "longitude" => DataLabel.Longitude, "altitude" => DataLabel.Altitude, - string entry => throw new InvalidDataException($"Received unknown schema entry {entry} from serial port, consider adding a new item to {nameof(DataLabel)}") + string entry => throw new InvalidDataException( + $"Received unknown schema entry {entry} from serial port, consider adding a new item to {nameof(DataLabel)}" + ), }; _schema[key] = i; } - if (_schema.ContainsValue(-1)) throw new InvalidDataException($"Schema received from serial port didn't contain entry for every {nameof(DataLabel)}"); + if (_schema.ContainsValue(-1)) + throw new InvalidDataException( + $"Schema received from serial port didn't contain entry for every {nameof(DataLabel)}" + ); } /// @@ -171,13 +183,20 @@ private EventData> WrapInEventData(string message) // Separate values string[] data = message.Split(':').Select(x => x.Trim().Trim('[', ']', ';')).ToArray(); // Build dictionary - Dictionary dict = _schema.Select(x => (x.Key, data[x.Value])).ToDictionary(); - float latitude = 0f, longitude = 0f, altitude = 0f; - if(dict[DataLabel.Latitude] != "nan") latitude = float.Parse(dict[DataLabel.Latitude]); - if(dict[DataLabel.Longitude] != "nan") longitude = float.Parse(dict[DataLabel.Longitude]); - if(dict[DataLabel.Altitude] != "nan") altitude = float.Parse(dict[DataLabel.Altitude]); - // Wrap data - return new EventData> + Dictionary dict = _schema + .Select(x => (x.Key, data[x.Value])) + .ToDictionary(); + float latitude = 0f, + longitude = 0f, + altitude = 0f; + if (dict[DataLabel.Latitude] != "nan") + latitude = float.Parse(dict[DataLabel.Latitude]); + if (dict[DataLabel.Longitude] != "nan") + longitude = float.Parse(dict[DataLabel.Longitude]); + if (dict[DataLabel.Altitude] != "nan") + altitude = float.Parse(dict[DataLabel.Altitude]); + // Wrap data + return new EventData> { DataStamp = new DataStamp { @@ -187,19 +206,20 @@ private EventData> WrapInEventData(string message) { Latitude = latitude, Longitude = longitude, - Altitude = altitude - } + Altitude = altitude, + }, }, - Data = dict + Data = dict, }; } - private void AppendToFile(string toAppend) { - string filePath = @"D:\escola\20232024\clube\cansat\code\datasave"; - File.AppendAllText(filePath, toAppend); - } + private void AppendToFile(string toAppend) + { + string filePath = @"D:\escola\20232024\clube\cansat\code\datasave"; + File.AppendAllText(filePath, toAppend); + } - public void Dispose() + public void Dispose() { // Close the serial port so it can be used by other apps _serialPort.Close(); diff --git a/Services/EventFinalizers/AltitudeDeltaFinalizer.cs b/Services/EventFinalizers/AltitudeDeltaFinalizer.cs index e992bc3..b55d072 100644 --- a/Services/EventFinalizers/AltitudeDeltaFinalizer.cs +++ b/Services/EventFinalizers/AltitudeDeltaFinalizer.cs @@ -3,16 +3,20 @@ namespace backend24.Services.EventFinalizers { - public class AltitudeDeltaFinalizer : EventFinalizerBase - { - public AltitudeDeltaFinalizer([FromKeyedServices(ServiceKeys.AltitudeDeltaProcessor)]IDataProvider provider) : base(provider) { - } + public class AltitudeDeltaFinalizer : EventFinalizerBase + { + public AltitudeDeltaFinalizer( + [FromKeyedServices(ServiceKeys.AltitudeDeltaProcessor)] IDataProvider provider + ) + : base(provider) { } - protected override EventData<(string, object)> Process(EventData data) { - return new EventData<(string, object)> { - DataStamp = data.DataStamp, - Data = ("primary/altitudedelta", data.Data) - }; - } - } + protected override EventData<(string, object)> Process(EventData data) + { + return new EventData<(string, object)> + { + DataStamp = data.DataStamp, + Data = ("primary/altitudedelta", data.Data), + }; + } + } } diff --git a/Services/EventFinalizers/AltitudeFinalizer.cs b/Services/EventFinalizers/AltitudeFinalizer.cs index 99419e5..af5fa71 100644 --- a/Services/EventFinalizers/AltitudeFinalizer.cs +++ b/Services/EventFinalizers/AltitudeFinalizer.cs @@ -3,16 +3,20 @@ namespace backend24.Services.EventFinalizers { - public class AltitudeFinalizer : EventFinalizerBase - { - public AltitudeFinalizer([FromKeyedServices(ServiceKeys.AltitudeExtractor)]IDataProvider provider) : base(provider) { - } + public class AltitudeFinalizer : EventFinalizerBase + { + public AltitudeFinalizer( + [FromKeyedServices(ServiceKeys.AltitudeExtractor)] IDataProvider provider + ) + : base(provider) { } - protected override EventData<(string, object)> Process(EventData data) { - return new EventData<(string, object)> { - DataStamp = data.DataStamp, - Data = ("primary/altitude", data.Data), - }; - } - } + protected override EventData<(string, object)> Process(EventData data) + { + return new EventData<(string, object)> + { + DataStamp = data.DataStamp, + Data = ("primary/altitude", data.Data), + }; + } + } } diff --git a/Services/EventFinalizers/AltitudeGPSFinalizer.cs b/Services/EventFinalizers/AltitudeGPSFinalizer.cs index 81cb6f2..6ab0020 100644 --- a/Services/EventFinalizers/AltitudeGPSFinalizer.cs +++ b/Services/EventFinalizers/AltitudeGPSFinalizer.cs @@ -3,16 +3,20 @@ namespace backend24.Services.EventFinalizers { - public class AltitudeGPSFinalizer : EventFinalizerBase - { - public AltitudeGPSFinalizer([FromKeyedServices(ServiceKeys.AltitudeGPSExtractor)] IDataProvider provider) : base(provider) { - } + public class AltitudeGPSFinalizer : EventFinalizerBase + { + public AltitudeGPSFinalizer( + [FromKeyedServices(ServiceKeys.AltitudeGPSExtractor)] IDataProvider provider + ) + : base(provider) { } - protected override EventData<(string, object)> Process(EventData data) { - return new EventData<(string, object)> { - DataStamp = data.DataStamp, - Data = ("primary/altitudegps", data.Data), - }; - } - } + protected override EventData<(string, object)> Process(EventData data) + { + return new EventData<(string, object)> + { + DataStamp = data.DataStamp, + Data = ("primary/altitudegps", data.Data), + }; + } + } } diff --git a/Services/EventFinalizers/EventFinalizerAttribute.cs b/Services/EventFinalizers/EventFinalizerAttribute.cs index ec89535..f8a168e 100644 --- a/Services/EventFinalizers/EventFinalizerAttribute.cs +++ b/Services/EventFinalizers/EventFinalizerAttribute.cs @@ -1,17 +1,15 @@ namespace backend24.Services.EventFinalizers { - /// - /// Marks a class as an event finalizer, telling ServerEventsController to subscribe to it. - /// A class shouldn't use this attribute directly, but instead inherit EventFinalizerBase. - /// - /// - /// The compiler allows classes besides EventFinalizerBase to use this attribute, - /// which may result in runtime errors. - /// TODO: find a way to improve this, perhaps with a code analyser. - /// TODO: I think this is useless... - /// - [System.AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] - sealed class EventFinalizerAttribute : Attribute - { - } + /// + /// Marks a class as an event finalizer, telling ServerEventsController to subscribe to it. + /// A class shouldn't use this attribute directly, but instead inherit EventFinalizerBase. + /// + /// + /// The compiler allows classes besides EventFinalizerBase to use this attribute, + /// which may result in runtime errors. + /// TODO: find a way to improve this, perhaps with a code analyser. + /// TODO: I think this is useless... + /// + [System.AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] + sealed class EventFinalizerAttribute : Attribute { } } diff --git a/Services/EventFinalizers/EventFinalizerBase.cs b/Services/EventFinalizers/EventFinalizerBase.cs index 7626ea2..029be02 100644 --- a/Services/EventFinalizers/EventFinalizerBase.cs +++ b/Services/EventFinalizers/EventFinalizerBase.cs @@ -1,18 +1,22 @@ // Type alias for the IDataProvider specialization implemented by a finalizer -global using IFinalizedProvider = backend24.Services.DataProviders.IDataProvider<(string tag, object content)>; +global using IFinalizedProvider = backend24.Services.DataProviders.IDataProvider<( + string tag, + object content +)>; using backend24.Services.DataProcessors; using backend24.Services.DataProviders; namespace backend24.Services.EventFinalizers { - /// - /// Base class for event finalizers. An event finalizer must inherit this class - /// and must add a tag to incoming data (along with any processing deemed necessary). - /// - /// Type of incoming data - [EventFinalizer] - public abstract class EventFinalizerBase : DataProcessorBase - { - protected EventFinalizerBase(IDataProvider provider) : base(provider) { } - } + /// + /// Base class for event finalizers. An event finalizer must inherit this class + /// and must add a tag to incoming data (along with any processing deemed necessary). + /// + /// Type of incoming data + [EventFinalizer] + public abstract class EventFinalizerBase : DataProcessorBase + { + protected EventFinalizerBase(IDataProvider provider) + : base(provider) { } + } } diff --git a/Services/EventFinalizers/PressureFinalizer.cs b/Services/EventFinalizers/PressureFinalizer.cs index 6886964..bdbe3fb 100644 --- a/Services/EventFinalizers/PressureFinalizer.cs +++ b/Services/EventFinalizers/PressureFinalizer.cs @@ -3,16 +3,20 @@ namespace backend24.Services.EventFinalizers { - public class PressureFinalizer : EventFinalizerBase - { - public PressureFinalizer([FromKeyedServices(ServiceKeys.PressureExtractor)]IDataProvider provider) : base(provider) { - } + public class PressureFinalizer : EventFinalizerBase + { + public PressureFinalizer( + [FromKeyedServices(ServiceKeys.PressureExtractor)] IDataProvider provider + ) + : base(provider) { } - protected override EventData<(string, object)> Process(EventData data) { - return new EventData<(string, object)> { - DataStamp = data.DataStamp, - Data = ("primary/pressure", data.Data), - }; - } - } + protected override EventData<(string, object)> Process(EventData data) + { + return new EventData<(string, object)> + { + DataStamp = data.DataStamp, + Data = ("primary/pressure", data.Data), + }; + } + } } diff --git a/Services/EventFinalizers/TemperatureFinalizer.cs b/Services/EventFinalizers/TemperatureFinalizer.cs index 266585a..442172d 100644 --- a/Services/EventFinalizers/TemperatureFinalizer.cs +++ b/Services/EventFinalizers/TemperatureFinalizer.cs @@ -3,18 +3,23 @@ namespace backend24.Services.EventFinalizers { - /// - /// Finalizes a temperature event, tagged with "primary/temperature". - /// - public class TemperatureFinalizer : EventFinalizerBase - { - public TemperatureFinalizer([FromKeyedServices(ServiceKeys.TemperatureExtractor)]IDataProvider provider) : base(provider) { } + /// + /// Finalizes a temperature event, tagged with "primary/temperature". + /// + public class TemperatureFinalizer : EventFinalizerBase + { + public TemperatureFinalizer( + [FromKeyedServices(ServiceKeys.TemperatureExtractor)] IDataProvider provider + ) + : base(provider) { } - protected override EventData<(string, object)> Process(EventData data) { - return new EventData<(string, object)> { - DataStamp = data.DataStamp, - Data = ("primary/temperature", data.Data), - }; - } - } + protected override EventData<(string, object)> Process(EventData data) + { + return new EventData<(string, object)> + { + DataStamp = data.DataStamp, + Data = ("primary/temperature", data.Data), + }; + } + } } diff --git a/Services/EventFinalizers/VelocityFinalizer.cs b/Services/EventFinalizers/VelocityFinalizer.cs index 926a914..ace0050 100644 --- a/Services/EventFinalizers/VelocityFinalizer.cs +++ b/Services/EventFinalizers/VelocityFinalizer.cs @@ -3,16 +3,20 @@ namespace backend24.Services.EventFinalizers { - public class VelocityFinalizer : EventFinalizerBase - { - public VelocityFinalizer([FromKeyedServices(ServiceKeys.VelocityProcessor)]IDataProvider provider) : base(provider) { - } + public class VelocityFinalizer : EventFinalizerBase + { + public VelocityFinalizer( + [FromKeyedServices(ServiceKeys.VelocityProcessor)] IDataProvider provider + ) + : base(provider) { } - protected override EventData<(string, object)> Process(EventData data) { - return new EventData<(string, object)> { - DataStamp = data.DataStamp, - Data = ("primary/velocity", data.Data) - }; - } - } + protected override EventData<(string, object)> Process(EventData data) + { + return new EventData<(string, object)> + { + DataStamp = data.DataStamp, + Data = ("primary/velocity", data.Data), + }; + } + } } diff --git a/Services/ServiceKeys.cs b/Services/ServiceKeys.cs index 3cbcab1..8bf973c 100644 --- a/Services/ServiceKeys.cs +++ b/Services/ServiceKeys.cs @@ -1,13 +1,13 @@ namespace backend24.Services { - public enum ServiceKeys - { - SerialProvider, - PressureExtractor, - TemperatureExtractor, - AltitudeExtractor, - AltitudeGPSExtractor, - AltitudeDeltaProcessor, - VelocityProcessor + public enum ServiceKeys + { + SerialProvider, + PressureExtractor, + TemperatureExtractor, + AltitudeExtractor, + AltitudeGPSExtractor, + AltitudeDeltaProcessor, + VelocityProcessor, } } diff --git a/backend24.csproj b/backend24.csproj index 7d8178c..a556825 100644 --- a/backend24.csproj +++ b/backend24.csproj @@ -7,6 +7,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/githooks/pre-commit b/githooks/pre-commit new file mode 100644 index 0000000..38d1b1e --- /dev/null +++ b/githooks/pre-commit @@ -0,0 +1,134 @@ +#!/usr/bin/python +import datetime +import subprocess +import sys +from contextlib import contextmanager + + +@contextmanager +def stash_unstaged(): + try: + # See https://stackoverflow.com/a/20480591/22222542 + print("Stashing unstaged code") + # Get the name/hash of the most recent stash, if it exists + old_stash = subprocess.run( + ["git", "rev-parse", "-q", "--verify", "refs/stash"], stdout=subprocess.PIPE + ).stdout + # Stash the index and work dir, keeping only the to-be-committed + # changes in the work dir + stash_name = f"pre-commit-{datetime.datetime.now()}" + subprocess.call(["git", "stash", "--quiet", "--keep-index", "--include-untracked"]) + # Get the name/hash of the new most recent stash + new_stash = subprocess.run( + ["git", "rev-parse", "-q", "--verify", "refs/stash"], stdout=subprocess.PIPE + ).stdout + # If there were no changes then nothing was stashed, and we can exit early + # (Presumably, the code which has been committed is already formatted and tested) + if old_stash == new_stash: + print("No changes detected, so pre-commit is passing") + # exit(0) + # Give control to the caller, so it can do its stuff + yield + finally: + print("Popping stash") + # Restore any stashed changes + subprocess.call(["git", "reset", "--hard", "--quiet"]) + subprocess.call(["git", "stash", "pop", "--index", "--quiet"]) + + +def autofix_msg(command: list[str]): + # Suggest a command to fix the errors + print("\033[96mTo autofix errors, try running the following command:") + print(" ".join(command)) + print("\033[0m") + + +def exit_on_error(code: int): + if code != 0: + sys.exit(code) + + +def get_files() -> list[str] | None: + # Get a list of files to be commited + files = subprocess.run( + ["git", "diff-index", "--cached", "--name-only", "HEAD"], stdout=subprocess.PIPE + ).stdout + files = files.decode("utf-8").split("\n") + files = filter((lambda f: f.endswith(".cs")), files) + files = list(files) + print(files) + if len(files) == 0: + return None + return files + + +def lint_style(files: list[str]) -> int: + # Check the files using dotnet format + # (See https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format) + command = [ + "dotnet", + "format", + "style", + "backend24.sln", + "--verify-no-changes", + "--include", + ] + files + exit_code = subprocess.call(command) + + if exit_code != 0: + command.remove("--verify-no-changes") + autofix_msg(command) + + return exit_code + + +def lint_analyzers(files: list[str]) -> int: + # Check the files using dotnet format + # (See https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format) + command = [ + "dotnet", + "format", + "analyzers", + "backend24.sln", + "--verify-no-changes", + "--include", + ] + files + exit_code = subprocess.call(command) + + if exit_code != 0: + command.remove("--verify-no-changes") + autofix_msg(command) + + return exit_code + + +def format(files: list[str]) -> int: + # Check the files using dotnet format + # (See https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format) + command = [ + "dotnet", + "csharpier", + "--check", + ] + files + exit_code = subprocess.call(command) + + if exit_code != 0: + command.remove("--check") + autofix_msg(command) + + return exit_code + + +def main(): + exit_code = 0 + with stash_unstaged(): + files = get_files() + if files is None: + print("No .cs files changed, skipping checks") + sys.exit(0) + for method in [format, lint_style, lint_analyzers]: + exit_on_error(method(files)) + + +if __name__ == "__main__": + main()