Skip to content
Shiny.NET
Shiny MAUI Shell v7 - App Links, App Shortcuts, & Navigation Interception!Shortcut me to it

Beacons Releases

Feature
Shiny.Beacons is back, and now covers every platform. iBeacon ranging, background region monitoring, and broadcasting across iOS, Mac Catalyst, macOS, Android, Windows, Linux and Blazor WebAssembly, with Eddystone (UID, URL and TLM) added as a first-class citizen alongside it. AddBeaconRanging(), AddBeaconMonitoring<TDelegate>(), AddEddystoneScanning() and AddBeaconBroadcasting() register the four capabilities independently, so an app that only wants to read a beacon does not pay for the foreground service or the always-on location prompt that monitoring needs. There is no tvOS target: tvOS binds no CLBeacon type of any kind, so iBeacon is impossible there and an Eddystone-only surface that threw for half its members was not worth shipping.
Feature
Eddystone - UID, URL and TLM. IEddystoneScanner.WhenFrameReceived() emits typed frames: EddystoneUidFrame (10-byte namespace + 6-byte instance), EddystoneUrlFrame (with the full scheme and top-level-domain compression table, so https://www.example.com/ costs 9 bytes on the air), and EddystoneTlmFrame (battery volts, temperature, advertisement count, uptime). The spec’s two sentinel values are honoured rather than passed through: 0 millivolts means the beacon is mains powered and 0x8000 means no temperature sensor is fitted, both surfaced as null instead of as a flat battery and a 128°C beacon. Encrypted TLM is handed back intact for a caller holding the identity key, and EID frames surface their raw rotating identifier - resolving one needs the Curve25519/AES-EAX derivation from the EID spec, which is not implemented. Because Eddystone rides in service data under 0xFEAA rather than manufacturer data, it works identically on every platform including Apple’s, and filtering the scan on that UUID is also what lets it keep delivering while an iOS app is backgrounded.
Feature
Distance estimates are filtered, and the estimator is pluggable. BLE RSSI swings 10 dBm or more between consecutive advertisements from a stationary beacon, which a per-packet distance calculation turns into metres of jitter - and per-packet is exactly what the pre-5.0 module did. Every sample now goes through RssiFilter, a windowed trimmed mean (20 seconds, top and bottom tenth discarded) before it reaches an IBeaconDistanceEstimator. Two ship: PathLossDistanceEstimator (d = 10^((txPower-rssi)/(10n)), the default, with n configurable from free-space 2.0 up towards 3-4 for cluttered indoor spaces) and CurveFitDistanceEstimator (the Radius Networks/AltBeacon empirical fit, for parity with other Android beacon stacks). Window, trim fraction, default calibration power, proximity thresholds and region exit timeout are all on BeaconRangingOptions. On Apple platforms CoreLocation does its own smoothing and hands back a distance directly, so the filter and estimator are deliberately bypassed there and only the thresholds apply.
Fix
Proximity was computed from a formula that was not a distance in any unit. CalculateProximity evaluated Math.Pow(10, (txpower - rssi) / 20) and compared the result against 6E-6 and 0.5E-6, so in practice every beacon at every distance came back Immediate. Proximity is now derived from the estimated distance against Apple’s own boundaries - under 0.5 m immediate, under 3 m near, beyond that far - and both thresholds are configurable.
Fix
A beacon region entered from cold never raised an entry event. The Android monitoring task seeded the first sighting with state.IsInRange ??= true and then tested if (!state.IsInRange.Value) before firing, so the very transition monitoring exists to report was swallowed on every first encounter. Only a beacon that had already been seen, gone out of range, and come back could produce an Entered. The state machine is now a separate testable type (BeaconRegionMonitor) with no Bluetooth dependency, covered by tests driving a fake clock through entry, silence, exit, re-entry, and per-region notify flags.
Fix
Restarting monitoring crashed with a duplicate key. StartScan added every stored region into a dictionary it never cleared, and the repository-change handler added again on top of that, so a second start - or a region re-registered with new bounds - threw An item with the same key has already been added. Re-registering an identifier now updates the region in place, which is a legitimate way to change what a region matches.
Fix
Major = 0 was rejected as invalid. BeaconRegion threw ArgumentException on major < 1, making zero - a perfectly legal iBeacon major and minor value - impossible to monitor or range. Only the genuine rule remains: a minor requires a major.
Fix
Any manufacturer payload starting 0x02 0x15 was parsed as an iBeacon, whichever company identifier it arrived under. The company id is now checked against Apple’s 0x004C; BeaconRangingOptions.AllowNonAppleCompanyId re-enables the loose behaviour for the handful of vendors that ship iBeacon-shaped payloads under their own identifier.
FixAndroid
The monitoring foreground service declared the wrong type. It ran as ForegroundService.TypeLocation, but a BLE scan is a connected-device activity, and from Android 14 the declared type has to match what the service does or the service is refused outright. It is now TypeConnectedDevice and asks for FOREGROUND_SERVICE_CONNECTED_DEVICE - which also means beacon monitoring no longer prompts for a location permission it never used. Add <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" /> to your manifest.
FixAndroid
Ranging scanned in ScanMode.Balanced, which delivers a fraction of the advertisements the radio could report - and averaging is only as good as its sample count. Ranging now uses LowLatency and monitoring LowPower, with scan batching off for both: batched results arrive with stale, coalesced RSSI values that defeat the filter.
FeatureiOS
Region monitoring uses CLMonitor with CLBeaconIdentityCondition on iOS and Mac Catalyst 18+, the same path Shiny.Locations already takes for geofences - including the CLServiceSession background delivery needs and the cold-start replay suppression that stops a re-attached condition from firing a spurious entry. Below 18 it falls back to CLLocationManager region monitoring, where iOS’s 20-region-per-app cap (shared with geofences) is now enforced with a named error instead of iOS silently dropping the excess.
FeaturemacOS
macOS ranges iBeacons and reads Eddystone. It has no beacon region API in CoreLocation at all, so the monitoring manager registers - keeping shared startup code compiling and running - but reports AccessState.NotSupported and throws from StartMonitoring. Branch on CurrentStatus, not on OperatingSystem.IsMacOS().
FeatureLinux
Linux broadcasts both formats. This needed BlueZ advertising implementing in Shiny.BluetoothLE.Hosting.Linux first, where StartAdvertising was an unimplemented stub - see the BluetoothLE Hosting release notes. Linux is now the only non-Apple platform where every beacon capability works: ranging, monitoring, Eddystone and broadcasting.
FeatureBlazor
Shiny.BluetoothLE.Blazor now surfaces manufacturerData and serviceData from Web Bluetooth advertisements, which it previously discarded - IAdvertisementData.ManufacturerData and .ServiceData both returned null. Both beacon formats are therefore readable in the browser wherever navigator.bluetooth.requestLEScan is available (Chromium, behind #enable-experimental-web-platform-features). The chooser fallback carries no advertisement payload, so beacons stay invisible through that path.