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
<PackageReference Include="Shiny.BluetoothLE.Hubs.Client" Version="1.3.0" />
<PackageVersion Include="Shiny.BluetoothLE.Hubs.Client" Version="1.3.0" />
<PackageReference Include="Shiny.BluetoothLE.Hubs.Client" />
paket add Shiny.BluetoothLE.Hubs.Client --version 1.3.0
#r "nuget: Shiny.BluetoothLE.Hubs.Client, 1.3.0"
#:package Shiny.BluetoothLE.Hubs.Client@1.3.0
#addin nuget:?package=Shiny.BluetoothLE.Hubs.Client&version=1.3.0
#tool nuget:?package=Shiny.BluetoothLE.Hubs.Client&version=1.3.0
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:
AddBleHubServeris called once, with at least one hub. It refuses a hub added twice, two hubs on one characteristic, and malformed UUIDs..Host(...)setsBleHubHostOptions(name, file transfers, sweep) and.Protocol(...)the protocol limits.AddBleHubClient<T>(characteristicUuid, o => ...)is called per contract.BleHubClientOptions.ServiceUuidmust match the host's, ando.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,
AddBleHubServercallsAddBluetoothLeHosting()andAddBleHubClientcallsAddBluetoothLE(). On Apple the client turns off iOS's background alerts (NotifyOnConnection,NotifyOnDisconnection,NotifyOnNotification), because hubs are used in the foreground. To use your ownAppleBleConfiguration, callAddBluetoothLE(config)beforeAddBleHubClient: the first registration wins. On plainnet10.0(Linux, tests), register anIBleHostingManager/IBleManageryourself.
Contract: hub methods return
Task,Task<T>orIAsyncEnumerable<T>. A trailingCancellationTokenis passed through to the host. Events areAction/Action<T1..T4>. Compile errors are reported asSBH001–SBH006.Hubs: a new hub instance runs, in its own DI scope, for every call, like SignalR. The hub may take the contract's
CancellationTokenor leave it out.Outside a hub: inject
IHubContext<GameHub>and callcontext.Clients.All.StateChanged(state).Clientsis 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()andIHubContext.Disconnect()ask the client to leave. The client library does so and raisesDisconnectedwith the reason.Disconnect reasons are typed. Both sides get a
HubDisconnect(Reason, Message)whoseHubDisconnectReasonyou can switch on:ClientDisconnect: the client calledDisconnect()or was disposed. Over BLE the client sends aDisconnectframe 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)orIHubContext.Disconnect(id, reason).ServerShutdown:IBleHubHost.Stop(reason)orIHubContext.Stop(reason).ConnectionFailed: a connect or handshake failed. NoDisconnectedevent is raised, because the client never connected.
On the host, override
OnDisconnectedAsync(HubDisconnect)or readClientDisconnected'se.Disconnect. On the client,Disconnectedis anEventHandler<HubDisconnect>, andStatusChangedandBleHubDisconnectedExceptioncarry it asDisconnect.Description(and theReasonstring properties) give the message, or a default text for the reason. ExistingOnDisconnectedAsync(string? reason)overrides keep working.Renaming without reconnecting:
- Client:
await client.Rename("Allan B")changes the name the host knows it by. The host runsValidateClientwith the new name, then the hub'sOnRenamedAsync(previousName). Either can refuse, which throwsBleHubRemoteExceptionwithRemoteErrorType == BleHubRemoteException.RenameRefusedand leaves the name unchanged.ClientNameis the current name. - Host:
BleHubConnectedClient.Namefollows renames, andIHubContext<THub>.ClientRenamedis raised. Telling other clients is up to the hub, for exampleClients.Others.PlayerRenamed(previousName, Context.Client.Name)inOnRenamedAsync. IBleHubHost.Rename("TTT 2")changes the host's name without stopping anything. It re-advertises under the new name, and every connected client'sHostNameupdates andHostRenamedis raised, in order with hub events. While stopped, it sets the name for the nextStart.- 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");- Client:
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 toBleHubProtocolOptions.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
spectatorsgroup. - 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 | Versions 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. |
-
net10.0
- Shiny.BluetoothLE (>= 6.0.0)
- Shiny.BluetoothLE.Hubs (>= 1.3.0)
-
net10.0-android36.0
- Shiny.BluetoothLE (>= 6.0.0)
- Shiny.BluetoothLE.Hubs (>= 1.3.0)
-
net10.0-ios26.0
- Shiny.BluetoothLE (>= 6.0.0)
- Shiny.BluetoothLE.Hubs (>= 1.3.0)
-
net10.0-maccatalyst26.0
- Shiny.BluetoothLE (>= 6.0.0)
- Shiny.BluetoothLE.Hubs (>= 1.3.0)
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 |