Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global Settings #764

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
6 changes: 3 additions & 3 deletions src/Controllers/StateController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@
{
var rxNs = _nsd.Get(rxId);
var rxM = txM.GetOrAdd(rx.Rx?.Name ?? rxId);
if (txNs.TxRefRssi is not null) rxM["tx_ref_rssi"] = txNs.TxRefRssi.Value;
if (rxNs.RxAdjRssi is not null) rxM["rx_adj_rssi"] = rxNs.RxAdjRssi.Value;
if (rxNs.Absorption is not null) rxM["absorption"] = rxNs.Absorption.Value;
if (txNs.Calibration.TxRefRssi is not null) rxM["tx_ref_rssi"] = txNs.Calibration.TxRefRssi.Value;
if (rxNs.Calibration.RxAdjRssi is not null) rxM["rx_adj_rssi"] = rxNs.Calibration.RxAdjRssi.Value;
if (rxNs.Calibration.Absorption is not null) rxM["absorption"] = rxNs.Calibration.Absorption.Value;
rxM["expected"] = rx.Expected;
rxM["actual"] = rx.Distance;
rxM["rssi"] = rx.Rssi;
Expand Down Expand Up @@ -145,9 +145,9 @@
foreach (var node in _state.Nodes.Values)
{
var nodeSettings = _nsd.Get(node.Id);
nodeSettings.TxRefRssi = null;

Check failure on line 148 in src/Controllers/StateController.cs

View workflow job for this annotation

GitHub Actions / build

'NodeSettings' does not contain a definition for 'TxRefRssi' and no accessible extension method 'TxRefRssi' accepting a first argument of type 'NodeSettings' could be found (are you missing a using directive or an assembly reference?)
nodeSettings.RxAdjRssi = null;

Check failure on line 149 in src/Controllers/StateController.cs

View workflow job for this annotation

GitHub Actions / build

'NodeSettings' does not contain a definition for 'RxAdjRssi' and no accessible extension method 'RxAdjRssi' accepting a first argument of type 'NodeSettings' could be found (are you missing a using directive or an assembly reference?)
nodeSettings.Absorption = null;

Check failure on line 150 in src/Controllers/StateController.cs

View workflow job for this annotation

GitHub Actions / build

'NodeSettings' does not contain a definition for 'Absorption' and no accessible extension method 'Absorption' accepting a first argument of type 'NodeSettings' could be found (are you missing a using directive or an assembly reference?)
await _nsd.Set(node.Id, nodeSettings);
}

Expand Down
72 changes: 53 additions & 19 deletions src/Models/NodeSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,62 @@ public class NodeSettings(string id)
[StringLength(64)]
public string? Id { get; set; } = id;

[JsonPropertyName("absorption")]
[JsonProperty("absorption")]
[Range(1, 10)]
public double? Absorption { get; set; }
public UpdatingSettings Updating { get; set; } = new UpdatingSettings();
public ScanningSettings Scanning { get; set; } = new ScanningSettings();
public CountingSettings Counting { get; set; } = new CountingSettings();
public FilteringSettings Filtering { get; set; } = new FilteringSettings();
public CalibrationSettings Calibration { get; set; } = new CalibrationSettings();

[JsonPropertyName("rx_adj_rssi")]
[JsonProperty("rx_adj_rssi")]
[Range(-127, 128)]
public int? RxAdjRssi { get; set; }
public NodeSettings Clone()
{
return new NodeSettings(id)
{
Updating = Updating.Clone(),
Scanning = Scanning.Clone(),
Counting = Counting.Clone(),
Filtering = Filtering.Clone(),
Calibration = Calibration.Clone()
};
}
}

[JsonPropertyName("tx_ref_rssi")]
[JsonProperty("tx_ref_rssi")]
[Range(-127, 128)]
public int? TxRefRssi { get; set; }
public class UpdatingSettings
{
public bool? AutoUpdate { get; set; }
public bool? PreRelease { get; set; }
public UpdatingSettings Clone() => (UpdatingSettings)MemberwiseClone();
}

[JsonPropertyName("max_distance")]
[JsonProperty("max_distance")]
[Range(0, 100)]
public class ScanningSettings
{
public int? ForgetAfterMs { get; set; }
public ScanningSettings Clone() => (ScanningSettings)MemberwiseClone();
}

public class CountingSettings
{
public string? IdPrefixes { get; set; }
public double? StartCountingDistance { get; set; }
public double? StopCountingDistance { get; set; }
public int? IncludeDevicesAge { get; set; }
public CountingSettings Clone() => (CountingSettings)MemberwiseClone();
}

public class FilteringSettings
{
public string? IncludeIds { get; set; }
public string? ExcludeIds { get; set; }
public double? MaxDistance { get; set; }
public double? EarlyReportDistance { get; set; }
public int? SkipReportAge { get; set; }
public FilteringSettings Clone() => (FilteringSettings)MemberwiseClone();
}

public NodeSettings Clone()
{
return (NodeSettings)MemberwiseClone();
}
public class CalibrationSettings
{
public int? RssiAt1m { get; set; }
public int? RxAdjRssi { get; set; }
public double? Absorption { get; set; }
public int? TxRefRssi { get; set; }
public CalibrationSettings Clone() => (CalibrationSettings)MemberwiseClone();
}
6 changes: 3 additions & 3 deletions src/Models/OptimizationResults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ public double Evaluate(List<OptimizationSnapshot> oss, NodeSettingsStore nss)
var rx = nss.Get(m.Rx.Id);

RxNodes.TryGetValue(m.Rx.Id, out var pv);
double rxAdjRssi = pv?.RxAdjRssi ?? rx.RxAdjRssi ?? 0;
double txPower = tx.TxRefRssi ?? -59;
double pathLossExponent = pv?.Absorption ?? rx.Absorption ?? 3;
double rxAdjRssi = pv?.RxAdjRssi ?? rx.Calibration.RxAdjRssi ?? 0;
double txPower = tx.Calibration.TxRefRssi ?? -59;
double pathLossExponent = pv?.Absorption ?? rx.Calibration.Absorption ?? 3;
double distance = m.Rx.Location.DistanceTo(m.Tx.Location);
double predictedRssi = txPower + rxAdjRssi - 10 * pathLossExponent * Math.Log10(distance);

Expand Down
4 changes: 2 additions & 2 deletions src/Optimizers/OptimizationRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
Log.Information("Optimizer set {0,-20} to Absorption: {1:0.00} RxAdj: {2:00} Error: {3}", id, result.Absorption, result.RxAdjRssi, result.Error);
var a = _nsd.Get(id);
if (optimization == null) continue;
if (result.Absorption != null && result.Absorption > optimization.AbsorptionMin && result.Absorption < optimization.AbsorptionMax) a.Absorption = result.Absorption;
if (result.RxAdjRssi != null && result.RxAdjRssi > optimization.RxAdjRssiMin && result.RxAdjRssi < optimization.RxAdjRssiMax) a.RxAdjRssi = result.RxAdjRssi == null ? 0 : (int?)Math.Round(result.RxAdjRssi.Value);
if (result.Absorption != null && result.Absorption > optimization.AbsorptionMin && result.Absorption < optimization.AbsorptionMax) a.Calibration.Absorption = result.Absorption;
if (result.RxAdjRssi != null && result.RxAdjRssi > optimization.RxAdjRssiMin && result.RxAdjRssi < optimization.RxAdjRssiMax) a.Calibration.RxAdjRssi = result.RxAdjRssi == null ? 0 : (int?)Math.Round(result.RxAdjRssi.Value);
await _nsd.Set(id, a);
}

Expand Down
12 changes: 6 additions & 6 deletions src/Services/NodeSettingsStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
public async Task Set(string id, NodeSettings ds)
{
var old = Get(id);
if (ds.Absorption == null || ds.Absorption != old.Absorption)
await mqtt.EnqueueAsync($"espresense/rooms/{id}/absorption/set", $"{ds.Absorption:0.00}");
if (ds.RxAdjRssi == null || ds.RxAdjRssi != old.RxAdjRssi)
await mqtt.EnqueueAsync($"espresense/rooms/{id}/rx_adj_rssi/set", $"{ds.RxAdjRssi}");
if (ds.TxRefRssi == null || ds.TxRefRssi != old.TxRefRssi)
await mqtt.EnqueueAsync($"espresense/rooms/{id}/tx_ref_rssi/set", $"{ds.TxRefRssi}");
if (ds.Absorption == null || ds.Calibration.Absorption != old.Calibration.Absorption)

Check failure on line 18 in src/Services/NodeSettingsStore.cs

View workflow job for this annotation

GitHub Actions / build

'NodeSettings' does not contain a definition for 'Absorption' and no accessible extension method 'Absorption' accepting a first argument of type 'NodeSettings' could be found (are you missing a using directive or an assembly reference?)
await mqtt.EnqueueAsync($"espresense/rooms/{id}/absorption/set", $"{ds.Calibration.Absorption:0.00}");
if (ds.RxAdjRssi == null || ds.Calibration.RxAdjRssi != old.Calibration.RxAdjRssi)

Check failure on line 20 in src/Services/NodeSettingsStore.cs

View workflow job for this annotation

GitHub Actions / build

'NodeSettings' does not contain a definition for 'RxAdjRssi' and no accessible extension method 'RxAdjRssi' accepting a first argument of type 'NodeSettings' could be found (are you missing a using directive or an assembly reference?)
await mqtt.EnqueueAsync($"espresense/rooms/{id}/rx_adj_rssi/set", $"{ds.Calibration.RxAdjRssi}");
if (ds.TxRefRssi == null || ds.Calibration.TxRefRssi != old.Calibration.TxRefRssi)

Check failure on line 22 in src/Services/NodeSettingsStore.cs

View workflow job for this annotation

GitHub Actions / build

'NodeSettings' does not contain a definition for 'TxRefRssi' and no accessible extension method 'TxRefRssi' accepting a first argument of type 'NodeSettings' could be found (are you missing a using directive or an assembly reference?)
await mqtt.EnqueueAsync($"espresense/rooms/{id}/tx_ref_rssi/set", $"{ds.Calibration.TxRefRssi}");
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
Expand All @@ -33,16 +33,16 @@
switch (arg.Setting)
{
case "absorption":
ns.Absorption = double.Parse(arg.Payload);

Check failure on line 36 in src/Services/NodeSettingsStore.cs

View workflow job for this annotation

GitHub Actions / build

'NodeSettings' does not contain a definition for 'Absorption' and no accessible extension method 'Absorption' accepting a first argument of type 'NodeSettings' could be found (are you missing a using directive or an assembly reference?)
break;
case "rx_adj_rssi":
ns.RxAdjRssi = int.Parse(arg.Payload);

Check failure on line 39 in src/Services/NodeSettingsStore.cs

View workflow job for this annotation

GitHub Actions / build

'NodeSettings' does not contain a definition for 'RxAdjRssi' and no accessible extension method 'RxAdjRssi' accepting a first argument of type 'NodeSettings' could be found (are you missing a using directive or an assembly reference?)
break;
case "tx_ref_rssi":
ns.TxRefRssi = int.Parse(arg.Payload);

Check failure on line 42 in src/Services/NodeSettingsStore.cs

View workflow job for this annotation

GitHub Actions / build

'NodeSettings' does not contain a definition for 'TxRefRssi' and no accessible extension method 'TxRefRssi' accepting a first argument of type 'NodeSettings' could be found (are you missing a using directive or an assembly reference?)
break;
case "max_distance":
ns.MaxDistance = double.Parse(arg.Payload);

Check failure on line 45 in src/Services/NodeSettingsStore.cs

View workflow job for this annotation

GitHub Actions / build

'NodeSettings' does not contain a definition for 'MaxDistance' and no accessible extension method 'MaxDistance' accepting a first argument of type 'NodeSettings' could be found (are you missing a using directive or an assembly reference?)
break;
default:
return Task.CompletedTask;
Expand Down
155 changes: 155 additions & 0 deletions src/ui/src/lib/GlobalSettings.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<script lang="ts">
import TriStateCheckbox from '$lib/TriStateCheckbox.svelte';
import { settings } from '$lib/stores';
import { onMount } from 'svelte';

let loading = true;
let error: string | null = null;

onMount(async () => {
try {
await settings.load();
} catch (e: unknown) {
error = e instanceof Error ? e.message : 'An unknown error occurred';
} finally {
loading = false;
}
});

async function handleUpdate() {
try {
if ($settings) {
await settings.save($settings);
}
} catch (e: unknown) {
error = e instanceof Error ? `Error updating settings: ${e.message}` : 'An unknown error occurred while updating settings';
}
}
</script>

{#if loading}
<div class="card m-2 p-4 variant-filled-surface">
<div class="flex items-center space-x-4">
<span class="loading loading-spinner loading-lg" />
<p>Loading settings...</p>
</div>
</div>
{:else if error}
<div class="card m-2 p-4 variant-filled-error">
<p>Error: {error}</p>
</div>
{:else if $settings}
<div class="card m-2 p-4 variant-filled-surface">
<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4">Updating</h2>
<div class="space-y-4 ml-4">
<div class="flex items-center space-x-2">
<TriStateCheckbox id="auto-update" bind:checked={$settings.updating.autoUpdate} />
<label for="auto-update">Automatically update</label>
</div>
<div class="flex items-center space-x-2">
<TriStateCheckbox id="pre-release" bind:checked={$settings.updating.preRelease} />
<label for="pre-release">Include pre-released versions in auto-update</label>
</div>
</div>
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4">Scanning</h2>
<div class="space-y-4 ml-4">
<label class="label">
<span>Forget beacon if not seen for (in milliseconds):</span>
<input type="number" class="input" min="0" bind:value={$settings.scanning.forgetAfterMs} placeholder="150000" />
</label>
</div>
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4">Counting</h2>
<div class="space-y-4 ml-4">
<label class="label">
<span>Include id prefixes (space separated):</span>
<input type="text" class="input" bind:value={$settings.counting.idPrefixes} placeholder="" />
</label>

<label class="label">
<span>Start counting devices less than distance (in meters):</span>
<input type="number" class="input" step="0.01" min="0" bind:value={$settings.counting.startCountingDistance} placeholder="2.00" />
</label>

<label class="label">
<span>Stop counting devices greater than distance (in meters):</span>
<input type="number" class="input" step="0.01" min="0" bind:value={$settings.counting.stopCountingDistance} placeholder="4.00" />
</label>

<label class="label">
<span>Include devices with age less than (in ms):</span>
<input type="number" class="input" min="0" bind:value={$settings.counting.includeDevicesAge} placeholder="30000" />
</label>
</div>
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4">Filtering</h2>
<div class="space-y-4 ml-4">
<label class="label">
<span>Include only sending these ids to mqtt (eg. apple:iphone10-6 apple:iphone13-2):</span>
<input type="text" class="input" bind:value={$settings.filtering.includeIds} placeholder="" />
</label>

<label class="label">
<span>Exclude sending these ids to mqtt (eg. exp:20 apple:iphone10-6):</span>
<input type="text" class="input" bind:value={$settings.filtering.excludeIds} placeholder="" />
</label>

<label class="label">
<span>Max report distance (in meters):</span>
<input type="number" class="input" step="0.01" min="0" bind:value={$settings.filtering.maxReportDistance} placeholder="16.00" />
</label>

<label class="label">
<span>Report early if beacon has moved more than this distance (in meters):</span>
<input type="number" class="input" step="0.01" min="0" bind:value={$settings.filtering.earlyReportDistance} placeholder="0.50" />
</label>

<label class="label">
<span>Skip reporting if message age is less that this (in milliseconds):</span>
<input type="number" class="input" min="0" bind:value={$settings.filtering.skipReportAge} placeholder="5000" />
</label>
</div>
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4">Calibration</h2>
<div class="space-y-4 ml-4">
<label class="label">
<span>Rssi expected from a 0dBm transmitter at 1 meter (NOT used for iBeacons or Eddystone):</span>
<input type="number" class="input" bind:value={$settings.calibration.rssiAt1m} placeholder="-65" />
</label>

<label class="label">
<span>Rssi adjustment for receiver (use only if you know this device has a weak antenna):</span>
<input type="number" class="input" bind:value={$settings.calibration.rssiAdjustment} placeholder="0" />
</label>

<label class="label">
<span>Factor used to account for absorption, reflection, or diffraction:</span>
<input type="number" class="input" step="0.01" min="0" bind:value={$settings.calibration.absorptionFactor} placeholder="3.50" />
</label>

<label class="label">
<span>Rssi expected from this tx power at 1m (used for node iBeacon):</span>
<input type="number" class="input" bind:value={$settings.calibration.iBeaconRssiAt1m} placeholder="-59" />
</label>
</div>
</section>

<div class="flex justify-end mt-8">
<button class="btn variant-filled-primary" on:click={handleUpdate}>Update Settings</button>
</div>
</div>
{:else}
<div class="card p-4 variant-filled-warning">
<p>No settings available.</p>
</div>
{/if}
38 changes: 38 additions & 0 deletions src/ui/src/lib/TriStateCheckbox.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script lang="ts">
export let checked: boolean | null = false;
export let id: string;

function handleClick(event: Event) {
const cb = event.target as HTMLInputElement;
if (cb.readOnly) {
cb.checked = false;
cb.readOnly = false;
checked = false;
} else if (!cb.checked) {
cb.readOnly = true;
cb.indeterminate = true;
checked = null;
} else {
checked = true;
}
}

$: ariaChecked = checked === null ? 'mixed' : checked;
</script>

<input
type="checkbox"
class="checkbox"
{id}
on:click={handleClick}
checked={checked === true}
indeterminate={checked === null}
readOnly={checked === null}
aria-checked={ariaChecked}
/>

<style>
input[type="checkbox"]:indeterminate {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e");
}
</style>
1 change: 1 addition & 0 deletions src/ui/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
{ href: '/devices', name: 'devices', icon: devices, alt: 'Devices' },
{ href: '/nodes', name: 'nodes', icon: nodes, alt: 'Nodes' },
{ href: '/calibration', name: 'calibration', icon: calibration, alt: 'Calibration' },
{ href: '/settings', name: 'settings', icon: settings, alt: 'Settings' }
];
</script>

Expand Down
14 changes: 14 additions & 0 deletions src/ui/src/routes/settings/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts">
import GlobalSettings from '$lib/GlobalSettings.svelte';
</script>

<svelte:head>
<title>ESPresense Companion: Settings</title>
</svelte:head>

<div class="container mx-auto p-2">
<h1 class="text-3xl font-bold my-2 px-2">Settings</h1>
<p class="mb-6 text-lg px-2">These settings will be applied to every node, including new nodes.</p>

<GlobalSettings />
</div>
Loading