Many cool stuff

This commit is contained in:
Holger Börchers 2025-04-02 15:09:40 +02:00
parent fd054d854b
commit 0e75065ff7
9 changed files with 264 additions and 176 deletions

View File

@ -14,9 +14,9 @@ indent_size = 2
#### Core EditorConfig Options #### #### Core EditorConfig Options ####
# Indentation and spacing # Indentation and spacing
indent_size = 4 indent_size = 2
tab_width = 4 tab_width = 2
max_line_length = 140
# New line preferences # New line preferences
end_of_line = crlf end_of_line = crlf
insert_final_newline = false insert_final_newline = false

View File

@ -1,31 +1,37 @@
namespace SddpViewer; using System.Net;
namespace SddpViewer;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Rssdp; using Rssdp;
public partial class DiscoveredDeviceViewModel : ObservableObject public partial class DiscoveredDeviceViewModel : ObservableObject
{ {
private readonly DiscoveredSsdpDevice _device; private readonly DiscoveredSsdpDevice _device;
private SsdpDevice? _ssdpDevice;
public DiscoveredDeviceViewModel(DiscoveredSsdpDevice device) public DiscoveredDeviceViewModel(DiscoveredSsdpDevice device)
{ {
_device = device; _device = device;
ResponseHeader = GetResponseHeader(); ResponseHeader = GetResponseHeader();
DiscoveredAt = DateTime.Now;
IpAddress = EvaluateIpAddress(device);
MacAddress = EvaluateMacAddress();
HostName = EvaluateHostName();
} }
private string GetResponseHeader() public string HostName { get; set; }
{
return string.Join(
"," + Environment.NewLine,
_device.ResponseHeaders.Select(x => $"{{{x.Key} : {string.Join(";", x.Value)}}}")
);
}
public string ResponseHeader { get; } public string MacAddress { get; set; }
public string IpAddress { get; set; }
public DateTime DiscoveredAt { get; }
[ObservableProperty]
public partial string ResponseHeader { get; set; }
/// <summary> /// <summary>
/// Sets or returns the type of notification, being either a uuid, device type, service type or upnp:rootdevice. /// Sets or returns the type of notification, being either a uuid, device type, service type or upnp:rootdevice.
@ -53,23 +59,48 @@ public partial class DiscoveredDeviceViewModel : ObservableObject
public DateTimeOffset AsAt => _device.AsAt; public DateTimeOffset AsAt => _device.AsAt;
[ObservableProperty] [ObservableProperty]
private string _friendlyName = ""; public partial string FriendlyName { get; set; } = "";
[ObservableProperty] [ObservableProperty]
private SsdpDeviceIcon? _icon; public partial SsdpDeviceIcon? Icon { get; set; }
[ObservableProperty] [ObservableProperty]
private Uri? _presentationUrl; public partial Uri? PresentationUrl { get; set; }
[ObservableProperty] [ObservableProperty]
private string _modelNumber = ""; public partial string ModelNumber { get; set; } = "";
[ObservableProperty]
public partial string Version { get; set; } = "";
public async Task GetFurtherInformationAsync() public async Task GetFurtherInformationAsync()
{ {
var ssdpDevice = await _device.GetDeviceInfo().ConfigureAwait(false); _ssdpDevice = await _device.GetDeviceInfo().ConfigureAwait(false);
FriendlyName = ssdpDevice.FriendlyName; FriendlyName = _ssdpDevice.ModelDescription;
Icon = ssdpDevice.Icons.MinBy(x => x.Height); Icon = _ssdpDevice.Icons.MinBy(x => x.Height);
PresentationUrl = ssdpDevice.PresentationUrl; PresentationUrl = _ssdpDevice.PresentationUrl;
ModelNumber = ssdpDevice.ModelNumber; ModelNumber = _ssdpDevice.ModelNumber;
Version = _ssdpDevice.SerialNumber?.Split(',').Last() ?? new Version().ToString();
HttpClient client = new HttpClient();
var response = await client.GetAsync(_ssdpDevice.ModelUrl).ConfigureAwait(false);
ResponseHeader = await response.Content.ReadAsStringAsync();
}
private string EvaluateHostName() => Dns.GetHostEntry(IpAddress).HostName;
private string GetResponseHeader() =>
string.Join("," + Environment.NewLine, _device.ResponseHeaders.Select(x => $"{{{x.Key} : {string.Join(";", x.Value)}}}"));
private string EvaluateIpAddress(DiscoveredSsdpDevice device) => device.DescriptionLocation.Host;
private string EvaluateMacAddress()
{
var lookupResult = ArpLookup.Arp.Lookup(IPAddress.Parse(IpAddress));
return lookupResult is null ? "Unknown" : string.Join(":", lookupResult.GetAddressBytes().Select(b => $"{b:x2}"));
}
public override bool Equals(object? obj)
{
return obj is DiscoveredDeviceViewModel viewModel && Equals(viewModel.Usn, Usn);
} }
} }

View File

@ -14,7 +14,7 @@
<Window.DataContext> <Window.DataContext>
<sddpViewer:MainWindowViewModel /> <sddpViewer:MainWindowViewModel />
</Window.DataContext> </Window.DataContext>
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto, *"> <Grid ColumnDefinitions="*,*" RowDefinitions="180, *">
<StackPanel Margin="10" Spacing="5"> <StackPanel Margin="10" Spacing="5">
<TextBlock Text="Device IP Address" /> <TextBlock Text="Device IP Address" />
<ComboBox <ComboBox
@ -24,25 +24,36 @@
SelectedItem="{Binding SelectedNetworkAdapter}" /> SelectedItem="{Binding SelectedNetworkAdapter}" />
<TextBlock Text="Notification filter" /> <TextBlock Text="Notification filter" />
<TextBox Text="{Binding NotificationFilter}" /> <TextBox Text="{Binding NotificationFilter}" />
<StackPanel Orientation="Horizontal" Spacing="5">
<Button Command="{Binding StartListeningCommand}" Content="Listen for devices" />
<Button Command="{Binding SearchDevicesNowCommand}" Content="Search now" /> <Button Command="{Binding SearchDevicesNowCommand}" Content="Search now" />
<Button Command="{Binding ResearchCommand}" Content="Search more" />
</StackPanel> </StackPanel>
</StackPanel>
<Grid Grid.Row="0" Grid.Column="1">
<TextBox
IsReadOnly="True"
Text="{ReflectionBinding SelectedItem.ResponseHeader,
ElementName=DevicesDataGrid}"
TextWrapping="Wrap" />
</Grid>
<DataGrid <DataGrid
Grid.Row="1" Grid.ColumnSpan="2" x:Name="DevicesDataGrid"
Grid.Row="1"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="2"
AutoGenerateColumns="False" AutoGenerateColumns="False"
IsReadOnly="True" IsReadOnly="True"
ItemsSource="{Binding SddpDevices}"> ItemsSource="{Binding SddpDevices}">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Usn}" Header="Usn" />
<DataGridTextColumn Binding="{Binding FriendlyName}" Header="Name" /> <DataGridTextColumn Binding="{Binding FriendlyName}" Header="Name" />
<DataGridTextColumn Binding="{Binding Usn}" Header="Usn" />
<DataGridTextColumn Binding="{Binding IpAddress}" Header="Ip address" />
<DataGridTextColumn Binding="{Binding HostName}" Header="Hostname" />
<DataGridTextColumn Binding="{Binding MacAddress}" Header="Mac address" />
<DataGridTextColumn Binding="{Binding Version}" Header="Version" />
<DataGridTextColumn Binding="{Binding DiscoveredAt}" Header="Discovered at" />
</DataGrid.Columns> </DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate DataType="sddpViewer:DiscoveredDeviceViewModel">
<StackPanel>
<TextBlock Text="{Binding ResponseHeader}" />
</StackPanel>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid> </DataGrid>
</Grid> </Grid>
</Window> </Window>

View File

@ -1,13 +1,12 @@
namespace SddpViewer; using System.Collections.Specialized;
using Avalonia.Threading; namespace SddpViewer;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Rssdp; using Rssdp;
public sealed partial class MainWindowViewModel : ObservableObject, IDisposable public sealed partial class MainWindowViewModel : ObservableObject, IDisposable
@ -16,19 +15,37 @@ public sealed partial class MainWindowViewModel : ObservableObject, IDisposable
{ {
NetworkAdapters = NetworkAdapter.GetAvailableNetworkAdapter().ToArray(); NetworkAdapters = NetworkAdapter.GetAvailableNetworkAdapter().ToArray();
SelectedNetworkAdapter = NetworkAdapters.FirstOrDefault(); SelectedNetworkAdapter = NetworkAdapters.FirstOrDefault();
SddpDevices = new ObservableCollection<DiscoveredDeviceViewModel>();
SddpDevices.CollectionChanged += SddpDevices_OnCollectionChanged;
}
private static async void SddpDevices_OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
try
{
foreach (object eNewItem in e.NewItems ?? Array.Empty<object>())
{
if (eNewItem is DiscoveredDeviceViewModel discoveredDeviceViewModel)
{
await discoveredDeviceViewModel.GetFurtherInformationAsync();
}
}
}
catch (Exception ex)
{
throw; // TODO handle exception
}
} }
private void LocatorOnDeviceUnavailable(object? sender, DeviceUnavailableEventArgs e) private void LocatorOnDeviceUnavailable(object? sender, DeviceUnavailableEventArgs e)
{ {
var existingDevice = SddpDevices.FirstOrDefault( var existingDevice = SddpDevices.FirstOrDefault(x => string.Equals(x.Usn, e.DiscoveredDevice.Usn, StringComparison.Ordinal));
x => string.Equals(x.Usn, e.DiscoveredDevice.Usn, StringComparison.Ordinal)
);
if (existingDevice is null) if (existingDevice is null)
return; return;
Dispatcher.UIThread.Invoke(() => SddpDevices.Remove(existingDevice)); Dispatcher.UIThread.Invoke(() => SddpDevices.Remove(existingDevice));
} }
private async void LocatorOnDeviceAvailable(object? sender, DeviceAvailableEventArgs e) private void LocatorOnDeviceAvailable(object? sender, DeviceAvailableEventArgs e)
{ {
if (!e.IsNewlyDiscovered) if (!e.IsNewlyDiscovered)
{ {
@ -36,8 +53,8 @@ public sealed partial class MainWindowViewModel : ObservableObject, IDisposable
} }
var discoveredDeviceViewModel = new DiscoveredDeviceViewModel(e.DiscoveredDevice); var discoveredDeviceViewModel = new DiscoveredDeviceViewModel(e.DiscoveredDevice);
if (!SddpDevices.Contains(discoveredDeviceViewModel))
Dispatcher.UIThread.Invoke(() => SddpDevices.Add(discoveredDeviceViewModel)); Dispatcher.UIThread.Invoke(() => SddpDevices.Add(discoveredDeviceViewModel));
await discoveredDeviceViewModel.GetFurtherInformationAsync();
} }
[ObservableProperty] [ObservableProperty]
@ -54,7 +71,7 @@ public sealed partial class MainWindowViewModel : ObservableObject, IDisposable
private SsdpDeviceLocator? _locator; private SsdpDeviceLocator? _locator;
public ObservableCollection<DiscoveredDeviceViewModel> SddpDevices { get; } = new(); public ObservableCollection<DiscoveredDeviceViewModel> SddpDevices { get; }
[RelayCommand] [RelayCommand]
private async Task SearchDevicesNowAsync() private async Task SearchDevicesNowAsync()
@ -66,16 +83,43 @@ public sealed partial class MainWindowViewModel : ObservableObject, IDisposable
_locator.DeviceAvailable -= LocatorOnDeviceAvailable; _locator.DeviceAvailable -= LocatorOnDeviceAvailable;
_locator.DeviceUnavailable -= LocatorOnDeviceUnavailable; _locator.DeviceUnavailable -= LocatorOnDeviceUnavailable;
} }
await StartListening(async locator =>
{
foreach (var discoveredSsdpDevice in await locator.SearchAsync())
{
var discoveredDeviceViewModel = new DiscoveredDeviceViewModel(discoveredSsdpDevice);
Dispatcher.UIThread.Invoke(() => SddpDevices.Add(discoveredDeviceViewModel));
}
});
}
[RelayCommand]
private async Task ResearchAsync()
{
if (_locator is not null)
{
await _locator.SearchAsync();
}
}
[RelayCommand]
private async Task StartListening(Func<SsdpDeviceLocator, Task>? action = null)
{
_locator = new SsdpDeviceLocator(SelectedNetworkAdapter?.IpAddress.ToString()); _locator = new SsdpDeviceLocator(SelectedNetworkAdapter?.IpAddress.ToString());
if (!string.IsNullOrWhiteSpace(NotificationFilter)) if (!string.IsNullOrWhiteSpace(NotificationFilter))
{ {
_locator.NotificationFilter = NotificationFilter; _locator.NotificationFilter = NotificationFilter;
} }
if (action is not null)
{
await action.Invoke(_locator);
}
_locator.DeviceAvailable += LocatorOnDeviceAvailable; _locator.DeviceAvailable += LocatorOnDeviceAvailable;
_locator.DeviceUnavailable += LocatorOnDeviceUnavailable; _locator.DeviceUnavailable += LocatorOnDeviceUnavailable;
_locator.StartListeningForNotifications(); _locator.StartListeningForNotifications();
await _locator.SearchAsync();
} }
public void Dispose() public void Dispose()

View File

@ -17,9 +17,7 @@ public record NetworkAdapter(string Name, IPAddress IpAddress)
if (nic.NetworkInterfaceType == NetworkInterfaceType.Loopback) if (nic.NetworkInterfaceType == NetworkInterfaceType.Loopback)
continue; continue;
var physicalAddress = nic.GetIPProperties() var physicalAddress = nic.GetIPProperties()
.UnicastAddresses.FirstOrDefault( .UnicastAddresses.FirstOrDefault(x => x.Address.AddressFamily == AddressFamily.InterNetwork);
x => x.Address.AddressFamily == AddressFamily.InterNetwork
);
if (physicalAddress is not null) if (physicalAddress is not null)
{ {
yield return new NetworkAdapter(nic.Name, physicalAddress.Address); yield return new NetworkAdapter(nic.Name, physicalAddress.Address);

View File

@ -6,10 +6,8 @@ internal static class Program
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break. // yet and stuff might break.
[STAThread] [STAThread]
public static void Main(string[] args) => public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer. // Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp() => public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>().UsePlatformDetect().WithInterFont().LogToTrace();
AppBuilder.Configure<App>().UsePlatformDetect().WithInterFont().LogToTrace();
} }

View File

@ -7,17 +7,23 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<LangVersion>preview</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.5" /> <PackageReference Include="ArpLookup" Version="2.0.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.5" /> <PackageReference Include="Avalonia" Version="11.2.5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.5" /> <PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.5" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.5" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.5" /> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.5" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="rssdp" Version="4.0.4" /> <PackageReference Include="rssdp" Version="4.0.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>