Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ csharp_new_line_before_finally = true
# The codebase uses block-scoped namespaces; keep new files consistent.
csharp_style_namespace_declarations = block_scoped:suggestion
dotnet_sort_system_directives_first = true
# Namespace must equal project root + relative folder path (issue #102).
# Deliberate exceptions disable IDE0130 in-file with a one-line reason.
dotnet_style_namespace_match_folder = true
dotnet_diagnostic.IDE0130.severity = error

# ── this. / var ──────────────────────────────────────────────────────
dotnet_style_qualification_for_field = false:suggestion
Expand Down
4 changes: 4 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
IsExternalInit shim). -->
<LangVersion>latest</LangVersion>

<!-- Surface .editorconfig style rules (incl. IDE0130 namespace-folder match)
as build diagnostics, not IDE-only suggestions. -->
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>

<!-- Single source of truth for product versioning. -->
<VersionPrefix>0.7.0</VersionPrefix>
<Version Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)</Version>
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Documentation specific to the FanaBridge plugin:

| Document | Description |
|----------|-------------|
| [Architecture](architecture.md) | Layers, namespace rule, test-tree convention, and frozen names |
| [Supported Devices](supported-devices.md) | Wheels and hub + module combos with built-in FanaBridge profiles — tested vs. unverified, and their LED/display capabilities |
| [Device Settings Lifecycle](device-settings-lifecycle.md) | How a device decides what to store, why the LED editor is built up front, and the rules that keep a save from erasing settings |

Expand Down
66 changes: 66 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Architecture

Rules and frozen names for the FanaBridge solution. Folder inventories and history live elsewhere; this page is the contract.

## Layers

Three source projects, layered by dependency:

| Project | Role |
|---------|------|
| **FanaBridge.Core** | SimHub-free device stack: HID transport, Fanatec protocol, profiles, LEDs, display encoding, tuning, diagnostics. References only non-SimHub libraries (HidSharp, Newtonsoft.Json). |
| **FanaBridge** (plugin) | SimHub / WPF shell: plugin entrypoint, device registry, Control Mapper bridge, settings, UI, display drivers. References Core and Updater. |
| **FanaBridge.Updater** | Isolated self-updater (release feed, download, file swap). References neither Core nor the plugin — audit boundary for code that rewrites files next to SimHub. |

**Reference rules:** Core and Updater must not reference SimHub assemblies or each other. The plugin may reference both. Packaging uses **ILRepack** to merge Core + Updater into the shipped **`FanaBridge.dll`** (development builds keep separate assemblies for layering).

## Namespace rule

```
namespace = project namespace root + relative folder path
```

| Project | Namespace root |
|---------|----------------|
| `src/FanaBridge.Core` | `FanaBridge.Core` |
| `src/FanaBridge` | `FanaBridge` |
| `src/FanaBridge.Updater` | `FanaBridge.Updater` |
| `tests/FanaBridge.Tests` | `FanaBridge.Tests` |

Enforced at compile time via **IDE0130** (`dotnet_style_namespace_match_folder = true`, severity error, `EnforceCodeStyleInBuild`). Deliberate exceptions use an in-file `#pragma warning disable IDE0130` with a one-line reason.

**Product exceptions:**

1. **`Log` in namespace `FanaBridge`** (`FanaBridge.Core/Logging/Log.cs`) — unqualified `Log.*` call sites in both Core and plugin resolve only when the type lives on the shared root ancestor.
2. **`ModuleInitializerAttribute`** (`FanaBridge/Properties/ModuleInitializerAttribute.cs`) — net48 polyfill; must live in `System.Runtime.CompilerServices` by definition.

XAML `x:Class` values follow the same path rule and are checked by a contract test (the C# analyzer does not cover markup).

Domain folders and their matching namespaces are a **navigational taxonomy**, not directional boundaries. Cross-domain references within a project are expected (e.g. Devices ↔ Transport in Core). The only compiler-enforced directional boundaries are the assembly references: Core references nothing internal, Updater references nothing internal, and the plugin references both.

## Test tree

`tests/FanaBridge.Tests` mirrors the product projects:

- `Core/` → `FanaBridge.Tests.Core…`
- `Plugin/` → `FanaBridge.Tests.Plugin…`
- `Updater/` → `FanaBridge.Tests.Updater…`
- `Contracts/` → repo-wide / external-contract tests (XAML layout, SimHub enum snapshot)

Domain-local fakes live next to their domain; multi-domain doubles live under `TestDoubles/`. A third layout exception lives in the test tree: the Control Mapper reflection shim (see `tests/FanaBridge.Tests/README.md` for conventions and details).

## Frozen names

These strings are invisible to the C# namespace analyzer but **breaking to rename** (SimHub persistence, embedded resources, dashboards, or the updater whitelist):

| Name | Why frozen |
|------|------------|
| **`FanaBridge.dll`** | Shipped assembly file name; updater package whitelist and install path. |
| **`FanaBridge.FanatecPlugin`** | Fully-qualified plugin type name persisted by SimHub. |
| **`Profiles` path segment** (under Core's embedded resources) | The built-in wheel-profile loader matches manifest names on a `.Profiles.` substring and `.json` suffix; keep profile JSON under a `Profiles/` folder. |
| **`FanatecPlugin.FanaBridgeSettings.json`** | On-disk settings file under SimHub `PluginsData/Common/`. |
| **`AttachDelegate` / `AddEvent` keys** | Property and event names registered with SimHub (`FanaBridge.*` properties, `DeviceConnected`, `DeviceDisconnected`, `WheelChanged`, …). Dashboards and automations bind to these strings. |
| **`Fanatec_<wheel>[_<module>]`** | `DeviceTypeID` format for SimHub device descriptors (and `Fanatec_Module_<module>` parents for hub logos). |
| **`FS_WHEEL_SWTYPE_<code>`** | Control Mapper variant ids (`FanaBridgeVariantProvider`); persisted in users' Control Mapper settings. |

Not frozen, for the record: SimHub's `ResolveCache.json` stores plugin/registry FQNs, but the cache is hash-invalidated whenever `FanaBridge.dll` changes on disk, so those entries rebuild on every update — cache state, not durable state.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;

namespace FanaBridge.Transport
namespace FanaBridge.Core.Devices
{
/// <summary>
/// Encapsulates the Fanatec device connection state machine:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FanaBridge.Profiles;
using FanaBridge.Protocol;
using FanaBridge.Core.Devices.Identity;
using FanaBridge.Core.Devices.Profiles;
using FanaBridge.Core.Transport;

namespace FanaBridge.Transport
namespace FanaBridge.Core.Devices
{
/// <summary>
/// Represents a connected Fanatec wheelbase — the root of all communication.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FanaBridge.Transport
namespace FanaBridge.Core.Devices
{
/// <summary>
/// Connection-check surface of a <see cref="FanatecWheelbase"/>, used by
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Generic;

namespace FanaBridge.Protocol
namespace FanaBridge.Core.Devices.Identity
{
/// <summary>
/// Reference tables mapping the raw FF 08 system-report bytes to FanaBridge
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FanaBridge.Protocol
namespace FanaBridge.Core.Devices.Identity
{
/// <summary>
/// Decodes wheelbase + attachment + module identity from the col03 <c>FF 08</c>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FanaBridge.Transport
namespace FanaBridge.Core.Devices.Identity
{
/// <summary>
/// Settles raw wheel/hub + module identity readings before they are committed.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using FanaBridge.Transport;
using FanaBridge.Core.Transport;

namespace FanaBridge.Protocol
namespace FanaBridge.Core.Devices.Identity
{
/// <summary>
/// SRM Conversion Kit identity, recovered from the kit's private <c>DE FA AD</c> → <c>0xDD</c>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using FanaBridge.Transport;
using FanaBridge.Core.Transport;

namespace FanaBridge.Protocol
namespace FanaBridge.Core.Devices.Identity
{
/// <summary>
/// The col03 <c>FF 08</c> system-report codec — the wire-level side of identity.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using FanaBridge.Transport;
using FanaBridge.Core.Transport;

namespace FanaBridge.Protocol
namespace FanaBridge.Core.Devices.Identity
{
/// <summary>
/// Replicates the kernel filter's engage sequence (FWFUProtocolUsbBulkOrInterruptTransfer)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// Pixel encoding for the Color LED channel (subcmd 0x02).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;

namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// Identifies a specific selectable device configuration — a standalone
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// The type of display available on a wheel or button module.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using Newtonsoft.Json;

namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// Structured input association for a single LED.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json;

namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// JSON converter for <see cref="LedChannel"/> that accepts both v1 and v2
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;

namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// A way in which a wheel's LEDs cannot show what SimHub's color picker offers.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Newtonsoft.Json;

namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// Describes a single physical LED on the device.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// Hardware communication channel for a single LED.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Newtonsoft.Json;

namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// Matching criteria to associate a profile with the connected hardware.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// Indicates whether a profile was shipped with the plugin or created by the user.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Linq;

namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// Runtime view of a wheel's hardware capabilities, computed from a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.Linq;
using Newtonsoft.Json;

namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// A complete wheel profile — the single source of truth for a device's
Expand Down Expand Up @@ -104,17 +104,17 @@ public DisplayType DisplayType
{
if (Enum.TryParse(Display, true, out DisplayType dt))
return dt;
return FanaBridge.Profiles.DisplayType.None;
return FanaBridge.Core.Devices.Profiles.DisplayType.None;
}
}

/// <summary>
/// ITM display wire id (which device the wheel's ITM screen is: 1 base, 3 wheel OLED,
/// 4 Bentley). Defaults to <see cref="FanaBridge.Protocol.ItmEncoder.DefaultDeviceId"/>
/// 4 Bentley). Defaults to <see cref="FanaBridge.Core.Display.Protocol.ItmEncoder.DefaultDeviceId"/>
/// (3) when omitted — correct for PBME and GTSWX.
/// </summary>
[JsonIgnore]
public byte ItmDeviceId => ItmDeviceIdRaw ?? FanaBridge.Protocol.ItmEncoder.DefaultDeviceId;
public byte ItmDeviceId => ItmDeviceIdRaw ?? FanaBridge.Core.Display.Protocol.ItmEncoder.DefaultDeviceId;

/// <summary>Total LED count across all channels.</summary>
[JsonIgnore]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using System.Threading;
using Newtonsoft.Json;

namespace FanaBridge.Profiles
namespace FanaBridge.Core.Devices.Profiles
{
/// <summary>
/// Loads <see cref="WheelProfile"/> definitions and provides lookup by
Expand Down Expand Up @@ -126,7 +126,7 @@ private static void LoadFromEmbeddedResources(
foreach (string resourceName in assembly.GetManifestResourceNames())
{
// Embedded resource names follow: {RootNamespace}.{RelativePath}.{filename}
// e.g. "FanaBridge.Resources.Profiles.PSWBMW.json"
// e.g. "FanaBridge.Core.Resources.Profiles.PSWBMW.json"
if (!resourceName.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
continue;
if (resourceName.IndexOf(".Profiles.", StringComparison.OrdinalIgnoreCase) < 0)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using FanaBridge.Transport;
using FanaBridge.Core.Devices.Identity;
using FanaBridge.Core.Transport;

namespace FanaBridge.Protocol
namespace FanaBridge.Core.Diagnostics
{
/// <summary>
/// One-shot, read-only identity capture run at diagnostics time so a SINGLE detection report from
Expand Down
2 changes: 1 addition & 1 deletion src/FanaBridge.Core/Diagnostics/FanatecSoftwareMonitor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;

namespace FanaBridge.Diagnostics
namespace FanaBridge.Core.Diagnostics
{
/// <summary>
/// Detects whether the Fanatec app/service is running alongside FanaBridge. Both drive
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using FanaBridge.Core.Display.Protocol;

namespace FanaBridge.Protocol
namespace FanaBridge.Core.Display.Catalog
{
/// <summary>
/// One slot in an ITM device's page set: the on-wire page number, which page content
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using FanaBridge.Transport;
using FanaBridge.Core.Transport;

namespace FanaBridge.Protocol
namespace FanaBridge.Core.Display.Protocol
{
/// <summary>
/// Encodes and sends display control reports for the Fanatec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using FanaBridge.Transport;
using FanaBridge.Core.Transport;

namespace FanaBridge.Protocol
namespace FanaBridge.Core.Display.Protocol
{
/// <summary>
/// A single telemetry value for an ITM ValueUpdate entry (col03 <c>FF 05 01</c>).
Expand Down Expand Up @@ -129,7 +129,7 @@ public static ItmParamDef WithSuffix(byte slotId, string suffix, ushort position
/// reference: Enable (<c>FF 02 02</c>), ParamDefs (<c>FF 05 03</c>), ValueUpdate
/// (<c>FF 05 01</c>), and the PageSet frame (<c>FF 05 04</c>).
///
/// This is a pure framing layer — like <see cref="LedEncoder"/> and
/// This is a pure framing layer — like <see cref="FanaBridge.Core.Leds.LedEncoder"/> and
/// <see cref="DisplayEncoder"/>, it builds and writes reports but holds no display
/// state. Page selection, telemetry-to-parameter mapping, and the firmware-safety rate
/// limits (e.g. value-update pacing) are the caller's
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using FanaBridge.Core.Display.Catalog;
using FanaBridge.Core.Transport;

namespace FanaBridge.Protocol
namespace FanaBridge.Core.Display.Protocol
{
/// <summary>
/// A page's <b>content identity</b> — the fixed parameter layout the firmware renders (SPEED
Expand Down Expand Up @@ -100,7 +102,7 @@ public ItmSubscription(byte firmwareHandle, ushort paramId, byte dataType = 0)
/// Wire-side ITM protocol vocabulary: the per-page parameter <b>catalog</b> (which
/// parameter IDs a page carries, in order) and firmware subscription-report parsing.
/// This is pure wire — no SimHub <c>GameData</c>. The SimHub telemetry → value/suffix
/// mapping lives in <c>ItmTelemetryMapper</c> (Adapters), which knows both sides.
/// mapping lives in <c>ItmTelemetryMapper</c> (Display.Drivers), which knows both sides.
///
/// This declares each page's parameter list (<see cref="ParamsFor"/>) and display name
/// (<see cref="NameOf"/>), keyed by the <see cref="ItmPage"/> content identity. Which pages a
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FanaBridge.Protocol
namespace FanaBridge.Core.Display.Protocol
{
/// <summary>
/// 7-segment display encoding table.
Expand Down
Loading
Loading