diff --git a/Controllers/SettingsController.cs b/Controllers/SettingsController.cs new file mode 100644 index 00000000..975c8e53 --- /dev/null +++ b/Controllers/SettingsController.cs @@ -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 GetSettings() + { + if (!System.IO.File.Exists(SettingsFilePath)) + { + return new Settings(); + } + + var json = System.IO.File.ReadAllText(SettingsFilePath); + return JsonSerializer.Deserialize(json); + } + + [HttpPost] + public IActionResult SaveSettings([FromBody] Settings settings) + { + var json = JsonSerializer.Serialize(settings); + System.IO.File.WriteAllText(SettingsFilePath, json); + return Ok(); + } +} + +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; } +} + +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; } +} \ No newline at end of file diff --git a/src/Controllers/SettingsController.cs b/src/Controllers/SettingsController.cs new file mode 100644 index 00000000..27934ff2 --- /dev/null +++ b/src/Controllers/SettingsController.cs @@ -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 GetSettings() + { + if (!System.IO.File.Exists(SettingsFilePath)) + { + return new Settings(); + } + + var json = System.IO.File.ReadAllText(SettingsFilePath); + return JsonSerializer.Deserialize(json); + } + + [HttpPost] + public IActionResult SaveSettings([FromBody] Settings settings) + { + var json = JsonSerializer.Serialize(settings); + System.IO.File.WriteAllText(SettingsFilePath, json); + return Ok(); + } +} + +public class Settings +{ + 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(); +} + +public class UpdatingSettings +{ + public bool? AutoUpdate { get; set; } + public bool? PreRelease { get; set; } +} + +public class ScanningSettings +{ + public int? ForgetAfterMs { get; set; } +} + +public class CountingSettings +{ + public string IdPrefixes { get; set; } + public double? StartCountingDistance { get; set; } + public double? StopCountingDistance { get; set; } + public int? IncludeDevicesAge { get; set; } +} + +public class FilteringSettings +{ + 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 CalibrationSettings +{ + public int? RssiAt1m { get; set; } + public int? RssiAdjustment { get; set; } + public double? AbsorptionFactor { get; set; } + public int? IBeaconRssiAt1m { get; set; } +} \ No newline at end of file diff --git a/src/Pages/Error.cshtml b/src/Pages/Error.cshtml index e47f90d0..aa11cc2c 100644 --- a/src/Pages/Error.cshtml +++ b/src/Pages/Error.cshtml @@ -4,7 +4,7 @@ ViewData["Title"] = "Error"; } -

Error.

+

Error.

An error occurred while processing your request.

@if (Model.ShowRequestId) diff --git a/src/ui/src/lib/CalibrationMatrix.svelte b/src/ui/src/lib/CalibrationMatrix.svelte new file mode 100644 index 00000000..43ecf3c7 --- /dev/null +++ b/src/ui/src/lib/CalibrationMatrix.svelte @@ -0,0 +1,109 @@ + + +{#if $calibration?.matrix} +{#each Object.entries($calibration?.matrix) as [id1, n1] (id1)} + {#each rxColumns as id2 (id2)} +
+ {#if n1[id2]} + Expected {@html Number(n1[id2].expected?.toPrecision(3))} - Actual {@html Number(n1[id2]?.actual?.toPrecision(3))} = Error {@html Number(n1[id2]?.err?.toPrecision(3))} + {:else} + No beacon Received in last 30 seconds + {/if} +
+
+ {/each} +{/each} +{/if} + +
+{#if $calibration?.matrix} +
+
+ + Error % + Error (m) + Absorption + Rx Rssi Adj + Tx Rssi Ref + Variance (m) + +
+
+
+ + + + + {#each rxColumns as id} + + {/each} + + + + {#each Object.entries($calibration.matrix) as [id1, n1] (id1)} + + + {#each rxColumns as id2 (id2)} + {#if n1[id2]} + + {:else} + + {/each} + +
NameRx: {@html id}
Tx: {@html id1}{@html value(n1[id2], data_point)} + {/if} + {/each} +
+
+{:else} +

Loading...

+{/if} +
\ No newline at end of file diff --git a/src/ui/src/lib/DevicesTable.svelte b/src/ui/src/lib/DevicesTable.svelte index 1627af8f..ba203602 100644 --- a/src/ui/src/lib/DevicesTable.svelte +++ b/src/ui/src/lib/DevicesTable.svelte @@ -31,7 +31,7 @@ } -
+
{#if $devices} {/if} diff --git a/src/ui/src/lib/GlobalSettings.svelte b/src/ui/src/lib/GlobalSettings.svelte new file mode 100644 index 00000000..8e3aecb1 --- /dev/null +++ b/src/ui/src/lib/GlobalSettings.svelte @@ -0,0 +1,155 @@ + + +{#if loading} +
+
+ +

Loading settings...

+
+
+{:else if error} +
+

Error: {error}

+
+{:else if $settings} +
+
+

Updating

+
+
+ + +
+
+ + +
+
+
+ +
+

Scanning

+
+ +
+
+ +
+

Counting

+
+ + + + + + + +
+
+ +
+

Filtering

+
+ + + + + + + + + +
+
+ +
+

Calibration

+
+ + + + + + + +
+
+ +
+ +
+
+{:else} +
+

No settings available.

+
+{/if} \ No newline at end of file diff --git a/src/ui/src/lib/NodesTable.svelte b/src/ui/src/lib/NodesTable.svelte index 19f17634..f5931371 100644 --- a/src/ui/src/lib/NodesTable.svelte +++ b/src/ui/src/lib/NodesTable.svelte @@ -31,7 +31,7 @@ } -
+
{#if $nodes} diff --git a/src/ui/src/lib/TriStateCheckbox.svelte b/src/ui/src/lib/TriStateCheckbox.svelte new file mode 100644 index 00000000..b96b7480 --- /dev/null +++ b/src/ui/src/lib/TriStateCheckbox.svelte @@ -0,0 +1,38 @@ + + + + + \ No newline at end of file diff --git a/src/ui/src/lib/images/settings.svg b/src/ui/src/lib/images/settings.svg new file mode 100644 index 00000000..fb211293 --- /dev/null +++ b/src/ui/src/lib/images/settings.svg @@ -0,0 +1 @@ + diff --git a/src/ui/src/lib/stores.ts b/src/ui/src/lib/stores.ts index 621e97bf..55cd48ff 100644 --- a/src/ui/src/lib/stores.ts +++ b/src/ui/src/lib/stores.ts @@ -1,6 +1,6 @@ import { readable, writable, derived } from 'svelte/store'; import { base } from '$app/paths'; -import type { Device, Config, Node } from './types'; +import type { Device, Config, Node, Settings, CalibrationData } from './types'; export const showAll: SvelteStore = writable(false); export const config = writable(); @@ -54,8 +54,8 @@ export const devices = readable([], function start(set) { function fetchDevices() { fetch(`${base}/api/state/devices`) .then((d) => d.json()) - .then((r) => { - deviceMap = new Map(r.map((device) => [device.id, device])); + .then((r: Device[]) => { + deviceMap = new Map(r.map((device: Device) => [device.id, device])); updateDevicesFromMap(); }) .catch((ex) => { @@ -117,7 +117,7 @@ export const nodes = readable([], function start(set) { }; }); -export const calibration = readable({}, function start(set) { +export const calibration = readable({ matrix: {} }, function start(set) { async function fetchAndSet() { const response = await fetch(`${base}/api/state/calibration`); var data = await response.json(); @@ -133,3 +133,30 @@ export const calibration = readable({}, function start(set) { clearInterval(interval); }; }); + +export const settings = (() => { + const { subscribe, set, update } = writable(null); + + return { + subscribe, + set, + update, + load: async () => { + const response = await fetch(`${base}/api/settings`); + if (!response.ok) throw new Error("Something went wrong loading settings (error="+response.status+" "+response.statusText+")"); + const data = await response.json(); + set(data); + }, + save: async (newSettings: Settings) => { + const response = await fetch(`${base}/api/settings`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newSettings), + }); + const data = await response.json(); + set(data); + }, + }; +})(); \ No newline at end of file diff --git a/src/ui/src/lib/types.ts b/src/ui/src/lib/types.ts index 4ad9462b..55d0d3f7 100644 --- a/src/ui/src/lib/types.ts +++ b/src/ui/src/lib/types.ts @@ -122,6 +122,52 @@ export interface Release { name: string; } +export interface CalibrationData { + matrix: { + [key: string]: { + [key: string]: { + expected?: number; + actual?: number; + err?: number; + percent?: number; + absorption?: number; + rx_adj_rssi?: number; + tx_ref_rssi?: number; + var?: number; + } + } + } +} + +export type Settings = { + updating: { + autoUpdate: boolean; + preRelease: boolean; + }; + scanning: { + forgetAfterMs: number | null; + }; + counting: { + idPrefixes: string | null; + startCountingDistance: number | null; + stopCountingDistance: number | null; + includeDevicesAge: number | null; + }; + filtering: { + includeIds: string | null; + excludeIds: string | null; + maxReportDistance: number | null; + earlyReportDistance: number | null; + skipReportAge: number | null; + }; + calibration: { + rssiAt1m: number | null; + rssiAdjustment: number | null; + absorptionFactor: number | null; + iBeaconRssiAt1m: number | null; + }; +}; + export function isNode(d: Device | Node | null): d is Node { return (d as Node)?.telemetry !== undefined; -} +} \ No newline at end of file diff --git a/src/ui/src/routes/+layout.svelte b/src/ui/src/routes/+layout.svelte index 00bf2504..75f33552 100644 --- a/src/ui/src/routes/+layout.svelte +++ b/src/ui/src/routes/+layout.svelte @@ -14,6 +14,7 @@ import nodes from '$lib/images/nodes.svg'; import devices from '$lib/images/devices.svg'; import calibration from '$lib/images/calibration.svg'; + import settings from '$lib/images/settings.svg'; initializeStores(); @@ -62,6 +63,11 @@ Calibration + + Settings + Settings + + GitHub diff --git a/src/ui/src/routes/calibration/+page.svelte b/src/ui/src/routes/calibration/+page.svelte index b62cb8fd..cf23c9c9 100644 --- a/src/ui/src/routes/calibration/+page.svelte +++ b/src/ui/src/routes/calibration/+page.svelte @@ -1,117 +1,13 @@ ESPresense Companion: Calibration -
-

Calibration

- - {#if $calibration?.matrix} - {#each Object.entries($calibration?.matrix) as [id1, n1] (id1)} - {#each rxColumns as id2 (id2)} -
- {#if n1[id2]} - Expected {@html Number(n1[id2].expected?.toPrecision(3))} - Actual {@html Number(n1[id2]?.actual?.toPrecision(3))} = Error {@html Number(n1[id2]?.err?.toPrecision(3))} - {:else} - No beacon Received in last 30 seconds - {/if} -
-
- {/each} - {/each} - {/if} +
+

Calibration

-
- {#if $calibration?.matrix} -
-
- - Error % - Error (m) - Absorption - Rx Rssi Adj - Tx Rssi Ref - Variance (m) - -
-
-
- - - - - {#each rxColumns as id} - - {/each} - - - - {#each Object.entries($calibration.matrix) as [id1, n1] (id1)} - - - {#each rxColumns as id2 (id2)} - {#if n1[id2]} - - {:else} - - {/each} - -
NameRx: {@html id}
Tx: {@html id1}{@html value(n1[id2], data_point)} - {/if} - {/each} -
-
- {:else} -

Loading...

- {/if} -
+
diff --git a/src/ui/src/routes/devices/+page.svelte b/src/ui/src/routes/devices/+page.svelte index d8165067..d94d01df 100644 --- a/src/ui/src/routes/devices/+page.svelte +++ b/src/ui/src/routes/devices/+page.svelte @@ -7,7 +7,7 @@ ESPresense Companion: Devices -
-

Devices

+
+

Devices

detail(d.detail)} />
diff --git a/src/ui/src/routes/nodes/+page.svelte b/src/ui/src/routes/nodes/+page.svelte index 4101d8fb..b72bbb21 100644 --- a/src/ui/src/routes/nodes/+page.svelte +++ b/src/ui/src/routes/nodes/+page.svelte @@ -6,8 +6,8 @@ ESPresense Companion: Nodes -
-

Nodes

+
+

Nodes

diff --git a/src/ui/src/routes/settings/+page.svelte b/src/ui/src/routes/settings/+page.svelte new file mode 100644 index 00000000..a77dcab0 --- /dev/null +++ b/src/ui/src/routes/settings/+page.svelte @@ -0,0 +1,14 @@ + + + + ESPresense Companion: Settings + + +
+

Settings

+

These settings will be applied to every node, including new nodes.

+ + +