2 Commits

Author SHA1 Message Date
54cea349c0 Add version number display to main window 2025-11-18 22:25:50 -08:00
a1d03fec2a Release v1.0.3: Enhanced NDEF reading and UX improvements
Major Improvements:
- Implement exclusive card access with retry logic and exponential backoff
- Fix Type 4 NDEF length parsing (now correctly reads NTAG424 and similar chips)
- Add enhanced card type detection (Type 2 vs Type 4)
- Implement chunked reading for large NDEF messages
- Add proper TLV parsing for Type 2 tags

Bug Fixes:
- Fix WPF window lifecycle issue (visual tree error on reopen)
- Fix NDEF length parsing incorrectly detecting extended format
- Correct data offset for Type 4 tag reading

New Features:
- Multi-line log selection and copy to clipboard
- Context menu with Copy Selected Lines, Copy All, Select All
- Runtime version roll-forward support (.NET 8.0.x compatibility)

Technical Details:
- Type 4 tags now use correct 2-byte NLEN field per NFC Forum spec
- Removed incorrect 3-byte extended length detection
- Window now hides instead of closing for proper tray app behavior
- Connection attempts exclusive access first, falls back to shared mode
- Status timeout increased from 0ms to 1000ms for better card detection
2025-11-18 22:20:13 -08:00
6 changed files with 602 additions and 36 deletions

View File

@@ -3,7 +3,7 @@
<Product Id="*" <Product Id="*"
Name="NFC Actions" Name="NFC Actions"
Language="1033" Language="1033"
Version="1.0.2.0" Version="1.0.3.0"
Manufacturer="Dangerous Things" Manufacturer="Dangerous Things"
UpgradeCode="A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D"> UpgradeCode="A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D">

View File

@@ -24,6 +24,7 @@ public partial class App : Application
private ActionService? _actionService; private ActionService? _actionService;
private LogService? _logService; private LogService? _logService;
private MainViewModel? _viewModel; private MainViewModel? _viewModel;
private System.ComponentModel.CancelEventHandler? _windowClosingHandler;
public App() public App()
{ {
@@ -121,16 +122,44 @@ public partial class App : Application
private void ShowMainWindow() private void ShowMainWindow()
{ {
if (_mainWindow != null) // If window was closed, recreate it
if (_mainWindow == null || !_mainWindow.IsLoaded)
{ {
_mainWindow.Show(); if (_viewModel == null) return;
_mainWindow.WindowState = WindowState.Normal;
_mainWindow.Activate(); _mainWindow = new MainWindow(_viewModel);
// Handle window closing - hide instead of close
_windowClosingHandler = (s, e) =>
{
e.Cancel = true;
if (_mainWindow != null)
{
_mainWindow.Hide();
}
};
_mainWindow.Closing += _windowClosingHandler;
} }
_mainWindow.Show();
_mainWindow.WindowState = WindowState.Normal;
_mainWindow.Activate();
} }
private void ExitApplication() private void ExitApplication()
{ {
// Properly close the main window if it exists
if (_mainWindow != null)
{
// Remove the cancel handler so the window can actually close
if (_windowClosingHandler != null)
{
_mainWindow.Closing -= _windowClosingHandler;
}
_mainWindow.Close();
}
_notifyIcon?.Dispose(); _notifyIcon?.Dispose();
_customIcon?.Dispose(); _customIcon?.Dispose();
_cardReaderService?.Dispose(); _cardReaderService?.Dispose();

View File

@@ -19,11 +19,16 @@
<!-- Header --> <!-- Header -->
<Grid Grid.Row="0" Margin="0,0,0,15"> <Grid Grid.Row="0" Margin="0,0,0,15">
<TextBlock Text="NFC Actions Configuration" <StackPanel VerticalAlignment="Center" HorizontalAlignment="Left">
FontSize="20" <TextBlock Text="NFC Actions Configuration"
FontWeight="Bold" FontSize="20"
VerticalAlignment="Center" FontWeight="Bold"/>
HorizontalAlignment="Left"/> <TextBlock x:Name="VersionText"
Text="v1.0.3"
FontSize="10"
Foreground="Gray"
Margin="0,2,0,0"/>
</StackPanel>
<Image Source="Resources/logo.png" <Image Source="Resources/logo.png"
Height="60" Height="60"
HorizontalAlignment="Right" HorizontalAlignment="Right"
@@ -99,10 +104,20 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<ListBox Grid.Row="0" <ListBox Grid.Row="0"
x:Name="LogListBox"
ItemsSource="{Binding LogEntries}" ItemsSource="{Binding LogEntries}"
Margin="0,0,0,5" Margin="0,0,0,5"
ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"> ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectionMode="Extended">
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Copy Selected Lines" Click="CopySelectedLogs_Click"/>
<MenuItem Header="Copy All" Click="CopyAllLogs_Click"/>
<Separator/>
<MenuItem Header="Select All" Click="SelectAllLogs_Click"/>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding FormattedMessage}" <TextBlock Text="{Binding FormattedMessage}"

View File

@@ -1,7 +1,11 @@
using System.ComponentModel; using System;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using NfcActions.Services;
using NfcActions.ViewModels; using NfcActions.ViewModels;
namespace NfcActions; namespace NfcActions;
@@ -15,6 +19,13 @@ public partial class MainWindow : Window
{ {
InitializeComponent(); InitializeComponent();
DataContext = viewModel; DataContext = viewModel;
// Set version number from assembly
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
if (version != null)
{
VersionText.Text = $"v{version.Major}.{version.Minor}.{version.Build}";
}
} }
private void Window_Closing(object? sender, CancelEventArgs e) private void Window_Closing(object? sender, CancelEventArgs e)
@@ -39,4 +50,84 @@ public partial class MainWindow : Window
// Silently fail if browser can't be opened // Silently fail if browser can't be opened
} }
} }
private void CopySelectedLogs_Click(object sender, RoutedEventArgs e)
{
try
{
var selectedItems = LogListBox.SelectedItems.Cast<LogEntry>().ToList();
if (selectedItems.Count == 0)
{
MessageBox.Show("No log entries selected. Please select one or more lines first.",
"No Selection",
MessageBoxButton.OK,
MessageBoxImage.Information);
return;
}
var logText = new StringBuilder();
foreach (var entry in selectedItems)
{
logText.AppendLine(entry.FormattedMessage);
}
Clipboard.SetText(logText.ToString());
MessageBox.Show($"Copied {selectedItems.Count} log {(selectedItems.Count == 1 ? "entry" : "entries")} to clipboard.",
"Copied",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to copy to clipboard: {ex.Message}",
"Error",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
}
private void CopyAllLogs_Click(object sender, RoutedEventArgs e)
{
try
{
if (DataContext is MainViewModel viewModel)
{
var allEntries = viewModel.LogEntries.ToList();
if (allEntries.Count == 0)
{
MessageBox.Show("No log entries available.",
"Empty Log",
MessageBoxButton.OK,
MessageBoxImage.Information);
return;
}
var logText = new StringBuilder();
foreach (var entry in allEntries)
{
logText.AppendLine(entry.FormattedMessage);
}
Clipboard.SetText(logText.ToString());
MessageBox.Show($"Copied all {allEntries.Count} log entries to clipboard.",
"Copied",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to copy to clipboard: {ex.Message}",
"Error",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
}
private void SelectAllLogs_Click(object sender, RoutedEventArgs e)
{
LogListBox.SelectAll();
}
} }

View File

@@ -7,13 +7,14 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>Resources\icon.ico</ApplicationIcon> <ApplicationIcon>Resources\icon.ico</ApplicationIcon>
<Version>1.0.2</Version> <Version>1.0.3</Version>
<AssemblyVersion>1.0.2.0</AssemblyVersion> <AssemblyVersion>1.0.3.0</AssemblyVersion>
<FileVersion>1.0.2.0</FileVersion> <FileVersion>1.0.3.0</FileVersion>
<Company>Dangerous Things</Company> <Company>Dangerous Things</Company>
<Product>NFC Actions</Product> <Product>NFC Actions</Product>
<Description>NFC card reader monitoring and action automation</Description> <Description>NFC card reader monitoring and action automation</Description>
<Copyright>Copyright © 2025 Dangerous Things</Copyright> <Copyright>Copyright © 2025 Dangerous Things</Copyright>
<RollForward>LatestMinor</RollForward>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'"> <PropertyGroup Condition="'$(Configuration)'=='Release'">

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using PCSC; using PCSC;
using PCSC.Exceptions;
namespace NfcActions.Services; namespace NfcActions.Services;
@@ -16,6 +17,9 @@ public class CardReaderService : IDisposable
private readonly LogService? _logService; private readonly LogService? _logService;
private const int POLL_INTERVAL_MS = 500; private const int POLL_INTERVAL_MS = 500;
private const int MAX_RETRY_ATTEMPTS = 3;
private const int INITIAL_RETRY_DELAY_MS = 100;
private const int STATUS_TIMEOUT_MS = 1000;
public event EventHandler<ReaderEventArgs>? ReaderAdded; public event EventHandler<ReaderEventArgs>? ReaderAdded;
public event EventHandler<ReaderEventArgs>? ReaderRemoved; public event EventHandler<ReaderEventArgs>? ReaderRemoved;
@@ -150,7 +154,7 @@ public class CardReaderService : IDisposable
} }
}; };
var result = context.GetStatusChange(0, readerStates); var result = context.GetStatusChange(STATUS_TIMEOUT_MS, readerStates);
if (result == SCardError.Success && readerStates.Length > 0) if (result == SCardError.Success && readerStates.Length > 0)
{ {
@@ -215,6 +219,112 @@ public class CardReaderService : IDisposable
} }
} }
private ICardReader? ConnectReaderWithRetry(ISCardContext context, string readerName)
{
int retryDelay = INITIAL_RETRY_DELAY_MS;
for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++)
{
try
{
_logService?.Debug($"Attempting to connect to reader (attempt {attempt}/{MAX_RETRY_ATTEMPTS})...");
// Try exclusive mode first
try
{
var reader = context.ConnectReader(readerName, SCardShareMode.Exclusive, SCardProtocol.Any);
_logService?.Debug($"Successfully connected with exclusive access on attempt {attempt}");
return reader;
}
catch (PCSCException ex) when (ex.SCardError == SCardError.SharingViolation && attempt < MAX_RETRY_ATTEMPTS)
{
_logService?.Debug($"Exclusive access denied (sharing violation), retrying in {retryDelay}ms...");
Thread.Sleep(retryDelay);
retryDelay *= 2; // Exponential backoff
continue;
}
}
catch (Exception ex)
{
_logService?.Warning($"Connection attempt {attempt} failed: {ex.Message}");
if (attempt == MAX_RETRY_ATTEMPTS)
{
// On final attempt, try shared mode as fallback
try
{
_logService?.Warning("All exclusive access attempts failed, falling back to shared mode...");
var reader = context.ConnectReader(readerName, SCardShareMode.Shared, SCardProtocol.Any);
_logService?.Warning("Connected with shared access (fallback)");
return reader;
}
catch (Exception fallbackEx)
{
_logService?.Error($"Failed to connect even with shared mode: {fallbackEx.Message}");
throw;
}
}
Thread.Sleep(retryDelay);
retryDelay *= 2; // Exponential backoff
}
}
return null;
}
private enum NfcCardType
{
Unknown,
Type2, // NTAG, MIFARE Ultralight
Type4 // ISO-DEP, DESFire
}
private NfcCardType DetectCardType(ICardReader reader, byte[]? atr)
{
try
{
// Check ATR for Type 4 indicators
if (atr != null && atr.Length > 0)
{
// Check for ISO 14443-4 support in historical bytes
_logService?.Debug($"Analyzing ATR for card type detection: {BitConverter.ToString(atr).Replace("-", " ")}");
// Type 4 cards typically have longer ATRs with historical bytes
if (atr.Length > 10)
{
_logService?.Debug("Long ATR detected, likely Type 4 card");
return NfcCardType.Type4;
}
}
// Try to detect card type using protocol
if (reader.Protocol == SCardProtocol.T0 || reader.Protocol == SCardProtocol.T1)
{
_logService?.Debug($"T=0/T=1 protocol detected, likely Type 4 card");
// T=0 or T=1 protocols typically indicate Type 4 cards
}
// Try a simple Type 2 read command
var testRead = new byte[] { 0x30, 0x00 }; // READ block 0
var response = TransmitApdu(reader, testRead, "Type 2 detection probe");
if (response != null && response.Length >= 16)
{
_logService?.Debug("Type 2 READ command successful");
return NfcCardType.Type2;
}
// Default to Type 4 for ISO-DEP cards
_logService?.Debug("Defaulting to Type 4 card");
return NfcCardType.Type4;
}
catch (Exception ex)
{
_logService?.Debug($"Card type detection failed: {ex.Message}");
return NfcCardType.Unknown;
}
}
private byte[]? ReadCardData(string readerName) private byte[]? ReadCardData(string readerName)
{ {
try try
@@ -224,7 +334,13 @@ public class CardReaderService : IDisposable
using var context = ContextFactory.Instance.Establish(SCardScope.System); using var context = ContextFactory.Instance.Establish(SCardScope.System);
_logService?.Debug("Connecting to reader..."); _logService?.Debug("Connecting to reader...");
using var reader = context.ConnectReader(readerName, SCardShareMode.Shared, SCardProtocol.Any); using var reader = ConnectReaderWithRetry(context, readerName);
if (reader == null)
{
_logService?.Error("Failed to connect to reader after all retry attempts");
return null;
}
_logService?.Debug($"Connected. Active protocol: {reader.Protocol}"); _logService?.Debug($"Connected. Active protocol: {reader.Protocol}");
@@ -235,32 +351,55 @@ public class CardReaderService : IDisposable
_logService?.Debug($"ATR: {BitConverter.ToString(atr).Replace("-", " ")}"); _logService?.Debug($"ATR: {BitConverter.ToString(atr).Replace("-", " ")}");
} }
// Detect card type
var cardType = DetectCardType(reader, atr);
_logService?.Info($"Detected card type: {cardType}");
byte[]? ndefData = null; byte[]? ndefData = null;
// Strategy 1: Try Type 4 Tag (ISO 14443-4 / ISO-DEP) // Use appropriate strategy based on detected card type
_logService?.Debug("=== Attempting Type 4 Tag NDEF read ==="); switch (cardType)
ndefData = TryReadType4Tag(reader);
if (ndefData != null && ndefData.Length > 0)
{ {
_logService?.Info("Successfully read NDEF data using Type 4 method"); case NfcCardType.Type2:
return ndefData; _logService?.Debug("=== Reading Type 2 Tag ===");
ndefData = TryReadType2TagEnhanced(reader);
if (ndefData != null && ndefData.Length > 0)
{
_logService?.Info("Successfully read NDEF data from Type 2 tag");
return ndefData;
}
// Fallback to Type 4 if Type 2 fails
_logService?.Debug("Type 2 read failed, trying Type 4 as fallback");
ndefData = TryReadType4TagEnhanced(reader);
break;
case NfcCardType.Type4:
_logService?.Debug("=== Reading Type 4 Tag ===");
ndefData = TryReadType4TagEnhanced(reader);
if (ndefData != null && ndefData.Length > 0)
{
_logService?.Info("Successfully read NDEF data from Type 4 tag");
return ndefData;
}
// Fallback to Type 2 if Type 4 fails
_logService?.Debug("Type 4 read failed, trying Type 2 as fallback");
ndefData = TryReadType2TagEnhanced(reader);
break;
default:
// Try both methods if unknown
_logService?.Debug("Unknown card type, trying all methods");
ndefData = TryReadType4TagEnhanced(reader);
if (ndefData == null || ndefData.Length == 0)
{
ndefData = TryReadType2TagEnhanced(reader);
}
break;
} }
// Strategy 2: Try direct NDEF file read
_logService?.Debug("=== Attempting direct NDEF file read ===");
ndefData = TryReadNdefDirect(reader);
if (ndefData != null && ndefData.Length > 0) if (ndefData != null && ndefData.Length > 0)
{ {
_logService?.Info("Successfully read NDEF data using direct method"); _logService?.Info($"Successfully read {ndefData.Length} bytes of NDEF data");
return ndefData;
}
// Strategy 3: Try reading raw tag memory (Type 2)
_logService?.Debug("=== Attempting Type 2 Tag read ===");
ndefData = TryReadType2Tag(reader);
if (ndefData != null && ndefData.Length > 0)
{
_logService?.Info("Successfully read data using Type 2 method");
return ndefData; return ndefData;
} }
@@ -275,6 +414,297 @@ public class CardReaderService : IDisposable
} }
} }
private byte[]? TryReadType4TagEnhanced(ICardReader reader)
{
try
{
// Step 1: Select NDEF Tag Application (AID: D2760000850101)
var selectNdef = new byte[] { 0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00 };
var response = TransmitApdu(reader, selectNdef, "Select NDEF Application");
if (!IsSuccess(response))
{
_logService?.Debug("NDEF application not found");
return null;
}
// Step 2: Select Capability Container (CC) file
var selectCC = new byte[] { 0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03 };
response = TransmitApdu(reader, selectCC, "Select CC File");
if (!IsSuccess(response))
{
_logService?.Debug("Failed to select CC file");
return null;
}
// Step 3: Read CC to get NDEF file info
var readCC = new byte[] { 0x00, 0xB0, 0x00, 0x00, 0x0F };
response = TransmitApdu(reader, readCC, "Read CC");
if (!IsSuccess(response) || response?.Length < 17)
{
_logService?.Debug("Failed to read CC");
return null;
}
// Parse CC to get max NDEF size
var ccData = new byte[15];
if (response != null)
{
Array.Copy(response, ccData, Math.Min(15, response.Length - 2));
}
// CC format: 2 bytes length, 1 byte version, 2 bytes MLe, 2 bytes MLc, then TLV
var maxNdefSize = (ccData[3] << 8) | ccData[4]; // MLe (Maximum Length for data read)
var maxApduSize = (ccData[5] << 8) | ccData[6]; // MLc (Maximum Length for command)
_logService?.Debug($"CC: Max NDEF size = {maxNdefSize}, Max APDU size = {maxApduSize}");
// Step 4: Select NDEF file
var selectNdefFile = new byte[] { 0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04 };
response = TransmitApdu(reader, selectNdefFile, "Select NDEF File");
if (!IsSuccess(response))
{
_logService?.Debug("Failed to select NDEF file");
return null;
}
// Step 5: Read NDEF length (always 2 bytes for Type 4 tags per NFC Forum spec)
var readLength = new byte[] { 0x00, 0xB0, 0x00, 0x00, 0x02 }; // Read 2 bytes
response = TransmitApdu(reader, readLength, "Read NDEF Length");
if (response == null || response.Length < 4)
{
_logService?.Debug("Failed to read NDEF length");
return null;
}
// Type 4 tags use 2-byte NLEN field (big-endian)
int ndefLength = (response[0] << 8) | response[1];
int dataOffset = 2; // NDEF message starts at byte 2
_logService?.Debug($"NDEF length: {ndefLength} bytes (0x{response[0]:X2}{response[1]:X2})");
if (ndefLength == 0)
{
_logService?.Warning("NDEF length is 0 - empty tag");
return null;
}
if (ndefLength > 65535)
{
_logService?.Warning($"NDEF length too large: {ndefLength}");
return null;
}
// Step 6: Read NDEF data in chunks
var allData = new List<byte>();
var offset = dataOffset;
var maxReadSize = Math.Min(maxApduSize > 0 ? maxApduSize : 250, 250); // Use CC info or default to 250
while (allData.Count < ndefLength)
{
var remainingBytes = ndefLength - allData.Count;
var readSize = Math.Min(remainingBytes, maxReadSize);
var readData = new byte[] {
0x00, 0xB0,
(byte)(offset >> 8), (byte)(offset & 0xFF),
(byte)readSize
};
response = TransmitApdu(reader, readData, $"Read NDEF chunk at offset {offset}");
if (!IsSuccess(response))
{
_logService?.Error($"Failed to read NDEF data at offset {offset}");
break;
}
var dataLength = response?.Length - 2 ?? 0;
if (dataLength > 0 && response != null)
{
for (int i = 0; i < dataLength && allData.Count < ndefLength; i++)
{
allData.Add(response[i]);
}
offset += dataLength;
}
else
{
_logService?.Warning("No data received in chunk read");
break;
}
_logService?.Debug($"Read {dataLength} bytes, total: {allData.Count}/{ndefLength}");
}
if (allData.Count > 0)
{
_logService?.Info($"Successfully read {allData.Count} bytes from Type 4 tag");
return allData.ToArray();
}
return null;
}
catch (Exception ex)
{
_logService?.Debug($"Type 4 enhanced read exception: {ex.Message}");
return null;
}
}
private byte[]? TryReadType2TagEnhanced(ICardReader reader)
{
try
{
_logService?.Debug("Starting enhanced Type 2 tag read");
// Step 1: Read first 16 bytes (blocks 0-3) to identify tag
var readHeader = new byte[] { 0x30, 0x00 }; // READ from block 0
var response = TransmitApdu(reader, readHeader, "Read header blocks 0-3");
if (response == null || response.Length < 16)
{
_logService?.Debug("Failed to read header blocks");
return null;
}
// Step 2: Check Capability Container (CC) at block 3 (bytes 12-15)
// CC format: Magic number, Version, Memory size, Read/Write access
if (response.Length >= 16)
{
var cc0 = response[12]; // Should be 0xE1 for NDEF
var cc1 = response[13]; // Version
var cc2 = response[14]; // Memory size
var cc3 = response[15]; // Read/Write access
_logService?.Debug($"CC: {cc0:X2} {cc1:X2} {cc2:X2} {cc3:X2}");
if (cc0 != 0xE1)
{
_logService?.Debug("Not an NDEF formatted tag (CC0 != 0xE1)");
// Continue anyway as some tags might still have NDEF data
}
var memorySize = (cc2 & 0xFF) * 8;
_logService?.Debug($"Tag memory size: {memorySize} bytes");
}
// Step 3: Read NDEF data starting from block 4
var allData = new List<byte>();
byte currentBlock = 4;
int tlvPosition = 0;
bool ndefFound = false;
int ndefLength = 0;
// Read up to 64 blocks (256 bytes) or until terminator
while (currentBlock < 64)
{
var readBlock = new byte[] { 0x30, currentBlock };
response = TransmitApdu(reader, readBlock, $"Read block {currentBlock}");
if (response == null || response.Length < 16)
{
_logService?.Debug($"Failed to read block {currentBlock}");
break;
}
// Process 4 blocks (16 bytes) at a time
for (int i = 0; i < 16 && (currentBlock * 4 + i / 4) < 256; i++)
{
allData.Add(response[i]);
// Parse TLV structure to find NDEF message
if (!ndefFound && allData.Count >= 2)
{
// Check for NDEF TLV (0x03)
if (allData[tlvPosition] == 0x03)
{
ndefFound = true;
_logService?.Debug($"Found NDEF TLV at position {tlvPosition}");
// Parse length
if (allData.Count > tlvPosition + 1)
{
var lengthByte = allData[tlvPosition + 1];
if (lengthByte == 0xFF && allData.Count > tlvPosition + 3)
{
// 3-byte length
ndefLength = (allData[tlvPosition + 2] << 8) | allData[tlvPosition + 3];
tlvPosition += 4; // Skip TLV header
}
else
{
// 1-byte length
ndefLength = lengthByte;
tlvPosition += 2; // Skip TLV header
}
_logService?.Debug($"NDEF length: {ndefLength} bytes");
}
}
else if (allData[tlvPosition] == 0xFE)
{
// Terminator TLV
_logService?.Debug("Found terminator TLV");
break;
}
else if (allData[tlvPosition] == 0x00)
{
// NULL TLV, skip
tlvPosition++;
}
else
{
// Other TLV, skip based on length
if (allData.Count > tlvPosition + 1)
{
var len = allData[tlvPosition + 1];
tlvPosition += 2 + len;
}
}
}
}
// Check if we've read enough NDEF data
if (ndefFound && allData.Count >= tlvPosition + ndefLength)
{
_logService?.Debug("Read complete NDEF message");
break;
}
currentBlock += 4; // Move to next set of 4 blocks
}
// Extract NDEF data if found
if (ndefFound && ndefLength > 0 && allData.Count >= tlvPosition + ndefLength)
{
var ndefData = new byte[ndefLength];
for (int i = 0; i < ndefLength; i++)
{
ndefData[i] = allData[tlvPosition + i];
}
_logService?.Info($"Successfully extracted {ndefLength} bytes of NDEF data from Type 2 tag");
return ndefData;
}
// If no NDEF TLV found but we have data, return raw data
if (allData.Count > 0)
{
_logService?.Warning("No NDEF TLV found, returning raw data");
return allData.ToArray();
}
return null;
}
catch (Exception ex)
{
_logService?.Debug($"Type 2 enhanced read exception: {ex.Message}");
return null;
}
}
private byte[]? TryReadType4Tag(ICardReader reader) private byte[]? TryReadType4Tag(ICardReader reader)
{ {
try try