Public/Read-OpenTag3DTag.ps1

function Read-OpenTag3DTag {
<#
.SYNOPSIS
    Reads an OpenTag3D tag, or a saved tag image, and decodes the spool data.
 
.DESCRIPTION
    Reads the application/opentag3d NDEF record and decodes it per the OpenTag3D spec
    (https://opentag3d.info/spec.html): core fields at 0x00-0x6F and, where present,
    extended fields at 0x70-0xBA.
 
    Reads through PC/SC: winscard on Windows, pcsc-lite on Linux and macOS. Reading a .bin
    image with -Path needs no reader at all.
 
.PARAMETER Path
    Decode a saved tag image instead of reading a physical tag.
 
.PARAMETER ReaderName
    Substring of the PC/SC reader name. Defaults to the first reader matching 'ACR122'.
 
.PARAMETER Raw
    Also return the decoded payload bytes on a PayloadBytes property.
 
.EXAMPLE
    Read-OpenTag3DTag
 
    Reads the tag on the reader and returns the decoded fields.
 
.EXAMPLE
    Read-OpenTag3DTag | Format-List material, color_name, serial, print_temp
 
    Picks individual fields off the returned object.
 
.EXAMPLE
    Read-OpenTag3DTag -Path .\50017-FYG5-NTAG215-Extended-Ndef.bin | Select-Object -ExpandProperty Fields | Format-Table
 
    Decodes a saved image and shows every populated field as a table. Works on Linux too.
#>

    [CmdletBinding(PositionalBinding = $false, DefaultParameterSetName = 'Tag')]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Path')]
        [Alias('FullName','PSPath')]
        [ValidateNotNullOrEmpty()]
        [string]$Path,

        [Parameter(ParameterSetName = 'Tag')]
        [string]$ReaderName,

        [switch]$Raw
    )
    process {
        if ($PSCmdlet.ParameterSetName -eq 'Path') {
            $file = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path)
            if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { throw "Image not found: $file" }
            $bytes = [IO.File]::ReadAllBytes($file)
            Write-Verbose "Decoding $($bytes.Length)-byte image $file"
            $payload = Get-OpenTag3DNdefPayload -UserMemory $bytes
        }
        else {
            $session = Connect-PcscCard -ReaderName $ReaderName
            try {
                Write-Verbose "Reader: $($session.Reader)"
                $actual = Get-NtagType -Session $session
                Write-Verbose "Detected $($actual.TagType) via $($actual.Method)"

                # Read the TLV header first so we only pull as much as the message needs.
                $head = Invoke-PcscApdu -Session $session -Apdu ([byte[]]@(0xFF,0xB0,0x00,0x04,0x10)) -ReceiveLength 32
                if (-not $head.Success) { throw "Could not read page 4 (SW=$($head.SW))." }
                if ($head.Data[0] -ne 0x03) {
                    throw ("No NDEF TLV at page 4 (found 0x{0:X2}). This tag is not NDEF formatted." -f $head.Data[0])
                }
                $need = if ($head.Data[1] -eq 0xFF) { 4 + (([int]$head.Data[2] -shl 8) -bor $head.Data[3]) }
                        else { 2 + $head.Data[1] }
                $need = [Math]::Min($need + 1, $actual.UserSize)     # +1 for the terminator TLV
                Write-Verbose "NDEF message needs $need bytes of user memory"

                $mem = [byte[]]::new($need)
                for ($off = 0; $off -lt $need; $off += 16) {
                    $page  = 4 + ($off / 4)
                    $count = [Math]::Min(16, $need - $off)
                    $r = Invoke-PcscApdu -Session $session -Apdu ([byte[]]@(0xFF,0xB0,0x00,[byte]$page,0x10)) -ReceiveLength 32
                    if (-not $r.Success) { throw "Read failed at page $page (SW=$($r.SW))." }
                    [Array]::Copy($r.Data, 0, $mem, $off, $count)
                }
                $payload = Get-OpenTag3DNdefPayload -UserMemory $mem
            }
            finally { Disconnect-PcscCard -Session $session }
        }

        $result = ConvertFrom-OpenTag3DPayload -Payload $payload
        if ($Raw) { $result | Add-Member -NotePropertyName PayloadBytes -NotePropertyValue $payload }
        $result
    }
}