Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions Daqifi.Desktop.Test/ViewModels/DaqifiViewModelLoggingStateTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System.ComponentModel;
using Daqifi.Desktop.Device;
using Daqifi.Desktop.DialogService;
using Daqifi.Desktop.ViewModels;
using Moq;

namespace Daqifi.Desktop.Test.ViewModels;

[TestClass]
public class DaqifiViewModelLoggingStateTests
{
[TestMethod]
public void IsLogging_ReturnsTrue_WhenConnectedDeviceReportsSdCardLogging()
{
var viewModel = CreateViewModel();
var device = new Mock<IStreamingDevice>();
device.SetupGet(d => d.IsLoggingToSdCard).Returns(true);

viewModel.ConnectedDevices.Add(device.Object);

Assert.IsTrue(viewModel.IsLogging,
"IsLogging should reflect device-reported SD logging even when the toggle was never flipped.");
}

[TestMethod]
public void IsSdCardLoggingActive_FollowsDevicePropertyChanged()
{
var viewModel = CreateViewModel();
var device = new Mock<IStreamingDevice>();
var isLoggingToSd = false;
device.SetupGet(d => d.IsLoggingToSdCard).Returns(() => isLoggingToSd);

viewModel.ConnectedDevices.Add(device.Object);
Assert.IsFalse(viewModel.IsSdCardLoggingActive);

var raisedProperties = new List<string?>();
viewModel.PropertyChanged += (_, args) => raisedProperties.Add(args.PropertyName);

isLoggingToSd = true;
device.Raise(d => d.PropertyChanged += null,
new PropertyChangedEventArgs(nameof(IStreamingDevice.IsLoggingToSdCard)));

Assert.IsTrue(viewModel.IsSdCardLoggingActive,
"IsSdCardLoggingActive should turn true once the device raises PropertyChanged for IsLoggingToSdCard.");
CollectionAssert.Contains(raisedProperties, nameof(DaqifiViewModel.IsSdCardLoggingActive));
CollectionAssert.Contains(raisedProperties, nameof(DaqifiViewModel.IsLogging));
}

[TestMethod]
public void SdLoggingElapsed_ResetsToZero_WhenSdLoggingBegins()
{
var viewModel = CreateViewModel();
var device = new Mock<IStreamingDevice>();
var isLoggingToSd = false;
device.SetupGet(d => d.IsLoggingToSdCard).Returns(() => isLoggingToSd);

viewModel.ConnectedDevices.Add(device.Object);

isLoggingToSd = true;
device.Raise(d => d.PropertyChanged += null,
new PropertyChangedEventArgs(nameof(IStreamingDevice.IsLoggingToSdCard)));

Assert.AreEqual("00:00:00", viewModel.SdLoggingElapsed,
"Elapsed time should reset to 00:00:00 the moment SD logging begins.");
}

[TestMethod]
public void RemovingTheLastLoggingDevice_ClearsSdCardLoggingActive()
{
var viewModel = CreateViewModel();
var device = new Mock<IStreamingDevice>();
device.SetupGet(d => d.IsLoggingToSdCard).Returns(true);

viewModel.ConnectedDevices.Add(device.Object);
Assert.IsTrue(viewModel.IsSdCardLoggingActive);

viewModel.ConnectedDevices.Remove(device.Object);

Assert.IsFalse(viewModel.IsSdCardLoggingActive,
"Once no devices are reporting SD logging, the active flag should clear.");
}

[TestMethod]
public void DevicePropertyChangedForUnrelatedProperty_DoesNotRaiseLoggingState()
{
var viewModel = CreateViewModel();
var device = new Mock<IStreamingDevice>();
device.SetupGet(d => d.IsLoggingToSdCard).Returns(false);

viewModel.ConnectedDevices.Add(device.Object);

var raised = new List<string?>();
viewModel.PropertyChanged += (_, args) => raised.Add(args.PropertyName);

device.Raise(d => d.PropertyChanged += null,
new PropertyChangedEventArgs(nameof(IStreamingDevice.MacAddress)));

CollectionAssert.DoesNotContain(raised, nameof(DaqifiViewModel.IsLogging));
CollectionAssert.DoesNotContain(raised, nameof(DaqifiViewModel.IsSdCardLoggingActive));
}

[TestMethod]
public void Clear_UnsubscribesPropertyChanged_AndDoesNotLeak()
{
// ObservableCollection<T>.Clear() raises a Reset event with OldItems == null;
// a naive subscribe/unsubscribe handler would leak the original subscription.
var viewModel = CreateViewModel();
var device = new Mock<IStreamingDevice>();
device.SetupGet(d => d.IsLoggingToSdCard).Returns(false);

viewModel.ConnectedDevices.Add(device.Object);
viewModel.ConnectedDevices.Clear();

var raised = new List<string?>();
viewModel.PropertyChanged += (_, args) => raised.Add(args.PropertyName);

// After Clear(), the device should be unsubscribed — its PropertyChanged
// events must not bubble up to the view model.
device.Raise(d => d.PropertyChanged += null,
new PropertyChangedEventArgs(nameof(IStreamingDevice.IsLoggingToSdCard)));

CollectionAssert.DoesNotContain(raised, nameof(DaqifiViewModel.IsLogging));
CollectionAssert.DoesNotContain(raised, nameof(DaqifiViewModel.IsSdCardLoggingActive));
}

private static DaqifiViewModel CreateViewModel()
{
var dialogService = new Mock<IDialogService>();
return new DaqifiViewModel(dialogService.Object);
}
}
128 changes: 111 additions & 17 deletions Daqifi.Desktop/View/Prototype/LiveGraphPane.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,19 +188,40 @@
Foreground="{StaticResource TextTertiary}"
VerticalAlignment="Center"/>

<!-- Sample rate -->
<TextBlock Text="RATE"
FontSize="9"
FontWeight="SemiBold"
Foreground="{StaticResource TextTertiary}"
VerticalAlignment="Center"
Margin="0,1,5,0"/>
<TextBlock FontFamily="Consolas"
FontSize="11"
Foreground="{StaticResource TextSecondary}"
VerticalAlignment="Center">
<Run Text="{Binding SelectedStreamingFrequency}"/><Run Text=" Hz"/>
</TextBlock>
<!-- Sample rate (stream mode) -->
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center"
Visibility="{Binding IsSdCardLoggingActive, Converter={StaticResource InvertedBoolToVis}}">
<TextBlock Text="RATE"
FontSize="9"
FontWeight="SemiBold"
Foreground="{StaticResource TextTertiary}"
VerticalAlignment="Center"
Margin="0,1,5,0"/>
<TextBlock FontFamily="Consolas"
FontSize="11"
Foreground="{StaticResource TextSecondary}"
VerticalAlignment="Center">
<Run Text="{Binding SelectedStreamingFrequency}"/><Run Text=" Hz"/>
</TextBlock>
</StackPanel>

<!-- Elapsed time (SD-card mode) -->
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center"
Visibility="{Binding IsSdCardLoggingActive, Converter={StaticResource BoolToVis}}">
<TextBlock Text="ELAPSED"
FontSize="9"
FontWeight="SemiBold"
Foreground="{StaticResource TextTertiary}"
VerticalAlignment="Center"
Margin="0,1,5,0"/>
<TextBlock Text="{Binding SdLoggingElapsed}"
FontFamily="Consolas"
FontSize="11"
Foreground="{StaticResource TextSecondary}"
VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>

<!-- Plot toolbar pill -->
Expand Down Expand Up @@ -328,7 +349,8 @@
<Grid Grid.Column="0">
<oxy:PlotView x:Name="DataLog"
Model="{Binding Plotter.PlotModel}"
TabIndex="0">
TabIndex="0"
Visibility="{Binding IsSdCardLoggingActive, Converter={StaticResource InvertedBoolToVis}}">
<oxy:PlotView.DefaultTrackerTemplate>
<ControlTemplate>
<oxy:TrackerControl Position="{Binding Position}"
Expand All @@ -349,7 +371,9 @@
</oxy:PlotView.DefaultTrackerTemplate>
</oxy:PlotView>

<!-- Empty-state overlay: shown when no channels are streaming -->
<!-- Empty-state overlay: shown when no channels are streaming
AND we're not in SD-card logging mode (otherwise the
"Logging to Device" panel below covers this state). -->
<Border IsHitTestVisible="False"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Expand All @@ -362,9 +386,13 @@
<Style TargetType="Border">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ActiveInputChannels.Count}" Value="0">
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ActiveInputChannels.Count}" Value="0"/>
<Condition Binding="{Binding IsSdCardLoggingActive}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
Expand All @@ -386,6 +414,72 @@
Margin="0,4,0,0"/>
</StackPanel>
</Border>

<!-- SD-card logging overlay: replaces the (necessarily empty)
plot when the device is recording to its own SD card. -->
<Border IsHitTestVisible="False"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Padding="32,24"
MinWidth="380"
Background="{StaticResource SurfaceRaised}"
BorderBrush="{StaticResource BorderDim}"
BorderThickness="1"
CornerRadius="6"
Visibility="{Binding IsSdCardLoggingActive, Converter={StaticResource BoolToVis}}">
<StackPanel HorizontalAlignment="Center">
<Grid HorizontalAlignment="Center" Margin="0,0,0,14">
<Ellipse Width="44" Height="44"
Fill="{StaticResource StatusGreen}"
Opacity="0.15"/>
<iconPacks:PackIconMaterial Kind="RecordRec"
Width="28" Height="28"
Foreground="{StaticResource StatusGreen}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>

<TextBlock Text="Logging to Device"
FontSize="15"
FontWeight="SemiBold"
Foreground="{StaticResource TextPrimary}"
HorizontalAlignment="Center"/>

<TextBlock Text="{Binding SdLoggingElapsed}"
FontFamily="Consolas"
FontSize="22"
Foreground="{StaticResource StatusGreen}"
HorizontalAlignment="Center"
Margin="0,10,0,0"/>

<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Margin="0,10,0,0">
<TextBlock Text="FORMAT"
FontSize="9"
FontWeight="SemiBold"
Foreground="{StaticResource TextTertiary}"
VerticalAlignment="Center"
Margin="0,1,5,0"/>
<TextBlock Text="{Binding SelectedSdCardLogFormat}"
FontFamily="Consolas"
FontSize="11"
Foreground="{StaticResource TextSecondary}"
VerticalAlignment="Center"/>
</StackPanel>

<TextBlock Text="Live preview is unavailable in this mode."
FontSize="11"
Foreground="{StaticResource TextSecondary}"
HorizontalAlignment="Center"
Margin="0,16,0,0"/>
<TextBlock Text="Recorded data will appear in Logged Data after retrieval."
FontSize="11"
Foreground="{StaticResource TextTertiary}"
HorizontalAlignment="Center"
Margin="0,2,0,0"/>
</StackPanel>
</Border>
</Grid>

<!-- ── Channel legend sidebar ── -->
Expand Down
Loading
Loading