using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models; using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services; using LiveChartsCore; using LiveChartsCore.Defaults; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using Serilog; using SkiaSharp; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModels { public partial class MainWindowViewModel : ViewModelBase, IDisposable { private readonly SlipResistanceDeviceService deviceService = new(); private readonly SlipExcelExportService excelExportService = new(); private readonly DispatcherTimer refreshTimer; private readonly Stopwatch runStopwatch = new(); private readonly List currentRun = []; private readonly ObservableCollection verticalLoadPoints = []; private readonly ObservableCollection horizontalFrictionPoints = []; private readonly ObservableCollection frictionCoefficientPoints = []; private readonly ObservableCollection displacementPoints = []; private bool isLoadingDeviceSettings; private bool wasRunning; private List lastCompletedRun = []; [ObservableProperty] private string testNumber = $"SLIP-{DateTime.Now:yyyyMMdd-HHmm}"; [ObservableProperty] private string operatorName = string.Empty; [ObservableProperty] private string methodName = "GB/T 3903.6-2024"; [ObservableProperty] private string reportName = string.Empty; [ObservableProperty] private string sampleFeature = "整鞋样品 / 瓷砖接触面 / 水平测试"; [ObservableProperty] private string currentStatus = "设备连接中,等待 PLC 与 ADC 实时数据"; [ObservableProperty] private int uploadProgress; [ObservableProperty] private string manualDistance = "0"; [ObservableProperty] private int selectedSampleIndex; [ObservableProperty] private bool isSettingsDialogOpen; [ObservableProperty] private string manualSpeed = "0.00"; [ObservableProperty] private string manualDisplacement = "0.00"; [ObservableProperty] private string testSpeed = "0.30"; [ObservableProperty] private string normalPressureZero = "0"; [ObservableProperty] private string normalPressureCoefficient = "0.00"; [ObservableProperty] private string frictionZero1 = "0"; [ObservableProperty] private string frictionCoefficient1 = "0.00"; [ObservableProperty] private string frictionZero2 = "0"; [ObservableProperty] private string frictionCoefficient2 = "0.00"; [ObservableProperty] private string plcPortName = "COM7"; [ObservableProperty] private string adcPortName = "COM8"; [ObservableProperty] private int baudRate = 115200; [ObservableProperty] private int selectedShoeSizeIndex; [ObservableProperty] private int selectedLubricantIndex; [ObservableProperty] private int selectedModeIndex = 2; [ObservableProperty] private int selectedSurfaceIndex; [ObservableProperty] private string targetLoadText = "400 N"; [ObservableProperty] private string actualLoadText = "0.0 N"; [ObservableProperty] private string deviceStatus = "离线"; [ObservableProperty] private string activeMode = "水平测试模式"; [ObservableProperty] private string resultSummary = "等待 3 次有效试验"; [ObservableProperty] private string staticCoefficient = "0.000"; [ObservableProperty] private string dynamicCoefficient = "0.000"; [ObservableProperty] private string verticalPressure = "0.0"; [ObservableProperty] private string horizontalForce = "0.0"; [ObservableProperty] private string frictionCoefficient = "0.000"; [ObservableProperty] private string distance = "0.0"; public string TestSpeedText => $"{TestSpeed} m/s"; public string SampleRateText { get; } = "30 Hz"; public string BatchNumber { get; } = DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); public string UploadProgressText => $"{UploadProgress}%"; public string StandardReference { get; } = "GB/T 3903.6-2024 5.1.3、7.3.1.4、8.1-8.3:实时采集、静/动摩擦系数、三次平均与重测判定"; public ObservableCollection Samples { get; } = []; public DrawMarginFrame ChartFrame { get; } = new() { Fill = new SolidColorPaint(SKColor.Parse("#FFFFFF")), Stroke = new SolidColorPaint(SKColor.Parse("#CBD5E1")) { StrokeThickness = 1.4f } }; public SolidColorPaint LegendTextPaint { get; } = new(SKColor.Parse("#334155")); public ISeries[] Series { get; } public Axis[] XAxes { get; } = [ new Axis { Name = "时间(s)", MinLimit = 0, UnitWidth = 0.1, SeparatorsPaint = new SolidColorPaint(SKColor.Parse("#D7E0EA")) { StrokeThickness = 1 }, SubseparatorsPaint = new SolidColorPaint(SKColor.Parse("#EEF3F8")) { StrokeThickness = 1 }, SubseparatorsCount = 4, LabelsPaint = new SolidColorPaint(SKColor.Parse("#475569")), NamePaint = new SolidColorPaint(SKColor.Parse("#334155")), TextSize = 12, NameTextSize = 13, Padding = new LiveChartsCore.Drawing.Padding(4, 3, 4, 3) } ]; public Axis[] YAxes { get; } = [ new Axis { Name = "压力 / 摩擦力 / 位移", MinLimit = 0, SeparatorsPaint = new SolidColorPaint(SKColor.Parse("#D7E0EA")) { StrokeThickness = 1 }, SubseparatorsPaint = new SolidColorPaint(SKColor.Parse("#EEF3F8")) { StrokeThickness = 1 }, SubseparatorsCount = 4, LabelsPaint = new SolidColorPaint(SKColor.Parse("#475569")), NamePaint = new SolidColorPaint(SKColor.Parse("#334155")), TextSize = 11, NameTextSize = 12, Padding = new LiveChartsCore.Drawing.Padding(2, 2, 3, 2) }, new Axis { Name = "摩擦系数", Position = LiveChartsCore.Measure.AxisPosition.End, MinLimit = 0, UnitWidth = 0.1, LabelsPaint = new SolidColorPaint(SKColor.Parse("#7E22CE")), NamePaint = new SolidColorPaint(SKColor.Parse("#7E22CE")), SeparatorsPaint = new SolidColorPaint(SKColor.Parse("#F1E7FF")) { StrokeThickness = 1 }, TextSize = 11, NameTextSize = 12, Padding = new LiveChartsCore.Drawing.Padding(3, 2, 2, 2) } ]; public MainWindowViewModel() { Log.Information("初始化主页面 ViewModel"); Series = [ CreateLineSeries("垂直压力(N)", verticalLoadPoints, "#DC2626", 0, 0.65), CreateLineSeries("水平摩擦力(N)", horizontalFrictionPoints, "#16A34A", 0, 0.65), CreateLineSeries("摩擦系数", frictionCoefficientPoints, "#C026D3", 1, 0.65), CreateLineSeries("位移(mm)", displacementPoints, "#2563EB", 0, 0.35) ]; LoadDeviceSettings(); UpdateTargetLoad(); deviceService.Start(CurrentSettings()); _ = LoadPlcParametersAsync(); refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(33) }; refreshTimer.Tick += (_, _) => RefreshFromDevice(); refreshTimer.Start(); } [RelayCommand] private async Task Clear() { await RunDeviceCommand(deviceService.PulseResetAsync(), "已发送复位指令 M90"); } [RelayCommand] private void Preload() { CurrentStatus = "请在 0.2 s 内施加目标垂直载荷,载荷达到后启动滑动测试"; } [RelayCommand] private async Task StartTest() { BeginRun(); await RunDeviceCommand(deviceService.PulseStartTestAsync(), "已发送测试启动指令 M80,等待 M81 运行状态"); } [RelayCommand] private async Task StopTest() { await RunDeviceCommand(deviceService.PulseStopTestAsync(), "已发送测试停止指令 M83"); } [RelayCommand] private void ExportReport() { var points = currentRun.Count > 0 ? currentRun.ToList() : lastCompletedRun; if (points.Count == 0) { Log.Warning("请求导出 Excel 时没有可导出的实时采样数据:TestNumber={TestNumber}", TestNumber); CurrentStatus = "没有可导出的实时采样数据,请先完成一次测试"; return; } try { var report = new SlipReportExport( TestNumber, OperatorName, MethodName, ReportName, SampleFeature, CurrentShoeSize(), CurrentLubricant(), CurrentMode(), CurrentSurface(), TargetLoadText, ActualLoadText, TestSpeedText, StandardReference, Samples.ToList(), points); var path = excelExportService.Export(report); UploadProgress = 100; CurrentStatus = $"Excel 已导出:{path}"; } catch (Exception ex) { Log.Error(ex, "Excel 导出失败:TestNumber={TestNumber}, PointCount={PointCount}", TestNumber, points.Count); CurrentStatus = $"Excel 导出失败:{ex.Message}"; } } [RelayCommand] private async Task Lift() => await RunDeviceCommand(deviceService.LiftAsync(), "垂直架提升指令已发送 M5"); [RelayCommand] private async Task Lower() => await RunDeviceCommand(deviceService.LowerAsync(), "垂直架下降指令已发送 M4"); [RelayCommand] private async Task MoveLeft() => await RunDeviceCommand(deviceService.ToggleMoveLeftAsync(), "水平板左移状态已切换 M1"); [RelayCommand] private async Task MoveRight() => await RunDeviceCommand(deviceService.ToggleMoveRightAsync(), "水平板右移状态已切换 M2"); [RelayCommand] private void DeleteSelectedSample() { var sample = Samples.FirstOrDefault(x => x.Index == SelectedSampleIndex); if (sample is null) { CurrentStatus = $"未找到序号 {SelectedSampleIndex} 的实验数据"; return; } Samples.Remove(sample); UpdateResultSummary(); CurrentStatus = $"已删除序号 {SelectedSampleIndex} 的实验数据"; } [RelayCommand] private void ShowSettingsDialog() { IsSettingsDialogOpen = true; } [RelayCommand] private void CloseSettingsDialog() { IsSettingsDialogOpen = false; deviceService.UpdateSettings(CurrentSettings()); } [RelayCommand] private void CalibrateNormalPressureZero() { try { var zero = deviceService.CaptureCurrentAdcZero(); NormalPressureZero = zero.NormalPressureZero.ToString(CultureInfo.InvariantCulture); SaveAndApplySettings(); CurrentStatus = "正压力零点已按当前 ADC 原始值采集"; } catch (Exception ex) { Log.Error(ex, "正压力零点采集失败"); CurrentStatus = $"正压力零点采集失败:{ex.Message}"; } } [RelayCommand] private void CalibrateFrictionZero() { try { var zero = deviceService.CaptureCurrentAdcZero(); FrictionZero1 = zero.FrictionZero1.ToString(CultureInfo.InvariantCulture); FrictionZero2 = zero.FrictionZero2.ToString(CultureInfo.InvariantCulture); SaveAndApplySettings(); CurrentStatus = "摩擦零点已按当前 ADC 原始值采集"; } catch (Exception ex) { Log.Error(ex, "摩擦零点采集失败"); CurrentStatus = $"摩擦零点采集失败:{ex.Message}"; } } partial void OnUploadProgressChanged(int value) => OnPropertyChanged(nameof(UploadProgressText)); partial void OnManualSpeedChanged(string value) { SaveAndApplySettings(); WriteNumericSetting(value, deviceService.WriteManualSpeedAsync, "手动速度"); } partial void OnManualDisplacementChanged(string value) { SaveAndApplySettings(); WriteNumericSetting(value, deviceService.WriteManualDisplacementAsync, "手动位移"); } partial void OnTestSpeedChanged(string value) { OnPropertyChanged(nameof(TestSpeedText)); SaveAndApplySettings(); WriteNumericSetting(value, deviceService.WriteTestSpeedAsync, "测试速度"); } partial void OnNormalPressureZeroChanged(string value) => SaveAndApplySettings(); partial void OnNormalPressureCoefficientChanged(string value) => SaveAndApplySettings(); partial void OnFrictionZero1Changed(string value) => SaveAndApplySettings(); partial void OnFrictionCoefficient1Changed(string value) => SaveAndApplySettings(); partial void OnFrictionZero2Changed(string value) => SaveAndApplySettings(); partial void OnFrictionCoefficient2Changed(string value) => SaveAndApplySettings(); partial void OnPlcPortNameChanged(string value) => SaveAndApplySettings(); partial void OnAdcPortNameChanged(string value) => SaveAndApplySettings(); partial void OnBaudRateChanged(int value) => SaveAndApplySettings(); partial void OnSelectedShoeSizeIndexChanged(int value) => UpdateTargetLoad(); partial void OnSelectedModeIndexChanged(int value) { ActiveMode = CurrentMode(); } public void Dispose() { refreshTimer.Stop(); deviceService.Dispose(); } private void RefreshFromDevice() { var device = deviceService.CurrentSnapshot; DeviceStatus = device.IsConnected ? device.IsTestRunning ? "联机 / 测试中" : device.IsResetting ? "联机 / 复位中" : "联机 / 待机" : "离线"; VerticalPressure = device.VerticalLoadN.ToString("F1", CultureInfo.InvariantCulture); HorizontalForce = device.HorizontalFrictionN.ToString("F1", CultureInfo.InvariantCulture); FrictionCoefficient = device.FrictionCoefficient.ToString("F3", CultureInfo.InvariantCulture); Distance = device.DisplacementMm.ToString("F1", CultureInfo.InvariantCulture); ActualLoadText = $"{VerticalPressure} N"; if (!device.IsConnected && !string.IsNullOrWhiteSpace(device.LastError)) { CurrentStatus = $"设备离线:{device.LastError}"; } if (!wasRunning && device.IsTestRunning) { BeginRun(); } if (device.IsTestRunning) { RecordPoint(device); } if (wasRunning && !device.IsTestRunning) { CompleteRun(); } wasRunning = device.IsTestRunning; } private void BeginRun() { currentRun.Clear(); verticalLoadPoints.Clear(); horizontalFrictionPoints.Clear(); frictionCoefficientPoints.Clear(); displacementPoints.Clear(); runStopwatch.Restart(); UploadProgress = 0; CurrentStatus = "测试运行:按标准采集垂直载荷、摩擦力、位移与摩擦系数"; Log.Information("测试开始:TestNumber={TestNumber}, TargetLoad={TargetLoad}, TestSpeed={TestSpeed}", TestNumber, TargetLoadText, TestSpeedText); } private void RecordPoint(SlipDeviceSnapshot device) { var time = runStopwatch.Elapsed.TotalSeconds; if (currentRun.Count > 0 && time - currentRun[^1].TimeSeconds < 1.0 / 30.0) { return; } var point = new SlipDataPoint( device.Timestamp, time, device.VerticalLoadN, device.HorizontalFrictionN, device.DisplacementMm, device.FrictionCoefficient); currentRun.Add(point); verticalLoadPoints.Add(new ObservablePoint(time, point.VerticalLoadN)); horizontalFrictionPoints.Add(new ObservablePoint(time, point.HorizontalFrictionN)); frictionCoefficientPoints.Add(new ObservablePoint(time, point.FrictionCoefficient)); displacementPoints.Add(new ObservablePoint(time, point.DisplacementMm)); UploadProgress = Math.Min(99, currentRun.Count); } private void CompleteRun() { runStopwatch.Stop(); if (currentRun.Count < 3) { Log.Warning("测试停止但采样点不足:TestNumber={TestNumber}, PointCount={PointCount}", TestNumber, currentRun.Count); CurrentStatus = "测试已停止,但有效采样点不足,未生成结果"; return; } lastCompletedRun = currentRun.ToList(); var peak = FindStaticPeak(currentRun); var dynamicWindow = currentRun .Where(point => point.TimeSeconds >= 0.3 && point.TimeSeconds <= 0.6) .ToList(); if (dynamicWindow.Count == 0) { dynamicWindow = currentRun.ToList(); } var staticCoefficientValue = CalculateCoefficient(peak.HorizontalFrictionN, peak.VerticalLoadN); var dynamicForce = dynamicWindow.Average(point => point.HorizontalFrictionN); var dynamicLoad = dynamicWindow.Average(point => point.VerticalLoadN); var dynamicCoefficientValue = CalculateCoefficient(dynamicForce, dynamicLoad); var verdict = NeedsRetest(staticCoefficientValue, dynamicCoefficientValue) ? "需重测" : "有效"; var nextIndex = Samples.Count == 0 ? 1 : Samples.Max(sample => sample.Index) + 1; Samples.Insert(0, new TestSample( nextIndex, DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture), staticCoefficientValue.ToString("F3", CultureInfo.InvariantCulture), dynamicCoefficientValue.ToString("F3", CultureInfo.InvariantCulture), verdict, staticCoefficientValue, dynamicCoefficientValue)); StaticCoefficient = staticCoefficientValue.ToString("F3", CultureInfo.InvariantCulture); DynamicCoefficient = dynamicCoefficientValue.ToString("F3", CultureInfo.InvariantCulture); UpdateResultSummary(); UploadProgress = 100; CurrentStatus = verdict == "有效" ? "测试完成:已按标准生成静/动摩擦系数" : "测试完成:最近三次结果差异超过 10%,建议重新测试"; Log.Information( "测试完成:TestNumber={TestNumber}, PointCount={PointCount}, StaticCoefficient={StaticCoefficient:F3}, DynamicCoefficient={DynamicCoefficient:F3}, Verdict={Verdict}", TestNumber, currentRun.Count, staticCoefficientValue, dynamicCoefficientValue, verdict); } private bool NeedsRetest(double staticCoefficientValue, double dynamicCoefficientValue) { var latest = Samples.Take(2).ToList(); if (latest.Count < 2) { return false; } var staticValues = latest.Select(sample => sample.StaticCoefficientValue).Append(staticCoefficientValue).ToArray(); var dynamicValues = latest.Select(sample => sample.DynamicCoefficientValue).Append(dynamicCoefficientValue).ToArray(); return ExceedsTenPercent(staticValues) || ExceedsTenPercent(dynamicValues); } private static bool ExceedsTenPercent(double[] values) { var average = values.Average(); if (Math.Abs(average) < 0.0001) { return false; } return (values.Max() - values.Min()) / Math.Abs(average) > 0.10; } private void UpdateResultSummary() { var latest = Samples.Take(3).ToList(); if (latest.Count == 0) { ResultSummary = "等待 3 次有效试验"; return; } var staticAverage = latest.Average(sample => sample.StaticCoefficientValue); var dynamicAverage = latest.Average(sample => sample.DynamicCoefficientValue); ResultSummary = $"近 {latest.Count} 次平均 静 {staticAverage:F3} / 动 {dynamicAverage:F3}"; } private static SlipDataPoint FindStaticPeak(IReadOnlyList points) { for (var index = 1; index < points.Count - 1; index++) { var previous = points[index - 1].HorizontalFrictionN; var current = points[index].HorizontalFrictionN; var next = points[index + 1].HorizontalFrictionN; if (current >= previous && current >= next && current > 0) { return points[index]; } } return points.MaxBy(point => point.HorizontalFrictionN) ?? points[0]; } private static double CalculateCoefficient(double frictionForce, double verticalLoad) => Math.Abs(verticalLoad) > 0.0001 ? frictionForce / verticalLoad : 0; private async Task RunDeviceCommand(Task command, string successMessage) { try { await command; CurrentStatus = successMessage; } catch (Exception ex) { Log.Error(ex, "设备指令失败:{SuccessMessage}", successMessage); CurrentStatus = $"设备指令失败:{ex.Message}"; } } private void WriteNumericSetting(string value, Func writer, string label) { if (isLoadingDeviceSettings || !TryParseDouble(value, out var numericValue)) { return; } _ = RunDeviceCommand(writer(numericValue), $"{label}已写入 PLC"); } private void SaveAndApplySettings() { SaveDeviceSettings(); deviceService.UpdateSettings(CurrentSettings()); } private async Task LoadPlcParametersAsync() { try { var parameters = await deviceService.ReadDeviceParametersAsync(); isLoadingDeviceSettings = true; ManualSpeed = parameters.ManualSpeed.ToString("F2", CultureInfo.InvariantCulture); ManualDisplacement = parameters.ManualDisplacement.ToString("F2", CultureInfo.InvariantCulture); TestSpeed = parameters.TestSpeed.ToString("F2", CultureInfo.InvariantCulture); isLoadingDeviceSettings = false; SaveAndApplySettings(); } catch (Exception ex) { isLoadingDeviceSettings = false; Log.Warning(ex, "启动时读取 PLC 参数失败,将使用本地保存的设备设置"); } } private void LoadDeviceSettings() { if (!File.Exists(DeviceSettingsPath)) { return; } try { var json = File.ReadAllText(DeviceSettingsPath); var settings = JsonSerializer.Deserialize(json); if (settings is null) { return; } isLoadingDeviceSettings = true; ManualSpeed = settings.ManualSpeed ?? ManualSpeed; ManualDisplacement = settings.ManualDisplacement ?? ManualDisplacement; TestSpeed = settings.TestSpeed ?? TestSpeed; NormalPressureZero = settings.NormalPressureZero ?? NormalPressureZero; NormalPressureCoefficient = settings.NormalPressureCoefficient ?? NormalPressureCoefficient; FrictionZero1 = settings.FrictionZero1 ?? FrictionZero1; FrictionCoefficient1 = settings.FrictionCoefficient1 ?? FrictionCoefficient1; FrictionZero2 = settings.FrictionZero2 ?? FrictionZero2; FrictionCoefficient2 = settings.FrictionCoefficient2 ?? FrictionCoefficient2; PlcPortName = settings.PlcPortName ?? PlcPortName; AdcPortName = settings.AdcPortName ?? AdcPortName; BaudRate = settings.BaudRate > 0 ? settings.BaudRate : BaudRate; } catch (Exception ex) { Log.Warning(ex, "读取设备设置失败:Path={Path}", DeviceSettingsPath); } finally { isLoadingDeviceSettings = false; } } private void SaveDeviceSettings() { if (isLoadingDeviceSettings) { return; } try { Directory.CreateDirectory(Path.GetDirectoryName(DeviceSettingsPath)!); var json = JsonSerializer.Serialize(CurrentSettings(), new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(DeviceSettingsPath, json); } catch (Exception ex) { Log.Warning(ex, "保存设备设置失败:Path={Path}", DeviceSettingsPath); } } private DeviceSettings CurrentSettings() => new( ManualSpeed, ManualDisplacement, TestSpeed, NormalPressureZero, NormalPressureCoefficient, FrictionZero1, FrictionCoefficient1, FrictionZero2, FrictionCoefficient2, PlcPortName, AdcPortName, BaudRate); private void UpdateTargetLoad() { TargetLoadText = SelectedShoeSizeIndex switch { 1 => "350 N", 2 => "160 N", _ => "400 N" }; } private string CurrentShoeSize() => SelectedShoeSizeIndex switch { 1 => "205-250", 2 => "205以下", _ => "250(含)以上" }; private string CurrentLubricant() => SelectedLubricantIndex switch { 1 => "湿态 - 蒸馏水", 2 => "湿态 - 洗涤剂", 3 => "冰霜", _ => "干态" }; private string CurrentMode() => SelectedModeIndex switch { 0 => "后跟测试模式", 1 => "前掌测试模式", _ => "水平测试模式" }; private string CurrentSurface() => SelectedSurfaceIndex switch { 1 => "木地板", 2 => "石材", 3 => "冰霜表面", _ => "瓷砖" }; private static bool TryParseDouble(string value, out double numericValue) => double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out numericValue) || double.TryParse(value, NumberStyles.Float, CultureInfo.CurrentCulture, out numericValue); private static LineSeries CreateLineSeries( string name, ObservableCollection values, string color, int yAxis, double smoothness) => new() { Name = name, Values = values, MiniatureShapeSize = 8, MiniatureStrokeThickness = 2, Stroke = new SolidColorPaint(SKColor.Parse(color)) { StrokeThickness = 3.2f }, Fill = new SolidColorPaint(SKColors.Transparent), GeometryFill = new SolidColorPaint(SKColors.White), GeometryStroke = new SolidColorPaint(SKColor.Parse(color)) { StrokeThickness = 2 }, GeometrySize = 4, LineSmoothness = smoothness, ScalesYAt = yAxis }; private static string DeviceSettingsPath => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FootwearSlipResistance", "device-settings.json"); } }