-
-
Notifications
You must be signed in to change notification settings - Fork 14
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
Settings ui and service #717
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
using Microsoft.AspNetCore.Mvc; | ||
using System.IO; | ||
using System.Text.Json; | ||
|
||
[ApiController] | ||
[Route("api/settings")] | ||
public class SettingsController : ControllerBase | ||
{ | ||
private const string SettingsFilePath = "settings.json"; | ||
|
||
[HttpGet] | ||
public ActionResult<Settings> GetSettings() | ||
{ | ||
if (!System.IO.File.Exists(SettingsFilePath)) | ||
{ | ||
return new Settings(); | ||
} | ||
|
||
var json = System.IO.File.ReadAllText(SettingsFilePath); | ||
return JsonSerializer.Deserialize<Settings>(json); | ||
} | ||
DTTerastar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
[HttpPost] | ||
public IActionResult SaveSettings([FromBody] Settings settings) | ||
{ | ||
var json = JsonSerializer.Serialize(settings); | ||
System.IO.File.WriteAllText(SettingsFilePath, json); | ||
return Ok(); | ||
} | ||
DTTerastar marked this conversation as resolved.
Show resolved
Hide resolved
DTTerastar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
public class Settings | ||
{ | ||
public Updating Updating { get; set; } | ||
public Scanning Scanning { get; set; } | ||
public Counting Counting { get; set; } | ||
public Filtering Filtering { get; set; } | ||
public Calibration Calibration { get; set; } | ||
} | ||
Comment on lines
+32
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initialize properties to prevent null reference exceptions. The Consider initializing the properties in the constructor: public class Settings
{
public Settings()
{
Updating = new Updating();
Scanning = new Scanning();
Counting = new Counting();
Filtering = new Filtering();
Calibration = new Calibration();
}
public Updating Updating { get; set; }
public Scanning Scanning { get; set; }
public Counting Counting { get; set; }
public Filtering Filtering { get; set; }
public Calibration Calibration { get; set; }
} |
||
|
||
public class Updating | ||
{ | ||
public bool AutoUpdate { get; set; } | ||
public bool PreRelease { get; set; } | ||
} | ||
|
||
public class Scanning | ||
{ | ||
public int? ForgetAfterMs { get; set; } | ||
} | ||
|
||
public class Counting | ||
{ | ||
public string IdPrefixes { get; set; } | ||
public double? StartCountingDistance { get; set; } | ||
public double? StopCountingDistance { get; set; } | ||
public int? IncludeDevicesAge { get; set; } | ||
} | ||
|
||
public class Filtering | ||
{ | ||
public string IncludeIds { get; set; } | ||
public string ExcludeIds { get; set; } | ||
public double? MaxReportDistance { get; set; } | ||
public double? EarlyReportDistance { get; set; } | ||
public int? SkipReportAge { get; set; } | ||
} | ||
|
||
public class Calibration | ||
{ | ||
public int? RssiAt1m { get; set; } | ||
public int? RssiAdjustment { get; set; } | ||
public double? AbsorptionFactor { get; set; } | ||
public int? IBeaconRssiAt1m { get; set; } | ||
} | ||
DTTerastar marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -19,22 +19,26 @@ | |||||||||||||||||||||||||||
_state = state; | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
[HttpGet("{id}")] | ||||||||||||||||||||||||||||
public DeviceSettingsDetails Get(string id) | ||||||||||||||||||||||||||||
[HttpGet("{id}/details")] | ||||||||||||||||||||||||||||
public async Task<IList<KeyValuePair<string, string>>> Details(string id) | ||||||||||||||||||||||||||||
Check warning on line 23 in src/Controllers/DeviceController.cs
|
||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
var deviceSettings = _deviceSettingsStore.Get(id); | ||||||||||||||||||||||||||||
var details = new List<KeyValuePair<string, string>>(); | ||||||||||||||||||||||||||||
if (deviceSettings?.Id != null && _state.Devices.TryGetValue(deviceSettings.Id, out var device)) | ||||||||||||||||||||||||||||
details.AddRange(device.GetDetails()); | ||||||||||||||||||||||||||||
return new DeviceSettingsDetails(deviceSettings ?? new DeviceSettings { Id = id, OriginalId = id }, details); | ||||||||||||||||||||||||||||
if (_state.Devices.TryGetValue(id, out var device)) | ||||||||||||||||||||||||||||
return device.GetDetails().ToList(); | ||||||||||||||||||||||||||||
return new List<KeyValuePair<string, string>>(); | ||||||||||||||||||||||||||||
DTTerastar marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
[HttpPut("{id}")] | ||||||||||||||||||||||||||||
[HttpGet("{id}/settings")] | ||||||||||||||||||||||||||||
public Task<DeviceSettings?> Get(string id) | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
var settings = _deviceSettingsStore.Get(id); | ||||||||||||||||||||||||||||
settings ??= new DeviceSettings { OriginalId = id, Id = id }; | ||||||||||||||||||||||||||||
return Task.FromResult(settings); | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
Comment on lines
+30
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Address nullability mismatch and consider optimization The
- public Task<DeviceSettings?> Get(string id)
+ public Task<DeviceSettings> Get(string id)
- var settings = _deviceSettingsStore.Get(id);
- settings ??= new DeviceSettings { OriginalId = id, Id = id };
+ var settings = _deviceSettingsStore.Get(id) ?? new DeviceSettings { OriginalId = id, Id = id }; These changes will resolve the nullability warning and slightly improve the code's conciseness. 📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: build
|
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
[HttpPut("{id}/settings")] | ||||||||||||||||||||||||||||
public async Task Set(string id, [FromBody] DeviceSettings value) | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
await _deviceSettingsStore.Set(id, value); | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
public readonly record struct DeviceSettingsDetails(DeviceSettings? settings, IList<KeyValuePair<string, string>> details); | ||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -21,28 +21,28 @@ | |||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
private readonly ConcurrentDictionary<string, NodeSettings> _storeById = new(); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
public NodeSettings Get(string id) | ||||||||||||||||||||||||||
public Models.NodeSettings Get(string id) | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
return _storeById.TryGetValue(id, out var ns) ? ns.Clone() : new NodeSettings(id); | ||||||||||||||||||||||||||
return _storeById.TryGetValue(id, out var ns) ? ns.Clone() : new Models.NodeSettings(id); | ||||||||||||||||||||||||||
Comment on lines
+24
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure In the Apply this diff to initialize -return _storeById.TryGetValue(id, out var ns) ? ns.Clone() : new Models.NodeSettings(id);
+return _storeById.TryGetValue(id, out var ns) ? ns.Clone() : new Models.NodeSettings(id)
+{
+ Calibration = new CalibrationSettings(),
+ Filtering = new FilteringSettings()
+}; 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
public async Task Set(string id, NodeSettings ds) | ||||||||||||||||||||||||||
public async Task Set(string id, Models.NodeSettings ns) | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
var retain = id == "*"; | ||||||||||||||||||||||||||
var old = Get(id); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Updating settings | ||||||||||||||||||||||||||
if (ds.Updating.AutoUpdate == null || ds.Updating.AutoUpdate != old.Updating.AutoUpdate) | ||||||||||||||||||||||||||
Check failure on line 35 in src/Services/NodeSettingsStore.cs
|
||||||||||||||||||||||||||
await mqtt.EnqueueAsync($"espresense/rooms/{id}/auto_update/set", ds.Updating.AutoUpdate == true ? "ON" : "OFF", retain); | ||||||||||||||||||||||||||
if (ds.Updating.Prerelease == null || ds.Updating.Prerelease != old.Updating.Prerelease) | ||||||||||||||||||||||||||
Check failure on line 37 in src/Services/NodeSettingsStore.cs
|
||||||||||||||||||||||||||
await mqtt.EnqueueAsync($"espresense/rooms/{id}/prerelease/set", ds.Updating.Prerelease == true ? "ON" : "OFF", retain); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Scanning settings | ||||||||||||||||||||||||||
if (ds.Scanning.ForgetAfterMs == null || ds.Scanning.ForgetAfterMs != old.Scanning.ForgetAfterMs) | ||||||||||||||||||||||||||
Check failure on line 41 in src/Services/NodeSettingsStore.cs
|
||||||||||||||||||||||||||
await mqtt.EnqueueAsync($"espresense/rooms/{id}/forget_after_ms/set", $"{ds.Scanning.ForgetAfterMs}", retain); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// Counting settings | ||||||||||||||||||||||||||
if (ds.Counting.IdPrefixes == null || ds.Counting.IdPrefixes != old.Counting.IdPrefixes) | ||||||||||||||||||||||||||
await mqtt.EnqueueAsync($"espresense/rooms/{id}/count_ids/set", $"{ds.Counting.IdPrefixes}", retain); | ||||||||||||||||||||||||||
if (ds.Counting.MinDistance == null || ds.Counting.MinDistance != old.Counting.MinDistance) | ||||||||||||||||||||||||||
await mqtt.EnqueueAsync($"espresense/rooms/{id}/count_min_dist/set", $"{ds.Counting.MinDistance:0.00}", retain); | ||||||||||||||||||||||||||
|
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'; | ||
} | ||
} | ||
DTTerastar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</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} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider making the settings file path configurable.
The
SettingsFilePath
is currently hardcoded as a constant. To improve flexibility across different environments, consider making this path configurable through application settings or environment variables.You could modify the controller to accept the file path through dependency injection: