Shiny.BluetoothLE.Hubs.Client 1.3.0

Prefix Reserved
dotnet add package Shiny.BluetoothLE.Hubs.Client --version 1.3.0
                    
NuGet\Install-Package Shiny.BluetoothLE.Hubs.Client -Version 1.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Shiny.BluetoothLE.Hubs.Client" Version="1.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Shiny.BluetoothLE.Hubs.Client" Version="1.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Shiny.BluetoothLE.Hubs.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Shiny.BluetoothLE.Hubs.Client --version 1.3.0
                    
#r "nuget: Shiny.BluetoothLE.Hubs.Client, 1.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Shiny.BluetoothLE.Hubs.Client@1.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Shiny.BluetoothLE.Hubs.Client&version=1.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Shiny.BluetoothLE.Hubs.Client&version=1.3.0
                    
Install as a Cake Tool

Shiny.BluetoothLE.Hubs

Shiny.BluetoothLE.Hubs gives you SignalR-style hubs over Bluetooth LE. One device hosts a hub, and nearby devices discover it, connect, and call it through a source-generated, strongly typed proxy. The host pushes events back to everyone, to some clients, or to groups. Files move over L2CAP, a faster direct channel between the devices.

It's built on Shiny.BluetoothLE and Shiny.BluetoothLE.Hosting, and it is AOT- and trim-safe with no reflection.

// shared contract
[BleHubClient]
public interface IGameHub
{
    Task<MoveResult> MakeMove(int cell);                          // client -> host
    IAsyncEnumerable<int> Countdown(int from, CancellationToken ct);
    event Action<GameState> StateChanged;                         // host -> clients
}

// host
public class GameHub(GameEngine engine) : BleHub<IGameHub>
{
    public async Task<MoveResult> MakeMove(int cell)
    {
        // Context.Client, Context.Abort(), Groups, OnConnectedAsync/OnDisconnectedAsync
        await Clients.All.StateChanged(engine.Snapshot());        // generated typed push
        return new MoveResult(true, null);
    }
    ...
}

// client - GameHubClient is generated
client.Hub.StateChanged += state => ...;
await client.Connect(host, new BleHubConnectOptions("Allan"));
var result = await client.Hub.MakeMove(4);
await client.UploadFile(path, "avatar.jpg");                      // L2CAP

Packages

Project Purpose
Shiny.BluetoothLE.Hubs Wire protocol (framing, chunking, reassembly), argument codec, serializer, [BleHubClient]. Ships the source generator
Shiny.BluetoothLE.Hubs.Host BleHub<T>, IBleHubHost, IHubContext<THub>, groups, L2CAP file server
Shiny.BluetoothLE.Hubs.Client BleHubClient (base of the generated proxies), discovery (which leaves out the app's own host), shared connections, file upload and download

Setup

Json.AddContext(MyJsonContext.Default);     // hub arguments/results are AOT-safe JSON by default

// host - one call registers every hub (and the platform BLE hosting stack)
services.AddBleHubServer(server => server
    .ServiceUuid(MyServiceUuid)                                                  // optional - a library default applies
    .Host(o => o.EnableFileTransfers(Path.Combine(FileSystem.AppDataDirectory, "files")))
    .AddHub<GameHub>(GameCharacteristicUuid, o => o.MaxClients = 6)
    .AddHub<ChatHub>(ChatCharacteristicUuid));
await serviceProvider.GetRequiredService<IBleHubHost>().Start();

// client - the same service UUID as the host (and the platform BLE stack)
services.AddBleHubClient<IGameHub>(GameCharacteristicUuid, o => o.ServiceUuid = MyServiceUuid);   // inject IBleHubClient<IGameHub>, GameHubClient or IGameHub
  • Registration:

    • AddBleHubServer is called once, with at least one hub. It refuses a hub added twice, two hubs on one characteristic, and malformed UUIDs. .Host(...) sets BleHubHostOptions (name, file transfers, sweep) and .Protocol(...) the protocol limits.
    • AddBleHubClient<T>(characteristicUuid, o => ...) is called per contract. BleHubClientOptions.ServiceUuid must match the host's, and o.Protocol(...) sets the limits.
    • Protocol limits (BleHubProtocolOptions: payload size, timeouts) are app-wide, shared by every host and client registration.
    • Platform BLE stacks are registered for you. On Android, iOS and Mac Catalyst, AddBleHubServer calls AddBluetoothLeHosting() and AddBleHubClient calls AddBluetoothLE(). On Apple the client turns off iOS's background alerts (NotifyOnConnection, NotifyOnDisconnection, NotifyOnNotification), because hubs are used in the foreground. To use your own AppleBleConfiguration, call AddBluetoothLE(config) before AddBleHubClient: the first registration wins. On plain net10.0 (Linux, tests), register an IBleHostingManager / IBleManager yourself.
  • Contract: hub methods return Task, Task<T> or IAsyncEnumerable<T>. A trailing CancellationToken is passed through to the host. Events are Action / Action<T1..T4>. Compile errors are reported as SBH001–SBH006.

  • Hubs: a new hub instance runs, in its own DI scope, for every call, like SignalR. The hub may take the contract's CancellationToken or leave it out.

  • Outside a hub: inject IHubContext<GameHub> and call context.Clients.All.StateChanged(state). Clients is a generated C# 14 extension property.

  • Start/stop:

    • IBleHubHost.Start() / Stop() start or stop every hub.
    • IHubContext<THub>.Start() / Stop(reason) control one hub. A stopped hub disconnects its clients and refuses new ones.
    • A GATT service shared by several hubs stays up while any of them is running.
    • The advertisement follows the running hubs.
  • Disconnect is cooperative, because iOS peripherals can't drop a central. Context.Abort() and IHubContext.Disconnect() ask the client to leave. The client library does so and raises Disconnected with the reason.

  • Disconnect reasons are typed. Both sides get a HubDisconnect(Reason, Message) whose HubDisconnectReason you can switch on:

    • ClientDisconnect: the client called Disconnect() or was disposed. Over BLE the client sends a Disconnect frame before it unsubscribes, so the host can tell it left on purpose.
    • ClientTimeout: the link was lost (an unsubscribe with no goodbye, the host's sweep, or a drop seen by the client).
    • ServerDisconnect: Context.Abort(reason) or IHubContext.Disconnect(id, reason).
    • ServerShutdown: IBleHubHost.Stop(reason) or IHubContext.Stop(reason).
    • ConnectionFailed: a connect or handshake failed. No Disconnected event is raised, because the client never connected.

    On the host, override OnDisconnectedAsync(HubDisconnect) or read ClientDisconnected's e.Disconnect. On the client, Disconnected is an EventHandler<HubDisconnect>, and StatusChanged and BleHubDisconnectedException carry it as Disconnect. Description (and the Reason string properties) give the message, or a default text for the reason. Existing OnDisconnectedAsync(string? reason) overrides keep working.

  • Renaming without reconnecting:

    • Client: await client.Rename("Allan B") changes the name the host knows it by. The host runs ValidateClient with the new name, then the hub's OnRenamedAsync(previousName). Either can refuse, which throws BleHubRemoteException with RemoteErrorType == BleHubRemoteException.RenameRefused and leaves the name unchanged. ClientName is the current name.
    • Host: BleHubConnectedClient.Name follows renames, and IHubContext<THub>.ClientRenamed is raised. Telling other clients is up to the hub, for example Clients.Others.PlayerRenamed(previousName, Context.Client.Name) in OnRenamedAsync.
    • IBleHubHost.Rename("TTT 2") changes the host's name without stopping anything. It re-advertises under the new name, and every connected client's HostName updates and HostRenamed is raised, in order with hub events. While stopped, it sets the name for the next Start.
    • Older hosts refuse a client's rename with a BleHubRemoteException. Older clients ignore a host rename and keep the handshake's name.
    // hub
    public override Task OnRenamedAsync(string? previousName)
        => this.Clients.Others.PlayerRenamed(previousName, this.Context.Client.Name);   // throw to refuse
    
    // client
    client.HostRenamed += (_, name) => ...;                                            // client.HostName is already updated
    await client.Rename("Allan B");
    
  • One service, a characteristic per hub: every hub on a host is a characteristic inside one GATT service, set with server.ServiceUuid(...) (BleHubHostOptions.ServiceUuid). It is the only UUID the host advertises (two 128-bit UUIDs overflow the 31 byte advertisement) and the one clients scan for (BleHubClientOptions.ServiceUuid), so set it the same on both sides. Both default to BleHubProtocolOptions.DefaultServiceUuid, which is shared by every app using this library, so set your own to keep other apps' hosts out of your scans. A scan can't tell which hubs a host is running: joining a stopped hub is refused by the handshake. Hub clients on the same device share one BLE connection.

  • Over Wi-Fi too: Shiny.UniversalHubs serves the same hubs over Wi-Fi (with mDNS discovery) alongside BLE, and lets clients connect over whichever transport is available, with no change to hub or contract code. It builds on hidden transport seams in this library (IHubContext<THub>.TransportEndpoint, BleHubClient.ConnectExternal).

See PLAN.md for the wire protocol, the design decisions and the roadmap.

Sample: Tic Tac Toe

samples/TicTacToe is a .NET MAUI app for iOS and Android that uses Shiny.Maui.Shell.

  • One phone taps Host a game and plays X.
  • The next phone to tap Join a game plays O. Later phones join the spectators group.
  • Moves are hub calls, and board updates and emotes are hub pushes.
  • Players and spectators can chat. The host stamps each line with the sender and pushes it to everyone.
  • Avatars are uploaded and downloaded over L2CAP.

BLE needs two physical devices: simulators and emulators have no usable Bluetooth.

dotnet build samples/TicTacToe -f net10.0-android -t:Run
dotnet build samples/TicTacToe -f net10.0-ios -t:Run

Tests

dotnet test tests/Shiny.BluetoothLE.Hubs.Tests

The tests run the generated hub dispatcher and the generated client proxy against each other through an in-memory "radio", plus diagnostic tests for the generator. No hardware is needed.

Documentation and release notes: https://shinylib.net/blehubs/

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-android36.0 is compatible.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-ios26.0 is compatible.  net10.0-maccatalyst was computed.  net10.0-maccatalyst26.0 is compatible.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Shiny.BluetoothLE.Hubs.Client:

Package Downloads
Shiny.UniversalHubs.Client

Shiny.UniversalHubs client - finds hub hosts over Bluetooth LE and mDNS and connects to them over whichever is available, behind the same generated proxy

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.3.0 61 10/12/2026
1.2.0 71 10/10/2026
1.1.0 60 10/9/2026
1.1.0-beta-0002 65 10/9/2026
1.1.0-beta-0001 53 10/9/2026
1.0.0 61 10/9/2026
1.0.0-alpha-0007-g54008fa8c3 62 10/9/2026
1.0.0-alpha-0007 54 10/9/2026
1.0.0-alpha-0006-g9e9412ee84 62 10/8/2026
1.0.0-alpha-0006 56 10/8/2026
1.0.0-alpha-0005-gd56d166e6b 57 10/7/2026
1.0.0-alpha-0005 57 10/7/2026
1.0.0-alpha-0004-gbc068ff52e 59 10/3/2026
1.0.0-alpha-0004 55 10/3/2026