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()