Full Code of gmenounos/kw1281test for AI

master d091a1cf0624 cached
67 files
295.5 KB
81.8k tokens
458 symbols
1 requests
Download .txt
Showing preview only (314K chars total). Download the full file or copy to clipboard to get everything.
Repository: gmenounos/kw1281test
Branch: master
Commit: d091a1cf0624
Files: 67
Total size: 295.5 KB

Directory structure:
gitextract_0ekb2w7j/

├── .gitattributes
├── .github/
│   └── FUNDING.yml
├── .gitignore
├── .vscode/
│   └── launch.json
├── BlockTitle.cs
├── Blocks/
│   ├── AckBlock.cs
│   ├── ActuatorTestResponseBlock.cs
│   ├── AdaptationResponseBlock.cs
│   ├── AsciiDataBlock.cs
│   ├── Block.cs
│   ├── CodingWscBlock.cs
│   ├── CustomBlock.cs
│   ├── FaultCodesBlock.cs
│   ├── GroupReadResponseBlock.cs
│   ├── GroupReadResponseWithTextBlock.cs
│   ├── NakBlock.cs
│   ├── RawDataReadResponseBlock.cs
│   ├── ReadEepromResponseBlock.cs
│   ├── ReadRomEepromResponse.cs
│   ├── SecurityAccessMode2Block.cs
│   ├── SensorValue.cs
│   ├── UnknownBlock.cs
│   └── WriteEepromResponseBlock.cs
├── BusyWait.cs
├── Cluster/
│   ├── AudiC5Cluster.cs
│   ├── BoschRBxCluster.cs
│   ├── ICluster.cs
│   ├── MarelliCluster.cs
│   ├── MotometerBOOCluster.cs
│   ├── VdoCluster.cs
│   └── VdoKeyFinder.cs
├── ControllerAddress.cs
├── ControllerIdent.cs
├── ControllerInfo.cs
├── EDC15/
│   ├── Edc15VM.cs
│   └── Loader.a66
├── Interface/
│   ├── FtdiInterface.cs
│   ├── GenericInterface.cs
│   ├── IInterface.cs
│   └── LinuxInterface.cs
├── KW1281Dialog.cs
├── Kwp2000/
│   ├── DiagnosticService.cs
│   ├── KW2000Dialog.cs
│   ├── Kwp2000Message.cs
│   ├── NegativeResponseException.cs
│   └── ResponseCode.cs
├── KwpCommon.cs
├── LICENSE.txt
├── Logging/
│   ├── ConsoleLog.cs
│   ├── FileLog.cs
│   └── ILog.cs
├── Program.cs
├── Publish_Mac.ps1
├── Publish_Win.ps1
├── README.md
├── Tester.cs
├── Tests/
│   ├── BitFab.KW1281Test.Tests.csproj
│   ├── Cluster/
│   │   ├── MarelliClusterTests.cs
│   │   └── VdoClusterTests.cs
│   ├── GlobalUsings.cs
│   ├── ProgramTests.cs
│   ├── TesterTests.cs
│   └── UtilsTests.cs
├── UnableToProceedException.cs
├── UnexpectedProtocolException.cs
├── Utils.cs
└── kw1281test.slnx

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto

###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs     diff=csharp

###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following 
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln       merge=binary
#*.csproj    merge=binary
#*.vbproj    merge=binary
#*.vcxproj   merge=binary
#*.vcproj    merge=binary
#*.dbproj    merge=binary
#*.fsproj    merge=binary
#*.lsproj    merge=binary
#*.wixproj   merge=binary
#*.modelproj merge=binary
#*.sqlproj   merge=binary
#*.wwaproj   merge=binary

###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg   binary
#*.png   binary
#*.gif   binary

###############################################################################
# diff behavior for common document formats
# 
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the 
# entries below.
###############################################################################
#*.doc   diff=astextplain
#*.DOC   diff=astextplain
#*.docx  diff=astextplain
#*.DOCX  diff=astextplain
#*.dot   diff=astextplain
#*.DOT   diff=astextplain
#*.pdf   diff=astextplain
#*.PDF   diff=astextplain
#*.rtf   diff=astextplain
#*.RTF   diff=astextplain


================================================
FILE: .github/FUNDING.yml
================================================
custom: ["https://paypal.me/GregMenounos"]


================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# Visual Studio 2017 auto generated files
Generated\ Files/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUNIT
*.VisualState.xml
TestResult.xml

# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c

# Benchmark Results
BenchmarkDotNet.Artifacts/

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/

# StyleCop
StyleCopReport.xml

# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc

# Chutzpah Test files
_Chutzpah*

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb

# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap

# Visual Studio Trace Files
*.e2e

# TFS 2012 Local Workspace
$tf/

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# JustCode is a .NET coding add-in
.JustCode

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json

# Visual Studio code coverage results
*.coverage
*.coveragexml

# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*

# MightyMoose
*.mm.*
AutoTest.Net/

# Web workbench (sass)
.sass-cache/

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj

# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/

# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets

# Microsoft Azure Build Output
csx/
*.build.csdef

# Microsoft Azure Emulator
ecf/
rcf/

# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx

# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/

# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk

# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak

# SQL Server files
*.mdf
*.ldf
*.ndf

# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- Backup*.rdl

# Microsoft Fakes
FakesAssemblies/

# GhostDoc plugin setting file
*.GhostDoc.xml

# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/

# Visual Studio 6 build log
*.plg

# Visual Studio 6 workspace options file
*.opt

# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw

# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions

# Paket dependency manager
.paket/paket.exe
paket-files/

# FAKE - F# Make
.fake/

# JetBrains Rider
.idea/
*.sln.iml

# CodeRush personal settings
.cr/personal

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config

# Tabs Studio
*.tss

# Telerik's JustMock configuration file
*.jmconfig

# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs

# OpenCover UI analysis results
OpenCover/

# Azure Stream Analytics local run output
ASALocalRun/

# MSBuild Binary and Structured Log
*.binlog

# NVidia Nsight GPU debugger configuration file
*.nvuser

# MFractors (Xamarin productivity tool) working folder
.mfractor/

# Local History for Visual Studio
.localhistory/

# BeatPulse healthcheck temp database
healthchecksdb
/GitHub
/kw1281test.csproj

.DS_Store


================================================
FILE: .vscode/launch.json
================================================
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": []
}

================================================
FILE: BlockTitle.cs
================================================
namespace BitFab.KW1281Test
{
    public enum BlockTitle : byte
    {
        ReadIdent = 0x00,
        ReadRam = 0x01,
        GroupReadResponseWithText = 0x02,
        ReadRomEeprom = 0x03,
        ActuatorTest = 0x04,
        FaultCodesDelete = 0x05,
        End = 0x06, // end output, end of communication
        FaultCodesRead = 0x07, // get errors, all errors output
        ACK = 0x09,
        NAK = 0x0A,
        SoftwareCoding = 0x10,
        BasicSettingRawDataRead = 0x11,
        RawDataRead = 0x12,
        ReadEeprom = 0x19,
        WriteEeprom = 0x1A,
        Custom = 0x1B,
        AdaptationRead = 0x21,
        AdaptationTest = 0x22,
        BasicSettingRead = 0x28,
        GroupRead = 0x29,
        AdaptationSave = 0x2A,
        Login = 0x2B,
        SecurityAccessMode2 = 0x3D,
        SecurityImmoAccess1 = 0x5C,
        SecurityAccessMode1 = 0xD7,
        AdaptationResponse = 0xE6,
        GroupReadResponse = 0xE7,
        ReadEepromResponse = 0xEF,
        RawDataReadResponse = 0xF4,
        ActuatorTestResponse = 0xF5,
        AsciiData = 0xF6,
        WriteEepromResponse = 0xF9,
        FaultCodesResponse = 0xFC,
        ReadRomEepromResponse = 0xFD,
        ReadRamResponse = 0xFE,
    }

    // http://nefariousmotorsports.com/forum/index.php?topic=8274.0
    // Block Title - Answer
    // $00 ECU identification read - $F6
    // $01 RAM cells read - $FE
    // $02 RAM cells write - $09
    // $03 ROM/EPROM/EEPROM read - $FD
    // $04 Actuator test - $F5
    // $05 Fault codes delete -$FC
    // $06 Output end
    // $07 Fault codes read - $FC
    // $08 ADC channel read - $FB
    // $09 Acknowledge
    // $0A NoAck
    // $0C EPROM/EEPROM write - $F9
    // $10 Parameter coding - $F6
    // $11 Basic setting read - $F4
    // $12 Measuring values read - $F4
    // $19 EEPROM (serial) read - $EF
    // $1A EEPROM (serial) write - $F9
    // $1B Custom usage 
    // $21 Adaption read - $E6
    // $22 Adaption transfer - $E6
    // $27 Start download routine - $E8
    // $28 Basic setting normed read - $E7
    // $29 Measuring values normed read - $E7
    // $2A Adaption save - $E6
    // $2B Login request - $09/$FD
    // $3D Security access mode 2 - $09/$D7
    // $D7 Security access mode 1 - $3D
}


================================================
FILE: Blocks/AckBlock.cs
================================================
using System;
using System.Collections.Generic;

namespace BitFab.KW1281Test.Blocks
{
    internal class AckBlock : Block
    {
        public AckBlock(List<byte> bytes) : base(bytes)
        {
            // Dump();
        }

        private void Dump()
        {
            Log.WriteLine("Received ACK block");
        }
    }
}


================================================
FILE: Blocks/ActuatorTestResponseBlock.cs
================================================
using System.Collections.Generic;

namespace BitFab.KW1281Test.Blocks
{
    internal class ActuatorTestResponseBlock : Block
    {
        public ActuatorTestResponseBlock(List<byte> bytes) : base(bytes)
        {
            Dump();
        }

        public string ActuatorName
        {
            get
            {
                var id = Utils.Dump(Body).Trim();
                if (_idToName.TryGetValue(id, out string? name))
                {
                    return name!;
                }
                return id;
            }
        }

        private void Dump()
        {
            Log.Write("Received \"Actuator Test Response\" block:");
            foreach (var b in Body)
            {
                Log.Write($" {b:X2}");
            }

            Log.WriteLine();
        }

        private static readonly Dictionary<string, string> _idToName = new()
        {
            { "02 96", "Tachometer" },
            { "02 95", "Coolant Temp Gauge" },
            { "02 98", "Fuel Gauge" },
            { "02 97", "Speedometer" },
            { "03 1E", "Segment Test" },
            { "02 72", "Glow Plug Warning" },
            { "02 F2", "Coolant Temp Warning" },
            { "02 F3", "Oil Pressure Warning" },
            { "01 F5", "Oil Level Warning" },
            { "04 16", "Brake Pad Warning" },
            { "04 3B", "Low Washer Fluid Warning" },
            { "04 3A", "Low Fuel Warning" },
            { "01 F6", "Immobilizer Warning" },
            { "04 17", "Brake Warning" },
            { "02 99", "Seatbelt Warning" },
            { "02 9A", "Gong" },
            { "03 FF", "Chime" },
            { "04 AB", "End" },
        };
    }
}


================================================
FILE: Blocks/AdaptationResponseBlock.cs
================================================
using System.Collections.Generic;

namespace BitFab.KW1281Test.Blocks
{
    internal class AdaptationResponseBlock : Block
    {
        public AdaptationResponseBlock(List<byte> bytes) : base(bytes)
        {
            Dump();
        }

        public byte ChannelNumber => Body[0];

        public ushort ChannelValue => (ushort)(Body[1] * 256 + Body[2]);

        private void Dump()
        {
            Log.Write("Received \"Adaptation Response\" block:");
            foreach (var b in Body)
            {
                Log.Write($" {b:X2}");
            }

            Log.WriteLine();
        }
    }
}


================================================
FILE: Blocks/AsciiDataBlock.cs
================================================
using System;
using System.Collections.Generic;
using System.Text;

namespace BitFab.KW1281Test.Blocks
{
    internal class AsciiDataBlock : Block
    {
        public AsciiDataBlock(List<byte> bytes) : base(bytes)
        {
            // Dump();
        }

        public bool MoreDataAvailable => Bytes[3] > 0x7F;

        public override string ToString()
        {
            var sb = new StringBuilder();
            foreach (var b in Body)
            {
                sb.Append((char)(b & 0x7F));
            }
            return sb.ToString();
        }

        private void Dump()
        {
            Log.Write($"Received Ascii data block: \"{ToString()}\"");

            if (MoreDataAvailable)
            {
                Log.Write(" (More data available via ReadIdent)");
            }

            Log.WriteLine();
        }
    }
}


================================================
FILE: Blocks/Block.cs
================================================
using System.Collections.Generic;
using System.Linq;

namespace BitFab.KW1281Test.Blocks
{
    /// <summary>
    /// KWP1281 block
    /// </summary>
    class Block
    {
        public Block(List<byte> bytes)
        {
            Bytes = bytes;
        }

        /// <summary>
        /// Returns the entire raw block bytes.
        /// </summary>
        public List<byte> Bytes { get; }

        public byte Title => Bytes[2];

        /// <summary>
        /// Returns the body of the block, excluding the length, counter, title and end bytes.
        /// </summary>
        public List<byte> Body => Bytes.Skip(3).Take(Bytes.Count - 4).ToList();

        public bool IsAck => Title == (byte)BlockTitle.ACK;

        public bool IsNak => Title == (byte)BlockTitle.NAK;

        public bool IsAckNak => IsAck || IsNak;
    }
}


================================================
FILE: Blocks/CodingWscBlock.cs
================================================
using System.Collections.Generic;
using System.Linq;

namespace BitFab.KW1281Test.Blocks
{
    internal class CodingWscBlock : Block
    {
        public CodingWscBlock(List<byte> bytes) : base(bytes)
        {
            var data = bytes.Skip(4).ToList();

            SoftwareCoding = (data[0] * 256 + data[1]) / 2;
            WorkshopCode = data[2] * 256 + data[3];

            // Workshop codes > 65535 overflow into the low bit of the software coding
            if ((data[1] & 1) == 1)
            {
                WorkshopCode += 65536;
            }
        }

        public override string ToString()
        {
            return $"Software Coding {SoftwareCoding:d5}, Workshop Code: {WorkshopCode:d5}";
        }

        public int SoftwareCoding { get; }

        public int WorkshopCode { get; }
    }
}


================================================
FILE: Blocks/CustomBlock.cs
================================================
using System.Collections.Generic;

namespace BitFab.KW1281Test.Blocks
{
    internal class CustomBlock : Block
    {
        public CustomBlock(List<byte> bytes) : base(bytes)
        {
            // Dump();
        }

        private void Dump()
        {
            Log.Write("Received Custom block:");
            for (var i = 3; i < Bytes.Count - 1; i++)
            {
                Log.Write($" {Bytes[i]:X2}");
            }

            Log.WriteLine();
        }
    }
}

================================================
FILE: Blocks/FaultCodesBlock.cs
================================================
using System.Collections.Generic;
using System.Linq;

namespace BitFab.KW1281Test.Blocks
{
    internal class FaultCodesBlock : Block
    {
        public FaultCodesBlock(List<byte> bytes) : base(bytes)
        {
            FaultCodes = new();

            IEnumerable<byte> data = Body;

            while (true)
            {
                var code = data.Take(3).ToArray();
                if (code.Length == 0)
                {
                    break;
                }

                var dtc = code[0] * 256 + code[1];
                var status = code[2];

                var faultCode = new FaultCode(dtc, status);
                if (!faultCode.Equals(FaultCode.None))
                {
                    FaultCodes.Add(faultCode);
                }

                data = data.Skip(3);
            }
        }

        public List<FaultCode> FaultCodes { get; }
    }

    internal struct FaultCode
    {
        public FaultCode(int dtc, int status)
        {
            Dtc = dtc;
            Status = status;
        }

        public override string ToString()
        {
            var status1 = Status & 0x7F;
            var status2 = (Status >> 7) * 10;
            return $"{Dtc:d5} - {status1:d2}-{status2:d2}";
        }

        public int Dtc { get; }

        public int Status { get; }

        public static readonly FaultCode None = new FaultCode(0xFFFF, 0x88);
    }
}


================================================
FILE: Blocks/GroupReadResponseBlock.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BitFab.KW1281Test.Blocks
{
    internal class GroupReadResponseBlock : Block
    {
        public GroupReadResponseBlock(List<byte> bytes) : base(bytes)
        {
            SensorValues = new List<SensorValue>();

            var bodyBytes = new List<byte>(Body);
            while (bodyBytes.Count > 2)
            {
                var valueBytes = bodyBytes.Take(3).ToArray();
                SensorValues.Add(
                    new SensorValue(valueBytes[0], valueBytes[1], valueBytes[2]));
                bodyBytes = bodyBytes.Skip(3).ToList();
            }

            if (bodyBytes.Count > 0)
            {
                throw new InvalidOperationException(
                    $"{nameof(GroupReadResponseBlock)} body ({Utils.DumpBytes(Body)}) should be a multiple of 3 bytes long.");
            }
        }

        public List<SensorValue> SensorValues { get; }

        public override string ToString()
        {
            var sb = new StringBuilder();

            foreach(var sensorValue in SensorValues)
            {
                if (sb.Length > 0)
                {
                    sb.Append(" | ");
                }
                sb.Append(sensorValue.ToString());
            }

            return sb.ToString();
        }
    }
}
 

================================================
FILE: Blocks/GroupReadResponseWithTextBlock.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BitFab.KW1281Test.Blocks
{
    internal class GroupReadResponseWithTextBlock : Block
    {
        public GroupReadResponseWithTextBlock(List<byte> bytes)
            : base(bytes)
        {
            var bodyBytes = new List<byte>(Body);
            while (bodyBytes.Count > 2)
            {
                var subBlockHeader = bodyBytes.Take(3).ToArray();
                bodyBytes = bodyBytes.Skip(3).ToList();

                int subBlockBodyLength = subBlockHeader[2];
                if (bodyBytes.Count < subBlockBodyLength)
                {
                    throw new InvalidOperationException(
                        $"{nameof(GroupReadResponseWithTextBlock)} body ({Utils.DumpBytes(Body)}) contains extra bytes after sub-blocks.");
                }

                var subBlock = new SubBlock
                {
                    BlockType = subBlockHeader[0],
                    Data = subBlockHeader[1],
                    Body = bodyBytes.Take(subBlockBodyLength).ToArray()
                };
                bodyBytes = bodyBytes.Skip(subBlockBodyLength).ToList();

                SubBlocks.Add(subBlock);
                
                if (subBlock.BlockType == 0x8D)
                {
                    var text = Encoding.ASCII.GetString(subBlock.Body, 0, subBlock.Body.Length);
                    _text = text.Split((char)0x03).ToList();
                }
            }

            if (bodyBytes.Count > 0)
            {
                throw new InvalidOperationException(
                    $"{nameof(GroupReadResponseWithTextBlock)} body ({Utils.DumpBytes(Body)}) contains extra bytes after sub-blocks.");
            }
        }

        private readonly List<string> _text = new();

        public string GetText(int i)
        {
            if (i >= 0 && i < _text.Count)
            {
                return $"\"{_text[i]}\"";
            }

            return i.ToString();
        }

        public override string ToString()
        {
            var sb = new StringBuilder();

            foreach (var subBlock in SubBlocks)
            {
                sb.Append(subBlock.ToString());
            }

            return sb.ToString();
        }

        readonly List<SubBlock> SubBlocks = new();

        class SubBlock
        {
            public byte BlockType { get; init; }

            public byte Data { get; init; }

            public byte[] Body { get; init; } = Array.Empty<byte>();

            public override string ToString()
            {
                switch(BlockType)
                {
                    case 0x8D:
                        return $"(${BlockType:X2} ${Data:X2} {Encoding.ASCII.GetString(Body, 0, Body.Length).Replace((char)0x03, '|')})";

                    default:
                        return $"(${BlockType:X2} ${Data:X2}{Utils.Dump(Body)})";
                }
            }
        }
    }
}


================================================
FILE: Blocks/NakBlock.cs
================================================
using System;
using System.Collections.Generic;

namespace BitFab.KW1281Test.Blocks
{
    class NakBlock : Block
    {
        public NakBlock(List<byte> bytes) : base(bytes)
        {
        }
    }
}


================================================
FILE: Blocks/RawDataReadResponseBlock.cs
================================================
using System.Collections.Generic;

namespace BitFab.KW1281Test.Blocks
{
    internal class RawDataReadResponseBlock : Block
    {
        public RawDataReadResponseBlock(List<byte> bytes) : base(bytes)
        {
        }

        public override string ToString()
        {
            return $"Raw Data:{Utils.DumpDecimal(Body)}";
        }
    }
}

================================================
FILE: Blocks/ReadEepromResponseBlock.cs
================================================
using System.Collections.Generic;

namespace BitFab.KW1281Test.Blocks
{
    internal class ReadEepromResponseBlock : Block
    {
        public ReadEepromResponseBlock(List<byte> bytes) : base(bytes)
        {
            Dump();
        }

        private void Dump()
        {
            Log.Write("Received \"Read EEPROM Response\" block:");
            foreach (var b in Body)
            {
                Log.Write($" {b:X2}");
            }

            Log.WriteLine();
        }
    }
}

================================================
FILE: Blocks/ReadRomEepromResponse.cs
================================================
using System.Collections.Generic;

namespace BitFab.KW1281Test.Blocks
{
    internal class ReadRomEepromResponse : Block
    {
        public ReadRomEepromResponse(List<byte> bytes) : base(bytes)
        {
            Dump();
        }

        private void Dump()
        {
            Log.Write("Received \"Read ROM/EEPROM Response\" block:");
            foreach (var b in Body)
            {
                Log.Write($" {b:X2}");
            }

            Log.WriteLine();
        }
    }
}


================================================
FILE: Blocks/SecurityAccessMode2Block.cs
================================================
using System.Collections.Generic;

namespace BitFab.KW1281Test.Blocks
{
    internal class SecurityAccessMode2Block : Block
    {
        public SecurityAccessMode2Block(List<byte> bytes) : base(bytes)
        {
            Dump();
        }

        private void Dump()
        {
            Log.Write("Received \"Security Access Mode 2\" block:");
            foreach (var b in Body)
            {
                Log.Write($" ${b:X2}");
            }

            Log.WriteLine();
        }
    }
}


================================================
FILE: Blocks/SensorValue.cs
================================================
using System;

namespace BitFab.KW1281Test.Blocks
{
    public class SensorValue
    {
        public byte SensorID { get; }

        public byte A { get; }

        public byte B { get; }

        public SensorValue(byte sensorID, byte a, byte b)
        {
            SensorID = sensorID;
            A = a;
            B = b;
        }

        public override string ToString()
        {
            // https://www.blafusel.de/obd/obd2_kw1281.html#7
            return SensorID switch
            {
                1 => $"{0.2 * A * B} rpm",
                2 => $"{(A * 0.002 * B):F1} %",
                3 => $"{(0.002 * A * B):F1} Deg",
                4 => $"{(Math.Abs(B - 127) * 0.01 * A):F1} \u00B0{(B > 127 ? "ATDC" : "BTDC")}", // Degrees
                5 => $"{(A * (B-100) * 0.1):F1} \u00B0C", // Degrees C
                6 => $"{(0.001 * A * B):F1} V",
                7 => $"{0.01 * A * B} km/h",
                8 => $"{(0.1 * A * B):F1}",
                9 => $"{((B - 127) * 0.02 * A):F1} Deg",
                10 => $"{(B == 0 ? "Cold" : "Warm")}",
                11 => $"{(0.0001 * A * (B - 128) + 1):F1}",
                12 => $"{(0.001 * A * B):F1} \u2126", // Ohm
                13 => $"{((B - 127) * 0.001 * A):F1} mm",
                14 => $"{(0.005 * A * B):F1} bar",
                15 => $"{(0.01 * A * B):F1} ms",
                16 => $"{Convert.ToString(A, 2).PadLeft(8, '0')} {Convert.ToString(B, 2).PadLeft(8, '0')}",
                17 => $"\"{(char)A}{(char)B}\"",
                18 => $"{(0.04 * A * B):F1} mbar",
                19 => $"{(A * B * 0.01):F1} l",
                20 => $"{(A * (B - 128) / 128.0):F1} %",
                21 => $"{(0.001 * A * B):F1} V",
                22 => $"{(0.001 * A * B):F1} ms",
                23 => $"{(B / 256.0 * A):F1} %",
                24 => $"{(0.001 * A * B):F1} A",
                25 => $"{((B * 1.421) + (A / 182.0)):F1} g/s",
                26 => $"{B - A} C",
                27 => $"{(Math.Abs(B - 128) * 0.01 * A):F1} \u00B0{(B < 128 ? "ATDC" : "BTDC")}", // Degrees
                28 => $"{B - A}",
                29 => $"{(B<A ? "Map 1" : "Map 2")}",
                30 => $"{(B / 12.0 * A):F1} Deg k/w",
                31 => $"{(B / 2560.0 * A):F1}",
                32 => $"{(B>128 ? B-256 : B)}",
                33 => $"{(A == 0 ? 100.0*B : 100.0*B/A):F1} %",
                34 => $"{((B - 128) * 0.01 * A):F1} kW",
                35 => $"{(0.01 * A * B):F1} l/h",
                36 => $"{A * 2560 + B * 10} km",
                // 37 => ???,
                38 => $"{((B - 128) * 0.001 * A):F1} Deg k/w",
                39 => $"{(B/256.0*A):F1} mg/h",
                40 => $"{(B * 0.1 + (25.5 * A) - 400):F1} A",
                41 => $"{(B + A * 255)} Ah",
                42 => $"{(B * 0.1 + (25.5 * A) - 400):F1} Kw",
                43 => $"{(B * 0.1 + (25.5 * A)):F1} V",
                44 => $"{A:D2}:{B:D2}",
                45 => $"{(0.1 * A * B / 100.0):F1}",
                46 => $"{((A * B - 3200) * 0.0027):F1} Deg k/w",
                47 => $"{((B - 128) * A)} ms",
                48 => $"{B + A * 255}",
                49 => $"{(B / 4.0 * A * 0.1):F1} mg/h",
                50 => $"{(A == 0 ? (B - 128) / 0.01 : (B - 128) / (0.01 * A)):F1} mbar",
                51 => $"{(((B - 128) / 255.0) * A):F1} mg/h",
                52 => $"{(B * 0.02 * A - A):F1} Nm",
                53 => $"{((B - 128) * 1.4222 + 0.006 * A):F1} g/s",
                54 => $"{A * 256 + B}",
                55 => $"{(A * B / 200.0):F1} s",
                56 => $"{A * 256 + B} WSC",
                57 => $"{A * 256 + B + 65536} WSC",
                58 => $"{(B > 128 ? 1.0225 * (256 - B) : 1.0225 * B):F1} /s",
                59 => $"{((A * 256 + B) / 32768.0):F1}",
                60 => $"{((A * 256 + B) * 0.01):F1} sec",
                61 => $"{(A==0 ? (B - 128) : (B - 128) / A):F1}",
                62 => $"{(0.256 * A * B):F1} S",
                63 => $"\"{(char)A}{(char)B}\"?",
                64 => $"{A+B} \u2126", // Ohm
                65 => $"{(0.01 * A * (B - 127)):F1} mm",
                66 => $"{((A * B) / 511.12):F1} V",
                67 => $"{((640 * A) + B * 2.5):F1} Deg",
                68 => $"{((256 * A + B) / 7.365):F1} deg/s",
                69 => $"{((256 * A + B) * 0.3254):F1} Bar",
                70 => $"{((256 * A + B) * 0.192):F1} m/s\u00B2", // squared
                _ => $"({SensorID} {A} {B})",
            };
        }
    }
}


================================================
FILE: Blocks/UnknownBlock.cs
================================================
using System.Collections.Generic;

namespace BitFab.KW1281Test.Blocks
{
    internal class UnknownBlock : Block
    {
        public UnknownBlock(List<byte> bytes) : base(bytes)
        {
            Dump();
        }

        private void Dump()
        {
            Log.Write($"Received ${Title:X2} block:");
            foreach (var b in Bytes)
            {
                Log.Write($" 0x{b:X2}");
            }
            Log.WriteLine();
        }
    }
}

================================================
FILE: Blocks/WriteEepromResponseBlock.cs
================================================
using System;
using System.Collections.Generic;

namespace BitFab.KW1281Test.Blocks
{
    internal class WriteEepromResponseBlock : Block
    {
        public WriteEepromResponseBlock(List<byte> bytes) : base(bytes)
        {
            Dump();
        }

        private void Dump()
        {
            Log.Write("Received \"Write EEPROM Response\" block:");
            foreach (var b in Body)
            {
                Log.Write($" {b:X2}");
            }

            Log.WriteLine();
        }
    }
}

================================================
FILE: BusyWait.cs
================================================
using System.Diagnostics;

namespace BitFab.KW1281Test;

public class BusyWait
{
    private readonly long _ticksPerCycle;
    private long? _nextTickTimestamp;

    public BusyWait(long msPerCycle)
    {
        _ticksPerCycle = msPerCycle * TicksPerMs;
    }

    public void DelayUntilNextCycle()
    {
        _nextTickTimestamp ??= Stopwatch.GetTimestamp() + _ticksPerCycle;

        while (Stopwatch.GetTimestamp() < _nextTickTimestamp)
        {
        }
        _nextTickTimestamp += _ticksPerCycle;
    }

    public static void Delay(long ms)
    {
        var waiter = new BusyWait(ms);
        waiter.DelayUntilNextCycle();
    }

    private static readonly long TicksPerMs = Stopwatch.Frequency / 1000;
}

================================================
FILE: Cluster/AudiC5Cluster.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using BitFab.KW1281Test.Blocks;

namespace BitFab.KW1281Test.Cluster;

internal class AudiC5Cluster : ICluster
{
    public void UnlockForEepromReadWrite()
    {
        string[] passwords =
        [
            "loginas9",
            "n7KB2Qat",
        ];

        var succeeded = false;
        foreach (var password in passwords)
        {
            Log.WriteLine("Sending custom login block");
            var blockBytes = new List<byte>([0x1B, 0x80]); // Custom 0x80
            blockBytes.AddRange(Encoding.ASCII.GetBytes(password));
            _kw1281Dialog.SendBlock(blockBytes);

            var block = _kw1281Dialog.ReceiveBlock();
            if (block is NakBlock)
            {
                continue;
            }
            else if (block is not AckBlock)
            {
                throw new InvalidOperationException(
                    $"Expected ACK block but received: {block}");
            }

            succeeded = true;
        }

        if (!succeeded)
        {
            throw new InvalidOperationException("Unable to login to cluster");
        }

        var @interface = _kw1281Dialog.KwpCommon.Interface;
        @interface.SetBaudRate(19200);
        @interface.SetParity(Parity.Even);
        @interface.ClearReceiveBuffer();

        Thread.Sleep(TimeSpan.FromSeconds(2));
    }

    public string DumpEeprom(uint? address, uint? length, string? dumpFileName)
    {
        ArgumentNullException.ThrowIfNull(address);
        ArgumentNullException.ThrowIfNull(length);
        ArgumentNullException.ThrowIfNull(dumpFileName);

        WriteBlock([Constants.Hello]);

        var blockBytes = ReadBlock();
        Log.WriteLine($"Received block:{Utils.Dump(blockBytes)}");
        if (BlockTitle(blockBytes) != Constants.Hello)
        {
            Log.WriteLine($"Warning: Expected block of type ${Constants.Hello:X2}");
        }

        string[] passwords =
        [
            "19xDR8xS",
            "vdokombi",
            "w10kombi",
            "w10serie",
        ];

        var succeeded = false;
        foreach (var password in passwords)
        {
            Log.WriteLine("Sending login request");

            blockBytes = [Constants.Login, 0x9D];
            blockBytes.AddRange(Encoding.ASCII.GetBytes(password));
            WriteBlock(blockBytes);

            blockBytes = ReadBlock();
            Log.WriteLine($"Received block:{Utils.Dump(blockBytes)}");

            if (BlockTitle(blockBytes) == Constants.Ack)
            {
                succeeded = true;
                break;
            }
            else
            {
                Log.WriteLine($"Warning: Expected block of type ${Constants.Ack:X2}");
            }
        }

        if (!succeeded)
        {
            throw new InvalidOperationException("Unable to login to cluster");
        }
        else
        {
            Log.WriteLine("Succeeded");
        }

        Log.WriteLine($"Dumping EEPROM to {dumpFileName}");
        DumpEeprom(address.Value, length.Value, maxReadLength: 0x10, dumpFileName);

        _kw1281Dialog.SetDisconnected();

        return dumpFileName;
    }

    private void DumpEeprom(
        uint startAddr, uint length, byte maxReadLength, string fileName)
    {
        using var fs = File.Create(fileName, bufferSize: maxReadLength, FileOptions.WriteThrough);

        var succeeded = true;
        for (var addr = startAddr; addr < startAddr + length; addr += maxReadLength)
        {
            var readLength = (byte)Math.Min(startAddr + length - addr, maxReadLength);
            var blockBytes = ReadEepromByAddress(addr, readLength);

            if (blockBytes.Count != readLength)
            {
                succeeded = false;
                blockBytes.AddRange(
                    Enumerable.Repeat((byte)0, readLength - blockBytes.Count));
            }

            fs.Write(blockBytes.ToArray(), offset: 0, blockBytes.Count);
            fs.Flush();
        }

        if (!succeeded)
        {
            Log.WriteLine();
            Log.WriteLine("**********************************************************************");
            Log.WriteLine("*** Warning: Some bytes could not be read and were replaced with 0 ***");
            Log.WriteLine("**********************************************************************");
            Log.WriteLine();
        }
    }

    private List<byte> ReadEepromByAddress(uint addr, byte readLength)
    {
        List<byte> blockBytes =
        [
            Constants.ReadEeprom,
            readLength,
            (byte)(addr >> 8),
            (byte)(addr & 0xFF)
        ];
        WriteBlock(blockBytes);

        blockBytes = ReadBlock();
        Log.WriteLine($"Received block:{Utils.Dump(blockBytes)}");

        if (BlockTitle(blockBytes) != Constants.ReadEeprom)
        {
            throw new InvalidOperationException($"Expected block of type ${Constants.ReadEeprom:X2}");
        }

        var expectedLength = readLength + 4;
        var actualLength = blockBytes.Count;
        if (blockBytes.Count != expectedLength)
        {
            Log.WriteLine(
        $"Warning: Expected block length ${expectedLength:X2} but length is ${actualLength:X2}");
        }

        return blockBytes.Skip(3).Take(actualLength - 4).ToList();
    }

    private static byte BlockTitle(IReadOnlyList<byte> blockBytes)
    {
        return blockBytes[2];
    }

    private void WriteBlock(IReadOnlyCollection<byte> bodyBytes)
    {
        byte checksum = 0x00;

        WriteBlockByte(Constants.StartOfBlock);
        WriteBlockByte((byte)(bodyBytes.Count + 3)); // Block length
        foreach (var bodyByte in bodyBytes)
        {
            WriteBlockByte(bodyByte);
        }

        _kw1281Dialog.KwpCommon.WriteByte(checksum);
        return;

        void WriteBlockByte(byte b)
        {
            _kw1281Dialog.KwpCommon.WriteByte(b);
            checksum ^= b;
        }
    }

    private List<byte> ReadBlock()
    {
        var blockBytes = new List<byte>();
        byte checksum = 0x00;

        try
        {
            var header = ReadByte();
            var blockSize = ReadByte();
            for (var i = 0; i < blockSize - 2; i++)
            {
                ReadByte();
            }

            if (header != Constants.StartOfBlock)
            {
                throw new InvalidOperationException($"Expected $D1 header byte but got ${header:X2}");
            }

            if (checksum != 0x00)
            {
                throw new InvalidOperationException($"Expected $00 block checksum but got ${checksum:X2}");
            }
        }
        catch (Exception e)
        {
            Log.WriteLine($"Error reading block: {e}");
            Log.WriteLine($"Partial block: {Utils.Dump(blockBytes)}");
            throw;
        }

        return blockBytes;

        byte ReadByte()
        {
            var b = _kw1281Dialog.KwpCommon.ReadByte();
            checksum ^= b;
            blockBytes.Add(b);
            return b;
        }
    }

    private static class Constants
    {
        public const byte StartOfBlock = 0xD1;

        public const byte Ack = 0x06;
        public const byte Nak = 0x15;
        public const byte Hello = 0x49;
        public const byte Login = 0x53;
        public const byte ReadEeprom = 0x72;
    }

    private readonly IKW1281Dialog _kw1281Dialog;

    public AudiC5Cluster(IKW1281Dialog kw1281Dialog)
    {
        _kw1281Dialog = kw1281Dialog;
    }
}

================================================
FILE: Cluster/BoschRBxCluster.cs
================================================
using BitFab.KW1281Test.Kwp2000;
using System;
using System.Linq;
using System.Threading;
using Service = BitFab.KW1281Test.Kwp2000.DiagnosticService;

namespace BitFab.KW1281Test.Cluster;

class BoschRBxCluster : ICluster
{
    public void UnlockForEepromReadWrite()
    {
        SecurityAccess(0xFB);
    }

    public string DumpEeprom(
        uint? optionalAddress, uint? optionalLength, string? optionalFileName)
    {
        uint address = optionalAddress ?? 0x10400;
        uint length = optionalLength ?? 0x400;
        string filename = optionalFileName ?? $"RBx_0x{address:X6}_mem.bin";

        _kwp2000.DumpMem(address, length, filename);

        return filename;
    }

    public bool SecurityAccess(byte accessMode)
    {
        const byte identificationOption = 0x94;
        var responseMsg = _kwp2000.SendReceive(Service.readEcuIdentification, new byte[] { identificationOption });
        if (responseMsg.Body[0] != identificationOption)
        {
            throw new InvalidOperationException($"Received unexpected identificationOption: {responseMsg.Body[0]:X2}");
        }
        Log.WriteLine(Utils.DumpAscii(responseMsg.Body.Skip(1)));

        const int maxTries = 16;
        for (var i = 0; i < maxTries; i++)
        {
            responseMsg = _kwp2000.SendReceive(Service.securityAccess, [accessMode]);
            if (responseMsg.Body[0] != accessMode)
            {
                throw new InvalidOperationException($"Received unexpected accessMode: {responseMsg.Body[0]:X2}");
            }
            var seedBytes = responseMsg.Body.Skip(1).ToArray();
            var seed = (uint)(
                (seedBytes[0] << 24) |
                (seedBytes[1] << 16) |
                (seedBytes[2] << 8) |
                seedBytes[3]);
            var key = CalcRBxKey(seed);

            try
            {
                responseMsg = _kwp2000.SendReceive(Service.securityAccess,
                    new[] {
                        (byte)(accessMode + 1),
                        (byte)((key >> 24) & 0xFF),
                        (byte)((key >> 16) & 0xFF),
                        (byte)((key >> 8) & 0xFF),
                        (byte)(key & 0xFF)
                    });

                Log.WriteLine("Success!!!");
                return true;
            }
            catch (NegativeResponseException)
            {
                if (i < (maxTries - 1))
                {
                    Log.WriteLine("Trying again.");
                }
            }
        }

        return false;
    }

    /// <summary>
    /// Toggle an Audi A4 RB4 cluster between Adapted mode (6) and New mode (4).
    /// Cluster should already be logged in and unlocked for EEPROM read/write.
    /// </summary>
    public void ToggleRB4Mode()
    {
        _kwp2000.StartDiagnosticSession(0x84, 0x14);

        Thread.Sleep(350);

        byte[] bytes = _kwp2000.ReadMemoryByAddress(0x010450, 2);
        if (bytes[0] != (byte)'A' && bytes[1] != (byte)'U')
        {
            Log.WriteLine("Cluster is not an Audi cluster!");
        }
        else
        {
            try
            {
                bytes = _kwp2000.ReadMemoryByAddress(0x010000, 0x10);
                Log.WriteLine("Cluster is in New mode (4).");
            }
            catch (NegativeResponseException)
            {
                Log.WriteLine("Cluster is in Adapted mode (6).");
            }

            Log.WriteLine("Toggling cluster mode...");

            foreach (var address in new uint[] { 0x01044F, 0x01052F, 0x01062F })
            {
                bytes = _kwp2000.ReadMemoryByAddress(address, 1);
                bytes[0] ^= 0x12;
                _kwp2000.WriteMemoryByAddress(address, 1, bytes);
            }
        }

        Log.WriteLine("Resetting cluster...");

        _kwp2000.EcuReset(0x01);
    }

    static uint CalcRBxKey(uint seed)
    {
        uint key = 0x03249272 + (seed ^ 0xf8253947);
        return key;
    }

    private readonly KW2000Dialog _kwp2000;

    public BoschRBxCluster(KW2000Dialog kwp2000)
    {
        _kwp2000 = kwp2000;
    }
}


================================================
FILE: Cluster/ICluster.cs
================================================
namespace BitFab.KW1281Test.Cluster
{
    internal interface ICluster
    {
        void UnlockForEepromReadWrite();

        string DumpEeprom(uint? address, uint? length, string? dumpFileName);
    }
}


================================================
FILE: Cluster/MarelliCluster.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;

namespace BitFab.KW1281Test.Cluster
{
    class MarelliCluster : ICluster
    {
        public void UnlockForEepromReadWrite()
        {
            // Nothing to do
        }

        public string DumpEeprom(uint? address, uint? length, string? dumpFileName)
        {
            address ??= GetDefaultAddress();
            dumpFileName ??= $"marelli_mem_${address:X4}.bin";

            _ = DumpMem(dumpFileName, (ushort)address, (ushort?)length);

            return dumpFileName;
        }

        private ushort GetDefaultAddress()
        {
            if (HasSmallEeprom())
            {
                return 3072; // $0C00
            }
            else if (HasLargeEeprom())
            {
                return 14336; // $3800
            }
            else
            {
                Log.WriteLine();
                Log.WriteLine("Unsupported Marelli cluster version.");
                Log.WriteLine("You can try the following commands to see if either produces a dump file.");
                Log.WriteLine("Then please contact the program author with the results.");
                Log.WriteLine();

                var prefix = string.Join(' ', Program.CommandAndArgs.Take(4));
                Log.WriteLine($"{prefix} DumpMarelliMem 3072 1024");
                Log.WriteLine($"{prefix} DumpMarelliMem 14336 2048");

                throw new UnableToProceedException();
            }
        }

        /// <summary>
        /// Dumps memory from a Marelli cluster to a file.
        /// </summary>
        private byte[] DumpMem(
            string filename,
            ushort address,
            ushort? count = null)
        {
            byte entryH; // High byte of code entry point
            byte regBlockH; // High byte of register block

            if (_ecuInfo.Contains("M73 D0"))    // Audi TT
            {
                entryH = 0x00; // $0000
                regBlockH = (byte)((address == 0x3800) ? 0x20 : 0x08);
                count ??= (ushort)((address == 0x3800) ? 0x800 : 0x400);
            }
            else if (HasSmallEeprom())
            {
                entryH = 0x02; // $0200
                regBlockH = 0x08; // $0800
                count ??= 1024; // $0400
            }
            else if (HasLargeEeprom())
            {
                entryH = 0x18; // $1800
                regBlockH = 0x20; // $2000
                count ??= 2048; // $0800
            }
            else if (address == 3072 && count == 1024)
            {
                Log.WriteLine("Untested cluster version! You may need to disconnect your battery if this fails.");

                entryH = 0x02;
                regBlockH = 0x08;
            }
            else if (address == 14336 && count == 2048)
            {
                Log.WriteLine("Untested cluster version! You may need to disconnect your battery if this fails.");

                entryH = 0x18;
                regBlockH = 0x20;
            }
            else
            {
                Log.WriteLine("Unsupported cluster software version");
                return [];
            }

            Log.WriteLine($"entryH: 0x{entryH:X2}, regBlockH: 0x{regBlockH:X2}, count: 0x{count:X4}");

            Log.WriteLine("Sending block 0x6C");
            _kwp1281.SendBlock([0x6C]);

            Thread.Sleep(250);

            Log.WriteLine("Writing data to cluster microcontroller");
            var data = new byte[]
            {
                0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x50, 0x34,
                entryH, 0x00, // Entry point $xx00
            };
            if (!WriteMarelliBlockAndReadAck(data))
            {
                return [];
            }

            // Now we write a small memory dump program to the 68HC12 processor

            Log.WriteLine("Writing memory dump program to cluster microcontroller");
            Log.WriteLine($"(Entry: ${entryH:X2}00, RegBlock: ${regBlockH:X2}00, Start: ${address:X4}, Count: ${count:X4})");

            var startH = (byte)(address / 256);
            var startL = (byte)(address % 256);

            var end = address + count;
            var endH = (byte)(end / 256);
            var endL = (byte)(end % 256);

            var program = new byte[]
            {
                entryH, 0x00, // Address $xx00

                0x14, 0x50,                     // orcc #$50
                0x07, 0x32,                     // bsr FeedWatchdog

                // Set baud rate to 9600
                0xC7,                           // clrb
                0x7B, regBlockH, 0xC8,          // stab SC1BDH
                0xC6, 0x34,                     // ldab #$34
                0x7B, regBlockH, 0xC9,          // stab SC1BDL

                // Enable transmit, disable UART interrupts
                0xC6, 0x08,                     // ldab #$08
                0x7B, regBlockH, 0xCB,          // stab SC1CR2

                0xCE, startH, startL,           // ldx #start
                // SendLoop:
                0xA6, 0x30,                     // ldaa 1,X+
                0x07, 0x0F,                     // bsr SendByte
                0x8E, endH, endL,               // cpx #end
                0x26, 0xF7,                     // bne SendLoop
                // Poison the watchdog to force a reboot
                0xCC, 0x11, 0x11,               // ldd #$1111
                0x7B, regBlockH, 0x17,          // stab COPRST
                0x7A, regBlockH, 0x17,          // staa COPRST
                0x3D,                           // rts

                // SendByte:
                0xF6, regBlockH, 0xCC,          // ldab SC1SR1
                0x7A, regBlockH, 0xCF,          // staa SC1DRL
                // TxBusy:
                0x07, 0x06,                     // bsr FeedWatchdog
                // Loop until TC (Transmit Complete) bit is set
                0x1F, regBlockH, 0xCC, 0x40, 0xF9,   // brclr SC1SR1,$40,TxBusy
                0x3D,                           // rts

                // FeedWatchdog:
                0xCC, 0x55, 0xAA,               // ldd #$55AA
                0x7B, regBlockH, 0x17,          // stab COPRST
                0x7A, regBlockH, 0x17,          // staa COPRST
                0x3D,                           // rts
            };
            if (!WriteMarelliBlockAndReadAck(program))
            {
                return Array.Empty<byte>();
            }

            Log.WriteLine("Receiving memory dump");

            var kwpCommon = _kwp1281.KwpCommon;
            var mem = new List<byte>();
            for (int i = 0; i < count; i++)
            {
                var b = kwpCommon.ReadByte();
                mem.Add(b);
            }

            File.WriteAllBytes(filename, mem.ToArray());
            Log.WriteLine($"Saved memory dump to {filename}");

            Log.WriteLine("Done");

            _kwp1281.SetDisconnected(); // Don't try to send EndCommunication block

            return mem.ToArray();
        }

        private bool WriteMarelliBlockAndReadAck(byte[] data)
        {
            var kwpCommon = _kwp1281.KwpCommon;

            var count = (ushort)(data.Length + 2); // Count includes 2-byte checksum
            var countH = (byte)(count / 256);
            var countL = (byte)(count % 256);
            kwpCommon.WriteByte(countH);
            kwpCommon.WriteByte(countL);

            var sum = (ushort)(countH + countL);
            foreach (var b in data)
            {
                kwpCommon.WriteByte(b);
                sum += b;
            }
            kwpCommon.WriteByte((byte)(sum / 256));
            kwpCommon.WriteByte((byte)(sum % 256));

            var expectedAck = new byte[] { 0x03, 0x09, 0x00, 0x0C };

            Log.WriteLine("Receiving ACK");
            var ack = new List<byte>();
            for (int i = 0; i < 4; i++)
            {
                var b = kwpCommon.ReadByte();
                ack.Add(b);
            }
            if (!ack.SequenceEqual(expectedAck))
            {
                Log.WriteLine($"Expected ACK but received {Utils.Dump(ack)}");
                return false;
            }

            return true;
        }

        private readonly string[] _smallEepromEcus =
        [
            "1C0920800",    // Beetle 1C0920800C M73 V07
            "1C0920806",    // Beetle 1C0920806G M73 V03
            "1C0920901",    // Beetle 1C0920901C M73 V07
            "1C0920905",    // Beetle 1C0920905F M73 V03
            "1C0920906",    // Beetle 1C0920906A M73 V03
            "8N1919880E KOMBI+WEGFAHRS. M73 D23",   // Audi TT
            "8N1920930",    // Audi TT 8N1920930B M73 D23
        ];

        private bool HasSmallEeprom() => _smallEepromEcus.Any(model => _ecuInfo.Contains(model));

        private readonly string[] _largeEepromEcus =
        [
            "1C0920821",    // KOMBI+WEGFAHRS. M73 V08 (Beetle 2003)
            "1C0920921",    // Beetle 1C0920921G M73 V08
            "1C0920941",    // Beetle 1C0920941LX M73 V03
            "1C0920951",    // Beetle 1C0920951A M73 V02
            "8D0920900R",   // KOMBI+WEGFAHRS. M73 D54 (Audi A4 B5 2001)
            "8L0920900B",   // KOMBI+WEGFAHRS. M73 D13 (Audi A3 8L 2002, ASZ diesel engine)
            "8L0920900E",   // KOMBI+WEGFAHRS. M73 D56
            "8N1919880E KOMBI+WEGFAHRS. M73 D26",   // Audi TT
            "8N1920980",    // Audi TT 8N1920980E M73 D14
            "8N2919910A",   // KOMBI+WEGFAHRS. M73 D29, Audi TT
            "8N2920930",    // Audi TT 8N2920930C M73 D55
            "8N2920980",    // Audi TT 8N2920980A M73 D14
        ];

        // 1C0920821 KOMBI+WEGFAHRS. M73 V08

        private bool HasLargeEeprom() => _largeEepromEcus.Any(model => _ecuInfo.Contains(model));

        /// <summary>
        /// Search for the SKC using the 2 methods described here:
        /// https://github.com/gmenounos/kw1281test/issues/50#issuecomment-1770255129
        /// </summary>
        public static ushort? GetSkc(byte[] buf)
        {
            // If the EEPROM contains a 14-digit Immobilizer ID then the SKC should be immediately prior to that
            var immoIdOffset = FindImmobilizerId(buf);
            if (immoIdOffset is >= 2)
            {
                return Utils.GetShortBE(buf, immoIdOffset.Value-2);
            }

            // Otherwise search for 00,01,0F or 00,02,0F or 00,03,0F or 00,04,0F and the SKC should be immediately prior
            var keyCountOffset = FindKeyCount(buf);
            if (keyCountOffset is >= 2)
            {
                return Utils.GetShortBE(buf, keyCountOffset.Value-2);
            }

            return null;
        }

        /// <summary>
        /// Search the buffer for a 14 byte long string of uppercase letters and numbers beginning with VWZ or AUZ
        /// </summary>
        private static int? FindImmobilizerId(IReadOnlyList<byte> buf)
        {
            for (var i = 0; i < buf.Count - 14; i++)
            {
                if (!(buf[i] == 'V' && buf[i + 1] == 'W') &&
                    !(buf[i] == 'A' && buf[i + 1] == 'U'))
                {
                    continue;
                }

                if (buf[i + 2] != 'Z')
                {
                    continue;
                }

                var isValid = true;
                for (var j = 3; j < 14; j++)
                {
                    var b = buf[i + j];
                    if (b is >= (byte)'0' and <= (byte)'9' or >= (byte)'A' and <= (byte)'Z')
                    {
                        continue;
                    }

                    isValid = false;
                    break;
                }

                if (isValid)
                {
                    return i;
                }
            }

            return null;
        }

        /// <summary>
        /// Search the buffer for the 3 byte sequence 00,01,0F or 00,02,0F or 00,03,0F or 00,04,0F
        /// (2nd digit is probably the number of keys)
        /// </summary>
        private static int? FindKeyCount(IReadOnlyList<byte> buf)
        {
            for (var i = 0; i < buf.Count - 3; i++)
            {
                if (buf[i] != 0)
                {
                    continue;
                }

                if (buf[i + 1] != 1 && buf[i + 1] != 2 && buf[i + 1] != 3 && buf[i + 1] != 4)
                {
                    continue;
                }

                if (buf[i + 2] != 0x0F)
                {
                    continue;
                }

                return i;
            }

            return null;
        }

        private readonly IKW1281Dialog _kwp1281;
        private readonly string _ecuInfo;

        public MarelliCluster(IKW1281Dialog kwp1281, string ecuInfo)
        {
            _kwp1281 = kwp1281;
            _ecuInfo = ecuInfo;
        }
    }
}


================================================
FILE: Cluster/MotometerBOOCluster.cs
================================================
using BitFab.KW1281Test.Blocks;
using System;
using System.Collections.Generic;
using System.Linq;

namespace BitFab.KW1281Test.Cluster;

internal class MotometerBOOCluster : ICluster
{
    public void UnlockForEepromReadWrite()
    {
        string softwareVersion = GetClusterInfo();
        if (softwareVersion.Length < 10 ||
            !VersionToLogin.TryGetValue(softwareVersion[..10], out ushort login))
        {
            Log.WriteLine("Warning: Unknown software version. Login may fail.");
            login = 11899;
        }

        _kwp1281!.Login(login, workshopCode: 0);

        Log.WriteLine($"Sending Custom $08 $15 block");
        if (SendCustom(0x08, 0x15))
        {
            return;
        }

        Log.WriteLine("$08 $15 failed. Trying all combinations (this may take a while)...");

        for (int first = 0; first < 0x100; first++)
        {
            Log.WriteLine($"Trying ${first:X2} $00-$FF");

            for (int second = 0; second < 0x100; second++)
            {
                if (SendCustom(first, second))
                {
                    Log.WriteLine($"Combination ${first:X2} ${second:X2} Succeeded.");
                    Log.WriteLine("Please report this to the program author.");
                    return;
                }
            }
        }

        Log.WriteLine("All combinations failed. EEPROM access will likely fail.");
    }

    private bool SendCustom(int first, int second)
    {
        _kwp1281.SendBlock(new List<byte> { 0x1B, (byte)first, (byte)second });
        var block = _kwp1281.ReceiveBlocks().FirstOrDefault();

        if (block is NakBlock)
        {
            return false;
        }
        else if (block is AckBlock)
        {
            return true;
        }
        else
        {
            throw new InvalidOperationException(
                $"Expected ACK or NAK block but got: {block}");
        }
    }

    private readonly Dictionary<string, ushort> VersionToLogin = new()
    {
        { "a0prj008.1", 10164 },
        { "A0prj008.2", 10164 },
        { "a4prj010.1", 21597 },
        { "a4prj012.1", 21597 },
        { "h1340_05.2", 21701 },
        { "h1340_06.2", 21601 },
        { "h9340_08.1", 11899 },
        { "h9340_08.2", 11899 },
        { "h9340_09.1", 11899 },
        { "h9340_10.1", 11899 },
        { "h9340_10.2", 11899 },
        { "h9340_10.3", 11899 },
        { "h9340_11.2", 11899 },
        { "se110_05.2", 13473 },
        { "v9119_07.1", 19126 },
        { "v9119_07.3", 11064 },
        { "v9230_03.1", 10501 },
        { "v9230_03.2", 10501 },
        { "v9230_05.1", 44479 },
        { "v9230_05.2", 44479 },
        { "v9230_06.3", 23775 },
        { "v9230_07.2", 10164 },
        { "v9230_08.1", 10164 },
        { "v9230_08.2", 10164 },
        { "vw110_04.2", 08721 },
        { "VW230_06.1", 47165 },
        { "vw340_07.2", 05555 },
    };

    public string DumpEeprom(
        uint? optionalAddress, uint? optionalLength, string? optionalFileName)
    {
        uint address = optionalAddress ?? 0;
        uint length = optionalLength ?? 0x100;
        string filename = optionalFileName ?? $"BOOMM0_0x{address:X6}_eeprom.bin";

#if false
        var identInfo = _kwp1281.ReadIdent().First().ToString()
            .Split(Environment.NewLine).First() // Sometimes ReadIdent() can return multiple lines
            .Replace(' ', '_');

        var dumpFileName = filename ?? $"{identInfo}_0x{startAddress:X4}_eeprom.bin";
        foreach (var c in Path.GetInvalidFileNameChars())
        {
            dumpFileName = dumpFileName.Replace(c, 'X');
        }
        foreach (var c in Path.GetInvalidPathChars())
        {
            dumpFileName = dumpFileName.Replace(c, 'X');
        }

        Log.WriteLine($"Saving EEPROM dump to {dumpFileName}");
        DumpEeprom(startAddress, length, maxReadLength: 16, dumpFileName);
        Log.WriteLine($"Saved EEPROM dump to {dumpFileName}");

        return dumpFileName;
#endif
        throw new NotImplementedException();
    }

    private string GetClusterInfo()
    {
        Log.WriteLine("Sending 0x43 block");

        _kwp1281.SendBlock([0x43]);
        var blocks = _kwp1281.ReceiveBlocks().Where(b => !b.IsAckNak).ToList();
        foreach (var block in blocks)
        {
            Log.WriteLine($"{Utils.DumpAscii(block.Body)}");
        }

        return Utils.DumpAscii(blocks[0].Body);
    }

    private readonly IKW1281Dialog _kwp1281;

    public MotometerBOOCluster(IKW1281Dialog kwp1281)
    {
        _kwp1281 = kwp1281;
    }
}


================================================
FILE: Cluster/VdoCluster.cs
================================================
using BitFab.KW1281Test.Blocks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace BitFab.KW1281Test.Cluster;

internal class VdoCluster : ICluster
{
    public void UnlockForEepromReadWrite()
    {
        var (isUnlocked, softwareVersion) = Unlock();
        if (!isUnlocked)
        {
            Log.WriteLine("Unknown cluster software version. EEPROM access will likely fail.");
        }

        if (!RequiresSeedKey())
        {
            Log.WriteLine(
                "Cluster is unlocked for ROM/EEPROM access. Skipping Seed/Key login.");
            return;
        }

        SeedKeyAuthenticate(softwareVersion);
        if (RequiresSeedKey())
        {
            Log.WriteLine("Failed to unlock cluster.");
        }
        else
        {
            Log.WriteLine("Cluster is unlocked for ROM/EEPROM access.");
        }
    }

    public string DumpEeprom(
        uint? optionalAddress, uint? optionalLength, string? optionalFileName)
    {
        var address = optionalAddress ?? 0;
        var length = optionalLength ?? 0x800;
        var filename = optionalFileName ?? $"VDO_0x{address:X6}_eeprom.bin";

        DumpEeprom((ushort)address, (ushort)length, maxReadLength: 16, filename);

        return filename;
    }

    /// <summary>
    /// http://www.maltchev.com/kiti/VAG_guide.txt
    /// </summary>
    public Dictionary<int, Block> CustomReadSoftwareVersion()
    {
        var versionBlocks = new Dictionary<int, Block>();

        Log.WriteLine("Sending Custom \"Read Software Version\" blocks");

        // The cluster can return 4 variations of software version, specified by the 2nd byte
        // of the block:
        // 0x00 - Cluster software version
        // 0x01 - Unknown
        // 0x02 - Unknown
        // 0x03 - Unknown
        for (byte variation = 0x00; variation < 0x04; variation++)
        {
            var blocks = SendCustom([0x84, variation]);
            foreach (var block in blocks.Where(b => !b.IsAckNak))
            {
                if (variation is 0x00 or 0x03)
                {
                    Log.WriteLine($"{variation:X2}: {DumpMixedContent(block)}");
                }
                else
                {
                    Log.WriteLine($"{variation:X2}: {DumpBinaryContent(block)}");
                }
                versionBlocks[variation] = block;
            }
        }

        return versionBlocks;
    }

    public void CustomReset()
    {
        Log.WriteLine("Sending Custom Reset block");
        SendCustom([0x82]);
    }

    public List<byte> CustomReadMemory(uint address, byte count)
    {
        Log.WriteLine($"Sending Custom \"Read Memory\" block (Address: ${address:X6}, Count: ${count:X2})");
        var blocks = SendCustom(
        [
            0x86,
            count,
            (byte)(address & 0xFF),
            (byte)((address >> 8) & 0xFF),
            (byte)((address >> 16) & 0xFF),
        ]);
        blocks = blocks.Where(b => !b.IsAckNak).ToList();
        if (blocks.Count != 1)
        {
            // Permissions issue?
            return [];
        }
        return blocks[0].Body.ToList();
    }

    /// <summary>
    /// Read the low 64KB of the cluster's NEC controller ROM.
    /// For MFA clusters, that should cover the entire ROM.
    /// For FIS clusters, the ROM is 128KB and more work is needed to retrieve the high 64KB.
    /// </summary>
    /// <param name="address"></param>
    /// <param name="count"></param>
    /// <returns></returns>
    public List<byte> CustomReadNecRom(ushort address, byte count)
    {
        Log.WriteLine($"Sending Custom \"Read NEC ROM\" block (Address: ${address:X4}, Count: ${count:X2})");
        var blocks = SendCustom(
        [
            0xA6,
            count,
            (byte)(address & 0xFF),
            (byte)((address >> 8) & 0xFF),
        ]);
        blocks = blocks.Where(b => !b.IsAckNak).ToList();
        if (blocks.Count != 1)
        {
            throw new InvalidOperationException($"Custom \"Read NEC ROM\" returned {blocks.Count} blocks instead of 1");
        }
        return blocks[0].Body.ToList();
    }

    public List<byte> MapEeprom()
    {
        // Unlock partial EEPROM read
        Unlock();

        var map = new List<byte>();
        const byte blockSize = 1;
        for (ushort addr = 0; addr < 2048; addr += blockSize)
        {
            var blockBytes = _kwp1281.ReadEeprom(addr, blockSize);
            blockBytes = Enumerable.Repeat(
                blockBytes == null ? (byte)0 : (byte)0xFF,
                blockSize).ToList();
            map.AddRange(blockBytes);
        }

        return map;
    }

    public void DumpMem(string dumpFileName, uint startAddress, uint length)
    {
        const byte blockSize = 15;

        bool succeeded = true;
        using (var fs = File.Create(dumpFileName, blockSize, FileOptions.WriteThrough))
        {
            for (var addr = startAddress; addr < startAddress + length; addr += blockSize)
            {
                var readLength = (byte)Math.Min(startAddress + length - addr, blockSize);
                var blockBytes = CustomReadMemory(addr, readLength);
                if (blockBytes.Count != readLength)
                {
                    succeeded = false;
                    blockBytes.AddRange(
                        Enumerable.Repeat((byte)0, readLength - blockBytes.Count));
                    Log.WriteLine($"{readLength - blockBytes.Count} missing");
                }
                fs.Write(blockBytes.ToArray(), 0, blockBytes.Count);
                fs.Flush();
            }
        }

        if (!succeeded)
        {
            Log.WriteLine();
            Log.WriteLine("**********************************************************************");
            Log.WriteLine("*** Warning: Some bytes could not be read and were replaced with 0 ***");
            Log.WriteLine("**********************************************************************");
            Log.WriteLine();
        }
    }

    private List<Block> SendCustom(List<byte> blockCustomBytes)
    {
        if (blockCustomBytes[0] > 0x80 && !_additionalCustomCommandsUnlocked)
        {
            CustomUnlockAdditionalCommands();
            _additionalCustomCommandsUnlocked = true;
        }

        blockCustomBytes.Insert(0, (byte)BlockTitle.Custom);
        _kwp1281.SendBlock(blockCustomBytes);
        return _kwp1281.ReceiveBlocks();
    }

    public (bool succeeded, string? softwareVersion) Unlock()
    {
        var versionBlocks = CustomReadSoftwareVersion();
        if (versionBlocks.Count == 0)
        {
            Log.WriteLine("Cluster did not return software version.");
            return (succeeded: false, softwareVersion: null);
        }

        // Now we need to send an unlock code that is unique to each ROM version
        Log.WriteLine("Sending Custom \"Unlock partial EEPROM read\" block");
        var softwareVersion = SoftwareVersionToString(versionBlocks[0].Body);
        var unlockCodes = GetClusterUnlockCodes(softwareVersion);
        var unlocked = false;
        foreach (var unlockCode in unlockCodes)
        {
            var unlockCommand = new List<byte> { 0x9D };
            unlockCommand.AddRange(unlockCode);
            var unlockResponse = SendCustom(unlockCommand);
            if (unlockResponse.Count != 1)
            {
                throw new InvalidOperationException(
                    $"Received multiple responses from unlock request.");
            }
            if (unlockResponse[0].IsAck)
            {
                Log.WriteLine(
                    $"Unlock code for software version '{softwareVersion}' is{Utils.Dump(unlockCode)}");
                if (unlockCodes.Length > 1)
                {
                    Log.WriteLine("Please report this to the program author.");
                }
                unlocked = true;
                break;
            }
            else if (!unlockResponse[0].IsNak)
            {
                throw new InvalidOperationException(
                    $"Received non-ACK/NAK ${unlockResponse[0].Title:X2} from unlock request.");
            }
        }
        return (unlocked, softwareVersion);
    }

    private const int MaxAccessLevel = 7;

    /// <summary>
    /// Tries to perform seed/key authentication with cluster.
    /// </summary>
    /// <param name="softwareVersion">Software version string like "VQMJ07LM 09.00"</param>
    public void SeedKeyAuthenticate(string? softwareVersion)
    {
        // Perform Seed/Key authentication
        Log.WriteLine("Sending Custom \"Seed request\" block");
        var response = SendCustom([0x96, 0x01]);

        var responseBlocks = response.Where(b => !b.IsAckNak).ToList();
        if (responseBlocks is [CustomBlock customBlock])
        {
            Log.WriteLine($"Block: {Utils.Dump(customBlock.Body)}");

            var keyBytes = VdoKeyFinder.FindKey(
                customBlock.Body.ToArray(), MaxAccessLevel);

            Log.WriteLine("Sending Custom \"Key response\" block");

            var keyResponse = new List<byte> { 0x96, 0x02 };
            keyResponse.AddRange(keyBytes);

            _ = SendCustom(keyResponse);
        }
    }

    public bool RequiresSeedKey()
    {
        var accessLevel = GetAccessLevel();
        return accessLevel != MaxAccessLevel;
    }

    private int? GetAccessLevel()
    {
        Log.WriteLine("Sending Custom \"Get Access Level\" block");
        var response = SendCustom([0x96, 0x04]);
        var responseBlocks = response.Where(b => !b.IsAckNak).ToList();
        if (responseBlocks is [CustomBlock])
        {
            int accessLevel = responseBlocks[0].Body.First();
            Log.WriteLine($"Access level is {accessLevel}.");

            return accessLevel;
        }
        else
        {
            Log.WriteLine("Access level is unknown.");
            return null;
        }
    }

    /// <summary>
    /// Given a VDO cluster EEPROM dump, attempt to determine the SKC and return it if found.
    /// </summary>
    /// <param name="bytes">A portion of a VDO cluster EEPROM dump.</param>
    /// <param name="startAddress">The start address of bytes within the EEPROM.</param>
    /// <returns>The SKC or null if the SKC could not be determined.</returns>
    public static ushort? GetSkc(byte[] bytes, int startAddress)
    {
        string text = Encoding.ASCII.GetString(bytes);

        // There are several EEPROM formats. We can determine the format by locating the
        // 14-character immobilizer ID and noting its offset in the dump.

        var immoMatch = Regex.Match(
            text,
            @"[A-Z]{2}Z\dZ0[A-Z]\d{7}");
        if (!immoMatch.Success)
        {
            Log.WriteLine("GetSkc: Unable to find Immobilizer ID in cluster dump.");
            return null;
        }

        ushort skc;
        var index = immoMatch.Index + startAddress;

        switch (index)
        {
            case 0x090:
            case 0x0AC:
                // Immo2
                skc = Utils.GetBcd(bytes, 0x0BA - startAddress);
                return skc;
            case 0x0A2:
                // VWK501
                skc = Utils.GetShort(bytes, 0x0CC - startAddress);
                return skc;
            case 0x0E0:
                // VWK503
                skc = Utils.GetShort(bytes, 0x10A - startAddress);
                return skc;
            default:
                Log.WriteLine(
                    $"GetSkc: Unknown EEPROM (Immobilizer offset: 0x{immoMatch.Index:X3})");
                return null;
        }
    }

    /// <summary>
    /// http://www.maltchev.com/kiti/VAG_guide.txt
    /// This unlocks additional custom commands $81-$AF
    /// </summary>
    private void CustomUnlockAdditionalCommands()
    {
        Log.WriteLine("Sending Custom \"Unlock Additional Commands\" block");
        SendCustom([0x80, 0x01, 0x02, 0x03, 0x04]);
    }

    /// <summary>
    /// Different cluster models have different unlock codes. Return the appropriate one based
    /// on the cluster's software version.
    /// </summary>
    internal static byte[][] GetClusterUnlockCodes(string softwareVersion)
    {
        switch(softwareVersion)
        {
            case "VT5P07MH 09.00": // 7H5920872L VDO V03
                return [[0x00, 0x07, 0x43, 0x35]];

            case "VAT500LL 01.00":
            case "VAT500LL 01.20": // 1J0920905L V01
            case "VAT500MH 01.10": // 1J0920925D V06
            case "VAT500MH 01.20": // 1J5920925C V09
                return [[0x01, 0x04, 0x3D, 0x35]];

            case "$01 $00 $14 $01": // 1J0919860B V15
                return [[0x01, 0x08, 0x05, 0x02]];

            case "V798MLA 01.00": // 7D0920800F V01, 1J0919951C V55
                return [[0x02, 0x03, 0x05, 0x09]];

            case "$00 $00 $13 $01": // 8D0919880M D02
                return [[0x09, 0x06, 0x05, 0x02]];

            case "VSQX01LM 01.00": // 6Q0920800 V11
                return [[0x31, 0x39, 0x34, 0x46]];

            case "VCLM09MH $00 $09": // 3BD920848E V03
                return [[0x32, 0x31, 0x36, 0x31]];

            case "VCB07LL  09.00": // 1JD920826E V01
                return [[0x33, 0x34, 0x46, 0x4A]];

            case "VKQ501HH 09.00":
            case "VQMJ07HH 08.40": // 6Y0920843L V04
            case "VQMJ07LM 08.40": // 6Q0920923Q V02
            case "VQMJ07LM 09.00": // 6Q0920804Q V06
                return [[0x34, 0x3F, 0x43, 0x39]];

            case "VQMJ06LM 09.00": // 6Q0920903 V02
                return [[0x35, 0x3D, 0x47, 0x3E]];

            case "SS5501LM 00.80":
            case "SS5501ML 00.80":
                return [[0x36, 0x3B, 0x36, 0x3D]];

            case "VWK501LL 00.88": // 1J0920906L V58
            case "VWK501MH 00.88":
            case "VWK501LL 01.00":
            case "VWK501MH 01.00":
                return [[0x36, 0x3D, 0x3E, 0x47]];

            case "VT5X02LL 09.40":
                return [[0x36, 0x3F, 0x45, 0x42]];

            case "VQMJ09HH 05.10": // 6QE920827C V06
                return [[0x37, 0x42, 0x47, 0x43]];

            case "VWK502MH 09.00":
                return [[0x38, 0x37, 0x3E, 0x31]];

            case "VT5X02LL 09.00":
                return [[0x38, 0x39, 0x3A, 0x47]];

            case "S599CAA  01.00": // 1M0920800C V15
            case "V599HLA  00.91": // 7D0920841A V18
            case "V599LLA  00.91": // 7D0920801B V18
            case "V599LLA  01.00": // 1J0920800L V59
            case "V599LLA  03.00": // 1J0920900J V60
            case "V599MLA  01.00": // 7D0920821D V22
            case "V599MLA  03.00": // 3B0920920B V26
                return [[0x38, 0x3F, 0x40, 0x35]];

            case "MPV300LL 04.00":
            case "MPV501MH 01.00": // 7M3920820H V57
                return [[0x38, 0x47, 0x34, 0x3A]];

            case "VWK501MH 00.92": // 3B0920827C V06
            case "VWK501MH 01.10":
                return [[0x39, 0x34, 0x34, 0x40]];

            case "VBK700LL 00.96":
            case "VBK700LL 01.00":
            case "VBKX00MH 01.00":
                return [[0x3A, 0x39, 0x31, 0x43]];

            case "MPV300LL 02.00":
                return [[0x3B, 0x47, 0x03, 0x02]];

            case "SS5501LM 01.00": // 1M0920802D V05
            case "SS5501ML 01.00":
                return [[0x3C, 0x34, 0x47, 0x35]];

            case "VSQX01LM 01.20":
                return [[0x3D, 0x36, 0x40, 0x36]];

            case "S599CAA  00.80":
                return [[0x3D, 0x39, 0x3B, 0x35]];

            case "KB5M07HH 09.00": // 3U0920842B V06
            case "VWK503LL 09.00":
            case "VWK503MH 09.00": // 1J0920927 V02
                return [[0x3E, 0x35, 0x3D, 0x3A]];

            case "VMMJ08MH 09.00": // 1J5920826L V75
                return [[0x3E, 0x47, 0x3D, 0x48]];

            case "MPV300LL 00.90":
            case "MPV500LL 00.90":
                return [[0x3F, 0x38, 0x43, 0x38]];

            case "SS5500LM 01.00":
                return [[0x40, 0x39, 0x39, 0x38]];

            case "VSQX01LM 01.10": // 6Q0920900 V18
                return [[0x43, 0x43, 0x3D, 0x37]];

            case "MPV300LL 03.00":
                return [[0x43, 0x43, 0x43, 0x39]];

            case "KPQMLA` $01": // 6Y1920860G V12
                return [[0x47, 0x3B, 0x31, 0x3F]];

            case "K5MJ07HH 09.00": // 5J0920840B V92
            case "K5MJ07LM 08.10": // 5J0920810C V2721446
            case "K5MJ07LM 09.00": // 5J0920900B V2823466
                return [[0x47, 0x3F, 0x39, 0x44]];

            default:
                return ClusterUnlockCodes;
        }
    }

    private static string SoftwareVersionToString(List<byte> versionBytes)
    {
        if (versionBytes.Count < 9 || versionBytes.Count > 10)
        {
            return Utils.DumpMixedContent(versionBytes);
        }

        var asciiPart = Encoding.ASCII.GetString(versionBytes.ToArray()[0..^2]);
        return $"{asciiPart} {versionBytes[^1]:X2}.{versionBytes[^2]:X2}";
    }

    internal static readonly byte[][] ClusterUnlockCodes =
    [
        [0x00, 0x00, 0x00, 0x00],
        [0x00, 0x00, 0x03, 0x02],
        [0x00, 0x01, 0x03, 0x02],
        [0x00, 0x02, 0x03, 0x02],
        [0x00, 0x02, 0x09, 0x07],
        [0x00, 0x03, 0x03, 0x02],
        [0x00, 0x03, 0x04, 0x02],
        [0x00, 0x04, 0x03, 0x02],
        [0x00, 0x04, 0x06, 0x07],
        [0x00, 0x05, 0x03, 0x02],
        [0x00, 0x06, 0x03, 0x02],
        [0x00, 0x07, 0x02, 0x04],
        [0x00, 0x07, 0x03, 0x08],
        [0x00, 0x07, 0x43, 0x35],
        [0x00, 0x08, 0x02, 0x04],
        [0x01, 0x00, 0x03, 0x02],
        [0x01, 0x00, 0x09, 0x05],
        [0x01, 0x01, 0x00, 0x04],
        [0x01, 0x01, 0x00, 0x05],
        [0x01, 0x01, 0x00, 0x06],
        [0x01, 0x01, 0x00, 0x07],
        [0x01, 0x01, 0x00, 0x08],
        [0x01, 0x01, 0x00, 0x09],
        [0x01, 0x01, 0x01, 0x00],
        [0x01, 0x01, 0x01, 0x01],
        [0x01, 0x01, 0x01, 0x02],
        [0x01, 0x01, 0x01, 0x03],
        [0x01, 0x01, 0x01, 0x04],
        [0x01, 0x01, 0x01, 0x05],
        [0x01, 0x01, 0x01, 0x06],
        [0x01, 0x01, 0x03, 0x02],
        [0x01, 0x01, 0x03, 0x07],
        [0x01, 0x01, 0x05, 0x08],
        [0x01, 0x01, 0x07, 0x09],
        [0x01, 0x02, 0x03, 0x02],
        [0x01, 0x03, 0x03, 0x02],
        [0x01, 0x04, 0x02, 0x02],
        [0x01, 0x04, 0x03, 0x02],
        [0x01, 0x04, 0x3D, 0x35],
        [0x01, 0x05, 0x03, 0x02],
        [0x01, 0x05, 0x06, 0x08],
        [0x01, 0x05, 0x3D, 0x35],
        [0x01, 0x06, 0x00, 0x02],
        [0x01, 0x06, 0x02, 0x00],
        [0x01, 0x06, 0x03, 0x02],
        [0x01, 0x06, 0x04, 0x02],
        [0x01, 0x07, 0x00, 0x03],
        [0x01, 0x08, 0x02, 0x05],
        [0x01, 0x08, 0x03, 0x00],
        [0x01, 0x08, 0x05, 0x02],
        [0x02, 0x00, 0x03, 0x02],
        [0x02, 0x00, 0x06, 0x01],
        [0x02, 0x01, 0x03, 0x02],
        [0x02, 0x02, 0x03, 0x02],
        [0x02, 0x02, 0x04, 0x01],
        [0x02, 0x02, 0x09, 0x02],
        [0x02, 0x03, 0x03, 0x02],
        [0x02, 0x03, 0x05, 0x09],
        [0x02, 0x04, 0x00, 0x02],
        [0x02, 0x04, 0x03, 0x02],
        [0x02, 0x05, 0x00, 0x02],
        [0x02, 0x05, 0x03, 0x02],
        [0x02, 0x05, 0x06, 0x09],
        [0x02, 0x05, 0x08, 0x01],
        [0x02, 0x06, 0x03, 0x02],
        [0x02, 0x06, 0x06, 0x09],
        [0x02, 0x09, 0x02, 0x06],
        [0x02, 0x09, 0x04, 0x02],
        [0x02, 0x09, 0x04, 0x03],
        [0x02, 0x32, 0x3B, 0x37],
        [0x03, 0x00, 0x03, 0x02],
        [0x03, 0x00, 0x03, 0x07],
        [0x03, 0x00, 0x07, 0x01],
        [0x03, 0x01, 0x03, 0x02],
        [0x03, 0x02, 0x03, 0x02],
        [0x03, 0x02, 0x05, 0x02],
        [0x03, 0x03, 0x03, 0x02],
        [0x03, 0x03, 0x08, 0x04],
        [0x03, 0x03, 0x09, 0x03],
        [0x03, 0x04, 0x03, 0x02],
        [0x03, 0x05, 0x03, 0x02],
        [0x03, 0x06, 0x03, 0x02],
        [0x03, 0x08, 0x02, 0x05],
        [0x04, 0x00, 0x03, 0x02],
        [0x04, 0x01, 0x03, 0x02],
        [0x04, 0x01, 0x03, 0x08],
        [0x04, 0x02, 0x03, 0x02],
        [0x04, 0x02, 0x06, 0x06],
        [0x04, 0x03, 0x03, 0x02],
        [0x04, 0x04, 0x03, 0x02],
        [0x04, 0x04, 0x09, 0x04],
        [0x04, 0x05, 0x03, 0x02],
        [0x04, 0x05, 0x05, 0x02],
        [0x04, 0x06, 0x03, 0x02],
        [0x04, 0x07, 0x00, 0x07],
        [0x05, 0x00, 0x03, 0x02],
        [0x05, 0x01, 0x03, 0x02],
        [0x05, 0x01, 0x04, 0x08],
        [0x05, 0x02, 0x03, 0x02],
        [0x05, 0x02, 0x03, 0x09],
        [0x05, 0x02, 0x09, 0x02],
        [0x05, 0x03, 0x03, 0x02],
        [0x05, 0x04, 0x03, 0x02],
        [0x05, 0x05, 0x03, 0x02],
        [0x05, 0x05, 0x08, 0x09],
        [0x05, 0x05, 0x09, 0x05],
        [0x05, 0x06, 0x03, 0x02],
        [0x05, 0x08, 0x05, 0x02],
        [0x06, 0x00, 0x02, 0x02],
        [0x06, 0x00, 0x03, 0x00],
        [0x06, 0x00, 0x03, 0x02],
        [0x06, 0x01, 0x03, 0x02],
        [0x06, 0x02, 0x03, 0x02],
        [0x06, 0x03, 0x03, 0x02],
        [0x06, 0x04, 0x03, 0x02],
        [0x06, 0x04, 0x07, 0x01],
        [0x06, 0x05, 0x03, 0x02],
        [0x06, 0x06, 0x03, 0x02],
        [0x06, 0x06, 0x09, 0x06],
        [0x06, 0x09, 0x01, 0x02],
        [0x06, 0x09, 0x03, 0x09],
        [0x06, 0x09, 0x05, 0x03],
        [0x07, 0x00, 0x03, 0x02],
        [0x07, 0x00, 0x06, 0x04],
        [0x07, 0x01, 0x03, 0x02],
        [0x07, 0x02, 0x03, 0x02],
        [0x07, 0x03, 0x03, 0x02],
        [0x07, 0x03, 0x05, 0x03],
        [0x07, 0x04, 0x03, 0x02],
        [0x07, 0x05, 0x03, 0x02],
        [0x07, 0x06, 0x03, 0x02],
        [0x07, 0x07, 0x09, 0x04],
        [0x07, 0x07, 0x09, 0x07],
        [0x08, 0x00, 0x03, 0x02],
        [0x08, 0x01, 0x03, 0x02],
        [0x08, 0x01, 0x06, 0x05],
        [0x08, 0x02, 0x01, 0x04],
        [0x08, 0x02, 0x03, 0x02],
        [0x08, 0x02, 0x03, 0x05],
        [0x08, 0x03, 0x03, 0x02],
        [0x08, 0x04, 0x02, 0x02],
        [0x08, 0x04, 0x03, 0x02],
        [0x08, 0x05, 0x03, 0x02],
        [0x08, 0x06, 0x03, 0x02],
        [0x08, 0x06, 0x07, 0x06],
        [0x08, 0x08, 0x09, 0x08],
        [0x09, 0x00, 0x03, 0x02],
        [0x09, 0x01, 0x01, 0x07],
        [0x09, 0x01, 0x03, 0x02],
        [0x09, 0x02, 0x03, 0x02],
        [0x09, 0x02, 0x06, 0x06],
        [0x09, 0x03, 0x03, 0x02],
        [0x09, 0x03, 0x09, 0x06],
        [0x09, 0x04, 0x03, 0x02],
        [0x09, 0x05, 0x02, 0x03],
        [0x09, 0x05, 0x03, 0x02],
        [0x09, 0x05, 0x05, 0x08],
        [0x09, 0x06, 0x03, 0x02],
        [0x09, 0x06, 0x04, 0x09],
        [0x09, 0x06, 0x05, 0x02],
        [0x09, 0x09, 0x03, 0x02],
        [0x09, 0x09, 0x09, 0x09],
        [0x31, 0x39, 0x34, 0x46],
        [0x31, 0x44, 0x35, 0x43],
        [0x32, 0x31, 0x36, 0x31],
        [0x32, 0x37, 0x3E, 0x31],
        [0x33, 0x34, 0x46, 0x4A],
        [0x34, 0x3F, 0x43, 0x39],
        [0x35, 0x3B, 0x39, 0x3D],
        [0x35, 0x3C, 0x31, 0x3C],
        [0x35, 0x3D, 0x04, 0x01],
        [0x35, 0x3D, 0x47, 0x3E],
        [0x35, 0x40, 0x3F, 0x38],
        [0x35, 0x43, 0x31, 0x38],
        [0x35, 0x47, 0x34, 0x3C],
        [0x36, 0x3B, 0x36, 0x3D],
        [0x36, 0x3D, 0x3E, 0x47],
        [0x36, 0x3F, 0x45, 0x42],
        [0x36, 0x40, 0x36, 0x3D],
        [0x37, 0x39, 0x3C, 0x47],
        [0x37, 0x3B, 0x32, 0x02],
        [0x37, 0x3D, 0x43, 0x43],
        [0x37, 0x42, 0x47, 0x43],
        [0x38, 0x34, 0x34, 0x37],
        [0x38, 0x37, 0x3E, 0x31],
        [0x38, 0x39, 0x39, 0x40],
        [0x38, 0x39, 0x3A, 0x47],
        [0x38, 0x3F, 0x40, 0x35],
        [0x38, 0x43, 0x38, 0x3F],
        [0x38, 0x47, 0x34, 0x3A],
        [0x39, 0x34, 0x34, 0x40],
        [0x39, 0x43, 0x43, 0x43],
        [0x3A, 0x31, 0x31, 0x36],
        [0x3A, 0x34, 0x47, 0x38],
        [0x3A, 0x39, 0x31, 0x43],
        [0x3A, 0x39, 0x41, 0x43],
        [0x3A, 0x3B, 0x35, 0x3C],
        [0x3A, 0x3B, 0x35, 0x4C],
        [0x3A, 0x3D, 0x35, 0x3E],
        [0x3B, 0x33, 0x3E, 0x37],
        [0x3B, 0x3A, 0x37, 0x3E],
        [0x3B, 0x46, 0x23, 0x10],
        [0x3B, 0x46, 0x23, 0x1B],
        [0x3B, 0x46, 0x23, 0x1D],
        [0x3B, 0x47, 0x03, 0x02],
        [0x3C, 0x31, 0x3C, 0x35],
        [0x3C, 0x34, 0x47, 0x35],
        [0x3D, 0x36, 0x40, 0x36],
        [0x3D, 0x39, 0x3B, 0x35],
        [0x3E, 0x35, 0x3D, 0x3A],
        [0x3E, 0x35, 0x43, 0x30],
        [0x3E, 0x35, 0x43, 0x39],
        [0x3E, 0x35, 0x43, 0x40],
        [0x3E, 0x35, 0x43, 0x41],
        [0x3E, 0x35, 0x43, 0x42],
        [0x3E, 0x35, 0x43, 0x43],
        [0x3E, 0x35, 0x43, 0x44],
        [0x3E, 0x39, 0x31, 0x43],
        [0x3E, 0x39, 0x35, 0x40],
        [0x3E, 0x39, 0x43, 0x34],
        [0x3E, 0x3F, 0x40, 0x35],
        [0x3E, 0x47, 0x3D, 0x48],
        [0x3F, 0x31, 0x3B, 0x47],
        [0x3F, 0x38, 0x43, 0x38],
        [0x3F, 0x43, 0x35, 0x3E],
        [0x40, 0x30, 0x3E, 0x39],
        [0x40, 0x34, 0x34, 0x39],
        [0x40, 0x39, 0x39, 0x38],
        [0x40, 0x43, 0x35, 0x3E],
        [0x41, 0x43, 0x35, 0x3E],
        [0x42, 0x43, 0x35, 0x3E],
        [0x42, 0x45, 0x3F, 0x36],
        [0x43, 0x31, 0x39, 0x3A],
        [0x43, 0x43, 0x35, 0x3E],
        [0x43, 0x43, 0x3D, 0x37],
        [0x43, 0x43, 0x43, 0x39],
        [0x43, 0x45, 0x31, 0x3D],
        [0x44, 0x43, 0x35, 0x3E],
        [0x45, 0x39, 0x34, 0x43],
        [0x47, 0x3A, 0x39, 0x38],
        [0x47, 0x3B, 0x31, 0x3F],
        [0x47, 0x3C, 0x39, 0x37],
        [0x47, 0x3E, 0x3D, 0x36],
        [0x47, 0x3F, 0x39, 0x44],
    ];

    private static string DumpMixedContent(Block block)
    {
        if (block.IsNak)
        {
            return "NAK";
        }

        return Utils.DumpMixedContent(block.Body);
    }

    private static string DumpBinaryContent(Block block)
    {
        if (block.IsNak)
        {
            return "NAK";
        }

        return Utils.DumpBytes(block.Body);
    }

    private void DumpEeprom(
        ushort startAddr, ushort length, byte maxReadLength, string fileName)
    {
        bool succeeded = true;

        using (var fs = File.Create(fileName, maxReadLength, FileOptions.WriteThrough))
        {
            for (uint addr = startAddr; addr < (startAddr + length); addr += maxReadLength)
            {
                byte readLength = (byte)Math.Min(startAddr + length - addr, maxReadLength);
                List<byte>? blockBytes = _kwp1281.ReadEeprom((ushort)addr, readLength);
                if (blockBytes == null)
                {
                    blockBytes = Enumerable.Repeat((byte)0, readLength).ToList();
                    succeeded = false;
                }
                fs.Write(blockBytes.ToArray(), 0, blockBytes.Count);
                fs.Flush();
            }
        }

        if (!succeeded)
        {
            Log.WriteLine();
            Log.WriteLine("**********************************************************************");
            Log.WriteLine("*** Warning: Some bytes could not be read and were replaced with 0 ***");
            Log.WriteLine("**********************************************************************");
            Log.WriteLine();
        }
    }

    public void WriteRam(ushort address, byte value)
    {
        Log.WriteLine("Sending Custom \"Write RAM\" block");

        SendCustom(
            [
                0x87,
                1, // Count
                (byte)(address & 0xFF),
                (byte)((address >> 8) & 0xFF),
                value
            ]);
    }

    private readonly IKW1281Dialog _kwp1281;
    private bool _additionalCustomCommandsUnlocked;

    public VdoCluster(IKW1281Dialog kwp1281)
    {
        _kwp1281 = kwp1281;
        _additionalCustomCommandsUnlocked = false;
    }
}


================================================
FILE: Cluster/VdoKeyFinder.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;

namespace BitFab.KW1281Test.Cluster
{
    public static class VdoKeyFinder
    {
        /// <summary>
        /// Takes a 10-byte seed block, desired access level and optional cluster software version and generates an
        /// 8-byte key block.
        /// </summary>
        public static byte[] FindKey(
            byte[] seed, int accessLevel)
        {
            if (seed.Length != 10)
            {
                throw new InvalidOperationException(
                    $"Unexpected seed length: {seed.Length} (Expected 10)");
            }

            byte[] secret;
            switch (seed[8])
            {
                case 0x01 when seed[9] == 0x00:
                    secret = Secrets0100[accessLevel];
                    break;
                case 0x03 when seed[9] == 0x00:
                    secret = Secrets0300[accessLevel];
                    break;
                case 0x09 when seed[9] == 0x00:
                    secret = Secrets0900[accessLevel];
                    break;
                case 0x0B when seed[9] == 0x00:
                    secret = Secrets0B00[accessLevel];
                    break;
                case 0x0D when seed[9] == 0x00:
                    secret = Secrets0D00[accessLevel];
                    break;
                default:
                    Log.WriteLine(
                        $"Unexpected seed suffix: ${seed[8]:X2} ${seed[9]:X2}");
                    secret = Secrets0100[accessLevel]; // Try something
                    break;
            }

            Log.WriteLine($"Access level {accessLevel} secret: {Utils.DumpBytes(secret)}");

            var key = CalculateKey(
                [seed[1], seed[3], seed[5], seed[7]],
                secret);

            return [(byte)accessLevel, key[0], key[1], 0x00, key[2], 0x00, key[3]];
        }

        /// <summary>
        /// Table of secrets, one for each access level.
        /// </summary>
        private static readonly byte[][] Secrets0100 =
        [
            [0xe5, 0x7c, 0x20, 0xb3],   // AccessLevel 0
            [0x67, 0xb8, 0xf0, 0xe2],
            [0x59, 0xd0, 0x4f, 0xcb],
            [0x46, 0x83, 0xb6, 0x27],
            [0xc9, 0xde, 0xe3, 0xca],
            [0x7f, 0x50, 0x44, 0xbc],
            [0x4b, 0xd0, 0x7f, 0xad],
            [0x55, 0x16, 0xa8, 0x94]    // AccessLevel 7
        ];

        /// <summary>
        /// Table of secrets, one for each access level.
        /// </summary>
        private static readonly byte[][] Secrets0300 =
        [
            [0x4c, 0x29, 0x92, 0x1b],   // AccessLevel 0
            [0x42, 0x0a, 0x0b, 0x66],
            [0x1c, 0x4c, 0x91, 0x4d],
            [0xe2, 0xfd, 0xa2, 0x28],
            [0x48, 0x34, 0x58, 0x71],
            [0xb1, 0xf5, 0xd0, 0xb8],
            [0xac, 0xfc, 0x5e, 0x6c],
            [0x98, 0xe1, 0x56, 0x5f]    // AccessLevel 7
        ];

        /// <summary>
        /// Table of secrets, one for each access level.
        /// </summary>
        private static readonly byte[][] Secrets0900 =
        [
            [0xa7, 0xd2, 0xe9, 0x8d],  // AccessLevel 0
            [0xe6, 0xfa, 0x9e, 0xba],
            [0x63, 0x92, 0xe3, 0x08],
            [0x55, 0x3e, 0x68, 0x24],
            [0x03, 0x2a, 0x70, 0xdc],
            [0xe7, 0xb4, 0x71, 0x86],
            [0x4f, 0x58, 0xcd, 0x81],
            [0xfd, 0x8e, 0x31, 0x96]    // AccessLevel 7
        ];

        /// <summary>
        /// Table of secrets, one for each access level.
        /// </summary>
        private static readonly byte[][] Secrets0D00 =
        [
            [0xc9, 0x18, 0xe6, 0x6e],  // AccessLevel 0
            [0x69, 0xc3, 0x08, 0xcd],
            [0x37, 0x15, 0xd3, 0x23],
            [0xe1, 0xe1, 0xa9, 0x3b],
            [0x19, 0x74, 0x72, 0x18],
            [0x08, 0x2b, 0x49, 0x1a],
            [0x82, 0xd1, 0x7d, 0x50],
            [0x0a, 0x5b, 0x41, 0x4f]    // AccessLevel 7
        ];

        private static readonly byte[][] Secrets0B00 =
        [
            [0x47, 0x36, 0x9a, 0xbb],   // AccessLevel 0
            [0xad, 0x4e, 0x61, 0x44],
            [0xd3, 0xd6, 0x42, 0x59],
            [0x13, 0x6f, 0x43, 0x74],
            [0xfc, 0xb8, 0x59, 0x2e],
            [0x09, 0x58, 0x9d, 0x7f],
            [0x24, 0x27, 0xc3, 0x9d],
            [0x87, 0xed, 0x34, 0x63]    // AccessLevel 7
        ];

        /// <summary>
        /// Takes a 4-byte seed and calculates a 4-byte key.
        /// </summary>
        private static byte[] CalculateKey(
            IReadOnlyList<byte> seed,
            IReadOnlyList<byte> secret)
        {
            var work = new byte[] { seed[0], seed[1], seed[2], seed[3], 0x00, 0x00 };
            var secretBuf = secret.ToArray();

            Scramble(work);

            var y = work[0] & 0x07;
            var temp = y + 1;

            var a = LeftRotate(0x01, y);

            do
            {
                var set = ((secretBuf[0] ^ secretBuf[1] ^ secretBuf[2] ^ secretBuf[3]) & 0x40) != 0;
                secretBuf[3] = SetOrClearBits(secretBuf[3], a, set);

                RightRotateFirst4Bytes(secretBuf, 0x01);
                temp--;
            }
            while (temp != 0);

            for (var x = 0; x < 2; x++)
            {
                work[4] = work[0];
                work[0] ^= work[2];

                work[5] = work[1];
                work[1] ^= work[3];

                work[3] = work[5];
                work[2] = work[4];

                LeftRotateFirstTwoBytes(work, work[2] & 0x07);

                y = x << 1;

                var carry = true;
                (work[0], carry) = Utils.SubtractWithCarry(work[0], secretBuf[y], carry);
                (work[1], _) = Utils.SubtractWithCarry(work[1], secretBuf[y + 1], carry);
            }

            Scramble(work);

            return [work[0], work[1], work[2], work[3]];
        }

        private static void Scramble(byte[] work)
        {
            work[4] = work[0];
            work[0] = work[1];
            work[1] = work[3];
            work[3] = work[2];
            work[2] = work[4];
        }

        private static byte SetOrClearBits(
            byte value, byte mask, bool set)
        {
            if (set)
            {
                return (byte)(value | mask);
            }
            else
            {
                return (byte)(value & (byte)(mask ^ 0xFF));
            }
        }

        /// <summary>
        /// Right-Rotate the first 4 bytes of a buffer count times.
        /// </summary>
        private static void RightRotateFirst4Bytes(
            byte[] buf, int count)
        {
            while (count != 0)
            {
                var carry = (buf[0] & 0x01) != 0;
                (buf[3], carry) = Utils.RightRotate(buf[3], carry);
                (buf[2], carry) = Utils.RightRotate(buf[2], carry);
                (buf[1], carry) = Utils.RightRotate(buf[1], carry);
                (buf[0], _) = Utils.RightRotate(buf[0], carry);
                count--;
            }
        }

        private static void LeftRotateFirstTwoBytes(
            byte[] work, int count)
        {
            while (count > 0)
            {
                var carry = (work[1] & 0x80) != 0;
                (work[0], carry) = Utils.LeftRotate(work[0], carry);
                (work[1], _) = Utils.LeftRotate(work[1], carry);
                count--;
            }
        }

        /// <summary>
        /// Left-Rotate a value count-times.
        /// </summary>
        private static byte LeftRotate(
            byte value, int count)
        {
            while (count != 0)
            {
                var carry = (value & 0x80) != 0;
                (value, _) = Utils.LeftRotate(value, carry);
                count--;
            }

            return value;
        }
    }
}


================================================
FILE: ControllerAddress.cs
================================================
namespace BitFab.KW1281Test
{
    /// <summary>
    /// VW controller addresses
    /// </summary>
    enum ControllerAddress
    {
        Ecu = 0x01,
        CentralElectric = 0x09,
        Cluster = 0x17,
        CanGateway = 0x19,
        Immobilizer = 0x25,
        CentralLocking = 0x35,
        Navigation = 0x37,
        CCM = 0x46,
        Radio = 0x56,
        RadioManufacturing = 0x7C,
    }
}


================================================
FILE: ControllerIdent.cs
================================================
using BitFab.KW1281Test.Blocks;
using System;
using System.Collections.Generic;
using System.Text;

namespace BitFab.KW1281Test
{
    /// <summary>
    /// The info returned by the controller to a ReadIdent block.
    /// </summary>
    internal class ControllerIdent
    {
        public ControllerIdent(IEnumerable<Block> blocks)
        {
            var sb = new StringBuilder();
            foreach (var block in blocks)
            {
                if (block is AsciiDataBlock asciiBlock)
                {
                    sb.Append(asciiBlock);
                }
                else if (block is CodingWscBlock codingWscBlock)
                {
                    sb.AppendLine();
                    sb.Append(codingWscBlock);
                }
                else
                {
                    Log.WriteLine($"ReadIdent returned block of type {block.GetType()}");
                }
            }
            Text = sb.ToString();
        }

        public string Text { get; }

        public override string ToString()
        {
            return Text;
        }
    }
}

================================================
FILE: ControllerInfo.cs
================================================
using BitFab.KW1281Test.Blocks;
using System;
using System.Collections.Generic;
using System.Text;

namespace BitFab.KW1281Test
{
    /// <summary>
    /// The info returned when a controller wakes up.
    /// </summary>
    internal class ControllerInfo
    {
        public ControllerInfo(IEnumerable<Block> blocks)
        {
            var sb = new StringBuilder();
            foreach (var block in blocks)
            {
                if (block is AsciiDataBlock asciiBlock)
                {
                    sb.Append(asciiBlock);
                    if (asciiBlock.MoreDataAvailable)
                    {
                        MoreDataAvailable = true;
                    }
                }
                else if (block is CodingWscBlock codingBlock)
                {
                    sb.Append($"{Environment.NewLine}{codingBlock}");
                    SoftwareCoding = codingBlock.SoftwareCoding;
                    WorkshopCode = codingBlock.WorkshopCode;
                }
                else
                {
                    Log.WriteLine($"Controller wakeup returned block of type {block.GetType()}");
                }
            }
            Text = sb.ToString();
        }

        public string Text { get; }

        public bool MoreDataAvailable { get; }

        public int SoftwareCoding { get; }

        public int WorkshopCode { get; }

        public override string ToString()
        {
            return Text;
        }
    }
}


================================================
FILE: EDC15/Edc15VM.cs
================================================
using BitFab.KW1281Test.Kwp2000;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;

namespace BitFab.KW1281Test.EDC15
{
    public class Edc15VM
    {
        public byte[] ReadWriteEeprom(
            string filename,
            List<KeyValuePair<ushort, byte>>? addressValuePairs = null)
        {
            addressValuePairs ??= [];

            var kwp2000 = new KW2000Dialog(_kwpCommon, (byte)_controllerAddress);

            _ = kwp2000.SendReceive(DiagnosticService.startDiagnosticSession, [0x89]);

            _ = kwp2000.SendReceive(DiagnosticService.startDiagnosticSession, [0x85]);

            const byte accMod = 0x41;
            var resp = kwp2000.SendReceive(DiagnosticService.securityAccess, [accMod]);

            // ECU normally doesn't require seed/key authentication the first time it wakes up in
            // KWP2000 mode so sending an empty key is sufficient.
            var buf = new List<byte> { accMod + 1 };

            if (!resp.Body.SequenceEqual(new byte[] { accMod, 0x00, 0x00 }))
            {
                // Normally we'll only get here if we wake up the ECU and it's already in KWP2000 mode,
                // which can happen if a previous download attempt did not complete. In that case we
                // need to calculate and send back a real key.
                var seedBuf = resp.Body.Skip(1).Take(4).ToArray();
                var keyBuf = LVL41Auth(0x508DA647, 0x3800000, seedBuf);

                buf.AddRange(keyBuf);
            }
            _ = kwp2000.SendReceive(DiagnosticService.securityAccess, buf.ToArray());

            var loader = Edc15VM.GetLoader();
            var len = loader.Length;

            // Ask the ECU to accept our loader and store it in RAM
            _ = kwp2000.SendReceive(DiagnosticService.requestDownload, [
                0x40, 0xE0, 0x00, // Load address 0x40E000
                0x00, // Not compressed, not encrypted
                (byte)(len >> 16), (byte)(len >> 8), (byte)(len & 0xFF) // Length
                ],
            excludeAddresses: true);

            // Break the loader into blocks and send each one
            var maxBlockLen = resp.Body[0];
            var s = new MemoryStream(loader);
            while (true)
            {
                Thread.Sleep(5);

                var blockBytes = new byte[maxBlockLen];
                var readCount = s.Read(blockBytes, 0, maxBlockLen - 1);
                if (readCount == 0)
                {
                    break;
                }

                _ = kwp2000.SendReceive(
                    DiagnosticService.transferData, blockBytes.Take(readCount).ToArray(),
                    excludeAddresses: true);
            }

            // Ask the ECU to execute our loader
            kwp2000.SendMessage(
                DiagnosticService.startRoutineByLocalIdentifier, [0x02],
                excludeAddresses: true);
            _ = kwp2000.ReceiveMessage();

            // Custom loader command to send all 512 bytes of the EEPROM
            kwp2000.SendMessage(
                (DiagnosticService)0xA6, [],
                excludeAddresses: true);
            resp = kwp2000.ReceiveMessage();
            if (!resp.IsPositiveResponse(DiagnosticService.transferData))
            {
                throw new InvalidOperationException($"Dump EEPROM failed.");
            }

            var eeprom = new byte[512];
            for (var i = 0; i < 512; i++)
            {
                eeprom[i] = _kwpCommon.Interface.ReadByte();
            }

            File.WriteAllBytes(filename, eeprom);
            Log.WriteLine($"Saved EEPROM to {filename}");

            _ = kwp2000.ReceiveMessage();

            // Now write any supplied values
            foreach (var addressValuePair in addressValuePairs)
            {
                var service = (DiagnosticService)(
                    addressValuePair.Key > 0xFF
                        ? 0xA8  // Write 1 byte to EEPROM (Page 1)
                        : 0xA7); // Write 1 byte to EEPROM (Page 0)

                kwp2000.SendMessage(
                    service, [],
                    excludeAddresses: true);
                resp = kwp2000.ReceiveMessage();
                if (!resp.IsPositiveResponse(DiagnosticService.transferData))
                {
                    throw new InvalidOperationException($"Write EEPROM failed.");
                }

                var address = (byte)(addressValuePair.Key & 0xFF);
                var value = addressValuePair.Value;

                _kwpCommon.WriteByte(address);
                _kwpCommon.WriteByte(value);
                Log.WriteLine($"Sent: {address:X2} {value:X2}");

                resp = kwp2000.ReceiveMessage();
                if (!resp.IsPositiveResponse(DiagnosticService.transferData))
                {
                    throw new InvalidOperationException($"Write EEPROM failed.");
                }
            }

            // Custom loader command to reboot the ECU to return it to normal operation.
            kwp2000.SendMessage(
                    (DiagnosticService)0xA2, [],
                excludeAddresses: true);
            _ = kwp2000.ReceiveMessage();

            var b = _kwpCommon.Interface.ReadByte();
            if (b == 0x55)
            {
                Log.WriteLine($"Reboot successful!");
            }

            return eeprom;
        }

        public static void DisplayEepromInfo(ReadOnlySpan<byte> eeprom)
        {
            var skc = Utils.GetShort(eeprom, 0x12E);
            Log.WriteLine($"SKC: {skc:D5}");

            double odometerKm =
                eeprom[0x1BF] +
                (eeprom[0x1C0] << 8) +
                (eeprom[0x1C1] << 16) +
                ((eeprom[0x1C2] & 0x3F) << 24);
            odometerKm /= 100.0;
            Log.WriteLine($"Odometer: {odometerKm} km");

            var vin = Utils.DumpAscii(eeprom.Slice(0x140, 17).ToArray());
            Log.WriteLine($"VIN: {vin}");

            var immoNumber = Utils.DumpAscii(eeprom.Slice(0x131, 14).ToArray());
            Log.WriteLine($"Immo Number: {immoNumber}");

            var immoId = Utils.DumpBytes(eeprom.Slice(0x126, 7).ToArray());
            Log.WriteLine($"Immo Id: {immoId}");

            const ushort immo1Addr = 0x1B0;
            var immo1 = eeprom[immo1Addr];
            const ushort immo2Addr = 0x1DE;
            var immo2 = eeprom[immo2Addr];
            var immoStatus = immo1 == 0x60 && immo2 == 0x60 ? "Off" : "On";
            Log.WriteLine($"Immo is {immoStatus} (${immo1Addr:X3}=${immo1:X2}, ${immo2Addr:X3}=${immo2:X2})");
        }

        /// <summary>
        /// This algorithm borrowed from https://github.com/fjvva/ecu-tool
        /// Thanks to Javier Vazquez Vidal https://github.com/fjvva
        /// </summary>
        private static byte[] LVL41Auth(long key, long key3, byte[] buf)
        {
            // long Key3 = 0x3800000;
            long tempstring = buf[0];
            tempstring <<= 8;
            var keyread1 = tempstring + buf[1];
            tempstring = buf[2];
            tempstring <<= 8;
            var keyread2 = tempstring + buf[3];
            // Process the algorithm
            var key2 = key;
            key2 &= 0xFFFF;
            key >>= 16;
            var key1 = key;
            for (byte counter = 0; counter < 5; counter++)
            {
                var keyTemp = keyread1;
                keyTemp &= 0x8000;
                keyread1 <<= 1;
                var temp1 = keyTemp & 0x0FFFF;
                if (temp1 == 0)
                {
                    var temp2 = keyread2 & 0xFFFF;
                    var temp3 = keyTemp & 0xFFFF0000;
                    keyTemp = temp2 + temp3;
                    keyread1 &= 0xFFFE;
                    temp2 = keyTemp & 0xFFFF;
                    temp2 >>= 0x0F;
                    keyTemp &= 0xFFFF0000;
                    keyTemp += temp2;
                    keyread1 |= keyTemp;
                    keyread2 <<= 0x01;
                }
                else
                {
                    keyTemp = keyread2 + keyread2;
                    keyread1 &= 0xFFFE;
                    var temp2 = keyTemp & 0xFF;
                    temp2 |= 1;
                    var temp3 = key3 & 0xFFFFFF00;
                    key3 = temp2 + temp3;
                    key3 &= 0xFFFF00FF;
                    key3 |= keyTemp;
                    temp2 = keyread2 & 0xFFFF;
                    temp3 = keyTemp & 0xFFFF0000;
                    keyTemp = temp2 + temp3;
                    temp2 = keyTemp & 0xFFFF;
                    temp2 >>= 0x0F;
                    keyTemp &= 0xFFFF0000;
                    keyTemp += temp2;
                    keyTemp |= keyread1;
                    key3 ^= key1;
                    keyTemp ^= key2;
                    keyread2 = key3;
                    keyread1 = keyTemp;
                }
            }
            //Done with the key generation
            keyread2 &= 0xFFFF; // Clean first and second word from garbage
            keyread1 &= 0xFFFF;

            var keybuf = new byte[4];
            keybuf[1] = (byte)keyread1;
            keyread1 >>= 8;
            keybuf[0] = (byte)keyread1;
            keybuf[3] = (byte)keyread2;
            keyread2 >>= 8;
            keybuf[2] = (byte)keyread2;

            return keybuf;
        }

        /// <summary>
        /// Loader that can read/write the serial EEPROM.
        /// </summary>
        private static byte[] GetLoader()
        {
            var assembly = Assembly.GetEntryAssembly()!;
            var resourceStream = assembly.GetManifestResourceStream(
                "BitFab.KW1281Test.EDC15.Loader.bin");
            if (resourceStream == null)
            {
                throw new InvalidOperationException(
                    $"Unable to load BitFab.KW1281Test.EDC15.Loader.bin embedded resource.");
            }

            var loaderLength = resourceStream.Length + 4; // Add 4 bytes for checksum correction
            loaderLength = (loaderLength + 7) / 8 * 8; // Round up to a multiple of 8 bytes
            var buf = new byte[loaderLength];

            resourceStream.ReadExactly(buf, 0, (int)resourceStream.Length);

            // In order for this loader to be executed by the ECU, the checksum of all the bytes
            // must be EFCD8631.

            // Patch the loader with the location of the end (actually 1 byte past the end)
            ushort loaderEnd = (ushort)(0xE000 + loaderLength);
            buf[0x0E] = (byte)(loaderEnd & 0xFF);
            buf[0x0F] = (byte)(loaderEnd >> 8);

            // Take the checksum of the loader up to but not including the checksum correction
            ushort r6 = 0xEFCD;
            ushort r1 = 0x8631;
            Checksum(ref r6, ref r1, buf.Take(buf.Length - 4).ToArray());

            // Calculate the checksum correction bytes and insert them at the end of the loader
            var padding = CalcPadding(r6, r1);
            Array.Copy(padding, 0, buf, buf.Length - 4, 4);

            return buf;
        }

        /// <summary>
        /// Calculate the checksum correction padding needed to result in a checksum of EFCD8631
        /// </summary>
        /// <param name="r6"></param>
        /// <param name="r1"></param>
        /// <returns></returns>
        private static byte[] CalcPadding(ushort r6, ushort r1)
        {
            var paddingH = (ushort)(0xDF9B ^ r6);
            var paddingL = (ushort)(r1 - 0xAB85);

            return
            [
                (byte)(paddingL & 0xFF),
                (byte)(paddingL >> 8),
                (byte)(paddingH & 0xFF),
                (byte)(paddingH >> 8)
            ];
        }

        /// <summary>
        /// EDC15 checksum algorithm (sub_1584).
        /// Calculates a 32-bit checksum of an array of bytes based on an initial 32-bit seed.
        /// Based on https://www.ecuconnections.com/forum/viewtopic.php?f=211&t=49704&sid=5cf324c44d2c74d372984f428ffea5ed
        /// </summary>
        /// <param name="r6">Input: High word of seed, Output: High word of checksum</param>
        /// <param name="r1">Input: Low word of seed, Output: Low word of checksum</param>
        /// <param name="buf">Buffer to calculate checksum for</param>
        static void Checksum(ref ushort r6, ref ushort r1, byte[] buf)
        {
            int r3 = 0; // Buffer index
            int r0 = buf.Length;
            while (true)
            {
                r1 ^= GetBuf(buf, r3); r3 += 2;
                r1 = Rol(r1, r6, out ushort c);
                r6 = (ushort)(r6 - GetBuf(buf, r3) - c); r3 += 2;
                r6 ^= r1;
                if (r3 >= r0)
                {
                    break;
                }

                r1 = (ushort)(r1 - GetBuf(buf, r3) - 1); r3 += 2;
                r1 += 0xDAAD;
                r6 ^= GetBuf(buf, r3); r3 += 2;
                r6 = Ror(r6, r1);
                if (r3 >= r0)
                {
                    break;
                }
            }
        }

        /// <summary>
        /// Rotates a 16-bit value right by count bits.
        /// </summary>
        private static ushort Ror(ushort value, ushort count)
        {
            count &= 0xF;
            value = (ushort)((value >> count) | (value << (16 - count)));
            return value;
        }

        /// <summary>
        /// Rotates a 16-bit value left by count bits. Carry will be equal to the last bit rotated
        /// or 0 if the low 4 bits of count are 0;
        /// </summary>
        private static ushort Rol(ushort value, ushort count, out ushort carry)
        {
            count &= 0xF;
            value = (ushort)((value << count) | (value >> (16 - count)));
            carry = ((value & 1) == 0 || (count == 0)) ? (ushort)0 : (ushort)1;
            return value;
        }

        private static ushort GetBuf(byte[] buf, int ix)
        {
            return (ushort)(buf[ix] + (buf[ix + 1] << 8));
        }

        private readonly IKwpCommon _kwpCommon;
        private readonly int _controllerAddress;

        public Edc15VM(IKwpCommon kwpCommon, int controllerAddress)
        {
            _kwpCommon = kwpCommon;
            _controllerAddress = controllerAddress;
        }
    }
}


================================================
FILE: EDC15/Loader.a66
================================================
; Custom loader. When loaded at address 40E000 and started via startRoutineByLocalIdentifier 0x02,
; it will accept several custom KWP2000-like commands via the K-Line to allow dumping the EEPROM
; and other special functions.

; The following Linux command will convert the Intel Hex-86 format output to raw binary:
; srec_cat Loader.H86 -Intel -Output Loader.bin -Binary

$M167
$NOLI
$INCLUDE (REG167.INC)
$LI

All		SECTION CODE AT 0E000H
		DB		0A5H, 0A5H, 14H, 0E0H, 00H, 00H, 3CH, 0E0H
		DB		00H, 00H
		DW		RoutineStart
		DB		00H, 00H
		DW		RoutineEnd
		DB		00H, 00H, 02H, 47H, 13H, 00H, 00H, 01H
		DB		00H, 02H, 00H, 03H, 00H, 04H, 00H, 05H
		DB		00H, 06H, 00H, 07H, 00H, 08H, 00H, 09H
		DB		00H, 0AH, 00H, 0BH, 00H, 0CH, 00H, 0DH
		DB		00H, 0EH, 00H, 0FH, 80H, 0FH, 0A0H, 0FH
		DB		0C0H, 0FH, 00H, 10H, 45H, 2FH, 7DH, 64H
		DB		9BH, 0C3H

; Entry
RoutineStart	PROC FAR
		BCLR	IEN			; Disable interrupts
		BSET    S0REN		; Receiver enabled
		MOV		R0,#0E7FEH	; "SP"
		
		; Receive and dispatch message
E04A:	CALL    E128	; Receive message

		CMP     R6,#1		; Timeout?
		JMPR    CC_Z,E0C6

		CMP     R6,#2 ; Bad checksum?
		JMPR    CC_Z,E082 ; Send NAK
		
		MOV     R1,#0E600H ; Receive buffer
		MOVB    RL2,[R1+#0001H] ; Service => RL2
		
		CMPB    RL2,#0023H ; 23: Read flash
		JMPR    CC_Z,Cmd23

		CMPB    RL2,#0036H ; 36: Program flash
		JMPR    CC_Z,Cmd36
		
		CMPB    RL2,#00A2H ; A2: Reboot
		JMPR    CC_Z,CmdA2
		
		CMPB    RL2,#00A3H ; A3: Respond with 0x55
		JMPR    CC_Z,CmdA3

		CMPB    RL2,#00A4H ; A4: Set baud rate
		JMPR    CC_Z,CmdA4
	
		CMPB    RL2,#00A5H ; A5: Erase flash
		JMPR    CC_Z,CmdA5
		
		CMPB    RL2,#00A6H ; A6: Dump EEPROM
		JMPR    CC_Z,CmdA6

		CMPB    RL2,#00A7H ; A7: Write 1 byte to EEPROM (Page 0)
		JMPR    CC_Z,CmdA7

		CMPB    RL2,#00A8H ; A8: Write 1 byte to EEPROM (Page 1)
		JMPR    CC_Z,CmdA8

E082:	CALL   	SendNAK
		JMPR    CC_UC,E04A ; Receive and dispatch message

; Read flash
Cmd23:	CALL    E23E
		JMPR    CC_UC,E04A ; Receive and dispatch message

; Program flash
Cmd36:	CALL    E1C4 ; Program flash
Cmd36x:	CALL    SendAck ; Send ACK
		JMPR    CC_UC,E04A ; Receive and dispatch message

; Reboot
CmdA2:	CALL	SendACK
		CALL   	DELAY256
		MOVB    RL7,#0055H
		CALL	XmitRL7
		SRST    ; Reboot

; Respond with 0x55
CmdA3:	MOVB    RL7,#0055H
CmdA3x:	CALL    XmitRL7
		JMPR    CC_UC,E04A ; Receive and dispatch message

; Set baud rate (buf[2] => S0BG)
CmdA4:	CALL    SendACK
		CALL    Delay256
		MOV     R2,#00H
		MOVB    RL2,[R1+#0002H]
		MOV     S0BG,R2
		MOVB    RL7,#00AAH
		JMPR    CC_UC,CmdA3x ; XmitRL7 + jump to dispatcher

; Erase flash
CmdA5:	CALL    SendACK
		CALL    E2E6 ; Erase flash
		JMPR    CC_UC,Cmd36x ; SendACK + jump to dispatcher

; Dump EEPROM
CmdA6:	CALL	SendACK
		CALL   	E1C6 ; Dump EEPROM
		JMPR    CC_UC,Cmd36x ; SendACK + jump to dispatcher

; Write 1 byte to EEPROM (Page 0)
CmdA7:	CALL	SendACK
		CALL   	E324 ; Write 1 byte to EEPROM (Page 0)
		JMPR    CC_UC,Cmd36x ; SendACK + jump to dispatcher

; Write 1 byte to EEPROM (Page 1)
CmdA8:	CALL	SendACK
		CALL    E356
		JMPR    CC_UC,Cmd36x ; SendACK + jump to dispatcher

; Handle timeout?
E0C6:	MOVB    RL7,[R1]
		ADDB    RL7,#1
		JMPR    CC_UC,CmdA3x ; XmitRL7 + jump to dispatcher

RoutineStart	ENDP

; Send NAK
SendNAK	PROC NEAR
		MOV     [-R0],R1 ; Push R1
		MOV     [-R0],R2 ; Push R2
		MOV     R1,#0E600H ; Transmit buf
		MOVB    RL2,#01H ; Length 1
		MOVB    [R1],RL2
		MOVB    RL2,#007FH ; Code 7F (NAK)
		JMPR	CC_UC,SendACK2
SendNAK	ENDP

; Send ACK
SendACK	PROC NEAR
		MOV     [-R0],R1 ; Push R1
		MOV     [-R0],R2 ; Push R2
		MOV     R1,#0E600H ; Transmit buf
		MOVB    RL2,#01H
		MOVB    [R1],RL2
		MOVB    RL2,#0076H ; Code 76 (transferData positive response)
SendACK2:		
		MOVB    [R1+#0001H],RL2
		CALL	E15E ; Send message
		JMPR    CC_UC,E156x ; Pop R2,R1 + RET
SendACK	ENDP

; Receive message into buf (E600) - Status returned in R6 (0: Success, 1: Timeout, 2: Checksum error)
; This code has a bug where it does not initialize R6 to 0 and doesn't call read-with-timeout so 1 will never be returned.
E128	PROC NEAR
		MOV		[-R0],R1 ; Push R1
		MOV     [-R0],R2 ; Push R2
		MOV     [-R0],R3 ; Push R3
		MOV     R1,#0E600H ; Receive buf
		CALL    E192 ; Receive a byte in R7
		MOV     R2,#00H ; 0 => R2
		MOVB    RL2,RL7 ; RL7 => RL2 (RL2 is message length)
		MOVB    RL3,RL7 ; RL7 => RL3 (RL3 is message checksum)
		MOVB    [R1],RL7 ; Put received byte in buf
		ADD     R1,#1 ; Advance to next buf location
E140:	CALL    E1A2 ; Receive a byte in R7. R6: 0 if success, 1 if timeout
		CMP     R6,#0 ; Success?
		JMPR    CC_NZ,E156 ; Return if error
		MOVB    [R1],RL7 ; Put received byte in buf
		ADDB    RL3,RL7 ; RL3 += RL7
		ADD     R1,#1 ; Advance to next buf location
		CMPD1   R2,#00H ; (R2-- == 0)?
		JMPR    CC_NZ,E140 ; Loop if not
		SUBB    RL3,RL7 ; RL3 - RL7 => RL3
		CMPB    RL3,RL7 ; (RL3 == RL7)?
		JMPR    CC_Z,E156 ; Return if true
		MOV     R6,#02H ; Checksum error
E156:	MOV     R3,[R0+] ; Pop R3
E156x:	MOV     R2,[R0+] ; Pop R2
E156y:	MOV     R1,[R0+] ; Pop R1
		RET
E128	ENDP

; Send message in buf (E600)
E15E	PROC NEAR
		MOV		[-R0],R1 ; Push R1
		MOV     [-R0],R2 ; Push R2
		MOV     [-R0],R3 ; Push R3
		MOV     R1,#0E600H ; Transmit buf
		MOV     R2,#00H
		MOVB    RL2,[R1] ; RL2 is length
		MOVB    RL3,#00H ; R3 is checksum
E16E:	MOVB    RL7,[R1+] ; message byte => RL7
		ADDB    RL3,RL7 ; checksum += byte
		CALL    XmitRL7
		CMPD1   R2,#00H
		JMPR    CC_NZ,E16E ; Loop until all characters sent
		MOVB    RL7,RL3 ; checksum => RL7
		CALL    XmitRL7
		JMPR    CC_UC,E156 ; Pop R3,R2,R1 + RET
E15E	ENDP

		; Transmit byte in R7
XmitRL7	PROC NEAR
		MOVB	S0TBUF,RL7	; Transmit RL7
		JMPR	CC_UC,E192x	; Receive echo in R7
XmitRL7	ENDP

		; Receive a byte in R7
E192	PROC NEAR
E192x:
		SRVWDT					; Feed watchdog
		JNB		S0RIR,E192x		; Loop until receive flag set
		MOV     R7,S0RBUF		; R7 = received byte
		BCLR    S0RIR			; Clear receive flag
		RET
E192	ENDP

; Receive a byte in R7. R6: 0 if success, 1 if timeout.
E1A2	PROC NEAR
		MOV		[-R0],R1 ; Push R1
		MOV     R1,#0FFFFH ; Timeout to receive a byte
		MOV     R6,#00H ; Success => R6
E1AA:	SRVWDT  ; Feed watchdog
		JB      S0RIR,E1BA ; Jump if byte received
		CMPD1   R1,#00H
		JMPR    CC_NZ,E1AA ; Loop is not timeout
		MOV     R6,#01H ; Timeout => R6
		JMPR    CC_UC,E1C0
E1BA:	MOV     R7,S0RBUF ; Byte => R7
		BCLR    S0RIR ; Clear receive flag
E1C0:	JMPR    CC_UC,E156y ; Pop R1 + RET
E1A2	ENDP

; Dump EEPROM
E1C6	PROC NEAR
		BSET	DP2.8 ; P2.8 is an output
		BSET    P2.8 ; 1 => P2.8
		BSET    DP2.9 ; P2.9 is an output
		BSET    P2.9 ; 1 => P2.9
		CALL    Delay256
		CALL    Delay256
		CALL    E246 ; 11 01 00 - Start bit
		MOVB    RL7,#00ACH ; 1010 1100 (Dummy Write)
		CALL    E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8
		CALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0
		MOVB    RL7,#00H ; Address 0
		CALL    E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8
		CALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0
		CALL    E25C ; 0x 01 11
		CALL    E246 ; 11 01 00
		MOVB    RL7,#00ADH ; 1010 1101 (Read)
		CALL    E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8
		CALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0
		MOV     R5,#00H
		MOV     R2,#01FEH ; Count = 512-2
E20A:	MOV     R5,#07H
E20C:	CALL    Delay256
		CMPD1   R5,#00H
		JMPR    CC_NZ,E20C
		CALL    E2C6 ; Clock (P2.9) 8 bits of P2.8 into RL7 (MSB first)
		CALL    E298 ; 0x 01 00
		CALL    XmitRL7
		CMPD1   R2,#00H
		JMPR    CC_NZ,E20A
		CALL    E2C6 ; Clock (P2.9) 8 bits of P2.8 into RL7 (MSB first)
		CALL    XmitRL7
		BSET    P2.9 ; 1 => P2.9
		BSET    P2.8 ; 1 => P2.8
		CALL    Delay7
		BCLR    P2.9 ; 0 => P2.9
		CALL    Delay7
		BCLR    P2.8 ; 0 => P2.8
		CALL    Delay7
		JMPR	CC_UC,E25C ; 0x 01 11
E1C6	ENDP

; 11 01 00
E246	PROC NEAR
		BSET    P2.8 ; 1 => P2.8
		BSET    P2.9 ; 1 => P2.9
		CALL	Delay7
		BCLR    P2.8 ; 0 => P2.8
		CALL    Delay7
		BCLR    P2.9 ; 0 => P2.9
		CALL    Delay7
		RET
E246	ENDP

; 0x 01 11
E25C	PROC NEAR
		BCLR    P2.8 ; 0 => P2.8
		CALL	Delay7
		BSET    P2.9 ; 1 => P2.9
		CALL    Delay7
		BSET    P2.8 ; 1 => P2.8
		RET
E25C	ENDP

; x1 x0
E26C	PROC NEAR
		CALL	Delay7
		BSET    P2.9 ; 1 => P2.9
		CALL	Delay7
		BCLR    P2.9 ; 0 => P2.9
		RET
E26C	ENDP

; Clock (P2.9) one bit of P2.8 into R7.0
E27A	PROC NEAR
		MOV		R7,#00H
		BCLR    DP2.8 ; P2.8 is an input
		CALL    Delay7
		BSET    P2.9 ; 1 => P2.9
		CALL    Delay7
		BMOV    R7.0,P2.8 ; P2.8 => R7.0
		BCLR    P2.9 ; 0 => P2.9
		CALL    Delay7
		BCLR    P2.8 ; 0 => P2.8
		BSET    DP2.8 ; P2.8 is an output
		RET
E27A	ENDP

; 0x 01 00
E298	PROC NEAR
		BCLR	P2.8 ; 0 => P2.8
		CALL    Delay7
		BSET    P2.9 ; 1 => P2.9
		CALL    Delay7
		BCLR    P2.9 ; 0 => P2.9
		CALL    Delay7
		RET
E298	ENDP

; Clock (P2.9) all bits of RL7 (MSB first) to P2.8
E2AC	PROC NEAR
		MOV  	[-R0],R1 ; Push R1
		MOV     R1,#07H ; Count = 7
		SHL     R7,#08H
E2B2:	SHL     R7,#01H
		BMOV    P2.8,C ; High bit of R7 => P2.8
		CALL    E26C ; x1 x0 (clock)
		CMPD1   R1,#00H ; Count-- == 0?
		JMPR    CC_NZ,E2B2 ; Repeat
		BCLR    P2.8 ; 0 => P2.8
		JMPR	CC_UC,PopR1
E2AC	ENDP

; Clock (P2.9) 8 bits of P2.8 into RL7 (MSB first)
E2C6	PROC NEAR
		MOV  	[-R0],R1 ; Push R1
		BCLR    DP2.8 ; P2.8 is an input
		MOV     R1,#07H ; Count = 7
		MOV     R7,#00H
E2CE:	BSET    P2.9 ; 1 => P2.9
		CALL    Delay7
		SHL     R7,#01H
		BMOV    R7.0,P2.8 ; P2.8 => Low bit of R7
		BCLR    P2.9 ; 0 => P2.9
		CALL    Delay7
		CMPD1   R1,#00H ; Count-- == 0?
		JMPR    CC_NZ,E2CE ; Repeat
		BSET    DP2.8 ; P2.8 is an output
		BCLR    P2.8 ; 0 => P2.8
		JMPR	CC_UC,PopR1
E2C6	ENDP

Delay256	PROC NEAR
		MOV  	[-R0],R3 ; Push R3
		MOV     R3,#0100H
		JMPR    CC_UC,DelayLoop
Delay256	ENDP

Delay7	PROC NEAR
		MOV  	[-R0],R3 ; Push R3
		MOV     R3,#7
DelayLoop:
		SRVWDT  ; Feed watchdog
		SUB     R3,#1
		CMP     R3,#00H
		JMPR    CC_NZ,DelayLoop
		MOV     R3,[R0+] ; Pop R3
		RET
Delay7	ENDP

; Write 1 byte to EEPROM (Page 0)
E324	PROC NEAR
E324x:
		CALL	E246 ; 11 01 00 - Start bit
		MOV     R7,#00ACH ; 1010 1100 (Write)
		CALL	E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8
		CALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0
		CMP     R7,#0
		JMPR    CC_NZ,E324x
		JMPR	CC_UC,E356y
E324	ENDP

; Write 1 byte to EEPROM (Page 1)
E356	PROC NEAR
E356x:
		CALL	E246 ; 11 01 00 - Start bit
		MOV     R7,#00AEH ; 1010 1110
		CALL    E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8
		CALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0
		CMP     R7,#0
		JMPR    CC_NZ,E356x
E356y:
		CALL    E192 ; Receive a byte in R7
		CALL    E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8
		CALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0
		CALL    E192 ; Receive a byte in R7
		CALL    E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8
		CALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0
		JMPR	CC_UC,E25C ; 0x 01 11
E356	ENDP
	
; Program flash
E1C4	PROC NEAR
		MOV     [-R0],R1 ; Push R1
		MOV     [-R0],R2 ; Push R2
		MOV     [-R0],R3 ; Push R3
		MOV     [-R0],R4 ; Push R4
		MOV     R3,#0E600H ; Buf
		MOV     R2,#00H
		MOVB    RL2,[R3+#0002H] ; Address high
		MOVB    RH1,[R3+#0003H] ; Address medium
		MOVB    RL1,[R3+#0004H] ; Address low
		MOV     R4,#00H
		MOVB    RL4,[R3] ; Count
		SUB     R4,#4
		SHR     R4,#01H
		SUB     R4,#1
		ADD     R3,#5
E1EA:	SRVWDT  ; Feed watchdog
		MOVB    RL7,[R3+]
		MOVB    RH7,[R3+]
		CALL    E208 ; Program flash (R1: Address, R7: Data)
		ADD     R1,#2
		ADDC    R2,#0
		CMPD1   R4,#00H
		JMPR    CC_NZ,E1EA
PopR4:	MOV     R4,[R0+] ; Pop R4
PopR3:	MOV     R3,[R0+] ; Pop R3
PopR2:	MOV     R2,[R0+] ; Pop R2
PopR1:	MOV     R1,[R0+] ; Pop R1
		RET
E1C4	ENDP

; Program flash (R1: Address, R7: Data)
E208	PROC NEAR
		MOV     [-R0],R1 ; Push R1
		MOV     [-R0],R7 ; Push R7
		MOV     R1,#0AAAAH ; Address AAA
		MOVB    RL7,#00AAH ; Data AA
		CALL    E374 ; Write extended data byte (source is RL7)
		MOV     R1,#5555H ; Address 555
		MOVB    RL7,#0055H ; Data 55
		CALL    E374 ; Write extended data byte (source is RL7)
		MOV     R1,#0AAAAH ; Address AAA
		MOVB    RL7,#00A0H ; Data A0
		CALL    E374 ; Write extended data byte (source is RL7)
		MOV     R7,[R0+] ; Pop R7
		MOV     R1,[R0+] ; Pop R1
		CALL    E384 ; Write extended data word (source is R7)
		CALL    E354 ; Wait until operation is complete
		RET
E208	ENDP

; Read flash
E23E	PROC NEAR
		MOV     [-R0],R1 ; Push R1
		MOV     [-R0],R2 ; Push R2
		MOV     [-R0],R3 ; Push R3
		MOV     [-R0],R4 ; Push R4
		MOV     [-R0],R5 ; Push R5
		MOV     R3,#0E600H ; Buf
		MOV     R2,#00H
		MOVB    RL2,[R3+#0002H] ; Address high
		MOVB    RH1,[R3+#0003H] ; Address medium
		MOVB    RL1,[R3+#0004H] ; Address low
		MOV     R4,#00H
		MOVB    RL4,[R3+#0005H] ; Count
		ADDB    RL4,#1
		MOVB    [R3],RL4
		ADD     R3,#1
		MOVB    RL7,#0036H
		MOVB    [R3],RL7
		ADD     R3,#1
		SUBB    RL4,#1
E270:	CALL    E290 ; Read extended data (returned in RL7)
		MOVB    [R3],RL7
		ADD     R3,#1
		ADD     R1,#1
		ADDC    R2,#0
		CMPD1   R4,#00H
		JMPR    CC_NZ,E270
		CALL    E15E ; Send message in buf (E600)
		MOV     R5,[R0+] ; Pop R5
		JMPR	CC_UC,PopR4
E23E	ENDP

; Read extended data (returned in RL7)
E290	PROC NEAR
		MOV     [-R0],R1 ; Push R1
		SRVWDT  ; Feed watchdog
		CALL    E2A4 ; Set Data Page Pointer 0 based on R2
		AND     R1,#3FFFH
		MOVB    RL7,[R1]
		JMPR	CC_UC,PopR1
E290	ENDP

; Set Data Page Pointer 0 based on R1, R2
E2A4	PROC NEAR
		MOV     [-R0],R1 ; Push R1
		MOV     [-R0],R2 ; Push R2
		SHL     R2,#02H
		AND     R2,#00FCH
		ROL     R1,#02H
		AND     R1,#3
		OR      R2,R1
		MOV     DPP0,R2 ; R2 => Data Page Pointer 0
		JMPR	CC_UC,PopR2
E2A4	ENDP

; Erase flash
E2E6	PROC NEAR
		MOV     [-R0],R1 ; Push R1
		MOV     [-R0],R2 ; Push R2
		MOV     [-R0],R3 ; Push R3
		MOV     [-R0],R7 ; Push R7
		MOV     R3,#0E600H ; Buf
		MOV     R2,#0020H
		MOV     R1,#0AAAAH ; Address AAA
		MOVB    RL7,#00AAH ; Data AA
		CALL    E374 ; Write extended data byte (source is RL7)
		MOV     R1,#5555H ; Address 555
		MOVB    RL7,#0055H ; Data 55
		CALL    E374 ; Write extended data byte (source is RL7)
		MOV     R1,#0AAAAH ; Address AAA
		MOVB    RL7,#0080H ; Data 80
		CALL    E374 ; Write extended data byte (source is RL7)
		MOV     R1,#0AAAAH ; Address AAA
		MOVB    RL7,#00AAH ; Data AA
		CALL    E374 ; Write extended data byte (source is RL7)
		MOV     R1,#5555H ; Address 555
		MOVB    RL7,#0055H ; Data 55
		CALL    E374 ; Write extended data byte (source is RL7)
		MOV     R1,#0AAAAH ; Address AAA
		MOVB    RL7,#0010H ; Data 10
		CALL    E374 ; Write extended data byte (source is RL7)
		CALL    Delay256
		MOVB    RL7,#0080H
		CALL    E354 ; Wait until erase operation is complete
		MOV     R7,[R0+] ; Pop R7
		JMPR	CC_UC,PopR3
E2E6	ENDP
	
; Wait until operation is complete
E354	PROC NEAR
		MOV     [-R0],R2 ; Push R2
		MOV     [-R0],R7 ; Push R7
		SUB     R2,#0018H
		MOVB    RH7,RL7
		ANDB    RH7,#0080H
E362:	CALL    E290 ; Read extended data (returned in RL7)
		ANDB    RL7,#0080H
		CMPB    RL7,RH7
		JMPR    CC_NZ,E362
		MOV     R7,[R0+] ; Pop R7
		MOV     R2,[R0+] ; Pop R2
		RET
E354	ENDP

; Write extended data byte (source is RL7)
E374	PROC NEAR
		MOV     [-R0],R1 ; Push R1
		CALL    E2A4 ; Set Data Page Pointer 0 based on R2
		AND     R1,#3FFFH
		MOVB    [R1],RL7
		JMPR	CC_UC,E384x ; Pop R1 + RET
E374	ENDP
	
; Write extended data word (source is R7)
E384	PROC NEAR
		MOV     [-R0],R1 ; Push R1
		CALL    E2A4 ; Set Data Page Pointer 0 based on R2
		AND     R1,#3FFFH
		MOV     [R1],R7
E384x:	MOV     R1,[R0+] ; Pop R1
		RET
E384	ENDP

RoutineEnd:

All		ENDS

END


================================================
FILE: Interface/FtdiInterface.cs
================================================
using System;
using System.IO.Ports;
using System.Reflection;
using System.Runtime.InteropServices;

namespace BitFab.KW1281Test.Interface
{
    internal class FtdiInterface : IInterface 
    {
        private readonly FT _ft;
        private IntPtr _handle = IntPtr.Zero;
        private readonly byte[] _buf = new byte[1];

        public FtdiInterface(string serialNumber, int baudRate)
        {
            _ft = new FT();

            var status = _ft.Open(
                serialNumber, FT.OpenExFlags.BySerialNumber, out _handle);
            FT.AssertOk(status);

            status = _ft.SetBaudRate(_handle, (uint)baudRate);
            FT.AssertOk(status);

            status = _ft.SetDataCharacteristics(
                _handle,
                FT.Bits.Eight,
                FT.StopBits.One,
                FT.Parity.None);
            FT.AssertOk(status);

            status = _ft.SetFlowControl(
                _handle,
                FT.FlowControl.None, 0, 0);
            FT.AssertOk(status);

            SetRts(false);
            SetDtr(true);

            _readTimeout = _writeTimeout = ((IInterface)this).DefaultTimeoutMilliseconds;
            ReadTimeout = _readTimeout; // Also sets the write timeout

            // Should allow faster response times for small packets
            status = _ft.SetLatencyTimer(_handle, 2);
            FT.AssertOk(status);
        }

        public void Dispose()
        {
            if (_handle != IntPtr.Zero)
            {
                SetDtr(false);

                var status = _ft.Close(_handle);
                _handle = IntPtr.Zero;
                FT.AssertOk(status);
            }

            _ft.Dispose();
        }

        public byte ReadByte()
        {
            var status = _ft.Read(_handle, _buf, 1, out uint countOfBytesRead);
            FT.AssertOk(status);
            if (countOfBytesRead != 1)
            {
                throw new TimeoutException("Read timed out");
            }

            var b = _buf[0];
            return b;
        }

        /// <summary>
        /// Write a byte to the interface but do not read/discard its echo.
        /// </summary>
        public void WriteByteRaw(byte b)
        {
            _buf[0] = b;
            var status = _ft.Write(_handle, _buf, 1, out uint countOfBytesWritten);
            FT.AssertOk(status);
            if (countOfBytesWritten != 1)
            {
                throw new InvalidOperationException(
                    $"Expected to write 1 byte but wrote {countOfBytesWritten} bytes");
            }
        }

        public void SetBreak(bool on)
        {
            FT.Status status;
            if (on)
            {
                status = _ft.SetBreakOn(_handle);
            }
            else
            {
                status = _ft.SetBreakOff(_handle);
            }
            FT.AssertOk(status);
        }

        public void ClearReceiveBuffer()
        {
            var status = _ft.Purge(_handle, FT.PurgeMask.RX);
            FT.AssertOk(status);
        }

        public void SetBaudRate(int baudRate)
        {
            var status = _ft.SetBaudRate(_handle, (uint)baudRate);
            FT.AssertOk(status);
        }

        public void SetParity(Parity parity)
        {
            var ftParity = parity switch
            {
                Parity.None => FT.Parity.None,
                Parity.Even => FT.Parity.Even,
                Parity.Odd => FT.Parity.Odd,
                Parity.Mark => FT.Parity.Mark,
                Parity.Space => FT.Parity.Space,
                _ => throw new ArgumentException($"Unsupported parity: {parity}", nameof(parity))
            };

            var status = _ft.SetDataCharacteristics(
                _handle,
                FT.Bits.Eight,
                FT.StopBits.One, 
                ftParity);
            FT.AssertOk(status);
        }

        public void SetDtr(bool on)
        {
            FT.Status status;
            if (on)
            {
                status = _ft.SetDtr(_handle);
            }
            else
            {
                status = _ft.ClrDtr(_handle);
            }
            FT.AssertOk(status);
        }

        public void SetRts(bool on)
        {
            FT.Status status;
            if (on)
            {
                status = _ft.SetRts(_handle);
            }
            else
            {
                status = _ft.ClrRts(_handle);
            }
            FT.AssertOk(status);
        }

        private int _readTimeout;
        
        public int ReadTimeout
        {
            get => _readTimeout;

            set
            {
                var status = _ft.SetTimeouts(
                    _handle,
                    (uint)value,
                    (uint)WriteTimeout);
                FT.AssertOk(status);
                _readTimeout = value;
            }
        }

        private int _writeTimeout;
        
        public int WriteTimeout
        {
            get => _writeTimeout;
            set
            {
                var status = _ft.SetTimeouts(
                    _handle,
                    (uint)ReadTimeout,
                    (uint)value);
                FT.AssertOk(status);
                _writeTimeout = value;
            }
        }
    }

    class FT : IDisposable
    {
        private IntPtr _d2xx = IntPtr.Zero;

        // Delegates used to call into the FTID D2xx DLL
#pragma warning disable CS0649
        private readonly FTDll.SetVidPid _setVidPid;
        private readonly FTDll.OpenBySerialNumber _openBySerialNumber;
        private readonly FTDll.Close _close;
        private readonly FTDll.SetBaudRate _setBaudRate;
        private readonly FTDll.SetDataCharacteristics _setDataCharacteristics;
        private readonly FTDll.SetFlowControl _setFlowControl;
        private readonly FTDll.SetDtr _setDtr;
        private readonly FTDll.ClrDtr _clrDtr;
        private readonly FTDll.SetRts _setRts;
        private readonly FTDll.ClrRts _clrRts;
        private readonly FTDll.SetTimeouts _setTimeouts;
        private readonly FTDll.SetLatencyTimer _setLatencyTimer;
        private readonly FTDll.Purge _purge;
        private readonly FTDll.SetBreakOn _setBreakOn;
        private readonly FTDll.SetBreakOff _setBreakOff;
        private readonly FTDll.Read _read;
        private readonly FTDll.Write _write;
#pragma warning restore CS0649

        public FT()
        {
            string libName;
            bool isMacOs = false;
            bool isLinux = false;

            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                libName = "libftd2xx.dylib";
                isMacOs = true;
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                libName = "ftd2xx.so";
                isLinux = true;
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                libName = Environment.Is64BitProcess ? "ftd2xx64.dll" : "ftd2xx.dll";
            }
            else
            {
                throw new InvalidOperationException($"Unknown OS: {RuntimeInformation.OSDescription}");
            }

            _d2xx = NativeLibrary.Load(
                libName, typeof(FT).Assembly, DllImportSearchPath.SafeDirectories);

            InitDelegate(nameof(_openBySerialNumber), out _openBySerialNumber);
            InitDelegate(nameof(_close), out _close);
            InitDelegate(nameof(_setBaudRate), out _setBaudRate);
            InitDelegate(nameof(_setDataCharacteristics), out _setDataCharacteristics);
            InitDelegate(nameof(_setFlowControl), out _setFlowControl);
            InitDelegate(nameof(_setDtr), out _setDtr);
            InitDelegate(nameof(_clrDtr), out _clrDtr);
            InitDelegate(nameof(_setRts), out _setRts);
            InitDelegate(nameof(_clrRts), out _clrRts);
            InitDelegate(nameof(_setTimeouts), out _setTimeouts);
            InitDelegate(nameof(_setLatencyTimer), out _setLatencyTimer);
            InitDelegate(nameof(_purge), out _purge);
            InitDelegate(nameof(_setBreakOn), out _setBreakOn);
            InitDelegate(nameof(_setBreakOff), out _setBreakOff);
            InitDelegate(nameof(_read), out _read);
            InitDelegate(nameof(_write), out _write);
            if (isMacOs || isLinux)
            {
                InitDelegate(nameof(_setVidPid), out _setVidPid);
            }
            else
            {
                _setVidPid = (uint vid, uint pid) => Status.Ok;
            }

            if (isMacOs || isLinux)
            {
                var vidStr = Environment.GetEnvironmentVariable("FTDI_VID");
                var pidStr = Environment.GetEnvironmentVariable("FTDI_PID");
                if (!string.IsNullOrEmpty(vidStr) && !string.IsNullOrEmpty(pidStr))
                {
                    var vid = Utils.ParseUint(vidStr);
                    var pid = Utils.ParseUint(pidStr);
                    Log.WriteLine($"Setting FTDI VID=0x{vid:X4}, PID=0x{pid:X4}");
                    var status = SetVidPid(vid, pid);
                    AssertOk(status);
                }
            }
        }

        private void InitDelegate<T>(string fieldName, out T delegateVal) where T : Delegate
        {
            var symbolNameAttribute = typeof(T).GetCustomAttribute<SymbolNameAttribute>();
            if (symbolNameAttribute == null)
            {
                throw new InvalidOperationException(
                    $"Type {typeof(T)} is missing required SymbolName attribute.");
            }
            var nativeMethodName = symbolNameAttribute.Name;
            var export = NativeLibrary.GetExport(_d2xx, nativeMethodName);
            delegateVal = Marshal.GetDelegateForFunctionPointer<T>(export);
        }

        public void Dispose()
        {
            if (_d2xx != IntPtr.Zero)
            {
                NativeLibrary.Free(_d2xx);
                _d2xx = IntPtr.Zero;
            }
        }

        public static void AssertOk(FT.Status status)
        {
            if (status != FT.Status.Ok)
            {
                throw new InvalidOperationException(
                    $"D2xx library returned {status} instead of Ok");
            }
        }

        public Status SetVidPid(
            uint vid,
            uint pid)
        {
            return _setVidPid(vid, pid);
        }

        public Status Open(
            string serialNumber,
            OpenExFlags flags,
            out IntPtr handle)
        {
            return _openBySerialNumber(serialNumber, flags, out handle);
        }

        public Status Close(
            IntPtr handle)
        {
            return _close(handle);
        }

        public Status SetBaudRate(
            IntPtr handle,
            uint baudRate)
        {
            return _setBaudRate(handle, baudRate);
        }

        public Status SetDataCharacteristics(
            IntPtr handle,
            Bits wordLength,
            StopBits stopBits,
            Parity parity)
        {
            return _setDataCharacteristics(handle, wordLength, stopBits, parity);
        }

        public Status SetFlowControl(
            IntPtr handle,
            FlowControl flowControl,
            byte xonChar,
            byte xoffChar)
        {
            return _setFlowControl(handle, flowControl, xonChar, xoffChar);
        }

        public Status SetDtr(
            IntPtr handle)
        {
            return _setDtr(handle);
        }

        public Status ClrDtr(
            IntPtr handle)
        {
            return _clrDtr(handle);
        }

        public Status SetRts(
            IntPtr handle)
        {
            return _setRts(handle);
        }

        public Status ClrRts(
            IntPtr handle)
        {
            return _clrRts(handle);
        }

        public Status SetTimeouts(
            IntPtr handle,
            uint readTimeoutMS,
            uint writeTimeoutMS)
        {
            return _setTimeouts(handle, readTimeoutMS, writeTimeoutMS);
        }

        public Status SetLatencyTimer(
            IntPtr handle,
            byte timerMS)
        {
            return _setLatencyTimer(handle, timerMS);
        }

        public Status Purge(
            IntPtr handle,
            PurgeMask mask)
        {
            return _purge(handle, mask);
        }

        public Status SetBreakOn(
            IntPtr handle)
        {
            return _setBreakOn(handle);
        }

        public Status SetBreakOff(
            IntPtr handle)
        {
            return _setBreakOff(handle);
        }

        public Status Read(
            IntPtr handle,
            byte[] buffer,
            uint countOfBytesToRead,
            out uint countOfBytesRead)
        {
            return _read(handle, buffer, countOfBytesToRead, out countOfBytesRead);
        }

        public Status Write(
            IntPtr handle,
            byte[] buffer,
            uint countOfBytesToWrite,
            out uint countOfBytesWritten)
        {
            return _write(handle, buffer, countOfBytesToWrite, out countOfBytesWritten);
        }

        public enum Status : uint
        {
            Ok = 0,
            InvalidHandle,
            DeviceNotFound,
            DeviceNotOpened,
            IOError,
            insufficient_resources,
            InvalidParameter,
            InvalidBaudRate,
            DeviceNotOpenedForErase,
            DeviceNotOpenedForWrite,
            FailedToWriteDevice,
            EepromReadFailed,
            EepromWriteFailed,
            EepromEraseFailed,
            EepromNotPresent,
            EepromNotProgrammed,
            InvalidArgs,
            NotSupported,
            OtherError,
            DeviceListNotReady,
        };

        [Flags]
        public enum OpenExFlags : uint
        {
            BySerialNumber = 1,
            ByDescription = 2,
            ByLocation = 4
        };

        public enum Bits : byte
        {
            Eight = 8,
            Seven = 7
        };

        public enum StopBits : byte
        {
            One = 0,
            Two = 2
        };

        public enum Parity : byte
        {
            None = 0,
            Odd = 1,
            Even = 2,
            Mark = 3,
            Space = 4
        };

        public enum FlowControl : ushort
        {
            None = 0x0000,
            RtsCts = 0x0100,
            DtrDsr = 0x0200,
            XonXoff = 0x0400
        };

        [Flags]
        public enum PurgeMask : uint
        {
            RX = 1,
            TX = 2
        };
    }

    [AttributeUsage(AttributeTargets.Delegate)]
    internal class SymbolNameAttribute : Attribute
    {
        public SymbolNameAttribute(string name)
        {
            Name = name;
        }

        public string Name { get; private set; }
    }

    static class FTDll
    {
        [SymbolName("FT_SetVIDPID")]
        public delegate FT.Status SetVidPid(
            uint vid, uint pid);

        [SymbolName("FT_OpenEx")]
        public delegate FT.Status OpenBySerialNumber(
            [MarshalAs(UnmanagedType.LPStr)] string serialNumber,
            FT.OpenExFlags flags,
            out IntPtr handle);

        [SymbolName("FT_Close")]
        public delegate FT.Status Close(
            IntPtr handle);

        [SymbolName("FT_SetBaudRate")]
        public delegate FT.Status SetBaudRate(
            IntPtr handle,
            uint baudRate);

        [SymbolName("FT_SetDataCharacteristics")]
        public delegate FT.Status SetDataCharacteristics(
            IntPtr handle,
            FT.Bits wordLength,
            FT.StopBits stopBits,
            FT.Parity parity);

        [SymbolName("FT_SetFlowControl")]
        public delegate FT.Status SetFlowControl(
            IntPtr handle,
            FT.FlowControl flowControl,
            byte xonChar,
            byte xoffChar);

        [SymbolName("FT_SetDtr")]
        public delegate FT.Status SetDtr(
            IntPtr handle);

        [SymbolName("FT_ClrDtr")]
        public delegate FT.Status ClrDtr(
            IntPtr handle);

        [SymbolName("FT_SetRts")]
        public delegate FT.Status SetRts(
            IntPtr handle);

        [SymbolName("FT_ClrRts")]
        public delegate FT.Status ClrRts(
            IntPtr handle);

        [SymbolName("FT_SetTimeouts")]
        public delegate FT.Status SetTimeouts(
            IntPtr handle,
            uint readTimeoutMS,
            uint writeTimeoutMS);

        [SymbolName("FT_SetLatencyTimer")]
        public delegate FT.Status SetLatencyTimer(
            IntPtr handle,
            byte timerMS);

        [SymbolName("FT_Purge")]
        public delegate FT.Status Purge(
            IntPtr handle,
            FT.PurgeMask mask);

        [SymbolName("FT_SetBreakOn")]
        public delegate FT.Status SetBreakOn(
            IntPtr handle);

        [SymbolName("FT_SetBreakOff")]
        public delegate FT.Status SetBreakOff(
            IntPtr handle);

        [SymbolName("FT_Read")]
        public delegate FT.Status Read(
            IntPtr handle,
            byte[] buffer,
            uint countOfBytesToRead,
            out uint countOfBytesRead);

        [SymbolName("FT_Write")]
        public delegate FT.Status Write(
            IntPtr handle,
            byte[] buffer,
            uint countOfBytesToWrite,
            out uint countOfBytesWritten);
    }
}


================================================
FILE: Interface/GenericInterface.cs
================================================
using System.IO.Ports;

namespace BitFab.KW1281Test.Interface
{
    internal class GenericInterface : IInterface
    {
        public GenericInterface(string portName, int baudRate)
        {
            var timeout = ((IInterface)this).DefaultTimeoutMilliseconds;
            _port = new SerialPort(portName)
            {
                BaudRate = baudRate,
                DataBits = 8,
                Parity = Parity.None,
                StopBits = StopBits.One,
                Handshake = Handshake.None,
                RtsEnable = false,
                DtrEnable = true,
                ReadTimeout = timeout,
                WriteTimeout = timeout
            };

            _port.Open();
        }

        public void Dispose()
        {
            SetDtr(false);
            _port.Close();
        }

        public byte ReadByte()
        {
            var b = (byte)_port.ReadByte();
            return b;
        }

        public void WriteByteRaw(byte b)
        {
            _buf[0] = b;
            _port.Write(_buf, 0, 1);
        }

        public void SetBreak(bool on)
        {
            _port.BreakState = on;
        }

        public void ClearReceiveBuffer()
        {
            _port.DiscardInBuffer();
        }

        public void SetBaudRate(int baudRate)
        {
            _port.BaudRate = baudRate;
        }

        public void SetParity(Parity parity)
        {
            _port.Parity = parity;
        }

        public void SetDtr(bool on)
        {
            _port.DtrEnable = on;
        }

        public void SetRts(bool on)
        {
            _port.RtsEnable = on;
        }

        public int ReadTimeout
        {
            get => _port.ReadTimeout;
            set => _port.ReadTimeout = value;
        }

        public int WriteTimeout
        {
            get => _port.WriteTimeout;
            set => _port.WriteTimeout = value;
        }

        private readonly SerialPort _port;

        private readonly byte[] _buf = new byte[1];
    }
}


================================================
FILE: Interface/IInterface.cs
================================================
using System;
using System.IO.Ports;

namespace BitFab.KW1281Test.Interface
{
    public interface IInterface : IDisposable
    {
        int DefaultTimeoutMilliseconds => (int)TimeSpan.FromSeconds(8).TotalMilliseconds;

        /// <summary>
        /// Read a byte from the interface.
        /// </summary>
        /// <returns>The byte.</returns>
        byte ReadByte();

        /// <summary>
        /// Write a byte to the interface but do not read/discard its echo.
        /// </summary>
        void WriteByteRaw(byte b);

        void SetBreak(bool on);

        void ClearReceiveBuffer();

        void SetBaudRate(int baudRate);

        void SetParity(Parity parity);

        void SetDtr(bool on);

        void SetRts(bool on);

        int ReadTimeout { get; set; }

        int WriteTimeout { get; set; }
    }
}


================================================
FILE: Interface/LinuxInterface.cs
================================================
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.IO.Ports;

using tcflag_t = System.UInt32;
using cc_t = System.Byte;
using speed_t = System.UInt32;

namespace BitFab.KW1281Test.Interface;

public class LinuxInterface : IInterface
{
    private const uint CBAUD  = 0x100F; // Clear normal baudrates
    private const uint CBAUDEX = 0x1000;
    private const uint BOTHER = 0x1000; // Other baudrate
    private const int IBSHIFT = 16; // Shift from CBAUD to CIBAUD
    private const uint PARENB = 0x0100; // Enable parity bit
    private const uint PARODD = 0x0200; // Use odd parity rather than even parity

    private const string libc = "libc";

#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time

    // Linux ioctl function
    [DllImport(libc, SetLastError = true)]
    private static extern int ioctl(int fd, int request, ref int data);

    [DllImport(libc, SetLastError = true)]
    private static extern int ioctl(int fd, uint request, IntPtr data);

    // Native method declarations
    [DllImport(libc)]
    private static extern int open(string pathname, int flags);

    [DllImport(libc)]
    private static extern int close(int fd);

    [DllImport(libc, CallingConvention = CallingConvention.StdCall)]
    private static extern int read(int fd, byte[] buf, int count);

    [DllImport(libc, CallingConvention = CallingConvention.StdCall)]
    private static extern int write(int fd, byte[] buf, int count);

    [DllImport(libc, CallingConvention = CallingConvention.StdCall)]
    private static extern int tcflush(int fd, int queue);

#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time

    private const int NCCS = 19;

    // Define the termios structure to interact with terminal I/O settings
    [StructLayout(LayoutKind.Sequential)]
    public struct Termios
    {
        public tcflag_t c_iflag;    // input mode flags
        public tcflag_t c_oflag;    // output mode flags
        public tcflag_t c_cflag;    // control mode flags
        public tcflag_t c_lflag;    // local mode flags
        public cc_t c_line;         // line discipline

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = NCCS)]
        public cc_t[] c_cc;         // control characters

        public speed_t c_ispeed;    // input speed
        public speed_t c_ospeed;    // output speed
    }

    private const int _IOC_NRBITS =	8;
    private const int _IOC_TYPEBITS	= 8;
    private const int _IOC_SIZEBITS = 14;

    private const int _IOC_NRSHIFT = 0;
    private const int _IOC_TYPESHIFT = (_IOC_NRSHIFT + _IOC_NRBITS);
    private const int _IOC_SIZESHIFT = (_IOC_TYPESHIFT + _IOC_TYPEBITS);
    private const int _IOC_DIRSHIFT = (_IOC_SIZESHIFT + _IOC_SIZEBITS);

    private const int _IOC_READ = 2;
    private const int _IOC_WRITE = 1;

#pragma warning disable IDE1006 // Naming Styles
    private static uint _IOC(int dir, int type, int nr, int size)
    {
        return (uint)((dir << _IOC_DIRSHIFT) |
            (type << _IOC_TYPESHIFT) |
            (nr   << _IOC_NRSHIFT) |
            (size << _IOC_SIZESHIFT));
    }

    private static int _IOC_TYPECHECK(Type type)
    {
        return Marshal.SizeOf(type);
    }

    private static uint _IOR(int type, int nr, Type size)
    {
        return _IOC(_IOC_READ, type, nr, _IOC_TYPECHECK(size));
    }

    private static uint _IOW(int type, int nr, Type size)
    {
        return _IOC(_IOC_WRITE, type, nr, _IOC_TYPECHECK(size));
    }
#pragma warning restore IDE1006 // Naming Styles

    private static readonly uint TCGETS2 = _IOR('T', 0x2A, typeof(Termios));
    private static readonly uint TCSETS2 = _IOW('T', 0x2B, typeof(Termios));

    private const int TIOCSBRK = 0x5427;
    private const int TIOCCBRK = 0x5428;
    private const int TIOCM_RTS = 0x004;
    private const int TIOCMGET = 0x5415;
    private const int TIOCMSET = 0x5418;
    private const int TIOCM_DTR = 0x002;
    private const int O_RDWR = 2;
    private const int O_NOCTTY = 00000400;
    private const int TCIFLUSH = 0; // Discard data received but not yet read

    private const int VTIME = 5;
    private const int VMIN = 6;

    private int _fd = -1;

    private IntPtr _termios;

    public int ReadTimeout { get; set; }

    public int WriteTimeout { get; set; }

    public LinuxInterface(string portName, int baudRate)
    {
        _fd = open(portName, O_RDWR | O_NOCTTY);
        if (_fd == -1)
        {
            throw new IOException($"Failed to open port {portName}");
        }

        // Allocate struct and memory
        _termios = Marshal.AllocHGlobal(Marshal.SizeOf<Termios>());

        var termios = GetTtyConfiguration();

        // Update termio struct with timeouts
        termios.c_iflag = 0;
        termios.c_oflag = 0;
        termios.c_lflag = 0;

        var timeout = ((IInterface)this).DefaultTimeoutMilliseconds;
        termios.c_cc[VTIME] = (byte)(timeout / 100);
        termios.c_cc[VMIN] = 0;

        SetTtyConfiguration(termios);

        SetBaudRate(baudRate);
    }

    private Termios GetTtyConfiguration()
    {
        if (ioctl(_fd, TCGETS2, _termios) == -1)
        {
            throw new IOException("Failed to get the UART configuration");
        }
        var termios = Marshal.PtrToStructure<Termios>(_termios);
        return termios;
    }

    private void SetTtyConfiguration(Termios termios)
    {
        // Get a C pointer to struct
        Marshal.StructureToPtr(termios, _termios, fDeleteOld: true);

        // Update configuration
        if (ioctl(_fd, TCSETS2, _termios) == -1)
        {
            throw new IOException("Failed to set the UART configuration");
        }
    }

    private readonly byte[] _buffer = new byte[1];

    public byte ReadByte()
    {
        // Console.WriteLine("XX ReadByte");

        int bytesRead = read(_fd, _buffer, 1);
        if (bytesRead != 1)
        {
            throw new IOException("Failed to read byte from UART");
        }
        return _buffer[0];
    }

    public void WriteByteRaw(byte b)
    {
        _buffer[0] = b;
        int bytesWritten = write(_fd, _buffer, 1);
        if (bytesWritten != 1)
        {
            throw new IOException("Failed to write byte to UART");
        }
    }

    public void SetBreak(bool on)
    {
         var iov = on ? TIOCSBRK : TIOCCBRK;

        int data = 0;

        if (ioctl(_fd, iov, ref data) == -1)
        {
            throw new IOException("Failed to set/clear UART break");
        }
    }

    public void ClearReceiveBuffer()
    {
        if (tcflush(_fd, TCIFLUSH) == -1)
        {
            throw new IOException("Failed to clear the UART receive buffer");
        }
    }

    public void SetBaudRate(int baudRate)
    {
        var termios = GetTtyConfiguration();

        // Output speed
        termios.c_cflag &= ~(CBAUD | CBAUDEX);
        termios.c_cflag |= BOTHER;
        termios.c_ospeed = (uint)baudRate;

        // Input speed
        termios.c_cflag &= ~((CBAUD | CBAUDEX) << IBSHIFT);
        termios.c_cflag |= (BOTHER << IBSHIFT);
        termios.c_ispeed = (uint)baudRate;

        SetTtyConfiguration(termios);
    }

    public void SetParity(Parity parity)
    {
        var termios = GetTtyConfiguration();

        // Set parity
        switch (parity)
        {
            case Parity.None:
                termios.c_cflag &= ~PARENB; // Disable parity
                break;
            case Parity.Odd:
                termios.c_cflag |= PARENB;  // Enable parity
                termios.c_cflag |= PARODD;  // Set odd parity
                break;
            case Parity.Even:
                termios.c_cflag |= PARENB;  // Enable parity
                termios.c_cflag &= ~PARODD; // Set even parity
                break;
            case Parity.Mark:
                // Mark parity is not supported on Linux, set as None
                termios.c_cflag &= ~PARENB; // Disable parity
                break;
            case Parity.Space:
                // Space parity is not supported on Linux, set as None
                termios.c_cflag &= ~PARENB; // Disable parity
                break;
        }

        SetTtyConfiguration(termios);
    }

    public void SetDtr(bool on)
    {
        // Get the current control lines state
        int controlLinesState = 0;

        if (ioctl(_fd, TIOCMGET, ref controlLinesState) == -1)
        {
            throw new IOException("Failed to get control lines state.");
        }

        // Set DTR flag
        if (on)
        {
            controlLinesState |= TIOCM_DTR;
        }
        else
        {
            controlLinesState &= ~TIOCM_DTR;
        }

        // Set the modified control lines state
        if (ioctl(_fd, TIOCMSET, ref controlLinesState) == -1)
        {
            throw new IOException("Failed to set DTR");
        }
    }

    public void SetRts(bool on)
    {
        // Get the current control lines state
        int controlLinesState = 0;
        if (ioctl(_fd, TIOCMGET, ref controlLinesState) == -1)
        {
            throw new IOException("Failed to get uart line state");
        }

        // Set RTS flag
        if (on)
            controlLinesState |= TIOCM_RTS;
        else
            controlLinesState &= ~TIOCM_RTS;

        // Set the modified control lines state
        if (ioctl(_fd, TIOCMSET, ref controlLinesState) == -1)
        {
            throw new IOException("Failed to set RTS");
        }
    }

    public void Dispose()
    {
        // Dispose of unmanaged resources.
        Dispose(true);

        // Suppress finalization.
        GC.SuppressFinalize(this);
    }

    private bool _disposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
        {
            return;
        }

        if (disposing)
        {
            // TODO: Dispose managed state (managed objects).
        }

        // Free unmanaged resources (unmanaged objects) and override a finalizer below.

        if (_termios != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(_termios);
            _termios = IntPtr.Zero;
        }
        if (_fd != -1)
        {
            _ = close(_fd);
            _fd = -1;
        }

        // TODO: Set large fields to null.

        _disposed = true;
    }
}


================================================
FILE: KW1281Dialog.cs
================================================
using BitFab.KW1281Test.Blocks;
using BitFab.KW1281Test.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace BitFab.KW1281Test;

/// <summary>
/// Manages a dialog with a VW controller using the KW1281 protocol.
/// </summary>
internal interface IKW1281Dialog
{
    ControllerInfo Connect();

    void EndCommunication();

    void SetDisconnected();

    List<Block> Login(ushort code, int workshopCode);

    List<ControllerIdent> ReadIdent();

    /// <summary>
    /// Corresponds to VDS-Pro function 19
    /// </summary>
    List<byte>? ReadEeprom(ushort address, byte count);

    bool WriteEeprom(ushort address, List<byte> values);

    /// <summary>
    /// Corresponds to VDS-Pro functions 21 and 22
    /// </summary>
    List<byte>? ReadRomEeprom(ushort address, byte count);

    /// <summary>
    /// Corresponds to VDS-Pro functions 20 and 25
    /// </summary>
    List<byte>? ReadRam(ushort address, byte count);

    bool AdaptationRead(byte channelNumber);

    bool AdaptationTest(byte channelNumber, ushort channelValue);

    bool AdaptationSave(byte channelNumber, ushort channelValue, int workshopCode);

    void SendBlock(List<byte> blockBytes);

    List<Block> ReceiveBlocks();

    List<byte>? ReadCcmRom(byte seg, byte msb, byte lsb, byte count);

    /// <summary>
    /// Keep the dialog alive by sending an ACK and receiving a response.
    /// </summary>
    void KeepAlive();

    ActuatorTestResponseBlock? ActuatorTest(byte value);

    List<FaultCode>? ReadFaultCodes();

    /// <summary>
    /// Clear all of the controllers fault codes.
    /// </summary>
    /// <param name="controllerAddress"></param>
    /// <returns>Any remaining fault codes.</returns>
    List<FaultCode>? ClearFaultCodes(int controllerAddress);

    /// <summary>
    /// Set the controller's software coding and workshop code.
    /// </summary>
    /// <param name="controllerAddress"></param>
    /// <param name="softwareCoding"></param>
    /// <param name="workshopCode"></param>
    /// <returns>True if successful.</returns>
    bool SetSoftwareCoding(int controllerAddress, int softwareCoding, int workshopCode);

    bool GroupRead(byte groupNumber, bool useBasicSetting = false);

    List<byte> ReadSecureImmoAccess(List<byte> blockBytes);

    public IKwpCommon KwpCommon { get; }
    Block ReceiveBlock();
}

internal class KW1281Dialog : IKW1281Dialog
{
    public ControllerInfo Connect()
    {
        _isConnected = true;
        var blocks = ReceiveBlocks();
        return new ControllerInfo(blocks.Where(b => !b.IsAckNak));
    }

    public List<Block> Login(ushort code, int workshopCode)
    {
        Log.WriteLine("Sending Login block");
        SendBlock(
        [
            (byte)BlockTitle.Login,
            (byte)(code >> 8),
            (byte)(code & 0xFF),
            (byte)(workshopCode >> 16),
            (byte)((workshopCode >> 8) & 0xFF),
            (byte)(workshopCode & 0xFF)
        ]);

        return ReceiveBlocks();
    }

    public List<ControllerIdent> ReadIdent()
    {
        var idents = new List<ControllerIdent>();
        bool moreAvailable;
        do
        {
            Log.WriteLine("Sending ReadIdent block");

            SendBlock(new List<byte> { (byte)BlockTitle.ReadIdent });

            var blocks = ReceiveBlocks();
            var ident = new ControllerIdent(blocks.Where(b => !b.IsAckNak));
            idents.Add(ident);

            moreAvailable = blocks
                .OfType<AsciiDataBlock>()
                .Any(b => b.MoreDataAvailable);
        } while (moreAvailable);

        return idents;
    }

    /// <summary>
    /// Reads a range of bytes from the EEPROM.
    /// </summary>
    /// <param name="address"></param>
    /// <param name="count"></param>
    /// <returns>The bytes or null if the bytes could not be read</returns>
    public List<byte>? ReadEeprom(ushort address, byte count)
    {
        Log.WriteLine($"Sending ReadEeprom block (Address: ${address:X4}, Count: ${count:X2})");
        SendBlock(new List<byte>
        {
            (byte)BlockTitle.ReadEeprom,
            count,
            (byte)(address >> 8),
            (byte)(address & 0xFF)
        });
        var blocks = ReceiveBlocks();

        if (blocks.Count == 1 && blocks[0] is NakBlock)
        {
            // Permissions issue
            return null;
        }

        blocks = blocks.Where(b => !b.IsAckNak).ToList();
        if (blocks.Count != 1)
        {
            throw new InvalidOperationException($"ReadEeprom returned {blocks.Count} blocks instead of 1");
        }
        return blocks[0].Body.ToList();
    }

    /// <summary>
    /// Reads a range of bytes from the RAM.
    /// </summary>
    /// <param name="address"></param>
    /// <param name="count"></param>
    /// <returns>The bytes or null if the bytes could not be read</returns>
    public List<byte>? ReadRam(ushort address, byte count)
    {
        Log.WriteLine($"Sending ReadRam block (Address: ${address:X4}, Count: ${count:X2})");
        SendBlock(new List<byte>
        {
            (byte)BlockTitle.ReadRam,
            count,
            (byte)(address >> 8),
            (byte)(address & 0xFF)
        });
        var blocks = ReceiveBlocks();

        if (blocks.Count == 1 && blocks[0] is NakBlock)
        {
            // Permissions issue
            return null;
        }

        blocks = blocks.Where(b => !b.IsAckNak).ToList();
        if (blocks.Count != 1)
        {
            throw new InvalidOperationException($"ReadEeprom returned {blocks.Count} blocks instead of 1");
        }
        return blocks[0].Body.ToList();
    }

    /// <summary>
    /// Reads a range of bytes from the CCM ROM.
    /// </summary>
    /// <param name="seg">0-15</param>
    /// <param name="msb">0-15</param>
    /// <param name="lsb">0-255</param>
    /// <param name="count">8(-12?)</param>
    /// <returns>The bytes or null if the bytes could not be read</returns>
    public List<byte>? ReadCcmRom(byte seg, byte msb, byte lsb, byte count)
    {
        Log.WriteLine(
            $"Sending ReadEeprom block (Address: ${seg:X2}{msb:X2}{lsb:X2}, Count: ${count:X2})");
        var block = new List<byte>
        {
            (byte)BlockTitle.ReadEeprom,
            count,
            msb,
            lsb,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            (byte)(seg << 4)
        };
        Log.WriteLine($"SEND {Utils.Dump(block)}");
        SendBlock(block);
        var blocks = ReceiveBlocks();
#if true
        foreach (var b in blocks)
        {
            Log.WriteLine($"Received:{Utils.Dump(b.Bytes)}");
        }
#endif

        if (blocks.Count == 1 && blocks[0] is NakBlock)
        {
            // Log.WriteLine($"RECV {Utils.Dump(blocks.First().Bytes)}");
            // Permissions issue
            return null;
        }

        blocks = blocks.Where(b => !b.IsAckNak).ToList();
        if (blocks.Count != 1)
        {
            throw new InvalidOperationException($"ReadEeprom returned {blocks.Count} blocks instead of 1");
        }
        return blocks[0].Body.ToList();
    }

    public bool WriteEeprom(ushort address, List<byte> values)
    {
        Log.WriteLine($"Sending WriteEeprom block (Address: ${address:X4}, Values: {Utils.DumpBytes(values)}");

        byte count = (byte)values.Count;
        var sendBody = new List<byte>
        {
            (byte)BlockTitle.WriteEeprom,
            count,
            (byte)(address >> 8),
            (byte)(address & 0xFF),
        };
        sendBody.AddRange(values);

        SendBlock(sendBody.ToList());
        var blocks = ReceiveBlocks();

        if (blocks.Count == 1 && blocks[0] is NakBlock)
        {
            // Permissions issue
            Log.WriteLine("WriteEeprom failed");
            return false;
        }

        blocks = blocks.Where(b => !b.IsAckNak).ToList();
        if (blocks.Count != 1)
        {
            Log.WriteLine($"WriteEeprom returned {blocks.Count} blocks instead of 1");
            return false;
        }

        var block = blocks[0];
        if (block is not WriteEepromResponseBlock)
        {
            Log.WriteLine($"Expected WriteEepromResponseBlock but got {block.GetType()}");
            return false;
        }

        if (!Enumerable.SequenceEqual(block.Body, sendBody.Skip(1).Take(4)))
        {
            Log.WriteLine("WriteEepromResponseBlock body does not match WriteEepromBlock");
            return false;
        }

        return true;
    }

    public List<byte>? ReadRomEeprom(ushort address, byte count)
    {
        Log.WriteLine($"Sending ReadRomEeprom block (Address: ${address:X4}, Count: ${count:X2})");
        SendBlock(new List<byte>
        {
            (byte)BlockTitle.ReadRomEeprom,
            count,
            (byte)(address >> 8),
            (byte)(address & 0xFF)
        });
        var blocks = ReceiveBlocks();

        if (blocks.Count == 1 && blocks[0] is NakBlock)
        {
            // Permissions issue
            return null;
        }

        blocks = blocks.Where(b => !b.IsAckNak).ToList();
        if (blocks.Count != 1)
        {
            throw new InvalidOperationException($"ReadRomEeprom returned {blocks.Count} blocks instead of 1");
        }
        return blocks[0].Body.ToList();
    }

    public void EndCommunication()
    {
        if (_isConnected)
        {
            Log.WriteLine("Sending EndCommunication block");
            SendBlock(new List<byte> { (byte)BlockTitle.End });
            _isConnected = false;
        }
    }

    public void SetDisconnected()
    {
        _isConnected = false;
        _blockCounter = null;
    }

    public void SendBlock(List<byte> blockBytes)
    {
        Thread.Sleep(25); // For better support of 4D0919035AJ

        var blockLength = (byte)(blockBytes.Count + 2);

        blockBytes.Insert(0, _blockCounter!.Value);
        _blockCounter++;

        blockBytes.Insert(0, blockLength);

        Thread.Sleep(TimeInterval.R6);

        foreach (var b in blockBytes)
        {
            WriteByteAndReadAck(b);
            Thread.Sleep(TimeInterval.R6);
        }

        KwpCommon.WriteByte(0x03); // Block end, does not get ACK'd
    }

    public List<Block> ReceiveBlocks()
    {
        var blocks = new List<Block>();

        try
        {
            while (true)
            {
                var block = ReceiveBlock();
                blocks.Add(block); // TODO: Maybe don't add the block if it's an Ack
                if (block is AckBlock || block is NakBlock)
                {
                    break;
                }
                SendAckBlock();
            }
        }
        catch (Exception ex)
        {
            Log.WriteLine($"Error receiving blocks: {ex.Message}");
            if (blocks.Count > 0)
            {
                Log.WriteLine("Blocks received:");
                foreach (var block in blocks)
                {
                    Log.WriteLine($"Block: {Utils.DumpBytes(block.Bytes)}");
                }
            }
            throw;
        }

        return blocks;
    }

    private void WriteByteAndReadAck(byte b)
    {
        KwpCommon.WriteByte(b);
        KwpCommon.ReadComplement(b);
    }

    public Block ReceiveBlock()
    {
        var blockBytes = new List<byte>();

        try
        {
            var blockLength = ReadAndAckByteFirst();
            blockBytes.Add(blockLength);

            var blockCounter = ReadBlockCounter();
            blockBytes.Add(blockCounter);

            var blockTitle = ReadAndAckByte();
            blockBytes.Add(blockTitle);

            for (var i = 0; i < blockLength - 3; i++)
            {
                var b = ReadAndAckByte();
                blockBytes.Add(b);
            }

            var blockEnd = KwpCommon.ReadByte();
            blockBytes.Add(blockEnd);
            if (blockEnd != 0x03)
            {
                throw new InvalidOperationException(
                    $"Received block end ${blockEnd:X2} but expected $03. Block bytes: {Utils.Dump(blockBytes)}");
            }

            return (BlockTitle)blockTitle switch
            {
                BlockTitle.ACK => new AckBlock(blockBytes),
                BlockTitle.GroupReadResponseWithText => new GroupReadResponseWithTextBlock(blockBytes),
                BlockTitle.ActuatorTestResponse => new ActuatorTestResponseBlock(blockBytes),
                BlockTitle.AsciiData =>
                    blockBytes[3] == 0x00 ? new CodingWscBlock(blockBytes) : new AsciiDataBlock(blockBytes),
                BlockTitle.Custom => new CustomBlock(blockBytes),
                BlockTitle.NAK => new NakBlock(blockBytes),
                BlockTitle.ReadEepromResponse => new ReadEepromResponseBlock(blockBytes),
                BlockTitle.FaultCodesResponse => new FaultCodesBlock(blockBytes),
                BlockTitle.ReadRomEepromResponse => new ReadRomEepromResponse(blockBytes),
                BlockTitle.WriteEepromResponse => new WriteEepromResponseBlock(blockBytes),
                BlockTitle.AdaptationResponse => new AdaptationResponseBlock(blockBytes),
                BlockTitle.GroupReadResponse => new GroupReadResponseBlock(blockBytes),
                BlockTitle.RawDataReadResponse => new RawDataReadResponseBlock(blockBytes),
                BlockTitle.SecurityAccessMode2 => new SecurityAccessMode2Block(blockBytes),
                _ => new UnknownBlock(blockBytes),
            };
        }
        catch (Exception ex)
        {
            Log.WriteLine($"Error receiving block: {ex.Message}");
            Log.WriteLine($"Partial block: {Utils.DumpBytes(blockBytes)}");
            if (ex is TimeoutException)
            {
                Log.WriteLine($"Read timeout: {KwpCommon.Interface.ReadTimeout}");
                Log.WriteLine($"Write timeout: {KwpCommon.Interface.WriteTimeout}");
            }
            throw;
        }
    }

    private void SendAckBlock()
    {
        var blockBytes = new List<byte> { (byte)BlockTitle.ACK };
        SendBlock(blockBytes);
    }

    private byte ReadBlockCounter()
    {
        var blockCounter = ReadAndAckByte();
        if (!_blockCounter.HasValue)
        {
            // First block
            _blockCounter = blockCounter;
        }
        else if (blockCounter != _blockCounter)
        {
            throw new InvalidOperationException(
                $"Received block counter ${blockCounter:X2} but expected ${_blockCounter:X2}");
        }
        _blockCounter++;
        return blockCounter;
    }

    private byte ReadAndAckByte()
    {
        var b = KwpCommon.ReadByte();
        Thread.Sleep(TimeInterval.R6);
        var complement = (byte)~b;
        KwpCommon.WriteByte(complement);
        return b;
    }

    /// <summary>
    /// https://github.com/gmenounos/kw1281test/issues/93
    /// </summary>
    private byte ReadAndAckByteFirst(int count = 0)
    {
        if (count > 5)
        {
            throw new InvalidOperationException(
                $"Cannot sync with {count} repeated attempts.");
        }
        var b = KwpCommon.ReadByte();
        if (b == 0x55)
        {
            var keywordLsb = KwpCommon.ReadByte();
            var keywordMsb = KwpCommon.ReadByte();
            var complement = (byte)~keywordMsb;
            BusyWait.Delay(25);
            KwpCommon.WriteByte(complement);
            Log.WriteLine($"Warning. Sync repeated.");
            return ReadAndAckByteFirst(count);
        }
        else
        {
            Thread.Sleep(TimeInterval.R6);
            var complement = (byte)~b;
            KwpCommon.WriteByte(complement);
            return b;
        }
    }

    public void KeepAlive()
    {
        SendAckBlock();
        var block = ReceiveBlock();
        if (block is not AckBlock)
        {
            throw new InvalidOperationException(
                $"Received 0x{block.Title:X2} block but expected ACK");
        }
    }

    public ActuatorTestResponseBlock? ActuatorTest(byte value)
    {
        Log.WriteLine($"Sending actuator test 0x{value:X2} block");
        SendBlock(new List<byte>
        {
            (byte)BlockTitle.ActuatorTest,
            value
        });

        var blocks = ReceiveBlocks();
        blocks = blocks.Where(b => !b.IsAckNak).ToList();
        if (blocks.Count != 1)
        {
            Log.WriteLine($"ActuatorTest returned {blocks.Count} blocks instead of 1");
            return null;
        }

        var block = blocks[0];
        if (block is not ActuatorTestResponseBlock)
        {
            Log.WriteLine($"Expected ActuatorTestResponseBlock but got {block.GetType()}");
            return null;
        }

        return (ActuatorTestResponseBlock)block;
    }

    public List<FaultCode>? ReadFaultCodes()
    {
        Log.WriteLine($"Sending ReadFaultCodes block");
        SendBlock(new List<byte>
        {
            (byte)BlockTitle.FaultCodesRead
        });

        var blocks = ReceiveBlocks();
        blocks = blocks.Where(b => !b.IsAckNak).ToList();

        var faultCodes = new List<FaultCode>();
        foreach (var block in blocks)
        {
            if (block is not FaultCodesBlock)
            {
                Log.WriteLine($"Expected FaultCodesBlock but got {block.GetType()}");
                return null;
            }

            var faultCodesBlock = (FaultCodesBlock)block;
            faultCodes.AddRange(faultCodesBlock.FaultCodes);
        }

        return faultCodes;
    }

    public List<FaultCode>? ClearFaultCodes(int controllerAddress)
    {
        Log.WriteLine($"Sending ClearFaultCodes block");
        SendBlock(new List<byte>
        {
            (byte)BlockTitle.FaultCodesDelete
        });

        var blocks = ReceiveBlocks();
        blocks = blocks.Where(b => !b.IsAckNak).ToList();

        var faultCodes = new List<FaultCode>();
        foreach (var block in blocks)
        {
            if (block is not FaultCodesBlock)
            {
                Log.WriteLine($"Expected FaultCodesBlock but got {block.GetType()}");
                return null;
            }

            var faultCodesBlock = (FaultCodesBlock)block;
            faultCodes.AddRange(faultCodesBlock.FaultCodes);
        }

        return faultCodes;
    }

    public bool SetSoftwareCoding(int controllerAddress, int softwareCoding, int workshopCode)
    {
        // Workshop codes > 65535 overflow into the low bit of the software coding
        var bytes = new List<byte>
        {
            (byte)BlockTitle.SoftwareCoding,
            (byte)((softwareCoding * 2) / 256),
            (byte)((softwareCoding * 2) % 256),
            (byte)((workshopCode & 65535) / 256),
            (byte)(workshopCode % 256)
        };

        if (workshopCode > 65535)
        {
            bytes[2]++;
        }

        Log.WriteLine($"Sending SoftwareCoding block");
        SendBlock(bytes);

        var blocks = ReceiveBlocks();
        if (blocks.Count == 1 && blocks[0] is NakBlock)
        {
            return false;
        }

        var controllerInfo = new ControllerInfo(blocks.Where(b => !b.IsAckNak));
        return
            controllerInfo.SoftwareCoding == softwareCoding &&
            controllerInfo.WorkshopCode == workshopCode;
    }

    public bool AdaptationRead(byte channelNumber)
    {
        var bytes = new List<byte>
        {
            (byte)BlockTitle.AdaptationRead,
            channelNumber
        };

        Log.WriteLine($"Sending AdaptationRead block");
        SendBlock(bytes);

        return ReceiveAdaptationBlock();
    }

    public bool AdaptationTest(byte channelNumber, ushort channelValue)
    {
        var bytes = new List<byte>
        {
            (byte)BlockTitle.AdaptationTest,
            channelNumber,
            (byte)(channelValue / 256),
            (byte)(channelValue % 256)
        };

        Log.WriteLine($"Sending AdaptationTest block");
        SendBlock(bytes);

        return ReceiveAdaptationBlock();
    }

    public bool AdaptationSave(byte channelNumber, ushort channelValue, int workshopCode)
    {
        var bytes = new List<byte>
        {
            (byte)BlockTitle.AdaptationSave,
            channelNumber,
            (byte)(channelValue / 256),
            (byte)(channelValue % 256),
            (byte)(workshopCode >> 16),
            (byte)((workshopCode >> 8) & 0xFF),
            (byte)(workshopCode & 0xFF)
        };

        Log.WriteLine($"Sending AdaptationSave block");
        SendBlock(bytes);

        return ReceiveAdaptationBlock();
    }

    private bool ReceiveAdaptationBlock()
    {
        var responseBlock = ReceiveBlock();
        if (responseBlock is NakBlock)
        {
            Log.WriteLine($"Received a NAK.");
            return false;
        }

        if (responseBlock is not AdaptationResponseBlock adaptationResponse)
        {
            Log.WriteLine($"Expected an Adaptation response block but received a ${responseBlock.Title:X2} block.");
            return false;
        }

        Log.WriteLine($"Adaptation value: {adaptationResponse.ChannelValue}");

        return true;
    }

    public bool GroupRead(byte groupNumber, bool useBasicSetting = false)
    {
        if (groupNumber == 0)
        {
            return RawDataRead(useBasicSetting);
        }

        if (useBasicSetting)
        {
            Log.WriteLine($"Sending Basic Setting Read blocks...");
        }
        else
        {
            Log.WriteLine($"Sending Group Read blocks...");
        }

        GroupReadResponseWithTextBlock? textBlock = null;

        Log.WriteLine("[Up arrow | Down arrow | Q to quit]", LogDest.Console);
        while (true)
        {
            if (Console.KeyAvailable)
            {
                var keyInfo = Console.ReadKey(intercept: true);
                if (keyInfo.Key == ConsoleKey.UpArrow)
                {
                    if (groupNumber < 255)
                    {
                        groupNumber++;
                    }
                }
                else if (keyInfo.Key == ConsoleKey.DownArrow)
                {
                    if (groupNumber > 1)
                    {
                        groupNumber--;
                    }
                }
                else if (keyInfo.Key == ConsoleKey.Q)
                {
                    break;
                }
            }

            var bytes = new List<byte>
            {
                (byte)(useBasicSetting ? BlockTitle.BasicSettingRead : BlockTitle.GroupRead),
                groupNumber
            };
            SendBlock(bytes);

            var responseBlock = ReceiveBlock();
            if (responseBlock is NakBlock)
            {
                Overlay($"Group {groupNumber:D3}: Not Available");
            }
            else if (responseBlock is GroupReadResponseWithTextBlock groupReadResponseWithText)
            {
                Log.WriteLine($"{groupReadResponseWithText}", LogDest.File);
                textBlock = groupReadResponseWithText;
            }
            else if (responseBlock is GroupReadResponseBlock groupReading)
            {
                Overlay($"Group {groupNumber:D3}: {groupReading}");
            }
            else if (responseBlock is RawDataReadResponseBlock rawData)
            {
                if (textBlock != null && rawData.Body.Count > 0)
                {
                    var sb = new StringBuilder($"Group {groupNumber:D3}: ");
                    sb.Append(textBlock.GetText(rawData.Body[0]));
                    sb.Append(Utils.DumpDecimal(rawData.Body.Skip(1)));
                    Overlay(sb.ToString());
                }
                else
                {
                    Overlay($"Group {groupNumber:D3}: {rawData}");
                }
            }
            else
            {
                Log.WriteLine($"Expected a Group Reading response block but received a ${responseBlock.Title:X2} block.");
                return false;
            }
        }
        Log.WriteLine(LogDest.Console);

        return true;
    }

    private bool RawDataRead(bool useBasicSetting)
    {
        if (useBasicSetting)
        {
            Log.WriteLine($"Sending Basic Setting Raw Data Read block");
        }
        else
        {
            Log.WriteLine($"Sending Raw Data Read block");
        }

        Log.WriteLine("[Press a key to quit]", LogDest.Console);
        while (!Console.KeyAvailable)
        {
            var bytes = new List<byte>
            {
                (byte)(useBasicSetting ? BlockTitle.BasicSettingRawDataRead : BlockTitle.RawDataRead)
            };
            SendBlock(bytes);

            var responseBlock = ReceiveBlock();

            if (responseBlock is not RawDataReadResponseBlock rawDataReadResponse)
            {
                Log.WriteLine($"Expected a Raw Data Read response block but received a ${responseBlock.Title:X2} block.");
                return false;
            }

            Overlay(rawDataReadResponse.ToString());
        }
        Log.WriteLine(LogDest.Console);

        return true;
    }

    public List<byte> ReadSecureImmoAccess(List<byte> blockBytes)
    {

        blockBytes.Insert(0, (byte)BlockTitle.SecurityImmoAccess1);

        Log.WriteLine($"Sending ReadSecureImmoAccess block: {Utils.DumpBytes(blockBytes)}");

        SendBlock(blockBytes);
        var blocks = ReceiveBlocks();

        if (blocks.Count == 1 && blocks[0] is NakBlock)
        {
            return [];
        }

        blocks = blocks.Where(b => !b.IsAckNak).ToList();
        if (blocks.Count != 1)
        {
            throw new InvalidOperationException($"ReadRomEeprom returned {blocks.Count} blocks instead of 1");
        }
        return blocks[0].Body.ToList();
    }

    /// <summary>
    /// Erase the current console line and replace it with message.
    /// Also writes the message to the log.
    /// </summary>
    private static void Overlay(string message)
    {
        (int left, int top) = Console.GetCursorPosition();
        Console.SetCursorPosition(0, top);
        if (left > 0)
        {
            Log.Write(new string(' ', left), LogDest.Console);
            Console.SetCursorPosition(0, top);
        }
        Log.Write(message, LogDest.Console);
        Log.WriteLine(message, LogDest.File);
    }

    private static class TimeInterval
    {
        /// <summary>
        /// Time to wait in milliseconds after receiving a byte from the ECU before sending the next byte.
        /// Valid range: 1-50ms (according to SAE J2818)
        /// </summary>
        public const int R6 = 2;
    }

    public IKwpCommon KwpCommon { get; }

    private bool _isConnected;

    private byte? _blockCounter;

    public KW1281Dialog(IKwpCommon kwpCommon)
    {
        KwpCommon = kwpCommon;
        _isConnected = false;
        _blockCounter = null;
    }
}

/// <summary>
/// Used for commands such as ActuatorTest which need to be kept alive with ACKs while waiting
/// for user input.
/// </summary>
internal class KW1281KeepAlive : IDisposable
{
    private readonly IKW1281Dialog _kw1281Dialog;
    private volatile bool _cancel = false;
    private Task? _keepAliveTask = null;

    public KW1281KeepAlive(IKW1281Dialog kw1281Dialog)
    {
        _kw1281Dialog = kw1281Dialog;
    }

    public ActuatorTestResponseBlock? ActuatorTest(byte value)
    {
        Pause();
        var result = _kw1281Dialog.ActuatorTest(value);
        Resume();
        return result;
    }

    public void Dispose()
    {
        Pause();
    }

    private void Pause()
    {
        _cancel = true;
        if (_keepAliveTask != null)
        {
            _keepAliveTask.Wait();
        }
    }

    private void Resume()
    {
        _keepAliveTask = Task.Run(KeepAlive);
    }

    private void KeepAlive()
    {
        _cancel = false;
        while (!_cancel)
        {
            _kw1281Dialog.KeepAlive();
            Log.Write(".", LogDest.Console);
        }
    }
}


================================================
FILE: Kwp2000/DiagnosticService.cs
================================================
namespace BitFab.KW1281Test.Kwp2000
{
    public enum DiagnosticService : byte
    {
        startDiagnosticSession = 0x10,
        ecuReset = 0x11,
        readEcuIdentification = 0x1A,
        stopDiagnosticSession = 0x20,
        readMemoryByAddress = 0x23,
        securityAccess = 0x27,
        startRoutineByLocalIdentifier = 0x31,
        requestDownload = 0x34,
        transferData = 0x36,
        writeMemoryByAddress = 0x3D,
        testerPresent = 0x3E,
        startCommunication = 0x81,
        stopCommunication = 0x82,
        accessTimingParameters = 0x83,
    };
}


================================================
FILE: Kwp2000/KW2000Dialog.cs
================================================
using BitFab.KW1281Test.Kwp2000;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading;
using Service = BitFab.KW1281Test.Kwp2000.DiagnosticService;

namespace BitFab.KW1281Test
{
    internal class KW2000Dialog
    {
        private const byte _testerAddress = 0xF1;

        /// <summary>
        /// Inter-command delay (milliseconds)
        /// </summary>
        public int P3 { get; set; } = 55;

        /// <summary>
        /// Inter-byte delay (milliseconds)
        /// </summary>
        public int P4 { get; set; } = 5;

        public void DumpMem(uint address, uint length, string dumpFileName)
        {
            StartDiagnosticSession(0x84, 0x14);

            Thread.Sleep(350);

            Log.WriteLine($"Saving memory dump to {dumpFileName}");
            DumpMemory(address, length, maxReadLength: 32, dumpFileName);
            Log.WriteLine($"Saved memory dump to {dumpFileName}");

            EcuReset(0x01);
        }

        private void DumpMemory(
            uint startAddr, uint length, byte maxReadLength, string fileName)
        {
            using var fs = File.Create(fileName, maxReadLength, FileOptions.WriteThrough);
            for (uint addr = startAddr; addr < (startAddr + length); addr += maxReadLength)
            {
                var readLength = (byte)Math.Min(startAddr + length - addr, maxReadLength);
                try
                {
                    var blockBytes = ReadMemoryByAddress(addr, readLength);
                    fs.Write(blockBytes, 0, blockBytes.Length);

                    if (blockBytes.Length != readLength)
                    {
                        throw new InvalidOperationException(
                            $"Expected {readLength} bytes from ReadMemoryByAddress() but received {blockBytes.Length} bytes");
                    }
                }
                catch (NegativeResponseException)
                {
                    // Access not allowed?
                    Log.WriteLine("Failed to read memory.");
                }
                finally
                {
                    fs.Flush();
                }
            }
        }

        public void StartDiagnosticSession(byte v1, byte v2)
        {
            var responseMessage = SendReceive(Service.startDiagnosticSession, new[] { v1, v2 });
            if (responseMessage.Body[0] != v1)
            {
                throw new InvalidOperationException($"Unexpected diagnosticMode: {responseMessage.Body[0]:X2}");
            }
        }

        public void EcuReset(byte value)
        {
            var responseMessage = SendReceive(Service.ecuReset, new[] { value });
        }

        public byte[] ReadMemoryByAddress(uint address, byte count)
        {
            var addressBytes = Utils.GetBytes(address);

            var responseMessage = SendReceive(Service.readMemoryByAddress,
                new byte[]
                {
                    addressBytes[2], addressBytes[1], addressBytes[0],
                    count
                });

            return responseMessage.Body.ToArray();
        }

        public byte[] WriteMemoryByAddress(uint address, byte count, byte[] data)
        {
            var addressBytes = Utils.GetBytes(address);

            var messageBytes = new List<byte>
            {
                addressBytes[2],
                addressBytes[1],
                addressBytes[0],
                count
            };
            messageBytes.AddRange(data);

            var responseMessage = SendReceive(Service.writeMemoryByAddress,
                messageBytes.ToArray());

            return responseMessage.Body.ToArray();
        }

        public Kwp2000Message SendReceive(
            Service service, byte[] body, bool excludeAddresses = false)
        {
            SendMessage(service, body, excludeAddresses);

            while (true)
            {
                var message = ReceiveMessage();

                if (message.SrcAddress.HasValue)
                {
                    if (message.SrcAddress != _controllerAddress)
                    {
                        throw new InvalidOperationException($"Unexpected SrcAddress: {message.SrcAddress:X2}");
                    }

                    if (message.DestAddress != _testerAddress)
                    {
                        throw new InvalidOperationException($"Unexpected DestAddress: {message.DestAddress:X2}");
                    }
                }

                if ((byte)message.Service == 0x7F)
                {
                    if (message.Body[0] == (byte)service &&
                        message.Body[1] == (byte)ResponseCode.reqCorrectlyRcvdRspPending)
                    {
                        continue;
                    }
                    throw new NegativeResponseException(message);
                }

                if (!message.IsPositiveResponse(service))
                {
                    throw new InvalidOperationException($"Unexpected response: {message.Service}");
                }

                return message;
            }
        }

        public void SendMessage(Service service, byte[] body, bool excludeAddresses = false)
        {
            static void Sleep(int ms)
            {
                var maxTick = Stopwatch.GetTimestamp() + Stopwatch.Frequency / 1000 * ms;
                while (Stopwatch.GetTimestamp() < maxTick)
                    ;
            }

            Kwp2000Message message;
            if (excludeAddresses)
            {
                message = new Kwp2000Message(service, body);
            }
            else
            {
                message = new Kwp2000Message(
                    _controllerAddress, _testerAddress, service, body);
            }
            var checksum = message.CalcChecksum();
            Sleep(P3);

            foreach (var b in message.HeaderBytes)
            {
                _kwpCommon.WriteByte(b);
                Sleep(P4);
            }

            _kwpCommon.WriteByte((byte)message.Service);
            Sleep(P4);

            foreach (var b in message.Body)
            {
                _kwpCommon.Writ
Download .txt
gitextract_0ekb2w7j/

├── .gitattributes
├── .github/
│   └── FUNDING.yml
├── .gitignore
├── .vscode/
│   └── launch.json
├── BlockTitle.cs
├── Blocks/
│   ├── AckBlock.cs
│   ├── ActuatorTestResponseBlock.cs
│   ├── AdaptationResponseBlock.cs
│   ├── AsciiDataBlock.cs
│   ├── Block.cs
│   ├── CodingWscBlock.cs
│   ├── CustomBlock.cs
│   ├── FaultCodesBlock.cs
│   ├── GroupReadResponseBlock.cs
│   ├── GroupReadResponseWithTextBlock.cs
│   ├── NakBlock.cs
│   ├── RawDataReadResponseBlock.cs
│   ├── ReadEepromResponseBlock.cs
│   ├── ReadRomEepromResponse.cs
│   ├── SecurityAccessMode2Block.cs
│   ├── SensorValue.cs
│   ├── UnknownBlock.cs
│   └── WriteEepromResponseBlock.cs
├── BusyWait.cs
├── Cluster/
│   ├── AudiC5Cluster.cs
│   ├── BoschRBxCluster.cs
│   ├── ICluster.cs
│   ├── MarelliCluster.cs
│   ├── MotometerBOOCluster.cs
│   ├── VdoCluster.cs
│   └── VdoKeyFinder.cs
├── ControllerAddress.cs
├── ControllerIdent.cs
├── ControllerInfo.cs
├── EDC15/
│   ├── Edc15VM.cs
│   └── Loader.a66
├── Interface/
│   ├── FtdiInterface.cs
│   ├── GenericInterface.cs
│   ├── IInterface.cs
│   └── LinuxInterface.cs
├── KW1281Dialog.cs
├── Kwp2000/
│   ├── DiagnosticService.cs
│   ├── KW2000Dialog.cs
│   ├── Kwp2000Message.cs
│   ├── NegativeResponseException.cs
│   └── ResponseCode.cs
├── KwpCommon.cs
├── LICENSE.txt
├── Logging/
│   ├── ConsoleLog.cs
│   ├── FileLog.cs
│   └── ILog.cs
├── Program.cs
├── Publish_Mac.ps1
├── Publish_Win.ps1
├── README.md
├── Tester.cs
├── Tests/
│   ├── BitFab.KW1281Test.Tests.csproj
│   ├── Cluster/
│   │   ├── MarelliClusterTests.cs
│   │   └── VdoClusterTests.cs
│   ├── GlobalUsings.cs
│   ├── ProgramTests.cs
│   ├── TesterTests.cs
│   └── UtilsTests.cs
├── UnableToProceedException.cs
├── UnexpectedProtocolException.cs
├── Utils.cs
└── kw1281test.slnx
Download .txt
SYMBOL INDEX (458 symbols across 55 files)

FILE: BlockTitle.cs
  type BlockTitle (line 3) | public enum BlockTitle : byte

FILE: Blocks/AckBlock.cs
  class AckBlock (line 6) | internal class AckBlock : Block
    method AckBlock (line 8) | public AckBlock(List<byte> bytes) : base(bytes)
    method Dump (line 13) | private void Dump()

FILE: Blocks/ActuatorTestResponseBlock.cs
  class ActuatorTestResponseBlock (line 5) | internal class ActuatorTestResponseBlock : Block
    method ActuatorTestResponseBlock (line 7) | public ActuatorTestResponseBlock(List<byte> bytes) : base(bytes)
    method Dump (line 25) | private void Dump()

FILE: Blocks/AdaptationResponseBlock.cs
  class AdaptationResponseBlock (line 5) | internal class AdaptationResponseBlock : Block
    method AdaptationResponseBlock (line 7) | public AdaptationResponseBlock(List<byte> bytes) : base(bytes)
    method Dump (line 16) | private void Dump()

FILE: Blocks/AsciiDataBlock.cs
  class AsciiDataBlock (line 7) | internal class AsciiDataBlock : Block
    method AsciiDataBlock (line 9) | public AsciiDataBlock(List<byte> bytes) : base(bytes)
    method ToString (line 16) | public override string ToString()
    method Dump (line 26) | private void Dump()

FILE: Blocks/Block.cs
  class Block (line 9) | class Block
    method Block (line 11) | public Block(List<byte> bytes)

FILE: Blocks/CodingWscBlock.cs
  class CodingWscBlock (line 6) | internal class CodingWscBlock : Block
    method CodingWscBlock (line 8) | public CodingWscBlock(List<byte> bytes) : base(bytes)
    method ToString (line 22) | public override string ToString()

FILE: Blocks/CustomBlock.cs
  class CustomBlock (line 5) | internal class CustomBlock : Block
    method CustomBlock (line 7) | public CustomBlock(List<byte> bytes) : base(bytes)
    method Dump (line 12) | private void Dump()

FILE: Blocks/FaultCodesBlock.cs
  class FaultCodesBlock (line 6) | internal class FaultCodesBlock : Block
    method FaultCodesBlock (line 8) | public FaultCodesBlock(List<byte> bytes) : base(bytes)
  type FaultCode (line 38) | internal struct FaultCode
    method FaultCode (line 40) | public FaultCode(int dtc, int status)
    method ToString (line 46) | public override string ToString()

FILE: Blocks/GroupReadResponseBlock.cs
  class GroupReadResponseBlock (line 8) | internal class GroupReadResponseBlock : Block
    method GroupReadResponseBlock (line 10) | public GroupReadResponseBlock(List<byte> bytes) : base(bytes)
    method ToString (line 32) | public override string ToString()

FILE: Blocks/GroupReadResponseWithTextBlock.cs
  class GroupReadResponseWithTextBlock (line 8) | internal class GroupReadResponseWithTextBlock : Block
    method GroupReadResponseWithTextBlock (line 10) | public GroupReadResponseWithTextBlock(List<byte> bytes)
    method GetText (line 52) | public string GetText(int i)
    method ToString (line 62) | public override string ToString()
    class SubBlock (line 76) | class SubBlock
      method ToString (line 84) | public override string ToString()

FILE: Blocks/NakBlock.cs
  class NakBlock (line 6) | class NakBlock : Block
    method NakBlock (line 8) | public NakBlock(List<byte> bytes) : base(bytes)

FILE: Blocks/RawDataReadResponseBlock.cs
  class RawDataReadResponseBlock (line 5) | internal class RawDataReadResponseBlock : Block
    method RawDataReadResponseBlock (line 7) | public RawDataReadResponseBlock(List<byte> bytes) : base(bytes)
    method ToString (line 11) | public override string ToString()

FILE: Blocks/ReadEepromResponseBlock.cs
  class ReadEepromResponseBlock (line 5) | internal class ReadEepromResponseBlock : Block
    method ReadEepromResponseBlock (line 7) | public ReadEepromResponseBlock(List<byte> bytes) : base(bytes)
    method Dump (line 12) | private void Dump()

FILE: Blocks/ReadRomEepromResponse.cs
  class ReadRomEepromResponse (line 5) | internal class ReadRomEepromResponse : Block
    method ReadRomEepromResponse (line 7) | public ReadRomEepromResponse(List<byte> bytes) : base(bytes)
    method Dump (line 12) | private void Dump()

FILE: Blocks/SecurityAccessMode2Block.cs
  class SecurityAccessMode2Block (line 5) | internal class SecurityAccessMode2Block : Block
    method SecurityAccessMode2Block (line 7) | public SecurityAccessMode2Block(List<byte> bytes) : base(bytes)
    method Dump (line 12) | private void Dump()

FILE: Blocks/SensorValue.cs
  class SensorValue (line 5) | public class SensorValue
    method SensorValue (line 13) | public SensorValue(byte sensorID, byte a, byte b)
    method ToString (line 20) | public override string ToString()

FILE: Blocks/UnknownBlock.cs
  class UnknownBlock (line 5) | internal class UnknownBlock : Block
    method UnknownBlock (line 7) | public UnknownBlock(List<byte> bytes) : base(bytes)
    method Dump (line 12) | private void Dump()

FILE: Blocks/WriteEepromResponseBlock.cs
  class WriteEepromResponseBlock (line 6) | internal class WriteEepromResponseBlock : Block
    method WriteEepromResponseBlock (line 8) | public WriteEepromResponseBlock(List<byte> bytes) : base(bytes)
    method Dump (line 13) | private void Dump()

FILE: BusyWait.cs
  class BusyWait (line 5) | public class BusyWait
    method BusyWait (line 10) | public BusyWait(long msPerCycle)
    method DelayUntilNextCycle (line 15) | public void DelayUntilNextCycle()
    method Delay (line 25) | public static void Delay(long ms)

FILE: Cluster/AudiC5Cluster.cs
  class AudiC5Cluster (line 12) | internal class AudiC5Cluster : ICluster
    method UnlockForEepromReadWrite (line 14) | public void UnlockForEepromReadWrite()
    method DumpEeprom (line 57) | public string DumpEeprom(uint? address, uint? length, string? dumpFile...
    method DumpEeprom (line 120) | private void DumpEeprom(
    method ReadEepromByAddress (line 152) | private List<byte> ReadEepromByAddress(uint addr, byte readLength)
    method BlockTitle (line 182) | private static byte BlockTitle(IReadOnlyList<byte> blockBytes)
    method WriteBlock (line 187) | private void WriteBlock(IReadOnlyCollection<byte> bodyBytes)
    method ReadBlock (line 208) | private List<byte> ReadBlock()
    class Constants (line 250) | private static class Constants
    method AudiC5Cluster (line 263) | public AudiC5Cluster(IKW1281Dialog kw1281Dialog)

FILE: Cluster/BoschRBxCluster.cs
  class BoschRBxCluster (line 9) | class BoschRBxCluster : ICluster
    method UnlockForEepromReadWrite (line 11) | public void UnlockForEepromReadWrite()
    method DumpEeprom (line 16) | public string DumpEeprom(
    method SecurityAccess (line 28) | public bool SecurityAccess(byte accessMode)
    method ToggleRB4Mode (line 84) | public void ToggleRB4Mode()
    method CalcRBxKey (line 122) | static uint CalcRBxKey(uint seed)
    method BoschRBxCluster (line 130) | public BoschRBxCluster(KW2000Dialog kwp2000)

FILE: Cluster/ICluster.cs
  type ICluster (line 3) | internal interface ICluster
    method UnlockForEepromReadWrite (line 5) | void UnlockForEepromReadWrite();
    method DumpEeprom (line 7) | string DumpEeprom(uint? address, uint? length, string? dumpFileName);

FILE: Cluster/MarelliCluster.cs
  class MarelliCluster (line 9) | class MarelliCluster : ICluster
    method UnlockForEepromReadWrite (line 11) | public void UnlockForEepromReadWrite()
    method DumpEeprom (line 16) | public string DumpEeprom(uint? address, uint? length, string? dumpFile...
    method GetDefaultAddress (line 26) | private ushort GetDefaultAddress()
    method DumpMem (line 55) | private byte[] DumpMem(
    method WriteMarelliBlockAndReadAck (line 200) | private bool WriteMarelliBlockAndReadAck(byte[] data)
    method HasSmallEeprom (line 248) | private bool HasSmallEeprom() => _smallEepromEcus.Any(model => _ecuInf...
    method HasLargeEeprom (line 268) | private bool HasLargeEeprom() => _largeEepromEcus.Any(model => _ecuInf...
    method GetSkc (line 274) | public static ushort? GetSkc(byte[] buf)
    method FindImmobilizerId (line 296) | private static int? FindImmobilizerId(IReadOnlyList<byte> buf)
    method FindKeyCount (line 337) | private static int? FindKeyCount(IReadOnlyList<byte> buf)
    method MarelliCluster (line 365) | public MarelliCluster(IKW1281Dialog kwp1281, string ecuInfo)

FILE: Cluster/MotometerBOOCluster.cs
  class MotometerBOOCluster (line 8) | internal class MotometerBOOCluster : ICluster
    method UnlockForEepromReadWrite (line 10) | public void UnlockForEepromReadWrite()
    method SendCustom (line 48) | private bool SendCustom(int first, int second)
    method DumpEeprom (line 99) | public string DumpEeprom(
    method GetClusterInfo (line 130) | private string GetClusterInfo()
    method MotometerBOOCluster (line 146) | public MotometerBOOCluster(IKW1281Dialog kwp1281)

FILE: Cluster/VdoCluster.cs
  class VdoCluster (line 11) | internal class VdoCluster : ICluster
    method UnlockForEepromReadWrite (line 13) | public void UnlockForEepromReadWrite()
    method DumpEeprom (line 39) | public string DumpEeprom(
    method CustomReadSoftwareVersion (line 54) | public Dictionary<int, Block> CustomReadSoftwareVersion()
    method CustomReset (line 86) | public void CustomReset()
    method CustomReadMemory (line 92) | public List<byte> CustomReadMemory(uint address, byte count)
    method CustomReadNecRom (line 120) | public List<byte> CustomReadNecRom(ushort address, byte count)
    method MapEeprom (line 138) | public List<byte> MapEeprom()
    method DumpMem (line 157) | public void DumpMem(string dumpFileName, uint startAddress, uint length)
    method SendCustom (line 190) | private List<Block> SendCustom(List<byte> blockCustomBytes)
    method Unlock (line 203) | public (bool succeeded, string? softwareVersion) Unlock()
    method SeedKeyAuthenticate (line 253) | public void SeedKeyAuthenticate(string? softwareVersion)
    method RequiresSeedKey (line 276) | public bool RequiresSeedKey()
    method GetAccessLevel (line 282) | private int? GetAccessLevel()
    method GetSkc (line 307) | public static ushort? GetSkc(byte[] bytes, int startAddress)
    method CustomUnlockAdditionalCommands (line 352) | private void CustomUnlockAdditionalCommands()
    method GetClusterUnlockCodes (line 362) | internal static byte[][] GetClusterUnlockCodes(string softwareVersion)
    method SoftwareVersionToString (line 493) | private static string SoftwareVersionToString(List<byte> versionBytes)
    method DumpMixedContent (line 752) | private static string DumpMixedContent(Block block)
    method DumpBinaryContent (line 762) | private static string DumpBinaryContent(Block block)
    method DumpEeprom (line 772) | private void DumpEeprom(
    method WriteRam (line 803) | public void WriteRam(ushort address, byte value)
    method VdoCluster (line 820) | public VdoCluster(IKW1281Dialog kwp1281)

FILE: Cluster/VdoKeyFinder.cs
  class VdoKeyFinder (line 7) | public static class VdoKeyFinder
    method FindKey (line 13) | public static byte[] FindKey(
    method CalculateKey (line 131) | private static byte[] CalculateKey(
    method Scramble (line 180) | private static void Scramble(byte[] work)
    method SetOrClearBits (line 189) | private static byte SetOrClearBits(
    method RightRotateFirst4Bytes (line 205) | private static void RightRotateFirst4Bytes(
    method LeftRotateFirstTwoBytes (line 219) | private static void LeftRotateFirstTwoBytes(
    method LeftRotate (line 234) | private static byte LeftRotate(

FILE: ControllerAddress.cs
  type ControllerAddress (line 6) | enum ControllerAddress

FILE: ControllerIdent.cs
  class ControllerIdent (line 11) | internal class ControllerIdent
    method ControllerIdent (line 13) | public ControllerIdent(IEnumerable<Block> blocks)
    method ToString (line 37) | public override string ToString()

FILE: ControllerInfo.cs
  class ControllerInfo (line 11) | internal class ControllerInfo
    method ControllerInfo (line 13) | public ControllerInfo(IEnumerable<Block> blocks)
    method ToString (line 48) | public override string ToString()

FILE: EDC15/Edc15VM.cs
  class Edc15VM (line 11) | public class Edc15VM
    method ReadWriteEeprom (line 13) | public byte[] ReadWriteEeprom(
    method DisplayEepromInfo (line 147) | public static void DisplayEepromInfo(ReadOnlySpan<byte> eeprom)
    method LVL41Auth (line 181) | private static byte[] LVL41Auth(long key, long key3, byte[] buf)
    method GetLoader (line 256) | private static byte[] GetLoader()
    method CalcPadding (line 299) | private static byte[] CalcPadding(ushort r6, ushort r1)
    method Checksum (line 321) | static void Checksum(ref ushort r6, ref ushort r1, byte[] buf)
    method Ror (line 350) | private static ushort Ror(ushort value, ushort count)
    method Rol (line 361) | private static ushort Rol(ushort value, ushort count, out ushort carry)
    method GetBuf (line 369) | private static ushort GetBuf(byte[] buf, int ix)
    method Edc15VM (line 377) | public Edc15VM(IKwpCommon kwpCommon, int controllerAddress)

FILE: Interface/FtdiInterface.cs
  class FtdiInterface (line 8) | internal class FtdiInterface : IInterface
    method FtdiInterface (line 14) | public FtdiInterface(string serialNumber, int baudRate)
    method Dispose (line 48) | public void Dispose()
    method ReadByte (line 62) | public byte ReadByte()
    method WriteByteRaw (line 78) | public void WriteByteRaw(byte b)
    method SetBreak (line 90) | public void SetBreak(bool on)
    method ClearReceiveBuffer (line 104) | public void ClearReceiveBuffer()
    method SetBaudRate (line 110) | public void SetBaudRate(int baudRate)
    method SetParity (line 116) | public void SetParity(Parity parity)
    method SetDtr (line 136) | public void SetDtr(bool on)
    method SetRts (line 150) | public void SetRts(bool on)
  class FT (line 198) | class FT : IDisposable
    method FT (line 223) | public FT()
    method InitDelegate (line 291) | private void InitDelegate<T>(string fieldName, out T delegateVal) wher...
    method Dispose (line 304) | public void Dispose()
    method AssertOk (line 313) | public static void AssertOk(FT.Status status)
    method SetVidPid (line 322) | public Status SetVidPid(
    method Open (line 329) | public Status Open(
    method Close (line 337) | public Status Close(
    method SetBaudRate (line 343) | public Status SetBaudRate(
    method SetDataCharacteristics (line 350) | public Status SetDataCharacteristics(
    method SetFlowControl (line 359) | public Status SetFlowControl(
    method SetDtr (line 368) | public Status SetDtr(
    method ClrDtr (line 374) | public Status ClrDtr(
    method SetRts (line 380) | public Status SetRts(
    method ClrRts (line 386) | public Status ClrRts(
    method SetTimeouts (line 392) | public Status SetTimeouts(
    method SetLatencyTimer (line 400) | public Status SetLatencyTimer(
    method Purge (line 407) | public Status Purge(
    method SetBreakOn (line 414) | public Status SetBreakOn(
    method SetBreakOff (line 420) | public Status SetBreakOff(
    method Read (line 426) | public Status Read(
    method Write (line 435) | public Status Write(
    type Status (line 444) | public enum Status : uint
    type OpenExFlags (line 468) | [Flags]
    type Bits (line 476) | public enum Bits : byte
    type StopBits (line 482) | public enum StopBits : byte
    type Parity (line 488) | public enum Parity : byte
    type FlowControl (line 497) | public enum FlowControl : ushort
    type PurgeMask (line 505) | [Flags]
  class SymbolNameAttribute (line 513) | [AttributeUsage(AttributeTargets.Delegate)]
    method SymbolNameAttribute (line 516) | public SymbolNameAttribute(string name)
  class FTDll (line 524) | static class FTDll

FILE: Interface/GenericInterface.cs
  class GenericInterface (line 5) | internal class GenericInterface : IInterface
    method GenericInterface (line 7) | public GenericInterface(string portName, int baudRate)
    method Dispose (line 26) | public void Dispose()
    method ReadByte (line 32) | public byte ReadByte()
    method WriteByteRaw (line 38) | public void WriteByteRaw(byte b)
    method SetBreak (line 44) | public void SetBreak(bool on)
    method ClearReceiveBuffer (line 49) | public void ClearReceiveBuffer()
    method SetBaudRate (line 54) | public void SetBaudRate(int baudRate)
    method SetParity (line 59) | public void SetParity(Parity parity)
    method SetDtr (line 64) | public void SetDtr(bool on)
    method SetRts (line 69) | public void SetRts(bool on)

FILE: Interface/IInterface.cs
  type IInterface (line 6) | public interface IInterface : IDisposable
    method ReadByte (line 14) | byte ReadByte();
    method WriteByteRaw (line 19) | void WriteByteRaw(byte b);
    method SetBreak (line 21) | void SetBreak(bool on);
    method ClearReceiveBuffer (line 23) | void ClearReceiveBuffer();
    method SetBaudRate (line 25) | void SetBaudRate(int baudRate);
    method SetParity (line 27) | void SetParity(Parity parity);
    method SetDtr (line 29) | void SetDtr(bool on);
    method SetRts (line 31) | void SetRts(bool on);

FILE: Interface/LinuxInterface.cs
  class LinuxInterface (line 12) | public class LinuxInterface : IInterface
    method ioctl (line 26) | [DllImport(libc, SetLastError = true)]
    method ioctl (line 29) | [DllImport(libc, SetLastError = true)]
    method open (line 33) | [DllImport(libc)]
    method close (line 36) | [DllImport(libc)]
    method read (line 39) | [DllImport(libc, CallingConvention = CallingConvention.StdCall)]
    method write (line 42) | [DllImport(libc, CallingConvention = CallingConvention.StdCall)]
    method tcflush (line 45) | [DllImport(libc, CallingConvention = CallingConvention.StdCall)]
    type Termios (line 53) | [StructLayout(LayoutKind.Sequential)]
    method _IOC (line 82) | private static uint _IOC(int dir, int type, int nr, int size)
    method _IOC_TYPECHECK (line 90) | private static int _IOC_TYPECHECK(Type type)
    method _IOR (line 95) | private static uint _IOR(int type, int nr, Type size)
    method _IOW (line 100) | private static uint _IOW(int type, int nr, Type size)
    method LinuxInterface (line 130) | public LinuxInterface(string portName, int baudRate)
    method GetTtyConfiguration (line 157) | private Termios GetTtyConfiguration()
    method SetTtyConfiguration (line 167) | private void SetTtyConfiguration(Termios termios)
    method ReadByte (line 181) | public byte ReadByte()
    method WriteByteRaw (line 193) | public void WriteByteRaw(byte b)
    method SetBreak (line 203) | public void SetBreak(bool on)
    method ClearReceiveBuffer (line 215) | public void ClearReceiveBuffer()
    method SetBaudRate (line 223) | public void SetBaudRate(int baudRate)
    method SetParity (line 240) | public void SetParity(Parity parity)
    method SetDtr (line 271) | public void SetDtr(bool on)
    method SetRts (line 298) | public void SetRts(bool on)
    method Dispose (line 320) | public void Dispose()
    method Dispose (line 331) | protected virtual void Dispose(bool disposing)

FILE: KW1281Dialog.cs
  type IKW1281Dialog (line 15) | internal interface IKW1281Dialog
    method Connect (line 17) | ControllerInfo Connect();
    method EndCommunication (line 19) | void EndCommunication();
    method SetDisconnected (line 21) | void SetDisconnected();
    method Login (line 23) | List<Block> Login(ushort code, int workshopCode);
    method ReadIdent (line 25) | List<ControllerIdent> ReadIdent();
    method ReadEeprom (line 30) | List<byte>? ReadEeprom(ushort address, byte count);
    method WriteEeprom (line 32) | bool WriteEeprom(ushort address, List<byte> values);
    method ReadRomEeprom (line 37) | List<byte>? ReadRomEeprom(ushort address, byte count);
    method ReadRam (line 42) | List<byte>? ReadRam(ushort address, byte count);
    method AdaptationRead (line 44) | bool AdaptationRead(byte channelNumber);
    method AdaptationTest (line 46) | bool AdaptationTest(byte channelNumber, ushort channelValue);
    method AdaptationSave (line 48) | bool AdaptationSave(byte channelNumber, ushort channelValue, int works...
    method SendBlock (line 50) | void SendBlock(List<byte> blockBytes);
    method ReceiveBlocks (line 52) | List<Block> ReceiveBlocks();
    method ReadCcmRom (line 54) | List<byte>? ReadCcmRom(byte seg, byte msb, byte lsb, byte count);
    method KeepAlive (line 59) | void KeepAlive();
    method ActuatorTest (line 61) | ActuatorTestResponseBlock? ActuatorTest(byte value);
    method ReadFaultCodes (line 63) | List<FaultCode>? ReadFaultCodes();
    method ClearFaultCodes (line 70) | List<FaultCode>? ClearFaultCodes(int controllerAddress);
    method SetSoftwareCoding (line 79) | bool SetSoftwareCoding(int controllerAddress, int softwareCoding, int ...
    method GroupRead (line 81) | bool GroupRead(byte groupNumber, bool useBasicSetting = false);
    method ReadSecureImmoAccess (line 83) | List<byte> ReadSecureImmoAccess(List<byte> blockBytes);
    method ReceiveBlock (line 86) | Block ReceiveBlock();
  class KW1281Dialog (line 89) | internal class KW1281Dialog : IKW1281Dialog
    method Connect (line 91) | public ControllerInfo Connect()
    method Login (line 98) | public List<Block> Login(ushort code, int workshopCode)
    method ReadIdent (line 114) | public List<ControllerIdent> ReadIdent()
    method ReadEeprom (line 142) | public List<byte>? ReadEeprom(ushort address, byte count)
    method ReadRam (line 174) | public List<byte>? ReadRam(ushort address, byte count)
    method ReadCcmRom (line 208) | public List<byte>? ReadCcmRom(byte seg, byte msb, byte lsb, byte count)
    method WriteEeprom (line 246) | public bool WriteEeprom(ushort address, List<byte> values)
    method ReadRomEeprom (line 293) | public List<byte>? ReadRomEeprom(ushort address, byte count)
    method EndCommunication (line 319) | public void EndCommunication()
    method SetDisconnected (line 329) | public void SetDisconnected()
    method SendBlock (line 335) | public void SendBlock(List<byte> blockBytes)
    method ReceiveBlocks (line 357) | public List<Block> ReceiveBlocks()
    method WriteByteAndReadAck (line 391) | private void WriteByteAndReadAck(byte b)
    method ReceiveBlock (line 397) | public Block ReceiveBlock()
    method SendAckBlock (line 459) | private void SendAckBlock()
    method ReadBlockCounter (line 465) | private byte ReadBlockCounter()
    method ReadAndAckByte (line 482) | private byte ReadAndAckByte()
    method ReadAndAckByteFirst (line 494) | private byte ReadAndAckByteFirst(int count = 0)
    method KeepAlive (line 521) | public void KeepAlive()
    method ActuatorTest (line 532) | public ActuatorTestResponseBlock? ActuatorTest(byte value)
    method ReadFaultCodes (line 559) | public List<FaultCode>? ReadFaultCodes()
    method ClearFaultCodes (line 586) | public List<FaultCode>? ClearFaultCodes(int controllerAddress)
    method SetSoftwareCoding (line 613) | public bool SetSoftwareCoding(int controllerAddress, int softwareCodin...
    method AdaptationRead (line 645) | public bool AdaptationRead(byte channelNumber)
    method AdaptationTest (line 659) | public bool AdaptationTest(byte channelNumber, ushort channelValue)
    method AdaptationSave (line 675) | public bool AdaptationSave(byte channelNumber, ushort channelValue, in...
    method ReceiveAdaptationBlock (line 694) | private bool ReceiveAdaptationBlock()
    method GroupRead (line 714) | public bool GroupRead(byte groupNumber, bool useBasicSetting = false)
    method RawDataRead (line 804) | private bool RawDataRead(bool useBasicSetting)
    method ReadSecureImmoAccess (line 839) | public List<byte> ReadSecureImmoAccess(List<byte> blockBytes)
    method Overlay (line 866) | private static void Overlay(string message)
    class TimeInterval (line 879) | private static class TimeInterval
    method KW1281Dialog (line 894) | public KW1281Dialog(IKwpCommon kwpCommon)
  class KW1281KeepAlive (line 906) | internal class KW1281KeepAlive : IDisposable
    method KW1281KeepAlive (line 912) | public KW1281KeepAlive(IKW1281Dialog kw1281Dialog)
    method ActuatorTest (line 917) | public ActuatorTestResponseBlock? ActuatorTest(byte value)
    method Dispose (line 925) | public void Dispose()
    method Pause (line 930) | private void Pause()
    method Resume (line 939) | private void Resume()
    method KeepAlive (line 944) | private void KeepAlive()

FILE: Kwp2000/DiagnosticService.cs
  type DiagnosticService (line 3) | public enum DiagnosticService : byte

FILE: Kwp2000/KW2000Dialog.cs
  class KW2000Dialog (line 12) | internal class KW2000Dialog
    method DumpMem (line 26) | public void DumpMem(uint address, uint length, string dumpFileName)
    method DumpMemory (line 39) | private void DumpMemory(
    method StartDiagnosticSession (line 69) | public void StartDiagnosticSession(byte v1, byte v2)
    method EcuReset (line 78) | public void EcuReset(byte value)
    method ReadMemoryByAddress (line 83) | public byte[] ReadMemoryByAddress(uint address, byte count)
    method WriteMemoryByAddress (line 97) | public byte[] WriteMemoryByAddress(uint address, byte count, byte[] data)
    method SendReceive (line 116) | public Kwp2000Message SendReceive(
    method SendMessage (line 157) | public void SendMessage(Service service, byte[] body, bool excludeAddr...
    method ReceiveMessage (line 199) | public Kwp2000Message ReceiveMessage()
    method KW2000Dialog (line 232) | public KW2000Dialog(IKwpCommon kwpCommon, byte controllerAddress)

FILE: Kwp2000/Kwp2000Message.cs
  class Kwp2000Message (line 9) | public class Kwp2000Message
    method CalcChecksum (line 23) | public byte CalcChecksum()
    method Kwp2000Message (line 34) | public Kwp2000Message(
    method Kwp2000Message (line 45) | public Kwp2000Message(
    method Kwp2000Message (line 56) | public Kwp2000Message(
    method ToString (line 93) | public override string ToString()
    method IsPositiveResponse (line 107) | public bool IsPositiveResponse(DiagnosticService service)
    method DescribeService (line 112) | public string DescribeService()
    method CalcFormatByte (line 142) | private static byte CalcFormatByte(IList<byte> body, bool excludeAddre...
    method CalcLengthByte (line 153) | private static byte? CalcLengthByte(IList<byte> body)

FILE: Kwp2000/NegativeResponseException.cs
  class NegativeResponseException (line 5) | public class NegativeResponseException : Exception
    method NegativeResponseException (line 7) | public NegativeResponseException(Kwp2000Message kwp2000Message)

FILE: Kwp2000/ResponseCode.cs
  type ResponseCode (line 6) | public enum ResponseCode

FILE: KwpCommon.cs
  type IKwpCommon (line 9) | public interface IKwpCommon
    method WakeUp (line 13) | int WakeUp(byte controllerAddress, bool evenParity = false, bool failQ...
    method ReadByte (line 15) | byte ReadByte();
    method WriteByte (line 21) | void WriteByte(byte b);
    method ReadComplement (line 23) | void ReadComplement(byte b);
  class KwpCommon (line 26) | internal class KwpCommon : IKwpCommon
    method WakeUp (line 30) | public int WakeUp(byte controllerAddress, bool evenParity, bool failQu...
    method WakeUpNoRetry (line 90) | private int WakeUpNoRetry(byte controllerAddress, bool evenParity)
    method ReadByte (line 146) | public byte ReadByte()
    method WriteByte (line 151) | public void WriteByte(byte b)
    method ReadComplement (line 156) | public void ReadComplement(byte b)
    method BitBang5Baud (line 175) | private void BitBang5Baud(byte b, bool evenParity)
    method WriteByteAndDiscardEcho (line 215) | private void WriteByteAndDiscardEcho(byte b)
    method KwpCommon (line 227) | public KwpCommon(IInterface @interface)

FILE: Logging/ConsoleLog.cs
  class ConsoleLog (line 5) | internal class ConsoleLog : ILog
    method Write (line 7) | public void Write(string message, LogDest dest)
    method WriteLine (line 15) | public void WriteLine(LogDest dest)
    method WriteLine (line 23) | public void WriteLine(string message, LogDest dest)
    method Close (line 31) | public void Close()
    method Dispose (line 35) | public void Dispose()

FILE: Logging/FileLog.cs
  class FileLog (line 6) | internal class FileLog : ILog
    method FileLog (line 10) | public FileLog(string filename)
    method WriteLine (line 15) | public void WriteLine(string message, LogDest dest)
    method WriteLine (line 27) | public void WriteLine(LogDest dest)
    method Write (line 39) | public void Write(string message, LogDest dest)
    method Close (line 51) | public void Close()
    method Dispose (line 56) | public void Dispose()

FILE: Logging/ILog.cs
  type LogDest (line 5) | internal enum LogDest
  type ILog (line 12) | internal interface ILog : IDisposable
    method Write (line 14) | void Write(string message, LogDest dest = LogDest.All);
    method WriteLine (line 16) | void WriteLine(LogDest dest = LogDest.All);
    method WriteLine (line 18) | void WriteLine(string message, LogDest dest = LogDest.All);
    method Close (line 20) | void Close();

FILE: Program.cs
  class Program (line 23) | class Program
    method Main (line 29) | static void Main(string[] args)
    method Run (line 56) | void Run(string[] args)
    method AutoScan (line 449) | private static void AutoScan(IInterface @interface)
    method ParseAddressesAndValues (line 487) | internal static bool ParseAddressesAndValues(
    method OpenPort (line 551) | private static IInterface OpenPort(string portName, int baudRate)
    method ShowUsage (line 571) | private static void ShowUsage()

FILE: Tester.cs
  class Tester (line 13) | internal class Tester
    method Tester (line 20) | public Tester(IInterface @interface, int controllerAddress)
    method Kwp1281Wakeup (line 27) | public ControllerInfo Kwp1281Wakeup(bool evenParityWakeup = false, boo...
    method Kwp2000Wakeup (line 43) | public KW2000Dialog Kwp2000Wakeup(bool evenParityWakeup = false)
    method EndCommunication (line 59) | public void EndCommunication()
    method ActuatorTest (line 66) | public void ActuatorTest()
    method AdaptationRead (line 91) | public void AdaptationRead(
    method AdaptationSave (line 102) | public void AdaptationSave(
    method AdaptationTest (line 113) | public void AdaptationTest(
    method BasicSettingRead (line 124) | public void BasicSettingRead(byte groupNumber)
    method ClarionVWPremium4SafeCode (line 129) | public void ClarionVWPremium4SafeCode()
    method ClearFaultCodes (line 160) | public void ClearFaultCodes()
    method DelcoVWPremium5SafeCode (line 185) | public void DelcoVWPremium5SafeCode()
    method DumpCcmRom (line 210) | public void DumpCcmRom(string? filename)
    method DumpClusterNecRom (line 264) | public void DumpClusterNecRom(string? filename)
    method FindLogins (line 311) | public void FindLogins(ushort goodLogin, int workshopCode)
    method ReadWriteEdc15Eeprom (line 341) | public byte[] ReadWriteEdc15Eeprom(
    method DumpEeprom (line 365) | public void DumpEeprom(uint address, uint length, string? filename)
    method DumpMarelliMem (line 383) | public void DumpMarelliMem(
    method DumpMem (line 397) | public void DumpMem(uint address, uint length, string? filename)
    method DumpRam (line 408) | public void DumpRam(uint startAddr, uint length, string? filename)
    method DumpRom (line 442) | public void DumpRom(uint startAddr, uint length, string? filename)
    method DumpRBxMem (line 480) | public string? DumpRBxMem(
    method GetClusterId (line 505) | public void GetClusterId()
    method GetSkc (line 542) | public void GetSkc()
    method FindAndParsePartNumber (line 773) | internal static string[] FindAndParsePartNumber(string ecuInfo)
    method GroupRead (line 789) | public void GroupRead(byte groupNumber)
    method LoadEeprom (line 794) | public void LoadEeprom(uint address, string filename)
    method MapEeprom (line 812) | public void MapEeprom(string? filename)
    method ReadEeprom (line 830) | public void ReadEeprom(uint address)
    method ReadRam (line 847) | public void ReadRam(uint address)
    method ReadRom (line 864) | public void ReadRom(uint address)
    method ReadFaultCodes (line 881) | public void ReadFaultCodes()
    method ReadIdent (line 894) | public void ReadIdent()
    method ReadSoftwareVersion (line 902) | public void ReadSoftwareVersion()
    method Reset (line 915) | public void Reset()
    method SetSoftwareCoding (line 928) | public void SetSoftwareCoding(
    method ToggleRB4Mode (line 942) | public void ToggleRB4Mode()
    method WriteEeprom (line 951) | public void WriteEeprom(uint address, byte value)
    method WriteRam (line 958) | public void WriteRam(uint address, byte value)
    method ClusterWriteRam (line 974) | private void ClusterWriteRam(ushort address, byte value)
    method BOOClusterDumpEeprom (line 997) | private string BOOClusterDumpEeprom(ushort startAddress, ushort length...
    method ClusterDumpEeprom (line 1022) | private string ClusterDumpEeprom(
    method CcmMapEeprom (line 1041) | private void CcmMapEeprom(string? filename)
    method ClusterMapEeprom (line 1060) | private void ClusterMapEeprom(string? filename)
    method CcmDumpEeprom (line 1071) | private void CcmDumpEeprom(ushort startAddress, ushort length, string?...
    method UnlockControllerForEepromReadWrite (line 1082) | private void UnlockControllerForEepromReadWrite()
    method DumpEeprom (line 1128) | private void DumpEeprom(
    method WriteEeprom (line 1159) | private void WriteEeprom(
    method CcmLoadEeprom (line 1181) | private void CcmLoadEeprom(ushort address, string filename)
    method ClusterLoadEeprom (line 1200) | private void ClusterLoadEeprom(ushort address, string filename)
    method ClusterDumpMem (line 1219) | private void ClusterDumpMem(uint startAddress, uint length, string? fi...
    method DecodeClusterId (line 1247) | private static (byte, byte) DecodeClusterId(byte b1, byte b2, byte b3,...

FILE: Tests/Cluster/MarelliClusterTests.cs
  class MarelliClusterTests (line 5) | [TestClass]
    method GetSkc_ReturnsCorrectSkc (line 8) | [TestMethod]
    method GetSkc_NoImmoIdAndNoKeyCountPattern_ReturnsNull (line 20) | [TestMethod]

FILE: Tests/Cluster/VdoClusterTests.cs
  class VdoClusterTests (line 6) | [TestClass]
    method GetClusterUnlockCodes_ReturnsCorrectCode (line 9) | [TestMethod]
    method ClusterUnlockCodes_ContainsKnownCodes (line 58) | [TestMethod]
    method ClusterUnlockCodes_ContainsNoDuplicates (line 91) | [TestMethod]

FILE: Tests/ProgramTests.cs
  class ProgramTests (line 3) | [TestClass]
    method ParseAddressesAndValues_NumberOfArgumentsIsOdd_ReturnsFalse (line 6) | [TestMethod]
    method ParseAddressesAndValues_ValidArguments_ReturnsList (line 14) | [TestMethod]
    method ParseAddressesAndValues_AddressTooLarge_ReturnsFalse (line 26) | [TestMethod]
    method ParseAddressesAndValues_ValueTooLarge_ReturnsFalse (line 35) | [TestMethod]

FILE: Tests/TesterTests.cs
  class TesterTests (line 3) | [TestClass]
    method FindAndParsePartNumber_ReturnsExpectedGroups (line 6) | [TestMethod]

FILE: Tests/UtilsTests.cs
  class UtilsTests (line 5) | [TestClass]
    method DumpMixedContent (line 8) | [TestMethod]

FILE: UnableToProceedException.cs
  class UnableToProceedException (line 5) | class UnableToProceedException : Exception

FILE: UnexpectedProtocolException.cs
  class UnexpectedProtocolException (line 5) | [Serializable]
    method UnexpectedProtocolException (line 8) | public UnexpectedProtocolException()
    method UnexpectedProtocolException (line 12) | public UnexpectedProtocolException(string? message) : base(message)
    method UnexpectedProtocolException (line 16) | public UnexpectedProtocolException(string? message, Exception? innerEx...

FILE: Utils.cs
  class Utils (line 8) | internal static class Utils
    method Dump (line 10) | public static string Dump(IEnumerable<byte> bytes)
    method DumpBytes (line 21) | public static string DumpBytes(IEnumerable<byte> bytes)
    method DumpDecimal (line 31) | public static string DumpDecimal(IEnumerable<byte> bytes)
    method DumpAscii (line 41) | public static string DumpAscii(IEnumerable<byte> bytes)
    method DumpMixedContent (line 51) | public static string DumpMixedContent(IEnumerable<byte> content)
    method ParseUint (line 81) | public static uint ParseUint(string numberString)
    method GetShort (line 104) | public static ushort GetShort(ReadOnlySpan<byte> buf, int offset)
    method GetShortBE (line 112) | public static ushort GetShortBE(byte[] buf, int offset)
    method GetBcd (line 120) | public static ushort GetBcd(byte[] buf, int offset)
    method GetBytes (line 138) | public static byte[] GetBytes(uint value)
    method RightRotate (line 156) | public static (byte result, bool carry) RightRotate(
    method LeftRotate (line 173) | public static (byte result, bool carry) LeftRotate(
    method SubtractWithCarry (line 187) | public static (byte result, bool carry) SubtractWithCarry(
    method AdjustParity (line 196) | public static byte AdjustParity(
Condensed preview — 67 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (319K chars).
[
  {
    "path": ".gitattributes",
    "chars": 2518,
    "preview": "###############################################################################\n# Set default behavior to automatically "
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 43,
    "preview": "custom: [\"https://paypal.me/GregMenounos\"]\n"
  },
  {
    "path": ".gitignore",
    "chars": 5784,
    "preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 253,
    "preview": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n"
  },
  {
    "path": "BlockTitle.cs",
    "chars": 2256,
    "preview": "namespace BitFab.KW1281Test\n{\n    public enum BlockTitle : byte\n    {\n        ReadIdent = 0x00,\n        ReadRam = 0x01,"
  },
  {
    "path": "Blocks/AckBlock.cs",
    "chars": 334,
    "preview": "using System;\nusing System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class AckBlock : Blo"
  },
  {
    "path": "Blocks/ActuatorTestResponseBlock.cs",
    "chars": 1689,
    "preview": "using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class ActuatorTestResponseBlock : "
  },
  {
    "path": "Blocks/AdaptationResponseBlock.cs",
    "chars": 618,
    "preview": "using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class AdaptationResponseBlock : Bl"
  },
  {
    "path": "Blocks/AsciiDataBlock.cs",
    "chars": 855,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Text;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal c"
  },
  {
    "path": "Blocks/Block.cs",
    "chars": 834,
    "preview": "using System.Collections.Generic;\nusing System.Linq;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    /// <summary>\n    /// KW"
  },
  {
    "path": "Blocks/CodingWscBlock.cs",
    "chars": 823,
    "preview": "using System.Collections.Generic;\nusing System.Linq;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class CodingWsc"
  },
  {
    "path": "Blocks/CustomBlock.cs",
    "chars": 483,
    "preview": "using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class CustomBlock : Block\n    {\n  "
  },
  {
    "path": "Blocks/FaultCodesBlock.cs",
    "chars": 1411,
    "preview": "using System.Collections.Generic;\nusing System.Linq;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class FaultCode"
  },
  {
    "path": "Blocks/GroupReadResponseBlock.cs",
    "chars": 1370,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\n\nnamespace BitFab.KW1281Test.Bloc"
  },
  {
    "path": "Blocks/GroupReadResponseWithTextBlock.cs",
    "chars": 2989,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\n\nnamespace BitFab.KW1281Test.Bloc"
  },
  {
    "path": "Blocks/NakBlock.cs",
    "chars": 204,
    "preview": "using System;\nusing System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    class NakBlock : Block\n    {\n"
  },
  {
    "path": "Blocks/RawDataReadResponseBlock.cs",
    "chars": 351,
    "preview": "using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class RawDataReadResponseBlock : B"
  },
  {
    "path": "Blocks/ReadEepromResponseBlock.cs",
    "chars": 497,
    "preview": "using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class ReadEepromResponseBlock : Bl"
  },
  {
    "path": "Blocks/ReadRomEepromResponse.cs",
    "chars": 498,
    "preview": "using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class ReadRomEepromResponse : Bloc"
  },
  {
    "path": "Blocks/SecurityAccessMode2Block.cs",
    "chars": 503,
    "preview": "using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class SecurityAccessMode2Block : B"
  },
  {
    "path": "Blocks/SensorValue.cs",
    "chars": 4499,
    "preview": "using System;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    public class SensorValue\n    {\n        public byte SensorID { g"
  },
  {
    "path": "Blocks/UnknownBlock.cs",
    "chars": 465,
    "preview": "using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class UnknownBlock : Block\n    {\n "
  },
  {
    "path": "Blocks/WriteEepromResponseBlock.cs",
    "chars": 514,
    "preview": "using System;\nusing System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class WriteEepromRes"
  },
  {
    "path": "BusyWait.cs",
    "chars": 719,
    "preview": "using System.Diagnostics;\n\nnamespace BitFab.KW1281Test;\n\npublic class BusyWait\n{\n    private readonly long _ticksPerCycl"
  },
  {
    "path": "Cluster/AudiC5Cluster.cs",
    "chars": 7647,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.IO.Ports;\nusing System.Linq;\nusing System."
  },
  {
    "path": "Cluster/BoschRBxCluster.cs",
    "chars": 4109,
    "preview": "using BitFab.KW1281Test.Kwp2000;\nusing System;\nusing System.Linq;\nusing System.Threading;\nusing Service = BitFab.KW1281"
  },
  {
    "path": "Cluster/ICluster.cs",
    "chars": 205,
    "preview": "namespace BitFab.KW1281Test.Cluster\n{\n    internal interface ICluster\n    {\n        void UnlockForEepromReadWrite();\n\n "
  },
  {
    "path": "Cluster/MarelliCluster.cs",
    "chars": 12952,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\n\nnamespace "
  },
  {
    "path": "Cluster/MotometerBOOCluster.cs",
    "chars": 4555,
    "preview": "using BitFab.KW1281Test.Blocks;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace BitFab.KW"
  },
  {
    "path": "Cluster/VdoCluster.cs",
    "chars": 27740,
    "preview": "using BitFab.KW1281Test.Blocks;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusi"
  },
  {
    "path": "Cluster/VdoKeyFinder.cs",
    "chars": 7879,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace BitFab.KW1281Test.Cluster\n{\n    public st"
  },
  {
    "path": "ControllerAddress.cs",
    "chars": 407,
    "preview": "namespace BitFab.KW1281Test\n{\n    /// <summary>\n    /// VW controller addresses\n    /// </summary>\n    enum ControllerA"
  },
  {
    "path": "ControllerIdent.cs",
    "chars": 1098,
    "preview": "using BitFab.KW1281Test.Blocks;\nusing System;\nusing System.Collections.Generic;\nusing System.Text;\n\nnamespace BitFab.KW"
  },
  {
    "path": "ControllerInfo.cs",
    "chars": 1484,
    "preview": "using BitFab.KW1281Test.Blocks;\nusing System;\nusing System.Collections.Generic;\nusing System.Text;\n\nnamespace BitFab.KW"
  },
  {
    "path": "EDC15/Edc15VM.cs",
    "chars": 14395,
    "preview": "using BitFab.KW1281Test.Kwp2000;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nus"
  },
  {
    "path": "EDC15/Loader.a66",
    "chars": 15519,
    "preview": "; Custom loader. When loaded at address 40E000 and started via startRoutineByLocalIdentifier 0x02,\n; it will accept seve"
  },
  {
    "path": "Interface/FtdiInterface.cs",
    "chars": 17575,
    "preview": "using System;\nusing System.IO.Ports;\nusing System.Reflection;\nusing System.Runtime.InteropServices;\n\nnamespace BitFab.K"
  },
  {
    "path": "Interface/GenericInterface.cs",
    "chars": 2023,
    "preview": "using System.IO.Ports;\n\nnamespace BitFab.KW1281Test.Interface\n{\n    internal class GenericInterface : IInterface\n    {\n"
  },
  {
    "path": "Interface/IInterface.cs",
    "chars": 833,
    "preview": "using System;\nusing System.IO.Ports;\n\nnamespace BitFab.KW1281Test.Interface\n{\n    public interface IInterface : IDispos"
  },
  {
    "path": "Interface/LinuxInterface.cs",
    "chars": 10472,
    "preview": "using System;\nusing System.IO;\nusing System.Runtime.InteropServices;\nusing System.IO.Ports;\n\nusing tcflag_t = System.UI"
  },
  {
    "path": "KW1281Dialog.cs",
    "chars": 28165,
    "preview": "using BitFab.KW1281Test.Blocks;\nusing BitFab.KW1281Test.Logging;\nusing System;\nusing System.Collections.Generic;\nusing "
  },
  {
    "path": "Kwp2000/DiagnosticService.cs",
    "chars": 584,
    "preview": "namespace BitFab.KW1281Test.Kwp2000\n{\n    public enum DiagnosticService : byte\n    {\n        startDiagnosticSession = 0"
  },
  {
    "path": "Kwp2000/KW2000Dialog.cs",
    "chars": 7777,
    "preview": "using BitFab.KW1281Test.Kwp2000;\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System"
  },
  {
    "path": "Kwp2000/Kwp2000Message.cs",
    "chars": 4639,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Text;\n\nnamesp"
  },
  {
    "path": "Kwp2000/NegativeResponseException.cs",
    "chars": 315,
    "preview": "using System;\n\nnamespace BitFab.KW1281Test.Kwp2000\n{\n    public class NegativeResponseException : Exception\n    {\n     "
  },
  {
    "path": "Kwp2000/ResponseCode.cs",
    "chars": 1280,
    "preview": "namespace BitFab.KW1281Test.Kwp2000\n{\n    /// <summary>\n    /// \n    /// </summary>\n    public enum ResponseCode\n    {\n"
  },
  {
    "path": "KwpCommon.cs",
    "chars": 7818,
    "preview": "using BitFab.KW1281Test.Interface;\nusing System;\nusing System.Collections.Generic;\nusing System.Runtime;\nusing System.T"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright © 2021 Greg Menounos\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "Logging/ConsoleLog.cs",
    "chars": 746,
    "preview": "using System;\n\nnamespace BitFab.KW1281Test.Logging\n{\n    internal class ConsoleLog : ILog\n    {\n        public void Wri"
  },
  {
    "path": "Logging/FileLog.cs",
    "chars": 1311,
    "preview": "using System;\nusing System.IO;\n\nnamespace BitFab.KW1281Test.Logging\n{\n    internal class FileLog : ILog\n    {\n        p"
  },
  {
    "path": "Logging/ILog.cs",
    "chars": 401,
    "preview": "using System;\n\nnamespace BitFab.KW1281Test.Logging\n{\n    internal enum LogDest\n    {\n        All,\n        Console,\n    "
  },
  {
    "path": "Program.cs",
    "chars": 21243,
    "preview": "global using static BitFab.KW1281Test.Program;\n\nusing BitFab.KW1281Test.Interface;\nusing BitFab.KW1281Test.Logging;\nusi"
  },
  {
    "path": "Publish_Mac.ps1",
    "chars": 1265,
    "preview": "dotnet publish kw1281test.csproj /p:PublishProfile=Win\ndotnet publish kw1281test.csproj /p:PublishProfile=Mac\ndotnet pub"
  },
  {
    "path": "Publish_Win.ps1",
    "chars": 1275,
    "preview": "dotnet publish kw1281test.csproj /p:PublishProfile=Win\ndotnet publish kw1281test.csproj /p:PublishProfile=Mac\ndotnet pub"
  },
  {
    "path": "README.md",
    "chars": 5795,
    "preview": "# kw1281test\nVW KW1281 Protocol Test Tool\n\nThis tool can send some KW1281 (and a few KW2000) commands over a dumb serial"
  },
  {
    "path": "Tester.cs",
    "chars": 43229,
    "preview": "using BitFab.KW1281Test.Cluster;\nusing BitFab.KW1281Test.EDC15;\nusing BitFab.KW1281Test.Interface;\nusing System;\nusing "
  },
  {
    "path": "Tests/BitFab.KW1281Test.Tests.csproj",
    "chars": 1304,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <ImplicitUsings"
  },
  {
    "path": "Tests/Cluster/MarelliClusterTests.cs",
    "chars": 831,
    "preview": "using BitFab.KW1281Test.Cluster;\n\nnamespace BitFab.KW1281Test.Tests.Cluster;\n\n[TestClass]\npublic class MarelliClusterTes"
  },
  {
    "path": "Tests/Cluster/VdoClusterTests.cs",
    "chars": 4978,
    "preview": "using BitFab.KW1281Test.Cluster;\nusing Shouldly;\n\nnamespace BitFab.KW1281Test.Tests.Cluster\n{\n    [TestClass]\n    publi"
  },
  {
    "path": "Tests/GlobalUsings.cs",
    "chars": 189,
    "preview": "global using Microsoft.VisualStudio.TestTools.UnitTesting;\nusing System.Diagnostics.CodeAnalysis;\n\n[assembly: Paralleliz"
  },
  {
    "path": "Tests/ProgramTests.cs",
    "chars": 1362,
    "preview": "namespace BitFab.KW1281Test.Tests;\n\n[TestClass]\npublic class ProgramTests\n{\n    [TestMethod]\n    public void ParseAddres"
  },
  {
    "path": "Tests/TesterTests.cs",
    "chars": 1023,
    "preview": "namespace BitFab.KW1281Test.Tests\n{\n    [TestClass]\n    public class TesterTests\n    {\n        [TestMethod]\n        [Dat"
  },
  {
    "path": "Tests/UtilsTests.cs",
    "chars": 883,
    "preview": "using Shouldly;\n\nnamespace BitFab.KW1281Test.Tests;\n\n[TestClass]\npublic class UtilsTests\n{\n    [TestMethod]\n    [DataRo"
  },
  {
    "path": "UnableToProceedException.cs",
    "chars": 107,
    "preview": "using System;\n\nnamespace BitFab.KW1281Test\n{\n    class UnableToProceedException : Exception\n    {\n    }\n}\n"
  },
  {
    "path": "UnexpectedProtocolException.cs",
    "chars": 439,
    "preview": "using System;\n\nnamespace BitFab.KW1281Test\n{\n    [Serializable]\n    internal class UnexpectedProtocolException : Except"
  },
  {
    "path": "Utils.cs",
    "chars": 5007,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Text;\n\nnamespace BitFab.KW1281"
  },
  {
    "path": "kw1281test.slnx",
    "chars": 120,
    "preview": "<Solution>\n  <Project Path=\"kw1281test.csproj\" />\n  <Project Path=\"Tests/BitFab.KW1281Test.Tests.csproj\" />\n</Solution>\n"
  }
]

About this extraction

This page contains the full source code of the gmenounos/kw1281test GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 67 files (295.5 KB), approximately 81.8k tokens, and a symbol index with 458 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!