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/DeviceController.cs b/src/Controllers/DeviceController.cs index a4673b06..42a74896 100644 --- a/src/Controllers/DeviceController.cs +++ b/src/Controllers/DeviceController.cs @@ -19,22 +19,26 @@ public DeviceController(ILogger logger, DeviceSettingsStore de _state = state; } - [HttpGet("{id}")] - public DeviceSettingsDetails Get(string id) + [HttpGet("{id}/details")] + public async Task>> Details(string id) { - var deviceSettings = _deviceSettingsStore.Get(id); - var details = new List>(); - 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>(); } - [HttpPut("{id}")] + [HttpGet("{id}/settings")] + public Task Get(string id) + { + var settings = _deviceSettingsStore.Get(id); + settings ??= new DeviceSettings { OriginalId = id, Id = id }; + return Task.FromResult(settings); + } + + [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> details); } diff --git a/src/Controllers/NodeController.cs b/src/Controllers/NodeController.cs index 8cfe229f..8a946d68 100644 --- a/src/Controllers/NodeController.cs +++ b/src/Controllers/NodeController.cs @@ -8,18 +8,18 @@ namespace ESPresense.Controllers; [ApiController] public class NodeController(NodeSettingsStore nodeSettingsStore, State state) : ControllerBase { - [HttpGet("{id}")] + [HttpGet("{id}/settings")] public NodeSettingsDetails Get(string id) { var nodeSettings = nodeSettingsStore.Get(id); var details = new List>(); if (nodeSettings?.Id != null && state.Nodes.TryGetValue(id, out var node)) details.AddRange(node.GetDetails()); - return new NodeSettingsDetails(nodeSettings ?? new NodeSettings(id), details); + return new NodeSettingsDetails(nodeSettings ?? new Models.NodeSettings(id), details); } - [HttpPut("{id}")] - public Task Set(string id, [FromBody] NodeSettings ds) + [HttpPut("{id}/settings")] + public Task Set(string id, [FromBody] Models.NodeSettings ds) { return nodeSettingsStore.Set(id, ds); } @@ -36,5 +36,5 @@ public async Task Restart(string id) await nodeSettingsStore.Restart(id); } - public readonly record struct NodeSettingsDetails(NodeSettings? settings, IList> details); + public readonly record struct NodeSettingsDetails(Models.NodeSettings? settings, IList> details); } \ No newline at end of file diff --git a/src/Controllers/StateController.cs b/src/Controllers/StateController.cs index 8733fd74..4c431b35 100644 --- a/src/Controllers/StateController.cs +++ b/src/Controllers/StateController.cs @@ -66,9 +66,9 @@ public Calibration GetCalibration() { 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; diff --git a/src/Models/NodeSettings.cs b/src/Models/NodeSettings.cs index a7dceb02..4b101b97 100644 --- a/src/Models/NodeSettings.cs +++ b/src/Models/NodeSettings.cs @@ -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(); } \ No newline at end of file diff --git a/src/Models/OptimizationResults.cs b/src/Models/OptimizationResults.cs index 8dffa723..0308901c 100644 --- a/src/Models/OptimizationResults.cs +++ b/src/Models/OptimizationResults.cs @@ -20,9 +20,9 @@ public double Evaluate(List 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); diff --git a/src/Optimizers/OptimizationRunner.cs b/src/Optimizers/OptimizationRunner.cs index 87b25fc7..e20bfc07 100644 --- a/src/Optimizers/OptimizationRunner.cs +++ b/src/Optimizers/OptimizationRunner.cs @@ -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); } diff --git a/src/Services/NodeSettingsStore.cs b/src/Services/NodeSettingsStore.cs index cebd9337..9d732003 100644 --- a/src/Services/NodeSettingsStore.cs +++ b/src/Services/NodeSettingsStore.cs @@ -5,22 +5,23 @@ namespace ESPresense.Services { public class NodeSettingsStore(MqttCoordinator mqtt, ILogger logger) : BackgroundService { - private readonly ConcurrentDictionary _storeById = new(); + private readonly ConcurrentDictionary _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); } - public async Task Set(string id, NodeSettings ds) + public async Task Set(string id, Models.NodeSettings ns) { - 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}"); + var oCs = Get(id).Calibration; + var nCs = ns.Calibration; + if (nCs.Absorption != null && nCs.Absorption != oCs.Absorption) + await mqtt.EnqueueAsync($"espresense/rooms/{id}/absorption/set", $"{nCs.Absorption:0.00}"); + if (nCs.RxAdjRssi != null && nCs.RxAdjRssi != oCs.RxAdjRssi) + await mqtt.EnqueueAsync($"espresense/rooms/{id}/rx_adj_rssi/set", $"{nCs.RxAdjRssi}"); + if (nCs.TxRefRssi != null && nCs.TxRefRssi != oCs.TxRefRssi) + await mqtt.EnqueueAsync($"espresense/rooms/{id}/tx_ref_rssi/set", $"{nCs.TxRefRssi}"); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -33,16 +34,16 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) switch (arg.Setting) { case "absorption": - ns.Absorption = double.Parse(arg.Payload); + ns.Calibration.Absorption = double.Parse(arg.Payload); break; case "rx_adj_rssi": - ns.RxAdjRssi = int.Parse(arg.Payload); + ns.Calibration.RxAdjRssi = int.Parse(arg.Payload); break; case "tx_ref_rssi": - ns.TxRefRssi = int.Parse(arg.Payload); + ns.Calibration.TxRefRssi = int.Parse(arg.Payload); break; case "max_distance": - ns.MaxDistance = double.Parse(arg.Payload); + ns.Filtering.MaxDistance = double.Parse(arg.Payload); break; default: return Task.CompletedTask; 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/NodeActions.svelte b/src/ui/src/lib/NodeActions.svelte index 4a092f7d..c3ab6ee7 100644 --- a/src/ui/src/lib/NodeActions.svelte +++ b/src/ui/src/lib/NodeActions.svelte @@ -5,18 +5,18 @@ import { getModalStore, getToastStore, type ToastSettings } from '@skeletonlabs/skeleton'; import { updateMethod, flavor, version, artifact, flavorNames } from '$lib/firmware'; import Firmware from '$lib/modals/Firmware.svelte'; + import { restartNode, updateNodeSelf } from '$lib/node'; const modalStore = getModalStore(); const toastStore = getToastStore(); async function onRestart(i: Node) { try { - var response = await fetch(`${base}/api/node/${i.id}/restart`, { method: 'POST' }); - if (response.status != 200) throw new Error(response.statusText); + await restartNode(i.id); toastStore.trigger({ message: i.name + ' asked to reboot', background: 'variant-filled-primary' }); } catch (e) { console.log(e); - toastStore.trigger({ message: e, background: 'variant-filled-error' }); + toastStore.trigger({ message: e instanceof Error ? e.message : String(e), background: 'variant-filled-error' }); } } @@ -44,12 +44,8 @@ }); } else { if (i) { - fetch(`${base}/api/node/${i.id}/update`, { - method: 'POST', - body: '' - }) - .then((response) => { - if (response.status != 200) throw new Error(response.statusText); + updateNodeSelf(i.id) + .then(() => { const t: ToastSettings = { message: (i.name ?? i.id) + ' asked to update itself', background: 'variant-filled-primary' }; toastStore.trigger(t); }) diff --git a/src/ui/src/lib/NodeSettings.svelte b/src/ui/src/lib/NodeSettings.svelte index 32d72df6..ffa71c31 100644 --- a/src/ui/src/lib/NodeSettings.svelte +++ b/src/ui/src/lib/NodeSettings.svelte @@ -1,31 +1,22 @@ @@ -62,7 +53,7 @@ Max Distance - + 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/device.ts b/src/ui/src/lib/device.ts new file mode 100644 index 00000000..a6d50b1b --- /dev/null +++ b/src/ui/src/lib/device.ts @@ -0,0 +1,14 @@ +import { base } from '$app/paths'; +import type { DeviceSetting, DeviceDetail } from './types'; + +export async function fetchDeviceSettings(fetch, id: string): Promise { + const response = await fetch(`${base}/api/device/${id}/settings`); + if (!response.ok) throw new Error("Something went wrong loading device details (error="+response.status+" "+response.statusText+")"); + return await response.json(); +} + +export async function fetchDeviceDetails(id: string): Promise { + const response = await fetch(`${base}/api/device/${id}/details`); + if (!response.ok) throw new Error("Something went wrong loading device details (error="+response.status+" "+response.statusText+")"); + return await response.json(); +} diff --git a/src/ui/src/lib/node.ts b/src/ui/src/lib/node.ts new file mode 100644 index 00000000..7bc8db8e --- /dev/null +++ b/src/ui/src/lib/node.ts @@ -0,0 +1,47 @@ +import { base } from '$app/paths'; +import type { Settings, Node, NodeSetting } from './types'; + +export async function loadSettings(id: string): Promise { + const response = await fetch(`${base}/api/node/${id}/settings`); + if (!response.ok) throw new Error("Something went wrong loading settings (error="+response.status+" "+response.statusText+")"); + return await response.json(); +} + +export async function saveSettings(newSettings: Settings): Promise { + const response = await fetch(`${base}/api/node/${newSettings.id}/settings`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newSettings), + }); + if (!response.ok) throw new Error("Something went wrong loading settings (error="+response.status+" "+response.statusText+")"); + return await response.json(); +} + +export async function restartNode(nodeId: string): Promise { + const response = await fetch(`${base}/api/node/${nodeId}/restart`, { method: 'POST' }); + if (!response.ok) throw new Error(response.statusText); +} + +export async function updateNodeSelf(nodeId: string): Promise { + const response = await fetch(`${base}/api/node/${nodeId}/update`, { method: 'POST' }); + if (!response.ok) throw new Error(response.statusText); +} + +export async function saveNodeSettings(nodeId: string, settings: NodeSetting): Promise { + const response = await fetch(`${base}/api/node/${nodeId}/settings`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(settings) + }); + if (!response.ok) throw new Error(response.statusText); +} + +export async function fetchNode(nodeId: string): Promise { + const response = await fetch(`${base}/api/node/${nodeId}/settings`); + if (!response.ok) throw new Error(response.statusText); + return await response.json(); +} \ No newline at end of file diff --git a/src/ui/src/lib/state.ts b/src/ui/src/lib/state.ts new file mode 100644 index 00000000..82ca3016 --- /dev/null +++ b/src/ui/src/lib/state.ts @@ -0,0 +1,26 @@ +import { base } from '$app/paths'; +import type { Config, CalibrationData, Device } from './types'; + +export async function fetchConfig(): Promise { + const response = await fetch(`${base}/api/state/config`); + if (!response.ok) throw new Error("Something went wrong loading config (error="+response.status+" "+response.statusText+")"); + return await response.json(); +} + +export async function fetchCalibrationState(): Promise { + const response = await fetch(`${base}/api/state/calibration`); + if (!response.ok) throw new Error("Something went wrong loading calibration state (error="+response.status+" "+response.statusText+")"); + return await response.json(); +} + +export async function fetchDeviceState(): Promise { + const response = await fetch(`${base}/api/state/devices`); + if (!response.ok) throw new Error("Something went wrong loading device state (error="+response.status+" "+response.statusText+")"); + return await response.json(); +} + +export async function fetchNodeState(includeTele: boolean = true): Promise { + const response = await fetch(`${base}/api/state/nodes?includeTele=${includeTele}`); + if (!response.ok) throw new Error("Something went wrong loading node state (error="+response.status+" "+response.statusText+")"); + return await response.json(); +} \ No newline at end of file diff --git a/src/ui/src/lib/stores.ts b/src/ui/src/lib/stores.ts index 2a59607e..939c6329 100644 --- a/src/ui/src/lib/stores.ts +++ b/src/ui/src/lib/stores.ts @@ -1,6 +1,8 @@ import { readable, writable, derived } from 'svelte/store'; import { base } from '$app/paths'; -import type { Device, Config, Node, CalibrationResponse } from './types'; +import type { Device, Config, Node, NodeSettings, CalibrationResponse } from './types'; +import { loadSettings, saveSettings } from './node'; +import { fetchConfig, fetchCalibrationState, fetchDeviceState, fetchNodeState } from './state'; export const showAll: SvelteStore = writable(false); export const config = writable(); @@ -37,8 +39,8 @@ let socket: WebSocket; export const history = writable(['/']); async function getConfig() { - const response = await fetch(`${base}/api/state/config`); - config.set(await response.json()); + const data = await fetchConfig(); + config.set(data); } getConfig(); @@ -112,8 +114,7 @@ export const nodes = readable([], function start(set) { const interval = setInterval(() => { if (outstanding) return; outstanding = true; - fetch(`${base}/api/state/nodes?includeTele=true`) - .then((d) => d.json()) + fetchNodeState() .then((r) => { outstanding = false; errors = 0; @@ -133,17 +134,32 @@ export const nodes = 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(); + const data = await fetchCalibrationState(); set(data); } fetchAndSet(); - const interval = setInterval(() => { - fetchAndSet(); - }, 1000); + const interval = setInterval(fetchAndSet, 1000); return function stop() { clearInterval(interval); }; }); + +export const settings = (() => { + const { subscribe, set, update } = writable(null); + + return { + subscribe, + set, + update, + load: async () => { + const data = await loadSettings("*"); + set(data); + }, + save: async (newSettings: Settings) => { + const data = await saveSettings(newSettings); + 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 681bab04..3a0d2588 100644 --- a/src/ui/src/lib/types.ts +++ b/src/ui/src/lib/types.ts @@ -73,14 +73,7 @@ export interface Config { map: MapConfig; } -export type NodeSetting = { - id: string | null; - name: string | null; - absorption: number | null; - rx_adj_rssi: number | null; - tx_ref_rssi: number | null; - max_distance: number | null; -}; +export type DeviceDetail = Array<{ key: string; value: string }>; export type DeviceSetting = { originalId: string; @@ -160,6 +153,37 @@ export interface CalibrationResponse { matrix: CalibrationMatrix; } +export type NodeSetting = { + id: string | null; + name: string | null; + 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; + absorption: 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 3a0fe504..8a8061e6 100644 --- a/src/ui/src/routes/+layout.svelte +++ b/src/ui/src/routes/+layout.svelte @@ -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' } ]; diff --git a/src/ui/src/routes/devices/[id]/+page.svelte b/src/ui/src/routes/devices/[id]/+page.svelte index dd6ce578..052dd381 100644 --- a/src/ui/src/routes/devices/[id]/+page.svelte +++ b/src/ui/src/routes/devices/[id]/+page.svelte @@ -2,22 +2,27 @@ import { base } from '$app/paths'; import { devices } from '$lib/stores'; import { readable } from 'svelte/store'; - import type { DeviceSetting } from '$lib/types'; + import type { DeviceSetting, DeviceDetail } from '$lib/types'; import { Accordion, AccordionItem } from '@skeletonlabs/skeleton'; + import { fetchDeviceDetails } from '$lib/device'; import Map from '$lib/Map.svelte'; import DeviceDetailTabs from '$lib/DeviceDetailTabs.svelte'; import DeviceSettings from '$lib/DeviceSettings.svelte'; export let tab = 'map'; - export let data: { settings?: DeviceSetting } = {}; - $: device = $devices.find((d) => d.id === data.settings?.id); + export let data: { id: string, settings: DeviceSetting }; + $: device = $devices.find((d) => d.id === data.id); - export const deviceDetails = readable([], (set) => { + export const deviceDetails = readable([], (set) => { async function fetchAndSet() { try { - const response = await fetch(`${base}/api/device/${data.settings?.id}`); - const result = await response.json(); + const id = data.id; + if (!id) { + console.error('No device id'); + return; + } + const result = await fetchDeviceDetails(id); set(result.details); } catch (ex) { console.error(ex); diff --git a/src/ui/src/routes/devices/[id]/+page.ts b/src/ui/src/routes/devices/[id]/+page.ts index 6743de77..0ac0c4b3 100644 --- a/src/ui/src/routes/devices/[id]/+page.ts +++ b/src/ui/src/routes/devices/[id]/+page.ts @@ -1,13 +1,16 @@ -import { base } from '$app/paths'; +import { error } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; +import { fetchDeviceSettings } from '$lib/device' -export async function load({ fetch, params }) { - return await fetch(`${base}/api/device/${params.id}`) - .then((response) => { - if (response.status != 200) throw new Error(response.statusText); - var data = response.json(); - return data; - }) - .catch((e) => { - return { settings: { originalId: params.id, id: null, name: null, 'rssi@1m': null, error: e } }; - }); -} +export const load: PageLoad = async ({ fetch, params }) => { + if (!params.id) { + throw error(400, 'No device id'); + } + try { + var settings = fetchDeviceSettings(fetch, params.id); + return { id: params.id, settings: settings }; + } + catch (e) { + return { settings: { originalId: params.id, id: null, name: null, 'rssi@1m': null, error: e } }; + } +}; diff --git a/src/ui/src/routes/nodes/[id]/+page.svelte b/src/ui/src/routes/nodes/[id]/+page.svelte index d50bd5c4..67520027 100644 --- a/src/ui/src/routes/nodes/[id]/+page.svelte +++ b/src/ui/src/routes/nodes/[id]/+page.svelte @@ -1,72 +1,27 @@ - - ESPresense Companion: Map - - - - -
-
- {#if floorId !== 'settings'} - - {/if} - {#if floorId === 'settings'} - - {/if} -
-
- - - -

Details

-
- - {#if $nodeDetails} - {#each $nodeDetails as d} - - {/each} - {/if} - -
-
-
-
+{#if error} +

{error}

+{:else if node} +

{node.name ?? node.id}

+ +{:else} +

Loading...

+{/if} diff --git a/src/ui/src/routes/nodes/[id]/+page.ts b/src/ui/src/routes/nodes/[id]/+page.ts index dfb014c9..df7c64bb 100644 --- a/src/ui/src/routes/nodes/[id]/+page.ts +++ b/src/ui/src/routes/nodes/[id]/+page.ts @@ -1,7 +1,7 @@ import { base } from '$app/paths'; export async function load({ fetch, params }) { - return await fetch(`${base}/api/node/${params.id}`) + return await fetch(`${base}/api/node/${params.id}/settings`) .then((response) => { if (response.status != 200) throw new Error(response.statusText); var data = response.json(); 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.

+ + +